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>
147 lines
7.2 KiB
C
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 */
|