// Copyright 2026 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package arm64 import ( "cmd/internal/obj" "cmd/internal/obj/arm64" "cmd/internal/src" "testing" ) func TestPairSpills(t *testing.T) { movdLoad := func(dst, base int16, off int64) *obj.Prog { return &obj.Prog{ As: arm64.AMOVD, From: obj.Addr{Type: obj.TYPE_MEM, Reg: base, Offset: off, Name: obj.NAME_AUTO}, To: obj.Addr{Type: obj.TYPE_REG, Reg: dst}, } } movdStore := func(src, base int16, off int64) *obj.Prog { return &obj.Prog{ As: arm64.AMOVD, From: obj.Addr{Type: obj.TYPE_REG, Reg: src}, To: obj.Addr{Type: obj.TYPE_MEM, Reg: base, Offset: off, Name: obj.NAME_AUTO}, } } // param rewrites p's memory operand to NAME_PARAM. param := func(p *obj.Prog) *obj.Prog { if p.From.Type == obj.TYPE_MEM { p.From.Name = obj.NAME_PARAM } else { p.To.Name = obj.NAME_PARAM } return p } chain := func(progs ...*obj.Prog) *obj.Prog { for i := 0; i < len(progs)-1; i++ { progs[i].Link = progs[i+1] } return progs[0] } countAs := func(head *obj.Prog) map[obj.As]int { m := map[obj.As]int{} for p := head; p != nil; p = p.Link { m[p.As]++ } return m } // pairWant describes the expected operands of the fused LDP/STP: // MOVDs of lo and hi (lo at the lower address off, hi at off+8) // against base, with the memory operand's addressing class name. type pairWant struct { as obj.As base int16 off int64 lo, hi int16 name obj.AddrName } tests := []struct { name string framesize int64 setup func() (*obj.Prog, []obj.JumpTable) wantLDP int wantSTP int wantMOVD int wantNOP int wantPair *pairWant }{ { name: "adjacent LDRs fuse to LDP", setup: func() (*obj.Prog, []obj.JumpTable) { return chain( movdLoad(arm64.REG_R0, arm64.REGSP, 16), movdLoad(arm64.REG_R1, arm64.REGSP, 24), ), nil }, wantLDP: 1, wantNOP: 1, wantPair: &pairWant{arm64.ALDP, arm64.REGSP, 16, arm64.REG_R0, arm64.REG_R1, obj.NAME_AUTO}, }, { name: "adjacent STRs fuse to STP", setup: func() (*obj.Prog, []obj.JumpTable) { return chain( movdStore(arm64.REG_R0, arm64.REGSP, 16), movdStore(arm64.REG_R1, arm64.REGSP, 24), ), nil }, wantSTP: 1, wantNOP: 1, wantPair: &pairWant{arm64.ASTP, arm64.REGSP, 16, arm64.REG_R0, arm64.REG_R1, obj.NAME_AUTO}, }, { name: "reverse-order LDRs fuse to LDP", setup: func() (*obj.Prog, []obj.JumpTable) { // p loads from the higher address, q from the lower. return chain( movdLoad(arm64.REG_R0, arm64.REGSP, 24), movdLoad(arm64.REG_R1, arm64.REGSP, 16), ), nil }, wantLDP: 1, wantNOP: 1, wantPair: &pairWant{arm64.ALDP, arm64.REGSP, 16, arm64.REG_R1, arm64.REG_R0, obj.NAME_AUTO}, }, { name: "reverse-order STRs fuse to STP", setup: func() (*obj.Prog, []obj.JumpTable) { // p stores to the higher address, q to the lower. return chain( movdStore(arm64.REG_R0, arm64.REGSP, 24), movdStore(arm64.REG_R1, arm64.REGSP, 16), ), nil }, wantSTP: 1, wantNOP: 1, wantPair: &pairWant{arm64.ASTP, arm64.REGSP, 16, arm64.REG_R1, arm64.REG_R0, obj.NAME_AUTO}, }, { name: "PARAM pair fuses", setup: func() (*obj.Prog, []obj.JumpTable) { return chain( param(movdLoad(arm64.REG_R0, arm64.REGSP, 16)), param(movdLoad(arm64.REG_R1, arm64.REGSP, 24)), ), nil }, wantLDP: 1, wantNOP: 1, wantPair: &pairWant{arm64.ALDP, arm64.REGSP, 16, arm64.REG_R0, arm64.REG_R1, obj.NAME_PARAM}, }, { name: "mixed AUTO and PARAM do not fuse", setup: func() (*obj.Prog, []obj.JumpTable) { return chain( movdLoad(arm64.REG_R0, arm64.REGSP, 16), param(movdLoad(arm64.REG_R1, arm64.REGSP, 24)), ), nil }, wantMOVD: 2, }, { name: "non-adjacent offsets do not fuse", setup: func() (*obj.Prog, []obj.JumpTable) { return chain( movdLoad(arm64.REG_R0, arm64.REGSP, 16), movdLoad(arm64.REG_R1, arm64.REGSP, 32), ), nil }, wantMOVD: 2, }, { name: "different base registers do not fuse", setup: func() (*obj.Prog, []obj.JumpTable) { return chain( movdLoad(arm64.REG_R0, arm64.REGSP, 16), movdLoad(arm64.REG_R1, arm64.REG_R28, 24), ), nil }, wantMOVD: 2, }, { name: "load followed by store does not fuse", setup: func() (*obj.Prog, []obj.JumpTable) { return chain( movdLoad(arm64.REG_R0, arm64.REGSP, 16), movdStore(arm64.REG_R1, arm64.REGSP, 24), ), nil }, wantMOVD: 2, }, { name: "same destination register does not fuse", setup: func() (*obj.Prog, []obj.JumpTable) { return chain( movdLoad(arm64.REG_R0, arm64.REGSP, 16), movdLoad(arm64.REG_R0, arm64.REGSP, 24), ), nil }, wantMOVD: 2, }, { name: "first load writing the second's base does not fuse", setup: func() (*obj.Prog, []obj.JumpTable) { // Executed sequentially, the second load computes its // address from the value the first load just wrote into // R1; LDP would compute both addresses from the original // R1. return chain( movdLoad(arm64.REG_R1, arm64.REG_R1, 16), movdLoad(arm64.REG_R2, arm64.REG_R1, 24), ), nil }, wantMOVD: 2, }, { name: "second load writing the base fuses", setup: func() (*obj.Prog, []obj.JumpTable) { // No address depends on the base after the second load // overwrites it, so LDP has identical semantics. return chain( movdLoad(arm64.REG_R0, arm64.REG_R5, 16), movdLoad(arm64.REG_R5, arm64.REG_R5, 24), ), nil }, wantLDP: 1, wantNOP: 1, wantPair: &pairWant{arm64.ALDP, arm64.REG_R5, 16, arm64.REG_R0, arm64.REG_R5, obj.NAME_AUTO}, }, { name: "store whose source is the base fuses", setup: func() (*obj.Prog, []obj.JumpTable) { // Stores never write registers, so there is no base // hazard for STP. return chain( movdStore(arm64.REG_R5, arm64.REG_R5, 16), movdStore(arm64.REG_R1, arm64.REG_R5, 24), ), nil }, wantSTP: 1, wantNOP: 1, wantPair: &pairWant{arm64.ASTP, arm64.REG_R5, 16, arm64.REG_R5, arm64.REG_R1, obj.NAME_AUTO}, }, { name: "skip when second instruction is a branch target", setup: func() (*obj.Prog, []obj.JumpTable) { p1 := movdLoad(arm64.REG_R0, arm64.REGSP, 16) p2 := movdLoad(arm64.REG_R1, arm64.REGSP, 24) br := &obj.Prog{As: arm64.AB, To: obj.Addr{Type: obj.TYPE_BRANCH}} br.To.SetTarget(p2) return chain(br, p1, p2), nil }, wantMOVD: 2, }, { name: "skip when second instruction is a backward-branch target", setup: func() (*obj.Prog, []obj.JumpTable) { // The branch sits after the pair, so target collection // must consider the whole Prog list, not just what // precedes the pair. p1 := movdLoad(arm64.REG_R0, arm64.REGSP, 16) p2 := movdLoad(arm64.REG_R1, arm64.REGSP, 24) br := &obj.Prog{As: arm64.AB, To: obj.Addr{Type: obj.TYPE_BRANCH}} br.To.SetTarget(p2) return chain(p1, p2, br), nil }, wantMOVD: 2, }, { name: "skip when second instruction is a jump-table target", setup: func() (*obj.Prog, []obj.JumpTable) { p1 := movdLoad(arm64.REG_R0, arm64.REGSP, 16) p2 := movdLoad(arm64.REG_R1, arm64.REGSP, 24) return chain(p1, p2), []obj.JumpTable{{Targets: []*obj.Prog{p2}}} }, wantMOVD: 2, }, { name: "skip when second instruction is a statement boundary", setup: func() (*obj.Prog, []obj.JumpTable) { // Statement-marked instructions (including those genssa // reuses as inline marks, which it promotes to // statements) must not become zero-sized. The statement // bit needs a known position to stick to. var tab src.PosTable pos := tab.XPos(src.MakePos(src.NewFileBase("f.go", "f.go"), 1, 1)) p1 := movdLoad(arm64.REG_R0, arm64.REGSP, 16) p2 := movdLoad(arm64.REG_R1, arm64.REGSP, 24) p2.Pos = pos.WithIsStmt() return chain(p1, p2), nil }, wantMOVD: 2, }, { name: "at the encodable bound fuses", setup: func() (*obj.Prog, []obj.JumpTable) { // Resolved offset 496+0+8 = 504, LDP's maximum. return chain( movdLoad(arm64.REG_R0, arm64.REGSP, 496), movdLoad(arm64.REG_R1, arm64.REGSP, 504), ), nil }, wantLDP: 1, wantNOP: 1, wantPair: &pairWant{arm64.ALDP, arm64.REGSP, 496, arm64.REG_R0, arm64.REG_R1, obj.NAME_AUTO}, }, { name: "resolved offset out of LDP range does not fuse", setup: func() (*obj.Prog, []obj.JumpTable) { // Resolved offset 504+0+8 = 512, just past the bound. return chain( movdLoad(arm64.REG_R0, arm64.REGSP, 504), movdLoad(arm64.REG_R1, arm64.REGSP, 512), ), nil }, wantMOVD: 2, }, { name: "large frame pushes resolved offset out of range", framesize: 1024, setup: func() (*obj.Prog, []obj.JumpTable) { // 16+1024+8 = 1048: fusing would force an // assembler-synthesized address, no smaller than the // original pair. return chain( movdLoad(arm64.REG_R0, arm64.REGSP, 16), movdLoad(arm64.REG_R1, arm64.REGSP, 24), ), nil }, wantMOVD: 2, }, { name: "deep spill slots fuse in a large frame", framesize: 1024, setup: func() (*obj.Prog, []obj.JumpTable) { // -528+1024+8 = 504: encodable even though the // pre-resolution offset is far below -512. return chain( movdLoad(arm64.REG_R0, arm64.REGSP, -528), movdLoad(arm64.REG_R1, arm64.REGSP, -520), ), nil }, wantLDP: 1, wantNOP: 1, wantPair: &pairWant{arm64.ALDP, arm64.REGSP, -528, arm64.REG_R0, arm64.REG_R1, obj.NAME_AUTO}, }, { name: "misaligned offsets do not fuse", setup: func() (*obj.Prog, []obj.JumpTable) { return chain( movdLoad(arm64.REG_R0, arm64.REGSP, -12), movdLoad(arm64.REG_R1, arm64.REGSP, -4), ), nil }, wantMOVD: 2, }, { name: "three adjacent loads fuse greedily", setup: func() (*obj.Prog, []obj.JumpTable) { return chain( movdLoad(arm64.REG_R0, arm64.REGSP, 16), movdLoad(arm64.REG_R1, arm64.REGSP, 24), movdLoad(arm64.REG_R2, arm64.REGSP, 32), ), nil }, wantLDP: 1, wantNOP: 1, wantMOVD: 1, }, { name: "intervening instruction blocks fusion", setup: func() (*obj.Prog, []obj.JumpTable) { return chain( movdLoad(arm64.REG_R0, arm64.REGSP, 16), &obj.Prog{As: arm64.AHINT}, movdLoad(arm64.REG_R1, arm64.REGSP, 24), ), nil }, wantMOVD: 2, }, { name: "pre-indexed addressing does not fuse", setup: func() (*obj.Prog, []obj.JumpTable) { p1 := movdLoad(arm64.REG_R0, arm64.REGSP, 16) p1.Scond = arm64.C_XPRE return chain(p1, movdLoad(arm64.REG_R1, arm64.REGSP, 24)), nil }, wantMOVD: 2, }, { name: "post-indexed addressing does not fuse", setup: func() (*obj.Prog, []obj.JumpTable) { p1 := movdLoad(arm64.REG_R0, arm64.REGSP, 16) p1.Scond = arm64.C_XPOST return chain(p1, movdLoad(arm64.REG_R1, arm64.REGSP, 24)), nil }, wantMOVD: 2, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { head, jumpTables := tt.setup() pairSpills(head, tt.framesize, jumpTables) got := countAs(head) if got[arm64.ALDP] != tt.wantLDP { t.Errorf("ALDP count = %d, want %d", got[arm64.ALDP], tt.wantLDP) } if got[arm64.ASTP] != tt.wantSTP { t.Errorf("ASTP count = %d, want %d", got[arm64.ASTP], tt.wantSTP) } if got[arm64.AMOVD] != tt.wantMOVD { t.Errorf("AMOVD count = %d, want %d", got[arm64.AMOVD], tt.wantMOVD) } if got[obj.ANOP] != tt.wantNOP { t.Errorf("ANOP count = %d, want %d", got[obj.ANOP], tt.wantNOP) } if w := tt.wantPair; w != nil { var fused *obj.Prog for p := head; p != nil; p = p.Link { if p.As == arm64.ALDP || p.As == arm64.ASTP { fused = p break } } if fused == nil { t.Fatal("no LDP/STP emitted") } mem, regs := &fused.From, &fused.To // LDP order if fused.As == arm64.ASTP { regs, mem = &fused.From, &fused.To } if fused.As != w.as { t.Errorf("fused As = %v, want %v", fused.As, w.as) } if mem.Type != obj.TYPE_MEM || mem.Reg != w.base || mem.Offset != w.off || mem.Name != w.name { t.Errorf("memory operand = {Type %v, Reg %v, Offset %d, Name %v}, want {TYPE_MEM, %v, %d, %v}", mem.Type, mem.Reg, mem.Offset, mem.Name, w.base, w.off, w.name) } if regs.Type != obj.TYPE_REGREG || regs.Reg != w.lo || regs.Offset != int64(w.hi) { t.Errorf("register pair = {Type %v, Reg %v, Offset %v}, want {TYPE_REGREG, %v, %v}", regs.Type, regs.Reg, regs.Offset, w.lo, w.hi) } } }) } }