Skip to main content

revmc_codegen/bytecode/passes/
block_analysis.rs

1//! Abstract stack interpretation for resolving dynamic jump targets and constant propagation.
2//!
3//! This pass builds a basic-block CFG and runs worklist-based abstract interpretation to a
4//! fixpoint, propagating abstract stack states across blocks. It resolves jump targets that the
5//! block-local pass `block_analysis_local` (which interprets each block independently) cannot
6//! handle — including internal function return patterns where multiple callers push different
7//! return addresses.
8//!
9//! After the fixpoint converges, the known-constant operands at each instruction are persisted
10//! as [`OperandSnapshot`]s so that later passes (e.g. code generation) can query the
11//! known-constant value of stack operands.
12//!
13//! ## Abstract domain
14//!
15//! Per-value lattice (ascending order):
16//! - `Const(U256Imm)` — a single known constant.
17//! - `ConstSet(ConstSetIdx)` — multiple known constants (interned, sorted, deduplicated).
18//! - `Top` — reachable but unknown.
19//!
20//! Per-block lattice:
21//! - `Bottom` — block not yet reached.
22//! - `Known(Vec<AbsValue>)` — reached with a known stack state (top-aligned; when predecessors have
23//!   different stack heights, the shorter stack is bottom-padded with `Top`).
24//!
25//! ## Soundness
26//!
27//! When the fixpoint does not converge (iteration cap reached) or unresolved `Top` jumps remain,
28//! a transitive predecessor analysis invalidates suspect resolutions that may be based on
29//! incomplete information, ensuring that only sound jump targets are reported as resolved.
30
31use super::StackSection;
32use crate::bytecode::{Bytecode, Inst, InstFlags, Interner, U256Imm};
33use bitvec::vec::BitVec;
34use either::Either;
35use oxc_index::{IndexVec, index_vec};
36use revm_bytecode::opcode as op;
37use smallvec::SmallVec;
38use std::{cmp::Ordering, collections::VecDeque, ops::Range};
39
40oxc_index::define_nonmax_u32_index_type! {
41    /// Index into the interned constant-set pool.
42    pub(crate) struct ConstSetIdx;
43}
44crate::bytecode::impl_index_display!(ConstSetIdx, "{}");
45
46/// Per-instruction snapshot of abstract operand values.
47///
48/// Stored in stack order: index 0 is the deepest operand, last element is TOS.
49/// Only the instruction's inputs are stored, not the entire stack.
50pub(crate) type OperandSnapshot = SmallVec<[AbsValue; 4]>;
51
52/// Bundles input and output snapshots for recording during abstract interpretation.
53#[derive(Clone, Default)]
54pub(crate) struct Snapshots {
55    /// Pre-instruction input operand snapshots.
56    pub(crate) inputs: IndexVec<Inst, OperandSnapshot>,
57    /// Post-instruction output snapshot (single value per instruction).
58    pub(crate) outputs: IndexVec<Inst, Option<AbsValue>>,
59}
60
61impl Snapshots {
62    /// Restores snapshots for the given instructions from a previously saved copy.
63    pub(crate) fn restore_from(&mut self, insts: impl Iterator<Item = Inst>, saved: &Self) {
64        for inst in insts {
65            self.inputs[inst].clone_from(&saved.inputs[inst]);
66            self.outputs[inst] = saved.outputs[inst];
67        }
68    }
69}
70
71/// Abstract value on the stack.
72#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
73pub(crate) enum AbsValue {
74    Const(U256Imm),
75    /// Multiple known constant values (interned, sorted, deduplicated).
76    ConstSet(ConstSetIdx),
77    /// Top value (unknown concrete value).
78    #[default]
79    Top,
80}
81
82impl AbsValue {
83    /// Returns the single constant if this is `Const`, or `None` otherwise.
84    pub(crate) fn as_const(self) -> Option<U256Imm> {
85        match self {
86            Self::Const(v) => Some(v),
87            _ => None,
88        }
89    }
90}
91
92/// Constant-set interner used by the abstract interpreter.
93struct ConstSetInterner {
94    interner: Interner<ConstSetIdx, Box<[U256Imm]>>,
95}
96
97impl ConstSetInterner {
98    fn new() -> Self {
99        Self { interner: Interner::new() }
100    }
101
102    /// Returns the constants in the given set.
103    fn get(&self, idx: ConstSetIdx) -> &[U256Imm] {
104        self.interner.get(idx)
105    }
106
107    /// Returns the set of known constants for an abstract value, or `None` if `Top`.
108    fn abs_const_set<'a>(&'a self, val: &'a AbsValue) -> Option<&'a [U256Imm]> {
109        match val {
110            AbsValue::Top => None,
111            AbsValue::Const(v) => Some(std::slice::from_ref(v)),
112            AbsValue::ConstSet(idx) => Some(self.get(*idx)),
113        }
114    }
115
116    /// Interns a sorted, deduplicated set and returns the corresponding `AbsValue`.
117    fn intern_set(&mut self, set: &[U256Imm]) -> AbsValue {
118        match set.len() {
119            0 => unreachable!("empty const set"),
120            1 => AbsValue::Const(set[0]),
121            _ => AbsValue::ConstSet(self.interner.intern(set.into())),
122        }
123    }
124
125    /// Lattice join: two values merge to their least upper bound.
126    fn join(&mut self, a: AbsValue, b: AbsValue) -> AbsValue {
127        match (a, b) {
128            (AbsValue::Top, _) | (_, AbsValue::Top) => AbsValue::Top,
129            (AbsValue::Const(x), AbsValue::Const(y)) if x == y => AbsValue::Const(x),
130            _ => {
131                let a = self.abs_const_set(&a).unwrap();
132                let b = self.abs_const_set(&b).unwrap();
133                // Sorted merge with dedup.
134                let mut merged = SmallVec::<[U256Imm; 8]>::new();
135                let (mut i, mut j) = (0, 0);
136                while i < a.len() && j < b.len() {
137                    match a[i].cmp(&b[j]) {
138                        Ordering::Less => {
139                            merged.push(a[i]);
140                            i += 1;
141                        }
142                        Ordering::Greater => {
143                            merged.push(b[j]);
144                            j += 1;
145                        }
146                        Ordering::Equal => {
147                            merged.push(a[i]);
148                            i += 1;
149                            j += 1;
150                        }
151                    }
152                }
153                merged.extend_from_slice(&a[i..]);
154                merged.extend_from_slice(&b[j..]);
155                self.intern_set(&merged)
156            }
157        }
158    }
159}
160
161/// Maximum abstract stack depth tracked by the analysis. Top-aligned joins across paths with
162/// differing stack heights pad the shorter stack with `Top` at the bottom; on CFG cycles this
163/// padding can grow without bound. Clamping discards only those bottom `Top` entries, so no
164/// precision is lost.
165///
166/// Instructions that access deeper than this (e.g. Amsterdam DUPN/SWAPN up to depth 236)
167/// treat the out-of-range slot as `Top` rather than aborting block interpretation.
168const MAX_ABS_STACK_DEPTH: usize = 64;
169
170/// Abstract state at the entry of a block.
171#[derive(Clone, Debug)]
172enum BlockState {
173    /// Block has not been reached yet.
174    Bottom,
175    /// Block has been reached with a known stack state (top-aligned).
176    Known(Vec<AbsValue>),
177}
178
179impl BlockState {
180    /// Join another incoming state into this one. Returns `true` if the state changed.
181    ///
182    /// When stack heights differ, the stacks are top-aligned and the shorter one is
183    /// bottom-padded with `Top`. This handles the common Solidity dispatch pattern where
184    /// the "no selector match" fallthrough leaves an extra item on the stack that the
185    /// fallback ignores.
186    fn join(&mut self, incoming: &[AbsValue], sets: &mut ConstSetInterner) -> bool {
187        match self {
188            Self::Bottom => {
189                let start = incoming.len().saturating_sub(MAX_ABS_STACK_DEPTH);
190                *self = Self::Known(incoming[start..].to_vec());
191                true
192            }
193            Self::Known(existing) => {
194                let new_len = existing.len().max(incoming.len());
195                let mut changed = false;
196
197                // Clamp to MAX_ABS_STACK_DEPTH by only joining the top portion;
198                // elements below that are unreachable by any EVM instruction and
199                // discarding them preserves soundness.
200                let join_len = new_len.min(MAX_ABS_STACK_DEPTH);
201
202                // Resize existing to join_len: pad at bottom with Top or truncate.
203                if existing.len() < join_len {
204                    let pad = join_len - existing.len();
205                    existing.splice(0..0, std::iter::repeat_n(AbsValue::Top, pad));
206                    changed = true;
207                } else if existing.len() > join_len {
208                    existing.drain(..existing.len() - join_len);
209                    changed = true;
210                }
211
212                // Join element-wise, top-aligned. Both stacks have their top at the end.
213                // `incoming` may be longer than join_len — we only look at its top portion.
214                let incoming_start = incoming.len().saturating_sub(join_len);
215                let incoming_top = &incoming[incoming_start..];
216                // incoming_top.len() <= join_len. If shorter, bottom positions get Top.
217                let pad = join_len - incoming_top.len();
218                for i in 0..join_len {
219                    let inc = if i < pad { AbsValue::Top } else { incoming_top[i - pad] };
220                    let joined = sets.join(existing[i], inc);
221                    if joined != existing[i] {
222                        existing[i] = joined;
223                        changed = true;
224                    }
225                }
226
227                changed
228            }
229        }
230    }
231}
232
233oxc_index::define_nonmax_u32_index_type! {
234    /// A block index in the CFG.
235    pub(crate) struct Block;
236}
237crate::bytecode::impl_index_display!(Block, "bb{}");
238
239/// FIFO worklist with deduplication.
240struct Worklist {
241    queue: VecDeque<Block>,
242    in_queue: BitVec,
243}
244
245impl Worklist {
246    fn new(size: usize) -> Self {
247        Self { queue: VecDeque::new(), in_queue: BitVec::repeat(false, size) }
248    }
249
250    fn push(&mut self, id: Block) {
251        let idx = id.index();
252        if !self.in_queue[idx] {
253            self.in_queue.set(idx, true);
254            self.queue.push_back(id);
255        }
256    }
257
258    fn pop(&mut self) -> Option<Block> {
259        let id = self.queue.pop_front()?;
260        self.in_queue.set(id.index(), false);
261        Some(id)
262    }
263}
264
265/// A basic block in the CFG.
266pub(crate) struct BlockData {
267    /// Instruction index range (exclusive end). All instructions in this range are live.
268    pub(crate) insts: Range<Inst>,
269    /// Predecessor block IDs.
270    pub(crate) preds: SmallVec<[Block; 4]>,
271    /// Successor block IDs.
272    pub(crate) succs: SmallVec<[Block; 4]>,
273}
274
275impl BlockData {
276    #[inline]
277    pub(crate) fn terminator(&self) -> Inst {
278        self.insts.end - 1
279    }
280
281    /// Returns the instruction range as `Range<usize>` for indexing into raw arrays.
282    #[inline]
283    pub(crate) fn insts(
284        &self,
285    ) -> impl DoubleEndedIterator<Item = Inst> + ExactSizeIterator + use<> {
286        (self.insts.start.index()..self.insts.end.index()).map(Inst::from_usize)
287    }
288}
289
290/// Resolved jump including compile-time condition information.
291#[derive(Clone, Debug)]
292struct JumpResolution {
293    target: JumpTarget,
294    condition: JumpCondition,
295}
296
297/// Resolved jump target kind after fixpoint.
298#[derive(Clone, Debug)]
299enum JumpTarget {
300    /// Not yet observed.
301    Bottom,
302    /// One or more known constant target instruction indices.
303    Resolved(SmallVec<[Inst; 4]>),
304    /// Known constant but invalid target.
305    Invalid,
306    /// Unknown target.
307    Top,
308}
309
310#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
311enum JumpCondition {
312    #[default]
313    Unknown,
314    AlwaysTrue,
315    AlwaysFalse,
316}
317
318impl JumpResolution {
319    fn new(target: JumpTarget) -> Self {
320        Self { target, condition: JumpCondition::Unknown }
321    }
322
323    fn bottom() -> Self {
324        Self::new(JumpTarget::Bottom)
325    }
326
327    fn invalid() -> Self {
328        Self::new(JumpTarget::Invalid)
329    }
330
331    fn top() -> Self {
332        Self::new(JumpTarget::Top)
333    }
334
335    fn resolved(targets: SmallVec<[Inst; 4]>) -> Self {
336        Self::new(JumpTarget::Resolved(targets))
337    }
338
339    /// Creates a resolved target with a single constant.
340    fn single(inst: Inst) -> Self {
341        Self::resolved(SmallVec::from_elem(inst, 1))
342    }
343
344    /// Returns the single resolved target, if exactly one.
345    fn as_single(&self) -> Option<Inst> {
346        match &self.target {
347            JumpTarget::Resolved(targets) if targets.len() == 1 => Some(targets[0]),
348            _ => None,
349        }
350    }
351
352    fn with_condition(mut self, condition: JumpCondition) -> Self {
353        self.condition = condition;
354        self
355    }
356
357    fn is_top(&self) -> bool {
358        matches!(self.target, JumpTarget::Top)
359    }
360
361    fn is_resolved(&self) -> bool {
362        matches!(self.target, JumpTarget::Resolved(_) | JumpTarget::Invalid)
363    }
364}
365
366/// CFG for abstract interpretation.
367#[derive(Default)]
368pub(crate) struct Cfg {
369    pub(crate) blocks: IndexVec<Block, BlockData>,
370    /// Maps instruction index to block ID. `None` for dead-code instructions.
371    pub(crate) inst_to_block: IndexVec<Inst, Option<Block>>,
372}
373
374impl Bytecode<'_> {
375    /// Ensures `self.snapshots` is sized for the current instruction count.
376    fn init_snapshots(&mut self) {
377        let n = self.insts.len();
378        self.snapshots.inputs.resize(n, SmallVec::new());
379        self.snapshots.outputs.resize(n, None);
380    }
381
382    /// Block-local jump resolution: interpret each block independently to discover
383    /// jumps resolvable from block-local computation alone.
384    ///
385    /// Also initializes `self.snapshots`.
386    #[instrument(name = "local_jumps", level = "debug", skip_all)]
387    #[inline(never)]
388    pub(crate) fn block_analysis_local(&mut self) {
389        self.init_snapshots();
390
391        let empty_sets = ConstSetInterner::new();
392        let mut resolved = Vec::new();
393        let mut stack = Vec::new();
394        let trace_logs = enabled!(tracing::Level::TRACE);
395
396        for bid in self.cfg.blocks.indices() {
397            let block = &self.cfg.blocks[bid];
398
399            // Compute required entry depth for this block using stack section analysis.
400            let section =
401                StackSection::from_stack_io(block.insts().map(|i| self.insts[i].stack_io()));
402
403            // Interpret the block with `Top` as inputs.
404            stack.clear();
405            stack.resize(section.inputs as usize, AbsValue::Top);
406            if !self.interpret_block(block.insts(), &mut stack) {
407                continue;
408            }
409
410            // Check if the block's terminator is an unresolved jump.
411            // We interpret every block to initialize `snapshots`, so we check this after.
412            let block = &self.cfg.blocks[bid];
413            let term_inst = block.terminator();
414            let term = &self.insts[term_inst];
415            if !term.is_jump() || term.flags.contains(InstFlags::STATIC_JUMP) {
416                continue;
417            }
418
419            let target = self.resolve_jump(term_inst, &empty_sets);
420            let Some(target_inst) = target.as_single() else { continue };
421
422            // Log non-adjacent resolutions (not simple PUSH+JUMP).
423            if trace_logs
424                && let is_adjacent = (term_inst > 0 && {
425                    let prev_inst = term_inst - 1;
426                    let prev = &self.insts[prev_inst];
427                    matches!(prev.opcode, op::PUSH0..=op::PUSH32)
428                        && !prev.is_dead_code()
429                        && block.insts.contains(&prev_inst)
430                })
431                && !is_adjacent
432            {
433                trace!(%term_inst, %target_inst, pc = self.pc(term_inst), "resolved non-adjacent jump");
434            }
435
436            resolved.push((term_inst, target));
437        }
438
439        let newly_resolved = self.commit_resolved_jumps(&resolved);
440        debug!(newly_resolved, "finished");
441        self.recompute_has_dynamic_jumps();
442    }
443
444    /// Runs abstract stack interpretation to resolve additional jump targets.
445    ///
446    /// Also computes and stores per-instruction stack snapshots for constant propagation.
447    #[instrument(name = "ba", level = "debug", skip_all)]
448    #[inline(never)]
449    pub(crate) fn block_analysis(&mut self, local_snapshots: &Snapshots) {
450        self.init_snapshots();
451        let (resolved, count) = self.run_abstract_interp(local_snapshots);
452
453        let has_const_condition =
454            resolved.iter().any(|(_, target)| target.condition != JumpCondition::Unknown);
455        if count > 0 || has_const_condition {
456            let newly_resolved = self.commit_resolved_jumps(&resolved);
457            debug!(newly_resolved, "resolved jumps");
458        }
459
460        self.recompute_has_dynamic_jumps();
461    }
462
463    /// Commits resolved jump targets by setting flags and data on the corresponding instructions.
464    ///
465    /// Returns the number of newly resolved jumps.
466    fn commit_resolved_jumps(&mut self, resolved: &[(Inst, JumpResolution)]) -> u32 {
467        let has_top_jump = resolved.iter().any(|(_, target)| target.is_top());
468
469        let mut newly_resolved = 0u32;
470        for &(jump_inst, ref target) in resolved {
471            let was_static = self.insts[jump_inst].flags.contains(InstFlags::STATIC_JUMP);
472            if was_static && target.condition == JumpCondition::Unknown {
473                continue;
474            }
475
476            if target.condition != JumpCondition::Unknown {
477                self.insts[jump_inst].flags |= InstFlags::CONST_JUMP_CONDITION;
478            }
479
480            match &target.target {
481                JumpTarget::Resolved(targets) => {
482                    self.insts[jump_inst]
483                        .flags
484                        .remove(InstFlags::INVALID_JUMP | InstFlags::MULTI_JUMP);
485                    if target.condition != JumpCondition::AlwaysFalse {
486                        for &target_inst in targets {
487                            debug_assert_eq!(
488                                self.insts[target_inst].opcode,
489                                op::JUMPDEST,
490                                "block_analysis resolved to non-JUMPDEST"
491                            );
492                            self.insts[target_inst].set_jumpdest_reachable();
493                        }
494                    }
495                    if targets.len() == 1 {
496                        self.multi_jump_targets.remove(&jump_inst);
497                        self.insts[jump_inst].flags |= InstFlags::STATIC_JUMP;
498                        self.insts[jump_inst].set_static_jump_target(targets[0]);
499                        trace!(%jump_inst, target_inst = %targets[0], "resolved jump");
500                    } else {
501                        self.insts[jump_inst].flags |=
502                            InstFlags::STATIC_JUMP | InstFlags::MULTI_JUMP;
503                        self.multi_jump_targets.insert(jump_inst, targets.clone());
504                        trace!(%jump_inst, n_targets = targets.len(), "resolved multi-target jump");
505                    }
506                    if !was_static {
507                        newly_resolved += 1;
508                    }
509                }
510                JumpTarget::Invalid => {
511                    self.multi_jump_targets.remove(&jump_inst);
512                    self.insts[jump_inst].flags |= InstFlags::STATIC_JUMP | InstFlags::INVALID_JUMP;
513                    self.insts[jump_inst].flags.remove(InstFlags::MULTI_JUMP);
514                    if !was_static {
515                        newly_resolved += 1;
516                    }
517                    trace!(%jump_inst, "resolved invalid jump");
518                }
519                JumpTarget::Bottom if !has_top_jump => {
520                    // Truly unreachable: no unresolved jumps remain, so this
521                    // code cannot be reached at runtime. Mark as invalid.
522                    self.multi_jump_targets.remove(&jump_inst);
523                    self.insts[jump_inst].flags |= InstFlags::STATIC_JUMP | InstFlags::INVALID_JUMP;
524                    self.insts[jump_inst].flags.remove(InstFlags::MULTI_JUMP);
525                    if !was_static {
526                        newly_resolved += 1;
527                    }
528                    trace!(%jump_inst, "unreachable jump");
529                }
530                JumpTarget::Bottom => {
531                    // Unreachable according to the analysis, but there are
532                    // unresolved (Top) jumps that might reach this code at
533                    // runtime. Leave as-is.
534                    trace!(%jump_inst, "unreachable jump (not marking, has_top_jump)");
535                }
536                JumpTarget::Top => {
537                    trace!(%jump_inst, "unresolved jump (Top)");
538                }
539            }
540        }
541        self.recompute_reachable_jumpdests();
542        newly_resolved
543    }
544
545    /// Refresh reachability and CFG-derived state after control-flow changes.
546    #[instrument(level = "debug", name = "re_cfg", skip_all)]
547    #[inline(never)]
548    pub(crate) fn refresh_cfg(&mut self) {
549        self.recompute_has_dynamic_jumps();
550        self.recompute_reachable_jumpdests(); // Depends on has_dynamic_jumps
551        self.mark_dead_code(); // Depends on reachable jumpdests
552        self.compress_redirects();
553        self.rebuild_cfg();
554    }
555
556    #[instrument(level = "debug", name = "redirects", skip_all)]
557    fn compress_redirects(&mut self) {
558        // Compress redirect chains so that earlier redirects (e.g. t2 -> t1) are
559        // updated when their target gets deduped in a later round (t1 -> t0).
560        // Without this, rebuild_cfg resolves edges only one hop, leaving stale
561        // intermediate targets in DedupKey.succs (preventing valid merges) and
562        // in translate.rs inst_entries (causing InvalidJump on valid bytecode).
563        for inst in self.redirects.keys().copied().collect::<Vec<_>>() {
564            let mut target = self.redirects[&inst];
565            let original = target;
566            while let Some(&next) = self.redirects.get(&target) {
567                target = next;
568            }
569            if target != original {
570                self.redirects.insert(inst, target);
571            }
572        }
573    }
574
575    /// Rebuild the basic-block CFG from the current instruction state.
576    #[instrument(level = "debug", name = "cfg", skip_all)]
577    fn rebuild_cfg(&mut self) {
578        let finish_block = |cfg: &mut Cfg, start: usize, end: usize| {
579            debug_assert!(start < end, "empty block range: {start}..{end}");
580            let bid = cfg.blocks.push(BlockData {
581                insts: Inst::from_usize(start)..Inst::from_usize(end),
582                preds: SmallVec::new(),
583                succs: SmallVec::new(),
584            });
585            for i in start..end {
586                cfg.inst_to_block[Inst::from_usize(i)] = Some(bid);
587            }
588        };
589
590        let n = self.insts.len();
591        assert_ne!(n, 0, "insts should never be empty");
592
593        let cfg = &mut self.cfg;
594        cfg.blocks.clear();
595        cfg.inst_to_block.clear();
596
597        // Identify block leaders.
598        let mut is_leader: BitVec = BitVec::repeat(false, n);
599        is_leader.set(0, true);
600
601        for (i, inst) in self.insts.raw.iter().enumerate() {
602            // Reachable JUMPDESTs must be leaders even when dead (e.g. deduped).
603            // Without this, `pending_leader` in the block-building loop below never
604            // fires for a dead JUMPDEST, and the preceding live block absorbs the
605            // next alive instruction — potentially a diverging terminator that
606            // poisons DSE.
607            if inst.is_reachable_jumpdest(self.has_dynamic_jumps) {
608                is_leader.set(i, true);
609            }
610            if inst.is_dead_code() {
611                continue;
612            }
613            if (inst.is_branching() || inst.is_diverging()) && i + 1 < n {
614                is_leader.set(i + 1, true);
615            }
616        }
617
618        // Build blocks. Dead instructions are skipped; `current_end` tracks the
619        // exclusive end of live instructions so block ranges never include dead code.
620        cfg.inst_to_block.resize(n, None);
621        let mut current_start = None;
622        let mut current_end = 0;
623        let mut pending_leader = false;
624
625        for i in 0..n {
626            if self.insts.raw[i].is_dead_code() {
627                // Propagate leader marks past dead code so that the next alive
628                // instruction starts a new block. Without this, a block ending
629                // in JUMPI whose fall-through was deduped (dead) would merge
630                // with the next alive instruction, potentially changing the
631                // block's terminator and poisoning DSE liveness.
632                pending_leader |= is_leader[i];
633                continue;
634            }
635
636            if is_leader[i] || pending_leader || current_start.is_none() {
637                if let Some(start) = current_start {
638                    finish_block(cfg, start, current_end);
639                }
640                current_start = Some(i);
641            }
642            pending_leader = false;
643            current_end = i + 1;
644        }
645
646        // Close the last block.
647        if let Some(start) = current_start {
648            finish_block(cfg, start, current_end);
649        }
650
651        // Build edges based on known control flow.
652        for bid in cfg.blocks.indices() {
653            let term = &self.insts[cfg.blocks[bid].terminator()];
654
655            // Resolve a target instruction through redirects to a CFG block.
656            let resolve = |target: Inst| -> Option<Block> {
657                let target = self.redirects.get(&target).copied().unwrap_or(target);
658                cfg.inst_to_block.get(target).copied().flatten()
659            };
660
661            // Fallthrough edge.
662            if term.can_fall_through()
663                && let Some(target_block) = resolve(cfg.blocks[bid].terminator() + 1)
664            {
665                cfg.blocks[target_block].preds.push(bid);
666                cfg.blocks[bid].succs.push(target_block);
667            }
668
669            // Jump edges: static single-target, or multi-jump.
670            let term_inst = cfg.blocks[bid].terminator();
671            if term.flags.contains(InstFlags::MULTI_JUMP)
672                && let Some(targets) = self.multi_jump_targets.get(&term_inst)
673            {
674                for &t in targets {
675                    if let Some(target_block) = resolve(t)
676                        && !cfg.blocks[bid].succs.contains(&target_block)
677                    {
678                        cfg.blocks[target_block].preds.push(bid);
679                        cfg.blocks[bid].succs.push(target_block);
680                    }
681                }
682            } else if term.is_static_jump()
683                && !term.flags.contains(InstFlags::INVALID_JUMP)
684                && let Some(target_block) = resolve(term.static_jump_target())
685                && !cfg.blocks[bid].succs.contains(&target_block)
686            {
687                cfg.blocks[target_block].preds.push(bid);
688                cfg.blocks[bid].succs.push(target_block);
689            }
690        }
691
692        assert_ne!(cfg.blocks.len(), 0, "should always build at least one block");
693    }
694
695    /// Run worklist-based abstract interpretation over the CFG.
696    ///
697    /// Returns a list of (jump_inst, resolved_target) pairs and the count of resolvable jumps.
698    /// Stack snapshots are recorded into `self.snapshots` during the fixpoint.
699    fn run_abstract_interp(
700        &mut self,
701        local_snapshots: &Snapshots,
702    ) -> (Vec<(Inst, JumpResolution)>, usize) {
703        let num_blocks = self.cfg.blocks.len();
704
705        // Initialize block states. Entry block starts with an empty stack.
706        let mut block_states: IndexVec<Block, BlockState> =
707            index_vec![BlockState::Bottom; num_blocks];
708        block_states[Block::from_usize(0)] = BlockState::Known(Vec::new());
709
710        // Collect unresolved jumps, plus static JUMPI instructions whose condition
711        // may become constant only after CFG fixpoint.
712        let mut jump_insts: Vec<Inst> = Vec::new();
713        for (i, inst) in self.insts.iter_enumerated() {
714            if inst.is_jump()
715                && (!inst.flags.contains(InstFlags::STATIC_JUMP)
716                    || (inst.opcode == op::JUMPI && !inst.has_const_jumpi_condition()))
717            {
718                jump_insts.push(i);
719            }
720        }
721
722        let mut const_sets = ConstSetInterner::new();
723        let (discovered_edges, converged) = self.run_fixpoint(&mut block_states, &mut const_sets);
724
725        // On non-convergence, all fixpoint-derived snapshots are potentially stale.
726        // Restore the safe block-local snapshots computed by `block_analysis_local`.
727        if !converged {
728            self.snapshots.restore_from(self.insts.indices(), local_snapshots);
729        }
730
731        // After convergence, resolve each dynamic jump from its snapshot operand.
732        let mut jump_targets: Vec<(Inst, JumpResolution)> = Vec::new();
733        let mut has_top_jump = false;
734        for &jump_inst in &jump_insts {
735            let target = self.resolve_jump(jump_inst, &const_sets);
736            if target.is_top() {
737                has_top_jump = true;
738            }
739            jump_targets.push((jump_inst, target));
740        }
741
742        // Invalidate resolutions that may be unsound due to incomplete analysis.
743        // When the fixpoint didn't converge, partially-discovered ConstSets may be
744        // incomplete, so we must conservatively invalidate them too.
745        if has_top_jump || !converged {
746            self.invalidate_suspect_jumps(
747                &mut jump_targets,
748                &block_states,
749                &discovered_edges,
750                local_snapshots,
751            );
752        }
753
754        let count = jump_targets.iter().filter(|(_, target)| target.is_resolved()).count();
755
756        (jump_targets, count)
757    }
758
759    fn resolve_jump(&self, jump_inst: Inst, const_sets: &ConstSetInterner) -> JumpResolution {
760        let snap = &self.snapshots.inputs[jump_inst];
761        let condition = if self.insts[jump_inst].opcode == op::JUMPI {
762            snap.first()
763                .map(|&value| self.resolve_jump_condition(value, const_sets))
764                .unwrap_or_default()
765        } else {
766            JumpCondition::Unknown
767        };
768
769        if condition == JumpCondition::AlwaysFalse {
770            return JumpResolution::single(jump_inst + 1)
771                .with_condition(JumpCondition::AlwaysFalse);
772        }
773
774        match snap.last() {
775            Some(&operand) => {
776                self.resolve_jump_operand(operand, const_sets).with_condition(condition)
777            }
778            None => {
779                trace!(%jump_inst, pc = self.pc(jump_inst), "jump in unreached block");
780                JumpResolution::bottom()
781            }
782        }
783    }
784
785    /// Resolves a jump target from the snapshot operand recorded during the fixpoint.
786    fn resolve_jump_operand(
787        &self,
788        operand: AbsValue,
789        const_sets: &ConstSetInterner,
790    ) -> JumpResolution {
791        match operand {
792            AbsValue::Const(imm) => {
793                let val = imm.get(&self.u256_interner.borrow());
794                match usize::try_from(val) {
795                    Ok(target_pc) if self.is_valid_jump(target_pc) => {
796                        JumpResolution::single(self.pc_to_inst(target_pc))
797                    }
798                    _ => JumpResolution::invalid(),
799                }
800            }
801            AbsValue::ConstSet(set_idx) => {
802                let consts = const_sets.get(set_idx);
803                let interner = self.u256_interner.borrow();
804                let mut targets = SmallVec::new();
805                for &imm in consts {
806                    let val = imm.get(&interner);
807                    match usize::try_from(val) {
808                        Ok(pc) if self.is_valid_jump(pc) => {
809                            targets.push(self.pc_to_inst(pc));
810                        }
811                        _ => {
812                            // Mixed valid + invalid: can't resolve since at runtime
813                            // the value might be any member of the set.
814                            return JumpResolution::top();
815                        }
816                    }
817                }
818                if !targets.is_empty() {
819                    JumpResolution::resolved(targets)
820                } else {
821                    JumpResolution::invalid()
822                }
823            }
824            AbsValue::Top => JumpResolution::top(),
825        }
826    }
827
828    fn resolve_jump_condition(
829        &self,
830        condition: AbsValue,
831        const_sets: &ConstSetInterner,
832    ) -> JumpCondition {
833        let consts = match condition {
834            AbsValue::Const(imm) => Either::Left(std::iter::once(imm)),
835            AbsValue::ConstSet(set_idx) => Either::Right(const_sets.get(set_idx).iter().copied()),
836            AbsValue::Top => return JumpCondition::Unknown,
837        };
838        let interner = self.u256_interner.borrow();
839        let mut saw_zero = false;
840        let mut saw_nonzero = false;
841        for imm in consts {
842            if imm.get(&interner).is_zero() {
843                saw_zero = true;
844            } else {
845                saw_nonzero = true;
846            }
847        }
848        match (saw_zero, saw_nonzero) {
849            (true, false) => JumpCondition::AlwaysFalse,
850            (false, true) => JumpCondition::AlwaysTrue,
851            _ => JumpCondition::Unknown,
852        }
853    }
854
855    fn local_jumpi_condition(&self, jump_inst: Inst, local_snapshots: &Snapshots) -> JumpCondition {
856        if self.insts[jump_inst].opcode != op::JUMPI {
857            return JumpCondition::Unknown;
858        }
859        let Some(condition) = local_snapshots.inputs[jump_inst].first().copied() else {
860            return JumpCondition::Unknown;
861        };
862        let Some(imm) = condition.as_const() else {
863            return JumpCondition::Unknown;
864        };
865        if imm.get(&self.u256_interner.borrow()).is_zero() {
866            JumpCondition::AlwaysFalse
867        } else {
868            JumpCondition::AlwaysTrue
869        }
870    }
871
872    /// Adds discovered dynamic-jump target edges for a block.
873    fn discover_jump_edges(
874        &self,
875        operand: AbsValue,
876        bid: Block,
877        const_sets: &ConstSetInterner,
878        discovered: &mut IndexVec<Block, SmallVec<[Block; 4]>>,
879        disc_preds: &mut IndexVec<Block, SmallVec<[Block; 4]>>,
880    ) {
881        let consts = match operand {
882            AbsValue::Const(imm) => Either::Left(std::iter::once(imm)),
883            AbsValue::ConstSet(set_idx) => Either::Right(const_sets.get(set_idx).iter().copied()),
884            AbsValue::Top => return,
885        };
886        let interner = self.u256_interner.borrow();
887        for imm in consts {
888            let Ok(target_pc) = usize::try_from(imm.get(&interner)) else { continue };
889            if !self.is_valid_jump(target_pc) {
890                continue;
891            }
892            let ti = self.pc_to_inst(target_pc);
893            if let Some(tb) = self.cfg.inst_to_block[ti]
894                && !discovered[bid].contains(&tb)
895            {
896                discovered[bid].push(tb);
897                disc_preds[tb].push(bid);
898            }
899        }
900    }
901
902    /// Invalidates jump resolutions and operand snapshots that may be unsound due to
903    /// unresolved `Top` jumps.
904    ///
905    /// When unresolved dynamic jumps remain, any reachable `JUMPDEST` block could
906    /// potentially be reached by those jumps with an arbitrary stack state. This means
907    /// resolutions and snapshots derived from the fixpoint may be based on incomplete
908    /// information.
909    ///
910    /// The conservative rule: seed every reachable `JUMPDEST` block as suspect,
911    /// propagate forward through the CFG and discovered edges, and invalidate all
912    /// resolved jumps and operand snapshots in suspect blocks.
913    fn invalidate_suspect_jumps(
914        &mut self,
915        jump_targets: &mut [(Inst, JumpResolution)],
916        block_states: &IndexVec<Block, BlockState>,
917        discovered_edges: &IndexVec<Block, SmallVec<[Block; 4]>>,
918        local_snapshots: &Snapshots,
919    ) {
920        let num_blocks = self.cfg.blocks.len();
921
922        // Seed: every reachable JUMPDEST block is suspect when Top jumps exist.
923        let mut suspect: BitVec = BitVec::repeat(false, num_blocks);
924        for bid in self.cfg.blocks.indices() {
925            if !matches!(block_states[bid], BlockState::Bottom)
926                && self.insts[self.cfg.blocks[bid].insts.start].is_jumpdest()
927            {
928                suspect.set(bid.index(), true);
929            }
930        }
931
932        // Propagate suspect flag forward through the CFG.
933        let mut propagate = Worklist::new(num_blocks);
934        for bid in self.cfg.blocks.indices() {
935            if suspect[bid.index()] {
936                propagate.push(bid);
937            }
938        }
939        while let Some(bid) = propagate.pop() {
940            for &succ in self.cfg.blocks[bid].succs.iter().chain(discovered_edges[bid].iter()) {
941                let si = succ.index();
942                if !suspect[si] {
943                    suspect.set(si, true);
944                    propagate.push(succ);
945                }
946            }
947        }
948
949        for (inst, target) in jump_targets.iter_mut() {
950            if let Some(bid) = self.cfg.inst_to_block[*inst]
951                && suspect[bid.index()]
952            {
953                let condition = self.local_jumpi_condition(*inst, local_snapshots);
954                if condition == JumpCondition::AlwaysFalse {
955                    *target = JumpResolution::single(*inst + 1).with_condition(condition);
956                    continue;
957                }
958                target.condition = condition;
959                if target.is_resolved() {
960                    *target = JumpResolution::top().with_condition(condition);
961                }
962            }
963        }
964
965        // Restore block-local snapshots in suspect blocks: the fixpoint may have recorded
966        // precise values that are unsound because undiscovered edges (from Top jumps)
967        // could bring in different stack states. However, block-local constants (computed
968        // by block_analysis_local without depending on the incoming stack) are always valid.
969        for bid in self.cfg.blocks.indices() {
970            if !suspect[bid.index()] {
971                continue;
972            }
973            self.snapshots.restore_from(self.cfg.blocks[bid].insts(), local_snapshots);
974        }
975    }
976
977    /// Run a worklist-based fixpoint to compute abstract block states.
978    ///
979    /// Returns `(discovered_edges, converged)`.
980    fn run_fixpoint(
981        &mut self,
982        block_states: &mut IndexVec<Block, BlockState>,
983        const_sets: &mut ConstSetInterner,
984    ) -> (IndexVec<Block, SmallVec<[Block; 4]>>, bool) {
985        let num_blocks = self.cfg.blocks.len();
986        let mut worklist = Worklist::new(num_blocks);
987        worklist.push(Block::from_usize(0));
988
989        // Discovered dynamic-jump target edges per block.
990        let mut discovered: IndexVec<Block, SmallVec<[Block; 4]>> =
991            IndexVec::from_vec(vec![SmallVec::new(); num_blocks]);
992        // Reverse map: discovered predecessors per block.
993        let mut disc_preds: IndexVec<Block, SmallVec<[Block; 4]>> =
994            IndexVec::from_vec(vec![SmallVec::new(); num_blocks]);
995
996        let max_iterations = num_blocks * 8;
997        let mut iterations = 0;
998        let mut converged = true;
999
1000        // Reusable buffer to avoid per-iteration allocations.
1001        let mut stack_buf: Vec<AbsValue> = Vec::with_capacity(MAX_ABS_STACK_DEPTH);
1002
1003        while let Some(bid) = worklist.pop() {
1004            iterations += 1;
1005            if iterations > max_iterations {
1006                converged = false;
1007                break;
1008            }
1009
1010            // Copy input state into reusable buffer.
1011            stack_buf.clear();
1012            match &block_states[bid] {
1013                BlockState::Known(s) => stack_buf.extend_from_slice(s),
1014                BlockState::Bottom => continue,
1015            };
1016
1017            let block = &self.cfg.blocks[bid];
1018            if !self.interpret_block(block.insts(), &mut stack_buf) {
1019                continue;
1020            }
1021            let block = &self.cfg.blocks[bid];
1022
1023            // Discover dynamic-jump target edges from the snapshot recorded above.
1024            let term_inst = block.terminator();
1025            let term = &self.insts[term_inst];
1026            if term.is_jump()
1027                && !term.flags.contains(InstFlags::STATIC_JUMP)
1028                && let Some(&operand) = self.snapshots.inputs[term_inst].last()
1029            {
1030                self.discover_jump_edges(
1031                    operand,
1032                    bid,
1033                    const_sets,
1034                    &mut discovered,
1035                    &mut disc_preds,
1036                );
1037            }
1038
1039            // Propagate to static CFG successors and discovered dynamic-jump targets.
1040            for &succ in block.succs.iter().chain(&discovered[bid]) {
1041                if block_states[succ].join(&stack_buf, const_sets) {
1042                    worklist.push(succ);
1043                }
1044            }
1045        }
1046
1047        debug!(
1048            "{msg} after {iterations} iterations (max={max_iterations})",
1049            msg = if converged { "converged" } else { "did not converge" },
1050        );
1051
1052        (discovered, converged)
1053    }
1054
1055    /// Interpret a sequence of instructions on the abstract stack.
1056    /// Returns `false` on stack underflow (conflict).
1057    ///
1058    /// The caller must pre-fill `stack` with the input state; on return it contains the output.
1059    /// Records per-instruction operand snapshots into `self.snapshots`.
1060    fn interpret_block(
1061        &mut self,
1062        insts: impl IntoIterator<Item = Inst>,
1063        stack: &mut Vec<AbsValue>,
1064    ) -> bool {
1065        for i in insts {
1066            let inst = &self.insts[i];
1067            if inst.is_dead_code() {
1068                continue;
1069            }
1070
1071            let (inp, out) = inst.stack_io();
1072            let inp = inp as usize;
1073            let out = out as usize;
1074
1075            // Record pre-instruction input operand snapshot (in stack order, TOS last).
1076            if inp > 0 {
1077                let start = stack.len().saturating_sub(inp);
1078                let snap = &mut self.snapshots.inputs[i];
1079                snap.clear();
1080                snap.extend_from_slice(&stack[start..]);
1081            }
1082
1083            match inst.opcode {
1084                op::PUSH0..=op::PUSH32 => {
1085                    stack.push(AbsValue::Const(inst.imm()));
1086                }
1087                op::POP => {
1088                    if stack.pop().is_none() {
1089                        return false;
1090                    }
1091                }
1092                op::DUP1..=op::DUP16 => {
1093                    let depth = (inst.opcode - op::DUP1 + 1) as usize;
1094                    if stack.len() < depth {
1095                        return false;
1096                    }
1097                    stack.push(stack[stack.len() - depth]);
1098                }
1099                op::SWAP1..=op::SWAP16 => {
1100                    let depth = (inst.opcode - op::SWAP1 + 1) as usize;
1101                    let len = stack.len();
1102                    if len < depth + 1 {
1103                        return false;
1104                    }
1105                    stack.swap(len - 1, len - 1 - depth);
1106                }
1107                op::DUPN => {
1108                    let depth = crate::decode_single(inst.imm_byte());
1109                    match depth {
1110                        Some(n) => {
1111                            let n = n as usize;
1112                            if stack.len() < n {
1113                                // Depth exceeds the tracked abstract stack (truncated by
1114                                // MAX_ABS_STACK_DEPTH). The slot is reachable at runtime
1115                                // but unknown abstractly.
1116                                stack.push(AbsValue::Top);
1117                            } else {
1118                                stack.push(stack[stack.len() - n]);
1119                            }
1120                        }
1121                        None => return false,
1122                    }
1123                }
1124                op::SWAPN => {
1125                    let depth = crate::decode_single(inst.imm_byte());
1126                    match depth {
1127                        Some(n) => {
1128                            let n = n as usize;
1129                            let len = stack.len();
1130                            if len < n + 1 {
1131                                // Deep slot beyond tracked abstract stack; TOS becomes Top
1132                                // and the deep slot (not tracked) is unchanged.
1133                                if let Some(tos) = stack.last_mut() {
1134                                    *tos = AbsValue::Top;
1135                                }
1136                            } else {
1137                                stack.swap(len - 1, len - 1 - n);
1138                            }
1139                        }
1140                        None => return false,
1141                    }
1142                }
1143                op::EXCHANGE => {
1144                    let pair = crate::decode_pair(inst.imm_byte());
1145                    match pair {
1146                        Some((n, m)) => {
1147                            let (n, m) = (n as usize, m as usize);
1148                            let len = stack.len();
1149                            if len < m + 1 {
1150                                // Deep slot beyond tracked abstract stack; the shallower
1151                                // slot (if tracked) becomes Top.
1152                                if len > n {
1153                                    stack[len - 1 - n] = AbsValue::Top;
1154                                }
1155                            } else {
1156                                stack.swap(len - 1 - n, len - 1 - m);
1157                            }
1158                        }
1159                        None => return false,
1160                    }
1161                }
1162                _ => {
1163                    if stack.len() < inp {
1164                        return false;
1165                    }
1166
1167                    // Try constant folding for common arithmetic, respecting the gas budget.
1168                    let result = if out > 0 && self.compiler_gas_used < self.compiler_gas_limit {
1169                        let inputs_slice = &stack[stack.len() - inp..];
1170                        let mut interner = self.u256_interner.borrow_mut();
1171
1172                        // Check gas cost before doing the actual fold.
1173                        let gas =
1174                            super::const_fold::const_fold_gas(inst.opcode, inputs_slice, &interner);
1175                        if let Some(cost) = gas
1176                            && self.compiler_gas_used.saturating_add(cost)
1177                                <= self.compiler_gas_limit
1178                        {
1179                            let folded = super::const_fold::try_const_fold(
1180                                inst,
1181                                inputs_slice,
1182                                &mut interner,
1183                                self.code.len(),
1184                            );
1185                            if folded.is_some() {
1186                                self.compiler_gas_used += cost;
1187                            }
1188                            folded
1189                        } else {
1190                            None
1191                        }
1192                    } else {
1193                        None
1194                    };
1195
1196                    // Pop inputs.
1197                    stack.truncate(stack.len() - inp);
1198
1199                    // Push outputs.
1200                    if let Some(folded) = result {
1201                        debug_assert_eq!(out, 1);
1202                        stack.push(folded);
1203                    } else {
1204                        stack.resize(stack.len() + out, AbsValue::Top);
1205                    }
1206                }
1207            }
1208
1209            // Record post-instruction output snapshot.
1210            // Skip SWAP/SWAPN/EXCHANGE: they modify two positions and have no single "output".
1211            if out > 0 && !matches!(inst.opcode, op::SWAP1..=op::SWAP16 | op::SWAPN | op::EXCHANGE)
1212            {
1213                self.snapshots.outputs[i] = stack.last().copied();
1214            }
1215
1216            #[cfg(test)]
1217            if inst.opcode == crate::TEST_SUSPEND {
1218                stack.fill(AbsValue::Top);
1219            }
1220        }
1221
1222        true
1223    }
1224}
1225
1226#[cfg(test)]
1227pub(crate) mod tests {
1228    use super::*;
1229    pub(crate) use crate::bytecode::Inst;
1230    pub(crate) use revm_primitives::{U256, hardfork::SpecId, hex};
1231
1232    pub(crate) fn analyze_hex(hex: &str) -> Bytecode<'static> {
1233        let code = hex::decode(hex.trim()).unwrap();
1234        analyze_code(code)
1235    }
1236
1237    pub(crate) fn analyze_code(code: Vec<u8>) -> Bytecode<'static> {
1238        analyze_code_with(code, Default::default())
1239    }
1240
1241    pub(crate) fn analyze_code_spec(code: Vec<u8>, spec_id: SpecId) -> Bytecode<'static> {
1242        crate::tests::init_tracing();
1243
1244        eprintln!("{}", hex::encode(&code));
1245        let mut bytecode = Bytecode::new(code, spec_id, None);
1246        bytecode.analyze().unwrap();
1247        eprintln!("{bytecode}");
1248        bytecode
1249    }
1250
1251    pub(crate) fn analyze_code_with(
1252        code: Vec<u8>,
1253        config: crate::bytecode::AnalysisConfig,
1254    ) -> Bytecode<'static> {
1255        crate::tests::init_tracing();
1256
1257        eprintln!("{}", hex::encode(&code));
1258        let mut bytecode = Bytecode::test(code);
1259        bytecode.config = config;
1260        bytecode.analyze().unwrap();
1261        eprintln!("{bytecode}");
1262        bytecode
1263    }
1264
1265    pub(crate) fn analyze_asm(src: &str) -> Bytecode<'static> {
1266        analyze_code(crate::parse_asm(src).unwrap())
1267    }
1268
1269    pub(crate) fn analyze_asm_spec(src: &str, spec_id: SpecId) -> Bytecode<'static> {
1270        analyze_code_spec(crate::parse_asm(src).unwrap(), spec_id)
1271    }
1272
1273    pub(crate) fn analyze_asm_with(
1274        src: &str,
1275        config: crate::bytecode::AnalysisConfig,
1276    ) -> Bytecode<'static> {
1277        analyze_code_with(crate::parse_asm(src).unwrap(), config)
1278    }
1279
1280    #[test]
1281    fn revert_sub_call_storage_oog() {
1282        let bytecode = analyze_hex(
1283            "60606040526000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063b28175c4146046578063c0406226146052575b6000565b3460005760506076565b005b34600057605c6081565b604051808215151515815260200191505060405180910390f35b600c6000819055505b565b600060896076565b600d600181905550600e600281905550600190505b905600a165627a7a723058202a8a75d7d795b5bcb9042fb18b283daa90b999a11ddec892f5487322",
1284        );
1285        assert!(bytecode.has_dynamic_jumps());
1286    }
1287
1288    #[test]
1289    fn revert_remote_sub_call_storage_oog() {
1290        let bytecode = analyze_hex(
1291            "608060405234801561001057600080fd5b506004361061002b5760003560e01c806373027f6d14610030575b600080fd5b61004a600480360381019061004591906101a9565b61004c565b005b6000808273ffffffffffffffffffffffffffffffffffffffff166040516024016040516020818303038152906040527fb28175c4000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffff83818316178352505050506040516100f69190610247565b6000604051808303816000865af19150503d8060008114610133576040519150601f19603f3d011682016040523d82523d6000602084013e610138565b606091505b509150915081600155505050565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006101768261014b565b9050919050565b6101868161016b565b811461019157600080fd5b50565b6000813590506101a38161017d565b92915050565b6000602082840312156101bf576101be610146565b5b60006101cd84828501610194565b91505092915050565b600081519050919050565b600081905092915050565b60005b8381101561020a5780820151818401526020810190506101ef565b60008484015250505050565b6000610221826101d6565b61022b81856101e1565b935061023b8185602086016101ec565b80840191505092915050565b60006102538284610216565b91508190509291505056fea2646970667358221220b4673c55c7b0268d7d118059e6509196d2185bb7fe040a7d3900f902c8542ea464736f6c63430008180033",
1292        );
1293        assert!(bytecode.has_dynamic_jumps());
1294    }
1295
1296    #[test]
1297    fn trans_storage_ok() {
1298        let contracts = [
1299            "366012575b600b5f6020565b5f5260205ff35b601c6160a75f6024565b6004565b5c90565b5d56",
1300            "60106001600a5f6012565b015f6016565b005b5c90565b5d56",
1301            "3033146033575b303303600e57005b601b5f35806001555f608d565b5f80808080305af1600255602e60016089565b600355005b603a5f6089565b8015608757604a600182035f608d565b5f80808080305af1156083576001606191035f608d565b5f80808080305af115608357607f60016078816089565b016001608d565b6006565b5f80fd5b005b5c90565b5d56",
1302            "60065f601d565b5f5560106001601d565b6001555f80808080335af1005b5c9056",
1303        ];
1304        let has_dynamic = [false, false, true, false];
1305        for (hex, &expected) in contracts.iter().zip(&has_dynamic) {
1306            let bytecode = analyze_hex(hex);
1307            assert_eq!(bytecode.has_dynamic_jumps(), expected, "contract: {hex}");
1308        }
1309    }
1310
1311    #[test]
1312    fn const_operand_basic() {
1313        // inst 0: PUSH1 0x42 -> stack: [0x42]
1314        // inst 1: PUSH1 0x01 -> stack: [0x42, 0x01]
1315        // inst 2: ADD        -> stack: [0x43]  (const-folded)
1316        // inst 3: PUSH1 0x00 -> stack: [0x43, 0x00]
1317        // inst 4: MSTORE     -> pops 2
1318        // inst 5: JUMPDEST
1319        // inst 6: STOP
1320        let bytecode = analyze_asm(
1321            "
1322            PUSH1 0x42
1323            PUSH1 0x01
1324            ADD
1325            PUSH1 0x00
1326            MSTORE
1327            JUMPDEST
1328            STOP
1329        ",
1330        );
1331        // At inst 2 (ADD), operand 0 (TOS) = 0x01, operand 1 = 0x42.
1332        assert_eq!(bytecode.const_operand(Inst::from_usize(2), 0), Some(U256::from(0x01)));
1333        assert_eq!(bytecode.const_operand(Inst::from_usize(2), 1), Some(U256::from(0x42)));
1334        // At inst 4 (MSTORE), operand 0 (TOS) = 0x00, operand 1 = 0x43 (folded ADD result).
1335        assert_eq!(bytecode.const_operand(Inst::from_usize(4), 0), Some(U256::from(0x00)));
1336        assert_eq!(bytecode.const_operand(Inst::from_usize(4), 1), Some(U256::from(0x43)));
1337    }
1338
1339    #[test]
1340    fn const_operand_dynamic() {
1341        // CALLDATALOAD pushes an unknown value -> const_operand should return None.
1342        let bytecode = analyze_asm(
1343            "
1344            PUSH0
1345            CALLDATALOAD
1346            PUSH0
1347            MSTORE
1348            STOP
1349        ",
1350        );
1351        // At inst 3 (MSTORE), operand 0 (TOS) = 0x00, operand 1 = unknown (CALLDATALOAD result).
1352        assert_eq!(bytecode.const_operand(Inst::from_usize(3), 0), Some(U256::ZERO));
1353        assert_eq!(bytecode.const_operand(Inst::from_usize(3), 1), None);
1354    }
1355
1356    #[test]
1357    fn const_output_basic() {
1358        // PUSH1 0x42 -> output[0] = 0x42
1359        // PUSH1 0x01 -> output[0] = 0x01
1360        // ADD        -> output[0] = 0x43 (const-folded)
1361        // PUSH0      -> output[0] = 0x00
1362        // MSTORE     -> no outputs
1363        // STOP
1364        let bytecode = analyze_asm(
1365            "
1366            PUSH1 0x42
1367            PUSH1 0x01
1368            ADD
1369            PUSH0
1370            MSTORE
1371            STOP
1372        ",
1373        );
1374        assert_eq!(bytecode.const_output(Inst::from_usize(0)), Some(U256::from(0x42)));
1375        assert_eq!(bytecode.const_output(Inst::from_usize(1)), Some(U256::from(0x01)));
1376        assert_eq!(bytecode.const_output(Inst::from_usize(2)), Some(U256::from(0x43)));
1377        assert_eq!(bytecode.const_output(Inst::from_usize(3)), Some(U256::ZERO));
1378        // MSTORE has no outputs.
1379        assert_eq!(bytecode.const_output(Inst::from_usize(4)), None);
1380    }
1381
1382    #[test]
1383    fn const_output_dynamic() {
1384        // CALLDATALOAD pushes an unknown -> output should be None.
1385        let bytecode = analyze_asm(
1386            "
1387            PUSH0
1388            CALLDATALOAD
1389            STOP
1390        ",
1391        );
1392        assert_eq!(bytecode.const_output(Inst::from_usize(0)), Some(U256::ZERO));
1393        assert_eq!(bytecode.const_output(Inst::from_usize(1)), None);
1394    }
1395
1396    /// DUPN, SWAPN, and EXCHANGE should propagate constants like their fixed counterparts.
1397    #[test]
1398    fn const_snapshot_eof_stack_ops() {
1399        // DUPN/SWAPN min index is 17, so we need 17 values on the stack.
1400        // 16 × PUSH0, PUSH1 0xAA, DUPN 17 (raw=0x80), STOP.
1401        let mut code: Vec<u8> = vec![op::PUSH0; 16];
1402        code.extend([op::PUSH1, 0xAA]); // inst 16: TOS = 0xAA
1403        code.extend([op::DUPN, 0x80]); // inst 17: DUPN 17 (dup bottom = 0x00)
1404        code.push(op::STOP); // inst 18
1405        let bytecode = analyze_code_spec(code, SpecId::AMSTERDAM);
1406        // DUPN duplicates the 17th item (bottom PUSH0 = 0x00).
1407        assert_eq!(bytecode.const_output(Inst::from_usize(17)), Some(U256::ZERO));
1408        // The PUSH1 0xAA is still there.
1409        assert_eq!(bytecode.const_output(Inst::from_usize(16)), Some(U256::from(0xAA)));
1410    }
1411
1412    #[test]
1413    fn snapshots_all_blocks() {
1414        // Blocks with unresolvable dynamic jumps must still get snapshots from
1415        // block_analysis_local. Here CALLDATALOAD produces an opaque jump target,
1416        // so the JUMP stays dynamic, but the block's other instructions should
1417        // still have snapshots populated.
1418        let bytecode = analyze_asm(
1419            "
1420            PUSH1 0x42          ; inst 0
1421            PUSH1 0x01          ; inst 1
1422            ADD                 ; inst 2: 0x42 + 0x01 = 0x43
1423            PUSH0               ; inst 3
1424            CALLDATALOAD        ; inst 4: opaque value
1425            JUMP                ; inst 5: dynamic, unresolvable
1426        target1:
1427            JUMPDEST            ; inst 6
1428            PUSH1 0x01          ; inst 7
1429            ADD                 ; inst 8
1430            STOP                ; inst 9
1431        target2:
1432            JUMPDEST            ; inst 10
1433            PUSH1 0x02          ; inst 11
1434            SUB                 ; inst 12
1435            STOP                ; inst 13
1436        ",
1437        );
1438        // The JUMP at inst 5 should remain dynamic.
1439        assert!(bytecode.has_dynamic_jumps, "expected unresolved dynamic jump");
1440        let jump = bytecode.inst(Inst::from_usize(5));
1441        assert!(jump.is_jump());
1442        assert!(!jump.flags.contains(InstFlags::STATIC_JUMP));
1443        // JUMP's TOS operand is Top (CALLDATALOAD result).
1444        assert_eq!(bytecode.const_operand(Inst::from_usize(5), 0), None);
1445
1446        // ADD 1
1447        assert_eq!(bytecode.const_operand(Inst::from_usize(2), 0), Some(U256::from(0x01)));
1448        assert_eq!(bytecode.const_operand(Inst::from_usize(2), 1), Some(U256::from(0x42)));
1449        assert_eq!(bytecode.const_output(Inst::from_usize(2)), Some(U256::from(0x43)));
1450
1451        // ADD 2
1452        assert_eq!(bytecode.const_operand(Inst::from_usize(8), 0), Some(U256::from(0x01)));
1453        assert_eq!(bytecode.const_operand(Inst::from_usize(8), 1), None);
1454        assert_eq!(bytecode.const_output(Inst::from_usize(8)), None);
1455
1456        // SUB
1457        assert_eq!(bytecode.const_operand(Inst::from_usize(12), 0), Some(U256::from(0x02)));
1458        assert_eq!(bytecode.const_operand(Inst::from_usize(12), 1), None);
1459        assert_eq!(bytecode.const_output(Inst::from_usize(12)), None);
1460    }
1461
1462    #[test]
1463    fn multi_target_jump() {
1464        // Internal function called from two sites with different return addresses.
1465        // The return JUMP at the end should resolve to Multi([ret1, ret2]).
1466        let bytecode = analyze_asm(
1467            "
1468            ; Call site 1.
1469            PUSH %ret1          ; push return address
1470            PUSH %func          ; push function entry
1471            JUMP                ; call function (static: PUSH+JUMP)
1472        ret1:
1473            JUMPDEST
1474            POP                 ; consume function result
1475            ; Call site 2.
1476            PUSH %ret2          ; push return address
1477            PUSH %func          ; push function entry
1478            JUMP                ; call function (static: PUSH+JUMP)
1479        ret2:
1480            JUMPDEST
1481            POP                 ; consume function result
1482            STOP
1483            ; Internal function.
1484        func:
1485            JUMPDEST
1486            PUSH1 0x42          ; push a result
1487            SWAP1               ; swap result and return address
1488            JUMP                ; return (dynamic)
1489        ",
1490        );
1491
1492        // The return JUMP (last inst before STOP's block) should be multi-target.
1493        let return_jump = bytecode
1494            .iter_insts()
1495            .find(|(_, d)| d.is_jump() && d.flags.contains(InstFlags::MULTI_JUMP));
1496        assert!(return_jump.is_some(), "expected a multi-target jump");
1497        let (rj_inst, _) = return_jump.unwrap();
1498        let targets = bytecode.multi_jump_targets(rj_inst).unwrap();
1499        assert_eq!(targets.len(), 2, "expected 2 targets, got {}", targets.len());
1500        // Verify both targets are JUMPDESTs.
1501        for &t in targets {
1502            assert_eq!(bytecode.inst(t).opcode, op::JUMPDEST);
1503        }
1504        // No dynamic jumps should remain.
1505        assert!(!bytecode.has_dynamic_jumps, "expected no dynamic jumps");
1506    }
1507
1508    /// Demonstrates a known unsoundness in `invalidate_suspect_jumps`.
1509    ///
1510    /// The invalidation pass only seeds suspicion from `Bottom + JUMPDEST` predecessors,
1511    /// but an unresolved dynamic jump can also reach a `Known` JUMPDEST block at runtime
1512    /// with a different stack than the fixpoint computed. This test constructs a case
1513    /// where the analysis incorrectly resolves a return JUMP to `Multi` even though an
1514    /// opaque (MLOAD-based) dynamic jump could reach the same function entry with an
1515    /// arbitrary return address.
1516    ///
1517    /// Layout:
1518    ///   Call site 1: PUSH ret1, PUSH fn, JUMP → fn returns to ret1
1519    ///   Call site 2: PUSH ret2, PUSH fn, JUMP → fn returns to ret2
1520    ///   Opaque jump: PUSH0, MLOAD, JUMP      → Top target, could be fn at runtime
1521    ///   fn:          JUMPDEST, PUSH 0x42, SWAP1, JUMP → return (should be Top)
1522    ///
1523    /// The fn JUMPDEST block is `Known([ConstSet({ret1, ret2})])` from the two static
1524    /// callers, so the return JUMP resolves to `Multi([ret1, ret2])`. But the opaque
1525    /// jump could call fn with any return address, making that resolution unsound.
1526    /// `invalidate_suspect_jumps` now seeds every reachable JUMPDEST block as suspect,
1527    /// so the return jump is correctly invalidated.
1528    #[test]
1529    fn known_jumpdest_unsound_resolution() {
1530        let bytecode = analyze_asm(
1531            "
1532            ; Call site 1.
1533            PUSH %ret1              ; pc=0
1534            PUSH %fn_entry          ; pc=2
1535            JUMP                    ; pc=4: -> fn_entry
1536        ret1:
1537            JUMPDEST                ; pc=5
1538            POP                     ; pc=6: drop result
1539            ; Call site 2.
1540            PUSH %ret2              ; pc=7
1541            PUSH %fn_entry          ; pc=9
1542            JUMP                    ; pc=11: -> fn_entry
1543        ret2:
1544            JUMPDEST                ; pc=12
1545            POP                     ; pc=13: drop result
1546            ; Opaque jump: loads target from memory (Top).
1547            PUSH0                   ; pc=14
1548            MLOAD                   ; pc=15: -> Top
1549            JUMP                    ; pc=16: dynamic, target = Top
1550            INVALID                 ; pc=17
1551            INVALID                 ; pc=18
1552            INVALID                 ; pc=19
1553            ; Internal function: pushes result, swaps with return addr, jumps back.
1554        fn_entry:
1555            JUMPDEST                ; pc=20
1556            PUSH1 0x42              ; pc=21: result
1557            SWAP1                   ; pc=23: swap result and return addr
1558            JUMP                    ; pc=24: return (UNSOUND: resolved to Multi)
1559        ",
1560        );
1561
1562        // The return JUMP at pc=24 should NOT be resolved — the opaque jump at
1563        // pc=16 can reach fn_entry with any return address. But the current
1564        // invalidation misses this because fn_entry's block is Known, not Bottom.
1565        let return_jump = bytecode.iter_insts().find(|(i, d)| bytecode.pc(*i) == 24 && d.is_jump());
1566        let (_, rj) = return_jump.unwrap();
1567        assert!(
1568            !rj.flags.contains(InstFlags::STATIC_JUMP),
1569            "unsound: return jump should not be resolved"
1570        );
1571    }
1572
1573    #[test]
1574    fn nested_call_return() {
1575        // Two-level call chain: outer calls wrapper, wrapper calls inner function.
1576        // Direct caller also calls the inner function directly.
1577        //
1578        //   call site 1 (direct):   PUSH ret1, PUSH inner, JUMP
1579        //   call site 2 (wrapper):  PUSH ret2, PUSH wrapper, JUMP
1580        //     wrapper:              PUSH ret_w, PUSH inner, JUMP
1581        //     ret_w:                SWAP1 POP JUMP  (returns to ret2)
1582        //
1583        // The inner function is called with different stack depths from the two
1584        // sites. Its return JUMP resolves to Multi([ret1, ret_w]). But the
1585        // wrapper's final JUMP (at ret_w) must recover the outer return address
1586        // from deep in the stack — which the analysis currently cannot resolve
1587        // because the top-aligned join pads it to Top.
1588        let bytecode = analyze_asm(
1589            "
1590            ; Call site 1: direct call to inner.
1591            PUSH %ret1              ; pc=0
1592            PUSH %inner             ; pc=2
1593            JUMP                    ; pc=4: -> inner
1594        ret1:
1595            JUMPDEST                ; pc=5
1596            POP                     ; pc=6: drop result
1597            ; Call site 2: call through wrapper.
1598            PUSH %ret2              ; pc=7
1599            PUSH1 0x42              ; pc=9: an argument
1600            PUSH %wrapper           ; pc=11
1601            JUMP                    ; pc=13: -> wrapper
1602        ret2:
1603            JUMPDEST                ; pc=14
1604            POP                     ; pc=15: drop result
1605            POP                     ; pc=16: drop arg
1606            STOP                    ; pc=17
1607            INVALID                 ; pc=18
1608            ; Wrapper function: calls inner, then returns to caller.
1609            ; Entry stack: [outer_ret, arg]
1610        wrapper:
1611            JUMPDEST                ; pc=19
1612            PUSH %ret_w             ; pc=20: push return addr for inner
1613            PUSH %inner             ; pc=22: push inner entry
1614            JUMP                    ; pc=24: -> inner
1615            INVALID                 ; pc=25
1616            INVALID                 ; pc=26
1617        ret_w:
1618            JUMPDEST                ; pc=27: inner returned here
1619            ; Stack: [outer_ret, arg, inner_result]
1620            POP                     ; pc=28: drop inner_result
1621            POP                     ; pc=29: drop arg
1622            JUMP                    ; pc=30: return to caller via outer_ret (dynamic)
1623            INVALID                 ; pc=31
1624            ; Inner function: pushes a result and returns.
1625            ; Entry stack: [..., ret_addr]
1626        inner:
1627            JUMPDEST                ; pc=32
1628            PUSH1 0x42              ; pc=33: push result
1629            SWAP1                   ; pc=35
1630            JUMP                    ; pc=36: jump to ret_addr
1631        ",
1632        );
1633
1634        // The wrapper return JUMP (pc=30) remains dynamic because the outer
1635        // return address is lost to Top during the top-aligned join.
1636        // Because an unresolved Top jump exists, the conservative invalidation
1637        // also invalidates the inner return JUMP — any reachable JUMPDEST
1638        // (including inner's entry) is suspect.
1639        assert!(bytecode.has_dynamic_jumps, "expected dynamic jumps to remain");
1640    }
1641
1642    /// Regression test: deep DUPN on Amsterdam must not cause the abstract interpreter to
1643    /// abort a block and leave a reachable JUMP as Bottom → INVALID_JUMP.
1644    #[test]
1645    fn amsterdam_deep_dupn_jump_not_invalid() {
1646        // Build a stack with 70 items, where the deepest is the final JUMP target.
1647        // A dispatcher JUMP is resolvable; a later JUMP in a block that uses DUPN 70
1648        // must also be correctly resolved (not marked INVALID_JUMP).
1649        let code = crate::parse_asm(
1650            "
1651            PUSH %target
1652            ; 69 × PUSH0
1653            PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0
1654            PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0
1655            PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0
1656            PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0
1657            PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0
1658            PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0
1659            PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0
1660            PUSH %resolved
1661            PUSH %dispatcher
1662            JUMP
1663        dispatcher:
1664            JUMPDEST
1665            JUMP
1666        resolved:
1667            JUMPDEST
1668            DUPN 70
1669            POP
1670        problem:
1671            JUMPDEST
1672            DUPN 70
1673            JUMP
1674        target:
1675            JUMPDEST
1676            STOP
1677        ",
1678        )
1679        .unwrap();
1680        let bytecode = analyze_code_spec(code, SpecId::AMSTERDAM);
1681
1682        // The "problem" JUMP must NOT be marked INVALID_JUMP.
1683        let jumps: Vec<_> = bytecode.iter_insts().filter(|(_, d)| d.opcode == op::JUMP).collect();
1684        for &(inst, data) in &jumps {
1685            assert!(
1686                !data.flags.contains(InstFlags::INVALID_JUMP),
1687                "JUMP inst={inst} pc={} incorrectly marked INVALID_JUMP",
1688                bytecode.pc(inst),
1689            );
1690        }
1691    }
1692
1693    #[test]
1694    fn hash_10k() {
1695        let code = fixture_entry_code(include_str!("../../../../../data/hash_10k.json"));
1696        let mut bytecode = Bytecode::test(code);
1697        bytecode.analyze().unwrap();
1698        assert!(!bytecode.has_dynamic_jumps, "expected all jumps to be resolved");
1699    }
1700
1701    fn fixture_entry_code(json: &str) -> Vec<u8> {
1702        let v: serde_json::Value = serde_json::from_str(json).unwrap();
1703        let case = v.as_object().unwrap().values().next().unwrap();
1704        let to = case["transaction"][0]["to"].as_str().unwrap();
1705        let code = case["pre"][to]["code"].as_str().unwrap().trim_start_matches("0x");
1706        revm_primitives::hex::decode(code).unwrap()
1707    }
1708}
1709
1710#[cfg(test)]
1711mod tests_edge_cases {
1712    use super::{tests::*, *};
1713
1714    /// Three callers to the same internal function. The return JUMP should
1715    /// resolve to Multi with three targets.
1716    #[test]
1717    fn multi_target_jump_three_callers() {
1718        let bytecode = analyze_asm(
1719            "
1720            ; Call site 1.
1721            PUSH %ret1
1722            PUSH %func
1723            JUMP
1724        ret1:
1725            JUMPDEST
1726            POP
1727            ; Call site 2.
1728            PUSH %ret2
1729            PUSH %func
1730            JUMP
1731        ret2:
1732            JUMPDEST
1733            POP
1734            ; Call site 3.
1735            PUSH %ret3
1736            PUSH %func
1737            JUMP
1738        ret3:
1739            JUMPDEST
1740            POP
1741            STOP
1742            ; Internal function.
1743        func:
1744            JUMPDEST
1745            PUSH1 0x42
1746            SWAP1
1747            JUMP
1748        ",
1749        );
1750
1751        let return_jump = bytecode
1752            .iter_insts()
1753            .find(|(_, d)| d.is_jump() && d.flags.contains(InstFlags::MULTI_JUMP));
1754        assert!(return_jump.is_some(), "expected a multi-target jump");
1755        let (rj_inst, _) = return_jump.unwrap();
1756        let targets = bytecode.multi_jump_targets(rj_inst).unwrap();
1757        assert_eq!(targets.len(), 3, "expected 3 targets, got {}", targets.len());
1758        assert!(!bytecode.has_dynamic_jumps, "expected no dynamic jumps");
1759    }
1760
1761    /// DUP and SWAP should preserve constant values through the stack.
1762    #[test]
1763    fn dup_swap_const_propagation() {
1764        // PUSH1 0xAA -> [0xAA]
1765        // PUSH1 0xBB -> [0xAA, 0xBB]
1766        // DUP2       -> [0xAA, 0xBB, 0xAA]
1767        // SWAP1      -> [0xAA, 0xAA, 0xBB]
1768        // PUSH0      -> [0xAA, 0xAA, 0xBB, 0x00]
1769        // MSTORE     -> pops (0x00, 0xBB), leaves [0xAA, 0xAA]
1770        // STOP
1771        let bytecode = analyze_asm(
1772            "
1773            PUSH1 0xAA          ; inst 0
1774            PUSH1 0xBB          ; inst 1
1775            DUP2                ; inst 2
1776            SWAP1               ; inst 3
1777            PUSH0               ; inst 4
1778            MSTORE              ; inst 5
1779            STOP                ; inst 6
1780        ",
1781        );
1782        // DUP2 output should be 0xAA.
1783        assert_eq!(bytecode.const_output(Inst::from_usize(2)), Some(U256::from(0xAA)));
1784        // At MSTORE (inst 5): TOS = 0x00, second = 0xBB (swapped back).
1785        assert_eq!(bytecode.const_operand(Inst::from_usize(5), 0), Some(U256::ZERO));
1786        assert_eq!(bytecode.const_operand(Inst::from_usize(5), 1), Some(U256::from(0xBB)));
1787    }
1788
1789    /// JUMPI with a constant condition should still record both operands.
1790    #[test]
1791    fn jumpi_const_condition_snapshot() {
1792        let bytecode = analyze_asm(
1793            "
1794            PUSH1 0x01              ; inst 0: condition = 1 (always taken)
1795            PUSH %target            ; inst 1: target
1796            JUMPI                   ; inst 2: static JUMPI
1797            STOP                    ; inst 3: fallthrough
1798        target:
1799            JUMPDEST                ; inst 4: target
1800            STOP                    ; inst 5
1801        ",
1802        );
1803        // The JUMPI should be resolved as static.
1804        let (jump_inst, jump) =
1805            bytecode.iter_insts().find(|(_, data)| data.opcode == op::JUMPI).unwrap();
1806        assert!(jump.flags.contains(InstFlags::STATIC_JUMP));
1807        assert_eq!(bytecode.inst(jump.static_jump_target()).opcode, op::JUMPDEST);
1808        assert_ne!(jump.static_jump_target(), jump_inst + 1);
1809        assert!(!bytecode.has_dynamic_jumps);
1810    }
1811
1812    /// A loop where the stack grows each iteration until clamped.
1813    /// Verifies the analysis converges without losing precision at the top.
1814    #[test]
1815    fn loop_stack_growth_clamping() {
1816        // bb0: PUSH1 0x42, then fall through to bb1.
1817        // bb1: JUMPDEST, PUSH1 0x01, PUSH1 bb1_pc, JUMP  (loop back, growing stack).
1818        //
1819        // The stack grows by 1 each iteration (net +1 from PUSH before the loop jump).
1820        // The abstract stack should clamp to MAX_ABS_STACK_DEPTH without conflict.
1821        let bytecode = analyze_asm(
1822            "
1823            PUSH1 0x42              ; pc=0: initial value
1824            PUSH1 0x01              ; pc=2: another
1825        loop_header:
1826            JUMPDEST                ; loop header (bb1)
1827            PUSH1 0x01              ; push each iteration
1828            PUSH %loop_header       ; loop target
1829            JUMP                    ; back-edge
1830        ",
1831        );
1832        // Should converge without panicking.
1833        // The JUMP should be static (resolved by adjacent PUSH+JUMP).
1834        assert!(!bytecode.has_dynamic_jumps);
1835    }
1836
1837    /// A single-instruction block (just JUMPDEST as the terminator) used as a
1838    /// jump target. This tests the edge case where block.len() == 1.
1839    #[test]
1840    fn single_inst_block_jump_target() {
1841        let bytecode = analyze_asm(
1842            "
1843            PUSH %target            ; pc=0
1844            JUMP                    ; static jump
1845            INVALID
1846            INVALID
1847        target:
1848            JUMPDEST                ; single-inst block
1849            STOP
1850        ",
1851        );
1852        assert!(!bytecode.has_dynamic_jumps);
1853    }
1854
1855    /// A JUMP with an invalid target (not a JUMPDEST) should be marked invalid.
1856    #[test]
1857    fn invalid_jump_target() {
1858        // The dynamic jump target points to a non-JUMPDEST instruction.
1859        // The function pushes a constant that isn't a valid JUMPDEST pc.
1860        let bytecode = analyze_asm(
1861            "
1862            PUSH1 0xFF              ; pc=0: push invalid target
1863            PUSH %func              ; pc=2: push function entry
1864            JUMP                    ; pc=4: -> func
1865            ; Function: swap and jump to the caller-provided target.
1866        func:
1867            JUMPDEST
1868            JUMP                    ; jump to 0xFF (invalid)
1869        ",
1870        );
1871        // The dynamic JUMP at pc=6 should be resolved as invalid.
1872        let jump_inst = bytecode
1873            .iter_insts()
1874            .find(|(_, d)| d.is_jump() && d.flags.contains(InstFlags::INVALID_JUMP));
1875        assert!(jump_inst.is_some(), "expected an invalid jump");
1876    }
1877
1878    #[test]
1879    fn jumpi_with_zero_condition_ignores_unknown_target() {
1880        let bytecode = analyze_asm(
1881            "
1882            PUSH0
1883            CALLDATASIZE
1884            JUMPI
1885            STOP
1886        target:
1887            JUMPDEST
1888            STOP
1889        ",
1890        );
1891
1892        let (_, jump) = bytecode.iter_insts().find(|(_, data)| data.opcode == op::JUMPI).unwrap();
1893        assert!(jump.flags.contains(InstFlags::STATIC_JUMP));
1894        assert!(!jump.flags.contains(InstFlags::INVALID_JUMP));
1895        assert_eq!(jump.static_jump_target(), Inst::from_usize(3));
1896        assert!(!bytecode.has_dynamic_jumps);
1897    }
1898
1899    #[test]
1900    fn jumpi_with_zero_condition_ignores_valid_target() {
1901        let bytecode = analyze_asm(
1902            "
1903            PUSH0
1904            PUSH %target
1905            JUMPI
1906            STOP
1907        target:
1908            JUMPDEST
1909            STOP
1910        ",
1911        );
1912
1913        let (_, jump) = bytecode.iter_insts().find(|(_, data)| data.opcode == op::JUMPI).unwrap();
1914        assert!(jump.flags.contains(InstFlags::STATIC_JUMP));
1915        assert!(!jump.flags.contains(InstFlags::INVALID_JUMP));
1916        assert_eq!(jump.static_jump_target(), Inst::from_usize(3));
1917        assert!(!bytecode.has_dynamic_jumps);
1918    }
1919
1920    #[test]
1921    fn jumpi_with_zero_condition_cfg_only_falls_through() {
1922        let bytecode = analyze_asm(
1923            "
1924            PUSH0
1925            PUSH %target
1926            JUMPI
1927            PUSH1 0xAA
1928            STOP
1929        target:
1930            JUMPDEST
1931            STOP
1932        ",
1933        );
1934
1935        let (jump_inst, jump) =
1936            bytecode.iter_insts().find(|(_, data)| data.opcode == op::JUMPI).unwrap();
1937        let jump_block = bytecode.cfg.inst_to_block[jump_inst].unwrap();
1938        let fallthrough_block = bytecode.cfg.inst_to_block[jump_inst + 1].unwrap();
1939        let target_inst = bytecode
1940            .iter_all_insts()
1941            .rev()
1942            .find(|(_, data)| data.opcode == op::JUMPDEST)
1943            .unwrap()
1944            .0;
1945
1946        assert!(jump.has_const_jumpi_condition());
1947        assert_eq!(jump.static_jump_target(), jump_inst + 1);
1948        assert_eq!(bytecode.cfg.blocks[jump_block].succs.as_slice(), &[fallthrough_block]);
1949        assert!(
1950            bytecode.cfg.inst_to_block[target_inst].is_none(),
1951            "never-taken JUMPI target should be dead"
1952        );
1953    }
1954
1955    #[test]
1956    fn jumpi_with_zero_condition_from_predecessor_rewrites_local_static_cfg() {
1957        let bytecode = analyze_asm(
1958            "
1959            PUSH0
1960            PUSH %branch
1961            JUMP
1962        branch:
1963            JUMPDEST
1964            PUSH %target
1965            JUMPI
1966            PUSH1 0xAA
1967            STOP
1968        target:
1969            JUMPDEST
1970            STOP
1971        ",
1972        );
1973
1974        let (jump_inst, jump) =
1975            bytecode.iter_insts().find(|(_, data)| data.opcode == op::JUMPI).unwrap();
1976        let jump_block = bytecode.cfg.inst_to_block[jump_inst].unwrap();
1977        let fallthrough_block = bytecode.cfg.inst_to_block[jump_inst + 1].unwrap();
1978        let target_inst = bytecode
1979            .iter_all_insts()
1980            .rev()
1981            .find(|(_, data)| data.opcode == op::JUMPDEST)
1982            .unwrap()
1983            .0;
1984
1985        assert!(jump.has_const_jumpi_condition());
1986        assert_eq!(jump.static_jump_target(), jump_inst + 1);
1987        assert_eq!(bytecode.cfg.blocks[jump_block].succs.as_slice(), &[fallthrough_block]);
1988        assert!(
1989            bytecode.cfg.inst_to_block[target_inst].is_none(),
1990            "locally resolved but never-taken JUMPI target should be dead"
1991        );
1992    }
1993
1994    #[test]
1995    fn jumpi_with_true_condition_cfg_only_jumps_to_target() {
1996        let bytecode = analyze_asm(
1997            "
1998            PUSH1 0x01
1999            PUSH %target
2000            JUMPI
2001            PUSH1 0xAA
2002            STOP
2003        target:
2004            JUMPDEST
2005            STOP
2006        ",
2007        );
2008
2009        let (jump_inst, jump) =
2010            bytecode.iter_insts().find(|(_, data)| data.opcode == op::JUMPI).unwrap();
2011        let jump_block = bytecode.cfg.inst_to_block[jump_inst].unwrap();
2012        let target_block = bytecode.cfg.inst_to_block[jump.static_jump_target()].unwrap();
2013
2014        assert!(jump.has_const_jumpi_condition());
2015        assert_ne!(jump.static_jump_target(), jump_inst + 1);
2016        assert_eq!(bytecode.cfg.blocks[jump_block].succs.as_slice(), &[target_block]);
2017        assert!(
2018            bytecode.cfg.inst_to_block[jump_inst + 1].is_none(),
2019            "always-taken JUMPI fallthrough should be dead"
2020        );
2021    }
2022
2023    #[test]
2024    fn jumpi_with_true_condition_invalid_target_has_no_fallthrough() {
2025        let bytecode = analyze_asm(
2026            "
2027            PUSH1 0x01
2028            PUSH1 0xFF
2029            JUMPI
2030            STOP
2031        ",
2032        );
2033
2034        let (jump_inst, jump) =
2035            bytecode.iter_insts().find(|(_, data)| data.opcode == op::JUMPI).unwrap();
2036        let jump_block = bytecode.cfg.inst_to_block[jump_inst].unwrap();
2037
2038        assert!(jump.has_const_jumpi_condition());
2039        assert!(jump.flags.contains(InstFlags::STATIC_JUMP | InstFlags::INVALID_JUMP));
2040        assert!(bytecode.cfg.blocks[jump_block].succs.is_empty());
2041        assert!(
2042            bytecode.cfg.inst_to_block[jump_inst + 1].is_none(),
2043            "always-taken invalid JUMPI fallthrough should be dead"
2044        );
2045    }
2046
2047    #[test]
2048    fn jumpi_with_true_condition_keeps_unknown_target_dynamic() {
2049        let bytecode = analyze_asm(
2050            "
2051            PUSH1 0x01
2052            CALLDATASIZE
2053            JUMPI
2054            STOP
2055        target:
2056            JUMPDEST
2057            STOP
2058        ",
2059        );
2060
2061        let (_, jump) = bytecode.iter_insts().find(|(_, data)| data.opcode == op::JUMPI).unwrap();
2062        assert!(!jump.flags.contains(InstFlags::STATIC_JUMP));
2063        assert!(jump.has_const_jumpi_condition());
2064        assert!(!jump.can_fall_through());
2065        assert!(bytecode.has_dynamic_jumps);
2066    }
2067
2068    /// Constant propagation through a diamond CFG (if-then-else merge).
2069    /// Both branches push the same constant, so the merge should preserve it.
2070    #[test]
2071    fn diamond_cfg_same_const() {
2072        let bytecode = analyze_asm(
2073            "
2074            PUSH1 0x42              ; push same const on both paths
2075            CALLDATASIZE            ; condition
2076            PUSH %then_pc           ; then target
2077            JUMPI                   ; branch
2078            ; Else: push same const.
2079            PUSH %merge
2080            JUMP                    ; -> merge
2081            ; Then:
2082        then_pc:
2083            JUMPDEST
2084            PUSH %merge
2085            JUMP                    ; -> merge
2086            ; Merge:
2087        merge:
2088            JUMPDEST
2089            PUSH0
2090            MSTORE                  ; MSTORE(0, 0x42)
2091            STOP
2092        ",
2093        );
2094        // At the merge MSTORE, the value (operand 1) should still be 0x42
2095        // since both branches had the same constant on the stack.
2096        let mstore = bytecode.iter_insts().find(|(_, d)| d.opcode == op::MSTORE).unwrap().0;
2097        assert_eq!(bytecode.const_operand(mstore, 1), Some(U256::from(0x42)));
2098    }
2099
2100    /// Diamond CFG where branches push different constants — the merge should
2101    /// yield None (Top) for const_operand.
2102    #[test]
2103    fn diamond_cfg_different_const() {
2104        let bytecode = analyze_asm(
2105            "
2106            CALLDATASIZE            ; condition
2107            PUSH %then_pc
2108            JUMPI                   ; branch
2109            ; Else: push 0xAA.
2110            PUSH1 0xAA
2111            PUSH %merge
2112            JUMP                    ; -> merge
2113            INVALID
2114            ; Then: push 0xBB.
2115        then_pc:
2116            JUMPDEST
2117            PUSH1 0xBB
2118            PUSH %merge
2119            JUMP                    ; -> merge
2120            ; Merge:
2121        merge:
2122            JUMPDEST
2123            PUSH0
2124            MSTORE                  ; MSTORE(0, ???)
2125            STOP
2126        ",
2127        );
2128        // At the merge MSTORE, the value (operand 1) should be None
2129        // since the two branches push different constants.
2130        let mstore = bytecode.iter_insts().find(|(_, d)| d.opcode == op::MSTORE).unwrap().0;
2131        assert_eq!(bytecode.const_operand(mstore, 1), None);
2132    }
2133
2134    /// A private-return trampoline (`JUMPDEST; JUMP`) reached by an opaque jump
2135    /// can forward arbitrary stacks into a real function entry. The trampoline's
2136    /// Top jump must not be filtered out by the `is_private_return` check.
2137    #[test]
2138    fn trampoline_private_return_unsound() {
2139        let bytecode = analyze_asm(
2140            "
2141            ; Entry: branch to opaque path or call site 1.
2142            PUSH0                       ; pc=0
2143            CALLDATALOAD                ; pc=1
2144            PUSH %opaque_path           ; pc=2
2145            JUMPI                       ; pc=4
2146            ; Fallthrough: call site 1.
2147            PUSH %ret1                  ; pc=5
2148            PUSH %fn_entry              ; pc=7
2149            JUMP                        ; pc=9
2150        ret1:
2151            JUMPDEST                    ; pc=10
2152            POP                         ; pc=11
2153            ; Call site 2.
2154            PUSH %ret2                  ; pc=12
2155            PUSH %fn_entry              ; pc=14
2156            JUMP                        ; pc=16
2157        ret2:
2158            JUMPDEST                    ; pc=17
2159            POP                         ; pc=18
2160            STOP                        ; pc=19
2161            ; Opaque path (reachable from JUMPI).
2162        opaque_path:
2163            JUMPDEST                    ; pc=20
2164            PUSH0                       ; pc=21
2165            MLOAD                       ; pc=22: any_ret (Top)
2166            PUSH %tramp                 ; pc=23
2167            JUMP                        ; pc=25
2168            ; Trampoline: bare JUMPDEST + JUMP (is_private_return = true).
2169        tramp:
2170            JUMPDEST                    ; pc=26
2171            JUMP                        ; pc=27: target = any_ret (Top)
2172            ; Internal function.
2173        fn_entry:
2174            JUMPDEST                    ; pc=28
2175            PUSH1 0x42                  ; pc=29
2176            SWAP1                       ; pc=31
2177            JUMP                        ; pc=32: return
2178        ",
2179        );
2180
2181        // The fn return JUMP at pc=32 must NOT be resolved — the trampoline
2182        // can reach fn_entry with any return address.
2183        let return_jump = bytecode.iter_insts().find(|(i, d)| bytecode.pc(*i) == 32 && d.is_jump());
2184        let (_, rj) = return_jump.unwrap();
2185        assert!(
2186            !rj.flags.contains(InstFlags::STATIC_JUMP),
2187            "unsound: return jump should not be resolved when trampoline exists"
2188        );
2189    }
2190
2191    /// A JUMPI-based return where the target comes from the entry stack should
2192    /// be invalidated when a Top jump exists, just like JUMP-based returns.
2193    #[test]
2194    fn jumpi_return_unsound() {
2195        let bytecode = analyze_asm(
2196            "
2197            ; Call site 1.
2198            PUSH %ret1              ; pc=0
2199            PUSH %fn_entry          ; pc=2
2200            JUMP                    ; pc=4
2201        ret1:
2202            JUMPDEST                ; pc=5
2203            POP                     ; pc=6
2204            ; Call site 2.
2205            PUSH %ret2              ; pc=7
2206            PUSH %fn_entry          ; pc=9
2207            JUMP                    ; pc=11
2208        ret2:
2209            JUMPDEST                ; pc=12
2210            POP                     ; pc=13
2211            ; Opaque jump.
2212            PUSH0                   ; pc=14
2213            MLOAD                   ; pc=15
2214            JUMP                    ; pc=16: Top
2215            INVALID                 ; pc=17
2216            INVALID                 ; pc=18
2217            INVALID                 ; pc=19
2218            ; Internal function using JUMPI to return.
2219        fn_entry:
2220            JUMPDEST                ; pc=20
2221            PUSH1 0x42              ; pc=21: result
2222            SWAP1                   ; pc=23: [result, ret_addr]
2223            PUSH1 0x01              ; pc=24: condition (always true)
2224            SWAP1                   ; pc=26: [result, 1, ret_addr]
2225            JUMPI                   ; pc=27: conditional return (always taken)
2226            STOP                    ; pc=28: fallthrough (dead)
2227        ",
2228        );
2229
2230        // The JUMPI at pc=27 should NOT be resolved — opaque jump can reach
2231        // fn_entry with any return address.
2232        let return_jumpi =
2233            bytecode.iter_insts().find(|(i, d)| bytecode.pc(*i) == 27 && d.is_jump());
2234        let (_, rj) = return_jumpi.unwrap();
2235        assert!(
2236            !rj.flags.contains(InstFlags::STATIC_JUMP),
2237            "unsound: JUMPI return should not be resolved"
2238        );
2239    }
2240
2241    /// A suspect JUMPI block must not keep a fixpoint-derived constant
2242    /// condition when block-local analysis cannot prove it.
2243    #[test]
2244    fn suspect_jumpi_top_target_invalidates_const_condition() {
2245        let bytecode = analyze_asm(
2246            "
2247            ; Branch to an opaque caller or fall through to a known caller.
2248            PUSH0                       ; pc=0
2249            CALLDATALOAD                ; pc=1
2250            PUSH %opaque_caller         ; pc=2
2251            JUMPI                       ; pc=4
2252            ; Known caller reaches fn_entry with condition=1 and a Top target.
2253            PUSH1 0x01                  ; pc=5: condition
2254            PUSH0                       ; pc=7
2255            MLOAD                       ; pc=8: target = Top
2256            PUSH %fn_entry              ; pc=9
2257            JUMP                        ; pc=11
2258            ; Opaque caller could reach fn_entry with condition=0.
2259        opaque_caller:
2260            JUMPDEST                    ; pc=12
2261            PUSH0                       ; pc=13: condition
2262            PUSH %taken                 ; pc=14: target
2263            PUSH0                       ; pc=16
2264            MLOAD                       ; pc=17: jump destination = Top
2265            JUMP                        ; pc=18
2266        fn_entry:
2267            JUMPDEST                    ; pc=19
2268            JUMPI                       ; pc=20
2269            STOP                        ; pc=21
2270        taken:
2271            JUMPDEST                    ; pc=22
2272            STOP                        ; pc=23
2273        ",
2274        );
2275
2276        let (_, jumpi) = bytecode
2277            .iter_insts()
2278            .find(|(i, d)| bytecode.pc(*i) == 20 && d.opcode == op::JUMPI)
2279            .unwrap();
2280        assert!(
2281            !jumpi.has_const_jumpi_condition(),
2282            "suspect JUMPI should not keep a non-local const condition"
2283        );
2284        assert!(
2285            !jumpi.flags.contains(InstFlags::STATIC_JUMP),
2286            "suspect JUMPI target should remain dynamic"
2287        );
2288    }
2289
2290    /// A block-local false JUMPI condition is sound even in a suspect block.
2291    #[test]
2292    fn suspect_jumpi_top_target_keeps_local_false_condition() {
2293        let bytecode = analyze_asm(
2294            "
2295            PUSH0
2296            CALLDATALOAD
2297            PUSH %opaque
2298            JUMPI
2299            PUSH %fn_entry
2300            JUMP
2301        opaque:
2302            JUMPDEST
2303            PUSH0
2304            MLOAD
2305            JUMP
2306        fn_entry:
2307            JUMPDEST
2308            PUSH0
2309            PUSH0
2310            MLOAD
2311            JUMPI
2312            STOP
2313        taken:
2314            JUMPDEST
2315            STOP
2316        ",
2317        );
2318
2319        let (jump_inst, jumpi) =
2320            bytecode.iter_insts().rev().find(|(_, data)| data.opcode == op::JUMPI).unwrap();
2321        assert!(jumpi.has_const_jumpi_condition());
2322        assert!(jumpi.flags.contains(InstFlags::STATIC_JUMP));
2323        assert_eq!(jumpi.static_jump_target(), jump_inst + 1);
2324    }
2325
2326    /// A block-local true JUMPI condition is also sound in a suspect block, but
2327    /// an unknown target must stay dynamic and must not retain a fallthrough.
2328    #[test]
2329    fn suspect_jumpi_top_target_keeps_local_true_condition() {
2330        let bytecode = analyze_asm(
2331            "
2332            PUSH0
2333            CALLDATALOAD
2334            PUSH %opaque
2335            JUMPI
2336            PUSH %fn_entry
2337            JUMP
2338        opaque:
2339            JUMPDEST
2340            PUSH0
2341            MLOAD
2342            JUMP
2343        fn_entry:
2344            JUMPDEST
2345            PUSH1 0x01
2346            PUSH0
2347            MLOAD
2348            JUMPI
2349            STOP
2350        taken:
2351            JUMPDEST
2352            STOP
2353        ",
2354        );
2355
2356        let (jump_inst, jumpi) =
2357            bytecode.iter_insts().rev().find(|(_, data)| data.opcode == op::JUMPI).unwrap();
2358        assert!(jumpi.has_const_jumpi_condition());
2359        assert!(!jumpi.flags.contains(InstFlags::STATIC_JUMP));
2360        assert!(!jumpi.can_fall_through());
2361        assert!(bytecode.has_dynamic_jumps);
2362        assert!(
2363            bytecode.cfg.inst_to_block[jump_inst + 1].is_none(),
2364            "always-taken dynamic JUMPI fallthrough should be dead"
2365        );
2366    }
2367
2368    /// A third caller reaches the function entry with a known callee (static
2369    /// PUSH+JUMP) but an opaque return address (from MLOAD). The function's
2370    /// return jump must not be resolved because the return address is Top.
2371    #[test]
2372    fn opaque_return_addr_caller_unsound() {
2373        let bytecode = analyze_asm(
2374            "
2375            ; Entry: branch to opaque caller or fallthrough.
2376            PUSH0                       ; pc=0
2377            CALLDATALOAD                ; pc=1
2378            PUSH %opaque_caller         ; pc=2
2379            JUMPI                       ; pc=4
2380            ; Fallthrough: call site 1.
2381            PUSH %ret1                  ; pc=5
2382            PUSH %fn_entry              ; pc=7
2383            JUMP                        ; pc=9
2384        ret1:
2385            JUMPDEST                    ; pc=10
2386            POP                         ; pc=11
2387            ; Call site 2.
2388            PUSH %ret2                  ; pc=12
2389            PUSH %fn_entry              ; pc=14
2390            JUMP                        ; pc=16
2391        ret2:
2392            JUMPDEST                    ; pc=17
2393            POP                         ; pc=18
2394            STOP                        ; pc=19
2395            ; Opaque third caller: loads return addr from memory (reachable via JUMPI).
2396        opaque_caller:
2397            JUMPDEST                    ; pc=20
2398            PUSH0                       ; pc=21
2399            MLOAD                       ; pc=22: any_ret
2400            PUSH %fn_entry              ; pc=23
2401            JUMP                        ; pc=25: -> fn_entry with arbitrary return addr
2402            ; Internal function.
2403        fn_entry:
2404            JUMPDEST                    ; pc=26
2405            PUSH1 0x42                  ; pc=27
2406            SWAP1                       ; pc=29
2407            JUMP                        ; pc=30: return
2408        ",
2409        );
2410
2411        // The return JUMP at pc=30 must NOT be resolved — the opaque caller
2412        // at pc=25 reaches fn_entry with an arbitrary return address.
2413        let return_jump = bytecode.iter_insts().find(|(i, d)| bytecode.pc(*i) == 30 && d.is_jump());
2414        let (_, rj) = return_jump.unwrap();
2415        assert!(
2416            !rj.flags.contains(InstFlags::STATIC_JUMP),
2417            "unsound: return jump should not be resolved with opaque caller"
2418        );
2419    }
2420
2421    /// Loop counter (inline increment) must be Top after fixpoint.
2422    #[test]
2423    fn const_snapshot_loop_counter_inline() {
2424        let bytecode = analyze_asm(
2425            "
2426            PUSH1 0x00          ; inst 0: i = 0
2427        loop:
2428            JUMPDEST            ; inst 1
2429            DUP1                ; inst 2
2430            PUSH1 0x0a          ; inst 3
2431            LT                  ; inst 4
2432            ISZERO              ; inst 5
2433            PUSH %exit          ; inst 6
2434            JUMPI               ; inst 7
2435            PUSH1 0x01          ; inst 8
2436            ADD                 ; inst 9: i++
2437            PUSH %loop          ; inst 10
2438            JUMP                ; inst 11
2439        exit:
2440            JUMPDEST            ; inst 12
2441            POP                 ; inst 13
2442            STOP                ; inst 14
2443        ",
2444        );
2445
2446        // DUP1 at inst 2: counter is loop-variant.
2447        assert_eq!(bytecode.const_operand(Inst::from_usize(2), 0), None);
2448        // ADD at inst 9: TOS=1 (const), second=counter (Top).
2449        assert_eq!(bytecode.const_operand(Inst::from_usize(9), 0), Some(U256::from(1)));
2450        assert_eq!(bytecode.const_operand(Inst::from_usize(9), 1), None);
2451    }
2452
2453    /// Loop counter incremented via single-caller subroutine must be Top.
2454    #[test]
2455    fn const_snapshot_loop_counter_subroutine() {
2456        let bytecode = analyze_asm(
2457            "
2458            PUSH1 0x00
2459        loop:
2460            JUMPDEST            ; inst 1
2461            DUP1                ; inst 2
2462            PUSH1 0x0a
2463            LT
2464            ISZERO
2465            PUSH %exit
2466            JUMPI
2467            ; call add_fn(counter, 1) -> counter'
2468            PUSH1 0x01
2469            DUP2
2470            PUSH %ret
2471            SWAP2
2472            SWAP1
2473            PUSH %add_fn
2474            JUMP
2475        ret:
2476            JUMPDEST
2477            SWAP1
2478            POP
2479            PUSH %loop
2480            JUMP
2481        exit:
2482            JUMPDEST
2483            POP
2484            STOP
2485        add_fn:
2486            JUMPDEST
2487            PUSH1 0x00
2488            DUP3
2489            DUP3
2490            ADD
2491            SWAP1
2492            POP
2493            DUP1
2494            DUP3
2495            GT
2496            ISZERO
2497            PUSH %add_ok
2498            JUMPI
2499            INVALID
2500        add_ok:
2501            JUMPDEST
2502            SWAP3
2503            SWAP2
2504            POP
2505            POP
2506            JUMP
2507        ",
2508        );
2509
2510        assert_eq!(bytecode.const_operand(Inst::from_usize(2), 0), None);
2511    }
2512
2513    /// Loop counter incremented via multi-caller subroutine must be Top.
2514    #[test]
2515    fn const_snapshot_loop_counter_multi_caller() {
2516        let bytecode = analyze_asm(
2517            "
2518            ; Caller 1 (outside loop) to make add_fn multi-target.
2519            PUSH1 0x02
2520            PUSH1 0x03
2521            PUSH %ret_outer
2522            SWAP2
2523            SWAP1
2524            PUSH %add_fn
2525            JUMP
2526        ret_outer:
2527            JUMPDEST            ; inst 7
2528            POP
2529            PUSH1 0x00          ; i = 0
2530        loop:
2531            JUMPDEST            ; inst 10
2532            DUP1                ; inst 11
2533            PUSH1 0x0a
2534            LT
2535            ISZERO
2536            PUSH %exit
2537            JUMPI
2538            ; Caller 2 (inside loop).
2539            PUSH1 0x01
2540            DUP2
2541            PUSH %ret_loop
2542            SWAP2
2543            SWAP1
2544            PUSH %add_fn
2545            JUMP
2546        ret_loop:
2547            JUMPDEST
2548            SWAP1
2549            POP
2550            PUSH %loop
2551            JUMP
2552        exit:
2553            JUMPDEST
2554            POP
2555            STOP
2556        add_fn:
2557            JUMPDEST
2558            PUSH1 0x00
2559            DUP3
2560            DUP3
2561            ADD
2562            SWAP1
2563            POP
2564            DUP1
2565            DUP3
2566            GT
2567            ISZERO
2568            PUSH %add_ok
2569            JUMPI
2570            INVALID
2571        add_ok:
2572            JUMPDEST
2573            SWAP3
2574            SWAP2
2575            POP
2576            POP
2577            JUMP
2578        ",
2579        );
2580
2581        // DUP1 at inst 11: counter is loop-variant.
2582        assert_eq!(bytecode.const_operand(Inst::from_usize(11), 0), None);
2583    }
2584
2585    /// loopExp statetest: loop counters through checked_add subroutine must be Top.
2586    #[test]
2587    fn const_snapshot_loop_exp() {
2588        // tests/GeneralStateTests/VMTests/vmPerformance/loopExp.json
2589        let bytecode = analyze_hex(
2590            "608060405234801561001057600080fd5b506004361061004c5760003560e01c806363ad973214610051578063a34f51e414610081578063dfaf4a87146100b1578063f078245b146100e1575b600080fd5b61006b60048036038101906100669190610275565b610111565b60405161007891906102d7565b60405180910390f35b61009b60048036038101906100969190610275565b610199565b6040516100a891906102d7565b60405180910390f35b6100cb60048036038101906100c69190610275565b6101cb565b6040516100d891906102d7565b60405180910390f35b6100fb60048036038101906100f69190610275565b610208565b60405161010891906102d7565b60405180910390f35b60008083905060005b838110156101865785820a915085820a915085820a915085820a915085820a915085820a915085820a915085820a915085820a915085820a915085820a915085820a915085820a915085820a915085820a915085820a915060108161017f9190610321565b905061011a565b5080600081905550809150509392505050565b6000805b828110156101b9576001816101b29190610321565b905061019d565b508260008190555082905093925050505650565b60008083905060005b838110156101f55785820a91506001816101ee9190610321565b90506101d4565b5080600081905550809150509392505050565b6000805b82811015610228576010816102219190610321565b905061020c565b50826000819055508290509392505050565b600080fd5b6000819050919050565b6102528161023f565b811461025d57600080fd5b50565b60008135905061026f81610249565b92915050565b60008060006060848603121561028e5761028d61023a565b5b600061029c86828701610260565b93505060206102ad86828701610260565b92505060406102be86828701610260565b9150509250925092565b6102d18161023f565b82525050565b60006020820190506102ec60008301846102c8565b92915050565b7f4e487b71000000000000000000000000000000000000000000000000000000006000526011600452602460006000fd5b600061032c8261023f565b91506103378361023f565b9250828201905080821115610340f5761034e6102f2565b5b9291505056fea264697066735822122000b253a2b246ddfeb0f09ddf5ec7dc70e2235369b8a2f3f9b55e32a5ca01e04ff64736f6c63430008180033",
2591        );
2592
2593        // simpleLoop LT at pc=416: counter (depth=0) is loop-variant.
2594        let lt_inst = bytecode
2595            .insts
2596            .iter_enumerated()
2597            .find(|(i, inst)| inst.opcode == op::LT && bytecode.pc(*i) == 416)
2598            .map(|(i, _)| i)
2599            .expect("LT at pc=416 not found");
2600        assert_eq!(bytecode.const_operand(lt_inst, 0), None);
2601    }
2602
2603    /// Suspect-block invalidation must preserve block-local constants while
2604    /// restoring cross-block values to Top.
2605    ///
2606    /// The target block receives 0xAA from a predecessor (cross-block) and also
2607    /// has block-local PUSHes. Without restoration, the fixpoint's precise 0xAA
2608    /// would persist unsoundly; with restoration, only block-local constants survive.
2609    #[test]
2610    fn suspect_block_preserves_local_const_operand() {
2611        let bytecode = analyze_asm(
2612            "
2613            PUSH1 0xAA          ; inst 0: inherited into target
2614            CALLDATASIZE        ; inst 1: unknown condition
2615            PUSH %target        ; inst 2
2616            JUMPI               ; inst 3: target reachable, fallthrough reachable
2617
2618            PUSH0               ; inst 4
2619            CALLDATALOAD        ; inst 5: Top
2620            JUMP                ; inst 6: unresolved -> triggers suspect invalidation
2621
2622        target:
2623            JUMPDEST            ; inst 7: suspect seed
2624            PUSH0               ; inst 8: block-local constant
2625            MSTORE              ; inst 9: offset=0 (local), value=inherited 0xAA
2626            STOP                ; inst 10
2627        ",
2628        );
2629        assert!(bytecode.has_dynamic_jumps);
2630        // Block-local constant survives.
2631        assert_eq!(bytecode.const_output(Inst::from_usize(8)), Some(U256::ZERO));
2632        assert_eq!(bytecode.const_operand(Inst::from_usize(9), 0), Some(U256::ZERO));
2633        // Cross-block value must be restored to Top.
2634        assert_eq!(bytecode.const_operand(Inst::from_usize(9), 1), None);
2635    }
2636
2637    /// Same as above but asserting const_output: block-local PUSH output survives,
2638    /// but anything depending on cross-block values becomes None.
2639    #[test]
2640    fn suspect_block_preserves_local_const_output() {
2641        let bytecode = analyze_asm(
2642            "
2643            PUSH1 0xAA          ; inst 0: inherited into target
2644            CALLDATASIZE        ; inst 1
2645            PUSH %target        ; inst 2
2646            JUMPI               ; inst 3
2647
2648            PUSH0               ; inst 4
2649            CALLDATALOAD        ; inst 5: Top
2650            JUMP                ; inst 6: unresolvable
2651
2652        target:
2653            JUMPDEST            ; inst 7: suspect seed
2654            PUSH1 0x01          ; inst 8: block-local const output
2655            ADD                 ; inst 9: 0xAA + 0x01 in fixpoint, Top + 0x01 after restore
2656            STOP                ; inst 10
2657        ",
2658        );
2659        assert!(bytecode.has_dynamic_jumps);
2660        // Purely local push output preserved.
2661        assert_eq!(bytecode.const_output(Inst::from_usize(8)), Some(U256::from(0x01)));
2662        // ADD: local operand preserved, inherited operand restored to Top.
2663        assert_eq!(bytecode.const_operand(Inst::from_usize(9), 0), Some(U256::from(0x01)));
2664        assert_eq!(bytecode.const_operand(Inst::from_usize(9), 1), None);
2665        assert_eq!(bytecode.const_output(Inst::from_usize(9)), None);
2666    }
2667
2668    /// Fallthrough block after a JUMPI in a suspect JUMPDEST block becomes
2669    /// suspect via forward propagation. Block-local constants in the fallthrough
2670    /// survive, but cross-block inherited values are restored to Top.
2671    #[test]
2672    fn suspect_jumpi_fallthrough_preserves_consts() {
2673        let bytecode = analyze_asm(
2674            "
2675            PUSH1 0xAA          ; inst 0: inherited into target
2676            CALLDATASIZE        ; inst 1: unknown condition
2677            PUSH %target        ; inst 2
2678            JUMPI               ; inst 3
2679
2680            PUSH0               ; inst 4
2681            CALLDATALOAD        ; inst 5: Top
2682            JUMP                ; inst 6: unresolved
2683
2684        target:
2685            JUMPDEST            ; inst 7: suspect seed, entry stack [0xAA]
2686            PUSH1 0x01          ; inst 8: always take
2687            PUSH %after         ; inst 9
2688            JUMPI               ; inst 10: leaves [0xAA] for after
2689            STOP                ; inst 11
2690
2691        after:
2692            JUMPDEST            ; inst 12: suspect via forward propagation
2693            PUSH0               ; inst 13: block-local constant
2694            MSTORE              ; inst 14: offset=0 (local), value=inherited 0xAA
2695            STOP                ; inst 15
2696        ",
2697        );
2698        assert!(bytecode.has_dynamic_jumps);
2699        // Block-local constant in propagated-suspect fallthrough survives.
2700        assert_eq!(bytecode.const_output(Inst::from_usize(13)), Some(U256::ZERO));
2701        assert_eq!(bytecode.const_operand(Inst::from_usize(14), 0), Some(U256::ZERO));
2702        // Cross-block inherited value restored to Top.
2703        assert_eq!(bytecode.const_operand(Inst::from_usize(14), 1), None);
2704    }
2705
2706    /// Non-suspect blocks must retain their fixpoint-derived snapshots even when
2707    /// invalidation runs elsewhere. The fallthrough after a JUMPI is NOT a
2708    /// JUMPDEST, so it is never suspect. Block-local constants, const-folded
2709    /// results, and cross-block values resolved via the single-predecessor path
2710    /// (MLOAD after MSTORE) must all be preserved.
2711    #[test]
2712    fn nonsuspect_fallthrough_keeps_fixpoint_snapshots() {
2713        let bytecode = analyze_asm(
2714            "
2715            PUSH1 0x69          ; inst 0: cross-block value
2716            CALLDATASIZE        ; inst 1: unknown condition
2717            PUSH %target        ; inst 2
2718            JUMPI               ; inst 3
2719            ; Non-suspect fallthrough block (no JUMPDEST).
2720            PUSH1 0x0A          ; inst 4
2721            PUSH1 0x0B          ; inst 5
2722            ADD                 ; inst 6: 0x0A + 0x0B = 0x15, block-local
2723            PUSH0               ; inst 7
2724            MSTORE              ; inst 8
2725            MLOAD               ; inst 9: cross-block via single predecessor
2726            PUSH1 0x01          ; inst 10
2727            MSTORE              ; inst 11
2728            STOP                ; inst 12
2729            ; Opaque dynamic jump — triggers suspect invalidation.
2730            JUMPDEST            ; inst 13
2731            PUSH0               ; inst 14
2732            CALLDATALOAD        ; inst 15: Top
2733            JUMP                ; inst 16: unresolvable
2734        target:
2735            JUMPDEST            ; inst 17: suspect seed
2736            PUSH1 0x42          ; inst 18
2737            STOP                ; inst 19
2738        ",
2739        );
2740        assert!(bytecode.has_dynamic_jumps);
2741        // Fallthrough block: block-local constants survive.
2742        // ADD
2743        assert_eq!(bytecode.const_operand(Inst::from_usize(6), 0), Some(U256::from(0x0B)));
2744        assert_eq!(bytecode.const_operand(Inst::from_usize(6), 1), Some(U256::from(0x0A)));
2745        assert_eq!(bytecode.const_output(Inst::from_usize(6)), Some(U256::from(0x15)));
2746        // MSTORE 1
2747        assert_eq!(bytecode.const_operand(Inst::from_usize(8), 0), Some(U256::from(0)));
2748        assert_eq!(bytecode.const_operand(Inst::from_usize(8), 1), Some(U256::from(0x15)));
2749        // MLOAD: single predecessor (no JUMPDEST), so cross-block value resolves.
2750        assert_eq!(bytecode.const_operand(Inst::from_usize(9), 0), Some(U256::from(0x69)));
2751        // Target block: block-local PUSH survives.
2752        assert_eq!(bytecode.const_output(Inst::from_usize(18)), Some(U256::from(0x42)));
2753    }
2754
2755    /// JUMPI in a suspect JUMPDEST block: the fallthrough block inherits
2756    /// suspect status via forward propagation. Incoming stack values from the
2757    /// predecessor become Top (block-local pass doesn't see cross-block flow),
2758    /// while block-local PUSHes in the fallthrough are preserved.
2759    #[test]
2760    fn suspect_jumpi_fallthrough_mixed_operands() {
2761        let bytecode = analyze_asm(
2762            "
2763            ; Jump into the suspect JUMPDEST block with a value on the stack.
2764            PUSH1 0xAA          ; inst 0: will be on stack at entry to target
2765            PUSH %target        ; inst 1
2766            JUMP                ; inst 2: static jump
2767            ; Opaque dynamic jump — triggers suspect invalidation.
2768            JUMPDEST            ; inst 3
2769            PUSH0               ; inst 4
2770            CALLDATALOAD        ; inst 5: Top
2771            JUMP                ; inst 6: unresolvable
2772        target:
2773            ; Suspect JUMPDEST block with JUMPI.
2774            ; Entry stack: [0xAA] (from pred, but unknown to block-local pass).
2775            JUMPDEST            ; inst 7: suspect seed
2776            PUSH1 0x01          ; inst 8: condition
2777            PUSH %after         ; inst 9
2778            JUMPI               ; inst 10: pops condition+target, [0xAA] remains
2779            STOP                ; inst 11
2780        after:
2781            ; Fallthrough/target of JUMPI — suspect via propagation.
2782            ; Entry stack: [0xAA] (inherited, but Top in block-local pass).
2783            JUMPDEST            ; inst 12
2784            PUSH1 0xBB          ; inst 13: block-local
2785            ADD                 ; inst 14: 0xAA + 0xBB — depth 1 is incoming
2786            STOP                ; inst 15
2787        ",
2788        );
2789        assert!(bytecode.has_dynamic_jumps);
2790        // ADD at inst 14: TOS (depth 0) = 0xBB (block-local), depth 1 = Top
2791        // (incoming from suspect predecessor, block-local pass saw it as Top).
2792        assert_eq!(bytecode.const_operand(Inst::from_usize(14), 0), Some(U256::from(0xBB)));
2793        assert_eq!(bytecode.const_operand(Inst::from_usize(14), 1), None);
2794        assert_eq!(bytecode.const_output(Inst::from_usize(14)), None);
2795    }
2796
2797    /// Bytecode that is just STOP — minimal edge case.
2798    #[test]
2799    fn empty_bytecode() {
2800        let bytecode = analyze_asm("STOP");
2801        assert!(!bytecode.has_dynamic_jumps);
2802    }
2803
2804    /// A dynamic jump in dead code should not cause `has_dynamic_jumps` to be true.
2805    #[test]
2806    fn dead_code_dynamic_jump() {
2807        let bytecode = analyze_asm(
2808            "
2809            STOP
2810            PUSH0
2811            CALLDATALOAD
2812            JUMP
2813        ",
2814        );
2815        assert!(!bytecode.has_dynamic_jumps);
2816    }
2817
2818    /// Many callers to a shared internal function through relay blocks.
2819    /// When the fixpoint iteration cap is exceeded, partially-discovered
2820    /// multi-target jumps must NOT be committed as closed MULTI_JUMPs.
2821    #[test]
2822    fn non_converged_multi_jump_stays_dynamic() {
2823        // Generate bytecode with k call sites, b relay blocks, and one shared function.
2824        // Each call site pushes a different return address and jumps through the relay
2825        // chain into the shared function, which does PUSH1 0x42; SWAP1; JUMP.
2826        // With enough callers and relays, the fixpoint cap is exceeded before all
2827        // return addresses are discovered.
2828        let k = 15;
2829        let b = 31;
2830        let mut lines = Vec::new();
2831        lines.push("PUSH %call0".to_string());
2832        lines.push("JUMP".to_string());
2833        for i in 0..k {
2834            lines.push(format!("call{i}:"));
2835            lines.push("JUMPDEST".to_string());
2836            lines.push(format!("PUSH %ret{i}"));
2837            lines.push("PUSH %relay0".to_string());
2838            lines.push("JUMP".to_string());
2839
2840            lines.push(format!("ret{i}:"));
2841            lines.push("JUMPDEST".to_string());
2842            lines.push("POP".to_string());
2843            if i + 1 < k {
2844                lines.push(format!("PUSH %call{}", i + 1));
2845                lines.push("JUMP".to_string());
2846            } else {
2847                lines.push("STOP".to_string());
2848            }
2849        }
2850        for i in 0..b {
2851            lines.push(format!("relay{i}:"));
2852            lines.push("JUMPDEST".to_string());
2853            if i + 1 < b {
2854                lines.push(format!("PUSH %relay{}", i + 1));
2855            } else {
2856                lines.push("PUSH %fn_entry".to_string());
2857            }
2858            lines.push("JUMP".to_string());
2859        }
2860        lines.push("fn_entry:".to_string());
2861        lines.push("JUMPDEST".to_string());
2862        lines.push("PUSH1 0x42".to_string());
2863        lines.push("SWAP1".to_string());
2864        lines.push("JUMP".to_string());
2865
2866        let bytecode = analyze_asm(&lines.join("\n"));
2867
2868        // The shared function's return JUMP must NOT be committed as a closed
2869        // MULTI_JUMP when the fixpoint didn't converge — doing so would produce
2870        // an incomplete switch table missing some valid return addresses.
2871        let return_jump = bytecode
2872            .iter_insts()
2873            .find(|(_, d)| d.is_jump() && d.flags.contains(InstFlags::MULTI_JUMP));
2874        assert!(return_jump.is_none(), "non-converged fixpoint must not commit partial MULTI_JUMP");
2875        // It should remain dynamic instead.
2876        assert!(
2877            bytecode.has_dynamic_jumps,
2878            "return jump should remain dynamic when fixpoint doesn't converge"
2879        );
2880    }
2881}