ENGINE.md 3b, the node's side of ruling A (a word's code is the node's, its
accounts the kernel's).
- dict.v4, system.v4: (WORD-DEFINED) ( xt -- ) is run when an entry is
made, (WORD-FORGOTTEN) ( w -- ) when FORGET or COLD removes entries; with
0 there no one is told, as on the hosted product
- test_host_quit.c: a kernel that keeps the list of words and is checked to
hold exactly the node's dictionary after definitions, a vocabulary, an
abandoned definition, FORGET, a refused FORGET and COLD; KERNEL-WORD
called from the prompt and from a definition
Fixed, found while writing that test: since the capsules moved from build
time to boot time (294e6946), what COLD returns to and FORGET protects was
still the nucleus alone, so COLD lost U*, U/MOD and BYE and FORGET U* was
allowed. The boot now seals the system when it has loaded it
(v4_image_seal), and hosted-check checks COLD, the capsule word after it,
the refused FORGET and BYE.
Verified: make -C v4 test passes at both widths (1283 checks in
test_host_quit.c); hosted-check passes on three ISAs; clean qemu with
STARFORTH_V4=1 on amd64, aarch64 and riscv64 passes POST, and COLD, U*
after it, FORGET U* (refused), an unserved kernel word and BYE typed at
each prompt are answered correctly (logs/20261005-193045, -193307, -193636).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ENGINE.md step 2, the carrier. Ruled 2026-10-05 (V3-PARITY.md 1i), on
DECOMPOSITION.md section 6: a write to a port blocks until the neighbour
reads.
- node: v4_node_port_attach, v4_node_port_served; a store to the port keeps
the value as the request and blocks the node
- exec: a blocked node executes nothing; served, it goes on from the opcode
after the store, in the same instruction word; a fault meanwhile abandons
the rest of the word
- compile.v4: n KERNEL-WORD name makes a word whose body writes n to the
port; its arguments and results are on the data stack
- boot: the kernel's words are made by handing the node text, and requests
are served between the node's opcodes; one no one serves is error 12
- BYE, the first kernel word: hosted it leaves the program, as hosted v3;
on the lone node it is v3's cold restart
- ENGINE.md 3a: multiuser, multitasking, preemptive and cooperative, and
what that asks of the engine
Verified: make -C v4 test passes at both widths, with tests/test_port.c;
hosted-check passes on three ISAs; clean qemu with STARFORTH_V4=1 on amd64,
aarch64 and riscv64 passes POST with the same hashes as hosted, and a
kernel word no one serves and BYE typed at each prompt are answered
(logs/20261005-185506, -185734, -190101; -185234 is an amd64 run in which
those two lines were not typed).
Not done: v3's own C functions serving a node.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A v4 node no longer reads its own command line or prints a prompt. Its
host puts a line of text in the node's input buffer and starts it at
(LINE); the node interprets it and stops at (IDLE), leaving in
(LINE-STATUS) how it ended: completed, an error, or QUIT. The host says
" ok" or " ERROR" and prompts, as the kernel's REPL does for a v3 VM. A
line may be 1024 characters, a block, as v3's. Ruled 2026-10-05
(V3-PARITY.md 1b); design ENGINE.md 3.1.
- quit.v4: (REPL), the node's prompt loop, is gone; (LINE) (IDLE) (DONE)
- image.h/.c: v4_line_begin, v4_line_done, v4_line_status; the node is
idle at switch-on
- boot.c: v4_boot_line, the one loop the hosted binary, the kernel and the
capsule loader hand a line with; the code that took " ok" and the prompt
back out of the node's output is gone
- hosted.c, sk_v4.c: the prompt and the line editing are the host's
- test_host_quit.c: the tests are the node's host; two tests of the old
80-character prompt line now test a whole line, 1024 and 1025 characters
Verified: make -C v4 test passes at both widths; hosted-check passes on
three ISAs; clean qemu with STARFORTH_V4=1 on amd64, aarch64 and riscv64
passes POST (550 of 550) with the same hashes as hosted, and three lines
typed at each bare-metal prompt through the serial port are answered
correctly (logs/20261005-180922, -181152, -181541).
Still the lone node: kernel_main.c starts it before the fleet tables.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(CATCH): while it is not zero, a line that ends in an error sets it to -1
and ends " ok" instead of " ERROR". (EMIT-HOOK): the xt of a word that is
given each character EMIT would send to the console. The prompt loop sets
(EMIT-HOOK) to 0 at the end of every line. docs/v4.0.0/NUCLEUS.md 6.3,
amended: it said one nucleus word would do.
Verified: make -C v4 test passes at both widths; by hand at the hosted
prompt, an unknown word and a division by zero are caught, the output hook
receives every character, and an uncaught error still says ERROR.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Each entry's flags cell also holds v3's four fields: denied, pinned,
mode and a 16-bit TTL.
- compile.v4: INTERPRET checks every word it is about to execute or
compile -- recheck at TTL 0, else count down; a denied word is refused
with v3's line, the stack emptied and the line ended.
- capsule/acl.v4: ACL-MODE@ ACL-MODE! ACL-TTL@ ACL-TTL! ACL-ALLOW@
ACL-ALLOW! ACL-PINNED? ACL-PIN ACL-INHERIT ACL-INIT-PRIMITIVES ACL-HEAT@
ACL-WORD-ID, and ACL-HOOK.
- capsule/ACL.fth: v3's ACL.4th as FORTH source the node compiles; loading
it switches access control on.
- v3 checks every execution, inside definitions too. v4's code is native,
so it checks a word when it is compiled as well as when interpreted; a
call compiled while the word was allowed is not checked again.
DECOMPOSITION.md 5.20 says so.
- ACL-HEAT@ is 0 until heat is readable (D-6).
- tests/test_host_quit.c: seven transcripts of the v3 binary; the policy
file loaded and exercised; COLD.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- capsule/log.v4: the level constants LOG-ERROR .. LOG-DEBUG, LOG-LEVEL!
and LOG-LEVEL@, LOG-ERROR" .. LOG-DEBUG" and LOG-ERROR-STR ..
LOG-DEBUG-STR. A message is printed if its level is at or below
LOG-LEVEL, as v3's line -- colour, level, text -- without the time of
day, which a node has not got.
- LOG-xxx" compiles like ." : the level, a call to (LOG"), the text. SEE
shows it as text. COLD puts LOG-LEVEL back to LOG-INFO.
- tests/test_host_quit.c: ten transcripts of the v3 binary, every level
at every setting.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- capsule/editor.fth: the block editor as FORTH source, which the node
compiles itself. It is a vocabulary, EDITOR, used at the ordinary
prompt: n EDIT, then L N B T P E D S H R I WIPE DONE. v3's EDIT, a
shell of its own, is not carried over (ruled 2026-10-05); v3's L S and
SHOW are kept in FORTH as they were, with COPY.
- system.v4: COLD (the system as the loader left it), WARM, PAGE,
VERSION, 79-STANDARD (FORTH-79: silent), and DEFER IS DEFER@ as v3.
- input.v4: where words are split at blanks a zero byte reads as a blank,
so a block never written, or filled a line at a time, loads cleanly.
- tests/test_host_quit.c: the editor's source fed to the prompt line by
line, every command on empty and full screens, what reaches storage;
the system words; deferred words.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The golden model's host node gets a block storage device: four
memory-mapped registers (number, address, command, status), 1024-byte
blocks as 256 cells. tests/test_node.c.
- capsule/blocks.v4: BLOCK BUFFER UPDATE SAVE-BUFFERS EMPTY-BUFFERS LIST
LOAD SCR BLK (FORTH-79) and v3's FLUSH THRU and -->, over two buffers.
- The text being interpreted is at the address in (SRC), which QUERY makes
the terminal's buffer and LOAD a block's; LOAD saves and restores it, so
blocks nest and the rest of LOAD's line runs afterwards. WORD makes
sure a loading block is still in a buffer before it reads.
- In a block, \ skips to the next 64-character line.
- A block number that does not exist, and --> at the terminal, are errors
with messages (D-18).
- tests/test_host_quit.c: ten transcripts of the v3 binary; nesting three
deep on two buffers; what reaches the device and when.
v3's LOAD drops the rest of its line, and v3 has no BLK.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- VOCABULARY DEFINITIONS CONTEXT CURRENT FORTH, and v3's ORDER. A name is
looked up in the CONTEXT vocabulary and then in FORTH; a new entry goes
into the CURRENT vocabulary; : makes the CURRENT vocabulary CONTEXT;
FORTH is immediate.
- WORDS lists the CONTEXT vocabulary. FORGET takes what was defined later
out of every vocabulary, and a vocabulary that goes gives way to FORTH.
- v3's vocabularies separate nothing: a word defined in one is found from
every other, and one redefined in a vocabulary replaces FORTH's for good.
- tests/test_host_quit.c: isolation, chaining to FORTH, two vocabularies
with the same names, FORGET across them; and DUMP is now checked to put
BASE back.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- capsule/qmath.v4: Q.FROM-INT Q.TO-INT Q.1 Q.0 Q.SCALE Q.+ Q.- Q.* Q./
Q.ABS Q.NEG Q.= Q.< Q.> Q.0= Q.MAX Q.MIN Q.EXP Q.SQRT Q.LOG Q.SIN Q.COS
Q.PRINT -- the definitions test_foundation.c executes on the mesh node,
now in the host node's vocabulary.
- numout.v4: DUMP.
- Q./ by zero, and Q.SQRT and Q.LOG outside their domain, leave the
result D-11 and D-12 give and then raise an error (D-18): "Division by
zero", "Argument out of range". v3 returns 0 silently.
- tests/test_host_quit.c: 122 results printed by Q.PRINT are transcripts
of the v3 binary, the same at both cell widths; signed values, the
errors and DUMP's layout besides.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- compile.v4: CASE OF ENDOF ENDCASE, as v3 (they nest; a word out of
place is a control structure mismatch); ['] and [LITERAL].
- quit.v4: S" and its run-time word; at the prompt the text is copied to
PAD.
- capsule/system.v4: WORDS and VLIST; FORGET (FORTH-79), which gives the
space back and will not remove a word below FENCE -- the capsule's own
words -- where v3's FORGET DUP succeeds.
- tests/test_host_quit.c: ten more transcripts of the v3 binary, and
nesting, the fence and the listing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
capsule/words.v4: the stack, comparison, shift, double, mixed and string
words whose definitions DECOMPOSITION.md already gives and the mesh-node
tests execute, now in the host node's vocabulary:
2SWAP 2OVER 2ROT 2>R 2R> 2R@ 2@ 2! -! 0<> 0> <> <= >= U< U> ABS MAX MIN
WITHIN LSHIFT RSHIFT D- DABS D0= D0< D= D2* D2/ D< DMAX DMIN M+ M-
CMOVE> MOVE FILL ERASE BLANK -TRAILING COMPARE SEARCH SCAN SKIP
?TERMINAL TRUE FALSE INVERT NOP
- A shift count that is negative or as large as the cell is an error, as
in v3 (code 9, "Shift count out of range").
- tests/test_host_quit.c: 36 sessions from the prompt that are
transcripts of the v3 binary, and the cases where v4 keeps the standard
(MOVE in cells; M+ and M- with the double low cell first).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- capsule/numout.v4: <# # #S HOLD SIGN #>, . .R U. U.R D. D.R, ?, SPACES,
DECIMAL HEX OCTAL -- the definitions DECOMPOSITION.md 5.8 gives and the
mesh-node tests execute, now words of the host node's vocabulary.
- .S, which D-16 makes possible again: as v3, the depth, then every value
from the deepest, then a new line. It needs six cells of the stack
free.
- tests/test_host_quit.c: printed from the prompt, with 14 more sessions
that are transcripts of the v3 binary, and the ends of the number range
at each cell width.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With the stacks counted and guarded (D-16) their size is a parameter of
the node. The host node, which runs the interpreter and the compiler
under the user's programme, gets 32 values and 32 return entries; a mesh
node keeps the F18's 10 and 9. Nothing else about the mechanism or any
word's definition changes.
- stack.h: V4_DATA_RING and V4_RET_RING are build parameters; the
Makefile sets them for the host-node tests.
- tests/test_host_quit.c no longer assumes a size: it fills the stacks to
whatever they are, and takes every exit with the return stack full.
- Measured on the host node now: 28 values on a line, 29 waiting between
lines, words 31 deep from the prompt.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
Ruled 2026-10-04: guarded, an error shown, back to the prompt.
- capsule/forth.v4: / MOD /MOD */ */MOD M/MOD, M* and S>D join the
vocabulary. Each dividing word tests its divisor first; zero prints
v3's message ("/: Division by zero"), ends an open definition, prints
ERROR and returns to the prompt from however deep.
- SM/REM here has UM/MOD written into it and keeps its signs in memory,
and */MOD has M* written in, so that division can be used inside words
that other words call: / from four words deep, */MOD from three.
- forth.v4 now loads after quit.v4, which the guard leaves through.
- tests: v3 transcripts for each message; /MOD, /, MOD and */MOD against
C on every pair of edge values.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Layer 5 of the compiler capsule. The host node is now started at QUIT and
left running: it prompts, reads a line from its console with QUERY,
interprets it, says " ok" or " ERROR", and goes round again.
- capsule/quit.v4: QUIT and ABORT (FORTH-79), ABORT" and (ABORT") as v3 has
them, ." and (.") (FORTH-79). Text is compiled as a counted string after
the call; the run-time word returns to the cell after it.
- INTERPRET prints v3's messages before abandoning a line:
UNKNOWN WORD: 'xxx' and xxx: compile-only.
- core.v4: CR SPACE COUNT TYPE, as DECOMPOSITION.md gives them.
- input.v4: WORD split so that (PARSE) can take text without skipping
leading delimiters (." " is an empty string); EXPECT keeps its place in
memory, so six values may wait on the stack while a line is typed.
- tests/test_host_quit.c: the node is fed characters and its output read;
16 sessions are transcripts of the v3 binary.
QUIT stops the line from inside any word and says nothing; v3's goes on
with the line and cannot be compiled. v3's compiled ABORT" crashes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Measured, compiling a DO loop left one return entry spare under
INTERPRET and a defining word called from inside another word
overflowed the nine-entry return stack (D-2); the prompt loop will take
one more.
C@ and C! shift without a loop. The code generator shifts each opcode
into the word being built, so nothing is placed by a counted shift, and
the address masks come from a table. The dictionary search, the
in-liner, the end of a loop and the making of a data word are each one
word reached by a jump.
Every kind of line now leaves at least three return entries spare while
it compiles; a word run from the interpreter has six; and CREATE ...
DOES> works from the prompt, from a word, and from a word that calls
that.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
WORD, the dictionary words and the code generator run underneath
whatever the user has on the stacks, which are ten and nine deep (D-2).
Measured, they used five to seven data cells of their own, leaving a
line about three. Each now keeps what it works on in its file's
scratch cells and has at most three cells on the data stack; , calls
nothing; and the longest chains of calls are shorter.
The public words of core.v4, input.v4 and dict.v4 get dictionary
headers. NUMBER is split so the interpreter can have a flag instead of
NODE-ERROR. The host-node tests share one memory map, host_map.h.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>