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