feat(v4.0.0): the stacks are guarded (D-16); DEPTH, PICK and ROLL

Ruled 2026-10-04, revising D-2: stack overflow and underflow are errors
that are shown and return to the prompt, not silent wrap-around.

- Each stack counts what it holds.  Before every opcode the executor
  checks that the stacks hold what it takes and have room for what it
  leaves; otherwise the opcode does nothing and the node faults, as for a
  bad address, to that kind's handler.  Every fault empties both stacks.
- The fault handler is now a table of five jumps: address, data overflow,
  data underflow, return overflow, return underflow.  The host node says
  "Stack overflow", "Stack underflow", "Return stack overflow",
  "Return stack underflow", then ERROR and the prompt.
- Two registers, DSTACK-DEPTH and RSTACK-DEPTH: a fetch reads the depth,
  a store empties the stack.  QUIT, ABORT and the error exits empty the
  return stack before they call anything; ABORT empties the data stack.
- capsule/forth.v4: DEPTH, PICK and ROLL, to FORTH-79 (counting from
  one).  PICK and ROLL set the values above the one wanted aside in
  memory, and work with the stack full.
- Division by zero now takes its operands off the stack, as v3 does.
- A colon with no room for its entry abandons the line.
- tests: every opcode at every depth of both stacks; the faults, the
  registers and the three words from the prompt.

The sizes are unchanged: ten values, nine return entries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rajames
2026-10-04 17:31:18 -04:00
co-authored by Claude Opus 5.5
parent 361dcd1148
commit 0da7e32a0b
16 changed files with 542 additions and 106 deletions
+44 -1
View File
@@ -39,10 +39,51 @@ static int ends_word(unsigned op)
static int faulted(v4_node *n, v4_cell addr)
{
if (v4_node_addr_ok(addr)) return 0;
v4_node_fault(n, addr);
v4_node_fault(n, V4_FAULT_ADDRESS, addr);
return 1;
}
/* D-16: what each opcode takes from the stacks and leaves on them. If the
* data stack holds fewer than `dneed`, or the return stack fewer than
* `rneed`, or a stack the opcode leaves one more on is full, it is a fault
* and the opcode does nothing. An opcode that only looks at T (`if`) or at
* T and S (`+*`) needs them to be there. */
static int stack_faulted(v4_node *n, unsigned op)
{
unsigned dneed = 0, rneed = 0, dgrow = 0, rgrow = 0;
switch (op) {
case V4_OP_SEMI: case V4_OP_EX: case V4_OP_UNEXT: case V4_OP_NEXT:
rneed = 1; break;
case V4_OP_CALL:
rgrow = 1; break;
case V4_OP_FETCH_P: case V4_OP_FETCH_INC: case V4_OP_FETCH_B: case V4_OP_FETCH_A: case V4_OP_PUSH_A:
dgrow = 1; break;
case V4_OP_IF: case V4_OP_MINUS_IF:
case V4_OP_STORE_P: case V4_OP_STORE_INC: case V4_OP_STORE_B: case V4_OP_STORE_A:
case V4_OP_TWO_STAR: case V4_OP_TWO_SLASH: case V4_OP_INV: case V4_OP_DROP:
case V4_OP_BANG_B: case V4_OP_BANG_A:
dneed = 1; break;
case V4_OP_MUL_STEP: case V4_OP_ADD: case V4_OP_AND: case V4_OP_XOR:
dneed = 2; break;
case V4_OP_DUP:
dneed = 1; dgrow = 1; break;
case V4_OP_OVER:
dneed = 2; dgrow = 1; break;
case V4_OP_RPOP:
rneed = 1; dgrow = 1; break;
case V4_OP_PUSH:
dneed = 1; rgrow = 1; break;
default:
break; /* jump, nop */
}
if (n->ds.depth < dneed) { v4_node_fault(n, V4_FAULT_DATA_UNDER, 0); return 1; }
if (n->rs.depth < rneed) { v4_node_fault(n, V4_FAULT_RET_UNDER, 0); return 1; }
if (dgrow && n->ds.depth >= V4_DATA_DEPTH) { v4_node_fault(n, V4_FAULT_DATA_OVER, 0); return 1; }
if (rgrow && n->rs.depth >= V4_RET_DEPTH) { v4_node_fault(n, V4_FAULT_RET_OVER, 0); return 1; }
return 0;
}
void v4_exec_reset(v4_exec_state *es)
{
es->anticlock = 0;
@@ -55,6 +96,8 @@ void v4_exec_op(v4_node *n, v4_exec_state *es, v4_heat *h,
v4_rstack *rs = &n->rs;
v4_cell x;
if (op <= V4_OP_BANG_A && stack_faulted(n, op)) return;
switch (op) {
case V4_OP_SEMI:
n->p = v4_rstack_pop(rs);