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();
+37 -9
View File
@@ -18,7 +18,9 @@
* v3 goes on compiling the lines that follow into it.
* - ABORT" at the prompt stops the line when its flag is true. v3 prints
* the text and carries on. Compiled into a word, v3's crashes (SIGSEGV).
* - control words that do not match give ERROR alone; v3 names the word.
* - every error has a message (D-18), but v4's says what was wrong, not
* which word: "Control structure mismatch" where v3 says
* "LOOP: missing DO".
* - QUERY takes 80 characters (FORTH-79); v3's line is 255.
* - an address outside memory says so (D-14); v3 gives ERROR alone.
* - M/MOD by zero says so (D-15); v3 gives ERROR alone.
@@ -82,6 +84,7 @@ static void boot_with(unsigned depth)
v4_node_console_attach(&n, CONSOLE_TX);
v4_node_console_input_attach(&n, CONSOLE_RX, CONSOLE_ST);
v4_node_fault_attach(&n, w_fault);
v4_node_error_attach(&n, NODE_ERROR);
v4_dstack_reset(&n.ds);
v4_rstack_reset(&n.rs);
v4_exec_reset(&es);
@@ -223,14 +226,28 @@ static const transcript script[] = {
{ ": Z3 5 0 DO 9 3 I - / DROP LOOP 65 EMIT ; Z3\n66 EMIT\n", "/: Division by zero\n ERROR\nok> B ok\nok> ", 0 },
{ ": BAD 1 NOSUCH 2 ;\nBAD\n65 EMIT\n", "UNKNOWN WORD: 'NOSUCH'\n ERROR\nok> UNKNOWN WORD: 'BAD'\n ERROR\nok> A ok\nok> ", 0 },
{ "1 ABORT\" now\" 65 EMIT\n66 EMIT\n", "now\n ok\nok> B ok\nok> ", 0 },
{ ": B1 IF LOOP ;\n65 EMIT\n", " ERROR\nok> A ok\nok> ", 0 },
{ ": B1 IF LOOP ;\n65 EMIT\n", "Control structure mismatch\n ERROR\nok> A ok\nok> ", 0 },
{ ": A2 0 ABORT\" boom\" 67 EMIT ; A2\n", "C ok\nok> ", 0 },
{ ": A1 ABORT\" boom\" 65 EMIT ; 1 A1 66 EMIT\n0 A1\n", "boom\n ok\nok> A ok\nok> ", 0 },
{ "1 2 3 QUIT 68 EMIT\n+ + 48 + EMIT\n", "\nok> 6 ok\nok> ", 0 },
{ ": Q1 QUIT ; : Q2 65 EMIT Q1 66 EMIT ; : Q3 Q2 67 EMIT ; Q3 68 EMIT\n69 EMIT\n", "A\nok> E ok\nok> ", 0 },
{ ": HALF 1 2 [ QUIT\n65 EMIT\nHALF\n", "\nok> A ok\nok> UNKNOWN WORD: 'HALF'\n ERROR\nok> ", 0 },
{ ": HALF 1 2 [ ABORT\n65 EMIT\n", " ok\nok> A ok\nok> ", 0 },
{ "PAD PAD -1 CMOVE 65 EMIT\n66 EMIT\n", "A ERROR\nok> B ok\nok> ", 0 },
/* every error stops the line at once, says what it was, and leaves the data stack (D-18) */
{ "1 2 3 PAD PAD -1 CMOVE 65 EMIT\n66 EMIT .S\n", "Negative count\n ERROR\nok> B<3> 1 2 3 \n ok\nok> ", 0 },
{ "7 PAD -5 TYPE 65 EMIT\n.S\n", "Negative count\n ERROR\nok> <1> 7 \n ok\nok> ", 0 },
/* NUMBER takes a counted string: "-12" and "-1x", built at PAD */
{ "3 PAD C! 45 PAD 1+ C! 49 PAD 2+ C! 50 PAD 3 + C! 0 PAD 4 + C! PAD NUMBER D.\n", "-12 ok\nok> ", 0 },
{ "3 PAD C! 45 PAD 1+ C! 49 PAD 2+ C! 120 PAD 3 + C! 7 PAD NUMBER 65 EMIT\n.S\n", "Not a number\n ERROR\nok> <3> 7 0 0 \n ok\nok> ", 0 },
{ "7 <# 300 HOLD 65 EMIT\n.S\n", "Not a character\n ERROR\nok> <1> 7 \n ok\nok> ", 0 },
{ ": HH <# 70 0 DO 65 HOLD LOOP ; HH 66 EMIT\n67 EMIT\n", "Number too long\n ERROR\nok> C ok\nok> ", 0 },
{ "7 : \n.S\n", "Name missing\n ERROR\nok> <1> 7 \n ok\nok> ", 0 },
{ "VARIABLE\nCREATE\n5 CONSTANT\n", "Name missing\n ERROR\nok> Name missing\n ERROR\nok> Name missing\n ERROR\nok> ", 0 },
{ "7 ' NOSUCH 65 EMIT\n.S\n", "UNKNOWN WORD: 'NOSUCH'\n ERROR\nok> <1> 7 \n ok\nok> ", 0 },
{ ": X1 COMPILE NOSUCH ;\n: X2 [COMPILE] NOPE ;\n65 EMIT\n", "UNKNOWN WORD: 'NOSUCH'\n ERROR\nok> UNKNOWN WORD: 'NOPE'\n ERROR\nok> A ok\nok> ", 0 },
{ ": X3 THEN ;\n: X4 BEGIN IF UNTIL ;\n: X5 DO REPEAT ;\n", "Control structure mismatch\n ERROR\nok> Control structure mismatch\n ERROR\nok> "
"Control structure mismatch\n ERROR\nok> ", 0 },
{ ": X6 IF IF IF IF IF IF IF IF IF IF IF IF IF IF IF IF IF 65 EMIT ;\n66 EMIT\n", "Control structures too deep\n ERROR\nok> B ok\nok> ", 0 },
{ ".\" no closing quote\n65 EMIT\n", "no closing quote ok\nok> A ok\nok> ", 0 },
{ ".\" \"\n", " ok\nok> ", 0 },
{ ": T ABORT\" \" ; 1 T\n", "\n ok\nok> ", 0 },
@@ -303,8 +320,8 @@ int main(void)
CHECK(is(say("65 EMIT\n"), "A ok\nok> ") && is(say("H1\n"), "UNKNOWN WORD: 'H1'\n ERROR\nok> "), "and the definition is gone");
CHECK(is(say(": H2 1 IF IA\n"), " ok\nok> ") && n.mem[STATE] == 0 && n.mem[CFP] == CFS_W, "ABORT while compiling stops compiling");
CHECK(is(say("66 EMIT\n"), "B ok\nok> ") && is(say("H2\n"), "UNKNOWN WORD: 'H2'\n ERROR\nok> "), "and the definition is gone");
CHECK(is(say(": H3 1 IF [ PAD PAD -1 CMOVE ]\n"), " ERROR\nok> ") && n.mem[STATE] == 0 && n.mem[CFP] == CFS_W,
"a word that sets NODE-ERROR while a definition is open ends it");
CHECK(is(say(": H3 1 IF [ PAD PAD -1 CMOVE ]\n"), "Negative count\n ERROR\nok> ") && n.mem[STATE] == 0 && n.mem[CFP] == CFS_W,
"a word that raises an error while a definition is open ends it");
CHECK(is(say("67 EMIT\n"), "C ok\nok> ") && is(say("H3\n"), "UNKNOWN WORD: 'H3'\n ERROR\nok> "), "and the definition is gone");
CHECK(is(say(": H4 68 EMIT ; H4\n"), "D ok\nok> "), "the next definition compiles and runs");
@@ -527,8 +544,9 @@ int main(void)
CHECK(is(say("-1 U.\n"), "18446744073709551615 ok\nok> "), "the largest number (as v3)");
CHECK(is(say("-9223372036854775808 . 9223372036854775807 .\n"), "-9223372036854775808 9223372036854775807 ok\nok> "), "the smallest and largest signed");
CHECK(is(say("HEX -1 U. DECIMAL\n"), "FFFFFFFFFFFFFFFF ok\nok> "), "in hex");
/* 64 binary digits are one more than the hold buffer's 63 (D-13): the last is dropped and the line is an error */
CHECK(is(say("2 BASE ! -1 U. DECIMAL\n"), "111111111111111111111111111111111111111111111111111111111111111 ERROR\nok> "), "in binary, 63 of its 64 digits and ERROR");
/* 64 binary digits are one more than the hold buffer's 63 (D-13): an error, and nothing is printed */
CHECK(is(say("2 BASE ! -1 U. DECIMAL\n"), "Number too long\n ERROR\nok> "), "in binary, its 64 digits are one too many");
CHECK(is(say("DECIMAL\n"), " ok\nok> "), "(back to decimal)");
CHECK(is(say("9223372036854775807 2 BASE ! U. DECIMAL\n"), "111111111111111111111111111111111111111111111111111111111111111 ok\nok> "), "a 63-digit number prints whole");
#endif
/* .S works on the stack it is printing, so it needs some of it free */
@@ -553,9 +571,19 @@ int main(void)
CHECK(most + 8u >= V4_DATA_DEPTH, ".S works with all but eight cells of the stack in use");
}
/* ---- the fault table: five words, each a jump to its handler ---- */
/* ---- a full dictionary ---- */
boot_bare();
n.mem[DP] = (DICT_END_W - 2) * 4;
CHECK(is(say(": FULL 1 2 3 4 5 6 7 8 9 ; 65 EMIT\n"), "Dictionary full\n ERROR\nok> ") && n.mem[STATE] == 0, "a definition that does not fit");
CHECK(is(say("FULL\n"), "UNKNOWN WORD: 'FULL'\n ERROR\nok> "), "is not defined");
n.mem[DP] = (DICT_END_W - 1) * 4;
CHECK(is(say("1 , 65 EMIT\n"), "A ok\nok> ") && is(say("2 , 66 EMIT\n"), "Dictionary full\n ERROR\nok> "), "the last cell can be used; the one after it cannot");
CHECK(is(say("5 ALLOT 67 EMIT\n"), "Dictionary full\n ERROR\nok> ") && is(say("-99999 ALLOT\n"), "Dictionary full\n ERROR\nok> "), "nor can ALLOT go past either end");
CHECK(is(say("68 EMIT .S\n"), "D<0> \n ok\nok> "), "and the prompt goes on");
/* ---- the fault table: six words, each a jump to its handler ---- */
{
static const char *const handler[] = { "(FAULT)", "(D-OVER)", "(D-UNDER)", "(R-OVER)", "(R-UNDER)" };
static const char *const handler[] = { "(FAULT)", "(D-OVER)", "(D-UNDER)", "(R-OVER)", "(R-UNDER)", "(RAISED)" };
for (i = 0; i < V4_FAULT_KINDS; i++) {
v4_iword w = (v4_iword)((v4_ucell)n.mem[w_fault + (v4_cell)i] & 0xFFFFFFFFu);
CHECK(v4_iword_op(w, 0) == V4_OP_JUMP && v4_iword_branch(w_fault + (v4_cell)i + 1, w, 0) == v4_text_word(&tx, handler[i]),