The model
- An
Instis aMnemonicand up to three **Operand**s: aReg, aMem(qword ptr [base+index*scale+disp],[rip+disp], or absolute), an immediate, or a relative branch target. It says what to execute; the encoder chooses the bytes. encodepicks the shortest form that fits and returns the bytes on the stack, with the form used.decodeturns bytes back into the sameInst.Displayprints Intel syntax;disassemblelists a buffer.- An
Assembleradds labels: branches to labels are relaxed torel8where they reach andrel32where they do not, andcalls and RIP-relative references to labels are patched. formsexposes the target description itself: everyForm's encoding recipe, operand kinds, and flag effects.encode_formemits one named form, which is what an instruction selector generated from the description calls.
What it guarantees, and how each guarantee is checked:
| Guarantee | How it is held |
|---|---|
decode(encode(i)) == i, and the decoder reports the form that was used. |
Every form's operand space is enumerated position by position (all registers, every base × index × scale, every displacement and immediate boundary, every condition code) and every register pair: over 500,000 instructions, plus random operands. |
| Whatever the decoder accepts, the encoder encodes back to the same instruction. | Every prefix combination × every opcode in both maps × every ModRM byte × several SIB/displacement tails: 13.4 million inputs, plus every 1- and 2-byte input and random bytes. |
| The decoder never panics, and every error names the byte at fault. | The same inputs, including every accepted instruction cut short at every length. |
| Encodings match the Intel SDM byte for byte. | 171 known-answer tests with the REX/ModRM/SIB arithmetic written out. While building, the encodings were also checked against LLVM 18 (assembler and disassembler); see the release notes. |
| The encoder picks a shortest form. | Checked against every other form that fits, in the enumeration above. |
| Branch relaxation is optimal and fast at scale. | 3,000 random programs compared byte for byte with a naive reference; a 100,000-branch cascade relaxes in well under a second. |
Installation
[]
= "0.2"
Without the standard library:
[]
= { = "0.2", = false }
Quick start
use ;
// add rax, 8
let add = new;
assert_eq!;
// mov dword ptr [rsp+4], r9d
let store = new;
let bytes = encode?;
assert_eq!;
// And back.
let decoded = decode.unwrap;
assert_eq!;
assert_eq!;
assert_eq!;
# Ok::
Labels and branches
use ;
// Sum 1..=n, with n in edi.
let mut asm = new;
let top = asm.label;
let done = asm.label;
asm.emit?;
asm.emit?;
asm.jcc?;
asm.bind?;
asm.emit?;
asm.emit?;
asm.jcc?;
asm.bind?;
asm.emit?;
let code = asm.finish?;
assert_eq!;
# Ok::
Errors say what is wrong
Immediates are the operand's value as a signed integer of the operation's width, and nothing is ever truncated:
use ;
// A 64-bit add takes at most a sign-extended 32-bit immediate.
let too_big = new;
assert_eq!;
// ah cannot be encoded with a REX prefix, which r8b requires.
let mix = new;
assert_eq!;
// The decoder names the byte at fault.
assert_eq!;
assert_eq!;
The description, as data
use ;
let form = forms.iter.find.unwrap;
assert_eq!;
assert_eq!;
// Force this form even though an 8-bit immediate would fit.
let inst = new;
assert_eq!;
# Ok::
What is covered
mov (register, memory, and immediate forms including imm64), lea, add or adc sbb and sub xor cmp test, inc dec neg not, mul imul (one, two, and three operands) div idiv, cbw cwde cdqe cwd cdq cqo, shl shr sar rol ror (by 1, imm8, cl), movzx movsx movsxd, setcc, cmovcc, jcc (rel8/rel32), jmp (rel8/rel32/indirect), call (rel32/indirect), ret, push, pop, nop (one-byte and nop r/m, plus the manual's recommended 1–9 byte sequences), ud2, int3, xchg, bt — at 8, 16, 32, and 64 bits wherever the architecture has the form.
Not covered yet, and reported as errors by the decoder: floating point and SIMD, string instructions, LOCK/REP/segment/address-size prefixes (so no fs:/gs: TLS access yet), enter/leave, far and system instructions, and the other bit-manipulation and atomic instructions. The decoder is exact: it accepts the encodings the table describes, and rejects encodings that carry prefix bits the hardware would ignore (see docs/API.md).
Examples
Runnable programs in examples/:
| Example | What it shows |
|---|---|
encode |
The one-call path: encode a function's instructions, print bytes, text, and the form each used, and decode them back. |
assemble |
A loop with labels, a RIP-relative constant, and the disassembly listing of the result. |
disassemble |
Hex bytes in, an Intel-syntax listing out. cargo run --example disassemble -- 55 48 89 e5 c3 |
Performance
Encoding and decoding allocate nothing: an encoded instruction is at most 15 bytes on the stack, and a decode is a few table lookups and bounds-checked reads. Candidate forms come from two indexes built at compile time (by mnemonic and by opcode byte), so neither direction searches the whole table.
Measured with the benchmarks in benches/ over a 100,000-instruction mix shaped like compiler output (frame loads and stores, array indexing, arithmetic, compares, branches, calls), Windows x86_64, Rust stable, release profile:
| Benchmark | What it measures | Per instruction | Throughput |
|---|---|---|---|
encode/select_and_encode_100k |
encode: choose the form, lay out, append to a buffer |
~40 ns | ~25 M/s |
encode/encode_form_100k |
encode_form: the form already chosen |
~22 ns | ~45 M/s |
decode/decode_100k |
decode, one instruction after another |
~40 ns | ~25 M/s |
decode/display_100k |
Intel-syntax text into a String |
~55 ns | ~18 M/s |
assemble/labels_and_relaxation_100k |
Assembler with a loop branch every 16 instructions, then finish |
~49 ns | ~20 M/s |
Numbers vary by CPU; use the trend across runs, not a single absolute. Linux numbers are measured centrally for the release.
Design notes
- One canonical instruction. An immediate is the operand's signed value at the operation's width, a branch target is measured from the start of the branch, and a memory operand is an address, not a byte layout. So every encoding of the same operation decodes to the same
Inst, and equal instructions compare equal. - Rows, not code. The table is written as the manual writes it and compiled by a
const fnwhile the crate builds; a malformed row, or a row whose operands and encoding disagree, is a compile error. The encoder and decoder never mention a mnemonic. - Strict decoding. The decoder is the exact inverse of the encoder over the table, plus two redundant prefixes that real code contains (an empty REX, and REX.W on 64-bit-default forms). Everything else it rejects with the offset of the offending byte, which is what makes it a precise oracle for the encoder.
- Least-fixed-point relaxation. Branches start short and lengthen only when they cannot reach; lengthening never shrinks anything, so the result is unique and minimal. A lengthening re-checks only the branches within 256 bytes of it, so relaxation is O(n log n).
The target-description model, the engines, and how AArch64 fits the same model are specified in _lexersketch/specs/TARGET-DESC.md in the family repository.
Testing
tests/properties.rs enumerates every form's operand space; tests/decoder.rs sweeps the prefix × opcode × ModRM space and random bytes; tests/known_answers.rs holds the byte-exact SDM cases; tests/assembler.rs compares the assembler with a naive reference. Every rust example in this README and in docs/API.md is compiled and run as a doctest.
Cross-platform support
- Linux (x86_64, aarch64)
- macOS (x86_64, Apple Silicon)
- Windows (x86_64)
The crate produces and reads x86-64 machine code on any host: it uses no operating-system facilities and no platform-specific code. It does not execute the code it produces; mapping code executable is pager-lang's job.
Contributing
See REPS.md for the engineering standards every change is held to, dev/DIRECTIVES.md for this crate's invariants, and dev/ROADMAP.md for what comes next. Before a PR: cargo fmt --all, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features must be clean.