feat(v4.0.0): address faults -- an address outside memory is guarded (D-14)

Ruled 2026-10-04: guarded, an error shown, back to the prompt.

- The executor checks every address a programme uses (P, A, B) before
  using it.  Outside memory the opcode does nothing, the rest of its word
  is not executed, and P becomes the node's fault handler; a node with no
  handler stops.  v4_node_load/store never index outside memory.
- capsule/quit.v4: (FAULT), the host node's handler, prints
  "Address out of range", ends an open definition, prints ERROR and
  returns to the prompt.
- tests: every memory opcode and P in test_exec.c; from the prompt, from
  inside nested words and loops, in test_host_quit.c.

This closes the hole node.h described: a wild address used to index the
model's own memory gigabytes out of bounds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rajames
2026-10-04 12:04:59 -04:00
co-authored by Claude Opus 5.5
parent b5d644d498
commit b3d2d56d11
11 changed files with 277 additions and 74 deletions
+105
View File
@@ -316,6 +316,110 @@ static void test_soak(void)
CHECK(v4_node_guards_intact(&n), "soak left the guards intact");
}
/* D-14: an address outside node memory is a fault. The opcode does nothing,
* the rest of its word is not executed, and P becomes the handler; with no
* handler the node stops. */
#define HANDLER ((v4_cell)40)
static void test_address_faults(void)
{
static const v4_cell bad[] = { -1, (v4_cell)V4_NODE_WORDS, (v4_cell)V4_NODE_WORDS + 1, MINC, MAXC, (v4_cell)(V4_NODE_WORDS * 4u) };
static const struct { unsigned op; int reg; const char *name; } m[] = {
{ V4_OP_FETCH_A, 'a', "@" }, { V4_OP_FETCH_INC, 'a', "@+" }, { V4_OP_FETCH_B, 'b', "@b" },
{ V4_OP_STORE_A, 'a', "!" }, { V4_OP_STORE_INC, 'a', "!+" }, { V4_OP_STORE_B, 'b', "!b" },
};
unsigned i, k;
for (i = 0; i < sizeof bad / sizeof bad[0]; i++)
for (k = 0; k < sizeof m / sizeof m[0]; k++) {
unsigned retired;
fresh();
v4_node_fault_attach(&n, HANDLER);
dpush(11); dpush(22);
n.a = m[k].reg == 'a' ? bad[i] : 7;
n.b = m[k].reg == 'b' ? bad[i] : 7;
n.mem[0] = word6(V4_OP_DUP, m[k].op, V4_OP_DROP, V4_OP_DROP, NOP, NOP);
n.p = 0;
retired = v4_exec_step_word(&n, &es, &h);
CHECK(n.faults == 1 && n.fault_addr == bad[i] && !n.stopped, "%s at %lld is a fault", m[k].name, (long long)bad[i]);
CHECK(n.p == HANDLER, "%s: P is the handler", m[k].name);
CHECK(retired == 1, "%s: only the dup before it retired (%u)", m[k].name, retired);
CHECK(n.ds.t == 22 && n.ds.s == 22, "%s: it neither pushed nor popped, and the drops after it did not run", m[k].name);
CHECK(n.a == (m[k].reg == 'a' ? bad[i] : 7) && n.b == (m[k].reg == 'b' ? bad[i] : 7), "%s: A and B untouched", m[k].name);
CHECK(v4_node_guards_intact(&n), "%s: guards intact", m[k].name);
}
/* the last word of memory is not a fault, and the first is not */
fresh(); v4_node_fault_attach(&n, HANDLER);
n.mem[V4_NODE_WORDS - 1u] = 77; n.a = (v4_cell)(V4_NODE_WORDS - 1u); run1(V4_OP_FETCH_A);
CHECK(n.faults == 0 && n.ds.t == 77, "the last word of memory can be fetched");
fresh(); v4_node_fault_attach(&n, HANDLER);
dpush(5); n.b = (v4_cell)(V4_NODE_WORDS - 1u); run1(V4_OP_STORE_B);
CHECK(n.faults == 0 && n.mem[V4_NODE_WORDS - 1u] == 5, "and stored");
/* P outside memory: nothing is fetched or executed */
for (i = 0; i < sizeof bad / sizeof bad[0]; i++) {
fresh(); v4_node_fault_attach(&n, HANDLER);
dpush(1);
n.p = bad[i];
CHECK(v4_exec_step_word(&n, &es, &h) == 0 && n.faults == 1 && n.fault_addr == bad[i] && n.p == HANDLER && n.ds.t == 1,
"P at %lld is a fault", (long long)bad[i]);
}
/* @p in the last word of memory: its literal would be past the end */
fresh(); v4_node_fault_attach(&n, HANDLER);
dpush(1);
n.mem[V4_NODE_WORDS - 1u] = word6(V4_OP_FETCH_P, NOP, NOP, NOP, NOP, NOP);
n.p = (v4_cell)(V4_NODE_WORDS - 1u);
CHECK(v4_exec_step_word(&n, &es, &h) == 0 && n.faults == 1 && n.fault_addr == (v4_cell)V4_NODE_WORDS && n.p == HANDLER && n.ds.t == 1,
"a literal past the end of memory is a fault");
fresh(); v4_node_fault_attach(&n, HANDLER);
dpush(1);
n.mem[V4_NODE_WORDS - 1u] = word6(V4_OP_STORE_P, NOP, NOP, NOP, NOP, NOP);
n.p = (v4_cell)(V4_NODE_WORDS - 1u);
CHECK(v4_exec_step_word(&n, &es, &h) == 0 && n.faults == 1 && n.p == HANDLER && n.ds.t == 1, "and so is !p there");
/* a jump out of memory faults at the next fetch */
fresh(); v4_node_fault_attach(&n, HANDLER);
rpush(-5);
n.mem[0] = word6(V4_OP_SEMI, NOP, NOP, NOP, NOP, NOP);
n.p = 0;
CHECK(v4_exec_step_word(&n, &es, &h) == 1 && n.p == -5 && n.faults == 0, "; to an address outside memory is not yet a fault");
CHECK(v4_exec_step_word(&n, &es, &h) == 0 && n.p == HANDLER && n.faults == 1 && n.fault_addr == -5, "the fetch there is");
/* the handler runs, and faults are counted */
fresh(); v4_node_fault_attach(&n, HANDLER);
n.mem[HANDLER] = word6(V4_OP_DUP, V4_OP_ADD, NOP, NOP, NOP, NOP);
dpush(21); n.a = -1; run1(V4_OP_FETCH_A);
CHECK(v4_exec_step_word(&n, &es, &h) == 6 && n.ds.t == 42, "the handler's code runs next");
n.a = -1; run1(V4_OP_FETCH_A); n.a = -1; run1(V4_OP_STORE_A);
CHECK(n.faults == 3, "each fault is counted");
v4_node_fault_attach(&n, HANDLER);
CHECK(n.faults == 0 && n.fault_vector == HANDLER, "attaching clears the count");
/* with no handler the node stops */
fresh();
CHECK(n.fault_vector == -1 && n.faults == 0 && !n.stopped, "a fresh node has no handler and has not stopped");
dpush(9); n.a = -1;
CHECK(run1(V4_OP_FETCH_A) == 0 && n.stopped && n.faults == 1 && n.ds.t == 9, "with no handler a fault stops the node");
n.mem[0] = word6(V4_OP_DUP, V4_OP_ADD, NOP, NOP, NOP, NOP); n.p = 0;
CHECK(v4_exec_step_word(&n, &es, &h) == 0 && n.p == 0 && n.ds.t == 9, "a stopped node executes nothing");
v4_node_fault_attach(&n, HANDLER);
CHECK(!n.stopped && v4_exec_step_word(&n, &es, &h) == 6 && n.ds.t == 18, "attaching a handler lets it run again");
v4_node_fault_attach(&n, (v4_cell)V4_NODE_WORDS);
CHECK(n.fault_vector == -1, "a handler address outside memory detaches");
v4_node_reset(&n);
CHECK(n.fault_vector == -1 && !n.stopped && n.faults == 0, "reset detaches the handler");
/* the C accessors never index outside memory */
fresh();
for (i = 0; i < sizeof bad / sizeof bad[0]; i++) {
v4_node_store(&n, bad[i], 123);
CHECK(v4_node_load(&n, bad[i]) == 0 && v4_node_fetch(&n, bad[i]) == 0 && !v4_node_addr_ok(bad[i]),
"load, fetch and store at %lld touch nothing", (long long)bad[i]);
}
CHECK(n.faults == 0 && v4_node_guards_intact(&n) && v4_node_addr_ok(0) && v4_node_addr_ok((v4_cell)(V4_NODE_WORDS - 1u)),
"and they are not faults: only a running programme faults");
}
int main(void)
{
printf("v4 exec tests: V4_CELL_BITS=%d\n", V4_CELL_BITS);
@@ -332,6 +436,7 @@ int main(void)
#if V4_CELL_BITS == 64
test_iword_in_wide_cell();
#endif
test_address_faults();
test_soak();
printf(" %d checks, %d failures\n", checks, failures);