feat(lint): surface the amd64 extension layer's refusals
Assisted-by: GLM 5.3 Flash
This commit is contained in:
1 parent
5ee60c860b
commit
fa50521619
5 files changed
+281
-6
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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(),
|
||||
}}
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user