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
+7 -4
View File
@@ -45,10 +45,13 @@ void v4_exec_reset(v4_exec_state *es);
* so the call does not return while R is nonzero at a `unext`. Returns the
* number of instructions retired.
*
* An address outside node memory -- P at the fetch, or the address an opcode
* uses -- is a fault (node.h, D-14): that opcode and the rest of the word do
* not execute, and P is the node's fault handler on return. With no handler
* the node stops, and this does nothing and returns 0 from then on. */
* FAULTS (node.h; D-14 and D-16). Before an opcode runs, the stacks must
* hold what it takes -- a T or S it only reads counts -- and have room for
* what it leaves; and an address it uses, or P at the fetch, must be inside
* node memory. If not, that opcode and the rest of the word do not execute,
* both stacks are emptied, and P is that kind's word of the node's fault
* table on return. With no table the node stops, and this does nothing and
* returns 0 from then on. */
unsigned v4_exec_step_word(v4_node *n, v4_exec_state *es, v4_heat *h);
/* Execute a single opcode. The branch opcodes read their target from `w` and
+58 -26
View File
@@ -81,11 +81,16 @@ typedef struct {
unsigned input_pos; /* characters taken; input_len - input_pos are pending */
unsigned char input[V4_CONSOLE_CAP];
/* Address faults (D-14). See v4_node_fault_attach below. */
v4_cell fault_vector; /* word address of the handler, or -1: none */
v4_cell fault_addr; /* the address of the latest fault */
/* Faults (D-14, D-16). See v4_node_fault_attach below. */
v4_cell fault_vector; /* word address of the handler table, or -1: none */
v4_cell fault_addr; /* the address of the latest address fault */
unsigned fault_kind; /* which kind the latest fault was (V4_FAULT_*) */
unsigned faults; /* how many there have been */
int stopped; /* non-zero: faulted with no handler */
/* DSTACK-DEPTH and RSTACK-DEPTH (D-16). See v4_node_stack_regs_attach. */
v4_cell dstack_reg; /* its word address, or -1 */
v4_cell rstack_reg; /* its word address, or -1 */
} v4_node;
/* Zero P, A and B, empty the stacks, and clear memory. Installs every
@@ -173,31 +178,58 @@ v4_cell v4_node_fetch(v4_node *n, v4_cell addr);
/* 1 if `addr` is a word of node memory. */
int v4_node_addr_ok(v4_cell addr);
/* ADDRESS FAULTS (DECOMPOSITION.md D-14, ruled 2026-10-04: an address outside
* the node's memory is guarded, an error is shown, and the node returns to
* its prompt).
/* FAULTS (DECOMPOSITION.md D-14 and D-16, ruled 2026-10-04: an address
* outside the node's memory, and a stack pushed when full or popped when
* empty, are guarded; an error is shown; the node returns to its prompt).
*
* The executor checks every address a programme uses before using it: P when
* an instruction word or an `@p` literal is fetched or `!p` stores, A for
* `@ @+ ! !+`, B for `@b !b`. If it is outside memory there is a fault:
* - the opcode does nothing: no fetch, no store, the stacks and A and B
* untouched, nothing retired; the rest of its instruction word is not
* executed;
* - fault_addr is the address and faults is one more;
* - P becomes fault_vector, the address of the node's handler. Nothing is
* pushed: the handler does not return to the programme, it reports and
* goes back to the prompt (capsule/quit.v4, (FAULT)).
* With no handler attached (fault_vector -1) the node stops instead:
* `stopped` is set and v4_exec_step_word does nothing more until the node is
* reset or a handler is attached.
* The executor checks before every opcode (exec.h):
* - that the stacks hold what the opcode takes and have room for what it
* leaves;
* - every address a programme uses: P when an instruction word or an `@p`
* literal is fetched or `!p` stores, A for `@ @+ ! !+`, B for `@b !b`.
* If a check fails there is a fault:
* - the opcode does nothing, and the rest of its instruction word is not
* executed; A and B are untouched;
* - fault_kind says which, fault_addr is the address for an address fault,
* and faults is one more;
* - both stacks are emptied. The handler does not return to the
* programme, and what a word stopped in the middle of its work has left
* on the data stack is not something its caller can use;
* - P becomes fault_vector + fault_kind. The handler table is five words,
* one for each kind in the order below, each a jump to that kind's
* handler (capsule/quit.v4, (FAULTS)).
* With no table attached (fault_vector -1) the node stops instead: `stopped`
* is set and v4_exec_step_word does nothing more until the node is reset or
* a table is attached.
*
* v4_node_reset detaches the handler and clears the count. Attaching clears
* `stopped` and the count; `handler` must be a word of memory, or -1 to
* detach. */
void v4_node_fault_attach(v4_node *n, v4_cell handler);
* v4_node_reset detaches the table and clears the count. Attaching clears
* `stopped` and the count; all five words of the table must be in memory,
* or `table` -1 to detach. */
#define V4_FAULT_ADDRESS 0u /* an address outside memory (D-14) */
#define V4_FAULT_DATA_OVER 1u /* a push onto a full data stack */
#define V4_FAULT_DATA_UNDER 2u /* the data stack holds too little for the opcode */
#define V4_FAULT_RET_OVER 3u /* a push onto a full return stack */
#define V4_FAULT_RET_UNDER 4u /* the return stack holds too little for the opcode */
#define V4_FAULT_KINDS 5u
/* For the executor: record a fault at `addr` and redirect or stop the node,
* as above. */
void v4_node_fault(v4_node *n, v4_cell addr);
void v4_node_fault_attach(v4_node *n, v4_cell table);
/* For the executor: record a fault of `kind` (at `addr`, for an address
* fault) and redirect or stop the node, as above. */
void v4_node_fault(v4_node *n, unsigned kind, v4_cell addr);
/* DSTACK-DEPTH and RSTACK-DEPTH, the stack registers (D-16).
*
* After v4_node_stack_regs_attach(n, d, r):
* a data fetch from `d` gives how many values the data stack holds (the
* fetch's own push not counted), and from `r` how many entries the return
* stack holds;
* a store to `d` empties the data stack (the value stored is taken off
* first and ignored), and a store to `r` empties the return stack.
* Neither reads or writes n->mem. This is all a programme can see of the
* stacks: there is still no stack pointer and no address for a stack cell.
* DEPTH reads the first; QUIT and ABORT store to them. v4_node_reset
* detaches both (-1); the addresses are the caller's choice (D-4). */
void v4_node_stack_regs_attach(v4_node *n, v4_cell d, v4_cell r);
#endif /* V4_NODE_H */
+23 -1
View File
@@ -1,4 +1,11 @@
/* stack.h -- F18 circular hardware stacks (DECOMPOSITION.md D-2).
/* stack.h -- F18 circular hardware stacks (DECOMPOSITION.md D-2), counted (D-16).
*
* REVISED BY D-16 (2026-10-04). What follows describes the storage, which is
* unchanged: registers over a ring that wraps. But a running programme no
* longer sees the wrap. Each stack also counts what it holds, and the
* executor faults before an opcode would push onto a full stack or pop an
* empty one (exec.h). The wrap is now only what push and pop do when C code
* calls them directly at either end.
*
* D-2 rules these explicitly: "F18 circular stacks, hidden. Data stack 10
* deep (T, S + 8 circular), return stack 9 deep (R + 8 circular), exactly as
@@ -51,6 +58,7 @@ typedef struct {
v4_cell t; /* T -- top of data stack (a register) */
v4_cell s; /* S -- second (a register) */
unsigned head;
unsigned depth; /* how many values it holds, 0 .. V4_DATA_DEPTH (D-16) */
v4_cell guard_head[V4_DATA_BOUND];
v4_cell ring[V4_DATA_RING];
@@ -60,6 +68,7 @@ typedef struct {
typedef struct {
v4_cell r; /* R -- top of return stack (a register) */
unsigned head;
unsigned depth; /* how many entries it holds, 0 .. V4_RET_DEPTH (D-16) */
v4_cell guard_head[V4_RET_BOUND];
v4_cell ring[V4_RET_RING];
@@ -83,6 +92,19 @@ v4_cell v4_rstack_peek(const v4_rstack *st);
/* 1 if both boundaries of the data stack are intact, else 0. See guard.h for
* what this catches that AddressSanitizer does not. */
/* THE DEPTH COUNT (DECOMPOSITION.md D-16, ruled 2026-10-04). Each stack
* counts what it holds: a push adds one, up to the stack's size, and a pop
* takes one, down to zero. The storage is still the F18's -- registers over
* a ring -- and push and pop still do exactly what they did when the count
* is at either end, so these functions never fail. It is the executor that
* guards: it reads the count before every opcode and faults instead of
* pushing onto a full stack or popping an empty one (exec.h).
*
* v4_dstack_clear and v4_rstack_clear empty a stack: the count goes to zero
* and nothing else changes. */
void v4_dstack_clear(v4_dstack *st);
void v4_rstack_clear(v4_rstack *st);
int v4_dstack_guards_intact(const v4_dstack *st);
int v4_rstack_guards_intact(const v4_rstack *st);
+2 -1
View File
@@ -12,7 +12,8 @@
* and step instruction words until the word returns. The stacks and A, B are
* left as the caller set them, so arguments go on the data stack first.
* Returns the number of instruction words stepped, or -1 if the word had not
* returned after `max_words` or the node stopped on an address fault (D-14). */
* returned after `max_words` or the node stopped on a fault (D-14, D-16). A
* stop left by an earlier call is cleared first. */
long v4_test_call(v4_node *n, v4_exec_state *es, v4_heat *h,
v4_cell entry, long max_words);