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:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user