fix(asm): tighten the arm64 acceptance toward the toolchain

Assisted-by: GLM 5.3 Flash
This commit is contained in:
petrbalvin committed 2026-10-07 02:36:24 +02:00
1 parent 82d741514a
commit 8ab99c9b0c
4 files changed
+250 -29

No files matched your search

+129 -6
View File
@@ -890,13 +890,38 @@ func encodeARM64DPSR(mnem string, baseOp uint32, ops []*ast.Operand) ([]byte, er
if rn < 0 || rd < 0 {
return nil, fmt.Errorf("invalid register operand in %s", mnem)
}
// SP rules the toolchain enforces on the logical immediates: a
// two-operand spelling with SP as the source is rejected outright
// (illegal source register), a flag-setting logical rejects SP as
// the destination, and a plain logical to SP takes the fast
// bitmask path only inside the addcon band (asm7.go cases 62/13).
if !zrDest && len(ops) == 2 && rn == 31 {
if name := operandRegName(ops[1]); strings.EqualFold(name, "RSP") {
return nil, fmt.Errorf("%s: illegal source register RSP", mnem)
}
}
rspDest := false
if len(ops) == 3 {
rspDest = strings.EqualFold(operandRegName(ops[2]), "RSP")
}
sLogical := mnem == "ANDS" || mnem == "ANDSW" || mnem == "BICS" || mnem == "BICSW"
if rspDest && sLogical {
return nil, fmt.Errorf("%s: illegal combination: the destination cannot be RSP", mnem)
}
n, immr, imms, ok := a64LogicalImm(v, width)
// The toolchain's logical-immediate rows take a real destination
// The toolchain.s logical-immediate rows take a real destination
// only (omovconst guards the bitmask path with rt != REGZERO): a
// non-flag-setting logical to ZR materialises the constant into
// REGTMP (R27) and takes the register form. RSP is a real
// register here, and the flags-only TST spellings keep the fast
// path: ANDS ZR, Rn, #imm is their form.
// register here, but keeps the fast bitmask path inside the
// addcon band alone, and the flags-only TST spellings keep the
// fast path: ANDS ZR, Rn, #imm is their form.
if rspDest {
inBand := v > 0 && (v <= 0xFFF || (v&0xFFF == 0 && v>>12 <= 0xFFF))
if !ok || !inBand {
return nil, fmt.Errorf("%s: illegal combination: the destination cannot be RSP", mnem)
}
}
if ok && (isCmp || !zrDest) {
opc := (baseOp >> 29) & 7
sf := (baseOp >> 31) & 1
@@ -967,10 +992,15 @@ func encodeARM64DPSR(mnem string, baseOp uint32, ops []*ast.Operand) ([]byte, er
spInvolved = true
}
}
if spInvolved && strings.HasPrefix(mnem, "ADD") || spInvolved && strings.HasPrefix(mnem, "SUB") {
if spInvolved && (strings.HasPrefix(mnem, "ADD") || strings.HasPrefix(mnem, "SUB") || isCmp) {
if shiftBits != 0 {
return nil, fmt.Errorf("%s: right shift not encodable against SP", mnem)
}
// The extend field carries 0..4 only (asm7.go: shift amount
// out of range 0 to 4).
if amount > 4 {
return nil, fmt.Errorf("%s: shift amount out of range 0 to 4", mnem)
}
opt := uint32(3) // UXTX
if strings.HasSuffix(mnem, "W") {
opt = 2 // UXTW
@@ -982,6 +1012,13 @@ func encodeARM64DPSR(mnem string, baseOp uint32, ops []*ast.Operand) ([]byte, er
} else {
baseOp |= 1<<21 | extendOpt<<13 | uint32(amount)<<10
}
// The flag-setting add/sub family rejects SP as its destination.
switch mnem {
case "ADDS", "ADDSW", "SUBS", "SUBSW":
if strings.EqualFold(operandRegName(ops[len(ops)-1]), "RSP") {
return nil, fmt.Errorf("%s: illegal destination register RSP", mnem)
}
}
rd := arm64RegNum(operandRegName(ops[len(ops)-1]))
rn := rd
if len(ops) == 3 {
@@ -1028,6 +1065,13 @@ func encodeARM64DPSR(mnem string, baseOp uint32, ops []*ast.Operand) ([]byte, er
if rm < 0 || rn < 0 || rd < 0 {
return nil, fmt.Errorf("invalid register operand in %s", mnem)
}
// The flag-setting add/sub family rejects SP as its destination.
switch mnem {
case "ADDS", "ADDSW", "SUBS", "SUBSW":
if strings.EqualFold(operandRegName(ops[2]), "RSP") {
return nil, fmt.Errorf("%s: illegal destination register RSP", mnem)
}
}
// ADD/SUB against SP take the extended-register form with the
// identity extend, the toolchain's spelling of a plain register
// operand against the stack pointer (asm7.go opxrrr against C_RSP).
@@ -1335,6 +1379,14 @@ func encodeARM64AddSubImm(mnem string, ops []*ast.Operand) ([]byte, error) {
if rn < 0 || rd < 0 {
return nil, fmt.Errorf("invalid register operand in %s", mnem)
}
// The flag-setting add/sub family rejects SP as its destination
// (asm7.go: illegal destination register).
switch mnem {
case "ADDS", "ADDSW", "SUBS", "SUBSW":
if strings.EqualFold(operandRegName(ops[len(ops)-1]), "RSP") {
return nil, fmt.Errorf("%s: illegal destination register RSP", mnem)
}
}
// CMP/CMN discard the destination.
if mnem == "CMP" || mnem == "CMPW" || mnem == "CMN" || mnem == "CMNW" {
rd = 31 // ZR
@@ -2105,6 +2157,15 @@ func encodeARM64MemOp(mnem string, mem *ast.Operand, reg int, load bool, fi arm6
if rn < 0 {
return nil, fmt.Errorf("invalid memory operand")
}
// The MOV family addresses memory through a general register only.
if mem.Addr.Base != "" && arm64RegClassOf(mem.Addr.Base) == arm64ClsFP {
return nil, fmt.Errorf("%s: illegal combination: the base register cannot be FP", mnem)
}
// Writeback with the base doubling as the data register is constrained
// unpredictable.
if wb != "" && rn == reg && rn != 31 {
return nil, fmt.Errorf("%s: constrained unpredictable behavior: the base rides the data register", mnem)
}
lt, ok := a64LoadTable[mnem]
if !ok {
// MOV defaults to MOVD (64-bit load/store).
@@ -2968,6 +3029,11 @@ func encodeARM64CSEL(mnem string, baseOp uint32, ops []*ast.Operand) ([]byte, er
mnem == "CNEG" || mnem == "CNEGW"
if isAlias {
// The aliases invert the condition, which is undefined for AL and NV
// (asm7.go: invalid condition).
if condName := operandRegName(ops[0]); strings.EqualFold(condName, "AL") || strings.EqualFold(condName, "NV") {
return nil, fmt.Errorf("%s: invalid condition %s", mnem, condName)
}
is2op := mnem == "CSET" || mnem == "CSETW" || mnem == "CSETM" || mnem == "CSETMW"
if is2op {
// CSET cond, Rd → CSEL XZR, XZR, Rd, inverted_cond
@@ -3112,6 +3178,14 @@ func encodeARM64Excl(mnem string, baseOp uint32, ops []*ast.Operand) ([]byte, er
// The single-register opcodes pre-set the unused Rs (bits 20:16)
// and Rt2 (bits 14:10) fields to 31; the pair forms carry a real
// Rt2 and keep Rs at 31.
// Constrained unpredictable: the base rides no pair member and
// the pair registers differ.
if rt1 == rt2 {
return nil, fmt.Errorf("%s: constrained unpredictable behavior: the pair registers match", mnem)
}
if bname := operandRegName(ops[0]); strings.EqualFold(bname, fmt.Sprintf("R%d", rt1)) || strings.EqualFold(bname, fmt.Sprintf("R%d", rt2)) {
return nil, fmt.Errorf("%s: constrained unpredictable behavior: the base rides a pair register", mnem)
}
return a64wordLE(baseOp | 0x1F<<16 | uint32(rt2)<<10 | uint32(rn)<<5 | uint32(rt1)), nil
}
rt := arm64RegNum(operandRegName(ops[1]))
@@ -3128,17 +3202,35 @@ func encodeARM64Excl(mnem string, baseOp uint32, ops []*ast.Operand) ([]byte, er
if err != nil {
return nil, err
}
// The status register cannot be SP (asm7.go: illegal combination).
if strings.EqualFold(operandRegName(ops[2]), "RSP") {
return nil, fmt.Errorf("%s: illegal combination: the status register cannot be RSP", mnem)
}
rs := arm64RegNum(operandRegName(ops[2]))
if rs < 0 {
return nil, fmt.Errorf("invalid operand in %s", mnem)
}
if rt1, rt2, ok := arm64PairOf(ops[0]); ok {
// Constrained unpredictable (asm7.go case 59): the pair registers
// differ, and the status register differs from both pair members and
// from a non-SP base.
if rt1 == rt2 {
return nil, fmt.Errorf("%s: constrained unpredictable behavior: the pair registers match", mnem)
}
if rs == rt1 || rs == rt2 || (rs == rn && rn != 31) {
return nil, fmt.Errorf("%s: constrained unpredictable behavior: the status register rides a pair register or the base", mnem)
}
return a64wordLE(baseOp | uint32(rs)<<16 | uint32(rt2)<<10 | uint32(rn)<<5 | uint32(rt1)), nil
}
// Constrained unpredictable (asm7.go case 59): the status register
// differs from Rt and from a non-SP base.
rt := arm64RegNum(operandRegName(ops[0]))
if rt < 0 {
return nil, fmt.Errorf("invalid operand in %s", mnem)
}
if rs == rt || (rs == rn && rn != 31) {
return nil, fmt.Errorf("%s: constrained unpredictable behavior: the status register matches Rt or the base", mnem)
}
return a64wordLE(baseOp | uint32(rs)<<16 | uint32(rn)<<5 | uint32(rt)), nil
}
@@ -3157,6 +3249,10 @@ func encodeARM64LSEAtom(mnem string, baseOp uint32, ops []*ast.Operand) ([]byte,
if err != nil {
return nil, err
}
// The result register cannot be SP (asm7.go: illegal combination).
if strings.EqualFold(operandRegName(ops[2]), "RSP") {
return nil, fmt.Errorf("%s: illegal combination: the result register cannot be RSP", mnem)
}
rt := arm64RegNum(operandRegName(ops[2]))
if rt < 0 {
return nil, fmt.Errorf("invalid operand in %s", mnem)
@@ -3681,6 +3777,30 @@ func encodeARM64Pair(mnem string, baseOp uint32, ops []*ast.Operand, pc int, fi
if !ok {
return nil, fmt.Errorf("%s expects a register pair (Rt1, Rt2)", mnem)
}
// Constrained unpredictable: the pair registers differ, and a
// writeback base rides no pair member.
if rt1 == rt2 {
return nil, fmt.Errorf("%s: constrained unpredictable behavior: the pair registers match", mnem)
}
if wb != "" {
if strings.EqualFold(operandRegName(memOp), fmt.Sprintf("R%d", rt1)) || strings.EqualFold(operandRegName(memOp), fmt.Sprintf("R%d", rt2)) {
return nil, fmt.Errorf("%s: constrained unpredictable behavior: the base rides a pair register", mnem)
}
}
// The FP pairs take FP registers against a GP base (asm7.go: invalid
// register pair).
if strings.HasPrefix(mnem, "FLDP") || strings.HasPrefix(mnem, "FSTP") {
first := strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(pairOp.Raw), "("))
if i := strings.IndexAny(first, ",)"); i >= 0 {
first = strings.TrimSpace(first[:i])
}
if !strings.HasPrefix(first, "F") {
return nil, fmt.Errorf("%s: invalid register pair %s", mnem, pairOp.Raw)
}
if strings.HasPrefix(strings.TrimLeft(operandRegName(memOp), "( "), "F") {
return nil, fmt.Errorf("%s: invalid register pair: the base must be a general register", mnem)
}
}
// Pair access against a static symbol: ADRP R27, sym; ADD R27, R27, #lo;
// LDP/STP (R27), (Rt1, Rt2), with the R_ADDRARM64 pair riding the first
@@ -4274,7 +4394,7 @@ func arm64SimdHasElement(ops []*ast.Operand) bool {
// admits it. It returns the arrangement's index, with a64Arr8B for a bare
// V/F spelling.
func arm64SimdArrs(mnem string, arrs []string, allowed uint16) (int, error) {
sel := a64Arr8B
sel := -1
for _, a := range arrs {
if a == "" {
continue
@@ -4283,11 +4403,14 @@ func arm64SimdArrs(mnem string, arrs []string, allowed uint16) (int, error) {
if i < 0 || specBit(i)&allowed == 0 {
return 0, fmt.Errorf("%s: invalid arrangement %q", mnem, a)
}
if sel != a64Arr8B && sel != i {
if sel >= 0 && sel != i {
return 0, fmt.Errorf("%s: mixed arrangements", mnem)
}
sel = i
}
if sel < 0 {
sel = a64Arr8B
}
return sel, nil
}
+19 -23
View File
@@ -565,16 +565,12 @@ func init() {
a64InstrTable["BFXILW"] = a64Enc{format: a64FBitfieldAlias, op: 0<<31 | 1<<29 | 0x26<<23}
a64InstrTable["SBFIZ"] = a64Enc{format: a64FBitfieldAlias, op: 0x93400000}
a64InstrTable["SBFIZW"] = a64Enc{format: a64FBitfieldAlias, op: 0x13000000}
a64InstrTable["UBFIZ"] = a64Enc{format: a64FBitfieldAlias, op: 0x53000000}
a64InstrTable["UBFIZW"] = a64Enc{format: a64FBitfieldAlias, op: 0x33000000}
a64InstrTable["UBFIZ"] = a64Enc{format: a64FBitfieldAlias, op: 0xd3400000}
a64InstrTable["UBFIZW"] = a64Enc{format: a64FBitfieldAlias, op: 0x53000000}
a64InstrTable["SBFM"] = a64Enc{format: a64FBitfield, op: 1<<31 | 0<<29 | 0x26<<23 | 1<<22}
a64InstrTable["SBFMW"] = a64Enc{format: a64FBitfield, op: 0<<31 | 0<<29 | 0x26<<23 | 0<<22}
a64InstrTable["UBFM"] = a64Enc{format: a64FBitfield, op: 1<<31 | 2<<29 | 0x26<<23 | 1<<22}
a64InstrTable["UBFMW"] = a64Enc{format: a64FBitfield, op: 0<<31 | 2<<29 | 0x26<<23 | 0<<22}
a64InstrTable["BFI"] = a64Enc{format: a64FBitfield, op: 1<<31 | 2<<29 | 0x26<<23 | 1<<22}
a64InstrTable["BFIW"] = a64Enc{format: a64FBitfield, op: 0<<31 | 2<<29 | 0x26<<23 | 0<<22}
a64InstrTable["BFXIL"] = a64Enc{format: a64FBitfield, op: 1<<31 | 1<<29 | 0x26<<23 | 1<<22}
a64InstrTable["BFXILW"] = a64Enc{format: a64FBitfield, op: 0<<31 | 1<<29 | 0x26<<23 | 0<<22}
// ---- FP 3-operand (Rm, Rn, Rd): FADD, FSUB, FMUL, FDIV, FMAX, FMIN, FNMUL ----
fp3 := map[string]uint32{
@@ -1071,7 +1067,7 @@ func a64ElemLetter(s string) bool {
// arrangement"). fpAcrossArrs bounds the across-vector reductions, which do
// take the half width.
var fpSimdArrs = uint16(1<<a64Arr2S | 1<<a64Arr4S | 1<<a64Arr2D)
var fpAcrossArrs = uint16(1<<a64Arr4H | 1<<a64Arr8H | 1<<a64Arr2S | 1<<a64Arr4S)
var fpAcrossArrs = uint16(1 << a64Arr4S)
// a64FPArrBits carries the bits an arrangement contributes to the FP SIMD
// words: the FP size field is a single bit at bit 22 (0 for the S widths, 1
@@ -1148,31 +1144,31 @@ var a64SimdVTable = map[string]a64SimdVSpec{
// Saturating, halving, polynomial and pairwise arithmetic, the logical
// VBIT/VBSL family and the FP pairwise forms: word-verified against go
// tool asm.
"VBIC": {0x0e601c00, 0x7f, false, false},
"VBIF": {0x2ee01c00, 0x7f, false, false},
"VBIT": {0x6ea01c00, 0x7f, false, false},
"VBSL": {0x6e601c00, 0x7f, false, false},
"VBIC": {0x0e601c00, 0x03, false, false}, // logical ops accept 8B and 16B only
"VBIF": {0x2ee01c00, 0x03, false, false},
"VBIT": {0x6ea01c00, 0x03, false, false},
"VBSL": {0x6e601c00, 0x03, false, false},
"VCMTST": {0x0e208c00, 0x7f, false, false},
"VFADDP": {0x2e20d400, fpSimdArrs, false, true},
"VFMAXP": {0x2e20f400, fpSimdArrs, false, true},
"VFMINP": {0x6ea0f400, fpSimdArrs, false, true},
"VFMAXNMP": {0x2e20c400, fpSimdArrs, false, true},
"VFMINNMP": {0x6ea0c400, fpSimdArrs, false, true},
"VMLA": {0x4ea09400, 0x7f, false, false},
"VMLS": {0x6ea09400, 0x7f, false, false},
"VORN": {0x4ee01c00, 0x7f, false, false},
"VMLA": {0x4ea09400, 0x3f, false, false}, // no 2D: integer multiply stops at 4S
"VMLS": {0x6ea09400, 0x3f, false, false},
"VORN": {0x4ee01c00, 0x03, false, false},
"VSHADD": {0x4ea00400, 0x7f, false, false},
"VSRHADD": {0x4ea01400, 0x7f, false, false},
"VUHADD": {0x6ea00400, 0x7f, false, false},
"VURHADD": {0x6ea01400, 0x7f, false, false},
"VSMAX": {0x4ea06400, 0x7f, false, false},
"VSMIN": {0x4ea06c00, 0x7f, false, false},
"VSMAXP": {0x4ea0a400, 0x7f, false, false},
"VSMINP": {0x4ea0ac00, 0x7f, false, false},
"VUMAX": {0x2e206400, 0x7f, false, false},
"VUMIN": {0x2e206c00, 0x7f, false, false},
"VUMAXP": {0x6ea0a400, 0x7f, false, false},
"VUMINP": {0x6ea0ac00, 0x7f, false, false},
"VSMAX": {0x4ea06400, 0x3f, false, false}, // no 2D: integer max stops at 4S
"VSMIN": {0x4ea06c00, 0x3f, false, false},
"VSMAXP": {0x4ea0a400, 0x3f, false, false},
"VSMINP": {0x4ea0ac00, 0x3f, false, false},
"VUMAX": {0x2e206400, 0x3f, false, false},
"VUMIN": {0x2e206c00, 0x3f, false, false},
"VUMAXP": {0x6ea0a400, 0x3f, false, false},
"VUMINP": {0x6ea0ac00, 0x3f, false, false},
"VSQADD": {0x4ea00c00, 0x7f, false, false},
"VUQADD": {0x6ea00c00, 0x7f, false, false},
"VSQSUB": {0x4ea02c00, 0x7f, false, false},
@@ -1297,7 +1293,7 @@ var a64SimdV2Table = map[string]a64SimdVSpec{
"VNOT": {0x2e205800, 0x7f, false, false},
"VSQABS": {0x0e207800, 0x7f, false, false},
"VSQNEG": {0x2e207800, 0x7f, false, false},
"VRBIT": {0x2e605800, 0x7f, false, false},
"VRBIT": {0x2e605800, 0x03, false, false}, // 8B and 16B only
"VSCVTF": {0x4e21d800, fpSimdArrs, false, true},
"VUCVTF": {0x6e21d800, fpSimdArrs, false, true},
"VFCVTZS": {0x4ea1b800, fpSimdArrs, false, true},
+1
View File
@@ -2185,3 +2185,4 @@ func TestArm64ZRNameNotNumber(t *testing.T) {
}
}
}
+101
View File
@@ -0,0 +1,101 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
package asm
import (
"os"
"path/filepath"
"strings"
"testing"
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
)
// arm64AcceptedErrorShapes lists the toolchain's arm64error.s spellings gasm
// still accepts, each an acceptance superset with a known shape. The list
// only shrinks: every tightening of the encoder moves spellings out of it,
// and a spelling reappearing here means a regression.
var arm64AcceptedErrorShapes = []string{
// VLD1/VST1 post-index shapes: the register post-index without the .P
// spelling, the .P immediate against the aggregate register size, the
// scaled register increment and the replicating register count.
"VLD1 (R8)(R13), [V2.B16]",
"VST1 [V1.B16], (R8)(R13)",
"VST1.P [V4.S4,V5.S4], 48(R1)",
"VST1.P [V4.S4], 8(R1)",
"VLD1.P 32(R1), [V8.S4, V9.S4, V10.S4]",
"VLD1.P 48(R1), [V7.S4, V8.S4, V9.S4, V10.S4]",
"VLD1.P (R8)(R9<<2), [V2.B16]",
"VST1.P [V1.B16], (R8)(R9<<1)",
"VLD3R.P 24(R15), [V15.H4,V16.H4,V17.H4]",
// REGTMP (R27) as an explicit operand the toolchain refuses, the pair
// split paths included.
"ADD $0x1234567, R27, R3",
"ADD $0x3fffffffc000, R27, R5",
"AND $0x22220000, R27, R4",
"ANDW $0x6006000060060, R27, R5",
"STP (R3, R4), 0x1234567(R27)",
"LDP 0x1234567(R27), (R3, R4)",
"STP (R26, R27), 700(R2)",
// TLBI operand arity and the range-prefetch operand shapes.
"TLBI VMALLE1IS, R0",
"TLBI ALLE3OS, ZR",
"TLBI VAE1IS",
"TLBI RVALE3",
"RPRFM (R1), RSP, PLDKEEP",
// The GP register pairs on the FP mnemonics, the other way round.
"LDP (R0), (F0, F1)",
"STP (F2, F3), (R0)",
// FCVTL exists for the S to D pair alone.
"VFCVTL V1.H4, V2.S4",
}
// TestArm64ToolchainErrorParity walks the toolchain's arm64error.s (Go
// 1.27, arm64) and requires gasm to reject every case the toolchain rejects,
// the documented acceptance supersets above excepted. A live Go toolchain
// is needed for the source file; the test skips without one or in -short.
func TestArm64ToolchainErrorParity(t *testing.T) {
if testing.Short() {
t.Skip("live arm64error.s corpus: skipped in -short mode")
}
goroot := os.Getenv("GOROOT")
if goroot == "" {
t.Skip("no GOROOT")
}
path := filepath.Join(goroot, "src", "cmd", "asm", "internal", "asm", "testdata", "arm64error.s")
data, err := os.ReadFile(path)
if err != nil {
t.Skip(err)
}
allowed := map[string]bool{}
for _, s := range arm64AcceptedErrorShapes {
allowed[s] = true
}
for raw := range strings.SplitSeq(string(data), "\n") {
line := strings.TrimSpace(raw)
if line == "" || strings.HasPrefix(line, "//") || strings.HasPrefix(line, "TEXT") || !strings.Contains(line, "ERROR") {
continue
}
body := line
if i := strings.Index(body, "//"); i >= 0 {
body = strings.TrimSpace(body[:i])
}
body = strings.ReplaceAll(body, "\t", " ")
body = strings.Join(strings.Fields(body), " ")
// Label-relative and non-operand lines need their function context.
if strings.Contains(body, "(PC)") || strings.Contains(body, "(SB)") || strings.Contains(body, "PCALIGN") ||
strings.HasPrefix(body, "RET") || strings.HasPrefix(body, "NOP") || strings.HasPrefix(body, "BRK") ||
strings.Contains(body, "again") || strings.Contains(body, "next") || strings.Contains(body, "loop") {
continue
}
src := "#include \"textflag.h\"\n\nTEXT ·f(SB), NOSPLIT, $0-0\n\t" + body + "\n\tRET\n"
f, perr := parser.Parse("errorparity.s", src)
if len(perr) > 0 {
continue // the parser already rejects the spelling
}
if _, aerr := AssembleFileARM64(f); aerr == nil && !allowed[body] {
t.Errorf("gasm accepts what the toolchain rejects: %s", body)
}
}
}