/*! \file dbt_test.cpp * \brief Standalone test harness for the RV64IMD interpreter and DBT. * * Compile (dbt.cpp + the backend that matches the build host are needed for * the differential interpreter-vs-translator tests). Use the same backend * configure would pick for this host: dbt_a64_sysv.cpp on AArch64, * dbt_x64_sysv.cpp on x86-64 SysV, dbt_x64_win64.cpp on Win64: * g++ -std=c++17 -O2 -I../../include -o dbt_test \ * dbt_test.cpp dbt_interp.cpp dbt_elf64.cpp dbt.cpp dbt_a64_sysv.cpp * * Run: * ./dbt_test # hand-assembled tests only * ./dbt_test dbt_rt/test_rv64.elf # also run cross-compiled ELF * * Tests hand-assembled RV64IMD instruction sequences against expected * results. Each test allocates a small memory buffer, writes machine * code, runs the interpreter, and checks register values. Some tests also * run the same code through the host DBT translator and check that it * agrees with the interpreter (run_code_dbt). */ #include "dbt_interp.h" #include "dbt_decoder.h" #include "dbt_elf64.h" #include "dbt.h" #include "dbt_internal.h" #include "dbt_jit_mem.h" #include #include #include #include #include // The only thing this harness wants from is write(), for the // RV64 SYS_write emulation below. MSVC has it as _write() in and // has no ssize_t, so name both portably rather than sprinkling #ifdef at // the two call sites (#1441). // #if defined(_WIN32) #include typedef int dbt_ssize_t; #define dbt_write _write #else #include typedef ssize_t dbt_ssize_t; #define dbt_write write #endif // --------------------------------------------------------------- // Test infrastructure // --------------------------------------------------------------- static int g_tests_run = 0; static int g_tests_passed = 0; static int g_tests_failed = 0; #define CHECK_EQ(desc, actual, expected) do { \ g_tests_run++; \ if ((actual) == (expected)) { \ g_tests_passed++; \ } else { \ g_tests_failed++; \ fprintf(stderr, " FAIL: %s: got 0x%llX, expected 0x%llX\n", \ (desc), \ (unsigned long long)(actual), \ (unsigned long long)(expected)); \ } \ } while (0) #define CHECK_FEQ(desc, actual, expected) do { \ g_tests_run++; \ double a_ = (actual), e_ = (expected); \ if (a_ == e_ || (std::isnan(a_) && std::isnan(e_))) { \ g_tests_passed++; \ } else { \ g_tests_failed++; \ fprintf(stderr, " FAIL: %s: got %g, expected %g\n", \ (desc), a_, e_); \ } \ } while (0) // ECALL handler for tests: ECALL 93 = exit(a0). // static int test_ecall(rv64_state_t *state, void *) { uint64_t syscall_num = state->x[17]; // a7 if (syscall_num == 93) { return static_cast(state->x[10]); // a0 = exit code } fprintf(stderr, " test_ecall: unhandled ecall %llu\n", (unsigned long long)syscall_num); return -1; // continue } // Helper: encode RV64 instructions. // // R-type: opcode | rd<<7 | funct3<<12 | rs1<<15 | rs2<<20 | funct7<<25 // static uint32_t r_type(uint8_t opcode, uint8_t rd, uint8_t funct3, uint8_t rs1, uint8_t rs2, uint8_t funct7) { return opcode | (rd << 7) | (funct3 << 12) | (rs1 << 15) | (rs2 << 20) | (funct7 << 25); } // I-type: opcode | rd<<7 | funct3<<12 | rs1<<15 | imm[11:0]<<20 // static uint32_t i_type(uint8_t opcode, uint8_t rd, uint8_t funct3, uint8_t rs1, int32_t imm) { return opcode | (rd << 7) | (funct3 << 12) | (rs1 << 15) | ((static_cast(imm) & 0xFFF) << 20); } // S-type: opcode | imm[4:0]<<7 | funct3<<12 | rs1<<15 | rs2<<20 | imm[11:5]<<25 // static uint32_t s_type(uint8_t opcode, uint8_t funct3, uint8_t rs1, uint8_t rs2, int32_t imm) { return opcode | ((static_cast(imm) & 0x1F) << 7) | (funct3 << 12) | (rs1 << 15) | (rs2 << 20) | (((static_cast(imm) >> 5) & 0x7F) << 25); } // U-type: opcode | rd<<7 | imm[31:12] // static uint32_t u_type(uint8_t opcode, uint8_t rd, int32_t imm) { return opcode | (rd << 7) | (static_cast(imm) & 0xFFFFF000); } // B-type: opcode | imm[11]<<7 | imm[4:1]<<8 | funct3<<12 | rs1<<15 | rs2<<20 | imm[10:5]<<25 | imm[12]<<31 // static uint32_t b_type(uint8_t opcode, uint8_t funct3, uint8_t rs1, uint8_t rs2, int32_t imm) { uint32_t i = static_cast(imm); return opcode | (((i >> 11) & 1) << 7) | (((i >> 1) & 0xF) << 8) | (funct3 << 12) | (rs1 << 15) | (rs2 << 20) | (((i >> 5) & 0x3F) << 25) | (((i >> 12) & 1) << 31); } // R4-type (FMA): opcode | rd<<7 | funct3<<12 | rs1<<15 | rs2<<20 | fmt<<25 | rs3<<27 // static uint32_t r4_type(uint8_t opcode, uint8_t rd, uint8_t funct3, uint8_t rs1, uint8_t rs2, uint8_t rs3, uint8_t fmt) { return opcode | (rd << 7) | (funct3 << 12) | (rs1 << 15) | (rs2 << 20) | (fmt << 25) | (rs3 << 27); } // Convenience: ADDI rd, rs1, imm static uint32_t ADDI(uint8_t rd, uint8_t rs1, int32_t imm) { return i_type(OP_IMM, rd, ALU_ADDI, rs1, imm); } // Convenience: ADDIW rd, rs1, imm static uint32_t ADDIW(uint8_t rd, uint8_t rs1, int32_t imm) { return i_type(OP_IMM32, rd, 0, rs1, imm); } // Convenience: ADD rd, rs1, rs2 static uint32_t ADD(uint8_t rd, uint8_t rs1, uint8_t rs2) { return r_type(OP_REG, rd, ALU_ADD, rs1, rs2, 0x00); } // Convenience: SUB rd, rs1, rs2 static uint32_t SUB(uint8_t rd, uint8_t rs1, uint8_t rs2) { return r_type(OP_REG, rd, ALU_ADD, rs1, rs2, 0x20); } // Convenience: MUL rd, rs1, rs2 static uint32_t MUL(uint8_t rd, uint8_t rs1, uint8_t rs2) { return r_type(OP_REG, rd, 0, rs1, rs2, 0x01); } // Convenience: DIV rd, rs1, rs2 static uint32_t DIV(uint8_t rd, uint8_t rs1, uint8_t rs2) { return r_type(OP_REG, rd, 4, rs1, rs2, 0x01); } // Convenience: REM rd, rs1, rs2 static uint32_t REM(uint8_t rd, uint8_t rs1, uint8_t rs2) { return r_type(OP_REG, rd, 6, rs1, rs2, 0x01); } // Convenience: DIVU rd, rs1, rs2 static uint32_t DIVU(uint8_t rd, uint8_t rs1, uint8_t rs2) { return r_type(OP_REG, rd, 5, rs1, rs2, 0x01); } // Convenience: REMU rd, rs1, rs2 static uint32_t REMU(uint8_t rd, uint8_t rs1, uint8_t rs2) { return r_type(OP_REG, rd, 7, rs1, rs2, 0x01); } // Convenience: SLLI rd, rs1, shamt (64-bit) static uint32_t SLLI(uint8_t rd, uint8_t rs1, int shamt) { return i_type(OP_IMM, rd, ALU_SLLI, rs1, shamt & 0x3F); } // Convenience: SRLI rd, rs1, shamt (64-bit) static uint32_t SRLI(uint8_t rd, uint8_t rs1, int shamt) { return i_type(OP_IMM, rd, ALU_SRLI, rs1, shamt & 0x3F); } // Convenience: SRAI rd, rs1, shamt (64-bit) static uint32_t SRAI(uint8_t rd, uint8_t rs1, int shamt) { return i_type(OP_IMM, rd, ALU_SRLI, rs1, (shamt & 0x3F) | 0x400); } // Convenience: LUI rd, imm (upper 20 bits) static uint32_t LUI(uint8_t rd, int32_t imm) { return u_type(OP_LUI, rd, imm); } // Convenience: SD rs2, offset(rs1) static uint32_t SD(uint8_t rs1, uint8_t rs2, int32_t offset) { return s_type(OP_STORE, ST_SD, rs1, rs2, offset); } // Convenience: LD rd, offset(rs1) static uint32_t LD(uint8_t rd, uint8_t rs1, int32_t offset) { return i_type(OP_LOAD, rd, LD_LD, rs1, offset); } // Convenience: BEQ rs1, rs2, offset static uint32_t BEQ(uint8_t rs1, uint8_t rs2, int32_t offset) { return b_type(OP_BRANCH, BR_BEQ, rs1, rs2, offset); } // Convenience: BNE rs1, rs2, offset static uint32_t BNE(uint8_t rs1, uint8_t rs2, int32_t offset) { return b_type(OP_BRANCH, BR_BNE, rs1, rs2, offset); } // Convenience: ECALL static uint32_t ECALL() { return i_type(OP_SYSTEM, 0, 0, 0, 0); } // CSR immediates (SYSTEM, funct3 5-7): rs1 field is the zimm. // static uint32_t CSRRWI(uint8_t rd, uint16_t csr, uint8_t zimm) { return i_type(OP_SYSTEM, rd, 5, zimm, csr); } static uint32_t CSRRSI(uint8_t rd, uint16_t csr, uint8_t zimm) { return i_type(OP_SYSTEM, rd, 6, zimm, csr); } static uint32_t CSRRCI(uint8_t rd, uint16_t csr, uint8_t zimm) { return i_type(OP_SYSTEM, rd, 7, zimm, csr); } // CSR register forms (SYSTEM, funct3 1-3). // static uint32_t CSRRW(uint8_t rd, uint16_t csr, uint8_t rs1) { return i_type(OP_SYSTEM, rd, 1, rs1, csr); } static uint32_t CSRRS(uint8_t rd, uint16_t csr, uint8_t rs1) { return i_type(OP_SYSTEM, rd, 2, rs1, csr); } // Convenience: ADDW rd, rs1, rs2 static uint32_t ADDW(uint8_t rd, uint8_t rs1, uint8_t rs2) { return r_type(OP_REG32, rd, ALU_ADD, rs1, rs2, 0x00); } // Convenience: SUBW rd, rs1, rs2 static uint32_t SUBW(uint8_t rd, uint8_t rs1, uint8_t rs2) { return r_type(OP_REG32, rd, ALU_ADD, rs1, rs2, 0x20); } // Convenience: MULW rd, rs1, rs2 static uint32_t MULW(uint8_t rd, uint8_t rs1, uint8_t rs2) { return r_type(OP_REG32, rd, 0, rs1, rs2, 0x01); } // Convenience: DIVW / DIVUW / REMW / REMUW rd, rs1, rs2 static uint32_t DIVW(uint8_t rd, uint8_t rs1, uint8_t rs2) { return r_type(OP_REG32, rd, 4, rs1, rs2, 0x01); } static uint32_t DIVUW(uint8_t rd, uint8_t rs1, uint8_t rs2) { return r_type(OP_REG32, rd, 5, rs1, rs2, 0x01); } static uint32_t REMW(uint8_t rd, uint8_t rs1, uint8_t rs2) { return r_type(OP_REG32, rd, 6, rs1, rs2, 0x01); } static uint32_t REMUW(uint8_t rd, uint8_t rs1, uint8_t rs2) { return r_type(OP_REG32, rd, 7, rs1, rs2, 0x01); } // Run a code sequence, return exit code. // struct TestResult { int exit_code; rv64_state_t state; }; static TestResult run_code(const std::vector& code, rv64_state_t *init_state = nullptr) { // Memory layout: code at offset 0, stack at end. // const size_t MEM_SIZE = 64 * 1024; // 64K std::vector memory(MEM_SIZE, 0); // Write code to memory at offset 0. // for (size_t i = 0; i < code.size(); i++) { memcpy(memory.data() + i * 4, &code[i], 4); } rv64_memory_t mem = { memory.data(), MEM_SIZE }; rv64_state_t state = {}; if (init_state) { state = *init_state; } state.pc = 0; state.x[0] = 0; // Set stack pointer to near end of memory. state.x[2] = MEM_SIZE - 16; int rc = rv64_interp_run(&state, &mem, test_ecall, nullptr); TestResult result; result.exit_code = rc; result.state = state; return result; } // Run a code sequence through the host DBT translator and return guest // register `reg`. Used to differential-test the translator against the // reference interpreter (run_code). The code must set up its own registers // (dbt_run zeroes the guest context on entry) and end with ECALL. // static rv64_ctx_t g_dbt_exit_ctx; static int dbt_test_ecall2(rv64_ctx_t *ctx, void *) { g_dbt_exit_ctx = *ctx; return 0; // halt } static uint64_t run_code_dbt(const std::vector& code, int reg) { const size_t MEM_SIZE = 64 * 1024; std::vector memory(MEM_SIZE, 0); for (size_t i = 0; i < code.size(); i++) { memcpy(memory.data() + i * 4, &code[i], 4); } dbt_state_t dbt; if (dbt_init(&dbt, memory.data(), MEM_SIZE, dbt_test_ecall2, nullptr) != 0) { return ~0ULL; } dbt.max_dispatch = 1000000; dbt_run(&dbt, 0, MEM_SIZE - 16); dbt_cleanup(&dbt); return g_dbt_exit_ctx.x[reg]; } // As run_code_dbt, but returns an FP register's raw bits. rv64_ctx_t::f is // double[32] where rv64_state_t::f is uint64_t[32], so the bits are copied // out rather than converted: the sign-manipulation tests below differ only // in the sign bit of a value whose magnitude is unchanged, and comparing as // double would call -1.0 and +1.0 unequal but -0.0 and +0.0 equal — hiding // exactly the class of bug being tested. static uint64_t run_code_dbt_fbits(const std::vector& code, int freg) { const size_t MEM_SIZE = 64 * 1024; std::vector memory(MEM_SIZE, 0); for (size_t i = 0; i < code.size(); i++) { memcpy(memory.data() + i * 4, &code[i], 4); } dbt_state_t dbt; if (dbt_init(&dbt, memory.data(), MEM_SIZE, dbt_test_ecall2, nullptr) != 0) { return ~0ULL; } dbt.max_dispatch = 1000000; dbt_run(&dbt, 0, MEM_SIZE - 16); dbt_cleanup(&dbt); uint64_t bits; memcpy(&bits, &g_dbt_exit_ctx.f[freg], sizeof(bits)); return bits; } // --------------------------------------------------------------- // Tests // --------------------------------------------------------------- static void test_addi() { printf("test_addi...\n"); // ADDI x1, x0, 42; ADDI x17, x0, 93; ADDI x10, x1, 0; ECALL auto r = run_code({ ADDI(1, 0, 42), // x1 = 42 ADDI(17, 0, 93), // a7 = 93 (exit) ADDI(10, 1, 0), // a0 = x1 ECALL() }); CHECK_EQ("exit code", r.exit_code, 42); CHECK_EQ("x1", r.state.x[1], 42); } static void test_addi_negative() { printf("test_addi_negative...\n"); auto r = run_code({ ADDI(1, 0, -10), // x1 = -10 ADDI(17, 0, 93), ADDI(10, 0, 0), ECALL() }); CHECK_EQ("x1 = -10", r.state.x[1], static_cast(-10LL)); } static void test_add_sub() { printf("test_add_sub...\n"); auto r = run_code({ ADDI(1, 0, 100), // x1 = 100 ADDI(2, 0, 58), // x2 = 58 ADD(3, 1, 2), // x3 = 158 SUB(4, 1, 2), // x4 = 42 ADDI(17, 0, 93), ADDI(10, 4, 0), // a0 = x4 ECALL() }); CHECK_EQ("x3 = 158", r.state.x[3], 158); CHECK_EQ("x4 = 42", r.state.x[4], 42); CHECK_EQ("exit code", r.exit_code, 42); } static void test_lui_addi() { printf("test_lui_addi...\n"); // Load 0x12345000 + 0x678 = 0x12345678 auto r = run_code({ LUI(1, 0x12345000), // x1 = 0x12345000 ADDI(1, 1, 0x678), // x1 = 0x12345678 ADDI(17, 0, 93), ADDI(10, 0, 0), ECALL() }); CHECK_EQ("x1 = 0x12345678", r.state.x[1], 0x12345678ULL); } static void test_lui_sign_extend() { printf("test_lui_sign_extend...\n"); // LUI with bit 31 set: should sign-extend to 64 bits. // LUI x1, 0x80000000 -> x1 = 0xFFFFFFFF80000000 auto r = run_code({ LUI(1, static_cast(0x80000000)), ADDI(17, 0, 93), ADDI(10, 0, 0), ECALL() }); CHECK_EQ("LUI sign-extend", r.state.x[1], 0xFFFFFFFF80000000ULL); } static void test_lui_str_base_ecall_flush() { printf("test_lui_str_base_ecall_flush...\n"); // Materialize 0x8000 with LUI+ADDI, offset into it, and hand a0 to an // ECALL -- so the value has to survive the pinned-register flush an ECALL // performs. Asserted on BOTH routes: the interpreter and the host DBT. // // 0x8000 is not an arbitrary constant. On the current guest layout it is // rv_compiler::STR_BASE exactly, and simultaneously FARGS_LIMIT -- the // boundary the two regions share (dbt_compile.h asserts // FARGS_LIMIT == STR_BASE). Compiled code materializes addresses in that // band constantly, because every string constant a program references // lives there. // // Salvaged from #2116. That PR built it while #2107 was believed to be a // backend fault at this address; #2107 turned out to be a mixed build and // was closed invalid, so the motivating theory is gone. The property is // worth pinning regardless, and it is cheap: a backend that mis-sign- // extends LUI here, or drops a0 across an ECALL flush, would corrupt every // string reference in every compiled program, and this is the smallest // possible case that would catch it. // const std::vector code = { LUI(10, 0x8000), // a0 = 0x8000 (STR_BASE) ADDI(10, 10, 8), // a0 = 0x8008 ADDI(17, 0, 93), // a7 = exit ECALL() }; auto r = run_code(code); CHECK_EQ("LUI STR_BASE+8 a0 (interp)", r.state.x[10], 0x8008ULL); CHECK_EQ("LUI STR_BASE+8 exit (interp)", static_cast(static_cast(r.exit_code)), 0x8008ULL); CHECK_EQ("LUI STR_BASE+8 a0 (DBT)", run_code_dbt(code, 10), 0x8008ULL); } static void test_shifts_64bit() { printf("test_shifts_64bit...\n"); auto r = run_code({ ADDI(1, 0, 1), // x1 = 1 SLLI(2, 1, 32), // x2 = 1 << 32 = 0x100000000 SLLI(3, 1, 63), // x3 = 1 << 63 = 0x8000000000000000 SRLI(4, 3, 63), // x4 = 0x8000000000000000 >> 63 = 1 SRAI(5, 3, 63), // x5 = (int64_t)0x8000000000000000 >> 63 = -1 ADDI(17, 0, 93), ADDI(10, 0, 0), ECALL() }); CHECK_EQ("SLLI 32", r.state.x[2], 0x100000000ULL); CHECK_EQ("SLLI 63", r.state.x[3], 0x8000000000000000ULL); CHECK_EQ("SRLI 63", r.state.x[4], 1); CHECK_EQ("SRAI 63", r.state.x[5], static_cast(-1LL)); } static void test_mul_div_rem() { printf("test_mul_div_rem...\n"); auto r = run_code({ ADDI(1, 0, 7), // x1 = 7 ADDI(2, 0, 6), // x2 = 6 MUL(3, 1, 2), // x3 = 42 DIV(4, 3, 1), // x4 = 42 / 7 = 6 REM(5, 3, 2), // x5 = 42 % 6 = 0 ADDI(6, 0, 5), REM(7, 3, 6), // x7 = 42 % 5 = 2 ADDI(17, 0, 93), ADDI(10, 0, 0), ECALL() }); CHECK_EQ("MUL 7*6", r.state.x[3], 42); CHECK_EQ("DIV 42/7", r.state.x[4], 6); CHECK_EQ("REM 42%6", r.state.x[5], 0); CHECK_EQ("REM 42%5", r.state.x[7], 2); } static void test_div_by_zero() { printf("test_div_by_zero...\n"); auto r = run_code({ ADDI(1, 0, 42), DIV(2, 1, 0), // x2 = 42 / 0 = -1 (all ones) ADDI(17, 0, 93), ADDI(10, 0, 0), ECALL() }); CHECK_EQ("DIV by zero", r.state.x[2], UINT64_MAX); } static void test_branch_beq() { printf("test_branch_beq...\n"); auto r = run_code({ ADDI(1, 0, 5), // x1 = 5 ADDI(2, 0, 5), // x2 = 5 BEQ(1, 2, 8), // if x1 == x2, skip next ADDI(3, 0, 1), // x3 = 1 (skipped) ADDI(3, 0, 2), // x3 = 2 (taken) ADDI(17, 0, 93), ADDI(10, 3, 0), ECALL() }); CHECK_EQ("BEQ taken", r.state.x[3], 2); CHECK_EQ("exit code", r.exit_code, 2); } static void test_branch_bne() { printf("test_branch_bne...\n"); auto r = run_code({ ADDI(1, 0, 5), ADDI(2, 0, 6), BNE(1, 2, 8), // if x1 != x2, skip next ADDI(3, 0, 1), // skipped ADDI(3, 0, 2), // taken ADDI(17, 0, 93), ADDI(10, 3, 0), ECALL() }); CHECK_EQ("BNE taken", r.state.x[3], 2); } static void test_load_store_64() { printf("test_load_store_64...\n"); // Store a 64-bit value to stack, load it back. auto r = run_code({ ADDI(1, 0, 1), // x1 = 1 SLLI(1, 1, 40), // x1 = 1 << 40 = 0x10000000000 ADDI(1, 1, 42), // x1 = 0x1000000002A SD(2, 1, -8), // mem[sp-8] = x1 LD(3, 2, -8), // x3 = mem[sp-8] ADDI(17, 0, 93), ADDI(10, 0, 0), ECALL() }); CHECK_EQ("SD/LD roundtrip", r.state.x[3], 0x1000000002AULL); } static void test_load_store_32_sign_ext() { printf("test_load_store_32_sign_ext...\n"); // Store 0xFFFFFFFF to memory, load with LW (sign-extend) and LWU (zero-extend). auto r = run_code({ ADDI(1, 0, -1), // x1 = 0xFFFFFFFFFFFFFFFF s_type(OP_STORE, ST_SW, 2, 1, -8), // SW x1, -8(sp) i_type(OP_LOAD, 3, LD_LW, 2, -8), // LW x3, -8(sp) — sign-extend i_type(OP_LOAD, 4, LD_LWU, 2, -8), // LWU x4, -8(sp) — zero-extend ADDI(17, 0, 93), ADDI(10, 0, 0), ECALL() }); CHECK_EQ("LW sign-ext", r.state.x[3], 0xFFFFFFFFFFFFFFFFULL); CHECK_EQ("LWU zero-ext", r.state.x[4], 0x00000000FFFFFFFFULL); } static void test_w_suffix_ops() { printf("test_w_suffix_ops...\n"); auto r = run_code({ // ADDIW: operate on lower 32, sign-extend. // Start with 0x7FFF, add 1 via ADDIW: 0x8000 fits in 32 bits, positive. // Better test: use LUI to get 0x7FFFF000, ADDIW 0x7FF+1 overflows. // Simplest: load a value where ADDIW wraps bit 31. LUI(1, static_cast(0x80000000)), // x1 = 0xFFFFFFFF80000000 ADDIW(2, 1, -1), // ADDIW: lower32(0x80000000) + (-1) = 0x7FFFFFFF -> sext = 0x7FFFFFFF // ADDW ADDI(3, 0, 100), ADDI(4, 0, 200), ADDW(5, 3, 4), // x5 = sext32(300) // SUBW SUBW(6, 3, 4), // x6 = sext32(-100) // MULW ADDI(7, 0, -7), MULW(8, 4, 7), // x8 = sext32(200 * -7 = -1400) ADDI(17, 0, 93), ADDI(10, 0, 0), ECALL() }); CHECK_EQ("ADDIW wrap", r.state.x[2], 0x000000007FFFFFFFULL); CHECK_EQ("ADDW 100+200", r.state.x[5], 300); CHECK_EQ("SUBW 100-200", r.state.x[6], static_cast(static_cast(-100))); CHECK_EQ("MULW 200*-7", r.state.x[8], static_cast(static_cast(-1400))); } static void test_loop() { printf("test_loop...\n"); // Sum 1..10 using a loop. // x1 = counter (starts at 10), x2 = accumulator auto r = run_code({ ADDI(1, 0, 10), // 0: x1 = 10 ADDI(2, 0, 0), // 4: x2 = 0 ADD(2, 2, 1), // 8: x2 += x1 (loop body) ADDI(1, 1, -1), // 12: x1-- BNE(1, 0, -8), // 16: if x1 != 0, goto offset -8 (back to +8) ADDI(17, 0, 93), // 20: ADDI(10, 2, 0), // 24: ECALL() // 28: }); CHECK_EQ("loop sum 1..10", r.state.x[2], 55); CHECK_EQ("exit code", r.exit_code, 55); } static void test_fp_add() { printf("test_fp_add...\n"); // Load 3.14 and 2.72 into FP regs via integer move, add them. double a = 3.14, b = 2.72; uint64_t a_bits, b_bits; memcpy(&a_bits, &a, 8); memcpy(&b_bits, &b, 8); // We need to load 64-bit immediates into integer regs. // Strategy: store the doubles in memory (at stack area), load via FLD. // // Code: write a_bits to mem[sp-16], b_bits to mem[sp-8], // FLD f1, -16(sp); FLD f2, -8(sp); FADD.D f3, f1, f2 // FMV.X.D x3, f3; exit. // // But we can't assemble 64-bit immediates easily in RV64. // Instead, pre-fill the memory. const size_t MEM_SIZE = 64 * 1024; std::vector memory(MEM_SIZE, 0); // Write doubles at known addresses (0x1000). memcpy(memory.data() + 0x1000, &a_bits, 8); memcpy(memory.data() + 0x1008, &b_bits, 8); // Code at offset 0. std::vector code = { LUI(1, 0x1000), // x1 = 0x1000 i_type(OP_FP_LOAD, 1, 3, 1, 0), // FLD f1, 0(x1) i_type(OP_FP_LOAD, 2, 3, 1, 8), // FLD f2, 8(x1) r_type(OP_FP, 3, 0, 1, 2, (FP_FADD << 2) | FP_FMT_D), // FADD.D f3, f1, f2 r_type(OP_FP, 3, 0, 3, 0, (FP_FCLASS << 2) | FP_FMT_D), // FMV.X.D x3, f3 ADDI(17, 0, 93), ADDI(10, 0, 0), ECALL() }; for (size_t i = 0; i < code.size(); i++) { memcpy(memory.data() + i * 4, &code[i], 4); } rv64_memory_t mem = { memory.data(), MEM_SIZE }; rv64_state_t state = {}; state.pc = 0; state.x[2] = MEM_SIZE - 16; rv64_interp_run(&state, &mem, test_ecall, nullptr); // x3 should have the bits of 3.14 + 2.72 = 5.86 double result; memcpy(&result, &state.x[3], 8); CHECK_FEQ("FADD.D 3.14+2.72", result, 3.14 + 2.72); } static void test_fp_mul_div() { printf("test_fp_mul_div...\n"); double a = 6.0, b = 7.0; uint64_t a_bits, b_bits; memcpy(&a_bits, &a, 8); memcpy(&b_bits, &b, 8); const size_t MEM_SIZE = 64 * 1024; std::vector memory(MEM_SIZE, 0); memcpy(memory.data() + 0x1000, &a_bits, 8); memcpy(memory.data() + 0x1008, &b_bits, 8); std::vector code = { LUI(1, 0x1000), i_type(OP_FP_LOAD, 1, 3, 1, 0), // FLD f1, 0(x1) i_type(OP_FP_LOAD, 2, 3, 1, 8), // FLD f2, 8(x1) r_type(OP_FP, 3, 0, 1, 2, (FP_FMUL << 2) | FP_FMT_D), // FMUL.D f3, f1, f2 r_type(OP_FP, 4, 0, 3, 2, (FP_FDIV << 2) | FP_FMT_D), // FDIV.D f4, f3, f2 r_type(OP_FP, 3, 0, 3, 0, (FP_FCLASS << 2) | FP_FMT_D), // FMV.X.D x3, f3 r_type(OP_FP, 4, 0, 4, 0, (FP_FCLASS << 2) | FP_FMT_D), // FMV.X.D x4, f4 ADDI(17, 0, 93), ADDI(10, 0, 0), ECALL() }; for (size_t i = 0; i < code.size(); i++) { memcpy(memory.data() + i * 4, &code[i], 4); } rv64_memory_t mem = { memory.data(), MEM_SIZE }; rv64_state_t state = {}; state.pc = 0; state.x[2] = MEM_SIZE - 16; rv64_interp_run(&state, &mem, test_ecall, nullptr); double mul_result, div_result; memcpy(&mul_result, &state.x[3], 8); memcpy(&div_result, &state.x[4], 8); CHECK_FEQ("FMUL.D 6*7", mul_result, 42.0); CHECK_FEQ("FDIV.D 42/7", div_result, 6.0); } static void test_fp_convert() { printf("test_fp_convert...\n"); // FCVT.D.L: convert int64 42 to double, then FCVT.L.D back. auto r = run_code({ ADDI(1, 0, 42), // FCVT.D.L f1, x1 (rs2=2 means L, fmt=D=1) r_type(OP_FP, 1, 0, 1, 2, (FP_FCVTDW << 2) | FP_FMT_D), // FCVT.L.D x2, f1 (rs2=2 means L, fmt=D=1) r_type(OP_FP, 2, 0, 1, 2, (FP_FCVTW << 2) | FP_FMT_D), ADDI(17, 0, 93), ADDI(10, 2, 0), ECALL() }); CHECK_EQ("FCVT roundtrip", r.state.x[2], 42); CHECK_EQ("exit code", r.exit_code, 42); } static void test_fp_compare() { printf("test_fp_compare...\n"); double a = 3.0, b = 5.0; uint64_t a_bits, b_bits; memcpy(&a_bits, &a, 8); memcpy(&b_bits, &b, 8); const size_t MEM_SIZE = 64 * 1024; std::vector memory(MEM_SIZE, 0); memcpy(memory.data() + 0x1000, &a_bits, 8); memcpy(memory.data() + 0x1008, &b_bits, 8); std::vector code = { LUI(1, 0x1000), i_type(OP_FP_LOAD, 1, 3, 1, 0), // FLD f1, 0(x1) = 3.0 i_type(OP_FP_LOAD, 2, 3, 1, 8), // FLD f2, 8(x1) = 5.0 r_type(OP_FP, 3, 1, 1, 2, (FP_FCMP << 2) | FP_FMT_D), // FLT.D x3, f1, f2 (3<5 = 1) r_type(OP_FP, 4, 1, 2, 1, (FP_FCMP << 2) | FP_FMT_D), // FLT.D x4, f2, f1 (5<3 = 0) r_type(OP_FP, 5, 2, 1, 1, (FP_FCMP << 2) | FP_FMT_D), // FEQ.D x5, f1, f1 (3==3 = 1) ADDI(17, 0, 93), ADDI(10, 0, 0), ECALL() }; for (size_t i = 0; i < code.size(); i++) { memcpy(memory.data() + i * 4, &code[i], 4); } rv64_memory_t mem = { memory.data(), MEM_SIZE }; rv64_state_t state = {}; state.pc = 0; state.x[2] = MEM_SIZE - 16; rv64_interp_run(&state, &mem, test_ecall, nullptr); CHECK_EQ("FLT 3<5", state.x[3], 1); CHECK_EQ("FLT 5<3", state.x[4], 0); CHECK_EQ("FEQ 3==3", state.x[5], 1); } static void test_mulh() { printf("test_mulh...\n"); // MULH: signed high multiply. // 0x7FFFFFFFFFFFFFFF * 2 = 0xFFFFFFFFFFFFFFFE (low), 0x0000000000000000 (high) // Wait, let's use a case that gives a non-zero high part. // 0x100000000 * 0x100000000 = 0x10000000000000000 -> high = 1 auto r = run_code({ ADDI(1, 0, 1), SLLI(1, 1, 32), // x1 = 0x100000000 ADDI(2, 0, 1), SLLI(2, 2, 32), // x2 = 0x100000000 r_type(OP_REG, 3, 3, 1, 2, 0x01), // MULHU x3, x1, x2 (funct3=3) ADDI(17, 0, 93), ADDI(10, 0, 0), ECALL() }); CHECK_EQ("MULHU high", r.state.x[3], 1); } static void test_x0_always_zero() { printf("test_x0_always_zero...\n"); // Attempt to write to x0 — should remain 0. auto r = run_code({ ADDI(0, 0, 42), // try to write 42 to x0 ADDI(17, 0, 93), ADDI(10, 0, 0), // a0 = x0 = should be 0 ECALL() }); CHECK_EQ("x0 = 0", r.state.x[0], 0); CHECK_EQ("exit code", r.exit_code, 0); } // --------------------------------------------------------------- // Coverage gap tests // --------------------------------------------------------------- static void test_sub_word_loads() { printf("test_sub_word_loads...\n"); // Write 0xFEDCBA98 to memory, then test LB/LBU/LH/LHU/SB/SH. auto r = run_code({ ADDI(1, 0, -1), // x1 = 0xFFFF...FF s_type(OP_STORE, ST_SW, 2, 1, -16), // SW x1, -16(sp) -> 0xFFFFFFFF // LB: load byte signed from byte 0 -> 0xFF -> sign-ext to -1 i_type(OP_LOAD, 3, LD_LB, 2, -16), // LBU: load byte unsigned from byte 0 -> 0xFF -> zero-ext to 255 i_type(OP_LOAD, 4, LD_LBU, 2, -16), // LH: load halfword signed -> 0xFFFF -> sign-ext to -1 i_type(OP_LOAD, 5, LD_LH, 2, -16), // LHU: load halfword unsigned -> 0xFFFF -> zero-ext to 65535 i_type(OP_LOAD, 6, LD_LHU, 2, -16), // SB: store byte 0x42 then load it back ADDI(7, 0, 0x42), s_type(OP_STORE, ST_SB, 2, 7, -8), i_type(OP_LOAD, 8, LD_LBU, 2, -8), // SH: store halfword 0x1234 then load it back ADDI(9, 0, 0x234), ADDI(11, 0, 1), SLLI(11, 11, 12), // x11 = 0x1000 ADD(9, 9, 11), // x9 = 0x1234 s_type(OP_STORE, ST_SH, 2, 9, -8), i_type(OP_LOAD, 12, LD_LHU, 2, -8), ADDI(17, 0, 93), ADDI(10, 0, 0), ECALL() }); CHECK_EQ("LB sign-ext", r.state.x[3], static_cast(-1LL)); CHECK_EQ("LBU zero-ext", r.state.x[4], 0xFF); CHECK_EQ("LH sign-ext", r.state.x[5], static_cast(-1LL)); CHECK_EQ("LHU zero-ext", r.state.x[6], 0xFFFF); CHECK_EQ("SB/LBU roundtrip", r.state.x[8], 0x42); CHECK_EQ("SH/LHU roundtrip", r.state.x[12], 0x1234); } static void test_branches_all() { printf("test_branches_all...\n"); // BLT: signed less than auto r1 = run_code({ ADDI(1, 0, -5), // x1 = -5 (signed) ADDI(2, 0, 3), // x2 = 3 b_type(OP_BRANCH, BR_BLT, 1, 2, 8), // -5 < 3 -> taken ADDI(3, 0, 0), // skipped ADDI(3, 0, 1), // x3 = 1 ADDI(17, 0, 93), ADDI(10, 3, 0), ECALL() }); CHECK_EQ("BLT signed taken", r1.state.x[3], 1); // BGE: signed greater-or-equal auto r2 = run_code({ ADDI(1, 0, 5), ADDI(2, 0, 5), b_type(OP_BRANCH, BR_BGE, 1, 2, 8), // 5 >= 5 -> taken ADDI(3, 0, 0), ADDI(3, 0, 1), ADDI(17, 0, 93), ADDI(10, 3, 0), ECALL() }); CHECK_EQ("BGE equal taken", r2.state.x[3], 1); // BLTU: unsigned less than (-1 unsigned is MAX, so not < 3) auto r3 = run_code({ ADDI(1, 0, -1), // x1 = 0xFFFF...FF (huge unsigned) ADDI(2, 0, 3), b_type(OP_BRANCH, BR_BLTU, 1, 2, 8), // MAX < 3 -> NOT taken ADDI(3, 0, 0), // x3 = 0 (not skipped) ADDI(3, 0, 1), ADDI(17, 0, 93), ADDI(10, 3, 0), ECALL() }); CHECK_EQ("BLTU unsigned not taken", r3.state.x[3], 1); // BGEU: unsigned greater-or-equal auto r4 = run_code({ ADDI(1, 0, -1), // MAX unsigned ADDI(2, 0, 3), b_type(OP_BRANCH, BR_BGEU, 1, 2, 8), // MAX >= 3 -> taken ADDI(3, 0, 0), ADDI(3, 0, 1), ADDI(17, 0, 93), ADDI(10, 3, 0), ECALL() }); CHECK_EQ("BGEU unsigned taken", r4.state.x[3], 1); } static void test_logical_ops() { printf("test_logical_ops...\n"); auto r = run_code({ ADDI(1, 0, 0x0F), // x1 = 0x0F ADDI(2, 0, 0x36), // x2 = 0x36 // Immediate forms i_type(OP_IMM, 3, ALU_XORI, 1, -1), // XORI x3 = 0x0F ^ -1 = ~0x0F i_type(OP_IMM, 4, ALU_ORI, 1, 0x30), // ORI x4 = 0x0F | 0x30 = 0x3F i_type(OP_IMM, 5, ALU_ANDI, 2, 0x0F), // ANDI x5 = 0x36 & 0x0F = 0x06 // Register forms r_type(OP_REG, 6, ALU_XOR, 1, 2, 0x00), // XOR x6 = 0x0F ^ 0x36 = 0x39 r_type(OP_REG, 7, ALU_OR, 1, 2, 0x00), // OR x7 = 0x0F | 0x36 = 0x3F r_type(OP_REG, 8, ALU_AND, 1, 2, 0x00), // AND x8 = 0x0F & 0x36 = 0x06 // SLT/SLTU ADDI(9, 0, -1), // x9 = -1 r_type(OP_REG, 11, ALU_SLT, 9, 1, 0x00), // SLT x11 = (-1 < 15) = 1 r_type(OP_REG, 12, ALU_SLTU, 9, 1, 0x00), // SLTU x12 = (MAX < 15) = 0 i_type(OP_IMM, 13, ALU_SLTI, 9, 5), // SLTI x13 = (-1 < 5) = 1 i_type(OP_IMM, 14, ALU_SLTIU, 1, 0x20), // SLTIU x14 = (15 < 32) = 1 ADDI(17, 0, 93), ADDI(10, 0, 0), ECALL() }); // XORI with -1 (sign-extended to 64-bit) = bitwise NOT CHECK_EQ("XORI", r.state.x[3], ~static_cast(0x0F)); CHECK_EQ("ORI", r.state.x[4], 0x3F); CHECK_EQ("ANDI", r.state.x[5], 0x06); CHECK_EQ("XOR", r.state.x[6], 0x39); CHECK_EQ("OR", r.state.x[7], 0x3F); CHECK_EQ("AND", r.state.x[8], 0x06); CHECK_EQ("SLT signed", r.state.x[11], 1); CHECK_EQ("SLTU unsigned", r.state.x[12], 0); CHECK_EQ("SLTI", r.state.x[13], 1); CHECK_EQ("SLTIU", r.state.x[14], 1); } static void test_register_shifts() { printf("test_register_shifts...\n"); auto r = run_code({ ADDI(1, 0, 1), ADDI(2, 0, 40), r_type(OP_REG, 3, ALU_SLL, 1, 2, 0x00), // SLL x3 = 1 << 40 ADDI(4, 0, -1), // x4 = all ones ADDI(5, 0, 60), r_type(OP_REG, 6, ALU_SRL, 4, 5, 0x00), // SRL x6 = 0xFFF...F >> 60 = 0xF r_type(OP_REG, 7, ALU_SRL, 4, 5, 0x20), // SRA x7 = (int64)-1 >> 60 = -1 ADDI(17, 0, 93), ADDI(10, 0, 0), ECALL() }); CHECK_EQ("SLL reg", r.state.x[3], UINT64_C(1) << 40); CHECK_EQ("SRL reg", r.state.x[6], 0xF); CHECK_EQ("SRA reg", r.state.x[7], static_cast(-1LL)); } static void test_auipc() { printf("test_auipc...\n"); // AUIPC x1, 0x1000 — at PC=0, x1 = 0 + 0x1000 = 0x1000 auto r = run_code({ u_type(OP_AUIPC, 1, 0x1000), // AUIPC x1, 0x1000 (at PC=0) ADDI(17, 0, 93), ADDI(10, 1, 0), ECALL() }); CHECK_EQ("AUIPC", r.state.x[1], 0x1000); } static void test_jal_jalr() { printf("test_jal_jalr...\n"); // JAL: jump forward 8, save return address // 0: JAL x1, +12 -> x1 = 4, PC = 12 // 4: ADDI x2, x0, 1 (skipped) // 8: ADDI x2, x0, 2 (skipped) // 12: ADDI x2, x0, 3 (landed here) // J-type encoding for JAL: imm[20|10:1|11|19:12] // imm=12 -> bits: 0|000000110|0|00000000 -> complex encoding // Let me use the raw encoding helper. // JAL rd=1, imm=12 uint32_t jal_imm = 12; uint32_t jal_word = OP_JAL | (1 << 7) // rd=1 | (((jal_imm >> 12) & 0xFF) << 12) // bits 19:12 | (((jal_imm >> 11) & 1) << 20) // bit 11 | (((jal_imm >> 1) & 0x3FF) << 21) // bits 10:1 | (((jal_imm >> 20) & 1) << 31); // bit 20 (sign) auto r = run_code({ jal_word, // JAL x1, +12 ADDI(2, 0, 1), // skipped ADDI(2, 0, 2), // skipped ADDI(2, 0, 3), // landed ADDI(17, 0, 93), ADDI(10, 2, 0), ECALL() }); CHECK_EQ("JAL target", r.state.x[2], 3); CHECK_EQ("JAL link", r.state.x[1], 4); // return addr = next after JAL // JALR: indirect jump auto r2 = run_code({ ADDI(1, 0, 12), // x1 = 12 (target addr) i_type(OP_JALR, 3, 0, 1, 0), // JALR x3, x1, 0 -> x3 = 8, PC = 12 ADDI(2, 0, 1), // skipped ADDI(2, 0, 3), // landed ADDI(17, 0, 93), ADDI(10, 2, 0), ECALL() }); CHECK_EQ("JALR target", r2.state.x[2], 3); CHECK_EQ("JALR link", r2.state.x[3], 8); } static void test_fp_fsqrt() { printf("test_fp_fsqrt...\n"); double val = 49.0; uint64_t bits; memcpy(&bits, &val, 8); const size_t MEM_SIZE = 64 * 1024; std::vector memory(MEM_SIZE, 0); memcpy(memory.data() + 0x1000, &bits, 8); std::vector code = { LUI(1, 0x1000), i_type(OP_FP_LOAD, 1, 3, 1, 0), // FLD f1, 0(x1) = 49.0 r_type(OP_FP, 2, 0, 1, 0, (FP_FSQRT << 2) | FP_FMT_D), // FSQRT.D f2, f1 r_type(OP_FP, 3, 0, 2, 0, (FP_FCLASS << 2) | FP_FMT_D), // FMV.X.D x3, f2 ADDI(17, 0, 93), ADDI(10, 0, 0), ECALL() }; for (size_t i = 0; i < code.size(); i++) { memcpy(memory.data() + i * 4, &code[i], 4); } rv64_memory_t mem = { memory.data(), MEM_SIZE }; rv64_state_t state = {}; state.pc = 0; state.x[2] = MEM_SIZE - 16; rv64_interp_run(&state, &mem, test_ecall, nullptr); double result; memcpy(&result, &state.x[3], 8); CHECK_FEQ("FSQRT.D 49", result, 7.0); } static void test_fp_minmax() { printf("test_fp_minmax...\n"); double a = 3.0, b = 5.0; uint64_t a_bits, b_bits; memcpy(&a_bits, &a, 8); memcpy(&b_bits, &b, 8); const size_t MEM_SIZE = 64 * 1024; std::vector memory(MEM_SIZE, 0); memcpy(memory.data() + 0x1000, &a_bits, 8); memcpy(memory.data() + 0x1008, &b_bits, 8); std::vector code = { LUI(1, 0x1000), i_type(OP_FP_LOAD, 1, 3, 1, 0), // FLD f1 = 3.0 i_type(OP_FP_LOAD, 2, 3, 1, 8), // FLD f2 = 5.0 r_type(OP_FP, 3, 0, 1, 2, (FP_FMINMAX << 2) | FP_FMT_D), // FMIN.D f3, f1, f2 r_type(OP_FP, 4, 1, 1, 2, (FP_FMINMAX << 2) | FP_FMT_D), // FMAX.D f4, f1, f2 // Move results to integer regs r_type(OP_FP, 5, 0, 3, 0, (FP_FCLASS << 2) | FP_FMT_D), // FMV.X.D x5, f3 r_type(OP_FP, 6, 0, 4, 0, (FP_FCLASS << 2) | FP_FMT_D), // FMV.X.D x6, f4 ADDI(17, 0, 93), ADDI(10, 0, 0), ECALL() }; for (size_t i = 0; i < code.size(); i++) { memcpy(memory.data() + i * 4, &code[i], 4); } rv64_memory_t mem = { memory.data(), MEM_SIZE }; rv64_state_t state = {}; state.pc = 0; state.x[2] = MEM_SIZE - 16; rv64_interp_run(&state, &mem, test_ecall, nullptr); double min_r, max_r; memcpy(&min_r, &state.x[5], 8); memcpy(&max_r, &state.x[6], 8); CHECK_FEQ("FMIN.D", min_r, 3.0); CHECK_FEQ("FMAX.D", max_r, 5.0); } static void test_fp_sign_inject() { printf("test_fp_sign_inject...\n"); double pos = 42.0, neg = -42.0; uint64_t pos_bits, neg_bits; memcpy(&pos_bits, &pos, 8); memcpy(&neg_bits, &neg, 8); const size_t MEM_SIZE = 64 * 1024; std::vector memory(MEM_SIZE, 0); memcpy(memory.data() + 0x1000, &pos_bits, 8); memcpy(memory.data() + 0x1008, &neg_bits, 8); std::vector code = { LUI(1, 0x1000), i_type(OP_FP_LOAD, 1, 3, 1, 0), // f1 = +42.0 i_type(OP_FP_LOAD, 2, 3, 1, 8), // f2 = -42.0 // FSGNJ: take sign of f2 -> -42.0 r_type(OP_FP, 3, 0, 1, 2, (FP_FSGNJ << 2) | FP_FMT_D), // FSGNJN: take negated sign of f2 -> +42.0 r_type(OP_FP, 4, 1, 1, 2, (FP_FSGNJ << 2) | FP_FMT_D), // FSGNJX: XOR signs (+42 ^ -42 = negative) -> -42.0 r_type(OP_FP, 5, 2, 1, 2, (FP_FSGNJ << 2) | FP_FMT_D), // Move to integer regs r_type(OP_FP, 6, 0, 3, 0, (FP_FCLASS << 2) | FP_FMT_D), r_type(OP_FP, 7, 0, 4, 0, (FP_FCLASS << 2) | FP_FMT_D), r_type(OP_FP, 8, 0, 5, 0, (FP_FCLASS << 2) | FP_FMT_D), ADDI(17, 0, 93), ADDI(10, 0, 0), ECALL() }; for (size_t i = 0; i < code.size(); i++) { memcpy(memory.data() + i * 4, &code[i], 4); } rv64_memory_t mem = { memory.data(), MEM_SIZE }; rv64_state_t state = {}; state.pc = 0; state.x[2] = MEM_SIZE - 16; rv64_interp_run(&state, &mem, test_ecall, nullptr); double fsgnj_r, fsgnjn_r, fsgnjx_r; memcpy(&fsgnj_r, &state.x[6], 8); memcpy(&fsgnjn_r, &state.x[7], 8); memcpy(&fsgnjx_r, &state.x[8], 8); CHECK_FEQ("FSGNJ.D", fsgnj_r, -42.0); CHECK_FEQ("FSGNJN.D", fsgnjn_r, 42.0); CHECK_FEQ("FSGNJX.D", fsgnjx_r, -42.0); } // #1313 / #1314: FCVT.{W,WU,L,LU}.D for NaN and the infinities. // // RISC-V and the host ISAs disagree here, so BOTH routes must be checked and // the expected values must come from the RISC-V unprivileged spec ("Invalid // Operation" / out-of-range table), not from either implementation: // // NaN -> the destination type's MAXIMUM (not 0, which is what ARM // FCVTZS/FCVTZU and x86 CVTTSD2SI give) // +inf -> destination maximum // -inf -> destination minimum (0 for the unsigned forms) // // and every 32-bit (W/WU) result is SIGN-extended to 64 bits on RV64 -- // including the unsigned form, so fcvt.wu.d(+inf) is 0xFFFFFFFF_FFFFFFFF. // // The differential fuzzer structurally cannot find the NaN unsigned rows: // the interpreter and the a64 backend were wrong in the same direction, so // the two routes agreed with each other (#1314). static void check_fcvt_case(const char *what, uint64_t bits, int rs2, uint64_t expected) { // Build the double in an integer register, FMV it across, convert, and // leave the result in x5. Constructed in-register because run_code_dbt // has no data segment. // A byte at a time: ADDI's immediate is 12-bit SIGNED (max 2047), so a // 16-bit chunk does not fit and a naive split still overflows. Every // byte is <= 255 and always encodes cleanly. std::vector code; code.push_back(ADDI(1, 0, 0)); for (int shift = 56; shift >= 0; shift -= 8) { code.push_back(SLLI(1, 1, 8)); uint8_t byte = static_cast((bits >> shift) & 0xFF); if (byte) { code.push_back(ADDI(2, 0, static_cast(byte))); code.push_back(r_type(0x33, 1, 0, 1, 2, 0)); // ADD x1, x1, x2 } } code.push_back(r_type(OP_FP, 1, 0, 1, 0, (FP_FMVDX << 2) | FP_FMT_D)); // The W/WU/L/LU variant lives in the rs2 field; funct3 is the // rounding mode (0 = RNE). code.push_back(r_type(OP_FP, 5, 0, 1, rs2, (FP_FCVTW << 2) | FP_FMT_D)); code.push_back(ADDI(17, 0, 93)); code.push_back(ADDI(10, 0, 0)); code.push_back(ECALL()); TestResult r = run_code(code); char desc[128]; snprintf(desc, sizeof(desc), "%s: interp", what); CHECK_EQ(desc, r.state.x[5], expected); snprintf(desc, sizeof(desc), "%s: DBT", what); CHECK_EQ(desc, run_code_dbt(code, 5), expected); } static void test_fp_cvt_nan_inf() { printf("test_fp_cvt_nan_inf...\n"); const uint64_t PINF = 0x7FF0000000000000ULL; const uint64_t NINF = 0xFFF0000000000000ULL; const uint64_t QNAN = 0x7FF8000000000000ULL; // fcvt.w.d (rs2=0) — signed 32, sign-extended check_fcvt_case("fcvt.w.d(+inf)", PINF, 0, 0x000000007FFFFFFFULL); check_fcvt_case("fcvt.w.d(-inf)", NINF, 0, 0xFFFFFFFF80000000ULL); check_fcvt_case("fcvt.w.d(NaN)", QNAN, 0, 0x000000007FFFFFFFULL); // fcvt.wu.d (rs2=1) — unsigned 32, still SIGN-extended check_fcvt_case("fcvt.wu.d(+inf)", PINF, 1, 0xFFFFFFFFFFFFFFFFULL); check_fcvt_case("fcvt.wu.d(-inf)", NINF, 1, 0x0000000000000000ULL); check_fcvt_case("fcvt.wu.d(NaN)", QNAN, 1, 0xFFFFFFFFFFFFFFFFULL); // fcvt.l.d (rs2=2) — signed 64 check_fcvt_case("fcvt.l.d(+inf)", PINF, 2, 0x7FFFFFFFFFFFFFFFULL); check_fcvt_case("fcvt.l.d(-inf)", NINF, 2, 0x8000000000000000ULL); check_fcvt_case("fcvt.l.d(NaN)", QNAN, 2, 0x7FFFFFFFFFFFFFFFULL); // fcvt.lu.d (rs2=3) — unsigned 64 check_fcvt_case("fcvt.lu.d(+inf)", PINF, 3, 0xFFFFFFFFFFFFFFFFULL); check_fcvt_case("fcvt.lu.d(-inf)", NINF, 3, 0x0000000000000000ULL); check_fcvt_case("fcvt.lu.d(NaN)", QNAN, 3, 0xFFFFFFFFFFFFFFFFULL); } static void test_fp_fma() { printf("test_fp_fma...\n"); double a = 3.0, b = 5.0, c = 7.0; uint64_t a_bits, b_bits, c_bits; memcpy(&a_bits, &a, 8); memcpy(&b_bits, &b, 8); memcpy(&c_bits, &c, 8); const size_t MEM_SIZE = 64 * 1024; std::vector memory(MEM_SIZE, 0); memcpy(memory.data() + 0x1000, &a_bits, 8); memcpy(memory.data() + 0x1008, &b_bits, 8); memcpy(memory.data() + 0x1010, &c_bits, 8); std::vector code = { LUI(1, 0x1000), i_type(OP_FP_LOAD, 1, 3, 1, 0), // f1 = 3.0 i_type(OP_FP_LOAD, 2, 3, 1, 8), // f2 = 5.0 i_type(OP_FP_LOAD, 3, 3, 1, 16), // f3 = 7.0 // FMADD.D f4, f1, f2, f3 = 3*5 + 7 = 22 r4_type(OP_FMADD, 4, 0, 1, 2, 3, FP_FMT_D), // FMSUB.D f5, f1, f2, f3 = 3*5 - 7 = 8 r4_type(OP_FMSUB, 5, 0, 1, 2, 3, FP_FMT_D), // FNMSUB.D f6, f1, f2, f3 = -(3*5) + 7 = -8 r4_type(OP_FNMSUB, 6, 0, 1, 2, 3, FP_FMT_D), // FNMADD.D f7, f1, f2, f3 = -(3*5) - 7 = -22 r4_type(OP_FNMADD, 7, 0, 1, 2, 3, FP_FMT_D), // Move to integer regs (use x20-x23 to avoid clobbering a0) r_type(OP_FP, 20, 0, 4, 0, (FP_FCLASS << 2) | FP_FMT_D), r_type(OP_FP, 21, 0, 5, 0, (FP_FCLASS << 2) | FP_FMT_D), r_type(OP_FP, 22, 0, 6, 0, (FP_FCLASS << 2) | FP_FMT_D), r_type(OP_FP, 23, 0, 7, 0, (FP_FCLASS << 2) | FP_FMT_D), ADDI(17, 0, 93), ADDI(10, 0, 0), ECALL() }; for (size_t i = 0; i < code.size(); i++) { memcpy(memory.data() + i * 4, &code[i], 4); } rv64_memory_t mem = { memory.data(), MEM_SIZE }; rv64_state_t state = {}; state.pc = 0; state.x[2] = MEM_SIZE - 16; rv64_interp_run(&state, &mem, test_ecall, nullptr); double fmadd_r, fmsub_r, fnmsub_r, fnmadd_r; memcpy(&fmadd_r, &state.x[20], 8); memcpy(&fmsub_r, &state.x[21], 8); memcpy(&fnmsub_r, &state.x[22], 8); memcpy(&fnmadd_r, &state.x[23], 8); CHECK_FEQ("FMADD.D 3*5+7", fmadd_r, 22.0); CHECK_FEQ("FMSUB.D 3*5-7", fmsub_r, 8.0); CHECK_FEQ("FNMSUB.D -(3*5)+7", fnmsub_r, -8.0); CHECK_FEQ("FNMADD.D -(3*5)-7", fnmadd_r, -22.0); } // #1333: CSR access must not be misdecoded as ECALL/EBREAK, and the // fflags/frm/fcsr trio must round-trip on both interpreter and DBT. // static void test_csr_fcsr_family() { printf("test_csr_fcsr_family...\n"); // CSRRWI frm, 1 → fcsr.frm = 1 (RTZ) // CSRRWI fflags, 0x1F → set all exception flags // CSRRS x5, fcsr, x0 → read full fcsr into x5 (should be 0x3F) // CSRRWI frm, 0 → clear frm // CSRRS x6, fcsr, x0 → read fcsr (should be 0x1F — flags remain) // CSRRW x7, fflags, x0 → read fflags, write 0 // exit with a0 = x5, a1 = x6, a2 = x7 // std::vector code = { CSRRWI(0, 0x002, 1), // frm = 1 CSRRWI(0, 0x001, 0x1F), // fflags = 0x1F CSRRS(5, 0x003, 0), // x5 = fcsr CSRRWI(0, 0x002, 0), // frm = 0 CSRRS(6, 0x003, 0), // x6 = fcsr CSRRW(7, 0x001, 0), // x7 = fflags; fflags = 0 ADDI(10, 5, 0), // a0 = x5 ADDI(11, 6, 0), // a1 = x6 ADDI(12, 7, 0), // a2 = x7 ADDI(17, 0, 93), ECALL() }; // Interpreter path. // { const size_t MEM_SIZE = 4096; std::vector memory(MEM_SIZE, 0); for (size_t i = 0; i < code.size(); i++) { memcpy(memory.data() + i * 4, &code[i], 4); } rv64_state_t state{}; rv64_memory_t mem = { memory.data(), MEM_SIZE }; int rc = rv64_interp_run(&state, &mem, test_ecall, nullptr); CHECK_EQ("csr interp: exit rc", static_cast(rc), 0x3FULL); CHECK_EQ("csr interp: fcsr after frm=1|fflags", state.x[5], 0x3FULL); CHECK_EQ("csr interp: fcsr after frm clear", state.x[6], 0x1FULL); CHECK_EQ("csr interp: fflags readback", state.x[7], 0x1FULL); CHECK_EQ("csr interp: fcsr final", static_cast(state.fcsr), 0ULL); } // DBT path. // { const size_t MEM_SIZE = 64 * 1024; std::vector memory(MEM_SIZE, 0); for (size_t i = 0; i < code.size(); i++) { memcpy(memory.data() + i * 4, &code[i], 4); } dbt_state_t dbt; if (dbt_init(&dbt, memory.data(), MEM_SIZE, dbt_test_ecall2, nullptr) != 0) { g_tests_run++; g_tests_failed++; fprintf(stderr, " FAIL: test_csr_fcsr_family: dbt_init\n"); return; } dbt.max_dispatch = 10000; g_dbt_exit_ctx = {}; int rc = dbt_run(&dbt, 0, MEM_SIZE - 16); // dbt_test_ecall2 returns 0; results live in the exit context. // g_tests_run++; if (rc == 0) { g_tests_passed++; } else { g_tests_failed++; fprintf(stderr, " FAIL: csr dbt: dbt_run rc=%d, expected 0\n", rc); } CHECK_EQ("csr dbt: a0 (x5 snapshot)", g_dbt_exit_ctx.x[10], 0x3FULL); CHECK_EQ("csr dbt: fcsr after frm=1|fflags", g_dbt_exit_ctx.x[5], 0x3FULL); CHECK_EQ("csr dbt: fcsr after frm clear", g_dbt_exit_ctx.x[6], 0x1FULL); CHECK_EQ("csr dbt: fflags readback", g_dbt_exit_ctx.x[7], 0x1FULL); CHECK_EQ("csr dbt: fcsr final", static_cast(g_dbt_exit_ctx.fcsr), 0ULL); // Must not have burned the dispatch budget (old livelock path). // g_tests_run++; if (dbt.dispatch_count < 100) { g_tests_passed++; } else { g_tests_failed++; fprintf(stderr, " FAIL: csr dbt: dispatch_count=%llu (suspected livelock)\n", static_cast(dbt.dispatch_count)); } dbt_cleanup(&dbt); } } // #1333: CSR number 0x001 (fflags) must never be taken as EBREAK. // static void test_csr_fflags_not_ebreak() { printf("test_csr_fflags_not_ebreak...\n"); std::vector code = { CSRRWI(0, 0x001, 3), // fflags = 3 CSRRS(10, 0x001, 0), // a0 = fflags ADDI(17, 0, 93), ECALL() }; const size_t MEM_SIZE = 64 * 1024; std::vector memory(MEM_SIZE, 0); for (size_t i = 0; i < code.size(); i++) { memcpy(memory.data() + i * 4, &code[i], 4); } dbt_state_t dbt; if (dbt_init(&dbt, memory.data(), MEM_SIZE, dbt_test_ecall2, nullptr) != 0) { g_tests_run++; g_tests_failed++; fprintf(stderr, " FAIL: test_csr_fflags_not_ebreak: dbt_init\n"); return; } dbt.max_dispatch = 10000; g_dbt_exit_ctx = {}; int rc = dbt_run(&dbt, 0, MEM_SIZE - 16); // Pre-fix: dbt_run returned -1 with "EBREAK at 0x0". Now it should // exit via ECALL with a0 = 3 (dbt_test_ecall2 returns 0; check ctx). // g_tests_run++; if (rc == 0) { g_tests_passed++; } else { g_tests_failed++; fprintf(stderr, " FAIL: csr fflags: dbt_run rc=%d (pre-fix: -1 EBREAK)\n", rc); } CHECK_EQ("csr fflags: a0", g_dbt_exit_ctx.x[10], 3ULL); CHECK_EQ("csr fflags: fcsr", static_cast(g_dbt_exit_ctx.fcsr), 3ULL); dbt_cleanup(&dbt); } // #1333: unsupported CSR numbers refuse the block (no same-PC spin). // static void test_unsupported_csr_refuses_block() { printf("test_unsupported_csr_refuses_block...\n"); // cycle (0xC00) is not implemented. // std::vector code = { CSRRS(5, 0xC00, 0), ADDI(10, 5, 0), ADDI(17, 0, 93), ECALL() }; const size_t MEM_SIZE = 64 * 1024; std::vector memory(MEM_SIZE, 0); for (size_t i = 0; i < code.size(); i++) { memcpy(memory.data() + i * 4, &code[i], 4); } dbt_state_t dbt; if (dbt_init(&dbt, memory.data(), MEM_SIZE, dbt_test_ecall2, nullptr) != 0) { g_tests_run++; g_tests_failed++; fprintf(stderr, " FAIL: test_unsupported_csr_refuses_block: dbt_init\n"); return; } dbt.max_dispatch = 1000; jit_write_begin(); uint8_t *native = dbt_backend_translate_block(&dbt, 0); CHECK_EQ("unsupported csr: translate returns null", reinterpret_cast(native), 0ULL); g_dbt_exit_ctx = {}; int rc = dbt_run(&dbt, 0, MEM_SIZE - 16); g_tests_run++; if (rc == -1) { g_tests_passed++; } else { g_tests_failed++; fprintf(stderr, " FAIL: unsupported csr: dbt_run rc=%d, expected -1\n", rc); } dbt_cleanup(&dbt); } // #1323: unhandled guest instructions must refuse block translation, not // silently advance past the insn (leaving rd stale) or spin at the same PC. // FCLASS.D (funct3=1) is unimplemented on every host backend today. // static void test_unhandled_refuses_block() { printf("test_unhandled_refuses_block...\n"); // Poison x5, then an unhandled FCLASS.D that would write x5, then a // sentinel that only runs if the unhandled insn was skipped, then exit. // std::vector code = { ADDI(5, 0, 0x55AA), // x5 = poison r_type(OP_FP, 5, 1, 0, 0, (FP_FCLASS << 2) | FP_FMT_D), // FCLASS.D x5, f0 ADDI(5, 0, 0x0BEE), // sentinel if skip ADDI(10, 5, 0), // a0 = x5 ADDI(17, 0, 93), ECALL() }; const size_t MEM_SIZE = 64 * 1024; std::vector memory(MEM_SIZE, 0); for (size_t i = 0; i < code.size(); i++) { memcpy(memory.data() + i * 4, &code[i], 4); } dbt_state_t dbt; if (dbt_init(&dbt, memory.data(), MEM_SIZE, dbt_test_ecall2, nullptr) != 0) { g_tests_run++; g_tests_failed++; fprintf(stderr, " FAIL: test_unhandled_refuses_block: dbt_init\n"); return; } dbt.max_dispatch = 10000; // Translate of the entry block must fail — not produce a runnable stub. // dbt.xlate_fail = dbt_state_t::XLATE_OK; jit_write_begin(); uint8_t *native = dbt_backend_translate_block(&dbt, 0); CHECK_EQ("unhandled refuse: translate returns null", reinterpret_cast(native), 0ULL); CHECK_EQ("unhandled refuse: xlate_fail is REFUSE", static_cast(dbt.xlate_fail), static_cast(dbt_state_t::XLATE_REFUSE)); // dbt_run must not silently skip to the sentinel (a0 == 0x0BEE). // g_dbt_exit_ctx = {}; int rc = dbt_run(&dbt, 0, MEM_SIZE - 16); g_tests_run++; if (rc == -1) { g_tests_passed++; } else { g_tests_failed++; fprintf(stderr, " FAIL: unhandled refuse: dbt_run rc=%d, expected -1\n", rc); } // If skip were still in effect, ecall would exit with a0 = 0x0BEE. CHECK_EQ("unhandled refuse: sentinel not executed", g_dbt_exit_ctx.x[10], 0ULL); // Refuse must not be miscounted as buffer-full (#1331). // CHECK_EQ("unhandled refuse: code_full stays 0", dbt.code_full, 0ULL); CHECK_EQ("unhandled refuse: code_reclaims stays 0", dbt.code_reclaims, 0ULL); dbt_cleanup(&dbt); } // #1331: a refuse after program code is already in the buffer must not // reclaim that region (would burn the 1/run thrash budget and wipe live // translations for a non-occupancy failure). // static void test_refuse_does_not_reclaim() { printf("test_refuse_does_not_reclaim...\n"); // Block A at 0: ADDI; ECALL — fills some program space. // Block B at 0x40: unhandled FCLASS then ECALL — refuse. // std::vector code_a = { ADDI(10, 0, 7), ADDI(17, 0, 93), ECALL() }; std::vector code_b = { r_type(OP_FP, 5, 1, 0, 0, (FP_FCLASS << 2) | FP_FMT_D), ADDI(10, 0, 0), ADDI(17, 0, 93), ECALL() }; const size_t MEM_SIZE = 64 * 1024; std::vector memory(MEM_SIZE, 0); for (size_t i = 0; i < code_a.size(); i++) { memcpy(memory.data() + i * 4, &code_a[i], 4); } const uint64_t block_b_pc = 0x40; for (size_t i = 0; i < code_b.size(); i++) { memcpy(memory.data() + block_b_pc + i * 4, &code_b[i], 4); } dbt_state_t dbt; if (dbt_init(&dbt, memory.data(), MEM_SIZE, dbt_test_ecall2, nullptr) != 0) { g_tests_run++; g_tests_failed++; fprintf(stderr, " FAIL: test_refuse_does_not_reclaim: dbt_init\n"); return; } dbt.max_dispatch = 10000; // Translate block A successfully so code_used > 0 (and > blob if any). // jit_write_begin(); uint8_t *a = dbt_backend_translate_block(&dbt, 0); g_tests_run++; if (a) { g_tests_passed++; dbt_cache_insert(&dbt, 0, a); } else { g_tests_failed++; fprintf(stderr, " FAIL: refuse/reclaim: block A translate failed\n"); dbt_cleanup(&dbt); return; } const uint32_t used_after_a = dbt.code_used; const uint64_t reclaims_before = dbt.code_reclaims; // Pretend a blob boundary exists below A so reclaim would be eligible // if we wrongly treated refuse as FULL. // dbt.blob_code_end = 1; dbt.reclaims_this_run = 0; dbt.xlate_fail = dbt_state_t::XLATE_OK; jit_write_begin(); uint8_t *b = dbt_backend_translate_block(&dbt, block_b_pc); CHECK_EQ("refuse/reclaim: B returns null", reinterpret_cast(b), 0ULL); CHECK_EQ("refuse/reclaim: B is REFUSE", static_cast(dbt.xlate_fail), static_cast(dbt_state_t::XLATE_REFUSE)); // Drive the dispatch path that used to reclaim on any null: start at B. // g_dbt_exit_ctx = {}; int rc = dbt_run(&dbt, block_b_pc, MEM_SIZE - 16); g_tests_run++; if (rc == -1) { g_tests_passed++; } else { g_tests_failed++; fprintf(stderr, " FAIL: refuse/reclaim: dbt_run rc=%d, expected -1\n", rc); } CHECK_EQ("refuse/reclaim: code_full stays 0", dbt.code_full, 0ULL); CHECK_EQ("refuse/reclaim: no reclaim", dbt.code_reclaims, reclaims_before); // Program region for A must still be present (code_used not rewound // to blob_code_end=1). // g_tests_run++; if (dbt.code_used >= used_after_a) { g_tests_passed++; } else { g_tests_failed++; fprintf(stderr, " FAIL: refuse/reclaim: code_used=%u rewound below %u\n", dbt.code_used, used_after_a); } dbt_cleanup(&dbt); } static void test_fp_fclass() { printf("test_fp_fclass...\n"); // FCLASS.D returns a 10-bit mask classifying the FP value. double pos_normal = 42.0; double neg_inf; uint64_t neg_inf_bits = 0xFFF0000000000000ULL; // -infinity memcpy(&neg_inf, &neg_inf_bits, 8); double pos_zero = 0.0; uint64_t pn_bits, ni_bits, pz_bits; memcpy(&pn_bits, &pos_normal, 8); memcpy(&ni_bits, &neg_inf, 8); memcpy(&pz_bits, &pos_zero, 8); const size_t MEM_SIZE = 64 * 1024; std::vector memory(MEM_SIZE, 0); memcpy(memory.data() + 0x1000, &pn_bits, 8); memcpy(memory.data() + 0x1008, &ni_bits, 8); memcpy(memory.data() + 0x1010, &pz_bits, 8); std::vector code = { LUI(1, 0x1000), i_type(OP_FP_LOAD, 1, 3, 1, 0), // f1 = +42.0 i_type(OP_FP_LOAD, 2, 3, 1, 8), // f2 = -inf i_type(OP_FP_LOAD, 3, 3, 1, 16), // f3 = +0.0 r_type(OP_FP, 4, 1, 1, 0, (FP_FCLASS << 2) | FP_FMT_D), // FCLASS x4, f1 r_type(OP_FP, 5, 1, 2, 0, (FP_FCLASS << 2) | FP_FMT_D), // FCLASS x5, f2 r_type(OP_FP, 6, 1, 3, 0, (FP_FCLASS << 2) | FP_FMT_D), // FCLASS x6, f3 ADDI(17, 0, 93), ADDI(10, 0, 0), ECALL() }; for (size_t i = 0; i < code.size(); i++) { memcpy(memory.data() + i * 4, &code[i], 4); } rv64_memory_t mem = { memory.data(), MEM_SIZE }; rv64_state_t state = {}; state.pc = 0; state.x[2] = MEM_SIZE - 16; rv64_interp_run(&state, &mem, test_ecall, nullptr); CHECK_EQ("FCLASS +normal", state.x[4], 1 << 6); // bit 6 = +normal CHECK_EQ("FCLASS -inf", state.x[5], 1 << 0); // bit 0 = -inf CHECK_EQ("FCLASS +zero", state.x[6], 1 << 4); // bit 4 = +zero } static void test_fp_fmv_dx() { printf("test_fp_fmv_dx...\n"); // FMV.D.X: move integer bits to FP, then FMV.X.D back. double val = 123.456; uint64_t bits; memcpy(&bits, &val, 8); auto r = run_code({ // Load bits into x1 via LUI+ADDI sequence (hard with 64-bit). // Instead, use FCVT to get a known value, then FMV.X.D, then FMV.D.X. ADDI(1, 0, 42), r_type(OP_FP, 1, 0, 1, 2, (FP_FCVTDW << 2) | FP_FMT_D), // FCVT.D.L f1, x1 -> f1 = 42.0 r_type(OP_FP, 2, 0, 1, 0, (FP_FCLASS << 2) | FP_FMT_D), // FMV.X.D x2, f1 -> x2 = bits(42.0) r_type(OP_FP, 3, 0, 2, 0, (FP_FMVDX << 2) | FP_FMT_D), // FMV.D.X f3, x2 -> f3 = 42.0 r_type(OP_FP, 4, 0, 3, 0, (FP_FCLASS << 2) | FP_FMT_D), // FMV.X.D x4, f3 -> x4 = bits(42.0) ADDI(17, 0, 93), ADDI(10, 0, 0), ECALL() }); CHECK_EQ("FMV roundtrip", r.state.x[2], r.state.x[4]); double result; memcpy(&result, &r.state.x[4], 8); CHECK_FEQ("FMV.D.X value", result, 42.0); } // --------------------------------------------------------------- // Cross-compiled ELF test // --------------------------------------------------------------- // ECALL handler for ELF binaries: exit + write. // static int elf_ecall(rv64_state_t *state, void *user_data) { rv64_memory_t *mem = static_cast(user_data); uint64_t syscall_num = state->x[17]; // a7 switch (syscall_num) { case 93: // exit(code) return static_cast(state->x[10]); case 64: { // write(fd, buf, len) uint64_t fd = state->x[10]; uint64_t buf = state->x[11]; uint64_t len = state->x[12]; if (buf + len > mem->size) { state->x[10] = static_cast(-1LL); return -1; } dbt_ssize_t written = dbt_write(static_cast(fd), mem->data + buf, static_cast(len)); state->x[10] = static_cast(written); return -1; // continue } default: fprintf(stderr, "elf_ecall: unhandled ecall %llu at PC=0x%llX\n", (unsigned long long)syscall_num, (unsigned long long)(state->pc - 4)); return -1; } } static bool run_elf_test(const char *path) { printf("\nRunning ELF: %s\n", path); rv64_binary_t bin; if (rv64_load_elf(path, &bin) != 0) { fprintf(stderr, "Failed to load %s\n", path); return false; } rv64_state_t state = {}; state.pc = bin.entry_point; state.x[2] = bin.stack_top; // sp rv64_memory_t mem = { bin.memory, bin.memory_size }; int rc = rv64_interp_run(&state, &mem, elf_ecall, &mem); printf("ELF exited with code %d (%llu instructions)\n", rc, (unsigned long long)state.insn_count); rv64_free_binary(&bin); return rc == 0; } // --------------------------------------------------------------- // DBT (JIT) ELF test // --------------------------------------------------------------- struct dbt_ecall_ctx { uint8_t *memory; size_t memory_size; }; static int dbt_elf_ecall2(rv64_ctx_t *ctx, void *user_data) { dbt_ecall_ctx *ec = static_cast(user_data); uint64_t syscall_num = ctx->x[17]; switch (syscall_num) { case 93: return static_cast(ctx->x[10]); case 64: { uint64_t fd = ctx->x[10]; uint64_t buf = ctx->x[11]; uint64_t len = ctx->x[12]; if (buf + len > ec->memory_size) { ctx->x[10] = static_cast(-1LL); return -1; } dbt_ssize_t written = dbt_write(static_cast(fd), ec->memory + buf, static_cast(len)); ctx->x[10] = static_cast(written); return -1; } default: fprintf(stderr, "dbt_elf_ecall: unhandled ecall %llu\n", (unsigned long long)syscall_num); return -1; } } static bool run_dbt_elf_test(const char *path) { printf("\nRunning DBT/JIT: %s\n", path); rv64_binary_t bin; if (rv64_load_elf(path, &bin) != 0) { fprintf(stderr, "Failed to load %s\n", path); return false; } dbt_ecall_ctx ec = { bin.memory, bin.memory_size }; dbt_state_t dbt; if (dbt_init(&dbt, bin.memory, bin.memory_size, dbt_elf_ecall2, &ec) != 0) { fprintf(stderr, "Failed to init DBT\n"); rv64_free_binary(&bin); return false; } int rc = dbt_run(&dbt, bin.entry_point, bin.stack_top); printf("DBT exited with code %d (%llu blocks, %llu hits, %llu misses)\n", rc, (unsigned long long)dbt.blocks_translated, (unsigned long long)dbt.cache_hits, (unsigned long long)dbt.cache_misses); dbt_cleanup(&dbt); rv64_free_binary(&bin); return rc == 0; } // --------------------------------------------------------------- // Main // --------------------------------------------------------------- // Self-loop with more live non-pinned registers than the a64 register // cache has free slots. The translator forms a "warm-loop" superblock that // keeps the loop body resident across the back-edge; with a0-a3 pinned, only // 4 slots are free, but this loop reads 5 loop-invariant registers (t1-t5). // A buggy translator evicts an invariant without reloading it at the back- // edge, corrupting later iterations. This was observed as itoa() producing // garbage digits for >=4-digit values (the ÷10 magic-reciprocal divisor was // the evicted invariant). Differential-tested against the interpreter. // static void test_selfloop_register_pressure() { printf("test_selfloop_register_pressure...\n"); // Reproduces the warm-loop register-eviction bug exactly as itoa hit it: // a self-loop that divides by 10 via a magic-reciprocal multiply. The // divisor magic lives in a high-numbered, loop-invariant register (t5/x30) // so it survives the warm_entry pre-load and is read at the loop top with // no reload — but it is then evicted mid-body for a working register. A // translator that doesn't reconcile the cache at the back-edge reads a // stale host register for the magic on later iterations, corrupting the // quotient. Here we sum the decimal digits of a1 (>=4 digits to get // enough iterations) and differential-test against the interpreter. // // x30 = 0xCCCCCCCCCCCCCCCD (the /10 magic), x28 = digit sum. const int A1 = 11, Q = 5, T = 6, R = 7, SUM = 28, M = 30; auto MULHU = [](int rd, int rs1, int rs2) { return r_type(OP_REG, rd, 3, rs1, rs2, 0x01); }; std::vector code = { // a1 = 123456 (0x1E000 + 576) LUI(A1, 0x1E000), ADDI(A1, A1, 576), // M = 0xCCCCCCCCCCCCCCCD LUI(M, 0xCCCCD000), ADDI(M, M, -819), // M = 0xFFFFFFFFCCCCCCCD SLLI(Q, M, 32), ADD(M, Q, M), // M = 0xCCCCCCCCCCCCCCCD ADDI(SUM, 0, 0), // sum = 0 // loop (self-loop back-edge below): MULHU(Q, A1, M), // q = high64(a1 * magic) SRLI(Q, Q, 3), // q = a1 / 10 SLLI(T, Q, 2), ADD(T, T, Q), SLLI(T, T, 1), // t = q * 10 SUB(R, A1, T), // r = a1 - q*10 (low digit) ADD(SUM, SUM, R), // sum += digit ADD(A1, Q, 0), // a1 = q BNE(A1, 0, -32), // while a1 != 0 (8 instrs back = -32) ECALL(), }; uint64_t interp = run_code(code).state.x[SUM]; uint64_t dbt = run_code_dbt(code, SUM); CHECK_EQ("self-loop regpressure: interpreter", interp, 21); // 1+2+3+4+5+6 CHECK_EQ("self-loop regpressure: DBT matches interpreter", dbt, interp); } // The over-commit guard is only as good as the pressure estimate feeding // it, and three separate paths used to slip past it. Each loop below is // arranged so the *estimate* lands on exactly the four free non-pinned // slots — so the superblock is admitted — while the body actually touches // more. The back edge re-enters at warm_entry, past the preload, so an // evicted preloaded register makes later iterations read a host register // that now holds something else; the value that leaks in is usually the // loop counter. Each is differential against the interpreter *and* pinned // to a hand-computed constant, so it cannot pass by both routes agreeing // on a wrong answer. static void test_selfloop_pressure_forward_branch() { printf("test_selfloop_pressure_forward_branch...\n"); // The scan stopped accumulating pressure at the first forward branch and // then followed the branch *target*, while the emitter records the taken // path as a cold side exit and keeps translating the fall-through. // Everything after the branch was therefore invisible to the estimate: // it saw only {x5,x9,x6,x7} and admitted a body touching nine registers. std::vector code = { ADDI(9, 0, 1000), // loop-invariant addend: preloaded, then evicted ADDI(8, 0, 5), // trip count // loop: ADD(5, 5, 9), // x5 += 1000 — reads the preloaded x9 BNE(6, 7, 24), // forward branch to the decrement; never taken ADDI(20, 20, 1), ADDI(21, 21, 2), ADDI(22, 22, 3), ADDI(23, 23, 4), ADDI(24, 24, 5), ADDI(8, 8, -1), BNE(8, 0, -32), ECALL() }; uint64_t interp = run_code(code).state.x[5]; uint64_t dbt = run_code_dbt(code, 5); CHECK_EQ("selfloop fwd-branch: interpreter", interp, 5000); // 5 * 1000 CHECK_EQ("selfloop fwd-branch: DBT matches interpreter", dbt, interp); } static void test_selfloop_pressure_regw_rs2() { printf("test_selfloop_pressure_regw_rs2...\n"); // rc_mark_used listed rs2 for OP_REG but not OP_REG32, so SUBW's second // source went uncounted. x12 is a2 — a pinned register — which keeps the // undercounted estimate at exactly the four free slots. std::vector code = { ADDI(24, 0, 100), ADDI(21, 0, 30), ADDI(8, 0, 4), // loop: SUBW(1, 24, 21), // x1 = 70 r_type(OP_REG, 12, 1, 7, 0, 0), // SLL x12, x7, x0 — pinned destination ADDI(8, 8, -1), BNE(8, 0, -12), ECALL() }; uint64_t interp = run_code(code).state.x[1]; uint64_t dbt = run_code_dbt(code, 1); CHECK_EQ("selfloop SUBW rs2: interpreter", interp, 70); // 100 - 30 CHECK_EQ("selfloop SUBW rs2: DBT matches interpreter", dbt, interp); } static void test_selfloop_pressure_fp_int_rd() { printf("test_selfloop_pressure_fp_int_rd...\n"); // FEQ/FLT/FLE, FCVT.W/WU/L/LU and FCLASS/FMV.X write an *integer* rd and // so occupy an integer cache slot, but rc_mark_referenced only took rd // for the integer opcodes. std::vector code = { ADDI(8, 0, 5), ADDI(5, 0, 42), ADDI(6, 0, 7), // loop: ADD(7, 5, 6), // x7 = 49 r_type(OP_FP, 4, 2, 0, 0, (FP_FCMP << 2) | FP_FMT_D), // FEQ.D x4, f0, f0 ADDI(8, 8, -1), BNE(8, 0, -12), ECALL() }; uint64_t interp = run_code(code).state.x[7]; uint64_t dbt = run_code_dbt(code, 7); CHECK_EQ("selfloop FP int-rd: interpreter", interp, 49); // 42 + 7 CHECK_EQ("selfloop FP int-rd: DBT matches interpreter", dbt, interp); } // FSGNJ.D/FSGNJN.D take the magnitude from rs1 and the sign from rs2 by // writing ABS(rs1) into rd and then testing rs2's sign. When rd == rs2 the // register cache hands out one host register for both, so the ABS destroyed // the very sign about to be tested — and the sign bit of an absolute value // is always clear, so the branch resolved the same way every time and the // result was -|rs1| regardless of rs2. The rs1 == rs2 shortcut in that code // is a different aliasing case and never covered this one. Golden values // cross-checked against qemu-riscv64. static void test_fsgnj_rd_aliases_rs2() { printf("test_fsgnj_rd_aliases_rs2...\n"); for (int funct3 = 0; funct3 <= 1; funct3++) { std::vector code = { ADDI(5, 0, -1), r_type(OP_FP, 3, 0, 5, 0, (FP_FMVDX << 2) | FP_FMT_D), // f3 = all ones ADDI(6, 0, 0x3FF), SLLI(6, 6, 52), r_type(OP_FP, 4, 0, 6, 0, (FP_FMVDX << 2) | FP_FMT_D), // f4 = +1.0 // FSGNJ[N].D f3, f4, f3 — rd aliases rs2. r_type(OP_FP, 3, (uint8_t)funct3, 4, 3, (FP_FSGNJ << 2) | FP_FMT_D), ECALL() }; // rs2's sign bit is set: FSGNJ copies it (-1.0), FSGNJN inverts (+1.0). uint64_t want = (funct3 == 0) ? 0xBFF0000000000000ULL : 0x3FF0000000000000ULL; uint64_t interp = run_code(code).state.f[3]; uint64_t dbt = run_code_dbt_fbits(code, 3); CHECK_EQ(funct3 == 0 ? "FSGNJ.D rd==rs2: interpreter" : "FSGNJN.D rd==rs2: interpreter", interp, want); CHECK_EQ(funct3 == 0 ? "FSGNJ.D rd==rs2: DBT matches interpreter" : "FSGNJN.D rd==rs2: DBT matches interpreter", dbt, interp); } } // Load/store with rs1 == x0 and a nonzero immediate (absolute small-address // access). The a64 backend's rc_read(x0) returns scratch X0; materializing // the offset into X0 before the address add clobbered the base, computing // 2*imm instead of imm. Each access pairs an x0-base op with a register-base // op at the same address so a wrong address can't cancel out in a roundtrip. // Differential-tested against the interpreter (issue #804). // static void test_x0_base_load_store() { printf("test_x0_base_load_store...\n"); std::vector code = { ADDI(1, 0, 0x100), // x1 = 0x100 ADDI(4, 0, 0x55), // x4 = 0x55 SD(1, 4, 0), // mem[0x100] = 0x55 (reg base) LD(5, 0, 0x100), // x5 = mem[0x100] (x0 base) ADDI(6, 0, 0x77), // x6 = 0x77 s_type(OP_STORE, ST_SD, 0, 6, 0x108), // mem[0x108] = 0x77 (x0 base) LD(7, 1, 8), // x7 = mem[0x108] (reg base) i_type(OP_FP_LOAD, 1, 3, 0, 0x100), // FLD f1, 0x100(x0) s_type(OP_FP_STORE, 3, 0, 1, 0x110), // FSD f1, 0x110(x0) LD(8, 1, 0x10), // x8 = mem[0x110] (reg base) ECALL(), }; auto r = run_code(code); CHECK_EQ("LD imm(x0): interpreter", r.state.x[5], 0x55ULL); CHECK_EQ("SD imm(x0): interpreter", r.state.x[7], 0x77ULL); CHECK_EQ("FLD/FSD imm(x0): interpreter", r.state.x[8], 0x55ULL); CHECK_EQ("LD imm(x0): DBT", run_code_dbt(code, 5), 0x55ULL); CHECK_EQ("SD imm(x0): DBT", run_code_dbt(code, 7), 0x77ULL); CHECK_EQ("FLD/FSD imm(x0): DBT", run_code_dbt(code, 8), 0x55ULL); } // OP_IMM with rs1 == x0 (issue #809). The a64 backend's rc_read(x0) // returns scratch X0; materializing the immediate into X0 clobbered the // x0 operand, so SLTI/SLTIU compared imm against itself and the // XORI/ORI/ANDI logical-immediate fallback computed imm OP imm. // static void test_x0_operand_alu() { printf("test_x0_operand_alu...\n"); std::vector code = { i_type(OP_IMM, 5, ALU_SLTI, 0, 5), // slti x5, x0, 5 = 1 i_type(OP_IMM, 6, ALU_SLTI, 0, -5), // slti x6, x0, -5 = 0 i_type(OP_IMM, 7, ALU_SLTIU, 0, 1), // seqz x7, x0 = 1 i_type(OP_IMM, 8, ALU_XORI, 0, -1), // not x8, x0 = -1 i_type(OP_IMM, 9, ALU_ANDI, 0, -1), // andi x9, x0, -1 = 0 i_type(OP_IMM, 11, ALU_ORI, 0, 5), // ori x11, x0, 5 = 5 i_type(OP_IMM, 12, ALU_XORI, 0, 5), // xori x12, x0, 5 = 5 ECALL(), }; auto r = run_code(code); CHECK_EQ("slti x0,5: interp", r.state.x[5], 1ULL); CHECK_EQ("slti x0,-5: interp", r.state.x[6], 0ULL); CHECK_EQ("seqz x0: interp", r.state.x[7], 1ULL); CHECK_EQ("not x0: interp", r.state.x[8], ~0ULL); CHECK_EQ("andi x0,-1: interp", r.state.x[9], 0ULL); CHECK_EQ("ori x0,5: interp", r.state.x[11], 5ULL); CHECK_EQ("xori x0,5: interp", r.state.x[12], 5ULL); CHECK_EQ("slti x0,5: DBT", run_code_dbt(code, 5), 1ULL); CHECK_EQ("slti x0,-5: DBT", run_code_dbt(code, 6), 0ULL); CHECK_EQ("seqz x0: DBT", run_code_dbt(code, 7), 1ULL); CHECK_EQ("not x0: DBT", run_code_dbt(code, 8), ~0ULL); CHECK_EQ("andi x0,-1: DBT", run_code_dbt(code, 9), 0ULL); CHECK_EQ("ori x0,5: DBT", run_code_dbt(code, 11), 5ULL); CHECK_EQ("xori x0,5: DBT", run_code_dbt(code, 12), 5ULL); } // SLT+branch fusion paths with x0 operands (issue #809). // (a) slti rs1==x0 + branch over TWO insns: the fused compare itself // must not clobber the x0 operand. // (b) slti rs1==x0 + branch over ONE insn (CSEL diamond), not taken: // re-emitting the compare must not wipe the predicated result in // X0 (rc_read(x0) emits MOV X0, XZR) before the CSEL consumes it. // (c) diamond whose skipped insn is `add rd, rs1, x0`: the predicated // OP_REG path must not clobber the zero rs2 while staging rs1. // static void test_slt_branch_fusion_x0() { printf("test_slt_branch_fusion_x0...\n"); // (a) slti x5, x0, 5 = 1 → bne taken → both ADDIs skipped. std::vector code_a = { i_type(OP_IMM, 5, ALU_SLTI, 0, 5), BNE(5, 0, 12), ADDI(6, 0, 111), ADDI(7, 0, 222), ADDI(8, 0, 99), ECALL(), }; auto ra = run_code(code_a); CHECK_EQ("fused slti x0 taken: interp x6", ra.state.x[6], 0ULL); CHECK_EQ("fused slti x0 taken: interp x8", ra.state.x[8], 99ULL); CHECK_EQ("fused slti x0 taken: DBT x6", run_code_dbt(code_a, 6), 0ULL); CHECK_EQ("fused slti x0 taken: DBT x7", run_code_dbt(code_a, 7), 0ULL); CHECK_EQ("fused slti x0 taken: DBT x8", run_code_dbt(code_a, 8), 99ULL); // (b) slti x5, x0, -5 = 0 → bne not taken → ADDI executes; the // diamond CSEL must select the new value still held in X0. std::vector code_b = { i_type(OP_IMM, 5, ALU_SLTI, 0, -5), BNE(5, 0, 8), ADDI(6, 0, 111), ECALL(), }; auto rb = run_code(code_b); CHECK_EQ("diamond slti x0 not-taken: interp x6", rb.state.x[6], 111ULL); CHECK_EQ("diamond slti x0 not-taken: DBT x5", run_code_dbt(code_b, 5), 0ULL); CHECK_EQ("diamond slti x0 not-taken: DBT x6", run_code_dbt(code_b, 6), 111ULL); // (c) skipped insn is `add x10, x9, x0` — must yield x9, not 2*x9. std::vector code_c = { ADDI(9, 0, 7), i_type(OP_IMM, 5, ALU_SLTI, 9, 100), // x5 = (7 < 100) = 1 BEQ(5, 0, 8), // not taken ADD(10, 9, 0), // x10 = x9 + x0 = 7 ECALL(), }; auto rc = run_code(code_c); CHECK_EQ("diamond add rs2==x0: interp", rc.state.x[10], 7ULL); CHECK_EQ("diamond add rs2==x0: DBT", run_code_dbt(code_c, 10), 7ULL); // (d) taken diamond (no x0 operands): the CSEL must KEEP the old // value when the branch is taken and the insn is skipped. std::vector code_d = { ADDI(9, 0, 7), i_type(OP_IMM, 5, ALU_SLTI, 9, 100), // x5 = 1 BNE(5, 0, 8), // taken → skip ADDI(10, 0, 111), // skipped → x10 stays 0 ECALL(), }; auto rd = run_code(code_d); CHECK_EQ("diamond taken keeps old: interp", rd.state.x[10], 0ULL); CHECK_EQ("diamond taken keeps old: DBT", run_code_dbt(code_d, 10), 0ULL); } // MULHSU sign-correction hazards (found with issue #809). The // correction term must be computed from the ORIGINAL rs1/rs2: with // rs1 == x0 the AND self-aliased scratch X0, and with rd aliasing // rs1/rs2 the SMULH overwrote an input the correction still needed. // static void test_mulhsu_aliasing() { printf("test_mulhsu_aliasing...\n"); auto MULHSU = [](int rd, int rs1, int rs2) { return r_type(OP_REG, rd, ALU_SLT, rs1, rs2, 0x01); }; std::vector code = { ADDI(11, 0, -1), // x11 = all-ones (unsigned 2^64-1) MULHSU(12, 0, 11), // x12 = high(0 × 2^64-1) = 0 ADDI(13, 0, 3), ADDI(14, 0, -1), MULHSU(13, 13, 14), // rd==rs1: high(3 × 2^64-1) = 2 ADDI(15, 0, -1), ADDI(16, 0, 1), SLLI(16, 16, 63), // x16 = 2^63 (sign bit set) MULHSU(16, 15, 16), // rd==rs2: high(-1 × 2^63) = -1 ECALL(), }; auto r = run_code(code); CHECK_EQ("mulhsu rs1==x0: interp", r.state.x[12], 0ULL); CHECK_EQ("mulhsu rd==rs1: interp", r.state.x[13], 2ULL); CHECK_EQ("mulhsu rd==rs2: interp", r.state.x[16], ~0ULL); CHECK_EQ("mulhsu rs1==x0: DBT", run_code_dbt(code, 12), 0ULL); CHECK_EQ("mulhsu rd==rs1: DBT", run_code_dbt(code, 13), 2ULL); CHECK_EQ("mulhsu rd==rs2: DBT", run_code_dbt(code, 16), ~0ULL); } // M-extension div/rem edge cases, differential against the interpreter // (issue #811). x86 idiv/div raise #DE (SIGFPE) on a zero divisor, and // idiv also on INT_MIN / -1; RV64 defines non-trapping results for all // of these (div-by-zero -> all ones / dividend, signed overflow -> // INT_MIN / 0). The unguarded x64 backends crashed the whole process // here. Covers all eight forms at both trap points, a divisor of x0 // (whose host register aliases scratch RAX), a W-form divisor whose low // 32 bits are zero but whose full 64 bits are not, and ordinary // quotients to confirm the guarded sequences still divide correctly. // static void test_div_rem_edge_cases() { printf("test_div_rem_edge_cases...\n"); std::vector code = { ADDI(1, 0, 1), SLLI(1, 1, 63), // x1 = INT64_MIN ADDI(2, 0, -1), // x2 = -1 ADDI(3, 0, 0), // x3 = 0 (runtime zero divisor) ADDI(4, 0, 42), // x4 = 42 ADDI(5, 0, 7), // x5 = 7 ADDI(6, 0, -100), // x6 = -100 ADDI(7, 0, 1), SLLI(7, 7, 32), // x7 = 1<<32 (low 32 bits zero) ADDI(8, 0, 1), SLLI(8, 8, 31), // x8 = 1<<31 (low 32 = INT32_MIN) // 64-bit forms, zero divisor. DIV(9, 4, 3), DIVU(10, 4, 3), REM(11, 4, 3), REMU(12, 4, 3), // 64-bit signed overflow: INT64_MIN / -1. DIV(13, 1, 2), REM(14, 1, 2), // Divisor is x0 itself. DIV(15, 4, 0), REM(16, 4, 0), // W forms, zero divisor. DIVW(17, 4, 3), DIVUW(18, 4, 3), REMW(19, 4, 3), REMUW(20, 4, 3), // W-form signed overflow: INT32_MIN / -1. DIVW(21, 8, 2), REMW(22, 8, 2), // W-form divisor with zero low 32 bits but nonzero upper bits. // This also guards the AArch64 div-by-zero "-1" encoding: it must be // ORN Xd,XZR,XZR (Rm=31), not Rm=0 (= ~X0). The preceding INT32_MIN // /-1 leaves 0x80000000 in the X0 scratch, so a wrong Rm yields // ~0x...80000000 = 0x7FFFFFFF instead of -1. DIVW(23, 4, 7), REMW(24, 4, 7), // Ordinary divisions through the guarded sequences. DIV(25, 4, 5), REM(26, 6, 5), DIV(27, 6, 5), DIVU(28, 2, 5), REMU(29, 2, 5), DIVW(30, 6, 5), REMUW(31, 2, 5), ECALL(), }; static const struct { const char *name; int reg; uint64_t expect; } cases[] = { { "div 42/0", 9, UINT64_MAX }, { "divu 42/0", 10, UINT64_MAX }, { "rem 42%0", 11, 42 }, { "remu 42%0", 12, 42 }, { "div MIN/-1", 13, 0x8000000000000000ULL }, { "rem MIN%-1", 14, 0 }, { "div 42/x0", 15, UINT64_MAX }, { "rem 42%x0", 16, 42 }, { "divw 42/0", 17, UINT64_MAX }, { "divuw 42/0", 18, UINT64_MAX }, { "remw 42%0", 19, 42 }, { "remuw 42%0", 20, 42 }, { "divw MIN32/-1", 21, 0xFFFFFFFF80000000ULL }, { "remw MIN32%-1", 22, 0 }, { "divw 42/(1<<32)", 23, UINT64_MAX }, { "remw 42%(1<<32)", 24, 42 }, { "div 42/7", 25, 6 }, { "rem -100%7", 26, static_cast(-2LL) }, { "div -100/7", 27, static_cast(-14LL) }, { "divu MAX/7", 28, 0x2492492492492492ULL }, { "remu MAX%7", 29, 1 }, { "divw -100/7", 30, static_cast(-14LL) }, { "remuw 0xFFFFFFFF%7", 31, 3 }, }; auto r = run_code(code); for (const auto& c : cases) { char desc[64]; snprintf(desc, sizeof(desc), "%s: interp", c.name); CHECK_EQ(desc, r.state.x[c.reg], c.expect); } // One DBT run translates the whole sequence; pre-#811 the first // unguarded idiv took the process down with SIGFPE right here. run_code_dbt(code, 0); for (const auto& c : cases) { char desc[64]; snprintf(desc, sizeof(desc), "%s: DBT", c.name); CHECK_EQ(desc, g_dbt_exit_ctx.x[c.reg], c.expect); } } // --------------------------------------------------------------- // rv64_alloc intrinsic emitter (DBT_EMIT_ALLOC). // // The guest passes a size in a0 and gets back a *guest* address in a0. // The emitter must NOT host<->guest convert either the argument or the // return (ptr_mask=0): the host bump-allocator returns a guest offset // the guest then dereferences directly. This test registers a stub // allocator at a guest address, JALs to it twice, and verifies the // returned offsets are passed through verbatim, advance by the aligned // size, and are usable as guest pointers (store/load round-trip). // --------------------------------------------------------------- static uint64_t g_test_alloc_cursor; static uint64_t test_host_alloc(uint64_t size) { uint64_t a = g_test_alloc_cursor; g_test_alloc_cursor += (size + 15) & ~15ULL; // 16-byte align return a; // guest offset, not a host ptr } static void test_intrinsic_alloc() { printf("test_intrinsic_alloc...\n"); const size_t MEM_SIZE = 64 * 1024; std::vector memory(MEM_SIZE, 0); const uint32_t ALLOC_ADDR = 0x400; // intrinsic stub guest PC (no real code) g_test_alloc_cursor = 0x2000; // test heap region inside guest memory // J-type JAL encoder: imm[20|10:1|11|19:12]. auto JAL = [](uint8_t rd, uint32_t pc, uint32_t target) -> uint32_t { uint32_t i = static_cast( static_cast(target) - static_cast(pc)); return OP_JAL | (rd << 7) | (((i >> 12) & 0xFF) << 12) | (((i >> 11) & 1) << 20) | (((i >> 1) & 0x3FF) << 21) | (((i >> 20) & 1) << 31); }; std::vector code = { ADDI(10, 0, 48), // 0: a0 = 48 JAL(1, 4, ALLOC_ADDR), // 4: a0 = alloc(48) -> 0x2000 ADDI(8, 10, 0), // 8: s0 = ptr1 ADDI(10, 0, 16), // 12: a0 = 16 JAL(1, 16, ALLOC_ADDR), // 16: a0 = alloc(16) -> 0x2030 (48->aligned) ADDI(9, 10, 0), // 20: s1 = ptr2 ADDI(5, 0, 0x5A), // 24: t0 = 0x5A SD(8, 5, 0), // 28: mem[s0] = t0 LD(6, 8, 0), // 32: t1 = mem[s0] ADDI(17, 0, 93), // 36: a7 = 93 (exit) ADDI(10, 0, 0), // 40: a0 = 0 ECALL(), // 44 }; for (size_t i = 0; i < code.size(); i++) { memcpy(memory.data() + i * 4, &code[i], 4); } dbt_state_t dbt; if (dbt_init(&dbt, memory.data(), MEM_SIZE, dbt_test_ecall2, nullptr) != 0) { g_tests_run++; g_tests_failed++; fprintf(stderr, " FAIL: test_intrinsic_alloc: dbt_init\n"); return; } dbt.max_dispatch = 1000000; dbt_register_intrinsic(&dbt, ALLOC_ADDR, DBT_EMIT_ALLOC, reinterpret_cast(test_host_alloc)); dbt_run(&dbt, 0, MEM_SIZE - 16); dbt_cleanup(&dbt); CHECK_EQ("alloc: ptr1 returned verbatim", g_dbt_exit_ctx.x[8], 0x2000ULL); CHECK_EQ("alloc: ptr2 advanced by aligned size", g_dbt_exit_ctx.x[9], 0x2030ULL); CHECK_EQ("alloc: guest ptr store/load round-trip", g_dbt_exit_ctx.x[6], 0x5AULL); } // --------------------------------------------------------------- // FCVT rounding modes (#1320) // --------------------------------------------------------------- // // RISC-V selects the rounding mode per instruction; ARM selects it per // opcode. The backend used FCVTZS/FCVTZU for everything, which truncates, // so RNE -- the default, and what the assembler emits when no mode is // written -- behaved as RTZ and turned 1.5 into 1. // // Expected values come from qemu-riscv64 executing the same instructions, // not from the interpreter: until #1319 the interpreter truncated too, so // a differential check against it would have agreed and proved nothing. // Inputs are the ones that discriminate between modes -- halfway ties in // both parities and both signs, a value above and below the halfway point, // and the boundary where rounding pushes the result out of int32 range. // Saturation and NaN are covered by test_fcvt_nan_and_range above (#1313). // struct fcvt_rm_row_t { uint64_t in; // input double, as bits uint8_t sel; // rs2: 0=W 1=WU 2=L 3=LU uint64_t expect[5]; // by rounding mode: RNE RTZ RDN RUP RMM }; static const fcvt_rm_row_t FCVT_RM_GOLDEN[] = { { 0x3FE0000000000000ULL, 0, { 0x0000000000000000ULL, 0x0000000000000000ULL, 0x0000000000000000ULL, 0x0000000000000001ULL, 0x0000000000000001ULL } }, // 0.5 fcvt.w.d { 0x3FE0000000000000ULL, 1, { 0x0000000000000000ULL, 0x0000000000000000ULL, 0x0000000000000000ULL, 0x0000000000000001ULL, 0x0000000000000001ULL } }, // 0.5 fcvt.wu.d { 0x3FE0000000000000ULL, 2, { 0x0000000000000000ULL, 0x0000000000000000ULL, 0x0000000000000000ULL, 0x0000000000000001ULL, 0x0000000000000001ULL } }, // 0.5 fcvt.l.d { 0x3FE0000000000000ULL, 3, { 0x0000000000000000ULL, 0x0000000000000000ULL, 0x0000000000000000ULL, 0x0000000000000001ULL, 0x0000000000000001ULL } }, // 0.5 fcvt.lu.d { 0x3FF8000000000000ULL, 0, { 0x0000000000000002ULL, 0x0000000000000001ULL, 0x0000000000000001ULL, 0x0000000000000002ULL, 0x0000000000000002ULL } }, // 1.5 fcvt.w.d { 0x3FF8000000000000ULL, 1, { 0x0000000000000002ULL, 0x0000000000000001ULL, 0x0000000000000001ULL, 0x0000000000000002ULL, 0x0000000000000002ULL } }, // 1.5 fcvt.wu.d { 0x3FF8000000000000ULL, 2, { 0x0000000000000002ULL, 0x0000000000000001ULL, 0x0000000000000001ULL, 0x0000000000000002ULL, 0x0000000000000002ULL } }, // 1.5 fcvt.l.d { 0x3FF8000000000000ULL, 3, { 0x0000000000000002ULL, 0x0000000000000001ULL, 0x0000000000000001ULL, 0x0000000000000002ULL, 0x0000000000000002ULL } }, // 1.5 fcvt.lu.d { 0x4004000000000000ULL, 0, { 0x0000000000000002ULL, 0x0000000000000002ULL, 0x0000000000000002ULL, 0x0000000000000003ULL, 0x0000000000000003ULL } }, // 2.5 fcvt.w.d { 0x4004000000000000ULL, 1, { 0x0000000000000002ULL, 0x0000000000000002ULL, 0x0000000000000002ULL, 0x0000000000000003ULL, 0x0000000000000003ULL } }, // 2.5 fcvt.wu.d { 0x4004000000000000ULL, 2, { 0x0000000000000002ULL, 0x0000000000000002ULL, 0x0000000000000002ULL, 0x0000000000000003ULL, 0x0000000000000003ULL } }, // 2.5 fcvt.l.d { 0x4004000000000000ULL, 3, { 0x0000000000000002ULL, 0x0000000000000002ULL, 0x0000000000000002ULL, 0x0000000000000003ULL, 0x0000000000000003ULL } }, // 2.5 fcvt.lu.d { 0x4004CCCCCCCCCCCDULL, 0, { 0x0000000000000003ULL, 0x0000000000000002ULL, 0x0000000000000002ULL, 0x0000000000000003ULL, 0x0000000000000003ULL } }, // 2.6 fcvt.w.d { 0x4004CCCCCCCCCCCDULL, 1, { 0x0000000000000003ULL, 0x0000000000000002ULL, 0x0000000000000002ULL, 0x0000000000000003ULL, 0x0000000000000003ULL } }, // 2.6 fcvt.wu.d { 0x4004CCCCCCCCCCCDULL, 2, { 0x0000000000000003ULL, 0x0000000000000002ULL, 0x0000000000000002ULL, 0x0000000000000003ULL, 0x0000000000000003ULL } }, // 2.6 fcvt.l.d { 0x4004CCCCCCCCCCCDULL, 3, { 0x0000000000000003ULL, 0x0000000000000002ULL, 0x0000000000000002ULL, 0x0000000000000003ULL, 0x0000000000000003ULL } }, // 2.6 fcvt.lu.d { 0xC004000000000000ULL, 0, { 0xFFFFFFFFFFFFFFFEULL, 0xFFFFFFFFFFFFFFFEULL, 0xFFFFFFFFFFFFFFFDULL, 0xFFFFFFFFFFFFFFFEULL, 0xFFFFFFFFFFFFFFFDULL } }, // -2.5 fcvt.w.d { 0xC004000000000000ULL, 1, { 0x0000000000000000ULL, 0x0000000000000000ULL, 0x0000000000000000ULL, 0x0000000000000000ULL, 0x0000000000000000ULL } }, // -2.5 fcvt.wu.d { 0xC004000000000000ULL, 2, { 0xFFFFFFFFFFFFFFFEULL, 0xFFFFFFFFFFFFFFFEULL, 0xFFFFFFFFFFFFFFFDULL, 0xFFFFFFFFFFFFFFFEULL, 0xFFFFFFFFFFFFFFFDULL } }, // -2.5 fcvt.l.d { 0xC004000000000000ULL, 3, { 0x0000000000000000ULL, 0x0000000000000000ULL, 0x0000000000000000ULL, 0x0000000000000000ULL, 0x0000000000000000ULL } }, // -2.5 fcvt.lu.d { 0xC004CCCCCCCCCCCDULL, 0, { 0xFFFFFFFFFFFFFFFDULL, 0xFFFFFFFFFFFFFFFEULL, 0xFFFFFFFFFFFFFFFDULL, 0xFFFFFFFFFFFFFFFEULL, 0xFFFFFFFFFFFFFFFDULL } }, // -2.6 fcvt.w.d { 0xC004CCCCCCCCCCCDULL, 1, { 0x0000000000000000ULL, 0x0000000000000000ULL, 0x0000000000000000ULL, 0x0000000000000000ULL, 0x0000000000000000ULL } }, // -2.6 fcvt.wu.d { 0xC004CCCCCCCCCCCDULL, 2, { 0xFFFFFFFFFFFFFFFDULL, 0xFFFFFFFFFFFFFFFEULL, 0xFFFFFFFFFFFFFFFDULL, 0xFFFFFFFFFFFFFFFEULL, 0xFFFFFFFFFFFFFFFDULL } }, // -2.6 fcvt.l.d { 0xC004CCCCCCCCCCCDULL, 3, { 0x0000000000000000ULL, 0x0000000000000000ULL, 0x0000000000000000ULL, 0x0000000000000000ULL, 0x0000000000000000ULL } }, // -2.6 fcvt.lu.d { 0x400C000000000000ULL, 0, { 0x0000000000000004ULL, 0x0000000000000003ULL, 0x0000000000000003ULL, 0x0000000000000004ULL, 0x0000000000000004ULL } }, // 3.5 fcvt.w.d { 0x400C000000000000ULL, 1, { 0x0000000000000004ULL, 0x0000000000000003ULL, 0x0000000000000003ULL, 0x0000000000000004ULL, 0x0000000000000004ULL } }, // 3.5 fcvt.wu.d { 0x400C000000000000ULL, 2, { 0x0000000000000004ULL, 0x0000000000000003ULL, 0x0000000000000003ULL, 0x0000000000000004ULL, 0x0000000000000004ULL } }, // 3.5 fcvt.l.d { 0x400C000000000000ULL, 3, { 0x0000000000000004ULL, 0x0000000000000003ULL, 0x0000000000000003ULL, 0x0000000000000004ULL, 0x0000000000000004ULL } }, // 3.5 fcvt.lu.d { 0xBFE0000000000000ULL, 0, { 0x0000000000000000ULL, 0x0000000000000000ULL, 0xFFFFFFFFFFFFFFFFULL, 0x0000000000000000ULL, 0xFFFFFFFFFFFFFFFFULL } }, // -0.5 fcvt.w.d { 0xBFE0000000000000ULL, 1, { 0x0000000000000000ULL, 0x0000000000000000ULL, 0x0000000000000000ULL, 0x0000000000000000ULL, 0x0000000000000000ULL } }, // -0.5 fcvt.wu.d { 0xBFE0000000000000ULL, 2, { 0x0000000000000000ULL, 0x0000000000000000ULL, 0xFFFFFFFFFFFFFFFFULL, 0x0000000000000000ULL, 0xFFFFFFFFFFFFFFFFULL } }, // -0.5 fcvt.l.d { 0xBFE0000000000000ULL, 3, { 0x0000000000000000ULL, 0x0000000000000000ULL, 0x0000000000000000ULL, 0x0000000000000000ULL, 0x0000000000000000ULL } }, // -0.5 fcvt.lu.d { 0x41DFFFFFFFE00000ULL, 0, { 0x000000007FFFFFFFULL, 0x000000007FFFFFFFULL, 0x000000007FFFFFFFULL, 0x000000007FFFFFFFULL, 0x000000007FFFFFFFULL } }, // 2^31-0.5 fcvt.w.d { 0x41DFFFFFFFE00000ULL, 1, { 0xFFFFFFFF80000000ULL, 0x000000007FFFFFFFULL, 0x000000007FFFFFFFULL, 0xFFFFFFFF80000000ULL, 0xFFFFFFFF80000000ULL } }, // 2^31-0.5 fcvt.wu.d { 0x41DFFFFFFFE00000ULL, 2, { 0x0000000080000000ULL, 0x000000007FFFFFFFULL, 0x000000007FFFFFFFULL, 0x0000000080000000ULL, 0x0000000080000000ULL } }, // 2^31-0.5 fcvt.l.d { 0x41DFFFFFFFE00000ULL, 3, { 0x0000000080000000ULL, 0x000000007FFFFFFFULL, 0x000000007FFFFFFFULL, 0x0000000080000000ULL, 0x0000000080000000ULL } }, // 2^31-0.5 fcvt.lu.d }; // Run one FCVT through the DBT. `frm` is only consulted when rm == 7, and // is seeded straight into ctx because the DBT cannot execute a CSR write to // set it (see #1333); dbt_run wipes ctx, so this goes in through dbt_resume. static uint64_t run_fcvt_dbt(uint64_t in, uint8_t sel, uint8_t rm, uint8_t frm) { const size_t MEM_SIZE = 64 * 1024; const uint64_t DATA = 0x400; // must fit a signed 12-bit ADDI immediate std::vector memory(MEM_SIZE, 0); memcpy(memory.data() + DATA, &in, 8); const uint32_t code[4] = { ADDI(9, 0, (int32_t)DATA), (uint32_t)(0x07u | (1u << 7) | (3u << 12) | (9u << 15)), // FLD f1,0(x9) (uint32_t)(0xC2000000u | (5u << 7) | ((uint32_t)rm << 12) | (1u << 15) | ((uint32_t)sel << 20) | 0x53u), // FCVT x5,f1 ECALL() }; memcpy(memory.data(), code, sizeof(code)); dbt_state_t dbt; if (dbt_init(&dbt, memory.data(), MEM_SIZE, dbt_test_ecall2, nullptr) != 0) { return ~0ULL; } dbt.max_dispatch = 1000000; dbt.ctx = {}; dbt.ctx.x[2] = MEM_SIZE - 16; dbt.ctx.mem_size = MEM_SIZE; dbt.ctx.fcsr = (uint32_t)(frm & 7u) << 5; dbt_resume(&dbt, 0); dbt_cleanup(&dbt); return g_dbt_exit_ctx.x[5]; } static void test_fcvt_rounding_modes() { printf("test_fcvt_rounding_modes...\n"); static const char *RM_NAME[5] = { "rne", "rtz", "rdn", "rup", "rmm" }; static const char *CVT[4] = { "fcvt.w.d", "fcvt.wu.d", "fcvt.l.d", "fcvt.lu.d" }; const size_t n = sizeof(FCVT_RM_GOLDEN) / sizeof(FCVT_RM_GOLDEN[0]); char desc[160]; for (size_t i = 0; i < n; i++) { const fcvt_rm_row_t &row = FCVT_RM_GOLDEN[i]; for (uint8_t rm = 0; rm < 5; rm++) { // Static mode: the rm field selects directly. uint64_t got = run_fcvt_dbt(row.in, row.sel, rm, 0); snprintf(desc, sizeof(desc), "DBT %s %s in=0x%llX -> 0x%llX (got 0x%llX)", CVT[row.sel], RM_NAME[rm], (unsigned long long)row.in, (unsigned long long)row.expect[rm], (unsigned long long)got); CHECK_EQ(desc, got, row.expect[rm]); // Dynamic mode: rm=7 must resolve to the same answer through // fcsr.frm. This is the encoding the assembler emits by // default, so it is the common case rather than the exotic one. got = run_fcvt_dbt(row.in, row.sel, 7, rm); snprintf(desc, sizeof(desc), "DBT %s dyn frm=%s in=0x%llX -> 0x%llX (got 0x%llX)", CVT[row.sel], RM_NAME[rm], (unsigned long long)row.in, (unsigned long long)row.expect[rm], (unsigned long long)got); CHECK_EQ(desc, got, row.expect[rm]); } } } // #1338: superblock side exits must not drop dirty FP state. // // Fuzzer seed-7 shape: a counted loop with two forward branches and an // fdiv on the fall-through of the first. Every iteration that executes // fdiv then takes the second side exit (bgeu), so without an FP flush at // side-exit points the NaN never lands in ctx.f[4]. // static void test_1338_superblock_side_exit_fp() { printf("test_1338_superblock_side_exit_fp...\n"); // Guest words from the issue (plus a7=93 so the interpreter ecall exits). // std::vector code = { 0x40000493u, // addi x9, x0, 1024 0x00500413u, // addi x8, x0, 5 0x00D35463u, // bge x6, x13, +8 (skip fdiv when x6>=x13) 0x1A140253u, // fdiv.d f4, f8, f1 (0/0 -> canonical NaN) 0x01B37663u, // bgeu x6, x27, +12 (always taken while both stay 0) 0xFFF00693u, // addi x13, x0, -1 0x03569693u, // slli x13, x13, 53 0x7FF68693u, // addi x13, x13, 2047 0xFFF40413u, // addi x8, x8, -1 0xFE0412E3u, // bne x8, x0, -28 (back edge to bge) 0x05D00893u, // addi x17, x0, 93 (a7 = exit) 0x00000073u, // ecall }; TestResult ir = run_code(code); const uint64_t f4_i = ir.state.f[4]; const uint64_t x13_i = ir.state.x[13]; CHECK_EQ("1338: interp f4 is canonical NaN", f4_i, 0x7FF8000000000000ULL); CHECK_EQ("1338: interp x13", x13_i, 0x27FBULL); const size_t MEM_SIZE = 64 * 1024; std::vector memory(MEM_SIZE, 0); for (size_t i = 0; i < code.size(); i++) { memcpy(memory.data() + i * 4, &code[i], 4); } dbt_state_t dbt; if (dbt_init(&dbt, memory.data(), MEM_SIZE, dbt_test_ecall2, nullptr) != 0) { g_tests_run++; g_tests_failed++; fprintf(stderr, " FAIL: test_1338: dbt_init\n"); return; } dbt.max_dispatch = 1000000; g_dbt_exit_ctx = {}; int rc = dbt_run(&dbt, 0, MEM_SIZE - 16); g_tests_run++; if (rc == 0) { g_tests_passed++; } else { g_tests_failed++; fprintf(stderr, " FAIL: test_1338: dbt_run rc=%d\n", rc); } uint64_t f4_d = 0; memcpy(&f4_d, &g_dbt_exit_ctx.f[4], 8); CHECK_EQ("1338: dbt f4 matches interp (not dropped on side exit)", f4_d, f4_i); CHECK_EQ("1338: dbt x13 matches", g_dbt_exit_ctx.x[13], x13_i); // The bug is superblock-specific; require we actually formed one so // the test cannot pass by silently disabling superblocks. // g_tests_run++; if (dbt.superblock_count >= 1) { g_tests_passed++; } else { g_tests_failed++; fprintf(stderr, " FAIL: test_1338: expected a superblock, got %llu\n", static_cast(dbt.superblock_count)); } dbt_cleanup(&dbt); } // NaN canonicalisation (#1337). // // RISC-V returns the canonical quiet NaN 0x7FF8000000000000 from any // operation that produces a NaN. It never propagates an operand's payload, // which is what both hosts do by default and what both routes therefore // used to do: `fadd.d` of a payload-carrying NaN returned that payload. // // The interpreter canonicalises explicitly; the a64 backend runs translated // code with FPCR.DN set, which makes the hardware do it for the whole // arithmetic surface. Both are checked here because the differential // fuzzer cannot distinguish "both correct" from "both wrong in the same // way" -- and before this they were wrong in the same way. Expected values // are from qemu-riscv64. // // Not covered here: FMIN/FMAX given a signalling NaN, where RISC-V returns // the *number* and ARM returns a NaN whatever DN says. Separate defect. // static void test_fp_nan_canonicalisation() { printf("test_fp_nan_canonicalisation...\n"); const uint64_t CANON = 0x7FF8000000000000ULL; struct { const char *name; uint8_t f5; } OPS[] = { { "fadd.d", FP_FADD }, { "fsub.d", FP_FSUB }, { "fmul.d", FP_FMUL }, { "fdiv.d", FP_FDIV }, }; for (size_t i = 0; i < sizeof(OPS)/sizeof(OPS[0]); i++) { for (int swap = 0; swap <= 1; swap++) { // Put the payload NaN in one operand and 1.0 in the other, so // the result is a NaN produced from a NaN input -- which is the // case that propagates a payload rather than generating a fresh // canonical one. std::vector code = { LUI(6, 0x2000), ADDI(5, 0, -1), r_type(OP_FP, 1, 0, 5, 0, (FP_FMVDX << 2) | FP_FMT_D), // f1 = payload NaN ADDI(7, 0, 0x3FF), SLLI(7, 7, 52), r_type(OP_FP, 2, 0, 7, 0, (FP_FMVDX << 2) | FP_FMT_D), // f2 = 1.0 r_type(OP_FP, 3, 0, swap ? 2 : 1, swap ? 1 : 2, (uint8_t)((OPS[i].f5 << 2) | FP_FMT_D)), ECALL() }; char desc[128]; uint64_t interp = run_code(code).state.f[3]; uint64_t dbt = run_code_dbt_fbits(code, 3); snprintf(desc, sizeof(desc), "%s %s payload NaN: interpreter canonicalises", OPS[i].name, swap ? "rs2" : "rs1"); CHECK_EQ(desc, interp, CANON); snprintf(desc, sizeof(desc), "%s %s payload NaN: DBT canonicalises", OPS[i].name, swap ? "rs2" : "rs1"); CHECK_EQ(desc, dbt, CANON); } } } // FMIN.D / FMAX.D with a signalling NaN operand (#1344). // // RISC-V prefers the NUMBER over a NaN operand whether that NaN is quiet or // signalling: an sNaN raises the invalid flag but still loses to a real // value, and only when BOTH operands are NaN is the result canonical. // // ARM's FMINNM/FMAXNM are IEEE minNum/maxNum, which prefer the number only // for a quiet NaN -- an sNaN comes back as a NaN. FPCR.DN (#1343) does not // help; it only changes which NaN. The backend now quietens both operands // first, which makes the two rules coincide. // // The interpreter is already correct here, by way of host fmin/fmax // semantics rather than anything the tests pinned down -- so both routes are // asserted. Expected values follow the RISC-V unprivileged spec. // static void test_fp_minmax_snan() { printf("test_fp_minmax_snan...\n"); const uint64_t SIGNAN = 0x7FF0000000000001ULL; // exp all 1s, mantissa // MSB clear => signalling // (SNAN is a glibc math.h // macro, like NZERO below) const uint64_t QNAN = 0x7FF8000000000000ULL; const uint64_t PINF = 0x7FF0000000000000ULL; const uint64_t NINF = 0xFFF0000000000000ULL; const uint64_t ONE = 0x3FF0000000000000ULL; const uint64_t NEGZERO = 0x8000000000000000ULL; // NZERO is a POSIX macro struct { const char *name; uint64_t bits; } VALS[] = { { "+inf", PINF }, { "-inf", NINF }, { "1.0", ONE }, { "-0.0", NEGZERO }, }; // f1 = a, f2 = b, f3 = min/max(f1,f2). Constants are built a byte at a // time because ADDI's immediate is 12-bit SIGNED. auto build = [](uint64_t a, uint64_t b, int is_max) { std::vector code; auto load = [&](int freg, int xreg, uint64_t bits) { code.push_back(ADDI(xreg, 0, 0)); for (int sh = 56; sh >= 0; sh -= 8) { code.push_back(SLLI(xreg, xreg, 8)); uint8_t byte = (uint8_t)((bits >> sh) & 0xFF); if (byte) { code.push_back(ADDI(30, 0, (int32_t)byte)); code.push_back(r_type(0x33, xreg, 0, xreg, 30, 0)); } } code.push_back(r_type(OP_FP, freg, 0, xreg, 0, (FP_FMVDX << 2) | FP_FMT_D)); }; load(1, 5, a); load(2, 6, b); code.push_back(r_type(OP_FP, 3, is_max ? 1 : 0, 1, 2, (FP_FMINMAX << 2) | FP_FMT_D)); code.push_back(ECALL()); return code; }; for (int is_max = 0; is_max <= 1; is_max++) { const char *op = is_max ? "fmax.d" : "fmin.d"; for (size_t i = 0; i < sizeof(VALS)/sizeof(VALS[0]); i++) { for (int swap = 0; swap <= 1; swap++) { // sNaN against a real value: the real value wins, in either // operand position. std::vector code = build(swap ? VALS[i].bits : SIGNAN, swap ? SIGNAN : VALS[i].bits, is_max); char desc[160]; snprintf(desc, sizeof(desc), "%s(sNaN,%s) sNaN in %s: interp", op, VALS[i].name, swap ? "rs2" : "rs1"); CHECK_EQ(desc, run_code(code).state.f[3], VALS[i].bits); snprintf(desc, sizeof(desc), "%s(sNaN,%s) sNaN in %s: DBT", op, VALS[i].name, swap ? "rs2" : "rs1"); CHECK_EQ(desc, run_code_dbt_fbits(code, 3), VALS[i].bits); } } // A QUIET NaN must lose to a real value just as the signalling one // does. Worth asserting separately: MINSD/MAXSD return their second // source on any NaN, so a backend can be wrong here for exactly the // same reason without any sNaN being involved (#1357). for (size_t i = 0; i < sizeof(VALS)/sizeof(VALS[0]); i++) { for (int swap = 0; swap <= 1; swap++) { std::vector code = build(swap ? VALS[i].bits : QNAN, swap ? QNAN : VALS[i].bits, is_max); char desc[160]; snprintf(desc, sizeof(desc), "%s(qNaN,%s) qNaN in %s: interp", op, VALS[i].name, swap ? "rs2" : "rs1"); CHECK_EQ(desc, run_code(code).state.f[3], VALS[i].bits); snprintf(desc, sizeof(desc), "%s(qNaN,%s) qNaN in %s: DBT", op, VALS[i].name, swap ? "rs2" : "rs1"); CHECK_EQ(desc, run_code_dbt_fbits(code, 3), VALS[i].bits); } } // Both NaN -> canonical. Without this the fix could be "always take // the other operand", which would be wrong here. Both orders: with // only (sNaN,qNaN), a backend that returns its second source passes // by accident, because that source already holds the value expected. for (int swap = 0; swap <= 1; swap++) { std::vector both = build(swap ? QNAN : SIGNAN, swap ? SIGNAN : QNAN, is_max); char d2[160]; snprintf(d2, sizeof(d2), "%s(%s,%s) both NaN: interp", op, swap ? "qNaN" : "sNaN", swap ? "sNaN" : "qNaN"); CHECK_EQ(d2, run_code(both).state.f[3], QNAN); snprintf(d2, sizeof(d2), "%s(%s,%s) both NaN: DBT", op, swap ? "qNaN" : "sNaN", swap ? "sNaN" : "qNaN"); CHECK_EQ(d2, run_code_dbt_fbits(both, 3), QNAN); } // Zeros of opposite sign. RISC-V is specific where IEEE minNum is // not: fmin returns -0.0 and fmax returns +0.0, regardless of operand // order. MINSD/MAXSD return their second source when the operands // compare equal, so this is order-sensitive on x86 for the same // reason the NaN cases are. for (int swap = 0; swap <= 1; swap++) { const uint64_t PLUSZERO = 0ULL; std::vector zc = build(swap ? PLUSZERO : NEGZERO, swap ? NEGZERO : PLUSZERO, is_max); uint64_t want = is_max ? PLUSZERO : NEGZERO; char d3[160]; snprintf(d3, sizeof(d3), "%s(%s,%s) signed zero: interp", op, swap ? "+0.0" : "-0.0", swap ? "-0.0" : "+0.0"); CHECK_EQ(d3, run_code(zc).state.f[3], want); snprintf(d3, sizeof(d3), "%s(%s,%s) signed zero: DBT", op, swap ? "+0.0" : "-0.0", swap ? "-0.0" : "+0.0"); CHECK_EQ(d3, run_code_dbt_fbits(zc, 3), want); } } } // FEQ.D / FLT.D / FLE.D (#1359). // // test_fp_compare() above predates this and could not have caught the bug: // it drives rv64_interp_run directly, so the DBT never runs, and the // interpreter is correct here. It also covers only FLT and FEQ -- never // FLE, the one that was inverted -- and uses no NaN. A test that cannot // distinguish the two routes proves nothing about the backend, which is // why FLE.D shipped computing rs1 >= rs2: wrong for ordinary ordered // operands, with no NaN involved anywhere. // // Both routes are asserted below. The two cases a careless table would // reach for, fle(1,1) and fle(NaN,1), both come out right even under the // inverted form, so the table deliberately includes the asymmetric // orderings that do not. // // RISC-V: every comparison against NaN is false, quiet or signalling. // Signed zeros compare EQUAL, unlike FMIN/FMAX where the sign is decisive. // static void test_fp_compare_semantics() { printf("test_fp_compare_semantics...\n"); const uint64_t SIGNAN = 0x7FF0000000000001ULL; const uint64_t QNAN = 0x7FF8000000000000ULL; const uint64_t PINF = 0x7FF0000000000000ULL; const uint64_t NINF = 0xFFF0000000000000ULL; const uint64_t ONE = 0x3FF0000000000000ULL; const uint64_t TWO = 0x4000000000000000ULL; const uint64_t NEGONE = 0xBFF0000000000000ULL; const uint64_t PZERO = 0x0000000000000000ULL; const uint64_t NEGZERO = 0x8000000000000000ULL; // f1 = a, f2 = b, x7 = cmp(f1,f2). auto build = [](uint64_t a, uint64_t b, int funct3) { std::vector code; auto load = [&](int freg, int xreg, uint64_t bits) { code.push_back(ADDI(xreg, 0, 0)); for (int sh = 56; sh >= 0; sh -= 8) { code.push_back(SLLI(xreg, xreg, 8)); uint8_t byte = (uint8_t)((bits >> sh) & 0xFF); if (byte) { code.push_back(ADDI(30, 0, (int32_t)byte)); code.push_back(r_type(0x33, xreg, 0, xreg, 30, 0)); } } code.push_back(r_type(OP_FP, freg, 0, xreg, 0, (FP_FMVDX << 2) | FP_FMT_D)); }; load(1, 5, a); load(2, 6, b); code.push_back(r_type(OP_FP, 7, funct3, 1, 2, (FP_FCMP << 2) | FP_FMT_D)); code.push_back(ECALL()); return code; }; // funct3: 2 = FEQ.D, 1 = FLT.D, 0 = FLE.D struct { const char *nm; uint64_t a, b; int f3; uint64_t want; } C[] = { // Ordered, both directions -- the asymmetry an inverted FLE fails. { "feq(1,2)", ONE, TWO, 2, 0 }, { "feq(2,1)", TWO, ONE, 2, 0 }, { "feq(1,1)", ONE, ONE, 2, 1 }, { "flt(1,2)", ONE, TWO, 1, 1 }, { "flt(2,1)", TWO, ONE, 1, 0 }, { "flt(1,1)", ONE, ONE, 1, 0 }, { "fle(1,2)", ONE, TWO, 0, 1 }, { "fle(2,1)", TWO, ONE, 0, 0 }, { "fle(1,1)", ONE, ONE, 0, 1 }, // Negatives and infinities. { "flt(-1,1)", NEGONE, ONE, 1, 1 }, { "flt(1,-1)", ONE, NEGONE, 1, 0 }, { "fle(-inf,+inf)", NINF, PINF, 0, 1 }, { "fle(+inf,-inf)", PINF, NINF, 0, 0 }, { "feq(+inf,+inf)", PINF, PINF, 2, 1 }, { "flt(-inf,-inf)", NINF, NINF, 1, 0 }, // Signed zeros compare equal. { "feq(-0,+0)", NEGZERO, PZERO, 2, 1 }, { "flt(-0,+0)", NEGZERO, PZERO, 1, 0 }, { "fle(-0,+0)", NEGZERO, PZERO, 0, 1 }, { "fle(+0,-0)", PZERO, NEGZERO, 0, 1 }, // Every comparison against NaN is false, in either position, and // whether the NaN is quiet or signalling. { "feq(qNaN,1)", QNAN, ONE, 2, 0 }, { "feq(1,qNaN)", ONE, QNAN, 2, 0 }, { "flt(qNaN,1)", QNAN, ONE, 1, 0 }, { "flt(1,qNaN)", ONE, QNAN, 1, 0 }, { "fle(qNaN,1)", QNAN, ONE, 0, 0 }, { "fle(1,qNaN)", ONE, QNAN, 0, 0 }, { "feq(sNaN,1)", SIGNAN, ONE, 2, 0 }, { "feq(1,sNaN)", ONE, SIGNAN, 2, 0 }, { "flt(sNaN,1)", SIGNAN, ONE, 1, 0 }, { "flt(1,sNaN)", ONE, SIGNAN, 1, 0 }, { "fle(sNaN,1)", SIGNAN, ONE, 0, 0 }, { "fle(1,sNaN)", ONE, SIGNAN, 0, 0 }, { "feq(qNaN,qNaN)", QNAN, QNAN, 2, 0 }, { "fle(qNaN,qNaN)", QNAN, QNAN, 0, 0 }, }; for (size_t i = 0; i < sizeof(C)/sizeof(C[0]); i++) { std::vector code = build(C[i].a, C[i].b, C[i].f3); char desc[160]; snprintf(desc, sizeof(desc), "%s: interp", C[i].nm); CHECK_EQ(desc, run_code(code).state.x[7], C[i].want); snprintf(desc, sizeof(desc), "%s: DBT", C[i].nm); CHECK_EQ(desc, run_code_dbt(code, 7), C[i].want); } } // MULH / MULHSU / MULHU against x0 (#1361). // // x0 is materialised as RAX-as-zero, so the host register for a guest x0 // operand IS RAX -- the same register the dividend is loaded into. The // high multiplies loaded rs1 into RAX first, destroying the zero, and then // multiplied by "rs2" (still RAX), squaring rs1. MULHSU also re-read rs2 // after the multiply, where RAX holds the low half of the product. // // test_mul_div_rem() covers this family but never with x0 as an operand, // which is exactly why it survived. Both routes are asserted: the // interpreter is correct here, so an interpreter-only check proves nothing // about the backend (#1359). // static void test_mulh_x0() { printf("test_mulh_x0...\n"); // f3 = MULH-family(rs1, rs2) for the given funct3, via x0 in one or // both operand positions. Anything times zero is zero -- including // the high half -- so every expectation below is 0. auto build = [](int64_t v, int shift, int funct3, int rs1_is_x0, int rs2_is_x0) { std::vector code; // x14 = v << shift. ADDI's immediate is 12-bit signed, so the // magnitude comes from the shift rather than from repeated adds. code.push_back(ADDI(14, 0, (int32_t)v)); if (shift) code.push_back(SLLI(14, 14, shift)); code.push_back(r_type(0x33, 4, funct3, rs1_is_x0 ? 0 : 14, rs2_is_x0 ? 0 : 14, 0x01)); code.push_back(ECALL()); return code; }; struct { const char *nm; int f3; } OPS[] = { { "mulh", 1 }, { "mulhsu", 2 }, { "mulhu", 3 }, }; // Negative values matter for MULHSU: the sign of rs1 drives the // correction term that read the clobbered register. // // The shifts are load-bearing for MULH. Squaring a small rs1 leaves a // zero high half, so the broken form returns the right answer anyway -- // MULH only diverges once |rs1| exceeds 2^32 and rs1*rs1 overflows 64 // bits. Without these rows this test would pass against the bug. struct { int64_t v; int shift; } VALS[] = { { -7, 0 }, { -1, 0 }, { 1, 0 }, { 2047, 0 }, { -2048, 0 }, { -7, 34 }, { 1023, 40 }, { -1, 63 }, { 3, 62 }, }; for (size_t o = 0; o < sizeof(OPS)/sizeof(OPS[0]); o++) { for (size_t i = 0; i < sizeof(VALS)/sizeof(VALS[0]); i++) { struct { const char *pos; int a, b; } P[] = { { "rs2=x0", 0, 1 }, { "rs1=x0", 1, 0 }, { "both=x0", 1, 1 }, }; for (size_t p = 0; p < 3; p++) { std::vector code = build(VALS[i].v, VALS[i].shift, OPS[o].f3, P[p].a, P[p].b); char desc[160]; snprintf(desc, sizeof(desc), "%s(%lld<<%d,%s): interp", OPS[o].nm, (long long)VALS[i].v, VALS[i].shift, P[p].pos); CHECK_EQ(desc, run_code(code).state.x[4], 0); snprintf(desc, sizeof(desc), "%s(%lld<<%d,%s): DBT", OPS[o].nm, (long long)VALS[i].v, VALS[i].shift, P[p].pos); CHECK_EQ(desc, run_code_dbt(code, 4), 0); } } } } // a64 mirrors of the x86-64 defects found on the other host. // // Kagura's x64 run turned up three bugs in quick succession -- #1357 // (FMIN/FMAX returning a signalling NaN), #1359 (FLE.D inverted, FEQ.D/FLT.D // true for NaN) and #1361 (MULH* squaring rs1 when rs2 is x0). Each is a // backend-local mistake, so the a64 side has to be checked separately rather // than assumed clean: the differential fuzzer compares interpreter against // *this host's* DBT, so an x64-only bug is invisible here and vice versa. // // a64 looks structurally immune to #1361 -- SMULH/UMULH are three-operand, so // there is no implicit-register clobber of the kind that bit x86-64's MUL -- // and MULHSU already carries a comment about avoiding X0 for exactly that // reason. That is an argument, not evidence, so these pin it. // // Constant names are prefixed: a bare SNAN collided with a glibc math.h macro // and broke the Linux build (#1356), as NZERO would have before it. // static void test_a64_mirrors_x64_defects() { printf("test_a64_mirrors_x64_defects...\n"); const uint64_t kQNan = 0x7FF8000000000000ULL; const uint64_t kOne = 0x3FF0000000000000ULL; const uint64_t kTwo = 0x4000000000000000ULL; // --- #1361 mirror: MULH/MULHSU/MULHU with rs2 = x0 ------------------- // // x1 = a large non-zero value, then high-multiply it by x0. Every form // must yield 0; squaring rs1 (the x64 bug) would not. { std::vector code = { ADDI(1, 0, 0), ADDI(2, 0, 0x7FF), SLLI(2, 2, 40), r_type(0x33, 1, 0, 1, 2, 0), // x1 = 0x7FF << 40 r_type(0x33, 5, 1, 1, 0, 1), // MULH x5, x1, x0 r_type(0x33, 6, 2, 1, 0, 1), // MULHSU x6, x1, x0 r_type(0x33, 7, 3, 1, 0, 1), // MULHU x7, x1, x0 ADDI(17, 0, 93), ECALL() }; TestResult r = run_code(code); CHECK_EQ("#1361 mirror: MULH rs2=x0 interp", r.state.x[5], 0ULL); CHECK_EQ("#1361 mirror: MULHSU rs2=x0 interp", r.state.x[6], 0ULL); CHECK_EQ("#1361 mirror: MULHU rs2=x0 interp", r.state.x[7], 0ULL); CHECK_EQ("#1361 mirror: MULH rs2=x0 DBT", run_code_dbt(code, 5), 0ULL); CHECK_EQ("#1361 mirror: MULHSU rs2=x0 DBT", run_code_dbt(code, 6), 0ULL); CHECK_EQ("#1361 mirror: MULHU rs2=x0 DBT", run_code_dbt(code, 7), 0ULL); } // Positive control for the block above. Every expected value there is // zero, which is also what a declined translation or a zeroed exit // context would produce -- so on its own it cannot distinguish "MULH by // x0 is correct" from "the DBT never ran". Same shape with a non-zero // rs2 and a non-zero expected high half. { // x1 = x2 = 0x7FF << 40. (0x7FF<<40)^2 = 0x7FF*0x7FF << 80, so the // high 64 bits are 0x7FF*0x7FF >> 16 == 0x3FF000 >> 16 ... computed // below from the interpreter, which the FCVT/qemu work has pinned // independently; the point here is only that DBT == interp != 0. std::vector code = { ADDI(1, 0, 0), ADDI(2, 0, 0x7FF), SLLI(2, 2, 40), r_type(0x33, 1, 0, 1, 2, 0), // x1 = 0x7FF << 40 r_type(0x33, 5, 3, 1, 1, 1), // MULHU x5, x1, x1 ADDI(17, 0, 93), ECALL() }; const uint64_t interp = run_code(code).state.x[5]; g_tests_run++; if (interp != 0) { g_tests_passed++; } else { g_tests_failed++; fprintf(stderr, " FAIL: #1361 control: expected a non-zero " "high half, got 0 (control is useless)\n"); } CHECK_EQ("#1361 control: MULHU non-zero DBT matches interp", run_code_dbt(code, 5), interp); } // --- #1359 mirror: FLE.D / FLT.D / FEQ.D, ordered and with NaN ------- // // funct3: 0 = FLE.D, 1 = FLT.D, 2 = FEQ.D. NaN makes all three false; // the ordered rows catch an inverted comparison. struct { const char *name; int f3; uint64_t a, b; uint64_t want; } CASES[] = { { "fle(1,2)", 0, kOne, kTwo, 1 }, { "fle(2,1)", 0, kTwo, kOne, 0 }, { "fle(1,1)", 0, kOne, kOne, 1 }, { "fle(NaN,1)", 0, kQNan, kOne, 0 }, { "fle(1,NaN)", 0, kOne, kQNan, 0 }, { "flt(1,2)", 1, kOne, kTwo, 1 }, { "flt(2,1)", 1, kTwo, kOne, 0 }, { "flt(NaN,1)", 1, kQNan, kOne, 0 }, { "flt(1,NaN)", 1, kOne, kQNan, 0 }, { "feq(1,1)", 2, kOne, kOne, 1 }, { "feq(1,2)", 2, kOne, kTwo, 0 }, { "feq(NaN,1)", 2, kQNan, kOne, 0 }, { "feq(1,NaN)", 2, kOne, kQNan, 0 }, }; for (size_t i = 0; i < sizeof(CASES)/sizeof(CASES[0]); i++) { std::vector code; auto load = [&](int freg, int xreg, uint64_t bits) { code.push_back(ADDI(xreg, 0, 0)); for (int sh = 56; sh >= 0; sh -= 8) { code.push_back(SLLI(xreg, xreg, 8)); uint8_t byte = (uint8_t)((bits >> sh) & 0xFF); if (byte) { code.push_back(ADDI(30, 0, (int32_t)byte)); code.push_back(r_type(0x33, xreg, 0, xreg, 30, 0)); } } code.push_back(r_type(OP_FP, freg, 0, xreg, 0, (FP_FMVDX << 2) | FP_FMT_D)); }; load(1, 5, CASES[i].a); load(2, 6, CASES[i].b); code.push_back(r_type(OP_FP, 7, CASES[i].f3, 1, 2, (FP_FCMP << 2) | FP_FMT_D)); code.push_back(ADDI(17, 0, 93)); code.push_back(ECALL()); char desc[128]; snprintf(desc, sizeof(desc), "#1359 mirror: %s interp", CASES[i].name); CHECK_EQ(desc, run_code(code).state.x[7], CASES[i].want); snprintf(desc, sizeof(desc), "#1359 mirror: %s DBT", CASES[i].name); CHECK_EQ(desc, run_code_dbt(code, 7), CASES[i].want); } } int main(int argc, char *argv[]) { printf("RV64IMD Interpreter Test Suite\n"); printf("==============================\n\n"); test_addi(); test_addi_negative(); test_add_sub(); test_lui_addi(); test_lui_sign_extend(); test_lui_str_base_ecall_flush(); test_shifts_64bit(); test_mul_div_rem(); test_div_by_zero(); test_branch_beq(); test_branch_bne(); test_load_store_64(); test_load_store_32_sign_ext(); test_w_suffix_ops(); test_loop(); test_fp_add(); test_fp_mul_div(); test_fp_convert(); test_fp_compare(); test_mulh(); test_x0_always_zero(); test_sub_word_loads(); test_branches_all(); test_logical_ops(); test_register_shifts(); test_auipc(); test_jal_jalr(); test_fp_fsqrt(); test_fp_minmax(); test_fp_sign_inject(); test_fp_cvt_nan_inf(); test_fp_fma(); test_fp_fclass(); test_unhandled_refuses_block(); test_refuse_does_not_reclaim(); test_csr_fcsr_family(); test_csr_fflags_not_ebreak(); test_unsupported_csr_refuses_block(); test_fp_fmv_dx(); test_selfloop_register_pressure(); test_selfloop_pressure_forward_branch(); test_selfloop_pressure_regw_rs2(); test_selfloop_pressure_fp_int_rd(); test_fsgnj_rd_aliases_rs2(); test_fcvt_rounding_modes(); test_1338_superblock_side_exit_fp(); test_fp_nan_canonicalisation(); test_fp_minmax_snan(); test_fp_compare_semantics(); test_mulh_x0(); // Host-agnostic pins of the same three x64 defects; written on a64 to // prove that backend is clean, and kept after the x64 stack lands so // both hosts keep the shape (#1364 rebased on #1358/#1360/#1363). // test_a64_mirrors_x64_defects(); test_x0_base_load_store(); test_x0_operand_alu(); test_slt_branch_fusion_x0(); test_mulhsu_aliasing(); test_div_rem_edge_cases(); test_intrinsic_alloc(); printf("\n==============================\n"); printf("Hand-assembled: %d run, %d passed, %d failed\n", g_tests_run, g_tests_passed, g_tests_failed); // Run ELF tests if provided on command line. // int elf_failures = 0; for (int i = 1; i < argc; i++) { if (!run_elf_test(argv[i])) { elf_failures++; } } if (argc > 1) { printf("\nELF interpreter: %d run, %d passed, %d failed\n", argc - 1, argc - 1 - elf_failures, elf_failures); } // Run ELF tests through the DBT (JIT). // int dbt_failures = 0; for (int i = 1; i < argc; i++) { if (!run_dbt_elf_test(argv[i])) { dbt_failures++; } } if (argc > 1) { printf("\nELF DBT/JIT: %d run, %d passed, %d failed\n", argc - 1, argc - 1 - dbt_failures, dbt_failures); } return (g_tests_failed > 0 || elf_failures > 0 || dbt_failures > 0) ? 1 : 0; }