Stage S-1a: parity hashing covers the whole dictionary; failures are logged, counted and marked INCOMPLETE (FABRIC-3.9.0 §8.1)

- MAX_DICT_ENTRIES (2048) removed: the entry list is allocated to the
  dictionary's size; a cyclic chain is detected (Floyd) instead of capped.
- Colon-body walk bounded by vm->here, not a 1024-cell cap.
- Every failure prints PARITY:INCOMPLETE #n with its full reason, logs at
  LOG_ERROR, bumps sk_parity_incomplete_count(), and the record reads
  hash=INCOMPLETE / PARITY:FAIL code=4 rather than a plausible hash.
- Pre-existing #GP fixed: the walker dereferenced inline body data (S"
  bytes, offsets); it now follows only known dictionary entries.
- LIT operands that are entry pointers are hashed by word_id, not address,
  so a mature dictionary hashes identically across ISAs.

Probe (reverted): 563 -> 2563 words hashed, identical on amd64/aarch64/
riscv64; cyclic chain -> INCOMPLETE, counter 2. Boot parity unchanged
(531, 0x572daac83b797332). 3-ISA acceptance passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Robert Allan James
2026-09-24 12:51:34 -04:00
co-authored by Claude Opus 5.5
parent 9bf2e8ec95
commit ee3db96ea8
35 changed files with 164134 additions and 37 deletions
+1 -1
View File
@@ -345,7 +345,7 @@ words. It should all be fairly dynamic."*
| Where | Today | At the limit |
|---|---|---|
| `parity.c:498/646`, `MAX_DICT_ENTRIES` 2048 | parity walks at most 2048 dictionary entries | **Hashing stops.** Beyond 2048 words, `dict_hash` silently ignores the rest, so the determinism evidence would be computed over part of the dictionary. |
| `vm.h` `DICTIONARY_SIZE` 1024: `word_id_map[1024]`, `recycled_word_ids[1024]`; `dictionary_management.c:87` | word ids 0–1023 | **Word 1025 onward gets `WORD_ID_INVALID`,** and the physics hooks skip it (`physics_execution_hooks.c:95`). Execution heat, pipelining and transitions silently stop for later words. Hera is at 531. |
| `vm.h` `DICTIONARY_SIZE` 1024: `word_id_map[1024]`, `recycled_word_ids[1024]`; `dictionary_management.c:87` | word ids 0–1023 | **Word 1025 onward gets `WORD_ID_INVALID`,** and the physics hooks skip it (`physics_execution_hooks.c:95`). Execution heat, pipelining and transitions stop for later words. Hera is at 531. *(Corrected at S-1a: not strictly silent. `vm_dictionary_track_entry` logs a `LOG_WARN`, "out of stable IDs, disabling metrics for '…'", per word. But nothing counts it and nothing refuses: the word is still defined and runs outside the physics, so §8.1's rules are still unmet.)* |
| `string_words.c` `WORD` scratch, `WORD_SCRATCH_CAP` 64 | one **static** `word_scratch_addr`, allotted in the first caller's VM space | Not a count limit, but a static shared across VMs: a second VM calling `WORD` gets an address in someone else's space. Found at Stage G. |
**Captain Bob, 2026-09-24:** *"both of those failure modes that you've noted need to be handled
+78
View File
@@ -145,3 +145,81 @@ Evidence is named **before** each stage runs.
- `dict_hash` unchanged (531, `0x572daac83b797332`);
- POST passes;
- clean kernel and hosted builds.
- [x] **Stage S-1a — parity hashing covers the whole dictionary (`FABRIC-3.9.0.md` §8.1, class C).**
2026-09-24. Captain Bob: *"all as proposed, commit and start S-1."*
**The change** (`src/starkernel/vm/parity.c`, `include/starkernel/vm/parity.h`):
- **`MAX_DICT_ENTRIES` (2048) removed.** The walk covers the whole link chain. The entry list
is allocated to the dictionary's actual size (`kmalloc` in the kernel, `malloc` hosted) and
freed after.
- **Cycle check replaces the cap.** The only thing the cap guarded against, a corrupt cyclic
chain, is now detected directly (Floyd: a second pointer moving two links per step).
- **The colon-body walk is bounded by `vm->here`,** not a 1024-cell cap. A body that reaches
`HERE` without `EXIT` or a NULL cell is marked incomplete, not quietly truncated.
- **Failures follow §8.1's rules.** Every failure (cyclic chain, entry list not allocated,
malformed body) is:
- printed in full to the console, `PARITY:INCOMPLETE #n -- <why>`. `log_message`'s line
buffer is 64 bytes and cut every reason off mid-sentence.
- logged at `LOG_ERROR` for the persisted record;
- counted (`sk_parity_incomplete_count()`).
- **An incomplete hash is marked as such.** The record prints `hash=INCOMPLETE` (new
`ParityPacket.hash_complete`) and `PARITY:FAIL code=4`
(`SK_BOOTSTRAP_PARITY_INCOMPLETE`), never a value that looks valid.
- `sk_dict_word_count` returns 0 on a cyclic chain, logged and counted.
**Two pre-existing defects found by the probe, both fixed in the walker:**
1. **The colon-body walker dereferenced inline data.** It treated every cell of a body as a
`DictEntry*` and followed it. Only `LIT`'s operand was known to be data. `S"` string
bytes, branch offsets and other inline operands were followed too.
- This was harmless while parity only ever ran at early boot, before any colon bodies
existed.
- The first hash of a mature dictionary took a #GP (vector 13) and halted the kernel. This
is the "interrupt exception" Captain Bob reported (`logs/20260924-094537/amd64/`,
`-095154/amd64/`).
- The **original** code faults the same way (`logs/20260924-095730/amd64/`), so the
defect predates S-1a.
- **Fix:** a cell is dereferenced only if it is a known dictionary entry, found by binary
search in a sorted copy of the entry list. Any other cell is hashed as its raw value and
never followed.
2. **A `LIT` operand holding an entry pointer was hashed by address.** `['] WORD` compiles
`LIT <DictEntry*>`, a native address that differs per ISA (seen in `ACL-BOOT`'s
`ACL-PIN` sequences). So the mature-dictionary hash differed across ISAs:
- amd64 `0x945a76bc6ace1fdc`, aarch64 `0x8cc1255e6a40c12e` (`-100252`, `-100732`).
- **Fix:** rule 2 (word_id, never address) now applies to such an operand. The walker
hashes a tag byte (`0xE7`) plus the entry's `word_id`, so it can't collide with a plain
number. Any other literal is still hashed as its value.
A third defect, an incomplete counter one too high on aarch64 only (`-101803`, `-102504`,
`-103207`, `-103820`), was not in parity. It was the out-of-bounds stack write fixed in its
own commit (the entry above).
**Evidence.** A probe (reverted) at the end of boot:
1. hashed Hera's full dictionary;
2. registered 2,000 more words (`PZ0000`…) and hashed again;
3. made the chain cyclic and asked for both the word count and the hash.
The results are byte-identical on all three ISAs (`logs/20260924-115524/amd64/`,
`-115954/aarch64/`, `-120533/riscv64/`):
| Probe step | Result |
|---|---|
| before | `word_count=563 hash=0x224007f9af328111`, `PARITY:OK` |
| after +2000 | `word_count=2563 hash=0xcf85876337e75a84`, `PARITY:OK`. Past the old 2048 cap: the whole dictionary is hashed. |
| cyclic | `PARITY:INCOMPLETE #1`/`#2 -- dictionary link chain is cyclic (corrupt)`, two `[HADES][ERROR]` lines, `word_count=0 hash=INCOMPLETE`, `PARITY:FAIL code=4` |
| counter | `incomplete_count=2` (word count + hash) |
The "after" record shows `latest_id=4294967295`. That is `WORD_ID_INVALID`: the 2,000
probe words ran past the 1024 stable word ids, and each logged "out of stable IDs, disabling
metrics". That is S-1b's defect, not S-1a's. The §8.1 row now notes that this path does log
a warning, but neither counts nor refuses.
**Acceptance** on the clean tree, with the probe reverted (`logs/20260924-121207/amd64/`,
`-122500/aarch64/`, `-123759/riscv64/`):
- Boot M7.1a is unchanged: `word_count=531 hash=0x572daac83b797332`. At boot there are no
colon bodies or entry-pointer literals yet, so neither fix changes the boot hash.
- The `dict_hash` set is unchanged: Hera `0xda9742ce9c3376a2`, Hestia `0x413e7d086f342d8b`,
Artemis `0xdfd68316d5f5ba85`.
- The O-A checks pass: G interleave A = 40/B = 40/48 runs, `HHH` precedence, and a typed
`1 2 + .` gives `3` mid-stream.
- Mint → WIREBIND → `USE` → `1 2 + .` → `3`. Idle log growth 0. Zero `UNKNOWN WORD` or
faults. QEMU exits cleanly.
+29 -5
View File
@@ -66,6 +66,13 @@
#define SK_BOOTSTRAP_ARENA_FAIL 1
#define SK_BOOTSTRAP_INIT_FAIL 2
#define SK_BOOTSTRAP_DICT_FAIL 3
/* FABRIC-3.9.0.md §8.1 (Stage S-1a, 2026-09-24): the canonical dictionary
* hash could not cover the whole dictionary (a cyclic/corrupt link chain,
* the entry list could not be allocated, or a colon body ran off the end
* of VM memory without EXIT). The M7.1a line prints hash=INCOMPLETE and the
* result line PARITY:FAIL with this code -- never a plausible-looking hash
* computed over part of the dictionary. */
#define SK_BOOTSTRAP_PARITY_INCOMPLETE 4
/**
* ParityPacket - Summary of VM state for comparison
@@ -79,6 +86,8 @@ typedef struct {
uint32_t here_offset; /* vm->here (bytes used in dictionary) */
uint32_t latest_word_id; /* vm->latest->word_id (stable ID) */
uint64_t header_hash64; /* Canonical dictionary hash (FNV-1a) */
int hash_complete; /* 1 if header_hash64 covers the whole dictionary;
* 0 = INCOMPLETE, header_hash64 meaningless (S-1a) */
/* === M7.1b: POST Parity === */
uint32_t tests_total; /* Total tests executed */
@@ -135,21 +144,36 @@ void sk_parity_print(const ParityPacket *pkt);
* - physics (runtime)
* - transition_metrics (pointer)
*
* @param vm Pointer to VM instance
* @return 64-bit FNV-1a hash
* FABRIC-3.9.0.md §8.1 (Stage S-1a, 2026-09-24): covers the WHOLE
* dictionary -- no fixed cap on entries (was 2048) or on a colon body's
* cells (was 1024). *complete is set to 1 if every entry and every colon
* body was hashed; to 0 if the link chain is cyclic (corrupt), the entry
* list could not be allocated, or a colon body runs off the end of VM
* memory without EXIT. On 0 the returned value is meaningless, the failure
* is logged at LOG_ERROR, and sk_parity_incomplete_count() is bumped.
*
* @param vm Pointer to VM instance
* @param complete Out: 1 = complete, 0 = incomplete (must not be NULL)
* @return 64-bit FNV-1a hash (valid only when *complete == 1)
*/
uint64_t sk_dict_canonical_hash(struct VM *vm);
uint64_t sk_dict_canonical_hash(struct VM *vm, int *complete);
/*
* sk_dict_word_count - Count dictionary entries
*
* Traverses from vm->latest to NULL.
* Traverses from vm->latest to NULL -- the whole chain, no cap (S-1a). A
* cyclic (corrupt) chain is detected rather than walked forever: it is
* logged, counted, and 0 is returned.
*
* @param vm Pointer to VM instance
* @return Number of dictionary entries
* @return Number of dictionary entries (0 on a cyclic chain)
*/
uint32_t sk_dict_word_count(struct VM *vm);
/* Number of times a dictionary walk or canonical hash has come out
* incomplete since boot (S-1a) -- 0 in any healthy run. */
uint32_t sk_parity_incomplete_count(void);
/**
* FNV-1a constants
*/
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+196 -31
View File
@@ -54,12 +54,19 @@
#include "starkernel/vm/parity.h"
#include "vm.h"
#include "log.h" /* log_message(LOG_ERROR) -- S-1a: failures are never silent */
#ifdef __STARKERNEL__
#include "console.h"
#include "starkernel/hal/hal.h"
#include "starkernel/kmalloc.h" /* S-1a: the entry list is sized to the dictionary */
#define PARITY_ALLOC(n) kmalloc(n)
#define PARITY_FREE(p) kfree(p)
#else
#include <stdio.h>
#include <stdlib.h>
#define PARITY_ALLOC(n) malloc(n)
#define PARITY_FREE(p) free(p)
#endif
#include <stdbool.h>
@@ -118,8 +125,61 @@ static void sk_parity_debug_check_ptr(struct VM *vm,
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
/* FABRIC-3.9.0.md §8.1 (Stage S-1a, 2026-09-24): MAX_DICT_ENTRIES (2048) is
* gone. The walk used to stop at 2048 entries -- a dictionary larger than
* that got a canonical hash that silently ignored the rest. The walk now
* covers the whole chain; a cyclic (corrupt) chain, the guard the cap stood
* in for, is detected directly (dict_walk_count() below), and every failure
* is logged, counted and marked INCOMPLETE rather than returned as a hash. */
static uint32_t g_parity_incomplete_count;
uint32_t sk_parity_incomplete_count(void) { return g_parity_incomplete_count; }
static void parity_incomplete(const char *why) {
g_parity_incomplete_count++;
/* The full reason, straight to the console: log_message()'s own line
* buffer is LOG_LINE_MAX (64) bytes, which cut every reason here off
* mid-sentence. The log line stays too, for the persisted record. */
print_str("PARITY:INCOMPLETE #");
print_u32(g_parity_incomplete_count);
print_str(" -- ");
print_str(why);
print_nl();
log_message(LOG_ERROR, "PARITY: INCOMPLETE -- %s", why);
}
/* S-1a finding (2026-09-24): the colon-body walker used to treat EVERY
* cell of a body as a DictEntry* and dereference it. Only LIT's operand
* was known to be data; S" string bytes, branch and loop offsets and other
* inline operands were dereferenced too. Harmless while parity only ever
* ran at early boot (no colon bodies yet), but the first hash of a mature
* dictionary took a #GP -- reproduced on the ORIGINAL code
* (logs/20260924-095730/amd64/), so it predates S-1a. The walker now
* dereferences a cell only if it is a known dictionary entry, looked up in
* a sorted copy of the entry list; any other cell is hashed as its raw
* value and never followed. */
static void parity_sort_entries(DictEntry **a, uint32_t n) {
uint32_t gap, i, j;
for (gap = n / 2; gap > 0; gap /= 2) { /* shell sort -- no libc qsort */
for (i = gap; i < n; i++) {
DictEntry *t = a[i];
for (j = i; j >= gap && (uintptr_t)a[j - gap] > (uintptr_t)t; j -= gap) {
a[j] = a[j - gap];
}
a[j] = t;
}
}
}
static int parity_is_known_entry(DictEntry * const *known, uint32_t n, const void *p) {
uint32_t lo = 0, hi = n;
while (lo < hi) {
uint32_t mid = lo + (hi - lo) / 2;
if ((uintptr_t)known[mid] == (uintptr_t)p) return 1;
if ((uintptr_t)known[mid] < (uintptr_t)p) lo = mid + 1; else hi = mid;
}
return 0;
}
#if SK_PARITY_DEBUG
#define SK_PARITY_CANONICAL_MASK 0xffff800000000000ULL
@@ -482,20 +542,40 @@ static uint64_t fnv1a_64_u32(uint32_t val, uint64_t 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.
* @c link pointer until @c NULL -- the whole chain, no cap (S-1a). A
* corrupt, cyclic chain is detected (dict_walk_count()), logged, counted,
* and reported as 0 rather than walked forever.
*
* @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.
* @return Number of dictionary entries found (0 on a cyclic chain).
*/
uint32_t sk_dict_word_count(struct VM *vm) {
if (!vm) return 0;
/* S-1a: length of the whole link chain from vm->latest, with Floyd's
* cycle check (a second pointer moving two links per step) standing in for
* the old fixed cap. Returns 0 and sets *out, or -1 on a cyclic chain. */
static int dict_walk_count(struct VM *vm, uint32_t *out) {
DictEntry *slow = vm->latest, *fast = vm->latest;
uint32_t count = 0;
for (DictEntry *e = vm->latest; e != NULL; e = e->link) {
while (slow) {
count++;
if (count >= MAX_DICT_ENTRIES) break;
slow = slow->link;
if (fast && fast->link) {
fast = fast->link->link;
if (fast && fast == slow) return -1;
} else {
fast = (DictEntry *)0;
}
}
*out = count;
return 0;
}
uint32_t sk_dict_word_count(struct VM *vm) {
uint32_t count = 0;
if (!vm) return 0;
if (dict_walk_count(vm, &count) != 0) {
parity_incomplete("dictionary link chain is cyclic (corrupt)");
return 0;
}
return count;
}
@@ -515,7 +595,9 @@ uint32_t sk_dict_word_count(struct VM *vm) {
* - 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.
* has no valid data-field. Bounded by vm->here (the end of compiled code),
* not a cell cap (S-1a); a body that reaches HERE without EXIT or a NULL
* cell sets *complete to 0 (logged, counted).
*
* @param vm Active VM instance.
* @param entry Dictionary entry whose body will be hashed; must have
@@ -524,7 +606,8 @@ uint32_t sk_dict_word_count(struct VM *vm) {
* @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) {
static uint64_t hash_colon_body(struct VM *vm, DictEntry *entry, uint32_t word_index, uint64_t hash,
int *complete, DictEntry * const *known, uint32_t nknown) {
if (!vm || !entry || !(entry->flags & WORD_COMPILED)) {
return hash;
}
@@ -573,9 +656,19 @@ static uint64_t hash_colon_body(struct VM *vm, DictEntry *entry, uint32_t word_i
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;
/* FABRIC-3.9.0.md §8.1 (Stage S-1a): no fixed cell cap (was 1024 -- a
* longer body was silently cut off). A body cannot extend past the end
* of compiled code, vm->here, so that is the bound; reaching it without
* EXIT or a NULL cell means the body is not a well-formed colon body,
* and the hash is marked INCOMPLETE rather than quietly truncated. */
size_t max_cells;
int terminated = 0;
if ((size_t)vm->here <= (size_t)body_addr) {
parity_incomplete("colon body starts at or past HERE");
*complete = 0;
return hash;
}
max_cells = ((size_t)vm->here - (size_t)body_addr) / sizeof(cell_t);
for (size_t i = 0; i < max_cells; i++) {
DictEntry *w = (DictEntry *)(uintptr_t)(ip[i]);
@@ -584,12 +677,25 @@ static uint64_t hash_colon_body(struct VM *vm, DictEntry *entry, uint32_t word_i
#endif
if (w == NULL) {
terminated = 1;
break; /* End of body */
}
/* Not a dictionary entry: inline data (string bytes, an offset, an
* operand). Hash the raw cell and never dereference it. */
if (!parity_is_known_entry(known, nknown, w)) {
cell_t raw = ip[i];
for (int b = 0; b < 8; b++) {
hash = fnv1a_64_u8((uint8_t)(raw & 0xFF), hash);
raw >>= 8;
}
continue;
}
if (w == exit_entry) {
/* Hash EXIT's word_id and stop */
hash = fnv1a_64_u32(w->word_id, hash);
terminated = 1;
break;
}
@@ -601,15 +707,31 @@ static uint64_t hash_colon_body(struct VM *vm, DictEntry *entry, uint32_t word_i
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;
/* S-1a finding (2026-09-24): `['] WORD` compiles LIT <DictEntry*>
* -- a native address that differs per ISA (seen: ACL-BOOT's
* ACL-PIN sequences), which made the hash of a mature
* dictionary differ across ISAs. Rule 2 (word_id, never
* address) applies to a LIT operand that is an entry too:
* hash that entry's word_id, tagged so it can't collide with
* a plain number. Any other literal is hashed as its value. */
if (parity_is_known_entry(known, nknown, (void *)(uintptr_t)lit_val)) {
hash = fnv1a_64_u8(0xE7, hash);
hash = fnv1a_64_u32(((DictEntry *)(uintptr_t)lit_val)->word_id, hash);
} else {
/* 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;
}
}
}
}
}
if (!terminated) {
parity_incomplete("colon body reaches HERE without EXIT");
*complete = 0;
}
return hash;
}
@@ -633,35 +755,68 @@ static uint64_t hash_colon_body(struct VM *vm, DictEntry *entry, uint32_t word_i
* 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.
* Traversal covers the whole dictionary (S-1a): the entry list is
* allocated to its actual size, and a cyclic chain, an allocation failure
* or an unterminated colon body sets *complete to 0 instead of returning a
* hash over part of it.
*
* @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) {
uint64_t sk_dict_canonical_hash(struct VM *vm, int *complete) {
*complete = 0;
if (!vm) return 0;
/* FABRIC-3.9.0.md §8.1 (Stage S-1a): the entry list is sized to the
* dictionary actually present -- no fixed 2048-entry array, no silent
* stop. The chain is measured first (cycle-checked), then collected. */
uint32_t total = 0;
if (dict_walk_count(vm, &total) != 0) {
parity_incomplete("dictionary link chain is cyclic (corrupt)");
return 0;
}
DictEntry **entries = (DictEntry **)0;
if (total > 0) {
entries = (DictEntry **)PARITY_ALLOC((size_t)total * sizeof(DictEntry *));
if (!entries) {
parity_incomplete("could not allocate the dictionary entry list");
return 0;
}
}
*complete = 1;
/* 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) {
for (DictEntry *e = vm->latest; e != NULL && count < total; 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;
}
entries[count++] = e;
#if SK_PARITY_DEBUG
sk_parity_debug_check_ptr(vm, "dict_link", e->link, count, e, e->func, e->link);
#endif
}
/* A sorted copy of the entry list: colon-body cells are dereferenced
* only if they are one of these (see parity_sort_entries()). */
DictEntry **known = (DictEntry **)0;
if (count > 0) {
known = (DictEntry **)PARITY_ALLOC((size_t)count * sizeof(DictEntry *));
if (!known) {
PARITY_FREE(entries);
*complete = 0;
parity_incomplete("could not allocate the sorted entry list");
return 0;
}
memcpy(known, entries, (size_t)count * sizeof(DictEntry *));
parity_sort_entries(known, count);
}
/* Hash in creation order (oldest to newest = reverse of array) */
uint64_t hash = FNV1A_64_OFFSET_BASIS;
@@ -710,10 +865,12 @@ uint64_t sk_dict_canonical_hash(struct VM *vm) {
/* 6. For colon definitions: hash body as word_id sequence */
if (e->flags & WORD_COMPILED) {
hash = hash_colon_body(vm, e, ordinal, hash);
hash = hash_colon_body(vm, e, ordinal, hash, complete, known, count);
}
}
if (known) PARITY_FREE(known);
if (entries) PARITY_FREE(entries);
return hash;
}
@@ -748,6 +905,7 @@ void sk_parity_collect(struct VM *vm, ParityPacket *out) {
out->here_offset = 0;
out->latest_word_id = 0;
out->header_hash64 = 0;
out->hash_complete = 0;
out->tests_total = 0;
out->tests_passed = 0;
out->tests_failed = 0;
@@ -765,7 +923,10 @@ void sk_parity_collect(struct VM *vm, ParityPacket *out) {
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);
out->header_hash64 = sk_dict_canonical_hash(vm, &out->hash_complete);
if (!out->hash_complete && out->bootstrap_result == SK_BOOTSTRAP_OK) {
out->bootstrap_result = SK_BOOTSTRAP_PARITY_INCOMPLETE; /* S-1a: never a quiet wrong hash */
}
/* M7.1b fields - from global test stats (if tests were run) */
#ifdef STARFORTH_ENABLE_TESTS
@@ -896,7 +1057,11 @@ void sk_parity_print(const ParityPacket *pkt) {
print_str(" latest_id=");
print_u32(pkt->latest_word_id);
print_str(" hash=");
print_hex64(pkt->header_hash64);
if (pkt->hash_complete) {
print_hex64(pkt->header_hash64);
} else {
print_str("INCOMPLETE"); /* S-1a: never print a hash over part of the dictionary */
}
print_nl();
/* M7.1b line (only if tests were run) */