// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) // SPDX-License-Identifier: BSD-3-Clause package asm import ( "strings" "testing" "sourcedock.dev/petrbalvin/gasm-sdk/ast" "sourcedock.dev/petrbalvin/gasm-sdk/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}, {"R18_PLATFORM", 18}, {"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, 0x3C, true}, // alternating bits (esize=2, ones=1) {0xFFFFFFFF00000000, 1, 1, 32, 31, true}, // upper 32 bits set (esize=64, ones=32) {0x3fffffc0, 0, 0, 26, 23, true}, // 24 ones at bit 6: the 32-bit period marker } 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) } } // TestArm64FPSel tests FP conditional select encoding. func TestArm64FPSel(t *testing.T) { src := `#include "textflag.h" TEXT ·f(SB), NOSPLIT, $0-0 FCSELD GE, F10, F11, F12 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) } // FCSELD should be 4 bytes + RET 4 bytes = 8 if img.Funcs[0].Size != 8 { t.Errorf("size: got %d, want 8", img.Funcs[0].Size) } } // TestArm64FPCvt tests FP conversion encoding. func TestArm64FPCvt(t *testing.T) { src := `#include "textflag.h" TEXT ·f(SB), NOSPLIT, $0-0 FCVTZSD F4, R0 SCVTFD R4, F8 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 img.Funcs[0].Size != 12 { t.Errorf("size: got %d, want 12", img.Funcs[0].Size) } } // TestArm64CSEL tests conditional select encoding. func TestArm64CSEL(t *testing.T) { src := `#include "textflag.h" TEXT ·f(SB), NOSPLIT, $0-0 CSEL EQ, R0, R1, R2 CSET NE, R3 CINC GE, R4, R5 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 img.Funcs[0].Size != 16 { t.Errorf("size: got %d, want 16", img.Funcs[0].Size) } } // TestArm64CRC32 tests CRC32 encoding. func TestArm64CRC32(t *testing.T) { src := `#include "textflag.h" TEXT ·f(SB), NOSPLIT, $0-0 CRC32B R0, R2 CRC32W R6, R8 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 img.Funcs[0].Size != 12 { t.Errorf("size: got %d, want 12", img.Funcs[0].Size) } } // TestArm64Bitfield tests bitfield/shift encoding. func TestArm64Bitfield(t *testing.T) { src := `#include "textflag.h" TEXT ·f(SB), NOSPLIT, $0-0 ASR $4, R0, R1 LSL $12, R4, R5 EXTR $8, R0, R1, R2 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 img.Funcs[0].Size != 16 { t.Errorf("size: got %d, want 16", img.Funcs[0].Size) } } // TestArm64SIMD tests SIMD encoding (via the arrangement-aware table). func TestArm64SIMD(t *testing.T) { // Verify SIMD instructions are in the arrangement table. for _, mnem := range []string{"VADD", "VSUB", "VMUL", "VAND", "VEOR", "VORR", "VCMEQ", "VZIP1", "VZIP2"} { if _, ok := a64SimdVTable[mnem]; !ok { t.Errorf("%s not in the SIMD arrangement table", mnem) } } } // TestArm64CarryAndBitOps pins the carry-setting arithmetic, the widening // multiplies and the data-processing (1 source) group against go tool asm. func TestArm64CarryAndBitOps(t *testing.T) { got := arm64Words(t, "\tADC R0, R2, R12\n\tADCS $0, R1\n\tSBCS R5, R9, R5\n\tSBC R25, R10, R26\n"+ "\tMUL R4, R3, R0\n\tUMULH R24, R20, R24\n\tSMULH R1, R2, R3\n\tMSUB R19, R16, R26, R2\n"+ "\tRBIT R11, R4\n\tREV R1, R2\n\tCLZ R21, R9\n\tREVW R1, R2\n\tCLSW R1, R2\n") want := []uint32{ 0x9a00004c, // ADC R12, R2, R0 0xba1f0021, // ADCS R1, R1, ZR 0xfa050125, // SBCS R5, R9, R5 0xda19015a, // SBC R26, R10, R25 0x9b047c60, // MUL R0, R3, R4 0x9bd87e98, // UMULH R24, R20, R24 0x9b417c43, // SMULH R3, R2, R1 0x9b13c342, // MSUB R2, R26, R19, R16 0xdac00164, // RBIT R4, R11 0xdac00c22, // REV R2, R1 0xdac012a9, // CLZ R9, R21 0x5ac00822, // REVW R2, R1 0x5ac01422, // CLSW R2, R1 0xd65f03c0, // RET } if len(got) != len(want) { t.Fatalf("word count = %d, want %d", len(got), len(want)) } for i := range want { if got[i] != want[i] { t.Errorf("word %d = %08x, want %08x", i, got[i], want[i]) } } } // TestArm64BitfieldExtract pins UBFX/SBFX: immr wraps to the register // width, an out-of-range imms is an error. func TestArm64BitfieldExtract(t *testing.T) { got := arm64Words(t, "\tUBFX $33, R17, $25, R5\n\tUBFXW $4, R1, $9, R2\n") want := []uint32{ 0xd361e625, // UBFX immr=1 (33 wrapped), imms=25 0x53043022, // UBFXW immr=4, imms=9 0xd65f03c0, } if len(got) != len(want) { t.Fatalf("word count = %d, want %d", len(got), len(want)) } for i := range want { if got[i] != want[i] { t.Errorf("word %d = %08x, want %08x", i, got[i], want[i]) } } for _, body := range []string{"\tUBFX $33, R17, $70, R5\n", "\tUBFX $-1, R17, $3, R5\n"} { f, errs := parser.Parse("test_arm64.s", "#include \"textflag.h\"\n\nTEXT ·f(SB), NOSPLIT, $0-0\n"+body+"\tRET\n") if len(errs) > 0 { t.Fatalf("parse: %v", errs) } if _, err := AssembleFileARM64(f); err == nil { t.Errorf("%s: expected an error, got none", body) } } } // TestArm64CondCompare pins CCMP/CCMN. func TestArm64CondCompare(t *testing.T) { got := arm64Words(t, "\tCCMP LE, R7, $19, $3\n\tCCMP LT, R30, R6, $7\n\tCCMN EQ, R1, R2, $3\n\tCCMPW LE, R7, $19, $3\n") want := []uint32{ 0xfa53d8e3, // CCMP imm form 0xfa46b3c7, // CCMP register form 0xba420023, // CCMN register form 0x7a53d8e3, // CCMPW 0xd65f03c0, } if len(got) != len(want) { t.Fatalf("word count = %d, want %d", len(got), len(want)) } for i := range want { if got[i] != want[i] { t.Errorf("word %d = %08x, want %08x", i, got[i], want[i]) } } } // TestArm64CompareBranch pins CBZ/CBNZ/TBZ/TBNZ against a label five and // six words ahead, matching go tool asm's own offsets. func TestArm64CompareBranch(t *testing.T) { // Layout: CBZ(0) TBZ(4) TBNZ(8) CBNZ(12) NOOP(16) NOOP(17th word...) done. // The fillers are NOOP, not NOP: the toolchain's NOP emits nothing. src := "#include \"textflag.h\"\n\nTEXT ·f(SB), NOSPLIT, $0-0\n" + "\tCBZ R1, done\n\tTBZ $4, R7, done\n\tTBNZ $33, R7, done\n\tCBNZW R2, done\n" + "\tNOOP\n\tNOOP\n\tdone:\tNOOP\n\tRET\n" 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) } got := leWords(img.Code) // done sits at word 6 from each branch's own pc: CBZ rel 6, TBZ rel 5, // TBNZ rel 4, CBNZW rel 3. want := []uint32{ 0xb40000c1, // CBZ R1, +6 0x362000a7, // TBZ $4, R7, +5 0xb7080087, // TBNZ $33, R7, +4 0x35000062, // CBNZW R2, +3 0xd503201f, 0xd503201f, 0xd503201f, 0xd65f03c0, } if len(got) != len(want) { t.Fatalf("word count = %d, want %d", len(got), len(want)) } for i := range want { if got[i] != want[i] { t.Errorf("word %d = %08x, want %08x", i, got[i], want[i]) } } } // TestArm64ADR pins ADR against a forward label. func TestArm64ADR(t *testing.T) { src := "#include \"textflag.h\"\n\nTEXT ·f(SB), NOSPLIT, $0-0\n" + "\tADR done, R10\n\tNOOP\n\tNOOP\n\tdone:\tNOOP\n\tRET\n" 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) } got := leWords(img.Code) // rel = 12 bytes: immlo 0, immhi 3. want := []uint32{0x1000006a, 0xd503201f, 0xd503201f, 0xd503201f, 0xd65f03c0} if len(got) != len(want) { t.Fatalf("word count = %d, want %d", len(got), len(want)) } for i := range want { if got[i] != want[i] { t.Errorf("word %d = %08x, want %08x", i, got[i], want[i]) } } } // TestArm64PairLoadStore pins LDP/STP/LDPW/FLDPD/FSTPD and the 128-bit FP // pairs, whose offsets scale by sixteen. func TestArm64PairLoadStore(t *testing.T) { got := arm64Words(t, "\tSTP (R2, R3), 8(R5)\n\tLDP -8(R5), (R2, R3)\n\tLDPW 4(R0), (R1, R2)\n\tSTPW (R1, R2), 4(R0)\n"+ "\tFLDPD 8(R0), (F1, F2)\n\tFSTPD (F3, F4), -8(R5)\n"+ "\tFLDPQ 16(R0), (F1, F2)\n\tFSTPQ (F1, F2), 16(R0)\n"+ "\tFLDPS 4(R0), (F1, F2)\n\tFSTPS (F1, F2), -4(R0)\n") want := []uint32{ 0xa9008ca2, // STP (R2, R3), 8(R5) 0xa97f8ca2, // LDP -8(R5), (R2, R3) 0x29408801, // LDPW 4(R0), (R1, R2) 0x29008801, // STPW (R1, R2), 4(R0) 0x6d408801, // FLDPD 8(R0), (F1, F2) 0x6d3f90a3, // FSTPD (F3, F4), -8(R5) 0xad408801, // FLDPQ 16(R0), (F1, F2) 0xad008801, // FSTPQ (F1, F2), 16(R0) 0x2d408801, // FLDPS 4(R0), (F1, F2) 0x2d3f8801, // FSTPS (F1, F2), -4(R0) 0xd65f03c0, } if len(got) != len(want) { t.Fatalf("word count = %d, want %d", len(got), len(want)) } for i := range want { if got[i] != want[i] { t.Errorf("word %d = %08x, want %08x", i, got[i], want[i]) } } } // TestArm64AcquireRelease pins LDAR/STLR and the acquire/release LSE // families. func TestArm64AcquireRelease(t *testing.T) { got := arm64Words(t, "\tLDAR (R27), R22\n\tLDARB (R25), R2\n\tLDARW (R12), R29\n\tSTLR R3, (R24)\n\tSTLRB R11, (R22)\n"+ "\tCASALD R5, (R6), R7\n\tLDADDALD R5, (R6), R7\n\tLDCLRALB R5, (R6), R7\n\tLDORALD R5, (RSP), R7\n\tSWPALW R5, (R6), R7\n"+ "\tLDEORALD R5, (R6), R7\n\tLDEORALW R5, (RSP), R7\n"+ "\tCASALH ZR, (R5), R8\n") want := []uint32{ 0xc8dfff76, // LDAR R22, (R27) 0x08dfff22, // LDARB R2, (R25) 0x88dffd9d, // LDARW R29, (R12) 0xc89fff03, // STLR R3, (R24) 0x089ffecb, // STLRB R11, (R22) 0xc8e5fcc7, // CASALD R7, (R6), R5 0xf8e500c7, // LDADDALD R7, (R6), R5 0x38e510c7, // LDCLRALB R7, (R6), R5 0xf8e533e7, // LDORALD R7, (RSP), R5 0xb8e580c7, // SWPALW R7, (R6), R5 0xf8e520c7, // LDEORALD R7, (R6), R5 0xb8e523e7, // LDEORALW R7, (RSP), R5 0x48fffca8, // CASALH R8, (R5), ZR 0xd65f03c0, } if len(got) != len(want) { t.Fatalf("word count = %d, want %d", len(got), len(want)) } for i := range want { if got[i] != want[i] { t.Errorf("word %d = %08x, want %08x", i, got[i], want[i]) } } } // TestArm64QMove pins the Q-width FP move against the toolchain words the // corpus records: the plain, writeback and static-symbol load/store forms. // FMOVQ carries no register-to-register or immediate form, and both are // rejected the way the toolchain rejects them. func TestArm64QMove(t *testing.T) { got := arm64Words(t, "\tFMOVQ.P F13, 11(R10)\n\tFMOVQ.W F15, 11(R20)\n\tFMOVQ.P 11(R10), F13\n\tFMOVQ.W 11(R20), F15\n"+ "\tFMOVQ F0, 32(R5)\n\tFMOVQ F10, 65520(R10)\n\tFMOVQ 32(R5), F2\n") want := []uint32{ 0x3c80b54d, // FMOVQ.P F13, 11(R10) 0x3c80be8f, // FMOVQ.W F15, 11(R20) 0x3cc0b54d, // FMOVQ.P 11(R10), F13 0x3cc0be8f, // FMOVQ.W 11(R20), F15 0x3d8008a0, // FMOVQ F0, 32(R5) 0x3dbffd4a, // FMOVQ F10, 65520(R10) 0x3dc008a2, // FMOVQ 32(R5), F2 0xd65f03c0, } if len(got) != len(want) { t.Fatalf("word count = %d, want %d", len(got), len(want)) } for i := range want { if got[i] != want[i] { t.Errorf("word %d = %08x, want %08x", i, got[i], want[i]) } } for _, src := range []string{"\tFMOVQ F1, F2\n", "\tFMOVQ $1, R2\n", "\tFMOVQ $0, 8(R3)\n"} { f, errs := parser.Parse("test_arm64.s", "#include \"textflag.h\"\n\nTEXT ·f(SB), NOSPLIT, $0-0\n"+src+"\tRET\n") if len(errs) > 0 { t.Fatalf("parse %q: %v", src, errs) } if _, err := AssembleFileARM64(f); err == nil { t.Errorf("%q: assembled, want rejection", strings.TrimSpace(src)) } } } // TestArm64OffsetSplit pins the ADD/SUB-into-REGTMP fallbacks against the // toolchain words: the whole-offset form for ±4095 and the 24-bit hi/lo // split for the wide bands, on the MOV and pair families alike. func TestArm64OffsetSplit(t *testing.T) { got := arm64Words(t, "\tMOVD R1, 4094(R2)\n\tMOVD R1, -300(R2)\n\tMOVD R1, 0x1006ff8(R2)\n\tMOVB R1, 4096(R2)\n"+ "\tSTP (R3, R4), 11(R0)\n\tSTP (R3, R4), 65536(R2)\n\tLDP -31(R0), (R1, R2)\n") want := []uint32{ 0x913ff85b, // ADD $4094, R2, R27 0xf9000361, // MOVD R1, (R27) 0xd104b05b, // SUB $300, R2, R27 0xf9000361, // MOVD R1, (R27) 0x917ffc5b, // ADD $(4095<<12), R2, R27 0xf93fff61, // MOVD R1, 32760(R27) 0x9100045b, // ADD $1, R2, R27 0x393fff61, // MOVB R1, 4095(R27) 0x91002c1b, // ADD $11, R0, R27 0xa9001363, // STP (R3, R4), (R27) 0x9100005b, // ADD $0, R2, R27 0x9140437b, // ADD $(16<<12), R27, R27 0xa9001363, // STP (R3, R4), (R27) 0xd1007c1b, // SUB $31, R0, R27 0xa9400b61, // LDP (R27), (R1, R2) 0xd65f03c0, } if len(got) != len(want) { t.Fatalf("word count = %d, want %d", len(got), len(want)) } for i := range want { if got[i] != want[i] { t.Errorf("word %d = %08x, want %08x", i, got[i], want[i]) } } } // TestArm64BTI pins the landing-pad family against the toolchain words: // only the uppercase C/J/JC spellings assemble, and bare BTI is a // diagnostic, never a panic. func TestArm64BTI(t *testing.T) { got := arm64Words(t, "\tBTI C\n\tBTI J\n\tBTI JC\n") want := []uint32{ 0xd503245f, // BTI C 0xd503249f, // BTI J 0xd50324df, // BTI JC 0xd65f03c0, // RET } if len(got) != len(want) { t.Fatalf("got %d words, want %d", len(got), len(want)) } for i := range want { if got[i] != want[i] { t.Errorf("word %d = %#x, want %#x", i, got[i], want[i]) } } for _, src := range []string{"\tBTI\n", "\tBTI c\n", "\tBTI B\n"} { f, errs := parser.Parse("test_arm64.s", "#include \"textflag.h\"\n\nTEXT ·f(SB), NOSPLIT, $0-0\n"+src+"\tRET\n") if len(errs) > 0 { continue } if _, err := AssembleFileARM64(f); err == nil { t.Errorf("BTI spelling %q should be rejected, as go tool asm rejects it", src) } } } // TestArm64System pins BRK, SVC, the barriers, cache maintenance and the // system register accesses. func TestArm64System(t *testing.T) { got := arm64Words(t, "\tBRK $35943\n\tBRK\n\tSVC $7165\n\tDMB $1\n\tDSB $1\n\tISB $15\n"+ "\tDC ZVA, R4\n\tDC IVAC, R1\n\tMRS DCZID_EL0, R3\n\tMRS CNTVCT_EL0, R0\n\tMSR $9, DAIFSet\n\tMSR $3, SPSel\n"+ "\tPRFM (R0), PLDL1KEEP\n\tPRFM (R3), PLDL3KEEP\n\tPRFM (R2), $25\n") want := []uint32{ 0xd4318ce0, // BRK $35943 0xd4200000, // BRK 0xd4037fa1, // SVC $7165 0xd50331bf, // DMB $1 0xd503319f, // DSB $1 0xd5033fdf, // ISB $15 0xd50b7424, // DC ZVA, R4 0xd5087621, // DC IVAC, R1 0xd53b00e3, // MRS DCZID_EL0, R3 0xd53be040, // MRS CNTVCT_EL0, R0 0xd50349df, // MSR $9, DAIFSet 0xd50043bf, // MSR $3, SPSel 0xf9800000, // PRFM (R0), PLDL1KEEP 0xf9800064, // PRFM (R3), PLDL3KEEP 0xf9800059, // PRFM (R2), $25 0xd65f03c0, } if len(got) != len(want) { t.Fatalf("word count = %d, want %d", len(got), len(want)) } for i := range want { if got[i] != want[i] { t.Errorf("word %d = %08x, want %08x", i, got[i], want[i]) } } } // TestArm64Crypto pins the AES and SHA families. func TestArm64Crypto(t *testing.T) { got := arm64Words(t, "\tAESE V31.B16, V29.B16\n\tAESD V22.B16, V19.B16\n\tAESIMC V12.B16, V27.B16\n\tAESMC V14.B16, V28.B16\n"+ "\tSHA1C V8.S4, V8, V2\n\tSHA1H V17, V25\n\tSHA1P V3.S4, V20, V27\n\tSHA1SU0 V17.S4, V13.S4, V16.S4\n\tSHA1SU1 V24.S4, V23.S4\n"+ "\tSHA256H V4.S4, V2, V11\n\tSHA256H2 V6.S4, V16, V11\n\tSHA256SU0 V0.S4, V16.S4\n\tSHA256SU1 V31.S4, V3.S4, V15.S4\n"+ "\tSHA512H V2.D2, V1, V0\n\tSHA512H2 V4.D2, V3, V2\n\tSHA512SU0 V9.D2, V8.D2\n\tSHA512SU1 V7.D2, V6.D2, V5.D2\n") want := []uint32{ 0x4e284bfd, // AESE 0x4e285ad3, // AESD 0x4e28799b, // AESIMC 0x4e2869dc, // AESMC 0x5e080102, // SHA1C 0x5e280a39, // SHA1H 0x5e03129b, // SHA1P 0x5e1131b0, // SHA1SU0 0x5e281b17, // SHA1SU1 0x5e04404b, // SHA256H 0x5e06520b, // SHA256H2 0x5e282810, // SHA256SU0 0x5e1f606f, // SHA256SU1 0xce628020, // SHA512H 0xce648462, // SHA512H2 0xcec08128, // SHA512SU0 0xce6788c5, // SHA512SU1 0xd65f03c0, } if len(got) != len(want) { t.Fatalf("word count = %d, want %d", len(got), len(want)) } for i := range want { if got[i] != want[i] { t.Errorf("word %d = %08x, want %08x", i, got[i], want[i]) } } } // TestArm64SIMDLogical pins the arrangement-aware three- and two-register // SIMD paths. func TestArm64SIMDLogical(t *testing.T) { got := arm64Words(t, "\tVADD V1.B16, V2.B16, V3.B16\n\tVAND V4.B16, V4.B16, V9.B16\n\tVEOR V0.B16, V1.B16, V0.B16\n"+ "\tVORR V5.B16, V4.B16, V3.B16\n\tVADDP V1.H8, V2.H8, V3.H8\n\tVZIP1 V16.H8, V3.H8, V19.H8\n\tVZIP2 V22.D2, V25.D2, V21.D2\n"+ "\tVCMEQ V24.S4, V13.S4, V12.S4\n\tVCMEQ $0, V2.H4, V3.H4\n\tVREV32 V2.H8, V1.H8\n\tVREV64 V2.S4, V3.S4\n\tVUADDLV V31.S4, V11\n"+ "\tVPMULL V2.D1, V1.D1, V3.Q1\n\tVPMULL2 V2.B16, V1.B16, V4.H8\n\tVRAX1 V26.D2, V29.D2, V30.D2\n\tVMOV V2.B16, V4.B16\n") want := []uint32{ 0x4e218443, // VADD 16B 0x4e241c89, // VAND 0x6e201c20, // VEOR 0x4ea51c83, // VORR 0x4e61bc43, // VADDP 8H 0x4e503873, // VZIP1 8H 0x4ed67b35, // VZIP2 2D 0x6eb88dac, // VCMEQ 4S 0x0e609843, // VCMEQ $0, 4H 0x6e600841, // VREV32 8H 0x4ea00843, // VREV64 4S 0x6eb03beb, // VUADDLV 4S 0x0ee2e023, // VPMULL D1 0x4e22e024, // VPMULL2 16B 0xce7a8fbe, // VRAX1 2D 0x4ea21c44, // VMOV 16B pair 0xd65f03c0, } if len(got) != len(want) { t.Fatalf("word count = %d, want %d", len(got), len(want)) } for i := range want { if got[i] != want[i] { t.Errorf("word %d = %08x, want %08x", i, got[i], want[i]) } } } // TestArm64SIMDWide pins the four-register crypto group, VXAR, VEXT and the // shift-by-immediate encodings. func TestArm64SIMDWide(t *testing.T) { got := arm64Words(t, "\tVEOR3 V2.B16, V7.B16, V12.B16, V25.B16\n\tVBCAX V1.B16, V2.B16, V26.B16, V31.B16\n"+ "\tVXAR $63, V27.D2, V21.D2, V26.D2\n\tVEXT $4, V2.B8, V1.B8, V3.B8\n\tVEXT $8, V2.B16, V1.B16, V3.B16\n"+ "\tVSHL $7, V22.D2, V25.D2\n\tVUSHR $6, V22.H8, V23.H8\n\tVSRI $24, V1.S4, V2.S4\n") want := []uint32{ 0xce070999, // VEOR3 0xce22075f, // VBCAX 0xce9bfeba, // VXAR 0x2e022023, // VEXT B8 0x6e024023, // VEXT B16 0x4f4756d9, // VSHL D2 $7 0x6f1a06d7, // VUSHR H8 $6 0x6f284422, // VSRI S4 $24 0xd65f03c0, } if len(got) != len(want) { t.Fatalf("word count = %d, want %d", len(got), len(want)) } for i := range want { if got[i] != want[i] { t.Errorf("word %d = %08x, want %08x", i, got[i], want[i]) } } } // TestArm64SIMDElement pins VDUP and the VMOV element forms. func TestArm64SIMDElement(t *testing.T) { got := arm64Words(t, "\tVDUP V31.B[15], V18\n\tVDUP V19.S[3], V18.S4\n\tVDUP V1.D[1], V2.D2\n"+ "\tVMOV V13.S[0], R20\n\tVMOV V11.B[11], V16.B[12]\n\tVMOV R20, V21.B[2]\n") want := []uint32{ 0x5e1f07f2, // VDUP element to register 0x4e1c0672, // VDUP element across S4 0x4e180422, // VDUP element across D2 0x0e043db4, // VMOV element to register 0x6e195d70, // VMOV element to element 0x4e051e95, // VMOV register into element 0xd65f03c0, } if len(got) != len(want) { t.Fatalf("word count = %d, want %d", len(got), len(want)) } for i := range want { if got[i] != want[i] { t.Errorf("word %d = %08x, want %08x", i, got[i], want[i]) } } } // TestArm64GPIntoVector pins the whole-vector moves VMOV/VDUP Rs, Vd. // against `go tool asm -S` output (Go 1.27, arm64): word = Q | 7<<25 | // imm5<<16 | 3<<10 | rs<<5 | rd, shared by both mnemonics, the form // sys_windows_arm64.s and the bytealg loops use. The D1 destination is // rejected, as the toolchain rejects it. func TestArm64GPIntoVector(t *testing.T) { got := arm64Words(t, "\tVMOV R5, V5.B16\n\tVMOV R1, V2.B8\n\tVMOV R3, V4.H4\n"+ "\tVMOV R9, V10.S4\n\tVMOV R7, V31.H8\n\tVMOV R11, V12.D2\n"+ "\tVDUP R5, V5.B16\n\tVDUP R9, V10.H8\n\tVMOV V4.B16, V20.B16\n") want := []uint32{ 0x4e010ca5, // VMOV R5, V5.B16 0x0e010c22, // VMOV R1, V2.B8 0x0e020c64, // VMOV R3, V4.H4 0x4e040d2a, // VMOV R9, V10.S4 0x4e020cff, // VMOV R7, V31.H8 0x4e080d6c, // VMOV R11, V12.D2 0x4e010ca5, // VDUP R5, V5.B16 (same word as VMOV) 0x4e020d2a, // VDUP R9, V10.H8 0x4ea41c94, // VMOV V4.B16, V20.B16 (vector to vector stays ORR) 0xd65f03c0, } if len(got) != len(want) { t.Fatalf("word count = %d, want %d", len(got), len(want)) } for i := range want { if got[i] != want[i] { t.Errorf("word %d = %08x, want %08x", i, got[i], want[i]) } } f, errs := parser.Parse("test_arm64.s", "#include \"textflag.h\"\n\nTEXT ·f(SB), NOSPLIT, $0-0\n\tVMOV R7, V8.D1\n\tRET\n") if len(errs) > 0 { t.Fatalf("parse: %v", errs) } if _, err := AssembleFileARM64(f); err == nil { t.Errorf("VMOV R7, V8.D1 assembled, want an arrangement error") } } // TestArm64SimdTwoOperand pins the two-operand accumulate spellings // VADD/VSUB Vm, Vn against `go tool asm -S` output (Go 1.27, arm64): // word = 5<<28|7<<25|7<<21|1<<15|1<<10 for VADD (7<<28 for VSUB) with // rf<<16 | rn<<5 | rn, bare V registers only (asm7.go case 89). func TestArm64SimdTwoOperand(t *testing.T) { got := arm64Words(t, "\tVADD V7, V8\n\tVSUB V7, V8\n\tVADD V1, V2\n\tVADD V0.B16, V1.B16, V2.B16\n") want := []uint32{ 0x5ee78508, // VADD V7, V8 0x7ee78508, // VSUB V7, V8 0x5ee18442, // VADD V1, V2 0x4e208422, // VADD arranged: the ordinary three-register path 0xd65f03c0, } if len(got) != len(want) { t.Fatalf("word count = %d, want %d", len(got), len(want)) } for i := range want { if got[i] != want[i] { t.Errorf("word %d = %08x, want %08x", i, got[i], want[i]) } } } // TestArm64TruncMove pins the truncating register moves against // `go tool asm -S` output (Go 1.27, arm64): the signed forms lower to SXTB, // SXTH and SXTW (SBFM), the unsigned byte and halfword forms to UXTB and // UXTH (UBFM), MOVWU to a W ORR, and a narrow move out of the zero register // drops to the W ORR too (asm7.go case 45). func TestArm64TruncMove(t *testing.T) { got := arm64Words(t, "\tMOVB R3, R4\n\tMOVH R5, R6\n\tMOVW R9, R10\n"+ "\tMOVBU R3, R4\n\tMOVHU R3, R4\n\tMOVWU R3, R4\n\tMOVD R3, R4\n"+ "\tMOVD ZR, R4\n\tMOVB ZR, R4\n\tMOVWU ZR, R5\n") want := []uint32{ 0x93401c64, // MOVB = SXTB 0x93403ca6, // MOVH = SXTH 0x93407d2a, // MOVW = SXTW 0xd3401c64, // MOVBU = UXTB 0xd3403c64, // MOVHU = UXTH 0x2a0303e4, // MOVWU = ORR W 0xaa0303e4, // MOVD = ORR X 0xaa1f03e4, // MOVD ZR, R4 keeps the X form 0x2a1f03e4, // MOVB ZR, R4 drops to the W form 0x2a1f03e5, // MOVWU ZR, R5 0xd65f03c0, } if len(got) != len(want) { t.Fatalf("word count = %d, want %d", len(got), len(want)) } for i := range want { if got[i] != want[i] { t.Errorf("word %d = %08x, want %08x", i, got[i], want[i]) } } } // TestArm64SIMDLoadStore pins the structure loads and stores. func TestArm64SIMDLoadStore(t *testing.T) { got := arm64Words(t, "\tVLD1 (R2), [V21.B16]\n\tVLD1 (R1), [V2.B16, V3.B16]\n\tVLD1 (R29), [V14.D1, V15.D1, V16.D1, V17.D1]\n"+ "\tVLD1.P 32(R1), [V2.B16, V3.B16]\n\tVST1 [V2.S4, V3.S4, V4.S4, V5.S4], (R14)\n\tVST1.P [V2.B16], (R1)\n"+ "\tVLD1R (R1), [V9.B8]\n\tVLD4R (R0), [V0.B8, V1.B8, V2.B8, V3.B8]\n") want := []uint32{ 0x4c407055, // VLD1 one register 0x4c40a022, // VLD1 two registers 0x0c402fae, // VLD1 four registers D1 0x4cdfa022, // VLD1.P two registers 0x4c0029c2, // VST1 four registers S4 0x4c9f7022, // VST1.P one register 0x0d40c029, // VLD1R 0x0d60e000, // VLD4R 0xd65f03c0, } if len(got) != len(want) { t.Fatalf("word count = %d, want %d", len(got), len(want)) } for i := range want { if got[i] != want[i] { t.Errorf("word %d = %08x, want %08x", i, got[i], want[i]) } } } // TestArm64MoviLiteral pins the VMOVS/VMOVD/VMOVQ constant loads: three // words each (ADRP, ADD, wide load) plus the pooled literal in the data // section. func TestArm64MoviLiteral(t *testing.T) { src := "#include \"textflag.h\"\n\nTEXT ·f(SB), NOSPLIT, $0-0\n" + "\tVMOVS $0x80402010, V11\n\tVMOVD $0x8040201008040201, V20\n" + "\tVMOVQ $0x7040201008040201, $0x8040201008040201, V10\n\tRET\n" 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 img.Funcs[0].Size != 12*3+4 { t.Errorf("func size = %d, want %d", img.Funcs[0].Size, 12*3+4) } want := []uint32{ 0x9000001b, 0x9100037b, 0xbd40036b, // VMOVS: ADRP, ADD, LDR S 0x9000001b, 0x9100037b, 0xfd400374, // VMOVD: ADRP, ADD, LDR D 0x9000001b, 0x9100037b, 0x3dc0036a, // VMOVQ: ADRP, ADD, LDR Q 0xd65f03c0, } got := leWords(img.Code) if len(got) != len(want) { t.Fatalf("word count = %d, want %d", len(got), len(want)) } for i := range want { if got[i] != want[i] { t.Errorf("word %d = %08x, want %08x", i, got[i], want[i]) } } // The literals sit in the data section. var found32, found64, found128 bool for _, d := range img.DataSyms { switch d.Name { case "$i32.80402010": found32 = d.Size == 4 case "$i64.8040201008040201": found64 = d.Size == 8 case "$i128.80402010080402017040201008040201": found128 = d.Size == 16 } } if !found32 || !found64 || !found128 { t.Errorf("literals missing: i32=%v i64=%v i128=%v", found32, found64, found128) } } // TestArm64MOVK pins standalone MOVK with the hw field derived from the // chunk position. func TestArm64MOVK(t *testing.T) { got := arm64Words(t, "\tMOVK $1234, R5\n\tMOVK $305397760, R5\n\tMOVKW $1234, R5\n") want := []uint32{ 0xf2809a45, // MOVK hw=0 0xf2a24685, // MOVK hw=1 0x72809a45, // MOVKW hw=0 0xd65f03c0, } if len(got) != len(want) { t.Fatalf("word count = %d, want %d", len(got), len(want)) } for i := range want { if got[i] != want[i] { t.Errorf("word %d = %08x, want %08x", i, got[i], want[i]) } } } // TestArm64MOVKHighLane pins the shifted high-lane immediate the arm64 test // kernels write: $(40000<<48) folds to a negative int64, and the toolchain // reads the value as an unsigned 64-bit pattern when it picks the lane. func TestArm64MOVKHighLane(t *testing.T) { got := arm64Words(t, "\tMOVK $(40000<<48), R0\n\tMOVK $0x9c40000000000000, R1\n") want := []uint32{ 0xf2f38800, // MOVK $(40000<<48), R0 (go tool asm: f2f38800) 0xf2f38801, // MOVK hw=3 0xd65f03c0, } if len(got) != len(want) { t.Fatalf("word count = %d, want %d", len(got), len(want)) } for i := range want { if got[i] != want[i] { t.Errorf("word %d = %08x, want %08x", i, got[i], want[i]) } } } // TestArm64MoveWideZeroImmediate pins the toolchain's rejection of a zero // immediate in the move-wide family (optab case 33: "zero shifts cannot be // handled"): every lane is zero, so no hw field can carry it. func TestArm64MoveWideZeroImmediate(t *testing.T) { for _, mnem := range []string{"MOVK", "MOVZ", "MOVN"} { f, errs := parser.Parse("test_arm64.s", "#include \"textflag.h\"\n\nTEXT ·f(SB), NOSPLIT, $0-0\n\t"+mnem+" $0, R0\n\tRET\n") if len(errs) > 0 { t.Fatalf("%s: parse: %v", mnem, errs) } if _, err := AssembleFileARM64(f); err == nil { t.Errorf("%s $0: expected error, got nil", mnem) } } } // TestArm64LoadImm64 tests 64-bit immediate loading. func TestArm64LoadImm64(t *testing.T) { src := `#include "textflag.h" TEXT ·f(SB), NOSPLIT, $0-0 MOVD $0x123456789ABCDEF0, R0 MOVD $0, R1 MOVD $1, R2 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) } // $0x123456789ABCDEF0 needs 4 MOVZ/MOVK instructions (16 bytes) // $0 is 1 instruction (4 bytes) // $1 is 1 bitmask instruction (4 bytes) // RET is 1 instruction (4 bytes) if img.Funcs[0].Size != 28 { t.Errorf("size: got %d, want 28", img.Funcs[0].Size) } } // TestArm64BranchCond tests conditional branch encoding. func TestArm64BranchCond(t *testing.T) { src := `#include "textflag.h" TEXT ·f(SB), NOSPLIT, $0-0 BEQ done BNE done BGE done BLT done 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) } // 4 branches + 1 ADD + 1 RET = 24 bytes if img.Funcs[0].Size != 24 { t.Errorf("size: got %d, want 24", img.Funcs[0].Size) } } // TestArm64Errors tests error paths. func TestArm64Errors(t *testing.T) { tests := []struct { name string src string }{ {"bad mnemonic", "TEXT ·f(SB), NOSPLIT, $0-0\n\tINVALID\tR4\n\tRET\n"}, {"bad label", "TEXT ·f(SB), NOSPLIT, $0-0\n\tB\tnosuch\n\tRET\n"}, {"bad register", "TEXT ·f(SB), NOSPLIT, $0-0\n\tADD\tR99, R0\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 } _, err := AssembleFileARM64(f) if err == nil { t.Error("expected error, got nil") } }) } } // 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 } // TestArm64IndirectBranch pins the indirect branch forms in a leaf function: // JMP (Rn) lowers to BR Rn, matching the toolchain's spelling, and the raw // BR/BLR mnemonics encode directly (a gasm superset the toolchain's front // end does not accept). CALL (Rn) shares the BLR path and its non-leaf // prologue parity is covered by the ground-truth kernel. func TestArm64IndirectBranch(t *testing.T) { src := `#include "textflag.h" TEXT ·f(SB), NOSPLIT, $0-0 JMP (R0) BR R5 BLR 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) } want := []uint32{ 0xd61f0000, // BR R0 0xd61f00a0, // BR R5 0xd63f00c0, // BLR R6 0xd65f03c0, // RET (BR LR) } got := leWords(img.Code) if len(got) != len(want) { t.Fatalf("word count = %d, want %d", len(got), len(want)) } for i := range want { if got[i] != want[i] { t.Errorf("word %d = %08x, want %08x", i, got[i], want[i]) } } } // arm64Words assembles a single NOSPLIT leaf body and returns its words. func arm64Words(t *testing.T, body string) []uint32 { t.Helper() f, errs := parser.Parse("test_arm64.s", "#include \"textflag.h\"\n\nTEXT ·f(SB), NOSPLIT, $0-0\n"+body+"\tRET\n") if len(errs) > 0 { t.Fatalf("parse: %v", errs) } img, err := AssembleFileARM64(f) if err != nil { t.Fatalf("AssembleFileARM64: %v", err) } return leWords(img.Code) } // TestArm64ShiftEncodings pins the shift words against `go tool asm -S` // output (Go 1.27, arm64): immediate forms alias SBFM/UBFM with ROR as EXTR, // register forms are the two-source LSLV/LSRV/ASRV/RORV. func TestArm64ShiftEncodings(t *testing.T) { got := arm64Words(t, "\tLSL $4, R0, R1\n\tLSR $8, R0, R2\n\tASR $4, R0, R3\n\tROR $12, R0, R4\n"+ "\tLSLW $4, R0, R5\n\tLSRW $8, R0, R6\n\tASRW $4, R0, R7\n\tRORW $12, R0, R8\n") want := []uint32{ 0xd37cec01, // LSL $4 = UBFM X1, X0, #60, #59 0xd348fc02, // LSR $8 = UBFM X2, X0, #8, #63 0x9344fc03, // ASR $4 = SBFM X3, X0, #4, #63 0x93c03004, // ROR $12 = EXTR X4, X0, X0, #12 0x531c6c05, // LSLW $4 = UBFM W5, W0, #28, #27 0x53087c06, // LSRW $8 = UBFM W6, W0, #8, #31 0x13047c07, // ASRW $4 = SBFM W7, W0, #4, #31 0x13803008, // RORW $12 = EXTR W8, W0, W0, #12 0xd65f03c0, // RET } if len(got) != len(want) { t.Fatalf("word count = %d, want %d", len(got), len(want)) } for i := range want { if got[i] != want[i] { t.Errorf("imm shift word %d = %08x, want %08x", i, got[i], want[i]) } } got = arm64Words(t, "\tLSL R9, R0, R10\n\tLSR R9, R0, R11\n\tASR R9, R0, R12\n\tROR R9, R0, R13\n"+ "\tLSLW R9, R0, R14\n\tLSRW R9, R0, R15\n\tASRW R9, R0, R16\n\tRORW R9, R0, R17\n") want = []uint32{ 0x9ac9200a, // LSLV X10, X0, X9 0x9ac9240b, // LSRV X11, X0, X9 0x9ac9280c, // ASRV X12, X0, X9 0x9ac92c0d, // RORV X13, X0, X9 0x1ac9200e, // LSLV W14, W0, W9 0x1ac9240f, // LSRV W15, W0, W9 0x1ac92810, // ASRV W16, W0, W9 0x1ac92c11, // RORV W17, W0, W9 0xd65f03c0, // RET } for i := range want { if got[i] != want[i] { t.Errorf("reg shift word %d = %08x, want %08x", i, got[i], want[i]) } } // Two-operand spellings fold to Rn = Rd. got = arm64Words(t, "\tLSL $4, R1\n\tLSR R9, R1\n\tASR $4, R1\n\tROR R9, R1\n\tLSLW $4, R1\n\tRORW R9, R1\n") want = []uint32{ 0xd37cec21, // LSL $4, R1 = UBFM X1, X1, #60, #59 0x9ac92421, // LSRV X1, X1, X9 0x9344fc21, // ASR $4, R1 = SBFM X1, X1, #4, #63 0x9ac92c21, // RORV X1, X1, X9 0x531c6c21, // LSLW $4, R1 = UBFM W1, W1, #28, #27 0x1ac92c21, // RORV W1, W1, W9 0xd65f03c0, // RET } for i := range want { if got[i] != want[i] { t.Errorf("2op shift word %d = %08x, want %08x", i, got[i], want[i]) } } } // TestArm64ShiftRangeErrors: the toolchain reports "illegal bit number" for // shift amounts at or above the operand width. func TestArm64ShiftRangeErrors(t *testing.T) { for _, src := range []string{ "\tLSL $64, R0, R1\n", "\tLSRW $32, R0, R1\n", "\tRORW $32, R0, R1\n", "\tASR $-1, R0, R1\n", } { f, errs := parser.Parse("test_arm64.s", "#include \"textflag.h\"\n\nTEXT ·f(SB), NOSPLIT, $0-0\n"+src+"\tRET\n") if len(errs) > 0 { t.Fatalf("parse: %v", errs) } if _, err := AssembleFileARM64(f); err == nil { t.Errorf("%s: expected an error, got none", src) } } } // TestArm64DivEncodings pins SDIV/UDIV in both widths: the 2-source opcode // field (bits 15:10 of the 0xd6<<21 fixed field) is UDIV=0b0010, SDIV=0b0011. func TestArm64DivEncodings(t *testing.T) { got := arm64Words(t, "\tSDIV R1, R2, R3\n\tUDIV R1, R2, R3\n\tSDIVW R1, R2, R3\n\tUDIVW R1, R2, R3\n") want := []uint32{ 0x9ac10c43, // SDIV X3, X2, X1 0x9ac10843, // UDIV X3, X2, X1 0x1ac10c43, // SDIV W3, W2, W1 0x1ac10843, // UDIV W3, W2, W1 0xd65f03c0, // RET } if len(got) != len(want) { t.Fatalf("word count = %d, want %d", len(got), len(want)) } for i := range want { if got[i] != want[i] { t.Errorf("div word %d = %08x, want %08x", i, got[i], want[i]) } } } // TestArm64MAddSub pins the four-operand MADD/MSUB words (Rm, Ra, Rn, Rd, // with Ra in bits 14:10) and rejects the shorter spellings the toolchain // also rejects. func TestArm64MAddSub(t *testing.T) { got := arm64Words(t, "\tMADD R1, R2, R3, R4\n\tMSUB R1, R2, R3, R4\n\tMADDW R1, R2, R3, R5\n\tMSUBW R1, R2, R3, R5\n") want := []uint32{ 0x9b010864, // MADD X4, X3, X1, X2 (Rm=1, Ra=2, Rn=3) 0x9b018864, // MSUB X4, X3, X1, X2 0x1b010865, // MADD W5, W3, W1, W2 0x1b018865, // MSUB W5, W3, W1, W2 0xd65f03c0, // RET } if len(got) != len(want) { t.Fatalf("word count = %d, want %d", len(got), len(want)) } for i := range want { if got[i] != want[i] { t.Errorf("madd word %d = %08x, want %08x", i, got[i], want[i]) } } // The accumulate operand is mandatory: 2- and 3-operand forms error // rather than silently reading R0 or ZR as the accumulator. for _, body := range []string{ "\tMADD R1, R2\n", "\tMADD R1, R2, R3\n", "\tMSUBW R1, R2, R3\n", } { f, errs := parser.Parse("test_arm64.s", "#include \"textflag.h\"\n\nTEXT ·f(SB), NOSPLIT, $0-0\n"+body+"\tRET\n") if len(errs) > 0 { t.Fatalf("parse: %v", errs) } if _, err := AssembleFileARM64(f); err == nil { t.Errorf("%s: expected an error, got none", body) } } } // TestArm64MovImmWidth pins the immediate classifications whose size pass // once disagreed with the encoder: negative and 0xFFFFFFFF W values go // through MOVN after 32-bit truncation, and 3- to 4-chunk constants expand // to one word per non-zero chunk. func TestArm64MovImmWidth(t *testing.T) { got := arm64Words(t, "\tMOVW $-1, R0\n\tMOVW $0xFFFFFFFF, R3\n") want := []uint32{ 0x12800000, // MOVN W0, #0 0x12800003, // MOVN W3, #0 0xd65f03c0, // RET } for i := range want { if got[i] != want[i] { t.Errorf("movw word %d = %08x, want %08x", i, got[i], want[i]) } } for _, tt := range []struct { body string words int }{ {"\tMOVD $0x0001000200030000, R2\n", 3}, // three chunks {"\tMOVD $0x0001000200030004, R1\n", 4}, // four chunks {"\tMOVW $-1, R0\n", 1}, // MOVN after truncation } { if got := arm64Words(t, tt.body); len(got) != tt.words+1 { t.Errorf("%s: %d words, want %d (including RET)", tt.body, len(got), tt.words+1) } } } // TestArm64ExclOffsetErrors: exclusive and atomic encodings carry no // immediate field, so a non-zero offset is rejected the way the toolchain // reports "illegal combination" for it, never silently dropped. func TestArm64ExclOffsetErrors(t *testing.T) { for _, body := range []string{ "\tLDXR 8(R1), R2\n", "\tLDAXR 8(R1), R2\n", "\tSTXR R3, 8(R1), R4\n", "\tSTLXR R3, 8(R1), R4\n", "\tCASD R3, 8(R1), R4\n", "\tLDADDD R3, 8(R1), R4\n", } { f, errs := parser.Parse("test_arm64.s", "#include \"textflag.h\"\n\nTEXT ·f(SB), NOSPLIT, $0-0\n"+body+"\tRET\n") if len(errs) > 0 { t.Fatalf("parse: %v", errs) } if _, err := AssembleFileARM64(f); err == nil { t.Errorf("%s: expected an error, got none", body) } } } // TestArm64ExclNoOffset pins the plain (Rn) forms, byte-for-byte against // go tool asm. The toolchain parses the FIRST register of a store as the // data register and the LAST as the status register (asm7.go case 59), and // the pair forms as (Rt1, Rt2) (case 58/59): // // STXR R3, (R1), R4 → c8047c23 (Rt=3, Rn=1, Rs=4) // STXP (R3, R4), (R1), R5 → c8251023 (Rt=3, Rt2=4, Rn=1, Rs=5) // LDXP (R1), (R3, R4) → c87f1023 (Rn=1, Rt=3, Rt2=4) func TestArm64ExclNoOffset(t *testing.T) { got := arm64Words(t, "\tLDXR (R1), R2\n\tSTXR R3, (R1), R4\n"+ "\tSTXP (R3, R4), (R1), R5\n\tSTXPW (R3, R4), (R1), R5\n"+ "\tLDXP (R1), (R3, R4)\n\tLDXPW (R1), (R3, R4)\n"+ "\tSTXR R3, (RSP), R4\n\tLDXR (RSP), R2\n") want := []uint32{ 0xc85f7c22, // LDXR X2, [X1] 0xc8047c23, // STXR W3, [X1], W4 with Rt = R3, Rs = R4 0xc8251023, // STXP (R3, R4), [X1], R5 0x88251023, // STXPW (R3, R4), [X1], R5 0xc87f1023, // LDXP [X1], (R3, R4) 0x887f1023, // LDXPW [X1], (R3, R4) 0xc8047fe3, // STXR R3, [SP], R4 0xc85f7fe2, // LDXR [SP], R2 0xd65f03c0, // RET } for i := range want { if got[i] != want[i] { t.Errorf("excl word %d = %08x, want %08x", i, got[i], want[i]) } } } // TestArm64AddSubImmWide pins the wide-immediate classification the toolchain // applies to the ADD/SUB family (asm7.go cases 48, 62, 13): the ADDCON2 split // into two imm12 instructions for plain ADD/SUB, the bitmask ORR into REGTMP, // and the MOVZ/MOVN/MOVK materialisations followed by the register form. // Comparisons never split, and the W forms classify the 32-bit value. Every // word is go tool asm's own for the same source. func TestArm64AddSubImmWide(t *testing.T) { got := arm64Words(t, strings.Join([]string{ "\tADD $0xaaaaaa, R2, R3", "\tSUB $0xaaaaaa, R2", "\tADD $0x186a0, R2, R5", "\tADD $0x1ffe00, R2, R3", "\tADD $0x3fffffffc000, R5", "\tADD $-100000, R2, R3", "\tADD $-2048, R2, R3", "\tCMP $0xaaaaaa, R2", "\tCMP $0xffffffffffa0, R3", "\tCMPW $27745, R2", "\tCMPW $0x60060, R2", "\tADDS $0xaaaaaa, R2, R3", "\tADD $0x12345678, R2, R3", "\tADDW $0x60060, R2", "\tSUB $0xe7791f700, R3, R1", "\tADDW $0x12345678, R2, R3", "\tCMN $0x1000000, R2", }, "\n")+"\n") want := []uint32{ 0x912aa843, 0x916aa863, // ADD $0xaaaaaa, R2, R3: ADDCON2 split 0xd12aa842, 0xd16aa842, // SUB $0xaaaaaa, R2: split with Rd = Rn 0x911a8045, 0x914060a5, // ADD $0x186a0, R2, R5: split 0xb2772ffb, 0x8b1b0043, // ADD $0x1ffe00: bitmask beats the split 0xb2727ffb, 0x8b1b00a5, // ADD $0x3fffffffc000: bitmask into REGTMP 0x9290d3fb, 0xf2bfffdb, 0x8b1b0043, // ADD $-100000: MOVN + MOVK 0x9280fffb, 0x8b1b0043, // ADD $-2048: single MOVN + ADD 0xd295555b, 0xf2a0155b, 0xeb1b005f, // CMP: never split, MOVZ + MOVK 0x92800bfb, 0xf2e0001b, 0xeb1b007f, // CMP $0xffffffffffa0: MOVN + fixup 0x528d8c3b, 0x6b1b005f, // CMPW $27745: W movcon, single MOVZW 0x52800c1b, 0x72a000db, 0x6b1b005f, // CMPW $0x60060: S form skips the split 0xd295555b, 0xf2a0155b, 0xab1b0043, // ADDS $0xaaaaaa: MOVZ + MOVK + ADDS 0xd28acf1b, 0xf2a2469b, 0x8b1b0043, // ADD $0x12345678: MOVZ + MOVK 0x11018042, 0x11418042, // ADDW $0x60060: W split 0xd29ee01b, 0xf2aef23b, 0xf2c001db, 0xcb1b0061, // SUB $0xe7791f700 0x528acf1b, 0x72a2469b, 0x0b1b0043, // ADDW $0x12345678: MOVZW + MOVKW 0xd2a0201b, 0xab1b005f, // CMN $0x1000000: single MOVZ + CMN 0xd65f03c0, // RET } if len(got) != len(want) { t.Fatalf("word count = %d, want %d", len(got), len(want)) } for i := range want { if got[i] != want[i] { t.Errorf("wide word %d = %08x, want %08x", i, got[i], want[i]) } } } // TestArm64CarryImmWide pins the carry family's $0 spellings in two and // three operands, the ROR shift on the logical group (and its rejection for // the arithmetic forms), the NGC/MNEG zero-register aliases and the vector // alias with an element selector. Words are go tool asm's own. func TestArm64CarryShiftAlias(t *testing.T) { got := arm64Words(t, "\tADC $0, R20\n\tADC $0, R20, R4\n\tSBCS $0, R4, R12\n"+ "\tSBCS R15, R4, R12\n\tANDW R9@>7, R19, R26\n\tAND R1@>33, R2, R3\n"+ "\tNEGSW R23<<1, R30\n\tNGC R2, R7\n\tMNEG R14, R27, R23\n") want := []uint32{ 0x9a1f0294, // ADC ZR, R20, R20 0x9a1f0284, // ADC ZR, R20, R4 0xfa1f008c, // SBCS ZR, R4, R12 0xfa0f008c, // SBCS R15, R4, R12 0x0ac91e7a, // ANDW R9 ROR 7, R19, R26 0x8ac18443, // AND R1 ROR 33, R2, R3 0x6b1707fe, // SUBSW ZR, R30, R23 LSL 1 0xda0203e7, // SBC ZR, R7, R2 0x9b0eff77, // MSUB ZR, R27, R14, R23 0xd65f03c0, // RET } if len(got) != len(want) { t.Fatalf("word count = %d, want %d", len(got), len(want)) } for i := range want { if got[i] != want[i] { t.Errorf("carry word %d = %08x, want %08x", i, got[i], want[i]) } } // ROR on an arithmetic form is unallocated: the toolchain reports an // unsupported shift operator. f, errs := parser.Parse("test_arm64.s", "#include \"textflag.h\"\n\nTEXT ·f(SB), NOSPLIT, $0-0\n\tADD R1@>33, R2, R3\n\tRET\n") if len(errs) > 0 { t.Fatalf("parse: %v", errs) } if _, err := AssembleFileARM64(f); err == nil { t.Error("ADD R1@>33: expected an error, got none") } } // TestArm64VecAliasElement pins the register-alias rewrite inside a vector // operand with an element selector and inside a split register list: the // aliases resolve textually where the parser carries the selector apart from // the name. Words are go tool asm's own. func TestArm64VecAliasElement(t *testing.T) { src := `#include "textflag.h" #define POLY V15 #define ACC0 V8 #define ACC1 V9 TEXT ·f(SB), NOSPLIT, $0-0 VMOV R1, POLY.D[0] VEOR POLY.B16, POLY.B16, POLY.B16 VLD1 (R0), [ACC0.B16] VLD1.P (R0), [ACC0.B16, ACC1.B16] VST1.P [ACC0.B16, ACC1.B16], 32(R1) 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) } got := leWords(img.Code) want := []uint32{ 0x4e081c2f, // INS V15.D[0], R1 0x6e2f1def, // VEOR V15.B16, V15.B16, V15.B16 0x4c407008, // VLD1 (R0), [V8.B16] 0x4cdfa008, // VLD1.P (R0), [V8.B16, V9.B16] 0x4c9fa028, // VST1.P [V8.B16, V9.B16], 32(R1) 0xd65f03c0, // RET } if len(got) != len(want) { t.Fatalf("word count = %d, want %d", len(got), len(want)) } for i := range want { if got[i] != want[i] { t.Errorf("vecalias word %d = %08x, want %08x", i, got[i], want[i]) } } } // TestArm64AddSubImmBeyond32 pins the materialisation the toolchain applies // once the value leaves every imm12 form: a constant sequence into REGTMP // (R27) followed by the register form. SUB $-0x100000000 is a bitmask // immediate, so it rides the ORR form; the others take MOVZ. Words are go // tool asm's own. func TestArm64AddSubImmBeyond32(t *testing.T) { got := arm64Words(t, "\tADD $0x100000000, R0, R1\n\tSUB $-0x100000000, R0, R1\n\tCMP $0x100000000, R0\n") want := []uint32{ 0xd2c0003b, // MOVZ $(1<<32>>16), R27 (hw=2) 0x8b1b0001, // ADD R27, R0, R1 0xb2607ffb, // ORR $-4294967296, ZR, R27 (bitmask) 0xcb1b0001, // SUB R27, R0, R1 0xd2c0003b, // MOVZ $(1<<32>>16), R27 (hw=2) 0xeb1b001f, // CMP R27, R0 0xd65f03c0, // RET } if len(got) != len(want) { t.Fatalf("word count = %d, want %d", len(got), len(want)) } for i := range want { if got[i] != want[i] { t.Errorf("word %d = %08x, want %08x", i, got[i], want[i]) } } } // TestArm64LargeRegisterOffset pins the large-offset path for a register // base: the ADD offsets from the operand's own base, not from SP, matching // the toolchain's `ADD $(256<<12), R2, R27; MOVD (R27), R3`. func TestArm64LargeRegisterOffset(t *testing.T) { got := arm64Words(t, "\tMOVD 0x100000(R2), R3\n\tMOVD R3, 0x100000(R2)\n") want := []uint32{ 0x9144005b, // ADD $(256<<12), R2, R27 0xf9400363, // MOVD (R27), R3 0x9144005b, // ADD $(256<<12), R2, R27 0xf9000363, // MOVD R3, (R27) 0xd65f03c0, // RET } if len(got) != len(want) { t.Fatalf("word count = %d, want %d", len(got), len(want)) } for i := range want { if got[i] != want[i] { t.Errorf("large offset word %d = %08x, want %08x", i, got[i], want[i]) } } } // TestArm64LargeFrameSpadj checks the stack-adjustment boundaries of a frame // whose autosize must be materialised into REGTMP: $5000 rounds the autosize // to 5024, so the prologue is [MOVD $5024, R27][SUB R27, RSP, R20][STP][ADD // R20, SP][SUB $8] and SP moves only at its fourth word, while the RET's // epilogue is [LDP][MOVD $5024, R27][ADD R27, RSP, RSP] before the final // RET. These PCs feed the DWARF CFA rules and the goobj stack maps. func TestArm64LargeFrameSpadj(t *testing.T) { f, errs := parser.Parse("frame_arm64.s", "#include \"textflag.h\"\n\nTEXT ·framed(SB), $5000-0\n\tCALL ·other(SB)\n\tRET\n\nTEXT ·other(SB), NOSPLIT, $0\n\tRET\n") 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] // autosize 5024: class-2 guard of 6 words (24 bytes), a 5-word prologue // whose ADD R20, SP sits at byte 8 inside it, a one-instruction body, // then a 3-word epilogue before the final RET. wantSpadj := []SpadjStep{{PC: 24 + 12, Value: 5024}, {PC: 24 + 20 + 4 + 12, Value: 0}} if len(fn.Spadj) != len(wantSpadj) { t.Fatalf("spadj = %v, want %v", fn.Spadj, wantSpadj) } for i := range wantSpadj { if fn.Spadj[i] != wantSpadj[i] { t.Errorf("spadj[%d] = %v, want %v", i, fn.Spadj[i], wantSpadj[i]) } } // The words those PCs point between: the prologue's ADD R20, SP at byte // 36, and the epilogue's materialised ADD R27, RSP, RSP right before the // final RET at byte 60. words := leWords(img.Code[fn.Offset : fn.Offset+fn.Size]) if got := words[(24+12)/4]; got != 0x9100029f { t.Errorf("prologue word at byte 36 = %08x, want 9100029f (ADD R20, SP)", got) } if got := words[(24+20+4+8)/4]; got != 0x8b3b63ff { t.Errorf("epilogue word at byte 56 = %08x, want 8b3b63ff (ADD R27, RSP, RSP)", got) } if got := words[(24+20+4+12)/4]; got != 0xd65f03c0 { t.Errorf("final RET word at byte 60 = %08x, want d65f03c0", got) } } // TestArm64SplitFrameSpadj pins the addcon2 band, where neither imm12 form // nor a single MOVZ carries the autosize and the toolchain splits the // prologue SUB into two imm12 instructions (asm7.go case 48) while the // non-leaf RET still materialises the value into REGTMP (obj7.go ARET, // issue 73259). $65664 rounds the autosize to 65680 = 144 + 16<<12: // // [SUB $144, RSP, R20][SUB $(16<<12), R20, R20][STP][MOVD R20, SP][SUB $8] // [CALL] // [LDP][MOVD $144, R27][MOVK $(1<<16), R27][ADD R27, RSP, RSP][RET] // // SP moves at the fourth word (byte 12) and returns to zero at the final // RET (byte 40); the words are go tool asm's own for the same source. func TestArm64SplitFrameSpadj(t *testing.T) { f, errs := parser.Parse("frame_arm64.s", "#include \"textflag.h\"\n\nTEXT ·framed(SB), NOSPLIT, $65664-0\n\tCALL ·other(SB)\n\tRET\n\nTEXT ·other(SB), NOSPLIT, $0\n\tRET\n") 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] wantSpadj := []SpadjStep{{PC: 12, Value: 65680}, {PC: 40, Value: 0}} if len(fn.Spadj) != len(wantSpadj) { t.Fatalf("spadj = %v, want %v", fn.Spadj, wantSpadj) } for i := range wantSpadj { if fn.Spadj[i] != wantSpadj[i] { t.Errorf("spadj[%d] = %v, want %v", i, fn.Spadj[i], wantSpadj[i]) } } want := []uint32{ 0xd10243f4, // SUB $144, RSP, R20 0xd1404294, // SUB $(16<<12), R20, R20 0xa93ffa9d, // STP (R29, R30), -8(R20) 0x9100029f, // MOVD R20, RSP 0xd10023fd, // SUB $8, RSP, R29 0x94000000, // CALL (relocation masked at link time) 0xa97ffbfd, // LDP -8(RSP), (R29, R30) 0xd280121b, // MOVD $144, R27 0xf2a0003b, // MOVK $(1<<16), R27 0x8b3b63ff, // ADD R27, RSP, RSP 0xd65f03c0, // RET } words := leWords(img.Code[fn.Offset : fn.Offset+fn.Size]) if len(words) != len(want) { t.Fatalf("framed = %d words, want %d", len(words), len(want)) } for i, w := range want { if words[i] != w { t.Errorf("word %d = %08x, want %08x", i, words[i], w) } } } // TestArm64RegOffsetEncodings pins the register-offset addressing forms of // the MOV family against `go tool asm` words (Go 1.27, arm64): the LSL, // UXTW, SXTW and SXTX options, the shift amount equal to the access size's // log2, and the rejections the toolchain raises for the other spellings. func TestArm64RegOffsetEncodings(t *testing.T) { got := arm64Words(t, "\tMOVD (R2)(R6.SXTW), R4\n"+ "\tMOVD (R3)(R6), R5\n"+ "\tMOVD (R2)(R6<<3), R4\n"+ "\tMOVWU (R5)(R4.UXTW), R10\n"+ "\tMOVW (R9)(R8.SXTW<<2), R19\n"+ "\tMOVD (R3)(R7.SXTX<<3), R8\n"+ "\tMOVBU (R10)(R6), R15\n"+ "\tFMOVS (R2)(R6<<2), F4\n"+ "\tFMOVD (R2)(R6), F4\n"+ "\tMOVD R5, (R2)(R6<<3)\n"+ "\tMOVW R7, (R3)(R4.SXTW)\n"+ "\tMOVD ZR, (R6)(R7.SXTX<<3)\n") want := []uint32{ 0xf866c844, // MOVD (R2)(R6.SXTW), R4 0xf8666865, // MOVD (R3)(R6), R5 0xf8667844, // MOVD (R2)(R6<<3), R4 0xb86448aa, // MOVWU (R5)(R4.UXTW), R10 0xb8a8d933, // MOVW (R9)(R8.SXTW<<2), R19 0xf867f868, // MOVD (R3)(R7.SXTX<<3), R8 0x3866694f, // MOVBU (R10)(R6), R15 0xbc667844, // FMOVS (R2)(R6<<2), F4 0xfc666844, // FMOVD (R2)(R6), F4 0xf8267845, // MOVD R5, (R2)(R6<<3) 0xb824c867, // MOVW R7, (R3)(R4.SXTW) 0xf827f8df, // MOVD ZR, (R6)(R7.SXTX<<3) 0xd65f03c0, // RET } if len(got) != len(want) { t.Fatalf("word count = %d, want %d", len(got), len(want)) } for i := range want { if got[i] != want[i] { t.Errorf("word %d = %08x, want %08x", i, got[i], want[i]) } } } // TestArm64RegOffsetRejections pins the spellings the toolchain refuses for // the register-offset addressing: the UXTX/UXTB/SXTH extend names, a shift // amount outside {0, log2(access)}, the scaled *k index, writeback with an // index, and the FMOVQ form the toolchain rejects outright. func TestArm64RegOffsetRejections(t *testing.T) { for _, src := range []string{ "\tMOVD\t(R2)(R3.UXTX), R1\n", "\tMOVD\t(R2)(R3.UXTB), R1\n", "\tMOVD\t(R2)(R3.SXTH), R1\n", "\tMOVD\t(R2)(R3<<1), R1\n", "\tMOVD\t(R2)(R3<<2), R1\n", "\tMOVBU\t(R1)(R2<<1), R3\n", "\tMOVD\t(R2)(R3*8), R1\n", "\tMOVD.P\tR5, (R2)(R3)\n", "\tFMOVQ\t(R2)(R6), F4\n", } { f, errs := parser.Parse("test_arm64.s", "#include \"textflag.h\"\n\nTEXT ·f(SB), NOSPLIT, $0-0\n"+src+"\tRET\n") if len(errs) > 0 { continue // a parse rejection is a rejection } if _, err := AssembleFileARM64(f); err == nil { t.Errorf("expected rejection for %q, got nil", strings.TrimSpace(src)) } } } // TestArm64SimdArrangementBits pins the arrangement bits the first SIMD pass // got wrong, word-verified against `go tool asm` (Go 1.27, arm64): the FP // one-bit size field (S=0, D=1 at bit 22), the SSHL/USHL size bits, the long // extends' immh field, the narrow family's fixed bit 16, the UADDW size bits // off the narrow side with Q from the spelling, the scalar D forms of the // bare VADD/VSUB spellings and the INS lane packing. func TestArm64SimdArrangementBits(t *testing.T) { got := arm64Words(t, "\tVFADD V0.S4, V0.S4, V1.S4\n"+ "\tVFADD V0.D2, V0.D2, V1.D2\n"+ "\tVFABS V0.D2, V1.D2\n"+ "\tVSCVTF V1.D2, V2.D2\n"+ "\tVSSHL V1.B8, V2.B8, V3.B8\n"+ "\tVSSHL V1.S4, V2.S4, V3.S4\n"+ "\tVUSHL V1.H4, V2.H4, V3.H4\n"+ "\tVRBIT V24.B8, V24.B8\n"+ "\tVUXTL V30.B8, V30.H8\n"+ "\tVUXTL V29.S2, V2.D2\n"+ "\tVUXTL2 V30.H8, V30.S4\n"+ "\tVXTN V1.H8, V2.B8\n"+ "\tVFCVTN V1.D2, V2.S2\n"+ "\tVFCVTL V1.S2, V2.D2\n"+ "\tVUADDW V13.H4, V10.S4, V11.S4\n"+ "\tVUADDW2 V13.H8, V20.S4, V30.S4\n"+ "\tVADD V1, V2, V3\n"+ "\tVSUB V12, V20, V30\n"+ "\tVMOV V12.S[2], V12.S[3]\n"+ "\tVMOV V12.H[3], V12.H[5]\n") want := []uint32{ 0x4e20d401, // VFADD V0.4S: FP size field clear for S 0x4e60d401, // VFADD V0.2D: FP size bit 22, not bit 23 0x4ee0f801, // VFABS V1.2D: one-bit FP size 0x4e61d822, // VSCVTF V2.2D: bit 22, the Q-only mask must not strip it 0x0e214443, // VSSHL V3.8B: base without the pre-set size and Q bits 0x4ea14443, // VSSHL V3.4S: integer size bits from the arrangement 0x2e614443, // VUSHL V3.4H: U bit plus the H size 0x2e605b18, // VRBIT V24.8B: no Q bit in the base 0x2f08a7de, // VUXTL: immh = 1 at bit 19 for the byte extend 0x2f20a7a2, // VUXTL: immh = 4 at bit 21 for the word extend 0x6f10a7de, // VUXTL2: immh = 2 plus the 128-bit flag 0x0e212822, // VXTN: fixed bit 16 in the base 0x0e616822, // VFCVTN: bits 16 and 22, Q rides the spelling 0x0e617822, // VFCVTL: bit 22, Q rides the spelling 0x2e6d114b, // VUADDW: size bits off the narrow side, Q clear 0x6e6d129e, // VUADDW2: size off the narrow side, Q from the spelling 0x5ee18443, // VADD scalar D form for the bare spelling 0x7eec869e, // VSUB scalar D form for the bare spelling 0x6e1c458c, // INS: imm4 = 2<<2 for the word source lane 2 0x6e16358c, // INS: imm4 = 3<<1 for the halfword source lane 3 0xd65f03c0, // RET } if len(got) != len(want) { t.Fatalf("word count = %d, want %d", len(got), len(want)) } for i := range want { if got[i] != want[i] { t.Errorf("word %d = %08x, want %08x", i, got[i], want[i]) } } } // TestArm64SimdArrangementRejections pins the arrangements the toolchain // refuses on the FP SIMD rows and the element-to-element moves: the // half-width FP spellings, the Q1 spelling on the integer shifts, and the // mixed element letters of INS. func TestArm64SimdArrangementRejections(t *testing.T) { for _, src := range []string{ "\tVFADD\tV1.H4, V2.H4, V3.H4\n", "\tVFADD\tV1.H8, V2.H8, V3.H8\n", "\tVFABS\tV1.H4, V2.H4\n", "\tVSCVTF\tV1.H4, V2.H4\n", "\tVSSHL\tV1.Q1, V2.Q1, V3.Q1\n", "\tVMOV\tV12.S[0], V12.D[1]\n", } { f, errs := parser.Parse("test_arm64.s", "#include \"textflag.h\"\n\nTEXT ·f(SB), NOSPLIT, $0-0\n"+src+"\tRET\n") if len(errs) > 0 { continue // a parse rejection is a rejection } if _, err := AssembleFileARM64(f); err == nil { t.Errorf("expected rejection for %q, got nil", strings.TrimSpace(src)) } } } // TestArm64MovImmZR pins the immediate-to-ZR spellings against `go tool asm` // words: omovconst takes the bitmask path only for a real register, so an // immediate to ZR rides the MOVZ/MOVN sequence carrying the value. func TestArm64MovImmZR(t *testing.T) { got := arm64Words(t, "\tMOVW $1, ZR\n"+ "\tMOVD $1, ZR\n"+ "\tMOVD $0x123456789, ZR\n"+ "\tMOVD $-1, ZR\n"+ "\tMOVD $0, ZR\n") want := []uint32{ 0x5280003f, // MOVZ W31, #1 0xd280003f, // MOVZ X31, #1 0xd28cf13f, // MOVZ X31, #26505 0xf2a468bf, // MOVK $(9029<<16), X31 0xf2c0003f, // MOVK $(1<<32), X31 0x9280001f, // MOVN X31, #0 0xaa1f03ff, // ORR X31, XZR, XZR 0xd65f03c0, // RET } if len(got) != len(want) { t.Fatalf("word count = %d, want %d", len(got), len(want)) } for i := range want { if got[i] != want[i] { t.Errorf("word %d = %08x, want %08x", i, got[i], want[i]) } } } // TestArm64ConRn pins the MOVD $con(Rn), Rd lowering against `go tool asm` // words: the single ADD/SUB inside the addcon band, the hi<<12 plus lo pair // in the 24-bit band, and the pool plus UXTX add beyond it. func TestArm64ConRn(t *testing.T) { got := arm64Words(t, "\tMOVD $0x1002(RSP), R1\n"+ "\tMOVD $0x1708(RSP), RSP\n"+ "\tMOVD $0x2001(R7), R1\n"+ "\tMOVD $0xffffff(R7), R1\n"+ "\tMOVD $-1(R7), R1\n"+ "\tMOVD $-0x30(R7), R1\n"+ "\tMOVD $-0x2000(RSP), R1\n"+ "\tMOVD $-0x10000(RSP), RSP\n"+ "\tMOVD $0(R7), R1\n"+ "\tMOVD $4096(R7), R1\n"+ "\tMOVD $5(R3), R1\n") want := []uint32{ 0x914007e1, // ADD $(1<<12), RSP, R1 0x91000821, // ADD $2, R1, R1 0x914007ff, // ADD $(1<<12), RSP, RSP 0x911c23ff, // ADD $0x708, RSP, RSP 0x914008e1, // ADD $(2<<12), R7, R1 0x91000421, // ADD $1, R1, R1 0x917ffce1, // ADD $(4095<<12), R7, R1 0x913ffc21, // ADD $4095, R1, R1 0xd10004e1, // SUB $1, R7, R1 0xd100c0e1, // SUB $0x30, R7, R1 0xd1400be1, // SUB $(2<<12), RSP, R1 0xd14043ff, // SUB $(16<<12), RSP, RSP 0x910000e1, // ADD $0, R7, R1 0x914004e1, // ADD $(1<<12), R7, R1 0x91001461, // ADD $5, R3, R1 0xd65f03c0, // RET } if len(got) != len(want) { t.Fatalf("word count = %d, want %d", len(got), len(want)) } for i := range want { if got[i] != want[i] { t.Errorf("word %d = %08x, want %08x", i, got[i], want[i]) } } } // TestArm64ConRnRejections pins the shapes the toolchain refuses for the // con(register) form: every other width and the ZR destination. func TestArm64ConRnRejections(t *testing.T) { for _, src := range []string{ "\tMOVW\t$5(R3), R1\n", "\tMOVD\t$5(R7), ZR\n", } { f, errs := parser.Parse("test_arm64.s", "#include \"textflag.h\"\n\nTEXT ·f(SB), NOSPLIT, $0-0\n"+src+"\tRET\n") if len(errs) > 0 { continue // a parse rejection is a rejection } if _, err := AssembleFileARM64(f); err == nil { t.Errorf("expected rejection for %q, got nil", strings.TrimSpace(src)) } } } // arm64WordsTail assembles a NOSPLIT leaf body exactly as written, adding no // RET: the pool guard tests need bodies whose last statement is not a branch. func arm64WordsTail(t *testing.T, body string) []uint32 { t.Helper() f, errs := parser.Parse("test_arm64.s", "#include \"textflag.h\"\n\nTEXT ·f(SB), NOSPLIT, $0-0\n"+body) if len(errs) > 0 { t.Fatalf("parse: %v", errs) } img, err := AssembleFileARM64(f) if err != nil { t.Fatalf("AssembleFileARM64: %v", err) } return leWords(img.Code) } // TestArm64LiteralPool pins the offset literal pool against `go tool asm -S` // output (Go 1.27, arm64): the PC-relative literal loads into REGTMP, the // register-offset accesses, first-use ordering with value-only dedup, the // entry widths (four-byte words for [0, 0x7FFFFFFF], eight-byte slots for // the lacon displacements, the negatives and the beyond-32-bit values, with // no alignment padding between entries) and the shared entries' widths // following the entry rather than the referrer. func TestArm64LiteralPool(t *testing.T) { tests := []struct { name string body string want []uint32 }{ { name: "dedup and first-use order", body: "\tMOVD\tR1, 0x1007000(R2)\n\tMOVD\tR1, 0x44332211(R2)\n\tMOVD\tR1, 0x1007000(R2)\n", want: []uint32{ 0x180000fb, 0xf83b6841, // LDR W27, pool0; MOVD R1, (R2)(R27) 0x180000db, 0xf83b6841, // LDR W27, pool1; MOVD R1, (R2)(R27) 0x1800007b, 0xf83b6841, // LDR W27, pool0; MOVD R1, (R2)(R27) 0xd65f03c0, // RET 0x01007000, 0x44332211, // WORD 0x1007000, WORD 0x44332211 }, }, { name: "mixed widths, no padding, cross-width dedup", body: "\tMOVB\tR1, 0x1000000(R2)\n\tMOVB\tR1, -0x1000000(R3)\n\tMOVB\tR1, 0x1001000(R4)\n\tMOVD\t$0x1000000(R7), R1\n", want: []uint32{ 0x1800013b, 0x383b6841, // LDR W27, pool0; MOVB R1, (R2)(R27) 0x5800011b, 0x383b6861, // LDR X27, pool1; MOVB R1, (R3)(R27) 0x1800011b, 0x383b6881, // LDR W27, pool2; MOVB R1, (R4)(R27) 0x1800007b, 0x8b3b60e1, // LDR W27, pool0 (lacon reuse); ADD R27.UXTX, R7, R1 0xd65f03c0, // RET 0x01000000, // WORD 0x1000000 (off 0) 0xff000000, 0xffffffff, // DWORD -0x1000000 (off 4, unpadded) 0x01001000, // WORD 0x1001000 (off 12) }, }, { name: "lacon entry takes the eight-byte slot", body: "\tMOVD\t$0x1000000(R7), R1\n", want: []uint32{ 0x5800007b, 0x8b3b60e1, // LDR X27, pool; ADD R27.UXTX, R7, R1 0xd65f03c0, // RET 0x01000000, 0x00000000, // DWORD 0x1000000 }, }, { name: "negative offsets pool as DWORD with LDR X", body: "\tMOVB\tR1, -0x1000000(R2)\n\tMOVD\t$-0x1000000(R7), R1\n", want: []uint32{ 0x580000bb, 0x383b6841, // LDR X27, pool; MOVB R1, (R2)(R27) 0x5800007b, 0x8b3b60e1, // LDR X27, pool; ADD R27.UXTX, R7, R1 0xd65f03c0, // RET 0xff000000, 0xffffffff, // DWORD -0x1000000 }, }, { name: "beyond 32-bit offsets", body: "\tMOVD\tR1, 0x12345678901(R2)\n\tMOVB\tR2, 0x12345678901(R3)\n", want: []uint32{ 0x580000bb, 0xf83b6841, // LDR X27, pool; MOVD R1, (R2)(R27) 0x5800007b, 0x383b6862, // LDR X27, pool; MOVB R2, (R3)(R27) 0xd65f03c0, // RET 0x45678901, 0x00000123, // DWORD 0x12345678901 }, }, { name: "pair offsets ride the pool", body: "\tMOVD\tR1, 0x1000000(R2)\n\tLDP\t0x1000000(R2), (R1, R3)\n", want: []uint32{ 0x917ffc5b, 0xf9080361, // ADD $(4095<<12), R2, R27; MOVD R1, 64(R27) 0x1800009b, 0x8b3b605b, // LDR W27, pool; ADD R27.UXTX, R2, R27 0xa9400f61, // LDP (R27), (R1, R3) 0xd65f03c0, // RET 0x01000000, // WORD 0x1000000 }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := arm64Words(t, tt.body) if len(got) != len(tt.want) { t.Fatalf("word count = %d, want %d (got %08x)", len(got), len(tt.want), got) } for i := range tt.want { if got[i] != tt.want[i] { t.Errorf("word %d = %08x, want %08x", i, got[i], tt.want[i]) } } }) } } // TestArm64LiteralPoolGuard pins the flushpool guard: the word-zero UNDEF // that keeps execution from falling into the pool when the last statement is // not a branch. END closes the body without becoming an instruction, so it // does not count; a trailing PCDATA is a real statement and takes the guard; // RET and the morestack block's branch need none. func TestArm64LiteralPoolGuard(t *testing.T) { tests := []struct { name string body string want []uint32 }{ { name: "END without RET still guards", body: "\tMOVB\tR1, 0x1000000(R2)\n\tEND\n", want: []uint32{ 0x1800007b, 0x383b6841, // LDR W27, pool; MOVB R1, (R2)(R27) 0x00000000, // UNDEF guard 0x01000000, // WORD 0x1000000 }, }, { name: "trailing PCDATA keeps the guard", body: "\tMOVD\tR1, 0x1007000(R2)\n\tRET\n\tPCDATA\t$0, $-1\n", want: []uint32{ 0x1800009b, 0xf83b6841, // LDR W27, pool; MOVD R1, (R2)(R27) 0xd65f03c0, // RET 0x00000000, // UNDEF guard 0x01007000, // WORD 0x1007000 }, }, { name: "RET closes without a guard", body: "\tMOVD\tR1, 0x1007000(R2)\n\tRET\n\tEND\n", want: []uint32{ 0x1800007b, 0xf83b6841, // LDR W27, pool; MOVD R1, (R2)(R27) 0xd65f03c0, // RET 0x01007000, // WORD 0x1007000 }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := arm64WordsTail(t, tt.body) if len(got) != len(tt.want) { t.Fatalf("word count = %d, want %d (got %08x)", len(got), len(tt.want), got) } for i := range tt.want { if got[i] != tt.want[i] { t.Errorf("word %d = %08x, want %08x", i, got[i], tt.want[i]) } } }) } } // TestArm64NoopVsNop pins the two spellings apart, the way the toolchain // holds them: NOOP is the real hint instruction and NOP a zero-size pseudo // whose operand, when it is an immediate or a register, rides along for go // vet's benefit; every other operand shape and the NOOP operands are illegal // combinations. func TestArm64NoopVsNop(t *testing.T) { got := arm64Words(t, "\tNOP\n\tNOOP\n\tNOP\tR0\n") want := []uint32{0xd503201f, 0xd65f03c0} if len(got) != len(want) { t.Fatalf("word count = %d, want %d", len(got), len(want)) } for i := range want { if got[i] != want[i] { t.Errorf("word %d = %08x, want %08x", i, got[i], want[i]) } } for _, src := range []string{ "\tNOOP\t$0\n", "\tNOOP\tR0\n", "\tNOP\t8(R0)\n", "\tNOP\t$0, $1\n", } { f, errs := parser.Parse("test_arm64.s", "#include \"textflag.h\"\n\nTEXT ·f(SB), NOSPLIT, $0-0\n"+src+"\tRET\n") if len(errs) > 0 { continue // a parse rejection is a rejection } if _, err := AssembleFileARM64(f); err == nil { t.Errorf("expected rejection for %q, got nil", strings.TrimSpace(src)) } } } // TestArm64RegtmpBoundaries pins the REGTMP acceptance the toolchain holds // (probed against `go tool asm`, Go 1.27, arm64): the plain imm12 and the // ±4095 offsets tolerate a REGTMP source or pair member, while every // lowering that itself writes REGTMP (the constant materialisations, the // pool path, the pair store expansions) refuses one, and the pool path // refuses a REGTMP base on both the loads and the stores. func TestArm64RegtmpBoundaries(t *testing.T) { accept := []string{ "\tADD\t$5, R27, R3\n", // imm12, no REGTMP sequence "\tLDP\t700(R2), (R26, R27)\n", // load add/sub path: unchecked pair "\tMOVD\tR27, 4000(R2)\n", // misaligned ±4095: unchecked data "\tSTP\t(R26, R3), 700(R2)\n", // store add/sub path, no pair member "\tLDP\t0x1234567(R2), (R27, R3)\n", // pool load: base alone decides "\tMOVD\t$0x1000000(R27), R1\n", // lacon: case 34 reads the base } reject := []string{ "\tADD\t$0x1234567, R27, R3\n", // materialisation, REGTMP source "\tAND\t$0x22220000, R27, R4\n", // logical materialisation, ditto "\tSTP\t(R26, R27), 700(R2)\n", // store add/sub path, pair member "\tSTP\t(R3, R4), 0x1234567(R27)\n", // pool store, REGTMP base "\tLDP\t0x1234567(R27), (R3, R4)\n", // pool load, REGTMP base "\tSTP\t(R27, R3), 0x1234567(R2)\n", // pool store, REGTMP pair member "\tMOVD\tR27, 0x1234567(R2)\n", // pool store, REGTMP data "\tMOVD\t0x1234567(R2), R27\n", // pool load, REGTMP data } for _, src := range accept { f, errs := parser.Parse("test_arm64.s", "#include \"textflag.h\"\n\nTEXT ·f(SB), NOSPLIT, $0-0\n"+src+"\tRET\n") if len(errs) > 0 { t.Errorf("expected acceptance for %q, got parse rejection", strings.TrimSpace(src)) continue } if _, err := AssembleFileARM64(f); err != nil { t.Errorf("expected acceptance for %q, got %v", strings.TrimSpace(src), err) } } for _, src := range reject { f, errs := parser.Parse("test_arm64.s", "#include \"textflag.h\"\n\nTEXT ·f(SB), NOSPLIT, $0-0\n"+src+"\tRET\n") if len(errs) > 0 { continue // a parse rejection is a rejection } if _, err := AssembleFileARM64(f); err == nil { t.Errorf("expected rejection for %q, got nil", strings.TrimSpace(src)) } } } // TestArm64VLDSTPostIndexContract pins the structure-load post-index rules // against `go tool asm` (Go 1.27, arm64): the implicit by-size increment of // an unspelled or zero offset (both load and replicate forms, the words // taken from the toolchain's own listing), the register post-index that // takes a bare register only, and the spelled immediate that must match the // transferred bytes. func TestArm64VLDSTPostIndexContract(t *testing.T) { got := arm64Words(t, "\tVLD1.P (R3), [V31.H8, V0.H8]\n"+ // implicit 2*16 "\tVLD1R.P (R1), [V9.B8]\n"+ // implicit 1*1 "\tVLD3R.P 6(R15), [V15.H4,V16.H4,V17.H4]\n"+ // 3*2 spelled "\tVLD1.P (R8)(R20), [V21.B16, V22.B16]\n") // register post-index want := []uint32{ 0x4cdfa47f, // VLD1.P (R3), [V31.H8, V0.H8] 0x0ddfc029, // VLD1R.P (R1), [V9.B8] 0x0ddfe5ef, // VLD3R.P 6(R15), [V15.H4,V16.H4,V17.H4] 0x4cd4a115, // VLD1.P (R8)(R20), [V21.B16, V22.B16] 0xd65f03c0, // RET } if len(got) != len(want) { t.Fatalf("word count = %d, want %d (got %08x)", len(got), len(want), got) } for i := range want { if got[i] != want[i] { t.Errorf("word %d = %08x, want %08x", i, got[i], want[i]) } } reject := []string{ "\tVLD1\t(R8)(R13), [V2.B16]\n", // register index without .P "\tVLD1\t8(R9), [V2.B16]\n", // offset without .P "\tVLD1.P\t8(R8)(R13), [V2.B16]\n", // offset beside the register index "\tVLD1.P\t(R8)(R9.UXTW), [V2.B16]\n", // extended post-index register "\tVLD1.P\t(R8)(R9<<2), [V2.B16]\n", // shifted post-index register "\tVST1.P\t[V1.B16], (R8)(R9.UXTW)\n", // extended, store side "\tVLD1.P\t17(R1), [V2.B16]\n", // 16 bytes transferred "\tVLD1.P\t-16(R1), [V2.B16]\n", // negative increment "\tVST1.P\t[V4.S4,V5.S4], 48(R1)\n", // 2*16, not 48 "\tVLD3R.P\t24(R15), [V15.H4,V16.H4,V17.H4]\n", // 3*2, not 24 } for _, src := range reject { f, errs := parser.Parse("test_arm64.s", "#include \"textflag.h\"\n\nTEXT ·f(SB), NOSPLIT, $0-0\n"+src+"\tRET\n") if len(errs) > 0 { continue // a parse rejection is a rejection } if _, err := AssembleFileARM64(f); err == nil { t.Errorf("expected rejection for %q, got nil", strings.TrimSpace(src)) } } } // TestArm64OperandArities pins the operand-shape rules the error corpus // drove home: the TLBI register arity follows the operation, RPRFM's second // operand is a plain register, the FCVT conversions are pinned to their one // width pair, and the integer pairs take integer registers alone. func TestArm64OperandArities(t *testing.T) { accept := []string{ "\tTLBI\tVAE1, R1\n", "\tTLBI\tVMALLE1IS\n", "\tTLBI\tALLE3OS\n", "\tRPRFM\t(R1), R2, PLDKEEP\n", "\tRPRFM\t(R1), R27, PLDKEEP\n", "\tVFCVTL\tV1.S2, V2.D2\n", "\tVFCVTL2\tV1.S4, V2.D2\n", "\tVFCVTN\tV1.D2, V2.S2\n", "\tVFCVTN2\tV1.D2, V2.S4\n", "\tLDP\t(R0), (R0, R1)\n", } reject := []string{ "\tTLBI\tVMALLE1IS, R0\n", // whole-entry op: extraneous register "\tTLBI\tALLE3OS, ZR\n", // ditto, the ZR spelling included "\tTLBI\tVAE1IS\n", // by-address op: missing register "\tTLBI\tRVALE3\n", // ditto "\tRPRFM\t(R1), RSP, PLDKEEP\n", "\tRPRFM\t(R1), ZR, PLDKEEP\n", "\tVFCVTL\tV1.H4, V2.S4\n", // no half-precision conversion "\tVFCVTN\tV1.D2, V2.H4\n", // ditto on the narrowing side "\tLDP\t(R0), (F0, F1)\n", // FP pair on the integer mnemonic "\tSTP\t(F2, F3), (R0)\n", // ditto } for _, src := range accept { f, errs := parser.Parse("test_arm64.s", "#include \"textflag.h\"\n\nTEXT ·f(SB), NOSPLIT, $0-0\n"+src+"\tRET\n") if len(errs) > 0 { t.Errorf("expected acceptance for %q, got parse rejection", strings.TrimSpace(src)) continue } if _, err := AssembleFileARM64(f); err != nil { t.Errorf("expected acceptance for %q, got %v", strings.TrimSpace(src), err) } } for _, src := range reject { f, errs := parser.Parse("test_arm64.s", "#include \"textflag.h\"\n\nTEXT ·f(SB), NOSPLIT, $0-0\n"+src+"\tRET\n") if len(errs) > 0 { continue // a parse rejection is a rejection } if _, err := AssembleFileARM64(f); err == nil { t.Errorf("expected rejection for %q, got nil", strings.TrimSpace(src)) } } } // TestArm64FPImmediate pins the FP immediate moves against `go tool asm` // words: the FMOV (immediate) instruction for the 8-bit encodable values and // the FMOV-from-ZR move for zero, plus the rejections the toolchain raises // (integer immediates to FP registers, FMOVQ immediates) and the values that // only the toolchain's $f64 pool reaches. func TestArm64FPImmediate(t *testing.T) { got := arm64Words(t, "\tFMOVS $(4.0), F0\n"+ "\tFMOVD $(4.0), F0\n"+ "\tFMOVS $(0.265625), F1\n"+ "\tFMOVD $(0.1796875), F2\n"+ "\tFMOVD $(28.0), F4\n"+ "\tFMOVD $(-4.0), F6\n"+ "\tFMOVD $0, F0\n"+ "\tFMOVD $(0.0), F5\n") want := []uint32{ 0x1e221000, // FMOV S0, #4.0 0x1e621000, // FMOV D0, #4.0 0x1e2a3001, // FMOV S1, #0.265625 0x1e68f002, // FMOV D2, #0.1796875 0x1e679004, // FMOV D4, #28.0 0x1e721006, // FMOV D6, #-4.0 0x9e6703e0, // FMOV D0, XZR 0x9e6703e5, // FMOV D5, XZR 0xd65f03c0, // RET } if len(got) != len(want) { t.Fatalf("word count = %d, want %d", len(got), len(want)) } for i := range want { if got[i] != want[i] { t.Errorf("word %d = %08x, want %08x", i, got[i], want[i]) } } for _, src := range []string{ "\tFMOVD\t$5, F0\n", "\tFMOVS\t$4, F0\n", "\tFMOVQ\t$(4.0), F0\n", } { f, errs := parser.Parse("test_arm64.s", "#include \"textflag.h\"\n\nTEXT ·f(SB), NOSPLIT, $0-0\n"+src+"\tRET\n") if len(errs) > 0 { continue // a parse rejection is a rejection } if _, err := AssembleFileARM64(f); err == nil { t.Errorf("expected rejection for %q, got nil", strings.TrimSpace(src)) } } } // TestArm64LogicalImmZR pins the logical-immediate spellings the destination // changes, against `go tool asm` words: TST keeps the fast ANDS-to-ZR form // while a non-flag-setting logical to ZR materialises the constant into // REGTMP and takes the register form (omovconst's rt != REGZERO guard). func TestArm64LogicalImmZR(t *testing.T) { got := arm64Words(t, "\tTSTW $0x600000006, R1\n"+ "\tTST $15, R2\n"+ "\tAND $1, ZR\n"+ "\tANDW $1, ZR\n"+ "\tEOR $1, ZR\n"+ "\tAND $15, R2, R3\n"+ "\tANDS $0xff, R2, R3\n"+ "\tORR $0x10, R2, R3\n") want := []uint32{ 0x721f043f, // ANDS W31, W1, #bitmask (TSTW) 0xf2400c5f, // ANDS X31, X2, #15 (TST) 0xb24003fb, // ORR X27, XZR, #1 0x8a1b03ff, // AND X31, X31, R27 0x320003fb, // ORR W27, WZR, #1 (ANDW to ZR) 0x0a1b03ff, // AND W31, W31, W27 0xb24003fb, // ORR X27, XZR, #1 (EOR to ZR) 0xca1b03ff, // EOR X31, X31, R27 0x92400c43, // AND X3, X2, #15 0xf2401c43, // ANDS X3, X2, #0xff 0xb27c0043, // ORR X3, X2, #0x10 0xd65f03c0, // RET } if len(got) != len(want) { t.Fatalf("word count = %d, want %d", len(got), len(want)) } for i := range want { if got[i] != want[i] { t.Errorf("word %d = %08x, want %08x", i, got[i], want[i]) } } } // TestArm64ZRNameNotNumber pins the spellings where ZR and RSP share the // register number but not the class: the toolchain's omovconst guards the // bitmask path with rt != REGZERO, so RSP keeps the fast logical and ORR // forms while ZR materialises or rides MOVZ. func TestArm64ZRNameNotNumber(t *testing.T) { got := arm64Words(t, "\tAND $8, R0, RSP\n"+ "\tORR $8, R0, RSP\n"+ "\tMOVW $0x10001000, RSP\n"+ "\tADDW $0x10001000, R1\n") want := []uint32{ 0x927d001f, // AND X31(SP), X0, #bitmask: the fast path for RSP 0xb27d001f, // ORR X31(SP), X0, #bitmask 0x320483ff, // ORR W31(SP), WZR, #0x10001000 (the MOVW bitmask) 0x320483fb, // ORR W27, WZR, #0x10001000 0x0b1b0021, // ADDW W1, W1, W27 0xd65f03c0, // RET } if len(got) != len(want) { t.Fatalf("word count = %d, want %d", len(got), len(want)) } for i := range want { if got[i] != want[i] { t.Errorf("word %d = %08x, want %08x", i, got[i], want[i]) } } } // TestArm64BitfieldAlias pins the bitfield alias encodings against // `go tool asm` words: BFI rides the BFM opc at every lsb (asm7.go case 43 // routes ABFI to ABFM), SBFIZ keeps SBFM and UBFIZ keeps UBFM at the spelled // width. func TestArm64BitfieldAlias(t *testing.T) { got := arm64Words(t, "\tBFI $0, R1, $1, R2\n"+ "\tBFIW $0, R1, $1, R2\n"+ "\tBFI $4, R1, $4, R2\n"+ "\tSBFIZ $0, R1, $1, R2\n"+ "\tSBFIZW $0, R1, $1, R2\n"+ "\tUBFIZ $0, R1, $1, R2\n"+ "\tUBFIZW $0, R1, $1, R2\n"+ "\tUBFIZ $4, R1, $4, R2\n"+ "\tBFXIL $0, R1, $8, R2\n") want := []uint32{ 0xb3400022, // BFM X2, X1, #0, #0 (BFI lsb 0) 0x33000022, // BFM W2, W1, #0, #0 (BFIW lsb 0) 0xb37c0c22, // BFM X2, X1, #28, #3 (BFI lsb 4) 0x93400022, // SBFM X2, X1, #0, #0 (SBFIZ lsb 0) 0x13000022, // SBFM W2, W1, #0, #0 (SBFIZW lsb 0) 0xd3400022, // UBFM X2, X1, #0, #0 (UBFIZ lsb 0) 0x53000022, // UBFM W2, W1, #0, #0 (UBFIZW lsb 0) 0xd37c0c22, // UBFM X2, X1, #28, #3 (UBFIZ lsb 4) 0xb3401c22, // BFM X2, X1, #0, #7 (BFXIL lsb 0) 0xd65f03c0, // RET } if len(got) != len(want) { t.Fatalf("word count = %d, want %d", len(got), len(want)) } for i := range want { if got[i] != want[i] { t.Errorf("word %d = %08x, want %08x", i, got[i], want[i]) } } } // TestArm64VTBLShapes pins the VTBL/VTBX list handling: a table list that // closes on the last operand (the fuzz minimaliser's shape) is rejected with // a diagnostic instead of indexing past the operand slice, and the ordinary // spellings keep their words. func TestArm64VTBLShapes(t *testing.T) { got := arm64Words(t, "\tVTBL V0.[B8], [V1.B8, V2.B8], V3.B8\n") want := []uint32{ 0x0e002023, // VTBL V3.8B, [V1.8B, V2.8B], V0.8B 0xd65f03c0, // RET } if len(got) != len(want) { t.Fatalf("word count = %d, want %d", len(got), len(want)) } for i := range want { if got[i] != want[i] { t.Errorf("word %d = %08x, want %08x", i, got[i], want[i]) } } for _, src := range []string{ "\tVTBX\tV0,[V0,V0]\n", "\tVTBL\tV0,[V0,V0]\n", } { f, errs := parser.Parse("test_arm64.s", "#include \"textflag.h\"\n\nTEXT ·f(SB), NOSPLIT, $0\n"+src+"\tRET\n") if len(errs) > 0 { continue } if _, err := AssembleFileARM64(f); err == nil { t.Errorf("expected rejection for %q, got nil", strings.TrimSpace(src)) } } } // TestArm64LogicalToZR pins the toolchain's split between the flag-setting // logicals, whose ZR destination keeps the bitmask fast path (asm7.go case // 53), and the plain forms, which materialise into REGTMP: `go tool asm` // encodes ANDSW $0x100, R13, ZR as one TSTW word and ANDW $1, R5, ZR as the // ORRW-plus-register pair. func TestArm64LogicalToZR(t *testing.T) { got := arm64Words(t, "\tANDSW $0x100, R13, ZR\n\tBICSW $1, R5, ZR\n\tANDSW $0x101, R13, ZR\n\tANDW $1, R5, ZR\n") want := []uint32{ 0x721801bf, // ANDS (bitmask) R13, ZR: the TSTW word 0x721f78bf, // BICS (bitmask of $1) R5, ZR 0x5280203b, // MOVW $257, R27: not a bitmask, materialised 0x6a1b01bf, // ANDSW R27, R13 0x320003fb, // ORRW $1, ZR, R27: the plain form materialises too 0x0a1b00bf, // ANDW R27, R5, ZR 0xd65f03c0, // RET } if len(got) != len(want) { t.Fatalf("word count = %d, want %d", len(got), len(want)) } for i := range want { if got[i] != want[i] { t.Errorf("word %d = %08x, want %08x", i, got[i], want[i]) } } } // TestArm64ImmediateExpression pins the folded arithmetic spellings the // parser leaves half-parsed: `$14*16` parses as the leading literal 14, and // the encoder must read the raw expression's 224 (`go tool asm` folds it). func TestArm64ImmediateExpression(t *testing.T) { got := arm64Words(t, "\tADD $14*16, R0\n\tMOVD $4*8+1, R1\n") want := []uint32{ 0x91038000, // ADD $224, R0 0xd2800421, // MOVZ $33, R1: MOVD $con rides MOVZ for a movcon value 0xd65f03c0, // RET } if len(got) != len(want) { t.Fatalf("word count = %d, want %d", len(got), len(want)) } for i := range want { if got[i] != want[i] { t.Errorf("word %d = %08x, want %08x", i, got[i], want[i]) } } } // TestArm64RemFamily pins the remainder family the toolchain lowers to a // division into REGTMP plus the MSUB tail (asm7.go case 16): the words are // `go tool asm`'s own, and the two-operand spelling divides into the // destination. func TestArm64RemFamily(t *testing.T) { got := arm64Words(t, "\tREM R1, R2, R3\n\tREMW R1, R2, R3\n\tUREM R1, R2, R3\n\tUREMW R1, R2, R3\n\tREM R1, R2\n") want := []uint32{ 0x9ac10c5b, // SDIV R1, R2, R27 0x9b018b63, // MSUB R3 = R2 - R27*R1 0x1ac10c5b, // SDIVW R1, R2, R27 0x1b018b63, // MSUBW 0x9ac1085b, // UDIV R1, R2, R27 0x9b018b63, // MSUB 0x1ac1085b, // UDIVW R1, R2, R27 0x1b018b63, // MSUBW 0x9ac10c5b, // SDIV R1, R2, R27 0x9b018b62, // MSUB R2 = R2 - R27*R1 0xd65f03c0, // RET } if len(got) != len(want) { t.Fatalf("word count = %d, want %d", len(got), len(want)) } for i := range want { if got[i] != want[i] { t.Errorf("word %d = %08x, want %08x", i, got[i], want[i]) } } f, errs := parser.Parse("test_arm64.s", "#include \"textflag.h\"\n\nTEXT ·f(SB), NOSPLIT, $0-0\n\tREM R1, R2, RSP\n\tRET\n") if len(errs) > 0 { t.Fatalf("parse: %v", errs) } if _, err := AssembleFileARM64(f); err == nil { t.Error("REM with an RSP destination assembled, want an illegal combination") } } // TestArm64GetCallerPC pins the toolchain's rewrite (obj7.go AGETCALLERPC): // a leaf reads the link register, a function with a frame reads the saved // LR at 0(SP). func TestArm64GetCallerPC(t *testing.T) { got := arm64Words(t, "\tGETCALLERPC R0\n\tGETCALLERPC R5\n") want := []uint32{ 0xaa1e03e0, // MOVD R30, R0: the leaf reads LR 0xaa1e03e5, // MOVD R30, R5 0xd65f03c0, // RET } if len(got) != len(want) { t.Fatalf("word count = %d, want %d", len(got), len(want)) } for i := range want { if got[i] != want[i] { t.Errorf("word %d = %08x, want %08x", i, got[i], want[i]) } } // With a call in the body the frame exists and the saved LR sits at // 0(SP): prologue (3 words), CALL, then the load. f, errs := parser.Parse("test_arm64.s", "#include \"textflag.h\"\n\nTEXT ·f(SB), NOSPLIT, $8-8\n\tCALL ·x(SB)\n\tGETCALLERPC R5\n\tRET\n") if len(errs) > 0 { t.Fatalf("parse: %v", errs) } img, err := AssembleFileARM64(f) if err != nil { t.Fatalf("AssembleFileARM64: %v", err) } ws := leWords(img.Code) // words 3: CALL, 4: MOVD (RSP), R5. if len(ws) < 6 { t.Fatalf("word count = %d, want at least 6", len(ws)) } if ws[4] != 0xf94003e5 { t.Errorf("non-leaf GETCALLERPC = %08x, want MOVD (RSP), R5 (f94003e5)", ws[4]) } } // TestArm64Dword pins the DWORD pseudo-statement: eight little-endian bytes // per immediate, the toolchain's ADWORD. func TestArm64Dword(t *testing.T) { got := arm64Words(t, "\tDWORD $1\n\tDWORD $0x1122334455667788\n") // The data words ride the listing raw: DWORD $1 is 01 00 00 00 00 00 00 00, // the second an 8-byte little-endian constant. wantBytes := []byte{ 0x01, 0, 0, 0, 0, 0, 0, 0, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, } // The RET follows; locate the 16 data bytes at the head. _ = got code := imgCode(t, "\tDWORD $1\n\tDWORD $0x1122334455667788\n") if len(code) != len(wantBytes)+4 { t.Fatalf("code length = %d, want %d", len(code), len(wantBytes)+4) } for i, b := range wantBytes { if code[i] != b { t.Errorf("byte %d = %02x, want %02x", i, code[i], b) } } } // imgCode assembles a leaf body and returns the whole function image. func imgCode(t *testing.T, body string) []byte { t.Helper() f, errs := parser.Parse("test_arm64.s", "#include \"textflag.h\"\n\nTEXT ·f(SB), NOSPLIT, $0-0\n"+body+"\tRET\n") if len(errs) > 0 { t.Fatalf("parse: %v", errs) } img, err := AssembleFileARM64(f) if err != nil { t.Fatalf("AssembleFileARM64: %v", err) } return img.Code } // TestArm64HalfFCVT pins the half-precision conversions beside their single // and double relatives, the FPOP1S rows with the half type field. func TestArm64HalfFCVT(t *testing.T) { got := arm64Words(t, "\tFCVTHS F1, F2\n\tFCVTSH F1, F2\n\tFCVTDH F1, F2\n\tFCVTHD F1, F2\n") want := []uint32{ 0x1ee24022, // FCVTHS F1, F2: half to single 0x1e23c022, // FCVTSH F1, F2: single to half 0x1e63c022, // FCVTDH F1, F2: half to double 0x1ee2c022, // FCVTHD F1, F2: double to half 0xd65f03c0, // RET } if len(got) != len(want) { t.Fatalf("word count = %d, want %d", len(got), len(want)) } for i := range want { if got[i] != want[i] { t.Errorf("word %d = %08x, want %08x", i, got[i], want[i]) } } }