← board

NilPy corpus: uforth — a real Python Forth system as Track N's forcing target

RECHECK 2026-07-31 — the line-640 tick/EXECUTE wall is CLEAR; a new wall found further in

Isolated the exact tick/EXECUTE repro this ticket's log names as the next wall (vm.dict.get(name.upper()) -> a Word object -> push() -> isinstance(value, Word) -> value.xt_id) and it now matches CPython exactly — this specific wall is gone (landed as a side effect of other session work, not attributed to one commit).

Compiled the real /home/rene/projects/uforth/uforth.py directly (not just the isolated repro) to see how far the corpus actually gets now: reaches line 3887 before erroring expected newline after statement (context: res_addr res_addr length — needs its own isolated repro to pin down, not done this pass). Also surfaced, not fatal but worth noting: two NIL PYTHON warnings that a nested def (w_traverse_wordlist, w_name_to_string) captures MORE THAN 12 enclosing values and so "cannot carry them as a value... will read garbage" if called after the enclosing call returns — this is the SAME closure-capture-limit family already tracked elsewhere this session (closure-ABI representation gaps), not a new bug, but a concrete data point on where the corpus actually presses on it.

Not chased further this pass: a real corpus drive is a "fix one wall, recompile, hit the next" campaign by nature (as this ticket's own log already shows across multiple prior sessions) — the line-3887 wall is the next actionable item for whoever picks this up next, isolate it into its own minimal repro first per this ticket's own established practice.

Attempted to isolate line 3887 directly and ruled out the obvious suspects: a bytes-slice assignment with EXPRESSION bounds (mem[res_addr:res_addr+length] = mem[tib_base+start:tib_base+pos]), a hex-literal slice bound (mem[0x0100:0x0100+8]), and int(x).to_bytes(8, 'little', signed=True) (a keyword argument on a chained method call) ALL compile fine in isolation, matching CPython (one repr difference noted in passing: pxx prints a bytearray as b'...', CPython as bytearray(b'...') — a separate, minor, non-blocking divergence). So the actual trigger is something else nearby in the real file that a hand-assembled repro didn't reproduce — narrows the search but does not close it; whoever picks this up next should bisect the REAL file (comment it out top-down, or binary-search the token range) rather than guess at another synthetic repro.

MILESTONE 2026-07-21 (session 5g): DO/LOOP runs, prelim suite BYTE-IDENTICAL to CPython; core.fr arithmetic green

Full tier-2 promotable-int adoption landed (commits f058b95b + e2eb2ade; the promo ticket bug-nilpy-wide-int-literal-and-unsigned-mask-not-promoted is RESOLVED — read its final notes for the design). Results:

NEXT WALL (line 640): tick/EXECUTE xt handling. T{ ' GT1 EXECUTE -> 123 }T dies with TypeError: expected a number, got str. Repro: : GT1 123 ; ' GT1 . — CPython prints the int xt (369); ours ends with a STRING cell on the stack. Tick is a PYTHON word (CORE.UFO:274): w = vm.dict.get(name.upper()); vm.push(w) pushes a Word OBJECT; compiled push() must hit isinstance(value, Word)value = value.xt_id. Somewhere that chain yields the name string instead — object-identity / isinstance-on-boxed-object territory (NOT arithmetic). Isolated S" XY"/C@ tests pass standalone. Full runtests.fth hangs if run naively (buffered output; run core.fr via a driver as in tests/_t3.fth pattern — note stray tests/_t*.fth/_c.fr are scratch, not checked in).

Landmines added this session (details in memory project_promotable_int_stages123 + the resolved promo ticket): Low(Int64) is a bug FAMILY (StrInt print, BToNative demote, pyeval abs/neg/~, PyIMul div-probe SIGFPE, idiv Low//-1); heap-promo variants now survive TPyList/TPyDict slots (PyVarSlotSet refcounts tag 8193); promo->variant stores must be statement-marked or arg-boxing temps stay VT_EMPTY.

Why this target

uforth = 4344-line single-file Python Forth VM (uforth.py) + layered .UFO stdlib + the bundled Forth-2012 conformance suite as a ready-made oracle. Self-contained (os/sys/select/textwrap/itertools/dataclasses only), heavy on real data structures (heterogeneous Any stacks, dicts of words, byte memory, isinstance dispatch) — deliberately chosen to DRIVE NilPy feature development (the P-corpus role fpjson played). Goal: uforth runs UNMODIFIED under pxx-NilPy, suite output matching CPython's.

MILESTONE 2026-07-21: uforth.py COMPILES end to end

The full 4357-line uforth.py now compiles under pxx-NilPy to an ~858KB binary, and the interpreter loop RUNS (reaches uforth-level runtime behaviour — e.g. a uforth TypeError from the running VM, not a compiler crash). The wall went 267 -> full-parse -> compiled-binary across the 2026-07-21 session (~15 commits, ~30 NilPy features + one Track A IR fix). See "Wall progression" below.

Next phase = uforth RUNS CORRECTLY (in progress). The runtime-correctness lane fixed a chain of bugs this session and now gets uforth through construction, banner, REPL, line-read and tokenization:

MILESTONE 2 (2026-07-21, session 2): uforth EXECUTES native words without crashing. 1 2 + . runs the tokenizer, dispatch loop, exec_token_runtime, and the + / . native words to completion (exit 0). The tokenize SIGSEGV is FIXED and a chain of runtime bugs behind it:

REMAINING: . prints its trailing space but not the number value — a further runtime bug (number push/pop or the . word's cell read). Then [[feature-lib-pyexec]] for the PYTHON native-word blocks. The dispatch/exec substrate is now live, so these are incremental runtime-correctness items.

MILESTONE 3 (2026-07-21, session 3): 1 2 + . PRINTS 3, and the FULL STD.UFO stdlib LOADS (CORE/IO/FLOAT/DEBUG/VARIABLE/MEMORY/RSTACK/EXTRA/STRING/MATH all INCLUDEd, banner shown, no error). A chain of nine silent variant/static-type boundary bugs fixed — every one was a wrong-VALUE or hard-abort in the compiled path that CPython never hit:

All nine committed green (testmgr quick + self-host byte-identical), pushed.

NEXT / CRITICAL PATH = [[feature-lib-pyexec]]. Running a PYTHON-bodied word (/, 2/, and much of CORE) now SEGFAULTS: exec_python_inline does exec(wrapper, env, ns); ns["__body__"](), but exec() is the no-op stub (pylib.pas ~2787), so ns stays empty and ns["__body__"]() calls a non-existent function → crash. 1 2 + . works because +/./native words are real Pascal; the stdlib's Python-bodied words do not. exec() (parse-once-cached AST → tree-walker over the tiny pop/push/arith/ternary subset uforth uses) is the subsystem that unblocks correct execution of the stdlib and the conformance oracle. This is the forcing function's next lane.

Landed this session toward compile: captured-class identity in nested defs, dict comprehensions + dict(), call-result subscript-assign, os/sys/select/stdin shims, TPyFile stubs, list.pop(i), except tuples, del/assign list slices, zip(), getattr-checks-dynamic-store + the variant-ternary IR fix, unpack-from-variant, file/name, split(sep,max), nonlocal, and nested defs inside methods (the last unlock — flush_current's forward).

Architecture decisions (settled 2026-07-19 session)

Measured feature census (what N must grow)

uforth.py: 12 @dataclass (+field), @property/setters, 39 f-strings, 17 comprehensions, 9 generator expressions, 123 slice uses, dicts/sets/tuples throughout, nonlocal, del, exception payloads (ForthThrow), List[Any] variant stacks, isinstance dispatch, select.select stdin polling (KEY — needs a PAL primitive), file IO. PYTHON blocks (134, all statically parseable): imperative subset only — if/while/for, def, calls, subscripts, slices, f-strings, raise, isinstance, augassign.

Milestone ladder (each lands green independently)

  1. Oracle green — DONE (2026-07-19, uforth commit 9f9b45a): full suite cd tests && python3 ../uforth.py runtests.fth under CPython reports Total 0 errors across all 12 word sets (Core, Core ext, Block, Double, Exception, Facility, File-access, Locals, Memory-alloc, Programming-tools, Search-order, String). The 9f9b45a lexer fix (standalone-\ token) closed both the core.fr:429 red and the utilities.fth \? wall. This CPython run is the byte-diff oracle for milestones 1-3.

  2. N features by need, ranked by what blocks uforth.py's PARSE first (dataclass, dict, slice, f-string, ...) — each = own N ticket hung off this umbrella; CPython remains the oracle for every increment. Progress 2026-07-19 (session 2): landed green, in wall order — lexer literals (hex/oct/bin/_ , triple-quoted, line counting); def/method params past 4 (internal ABI: 6 regs, >6 all-stack); @dataclass v1 (scalar fields, defaults, synthesized ctor; + fixed two latent field bugs: bool-spill width clobber, str-field string[N] semantics); bitwise ops + augassign (Python precedence chain, boolean guard) with a Track A PyExprMode tkShl/tkXor suppression; / = true division; from-imports + annotated assignments + module-scope inference fixes; annotation grammar (Optional/Callable/Any/forward refs; Optional[int] None==0 sentinel CAVEAT); stdlib imports consume-and-defer (sys/os/textwrap/select/itertools); list v1 — pylib TPyList builtin (variant slots, default indexed property, len()), [..] literal desugar, plus Track A: scalar->Variant call boxing, Variant-returning functions (hidden-dest ABI), EmitWriteVariant True/False (x86-64 only — cross variant writers still print ints); field(default_factory=list). Landed since: classvar/counter/lambda-factory chain (Word.xt_id), is/is-not, in/not-in + {set} literals + class inheritance headers, rich def-param annotations (Any params by-ref const, Any returns via hidden dest), isinstance() (VT_OBJECT boxing + RTTI for .npy + ctor calls in expression positions). Progress 2026-07-19 (session 3, overnight): landed green, in wall order — AST-based local typing ([[feature-n-nilpy-ast-based-typing]], which also gave methods the widening they never had); ctor fields via the annotated self.x: T = ... form plus richer initialisers, with real line numbers on the pre-pass diagnostics; dict v1 ([[feature-nilpy-dict]] — TPyDict, literals, subscript, .get, in, del); set v1 (TPyList-backed, set() and dedup add); method parameters taking the full annotation grammar; -> None on non-ctor methods; constant parameter defaults on defs, methods and ctors (the ctor path was a segfault); f-strings ([[feature-nilpy-fstrings]] — plain holes, escapes, !r/!s).

    Current wall picture. The corpus is now blocked mostly on TRACK A, not on frontend syntax:

    • [[bug-a-nilpy-variant-element-not-usable-as-scalar]] (p85) — return xs[0] is silent garbage. List-wide, predates dict, and it is the one that matters most: uforth reads from vm.stack / vm.dict / vm.xt_table on almost every line. Now the binding constraint on for-in too: the loop variable is a variant, so for v in xs: acc = acc + v — iterate a list of strings, build a string, the most ordinary loop there is — produces GARBAGE BYTES. for-in landed usable for printing and passing along, not for consuming.
    • [[bug-a-str-boxed-into-variant-does-not-own-bytes]] (p80) — a str boxed into a variant has frame lifetime, so same-length keys collide.
    • [[bug-nilpy-method-returning-str-garbage]] — VM has many -> str methods; per the user's 2026-07-19 call this waits for [[feature-a-abi-oracle]] rather than a ninth copy of the return rule.
    • [[feature-rtti-field-reflection]] — t.word.name on an Any (uforth.py:190).
    • [[bug-nilpy-string-local-truncates-at-255]] (p65) — a string local is the FROZEN kind, so anything built past 255 characters is silently cut. uforth's token buffers and assembled output lines go well past that. Gated on the same Track A boxing bug: making string locals managed is what corrupted the heap when it was tried.

    Also landed session 3, after the wall picture above was written: f-string conversions and format specs (the whole 67-hole census); class-returning top-level defs keeping their class identity (was silent garbage); top-level def SIGNATURES registered up front, which also makes FORWARD CALLS work; module-level names typed from the AST; for-in over list / set / dict / str, and break / continue, which had never been parsed at all — not even inside a plain while.

    Still frontend, still ours, and independent of the Track A blockers: [[feature-nilpy-bytes-and-slices]] (bytearray + slices + to_bytes; 99 slice sites, and vm.memory IS the Forth data space) — but note both slices and int.to_bytes(..., signed=True) need a shared-parser hook, so that one is not purely ours after all.

    Sweeping NilPy's operators and builtins against CPython (2026-07-20) turned up more than the feature work did — three SILENT wrong answers and a segfault that no feature test would have found, because they are in constructs nobody thought to re-check: not <non-zero int> was the BITWISE complement (fixed, 8f18dba4), in on a string segfaulted (fixed, c49064af), int("42") returns a pointer ([[bug-a-nilpy-int-of-string-returns-a-pointer]], Track A), and s * 2 returns a pointer ([[bug-nilpy-string-repeat-returns-a-pointer]]). Worth repeating the sweep after each feature wave.

    Biggest newly-measured gap: [[feature-nilpy-nested-defs]] — 214 sites. The word-registration functions define their natives inline, so this is the structure of uforth's second half, not an occasional idiom.

    The rest of the census is now characterised and filed rather than left as a list: [[feature-nilpy-exceptions]] (p60 — raise / try / except have NO statement rule at all, and exceptions are uforth's control flow, not its error path), [[feature-nilpy-tuple-unpack]] (p55 — the enabler for for k, v in d.items()), and, still unfiled because neither blocks as early: @property (only @dataclass is accepted as a decorator), comprehensions, nonlocal (1 site), and lambda as a VALUE — 3 sites, all vm.define_word("X", native=lambda vm: ...), which is a real closure passed as a Callable parameter rather than the dataclass default-factory form already handled. Small in count, not small in scope.

  3. [[feature-rtti-field-reflection]] + [[feature-lib-pyexec]] (walker) — independently tested against the 134-block corpus extracted standalone.

  4. uforth boots STD/CORE.UFO, then prelim tests (57), then full suite, under pxx-NilPy — byte-diff vs CPython run.

  5. (Optimization, later) pyexec JIT engine via the in-tree obj/asmcore backend.

Non-goals

Session 2026-07-21 (session 3, continued) — 267 -> ~3830 (~88%)

An extended run took the first parse error from 1290 (the exec boundary) to ~3830 (88% of the file), landing (each green, gated, pushed): dynamic call through a variant callable, class-in-variant widen + subscript/slice, decode keyword arg, ternary/genexpr as method/call arguments, and/or returning the operand (Python value semantics), range() with a step, lambda parse-stub, optional parameter annotations, user-class-over-container variant dispatch, method-call ';' statements, bytes literals + bytes.find, str-method chaining after a class-value method, and DYNAMIC INSTANCE ATTRIBUTES (get/set/+=/hasattr on variants and class instances, via a pointer-keyed store). ~40 commits.

Current wall uforth.py:3829 — a closure-captured parameter default. Remaining: [[feature-nilpy-closure-default-and-remaining]] then [[feature-lib-pyexec]] for actual execution. The file parses ~88% of the way; exec() is a stub, so native words do not run yet.

Wall progression — session 2026-07-20 (session 3)

uforth.py's first parse error, tracked as each gap closed. Each step is a landed, gated, pushed commit with a CPython-diffed test in test-nilpy:

wall what blocked resolved by
267 bytearray (session 2)
271 slice assign mem[a:b] = ... [[feature-nilpy-bytes-and-slices]]
271 to_bytes keyword arg same ticket, second half
308 print(..., file=sys.stderr, flush=True) [[feature-nilpy-print-kwargs]]
311 os.path.* os/sys shim table
331 one-line suite if not t: return None PyParseSuite
359 int(s, base) + except ValueError [[feature-nilpy-builtin-exceptions]]
362 conditional expression a if c else b same commit (32 sites)
373 res = None then res = <int> [[bug-nilpy-none-assign-to-plain-local]]
377 float(token) float() conversion, raises ValueError
384 @property / @x.setter property decorator, via AddUProperty
428 variant bitwise v & mask EmitVarBinOp bitwise ops
431 RuntimeError pylib exception tree
458 keyword args in Word(...) ctor keyword binding by FIELD index
460 dict.setdefault pylib
471 ambiguous .get on a variant renamed TPyList/TPyBytes .get -> .at
476 statement after a for in a def [[bug-nilpy-statement-after-for-in-a-def]]
494 hasattr / getattr resolved against declared fields
516 xs.append(a + b) expression arg to a const (by-ref) param
538 x in ("a","b") tuple on the in RHS
585 .find(sub, start) pylib
766 def without -> ret [[feature-nilpy-optional-return-annotation]]
772 for p in (tuple) tuple iteration
774 return (a, b) tuples as values + return-type inference
777 bytes annotation maps to TPyBytes
778 bytearray() zero-arg overload
783 out.extend(ch.encode(...)) TPyBytes.extend + str.encode
789 out.append(ord(ch)) TPyBytes.append (+ the list/bytes name collision)
840 word.is_native() after Optional[Word] full annotation grammar for RETURN types
841 word.native(self) — procedural FIELD on a class local @dataclass Callable field keeps its signature
849 pfx, content = cs unpack a single sequence value into several names
883 isinstance(num, list) container type names -> pylib classes
884 for n in num (num is Any) unbox variant to TPyList
1039 multi-line self.trace_log(...,) trailing comma in a method call
1079 four adjacent f-strings implicit string concatenation
1228 with open(...) + file iteration with/open + pyopen + list comprehensions
1263 raise X(...) from e exception chaining (from-clause dropped)
1272 dict.pop(k, default) pylib
1284 textwrap.dedent stdlib shim
1286 join(EXPR for x in it) genexpr arg [[feature-nilpy-generator-expression-arg]] (OPEN)
1286 join(EXPR for x in it) genexpr arg expression comprehensions + hoisting
1289 exec(wrapper, env, ns) compiles via pylib stub; real engine = [[feature-lib-pyexec]]
1290 r = ns["__body__"]() CURRENT — a DYNAMIC call through a variant callable (the function exec created); needs the dynamic-dispatch machinery that is part of [[feature-lib-pyexec]]

Wall moved 267 -> 1290 on 2026-07-20/21 (session 3), across ~48 landed commits. From 1290 on the code is the exec EXECUTION MODEL: ns["__body__"]() calls the function exec() was meant to build, so it cannot be meaningfully compiled without the pyexec evaluator (a synthetic dynamic-call that crashes at runtime under the stub is not real progress). Remaining VM methods past the exec block (1294+) are independent, but reaching them means compiling a call through a variant callable first. This is the [[feature-lib-pyexec]] boundary: the file parses through exec, and running the exec'd PYTHON blocks (uforth's native words) is the next subsystem.

— roughly the first 30% of the 4357-line file, and past every class definition, the VM core, the dictionary, number parsing, the tokenizer, and file I/O (with/open, comprehensions, strip(chars), raise-from, dict.pop, dedent). The next wall is exec() — the pyexec interpreter library, a genuine multi-session subsystem.

Wall moved 267 -> 1228 on 2026-07-20 (session 3), across ~30 landed commits — roughly the first quarter of the 4357-line file, and past every class definition, the VM core, the dictionary, number parsing, and the tokenizer. The remaining walls are large feature CLUSTERS (file I/O, comprehensions, generators, sys.stdin+select), each multiple sessions.

Bugs found UNDER this work, all silent-wrong-behaviour rather than parse errors, all filed and fixed: [[bug-nilpy-call-returning-class-loses-identity]] (a call returning a class dropped which class — len() on a bytearray field segfaulted) and [[bug-nilpy-not-on-string-always-true]] (not s was True for every string; if s: and bool(s) were both correct, which hid it).

Measured remaining scope (2026-07-20)

Counted in uforth.py, so this is the real distance to milestone 2, not an estimate. Roughly in wall order:

construct sites note
f-strings 42 expander exists; needs verifying against these
@property + setter 2 (x2 accessors) the current wall
decorators 15 @property/setters beyond the landed @dataclass
getattr(o, "n", default) 16 needs [[feature-rtti-field-reflection]]
comprehensions 4 list comprehensions
lambda 4
del 4
select.select on stdin 2 needs a PAL primitive AND a stdin file object
with 1
nonlocal 1
builtin exception classes 6 the current wall

sys.stdin is the awkward one: 8 sites needing .isatty(), .readline(), .read(1) and membership in select.select([...]) — i.e. a real file-object model, not another shim function. Worth its own ticket when reached.

MILESTONE 2026-07-21 (session 4): exec() PYTHON words run; wall = bound-method values

The [[feature-lib-pyexec]] critical path is CLOSED end to end. Landed this session:

NEW WALL = bound-method values ([[feature-nilpy-bound-method-value]]). uforth builds env = {"vm": self, "push": self.push, "pop": self.pop, ...} and env = {"push": vm.push} SIGSEGVs at construction — capturing obj.method (no call) as a dict value is unimplemented; NilPy reads it as a bogus field. This is why 10 3 / (a PYTHON-bodied word) still cores while 1 2 + . (native) works: the crash is uforth's env construction, NOT exec/floordiv (those now run — proven by driving the exact / body with an env of {"vm": vm} only, which pyeval resolves via g["vm"].method and evaluates correctly). pyeval never actually calls env["push"] (it intercepts push/pop through g["vm"]), so the fix is: NilPy must represent obj.method as a callable variant ({recv, method-ref}) without crashing. That unlocks the whole PYTHON-bodied stdlib.

MILESTONE 2026-07-21 (session 5): colon words RUN; STD load stops after CORE

Two concrete findings driving the conformance suite:

FIXED (commit 68b6abc6): colon-word execution. Every colon word that called a sub-word SIGSEGV'd. Root: if w.forth_body: in uforth's inner interpreter (run_forth_word) — Word.forth_body is Optional[List], None for native words. NilPy's PyMakeTruthy emitted forth_body.count() != 0 with NO nil-check, so a None field derefed null. Now nil-guarded: (x <> nil) ? (count<>0) : false. A general NilPy bug (if optionalContainer:), fixed for all. Colon words now run: : SQ DUP * ; 7 SQ . = 49, and every native/PYTHON word chained through them.

NEXT BLOCKER: STD.UFO loads ONLY CORE.UFO. The whole time, only CORE.UFO's words were defined — IO/FLOAT/DEBUG/VARIABLE/MEMORY/RSTACK/EXTRA/STRING/MATH never loaded. That is why +!, >IN, VARIABLE, BL, UM* etc. all THROW -13 (undefined word), and why the conformance prelimtest dies at Pass #1 (0 >IN +!). Instrumented: STD.UFO runs "CORE.UFO" INCLUDE (completes), then its line-loop never reaches line 2 ("IO.UFO" INCLUDE). The resync at interpret_file's loop (uforth.py ~1265: if self.current_source is not before_source ...) sees the source as CHANGED after CORE's nested interpret_file returns, and breaks (current_source.kind != "file"). So the outer STD source is not correctly restored across a nested INCLUDE. Suspect: NilPy class-instance IDENTITY (is) after the InputSource round-trips through the _snapshot_input_state() dict, or the dict-stored class value not restoring as the same object. _restore_input_state did not even appear to be invoked in the last trace — needs confirming whether interpret_file's finally runs restore under NilPy. Fixing this loads the ENTIRE stdlib in one shot → unblocks +!/VARIABLE/>IN/memory words and most of the suite. This is the single highest-leverage next hunt.

2026-07-21 (session 5c): STD multi-file INCLUDE FIXED — 5 files load, next=dynamic attrs

FIXED (commit 58d9a422): STD.UFO now loads CORE→IO→FLOAT→DEBUG→VARIABLE (was ONLY CORE). Root: storing a VARIANT into a tyClass slot didn't unbox — the raw 16-byte variant overwrote the 8-byte class pointer. interpret_file snapshots the input state into a dict and restores self.current_source = snap["current_source"] (a class instance) in a nested INCLUDE's finally; the round-trip lost object identity, so after CORE's INCLUDE the outer STD source read is not before_source and the line-loop broke. Fix: PyUnboxVariantToClass wraps a variant→tyClass store as pyvarobj(v) + AN_CLASS_CAST (pyparser.inc, attribute-assign site). General NilPy correctness fix.

NEXT BLOCKER: pyeval lacks DYNAMIC ATTRIBUTES. Load halts at VARIABLE.UFO:352 TRUE CONSTANT <TRUE> (a top-level stmt) → runs CONSTANT→ENSURE-VARS whose PYTHON body is if not hasattr(vm, 'vars'): vm.vars = {}. vars is NOT a declared VM field — it's a pure dynamic attribute (uforth sets it lazily; also _trans_ptr etc). pyeval's attribute get/set uses GetFieldPtr (declared fields only) → "pyeval: object has no attribute vars". Fix = pyeval attribute access falls back to pydynattr_get/set/has (already in pylib for NilPy) when the name isn't a declared field: vm.vars read → pydynattr_get, vm.vars = x write → pydynattr_set, hasattr(vm,'vars') → declared-or-pydynattr_has. Bounded pyeval M2 addition; unblocks VARIABLE/CONSTANT/arrays and the rest of the stdlib load.

2026-07-21 (session 5d): dynamic attrs FIXED — next=closure-as-native-word

FIXED (commit 6bbf3f3f): pyeval dynamic attributes. PyFieldGet/PyFieldSet (pyeval.pas ~566/597) Halt(1)'d on any name GetFieldPtr couldn't resolve; now a nil GetFieldPtr routes to pydynattr_get / pydynattr_set — the same pointer-keyed store (addr:name) the NilPy frontend uses, so pyeval reads see frontend writes and vice versa. hasattr already routed to pydynattr_has. vm.vars, vm._trans_ptr etc. now work; STD load passes VARIABLE.UFO:352. NilPy-path only; self-host byte-identical, pyeval standalone + test-nilpy + quick green.

2026-07-21 (session 5e): closure-as-native-word LANDED; STD fully loads; suite RUNS to Pass #21

MILESTONE: uforth's STD.UFO fully loads and the conformance prelim suite RUNS, matching the CPython oracle byte-for-byte through Pass #21 of 23. Landed this session (all gated, pushed):

Verify targets ALL pass: VARIABLE Q 42 Q ! Q @ .=42, 100 8192 ! 5 8192 +! 8192 @ .=105, BL .=32. 1 2 + .=3, 10 3 / .=3. make test-uforth green.

NEXT BLOCKER (hard, architectural) = DO/LOOP needs promotable-int/bignum. The prelim suite hangs at the first DO/LOOP (Pass #22 region). uforth's _loop_crossed uses (idx-limit) & 0xFFFFFFFFFFFFFFFF + an UNSIGNED compare; NilPy's 64-bit ints make the mask a no-op and the compare signed → infinite loop. Wide int literals (0xFFFF..., 18446744073709551615), 1<<64, and additive overflow all wrap to 64-bit instead of promoting. This is the [[decide-nilpy-bigint-vs-64bit-cells]] decision (RESOLVED: promotable-int) whose [[feature-a-promotable-int]] is done for stages 1-3 but does NOT yet cover these paths. Filed as [[bug-nilpy-wide-int-literal-and-unsigned-mask-not-promoted]] (Track A). Completing it unblocks DO/LOOP → the rest of the suite.

Earlier this session (5d): closure-as-native-word BLOCKER

NEXT BLOCKER = closure-as-native-word ([[feature-pyeval-closure-as-native-word]]). Load now halts at VARIABLE.UFO:30: the word body does def _w(vm2): vm2.push(name) then vm.define_word(name, native=_w) — a pyeval-internal nested def passed as a VALUE to a host method, called back much later as word.native(vm2) (NilPy PyMakeDynCall). Two parts: (1) pyeval EnvGet must resolve a bare def name to a callable value (today pyeval: name not defined: _w); (2) the closure must be snapshotted (body + captured name) into a persistent stateful callable variant, and PyMakeDynCall gains a tag-branch prepending the closure state — same reverse-bridge gap as VT_BOUNDMETHOD. Direction set by [[decide-nilpy-closure-model]]. Backs VARIABLE/CONSTANT/CREATE/arrays → gates the memory-word half of the conformance suite. Full scope in the new ticket.