// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) // SPDX-License-Identifier: BSD-3-Clause package asm import ( "bytes" "encoding/binary" "encoding/hex" "strings" "testing" "sourcedock.dev/petrbalvin/gasm-devkit/ast" "sourcedock.dev/petrbalvin/gasm-devkit/parser" ) // firstTextRISCV parses assembly source and returns the first TEXT function body. func firstTextRISCV(t *testing.T, src string) *ast.Text { t.Helper() f, errs := parser.Parse("f_riscv64.s", src) if len(errs) > 0 { t.Fatalf("parse: %v", errs) } for _, d := range f.Decls { if fn, ok := d.(*ast.Text); ok { return fn } } t.Fatal("no TEXT found") return nil } // assembleRISCVHelper assembles one TEXT function and returns its code bytes. func assembleRISCVHelper(t *testing.T, fn *ast.Text) []byte { t.Helper() code, _, _, _, _, _, err := assembleRISCV(fn) if err != nil { t.Fatalf("assemble: %v", err) } return code } func TestRISCV_add(t *testing.T) { // func add(a, b int64) int64 fn := firstTextRISCV(t, `#include "textflag.h" TEXT ·add(SB), NOSPLIT, $0-24 MOV a+0(FP), X10 MOV b+8(FP), X11 ADD X11, X10, X10 MOV X10, ret+16(FP) RET `) code := assembleRISCVHelper(t, fn) // should be 12 bytes with RVC: C.LDSP + C.LDSP + ADD + C.SDSP + C.JR _ = code if len(code) == 0 { t.Error("empty output") } } func TestRISCV_arithmetic(t *testing.T) { fn := firstTextRISCV(t, `#include "textflag.h" TEXT ·arith(SB), NOSPLIT, $0 ADD X10, X11, X12 SUB X12, X13, X14 MUL X14, X15, X16 DIV X16, X17, X18 REM X18, X19, X20 RET `) code := assembleRISCVHelper(t, fn) // 5 R-type instructions + RET = 5*4 + 4 = 24 if len(code) != 24 { t.Errorf("expected 24 bytes, got %d", len(code)) } } func TestRISCV_loadStore(t *testing.T) { fn := firstTextRISCV(t, `#include "textflag.h" TEXT ·mem(SB), NOSPLIT, $0 LD (X10), X11 SD X11, (X12) LW (X13), X14 SW X14, (X15) RET `) code := assembleRISCVHelper(t, fn) // Four register-relative loads/stores compress (2B each) + JALR (4B) = 12. if len(code) != 12 { t.Errorf("expected 12 bytes, got %d", len(code)) } } func TestRISCV_immediate(t *testing.T) { fn := firstTextRISCV(t, `#include "textflag.h" TEXT ·imm(SB), NOSPLIT, $0 ADDI $42, X10, X11 ANDI $0xFF, X11, X12 ORI $1, X12, X13 XORI $0, X13, X14 RET `) code := assembleRISCVHelper(t, fn) // 4 I-type + JALR = 4*4 + 4 = 20 if len(code) != 20 { t.Errorf("expected 20 bytes, got %d", len(code)) } } func TestRISCV_branches(t *testing.T) { fn := firstTextRISCV(t, `#include "textflag.h" TEXT ·br(SB), NOSPLIT, $0 ADDI $1, X10, X10 loop: BEQ X10, X11, done ADDI $1, X10, X10 JMP loop done: RET `) code := assembleRISCVHelper(t, fn) _ = code if len(code) == 0 { t.Error("empty output") } } func TestRISCV_MOV_imm_small(t *testing.T) { // MOV $42, rd → ADDI (fits in 12 bits, but not C.LI's 6-bit immediate). fn := firstTextRISCV(t, `#include "textflag.h" TEXT ·small(SB), NOSPLIT, $0 MOV $42, X10 RET `) code := assembleRISCVHelper(t, fn) // ADDI (4B) + JALR (4B) = 8 if len(code) != 8 { t.Errorf("expected 8 bytes, got %d", len(code)) } } func TestRISCV_MOV_imm_large(t *testing.T) { // MOV $0x12345, rd → C.LUI $18 (2B) + ADDIW $837 (4B). fn := firstTextRISCV(t, `#include "textflag.h" TEXT ·large(SB), NOSPLIT, $0 MOV $0x12345, X10 RET `) code := assembleRISCVHelper(t, fn) // C.LUI (2B) + ADDIW (4B) + JALR (4B) = 10 if len(code) != 10 { t.Errorf("expected 10 bytes, got %d", len(code)) } } func TestRISCV_MOV_reg(t *testing.T) { // MOV rs, rd → ADDI $0, rs, rd, compresses to C.MV fn := firstTextRISCV(t, `#include "textflag.h" TEXT ·reg(SB), NOSPLIT, $0 MOV X10, X11 RET `) code := assembleRISCVHelper(t, fn) // C.MV (2B) + JALR (4B) = 6 if len(code) != 6 { t.Errorf("expected 6 bytes, got %d (% x)", len(code), code) } } func TestRISCV_MOV_frame(t *testing.T) { // MOV name+off(FP), rd → load with frame mapping fn := firstTextRISCV(t, `#include "textflag.h" TEXT ·frame(SB), NOSPLIT, $0-8 MOV a+0(FP), X10 MOV X10, ret+0(FP) RET `) code := assembleRISCVHelper(t, fn) // C.LDSP (2B) + C.SDSP (2B) + JALR (4B) = 8 if len(code) != 8 { t.Errorf("expected 8 bytes, got %d", len(code)) } } func TestRISCV_RVC_loadStore(t *testing.T) { // Verify that loads/stores from SP are compressed. fn := firstTextRISCV(t, `#include "textflag.h" TEXT ·rvcstore(SB), NOSPLIT, $0 LD 0(SP), X10 SD X10, 8(SP) RET `) code := assembleRISCVHelper(t, fn) // C.LDSP (2B) + C.SDSP (2B) + JALR (4B) = 8 if len(code) != 8 { t.Errorf("expected 8 bytes, got %d (% x)", len(code), code) } } func TestRISCV_atomics(t *testing.T) { fn := firstTextRISCV(t, `#include "textflag.h" TEXT ·amo(SB), NOSPLIT, $0 AMOADDD X10, (X11), X12 LRD (X13), X14 SCD X15, (X16), X17 RET `) code := assembleRISCVHelper(t, fn) // 3 AMO instructions (4B each) + JALR (4B) = 16 if len(code) != 16 { t.Errorf("expected 16 bytes, got %d", len(code)) } } func TestRISCV_fpArith(t *testing.T) { fn := firstTextRISCV(t, `#include "textflag.h" TEXT ·fpadd(SB), NOSPLIT, $0 FADDD F10, F11, F12 FSUBD F12, F13, F14 FMULD F14, F15, F16 FDIVD F16, F17, F18 FSQRTD F18, F19 RET `) code := assembleRISCVHelper(t, fn) // 5 FP instructions (4B each) + JALR (4B) = 24 if len(code) != 24 { t.Errorf("expected 24 bytes, got %d (%d)", len(code), len(code)) } } func TestRISCV_csr(t *testing.T) { fn := firstTextRISCV(t, `#include "textflag.h" TEXT ·csrtest(SB), NOSPLIT, $0 CSRRS $0x300, X0, X10 CSRRW $0x305, X10, X11 CSRRSI $0x304, $5, X12 RET `) code := assembleRISCVHelper(t, fn) // 3 CSR instructions (4B each) + JALR (4B) = 16 if len(code) != 16 { t.Errorf("expected 16 bytes, got %d", len(code)) } } func TestRISCV_fma(t *testing.T) { fn := firstTextRISCV(t, `#include "textflag.h" TEXT ·fmatest(SB), NOSPLIT, $0 FMADDD F10, F11, F12, F13 FMSUBD F13, F14, F15, F16 FNMSUBD F16, F17, F18, F19 FNMADDD F19, F10, F11, F12 RET `) code := assembleRISCVHelper(t, fn) // 4 FMA instructions (4B each) + JALR (4B) = 20 if len(code) != 20 { t.Errorf("expected 20 bytes, got %d", len(code)) } } func TestRISCV_conversions(t *testing.T) { fn := firstTextRISCV(t, `#include "textflag.h" TEXT ·cvt(SB), NOSPLIT, $0 FCVTDL X10, F10 FCVTLD F10, X11 FMVXD F10, X12 FMVDX X12, F11 RET `) code := assembleRISCVHelper(t, fn) // 4 conversion instructions (4B each) + JALR (4B) = 20 if len(code) != 20 { t.Errorf("expected 20 bytes, got %d", len(code)) } } func TestRISCV_fpCmp(t *testing.T) { fn := firstTextRISCV(t, `#include "textflag.h" TEXT ·cmp(SB), NOSPLIT, $0 FEQD F10, F11, X10 FLTD F12, F13, X11 FLED F14, F15, X12 RET `) code := assembleRISCVHelper(t, fn) // 3 FP compare (4B each) + JALR (4B) = 16 if len(code) != 16 { t.Errorf("expected 16 bytes, got %d", len(code)) } } func TestRISCV_forwardBranch(t *testing.T) { // Forward label reference; must not fail. fn := firstTextRISCV(t, `#include "textflag.h" TEXT ·fwd(SB), NOSPLIT, $0 ADDI $1, X10, X10 BEQ X10, X11, done ADDI $1, X10, X10 done: RET `) code := assembleRISCVHelper(t, fn) _ = code if len(code) == 0 { t.Error("empty output") } } func TestRISCV_RVC_ADDI(t *testing.T) { // ADDI where rd=rs1 and small imm → C.ADDI fn := firstTextRISCV(t, `#include "textflag.h" TEXT ·caddi(SB), NOSPLIT, $0 ADDI $5, X10, X10 RET `) code := assembleRISCVHelper(t, fn) // C.ADDI (2B) + JALR (4B) = 6 if len(code) != 6 { t.Errorf("expected 6 bytes, got %d", len(code)) } } func TestRISCV_RVC_LI(t *testing.T) { // ADDI X0, $imm, rd → C.LI fn := firstTextRISCV(t, `#include "textflag.h" TEXT ·cli(SB), NOSPLIT, $0 ADDI $7, X0, X10 RET `) code := assembleRISCVHelper(t, fn) // C.LI (2B) + JALR (4B) = 6 if len(code) != 6 { t.Errorf("expected 6 bytes, got %d", len(code)) } } func TestRISCV_RVC_LUI(t *testing.T) { // LUI rd, small nonzero imm → C.LUI fn := firstTextRISCV(t, `#include "textflag.h" TEXT ·clui(SB), NOSPLIT, $0 LUI X10, $1 RET `) code := assembleRISCVHelper(t, fn) // C.LUI (2B) + JALR (4B) = 6 if len(code) != 6 { t.Errorf("expected 6 bytes, got %d", len(code)) } } func TestRISCV_AssembleFile(t *testing.T) { src := `#include "textflag.h" TEXT ·add(SB), NOSPLIT, $0-24 MOV a+0(FP), X10 RET TEXT ·sub(SB), NOSPLIT, $0 SUB X10, X11, X12 RET ` f, errs := parser.Parse("t_riscv64.s", src) if len(errs) > 0 { t.Fatalf("parse: %v", errs) } img, err := AssembleFileRISCV(f) if err != nil { t.Fatalf("AssembleFileRISCV: %v", err) } if len(img.Funcs) != 2 { t.Fatalf("expected 2 functions, got %d", len(img.Funcs)) } // func add: C.LDSP(2) + JALR(4) = 6 if img.Funcs[0].Size != 6 { t.Errorf("add: expected 6 bytes, got %d", img.Funcs[0].Size) } // func sub: SUB(4) + JALR(4) = 8 if img.Funcs[1].Size != 8 { t.Errorf("sub: expected 8 bytes, got %d", img.Funcs[1].Size) } } func TestRISCV_encodings(t *testing.T) { // Smoke test that all known RISC-V mnemonics encode successfully. tests := []struct { name, src string wantBytes int }{ {"ADD", "ADD X10, X11, X12\nRET\n", 8}, {"SUBW", "SUBW X10, X11, X12\nRET\n", 8}, {"MUL", "MUL X10, X11, X12\nRET\n", 8}, {"DIVW", "DIVW X10, X11, X12\nRET\n", 8}, {"REMUW", "REMUW X10, X11, X12\nRET\n", 8}, {"ADDIW", "ADDIW $5, X10, X11\nRET\n", 8}, {"SLLI", "SLLI $3, X10, X11\nRET\n", 8}, // ADDI+SLLI? No, SLLI uses I-type {"SRLI", "SRLI $2, X10, X11\nRET\n", 8}, {"SRAI", "SRAI $1, X10, X11\nRET\n", 8}, {"LB", "LB (X10), X11\nRET\n", 8}, {"LBU", "LBU (X10), X11\nRET\n", 8}, {"LH", "LH (X10), X11\nRET\n", 8}, {"LHU", "LHU (X10), X11\nRET\n", 8}, {"LWU", "LWU (X10), X11\nRET\n", 8}, {"SB", "SB X10, (X11)\nRET\n", 8}, {"SH", "SH X10, (X11)\nRET\n", 8}, {"SW", "SW X10, (X11)\nRET\n", 6}, {"LUI", "LUI X10, $0x12345\nRET\n", 8}, {"AUIPC", "AUIPC X10, $0\nRET\n", 8}, {"FLW", "FLW (X10), F10\nRET\n", 8}, {"FSW", "FSW F10, (X11)\nRET\n", 8}, {"FADDS", "FADDS F10, F11, F12\nRET\n", 8}, {"FMINS", "FMINS F10, F11, F12\nRET\n", 8}, {"FMAXD", "FMAXD F10, F11, F12\nRET\n", 8}, {"FCVTSD", "FCVTSD F10, F11\nRET\n", 8}, {"FCVTDS", "FCVTDS F10, F11\nRET\n", 8}, {"FMVXW", "FMVXW F10, X10\nRET\n", 8}, {"FMADD_S", "FMADDS F10, F11, F12, F13\nRET\n", 8}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { fn := firstTextRISCV(t, `#include "textflag.h" TEXT ·`+tt.name+`(SB), NOSPLIT, $0 `+tt.src) code := assembleRISCVHelper(t, fn) if len(code) != tt.wantBytes { t.Errorf("expected %d bytes, got %d", tt.wantBytes, len(code)) } }) } } func TestRISCV_RVC_branch(t *testing.T) { // Branches are never RVC-compressed (no C.BEQZ/C.BNEZ), matching go tool asm. fn := firstTextRISCV(t, `#include "textflag.h" TEXT ·cbeqz(SB), NOSPLIT, $0 ADDI $1, X10, X10 BEQ X10, X0, done ADDI $1, X10, X10 done: RET `) code := assembleRISCVHelper(t, fn) // C.ADDI(2) + BEQ(4) + C.ADDI(2) + JALR(4) = 12 if len(code) != 12 { t.Errorf("expected 12 bytes with uncompressed BEQ, got %d", len(code)) } } func TestRISCV_RVC_CJ(t *testing.T) { // JMP target → JAL X0 (never compressed to C.J), matching go tool asm. fn := firstTextRISCV(t, `#include "textflag.h" TEXT ·cj(SB), NOSPLIT, $0 JMP done done: RET `) code := assembleRISCVHelper(t, fn) // JAL(4) + JALR(4) = 8 if len(code) != 8 { t.Errorf("expected 8 bytes with uncompressed JMP, got %d", len(code)) } } func TestRISCV_RVC_CADD(t *testing.T) { // ADD where rd==rs1 and both in prime regs → C.ADD. fn := firstTextRISCV(t, `#include "textflag.h" TEXT ·cadd(SB), NOSPLIT, $0 ADD X10, X11, X10 RET `) code := assembleRISCVHelper(t, fn) // C.ADD(2) + JALR(4) = 6 if len(code) != 6 { t.Errorf("expected 6 bytes with C.ADD, got %d", len(code)) } } func TestRISCV_RVC_CADD_commute(t *testing.T) { // ADD where rd==rs2 (commutative swap) → C.ADD. fn := firstTextRISCV(t, `#include "textflag.h" TEXT ·cadd2(SB), NOSPLIT, $0 ADD X11, X10, X10 RET `) code := assembleRISCVHelper(t, fn) // C.ADD(2) + JALR(4) = 6 if len(code) != 6 { t.Errorf("expected 6 bytes with C.ADD (commuted), got %d", len(code)) } } func TestRISCV_RVC_CSUB(t *testing.T) { // SUB rs2, rs1, rd → C.SUB when rd == rs1 and both in prime regs. fn := firstTextRISCV(t, `#include "textflag.h" TEXT ·csub(SB), NOSPLIT, $0 SUB X11, X10, X10 RET `) code := assembleRISCVHelper(t, fn) // SUB X11, X10, X10 → rs2=X11, rs1=X10, rd=X10; rd==rs1 → C.SUB (2B) + JALR (4B) = 6. if len(code) != 6 { t.Errorf("expected 6 bytes with C.SUB, got %d (% x)", len(code), code) } } func TestRISCV_RVC_CXOR(t *testing.T) { fn := firstTextRISCV(t, `#include "textflag.h" TEXT ·cxor(SB), NOSPLIT, $0 XOR X10, X11, X10 RET `) code := assembleRISCVHelper(t, fn) if len(code) != 6 { t.Errorf("expected 6 bytes with C.XOR, got %d", len(code)) } } func TestRISCV_RVC_COR(t *testing.T) { fn := firstTextRISCV(t, `#include "textflag.h" TEXT ·cor(SB), NOSPLIT, $0 OR X10, X11, X10 RET `) code := assembleRISCVHelper(t, fn) if len(code) != 6 { t.Errorf("expected 6 bytes with C.OR, got %d", len(code)) } } func TestRISCV_RVC_CAND(t *testing.T) { fn := firstTextRISCV(t, `#include "textflag.h" TEXT ·cand(SB), NOSPLIT, $0 AND X10, X11, X10 RET `) code := assembleRISCVHelper(t, fn) if len(code) != 6 { t.Errorf("expected 6 bytes with C.AND, got %d", len(code)) } } func TestRISCV_RVC_CFLDSP(t *testing.T) { fn := firstTextRISCV(t, `#include "textflag.h" TEXT ·cfldsp(SB), NOSPLIT, $0-8 FLD a+0(FP), F10 RET `) code := assembleRISCVHelper(t, fn) // C.FLDSP(2) + JALR(4) = 6 if len(code) != 6 { t.Errorf("expected 6 bytes with C.FLDSP, got %d", len(code)) } } func TestRISCV_RVC_CFSDSP(t *testing.T) { fn := firstTextRISCV(t, `#include "textflag.h" TEXT ·cfsdsp(SB), NOSPLIT, $0-8 FSD F10, ret+0(FP) RET `) code := assembleRISCVHelper(t, fn) // C.FSDSP(2) + JALR(4) = 6 if len(code) != 6 { t.Errorf("expected 6 bytes with C.FSDSP, got %d", len(code)) } } func TestRISCV_SB_addr(t *testing.T) { // MOV $sym<>(SB), rd → AUIPC + ADDI (8 bytes for SB). src := `#include "textflag.h" TEXT ·sbaddr(SB), NOSPLIT, $0 MOV $answer<>(SB), X10 RET GLOBL answer<>(SB), RODATA, $8 DATA answer<>+0(SB)/8, $42 ` f, errs := parser.Parse("t_riscv64.s", src) if len(errs) > 0 { t.Fatalf("parse: %v", errs) } img, err := AssembleFileRISCV(f) if err != nil { t.Fatalf("AssembleFileRISCV: %v", err) } // AUIPC(4) + ADDI(4) + JALR(4) = 12 if img.Funcs[0].Size != 12 { t.Errorf("expected 12 bytes, got %d", img.Funcs[0].Size) } } func TestRISCV_SB_store(t *testing.T) { // MOV rd, sym<>(SB) → AUIPC + SD (8 bytes for SB). src := `#include "textflag.h" TEXT ·sbstore(SB), NOSPLIT, $0 MOV X10, result<>(SB) RET GLOBL result<>(SB), NOPTR, $8 ` f, errs := parser.Parse("t_riscv64.s", src) if len(errs) > 0 { t.Fatalf("parse: %v", errs) } img, err := AssembleFileRISCV(f) if err != nil { t.Fatalf("AssembleFileRISCV: %v", err) } // AUIPC X31(4) + SD X10,0(X31)(4) + JALR(4) = 12 if img.Funcs[0].Size != 12 { t.Errorf("expected 12 bytes, got %d", img.Funcs[0].Size) } } func TestRISCV_ELF(t *testing.T) { src := `#include "textflag.h" TEXT ·simple(SB), NOSPLIT, $0 RET ` f, errs := parser.Parse("t_riscv64.s", src) if len(errs) > 0 { t.Fatalf("parse: %v", errs) } img, err := AssembleFileRISCV(f) if err != nil { t.Fatalf("AssembleFileRISCV: %v", err) } obj, err := img.ELFRISCVObject() if err != nil { t.Fatalf("ELFRISCVObject: %v", err) } if len(obj) < 4 || obj[0] != 0x7f || obj[1] != 'E' || obj[2] != 'L' || obj[3] != 'F' { t.Fatal("not a valid ELF file") } if len(obj) >= 20 { machine := uint16(obj[18]) | uint16(obj[19])<<8 if machine != 243 { t.Errorf("e_machine = %d, want 243 (EM_RISCV)", machine) } } } func TestRISCV_ELF_withData(t *testing.T) { src := `#include "textflag.h" TEXT ·get(SB), NOSPLIT, $0 RET GLOBL val<>(SB), RODATA, $4 DATA val<>+0(SB)/4, $7 ` f, errs := parser.Parse("t_riscv64.s", src) if len(errs) > 0 { t.Fatalf("parse: %v", errs) } img, err := AssembleFileRISCV(f) if err != nil { t.Fatalf("AssembleFileRISCV: %v", err) } if len(img.DataSyms) != 1 { t.Fatalf("expected 1 data symbol, got %d", len(img.DataSyms)) } if img.DataSyms[0].Name != "val" { t.Errorf("data symbol name = %q, want val", img.DataSyms[0].Name) } if img.DataSyms[0].Size != 4 { t.Errorf("data symbol size = %d, want 4", img.DataSyms[0].Size) } obj, err := img.ELFRISCVObject() if err != nil { t.Fatalf("ELFRISCVObject: %v", err) } _ = obj } func TestRISCV_SB_load(t *testing.T) { // MOV sym<>(SB), rd → AUIPC + LD (8 bytes for SB). src := `#include "textflag.h" TEXT ·sbload(SB), NOSPLIT, $0 MOV answer<>(SB), X10 RET GLOBL answer<>(SB), RODATA, $8 DATA answer<>+0(SB)/8, $42 ` f, errs := parser.Parse("t_riscv64.s", src) if len(errs) > 0 { t.Fatalf("parse: %v", errs) } img, err := AssembleFileRISCV(f) if err != nil { t.Fatalf("AssembleFileRISCV: %v", err) } // AUIPC(4) + LD(4) + JALR(4) = 12 if img.Funcs[0].Size != 12 { t.Errorf("expected 12 bytes, got %d", img.Funcs[0].Size) } } // TestRISCV_RVC_StorePatterns pins the register-relative compressed store // encodings for offsets with immediate bits 4 and 5 set, byte-identical to // the toolchain's encodeCS (patterns {5,4,3,7,6} and {5,4,3,2,6}). // Regression: the store-side patterns dropped imm[4], so every such store // silently encoded the wrong address while the loads stayed correct. func TestRISCV_RVC_StorePatterns(t *testing.T) { fn := firstTextRISCV(t, `#include "textflag.h" TEXT ·csstores(SB), NOSPLIT, $0 SD X9, 24(X8) SW X10, 16(X11) FSD F8, 40(X12) LD 24(X8), X9 LW 16(X11), X10 FLD 40(X12), F8 RET `) code := assembleRISCVHelper(t, fn) want := []byte{ 0x04, 0xec, // c.sd x9, 24(x8) 0x88, 0xc9, // c.sw x10, 16(x11) 0x00, 0xb6, // c.fsd f8, 40(x12) 0x04, 0x6c, // c.ld x9, 24(x8) 0x88, 0x49, // c.lw x10, 16(x11) 0x00, 0x36, // c.fld f8, 40(x12) 0x67, 0x80, 0x00, 0x00, // jalr x0, 0(x1) } if !bytes.Equal(code, want) { t.Errorf("code = % x\nwant % x", code, want) } } // TestRISCV_FENCE pins the FENCE encoding: the toolchain expands the bare // mnemonic to fence iorw, iorw (0x0FF0000F), not fence 0,0. func TestRISCV_FENCE(t *testing.T) { fn := firstTextRISCV(t, `#include "textflag.h" TEXT ·fence(SB), NOSPLIT, $0 FENCE RET `) code := assembleRISCVHelper(t, fn) want := []byte{ 0x0f, 0x00, 0xf0, 0x0f, // fence iorw, iorw 0x67, 0x80, 0x00, 0x00, // jalr x0, 0(x1) } if !bytes.Equal(code, want) { t.Errorf("code = % x\nwant % x", code, want) } } // TestRISCV_RVC_WidthSpellings pins the compression of the GOROOT width // spellings: MOVW and MOVD lower to their base load/store and compress // exactly like LW/SW/FLD/FSD would (the toolchain compresses these shapes; // before the normalisation they stayed 4 bytes). func TestRISCV_RVC_WidthSpellings(t *testing.T) { fn := firstTextRISCV(t, `#include "textflag.h" TEXT ·widths(SB), NOSPLIT, $0-16 MOVW w+0(FP), X9 MOVW X9, v+4(FP) MOVD d+0(FP), F8 MOVD F8, r+8(FP) RET `) code := assembleRISCVHelper(t, fn) want := []byte{ 0xa2, 0x44, // c.lwsp x9, 8 0x26, 0xc6, // c.swsp x9, 12 0x22, 0x24, // c.fldsp f8, 8 0x22, 0xa8, // c.fsdsp f8, 16 0x67, 0x80, 0x00, 0x00, // jalr x0, 0(x1) } if !bytes.Equal(code, want) { t.Errorf("code = % x\nwant % x", code, want) } } func TestRISCV_system_instrs(t *testing.T) { // Test FENCE, ECALL, EBREAK encoding. fn := firstTextRISCV(t, `#include "textflag.h" TEXT ·sys(SB), NOSPLIT, $0 FENCE ECALL EBREAK RET `) code := assembleRISCVHelper(t, fn) // FENCE(4) + ECALL(4) + C.EBREAK(2) + JALR(4) = 14 if len(code) != 14 { t.Errorf("expected 14 bytes, got %d (% x)", len(code), code) } } func TestRISCV_MOV_sym_FP(t *testing.T) { // MOV $sym(FP), rd lowers to the frame-adjusted ADDI against SP: the // toolchain's argframe spelling. A zero frame leaves the offset at the // 8-byte link slot, compressed to C.ADDI4SPN. fn := firstTextRISCV(t, `#include "textflag.h" TEXT ·argfp(SB), NOSPLIT, $0 MOV $arg(FP), X10 RET `) code, _, _, _, _, _, err := assembleRISCV(fn) if err != nil { t.Fatalf("assemble: %v", err) } // prologue (0: leaf, zero frame) + C.ADDI4SPN (2) + RET (4) = 6 want := []byte{0x28, 0x00, 0x67, 0x80, 0x00, 0x00} if string(code) != string(want) { t.Errorf("got % x, want % x", code, want) } } func TestRISCV_Bookkeeping(t *testing.T) { // FUNCDATA and PCDATA contribute no bytes; UNDEF is the toolchain's // ebreak, compressed to C.EBREAK under RVC. fn := firstTextRISCV(t, `#include "textflag.h" TEXT ·book(SB), NOSPLIT, $0-8 FUNCDATA $0, marks<>(SB) PCDATA $1, $1 UNDEF MOV $1, X10 MOV X10, ret+0(FP) RET `) code, _, _, _, _, _, err := assembleRISCV(fn) if err != nil { t.Fatalf("assemble: %v", err) } // C.EBREAK (2) + C.LI X10, 1 (2) + C.SWSP (2) + RET (4) = 10: the // FUNCDATA and PCDATA statements contribute nothing. want := []byte{0x02, 0x90, 0x05, 0x45, 0x2a, 0xe4, 0x67, 0x80, 0x00, 0x00} if string(code) != string(want) { t.Errorf("got % x, want % x", code, want) } } func TestRISCV_JMPPCRel(t *testing.T) { // JMP N(PC): the displacement tracks the instruction N source slots // away in the final layout (0 the jump itself, negative backwards). fn := firstTextRISCV(t, `#include "textflag.h" TEXT ·slots(SB), NOSPLIT, $0-0 JMP 2(PC) MOV $1, X11 MOV $2, X12 MOV X12, X11 JMP -3(PC) RET `) code, _, _, _, _, _, err := assembleRISCV(fn) if err != nil { t.Fatalf("assemble: %v", err) } // JMP 2(PC) lands on the C.MV six bytes ahead; JMP -3(PC) lands back on // the first C.LI, six bytes behind. want := []byte{ 0x6f, 0x00, 0x60, 0x00, // JAL X0, 6 0x85, 0x45, // C.LI X11, 1 0x09, 0x46, // C.LI X12, 2 0xb2, 0x85, // C.MV X11, X12 0x6f, 0xf0, 0xbf, 0xff, // JAL X0, -6 0x67, 0x80, 0x00, 0x00, // RET } if string(code) != string(want) { t.Errorf("got % x, want % x", code, want) } } func TestRISCV_MOVWideImm(t *testing.T) { // Shift-sequence constants compress like the toolchain's expansion. fn := firstTextRISCV(t, `#include "textflag.h" TEXT ·wide(SB), NOSPLIT, $0-0 MOV $0x8000000000000000, X5 MOV $0x100000000, X5 MOV $0x000fffffffffffda, X5 RET `) code, _, _, _, _, _, err := assembleRISCV(fn) if err != nil { t.Fatalf("assemble: %v", err) } // C.LI -1, C.SLLI 63; C.LI 1, C.SLLI 32; C.LI -19, C.SLLI 13, SRLI 12. want := []byte{ 0xfd, 0x52, 0xfe, 0x12, 0x85, 0x42, 0x82, 0x12, 0xb5, 0x52, 0xb6, 0x02, 0x93, 0xd2, 0xc2, 0x00, 0x67, 0x80, 0x00, 0x00, } if string(code) != string(want) { t.Errorf("got % x, want % x", code, want) } } func TestRISCV_MOVImmPool(t *testing.T) { // A constant outside the shift shapes loads from the pooled $i64 data // symbol via AUIPC+LD, named like the toolchain's pool. src := `#include "textflag.h" TEXT ·pool(SB), NOSPLIT, $0-8 MOV $0x0101010101010101, X16 MOV X16, ret+0(FP) RET ` f, errs := parser.Parse("pool_riscv64.s", src) if len(errs) > 0 { t.Fatalf("parse: %v", errs) } img, err := AssembleFileRISCV(f) if err != nil { t.Fatalf("AssembleFileRISCV: %v", err) } // AUIPC X16, 0 + LD X16, 0(X16): the relocation pair carries the symbol. wantCode := []byte{0x17, 0x08, 0x00, 0x00, 0x03, 0x38, 0x08, 0x00} if string(img.Code[0:8]) != string(wantCode) { t.Errorf("pool load: got % x", img.Code[0:8]) } var lit *DataSymbol for i := range img.DataSyms { if img.DataSyms[i].Name == "$i64.0101010101010101" { lit = &img.DataSyms[i] } } if lit == nil { t.Fatalf("pool symbol missing: %v", img.DataSyms) } wantData := []byte{0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01} if string(img.Data[lit.Offset:lit.Offset+8]) != string(wantData) { t.Errorf("pool bytes: got % x", img.Data[lit.Offset:lit.Offset+8]) } } func TestRISCV_CALL(t *testing.T) { // CALL sym(SB) → JAL X1, sym(SB) with a single R_RISCV_JAL relocation. fn := firstTextRISCV(t, `#include "textflag.h" TEXT ·calltest(SB), NOSPLIT, $0 CALL ext(SB) RET `) code, _, relocs, _, _, _, err := assembleRISCV(fn) if err != nil { t.Fatalf("assemble: %v", err) } // prologue (8) + JAL (4) + epilogue+JALR (8) = 20 if len(code) != 20 { t.Fatalf("expected 20 bytes with CALL sym(SB), got %d", len(code)) } if len(relocs) != 1 { t.Fatalf("relocs = %d, want 1", len(relocs)) } r := relocs[0] if r.Kind != RelRISCVJal || r.Name != "ext" || r.Off != 8 || r.After != 12 || r.Addend != 0 { t.Errorf("reloc = {kind %v off %d after %d name %q addend %d}", r.Kind, r.Off, r.After, r.Name, r.Addend) } // The JAL instruction itself is JAL X1, 0 at function offset 8. wantJAL := wordLE(riscvJType(1, 0)) if !bytes.Equal(code[8:12], wantJAL) { t.Errorf("JAL = % x, want % x", code[8:12], wantJAL) } } func TestRISCV_CALL_local_error(t *testing.T) { fn := firstTextRISCV(t, `#include "textflag.h" TEXT ·calllocal(SB), NOSPLIT, $0 CALL sub sub: RET `) _, _, _, _, _, _, err := assembleRISCV(fn) if err == nil { t.Error("expected error for CALL to local label, got nil") } } // TestRISCVIndirectBranch pins the indirect branch encodings: JMP (X5) is the // toolchain's JALR X0, 0(X5), and the trampoline form JALR rd, offset(rs1) // takes its destination from the first operand (regression: the base // register was once read as the destination, silently jumping to X0). func TestRISCVIndirectBranch(t *testing.T) { fn := firstTextRISCV(t, `#include "textflag.h" TEXT ·f(SB), NOSPLIT, $0-0 JMP (X5) JALR X0, 0(X6) JALR X28, 0(X9) RET `) code := assembleRISCVHelper(t, fn) wantWords(t, code, 0x00028067, // jalr x0, 5(x0), 0 0x00030067, // jalr x0, 6(x0), 0 0x00048e67, // jalr x28, 9(x0), 0 0x00008067, // jalr x0, 1(x0), 0 (RET) ) } // encodeOneInstrRISCV encodes a single parsed instruction against a synthetic // offsets map, the smallest honest harness for the branch-range diagnostics: // the spans are far larger than any source a test would want to spell out. func encodeOneInstrRISCV(t *testing.T, src string, pc int, offsets map[string]int) ([]byte, error) { t.Helper() fn := firstTextRISCV(t, "#include \"textflag.h\"\n"+src) instr := fn.Body[0].(*ast.Instr) return encodeRISCVInstr(instr, pc, offsets, riscvFrameInfo{}, nil, nil, nil) } // TestRISCVBranchJumpRange checks that displacements beyond the B-type span // [-4096, 4094] and the J-type span [-1048576, 1048574] are diagnosed instead // of wrapping silently to a wrong target. func TestRISCVBranchJumpRange(t *testing.T) { cases := []struct { name string src string off int // the target's function-relative offset (pc 0) ok bool }{ {"branch max", "BEQ X10, X11, tgt\nRET\n", 4094, true}, {"branch past max", "BEQ X10, X11, tgt\nRET\n", 4096, false}, {"branch back max", "BEQ X10, X11, tgt\nRET\n", -4096, true}, {"branch back past max", "BEQ X10, X11, tgt\nRET\n", -4098, false}, {"branchz past max", "BEQZ X10, tgt\nRET\n", 4096, false}, {"jump max", "JMP tgt\nRET\n", 1048574, true}, {"jump past max", "JMP tgt\nRET\n", 1048576, false}, {"jump back max", "JMP tgt\nRET\n", -1048576, true}, {"jump back past max", "JMP tgt\nRET\n", -1048578, false}, {"jal past max", "JAL tgt\nRET\n", 1048576, false}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { _, err := encodeOneInstrRISCV(t, "TEXT ·f(SB), NOSPLIT, $0\n\t"+c.src, 0, map[string]int{"tgt": c.off}) if c.ok && err != nil { t.Fatalf("unexpected error: %v", err) } if !c.ok && err == nil { t.Fatal("expected an out-of-range diagnostic, got none") } }) } } // TestRISCVBranchFarBody drives the relaxation pass through the full // assembler: a forward branch over a body larger than the B-type span is // rewritten as an inverted branch over an inserted JMP, the same layout the // toolchain produces, instead of wrapping to a wrong target. func TestRISCVBranchFarBody(t *testing.T) { var sb strings.Builder sb.WriteString("#include \"textflag.h\"\nTEXT ·far(SB), NOSPLIT, $0\n\tBEQ X10, X11, done\n") for range 1100 { sb.WriteString("\tADD X10, X11, X12\n") } sb.WriteString("done:\n\tRET\n") fn := firstTextRISCV(t, sb.String()) out, _, _, _, _, _, err := assembleRISCV(fn) if err != nil { t.Fatalf("unexpected error: %v", err) } // The relaxed branch at offset 0 targets the inserted JMP at 4 (bne // x10, x11, +4); the JMP at 4 carries the far forward displacement. wantBranch := wordLE(riscvBType(riscvEnc{0x63, 0x1, 0x00}, 10, 11, 4)) if !bytes.Equal(out[0:4], wantBranch) { t.Errorf("relaxed branch = %x, want %x", out[0:4], wantBranch) } // done sits after 1100 ADDs: 4 + 4400, i.e. offset 4404 from the JMP at 4. wantJmp := wordLE(riscvJType(0, 4404)) if !bytes.Equal(out[4:8], wantJmp) { t.Errorf("inserted JMP = %x, want %x", out[4:8], wantJmp) } } // TestRISCV_CSRRange checks the CSR address range: the 12-bit field is // diagnosed rather than masked, so CSRRW $4096 does not silently address // CSR 0. func TestRISCV_CSRRange(t *testing.T) { fn := firstTextRISCV(t, `#include "textflag.h" TEXT ·csrhi(SB), NOSPLIT, $0 CSRRW $4096, X10, X11 RET `) if _, _, _, _, _, _, err := assembleRISCV(fn); err == nil { t.Error("expected an out-of-range error for CSR $4096, got none") } fn = firstTextRISCV(t, `#include "textflag.h" TEXT ·csrmax(SB), NOSPLIT, $0 CSRRW $4095, X10, X11 RET `) if _, _, _, _, _, _, err := assembleRISCV(fn); err != nil { t.Errorf("CSR $4095 must assemble: %v", err) } } // TestRISCV_Imm64Rejected checks that immediates outside the signed 32-bit // span are diagnosed instead of silently truncated to their low 32 bits for // the I-type arithmetic; the MOV forms materialise the wide constant instead // (shift sequence or pooled load), like the toolchain. func TestRISCV_Imm64Rejected(t *testing.T) { cases := []string{ "ADDI $0x100000000, X10, X11", "ANDI $-0x800000001, X10, X11", "SUB $0x100000000, X10, X11", } for _, src := range cases { fn := firstTextRISCV(t, "#include \"textflag.h\"\nTEXT ·wide(SB), NOSPLIT, $0\n\t"+src+"\n\tRET\n") if _, _, _, _, _, _, err := assembleRISCV(fn); err == nil { t.Errorf("%s: expected an out-of-range error, got none", src) } } // The full signed 32-bit span still assembles, including the SUB form // whose negated immediate only just fits. fn := firstTextRISCV(t, `#include "textflag.h" TEXT ·edge(SB), NOSPLIT, $0 MOV $2147483647, X10 MOV $-2147483648, X11 SUB $0x80000000, X12, X13 RET `) if _, _, _, _, _, _, err := assembleRISCV(fn); err != nil { t.Errorf("int32-span immediates must assemble: %v", err) } // Beyond the span the MOV forms materialise the constant like the // toolchain instead of diagnosing it. fn = firstTextRISCV(t, `#include "textflag.h" TEXT ·pool(SB), NOSPLIT, $0 MOV $0x123456789, X10 RET `) if _, _, _, _, _, _, err := assembleRISCV(fn); err != nil { t.Errorf("MOV with a 64-bit immediate must assemble: %v", err) } } // riscvWants decodes code as little-endian words and pins each one; the // expected values below were read off GOARCH=riscv64 go tool objdump of // kernels assembled with go tool asm (the toolchain's riscv64.s testdata // cross-checks the same words). func riscvWants(t *testing.T, code []byte, want ...uint32) { t.Helper() got := make([]uint32, 0, len(code)/4) for i := 0; i+4 <= len(code); i += 4 { got = append(got, binary.LittleEndian.Uint32(code[i:])) } if len(got) < len(want) { t.Fatalf("word count = %d, want %d\ncode: % x", len(got), len(want), code) } // The RET (JALR) ends the sequence; only the pinned prefix is compared. for i := range want { if got[i] != want[i] { t.Errorf("word %d = %08x, want %08x", i, got[i], want[i]) } } } // riscvWantsHex pins the exact hex encoding of a function's instruction // bytes, including any 2-byte compressed instructions in the stream; the // expected strings were read off GOARCH=riscv64 go tool objdump of kernels // assembled with go tool asm (the toolchain's riscv64.s testdata // cross-checks the same words). func riscvWantsHex(t *testing.T, code []byte, wantHex string) { t.Helper() got := hex.EncodeToString(code) if got != wantHex { t.Errorf("code = %s, want %s", got, wantHex) } } // TestRISCV_extendedPseudos pins the toolchain-synthesised instructions: // ANDN/ORN (XORI + AND/OR through the destination or TMP), the five-word // MIN/MAX expansion, the four-word rotate, ROR's compressed reverse shift // (C.SLLI when rd == rs1, both non-zero, 1 <= sll <= 63), the identical- // input MIN/MAX fold to C.MV, FABSD (FSGNJX.D), SEQZ and RDTIME (csrrs with // the time CSR). func TestRISCV_extendedPseudos(t *testing.T) { t.Run("logic and minmax", func(t *testing.T) { fn := firstTextRISCV(t, `#include "textflag.h" TEXT ·l(SB), NOSPLIT, $0 ANDN X19, X20, X21 ANDN X19, X20 ORN X20, X19 MAX X26, X28, X29 MIN X29, X30, X5 MAX X5, X5 MAX X5, X5, X6 SEQZ X5, X6 NEG X5, X6 NOT X5 RDTIME X5 RET `) code := assembleRISCVHelper(t, fn) // Words 0-10 up to the folded C.MV pair (halfwords 96 82 and 16 83), // then SEQZ, NEG, NOT and RDTIME. riscvWantsHex(t, code, "93caf9ffb37a5a01"+"93cff9ff337afa01"+"934ffaffb3e9f901"+ "b32fae01b30ff041b34eae01b3fedf01b34ede01"+ "b3afee01b30ff041b342df01b3f25f00b3425f00"+ "9682"+"1683"+ "13b31200"+"33035040"+"93c2f2ff"+"f32210c0"+"67800000") }) t.Run("rotate", func(t *testing.T) { fn := firstTextRISCV(t, `#include "textflag.h" TEXT ·r(SB), NOSPLIT, $0 ROR X10, X11, X12 ROR X10, X11 ROR $63, X11 RORIW $31, X13, X14 RORIW $1, X14, X15 RORIW $3, X14 RORW X15, X16, X17 RORW $31, X13 RET `) code := assembleRISCVHelper(t, fn) // The third ROR carries the compressed C.SLLI (05 86) in mid-stream. riscvWantsHex(t, code, "b30fa040b39ff50133d6a50033e6cf00"+ "b30fa040b39ff501b3d5a500b3e5bf00"+ "93dff5038605b3e5bf00"+ "9bdff6011b97160033e7ef00"+ "9b5f17009b17f701b3e7ff00"+ "9b5f37001b17d70133e7ef00"+ "b30ff040bb1ff801bb58f800b3e81f01"+ "9bdff6019b961600b3e6df00"+"67800000") }) t.Run("fp and branches", func(t *testing.T) { fn := firstTextRISCV(t, `#include "textflag.h" TEXT ·f(SB), NOSPLIT, $0 FABSD F1, F2 FSGNJD F1, F0, F2 FMADDD F1, F2, F3, F4 FMSUBD F1, F2, F3, F4 FNMSUBD F1, F2, F3, F4 BGT X5, X6, tgt BLE X5, X6, tgt BGTU X5, X6, tgt BLEU X5, X6, tgt tgt: RDTIME X5 RET `) code := assembleRISCVHelper(t, fn) riscvWantsHex(t, code, "53a11022"+"53011022"+"4382201a4782201a4b82201a"+ "63485300635653006364530063725300"+ // blt/bge/bltu/bgeu x6, x5 "f32210c0"+"67800000") }) } // TestRISCV_amoWords pins the full AMO family: every AMO carries aq and rl // (funct7 |= 3), LR is acquire (funct7 |= 2) and SC release (funct7 |= 1), // exactly as GOARCH=riscv64 go tool asm encodes them. func TestRISCV_amoWords(t *testing.T) { fn := firstTextRISCV(t, `#include "textflag.h" TEXT ·amo(SB), NOSPLIT, $0 AMOSWAPW X5, (X6), X7 AMOSWAPD X5, (X6), X7 AMOADDW X5, (X6), X7 AMOADDD X5, (X6), X7 AMOANDW X5, (X6), X7 AMOANDD X5, (X6), X7 AMOORW X5, (X6), X7 AMOORD X5, (X6), X7 AMOXORW X5, (X6), X7 AMOXORD X5, (X6), X7 AMOMAXW X5, (X6), X7 AMOMAXD X5, (X6), X7 AMOMAXUW X5, (X6), X7 AMOMAXUD X5, (X6), X7 AMOMINUW X5, (X6), X7 AMOMINUD X5, (X6), X7 LRW (X5), X6 LRD (X5), X6 SCW X5, (X6), X7 SCD X5, (X6), X7 RET `) code := assembleRISCVHelper(t, fn) riscvWants(t, code, 0x0E5323AF, // amoswap.w 0x0E5333AF, // amoswap.d 0x065323AF, // amoaddd.w 0x065333AF, // amoadd.d 0x665323AF, // amoand.w 0x665333AF, // amoand.d 0x465323AF, // amoor.w 0x465333AF, // amoor.d 0x265323AF, // amoxor.w 0x265333AF, // amoxor.d 0xA65323AF, // amomax.w 0xA65333AF, // amomax.d 0xE65323AF, // amomaxu.w 0xE65333AF, // amomaxu.d 0xC65323AF, // amominu.w 0xC65333AF, // amominu.d 0x1402A32F, // lr.w (aq) 0x1402B32F, // lr.d 0x1A5323AF, // sc.w (rl) 0x1A5333AF, // sc.d ) } // TestRISCV_vectorWords pins the RVV slice and the VSET* encodings. The // toolchain canonicalises an immediate avl to vsetivli even under the // VSETVLI spelling (`VSETVLI $15` and `VSETIVLI $15` come out byte- // identical), which is what the 0xC00 bit of the first word carries. func TestRISCV_vectorWords(t *testing.T) { fn := firstTextRISCV(t, `#include "textflag.h" TEXT ·v(SB), NOSPLIT, $0 VSETVLI X5, E8, M8, TA, MA, X6 VSETIVLI $4, E32, M1, TA, MA, X0 VSETVLI $15, E32, M1, TA, MA, X12 VADDVV V1, V2, V3 VADDVX X12, V12, V12 VXORVV V8, V16, V24 VMSEQVX X12, V8, V0 VMSNEVV V8, V16, V0 VSLLVI $8, V28, V30 VSRLVI $25, V29, V29 VFIRSTM V0, X6 VIDV V12 VMV4RV V8, V24 VLE8V (X10), V8 VSE8V V24, (X10) VSE32V V9, (X11) VLSSEG4E32V (X14), X0, V0 VLSSEG8E32V (X10), X0, V4 RET `) code := assembleRISCVHelper(t, fn) riscvWants(t, code, 0x0C32F357, // vsetvli x6, x5, vtype 0xc3 (E8, M8, TA, MA) 0xCD027057, // vsetivli x0, 4 0xCD07F657, // vsetivli x12, 15: VSETVLI $15 canonicalises to the same word 0x022081D7, // vadd.vv v3, v2, v1 0x02C64657, // vadd.vx v12, v12, x12 0x2F040C57, // vxor.vv v24, v16, v8 0x62864057, // vmseq.vx v0, v8, x12 0x67040057, // vmsne.vv v0, v16, v8 0x97C43F57, // vsll.vi v30, v28, 8 0xA3DCBED7, // vsrl.vi v29, v29, 25 0x4208A357, // vmfirst.m x6, v0 0x5208A657, // vid.v v12 0x9E81BC57, // vmv4r.v v24, v8 0x02050407, // vle8.v v8, (x10) 0x02050C27, // vse8.v v24, (x10) 0x0205E4A7, // vse32.v v9, (x11) 0x6A076007, // vlsseg4e32.v v0, (x14), x0 0xEA056207, // vlsseg8e32.v v4, (x10), x0 ) }