S-1b follow-up: free the refused word's metrics context_window; correct the pipelining claim; review record

- vm_create_word()'s S-1b refusal path now calls transition_metrics_cleanup()
  before freeing the metrics struct (its context_window leaked).
- FABRIC-3.9.8: the transition_heat grow is untested -- ENABLE_PIPELINING=0
  in the kernel AND the default hosted build; the doc wrongly said hosted.
- Recorded the review: moving id tables have no unlocked or interrupt
  readers (heartbeat lookups hold dict_lock, as does the grow); sweep for
  other word-id-indexed arrays found none unbounded. Status line updated.

3-ISA acceptance passes; boot parity and 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 14:42:05 -04:00
co-authored by Claude Opus 5.5
parent 1dbd0c74ed
commit a2e3356b2e
9 changed files with 27559 additions and 4 deletions
+22 -4
View File
@@ -1,7 +1,7 @@
# FABRIC-3.9.8.md — Observers and participants: execution plan and log
**Status: PLANNED. No stage started.** The design and its open rulings (O-1 to O-4, plus the
discard question) are in `FABRIC-3.9.0.md`.
**Status: IN PROGRESS.** Done: O-A, the stack-convention fix, S-1a and S-1b (§2). The design and
its rulings (O-1 to O-4, the discard, and §7/§8's M- and S- rulings) are in `FABRIC-3.9.0.md`.
Each stage:
- needs Captain Bob's explicit go;
@@ -267,8 +267,9 @@ Evidence is named **before** each stage runs.
- Every reader bounds by that field. `get_probability` had no bounds check at all.
- A failed grow drops that one transition (the word already ran; nothing is left to refuse).
It is logged and counted (`transition_metrics_grow_failures()`).
- Pipelining is compiled out of the kernel (`ENABLE_PIPELINING=0`), so this path is live
only in hosted builds.
- **Untested at runtime.** Pipelining is compiled out of the kernel *and* the default
hosted build (`ENABLE_PIPELINING=0` in both), so this grow path is dead in every shipped
configuration. No run exercised it. It is corrected, not proven.
- **Stadium's id→cell rows (`stadium_words.c`).** These were `[slot][DICTIONARY_SIZE]`.
- Each row now starts empty and grows on dispatch (`row_cap[slot]`), not at definition,
because a quota slot can be granted after a VM's words exist, and slots are reused across
@@ -320,3 +321,20 @@ Evidence is named **before** each stage runs.
`vm_dictionary_word_id_refusals()`, `transition_metrics_grow_failures()`,
`stadium_words_row_grow_failures()`). Adding a word changes the `dict_hash` set, so this
awaits Captain Bob's ruling.
**Follow-up review (same day, own commit; acceptance `logs/20260924-140359/amd64/`, `-141643/aarch64/`, `-142946/riscv64/`: boot parity and `dict_hash` set unchanged, O-A checks, relay `3`, idle growth 0, no faults):**
- **The refusal path leaked each metrics' `context_window`.** It freed the
`WordTransitionMetrics` struct without `transition_metrics_cleanup()`. That call is now made
first.
- **Moving tables were checked for concurrent readers.** Before S-1b the id map never moved;
now a grow frees the old map, and likewise the Stadium rows. Every id lookup off the
interpreter path runs under `dict_lock`: the heartbeat decay cursor (`vm_time.c`,
`vm_runtime.c`), the rolling-window seed, and inference. `vm_create_word()` holds that
lock across the grow. In the kernel, the heartbeat is mainline and execution-paced, not an
interrupt, and Stadium rows are touched only from the interpreter and
`stadium_words_resident_heat`'s FORTH word. No interrupt reads either.
- **Sweep for other arrays indexed by word ids** (not only those named `DICTIONARY_SIZE`):
- `rolling_window_find_hottest_word()` sizes by its own `dict_size` parameter and has no
callers.
- No word-id field narrower than 32 bits.
- Nothing else found.
Binary file not shown.
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
+1
View File
@@ -535,6 +535,7 @@ DictEntry *vm_create_word(VM *vm, const char *name, size_t len, word_func_t func
if (vm_dictionary_track_entry(vm, entry) != 0) {
vm->error = 1;
if (entry->transition_metrics) {
transition_metrics_cleanup(entry->transition_metrics); /* its context_window too */
sf_free(entry->transition_metrics);
}
sf_free(entry);