feat(v4.0.0): every error is raised and ends the line (D-18)

Ruled 2026-10-04: guard all errors.  The errors that set NODE-ERROR and
let the line run on now stop it at once, with a message.

- A store of a non-zero code to NODE-ERROR is a trap, a sixth kind of
  fault: nothing after it executes, the return stack is emptied, and the
  data stack is left as the word left it.  Not attached, NODE-ERROR is
  plain memory, as the tests below the prompt use it.
- The capsule's words store a code where they stored -1, and the prompt's
  (RAISED) prints its message: Negative count, Not a number, Number too
  long, Not a character, Dictionary full, Name missing, Control structure
  mismatch, Control structures too deep.
- ' and COMPILE and [COMPILE] of a word that is not there say
  UNKNOWN WORD: 'xxx', as the interpreter does.
- tests: the trap in test_exec.c; every message from the prompt, with the
  rest of the line not run and the stack kept, in test_host_quit.c.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rajames
2026-10-04 20:13:23 -04:00
co-authored by Claude Opus 5.5
parent 644bfc0a25
commit dc7e37ba5c
12 changed files with 181 additions and 38 deletions
+43
View File
@@ -421,6 +421,48 @@ static void test_stack_faults(void)
CHECK(n.ds.t == 55, "detached, the address is memory");
}
/* D-18: NODE-ERROR as a trap. A store of a non-zero code is a fault of its
* own kind: the code is written, the return stack is emptied, the data stack
* is left, and P is the sixth word of the table. */
static void test_raised_errors(void)
{
static const unsigned store[] = { V4_OP_STORE_A, V4_OP_STORE_B, V4_OP_STORE_INC };
unsigned k;
for (k = 0; k < 3; k++) {
fresh(); v4_node_fault_attach(&n, HANDLER);
v4_node_error_attach(&n, 700);
dpush(11); dpush(22); dpush(5); rpush(1); rpush(2);
n.a = 700; n.b = 700;
n.mem[0] = word6(store[k], V4_OP_DROP, V4_OP_DROP, NOP, NOP, NOP); n.p = 0;
CHECK(v4_exec_step_word(&n, &es, &h) == 1, "the store retires and nothing after it runs");
CHECK(n.faults == 1 && n.fault_kind == V4_FAULT_RAISED && n.fault_addr == 5 && n.p == HANDLER + (v4_cell)V4_FAULT_RAISED,
"a non-zero store to NODE-ERROR raises: kind %u, code %lld", n.fault_kind, (long long)n.fault_addr);
CHECK(n.mem[700] == 5, "the code is in NODE-ERROR");
CHECK(n.rs.depth == 0 && n.ds.depth == 2 && n.ds.t == 22 && n.ds.s == 11, "the return stack is emptied and the data stack left");
/* zero clears it and is no fault */
dpush(0); n.a = 700; v4_exec_op(&n, &es, &h, V4_OP_STORE_A, 0, 0);
CHECK(n.faults == 1 && n.mem[700] == 0, "a store of zero is an ordinary store");
dpush(-1); n.a = 699; v4_exec_op(&n, &es, &h, V4_OP_STORE_A, 0, 0);
CHECK(n.faults == 1 && n.mem[699] == -1, "and so is a store next to it");
}
/* detached: ordinary memory */
fresh(); v4_node_fault_attach(&n, HANDLER);
CHECK(n.error_reg == -1, "a fresh node has no error trap");
dpush(7); n.a = 700; v4_exec_op(&n, &es, &h, V4_OP_STORE_A, 0, 0);
CHECK(n.faults == 0 && n.mem[700] == 7, "not attached, NODE-ERROR is memory and the word goes on");
/* with no table the node stops */
fresh(); v4_node_error_attach(&n, 700);
dpush(7); n.a = 700;
CHECK(run1(V4_OP_STORE_A) == 1 && n.stopped && n.mem[700] == 7, "with no handler a raised error stops the node");
v4_node_reset(&n);
CHECK(n.error_reg == -1, "reset detaches the trap");
/* the table must be six words inside memory */
v4_node_fault_attach(&n, (v4_cell)(V4_NODE_WORDS - V4_FAULT_KINDS));
CHECK(n.fault_vector == (v4_cell)(V4_NODE_WORDS - V4_FAULT_KINDS), "a table that ends with memory is accepted");
v4_node_fault_attach(&n, (v4_cell)(V4_NODE_WORDS - V4_FAULT_KINDS + 1u));
CHECK(n.fault_vector == -1, "one that runs past it is not");
}
/* 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. */
@@ -542,6 +584,7 @@ int main(void)
test_iword_in_wide_cell();
#endif
test_stack_faults();
test_raised_errors();
test_address_faults();
test_soak();