diff --git a/asm/encodable.go b/asm/encodable.go new file mode 100644 index 0000000..0419b14 --- /dev/null +++ b/asm/encodable.go @@ -0,0 +1,89 @@ +// Copyright (c) 2026 Petr Balvín (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 +} diff --git a/lint/encodable_test.go b/lint/encodable_test.go new file mode 100644 index 0000000..2845b00 --- /dev/null +++ b/lint/encodable_test.go @@ -0,0 +1,62 @@ +// Copyright (c) 2026 Petr Balvín (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) + } + } +} diff --git a/lint/lint.go b/lint/lint.go index a02f4b7..6362b88 100644 --- a/lint/lint.go +++ b/lint/lint.go @@ -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), + }) } }