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
+54 -1
View File
@@ -207,7 +207,7 @@ static void test_branches(void)
fresh(); rpush(7); n.mem[10] = bword(0, V4_OP_CALL, 200); n.p = 10;
(void)v4_exec_step_word(&n, &es, &h);
CHECK(n.p == 200 && n.rs.r == 11, "call pushes the return address");
CHECK(v4_heat_call_get(&h, 200) == 1 && h.op[V4_OP_CALL] == 1, "call heats its target");
CHECK(h.op[V4_OP_CALL] == 1, "call is counted as an opcode (its target: the hook, below)");
(void)v4_rstack_pop(&n.rs);
CHECK(n.rs.r == 7, "call pushes exactly one");
@@ -570,6 +570,58 @@ static void test_address_faults(void)
"and they are not faults: only a running programme faults");
}
/* ENGINE.md step 3 (plan 2026-10-20, Task A1): a hook at `call`, for whoever runs the node -- the
* kernel keeps a word's accounts there (#189). It sees every call, in order, once the return address
* is pushed and before P moves; with no hook the node runs exactly as before. */
static v4_cell seen[8];
static unsigned nseen, hook_bad;
static void record_call(void *ctx, v4_node *nd, v4_cell target)
{
if (ctx != (void *)&seen || nd != &n) hook_bad++;
if (nd->p != nd->rs.r) hook_bad++; /* P not moved yet: still the return address */
if (nseen < 8) seen[nseen] = target;
nseen++;
}
static void call_programme(void) /* 100, 200, 100 again, 300; then a halt loop */
{
fresh();
n.mem[10] = bword(0, V4_OP_CALL, 100);
n.mem[11] = bword(0, V4_OP_CALL, 200);
n.mem[12] = bword(0, V4_OP_CALL, 100);
n.mem[13] = bword(0, V4_OP_CALL, 300);
n.mem[14] = bword(0, V4_OP_JUMP, 14);
n.mem[100] = word6(V4_OP_SEMI, NOP, NOP, NOP, NOP, NOP);
n.mem[200] = word6(V4_OP_SEMI, NOP, NOP, NOP, NOP, NOP);
n.mem[300] = word6(V4_OP_SEMI, NOP, NOP, NOP, NOP, NOP);
n.p = 10;
}
static void test_call_hook_sees_every_call(void)
{
unsigned i, retired_hooked = 0, retired_plain = 0;
v4_cell p_hooked, p_plain;
unsigned depth_hooked, depth_plain;
call_programme();
es.on_call = record_call; es.on_call_ctx = (void *)&seen;
nseen = 0; hook_bad = 0;
for (i = 0; i < 9; i++) retired_hooked += v4_exec_step_word(&n, &es, &h);
p_hooked = n.p; depth_hooked = n.rs.depth;
CHECK(nseen == 4, "the hook sees four calls (%u)", nseen);
CHECK(nseen >= 4 && seen[0] == 100 && seen[1] == 200 && seen[2] == 100 && seen[3] == 300,
"the hook sees 100, 200, 100, 300, in order");
CHECK(hook_bad == 0, "the hook is called with its context and the node, before P moves (%u wrong)", hook_bad);
call_programme(); /* the same with no hook */
for (i = 0; i < 9; i++) retired_plain += v4_exec_step_word(&n, &es, &h);
p_plain = n.p; depth_plain = n.rs.depth;
CHECK(p_plain == p_hooked && depth_plain == depth_hooked && retired_plain == retired_hooked,
"with no hook the node runs the same (P %ld/%ld, depth %u/%u, retired %u/%u)",
(long)p_plain, (long)p_hooked, depth_plain, depth_hooked, retired_plain, retired_hooked);
CHECK(es.on_call == 0, "v4_exec_reset leaves no hook");
}
int main(void)
{
printf("v4 exec tests: V4_CELL_BITS=%d\n", V4_CELL_BITS);
@@ -581,6 +633,7 @@ int main(void)
test_memory();
test_return_and_ex();
test_branches();
test_call_hook_sees_every_call();
test_unext();
test_retirement();
#if V4_CELL_BITS == 64