/* 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 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; }