grammardoc: render the parse table as a manual - #118
Open
zmaril wants to merge 1 commit into
Open
Conversation
Owning the grammars means owning their documentation, and the honest way to document a parser is to render the parse table rather than describe it. `tools/grammardoc` renders each grammar the way a language reference does: every production as EBNF and as a railroad diagram, plus the precedence table and the vocabulary index. 553 productions across the three grammars, about 70ms each, with no per-language code. It reads src/grammar.json, not grammar.js. grammar.js is arbitrary JavaScript and reading it means running it; grammar.json is what `tree-sitter generate` normalises it into, and is already an EBNF syntax tree over sixteen node kinds. Rendering is therefore a fold over those sixteen cases rather than a parse, and the page cannot drift from the parser -- if a production is on the page, the parse table has it. Two things a BNF listing cannot show are drawn anyway. Precedence goes around the production it applies to as well as into a table, because EBNF cannot express it at all, which is why every language manual prints it separately. Fields appear as captions inside the boxes, so the edge names a query can use sit next to the shape they attach to. Hidden rules are deliberately not inlined. The _or_test -> _and_test -> _not_test chain IS the precedence structure, and it is what a manual shows as expr -> boolean_primary -> predicate -> bit_expr -> simple_expr; inlining it would delete the most informative part of the page. The comma-list idiom is collapsed to a single loop, without which about half the diagrams are unreadable. CI renders every grammar on every change. The renderer is total over grammar.json's node kinds and raises on one it does not know, so a grammar that starts using an unfamiliar DSL construct fails the build instead of quietly dropping part of a production from its documentation. Verified against a synthetic grammar carrying an invented node type. The monospace face is embedded rather than named, for a functional reason rather than a typographic one: the SVG box widths are computed here from a fixed character advance, so a fallback in the browser would clip every label. `--no-fonts` drops it where the face is already available. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ZQR1QTvqArX8vMxYKK3rv
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.
Branches off
main(post-#116), independent of the grammar stack.Live example: Python Rulebook — all 173 productions.
Owning the grammars means owning their documentation, and the honest way to document a parser is to render the parse table rather than describe it.
python3 tools/grammardoc/emit.py crates/treebank-python /tmp/python.html python3 tools/grammardoc/emit.py --check crates/treebank-python # what CI runs553 productions across the three grammars, ~70 ms each, stdlib only, no per-language code.
Why it's small
The input is
src/grammar.json, notgrammar.js.grammar.jsis arbitrary JavaScript and reading it means running it.grammar.jsonis whattree-sitter generatenormalises it into, and it is already an EBNF syntax tree over sixteen node kinds —SEQ,CHOICE,REPEAT,REPEAT1,SYMBOL,STRING,PATTERN,BLANK,FIELD,ALIAS,TOKEN,IMMEDIATE_TOKEN,RESERVED, and the fourPRECforms.So rendering is a fold over sixteen cases, not a parse. And because it reads the generated grammar, the page cannot drift from the parser: if a production is on the page, the parse table has it.
What it shows that a BNF listing can't
Decisions
Hidden rules stay visible. Tempting to inline
_or_testand friends, but that chain is the precedence structure — it is what the MySQL manual shows asexpr → boolean_primary → predicate → bit_expr → simple_expr. Inlining deletes the most informative part of the page.The comma-list idiom is collapsed.
seq(X, repeat(seq(',', X)))is recognised and drawn as one loop instead of five boxes. Without it roughly half the diagrams are unreadable.The mono face is embedded, for a functional reason. SVG box widths are computed in the tool from a fixed character advance (DejaVu Sans Mono, 0.60205 em); a fallback in the browser would clip every label. ~1.3 MB per page,
--no-fontsto drop it.The CI check has teeth
to_rrandto_ebnfare total over grammar.json's node kinds and raise on one they do not know. Verified against a synthetic grammar carrying an invented node type:Without that, a missing case silently omits part of a production from the docs — which nobody would notice.
On the layout engine
railroad.pysizes a node in its constructor and draws it later, and the two must agree. Both bugs found while writing it were achoicesized with one formula and drawn with another, which reads as plausible source and as obvious nonsense in a picture. Offsets are now computed once in__init__and read back bydraw;preview.pyrasterises a diagram offline so a layout change can be checked by looking at it.Follow-ups, deliberately not here
treebank docs <grammar>if the CLI should own it — it is Python today because that matches the existingtools/and oracle scripts.🤖 Generated with Claude Code
https://claude.ai/code/session_015ZQR1QTvqArX8vMxYKK3rv