From 0cfed5516c4f1c8b50853c59f58ed6ec035cf7f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Balv=C3=ADn?= Date: Wed, 7 Oct 2026 02:09:52 +0200 Subject: [PATCH] fix(asm): carry the riscv64 U-type immediate raw The toolchain writes the source immediate straight into imm[31:12] (riscv64.s: AUIPC 24287, X10 encodes 7ffff517), and rejects values beyond the signed 20-bit span; the encoder divided by 4096 instead and truncated silently, so the high bits of every large AUIPC and LUI were lost. Assisted-by: GLM 5.3 Flash --- asm/riscv_assemble.go | 10 ++++++++-- asm/riscv_encode_test.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/asm/riscv_assemble.go b/asm/riscv_assemble.go index b3f3ab3..dafd86c 100644 --- a/asm/riscv_assemble.go +++ b/asm/riscv_assemble.go @@ -1751,7 +1751,10 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv // The Go assembler never compresses branches to C.BEQZ/C.BNEZ. word = riscvBType(enc, rs1, rs2, offset) - // U-type: rd, imm (or the toolchain testdata's INSTR $imm, rd). + // U-type: rd, imm (or the toolchain testdata's INSTR $imm, rd). The + // immediate rides the field raw (riscv64.s: AUIPC $524287, X10 encodes + // 7ffff517), so it shifts into imm[31:12] here, and the span is the + // signed 20-bit range the toolchain checks. case len(ops) == 2 && isUTypeInstr(mnem): var rd int var imm int32 @@ -1763,7 +1766,10 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv if rd < 0 { return nil, fmt.Errorf("invalid register in %s", mnem) } - word = riscvUType(enc, rd, imm) + if imm < -(1<<19) || imm > (1<<19)-1 { + return nil, fmt.Errorf("%s: signed immediate 0x%x must be in range [-0x80000, 0x7ffff] (20 bits)", mnem, imm) + } + word = riscvUType(enc, rd, imm<<12) default: return nil, fmt.Errorf("cannot encode %s with %d operands", mnem, len(ops)) diff --git a/asm/riscv_encode_test.go b/asm/riscv_encode_test.go index 28fca0b..7bad1a1 100644 --- a/asm/riscv_encode_test.go +++ b/asm/riscv_encode_test.go @@ -109,6 +109,34 @@ TEXT ·imm(SB), NOSPLIT, $0 } } +// TestRISCV_utypeImmediate pins the U-type immediate semantics byte for byte +// against go tool asm (riscv64.s lines 76 to 85): the source immediate is the +// raw 20-bit field, not a byte address to divide, and both sign extremes +// encode. The range beyond the signed 20 bits is the toolchain's own error. +func TestRISCV_utypeImmediate(t *testing.T) { + fn := firstTextRISCV(t, `#include "textflag.h" +TEXT ·utype(SB), NOSPLIT, $0 + AUIPC $524287, X10 + LUI $524287, X15 + LUI $167, X15 + AUIPC $-524288, X15 + RET +`) + code := assembleRISCVHelper(t, fn) + want := "17f5ff7f" + "b7f7ff7f" + "b7770a00" + "97070080" + if got := hex.EncodeToString(code[:16]); got != want { + t.Errorf("U-type immediates: %s, want %s", got, want) + } + fn = firstTextRISCV(t, `#include "textflag.h" +TEXT ·wide(SB), NOSPLIT, $0 + AUIPC $524288, X10 + RET +`) + if _, _, _, _, _, _, err := assembleRISCV(fn); err == nil { + t.Error("AUIPC $524288: expected the 20-bit range error, got none") + } +} + func TestRISCV_branches(t *testing.T) { fn := firstTextRISCV(t, `#include "textflag.h" TEXT ·br(SB), NOSPLIT, $0