feat(asm): encode the arm64 register-offset addressing forms

Assisted-by: GLM 5.3 Flash
This commit is contained in:
petrbalvin committed 2026-10-07 02:36:24 +02:00
1 parent a344399b81
commit 458cdd2066
4 files changed
+221 -1

No files matched your search

+80 -1
View File
@@ -2008,6 +2008,14 @@ func encodeARM64MemOp(mnem string, mem *ast.Operand, reg int, load bool, fi arm6
lt = a64LoadTable["MOVD"]
}
// Register-offset addressing: (Rn)(Rm), (Rn)(Rm<<k), (Rn)(Rm.UXTW) and
// friends. The toolchain accepts the LSL (UXTX), UXTW, SXTW and SXTX
// options with a shift amount of either zero or the access size's log2,
// rejects every other spelling, and rejects the whole form on FMOVQ.
if mem.Addr.Index != "" {
return encodeARM64RegOffset(mnem, mem, rn, reg, load, lt, wb)
}
scale := a64LSScale(lt)
storeOpc := a64StoreOpc(lt)
var opc int
@@ -2094,7 +2102,78 @@ func arm64PoolAccess(mnem string, lt a64LSType, opc int, off int64, rn, reg, pc
// option the toolchain's register-offset accesses use:
// size<<30 | 0x38<<24 | V<<26 | opc<<22 | 1<<21 | Rm<<16 | 011<<13 | 10<<10 | Rn<<5 | Rt.
func a64LSReg(size, V, opc, rm, rn, rt uint32) uint32 {
return size<<30 | 0x38<<24 | V<<26 | opc<<22 | 1<<21 | rm<<16 | 3<<13 | 2<<10 | rn<<5 | rt
return a64LSRegExt(size, V, opc, rm, rn, rt, 3, 0)
}
// a64LSRegExt encodes the extended-register-offset load/store:
// size<<30 | 0x38<<24 | V<<26 | opc<<22 | 1<<21 | Rm<<16 | option<<13 |
// S<<12 | 10<<10 | Rn<<5 | Rt, with S meaning "scale by the access size".
func a64LSRegExt(size, V, opc, rm, rn, rt, option, s uint32) uint32 {
return size<<30 | 0x38<<24 | V<<26 | opc<<22 | 1<<21 | rm<<16 | option<<13 | s<<12 | 2<<10 | rn<<5 | rt
}
// encodeARM64RegOffset encodes the register-offset addressing forms of the
// MOV family: (Rn)(Rm), (Rn)(Rm*1), (Rn)(Rm<<k) and the extend spellings
// (Rn)(Rm.UXTW<<k), (Rn)(Rm.SXTW), (Rn)(Rm.SXTX<<k). The toolchain's
// contract, probed against `go tool asm`: the four options UXTX/LSL, UXTW,
// SXTW and SXTX exist (the spellings UXTX, UXTB, SXTH and friends are
// rejected), the shift amount is either zero or the access size's log2, FMOVQ
// carries no register-offset form at all, and writeback does not combine with
// an index.
func encodeARM64RegOffset(mnem string, mem *ast.Operand, rn, reg int, load bool, lt a64LSType, wb string) ([]byte, error) {
if wb != "" {
return nil, fmt.Errorf("%s: writeback does not combine with a register offset", mnem)
}
if mnem == "FMOVQ" {
return nil, fmt.Errorf("%s: illegal combination: the register offset form does not exist", mnem)
}
if mem.Addr.Scale > 1 {
return nil, fmt.Errorf("%s: the register offset addressing mode takes no scale factor", mnem)
}
name, ext, _ := strings.Cut(mem.Addr.Index, ".")
var option uint32
switch strings.ToUpper(ext) {
case "":
option = 3 // UXTX / LSL
case "UXTW":
option = 2
case "SXTW":
option = 6
case "SXTX":
option = 7
default:
return nil, fmt.Errorf("%s: invalid shift for the register offset addressing mode: %s", mnem, ext)
}
rm := arm64RegNum(name)
if rm < 0 {
return nil, fmt.Errorf("%s: invalid index register %q", mnem, name)
}
amount := 0
shift := strings.Join(strings.Fields(mem.Addr.Shift), "")
shift = strings.TrimSuffix(shift, ")")
if shift != "" {
if !strings.HasPrefix(shift, "<<") {
return nil, fmt.Errorf("%s: invalid shift for the register offset addressing mode: %s", mnem, shift)
}
v, err := strconv.Atoi(strings.TrimPrefix(shift, "<<"))
if err != nil {
return nil, fmt.Errorf("%s: invalid shift for the register offset addressing mode: %s", mnem, shift)
}
amount = v
}
log2 := bits.TrailingZeros64(uint64(a64LSScale(lt)))
if amount != 0 && amount != log2 {
return nil, fmt.Errorf("%s: invalid index shift amount %d", mnem, amount)
}
s := uint32(0)
if amount == log2 && log2 > 0 {
s = 1
}
opc := lt.opc
if !load {
opc = a64StoreOpc(lt)
}
return a64wordLE(a64LSRegExt(uint32(lt.size), uint32(lt.V), uint32(opc), uint32(rm), uint32(rn), uint32(reg), option, s)), nil
}
// arm64OffsetSplitReach reports whether an offset stays within the band the
+69
View File
@@ -1825,3 +1825,72 @@ func TestArm64SplitFrameSpadj(t *testing.T) {
}
}
}
// 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))
}
}
}
+71
View File
@@ -0,0 +1,71 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
// Differential kernel for the arm64 register-offset addressing forms:
// (Rn)(Rm), (Rn)(Rm<<k), (Rn)(Rm*1) and the extend spellings
// (Rn)(Rm.UXTW<<k), (Rn)(Rm.SXTW), (Rn)(Rm.SXTX<<k). Every function is
// byte-compared against go tool asm.
#include "textflag.h"
// func loads()
TEXT ·loads(SB), NOSPLIT, $0-0
MOVD (R2)(R6.SXTW), R4
MOVD (R3)(R6), R5
MOVD (R3)(R6*1), R5
MOVD (R2)(R6), R4
MOVWU (R19)(R20<<2), R20
MOVD (R2)(R3<<0), R1
MOVD (R2)(R6<<3), R4
MOVD (R3)(R7.SXTX<<3), R8
MOVWU (R5)(R4.UXTW), R10
MOVBU (R3)(R9.UXTW), R8
MOVBU (R5)(R8), R10
MOVHU (R2)(R7.SXTW<<1), R11
MOVHU (R1)(R2<<1), R5
MOVBU (R9)(R3.UXTW), R6
MOVBU (R10)(R6), R15
MOVBU (R29)(R30<<0), R14
MOVH (R5)(R7.SXTX<<1), R19
MOVH (R8)(R4<<1), R10
MOVW (R9)(R8.SXTW<<2), R19
MOVW (R1)(R4.SXTX), R11
MOVW (R1)(R4.SXTX), ZR
MOVW (R2)(R5), R12
FMOVS (R2)(R6), F4
FMOVS (R2)(R6<<2), F4
FMOVD (R2)(R6), F4
FMOVD (R2)(R6<<3), F4
RET
// func stores()
TEXT ·stores(SB), NOSPLIT, $0-0
MOVD R5, (R2)(R6<<3)
MOVD R9, (R6)(R7.SXTX<<3)
MOVD ZR, (R6)(R7.SXTX<<3)
MOVW R8, (R2)(R3.UXTW<<2)
MOVW R7, (R3)(R4.SXTW)
MOVB R4, (R2)(R6.SXTX)
MOVB R8, (R3)(R9.UXTW)
MOVB R10, (R5)(R8)
MOVB R10, (R5)(R8*1)
MOVH R11, (R2)(R7.SXTW<<1)
MOVH R5, (R1)(R2<<1)
MOVH R7, (R2)(R5.SXTX<<1)
MOVH R8, (R3)(R6.UXTW)
MOVB R4, (R2)(R6.SXTX)
FMOVS F4, (R2)(R6)
FMOVS F4, (R2)(R6<<2)
FMOVD F4, (R2)(R6)
FMOVD F4, (R2)(R6<<3)
RET
// func frames()
TEXT ·frames(SB), NOSPLIT, $0-0
MOVD (RSP)(R3), R1
MOVD (R2)(RSP), R1
MOVD (R2)(R2), R2
MOVW (R2)(R3.SXTW), R1
MOVBU (R1)(R2<<0), R3
MOVH (R1)(R2<<1), R3
RET
+1
View File
@@ -37,6 +37,7 @@ func TestGroundTruthARM64(t *testing.T) {
"../testdata/verify/atomics_arm64.s",
"../testdata/verify/qmov_arm64.s",
"../testdata/verify/splits_arm64.s",
"../testdata/verify/regoffset_arm64.s",
"../testdata/verify/crypto_arm64.s",
"../testdata/verify/integer_arm64.s",
"../testdata/verify/simd_arm64.s",