Files
LithosAnanake/src/starkernel/vm/kernel_hermes.c
T
Robert Allan JamesandClaude Sonnet 5 493d410028 Add the four ledger counters (held/pulled/returned/consumed) -- FABRIC-3.6.md task 2.4
FABRIC-3.5.md SXL.4's ledger: held == pulled - returned - consumed,
epsilon zero. Added sk_hermes_ledger() (an accessor, not a mutator)
plus four static counters in kernel_hermes.c.

Each counter has exactly one increment/decrement site: held/pulled
both move at sk_hermes_alloc()'s single success path, after every
refusal branch has already returned; held/returned both move at
sk_hermes_release()'s single success path. consumed is declared and
always reads 0 -- its one increment site doesn't exist yet, and won't
until task 2.5 gives decay something to record.

sk_hermes_release() now reads the Stadium cell's live header.heat
immediately before calling stadium_evict(), rather than assuming the
original pulled amount -- stadium_evict() zeroes the header as part of
freeing the cell and its own return value is a success code, not the
credited amount, so this is the only point the true remaining heat is
available. Today this always equals the original Q.SLOT pull; once
task 2.5's decay exists, this is what keeps returned correct without
touching this function again.

Self-test (kernel_main.c) extended: snapshots the ledger before
running so it checks its own deltas, verifies held/pulled grow by
exactly got_n * Q_SLOT on allocation with returned/consumed untouched,
then verifies held returns to its starting value and returned grows by
the same amount on release, and checks the audit invariant itself as a
bonus (task 2.6 formalizes this properly).

Three-arch boot clean: amd64/aarch64/riscv64 all reach [zuse@Hera] ok>,
zero UNKNOWN WORD, dict_hash unmoved. All three print PASS with
identical final ledger: held=0 pulled=65536 returned=65536 consumed=0.
No compiler warnings.

Authorized by Captain Bob ("keep going with rhe 6.5 document").

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-20 05:59:09 -04:00

185 lines
7.2 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 "starkernel/vm/kernel_hermes.h"
#include "starkernel/vm/stadium.h"
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;
}
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;
*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;
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;
}
memset(msg, 0, sizeof(*msg));
return rc;
}
#endif /* __STARKERNEL__ */