FABRIC-3.8.md Stage FR: VM frame stacks, each frame with an ID
Captain Bob: "give frames IDs and call by ID" -- frames stack per VM, each
with an ID. New vm_frame.h/.c: VMFrameEntry { VMFrameID id; VMUuid vm_id;
kind (INTERPRET/DRAIN/IDLE) }, a growable VMFrameStack embedded as VM.frames
(reserved at birth, freed in vm_cleanup, grown by allocate-copy-free).
vm_interpret() -> vm_interpret_kind() pushes a frame onto the running stack's
owner, holds only its ID, fails closed if it cannot grow, and pops by ID
(mismatch reported as VM FRAME CORRUPTION).
The Stage A/B loose fields are gone and everything is derived from the stack:
- ownership gate: one frame, and it is vm's own
- drain guard: a DRAIN frame for vm already on the running stack
- parked-idle: an IDLE frame the trampoline pushes around its yield
- log attribution: the top frame's vm_id, resolved by ID through the registry
The single saved-and-restored attribution global is removed.
Evidence: the probe baseline on the pre-frame code read HSSAS (the global
named Hestia while Hera resumed). With frames it reads HSHAS on all three
ISAs, with Hestia's top frame IDLE and Hera's frames back to 0; the probe is
reverted. Formal acceptance on all three ISAs: dict_hash identical and
unchanged, zero UNKNOWN WORD, faults, frame corruption or refused interprets,
and the WIREBIND relay 1 2 + . -> 3 works.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
56da55aba9
commit
6a313c7bf2
+110
-3
@@ -1,7 +1,7 @@
|
||||
# FABRIC-3.8.md — Real concurrency: finishing what the reshuffle started
|
||||
|
||||
**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
|
||||
**Status: OPEN — Stages A, B and FR (VM frames) done (§6); C–I not started. §3.11 ruled
|
||||
(coolest VM with pending work, by physical comparison; no scheduler).** 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).
|
||||
|
||||
@@ -276,7 +276,18 @@ the OS sees an empty queue (§3.2), and every switch decision is kernel-side.
|
||||
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
|
||||
**RULED 2026-09-23, Captain Bob:** *"VM frames first, then coolest VM with work goes next"* —
|
||||
and, immediately after: ***"no scheduler allowed."*** So the switch decision at Stage C is a
|
||||
**physical comparison made at the ownership-gated checkpoint**. Among VMs with pending work, the
|
||||
one with the lowest heat is favored over the one running, the same least-dense judgment
|
||||
`stadium_admit()` already makes for eviction and the §XIX turn-attractor argument. It is **not**
|
||||
a scheduler: no run queue, no priority list, no ordering data structure, no component that owns
|
||||
or computes an order, nothing that runs on a timer. The comparison is re-evaluated only at the
|
||||
moments the physics already exposes (checkpoints, the trampoline's between-message point, idle
|
||||
points), and the heat it reads is the heat the fleet already conserves. The per-tick readiness
|
||||
accrual and the fixed `50` threshold are removed at Stage C, not tuned.
|
||||
|
||||
~~**This is a design question for Captain Bob before Stage C**~~ (settled above) (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)
|
||||
@@ -306,6 +317,71 @@ saved global. The vendored-source statics are `FABRIC-4.md`'s scope, but the fra
|
||||
go. **Should land before or with Stage C**, since Stage C is the first stage where switches
|
||||
happen for real.
|
||||
|
||||
**RULED 2026-09-23: VM frames land first, as Stage FR, before Stage C.** Concrete design:
|
||||
|
||||
- **`include/starkernel/vm/vm_frame.h`**, new: `typedef struct VMFrame { int interpret_depth;
|
||||
int hermes_draining; int parked_idle; struct VM *attrib_vm; } VMFrame;`. `VM` gets one field,
|
||||
`VMFrame frame;`. The three loose fields Stages A and B added (`interpret_depth`,
|
||||
`hermes_draining`, `parked_idle`) **move into it**, since they are exactly this cluster. All
|
||||
their uses are renamed; behavior is unchanged.
|
||||
- **Stack-scoped log attribution.** `attrib_vm` lives in the frame of the VM that **owns the
|
||||
physical stack** (`sk_vm_switch_current_vm()`), and means "the innermost VM interpreting on
|
||||
this stack." `vm_interpret(vm, …)` saves and restores `owner->frame.attrib_vm` instead of the
|
||||
global. A nested `VM-EXEC` into Artemis on Hera's stack still attributes to Artemis, exactly
|
||||
as today. `vm_log_attributed_vm()` returns `current->frame.attrib_vm` (or `current` itself when
|
||||
no interpret is live on its stack). Because each stack carries its own chain, a switch can
|
||||
never leave attribution pointing at the VM that just switched out. Before the switch
|
||||
mechanism is seeded at boot (no current VM), the existing global path is kept as the fallback.
|
||||
- **Not in the frame this stage:** `g_prng_state` (vendored `starforth_words.c`, shared with
|
||||
the hosted build, open ruling §5 item 5), the compile-time statics (covered by the Stage A
|
||||
`MODE_COMPILE` gate; `FABRIC-4.md` scope), and `g_vm_interpret_depth` (retired in Stage I).
|
||||
|
||||
**REVISED 2026-09-23, Captain Bob: *"give frames IDs and call by ID"*; chose *"frames stack per
|
||||
VM, each with an ID."*** This supersedes the single-struct shape above (written, never booted):
|
||||
|
||||
- **`VMFrameEntry { VMFrameID id; VMUuid vm_id; uint32_t kind; }`**. `id` is a system-wide
|
||||
monotonic 64-bit frame ID (0 = none, never reused). `vm_id` is the VM executing at that level,
|
||||
**held as an ID, never a pointer**, so nothing can dangle after a VM dies. `kind` is
|
||||
`VM_FRAME_INTERPRET` or `VM_FRAME_DRAIN`.
|
||||
- **`VMFrameStack { entries, count, cap, max }`**, one per VM, embedded as `VM.frames`. It holds the
|
||||
nested interpret levels running **on that VM's own native stack** (a nested `VM-EXEC` into
|
||||
Artemis on Hera's stack is a frame on *Hera's* stack whose `vm_id` is Artemis). Growable like
|
||||
`VMCallState`, but grown by allocate-copy-free rather than `sf_realloc` (which orphans the old
|
||||
block). Before the switch mechanism is seeded, a boot-time stack in `vm_core.c` stands in.
|
||||
- **`vm_interpret()` pushes one frame and pops it by ID.** It keeps its own frame's ID and, at
|
||||
exit, checks the top is that ID before popping. A mismatch is logged as frame corruption.
|
||||
That's the "call by ID" integrity check across switches.
|
||||
- **Everything the loose fields held is now derived from the stack:**
|
||||
- ownership gate (§3.1): `current == vm`, the stack holds exactly one frame, and its `vm_id` is
|
||||
`vm`;
|
||||
- drain guard (§3.3): a `VM_FRAME_DRAIN` frame for `vm` is already on the current stack (the
|
||||
drain pushes its interpret as `DRAIN` kind);
|
||||
- parked-idle (§3.5): `VM_STATE_SWITCHED_OUT` **and the top frame is `VM_FRAME_IDLE`**. The
|
||||
trampoline pushes that frame just around its idle yield and pops it on resume, *before*
|
||||
draining. An empty stack alone is not enough: Hera parked at an idle point (Stage F) has zero
|
||||
interpret frames but her whole REPL call chain live on her stack. The frame records *where*
|
||||
the VM is parked, which the flag used to;
|
||||
- log attribution: the top frame's `vm_id` on the current owner's stack, **resolved by ID**
|
||||
through the VM registry. An empty stack means NULL, which maps to `HADES`.
|
||||
- **Failure rules.** A frame push that can't grow **fails closed**: log it, set `vm->error`,
|
||||
don't interpret. Interpreting without a frame would silently break the gate and attribution.
|
||||
An initial capacity is allocated at birth so the hot path almost never allocates. A
|
||||
**pop-by-ID mismatch** means corruption (every stack is LIFO, since resume restores the owner as
|
||||
current): log both IDs, count it, and remove *our own* ID wherever it sits rather than popping
|
||||
someone else's frame. No code holds a `VMFrameEntry *` across an interpret body (growth
|
||||
copies), only the ID. The frame-ID counter has a single writer: `vm_interpret()`, mainline
|
||||
only, with no interrupt-context caller (checked by grep).
|
||||
|
||||
**Evidence, named in advance (probe, reverted after capture).** Two temporary C words, `SB-GO`
|
||||
(switch to Hestia) and `SB-BACK` (switch back to whoever switched in), plus `SB-ATTRIB` (record
|
||||
`vm_log_attributed_vm()`'s name). These are *probe-only* control words, never committed; they
|
||||
exist to force the interleaving that Stage C will produce naturally. Hestia is queued a
|
||||
message `SB-ATTRIB SB-BACK SB-ATTRIB`, then Hera runs `SB-ATTRIB SB-GO SB-ATTRIB SB-GO`. The
|
||||
recorded sequence must be **Hera, Hestia, Hera, Hestia**. The third entry is the discriminator:
|
||||
Hera resuming while Hestia is still parked mid-payload. **Baseline first:** the same probe on
|
||||
the pre-frame code must *fail* on that third entry (reading Hestia), proving the test detects
|
||||
the defect before the fix is claimed.
|
||||
|
||||
---
|
||||
|
||||
## 4. Staged plan
|
||||
@@ -318,6 +394,7 @@ healthy idle produce the same serial log (`FABRIC-3.md` §XXVIII.1).
|
||||
|---|---|---|
|
||||
| **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 **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. |
|
||||
| **FR** | §3.12 VM frames: `VMFrame` header; Stage A/B fields move into it; stack-scoped log attribution. | Probe `SB-ATTRIB`/`SB-GO`/`SB-BACK` (reverted): attribution sequence Hera, Hestia, **Hera**, Hestia on all three ISAs, and the **same probe fails on the pre-frame baseline** (third entry reads Hestia). Then formal acceptance: `dict_hash` unchanged, relay works. |
|
||||
| **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. |
|
||||
@@ -410,3 +487,33 @@ explicit instruction.
|
||||
`[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`.
|
||||
- [x] **Stage FR — VM frames: a frame stack per VM, each frame with an ID (§3.12).** 2026-09-23.
|
||||
Captain Bob: *"give frames IDs and call by ID"*, *"frames stack per VM, each with an ID."*
|
||||
New `include/starkernel/vm/vm_frame.h` / `src/starkernel/vm/vm_frame.c`: `VMFrameEntry`
|
||||
`{ VMFrameID id; VMUuid vm_id; kind }` (kinds `INTERPRET`/`DRAIN`/`IDLE`); `VMFrameStack`
|
||||
embedded as `VM.frames`, reserved at birth (`vm_init_with_host`), freed in `vm_cleanup`,
|
||||
grown by allocate-copy-free. `vm_interpret()` → `vm_interpret_kind()`: pushes one frame onto
|
||||
the running stack's owner (`vm_frame_current_stack()`; a boot stack before seeding), holds
|
||||
only its ID, **fails closed** if the push can't grow, and **pops by ID** at exit (a mismatch
|
||||
is printed as `VM FRAME CORRUPTION` and only our own frame is removed). The Stage A/B loose
|
||||
fields are gone, all derived now: the ownership gate is "the owner's stack holds exactly one
|
||||
frame and it's `vm`'s"; the drain guard is "a `DRAIN` frame for `vm` is on the running
|
||||
stack"; parked-idle is a `VM_FRAME_IDLE` frame the trampoline pushes around its yield and
|
||||
pops before draining; log attribution is the top frame's `vm_id`, **resolved by ID** through
|
||||
the VM registry (empty stack → NULL → `HADES`). The attribution global is removed.
|
||||
**Evidence, as named in §4:**
|
||||
- **Baseline (pre-frame code, probe only):** `logs/20260923-150901/amd64/` →
|
||||
`attribution=HSSAS want=HSHAS FAIL`. The old global named Hestia while Hera resumed,
|
||||
which is exactly the defect.
|
||||
- **With frames, probe on all three ISAs:** `logs/20260923-152109/amd64/`,
|
||||
`-152424/aarch64/`, `-152828/riscv64/` → `attribution=HSHAS hestia_idle_top=1
|
||||
hera_frames=0 PASS`. That covers interleaved attribution, nested `VM-EXEC` into Artemis
|
||||
still attributed to Artemis, Hestia parked with an `IDLE` top frame, and Hera's frames
|
||||
back to zero. The probe (`SB-GO`/`SB-BACK`/`SB-ATTRIB`, probe-only control words) was
|
||||
reverted (`git checkout -- src/starkernel/kernel_main.c`).
|
||||
- **Formal acceptance without the probe, coupled images reset before each:**
|
||||
`logs/20260923-153227/amd64/`, `-153644/aarch64/`, `-154208/riscv64/`. All reach
|
||||
`[zuse@Hera] ok>`, with zero `UNKNOWN WORD`/failures/faults, **zero `VM FRAME CORRUPTION`
|
||||
and zero refused interprets**. The `dict_hash` set is identical across ISAs and unchanged
|
||||
from Stages A/B. The full mint → WIREBIND → `USE` → `1 2 + .` → `3` relay works (the path
|
||||
with a nested frame on Hera's stack). Clean build on all three plus hosted, no new warnings.
|
||||
|
||||
Reference in New Issue
Block a user