// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) // SPDX-License-Identifier: BSD-3-Clause package asm import ( "testing" "sourcedock.dev/petrbalvin/gasm-devkit/ast" "sourcedock.dev/petrbalvin/gasm-devkit/parser" ) func TestArm64LDRSTREncoding(t *testing.T) { tests := []struct { name string got uint32 want uint32 }{ {"LDR X4, [SP, #56]", a64LSU(3, 0, 1, 7, 31, 4), 0xf9401fe4}, {"STR X4, [SP, #64]", a64LSU(3, 0, 0, 8, 31, 4), 0xf90023e4}, {"STR X5, [SP, #32]", a64LSU(3, 0, 0, 4, 31, 5), 0xf90013e5}, {"LDR X6, [SP, #32]", a64LSU(3, 0, 1, 4, 31, 6), 0xf94013e6}, } for _, tt := range tests { if tt.got != tt.want { t.Errorf("%s: got %08x, want %08x", tt.name, tt.got, tt.want) } } } func TestArm64PrologueEncoding(t *testing.T) { fi := arm64FrameInfo{autosize: 48, frame: 32, leaf: false} pro := arm64Prologue(fi) if len(pro) != 12 { t.Fatalf("prologue length: got %d, want 12", len(pro)) } expected := []uint32{0xf81d0ffe, 0xf81f83fd, 0xd10023fd} for i, w := range leWords(pro) { if w != expected[i] { t.Errorf("prologue word %d: got %08x, want %08x", i, w, expected[i]) } } } func TestArm64EpilogueSmallEncoding(t *testing.T) { fi := arm64FrameInfo{autosize: 48, frame: 32, leaf: false} ret := arm64Return(fi) if len(ret) != 12 { t.Fatalf("epilogue length: got %d, want 12", len(ret)) } // Non-leaf small frame: LDR FP, [SP, #-8]; LDR.P LR, [SP], #48; RET expected := []uint32{0xf85f83fd, 0xf84307fe, 0xd65f03c0} for i, w := range leWords(ret) { if w != expected[i] { t.Errorf("epilogue word %d: got %08x, want %08x", i, w, expected[i]) } } } func TestArm64LargeFrameEncoding(t *testing.T) { fi := arm64FrameInfo{autosize: 272, frame: 256, leaf: false} pro := arm64Prologue(fi) if len(pro) != 16 { t.Fatalf("prologue length: got %d, want 16", len(pro)) } expected := []uint32{0xd10443f4, 0xa93ffa9d, 0x9100029f, 0xd10023fd} for i, w := range leWords(pro) { if w != expected[i] { t.Errorf("prologue word %d: got %08x, want %08x", i, w, expected[i]) } } epi := arm64Return(fi) if len(epi) != 12 { t.Fatalf("epilogue length: got %d, want 12", len(epi)) } eexpected := []uint32{0xa97ffbfd, 0x910443ff, 0xd65f03c0} for i, w := range leWords(epi) { if w != eexpected[i] { t.Errorf("epilogue word %d: got %08x, want %08x", i, w, eexpected[i]) } } } func TestArm64NoFrame(t *testing.T) { fi := arm64FrameInfo{autosize: 0, frame: 0, leaf: true} pro := arm64Prologue(fi) if len(pro) != 0 { t.Errorf("no-frame prologue: got %d bytes, want 0", len(pro)) } ret := arm64Return(fi) if len(ret) != 4 { t.Fatalf("no-frame return: got %d bytes, want 4", len(ret)) } if leWord(ret) != 0xd65f03c0 { t.Errorf("no-frame RET: got %08x, want d65f03c0", leWord(ret)) } } func TestArm64RegNum(t *testing.T) { tests := []struct { name string want int }{ {"R0", 0}, {"R4", 4}, {"R29", 29}, {"R30", 30}, {"R31", 31}, {"FP", 29}, {"LR", 30}, {"LINK", 30}, {"SP", 31}, {"ZR", 31}, {"F0", 0}, {"F4", 4}, {"F31", 31}, {"INVALID", -1}, {"X0", -1}, {"", -1}, } for _, tt := range tests { got := arm64RegNum(tt.name) if got != tt.want { t.Errorf("arm64RegNum(%q) = %d, want %d", tt.name, got, tt.want) } } } func TestArm64ComputeFrame(t *testing.T) { src := "TEXT ·f(SB), NOSPLIT, $32-0\n\tADD\tR4, R5\n\tRET\n" f, errs := parser.Parse("test_arm64.s", src) if len(errs) > 0 { t.Fatalf("parse: %v", errs) } fi := arm64ComputeFrame(f.Decls[0].(*ast.Text)) if fi.frame != 32 { t.Errorf("frame: got %d, want 32", fi.frame) } if fi.autosize != 48 { // 32+8=40, aligned to48 t.Errorf("autosize: got %d, want 48", fi.autosize) } // ADD + RET with no CALL/BL → leaf if !fi.leaf { t.Error("expected leaf") } } func TestArm64IsLeaf(t *testing.T) { src := "TEXT ·f(SB), NOSPLIT, $0-0\n\tADD\tR4, R5\n\tRET\n" f, errs := parser.Parse("test_arm64.s", src) if len(errs) > 0 { t.Fatalf("parse: %v", errs) } if !arm64IsLeaf(f.Decls[0].(*ast.Text)) { t.Error("expected leaf") } src2 := "TEXT ·f(SB), NOSPLIT, $0-0\n\tBL\tother(SB)\n\tRET\n" f2, errs := parser.Parse("test_arm64.s", src2) if len(errs) > 0 { t.Fatalf("parse: %v", errs) } if arm64IsLeaf(f2.Decls[0].(*ast.Text)) { t.Error("expected non-leaf") } } func TestArm64Bitmask(t *testing.T) { tests := []struct { v uint64 sf int N, immr, imms uint32 ok bool }{ {1, 1, 1, 0, 0, true}, // single bit at pos 0 {2, 1, 1, 63, 0, true}, // single bit at pos 1 (immr = esize-1) {0, 1, 0, 0, 0, false}, // zero is not a bitmask {0xFFFFFFFFFFFFFFFF, 1, 0, 0, 0, false}, // all ones is not a bitmask {0x5555555555555555, 1, 0, 0, 0x3E, true}, // alternating bits (esize=2, ones=1) {0xFFFFFFFF00000000, 1, 1, 32, 31, true}, // upper 32 bits set (esize=64, ones=32) } for _, tt := range tests { N, immr, imms, ok := arm64Bitmask(tt.v, tt.sf) if ok != tt.ok { t.Errorf("arm64Bitmask(%#x, %d): ok=%v, want %v", tt.v, tt.sf, ok, tt.ok) continue } if ok && (N != tt.N || immr != tt.immr || imms != tt.imms) { t.Errorf("arm64Bitmask(%#x, %d): N=%d immr=%d imms=%d, want N=%d immr=%d imms=%d", tt.v, tt.sf, N, immr, imms, tt.N, tt.immr, tt.imms) } } } func TestArm64AssembleFile(t *testing.T) { src := `#include "textflag.h" TEXT ·simple(SB), NOSPLIT, $0-0 MOV R4, R5 ADD R4, R5, R6 RET ` f, errs := parser.Parse("test_arm64.s", src) if len(errs) > 0 { t.Fatalf("parse: %v", errs) } img, err := AssembleFileARM64(f) if err != nil { t.Fatalf("AssembleFileARM64: %v", err) } if len(img.Funcs) != 1 { t.Fatalf("got %d funcs, want 1", len(img.Funcs)) } fn := img.Funcs[0] if fn.Name != "simple" { t.Errorf("func name: got %q, want %q", fn.Name, "simple") } //3 instructions ×4 bytes =12 if fn.Size != 12 { t.Errorf("func size: got %d, want 12", fn.Size) } } func TestArm64AssembleFileWithFrame(t *testing.T) { src := `#include "textflag.h" TEXT ·framed(SB), NOSPLIT, $16-8 MOVD arg+0(FP), R4 ADD $1, R4, R4 MOVD R4, ret+0(FP) RET ` f, errs := parser.Parse("test_arm64.s", src) if len(errs) > 0 { t.Fatalf("parse: %v", errs) } img, err := AssembleFileARM64(f) if err != nil { t.Fatalf("AssembleFileARM64: %v", err) } if len(img.Funcs) != 1 { t.Fatalf("got %d funcs, want 1", len(img.Funcs)) } fn := img.Funcs[0] if fn.Frame != 16 { t.Errorf("frame: got %d, want 16", fn.Frame) } // Prologue (3×4=12) + body (3×4=12) + RET epilogue (3×4=12) = 36 if fn.Size != 36 { t.Errorf("func size: got %d, want 36", fn.Size) } } func TestArm64AssembleFileWithBranches(t *testing.T) { src := `#include "textflag.h" TEXT ·branch(SB), NOSPLIT, $0-0 BEQ done BNE skip skip: ADD R4, R5 done: RET ` f, errs := parser.Parse("test_arm64.s", src) if len(errs) > 0 { t.Fatalf("parse: %v", errs) } img, err := AssembleFileARM64(f) if err != nil { t.Fatalf("AssembleFileARM64: %v", err) } fn := img.Funcs[0] if fn.Size != 16 { t.Errorf("func size: got %d, want 16", fn.Size) } } func TestArm64AssembleFileWithJumpChain(t *testing.T) { src := `#include "textflag.h" TEXT ·chain(SB), NOSPLIT, $0-0 BNE skip ADD R4, R5 RET skip: B target target: ADD R6, R7 RET ` f, errs := parser.Parse("test_arm64.s", src) if len(errs) > 0 { t.Fatalf("parse: %v", errs) } img, err := AssembleFileARM64(f) if err != nil { t.Fatalf("AssembleFileARM64: %v", err) } // BNE should be redirected past skip→target to target directly. if img.Funcs[0].Size != 24 { t.Errorf("func size: got %d, want 24", img.Funcs[0].Size) } } func TestArm64AssembleErrors(t *testing.T) { tests := []struct { name string src string }{ {"unsupported", "TEXT ·f(SB), NOSPLIT, $0-0\n\tINVALID\tR4, R5\n\tRET\n"}, {"undefined label", "TEXT ·f(SB), NOSPLIT, $0-0\n\tB\tnosuch\n\tRET\n"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { f, errs := parser.Parse("test_arm64.s", tt.src) if len(errs) > 0 { return // parse error, that's fine } _, err := AssembleFileARM64(f) if err == nil { t.Error("expected error, got nil") } }) } } func TestArm64Movcon(t *testing.T) { tests := []struct { v int64 want int }{ {0, 0}, // 0 fits at shift 0 {1, 0}, // single bit at shift 0 {0x10000, 16}, // single bit at shift 16 {0x100000000, 32}, // single bit at shift 32 {0xFF, 0}, // 0xFF fits at shift 0 {0x12345, -1}, // multiple chunks, not movcon } for _, tt := range tests { got := arm64Movcon(tt.v) if got != tt.want { t.Errorf("arm64Movcon(%#x) = %d, want %d", tt.v, got, tt.want) } } } func TestArm64RegClassOf(t *testing.T) { if arm64RegClassOf("R4") != arm64ClsGR { t.Error("R4 should be GR") } if arm64RegClassOf("F4") != arm64ClsFP { t.Error("F4 should be FP") } if arm64RegClassOf("") != arm64ClsNone { t.Error("empty should be None") } } func TestArm64ResolvePseudo(t *testing.T) { fi := arm64FrameInfo{autosize: 48, frame: 32} // FP: offset = sym.Offset + autosize +8 base, off := arm64ResolvePseudo(&ast.Symbol{Pseudo: "FP", Offset: 0}, fi) if base != 31 || off != 56 { t.Errorf("FP: base=%d off=%d, want 31, 56", base, off) } // SP: offset = sym.Offset + frame +8 base, off = arm64ResolvePseudo(&ast.Symbol{Pseudo: "SP", Offset: -8}, fi) if base != 31 || off != 32 { t.Errorf("SP: base=%d off=%d, want 31, 32", base, off) } // SB: unresolved base, _ = arm64ResolvePseudo(&ast.Symbol{Pseudo: "SB"}, fi) if base != -1 { t.Errorf("SB: base=%d, want -1", base) } } // leWord reads a little-endian uint32 from b. func leWord(b []byte) uint32 { return uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24 } // leWords reads all little-endian uint32s from b. func leWords(b []byte) []uint32 { n := len(b) / 4 w := make([]uint32, n) for i := range w { w[i] = leWord(b[i*4:]) } return w }