diff --git a/lint/extension_test.go b/lint/extension_test.go new file mode 100644 index 0000000..481573c --- /dev/null +++ b/lint/extension_test.go @@ -0,0 +1,52 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +package lint + +import ( + "testing" + + "sourcedock.dev/petrbalvin/gasm-sdk/arch" + "sourcedock.dev/petrbalvin/gasm-sdk/asm" +) + +// TestExtensionInstructionsAreKnown pins the lint side of the +// extended-instruction layer: every mnemonic the arm64 registry carries +// lints without unknown-instruction, and a kernel the encoder accepts lints +// clean, because the wired layer is the acceptance rather than a gap of the +// generated table. +func TestExtensionInstructionsAreKnown(t *testing.T) { + src := `#include "textflag.h" +TEXT ·f(SB), NOSPLIT, $0 + ADD Z1.S, Z2.S, Z0.S + SUBR Z1.S, P0/M, Z0.S + SQADD Z0.D, Z1.D, Z2.D + UQADD Z0.D, Z1.D, Z2.D + SQSUB Z0.D, Z1.D, Z2.D + UQSUB Z0.D, Z1.D, Z2.D + SMULH Z3.D, Z4.D, Z5.D + ADD $255, Z0.S + MUL $-128, Z0.B + RET +` + diags := lintArchFile(t, "ext_arm64.s", src, arch.ARM64, asm.AssembleFileARM64) + for _, code := range []string{CodeUnknownInstr, CodeUnencodable} { + if n := codes(diags)[code]; n != 0 { + t.Errorf("%s fired %d times on the wired extension: %+v", code, n, diags) + } + } + if len(diags) != 0 { + t.Errorf("a kernel the encoder accepts must lint clean, got %+v", diags) + } + + // The registry consult is the layer's own, not a loosening of the rule: + // a mnemonic neither table nor registry carries still fires. + diags = lintSrcArch(t, "ext_arm64.s", `#include "textflag.h" +TEXT ·f(SB), NOSPLIT, $0 + FROBNICATE Z0.S, Z1.S, Z2.S + RET +`) + if codes(diags)[CodeUnknownInstr] != 1 { + t.Errorf("an unregistered mnemonic must fire unknown-instruction once: %+v", diags) + } +} diff --git a/lint/lint.go b/lint/lint.go index 2a2992d..6bf16ab 100644 --- a/lint/lint.go +++ b/lint/lint.go @@ -328,15 +328,23 @@ func lintText(t *ast.Text, tab *arch.Table, archKnown bool, cfg Config, macros m } if archKnown && !cfg.Disable[CodeUnknownInstr] && !pseudoOps[upper] && !isMacroInvocation(mnem, macros) { + // The extended-instruction layer is a mnemonic's second home: + // one registered there (arm64's SVE family today) is known, + // and it encodes through the extension registry rather than + // the encoder mirror the unencodable rule consults, so the + // wired extensions are never flagged as a gap of the table. + _, extended := asm.LookupExtension(cfg.Arch, mnem) if _, ok := tab.Lookup(mnem); !ok { - out = append(out, Diagnostic{ - Pos: st.Mnemonic.Pos, - End: st.Mnemonic.End, - Severity: Error, - Code: CodeUnknownInstr, - Message: fmt.Sprintf("unknown %s instruction %q", cfg.Arch, mnem), - }) - } else if cfg.Arch == arch.AMD64 && !cfg.Disable[CodeUnencodable] && !asm.Encodable(upper) { + if !extended { + out = append(out, Diagnostic{ + Pos: st.Mnemonic.Pos, + End: st.Mnemonic.End, + Severity: Error, + Code: CodeUnknownInstr, + Message: fmt.Sprintf("unknown %s instruction %q", cfg.Arch, mnem), + }) + } + } else if cfg.Arch == arch.AMD64 && !extended && !cfg.Disable[CodeUnencodable] && !asm.Encodable(upper) { // Known to the architecture table but missing from the // encoder: the file parses everywhere and then fails at // assembly time. Flag it at lint so the gap is visible