The bare-genexpr lookahead ran past the end of the statement
class P:
def __init__(self, n):
self.n = n
p = P(3)
p.extra = 7 # <- the value is parsed through ParseArgExpr
def to_dict(o, names):
out = {}
for nm in names: # pascal26: error: undefined variable (names)
out[nm] = getattr(o, nm)
return out
Found 2026-08-15 while checking neighbours after an unrelated property fix:
test/test_nilpy_getattr_computed_name.npy — a WIRED suite test — no longer
compiled.
Root cause
c003256c0 moved the bare-generator-expression diversion into ParseArgExpr,
which is right: it is the one routine every call argument passes through. But
its lookahead, PyBareGenExprAhead, only stopped at a comma or a closing
bracket at depth 0 — never at a statement boundary.
A dynamic attribute STORE parses its value through ParseArgExpr too, and that
value is not inside any brackets. So the scan left the statement, walked over
the newline, and found the for of an unrelated loop several lines below;
7 was then parsed as a generator expression over it.
The diagnostic pointed at the loop, not the store — "undefined variable
(names)" on a parameter that plainly exists — which is why it read as a
scoping bug in a function forty lines away from the cause. Bisected instead of
reasoned about: d9d3f39f1 clean, c003256c0 red, and the trigger line found
by compiling growing prefixes of the test.
The fix
The scan stops at tkNewline / tkIndent / tkDedent / ; / : at depth 0.
A genexpr argument always sits inside the call's parentheses, and NilPy
suppresses newlines inside brackets, so a newline at depth 0 means the scan has
left the argument list entirely.
Gate
make compiler/pascal26 + tools/gate.sh quick GREEN; pinned v336.
test_nilpy_getattr_computed_name compiles and matches again; the eight
neighbouring genexpr/comprehension/format tests re-checked against their
oracles.
Worth keeping: this is the second lookahead-shaped defect in two days
(PyStarArgAhead's siblings were the first). A token scan that answers "is
construct X ahead" needs a stop condition for the END of the region it is
allowed to look at, and "a bracket or a comma" is not one when the caller may
be outside brackets entirely.