Fix the operand-size/prefix bug class reported by the sigil backend, then close
every failure a whole-ISA encode->decode->re-encode audit surfaced -- 247 -> 0
over the 1165 round-trippable legacy forms.
Encode/decode:
- MOVSX/MOVZX take 66h from the destination, not the source (CRC32, whose r/m
source drives 66h, is unchanged)
- operand-less 16-bit forms (CBW/CWD, MOVSW/CMPSW/SCASW/LODSW/STOSW,
IRET/PUSHF/POPF) emit 66h via a new opsize_16 flag
- CRC32/MOVBE and POPCNT/LZCNT/TZCNT: mandatory F2/F3 beats 66h on decode
- mandatory-prefix REP double-encode: F2/F3 no longer re-emitted as REP
- fixed-ModR/M decode: x87 (FNOP/FCHS/FADD ST(i)/...) and the 0F 01/AE/C7/1E
system-op groups (VMCALL/RDTSCP/LFENCE/ENDBR/...); gen.odin preserves the
fixed ModR/M byte, the decoder matches and consumes it
- implicit-operand round-trip (accumulator short forms, x87 ST(i), xchg) via a
positional matcher + emit path; accumulators stay implicit so hand-built
`add eax, imm` keeps the general form (typed-builder consistency)
- MOV to/from CR/DR/segment (were decoding as GPRs); reg-vs-mem disambiguation
(RDRAND vs VMPTRLD, MOVLHPS vs MOVHPS); x87 M80 operand size; PINSRW; NOP vs
XCHG at 0x90
- LOCK standalone; ENTER two immediates; PUSH/POP FS/GS (segment fixed by the
opcode + non-cacheable); BOUND marked 32-bit-only; RDRAND/RDSEED r16
(66 is operand-size, resolved against the mandatory-66 VMCLEAR by ModR/M)
Tools/tests:
- revive dump_verify_input + verify_against_llvm (stale Result API; drop the
aliases that were masking the fixed bugs); align verify_tables with the
preserved fixed-ModR/M ext
- new tests/test_narrow_widths.odin: byte-exact vs llvm-mc + round-trip guards
- fold the bit-rotted tests32 package into the main suite (i386 Mode._32 paths)
248 tests pass; verify_tables passes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01115ktMoX5qGAfp1VemBzCP
Rewrite the builder generator to cover every opcode -- each operand maps to a
typed param by kind + quantifier, emitted with a running operand index so all
shapes compose:
optional '?' -> Maybe(T)
variadic '*' -> []T
composite Pair* -> []Pair_Id_Id / []Pair_Lit_Id / []Pair_Id_Lit
LiteralString -> string (packed into word operands)
Id/Lit/enum -> Id / i64 / the typed enum (ValueEnum) or bit_set (BitEnum)
A verb that would clash with an Odin keyword, builtin, builtin type name, or a
re-exported ir.type_* constructor gets a trailing '_' (return_, switch_, string_,
size_of_, type_void_, ...). builder.odin gains the Pair_* types + a string packer.
873 inst_<OpName> + 873 Builder methods. Package compiles; suite 10 passed.
Add a count-only mode to the Writer (advance pos without storing) and factor the
section walk into emit_module, so encode and the new encoded_size(m) share one
path. encoded_size returns the exact byte count encode will produce -- the SPIR-V
analog of an ISA's encode_max_code_size, here exact rather than an upper bound.
The printer now sizes its scratch buffer with it instead of retry-on-overflow.
Verified: encoded_size == encode byte_count; suite 10 passed.
Extend tablegen to emit builders_gen.odin -- 723 opcodes get a low-level
inst_<OpName>(buf, ...) constructor and a high-level Builder method, mapping each
grammar operand to a typed param (IdResultType->Type_Ref, IdResult->auto-allocated
Id, IdRef->Id, LiteralInteger->i64, ValueEnum/BitEnum->the typed enum, trailing
IdRef* -> []Id). builder.odin keeps just the hand-written Builder infrastructure.
Skipped (stay hand-writable, like an ISA's can_generate_builder): operands that
aren't simple typed params yet (optional, Pair* composites, LiteralString),
type-declaration opcodes (those are ir.Type), and verbs colliding with Odin
keywords/builtins (return_, switch_, size_of, ...).
Validated: a function body built via i_add / return_ encodes + round-trips
byte-exact (builder_made test) -> 10 passed.