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
+2 -49
View File
@@ -1,7 +1,8 @@
/* SPDX-License-Identifier: LicenseRef-Starship-1.0
* Copyright (c) 2023-2026 Robert A. James
*/
/* test_heat.c -- heat and anti-clock. */
/* test_heat.c -- heat and anti-clock. A call's target is counted through exec.h's hook
* (test_exec.c), not here: the per-call-target table is gone (#189). */
#include "v4/heat.h"
#include <stdint.h>
#include <stdio.h>
@@ -30,11 +31,6 @@ static void test_reset(void)
for (unsigned i = 0; i < V4_OPCODE_COUNT; i++) {
CHECK(h.op[i] == 0, "op %u heat non-zero after reset", i);
}
for (unsigned i = 0; i < V4_HEAT_MAX_CALL_TARGETS; i++) {
CHECK(h.call[i] == 0, "call target %u heat non-zero after reset", i);
CHECK(!v4_heat_call_is_frozen(&h, (v4_cell)i),
"call target %u frozen after reset", i);
}
v4_uheat_t ac = 999;
v4_heat_on_retire(&h, V4_OP_NOP, &ac);
CHECK(ac == 1000, "anticlock not advanced");
@@ -57,55 +53,12 @@ static void test_retire_advances_anticlock_and_opcode(void)
}
}
static void test_call_advances_target(void)
{
v4_heat h;
v4_heat_reset(&h);
v4_heat_on_call(&h, 10);
v4_heat_on_call(&h, 10);
v4_heat_on_call(&h, 5);
CHECK(v4_heat_call_get(&h, 10) == 2, "target 10");
CHECK(v4_heat_call_get(&h, 5) == 1, "target 5");
CHECK(v4_heat_call_get(&h, 1) == 0, "target 1");
}
static void test_freeze(void)
{
v4_heat h;
v4_heat_reset(&h);
v4_heat_on_call(&h, 7);
v4_heat_call_freeze(&h, 7);
CHECK(v4_heat_call_is_frozen(&h, 7), "frozen");
v4_heat_on_call(&h, 7);
CHECK(v4_heat_call_get(&h, 7) == 1, "still 1");
CHECK(v4_heat_call_set(&h, 7, 100) == 0, "set returns 0 when frozen");
CHECK(v4_heat_call_get(&h, 7) == 1, "unchanged");
v4_heat_call_unfreeze(&h, 7);
CHECK(!v4_heat_call_is_frozen(&h, 7), "unfrozen");
CHECK(v4_heat_call_set(&h, 7, 42) == 1, "set allowed");
CHECK(v4_heat_call_get(&h, 7) == 42, "changed");
}
static void test_out_of_range_ignored(void)
{
v4_heat h;
v4_heat_reset(&h);
v4_heat_on_call(&h, (v4_cell)V4_HEAT_MAX_CALL_TARGETS + 5);
CHECK(v4_heat_call_get(&h, (v4_cell)V4_HEAT_MAX_CALL_TARGETS + 5) == 0,
"out of range call ignored");
CHECK(v4_heat_call_set(&h, (v4_cell)-1, 123) == 0, "set out of range");
CHECK(v4_heat_call_is_frozen(&h, (v4_cell)-1) == 0, "frozen check");
}
int main(void)
{
printf("v4 heat tests: V4_CELL_BITS=%d\n", V4_CELL_BITS);
test_reset();
test_retire_advances_anticlock_and_opcode();
test_call_advances_target();
test_freeze();
test_out_of_range_ignored();
printf(" %d checks, %d failures\n", checks, failures);
return failures ? 1 : 0;