Skip to content

A root-program top-level let is not visible to a closure called after the entry frame has popped #786

Closed

Description

@Masterplanner25

Summary

A closure defined in the root program that reads a module-top-level let reports Undefined variable when it is called after the entry frame has popped — for example from a coroutine a module spawned.

Exposed by #783's fix. Before it, this program failed earlier with Stack underflow, so the binding was never reached.

Reproduction

n_inner.nd:

fn wrap_and_spawn(body) { return spawn(fn() { return body() }) }
import "./n_inner.nd" as m
let TOKEN = "main-let"
fn main() {
    spawn(coroutine(fn(){ m.wrap_and_spawn(fn(){ print(TOKEN) }); return 1i }))
    run_loop()
}
Name error at probe.nd:5:56: Undefined variable: TOKEN

It is about the frame, not the module

call shape result
m.call_now(fn(){ print(TOKEN) }) — frames still alive works (main-let)
m.wrap_and_spawn(fn(){ print(TOKEN) }) — entry frame popped Undefined variable: TOKEN
the same wrap-and-spawn with no module involved works
a top-level fn helper() instead of a let, wrapped and spawned works (main-fn)

The third row rules out spawning as the cause; the fourth rules out name resolution generally — a top-level function resolves fine, because it lives in the chunk's functions table, which #783's fix now supplies correctly. A top-level let does not.

Diagnosis

Measured: the root program's module_globals is empty — TOKEN is not in it, and neither are the module bindings. So a module-top-level let lives in the entry frame rather than in a namespace that outlives it, and once that frame has popped there is nowhere to look.

This is the neighbourhood of #671 (VM.binding_namespace, "where does this name live"), and probably wants deciding alongside it rather than separately.

Why it is filed rather than fixed

#783 answers which chunk a closure was compiled against. This is which bindings that chunk's top level can still see, which is a different question with a different owner. Fixing it by widening #783's context tuple would be wrong: the tuple already carries the correct (empty) namespace — the binding is simply not in any namespace.

Note the asymmetry it creates today, which is the part worth not shipping silently: a wrapped closure can call a root-level fn but cannot read a root-level let.

Affected versions

Reachable from main at the #783 fix. Present but masked before it.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions