Skip to content

BLOCKER: a comprehension var / catch name creates a SECOND live binding — adding a never-called lambda later in the body changes an earlier line's output #633

Description

@InauguralPhysicist

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_localOP_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

  1. One name → one binding per scope.
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions