PEGO

106. Dropping What Only Non-Start Rule Entries Reach

  • Status: Implemented
  • Date: 2026-10-22

Problem and Mechanism

A generated typed parser registered, for every rule of the grammar, the method that calls the rule (trules[N].Entry = u<N> or s<N>). The runtime calls an entry only to run a start rule; every other rule is called through the generated methods that name it. The registrations still referenced the methods, so every rule's value-free and standalone methods stayed in the source, including those no call site names because the grammar uses the rule only through its value form or only inside other rules' inlined bodies.

The generator registers an Entry only for the rules that a typed entry point runs (the start rules, which ParseAST and ParseRuleAST name). A first pass records which typed rules the entry points and the printed typed bodies call, and the second pass prints only those rules (record 120): a rule that no call names has no method, scope, class table or term, and the fixed conversion helpers that no typed code names are left out. The Results below measure an earlier form of the mechanism that worked on the printed source: it dropped the registrations and then the unexported functions and package variables that nothing reachable named, ignoring identifiers that the parser resolved to a local variable or parameter, because generated bodies name their locals v<N> and x<N>, the same names as package-level functions.

Results

Bytes of parser.go with -typed-only:

Parser Before After
duckdb 5.12 MB 2.98 MB
typescript 2.56 MB 1.94 MB
python 1.57 MB 1.07 MB
golang 1.43 MB 1.02 MB
cue 1.07 MB 0.74 MB
cel 0.28 MB 0.20 MB

Ignoring local names removes a further share of the source (bytes of parser.go):

Parser Counting local names Ignoring local names
typescript 1,310,067 1,183,066 (−9.7%)
yaml 1,719,403 1,616,471 (−6.0%)
golang 694,523 657,453 (−5.3%)
cel 141,550 136,982 (−3.2%)
duckdb 2,004,595 1,978,082 (−1.3%)
cue 486,676 481,913 (−1.0%)
python 792,174 785,557 (−0.8%)
xml 822,551 818,810 (−0.5%)

The linker already drops these functions, so binaries change by less than 600 bytes and parse speed is within the spread of an identical control.

Test binaries of the parser modules (go test -c -trimpath), which the linker already pruned of unreferenced functions only where nothing registered them:

Parser Before After
duckdb 22.3 MB 18.1 MB
typescript 13.4 MB 11.9 MB
python 14.1 MB 13.4 MB
golang 13.7 MB 12.7 MB
cue 12.6 MB 12.1 MB
cel 10.5 MB 10.3 MB

The removed functions are not called by any parse. Language-module benchmarks (six alternating rounds) change by +0.5% to +2.3% in the geometric mean, the unchanged yaml parser by +2.2%, so they are within noise; the ./bench generated parsers show +0.35%. Cold-compile times of one run each were within the noise of a shared machine and are not reported.

Correctness and Tradeoffs

The engine tests check that the typed output sets one Entry (the start rule's), that it leaves out the methods of a rule no start rule reaches, that a callee the first pass left out fails generation, and that the parser with the unreached rules left out returns what the one that keeps them returns. The engine tests, the root suite and parsers/test.sh pass.

Decision and Limits

Adopted for Go parsers with typed values. The runtime uses an entry only to enter a start rule, so only start rules keep theirs; every other rule is called through its body, which p.Call reaches.