mirror of
https://github.com/brazilofmux/tinymux
synced 2026-08-13 00:23:11 -04:00
Salvaged from #2116, which is closed. That PR built this case while #2107 was believed to be a backend fault at guest address 0x8000; #2107 turned out to be a mixed build and was closed invalid, so the motivating theory is gone. The property is still worth pinning, and the rationale in the comment is rewritten to say what it actually covers rather than pointing at a hypothesis nobody holds any more. 0x8000 is not an arbitrary constant. On the current layout it is rv_compiler::STR_BASE exactly, and simultaneously FARGS_LIMIT -- the boundary the two regions share, which dbt_compile.h already static_asserts. Every string constant a compiled program references lives in that band, so compiled code materializes addresses there constantly. The case is the smallest one that would catch a backend mis-materializing it: LUI+ADDI to build STR_BASE+8, then hand a0 to an ECALL so the value must survive the pinned-register flush. Asserted on both routes -- the interpreter and the host DBT -- because a differential between them is not what this is guarding; a fault shared by both is (#1319/#1320 are the precedent for that being a real possibility). tests/dbt exec: 963 run, 963 passed, 0 failed (up from 960). Negative control, because an assertion that has never failed has not been shown to work: expecting 0x8009 instead of 0x8008 produces FAIL: LUI STR_BASE+8 a0 (DBT): got 0x8008, expected 0x8009 Hand-assembled: 963 run, 962 passed, 1 failed and restoring it returns 963/963. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3192 lines
126 KiB
C++
3192 lines
126 KiB
C++
/*! \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 <cstdio>
|
||
#include <cstdlib>
|
||
#include <cstring>
|
||
#include <cmath>
|
||
#include <vector>
|
||
|
||
// The only thing this harness wants from <unistd.h> is write(), for the
|
||
// RV64 SYS_write emulation below. MSVC has it as _write() in <io.h> and
|
||
// has no ssize_t, so name both portably rather than sprinkling #ifdef at
|
||
// the two call sites (#1441).
|
||
//
|
||
#if defined(_WIN32)
|
||
#include <io.h>
|
||
typedef int dbt_ssize_t;
|
||
#define dbt_write _write
|
||
#else
|
||
#include <unistd.h>
|
||
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<int>(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<uint32_t>(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<uint32_t>(imm) & 0x1F) << 7)
|
||
| (funct3 << 12) | (rs1 << 15) | (rs2 << 20)
|
||
| (((static_cast<uint32_t>(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<uint32_t>(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<uint32_t>(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<uint32_t>& 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<uint8_t> 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<uint32_t>& code, int reg) {
|
||
const size_t MEM_SIZE = 64 * 1024;
|
||
std::vector<uint8_t> 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<uint32_t>& code, int freg) {
|
||
const size_t MEM_SIZE = 64 * 1024;
|
||
std::vector<uint8_t> 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<uint64_t>(-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<int32_t>(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<uint32_t> 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<uint64_t>(static_cast<unsigned>(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<uint64_t>(-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<int32_t>(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<uint64_t>(static_cast<int64_t>(-100)));
|
||
CHECK_EQ("MULW 200*-7", r.state.x[8], static_cast<uint64_t>(static_cast<int64_t>(-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<uint8_t> 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<uint32_t> 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<uint8_t> memory(MEM_SIZE, 0);
|
||
memcpy(memory.data() + 0x1000, &a_bits, 8);
|
||
memcpy(memory.data() + 0x1008, &b_bits, 8);
|
||
|
||
std::vector<uint32_t> 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<uint8_t> memory(MEM_SIZE, 0);
|
||
memcpy(memory.data() + 0x1000, &a_bits, 8);
|
||
memcpy(memory.data() + 0x1008, &b_bits, 8);
|
||
|
||
std::vector<uint32_t> 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<uint64_t>(-1LL));
|
||
CHECK_EQ("LBU zero-ext", r.state.x[4], 0xFF);
|
||
CHECK_EQ("LH sign-ext", r.state.x[5], static_cast<uint64_t>(-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<uint64_t>(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<uint64_t>(-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<uint8_t> memory(MEM_SIZE, 0);
|
||
memcpy(memory.data() + 0x1000, &bits, 8);
|
||
|
||
std::vector<uint32_t> 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<uint8_t> memory(MEM_SIZE, 0);
|
||
memcpy(memory.data() + 0x1000, &a_bits, 8);
|
||
memcpy(memory.data() + 0x1008, &b_bits, 8);
|
||
|
||
std::vector<uint32_t> 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<uint8_t> memory(MEM_SIZE, 0);
|
||
memcpy(memory.data() + 0x1000, &pos_bits, 8);
|
||
memcpy(memory.data() + 0x1008, &neg_bits, 8);
|
||
|
||
std::vector<uint32_t> 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<uint32_t> 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<uint8_t>((bits >> shift) & 0xFF);
|
||
if (byte) {
|
||
code.push_back(ADDI(2, 0, static_cast<int32_t>(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<uint8_t> 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<uint32_t> 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<uint32_t> 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<uint8_t> 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<uint64_t>(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<uint64_t>(state.fcsr), 0ULL);
|
||
}
|
||
|
||
// DBT path.
|
||
//
|
||
{
|
||
const size_t MEM_SIZE = 64 * 1024;
|
||
std::vector<uint8_t> 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<uint64_t>(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<unsigned long long>(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<uint32_t> 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<uint8_t> 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<uint64_t>(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<uint32_t> code = {
|
||
CSRRS(5, 0xC00, 0),
|
||
ADDI(10, 5, 0),
|
||
ADDI(17, 0, 93),
|
||
ECALL()
|
||
};
|
||
|
||
const size_t MEM_SIZE = 64 * 1024;
|
||
std::vector<uint8_t> 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<uintptr_t>(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<uint32_t> 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<uint8_t> 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<uintptr_t>(native), 0ULL);
|
||
CHECK_EQ("unhandled refuse: xlate_fail is REFUSE",
|
||
static_cast<uint64_t>(dbt.xlate_fail),
|
||
static_cast<uint64_t>(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<uint32_t> code_a = {
|
||
ADDI(10, 0, 7),
|
||
ADDI(17, 0, 93),
|
||
ECALL()
|
||
};
|
||
std::vector<uint32_t> 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<uint8_t> 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<uintptr_t>(b), 0ULL);
|
||
CHECK_EQ("refuse/reclaim: B is REFUSE",
|
||
static_cast<uint64_t>(dbt.xlate_fail),
|
||
static_cast<uint64_t>(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<uint8_t> 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<uint32_t> 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<rv64_memory_t *>(user_data);
|
||
uint64_t syscall_num = state->x[17]; // a7
|
||
|
||
switch (syscall_num) {
|
||
case 93: // exit(code)
|
||
return static_cast<int>(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<uint64_t>(-1LL);
|
||
return -1;
|
||
}
|
||
dbt_ssize_t written = dbt_write(static_cast<int>(fd),
|
||
mem->data + buf, static_cast<unsigned int>(len));
|
||
state->x[10] = static_cast<uint64_t>(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<dbt_ecall_ctx *>(user_data);
|
||
uint64_t syscall_num = ctx->x[17];
|
||
|
||
switch (syscall_num) {
|
||
case 93:
|
||
return static_cast<int>(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<uint64_t>(-1LL);
|
||
return -1;
|
||
}
|
||
dbt_ssize_t written = dbt_write(static_cast<int>(fd),
|
||
ec->memory + buf, static_cast<unsigned int>(len));
|
||
ctx->x[10] = static_cast<uint64_t>(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<uint32_t> 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<uint32_t> 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<uint32_t> 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<uint32_t> 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<uint32_t> 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<uint32_t> 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<uint32_t> 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<uint32_t> 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<uint32_t> 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<uint32_t> 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<uint32_t> 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<uint32_t> 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<uint32_t> 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<uint64_t>(-2LL) },
|
||
{ "div -100/7", 27, static_cast<uint64_t>(-14LL) },
|
||
{ "divu MAX/7", 28, 0x2492492492492492ULL },
|
||
{ "remu MAX%7", 29, 1 },
|
||
{ "divw -100/7", 30, static_cast<uint64_t>(-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<uint8_t> 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<uint32_t>(
|
||
static_cast<int32_t>(target) - static_cast<int32_t>(pc));
|
||
return OP_JAL | (rd << 7)
|
||
| (((i >> 12) & 0xFF) << 12)
|
||
| (((i >> 11) & 1) << 20)
|
||
| (((i >> 1) & 0x3FF) << 21)
|
||
| (((i >> 20) & 1) << 31);
|
||
};
|
||
|
||
std::vector<uint32_t> 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<void *>(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<uint8_t> 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<uint32_t> 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<uint8_t> 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<unsigned long long>(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<uint32_t> 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<uint32_t> 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<uint32_t> 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<uint32_t> 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<uint32_t> 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<uint32_t> 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<uint32_t> 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<uint32_t> 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<uint32_t> 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<uint32_t> 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<uint32_t> 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<uint32_t> 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<uint32_t> 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;
|
||
}
|
||
|