/* StarForth — Steady-State Virtual Machine Runtime Copyright (c) 2023–2025 Robert A. James All rights reserved. This file is part of the StarForth project. Licensed under the StarForth License, Version 1.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at: https://github.com/star.4th@proton.me/StarForth/LICENSE.txt This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and noninfringement. See the License for the specific language governing permissions and limitations under the License. */ /** * switch.c - Cooperative VM context switch glue (FABRIC-3.md §XXVIII, * Stage 2, 2026-09-13). See switch.h for the contract. */ #ifdef __STARKERNEL__ #include "starkernel/vm/switch.h" #include "vm.h" #include "starkernel/capsule_birth.h" /* capsule_vm_set_state */ #include "starkernel/vm/kernel_hermes.h" /* FABRIC-3.8.md §3.2 -- sk_hermes_drain_checkpoint() */ #include "starkernel/hal/hal.h" /* sk_hal_panic() */ #include "starkernel/capsule_vm_switch_signal.h" /* FABRIC-3.8.md §3.13.3 -- the decision */ #include "starkernel/vm/bootstrap/sk_vm_bootstrap.h" /* sk_get_mama_vm() -- the idle root */ #include /* Arch-specific (switch.S in each arch dir). sk_vm_switch_prime() builds * the exact synthetic first-entry frame sk_vm_switch_to() expects -- kept * in assembly, not duplicated here, so the layout can never drift out of * sync between the two. */ extern void sk_vm_switch_to(uint64_t *save_sp_out, uint64_t new_sp); extern uint64_t sk_vm_switch_prime(uint64_t stack_top, void *entry); /* Single-writer-mainline global, safe under this stage's cooperative-only, * one-VM-runs-at-a-time-on-one-hart discipline (same class of bookkeeping * as vm_core.c's g_log_attrib_vm) -- identifies which VM a freshly-primed * native stack belongs to. Read exactly once, synchronously, at the very * top of sk_vm_switch_entry() before any other first-entry could occur. * Safe to keep as a plain global (unlike the old g_switch_back_to this * file used to also carry, which moved to per-VM native_switch_back_to and * was then removed at FABRIC-3.8.md Stage I once the trampoline stopped * yielding to "whoever switched in"): a VM's identity * never changes across the lifetime of its own trampoline instance, only * who most recently switched into it does. */ static VM *g_switch_entry_vm; /* 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 * nested vm_interpret() calls for log-source attribution and has no way * to know a VM is "running" while parked inside this file's own raw C * trampoline (sk_vm_switch_entry()), never itself calling vm_interpret(). * sk_vm_switch_signal_tick() (capsule_vm_switch_signal.c) consults this * instead of vm_log_attributed_vm() for exactly that reason -- see its * own updated comment. Single-writer-mainline, same discipline as * g_switch_entry_vm above: set unconditionally on every switch, by the * only code path that can ever change who is physically running. * Seeded once at boot via sk_vm_switch_set_current() for whichever VM is * running before any switch has ever happened (Hera, at Stage 3 * registration time -- kernel_main.c). */ static VM *g_switch_current_vm; 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. * * 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 * 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. * * 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. */ /* * FABRIC-3.8.md §3.2 (Stage B, 2026-09-23): the placeholder above is * superseded -- this trampoline is now every switch participant's own * kernel-side idle loop, the base of its native stack. Entirely OS-driven: * nothing a VM executes decides to give up control (Captain Bob: "no * special words saying I'm giving up control... strictly all operating * system and strictly all compudynamically driven"). Each pass: * * - Drain one of this VM's own kernel-Hermes messages, in its own * context, on its own stack (sk_hermes_drain_checkpoint() at depth 0 * here, so the payload runs at this VM's depth 1 and its own word * checkpoints pass the Stage A ownership gate). * - If nothing was drained, this VM has no work: mark it parked-idle * (§3.5 -- nothing borrowed on its stack, nothing in progress) and * yield back to whoever switched in. * * Driven off the drain's return value, never a separate pending-count * probe: any path where a drain returns without consuming a message would * otherwise spin here forever with no word checkpoint to break it -- * indistinguishable from a switch storm. A failed yield is a kernel * invariant violation, not something to retry, so it panics loudly. * * Stage C-b (below) adds the compudynamic decision at this point between * messages, so a VM with a queue of checkpoint-free payloads cannot hold * the CPU indefinitely. */ /* * FABRIC-3.8.md §3.13.3 (Stage C-b, 2026-09-23): the trampoline now asks * the compudynamic decision at its between-messages point, instead of * always draining to empty and then yielding to whoever switched in: * * - still has mail: keep draining unless some VM with work is strictly * cooler (then switch to it; this VM stays eligible through its mail); * - no mail: switch to the coolest VM with work, or -- nobody has any -- * to Hera, the console root, whose idle point polls for input. * * The no-work fallback is Hera rather than native_switch_back_to because * back_to is "whoever switched in last": two idle trampolines that switched * into each other would each yield to the other forever -- a switch storm. * Hera is never waiting on a trampoline, so the chain always ends at her. */ void sk_vm_switch_entry(void) { VM *self = g_switch_entry_vm; for (;;) { VM *next; int drained, has_mail; drained = sk_hermes_drain_checkpoint(self); /* at most one message */ has_mail = sk_hermes_pending_count(self->stadium_vm_id) > 0; /* Stage B's rule, kept: never spin on mail the drain won't take -- * with no word checkpoint in this loop, that would be a silent storm. */ if (drained == 0 && has_mail) { sk_hal_panic("switch trampoline: pending mail the drain did not consume"); } /* FABRIC-3.9.8 Stage M-B (§7.3): a participant that did real work * and now has none tells whoever joined EV-DONE -- the OS emits its * completion; its own code never decides to. Funded by the VM it's * about (M-5); observers never announce their own idleness (M-4). */ if (!has_mail && self->ev_real_work) { self->ev_real_work = 0; if (!sk_vm_switch_signal_is_observer(self->stadium_vm_id)) { VMRegistryEntry e; if (capsule_vm_registry_get(self->stadium_vm_id, &e) == 0) { (void)sk_hermes_event(SK_HERMES_CHANNEL_EV_DONE, self->stadium_vm_id, self->stadium_vm_id, e.name); } } } sk_vm_switch_reap_pending(); next = sk_vm_switch_decide(self, has_mail); if (!next) { if (has_mail) continue; /* still coolest with work: keep going */ next = (VM *)sk_get_mama_vm(); /* nobody has work: back to the root */ if (!next || next == self) sk_hal_panic("switch trampoline: no root to idle to"); } /* Stage FR (§3.12): parked is a VM_FRAME_IDLE frame on this VM's own * stack, pushed only around the switch and popped (by ID) on resume, * BEFORE any further drain -- so the drain's own frame is still the * only frame and the ownership gate's exactly-one-frame rule holds. * It records WHERE the VM is parked: at its trampoline, nothing in * progress (any remaining mail keeps it eligible by itself). */ { VMFrameID idle_id = vm_frame_push(&self->frames, self->stadium_vm_id, VM_FRAME_IDLE); if (idle_id == 0) { sk_hal_panic("switch trampoline: idle frame push failed"); } sk_vm_switch_signal_note_switch_performed(next->stadium_vm_id); if (sk_vm_context_switch(self, next) != 0) { sk_hal_panic("switch trampoline: switch failed"); } vm_frame_pop(&self->frames, idle_id); /* resumed: someone switched back in */ } } } /* See switch.h. The caller has already established that `vm` owns its * outermost frame (sk_vm_owns_outermost_frame(), vm_core.c). */ void sk_vm_switch_checkpoint(VM *vm) { VM *next; if (!sk_vm_switch_signal_take_decision_due()) return; sk_vm_switch_reap_pending(); next = sk_vm_switch_decide(vm, 1); /* vm is mid-task: it still has work */ if (!next) return; sk_vm_switch_signal_note_switch_performed(next->stadium_vm_id); (void)sk_vm_context_switch(vm, next); /* Resumes here once the decision, somewhere, switches back to vm. */ } /* See switch.h. FABRIC-3.8.md §3.13.5 (ruling C-5): Hera between lines, * polling for input, is the idle point. She has no work of her own there, * so any VM with work goes -- the coolest one. She is parked with an IDLE * frame (nothing in progress), so she becomes eligible again only through * mail or pending console input, or is returned to by a trampoline with * nothing left to do. */ void sk_vm_idle_point(void) { VM *self = g_switch_current_vm; VM *next; if (!self || self != (VM *)sk_get_mama_vm()) return; if (self->frames.count != 0) return; /* inside a word (KEY/EXPECT): not idle */ sk_vm_switch_reap_pending(); next = sk_vm_switch_decide(self, 0); if (!next) return; { VMFrameID idle_id = vm_frame_push(&self->frames, self->stadium_vm_id, VM_FRAME_IDLE); if (idle_id == 0) return; /* fail closed: stay put rather than park unmarked */ sk_vm_switch_signal_note_switch_performed(next->stadium_vm_id); (void)sk_vm_context_switch(self, next); vm_frame_pop(&self->frames, idle_id); } } int sk_vm_parked_idle(const VM *vm) { return vm && vm->native_stack_saved_sp != 0 && vm->frames.count == 1 && vm->frames.entries[0].kind == VM_FRAME_IDLE && vm_uuid_equal(vm->frames.entries[0].vm_id, vm->stadium_vm_id); } int sk_vm_context_switch(VM *from, VM *to) { uint64_t new_sp; if (!from || !to || to->native_stack_top == 0) { return -1; } if (to->native_stack_saved_sp == 0) { /* First-ever entry: synthesize the initial frame. */ g_switch_entry_vm = to; new_sp = sk_vm_switch_prime(to->native_stack_top, (void *)&sk_vm_switch_entry); } else { new_sp = to->native_stack_saved_sp; to->native_stack_saved_sp = 0; /* about to be running, not parked */ } capsule_vm_set_state(to->stadium_vm_id, VM_STATE_LIVE); capsule_vm_set_state(from->stadium_vm_id, VM_STATE_SWITCHED_OUT); sk_vm_switch_signal_note_parked(from->stadium_vm_id); /* §3.13.3 wait stamp */ g_switch_current_vm = to; sk_vm_switch_to(&from->native_stack_saved_sp, new_sp); /* Resumes here only once something later switches back to `from` -- * from `from`'s own point of view, this call simply took a while. */ capsule_vm_set_state(from->stadium_vm_id, VM_STATE_LIVE); g_switch_current_vm = from; return 0; } #endif /* __STARKERNEL__ */