// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) // SPDX-License-Identifier: BSD-3-Clause package asm import ( "testing" "sourcedock.dev/petrbalvin/gasm-sdk/parser" ) // TestRISCVFrameSpadjAndLines checks that a framed function records its // stack-adjustment boundaries and source-line table, the inputs the GOOBJ // emitter turns into the pcsp/pcfile/pcline tables. func TestRISCVFrameSpadjAndLines(t *testing.T) { f, errs := parser.Parse("frame_riscv64.s", `#include "textflag.h" TEXT ·framed(SB), NOSPLIT, $16-16 MOV a+0(FP), X10 MOV b+8(FP), X11 ADD X11, X10, X10 MOV X10, ret+16(FP) RET `) if len(errs) > 0 { t.Fatalf("parse: %v", errs) } img, err := AssembleFileRISCV(f) if err != nil { t.Fatalf("AssembleFileRISCV: %v", err) } fn := img.Funcs[0] if fn.Size != 24 { t.Fatalf("size = %d, want 24", fn.Size) } // autosize = 16 + 8 = 24; the prologue boundary is just past its C.ADDI // (SD 4 + C.ADDI 2 = 6), and the RET restores SP just past its C.ADDI // (RET starts at 16; C.LDSP 2 + C.ADDI 2 = 20). wantSpadj := []SpadjStep{{PC: 6, Value: 24}, {PC: 20, Value: 0}} if len(fn.Spadj) != len(wantSpadj) { t.Fatalf("spadj = %v, want %v", fn.Spadj, wantSpadj) } for i := range wantSpadj { if fn.Spadj[i] != wantSpadj[i] { t.Errorf("spadj[%d] = %v, want %v", i, fn.Spadj[i], wantSpadj[i]) } } // One line entry per instruction, in emission order. wantLines := []LineEntry{ {Offset: 8, Line: 4}, {Offset: 10, Line: 5}, {Offset: 12, Line: 6}, {Offset: 14, Line: 7}, {Offset: 16, Line: 8}, } if len(fn.Lines) != len(wantLines) { t.Fatalf("lines = %v, want %v", fn.Lines, wantLines) } for i := range wantLines { if fn.Lines[i] != wantLines[i] { t.Errorf("lines[%d] = %v, want %v", i, fn.Lines[i], wantLines[i]) } } } // TestRISCVFrameSpadjLargeFrame checks the stack-adjustment boundaries of a // frame past the imm12 range: the prologue materialises the LR-store address // and the SP adjustment through X31 (C.LUI + C.ADD + SD, then C.LUI + ADDIW + // C.ADD), so the SP boundary lands at PC 16, and the RET closes with // C.LDSP plus the same X31 adjustment, 10 bytes. Regression: both helpers // assumed the small-frame prologue and reported 8 and 6. func TestRISCVFrameSpadjLargeFrame(t *testing.T) { f, errs := parser.Parse("bigframe_riscv64.s", `#include "textflag.h" TEXT ·big(SB), NOSPLIT, $9000-8 MOV a+0(FP), X10 RET `) if len(errs) > 0 { t.Fatalf("parse: %v", errs) } img, err := AssembleFileRISCV(f) if err != nil { t.Fatalf("AssembleFileRISCV: %v", err) } fn := img.Funcs[0] // autosize = 9008. Prologue: C.LUI X31 + C.ADD X31,SP (4) + SD (4) + // C.LUI X31 + ADDIW X31 + C.ADD SP,X31 (8) = 16 bytes to the SP boundary; // C.SDSP X1 (2) follows, so the body starts at 18. wantSpadj := []SpadjStep{{PC: 16, Value: 9008}, {PC: 36, Value: 0}} if len(fn.Spadj) != len(wantSpadj) { t.Fatalf("spadj = %v, want %v", fn.Spadj, wantSpadj) } for i := range wantSpadj { if fn.Spadj[i] != wantSpadj[i] { t.Errorf("spadj[%d] = %v, want %v", i, fn.Spadj[i], wantSpadj[i]) } } // The FP load materialises its 9016-byte offset through X31 as well // (8 bytes), then RET's epilogue (C.LDSP + X31 adjust = 10) plus JALR. if fn.Size != 18+8+14 { t.Errorf("size = %d, want %d", fn.Size, 18+8+14) } } // TestRISCVRegAliases checks the Go ABI register aliases that the toolchain // defines: LR is the link register (X1) and TMP is the assembler scratch // register (X31/T6). func TestRISCVRegAliases(t *testing.T) { for name, want := range map[string]int{ "X1": 1, "RA": 1, "LR": 1, "X31": 31, "T6": 31, "TMP": 31, "X2": 2, "SP": 2, } { if got := riscvRegNum(name); got != want { t.Errorf("riscvRegNum(%q) = %d, want %d", name, got, want) } } }