FABRIC-3.8.md Stage B: kernel-side idle loop in the switch trampoline

sk_vm_switch_entry() stops being the Stage-2 placeholder bounce and becomes
every switch participant's own idle loop: it drains one of its own
kernel-Hermes messages per pass, on its own native stack (driven by the drain's
return value, so a non-consuming drain cannot spin), and when nothing is
drained it marks the VM parked_idle and yields back. It is entirely OS-driven,
with no VM-side yield word. sk_hermes_drain_checkpoint() gains a per-VM
hermes_draining guard so a payload's own in-word checkpoint cannot drain a
second message inside the first. Still inert in normal operation: nothing
marks work until Stage C.

Finding fixed here: amd64 sk_vm_switch_prime built the first-entry frame at
top-56, entering with rsp 16-aligned instead of the SysV-required 8 mod 16, so
the first aligned SSE spill in a callee took #GP (logs/20260923-142526, kept).
It had been latent since SXXVIII Stage 2 because the placeholder never called
anything. The frame is now at top-64 with a zeroed pad slot. aarch64 and
riscv64 were already correct.

Evidence: a temporary kernel_main probe (reverted, not in this commit) queued
two messages to Hestia, the first calling a colon word, and switched into her.
It passed on all three ISAs: 0 pending, parked_idle, depth 0, context saved on
her own stack, SB-SEQ=12 (drained one at a time, not nested), ledger and
stadium_conserved restored. Formal acceptance without the probe on all three
ISAs: dict_hash identical and unchanged, zero UNKNOWN WORD or faults, and the
live WIREBIND mint -> USE -> 1 2 + . -> 3 relay.

FABRIC-3.8.md also records Captain Bob's rulings that no VM-side word ever gives
up control and that multitasking is a compudynamic hybrid, and opens SS3.12
(VM frames for per-VM execution state).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Robert Allan James
2026-09-23 14:57:51 -04:00
co-authored by Claude Opus 5.5
parent 2aa338483a
commit 56da55aba9
20 changed files with 64228 additions and 5 deletions
+85 -2
View File
@@ -1,6 +1,6 @@
# FABRIC-3.8.md — Real concurrency: finishing what the reshuffle started
**Status: OPEN — Stage A done (§6); Stages B–I not started. §3.11 (compudynamic preemption) needs
**Status: OPEN — Stages A and B done (§6); Stages C–I not started. §3.11 (compudynamic preemption) needs
a ruling before Stage C.** Each stage below needs Captain
Bob's explicit go, gets its own commit, and gets its own three-architecture acceptance (`clean
qemu`, amd64 → aarch64 → riscv64, foreground, visible framebuffer, one at a time).
@@ -251,9 +251,61 @@ one (e.g. cooler per §XIX), and never merely because N timer ticks have elapsed
would then keep only what physics needs from it, and nothing that decides turns. The fixed `50`
threshold and the per-tick readiness accrual go away.
**Clarified 2026-09-23, Captain Bob:** *"this is kind of like a mixture of preemptive and
cooperative multitasking… It's the compudynamics that drives the whole thing. Not necessarily a
preemptive timer. Not necessarily cooperatively, as in 'when I finish my task, I move on'. But
it's a mixture of both."* Read as:
- **Where a switch may happen is cooperative in shape.** Only at a VM's own clean boundary, the
ownership-gated word checkpoint (§3.1): stack owned, own outermost frame, not compiling. Never
at an arbitrary instruction, never because an interrupt fired.
- **Whether, and to whom, is preemptive in effect.** A running VM doesn't hold the CPU until it
finishes. At any such boundary it can be displaced mid-task when the compudynamics favors
another VM with pending work.
- **Running out of work parks a VM.** That's the plain cooperative case (§3.2's idle loop).
- **Neither a clock nor task completion decides. The physics does.**
**Constraint, Captain Bob 2026-09-23:** *"make sure nothing inside of a given execution state or
VM controls this. There are no special words saying I'm giving up control or anything like
that. This is strictly all operating system and strictly all compudynamically driven."* No yield
word, no `PAUSE`, no "I'm done" word, anywhere. Parking happens in the kernel's trampoline when
the OS sees an empty queue (§3.2), and every switch decision is kernel-side.
**Also a Stage C input:** the trampoline's point between two messages is a clean switch point
(own stack, depth 0). The compudynamic decision has to be consulted there too, or a VM whose queue
holds only checkpoint-free payloads (primitive words, no colon calls) never yields. Noted in
`sk_vm_switch_entry()`'s own comment; not built in Stage B.
**This is a design question for Captain Bob before Stage C** (Stage C is the first stage where
anything actually switches). Stages A and B don't depend on it.
### 3.12 VM frames: per-VM execution state that travels with the VM (OPEN, raised by Captain Bob)
**Captain Bob, 2026-09-23:** *"I'm thinking we may need VM frames for state."* Once VMs genuinely
interleave, any execution context that isn't already inside the `VM` struct or on the VM's own
native stack is shared across VMs, and it goes wrong at the first switch that lands inside it.
The Stage B probe hit an instance of this class on its first run (§6, the amd64 alignment finding
is the same family: state the first-entry frame should have carried and didn't).
**Known members of that class today, traced:**
| State | Where | Why it breaks under interleaving |
|---|---|---|
| `g_log_attrib_vm` | `vm_core.c` (save/restore in `vm_interpret()`) | Save/restore assumes last-in-first-out nesting. Hera switches out mid-interpret; Hestia's `vm_interpret()` saves `prev=Hera`; Hestia switches back mid-payload → attribution stays "Hestia" while Hera runs. `(LOG-APPEND-RAW)` takes its source from this, so persisted log records name the wrong VM (defeating `FABRIC-3.md` §XXVII's anti-spoof rule). |
| `g_vm_interpret_depth` | `vm_core.c` | Same LIFO assumption; superseded by per-VM `interpret_depth` (Stage A) and retired in Stage I. |
| `g_switch_entry_vm` | `switch.c` | Read once at first entry; safe only while first entries can't overlap. |
| `g_prng_state` | `starforth_words.c:71` | One `RANDOM` stream for every VM (G8). |
| Compile-time control-flow and vocabulary statics | vendored `src/word_source/` (Isabelle sweep memory) | Shared across VMs; kept safe for switches by the Stage A `MODE_COMPILE` gate, not by ownership. |
**Shape, for ruling (not decided):** a `VMFrame` struct in its own header (per
`feedback_dedicated_headers_not_inline_fields`: related state gets a header, not loose `VM`
fields), embedded in `VM`, holding the per-VM execution context that is global today. The
switch primitive then needs to swap nothing extra: whatever lives in the frame moves with the VM
by construction. Log attribution becomes "the frame of `sk_vm_switch_current_vm()`" rather than a
saved global. The vendored-source statics are `FABRIC-4.md`'s scope, but the frame is where they'd
go. **Should land before or with Stage C**, since Stage C is the first stage where switches
happen for real.
---
## 4. Staged plan
@@ -265,7 +317,7 @@ healthy idle produce the same serial log (`FABRIC-3.md` §XXVIII.1).
| Stage | Change | Evidence that proves it (named in advance) |
|---|---|---|
| **A** | §3.1 per-VM ownership gate (+ `MODE_COMPILE` gate). Nothing else. | Byte-identical behavior: `dict_hash` triple unchanged, identical boot, WIREBIND mint→attach→`USE`→relay (the `FABRIC-3.6.md` Phase 4 acceptance sequence) still `1 2 + .` → `3`. |
| **B** | §3.2 trampoline idle loop + §3.3 `hermes_draining` + §3.5 parked-idle flag. Still inert (nothing marks work yet). | Same as A: nothing can reach the new trampoline branch yet, so behavior is identical. A synthetic self-test (kernel_main.c, the §XXVIII Stage 2 `SWITCH-TEST` pattern: write, capture, revert) switches into Artemis with one message queued and confirms she drained it on her own stack and parked-idle. |
| **B** | §3.2 trampoline idle loop + §3.3 `hermes_draining` + §3.5 parked-idle flag. Still inert (nothing marks work yet). | Same as A: nothing can reach the new trampoline branch yet, so behavior is identical. A synthetic self-test (kernel_main.c, the §XXVIII Stage 2 `SWITCH-TEST` pattern: write, capture, revert) switches into **Hestia** (not Artemis, whom the Zuse attach flow `VM-EXEC`s; a parked VM refuses `VM-EXEC`) with **two** messages queued, the first calling a colon word so the in-word drain checkpoint is actually reached. The ordering witness `SB-SEQ` must equal 12 (one-at-a-time drain), not 21 (nested). She must end parked-idle at depth 0 with her context saved on her own stack, current VM back to Hera, and the ledger and `stadium_conserved()` restored. |
| **C** | §3.4 eligibility marked + pump skips participants (one commit). | The WIREBIND relay still works, now delivered by a real switch: `switch_count_cumulative` increments across the relay and the `~user` VM's own drain line appears. The ledger and `stadium_conserved()` hold (`fleet_conserved` is not evidence). No `native_stack_saved_sp` ever equals an address inside Hera's stack range (temporary probe). |
| **D** | §3.5 kill of parked-idle + unregister in `capsule_vm_kill()`. | Birth, message, idle-park, `KILL` succeeds; the switch-signal slot count returns to its prior value. |
| **E** | §3.6 generic registration in `capsule_birth_baby()`, duplicate call sites removed. | The Tripod and WIREBIND VMs still register exactly once (slot count), and a `BIRTH`-ed VM appears as a participant. |
@@ -327,3 +379,34 @@ explicit instruction.
Stage C). With the new gate, checkpoints inside that relayed payload now correctly **fail**
the ownership test (Hera owns the stack, not `rajames~user`). Before Stage A they passed on
the global depth count, which was the latent §1 hazard.
- [x] **Stage B — trampoline idle loop, one drain at a time, parked-idle (§3.2/§3.3/§3.5).**
2026-09-23. `include/vm.h`: `VM.hermes_draining`, `VM.parked_idle`. `kernel_hermes.c`:
`sk_hermes_drain_checkpoint()` refuses while `hermes_draining` is set, and sets and clears it
around its `vm_interpret()` (set only once a message is in hand; single exit). `switch.c`:
`sk_vm_switch_entry()` is now each participant's kernel-side idle loop. It drains one of its
own messages per pass, driven by the drain's return value, not a separate pending probe
(so a non-consuming drain can't spin with no checkpoint to break it). When nothing is
drained, it sets `parked_idle` and yields to `native_switch_back_to`; a failed yield panics.
Inert in normal operation: nothing marks work yet (Stage C), so no real boot enters it.
**Finding, fixed in this stage: amd64 first-entry stack misalignment.** The first probe boot
(`logs/20260923-142526/amd64/`, kept) took **#GP (vector 13)** the moment Hestia's trampoline
called into `vm_interpret()`. `sk_vm_switch_prime` (amd64 `switch.S`) built its frame at
`top − 56`, so the entry function ran with `rsp == top` (16-aligned). SysV needs
`rsp ≡ 8 (mod 16)` at entry, so every compiler-emitted aligned SSE spill faulted. Latent since
§XXVIII Stage 2, because the old placeholder never called anything that spilled. Fixed:
frame at `top − 64` with a zeroed pad slot. aarch64 and riscv64 return through the link
register and already land 16-aligned; no change there.
**Evidence, as named in §4:** a temporary probe (kernel_main.c, written, captured,
**reverted**; `git diff` afterward shows only `vm.h`, `kernel_hermes.c`, `switch.c` and amd64
`switch.S`) queued two messages to Hestia, the first calling a colon word, and switched
Hera→Hestia. `STAGE-B PROBE: PASS` on all three: `logs/20260923-143121/amd64/`,
`-143435/aarch64/`, `-143838/riscv64/`. Sends ok, switch ok, 0 pending, `parked_idle=1`,
depth 0, context saved on her own stack, current VM back to Hera, **`SB-SEQ = 12`** (one at a
time; a nested drain gives 21), ledger `held` restored, `stadium_conserved(hera)` true. The
probe's printed line reads `seq=ld …` because this kernel's freestanding `snprintf` has no
`%lld` and shifted the arguments after it; the PASS test compares values directly and is
unaffected. Formal acceptance with the probe gone, coupled images reset before each:
`logs/20260923-144234/amd64/`, `-144701/aarch64/`, `-145224/riscv64/`. All reach
`[zuse@Hera] ok>` with zero `UNKNOWN WORD`/failures/faults, the `dict_hash` set identical
across ISAs and unchanged from Stage A, and the full mint → WIREBIND → `USE` → `1 2 + .` →
`3` relay with `stadium_conserved(self)=true`.