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:
rajames
2026-10-02 14:38:27 -04:00
co-authored by Claude Opus 5.5
parent 1f98034311
commit 067f317c47
33 changed files with 3635 additions and 3 deletions
+110
View File
@@ -0,0 +1,110 @@
/* test_asm.c -- the slot packer lays words out as asm.h says it does. */
#include "v4/asm.h"
#include "v4/testcode.h"
#include <stdio.h>
static int failures = 0, checks = 0;
#define CHECK(c,...) do{checks++; if(!(c)){failures++; printf("FAIL %s:%d: ",__FILE__,__LINE__); printf(__VA_ARGS__); printf("\n");}}while(0)
static v4_node n;
static v4_exec_state es;
static v4_heat h;
static v4_asm as;
static void fresh(v4_cell origin)
{
v4_node_reset(&n);
v4_exec_reset(&es);
v4_heat_reset(&h);
v4_asm_begin(&as, &n, origin);
}
static v4_iword word_at(v4_cell addr) { return (v4_iword)(v4_ucell)n.mem[addr]; }
int main(void)
{
v4_asm_ref ref;
v4_cell loop, other;
printf("v4 asm tests: V4_CELL_BITS=%d\n", V4_CELL_BITS);
/* Six opcodes fill a word; the seventh starts the next, padded with nop. */
fresh(0);
for (unsigned k = 0; k < 7; k++) v4_asm_op(&as, V4_OP_DUP);
CHECK(v4_asm_label(&as) == 2, "seven opcodes take two words");
for (unsigned k = 0; k < 6; k++) CHECK(v4_iword_op(word_at(0), k) == V4_OP_DUP, "word 0 slot %u", k);
CHECK(v4_iword_op(word_at(1), 0) == V4_OP_DUP, "word 1 slot 0");
for (unsigned k = 1; k < 6; k++) CHECK(v4_iword_op(word_at(1), k) == V4_OP_NOP, "word 1 pad %u", k);
/* `;` closes the word. */
fresh(0);
v4_asm_op(&as, V4_OP_DUP); v4_asm_op(&as, V4_OP_SEMI);
CHECK(v4_asm_label(&as) == 1, "; closes the word");
/* Literals follow the instruction word, in order, and the code runs. */
fresh(0);
v4_asm_lit(&as, 30); v4_asm_lit(&as, 12); v4_asm_op(&as, V4_OP_ADD); v4_asm_op(&as, V4_OP_SEMI);
CHECK(v4_asm_ok(&as), "literal program assembles");
CHECK(n.mem[1] == 30 && n.mem[2] == 12 && v4_asm_label(&as) == 3, "literal layout");
CHECK(v4_test_call(&n, &es, &h, 0, 10) == 1 && n.ds.t == 42, "30 12 + ;");
/* A branch takes the next free slot when that slot is legal. */
fresh(0);
v4_asm_op(&as, V4_OP_DUP); v4_asm_op(&as, V4_OP_DROP);
v4_asm_branch(&as, V4_OP_JUMP, 0x155);
CHECK(v4_iword_op(word_at(0), 2) == V4_OP_JUMP, "branch in slot 2");
CHECK(v4_iword_target(word_at(0), 2) == 0x155, "branch target in slot 2");
CHECK(v4_asm_label(&as) == 1, "a branch closes the word");
/* With slots 0-3 used, the branch moves to slot 0 of a new word. */
fresh(0);
for (unsigned k = 0; k < 4; k++) v4_asm_op(&as, V4_OP_DUP);
v4_asm_branch(&as, V4_OP_JUMP, 0x155);
CHECK(v4_iword_op(word_at(0), 4) == V4_OP_NOP && v4_iword_op(word_at(0), 5) == V4_OP_NOP,
"word before the branch is padded");
CHECK(v4_iword_op(word_at(1), 0) == V4_OP_JUMP && v4_iword_target(word_at(1), 0) == 0x155,
"branch moved to slot 0");
/* Forward reference, section 2's IF ... ELSE ... THEN with the flag left
* on the stack by `if`: if L1 drop 111 ; L1: drop 222 ; */
fresh(0);
ref = v4_asm_branch_fwd(&as, V4_OP_IF);
v4_asm_op(&as, V4_OP_DROP); v4_asm_lit(&as, 111); v4_asm_op(&as, V4_OP_SEMI);
other = v4_asm_label(&as);
v4_asm_resolve(&as, ref, other);
v4_asm_op(&as, V4_OP_DROP); v4_asm_lit(&as, 222); v4_asm_op(&as, V4_OP_SEMI);
CHECK(v4_asm_ok(&as), "forward branch assembles");
v4_dstack_push(&n.ds, 5);
CHECK(v4_test_call(&n, &es, &h, 0, 10) > 0 && n.ds.t == 111, "flag nonzero takes the first arm");
v4_dstack_reset(&n.ds); v4_rstack_reset(&n.rs);
v4_dstack_push(&n.ds, 0);
CHECK(v4_test_call(&n, &es, &h, 0, 10) > 0 && n.ds.t == 222, "flag zero takes the second arm");
/* Backward reference: 4 FOR 1 + NEXT runs the body n+1 times. */
fresh(0);
v4_asm_lit(&as, 4); v4_asm_op(&as, V4_OP_PUSH);
loop = v4_asm_label(&as);
v4_asm_lit(&as, 1); v4_asm_op(&as, V4_OP_ADD);
v4_asm_branch(&as, V4_OP_NEXT, loop);
v4_asm_op(&as, V4_OP_SEMI);
CHECK(v4_asm_ok(&as), "loop assembles");
v4_dstack_push(&n.ds, 0);
CHECK(v4_test_call(&n, &es, &h, 0, 50) > 0 && n.ds.t == 5, "4 FOR 1 + NEXT");
/* Errors are reported, not encoded. */
fresh(0); v4_asm_op(&as, V4_OP_JUMP);
CHECK(!v4_asm_ok(&as), "a branch through v4_asm_op is refused");
fresh(0); v4_asm_op(&as, V4_OPCODE_COUNT);
CHECK(!v4_asm_ok(&as), "an out-of-range opcode is refused");
fresh(0);
for (unsigned k = 0; k < 3; k++) v4_asm_op(&as, V4_OP_DUP);
v4_asm_branch(&as, V4_OP_JUMP, 5000); /* slot 3 reaches 4096 words */
CHECK(!v4_asm_ok(&as), "a target outside the page is refused");
fresh((v4_cell)(V4_NODE_WORDS - 1u));
v4_asm_lit(&as, 1); v4_asm_op(&as, V4_OP_SEMI);
CHECK(!v4_asm_ok(&as), "running off the end of memory is refused");
CHECK(v4_node_guards_intact(&n), "and does not touch the guard band");
printf(" %d checks, %d failures\n", checks, failures);
return failures ? 1 : 0;
}
File diff suppressed because it is too large Load Diff
+209
View File
@@ -0,0 +1,209 @@
/* test_foundation.c -- the first DECOMPOSITION.md definitions, executed.
*
* Section 4 says of its colon definitions that each "has been traced by hand,
* but none has been executed". This file assembles the helpers, the sign and
* zero tests, U< and UM* exactly as written there (plus 2DUP and - from
* section 5, which U< needs) and runs them on the golden model against the C
* operation each one stands for.
*
* Every call is made with a canary under the arguments, and the canary must
* still be directly under the results afterwards: a definition that leaves
* the right answer but an unbalanced stack is wrong.
*/
#include "v4/asm.h"
#include "v4/testcode.h"
#include "v4/umul.h"
#include <stdio.h>
static int failures = 0, checks = 0;
#define CHECK(c,...) do{checks++; if(!(c)){failures++; printf("FAIL %s:%d: ",__FILE__,__LINE__); printf(__VA_ARGS__); printf("\n");}}while(0)
#define CANARY ((v4_cell)0x0C0FFEE5)
#define MAXU ((v4_ucell)~(v4_ucell)0)
#define FLAG(c) ((c) ? V4_ALL_ONES : (v4_cell)0)
static v4_node n;
static v4_exec_state es;
static v4_heat h;
static v4_asm as;
static v4_cell w_nip, w_swap, w_or, w_negate, w_rot, w_zless, w_zequal,
w_2dup, w_minus, w_uless, w_umstar;
#define O(name) v4_asm_op(&as, V4_OP_##name)
#define LIT(v) v4_asm_lit(&as, (v4_cell)(v))
#define CALL(w) v4_asm_branch(&as, V4_OP_CALL, (w))
static void build(void)
{
v4_asm_ref ref;
v4_node_reset(&n);
v4_asm_begin(&as, &n, 16);
/* : NIP push drop pop ; */
w_nip = v4_asm_label(&as);
O(PUSH); O(DROP); O(RPOP); O(SEMI);
/* : SWAP over push push drop pop pop ; */
w_swap = v4_asm_label(&as);
O(OVER); O(PUSH); O(PUSH); O(DROP); O(RPOP); O(RPOP); O(SEMI);
/* : OR over inv and xor ; */
w_or = v4_asm_label(&as);
O(OVER); O(INV); O(AND); O(XOR); O(SEMI);
/* : NEGATE inv 1 + ; */
w_negate = v4_asm_label(&as);
O(INV); LIT(1); O(ADD); O(SEMI);
/* : ROT push SWAP pop SWAP ; */
w_rot = v4_asm_label(&as);
O(PUSH); CALL(w_swap); O(RPOP); CALL(w_swap); O(SEMI);
/* : 0< -if L1 drop -1 ; L1: drop 0 ; */
w_zless = v4_asm_label(&as);
ref = v4_asm_branch_fwd(&as, V4_OP_MINUS_IF);
O(DROP); LIT(-1); O(SEMI);
v4_asm_resolve(&as, ref, v4_asm_label(&as));
O(DROP); LIT(0); O(SEMI);
/* : 0= if L1 drop 0 ; L1: drop -1 ; */
w_zequal = v4_asm_label(&as);
ref = v4_asm_branch_fwd(&as, V4_OP_IF);
O(DROP); LIT(0); O(SEMI);
v4_asm_resolve(&as, ref, v4_asm_label(&as));
O(DROP); LIT(-1); O(SEMI);
/* : 2DUP over over ; (section 5.1) */
w_2dup = v4_asm_label(&as);
O(OVER); O(OVER); O(SEMI);
/* : - NEGATE + ; (section 5.4), NEGATE in line */
w_minus = v4_asm_label(&as);
O(INV); LIT(1); O(ADD); O(ADD); O(SEMI);
/* : U< 2DUP xor 0< IF NIP 0< ELSE - 0< THEN ;
* with section 2's IF: the flag is dropped on both arms. */
w_uless = v4_asm_label(&as);
CALL(w_2dup); O(XOR); CALL(w_zless);
ref = v4_asm_branch_fwd(&as, V4_OP_IF);
O(DROP); CALL(w_nip); CALL(w_zless); O(SEMI);
v4_asm_resolve(&as, ref, v4_asm_label(&as));
O(DROP); CALL(w_minus); CALL(w_zless); O(SEMI);
/* : UM* a! 0 31 FOR +* UNEXT push drop a pop ;
* 31 is the cell width less one; written for a 32-bit cell in section 4.
* The loop body is the start of its own word so that unext restarts it. */
w_umstar = v4_asm_label(&as);
O(BANG_A); LIT(0); LIT(V4_CELL_BITS - 1); O(PUSH);
(void)v4_asm_label(&as);
O(MUL_STEP); O(UNEXT); O(PUSH); O(DROP); O(PUSH_A); O(RPOP);
O(SEMI);
CHECK(v4_asm_ok(&as), "foundation words assemble");
}
/* Call `word` with a canary and up to three arguments on fresh stacks. */
static int call(v4_cell word, unsigned argc, v4_cell a, v4_cell b, v4_cell c)
{
v4_dstack_reset(&n.ds);
v4_rstack_reset(&n.rs);
v4_exec_reset(&es);
v4_heat_reset(&h);
v4_dstack_push(&n.ds, CANARY);
if (argc > 0) v4_dstack_push(&n.ds, a);
if (argc > 1) v4_dstack_push(&n.ds, b);
if (argc > 2) v4_dstack_push(&n.ds, c);
return v4_test_call(&n, &es, &h, word, 1000) > 0;
}
/* The results, top first, then the canary. */
static int left1(v4_cell t)
{
return n.ds.t == t && n.ds.s == CANARY;
}
static int left2(v4_cell s, v4_cell t)
{
if (n.ds.t != t || n.ds.s != s) return 0;
(void)v4_dstack_pop(&n.ds);
return n.ds.s == CANARY;
}
static int left3(v4_cell third, v4_cell s, v4_cell t)
{
if (n.ds.t != t) return 0;
(void)v4_dstack_pop(&n.ds);
return left2(third, s);
}
static const v4_cell vec[] = {
0, 1, 2, 3, -1, -2, 12345, -12345,
(v4_cell)(V4_MSB - 1u), (v4_cell)V4_MSB, (v4_cell)(V4_MSB + 1u),
(v4_cell)(V4_MSB >> 1), (v4_cell)((V4_MSB >> 1) - 1u),
(v4_cell)(MAXU / 3u), (v4_cell)(MAXU / 3u * 2u)
};
#define NVEC (sizeof vec / sizeof vec[0])
/* Does UM* as written give the true product? */
static int umstar_exact(v4_ucell u1, v4_ucell u2)
{
v4_ucell lo, hi;
v4_umul(u1, u2, &lo, &hi);
return call(w_umstar, 2, (v4_cell)u1, (v4_cell)u2, 0) && left2((v4_cell)lo, (v4_cell)hi);
}
int main(void)
{
printf("v4 foundation tests: V4_CELL_BITS=%d\n", V4_CELL_BITS);
build();
for (unsigned i = 0; i < NVEC; i++) {
v4_cell a = vec[i];
v4_ucell ua = (v4_ucell)a;
CHECK(call(w_negate, 1, a, 0, 0) && left1((v4_cell)(0u - ua)), "NEGATE [%u]", i);
CHECK(call(w_zless, 1, a, 0, 0) && left1(FLAG(ua & V4_MSB)), "0< [%u]", i);
CHECK(call(w_zequal, 1, a, 0, 0) && left1(FLAG(a == 0)), "0= [%u]", i);
for (unsigned j = 0; j < NVEC; j++) {
v4_cell b = vec[j];
v4_ucell ub = (v4_ucell)b;
CHECK(call(w_nip, 2, a, b, 0) && left1(b), "NIP [%u,%u]", i, j);
CHECK(call(w_swap, 2, a, b, 0) && left2(b, a), "SWAP [%u,%u]", i, j);
CHECK(call(w_or, 2, a, b, 0) && left1((v4_cell)(ua | ub)), "OR [%u,%u]", i, j);
CHECK(call(w_2dup, 2, a, b, 0) && n.ds.t == b && n.ds.s == a
&& (v4_dstack_pop(&n.ds), v4_dstack_pop(&n.ds), left2(a, b)),
"2DUP [%u,%u]", i, j);
CHECK(call(w_minus, 2, a, b, 0) && left1((v4_cell)(ua - ub)), "- [%u,%u]", i, j);
CHECK(call(w_uless, 2, a, b, 0) && left1(FLAG(ua < ub)), "U< [%u,%u]", i, j);
for (unsigned k = 0; k < NVEC; k++) {
v4_cell c = vec[k];
CHECK(call(w_rot, 3, a, b, c) && left3(b, c, a), "ROT [%u,%u,%u]", i, j, k);
}
/* UM* as written is exact whenever the multiplicand (u1, which
* stays in S) is at most 2^(n-2): T is then always below S, so
* T+S stays below 2^(n-1) and neither the lost carry nor +*'s
* sign-keeping shift can touch the result. u2 is unrestricted. */
if (ua <= (V4_MSB >> 1))
CHECK(umstar_exact(ua, ub), "UM* [%u,%u]", i, j);
}
}
/* KNOWN LIMITS of UM* as written, recorded so they cannot be forgotten.
* Section 4 marks UM* "NEEDS REVISION" and gives the limit as "operands
* below 2^31". The measured limit is tighter and is on u1 only. These
* two checks assert that the definition is still wrong where it is known
* to be wrong; when UM* is rewritten they must be turned into ordinary
* exactness checks. */
CHECK(!umstar_exact(MAXU, MAXU), "UM* limit: carry out of T is lost (D-3)");
CHECK(!umstar_exact(V4_MSB - 1u, 3u), "UM* limit: u1 below 2^(n-1) still fails");
CHECK(v4_node_guards_intact(&n), "guards intact");
printf(" %d checks, %d failures\n", checks, failures);
return failures ? 1 : 0;
}
+213
View File
@@ -0,0 +1,213 @@
/* test_guard.c -- the 5% safety boundary.
*
* A guard band whose only assertion is "the patterns still look right" has not
* been shown to catch anything. These tests therefore do three things a
* structural test cannot:
*
* 1. pin the sizing arithmetic, including the case that makes a 5% margin
* evaporate -- a short list, where 5% rounds down to zero elements and a
* zero-width guard is a comment rather than a boundary;
* 2. index past each end of a real stack on purpose, and require the check to
* fail, and require it to say *which* end was hit;
* 3. run the real push/pop paths and require the boundary to survive them, so
* a boundary that fires on ordinary use is caught as a false alarm rather
* than being tuned into uselessness.
*/
#include "v4/guard.h"
#include "v4/stack.h"
#include <stdint.h>
#include <stdio.h>
#include <string.h>
static int failures = 0;
static int checks = 0;
#define CHECK(cond, ...) \
do { \
checks++; \
if (!(cond)) { \
failures++; \
printf(" FAIL %s:%d: ", __FILE__, __LINE__); \
printf(__VA_ARGS__); \
printf("\n"); \
} \
} while (0)
static void test_sizing_rounds_up(void)
{
/* 5% of each, rounded up. */
CHECK(V4_GUARD_ELEMS(20) == 1, "5%% of 20 is 1, got %u", V4_GUARD_ELEMS(20));
CHECK(V4_GUARD_ELEMS(40) == 2, "5%% of 40 is 2, got %u", V4_GUARD_ELEMS(40));
CHECK(V4_GUARD_ELEMS(100) == 5, "5%% of 100 is 5, got %u", V4_GUARD_ELEMS(100));
CHECK(V4_GUARD_ELEMS(1000) == 50, "5%% of 1000 is 50, got %u", V4_GUARD_ELEMS(1000));
CHECK(V4_GUARD_ELEMS(2000) == 100, "5%% of 2000 is 100, got %u", V4_GUARD_ELEMS(2000));
}
static void test_sizing_never_zero(void)
{
/* The case that matters. 5% of anything under 20 rounds to 0 by
* truncation, and a guard of zero elements catches nothing at all while
* still reading as "5% boundary" in the source. */
for (unsigned n = 1; n < 20u; n++) {
unsigned g = V4_GUARD_ELEMS(n);
CHECK(g >= 1u, "a %u-element list got a %u-element boundary", n, g);
}
CHECK(V4_GUARD_ELEMS(1) == 1, "1-element list boundary");
CHECK(V4_GUARD_ELEMS(3) == 1, "3-element list boundary");
CHECK(V4_GUARD_ELEMS(8) == 1, "8-element list boundary");
CHECK(V4_GUARD_ELEMS(19) == 1, "19-element list boundary");
CHECK(V4_GUARD_ELEMS(V4_DATA_RING) == 1, "data ring boundary at depth 8");
CHECK(V4_GUARD_ELEMS(V4_RET_RING) == 1, "return ring boundary at depth 8");
}
static void test_patterns_are_distinct_at_this_width(void)
{
/* The compile-time assertion in guard.h covers this, but the point of
* distinct patterns is "which end", and that is only meaningful if it
* survives truncation to the cell width. */
CHECK(V4_GUARD_PATTERN_HEAD != V4_GUARD_PATTERN_TAIL,
"head and tail patterns are equal at V4_CELL_BITS=%d", V4_CELL_BITS);
CHECK((v4_ucell)V4_GUARD_PATTERN_HEAD != 0u, "head pattern truncated to 0");
CHECK((v4_ucell)V4_GUARD_PATTERN_TAIL != 0u, "tail pattern truncated to 0");
}
static void test_fill_and_intact(void)
{
v4_cell band[7];
v4_guard_fill(band, 7, V4_GUARD_PATTERN_HEAD);
CHECK(v4_guard_intact(band, 7, V4_GUARD_PATTERN_HEAD), "filled band intact");
CHECK(!v4_guard_intact(band, 7, V4_GUARD_PATTERN_TAIL),
"a head-filled band must not read as a tail band");
/* Every element is checked, not just the ends. */
for (unsigned i = 0; i < 7; i++) {
band[i] = 0;
CHECK(!v4_guard_intact(band, 7, V4_GUARD_PATTERN_HEAD),
"band intact after corrupting interior element %u", i);
v4_guard_fill(band, 7, V4_GUARD_PATTERN_HEAD);
}
}
static void test_reset_installs_boundaries(void)
{
v4_dstack d;
v4_rstack r;
/* A struct that has never been reset holds whatever was on the stack, so
* the boundary cannot be assumed present. Poison both first: that the
* check fails before reset is the point, since it is what proves the
* check is reading the boundary and not returning a constant. */
memset(&d, 0xA5, sizeof d);
memset(&r, 0xA5, sizeof r);
CHECK(!v4_dstack_guards_intact(&d),
"data boundaries must not read as intact before reset");
CHECK(!v4_rstack_guards_intact(&r),
"return boundaries must not read as intact before reset");
v4_dstack_reset(&d);
v4_rstack_reset(&r);
CHECK(v4_dstack_guards_intact(&d), "data boundaries intact after reset");
CHECK(v4_rstack_guards_intact(&r), "return boundaries intact after reset");
}
static void test_head_overrun_is_detected(void)
{
/* Deliberately index one element before the ring: exactly what a lost
* modulo or a sign error on the head arithmetic would do. */
v4_dstack d;
v4_dstack_reset(&d);
CHECK(v4_dstack_guards_intact(&d), "intact before the deliberate overrun");
d.guard_head[0] = (v4_cell)V4_GUARD_PATTERN_TAIL; /* wrong pattern too */
CHECK(!v4_dstack_guards_intact(&d),
"head overrun past the data ring was not detected");
/* Which end is answerable because the two patterns differ. */
CHECK((v4_ucell)d.guard_head[0] != V4_GUARD_PATTERN_HEAD,
"head boundary no longer holds its own pattern");
v4_dstack_reset(&d);
CHECK(v4_dstack_guards_intact(&d), "intact after re-reset");
}
static void test_tail_overrun_is_detected(void)
{
v4_dstack d;
v4_dstack_reset(&d);
d.guard_tail[0] = 0;
CHECK(!v4_dstack_guards_intact(&d),
"tail overrun past the data ring was not detected");
v4_dstack_reset(&d);
CHECK(v4_dstack_guards_intact(&d), "intact after re-reset");
}
static void test_return_stack_boundaries(void)
{
v4_rstack r;
v4_rstack_reset(&r);
CHECK(v4_rstack_guards_intact(&r), "return boundaries intact after reset");
r.guard_head[0] = 0;
CHECK(!v4_rstack_guards_intact(&r), "return head overrun not detected");
v4_rstack_reset(&r);
r.guard_tail[V4_RET_BOUND - 1u] = 0;
CHECK(!v4_rstack_guards_intact(&r), "return tail overrun not detected");
v4_rstack_reset(&r);
CHECK(v4_rstack_guards_intact(&r), "return intact after re-reset");
}
static void test_ordinary_use_never_touches_the_boundaries(void)
{
/* The other half of the contract: a boundary that fires on legitimate use
* is worse than none, because it trains you to ignore it. Drive both
* stacks hard, well past their depths, and require the boundaries to
* survive every operation. */
v4_dstack d;
v4_rstack r;
v4_dstack_reset(&d);
v4_rstack_reset(&r);
uint64_t s = 0x243F6A8885A308D3ull;
for (int i = 0; i < 50000; i++) {
s ^= s << 13; s ^= s >> 7; s ^= s << 17;
v4_dstack_push(&d, (v4_cell)s);
v4_rstack_push(&r, (v4_cell)(s >> 11));
if (i & 1) {
(void)v4_dstack_pop(&d);
(void)v4_rstack_pop(&r);
}
}
CHECK(v4_dstack_guards_intact(&d),
"data boundary damaged by ordinary push/pop");
CHECK(v4_rstack_guards_intact(&r),
"return boundary damaged by ordinary push/pop");
/* head must still be in range, which is the invariant the boundary exists
* to police. */
CHECK(d.head < V4_DATA_RING, "data head %u out of range", d.head);
CHECK(r.head < V4_RET_RING, "return head %u out of range", r.head);
}
int main(void)
{
printf("v4 guard tests: V4_CELL_BITS=%d, data bound %d, return bound %d\n",
V4_CELL_BITS, V4_DATA_BOUND, V4_RET_BOUND);
test_sizing_rounds_up();
test_sizing_never_zero();
test_patterns_are_distinct_at_this_width();
test_fill_and_intact();
test_reset_installs_boundaries();
test_head_overrun_is_detected();
test_tail_overrun_is_detected();
test_return_stack_boundaries();
test_ordinary_use_never_touches_the_boundaries();
printf(" %d checks, %d failures\n", checks, failures);
return failures ? 1 : 0;
}
+109
View File
@@ -0,0 +1,109 @@
/* test_heat.c -- heat and anti-clock. */
#include "v4/heat.h"
#include <stdint.h>
#include <stdio.h>
#include <string.h>
static int failures = 0;
static int checks = 0;
#define CHECK(cond, ...) \
do { \
checks++; \
if (!(cond)) { \
failures++; \
printf(" FAIL %s:%d: ", __FILE__, __LINE__); \
printf(__VA_ARGS__); \
printf("\n"); \
} \
} while (0)
static void test_reset(void)
{
v4_heat h;
memset(&h, 0xFF, sizeof h);
v4_heat_reset(&h);
for (unsigned i = 0; i < V4_OPCODE_COUNT; i++) {
CHECK(h.op[i] == 0, "op %u heat non-zero after reset", i);
}
for (unsigned i = 0; i < V4_HEAT_MAX_CALL_TARGETS; i++) {
CHECK(h.call[i] == 0, "call target %u heat non-zero after reset", i);
CHECK(!v4_heat_call_is_frozen(&h, (v4_cell)i),
"call target %u frozen after reset", i);
}
v4_uheat_t ac = 999;
v4_heat_on_retire(&h, V4_OP_NOP, &ac);
CHECK(ac == 1000, "anticlock not advanced");
CHECK(h.op[V4_OP_NOP] == 1, "nop heat");
}
static void test_retire_advances_anticlock_and_opcode(void)
{
v4_heat h;
v4_heat_reset(&h);
v4_uheat_t ac = 0;
for (int i = 0; i < 100; i++) {
v4_heat_on_retire(&h, V4_OP_ADD, &ac);
}
CHECK(ac == 100, "anticlock");
CHECK(h.op[V4_OP_ADD] == 100, "add heat");
for (unsigned i = 0; i < V4_OPCODE_COUNT; i++) {
if (i == V4_OP_ADD) continue;
CHECK(h.op[i] == 0, "other op %u", i);
}
}
static void test_call_advances_target(void)
{
v4_heat h;
v4_heat_reset(&h);
v4_heat_on_call(&h, 10);
v4_heat_on_call(&h, 10);
v4_heat_on_call(&h, 5);
CHECK(v4_heat_call_get(&h, 10) == 2, "target 10");
CHECK(v4_heat_call_get(&h, 5) == 1, "target 5");
CHECK(v4_heat_call_get(&h, 1) == 0, "target 1");
}
static void test_freeze(void)
{
v4_heat h;
v4_heat_reset(&h);
v4_heat_on_call(&h, 7);
v4_heat_call_freeze(&h, 7);
CHECK(v4_heat_call_is_frozen(&h, 7), "frozen");
v4_heat_on_call(&h, 7);
CHECK(v4_heat_call_get(&h, 7) == 1, "still 1");
CHECK(v4_heat_call_set(&h, 7, 100) == 0, "set returns 0 when frozen");
CHECK(v4_heat_call_get(&h, 7) == 1, "unchanged");
v4_heat_call_unfreeze(&h, 7);
CHECK(!v4_heat_call_is_frozen(&h, 7), "unfrozen");
CHECK(v4_heat_call_set(&h, 7, 42) == 1, "set allowed");
CHECK(v4_heat_call_get(&h, 7) == 42, "changed");
}
static void test_out_of_range_ignored(void)
{
v4_heat h;
v4_heat_reset(&h);
v4_heat_on_call(&h, (v4_cell)V4_HEAT_MAX_CALL_TARGETS + 5);
CHECK(v4_heat_call_get(&h, (v4_cell)V4_HEAT_MAX_CALL_TARGETS + 5) == 0,
"out of range call ignored");
CHECK(v4_heat_call_set(&h, (v4_cell)-1, 123) == 0, "set out of range");
CHECK(v4_heat_call_is_frozen(&h, (v4_cell)-1) == 0, "frozen check");
}
int main(void)
{
printf("v4 heat tests: V4_CELL_BITS=%d\n", V4_CELL_BITS);
test_reset();
test_retire_advances_anticlock_and_opcode();
test_call_advances_target();
test_freeze();
test_out_of_range_ignored();
printf(" %d checks, %d failures\n", checks, failures);
return failures ? 1 : 0;
}
File diff suppressed because it is too large Load Diff
+266
View File
@@ -0,0 +1,266 @@
/* test_node.c -- the node: registers, memory, and the boundary on memory.
*
* The memory boundary does a job the stack boundaries do not: it is the landing
* place for a *linear* index error -- mem[-1], mem[V4_NODE_WORDS] -- which is
* inside the struct and therefore invisible to AddressSanitizer.
*
* It explicitly does NOT cover an out-of-range *word address*, where P, A or B
* has left the address space. That lands gigabytes away, outside any band.
* DECOMPOSITION.md does not say what such an access should do, so no policy is
* invented here; instead the limitation is measured and asserted, so the claim
* in node.h cannot rot. Closing that hole is a ruling on out-of-range
* addressing, not a wider band.
*/
#include "v4/node.h"
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static int failures = 0;
static int checks = 0;
#define CHECK(cond, ...) \
do { \
checks++; \
if (!(cond)) { \
failures++; \
printf(" FAIL %s:%d: ", __FILE__, __LINE__); \
printf(__VA_ARGS__); \
printf("\n"); \
} \
} while (0)
static void test_geometry(void)
{
/* The boundary is 5% of memory at each end, rounded up, never zero. */
CHECK(V4_MEM_BOUND == V4_GUARD_ELEMS(V4_NODE_WORDS),
"memory boundary is not 5%% of %u words", V4_NODE_WORDS);
CHECK(V4_MEM_BOUND >= 1u, "memory boundary must be at least one word");
CHECK((unsigned long)V4_MEM_BOUND * 100u
>= (unsigned long)V4_NODE_WORDS * V4_GUARD_PCT,
"memory boundary %u is less than %u%% of %u", V4_MEM_BOUND,
V4_GUARD_PCT, V4_NODE_WORDS);
/* At the default size that is a real cost, and naming it here means the
* number is on the record rather than discovered from a memory report. */
printf(" node: %u words + %u head + %u tail boundary "
"(%.1f%% overhead)\n",
V4_NODE_WORDS, V4_MEM_BOUND, V4_MEM_BOUND,
100.0 * 2.0 * (double)V4_MEM_BOUND / (double)V4_NODE_WORDS);
}
static void test_reset_state(void)
{
v4_node n;
v4_node_reset(&n);
CHECK(n.p == 0, "P after reset");
CHECK(n.a == 0, "A after reset");
CHECK(n.b == 0, "B after reset");
CHECK(v4_node_guards_intact(&n), "all boundaries intact after reset");
/* T, S and R live in the stacks, so they are checked through the stack
* API rather than as node fields. */
CHECK(v4_dstack_peek(&n.ds) == 0, "T after reset");
CHECK(v4_dstack_peek2(&n.ds) == 0, "S after reset");
CHECK(v4_rstack_peek(&n.rs) == 0, "R after reset");
}
static void test_guards_absent_before_reset(void)
{
/* A node that has never been reset holds whatever was on the stack. The
* check must notice, which is what proves it reads the boundary rather than
* returning a constant. */
v4_node *n = (v4_node *)malloc(sizeof *n);
if (n == NULL) {
failures++;
printf(" FAIL %s:%d: out of memory\n", __FILE__, __LINE__);
return;
}
memset(n, 0xA5, sizeof *n);
CHECK(!v4_node_guards_intact(n),
"boundaries must not read as intact before reset");
v4_node_reset(n);
CHECK(v4_node_guards_intact(n), "boundaries intact after reset");
free(n);
}
static void test_load_store_roundtrip(void)
{
v4_node n;
v4_node_reset(&n);
/* Every word, not a sample: a stray boundary in the middle of memory would
* not be found by spot checks. */
for (v4_cell i = 0; i < (v4_cell)V4_NODE_WORDS; i++) {
v4_node_store(&n, i, i * 3 + 1);
}
for (v4_cell i = 0; i < (v4_cell)V4_NODE_WORDS; i++) {
CHECK(v4_node_load(&n, i) == i * 3 + 1,
"word %lld: got %lld want %lld", (long long)i,
(long long)v4_node_load(&n, i), (long long)(i * 3 + 1));
}
CHECK(v4_node_guards_intact(&n), "memory boundary survived a full sweep");
}
static void test_load_store_edges(void)
{
/* The first and last words, which are the ones adjacent to the boundary.
* A boundary that is a word too wide would quietly steal word 0 or the
* last word, and the memory would still pass every interior test. */
v4_node n;
v4_node_reset(&n);
v4_node_store(&n, 0, 0x11111111);
v4_node_store(&n, (v4_cell)V4_NODE_WORDS - 1, 0x22222222);
CHECK(v4_node_load(&n, 0) == 0x11111111, "word 0 did not take its value");
CHECK(v4_node_load(&n, (v4_cell)V4_NODE_WORDS - 1) == 0x22222222,
"last word did not take its value");
CHECK(v4_node_guards_intact(&n), "edge writes disturbed the boundary");
}
static void test_boundary_is_adjacent_to_memory(void)
{
/* The band only catches a linear index error if it is immediately next to
* mem, so adjacency is asserted rather than assumed -- the compiler is free
* to reorder struct members, and a band that drifted to the far end of the
* struct would silently stop catching anything. */
CHECK(offsetof(v4_node, mem_guard_tail)
== offsetof(v4_node, mem) + sizeof(((v4_node *)0)->mem),
"tail boundary does not start immediately after memory");
CHECK(offsetof(v4_node, mem_guard_head)
+ sizeof(((v4_node *)0)->mem_guard_head)
== offsetof(v4_node, mem),
"head boundary does not end immediately before memory");
}
static void test_linear_overrun_low_is_caught(void)
{
/* A linear index error -- the kind an off-by-one in a loop bound or a
* pointer step produces. It lands in the head boundary, it is inside the
* struct so AddressSanitizer says nothing about it, and the boundary check
* is what reports it. Written through the boundary array rather than as
* mem[-1], because mem[-1] is out of bounds of a declared array and the
* standard lets the compiler do anything with it; the offsetof test above
* establishes that this *is* the word mem[-1] resolves to. */
v4_node n;
v4_node_reset(&n);
CHECK(v4_node_guards_intact(&n), "intact before the deliberate overrun");
n.mem_guard_head[V4_MEM_BOUND - 1u] = (v4_cell)0xDEADBEEF;
CHECK(!v4_node_guards_intact(&n),
"a linear access before mem was not detected");
v4_node_reset(&n);
CHECK(v4_node_guards_intact(&n), "intact after re-reset");
}
static void test_linear_overrun_high_is_caught(void)
{
v4_node n;
v4_node_reset(&n);
n.mem_guard_tail[0] = (v4_cell)0xDEADBEEF;
CHECK(!v4_node_guards_intact(&n),
"a linear access past the last word was not detected");
v4_node_reset(&n);
CHECK(v4_node_guards_intact(&n), "intact after re-reset");
}
static void test_out_of_range_word_address_is_not_covered_by_the_band(void)
{
/* What the band does NOT catch, asserted so the limitation stays visible
* and cannot be quietly forgotten.
*
* An out-of-range *word address* is a different animal from a linear index
* error. A v4_cell address of -1 becomes unsigned 0xFFFFFFFF, which is
* 2^32 words past mem -- 16 GB at 32-bit cells. That is far outside the
* struct, so no boundary can see it. The band was briefly documented as
* catching this; it does not, and the protection is a range check whose
* policy DECOMPOSITION.md does not define. This test records the gap by
* measuring it, so the number in node.h stays true. */
v4_cell neg = (v4_cell)-1;
uint64_t words_out = (uint64_t)(v4_ucell)neg;
uint64_t far = words_out * (uint64_t)sizeof(v4_cell);
printf(" out-of-range word address -1 lands %llu bytes past mem "
"(%.1f GB at this width): outside the band, as documented\n",
(unsigned long long)far, (double)far / 1073741824.0);
CHECK(far > (uint64_t)sizeof(v4_node),
"an out-of-range address should land outside the node");
/* The band is only V4_MEM_BOUND words, so even a modest overrun is caught
* only while it is within the band. Past that it is not, which is why the
* open question is a range check and not a wider band. */
CHECK(V4_MEM_BOUND < V4_NODE_WORDS / 2u,
"if the band were most of memory, this reasoning would be wrong");
}
static void test_ends_are_distinguishable(void)
{
/* The two boundaries carry different patterns precisely so that a failure
* says which end. Checked here on memory as well as in test_guard.c on the
* ring, because "which end" is the property that makes a report actionable. */
v4_node n;
v4_node_reset(&n);
CHECK((v4_ucell)n.mem_guard_head[0] == V4_GUARD_PATTERN_HEAD,
"memory head boundary pattern");
CHECK((v4_ucell)n.mem_guard_tail[0] == V4_GUARD_PATTERN_TAIL,
"memory tail boundary pattern");
CHECK(V4_GUARD_PATTERN_HEAD != V4_GUARD_PATTERN_TAIL,
"head and tail patterns are equal at V4_CELL_BITS=%d", V4_CELL_BITS);
}
static void test_workload_never_false_alarms(void)
{
/* A boundary that fires on legitimate use is worse than no boundary. Drive
* a workload across the whole address space, through both address registers
* and the program counter, and require every boundary to survive. */
v4_node n;
v4_node_reset(&n);
uint64_t s = 0xB5026F5AA96619E9ull;
for (int i = 0; i < 200000; i++) {
s ^= s << 13; s ^= s >> 7; s ^= s << 17;
v4_cell addr = (v4_cell)(s % (uint64_t)V4_NODE_WORDS);
v4_node_store(&n, addr, (v4_cell)s);
n.a = addr;
n.b = (v4_cell)((addr + 1u) % V4_NODE_WORDS);
n.p = (v4_cell)((addr + 2u) % V4_NODE_WORDS);
v4_dstack_push(&n.ds, (v4_cell)s);
v4_rstack_push(&n.rs, (v4_cell)(s >> 13));
if (i & 1) { (void)v4_dstack_pop(&n.ds); (void)v4_rstack_pop(&n.rs); }
}
CHECK(v4_node_guards_intact(&n),
"a boundary fired during ordinary workload");
CHECK(n.p < (v4_cell)V4_NODE_WORDS, "P left the address space");
CHECK(n.a < (v4_cell)V4_NODE_WORDS, "A left the address space");
CHECK(n.b < (v4_cell)V4_NODE_WORDS, "B left the address space");
}
int main(void)
{
printf("v4 node tests: V4_CELL_BITS=%d, %u words\n",
V4_CELL_BITS, V4_NODE_WORDS);
test_geometry();
test_reset_state();
test_guards_absent_before_reset();
test_load_store_roundtrip();
test_load_store_edges();
test_boundary_is_adjacent_to_memory();
test_linear_overrun_low_is_caught();
test_linear_overrun_high_is_caught();
test_out_of_range_word_address_is_not_covered_by_the_band();
test_ends_are_distinguishable();
test_workload_never_false_alarms();
printf(" %d checks, %d failures\n", checks, failures);
return failures ? 1 : 0;
}
File diff suppressed because it is too large Load Diff
+70
View File
@@ -0,0 +1,70 @@
/* test_umul.c -- the reference unsigned multiply.
*
* v4_umul is what the capsule-level UM* is checked against, so it is checked
* here against things that do not depend on it: algebraic identities that hold
* at any width, and at 32-bit cells the host's own 64-bit product.
*/
#include "v4/umul.h"
#include <stdio.h>
static int failures = 0, checks = 0;
#define CHECK(c,...) do{checks++; if(!(c)){failures++; printf("FAIL %s:%d: ",__FILE__,__LINE__); printf(__VA_ARGS__); printf("\n");}}while(0)
#define MAXU ((v4_ucell)~(v4_ucell)0)
static const v4_ucell vec[] = {
0u, 1u, 2u, 3u, 7u, 255u, 256u, 65535u, 65536u, 0x12345678u,
(v4_ucell)0x9E3779B97F4A7C15ULL, (v4_ucell)0xDEADBEEFCAFEF00DULL,
V4_MSB - 1u, V4_MSB, V4_MSB + 1u, MAXU - 1u, MAXU
};
#define NVEC (sizeof vec / sizeof vec[0])
int main(void)
{
v4_ucell lo, hi, lo2, hi2;
printf("v4 umul tests: V4_CELL_BITS=%d\n", V4_CELL_BITS);
v4_umul(3, 7, &lo, &hi); CHECK(lo == 21 && hi == 0, "3*7");
/* (2^n - 1)^2 = 2^2n - 2^(n+1) + 1 */
v4_umul(MAXU, MAXU, &lo, &hi); CHECK(lo == 1 && hi == MAXU - 1u, "max*max");
/* 2^(n-1) * 2^(n-1) = 2^(2n-2) */
v4_umul(V4_MSB, V4_MSB, &lo, &hi);
CHECK(lo == 0 && hi == (V4_MSB >> 1), "msb*msb");
for (unsigned i = 0; i < NVEC; i++) {
v4_ucell x = vec[i];
v4_umul(x, 0, &lo, &hi); CHECK(lo == 0 && hi == 0, "x*0 [%u]", i);
v4_umul(x, 1, &lo, &hi); CHECK(lo == x && hi == 0, "x*1 [%u]", i);
/* x * (2^n - 1) = x*2^n - x */
v4_umul(x, MAXU, &lo, &hi);
CHECK(lo == (v4_ucell)(0u - x) && hi == (x ? x - 1u : 0u), "x*max [%u]", i);
for (unsigned k = 0; k < V4_CELL_BITS; k++) {
/* x * 2^k is x shifted across the double cell */
v4_umul(x, ((v4_ucell)1) << k, &lo, &hi);
CHECK(lo == (v4_ucell)(x << k), "x<<%u lo [%u]", k, i);
CHECK(hi == (k ? x >> (V4_CELL_BITS - k) : 0u), "x<<%u hi [%u]", k, i);
}
for (unsigned j = 0; j < NVEC; j++) {
v4_ucell y = vec[j];
v4_umul(x, y, &lo, &hi);
v4_umul(y, x, &lo2, &hi2);
CHECK(lo == lo2 && hi == hi2, "commutes [%u,%u]", i, j);
CHECK(lo == (v4_ucell)(x * y), "low cell [%u,%u]", i, j);
#if V4_CELL_BITS == 32
{
uint64_t p = (uint64_t)x * (uint64_t)y;
CHECK(lo == (uint32_t)p && hi == (uint32_t)(p >> 32),
"host product [%u,%u]", i, j);
}
#endif
}
}
printf(" %d checks, %d failures\n", checks, failures);
return failures ? 1 : 0;
}