Files
Odin/core/rexcode/isa/x86/aliases.odin
Flāvius 5bc545cd97 x86: the decoder's choice of mnemonic is declared, not emergent
Two defects, one shape: something the decoder must decide was never stated, so
it fell out of where an entry landed in an unstably sorted table.

CANONICITY. Several mnemonics name one encoding -- SHL and SAL are both /4, JE
and JZ are both 0x74. Decoding a representative of each and diffing against
llvm-mc: 74 patterns, 22 agreed, 52 did not (0F 84 -> JZ where llvm-mc says JE,
A4 -> MOVS vs MOVSB, DB E2 -> FCLEX vs FNCLEX). aliases.odin now declares 57
{alias, canonical} rows, each canonical name MEASURED from llvm-mc rather than
picked, and gen.odin drops an aliased entry as it collects decode entries -- so
the name never reaches the tables. The drop is conditional on the canonical name
covering the byte-identical encoding, which is what lets MOV be the alias at the
A0-A3/B8 moffs forms while staying the only name for 88/89. Aliases stay fully
encodable; only decoding narrows. 119 of 1350 legacy entries dropped, 74 of 74
now agree.

This also fixes the four SAL/SHL failures left by the previous commit, at their
root rather than by extending a hand-written list.

ADDRESS SIZE. 0xE3's mnemonic is chosen by which counter register it tests --
the 67h axis -- and REX.W does not affect address size at all. Modelling JRCXZ
as force_rex_w made its encoding 48 E3 cb where llvm-mc emits a bare E3 cb, gave
JECXZ the bare encoding that is really JRCXZ in long mode, and left three
indistinguishable entries. Encoding_Flags gains addr_size (2 of 6 spare bits);
the matcher refuses a form the mode cannot express, the encoder emits 67h when
it differs from the default, the decoder selects on it before the operand-size
pass. All ten cases match llvm-mc, refusals included.

The tests keep no second copy of the alias table: mnemonics_eq compares
canonical_mnemonic, and run_alias_table_test asserts the stronger property the
generator guarantees -- zero encodings spelled by two surviving mnemonics. It
caught the E3 ambiguity on its own.

Also fixed: gen.odin's write_flags enumerates Encoding_Flags by hand and
silently dropped addr_size from the generated tables -- the flag read back as
its zero value and the instruction was quietly mis-modelled.

271 passed. Bites: dropping {.SAL, .SHL} returns the prime_sieve failure and the
guard names every shift-group opcode; restoring force_rex_w decodes E3 00 as
JCXZ and encodes JRCXZ to three bytes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Riok9vMpkLmo78wsVKJHhz
2026-08-28 16:23:17 -04:00

131 lines
4.7 KiB
Odin

// rexcode · Brendan Punsky (dotbmp@github), original author
package rexcode_x86
// =============================================================================
// MNEMONIC ALIASES — which of several names for one encoding a disassembler prints
// =============================================================================
//
// Some x86 instructions have several legal mnemonics for ONE encoding. SHL and
// SAL are both ModRM.reg=4 in the shift group; JE and JZ are both 0x74; there
// are 57 such pairs. A decoder has nothing to tell them apart with — the bytes
// are identical — so it must simply pick a name, and that choice has to be
// DECLARED. Left undeclared it falls out of wherever the entry happened to land
// in the (unstably sorted) decode table: correct, arbitrary, and free to move on
// any table regeneration. It did move once, and four tests that had always
// passed began reporting `SAL != expected SHL`.
//
// The canonical name here is the one **llvm-mc prints**, measured rather than
// chosen: llvm-mc is the ground truth every verifier in this library diffs
// against, so agreeing with it is what makes a disassembly comparable. Before
// this table, 52 of the 74 aliased encodings decoded to a name llvm-mc does not
// use (`0F 84` → JZ where it says JE, `A4` → MOVS where it says MOVSB, `DB E2` →
// FCLEX where it says FNCLEX).
//
// An alias stays fully ENCODABLE — `inst_r_r(.SAL, …)` emits the same bytes it
// always did. Only the decode direction is narrowed, and only where the
// canonical name covers the byte-identical encoding: that condition is what lets
// MOV be the alias at the `A0`-`A3`/`B8` moffs forms while remaining the only
// name for `88`/`89`. `tablegen/gen.odin` applies it when it collects decode
// entries, so an aliased mnemonic never reaches the decode tables at all.
//
// Adding an instruction whose mnemonic aliases another one's encoding requires a
// row here; `run_alias_table_test` in tests/ recomputes the ambiguity from the
// built tables and fails by name if one is missing, so it cannot be forgotten.
Mnemonic_Alias :: struct {
alias: Mnemonic, // never produced by the decoder
canonical: Mnemonic, // produced instead, at the byte-identical encoding
}
@(rodata)
MNEMONIC_ALIASES := [?]Mnemonic_Alias{
// -- Jcc, both the short 0x7x and near 0x0F 8x forms --------------------
{.JNAE, .JB}, {.JC, .JB},
{.JNB, .JAE}, {.JNC, .JAE},
{.JZ, .JE},
{.JNZ, .JNE},
{.JNA, .JBE},
{.JNBE, .JA},
{.JPE, .JP},
{.JPO, .JNP},
{.JNGE, .JL},
{.JNL, .JGE},
{.JNG, .JLE},
{.JNLE, .JG},
// -- CMOVcc (0x0F 4x) ---------------------------------------------------
{.CMOVNAE, .CMOVB}, {.CMOVC, .CMOVB},
{.CMOVNB, .CMOVAE}, {.CMOVNC, .CMOVAE},
{.CMOVZ, .CMOVE},
{.CMOVNZ, .CMOVNE},
{.CMOVNA, .CMOVBE},
{.CMOVNBE, .CMOVA},
{.CMOVPE, .CMOVP},
{.CMOVPO, .CMOVNP},
{.CMOVNGE, .CMOVL},
{.CMOVNL, .CMOVGE},
{.CMOVNG, .CMOVLE},
{.CMOVNLE, .CMOVG},
// -- SETcc (0x0F 9x) ----------------------------------------------------
{.SETNAE, .SETB}, {.SETC, .SETB},
{.SETNB, .SETAE}, {.SETNC, .SETAE},
{.SETZ, .SETE},
{.SETNZ, .SETNE},
{.SETNA, .SETBE},
{.SETNBE, .SETA},
{.SETPE, .SETP},
{.SETPO, .SETNP},
{.SETNGE, .SETL},
{.SETNL, .SETGE},
{.SETNG, .SETLE},
{.SETNLE, .SETG},
// -- Shift group: SAL and SHL are both /4, the same encoding ------------
{.SAL, .SHL},
// -- String ops: the bare name against the explicitly byte-sized one ----
{.CMPS, .CMPSB},
{.LODS, .LODSB},
{.MOVS, .MOVSB},
{.SCAS, .SCASB},
{.STOS, .STOSB},
// -- x87: the assembler's wait-prefixed spelling of a no-wait opcode ----
// (FSTENV is really `9B D9 /6`; the table gives it the bare `D9 /6`, which
// is FNSTENV. Modelling the 9B prefix is a separate question — until then
// the bare encoding decodes as the no-wait name, which is what it is.)
{.FCLEX, .FNCLEX},
{.FINIT, .FNINIT},
{.FSAVE, .FNSAVE},
{.FSTCW, .FNSTCW},
{.FSTENV, .FNSTENV},
{.FSTSW, .FNSTSW},
// -- Odds --------------------------------------------------------------
{.FWAIT, .WAIT},
{.XLAT, .XLATB},
// MOV aliases MOVABS only at the moffs (`A0`-`A3`) and imm64 (`B8+r`)
// forms; the coverage rule leaves every other MOV encoding untouched.
{.MOV, .MOVABS},
}
// The name a disassembler prints for `m`'s encoding — `m` itself unless it is a
// declared alias. Useful to an assembler front-end that accepts either spelling
// and wants to compare against decoder output.
canonical_mnemonic :: proc "contextless" (m: Mnemonic) -> Mnemonic {
for entry in MNEMONIC_ALIASES {
if entry.alias == m { return entry.canonical }
}
return m
}
// Is `m` a name the decoder never produces?
is_mnemonic_alias :: proc "contextless" (m: Mnemonic) -> bool {
for entry in MNEMONIC_ALIASES {
if entry.alias == m { return true }
}
return false
}