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
@@ -451,6 +451,65 @@ SkHermesMessage *sk_hermes_pending_peek(VMUuid vm_id);
|
||||
* vm_id has no queue or an empty one. */
|
||||
int sk_hermes_pending_pop(VMUuid vm_id);
|
||||
|
||||
/* Forward declaration only -- kernel_hermes.h deliberately does not
|
||||
* include vm.h (kept decoupled from the full VM struct, same posture as
|
||||
* every other type in this file), but sk_hermes_drain_checkpoint() below
|
||||
* needs a VM* parameter. Matches vm.h's own `typedef struct VM VM;`
|
||||
* shape exactly, so no redefinition conflict. */
|
||||
typedef struct VM VM;
|
||||
|
||||
/*
|
||||
* Drain at the outermost checkpoint (FABRIC-3.6.md task 3.4, SXLIII.3-.5;
|
||||
* one message per checkpoint, ruled 2026-09-21). Called from
|
||||
* execute_colon_word()'s existing cooperative checkpoint in vm_core.c,
|
||||
* gated the same way the switch-signal checkpoint already is
|
||||
* (sk_vm_at_outermost_interpret()).
|
||||
*
|
||||
* AMENDS SXLIII.5's own claim: "recursive drain is prevented for free"
|
||||
* via g_vm_interpret_depth is true (the SAME message cannot drain twice),
|
||||
* but that counter says nothing about a SEPARATE, real hazard SXLIII.5
|
||||
* never named -- calling vm_interpret() on this same vm, from inside its
|
||||
* own currently-running vm_interpret() call, overwrites vm->input_buffer/
|
||||
* input_length/input_pos with the drained payload's own state.
|
||||
* VMCallState (vm_state_push()/vm_state_pop(), mama_forth_words.c) saves
|
||||
* only rsp/exit_colon/ecw_nesting -- never these three fields (the exact
|
||||
* gap FABRIC-3.md SXX documents: "the same class... for input_buffer/
|
||||
* input_pos not being saved by vm_state_push/pop"). Left unaddressed,
|
||||
* the enclosing vm_interpret() call's own while loop would silently lose
|
||||
* the rest of its input line/block the moment a drain fires mid-line --
|
||||
* the same failure shape as the INPUT_BUFFER_SIZE 256 defect
|
||||
* .claude/CLAUDE.md calls non-negotiable, and trap #1 in this document's
|
||||
* own START HERE. Not a divergence from SXLIII.3's ruling (the mechanism
|
||||
* is exactly as ruled); cursor/mode/error/abort preservation is this
|
||||
* function's own implementation obligation, not a new design question.
|
||||
*
|
||||
* sk_hermes_drain_checkpoint() therefore snapshots vm->input_buffer/
|
||||
* input_length/input_pos/mode/error/abort_requested before calling
|
||||
* vm_interpret() on the queued payload, forces vm->mode to
|
||||
* MODE_INTERPRET for the duration (a checkpoint reached mid-colon-
|
||||
* definition must not let the payload's words compile into the
|
||||
* enclosing definition), and restores all six afterward -- fully
|
||||
* isolating the drain from whatever the enclosing execution was doing.
|
||||
*
|
||||
* Payload convention: payload_addr is trusted to point at a
|
||||
* NUL-terminated C string (vm_interpret()'s own signature takes no
|
||||
* length) -- payload_len is not consulted here. Bounding/validating that
|
||||
* is task 3.5's scope (payload bound and chunking), not this one's.
|
||||
*
|
||||
* Fast path: sk_hermes_publish()/sk_hermes_pending_pop() maintain a
|
||||
* single system-wide pending-total counter; this function reads it
|
||||
* first and returns immediately if it is 0, so the common case (nothing
|
||||
* in flight) costs one integer read on every word dispatch, not a
|
||||
* stadium_max_vm_count()-sized queue-table scan.
|
||||
*
|
||||
* @param vm The VM at its own outermost checkpoint. NULL is refused.
|
||||
* @return 1 if a message was drained cleanly, 0 if nothing was pending
|
||||
* for this vm, -1 if a message was drained but interpreting its
|
||||
* payload set vm->error (restored to its pre-drain value either
|
||||
* way -- a bad message must not abort the enclosing execution).
|
||||
*/
|
||||
int sk_hermes_drain_checkpoint(VM *vm);
|
||||
|
||||
#endif /* __STARKERNEL__ */
|
||||
|
||||
#endif /* STARKERNEL_VM_KERNEL_HERMES_H */
|
||||
|
||||
Reference in New Issue
Block a user