Files
LithosAnanake/v4/tests/test_exec.c
T
rajamesandClaude Opus 5.5 dc7e37ba5c feat(v4.0.0): every error is raised and ends the line (D-18)
Ruled 2026-10-04: guard all errors.  The errors that set NODE-ERROR and
let the line run on now stop it at once, with a message.

- A store of a non-zero code to NODE-ERROR is a trap, a sixth kind of
  fault: nothing after it executes, the return stack is emptied, and the
  data stack is left as the word left it.  Not attached, NODE-ERROR is
  plain memory, as the tests below the prompt use it.
- The capsule's words store a code where they stored -1, and the prompt's
  (RAISED) prints its message: Negative count, Not a number, Number too
  long, Not a character, Dictionary full, Name missing, Control structure
  mismatch, Control structures too deep.
- ' and COMPILE and [COMPILE] of a word that is not there say
  UNKNOWN WORD: 'xxx', as the interpreter does.
- tests: the trap in test_exec.c; every message from the prompt, with the
  rest of the line not run and the stack kept, in test_host_quit.c.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:13:23 -04:00

594 lines
29 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++) {
/* each opcode finds what it takes on the stacks, and room (D-16) */
v4_dstack_reset(&n.ds); v4_rstack_reset(&n.rs);
dpush(1); dpush(2); rpush(5);
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 + n.faults == count && n.faults > 0 && es.anticlock > 0,
"soak: every opcode retired or faulted (%u faults)", n.faults);
CHECK(v4_node_guards_intact(&n), "soak left the guards intact");
}
#define HANDLER ((v4_cell)40)
/* D-16: the stacks are guarded. An opcode that would pop an empty stack,
* read a T or S that is not there, or push onto a full stack is a fault: it
* does nothing, both stacks are emptied, and P is that kind's word of the
* handler table. */
static void test_stack_faults(void)
{
/* what each opcode needs on the data and return stacks, and which it adds one to */
static const struct { unsigned op, dneed, rneed, dgrow, rgrow; const char *name; } m[] = {
{ V4_OP_SEMI, 0, 1, 0, 0, ";" }, { V4_OP_EX, 0, 1, 0, 0, "ex" }, { V4_OP_JUMP, 0, 0, 0, 0, "jump" },
{ V4_OP_CALL, 0, 0, 0, 1, "call" }, { V4_OP_UNEXT, 0, 1, 0, 0, "unext" }, { V4_OP_NEXT, 0, 1, 0, 0, "next" },
{ V4_OP_IF, 1, 0, 0, 0, "if" }, { V4_OP_MINUS_IF, 1, 0, 0, 0, "-if" },
{ V4_OP_FETCH_P, 0, 0, 1, 0, "@p" }, { V4_OP_FETCH_INC, 0, 0, 1, 0, "@+" }, { V4_OP_FETCH_B, 0, 0, 1, 0, "@b" },
{ V4_OP_FETCH_A, 0, 0, 1, 0, "@" }, { V4_OP_STORE_P, 1, 0, 0, 0, "!p" }, { V4_OP_STORE_INC, 1, 0, 0, 0, "!+" },
{ V4_OP_STORE_B, 1, 0, 0, 0, "!b" }, { V4_OP_STORE_A, 1, 0, 0, 0, "!" }, { V4_OP_MUL_STEP, 2, 0, 0, 0, "+*" },
{ V4_OP_TWO_STAR, 1, 0, 0, 0, "2*" }, { V4_OP_TWO_SLASH, 1, 0, 0, 0, "2/" }, { V4_OP_INV, 1, 0, 0, 0, "inv" },
{ V4_OP_ADD, 2, 0, 0, 0, "+" }, { V4_OP_AND, 2, 0, 0, 0, "and" }, { V4_OP_XOR, 2, 0, 0, 0, "xor" },
{ V4_OP_DROP, 1, 0, 0, 0, "drop" }, { V4_OP_DUP, 1, 0, 1, 0, "dup" }, { V4_OP_RPOP, 0, 1, 1, 0, "pop" },
{ V4_OP_OVER, 2, 0, 1, 0, "over" }, { V4_OP_PUSH_A, 0, 0, 1, 0, "a" }, { V4_OP_NOP, 0, 0, 0, 0, "nop" },
{ V4_OP_PUSH, 1, 0, 0, 1, "push" }, { V4_OP_BANG_B, 1, 0, 0, 0, "b!" }, { V4_OP_BANG_A, 1, 0, 0, 0, "a!" },
};
unsigned k, i;
CHECK(sizeof m / sizeof m[0] == V4_OPCODE_COUNT, "every opcode is in the table");
for (k = 0; k < sizeof m / sizeof m[0]; k++) {
unsigned dd, rd;
/* every depth of both stacks: the opcode faults exactly when the table says */
for (dd = 0; dd <= V4_DATA_DEPTH; dd++)
for (rd = 0; rd <= V4_RET_DEPTH; rd += (rd == 2 ? V4_RET_DEPTH - 3u : 1u)) {
unsigned want = 99, before;
fresh();
v4_node_fault_attach(&n, HANDLER);
for (i = 0; i < dd; i++) dpush((v4_cell)(100 + i));
for (i = 0; i < rd; i++) rpush((v4_cell)(200 + i));
n.a = 7; n.b = 8; n.p = 3;
if (dd < m[k].dneed) want = V4_FAULT_DATA_UNDER;
else if (rd < m[k].rneed) want = V4_FAULT_RET_UNDER;
else if (m[k].dgrow && dd == V4_DATA_DEPTH) want = V4_FAULT_DATA_OVER;
else if (m[k].rgrow && rd == V4_RET_DEPTH) want = V4_FAULT_RET_OVER;
before = h.op[m[k].op];
v4_exec_op(&n, &es, &h, m[k].op, 0, 0);
if (want == 99) {
CHECK(n.faults == 0 && h.op[m[k].op] == before + 1, "%s with %u and %u on the stacks runs", m[k].name, dd, rd);
continue;
}
CHECK(n.faults == 1 && n.fault_kind == want, "%s with %u and %u on the stacks: fault %u (got %u)", m[k].name, dd, rd, want, n.fault_kind);
CHECK(n.p == HANDLER + (v4_cell)want && !n.stopped, "%s: P is that kind's handler", m[k].name);
CHECK(h.op[m[k].op] == before, "%s: it did not retire", m[k].name);
CHECK(n.rs.depth == 0 && n.ds.depth == 0, "%s: both stacks are emptied", m[k].name);
CHECK(n.a == 7 && n.b == 8, "%s: A and B untouched", m[k].name);
}
}
/* the rest of the word does not run, and the handler does */
fresh(); v4_node_fault_attach(&n, HANDLER);
n.mem[HANDLER + V4_FAULT_DATA_UNDER] = word6(V4_OP_PUSH_A, V4_OP_PUSH_A, V4_OP_ADD, NOP, NOP, NOP);
n.a = 21; dpush(5);
n.mem[0] = word6(V4_OP_DROP, V4_OP_DROP, V4_OP_PUSH_A, V4_OP_PUSH_A, NOP, NOP); n.p = 0;
CHECK(v4_exec_step_word(&n, &es, &h) == 1 && n.ds.depth == 0 && n.p == HANDLER + (v4_cell)V4_FAULT_DATA_UNDER, "a second drop faults; nothing after it runs");
CHECK(v4_exec_step_word(&n, &es, &h) == 6 && n.ds.t == 42 && n.ds.depth == 1, "the handler runs on the emptied stack");
/* ten pushes fit, the eleventh faults; nine calls fit, the tenth faults */
fresh(); v4_node_fault_attach(&n, HANDLER);
for (i = 0; i < V4_DATA_DEPTH; i++) { n.a = (v4_cell)i; v4_exec_op(&n, &es, &h, V4_OP_PUSH_A, 0, 0); }
CHECK(n.faults == 0 && n.ds.depth == V4_DATA_DEPTH && n.ds.t == (v4_cell)(V4_DATA_DEPTH - 1u), "the data stack holds %u", (unsigned)V4_DATA_DEPTH);
v4_exec_op(&n, &es, &h, V4_OP_PUSH_A, 0, 0);
CHECK(n.faults == 1 && n.fault_kind == V4_FAULT_DATA_OVER, "one more is a fault");
fresh(); v4_node_fault_attach(&n, HANDLER);
for (i = 0; i < V4_RET_DEPTH; i++) v4_exec_op(&n, &es, &h, V4_OP_CALL, 0, 0);
CHECK(n.faults == 0 && n.rs.depth == V4_RET_DEPTH, "the return stack holds %u", (unsigned)V4_RET_DEPTH);
v4_exec_op(&n, &es, &h, V4_OP_CALL, 0, 0);
CHECK(n.faults == 1 && n.fault_kind == V4_FAULT_RET_OVER && n.rs.depth == 0, "one more call is a fault");
/* with no handler the node stops */
fresh();
CHECK(run1(V4_OP_DROP) == 0 && n.stopped && n.fault_kind == V4_FAULT_DATA_UNDER, "with no handler a stack fault stops the node");
/* the stack registers */
fresh(); v4_node_fault_attach(&n, HANDLER);
v4_node_stack_regs_attach(&n, 900, 901);
n.mem[900] = 55; n.mem[901] = 66;
dpush(1); dpush(2); dpush(3); rpush(7); rpush(8);
n.a = 900; v4_exec_op(&n, &es, &h, V4_OP_FETCH_A, 0, 0);
CHECK(n.ds.t == 3 && n.ds.depth == 4, "DSTACK-DEPTH reads the depth, its own push not counted");
n.b = 901; v4_exec_op(&n, &es, &h, V4_OP_FETCH_B, 0, 0);
CHECK(n.ds.t == 2 && n.ds.depth == 5, "RSTACK-DEPTH reads the return stack's");
v4_exec_op(&n, &es, &h, V4_OP_STORE_B, 0, 0);
CHECK(n.rs.depth == 0 && n.ds.depth == 4 && n.mem[901] == 66, "a store to RSTACK-DEPTH empties the return stack and writes no memory");
v4_exec_op(&n, &es, &h, V4_OP_STORE_A, 0, 0);
CHECK(n.ds.depth == 0 && n.mem[900] == 55, "a store to DSTACK-DEPTH empties the data stack");
n.a = 900; v4_exec_op(&n, &es, &h, V4_OP_FETCH_A, 0, 0);
CHECK(n.ds.t == 0 && n.ds.depth == 1 && n.faults == 0, "and it then reads 0");
v4_node_reset(&n);
CHECK(n.dstack_reg == -1 && n.rstack_reg == -1, "reset detaches the stack registers");
n.mem[900] = 55; n.a = 900; v4_exec_op(&n, &es, &h, V4_OP_FETCH_A, 0, 0);
CHECK(n.ds.t == 55, "detached, the address is memory");
}
/* D-18: NODE-ERROR as a trap. A store of a non-zero code is a fault of its
* own kind: the code is written, the return stack is emptied, the data stack
* is left, and P is the sixth word of the table. */
static void test_raised_errors(void)
{
static const unsigned store[] = { V4_OP_STORE_A, V4_OP_STORE_B, V4_OP_STORE_INC };
unsigned k;
for (k = 0; k < 3; k++) {
fresh(); v4_node_fault_attach(&n, HANDLER);
v4_node_error_attach(&n, 700);
dpush(11); dpush(22); dpush(5); rpush(1); rpush(2);
n.a = 700; n.b = 700;
n.mem[0] = word6(store[k], V4_OP_DROP, V4_OP_DROP, NOP, NOP, NOP); n.p = 0;
CHECK(v4_exec_step_word(&n, &es, &h) == 1, "the store retires and nothing after it runs");
CHECK(n.faults == 1 && n.fault_kind == V4_FAULT_RAISED && n.fault_addr == 5 && n.p == HANDLER + (v4_cell)V4_FAULT_RAISED,
"a non-zero store to NODE-ERROR raises: kind %u, code %lld", n.fault_kind, (long long)n.fault_addr);
CHECK(n.mem[700] == 5, "the code is in NODE-ERROR");
CHECK(n.rs.depth == 0 && n.ds.depth == 2 && n.ds.t == 22 && n.ds.s == 11, "the return stack is emptied and the data stack left");
/* zero clears it and is no fault */
dpush(0); n.a = 700; v4_exec_op(&n, &es, &h, V4_OP_STORE_A, 0, 0);
CHECK(n.faults == 1 && n.mem[700] == 0, "a store of zero is an ordinary store");
dpush(-1); n.a = 699; v4_exec_op(&n, &es, &h, V4_OP_STORE_A, 0, 0);
CHECK(n.faults == 1 && n.mem[699] == -1, "and so is a store next to it");
}
/* detached: ordinary memory */
fresh(); v4_node_fault_attach(&n, HANDLER);
CHECK(n.error_reg == -1, "a fresh node has no error trap");
dpush(7); n.a = 700; v4_exec_op(&n, &es, &h, V4_OP_STORE_A, 0, 0);
CHECK(n.faults == 0 && n.mem[700] == 7, "not attached, NODE-ERROR is memory and the word goes on");
/* with no table the node stops */
fresh(); v4_node_error_attach(&n, 700);
dpush(7); n.a = 700;
CHECK(run1(V4_OP_STORE_A) == 1 && n.stopped && n.mem[700] == 7, "with no handler a raised error stops the node");
v4_node_reset(&n);
CHECK(n.error_reg == -1, "reset detaches the trap");
/* the table must be six words inside memory */
v4_node_fault_attach(&n, (v4_cell)(V4_NODE_WORDS - V4_FAULT_KINDS));
CHECK(n.fault_vector == (v4_cell)(V4_NODE_WORDS - V4_FAULT_KINDS), "a table that ends with memory is accepted");
v4_node_fault_attach(&n, (v4_cell)(V4_NODE_WORDS - V4_FAULT_KINDS + 1u));
CHECK(n.fault_vector == -1, "one that runs past it is not");
}
/* D-14: an address outside node memory is a fault. The opcode does nothing,
* the rest of its word is not executed, and P becomes the handler; with no
* handler the node stops. */
static void test_address_faults(void)
{
static const v4_cell bad[] = { -1, (v4_cell)V4_NODE_WORDS, (v4_cell)V4_NODE_WORDS + 1, MINC, MAXC, (v4_cell)(V4_NODE_WORDS * 4u) };
static const struct { unsigned op; int reg; const char *name; } m[] = {
{ V4_OP_FETCH_A, 'a', "@" }, { V4_OP_FETCH_INC, 'a', "@+" }, { V4_OP_FETCH_B, 'b', "@b" },
{ V4_OP_STORE_A, 'a', "!" }, { V4_OP_STORE_INC, 'a', "!+" }, { V4_OP_STORE_B, 'b', "!b" },
};
unsigned i, k;
for (i = 0; i < sizeof bad / sizeof bad[0]; i++)
for (k = 0; k < sizeof m / sizeof m[0]; k++) {
unsigned retired;
fresh();
v4_node_fault_attach(&n, HANDLER);
dpush(11); dpush(22);
n.a = m[k].reg == 'a' ? bad[i] : 7;
n.b = m[k].reg == 'b' ? bad[i] : 7;
n.mem[0] = word6(V4_OP_DUP, m[k].op, V4_OP_DROP, V4_OP_DROP, NOP, NOP);
n.p = 0;
retired = v4_exec_step_word(&n, &es, &h);
CHECK(n.faults == 1 && n.fault_addr == bad[i] && !n.stopped, "%s at %lld is a fault", m[k].name, (long long)bad[i]);
CHECK(n.p == HANDLER, "%s: P is the handler", m[k].name);
CHECK(retired == 1, "%s: only the dup before it retired (%u)", m[k].name, retired);
CHECK(n.ds.depth == 0 && n.rs.depth == 0, "%s: both stacks are emptied", m[k].name);
CHECK(n.a == (m[k].reg == 'a' ? bad[i] : 7) && n.b == (m[k].reg == 'b' ? bad[i] : 7), "%s: A and B untouched", m[k].name);
CHECK(v4_node_guards_intact(&n), "%s: guards intact", m[k].name);
}
/* the last word of memory is not a fault, and the first is not */
fresh(); v4_node_fault_attach(&n, HANDLER);
n.mem[V4_NODE_WORDS - 1u] = 77; n.a = (v4_cell)(V4_NODE_WORDS - 1u); run1(V4_OP_FETCH_A);
CHECK(n.faults == 0 && n.ds.t == 77, "the last word of memory can be fetched");
fresh(); v4_node_fault_attach(&n, HANDLER);
dpush(5); n.b = (v4_cell)(V4_NODE_WORDS - 1u); run1(V4_OP_STORE_B);
CHECK(n.faults == 0 && n.mem[V4_NODE_WORDS - 1u] == 5, "and stored");
/* P outside memory: nothing is fetched or executed */
for (i = 0; i < sizeof bad / sizeof bad[0]; i++) {
fresh(); v4_node_fault_attach(&n, HANDLER);
dpush(1);
n.p = bad[i];
CHECK(v4_exec_step_word(&n, &es, &h) == 0 && n.faults == 1 && n.fault_addr == bad[i] && n.p == HANDLER && n.ds.t == 1,
"P at %lld is a fault", (long long)bad[i]);
}
/* @p in the last word of memory: its literal would be past the end */
fresh(); v4_node_fault_attach(&n, HANDLER);
dpush(1);
n.mem[V4_NODE_WORDS - 1u] = word6(V4_OP_FETCH_P, NOP, NOP, NOP, NOP, NOP);
n.p = (v4_cell)(V4_NODE_WORDS - 1u);
CHECK(v4_exec_step_word(&n, &es, &h) == 0 && n.faults == 1 && n.fault_addr == (v4_cell)V4_NODE_WORDS && n.p == HANDLER && n.ds.t == 1,
"a literal past the end of memory is a fault");
fresh(); v4_node_fault_attach(&n, HANDLER);
dpush(1);
n.mem[V4_NODE_WORDS - 1u] = word6(V4_OP_STORE_P, NOP, NOP, NOP, NOP, NOP);
n.p = (v4_cell)(V4_NODE_WORDS - 1u);
CHECK(v4_exec_step_word(&n, &es, &h) == 0 && n.faults == 1 && n.p == HANDLER && n.ds.t == 1, "and so is !p there");
/* a jump out of memory faults at the next fetch */
fresh(); v4_node_fault_attach(&n, HANDLER);
rpush(-5);
n.mem[0] = word6(V4_OP_SEMI, NOP, NOP, NOP, NOP, NOP);
n.p = 0;
CHECK(v4_exec_step_word(&n, &es, &h) == 1 && n.p == -5 && n.faults == 0, "; to an address outside memory is not yet a fault");
CHECK(v4_exec_step_word(&n, &es, &h) == 0 && n.p == HANDLER && n.faults == 1 && n.fault_addr == -5, "the fetch there is");
/* the handler runs, and faults are counted */
fresh(); v4_node_fault_attach(&n, HANDLER);
n.mem[HANDLER] = word6(V4_OP_PUSH_A, V4_OP_PUSH_A, V4_OP_ADD, NOP, NOP, NOP);
dpush(21); n.a = -1; run1(V4_OP_FETCH_A);
CHECK(v4_exec_step_word(&n, &es, &h) == 6 && n.ds.t == -2 && n.ds.depth == 1, "the handler's code runs next, on empty stacks");
n.a = -1; run1(V4_OP_FETCH_A); dpush(1); n.a = -1; run1(V4_OP_STORE_A);
CHECK(n.faults == 3 && n.fault_kind == V4_FAULT_ADDRESS, "each fault is counted");
v4_node_fault_attach(&n, HANDLER);
CHECK(n.faults == 0 && n.fault_vector == HANDLER, "attaching clears the count");
/* with no handler the node stops */
fresh();
CHECK(n.fault_vector == -1 && n.faults == 0 && !n.stopped, "a fresh node has no handler and has not stopped");
dpush(9); n.a = -1;
CHECK(run1(V4_OP_FETCH_A) == 0 && n.stopped && n.faults == 1 && n.ds.depth == 0, "with no handler a fault stops the node");
n.a = 9;
n.mem[0] = word6(V4_OP_PUSH_A, V4_OP_PUSH_A, V4_OP_ADD, NOP, NOP, NOP); n.p = 0;
CHECK(v4_exec_step_word(&n, &es, &h) == 0 && n.p == 0 && n.ds.depth == 0, "a stopped node executes nothing");
v4_node_fault_attach(&n, HANDLER);
CHECK(!n.stopped && v4_exec_step_word(&n, &es, &h) == 6 && n.ds.t == 18, "attaching a handler lets it run again");
v4_node_fault_attach(&n, (v4_cell)V4_NODE_WORDS);
CHECK(n.fault_vector == -1, "a handler address outside memory detaches");
v4_node_reset(&n);
CHECK(n.fault_vector == -1 && !n.stopped && n.faults == 0, "reset detaches the handler");
/* the C accessors never index outside memory */
fresh();
for (i = 0; i < sizeof bad / sizeof bad[0]; i++) {
v4_node_store(&n, bad[i], 123);
CHECK(v4_node_load(&n, bad[i]) == 0 && v4_node_fetch(&n, bad[i]) == 0 && !v4_node_addr_ok(bad[i]),
"load, fetch and store at %lld touch nothing", (long long)bad[i]);
}
CHECK(n.faults == 0 && v4_node_guards_intact(&n) && v4_node_addr_ok(0) && v4_node_addr_ok((v4_cell)(V4_NODE_WORDS - 1u)),
"and they are not faults: only a running programme faults");
}
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_stack_faults();
test_raised_errors();
test_address_faults();
test_soak();
printf(" %d checks, %d failures\n", checks, failures);
return failures ? 1 : 0;
}