Stage S-1b: dictionary word ids grow with the dictionary; a word that can't get an id is refused whole (FABRIC-3.9.0 §8.1)

- word_id_map / recycled_word_ids are heap tables (one shared capacity,
  64 doubling), grown all-or-nothing by allocate-copy-free. Numbering is
  unchanged, so boot parity is unchanged.
- vm_create_word() takes the id before linking; on failure it frees the
  entry, sets vm->error, logs LOG_ERROR and counts
  (vm_dictionary_word_id_refusals()). No word runs with WORD_ID_INVALID.
- Readers of the id space bound by vm->next_word_id, not DICTIONARY_SIZE.
- transition_heat gets its own capacity and grows on record (was sized
  once -- a heap overflow under a dynamic bound); every reader bounds by it.
- Stadium id->cell rows start empty and grow on dispatch; a failed grow is
  logged, counted and not admitted. S-2 (the cell pool) stays open.

Probe (reverted), identical on amd64/aarch64/riscv64: +2000 words get real
ids (capacity 4096); a word at id 2738 lands 200x in the rolling window and
is Stadium-admitted; FORGET recycles across the old boundary; an injected
grow failure refuses the word whole (NULL, error, counter, dictionary
unchanged) and the retry succeeds. 3-ISA acceptance passes; boot parity
(531, 0x572daac83b797332) and the dict_hash set unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Robert Allan James
2026-09-24 13:59:43 -04:00
co-authored by Claude Opus 5.5
parent ee3db96ea8
commit 1dbd0c74ed
26 changed files with 55442 additions and 83 deletions
+11 -4
View File
@@ -154,11 +154,15 @@ typedef struct WordTransitionMetrics {
* transition_heat[i]: Number of times word with index i has been
* executed immediately after this word.
*
* Dynamically allocated array of size DICTIONARY_SIZE.
* Dynamically allocated, transition_heat_cap entries long.
* Initialized to NULL; allocated on first use to save memory
* for words that are never executed.
* for words that are never executed, and grown when a word id at or
* past transition_heat_cap is recorded (Stage S-1b: word ids are no
* longer capped at DICTIONARY_SIZE, so no fixed length can hold them).
* Every reader bounds by transition_heat_cap, never by a global size.
*/
uint64_t *transition_heat;
uint32_t transition_heat_cap;
/** Total number of transitions observed from this word */
uint64_t total_transitions;
@@ -264,11 +268,14 @@ int transition_metrics_init(WordTransitionMetrics *metrics);
*
* @param metrics Metrics for the current word
* @param next_word_id Index of word that executed next
* @param dict_size Total number of words in dictionary (for array bounds)
* @return 0 on success, -1 on invalid word_id
* @param dict_size The VM's word-id bound (vm->next_word_id): every issued id is below it
* @return 0 on success, -1 on invalid word_id or a failed grow (logged, counted)
*/
int transition_metrics_record(WordTransitionMetrics *metrics, uint32_t next_word_id, uint32_t dict_size);
/* S-1b: transitions dropped because a transition_heat array couldn't grow. */
uint64_t transition_metrics_grow_failures(void);
/*
* Get the probability (Q48.16 format) of a specific word following this word.
*
+10 -4
View File
@@ -48,7 +48,8 @@
/*
* stadium_words_init - Allocates and zeroes the word_id -> cell_index map
* (DICTIONARY_SIZE entries per VM quota slot, stadium_max_vm_count() slots,
* (one row per VM quota slot, stadium_max_vm_count() slots; since S-1b each
* row starts empty and grows on dispatch, no longer DICTIONARY_SIZE entries;
* kmalloc'd -- was a flat static array before 2026-08-15, when the VM count
* bound became RAM-derived rather than a compile-time constant). Must be
* called after stadium_boot_init() and stadium_birth_hera(), before any word
@@ -97,8 +98,10 @@ void stadium_words_init(void);
* the failed attempt). On success, records the mapping and increments the
* promotion counter.
*
* No-op if word_id == WORD_ID_INVALID, word_id >= DICTIONARY_SIZE, or vm_id
* holds no Stadium quota.
* No-op if word_id == WORD_ID_INVALID or vm_id holds no Stadium quota. A
* word_id past the slot's row grows the row first (S-1b); if that fails the
* dispatch is not admitted, logged and counted
* (stadium_words_row_grow_failures()).
*
* @param vm_id Owning VM -- vm->stadium_vm_id at every call site.
* Scopes the word_id -> cell_index lookup to this
@@ -140,7 +143,7 @@ void stadium_word_forget(VMUuid vm_id, uint32_t word_id);
* Bob's ruling 2026-08-06) can add this as an explicit term instead of
* silently omitting word-execution heat it has no other way to see.
*
* Walks all DICTIONARY_SIZE word_slots for vm_id's quota slot; each
* Walks all of the word_slots row for vm_id's quota slot; each
* resident entry contributes its cell's current heat, verified live against
* the discriminator bitmap (same self-healing pattern as
* resolve_resident_cell() -- a stale map entry contributes 0, not garbage).
@@ -161,6 +164,9 @@ uint64_t stadium_words_resident_heat(VMUuid vm_id);
*/
void stadium_words_stats(VMUuid vm_id, uint64_t *promotions, uint64_t *evictions);
/* S-1b: dispatches not admitted because a slot's word row couldn't grow. */
uint64_t stadium_words_row_grow_failures(void);
/*
* stadium_words_print_boot_diagnostics - Console output satisfying item
* 4.1's "observable via a diagnostic word or boot console output"
+15 -3
View File
@@ -510,8 +510,15 @@ typedef struct VM
uint8_t* memory; /**< Unified VM memory buffer */
uintptr_t here; /**< Next free memory location (byte offset or absolute pointer if M7) */
DictEntry* latest; /**< Most recent word */
DictEntry* word_id_map[DICTIONARY_SIZE]; /**< Stable ID → entry map for speculation */
uint32_t recycled_word_ids[DICTIONARY_SIZE]; /**< Reusable ID stack for FORGET */
/* FABRIC-3.9.0.md §8.1 (Stage S-1b): the stable-id tables grow with the
* dictionary (were fixed [DICTIONARY_SIZE] arrays -- word 1025 onward got
* WORD_ID_INVALID and ran outside the physics). Both share one capacity:
* the recycle stack can never hold more ids than were ever issued. Every
* issued id, live or recycled, is < next_word_id -- that is the bound for
* any reader of the id space. */
DictEntry** word_id_map; /**< Stable ID → entry map for speculation (word_id_capacity entries) */
uint32_t* recycled_word_ids; /**< Reusable ID stack for FORGET (word_id_capacity entries) */
uint32_t word_id_capacity; /**< Allocated length of both tables */
uint32_t recycled_word_id_count; /**< Depth of recycled stack */
uint32_t next_word_id; /**< Next fresh ID when recycle stack empty */
@@ -767,7 +774,12 @@ DictEntry* vm_dictionary_lookup_by_word_id(VM* vm, uint32_t word_id);
* popped off the data stack (EXECUTE, ENTROPY@/!, etc). */
int vm_dict_entry_ok(VM* vm, DictEntry* candidate);
void vm_dictionary_track_entry(VM* vm, DictEntry* entry);
/* Assign `entry` a stable word id, growing the id tables if needed (S-1b).
* Returns 0, or -1 if the tables can't grow: logged at LOG_ERROR, counted
* in vm_dictionary_word_id_refusals(), and the caller must refuse the word. */
int vm_dictionary_track_entry(VM* vm, DictEntry* entry);
uint32_t vm_dictionary_word_id_refusals(void);
void vm_dictionary_untrack_entry(VM* vm, DictEntry* entry);