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>
185 lines
7.2 KiB
C
185 lines
7.2 KiB
C
/*
|
||
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__ */
|