Files
LithosAnanake/v4/include/v4/node.h
T
rajamesandClaude Opus 5.5 8df6c16766 feat(v4.0.0): a node asks its kernel by a blocking write to its port
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>
2026-10-05 19:03:03 -04:00

318 lines
17 KiB
C

/* node.h -- the v4 node: registers, stacks, and word-addressed memory.
*
* DECOMPOSITION.md section 1.1:
*
* | Cell | 32 bits (mesh node). Host node's width is a build
* | parameter; see D-5. |
* | Addressing | Word-addressed (D-1). |
* | Registers | T (top of data stack), S (second), R (top of return
* | stack), P (program counter), A and B (address
* | registers). |
* | Stacks | F18 circular hardware stacks, not visible to code
* | (D-2) ... data stack 10 deep, return stack 9 deep. |
* | Instruction | Six 5-bit slots, plus 2 spare bits. |
*
* T, S and R are not fields here: they live inside v4_dstack and v4_rstack,
* because on the F18 they are the topmost slots of the circular stacks rather
* than registers that happen to shadow them, and the golden model has to model
* the mechanism and not just the values. P, A and B are genuinely separate
* registers and live here.
*
* MEMORY SIZE IS A BUILD PARAMETER, not a constant of the design.
* JUSTIFICATION.md section 10 names cell width, node count and node memory as
* the three parameters of the golden model, and no word count is fixed anywhere
* in DECOMPOSITION.md -- D-4, which is where a node memory map would be pinned
* down, is deferred to step 2. The default here is a round number that suits
* a host; the datapoint worth knowing is JUSTIFICATION.md section 4, that a
* GA144 node had 64 words of RAM and 64 of ROM. v4 keeps the same split --
* dictionary and interpreter live in the compiler capsule on the host and are
* streamed to nodes, so node memory is not sized by capsule size.
*/
#ifndef V4_NODE_H
#define V4_NODE_H
#include "v4/cell.h"
#include "v4/guard.h"
#include "v4/stack.h"
/* Words of node memory. Overridable at build time. */
#ifndef V4_NODE_WORDS
#define V4_NODE_WORDS 1024u
#endif
/* 5% safety boundary at each end of node memory, per guard.h. Note the
* arithmetic: at 1024 words that is 52 words either side, 5.2% of memory
* spent on a boundary. That is the cost of the convention on a resource that
* is scarce by construction, and it is worth naming rather than discovering
* later -- on a real mesh node the memory is 32-bit words on silicon with no
* second chance. A deployment that cannot afford it sets V4_GUARD_PCT to 0 at
* build time, which is why the percentage is a macro and not baked in. */
#define V4_MEM_BOUND V4_GUARD_ELEMS(V4_NODE_WORDS)
/* Characters the console capture holds. Overridable at build time. */
#ifndef V4_CONSOLE_CAP
#define V4_CONSOLE_CAP 4096u
#endif
typedef struct {
v4_cell p; /* P -- program counter */
v4_cell a; /* A -- address register */
v4_cell b; /* B -- address register */
v4_dstack ds; /* holds T and S */
v4_rstack rs; /* holds R */
v4_cell mem_guard_head[V4_MEM_BOUND];
v4_cell mem[V4_NODE_WORDS];
v4_cell mem_guard_tail[V4_MEM_BOUND];
/* CONSOLE-TX capture (DECOMPOSITION.md section 7). See
* v4_node_console_attach below. */
v4_cell console_tx; /* its word address, or -1: no console */
unsigned console_len; /* characters captured */
unsigned console_dropped; /* characters that did not fit */
unsigned char console[V4_CONSOLE_CAP];
/* CONSOLE-RX and CONSOLE-STATUS (section 7). See
* v4_node_console_input_attach below. */
v4_cell console_rx; /* its word address, or -1: no input */
v4_cell console_status; /* its word address, or -1 */
unsigned input_len; /* characters fed */
unsigned input_pos; /* characters taken; input_len - input_pos are pending */
unsigned char input[V4_CONSOLE_CAP];
/* 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 */
/* The port (DECOMPOSITION.md section 6): a write to it blocks the node
* until its neighbour has taken what was written. See
* v4_node_port_attach below. */
v4_cell port; /* its word address, or -1: no port */
int asking; /* non-zero: blocked, having written `request` */
v4_cell request; /* what it wrote */
/* 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 */
/* The block storage device (D-19). See v4_node_storage_attach. */
v4_cell storage_reg; /* word address of its four registers, or -1 */
unsigned char *storage; /* the blocks, 1024 bytes each */
unsigned storage_blocks; /* how many */
/* NODE-ERROR as a trap (D-18). See v4_node_error_attach. */
v4_cell error_reg; /* its word address, or -1 */
} v4_node;
/* Zero P, A and B, empty the stacks, and clear memory. Installs every
* boundary (memory and both stacks) and leaves the node in a defined state. */
void v4_node_reset(v4_node *n);
/* 1 if every boundary in the node is intact -- memory and both stacks -- else
* 0. This is the single call a caller needs to detect that an index went
* wrong somewhere; see guard.h for what it catches that AddressSanitizer
* cannot. */
int v4_node_guards_intact(const v4_node *n);
/* Read and write one word. D-1: word-addressed, so `addr` counts words and
* CELLS is a no-op. `addr` is a v4_cell because A, B and P are all v4_cells
* and a narrower parameter would put a conversion between the register and the
* access on every one of the eight memory opcodes.
*
* An address outside 0 .. V4_NODE_WORDS-1 is never used to index memory
* (D-14, ruled 2026-10-04): v4_node_load gives 0 for it and v4_node_store
* does nothing. That is all these two do. They are what C code uses -- the
* assemblers, the tests -- and they do not count a fault; the executor does,
* for the addresses a running programme uses (see v4_node_fault_attach).
*
* The 5% band on each side of mem is a different protection: it is where a
* linear index error in the model's own C -- mem[-1], mem[V4_NODE_WORDS] --
* would land, inside the struct where AddressSanitizer cannot see it, and
* v4_node_guards_intact() reports it. */
v4_cell v4_node_load(const v4_node *n, v4_cell addr);
void v4_node_store(v4_node *n, v4_cell addr, v4_cell value);
/* CONSOLE-TX, the console's transmit register, on the single-node model.
*
* EMIT is a device service: one character sent to the console node
* (DECOMPOSITION.md 5.10). The mesh and its ports are development step 2,
* and the memory map is open (D-4), so until then the model stands in for
* the console with one memory-mapped register and a capture buffer. That is
* enough to run every printing word and compare what it prints with v3.
*
* After v4_node_console_attach(n, addr), a store to word address `addr`
* appends the low 8 bits of the value to n->console instead of writing
* memory; n->mem[addr] is never changed by it, and a load from `addr` reads
* that memory word as before. All four store opcodes go through
* v4_node_store, so all four reach it. Once V4_CONSOLE_CAP characters are
* held, further ones are counted in n->console_dropped and discarded.
*
* A node has no console until one is attached: v4_node_reset detaches it
* (console_tx = -1) and empties the capture. Attaching empties it too.
* Passing -1 detaches. The address is the caller's choice. */
void v4_node_console_attach(v4_node *n, v4_cell addr);
/* THE PORT. A node reaches its neighbour through a port, and a port is an
* address (DECOMPOSITION.md section 6): "a write blocks until the neighbour
* reads. This is the GA144 model. No instruction is added."
*
* After v4_node_port_attach(n, addr), a store to word address `addr` does
* not write memory: the node keeps the value in n->request, sets n->asking
* and is blocked. A blocked node executes nothing -- v4_exec_step_word
* returns 0 and changes nothing -- until its neighbour calls
* v4_node_port_served(n). It then goes on from the opcode after the store,
* in the same instruction word if there are any left.
*
* This is how a node asks its kernel for something (docs/v4.0.0/ENGINE.md
* 3.3): the neighbour on the port is the kernel, what is written is the
* number of the request, and the arguments and results are on the node's
* data stack, which the kernel may use while the node is blocked. Only the
* write is here. Nothing reads from a port yet, and a fetch from the
* address is a fetch from memory.
*
* A node has no port until one is attached: v4_node_reset detaches it. */
void v4_node_port_attach(v4_node *n, v4_cell addr);
void v4_node_port_served(v4_node *n);
/* CONSOLE-RX and CONSOLE-STATUS, the console's receive side, on the
* single-node model.
*
* KEY and ?TERMINAL are device services too (DECOMPOSITION.md 5.10), and for
* the same reason as CONSOLE-TX the model stands in for the console with two
* memory-mapped registers and a queue of characters the test feeds.
*
* After v4_node_console_input_attach(n, rx, status), a data fetch (the
* opcodes @, @+ and @b, which go through v4_node_fetch) from word address
* `status` gives -1 when a character is pending and 0 when none is;
* `rx` gives the next pending character, 0 .. 255, and takes it off
* the queue; with none pending it gives -1 and takes nothing.
* Fetching the status changes nothing. Neither fetch reads or writes
* n->mem, a store to either address is an ordinary memory store, and
* v4_node_load -- which the executor uses to fetch instruction words and
* literals, and the assembler to patch them -- always reads memory, so code
* and literals are never mistaken for the registers.
*
* v4_node_console_feed appends characters to the queue and returns how many
* it took; the queue holds V4_CONSOLE_CAP characters that have not been
* read. A node has no input until it is attached: v4_node_reset detaches it
* (both addresses -1) and empties the queue. Attaching empties it too.
* Passing -1 for both detaches. The addresses are the caller's choice and
* must differ from each other and from CONSOLE-TX.
*
* Nothing here blocks. A KEY that must wait polls CONSOLE-STATUS, so on
* this model a wait is a loop that runs until the test feeds a character. */
void v4_node_console_input_attach(v4_node *n, v4_cell rx, v4_cell status);
unsigned v4_node_console_feed(v4_node *n, const void *chars, unsigned len);
/* A data fetch: what @, @+ and @b read at `addr`. The two receive registers
* above when attached, memory otherwise; 0 outside memory, as v4_node_load. */
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);
/* 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 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. (A raised
* error, below, empties only the return stack: the word that raised it
* chose to, after taking its own arguments off);
* - P becomes fault_vector + fault_kind. The handler table is six 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 table and clears the count. Attaching clears
* `stopped` and the count; all six 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_RAISED 5u /* a programme stored an error code in NODE-ERROR (D-18) */
#define V4_FAULT_KINDS 6u
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);
/* THE BLOCK STORAGE DEVICE (DECOMPOSITION.md D-19, 5.11).
*
* Mass storage is a device service; until the mesh exists the model stands
* in for it on the host node with four memory-mapped registers at `reg`:
*
* reg + 0 BLOCK-NUMBER which block, 0 .. blocks - 1
* reg + 1 BLOCK-ADDRESS word address of 256 cells of node memory
* reg + 2 BLOCK-COMMAND a store of 1 reads the block into those cells,
* a store of 2 writes it from them
* reg + 3 BLOCK-STATUS 0 after a command that worked, -1 after one
* that did not: no such block, cells that are
* not all in memory, or an unknown command
*
* The first, second and fourth are memory words the device reads and writes;
* only a store to the third does anything. A block is 1024 bytes, and a
* byte address is four times a word address plus 0 .. 3 (D-1), so a block
* is 256 cells, four bytes to a cell, the first byte lowest, whatever the
* cell width: a read leaves the rest of each cell zero, and a write takes
* the low 32 bits.
*
* `bytes` is the caller's: `blocks` * 1024 bytes, which the device reads and
* writes and never frees. v4_node_reset detaches it (reg -1). */
#define V4_BLOCK_BYTES 1024u
#define V4_BLOCK_CELLS 256u
void v4_node_storage_attach(v4_node *n, v4_cell reg, unsigned char *bytes, unsigned blocks);
/* NODE-ERROR as a trap (DECOMPOSITION.md D-18, ruled 2026-10-04: every error
* is guarded, shown, and returns the node to its prompt).
*
* A word that finds an error stores a code, which is not zero, in NODE-ERROR.
* After v4_node_error_attach(n, addr) a store of a non-zero value to `addr`
* writes that memory word, as before, and is then a fault of kind
* V4_FAULT_RAISED: the rest of the instruction word is not executed, the
* return stack is emptied, fault_addr is the code, and P becomes the sixth
* word of the fault table. The data stack is left alone. A store of zero
* -- which is how the flag is cleared -- is an ordinary store.
*
* Not attached (-1, as after v4_node_reset), NODE-ERROR is ordinary memory
* and a word that stores to it simply goes on: that is how a word can be run
* and its error seen with no prompt to return to. */
void v4_node_error_attach(v4_node *n, v4_cell addr);
#endif /* V4_NODE_H */