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:
co-authored by
Claude Opus 5.5
parent
2aa338483a
commit
56da55aba9
+85
-2
@@ -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`.
|
||||
|
||||
Reference in New Issue
Block a user