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>
179 lines
7.8 KiB
C
179 lines
7.8 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.
|
||
*/
|
||
|
||
/**
|
||
* log_region.c - Growable per-VM log-persistence ring on Artemis's disk.
|
||
* See starkernel/log_region.h for the format/interface design.
|
||
*
|
||
* Deliberately never calls log_message() (or anything documented to call
|
||
* it) -- this runs inside Artemis's own dictionary context during message
|
||
* delivery (LOG-APPEND, dispatched by a sender's MSG-SEND/MSG-TICK); a log
|
||
* call here would append to Artemis's own buffer and eventually flush back
|
||
* through this same path.
|
||
*/
|
||
|
||
#include "starkernel/log_region.h"
|
||
|
||
#include <stddef.h>
|
||
#include <string.h>
|
||
|
||
#include "block_subsystem.h" /* blk_meta_zone_read/write, compute_crc64 */
|
||
|
||
static uint64_t log_region_ctrl_compute_crc(const log_region_ctrl_t *ctrl) {
|
||
size_t crc_span = offsetof(log_region_ctrl_t, hdr_crc);
|
||
return compute_crc64((const uint8_t *)ctrl, crc_span);
|
||
}
|
||
|
||
/* Reads the control header; returns 0 and *out populated only if magic,
|
||
* version, and CRC all check out. Any other result (blank, foreign,
|
||
* corrupt, or a read failure) is treated identically by the caller: "not
|
||
* yet initialized," matching genesis_marker_read()'s own discipline. */
|
||
static int log_region_ctrl_read(log_region_ctrl_t *out) {
|
||
if (blk_meta_zone_read(LOG_REGION_DEVBLOCK_FROM_TOP_BASE, (uint8_t *)out) != 0) return -1;
|
||
if (LOG_REGION_GET_MAGIC(out->magic) != (uint32_t)(LOG_REGION_MAGIC & 0xFFFFFFFFull)) return -1;
|
||
if (LOG_REGION_GET_VERSION(out->magic) != LOG_REGION_VERSION_0) return -1;
|
||
uint64_t want_crc = log_region_ctrl_compute_crc(out);
|
||
if (want_crc != out->hdr_crc) return -1;
|
||
return 0;
|
||
}
|
||
|
||
static int log_region_ctrl_write(log_region_ctrl_t *ctrl) {
|
||
ctrl->hdr_crc = log_region_ctrl_compute_crc(ctrl);
|
||
return blk_meta_zone_write(LOG_REGION_DEVBLOCK_FROM_TOP_BASE, (const uint8_t *)ctrl) == 0 ? 0 : -1;
|
||
}
|
||
|
||
static void log_region_ctrl_init_fresh(log_region_ctrl_t *ctrl) {
|
||
memset(ctrl, 0, sizeof(*ctrl));
|
||
ctrl->magic = LOG_REGION_PACK(LOG_REGION_VERSION_0);
|
||
ctrl->devblocks = LOG_REGION_INITIAL_DEVBLOCKS;
|
||
ctrl->head_slot = 0;
|
||
ctrl->tail_slot = 0;
|
||
ctrl->record_count = 0;
|
||
}
|
||
|
||
/* Reads/writes the devblock holding slot_index (LOG_SLOTS_PER_DEVBLOCK
|
||
* slots per devblock), one devblock_from_top past the control header for
|
||
* every LOG_SLOTS_PER_DEVBLOCK slots. */
|
||
static uint32_t log_region_slot_devblock_from_top(uint32_t slot_index) {
|
||
return LOG_REGION_DEVBLOCK_FROM_TOP_BASE + 1u + (slot_index / LOG_SLOTS_PER_DEVBLOCK);
|
||
}
|
||
|
||
static int log_region_read_slot(uint32_t slot_index, log_slot_t *out) {
|
||
uint8_t devblock_buf[4096];
|
||
uint32_t dft = log_region_slot_devblock_from_top(slot_index);
|
||
uint32_t slot_in_devblock = slot_index % LOG_SLOTS_PER_DEVBLOCK;
|
||
|
||
if (blk_meta_zone_read(dft, devblock_buf) != 0) return -1;
|
||
memcpy(out, devblock_buf + (size_t)slot_in_devblock * LOG_SLOT_SIZE, sizeof(*out));
|
||
return 0;
|
||
}
|
||
|
||
static int log_region_write_slot(uint32_t slot_index, const log_slot_t *slot) {
|
||
uint8_t devblock_buf[4096];
|
||
uint32_t dft = log_region_slot_devblock_from_top(slot_index);
|
||
uint32_t slot_in_devblock = slot_index % LOG_SLOTS_PER_DEVBLOCK;
|
||
|
||
/* Read-modify-write: blk_meta_zone_*() only operates at whole-devblock
|
||
* granularity, and a devblock holds LOG_SLOTS_PER_DEVBLOCK slots, so a
|
||
* single-slot write must preserve its siblings. A read failure on a
|
||
* never-yet-written devblock (blank fence content) is not fatal here --
|
||
* proceed with a zeroed buffer, matching zuse_eligibility.c's own
|
||
* "blank fence content is indistinguishable from absent, treat as
|
||
* absent" discipline. */
|
||
if (blk_meta_zone_read(dft, devblock_buf) != 0) {
|
||
memset(devblock_buf, 0, sizeof(devblock_buf));
|
||
}
|
||
memcpy(devblock_buf + (size_t)slot_in_devblock * LOG_SLOT_SIZE, slot, sizeof(*slot));
|
||
return blk_meta_zone_write(dft, devblock_buf) == 0 ? 0 : -1;
|
||
}
|
||
|
||
int log_region_append(uint8_t level, uint64_t timestamp,
|
||
const char *source, uint32_t source_len,
|
||
const char *msg, uint32_t msg_len) {
|
||
log_region_ctrl_t ctrl;
|
||
if (log_region_ctrl_read(&ctrl) != 0) {
|
||
log_region_ctrl_init_fresh(&ctrl);
|
||
}
|
||
|
||
uint32_t total_slots = ctrl.devblocks * LOG_SLOTS_PER_DEVBLOCK;
|
||
if (ctrl.record_count >= total_slots) {
|
||
if (ctrl.devblocks < LOG_REGION_MAX_DEVBLOCKS) {
|
||
/* Grow: new slots appear at indices [old_total_slots ..
|
||
* new_total_slots-1], nothing already written moves -- safe
|
||
* to do at any point in the ring's lifecycle regardless of
|
||
* wrap state, since head/tail are slot INDICES modulo
|
||
* total_slots, and the modulus is simply larger from here on. */
|
||
ctrl.devblocks += LOG_REGION_GROWTH_INCREMENT;
|
||
if (ctrl.devblocks > LOG_REGION_MAX_DEVBLOCKS) ctrl.devblocks = LOG_REGION_MAX_DEVBLOCKS;
|
||
total_slots = ctrl.devblocks * LOG_SLOTS_PER_DEVBLOCK;
|
||
}
|
||
if (ctrl.record_count >= total_slots) {
|
||
/* Still full (growth ceiling already reached) -- evict oldest,
|
||
* unless doing so would discard higher-priority history for a
|
||
* lower-priority newcomer (FABRIC-3.md §XXXII.4, 2026-09-15:
|
||
* plain FIFO eviction here was flagged as level-blind -- a
|
||
* flood of INFO/DEBUG noise could push out real ERROR/WARN
|
||
* records). Read the oldest slot's own level before evicting:
|
||
* if it's ERROR/WARN and the incoming record is not, protect
|
||
* it -- drop the incoming record instead of evicting real
|
||
* history. Any other combination (oldest is already low
|
||
* priority, or the incoming record is itself ERROR/WARN)
|
||
* evicts exactly as before. A read failure on the oldest slot
|
||
* (should not happen -- it was written by this same function)
|
||
* falls back to the prior unconditional-eviction behavior
|
||
* rather than wedging the ring. */
|
||
log_slot_t oldest;
|
||
int oldest_protected = 0;
|
||
if (log_region_read_slot(ctrl.head_slot, &oldest) == 0) {
|
||
oldest_protected = (oldest.level <= LOG_REGION_PROTECTED_MAX_LEVEL);
|
||
}
|
||
int incoming_protected = (level <= LOG_REGION_PROTECTED_MAX_LEVEL);
|
||
if (oldest_protected && !incoming_protected) {
|
||
return 1; /* dropped by design, not a failure -- see header */
|
||
}
|
||
ctrl.head_slot = (ctrl.head_slot + 1u) % total_slots;
|
||
ctrl.record_count--;
|
||
}
|
||
}
|
||
|
||
log_slot_t slot;
|
||
memset(&slot, 0, sizeof(slot));
|
||
slot.timestamp = timestamp;
|
||
slot.level = level;
|
||
|
||
uint32_t src_n = source_len;
|
||
if (src_n > LOG_SLOT_SOURCE_MAX) src_n = LOG_SLOT_SOURCE_MAX;
|
||
memcpy(slot.source, source, src_n);
|
||
|
||
uint32_t msg_n = msg_len;
|
||
if (msg_n > LOG_SLOT_MSG_MAX) msg_n = LOG_SLOT_MSG_MAX;
|
||
memcpy(slot.msg, msg, msg_n);
|
||
slot.msg_len = (uint16_t)msg_n;
|
||
|
||
if (log_region_write_slot(ctrl.tail_slot, &slot) != 0) return -1;
|
||
|
||
ctrl.tail_slot = (ctrl.tail_slot + 1u) % total_slots;
|
||
if (ctrl.record_count < total_slots) ctrl.record_count++;
|
||
|
||
return log_region_ctrl_write(&ctrl);
|
||
}
|