Stage C-b: compudynamic switch decision, idle point, timer path removed (FABRIC-3.8 §3.13.3-§3.13.5)

Who runs next is now sk_vm_switch_decide(): among participants with work
(pending kernel-Hermes mail, parked mid-task, or Hera with console input),
the coolest by VM-physics heat; ties with the incumbent keep the incumbent,
ties among candidates go to the longest waiting (unique wait stamps). No
scheduler state, no timer: heartbeat_tick() no longer touches switching,
readiness/threshold/has_work/take_pending removed. Decided at owned word
checkpoints (colon words and, new, between top-level words) after a beat
or new mail, at the trampoline between messages, and at Hera's idle point
in the console poll loops. The idle pump skips participants (G3 closed for
them): the WIREBIND relay is delivered by a real switch to rajames~user.

Probe (restored away): relay via switch Hera->rajames~user->Hera on all
three ISAs, parked saved_sp inside own stack, walk-order disagreements 0
over tens of millions of decisions, 0 bytes idle log growth. Acceptance:
dict_hash unchanged, relay 3, no faults. Rulings: C-3, C-5 as proposed;
C-4 taken as (alpha), consequence recorded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Robert Allan James
2026-09-23 17:53:55 -04:00
co-authored by Claude Opus 5.5
parent 7c3736841d
commit 06bcab504d
27 changed files with 64636 additions and 218 deletions
+86 -17
View File
@@ -32,6 +32,8 @@
#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
@@ -121,39 +123,105 @@ void sk_vm_switch_set_current(VM *vm) { g_switch_current_vm = vm; }
* indistinguishable from a switch storm. A failed yield is a kernel
* invariant violation, not something to retry, so it panics loudly.
*
* Deliberately NOT here yet (FABRIC-3.8.md §3.11, a Stage C design
* question): this point between messages is also a clean switch point
* (own stack, depth 0), and the compudynamic switch decision should be
* consulted here too, so a VM with a queue of checkpoint-free payloads
* cannot hold the CPU indefinitely.
* 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 (;;) {
if (sk_hermes_drain_checkpoint(self) != 0) {
continue; /* consumed one message (1 = ok, -1 = payload erred) */
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");
}
/* Stage FR (§3.12): parked-idle is a VM_FRAME_IDLE frame on this VM's
* own stack, pushed only around the yield 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: an empty stack alone
* would also describe Hera parked at an idle point with her REPL call
* chain live, which is not "nothing in progress." */
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");
}
if (!self->native_switch_back_to ||
sk_vm_context_switch(self, self->native_switch_back_to) != 0) {
sk_hal_panic("switch trampoline: idle yield 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_context_switch(VM *from, VM *to) {
uint64_t new_sp;
@@ -177,6 +245,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);
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);