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>
594 lines
29 KiB
C
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;
|
|
}
|