← board

A module-level name bound inside a block cannot be READ by a later top-level assignment

Minimal

if True:
    payload = 3
documents = payload + 1     # pascal26:3: error: undefined variable (payload)
print(documents)

Three lines. It is not about with, not about json, and not about the module loader — any nested block does it.

What separates the working case from the broken one

Measured, one variable at a time:

module-level shape result
if True: payload = 3 then print(payload) ok — prints 3
if True: payload = 3 then documents = payload + 1 error: undefined variable (payload)
for i in [1]: payload = 3 then documents = payload + 1 error
with p.open(...) as f: payload = json.load(f) then documents = payload.get(...) error
payload = 0 first, then the same block and assignment ok
all of it inside a def ok

So the name IS bound — an expression statement reads it fine. What fails is reading it on the right-hand side of another top-level ASSIGNMENT. The two statement forms are evidently resolving against different views of module scope: the assignment path only sees names already bound at the top level, while the expression path sees the names bound inside blocks too.

A pre-declaration at top level (payload = 0) makes it work, which is the workaround — and exactly the app-side edit the compile-real-world-code mission forbids.

Why it matters

This is the ordinary shape of a Python entry point:

with SESSION_FILE.open("r", encoding="utf-8") as file:
    payload = json.load(file)
documents = payload.get("documents", [])

It is how songformatter's SongFormatter.py loads its session, and it is what made a Track B probe of that file fail to compile even though the module itself compiles (there the code sits inside def load_session(), which is the working case — so the bug hides until someone lifts the same lines to module level).

At least it is LOUD: a compile error, not a wrong value.

Where to look

The module-level statement path in compiler/pyparser.inc — specifically whatever set of bound names an assignment statement's RHS resolves against, versus the set an expression statement resolves against. The asymmetry is the bug; whichever view is the fuller one is presumably the right answer for both.

Gate

make test-nilpy green + self-host byte-identical, plus a .npy covering the table above (each of if / for / with, read from both an expression statement and an assignment RHS) diffed against CPython.

Log