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:
co-authored by
Claude Opus 5.5
parent
7c3736841d
commit
06bcab504d
@@ -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__ */
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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--;
|
||||
|
||||
@@ -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) */
|
||||
|
||||
Reference in New Issue
Block a user