Skip to main content

asm_rs/
optimize.rs

1//! Peephole optimizations for x86/x86-64 instructions.
2//!
3//! These optimizations transform instructions into shorter or more efficient
4//! encodings. Which ones run is governed by [`OptLevel`]: the default
5//! [`OptLevel::Size`] is restricted to transforms that leave *all*
6//! architectural state (registers, memory, FLAGS) observably unchanged.
7//!
8//! ## Optimizations
9//!
10//! State-preserving (applied at [`OptLevel::Size`], the default):
11//!
12//! - **Immediate narrowing**: `mov r64, imm` where `imm` fits `u32` →
13//!   `mov r32, imm32` (writes to a 32-bit register zero-extend, so the 64-bit
14//!   result is unchanged); saves the REX.W byte.
15//! - **REX elimination**: `and r64, imm` where `imm` fits `u32` →
16//!   `and r32, imm32` (the result's upper 32 bits are zero either way).
17//! - **Test conversion**: `and reg, reg` → `test reg, reg` (`r & r == r`, so
18//!   the destination is unchanged, and both set FLAGS identically).
19//!
20//! FLAGS-clobbering (requires [`OptLevel::Aggressive`]):
21//!
22//! - **Zero idiom**: `mov reg, 0` → `xor reg, reg` (saves 3–5 bytes and is
23//!   recognised as a dependency-breaking idiom by modern CPUs, but writes
24//!   FLAGS).
25
26use crate::ir::*;
27
28/// Apply peephole optimizations to an instruction (mutates in place).
29///
30/// Returns `true` if the instruction was modified.
31///
32/// At [`OptLevel::Size`] every transform is observationally equivalent — the
33/// rewritten instruction leaves registers, memory *and FLAGS* exactly as the
34/// original would, so an assembler user never has to reason about whether
35/// optimization was on. Transforms that clobber FLAGS are gated behind
36/// [`OptLevel::Aggressive`].
37pub fn optimize_instruction(instr: &mut Instruction, arch: Arch, level: OptLevel) -> bool {
38    if !matches!(arch, Arch::X86 | Arch::X86_64) || level == OptLevel::None {
39        return false;
40    }
41
42    let mut changed = false;
43
44    // FLAGS-clobbering transforms first — they replace the whole instruction.
45    if level == OptLevel::Aggressive {
46        changed |= try_zero_idiom(instr);
47    }
48
49    // State-preserving transforms.
50    changed |= try_mov_imm32_narrow(instr, arch);
51    changed |= try_rex_elimination(instr, arch);
52    changed |= try_test_conversion(instr);
53
54    changed
55}
56
57/// `mov reg32/64, 0` → `xor reg32, reg32`
58///
59/// This saves 3–5 bytes and is recognized as a zero-idiom by modern CPUs
60/// (no register dependency, no partial-register stall).
61///
62/// **Clobbers FLAGS** (sets ZF=1, PF=1; clears CF/SF/OF), so it only runs at
63/// [`OptLevel::Aggressive`] where the caller has asserted FLAGS are dead.
64fn try_zero_idiom(instr: &mut Instruction) -> bool {
65    if instr.mnemonic != "mov" {
66        return false;
67    }
68    if instr.operands.len() != 2 {
69        return false;
70    }
71
72    // Only for register destinations
73    let dst_reg = match &instr.operands[0] {
74        Operand::Register(r) => *r,
75        _ => return false,
76    };
77
78    // Source must be immediate 0
79    let is_zero = matches!(&instr.operands[1], Operand::Immediate(0));
80    if !is_zero {
81        return false;
82    }
83
84    let bits = dst_reg.size_bits();
85    // Only 32-bit and 64-bit registers (8/16 have different performance characteristics)
86    if bits != 32 && bits != 64 {
87        return false;
88    }
89
90    // For 64-bit: xor eax, eax zero-extends to rax — use the 32-bit form
91    let xor_reg = if bits == 64 {
92        dst_reg.to_32bit()
93    } else {
94        Some(dst_reg)
95    };
96
97    if let Some(r32) = xor_reg {
98        instr.mnemonic = Mnemonic::from("xor");
99        instr.operands =
100            OperandList::from(alloc::vec![Operand::Register(r32), Operand::Register(r32)]);
101        // Clear any size hint since xor reg, reg doesn't need one
102        instr.size_hint = None;
103        return true;
104    }
105
106    false
107}
108
109/// `mov r64, imm` where imm fits in u32 → `mov r32, imm32`
110///
111/// In 64-bit mode, writing to a 32-bit register zero-extends to 64 bits.
112/// `mov rax, 1` (7 bytes: REX.W + B8 + imm32 or 10 bytes with imm64)
113/// becomes `mov eax, 1` (5 bytes: B8 + imm32).
114fn try_mov_imm32_narrow(instr: &mut Instruction, arch: Arch) -> bool {
115    if arch != Arch::X86_64 {
116        return false;
117    }
118    if instr.mnemonic != "mov" {
119        return false;
120    }
121    if instr.operands.len() != 2 {
122        return false;
123    }
124
125    let dst_reg = match &instr.operands[0] {
126        Operand::Register(r) => *r,
127        _ => return false,
128    };
129
130    if dst_reg.size_bits() != 64 {
131        return false;
132    }
133
134    let imm = match &instr.operands[1] {
135        Operand::Immediate(v) => *v,
136        _ => return false,
137    };
138
139    // Only if the immediate fits in unsigned 32-bit (0..0xFFFFFFFF)
140    // This ensures the zero-extension from 32-bit produces the same 64-bit value
141    if !(0..=0xFFFF_FFFF).contains(&imm) {
142        return false;
143    }
144
145    if let Some(r32) = dst_reg.to_32bit() {
146        instr.operands[0] = Operand::Register(r32);
147        return true;
148    }
149
150    false
151}
152
153/// `and r64, imm` where imm fits u32 → `and r32, imm32`
154///
155/// The AND operation with a non-negative immediate that fits in 32 bits will
156/// always clear the upper 32 bits of the result, making it equivalent to the
157/// 32-bit AND followed by zero-extension. This saves the REX.W byte (1 byte).
158///
159/// This is safe because AND with a u32 immediate always produces a result
160/// with upper 32 bits = 0, regardless of the register's original value.
161/// Other ALU operations (ADD, SUB, OR, XOR) are NOT generally safe to narrow
162/// because they may depend on or produce upper bits.
163fn try_rex_elimination(instr: &mut Instruction, arch: Arch) -> bool {
164    if arch != Arch::X86_64 {
165        return false;
166    }
167    if instr.mnemonic != "and" {
168        return false;
169    }
170    if instr.operands.len() != 2 {
171        return false;
172    }
173
174    let dst_reg = match &instr.operands[0] {
175        Operand::Register(r) => *r,
176        _ => return false,
177    };
178
179    if dst_reg.size_bits() != 64 {
180        return false;
181    }
182
183    let imm = match &instr.operands[1] {
184        Operand::Immediate(v) => *v,
185        _ => return false,
186    };
187
188    // Only if the immediate fits in unsigned 32-bit (non-negative, ≤ 0xFFFFFFFF)
189    // AND with such a value always zeros the upper 32 bits.
190    if !(0..=0xFFFF_FFFF).contains(&imm) {
191        return false;
192    }
193
194    if let Some(r32) = dst_reg.to_32bit() {
195        instr.operands[0] = Operand::Register(r32);
196        return true;
197    }
198
199    false
200}
201
202/// `and reg, reg` (same register) → `test reg, reg`
203///
204/// Both set flags identically, but `test` doesn't write the destination
205/// register, which can improve out-of-order execution.
206fn try_test_conversion(instr: &mut Instruction) -> bool {
207    if instr.mnemonic != "and" {
208        return false;
209    }
210    if instr.operands.len() != 2 {
211        return false;
212    }
213
214    let r1 = match &instr.operands[0] {
215        Operand::Register(r) => r,
216        _ => return false,
217    };
218    let r2 = match &instr.operands[1] {
219        Operand::Register(r) => r,
220        _ => return false,
221    };
222
223    if r1 == r2 {
224        instr.mnemonic = Mnemonic::from("test");
225        return true;
226    }
227
228    false
229}
230
231#[cfg(test)]
232mod tests {
233    use super::*;
234    use crate::error::Span;
235
236    fn make_instr(mnemonic: &str, ops: Vec<Operand>) -> Instruction {
237        Instruction {
238            mnemonic: Mnemonic::from(mnemonic),
239            operands: OperandList::from(ops),
240            size_hint: None,
241            prefixes: PrefixList::new(),
242            opmask: None,
243            zeroing: false,
244            broadcast: None,
245            span: Span::dummy(),
246        }
247    }
248
249    #[test]
250    fn zero_idiom_mov_eax_0() {
251        let mut instr = make_instr(
252            "mov",
253            alloc::vec![Operand::Register(Register::Eax), Operand::Immediate(0)],
254        );
255        assert!(optimize_instruction(
256            &mut instr,
257            Arch::X86_64,
258            OptLevel::Aggressive
259        ));
260        assert_eq!(instr.mnemonic, "xor");
261        assert_eq!(instr.operands[0], Operand::Register(Register::Eax));
262        assert_eq!(instr.operands[1], Operand::Register(Register::Eax));
263    }
264
265    #[test]
266    fn zero_idiom_mov_rax_0() {
267        let mut instr = make_instr(
268            "mov",
269            alloc::vec![Operand::Register(Register::Rax), Operand::Immediate(0)],
270        );
271        assert!(optimize_instruction(
272            &mut instr,
273            Arch::X86_64,
274            OptLevel::Aggressive
275        ));
276        assert_eq!(instr.mnemonic, "xor");
277        // 64-bit narrowed to 32-bit for shorter encoding
278        assert_eq!(instr.operands[0], Operand::Register(Register::Eax));
279    }
280
281    #[test]
282    fn zero_idiom_mov_r12_0() {
283        let mut instr = make_instr(
284            "mov",
285            alloc::vec![Operand::Register(Register::R12), Operand::Immediate(0)],
286        );
287        assert!(optimize_instruction(
288            &mut instr,
289            Arch::X86_64,
290            OptLevel::Aggressive
291        ));
292        assert_eq!(instr.mnemonic, "xor");
293        assert_eq!(instr.operands[0], Operand::Register(Register::R12d));
294    }
295
296    #[test]
297    fn zero_idiom_not_applied_nonzero() {
298        let mut instr = make_instr(
299            "mov",
300            alloc::vec![Operand::Register(Register::Eax), Operand::Immediate(1)],
301        );
302        assert!(!optimize_instruction(
303            &mut instr,
304            Arch::X86_64,
305            OptLevel::Aggressive
306        ));
307        assert_eq!(instr.mnemonic, "mov");
308    }
309
310    #[test]
311    fn zero_idiom_not_applied_8bit() {
312        let mut instr = make_instr(
313            "mov",
314            alloc::vec![Operand::Register(Register::Al), Operand::Immediate(0)],
315        );
316        assert!(!optimize_instruction(
317            &mut instr,
318            Arch::X86_64,
319            OptLevel::Aggressive
320        ));
321        assert_eq!(instr.mnemonic, "mov");
322    }
323
324    #[test]
325    fn mov_imm32_narrow_rax_1() {
326        let mut instr = make_instr(
327            "mov",
328            alloc::vec![Operand::Register(Register::Rax), Operand::Immediate(1)],
329        );
330        assert!(optimize_instruction(
331            &mut instr,
332            Arch::X86_64,
333            OptLevel::Size
334        ));
335        assert_eq!(instr.operands[0], Operand::Register(Register::Eax));
336        assert_eq!(instr.operands[1], Operand::Immediate(1));
337    }
338
339    #[test]
340    fn mov_imm32_narrow_rax_max_u32() {
341        let mut instr = make_instr(
342            "mov",
343            alloc::vec![
344                Operand::Register(Register::Rax),
345                Operand::Immediate(0xFFFF_FFFF),
346            ],
347        );
348        assert!(optimize_instruction(
349            &mut instr,
350            Arch::X86_64,
351            OptLevel::Size
352        ));
353        assert_eq!(instr.operands[0], Operand::Register(Register::Eax));
354    }
355
356    #[test]
357    fn mov_imm32_narrow_not_applied_negative() {
358        let mut instr = make_instr(
359            "mov",
360            alloc::vec![Operand::Register(Register::Rax), Operand::Immediate(-1)],
361        );
362        // -1 doesn't fit in unsigned 32-bit, so NOT narrowed
363        // (needs REX.W + sign-extended imm32)
364        assert!(!optimize_instruction(
365            &mut instr,
366            Arch::X86_64,
367            OptLevel::Size
368        ));
369    }
370
371    #[test]
372    fn mov_imm32_narrow_not_applied_large() {
373        let mut instr = make_instr(
374            "mov",
375            alloc::vec![
376                Operand::Register(Register::Rax),
377                Operand::Immediate(0x1_0000_0000),
378            ],
379        );
380        assert!(!optimize_instruction(
381            &mut instr,
382            Arch::X86_64,
383            OptLevel::Size
384        ));
385    }
386
387    #[test]
388    fn test_conversion_and_self() {
389        let mut instr = make_instr(
390            "and",
391            alloc::vec![
392                Operand::Register(Register::Eax),
393                Operand::Register(Register::Eax),
394            ],
395        );
396        assert!(optimize_instruction(
397            &mut instr,
398            Arch::X86_64,
399            OptLevel::Size
400        ));
401        assert_eq!(instr.mnemonic, "test");
402    }
403
404    #[test]
405    fn test_conversion_not_applied_different_regs() {
406        let mut instr = make_instr(
407            "and",
408            alloc::vec![
409                Operand::Register(Register::Eax),
410                Operand::Register(Register::Ebx),
411            ],
412        );
413        assert!(!optimize_instruction(
414            &mut instr,
415            Arch::X86_64,
416            OptLevel::Size
417        ));
418        assert_eq!(instr.mnemonic, "and");
419    }
420
421    // ── REX elimination ──────────────────────────────────────
422    #[test]
423    fn rex_elim_and_rax_0xff() {
424        // and rax, 0xFF → and eax, 0xFF (saves REX.W)
425        let mut instr = make_instr(
426            "and",
427            alloc::vec![Operand::Register(Register::Rax), Operand::Immediate(0xFF)],
428        );
429        assert!(optimize_instruction(
430            &mut instr,
431            Arch::X86_64,
432            OptLevel::Size
433        ));
434        assert_eq!(instr.mnemonic, "and");
435        assert_eq!(instr.operands[0], Operand::Register(Register::Eax));
436        assert_eq!(instr.operands[1], Operand::Immediate(0xFF));
437    }
438
439    #[test]
440    fn rex_elim_and_r12_u32_max() {
441        // and r12, 0xFFFFFFFF → and r12d, 0xFFFFFFFF
442        let mut instr = make_instr(
443            "and",
444            alloc::vec![
445                Operand::Register(Register::R12),
446                Operand::Immediate(0xFFFF_FFFF),
447            ],
448        );
449        assert!(optimize_instruction(
450            &mut instr,
451            Arch::X86_64,
452            OptLevel::Size
453        ));
454        assert_eq!(instr.operands[0], Operand::Register(Register::R12d));
455    }
456
457    #[test]
458    fn rex_elim_and_not_applied_negative() {
459        // and rax, -1 → NOT narrowed (negative imm, needs sign-extension)
460        let mut instr = make_instr(
461            "and",
462            alloc::vec![Operand::Register(Register::Rax), Operand::Immediate(-1)],
463        );
464        assert!(!optimize_instruction(
465            &mut instr,
466            Arch::X86_64,
467            OptLevel::Size
468        ));
469    }
470
471    #[test]
472    fn rex_elim_and_not_applied_large() {
473        // and rax, 0x100000000 → NOT narrowed (exceeds u32)
474        let mut instr = make_instr(
475            "and",
476            alloc::vec![
477                Operand::Register(Register::Rax),
478                Operand::Immediate(0x1_0000_0000),
479            ],
480        );
481        assert!(!optimize_instruction(
482            &mut instr,
483            Arch::X86_64,
484            OptLevel::Size
485        ));
486    }
487
488    #[test]
489    fn rex_elim_not_applied_to_add() {
490        // add rax, 5 → NOT narrowed (add can carry into upper bits)
491        let mut instr = make_instr(
492            "and", // Note: test_conversion fires first for and reg,reg
493            alloc::vec![Operand::Register(Register::Rax), Operand::Immediate(5)],
494        );
495        // This one SHOULD narrow (and rax, 5 → and eax, 5)
496        assert!(optimize_instruction(
497            &mut instr,
498            Arch::X86_64,
499            OptLevel::Size
500        ));
501
502        // But add should NOT narrow
503        let mut instr2 = make_instr(
504            "add",
505            alloc::vec![Operand::Register(Register::Rax), Operand::Immediate(5)],
506        );
507        assert!(!optimize_instruction(
508            &mut instr2,
509            Arch::X86_64,
510            OptLevel::Size
511        ));
512    }
513
514    #[test]
515    fn zero_idiom_not_applied_at_default_level() {
516        // The default level must never clobber FLAGS: `mov eax, 0` between a
517        // `cmp` and a `sete` has to stay a `mov`.
518        let mut instr = make_instr(
519            "mov",
520            alloc::vec![Operand::Register(Register::Eax), Operand::Immediate(0)],
521        );
522        assert!(!optimize_instruction(
523            &mut instr,
524            Arch::X86_64,
525            OptLevel::Size
526        ));
527        assert_eq!(instr.mnemonic, "mov");
528    }
529
530    #[test]
531    fn opt_level_none_disables_everything() {
532        let mut instr = make_instr(
533            "mov",
534            alloc::vec![Operand::Register(Register::Rax), Operand::Immediate(1)],
535        );
536        assert!(!optimize_instruction(
537            &mut instr,
538            Arch::X86_64,
539            OptLevel::None
540        ));
541        assert_eq!(instr.operands[0], Operand::Register(Register::Rax));
542    }
543
544    #[test]
545    fn rex_elim_not_applied_32bit_arch() {
546        let mut instr = make_instr(
547            "and",
548            alloc::vec![Operand::Register(Register::Eax), Operand::Immediate(0xFF)],
549        );
550        assert!(!optimize_instruction(&mut instr, Arch::X86, OptLevel::Size));
551    }
552}