fix(hermes): one delivery path -- a VM takes its own messages on its own turn, never on another's stack

FABRIC-3.5 SXXXII, closing FABRIC-3 SXXIX bug 2 (SXXXVI).  A send marks the
receiver for the switch signal; the VM's own loop on its native stack takes
its messages and yields; Hera's idle beat makes the pending switch instead
of running other VMs' messages on her stack; every VM gets switcher turns
from birth; the drain refuses any call outside the VM's own context (the
test: it held every relayed line before, none now).

Found on the way: amd64's first-entry frame left rsp 8 bytes off the SysV
entry alignment (#GP on the first interpreter run on a VM's own stack), and
a VM yielding from its loop was marked SWITCHED_OUT, which stopped console
relay after one message and would have made KILL refuse it.

Relay of four lines to a WIREBIND identity on all three ISAs, five
hot-plugged identities, and standard acceptance (POST 1052, hash
0x08873e0f44b7cb2a) all pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rajames
2026-10-14 10:37:57 -04:00
co-authored by Claude Opus 5.5
parent b7e8e4b058
commit 029a8ed9cf
34 changed files with 110307 additions and 119 deletions
+57 -1
View File
@@ -3992,7 +3992,7 @@ are attached reliably in the first place, including under the "already plugged i
case §XIV's own second reproduction showed failing.
**Bug 2 -- the MSG-TICK/Stage-3-switch dual-ownership rough edge (§XXVIII's own admission,
never revisited).** Both MSG-TICK's idle-pump dispatch and the Stage 3 timer-driven switcher can
never revisited).** *Closed 2026-10-14: one delivery path, built -- §XXXVI.* Both MSG-TICK's idle-pump dispatch and the Stage 3 timer-driven switcher can
independently move control between the same VMs; not reconciled, not observed to interact badly
so far, but never proven safe under all interleavings either. Directly relevant to Stage 4 for
the same reason as Bug 1: WIREBIND VMs bring a second population of dynamically-birthed contexts
@@ -5524,3 +5524,59 @@ device it was minted on.
design/decision only, no capsule/C code, no new disk image, and no re-mint actually performed
or committed for the drive resize, the persistence region, or the N=2 campaign shape.
## XXXVI. One delivery path: a VM takes its own messages, on its own turn -- built (2026-10-14)
Closes the MSG-TICK/Stage-3 dual-ownership item (§XXVIII, §XXIX bug 2, §XXXI), as ruled in
`FABRIC-3.5.md` §XXXII ("one mover"); Captain Bob, 2026-10-14: collapse it before the bare-metal
builds. **The state before:** the MSG-TICK pump was gone (FABRIC-3.6 Stage E), but two paths
still ran a VM's kernel-Hermes messages -- the VM's own outermost colon-word checkpoint, and
Hera's idle beat, which walked every live VM and ran each one's messages *for* it, on Hera's
stack. The second was the only path for a VM nothing else drives (a WIREBIND identity's `~user`
VM, FABRIC-3.6 task 3.9). It was guarded (parked VMs are not LIVE and were skipped) but it was
two paths.
**What changed.**
- `sk_hermes_send_one()` marks the receiver as having work for the switch signal.
- What a VM runs on its own native stack (`sk_vm_switch_entry()`, until now a placeholder that
only yielded) now takes its own messages, then yields. Not while its interpreter is mid-line on
some other, parked, stack (new per-VM `interpret_depth`, counted in `vm_interpret()`): then
they wait and it asks for another turn.
- Hera's idle beat no longer runs anyone's messages but her own; it makes the switch the switch
signal has pending (`sk_vm_switch_service_pending()`, shared with the colon-word checkpoint),
so a message still arrives while she sits at her prompt.
- Every VM is registered with the switch signal at its birth (idempotent; the Tripod and
WIREBIND registrations stay) and unregistered when killed or reaped.
- `sk_hermes_drain_checkpoint()` refuses, with "Kernel-Hermes: message held -- its VM is not the
one running", any call not in the VM's own context: its interpreter running at the outermost
text, or it is the VM switched in and not mid-line elsewhere. This is the test: it failed
first, holding every relayed line (`logs/20261014-092558`), and never fires now.
- A VM yielding from the top of its own loop stays LIVE; only a VM switched out mid-line is
SWITCHED_OUT. Found by the test: with every yield SWITCHED_OUT, the console stopped relaying
to an identity after its first message (it relays only to a LIVE one) and KILL would have
refused every VM that had ever taken a message (`logs/20261014-095904`).
**A real defect found on the way, amd64 only:** `sk_vm_switch_prime()` (`arch/amd64/switch.S`)
built a first-entry frame that left `rsp` a multiple of 16 at `sk_vm_switch_entry()`'s entry; the
SysV ABI wants `rsp + 8` to be. Harmless while that function only switched back; a #GP (an
aligned SSE store to the stack) the first time it ran the interpreter there
(`logs/20261014-093823`). The frame is now 64 bytes with a null return address above the entry
address. aarch64 (160) and riscv64 (208) frames were already 16-aligned. This may also be what
stopped the 2026-09-14 attempt (`vm_interpret(self, "MSG-TICK")` in the same function, reverted
after a stall never root-caused) -- not proven.
**`FABRIC-3.5.md` §XXXII.5's three build questions, as answered here:** (1) the payload waits in
the receiver's kernel-Hermes queue and is run by the receiver on its own stack on its turn; (2)
Hera's special case stays only in this sense -- her messages run at her own idle beat, which is
her own context; (3) `g_mama_interpreting` and `g_idle_pump_active` stay, still guarding Hera's
own drain.
**Verified.** A WIREBIND identity (`S" 00" USE`, then four plain lines: `1 2 + .`, an unknown
word, `5 6 * .`, `7 8 + .`) receives and runs all four on its own stack -- `3`, the unknown-word
error, `30`, `15` -- with nothing held and no fault, on amd64 (`logs/20261014-101933`), aarch64
(`logs/20261014-102701`) and riscv64 (`logs/20261014-100948`). Five identity drives hot-plugged a
second apart at the prompt all attach (their attach acknowledgements are messages to Hera)
(`logs/20261014-102525`). Standard acceptance, committed disks: all three ISAs to `zuse@Hera`,
POST 1052 tests passed, `PARITY:M7.1a ... hash=0x08873e0f44b7cb2a` unchanged, no unknown word
(`logs/20261014-103248`, `-103409`, `-103616`). Superseded attempts, kept: `-092406` (wrong `USE`
syntax), `-094908` (before the LIVE-on-yield fix, one line only).
+11
View File
@@ -46,6 +46,13 @@ struct VM;
int sk_vm_context_switch(struct VM *from, struct VM *to);
/* Performs the switch the switch signal has pending, if any, from `from`
* -- the VM that owns the native stack now running. Returns 1 once it has
* switched and been switched back to, 0 when there was nothing to do.
* Called at the colon-word checkpoint (vm_core.c) and at Hera's idle beat
* (repl.c). */
int sk_vm_switch_service_pending(struct VM *from);
/* FABRIC-3.md §XXVIII Stage 3 follow-on (2026-09-14): which VM is
* currently physically running, tracked by the switch mechanism itself --
* see switch.c's own doc comment on g_switch_current_vm for why this
@@ -55,6 +62,10 @@ int sk_vm_context_switch(struct VM *from, struct VM *to);
struct VM *sk_vm_switch_current_vm(void);
void sk_vm_switch_set_current(struct VM *vm);
/* True when at most one vm_interpret() is in progress, i.e. the word now
* running belongs to the outermost text on this stack (vm_core.c). */
int sk_vm_at_outermost_interpret(void);
#endif /* __STARKERNEL__ */
#endif /* STARKERNEL_VM_SWITCH_H */
+9 -1
View File
@@ -68,7 +68,15 @@ sk_vm_switch_to:
.global sk_vm_switch_prime
sk_vm_switch_prime:
mov rax, rdi
sub rax, 56
/* 64, not 56 (2026-10-14): one more slot above the return address,
* where a caller's own return address would sit. `ret` into `entry`
* then leaves rsp at stack_top - 8, i.e. rsp + 8 a multiple of 16,
* exactly as after a real `call` -- the SysV ABI's entry alignment.
* With 56, entry began 8 bytes off: harmless while the entry function
* only switched back, a #GP (an aligned SSE store to the stack) the
* first time it ran the interpreter on this stack. */
sub rax, 64
mov qword ptr [rax+56], 0 /* no caller: a null return address */
mov qword ptr [rax+0], 0 /* r15 */
mov qword ptr [rax+8], 0 /* r14 */
mov qword ptr [rax+16], 0 /* r13 */
+9
View File
@@ -346,6 +346,8 @@ void capsule_vm_force_reap(VMUuid vm_id) {
* capsule_vm_kill()'s own unconditional rule. */
if (vm_uuid_is_hera(entry->vm_id)) return;
(void)sk_vm_switch_signal_unregister(entry->vm_id); /* no turns for the dead */
VM *vm = (VM *)entry->vm_ptr;
/* Deliberately NOT capsule_vm_kill()'s VM_STATE_SWITCHED_OUT refusal
@@ -467,6 +469,8 @@ int capsule_vm_kill(const char *name) {
vm_id = entry->vm_id;
vm = (VM *)entry->vm_ptr;
(void)sk_vm_switch_signal_unregister(vm_id); /* no turns for the dead */
/* FABRIC-2.md SS B, VM-COOL: reap this VM's own Stadium patron cell for
* real, dispatching COOL. Refusal (already naturally reclaimed by
* unrelated quota pressure, or never admitted) is silently tolerated --
@@ -724,6 +728,11 @@ CapsuleRunResult capsule_birth_baby(
((VM *)new_vm)->native_stack_paddr = ns.paddr;
((VM *)new_vm)->native_stack_guard_vaddr = ns.guard_vaddr;
((VM *)new_vm)->native_stack_top = ns.stack_top;
/* One path (2026-10-14): a VM that can be switched to is one
* the switcher can give a turn, so that a message sent to it
* is taken by the VM itself, on its own stack (switch.c) --
* every VM, not only the Tripod and WIREBIND identities. */
(void)sk_vm_switch_signal_register(vm_id);
} else {
log_message(LOG_WARN, "capsule_birth_baby: native stack allocation failed for %s",
capsule_name);
@@ -132,6 +132,11 @@ int sk_vm_switch_signal_boot_init(void) {
}
int sk_vm_switch_signal_register(VMUuid vm_id) {
/* Idempotent (2026-10-14): every VM is registered at its birth now
* (capsule_birth.c), and the Tripod and WIREBIND callers that predate
* that still register their own -- the second call finds the first. */
int slot = slot_for_vm_id(vm_id);
if (slot >= 0) return slot;
if (g_slot_count >= g_slot_capacity) return -1;
g_slots[g_slot_count].vm_id = vm_id;
g_slots[g_slot_count].readiness = 0;
+30 -68
View File
@@ -46,6 +46,7 @@
#include "starkernel/capsule_run.h"
#include "starkernel/vm/bootstrap/sk_vm_bootstrap.h"
#include "starkernel/vm/kernel_hermes.h" /* FABRIC-3.6.md task 3.8 -- sk_hermes_send_one() */
#include "starkernel/vm/switch.h" /* one path: sk_vm_switch_service_pending() */
#include "starkernel/vm/stadium.h" /* task 3.8 -- stadium_conserved() evidence print */
#include "block_subsystem.h"
#include "word_source/include/keyboard_words.h"
@@ -534,10 +535,11 @@ static void sk_cursor_show(void)
*
* FABRIC-3.6.md Phase 4 (Stage E), 2026-09-22: this used to guard a
* VM-EXEC "MSG-TICK" dispatch into every live child VM (FABRIC-2.md
* Phase C, common:messaging.4th, now removed). The mechanism changed --
* sk_repl_idle() now calls sk_hermes_drain_checkpoint() directly per VM,
* not VM-EXEC -- but the reentrancy hazard these two flags exist for is
* unchanged: drain_checkpoint() still calls vm_interpret() internally
* Phase C, common:messaging.4th, now removed). The mechanism changed
* twice -- to a direct sk_hermes_drain_checkpoint() per VM (2026-09-22),
* then to one switch per beat plus Hera's own drain (2026-10-14, one
* path) -- but the reentrancy hazard these two flags exist for is
* unchanged for Hera's own drain: it calls vm_interpret() internally
* (on the drained message's own payload), and sk_repl_idle() is itself
* called from the blocking KEY/EXPECT/QUERY reads (sk_console_getkey()/
* sk_console_readline(), which run *from inside* the executing VM's own
@@ -563,9 +565,9 @@ static int g_idle_pump_active; /* zero-initialized (BSS) */
* cursor, a fixed number of dispatches per beat). FABRIC-3.6.md Phase 4
* (Stage E), 2026-09-22: that whole mechanism (MSG-TICK, the batching
* cap, and this comment's own prior form) is gone along with
* common:messaging.4th itself -- see sk_repl_idle()'s own current
* comment, just below, for why the replacement (kernel-Hermes drain)
* does not carry the same O(N) cost and needs no cap. */
* common:messaging.4th itself. Since 2026-10-14 a beat makes at most one
* switch (the one the switch signal has pending) and drains only Hera:
* O(1), no cap needed. */
static void sk_repl_idle(VM *active_vm)
{
@@ -853,34 +855,24 @@ static void sk_repl_idle(VM *active_vm)
* comment. */
capsule_wirebind_reap_idle_check();
/* FABRIC-3.6.md Phase 4 (Stage E, Category B strip), 2026-09-22:
* this used to be a distributed FORTH messaging pump -- FABRIC-2.md
* Phase C's own design, VM-EXEC'ing MSG-TICK into every live VM's
* own dictionary once per idle beat (common:messaging.4th, now
* removed entirely). That mechanism, and the comment that used to
* sit here explaining it, are both gone: kernel-Hermes's own drain
* (below) has needed no FORTH word since task 3.9, and every
* messaging.4th-owned message type had a real cutover by task 3.10
* (FABRIC-3.6.md task 3.11, Phase 3 gate). The prior version of this
* comment also claimed "Hera never loads common:messaging.4th" --
* that was already wrong before this strip (capsules/init.4th line
* 21 EXECs it directly; task 3.10's own live FIND-based check
* confirmed it), not just made moot by removing the file. She still
* has the only persistent idle tick, so she is still the one that
* walks the registry once per idle beat -- now purely to give every
* live VM's own kernel-Hermes queue a chance to drain, batched the
* same round-robin way the old MSG-TICK dispatch was (SK_MSG_PUMP_
* BATCH below), kept for the same reason: bounding this to O(K)
* regardless of total VM count, not O(N) every beat. */
/* ONE PATH (FABRIC-3.5.md §XXXII, built 2026-10-14). Hera has the only
* persistent idle beat, so this is where a VM with messages waiting
* gets its turn while she sits at her prompt: the switch the switch
* signal has pending is made here, from whichever VM owns this stack,
* and the target takes its own messages on its own stack (switch.c's
* sk_vm_switch_entry()). Until 2026-10-14 this walked every live VM
* and ran each one's messages for it, here, on Hera's stack -- the
* second delivery path, now gone (and refused: kernel_hermes.c). Before
* 2026-09-22 it VM-EXEC'd MSG-TICK into each VM instead (FABRIC-2.md
* Phase C, common:messaging.4th, removed). */
VM *mama = (VM *)sk_get_mama_vm();
/* Reentrancy guard: never run the pump while mama is mid-interpret
* (a dispatched line, or recursively from within the pump's own
* vm_interpret). vm_interpret() clobbers the VM's single input
* buffer, so re-entering it here while KEY/EXPECT/QUERY blocks inside
* a live parse truncates the rest of that line. Deferring the MSG-TICK
* drain to the next prompt boundary is safe -- draining is best-effort
* and simply resumes next beat. */
/* Reentrancy guard: not while Hera is mid-line (a dispatched line, or
* recursively from within this beat's own work). A switch made from
* inside a KEY/EXPECT/QUERY wait would be safe -- the whole stack is
* parked and comes back intact -- but Hera's own messages run in her
* interpreter, which must not be entered mid-line; and nothing here is
* urgent: it resumes next beat, at the prompt. */
if (g_mama_interpreting || g_idle_pump_active) {
if (console_tx_count() == tx_before_idle) {
console_cancel_deferred_line_start();
@@ -890,42 +882,12 @@ static void sk_repl_idle(VM *active_vm)
g_idle_pump_active = 1;
{
/* FABRIC-3.6.md Phase 4 (Stage E), 2026-09-22: the SK_MSG_PUMP_
* BATCH cap this loop used to enforce existed to bound the cost
* of the old MSG-TICK VM-EXEC dispatch -- a genuinely expensive
* per-VM vm_interpret() call, confirmed live as a real O(N)
* bottleneck at just 9 VMs on riscv64 (see this section's own
* history above). sk_hermes_drain_checkpoint() (task 3.4) is not
* that: its own fast path is a single global-counter read
* (sk_hermes_pending_total == 0 -> return immediately,
* kernel_hermes.c's own doc comment), so walking every live VM
* every beat costs one cheap integer read per VM in the common
* case (nothing in flight), not an O(N) wall -- the cap this
* loop's own comment history worried about does not apply to
* this call. Removed: every live VM (except mama, handled
* separately below) is now visited every idle beat, matching
* what task 3.9's own testing already exercised, without the
* ceil(N/SK_MSG_PUMP_BATCH)-beat delivery latency the cap was
* accidentally imposing on kernel-Hermes drain specifically. */
uint32_t count = capsule_vm_registry_count();
uint32_t i;
for (i = 0; i < count; i++) {
VMRegistryEntry ent;
if (capsule_vm_registry_get_by_index(i, &ent) == 0 &&
ent.state == VM_STATE_LIVE &&
ent.vm_ptr != (void *)mama) {
(void)sk_hermes_drain_checkpoint((VM *)ent.vm_ptr);
}
}
VM *cur = sk_vm_switch_current_vm();
if (cur) (void)sk_vm_switch_service_pending(cur);
/* Hera's own messages, when this beat is running on her stack:
* she is the VM switched in, at her prompt -- her own context. */
if (!cur || cur == mama) (void)sk_hermes_drain_checkpoint(mama);
}
/* Hera's own kernel-Hermes queue needs draining too, same as every
* other VM's -- she is excluded from the loop above not because she
* has nothing to drain, but because she IS the pump: a self-
* targeting call here runs directly, no VM-EXEC/reentrancy concern
* (sk_hermes_drain_checkpoint() is a plain C call, not a dispatch
* into anyone else's input buffer). */
(void)sk_hermes_drain_checkpoint(mama);
g_idle_pump_active = 0;
if (console_tx_count() == tx_before_idle) {
+28
View File
@@ -52,6 +52,9 @@
#include "starkernel/kmalloc.h"
#include "console.h"
#include "vm.h" /* task 3.4 -- VM struct fields, vm_interpret() */
#include "starkernel/vm/switch.h" /* one path: sk_vm_switch_current_vm() */
#include "starkernel/log_attrib.h" /* one path: vm_log_attributed_vm() */
#include "starkernel/capsule_vm_switch_signal.h" /* one path: sk_vm_switch_signal_mark_work() */
/* Freestanding: no libc printf. Prints an unsigned decimal, no leading
* zeros -- same small helper stadium.c/capsule_vm_switch_signal.c each
@@ -531,6 +534,9 @@ int sk_hermes_send_one(VMUuid from, VMUuid to, uint32_t type, uint32_t channel_i
return -1;
}
/* One path (2026-10-14): the receiver takes it on its own turn -- ask
* the switcher to give it one. */
sk_vm_switch_signal_mark_work(to);
return 0;
}
@@ -594,6 +600,28 @@ int sk_hermes_drain_checkpoint(VM *vm) {
msg = sk_hermes_pending_peek(self);
if (!msg) return 0;
/* ONE PATH (FABRIC-3.5.md §XXXII, built 2026-10-14): a VM takes its
* messages itself, in its own context, and nowhere else -- either
* - its own interpreter is the one running, at the outermost text
* (the checkpoint in execute_colon_word()), or
* - it is the VM switched in, and its interpreter is not mid-line on
* some other, parked, stack (switch.c's sk_vm_switch_entry(), and
* Hera's own idle beat at her prompt).
* Anything else would run one VM's message on another's stack, or
* enter an interpreter that is already mid-line. Refused, and the
* message stays queued for the VM's own turn. */
{
VM *cur = sk_vm_switch_current_vm();
int running_here = vm->interpret_depth > 0 &&
vm == vm_log_attributed_vm() &&
sk_vm_at_outermost_interpret();
int switched_in = vm->interpret_depth == 0 && (!cur || vm == cur);
if (!running_here && !switched_in) {
console_println("Kernel-Hermes: message held -- its VM is not the one running");
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));
+74 -24
View File
@@ -30,6 +30,8 @@
#include "starkernel/vm/switch.h"
#include "vm.h"
#include "starkernel/capsule_birth.h" /* capsule_vm_set_state */
#include "starkernel/capsule_vm_switch_signal.h" /* one path: take_pending(), mark_work() */
#include "starkernel/vm/kernel_hermes.h" /* one path: sk_hermes_drain_checkpoint() */
#include <stdint.h>
/* Arch-specific (switch.S in each arch dir). sk_vm_switch_prime() builds
@@ -51,6 +53,8 @@ extern uint64_t sk_vm_switch_prime(uint64_t stack_top, void *entry);
* who most recently switched into it does. */
static VM *g_switch_entry_vm;
static int switch_core(struct VM *from, struct VM *to, VMState from_state);
/* FABRIC-3.md §XXVIII Stage 3 follow-on (2026-09-14): which VM is
* currently running, from the switch mechanism's own point of view --
* independent of vm_log_attributed_vm() (vm_core.c), which is scoped to
@@ -72,39 +76,81 @@ VM *sk_vm_switch_current_vm(void) { return g_switch_current_vm; }
void sk_vm_switch_set_current(VM *vm) { g_switch_current_vm = vm; }
/**
* sk_vm_switch_entry - trampoline for a VM's first-ever entry.
* sk_vm_switch_entry - what a VM runs on its own native stack.
*
* A freshly-entered VM has no production behavior defined yet -- what a
* switched-to VM should actually DO once running is later-stage work
* (message dispatch, an idle loop, whatever Stage 3+ needs). For now this
* is a minimal, permanent placeholder: immediately yield back to whoever
* most recently switched to it, forever. Reads self->native_switch_back_to
* Entered once, at a VM's first-ever switch-in, and never left: each turn
* it takes its own pending kernel-Hermes messages, then yields back to
* whoever most recently switched to it. Reads self->native_switch_back_to
* fresh every iteration (not a value captured once) so a later switch-in
* from a different VM is honored, not silently bounced to whoever entered
* first. Must never fall off the end -- there is no legitimate return
* address below a synthetic first-entry frame, unlike an ordinary
* function.
* from a different VM is honored. Must never fall off the end -- there is
* no legitimate return address below a synthetic first-entry frame.
*
* FABRIC-3.md §XXVIII Stage 3 follow-on (2026-09-14): an attempt to make
* this attributable by calling vm_interpret(self, "MSG-TICK") here was
* tried and reverted -- it introduced a second, distinct hang (a stall in
* sk_repl_headless_wait()'s WIREBIND-attachment wait, not root-caused) in
* place of the switch-storm it was meant to fix. The switch-storm's real
* cause (sk_vm_switch_signal_tick()'s "who is current" check via
* vm_log_attributed_vm() never seeing a VM that only ever runs this raw
* C trampoline) is instead fixed at the source in switch.c/
* capsule_vm_switch_signal.c: sk_vm_context_switch() now tracks "current"
* itself, independent of vm_interpret()'s own attribution bookkeeping.
* This placeholder's behavior is therefore unchanged from Stage 2.
* History. Until 2026-10-14 this only yielded (a Stage 2 placeholder), and
* a VM's messages were run for it from Hera's idle beat, on her stack --
* the second of the two delivery paths FABRIC-3.5.md §XXXII ruled down to
* one. A first attempt at giving it work (2026-09-14: vm_interpret(self,
* "MSG-TICK") here) was reverted after a stall in
* sk_repl_headless_wait()'s WIREBIND wait, never root-caused; the switch
* storm it was aimed at was fixed at its source instead (switch.c and
* capsule_vm_switch_signal.c track "current" themselves).
*/
void sk_vm_switch_entry(void) {
VM *self = g_switch_entry_vm;
for (;;) {
sk_vm_context_switch(self, self->native_switch_back_to);
/* ONE PATH (FABRIC-3.5.md §XXXII, built 2026-10-14): what a VM does
* on its turn is take its own messages, here, on its own stack --
* the only place a VM that nothing else drives ever takes one
* (Hera's idle beat used to run them for it, on her stack). Not
* while its interpreter is mid-line on some other, parked, stack
* (a console VM waiting in KEY, say): then they wait, and it asks
* for another turn. */
if (self->interpret_depth == 0) {
while (sk_hermes_drain_checkpoint(self) != 0) { }
}
if (sk_hermes_pending_count(self->stadium_vm_id) > 0) {
sk_vm_switch_signal_mark_work(self->stadium_vm_id);
}
switch_core(self, self->native_switch_back_to, VM_STATE_LIVE);
}
}
int sk_vm_context_switch(VM *from, VM *to) {
int sk_vm_switch_service_pending(VM *from) {
VMRegistryEntry target;
VMUuid target_id = sk_vm_switch_signal_take_pending();
if (!from || vm_uuid_equal(target_id, vm_uuid_none())) return 0;
if (capsule_vm_registry_get(target_id, &target) != 0 ||
!target.vm_ptr || target.vm_ptr == (void *)from) {
return 0;
}
/* FABRIC-3.md §XXVIII Stage 4 (2026-09-14): a target marked
* pending_reap is a WIREBIND identity whose owning device has
* physically detached while it sat parked SWITCHED_OUT --
* capsule_vm_kill() already refused to free it when that happened
* (see VMRegistryEntry.pending_reap's own doc comment). This is the
* safe point the mark-and-defer design calls for: reap instead of
* resuming, and skip the switch -- take_pending() already consumed the
* pending flag, so no other caller will try this same dead end. */
if (target.pending_reap) {
capsule_vm_force_reap(target_id);
return 0;
}
sk_vm_switch_signal_note_switch_performed(target_id);
sk_vm_context_switch(from, (VM *)target.vm_ptr);
/* Resumes here once something later switches back. */
return 1;
}
/* `from_state` is what `from` is while it is parked. VM_STATE_SWITCHED_OUT
* for a VM switched out mid-line (the colon-word checkpoint, Hera's idle
* beat): its stack holds live frames, and capsule_vm_kill() must refuse
* it. VM_STATE_LIVE for a VM yielding from the top of its own loop in
* sk_vm_switch_entry() (2026-10-14): its stack holds nothing but that loop,
* so it is as alive, as killable and as reachable by name as a VM that has
* never had a turn -- marking it SWITCHED_OUT made the console stop
* relaying to a WIREBIND identity after its first message, and made KILL
* refuse every VM that had ever taken one. */
static int switch_core(VM *from, VM *to, VMState from_state) {
uint64_t new_sp;
if (!from || !to || to->native_stack_top == 0) {
@@ -126,7 +172,7 @@ int sk_vm_context_switch(VM *from, VM *to) {
}
capsule_vm_set_state(to->stadium_vm_id, VM_STATE_LIVE);
capsule_vm_set_state(from->stadium_vm_id, VM_STATE_SWITCHED_OUT);
capsule_vm_set_state(from->stadium_vm_id, from_state);
g_switch_current_vm = to;
sk_vm_switch_to(&from->native_stack_saved_sp, new_sp);
@@ -138,4 +184,8 @@ int sk_vm_context_switch(VM *from, VM *to) {
return 0;
}
int sk_vm_context_switch(VM *from, VM *to) {
return switch_core(from, to, VM_STATE_SWITCHED_OUT);
}
#endif /* __STARKERNEL__ */
+3 -25
View File
@@ -958,31 +958,7 @@ void execute_colon_word(VM* vm)
* eligibility state directly (2026-09-14 correction) -- see its
* own doc comment for why that can't be left to ISR polling. */
if (sk_vm_at_outermost_interpret()) {
VMUuid target_id = sk_vm_switch_signal_take_pending();
if (!vm_uuid_equal(target_id, vm_uuid_none())) {
VMRegistryEntry target_entry;
if (capsule_vm_registry_get(target_id, &target_entry) == 0 &&
target_entry.vm_ptr && target_entry.vm_ptr != (void *)vm) {
/* FABRIC-3.md §XXVIII Stage 4 (2026-09-14): a target
* marked pending_reap is a WIREBIND identity whose
* owning device has physically detached while it sat
* parked SWITCHED_OUT -- capsule_vm_kill() already
* refused to free it when that happened (see
* VMRegistryEntry.pending_reap's own doc comment).
* This checkpoint is the safe point the mark-and-
* defer design calls for: reap instead of resuming,
* and skip the switch this tick -- take_pending()
* already consumed the pending flag, so no other
* checkpoint will try this same dead end. */
if (target_entry.pending_reap) {
capsule_vm_force_reap(target_id);
} else {
sk_vm_switch_signal_note_switch_performed(target_id);
sk_vm_context_switch(vm, (VM *)target_entry.vm_ptr);
/* Resumes here once something later switches back. */
}
}
}
(void)sk_vm_switch_service_pending(vm);
}
#endif
@@ -1202,6 +1178,7 @@ void vm_interpret(VM* vm, const char* input)
outer_buffer[outer_length] = '\0';
}
g_vm_interpret_depth++;
vm->interpret_depth++;
/* Load into input buffer (cap + NUL) */
size_t n = 0, cap = INPUT_BUFFER_SIZE ? INPUT_BUFFER_SIZE - 1 : 0;
@@ -1235,6 +1212,7 @@ void vm_interpret(VM* vm, const char* input)
}
g_vm_interpret_depth--;
vm->interpret_depth--;
if (nested) {
size_t k;
for (k = 0; k <= outer_length; k++) vm->input_buffer[k] = outer_buffer[k];
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+6
View File
@@ -669,6 +669,12 @@ typedef struct VM
* let a later switch-in from a different VM get
* silently bounced to the original caller instead
* of itself. */
int interpret_depth; /**< How many vm_interpret() calls on this VM are in
* progress, on any native stack (kernel only;
* 2026-10-14). Non-zero means this VM's
* interpreter is mid-line somewhere -- possibly
* on a stack that is parked right now -- and must
* not be entered again to deliver a message. */
/** @} */
/** @name Stadium Identity (item 4.2, FABRIC-0.md §25.5)