← board

Carve NilPy's selector/subscript parsing out of parser.inc

User, 2026-08-09: "parser support functions — instead of being a solution for multiple languages, as soon as those languages diverge, there is no shame in copying the helper function and make it language specific."

This extends the standing rule (feedback_duplicate_helpers_per_language_shared_ast, 2026-07-20) from RUNTIME helpers to PARSE routines. The shared thing is the AST/IR — not the parser.

Measured

count
PyExprMode guards in compiler/parser.inc 129
isNilPy guards in compiler/parser.inc 67
PyExprMode guards in ParseClassRecordSelectors alone 34

A function with 34 dialect tests is two functions interleaved.

Why it is not cosmetic — two bugs from one day

Both share a shape: the wrong dialect's behaviour is the DEFAULT, and the right one is an arm someone has to remember to add. A split inverts that — a NilPy-only routine cannot accidentally do the Pascal thing, because the Pascal code is not in it.

Scope

Start with ParseClassRecordSelectors — the densest, and the one that produced both bugs. Copy it into pyparser.inc as a NilPy-only selector parser, delete the Pascal arms from the copy and the NilPy arms from the original, and call the copy from the NilPy entry points. NilPy already owns pylexer.inc / pyparser.inc; selector parsing living in parser.inc is the anomaly.

Do NOT attempt all 129 at once. One routine per change, each landing green, with the guard count recorded so the trend is visible. Some guards are genuinely about shared behaviour (a NilPy-only diagnostic on a shared construct) and should stay — the test is whether the two dialects want different SEMANTICS there, not merely different messages.

Note on the Pascal side

parser.inc is Pascal's frontend as well as the shared expression parser, so this is Track A ground and carries the self-host gate. The self-host fixedpoint is a strong oracle for the Pascal half: if the carve-out changes Pascal behaviour at all, the compiler stops reproducing itself. Use that — it makes the Pascal side of each split nearly free to verify, which is the opposite of the NilPy side.

Gate

Per routine: make compiler/pascal26 (the fixedpoint) + tools/gate.sh quick, plus a whole-suite HEAD-vs-pinned .npy sweep — a carve-out is a NARROWING change, and narrowing cannot be regression-tested by the tests that motivated it (that is how the exact-case ctor fix broke aliased constructors the same day and was caught only by the sweep).

Progress — 2026-08-09, split 1 of N: ParseClassRecordSelectors

Done. PyParseClassRecordSelectors now lives in compiler/pyparser.inc; ParseClassRecordSelectors (parser.inc) dispatches to it on PyExprMode and is otherwise the Pascal-only selector parser.

Correction to the measurement in this ticket

The "34 PyExprMode guards in ParseClassRecordSelectors alone" line is wrong — it counts a wider span than the routine. Inside the routine's actual bounds there were nine dialect forks: six PyExprMode, two isNilPy, one NilPyUserCode. Nine forks in one 600-line routine is still exactly the shape the ticket describes, so the conclusion stands; only the number was inflated. Leaving the original line in place above rather than editing it, per the don't-rewrite-history rule.

What moved, and what deliberately did not

Guard trend

before after
forks inside the routine 9 1 (the dispatch)
PyExprMode in parser.inc 129 125
isNilPy in parser.inc 67 67

Gate

Next splits (not done here — one routine per change)

ParseLValueAST is the obvious next target: it is the sibling path to this one (bare-identifier receiver vs chained receiver), and this repo has now been bitten at least three times by teaching one and not the other (project_nilpy_lvalue_vs_selector_path_must_both_know). Splitting it has a second payoff beyond guard count: once both selector paths are NilPy-only routines, the two can be made to share a NilPy-side helper, which is the actual fix for that recurring double-case — not possible while both are entangled with Pascal.

Log