feat(v4): the mesh boot to spec -- Hera born empty in the kernel's fabric; births, kills, every node stepping together (#189)
ENGINE step 4 Phase 1, Tasks 5b-5d, and the edge cases (Captain Bob, 2026-11-13: "Build it to proper spec"; "Check for the edge cases"; "Yes, add it to v4-checks"). - v4/system/mesh.c, v4/include/v4/mesh.h: the nucleus assembled by the ROM and given to Hera as F18 cells; Hera born empty at place 0, the kernel on port 0, the console on port 1; told she is 10, in the registry, the kernel's words, FORTH-79, sealed, POST, the kernel's capsules. NODE-BORN / NODE-KILL in the owner's room. - No scheduler: v4_mesh_tell returns once the node told says how its text ended, and v4_mesh_step / v4_mesh_busy let the host step the fabric while it waits, so nodes run between lines as during them. A text left "waiting" is owed its end: the next line ends it "Interrupted" and is then run (MESH.md 7b.6). - kernel: sk_v4.c boots through the mesh; its idle steps the fabric while a node can run, and halts only when none can (#151 kept). stadium_return_quota and stadium_ops_forget on a kill. - Tests: v4/tests/test_mesh_boot.c (32 checks: the boot, births, kills, rebirth, a live number, a fault, no such node, Hera not killing herself, no room, a line too long, a node running for ever, a wait broken by the next line), run by `make -C v4 test` and under ASan+UBSan by `sanitize` (v4-checks); kernel/tests quota return; scripts/qemu_session.py births() and the edges on bare metal. - Docs: ENGINE.md 6 "who runs next" struck (settled: no scheduler); the Phase 1 plan; FABRIC-3 XLVIII.23; .claude/memory/rules.md: alpha.5 closed to new work. - Evidence: v4-checks 80 programs under the sanitizers, 0 failures; the hosted builds alike; two stopped v4-checks runs labelled STOPPED; three development amd64 boots labelled. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017RzZgrf7V7Zk2iczBXDRbm
This commit is contained in:
+15
-5
@@ -730,14 +730,15 @@ LOADER_CFLAGS += $(V4_DEFS)
|
||||
V4_ENGINE_SRCS := $(addprefix v4/src/,node.c exec.c stack.c iword.c heat.c guard.c image.c fabric.c capsule.c message.c wire.c manage.c)
|
||||
V4_ROM_C := $(BUILD_DIR)/v4_rom_64.c
|
||||
V4_MAP_C := $(BUILD_DIR)/v4_map_64.c
|
||||
V4_MESH_ROM_C := $(BUILD_DIR)/v4_mesh_rom_64.c # a mesh node's ROM: the nodes Hera births carry it (v4/mesh.h, #189)
|
||||
|
||||
LOADER_EXTRA_SRCS += $(KERNEL_SRC)/v4/sk_v4.c $(KERNEL_SRC)/v4/sk_v4_identity.c $(KERNEL_SRC)/v4/sk_v4_marker.c $(KERNEL_SRC)/v4/sk_v4_grow.c $(KERNEL_SRC)/v4/sk_v4_mint.c $(KERNEL_SRC)/v4/sk_v4_stop.c $(KERNEL_SRC)/v4/sk_v4_slots.c
|
||||
KERNEL_EXTRA_SRCS += $(KERNEL_SRC)/v4/sk_v4.c $(KERNEL_SRC)/v4/sk_v4_identity.c $(KERNEL_SRC)/v4/sk_v4_marker.c $(KERNEL_SRC)/v4/sk_v4_grow.c $(KERNEL_SRC)/v4/sk_v4_mint.c $(KERNEL_SRC)/v4/sk_v4_stop.c $(KERNEL_SRC)/v4/sk_v4_slots.c
|
||||
# v4/system/boot.c is the boot the hosted v4 system runs too: the ROM
|
||||
# assembles the system, then the capsules from this kernel's own capsule
|
||||
# directory, then the prompt.
|
||||
LOADER_V4_OBJS := $(patsubst v4/src/%.c,$(LOADER_OBJ_DIR)/v4engine/%.o,$(V4_ENGINE_SRCS)) $(LOADER_OBJ_DIR)/v4engine/v4_rom.o $(LOADER_OBJ_DIR)/v4engine/v4_map.o $(LOADER_OBJ_DIR)/v4system/boot.o $(LOADER_OBJ_DIR)/v4system/blocks.o $(LOADER_OBJ_DIR)/v4system/post.o $(LOADER_OBJ_DIR)/v4system/post_cases.o
|
||||
KERNEL_V4_OBJS := $(patsubst v4/src/%.c,$(KERNEL_OBJ_DIR)/v4engine/%.o,$(V4_ENGINE_SRCS)) $(KERNEL_OBJ_DIR)/v4engine/v4_rom.o $(KERNEL_OBJ_DIR)/v4engine/v4_map.o $(KERNEL_OBJ_DIR)/v4system/boot.o $(KERNEL_OBJ_DIR)/v4system/blocks.o $(KERNEL_OBJ_DIR)/v4system/post.o $(KERNEL_OBJ_DIR)/v4system/post_cases.o
|
||||
LOADER_V4_OBJS := $(patsubst v4/src/%.c,$(LOADER_OBJ_DIR)/v4engine/%.o,$(V4_ENGINE_SRCS)) $(LOADER_OBJ_DIR)/v4engine/v4_rom.o $(LOADER_OBJ_DIR)/v4engine/v4_map.o $(LOADER_OBJ_DIR)/v4engine/v4_mesh_rom.o $(LOADER_OBJ_DIR)/v4system/mesh.o $(LOADER_OBJ_DIR)/v4system/boot.o $(LOADER_OBJ_DIR)/v4system/blocks.o $(LOADER_OBJ_DIR)/v4system/post.o $(LOADER_OBJ_DIR)/v4system/post_cases.o
|
||||
KERNEL_V4_OBJS := $(patsubst v4/src/%.c,$(KERNEL_OBJ_DIR)/v4engine/%.o,$(V4_ENGINE_SRCS)) $(KERNEL_OBJ_DIR)/v4engine/v4_rom.o $(KERNEL_OBJ_DIR)/v4engine/v4_map.o $(KERNEL_OBJ_DIR)/v4engine/v4_mesh_rom.o $(KERNEL_OBJ_DIR)/v4system/mesh.o $(KERNEL_OBJ_DIR)/v4system/boot.o $(KERNEL_OBJ_DIR)/v4system/blocks.o $(KERNEL_OBJ_DIR)/v4system/post.o $(KERNEL_OBJ_DIR)/v4system/post_cases.o
|
||||
endif
|
||||
|
||||
LOADER_SRCS := $(LOADER_SRCS_BASE) $(LOADER_EXTRA_SRCS)
|
||||
@@ -903,12 +904,13 @@ endif
|
||||
# two copies met ("cp: cannot create regular file ... File exists", fixed
|
||||
# 2026-10-26).
|
||||
ifeq ($(STARFORTH_V4),1)
|
||||
$(V4_ROM_C) $(V4_MAP_C) &: FORCE
|
||||
$(V4_ROM_C) $(V4_MAP_C) $(V4_MESH_ROM_C) &: FORCE
|
||||
@mkdir -p $(dir $(V4_ROM_C))
|
||||
@echo " V4ROM capsules/v4/rom -> $(V4_ROM_C) $(V4_MAP_C)"
|
||||
@$(MAKE) --no-print-directory -C v4 CC=cc $(CURDIR)/v4/build/rom-64.c $(CURDIR)/v4/build/v4_map_64.c
|
||||
@echo " V4ROM capsules/v4/rom -> $(V4_ROM_C) $(V4_MAP_C) $(V4_MESH_ROM_C)"
|
||||
@$(MAKE) --no-print-directory -C v4 CC=cc $(CURDIR)/v4/build/rom-64.c $(CURDIR)/v4/build/v4_map_64.c $(CURDIR)/v4/build/mesh-rom-64.c
|
||||
@cmp -s v4/build/rom-64.c $(V4_ROM_C) || cp v4/build/rom-64.c $(V4_ROM_C)
|
||||
@cmp -s v4/build/v4_map_64.c $(V4_MAP_C) || cp v4/build/v4_map_64.c $(V4_MAP_C)
|
||||
@cmp -s v4/build/mesh-rom-64.c $(V4_MESH_ROM_C) || cp v4/build/mesh-rom-64.c $(V4_MESH_ROM_C)
|
||||
|
||||
# The system's memory map, capsules/v4/sys/map-<w>.f18, is generated from
|
||||
# the same description as the ROM and v4_map_image (v4/tests/host_map.h), and
|
||||
@@ -928,6 +930,14 @@ $(KERNEL_OBJ_DIR)/v4engine/v4_rom.o: $(V4_ROM_C) | $(KERNEL_OBJ_DIR)
|
||||
@mkdir -p $(dir $@)
|
||||
@echo "CC (kernel) $<"
|
||||
@$(CC) $(KERNEL_CFLAGS) -c $< -o $@
|
||||
$(LOADER_OBJ_DIR)/v4engine/v4_mesh_rom.o: $(V4_MESH_ROM_C) | $(LOADER_OBJ_DIR)
|
||||
@mkdir -p $(dir $@)
|
||||
@echo "CC (loader) $<"
|
||||
@$(LOADER_CC) $(LOADER_CFLAGS) -c $< -o $@
|
||||
$(KERNEL_OBJ_DIR)/v4engine/v4_mesh_rom.o: $(V4_MESH_ROM_C) | $(KERNEL_OBJ_DIR)
|
||||
@mkdir -p $(dir $@)
|
||||
@echo "CC (kernel) $<"
|
||||
@$(CC) $(KERNEL_CFLAGS) -c $< -o $@
|
||||
$(LOADER_OBJ_DIR)/v4engine/v4_map.o: $(V4_MAP_C) | $(LOADER_OBJ_DIR)
|
||||
@mkdir -p $(dir $@)
|
||||
@echo "CC (loader) $<"
|
||||
|
||||
@@ -195,6 +195,16 @@ VMUuid stadium_node_key(uint32_t node);
|
||||
int stadium_boot_init_for(VMUuid first);
|
||||
int stadium_birth_node(VMUuid key, const char *name);
|
||||
|
||||
/*
|
||||
* stadium_return_quota(vm, to) -- a node is gone: its quota is given back (#189, Phase 1 Task 5c),
|
||||
* the inverse of stadium_grant_quota(). Every resident patron of it is evicted, its heat cooling to
|
||||
* its reservoir as any eviction's does; its free cells go to `to`'s free list; its slot is free for
|
||||
* the next grant. Its reservoir -- the share of 1.0 its grant gave it, its own -- goes with it, as
|
||||
* the grant made it. 0, or -1: no such quota, `to` none or the same, the first quota (patron zero's),
|
||||
* or a patron that cannot be evicted (pinned, or holding another).
|
||||
*/
|
||||
int stadium_return_quota(VMUuid vm_id, VMUuid to_vm_id);
|
||||
|
||||
/* stadium_is_initialized - Whether stadium_boot_init() has succeeded. */
|
||||
int stadium_is_initialized(void);
|
||||
|
||||
|
||||
@@ -38,6 +38,10 @@ void stadium_ops_init(void);
|
||||
* `tick` the heartbeat's count now. Nothing for a key with no quota. */
|
||||
void stadium_ops_charge(VMUuid vm_id, const uint64_t counts[STADIUM_OPS_COUNT], uint64_t tick);
|
||||
|
||||
/* A node is gone: its opcode counts are forgotten, so a node that later has its quota's slot starts
|
||||
* from none (#189). Before stadium_return_quota(), while the key still has its slot. */
|
||||
void stadium_ops_forget(VMUuid vm_id);
|
||||
|
||||
/* An opcode patron's heat now (Q48.16), or 0: not resident, or no quota. */
|
||||
uint64_t stadium_ops_heat(VMUuid vm_id, unsigned op);
|
||||
|
||||
|
||||
+88
-133
File diff suppressed because it is too large
Load Diff
@@ -667,6 +667,42 @@ int stadium_grant_quota(VMUuid new_vm_id, VMUuid from_vm_id) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int stadium_return_quota(VMUuid vm_id, VMUuid to_vm_id) {
|
||||
int slot, to_slot;
|
||||
size_t idx, last = STADIUM_CELL_NONE, count = 0;
|
||||
|
||||
if (!stadium_initialized) return -1;
|
||||
slot = quota_slot_for_vm(vm_id);
|
||||
to_slot = quota_slot_for_vm(to_vm_id);
|
||||
if (slot <= 0 || to_slot < 0 || slot == to_slot) return -1; /* slot 0 is patron zero's */
|
||||
|
||||
/* its patrons leave, their heat to its reservoir */
|
||||
while (stadium_quotas[slot].resident_head != STADIUM_CELL_NONE)
|
||||
if (stadium_evict(stadium_quotas[slot].resident_head) != 0) return -1;
|
||||
|
||||
/* its free cells to `to`'s free list */
|
||||
for (idx = stadium_quotas[slot].free_head; idx != STADIUM_CELL_NONE;
|
||||
idx = link_to_size(stadium_cell_array[idx].header.link)) {
|
||||
stadium_owner[idx] = (uint16_t)to_slot;
|
||||
last = idx;
|
||||
count++;
|
||||
}
|
||||
if (last != STADIUM_CELL_NONE) {
|
||||
stadium_cell_array[last].header.link = size_to_link(stadium_quotas[to_slot].free_head);
|
||||
stadium_quotas[to_slot].free_head = stadium_quotas[slot].free_head;
|
||||
stadium_quotas[to_slot].free_count += count;
|
||||
}
|
||||
|
||||
stadium_quotas[slot].vm_id = vm_uuid_none();
|
||||
stadium_quotas[slot].in_use = 0;
|
||||
stadium_quotas[slot].free_head = STADIUM_CELL_NONE;
|
||||
stadium_quotas[slot].free_count = 0;
|
||||
stadium_quotas[slot].resident_head = STADIUM_CELL_NONE;
|
||||
stadium_quotas[slot].reservoir = 0;
|
||||
stadium_quotas[slot].consumed = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* stadium_best_donor - FABRIC-3.md SXVI donor-floor fix: which currently
|
||||
* in-use VM has the most free (unclaimed) cells right now. Callers granting
|
||||
|
||||
@@ -136,6 +136,20 @@ void stadium_ops_charge(VMUuid vm_id, const uint64_t counts[STADIUM_OPS_COUNT],
|
||||
}
|
||||
}
|
||||
|
||||
void stadium_ops_forget(VMUuid vm_id)
|
||||
{
|
||||
int slot;
|
||||
unsigned k;
|
||||
if (!ops_initialized) return;
|
||||
slot = stadium_quota_slot_for_vm(vm_id);
|
||||
if (slot < 0) return;
|
||||
for (k = 0; k < STADIUM_OPS_COUNT; k++) {
|
||||
op_slots[slot][k].cell_index = STADIUM_CELL_NONE;
|
||||
op_slots[slot][k].last_tick = 0;
|
||||
op_slots[slot][k].last_count = 0;
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t stadium_ops_heat(VMUuid vm_id, unsigned op)
|
||||
{
|
||||
int slot;
|
||||
|
||||
@@ -86,5 +86,23 @@ int main(void)
|
||||
CHECK(h5 > 2u * h0 && h5 < 4u * h0, "in proportion to their retirements, about 3 to 1");
|
||||
CHECK(stadium_reservoir_peek(k2) >= Q48_ONE / 3 && stadium_conserved(k2), "the floor kept, conservation held");
|
||||
}
|
||||
|
||||
/* a node killed: its quota given back (#189, Phase 1 Task 5c) -- its patrons evicted, its cells
|
||||
* to the node it came from, its slot free for the next birth; its opcode counts forgotten */
|
||||
{
|
||||
VMUuid k2 = stadium_node_key(12), k3 = stadium_node_key(13);
|
||||
uint64_t c3[32];
|
||||
stadium_ops_forget(k2);
|
||||
CHECK(stadium_return_quota(k2, key) == 0, "node 12's quota is given back to node 10");
|
||||
CHECK(stadium_quota_slot_for_vm(k2) < 0 && stadium_ops_heat(k2, 5) == 0, "node 12 has no quota, and no opcode heat");
|
||||
CHECK(stadium_conserved(key), "node 10 conserved after taking its cells back");
|
||||
CHECK(stadium_return_quota(key, key) != 0 && stadium_return_quota(k2, key) != 0, "a quota is not given back to itself, or twice");
|
||||
CHECK(stadium_grant_quota(k3, key) == 0, "the slot is free: node 13 is granted one");
|
||||
memset(c3, 0, sizeof c3);
|
||||
c3[7] = 2;
|
||||
stadium_ops_charge(k3, c3, 30);
|
||||
CHECK(stadium_ops_heat(k3, 7) == 2u * (uint64_t)STADIUM_WORD_HEAT_QUANTUM && stadium_ops_heat(k3, 5) == 0,
|
||||
"node 13 starts with no counts of node 12's: two retirements, two quanta");
|
||||
}
|
||||
return DONE();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user