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
@@ -50,6 +50,7 @@
|
||||
#include "starkernel/q48_16.h"
|
||||
#include "starkernel/kmalloc.h"
|
||||
#include "console.h"
|
||||
#include "vm.h" /* task 3.4 -- VM struct fields, vm_interpret() */
|
||||
|
||||
/* Freestanding: no libc printf. Prints an unsigned decimal, no leading
|
||||
* zeros -- same small helper stadium.c/capsule_vm_switch_signal.c each
|
||||
@@ -463,12 +464,20 @@ static SkHermesPendingQueue *find_or_create_queue(VMUuid vm_id) {
|
||||
return &sk_hermes_queues[free_slot];
|
||||
}
|
||||
|
||||
/* Task 3.4: system-wide sum of every queue's count, maintained alongside
|
||||
* queue_push()/sk_hermes_pending_pop() below -- lets
|
||||
* sk_hermes_drain_checkpoint() skip its per-VM queue lookup entirely
|
||||
* (stadium_max_vm_count()-sized linear scan) on every word dispatch when
|
||||
* nothing is pending anywhere, the overwhelmingly common case. */
|
||||
static int sk_hermes_pending_total = 0;
|
||||
|
||||
static int queue_push(SkHermesPendingQueue *q, int msg_index) {
|
||||
size_t tail;
|
||||
if (q->count >= SK_HERMES_PENDING_MAX) return -1;
|
||||
tail = (q->head + q->count) % SK_HERMES_PENDING_MAX;
|
||||
q->slots[tail] = msg_index;
|
||||
q->count++;
|
||||
sk_hermes_pending_total++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -529,7 +538,53 @@ int sk_hermes_pending_pop(VMUuid vm_id) {
|
||||
if (!q || q->count == 0) return -1;
|
||||
q->head = (q->head + 1) % SK_HERMES_PENDING_MAX;
|
||||
q->count--;
|
||||
sk_hermes_pending_total--;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sk_hermes_drain_checkpoint(VM *vm) {
|
||||
SkHermesMessage *msg;
|
||||
VMUuid self;
|
||||
char saved_input[INPUT_BUFFER_SIZE];
|
||||
size_t saved_length, saved_pos;
|
||||
vm_mode_t saved_mode;
|
||||
int saved_error, saved_abort;
|
||||
int drain_error;
|
||||
|
||||
if (!vm) return -1;
|
||||
if (sk_hermes_pending_total == 0) return 0; /* fast path -- see this
|
||||
* counter's own doc comment */
|
||||
|
||||
self = vm->stadium_vm_id;
|
||||
msg = sk_hermes_pending_peek(self);
|
||||
if (!msg) return 0;
|
||||
|
||||
/* See kernel_hermes.h's own doc comment on this function for why all
|
||||
* six of these must be preserved, not just the obvious ones. */
|
||||
memcpy(saved_input, vm->input_buffer, sizeof(saved_input));
|
||||
saved_length = vm->input_length;
|
||||
saved_pos = vm->input_pos;
|
||||
saved_mode = vm->mode;
|
||||
saved_error = vm->error;
|
||||
saved_abort = vm->abort_requested;
|
||||
|
||||
vm->mode = MODE_INTERPRET; /* a payload is never compiled into whatever
|
||||
* the enclosing context was mid-defining */
|
||||
vm_interpret(vm, (const char *)msg->payload_addr);
|
||||
drain_error = vm->error;
|
||||
|
||||
memcpy(vm->input_buffer, saved_input, sizeof(saved_input));
|
||||
vm->input_length = saved_length;
|
||||
vm->input_pos = saved_pos;
|
||||
vm->mode = saved_mode;
|
||||
vm->error = saved_error; /* a bad message must not abort the
|
||||
* enclosing execution */
|
||||
vm->abort_requested = saved_abort;
|
||||
|
||||
sk_hermes_release(msg);
|
||||
sk_hermes_pending_pop(self);
|
||||
|
||||
return drain_error ? -1 : 1;
|
||||
}
|
||||
|
||||
#endif /* __STARKERNEL__ */
|
||||
|
||||
Reference in New Issue
Block a user