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
+926
View File
@@ -0,0 +1,926 @@
/*
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.
*/
/**
* parity.c - Parity packet collection and canonical hash
*
* Implements M7 parity validation for hosted vs kernel comparison.
*
* M7 Normative Rules enforced here:
* - Rule 1: word_id is monotonic creation index
* - Rule 2: Colon bodies hashed as word_id sequence
* - Rule 3: Dictionary traversal in creation order
*/
#include "starkernel/vm/parity.h"
#include "vm.h"
#ifdef __STARKERNEL__
#include "console.h"
#include "starkernel/hal/hal.h"
#else
#include <stdio.h>
#include <stdlib.h>
#endif
#include <stdbool.h>
#include <string.h>
#ifndef SK_PARITY_DEBUG
#define SK_PARITY_DEBUG 0
#endif
static void print_u32(uint32_t val);
static void print_hex64(uint64_t val);
static void print_str(const char *s);
static void print_nl(void);
#if SK_PARITY_DEBUG
#define SK_PARITY_DEBUG_PREFIX "SKPD:"
enum sk_parity_ptr_region {
SK_PTR_REGION_NULL = 0,
SK_PTR_REGION_VM_ARENA,
SK_PTR_REGION_TEXT,
SK_PTR_REGION_RODATA,
SK_PTR_REGION_DATA,
SK_PTR_REGION_BSS,
SK_PTR_REGION_DIRECTMAP,
SK_PTR_REGION_UNKNOWN
};
static void sk_parity_debug_log_msg(const char *msg);
static bool sk_parity_debug_is_canonical(uint64_t addr);
static enum sk_parity_ptr_region sk_parity_classify_region(struct VM *vm, const void *ptr);
static const char *sk_parity_region_name(enum sk_parity_ptr_region region);
static void sk_parity_debug_print_word_name(const DictEntry *entry);
static void sk_parity_debug_panic(struct VM *vm,
const char *reason,
const DictEntry *entry,
uint32_t word_index,
const void *header_ptr,
const void *xt_ptr,
const void *bad_ptr,
enum sk_parity_ptr_region region,
bool canonical);
static void sk_parity_debug_log_ptr(const char *label,
const void *ptr,
enum sk_parity_ptr_region region,
bool canonical);
static void sk_parity_debug_check_ptr(struct VM *vm,
const char *label,
const DictEntry *entry,
uint32_t word_index,
const void *header_ptr,
const void *xt_ptr,
const void *ptr);
#else
#define sk_parity_debug_log_msg(msg) do { (void)(msg); } while (0)
#define sk_parity_debug_check_ptr(vm,label,entry,idx,hptr,xt,ptr) \
do { (void)(vm); (void)(label); (void)(entry); (void)(idx); (void)(hptr); (void)(xt); (void)(ptr); } while (0)
#endif
/* Maximum dictionary entries for traversal array */
#define MAX_DICT_ENTRIES 2048
#if SK_PARITY_DEBUG
#define SK_PARITY_CANONICAL_MASK 0xffff800000000000ULL
#define SK_PARITY_DIRECTMAP_BASE 0xffff800000000000ULL
/* Note: For __STARKERNEL__ builds, we use sk_hal_text_start/end() from hal.h
* to avoid GOT indirection issues with -fPIC. The rodata/data/bss section
* checks are skipped since XT pointers should only be in text section. */
/**
* @brief Emit a @c SK_PARITY_DEBUG_PREFIX-prefixed diagnostic message line.
*
* Prints @c "SKPD:<msg>\n" to the kernel console or stdout. Used throughout
* parity traversal to trace the hash walk when @c SK_PARITY_DEBUG=1.
* Prints @c "<null>" if @p msg is @c NULL.
*
* Only compiled when @c SK_PARITY_DEBUG is non-zero; all call sites are
* macro-eliminated otherwise.
*
* @param msg Null-terminated diagnostic message string.
*/
static void sk_parity_debug_log_msg(const char *msg)
{
print_str(SK_PARITY_DEBUG_PREFIX);
print_str(msg ? msg : "<null>");
print_nl();
}
/**
* @brief Test whether a 64-bit address is a canonical x86-64 virtual address.
*
* On x86-64, bits [63:48] must all equal bit 47 (sign-extend). This function
* checks that invariant: extracts bit 47, then verifies that the upper 16 bits
* (@c SK_PARITY_CANONICAL_MASK) are either all-zero (user space) or all-one
* (kernel space).
*
* Only compiled when @c SK_PARITY_DEBUG is non-zero.
*
* @param addr 64-bit virtual address to test.
* @return @c true if @p addr is canonical, @c false otherwise.
*/
static bool sk_parity_debug_is_canonical(uint64_t addr)
{
uint64_t sign = (addr >> 47) & 1ULL;
uint64_t mask = SK_PARITY_CANONICAL_MASK;
return sign ? ((addr & mask) == mask) : ((addr & mask) == 0);
}
/**
* @brief Classify a pointer into a known memory region for debug diagnostics.
*
* Checks @p ptr against known address ranges in the following priority order:
* 1. @c NULL → @c SK_PTR_REGION_NULL
* 2. VM arena [@c vm->memory, @c vm->memory + VM_MEMORY_SIZE) →
* @c SK_PTR_REGION_VM_ARENA
* 3. Kernel .text section [sk_hal_text_start(), sk_hal_text_end()) →
* @c SK_PTR_REGION_TEXT (kernel build only; rodata/data/bss skipped)
* 4. Direct-map region (≥ @c SK_PARITY_DIRECTMAP_BASE) →
* @c SK_PTR_REGION_DIRECTMAP (kernel build only)
* 5. Anything else → @c SK_PTR_REGION_UNKNOWN
*
* Only compiled when @c SK_PARITY_DEBUG is non-zero.
*
* @param vm Active VM (used for arena base/size); may be @c NULL.
* @param ptr Pointer to classify.
* @return One of the @c sk_parity_ptr_region enum values.
*/
static enum sk_parity_ptr_region sk_parity_classify_region(struct VM *vm, const void *ptr)
{
if (!ptr) {
return SK_PTR_REGION_NULL;
}
uintptr_t addr = (uintptr_t)ptr;
if (vm && vm->memory) {
uintptr_t arena_start = (uintptr_t)vm->memory;
uintptr_t arena_end = arena_start + VM_MEMORY_SIZE;
if (addr >= arena_start && addr < arena_end) {
return SK_PTR_REGION_VM_ARENA;
}
}
#ifdef __STARKERNEL__
/* Use HAL getters to avoid GOT indirection issues with -fPIC */
uintptr_t text_start = (uintptr_t)sk_hal_text_start();
uintptr_t text_end = (uintptr_t)sk_hal_text_end();
if (addr >= text_start && addr < text_end) {
return SK_PTR_REGION_TEXT;
}
/* Note: rodata/data/bss section checks skipped for kernel builds.
* Function pointers (XTs) should only be in the text section anyway. */
if (addr >= SK_PARITY_DIRECTMAP_BASE) {
return SK_PTR_REGION_DIRECTMAP;
}
#endif
return SK_PTR_REGION_UNKNOWN;
}
/**
* @brief Return a human-readable name for a @c sk_parity_ptr_region value.
*
* Used by debug print helpers to label pointer regions in diagnostic output.
* Returns @c "unknown" for any value not listed in the enum.
*
* Only compiled when @c SK_PARITY_DEBUG is non-zero.
*
* @param region Enum value from @c sk_parity_classify_region().
* @return Pointer to a static string literal naming the region.
*/
static const char *sk_parity_region_name(enum sk_parity_ptr_region region)
{
switch (region) {
case SK_PTR_REGION_NULL: return "null";
case SK_PTR_REGION_VM_ARENA: return "vm_arena";
case SK_PTR_REGION_TEXT: return "text";
case SK_PTR_REGION_RODATA: return "rodata";
case SK_PTR_REGION_DATA: return "data";
case SK_PTR_REGION_BSS: return "bss";
case SK_PTR_REGION_DIRECTMAP: return "directmap";
default: return "unknown";
}
}
/**
* @brief Print the name of a dictionary entry to the console for debug output.
*
* Copies @p entry->name into a local null-terminated buffer (bounded by
* @c WORD_NAME_MAX) and emits it via @c print_str(). Prints @c "<none>" if
* @p entry is @c NULL. Used by @c sk_parity_debug_panic() to identify the
* word being hashed when a pointer violation is detected.
*
* Only compiled when @c SK_PARITY_DEBUG is non-zero.
*
* @param entry Dictionary entry whose name to print; may be @c NULL.
*/
static void sk_parity_debug_print_word_name(const DictEntry *entry)
{
if (!entry) {
print_str("<none>");
return;
}
uint8_t len = entry->name_len;
if (len == 0 || len > WORD_NAME_MAX) {
len = (len > WORD_NAME_MAX) ? WORD_NAME_MAX : len;
}
char buf[WORD_NAME_MAX + 1];
if (len > 0) {
memcpy(buf, entry->name, len);
}
buf[len] = '\0';
print_str(buf);
}
/**
* @brief Log a pointer's address, canonicality, and region to the console.
*
* Emits a single debug line in the format:
* @c "SKPD:<label>=0xADDR canon=Y/N region=<region_name>\n".
* Used by @c sk_parity_debug_check_ptr() to trace each pointer inspected
* during the canonical hash walk.
*
* Only compiled when @c SK_PARITY_DEBUG is non-zero.
*
* @param label Short string label for the pointer (e.g., "colon_entry").
* @param ptr Pointer whose address is to be printed.
* @param region Region classification from @c sk_parity_classify_region().
* @param canonical Whether @p ptr is a canonical x86-64 address.
*/
static void sk_parity_debug_log_ptr(const char *label,
const void *ptr,
enum sk_parity_ptr_region region,
bool canonical)
{
print_str(SK_PARITY_DEBUG_PREFIX);
print_str(label ? label : "ptr");
print_str("=");
print_hex64((uint64_t)(uintptr_t)ptr);
print_str(" canon=");
print_str(canonical ? "Y" : "N");
print_str(" region=");
print_str(sk_parity_region_name(region));
print_nl();
}
/**
* @brief Emit a parity pointer-violation diagnostic and halt.
*
* Called when @c sk_parity_debug_check_ptr() finds a pointer that is either
* non-canonical or in an unexpected memory region during the hash walk.
* Prints a multi-line @c "SK_PARITY_PANIC:" report containing:
* - the violation reason string
* - the word's creation-order index and name
* - the entry's header and XT pointers
* - the bad pointer, its canonicality flag, and its region
* - the VM's current @c HERE and @c LATEST values
*
* Then calls @c sk_hal_panic() in kernel builds or @c abort() in hosted
* builds; never returns.
*
* Only compiled when @c SK_PARITY_DEBUG is non-zero.
*
* @param vm Active VM (for HERE/LATEST diagnostics); may be @c NULL.
* @param reason Short string describing the violation (e.g., "colon_entry").
* @param entry Dictionary entry being processed when the violation occurred.
* @param word_index Creation-order ordinal of @p entry.
* @param header_ptr Pointer to the dictionary entry header.
* @param xt_ptr Pointer to the word's execution token (func pointer).
* @param bad_ptr The offending pointer that failed the check.
* @param region Region classification of @p bad_ptr.
* @param canonical Whether @p bad_ptr is a canonical x86-64 address.
*/
static void sk_parity_debug_panic(struct VM *vm,
const char *reason,
const DictEntry *entry,
uint32_t word_index,
const void *header_ptr,
const void *xt_ptr,
const void *bad_ptr,
enum sk_parity_ptr_region region,
bool canonical)
{
print_str("SK_PARITY_PANIC: ");
print_str(reason ? reason : "unknown");
print_nl();
print_str(" word_idx=");
print_u32(word_index);
print_str(" name=");
sk_parity_debug_print_word_name(entry);
print_nl();
print_str(" header_ptr=");
print_hex64((uint64_t)(uintptr_t)header_ptr);
print_str(" xt_ptr=");
print_hex64((uint64_t)(uintptr_t)xt_ptr);
print_nl();
print_str(" bad_ptr=");
print_hex64((uint64_t)(uintptr_t)bad_ptr);
print_str(" canon=");
print_str(canonical ? "Y" : "N");
print_str(" region=");
print_str(sk_parity_region_name(region));
print_nl();
if (vm) {
print_str(" HERE=");
print_hex64((uint64_t)vm->here);
print_str(" LATEST=");
print_hex64((uint64_t)(uintptr_t)vm->latest);
print_nl();
}
#ifdef __STARKERNEL__
sk_hal_panic("parity pointer violation");
#else
fprintf(stderr, "Parity pointer violation\n");
abort();
#endif
}
/**
* @brief Validate a pointer and panic if it is non-canonical or out-of-region.
*
* Classifies @p ptr with @c sk_parity_classify_region() and tests it for
* canonicality with @c sk_parity_debug_is_canonical(). Logs the result via
* @c sk_parity_debug_log_ptr(). If the pointer is non-canonical or in the
* @c SK_PTR_REGION_UNKNOWN region, calls @c sk_parity_debug_panic() which
* never returns.
*
* This is the central guard point called at every pointer dereference in
* the hash walk to catch dictionary corruption or bad function pointers
* before they cause silent incorrect results.
*
* Only compiled when @c SK_PARITY_DEBUG is non-zero; call sites are
* macro-eliminated in release builds.
*
* @param vm Active VM (passed to @c sk_parity_classify_region()).
* @param label Short label string identifying which pointer is being checked.
* @param entry Current dictionary entry being hashed.
* @param word_index Creation-order ordinal of @p entry.
* @param header_ptr Pointer to the entry header (for panic diagnostics).
* @param xt_ptr Pointer to the entry's XT (for panic diagnostics).
* @param ptr The pointer to validate.
*/
static void sk_parity_debug_check_ptr(struct VM *vm,
const char *label,
const DictEntry *entry,
uint32_t word_index,
const void *header_ptr,
const void *xt_ptr,
const void *ptr)
{
enum sk_parity_ptr_region region = sk_parity_classify_region(vm, ptr);
bool canonical = sk_parity_debug_is_canonical((uint64_t)(uintptr_t)ptr);
sk_parity_debug_log_ptr(label, ptr, region, canonical);
if (!canonical || region == SK_PTR_REGION_UNKNOWN) {
sk_parity_debug_panic(vm, label, entry, word_index, header_ptr, xt_ptr, ptr, region, canonical);
}
}
#endif /* SK_PARITY_DEBUG */
/**
* @brief FNV-1a 64-bit incremental hash over a byte buffer.
*
* Updates a running FNV-1a hash by folding @p len bytes from @p data into
* @p hash. The caller supplies the current hash state so that multiple
* buffers can be chained: pass @c FNV1A_64_OFFSET_BASIS for the first call
* and the returned value as @p hash for subsequent calls.
*
* Algorithm: for each byte @c b, @c hash ^= b then @c hash *= FNV1A_64_PRIME.
*
* @param data Pointer to the byte buffer to hash.
* @param len Number of bytes to consume.
* @param hash Running hash state; seed with @c FNV1A_64_OFFSET_BASIS.
* @return Updated hash state after processing all @p len bytes.
*/
uint64_t fnv1a_64(const uint8_t *data, size_t len, uint64_t hash) {
for (size_t i = 0; i < len; i++) {
hash ^= data[i];
hash *= FNV1A_64_PRIME;
}
return hash;
}
/**
* @brief FNV-1a 64-bit incremental hash over a single byte.
*
* Convenience wrapper for folding one byte into a running FNV-1a hash state.
* Used by @c fnv1a_64_u32() to hash multi-byte values byte-by-byte in a
* defined endian order.
*
* @param byte Single byte to fold into the hash.
* @param hash Running hash state.
* @return Updated hash state.
*/
static uint64_t fnv1a_64_u8(uint8_t byte, uint64_t hash) {
hash ^= byte;
hash *= FNV1A_64_PRIME;
return hash;
}
/**
* @brief FNV-1a 64-bit incremental hash over a 32-bit value (little-endian).
*
* Folds all four bytes of @p val into @p hash in little-endian order
* (least-significant byte first) via four @c fnv1a_64_u8() calls. Used to
* hash @c word_id fields — which are @c uint32_t — into the canonical
* dictionary hash in a byte-order-stable way.
*
* @param val 32-bit value to hash.
* @param hash Running FNV-1a hash state.
* @return Updated hash state after consuming all four bytes.
*/
static uint64_t fnv1a_64_u32(uint32_t val, uint64_t hash) {
hash = fnv1a_64_u8((uint8_t)(val & 0xFF), hash);
hash = fnv1a_64_u8((uint8_t)((val >> 8) & 0xFF), hash);
hash = fnv1a_64_u8((uint8_t)((val >> 16) & 0xFF), hash);
hash = fnv1a_64_u8((uint8_t)((val >> 24) & 0xFF), hash);
return hash;
}
/**
* @brief Count the number of entries in the VM dictionary.
*
* Walks the linked list starting at @c vm->latest, following each entry's
* @c link pointer until @c NULL. Stops early and returns @c MAX_DICT_ENTRIES
* if the list exceeds that limit, preventing runaway traversal on a corrupt
* dictionary.
*
* @param vm Active VM whose dictionary will be counted; returns 0 if @c NULL.
* @return Number of dictionary entries found, capped at @c MAX_DICT_ENTRIES.
*/
uint32_t sk_dict_word_count(struct VM *vm) {
if (!vm) return 0;
uint32_t count = 0;
for (DictEntry *e = vm->latest; e != NULL; e = e->link) {
count++;
if (count >= MAX_DICT_ENTRIES) break;
}
return count;
}
/**
* @brief Hash the compiled body of a colon definition as a sequence of word IDs.
*
* Implements M7 Rule 2: colon bodies are hashed as their @c word_id sequence,
* not as raw code pointers, ensuring the hash is stable across address-space
* layout changes (ASLR, relocation, different build addresses).
*
* Traversal reads each cell from the threaded body in order:
* - A non-NULL @c DictEntry pointer → its @c word_id is hashed.
* - An @c EXIT entry → its @c word_id is hashed and traversal stops.
* - A @c LIT entry → its @c word_id is hashed and the following cell (the
* literal value) is hashed as eight individual bytes (little-endian).
* - A @c NULL cell → traversal stops (end-of-body sentinel).
*
* Returns @p hash unchanged if @p entry lacks the @c WORD_COMPILED flag or
* has no valid data-field. Capped at 1024 cells per body as a safety limit.
*
* @param vm Active VM instance.
* @param entry Dictionary entry whose body will be hashed; must have
* @c WORD_COMPILED set.
* @param word_index Creation-order ordinal of @p entry (used by debug mode).
* @param hash Running FNV-1a hash state from the caller.
* @return Updated hash state after processing the colon body.
*/
static uint64_t hash_colon_body(struct VM *vm, DictEntry *entry, uint32_t word_index, uint64_t hash) {
if (!vm || !entry || !(entry->flags & WORD_COMPILED)) {
return hash;
}
#if !SK_PARITY_DEBUG
(void)word_index;
#endif
#if SK_PARITY_DEBUG
sk_parity_debug_log_msg("hash_colon_body:entry");
sk_parity_debug_check_ptr(vm, "colon_entry", entry, word_index, entry, entry->func, entry);
#endif
/* Get body address from data field */
cell_t *df = vm_dictionary_get_data_field(entry);
if (!df) {
return hash;
}
#if SK_PARITY_DEBUG
sk_parity_debug_check_ptr(vm, "data_field", entry, word_index, entry, entry->func, df);
#endif
vaddr_t body_addr = (vaddr_t)(uint64_t)(*df);
if (body_addr == 0 || body_addr >= VM_MEMORY_SIZE) {
return hash;
}
#if SK_PARITY_DEBUG
sk_parity_debug_check_ptr(vm, "body_addr_ptr", entry, word_index, entry, entry->func,
(vm->memory && body_addr < VM_MEMORY_SIZE) ? (vm->memory + body_addr) : NULL);
#endif
/* Traverse threaded code until we hit EXIT or end */
/* Each cell is a DictEntry* (word to call) */
/* LIT is followed by a literal value */
DictEntry *lit_entry = vm_find_word(vm, "LIT", 3);
DictEntry *exit_entry = vm_find_word(vm, "EXIT", 4);
#if SK_PARITY_DEBUG
sk_parity_debug_check_ptr(vm, "lit_entry", lit_entry, word_index, entry, lit_entry ? lit_entry->func : NULL, lit_entry);
sk_parity_debug_check_ptr(vm, "exit_entry", exit_entry, word_index, entry, exit_entry ? exit_entry->func : NULL, exit_entry);
#endif
cell_t *ip = (cell_t *)vm_ptr(vm, body_addr);
if (!ip) {
return hash;
}
#if SK_PARITY_DEBUG
sk_parity_debug_check_ptr(vm, "colon_body_ip", entry, word_index, entry, entry->func, ip);
#endif
/* Safety limit on body length */
size_t max_cells = (VM_MEMORY_SIZE - body_addr) / sizeof(cell_t);
if (max_cells > 1024) max_cells = 1024;
for (size_t i = 0; i < max_cells; i++) {
DictEntry *w = (DictEntry *)(uintptr_t)(ip[i]);
#if SK_PARITY_DEBUG
sk_parity_debug_check_ptr(vm, "body_xt", w, word_index, entry, w ? w->func : NULL, w);
#endif
if (w == NULL) {
break; /* End of body */
}
if (w == exit_entry) {
/* Hash EXIT's word_id and stop */
hash = fnv1a_64_u32(w->word_id, hash);
break;
}
/* Hash the called word's word_id */
hash = fnv1a_64_u32(w->word_id, hash);
if (w == lit_entry) {
/* Next cell is literal value - hash it */
i++;
if (i < max_cells) {
cell_t lit_val = ip[i];
/* Hash as 64-bit value */
for (int b = 0; b < 8; b++) {
hash = fnv1a_64_u8((uint8_t)(lit_val & 0xFF), hash);
lit_val >>= 8;
}
}
}
}
return hash;
}
/**
* @brief Compute the canonical FNV-1a 64-bit hash of the VM dictionary.
*
* Implements M7 Rule 3: dictionary traversal is in creation order (oldest
* word first). Because @c vm->latest is the newest word, the function first
* builds a reverse array of entries and then hashes them in reverse-array
* order (oldest → newest).
*
* For each entry the following structural fields are folded into the hash:
* 1. @c flags (1 byte)
* 2. @c name_len (1 byte)
* 3. @c name (name_len bytes)
* 4. @c acl_default (1 byte)
* 5. @c word_id (4 bytes, little-endian)
* 6. Colon body (via @c hash_colon_body()) if @c WORD_COMPILED is set
*
* Pointer-valued fields (@c func, @c link) are intentionally excluded so
* the hash is stable across different build addresses and address-space
* layouts.
*
* Traversal is capped at @c MAX_DICT_ENTRIES (2048) entries to guard
* against corruption.
*
* @param vm Active VM whose dictionary will be hashed; returns 0 if @c NULL.
* @return 64-bit FNV-1a canonical hash of the dictionary structure.
*/
uint64_t sk_dict_canonical_hash(struct VM *vm) {
if (!vm) return 0;
/* Build array of entries in reverse order (latest to oldest) */
DictEntry *entries[MAX_DICT_ENTRIES];
uint32_t count = 0;
#if SK_PARITY_DEBUG
sk_parity_debug_log_msg("sk_dict_canonical_hash:walk_latest");
sk_parity_debug_check_ptr(vm, "latest_head", vm->latest, 0, vm->latest, vm->latest ? vm->latest->func : NULL, vm->latest);
#endif
for (DictEntry *e = vm->latest; e != NULL; e = e->link) {
#if SK_PARITY_DEBUG
sk_parity_debug_check_ptr(vm, "dict_entry", e, count, e, e->func, e);
#endif
if (count < MAX_DICT_ENTRIES) {
entries[count++] = e;
}
#if SK_PARITY_DEBUG
sk_parity_debug_check_ptr(vm, "dict_link", e->link, count, e, e->func, e->link);
#endif
}
/* Hash in creation order (oldest to newest = reverse of array) */
uint64_t hash = FNV1A_64_OFFSET_BASIS;
#if SK_PARITY_DEBUG
sk_parity_debug_log_msg("sk_dict_canonical_hash:hash_order");
#endif
for (int i = (int)count - 1; i >= 0; i--) {
DictEntry *e = entries[i];
uint32_t ordinal = (uint32_t)((count - 1) - i);
#if SK_PARITY_DEBUG
sk_parity_debug_check_ptr(vm, "hash_entry", e, ordinal, e, e ? e->func : NULL, e);
sk_parity_debug_check_ptr(vm, "hash_xt", e, ordinal, e, e ? e->func : NULL, e ? e->func : NULL);
#endif
/* Hash structural fields only */
/* 1. flags (1 byte) */
#if SK_PARITY_DEBUG
sk_parity_debug_log_msg("hash_field:flags");
#endif
hash = fnv1a_64_u8(e->flags, hash);
/* 2. name_len (1 byte) */
#if SK_PARITY_DEBUG
sk_parity_debug_log_msg("hash_field:name_len");
#endif
hash = fnv1a_64_u8(e->name_len, hash);
/* 3. name (name_len bytes) */
#if SK_PARITY_DEBUG
sk_parity_debug_log_msg("hash_field:name_bytes");
#endif
hash = fnv1a_64((const uint8_t *)e->name, e->name_len, hash);
/* 4. acl_default (1 byte) */
#if SK_PARITY_DEBUG
sk_parity_debug_log_msg("hash_field:acl");
#endif
hash = fnv1a_64_u8(e->acl_default, hash);
/* 5. word_id (4 bytes) */
#if SK_PARITY_DEBUG
sk_parity_debug_log_msg("hash_field:word_id");
#endif
hash = fnv1a_64_u32(e->word_id, hash);
/* 6. For colon definitions: hash body as word_id sequence */
if (e->flags & WORD_COMPILED) {
hash = hash_colon_body(vm, e, ordinal, hash);
}
}
return hash;
}
/**
* @brief Collect M7 parity data from the VM into a @c ParityPacket.
*
* Populates @p out with a snapshot of the VM's structural state for offline
* determinism verification. The packet contains:
*
* - M7.1a fields: @c word_count, @c here_offset, @c latest_word_id, and the
* canonical dictionary hash (@c header_hash64) computed by
* @c sk_dict_canonical_hash().
* - M7.1b fields: test result counters (@c tests_total / @c tests_passed /
* @c tests_failed / @c tests_skipped / @c tests_errors) populated from
* @c global_test_stats when @c STARFORTH_ENABLE_TESTS is defined.
*
* If @p vm is @c NULL, sets @c out->bootstrap_result to
* @c SK_BOOTSTRAP_INIT_FAIL and returns with all other fields zeroed. If
* @p out is @c NULL the function is a no-op.
*
* @param vm Active VM to collect parity data from; may be @c NULL.
* @param out Output @c ParityPacket to fill; caller must allocate.
*/
void sk_parity_collect(struct VM *vm, ParityPacket *out) {
if (!out) return;
#if SK_PARITY_DEBUG
sk_parity_debug_log_msg("sk_parity_collect:enter");
#endif
/* Clear packet */
out->word_count = 0;
out->here_offset = 0;
out->latest_word_id = 0;
out->header_hash64 = 0;
out->tests_total = 0;
out->tests_passed = 0;
out->tests_failed = 0;
out->tests_skipped = 0;
out->tests_errors = 0;
out->window_hash64 = 0;
out->bootstrap_result = SK_BOOTSTRAP_OK;
if (!vm) {
out->bootstrap_result = SK_BOOTSTRAP_INIT_FAIL;
return;
}
/* M7.1a fields */
out->word_count = sk_dict_word_count(vm);
out->here_offset = (uint32_t)vm->here;
out->latest_word_id = vm->latest ? vm->latest->word_id : 0;
out->header_hash64 = sk_dict_canonical_hash(vm);
/* M7.1b fields - from global test stats (if tests were run) */
#ifdef STARFORTH_ENABLE_TESTS
extern TestStats global_test_stats;
out->tests_total = (uint32_t)global_test_stats.total_tests;
out->tests_passed = (uint32_t)global_test_stats.total_pass;
out->tests_failed = (uint32_t)global_test_stats.total_fail;
out->tests_skipped = (uint32_t)global_test_stats.total_skip;
out->tests_errors = (uint32_t)global_test_stats.total_error;
#endif
}
/**
* @brief Print a @c uint32_t value as decimal to the kernel console or stdout.
*
* Converts @p val to a decimal ASCII string using a local buffer (no libc
* required) and emits it via @c console_puts() in kernel builds or
* @c printf() in hosted builds. Used by @c sk_parity_print() to format
* count and ID fields in parity output lines.
*
* @param val 32-bit unsigned value to print.
*/
static void print_u32(uint32_t val) {
char buf[16];
int i = 15;
buf[i--] = '\0';
if (val == 0) {
buf[i--] = '0';
} else {
while (val > 0 && i >= 0) {
buf[i--] = '0' + (val % 10);
val /= 10;
}
}
#ifdef __STARKERNEL__
console_puts(&buf[i + 1]);
#else
printf("%s", &buf[i + 1]);
#endif
}
/**
* @brief Print a @c uint64_t value as a 16-digit hex string with "0x" prefix.
*
* Formats @p val as "0xNNNNNNNNNNNNNNNN" using a fixed-size local buffer
* (no libc required) and emits it via @c console_puts() in kernel builds or
* @c printf() in hosted builds. Used by @c sk_parity_print() to format hash
* and address fields in parity output lines.
*
* @param val 64-bit value to print as lowercase hex.
*/
static void print_hex64(uint64_t val) {
char buf[19];
buf[0] = '0';
buf[1] = 'x';
for (int i = 17; i >= 2; i--) {
int d = val & 0xF;
buf[i] = (d < 10) ? ('0' + d) : ('a' + d - 10);
val >>= 4;
}
buf[18] = '\0';
#ifdef __STARKERNEL__
console_puts(buf);
#else
printf("%s", buf);
#endif
}
/**
* @brief Print a null-terminated string to the kernel console or stdout.
*
* Emits @p s via @c console_puts() in kernel builds or @c printf() in hosted
* builds. Serves as a single dispatch point so parity output compiles cleanly
* in both environments without scattering @c ifdef guards through the
* formatting code.
*
* @param s Null-terminated string to print.
*/
static void print_str(const char *s) {
#ifdef __STARKERNEL__
console_puts(s);
#else
printf("%s", s);
#endif
}
/**
* @brief Print a newline to the kernel console or stdout.
*
* Emits a line terminator via @c console_println("") in kernel builds or
* @c printf("\n") in hosted builds. Paired with @c print_str() to avoid
* scattering @c ifdef guards through parity formatting code.
*/
static void print_nl(void) {
#ifdef __STARKERNEL__
console_println("");
#else
printf("\n");
#endif
}
/**
* @brief Print a @c ParityPacket to the kernel console or stdout.
*
* Emits two or three structured lines that can be captured in the QEMU serial
* log for offline determinism verification:
*
* - @c "PARITY:M7.1a word_count=N here=0xH latest_id=N hash=0xH" — always
* emitted; contains the structural dictionary fingerprint.
* - @c "PARITY:M7.1b tests=N pass=N fail=N skip=N err=N" — emitted only
* when @c pkt->tests_total > 0; contains POST result counts.
* - @c "PARITY:OK" or @c "PARITY:FAIL code=N" — result verdict line.
*
* All output goes through @c print_str() / @c print_u32() / @c print_hex64()
* / @c print_nl() so the function compiles cleanly in both kernel and hosted
* builds. No-op if @p pkt is @c NULL.
*
* @param pkt Pointer to the @c ParityPacket to display; may be @c NULL.
*/
void sk_parity_print(const ParityPacket *pkt) {
if (!pkt) return;
/* M7.1a line */
print_str("PARITY:M7.1a word_count=");
print_u32(pkt->word_count);
print_str(" here=");
print_hex64(pkt->here_offset);
print_str(" latest_id=");
print_u32(pkt->latest_word_id);
print_str(" hash=");
print_hex64(pkt->header_hash64);
print_nl();
/* M7.1b line (only if tests were run) */
if (pkt->tests_total > 0) {
print_str("PARITY:M7.1b tests=");
print_u32(pkt->tests_total);
print_str(" pass=");
print_u32(pkt->tests_passed);
print_str(" fail=");
print_u32(pkt->tests_failed);
print_str(" skip=");
print_u32(pkt->tests_skipped);
print_str(" err=");
print_u32(pkt->tests_errors);
print_nl();
}
/* Result line */
if (pkt->bootstrap_result == SK_BOOTSTRAP_OK &&
pkt->tests_failed == 0 && pkt->tests_errors == 0) {
print_str("PARITY:OK");
} else {
print_str("PARITY:FAIL code=");
print_u32(pkt->bootstrap_result);
}
print_nl();
}