128 lines
4.2 KiB
Go
128 lines
4.2 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|