Stage K-2: COLD forgets to the fence; PAD moved below the block window (FABRIC-3.10.8 §4.2)

- Defect 1: COLD now forgets every word since the fence exactly as FORGET
  does (new vm_dictionary_forget_to_fence(): untrack, metrics, entry) and
  rewinds HERE to the fence. It rewound HERE alone to 1024, leaving live
  words to be compiled over.
- Defect 2: PAD = BLK_VM_WINDOW_BASE - 512 (was inside the window's top
  slot, shared with BLOCK/BUFFER).
- Probe (plain FORTH, all three ISAs identical): PAD..PAD+511 =
  5238272..5238783 below the window (5238784); on a worker KA/KC found,
  COLD, then KA/KB/KC not found, HERE at the fence (1424), new word works.
- Standard sequence on amd64/aarch64/riscv64: hash and G unchanged,
  MINT/WIREBIND/USE ok, conserved, 0 idle growth.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Robert Allan James
2026-09-26 03:13:21 -04:00
co-authored by Claude Opus 5.5
parent 27c430e0ca
commit c9c97cf769
11 changed files with 27609 additions and 14 deletions
+33
View File
@@ -479,3 +479,36 @@ In the same run, after the second churn, rajames's `1 2 + .` was **refused by ke
- The `dict_hash` set, the G string and the ledger line are unchanged.
- **K-1 closes §8 defects 5–9 and the `sf_realloc()`/call-state-stack bug.** Its churn-leak criterion carries into K-4.
### 4.2 K-2 — `COLD` and `PAD` (2026-09-26)
**Change.**
- **Defect 1 (`COLD`).** `reset_vm_state()`'s cold branch now calls a new `vm_dictionary_forget_to_fence()` (dictionary_management.c). It forgets every word defined since the fence exactly as FORGET does, and rewinds `HERE` to the fence:
- untrack (the id recycled, the Stadium cell reclaimed);
- free the transition metrics and their arrays;
- free the entry.
It used to rewind `HERE` alone, to 1024, leaving `latest` at live words that the next definitions compiled over.
- The fence is set after `vm_init()` allots the input buffers, so `HERE` can't land below them.
- What `COLD` forgets is therefore what FORGET could forget. For a worker that includes its workload capsule's words (the fence is set at `vm_init()`). Hera's SDK capsule raises FENCE, so her base and SDK words survive.
- FORGET itself is unchanged.
- **Defect 2 (`PAD`).** `PAD` is now `BLK_VM_WINDOW_BASE - 512`, the 512 bytes just below the block I/O window. It was `VM_MEMORY_SIZE - 512`, inside the window's top slot. The span between the 2 MB dictionary limit and the window is otherwise unused.
**Probe** (plain FORTH, `k2.sh`, run on the clean build after O-A; no temporary code):
- `7770 . PAD . PAD 511 + .` on Hera;
- a worker `kc` defines KA, KB and KC and prints whether KA and KC are findable, then HERE;
- then `COLD`, `FIND` for each of the three, HERE, and a new definition `: KD 4 ; KD .`.
The results are identical on all three ISAs:
| ISA | log | PAD … PAD+511 | window base / dictionary limit | before COLD: KA, KC found; HERE | after COLD: KA, KB, KC; HERE; KD |
|---|---|---|---|---|---|
| amd64 | `logs/20260926-023300` | 5,238,272 … 5,238,783 | 5,238,784 / 2,097,152 | yes, yes; 1856 | 0, 0, 0; 1424 (the fence); 4 |
| aarch64 | `logs/20260926-024629` | same | same | same | same |
| riscv64 | `logs/20260926-030013` | same | same | same | same |
**Standard sequence** (the same runs, after the probe).
- All three ISAs pass: hash `0x99b01134c3d3049c`, G byte-identical (A=40 B=40, run before the probe), MINT / WIREBIND / USE / `1 2 + .` → `3`, 0 B idle growth, 0 UNKNOWN and 0 faults.
- The ledger totals (pulled 262144 / returned 259359) are higher than the usual line only because the probe worker's messages are counted; held and consumed are unchanged, and `stadium_conserved(self)=true`.
- The hosted `make` builds.
- **K-2 closes §8 defects 1 and 2.**
+5
View File
@@ -808,6 +808,11 @@ uint32_t vm_dictionary_word_id_refusals(void);
void vm_dictionary_untrack_entry(VM* vm, DictEntry* entry);
/* FABRIC-3.10.8 K-2: forget every word defined since the fence (as FORGET
* does) and rewind HERE to the fence -- COLD's dictionary reset. Returns
* the number of words forgotten. */
int vm_dictionary_forget_to_fence(VM* vm);
cell_t* vm_dictionary_get_data_field(DictEntry* entry);
void vm_compile_word(VM* vm, DictEntry* entry);
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
+30
View File
@@ -692,3 +692,33 @@ void vm_unpin_execution_heat(VM *vm) {
/* MC-0c: vm_dictionary_index_reset() (the global index's hard reset, no
* callers) is gone; a VM's index is freed with the VM, vm_dictionary_index_free(). */
/* FABRIC-3.10.8 K-2: forget everything defined since the fence, exactly as
* FORGET removes words (untrack -- id recycled, Stadium cell reclaimed --
* then metrics, then the entry), and rewind HERE to the fence. COLD's
* dictionary half: it used to rewind HERE alone, leaving `latest` pointing
* at words whose code HERE would then compile over. Returns the number of
* words forgotten. */
int vm_dictionary_forget_to_fence(VM *vm) {
int n = 0;
DictEntry *e;
if (!vm) return 0;
sf_mutex_lock(&vm->dict_lock);
e = vm->latest;
while (e && e != vm->dict_fence_latest) {
DictEntry *next = e->link;
vm_dictionary_untrack_entry(vm, e);
if (e->transition_metrics) {
transition_metrics_cleanup(e->transition_metrics);
sf_free(e->transition_metrics);
e->transition_metrics = NULL;
}
sf_free(e);
n++;
e = next;
}
vm->latest = vm->dict_fence_latest;
vm->here = vm->dict_fence_here;
sf_mutex_unlock(&vm->dict_lock);
return n;
}
+6 -2
View File
@@ -161,10 +161,14 @@ void dictionary_word_2comma(VM *vm) {
/**
* @brief PAD ( -- addr ) Get scratch pad buffer address
* @param vm Pointer to the VM structure
* @details Returns VM address of 512-byte temporary text buffer at top of memory
* @details Returns VM address of the 512-byte temporary text buffer: the
* 512 bytes just below the block I/O window. FABRIC-3.10.8 K-2: it used to
* be VM_MEMORY_SIZE - 512, inside the window's top slot, so BLOCK/BUFFER
* and PAD overwrote each other. The span between the dictionary limit
* (DICTIONARY_MEMORY_SIZE) and the window is otherwise unused.
*/
void dictionary_word_pad(VM *vm) {
size_t pad_offset = VM_MEMORY_SIZE - 512;
size_t pad_offset = BLK_VM_WINDOW_BASE - 512;
vm_push(vm, (cell_t) pad_offset);
}
+8 -12
View File
@@ -88,18 +88,14 @@ static void reset_vm_state(VM *vm, int cold_start) {
vm->mode = MODE_INTERPRET;
if (cold_start) {
/* In a complete system, this would restore a boot image.
For now we keep a minimal fence by not crushing the base dictionary. */
if (vm->here > 1024) {
/* S-1c: never below the input buffers allotted at vm_init()
* (TIB/>IN/SPAN/WORD buffer end past 1024). */
size_t floor = 1024;
if (vm->word_buf) {
size_t buf_end = (size_t)(vm->word_buf - vm->memory) + WORD_BUFFER_SIZE;
if (buf_end > floor) floor = buf_end;
}
if (vm->here > floor) vm->here = floor;
}
/* FABRIC-3.10.8 K-2: back to the post-boot dictionary -- every word
* defined since the fence forgotten (as FORGET does it) and HERE at
* the fence. This used to rewind HERE alone, to 1024, leaving
* `latest` pointing at live words that the next definitions then
* compiled over. The fence is set after vm_init() allots the input
* buffers (S-1c), so HERE never lands below them. */
int n = vm_dictionary_forget_to_fence(vm);
log_message(LOG_INFO, "COLD: %d word(s) forgotten, HERE=%ld", n, (long) vm->here);
}
system_running = 1;