Files
LithosAnanake/v4/include/v4/node.h
T
rajamesandClaude Opus 5.5 0da7e32a0b feat(v4.0.0): the stacks are guarded (D-16); DEPTH, PICK and ROLL
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>
2026-10-04 17:31:18 -04:00

236 lines
12 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 */
/* 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 */
} 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);
/* 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;
* - P becomes fault_vector + fault_kind. The handler table is five 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 five 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_KINDS 5u
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);
#endif /* V4_NODE_H */