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>
367 lines
19 KiB
C
367 lines
19 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. */
|
|
/* How many ports a node has. A build parameter, as the cell width and the
|
|
* node's memory are: the geometry the nodes are wired in is data, and a node
|
|
* has as many ports as the geometry in use needs (docs/v4.0.0/MESH.md 4.1). */
|
|
#ifndef V4_PORTS
|
|
#define V4_PORTS 8u
|
|
#endif
|
|
#define V4_PORT_ANY V4_PORTS /* not a port: "whichever port has a neighbour writing" */
|
|
|
|
#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; /* word address of port 0, or -1: no ports */
|
|
int asking; /* non-zero: blocked, having written `request` to port `ask_port` */
|
|
v4_cell request; /* what it wrote */
|
|
unsigned ask_port; /* to which port */
|
|
int reading; /* non-zero: blocked, wanting a word from port `read_port` */
|
|
unsigned read_port; /* a port, or V4_PORT_ANY */
|
|
int given; /* non-zero: `given_value` has come from port `given_port` and not been taken yet */
|
|
v4_cell given_value;
|
|
unsigned given_port;
|
|
unsigned last_from; /* the port the last read from "any" took its word from */
|
|
|
|
/* 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 PORTS. A node reaches its neighbours through its ports, and a port is
|
|
* an address (DECOMPOSITION.md section 6): "a read from a port blocks until
|
|
* the neighbour writes; a write blocks until the neighbour reads. This is
|
|
* the GA144 model. No instruction is added." docs/v4.0.0/MESH.md 4.1.
|
|
*
|
|
* After v4_node_port_attach(n, base) the node's V4_PORTS ports are the word
|
|
* addresses base .. base + V4_PORTS - 1. Two more follow them:
|
|
* base + V4_PORTS "any port": a read here takes from whichever port
|
|
* has a neighbour writing
|
|
* base + V4_PORTS + 1 which port the last read from "any" came from
|
|
* (read only)
|
|
* None of them is memory while the ports are attached.
|
|
*
|
|
* A WRITE. A store to a port does not write memory: the node keeps the
|
|
* value in n->request and the port in n->ask_port, sets n->asking and is
|
|
* blocked. When its neighbour has taken the value -- v4_node_port_served --
|
|
* it goes on from the opcode after the store, in the same instruction word
|
|
* if there are any left.
|
|
*
|
|
* A READ. A fetch from a port -- @ @b @+, the literal fetch @p, or the
|
|
* fetch of an instruction word when P is a port -- does not happen until
|
|
* there is a word to fetch. The node sets n->reading and n->read_port and
|
|
* is blocked, the opcode not yet executed. When its neighbour writes --
|
|
* v4_node_port_give -- the fetch is made, and it gets that word. When P is
|
|
* a port it is not advanced by a fetch: the node goes on executing what
|
|
* arrives there, as an F18 node does.
|
|
*
|
|
* A blocked node executes nothing: v4_exec_step_word returns 0 and changes
|
|
* nothing. Who the neighbour is, and whether there is one, is not the
|
|
* node's to know (fabric.h). A store to "any port" or to the "which port"
|
|
* address is an address fault.
|
|
*
|
|
* A node has no ports until they are attached: v4_node_reset detaches them. */
|
|
void v4_node_port_attach(v4_node *n, v4_cell base);
|
|
|
|
/* The neighbour has taken what the node wrote. */
|
|
void v4_node_port_served(v4_node *n);
|
|
|
|
/* The neighbour on `port` writes `value`: the node, blocked reading that
|
|
* port or "any", is unblocked and will fetch it. */
|
|
void v4_node_port_give(v4_node *n, unsigned port, v4_cell value);
|
|
|
|
/* Which port `addr` is: 0 .. V4_PORTS - 1, V4_PORT_ANY, V4_PORTS + 1 for the
|
|
* "which port" address, or -1 if it is none of them. */
|
|
int v4_node_port_index(const v4_node *n, v4_cell addr);
|
|
|
|
/* 1 if a fetch from `addr` can be made now: it is not a port, or the word
|
|
* has been given. 0 if it is a port with nothing given: the node is then
|
|
* blocked reading it, and the fetch must not be made. */
|
|
int v4_node_read_ready(v4_node *n, v4_cell addr);
|
|
|
|
/* A NODE NEWLY BORN (docs/v4.0.0/MESH.md 4.2). Reset, its ports attached at
|
|
* `base`, and P at "any port": it is blocked reading its ports, and will
|
|
* execute what a neighbour sends it. Nothing else is in it. */
|
|
void v4_node_born(v4_node *n, v4_cell base);
|
|
|
|
/* 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 */
|