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
@@ -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