feat(asm): give riscv64 the END and GETCALLERPC the toolchain accepts

Assisted-by: GLM 5.3 Flash
This commit is contained in:
petrbalvin committed 2026-10-07 20:39:44 +02:00
1 parent 0211d6672d
commit 76f8ba6403
3 files changed
+195 -6

No files matched your search

+99 -2
View File
@@ -380,10 +380,14 @@ func riscvInstrSize(instr *ast.Instr, fi riscvFrameInfo, tlsSyms map[string]bool
mnem := instr.Mnemonic.Text
ops := instr.Operands
mnem = riscvNormalisePseudo(mnem)
if mnem == "FUNCDATA" || mnem == "PCDATA" {
// The bookkeeping statements contribute no bytes.
if mnem == "FUNCDATA" || mnem == "PCDATA" || mnem == "END" {
// The bookkeeping statements and the function-end marker contribute
// no bytes.
return 0
}
if mnem == "GETCALLERPC" {
return riscvGetCallerPCSize(ops, fi)
}
var immNeg bool
mnem, immNeg = riscvNormaliseImmAlias(mnem, ops)
if mnem == "RET" {
@@ -1136,6 +1140,12 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
return nil, fmt.Errorf("PCDATA expects $n, $m")
}
return nil, nil
case "END":
// The function-end marker: the toolchain accepts it anywhere in a
// body, ignores whatever operands follow it, and emits nothing.
return nil, nil
case "GETCALLERPC":
return encodeRISCVGetCallerPC(ops, fi, relocs)
case "WORD":
// WORD $w lays down a raw 32-bit little-endian word, in the range
// [0, 0xffffffff] exactly as the toolchain's validation bounds it.
@@ -2429,6 +2439,65 @@ func riscvFrameMemSize(op *ast.Operand, fi riscvFrameInfo) int {
return len(riscvAddressInX31WithBase(off, rs1)) + 4
}
// encodeRISCVGetCallerPC encodes the toolchain's GETCALLERPC rewrite: on
// entry the caller's address sits in the link register, so a leaf reads it
// straight from X1 (MOV X1, rd, which the compress pass turns into C.MV);
// a body that calls out has clobbered X1, and reads the prologue's save at
// 0(SP) instead (LD rd, 0(SP), compressed to C.LDSP). A memory destination
// stores the address there through the MOV store paths, which the leaf form
// shares with every width and offset; the framed form is the toolchain's
// own rejection, its rewrite reading (SP) into a MOV with two memory ends.
func encodeRISCVGetCallerPC(ops []*ast.Operand, fi riscvFrameInfo, relocs *[]Reloc) ([]byte, error) {
if len(ops) == 0 {
return nil, fmt.Errorf("GETCALLERPC: unsupported MOV")
}
dst := ops[len(ops)-1]
if isMemOperand(dst) {
if !fi.leaf {
return nil, fmt.Errorf("GETCALLERPC: unsupported MOV")
}
if dst.Addr.Sym != nil && dst.Addr.Sym.Pseudo == "SB" {
// A static destination rides the external store path, exactly
// as MOV X1, sym(SB) does.
return encodeRISCVSBStore(dst.Addr.Sym, 1, relocs), nil
}
if err := riscvWantMemBase("GETCALLERPC", "rs1", dst); err != nil {
return nil, err
}
rs1, off, err := riscvAccessMem("GETCALLERPC", dst, fi)
if err != nil {
return nil, err
}
return riscvFrameMemOp(riscvEnc{0x23, 0x3, 0x00}, true, 1, rs1, off), nil
}
n, bank := riscvBankedRegNum(operandRegName(dst))
if bank != riscvBankInt {
return nil, fmt.Errorf("GETCALLERPC: expected integer register in rd position but got non-integer register %s", operandRegName(dst))
}
if fi.leaf {
return wordLE(riscvIType(riscvInstrTable["ADDI"], n, 1, 0)), nil
}
return wordLE(riscvIType(riscvInstrTable["LD"], n, 2, 0)), nil
}
// riscvGetCallerPCSize returns the encoded size of a GETCALLERPC for the
// layout pass: one word for the register reads, the store's own size
// (expanded where the offset leaves imm12) for a memory destination.
func riscvGetCallerPCSize(ops []*ast.Operand, fi riscvFrameInfo) int {
if len(ops) == 0 {
return 4
}
dst := ops[len(ops)-1]
if isMemOperand(dst) && (dst.Addr.Sym == nil || dst.Addr.Sym.Pseudo != "SB") {
if err := riscvWantMemOffset("GETCALLERPC", dst, fi); err == nil {
if rs1, off := memFromOperandWithFrame(dst, fi); rs1 >= 0 && !fits12(off) {
return len(riscvAddressInX31WithBase(off, rs1)) + 4
}
}
}
return 4
}
// riscvMemInstrSize returns the encoded size of a plain load or store
// (integer and FP widths, one register end and one memory end) for the layout
// pass: 4 bytes when the offset fits the signed 12-bit span, otherwise the
@@ -3124,6 +3193,34 @@ func tryCompressRVC(instr *ast.Instr, fi riscvFrameInfo) (uint16, bool) {
return rvcCS(0x7, rvcReg3(rs2), rvcReg3(rs1), uint32(imm)), true
}
case "GETCALLERPC":
// The rewrite lands on MOV X1, rd (leaf), LD rd, 0(SP) (framed) or
// SD X1, off(base) (leaf, memory destination); each compresses the
// way its own shape does, the toolchain's compress() rules verbatim.
if len(ops) == 0 {
return 0, false
}
dst := ops[len(ops)-1]
if !isMemOperand(dst) {
rd := regFromOperand(dst)
if fi.leaf {
if rd > 0 {
return rvcCR(0x8, uint32(rd), 1), true // C.MV rd, X1
}
return 0, false
}
// LD rd, 0(SP): the SP form takes every destination but X0.
if rd > 0 {
return rvcLSP(0x3, uint32(rd), 0), true // C.LDSP rd, 0(SP)
}
return 0, false
}
if fi.leaf {
if rs1, off := memFromOperandWithFrame(dst, fi); rs1 == 2 && off >= 0 && off < 512 && off%8 == 0 {
return rvcSSP(0x7, 1, uint32(off)), true // C.SDSP X1, off
}
}
case "LW":
rd, rs1, imm := extractLDParams(instr, fi)
if rs1 == 2 && rd != 0 && rd != -1 && imm >= 0 && imm < 256 && imm%4 == 0 {
+11 -4
View File
@@ -34,6 +34,11 @@ import (
type riscvFrameInfo struct {
autosize int // the real SP adjustment (locals + saved LR)
// leaf mirrors the toolchain's cursym.Leaf: the body makes no call, so
// the link register still holds the caller's address. GETCALLERPC reads
// it there; a framed body reads the prologue's save at 0(SP) instead.
leaf bool
// Stack-split guard state: the toolchain emits the check for every
// non-NOSPLIT function whose autosize is nonzero (a zero autosize is
// "effectively NOSPLIT"); unlike amd64 and arm64 there is no leaf
@@ -44,12 +49,12 @@ type riscvFrameInfo struct {
// riscvComputeFrame derives the frame layout for a TEXT function.
func riscvComputeFrame(t *ast.Text) riscvFrameInfo {
frame := frameSize(t)
if frame != 0 || !riscvIsLeaf(t) {
leaf := riscvIsLeaf(t)
if frame := frameSize(t); frame != 0 || !leaf {
// FixedFrameSize = 8: space for the saved link register. A
// zero-frame non-leaf function still opens an 8-byte frame for LR.
autosize := frame + 8
fi := riscvFrameInfo{autosize: autosize}
fi := riscvFrameInfo{autosize: autosize, leaf: leaf}
if !hasNoSplitFlag(t) {
fi.needSplit = true
switch {
@@ -63,7 +68,9 @@ func riscvComputeFrame(t *ast.Text) riscvFrameInfo {
}
return fi
}
return riscvFrameInfo{}
// A zero-frame leaf: no prologue, no guard, and the link register still
// holding the caller's address.
return riscvFrameInfo{leaf: leaf}
}
// hasNoSplitFlag reports whether the TEXT directive carries NOSPLIT.
+85
View File
@@ -241,6 +241,91 @@ func TestRISCVToolchainRejections(t *testing.T) {
}
}
// TestRISCVGetCallerPC_Differential proves the toolchain's GETCALLERPC
// rewrite: a leaf reads the caller's address straight from the link register
// (MOV X1, rd, compressed to C.MV; X0 stays the discarded ADDI), a body that
// calls out reads the prologue's save at 0(SP) (LD rd, 0(SP), compressed to
// C.LDSP, the reserved SP destination included; X0 stays uncompressed), and a
// memory destination stores it there, through the frame mapping and the
// static-data path alike.
func TestRISCVGetCallerPC_Differential(t *testing.T) {
leaf := `#include "textflag.h"
TEXT ·leafgcp(SB), NOSPLIT, $0
GETCALLERPC X5 // c.mv X5, X1
GETCALLERPC X0 // addi X0, X1, 0, discarded
GETCALLERPC 8(X6) // sd X1, 8(X6)
GETCALLERPC x+8(FP) // c.sdsp X1, 16
END
RET
`
path := writeRISCVSrc(t, "gcp_leaf_riscv64.s", leaf)
assertRISCVDifferential(t, path, leaf, "leafgcp")
framed := `#include "textflag.h"
TEXT ·framegcp(SB), $8
CALL extcall(SB)
GETCALLERPC X5 // c.ldsp X5, 0(SP)
GETCALLERPC X2 // c.ldsp X2, 0(SP)
GETCALLERPC X0 // ld X0, 0(SP), uncompressed
END
RET
`
path = writeRISCVSrc(t, "gcp_frame_riscv64.s", framed)
assertRISCVDifferential(t, path, framed, "framegcp")
}
// TestRISCVGetCallerPCRejections pins the rewrite's rejections: a framed
// body with a memory destination would read (SP) into a MOV with two memory
// ends, a non-integer register is the rd bank rejection, and no destination
// at all is the toolchain's unsupported MOV.
func TestRISCVGetCallerPCRejections(t *testing.T) {
cases := []struct {
name string
src string
want string
}{
{
name: "memory destination in a framed body",
src: "CALL extcall(SB)\n\tGETCALLERPC 8(X6)\n",
want: "GETCALLERPC: unsupported MOV",
},
{
name: "float destination",
src: "\tGETCALLERPC F5\n",
want: "GETCALLERPC: expected integer register in rd position but got non-integer register F5",
},
{
name: "vector destination",
src: "\tGETCALLERPC V1\n",
want: "GETCALLERPC: expected integer register in rd position but got non-integer register V1",
},
{
name: "immediate destination",
src: "\tGETCALLERPC $5\n",
want: "GETCALLERPC: expected integer register in rd position",
},
{
name: "no destination",
src: "\tGETCALLERPC\n",
want: "GETCALLERPC: unsupported MOV",
},
}
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)
}
})
}
}
// TestRISCVTLSRelocKind pins the relocation the TLS sequence records: one
// R_RISCV_TLS_LE field spanning the LUI + ADDIW pair, the symbol named and
// no addend for a plain tls(SB) reference.