Finding
In a function scope, a comprehension variable or a catch name binds by name into the env, while a plain assignment of the same name is slot-promoted. The two never meet: the env gets a second i while the slot keeps a stale value.
The consequence is the sharp one: program text after a statement changes that statement's result.
Reproduction
Two programs. The only difference is one line, after the print, defining a lambda that is never called:
A (y1.eigs):
define f as:
i is 100
ys is [i for i in [1, 2]]
print of i
return 0
f of 1
B (y2.eigs) — byte-identical plus g is () => i:
define f as:
i is 100
ys is [i for i in [1, 2]]
print of i
g is () => i # never called
return 0
f of 1
=== A === === B ===
100 2
$ diff y1.eigs y2.eigs
4a5
> g is () => i
Identical under EIGS_JIT_OFF=1 — this is the shared compiler, not a tier divergence.
The double binding, directly
Slot-read and name-read of the same name in the same env disagree:
define f as:
i is 100
ys is [i for i in [1, 2]]
print of i # -> 100 (slot)
print of (eval of "i") # -> 2 (env, by name)
return 0
f of 1
Same via catch:
=== catch name shadow === same + a later lambda
caught caught
100 boom
Which side is right
2 / boom. Module scope already gives the leaking answer consistently, and the intended model says comprehension variables leak to the surrounding scope. The slot read is the stale one.
Root cause
src/compiler.c:2444 (listcomp binder) and src/compiler.c:2492 (catch binder) emit OP_SET_NAME_LOCAL (env, by name). A plain i is 100 in a function body is slot-promoted by emit_assign_for_tos (src/compiler.c:1450-1471, add_local → OP_SET_LOCAL).
g is () => i flips c->captured, which is the only thing suppressing slot promotion — so any closure or interrogative mentioning the name, anywhere in the body, flips which binding the earlier read sees. Silent wrong value, rc=0.
Invariants violated
- One name → one binding per scope.
- Program text after a statement must not change that statement's result.
Fix
The mechanism already exists and is used for exactly this reason. src/compiler.c:1268 deliberately keeps for-loop vars out of slot promotion, with the comment: "promoting it to a frame slot leaves the slot null while writes go to env".
Do the same in the other direction: collect listcomp vars and catch names into a per-scope env-bound name set (alongside c->captured / c->interrogated), so a same-named plain assignment routes to OP_SET_FN_NAME_LOCAL instead of a slot.
Tests to add
The y1/y2 pair and the z1/z2 (catch) pair, asserting A and B agree. Both fail before, pass after.
Finding
In a function scope, a comprehension variable or a
catchname binds by name into the env, while a plain assignment of the same name is slot-promoted. The two never meet: the env gets a secondiwhile the slot keeps a stale value.The consequence is the sharp one: program text after a statement changes that statement's result.
Reproduction
Two programs. The only difference is one line, after the
print, defining a lambda that is never called:A (
y1.eigs):B (
y2.eigs) — byte-identical plusg is () => i:Identical under
EIGS_JIT_OFF=1— this is the shared compiler, not a tier divergence.The double binding, directly
Slot-read and name-read of the same name in the same env disagree:
Same via
catch:Which side is right
2/boom. Module scope already gives the leaking answer consistently, and the intended model says comprehension variables leak to the surrounding scope. The slot read is the stale one.Root cause
src/compiler.c:2444(listcomp binder) andsrc/compiler.c:2492(catch binder) emitOP_SET_NAME_LOCAL(env, by name). A plaini is 100in a function body is slot-promoted byemit_assign_for_tos(src/compiler.c:1450-1471,add_local→OP_SET_LOCAL).g is () => iflipsc->captured, which is the only thing suppressing slot promotion — so any closure or interrogative mentioning the name, anywhere in the body, flips which binding the earlier read sees. Silent wrong value, rc=0.Invariants violated
Fix
The mechanism already exists and is used for exactly this reason.
src/compiler.c:1268deliberately keepsfor-loop vars out of slot promotion, with the comment: "promoting it to a frame slot leaves the slot null while writes go to env".Do the same in the other direction: collect listcomp vars and catch names into a per-scope env-bound name set (alongside
c->captured/c->interrogated), so a same-named plain assignment routes toOP_SET_FN_NAME_LOCALinstead of a slot.Tests to add
The
y1/y2pair and thez1/z2(catch) pair, asserting A and B agree. Both fail before, pass after.