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:
co-authored by
Claude Opus 5.5
parent
361dcd1148
commit
0da7e32a0b
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user