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:
co-authored by
Claude Sonnet 5
parent
1f6343bc03
commit
2b1ba031a5
+37
-1
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user