// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) // SPDX-License-Identifier: BSD-3-Clause package asm import ( "strings" "testing" ) // TestRISCVBigMemOffset_Differential proves the memory-offset expansion // against the oracle: a load or store whose offset leaves the signed 12-bit // span materialises the high part in the assembler's temporary register // (C.LUI, or LUI beyond its six-bit immediate) followed by C.ADD of the base, // and accesses through it, exactly as the toolchain's instructionsForLoad and // instructionsForStore synthesise. The battery spans both sides of the // 12-bit edge, both int32 boundaries, the compressed and uncompressed LUI // halves, every width family and the MOV spellings, under a prologue too so // the size accounting keeps the layout honest. func TestRISCVBigMemOffset_Differential(t *testing.T) { src := `#include "textflag.h" TEXT ·bigmem(SB), NOSPLIT, $16 LD 2047(X6), X5 // single word: the edge that still fits LD 2048(X6), X5 // hi 1, lo -2048 LD 4096(X6), X5 // hi 1, lo 0 SD X5, 8192(X6) // store side of the same split FLD 4096(X6), F5 // FP widths share the expansion FSD F5, 8192(X6) MOVB 4097(X6), X8 // the MOV widths route through the same load MOVHU 4098(X6), X9 MOV X7, 16384(X6) // and the same store LD 1048576(X6), X10 // hi 256: LUI, not C.LUI LD -4097(X6), X11 // hi -1, lo -1 SD X11, -8192(X6) LD 2147483647(X6), X12 // int32 upper boundary LD -2147483648(X6), X13 // int32 lower boundary RET ` path := writeRISCVSrc(t, "bigmem_riscv64.s", src) assertRISCVDifferential(t, path, src, "bigmem") } // TestRISCVBigMemOffsetFrame_Differential proves the expansion under the // stack-split guard: a function that opens a frame and calls out gets the // guard, the prologue and the epilogue ahead of the body, so every body // offset now depends on the expansion's size accounting having kept the // layout in step with the emitted bytes. func TestRISCVBigMemOffsetFrame_Differential(t *testing.T) { src := `#include "textflag.h" TEXT ·bigframe(SB), $16 CALL extcal(SB) LD 4096(X6), X5 SD X5, 4096(X6) FLD 8192(X6), F5 FSD F5, 8192(X6) RET ` path := writeRISCVSrc(t, "bigframe_riscv64.s", src) assertRISCVDifferential(t, path, src, "bigframe") } // TestRISCVBigMemOffsetRejections pins the honest rejections the expansion // cannot carry: a constant beyond the signed 32-bit span is the toolchain's // "constant too large" (its Split32BitImmediate has no wider split), and a // JALR displacement beyond the signed 12-bit span is its I-type range check, // because the jump would land somewhere else entirely. func TestRISCVBigMemOffsetRejections(t *testing.T) { cases := []struct { name string src string want string }{ { name: "store beyond int32", src: "\tSD X5, 4294967295(X6)\n", want: "SD: constant 4294967295 too large", }, { name: "load below int32", src: "\tLD -2147483649(X6), X5\n", want: "LD: constant -2147483649 too large", }, { name: "MOV load beyond int32", src: "\tMOV 4294967296(X6), X5\n", want: "MOV: constant 4294967296 too large", }, { name: "MOV store beyond int32", src: "\tMOV X5, 4294967296(X6)\n", want: "MOV: constant 4294967296 too large", }, { name: "FP store beyond int32", src: "\tFSD F5, 4294967296(X6)\n", want: "FSD: constant 4294967296 too large", }, { name: "FP load beyond int32", src: "\tFLD 4294967296(X6), F5\n", want: "FLD: constant 4294967296 too large", }, { name: "JALR displacement above 12 bits", src: "\tJALR 4096(X7)\n", want: "JALR: signed immediate 4096 must be in range [-2048, 2047] (12 bits)", }, { name: "JALR displacement below 12 bits", src: "\tJALR X5, -2049(X7)\n", want: "JALR: signed immediate -2049 must be in range [-2048, 2047] (12 bits)", }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { fn := firstTextRISCV(t, "#include \"textflag.h\"\n\nTEXT ·r(SB), NOSPLIT, $0\n"+tc.src+"\tRET\n") _, _, _, _, _, _, err := assembleRISCV(fn, nil) if err == nil { t.Fatalf("source assembled, want rejection %q", tc.want) } if !strings.Contains(err.Error(), tc.want) { t.Errorf("error %q does not carry %q", err.Error(), tc.want) } }) } }