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
@@ -593,7 +593,10 @@ int sk_hermes_drain_checkpoint(VM *vm) {
|
||||
* this function is already interpreting for `vm` runs at vm's own
|
||||
* depth 1, so its in-word checkpoint (vm_core.c) would otherwise drain
|
||||
* a SECOND message inside the first one's execution. */
|
||||
if (vm->hermes_draining) return 0;
|
||||
/* Stage FR (§3.12): derived from the frame stack -- a DRAIN frame for
|
||||
* this VM already on the running stack means we are inside one of its
|
||||
* own payloads. */
|
||||
if (vm_frame_has(vm_frame_current_stack(), vm->stadium_vm_id, VM_FRAME_DRAIN)) return 0;
|
||||
|
||||
self = vm->stadium_vm_id;
|
||||
msg = sk_hermes_pending_peek(self);
|
||||
@@ -656,12 +659,10 @@ int sk_hermes_drain_checkpoint(VM *vm) {
|
||||
|
||||
vm->mode = MODE_INTERPRET; /* a payload is never compiled into whatever
|
||||
* the enclosing context was mid-defining */
|
||||
vm->hermes_draining = 1; /* FABRIC-3.8.md §3.3 -- set only once a message
|
||||
* is actually in hand (every earlier return
|
||||
* above leaves it untouched), cleared on the
|
||||
* single path out below */
|
||||
vm_interpret(vm, (const char *)msg->payload_addr);
|
||||
vm->hermes_draining = 0;
|
||||
/* FABRIC-3.8.md §3.3 / §3.12: interpreted as a DRAIN-kind frame -- the
|
||||
* guard above finds it for as long as this payload runs, and it pops by
|
||||
* ID when vm_interpret_kind() returns. */
|
||||
vm_interpret_kind(vm, (const char *)msg->payload_addr, VM_FRAME_DRAIN);
|
||||
drain_error = vm->error;
|
||||
|
||||
memcpy(vm->input_buffer, saved_input, sizeof(saved_input));
|
||||
|
||||
@@ -133,12 +133,24 @@ void sk_vm_switch_entry(void) {
|
||||
if (sk_hermes_drain_checkpoint(self) != 0) {
|
||||
continue; /* consumed one message (1 = ok, -1 = payload erred) */
|
||||
}
|
||||
self->parked_idle = 1;
|
||||
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");
|
||||
/* 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." */
|
||||
{
|
||||
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");
|
||||
}
|
||||
vm_frame_pop(&self->frames, idle_id); /* resumed: someone switched back in */
|
||||
}
|
||||
self->parked_idle = 0; /* resumed: someone switched back in */
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -156,6 +156,12 @@ void vm_init_with_host(VM* vm, const VMHostServices *host)
|
||||
|
||||
*vm = (VM){0};
|
||||
vm->host = host ? host : vm_default_host_services();
|
||||
#ifdef __STARKERNEL__
|
||||
/* FABRIC-3.8.md §3.12: reserve the frame stack at birth so vm_interpret()'s
|
||||
* per-call push rarely has to grow. A failure here is not fatal to init --
|
||||
* the first push retries the allocation and fails closed if it still can't. */
|
||||
(void)vm_frame_reserve(&vm->frames, 8);
|
||||
#endif
|
||||
#ifndef __STARKERNEL__
|
||||
vm_reset_hosted_fake_ns(vm);
|
||||
#endif
|
||||
|
||||
+58
-15
@@ -375,6 +375,7 @@ void vm_cleanup(VM* vm)
|
||||
#ifdef __STARKERNEL__
|
||||
sf_free(vm->call_stack);
|
||||
vm->call_stack = NULL;
|
||||
vm_frame_free(&vm->frames); /* FABRIC-3.8.md §3.12 */
|
||||
|
||||
/* FABRIC-3.md §XXVIII, Stage 1 (2026-09-13): free the native stack
|
||||
* allocated at birth (capsule_birth_baby() / sk_vm_bootstrap_parity()).
|
||||
@@ -1162,15 +1163,34 @@ void vm_interpret_word(VM* vm, const char* word_str, size_t len)
|
||||
* @param vm Pointer to VM instance
|
||||
* @param input String containing Forth code to interpret
|
||||
*/
|
||||
/* FABRIC-3.md §XXVI follow-on, Step 4 (log_attrib.h): which VM's dictionary
|
||||
* context is currently executing, for log persistence attribution. Save/
|
||||
* restore (not a plain set) so a nested vm_interpret() call -- VM-EXEC
|
||||
* dispatching into a different VM's own dictionary, mid-interpret --
|
||||
* attributes to whichever VM is actually running when it returns to the
|
||||
* outer frame, not silently to the inner one. */
|
||||
static VM *g_log_attrib_vm; /* zero-initialized (BSS) */
|
||||
/* FABRIC-3.8.md §3.12 (Stage FR, 2026-09-23): frame stacks replace the old
|
||||
* single saved-and-restored attribution global (FABRIC-3.md §XXVI follow-on,
|
||||
* Step 4). That global assumed strictly nested calls; once VMs interleave on
|
||||
* their own native stacks, a switch left it naming the VM that had just
|
||||
* switched out -- found by this stage's probe baseline (HSSAS, not HSHAS).
|
||||
*
|
||||
* Each VM's frame stack (VM.frames) records the nested levels running on
|
||||
* its own stack. Before the switch mechanism is seeded at boot there is no
|
||||
* owning VM yet, so this boot stack stands in for that window only. */
|
||||
static VMFrameStack g_boot_frames;
|
||||
|
||||
VM *vm_log_attributed_vm(void) { return g_log_attrib_vm; }
|
||||
VMFrameStack *vm_frame_current_stack(void) {
|
||||
VM *current = sk_vm_switch_current_vm();
|
||||
return current ? ¤t->frames : &g_boot_frames;
|
||||
}
|
||||
|
||||
/* Log attribution, by ID: the VM executing at the top frame of the running
|
||||
* stack, resolved through the VM registry. NULL when nothing is
|
||||
* interpreting on this stack (or the ID no longer resolves) --
|
||||
* (LOG-APPEND-RAW) maps NULL to its documented "HADES" source, so this must
|
||||
* never fall back to the current VM itself. */
|
||||
VM *vm_log_attributed_vm(void) {
|
||||
VMFrameStack *s = vm_frame_current_stack();
|
||||
VMRegistryEntry e;
|
||||
if (s->count == 0) return (VM *)0;
|
||||
if (capsule_vm_registry_get(s->entries[s->count - 1].vm_id, &e) != 0) return (VM *)0;
|
||||
return (VM *)e.vm_ptr;
|
||||
}
|
||||
|
||||
/* FABRIC-3.md §XXVIII Stage 3 correction (2026-09-13): nesting depth of
|
||||
* vm_interpret() calls, tracked the same save/restore-adjacent way as
|
||||
@@ -1197,23 +1217,47 @@ int sk_vm_at_outermost_interpret(void) { return g_vm_interpret_depth <= 1; }
|
||||
* 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." */
|
||||
* passes -- matching "no switch has happened yet."
|
||||
*
|
||||
* Stage FR (§3.12): derived from the frame stack rather than a stored
|
||||
* depth -- `vm` owns the stack, and that stack holds exactly one frame, and
|
||||
* it is `vm`'s own (not a nested VM-EXEC target, not a second interpret of
|
||||
* itself via EXEC/LOAD). */
|
||||
int sk_vm_owns_outermost_frame(VM *vm) {
|
||||
return vm &&
|
||||
sk_vm_switch_current_vm() == vm &&
|
||||
vm->interpret_depth == 1 &&
|
||||
vm->frames.count == 1 &&
|
||||
vm_uuid_equal(vm->frames.entries[0].vm_id, vm->stadium_vm_id) &&
|
||||
vm->mode != MODE_COMPILE;
|
||||
}
|
||||
|
||||
void vm_interpret(VM* vm, const char* input)
|
||||
{
|
||||
vm_interpret_kind(vm, input, VM_FRAME_INTERPRET);
|
||||
}
|
||||
|
||||
void vm_interpret_kind(VM* vm, const char* input, uint32_t kind)
|
||||
{
|
||||
if (!vm || !input) return;
|
||||
vm_assert_interpreter_enabled(vm, "vm_interpret");
|
||||
|
||||
VM *prev_attrib_vm = g_log_attrib_vm;
|
||||
g_log_attrib_vm = vm;
|
||||
/* FABRIC-3.8.md §3.12 (Stage FR): push this level onto the frame stack
|
||||
* of the VM physically owning the running native stack -- chosen ONCE
|
||||
* here and popped through the same stack at exit, never re-decided (a
|
||||
* resume after a switch restores that stack's owner as current anyway,
|
||||
* and caching also covers a call that began before the switch mechanism
|
||||
* was seeded). Only the frame's ID is held across the body: a nested
|
||||
* interpret on this same stack can grow (copy) the array. */
|
||||
VMFrameStack *frame_stack = vm_frame_current_stack();
|
||||
VMFrameID frame_id = vm_frame_push(frame_stack, vm->stadium_vm_id, kind);
|
||||
if (frame_id == 0) {
|
||||
/* Fail closed: interpreting without a frame would silently break
|
||||
* the ownership gate and attribution. */
|
||||
console_println("vm_interpret: frame stack could not grow -- refusing to interpret");
|
||||
vm->error = 1;
|
||||
return;
|
||||
}
|
||||
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;
|
||||
@@ -1247,8 +1291,7 @@ void vm_interpret(VM* vm, const char* input)
|
||||
}
|
||||
|
||||
g_vm_interpret_depth--;
|
||||
vm->interpret_depth--;
|
||||
g_log_attrib_vm = prev_attrib_vm;
|
||||
vm_frame_pop(frame_stack, frame_id); /* by ID -- a mismatch is logged as corruption */
|
||||
}
|
||||
|
||||
/* ====================== VM memory helpers ======================= */
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
StarForth — Steady-State Virtual Machine Runtime
|
||||
|
||||
Copyright (c) 2023–2025 Robert A. James
|
||||
All rights reserved.
|
||||
|
||||
This file is part of the StarForth project.
|
||||
|
||||
Licensed under the StarForth License, Version 1.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
You may obtain a copy of the License at:
|
||||
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
|
||||
|
||||
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
express or implied, including but not limited to the warranties of
|
||||
merchantability, fitness for a particular purpose, and noninfringement.
|
||||
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* vm_frame.c - per-VM execution frame stack (FABRIC-3.8.md §3.12, Stage FR).
|
||||
* See vm_frame.h for the contract.
|
||||
*/
|
||||
|
||||
#ifdef __STARKERNEL__
|
||||
|
||||
#include "starkernel/vm/vm_frame.h"
|
||||
#include "platform_alloc.h" /* sf_malloc / sf_free */
|
||||
#include "starkernel/console.h" /* console_println -- corruption is loud */
|
||||
#include "freestanding/stdio.h" /* snprintf */
|
||||
#include <string.h>
|
||||
|
||||
/* Single writer: vm_frame_push(), reached only from vm_interpret_kind() and
|
||||
* the switch trampoline -- both mainline. No interrupt-context path calls
|
||||
* vm_interpret() (checked by grep across arch/, heartbeat.c, the switch
|
||||
* signal and hal/ when this was written), so this needs no lock -- the same
|
||||
* single-writer discipline heartbeat.c and the switch signal already use. */
|
||||
static VMFrameID g_next_frame_id = 1;
|
||||
|
||||
/* Grow by allocate-copy-free, deliberately not sf_realloc(): the kernel's
|
||||
* sf_realloc() is documented as orphaning the old block. */
|
||||
static int grow(VMFrameStack *s, int new_cap) {
|
||||
VMFrameEntry *n;
|
||||
if (new_cap <= s->cap) return 0;
|
||||
n = (VMFrameEntry *)sf_malloc((size_t)new_cap * sizeof(VMFrameEntry));
|
||||
if (!n) return -1;
|
||||
if (s->entries && s->count > 0) {
|
||||
memcpy(n, s->entries, (size_t)s->count * sizeof(VMFrameEntry));
|
||||
}
|
||||
if (s->entries) sf_free(s->entries);
|
||||
s->entries = n;
|
||||
s->cap = new_cap;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int vm_frame_reserve(VMFrameStack *s, int cap) {
|
||||
if (!s || cap <= 0) return -1;
|
||||
return grow(s, cap);
|
||||
}
|
||||
|
||||
VMFrameID vm_frame_push(VMFrameStack *s, VMUuid vm_id, uint32_t kind) {
|
||||
VMFrameEntry *e;
|
||||
if (!s) return 0;
|
||||
if (s->count >= s->cap) {
|
||||
if (grow(s, s->cap ? s->cap * 2 : 8) != 0) return 0;
|
||||
}
|
||||
e = &s->entries[s->count++];
|
||||
e->id = g_next_frame_id++;
|
||||
e->vm_id = vm_id;
|
||||
e->kind = kind;
|
||||
if (s->count > s->max) s->max = s->count;
|
||||
return e->id;
|
||||
}
|
||||
|
||||
void vm_frame_pop(VMFrameStack *s, VMFrameID id) {
|
||||
int i;
|
||||
if (!s || id == 0 || s->count == 0) return;
|
||||
if (s->entries[s->count - 1].id == id) {
|
||||
s->count--;
|
||||
return;
|
||||
}
|
||||
/* Every frame stack is LIFO (a resume always restores that stack's owner
|
||||
* as current), so reaching here is corruption. Loud, not log_message():
|
||||
* that channel is invisible in this build's serial capture
|
||||
* (FABRIC-3.6.md task 3.8's own finding), and this must never be silent. */
|
||||
{
|
||||
char line[128];
|
||||
snprintf(line, sizeof(line),
|
||||
"VM FRAME CORRUPTION: pop id=%llu but top id=%llu (depth %d)",
|
||||
(unsigned long long)id,
|
||||
(unsigned long long)s->entries[s->count - 1].id, s->count);
|
||||
console_println(line);
|
||||
}
|
||||
/* Remove OUR frame wherever it is, never someone else's. */
|
||||
for (i = s->count - 1; i >= 0; i--) {
|
||||
if (s->entries[i].id == id) {
|
||||
if (i < s->count - 1) {
|
||||
memmove(&s->entries[i], &s->entries[i + 1],
|
||||
(size_t)(s->count - 1 - i) * sizeof(VMFrameEntry));
|
||||
}
|
||||
s->count--;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int vm_frame_top_is(const VMFrameStack *s, VMUuid vm_id, uint32_t kind) {
|
||||
const VMFrameEntry *t;
|
||||
if (!s || s->count == 0) return 0;
|
||||
t = &s->entries[s->count - 1];
|
||||
return t->kind == kind && vm_uuid_equal(t->vm_id, vm_id);
|
||||
}
|
||||
|
||||
int vm_frame_has(const VMFrameStack *s, VMUuid vm_id, uint32_t kind) {
|
||||
int i;
|
||||
if (!s) return 0;
|
||||
for (i = 0; i < s->count; i++) {
|
||||
if (s->entries[i].kind == kind && vm_uuid_equal(s->entries[i].vm_id, vm_id)) return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void vm_frame_free(VMFrameStack *s) {
|
||||
if (!s) return;
|
||||
if (s->entries) sf_free(s->entries);
|
||||
s->entries = (VMFrameEntry *)0;
|
||||
s->count = s->cap = 0;
|
||||
}
|
||||
|
||||
#endif /* __STARKERNEL__ */
|
||||
Reference in New Issue
Block a user