/* 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 #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__ */