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
+10 -1
View File
@@ -11,7 +11,10 @@
*
* Section 1.4: "Every retired instruction increments that opcode's heat counter
* and advances the anti-clock; every `call` increments the heat counter of its
* target. None of this costs an instruction."
* target. None of this costs an instruction." A call's target is counted by
* whoever runs the node, through the hook below (ENGINE.md step 3; plan
* 2026-10-20, Task A1, #189): the kernel keeps a word's accounts. The engine's
* own per-call-target table, a stand-in, is gone.
*
* The node owns P, A, B, the stacks and memory; heat and the anti-clock are
* separate physics state (section 7 names them as registers).
@@ -37,8 +40,14 @@
#include "v4/iword.h"
#include "v4/node.h"
/* Called at `call` once the return address is pushed and before P moves to the
* target (so P is still the return address). NULL: no hook. */
typedef void (*v4_call_hook)(void *ctx, v4_node *n, v4_cell target);
typedef struct {
v4_uheat_t anticlock;
v4_call_hook on_call; /* reset leaves NULL */
void *on_call_ctx;
/* A node blocked at its port in the middle of an instruction word
* (node.h) goes on, when it has been served, from the slot after the
+9 -45
View File
@@ -8,27 +8,21 @@
* the anti-clock; every `call` increments the heat counter of its target. None
* of this costs an instruction."
*
* D-6 is deferred to step 2, but the note immediately adds: "The golden model
* implements per-opcode and per-call-target heat in the meantime." The
* counters here are therefore a two-dimensional heat structure (op × target)
* on a per-call basis, with counts stored in 64-bit (because heat is
* accumulated and must not roll over at 32-bit). The "per-call-target"
* counter is indexed by the address of the called target -- on a word-addressed
* node, that is a cell value equal to the address `a` passed to `call a`.
* The per-opcode counters are here. A call's target is counted by whoever runs
* the node, through exec.h's hook at `call` (ENGINE.md step 3: "The
* per-call-target count goes"; plan 2026-10-20, Task A1, #189) -- the kernel
* keeps a word's accounts. The 4096-slot per-call-target table and its freeze
* mask, the golden model's stand-in "in the meantime" (D-6), were here until
* 2026-11-12; a target at 4096 or above was not counted, so no capsule word was.
*
* The anti-clock is a virtual tick driven by execution -- one tick per retired
* instruction -- and the rest of §7 refers to it via `ANTICLOCK`. It is not a
* wall-clock time, it is a function of the execution stream, as specified.
*
* The MM names in §7: `HEAT-OP[0..31]` (R), `HEAT-CALL[...]` (R/W) with a
* freeze bit (D-6). D-4 (memory map) is deferred, so the model does not
* materialize those MM addresses yet. This header defines the backing storage
* that a later MM layer will expose; it keeps the counts separate from the node
* itself because heat is physics data, not the instruction word stream, and it
* needs to be accessible to assertions such as K conservation tests later.
* The MM names in §7: `HEAT-OP[0..31]` (R) are these counters; `HEAT-CALL[...]`
* is the kernel's word accounts (above). D-4 (memory map) is deferred.
*
* Counts are 64-bit. Even for a short run a call-heavy word can heat up many
* times, and the golden model must run POST and remain monotonic and
* Counts are 64-bit. Even for a short run an opcode can retire many times, and the golden model must run POST and remain monotonic and
* comparable. They are stored as `v4_uheat_t` (which is uint64_t by cell-width
* parameterisation) rather than cell-sized so that a 32-bit node's heat is not
* width-truncated in the model. (JUSTIFICATION.md §5 keeps heat arithmetic
@@ -40,20 +34,8 @@
#include "v4/cell.h"
#include "v4/opcode.h"
#define V4_HEAT_MAX_CALL_TARGETS 4096u
typedef struct {
v4_uheat_t op[V4_OPCODE_COUNT]; /* HEAT-OP[0..31] */
/* Per-call-target heat. Indexed by target address. The table is fixed-size
* for the golden model; the actual hardware table size is deferred with D-6,
* but "in the meantime" the model must have a per-call-target structure.
* 4096 entries is small (32KB if using 64-bit per entry), enough to cover a
* typical test program's call targets without needing to allocate on-the-fly
* -- and since the node has boundaries, over-indexing is caught. */
v4_uheat_t call[V4_HEAT_MAX_CALL_TARGETS];
v4_ucell call_freeze_mask[V4_HEAT_MAX_CALL_TARGETS / (sizeof(v4_ucell) * 8u)];
/* freeze bits: one per call-target entry (D-6). Stored as a bitmask. */
} v4_heat;
void v4_heat_reset(v4_heat *h);
@@ -61,22 +43,4 @@ void v4_heat_reset(v4_heat *h);
/* Every retired instruction: +1 to the opcode's heat and +1 to ANTICLOCK. */
void v4_heat_on_retire(v4_heat *h, unsigned op, v4_uheat_t *anticlock);
/* Every call: +1 to the call-target's heat. The target is the address passed
* to call (word address). If the target is outside the table range, the model
* must not silently wrap -- a boundary-style check is the conservative choice
* here, because the table size is a model parameter. */
void v4_heat_on_call(v4_heat *h, v4_cell target);
/* Read/write helpers for per-call-target heat (R/W). Freeze bits are set per
* target when D-6's freeze bit is asserted; writes to a frozen target should be
* ignored in a real implementation, but the MM layer is not defined yet. The
* golden model stores freeze bits and the accessor can enforce them later. */
v4_uheat_t v4_heat_call_get(const v4_heat *h, v4_cell target);
int v4_heat_call_set(v4_heat *h, v4_cell target, v4_uheat_t val);
/* Freeze control (D-6). */
void v4_heat_call_freeze(v4_heat *h, v4_cell target);
void v4_heat_call_unfreeze(v4_heat *h, v4_cell target);
int v4_heat_call_is_frozen(const v4_heat *h, v4_cell target);
#endif /* V4_HEAT_H */