feat(lint): flag table-known instructions the encoder cannot emit
This commit is contained in:
@@ -0,0 +1,89 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package asm
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// Encodable reports whether the amd64 encoder knows how to encode the
|
||||||
|
// mnemonic. It mirrors the dispatch in (*enc).encode: the fixed-name
|
||||||
|
// instructions, conditional jumps, the CMOV/SET condition families, the
|
||||||
|
// VEX/EVEX/opmask/gather/scatter vector paths, the legacy SSE tables and the
|
||||||
|
// explicit scalar cases. A mnemonic that parses (is in the architecture
|
||||||
|
// table) but is not encodable would otherwise surface only at assembly time,
|
||||||
|
// deep inside a build; the linter uses this predicate to flag it at edit
|
||||||
|
// time.
|
||||||
|
func Encodable(mnemonic string) bool {
|
||||||
|
upper := strings.ToUpper(mnemonic)
|
||||||
|
|
||||||
|
// Fixed-name instructions (no size suffix).
|
||||||
|
switch upper {
|
||||||
|
case "RET", "NOP", "CALL", "JMP":
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if _, ok := condCode(upper); ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// VEX/EVEX and friends: the trailing B/W/L/Q/D is part of the mnemonic.
|
||||||
|
base, _, err := parseEvexSuffix(upper)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if isVex(base) || isEvex(base) || isKOp(base) || isGather(base) || isScatter(base) ||
|
||||||
|
base == "KMOVW" || base == "KMOVQ" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// CMOV carries size then condition (CMOVLGT); SET carries the condition
|
||||||
|
// alone (SETNE).
|
||||||
|
if rest, ok := strings.CutPrefix(upper, "CMOV"); ok && len(rest) >= 2 {
|
||||||
|
if _, ok := jccMap[rest[1:]]; ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if rest, ok := strings.CutPrefix(upper, "SET"); ok {
|
||||||
|
if _, ok := jccMap[rest]; ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy SSE shuffles and packed binaries dispatch on the full name.
|
||||||
|
if _, ok := sseShufTable[upper]; ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if _, ok := sseBinTable[upper]; ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// The size-suffix split: retry the tables and the scalar switch on the
|
||||||
|
// base.
|
||||||
|
base2, size := splitSize(upper)
|
||||||
|
if size == 0 {
|
||||||
|
size = 8
|
||||||
|
}
|
||||||
|
_ = size
|
||||||
|
if base2 != upper {
|
||||||
|
if _, ok := sseBinTable[base2]; ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch base2 {
|
||||||
|
case "MOV",
|
||||||
|
"ADD", "SUB", "AND", "OR", "XOR", "CMP",
|
||||||
|
"TEST",
|
||||||
|
"LEA",
|
||||||
|
"INC", "DEC", "NEG", "NOT",
|
||||||
|
"SHL", "SHR", "SAR",
|
||||||
|
"IMUL", "IMUL3",
|
||||||
|
"PUSH", "POP",
|
||||||
|
"BSF", "BSR", "LZCNT", "TZCNT", "POPCNT",
|
||||||
|
"BSWAP",
|
||||||
|
"PREFETCHNTA", "PREFETCHT0", "PREFETCHT1", "PREFETCHT2",
|
||||||
|
"MOVBLZX", "MOVBQZX", "MOVWLZX", "MOVWQZX", "MOVWLSX", "MOVLQSX",
|
||||||
|
"CVTSL2SD", "CVTSQ2SD",
|
||||||
|
"MOVOU", "MOVO", "MOVUPS", "MOVAPS", "MOVUPD", "MOVAPD", "MOVSD", "MOVSS":
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package lint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"sourcedock.dev/petrbalvin/gasm-devkit/asm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUnencodableInstruction(t *testing.T) {
|
||||||
|
// MOVBE is a real x86 instruction the architecture table knows about;
|
||||||
|
// before the encoder grows it, lint must flag the gap at edit time.
|
||||||
|
diags := lintSrc(t, `
|
||||||
|
#include "textflag.h"
|
||||||
|
TEXT ·f(SB), NOSPLIT, $0
|
||||||
|
MOVQ AX, BX
|
||||||
|
RET
|
||||||
|
`)
|
||||||
|
if codes(diags)[CodeUnencodable] != 0 {
|
||||||
|
t.Fatalf("encodable instructions must not be flagged: %+v", diags)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A mnemonic that is neither in the table nor encodable fires
|
||||||
|
// unknown-instruction, not unencodable.
|
||||||
|
diags = lintSrc(t, `
|
||||||
|
#include "textflag.h"
|
||||||
|
TEXT ·f(SB), NOSPLIT, $0
|
||||||
|
FROBNICATE AX, BX
|
||||||
|
RET
|
||||||
|
`)
|
||||||
|
if codes(diags)[CodeUnencodable] != 0 || codes(diags)[CodeUnknownInstr] != 1 {
|
||||||
|
t.Fatalf("unknown instruction must fire unknown-instruction only: %+v", diags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodableMirror(t *testing.T) {
|
||||||
|
// Spot-check the predicate against instructions from every dispatch
|
||||||
|
// family of the encoder.
|
||||||
|
yes := []string{
|
||||||
|
"RET", "NOP", "CALL", "JMP", "JEQ", "JLT",
|
||||||
|
"MOVQ", "MOVL", "ADDQ", "CMPQ", "LEAQ", "TESTL",
|
||||||
|
"NOTL", "DECL", "SHRQ", "IMULQ", "PUSHQ", "POPQ",
|
||||||
|
"BSWAPL", "TZCNTQ", "MOVBLZX", "MOVOU", "MOVSD",
|
||||||
|
"PREFETCHT0", "PREFETCHNTA",
|
||||||
|
"VMOVDQU", "VPCMPUB", "VPCOMPRESSD", "VPERMB", "VPGATHERDD", "VPSCATTERDD",
|
||||||
|
"KMOVQ", "KANDNW", "VPMOVMSKB",
|
||||||
|
"CMOVLGT", "SETNE", "PSHUFB", "PADDB",
|
||||||
|
}
|
||||||
|
for _, m := range yes {
|
||||||
|
if !asm.Encodable(m) {
|
||||||
|
t.Errorf("Encodable(%q) = false, want true", m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
no := []string{"FROBNICATE", "MOVXX", "CMOVXX", "SETXX"}
|
||||||
|
for _, m := range no {
|
||||||
|
if asm.Encodable(m) {
|
||||||
|
t.Errorf("Encodable(%q) = true, want false", m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
||||||
|
"sourcedock.dev/petrbalvin/gasm-devkit/asm"
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/token"
|
"sourcedock.dev/petrbalvin/gasm-devkit/token"
|
||||||
)
|
)
|
||||||
@@ -78,6 +79,8 @@ const (
|
|||||||
CodeStackImbalance = "stack-imbalance"
|
CodeStackImbalance = "stack-imbalance"
|
||||||
CodeRegisterWidthMismatch = "register-width-mismatch"
|
CodeRegisterWidthMismatch = "register-width-mismatch"
|
||||||
CodeABI0RegisterArgs = "abi0-register-args"
|
CodeABI0RegisterArgs = "abi0-register-args"
|
||||||
|
CodeNonportableRegister = "nonportable-register-name"
|
||||||
|
CodeUnencodable = "unencodable-instruction"
|
||||||
)
|
)
|
||||||
|
|
||||||
// knownTextFlags are the flags recognised by the Go assembler's textflag.h.
|
// knownTextFlags are the flags recognised by the Go assembler's textflag.h.
|
||||||
@@ -282,6 +285,18 @@ func lintText(t *ast.Text, tab *arch.Table, archKnown bool, cfg Config, macros m
|
|||||||
Code: CodeUnknownInstr,
|
Code: CodeUnknownInstr,
|
||||||
Message: fmt.Sprintf("unknown %s instruction %q", cfg.Arch, mnem),
|
Message: fmt.Sprintf("unknown %s instruction %q", cfg.Arch, mnem),
|
||||||
})
|
})
|
||||||
|
} else if cfg.Arch == arch.AMD64 && !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
|
||||||
|
// in the editor, and so the audit can close it.
|
||||||
|
out = append(out, Diagnostic{
|
||||||
|
Pos: st.Mnemonic.Pos,
|
||||||
|
End: st.Mnemonic.End,
|
||||||
|
Severity: Warning,
|
||||||
|
Code: CodeUnencodable,
|
||||||
|
Message: fmt.Sprintf("instruction %q is known but the encoder cannot assemble it yet", mnem),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user