Skip to main content

solar_codegen/backend/evm/
codegen.rs

1//! EVM bytecode generation from MIR.
2//!
3//! This module generates EVM bytecode from MIR using:
4//! - Liveness analysis to know when values die
5//! - Phi elimination to convert SSA to parallel copies
6//! - Stack scheduling to generate DUP/SWAP sequences
7//! - Two-pass assembly for label resolution
8
9use super::{
10    assembler::{Assembler, AssemblerConfig, DeferredConst, ImmutableRef, Label, op},
11    stack::{
12        MAX_STACK_ACCESS, ScheduledOp, SpillSlot, StackModel, StackOp, StackScheduler, TargetSlot,
13    },
14};
15use crate::{
16    IMMUTABLE_SCRATCH_BASE,
17    analysis::{
18        CallGraphInfo, CfgInfo, CopyDest, CopySource, Liveness, Loop, LoopAnalyzer, ParallelCopy,
19        PhiEliminator,
20    },
21    mir::{BlockId, Function, FunctionId, InstId, InstKind, MirType, Module, Terminator, ValueId},
22    pass::{AnalysisManager, LivenessAnalysis, PipelineOptions, run_default_pipeline_with_options},
23};
24use alloy_primitives::U256;
25use solar_config::{EvmVersion, OptimizationMode};
26use solar_data_structures::map::{FxHashMap, FxHashSet};
27use solar_interface::Session;
28
29// 0x00..0x7f follows Solidity's scratch/free-pointer/zero-slot convention, and
30// 0x80 is used as the static ABI return buffer. Keep the internal-call frame
31// pointer in a dedicated low word so frame loads use PUSH1 instead of PUSH2.
32const INTERNAL_FRAME_PTR_SLOT: u64 = 0xa0;
33const LOW_MEMORY_START: u64 = 0x80;
34const CONSTRUCTOR_FREE_MEMORY_START: u64 = 0x4000;
35const CONSTRUCTOR_SPILL_BASE: u64 = 0x1000;
36const LINEAR_SELECTOR_DISPATCH_THRESHOLD: usize = 64;
37const STACK_PHI_LAYOUT_LIMIT: usize = 8;
38
39/// Configuration for the EVM backend.
40#[derive(Clone, Copy, Debug, Default)]
41pub struct EvmCodegenConfig {
42    /// EVM version to target when selecting hardfork-gated opcodes.
43    pub evm_version: EvmVersion,
44    /// Optimization mode for MIR passes and bytecode assembly.
45    pub optimization: OptimizationMode,
46    /// Print MIR after each pass before bytecode generation.
47    pub mir_print_after_each: bool,
48    /// Run the experimental EVM IR `StackSchedule` pass in the assembler bridge.
49    ///
50    /// Off by default: the default bytecode path must stay byte-for-byte
51    /// unchanged. When enabled the bridge runs `EvmIrPass::StackSchedule` on the
52    /// operand-cleared block IR. On that already-stack-scheduled input the pass
53    /// is a verified near no-op in `StructuredAsmProgram::optimize_with_evm_ir`.
54    pub evm_ir_stack_schedule: bool,
55    /// Run EVM IR layout/code-size passes in the assembler bridge.
56    pub evm_ir_layout_passes: bool,
57}
58
59impl EvmCodegenConfig {
60    /// Creates backend configuration from a compiler session.
61    #[must_use]
62    pub fn from_session(sess: &Session) -> Self {
63        Self {
64            evm_version: sess.opts.evm_version,
65            optimization: sess.opts.optimization,
66            mir_print_after_each: sess.opts.unstable.mir_print_after_each,
67            // Keep the experimental EVM IR stack scheduler off in every default
68            // compilation path so produced bytecode is unchanged.
69            evm_ir_stack_schedule: false,
70            evm_ir_layout_passes: false,
71        }
72    }
73
74    fn assembler_config(self) -> AssemblerConfig {
75        AssemblerConfig {
76            evm_version: self.evm_version,
77            optimization: self.optimization,
78            evm_ir_stack_schedule: self.evm_ir_stack_schedule,
79            evm_ir_layout_passes: self.evm_ir_layout_passes,
80        }
81    }
82}
83
84impl From<&Session> for EvmCodegenConfig {
85    fn from(sess: &Session) -> Self {
86        Self::from_session(sess)
87    }
88}
89
90impl From<solar_sema::Gcx<'_>> for EvmCodegenConfig {
91    fn from(gcx: solar_sema::Gcx<'_>) -> Self {
92        Self::from_session(gcx.sess)
93    }
94}
95
96/// Describes the stack effect of an EVM instruction.
97/// This is used to keep the scheduler's stack model in sync with the actual EVM stack.
98#[derive(Clone, Copy, Debug)]
99struct StackEffect {
100    /// Number of values popped from the stack.
101    pops: usize,
102    /// Number of values pushed to the stack.
103    pushes: usize,
104}
105
106/// What value to track for a pushed stack entry.
107#[derive(Clone, Copy, Debug)]
108enum StackPush {
109    /// No value is pushed (pushes == 0).
110    #[allow(dead_code)]
111    None,
112    /// Push a tracked ValueId (pushes == 1).
113    Tracked(ValueId),
114    /// Push an unknown/untracked value (pushes == 1).
115    Unknown,
116}
117
118#[derive(Clone, Copy, Debug)]
119struct SelectorDispatchEntry {
120    selector: u32,
121    label: Label,
122}
123
124#[derive(Clone, Debug, Default)]
125struct StackPhiPlan {
126    entries: FxHashMap<BlockId, Vec<ValueId>>,
127    edges: FxHashMap<BlockId, StackPhiEdge>,
128    edge_sources: FxHashMap<BlockId, FxHashSet<ValueId>>,
129}
130
131#[derive(Clone, Debug)]
132struct StackPhiEdge {
133    sources: Vec<ValueId>,
134    results: Vec<ValueId>,
135}
136
137impl StackPhiPlan {
138    fn analyze(func: &Function) -> Self {
139        StackPhiPlanner::new(func).plan()
140    }
141}
142
143struct StackPhiPlanner<'a> {
144    func: &'a Function,
145    loops: Vec<Loop>,
146    header_results: FxHashMap<BlockId, Vec<ValueId>>,
147}
148
149impl<'a> StackPhiPlanner<'a> {
150    fn new(func: &'a Function) -> Self {
151        let mut loop_analyzer = LoopAnalyzer::new();
152        let loop_info = loop_analyzer.analyze(func);
153        let mut loops: Vec<_> = loop_info.all_loops().cloned().collect();
154        loops.sort_by_key(|loop_info| loop_info.header.index());
155
156        let mut planner = Self { func, loops, header_results: FxHashMap::default() };
157        planner.collect_header_results();
158        planner
159    }
160
161    fn plan(&self) -> StackPhiPlan {
162        let mut plan = StackPhiPlan::default();
163        for loop_info in &self.loops {
164            self.plan_loop(loop_info, &mut plan);
165        }
166        plan
167    }
168
169    fn collect_header_results(&mut self) {
170        for loop_info in &self.loops {
171            let block = &self.func.blocks[loop_info.header];
172            let phi_insts = self.leading_phi_insts(block);
173            if let Some(results) = self.phi_result_values(&phi_insts) {
174                self.header_results.insert(loop_info.header, results);
175            }
176        }
177    }
178
179    fn plan_loop(&self, loop_info: &Loop, plan: &mut StackPhiPlan) {
180        let Some(preheader) = loop_info.preheader else {
181            return;
182        };
183        let [latch] = loop_info.back_edges.as_slice() else {
184            return;
185        };
186        if !matches!(self.func.blocks[preheader].terminator, Some(Terminator::Jump(target)) if target == loop_info.header)
187            || !matches!(self.func.blocks[*latch].terminator, Some(Terminator::Jump(target)) if target == loop_info.header)
188        {
189            return;
190        }
191        if plan.edges.contains_key(&preheader) || plan.edges.contains_key(latch) {
192            return;
193        }
194
195        let block = &self.func.blocks[loop_info.header];
196        let phi_insts = self.leading_phi_insts(block);
197        if phi_insts.is_empty() || phi_insts.len() > STACK_PHI_LAYOUT_LIMIT {
198            return;
199        }
200
201        let Some(results) = self.phi_result_values(&phi_insts) else {
202            return;
203        };
204        if results.len() > STACK_PHI_LAYOUT_LIMIT {
205            return;
206        }
207
208        let carry_through = self.carry_through_values(loop_info);
209        if carry_through.len() + results.len() > STACK_PHI_LAYOUT_LIMIT {
210            return;
211        }
212
213        let mut entry = carry_through.clone();
214        entry.extend(results.iter().copied());
215
216        let predecessors = [preheader, *latch];
217        let mut edges = Vec::with_capacity(predecessors.len());
218        for pred in predecessors {
219            let Some(phi_sources) = self.phi_sources_for_pred(&phi_insts, pred) else {
220                return;
221            };
222            let mut sources = carry_through.clone();
223            sources.extend(phi_sources);
224            debug_assert_eq!(sources.len(), entry.len());
225            edges.push((pred, sources));
226        }
227
228        plan.entries.insert(loop_info.header, entry.clone());
229        for (pred, sources) in edges {
230            plan.edge_sources.insert(pred, sources.iter().copied().collect());
231            plan.edges.insert(pred, StackPhiEdge { sources, results: entry.clone() });
232        }
233    }
234
235    fn leading_phi_insts(&self, block: &crate::mir::BasicBlock) -> Vec<InstId> {
236        block
237            .instructions
238            .iter()
239            .copied()
240            .take_while(|&inst| matches!(self.func.instructions[inst].kind, InstKind::Phi(_)))
241            .collect()
242    }
243
244    fn carry_through_values(&self, loop_info: &Loop) -> Vec<ValueId> {
245        let mut carry_through = Vec::new();
246        for outer in &self.loops {
247            if outer.header == loop_info.header || !outer.blocks.contains(&loop_info.header) {
248                continue;
249            }
250            let Some(results) = self.header_results.get(&outer.header) else {
251                continue;
252            };
253            for &value in results {
254                if carry_through.contains(&value)
255                    || !self.value_used_in_blocks(&loop_info.blocks, value)
256                {
257                    continue;
258                }
259                carry_through.push(value);
260            }
261        }
262        carry_through
263    }
264
265    fn value_used_in_blocks(&self, blocks: &FxHashSet<BlockId>, value: ValueId) -> bool {
266        for &block_id in blocks {
267            let block = &self.func.blocks[block_id];
268            for &inst_id in &block.instructions {
269                if matches!(self.func.instructions[inst_id].kind, InstKind::Phi(_)) {
270                    continue;
271                }
272                if self.func.instructions[inst_id].kind.operands().contains(&value) {
273                    return true;
274                }
275            }
276            if block.terminator.as_ref().is_some_and(|term| term.operands().contains(&value)) {
277                return true;
278            }
279        }
280        false
281    }
282
283    fn phi_result_values(&self, phi_insts: &[InstId]) -> Option<Vec<ValueId>> {
284        phi_insts.iter().map(|&inst| self.func.inst_result_value(inst)).collect()
285    }
286
287    fn phi_sources_for_pred(&self, phi_insts: &[InstId], pred: BlockId) -> Option<Vec<ValueId>> {
288        phi_insts
289            .iter()
290            .map(|&inst| {
291                let InstKind::Phi(incoming) = &self.func.instructions[inst].kind else {
292                    return None;
293                };
294                incoming.iter().find_map(|&(block, value)| (block == pred).then_some(value))
295            })
296            .collect()
297    }
298}
299
300/// EVM code generator.
301pub struct EvmCodegen {
302    /// The assembler for bytecode generation.
303    asm: Assembler,
304    /// Stack scheduler.
305    scheduler: StackScheduler,
306    /// Block labels.
307    block_labels: FxHashMap<BlockId, Label>,
308    /// Function labels for direct internal calls.
309    function_labels: FxHashMap<FunctionId, Label>,
310    /// Per-function static local frame sizes for direct internal calls.
311    ///
312    /// Spill slots are allocated lazily during body emission, so the spill part
313    /// of each frame is recorded separately after the function has emitted.
314    function_static_frame_sizes: FxHashMap<FunctionId, u64>,
315    /// Exact per-function spill area sizes, in bytes, recorded after emission.
316    function_spill_sizes: FxHashMap<FunctionId, u64>,
317    /// Internal-call frame-size constants waiting for exact callee spill sizes.
318    pending_frame_size_consts: Vec<(DeferredConst, FunctionId, u64)>,
319    /// Callees whose internal-call frame can be deallocated after return.
320    restorable_internal_frames: FxHashSet<FunctionId>,
321    /// Copies to insert at block exits (from phi elimination).
322    block_copies: FxHashMap<BlockId, Vec<ParallelCopy>>,
323    /// Values carried by planned stack-resident phi edges, keyed by predecessor block.
324    stack_phi_sources: FxHashMap<BlockId, FxHashSet<ValueId>>,
325    /// Immutable `PUSH32` placeholders in the last assembled runtime code.
326    runtime_immutable_refs: Vec<ImmutableRef>,
327    /// Whether we're currently generating constructor code.
328    /// When true, LoadArg uses CODECOPY from the end of code instead of CALLDATALOAD.
329    in_constructor: bool,
330    /// Number of constructor parameters (used for CODECOPY offset calculation).
331    constructor_param_count: u32,
332    /// Whether we're emitting an internal function body.
333    in_internal_function: bool,
334    /// Optimization mode for MIR passes and bytecode assembly.
335    optimization: OptimizationMode,
336    /// Print MIR after each pass before bytecode generation.
337    mir_print_after_each: bool,
338}
339
340impl EvmCodegen {
341    /// Creates a new EVM code generator.
342    #[must_use]
343    pub fn new(config: impl Into<EvmCodegenConfig>) -> Self {
344        let config = config.into();
345        Self {
346            asm: Assembler::with_config(config.assembler_config()),
347            scheduler: StackScheduler::new(),
348            block_labels: FxHashMap::default(),
349            function_labels: FxHashMap::default(),
350            function_static_frame_sizes: FxHashMap::default(),
351            function_spill_sizes: FxHashMap::default(),
352            pending_frame_size_consts: Vec::new(),
353            restorable_internal_frames: FxHashSet::default(),
354            block_copies: FxHashMap::default(),
355            stack_phi_sources: FxHashMap::default(),
356            runtime_immutable_refs: Vec::new(),
357            in_constructor: false,
358            constructor_param_count: 0,
359            in_internal_function: false,
360            optimization: config.optimization,
361            mir_print_after_each: config.mir_print_after_each,
362        }
363    }
364
365    // ==================== Stack-Aware Emitter API ====================
366    //
367    // These helpers ensure that all EVM stack mutations are tracked by the scheduler.
368    // Any opcode that changes the EVM stack must be emitted through these methods
369    // to keep the scheduler's StackModel in sync with the actual EVM stack.
370
371    /// Emits a stack manipulation operation (DUP, SWAP, POP) and updates the scheduler.
372    fn emit_stack_op(&mut self, op: StackOp) {
373        self.asm.emit_op(op.opcode());
374        match op {
375            StackOp::Dup(n) => self.scheduler.stack.dup(n),
376            StackOp::Swap(n) => self.scheduler.stack.swap(n),
377            StackOp::Pop => {
378                self.scheduler.stack.pop();
379            }
380        }
381    }
382
383    /// Emits an opcode with known stack effects and updates the scheduler.
384    ///
385    /// This is the core method for stack-aware emission. After emitting the opcode:
386    /// - `effect.pops` values are removed from the scheduler's stack model
387    /// - Values are pushed according to `push`:
388    ///   - `StackPush::None`: no value pushed (effect.pushes must be 0)
389    ///   - `StackPush::Tracked(v)`: push a tracked ValueId (effect.pushes must be 1)
390    ///   - `StackPush::Unknown`: push an untracked value (effect.pushes must be 1)
391    fn emit_op_with_effect(&mut self, opcode: u8, effect: StackEffect, push: StackPush) {
392        #[cfg(debug_assertions)]
393        let before = self.scheduler.depth();
394
395        self.asm.emit_op(opcode);
396
397        // Pop consumed values
398        for _ in 0..effect.pops {
399            self.scheduler.stack.pop();
400        }
401
402        // Push produced values
403        match (effect.pushes, push) {
404            (0, StackPush::None) => {}
405            (1, StackPush::Tracked(v)) => self.scheduler.stack.push(v),
406            (1, StackPush::Unknown) => self.scheduler.stack.push_unknown(),
407            (n, _) if n > 1 => {
408                // Multi-push: push unknown values
409                for _ in 0..n {
410                    self.scheduler.stack.push_unknown();
411                }
412            }
413            _ => {}
414        }
415
416        #[cfg(debug_assertions)]
417        {
418            let expected = before + effect.pushes - effect.pops;
419            debug_assert_eq!(
420                self.scheduler.depth(),
421                expected,
422                "Stack model drift after opcode 0x{:02x}: expected depth {}, got {}",
423                opcode,
424                expected,
425                self.scheduler.depth()
426            );
427        }
428    }
429
430    /// Generates bytecode for a module (runtime code only).
431    /// Returns empty bytecode for interfaces (they have no implementation).
432    ///
433    /// This runs optimization passes (including DCE) on the module before codegen unless disabled.
434    pub fn generate_module(&mut self, module: &mut Module) -> Vec<u8> {
435        if module.is_interface {
436            return Vec::new();
437        }
438        self.run_optimization_passes(module);
439        // Immutable reads are `PUSH32` zero placeholders here; only a
440        // constructor run patches them with actual values.
441        self.generate_runtime_code(module)
442    }
443
444    /// Generates deployment bytecode for a module.
445    /// Returns (deployment_bytecode, runtime_bytecode).
446    /// Returns empty bytecodes for interfaces (they have no implementation).
447    ///
448    /// This runs optimization passes (including DCE) on the module before codegen unless disabled.
449    pub fn generate_deployment_bytecode(&mut self, module: &mut Module) -> (Vec<u8>, Vec<u8>) {
450        if module.is_interface {
451            return (Vec::new(), Vec::new());
452        }
453        self.run_optimization_passes(module);
454        // First generate the runtime code
455        let runtime_code = self.generate_runtime_code(module);
456        let runtime_len = runtime_code.len();
457        let immutable_refs = std::mem::take(&mut self.runtime_immutable_refs);
458
459        // The constructor copies the runtime code to memory and patches the
460        // immutable placeholders with the staged scratch words before
461        // returning. Copy to offset 0 unless that would overwrite the scratch
462        // words before the patch loop reads them.
463        let copy_base = if !immutable_refs.is_empty() && runtime_len as u64 > IMMUTABLE_SCRATCH_BASE
464        {
465            IMMUTABLE_SCRATCH_BASE + module.immutable_data_len() as u64
466        } else {
467            0
468        };
469
470        // Generate constructor initialization code (if any). Constructor arguments are appended
471        // after the generated deployment bytecode, so the constructor arg offset depends on the
472        // constructor code length. Iterate until the push widths stabilize.
473        let mut deploy_code_len = 0usize;
474        let mut constructor_arg_offset = runtime_len;
475        let mut constructor_code = self.generate_constructor_code(module, Some(runtime_len));
476        for _ in 0..8 {
477            let postlude = self.build_deployment_postlude(
478                deploy_code_len,
479                runtime_len,
480                copy_base,
481                &immutable_refs,
482            );
483            let next_deploy_code_len = constructor_code.len() + postlude.len();
484            let next_arg_offset = next_deploy_code_len + runtime_len;
485            if next_deploy_code_len == deploy_code_len && next_arg_offset == constructor_arg_offset
486            {
487                break;
488            }
489            deploy_code_len = next_deploy_code_len;
490            constructor_arg_offset = next_arg_offset;
491            constructor_code = self.generate_constructor_code(module, Some(constructor_arg_offset));
492        }
493
494        // Deploy code structure:
495        // [constructor_code]    ; run constructor (SSTOREs + immutable staging)
496        // PUSH<n> runtime_len   ; size to copy from creation code
497        // DUP1                  ; duplicate for the final RETURN size
498        // PUSH<n> offset        ; where runtime starts
499        // PUSH<n> copy_base     ; memory destination
500        // CODECOPY              ; copy runtime to memory
501        // [immutable patches]   ; patch staged words into the PUSH32 placeholders
502        // PUSH<n> copy_base     ; memory offset
503        // RETURN                ; return the runtime code
504        let postlude = self.build_deployment_postlude(
505            deploy_code_len,
506            runtime_len,
507            copy_base,
508            &immutable_refs,
509        );
510
511        // Build the deployment bytecode
512        let mut deploy_bytecode = Vec::new();
513
514        // Add constructor code first
515        deploy_bytecode.extend_from_slice(&constructor_code);
516        deploy_bytecode.extend_from_slice(&postlude);
517
518        // Append runtime code
519        deploy_bytecode.extend_from_slice(&runtime_code);
520
521        // The returned runtime artifact keeps the zero placeholders, like
522        // solc's `deployedBytecode` for contracts with immutables.
523        (deploy_bytecode, runtime_code)
524    }
525
526    fn build_deployment_postlude(
527        &mut self,
528        deploy_code_len: usize,
529        runtime_len: usize,
530        copy_base: u64,
531        immutable_refs: &[ImmutableRef],
532    ) -> Vec<u8> {
533        self.asm.clear();
534
535        // Copy runtime code from creation code to memory at `copy_base`.
536        self.asm.emit_push(U256::from(runtime_len as u64));
537        self.asm.emit_op(op::dup(1));
538        self.asm.emit_push(U256::from(deploy_code_len as u64));
539        self.asm.emit_push(U256::from(copy_base));
540        self.asm.emit_op(op::CODECOPY);
541
542        // Patch each `PUSH32` placeholder with its staged immutable word.
543        // The placeholder data starts one byte after the PUSH32 opcode.
544        for r in immutable_refs {
545            self.asm.emit_push(U256::from(IMMUTABLE_SCRATCH_BASE + u64::from(r.id)));
546            self.asm.emit_op(op::MLOAD);
547            self.asm.emit_push(U256::from(copy_base + r.code_offset as u64 + 1));
548            self.asm.emit_op(op::MSTORE);
549        }
550
551        // Return the patched runtime code; the DUP'd length is still on the stack.
552        self.asm.emit_push(U256::from(copy_base));
553        self.asm.emit_op(op::RETURN);
554        self.asm.assemble().bytecode
555    }
556
557    /// Generates constructor code that runs during deployment.
558    /// This includes state variable initializers.
559    ///
560    /// Constructor arguments are read from the end of the initcode using CODECOPY.
561    /// The args are ABI-encoded and appended after the deployment bytecode.
562    fn generate_constructor_code(
563        &mut self,
564        module: &Module,
565        constructor_arg_offset: Option<usize>,
566    ) -> Vec<u8> {
567        // Find constructor function if it exists
568        let constructor =
569            module.functions.iter_enumerated().find(|(_, f)| f.attributes.is_constructor);
570
571        if let Some((ctor_id, ctor)) = constructor {
572            // Generate constructor bytecode
573            self.asm.clear();
574
575            // Clear state and generate function body
576            self.block_labels.clear();
577            self.block_copies.clear();
578            self.function_labels.clear();
579            self.function_static_frame_sizes.clear();
580            self.function_spill_sizes.clear();
581            self.pending_frame_size_consts.clear();
582            self.restorable_internal_frames.clear();
583            self.stack_phi_sources.clear();
584
585            for (func_id, func) in module.functions.iter_enumerated() {
586                self.function_static_frame_sizes.insert(func_id, func.internal_frame_size);
587                if !func.params.iter().chain(&func.returns).any(|ty| matches!(ty, MirType::MemPtr))
588                {
589                    self.restorable_internal_frames.insert(func_id);
590                }
591            }
592
593            let call_graph = CallGraphInfo::new(module);
594            let internal_targets = call_graph.reachable_bodies_from(std::iter::once(ctor_id));
595            for &func_id in &internal_targets {
596                self.function_labels.insert(func_id, self.asm.new_label());
597            }
598
599            // Constructor spill slots are absolute addresses starting at
600            // 0x1000. Keep the historical 0x4000 heap start as a floor, but
601            // patch it upward after emission if the lazily allocated spill area
602            // needs more room.
603            let constructor_free_memory_start = self.asm.new_deferred_const();
604            self.asm.emit_push_deferred(constructor_free_memory_start);
605            self.asm.emit_push(U256::from(0x40));
606            self.asm.emit_op(op::MSTORE);
607
608            // Set constructor context for LoadArg handling
609            self.in_constructor = true;
610            self.constructor_param_count = ctor.params.len() as u32;
611
612            // If constructor has parameters, copy the full ABI-encoded argument blob to memory.
613            // Constructor args are appended after generated deployment bytecode, so the copy size
614            // is `CODESIZE - constructor_arg_offset`.
615            if !ctor.params.is_empty() {
616                let arg_offset = constructor_arg_offset.unwrap_or(0);
617                self.asm.emit_push(U256::from(arg_offset));
618                self.asm.emit_op(op::CODESIZE);
619                self.asm.emit_op(op::SUB); // size = CODESIZE - arg_offset
620                self.asm.emit_push(U256::from(arg_offset)); // code offset
621                self.asm.emit_push(U256::from(0x80)); // destOffset in memory
622                self.asm.emit_op(op::CODECOPY);
623            }
624
625            if !internal_targets.is_empty() {
626                let constructor_entry = self.asm.new_label();
627                self.asm.emit_push_label(constructor_entry);
628                self.asm.emit_op(op::JUMP);
629
630                for (func_id, func) in module.functions.iter_enumerated() {
631                    if !internal_targets.contains(&func_id) {
632                        continue;
633                    }
634                    let label = self.function_labels[&func_id];
635                    self.asm.define_label(label);
636                    self.in_internal_function = true;
637                    self.generate_function_body(func);
638                    self.in_internal_function = false;
639                    self.record_function_spill_size(func_id);
640                }
641
642                self.asm.define_label(constructor_entry);
643            }
644
645            // Generate the constructor body (which includes SSTORE for initializers)
646            self.generate_function_body(ctor);
647            let constructor_spill_size = self.record_function_spill_size(ctor_id);
648            self.asm.set_deferred_const(
649                constructor_free_memory_start,
650                U256::from(Self::constructor_free_memory_start(constructor_spill_size)),
651            );
652
653            self.resolve_pending_frame_size_consts(module);
654
655            // Reset constructor context
656            self.in_constructor = false;
657            self.constructor_param_count = 0;
658
659            let mut bytecode = self.asm.assemble().bytecode;
660
661            // Remove trailing STOP (0x00) if present - we want to fall through to CODECOPY/RETURN
662            if bytecode.last() == Some(&op::STOP) {
663                bytecode.pop();
664            }
665
666            bytecode
667        } else {
668            Vec::new()
669        }
670    }
671
672    /// Runs the canonical MIR optimization pipeline on the module.
673    fn run_optimization_passes(&mut self, module: &mut Module) {
674        if self.optimization == OptimizationMode::None {
675            return;
676        }
677        run_default_pipeline_with_options(
678            module,
679            PipelineOptions {
680                print_after_each: self.mir_print_after_each,
681                ..PipelineOptions::default()
682            },
683        );
684    }
685
686    /// Generates runtime bytecode for a module.
687    fn generate_runtime_code(&mut self, module: &Module) -> Vec<u8> {
688        self.asm.clear();
689        self.block_labels.clear();
690        self.function_labels.clear();
691        self.function_static_frame_sizes.clear();
692        self.function_spill_sizes.clear();
693        self.pending_frame_size_consts.clear();
694        self.restorable_internal_frames.clear();
695        self.block_copies.clear();
696        self.stack_phi_sources.clear();
697
698        if !module.functions.is_empty() {
699            // The dispatcher generates function bodies inline
700            self.generate_dispatcher(module);
701        }
702
703        let result = self.asm.assemble();
704        self.runtime_immutable_refs = result.immutable_refs;
705        result.bytecode
706    }
707
708    /// Generates the function dispatcher.
709    ///
710    /// The dispatcher logic is:
711    /// ```text
712    /// if calldatasize == 0:
713    ///     if has_receive: jump to receive
714    ///     elif has_fallback: jump to fallback
715    ///     else: revert
716    /// else:
717    ///     match selector...
718    ///     if no match and has_fallback: jump to fallback
719    ///     else: revert
720    /// ```
721    fn generate_dispatcher(&mut self, module: &Module) {
722        // Find executable receive and fallback functions. Interface/abstract declarations can
723        // have ABI entries but no MIR body, so they must not participate in runtime dispatch.
724        let receive_idx =
725            module.functions.iter().position(|f| f.attributes.is_receive && Self::has_body(f));
726        let fallback_idx =
727            module.functions.iter().position(|f| f.attributes.is_fallback && Self::has_body(f));
728
729        let call_graph = CallGraphInfo::new(module);
730        let internal_targets = call_graph.reachable_bodies_from(
731            module
732                .functions
733                .iter_enumerated()
734                .filter_map(|(func_id, func)| Self::is_external_entry(func).then_some(func_id)),
735        );
736
737        for (func_id, func) in module.functions.iter_enumerated() {
738            self.function_static_frame_sizes.insert(func_id, func.internal_frame_size);
739            if !func.params.iter().chain(&func.returns).any(|ty| matches!(ty, MirType::MemPtr)) {
740                self.restorable_internal_frames.insert(func_id);
741            }
742        }
743
744        // Create labels for externally reachable runtime entry points and internal-call targets.
745        let mut func_labels: Vec<Option<Label>> = Vec::new();
746        for (func_id, func) in module.functions.iter_enumerated() {
747            let external = Self::is_external_entry(func);
748            let needs_body =
749                external || (Self::has_body(func) && internal_targets.contains(&func_id));
750            let label = needs_body.then(|| self.asm.new_label());
751            if let Some(label) = label {
752                self.function_labels.insert(func_id, label);
753            }
754            func_labels.push(label);
755        }
756        let revert_label = self.asm.new_label();
757        self.asm.mark_label_cold(revert_label);
758        let has_calldata_label = self.asm.new_label();
759        let all_external_entries_reject_value =
760            module.functions.iter().any(Self::is_external_entry)
761                && module
762                    .functions
763                    .iter()
764                    .filter(|func| Self::is_external_entry(func))
765                    .all(Self::rejects_callvalue);
766
767        if all_external_entries_reject_value {
768            self.emit_callvalue_check(revert_label);
769        }
770
771        // Check if calldatasize == 0
772        self.asm.emit_op(op::CALLDATASIZE);
773        self.asm.emit_push_label(has_calldata_label);
774        self.asm.emit_op(op::JUMPI);
775
776        // calldatasize == 0: Handle receive/fallback
777        // Solidity semantics: if receive exists, call it; else if fallback exists, call it; else
778        // revert
779        if let Some(recv_idx) = receive_idx {
780            self.asm.emit_push_label(func_labels[recv_idx].expect("receive label missing"));
781            self.asm.emit_op(op::JUMP);
782        } else if let Some(fb_idx) = fallback_idx {
783            self.asm.emit_push_label(func_labels[fb_idx].expect("fallback label missing"));
784            self.asm.emit_op(op::JUMP);
785        } else {
786            self.asm.emit_push_label(revert_label);
787            self.asm.emit_op(op::JUMP);
788        }
789
790        // calldatasize > 0: Load selector and match
791        self.asm.define_label(has_calldata_label);
792
793        // Load selector from calldata
794        self.asm.emit_push(U256::ZERO);
795        self.asm.emit_op(op::CALLDATALOAD);
796        self.asm.emit_push(U256::from(0xe0));
797        self.asm.emit_op(op::SHR);
798
799        let mut selectors: Vec<_> = module
800            .functions
801            .iter()
802            .enumerate()
803            .filter_map(|(i, func)| {
804                if !Self::is_external_entry(func) {
805                    return None;
806                }
807                let selector = func.selector?;
808                Some(SelectorDispatchEntry {
809                    selector: u32::from_be_bytes(selector),
810                    label: func_labels[i].expect("selector label missing"),
811                })
812            })
813            .collect();
814        selectors.sort_by_key(|entry| entry.selector);
815
816        let fallback_label =
817            fallback_idx.map(|idx| func_labels[idx].expect("fallback label missing"));
818        self.emit_selector_dispatch(&selectors, fallback_label, revert_label);
819
820        // Define external function entry points.
821        for (func_id, func) in module.functions.iter_enumerated() {
822            if !Self::is_external_entry(func) {
823                continue;
824            }
825            let Some(label) = func_labels[func_id.index()] else { continue };
826            self.asm.define_label(label);
827
828            // Pop the selector for regular functions (receive/fallback don't have it on stack)
829            if func.selector.is_some() {
830                self.asm.emit_op(op::POP);
831            }
832
833            if !all_external_entries_reject_value {
834                self.emit_payable_check(func, revert_label);
835            }
836
837            let free_memory_start = self.emit_external_free_memory_start();
838
839            // Generate function body
840            self.in_internal_function = false;
841            self.generate_function_body(func);
842
843            let spill_size = self.record_function_spill_size(func_id);
844            self.asm.set_deferred_const(
845                free_memory_start,
846                U256::from(Self::external_spill_base(func) + spill_size),
847            );
848        }
849
850        // Define internal-call targets once. Calls jump here and return through the frame's
851        // saved return address.
852        for (func_id, func) in module.functions.iter_enumerated() {
853            if Self::is_external_entry(func) || !Self::has_body(func) {
854                continue;
855            }
856            let Some(label) = func_labels[func_id.index()] else { continue };
857            self.asm.define_label(label);
858            self.in_internal_function = true;
859            self.generate_function_body(func);
860            self.in_internal_function = false;
861            self.record_function_spill_size(func_id);
862        }
863
864        // Revert label
865        self.asm.define_label(revert_label);
866        self.asm.emit_push(U256::ZERO);
867        self.asm.emit_push(U256::ZERO);
868        self.asm.emit_op(op::REVERT);
869
870        self.resolve_pending_frame_size_consts(module);
871    }
872
873    /// Records the exact spill area size of the function body that just emitted.
874    fn record_function_spill_size(&mut self, func_id: FunctionId) -> u64 {
875        let spill_size = u64::from(self.scheduler.spills.spill_area_size());
876        self.function_spill_sizes.insert(func_id, spill_size);
877        spill_size
878    }
879
880    /// Resolves all pending internal-call frame-size constants.
881    ///
882    /// The normal runtime path records every emitted callee's exact spill size
883    /// first. The conservative fallback covers unusual paths where a body was
884    /// not emitted by this assembler.
885    fn resolve_pending_frame_size_consts(&mut self, module: &Module) {
886        for (id, callee, static_size) in std::mem::take(&mut self.pending_frame_size_consts) {
887            let spill_size =
888                self.function_spill_sizes.get(&callee).copied().unwrap_or_else(|| {
889                    Self::conservative_spill_frame_size(&module.functions[callee])
890                });
891            self.asm.set_deferred_const(id, U256::from(static_size + spill_size));
892        }
893    }
894
895    fn is_external_entry(func: &Function) -> bool {
896        Self::has_body(func)
897            && (func.selector.is_some()
898                || func.attributes.is_receive
899                || func.attributes.is_fallback)
900    }
901
902    fn has_body(func: &Function) -> bool {
903        !func.attributes.is_constructor && !func.blocks.is_empty()
904    }
905
906    fn emit_selector_dispatch(
907        &mut self,
908        selectors: &[SelectorDispatchEntry],
909        fallback_label: Option<Label>,
910        revert_label: Label,
911    ) {
912        if selectors.len() <= LINEAR_SELECTOR_DISPATCH_THRESHOLD {
913            self.emit_linear_selector_dispatch(selectors, fallback_label, revert_label);
914        } else {
915            self.emit_binary_selector_dispatch(selectors, fallback_label, revert_label);
916        }
917    }
918
919    fn emit_linear_selector_dispatch(
920        &mut self,
921        selectors: &[SelectorDispatchEntry],
922        fallback_label: Option<Label>,
923        revert_label: Label,
924    ) {
925        for entry in selectors {
926            self.emit_selector_eq_jump(*entry);
927        }
928        self.emit_selector_dispatch_miss(fallback_label, revert_label);
929    }
930
931    fn emit_binary_selector_dispatch(
932        &mut self,
933        selectors: &[SelectorDispatchEntry],
934        fallback_label: Option<Label>,
935        revert_label: Label,
936    ) {
937        if selectors.len() <= LINEAR_SELECTOR_DISPATCH_THRESHOLD {
938            self.emit_linear_selector_dispatch(selectors, fallback_label, revert_label);
939            return;
940        }
941
942        let mid = selectors.len() / 2;
943        let left_label = self.asm.new_label();
944
945        // Stack has the selector. With the pivot pushed on top, GT checks
946        // `pivot > selector`, so jump left when selector < pivot.
947        self.asm.emit_op(op::dup(1));
948        self.asm.emit_push(U256::from(selectors[mid].selector));
949        self.asm.emit_op(op::GT);
950        self.asm.emit_push_label(left_label);
951        self.asm.emit_op(op::JUMPI);
952
953        self.emit_binary_selector_dispatch(&selectors[mid..], fallback_label, revert_label);
954
955        self.asm.define_label(left_label);
956        self.emit_binary_selector_dispatch(&selectors[..mid], fallback_label, revert_label);
957    }
958
959    fn emit_selector_eq_jump(&mut self, entry: SelectorDispatchEntry) {
960        self.asm.emit_op(op::dup(1));
961        self.asm.emit_push(U256::from(entry.selector));
962        self.asm.emit_op(op::EQ);
963        self.asm.emit_push_label(entry.label);
964        self.asm.emit_op(op::JUMPI);
965    }
966
967    fn emit_selector_dispatch_miss(&mut self, fallback_label: Option<Label>, revert_label: Label) {
968        if let Some(fallback_label) = fallback_label {
969            self.asm.emit_op(op::POP);
970            self.asm.emit_push_label(fallback_label);
971            self.asm.emit_op(op::JUMP);
972        } else {
973            self.asm.emit_push_label(revert_label);
974            self.asm.emit_op(op::JUMP);
975        }
976    }
977
978    /// Emits a payable check for non-payable functions.
979    /// Non-payable, view, and pure functions revert if called with value.
980    fn emit_payable_check(&mut self, func: &Function, revert_label: Label) {
981        if Self::rejects_callvalue(func) {
982            self.emit_callvalue_check(revert_label);
983        }
984    }
985
986    fn rejects_callvalue(func: &Function) -> bool {
987        use solar_sema::hir::StateMutability;
988
989        matches!(
990            func.attributes.state_mutability,
991            StateMutability::NonPayable | StateMutability::View | StateMutability::Pure
992        )
993    }
994
995    fn emit_callvalue_check(&mut self, revert_label: Label) {
996        self.asm.emit_op(op::CALLVALUE);
997        self.asm.emit_push_label(revert_label);
998        self.asm.emit_op(op::JUMPI);
999    }
1000
1001    /// Generates bytecode for a function.
1002    #[allow(dead_code)]
1003    fn generate_function(&mut self, func: &Function) {
1004        self.generate_function_body(func);
1005    }
1006
1007    /// Generates the body of a function.
1008    fn generate_function_body(&mut self, func: &Function) {
1009        // Run analysis pipeline via the pass manager.
1010        // Currently just liveness; future analyses (dominance, loops, etc.)
1011        // can be added here and queried from the same AnalysisManager.
1012        let mut am = AnalysisManager::new();
1013        let liveness: &Liveness = am.get_or_compute(&LivenessAnalysis, func);
1014
1015        // Eliminate phis
1016        let phi_result = PhiEliminator::analyze(func);
1017        for (block_id, copies) in phi_result.block_copies {
1018            self.block_copies.insert(block_id, copies.copies);
1019        }
1020        let stack_phi_plan = StackPhiPlan::analyze(func);
1021        self.stack_phi_sources = stack_phi_plan.edge_sources.clone();
1022
1023        // Reset scheduler
1024        self.scheduler = StackScheduler::new();
1025
1026        self.preallocate_cross_block_spills(func, liveness);
1027
1028        // Create labels for each block
1029        self.block_labels.clear();
1030        for block_id in func.blocks.indices() {
1031            let label = self.asm.new_label();
1032            if Self::block_is_cold(func, block_id) {
1033                self.asm.mark_label_cold(label);
1034            }
1035            self.block_labels.insert(block_id, label);
1036        }
1037
1038        // Generate each block.
1039        let block_order = Self::block_layout_order(func);
1040        let block_pos: FxHashMap<BlockId, usize> =
1041            block_order.iter().enumerate().map(|(pos, &b)| (b, pos)).collect();
1042        // Stack layout a block must start with when it is reached by a stack-
1043        // preserving jump from its single predecessor (recorded by that
1044        // predecessor, restored here).
1045        let mut block_entry_stacks: FxHashMap<BlockId, StackModel> = FxHashMap::default();
1046        let mut preserved_fallthrough: Option<BlockId> = None;
1047        for (pos, &block_id) in block_order.iter().enumerate() {
1048            let block = &func.blocks[block_id];
1049            let fallthrough = block_order.get(pos + 1).copied();
1050            let entered_by_preserved_fallthrough = preserved_fallthrough == Some(block_id);
1051            preserved_fallthrough = None;
1052
1053            let label = self.block_labels[&block_id];
1054            if !entered_by_preserved_fallthrough
1055                && (block_id != func.entry_block || !block.predecessors.is_empty())
1056            {
1057                self.asm.define_label(label);
1058            }
1059
1060            // Reset stack at block entry unless the block is reached with a
1061            // known live stack: a physical fallthrough carries the scheduler's
1062            // stack directly, and a stack-preserving jump from a single
1063            // predecessor restores the recorded layout. All other cross-block
1064            // values live in spill slots.
1065            if !entered_by_preserved_fallthrough {
1066                if let Some(entry_stack) = block_entry_stacks.remove(&block_id) {
1067                    self.scheduler.stack = entry_stack;
1068                    // Live-ins not on the carried stack still arrive in memory.
1069                    self.mark_live_in_spills(func, liveness, block_id);
1070                } else if let Some(entry) = stack_phi_plan.entries.get(&block_id) {
1071                    self.set_stack_to_values(entry);
1072                    self.mark_live_in_spills(func, liveness, block_id);
1073                } else {
1074                    self.scheduler.clear_stack();
1075                    self.mark_live_in_spills(func, liveness, block_id);
1076                }
1077            }
1078
1079            // Generate instructions
1080            for (inst_idx, &inst_id) in block.instructions.iter().enumerate() {
1081                let inst = &func.instructions[inst_id];
1082
1083                // Skip phi instructions (they're handled by copies)
1084                if matches!(inst.kind, InstKind::Phi(_)) {
1085                    continue;
1086                }
1087
1088                // Find the value ID that corresponds to this instruction (if any)
1089                let result_value = func.inst_result_value(inst_id);
1090
1091                // Generate the instruction
1092                self.generate_inst(func, &inst.kind, liveness, block_id, inst_idx, result_value);
1093                if let Some(result) = result_value {
1094                    self.spill_reserved_result_if_live(func, liveness, block_id, inst_idx, result);
1095                }
1096            }
1097
1098            let stack_phi_preserved = stack_phi_plan.edges.get(&block_id).is_some_and(|edge| {
1099                if !self.can_prepare_stack_phi_edge(func, edge) {
1100                    return false;
1101                }
1102                self.spill_live_out_values_except(func, liveness, block_id, &edge.sources);
1103                self.pop_stack_values_not_needed_by(&edge.sources);
1104                self.try_emit_stack_phi_edge(func, edge)
1105            });
1106
1107            // Insert phi copies before terminator. If the edge was materialized
1108            // as a stack-resident phi layout, the copies for this unconditional
1109            // predecessor are represented by the edge stack itself.
1110            if stack_phi_preserved {
1111                self.block_copies.remove(&block_id);
1112            } else if let Some(copies) = self.block_copies.remove(&block_id) {
1113                let mut temps = FxHashMap::default();
1114                for copy in &copies {
1115                    self.generate_copy(func, copy, &mut temps);
1116                }
1117            }
1118
1119            let preserve_stack_to_fallthrough =
1120                self.can_preserve_stack_fallthrough(func, block_id, fallthrough);
1121
1122            // A jump to a single-predecessor target that is emitted later can
1123            // keep its live stack instead of spilling: the target has exactly
1124            // one entry stack (this block's exit), so it can be restored there.
1125            let preserve_jump_target = (!preserve_stack_to_fallthrough)
1126                .then(|| self.single_pred_jump_target(func, block_id, fallthrough))
1127                .flatten()
1128                .filter(|target| block_pos.get(target).copied() > Some(pos));
1129
1130            // A conditional branch whose other arm is a cold revert can carry
1131            // its single freshly-computed live-out on the stack into the hot
1132            // arm, which restores it as its recorded entry layout.
1133            let preserve_branch_targets =
1134                if !preserve_stack_to_fallthrough && preserve_jump_target.is_none() {
1135                    self.branch_preserve_targets(func, liveness, block_id, pos, &block_pos)
1136                } else {
1137                    Vec::new()
1138                };
1139
1140            let preserve_stack = preserve_stack_to_fallthrough
1141                || preserve_jump_target.is_some()
1142                || !preserve_branch_targets.is_empty()
1143                || stack_phi_preserved;
1144
1145            // Spill all live-out values before the terminator so they can be reloaded in successor
1146            // blocks. For a preserved edge, keep stack values live instead.
1147            if !preserve_stack {
1148                self.spill_live_out_values(func, liveness, block_id);
1149            }
1150
1151            // Generate terminator
1152            if let Some(term) = &block.terminator {
1153                self.generate_terminator(func, term, fallthrough, preserve_stack);
1154            }
1155            if preserve_stack_to_fallthrough {
1156                preserved_fallthrough = fallthrough;
1157            } else if let Some(target) = preserve_jump_target {
1158                block_entry_stacks.insert(target, self.scheduler.stack.clone());
1159            }
1160            for target in preserve_branch_targets {
1161                block_entry_stacks.insert(target, self.scheduler.stack.clone());
1162            }
1163        }
1164    }
1165
1166    /// Returns the target of a stack-preservable jump: the block ends in
1167    /// `Jump(T)` to a non-fallthrough, single-predecessor block with no phis
1168    /// (whose copies would otherwise interfere with the carried layout).
1169    fn single_pred_jump_target(
1170        &self,
1171        func: &Function,
1172        block_id: BlockId,
1173        fallthrough: Option<BlockId>,
1174    ) -> Option<BlockId> {
1175        let Some(Terminator::Jump(target)) = func.blocks[block_id].terminator.as_ref() else {
1176            return None;
1177        };
1178        if Some(*target) == fallthrough
1179            || func.blocks[*target].predecessors.as_slice() != [block_id]
1180        {
1181            return None;
1182        }
1183        let has_phi = func.blocks[*target]
1184            .instructions
1185            .iter()
1186            .any(|&inst| matches!(func.instructions[inst].kind, InstKind::Phi(_)));
1187        (!has_phi).then_some(*target)
1188    }
1189
1190    /// Returns branch successors that can receive the current stack layout.
1191    ///
1192    /// This handles loop headers after stack-resident phi planning: the header
1193    /// computes the branch condition while the carried phi values remain below
1194    /// it. If both successors are private, later blocks, we can leave those
1195    /// values on the stack for both edges instead of spilling them before every
1196    /// loop condition.
1197    fn branch_preserve_targets(
1198        &self,
1199        func: &Function,
1200        liveness: &Liveness,
1201        block_id: BlockId,
1202        pos: usize,
1203        block_pos: &FxHashMap<BlockId, usize>,
1204    ) -> Vec<BlockId> {
1205        let Some(Terminator::Branch { condition, then_block, else_block }) =
1206            func.blocks[block_id].terminator.as_ref()
1207        else {
1208            return Vec::new();
1209        };
1210
1211        if self.scheduler.stack.depth() <= 1 || self.scheduler.stack.top() != Some(*condition) {
1212            return Vec::new();
1213        }
1214
1215        let Some(carried) = self
1216            .scheduler
1217            .stack
1218            .iter()
1219            .skip(1)
1220            .map(|slot| {
1221                let value = slot?;
1222                liveness.live_out(block_id).contains(value).then_some(value)
1223            })
1224            .collect::<Option<Vec<_>>>()
1225        else {
1226            return Vec::new();
1227        };
1228        if carried.len() > STACK_PHI_LAYOUT_LIMIT {
1229            return Vec::new();
1230        }
1231
1232        let targets = [*then_block, *else_block];
1233        let mut live_in_any_target = FxHashSet::default();
1234        for target in targets {
1235            live_in_any_target.extend(liveness.live_in(target).iter());
1236        }
1237        if carried.iter().any(|value| !live_in_any_target.contains(value)) {
1238            return Vec::new();
1239        }
1240
1241        for target in targets {
1242            if target == block_id
1243                || func.blocks[target].predecessors.as_slice() != [block_id]
1244                || block_pos.get(&target).copied() <= Some(pos)
1245                || func.blocks[target]
1246                    .instructions
1247                    .iter()
1248                    .any(|&inst| matches!(func.instructions[inst].kind, InstKind::Phi(_)))
1249            {
1250                return Vec::new();
1251            }
1252        }
1253
1254        targets.into()
1255    }
1256
1257    /// Returns true when a block is statically cold: it only aborts execution
1258    /// (revert or invalid), as panic and revert-string blocks do.
1259    fn block_is_cold(func: &Function, block_id: BlockId) -> bool {
1260        matches!(
1261            func.blocks[block_id].terminator,
1262            Some(Terminator::Revert { .. } | Terminator::Invalid)
1263        )
1264    }
1265
1266    fn block_layout_order(func: &Function) -> Vec<BlockId> {
1267        let cfg = CfgInfo::new(func);
1268        let reachable = cfg.reachable();
1269        let mut order = Vec::with_capacity(func.blocks.len());
1270        let mut placed = FxHashSet::default();
1271
1272        Self::append_layout_chain(func, func.entry_block, reachable, &mut placed, &mut order);
1273        for block_id in func.blocks.indices() {
1274            if reachable.contains(&block_id) {
1275                Self::append_layout_chain(func, block_id, reachable, &mut placed, &mut order);
1276            }
1277        }
1278
1279        order
1280    }
1281
1282    fn append_layout_chain(
1283        func: &Function,
1284        mut block_id: BlockId,
1285        reachable: &FxHashSet<BlockId>,
1286        placed: &mut FxHashSet<BlockId>,
1287        order: &mut Vec<BlockId>,
1288    ) {
1289        loop {
1290            if !reachable.contains(&block_id) || !placed.insert(block_id) {
1291                return;
1292            }
1293            order.push(block_id);
1294
1295            let Some(Terminator::Jump(target)) = func.blocks[block_id].terminator.as_ref() else {
1296                return;
1297            };
1298            if placed.contains(target) || func.blocks[*target].predecessors.as_slice() != [block_id]
1299            {
1300                return;
1301            }
1302
1303            block_id = *target;
1304        }
1305    }
1306
1307    fn set_stack_to_values(&mut self, values: &[ValueId]) {
1308        self.scheduler.stack.clear();
1309        for &value in values.iter().rev() {
1310            self.scheduler.stack.push(value);
1311        }
1312    }
1313
1314    fn try_emit_stack_phi_edge(&mut self, func: &Function, edge: &StackPhiEdge) -> bool {
1315        if edge.sources.len() != edge.results.len()
1316            || edge.sources.is_empty()
1317            || edge.sources.len() > STACK_PHI_LAYOUT_LIMIT
1318        {
1319            return false;
1320        }
1321        if !self.stack_contains_only_phi_sources(&edge.sources) {
1322            return false;
1323        }
1324
1325        for &source in Self::missing_stack_phi_sources(&self.scheduler.stack, &edge.sources).iter()
1326        {
1327            if !self.scheduler.can_emit_value(source, func) {
1328                return false;
1329            }
1330            self.emit_operand(func, source);
1331        }
1332        if !self.stack_contains_only_phi_sources(&edge.sources) {
1333            return false;
1334        }
1335
1336        let target: Vec<_> = edge.sources.iter().copied().map(TargetSlot::Value).collect();
1337        let shuffle = self.scheduler.shuffle_to_layout(&target);
1338        for op in shuffle.ops {
1339            self.asm.emit_op(op.opcode());
1340        }
1341
1342        if self.scheduler.depth() != edge.sources.len() {
1343            return false;
1344        }
1345        self.set_stack_to_values(&edge.results);
1346        true
1347    }
1348
1349    fn can_prepare_stack_phi_edge(&self, func: &Function, edge: &StackPhiEdge) -> bool {
1350        if edge.sources.len() != edge.results.len()
1351            || edge.sources.is_empty()
1352            || edge.sources.len() > STACK_PHI_LAYOUT_LIMIT
1353        {
1354            return false;
1355        }
1356
1357        let present =
1358            Self::stack_phi_source_counts_after_trim(&self.scheduler.stack, &edge.sources);
1359        if present.len() > STACK_PHI_LAYOUT_LIMIT {
1360            return false;
1361        }
1362
1363        let mut seen = Self::value_counts(present);
1364        for &source in &edge.sources {
1365            if let Some(count) = seen.get_mut(&source)
1366                && *count > 0
1367            {
1368                *count -= 1;
1369                continue;
1370            }
1371            if !self.scheduler.can_emit_value(source, func) {
1372                return false;
1373            }
1374        }
1375        true
1376    }
1377
1378    fn stack_phi_source_counts_after_trim(stack: &StackModel, sources: &[ValueId]) -> Vec<ValueId> {
1379        let mut remaining = Self::value_counts(sources.iter().copied());
1380        let mut kept = Vec::new();
1381        for value in stack.iter().flatten() {
1382            if let Some(count) = remaining.get_mut(&value)
1383                && *count > 0
1384            {
1385                *count -= 1;
1386                kept.push(value);
1387            }
1388        }
1389        kept
1390    }
1391
1392    fn stack_contains_only_phi_sources(&self, sources: &[ValueId]) -> bool {
1393        let mut remaining = Self::value_counts(sources.iter().copied());
1394        for slot in self.scheduler.stack.iter() {
1395            let Some(value) = slot else {
1396                return false;
1397            };
1398            let Some(count) = remaining.get_mut(&value) else {
1399                return false;
1400            };
1401            if *count == 0 {
1402                return false;
1403            }
1404            *count -= 1;
1405        }
1406        true
1407    }
1408
1409    fn missing_stack_phi_sources(stack: &StackModel, sources: &[ValueId]) -> Vec<ValueId> {
1410        let mut needed = Self::value_counts(sources.iter().copied());
1411        for value in stack.iter().flatten() {
1412            if let Some(count) = needed.get_mut(&value)
1413                && *count > 0
1414            {
1415                *count -= 1;
1416            }
1417        }
1418
1419        let mut missing = Vec::new();
1420        for &source in sources {
1421            if let Some(count) = needed.get_mut(&source)
1422                && *count > 0
1423            {
1424                missing.push(source);
1425                *count -= 1;
1426            }
1427        }
1428        missing
1429    }
1430
1431    fn value_counts(values: impl IntoIterator<Item = ValueId>) -> FxHashMap<ValueId, usize> {
1432        let mut counts = FxHashMap::default();
1433        for value in values {
1434            *counts.entry(value).or_insert(0) += 1;
1435        }
1436        counts
1437    }
1438
1439    fn can_preserve_stack_fallthrough(
1440        &self,
1441        func: &Function,
1442        block_id: BlockId,
1443        fallthrough: Option<BlockId>,
1444    ) -> bool {
1445        let Some(Terminator::Jump(target)) = func.blocks[block_id].terminator.as_ref() else {
1446            return false;
1447        };
1448        if Some(*target) != fallthrough {
1449            return false;
1450        }
1451
1452        // This block is the target's only predecessor, so no non-fallthrough edge can observe or
1453        // depend on a JUMPDEST at the target label.
1454        func.blocks[*target].predecessors.as_slice() == [block_id]
1455    }
1456
1457    fn is_stack_phi_source(&self, block: BlockId, value: ValueId) -> bool {
1458        self.stack_phi_sources.get(&block).is_some_and(|sources| sources.contains(&value))
1459    }
1460
1461    /// Preallocates stable spill slots for values that may cross block boundaries.
1462    ///
1463    /// Blocks are emitted in layout order, not necessarily dominance order, so a block can be
1464    /// emitted before the predecessor that stores one of its live-in values. Reserving the slot up
1465    /// front lets the later load use a stable memory location; stores still happen only when the
1466    /// value is actually available on the stack.
1467    fn preallocate_cross_block_spills(&mut self, func: &Function, liveness: &Liveness) {
1468        for val in Self::cross_block_spill_values(func, liveness) {
1469            self.scheduler.spills.allocate(val);
1470        }
1471    }
1472
1473    fn cross_block_spill_values(func: &Function, liveness: &Liveness) -> FxHashSet<ValueId> {
1474        let mut values = FxHashSet::default();
1475        for block_id in func.blocks.indices() {
1476            for val in liveness.live_in(block_id).iter().chain(liveness.live_out(block_id).iter()) {
1477                if matches!(func.value(val), crate::mir::Value::Inst(_)) {
1478                    values.insert(val);
1479                }
1480            }
1481            for &inst_id in &func.blocks[block_id].instructions {
1482                if matches!(func.instructions[inst_id].kind, InstKind::Phi(_))
1483                    && let Some(val) = func.inst_result_value(inst_id)
1484                {
1485                    values.insert(val);
1486                }
1487            }
1488        }
1489        values
1490    }
1491
1492    /// Spills all live-out values that are currently on the stack to memory.
1493    /// This ensures values that need to be accessed in successor blocks can be reloaded.
1494    fn spill_live_out_values(&mut self, func: &Function, liveness: &Liveness, block_id: BlockId) {
1495        let live_out = liveness.live_out(block_id);
1496
1497        for val in live_out.iter() {
1498            self.spill_value_if_needed(func, val);
1499        }
1500    }
1501
1502    fn spill_live_out_values_except(
1503        &mut self,
1504        func: &Function,
1505        liveness: &Liveness,
1506        block_id: BlockId,
1507        exempt: &[ValueId],
1508    ) {
1509        let exempt: FxHashSet<_> = exempt.iter().copied().collect();
1510        for val in liveness.live_out(block_id).iter() {
1511            if !exempt.contains(&val) {
1512                self.spill_value_if_needed(func, val);
1513            }
1514        }
1515    }
1516
1517    fn pop_stack_values_not_needed_by(&mut self, needed: &[ValueId]) {
1518        while let Some(depth) = self.first_stack_value_not_needed_by(needed) {
1519            if depth > 0 {
1520                self.emit_stack_op(StackOp::Swap(depth as u8));
1521            }
1522            self.emit_stack_op(StackOp::Pop);
1523        }
1524    }
1525
1526    fn first_stack_value_not_needed_by(&self, needed: &[ValueId]) -> Option<usize> {
1527        let mut remaining = Self::value_counts(needed.iter().copied());
1528        for (depth, slot) in self.scheduler.stack.iter().enumerate() {
1529            let Some(value) = slot else {
1530                return Some(depth);
1531            };
1532            let Some(count) = remaining.get_mut(&value) else {
1533                return Some(depth);
1534            };
1535            if *count == 0 {
1536                return Some(depth);
1537            }
1538            *count -= 1;
1539        }
1540        None
1541    }
1542
1543    fn mark_live_in_spills(&mut self, func: &Function, liveness: &Liveness, block_id: BlockId) {
1544        // Values already on the stack (carried in from a preserved predecessor
1545        // edge) are read directly; marking them reloadable would point at a
1546        // spill slot that may never have been stored.
1547        for val in liveness.live_in(block_id).iter() {
1548            if !self.scheduler.stack.contains(val) && self.scheduler.spills.get(val).is_some() {
1549                self.scheduler.spills.mark_reloadable(val);
1550            }
1551        }
1552        for &inst_id in &func.blocks[block_id].instructions {
1553            if matches!(func.instructions[inst_id].kind, InstKind::Phi(_))
1554                && let Some(val) = func.inst_result_value(inst_id)
1555                && !self.scheduler.stack.contains(val)
1556                && self.scheduler.spills.get(val).is_some()
1557            {
1558                self.scheduler.spills.mark_reloadable(val);
1559            }
1560        }
1561    }
1562
1563    fn spill_values_before_stack_clear(&mut self, func: &Function, values: &[ValueId]) {
1564        for &value in values {
1565            self.spill_value_if_needed(func, value);
1566        }
1567    }
1568
1569    /// Spills a single value to memory if it's on the stack and not already stored.
1570    /// Skips immediates and args since they can be re-emitted without spilling.
1571    fn spill_value_if_needed(&mut self, func: &Function, val: ValueId) {
1572        // Skip immediates and args - they can be re-emitted without spilling
1573        match func.value(val) {
1574            crate::mir::Value::Immediate(_) | crate::mir::Value::Arg { .. } => return,
1575            _ => {}
1576        }
1577
1578        if self.scheduler.spills.is_stored(val) {
1579            return;
1580        }
1581
1582        if let Some(depth) = self.scheduler.stack.find(val) {
1583            let slot = self.scheduler.spills.allocate(val);
1584            if depth >= MAX_STACK_ACCESS {
1585                self.spill_deep_stack_value(func, val, slot, depth);
1586                return;
1587            }
1588
1589            self.spill_accessible_stack_value(func, val, slot, depth);
1590        }
1591    }
1592
1593    fn spill_value_to_reserved_slot(&mut self, func: &Function, val: ValueId) -> bool {
1594        if Self::is_rematerializable_value(func, val) || self.scheduler.spills.get(val).is_none() {
1595            return false;
1596        }
1597
1598        let Some(depth) = self.scheduler.stack.find(val) else {
1599            return false;
1600        };
1601        let slot = self.scheduler.spills.allocate(val);
1602        if depth >= MAX_STACK_ACCESS {
1603            self.spill_deep_stack_value(func, val, slot, depth);
1604        } else {
1605            self.spill_accessible_stack_value(func, val, slot, depth);
1606        }
1607        true
1608    }
1609
1610    fn spill_reserved_result_if_live(
1611        &mut self,
1612        func: &Function,
1613        liveness: &Liveness,
1614        block: BlockId,
1615        inst_idx: usize,
1616        value: ValueId,
1617    ) {
1618        // This is not the normal first-store path; `generate_inst` handles live-out results.
1619        // It repairs physical emission orders where a successor block emitted first has already
1620        // marked this reserved cross-block slot as stored/reloadable before the defining block
1621        // materializes the value.
1622        if self.scheduler.spills.get(value).is_none()
1623            || !self.scheduler.spills.is_stored(value)
1624            || liveness.is_dead_after(value, block, inst_idx)
1625        {
1626            return;
1627        }
1628
1629        self.spill_value_to_reserved_slot(func, value);
1630    }
1631
1632    fn spill_accessible_stack_value(
1633        &mut self,
1634        func: &Function,
1635        val: ValueId,
1636        slot: SpillSlot,
1637        depth: usize,
1638    ) {
1639        debug_assert!(depth < MAX_STACK_ACCESS);
1640
1641        // DUP the value to top of stack for storing.
1642        // We need to DUP (not just use ensure_on_top) because:
1643        // 1. If value is on top, ensure_on_top does nothing but we need a copy
1644        // 2. MSTORE will consume the value, and we want to preserve the original
1645        let dup_n = (depth + 1) as u8;
1646        self.asm.emit_op(op::dup(dup_n));
1647        self.scheduler.stack.dup(dup_n);
1648
1649        self.store_stack_top_to_spill(func, val, slot);
1650    }
1651
1652    fn spill_deep_stack_value(
1653        &mut self,
1654        func: &Function,
1655        val: ValueId,
1656        slot: SpillSlot,
1657        depth: usize,
1658    ) {
1659        debug_assert!(depth >= MAX_STACK_ACCESS);
1660
1661        let mut saved_above = Vec::with_capacity(depth + 1 - MAX_STACK_ACCESS);
1662        for _ in 0..(depth + 1 - MAX_STACK_ACCESS) {
1663            let Some(top) = self.scheduler.stack.top() else {
1664                panic!("cannot spill deep stack value {val:?}: untracked stack entry above it");
1665            };
1666            let top_slot = self.scheduler.spills.allocate(top);
1667            if self.scheduler.spills.is_reloadable(top) {
1668                self.emit_stack_op(StackOp::Pop);
1669            } else {
1670                self.store_stack_top_to_spill(func, top, top_slot);
1671            }
1672            saved_above.push((top, top_slot));
1673        }
1674
1675        let Some(accessible_depth) = self.scheduler.stack.find(val) else {
1676            panic!("cannot spill deep stack value {val:?}: value disappeared while exposing it");
1677        };
1678        self.spill_accessible_stack_value(func, val, slot, accessible_depth);
1679
1680        for (saved, saved_slot) in saved_above.into_iter().rev() {
1681            self.emit_spill_slot_addr(func, saved_slot);
1682            self.asm.emit_op(op::MLOAD);
1683            self.scheduler.stack.push(saved);
1684        }
1685    }
1686
1687    fn store_stack_top_to_spill(&mut self, func: &Function, value: ValueId, slot: SpillSlot) {
1688        // Store to spill slot: PUSH offset, MSTORE.
1689        // The PUSH creates an untracked stack entry, so we track it as unknown.
1690        self.emit_spill_slot_addr(func, slot);
1691        self.scheduler.stack.push_unknown();
1692
1693        self.asm.emit_op(op::MSTORE);
1694        // MSTORE consumes 2 values: the untracked offset and the value being spilled.
1695        self.scheduler.stack.pop();
1696        self.scheduler.stack.pop();
1697        self.scheduler.spills.mark_stored(value);
1698    }
1699
1700    /// Spills operands that are live-out before an instruction consumes them.
1701    /// This ensures cross-block values are preserved in memory.
1702    fn spill_live_out_operands(
1703        &mut self,
1704        func: &Function,
1705        liveness: &Liveness,
1706        block_id: BlockId,
1707        operands: &[ValueId],
1708    ) {
1709        let live_out = liveness.live_out(block_id);
1710
1711        for &op in operands {
1712            if live_out.contains(op) && !self.is_stack_phi_source(block_id, op) {
1713                self.spill_value_if_needed(func, op);
1714            }
1715        }
1716    }
1717
1718    fn is_rematerializable_value(func: &Function, value: ValueId) -> bool {
1719        matches!(func.value(value), crate::mir::Value::Immediate(_) | crate::mir::Value::Arg { .. })
1720    }
1721
1722    fn is_cheap_recomputable_value(func: &Function, value: ValueId) -> bool {
1723        let crate::mir::Value::Inst(inst_id) = func.value(value) else {
1724            return false;
1725        };
1726        matches!(
1727            func.instruction(*inst_id).kind,
1728            InstKind::Add(_, _)
1729                | InstKind::Sub(_, _)
1730                | InstKind::Mul(_, _)
1731                | InstKind::And(_, _)
1732                | InstKind::Or(_, _)
1733                | InstKind::Xor(_, _)
1734                | InstKind::Shl(_, _)
1735                | InstKind::Shr(_, _)
1736                | InstKind::Sar(_, _)
1737        )
1738    }
1739
1740    fn spill_top_value_if_live(
1741        &mut self,
1742        func: &Function,
1743        liveness: &Liveness,
1744        block: BlockId,
1745        inst_idx: usize,
1746        value: ValueId,
1747    ) {
1748        if Self::is_rematerializable_value(func, value) {
1749            return;
1750        }
1751
1752        let has_reserved_cross_block_slot = self.scheduler.spills.get(value).is_some();
1753        if liveness.is_dead_after(value, block, inst_idx) && !has_reserved_cross_block_slot {
1754            return;
1755        }
1756
1757        debug_assert_eq!(self.scheduler.stack.top(), Some(value));
1758        if !self.spill_value_to_reserved_slot(func, value) {
1759            self.spill_value_if_needed(func, value);
1760        }
1761    }
1762
1763    /// Generates bytecode for an instruction.
1764    fn generate_inst(
1765        &mut self,
1766        func: &Function,
1767        kind: &InstKind,
1768        liveness: &Liveness,
1769        block: BlockId,
1770        inst_idx: usize,
1771        result_value: Option<ValueId>,
1772    ) {
1773        // Spill any operands that are live-out before they get consumed.
1774        // This ensures cross-block values are preserved in memory.
1775        let operands = kind.operands();
1776        self.spill_live_out_operands(func, liveness, block, &operands);
1777
1778        match kind {
1779            // Binary arithmetic operations
1780            InstKind::Add(a, b) => self.emit_binary_op_with_result(
1781                func,
1782                *a,
1783                *b,
1784                op::ADD,
1785                result_value,
1786                liveness,
1787                block,
1788                inst_idx,
1789            ),
1790            InstKind::Sub(a, b) => self.emit_binary_op_with_result(
1791                func,
1792                *a,
1793                *b,
1794                op::SUB,
1795                result_value,
1796                liveness,
1797                block,
1798                inst_idx,
1799            ),
1800            InstKind::Mul(a, b) => self.emit_binary_op_with_result(
1801                func,
1802                *a,
1803                *b,
1804                op::MUL,
1805                result_value,
1806                liveness,
1807                block,
1808                inst_idx,
1809            ),
1810            InstKind::Div(a, b) => self.emit_binary_op_with_result(
1811                func,
1812                *a,
1813                *b,
1814                op::DIV,
1815                result_value,
1816                liveness,
1817                block,
1818                inst_idx,
1819            ),
1820            InstKind::SDiv(a, b) => self.emit_binary_op_with_result(
1821                func,
1822                *a,
1823                *b,
1824                op::SDIV,
1825                result_value,
1826                liveness,
1827                block,
1828                inst_idx,
1829            ),
1830            InstKind::Mod(a, b) => self.emit_binary_op_with_result(
1831                func,
1832                *a,
1833                *b,
1834                op::MOD,
1835                result_value,
1836                liveness,
1837                block,
1838                inst_idx,
1839            ),
1840            InstKind::SMod(a, b) => self.emit_binary_op_with_result(
1841                func,
1842                *a,
1843                *b,
1844                op::SMOD,
1845                result_value,
1846                liveness,
1847                block,
1848                inst_idx,
1849            ),
1850            InstKind::Exp(a, b) => self.emit_binary_op_with_result(
1851                func,
1852                *a,
1853                *b,
1854                op::EXP,
1855                result_value,
1856                liveness,
1857                block,
1858                inst_idx,
1859            ),
1860
1861            // Bitwise operations
1862            InstKind::And(a, b) => self.emit_binary_op_with_result(
1863                func,
1864                *a,
1865                *b,
1866                op::AND,
1867                result_value,
1868                liveness,
1869                block,
1870                inst_idx,
1871            ),
1872            InstKind::Or(a, b) => self.emit_binary_op_with_result(
1873                func,
1874                *a,
1875                *b,
1876                op::OR,
1877                result_value,
1878                liveness,
1879                block,
1880                inst_idx,
1881            ),
1882            InstKind::Xor(a, b) => self.emit_binary_op_with_result(
1883                func,
1884                *a,
1885                *b,
1886                op::XOR,
1887                result_value,
1888                liveness,
1889                block,
1890                inst_idx,
1891            ),
1892            InstKind::Not(a) => self.emit_unary_op_with_result(
1893                func,
1894                *a,
1895                op::NOT,
1896                result_value,
1897                liveness,
1898                block,
1899                inst_idx,
1900            ),
1901            InstKind::Shl(shift, val) => self.emit_binary_op_with_result(
1902                func,
1903                *shift,
1904                *val,
1905                op::SHL,
1906                result_value,
1907                liveness,
1908                block,
1909                inst_idx,
1910            ),
1911            InstKind::Shr(shift, val) => self.emit_binary_op_with_result(
1912                func,
1913                *shift,
1914                *val,
1915                op::SHR,
1916                result_value,
1917                liveness,
1918                block,
1919                inst_idx,
1920            ),
1921            InstKind::Sar(shift, val) => self.emit_binary_op_with_result(
1922                func,
1923                *shift,
1924                *val,
1925                op::SAR,
1926                result_value,
1927                liveness,
1928                block,
1929                inst_idx,
1930            ),
1931            InstKind::Byte(i, x) => self.emit_binary_op_with_result(
1932                func,
1933                *i,
1934                *x,
1935                op::BYTE,
1936                result_value,
1937                liveness,
1938                block,
1939                inst_idx,
1940            ),
1941
1942            // Comparison operations - track results for branch conditions and Select
1943            InstKind::Lt(a, b) => self.emit_binary_op_with_result(
1944                func,
1945                *a,
1946                *b,
1947                op::LT,
1948                result_value,
1949                liveness,
1950                block,
1951                inst_idx,
1952            ),
1953            InstKind::Gt(a, b) => self.emit_binary_op_with_result(
1954                func,
1955                *a,
1956                *b,
1957                op::GT,
1958                result_value,
1959                liveness,
1960                block,
1961                inst_idx,
1962            ),
1963            InstKind::SLt(a, b) => self.emit_binary_op_with_result(
1964                func,
1965                *a,
1966                *b,
1967                op::SLT,
1968                result_value,
1969                liveness,
1970                block,
1971                inst_idx,
1972            ),
1973            InstKind::SGt(a, b) => self.emit_binary_op_with_result(
1974                func,
1975                *a,
1976                *b,
1977                op::SGT,
1978                result_value,
1979                liveness,
1980                block,
1981                inst_idx,
1982            ),
1983            InstKind::Eq(a, b) => self.emit_binary_op_with_result(
1984                func,
1985                *a,
1986                *b,
1987                op::EQ,
1988                result_value,
1989                liveness,
1990                block,
1991                inst_idx,
1992            ),
1993            InstKind::IsZero(a) => self.emit_unary_op_with_result(
1994                func,
1995                *a,
1996                op::ISZERO,
1997                result_value,
1998                liveness,
1999                block,
2000                inst_idx,
2001            ),
2002
2003            // Memory operations
2004            // Track MLOAD results so they can be used as operands in subsequent instructions.
2005            // This is essential for nested external calls where the return value from one call
2006            // becomes an argument to another call.
2007            InstKind::MLoad(addr) => self.emit_unary_op_with_result(
2008                func,
2009                *addr,
2010                op::MLOAD,
2011                result_value,
2012                liveness,
2013                block,
2014                inst_idx,
2015            ),
2016            InstKind::MStore(addr, val) => self.emit_store_op_live_aware(
2017                func,
2018                *addr,
2019                *val,
2020                op::MSTORE,
2021                liveness,
2022                block,
2023                inst_idx,
2024            ),
2025            InstKind::MStore8(addr, val) => self.emit_store_op_live_aware(
2026                func,
2027                *addr,
2028                *val,
2029                op::MSTORE8,
2030                liveness,
2031                block,
2032                inst_idx,
2033            ),
2034            InstKind::MSize => {
2035                self.asm.emit_op(op::MSIZE);
2036                self.scheduler.instruction_executed(0, result_value);
2037            }
2038
2039            // Storage operations
2040            InstKind::SLoad(slot) => self.emit_unary_op_with_result(
2041                func,
2042                *slot,
2043                op::SLOAD,
2044                result_value,
2045                liveness,
2046                block,
2047                inst_idx,
2048            ),
2049            InstKind::SStore(slot, val) => self.emit_store_op_live_aware(
2050                func,
2051                *slot,
2052                *val,
2053                op::SSTORE,
2054                liveness,
2055                block,
2056                inst_idx,
2057            ),
2058            InstKind::TLoad(slot) => self.emit_unary_op_with_result(
2059                func,
2060                *slot,
2061                op::TLOAD,
2062                result_value,
2063                liveness,
2064                block,
2065                inst_idx,
2066            ),
2067            InstKind::TStore(slot, val) => self.emit_store_op_live_aware(
2068                func,
2069                *slot,
2070                *val,
2071                op::TSTORE,
2072                liveness,
2073                block,
2074                inst_idx,
2075            ),
2076
2077            // Calldata operations
2078            InstKind::CalldataLoad(off) => self.emit_unary_op_with_result(
2079                func,
2080                *off,
2081                op::CALLDATALOAD,
2082                result_value,
2083                liveness,
2084                block,
2085                inst_idx,
2086            ),
2087            InstKind::CalldataSize => {
2088                self.asm.emit_op(op::CALLDATASIZE);
2089                self.scheduler.instruction_executed(0, result_value);
2090            }
2091
2092            // Hash operations
2093            InstKind::Keccak256(off, len) => self.emit_binary_op_with_result(
2094                func,
2095                *off,
2096                *len,
2097                op::KECCAK256,
2098                result_value,
2099                liveness,
2100                block,
2101                inst_idx,
2102            ),
2103
2104            // Environment operations
2105            InstKind::Caller => {
2106                self.asm.emit_op(op::CALLER);
2107                self.scheduler.instruction_executed(0, result_value);
2108            }
2109            InstKind::CallValue => {
2110                self.asm.emit_op(op::CALLVALUE);
2111                self.scheduler.instruction_executed(0, result_value);
2112            }
2113            InstKind::Address => {
2114                self.asm.emit_op(op::ADDRESS);
2115                self.scheduler.instruction_executed(0, result_value);
2116            }
2117            InstKind::Origin => {
2118                self.asm.emit_op(op::ORIGIN);
2119                self.scheduler.instruction_executed(0, result_value);
2120            }
2121            InstKind::GasPrice => {
2122                self.asm.emit_op(op::GASPRICE);
2123                self.scheduler.instruction_executed(0, result_value);
2124            }
2125            InstKind::Gas => {
2126                self.asm.emit_op(op::GAS);
2127                self.scheduler.instruction_executed(0, result_value);
2128            }
2129            InstKind::Timestamp => {
2130                self.asm.emit_op(op::TIMESTAMP);
2131                self.scheduler.instruction_executed(0, result_value);
2132            }
2133            InstKind::BlockNumber => {
2134                self.asm.emit_op(op::NUMBER);
2135                self.scheduler.instruction_executed(0, result_value);
2136            }
2137            InstKind::Coinbase => {
2138                self.asm.emit_op(op::COINBASE);
2139                self.scheduler.instruction_executed(0, result_value);
2140            }
2141            InstKind::ChainId => {
2142                self.asm.emit_op(op::CHAINID);
2143                self.scheduler.instruction_executed(0, result_value);
2144            }
2145            InstKind::SelfBalance => {
2146                self.asm.emit_op(op::SELFBALANCE);
2147                self.scheduler.instruction_executed(0, result_value);
2148            }
2149            InstKind::BaseFee => {
2150                self.asm.emit_op(op::BASEFEE);
2151                self.scheduler.instruction_executed(0, result_value);
2152            }
2153            InstKind::BlobBaseFee => {
2154                self.asm.emit_op(op::BLOBBASEFEE);
2155                self.scheduler.instruction_executed(0, result_value);
2156            }
2157            InstKind::GasLimit => {
2158                self.asm.emit_op(op::GASLIMIT);
2159                self.scheduler.instruction_executed(0, result_value);
2160            }
2161            InstKind::PrevRandao => {
2162                self.asm.emit_op(op::PREVRANDAO);
2163                self.scheduler.instruction_executed(0, result_value);
2164            }
2165            InstKind::Balance(addr) => self.emit_unary_op_with_result(
2166                func,
2167                *addr,
2168                op::BALANCE,
2169                result_value,
2170                liveness,
2171                block,
2172                inst_idx,
2173            ),
2174            InstKind::BlockHash(num) => self.emit_unary_op_with_result(
2175                func,
2176                *num,
2177                op::BLOCKHASH,
2178                result_value,
2179                liveness,
2180                block,
2181                inst_idx,
2182            ),
2183            InstKind::BlobHash(idx) => self.emit_unary_op_with_result(
2184                func,
2185                *idx,
2186                op::BLOBHASH,
2187                result_value,
2188                liveness,
2189                block,
2190                inst_idx,
2191            ),
2192            InstKind::ExtCodeSize(addr) => self.emit_unary_op_with_result(
2193                func,
2194                *addr,
2195                op::EXTCODESIZE,
2196                result_value,
2197                liveness,
2198                block,
2199                inst_idx,
2200            ),
2201            InstKind::ExtCodeHash(addr) => self.emit_unary_op_with_result(
2202                func,
2203                *addr,
2204                op::EXTCODEHASH,
2205                result_value,
2206                liveness,
2207                block,
2208                inst_idx,
2209            ),
2210            InstKind::CodeSize => {
2211                self.asm.emit_op(op::CODESIZE);
2212                self.scheduler.instruction_executed(0, result_value);
2213            }
2214            InstKind::LoadImmutable(offset) => {
2215                if self.in_constructor {
2216                    // The running constructor's own placeholders are never
2217                    // patched; read the staged scratch word instead.
2218                    self.asm.emit_push(U256::from(IMMUTABLE_SCRATCH_BASE + u64::from(*offset)));
2219                    self.asm.emit_op(op::MLOAD);
2220                } else {
2221                    self.asm.emit_push_immutable(*offset);
2222                }
2223                self.scheduler.instruction_executed(0, result_value);
2224            }
2225            InstKind::ReturnDataSize => {
2226                self.asm.emit_op(op::RETURNDATASIZE);
2227                self.scheduler.instruction_executed(0, result_value);
2228            }
2229
2230            // Ternary operations
2231            InstKind::AddMod(a, b, n) => {
2232                self.emit_ternary_op(func, *a, *b, *n, op::ADDMOD, result_value)
2233            }
2234            InstKind::MulMod(a, b, n) => {
2235                self.emit_ternary_op(func, *a, *b, *n, op::MULMOD, result_value)
2236            }
2237
2238            // Select is like a ternary conditional
2239            InstKind::Select(cond, true_val, false_val) => {
2240                // select(cond, t, f) = f + cond * (t - f)
2241                //
2242                // We emit all three values to the stack, then do inline computation.
2243                // Stack notation: rightmost = top (depth 0).
2244                // Stack after emit_value calls: [f, t, cond] with cond on top.
2245
2246                self.emit_value(func, *false_val); // Stack: [f]
2247                self.emit_operand(func, *true_val); // Stack: [f, t]
2248                self.emit_operand(func, *cond); // Stack: [f, t, cond]
2249
2250                // Now compute: f + cond * (t - f)
2251                // Stack is [f, t, cond] with cond on top (depth 0), t at depth 1, f at depth 2
2252                //
2253                // Step 1: DUP3 to get f -> [f, t, cond, f]
2254                self.emit_stack_op(StackOp::Dup(3));
2255                // Step 2: DUP3 to get t (now at depth 2) -> [f, t, cond, f, t]
2256                self.emit_stack_op(StackOp::Dup(3));
2257                // Step 3: SUB (top - second = t - f) -> [f, t, cond, t-f]
2258                self.emit_op_with_effect(
2259                    op::SUB,
2260                    StackEffect { pops: 2, pushes: 1 },
2261                    StackPush::Unknown,
2262                );
2263                // Step 4: MUL (cond * (t-f)) -> [f, t, cond*(t-f)]
2264                self.emit_op_with_effect(
2265                    op::MUL,
2266                    StackEffect { pops: 2, pushes: 1 },
2267                    StackPush::Unknown,
2268                );
2269                // Step 5: SWAP1 -> [f, cond*(t-f), t]
2270                self.emit_stack_op(StackOp::Swap(1));
2271                // Step 6: POP (remove t) -> [f, cond*(t-f)]
2272                self.emit_stack_op(StackOp::Pop);
2273                // Step 7: ADD (cond*(t-f) + f = f + cond*(t-f)) -> [result]
2274                let push = result_value.map_or(StackPush::Unknown, StackPush::Tracked);
2275                self.emit_op_with_effect(op::ADD, StackEffect { pops: 2, pushes: 1 }, push);
2276            }
2277
2278            // Sign extend
2279            InstKind::SignExtend(b, x) => self.emit_binary_op_with_result(
2280                func,
2281                *b,
2282                *x,
2283                op::SIGNEXTEND,
2284                result_value,
2285                liveness,
2286                block,
2287                inst_idx,
2288            ),
2289
2290            // Phi nodes are skipped (handled by copies)
2291            InstKind::Phi(_) => {}
2292
2293            // Contract creation
2294            InstKind::Create(value, offset, size) => {
2295                self.emit_value(func, *size);
2296                self.emit_operand(func, *offset);
2297                self.emit_operand(func, *value);
2298                self.asm.emit_op(op::CREATE);
2299                // CREATE consumes 3 values and produces 1 (new contract address)
2300                self.scheduler.instruction_executed(3, result_value);
2301            }
2302
2303            InstKind::Create2(value, offset, size, salt) => {
2304                // CREATE2 expects stack (top to bottom): salt, size, offset, value
2305                // So we push in reverse order: value first (goes deepest), then offset, size, salt
2306                self.emit_value(func, *value);
2307                self.emit_operand(func, *offset);
2308                self.emit_operand(func, *size);
2309                self.emit_operand(func, *salt);
2310                self.asm.emit_op(op::CREATE2);
2311                // CREATE2 consumes 4 values and produces 1 (new contract address)
2312                self.scheduler.instruction_executed(4, result_value);
2313            }
2314
2315            // External calls
2316            //
2317            // These use emit_value_fresh to guarantee correct values regardless of scheduler state.
2318            // The stack-aware emit_op_with_effect ensures proper tracking after emission.
2319            InstKind::Call { gas, addr, value, args_offset, args_size, ret_offset, ret_size } => {
2320                // CALL(gas, addr, value, argsOffset, argsSize, retOffset, retSize)
2321                // EVM pops in order: gas (TOS), addr, value, argsOffset, argsSize, retOffset,
2322                // retSize So we push in reverse order: retSize first (deepest), gas
2323                // last (TOS)
2324                self.emit_value_fresh(func, *ret_size);
2325                self.emit_value_fresh(func, *ret_offset);
2326                self.emit_value_fresh(func, *args_size);
2327                self.emit_value_fresh(func, *args_offset);
2328                self.emit_value_fresh(func, *value);
2329                self.emit_value_fresh(func, *addr);
2330                self.emit_value_fresh(func, *gas);
2331
2332                // CALL consumes 7 values and produces 1 (success bool)
2333                let push = result_value.map_or(StackPush::Unknown, StackPush::Tracked);
2334                self.emit_op_with_effect(op::CALL, StackEffect { pops: 7, pushes: 1 }, push);
2335            }
2336
2337            InstKind::StaticCall { gas, addr, args_offset, args_size, ret_offset, ret_size } => {
2338                // STATICCALL(gas, addr, argsOffset, argsSize, retOffset, retSize)
2339                self.emit_value_fresh(func, *ret_size);
2340                self.emit_value_fresh(func, *ret_offset);
2341                self.emit_value_fresh(func, *args_size);
2342                self.emit_value_fresh(func, *args_offset);
2343                self.emit_value_fresh(func, *addr);
2344                self.emit_value_fresh(func, *gas);
2345                // STATICCALL consumes 6 values and produces 1 (success bool)
2346                let push = result_value.map_or(StackPush::Unknown, StackPush::Tracked);
2347                self.emit_op_with_effect(op::STATICCALL, StackEffect { pops: 6, pushes: 1 }, push);
2348            }
2349
2350            InstKind::DelegateCall { gas, addr, args_offset, args_size, ret_offset, ret_size } => {
2351                // DELEGATECALL(gas, addr, argsOffset, argsSize, retOffset, retSize)
2352                self.emit_value_fresh(func, *ret_size);
2353                self.emit_value_fresh(func, *ret_offset);
2354                self.emit_value_fresh(func, *args_size);
2355                self.emit_value_fresh(func, *args_offset);
2356                self.emit_value_fresh(func, *addr);
2357                self.emit_value_fresh(func, *gas);
2358                // DELEGATECALL consumes 6 values and produces 1 (success bool)
2359                let push = result_value.map_or(StackPush::Unknown, StackPush::Tracked);
2360                self.emit_op_with_effect(
2361                    op::DELEGATECALL,
2362                    StackEffect { pops: 6, pushes: 1 },
2363                    push,
2364                );
2365            }
2366
2367            InstKind::InternalCall { function, args, returns } => {
2368                self.emit_internal_call(
2369                    func,
2370                    *function,
2371                    args,
2372                    *returns as usize,
2373                    result_value,
2374                    liveness,
2375                    block,
2376                    inst_idx,
2377                );
2378            }
2379
2380            InstKind::InternalFrameAddr(offset) => {
2381                self.emit_current_internal_frame_addr(*offset);
2382                if let Some(result) = result_value {
2383                    self.scheduler.stack.push(result);
2384                }
2385            }
2386
2387            // Log operations
2388            InstKind::Log0(offset, size) => {
2389                // LOG0(offset, size) - stack order: offset on top, then size
2390                self.emit_value(func, *size);
2391                self.emit_operand(func, *offset);
2392                self.asm.emit_op(op::LOG0);
2393                self.scheduler.instruction_executed(2, None);
2394            }
2395            InstKind::Log1(offset, size, topic1) => {
2396                // LOG1(offset, size, topic1) - stack order: offset, size, topic1
2397                self.emit_value(func, *topic1);
2398                self.emit_operand(func, *size);
2399                self.emit_operand(func, *offset);
2400                self.asm.emit_op(op::LOG1);
2401                self.scheduler.instruction_executed(3, None);
2402            }
2403            InstKind::Log2(offset, size, topic1, topic2) => {
2404                // LOG2(offset, size, topic1, topic2) - stack order: offset, size, topic1, topic2
2405                self.emit_value(func, *topic2);
2406                self.emit_operand(func, *topic1);
2407                self.emit_operand(func, *size);
2408                self.emit_operand(func, *offset);
2409                self.asm.emit_op(op::LOG2);
2410                self.scheduler.instruction_executed(4, None);
2411            }
2412            InstKind::Log3(offset, size, topic1, topic2, topic3) => {
2413                // LOG3(offset, size, topic1, topic2, topic3)
2414                self.emit_value(func, *topic3);
2415                self.emit_operand(func, *topic2);
2416                self.emit_operand(func, *topic1);
2417                self.emit_operand(func, *size);
2418                self.emit_operand(func, *offset);
2419                self.asm.emit_op(op::LOG3);
2420                self.scheduler.instruction_executed(5, None);
2421            }
2422            InstKind::Log4(offset, size, topic1, topic2, topic3, topic4) => {
2423                // LOG4(offset, size, topic1, topic2, topic3, topic4)
2424                self.emit_value(func, *topic4);
2425                self.emit_operand(func, *topic3);
2426                self.emit_operand(func, *topic2);
2427                self.emit_operand(func, *topic1);
2428                self.emit_operand(func, *size);
2429                self.emit_operand(func, *offset);
2430                self.asm.emit_op(op::LOG4);
2431                self.scheduler.instruction_executed(6, None);
2432            }
2433
2434            // Memory copy operations
2435            InstKind::CalldataCopy(dest, offset, size) => {
2436                // CALLDATACOPY(destOffset, offset, size)
2437                self.emit_copy_op_live_aware(
2438                    func,
2439                    &[*size, *offset, *dest],
2440                    op::CALLDATACOPY,
2441                    liveness,
2442                    block,
2443                    inst_idx,
2444                );
2445            }
2446
2447            InstKind::CodeCopy(dest, offset, size) => {
2448                // CODECOPY(destOffset, offset, size)
2449                self.emit_copy_op_live_aware(
2450                    func,
2451                    &[*size, *offset, *dest],
2452                    op::CODECOPY,
2453                    liveness,
2454                    block,
2455                    inst_idx,
2456                );
2457            }
2458
2459            InstKind::ReturnDataCopy(dest, offset, size) => {
2460                // RETURNDATACOPY(destOffset, offset, size)
2461                self.emit_copy_op_live_aware(
2462                    func,
2463                    &[*size, *offset, *dest],
2464                    op::RETURNDATACOPY,
2465                    liveness,
2466                    block,
2467                    inst_idx,
2468                );
2469            }
2470
2471            InstKind::MCopy(dest, src, size) => {
2472                // MCOPY(destOffset, srcOffset, size)
2473                self.emit_copy_op_live_aware(
2474                    func,
2475                    &[*size, *src, *dest],
2476                    op::MCOPY,
2477                    liveness,
2478                    block,
2479                    inst_idx,
2480                );
2481            }
2482
2483            InstKind::ExtCodeCopy(addr, dest, offset, size) => {
2484                // EXTCODECOPY(address, destOffset, offset, size)
2485                self.emit_copy_op_live_aware(
2486                    func,
2487                    &[*size, *offset, *dest, *addr],
2488                    op::EXTCODECOPY,
2489                    liveness,
2490                    block,
2491                    inst_idx,
2492                );
2493            }
2494        }
2495
2496        if let Some(result) = result_value
2497            && liveness.live_out(block).contains(result)
2498            && !self.is_stack_phi_source(block, result)
2499        {
2500            self.spill_value_if_needed(func, result);
2501        }
2502
2503        // Drop dead values after the instruction
2504        let dead_ops = self.scheduler.drop_dead_values(liveness, block, inst_idx);
2505        for op in dead_ops {
2506            self.asm.emit_op(op.opcode());
2507        }
2508        #[cfg(debug_assertions)]
2509        {
2510            debug_assert!(self.scheduler.depth() <= 1024);
2511        }
2512    }
2513
2514    fn emit_new_internal_frame_base_tracked(&mut self) {
2515        self.asm.emit_push(U256::from(0x40));
2516        self.asm.emit_op(op::MLOAD);
2517        self.scheduler.stack.push_unknown();
2518    }
2519
2520    fn emit_internal_frame_store_from_top_preserving_base(&mut self, offset: u64) {
2521        self.emit_stack_op(StackOp::Dup(2));
2522        if offset != 0 {
2523            self.asm.emit_push(U256::from(offset));
2524            self.scheduler.stack.push_unknown();
2525            self.emit_op_with_effect(
2526                op::ADD,
2527                StackEffect { pops: 2, pushes: 1 },
2528                StackPush::Unknown,
2529            );
2530        }
2531        self.asm.emit_op(op::MSTORE);
2532        self.scheduler.instruction_executed(2, None);
2533    }
2534
2535    fn emit_store_frame_base_to_current_frame_slot(&mut self) {
2536        self.emit_stack_op(StackOp::Dup(1));
2537        self.asm.emit_push(U256::from(INTERNAL_FRAME_PTR_SLOT));
2538        self.scheduler.stack.push_unknown();
2539        self.asm.emit_op(op::MSTORE);
2540        self.scheduler.instruction_executed(2, None);
2541    }
2542
2543    fn emit_store_new_free_pointer_from_frame_base(&mut self, frame_size: DeferredConst) {
2544        self.asm.emit_push_deferred(frame_size);
2545        self.scheduler.stack.push_unknown();
2546        self.emit_op_with_effect(op::ADD, StackEffect { pops: 2, pushes: 1 }, StackPush::Unknown);
2547        self.asm.emit_push(U256::from(0x40));
2548        self.scheduler.stack.push_unknown();
2549        self.asm.emit_op(op::MSTORE);
2550        self.scheduler.instruction_executed(2, None);
2551    }
2552
2553    fn emit_current_internal_frame_addr(&mut self, offset: u64) {
2554        self.asm.emit_push(U256::from(INTERNAL_FRAME_PTR_SLOT));
2555        self.asm.emit_op(op::MLOAD);
2556        if offset != 0 {
2557            self.asm.emit_push(U256::from(offset));
2558            self.asm.emit_op(op::ADD);
2559        }
2560    }
2561
2562    fn conservative_spill_frame_size(func: &Function) -> u64 {
2563        // The scheduler may allocate spill slots lazily while exposing deep stack values, not only
2564        // for values preallocated as cross-block live-ins/live-outs. Use this only when a body's
2565        // exact post-emission spill size is unavailable.
2566        func.values.len() as u64 * 32
2567    }
2568
2569    fn external_spill_base(func: &Function) -> u64 {
2570        let low_memory_start = if Self::uses_internal_frame_slot(func) {
2571            INTERNAL_FRAME_PTR_SLOT + 32
2572        } else {
2573            LOW_MEMORY_START
2574        };
2575        low_memory_start + func.internal_frame_size.max(func.external_static_return_size)
2576    }
2577
2578    fn constructor_free_memory_start(spill_size: u64) -> u64 {
2579        CONSTRUCTOR_FREE_MEMORY_START.max(CONSTRUCTOR_SPILL_BASE + spill_size)
2580    }
2581
2582    fn uses_internal_frame_slot(func: &Function) -> bool {
2583        func.instructions.iter().any(|inst| matches!(inst.kind, InstKind::InternalCall { .. }))
2584    }
2585
2586    fn emit_external_free_memory_start(&mut self) -> DeferredConst {
2587        let id = self.asm.new_deferred_const();
2588        self.asm.emit_push_deferred(id);
2589        self.asm.emit_push(U256::from(0x40));
2590        self.asm.emit_op(op::MSTORE);
2591        id
2592    }
2593
2594    fn emit_spill_slot_addr(&mut self, func: &Function, slot: SpillSlot) {
2595        if self.in_internal_function {
2596            let spill_base =
2597                64 + (func.params.len() as u64) * 32 + (func.returns.len() as u64) * 32;
2598            self.emit_current_internal_frame_addr(
2599                spill_base + func.internal_frame_size + u64::from(slot.offset) * 32,
2600            );
2601        } else if self.in_constructor {
2602            self.asm.emit_push(U256::from(slot.byte_offset()));
2603        } else {
2604            self.asm.emit_push(U256::from(
2605                Self::external_spill_base(func) + u64::from(slot.offset) * 32,
2606            ));
2607        }
2608    }
2609
2610    fn emit_internal_arg_load(&mut self, index: u32) {
2611        self.emit_current_internal_frame_addr(64 + u64::from(index) * 32);
2612        self.asm.emit_op(op::MLOAD);
2613    }
2614
2615    #[allow(clippy::too_many_arguments)]
2616    fn emit_internal_call(
2617        &mut self,
2618        func: &Function,
2619        callee: FunctionId,
2620        args: &[ValueId],
2621        returns: usize,
2622        result: Option<ValueId>,
2623        liveness: &Liveness,
2624        block: BlockId,
2625        inst_idx: usize,
2626    ) {
2627        let Some(&callee_label) = self.function_labels.get(&callee) else {
2628            return;
2629        };
2630        let return_label = self.asm.new_label();
2631        let static_local_frame_size =
2632            self.function_static_frame_sizes.get(&callee).copied().unwrap_or_default();
2633        // Frame layout: [return addr][saved frame ptr][args][returns][locals][spills].
2634        // The spill suffix is only known after the callee body has emitted.
2635        let static_frame_size = 64 + ((args.len() + returns) as u64) * 32 + static_local_frame_size;
2636        let frame_size = self.asm.new_deferred_const();
2637        self.pending_frame_size_consts.push((frame_size, callee, static_frame_size));
2638
2639        // Spill values that are live after this call BEFORE consuming the
2640        // arguments. An argument that is also used later (e.g. a flag passed to
2641        // a helper and then stored, as in `tryAdd`) would otherwise be popped by
2642        // the arg-store loop below and then lost when the stack is cleared for
2643        // the call, leaving it unavailable at its later use.
2644        self.spill_live_stack_values(func, liveness, block, inst_idx);
2645
2646        self.emit_new_internal_frame_base_tracked();
2647
2648        // frame[0] = return label
2649        self.asm.emit_push_label(return_label);
2650        self.scheduler.stack.push_unknown();
2651        self.emit_internal_frame_store_from_top_preserving_base(0);
2652
2653        // frame[32] = previous frame pointer
2654        self.asm.emit_push(U256::from(INTERNAL_FRAME_PTR_SLOT));
2655        self.asm.emit_op(op::MLOAD);
2656        self.scheduler.stack.push_unknown();
2657        self.emit_internal_frame_store_from_top_preserving_base(32);
2658
2659        for (i, &arg) in args.iter().enumerate() {
2660            self.emit_operand(func, arg);
2661            self.emit_internal_frame_store_from_top_preserving_base(64 + (i as u64) * 32);
2662        }
2663
2664        // current_frame = frame
2665        self.emit_store_frame_base_to_current_frame_slot();
2666
2667        // free_ptr += frame_size
2668        self.emit_store_new_free_pointer_from_frame_base(frame_size);
2669
2670        self.pop_all_stack_values();
2671        self.scheduler.clear_stack();
2672
2673        self.asm.emit_push_label(callee_label);
2674        self.asm.emit_op(op::JUMP);
2675
2676        self.asm.define_label(return_label);
2677        self.scheduler.clear_stack();
2678
2679        if let Some(result) = result
2680            && returns > 0
2681        {
2682            self.emit_current_internal_frame_addr(64 + (args.len() as u64) * 32);
2683            self.asm.emit_op(op::MLOAD);
2684            self.scheduler.stack.push(result);
2685        }
2686
2687        // Copy return values 2..N from the callee frame into scratch memory at
2688        // offset `i * 32`, matching what the caller's `lower_multi_var_decl`
2689        // reads via `mload(i * 32)`. This must happen before the frame pointer is
2690        // restored below, while the callee frame is still addressable. The first
2691        // return flows back as `result` on the stack (above); these copies have a
2692        // net-zero stack effect so they leave it untouched.
2693        for i in 1..returns {
2694            self.emit_current_internal_frame_addr(64 + (args.len() as u64) * 32 + (i as u64) * 32);
2695            self.asm.emit_op(op::MLOAD);
2696            self.asm.emit_push(U256::from((i as u64) * 32));
2697            self.asm.emit_op(op::MSTORE);
2698        }
2699
2700        // Deallocate the callee frame in strict LIFO order by restoring the
2701        // free memory pointer to the callee frame base. This must happen before
2702        // restoring the caller frame pointer because `emit_current_internal_frame_addr`
2703        // reads `INTERNAL_FRAME_PTR_SLOT`. Do this only when the callee's declared
2704        // params/returns contain no memory pointer: memory pointer returns may
2705        // reference the callee's frame/heap region, and a memory pointer param lets
2706        // the callee install a fresh pointer into caller-visible memory. Solidity
2707        // allocation lowering zero-initializes new arrays/bytes/structs, so reclaimed
2708        // frame bytes need not be wiped.
2709        if self.restorable_internal_frames.contains(&callee) {
2710            self.emit_current_internal_frame_addr(0);
2711            self.asm.emit_push(U256::from(0x40));
2712            self.asm.emit_op(op::MSTORE);
2713        }
2714
2715        // Restore the caller frame pointer. If a result is on the stack, this leaves it there.
2716        self.emit_current_internal_frame_addr(32);
2717        self.asm.emit_op(op::MLOAD);
2718        self.asm.emit_push(U256::from(INTERNAL_FRAME_PTR_SLOT));
2719        self.asm.emit_op(op::MSTORE);
2720    }
2721
2722    fn spill_live_stack_values(
2723        &mut self,
2724        func: &Function,
2725        liveness: &Liveness,
2726        block: BlockId,
2727        inst_idx: usize,
2728    ) {
2729        let stack_values: Vec<_> = self.scheduler.stack.iter().flatten().collect();
2730        for value in stack_values {
2731            if !liveness.is_dead_after(value, block, inst_idx) {
2732                self.spill_value_if_needed(func, value);
2733            }
2734        }
2735    }
2736
2737    /// Emits a value to the stack.
2738    fn emit_value(&mut self, func: &Function, val: ValueId) {
2739        self.emit_value_impl(func, val, true);
2740    }
2741
2742    /// Emits a consuming operand occurrence to the stack.
2743    fn emit_operand(&mut self, func: &Function, val: ValueId) {
2744        self.emit_value_impl(func, val, false);
2745    }
2746
2747    fn emit_value_impl(&mut self, func: &Function, val: ValueId, claim_top: bool) {
2748        if let Some(depth) = self.scheduler.stack.find(val)
2749            && depth >= MAX_STACK_ACCESS
2750            && !self.scheduler.spills.is_reloadable(val)
2751            && !matches!(
2752                func.value(val),
2753                crate::mir::Value::Immediate(_) | crate::mir::Value::Arg { .. }
2754            )
2755        {
2756            let slot = self.scheduler.spills.allocate(val);
2757            self.spill_deep_stack_value(func, val, slot, depth);
2758        }
2759
2760        if self.scheduler.stack.find(val).is_none()
2761            && self.scheduler.spills.get(val).is_some()
2762            && !self.scheduler.spills.is_stored(val)
2763            && Self::is_cheap_recomputable_value(func, val)
2764        {
2765            self.emit_value_fresh(func, val);
2766            return;
2767        }
2768
2769        let ops = if claim_top {
2770            self.scheduler.ensure_on_top(val, func)
2771        } else {
2772            self.scheduler.ensure_operand_on_top(val, func)
2773        }
2774        .to_vec();
2775        for op in ops {
2776            match op {
2777                ScheduledOp::Stack(stack_op) => {
2778                    self.asm.emit_op(stack_op.opcode());
2779                }
2780                ScheduledOp::PushImmediate(imm) => {
2781                    self.asm.emit_push(imm);
2782                }
2783                ScheduledOp::LoadSpill(slot) => {
2784                    // PUSH slot_offset, MLOAD
2785                    self.emit_spill_slot_addr(func, slot);
2786                    self.asm.emit_op(op::MLOAD);
2787                }
2788                ScheduledOp::SaveSpill(slot) => {
2789                    // PUSH slot_offset, MSTORE
2790                    self.emit_spill_slot_addr(func, slot);
2791                    self.asm.emit_op(op::MSTORE);
2792                }
2793                ScheduledOp::LoadArg(index) => {
2794                    if self.in_internal_function {
2795                        self.asm.emit_push(U256::from(INTERNAL_FRAME_PTR_SLOT));
2796                        self.asm.emit_op(op::MLOAD);
2797                        self.asm.emit_push(U256::from(64 + u64::from(index) * 32));
2798                        self.asm.emit_op(op::ADD);
2799                        self.asm.emit_op(op::MLOAD);
2800                    } else if self.in_constructor {
2801                        // Constructor args were copied to memory at 0x80
2802                        // Load from memory: 0x80 + index * 32
2803                        let offset = 0x80 + (index as u64) * 32;
2804                        self.asm.emit_push(U256::from(offset));
2805                        self.asm.emit_op(op::MLOAD);
2806                    } else {
2807                        // Runtime function: load from calldata
2808                        // ABI encoding: selector (4 bytes) + args (32 bytes each)
2809                        // Offset = 4 + index * 32
2810                        let offset = 4 + (index as u64) * 32;
2811                        self.asm.emit_push(U256::from(offset));
2812                        self.asm.emit_op(op::CALLDATALOAD);
2813                    }
2814                }
2815            }
2816        }
2817    }
2818
2819    /// Emits a value fresh, without trying to DUP from the stack.
2820    /// This is used for CALL operands where we need to guarantee correct values
2821    /// regardless of scheduler stack tracking state.
2822    fn emit_value_fresh(&mut self, func: &Function, val: ValueId) {
2823        match func.value(val) {
2824            crate::mir::Value::Immediate(imm) => {
2825                if let Some(u256) = imm.as_u256() {
2826                    self.asm.emit_push(u256);
2827                    self.scheduler.stack.push(val);
2828                }
2829            }
2830            crate::mir::Value::Arg { index, .. } => {
2831                if self.in_internal_function {
2832                    self.emit_internal_arg_load(*index);
2833                } else if self.in_constructor {
2834                    let offset = 0x80 + (*index as u64) * 32;
2835                    self.asm.emit_push(U256::from(offset));
2836                    self.asm.emit_op(op::MLOAD);
2837                } else {
2838                    let offset = 4 + (*index as u64) * 32;
2839                    self.asm.emit_push(U256::from(offset));
2840                    self.asm.emit_op(op::CALLDATALOAD);
2841                }
2842                self.scheduler.stack.push(val);
2843            }
2844            crate::mir::Value::Inst(inst_id) => {
2845                // For instruction results, we need to check if they're spilled
2846                // or if they're instruction results that produce fresh values (like GAS, MLOAD)
2847                if let Some(slot) = self.scheduler.spills.get(val)
2848                    && self.scheduler.spills.is_stored(val)
2849                {
2850                    // Load from spill slot
2851                    self.emit_spill_slot_addr(func, slot);
2852                    self.asm.emit_op(op::MLOAD);
2853                    self.scheduler.stack.push(val);
2854                } else {
2855                    // Check if the instruction is one that we can "re-execute" to get a fresh value
2856                    // This handles GAS (which is always fresh) and MLOAD (which re-reads from
2857                    // memory)
2858                    let inst_kind = &func.instruction(*inst_id).kind;
2859                    match inst_kind {
2860                        crate::mir::InstKind::Gas => {
2861                            self.asm.emit_op(op::GAS);
2862                            self.scheduler.stack.push(val);
2863                        }
2864                        crate::mir::InstKind::CallValue => {
2865                            self.asm.emit_op(op::CALLVALUE);
2866                            self.scheduler.stack.push(val);
2867                        }
2868                        crate::mir::InstKind::Caller => {
2869                            self.asm.emit_op(op::CALLER);
2870                            self.scheduler.stack.push(val);
2871                        }
2872                        crate::mir::InstKind::Origin => {
2873                            self.asm.emit_op(op::ORIGIN);
2874                            self.scheduler.stack.push(val);
2875                        }
2876                        crate::mir::InstKind::CalldataSize => {
2877                            self.asm.emit_op(op::CALLDATASIZE);
2878                            self.scheduler.stack.push(val);
2879                        }
2880                        crate::mir::InstKind::InternalFrameAddr(offset) => {
2881                            self.emit_current_internal_frame_addr(*offset);
2882                            self.scheduler.stack.push(val);
2883                        }
2884                        crate::mir::InstKind::Timestamp => {
2885                            self.asm.emit_op(op::TIMESTAMP);
2886                            self.scheduler.stack.push(val);
2887                        }
2888                        crate::mir::InstKind::BlockNumber => {
2889                            self.asm.emit_op(op::NUMBER);
2890                            self.scheduler.stack.push(val);
2891                        }
2892                        crate::mir::InstKind::MLoad(offset) => {
2893                            // Note: Re-emitting MLOAD(0x40) is incorrect for struct pointers
2894                            // because the free memory pointer changes. However, with spill
2895                            // slots now at 0x1000+ (away from dynamic allocations), values
2896                            // should be properly spilled and reloaded, so we shouldn't hit
2897                            // this path for struct pointers.
2898                            //
2899                            // For other MLOAD addresses (reading from constant locations),
2900                            // re-emit is safe.
2901                            self.emit_value_fresh(func, *offset);
2902                            self.asm.emit_op(op::MLOAD);
2903                            // Pop offset, push result
2904                            self.scheduler.stack.pop();
2905                            self.scheduler.stack.push(val);
2906                        }
2907                        crate::mir::InstKind::Keccak256(offset, size) => {
2908                            // Re-emit KECCAK256 - memory content should still be valid.
2909                            // KECCAK256 reads s[0] = offset, s[1] = size, so emit the
2910                            // offset last so it ends up on top.
2911                            self.emit_value_fresh(func, *size);
2912                            self.emit_value_fresh(func, *offset);
2913                            self.asm.emit_op(op::KECCAK256);
2914                            // Pop offset and size, push result
2915                            self.scheduler.stack.pop();
2916                            self.scheduler.stack.pop();
2917                            self.scheduler.stack.push(val);
2918                        }
2919                        crate::mir::InstKind::Add(a, b) => {
2920                            self.emit_fresh_binary(func, val, *a, *b, op::ADD, true);
2921                        }
2922                        crate::mir::InstKind::Sub(a, b) => {
2923                            self.emit_fresh_binary(func, val, *a, *b, op::SUB, false);
2924                        }
2925                        crate::mir::InstKind::Mul(a, b) => {
2926                            self.emit_fresh_binary(func, val, *a, *b, op::MUL, true);
2927                        }
2928                        crate::mir::InstKind::And(a, b) => {
2929                            self.emit_fresh_binary(func, val, *a, *b, op::AND, true);
2930                        }
2931                        crate::mir::InstKind::Or(a, b) => {
2932                            self.emit_fresh_binary(func, val, *a, *b, op::OR, true);
2933                        }
2934                        crate::mir::InstKind::Xor(a, b) => {
2935                            self.emit_fresh_binary(func, val, *a, *b, op::XOR, true);
2936                        }
2937                        crate::mir::InstKind::Shl(shift, value) => {
2938                            self.emit_fresh_binary(func, val, *shift, *value, op::SHL, false);
2939                        }
2940                        crate::mir::InstKind::Shr(shift, value) => {
2941                            self.emit_fresh_binary(func, val, *shift, *value, op::SHR, false);
2942                        }
2943                        crate::mir::InstKind::Sar(shift, value) => {
2944                            self.emit_fresh_binary(func, val, *shift, *value, op::SAR, false);
2945                        }
2946                        crate::mir::InstKind::SLoad(slot) => {
2947                            // Re-emit SLOAD. CALL operands are materialized in a
2948                            // tight sequence with no intervening store, so the
2949                            // storage slot reads the same value as the original
2950                            // load (same recompute contract as MLOAD above).
2951                            self.emit_value_fresh(func, *slot);
2952                            self.asm.emit_op(op::SLOAD);
2953                            self.scheduler.stack.pop();
2954                            self.scheduler.stack.push(val);
2955                        }
2956                        other => {
2957                            // For other instruction results that aren't spilled and can't be
2958                            // re-executed, this is a bug - the lowering
2959                            // should have ensured all CALL operands are
2960                            // either immediates, spilled, or re-executable instructions.
2961                            panic!(
2962                                "emit_value_fresh: unhandled instruction kind {other:?} for value {val:?}. \
2963                                 CALL operands should be immediates, spilled values, GAS, or MLOAD."
2964                            );
2965                        }
2966                    }
2967                }
2968            }
2969            crate::mir::Value::Undef(_) => {
2970                // Undef values shouldn't appear in CALL operands
2971                panic!(
2972                    "emit_value_fresh: unexpected undef value {val:?}. \
2973                     CALL operands should be concrete values."
2974                );
2975            }
2976            crate::mir::Value::Error(_) => {
2977                // A lowering error fails compilation before codegen runs.
2978                panic!("emit_value_fresh: error sentinel {val:?} reached the backend");
2979            }
2980        }
2981    }
2982
2983    fn emit_fresh_binary(
2984        &mut self,
2985        func: &Function,
2986        result: ValueId,
2987        a: ValueId,
2988        b: ValueId,
2989        opcode: u8,
2990        commutative: bool,
2991    ) {
2992        if commutative {
2993            self.emit_value_fresh(func, a);
2994            self.emit_value_fresh(func, b);
2995        } else {
2996            // EVM binary opcodes consume `a` from the top of stack and `b`
2997            // from the word below, matching the normal binary emitter.
2998            self.emit_value_fresh(func, b);
2999            self.emit_value_fresh(func, a);
3000        }
3001        self.asm.emit_op(opcode);
3002        self.scheduler.stack.pop();
3003        self.scheduler.stack.pop();
3004        self.scheduler.stack.push(result);
3005    }
3006
3007    /// Emits a binary operation with result tracking and liveness awareness.
3008    /// If an operand is still live after this instruction, we DUP it before it gets consumed.
3009    #[allow(clippy::too_many_arguments)]
3010    fn emit_binary_op_with_result(
3011        &mut self,
3012        func: &Function,
3013        a: ValueId,
3014        b: ValueId,
3015        opcode: u8,
3016        result: Option<ValueId>,
3017        liveness: &Liveness,
3018        block: BlockId,
3019        inst_idx: usize,
3020    ) {
3021        // Check if operands are still live after this instruction
3022        let a_is_live = !liveness.is_dead_after(a, block, inst_idx);
3023
3024        // Special case: same operand used twice (e.g., a + a, a - a)
3025        if a == b {
3026            self.emit_value(func, a);
3027            self.spill_top_value_if_live(func, liveness, block, inst_idx, a);
3028            // DUP for the second operand
3029            self.asm.emit_op(op::DUP1);
3030            self.scheduler.stack.dup(1);
3031            self.asm.emit_op(opcode);
3032            self.scheduler.instruction_executed(2, result);
3033            return;
3034        }
3035
3036        // Check if either operand is already on stack as an untracked value
3037        let a_can_emit = self.scheduler.can_emit_value(a, func);
3038        let b_can_emit = self.scheduler.can_emit_value(b, func);
3039        let has_untracked = self.scheduler.has_untracked_on_top();
3040        let has_untracked_at_1 = self.scheduler.has_untracked_at_depth(1);
3041
3042        if !a_can_emit && b_can_emit && has_untracked {
3043            // a is an untracked value on top of stack, emit b, then SWAP
3044            self.emit_value(func, b);
3045            self.spill_top_value_if_live(func, liveness, block, inst_idx, b);
3046            self.asm.emit_op(op::SWAP1);
3047            self.scheduler.stack_swapped();
3048        } else if a_can_emit && !b_can_emit && has_untracked {
3049            // b is an untracked value on top of stack, emit a on top
3050            self.emit_value(func, a);
3051            // Spill a if live-after (it's now at depth 0)
3052            if a_is_live && !Self::is_rematerializable_value(func, a) {
3053                self.spill_value_if_needed(func, a);
3054            }
3055        } else if !a_can_emit && b_can_emit && has_untracked_at_1 {
3056            // a is an untracked value at depth 1, b is tracked on top
3057            // Stack is [b, a_untracked], need [a, b]
3058            self.asm.emit_op(op::SWAP1);
3059            self.scheduler.stack_swapped();
3060        } else {
3061            // Normal case: emit b first (bottom), then a (top)
3062            self.emit_value(func, b);
3063            self.spill_top_value_if_live(func, liveness, block, inst_idx, b);
3064            self.emit_value(func, a);
3065            // Spill a if live-after (it's now at depth 0)
3066            if a_is_live && !Self::is_rematerializable_value(func, a) {
3067                self.spill_value_if_needed(func, a);
3068            }
3069        }
3070
3071        self.asm.emit_op(opcode);
3072        self.scheduler.instruction_executed(2, result);
3073    }
3074
3075    /// Emits a unary operation with result tracking and liveness awareness.
3076    /// If the operand is still live after this instruction, we spill it after emitting.
3077    #[allow(clippy::too_many_arguments)]
3078    fn emit_unary_op_with_result(
3079        &mut self,
3080        func: &Function,
3081        a: ValueId,
3082        opcode: u8,
3083        result: Option<ValueId>,
3084        liveness: &Liveness,
3085        block: BlockId,
3086        inst_idx: usize,
3087    ) {
3088        self.emit_value(func, a);
3089        self.spill_top_value_if_live(func, liveness, block, inst_idx, a);
3090
3091        self.asm.emit_op(opcode);
3092        self.scheduler.instruction_executed(1, result);
3093    }
3094
3095    /// Emits a store operation with liveness awareness.
3096    /// If the value operand is still live after this instruction, we spill it after emitting
3097    /// to preserve it for later use.
3098    #[allow(clippy::too_many_arguments)]
3099    fn emit_store_op_live_aware(
3100        &mut self,
3101        func: &Function,
3102        addr: ValueId,
3103        val: ValueId,
3104        opcode: u8,
3105        liveness: &Liveness,
3106        block: BlockId,
3107        inst_idx: usize,
3108    ) {
3109        // Check if addr is still live after this instruction
3110        let addr_is_live = !liveness.is_dead_after(addr, block, inst_idx);
3111
3112        // Emit val
3113        self.emit_value(func, val);
3114        self.spill_top_value_if_live(func, liveness, block, inst_idx, val);
3115
3116        // Emit addr
3117        self.emit_operand(func, addr);
3118        // Spill addr if live-after (it's now at depth 0)
3119        if addr_is_live && !Self::is_rematerializable_value(func, addr) {
3120            self.spill_value_if_needed(func, addr);
3121        }
3122
3123        self.asm.emit_op(opcode);
3124        self.scheduler.instruction_executed(2, None);
3125    }
3126
3127    /// Emits a copy-style instruction (no result) with liveness awareness.
3128    /// `operands` are pushed in order, so the last one ends up on top of the
3129    /// stack; any operand still live after this instruction is spilled before
3130    /// the instruction consumes it, preserving it for later uses.
3131    fn emit_copy_op_live_aware(
3132        &mut self,
3133        func: &Function,
3134        operands: &[ValueId],
3135        opcode: u8,
3136        liveness: &Liveness,
3137        block: BlockId,
3138        inst_idx: usize,
3139    ) {
3140        for (i, &op) in operands.iter().enumerate() {
3141            if i == 0 {
3142                self.emit_value(func, op);
3143            } else {
3144                // Repeated operands need their own stack item each.
3145                self.emit_operand(func, op);
3146            }
3147            self.spill_top_value_if_live(func, liveness, block, inst_idx, op);
3148        }
3149
3150        self.asm.emit_op(opcode);
3151        self.scheduler.instruction_executed(operands.len(), None);
3152    }
3153
3154    /// Emits a ternary operation.
3155    fn emit_ternary_op(
3156        &mut self,
3157        func: &Function,
3158        a: ValueId,
3159        b: ValueId,
3160        c: ValueId,
3161        opcode: u8,
3162        result: Option<ValueId>,
3163    ) {
3164        self.emit_value(func, c);
3165        self.emit_operand(func, b);
3166        self.emit_operand(func, a);
3167        self.asm.emit_op(opcode);
3168        self.scheduler.instruction_executed(3, result);
3169    }
3170
3171    /// Generates a parallel copy.
3172    ///
3173    /// Phi copies move values from source to destination. The destination is typically
3174    /// a phi result that needs to be available in the successor block. We handle this
3175    /// by spilling the source value to the destination's spill slot.
3176    fn generate_copy(
3177        &mut self,
3178        func: &Function,
3179        copy: &ParallelCopy,
3180        temps: &mut FxHashMap<u32, ValueId>,
3181    ) {
3182        // Handle source: either a MIR value or a temporary
3183        match &copy.src {
3184            CopySource::Value(val) => {
3185                self.emit_operand(func, *val);
3186            }
3187            CopySource::Temp(temp_id) => {
3188                // Temporaries are tracked in our temps map with their ValueId
3189                if let Some(&temp_val) = temps.get(temp_id) {
3190                    // DUP the temp value to top of stack
3191                    if let Some(depth) = self.scheduler.stack.find(temp_val) {
3192                        let dup_n = (depth + 1) as u8;
3193                        self.asm.emit_op(op::dup(dup_n));
3194                        self.scheduler.stack.dup(dup_n);
3195                    }
3196                }
3197            }
3198        }
3199
3200        // Handle destination: either a MIR value or a temporary
3201        match &copy.dst {
3202            CopyDest::Value(dst_val) => {
3203                // Spill the value on top of stack to the destination's spill slot
3204                // This allows the successor block to reload it
3205                let slot = self.scheduler.spills.allocate(*dst_val);
3206                self.emit_spill_slot_addr(func, slot);
3207                self.scheduler.stack.push_unknown();
3208                self.asm.emit_op(op::MSTORE);
3209                self.scheduler.stack.pop(); // pop the untracked offset
3210                self.scheduler.stack.pop(); // pop the value
3211                self.scheduler.spills.mark_stored(*dst_val);
3212            }
3213            CopyDest::Temp(temp_id) => {
3214                // Mark this temporary as defined - it's now on the stack
3215                // Get the ValueId of the value currently on top
3216                if let Some(val_on_top) = self.scheduler.stack.top() {
3217                    temps.insert(*temp_id, val_on_top);
3218                }
3219            }
3220        }
3221    }
3222
3223    /// Pops all remaining values from the stack.
3224    /// This ensures the stack is empty before control flow transfer to another block.
3225    fn pop_all_stack_values(&mut self) {
3226        while self.scheduler.stack_depth() > 0 {
3227            self.asm.emit_op(op::POP);
3228            self.scheduler.stack.pop();
3229        }
3230    }
3231
3232    fn emit_internal_return(&mut self, func: &Function, values: &[ValueId]) {
3233        let return_base = 64 + (func.params.len() as u64) * 32;
3234        for (i, &value) in values.iter().enumerate() {
3235            self.emit_operand(func, value);
3236            self.emit_current_internal_frame_addr(return_base + (i as u64) * 32);
3237            self.asm.emit_op(op::MSTORE);
3238            self.scheduler.stack.pop();
3239        }
3240
3241        self.pop_all_stack_values();
3242        self.emit_current_internal_frame_addr(0);
3243        self.asm.emit_op(op::MLOAD);
3244        self.asm.emit_op(op::JUMP);
3245    }
3246
3247    /// Generates bytecode for a terminator.
3248    fn generate_terminator(
3249        &mut self,
3250        func: &Function,
3251        term: &Terminator,
3252        fallthrough: Option<BlockId>,
3253        preserve_stack: bool,
3254    ) {
3255        match term {
3256            Terminator::Jump(target) => {
3257                // Pop any remaining values from the stack before jumping.
3258                // Each block normally starts with an empty stack, so we must
3259                // clean the stack before jumping — unless this edge preserves
3260                // its live stack into a single-predecessor target.
3261                if Some(*target) == fallthrough {
3262                    if !preserve_stack {
3263                        self.pop_all_stack_values();
3264                    }
3265                    return;
3266                }
3267                if !preserve_stack {
3268                    self.pop_all_stack_values();
3269                }
3270                self.asm.emit_push_label(self.block_labels[target]);
3271                self.asm.emit_op(op::JUMP);
3272            }
3273
3274            Terminator::Branch { condition, then_block, else_block } => {
3275                // Emit the condition first (it's still on the stack)
3276                self.emit_value(func, *condition);
3277
3278                // Now pop all OTHER values (condition is on top, keep it)
3279                // We do this by tracking that condition was just pushed and emitting POPs for
3280                // everything else. When this edge preserves its stack into the hot arm, the
3281                // values beneath the condition are the carried layout: leave them in place.
3282                if !preserve_stack {
3283                    while self.scheduler.depth() > 1 {
3284                        // SWAP to get unwanted value to top, then POP
3285                        self.asm.emit_op(op::SWAP1);
3286                        self.scheduler.stack_swapped();
3287                        self.asm.emit_op(op::POP);
3288                        self.scheduler.stack.pop();
3289                    }
3290                }
3291
3292                match fallthrough {
3293                    Some(next) if *else_block == next => {
3294                        // JUMPI consumes the condition; false falls through to `else_block`.
3295                        self.asm.emit_push_label(self.block_labels[then_block]);
3296                        self.asm.emit_op(op::JUMPI);
3297                        self.scheduler.stack.pop(); // condition consumed by JUMPI
3298                    }
3299                    Some(next) if *then_block == next => {
3300                        // Invert the condition so true falls through to `then_block`.
3301                        self.asm.emit_op(op::ISZERO);
3302                        self.scheduler.instruction_executed_untracked(1);
3303                        self.asm.emit_push_label(self.block_labels[else_block]);
3304                        self.asm.emit_op(op::JUMPI);
3305                        self.scheduler.stack.pop(); // inverted condition consumed by JUMPI
3306                    }
3307                    _ => {
3308                        // Neither target falls through. Route the likely-hot
3309                        // edge through JUMPI (16 gas) and leave the cold
3310                        // revert path on the trailing unconditional jump,
3311                        // instead of paying JUMPI + JUMP (24 gas) on the hot
3312                        // path.
3313                        if Self::block_is_cold(func, *then_block)
3314                            && !Self::block_is_cold(func, *else_block)
3315                        {
3316                            self.asm.emit_op(op::ISZERO);
3317                            self.scheduler.instruction_executed_untracked(1);
3318                            self.asm.emit_push_label(self.block_labels[else_block]);
3319                            self.asm.emit_op(op::JUMPI);
3320                            self.scheduler.stack.pop(); // inverted condition consumed by JUMPI
3321
3322                            self.asm.emit_push_label(self.block_labels[then_block]);
3323                            self.asm.emit_op(op::JUMP);
3324                        } else {
3325                            // JUMPI consumes the condition
3326                            self.asm.emit_push_label(self.block_labels[then_block]);
3327                            self.asm.emit_op(op::JUMPI);
3328                            self.scheduler.stack.pop(); // condition consumed by JUMPI
3329
3330                            self.asm.emit_push_label(self.block_labels[else_block]);
3331                            self.asm.emit_op(op::JUMP);
3332                        }
3333                    }
3334                }
3335            }
3336
3337            Terminator::Switch { value, default, cases } => {
3338                let mut operands = Vec::with_capacity(cases.len() + 1);
3339                operands.push(*value);
3340                operands.extend(cases.iter().map(|(case_val, _)| *case_val));
3341                self.spill_values_before_stack_clear(func, &operands);
3342
3343                // Pop all stack values first (live-out values are already spilled)
3344                self.pop_all_stack_values();
3345
3346                // Emit the switch value (will reload from spill if needed)
3347                self.emit_value(func, *value);
3348
3349                for (case_val, target) in cases {
3350                    // DUP the value, compare, jump if equal
3351                    self.asm.emit_op(op::DUP1);
3352                    self.scheduler.stack.dup(1);
3353                    self.emit_operand(func, *case_val);
3354                    self.asm.emit_op(op::EQ);
3355                    self.scheduler.instruction_executed_untracked(2);
3356                    self.asm.emit_push_label(self.block_labels[target]);
3357                    self.asm.emit_op(op::JUMPI);
3358                    self.scheduler.instruction_executed(1, None); // JUMPI consumes condition
3359                }
3360
3361                // Pop the value and jump to default
3362                self.asm.emit_op(op::POP);
3363                self.scheduler.stack.pop();
3364                if Some(*default) != fallthrough {
3365                    self.asm.emit_push_label(self.block_labels[default]);
3366                    self.asm.emit_op(op::JUMP);
3367                }
3368            }
3369
3370            Terminator::Return { values } => {
3371                if self.in_internal_function {
3372                    self.emit_internal_return(func, values);
3373                    return;
3374                }
3375
3376                assert!(values.is_empty(), "external ABI returns with values must use ReturnData");
3377                self.asm.emit_push(U256::ZERO);
3378                self.asm.emit_push(U256::ZERO);
3379                self.asm.emit_op(op::RETURN);
3380            }
3381
3382            Terminator::Revert { offset, size } => {
3383                self.emit_value(func, *size);
3384                self.emit_operand(func, *offset);
3385                self.asm.emit_op(op::REVERT);
3386            }
3387
3388            Terminator::ReturnData { offset, size } => {
3389                debug_assert!(!self.in_internal_function);
3390                self.emit_value(func, *size);
3391                self.emit_operand(func, *offset);
3392                self.asm.emit_op(op::RETURN);
3393            }
3394
3395            Terminator::Stop => {
3396                if self.in_internal_function {
3397                    self.emit_internal_return(func, &[]);
3398                } else {
3399                    self.asm.emit_op(op::STOP);
3400                }
3401            }
3402
3403            Terminator::SelfDestruct { recipient } => {
3404                self.emit_value(func, *recipient);
3405                self.asm.emit_op(op::SELFDESTRUCT);
3406            }
3407
3408            Terminator::Invalid => {
3409                self.asm.emit_op(op::INVALID);
3410            }
3411        }
3412    }
3413}
3414
3415impl Default for EvmCodegen {
3416    fn default() -> Self {
3417        Self::new(EvmCodegenConfig::default())
3418    }
3419}
3420
3421/// The artifact produced by the EVM backend: deployment and runtime bytecode.
3422#[derive(Clone, Debug, Default)]
3423pub struct EvmArtifact {
3424    /// Deployment (init) bytecode that, when run, returns the runtime code.
3425    pub deployment: Vec<u8>,
3426    /// Runtime bytecode, i.e. the code stored on-chain.
3427    pub runtime: Vec<u8>,
3428}
3429
3430impl crate::backend::Backend for EvmCodegen {
3431    type Output = EvmArtifact;
3432
3433    fn name(&self) -> &str {
3434        "evm"
3435    }
3436
3437    fn lower_module(&mut self, module: &mut Module) -> EvmArtifact {
3438        let (deployment, runtime) = self.generate_deployment_bytecode(module);
3439        EvmArtifact { deployment, runtime }
3440    }
3441}
3442
3443#[cfg(test)]
3444mod tests {
3445    use super::*;
3446    use crate::lower;
3447    use solar_config::{CompileOpts, UnstableOpts};
3448    use solar_interface::{Session, sym};
3449    use solar_sema::Compiler;
3450    use std::{ops::ControlFlow, path::PathBuf};
3451
3452    /// Helper to compile Solidity source to bytecode, returning Result.
3453    fn compile_source(source: &str) -> Result<Vec<u8>, String> {
3454        compile_source_with_stack_schedule(source, false)
3455    }
3456
3457    /// Compiles Solidity source to runtime bytecode, optionally enabling the
3458    /// experimental EVM IR `StackSchedule` bridge pass.
3459    fn compile_source_with_stack_schedule(
3460        source: &str,
3461        evm_ir_stack_schedule: bool,
3462    ) -> Result<Vec<u8>, String> {
3463        let opts = CompileOpts {
3464            unstable: UnstableOpts { codegen: true, ..Default::default() },
3465            ..Default::default()
3466        };
3467        let sess = Session::builder().with_buffer_emitter(Default::default()).opts(opts).build();
3468        let mut compiler = Compiler::new(sess);
3469
3470        // Parse
3471        let parse_result = compiler.enter_mut(|c| -> solar_interface::Result<_> {
3472            let mut ctx = c.parse();
3473            let file = c
3474                .sess()
3475                .source_map()
3476                .new_source_file(PathBuf::from("test.sol"), source.to_string())
3477                .unwrap();
3478            ctx.add_file(file);
3479            ctx.parse();
3480            Ok(())
3481        });
3482        if parse_result.is_err() {
3483            return Err("Parse error".to_string());
3484        }
3485
3486        // Lower and codegen
3487        compiler.enter_mut(|c| -> Result<Vec<u8>, String> {
3488            let ControlFlow::Continue(()) = c.lower_asts().map_err(|_| "Lower AST error")? else {
3489                return Err("Lower AST break".to_string());
3490            };
3491            let ControlFlow::Continue(()) = c.analysis().map_err(|_| "Analysis error")? else {
3492                return Err("Analysis break".to_string());
3493            };
3494
3495            let gcx = c.gcx();
3496            for (contract_id, contract) in gcx.hir.contracts_enumerated() {
3497                if contract.name.name == sym::Test {
3498                    let mut module = lower::lower_contract(gcx, contract_id);
3499                    let config =
3500                        EvmCodegenConfig { evm_ir_stack_schedule, ..EvmCodegenConfig::from(gcx) };
3501                    let mut codegen = EvmCodegen::new(config);
3502                    let bytecode = codegen.generate_module(&mut module);
3503                    return Ok(bytecode);
3504                }
3505            }
3506            Err("Contract 'Test' not found".to_string())
3507        })
3508    }
3509
3510    #[test]
3511    fn test_local_var_in_conditional_ice() {
3512        // Minimal repro for stack underflow ICE:
3513        // 1. Read storage into local variable
3514        // 2. Use local variable in conditional check
3515        // 3. Use local variable inside the conditional body
3516        let source = r#"
3517            // SPDX-License-Identifier: MIT
3518            pragma solidity ^0.8.0;
3519            contract Test {
3520                uint256 public value;
3521                function test() public {
3522                    uint256 v = value;
3523                    if (v != 0) value = v - 1;
3524                }
3525            }
3526        "#;
3527
3528        let result = compile_source(source);
3529        assert!(result.is_ok(), "Compilation failed: {:?}", result.err());
3530        let bytecode = result.unwrap();
3531        assert!(!bytecode.is_empty(), "Bytecode should not be empty");
3532    }
3533
3534    #[test]
3535    fn test_direct_storage_in_conditional_works() {
3536        // This works: directly referencing storage in both condition and body
3537        let source = r#"
3538            // SPDX-License-Identifier: MIT
3539            pragma solidity ^0.8.0;
3540            contract Test {
3541                uint256 public value;
3542                function test() public {
3543                    if (value != 0) value = value - 1;
3544                }
3545            }
3546        "#;
3547
3548        let result = compile_source(source);
3549        assert!(result.is_ok(), "Compilation failed: {:?}", result.err());
3550    }
3551
3552    #[test]
3553    fn test_phi_value_used_after_if_else() {
3554        // Test case for phi node handling when a variable is assigned in both
3555        // if/else branches and then used after the if/else.
3556        // This pattern is common in Uniswap V2 and similar contracts.
3557        let source = r#"
3558            // SPDX-License-Identifier: MIT
3559            pragma solidity ^0.8.0;
3560            contract Test {
3561                uint256 public totalSupply;
3562                function mint() external returns (uint256 liquidity) {
3563                    if (totalSupply == 0) {
3564                        liquidity = 1;
3565                    } else {
3566                        liquidity = 2;
3567                    }
3568                    totalSupply += liquidity;
3569                }
3570            }
3571        "#;
3572
3573        let result = compile_source(source);
3574        assert!(result.is_ok(), "Compilation failed: {:?}", result.err());
3575        let bytecode = result.unwrap();
3576        assert!(!bytecode.is_empty(), "Bytecode should not be empty");
3577    }
3578
3579    #[test]
3580    fn test_phi_value_used_multiple_times_after_if_else() {
3581        // Test case where the phi result is used multiple times after the if/else
3582        let source = r#"
3583            // SPDX-License-Identifier: MIT
3584            pragma solidity ^0.8.0;
3585            contract Test {
3586                uint256 public totalSupply;
3587                function mint() external returns (uint256 result) {
3588                    uint256 liquidity;
3589                    if (totalSupply == 0) {
3590                        liquidity = 1;
3591                    } else {
3592                        liquidity = 2;
3593                    }
3594                    totalSupply += liquidity;
3595                    uint256 x = liquidity * 2;
3596                    result = x + liquidity;
3597                }
3598            }
3599        "#;
3600
3601        let result = compile_source(source);
3602        assert!(result.is_ok(), "Compilation failed: {:?}", result.err());
3603        let bytecode = result.unwrap();
3604        assert!(!bytecode.is_empty(), "Bytecode should not be empty");
3605    }
3606
3607    /// Differential check for the experimental EVM IR `StackSchedule` bridge
3608    /// flag: for each sample contract, compiling with `evm_ir_stack_schedule`
3609    /// OFF (the default) and ON must produce byte-for-byte identical runtime
3610    /// bytecode. The bridge feeds the scheduler operand-cleared IR, where the
3611    /// pass is a verified near no-op, and `optimize_with_evm_ir` additionally
3612    /// guards the scheduled module behind the verifier oracle and a code-equality
3613    /// check — so turning the flag on can never diverge or produce invalid code.
3614    #[test]
3615    fn stack_schedule_bridge_flag_is_bytecode_neutral() {
3616        let samples = [
3617            // Simple storage read + conditional store.
3618            r#"
3619                // SPDX-License-Identifier: MIT
3620                pragma solidity ^0.8.0;
3621                contract Test {
3622                    uint256 public value;
3623                    function test() public {
3624                        uint256 v = value;
3625                        if (v != 0) value = v - 1;
3626                    }
3627                }
3628            "#,
3629            // Phi merge whose result is used after the if/else.
3630            r#"
3631                // SPDX-License-Identifier: MIT
3632                pragma solidity ^0.8.0;
3633                contract Test {
3634                    uint256 public totalSupply;
3635                    function mint() external returns (uint256 liquidity) {
3636                        if (totalSupply == 0) {
3637                            liquidity = 1;
3638                        } else {
3639                            liquidity = 2;
3640                        }
3641                        totalSupply += liquidity;
3642                    }
3643                }
3644            "#,
3645            // A small loop with arithmetic, exercising multiple blocks.
3646            r#"
3647                // SPDX-License-Identifier: MIT
3648                pragma solidity ^0.8.0;
3649                contract Test {
3650                    function sum(uint256 n) public pure returns (uint256 acc) {
3651                        for (uint256 i = 0; i < n; i++) {
3652                            acc += i;
3653                        }
3654                    }
3655                }
3656            "#,
3657        ];
3658
3659        for source in samples {
3660            let off = compile_source_with_stack_schedule(source, false);
3661            let on = compile_source_with_stack_schedule(source, true);
3662            let off = off.expect("baseline compilation should succeed");
3663            let on = on.expect("stack-schedule compilation should succeed");
3664            assert!(!off.is_empty(), "baseline bytecode should not be empty");
3665            assert_eq!(
3666                off, on,
3667                "enabling evm_ir_stack_schedule changed produced bytecode for sample:\n{source}"
3668            );
3669        }
3670    }
3671
3672    #[test]
3673    fn test_phi_with_ternary_in_branch() {
3674        // Complex phi case with nested ternary operators
3675        let source = r#"
3676            // SPDX-License-Identifier: MIT
3677            pragma solidity ^0.8.0;
3678            contract Test {
3679                uint256 public totalSupply;
3680                uint256 public reserve0;
3681                uint256 public reserve1;
3682
3683                function mint() external returns (uint256 liquidity) {
3684                    uint256 amount0 = 100;
3685                    uint256 amount1 = 200;
3686
3687                    if (totalSupply == 0) {
3688                        liquidity = amount0 * amount1;
3689                    } else {
3690                        uint256 l1 = (amount0 * totalSupply) / reserve0;
3691                        uint256 l2 = (amount1 * totalSupply) / reserve1;
3692                        liquidity = l1 < l2 ? l1 : l2;
3693                    }
3694
3695                    totalSupply += liquidity;
3696                }
3697            }
3698        "#;
3699
3700        let result = compile_source(source);
3701        assert!(result.is_ok(), "Compilation failed: {:?}", result.err());
3702        let bytecode = result.unwrap();
3703        assert!(!bytecode.is_empty(), "Bytecode should not be empty");
3704    }
3705}