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
+3 -3
View File
@@ -17,8 +17,8 @@
\ compile-only it may not be executed by the interpreter.
\ An immediate word is executed even when compiling.
\
\ THE STACKS ARE SHORT (D-2): ten cells of data, nine return entries, and no
\ way to see how full they are. The interpreter and compiler run on the same
\ THE STACKS ARE SHORT (D-2): ten cells of data, nine return entries, and one
\ more of either is a fault (D-16). The interpreter and compiler run on the same
\ stacks as the user's words, with the user's values beneath them, so:
\ - every word here keeps what it works on in (C), and has at most three
\ cells of its own on the data stack at any moment;
@@ -184,7 +184,7 @@ header :
32 WORD (HEADER)
LATEST (C)+2 a! @ xor if NONE drop
HIDDEN (CG-RESET) CFBASE (CFP) a! ! jump RIGHT-BRACKET
NONE: drop ;
NONE: drop jump (ABANDON) \ no entry: the rest of the line is not a definition
\ FORTH-79: compile the return, let the word be found, stop compiling
header ; immediate compile-only
+75 -10
View File
@@ -5,7 +5,7 @@
\ A word flagged inline is copied into a definition in place of a call; the
\ ones that touch the return stack are compile-only as well. Rests on core.v4,
\ compile.v4's loader constants, and quit.v4, which the division words leave
\ through when the divisor is zero. (F) is three cells of scratch for
\ through when the divisor is zero. (F) is seven cells of scratch for
\ this file.
\ ---- stack ----
@@ -19,6 +19,68 @@ header ?DUP : ?DUP if Z dup ; Z: ;
header 2DUP inline : 2DUP over over ;
header 2DROP inline : 2DROP drop drop ;
\ ---- the stack as a whole (D-16) ----
\ DEPTH reads the stack register. PICK and ROLL reach under the top of a
\ stack that has no addresses: the values above the one wanted are set aside
\ in (S), and put back. (F)+3 how many are still to set aside, (F)+4 where
\ the next goes, (F)+5 the value wanted, (F)+6 how many are set aside. With nine values and n on the
\ stack it is full, so until a value has been set aside nothing here pushes
\ anything while n is still there, and never more than one cell after.
\ FORTH-79: how many values were on the stack before DEPTH
header DEPTH
: DEPTH DSTACK-DEPTH b! @b ;
\ ( name -- ) "xxxx: Invalid index", and the line ends with ERROR
: (INDEX)
R-CLEAR (EMIT4) $6E49203A (EMIT4) $696C6176 (EMIT4) $6E692064 (EMIT4) $786564 (EMIT4) CR
2 jump (REPL)
\ ( x1 .. xn-1 -- ) A: n, which is 2 or more. Set the n-1 values above
\ the n-th aside. If there are not that many, the pop that finds the stack
\ empty is a fault: "Stack underflow". The count is taken down only after a
\ value has been set aside, when there is room to do the arithmetic.
: (DIG)
(F)+3 b! a !b
(F)+4 b! (S) !b
(F)+6 b! 0 !b
L: (F)+4 b! @b a! !+
a (F)+4 b! !b
(F)+6 b! @b 1 + !b
(F)+3 b! @b -1 + dup !b
-1 + if DONE drop jump L
DONE: drop ;
\ ( -- x1 .. xk ) put them back. The test that ends it pushes one cell:
\ after the last value is back the stack may be one short of full.
: (BURY)
L: (F)+6 b! @b if DONE -1 + !b
(F)+4 a! @ -1 + dup ! a! @
jump L
DONE: drop ;
\ ( n -- x ) FORTH-79: a copy of the n-th value, not counting n itself.
\ 1 PICK is DUP, 2 PICK is OVER. n less than one is an error.
header PICK
: PICK
-if POS jump BAD
POS: if BAD
a! a 2/ if ONE drop \ n is 1 when half of it is 0
(DIG) dup (F)+5 a! ! (BURY) (F)+5 a! @ ;
ONE: drop dup ;
BAD: drop $4B434950 jump (INDEX)
\ ( n -- ) FORTH-79: the n-th value, not counting n itself, is taken out
\ and put on top. 3 ROLL is ROT, 2 ROLL is SWAP, 1 ROLL does nothing. n
\ less than one is an error.
header ROLL
: ROLL
-if POS jump BAD
POS: if BAD
a! a 2/ if ONE drop
(DIG) (F)+5 a! ! (BURY) (F)+5 a! @ ;
ONE: drop ;
BAD: drop $4C4C4F52 jump (INDEX)
\ ---- return stack: in line, and only inside a definition ----
header >R inline compile-only : >R push ;
header R> inline compile-only : R> pop ;
@@ -56,11 +118,14 @@ header 2/ inline : TWO/ 2/ ;
\ does anything else, and a zero divisor prints v3's message --
\ /: Division by zero
\ -- and ends the line with ERROR, from however deep, as an address fault
\ does. Nothing is left for the word's caller: it is not returned to.
\ does. The word's operands are taken off the stack, as v3 takes them, and
\ its caller is not returned to.
\ ( name -- ) name is up to four characters of the word's name (EMIT4)
\ ( name2 name1 -- ) the word's name: its first four characters, and the
\ rest or 0 (EMIT4). The return stack is emptied before anything is called:
\ the word may have been run with it full.
: (DIV0)
(EMIT4) $6944203A (EMIT4) $69736976 (EMIT4) $62206E6F (EMIT4) \ ": Division b"
R-CLEAR (EMIT4) (EMIT4) $6944203A (EMIT4) $69736976 (EMIT4) $62206E6F (EMIT4) \ ": Division b"
$657A2079 (EMIT4) $6F72 (EMIT4) CR \ "y zero"
2 jump (REPL)
@@ -119,27 +184,27 @@ header M*
header /MOD
: /MOD ( n1 n2 -- rem quot )
if Z push S>D pop jump SM/REM
Z: $444F4D2F jump (DIV0)
Z: drop drop 0 $444F4D2F jump (DIV0)
header /
: / ( n1 n2 -- quot )
if Z push S>D pop SM/REM NIP ;
Z: $2F jump (DIV0)
Z: drop drop 0 $2F jump (DIV0)
header MOD
: MOD ( n1 n2 -- rem )
if Z push S>D pop SM/REM drop ;
Z: $444F4D jump (DIV0)
Z: drop drop 0 $444F4D jump (DIV0)
header */MOD
: */MOD ( n1 n2 n3 -- rem quot )
if Z jump (*/MOD)
Z: $4F4D2F2A (EMIT4) $44 jump (DIV0)
Z: drop drop drop $44 $4F4D2F2A jump (DIV0)
header */
: */ ( n1 n2 n3 -- quot )
if Z (*/MOD) NIP ;
Z: $2F2A jump (DIV0)
Z: drop drop drop 0 $2F2A jump (DIV0)
header M/MOD
: M/MOD ( d n -- rem quot )
if Z jump SM/REM
Z: $4F4D2F4D (EMIT4) $44 jump (DIV0)
Z: drop drop drop $44 $4F4D2F4D jump (DIV0)
\ ---- logic and comparison ----
header AND inline : AND and ;
+45 -16
View File
@@ -16,21 +16,29 @@
\ ERROR
\ The line itself is not sent back: the terminal shows what is typed.
\
\ THE RETURN STACK is circular (D-2): it has no bottom to be reset to, and
\ what is on it is simply never returned to once QUIT stops returning. So
\ QUIT and ABORT "clear" it by jumping into the loop, from however deep. The
\ data stack is circular too: ABORT has nothing to clear.
\ THE STACKS are guarded (D-16): each counts what it holds, and a push onto
\ a full one or a pop from an empty one is a fault. So QUIT and ABORT, which
\ never return to whatever ran them, must empty the return stack, and ABORT
\ the data stack too. A store to RSTACK-DEPTH or DSTACK-DEPTH does that.
\
\ Constants the loader supplies:
\ (Q) word address of two cells of scratch for this file
\ DSTACK-DEPTH RSTACK-DEPTH word addresses of the stack registers
\ ( -- ) empty the return stack. `a` is only something to store: any
\ store to the register empties the stack, and `a` needs nothing to be on
\ the data stack. One cell of the data stack is used for a moment.
macro R-CLEAR RSTACK-DEPTH b! a !b endmacro
\ ( -- ) stop compiling; forget the word being built and any open control
\ structures. A definition that was under way stays hidden.
: (RESET) 0 STATE a! ! (CG-RESET) CFBASE (CFP) a! ! ;
\ ( k -- ) the loop. It is entered with what to say first -- 0 nothing,
\ 1 " ok", anything else " ERROR" -- and never returns. Each line is
\ interpreted with one return entry under it, this word's call of INTERPRET.
\ 1 " ok", anything else " ERROR" -- and never returns. It is always jumped
\ to, never called, by something that has emptied the return stack (or by a
\ fault, which empties it). Each line is then interpreted with one return
\ entry under it, this word's call of INTERPRET.
: (REPL)
if GO -1 + if OK
drop (RESET) $52524520 (EMIT4) $524F (EMIT4) CR jump READ \ " ERROR"
@@ -48,21 +56,42 @@
\ FORTH-79: clear the return stack, set execution mode, return control to
\ the terminal; no message is given. The data stack is left as it is.
header QUIT
: QUIT (RESET) CR 0 jump (REPL)
: QUIT R-CLEAR (RESET) CR 0 jump (REPL)
\ FORTH-79: clear the data and return stacks, set execution mode, return
\ control to the terminal. As v3, the line it stops ends with " ok".
\ Both are emptied before anything is called: ABORT may be run with the
\ return stack full.
header ABORT
: ABORT (RESET) 1 jump (REPL)
: ABORT R-CLEAR DSTACK-DEPTH b! a !b (RESET) 1 jump (REPL)
\ ( -- ) where the node goes when a programme uses an address outside its
\ memory (D-14): the loader makes this word the node's fault handler. The
\ opcode that faulted did nothing; whatever was running is abandoned, as by
\ ABORT, and the line ends with ERROR.
: (FAULT)
$72646441 (EMIT4) $20737365 (EMIT4) $2074756F (EMIT4) \ "Address out "
$7220666F (EMIT4) $65676E61 (EMIT4) CR \ "of range"
\ ---- faults (D-14, D-16) -------------------------------------------------------
\ Where the node goes when a programme uses an address outside its memory,
\ pushes onto a full stack or pops an empty one. The opcode that faulted did
\ nothing, and both stacks have been emptied. Each handler says what happened;
\ whatever was running is abandoned, as by ABORT, and the line ends with
\ ERROR.
: (FAULT) \ "Address out of range"
$72646441 (EMIT4) $20737365 (EMIT4) $2074756F (EMIT4) $7220666F (EMIT4) $65676E61 (EMIT4) CR
2 jump (REPL)
: (D-OVER) \ "Stack overflow"
$63617453 (EMIT4) $766F206B (EMIT4) $6C667265 (EMIT4) $776F (EMIT4) CR
2 jump (REPL)
: (D-UNDER) \ "Stack underflow"
$63617453 (EMIT4) $6E75206B (EMIT4) $66726564 (EMIT4) $776F6C (EMIT4) CR
2 jump (REPL)
: (R-OVER) \ "Return stack overflow"
$75746552 (EMIT4) $73206E72 (EMIT4) $6B636174 (EMIT4) $65766F20 (EMIT4) $6F6C6672 (EMIT4) $77 (EMIT4) CR
2 jump (REPL)
: (R-UNDER) \ "Return stack underflow"
$75746552 (EMIT4) $73206E72 (EMIT4) $6B636174 (EMIT4) $646E7520 (EMIT4) $6C667265 (EMIT4) $776F (EMIT4) CR
2 jump (REPL)
\ The table the loader gives the node: five words, one for each kind of
\ fault in the node's order, each a jump. A jump fills its word, so they
\ are one after another.
: (FAULTS)
jump (FAULT) jump (D-OVER) jump (D-UNDER) jump (R-OVER) jump (R-UNDER)
\ ---- text in the source ---------------------------------------------------------
\ ." and ABORT" take the text up to the next " , which may be none at all
@@ -108,7 +137,7 @@ header ." immediate
\ the string.
: (ABORT")
if NO
drop pop 4* COUNT TYPE CR jump ABORT
drop pop 4* R-CLEAR COUNT TYPE CR jump ABORT
NO: drop pop 4* dup C@ + 4/ 1 + push ;
\ ( flag -- ) ABORT" text" as v3 (it is FORTH-83's, not FORTH-79's): if