NilPy: uforth exceptiontest source-unlink test fails under the full driver
Symptom
Running the Forth-2012 exception set under the standard 5-file driver (prelimtest + tester + utilities + errorreport + exceptiontest), our compiled uforth emits one extra failure CPython does not:
WRONG NUMBER OF RESULTS: 333 $$QWEQWEQWERT$$ 334^@13 3 }T \ Test unlinking of sources
(the ^@ is a NUL byte in the payload). Everything else in the set is
byte-identical.
Attribution
NOT from the 2026-07-22 session: reproduced identically with compilers built from fe14f188 (session start), from HEAD, and with the exception-message-text change — all three produce the same extra line. Session 5i's "exceptiontest passes" was under different driver/state (likely without the full 5-file preamble or with different leftover S"-buffer state).
Repro
cd ~/projects/uforth/tests
printf 'S" _drv_x.fth" INCLUDED\nBYE\n' | ./uforth # _drv_x = 5-file driver ending in exceptiontest.fth
Diff against python3 ../uforth.py with the same stdin. The failing test
sits near exceptiontest's "Test unlinking of sources" (CATCH across an
INCLUDED file boundary — source stack unlink during THROW). Suspect area:
uforth's InputSource stack restore interacting with a NilPy gap
(exception unwind across the interpret_include recursion, or the
S"/counted-string circular buffer state), not the THROW/CATCH core (all
other exception tests pass byte-identical).
2026-07-22 narrowing (fable-abcnp): repro reduced; NOT the finally machinery
Standalone repro (no 5-file driver needed):
: T7 S" 333 $$QWEQWEQWERT$$ 334" EVALUATE 335 ;
: T8 S" 222 T7 223" EVALUATE 224 ;
: T9 S" 111 112 T8 113" EVALUATE 114 ;
: C6 CATCH >R DROP DROP R> ;
6 7 ' T9 C6 . . . .
CPython: ForthThrow(-13) propagates, CATCH reports it. pxx: prints
0 224 223 335 and the stack holds ... 222 333 0 334 — the undefined-word
ForthThrow(-13) is swallowed somewhere BELOW CATCH, a 0 lands on the stack
mid-line (some handler's success push), and interpretation RESUMES after the
bad token (334 gets pushed). So this is an exception-unwind swallow inside the
nested-EVALUATE interpret machinery, not an input-source restore bug per se.
Ruled out with NilPy probes (all CPython-identical): single and 3-deep nested try/finally unwind; method-loop unwind through self-calls; except-arm selectivity (except ValueError does NOT catch ForthThrow; the right arm matches). So the swallow needs something in uforth's real call chain — suspects: exec'd PYTHON-word boundary (pyeval frames during unwind), the closure-captured get_in_pos/set_in_pos nested defs, or an except arm reached through the xt/EXECUTE dispatch path.
Debugging note: enabling uforth's TRACE under pxx malfunctions on its own —
if self.trace_filter and ... with trace_filter=None filters pseudo-randomly
(None-variant truthiness / .upper() on a None field?), so the trace harness
can't be used as-is to bisect; that misbehaviour is itself a lead
(None-holding field truthiness in a compiled method).
2026-07-22 RESOLVED (fable-abcnp): two NilPy bugs, neither in the unwind
Root cause of the swallow: return None in a variant-returning def lowered
the nil literal to integer 0, so uforth's _parse_number (annotated
-> Optional[Any]) returned a 0-variant instead of None for a junk token —
exec_token_runtime saw "a number", PUSHED 0 and resumed the line instead of
raising THROW -13. No exception was ever raised, which is why every unwind
probe passed. Fix: return None in a tyVariant-result def now emits a real
None (pynone / VT_EMPTY); non-variant results keep the documented 0/nil
sentinel. (pyparser tkExit branch.)
Side lead confirmed + fixed too: self.f = None / self.f: Optional[str] = None on a str-typed field coerced through variant->string and stored the TEXT
'None' (truthy, is None false) — the trace_filter misbehaviour. None into a
str-typed target now stores the NIL handle (new pylib pystr_none), matching
pystr_is_none and truthiness.
Full 5-file driver (prelim+tester+utilities+errorreport+exceptiontest) now
BYTE-IDENTICAL to CPython (needs STD.UFO/CORE.UFO reachable from cwd —
compiled binary has no file fallback). Regression tests:
test_nilpy_return_none_variant.npy, test_nilpy_none_str_field.npy (wired into
test-nilpy). Also fixed a dash-vs-bash printf \" portability bug in the
bytes_repr Makefile expectation (pre-existing, env-dependent).
Known residual gap (pre-existing, by design of the nil-handle sentinel):
field = "" then field is None answers True (nil handle conflates '' with
None for str-typed slots). Filed only as a note here — uforth does not depend
on the distinction.
Log
- 2026-07-22 — resolved, commit 98ea2c51.