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
+160 -124
View File
@@ -33,6 +33,12 @@
#include "starkernel/vm/stadium.h" /* stadium_max_vm_count() -- task 3.1's sizing bound */
#include "starkernel/kmalloc.h"
#include "starkernel/console.h"
#include "starkernel/capsule_birth.h" /* registry, capsule_vm_force_reap() */
#include "starkernel/capsule_vm_physics.h" /* vm_physics_heat_of() -- the decision's heat */
#include "starkernel/vm/kernel_hermes.h" /* sk_hermes_pending_peek() -- eligibility is the queue */
#include "starkernel/vm/vm_frame.h" /* top frame kind -- in progress vs parked idle */
#include "starkernel/vm/bootstrap/sk_vm_bootstrap.h" /* sk_get_mama_vm() */
#include "starkernel/repl.h" /* sk_console_key_available() -- Hera's mail (C-5) */
#include <stddef.h>
/* FABRIC-3.6.md task 3.1 (2026-09-21, ruled B2 / FABRIC-3.5.md §XLV.2):
@@ -47,36 +53,42 @@
* feedback_no_hardcoded_small_scale_bounds in project memory -- a fixed
* cap here was rejected before; this closes the same class of bound. */
/* Ticks a non-running participant must accumulate readiness before a
* switch to it is requested. Simple linear accumulate-then-threshold,
* not the fancier relaxation law a later pass might want -- correctness
* and a clean single-writer/single-reader story mattered more than the
* exact law for this first cut (this stage's own design-review latitude,
* per the plan). Reset to 0 the instant a slot becomes current, so a
* VM that just ran needs a full threshold's worth of ticks idle before
* it can be selected again -- a crude but real fairness property. */
#define SK_SWITCH_READINESS_THRESHOLD 50u
/* FABRIC-3.8.md §3.13 (Stage C-b, 2026-09-23): the timer-driven readiness
* accrual (SK_SWITCH_READINESS_THRESHOLD = 50 ISR ticks, per-slot
* readiness/has_work, a pending flag armed from heartbeat_tick()) is gone.
* Captain Bob: "the timer is NOT the source of preemption, compudynamics
* is" and "no scheduler allowed." Whether and to whom a VM switches is now
* sk_vm_switch_decide() below: a physical comparison (VM-physics heat,
* charged per beat of work -- Stage C-a) over the VMs that have work,
* made only at the moments the physics exposes. Nothing here runs from
* interrupt context any more. */
typedef struct {
VMUuid vm_id;
uint32_t readiness;
int has_work; /* Stage 3 follow-on (2026-09-14): set by
* sk_vm_switch_signal_mark_work() (mainline,
* MSG-SEND hook), cleared here in tick() the
* instant this slot becomes current -- see that
* function's own doc comment in the header. */
uint64_t parked_stamp; /* wait stamp taken when this VM last stopped
* running (sk_vm_switch_signal_note_parked());
* 0 = never parked. See sk_vm_switch_decide(). */
} sk_switch_slot_entry_t;
static sk_switch_slot_entry_t *g_slots = (sk_switch_slot_entry_t *)0;
static int g_slot_capacity; /* 0 until sk_vm_switch_signal_boot_init() succeeds */
static int g_slot_count;
static int g_pending;
static VMUuid g_pending_target;
/* Single-writer, mainline-only event count behind every wait stamp (mail
* admission and parking). Unique per event, so the "waiting longest"
* tie-break (§3.13.3, ruling C-3) always has exactly one answer. Starts
* at 1 so 0 can mean "no stamp". */
static uint64_t g_stamp_next = 1;
/* Set when something physical changed that could change the decision --
* the running VM produced a beat (and was just charged for it), or mail
* was admitted -- and consumed at the next owned checkpoint. Between such
* events every decision would repeat the last one (§3.13.3). */
static int g_decision_due;
/* DoE CSV exposure (FABRIC-3.md §XXVIII Stage 3 follow-on, 2026-09-13). */
static int g_current_slot_cached = -1;
static uint64_t g_switch_count;
static uint32_t g_ticks_since_switch;
static uint32_t g_beats_since_switch; /* §3.13.4: was ISR ticks, now beats */
/* Freestanding: no libc printf. Prints an unsigned decimal, no leading
* zeros -- same small helper stadium.c/stadium_blocks.c each carry their
@@ -115,9 +127,8 @@ int sk_vm_switch_signal_boot_init(void) {
if (!slots) return -1;
for (i = 0; i < max_vm_count; i++) {
slots[i].vm_id = vm_uuid_none();
slots[i].readiness = 0;
slots[i].has_work = 0;
slots[i].vm_id = vm_uuid_none();
slots[i].parked_stamp = 0;
}
g_slots = slots;
@@ -133,9 +144,8 @@ int sk_vm_switch_signal_boot_init(void) {
int sk_vm_switch_signal_register(VMUuid vm_id) {
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;
g_slots[g_slot_count].has_work = 0;
g_slots[g_slot_count].vm_id = vm_id;
g_slots[g_slot_count].parked_stamp = 0;
return g_slot_count++;
}
@@ -148,104 +158,140 @@ int sk_vm_switch_signal_unregister(VMUuid vm_id) {
}
g_slot_count--;
/* A pending switch targeting this VM must not survive its removal --
* the checkpoint in execute_colon_word() would otherwise try to
* switch into a VM no longer tracked here. */
if (g_pending && vm_uuid_equal(g_pending_target, vm_id)) {
g_pending = 0;
}
/* g_current_slot_cached is a DoE-observable value only (see its own
* declaration comment); if compaction just invalidated it, the very
* next tick() recomputes it from scratch via slot_for_vm_id(), so a
* momentarily-stale cache here is harmless. */
/* No pending target to clear any more (Stage C-b): the decision keeps
* no state between calls, so a removed VM simply stops being walked. */
return 0;
}
void sk_vm_switch_signal_tick(void) {
VM *current;
int current_slot, i;
/* FABRIC-3.md §XXVIII Stage 3 follow-on correction (2026-09-14): was
* vm_log_attributed_vm() -- wrong source. That tracks nested
* vm_interpret() calls for log attribution and has no way to know a
* VM is running while parked inside switch.c's own raw-C trampoline
* (sk_vm_switch_entry()), which never itself calls vm_interpret(). A
* VM that only ever ran the trampoline was therefore never seen as
* "current," so its own readiness/has_work below never reset --
* tick() re-armed the very next timer tick, forever, once nothing
* else happened to be independently flipping attribution around it
* (found live: a switch storm, 101% CPU, log frozen). sk_vm_switch_
* current_vm() is authoritative instead: the switch mechanism itself
* updates it on every switch, so it reflects who is physically
* running regardless of whether that VM ever calls vm_interpret(). */
current = sk_vm_switch_current_vm();
if (!current) return; /* boot: no switch has happened yet, not seeded */
current_slot = slot_for_vm_id(current->stadium_vm_id);
if (current_slot < 0) return; /* current VM isn't a registered participant */
g_current_slot_cached = current_slot;
g_ticks_since_switch++;
for (i = 0; i < g_slot_count; i++) {
if (i == current_slot) {
g_slots[i].readiness = 0;
g_slots[i].has_work = 0;
} else {
g_slots[i].readiness++;
}
}
if (!g_pending) {
for (i = 0; i < g_slot_count; i++) {
if (i != current_slot &&
g_slots[i].readiness >= SK_SWITCH_READINESS_THRESHOLD &&
g_slots[i].has_work) {
g_pending = 1;
g_pending_target = g_slots[i].vm_id;
break;
}
}
}
int sk_vm_switch_signal_is_participant(VMUuid vm_id) {
return slot_for_vm_id(vm_id) >= 0;
}
VMUuid sk_vm_switch_signal_take_pending(void) {
if (!g_pending) return vm_uuid_none();
g_pending = 0;
return g_pending_target;
uint64_t sk_vm_switch_signal_next_stamp(void) {
return g_stamp_next++;
}
void sk_vm_switch_signal_note_parked(VMUuid vm_id) {
int slot = slot_for_vm_id(vm_id);
if (slot >= 0) g_slots[slot].parked_stamp = g_stamp_next++;
}
void sk_vm_switch_signal_note_beat(void) {
g_beats_since_switch++;
g_decision_due = 1;
}
void sk_vm_switch_signal_note_mail(void) {
g_decision_due = 1;
}
int sk_vm_switch_signal_take_decision_due(void) {
int due = g_decision_due;
g_decision_due = 0;
return due;
}
/* One candidate's physical standing: its heat, and how long it has been
* waiting (the oldest of its first pending message's admission stamp and
* the stamp from when it last stopped running). Returns 0 if `v` has no
* work at all -- not eligible. */
static int candidate_standing(VM *v, VM *mama, uint64_t parked_stamp,
uint64_t *heat_out, uint64_t *stamp_out) {
SkHermesMessage *m = sk_hermes_pending_peek(v->stadium_vm_id);
int eligible = 0;
uint64_t stamp = UINT64_MAX;
if (m) { /* has mail */
eligible = 1;
stamp = m->wait_stamp;
}
if (v->native_stack_saved_sp != 0) { /* parked on its own stack */
const VMFrameStack *fs = &v->frames;
int in_progress = fs->count > 0 &&
fs->entries[fs->count - 1].kind != VM_FRAME_IDLE;
/* §3.13.5 / ruling C-5: pending console input is Hera's mail. */
int console_input = (v == mama) && sk_console_key_available();
if (in_progress || console_input) {
eligible = 1;
if (parked_stamp != 0 && parked_stamp < stamp) stamp = parked_stamp;
}
}
if (!eligible) return 0;
*heat_out = vm_physics_heat_of(v->stadium_vm_id);
*stamp_out = stamp;
return 1;
}
VM *sk_vm_switch_decide(VM *self, int self_competes) {
VM *mama = (VM *)sk_get_mama_vm();
VM *best = (VM *)0;
uint64_t best_heat = 0, best_stamp = 0;
int i;
for (i = 0; i < g_slot_count; i++) {
VMRegistryEntry e;
VM *v;
uint64_t heat, stamp;
if (self && vm_uuid_equal(g_slots[i].vm_id, self->stadium_vm_id)) continue;
if (capsule_vm_registry_get(g_slots[i].vm_id, &e) != 0) continue;
v = (VM *)e.vm_ptr;
if (!v || v == self || e.pending_reap) continue;
if (e.state == VM_STATE_DEAD || e.state == VM_STATE_STILLBORN) continue;
if (v->native_stack_top == 0) continue;
if (!candidate_standing(v, mama, g_slots[i].parked_stamp, &heat, &stamp)) continue;
/* Coolest wins; among equally cool, the one waiting longest.
* Stamps are unique, so the winner doesn't depend on the order
* this loop walks the table in -- the "not a scheduler" property
* (§3.13.3), checked directly by C-b's walk-order probe. */
if (!best || heat < best_heat || (heat == best_heat && stamp < best_stamp)) {
best = v;
best_heat = heat;
best_stamp = stamp;
}
}
/* A running VM that still has work keeps the CPU unless the best
* candidate is strictly cooler: a tie with the incumbent is "no reason
* to move," not a coin toss (ruling C-3). */
if (best && self_competes && !(best_heat < vm_physics_heat_of(self->stadium_vm_id))) {
return (VM *)0;
}
return best;
}
void sk_vm_switch_reap_pending(void) {
VM *current = sk_vm_switch_current_vm();
int i;
/* One per call: capsule_vm_force_reap() unregisters the VM, which
* compacts this table under the walk. Every safe point calls this, so
* any further pending reaps are picked up at the next one. */
for (i = 0; i < g_slot_count; i++) {
VMRegistryEntry e;
if (capsule_vm_registry_get(g_slots[i].vm_id, &e) == 0 &&
e.pending_reap && e.vm_ptr != (void *)current) {
capsule_vm_force_reap(e.vm_id);
return;
}
}
}
void sk_vm_switch_signal_note_switch_performed(VMUuid target_id) {
/* FABRIC-3.md §XXVIII Stage 3 follow-on correction (2026-09-14):
* reset the target's own readiness/has_work HERE, deterministically,
* at the switch decision point -- not by waiting for tick()'s
* current-slot polling to observe it running. That polling depends on
* sk_vm_switch_current_vm() catching a real ISR tick during whatever
* window the target is genuinely "current" for, and for a VM whose
* only production behavior is switch.c's placeholder trampoline (an
* immediate yield-back), that window can be as small as a few
* instructions -- too narrow for a timer tick to reliably land in.
* Without this, the target's eligibility never clears, tick() re-arms
* on the very next timer tick, and the result is a continuous switch
* storm (found live: 101% CPU, zero forward progress). This makes
* correctness independent of ISR timing; current_slot (via
* sk_vm_switch_current_vm()/tick()) remains useful as a DoE-observable
* value and for excluding the running slot from readiness accrual,
* but is no longer load-bearing for storm prevention. */
int slot = slot_for_vm_id(target_id);
if (slot >= 0) {
g_slots[slot].readiness = 0;
g_slots[slot].has_work = 0;
}
/* Stage C-b: counters only -- there is no readiness left to reset. */
(void)target_id;
g_switch_count++;
g_ticks_since_switch = 0;
g_beats_since_switch = 0;
}
void sk_vm_switch_signal_mark_work(VMUuid vm_id) {
int slot = slot_for_vm_id(vm_id);
if (slot < 0) return;
g_slots[slot].has_work = 1;
/* Stage C-b: eligibility is read from the kernel-Hermes queue itself,
* so there is no has_work flag to set. Kept (SWITCH-MARK-WORK still
* calls it) as a "something changed, decide again" hint; Stage I
* strips it if nothing calls it. */
(void)vm_id;
g_decision_due = 1;
}
uint64_t sk_vm_switch_signal_switch_count(void) {
@@ -253,11 +299,12 @@ uint64_t sk_vm_switch_signal_switch_count(void) {
}
uint32_t sk_vm_switch_signal_ticks_since_switch(void) {
return g_ticks_since_switch;
return g_beats_since_switch;
}
int sk_vm_switch_signal_current_slot(void) {
return g_current_slot_cached;
VM *current = sk_vm_switch_current_vm();
return current ? slot_for_vm_id(current->stadium_vm_id) : -1;
}
int sk_vm_switch_signal_slot_count(void) {
@@ -268,15 +315,4 @@ int sk_vm_switch_signal_slot_capacity(void) {
return g_slot_capacity;
}
uint32_t sk_vm_switch_signal_readiness(int slot) {
if (slot < 0 || slot >= g_slot_count) return 0;
return g_slots[slot].readiness;
}
uint32_t sk_vm_switch_signal_readiness_of(VMUuid vm_id) {
int slot = slot_for_vm_id(vm_id);
if (slot < 0) return 0;
return g_slots[slot].readiness;
}
#endif /* __STARKERNEL__ */
+5 -9
View File
@@ -216,16 +216,12 @@ void doe_log_tick_row(VM *vm, const HeartbeatTickSnapshot *snap)
int switch_slot = sk_vm_switch_signal_current_slot();
unsigned switch_current_slot = (switch_slot >= 0) ? (unsigned)switch_slot : 0xFFFFFFFFu;
uint32_t switch_ticks_since = sk_vm_switch_signal_ticks_since_switch();
/* FABRIC-3.8.md §3.13.4 (Stage C-b): readiness no longer exists (the
* timer-driven accrual it measured is removed). The three columns are
* kept, always 0, so the CSV's shape and every existing reader stay
* valid; Stage I decides whether to drop them. switch_ticks_since above
* now counts beats of the running VM since the last switch. */
uint32_t hera_readiness = 0, hermes_readiness = 0, artemis_readiness = 0;
{
VMRegistryEntry e;
if (capsule_vm_find_by_name_nocase("Hera", &e) == 0)
hera_readiness = sk_vm_switch_signal_readiness_of(e.vm_id);
if (capsule_vm_find_by_name_nocase("Hermes", &e) == 0)
hermes_readiness = sk_vm_switch_signal_readiness_of(e.vm_id);
if (capsule_vm_find_by_name_nocase("Artemis", &e) == 0)
artemis_readiness = sk_vm_switch_signal_readiness_of(e.vm_id);
}
char buf[DOE_BUF_SIZE];
snprintf(buf, sizeof(buf),
+5 -14
View File
@@ -36,7 +36,6 @@
*/
#include "starkernel/timer.h"
#include "starkernel/capsule_vm_switch_signal.h" /* FABRIC-3.md §XXVIII Stage 3 */
#if defined(ARCH_AMD64)
#define HEARTBEAT_HAS_VARIANCE 1
@@ -164,19 +163,11 @@ void heartbeat_tick(void)
g_heartbeat.ticks++;
g_pending_valid = 1;
/* FABRIC-3.md §XXVIII, Stage 3 (2026-09-13): the one deliberate,
* documented amendment to this file's own top-half/bottom-half
* discipline above. sk_vm_switch_signal_tick() reaches into VM
* scheduling state from real ISR context -- something this file's
* header comment previously said nothing here did. It stays cheap
* (bounded iteration over a small slot table, no locking needed for
* the same reason nothing else here needs it: single hart, nothing
* concurrent with this ISR while it runs) and it never itself
* switches anything -- it only ever sets a flag, consumed later at a
* safe cooperative checkpoint on the mainline
* (execute_colon_word()). The GAP-A1 split itself is not overturned:
* everything else in this function is exactly as minimal as before. */
sk_vm_switch_signal_tick();
/* FABRIC-3.8.md §3.13.4 (Stage C-b, 2026-09-23): the FABRIC-3.md
* §XXVIII Stage 3 amendment that called sk_vm_switch_signal_tick()
* from here is removed. Captain Bob: "the timer is NOT the source of
* preemption, compudynamics is." This ISR is back to its original
* minimal top half; no switch decision reads anything it produces. */
}
void heartbeat_service(void)
+15 -1
View File
@@ -46,6 +46,8 @@
#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" /* FABRIC-3.8.md §3.13.5 -- sk_vm_idle_point() */
#include "starkernel/capsule_vm_switch_signal.h" /* §3.13.5 -- pump skips participants */
#include "starkernel/vm/stadium.h" /* task 3.8 -- stadium_conserved() evidence print */
#include "block_subsystem.h"
#include "word_source/include/keyboard_words.h"
@@ -912,9 +914,15 @@ static void sk_repl_idle(VM *active_vm)
for (i = 0; i < count; i++) {
VMRegistryEntry ent;
/* FABRIC-3.8.md §3.4 / §3.13.5 (Stage C-b, G3): switch
* participants are skipped -- their mail is delivered on their
* own stacks, by a switch (sk_vm_idle_point() and the trampoline),
* never drained here on Hera's. Only VMs not yet participants
* (Stage E makes every born VM one) are still pumped here. */
if (capsule_vm_registry_get_by_index(i, &ent) == 0 &&
ent.state == VM_STATE_LIVE &&
ent.vm_ptr != (void *)mama) {
ent.vm_ptr != (void *)mama &&
!sk_vm_switch_signal_is_participant(ent.vm_id)) {
(void)sk_hermes_drain_checkpoint((VM *)ent.vm_ptr);
}
}
@@ -1081,6 +1089,7 @@ int sk_console_getkey(VM *active_vm)
VM *live_active = g_repl_active_vm ? g_repl_active_vm : (VM *)sk_get_mama_vm();
sk_repl_idle(live_active);
}
sk_vm_idle_point(); /* FABRIC-3.8.md §3.13.5 -- acts only between lines */
arch_relax();
}
}
@@ -1104,6 +1113,7 @@ void sk_repl_headless_wait(VM *mama)
g_last_beat_tick = now;
sk_repl_idle(mama);
}
sk_vm_idle_point(); /* FABRIC-3.8.md §3.13.5 */
arch_relax();
}
}
@@ -1303,6 +1313,10 @@ int sk_console_readline(char* buf, int size, VM* active_vm, int reanchor_prompt)
* On real hardware a wfi/hlt would be appropriate; add it here
* under an #ifdef REAL_HARDWARE guard when that path is needed.
*/
/* FABRIC-3.8.md §3.13.5 (Stage C-b): Hera waiting for a line is
* the idle point -- the coolest VM with work runs now, not at
* the next SK_IDLE_BEAT_INTERVAL (that cadence is the timer's). */
sk_vm_idle_point();
arch_relax(); /* PAUSE — reduce power, maintain tight poll */
continue;
}
+7
View File
@@ -52,6 +52,7 @@
#include "starkernel/kmalloc.h"
#include "console.h"
#include "vm.h" /* task 3.4 -- VM struct fields, vm_interpret() */
#include "starkernel/capsule_vm_switch_signal.h" /* FABRIC-3.8.md §3.13.3 -- wait stamps */
/* Freestanding: no libc printf. Prints an unsigned decimal, no leading
* zeros -- same small helper stadium.c/capsule_vm_switch_signal.c each
@@ -531,6 +532,12 @@ int sk_hermes_send_one(VMUuid from, VMUuid to, uint32_t type, uint32_t channel_i
return -1;
}
/* FABRIC-3.8.md §3.13.3 (Stage C-b): stamped only once admitted, and
* the switch decision told something changed -- the recipient may now
* be the coolest VM with work. */
msg->wait_stamp = sk_vm_switch_signal_next_stamp();
sk_vm_switch_signal_note_mail();
return 0;
}
+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);
+17 -25
View File
@@ -967,32 +967,14 @@ void execute_colon_word(VM* vm)
* stack AND is at its own outermost frame AND is not compiling --
* instead of the global depth count, which stops being meaningful
* once VMs genuinely interleave on their own native stacks. */
/* FABRIC-3.8.md §3.13.3 (Stage C-b, 2026-09-23): no longer a
* timer-armed pending target -- the compudynamic decision itself,
* re-made only after a beat of this VM or new mail. The pending_reap
* handling that lived inline here (FABRIC-3.md §XXVIII Stage 4) is
* now sk_vm_switch_reap_pending(), run at every safe point. */
if (sk_vm_owns_outermost_frame(vm)) {
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. */
}
}
}
sk_vm_switch_checkpoint(vm);
/* Resumes here once something later switches back. */
}
#endif
@@ -1288,6 +1270,16 @@ void vm_interpret_kind(VM* vm, const char* input, uint32_t kind)
vm_heartbeat_run_cycle(vm);
vm->heartbeat.check_counter = 0;
}
#ifdef __STARKERNEL__
/* FABRIC-3.8.md §3.13.3 (Stage C-b): between two top-level words
* is a clean switch point too -- without it, a payload made only
* of primitives (no colon word, so no execute_colon_word()
* checkpoint) could never be displaced. Same ownership gate. */
if (!vm->error && !vm->abort_requested && sk_vm_owns_outermost_frame(vm)) {
sk_vm_switch_checkpoint(vm);
}
#endif
}
g_vm_interpret_depth--;
+4
View File
@@ -52,6 +52,7 @@
#include "vm_internal.h"
#include "starkernel/capsule_vm_physics.h"
#include "starkernel/vm/switch.h" /* sk_vm_switch_current_vm() -- FABRIC-3.8.md §3.13.2 */
#include "starkernel/capsule_vm_switch_signal.h" /* note_beat() -- §3.13.3 */
#include "starkernel/timer.h"
#include <errno.h>
@@ -150,6 +151,9 @@ void vm_tick(VM* vm)
* executes can ask for, skip, or give back a charge. */
if (vm == sk_vm_switch_current_vm()) {
vm_physics_charge(vm->stadium_vm_id);
/* Stage C-b (§3.13.3): the running VM's heat just changed, so the
* next owned checkpoint re-decides who runs. */
sk_vm_switch_signal_note_beat();
}
/* Plugin 2: System State Monitoring (Future) */