Finding
A name assigned inside an unobserved: block is readable inside the block and after it, but interrogating it inside the block raises undefined variable. Plain reads and interrogations of the same name disagree about whether it exists.
Reproduction
Found while working #871, at 2148dc2 + that branch (the #871 diff only adds unobserved: depth bookkeeping and cannot affect name resolution, so this is pre-existing).
unobserved:
m is 9.0
m is m * 0.5
print of ("plain read inside: " + (str of m)) # works
print of ("read after block: " + (str of m)) # works
print of ("report after block: " + (report of m)) # works
plain read inside: 4.5
read after block: 4.5
report after block: equilibrium
Move the interrogation inside the block and the same name is undefined:
unobserved:
m is 9.0
print of (report of m)
Error line 3: undefined variable 'm'
3 | print of (report of m)
| ^
at <module> (line 3)
Assigning m before the block makes it work, so it is specific to names whose only assignment is inside the block.
Likely mechanism
AST_UNOBSERVED's compile path (src/compiler.c, the block around the assigned / outside / captured_here / interrogated_here name sets) does escape analysis to decide which names can use the in-place numeric fast path. A name that is assigned inside the block and interrogated inside the block appears not to be routed into the set that keeps it resolvable by the interrogation opcodes — OP_REPORT_NAME then resolves against the env and finds nothing, while the plain-read path uses the slot directly.
Why it matters
The error is loud, which is the good news. But it is loud in a misleading way — "undefined variable" for a variable that is very obviously defined two lines up, and which str of reads happily on the next line. The block is documented as a transparent performance annotation (README.md:189), so a name changing visibility depending on whether you read it or ask about it contradicts that.
This is the same family as #871 (unobserved: changing semantics rather than only speed) but a distinct root: that one is the dynamic depth counter, this one is compile-time name resolution.
Suggested fix
Include interrogated-inside names in whichever set keeps a binding env-resolvable, i.e. treat an interrogation inside the block the same way an interrogation from outside it is already treated.
Finding
A name assigned inside an
unobserved:block is readable inside the block and after it, but interrogating it inside the block raisesundefined variable. Plain reads and interrogations of the same name disagree about whether it exists.Reproduction
Found while working #871, at
2148dc2+ that branch (the #871 diff only addsunobserved:depth bookkeeping and cannot affect name resolution, so this is pre-existing).Move the interrogation inside the block and the same name is undefined:
Assigning
mbefore the block makes it work, so it is specific to names whose only assignment is inside the block.Likely mechanism
AST_UNOBSERVED's compile path (src/compiler.c, the block around theassigned/outside/captured_here/interrogated_herename sets) does escape analysis to decide which names can use the in-place numeric fast path. A name that is assigned inside the block and interrogated inside the block appears not to be routed into the set that keeps it resolvable by the interrogation opcodes —OP_REPORT_NAMEthen resolves against the env and finds nothing, while the plain-read path uses the slot directly.Why it matters
The error is loud, which is the good news. But it is loud in a misleading way — "undefined variable" for a variable that is very obviously defined two lines up, and which
str ofreads happily on the next line. The block is documented as a transparent performance annotation (README.md:189), so a name changing visibility depending on whether you read it or ask about it contradicts that.This is the same family as #871 (
unobserved:changing semantics rather than only speed) but a distinct root: that one is the dynamic depth counter, this one is compile-time name resolution.Suggested fix
Include interrogated-inside names in whichever set keeps a binding env-resolvable, i.e. treat an interrogation inside the block the same way an interrogation from outside it is already treated.