Files
LithosAnanake/src/starkernel/vm/parity.c
T

927 lines
32 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 20232025 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) 20232025 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();
}