// Copyright (c) 2026 Petr Balvín (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) } } }