diff --git a/asm/amd64_ext.go b/asm/amd64_ext.go index e9ca6c7..78bb237 100644 --- a/asm/amd64_ext.go +++ b/asm/amd64_ext.go @@ -71,6 +71,26 @@ func amd64ExtStatement(mnem string, ops []*ast.Operand) (extops []arch.ExtOperan return out, true, nil } +// EncodeAmd64Statement runs one parsed amd64 statement through the layer +// exactly as the assembler does: the operands convert the amd64ExtOperand way +// and the mnemonic resolves and encodes through the registry. pinned reports +// that the statement belongs to the layer alone (the mnemonic is registered +// and the scalar paths cannot encode it); err is the layer's own refusal of +// the operands, the same text the assembler prints, so the linter surfaces +// one diagnostic where assembly would fail. A statement the scalar paths own +// returns pinned false, with nothing to report. +func EncodeAmd64Statement(mnem string, ops []*ast.Operand) (code []byte, pinned bool, err error) { + extops, pinned, err := amd64ExtStatement(mnem, ops) + if !pinned || err != nil { + return nil, pinned, err + } + code, err = EncodeExtension(arch.AMD64, mnem, extops...) + if err != nil { + return nil, true, err + } + return code, true, nil +} + // amd64ExtOperand converts one parsed operand into the layer's form: a $ immediate, // a vector, general or opmask register, or a base-relative memory operand, each // with the brace decorations the spelling carries. diff --git a/lint/extension.go b/lint/extension.go new file mode 100644 index 0000000..525aa70 --- /dev/null +++ b/lint/extension.go @@ -0,0 +1,49 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +// The lint side of the extended-instruction layer. The generated tables and +// the extension registry are two homes for a mnemonic, so a registered +// mnemonic is known wherever the tables are consulted, and a statement pinned +// to the registry is validated by the registry's own encoding, whose refusal +// is the sharper diagnostic the layer carries. + +package lint + +import ( + "sourcedock.dev/petrbalvin/gasm-sdk/arch" + "sourcedock.dev/petrbalvin/gasm-sdk/asm" + "sourcedock.dev/petrbalvin/gasm-sdk/ast" +) + +// amd64ExtPinned reports whether a statement belongs to the amd64 extension +// layer alone: the mnemonic is registered in the registry and the encoder +// mirror cannot encode it, the same routing the assembler applies before its +// scalar paths run. A statement the layer pins never passes through go tool +// asm, so the per-operand portability advice the scalar statements earn is +// noise on it. +func amd64ExtPinned(upper string) bool { + if _, ok := asm.LookupExtension(arch.AMD64, upper); !ok { + return false + } + return !asm.Encodable(upper) +} + +// checkAmd64Extension validates one amd64 statement pinned to the extension +// layer by encoding it through the registry: the layer's own validators carry +// the sharper diagnostic (a mask on a scalar form, rounding on a VL form, k0 +// as a write mask), and their refusal is the diagnostic, the same text the +// assembler prints at build time. A statement the registry encodes is clean: +// the wired layer is the acceptance, not a gap of the generated table. +func checkAmd64Extension(st *ast.Instr, upper string) []Diagnostic { + _, _, err := asm.EncodeAmd64Statement(upper, st.Operands) + if err == nil { + return nil + } + return []Diagnostic{{ + Pos: st.Mnemonic.Pos, + End: st.Mnemonic.End, + Severity: Error, + Code: CodeExtensionForm, + Message: err.Error(), + }} +} diff --git a/lint/extension_test.go b/lint/extension_test.go index 481573c..6baba34 100644 --- a/lint/extension_test.go +++ b/lint/extension_test.go @@ -4,10 +4,12 @@ package lint import ( + "strings" "testing" "sourcedock.dev/petrbalvin/gasm-sdk/arch" "sourcedock.dev/petrbalvin/gasm-sdk/asm" + "sourcedock.dev/petrbalvin/gasm-sdk/ast" ) // TestExtensionInstructionsAreKnown pins the lint side of the @@ -50,3 +52,189 @@ TEXT ·f(SB), NOSPLIT, $0 t.Errorf("an unregistered mnemonic must fire unknown-instruction once: %+v", diags) } } + +// amd64ExtKernel spells one register-form statement per distinct mnemonic the +// amd64 extension layer registers, decorations beside them where the entries +// carry them, and the memory, broadcast and scaled-index spellings at the +// tail. TestAmd64ExtensionInstructionsAreKnown lints it; the completeness +// guard inside fails the moment a registered mnemonic lacks its row, so a +// family added to the registry lands here in the same change. +const amd64ExtKernel = `#include "textflag.h" +TEXT ·f(SB), NOSPLIT, $0 + VCVTNE2PS2BF16 Z1, Z2, Z3{K5} + VCVTNEPS2BF16 Z1, Y2{K5} + VDPBF16PS Z1, Z2, Z3{K5} + VP2INTERSECTD Z1, Z2, K3 + VP2INTERSECTQ Z1, Z2, K3 + VMOVSH X1, X2, X3 + VMOVW R12, X1 + VADDSH X1, X2, X3{RZ-SAE} + VSUBSH X1, X2, X3{RZ-SAE} + VMULSH X1, X2, X3{RZ-SAE} + VDIVSH X1, X2, X3{RZ-SAE} + VMINSH X1, X2, X3{SAE} + VMAXSH X1, X2, X3{SAE} + VSQRTSH X1, X2, X3{RZ-SAE} + VSCALEFSH X1, X2, X3{RZ-SAE} + VGETEXPSH X1, X2, X3{SAE} + VCOMISH X1, X2{SAE} + VUCOMISH X1, X2{SAE} + VCVTSS2SH X1, X2, X3{RZ-SAE} + VCVTSH2SS X1, X2, X3 + VCVTSH2SD X1, X2, X3 + VCVTSD2SH X1, X2, X3{RZ-SAE} + VCVTSI2SH X1, R12D, X2 + VCVTUSI2SH X1, R12D, X2 + VCVTSH2SI X1, R12D + VCVTSH2USI X1, R12D + VADDPH Z1, Z2, Z3{K5}{RZ-SAE} + VSUBPH Z1, Z2, Z3{K5}{RZ-SAE} + VMULPH Z1, Z2, Z3{K5}{RZ-SAE} + VDIVPH Z1, Z2, Z3{K5}{RZ-SAE} + VMINPH Z1, Z2, Z3{K5}{SAE} + VMAXPH Z1, Z2, Z3{K5}{SAE} + VSQRTPH Z1, Z2{K5}{RZ-SAE} + VCMPSH $0x7b, X1, X2, K3 + VGETMANTSH $0x0b, X1, X2, X4 + VREDUCESH $0x7b, X1, X2, X4 + VRNDSCALESH $0x7b, X1, X2, X4 + VCVTPH2W Z1, Z2{K5}{RZ-SAE} + VCVTPH2UW Z1, Z2{K5}{RZ-SAE} + VCVTW2PH Z1, Z2{K5}{RZ-SAE} + VCVTUW2PH Z1, Z2{K5}{RZ-SAE} + VCVTPH2DQ Y1, Z2{K5}{RZ-SAE} + VCVTPH2UDQ Y1, Z2{K5}{RZ-SAE} + VCVTDQ2PH Z1, Y2{K5}{RZ-SAE} + VCVTUDQ2PH Z1, Y2{K5}{RZ-SAE} + VCVTPH2QQ X1, Z2{K5}{RZ-SAE} + VCVTPH2UQQ X1, Z2{K5}{RZ-SAE} + VCVTQQ2PH Z1, X2{K5}{RZ-SAE} + VCVTUQQ2PH Z1, X2{K5}{RZ-SAE} + VCVTPH2PD X1, Z2{K5} + VCVTPD2PH Z1, X2{K5}{RZ-SAE} + VRNDSCALEPH $0x7b, Z1, Z3{K5} + VREDUCEPH $0x7b, Z1, Z3{K5} + VGETMANTPH $0x0b, Z1, Z3{K5} + VFMADD132PH Z1, Z2, Z3{K5}{RZ-SAE} + VFMADD213PH Z1, Z2, Z3{K5}{RZ-SAE} + VFMADD231PH Z1, Z2, Z3{K5}{RZ-SAE} + VFMSUB132PH Z1, Z2, Z3{K5}{RZ-SAE} + VFMSUB213PH Z1, Z2, Z3{K5}{RZ-SAE} + VFMSUB231PH Z1, Z2, Z3{K5}{RZ-SAE} + VFMADDSUB132PH Z1, Z2, Z3{K5}{RZ-SAE} + VFMADDSUB213PH Z1, Z2, Z3{K5}{RZ-SAE} + VFMADDSUB231PH Z1, Z2, Z3{K5}{RZ-SAE} + VFMSUBADD132PH Z1, Z2, Z3{K5}{RZ-SAE} + VFMSUBADD213PH Z1, Z2, Z3{K5}{RZ-SAE} + VFMSUBADD231PH Z1, Z2, Z3{K5}{RZ-SAE} + VFMADD132SH X1, X2, X3{RZ-SAE} + VFMADD213SH X1, X2, X3{RZ-SAE} + VFMADD231SH X1, X2, X3{RZ-SAE} + VFMSUB132SH X1, X2, X3{RZ-SAE} + VFMSUB213SH X1, X2, X3{RZ-SAE} + VFMSUB231SH X1, X2, X3{RZ-SAE} + VFMULCPH Z1, Z2, Z3{K5}{RZ-SAE} + VFCMULCPH Z1, Z2, Z3{K5}{RZ-SAE} + VFMULCSH X1, X2, X3{RZ-SAE} + VFCMULCSH X1, X2, X3{RZ-SAE} + VFMADDCPH Z1, Z2, Z3{K5}{RZ-SAE} + VFCMADDCPH Z1, Z2, Z3{K5}{RZ-SAE} + VFMADDCSH X1, X2, X3{RZ-SAE} + VFCMADDCSH X1, X2, X3{RZ-SAE} + VMINMAXPH $0x7b, Z1, Z2, Z4{K5} + VMINMAXSH $0x7b, X1, X2, X4 + VPDPWSUD X1, X2, X3 + VPDPWSUDS X1, X2, X3 + VPDPWUSD X1, X2, X3 + VPDPWUSDS X1, X2, X3 + VMOVSH (R9), X30 + VMOVSH X30, (R9) + VADDPH Z28, (R9), Z30{K7}{Z} + VCVTDQ2PH (R9){1TO8}, Y30 + VADDPH Z29, (R9)(R12*2), Z30 + VPDPWSUD X2, 127(R9), X1 + RET +` + +// TestAmd64ExtensionInstructionsAreKnown pins the lint side of the amd64 +// extension layer: every distinct mnemonic the registry carries lints without +// unknown-instruction, and the whole kernel, whose statements the encoder +// accepts one and all, lints clean, because the wired layer is the acceptance +// rather than a gap of the generated table. +func TestAmd64ExtensionInstructionsAreKnown(t *testing.T) { + // The completeness guard: a registered mnemonic without a row here is a + // gap in the kernel, not a pass. + for _, name := range asm.ExtensionNames(arch.AMD64) { + if !strings.Contains(amd64ExtKernel, "\t"+name+" ") { + t.Errorf("the amd64 extension kernel has no row for %q", name) + } + } + diags := lintArchFile(t, "ext_amd64.s", amd64ExtKernel, arch.AMD64, func(f *ast.File) (*asm.Image, error) { + return asm.AssembleFile(f) + }) + for _, code := range []string{CodeUnknownInstr, CodeUnencodable, CodeExtensionForm, + CodeNonportableRegister, CodeOperandCount, CodeRegisterWidthMismatch} { + 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_amd64.s", `#include "textflag.h" +TEXT ·f(SB), NOSPLIT, $0 + FROBNICATE Z1, Z2, Z3 + RET +`) + if codes(diags)[CodeUnknownInstr] != 1 { + t.Errorf("an unregistered mnemonic must fire unknown-instruction once: %+v", diags) + } +} + +// TestAmd64ExtensionRefusals pins the sharper diagnostic the amd64 layer +// carries: a statement the registry refuses at assembly time is a lint error +// here, under the same text the assembler prints, and one diagnostic alone. +// The rows are the layer's own validator refusals: the decorations a form +// does not take, the registers outside a class, and the immediate layouts +// with their reserved bits. +func TestAmd64ExtensionRefusals(t *testing.T) { + tests := []struct { + stmt string + want string + }{ + {"VADDSH X5, X4, X6{K3}", "the entry's destination takes none"}, + {"VADDPH Y5, Y4, Y6{RZ-SAE}", "wants a ZMM register"}, + {"VADDPH Z5, Z4, Z6{K0}", "outside the masking registers k1-k7"}, + {"VADDPH Z5, Z4, Z6{Z}", "zeroing without a write mask"}, + {"VADDPH Z5, Z4, Z6{SAE}", "spells {sae} without a mode"}, + {"VADDPH Z29, (R9){RZ-SAE}, Z30", "the memory operand takes none"}, + {"VFMULCPH Z5, (R9){1TO8}, Z6", "the entry's memory operand takes none"}, + {"VADDPH Z29, Z28, Z30{BOGUS}", "is not a decoration the layer reads"}, + {"VADDPH Z29, Z28, Z30{K7}{K3}", "carries two write masks"}, + {"VCMPSH $300, X29, X28, K5", "outside the unsigned byte range"}, + {"VGETMANTSH $0x20, X29, X28, X30", "the upper nibble of the mantissa control is reserved"}, + {"VCVTSI2SH X29, X12, X30", "general register"}, + {"VADDPH Z5, Z4", "got 2 operands"}, + {"VMOVSH (Z4), X30", "not an extended-layer operand"}, + {"VMOVSH foo+4(SB), X30", "not an extended-layer operand"}, + {"VMOVSH 8(R9)(DX*3), X30", "outside the byte multipliers"}, + } + for _, tt := range tests { + src := "#include \"textflag.h\"\nTEXT ·f(SB), NOSPLIT, $0\n\t" + tt.stmt + "\n\tRET\n" + diags := lintSrcArch(t, "ext_amd64.s", src) + if len(diags) != 1 { + t.Errorf("%s: got %d diagnostics, want the one extension-form error: %+v", tt.stmt, len(diags), diags) + continue + } + d := diags[0] + if d.Code != CodeExtensionForm || d.Severity != Error { + t.Errorf("%s: got %s %s, want an error-severity %s: %+v", tt.stmt, d.Severity, d.Code, CodeExtensionForm, d) + } + if !strings.Contains(d.Message, tt.want) { + t.Errorf("%s: message %q does not name %q", tt.stmt, d.Message, tt.want) + } + } +} diff --git a/lint/lint.go b/lint/lint.go index 6bf16ab..5894dc7 100644 --- a/lint/lint.go +++ b/lint/lint.go @@ -96,6 +96,7 @@ const ( CodeDataStringWidth = "data-string-width" CodeDataNoGlobl = "data-without-globl" CodeDataExceedsGlobl = "data-exceeds-globl" + CodeExtensionForm = "extension-form" ) // knownTextFlags are the flags recognised by runtime/textflag.h, plus the @@ -327,13 +328,14 @@ func lintText(t *ast.Text, tab *arch.Table, archKnown bool, cfg Config, macros m hasRet = true } + // The extended-instruction layer is a mnemonic's second home: + // one registered there (the arm64 SVE family and the amd64 + // BF16, VP2INTERSECT, FP16 and VNNI-INT16 families) 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 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 { if !extended { out = append(out, Diagnostic{ @@ -359,6 +361,15 @@ func lintText(t *ast.Text, tab *arch.Table, archKnown bool, cfg Config, macros m } } + // The amd64 extension layer carries the sharper diagnostic: a + // statement pinned to the registry is validated by encoding it, + // and the layer's own refusal (a mask on a scalar form, rounding + // on a VL form, k0 as a write mask) surfaces as the finding, the + // same text the assembler prints at build time. + if cfg.Arch == arch.AMD64 && amd64ExtPinned(upper) && !cfg.Disable[CodeExtensionForm] { + out = append(out, checkAmd64Extension(st, upper)...) + } + if archKnown && !cfg.Disable[CodeOperandCount] && !isMacroInvocation(mnem, macros) && !maskedEvex(mnem, st.Operands) { if in, ok := tab.Lookup(mnem); ok && in.MinOps >= 0 { n := len(st.Operands) diff --git a/lint/registers.go b/lint/registers.go index 5df11c1..df9ab64 100644 --- a/lint/registers.go +++ b/lint/registers.go @@ -71,6 +71,13 @@ func scanNonportableRegisters(t *ast.Text) []Diagnostic { if !ok { continue } + // A statement pinned to the extension layer sits outside the + // portability premise already: go tool asm knows none of its + // mnemonics, so advising the canonical word spelling inside one is + // noise on a statement the toolchain would refuse whole. + if amd64ExtPinned(strings.ToUpper(in.Mnemonic.Text)) { + continue + } for _, op := range in.Operands { if op.Addr.Sym != nil && op.Addr.Sym.Pseudo == "" { report(op.Addr.Sym.Name, op.Pos)