/* 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/capsule_vm_switch_signal.h" /* one path: take_pending(), mark_work() */ #include "starkernel/vm/kernel_hermes.h" /* one path: sk_hermes_drain_checkpoint() */ #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 -- see native_switch_back_to's own doc comment * in vm.h for why that one had to move to per-VM state): 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; 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 * 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 - what a VM runs on its own native stack. * * 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. Must never fall off the end -- there is * no legitimate return address below a synthetic first-entry frame. * * 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 (;;) { /* 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_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) { return -1; } /* Recorded unconditionally (first-ever entry or a resume) so whoever * is parked at `to`'s native stack -- most often this placeholder * trampoline -- always knows who most recently switched into it. */ to->native_switch_back_to = from; 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, from_state); 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; } int sk_vm_context_switch(VM *from, VM *to) { return switch_core(from, to, VM_STATE_SWITCHED_OUT); } #endif /* __STARKERNEL__ */