Files
LithosAnanake/kernel/src/vm/kernel_hermes.c
T
rajamesandJunie a8b70e88d3 Reorganize source tree: kernel/, v3/, v4/ split and board infrastructure
Source tree reorganization:
- Move StarForth v3 engine to v3/ (src/, include/, Makefile)
- Move kernel to kernel/ (src/, include/, linker/, Makefile)
- Create v4/ skeleton for F18-ISA golden model (DECOMPOSITION.md, JUSTIFICATION.md)
- Move FABRIC-0..4.md to docs/fabric/
- Move ONTOLOGY.md and ROADMAP.md to docs/

Board infrastructure:
- Add boards/ser5/, boards/raspi/, boards/milkv/, boards/zynq7020/
- Each board has board.mk (ISA, CPU flags, boot recipe) and README.md
- Root Makefile becomes thin dispatcher: boot_image, all, clean, docs take TARGET
- make boot_image TARGET=SER5|RASPI|MILKV builds one GPT/MBR image per board
- ZYNQ7020 target exists but stops with clear error (ARMv7 port not built yet)
- scripts/mkdiskimage.sh builds disk images for all boards

Docs pipeline:
- docs/book/ with LaTeX master (main.tex) and Makefile
- pandoc converts Markdown to LaTeX at build time
- Two Lua filters: table-widths.lua (wide tables wrap), code-breaks.lua (inline code breaks)
- make docs builds single PDF (754 pages, 0 missing characters)
- make docs TARGET=<board> adds board appendix
- build/docs/<book|board>/meta.tex stamps git commit into PDF

Bug fixes:
- 42 include paths that only worked by accident now use correct relative paths
- clang-18 hardcode replaced with configurable CC variable (fixed aarch64 build)
- Pi 5: kernel_2712.img linked at 0x80000, .bss zeroed, memory reserved
- Doxyfile, .clang-tidy, README.md, Kconfig paths updated

Verified:
- Hosted v3 build passes 1012 tests, 0 failures
- SER5 image boots in QEMU (OVMF), POST passes, K exact (65536 = Q48_ONE)
- Milk-V image boots in QEMU (OpenSBI + U-Boot + bootefi), POST passes
- make clean TARGET=<board> removes only that board and its ISA objects
- make all builds all boards, hosted v3, and docs in one run

Co-authored-by: Junie <junie@jetbrains.com>
2026-10-01 15:40:09 -04:00

819 lines
30 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
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.
*/
/**
* kernel_hermes.c - Kernel-resident Hermes: the heat-coupled allocator,
* its release path, and the four-counter ledger (FABRIC-3.6.md tasks
* 2.2/2.3/2.4, item 28).
*
* CORRECTION (task 2.3, found while starting it): task 2.2's first cut of
* sk_hermes_alloc() pulled reservoir heat but never admitted a real
* Stadium-floor patron -- it just set a local `in_use` flag. That is
* wrong: FABRIC-3.5.md SXXXIII.4 item 1 states plainly that
* "MSG-FREE-NODE returns it via STADIUM-EVICT", which only has something
* to evict if allocation admitted something. More importantly, SXL.4's
* invariant is Sigma(resident patron heat) + reservoir + consumed ==
* Q48_ONE -- if held message heat is not a resident patron, it is
* invisible to every term of that equation while held, which cannot be
* right. Corrected here: sk_hermes_alloc() now calls stadium_admit()
* with the pulled heat, storing the returned cell index in the message's
* own stadium_cell field for release to evict later. Behaviour
* DELIVER (1), matching messaging.4th's own `SB-DELIVER STADIUM-ADMIT`
* at its MSG-ALLOC site.
*/
#ifdef __STARKERNEL__
#include <string.h>
#include "freestanding/stdio.h" /* task 3.9 evidence print -- snprintf() */
#include "starkernel/vm/kernel_hermes.h"
#include "starkernel/vm/stadium.h"
#include "starkernel/q48_16.h"
#include "starkernel/kmalloc.h"
#include "console.h"
#include "vm.h" /* task 3.4 -- VM struct fields, vm_interpret() */
/* Freestanding: no libc printf. Prints an unsigned decimal, no leading
* zeros -- same small helper stadium.c/capsule_vm_switch_signal.c each
* carry their own copy of. */
static void console_put_u64(uint64_t v) {
char buf[21];
int i = 20;
buf[20] = '\0';
if (v == 0) {
console_puts("0");
return;
}
while (v > 0 && i > 0) {
buf[--i] = (char)('0' + (v % 10));
v /= 10;
}
console_puts(&buf[i]);
}
static SkHermesMessage sk_hermes_msgs[SK_HERMES_MSG_MAX];
/* Task 2.4 (item 28), FABRIC-3.5.md SXXXVII.3/SXL.4's four counters.
* `consumed` is declared now but touched nowhere yet -- task 2.5 adds
* its one increment site when decay exists to record a delta from. */
static uint64_t sk_hermes_held = 0;
static uint64_t sk_hermes_pulled = 0;
static uint64_t sk_hermes_returned = 0;
static uint64_t sk_hermes_consumed = 0;
void sk_hermes_ledger(uint64_t *held, uint64_t *pulled, uint64_t *returned, uint64_t *consumed) {
if (held) *held = sk_hermes_held;
if (pulled) *pulled = sk_hermes_pulled;
if (returned) *returned = sk_hermes_returned;
if (consumed) *consumed = sk_hermes_consumed;
}
int sk_hermes_audit_values(uint64_t held, uint64_t pulled, uint64_t returned, uint64_t consumed) {
/* Epsilon zero (SXXXVII.3): exact integer equality, no tolerance. */
return held == pulled - returned - consumed;
}
static uint64_t sk_hermes_audit_failures = 0;
int sk_hermes_audit(void) {
int ok = sk_hermes_audit_values(sk_hermes_held, sk_hermes_pulled,
sk_hermes_returned, sk_hermes_consumed);
if (!ok) {
sk_hermes_audit_failures++;
console_println("Kernel-Hermes AUDIT FAILURE: held != pulled - returned - consumed");
}
return ok;
}
uint64_t sk_hermes_audit_failure_count(void) {
return sk_hermes_audit_failures;
}
uint64_t sk_hermes_scan_held(size_t *live_count) {
uint64_t sum = 0;
size_t n = 0;
int i;
for (i = 0; i < SK_HERMES_MSG_MAX; i++) {
if (!sk_hermes_msgs[i].in_use || sk_hermes_msgs[i].stadium_cell < 0) continue;
sum += stadium_cells()[sk_hermes_msgs[i].stadium_cell].header.heat;
n++;
}
if (live_count) *live_count = n;
return sum;
}
int sk_hermes_scan_check(void) {
return sk_hermes_scan_held((size_t *)0) == sk_hermes_held;
}
static int sk_hermes_find_free_slot(void) {
int i;
for (i = 0; i < SK_HERMES_MSG_MAX; i++) {
if (!sk_hermes_msgs[i].in_use) return i;
}
return -1;
}
int sk_hermes_alloc(VMUuid vm_id, SkHermesMessage **out_msg) {
int slot;
uint64_t pulled;
StadiumPatronHeader candidate;
size_t cell;
if (!out_msg) return -1;
/* Check affordability before touching the reservoir at all -- this is
* what "roll back on refusal" reduces to when the check happens
* first: there is nothing to roll back. */
if (stadium_reservoir_peek(vm_id) < SK_HERMES_Q_SLOT) return -1;
slot = sk_hermes_find_free_slot();
if (slot < 0) return -1; /* arena full, reservoir untouched */
pulled = stadium_reservoir_pull(vm_id, SK_HERMES_Q_SLOT);
if (pulled < SK_HERMES_Q_SLOT) {
/* Should not happen given the peek check above (nothing else runs
* between the two calls on this single-core, cooperative kernel),
* but roll back explicitly rather than trust that invariant
* silently. */
if (pulled > 0) stadium_reservoir_push(vm_id, pulled);
return -1;
}
memset(&candidate, 0, sizeof(candidate));
/* Matches messaging.4th's own MSG-ALLOC: the slot's own index becomes
* the admitted patron's identity, which stadium_dispatch()'s DELIVER
* case later prints back as "msg_idx=" on release -- an arbitrary
* distinct value would satisfy the conservation math just as well,
* but this keeps the diagnostic meaningful instead of every message
* printing identity 0. */
candidate.identity = (uint64_t)slot;
candidate.heat = pulled; /* the reservoir pull itself, unmodified */
candidate.ttl = 0; /* decay/TTL is task 2.5's scope, not this one's */
candidate.link = 0;
candidate.contains = STADIUM_CONTAINS_NONE;
candidate.mass = 1; /* message body lives in sk_hermes_msgs[], not here */
candidate.flags = 0;
candidate.behaviour = (uint8_t)STADIUM_BEHAVIOUR_DELIVER;
cell = stadium_admit(vm_id, &candidate);
if (cell == STADIUM_CELL_NONE) {
/* Stadium floor itself refused (unrelated to heat affordability,
* already confirmed above) -- roll back the pull, same "leave
* everything exactly as found" discipline as every other refusal
* path here. */
stadium_reservoir_push(vm_id, pulled);
return -1;
}
memset(&sk_hermes_msgs[slot], 0, sizeof(SkHermesMessage));
sk_hermes_msgs[slot].in_use = 1;
sk_hermes_msgs[slot].stadium_cell = (int32_t)cell;
sk_hermes_msgs[slot].owner = vm_id;
*out_msg = &sk_hermes_msgs[slot];
/* Task 2.4: the one site where held/pulled increment -- only reached
* once every refusal path above has already returned. */
sk_hermes_held += pulled;
sk_hermes_pulled += pulled;
sk_hermes_audit();
return 0;
}
int sk_hermes_release(SkHermesMessage *msg) {
int rc;
uint64_t remaining;
if (!msg || !msg->in_use) return -1;
if (msg->stadium_cell < 0) {
memset(msg, 0, sizeof(*msg));
return -1;
}
/* Read the patron's current heat BEFORE eviction -- stadium_evict()
* zeroes the header as part of returning the cell to the free list,
* and its own return value is a success code, not the amount
* credited. Today (before task 2.5's decay exists) this always
* equals exactly what was pulled at allocation; once decay exists,
* this is the message's true remaining heat, which is what actually
* flows back to the reservoir -- reading it here rather than
* assuming the original pulled amount is what keeps this correct
* without changes once task 2.5 lands. */
remaining = stadium_cells()[msg->stadium_cell].header.heat;
/* stadium_evict() returns the departing patron's remaining heat to
* its owner's reservoir itself (stadium.c: "the departing patron's
* remaining heat must flow back to its owner's reservoir before the
* cell returns to the free list") -- release does not touch the
* reservoir directly, the eviction path does it, matching
* MSG-FREE-NODE's own shape ("DUP 5 CELLS + @ STADIUM-EVICT DROP"). */
rc = stadium_evict((size_t)msg->stadium_cell);
/* Task 2.4: the one site where held/returned change on release. Only
* on successful eviction -- a refused eviction leaves the patron
* (and its heat) exactly where it was, so the ledger must not move
* either. */
if (rc == 0) {
sk_hermes_held -= remaining;
sk_hermes_returned += remaining;
sk_hermes_audit();
}
memset(msg, 0, sizeof(*msg));
return rc;
}
int sk_hermes_decay(SkHermesMessage *msg) {
uint64_t before, after;
StadiumCell *cell;
if (!msg || !msg->in_use || msg->stadium_cell < 0) return -1;
cell = &stadium_cells()[msg->stadium_cell];
before = cell->header.heat;
after = (uint64_t)q48_mul((q48_16_t)before, (q48_16_t)SK_HERMES_Q_DECAY);
cell->header.heat = after;
/* Task 2.5: the one site where consumed increments (and where held
* pays for it). SXXXVII.3: decay's delta is exact and in hand here. */
sk_hermes_consumed += before - after;
sk_hermes_held -= before - after;
stadium_consumed_record(msg->owner, before - after);
sk_hermes_audit();
return 0;
}
/* Task 3.2 (item 27, B1 / FABRIC-3.5.md SXLV.1): the channel table. See
* kernel_hermes.h's own doc comment on this section for scope and sizing
* reasoning. */
static SkHermesChannel *sk_hermes_channels = (SkHermesChannel *)0;
static int sk_hermes_channel_capacity_val = 0;
int sk_hermes_channels_boot_init(void) {
size_t max_vm_count = stadium_max_vm_count();
SkHermesChannel *table;
size_t i;
if (max_vm_count == 0) return -1; /* Stadium not yet initialized */
table = (SkHermesChannel *)kmalloc(max_vm_count * sizeof(SkHermesChannel));
if (!table) return -1;
for (i = 0; i < max_vm_count; i++) {
memset(&table[i], 0, sizeof(SkHermesChannel));
}
/* The common channel exists from boot (SXLV.1), empty -- VMs join it
* at birth (kernel_main.c/capsule_birth.c task 3.2 wiring), not here. */
table[SK_HERMES_CHANNEL_COMMON].in_use = 1;
sk_hermes_channels = table;
sk_hermes_channel_capacity_val = (int)max_vm_count;
console_puts("Kernel-Hermes: ");
console_put_u64((uint64_t)max_vm_count);
console_println(" channel slots");
return 0;
}
int sk_hermes_channel_capacity(void) {
return sk_hermes_channel_capacity_val;
}
static int channel_valid(int channel_id) {
return sk_hermes_channels &&
channel_id >= 0 &&
channel_id < sk_hermes_channel_capacity_val &&
sk_hermes_channels[channel_id].in_use;
}
int sk_hermes_channel_create(void) {
int i;
if (!sk_hermes_channels) return -1;
/* Index 0 is always in_use (the common channel), so this scan never
* returns it -- start at 1 purely as a scan-cost micro-optimisation,
* not for correctness (the in_use check alone would already skip it). */
for (i = 1; i < sk_hermes_channel_capacity_val; i++) {
if (!sk_hermes_channels[i].in_use) {
memset(&sk_hermes_channels[i], 0, sizeof(SkHermesChannel));
sk_hermes_channels[i].in_use = 1;
return i;
}
}
return -1;
}
int sk_hermes_channel_destroy(int channel_id) {
if (channel_id == SK_HERMES_CHANNEL_COMMON) return -1; /* permanent, SXLV.1 */
if (!channel_valid(channel_id)) return -1;
memset(&sk_hermes_channels[channel_id], 0, sizeof(SkHermesChannel));
return 0;
}
int sk_hermes_channel_subscribe(int channel_id, VMUuid vm_id) {
SkHermesMembership *m;
size_t i;
if (!channel_valid(channel_id)) return -1;
m = &sk_hermes_channels[channel_id].membership;
for (i = 0; i < m->count; i++) {
if (vm_uuid_equal(m->members[i], vm_id)) return -1; /* already a member */
}
if (m->count >= SK_HERMES_MEMBER_MAX) return -1;
m->members[m->count] = vm_id;
m->count++;
return 0;
}
int sk_hermes_channel_unsubscribe(int channel_id, VMUuid vm_id) {
SkHermesMembership *m;
size_t i;
if (!channel_valid(channel_id)) return -1;
m = &sk_hermes_channels[channel_id].membership;
for (i = 0; i < m->count; i++) {
if (vm_uuid_equal(m->members[i], vm_id)) {
size_t j;
for (j = i; j < m->count - 1; j++) {
m->members[j] = m->members[j + 1];
}
m->count--;
return 0;
}
}
return -1; /* not a member */
}
int sk_hermes_channel_is_member(int channel_id, VMUuid vm_id) {
SkHermesMembership *m;
size_t i;
if (!channel_valid(channel_id)) return 0;
m = &sk_hermes_channels[channel_id].membership;
for (i = 0; i < m->count; i++) {
if (vm_uuid_equal(m->members[i], vm_id)) return 1;
}
return 0;
}
int sk_hermes_channel_member_count(int channel_id) {
if (!channel_valid(channel_id)) return -1;
return (int)sk_hermes_channels[channel_id].membership.count;
}
/* Task 3.3: per-subscriber pending queue table. See kernel_hermes.h's own
* doc comments on sk_hermes_queues_boot_init()/SK_HERMES_PENDING_MAX for
* scope and sizing reasoning. A queue is found-or-created lazily by
* vm_id, same shape as stadium.c's quota_slot_for_vm() and session.c's
* session_find() -- not pre-populated at birth like channel membership
* is, since not every VM ever receives a message. */
typedef struct {
VMUuid vm_id;
int in_use;
int slots[SK_HERMES_PENDING_MAX]; /* sk_hermes_msgs[] indices, circular FIFO */
size_t head;
size_t count;
} SkHermesPendingQueue;
static SkHermesPendingQueue *sk_hermes_queues = (SkHermesPendingQueue *)0;
static int sk_hermes_queue_capacity_val = 0;
int sk_hermes_queues_boot_init(void) {
size_t max_vm_count = stadium_max_vm_count();
SkHermesPendingQueue *table;
size_t i;
if (max_vm_count == 0) return -1; /* Stadium not yet initialized */
table = (SkHermesPendingQueue *)kmalloc(max_vm_count * sizeof(SkHermesPendingQueue));
if (!table) return -1;
for (i = 0; i < max_vm_count; i++) {
memset(&table[i], 0, sizeof(SkHermesPendingQueue));
}
sk_hermes_queues = table;
sk_hermes_queue_capacity_val = (int)max_vm_count;
console_puts("Kernel-Hermes: ");
console_put_u64((uint64_t)max_vm_count);
console_println(" pending-queue slots");
return 0;
}
static SkHermesPendingQueue *find_queue(VMUuid vm_id) {
int i;
if (!sk_hermes_queues) return (SkHermesPendingQueue *)0;
for (i = 0; i < sk_hermes_queue_capacity_val; i++) {
if (sk_hermes_queues[i].in_use && vm_uuid_equal(sk_hermes_queues[i].vm_id, vm_id)) {
return &sk_hermes_queues[i];
}
}
return (SkHermesPendingQueue *)0;
}
static SkHermesPendingQueue *find_or_create_queue(VMUuid vm_id) {
int i, free_slot = -1;
if (!sk_hermes_queues) return (SkHermesPendingQueue *)0;
for (i = 0; i < sk_hermes_queue_capacity_val; i++) {
if (sk_hermes_queues[i].in_use && vm_uuid_equal(sk_hermes_queues[i].vm_id, vm_id)) {
return &sk_hermes_queues[i];
}
if (!sk_hermes_queues[i].in_use && free_slot < 0) free_slot = i;
}
if (free_slot < 0) return (SkHermesPendingQueue *)0; /* table full */
memset(&sk_hermes_queues[free_slot], 0, sizeof(SkHermesPendingQueue));
sk_hermes_queues[free_slot].vm_id = vm_id;
sk_hermes_queues[free_slot].in_use = 1;
return &sk_hermes_queues[free_slot];
}
/* Task 3.4: system-wide sum of every queue's count, maintained alongside
* queue_push()/sk_hermes_pending_pop() below -- lets
* sk_hermes_drain_checkpoint() skip its per-VM queue lookup entirely
* (stadium_max_vm_count()-sized linear scan) on every word dispatch when
* nothing is pending anywhere, the overwhelmingly common case. */
static int sk_hermes_pending_total = 0;
static int queue_push(SkHermesPendingQueue *q, int msg_index) {
size_t tail;
if (q->count >= SK_HERMES_PENDING_MAX) return -1;
tail = (q->head + q->count) % SK_HERMES_PENDING_MAX;
q->slots[tail] = msg_index;
q->count++;
sk_hermes_pending_total++;
return 0;
}
int sk_hermes_send_one(VMUuid from, VMUuid to, uint32_t type, uint32_t channel_id,
void *payload_addr, uint32_t payload_len) {
SkHermesMessage *msg;
SkHermesPendingQueue *q;
int idx;
/* Task 3.5: the one-block bound, enforced uniformly here whether or
* not payload_addr happens to be a chunk carrier -- see
* kernel_hermes.h's own sizing note on why SK_HERMES_CHUNK_MAX_SLICE
* (not SK_HERMES_CHUNK_MAX_PAYLOAD) is a chunk's own content size. */
if (payload_len > SK_HERMES_CHUNK_MAX_PAYLOAD) return -1;
if (sk_hermes_alloc(from, &msg) != 0) return -1;
/* Real defect, found 2026-09-22 (FABRIC-3.6.md task 3.8's own
* findings log): this used to store the caller's own payload_addr
* as-is, a pointer this function does not own. Two sends before
* either drains meant both messages pointed at the same
* caller-owned buffer -- whichever send wrote last silently won.
* Copy into this message's own payload_buf instead (kernel_hermes.h,
* sized to SK_HERMES_CHUNK_MAX_PAYLOAD, the bound already enforced
* above) and point payload_addr at that -- every message now owns
* its payload bytes, independent of whatever the caller does with
* its own buffer afterward. payload_len == 0 is a real, valid case
* (an empty payload) -- memcpy of 0 bytes is well-defined even with
* a NULL payload_addr -- guarded anyway (C's memcpy() is technically
* undefined for a NULL argument even at length 0), matching this
* project's own STRICT_PTR discipline elsewhere. */
if (payload_len > 0) memcpy(msg->payload_buf, payload_addr, payload_len);
msg->payload_addr = msg->payload_buf;
msg->type = type;
msg->from = from;
msg->to = to;
msg->payload_len = payload_len;
msg->channel = channel_id;
q = find_or_create_queue(to);
if (!q) {
sk_hermes_release(msg); /* roll back -- no destination queue */
return -1;
}
idx = (int)(msg - sk_hermes_msgs);
if (queue_push(q, idx) != 0) {
sk_hermes_release(msg); /* roll back -- destination queue full */
return -1;
}
return 0;
}
int sk_hermes_publish(VMUuid from, int channel_id, uint32_t type,
void *payload_addr, uint32_t payload_len) {
SkHermesMembership *m;
size_t i;
int delivered = 0;
if (!channel_valid(channel_id)) return -1;
if (payload_len > SK_HERMES_CHUNK_MAX_PAYLOAD) return -1; /* see sk_hermes_send_one()'s
* own check -- duplicated here
* so publish() itself returns
* -1 (not a silent 0) on an
* oversized payload */
m = &sk_hermes_channels[channel_id].membership;
for (i = 0; i < m->count; i++) {
if (sk_hermes_send_one(from, m->members[i], type, (uint32_t)channel_id,
payload_addr, payload_len) == 0) {
delivered++;
}
}
return delivered;
}
int sk_hermes_pending_count(VMUuid vm_id) {
SkHermesPendingQueue *q = find_queue(vm_id);
return q ? (int)q->count : 0;
}
SkHermesMessage *sk_hermes_pending_peek(VMUuid vm_id) {
SkHermesPendingQueue *q = find_queue(vm_id);
if (!q || q->count == 0) return (SkHermesMessage *)0;
return &sk_hermes_msgs[q->slots[q->head]];
}
int sk_hermes_pending_pop(VMUuid vm_id) {
SkHermesPendingQueue *q = find_queue(vm_id);
if (!q || q->count == 0) return -1;
q->head = (q->head + 1) % SK_HERMES_PENDING_MAX;
q->count--;
sk_hermes_pending_total--;
return 0;
}
int sk_hermes_drain_checkpoint(VM *vm) {
SkHermesMessage *msg;
VMUuid self;
char saved_input[INPUT_BUFFER_SIZE];
size_t saved_length, saved_pos;
vm_mode_t saved_mode;
int saved_error, saved_abort;
int drain_error;
if (!vm) return -1;
if (sk_hermes_pending_total == 0) return 0; /* fast path -- see this
* counter's own doc comment */
self = vm->stadium_vm_id;
msg = sk_hermes_pending_peek(self);
if (!msg) return 0;
/* See kernel_hermes.h's own doc comment on this function for why all
* six of these must be preserved, not just the obvious ones. */
memcpy(saved_input, vm->input_buffer, sizeof(saved_input));
saved_length = vm->input_length;
saved_pos = vm->input_pos;
saved_mode = vm->mode;
saved_error = vm->error;
saved_abort = vm->abort_requested;
/* FABRIC-3.6.md task 3.9 evidence: CONSOLE-CMD-EVENT has no fixed
* FORTH handler word to hang a print on (task 3.8's BLK-ATTACH-ACK
* did) -- its payload is an arbitrary console line, interpreted
* as-is below. So the evidence line lives here instead, gated on
* msg->type, printed before vm_interpret() runs while this
* message's heat is still held (release()/pop() are further down
* in this same function) -- the same mid-hold-instant evidence task
* 3.8 used, same honest limit: evidence for mid-hold, not
* post-release. console_println, not log_message() (confirmed
* invisible in this build's serial capture, task 3.8's own
* finding); fires once per relayed console line, not per word. */
if (msg->type == SK_HERMES_MSG_TYPE_CONSOLE_CMD) {
uint64_t held, pulled, returned, consumed;
char line[160];
int conserved = stadium_conserved(self);
sk_hermes_ledger(&held, &pulled, &returned, &consumed);
snprintf(line, sizeof(line),
"Kernel-Hermes CONSOLE-CMD-EVENT (real, Stage D): ledger held=%llu "
"pulled=%llu returned=%llu consumed=%llu stadium_conserved(self)=%s",
(unsigned long long)held, (unsigned long long)pulled,
(unsigned long long)returned, (unsigned long long)consumed,
conserved ? "true" : "FALSE");
console_println(line);
}
/* FABRIC-3.6.md task 3.10 evidence: ELEVATE-REQUEST also has no fixed
* C-side handler word -- its payload is a FORTH command string
* calling ELEVATE-GRANT (zuse-eligibility.4th), interpreted as-is
* below, same shape as CONSOLE-CMD-EVENT above. Same mid-hold-instant
* evidence, same reasoning; `self` here is always Hera (`vm`'s own
* caller already resolved it that way in KH-ELEVATE-SEND, since
* ELEVATE-GRANT only ever runs on Hera). */
if (msg->type == SK_HERMES_MSG_TYPE_ELEVATE_REQUEST) {
uint64_t held, pulled, returned, consumed;
char line[160];
int conserved = stadium_conserved(self);
sk_hermes_ledger(&held, &pulled, &returned, &consumed);
snprintf(line, sizeof(line),
"Kernel-Hermes ELEVATE-REQUEST (real, Stage D): ledger held=%llu "
"pulled=%llu returned=%llu consumed=%llu stadium_conserved(self)=%s",
(unsigned long long)held, (unsigned long long)pulled,
(unsigned long long)returned, (unsigned long long)consumed,
conserved ? "true" : "FALSE");
console_println(line);
}
vm->mode = MODE_INTERPRET; /* a payload is never compiled into whatever
* the enclosing context was mid-defining */
vm_interpret(vm, (const char *)msg->payload_addr);
drain_error = vm->error;
memcpy(vm->input_buffer, saved_input, sizeof(saved_input));
vm->input_length = saved_length;
vm->input_pos = saved_pos;
vm->mode = saved_mode;
vm->error = saved_error; /* a bad message must not abort the
* enclosing execution */
vm->abort_requested = saved_abort;
sk_hermes_release(msg);
sk_hermes_pending_pop(self);
return drain_error ? -1 : 1;
}
uint32_t sk_hermes_chunk_count(uint32_t len) {
if (len == 0) return 0;
return (len + SK_HERMES_CHUNK_MAX_SLICE - 1) / SK_HERMES_CHUNK_MAX_SLICE;
}
int sk_hermes_reassemble(SkHermesMessage **chunks, int n_chunks,
uint8_t *out_buf, uint32_t out_buf_cap,
uint32_t *out_len) {
int present[SK_HERMES_MSG_MAX];
int i;
uint32_t msg_id;
uint32_t total;
uint32_t last_slice_len = 0;
if (!chunks || n_chunks <= 0 || !out_buf || !out_len) return -1;
if (n_chunks > SK_HERMES_MSG_MAX) return -1; /* guard before indexing present[] */
for (i = 0; i < n_chunks; i++) present[i] = 0;
if (!chunks[0] || !chunks[0]->payload_addr ||
chunks[0]->payload_len < (uint32_t)sizeof(SkHermesChunkHeader)) return -1;
msg_id = ((SkHermesChunkHeader *)chunks[0]->payload_addr)->msg_id;
for (i = 0; i < n_chunks; i++) {
SkHermesMessage *m = chunks[i];
SkHermesChunkHeader *h;
uint32_t slice_len;
if (!m || !m->payload_addr || m->payload_len < (uint32_t)sizeof(SkHermesChunkHeader)) return -1;
h = (SkHermesChunkHeader *)m->payload_addr;
slice_len = m->payload_len - (uint32_t)sizeof(SkHermesChunkHeader);
if (h->msg_id != msg_id) return -1;
if (h->seq >= (uint32_t)n_chunks) return -1;
if (present[h->seq]) return -1; /* duplicate seq */
present[h->seq] = 1;
if (h->seq == (uint32_t)(n_chunks - 1)) {
if (!h->is_last) return -1;
if (slice_len == 0 || slice_len > SK_HERMES_CHUNK_MAX_SLICE) return -1;
last_slice_len = slice_len;
} else {
if (h->is_last) return -1;
if (slice_len != SK_HERMES_CHUNK_MAX_SLICE) return -1;
}
}
for (i = 0; i < n_chunks; i++) if (!present[i]) return -1; /* gap */
/* Total computed once from validated seq completeness, checked once
* against out_buf_cap, before any memcpy -- never order-dependent on
* which chunk happens to overflow first. */
total = (uint32_t)(n_chunks - 1) * SK_HERMES_CHUNK_MAX_SLICE + last_slice_len;
if (total > out_buf_cap) return -1;
for (i = 0; i < n_chunks; i++) {
SkHermesMessage *m = chunks[i];
SkHermesChunkHeader *h = (SkHermesChunkHeader *)m->payload_addr;
uint32_t slice_len = m->payload_len - (uint32_t)sizeof(SkHermesChunkHeader);
uint32_t offset = h->seq * SK_HERMES_CHUNK_MAX_SLICE;
memcpy(out_buf + offset, (uint8_t *)m->payload_addr + sizeof(SkHermesChunkHeader), slice_len);
}
*out_len = total;
return 0;
}
int sk_hermes_channel_request(VMUuid requester, VMUuid target) {
return sk_hermes_send_one(requester, target, SK_HERMES_MSG_TYPE_CH_REQUEST,
SK_HERMES_CHANNEL_COMMON, (void *)0, 0);
}
int sk_hermes_channel_respond(VMUuid target, VMUuid requester, int approved) {
int ch = -1;
if (approved) {
ch = sk_hermes_channel_create();
if (ch >= 0) {
if (sk_hermes_channel_subscribe(ch, requester) != 0 ||
sk_hermes_channel_subscribe(ch, target) != 0) {
sk_hermes_channel_destroy(ch); /* leave no half-open channel */
ch = -1;
}
}
}
if (ch >= 0) {
sk_hermes_send_one(target, requester, SK_HERMES_MSG_TYPE_CH_GRANT,
(uint32_t)ch, (void *)0, 0);
/* Ruled ACK cadence (2026-09-21): channel-open + delivery, not
* every message -- this IS that moment. */
sk_hermes_send_one(target, requester, SK_HERMES_MSG_TYPE_ACK,
(uint32_t)ch, (void *)0, 0);
} else {
/* SXLV.1: "a deny is a NACK" -- no separate CH_DENY type. */
sk_hermes_send_one(target, requester, SK_HERMES_MSG_TYPE_NACK,
SK_HERMES_CHANNEL_COMMON, (void *)0, 0);
}
return ch;
}
int sk_hermes_channel_close(VMUuid closer, int channel_id) {
if (channel_id == SK_HERMES_CHANNEL_COMMON) return -1;
if (!sk_hermes_channel_is_member(channel_id, closer)) return -1;
return sk_hermes_channel_destroy(channel_id);
}
int sk_hermes_channel_open_policy(VM *target_vm, VMUuid requester) {
static const char word_name[] = "HERMES-CHANNEL-OPEN?";
DictEntry *entry;
DictEntry *saved_entry;
cell_t result;
if (!target_vm) return 0;
entry = vm_find_word(target_vm, word_name, sizeof(word_name) - 1);
if (!entry || !entry->func) return 0; /* fail closed: no policy, no open */
/* Colon words dispatch through execute_colon_word(), which reads its
* own body address from vm->current_executing_entry and no-ops
* silently if it is NULL (vm_core.c:730) -- vm_interpret_word() always
* sets this immediately before calling entry->func(); matched here
* for the same reason (found live: without it, every call silently
* did nothing, leaving the pushed requester untouched on the stack
* rather than erroring, which is why this needed a debug session to
* catch rather than showing up as an obvious crash). Saved and
* restored, not just set, in case target_vm is ever called into
* mid-dispatch (not exercised by this task's own self-test, but no
* reason to assume it away). */
saved_entry = target_vm->current_executing_entry;
target_vm->current_executing_entry = entry;
vm_push(target_vm, (cell_t)requester.hi);
vm_push(target_vm, (cell_t)requester.lo);
entry->func(target_vm);
target_vm->current_executing_entry = saved_entry;
if (target_vm->error || target_vm->dsp < 0) {
target_vm->error = 0; /* a broken policy word must not leak into
* whatever else target_vm is doing */
return 0;
}
result = vm_pop(target_vm);
return result != 0;
}
#endif /* __STARKERNEL__ */