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>
This commit is contained in:
rajames
2026-10-01 15:40:09 -04:00
co-authored by Junie
parent 3c709c115b
commit a8b70e88d3
630 changed files with 2646 additions and 2036 deletions
+178
View File
@@ -0,0 +1,178 @@
/*
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);
}