feat(v4): word accounts at call -- the word ID in the header, the kernel's records, the Stadium charged per tick (#189)

ENGINE step 4 Phase 2 (plan docs/v4.0.0/plans/2026-11-13-engine-step4-phase2-word-accounts.md). Rulings,
Captain Bob 2026-11-13: "ID field in the header"; "Per heartbeat tick"; the ACL "All of it in Phase 4".

- The word ID, at xt-4 before the flags (ENGINE.md 3b). The system's words are numbered as they are
  assembled, by a new directive, #id, (and #ids, the count), in the C assembler (text.c) and in the
  machine ROM's (capsules/v4/rom), which counts in the system's own (NEXT-ID). (HEADER) numbers later
  words; FORGET gives back the IDs it frees, always the newest; COLD resets to (BOOT-ID); (SEAL) records
  it. All 313 system headers carry the cell.
- The kernel's records (v4/mesh.h): (TELL-DEFINED) and (TELL-FORGOTTEN), requests -19/-20 on port 0,
  which the kernel puts in (WORD-DEFINED)/(WORD-FORGOTTEN) when it seals a node, and reads the system's
  own words once. A bit per address marks a word's xt (calls into the ROM or headerless code are not
  words); at call, the bit test and a count. v4_mesh_charge hands the owner the calls since.
- The Stadium: stadium_words_charge -- N calls as N of stadium_word_dispatch's dispatches, shared by calls
  when the reservoir is short; sk_v4.c charges every node's words at the tick beside its opcodes, and
  stadium_word_forget before an ID is used again.
- Tests first: test_text.c, test_host_dict.c, test_host_quit.c (numbering, FORGET, COLD),
  test_mesh_boot.c (44 checks), kernel/tests/test_stadium_words.c; qemu_session.py: Hera's words
  admitted after POST.
- amd64: Stadium words: promotions=141 after POST, CONSERVED; POST 539/539; word_count=377;
  dict_hash 0xa854ba15b437d288 (the new cell); the boot 142 s against Phase 1's 153 s.
- v4 test, test-32: all pass; v4-checks: 80 programs under the sanitizers, 0 failures; the hosted
  builds alike. docs/reference/v4-dictionary.tsv: the two new words; FABRIC-3 XLVIII.24.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017RzZgrf7V7Zk2iczBXDRbm
This commit is contained in:
Claude
2026-11-13 19:12:44 +00:00
parent d64ad4a1c3
commit e8e7ec8b9f
44 changed files with 1060 additions and 349 deletions
+45
View File
@@ -72,6 +72,32 @@ static v4_fabric_node sk_v4_nucleus, sk_v4_hera;
static v4_image sk_v4_image;
static v4_cell sk_v4_keyed[SK_V4_PLACES]; /* by place: the number its Stadium quota is keyed by; 0: none */
/* The words' accounts (#189, Phase 2; v4/mesh.h): each node's records, kept by word ID, and the bit for each
* address that is a word's xt -- from the kernel's heap at the boot -- and a tick's charge gathered. */
static uint32_t sk_v4_charge_ids[V4_MESH_WORD_IDS];
static uint64_t sk_v4_charge_calls[V4_MESH_WORD_IDS];
static uint32_t sk_v4_charge_n;
static void sk_v4_charge_one(void *ctx, v4_cell id, v4_uheat_t calls)
{
(void)ctx;
if (sk_v4_charge_n < V4_MESH_WORD_IDS) {
sk_v4_charge_ids[sk_v4_charge_n] = (uint32_t)id;
sk_v4_charge_calls[sk_v4_charge_n] = (uint64_t)calls;
sk_v4_charge_n++;
}
}
/* a word has gone: its ID will be used again, so the Stadium forgets its patron first (v3's coherence) */
static void sk_v4_forgot(void *self, unsigned place, v4_cell id)
{
(void)self;
#ifdef STARFORTH_ENABLE_VM
if (place < SK_V4_PLACES && sk_v4_keyed[place] != 0)
stadium_word_forget(stadium_node_key((uint32_t)sk_v4_keyed[place]), (uint32_t)id);
#else
(void)place; (void)id;
#endif
}
static v4_fabric_node *sk_v4_room(void *self)
{
v4_fabric_node *x = (v4_fabric_node *)kmalloc(sizeof *x);
@@ -142,6 +168,11 @@ void sk_v4_charge_opcodes(void)
sk_v4_keyed[k] = number;
}
stadium_ops_charge(stadium_node_key((uint32_t)number), x->h.op, now);
/* and its words, by their calls since (#189, Phase 2: "Per heartbeat tick") */
sk_v4_charge_n = 0;
v4_mesh_charge(&sk_v4_mesh, k, sk_v4_charge_one, 0);
if (sk_v4_charge_n)
stadium_words_charge(stadium_node_key((uint32_t)number), sk_v4_charge_ids, sk_v4_charge_calls, sk_v4_charge_n, now);
}
#endif
}
@@ -264,6 +295,20 @@ void sk_v4_boot(void (*chain)(void), const BootDisk *boot_disk)
sk_v4_mesh.nucleus = &sk_v4_nucleus; sk_v4_mesh.hera = &sk_v4_hera;
sk_v4_mesh.room = sk_v4_room; sk_v4_mesh.unroom = sk_v4_unroom;
sk_v4_mesh.born = 0; sk_v4_mesh.gone = sk_v4_gone; sk_v4_mesh.parity = sk_v4_parity;
sk_v4_mesh.accounts = (v4_mesh_account *)kmalloc((size_t)SK_V4_PLACES * V4_MESH_WORD_IDS * sizeof(v4_mesh_account));
sk_v4_mesh.is_word = (unsigned char *)kmalloc((size_t)SK_V4_PLACES * (V4_NODE_WORDS / 8u));
if (!sk_v4_mesh.accounts || !sk_v4_mesh.is_word) { /* no records kept: the words are not charged */
if (sk_v4_mesh.accounts) kfree(sk_v4_mesh.accounts);
if (sk_v4_mesh.is_word) kfree(sk_v4_mesh.is_word);
sk_v4_mesh.accounts = 0; sk_v4_mesh.is_word = 0;
console_println("StarForth v4: no room for the words' accounts; words are not charged");
} else { /* none kept yet: all zero */
unsigned char *z = (unsigned char *)sk_v4_mesh.accounts;
size_t i, n = (size_t)SK_V4_PLACES * V4_MESH_WORD_IDS * sizeof(v4_mesh_account);
for (i = 0; i < n; i++) z[i] = 0;
for (i = 0; i < (size_t)SK_V4_PLACES * (V4_NODE_WORDS / 8u); i++) sk_v4_mesh.is_word[i] = 0;
}
sk_v4_mesh.forgot = sk_v4_forgot;
sk_v4_mesh.out = sk_v4_out; sk_v4_mesh.key = sk_v4_key;
sk_v4_mesh.words = sk_v4_words; sk_v4_mesh.word_count = sizeof sk_v4_words / sizeof sk_v4_words[0];
sk_v4_mesh.serve = sk_v4_serve;
+68
View File
@@ -263,6 +263,74 @@ void stadium_word_dispatch(VMUuid vm_id, uint32_t word_id, uint64_t heartbeat_ti
}
}
void stadium_words_charge(VMUuid vm_id, const uint32_t *ids, const uint64_t *calls, uint32_t count,
uint64_t heartbeat_ticks) {
int slot;
uint32_t k;
uint64_t total = 0, want, pullable, avail, floor = Q48_ONE / 3;
unsigned shift = 0;
const uint64_t q = (uint64_t)STADIUM_WORD_HEAT_QUANTUM;
if (!words_initialized || !ids || !calls) return;
slot = stadium_quota_slot_for_vm(vm_id);
if (slot < 0) return;
/* First: every resident word charged now cools by the ticks since its own last, once, as its first
* dispatch would; and the calls in all. */
for (k = 0; k < count; k++) {
size_t cell;
if (calls[k] == 0 || ids[k] == WORD_ID_INVALID || ids[k] >= DICTIONARY_SIZE) continue;
total = (total + calls[k] < total) ? UINT64_MAX : total + calls[k];
cell = resolve_resident_cell(slot, ids[k]);
if (cell != STADIUM_CELL_NONE) {
StadiumPatronHeader *h = &stadium_cells()[cell].header;
uint64_t elapsed = heartbeat_ticks - word_slots[slot][ids[k]].last_decay_tick;
if (elapsed > 0) {
uint64_t cooled = ((h->heat * (uint64_t)STADIUM_WORD_COOL_RATE_Q48) >> 16) * elapsed;
if (cooled > h->heat) cooled = h->heat;
if (cooled > 0) { h->heat -= cooled; stadium_reservoir_push(vm_id, cooled); }
word_slots[slot][ids[k]].last_decay_tick = heartbeat_ticks;
}
}
}
if (total == 0) return;
/* Then: a quantum a call, or, when they want more than the floor leaves, a share by calls. */
avail = stadium_reservoir_peek(vm_id);
pullable = avail > floor ? avail - floor : 0;
want = total > UINT64_MAX / q ? UINT64_MAX : total * q;
while ((total >> shift) >= ((uint64_t)1 << 32)) shift++; /* pullable * share fits 64 bits */
for (k = 0; k < count; k++) {
uint64_t give;
size_t cell;
if (calls[k] == 0 || ids[k] == WORD_ID_INVALID || ids[k] >= DICTIONARY_SIZE) continue;
give = want <= pullable ? calls[k] * q
: (total >> shift) ? pullable * (calls[k] >> shift) / (total >> shift) : 0;
give = word_dispatch_pull(vm_id, give);
cell = resolve_resident_cell(slot, ids[k]);
if (cell != STADIUM_CELL_NONE) { stadium_cells()[cell].header.heat += give; continue; }
{
StadiumPatronHeader candidate;
uint8_t *raw = (uint8_t *)&candidate;
size_t i, idx;
for (i = 0; i < sizeof(candidate); i++) raw[i] = 0;
candidate.identity = (uint64_t)ids[k];
candidate.heat = give;
candidate.ttl = 0;
candidate.link = 0;
candidate.contains = STADIUM_CONTAINS_NONE;
candidate.mass = 1;
candidate.flags = 0;
candidate.behaviour = (uint8_t)STADIUM_BEHAVIOUR_COOL;
idx = stadium_admit(vm_id, &candidate);
if (idx == STADIUM_CELL_NONE) { stadium_reservoir_push(vm_id, give); continue; }
word_slots[slot][ids[k]].cell_index = idx;
word_slots[slot][ids[k]].last_decay_tick = heartbeat_ticks;
stat_promotions[slot]++;
}
}
}
void stadium_word_forget(VMUuid vm_id, uint32_t word_id) {
int slot;
size_t cell;