feat(v4.0.0): hosted golden model, single node
First code for StarForth v4 (JUSTIFICATION.md section 10, step 1): one node of the 32-instruction core as a C99 model, with cell width as a build parameter. - Node: P, A, B, F18 circular stacks (10 and 9 deep, D-2), word-addressed memory (D-1), 5% guard bands on every bounded list. - Instruction word: six 5-bit slots in 32 bits at every cell width. - Executor: all 32 opcodes of DECOMPOSITION.md 1.3. Cell arithmetic wraps explicitly; no signed overflow or implementation-defined shift. - Heat: per-opcode and per-call-target counters and the anti-clock, driven by instruction retirement (1.4, D-6 interim). - Slot packer and runner for tests, and a reference unsigned multiply in plain C99 with no 128-bit type. Tests run at 32- and 64-bit cells, and under ASan and UBSan. They cover every opcode and execute the first section 4 definitions (NIP SWAP OR NEGATE ROT 0< 0= 2DUP - U<) against the C operation each stands for. UM* as written in section 4 is exact only while u1 <= 2^(n-2). Two known failing cases are pinned in test_foundation.c until it is rewritten. DECOMPOSITION.md: record D-9, the instruction word is 32 bits at every cell width (ruled 2026-10-02). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
1f98034311
commit
067f317c47
@@ -0,0 +1,114 @@
|
||||
/* 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)
|
||||
|
||||
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];
|
||||
} 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);
|
||||
|
||||
#endif /* V4_NODE_H */
|
||||
Reference in New Issue
Block a user