Files
LithosAnanake/v4/include/v4/node.h
T
rajamesandClaude Opus 5.5 7e3932a4ad feat(v4.0.0): CONSOLE-TX capture register on the single-node model
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>
2026-10-03 11:57:49 -04:00

147 lines
7.2 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];
} 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.
*
* PRECONDITION: 0 <= addr < V4_NODE_WORDS.
*
* Out-of-range addressing is not defined by DECOMPOSITION.md. D-1 fixes word
* addressing and says nothing about an address outside the node's memory; a
* F18 has no such case because its address space is the memory. Whether a
* conforming model should wrap, saturate, or fault is a real open question and
* is raised as a gap rather than decided here, because each answer changes the
* ISA's observable behaviour and inventing one silently would bake a guess into
* every later test.
*
* WHAT THE BOUNDARY HERE DOES AND DOES NOT COVER, since it is easy to
* overclaim and the distinction decides whether out-of-range addresses are
* actually protected:
*
* - It DOES cover a linear index error. An access to mem[-1] or
* mem[V4_NODE_WORDS] lands in the 5% band, because the band is immediately
* adjacent (tests/test_node.c asserts the adjacency with offsetof rather
* than assuming it). That access is inside the struct, so AddressSanitizer
* stays silent about it and v4_node_guards_intact() is what reports it.
*
* - It does NOT cover an out-of-range *word address*. A v4_cell address of
* -1 casts to unsigned 0xFFFFFFFF, which is 16 GB past mem at 32-bit cells
* -- far outside the struct and outside the band. So a P, A or B that runs
* off the end of memory is caught by neither the band nor, in a production
* build without sanitizers, by anything else here. It is a precondition
* violation and the range check that would catch it is the open question
* above, not the band.
*
* The band is worth its 10.2% for the first case regardless. The second case
* is a hole, and it is closed by ruling on out-of-range addressing, not by
* widening the band. */
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);
#endif /* V4_NODE_H */