// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) // SPDX-License-Identifier: BSD-3-Clause package asm import ( "strings" "sourcedock.dev/petrbalvin/gasm-devkit/ast" ) // Loong64 frame mapping, matching the Go toolchain's loong64 backend. // // Go's loong64 functions have no frame pointer: FP and SP are synthetic // registers resolved against the hardware stack pointer (R3) and the frame // size. The return address lives in R1 (the link register). // // The autosize is the real stack adjustment: the declared local frame plus // the 8 bytes for the saved link register, rounded up to a multiple of 8 // (the toolchain aligns frames with `if autosize&4 != 0 { autosize += 4 }`). // A leaf function (no calls) with a zero frame gets no prologue at all. // // Prologue (autosize > 0), byte-identical to the toolchain: // // MOVV R1, -autosize(R3) // save LR below the new SP (traceback-safe) // ADDV $-autosize, R3 // open the frame // MOVV R1, 0(R3) // save LR again at SP (signal-safety) // // Epilogue: MOVV 0(R3), R1; ADDV $autosize, R3 (non-leaf only for the LR // restore); the RET's jirl r0, r1, 0 follows. // loong64FrameInfo holds the frame layout derived from a TEXT directive. type loong64FrameInfo struct { autosize int // the real SP adjustment (locals + saved LR, aligned) frame int // the declared $framesize args int // the declared -argsize noSplit bool // the NOSPLIT flag leaf bool // no call instructions in the body } // loong64ComputeFrame derives the frame layout for a TEXT function. func loong64ComputeFrame(t *ast.Text) loong64FrameInfo { fi := loong64FrameInfo{ frame: frameSize(t), args: argsSize(t), } for _, f := range t.Flags { if f == "NOSPLIT" { fi.noSplit = true } } fi.leaf = loong64IsLeaf(t) if fi.frame != 0 { fi.autosize = fi.frame + 8 // space for the saved LR if fi.autosize&4 != 0 { fi.autosize += 4 } } else if !fi.leaf { // A zero-frame non-leaf function still opens an 8-byte frame for LR. fi.autosize = 8 } return fi } // loong64IsLeaf reports whether a function contains no call instructions // (JAL/BL/CALL), matching the toolchain's LEAF mark, which drives the frame // and the epilogue shape. func loong64IsLeaf(t *ast.Text) bool { for _, stmt := range t.Body { in, ok := stmt.(*ast.Instr) if !ok { continue } switch strings.ToUpper(in.Mnemonic.Text) { case "JAL", "CALL", "BL": return false } } return true } // loong64Prologue returns the prologue bytes for a loong64 function. func loong64Prologue(fi loong64FrameInfo) []byte { if fi.autosize == 0 { return nil } addiD := l64DualTable["ADDV"].imm return l64WordsLE( l64irr(l64loadStoreTable["MOVV"].st, -fi.autosize, 3, 1), // MOVV R1, -autosize(R3) l64irr(addiD, -fi.autosize, 3, 3), // ADDV $-autosize, R3 l64irr(l64loadStoreTable["MOVV"].st, 0, 3, 1), // MOVV R1, 0(R3) ) } // loong64Return returns the bytes for a RET: the epilogue (restore LR and // deallocate the frame when present) followed by jirl r0, r1, 0. func loong64Return(fi loong64FrameInfo) []byte { var ws []uint32 if fi.autosize != 0 { if !fi.leaf { // MOVV 0(R3), R1 — restore the link register. ws = append(ws, l64irr(l64loadStoreTable["MOVV"].ld, 0, 3, 1)) } // ADDV $autosize, R3 — close the frame. ws = append(ws, l64irr(l64DualTable["ADDV"].imm, fi.autosize, 3, 3)) } // jirl r0, r1, 0 — return. ws = append(ws, l64irr16(l64branchTable["JIRL"], 0, 1, 0)) return l64WordsLE(ws...) } // loong64ResolvePseudo translates a pseudo-register memory reference into a // hardware base register and offset. x+N(FP) → (N + autosize + 8)(SP); // x-N(SP) → (autosize - N)(SP). Returns base = -1 for an unresolvable // reference (SB: static data, handled by the relocation path). func loong64ResolvePseudo(sym *ast.Symbol, fi loong64FrameInfo) (base int, off int32) { if sym == nil { return -1, 0 } switch sym.Pseudo { case "FP": return 3, int32(sym.Offset) + int32(fi.autosize) + 8 case "SP": return 3, int32(fi.autosize) + int32(sym.Offset) case "SB": return -1, int32(sym.Offset) } return -1, 0 }