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
+387
View File
@@ -0,0 +1,387 @@
/*
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.
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.
*/
/*
*** StarForth ***
physics_metadata.c - Phase 1 physics metadata helpers
Captures thermodynamic-style signals for dictionary entries without
introducing heavy runtime cost. Future phases can build on these helpers
to drive physics-aware scheduling and storage placement.
*/
#include "../include/physics_metadata.h"
#include "../include/ssm_jacquard.h"
#include <stddef.h>
#include <stdint.h>
#include <string.h>
/* L8 FINAL INTEGRATION: L1 heat tracking is always-on (internal physics signal) */
/**
* @brief Clamp a uint64_t value to the uint32_t range.
*
* @param value Input value to clamp
* @return @c UINT32_MAX if @c value > UINT32_MAX, else @c (uint32_t)value
*/
static uint32_t clamp_u32(uint64_t value) {
return (value > UINT32_MAX) ? UINT32_MAX : (uint32_t) value;
}
/**
* @brief Derive a smoothed Q8 temperature from raw execution heat.
*
* Converts @c execution_heat to Q8 by left-shifting 8 bits, clamps to
* UINT16_MAX, then applies a 1/4 EMA blend with @c prior_q8 to suppress
* jitter: result = (3*prior + target) / 4. Returns 0 for non-positive heat.
*
* @param execution_heat Raw execution heat counter for the word
* @param prior_q8 Previous smoothed temperature in Q8 format
* @return Updated smoothed temperature in Q8 format
*/
static uint16_t temperature_from_execution_heat(cell_t execution_heat, uint16_t prior_q8) {
if (execution_heat <= 0) return 0;
uint64_t scaled = ((uint64_t) execution_heat) << 8; /* convert to Q8 */
uint16_t target = (scaled > UINT16_MAX) ? UINT16_MAX : (uint16_t) scaled;
/* Smooth with a simple EMA to avoid jitter */
return (uint16_t)((3u * prior_q8 + target) / 4u);
}
/**
* @brief Derive the physics state flags bitmask from a DictEntry's word flags.
*
* Maps @c WORD_IMMEDIATE → @c PHYSICS_STATE_IMMEDIATE,
* @c WORD_PINNED → @c PHYSICS_STATE_PINNED,
* @c WORD_HIDDEN → @c PHYSICS_STATE_HIDDEN, and
* @c WORD_COMPILED → @c PHYSICS_STATE_COMPILED.
*
* @param entry DictEntry to inspect (NULL returns 0)
* @return Bitmask of @c PHYSICS_STATE_* flags
*/
static uint8_t derive_state_flags(const DictEntry *entry) {
if (!entry) return 0;
uint8_t flags = 0;
if (entry->flags & WORD_IMMEDIATE) flags |= PHYSICS_STATE_IMMEDIATE;
if (entry->flags & WORD_PINNED) flags |= PHYSICS_STATE_PINNED;
if (entry->flags & WORD_HIDDEN) flags |= PHYSICS_STATE_HIDDEN;
if (entry->flags & WORD_COMPILED) flags |= PHYSICS_STATE_COMPILED;
return flags;
}
/**
* @brief Initialise physics metadata for a newly registered DictEntry.
*
* Zeroes all @c DictPhysics fields, sets @c mass_bytes to @c header_bytes
* (the byte size of the entry header), and derives initial @c state_flags
* from the entry's word flags. Called from @c register_word() immediately
* after the entry is linked into the dictionary.
*
* @param entry DictEntry to initialise
* @param header_bytes Byte size of the entry's header region (for mass)
*/
void physics_metadata_init(DictEntry *entry, uint32_t header_bytes) {
if (!entry) return;
entry->physics.temperature_q8 = 0;
entry->physics.last_active_ns = 0;
entry->physics.last_decay_ns = 0;
entry->physics.last_decay_tick = 0;
entry->physics.mass_bytes = header_bytes;
entry->physics.avg_latency_ns = 0;
entry->physics.state_flags = derive_state_flags(entry);
entry->physics.acl_hint = 0;
entry->physics.pubsub_mask = 0;
}
/**
* @brief Update the mass (byte footprint) of a DictEntry's physics metadata.
*
* Called after a colon definition or CREATE body is fully compiled so the
* mass reflects the total compiled size, not just the header.
*
* @param entry DictEntry to update
* @param total_bytes Total compiled byte size (header + body)
*/
void physics_metadata_set_mass(DictEntry *entry, uint32_t total_bytes) {
if (!entry) return;
entry->physics.mass_bytes = total_bytes;
}
/* ============================================================================
* INTENT: Update word's physics metadata after execution
* FL1: Heat accumulation feedback loop - converts raw execution_heat counter
* to smoothed temperature_q8 metric and updates activity timestamps
* WHY: Atomic heat increment (FL1) happens in execution loop; this function
* derives secondary metrics (temperature, timestamps) for observability
* ============================================================================ */
/**
* @brief Update physics metadata after a word execution completes.
*
* Derives and stores a new smoothed @c temperature_q8 from @c execution_heat,
* and updates both @c last_active_ns and @c last_decay_ns to @c now_ns.
* Called by @c physics_post_execute() on the post-execution path.
*
* @param entry DictEntry that was just executed
* @param execution_heat Current raw execution heat counter
* @param now_ns Current monotonic timestamp in nanoseconds
*/
void physics_metadata_touch(DictEntry *entry, cell_t execution_heat, uint64_t now_ns) {
if (!entry) return;
entry->physics.temperature_q8 = temperature_from_execution_heat(execution_heat, entry->physics.temperature_q8);
entry->physics.last_active_ns = now_ns;
entry->physics.last_decay_ns = now_ns;
}
/**
* @brief Re-derive and update @c state_flags for a DictEntry.
*
* Called after word flags change (e.g. IMMEDIATE is set during compilation)
* to keep the physics state flags in sync with the word flags.
*
* @param entry DictEntry whose state flags are to be refreshed
*/
void physics_metadata_refresh_state(DictEntry *entry) {
if (!entry) return;
entry->physics.state_flags = derive_state_flags(entry);
}
/**
* @brief Record an execution latency sample into the per-word EMA.
*
* Clamps @c sample_ns to uint32_t, then blends it into @c avg_latency_ns
* with a 1/4 EMA: new = (3*prior + sample) / 4. Seeds @c avg_latency_ns
* directly from the first sample.
*
* @param entry DictEntry whose latency EMA is updated
* @param sample_ns Measured execution duration in nanoseconds
*/
void physics_metadata_record_latency(DictEntry *entry, uint64_t sample_ns) {
if (!entry) return;
uint32_t sample = clamp_u32(sample_ns);
uint32_t prior = entry->physics.avg_latency_ns;
if (prior == 0) {
entry->physics.avg_latency_ns = sample;
} else {
entry->physics.avg_latency_ns = (uint32_t)((3ull * prior + sample) / 4ull);
}
}
typedef struct physics_seed_definition {
const char *name;
uint16_t temperature_q8;
uint32_t avg_latency_ns;
uint8_t acl_hint;
uint8_t pubsub_mask;
} physics_seed_definition_t;
static const physics_seed_definition_t physics_seed_table[] = {
{"IF", 0x0100u, 400u, 0, 0},
{"ELSE", 0x0100u, 400u, 0, 0},
{"THEN", 0x0100u, 350u, 0, 0},
{"BEGIN", 0x0120u, 450u, 0, 0},
{"WHILE", 0x0120u, 450u, 0, 0},
{"REPEAT", 0x0120u, 450u, 0, 0},
{"DO", 0x0180u, 500u, 0, 0},
{"LOOP", 0x0180u, 500u, 0, 0},
{"+LOOP", 0x0200u, 520u, 0, 0},
{"LEAVE", 0x0200u, 520u, 0, 0},
{".", 0x0080u, 300u, 0, 0},
{"EMIT", 0x0180u, 800u, 0, 0x02u},
{"TYPE", 0x0180u, 900u, 0, 0x02u},
{"BLOCK", 0x0200u, 950u, 0, 0x04u},
{"BUFFER", 0x01C0u, 900u, 0, 0x04u},
{"SAVE-BUFFERS", 0x0260u, 1400u, 0, 0x04u},
{"FLUSH", 0x0240u, 1200u, 0, 0x04u},
{"LOAD", 0x0220u, 1100u, 0, 0x04u},
{"LIST", 0x0200u, 1000u, 0, 0x04u},
{"SAVE-SYSTEM", 0x0280u, 2000u, 0x80u, 0x08u},
{NULL, 0, 0, 0, 0}
};
/**
* @brief Apply DoE-derived prior physics values to a named DictEntry.
*
* Searches the static @c physics_seed_table for an entry matching
* @c entry->name. If found, seeds @c temperature_q8, @c avg_latency_ns,
* @c acl_hint, and @c pubsub_mask with values derived from the 2^7
* factorial DoE analysis. Only non-zero seed fields overwrite the current
* value. Called from @c register_word() for built-in primitives.
*
* @param entry DictEntry to seed (no-op if NULL or no matching table entry)
*/
void physics_metadata_apply_seed(DictEntry *entry) {
if (!entry || !entry->name_len) return;
for (const physics_seed_definition_t *seed = physics_seed_table; seed->name; ++seed) {
if ((int) strlen(seed->name) != entry->name_len) continue;
if (strncmp(seed->name, entry->name, entry->name_len) != 0) continue;
if (seed->temperature_q8) entry->physics.temperature_q8 = seed->temperature_q8;
if (seed->avg_latency_ns) entry->physics.avg_latency_ns = seed->avg_latency_ns;
if (seed->acl_hint) entry->physics.acl_hint = seed->acl_hint;
if (seed->pubsub_mask) entry->physics.pubsub_mask = seed->pubsub_mask;
break;
}
}
/*
* ============================================================================
* Phase 2: Linear Decay Mechanism
* ============================================================================
*
* Reduces execution_heat over time to model:
* - OS context switches (old task's heat becomes stale)
* - Temporal locality (recent access patterns > distant past)
* - Dissipation (physical analogy: thermal equilibrium)
*
* Mathematical Model (Discrete Linear):
* H(t) = max(0, H_0 - d*t)
*
* Where:
* H(t) = heat at time t
* H_0 = initial heat
* d = decay rate (heat units per heartbeat tick)
* t = heartbeat ticks elapsed since last execution -- never wall-clock
* time; see physics_metadata_apply_linear_decay()'s doc comment
*
* Frozen words (WORD_FROZEN flag) are exempt from decay.
* ============================================================================
*/
/**
* @brief Apply Loop #3 linear heat decay to a DictEntry.
*
* Implements the thermodynamic dissipation model: H(t) = max(0, H₀ - d*t),
* where t is measured in heartbeat ticks (@c vm->heartbeat.tick_count),
* never in wall-clock time. The decay amount is computed as
* (elapsed_ticks * slope_q48) >> 16 using the Q48.16 adaptive decay slope
* stored in the VM's heartbeat state. Uses a GCC CAS loop for atomic heat
* update when @c __GNUC__ is defined, falling back to a plain subtraction
* otherwise.
*
* Ticks, not nanoseconds: this entry's @c execution_heat feeds directly
* into @c dict_hash (see @c capsule_dict_hash_hook()), so any wall-clock
* dependency here makes dictionary state -- and therefore dict_hash --
* architecture-variable under QEMU/TCG's differing real emulation speed,
* or whenever real elapsed time between two touches of the same entry
* varies for reasons unrelated to execution (e.g. a blocking I/O wait
* between them). @c vm->heartbeat.tick_count only advances from
* @c vm_tick(), called synchronously from the interpreter's own dispatch
* path -- a purely execution-count-driven clock, immune to how long any
* intervening real-time work (including blocking I/O) actually took.
*
* No-op when:
* - @c WORD_FROZEN flag is set on the entry
* - @c elapsed_ticks == 0 (no heartbeat tick has fired since last decay)
* - @c vm->ssm_config has L3 disabled by the Jacquard mode selector
*
* @param entry DictEntry whose heat is decayed
* @param elapsed_ticks Heartbeat ticks elapsed since last decay application
* @param vm VM providing the adaptive decay slope and L3 gate flag
*/
void physics_metadata_apply_linear_decay(DictEntry *entry, uint64_t elapsed_ticks, VM *vm) {
if (!entry || !vm) {
return;
}
/* Frozen words do not decay */
if (entry->flags & WORD_FROZEN) {
return;
}
/* Don't bother when no tick has fired since the last application */
if (elapsed_ticks == 0) {
return;
}
/* L8 GATE: Check if L3 (linear decay) is enabled by Jacquard mode selector */
if (vm->ssm_config) {
ssm_config_t *config = (ssm_config_t*)vm->ssm_config;
if (!config->L3_linear_decay) {
return; /* L3 disabled by L8 mode selector */
}
}
/* ========================================================================
* INTENT: Calculate decay amount using adaptive Q48.16 fixed-point math
* FL1: Heat decay - counterbalances FL1 accumulation to prevent runaway
* FL3: Adaptive slope tuning - inference engine adjusts decay_slope_q48
* based on statistical analysis of heat trajectory effectiveness
* WHY: decay_slope_q48 is expressed as heat units per tick, not per
* microsecond -- the inference engine (Loop #6) adaptively
* calibrates it against whatever units it's actually applied in,
* so no conversion constant is needed here, just a consistent unit.
* ======================================================================== */
uint64_t slope_q48 = physics_decay_slope_load(vm);
uint64_t decay_amount_raw = (elapsed_ticks * slope_q48) >> 16;
/* Clamp to cell_t range (avoid overflow) */
cell_t decay_amount = (decay_amount_raw > (uint64_t)0x7FFFFFFFFFFFFFFF)
? 0x7FFFFFFFFFFFFFFF
: (cell_t)decay_amount_raw;
if (decay_amount == 0) {
return;
}
#if defined(__GNUC__)
cell_t old_heat;
cell_t new_heat;
do {
old_heat = __atomic_load_n(&entry->execution_heat, __ATOMIC_RELAXED);
if (old_heat == 0) {
return;
}
new_heat = (decay_amount >= old_heat) ? 0 : (old_heat - decay_amount);
} while (!__atomic_compare_exchange_n(&entry->execution_heat,
&old_heat,
new_heat,
0,
__ATOMIC_RELAXED,
__ATOMIC_RELAXED));
#else
if (decay_amount >= entry->execution_heat) {
entry->execution_heat = 0;
} else {
entry->execution_heat -= decay_amount;
}
#endif
}