/*! \file hir_codegen.cpp * \brief HIR to RV64 code generation. * * RV64 instruction encoding, register allocation (linear scan), * output buffer allocation (liveness-based), and the hir_codegen() * function that walks HIR and emits RV64 machine code. */ #include "copyright.h" #include "autoconf.h" #include "config.h" #include "externs.h" #include "dbt_compile.h" #include "dbt_decoder.h" #include "engine_api.h" #include #include #include #include #include #include #include "jit_tier1_stamp.h" // Tier 1 build stamp for this unit (#2061). Folded into the persisted // code_cache's staleness key so a codegen change here invalidates entries // compiled by the previous build. Updates when THIS unit is recompiled, // which is what makes it work under incremental make. TIER1_STAMP_DEFINE(TIER1_STAMP_HIR_CODEGEN); // RV64 instruction encoding // --------------------------------------------------------------- static uint32_t rv_i_type(uint8_t opcode, uint8_t rd, uint8_t funct3, uint8_t rs1, int32_t imm) { return opcode | (rd << 7) | (funct3 << 12) | (rs1 << 15) | ((static_cast(imm) & 0xFFF) << 20); } static uint32_t rv_u_type(uint8_t opcode, uint8_t rd, int32_t imm) { return opcode | (rd << 7) | (static_cast(imm) & 0xFFFFF000); } static uint32_t rv_ADDI(uint8_t rd, uint8_t rs1, int32_t imm) { return rv_i_type(OP_IMM, rd, ALU_ADDI, rs1, imm); } static uint32_t rv_LUI(uint8_t rd, int32_t imm) { return rv_u_type(OP_LUI, rd, imm); } static uint32_t rv_SLLI(uint8_t rd, uint8_t rs1, int32_t shamt) { return rv_i_type(OP_IMM, rd, ALU_SLLI, rs1, shamt); } static uint32_t rv_SRLI(uint8_t rd, uint8_t rs1, int32_t shamt) { return rv_i_type(OP_IMM, rd, ALU_SRLI, rs1, shamt); } static uint32_t rv_ECALL() { return rv_i_type(OP_SYSTEM, 0, 0, 0, 0); } // R-type encoding for register-register ALU ops. // static uint32_t rv_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) | (static_cast(funct7) << 25); } static uint32_t rv_ADD(uint8_t rd, uint8_t rs1, uint8_t rs2) { return rv_r_type(OP_REG, rd, ALU_ADD, rs1, rs2, 0x00); } static uint32_t rv_SUB(uint8_t rd, uint8_t rs1, uint8_t rs2) { return rv_r_type(OP_REG, rd, ALU_ADD, rs1, rs2, 0x20); } // B-type encoding (branches). // static uint32_t rv_b_type(uint8_t funct3, uint8_t rs1, uint8_t rs2, int32_t imm) { uint32_t u = static_cast(imm); return OP_BRANCH | (((u >> 11) & 1) << 7) | (((u >> 1) & 0xF) << 8) | (static_cast(funct3) << 12) | (static_cast(rs1) << 15) | (static_cast(rs2) << 20) | (((u >> 5) & 0x3F) << 25) | (((u >> 12) & 1) << 31); } static uint32_t rv_BEQ(uint8_t rs1, uint8_t rs2, int32_t off) { return rv_b_type(BR_BEQ, rs1, rs2, off); } static uint32_t rv_BNE(uint8_t rs1, uint8_t rs2, int32_t off) { return rv_b_type(BR_BNE, rs1, rs2, off); } static uint32_t rv_BGE(uint8_t rs1, uint8_t rs2, int32_t off) { return rv_b_type(BR_BGE, rs1, rs2, off); } static uint32_t rv_BGEU(uint8_t rs1, uint8_t rs2, int32_t off) { return rv_b_type(BR_BGEU, rs1, rs2, off); } // S-type encoding (stores). // static uint32_t rv_SB(uint8_t base, uint8_t src, int32_t off) { uint32_t u = static_cast(off); return OP_STORE | ((u & 0x1F) << 7) | (static_cast(ST_SB) << 12) | (static_cast(base) << 15) | (static_cast(src) << 20) | (((u >> 5) & 0x7F) << 25); } // Load byte unsigned. // static uint32_t rv_LBU(uint8_t rd, uint8_t base, int32_t off) { return rv_i_type(OP_LOAD, rd, LD_LBU, base, off); } // Store doubleword. // static uint32_t rv_SD(uint8_t base, uint8_t src, int32_t off) { uint32_t u = static_cast(off); return OP_STORE | ((u & 0x1F) << 7) | (static_cast(ST_SD) << 12) | (static_cast(base) << 15) | (static_cast(src) << 20) | (((u >> 5) & 0x7F) << 25); } // Load doubleword. // static uint32_t rv_LD(uint8_t rd, uint8_t base, int32_t off) { return rv_i_type(OP_LOAD, rd, LD_LD, base, off); } // J-type encoding (JAL). // static uint32_t rv_JAL(uint8_t rd, int32_t imm) { uint32_t u = static_cast(imm); return OP_JAL | (static_cast(rd) << 7) | (((u >> 12) & 0xFF) << 12) | (((u >> 11) & 1) << 20) | (((u >> 1) & 0x3FF) << 21) | (((u >> 20) & 1) << 31); } // Range checks for PC-relative immediates. The B-type and J-type encoders // silently drop bits that do not fit, so callers must verify the byte // offset is representable before emitting. Both immediates are even (the // implicit low bit is 0): JAL is 21-bit signed ([-2^20, +2^20-2]) and the // B-type branch is 13-bit signed ([-2^12, +2^12-2]). // static inline bool rv_jal_offset_ok(int32_t off) { return off >= -(1 << 20) && off <= ((1 << 20) - 2); } static inline bool rv_branch_offset_ok(int32_t off) { return off >= -(1 << 12) && off <= ((1 << 12) - 2); } // Emit a JAL, or mark the compilation out-of-range (forcing the AST // evaluator to handle the expression) if the byte offset does not fit // RV64's 21-bit signed immediate. A placeholder is still pushed so the // surrounding code layout is unchanged; the whole blob is discarded once // rc.out_exhausted is observed by the compiler driver. // static void rv_push_jal(rv_compiler &rc, uint8_t rd, int32_t off) { if (!rv_jal_offset_ok(off)) { rc.out_exhausted = true; } rc.code.push_back(rv_JAL(rd, off)); } // Inline string copy: copy NUL-terminated string from src_reg to dest_reg. // Clobbers t0 (x5). 5 instructions (byte-by-byte loop). // static void rv_emit_strcpy(std::vector &code, uint8_t dest_reg, uint8_t src_reg) { constexpr uint8_t t0 = 5; // loop: size_t loop = code.size(); code.push_back(rv_LBU(t0, src_reg, 0)); // LBU t0, 0(src) code.push_back(rv_SB(dest_reg, t0, 0)); // SB t0, 0(dest) code.push_back(rv_ADDI(src_reg, src_reg, 1)); // src++ code.push_back(rv_ADDI(dest_reg, dest_reg, 1)); // dest++ int32_t off = -static_cast((code.size() - loop) * 4); code.push_back(rv_BNE(t0, 0, off)); // BNE t0, x0, loop } // M extension: MUL, DIV, REM. // static uint32_t rv_MUL(uint8_t rd, uint8_t rs1, uint8_t rs2) { return rv_r_type(OP_REG, rd, 0, rs1, rs2, 0x01); } static uint32_t rv_DIV(uint8_t rd, uint8_t rs1, uint8_t rs2) { return rv_r_type(OP_REG, rd, 4, rs1, rs2, 0x01); } static uint32_t rv_REM(uint8_t rd, uint8_t rs1, uint8_t rs2) { return rv_r_type(OP_REG, rd, 6, rs1, rs2, 0x01); } // Bitwise operations. // static uint32_t rv_AND(uint8_t rd, uint8_t rs1, uint8_t rs2) { return rv_r_type(OP_REG, rd, ALU_AND, rs1, rs2, 0x00); } static uint32_t rv_OR(uint8_t rd, uint8_t rs1, uint8_t rs2) { return rv_r_type(OP_REG, rd, ALU_OR, rs1, rs2, 0x00); } static uint32_t rv_XOR(uint8_t rd, uint8_t rs1, uint8_t rs2) { return rv_r_type(OP_REG, rd, ALU_XOR, rs1, rs2, 0x00); } static uint32_t rv_SLT(uint8_t rd, uint8_t rs1, uint8_t rs2) { return rv_r_type(OP_REG, rd, ALU_SLT, rs1, rs2, 0x00); } static uint32_t rv_SLL(uint8_t rd, uint8_t rs1, uint8_t rs2) { return rv_r_type(OP_REG, rd, ALU_SLL, rs1, rs2, 0x00); } static uint32_t rv_SRL(uint8_t rd, uint8_t rs1, uint8_t rs2) { return rv_r_type(OP_REG, rd, ALU_SRL, rs1, rs2, 0x00); } // D extension: double-precision floating point. // RV64D uses R-type with opcode=OP_FP, funct7 encodes the operation, // and rm (funct3) = 0 (RNE) or 7 (dynamic) for arithmetic. // FLD/FSD use I/S-type with opcode OP_FP_LOAD/OP_FP_STORE, funct3=3. // static uint32_t rv_FADD_D(uint8_t fd, uint8_t fs1, uint8_t fs2) { return rv_r_type(OP_FP, fd, 7, fs1, fs2, 0x01); // funct7=0000001 } static uint32_t rv_FSUB_D(uint8_t fd, uint8_t fs1, uint8_t fs2) { return rv_r_type(OP_FP, fd, 7, fs1, fs2, 0x05); // funct7=0000101 } static uint32_t rv_FMUL_D(uint8_t fd, uint8_t fs1, uint8_t fs2) { return rv_r_type(OP_FP, fd, 7, fs1, fs2, 0x09); // funct7=0001001 } static uint32_t rv_FDIV_D(uint8_t fd, uint8_t fs1, uint8_t fs2) { return rv_r_type(OP_FP, fd, 7, fs1, fs2, 0x0D); // funct7=0001101 } static uint32_t rv_FSQRT_D(uint8_t fd, uint8_t fs1) { return rv_r_type(OP_FP, fd, 7, fs1, 0, 0x2D); // funct7=0101101, rs2=0 } static uint32_t rv_FSGNJN_D(uint8_t fd, uint8_t fs1, uint8_t fs2) { return rv_r_type(OP_FP, fd, 1, fs1, fs2, 0x11); // funct7=0010001, funct3=1 (FSGNJN) } // FNEG.D is a pseudo: FSGNJN.D fd, fs, fs static uint32_t rv_FNEG_D(uint8_t fd, uint8_t fs) { return rv_FSGNJN_D(fd, fs, fs); } // FCVT.D.L: int64 → double (rs2=2 for L) static uint32_t rv_FCVT_D_L(uint8_t fd, uint8_t rs1) { return rv_r_type(OP_FP, fd, 7, rs1, 2, 0x69); // funct7=1101001 } // FCVT.L.D: double → int64 (rs2=0 for L, rm=1 for RTZ) static uint32_t rv_FCVT_L_D(uint8_t rd, uint8_t fs1) { return rv_r_type(OP_FP, rd, 1, fs1, 2, 0x61); // funct7=1100001, rm=RTZ } // FMV.X.D: move FP bits to integer register static uint32_t rv_FMV_X_D(uint8_t rd, uint8_t fs1) { return rv_r_type(OP_FP, rd, 0, fs1, 0, 0x71); // funct7=1110001 } // FMV.D.X: move integer bits to FP register static uint32_t rv_FMV_D_X(uint8_t fd, uint8_t rs1) { return rv_r_type(OP_FP, fd, 0, rs1, 0, 0x79); // funct7=1111001 } // FEQ.D: float equality → integer rd static uint32_t rv_FEQ_D(uint8_t rd, uint8_t fs1, uint8_t fs2) { return rv_r_type(OP_FP, rd, 2, fs1, fs2, 0x51); // funct7=1010001, funct3=2 } // FLT.D: float less-than → integer rd static uint32_t rv_FLT_D(uint8_t rd, uint8_t fs1, uint8_t fs2) { return rv_r_type(OP_FP, rd, 1, fs1, fs2, 0x51); // funct7=1010001, funct3=1 } // FLE.D: float less-or-equal → integer rd static uint32_t rv_FLE_D(uint8_t rd, uint8_t fs1, uint8_t fs2) { return rv_r_type(OP_FP, rd, 0, fs1, fs2, 0x51); // funct7=1010001, funct3=0 } // FLD: load double from memory static uint32_t rv_FLD(uint8_t fd, uint8_t base, int32_t off) { return rv_i_type(OP_FP_LOAD, fd, 3, base, off); } // FSD: store double to memory static uint32_t rv_FSD(uint8_t base, uint8_t fs, int32_t off) { // S-type encoding, same as rv_SD but with OP_FP_STORE return OP_FP_STORE | ((off & 0x1F) << 7) | (3 << 12) | (base << 15) | (fs << 20) | (((off >> 5) & 0x7F) << 25); } // --------------------------------------------------------------- // Inline RISC-V atoi: parse decimal string → signed integer. // // Input: addr_reg = guest address of NUL-terminated string // Output: out_reg = signed 64-bit integer // Clobbers: t0(x5), t1(x6), t2(x7), t3(x28), t4(x29), addr_reg // 19 instructions. // --------------------------------------------------------------- static void rv_emit_atoi(std::vector &code, uint8_t addr_reg, uint8_t out_reg) { constexpr uint8_t t0=5, t1=6, t2=7, t3=28, t4=29; code.push_back(rv_ADDI(t1, 0, 0)); // 0: acc = 0 code.push_back(rv_ADDI(t2, 0, 0)); // 1: sign = 0 code.push_back(rv_LBU(t0, addr_reg, 0)); // 2: load byte code.push_back(rv_ADDI(t3, 0, 45)); // 3: t3 = '-' size_t bne_sign = code.size(); code.push_back(0); // 4: BNE → skip_sign (patch) code.push_back(rv_ADDI(t2, 0, 1)); // 5: sign = 1 code.push_back(rv_ADDI(addr_reg, addr_reg, 1)); // 6: advance past '-' // skip_sign: size_t skip_sign = code.size(); code[bne_sign] = rv_BNE(t0, t3, static_cast((skip_sign - bne_sign) * 4)); code.push_back(rv_LBU(t0, addr_reg, 0)); // 7: (re)load byte // digit_loop: size_t digit_loop = code.size(); code.push_back(rv_ADDI(t4, t0, -48)); // 8: digit = byte - '0' code.push_back(rv_ADDI(t3, 0, 10)); // 9: t3 = 10 size_t bgeu_done = code.size(); code.push_back(0); // 10: BGEU → done (patch) code.push_back(rv_MUL(t1, t1, t3)); // 11: acc *= 10 code.push_back(rv_ADD(t1, t1, t4)); // 12: acc += digit code.push_back(rv_ADDI(addr_reg, addr_reg, 1)); // 13: advance code.push_back(rv_LBU(t0, addr_reg, 0)); // 14: load next byte size_t bk = code.size(); code.push_back(rv_BEQ(0, 0, // 15: j digit_loop static_cast((digit_loop - bk) * 4))); // done: size_t done = code.size(); code[bgeu_done] = rv_BGEU(t4, t3, static_cast((done - bgeu_done) * 4)); size_t beq_pos = code.size(); code.push_back(0); // 16: BEQ → skip_neg (patch) code.push_back(rv_SUB(t1, 0, t1)); // 17: negate size_t skip_neg = code.size(); code[beq_pos] = rv_BEQ(t2, 0, static_cast((skip_neg - beq_pos) * 4)); code.push_back(rv_ADDI(out_reg, t1, 0)); // 18: mv out, acc } // --------------------------------------------------------------- // Inline RISC-V strcmp: compare two NUL-terminated strings. // // Input: addr_a, addr_b = guest addresses of the two strings // Output: out_reg = -1 (ab) // Clobbers: t0(x5), t1(x6), addr_a, addr_b // --------------------------------------------------------------- static void rv_emit_strcmp(std::vector &code, uint8_t addr_a, uint8_t addr_b, uint8_t out_reg) { constexpr uint8_t t0 = 5, t1 = 6; // loop: size_t loop = code.size(); code.push_back(rv_LBU(t0, addr_a, 0)); // t0 = *a code.push_back(rv_LBU(t1, addr_b, 0)); // t1 = *b size_t bne_differ = code.size(); code.push_back(0); // BNE t0, t1 → differ (patch) size_t beq_equal = code.size(); code.push_back(0); // BEQ t0, x0 → equal (patch) code.push_back(rv_ADDI(addr_a, addr_a, 1)); // a++ code.push_back(rv_ADDI(addr_b, addr_b, 1)); // b++ size_t j_loop = code.size(); code.push_back(rv_BEQ(0, 0, // j loop static_cast((loop - j_loop) * 4))); // differ: t0 != t1 size_t differ = code.size(); code[bne_differ] = rv_BNE(t0, t1, static_cast((differ - bne_differ) * 4)); code.push_back(rv_SLT(out_reg, t0, t1)); // out = (a < b) ? 1 : 0 size_t bne_done = code.size(); code.push_back(0); // BNE out, x0 → neg (patch) code.push_back(rv_ADDI(out_reg, 0, 1)); // out = 1 (a > b) size_t j_done = code.size(); code.push_back(0); // J → done (patch) // neg: a < b → out = -1 size_t neg = code.size(); code[bne_done] = rv_BNE(out_reg, 0, static_cast((neg - bne_done) * 4)); code.push_back(rv_SUB(out_reg, 0, out_reg)); // out = -1 (negate the 1 from SLT... wait, SLT gave 1, so -1 is correct) // Actually: SLT out, t0, t1 → out=1 if a((equal - beq_equal) * 4)); code.push_back(rv_ADDI(out_reg, 0, 0)); // out = 0 // done: size_t done = code.size(); code[j_done] = rv_BEQ(0, 0, static_cast((done - j_done) * 4)); code[j_done2] = rv_BEQ(0, 0, static_cast((done - j_done2) * 4)); } // --------------------------------------------------------------- // Inline RISC-V itoa: signed integer → decimal string. // // Input: val_reg = signed 64-bit integer // buf_reg = guest address of output buffer (≥21 bytes) // Output: NUL-terminated string at buf_reg // Clobbers: t0(x5), t1(x6), t2(x7), t3(x28), t4(x29), // t5(x30), t6(x31), buf_reg // 30 instructions. // --------------------------------------------------------------- static void rv_emit_itoa(std::vector &code, uint8_t val_reg, uint8_t buf_reg) { constexpr uint8_t t0=5, t1=6, t2=7, t3=28, t4=29, t5=30, t6=31; // Digits are accumulated in NEGATIVE space (#1326 / TC059). // // The obvious shape -- write '-', negate, then emit '0' + (v % 10) -- is // wrong for exactly one input. -INT64_MIN is not representable, so the // negation wraps and leaves the value negative; REM then yields negative // digits and '0' + (-d) writes the character d places BELOW '0'. // -9223372036854775808 came out as -'..--).0-*(+,))+(0(, each byte off // by twice its digit. // // Negating a POSITIVE value can never overflow, so normalize the other // way: make the value non-positive and negate each digit instead, where // the magnitude is at most 9. INT64_MIN then needs no special case. // code.push_back(rv_ADDI(t0, buf_reg, 0)); // 0: wr = buf code.push_back(rv_ADDI(t1, val_reg, 0)); // 1: t1 = val size_t bge_pos = code.size(); code.push_back(0); // 2: BGE → nonneg (patch) code.push_back(rv_ADDI(t4, 0, 45)); // 3: t4 = '-' code.push_back(rv_SB(t0, t4, 0)); // 4: write '-' code.push_back(rv_ADDI(t0, t0, 1)); // 5: advance wr size_t j_norm = code.size(); code.push_back(0); // 6: J → skip_neg (patch) // nonneg: value is >= 0, so negating it is safe and makes both paths // agree that t1 <= 0 from here on. size_t nonneg = code.size(); code[bge_pos] = rv_BGE(t1, 0, static_cast((nonneg - bge_pos) * 4)); code.push_back(rv_SUB(t1, 0, t1)); // 7: t1 = -t1 // skip_neg: size_t skip_neg = code.size(); code[j_norm] = rv_BEQ(0, 0, static_cast((skip_neg - j_norm) * 4)); code.push_back(rv_ADDI(t5, t0, 0)); // 7: digit_start = wr size_t bne_nz = code.size(); code.push_back(0); // 8: BNE → digit_loop (patch) code.push_back(rv_ADDI(t4, 0, 48)); // 9: '0' code.push_back(rv_SB(t0, t4, 0)); // 10: write '0' code.push_back(rv_ADDI(t0, t0, 1)); // 11: advance size_t beq_nul = code.size(); code.push_back(0); // 12: BEQ → nul_term (patch) // digit_loop: size_t digit_loop = code.size(); code[bne_nz] = rv_BNE(t1, 0, static_cast((digit_loop - bne_nz) * 4)); code.push_back(rv_ADDI(t3, 0, 10)); // 13: t3 = 10 code.push_back(rv_REM(t2, t1, t3)); // 14: t2 = val % 10 (<= 0) code.push_back(rv_DIV(t1, t1, t3)); // 15: t1 = val / 10 (<= 0) code.push_back(rv_SUB(t2, 0, t2)); // 15a: digit = -t2, 0..9 code.push_back(rv_ADDI(t2, t2, 48)); // 16: '0' + digit code.push_back(rv_SB(t0, t2, 0)); // 17: write digit code.push_back(rv_ADDI(t0, t0, 1)); // 18: advance code.push_back(rv_BNE(t1, 0, // 19: loop if more static_cast((digit_loop - (code.size())) * 4))); // nul_term: size_t nul_term = code.size(); code[beq_nul] = rv_BEQ(0, 0, static_cast((nul_term - beq_nul) * 4)); code.push_back(rv_SB(t0, 0, 0)); // 20: write '\0' code.push_back(rv_ADDI(t6, t0, -1)); // 21: end = wr - 1 // reverse_loop: size_t rev_loop = code.size(); size_t bge_rev = code.size(); code.push_back(0); // 22: BGE → done (patch) code.push_back(rv_LBU(t3, t5, 0)); // 23: t3 = *start code.push_back(rv_LBU(t4, t6, 0)); // 24: t4 = *end code.push_back(rv_SB(t5, t4, 0)); // 25: *start = t4 code.push_back(rv_SB(t6, t3, 0)); // 26: *end = t3 code.push_back(rv_ADDI(t5, t5, 1)); // 27: start++ code.push_back(rv_ADDI(t6, t6, -1)); // 28: end-- code.push_back(rv_BEQ(0, 0, // 29: j reverse_loop static_cast((rev_loop - (code.size())) * 4))); // done: size_t done = code.size(); code[bge_rev] = rv_BGE(t5, t6, static_cast((done - bge_rev) * 4)); } // Emit LUI+ADDI (or ADDI alone) that materializes the low 32 bits of // `bits` into rd. On RV64 the result is sign-extended from bit 31. // static void rv_load_i32_bits(std::vector &code, uint8_t rd, uint32_t bits) { int32_t sval = static_cast(bits); if (sval >= -2048 && sval <= 2047) { code.push_back(rv_ADDI(rd, 0, sval)); return; } uint32_t hi = bits & 0xFFFFF000u; int32_t lo = static_cast(bits & 0xFFFu); if (lo & 0x800) { hi += 0x1000u; lo -= 0x1000; } code.push_back(rv_LUI(rd, static_cast(hi))); if (lo) { code.push_back(rv_ADDI(rd, rd, lo)); } } // Load a signed 64-bit value into a register. // // Fits-in-12 → ADDI; fits-in-signed-32 → LUI+ADDI; otherwise a two-half // sequence (load high, slli 32, OR zero-extended low). Uses t0 (x5) as a // temporary, or t1 (x6) when rd is t0. // static void rv_load_i64(std::vector &code, uint8_t rd, int64_t val) { if (val >= -2048 && val <= 2047) { code.push_back(rv_ADDI(rd, 0, static_cast(val))); return; } if (val >= -2147483648LL && val <= 2147483647LL) { rv_load_i32_bits(code, rd, static_cast(static_cast(val))); return; } // Full 64-bit immediate: hi << 32 | lo. const uint64_t u = static_cast(val); const uint32_t lo = static_cast(u); const uint32_t hi = static_cast(u >> 32); const uint8_t tmp = (rd == 5) ? 6 : 5; // t0, else t1 if rd is t0 rv_load_i32_bits(code, rd, hi); code.push_back(rv_SLLI(rd, rd, 32)); if (lo != 0) { rv_load_i32_bits(code, tmp, lo); // LUI+ADDI sign-extends; clear upper 32 so OR cannot smear into hi. if (lo & 0x80000000u) { code.push_back(rv_SLLI(tmp, tmp, 32)); code.push_back(rv_SRLI(tmp, tmp, 32)); } code.push_back(rv_OR(rd, rd, tmp)); } } // Load a value into a register using LUI + ADDI. // static void rv_load_val(std::vector &code, uint8_t rd, uint64_t val) { if (val == 0) { code.push_back(rv_ADDI(rd, 0, 0)); return; } int32_t sval = static_cast(val); if (sval >= -2048 && sval <= 2047 && val == static_cast(static_cast(sval))) { code.push_back(rv_ADDI(rd, 0, sval)); return; } uint32_t hi = static_cast(val) & 0xFFFFF000; int32_t lo = static_cast(val & 0xFFF); if (lo & 0x800) { hi += 0x1000; lo = lo - 0x1000; } code.push_back(rv_LUI(rd, hi)); if (lo) code.push_back(rv_ADDI(rd, rd, lo)); } static void rv_load_guest_addr(std::vector &code, uint8_t rd, uint64_t addr) { if (!rv_compiler::is_output_frame_ref(addr)) { rv_load_val(code, rd, addr); return; } rv_load_val(code, rd, rv_compiler::output_frame_delta(addr)); code.push_back(rv_SUB(rd, 8, rd)); // rd = frame_top - delta } // Emit runtime patching of fargs entries that contain frame-relative // output references. For each tagged entry, resolves it using s0 // (frame pointer) and stores the resolved address back into the // fargs array in guest memory. // // Uses t0 (x5) and t1 (x6) as temporaries. // static void rv_patch_fargs(std::vector &code, uint64_t fargs_addr, const std::vector &farg_addrs) { for (size_t j = 0; j < farg_addrs.size(); j++) { if (rv_compiler::is_output_frame_ref(farg_addrs[j])) { // t0 = resolved address (s0 - delta) rv_load_val(code, 5, rv_compiler::output_frame_delta(farg_addrs[j])); code.push_back(rv_SUB(5, 8, 5)); // t0 = s0 - delta // Store resolved address into fargs[j] rv_load_val(code, 6, fargs_addr + j * 8); // t1 = &fargs[j] code.push_back(rv_SD(6, 5, 0)); // *t1 = t0 } } } // Emit ECALL to call a function. // // If func_idx > 0, uses indexed dispatch (ECALL_CALL_INDEX, a0 = index). // Otherwise, uses string dispatch (ECALL_CALL_FUNC, a0 = name_addr). // static void rv_emit_call(std::vector &code, uint64_t name_addr, uint64_t fargs_addr, int nfargs, uint64_t out_addr, int out_size, int func_idx = 0) { if (func_idx > 0) { // Indexed dispatch — no string lookup at runtime. code.push_back(rv_ADDI(17, 0, 0x101)); // a7 = ECALL_CALL_INDEX rv_load_val(code, 10, func_idx); // a0 = function index } else { // String-based dispatch (fallback). code.push_back(rv_ADDI(17, 0, 0x100)); // a7 = ECALL_CALL_FUNC rv_load_val(code, 10, name_addr); // a0 = name } rv_load_val(code, 11, fargs_addr); // a1 = fargs code.push_back(rv_ADDI(12, 0, nfargs)); // a2 = nfargs rv_load_guest_addr(code, 13, out_addr); // a3 = output rv_load_val(code, 14, out_size); // a4 = outsize code.push_back(rv_ECALL()); } static void rv_emit_exit(std::vector &code) { code.push_back(rv_ADDI(17, 0, ECALL_EXIT)); code.push_back(rv_ADDI(10, 0, 0)); code.push_back(rv_ECALL()); } // Emit a Tier 2 call: JAL to pre-compiled blob function. // Calling convention: a0=output, a1=fargs, a2=nfargs. // Return value in a0 (pointer to output buffer). // static void rv_emit_tier2_call(rv_compiler &rc, uint64_t fargs_addr, int nfargs, uint64_t out_addr, uint64_t func_guest_addr) { rv_load_guest_addr(rc.code, 10, out_addr); // a0 = output rv_load_val(rc.code, 11, fargs_addr); // a1 = fargs rc.code.push_back(rv_ADDI(12, 0, nfargs)); // a2 = nfargs // JAL ra, target — offset relative to current PC. uint64_t cur_pc = rc.current_pc(); int32_t offset = static_cast(func_guest_addr - cur_pc); rv_push_jal(rc, 1, offset); // JAL ra, blob_func } // --------------------------------------------------------------- // Walks the HIR instruction array and emits RV64 instructions. // Each HIR instruction gets a "location" — either a guest memory // address (TY_STRING) or an RV64 register (TY_INT). // =============================================================== struct hir_loc { uint64_t addr; // guest memory address (for strings) uint8_t reg; // RV64 register (for integers) bool in_reg; // true if value is in a register int spill_slot; // -1 = not spilled, >=0 = stack slot index }; // Branch patch record for backpatching. struct branch_patch { int code_idx; // index into rc.code int target_blk; // target block number }; // --------------------------------------------------------------- // Register allocation: linear scan over SSA live ranges // // Poletto-Sarkar algorithm. Computes live intervals for all // integer-typed SSA values, then assigns the 11 saved registers // (s1-s11). When register pressure exceeds 11, the interval // ending furthest in the future is spilled to the RV64 stack. // --------------------------------------------------------------- // Allocatable integer registers: s1-s10 (x9, x18-x26). // s11 (x27) is reserved as RA_SCRATCH. static constexpr int RA_NUM_REGS = 10; static constexpr uint8_t RA_REGS[RA_NUM_REGS] = { 9, 18, 19, 20, 21, 22, 23, 24, 25, 26 }; // Scratch register for spill/reload (s11 = x27, callee-saved). // Must be callee-saved to survive across ECALL and Tier 2 JAL calls. // Must not conflict with rv_emit_itoa/strcpy/atoi helpers (which use t0-t2). static constexpr uint8_t RA_SCRATCH = 27; // Frame-top register: incoming SP captured in the prologue (s0 = x8). static constexpr uint8_t RA_FRAME_TOP = 8; // Second scratch for two-operand instructions (t3 = x28). // Safe because arithmetic ops don't call atoi/itoa. static constexpr uint8_t RA_SCRATCH2 = 28; struct live_interval { int value; // HIR instruction index (SSA value number) int start; // program point of definition int end; // program point of last use (inclusive) }; struct reg_alloc_result { uint8_t reg[HIR_MAX_INSNS]; // assigned register (0 = spilled/none) int spill_slot[HIR_MAX_INSNS]; // -1 = not spilled int n_spill_slots; // total spill slots used }; static bool needs_output_buffer(hir_program &h, int i) { // CALL_T2I is TY_INT (the callee's return) but SPLIT_STEP-shaped calls // still write their string result into an output slot passed as a0 — // val[i] carries that flag (#2132). HIR_T2I_STR deliberately does NOT // allocate: it aliases its call's slot. if (h.kind[i] == HIR_CALL_T2I) return h.val[i] != 0; // A Lua handle is represented as a string buffer (#1579); only its // semantics differ, and those are enforced in the Lua lowerer. if (h.ty[i] != TY_STRING && h.ty[i] != TY_LUA_HANDLE) return false; switch (h.kind[i]) { case HIR_CALL: case HIR_STRCAT: case HIR_ITOA: case HIR_FTOA: case HIR_LUA_FTOA: // CALL_STR writes the library result into this slot and passes // OUT_SLOT as the bound (#1519 / #1679). Omitting it leaves // loc[i].addr at 0 so every CALL_STR aliases guest address 0 — // sequential single-result tests still pass by luck. // case HIR_LUA_CALL_STR: case HIR_LUA_MARSHAL: case HIR_PHI: case HIR_COPY: return true; default: return false; } } struct output_alloc_result { uint64_t addr[HIR_MAX_INSNS]; }; static output_alloc_result allocate_output_buffers(rv_compiler &rc, std::vector &intervals) { output_alloc_result result; memset(result.addr, 0, sizeof(result.addr)); if (intervals.empty()) return result; std::sort(intervals.begin(), intervals.end(), [](const live_interval &a, const live_interval &b) { return a.start < b.start; }); struct active_entry { int end; int value; uint64_t addr; }; std::vector active; std::vector free_pool; for (auto &iv : intervals) { // Defensive: every result.* array is sized HIR_MAX_INSNS and // indexed by the HIR value number. The HIR builder caps n_insns // at HIR_MAX_INSNS, so this never fires today, but a future pass // that synthesizes virtuals out of band must not silently corrupt // adjacent allocator state — bail the compile instead. if (iv.value < 0 || iv.value >= HIR_MAX_INSNS) { rc.out_exhausted = true; continue; } size_t j = 0; while (j < active.size()) { if (active[j].end >= iv.start) break; free_pool.push_back(active[j].addr); active.erase(active.begin() + j); } uint64_t addr; if (!free_pool.empty()) { addr = free_pool.back(); free_pool.pop_back(); } else { addr = rc.alloc_output(); if (addr == 0) break; } result.addr[iv.value] = addr; active_entry ae = {iv.end, iv.value, addr}; auto pos = std::lower_bound(active.begin(), active.end(), ae, [](const active_entry &a, const active_entry &b) { return a.end < b.end; }); active.insert(pos, ae); } return result; } // Returns true if HIR instruction i produces an integer that needs // a register. // static bool needs_int_reg(hir_program &h, int i) { switch (h.kind[i]) { case HIR_ICONST: case HIR_ATOI: case HIR_STRCMP: case HIR_CALL_T2I: // the callee's long return, in a0 (#2132) // NEWTABLE's result is a Lua stack index -- an integer as far as the // machine is concerned, and it must live in a register for SETI/GETI to // consume. Omitting it here does not fail loudly: the codegen's // `if (!dest) break;` emits nothing, so the ECALL silently never runs // and the consumer reads a garbage index (measured: SETI got idx=0). case HIR_LUA_NEWTABLE: case HIR_LUA_LEN: case HIR_LUA_INSN_BUDGET: case HIR_LUA_GETGLOBAL: case HIR_LUA_GETFIELD_REF: case HIR_LUA_CALL_INT: case HIR_LUA_CALL_VAL: case HIR_LUA_TOBOOL: case HIR_LUA_EQ: case HIR_LUA_GETFIELD: case HIR_LUA_GETI: case HIR_LUA_ALOAD: case HIR_ADD: case HIR_SUB: case HIR_MUL: case HIR_DIV: case HIR_REM: case HIR_NEG: case HIR_SIGN: case HIR_MAX: case HIR_MIN: case HIR_EQ: case HIR_NE: case HIR_GT: case HIR_LT: case HIR_GE: case HIR_LE: case HIR_NOT: case HIR_BOOL: case HIR_INC: case HIR_DEC: case HIR_BAND: case HIR_BOR: case HIR_BXOR: case HIR_BNOT: case HIR_SHL: case HIR_SHR: case HIR_FTOI: // float → int produces integer case HIR_FEQ: case HIR_FLT: case HIR_FLE: // float cmp → int 0/1 return true; case HIR_PHI: return h.ty[i] == TY_INT; case HIR_COPY: return h.ty[i] == TY_INT; default: return false; } } // Returns true if HIR instruction i produces a float that needs // an FP register (spilled to guest memory). // static bool needs_fp_reg(hir_program &h, int i) { switch (h.kind[i]) { case HIR_FCONST: case HIR_FADD: case HIR_FSUB: case HIR_FMUL: case HIR_FDIV: case HIR_FNEG: case HIR_FSQRT: case HIR_ITOF: case HIR_ATOF: case HIR_FCALL1: case HIR_FCALL2: // The first Lua opcode producing TY_FLOAT. Omitting a float producer // here is the FP twin of the needs_int_reg() trap above: no slot is // allocated, loc[].addr stays 0, and the FSD after the ECALL silently // writes guest address 0 (#1159's failure shape). case HIR_LUA_GETFIELD_FLT: return true; case HIR_PHI: return h.ty[i] == TY_FLOAT; case HIR_COPY: return h.ty[i] == TY_FLOAT; default: return false; } } // Compute live intervals for values passing the filter. // static void compute_live_ranges(hir_program &h, std::vector &intervals, bool (*filter)(hir_program &, int)) { // Assign program points in codegen order (blocks in layout order, // instructions within each block in order). // // Layout order is NOT execution order: the lowering can allocate an // outer loop's latch block before the blocks of an inner loop it // encloses (nested iter() does), and RPO is no help — a reverse // postorder only topologically orders the acyclic part and may also // place the latch before the loop body. The natural-loop closure // below is what makes intervals over this order sound. int prog_point[HIR_MAX_INSNS]; int block_end_pp[HIR_MAX_BLOCKS]; memset(prog_point, -1, sizeof(int) * h.n_insns); int pp = 0; for (int b = 0; b < h.n_blocks; b++) { if (h.block_first[b] <= h.block_last[b]) { for (int i = h.block_first[b]; i <= h.block_last[b]; i++) { if (h.blk[i] == b) { prog_point[i] = pp++; } } } block_end_pp[b] = pp++; // virtual point at end of block } int max_pp = pp; // Find last use program point for each value. int last_use[HIR_MAX_INSNS]; memset(last_use, -1, sizeof(int) * h.n_insns); for (int i = 0; i < h.n_insns; i++) { if (prog_point[i] < 0) continue; int pp_i = prog_point[i]; // Every operand extends its definition's live range to here. // // This was three hand-rolled cases -- src1, src2 with a BRC // exception, val[], then the carg[] loop -- duplicated from the two // walks in hir_opt.cpp. An operand shape the walk missed did not // fail loudly: the live range ended at the definition and the // allocator handed the register to something else while the ECALL // still expected it. The accessor knows every slot, including that // BRC's src2 is a block number. for (int sl = 0; sl < hir_operand_count(h, i); sl++) { int a = hir_operand_get(h, i, sl); if (a >= 0 && a < h.n_insns && pp_i > last_use[a]) { last_use[a] = pp_i; } } // PHI arguments: value is used at end of predecessor block. if (h.kind[i] == HIR_PHI) { for (int j = 0; j < h.pnargs[i]; j++) { int val = h.pval[h.pbase[i] + j]; int pred_blk = h.pblk[h.pbase[i] + j]; if (val >= 0 && val < h.n_insns && pred_blk >= 0 && pred_blk < h.n_blocks) { int end_pp = block_end_pp[pred_blk]; if (end_pp > last_use[val]) last_use[val] = end_pp; } } } } // Execution-reachability liveness closure (#2161). // // Linear scan needs every interval to be a superset of the value's // true live range under the chosen linearization — and BLOCK LAYOUT // ORDER IS NOT EXECUTION ORDER. The lowering allocates an outer // loop's latch before the blocks of an inner loop it encloses, and // allocates the program's result-assembly block before later loops // (this whole program's final STRCAT sat in block 27 while the last // iter's loop blocks were 28-33). Two observed corruptions, one // cause: // // - a value defined in an outer iter body and used in its latch had // an interval HOLE where the inner loop's points sit; the // allocator handed its register to inner-loop values, the latch // stored inner_final+1, and a triple-nested iter lost a level. // - a bracket's words() result died at the final STRCAT's layout // point; loops with HIGHER layout points but EARLIER execution // recycled its output slot, and the result read a later lnum's // leftovers. // // (RPO cannot fix the ordering either: a reverse postorder may also // place a latch before the body it follows at runtime.) // // The uniform sound rule: for every use of v, v must be live in every // block that can execute between the def and that use — every B with // def-block -> B reachable and B -> use-block reachable, back edges // included. Cycles make a loop's blocks mutually reachable, so this // subsumes the loop case; straight-line out-of-order blocks are just // the acyclic instance. Conservative — some intervals get longer and // some values spill that did not before — and that is the correct // direction. { // reach[b] = blocks reachable from b, reflexive. static thread_local std::bitset reach[HIR_MAX_BLOCKS]; for (int b = 0; b < h.n_blocks; b++) { reach[b].reset(); reach[b].set(b); } bool changed = true; while (changed) { changed = false; for (int b = 0; b < h.n_blocks; b++) { for (int s = 0; s < h.block_nsucc[b]; s++) { int t = h.block_succ[b][s]; if (t < 0 || t >= h.n_blocks) continue; std::bitset merged = reach[b] | reach[t]; if (merged != reach[b]) { reach[b] = merged; changed = true; } } } } auto extend_use = [&](int v, int use_blk) { if (v < 0 || v >= h.n_insns || prog_point[v] < 0) return; if (use_blk < 0 || use_blk >= h.n_blocks) return; int d = h.blk[v]; if (d < 0 || d >= h.n_blocks) return; int m = last_use[v]; for (int B = 0; B < h.n_blocks; B++) { if (reach[d][B] && reach[B][use_blk] && block_end_pp[B] > m) { m = block_end_pp[B]; } } if (m > last_use[v]) last_use[v] = m; }; for (int i = 0; i < h.n_insns; i++) { if (prog_point[i] < 0) continue; for (int sl = 0; sl < hir_operand_count(h, i); sl++) { extend_use(hir_operand_get(h, i, sl), h.blk[i]); } // PHI operands are uses at the end of their predecessor. if (h.kind[i] == HIR_PHI) { for (int j = 0; j < h.pnargs[i]; j++) { extend_use(h.pval[h.pbase[i] + j], h.pblk[h.pbase[i] + j]); } } } } // The final result must survive to the end of the program. if (h.result >= 0 && h.result < h.n_insns && filter(h, h.result)) { last_use[h.result] = max_pp - 1; } // Build intervals for values passing the filter. intervals.clear(); for (int i = 0; i < h.n_insns; i++) { if (!filter(h, i)) continue; if (prog_point[i] < 0) continue; // unreachable int def = prog_point[i]; // PHI: start at earliest predecessor's block end point. if (h.kind[i] == HIR_PHI && h.pnargs[i] > 0) { for (int j = 0; j < h.pnargs[i]; j++) { int pred_blk = h.pblk[h.pbase[i] + j]; if (pred_blk >= 0 && pred_blk < h.n_blocks) { int ep = block_end_pp[pred_blk]; if (ep < def) def = ep; } } } int end = (last_use[i] >= def) ? last_use[i] : def; intervals.push_back({i, def, end}); } } // Poletto-Sarkar linear scan register allocation. // static reg_alloc_result linear_scan(rv_compiler &rc, std::vector &intervals) { reg_alloc_result result; memset(result.reg, 0, sizeof(result.reg)); memset(result.spill_slot, -1, sizeof(result.spill_slot)); result.n_spill_slots = 0; if (intervals.empty()) return result; // Sort intervals by start point. std::sort(intervals.begin(), intervals.end(), [](const live_interval &a, const live_interval &b) { return a.start < b.start; }); // Free register pool (stack-based for fast alloc/free). uint8_t free_regs[RA_NUM_REGS]; int n_free = RA_NUM_REGS; for (int i = 0; i < RA_NUM_REGS; i++) { free_regs[i] = RA_REGS[RA_NUM_REGS - 1 - i]; // s11 at bottom } // Active intervals, sorted by end point ascending. // Small-N (max 11 entries), so linear insertion is fine. struct active_entry { int end; int value; uint8_t reg; }; std::vector active; for (auto &iv : intervals) { // Defensive bounds check: result.reg[]/spill_slot[] are sized // HIR_MAX_INSNS and indexed by iv.value. See allocate_output_buffers. if (iv.value < 0 || iv.value >= HIR_MAX_INSNS) { rc.out_exhausted = true; continue; } // ExpireOldIntervals: remove intervals that ended before iv.start. size_t j = 0; while (j < active.size()) { if (active[j].end >= iv.start) break; // sorted: rest are live // Return register to free pool. free_regs[n_free++] = active[j].reg; active.erase(active.begin() + j); // Don't increment j — next element shifted down. } if (n_free > 0) { // Assign a register. uint8_t reg = free_regs[--n_free]; result.reg[iv.value] = reg; // Insert into active, maintaining sort by end. active_entry ae = {iv.end, iv.value, reg}; auto pos = std::lower_bound(active.begin(), active.end(), ae, [](const active_entry &a, const active_entry &b) { return a.end < b.end; }); active.insert(pos, ae); } else { // Spill: evict the interval ending furthest in the future. auto &spill = active.back(); // largest end if (spill.end > iv.end) { // Spill the active interval, give its register to iv. result.reg[iv.value] = spill.reg; result.reg[spill.value] = 0; result.spill_slot[spill.value] = result.n_spill_slots++; // Remove spilled interval from active. active.pop_back(); // Insert iv into active. active_entry ae = {iv.end, iv.value, result.reg[iv.value]}; auto pos = std::lower_bound(active.begin(), active.end(), ae, [](const active_entry &a, const active_entry &b) { return a.end < b.end; }); active.insert(pos, ae); } else { // Spill the new interval (it ends later than everything). result.spill_slot[iv.value] = result.n_spill_slots++; } } } return result; } // Spill slot stack offset: +8*slot from the post-prologue SP. // // These MUST be at non-negative offsets — i.e. inside the frame the // prologue reserves — not below SP. They used to live at -8*(slot+1), // which kept them clear of the output buffers but put them in the one // region of the stack that belongs to nobody: RV64 has no red zone, so // the first JAL into a blob function let gcc-compiled callee code build // its frame right on top of them. A spilled value reloaded after any // tier2 call read the callee's dead locals. // // Nothing ever noticed because nothing ever both spilled AND called: // the ITER cursor rework (#2052) was the first program with more than // 10 live ints around a tier2 call, and the symptom was an is_first // comparison whose spilled operand read garbage — while the same SSA // value, read from a register two instructions earlier, was correct // (iter(a,%i0) twice in one expression: second loop lost its // accumulator). The prologue backpatch sizes the frame to cover // these slots; see "spill_area" below. static int32_t spill_offset(int slot) { return 8 * slot; } // Emit SD reg, off(sp) — store integer register to spill slot. static void emit_spill_store(std::vector &code, uint8_t reg, int slot) { code.push_back(rv_SD(2, reg, spill_offset(slot))); } // Emit LD rd, off(sp) — reload integer register from spill slot. static void emit_spill_load(std::vector &code, uint8_t rd, int slot) { code.push_back(rv_LD(rd, 2, spill_offset(slot))); } // Get the register holding integer value v, reloading from spill // slot if necessary. scratch = register to reload into if spilled. // static uint8_t ra_get_reg(rv_compiler &rc, hir_loc *loc, int v, uint8_t scratch) { if (v < 0) return 0; if (loc[v].spill_slot >= 0 && !loc[v].in_reg) { emit_spill_load(rc.code, scratch, loc[v].spill_slot); return scratch; } return loc[v].reg; } // Set loc[i] from allocation result and optionally emit spill. // dest = the register the value was computed into. // Returns the destination register. // static void ra_set_loc(rv_compiler &rc, hir_loc *loc, reg_alloc_result &alloc, int i, uint8_t computed_in) { uint8_t assigned = alloc.reg[i]; int slot = alloc.spill_slot[i]; if (assigned != 0) { // Value lives in a register. loc[i].reg = assigned; loc[i].in_reg = true; loc[i].spill_slot = -1; } else if (slot >= 0) { // Value is spilled — emit store. emit_spill_store(rc.code, computed_in, slot); loc[i].reg = 0; loc[i].in_reg = false; loc[i].spill_slot = slot; } } // Emit the argument setup shared by the three HIR_LUA_CALL_* variants. // The arguments come off the instruction's carg[] list; the kind bits in // val[] say what each register carries (see ecall_lua_push_call_args in // jit_compiler.cpp): 0 is an integer via ra_get_reg into x12+j, 1 is an // SCONST's guest address into x12+j, 2 is a double loaded from its FP // slot and moved as raw bits into x12+j over the FMV.X.D lane // ECALL_LUA_FTOA already proved on both execution routes. The kind bits // the lowering set are the single source of truth here; re-deriving them // from h.kind/h.ty would be a second opinion that could disagree with // what the handler will decode. // // Returns nargs | kinds<<8, the a1 payload every call variant sends. // static int emit_lua_call_args(rv_compiler &rc, hir_program &h, hir_loc *loc, int i) { const int nargs = h.cnargs[i]; const int kinds = static_cast(h.val[i]) & 0xFF; for (int j = 0; j < nargs && j < 3; j++) { const int v = h.carg[h.cbase[i] + j]; if (v < 0) continue; const uint8_t xd = static_cast(12 + j); switch ((kinds >> (2 * j)) & 3) { case 1: rv_load_guest_addr(rc.code, xd, loc[v].addr); break; case 2: rv_load_guest_addr(rc.code, RA_SCRATCH, loc[v].addr); rc.code.push_back(rv_FLD(0, RA_SCRATCH, 0)); rc.code.push_back(rv_FMV_X_D(xd, 0)); break; default: { uint8_t r = ra_get_reg(rc, loc, v, static_cast(28 + j)); rc.code.push_back(rv_ADDI(xd, r, 0)); break; } } } return (nargs & 0xFF) | (kinds << 8); } // Emit PHI copies: when branching from from_blk to to_blk, // emit moves for any PHI nodes at the target block. // static void emit_phi_copies(hir_program &h, rv_compiler &rc, hir_loc *loc, int from_blk, int to_blk) { // A branch target outside [0, n_blocks) is an unpatched placeholder // or an overflowed new_block() — indexing block_first[] with it reads // wild memory (#858). Flag it and skip; the branch emits no copies. if (to_blk < 0 || to_blk >= h.n_blocks) { fprintf(stderr, "hir_codegen: emit_phi_copies invalid to_blk=%d " "(from_blk=%d n_blocks=%d)\n", to_blk, from_blk, h.n_blocks); return; } if (h.block_first[to_blk] > h.block_last[to_blk]) return; for (int i = h.block_first[to_blk]; i <= h.block_last[to_blk]; i++) { if (h.blk[i] != to_blk || h.kind[i] != HIR_PHI) continue; // Find the PHI argument for from_blk. int base = h.pbase[i]; for (int j = 0; j < h.pnargs[i]; j++) { if (h.pblk[base + j] != from_blk) continue; int val = h.pval[base + j]; if (val < 0) break; // Float PHI: both slots are 8-byte guest doubles — FLD/FSD. // Must not fall into the string path (strcpy of float bits). if (h.ty[i] == TY_FLOAT) { rv_load_guest_addr(rc.code, RA_SCRATCH, loc[val].addr); rc.code.push_back(rv_FLD(0, RA_SCRATCH, 0)); rv_load_guest_addr(rc.code, RA_SCRATCH, loc[i].addr); rc.code.push_back(rv_FSD(RA_SCRATCH, 0, 0)); break; } bool phi_is_int = (loc[i].in_reg || loc[i].spill_slot >= 0); if (phi_is_int) { // Integer PHI (registered or spilled). uint8_t phi_dest = loc[i].in_reg ? loc[i].reg : RA_SCRATCH; uint8_t val_reg; if (loc[val].in_reg) { val_reg = loc[val].reg; } else if (loc[val].spill_slot >= 0) { // Spilled integer operand: reload. emit_spill_load(rc.code, RA_SCRATCH2, loc[val].spill_slot); val_reg = RA_SCRATCH2; } else { // String value used as int PHI — load addr and atoi. rv_load_guest_addr(rc.code, 10, loc[val].addr); rv_emit_atoi(rc.code, 10, phi_dest); if (loc[i].spill_slot >= 0 && !loc[i].in_reg) { emit_spill_store(rc.code, phi_dest, loc[i].spill_slot); } break; } rc.code.push_back(rv_ADD(phi_dest, val_reg, 0)); if (loc[i].spill_slot >= 0 && !loc[i].in_reg) { emit_spill_store(rc.code, phi_dest, loc[i].spill_slot); } } else { // String PHI: copy string to PHI's output buffer. if (loc[val].in_reg) { // Integer val → ITOA to PHI buffer. rv_load_guest_addr(rc.code, 10, loc[i].addr); rv_emit_itoa(rc.code, loc[val].reg, 10); } else if (loc[val].spill_slot >= 0) { // Spilled integer val → reload, then ITOA. emit_spill_load(rc.code, RA_SCRATCH, loc[val].spill_slot); rv_load_guest_addr(rc.code, 10, loc[i].addr); rv_emit_itoa(rc.code, RA_SCRATCH, 10); } else { // String → string: byte copy. rv_load_guest_addr(rc.code, 7, loc[i].addr); // t2 = dest rv_load_guest_addr(rc.code, 6, loc[val].addr); // t1 = src rv_emit_strcpy(rc.code, 7, 6); } } break; } } } void hir_codegen(hir_program &h, rv_compiler &rc) { // Location map: where each instruction's result lives. hir_loc loc[HIR_MAX_INSNS]; memset(loc, 0, sizeof(loc)); for (int i = 0; i < h.n_insns; i++) loc[i].spill_slot = -1; // Block code offsets for branch backpatching. int block_offset[HIR_MAX_BLOCKS]; memset(block_offset, 0, sizeof(block_offset)); std::vector patches; // 1. Run register allocation for integers. std::vector int_intervals; compute_live_ranges(h, int_intervals, needs_int_reg); reg_alloc_result int_alloc = linear_scan(rc, int_intervals); rc.spills = int_alloc.n_spill_slots; // 2. Run liveness-based allocation for output buffers. std::vector str_intervals; compute_live_ranges(h, str_intervals, needs_output_buffer); output_alloc_result str_alloc = allocate_output_buffers(rc, str_intervals); // 2b. A program with branches can reach more than one HIR_RET, and which // one runs is only known at runtime. The result location, though, is // fixed at compile time (h.result, set from the *first* return), so the // answer used to come from the first return site no matter which one // executed -- the branch was taken correctly and the value ignored // (#1486). Give every return one shared slot to write into. // // Only for multi-block programs. A single block runs top to bottom and // exits at its first HIR_RET, so first-return *is* the executed return // there, and keeping the compile-time result preserves constant folding // for straight-line chunks like `return 42`. // if (h.n_blocks > 1) { for (int i = 0; i < h.n_insns; i++) { if (h.kind[i] == HIR_RET && h.src1[i] >= 0) { rc.ret_out = rc.alloc_output(); break; } } } // 3. Allocate 8-byte guest memory slots for FP values. // Simple bump allocation — no register caching for FP in HIR. // Each FP value gets a slot; operations load/store via FLD/FSD. // The DBT's x86-64 translator will optimize these into XMM regs. uint64_t fp_pool = (rc.str_pool + 7) & ~7ULL; // align to 8 for (int i = 0; i < h.n_insns; i++) { if (needs_fp_reg(h, i)) { // Bound against THIS compile's pool limit, not the one-shot // compiler's static STR_LIMIT. The shared heap lays its // string pool at 0x40000..0x60000, well above the static // 0x4000, so the old test failed on the first FP value and // left every slot addr at 0 — all float temporaries then // aliased guest address 0 and silently computed garbage // (#1159). Genuine exhaustion must fail the compile, not // hand back address 0. if (fp_pool + 8 > rc.str_pool_limit) { rc.out_exhausted = true; break; } loc[i].addr = fp_pool; loc[i].in_reg = false; loc[i].spill_slot = -1; fp_pool += 8; } } rc.str_pool = fp_pool; // Pre-populate loc map from allocation results. for (int i = 0; i < h.n_insns; i++) { if (needs_int_reg(h, i)) { loc[i].reg = int_alloc.reg[i]; loc[i].in_reg = (loc[i].reg != 0); loc[i].spill_slot = int_alloc.spill_slot[i]; } if (needs_output_buffer(h, i)) { loc[i].addr = str_alloc.addr[i]; loc[i].in_reg = false; } } // Reserve prologue slots for stack frame setup. // First instruction snapshots the incoming SP into s0 so // stack-allocated output buffers can be addressed relative to // the function's entry stack pointer even across nested calls. // Output buffers are stack-allocated; the prologue decrements SP // by the total frame size. We don't know the final count yet // (the itoa path may allocate 1 more slot), so reserve 3 NOPs // and backpatch after all code is emitted. // // 4 instructions = mv s0, sp + LUI t0, upper + ADDI t0, t0, lower // + SUB sp, sp, t0 // rc.code.push_back(rv_ADDI(RA_FRAME_TOP, 2, 0)); // mv s0, sp size_t prologue_pos = rc.code.size(); static constexpr int PROLOGUE_SLOTS = 3; for (int p = 0; p < PROLOGUE_SLOTS; p++) { rc.code.push_back(rv_ADDI(0, 0, 0)); // NOP placeholder } // Process blocks in layout order. for (int b = 0; b < h.n_blocks; b++) { block_offset[b] = static_cast(rc.code.size()); if (h.block_first[b] > h.block_last[b]) continue; for (int i = h.block_first[b]; i <= h.block_last[b]; i++) { if (h.blk[i] != b) continue; switch (h.kind[i]) { case HIR_SCONST: loc[i].addr = static_cast(h.val[i]); loc[i].in_reg = false; break; case HIR_ICONST: { uint8_t reg = int_alloc.reg[i]; bool spilled = (reg == 0 && int_alloc.spill_slot[i] >= 0); uint8_t dest = spilled ? RA_SCRATCH : reg; if (!dest) break; rv_load_i64(rc.code, dest, h.val[i]); ra_set_loc(rc, loc, int_alloc, i, dest); break; } case HIR_ATOI: { int s1 = h.src1[i]; uint8_t reg = int_alloc.reg[i]; bool spilled = (reg == 0 && int_alloc.spill_slot[i] >= 0); uint8_t dest = spilled ? RA_SCRATCH : reg; if (!dest) break; // Compile-time only for true constants. runtime_ref SCONSTs // (CARGS/SUBST, mux.args) have empty sval but a live guest // address — atoi must run at runtime (#1309). // if ( h.kind[s1] == HIR_SCONST && !h.runtime_ref[s1]) { int64_t v = static_cast( mux_atoi64(u8(h.sval[s1]))); rv_load_i64(rc.code, dest, v); } else { // SCONST runtime_ref: loc.addr was set from h.val (guest // CARGS/SUBST slot). Other sources use normal loc. // uint64_t addr = (h.kind[s1] == HIR_SCONST) ? static_cast(h.val[s1]) : loc[s1].addr; rv_load_guest_addr(rc.code, 10, addr); rv_emit_atoi(rc.code, 10, dest); } ra_set_loc(rc, loc, int_alloc, i, dest); break; } case HIR_STRCMP: { int s1 = h.src1[i], s2 = h.src2[i]; uint8_t reg = int_alloc.reg[i]; bool spilled = (reg == 0 && int_alloc.spill_slot[i] >= 0); uint8_t dest = spilled ? RA_SCRATCH : reg; if (!dest) break; if ( h.kind[s1] == HIR_SCONST && h.kind[s2] == HIR_SCONST && !h.runtime_ref[s1] && !h.runtime_ref[s2]) { int r = strcmp(h.sval[s1].c_str(), h.sval[s2].c_str()); rv_load_i64(rc.code, dest, r < 0 ? -1 : r > 0 ? 1 : 0); } else { uint64_t a1 = (h.kind[s1] == HIR_SCONST) ? static_cast(h.val[s1]) : loc[s1].addr; uint64_t a2 = (h.kind[s2] == HIR_SCONST) ? static_cast(h.val[s2]) : loc[s2].addr; rv_load_guest_addr(rc.code, 10, a1); rv_load_guest_addr(rc.code, 11, a2); rv_emit_strcmp(rc.code, 10, 11, dest); } ra_set_loc(rc, loc, int_alloc, i, dest); break; } case HIR_LUA_GETGLOBAL: { // a0 = key addr -> a0 = stack idx. int s1 = h.src1[i]; uint8_t reg = int_alloc.reg[i]; bool spilled = (reg == 0 && int_alloc.spill_slot[i] >= 0); uint8_t dest = spilled ? RA_SCRATCH : reg; if (!dest) break; if (h.kind[s1] != HIR_SCONST) break; rv_load_guest_addr(rc.code, 10, loc[s1].addr); rc.code.push_back(rv_ADDI(17, 0, static_cast(ECALL_LUA_GETGLOBAL))); rc.code.push_back(rv_ECALL()); rc.code.push_back(rv_ADDI(dest, 10, 0)); ra_set_loc(rc, loc, int_alloc, i, dest); break; } case HIR_LUA_GETFIELD_REF: { // a0=tbl_idx, a1=key addr -> a0=stack idx. int s1 = h.src1[i], s2 = h.src2[i]; uint8_t reg = int_alloc.reg[i]; bool spilled = (reg == 0 && int_alloc.spill_slot[i] >= 0); uint8_t dest = spilled ? RA_SCRATCH : reg; if (!dest) break; if (h.kind[s2] != HIR_SCONST) break; uint8_t tbl_r = ra_get_reg(rc, loc, s1, RA_SCRATCH); rc.code.push_back(rv_ADDI(10, tbl_r, 0)); rv_load_guest_addr(rc.code, 11, loc[s2].addr); rc.code.push_back(rv_ADDI(17, 0, static_cast(ECALL_LUA_GETFIELD_REF))); rc.code.push_back(rv_ECALL()); rc.code.push_back(rv_ADDI(dest, 10, 0)); ra_set_loc(rc, loc, int_alloc, i, dest); break; } case HIR_LUA_CALL_STR: { // a0=fn, a1=nargs|kinds, a2..a4=args, a5=out addr, // a6=out size. Result is TY_STRING, so its guest buffer // is the output slot the allocator already assigned -- // OUT_SLOT bytes, passed explicitly. uint8_t fn_r = ra_get_reg(rc, loc, h.src1[i], RA_SCRATCH); rc.code.push_back(rv_ADDI(10, fn_r, 0)); int a1val = emit_lua_call_args(rc, h, loc, i); rv_load_i64(rc.code, 11, a1val); rv_load_guest_addr(rc.code, 15, loc[i].addr); rv_load_i64(rc.code, 16, rv_compiler::OUT_SLOT); rc.code.push_back(rv_ADDI(17, 0, static_cast(ECALL_LUA_CALL_STR))); rc.code.push_back(rv_ECALL()); break; } case HIR_LUA_CALL_VAL: { // a0=fn, a1=nargs|kinds, a2..a4=args → a0=stack index. // Result is a live Lua value (handle), not text. uint8_t reg = int_alloc.reg[i]; bool spilled = (reg == 0 && int_alloc.spill_slot[i] >= 0); uint8_t dest = spilled ? RA_SCRATCH : reg; if (!dest) break; uint8_t fn_r = ra_get_reg(rc, loc, h.src1[i], RA_SCRATCH); rc.code.push_back(rv_ADDI(10, fn_r, 0)); int a1val = emit_lua_call_args(rc, h, loc, i); rv_load_i64(rc.code, 11, a1val); rc.code.push_back(rv_ADDI(17, 0, static_cast(ECALL_LUA_CALL_VAL))); rc.code.push_back(rv_ECALL()); rc.code.push_back(rv_ADDI(dest, 10, 0)); ra_set_loc(rc, loc, int_alloc, i, dest); break; } case HIR_LUA_MARSHAL: { // a0=stack idx, a1=out addr, a2=out size. Result TY_STRING // lives in the output slot. int s1 = h.src1[i]; uint8_t r1 = ra_get_reg(rc, loc, s1, RA_SCRATCH); rc.code.push_back(rv_ADDI(10, r1, 0)); rv_load_guest_addr(rc.code, 11, loc[i].addr); rv_load_i64(rc.code, 12, rv_compiler::OUT_SLOT); rc.code.push_back(rv_ADDI(17, 0, static_cast(ECALL_LUA_MARSHAL))); rc.code.push_back(rv_ECALL()); break; } case HIR_LUA_TOBOOL: { // a0=stack idx → a0=0/1 Lua truthiness. int s1 = h.src1[i]; uint8_t reg = int_alloc.reg[i]; bool spilled = (reg == 0 && int_alloc.spill_slot[i] >= 0); uint8_t dest = spilled ? RA_SCRATCH : reg; if (!dest) break; uint8_t r1 = ra_get_reg(rc, loc, s1, RA_SCRATCH); rc.code.push_back(rv_ADDI(10, r1, 0)); rc.code.push_back(rv_ADDI(17, 0, static_cast(ECALL_LUA_TOBOOL))); rc.code.push_back(rv_ECALL()); rc.code.push_back(rv_ADDI(dest, 10, 0)); ra_set_loc(rc, loc, int_alloc, i, dest); break; } case HIR_LUA_EQ: { // a0=lhs handle, a1=kind, a2=rhs → a0=0/1 under Lua ==. int s1 = h.src1[i], s2 = h.src2[i]; int kind = static_cast(h.val[i]); uint8_t reg = int_alloc.reg[i]; bool spilled = (reg == 0 && int_alloc.spill_slot[i] >= 0); uint8_t dest = spilled ? RA_SCRATCH : reg; if (!dest) break; uint8_t r1 = ra_get_reg(rc, loc, s1, RA_SCRATCH); rc.code.push_back(rv_ADDI(10, r1, 0)); rv_load_i64(rc.code, 11, kind); if (kind == 1) { // String constant: guest address of the pool string. if (h.kind[s2] != HIR_SCONST) break; rv_load_guest_addr(rc.code, 12, loc[s2].addr); } else if (kind == 0 || kind == 4) { // Integer / bool payload in ICONST val, or register. if (h.kind[s2] == HIR_ICONST) { rv_load_i64(rc.code, 12, h.val[s2]); } else { uint8_t r2 = ra_get_reg(rc, loc, s2, 29); rc.code.push_back(rv_ADDI(12, r2, 0)); } } else if (kind == 2) { uint8_t r2 = ra_get_reg(rc, loc, s2, 29); rc.code.push_back(rv_ADDI(12, r2, 0)); } else if (kind == 3) { rv_load_i64(rc.code, 12, 0); } else { break; } rc.code.push_back(rv_ADDI(17, 0, static_cast(ECALL_LUA_EQ))); rc.code.push_back(rv_ECALL()); rc.code.push_back(rv_ADDI(dest, 10, 0)); ra_set_loc(rc, loc, int_alloc, i, dest); break; } case HIR_LUA_CALL_INT: { // a0=fn idx, a1=nargs|kinds, a2..a4=args -> a0=int result. // Arguments come off the carg[] list; see emit_lua_call in // hir.h for why there is no packed operand any more. uint8_t reg = int_alloc.reg[i]; bool spilled = (reg == 0 && int_alloc.spill_slot[i] >= 0); uint8_t dest = spilled ? RA_SCRATCH : reg; if (!dest) break; uint8_t fn_r = ra_get_reg(rc, loc, h.src1[i], RA_SCRATCH); rc.code.push_back(rv_ADDI(10, fn_r, 0)); // Argument setup is identical to CALL_STR -- one emitter; // only the result handling below differs. int a1val = emit_lua_call_args(rc, h, loc, i); rv_load_i64(rc.code, 11, a1val); rc.code.push_back(rv_ADDI(17, 0, static_cast(ECALL_LUA_CALL_INT))); rc.code.push_back(rv_ECALL()); rc.code.push_back(rv_ADDI(dest, 10, 0)); ra_set_loc(rc, loc, int_alloc, i, dest); break; } case HIR_LUA_CALL_VOID: { // a0=fn idx, a1=nargs|kinds, a2..a4=args; no result. The // call is FOR its side effect -- table.insert -- which is // why the opcode sits in has_side_effects(): an // unused-result call DCE may NOP is precisely the one this // exists to keep (#1145's SETI lesson). uint8_t fn_r = ra_get_reg(rc, loc, h.src1[i], RA_SCRATCH); rc.code.push_back(rv_ADDI(10, fn_r, 0)); int a1val = emit_lua_call_args(rc, h, loc, i); rv_load_i64(rc.code, 11, a1val); rc.code.push_back(rv_ADDI(17, 0, static_cast(ECALL_LUA_CALL_VOID))); rc.code.push_back(rv_ECALL()); break; } case HIR_LUA_GETFIELD: { // a0=tbl_idx, a1=key addr -> a0=value. int s1 = h.src1[i], s2 = h.src2[i]; uint8_t reg = int_alloc.reg[i]; bool spilled = (reg == 0 && int_alloc.spill_slot[i] >= 0); uint8_t dest = spilled ? RA_SCRATCH : reg; if (!dest) break; uint8_t tbl_r = ra_get_reg(rc, loc, s1, RA_SCRATCH); rc.code.push_back(rv_ADDI(10, tbl_r, 0)); // The key is an SCONST: it lives as loc[].addr with // in_reg=false, so ra_get_reg would hand back a register // that was never loaded. Materialize the address. Reading // it through ra_get_reg made every field read return the // LAST value written, because a1 held the same stale // address on every call. if (h.kind[s2] != HIR_SCONST) break; rv_load_guest_addr(rc.code, 11, loc[s2].addr); rc.code.push_back(rv_ADDI(17, 0, static_cast(ECALL_LUA_GETFIELD_INT))); rc.code.push_back(rv_ECALL()); rc.code.push_back(rv_ADDI(dest, 10, 0)); ra_set_loc(rc, loc, int_alloc, i, dest); break; } case HIR_LUA_GETFIELD_FLT: { // a0=tbl_idx, a1=key addr -> a0=double BITS, a1=ok. The // result's home is its FP slot, so store the bits there // directly; no integer register is allocated or needed. int s1 = h.src1[i], s2 = h.src2[i]; uint8_t tbl_r = ra_get_reg(rc, loc, s1, RA_SCRATCH); rc.code.push_back(rv_ADDI(10, tbl_r, 0)); // SCONST key: materialize the pool address, as GETFIELD // does and for the same reason (stale-register trap). if (h.kind[s2] != HIR_SCONST) break; rv_load_guest_addr(rc.code, 11, loc[s2].addr); rc.code.push_back(rv_ADDI(17, 0, static_cast(ECALL_LUA_GETFIELD_FLT))); rc.code.push_back(rv_ECALL()); rv_load_guest_addr(rc.code, RA_SCRATCH, loc[i].addr); rc.code.push_back(rv_SD(RA_SCRATCH, 10, 0)); break; } case HIR_LUA_LIMITED: { // Back-edge budget exhausted (#1732). The handler declines // the whole run; nothing after this executes, so there are // no operands and no result. rc.code.push_back(rv_ADDI(17, 0, static_cast(ECALL_LUA_LIMITED))); rc.code.push_back(rv_ECALL()); break; } case HIR_LUA_SETFIELD: { // a0=tbl_idx, a1=key addr, a2=value (third operand in val[]). int s1 = h.src1[i], s2 = h.src2[i]; int s3 = static_cast(h.val[i]); uint8_t tbl_r = ra_get_reg(rc, loc, s1, RA_SCRATCH); rc.code.push_back(rv_ADDI(10, tbl_r, 0)); // SCONST key: address, not a register. See GETFIELD above. if (h.kind[s2] != HIR_SCONST) break; uint8_t val_r = ra_get_reg(rc, loc, s3, 29); rc.code.push_back(rv_ADDI(12, val_r, 0)); rv_load_guest_addr(rc.code, 11, loc[s2].addr); rc.code.push_back(rv_ADDI(17, 0, static_cast(ECALL_LUA_SETFIELD_INT))); rc.code.push_back(rv_ECALL()); break; } case HIR_LUA_INSN_BUDGET: { // No-arg ECALL: a0 = the CURRENT lua_instruction_limit. // This exists so the back-edge budget is seeded per run // rather than baked as an ICONST at lowering -- a compiled // program must not contain a config value (#1745, the // #1613 shape on the compiled path). uint8_t reg = int_alloc.reg[i]; bool spilled = (reg == 0 && int_alloc.spill_slot[i] >= 0); uint8_t dest = spilled ? RA_SCRATCH : reg; if (!dest) break; rc.code.push_back(rv_ADDI(17, 0, static_cast(ECALL_LUA_INSN_BUDGET))); rc.code.push_back(rv_ECALL()); rc.code.push_back(rv_ADDI(dest, 10, 0)); // dest = a0 ra_set_loc(rc, loc, int_alloc, i, dest); break; } case HIR_LUA_LEN: { // Dedicated ECALL: a0=tbl_idx -> a0=length. int s1 = h.src1[i]; uint8_t reg = int_alloc.reg[i]; bool spilled = (reg == 0 && int_alloc.spill_slot[i] >= 0); uint8_t dest = spilled ? RA_SCRATCH : reg; if (!dest) break; uint8_t tbl_r = ra_get_reg(rc, loc, s1, RA_SCRATCH); rc.code.push_back(rv_ADDI(10, tbl_r, 0)); // a0 = tbl_idx rc.code.push_back(rv_ADDI(17, 0, static_cast(ECALL_LUA_LEN_INT))); rc.code.push_back(rv_ECALL()); rc.code.push_back(rv_ADDI(dest, 10, 0)); // dest = a0 ra_set_loc(rc, loc, int_alloc, i, dest); break; } case HIR_LUA_GETI: { // Dedicated ECALL: a0=tbl_idx, a1=key → a0=value (int64) int s1 = h.src1[i], s2 = h.src2[i]; uint8_t reg = int_alloc.reg[i]; bool spilled = (reg == 0 && int_alloc.spill_slot[i] >= 0); uint8_t dest = spilled ? RA_SCRATCH : reg; if (!dest) break; // s1 = table stack index (known_int), s2 = integer key uint8_t tbl_r = ra_get_reg(rc, loc, s1, RA_SCRATCH); rc.code.push_back(rv_ADDI(10, tbl_r, 0)); // a0 = tbl_idx uint8_t key_r = ra_get_reg(rc, loc, s2, 28); // t3 as scratch rc.code.push_back(rv_ADDI(11, key_r, 0)); // a1 = key rc.code.push_back(rv_ADDI(17, 0, static_cast(ECALL_LUA_GETI_INT))); rc.code.push_back(rv_ECALL()); // Result in a0 (x10). Success flag in a1 (x11) — ignored for now. rc.code.push_back(rv_ADDI(dest, 10, 0)); // dest = a0 ra_set_loc(rc, loc, int_alloc, i, dest); break; } case HIR_LUA_NEWTABLE: { // Dedicated ECALL: a0=narr, a1=nrec -> a0=stack_idx. // // The result IS a stack index, so table creation has no // value form -- but it rides in a register as a typed // TY_LUA_HANDLE rather than as a decimal string in guest // memory, which is what keeps it distinguishable from an // integer downstream (#1519). int s1 = h.src1[i], s2 = h.src2[i]; uint8_t reg = int_alloc.reg[i]; bool spilled = (reg == 0 && int_alloc.spill_slot[i] >= 0); uint8_t dest = spilled ? RA_SCRATCH : reg; if (!dest) break; uint8_t narr_r = ra_get_reg(rc, loc, s1, RA_SCRATCH); rc.code.push_back(rv_ADDI(10, narr_r, 0)); // a0 = narr uint8_t nrec_r = ra_get_reg(rc, loc, s2, 28); rc.code.push_back(rv_ADDI(11, nrec_r, 0)); // a1 = nrec rc.code.push_back(rv_ADDI(17, 0, static_cast(ECALL_LUA_NEWTABLE))); rc.code.push_back(rv_ECALL()); rc.code.push_back(rv_ADDI(dest, 10, 0)); // dest = a0 ra_set_loc(rc, loc, int_alloc, i, dest); break; } case HIR_LUA_SETI: { // Dedicated ECALL: a0=tbl_idx, a1=key, a2=value int s1 = h.src1[i], s2 = h.src2[i]; int s3 = static_cast(h.val[i]); // 3rd operand stored in val uint8_t tbl_r = ra_get_reg(rc, loc, s1, RA_SCRATCH); rc.code.push_back(rv_ADDI(10, tbl_r, 0)); // a0 = tbl_idx uint8_t key_r = ra_get_reg(rc, loc, s2, 28); rc.code.push_back(rv_ADDI(11, key_r, 0)); // a1 = key uint8_t val_r = ra_get_reg(rc, loc, s3, 29); rc.code.push_back(rv_ADDI(12, val_r, 0)); // a2 = value rc.code.push_back(rv_ADDI(17, 0, static_cast(ECALL_LUA_SETI_INT))); rc.code.push_back(rv_ECALL()); break; } case HIR_LUA_ALOAD: { // Native array load: result = *(int64*)(base + (key-1)*8) // src1 = key (TY_INT), val = guest base address int s1 = h.src1[i]; uint8_t reg = int_alloc.reg[i]; bool spilled = (reg == 0 && int_alloc.spill_slot[i] >= 0); uint8_t dest = spilled ? RA_SCRATCH : reg; if (!dest) break; uint8_t key_r = ra_get_reg(rc, loc, s1, RA_SCRATCH2); uint64_t base_addr = static_cast(h.val[i]); // t0 = key - 1 (0-based index) rc.code.push_back(rv_ADDI(5, key_r, -1)); // t0 = t0 << 3 (multiply by 8) rc.code.push_back(rv_SLLI(5, 5, 3)); // Load base address into t1 rv_load_val(rc.code, 6, base_addr); // t0 = base + offset rc.code.push_back(rv_ADD(5, 5, 6)); // dest = *(int64*)t0 rc.code.push_back(rv_LD(dest, 5, 0)); ra_set_loc(rc, loc, int_alloc, i, dest); h.native_ops++; break; } case HIR_ITOA: { int s1 = h.src1[i]; uint8_t s1r = ra_get_reg(rc, loc, s1, RA_SCRATCH); uint64_t out_addr = loc[i].addr; rv_load_guest_addr(rc.code, 10, out_addr); rv_emit_itoa(rc.code, s1r, 10); break; } case HIR_ADD: { int s1 = h.src1[i], s2 = h.src2[i]; uint8_t r1 = ra_get_reg(rc, loc, s1, RA_SCRATCH); uint8_t r2 = ra_get_reg(rc, loc, s2, RA_SCRATCH2); uint8_t reg = int_alloc.reg[i]; bool spilled = (reg == 0 && int_alloc.spill_slot[i] >= 0); uint8_t dest = spilled ? RA_SCRATCH : reg; if (!dest) break; rc.code.push_back(rv_ADD(dest, r1, r2)); ra_set_loc(rc, loc, int_alloc, i, dest); break; } case HIR_SUB: { int s1 = h.src1[i], s2 = h.src2[i]; uint8_t r1 = ra_get_reg(rc, loc, s1, RA_SCRATCH); uint8_t r2 = ra_get_reg(rc, loc, s2, RA_SCRATCH2); uint8_t reg = int_alloc.reg[i]; bool spilled = (reg == 0 && int_alloc.spill_slot[i] >= 0); uint8_t dest = spilled ? RA_SCRATCH : reg; if (!dest) break; rc.code.push_back(rv_SUB(dest, r1, r2)); ra_set_loc(rc, loc, int_alloc, i, dest); break; } case HIR_MUL: { int s1 = h.src1[i], s2 = h.src2[i]; uint8_t r1 = ra_get_reg(rc, loc, s1, RA_SCRATCH); uint8_t r2 = ra_get_reg(rc, loc, s2, RA_SCRATCH2); uint8_t reg = int_alloc.reg[i]; bool spilled = (reg == 0 && int_alloc.spill_slot[i] >= 0); uint8_t dest = spilled ? RA_SCRATCH : reg; if (!dest) break; rc.code.push_back(rv_MUL(dest, r1, r2)); ra_set_loc(rc, loc, int_alloc, i, dest); break; } case HIR_REM: { int s1 = h.src1[i], s2 = h.src2[i]; uint8_t r1 = ra_get_reg(rc, loc, s1, RA_SCRATCH); uint8_t r2 = ra_get_reg(rc, loc, s2, RA_SCRATCH2); uint8_t reg = int_alloc.reg[i]; bool spilled = (reg == 0 && int_alloc.spill_slot[i] >= 0); uint8_t dest = spilled ? RA_SCRATCH : reg; if (!dest) break; rc.code.push_back(rv_REM(dest, r1, r2)); ra_set_loc(rc, loc, int_alloc, i, dest); break; } case HIR_DIV: { int s1 = h.src1[i], s2 = h.src2[i]; uint8_t r1 = ra_get_reg(rc, loc, s1, RA_SCRATCH); uint8_t r2 = ra_get_reg(rc, loc, s2, RA_SCRATCH2); uint8_t reg = int_alloc.reg[i]; bool spilled = (reg == 0 && int_alloc.spill_slot[i] >= 0); uint8_t dest = spilled ? RA_SCRATCH : reg; if (!dest) break; rc.code.push_back(rv_DIV(dest, r1, r2)); ra_set_loc(rc, loc, int_alloc, i, dest); break; } // Bitwise operations. #define BITOP_RR(RV_INSN) \ { \ int s1 = h.src1[i], s2 = h.src2[i]; \ uint8_t r1 = ra_get_reg(rc, loc, s1, RA_SCRATCH); \ uint8_t r2 = ra_get_reg(rc, loc, s2, RA_SCRATCH2); \ uint8_t reg = int_alloc.reg[i]; \ bool spilled = (reg == 0 && int_alloc.spill_slot[i] >= 0); \ uint8_t dest = spilled ? RA_SCRATCH : reg; \ if (!dest) break; \ rc.code.push_back(RV_INSN(dest, r1, r2)); \ ra_set_loc(rc, loc, int_alloc, i, dest); \ break; \ } case HIR_BAND: BITOP_RR(rv_AND) case HIR_BOR: BITOP_RR(rv_OR) case HIR_BXOR: BITOP_RR(rv_XOR) case HIR_SHL: BITOP_RR(rv_SLL) case HIR_SHR: BITOP_RR(rv_SRL) #undef BITOP_RR case HIR_BNOT: { int s1 = h.src1[i]; uint8_t r1 = ra_get_reg(rc, loc, s1, RA_SCRATCH); uint8_t reg = int_alloc.reg[i]; bool spilled = (reg == 0 && int_alloc.spill_slot[i] >= 0); uint8_t dest = spilled ? RA_SCRATCH : reg; if (!dest) break; // XORI rd, rs, -1 (all-ones immediate = bitwise NOT) rc.code.push_back(rv_i_type(OP_IMM, dest, ALU_XORI, r1, -1)); ra_set_loc(rc, loc, int_alloc, i, dest); break; } // NEG: integer negate as SUB dest, x0, rs. // Two's-complement wrap: -INT64_MIN stays INT64_MIN (matches // RV64 SUB and the const-fold guard in hir_opt). (#1258) case HIR_NEG: { int s1 = h.src1[i]; uint8_t r1 = ra_get_reg(rc, loc, s1, RA_SCRATCH); uint8_t reg = int_alloc.reg[i]; bool spilled = (reg == 0 && int_alloc.spill_slot[i] >= 0); uint8_t dest = spilled ? RA_SCRATCH : reg; if (!dest) break; rc.code.push_back(rv_SUB(dest, 0, r1)); // dest = 0 - r1 ra_set_loc(rc, loc, int_alloc, i, dest); break; } // SIGN: returns -1, 0, or 1. // SLT t0, rs, x0 (t0 = 1 if rs < 0) // SLT dest, x0, rs (dest = 1 if rs > 0, i.e., 0 < rs) // SUB dest, dest, t0 case HIR_SIGN: { int s1 = h.src1[i]; uint8_t r1 = ra_get_reg(rc, loc, s1, RA_SCRATCH); uint8_t reg = int_alloc.reg[i]; bool spilled = (reg == 0 && int_alloc.spill_slot[i] >= 0); uint8_t dest = spilled ? RA_SCRATCH : reg; if (!dest) break; constexpr uint8_t t0 = 5; rc.code.push_back(rv_r_type(OP_REG, t0, ALU_SLT, r1, 0, 0)); rc.code.push_back(rv_r_type(OP_REG, dest, ALU_SLT, 0, r1, 0)); rc.code.push_back(rv_SUB(dest, dest, t0)); ra_set_loc(rc, loc, int_alloc, i, dest); break; } // MAX: max(a, b) — branchless via SLT + conditional select. // SLT t0, r1, r2 (t0 = 1 if r1 < r2) // BEQ t0, x0, +8 (skip if r1 >= r2, i.e., r1 is already max) // MV dest, r2 (r2 is larger) // Otherwise dest = r1. // Actually simpler: compute both, select. // SUB t0, r1, r2 // SRA t0, t0, 63 (sign mask: all 1s if r1 < r2) // AND t0, t0, SUB → use the mask to select // Better: just branch. // BLT r1, r2, +12; MV dest, r1; JAL x0, +8; MV dest, r2 case HIR_MAX: { int s1 = h.src1[i], s2 = h.src2[i]; uint8_t r1 = ra_get_reg(rc, loc, s1, RA_SCRATCH); uint8_t r2 = ra_get_reg(rc, loc, s2, RA_SCRATCH2); uint8_t reg = int_alloc.reg[i]; bool spilled = (reg == 0 && int_alloc.spill_slot[i] >= 0); uint8_t dest = spilled ? RA_SCRATCH : reg; if (!dest) break; // BGE r1, r2, +12 (skip to dest=r1 case when r1 >= r2) rc.code.push_back(rv_BGE(r1, r2, 12)); // r1 < r2: dest = r2 rc.code.push_back(rv_ADD(dest, r2, 0)); // MV dest, r2 rc.code.push_back(rv_JAL(0, 8)); // skip next // r1 >= r2: dest = r1 rc.code.push_back(rv_ADD(dest, r1, 0)); // MV dest, r1 ra_set_loc(rc, loc, int_alloc, i, dest); break; } // MIN: min(a, b) — mirror of MAX. case HIR_MIN: { int s1 = h.src1[i], s2 = h.src2[i]; uint8_t r1 = ra_get_reg(rc, loc, s1, RA_SCRATCH); uint8_t r2 = ra_get_reg(rc, loc, s2, RA_SCRATCH2); uint8_t reg = int_alloc.reg[i]; bool spilled = (reg == 0 && int_alloc.spill_slot[i] >= 0); uint8_t dest = spilled ? RA_SCRATCH : reg; if (!dest) break; // BLT r1, r2, +12 (skip to dest=r1 case when r1 < r2) rc.code.push_back(rv_b_type(BR_BLT, r1, r2, 12)); // r1 >= r2: dest = r2 rc.code.push_back(rv_ADD(dest, r2, 0)); // MV dest, r2 rc.code.push_back(rv_JAL(0, 8)); // skip next // r1 < r2: dest = r1 rc.code.push_back(rv_ADD(dest, r1, 0)); // MV dest, r1 ra_set_loc(rc, loc, int_alloc, i, dest); break; } case HIR_EQ: { int s1 = h.src1[i], s2 = h.src2[i]; uint8_t r1 = ra_get_reg(rc, loc, s1, RA_SCRATCH); uint8_t r2 = ra_get_reg(rc, loc, s2, RA_SCRATCH2); uint8_t reg = int_alloc.reg[i]; bool spilled = (reg == 0 && int_alloc.spill_slot[i] >= 0); uint8_t dest = spilled ? RA_SCRATCH : reg; if (!dest) break; rc.code.push_back(rv_SUB(5, r1, r2)); rc.code.push_back(rv_i_type(OP_IMM, dest, ALU_SLTIU, 5, 1)); ra_set_loc(rc, loc, int_alloc, i, dest); break; } case HIR_NE: { int s1 = h.src1[i], s2 = h.src2[i]; uint8_t r1 = ra_get_reg(rc, loc, s1, RA_SCRATCH); uint8_t r2 = ra_get_reg(rc, loc, s2, RA_SCRATCH2); uint8_t reg = int_alloc.reg[i]; bool spilled = (reg == 0 && int_alloc.spill_slot[i] >= 0); uint8_t dest = spilled ? RA_SCRATCH : reg; if (!dest) break; rc.code.push_back(rv_SUB(5, r1, r2)); rc.code.push_back(rv_r_type(OP_REG, dest, ALU_SLTU, 0, 5, 0)); ra_set_loc(rc, loc, int_alloc, i, dest); break; } case HIR_GT: { int s1 = h.src1[i], s2 = h.src2[i]; uint8_t r1 = ra_get_reg(rc, loc, s1, RA_SCRATCH); uint8_t r2 = ra_get_reg(rc, loc, s2, RA_SCRATCH2); uint8_t reg = int_alloc.reg[i]; bool spilled = (reg == 0 && int_alloc.spill_slot[i] >= 0); uint8_t dest = spilled ? RA_SCRATCH : reg; if (!dest) break; rc.code.push_back(rv_r_type(OP_REG, dest, ALU_SLT, r2, r1, 0)); ra_set_loc(rc, loc, int_alloc, i, dest); break; } case HIR_LT: { int s1 = h.src1[i], s2 = h.src2[i]; uint8_t r1 = ra_get_reg(rc, loc, s1, RA_SCRATCH); uint8_t r2 = ra_get_reg(rc, loc, s2, RA_SCRATCH2); uint8_t reg = int_alloc.reg[i]; bool spilled = (reg == 0 && int_alloc.spill_slot[i] >= 0); uint8_t dest = spilled ? RA_SCRATCH : reg; if (!dest) break; rc.code.push_back(rv_r_type(OP_REG, dest, ALU_SLT, r1, r2, 0)); ra_set_loc(rc, loc, int_alloc, i, dest); break; } case HIR_GE: { int s1 = h.src1[i], s2 = h.src2[i]; uint8_t r1 = ra_get_reg(rc, loc, s1, RA_SCRATCH); uint8_t r2 = ra_get_reg(rc, loc, s2, RA_SCRATCH2); uint8_t reg = int_alloc.reg[i]; bool spilled = (reg == 0 && int_alloc.spill_slot[i] >= 0); uint8_t dest = spilled ? RA_SCRATCH : reg; if (!dest) break; rc.code.push_back(rv_r_type(OP_REG, dest, ALU_SLT, r1, r2, 0)); rc.code.push_back(rv_i_type(OP_IMM, dest, ALU_XORI, dest, 1)); ra_set_loc(rc, loc, int_alloc, i, dest); break; } case HIR_LE: { int s1 = h.src1[i], s2 = h.src2[i]; uint8_t r1 = ra_get_reg(rc, loc, s1, RA_SCRATCH); uint8_t r2 = ra_get_reg(rc, loc, s2, RA_SCRATCH2); uint8_t reg = int_alloc.reg[i]; bool spilled = (reg == 0 && int_alloc.spill_slot[i] >= 0); uint8_t dest = spilled ? RA_SCRATCH : reg; if (!dest) break; rc.code.push_back(rv_r_type(OP_REG, dest, ALU_SLT, r2, r1, 0)); rc.code.push_back(rv_i_type(OP_IMM, dest, ALU_XORI, dest, 1)); ra_set_loc(rc, loc, int_alloc, i, dest); break; } case HIR_NOT: { int s1 = h.src1[i]; uint8_t r1 = ra_get_reg(rc, loc, s1, RA_SCRATCH); uint8_t reg = int_alloc.reg[i]; bool spilled = (reg == 0 && int_alloc.spill_slot[i] >= 0); uint8_t dest = spilled ? RA_SCRATCH : reg; if (!dest) break; rc.code.push_back(rv_i_type(OP_IMM, dest, ALU_SLTIU, r1, 1)); ra_set_loc(rc, loc, int_alloc, i, dest); break; } // BOOL (t function): SNEZ — set if not equal to zero. // SLTU dest, x0, r1 → dest = (0 < r1) unsigned = (r1 != 0) case HIR_BOOL: { int s1 = h.src1[i]; uint8_t r1 = ra_get_reg(rc, loc, s1, RA_SCRATCH); uint8_t reg = int_alloc.reg[i]; bool spilled = (reg == 0 && int_alloc.spill_slot[i] >= 0); uint8_t dest = spilled ? RA_SCRATCH : reg; if (!dest) break; rc.code.push_back(rv_r_type(OP_REG, dest, ALU_SLTU, 0, r1, 0)); ra_set_loc(rc, loc, int_alloc, i, dest); break; } case HIR_INC: { int s1 = h.src1[i]; uint8_t r1 = ra_get_reg(rc, loc, s1, RA_SCRATCH); uint8_t reg = int_alloc.reg[i]; bool spilled = (reg == 0 && int_alloc.spill_slot[i] >= 0); uint8_t dest = spilled ? RA_SCRATCH : reg; if (!dest) break; rc.code.push_back(rv_ADDI(dest, r1, 1)); ra_set_loc(rc, loc, int_alloc, i, dest); break; } case HIR_DEC: { int s1 = h.src1[i]; uint8_t r1 = ra_get_reg(rc, loc, s1, RA_SCRATCH); uint8_t reg = int_alloc.reg[i]; bool spilled = (reg == 0 && int_alloc.spill_slot[i] >= 0); uint8_t dest = spilled ? RA_SCRATCH : reg; if (!dest) break; rc.code.push_back(rv_ADDI(dest, r1, -1)); ra_set_loc(rc, loc, int_alloc, i, dest); break; } // ---- Float arithmetic (RV64D) ---- // // FP values are spilled to guest memory (8-byte aligned). // We load into f0/f1, compute into f0, store result. // The DBT's x86-64 translator handles the rest. case HIR_FCONST: { // Write the double constant into guest memory at the // allocated FP slot, then no codegen needed — the value // is already there for subsequent FLD instructions. uint64_t addr = loc[i].addr; double v = h.fval[i]; memcpy(rc.memory.data() + addr, &v, 8); break; } #define FP_BINOP(RV_INSN) \ { \ uint64_t a1 = loc[h.src1[i]].addr; \ uint64_t a2 = loc[h.src2[i]].addr; \ uint64_t dst = loc[i].addr; \ rv_load_guest_addr(rc.code, RA_SCRATCH, a1); \ rc.code.push_back(rv_FLD(0, RA_SCRATCH, 0)); \ rv_load_guest_addr(rc.code, RA_SCRATCH, a2); \ rc.code.push_back(rv_FLD(1, RA_SCRATCH, 0)); \ rc.code.push_back(RV_INSN(0, 0, 1)); \ rv_load_guest_addr(rc.code, RA_SCRATCH, dst); \ rc.code.push_back(rv_FSD(RA_SCRATCH, 0, 0)); \ break; \ } case HIR_FADD: FP_BINOP(rv_FADD_D) case HIR_FSUB: FP_BINOP(rv_FSUB_D) case HIR_FMUL: FP_BINOP(rv_FMUL_D) case HIR_FDIV: FP_BINOP(rv_FDIV_D) #undef FP_BINOP case HIR_FNEG: { uint64_t a1 = loc[h.src1[i]].addr; uint64_t dst = loc[i].addr; rv_load_guest_addr(rc.code, RA_SCRATCH, a1); rc.code.push_back(rv_FLD(0, RA_SCRATCH, 0)); rc.code.push_back(rv_FNEG_D(0, 0)); rv_load_guest_addr(rc.code, RA_SCRATCH, dst); rc.code.push_back(rv_FSD(RA_SCRATCH, 0, 0)); break; } case HIR_FSQRT: { uint64_t a1 = loc[h.src1[i]].addr; uint64_t dst = loc[i].addr; rv_load_guest_addr(rc.code, RA_SCRATCH, a1); rc.code.push_back(rv_FLD(0, RA_SCRATCH, 0)); rc.code.push_back(rv_FSQRT_D(0, 0)); rv_load_guest_addr(rc.code, RA_SCRATCH, dst); rc.code.push_back(rv_FSD(RA_SCRATCH, 0, 0)); break; } // ITOF: int64 → double. Load int reg, FCVT.D.L, store to FP slot. case HIR_ITOF: { uint8_t r1 = ra_get_reg(rc, loc, h.src1[i], RA_SCRATCH); uint64_t dst = loc[i].addr; rc.code.push_back(rv_FCVT_D_L(0, r1)); rv_load_guest_addr(rc.code, RA_SCRATCH, dst); rc.code.push_back(rv_FSD(RA_SCRATCH, 0, 0)); break; } // FTOI: double → int64 (truncate toward zero). case HIR_FTOI: { uint64_t a1 = loc[h.src1[i]].addr; rv_load_guest_addr(rc.code, RA_SCRATCH, a1); rc.code.push_back(rv_FLD(0, RA_SCRATCH, 0)); uint8_t reg = int_alloc.reg[i]; bool spilled = (reg == 0 && int_alloc.spill_slot[i] >= 0); uint8_t dest = spilled ? RA_SCRATCH : reg; if (!dest) break; rc.code.push_back(rv_FCVT_L_D(dest, 0)); ra_set_loc(rc, loc, int_alloc, i, dest); break; } // FTOA: double → string. Use ECALL to format. // HIR_LUA_FTOA is the same sequence against a host formatter // that follows Lua's tostring rules instead of MUX's (#1488). case HIR_FTOA: case HIR_LUA_FTOA: { uint64_t a1 = loc[h.src1[i]].addr; uint64_t out_addr = loc[i].addr; // Load double bits into a0 via FMV.X.D. rv_load_guest_addr(rc.code, RA_SCRATCH, a1); rc.code.push_back(rv_FLD(0, RA_SCRATCH, 0)); rc.code.push_back(rv_FMV_X_D(10, 0)); // a0 = double bits rv_load_guest_addr(rc.code, 11, out_addr); // a1 = output buffer rv_load_val(rc.code, 17, (h.kind[i] == HIR_LUA_FTOA) ? ECALL_LUA_FTOA : ECALL_FTOA); rc.code.push_back(rv_ECALL()); break; } // ATOF: string → double. // Fast path: JAL to blob rv64_strtod (DBT intercepts as // native intrinsic). Fallback: ECALL_ATOF. case HIR_ATOF: { uint64_t str_addr = loc[h.src1[i]].addr; uint64_t dst = loc[i].addr; uint64_t blob_addr = static_cast(h.val[i]); rv_load_guest_addr(rc.code, 10, str_addr); // a0 = string if (blob_addr) { // JAL to rv64_strtod — result in fa0. uint64_t pc = rc.current_pc(); int32_t offset = static_cast(blob_addr - pc); rv_push_jal(rc, 1, offset); } else { // ECALL fallback. rv_load_val(rc.code, 17, 0x141); // a7 = ECALL_ATOF rc.code.push_back(rv_ECALL()); } // Result in fa0 (f10). Store to FP slot. rv_load_guest_addr(rc.code, RA_SCRATCH, dst); rc.code.push_back(rv_FSD(RA_SCRATCH, 10, 0)); // *dst = fa0 break; } // FCALL1: unary FP intrinsic call (sin, cos, etc.). // arg in fa0, result in fa0. JAL to blob stub. case HIR_FCALL1: { uint64_t a1 = loc[h.src1[i]].addr; uint64_t dst = loc[i].addr; uint64_t func_addr = static_cast(h.val[i]); // Load argument into fa0 (f10). rv_load_guest_addr(rc.code, RA_SCRATCH, a1); rc.code.push_back(rv_FLD(10, RA_SCRATCH, 0)); // fa0 = *a1 // JAL to blob function. uint64_t pc = rc.current_pc(); int32_t offset = static_cast(func_addr - pc); rv_push_jal(rc, 1, offset); // JAL ra, func // Store result from fa0 to FP slot. rv_load_guest_addr(rc.code, RA_SCRATCH, dst); rc.code.push_back(rv_FSD(RA_SCRATCH, 10, 0)); // *dst = fa0 break; } // FCALL2: binary FP intrinsic call (pow, atan2, fmod). // args in fa0, fa1; result in fa0. case HIR_FCALL2: { uint64_t a1 = loc[h.src1[i]].addr; uint64_t a2 = loc[h.src2[i]].addr; uint64_t dst = loc[i].addr; uint64_t func_addr = static_cast(h.val[i]); // Load arguments into fa0, fa1. rv_load_guest_addr(rc.code, RA_SCRATCH, a1); rc.code.push_back(rv_FLD(10, RA_SCRATCH, 0)); // fa0 = *a1 rv_load_guest_addr(rc.code, RA_SCRATCH, a2); rc.code.push_back(rv_FLD(11, RA_SCRATCH, 0)); // fa1 = *a2 // JAL to blob function. uint64_t pc = rc.current_pc(); int32_t offset = static_cast(func_addr - pc); rv_push_jal(rc, 1, offset); // JAL ra, func // Store result from fa0 to FP slot. rv_load_guest_addr(rc.code, RA_SCRATCH, dst); rc.code.push_back(rv_FSD(RA_SCRATCH, 10, 0)); // *dst = fa0 break; } // Float comparisons: result is integer 0/1. #define FP_CMP(RV_INSN) \ { \ uint64_t a1 = loc[h.src1[i]].addr; \ uint64_t a2 = loc[h.src2[i]].addr; \ rv_load_guest_addr(rc.code, RA_SCRATCH, a1); \ rc.code.push_back(rv_FLD(0, RA_SCRATCH, 0)); \ rv_load_guest_addr(rc.code, RA_SCRATCH, a2); \ rc.code.push_back(rv_FLD(1, RA_SCRATCH, 0)); \ uint8_t reg = int_alloc.reg[i]; \ bool spilled = (reg == 0 && int_alloc.spill_slot[i] >= 0); \ uint8_t dest = spilled ? RA_SCRATCH : reg; \ if (!dest) break; \ rc.code.push_back(RV_INSN(dest, 0, 1)); \ ra_set_loc(rc, loc, int_alloc, i, dest); \ break; \ } case HIR_FEQ: FP_CMP(rv_FEQ_D) case HIR_FLT: FP_CMP(rv_FLT_D) case HIR_FLE: FP_CMP(rv_FLE_D) #undef FP_CMP case HIR_CALL: { uint64_t out_addr = loc[i].addr; int na = h.cnargs[i]; int base = h.cbase[i]; std::vector farg_addrs; for (int j = 0; j < na; j++) { int ai = h.carg[base + j]; farg_addrs.push_back(loc[ai].addr); } uint64_t fargs_addr = rc.alloc_fargs(farg_addrs); if (h.tier2_addr[i]) { // Tier 2: JAL to pre-compiled blob function. // Patch any frame-relative fargs at runtime. rv_patch_fargs(rc.code, fargs_addr, farg_addrs); rv_emit_tier2_call(rc, fargs_addr, na, out_addr, h.tier2_addr[i]); } else { // ECALL to engine function. int fidx = h.func_idx[i]; uint64_t name_addr = 0; if (fidx == 0 && !h.call_name[i].empty()) { name_addr = rc.pool_str(h.call_name[i]); } rv_emit_call(rc.code, name_addr, fargs_addr, na, out_addr, rv_compiler::OUT_SLOT, fidx); } break; } case HIR_CALL_T2I: { // Integer-ABI tier-2 call (#2132): args straight into // a0.., JAL, result back from a0. No fargs array, no // decimal marshalling. RA values live in s-regs, so the // moves below cannot clobber their sources, and the JAL // clobbers only a*/t* — the same contract every string // tier-2 call already relies on. int na = h.cnargs[i]; int base = h.cbase[i]; uint8_t areg = 10; // a0 if (h.val[i]) { // Output slot rides ahead of the declared args. rv_load_guest_addr(rc.code, areg++, loc[i].addr); } for (int j = 0; j < na; j++) { int ai = h.carg[base + j]; if (h.ty[ai] == TY_INT) { // From its register (reloading a spill into the // scratch is fine: the value is moved into its // a-reg before the next argument touches it). uint8_t r = ra_get_reg(rc, loc, ai, RA_SCRATCH); rc.code.push_back(rv_ADD(areg, r, 0)); } else { rv_load_guest_addr(rc.code, areg, loc[ai].addr); } areg++; } uint64_t cur_pc = rc.current_pc(); int32_t offset = static_cast(h.tier2_addr[i] - cur_pc); rv_push_jal(rc, 1, offset); // JAL ra, blob uint8_t reg = int_alloc.reg[i]; bool spilled = (reg == 0 && int_alloc.spill_slot[i] >= 0); uint8_t dest = spilled ? RA_SCRATCH : reg; if (dest) { rc.code.push_back(rv_ADD(dest, 10, 0)); // mv dest, a0 ra_set_loc(rc, loc, int_alloc, i, dest); } break; } case HIR_T2I_STR: { // Alias of the call's output slot; no code. int s1 = h.src1[i]; if (s1 >= 0) { loc[i].addr = loc[s1].addr; loc[i].in_reg = false; } break; } case HIR_STRCAT: { uint64_t out_addr = loc[i].addr; int na = h.cnargs[i]; int base = h.cbase[i]; std::vector farg_addrs; for (int j = 0; j < na; j++) { int ai = h.carg[base + j]; farg_addrs.push_back(loc[ai].addr); } uint64_t fargs_addr = rc.alloc_fargs(farg_addrs); uint64_t t2addr = tier2_lookup("STRCAT"); if (t2addr) { rv_patch_fargs(rc.code, fargs_addr, farg_addrs); rv_emit_tier2_call(rc, fargs_addr, na, out_addr, t2addr); } else { int fidx = h.func_idx[i]; uint64_t name_addr = fidx ? 0 : rc.pool_str("strcat"); rv_emit_call(rc.code, name_addr, fargs_addr, na, out_addr, rv_compiler::OUT_SLOT, fidx); } break; } case HIR_COPY: { int s1 = h.src1[i]; if (s1 < 0) break; if (needs_int_reg(h, i)) { uint8_t r1 = ra_get_reg(rc, loc, s1, RA_SCRATCH2); uint8_t reg = int_alloc.reg[i]; bool spilled = (reg == 0 && int_alloc.spill_slot[i] >= 0); uint8_t dest = spilled ? RA_SCRATCH : reg; if (!dest) { loc[i] = loc[s1]; break; } rc.code.push_back(rv_ADD(dest, r1, 0)); ra_set_loc(rc, loc, int_alloc, i, dest); } else { loc[i] = loc[s1]; } break; } case HIR_PHI: // Location already allocated above. break; case HIR_BRC: { // Conditional branch: if cond != 0, go to true_blk. int cond_insn = h.src1[i]; int true_blk = static_cast(h.val[i]); int false_blk = h.src2[i]; uint8_t cond_reg = ra_get_reg(rc, loc, cond_insn, RA_SCRATCH); // Emit PHI copies for true path, then BNE. emit_phi_copies(h, rc, loc, b, true_blk); int bne_idx = static_cast(rc.code.size()); rc.code.push_back(rv_BNE(cond_reg, 0, 0)); patches.push_back({bne_idx, true_blk}); // Emit PHI copies for false path. emit_phi_copies(h, rc, loc, b, false_blk); // If false block is not the next in layout, emit JAL. if (false_blk != b + 1) { int jal_idx = static_cast(rc.code.size()); rc.code.push_back(rv_JAL(0, 0)); patches.push_back({jal_idx, false_blk}); } break; } case HIR_BR: { int target = static_cast(h.val[i]); // Emit PHI copies for target. emit_phi_copies(h, rc, loc, b, target); // If target is not the next block, emit JAL. if (target != b + 1) { int jal_idx = static_cast(rc.code.size()); rc.code.push_back(rv_JAL(0, 0)); patches.push_back({jal_idx, target}); } break; } case HIR_RET: { // Materialize this return's value into the shared slot before // exiting, so the value follows the path actually taken // (#1486). Mirrors the string-PHI copy above. int rv = h.src1[i]; if (rc.ret_out != 0 && rv >= 0) { if (loc[rv].in_reg) { rv_load_guest_addr(rc.code, 10, rc.ret_out); rv_emit_itoa(rc.code, loc[rv].reg, 10); } else if (loc[rv].spill_slot >= 0) { emit_spill_load(rc.code, RA_SCRATCH, loc[rv].spill_slot); rv_load_guest_addr(rc.code, 10, rc.ret_out); rv_emit_itoa(rc.code, RA_SCRATCH, 10); } else { rv_load_guest_addr(rc.code, 7, rc.ret_out); // t2 = dest rv_load_guest_addr(rc.code, 6, loc[rv].addr); // t1 = src rv_emit_strcpy(rc.code, 7, 6); } // The answer is now produced by executing this code, so // the program can no longer be served by a compile-time // fold. rc.needs_jit = true; } rv_emit_exit(rc.code); break; } case HIR_SETQ_SYNC: { // Emit ECALL_SETQ_PACK: a0 = reg_num, a1 = value_addr, a2 = length. // We pass 0 for length to tell the host to use strlen() for now. int regnum = static_cast(h.val[i]); int val_idx = h.src1[i]; rc.code.push_back(rv_ADDI(17, 0, 0x130)); // a7 = ECALL_SETQ_PACK rv_load_val(rc.code, 10, static_cast(regnum)); // a0 = regnum if (val_idx >= 0) { rv_load_guest_addr(rc.code, 11, loc[val_idx].addr); // a1 = value addr } else { rv_load_val(rc.code, 11, 0); } rv_load_val(rc.code, 12, 0); // a2 = 0 (use strlen) rc.code.push_back(rv_ECALL()); break; } // Loop-context table maintenance (#2171). All table // addresses are compile-time constants; the DBT maps the // guest addresses at store time like every other SD. case HIR_LCTX_DEPTH: { rv_load_val(rc.code, RA_SCRATCH2, static_cast(h.val[i])); rv_load_guest_addr(rc.code, RA_SCRATCH, rv_compiler::LOOPCTX_BASE); rc.code.push_back(rv_SD(RA_SCRATCH, RA_SCRATCH2, 0)); break; } case HIR_LCTX_ELEM: { int v = h.src1[i]; if (v < 0) break; uint64_t slot = rv_compiler::LOOPCTX_BASE + (1 + 2 * static_cast(h.val[i])) * 8; // The element's slot ADDRESS is the payload — the host // dereferences it at ECALL time to read the current // iteration's element. It can be an output-frame // reference, so resolve it (rv_load_guest_addr) rather // than storing the tagged constant raw: the tag bits // would read as an out-of-range guest address. rv_load_guest_addr(rc.code, RA_SCRATCH2, loc[v].addr); rv_load_guest_addr(rc.code, RA_SCRATCH, slot); rc.code.push_back(rv_SD(RA_SCRATCH, RA_SCRATCH2, 0)); break; } case HIR_LCTX_INUM: { int v = h.src1[i]; if (v < 0) break; uint64_t slot = rv_compiler::LOOPCTX_BASE + (2 + 2 * static_cast(h.val[i])) * 8; uint8_t r; if (loc[v].in_reg) { r = loc[v].reg; } else if (loc[v].spill_slot >= 0) { emit_spill_load(rc.code, RA_SCRATCH2, loc[v].spill_slot); r = RA_SCRATCH2; } else { break; } rv_load_guest_addr(rc.code, RA_SCRATCH, slot); rc.code.push_back(rv_SD(RA_SCRATCH, r, 0)); break; } case HIR_LCTX_KEEP: // interval-only: no code (see hir.h) case HIR_NOP: case HIR_STORE_Q: // consumed by SSA construction case HIR_LOAD_Q: // should be COPY after SSA; harmless NOP break; default: break; } } } // Backpatch branch offsets. for (auto &p : patches) { int target_off = block_offset[p.target_blk]; int branch_off = p.code_idx; int32_t rel = static_cast((target_off - branch_off) * 4); uint32_t insn = rc.code[branch_off]; uint8_t opcode = insn & 0x7F; if (opcode == OP_BRANCH) { // B-type: re-encode with correct offset. Bail if the branch // target is beyond the 13-bit signed reach rather than emit a // truncated (wrong) offset. if (!rv_branch_offset_ok(rel)) { rc.out_exhausted = true; } uint8_t funct3 = (insn >> 12) & 7; uint8_t rs1 = (insn >> 15) & 0x1F; uint8_t rs2 = (insn >> 20) & 0x1F; rc.code[branch_off] = rv_b_type(funct3, rs1, rs2, rel); } else if (opcode == OP_JAL) { // J-type: re-encode with correct offset. Bail if beyond the // 21-bit signed reach. if (!rv_jal_offset_ok(rel)) { rc.out_exhausted = true; } uint8_t rd = (insn >> 7) & 0x1F; rc.code[branch_off] = rv_JAL(rd, rel); } } // Set the result location in the rv_compiler. When the returns write to // a shared slot, that slot is the answer and h.result -- which names only // the first return site -- must not override it (#1486). int ri = h.result; if (rc.ret_out != 0) { rc.final_out = rc.ret_out; } else if (ri >= 0) { if (loc[ri].in_reg) { if (!rc.needs_jit && h.kind[ri] == HIR_ICONST) { // Constant integer with no runtime code — convert to // string at compile time instead of emitting ITOA. // This keeps the result in the string pool (low memory) // so it survives SQLite cache persistence. char buf[32]; mux_snprintf(reinterpret_cast(buf), sizeof(buf), T("%lld"), static_cast(h.val[ri])); uint64_t addr = rc.pool_str(buf, strlen(buf)); rc.final_out = addr; } else { // Final result is in a register — need ITOA at runtime. uint64_t out_addr = rc.alloc_output(); rv_load_guest_addr(rc.code, 10, out_addr); rv_emit_itoa(rc.code, loc[ri].reg, 10); rc.final_out = out_addr; rc.needs_jit = true; } } else if (loc[ri].spill_slot >= 0) { // Final result is spilled — reload and ITOA. uint64_t out_addr = rc.alloc_output(); emit_spill_load(rc.code, RA_SCRATCH, loc[ri].spill_slot); rv_load_guest_addr(rc.code, 10, out_addr); rv_emit_itoa(rc.code, RA_SCRATCH, 10); rc.final_out = out_addr; rc.needs_jit = true; } else { rc.final_out = loc[ri].addr; } } // Backpatch prologue: set SP to accommodate output frame + spill area. // // The spill area sits at [SP, SP + spill_area) AFTER the SUB — inside // the frame, so callee frames (which start below SP) cannot touch it. // See spill_offset() for the failure this replaces. Rounded to 16 so // the SP alignment the callees see is unchanged from before. // // The SUB must be emitted when there are spills EVEN IF there are no // output slots: positive spill offsets address the frame the SUB // creates, and without it they would read the caller's stack. uint64_t spill_area = (static_cast(rc.spills) * 8 + 15) & ~static_cast(15); if (rc.n_output_slots > 0 || spill_area > 0) { // Frame includes 8-byte alignment pad + all output slots. uint64_t frame_size = 8 + static_cast(rc.n_output_slots) * rv_compiler::OUT_SLOT + spill_area; // LUI t0, upper20 uint32_t hi = static_cast(frame_size) & 0xFFFFF000; int32_t lo = static_cast(frame_size & 0xFFF); if (lo & 0x800) { hi += 0x1000; lo -= 0x1000; } rc.code[prologue_pos + 0] = rv_LUI(5, hi); // LUI t0, upper rc.code[prologue_pos + 1] = rv_ADDI(5, 5, lo); // ADDI t0, t0, lower rc.code[prologue_pos + 2] = rv_SUB(2, 2, 5); // SUB SP, SP, t0 } // If n_output_slots == 0, the NOPs remain (harmless). // Emit exit. rv_emit_exit(rc.code); } const char *hir_kind_name(hir_kind k) { switch (k) { case HIR_NOP: return "NOP"; case HIR_ICONST: return "ICONST"; case HIR_SCONST: return "SCONST"; case HIR_ADD: return "ADD"; case HIR_SUB: return "SUB"; case HIR_MUL: return "MUL"; case HIR_DIV: return "DIV"; case HIR_REM: return "REM"; case HIR_NEG: return "NEG"; case HIR_SIGN: return "SIGN"; case HIR_MAX: return "MAX"; case HIR_MIN: return "MIN"; case HIR_BAND: return "BAND"; case HIR_BOR: return "BOR"; case HIR_BXOR: return "BXOR"; case HIR_BNOT: return "BNOT"; case HIR_SHL: return "SHL"; case HIR_SHR: return "SHR"; case HIR_EQ: return "EQ"; case HIR_NE: return "NE"; case HIR_LT: return "LT"; case HIR_LE: return "LE"; case HIR_GT: return "GT"; case HIR_GE: return "GE"; case HIR_NOT: return "NOT"; case HIR_BOOL: return "BOOL"; case HIR_INC: return "INC"; case HIR_DEC: return "DEC"; case HIR_ATOI: return "ATOI"; case HIR_STRCMP: return "STRCMP"; case HIR_LUA_NEWTABLE: return "LUA_NEWTABLE"; case HIR_LUA_LEN: return "LUA_LEN"; case HIR_LUA_INSN_BUDGET: return "LUA_INSN_BUDGET"; case HIR_LUA_GETGLOBAL: return "LUA_GETGLOBAL"; case HIR_LUA_GETFIELD_REF: return "LUA_GETFIELD_REF"; case HIR_LUA_CALL_INT: return "LUA_CALL_INT"; case HIR_LUA_CALL_STR: return "LUA_CALL_STR"; case HIR_LUA_CALL_VOID: return "LUA_CALL_VOID"; case HIR_LUA_CALL_VAL: return "LUA_CALL_VAL"; case HIR_LUA_MARSHAL: return "LUA_MARSHAL"; case HIR_LUA_TOBOOL: return "LUA_TOBOOL"; case HIR_LUA_EQ: return "LUA_EQ"; case HIR_LUA_LIMITED: return "LUA_LIMITED"; case HIR_LUA_GETFIELD: return "LUA_GETFIELD"; case HIR_LUA_GETFIELD_FLT: return "LUA_GETFIELD_FLT"; case HIR_LUA_SETFIELD: return "LUA_SETFIELD"; case HIR_LUA_GETI: return "LUA_GETI"; case HIR_LUA_SETI: return "LUA_SETI"; case HIR_LUA_ALOAD: return "LUA_ALOAD"; case HIR_ITOA: return "ITOA"; case HIR_ITOF: return "ITOF"; case HIR_FTOI: return "FTOI"; case HIR_FTOA: return "FTOA"; case HIR_LUA_FTOA: return "LUA_FTOA"; case HIR_ATOF: return "ATOF"; case HIR_FCONST: return "FCONST"; case HIR_FADD: return "FADD"; case HIR_FSUB: return "FSUB"; case HIR_FMUL: return "FMUL"; case HIR_FDIV: return "FDIV"; case HIR_FNEG: return "FNEG"; case HIR_FSQRT: return "FSQRT"; case HIR_FEQ: return "FEQ"; case HIR_FLT: return "FLT"; case HIR_FLE: return "FLE"; case HIR_CALL: return "CALL"; case HIR_CALL_T2I: return "CALL_T2I"; case HIR_T2I_STR: return "T2I_STR"; case HIR_STRCAT: return "STRCAT"; case HIR_FCALL1: return "FCALL1"; case HIR_FCALL2: return "FCALL2"; case HIR_RET: return "RET"; case HIR_COPY: return "COPY"; case HIR_PHI: return "PHI"; case HIR_LOAD_Q: return "LOAD_Q"; case HIR_STORE_Q: return "STORE_Q"; case HIR_SETQ_SYNC: return "SETQ_SYNC"; case HIR_LCTX_DEPTH: return "LCTX_DEPTH"; case HIR_LCTX_ELEM: return "LCTX_ELEM"; case HIR_LCTX_INUM: return "LCTX_INUM"; case HIR_LCTX_KEEP: return "LCTX_KEEP"; case HIR_BR: return "BR"; case HIR_BRC: return "BRC"; default: return "UNKNOWN"; } } static const char *hir_type_name(hir_type t) { switch (t) { case TY_VOID: return "void"; case TY_INT: return "int"; case TY_FLOAT: return "flt"; case TY_LUA_HANDLE: return "lhnd"; case TY_STRING: return "str"; default: return "???"; } } void hir_dump(const hir_program &h) { printf("HIR Program: %d instructions, %d blocks\n", h.n_insns, h.n_blocks); printf("Result: v%d\n", h.result); for (int b = 0; b < h.n_blocks; b++) { printf("\nBLOCK %d:\n", b); printf(" Range: [%d, %d]\n", h.block_first[b], h.block_last[b]); printf(" Preds: "); for (int i = 0; i < h.n_pred[b]; i++) { printf("%d ", h.pblk[h.pred_base[b] + i]); } printf("\n Succs: "); for (int i = 0; i < h.block_nsucc[b]; i++) { printf("%d ", h.block_succ[b][i]); } printf("\n IDom: %d\n", h.idom[b]); if (h.block_first[b] >= 0 && h.block_first[b] <= h.block_last[b] && h.block_last[b] < h.n_insns) { for (int i = h.block_first[b]; i <= h.block_last[b]; i++) { if (h.blk[i] != b) continue; printf(" v%-3d = %-10s %-4s", i, hir_kind_name(h.kind[i]), hir_type_name(h.ty[i])); if (h.kind[i] == HIR_ICONST) { printf(" %lld", (long long)h.val[i]); } else if (h.kind[i] == HIR_SCONST) { const char *sv = (i < static_cast(h.sval.size())) ? h.sval[i].c_str() : ""; printf(" \"%s\" (0x%llX)", sv, (unsigned long long)h.val[i]); } else if (h.kind[i] == HIR_BR) { printf(" -> BLOCK %d", (int)h.val[i]); } else if (h.kind[i] == HIR_BRC) { printf(" v%d ? -> BLOCK %d : BLOCK %d", h.src1[i], (int)h.val[i], h.src2[i]); } else if (h.kind[i] == HIR_PHI) { printf(" Q%d { ", (int)h.val[i]); for (int j = 0; j < h.pnargs[i]; j++) { printf("B%d:v%d ", h.pblk[h.pbase[i] + j], h.pval[h.pbase[i] + j]); } printf("}"); } else if (h.kind[i] == HIR_CALL || h.kind[i] == HIR_STRCAT) { if (i < static_cast(h.call_name.size()) && !h.call_name[i].empty()) printf(" %s", h.call_name[i].c_str()); printf(" ( "); for (int j = 0; j < h.cnargs[i]; j++) { printf("v%d ", h.carg[h.cbase[i] + j]); } printf(")"); if (h.tier2_addr[i]) printf(" [T2:0x%llX]", (unsigned long long)h.tier2_addr[i]); } else { if (h.src1[i] >= 0) printf(" v%d", h.src1[i]); if (h.src2[i] >= 0) printf(", v%d", h.src2[i]); if (h.val[i] != 0 && h.kind[i] != HIR_COPY) printf(" imm:%lld", (long long)h.val[i]); } printf("\n"); } } } printf("--- end dump ---\n"); }