Skip to main content

solar_codegen/backend/evm/
assembler.rs

1//! Two-pass assembler with label resolution.
2//!
3//! The assembler handles:
4//! - Label definition and reference tracking
5//! - Two-pass assembly for resolving jump targets
6//! - Variable-width PUSH sizing based on offset magnitudes
7
8use crate::mir::IMMUTABLE_WORD_SIZE;
9use alloy_primitives::U256;
10use solar_config::{EvmVersion, OptimizationMode};
11use solar_data_structures::map::FxHashMap;
12
13const EVM_WORD_BYTES: usize = 32;
14const EVM_WORD_BITS: usize = EVM_WORD_BYTES * 8;
15const MIN_COMPACT_MASK_WIDTH: u8 = EVM_WORD_BYTES as u8 / 2;
16
17mod id_counter;
18use id_counter::IdCounter;
19
20mod inst;
21pub(super) use inst::{AsmInst, AsmInstKind, PushValueId};
22pub use inst::{DeferredConst, Label};
23
24mod local_interner;
25use local_interner::LocalInterner;
26
27mod program;
28pub(in crate::backend::evm) use program::{
29    EvmAsmProgram, StructuredAsmContext, StructuredAsmProgram,
30};
31
32/// A `PUSH32` immutable placeholder emitted into the assembled bytecode.
33///
34/// TODO: Track placeholder byte width here when smaller immutable references
35/// are supported.
36#[derive(Clone, Copy, Debug, PartialEq, Eq)]
37pub struct ImmutableRef {
38    /// The immutable's byte offset identifier.
39    pub id: u32,
40    /// Byte offset of the `PUSH32` opcode in the assembled bytecode.
41    /// The 32 placeholder bytes start one byte later.
42    pub code_offset: usize,
43}
44
45/// Result of assembly.
46#[derive(Debug)]
47pub struct AssembledCode {
48    /// The final bytecode.
49    pub bytecode: Vec<u8>,
50    /// Map from label to its final offset.
51    pub label_offsets: FxHashMap<Label, usize>,
52    /// All immutable placeholders, in emission order.
53    pub immutable_refs: Vec<ImmutableRef>,
54}
55
56/// Configuration for EVM bytecode assembly.
57#[derive(Clone, Copy, Debug, Default)]
58pub struct AssemblerConfig {
59    /// EVM version to target when selecting hardfork-gated opcodes.
60    pub evm_version: EvmVersion,
61    /// Optimization mode for alternate byte encodings.
62    pub optimization: OptimizationMode,
63    /// Run the experimental EVM IR `StackSchedule` pass in the assembler bridge.
64    ///
65    /// Off by default. See `StructuredAsmProgram::optimize_with_evm_ir` for why
66    /// the pass is a verified near no-op on the bridge's operand-cleared IR.
67    pub evm_ir_stack_schedule: bool,
68    /// Run EVM IR layout/code-size passes in the assembler bridge.
69    ///
70    /// Kept separate from `evm_ir_stack_schedule` so the experimental scheduler
71    /// flag remains bytecode-neutral.
72    pub evm_ir_layout_passes: bool,
73}
74
75/// Two-pass assembler for EVM bytecode.
76#[derive(Debug)]
77pub struct Assembler {
78    /// Bytecode assembly configuration.
79    config: AssemblerConfig,
80    /// Structured assembler block program to assemble.
81    pub(in crate::backend::evm) program: StructuredAsmProgram,
82    /// Interned push immediates too large for inline storage.
83    push_values: LocalInterner<U256, PushValueId>,
84    /// Next label ID.
85    next_label: IdCounter<Label>,
86    /// Next deferred constant ID.
87    next_deferred: IdCounter<DeferredConst>,
88    /// Resolved values for deferred constants.
89    deferred_values: FxHashMap<DeferredConst, U256>,
90}
91
92impl Assembler {
93    /// Creates a new assembler.
94    #[must_use]
95    pub fn new() -> Self {
96        Self::with_config(AssemblerConfig::default())
97    }
98
99    /// Creates a new assembler with the given configuration.
100    #[must_use]
101    pub fn with_config(config: AssemblerConfig) -> Self {
102        Self {
103            config,
104            program: StructuredAsmProgram::default(),
105            push_values: LocalInterner::new(),
106            next_label: IdCounter::new(),
107            next_deferred: IdCounter::new(),
108            deferred_values: FxHashMap::default(),
109        }
110    }
111
112    /// Clears all emitted instructions and local identifiers while retaining allocated storage.
113    pub fn clear(&mut self) {
114        self.program.clear();
115        self.push_values.clear();
116        self.next_label.clear();
117        self.next_deferred.clear();
118        self.deferred_values.clear();
119    }
120
121    /// Creates a new label.
122    pub fn new_label(&mut self) -> Label {
123        self.next_label.next()
124    }
125
126    /// Creates a new deferred constant.
127    pub fn new_deferred_const(&mut self) -> DeferredConst {
128        self.next_deferred.next()
129    }
130
131    /// Emits a raw opcode.
132    pub fn emit_op(&mut self, opcode: u8) {
133        self.program.push(AsmInst::op(opcode));
134    }
135
136    /// Emits a push instruction with an immediate value.
137    pub fn emit_push(&mut self, value: U256) {
138        let inst = self.push_inst(value);
139        self.program.push(inst);
140    }
141
142    /// Emits a push instruction that will be resolved to a label's offset.
143    pub fn emit_push_label(&mut self, label: Label) {
144        self.program.push(AsmInst::push_label(label));
145    }
146
147    /// Emits a push instruction for a deferred constant.
148    pub fn emit_push_deferred(&mut self, id: DeferredConst) {
149        self.program.push(AsmInst::push_deferred(id));
150    }
151
152    /// Sets the value of a deferred constant.
153    pub fn set_deferred_const(&mut self, id: DeferredConst, value: U256) {
154        self.deferred_values.insert(id, value);
155    }
156
157    /// Emits a `PUSH32` zero placeholder for the immutable identified by `id`.
158    pub fn emit_push_immutable(&mut self, id: u32) {
159        self.program.push(AsmInst::push_immutable(id));
160    }
161
162    /// Defines a label and emits a `JUMPDEST` at the current position.
163    pub fn define_label(&mut self, label: Label) {
164        self.program.define_label(label);
165    }
166
167    /// Marks a label-started block as cold for EVM IR layout passes.
168    pub(in crate::backend::evm) fn mark_label_cold(&mut self, label: Label) {
169        self.program.mark_cold(label);
170    }
171
172    fn push_inst(&mut self, value: U256) -> AsmInst {
173        if let Ok(value) = u32::try_from(value)
174            && let Some(inst) = AsmInst::push_inline(value)
175        {
176            return inst;
177        }
178
179        AsmInst::push(self.push_values.intern(value))
180    }
181
182    pub(super) fn push_value(&self, index: PushValueId) -> U256 {
183        *self.push_values.get(index)
184    }
185
186    pub(super) fn inst_push_value(&self, inst: AsmInst) -> Option<U256> {
187        match inst.kind() {
188            AsmInstKind::PushInline(value) => Some(U256::from(value)),
189            AsmInstKind::Push(index) => Some(self.push_value(index)),
190            _ => None,
191        }
192    }
193
194    /// Assembles the instructions into bytecode.
195    /// Uses an iterative two-pass algorithm that handles PUSH width changes.
196    #[must_use]
197    pub fn assemble(&mut self) -> AssembledCode {
198        let mut ir_program = std::mem::take(&mut self.program);
199        let deferred_values = &self.deferred_values;
200        let push_values = &mut self.push_values;
201        ir_program.resolve_deferred_consts(|id| {
202            let value = deferred_values
203                .get(&id)
204                .copied()
205                .unwrap_or_else(|| panic!("deferred constant {id:?} was never resolved"));
206            if let Ok(value) = u32::try_from(value)
207                && let Some(inst) = AsmInst::push_inline(value)
208            {
209                return inst;
210            }
211            AsmInst::push(push_values.intern(value))
212        });
213        if self.config.evm_ir_stack_schedule || self.config.evm_ir_layout_passes {
214            ir_program.optimize_with_evm_ir(self);
215        }
216        let mut program = ir_program.to_asm_program();
217        self.run_assembler_passes(&mut program);
218
219        // We need to iterate until PUSH widths stabilize
220        let mut push_widths: FxHashMap<usize, u8> = FxHashMap::default();
221
222        // Initialize all label pushes to 2 bytes (PUSH2)
223        for (idx, inst) in program.instructions.iter().enumerate() {
224            if matches!(inst.kind(), AsmInstKind::PushLabel(_)) {
225                push_widths.insert(idx, 2);
226            }
227        }
228
229        // Iterate until stable
230        let max_iterations = 10;
231        for _ in 0..max_iterations {
232            let (label_offsets, new_widths) = self.compute_offsets(&program, &push_widths);
233
234            let mut changed = false;
235            for (idx, &width) in &new_widths {
236                if push_widths.get(idx) != Some(&width) {
237                    changed = true;
238                }
239            }
240
241            if !changed {
242                // Stable - emit final bytecode
243                let result = self.emit_bytecode(&program, label_offsets, &push_widths);
244                self.clear();
245                return result;
246            }
247
248            for (idx, width) in new_widths {
249                push_widths.insert(idx, width);
250            }
251        }
252
253        // Fallback - just emit with current widths
254        let (label_offsets, _) = self.compute_offsets(&program, &push_widths);
255        let result = self.emit_bytecode(&program, label_offsets, &push_widths);
256        self.clear();
257        result
258    }
259
260    /// Computes label offsets given current PUSH widths.
261    fn compute_offsets(
262        &self,
263        program: &EvmAsmProgram,
264        push_widths: &FxHashMap<usize, u8>,
265    ) -> (FxHashMap<Label, usize>, FxHashMap<usize, u8>) {
266        let mut offset = 0usize;
267        let mut label_offsets = FxHashMap::default();
268        let mut new_widths = FxHashMap::default();
269        let out = BytecodeAssembler::new(self.config);
270
271        for (idx, inst) in program.instructions.iter().enumerate() {
272            match inst.kind() {
273                AsmInstKind::Op(_) => {
274                    offset += 1;
275                }
276                AsmInstKind::PushInline(value) => {
277                    offset += out.encoded_push_len(U256::from(value));
278                }
279                AsmInstKind::Push(index) => {
280                    offset += out.encoded_push_len(self.push_value(index));
281                }
282                AsmInstKind::PushLabel(_) => {
283                    // Use current estimated width
284                    let width = push_widths.get(&idx).copied().unwrap_or(2);
285                    offset += out.fixed_push_len(width);
286                }
287                AsmInstKind::PushDeferred(_) => {
288                    unreachable!("deferred constants must be resolved before assembly");
289                }
290                AsmInstKind::PushImmutable(_) => {
291                    // PUSH32 opcode plus 32 placeholder bytes.
292                    offset += 33;
293                }
294                AsmInstKind::Label(label) => {
295                    label_offsets.insert(label, offset);
296                    offset += 1;
297                }
298            }
299        }
300
301        // Compute new widths based on resolved offsets
302        for (idx, inst) in program.instructions.iter().enumerate() {
303            if let AsmInstKind::PushLabel(label) = inst.kind()
304                && let Some(&target_offset) = label_offsets.get(&label)
305            {
306                let width = out.push_width(U256::from(target_offset));
307                new_widths.insert(idx, width);
308            }
309        }
310
311        (label_offsets, new_widths)
312    }
313
314    /// Emits the final bytecode.
315    fn emit_bytecode(
316        &self,
317        program: &EvmAsmProgram,
318        label_offsets: FxHashMap<Label, usize>,
319        push_widths: &FxHashMap<usize, u8>,
320    ) -> AssembledCode {
321        let mut out = BytecodeAssembler::new(self.config);
322
323        for (idx, inst) in program.instructions.iter().enumerate() {
324            match inst.kind() {
325                AsmInstKind::Op(opcode) => {
326                    out.emit_op(opcode);
327                }
328                AsmInstKind::PushInline(value) => {
329                    out.emit_push_value(U256::from(value));
330                }
331                AsmInstKind::Push(index) => {
332                    out.emit_push_value(self.push_value(index));
333                }
334                AsmInstKind::PushLabel(label) => {
335                    let target_offset = label_offsets
336                        .get(&label)
337                        .copied()
338                        .unwrap_or_else(|| panic!("label {label:?} was never defined"));
339                    let width = push_widths.get(&idx).copied().unwrap_or(2);
340                    out.emit_push_fixed_width(U256::from(target_offset), width);
341                }
342                AsmInstKind::PushDeferred(_) => {
343                    unreachable!("deferred constants must be resolved before assembly");
344                }
345                AsmInstKind::PushImmutable(id) => {
346                    out.emit_push_immutable(id);
347                }
348                AsmInstKind::Label(_) => {
349                    out.emit_op(op::JUMPDEST);
350                }
351            }
352        }
353
354        out.finish(label_offsets)
355    }
356
357    /// Returns the minimum number of non-zero bytes needed to push a value.
358    #[cfg(test)]
359    fn push_width(value: U256) -> u8 {
360        value.byte_len() as u8
361    }
362}
363
364impl Default for Assembler {
365    fn default() -> Self {
366        Self::new()
367    }
368}
369
370impl StructuredAsmContext for Assembler {
371    fn push_value(&self, index: PushValueId) -> U256 {
372        self.push_value(index)
373    }
374
375    fn push_inst(&mut self, value: U256) -> AsmInst {
376        self.push_inst(value)
377    }
378
379    fn new_label(&mut self) -> Label {
380        self.new_label()
381    }
382
383    fn run_evm_ir_stack_schedule(&self) -> bool {
384        self.config.evm_ir_stack_schedule
385    }
386
387    fn run_evm_ir_layout_passes(&self) -> bool {
388        self.config.evm_ir_layout_passes
389    }
390}
391
392#[derive(Debug)]
393struct BytecodeAssembler {
394    config: AssemblerConfig,
395    bytecode: Vec<u8>,
396    immutable_refs: Vec<ImmutableRef>,
397}
398
399impl BytecodeAssembler {
400    fn new(config: AssemblerConfig) -> Self {
401        Self { config, bytecode: Vec::new(), immutable_refs: Vec::new() }
402    }
403
404    fn emit_op(&mut self, opcode: u8) {
405        self.bytecode.push(opcode);
406    }
407
408    fn emit_push_immutable(&mut self, id: u32) {
409        self.immutable_refs.push(ImmutableRef { id, code_offset: self.bytecode.len() });
410        self.bytecode.push(op::PUSH32);
411        self.bytecode.extend(std::iter::repeat_n(0, IMMUTABLE_WORD_SIZE));
412    }
413
414    fn encoded_push_len(&self, value: U256) -> usize {
415        match self.compact_push(value) {
416            CompactPush::Literal { width } => self.fixed_push_len(width),
417            CompactPush::FullWord => self.zero_push_len() + 1,
418            CompactPush::LowerAllOnesMask { .. } => self.zero_push_len() + 4,
419            CompactPush::Not => self.fixed_push_len(self.push_width(!value)) + 1,
420            CompactPush::Shl { shift } => {
421                self.fixed_push_len(self.push_width(value >> usize::from(shift))) + 3
422            }
423        }
424    }
425
426    fn compact_push(&self, value: U256) -> CompactPush {
427        let width = self.push_width(value);
428        let normal_len = self.fixed_push_len(width);
429        let mut best = (normal_len, CompactPush::Literal { width });
430
431        if self.config.optimization == OptimizationMode::None {
432            return best.1;
433        }
434
435        let mut consider = |len: usize, compact: CompactPush| {
436            if len < best.0 {
437                best = (len, compact);
438            }
439        };
440
441        if value == U256::MAX {
442            consider(self.zero_push_len() + 1, CompactPush::FullWord);
443        }
444
445        // `PUSH0 NOT PUSH1 <shift> SHR` is fixed-size apart from PUSH0
446        // availability: 5 bytes on Shanghai+, 6 bytes before Shanghai. Keep
447        // this shape for half-word-or-wider masks only: small masks are common
448        // immediates, while wide masks are where the bytecode-size win is
449        // substantial.
450        if width >= MIN_COMPACT_MASK_WIDTH {
451            let bytes = value.to_be_bytes::<EVM_WORD_BYTES>();
452            let start = EVM_WORD_BYTES - width as usize;
453            if bytes[start..].iter().all(|&byte| byte == 0xff) {
454                let shift = EVM_WORD_BITS - usize::from(width) * 8;
455                consider(
456                    self.zero_push_len() + 4,
457                    CompactPush::LowerAllOnesMask { shift: shift as u8 },
458                );
459            }
460        }
461
462        // `PUSH<!value> NOT` costs one extra opcode but can be much smaller
463        // for values with many leading one bits. It only has a chance to win
464        // for full-width values: narrower values have zero high bytes, so
465        // inversion turns those into leading `0xff` bytes and needs PUSH32.
466        if width as usize == EVM_WORD_BYTES {
467            let inverted = !value;
468            let inverted_width = self.push_width(inverted);
469            let inverted_len = self.fixed_push_len(inverted_width) + 1;
470            consider(inverted_len, CompactPush::Not);
471        }
472
473        // A left shift can avoid embedding right-aligned zero bytes. The
474        // sequence pays three bytes over the shifted literal (`PUSH1
475        // <shift> SHL`), so `consider` keeps it only when that actually beats
476        // the normal literal.
477        let trailing_zero_bytes = (0..EVM_WORD_BYTES).take_while(|&i| value.byte(i) == 0).count();
478        if trailing_zero_bytes > 0 && trailing_zero_bytes < EVM_WORD_BYTES {
479            let shift = trailing_zero_bytes * 8;
480            let shifted = value >> shift;
481            let shifted_width = self.push_width(shifted);
482            let shifted_len = self.fixed_push_len(shifted_width) + 3;
483            consider(shifted_len, CompactPush::Shl { shift: shift as u8 });
484        }
485
486        best.1
487    }
488
489    /// Emits a PUSH instruction with automatically sized width.
490    fn emit_push_value(&mut self, value: U256) {
491        match self.compact_push(value) {
492            CompactPush::Literal { width } => {
493                self.emit_push_fixed_width(value, width);
494            }
495            CompactPush::FullWord => {
496                self.emit_push_zero();
497                self.bytecode.push(op::NOT);
498            }
499            CompactPush::LowerAllOnesMask { shift } => {
500                self.emit_push_zero();
501                self.bytecode.push(op::NOT);
502                self.bytecode.push(op::PUSH1);
503                self.bytecode.push(shift);
504                self.bytecode.push(op::SHR);
505            }
506            CompactPush::Not => {
507                let inverted = !value;
508                self.emit_push_fixed_width(inverted, self.push_width(inverted));
509                self.bytecode.push(op::NOT);
510            }
511            CompactPush::Shl { shift } => {
512                let shifted = value >> usize::from(shift);
513                self.emit_push_fixed_width(shifted, self.push_width(shifted));
514                self.bytecode.push(op::PUSH1);
515                self.bytecode.push(shift);
516                self.bytecode.push(op::SHL);
517            }
518        }
519    }
520
521    /// Emits a PUSH instruction with a specific width.
522    fn emit_push_fixed_width(&mut self, value: U256, width: u8) {
523        if width == 0 {
524            self.emit_push_zero();
525            return;
526        }
527
528        self.bytecode.push(op::push(width));
529
530        let bytes = value.to_be_bytes::<EVM_WORD_BYTES>();
531        let start = EVM_WORD_BYTES - width as usize;
532        self.bytecode.extend_from_slice(&bytes[start..]);
533    }
534
535    fn emit_push_zero(&mut self) {
536        if self.config.evm_version.has_push0() {
537            self.bytecode.push(op::PUSH0);
538        } else {
539            self.bytecode.push(op::PUSH1);
540            self.bytecode.push(0);
541        }
542    }
543
544    fn fixed_push_len(&self, width: u8) -> usize {
545        if width == 0 { self.zero_push_len() } else { 1 + width as usize }
546    }
547
548    fn zero_push_len(&self) -> usize {
549        if self.config.evm_version.has_push0() { 1 } else { 2 }
550    }
551
552    /// Returns the minimum immediate width needed to push a value for this EVM version.
553    fn push_width(&self, value: U256) -> u8 {
554        if value.is_zero() && !self.config.evm_version.has_push0() {
555            1
556        } else {
557            value.byte_len() as u8
558        }
559    }
560
561    fn finish(self, label_offsets: FxHashMap<Label, usize>) -> AssembledCode {
562        AssembledCode {
563            bytecode: self.bytecode,
564            label_offsets,
565            immutable_refs: self.immutable_refs,
566        }
567    }
568}
569
570#[derive(Clone, Copy, Debug, PartialEq, Eq)]
571enum CompactPush {
572    /// Emit the value as the shortest literal PUSH for the active EVM version.
573    Literal { width: u8 },
574    /// Emit all ones as `PUSH0 NOT`.
575    FullWord,
576    /// Emit a lower-bit all-ones mask as `PUSH0 NOT PUSH1 <shift> SHR`.
577    LowerAllOnesMask { shift: u8 },
578    /// Emit a value with many leading one bits as `PUSH<!value> NOT`.
579    Not,
580    /// Emit a value with trailing zero bytes as `PUSH<value >> shift> PUSH1 <shift> SHL`.
581    Shl { shift: u8 },
582}
583
584/// Common EVM op.
585pub mod op {
586    pub const STOP: u8 = 0x00;
587    pub const ADD: u8 = 0x01;
588    pub const MUL: u8 = 0x02;
589    pub const SUB: u8 = 0x03;
590    pub const DIV: u8 = 0x04;
591    pub const SDIV: u8 = 0x05;
592    pub const MOD: u8 = 0x06;
593    pub const SMOD: u8 = 0x07;
594    pub const ADDMOD: u8 = 0x08;
595    pub const MULMOD: u8 = 0x09;
596    pub const EXP: u8 = 0x0a;
597    pub const SIGNEXTEND: u8 = 0x0b;
598
599    pub const LT: u8 = 0x10;
600    pub const GT: u8 = 0x11;
601    pub const SLT: u8 = 0x12;
602    pub const SGT: u8 = 0x13;
603    pub const EQ: u8 = 0x14;
604    pub const ISZERO: u8 = 0x15;
605    pub const AND: u8 = 0x16;
606    pub const OR: u8 = 0x17;
607    pub const XOR: u8 = 0x18;
608    pub const NOT: u8 = 0x19;
609    pub const BYTE: u8 = 0x1a;
610    pub const SHL: u8 = 0x1b;
611    pub const SHR: u8 = 0x1c;
612    pub const SAR: u8 = 0x1d;
613    pub const CLZ: u8 = 0x1e;
614
615    pub const KECCAK256: u8 = 0x20;
616
617    pub const ADDRESS: u8 = 0x30;
618    pub const BALANCE: u8 = 0x31;
619    pub const ORIGIN: u8 = 0x32;
620    pub const CALLER: u8 = 0x33;
621    pub const CALLVALUE: u8 = 0x34;
622    pub const CALLDATALOAD: u8 = 0x35;
623    pub const CALLDATASIZE: u8 = 0x36;
624    pub const CALLDATACOPY: u8 = 0x37;
625    pub const CODESIZE: u8 = 0x38;
626    pub const CODECOPY: u8 = 0x39;
627    pub const GASPRICE: u8 = 0x3a;
628    pub const EXTCODESIZE: u8 = 0x3b;
629    pub const EXTCODECOPY: u8 = 0x3c;
630    pub const RETURNDATASIZE: u8 = 0x3d;
631    pub const RETURNDATACOPY: u8 = 0x3e;
632    pub const EXTCODEHASH: u8 = 0x3f;
633
634    pub const BLOCKHASH: u8 = 0x40;
635    pub const COINBASE: u8 = 0x41;
636    pub const TIMESTAMP: u8 = 0x42;
637    pub const NUMBER: u8 = 0x43;
638    pub const PREVRANDAO: u8 = 0x44;
639    pub const GASLIMIT: u8 = 0x45;
640    pub const CHAINID: u8 = 0x46;
641    pub const SELFBALANCE: u8 = 0x47;
642    pub const BASEFEE: u8 = 0x48;
643    pub const BLOBHASH: u8 = 0x49;
644    pub const BLOBBASEFEE: u8 = 0x4a;
645
646    pub const POP: u8 = 0x50;
647    pub const MLOAD: u8 = 0x51;
648    pub const MSTORE: u8 = 0x52;
649    pub const MSTORE8: u8 = 0x53;
650    pub const SLOAD: u8 = 0x54;
651    pub const SSTORE: u8 = 0x55;
652    pub const JUMP: u8 = 0x56;
653    pub const JUMPI: u8 = 0x57;
654    pub const PC: u8 = 0x58;
655    pub const MSIZE: u8 = 0x59;
656    pub const GAS: u8 = 0x5a;
657    pub const JUMPDEST: u8 = 0x5b;
658    pub const TLOAD: u8 = 0x5c;
659    pub const TSTORE: u8 = 0x5d;
660    pub const MCOPY: u8 = 0x5e;
661    pub const PUSH0: u8 = 0x5f;
662    pub const PUSH1: u8 = 0x60;
663    pub const PUSH2: u8 = 0x61;
664    pub const PUSH3: u8 = 0x62;
665    pub const PUSH4: u8 = 0x63;
666    pub const PUSH5: u8 = 0x64;
667    pub const PUSH6: u8 = 0x65;
668    pub const PUSH7: u8 = 0x66;
669    pub const PUSH8: u8 = 0x67;
670    pub const PUSH9: u8 = 0x68;
671    pub const PUSH10: u8 = 0x69;
672    pub const PUSH11: u8 = 0x6a;
673    pub const PUSH12: u8 = 0x6b;
674    pub const PUSH13: u8 = 0x6c;
675    pub const PUSH14: u8 = 0x6d;
676    pub const PUSH15: u8 = 0x6e;
677    pub const PUSH16: u8 = 0x6f;
678    pub const PUSH17: u8 = 0x70;
679    pub const PUSH18: u8 = 0x71;
680    pub const PUSH19: u8 = 0x72;
681    pub const PUSH20: u8 = 0x73;
682    pub const PUSH21: u8 = 0x74;
683    pub const PUSH22: u8 = 0x75;
684    pub const PUSH23: u8 = 0x76;
685    pub const PUSH24: u8 = 0x77;
686    pub const PUSH25: u8 = 0x78;
687    pub const PUSH26: u8 = 0x79;
688    pub const PUSH27: u8 = 0x7a;
689    pub const PUSH28: u8 = 0x7b;
690    pub const PUSH29: u8 = 0x7c;
691    pub const PUSH30: u8 = 0x7d;
692    pub const PUSH31: u8 = 0x7e;
693    pub const PUSH32: u8 = 0x7f;
694
695    pub const DUP1: u8 = 0x80;
696    pub const DUP2: u8 = 0x81;
697    pub const DUP3: u8 = 0x82;
698    pub const DUP4: u8 = 0x83;
699    pub const DUP5: u8 = 0x84;
700    pub const DUP6: u8 = 0x85;
701    pub const DUP7: u8 = 0x86;
702    pub const DUP8: u8 = 0x87;
703    pub const DUP9: u8 = 0x88;
704    pub const DUP10: u8 = 0x89;
705    pub const DUP11: u8 = 0x8a;
706    pub const DUP12: u8 = 0x8b;
707    pub const DUP13: u8 = 0x8c;
708    pub const DUP14: u8 = 0x8d;
709    pub const DUP15: u8 = 0x8e;
710    pub const DUP16: u8 = 0x8f;
711
712    pub const SWAP1: u8 = 0x90;
713    pub const SWAP2: u8 = 0x91;
714    pub const SWAP3: u8 = 0x92;
715    pub const SWAP4: u8 = 0x93;
716    pub const SWAP5: u8 = 0x94;
717    pub const SWAP6: u8 = 0x95;
718    pub const SWAP7: u8 = 0x96;
719    pub const SWAP8: u8 = 0x97;
720    pub const SWAP9: u8 = 0x98;
721    pub const SWAP10: u8 = 0x99;
722    pub const SWAP11: u8 = 0x9a;
723    pub const SWAP12: u8 = 0x9b;
724    pub const SWAP13: u8 = 0x9c;
725    pub const SWAP14: u8 = 0x9d;
726    pub const SWAP15: u8 = 0x9e;
727    pub const SWAP16: u8 = 0x9f;
728
729    pub const LOG0: u8 = 0xa0;
730    pub const LOG1: u8 = 0xa1;
731    pub const LOG2: u8 = 0xa2;
732    pub const LOG3: u8 = 0xa3;
733    pub const LOG4: u8 = 0xa4;
734
735    pub const DATALOAD: u8 = 0xd0;
736    pub const DATALOADN: u8 = 0xd1;
737    pub const DATASIZE: u8 = 0xd2;
738    pub const DATACOPY: u8 = 0xd3;
739
740    pub const RJUMP: u8 = 0xe0;
741    pub const RJUMPI: u8 = 0xe1;
742    pub const RJUMPV: u8 = 0xe2;
743    pub const CALLF: u8 = 0xe3;
744    pub const RETF: u8 = 0xe4;
745    pub const JUMPF: u8 = 0xe5;
746    pub const DUPN: u8 = 0xe6;
747    pub const SWAPN: u8 = 0xe7;
748    pub const EXCHANGE: u8 = 0xe8;
749    pub const EOFCREATE: u8 = 0xec;
750    pub const RETURNCONTRACT: u8 = 0xee;
751
752    pub const CREATE: u8 = 0xf0;
753    pub const CALL: u8 = 0xf1;
754    pub const CALLCODE: u8 = 0xf2;
755    pub const RETURN: u8 = 0xf3;
756    pub const DELEGATECALL: u8 = 0xf4;
757    pub const CREATE2: u8 = 0xf5;
758    pub const RETURNDATALOAD: u8 = 0xf7;
759    pub const EXTCALL: u8 = 0xf8;
760    pub const EXTDELEGATECALL: u8 = 0xf9;
761    pub const STATICCALL: u8 = 0xfa;
762    pub const EXTSTATICCALL: u8 = 0xfb;
763    pub const REVERT: u8 = 0xfd;
764    pub const INVALID: u8 = 0xfe;
765    pub const SELFDESTRUCT: u8 = 0xff;
766
767    /// Returns the PUSH opcode for the given width (1-32).
768    #[must_use]
769    pub const fn push(width: u8) -> u8 {
770        debug_assert!(width >= 1 && width <= 32);
771        PUSH1 + width - 1
772    }
773
774    /// Returns the DUP opcode for the given depth (1-16).
775    #[must_use]
776    pub const fn dup(n: u8) -> u8 {
777        debug_assert!(n >= 1 && n <= 16);
778        DUP1 + n - 1
779    }
780
781    /// Returns the SWAP opcode for the given depth (1-16).
782    #[must_use]
783    pub const fn swap(n: u8) -> u8 {
784        debug_assert!(n >= 1 && n <= 16);
785        SWAP1 + n - 1
786    }
787
788    /// Returns whether an opcode halts or unconditionally transfers control.
789    #[must_use]
790    pub const fn is_terminal(op: u8) -> bool {
791        matches!(op, STOP | JUMP | RETURN | REVERT | INVALID | SELFDESTRUCT)
792    }
793}
794
795#[cfg(test)]
796mod tests {
797    use super::*;
798
799    fn size_optimized_assembler() -> Assembler {
800        Assembler::with_config(AssemblerConfig {
801            evm_version: EvmVersion::Shanghai,
802            optimization: OptimizationMode::Size,
803            ..AssemblerConfig::default()
804        })
805    }
806
807    #[test]
808    fn test_push_width() {
809        assert_eq!(Assembler::push_width(U256::ZERO), 0);
810        assert_eq!(Assembler::push_width(U256::from(1)), 1);
811        assert_eq!(Assembler::push_width(U256::from(255)), 1);
812        assert_eq!(Assembler::push_width(U256::from(256)), 2);
813        assert_eq!(Assembler::push_width(U256::from(0xFFFF)), 2);
814        assert_eq!(Assembler::push_width(U256::from(0x10000)), 3);
815    }
816
817    #[test]
818    fn assembler_inst_is_compact() {
819        assert_eq!(std::mem::size_of::<AsmInst>(), 4);
820    }
821
822    #[test]
823    fn push_values_are_inline_or_interned() {
824        let mut asm = Assembler::new();
825        let inline = u32::MAX >> 1;
826        let large = U256::from(1u64 << 31);
827
828        assert!(AsmInst::push_inline(inline).is_some());
829        assert!(AsmInst::push_inline(1u32 << 31).is_none());
830
831        asm.emit_push(U256::from(inline));
832        asm.emit_push(large);
833        asm.emit_push(large);
834
835        let instructions = asm.program.instructions();
836        assert_eq!(instructions[0].kind(), AsmInstKind::PushInline(inline));
837        assert_eq!(instructions[1].kind(), AsmInstKind::Push(PushValueId::from_usize(0)));
838        assert_eq!(instructions[1], instructions[2]);
839        assert_eq!(asm.push_values.len(), 1);
840        assert_eq!(*asm.push_values.get(PushValueId::from_usize(0)), large);
841    }
842
843    #[test]
844    fn assembler_can_be_reused_after_assembly() {
845        let mut asm = Assembler::new();
846        let large = U256::from(1u64 << 31);
847
848        asm.emit_push(large);
849        let first = asm.assemble();
850
851        assert_eq!(first.bytecode, vec![0x63, 0x80, 0, 0, 0]);
852        assert!(asm.program.instructions().is_empty());
853        assert_eq!(asm.push_values.len(), 0);
854
855        asm.emit_push(U256::from(2));
856        let second = asm.assemble();
857
858        assert_eq!(second.bytecode, vec![0x60, 2]);
859    }
860
861    #[test]
862    fn push_zero_uses_push0_when_available() {
863        let mut asm = Assembler::with_config(AssemblerConfig {
864            evm_version: EvmVersion::Shanghai,
865            optimization: OptimizationMode::None,
866            ..AssemblerConfig::default()
867        });
868
869        asm.emit_push(U256::ZERO);
870        let result = asm.assemble();
871
872        assert_eq!(result.bytecode, vec![op::PUSH0]);
873    }
874
875    #[test]
876    fn push_zero_uses_push1_before_shanghai() {
877        let mut asm = Assembler::with_config(AssemblerConfig {
878            evm_version: EvmVersion::Berlin,
879            optimization: OptimizationMode::Gas,
880            ..AssemblerConfig::default()
881        });
882
883        asm.emit_push(U256::ZERO);
884        let result = asm.assemble();
885
886        assert_eq!(result.bytecode, vec![op::PUSH1, 0]);
887    }
888
889    #[test]
890    fn compact_push_respects_optimization_mode() {
891        let mut size_optimized = Assembler::with_config(AssemblerConfig {
892            evm_version: EvmVersion::Shanghai,
893            optimization: OptimizationMode::Size,
894            ..AssemblerConfig::default()
895        });
896        size_optimized.emit_push(U256::MAX);
897
898        let mut gas_optimized = Assembler::with_config(AssemblerConfig {
899            evm_version: EvmVersion::Shanghai,
900            optimization: OptimizationMode::Gas,
901            ..AssemblerConfig::default()
902        });
903        gas_optimized.emit_push(U256::MAX);
904
905        let mut unoptimized = Assembler::with_config(AssemblerConfig {
906            evm_version: EvmVersion::Shanghai,
907            optimization: OptimizationMode::None,
908            ..AssemblerConfig::default()
909        });
910        unoptimized.emit_push(U256::MAX);
911
912        let compact = vec![op::PUSH0, op::NOT];
913        assert_eq!(size_optimized.assemble().bytecode, compact);
914        assert_eq!(gas_optimized.assemble().bytecode, compact);
915
916        let mut expected = vec![op::PUSH32];
917        expected.extend(std::iter::repeat_n(0xff, 32));
918        assert_eq!(unoptimized.assemble().bytecode, expected);
919    }
920
921    #[test]
922    fn compact_push_uses_push1_zero_before_shanghai() {
923        let mut asm = Assembler::with_config(AssemblerConfig {
924            evm_version: EvmVersion::Berlin,
925            optimization: OptimizationMode::Size,
926            ..AssemblerConfig::default()
927        });
928
929        asm.emit_push(U256::MAX);
930        let result = asm.assemble();
931
932        assert_eq!(result.bytecode, vec![op::PUSH1, 0, op::NOT]);
933    }
934
935    #[test]
936    fn test_simple_assembly() {
937        let mut asm = Assembler::new();
938
939        asm.emit_push(U256::from(42));
940        asm.emit_push(U256::from(10));
941        asm.emit_op(op::ADD);
942        asm.emit_op(op::STOP);
943
944        let result = asm.assemble();
945
946        // PUSH1 42, PUSH1 10, ADD, STOP
947        assert_eq!(result.bytecode, vec![0x60, 42, 0x60, 10, 0x01, 0x00]);
948    }
949
950    #[test]
951    fn test_label_resolution() {
952        let mut asm = Assembler::new();
953
954        let loop_label = asm.new_label();
955        let end_label = asm.new_label();
956
957        asm.define_label(loop_label);
958        asm.emit_push(U256::from(1));
959        asm.emit_push_label(end_label);
960        asm.emit_op(op::JUMPI);
961        asm.emit_push_label(loop_label);
962        asm.emit_op(op::JUMP);
963
964        asm.define_label(end_label);
965        asm.emit_op(op::STOP);
966
967        let result = asm.assemble();
968
969        // Check labels were resolved
970        assert!(result.label_offsets.contains_key(&loop_label));
971        assert!(result.label_offsets.contains_key(&end_label));
972        assert_eq!(result.label_offsets[&loop_label], 0);
973    }
974
975    #[test]
976    fn cold_terminal_block_moves_after_hot_block() {
977        let mut asm = Assembler::with_config(AssemblerConfig {
978            evm_ir_layout_passes: true,
979            ..AssemblerConfig::default()
980        });
981        let cold = asm.new_label();
982        let hot = asm.new_label();
983
984        asm.emit_push_label(hot);
985        asm.emit_op(op::JUMP);
986        asm.mark_label_cold(cold);
987        asm.define_label(cold);
988        asm.emit_push(U256::ZERO);
989        asm.emit_push(U256::ZERO);
990        asm.emit_op(op::REVERT);
991        asm.define_label(hot);
992        asm.emit_op(op::STOP);
993
994        let result = asm.assemble();
995
996        assert_eq!(
997            result.bytecode,
998            vec![
999                op::PUSH1,
1000                3,
1001                op::JUMP,
1002                op::JUMPDEST,
1003                op::STOP,
1004                op::JUMPDEST,
1005                op::PUSH0,
1006                op::PUSH0,
1007                op::REVERT,
1008            ]
1009        );
1010    }
1011
1012    #[test]
1013    fn cold_terminal_block_keeps_fallthrough_position() {
1014        let mut asm = Assembler::with_config(AssemblerConfig {
1015            evm_ir_layout_passes: true,
1016            ..AssemblerConfig::default()
1017        });
1018        let cold = asm.new_label();
1019
1020        asm.emit_push(U256::ONE);
1021        asm.mark_label_cold(cold);
1022        asm.define_label(cold);
1023        asm.emit_push(U256::ZERO);
1024        asm.emit_push(U256::ZERO);
1025        asm.emit_op(op::REVERT);
1026
1027        let result = asm.assemble();
1028
1029        assert_eq!(
1030            result.bytecode,
1031            vec![op::PUSH1, 1, op::JUMPDEST, op::PUSH0, op::PUSH0, op::REVERT]
1032        );
1033    }
1034
1035    #[test]
1036    fn compact_full_word_all_ones_push() {
1037        let mut asm = size_optimized_assembler();
1038
1039        asm.emit_push(U256::MAX);
1040        asm.emit_op(op::STOP);
1041
1042        let result = asm.assemble();
1043
1044        assert_eq!(result.bytecode, vec![op::PUSH0, op::NOT, op::STOP]);
1045    }
1046
1047    #[test]
1048    fn compact_lower_all_ones_mask_push() {
1049        let mut asm = size_optimized_assembler();
1050        let mask = (U256::from(1) << 160) - U256::from(1);
1051
1052        asm.emit_push(mask);
1053        asm.emit_op(op::STOP);
1054
1055        let result = asm.assemble();
1056
1057        assert_eq!(result.bytecode, vec![op::PUSH0, op::NOT, 0x60, 96, op::SHR, op::STOP]);
1058    }
1059
1060    #[test]
1061    fn compact_not_small_push() {
1062        let mut asm = size_optimized_assembler();
1063
1064        asm.emit_push(!U256::from(31));
1065        asm.emit_op(op::STOP);
1066
1067        let result = asm.assemble();
1068
1069        assert_eq!(result.bytecode, vec![0x60, 31, op::NOT, op::STOP]);
1070    }
1071
1072    #[test]
1073    fn compact_not_byte_push() {
1074        let mut asm = size_optimized_assembler();
1075
1076        asm.emit_push(!U256::from(255));
1077        asm.emit_op(op::STOP);
1078
1079        let result = asm.assemble();
1080
1081        assert_eq!(result.bytecode, vec![0x60, 255, op::NOT, op::STOP]);
1082    }
1083
1084    #[test]
1085    fn compact_left_aligned_selector_push() {
1086        let mut asm = size_optimized_assembler();
1087        let selector = U256::from(0x35ea6a75u64) << 224;
1088
1089        asm.emit_push(selector);
1090        asm.emit_op(op::STOP);
1091
1092        let result = asm.assemble();
1093
1094        assert_eq!(
1095            result.bytecode,
1096            vec![0x63, 0x35, 0xea, 0x6a, 0x75, 0x60, 224, op::SHL, op::STOP]
1097        );
1098    }
1099
1100    #[test]
1101    fn compact_right_padded_text_push() {
1102        let mut asm = size_optimized_assembler();
1103        let text = U256::from_be_slice(b"Machine finished:");
1104        let value = text << ((32 - "Machine finished:".len()) * 8);
1105
1106        asm.emit_push(value);
1107        asm.emit_op(op::STOP);
1108
1109        let result = asm.assemble();
1110
1111        let mut expected = vec![0x70];
1112        expected.extend_from_slice(b"Machine finished:");
1113        expected.extend_from_slice(&[0x60, 120, op::SHL, op::STOP]);
1114        assert_eq!(result.bytecode, expected);
1115    }
1116}