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>
340 lines
14 KiB
C
340 lines
14 KiB
C
/* 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;
|
|
}
|