MOOT 2026-08-01 — already fixed, verified directly
Before acting on a "capture everything" decision, tested the ticket's exact
repro (FAM_RO/FAM_WO/FAM_RW/FAM_BIN, the create param, sibling
call) against current master: it compiles and runs correctly, output
matches CPython (r/w+/rb). [[bug-nilpy-nested-def-capture-sets-are-not-final]]
(resolved 2026-07-30, commit 8cae8770b) already implemented full
fixpoint-based transitive capture — the ticket's own literal "option 1" —
for exactly this shape (a lambda/nested-def calling a sibling that captures
a name neither of them reads directly). This ticket's premise was stale by
the time it was worked tonight.
An earlier pass through this ticket (same session) resolved it as "capture everything, naive version of option 1" without checking whether the bug still reproduced first — exactly the "measure before concluding" mistake this project's own debugging discipline warns against. Caught and corrected before any code changed. No action needed; closing as moot.
decide: NilPy transitive capture for sibling nested-def calls
The fork
uforth's build_base_vm() has function-level constants (FAM_RO=1, FAM_WO,
FAM_RW, FAM_BIN) and two sibling nested defs:
def build_base_vm():
FAM_RO = 1; FAM_WO = 2; FAM_RW = 3; FAM_BIN = 0x10
def _file_open_mode(fam, create):
if fam == FAM_RO: ... # captures FAM_RO/WO/RW
def _open_file_common(vm, path, fam, create):
f = open(path, _file_open_mode(fam, create)) # calls the sibling
pxx flattens nested defs into top-level procs with the captured enclosing
locals appended as trailing by-value params, filled at each CALL site by
FindSym. But _open_file_common calls _file_open_mode, which captures
FAM_RO — and FAM_RO is NOT in _open_file_common's own scope (it is a
local of the COMMON PARENT build_base_vm). So the capture fill fails:
nested def captures FAM_RO, which is not in scope at this call.
CPython resolves this by closure chaining: _file_open_mode closes over
build_base_vm's FAM_RO at DEFINITION time, independent of who calls it.
Options
- Transitive capture (recommended): when a nested def A calls sibling B and B captures name X not in A's scope, A transitively captures X too and forwards it. Requires a fixpoint over the call graph within one parent.
- Hoist function-level constants to module globals: a nested def reading an all-caps enclosing constant binds it as a global-scoped value at def time (constants only, never mutated). Narrower, matches the corpus.
- Closure objects for every nested def (the pyeval-closure model already used for value-position defs): snapshot the defining scope. Uniform but a bigger perf/representation change for the common direct-call case.
Recommendation
Option 2 as an immediate unblock (uforth only needs the constant case), Option 1 as the correct general fix. Filed while driving uforth's filetest — the file words (CREATE-FILE/OPEN-FILE) are otherwise implemented (TPyFile, raw syscalls). Blocks the file-word conformance set only.