Conversation
Sections 1, 2 and 5 of an AST blueprint are a table of 19 types, 113 node kinds and 198 fields. That is transcription, and a hand typed transcription is right the day it is written and wrong the first time upstream adds a field, with nobody finding out because a reader who trusted the table has no reason to check it. So this adds tools/bpc, which reads Parser/Python.asdl with CPython's own Parser/asdl.py and generates those sections. The prose lives in blueprints/sources/BP-AST.md with a one line directive where each block goes, and the expanded document is committed at blueprints/BP-AST.md with markers around each generated part. `just build-blueprints` rebuilds it and `just blueprints` fails if what is committed has drifted. asdl.py parses to a tree with no line numbers on it, so model.py runs its tokenizer a second time and walks the two in step. A definition is found by looking for a type name followed by `=`, which is what keeps `arg` being declared on line 119 apart from `arg` being used as a field type on line 116. Every generated citation points at a single line with the cited name on it, so `just citations` catches upstream moving a definition instead of quietly pointing at whatever moved into that slot. Two facts the generator would have lost, and now does not. asdl.py keeps only the last quantifier in seq and opt, so `expr?* keys` and `expr?* kw_defaults` arrive looking like plain sequences; Field.marks keeps the full list and the tables say "sequence of optional". And attributes split two ways rather than one: end_lineno and end_col_offset default to None like any optional, lineno and col_offset have no value at all and reading one raises AttributeError, with compile being what refuses the tree. The conformance tests compare the pinned grammar against the ast module of the interpreter running them, and section 8 names each one, so a renamed test means a rebuilt document.
This was referenced Aug 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #17.
What this is
BP-ASTspecifies the node vocabulary: 19 types, 113 concrete node kinds, 198 fields, the C representation of all of it, the arena the tree lives in, and the validation pass that rejects trees the grammar allows. Sections 1, 2, 5 and 8 of it are not typed by anybody. They are compiled out ofParser/Python.asdlby a new tool,tools/bpc.The reason is that those sections are transcription rather than specification. A table of every field of every node is right on the day it is typed and wrong the first time upstream adds one, and nobody finds out, because a reader who trusted the table has no reason to check it. Where upstream ships the material in a form a program can read, the specification should be generated from it.
Parser/Python.asdlis the clearest case of that in CPython, and it is the same file CPython generates its own node structs, its C constructors and its Python classes from.How it works
blueprints/sources/BP-AST.mdholds the prose with a one line directive where each generated block belongs:uv run bpc buildswaps each directive for its block and writesblueprints/BP-AST.md. Both files are committed. The output is what people read and whatbpcheckandrefchecklint, and it keeps the boundary visible:That is what makes "no hand written content in a generated section" something anybody can check rather than something everybody has to remember.
just build-blueprintsrebuilds,just blueprintsnow runsbpc checkas well asbpcheck lintand fails if what is committed has drifted. It reports rather than repairs, because a checker that silently fixes what it finds checks nothing, and the diff is the thing somebody is supposed to read before the pin moves.Section 1 is the one exception to "the whole section is generated". Its scope prose is hand written and its table is generated, because where this blueprint stops and
BP-PARSERstarts is not in the ASDL and never will be. The document says that in the section itself rather than leaving it for a reader to work out.Where the line numbers come from
bpcimports CPython'sParser/asdl.pyfrom the pinned checkout rather than parsing the grammar itself. A second ASDL parser would be a second opinion about what the grammar means, and the reason to generate this material at all is that there should be one.What
asdl.pydoes not give back is where anything was written: it parses to a tree ofModule,Type,ConstructorandFieldwith no line numbers anywhere. Somodel.pyrunsasdl.py's own tokenizer a second time, which does carry them, and walks the two in step. A definition is found by looking for a type name followed by=. That is not a detail: a plain forward scan for the firstargtoken lands on line 116, whereargis the type of three fields ofarguments, rather than line 119 where it is declared, and every citation for the type would have pointed at another type's field list. There is a test named after exactly that.The result is 145 generated citations, each pointing at a single line with the cited name on it. If upstream moves a definition,
just citationsfails instead of quietly pointing at whatever moved into that slot.Two things the generator nearly lost
expr?*. Two fields in the grammar carry both quantifiers:Dict.keysandarguments.kw_defaults.asdl.pysetsField.seqandField.optfrom the last quantifier only, so both arrive looking like plain sequences, and the?survives inField.quantifiersand nowhere else. A port that readsseqandopttypes them as lists of expressions and then crashes on the first{**d}, because aNonekey is how dictionary unpacking is written and there is no node for it.Field.markskeeps the full list, the tables say "sequence of optional" for those two rows, and section 6.7 explains what the gaps mean in each.The attributes. They do not behave the way section 5 originally said. Attributes are never required by the constructor, so a node can always be built without them. The two declared
int?,end_linenoandend_col_offset, default toNonelike any other optional. The two declaredint,linenoandcol_offset, have no value at all, and reading one raisesAttributeError. Nothing complains untilcompilesees the tree, which is where a missing line number becomesTypeError: required field "lineno" missing from stmt. That is where INV-AST-008 is actually enforced, and it is not where a reader expects, so the document now says so and two tests hold it up.Same for the field defaults, which were checked against the running interpreter rather than assumed: required raises
TypeErrornaming the field, optional isNone, a sequence is a new empty list per node, andexpr_contextis theLoadsingleton.The conformance tests
tools/bpc/tests/test_bpc_conformance.pyreads the pinned grammar and compares it against theastmodule of the interpreter running the tests. Every type and every constructor is a class,_fieldsand_attributesare the grammar's names in the grammar's order, and leaving a field out does what section 5 says.Section 8 of the blueprint names those tests by their function names, and there is a test in
test_bpc_render.pythat reads the generated section 8 and asserts every function it names exists in the file it says it is in. A conformance section pointing at a test nobody can run is the failure this whole arrangement exists to avoid.They skip when the running interpreter's version does not match the pinned tag, on the same reasoning refcheck already uses: a difference between
v3.15.0rc1and whatever else is installed is a fact about the two versions, not a failure of the document.Also in here
tools/bpc/README.mdexplains the directives, the line number walk and whycheckdoes not repair.blueprints/README.mdgains theBP-ASTrow the index rule requires plus a section on what generating a blueprint section is and is not worth doing for. The rootREADME.mdgains abpcrow and aBP-ASTrow.CI runs
bpc checkin the citations job, which is the one with a CPython checkout, and the tree independentbpctests in the blueprints job.Checks
just checkis green. 1188 passed and 3 skipped, up from 1115 passed and 2 skipped on main, so 73 new tests. 535 citations resolve in 4 roots, up from 352. 3 blueprints lint clean and 1 is up to date against its source.Not in this PR
BP-ASTisStatus: partial, which is honest. Section 3 gives three algorithms and there are 113 constructors; the one shown is the shape they all share. Sections 6 and 9 are written from reading the source rather than from having ported it. Both get revisited whenBP-PARSERandBP-CODEGENland and the boundaries get tested by something other than prose.