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;
}
+339
View File
@@ -0,0 +1,339 @@
/* test_exec.c -- every opcode of DECOMPOSITION.md section 1.3, and the slot
* sequencing of section 1.2.
*
* Each expected value below is worked out from the table in section 1.3, not
* from the executor. Words are packed by hand with v4_iword_assemble so that
* this file does not depend on asm.c, which has its own test.
*/
#include "v4/exec.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 NOP V4_OP_NOP
#define MINC ((v4_cell)V4_MSB) /* most negative cell */
#define MAXC ((v4_cell)(V4_MSB - 1u)) /* most positive cell */
static v4_node n;
static v4_exec_state es;
static v4_heat h;
static void fresh(void)
{
v4_node_reset(&n);
v4_exec_reset(&es);
v4_heat_reset(&h);
}
static void dpush(v4_cell x) { v4_dstack_push(&n.ds, x); }
static void rpush(v4_cell x) { v4_rstack_push(&n.rs, x); }
/* A word of six plain opcodes. */
static v4_cell word6(unsigned a, unsigned b, unsigned c,
unsigned d, unsigned e, unsigned f)
{
unsigned op[V4_SLOT_COUNT];
op[0] = a; op[1] = b; op[2] = c; op[3] = d; op[4] = e; op[5] = f;
return (v4_cell)v4_iword_assemble(op, 0);
}
/* A word with `npre` nops, then a branch in the next slot, then its target. */
static v4_cell bword(unsigned npre, unsigned op, v4_iword target)
{
v4_iword w = 0;
for (unsigned k = 0; k < npre; k++) w |= (v4_iword)NOP << V4_SLOT_LOW_BIT(k);
w |= (v4_iword)op << V4_SLOT_LOW_BIT(npre);
w |= target & v4_iword_slot_mask(npre);
return (v4_cell)w;
}
/* Run one opcode, padded with nops, on the current stacks. Returns the count
* of instructions retired. */
static unsigned run1(unsigned op)
{
n.mem[0] = word6(op, NOP, NOP, NOP, NOP, NOP);
n.p = 0;
return v4_exec_step_word(&n, &es, &h);
}
static void test_stack_ops(void)
{
fresh(); dpush(1); dpush(2); run1(V4_OP_DUP);
CHECK(n.ds.t == 2 && n.ds.s == 2, "dup");
(void)v4_dstack_pop(&n.ds);
CHECK(n.ds.t == 2 && n.ds.s == 1, "dup keeps what was below");
fresh(); dpush(1); dpush(2); run1(V4_OP_OVER);
CHECK(n.ds.t == 1 && n.ds.s == 2, "over");
(void)v4_dstack_pop(&n.ds);
CHECK(n.ds.t == 2 && n.ds.s == 1, "over keeps what was below");
fresh(); dpush(1); dpush(2); dpush(3); run1(V4_OP_DROP);
CHECK(n.ds.t == 2 && n.ds.s == 1, "drop");
fresh(); dpush(1); dpush(2); CHECK(run1(NOP) == 6, "nop word retires six");
CHECK(n.ds.t == 2 && n.ds.s == 1 && n.p == 1, "nop changes nothing but P");
}
static void test_alu(void)
{
fresh(); dpush(9); dpush(3); dpush(4); run1(V4_OP_ADD);
CHECK(n.ds.t == 7 && n.ds.s == 9, "3 + 4, and 9 moves up to S");
fresh(); dpush(MAXC); dpush(1); run1(V4_OP_ADD);
CHECK(n.ds.t == MINC, "+ wraps");
fresh(); dpush(9); dpush(12); dpush(10); run1(V4_OP_AND);
CHECK(n.ds.t == 8 && n.ds.s == 9, "and");
fresh(); dpush(9); dpush(12); dpush(10); run1(V4_OP_XOR);
CHECK(n.ds.t == 6 && n.ds.s == 9, "xor");
fresh(); dpush(5); dpush(0); run1(V4_OP_INV);
CHECK(n.ds.t == -1 && n.ds.s == 5, "inv 0");
fresh(); dpush(MAXC); run1(V4_OP_INV);
CHECK(n.ds.t == MINC, "inv max");
fresh(); dpush(3); run1(V4_OP_TWO_STAR); CHECK(n.ds.t == 6, "2* 3");
fresh(); dpush(-1); run1(V4_OP_TWO_STAR); CHECK(n.ds.t == -2, "2* -1");
fresh(); dpush(MINC); run1(V4_OP_TWO_STAR); CHECK(n.ds.t == 0, "2* drops the top bit");
fresh(); dpush(6); run1(V4_OP_TWO_SLASH); CHECK(n.ds.t == 3, "2/ 6");
fresh(); dpush(-1); run1(V4_OP_TWO_SLASH); CHECK(n.ds.t == -1, "2/ -1 is -1, not 0");
fresh(); dpush(-7); run1(V4_OP_TWO_SLASH); CHECK(n.ds.t == -4, "2/ -7 floors");
fresh(); dpush(MINC); run1(V4_OP_TWO_SLASH);
CHECK((v4_ucell)n.ds.t == (V4_MSB | (V4_MSB >> 1)), "2/ keeps the sign bit");
}
static void test_mul_step(void)
{
/* A even: no add. */
fresh(); dpush(5); dpush(6); n.a = 4; run1(V4_OP_MUL_STEP);
CHECK(n.ds.t == 3 && n.ds.s == 5 && n.a == 2, "+* A even");
/* A odd: T+S = 11, shifted to 5, the 1 shifted out enters the top of A. */
fresh(); dpush(5); dpush(6); n.a = 5; run1(V4_OP_MUL_STEP);
CHECK(n.ds.t == 5 && n.ds.s == 5, "+* A odd, T");
CHECK((v4_ucell)n.a == (V4_MSB | 2u), "+* A odd, A");
fresh(); dpush(0); dpush(3); n.a = 0; run1(V4_OP_MUL_STEP);
CHECK(n.ds.t == 1 && (v4_ucell)n.a == V4_MSB, "+* T bit 0 enters A");
fresh(); dpush(0); dpush(-2); n.a = 0; run1(V4_OP_MUL_STEP);
CHECK(n.ds.t == -1 && n.a == 0, "+* keeps T's sign bit");
/* D-3: all-ones + 1 carries out of T and the carry is gone. */
fresh(); dpush(1); dpush(-1); n.a = 1; run1(V4_OP_MUL_STEP);
CHECK(n.ds.t == 0 && n.a == 0, "+* does not keep the carry (D-3)");
}
static void test_registers_and_rstack(void)
{
fresh(); dpush(4); dpush(5); run1(V4_OP_PUSH);
CHECK(n.rs.r == 5 && n.ds.t == 4, "push");
run1(V4_OP_RPOP);
CHECK(n.ds.t == 5 && n.ds.s == 4, "pop");
fresh(); dpush(4); dpush(77); run1(V4_OP_BANG_A);
CHECK(n.a == 77 && n.ds.t == 4, "a!");
run1(V4_OP_PUSH_A);
CHECK(n.ds.t == 77 && n.ds.s == 4 && n.a == 77, "a");
fresh(); dpush(4); dpush(88); run1(V4_OP_BANG_B);
CHECK(n.b == 88 && n.ds.t == 4 && n.a == 0, "b!");
}
static void test_memory(void)
{
fresh(); n.a = 100; n.b = 101; n.mem[100] = 11; n.mem[101] = 22;
run1(V4_OP_FETCH_A); CHECK(n.ds.t == 11 && n.a == 100, "@");
run1(V4_OP_FETCH_B); CHECK(n.ds.t == 22 && n.ds.s == 11 && n.b == 101, "@b");
run1(V4_OP_FETCH_INC); CHECK(n.ds.t == 11 && n.a == 101, "@+ first");
run1(V4_OP_FETCH_INC); CHECK(n.ds.t == 22 && n.a == 102, "@+ second");
fresh(); n.a = 100; n.b = 200; dpush(1); dpush(5);
run1(V4_OP_STORE_A);
CHECK(n.mem[100] == 5 && n.ds.t == 1 && n.a == 100, "!");
dpush(6); run1(V4_OP_STORE_INC);
CHECK(n.mem[100] == 6 && n.ds.t == 1 && n.a == 101, "!+");
dpush(7); run1(V4_OP_STORE_B);
CHECK(n.mem[200] == 7 && n.ds.t == 1 && n.b == 200, "!b");
/* @p takes the word after the instruction word and steps over it. */
fresh(); n.mem[1] = (v4_cell)0x1122334455667788LL;
run1(V4_OP_FETCH_P);
CHECK(n.ds.t == n.mem[1] && n.p == 2, "@p is a full cell and advances P");
fresh(); dpush(1); dpush(9); run1(V4_OP_STORE_P);
CHECK(n.mem[1] == 9 && n.ds.t == 1 && n.p == 2, "!p");
/* Two literals in one word are the two words after it, in order. */
fresh(); n.mem[0] = word6(V4_OP_FETCH_P, V4_OP_FETCH_P, V4_OP_ADD, NOP, NOP, NOP);
n.mem[1] = 30; n.mem[2] = 12; n.p = 0;
(void)v4_exec_step_word(&n, &es, &h);
CHECK(n.ds.t == 42 && n.p == 3, "two literals");
}
static void test_return_and_ex(void)
{
fresh(); rpush(7); rpush(50);
n.mem[0] = word6(V4_OP_SEMI, V4_OP_DUP, V4_OP_DUP, V4_OP_DUP, V4_OP_DUP, V4_OP_DUP);
n.p = 0;
CHECK(v4_exec_step_word(&n, &es, &h) == 1, "; ends the word");
CHECK(n.p == 50 && n.rs.r == 7, "; returns and pops R");
CHECK(h.op[V4_OP_DUP] == 0 && es.anticlock == 1, "nothing after ; retires");
fresh(); rpush(7); rpush(50);
n.mem[20] = word6(V4_OP_EX, V4_OP_DUP, NOP, NOP, NOP, NOP);
n.p = 20;
CHECK(v4_exec_step_word(&n, &es, &h) == 1, "ex ends the word");
CHECK(n.p == 50 && n.rs.r == 21, "ex swaps P and R");
(void)v4_rstack_pop(&n.rs);
CHECK(n.rs.r == 7, "ex does not change the return stack depth");
}
static void test_branches(void)
{
/* jump in each legal slot; the slots before it execute. */
for (unsigned slot = 0; slot < 4; slot++) {
fresh(); n.mem[10] = bword(slot, V4_OP_JUMP, 300); n.p = 10;
CHECK(v4_exec_step_word(&n, &es, &h) == slot + 1, "jump slot %u count", slot);
CHECK(n.p == 300, "jump slot %u", slot);
CHECK(h.op[NOP] == slot && h.op[V4_OP_JUMP] == 1, "jump slot %u heat", slot);
}
fresh(); rpush(7); n.mem[10] = bword(0, V4_OP_CALL, 200); n.p = 10;
(void)v4_exec_step_word(&n, &es, &h);
CHECK(n.p == 200 && n.rs.r == 11, "call pushes the return address");
CHECK(v4_heat_call_get(&h, 200) == 1 && h.op[V4_OP_CALL] == 1, "call heats its target");
(void)v4_rstack_pop(&n.rs);
CHECK(n.rs.r == 7, "call pushes exactly one");
fresh(); rpush(99); rpush(2); n.mem[5] = bword(0, V4_OP_NEXT, 40); n.p = 5;
(void)v4_exec_step_word(&n, &es, &h);
CHECK(n.p == 40 && n.rs.r == 1, "next, R nonzero");
fresh(); rpush(99); rpush(0); n.mem[5] = bword(0, V4_OP_NEXT, 40); n.p = 5;
(void)v4_exec_step_word(&n, &es, &h);
CHECK(n.p == 6 && n.rs.r == 99, "next, R zero");
fresh(); dpush(8); dpush(0); n.mem[5] = bword(0, V4_OP_IF, 40); n.p = 5;
(void)v4_exec_step_word(&n, &es, &h);
CHECK(n.p == 40 && n.ds.t == 0 && n.ds.s == 8, "if, T zero: taken, T kept");
/* The target bits 0x0FFFF would decode as opcodes if they were executed. */
fresh(); dpush(8); dpush(5); n.mem[5] = bword(0, V4_OP_IF, 0x0FFFF); n.p = 5;
CHECK(v4_exec_step_word(&n, &es, &h) == 1, "if not taken still ends the word");
CHECK(n.p == 6 && n.ds.t == 5 && n.ds.s == 8, "if, T nonzero: not taken, T kept");
fresh(); dpush(8); dpush(5); n.mem[5] = bword(0, V4_OP_MINUS_IF, 40); n.p = 5;
(void)v4_exec_step_word(&n, &es, &h);
CHECK(n.p == 40 && n.ds.t == 5 && n.ds.s == 8, "-if, T positive: taken");
fresh(); dpush(0); n.mem[5] = bword(0, V4_OP_MINUS_IF, 40); n.p = 5;
(void)v4_exec_step_word(&n, &es, &h);
CHECK(n.p == 40, "-if, T zero: taken");
fresh(); dpush(8); dpush(-1); n.mem[5] = bword(0, V4_OP_MINUS_IF, 40); n.p = 5;
(void)v4_exec_step_word(&n, &es, &h);
CHECK(n.p == 6 && n.ds.t == -1 && n.ds.s == 8, "-if, T negative: not taken");
}
static void test_unext(void)
{
/* R = 3: the body runs four times, then the rest of the word runs once. */
fresh(); dpush(1); rpush(99); rpush(3);
n.mem[0] = word6(V4_OP_TWO_STAR, V4_OP_UNEXT, NOP, NOP, NOP, NOP); n.p = 0;
CHECK(v4_exec_step_word(&n, &es, &h) == 12, "unext retire count");
CHECK(n.ds.t == 16, "unext body ran R+1 times");
CHECK(n.rs.r == 99, "unext pops R when done");
CHECK(n.p == 1, "unext leaves P at the next word");
CHECK(h.op[V4_OP_TWO_STAR] == 4 && h.op[V4_OP_UNEXT] == 4 && h.op[NOP] == 4,
"unext heat");
CHECK(es.anticlock == 12, "unext anticlock");
fresh(); dpush(1); rpush(99); rpush(0);
n.mem[0] = word6(V4_OP_TWO_STAR, V4_OP_UNEXT, NOP, NOP, NOP, NOP); n.p = 0;
CHECK(v4_exec_step_word(&n, &es, &h) == 6, "unext with R zero runs once");
CHECK(n.ds.t == 2 && n.rs.r == 99, "unext with R zero");
/* The word is not refetched and P is not rewound, so @p walks forward. */
fresh(); rpush(99); rpush(2); n.a = 100;
n.mem[0] = word6(V4_OP_FETCH_P, V4_OP_STORE_INC, V4_OP_UNEXT, NOP, NOP, NOP);
n.mem[1] = 11; n.mem[2] = 22; n.mem[3] = 33; n.p = 0;
(void)v4_exec_step_word(&n, &es, &h);
CHECK(n.mem[100] == 11 && n.mem[101] == 22 && n.mem[102] == 33,
"@p in a unext loop copies successive words");
CHECK(n.a == 103 && n.p == 4 && n.rs.r == 99, "@p in a unext loop, registers");
}
static void test_retirement(void)
{
fresh();
for (unsigned op = 0; op < V4_OPCODE_COUNT; op++) v4_exec_op(&n, &es, &h, op, 0, 0);
for (unsigned op = 0; op < V4_OPCODE_COUNT; op++)
CHECK(h.op[op] == 1, "opcode %u retires once", op);
CHECK(es.anticlock == V4_OPCODE_COUNT, "one anticlock tick per opcode");
v4_exec_op(&n, &es, &h, V4_OPCODE_COUNT, 0, 0);
CHECK(es.anticlock == V4_OPCODE_COUNT, "a non-opcode retires nothing");
}
#if V4_CELL_BITS == 64
static void test_iword_in_wide_cell(void)
{
/* D-9: the instruction word is the low 32 bits of the cell. The high half
* is not decoded and does not reach a branch target. */
v4_cell junk = (v4_cell)0xA5A5A5A500000000ULL;
fresh(); dpush(3);
n.mem[0] = junk | word6(V4_OP_DUP, V4_OP_ADD, NOP, NOP, NOP, NOP); n.p = 0;
CHECK(v4_exec_step_word(&n, &es, &h) == 6, "wide cell: six slots");
CHECK(n.ds.t == 6, "wide cell: decode ignores the high half");
fresh(); n.mem[10] = junk | bword(0, V4_OP_JUMP, 300); n.p = 10;
(void)v4_exec_step_word(&n, &es, &h);
CHECK(n.p == 300, "wide cell: branch ignores the high half");
}
#endif
/* Many opcodes on arbitrary values. The point is the sanitizer build: no
* opcode may overflow a signed cell or shift out of range, whatever is on the
* stacks. Memory opcodes are left out because A, B and P would be arbitrary. */
static void test_soak(void)
{
static const unsigned safe[] = {
V4_OP_MUL_STEP, V4_OP_TWO_STAR, V4_OP_TWO_SLASH, V4_OP_INV, V4_OP_ADD,
V4_OP_AND, V4_OP_XOR, V4_OP_DROP, V4_OP_DUP, V4_OP_RPOP, V4_OP_OVER,
V4_OP_PUSH_A, V4_OP_NOP, V4_OP_PUSH, V4_OP_BANG_B, V4_OP_BANG_A
};
uint64_t seed = 0x9E3779B97F4A7C15ULL;
const unsigned count = 200000;
fresh();
dpush(MINC); dpush(MAXC); dpush(-1); dpush(1);
for (unsigned i = 0; i < count; i++) {
seed = seed * 6364136223846793005ULL + 1442695040888963407ULL;
if ((seed >> 60) == 0) dpush((v4_cell)(seed >> 7));
v4_exec_op(&n, &es, &h, safe[(seed >> 33) % (sizeof safe / sizeof safe[0])], 0, 0);
}
CHECK(es.anticlock == count, "soak retired every opcode");
CHECK(v4_node_guards_intact(&n), "soak left the guards intact");
}
int main(void)
{
printf("v4 exec tests: V4_CELL_BITS=%d\n", V4_CELL_BITS);
test_stack_ops();
test_alu();
test_mul_step();
test_registers_and_rstack();
test_memory();
test_return_and_ex();
test_branches();
test_unext();
test_retirement();
#if V4_CELL_BITS == 64
test_iword_in_wide_cell();
#endif
test_soak();
printf(" %d checks, %d failures\n", checks, failures);
return failures ? 1 : 0;
}
+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;
}
+400
View File
@@ -0,0 +1,400 @@
/* test_iword.c -- the instruction-word format.
*
* The properties pinned here are the ones the rest of the machine is built on
* and the ones most likely to be got wrong by a plausible-looking edit:
*
* - the six slots occupy the bit ranges the diagram in section 1.2 gives
* them, with slot 0 highest;
* - the reach column (27 / 22 / 17 / 12 for slots 0-3) is what the layout
* actually produces, rather than four independently-entered constants;
* - branches are illegal in slots 4 and 5;
* - the two spare bits are inert, so a word that differs only in them
* decodes identically;
* - a branch is page-relative: it replaces the low bits of P and preserves
* the rest;
* - no opcode name collides with a FORTH-79 word name, which is the stated
* reason for the `inv` and `xor` renames in section 1.3.
*/
#include "v4/iword.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 v4_iword word_of(const unsigned op[6], unsigned spare)
{
return v4_iword_assemble(op, spare);
}
static void test_slot_bit_ranges(void)
{
/* The diagram's own boundaries, entered independently of the macros so the
* macros are checked rather than merely used. */
static const unsigned want_high[6] = { 31, 26, 21, 16, 11, 6 };
static const unsigned want_low[6] = { 27, 22, 17, 12, 7, 2 };
for (unsigned k = 0; k < 6; k++) {
CHECK(V4_SLOT_HIGH_BIT(k) == want_high[k],
"slot %u high bit: macro says %u, diagram says %u", k,
V4_SLOT_HIGH_BIT(k), want_high[k]);
CHECK(V4_SLOT_LOW_BIT(k) == want_low[k],
"slot %u low bit: macro says %u, diagram says %u", k,
V4_SLOT_LOW_BIT(k), want_low[k]);
}
/* Each slot is 5 bits, and the six slots do not overlap. */
for (unsigned k = 0; k < 6; k++) {
CHECK(V4_SLOT_HIGH_BIT(k) - V4_SLOT_LOW_BIT(k) == 4u,
"slot %u is not 5 bits wide", k);
}
CHECK(V4_SLOT_LOW_BIT(5) - V4_SPARE_BITS == 0u,
"slot 5 does not end just above the spare bits");
}
static void test_roundtrip_all_opcodes(void)
{
/* Every one of the 32 opcodes, in every slot, survives assemble+decode. */
for (unsigned op = 0; op < 32u; op++) {
for (unsigned k = 0; k < 6; k++) {
unsigned o[6];
for (unsigned j = 0; j < 6; j++) o[j] = 0x1Du; /* distinct filler */
o[k] = op;
v4_iword w = word_of(o, 0u);
CHECK(w != 0xFFFFFFFFu, "assemble rejected opcode %u", op);
CHECK(v4_iword_op(w, k) == op,
"opcode %u in slot %u decoded as %u", op, k,
v4_iword_op(w, k));
/* And the other slots are undisturbed. */
for (unsigned j = 0; j < 6; j++) {
if (j == k) continue;
CHECK(v4_iword_op(w, j) == 0x1Du,
"assembling %u into slot %u disturbed slot %u", op, k, j);
}
}
}
}
static void test_slots_are_independent(void)
{
/* All 32^6 words cannot be enumerated, but the packing is linear, so
* checking that each slot's bits land only in its own range is enough. */
for (unsigned k = 0; k < 6; k++) {
unsigned o[6] = { 0, 0, 0, 0, 0, 0 };
o[k] = 0x1Fu; /* all five bits set */
v4_iword w = word_of(o, 0u);
v4_iword expect = (v4_iword)0x1Fu << V4_SLOT_LOW_BIT(k);
CHECK(w == expect,
"slot %u: word is %08lX, expected %08lX", k,
(unsigned long)w, (unsigned long)expect);
}
}
static void test_spare_bits_are_inert_for_decode(void)
{
/* The two spare low bits sit below every slot, so they are never executed.
* A word that differs only in them must decode to the same six opcodes --
* otherwise the spare bits would have acquired decode meaning. */
unsigned o[6] = { 2, 3, 5, 6, 7, 1 };
v4_iword a = word_of(o, 0u);
v4_iword b = word_of(o, 3u); /* both spare patterns */
for (unsigned k = 0; k < 6; k++) {
CHECK(v4_iword_op(a, k) == v4_iword_op(b, k),
"spare bits changed the opcode in slot %u", k);
}
CHECK(v4_iword_spare(a) == 0u, "spare bits of word a");
CHECK(v4_iword_spare(b) == 3u, "spare bits of word b");
}
static void test_spare_bits_are_branch_address_bits(void)
{
/* The opposite property, and the one that is easy to get wrong: the spare
* bits are BELOW every slot, so "all bits to the right of its slot" -- the
* branch target, per section 1.2 -- includes them. They are the low bits
* of a branch address, not padding. Pinned here so that a later "cleanup"
* that treats them as unusable address bits fails a test instead of costing
* two bits of reach off every branch in the machine. */
/* Only slot 0 is set, so every bit below it -- all 27 of them, spare bits
* included -- is zero and the branch target is the spare field alone. */
unsigned o[6] = { 2, 0, 0, 0, 0, 0 };
v4_iword a = word_of(o, 0u);
v4_iword b = word_of(o, 3u); /* both spare patterns */
CHECK(v4_iword_op(a, 0) == V4_OP_JUMP, "slot 0 should be jump");
CHECK(v4_iword_target(a, 0u) == 0u,
"word a (spare 0) should have a zero low target field, got %u",
v4_iword_target(a, 0u));
CHECK(v4_iword_target(b, 0u) == 3u,
"word b (spare 3) should carry 3 in the low address bits, got %u",
v4_iword_target(b, 0u));
CHECK(v4_iword_branch((v4_cell)0, a, 0u) == 0, "branch with spare 0");
CHECK(v4_iword_branch((v4_cell)0, b, 0u) == 3,
"branch with spare 3 should land on address 3, got %lld",
(long long)v4_iword_branch((v4_cell)0, b, 0u));
/* Every legal branch slot reaches bit 0, so the spare bits are address in
* all four of them. */
for (unsigned k = 0; k < 4; k++) {
CHECK((v4_iword_slot_mask(k) & 3u) == 3u,
"slot %u branch target does not reach the spare bits", k);
}
}
static void test_reach_column(void)
{
/* The reach table from section 1.2, entered as literals. The
* implementation derives these from the layout; this asserts the two
* agree, which is the whole reason to derive rather than hard-code. */
static const unsigned want_bits[4] = { 27, 22, 17, 12 };
static const unsigned want_reach[4] = {
0x8000000u, /* 2^27 words = whole address space */
0x400000u, /* 2^22 words = 4M words */
0x20000u, /* 2^17 words = 128K words */
0x1000u /* 2^12 words = 4K words */
};
for (unsigned k = 0; k < 4; k++) {
CHECK(V4_BRANCH_ADDR_BITS(k) == want_bits[k],
"slot %u address bits: %u, spec says %u", k,
V4_BRANCH_ADDR_BITS(k), want_bits[k]);
CHECK(v4_iword_slot_mask(k) + 1u == want_reach[k],
"slot %u reach: %u words, spec table implies %u", k,
v4_iword_slot_mask(k) + 1u, want_reach[k]);
}
}
static void test_branch_targets_take_all_low_bits(void)
{
/* A target is every bit to the right of the slot, not just the low 5 or the
* low 12. Set a pattern of all-ones in the low region and confirm each
* slot recovers exactly its own low bits. */
for (unsigned k = 0; k < 4; k++) {
v4_iword w = 0xFFFFFFFFu; /* every slot and spare bit set */
uint32_t t = v4_iword_target(w, k);
uint32_t mask = v4_iword_slot_mask(k);
CHECK(t == mask,
"slot %u target from all-ones word is %08lX, expected %08lX", k,
(unsigned long)t, (unsigned long)mask);
/* And the slot's own five bits must be excluded from its target. */
unsigned op_in_slot = v4_iword_op(w, k);
CHECK(op_in_slot == 0x1Fu, "slot %u opcode from all-ones", k);
}
}
static void test_branch_legality(void)
{
for (unsigned k = 0; k < 4; k++) {
CHECK(v4_iword_branch_legal(k), "slot %u should allow branches", k);
}
for (unsigned k = 4; k < 6; k++) {
CHECK(!v4_iword_branch_legal(k),
"slot %u must not allow branches (section 1.2)", k);
}
}
static void test_page_relative_branch(void)
{
/* "That target replaces the same low bits of P (page-relative, as in the
* F18)." A branch inside one page must not disturb the bits above it, and
* must be able to reach every address in its page. */
for (unsigned k = 0; k < 4; k++) {
uint32_t mask = v4_iword_slot_mask(k);
v4_ucell page = ~(v4_ucell)mask; /* a high value, low bits zero */
v4_cell p = (v4_cell)page;
for (uint32_t target = 0; target <= mask; target = target * 2u + 1u) {
v4_iword w = target; /* target lives in the low bits */
v4_cell got = v4_iword_branch(p, w, k);
v4_ucell want = ((v4_ucell)page & ~(v4_ucell)mask)
| (v4_ucell)target;
CHECK((v4_ucell)got == want,
"slot %u: P=%lld target=%u gave %lld, expected %llu", k,
(long long)p, target, (long long)got,
(unsigned long long)want);
CHECK((v4_ucell)got == (v4_ucell)(page ^ target),
"slot %u: branch is not page-relative", k);
if (target > mask / 2u && target != mask) break;
}
}
}
static void test_page_relative_branch_preserves_high_page(void)
{
/* Same, from several distinct pages, to be sure nothing is hard-wired to
* page zero. */
static const v4_ucell pages[] = {
(v4_ucell)0,
(v4_ucell)1,
(v4_ucell)0x80000000ull,
~(v4_ucell)0
};
for (unsigned k = 0; k < 4; k++) {
uint32_t mask = v4_iword_slot_mask(k);
for (unsigned i = 0; i < sizeof(pages) / sizeof(pages[0]); i++) {
v4_cell p = (v4_cell)pages[i];
v4_cell got = v4_iword_branch(p, 0u, k);
v4_ucell want = (pages[i] & ~(v4_ucell)mask);
CHECK((v4_ucell)got == want,
"slot %u: P=%llX with target 0 gave %llX, expected %llX", k,
(unsigned long long)pages[i], (unsigned long long)got,
(unsigned long long)want);
}
}
}
static void test_branch_opcode_set(void)
{
/* Exactly five opcodes take a target, and they are the five section 1.2
* names. Everything else must not, so that a non-branch slot's low bits
* stay available to the compiler. */
static const int want[32] = {
/* 0 */ 0, /* 1 */ 0, /* 2 jump */ 1, /* 3 call */ 1, /* 4 */ 0,
/* 5 next */ 1, /* 6 if */ 1, /* 7 -if */ 1, /* 8 */ 0, /* 9 */ 0,
/* A */ 0, /* B */ 0, /* C */ 0, /* D */ 0, /* E */ 0, /* F */ 0,
/* 10 */ 0, /* 11 */ 0, /* 12 */ 0, /* 13 */ 0, /* 14 */ 0, /* 15 */ 0,
/* 16 */ 0, /* 17 */ 0, /* 18 */ 0, /* 19 */ 0, /* 1A */ 0,
/* 1B */ 0, /* 1C */ 0, /* 1D */ 0, /* 1E */ 0, /* 1F */ 0
};
unsigned nbranch = 0;
for (unsigned op = 0; op < 32u; op++) {
int got = v4_op_is_branch(op);
CHECK(got == want[op], "opcode %02X: is_branch=%d, expected %d", op,
got, want[op]);
nbranch += (unsigned)got;
}
CHECK(nbranch == 5u, "expected exactly 5 branch opcodes, found %u", nbranch);
}
static void test_opcode_enum_matches_spec_numbering(void)
{
/* Each named opcode must carry the number section 1.3 gives it. Transposed
* or off-by-one enum entries are the classic way a 32-entry table rots. */
CHECK(V4_OP_SEMI == 0x00, "opcode ;"); CHECK(V4_OP_EX == 0x01, "opcode ex");
CHECK(V4_OP_JUMP == 0x02, "opcode jump"); CHECK(V4_OP_CALL == 0x03, "opcode call");
CHECK(V4_OP_UNEXT == 0x04, "opcode unext"); CHECK(V4_OP_NEXT == 0x05, "opcode next");
CHECK(V4_OP_IF == 0x06, "opcode if"); CHECK(V4_OP_MINUS_IF == 0x07, "opcode -if");
CHECK(V4_OP_FETCH_P == 0x08, "opcode @p"); CHECK(V4_OP_FETCH_INC == 0x09, "opcode @+");
CHECK(V4_OP_FETCH_B == 0x0A, "opcode @b"); CHECK(V4_OP_FETCH_A == 0x0B, "opcode @");
CHECK(V4_OP_STORE_P == 0x0C, "opcode !p"); CHECK(V4_OP_STORE_INC == 0x0D, "opcode !+");
CHECK(V4_OP_STORE_B == 0x0E, "opcode !b"); CHECK(V4_OP_STORE_A == 0x0F, "opcode !");
CHECK(V4_OP_MUL_STEP == 0x10, "opcode +*"); CHECK(V4_OP_TWO_STAR == 0x11, "opcode 2*");
CHECK(V4_OP_TWO_SLASH == 0x12, "opcode 2/"); CHECK(V4_OP_INV == 0x13, "opcode inv");
CHECK(V4_OP_ADD == 0x14, "opcode +"); CHECK(V4_OP_AND == 0x15, "opcode and");
CHECK(V4_OP_XOR == 0x16, "opcode xor"); CHECK(V4_OP_DROP == 0x17, "opcode drop");
CHECK(V4_OP_DUP == 0x18, "opcode dup"); CHECK(V4_OP_RPOP == 0x19, "opcode pop");
CHECK(V4_OP_OVER == 0x1A, "opcode over"); CHECK(V4_OP_PUSH_A == 0x1B, "opcode a");
CHECK(V4_OP_NOP == 0x1C, "opcode nop"); CHECK(V4_OP_PUSH == 0x1D, "opcode push");
CHECK(V4_OP_BANG_B == 0x1E, "opcode b!"); CHECK(V4_OP_BANG_A == 0x1F, "opcode a!");
CHECK(V4_OPCODE_COUNT == 0x20, "opcode count");
}
static void test_ambiguous_f18_names_were_renamed(void)
{
/* Section 1.3 says: "Opcode numbering follows the F18. Two names differ
* from Moore's: F18 `-` is renamed `inv` and F18 `or` (which is
* exclusive-or) is renamed `xor`, so instruction names never collide with
* FORTH-79 word names."
*
* The renames are the part that carries weight, and they are asserted here.
* `-` and `or` are exactly the dangerous case: Moore's F18 uses them with
* meanings that differ from the FORTH-79 words of the same name, so an
* instruction table containing either would be ambiguous rather than merely
* duplicated. `and` and `xor` are also FORTH-79 words, but `and` agrees
* with FORTH and `xor` is F18's spelling, so neither is ambiguous.
*
* NOTE: the trailing clause of that sentence is an overclaim, and the test
* deliberately does not assert it. `dup`, `drop`, `over`, `and`, `if` and
* `+` are all instruction names in section 1.3 and all FORTH-79 words,
* spelled identically and meaning the same thing. They coincide; they do
* not collide. What the renames prevent is a *divergent* meaning under a
* shared name, which is the property asserted above. Raised for the spec
* text rather than silently encoded here as if it were true. */
static const char *opnames[32] = {
";", "ex", "jump", "call", "unext", "next", "if", "-if",
"@p", "@+", "@b", "@", "!p", "!+", "!b", "!",
"+*", "2*", "2/", "inv", "+", "and", "xor", "drop",
"dup", "pop", "over", "a", "nop", "push", "b!", "a!"
};
CHECK(strcmp(opnames[V4_OP_INV], "inv") == 0,
"opcode 13 must be named inv, got \"%s\"", opnames[V4_OP_INV]);
CHECK(strcmp(opnames[V4_OP_XOR], "xor") == 0,
"opcode 16 must be named xor, got \"%s\"", opnames[V4_OP_XOR]);
/* The two names the renames exist to remove must appear nowhere. */
for (unsigned i = 0; i < 32u; i++) {
CHECK(strcmp(opnames[i], "-") != 0,
"F18 `-` must not survive as an instruction name (slot %u)", i);
CHECK(strcmp(opnames[i], "or") != 0,
"F18 `or` must not survive as an instruction name (slot %u)", i);
CHECK(strcmp(opnames[i], "OR") != 0,
"F18 `or` must not survive as an instruction name (slot %u)", i);
}
/* The five branch opcodes, by the names section 1.2 lists. */
CHECK(strcmp(opnames[V4_OP_JUMP], "jump") == 0, "opcode 2 name");
CHECK(strcmp(opnames[V4_OP_CALL], "call") == 0, "opcode 3 name");
CHECK(strcmp(opnames[V4_OP_NEXT], "next") == 0, "opcode 5 name");
CHECK(strcmp(opnames[V4_OP_IF], "if") == 0, "opcode 6 name");
CHECK(strcmp(opnames[V4_OP_MINUS_IF], "-if") == 0, "opcode 7 name");
}
static void test_assemble_rejects_out_of_range(void)
{
unsigned o[6] = { 0, 0, 0, 0, 0, 0 };
o[3] = 32u; /* will not fit in 5 bits */
CHECK(v4_iword_assemble(o, 0u) == 0xFFFFFFFFu,
"assemble must reject a 6-bit opcode");
o[3] = 0;
CHECK(v4_iword_assemble(o, 4u) == 0xFFFFFFFFu,
"assemble must reject spare bits above 3");
}
static void test_out_of_range_slots_are_inert(void)
{
/* Slots past 5 do not exist. The decode must answer deterministically
* rather than shift by a nonsense amount. */
v4_iword w = 0xFFFFFFFFu;
CHECK(v4_iword_op(w, 6u) == 0u, "op in slot 6 must be 0");
CHECK(v4_iword_target(w, 6u) == 0u, "target in slot 6 must be 0");
CHECK(v4_iword_slot_mask(6u) == 0u, "mask in slot 6 must be 0");
CHECK(!v4_iword_branch_legal(6u), "slot 6 must not allow branches");
}
int main(void)
{
printf("v4 iword tests: V4_CELL_BITS=%d\n", V4_CELL_BITS);
test_slot_bit_ranges();
test_roundtrip_all_opcodes();
test_slots_are_independent();
test_spare_bits_are_inert_for_decode();
test_spare_bits_are_branch_address_bits();
test_reach_column();
test_branch_targets_take_all_low_bits();
test_branch_legality();
test_page_relative_branch();
test_page_relative_branch_preserves_high_page();
test_branch_opcode_set();
test_opcode_enum_matches_spec_numbering();
test_ambiguous_f18_names_were_renamed();
test_assemble_rejects_out_of_range();
test_out_of_range_slots_are_inert();
printf(" %d checks, %d failures\n", checks, failures);
return failures ? 1 : 0;
}
+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;
}
+338
View File
@@ -0,0 +1,338 @@
/* test_stack.c -- the F18 circular stacks, checked against an independent
* model of the same semantics.
*
* The implementation in stack.c is specified in DECOMPOSITION.md as
* "T, S + 8 circular" / "R + 8 circular". This test does not take that
* decomposition on trust. It builds a second, deliberately dumb model of
* what D-2 describes -- a flat fixed-depth circular buffer with no bounds
* check at all -- and drives both with identical operation sequences,
* requiring them to agree after every single operation. If the register/ring
* bookkeeping in stack.c has a wrong pointer direction or an off-by-one in
* the wrap, this catches it; an inspection-only check would not.
*
* Depth, ordering, and the "silently overwrites the oldest entry" behaviour
* are additionally pinned with explicit cases, because those are the three
* properties the ISA's push-heavy words actually depend on.
*
* Built and run at both V4_CELL_BITS=32 and 64; see v4/Makefile.
*/
#include "v4/stack.h"
#include <stdio.h>
#include <stdlib.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)
/* ---- the independent reference model ---------------------------------------
* A flat circular buffer of depth N with no overflow or underflow detection,
* which is exactly what D-2 describes. top indexes the newest element. */
typedef struct {
v4_cell buf[64];
unsigned top;
unsigned depth;
} flat_t;
static void flat_reset(flat_t *f, unsigned depth)
{
f->depth = depth;
f->top = 0;
for (unsigned i = 0; i < 64; i++) f->buf[i] = 0;
}
static void flat_push(flat_t *f, v4_cell x)
{
f->top = (f->top + 1u) % f->depth;
f->buf[f->top] = x;
}
static v4_cell flat_pop(flat_t *f)
{
v4_cell x = f->buf[f->top];
f->top = (f->top + f->depth - 1u) % f->depth;
return x;
}
static v4_cell flat_peek(const flat_t *f)
{
return f->buf[f->top];
}
/* ---- deterministic PRNG ---------------------------------------------------
* xorshift64, so a failure is reproducible from the seed alone. No rand(),
* whose sequence is implementation-defined and would make a failure on one
* host unreproducible on another. */
static uint64_t rng_state = 0x9E3779B97F4A7C15ull;
static uint64_t rng_next(void)
{
uint64_t x = rng_state;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
rng_state = x;
return x;
}
/* ---- tests ---------------------------------------------------------------- */
static void test_reset_is_zero(void)
{
v4_dstack d;
v4_rstack r;
v4_dstack_reset(&d);
v4_rstack_reset(&r);
CHECK(v4_dstack_peek(&d) == 0, "data stack peek after reset != 0");
CHECK(v4_dstack_peek2(&d) == 0, "data stack peek2 after reset != 0");
CHECK(v4_rstack_peek(&r) == 0, "return stack peek after reset != 0");
}
static void test_exact_depth_data(void)
{
/* Fill to exactly V4_DATA_DEPTH, then drain and confirm the order. */
v4_dstack d;
flat_t f;
v4_dstack_reset(&d);
flat_reset(&f, V4_DATA_DEPTH);
for (unsigned i = 0; i < V4_DATA_DEPTH; i++) {
v4_cell v = (v4_cell)(i + 1);
v4_dstack_push(&d, v);
flat_push(&f, v);
}
CHECK(v4_dstack_peek(&d) == (v4_cell)V4_DATA_DEPTH,
"peek at full depth should be the last pushed value (%d)",
(int)V4_DATA_DEPTH);
for (unsigned i = V4_DATA_DEPTH; i > 0; i--) {
v4_cell got = v4_dstack_pop(&d);
v4_cell exp = flat_pop(&f);
CHECK(got == exp, "drain at depth %u: got %lld want %lld", i,
(long long)got, (long long)exp);
CHECK(got == (v4_cell)i, "drain at depth %u: got %lld want %d", i,
(long long)got, (int)i);
}
}
static void test_overflow_overwrites_oldest(void)
{
/* D-2: "pushing past the bottom silently overwrites the oldest entry."
* Push one past depth: the very first value pushed must be gone, and the
* remaining V4_DATA_DEPTH-1 must come back newest-first. */
v4_dstack d;
flat_t f;
v4_dstack_reset(&d);
flat_reset(&f, V4_DATA_DEPTH);
for (unsigned i = 0; i < V4_DATA_DEPTH + 1u; i++) {
v4_cell v = (v4_cell)(i + 1);
v4_dstack_push(&d, v);
flat_push(&f, v);
}
CHECK(v4_dstack_peek(&d) == (v4_cell)(V4_DATA_DEPTH + 1u),
"peek after overflow should be the newest value");
for (unsigned i = 0; i < V4_DATA_DEPTH; i++) {
v4_cell got = v4_dstack_pop(&d);
v4_cell exp = flat_pop(&f);
CHECK(got == exp, "post-overflow drain %u: got %lld want %lld", i,
(long long)got, (long long)exp);
CHECK(got != 1,
"post-overflow drain %u: value 1 should have been overwritten",
i);
}
}
static void test_exact_depth_return(void)
{
v4_rstack r;
flat_t f;
v4_rstack_reset(&r);
flat_reset(&f, V4_RET_DEPTH);
for (unsigned i = 0; i < V4_RET_DEPTH + 3u; i++) {
v4_cell v = (v4_cell)(i + 1);
v4_rstack_push(&r, v);
flat_push(&f, v);
}
for (unsigned i = 0; i < V4_RET_DEPTH; i++) {
v4_cell got = v4_rstack_pop(&r);
v4_cell exp = flat_pop(&f);
CHECK(got == exp, "return drain %u: got %lld want %lld", i,
(long long)got, (long long)exp);
}
}
static void test_underflow_wraps_rather_than_trapping(void)
{
/* D-2 says there is no underflow detection. Popping an "empty" stack must
* therefore return a defined (stale) value, not fault and not abort. The
* golden model must diverge from hardware in neither direction, so this is
* asserted rather than left to chance. */
v4_dstack d;
flat_t f;
v4_dstack_reset(&d);
flat_reset(&f, V4_DATA_DEPTH);
for (unsigned i = 0; i < 5; i++) {
v4_dstack_pop(&d);
flat_pop(&f);
}
CHECK(1, "popping past empty must not trap");
}
/* ---- live-depth-aware comparison ------------------------------------------
* D-2 specifies what these stacks do while they hold live entries: LIFO order
* within the depth, and oldest-entry-overwritten past it. It explicitly does
* NOT specify the contents once the stack has been popped empty, because
* "no overflow or underflow" means the residue is whatever the physical
* register file happened to hold. Two independent models of a circular buffer
* will legitimately disagree down there -- it is not a semantic difference.
*
* So the differential test tracks how many live entries each stack holds and
* asserts agreement only where the spec makes a claim: the value a pop returns
* and the top peek while depth > 0, and the second element while depth > 1.
* The stale region is still exercised (the sequence runs right through it) and
* is still required not to trap, it is simply not required to agree. */
typedef struct {
v4_dstack hw;
flat_t model;
int live;
} pair_t;
static void pair_reset(pair_t *p)
{
v4_dstack_reset(&p->hw);
flat_reset(&p->model, V4_DATA_DEPTH);
p->live = 0;
}
static void pair_step(pair_t *p, int push, v4_cell v, int step, const char *tag)
{
if (push) {
v4_dstack_push(&p->hw, v);
flat_push(&p->model, v);
if (p->live < V4_DATA_DEPTH) p->live++;
} else {
v4_cell got = v4_dstack_pop(&p->hw);
v4_cell exp = flat_pop(&p->model);
if (p->live > 0) {
CHECK(got == exp, "%s op %d: pop got %lld want %lld (live=%d)",
tag, step, (long long)got, (long long)exp, p->live);
p->live--;
}
}
if (p->live > 0) {
CHECK(v4_dstack_peek(&p->hw) == flat_peek(&p->model),
"%s op %d: peek mismatch (live=%d)", tag, step, p->live);
}
if (p->live > 1) {
/* Second element of a depth-N circular buffer whose top is at `top` is
* N-1 further along the fill direction, i.e. (top-1) mod N. */
v4_cell want = p->model.buf[(p->model.top + p->model.depth - 1u)
% p->model.depth];
CHECK(v4_dstack_peek2(&p->hw) == want,
"%s op %d: peek2 got %lld want %lld (live=%d)", tag, step,
(long long)v4_dstack_peek2(&p->hw), (long long)want, p->live);
}
}
static void test_exhaustive_sequences(void)
{
/* Every push/pop sequence up to length 10 -- 2046 of them -- driven through
* both models. Exhaustive over short sequences rather than random, because
* a wrap-direction bug shows up in a handful of specific short patterns
* (notably push x N+1 then pop x N, which is the only sequence that ever
* overwrites the oldest entry) and a random driver finds those only by
* luck. This finds all of them, every run, deterministically. */
enum { MAXLEN = 10 };
int total = 0;
for (int len = 1; len <= MAXLEN; len++) total += 1 << len;
for (int len = 1; len <= MAXLEN; len++) {
for (int bits = 0; bits < (1 << len); bits++) {
pair_t p;
pair_reset(&p);
for (int i = 0; i < len; i++) {
int push = (bits >> i) & 1;
/* Distinct, non-zero values so a slot mix-up is visible. */
v4_cell v = (v4_cell)(100 + i * 7);
pair_step(&p, push, v, i, "exhaustive");
}
}
}
printf(" exhaustive: %d sequences of length <= %d\n", total, MAXLEN);
}
static void test_differential_random(void)
{
/* Long random walk across the wrap points, both widths, both stacks. */
enum { OPS = 200000 };
pair_t dp;
v4_rstack r;
flat_t rf;
int rlive;
pair_reset(&dp);
v4_rstack_reset(&r);
flat_reset(&rf, V4_RET_DEPTH);
rlive = 0;
for (int i = 0; i < OPS; i++) {
uint64_t bits = rng_next();
pair_step(&dp, (int)(bits & 1u), (v4_cell)bits, i, "differential data");
if (bits & 2u) {
v4_cell v = (v4_cell)(bits >> 8);
v4_rstack_push(&r, v);
flat_push(&rf, v);
if (rlive < V4_RET_DEPTH) rlive++;
} else {
v4_cell got = v4_rstack_pop(&r);
v4_cell exp = flat_pop(&rf);
if (rlive > 0) {
CHECK(got == exp,
"differential ret op %d: pop got %lld want %lld (live=%d)",
i, (long long)got, (long long)exp, rlive);
rlive--;
}
}
if (rlive > 0) {
CHECK(v4_rstack_peek(&r) == flat_peek(&rf),
"differential ret op %d: peek mismatch (live=%d)", i, rlive);
}
}
printf(" differential: %d ops\n", OPS);
}
int main(void)
{
printf("v4 stack tests: V4_CELL_BITS=%d, data depth %d, return depth %d\n",
V4_CELL_BITS, V4_DATA_DEPTH, V4_RET_DEPTH);
test_reset_is_zero();
test_exact_depth_data();
test_overflow_overwrites_oldest();
test_exact_depth_return();
test_underflow_wraps_rather_than_trapping();
test_exhaustive_sequences();
test_differential_random();
printf(" %d checks, %d failures\n", checks, failures);
return failures ? 1 : 0;
}
+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;
}