MESH.md step 3. The ports are the transport; the message is what is
transported: to, from, type, heat and TTL, ACL tag, sequence, length, then
text four characters to a word.
- quit.v4: a node with nothing to do is blocked reading "any port"; text
for it is interpreted; (FINISH) sends what it printed and then how the
text ended, and it waits again
- core.v4: EMIT keeps what is printed, (FLUSH-OUT) and (HDR) send it to the
sender on the port the message came on. EMIT still needs one free data
cell and no more; it works on the return stack and in A and B
- message.h/.c: the same format for whatever is on a port and is not a node
- boot.c: the boot is the node's console on port 1 and its kernel on port 0
- the prompt tests are a console that speaks messages
- gone: v4_line_begin, v4_line_done, v4_line_status; writing a node's input
buffer and setting its P from outside; any use of CONSOLE-TX
Verified: make -C v4 test (test_host_quit.c 1283 checks, the full-stack
figures unchanged) and make -C v4 sanitize pass; hosted-check passes on
three ISAs with POST 550 of 550; clean qemu with STARFORTH_V4=1 passes POST
and answers lines typed at each prompt on amd64, aarch64 and riscv64
(logs/20261006-110551, -111621, -111341). -110837 is an aarch64 run ended
by the test wrapper's limit while still in UEFI firmware; it shows nothing
about v4.
Not done: KEY, EXPECT and QUERY still read the console's input registers;
a message not for this node is let go (step 4).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MESH.md step 2. A capsule of F18 code is the words a neighbour writes to a
node's port: for each stretch of memory, "@p a! @p push", the address and
count, "@p !+ unext" and the words; then a jump to the start. A node born
empty executes that from its port, so it needs nothing in it beforehand.
- capsule.h/.c: v4_capsule_write, any node's memory as such a capsule
- mkimage writes the nucleus so, to capsules/v4/nucleus-64.f18, and the
addresses a host needs as a C file; the memory image is no longer linked
into either product
- mkcapsule is unchanged: the nucleus capsule is a built file kept under
capsules/, as BLOCK_MAP.md is, and is baked, hashed and signed with the
rest
- boot: the node is born empty (v4_image_born); the nucleus capsule is
found, its hash and signature checked, and given to the node a word at a
time as it reads its port; PARITY:V4_NUCLEUS carries its name and hash
Verified: test_fabric.c (59 checks, both widths, and under ASan and UBSan):
a memory with a programme and scattered words arrives word for word in an
empty node and runs. The nucleus capsule rebuilds byte for byte.
hosted-check passes on three ISAs; clean qemu with STARFORTH_V4=1 on amd64,
aarch64 and riscv64 takes the nucleus in, passes POST (550 of 550) and
answers lines typed at each prompt (logs/20261006-102421, -102706,
-103048).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MESH.md step 1, in the engine, which knows nothing of StarForth or of any
kernel.
- node: V4_PORTS ports (8), a build parameter; "any port" and the port the
last such read came from; a read blocks until the neighbour writes, as a
write blocks until the neighbour reads; v4_node_born: empty, P at "any
port"
- exec: a fetch from a port -- @ @b @+ @p, or of an instruction word when P
is a port -- waits for a word; a node executes what arrives at a port
without advancing P; a blocked node goes on from the slot it stopped at
- fabric: the nodes there are and the table of how their ports are wired,
both changed while the nodes run; devices on a port; asleep and awake; a
step is every unblocked node executing one instruction word, then every
write with a reader waiting being handed over
- DECOMPOSITION.md section 6: four named ports withdrawn for V4_PORTS
numbered ones and wiring as data, as ruled
Verified: tests/test_fabric.c, 53 checks at both widths: two nodes exchange
words; an empty node is filled through its port by a device, and by another
node, and runs what it was sent; a word is passed on by a node in between;
a waiting node executes nothing; the wiring is changed while they run; a
node is put to sleep, woken and removed while looping; a node is born while
others run; the fabric is given more room. make -C v4 test and make -C v4
sanitize pass. The single-node products are unchanged: hosted-check on
three ISAs, and clean qemu with STARFORTH_V4=1 on amd64, aarch64 and
riscv64 with lines typed at each prompt (logs/20261006-074907, -075150,
-075532).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
One boot, v4/system/boot.c, for both products: it starts the nucleus image,
finds each capsule in the baked capsule directory, recomputes its hash,
checks its signature, gives its blocks to the node a line at a time, and
prints PARITY:V4_NUCLEUS, PARITY:V4_CAPSULE and PARITY:OK before the prompt.
A line the node does not accept ends the boot with the capsule, block and
line named. docs/v4.0.0/NUCLEUS.md.
- hosted Linux product for amd64, aarch64 and riscv64 (make -C v4 hosted);
make -C v4 hosted-check boots all three and requires identical output
- the kernel's v4 entry (STARFORTH_V4=1) calls the same boot
- capsules/v4/forth79.4th, block 6000: no definitions yet
- mkimage builds the nucleus only; no FORTH source is compiled at build time
- capsule_blocks.c: the Block-header parse, free of any VM, for every loader
Verified: make -C v4 test passes; hosted-check passes on the three ISAs with
the same hashes; the kernel compiles with STARFORTH_V4=1 on the three.
Not verified: no bare-metal boot of v4 has been run.
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>
Ruled 2026-10-04: guard all errors. The errors that set NODE-ERROR and
let the line run on now stop it at once, with a message.
- A store of a non-zero code to NODE-ERROR is a trap, a sixth kind of
fault: nothing after it executes, the return stack is emptied, and the
data stack is left as the word left it. Not attached, NODE-ERROR is
plain memory, as the tests below the prompt use it.
- The capsule's words store a code where they stored -1, and the prompt's
(RAISED) prints its message: Negative count, Not a number, Number too
long, Not a character, Dictionary full, Name missing, Control structure
mismatch, Control structures too deep.
- ' and COMPILE and [COMPILE] of a word that is not there say
UNKNOWN WORD: 'xxx', as the interpreter does.
- tests: the trap in test_exec.c; every message from the prompt, with the
rest of the line not run and the stack kept, in test_host_quit.c.
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.
- The executor checks every address a programme uses (P, A, B) before
using it. Outside memory the opcode does nothing, the rest of its word
is not executed, and P becomes the node's fault handler; a node with no
handler stops. v4_node_load/store never index outside memory.
- capsule/quit.v4: (FAULT), the host node's handler, prints
"Address out of range", ends an open definition, prints ERROR and
returns to the prompt.
- tests: every memory opcode and P in test_exec.c; from the prompt, from
inside nested words and loops, in test_host_quit.c.
This closes the hole node.h described: a wild address used to index the
model's own memory gigabytes out of bounds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
&NAME is the address of a word as a literal; CONST+N is a loader
constant plus an offset as one literal, so that a capsule file can
address the cells of its scratch area without adding at run time.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
'header NAME [flags]' in front of a word gives it a dictionary entry in
the layout capsule/dict.v4 uses, linked to the header before it, so the
words of a capsule are in the dictionary as soon as it is assembled.
The name is taken as it stands (it may be ( or + or ;) and the
assembler's own name for the code may differ.
Tested in the assembler's own test, and by FIND and the field words
running over assembler-made entries beside ones (HEADER) made.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The first layer of the compiler capsule, on the host node: TIB >IN SPAN
SOURCE BL EXPECT QUERY WORD ENCLOSE CONVERT NUMBER and the comment
words. The definitions are text, v4/capsule/core.v4 (the core words
they rest on) and v4/capsule/input.v4, assembled by the text assembler,
which can now read a file.
EXPECT, QUERY, WORD and ENCLOSE behave as v3's. CONVERT and NUMBER are
FORTH-79 (ruled 2026-10-04): any BASE, a double in the standard order,
and NUMBER returns a signed double or sets NODE-ERROR.
Executed at both cell widths against C and against values recorded from
the v3 binary.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Test support, beside the slot packer: reads definitions in the notation
DECOMPOSITION.md uses -- words, opcodes, literals, constants, labels,
branches, FOR NEXT and FOR UNEXT, in-line macros, comments -- and lays
them down through the slot packer, so they no longer have to be retyped
opcode by opcode in C.
Tested by assembling SWAP, UM/MOD, C@ and C! both ways on two nodes and
comparing memory word for word, by running what is assembled, and on
every error it reports, each with its line number.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The console's receive side, standing in for the console node until the
mesh exists, as CONSOLE-TX does for output. A data fetch (@, @+, @b)
from CONSOLE-STATUS gives -1 when a character is pending and 0 when
not; from CONSOLE-RX it gives the next character and takes it, or -1
with none pending. The characters come from a queue the test feeds.
Instruction words and literals are still fetched with v4_node_load, so
code at a register's address is never taken for the register.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
EMIT is a device service: a character sent to the console node. The
mesh and its ports are development step 2 and the memory map is open
(D-4), so the single-node model now stands in for the console with one
memory-mapped register, so that printing words can be run and their
output compared with v3's.
v4_node_console_attach(n, addr): after it, a store to word address
`addr` appends the low 8 bits of the value to a buffer on the node
(V4_CONSOLE_CAP characters, 4096 by default) and does not write memory;
a load from `addr` reads the memory word as before. Characters past the
capacity are counted in console_dropped and discarded. The hook is in
v4_node_store, which all four store opcodes use.
It is off by default: v4_node_reset detaches the console (address -1)
and empties the capture, so the ISA's behaviour and every existing test
are unchanged unless a console is attached. The address is the
caller's choice.
New test v4/tests/test_console.c, 22 checks per width: direct stores,
!b, ! and !+ through the executor printing "Hi!", memory at and around
the register untouched, the low byte only, overflow, detach, a console
at word 0, and reset. At 32- and 64-bit cells, optimised and ASan+UBSan
(`make test`, `make sanitize`); all other v4 tests still pass. Four
mutations of the hook each fail.
DECOMPOSITION.md section 7 gains the CONSOLE-TX row. No printing word is
defined here; EMIT and the words on it are still to do.
Test results on the amd64 host only. This is a development check, not
acceptance (JUSTIFICATION.md section 16).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
First code for StarForth v4 (JUSTIFICATION.md section 10, step 1): one node
of the 32-instruction core as a C99 model, with cell width as a build
parameter.
- Node: P, A, B, F18 circular stacks (10 and 9 deep, D-2), word-addressed
memory (D-1), 5% guard bands on every bounded list.
- Instruction word: six 5-bit slots in 32 bits at every cell width.
- Executor: all 32 opcodes of DECOMPOSITION.md 1.3. Cell arithmetic wraps
explicitly; no signed overflow or implementation-defined shift.
- Heat: per-opcode and per-call-target counters and the anti-clock, driven
by instruction retirement (1.4, D-6 interim).
- Slot packer and runner for tests, and a reference unsigned multiply in
plain C99 with no 128-bit type.
Tests run at 32- and 64-bit cells, and under ASan and UBSan. They cover
every opcode and execute the first section 4 definitions (NIP SWAP OR
NEGATE ROT 0< 0= 2DUP - U<) against the C operation each stands for.
UM* as written in section 4 is exact only while u1 <= 2^(n-2). Two known
failing cases are pinned in test_foundation.c until it is rewritten.
DECOMPOSITION.md: record D-9, the instruction word is 32 bits at every
cell width (ruled 2026-10-02).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>