Drain at the outermost checkpoint -- FABRIC-3.6.md task 3.4

sk_hermes_drain_checkpoint() interprets one queued payload per checkpoint
(ruled: one message per checkpoint), reusing sk_vm_at_outermost_interpret()
and placed before the switch-signal block in vm_core.c's existing
cooperative checkpoint (sk_vm_context_switch() doesn't return until
switched back to, so drain must come first or it silently never runs on
a switching checkpoint).

Amends FABRIC-3.5.md SXLIII.5, caught by advisor() before writing the
naive version: "recursive drain is prevented for free" via
g_vm_interpret_depth is true but only for same-message re-drain -- it
doesn't cover the separate same-VM reentrancy hazard FABRIC-3.md SXX
already named for Hera specifically (VMCallState saves rsp/exit_colon/
ecw_nesting only, never input_buffer/input_length/input_pos). Draining
calls vm_interpret() on the same vm whose own vm_interpret() call is
still paused mid-word at the checkpoint; without saving and restoring
the cursor by hand, the enclosing REPL line or LOAD block would be
silently truncated. sk_hermes_drain_checkpoint() snapshots and restores
input_buffer/input_length/input_pos/mode/error/abort_requested around
the call. Not a divergence from the ruling -- cursor preservation is the
implementer's own obligation inside the ruled mechanism.

Gated behind a system-wide pending-total counter so the common
no-message-in-flight case costs one integer read per word dispatch, not
a stadium_max_vm_count()-sized queue scan (also flagged by advisor() as
a real hot-path cost, not deferred).

Verified live on all three architectures: a self-test publishes a real
payload to Hermes, proves the depth gate via VM-EXEC-ing an existing
harmless colon word into Hermes (genuine nested vm_interpret(), depth 2,
must not drain), then drains directly from genuinely-outermost context
and confirms exactly one clean drain. dict_hash unmoved and identical
across architectures.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Robert Allan James
2026-09-22 01:27:11 -04:00
co-authored by Claude Sonnet 5
parent 1f6343bc03
commit 2b1ba031a5
9 changed files with 28050 additions and 1 deletions
+37 -1
View File
@@ -862,10 +862,46 @@ ruling]** cannot be written precisely until Captain Bob settles the named sub-it
`dict_hash` for Hermes (`0xc95ef2d92fa0781f`) and Hestia (`0xfbde9fe105fd3b3d`) identical
across all three architectures, unmoved from tasks 3.1/3.2's values (this task touches no
capsule).
- [ ] **3.4** — **Drain at the outermost checkpoint** (§XLIII.3–.5). Reuse
- [x] **3.4** — **Drain at the outermost checkpoint** (§XLIII.3–.5). Reuse
`sk_vm_at_outermost_interpret()`; one message per checkpoint **[needs ruling 3.0e]**.
*Check:* a nested interpret does not drain; a queued payload is interpreted exactly once at
depth 1.
2026-09-22 · `logs/20260922-010622/amd64/`, `logs/20260922-011130/aarch64/`,
`logs/20260922-011648/riscv64/` — all three reach `[zuse@Hera] ok>`, zero `UNKNOWN WORD`,
`mkcapsule --lint capsules/` clean (38 files, 0 violations, unchanged -- no capsule
touched). **Finding, amended into `FABRIC-3.5.md` §XLIII.5** (caught by `advisor()` before
writing the naive version, not found live): §XLIII.5's "recursive drain is prevented for
free" via `g_vm_interpret_depth` is true but covers only same-message re-drain, not the
separate same-VM reentrancy hazard `FABRIC-3.md` §XX had already named for Hera
specifically (`VMCallState` saves `rsp`/`exit_colon`/`ecw_nesting` only, never
`input_buffer`/`input_length`/`input_pos`) -- draining calls `vm_interpret()` on the same
`vm` whose own `vm_interpret()` call is still paused mid-word at the checkpoint, which
would silently truncate the enclosing REPL line or LOAD block if the cursor isn't saved
and restored by hand. `sk_hermes_drain_checkpoint()` (kernel_hermes.c) snapshots
`input_buffer`/`input_length`/`input_pos`/`mode`/`error`/`abort_requested` around the
`vm_interpret()` call and restores all six -- `mode` forced to `MODE_INTERPRET` for the
duration (a checkpoint reached mid-colon-definition must not compile the payload's words
into the enclosing definition); `error`/`abort_requested` restored so a bad message can't
abort the enclosing execution. Placed in `vm_core.c`'s existing cooperative checkpoint
**before** the switch-signal block, not after (`sk_vm_context_switch()` does not return
until something switches back, so a drain placed after it would silently never run on any
checkpoint that switches). Gated behind a system-wide pending-total counter
(`sk_hermes_pending_total`, maintained in `queue_push()`/`sk_hermes_pending_pop()`) so the
common no-message-in-flight case costs one integer read per word dispatch, not a
`stadium_max_vm_count()`-sized queue-table scan -- flagged by `advisor()` as a real
hot-path cost against this project's own +0.0603% measurement floor, not deferred.
Self-test (kernel_main.c) publishes a real, stack-neutral payload (`"1 2 + DROP"`) to
Hermes (a real, already-born VM, not synthetic -- needed a genuine live dictionary),
proves the depth gate via `VM-EXEC`-ing the existing harmless colon word `WELCOME`
(`capsules/hermes/init.4th` block 4855) into Hermes from Hera's context -- a real, already-
proven-safe nested `vm_interpret()` call (`mama_forth_words.c`'s own VM-EXEC mechanism) --
confirming the message does NOT drain at the resulting depth 2, then calls
`sk_hermes_drain_checkpoint()` directly from this self-test's own genuinely-outermost C
context and confirms it DOES drain exactly once, with a further call a clean no-op.
Deliberately avoided the block/`LOAD` mechanism for the nested case -- real but touches
real disk-backed block storage, which a throwaway diagnostic has no business perturbing.
`dict_hash` for Hermes/Hestia unmoved and identical across all three architectures (this
task touches no capsule).
- [ ] **3.5** — **Payload bound and chunking** (B4) **[needs ruling 3.0d]**. Max 1024 bytes per
message; larger payloads sent as ordered chunks, reassembled before drain. *Check:*
1024-byte payload single message; 3000-byte payload chunked and reassembled byte-exact;