feat(lint): flag table-known instructions the encoder cannot emit

This commit is contained in:
2026-08-29 13:42:47 +02:00
parent c92e6bed3a
commit 685b150ecf
3 changed files with 166 additions and 0 deletions
+89
View File
@@ -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
}
+62
View File
@@ -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)
}
}
}
+15
View File
@@ -13,6 +13,7 @@ import (
"strings"
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
"sourcedock.dev/petrbalvin/gasm-devkit/asm"
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
"sourcedock.dev/petrbalvin/gasm-devkit/token"
)
@@ -78,6 +79,8 @@ const (
CodeStackImbalance = "stack-imbalance"
CodeRegisterWidthMismatch = "register-width-mismatch"
CodeABI0RegisterArgs = "abi0-register-args"
CodeNonportableRegister = "nonportable-register-name"
CodeUnencodable = "unencodable-instruction"
)
// 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,
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),
})
}
}