diff --git a/asm/arm64_assemble.go b/asm/arm64_assemble.go index e47af38..beea5d0 100644 --- a/asm/arm64_assemble.go +++ b/asm/arm64_assemble.go @@ -3448,6 +3448,34 @@ func arm64SimdNarrowPair(mnem, src, dst string, two bool) error { return nil } +// arm64SimdFCVTLongPair validates the FCVTL width pair: S to D alone, with +// the plain spelling reading S2 and the .2 spelling S4, the destination +// always D2. +func arm64SimdFCVTLongPair(mnem, src, dst string, two bool) error { + wantSrc := "S2" + if two { + wantSrc = "S4" + } + if src != wantSrc || dst != "D2" { + return fmt.Errorf("%s: operand mismatch for the %s spelling: want %s, %s", mnem, mnem, wantSrc, "D2") + } + return nil +} + +// arm64SimdFCVTNarrowPair validates the FCVTN width pair: D to S alone, the +// source always D2, the destination S2 for the plain spelling and S4 for +// the .2 spelling. +func arm64SimdFCVTNarrowPair(mnem, src, dst string, two bool) error { + wantDst := "S2" + if two { + wantDst = "S4" + } + if src != "D2" || dst != wantDst { + return fmt.Errorf("%s: operand mismatch for the %s spelling: want %s, %s", mnem, mnem, "D2", wantDst) + } + return nil +} + // arm64SimdNLArrBits returns the arrangement bits a narrow/long/wide // instruction contributes: the driving arrangement's size and Q bits, for // the FCVT family only the Q bit (whose size field is fixed in the base), @@ -3504,7 +3532,19 @@ func encodeARM64SimdNL(mnem string, spec a64SimdNLSpec, ops []*ast.Operand) ([]b } var drive string var pairErr error - if spec.form == a64NLTwoNarrow { + if spec.qonly { + // The FCVT conversions are pinned to one width pair: S to D for + // the lengthening (FCVTL S2→D2, FCVTL2 S4→D2), D to S for the + // narrowing (FCVTN D2→S2, FCVTN2 D2→S4); the size field is + // fixed in the opcode and no other arrangement exists. + if spec.form == a64NLTwoNarrow { + drive = vd.arr + pairErr = arm64SimdFCVTNarrowPair(mnem, vn.arr, vd.arr, two) + } else { + drive = vn.arr + pairErr = arm64SimdFCVTLongPair(mnem, vn.arr, vd.arr, two) + } + } else if spec.form == a64NLTwoNarrow { // XTN/FCVTN: wide source into a narrow destination; the // arrangement bits follow the destination. drive, pairErr = vd.arr, arm64SimdNarrowPair(mnem, vn.arr, vd.arr, two) @@ -3863,7 +3903,9 @@ func encodeARM64Pair(mnem string, baseOp uint32, ops []*ast.Operand, pc int, fi } } // The FP pairs take FP registers against a GP base (asm7.go: invalid - // register pair). + // register pair). The integer pairs are the mirror image: an F pair on + // LDP/STP is an invalid register pair too, the toolchain having no class + // for it. 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 { @@ -3875,6 +3917,14 @@ func encodeARM64Pair(mnem string, baseOp uint32, ops []*ast.Operand, pc int, fi if strings.HasPrefix(strings.TrimLeft(operandRegName(memOp), "( "), "F") { return nil, fmt.Errorf("%s: invalid register pair: the base must be a general register", mnem) } + } else { + 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) + } } // Pair access against a static symbol: ADRP R27, sym; ADD R27, R27, #lo; @@ -4192,19 +4242,28 @@ func encodeARM64Sys(mnem string, ops []*ast.Operand) ([]byte, error) { w := 0xd5080000 | inst.op1<<16 | 7<<12 | inst.cm<<8 | inst.op2<<5 return a64wordLE(w | uint32(rn)&31), nil case "TLBI": - // The register operand is optional: TLBI VMALLE1IS alone means ZR. + // The register operand's arity follows the operation (asm7.go's + // sysInstFields): the by-address spellings take the Xt and the + // whole-entry ones (VMALL*, ALL**) take none, an explicit register + // being extraneous. if len(ops) != 1 && len(ops) != 2 { return nil, fmt.Errorf("TLBI expects [, Rn]") } - inst, ok := a64TLBIOps[operandRegName(ops[0])] + name := operandRegName(ops[0]) + inst, ok := a64TLBIOps[name] if !ok { - return nil, fmt.Errorf("TLBI: unknown operation %q", operandRegName(ops[0])) + return nil, fmt.Errorf("TLBI: unknown operation %q", name) } rt := 31 if len(ops) == 2 { + if !arm64TLBITakesReg(name) { + return nil, fmt.Errorf("TLBI %s: extraneous register at operand 2", name) + } if rt = arm64RegNum(operandRegName(ops[1])); rt < 0 { return nil, fmt.Errorf("TLBI: invalid register operand") } + } else if arm64TLBITakesReg(name) { + return nil, fmt.Errorf("TLBI %s: missing register at operand 2", name) } w := 0xd5080000 | inst.op1<<16 | 8<<12 | inst.cm<<8 | inst.op2<<5 return a64wordLE(w | uint32(rt)&31), nil @@ -4328,6 +4387,11 @@ func encodeARM64Sys(mnem string, ops []*ast.Operand) ([]byte, error) { if rm < 0 { return nil, fmt.Errorf("RPRFM: invalid register operand") } + // The toolchain's class ladder takes a plain R register alone: RSP + // and ZR are illegal combinations (asm7.go case 110). + if name := operandRegName(ops[1]); strings.EqualFold(name, "RSP") || strings.EqualFold(name, "SP") || strings.EqualFold(name, "ZR") { + return nil, fmt.Errorf("RPRFM: illegal combination: %s is not a general register", name) + } var op uint64 if isImmOperand(ops[2]) { op = uint64(arm64Imm64(ops[2])) @@ -5015,8 +5079,8 @@ func encodeARM64VLDST(mnem string, post uint32, ops []*ast.Operand) ([]byte, err } // The toolchain's addressing contract for the structure forms (asm7.go's // class ladder): an index register exists only as the post-index - // increment, so it is illegal without .P and takes neither an offset nor - // an extend nor a shift beside it. + // increment, so it is illegal without .P, and the immediate increment + // must be exactly the bytes the whole list transfers. idx := ops[memIdx].Addr.Index if idx != "" && post == 0 { return nil, fmt.Errorf("%s: illegal combination: the register index is a post-index, it needs the .P spelling", mnem) diff --git a/asm/arm64_encode_test.go b/asm/arm64_encode_test.go index 1449a60..ba0be4c 100644 --- a/asm/arm64_encode_test.go +++ b/asm/arm64_encode_test.go @@ -2363,6 +2363,56 @@ func TestArm64VLDSTPostIndexContract(t *testing.T) { } } +// 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 diff --git a/asm/arm64_errorparity_test.go b/asm/arm64_errorparity_test.go index 5e7fdeb..4f27249 100644 --- a/asm/arm64_errorparity_test.go +++ b/asm/arm64_errorparity_test.go @@ -15,28 +15,15 @@ import ( // 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{ - // spelling, the .P immediate against the aggregate register size, the - // REGTMP (R27) as an explicit operand the toolchain refuses, the pair - // split paths included. - // 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", -} +// and a spelling reappearing here means a regression. The catalogue is +// empty as of Go 1.27: every line the toolchain's corpus rejects, gasm +// rejects too. +var arm64AcceptedErrorShapes = []string{} // 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. +// 1.27, arm64) and requires gasm to reject every case the toolchain rejects. +// 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") @@ -65,12 +52,6 @@ func TestArm64ToolchainErrorParity(t *testing.T) { } 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 { diff --git a/asm/arm64_sysregs.go b/asm/arm64_sysregs.go index 0782ae7..93db3bc 100644 --- a/asm/arm64_sysregs.go +++ b/asm/arm64_sysregs.go @@ -3,6 +3,8 @@ package asm +import "strings" + // arm64 system registers and system-instruction aliases. // // The tables are transcribed from the data the Go toolchain itself carries @@ -546,6 +548,16 @@ var a64TLBIOps = map[string]a64SysInst{ "VMALLS12E1OS": {0x4, 0x1, 0x6}, } +// arm64TLBITakesReg reports whether a TLBI operation spells a by-address +// invalidation that carries the address in its optional second register +// (asm7.go's sysInstFields hasOperand2). The whole-entry spellings are +// exactly the VMALL-prefixed and the ALL-prefixed ones; everything else +// (VAE*, VAAE*, VALE*, RVAA*, RVAE*, ASIDE1*, IPAS2*, RIPAS2*) is +// by-address and takes the register. +func arm64TLBITakesReg(name string) bool { + return !strings.HasPrefix(name, "VMALL") && !strings.HasPrefix(name, "ALL") +} + // a64DCOps2 maps the DC operation names to their fields; the register // operand is mandatory. var a64DCOps2 = map[string]a64SysInst{