Files
LithosAnanake/v4/include/v4/node.h
T
rajamesandClaude Opus 5.5 ff75e69e91 fix(v4): the final review's four defects in the ROM boot
- The boot left the source's last new line in the console input, and the
  first KEY after the boot read it.  What the ROM has not read when it
  starts the system is taken back (boot.c, rom_harness.h).  hosted-check:
  KEY after the boot reads the key typed.
- A comment or macro left open at the end of a blob ran on into the next,
  and the ROM accepted source text.c refuses.  The loader ends each blob
  with FS (V4_ROM_BLOB_END) after its new line; the ROM ends a blob there
  as text.c ends a file: an open comment or macro is an error, and the word
  being assembled ends.  test_host_rom_asm: seven blob pairs against text.c.
- store to an address outside RAM faulted the ROM with no message.  It is
  now refused, as text.c refuses it: ERR-RANGE, "address out of range".
- The kernel build did not regenerate capsules/v4/sys/map-<w>.f18 before
  packing the capsule directory; it now does.

ROM: 2084 words, rom_hash 0x4c458c6e48b35456.  dict_hash unchanged,
0xe5a57f3eb72ff94a.  make -C v4 test test-32 hosted-check pass.  QEMU:
amd64, aarch64, riscv64 to ok>, POST 538/538, POST: PASSED.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 16:35:07 -04:00

451 lines
25 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" */
/* The indexes, after the ports, of what a node asks of the fabric about whole
* messages on its wires (docs/v4.0.0/MESH.md 7d.3); see v4_node_port_attach. */
#define V4_PORT_WIRE (V4_PORTS + 4u) /* store the number of a port: what follows is about that wire */
#define V4_PORT_A (V4_PORTS + 5u) /* store a value an operation needs */
#define V4_PORT_B (V4_PORTS + 6u) /* store a second */
#define V4_PORT_DO (V4_PORTS + 7u) /* store the number of an operation: the node is blocked until it is done */
#define V4_PORT_HOW (V4_PORTS + 8u) /* fetch how it went */
#define V4_PORT_HAVE (V4_PORTS + 9u) /* fetch which wires have a message waiting: bit k for port k */
#ifndef V4_NODE_WORDS
#define V4_NODE_WORDS 1024u
#endif
/* THE ROM (docs/v4.0.0/MACHINE.md section 2, DATASHEET-F18.md section 5).
* A read-only range of V4_ROM_WORDS words at V4_ROM_BASE, just above RAM.
* An instruction fetch, an @p literal and a data fetch there read the ROM; a
* running programme's store there is an address fault. In the model it is
* attached, as every device is, and v4_node_reset detaches it: the owner
* attaches it again after a reset. The range's size is a build parameter,
* as RAM's is; 0 (the default) is a machine with no ROM. */
#ifndef V4_ROM_WORDS
#define V4_ROM_WORDS 0u
#endif
#define V4_ROM_BASE ((v4_cell)V4_NODE_WORDS)
/* What the ROM reads at the end of each blob of the source it is fed (ASCII
* FS, the file separator), after the blob's new line: the end of a file, as
* text.c ends one -- a comment or macro still open there is an error, and the
* word being assembled ends. The end of the whole source is EOT (4). */
#define V4_ROM_BLOB_END 28
/* 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];
/* The ROM: its words, and how many there are (0: none). See
* v4_node_rom_attach. */
const v4_cell *rom;
unsigned rom_words;
/* 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 */
unsigned writers; /* bit k: the neighbour on port k is blocked writing to this node */
unsigned readers; /* bit k: the neighbour on port k is blocked reading from this node */
/* Whole messages on the wires (MESH.md 7d.3). See v4_node_port_attach. */
unsigned wire; /* the port that A, B and Do are about; V4_PORTS if what was stored is not a port */
v4_cell wire_a, wire_b; /* A and B */
int doing; /* non-zero: blocked, having stored `do_op` to Do */
int spoke; /* non-zero: it has asked for a whole-message operation since it was born: it is a node that
* speaks whole messages, and not one that words are written to (fabric.h) */
v4_cell do_op; /* the operation */
v4_cell how; /* how the last one went */
unsigned have; /* which wires had a message waiting, as the fabric last said: bit k for port k */
int have_fetched; /* non-zero: `have` has been fetched since the fabric last cleared what had arrived */
/* 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 */
/* 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
* base + V4_PORTS + 2 which ports have a neighbour waiting to write to this
* node: bit k for port k
* base + V4_PORTS + 3 which ports have a neighbour waiting to read from it,
* bit k for port k; and, V4_PORTS bits higher, which
* ports have anything on them at all: bit V4_PORTS + k
* for port k. A port with nothing on it can never
* be read from or written to.
* base + V4_PORTS + 4 .. + 9 whole messages on the wires (MESH.md 7d.3), the
* indexes V4_PORT_WIRE, V4_PORT_A, V4_PORT_B, V4_PORT_DO,
* V4_PORT_HOW and V4_PORT_HAVE:
* Wire, A, B store: what an operation is about and
* needs; fetch gives back what was stored
* Do store: the number of an operation. The
* node is blocked until whoever connects
* its ports has done it (v4_node_doing,
* v4_node_done); fetch gives the number
* How fetch: how the last one went
* Have fetch: which wires have a message
* waiting, bit k for port k, as the fabric
* last said (v4_node_have)
* The two before them let a node that writes a word at a time see whether a
* neighbour is waiting (MESH.md 7a), as the F18's io register does; a node that
* speaks whole messages has no use for them. They are what whoever connects the ports last
* said, v4_node_port_status; fetching them waits for nothing and changes
* nothing. Of the whole-message addresses How and Have are read only: a
* store to either is an address fault, as to "any port".
* 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);
/* WHOLE MESSAGES (MESH.md 7d.3). A store to Do records the operation in
* n->do_op and blocks the node, as a store to a port does: n->doing. For
* whoever connects the node's ports: 1 if the node is blocked at one; the
* operation is done, `how` is what the node will fetch from How, and the
* node goes on from the opcode after the store; and `mask` is which wires
* have a message waiting, as the node will fetch it from Have. Fetching
* Have also sets n->have_fetched, for whoever connects the ports to see and
* clear. */
int v4_node_doing(const v4_node *n);
void v4_node_done(v4_node *n, v4_cell how);
void v4_node_have(v4_node *n, unsigned mask);
/* THERE IS NO ONE ON THE PORT the node is blocked at -- writing to it, or
* reading from that one port -- and there will not be. A node that can
* take an error (it has an error register and a fault table: node.h,
* v4_node_error_attach, v4_node_fault_attach) is let go and the error
* `code` is raised on it, as if it had stored the code to its error
* register: what it was doing ends there. Returns 1. A node that cannot
* stays blocked, as on the fabric, and 0 is returned; so is 0 for a node
* that is not blocked at a port, or is reading "any port". */
int v4_node_port_gone(v4_node *n, v4_cell code);
/* 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);
/* What is on the other end of the node's ports now: bit k of `writers` if
* the neighbour on port k is blocked writing to this node, bit k of
* `readers` if it is blocked reading from it and would take a word. */
void v4_node_port_status(v4_node *n, unsigned writers, unsigned readers);
/* 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);
/* 1 if `addr` can be fetched from: a word of node memory, or of the ROM. */
int v4_node_addr_readable(const v4_node *n, v4_cell addr);
/* Give the machine its ROM: `words` words at V4_ROM_BASE, read from `rom`,
* which must outlive the node. More than V4_ROM_WORDS words, or a null
* `rom`, attaches none. v4_node_reset detaches it. */
void v4_node_rom_attach(v4_node *n, const v4_cell *rom, unsigned words);
/* 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 (capsules/v4/sys/quit.f18, (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);
/* A BLOCK (DECOMPOSITION.md D-19, 5.11). Mass storage is the kernel's: a
* node asks it for a block by a request on port 0 (docs/v4.0.0/MESH.md
* section 8; blocks.h), and has nothing of it but these. 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. */
#define V4_BLOCK_BYTES 1024u
#define V4_BLOCK_CELLS 256u
/* 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 */