FABRIC-3.8.md Stage A: per-VM stack-ownership gate for drain and switch

Replaces the global g_vm_interpret_depth test at both execute_colon_word()
checkpoints (kernel-Hermes drain, switch-signal take) with
sk_vm_owns_outermost_frame(vm): the VM physically owns the stack
(sk_vm_switch_current_vm() == vm), is at its own outermost vm_interpret()
frame (new per-VM VM.interpret_depth), and is not compiling. Once VMs
interleave on their own native stacks, a global depth count across all VMs
no longer means "this VM owns this stack"; this is the invariant it was
approximating. The global and the old function stay until Stage I.

Acceptance, all three ISAs, foreground with the framebuffer visible, coupled
disk images reset before each: zero UNKNOWN WORD, zero self-test failures,
dict_hash identical across ISAs and unchanged from the pre-Stage-A boot, and
the live WIREBIND mint -> attach -> USE -> 1 2 + . -> 3 relay with
stadium_conserved(self)=true. Logs: 20260923-140120 (amd64), -140654
(aarch64), -141303 (riscv64).

FABRIC-3.8.md also records Captain Bob's ruling that preemption comes from
the compudynamics, not the timer: principle 5 is corrected and the design
question is opened as SS3.11, to be ruled before Stage C.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Robert Allan James
2026-09-23 14:18:55 -04:00
co-authored by Claude Opus 5.5
parent 0532c1d8b8
commit 2aa338483a
12 changed files with 27609 additions and 36968 deletions
+70 -3
View File
@@ -1,6 +1,7 @@
# FABRIC-3.8.md — Real concurrency: finishing what the reshuffle started
**Status: OPEN — design only. No code written or authorized.** Each stage below needs Captain
**Status: OPEN — Stage A done (§6); Stages B–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).
@@ -79,9 +80,19 @@ VMs; execution was strictly serialized. That's `FABRIC-3.md` §XIX's own named n
4. **Generic, not DoE-shaped.** Every VM born through `capsule_birth_baby()` is a participant,
drained the same way, killable the same way. The DoE gets concurrency by using `BIRTH` and a
message send, exactly like any future program would.
5. **Nothing new decides turn order.** The existing readiness/threshold signal stays as it is. Its
5. ~~**Nothing new decides turn order.** The existing readiness/threshold signal stays as it is. Its
hardcoded `50` threshold remains the flagged seam (`FABRIC-3.5.md` §XV.3), not something this
pass tunes.
pass tunes.~~ **CORRECTED 2026-09-23, Captain Bob: *"the timer is NOT the source of
preemption, compudynamics is."*** Today's switch decision is timer-driven:
`heartbeat_tick()` (`heartbeat.c:179`, ISR context) calls `sk_vm_switch_signal_tick()`, which
adds one unit of readiness per timer tick to every non-current participant and requests a
switch once readiness passes a fixed `50` (`capsule_vm_switch_signal.c:58`, `:191–210`).
That's a time-slice scheduler with a threshold, the opposite of `FABRIC-3.5.md` §XV.3 ("turn
order is compudynamic"). **Principle 5 is replaced by:** *the decision to preempt, and whom
to run, must come from the compudynamics (the physics the fleet already computes), not from
elapsed timer ticks.* §3.11 holds the design question; nothing is decided there yet. Stage A
(the ownership gate, §3.1) is unaffected: it governs *where* a switch may safely happen,
not *whether* one should.
---
@@ -214,6 +225,35 @@ serially at birth. For the concurrent phase to be the measured phase, workers sh
- **Not fixed in this pass.** Making these per-VM is real work in vendored shared source
(`src/word_source/`), which is `FABRIC-4.md`'s declared scope.
### 3.11 Preemption comes from the compudynamics, not the timer (OPEN, Captain Bob's ruling of principle)
**Ruled:** the timer isn't the source of preemption; the compudynamics is. **Not yet
designed:** which physical quantity decides "switch now, to whom." Material already in the tree,
listed so the design starts from what exists (traced, not proposed):
- **VM-physics heat** (`execution_heat_q48`, `capsule_vm_physics.c`; FORTH `VM-HEAT`). Moves
when a VM does real work (`vm_physics_touch()`) and is conserved fleet-wide (fleet K).
`FABRIC-3.md` §XIX already argued the turn-attractor from this: *the coolest live VM with
pending work goes next*, the same least-dense judgment `stadium_admit()` uses for eviction,
applied to "whose turn."
- **Stadium heat/reservoir** (per-VM quota conservation, `stadium_conserved()`). A message's heat
is the Stadium cell it occupies (`FABRIC-3.5.md` §XL.4), so pending mail already has a physical
weight.
- **Loop #7's adaptive heartbeat** (`HeartbeatState`, per VM). This project's "one and only
clock" (memory `project_acl_ttl_overhead_final`), and adaptive rather than a fixed period.
Distinct from the hardware timer ISR (`heartbeat_ticks()`), which is what currently drives
readiness.
**The shape this implies, for ruling (not decided):** the switch decision moves out of ISR
readiness counting and into the ownership-gated checkpoint (§3.1), where it compares physics.
Switch when some VM with pending work (`has_work`) is compudynamically favored over the current
one (e.g. cooler per §XIX), and never merely because N timer ticks have elapsed. The timer ISR
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.
**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.
---
## 4. Staged plan
@@ -260,3 +300,30 @@ QMP `send-key` computing a value, per `FABRIC-3.6.md` task 1.5).
**Nothing in this document authorizes an edit.** Per Captain Bob's Law, no stage starts without an
explicit instruction.
---
## 6. Execution log
- [x] **Stage A — per-VM ownership gate (§3.1).** 2026-09-23. `include/vm.h`: new
`VM.interpret_depth`. `vm_core.c`: `vm_interpret()` raises and lowers it beside the global
counter; new `sk_vm_owns_outermost_frame(vm)` (`sk_vm_switch_current_vm() == vm &&
vm->interpret_depth == 1 && vm->mode != MODE_COMPILE`) replaces `sk_vm_at_outermost_interpret()`
at both checkpoint sites in `execute_colon_word()` (drain and switch). The global counter and
the old function stay until Stage I.
**Evidence, as named in §4:** clean build on all three ISAs with no new warnings (only
the pre-existing `mkcapsule`/linker ones), hosted build clean. Foreground, visible-framebuffer
boots, with `disk/artemis.img` + `zuse-thumb-ident.img` + `bob-thumb-ident.img` reset together
before each: `logs/20260923-140120/amd64/`, `logs/20260923-140654/aarch64/`,
`logs/20260923-141303/riscv64/`. All reach `[zuse@Hera] ok>` with zero `UNKNOWN WORD`, zero
self-test FAIL/fault/panic, and a `dict_hash` set **identical across all three and unchanged
from the pre-Stage-A boot** (`logs/20260923-110505/amd64/`): M7.1a `0x3933d76939ef66c6`, Hera
`0x27ba08011349842b`, Hestia `0xdd1aa879b3a6c5df`, Artemis `0xe9d0408b1119d51f`. The full live
WIREBIND sequence on each ISA: hot-attach blank `bob` via QMP → `MINT` (`-1`,
restricted) → detach/reattach → `WIREBIND: rajames attached` → `S" rajames" USE` →
`1 2 + .` → `3`, delivered via `Kernel-Hermes CONSOLE-CMD-EVENT` with
`stadium_conserved(self)=true`. Clean QMP `quit` each time, no leaked processes.
note: this relay still runs through the idle pump on Hera's stack (G3, unchanged until
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.
+1 -1
View File
@@ -1,5 +1,5 @@
# Capsule Block Manifest — Auto-generated
<!-- Generated by mkcapsule --manifest 2026-09-23T15:28:38Z -->
<!-- Generated by mkcapsule --manifest 2026-09-23T18:12:28Z -->
<!-- DO NOT EDIT — re-run mkcapsule --manifest to refresh. -->
<!-- Hand-written justifications and immutability notes live -->
<!-- in MANIFEST.md alongside this auto-generated index. -->
File diff suppressed because it is too large Load Diff
+8
View File
@@ -669,6 +669,14 @@ typedef struct VM
* let a later switch-in from a different VM get
* silently bounced to the original caller instead
* of itself. */
int interpret_depth; /**< FABRIC-3.8.md §3.1 (Stage A, 2026-09-23): THIS
* VM's own vm_interpret() nesting depth. Replaces
* the single global g_vm_interpret_depth as the
* ownership gate's depth test -- once VMs genuinely
* interleave on their own native stacks, a global
* count across all VMs stops meaning "this VM is
* at its own outermost frame." 0 = not
* interpreting; 1 = outermost frame. */
/** @} */
/** @name Stadium Identity (item 4.2, FABRIC-0.md §25.5)
Binary file not shown.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+34 -4
View File
@@ -70,6 +70,7 @@
* than g_vm_interpret_depth's definition (kept next to g_log_attrib_vm,
* near vm_interpret() -- see that doc comment for the full rationale). */
int sk_vm_at_outermost_interpret(void);
int sk_vm_owns_outermost_frame(VM *vm); /* FABRIC-3.8.md §3.1 -- defined near vm_interpret() */
#endif
#include "word_source/include/vocabulary_words.h"
#include "vm_internal.h"
@@ -919,8 +920,10 @@ void execute_colon_word(VM* vm)
* generalized). Placed BEFORE the switch-signal block, not after:
* sk_vm_context_switch() below does not return until something
* later switches back to `vm`, so a drain placed after it would
* silently never run on any checkpoint that switches. */
if (sk_vm_at_outermost_interpret()) {
* silently never run on any checkpoint that switches.
* FABRIC-3.8.md §3.1 (Stage A): gated on per-VM stack ownership,
* not the global depth count. */
if (sk_vm_owns_outermost_frame(vm)) {
(void)sk_hermes_drain_checkpoint(vm);
}
@@ -956,8 +959,14 @@ void execute_colon_word(VM* vm)
* switch_ticks_since to the wrong (resume) event instead of the
* actual switch-out this counts. It also resets target_id's own
* eligibility state directly (2026-09-14 correction) -- see its
* own doc comment for why that can't be left to ISR polling. */
if (sk_vm_at_outermost_interpret()) {
* own doc comment for why that can't be left to ISR polling.
*
* FABRIC-3.8.md §3.1 (Stage A, 2026-09-23): gated on
* sk_vm_owns_outermost_frame(vm) -- `vm` physically owns this
* 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. */
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;
@@ -1177,6 +1186,25 @@ static int g_vm_interpret_depth;
int sk_vm_at_outermost_interpret(void) { return g_vm_interpret_depth <= 1; }
/* FABRIC-3.8.md §3.1 (Stage A, 2026-09-23): the real ownership invariant
* the global depth test above was approximating. A checkpoint may drain or
* switch only when `vm` physically owns the stack it is standing on
* (sk_vm_switch_current_vm() -- switch.c tracks the physical owner on every
* switch, seeded for Hera at boot), is at its OWN outermost vm_interpret()
* frame (per-VM depth, not a count across every VM), and is not mid-
* definition (a payload or a switch must never land inside a `:` ... `;`
* body -- FABRIC-3.8.md §3.10). A nested VM-EXEC/VM-CALL into another VM
* fails the first clause by construction (that VM does not own the stack),
* so the existing defer-if-nested behavior (§XXVIII.1) is preserved. Before
* the switch mechanism is seeded at boot (current VM == NULL), nothing
* passes -- matching "no switch has happened yet." */
int sk_vm_owns_outermost_frame(VM *vm) {
return vm &&
sk_vm_switch_current_vm() == vm &&
vm->interpret_depth == 1 &&
vm->mode != MODE_COMPILE;
}
void vm_interpret(VM* vm, const char* input)
{
if (!vm || !input) return;
@@ -1185,6 +1213,7 @@ void vm_interpret(VM* vm, const char* input)
VM *prev_attrib_vm = g_log_attrib_vm;
g_log_attrib_vm = vm;
g_vm_interpret_depth++;
vm->interpret_depth++; /* FABRIC-3.8.md §3.1: per-VM, see sk_vm_owns_outermost_frame() */
/* Load into input buffer (cap + NUL) */
size_t n = 0, cap = INPUT_BUFFER_SIZE ? INPUT_BUFFER_SIZE - 1 : 0;
@@ -1218,6 +1247,7 @@ void vm_interpret(VM* vm, const char* input)
}
g_vm_interpret_depth--;
vm->interpret_depth--;
g_log_attrib_vm = prev_attrib_vm;
}