Files
LithosAnanake/src/starkernel/vm/switch.c
T
Robert Allan JamesandClaude Opus 5.5 fe55038e9a Stage M-B: autonomous events -- EV-DONE / EV-BORN / EV-DIED published by the kernel itself (FABRIC-3.9.0 §7.3, M-3..M-6)
- Permanent event channels 1/2/3 created at boot beside channel 0, exposed
  as FORTH constants EV-DONE / EV-BORN / EV-DIED; CHANNEL-CLOSE refuses 0..3.
- sk_hermes_event(): `S" <name>" ON-...` as type 32 to every member except
  the VM it's about; never blocks -- undeliverable copies are dropped,
  counted (EV-DROPPED), printed and logged.
- Fired for participants only: EV-DONE from the switch trampoline when real
  work runs dry (funded by the VM), EV-BORN at a LIVE VM's first naming
  (funded by the newborn), EV-DIED on KILL/reap after departure (funded by
  the root). VM.ev_real_work keeps event handling from firing EV-DONE, so
  mutual subscribers can't storm.

Console evidence identical on amd64/aarch64/riscv64: one BORN per birth,
one DONE per job run dry (2 queued -> 1), one DIED per KILL, no SEND in any
job text; storm guard: exactly one reaction; channels 1-3 unclosable;
non-joined VMs see nothing; EV-DROPPED 0. Flood probe (reverted): 32
delivered, 7 dropped and counted, never blocked, all heat returned.
dict_hash set changes by the 4 words (546), identical across ISAs;
acceptance passes with held=2048.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 21:12:51 -04:00

278 lines
13 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
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 <stdint.h>
/* 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__ */