feat(lint): surface the amd64 extension layer's refusals

Assisted-by: GLM 5.3 Flash
This commit is contained in:
petrbalvin committed 2026-10-07 20:56:25 +02:00
1 parent 5ee60c860b
commit fa50521619
5 files changed
+281 -6

No files matched your search

+20
View File
@@ -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.
+49
View File
@@ -0,0 +1,49 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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(),
}}
}
+188
View File
@@ -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)
}
}
}
+17 -6
View File
@@ -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)
+7
View File
@@ -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)