feat(v4): a hook at call, for whoever runs the node

Plan 2026-10-20 (ENGINE step 4), Task A1, for #189 -- Captain Bob,
2026-11-12: the physics blocks alpha.5; order A, 1, 2, then 7.

- exec.h/exec.c: v4_call_hook, es->on_call/on_call_ctx; called at `call`
  once the return address is pushed, before P moves. NULL: as before.
- heat.h/heat.c: the per-call-target table (4096 slots, no capsule word
  counted), its freeze mask and v4_heat_call_* go (ENGINE.md step 3: "The
  per-call-target count goes"); the kernel will keep a word's accounts.
- tests: test_call_hook_sees_every_call, seen to fail first (no on_call);
  test_heat's per-call tests go with the table.
- the plan records today's rulings (order; HEAT-OP: both, a full array).

Suite (logs/20261112-225951/v4-tests): test, test-32, sanitize, 0
failures; hosted-check, the three alike. amd64 boot: POST 538/538,
dict_hash 0x8c92508f748f99cf, unchanged -- nothing sets the hook yet.

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-12 23:01:52 +00:00
parent f38761ff66
commit 3c97534c8b
14 changed files with 110 additions and 169 deletions
+3 -1
View File
@@ -98,6 +98,8 @@ static int stack_faulted(v4_node *n, unsigned op)
void v4_exec_reset(v4_exec_state *es)
{
es->anticlock = 0;
es->on_call = 0;
es->on_call_ctx = 0;
es->resume = 0;
es->resume_iw = 0;
es->resume_slot = 0;
@@ -133,7 +135,7 @@ void v4_exec_op(v4_node *n, v4_exec_state *es, v4_heat *h,
case V4_OP_CALL:
x = v4_iword_branch(n->p, w, slot);
v4_rstack_push(rs, n->p);
v4_heat_on_call(h, x);
if (es->on_call) es->on_call(es->on_call_ctx, n, x);
n->p = x;
break;
case V4_OP_UNEXT:
-67
View File
@@ -10,8 +10,6 @@ void v4_heat_reset(v4_heat *h)
* the bare-metal kernel can link it as it is. */
unsigned i;
for (i = 0; i < sizeof h->op / sizeof h->op[0]; i++) h->op[i] = 0;
for (i = 0; i < sizeof h->call / sizeof h->call[0]; i++) h->call[i] = 0;
for (i = 0; i < sizeof h->call_freeze_mask / sizeof h->call_freeze_mask[0]; i++) h->call_freeze_mask[i] = 0;
}
void v4_heat_on_retire(v4_heat *h, unsigned op, v4_uheat_t *anticlock)
@@ -21,68 +19,3 @@ void v4_heat_on_retire(v4_heat *h, unsigned op, v4_uheat_t *anticlock)
}
if (anticlock) (*anticlock)++;
}
static unsigned call_index(v4_cell target)
{
/* The per-call-target heat table is the golden model's stand-in until D-6 rules which
* heat structures exist in hardware (DECOMPOSITION.md D-6). It is indexed by the call's
* target address itself, V4_HEAT_MAX_CALL_TARGETS slots. A target at or past that has
* no slot: it is not counted, and (unsigned)-1 tells the callers so -- they then record
* nothing and read 0, rather than counting it in another target's slot. ENGINE.md step 4,
* Part A1, plans to remove the table with v4_heat_on_call (corrected 2026-11-12, #181:
* this comment read as an unfinished question). */
unsigned idx = (unsigned)(v4_ucell)target;
if (idx >= V4_HEAT_MAX_CALL_TARGETS) return (unsigned)-1;
return idx;
}
void v4_heat_on_call(v4_heat *h, v4_cell target)
{
unsigned idx = call_index(target);
if (idx == (unsigned)-1) return;
if (v4_heat_call_is_frozen(h, target)) return;
h->call[idx]++;
}
v4_uheat_t v4_heat_call_get(const v4_heat *h, v4_cell target)
{
unsigned idx = call_index(target);
if (idx == (unsigned)-1) return 0;
return h->call[idx];
}
int v4_heat_call_set(v4_heat *h, v4_cell target, v4_uheat_t val)
{
unsigned idx = call_index(target);
if (idx == (unsigned)-1) return 0;
if (v4_heat_call_is_frozen(h, target)) return 0;
h->call[idx] = val;
return 1;
}
void v4_heat_call_freeze(v4_heat *h, v4_cell target)
{
unsigned idx = call_index(target);
if (idx == (unsigned)-1) return;
unsigned word = idx / (8u * sizeof(v4_ucell));
unsigned bit = idx % (8u * sizeof(v4_ucell));
h->call_freeze_mask[word] |= ((v4_ucell)1u << bit);
}
void v4_heat_call_unfreeze(v4_heat *h, v4_cell target)
{
unsigned idx = call_index(target);
if (idx == (unsigned)-1) return;
unsigned word = idx / (8u * sizeof(v4_ucell));
unsigned bit = idx % (8u * sizeof(v4_ucell));
h->call_freeze_mask[word] &= ~((v4_ucell)1u << bit);
}
int v4_heat_call_is_frozen(const v4_heat *h, v4_cell target)
{
unsigned idx = call_index(target);
if (idx == (unsigned)-1) return 0;
unsigned word = idx / (8u * sizeof(v4_ucell));
unsigned bit = idx % (8u * sizeof(v4_ucell));
return (h->call_freeze_mask[word] & ((v4_ucell)1u << bit)) != 0;
}