Skip to main content

revmc_codegen/bytecode/
fmt.rs

1use super::{
2    Bytecode, Inst, InstData, InstFlags,
3    asm::{TokenKind, Tokenizer},
4    bitvec_as_bytes,
5    passes::block_analysis::Block,
6};
7use oxc_index::{IndexVec, index_vec};
8use revm_bytecode::opcode as op;
9use revm_primitives::hex;
10use std::{borrow::Cow, fmt, fmt::Write, io::IsTerminal};
11
12impl Bytecode<'_> {
13    /// Resolves a jump target instruction to its CFG block index.
14    fn target_block(&self, target: Inst) -> Option<Block> {
15        self.cfg.inst_to_block.get(target).copied().flatten()
16    }
17
18    /// Collects formatted lines and builds the inst-to-line map stored in `self.inst_lines`.
19    pub(super) fn collect_lines(&self) -> Vec<(String, String)> {
20        let mut lines: Vec<(String, String)> = Vec::new();
21        let mut inst_lines: IndexVec<Inst, u32> = index_vec![0u32; self.insts.len()];
22
23        // Compute field widths for aligned ic=/pc= columns.
24        let (max_ic, max_pc) = self
25            .cfg
26            .blocks
27            .iter()
28            .flat_map(|b| {
29                b.insts().filter(|&i| !self.inst(i).is_dead_code()).map(|i| (i, self.pc(i)))
30            })
31            .fold((0u32, 0u32), |(mi, mp), (i, p)| (mi.max(i.index() as u32), mp.max(p)));
32        let ic_width = decimal_width(max_ic);
33        let pc_width = decimal_width(max_pc);
34
35        let s = self.ir_stats();
36
37        lines.push((
38            String::new(),
39            format!(
40                "spec_id={} has_dynamic_jumps={} may_suspend={}",
41                self.spec_id, self.has_dynamic_jumps, self.may_suspend,
42            ),
43        ));
44        lines.push((
45            String::new(),
46            format!(
47                "insts={} live={} dead={} noops={} suspends={} blocks={} block_min={} block_max={} block_avg={:.1} block_median={}",
48                s.total, s.live, s.dead, s.noops, s.suspends, s.blocks, s.block_min, s.block_max, s.block_avg, s.block_median,
49            ),
50        ));
51        lines.push((String::new(), String::new()));
52
53        for (bid, block) in self.cfg.blocks.iter_enumerated() {
54            // Blank line between blocks.
55            if !lines.is_empty()
56                && lines.last().is_some_and(|(t, c)| !t.is_empty() || !c.is_empty())
57            {
58                lines.push((String::new(), String::new()));
59            }
60
61            // Block header.
62            let first = self.inst(block.insts.start);
63            let mut header = format!("{bid}:");
64            let mut comment = String::new();
65            if first.is_stack_section_head() {
66                write!(
67                    comment,
68                    "stack_in={} max_growth={}",
69                    first.stack_section.inputs, first.stack_section.max_growth,
70                )
71                .unwrap();
72            }
73            if !comment.is_empty() {
74                comment.push(' ');
75            }
76            write!(comment, "predecessors=").unwrap();
77            for (i, pred) in block.preds.iter().enumerate() {
78                if i > 0 {
79                    comment.push(',');
80                }
81                write!(comment, "{pred}").unwrap();
82            }
83            // Pad header to align with indented instructions.
84            while header.len() < 2 {
85                header.push(' ');
86            }
87            lines.push((header, comment));
88
89            // Instructions.
90            for inst in block.insts() {
91                let data = self.inst(inst);
92                if data.is_dead_code() {
93                    continue;
94                }
95
96                // 1-based line number (lines.len() is the 0-based index of the next line).
97                inst_lines[inst] = lines.len() as u32 + 1;
98
99                // Instruction text.
100                let mut text = String::from("  ");
101                let opcode = self.opcode(inst);
102                write!(text, "{opcode}").unwrap();
103                if data.flags.contains(InstFlags::INVALID_JUMP) {
104                    text.push_str(" %invalid");
105                } else if data.flags.contains(InstFlags::MULTI_JUMP) {
106                    if let Some(targets) = self.multi_jump_targets(inst) {
107                        for (i, &t) in targets.iter().enumerate() {
108                            if i > 0 {
109                                text.push(',');
110                            }
111                            match self.target_block(t) {
112                                Some(b) => write!(text, " %{b}").unwrap(),
113                                None => write!(text, " %inst{t}").unwrap(),
114                            }
115                        }
116                    }
117                } else if data.is_static_jump() {
118                    let target = data.static_jump_target();
119                    match self.target_block(target) {
120                        Some(b) => write!(text, " %{b}").unwrap(),
121                        None => write!(text, " %inst{target}").unwrap(),
122                    }
123                } else if data.is_jump() {
124                    text.push_str(" %dynamic");
125                }
126
127                // Comment with ic, pc, and flags/behavior.
128                let mut comment = String::new();
129                write!(comment, "ic={:>ic_width$}", inst.index()).unwrap();
130                write!(comment, " pc={:>pc_width$}", self.pc(inst)).unwrap();
131                if !data.gas_section.is_empty() {
132                    write!(comment, " gas={}", data.gas_section.gas_cost).unwrap();
133                }
134                if inst != block.insts.start && data.is_stack_section_head() {
135                    write!(
136                        comment,
137                        " stack_in={} max_growth={}",
138                        data.stack_section.inputs, data.stack_section.max_growth,
139                    )
140                    .unwrap();
141                }
142                let flags = data.flags;
143                if flags.contains(InstFlags::NOOP) {
144                    comment.push_str(" noop");
145                }
146                if flags.contains(InstFlags::DEAD_CODE) {
147                    comment.push_str(" dead");
148                }
149                if flags.contains(InstFlags::DISABLED) {
150                    comment.push_str(" disabled");
151                }
152                if flags.contains(InstFlags::UNKNOWN) {
153                    comment.push_str(" unknown");
154                }
155                if flags.contains(InstFlags::INVALID_JUMP) {
156                    comment.push_str(" invalid_jump");
157                }
158                if flags.contains(InstFlags::MULTI_JUMP) {
159                    comment.push_str(" multi_jump");
160                }
161                if data.may_suspend() {
162                    comment.push_str(" suspends");
163                }
164                if data.is_reachable_jumpdest(self.has_dynamic_jumps) {
165                    comment.push_str(" reachable");
166                }
167
168                lines.push((text, comment));
169            }
170        }
171
172        *self.inst_lines.borrow_mut() = inst_lines;
173        lines
174    }
175
176    /// Formats the bytecode IR as plain text.
177    fn display_plain(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178        let lines = self.collect_lines();
179        let max_text_width = lines.iter().map(|(t, _)| t.len()).max().unwrap_or(0);
180        let comment_col = max_text_width.clamp(4, 20);
181        for (text, comment) in &lines {
182            if text.is_empty() && comment.is_empty() {
183                writeln!(f)?;
184            } else if comment.is_empty() {
185                writeln!(f, "{text}")?;
186            } else if text.is_empty() {
187                writeln!(f, "; {comment}")?;
188            } else {
189                writeln!(f, "{text:<comment_col$} ; {comment}")?;
190            }
191        }
192        Ok(())
193    }
194
195    /// Formats the bytecode IR with ANSI syntax highlighting.
196    ///
197    /// Builds the plain-text output first, then runs it through the asm
198    /// tokenizer and emits colored spans.
199    fn display_colored(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
200        let plain = format!("{}", std::fmt::from_fn(|f| self.display_plain(f)));
201        colorize(f, &plain)
202    }
203}
204
205impl fmt::Display for Bytecode<'_> {
206    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
207        if f.alternate() && std::io::stdout().is_terminal() {
208            self.display_colored(f)
209        } else {
210            self.display_plain(f)
211        }
212    }
213}
214
215// ———————————————————————————————————————————————————————————————————————
216// Colorizer (uses asm::Tokenizer)
217// ———————————————————————————————————————————————————————————————————————
218
219const RESET: &str = "\x1b[0m";
220const BOLD: &str = "\x1b[1m";
221const DIM: &str = "\x1b[2m";
222const CYAN: &str = "\x1b[36m";
223const GREEN: &str = "\x1b[32m";
224const YELLOW: &str = "\x1b[33m";
225const RED: &str = "\x1b[31m";
226const BLUE: &str = "\x1b[34m";
227const WHITE: &str = "\x1b[37m";
228
229/// Writes ANSI-colored output by tokenizing `plain` with the asm tokenizer.
230fn colorize(f: &mut fmt::Formatter<'_>, plain: &str) -> fmt::Result {
231    for token in Tokenizer::new(plain) {
232        let s = token.src;
233        match token.kind {
234            TokenKind::Whitespace => write!(f, "{s}")?,
235            TokenKind::Label => write!(f, "{BOLD}{CYAN}{s}:{RESET}")?,
236            TokenKind::Comment => write!(f, "{DIM}{s}{RESET}")?,
237            TokenKind::Number(_) => write!(f, "{YELLOW}{s}{RESET}")?,
238            TokenKind::LabelRef if s == "dynamic" => write!(f, "{RED}%{s}{RESET}")?,
239            TokenKind::LabelRef => write!(f, "{CYAN}%{s}{RESET}")?,
240            TokenKind::Ident => {
241                let color = opcode_color(s);
242                write!(f, "{BOLD}{color}{s}{RESET}")?;
243            }
244            TokenKind::Comma => write!(f, ",")?,
245            TokenKind::Unknown => write!(f, "{s}")?,
246            TokenKind::LParen | TokenKind::RParen | TokenKind::ParamRef => {}
247        }
248    }
249    Ok(())
250}
251
252/// Returns the ANSI color for an opcode name based on its byte-value category.
253fn opcode_color(name: &str) -> &'static str {
254    use revm_bytecode::opcode::OpCode;
255    let byte = match OpCode::parse(name) {
256        Some(op) => op.get(),
257        // Bare "PUSH" without a number suffix.
258        None if name == "PUSH" => op::PUSH32,
259        None => return WHITE,
260    };
261    opcode_color_by_byte(byte)
262}
263
264fn opcode_color_by_byte(byte: u8) -> &'static str {
265    use op::*;
266    match byte {
267        // Terminating.
268        STOP | RETURN | REVERT | INVALID | SELFDESTRUCT => RED,
269        // Side effects: jumps, calls, creates, log.
270        JUMP
271        | JUMPI
272        | JUMPDEST
273        | CREATE
274        | CALL
275        | CALLCODE
276        | DELEGATECALL
277        | CREATE2
278        | STATICCALL
279        | LOG0..=LOG4 => GREEN,
280        // Arithmetic, comparison, bitwise, keccak.
281        ADD..=SIGNEXTEND | LT..=CLZ | KECCAK256 => WHITE,
282        // I/O: environment, memory, storage.
283        ADDRESS..=SLOTNUM | MLOAD..=MCOPY => BLUE,
284        // Stack: POP, PUSH, DUP, SWAP, EOF stack ops.
285        POP | PUSH0..=SWAP16 | DUPN | SWAPN | EXCHANGE => YELLOW,
286        _ => WHITE,
287    }
288}
289
290impl fmt::Debug for Bytecode<'_> {
291    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
292        f.debug_struct("Bytecode")
293            .field("code", &hex::encode(&*self.code))
294            .field("insts", &self.insts)
295            .field("jumpdests", &hex::encode(bitvec_as_bytes(&self.jumpdests)))
296            .field("spec_id", &self.spec_id)
297            .field("has_dynamic_jumps", &self.has_dynamic_jumps)
298            .field("may_suspend", &self.may_suspend)
299            .finish()
300    }
301}
302
303impl fmt::Debug for InstData {
304    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
305        f.debug_struct("InstData")
306            .field("opcode", &self.to_op())
307            .field("flags", &format_args!("{:?}", self.flags))
308            .field("data", &self.data)
309            .field("gas_section", &self.gas_section)
310            .field("stack_section", &self.stack_section)
311            .finish()
312    }
313}
314
315// DOT graph colors.
316mod dot_colors {
317    const DARK_NAVY: &str = "#1a1a2e";
318    const DARK_BLUE: &str = "#16213e";
319    const BLUE: &str = "#0f3460";
320    const DARK_TEAL: &str = "#1a2340";
321    const TEAL: &str = "#53a8b6";
322    const GREEN: &str = "#5cdb95";
323    const DARK_GREEN: &str = "#1a2e1a";
324    const DARK_ORANGE: &str = "#2e2416";
325    const ORANGE: &str = "#e0a030";
326    const DARK_RED: &str = "#2d1b2e";
327    const RED: &str = "#e94560";
328    const GRAY: &str = "#555577";
329    const LIGHT_GRAY: &str = "#e0e0e0";
330
331    pub(super) const BG: &str = DARK_NAVY;
332    pub(super) const TEXT: &str = LIGHT_GRAY;
333    // Default node.
334    pub(super) const NODE_FILL: &str = DARK_BLUE;
335    pub(super) const NODE_BORDER: &str = BLUE;
336    // Reverting/error blocks.
337    pub(super) const REVERT_FILL: &str = DARK_RED;
338    pub(super) const REVERT_BORDER: &str = RED;
339    // Non-reverting exit blocks (STOP, RETURN).
340    pub(super) const EXIT_FILL: &str = DARK_GREEN;
341    pub(super) const EXIT_BORDER: &str = GREEN;
342    // Suspending blocks (CALL, CREATE, ...).
343    pub(super) const SUSPEND_FILL: &str = DARK_ORANGE;
344    pub(super) const SUSPEND_BORDER: &str = ORANGE;
345    // Branching blocks.
346    pub(super) const BRANCH_FILL: &str = DARK_TEAL;
347    pub(super) const BRANCH_BORDER: &str = TEAL;
348    // Edges.
349    pub(super) const EDGE: &str = GRAY;
350    pub(super) const EDGE_JUMP: &str = TEAL;
351    pub(super) const EDGE_COND_JUMP: &str = GREEN;
352    pub(super) const EDGE_FALSE: &str = RED;
353}
354
355impl<'a> Bytecode<'a> {
356    /// Writes the bytecode as a DOT graph to the given writer.
357    #[doc(hidden)]
358    pub fn write_dot<W: fmt::Write>(&self, w: &mut W) -> fmt::Result {
359        use dot_colors::*;
360
361        writeln!(w, "digraph bytecode {{")?;
362        writeln!(w, "  graph [bgcolor=\"{BG}\" rankdir=TB];")?;
363        writeln!(
364            w,
365            "  node [shape=box style=\"rounded,filled\" fontname=\"Courier\" fontsize=10 \
366             fillcolor=\"{NODE_FILL}\" fontcolor=\"{TEXT}\" \
367             color=\"{NODE_BORDER}\" penwidth=1.5];"
368        )?;
369        writeln!(
370            w,
371            "  edge [fontname=\"Courier\" fontsize=9 color=\"{EDGE}\" tailport=s headport=n];"
372        )?;
373
374        // Emit nodes.
375        for (bid, block) in self.cfg.blocks.iter_enumerated() {
376            let last = self.inst(block.terminator());
377            let first = self.inst(block.insts.start);
378
379            // Color based on block behavior.
380            let has_suspend = block.insts().any(|i| self.inst(i).may_suspend());
381            let (fill, border) = if matches!(last.opcode, op::STOP | op::RETURN) {
382                (EXIT_FILL, EXIT_BORDER)
383            } else if last.is_diverging() {
384                (REVERT_FILL, REVERT_BORDER)
385            } else if has_suspend {
386                (SUSPEND_FILL, SUSPEND_BORDER)
387            } else if last.is_jump() {
388                (BRANCH_FILL, BRANCH_BORDER)
389            } else {
390                (NODE_FILL, NODE_BORDER)
391            };
392
393            write!(
394                w,
395                "  {bid} [fillcolor=\"{fill}\" color=\"{border}\" \
396                 label=\"{bid}",
397            )?;
398
399            if first.is_stack_section_head() {
400                write!(
401                    w,
402                    " [in={} growth={}]",
403                    first.stack_section.inputs, first.stack_section.max_growth
404                )?;
405            }
406
407            write!(w, "\\n")?;
408            for inst in block.insts() {
409                let data = self.inst(inst);
410                if data.is_dead_code() {
411                    continue;
412                }
413                // Show stack section header for mid-block section boundaries.
414                if inst != block.insts.start && data.is_stack_section_head() {
415                    write!(
416                        w,
417                        "--- [in={} growth={}]\\l",
418                        data.stack_section.inputs, data.stack_section.max_growth
419                    )?;
420                }
421                let opcode = self.opcode(inst);
422                let mut op_str =
423                    abbreviate_hex(&opcode.to_string()).replace('>', "\\>").replace('<', "\\<");
424                if !data.gas_section.is_empty() {
425                    write!(op_str, " [g={}]", data.gas_section.gas_cost).unwrap();
426                }
427                write!(w, "{op_str}\\l")?;
428            }
429            writeln!(w, "\"];")?;
430        }
431
432        // Emit edges from the CFG.
433        for (bid, block) in self.cfg.blocks.iter_enumerated() {
434            let last = self.inst(block.terminator());
435
436            if last.is_jump()
437                && !last.is_static_jump()
438                && !last.flags.contains(InstFlags::MULTI_JUMP)
439            {
440                writeln!(w, "  {bid} -> dynamic [color=\"{EDGE_FALSE}\" style=dashed];")?;
441                continue;
442            }
443
444            // Succs layout: [fallthrough?, jump_target(s)...].
445            // Fallthrough is first when present (JUMPI or non-branching terminator).
446            let mut succs = block.succs.iter().copied();
447            if last.can_fall_through()
448                && let Some(ft) = succs.next()
449            {
450                let color = if last.opcode == op::JUMPI { EDGE_FALSE } else { EDGE };
451                writeln!(w, "  {bid} -> {ft} [color=\"{color}\"];")?;
452            }
453            // Remaining succs are jump targets.
454            let is_multi = last.flags.contains(InstFlags::MULTI_JUMP) && block.succs.len() > 1;
455            for target in succs {
456                let color = if is_multi {
457                    "#e2a93b"
458                } else if last.opcode == op::JUMPI {
459                    EDGE_COND_JUMP
460                } else {
461                    EDGE_JUMP
462                };
463                let extra = if target <= bid { " constraint=false" } else { "" };
464                writeln!(w, "  {bid} -> {target} [color=\"{color}\"{extra}];")?;
465            }
466        }
467
468        // Dynamic jump table.
469        if self.has_dynamic_jumps {
470            writeln!(
471                w,
472                "  dynamic [shape=diamond style=filled fillcolor=\"{REVERT_FILL}\" \
473                 color=\"{REVERT_BORDER}\" fontcolor=\"{TEXT}\" \
474                 label=\"dynamic\\njump table\"];"
475            )?;
476            for (bid, block) in self.cfg.blocks.iter_enumerated() {
477                let first = self.inst(block.insts.start);
478                if first.is_reachable_jumpdest(self.has_dynamic_jumps) {
479                    writeln!(w, "  dynamic -> {bid} [color=\"{EDGE_FALSE}\" style=dashed];")?;
480                }
481            }
482        }
483
484        writeln!(w, "}}")
485    }
486
487    /// Returns the bytecode as a DOT graph string.
488    #[cfg(test)]
489    fn to_dot(&self) -> String {
490        let mut s = String::new();
491        self.write_dot(&mut s).unwrap();
492        s
493    }
494}
495
496fn decimal_width(n: u32) -> usize {
497    if n == 0 { 1 } else { n.ilog10() as usize + 1 }
498}
499
500/// Abbreviates hex strings with repeated leading byte pairs.
501/// E.g. `"PUSH32 0xffffffffff...ffe0"` → `"PUSH32 0xff..ffe0"`.
502fn abbreviate_hex(s: &str) -> Cow<'_, str> {
503    let Some(hex_start) = s.find("0x") else {
504        return Cow::Borrowed(s);
505    };
506    let hex = &s[hex_start + 2..];
507    // Need at least 2 byte pairs (4 hex chars) of repetition to abbreviate.
508    if hex.len() < 8 {
509        return Cow::Borrowed(s);
510    }
511    let prefix = &hex[..2];
512    let run_len = hex
513        .as_bytes()
514        .chunks(2)
515        .take_while(|chunk| chunk.len() == 2 && *chunk == prefix.as_bytes())
516        .count();
517    if run_len < 4 {
518        return Cow::Borrowed(s);
519    }
520    let suffix = &hex[run_len * 2..];
521    Cow::Owned(format!("{}0x{prefix}..{suffix}", &s[..hex_start]))
522}
523
524#[cfg(test)]
525mod tests {
526    use super::*;
527    use revm_bytecode::opcode as op;
528    use revm_primitives::hardfork::SpecId;
529
530    /// Test bytecode with SSTORE (splits gas but not stack), a loop (back-edge), and CALL
531    /// (suspending instruction that splits both gas and stack sections).
532    fn test_bytecode() -> Bytecode<'static> {
533        #[rustfmt::skip]
534        let code: &[u8] = &[
535            op::PUSH1, 0x03,
536            op::JUMP,
537            op::JUMPDEST,
538            op::PUSH1, 0x01,
539            op::PUSH1, 0x00,
540            op::SSTORE,
541            op::PUSH1, 0x01,
542            op::PUSH1, 0x03,
543            op::JUMPI,
544            op::PUSH1, 0x00,
545            op::PUSH1, 0x00,
546            op::PUSH1, 0x00,
547            op::PUSH1, 0x00,
548            op::PUSH1, 0x00,
549            op::PUSH1, 0x42,
550            op::PUSH2, 0xff, 0xff,
551            op::CALL,
552            op::POP,
553            op::STOP,
554        ];
555        let mut bytecode = Bytecode::new(code, SpecId::OSAKA, None);
556        bytecode.analyze().unwrap();
557        bytecode
558    }
559
560    #[test]
561    fn display_format() {
562        let bytecode = test_bytecode();
563        let actual = format!("{bytecode}");
564        snapbox::assert_data_eq!(
565            actual,
566            snapbox::str![[r#"
567; spec_id=Osaka has_dynamic_jumps=false may_suspend=true
568; insts=19 live=19 dead=0 noops=11 suspends=1 blocks=3 block_min=2 block_max=10 block_avg=6.3 block_median=7
569
570bb0:           ; stack_in=0 max_growth=1 predecessors=
571  PUSH1 0x03   ; ic= 0 pc= 0 gas=11 noop
572  JUMP %bb1    ; ic= 1 pc= 2
573
574bb1:           ; stack_in=0 max_growth=2 predecessors=bb0,bb1
575  JUMPDEST     ; ic= 2 pc= 3 gas=7 reachable
576  PUSH1 0x01   ; ic= 3 pc= 4 noop
577  PUSH1 0x00   ; ic= 4 pc= 6 noop
578  SSTORE       ; ic= 5 pc= 8
579  PUSH1 0x01   ; ic= 6 pc= 9 gas=16 noop
580  PUSH1 0x03   ; ic= 7 pc=11 noop
581  JUMPI %bb1   ; ic= 8 pc=13
582
583bb2:           ; stack_in=0 max_growth=7 predecessors=bb1
584  PUSH1 0x00   ; ic= 9 pc=14 gas=121
585  PUSH1 0x00   ; ic=10 pc=16 noop
586  PUSH1 0x00   ; ic=11 pc=18 noop
587  PUSH1 0x00   ; ic=12 pc=20 noop
588  PUSH1 0x00   ; ic=13 pc=22 noop
589  PUSH1 0x42   ; ic=14 pc=24 noop
590  PUSH2 0xffff ; ic=15 pc=26 noop
591  CALL         ; ic=16 pc=29 suspends
592  POP          ; ic=17 pc=30 gas=2 stack_in=1 max_growth=0
593  STOP         ; ic=18 pc=31
594
595"#]]
596        );
597    }
598
599    #[test]
600    fn display_roundtrip() {
601        let bytecode = test_bytecode();
602        let displayed = format!("{bytecode}");
603
604        // Strip the resolved jump-target refs (e.g. ` %bb1`) from JUMP/JUMPI
605        // lines — these are display metadata, not asm operands.
606        let stripped: String = displayed
607            .lines()
608            .map(|line| {
609                let (before_comment, comment) = line.split_once(';').unwrap_or((line, ""));
610                let cleaned = before_comment
611                    .replace(" %bb", " ; %bb")
612                    .replace(" %dynamic", " ; %dynamic")
613                    .replace(" %invalid", " ; %invalid");
614                if comment.is_empty() {
615                    format!("{cleaned}\n")
616                } else {
617                    format!("{cleaned};{comment}\n")
618                }
619            })
620            .collect();
621
622        println!("{stripped}");
623        let reparsed = crate::parse_asm(&stripped).unwrap();
624        assert_eq!(reparsed, bytecode.code.as_ref(), "display output did not roundtrip");
625    }
626
627    #[test]
628    fn dot_format() {
629        let bytecode = test_bytecode();
630        let dot = bytecode.to_dot();
631        eprintln!("{dot}");
632        assert!(dot.starts_with("digraph bytecode {"));
633        assert!(dot.contains("bb0"));
634        assert!(dot.contains("bb1"));
635        assert!(dot.contains("bb2"));
636        // SSTORE present in bb1.
637        assert!(dot.contains("SSTORE"), "missing SSTORE");
638        // SSTORE splits gas sections: two [g=] annotations in bb1.
639        assert!(dot.contains("[g=7]"), "missing first gas section");
640        assert!(dot.contains("[g=16]"), "missing second gas section");
641        // CALL present in bb2.
642        assert!(dot.contains("CALL"), "missing CALL");
643        assert!(dot.contains("[g=121]"), "missing CALL gas section");
644        // bb0 -> bb1 (unconditional jump).
645        assert!(dot.contains("bb0 -> bb1"), "missing jump edge");
646        // bb1 -> bb1 (loop back-edge).
647        assert!(dot.contains("bb1 -> bb1"), "missing loop back-edge");
648        // bb1 -> bb2 (fallthrough on false).
649        assert!(dot.contains("bb1 -> bb2"), "missing false edge");
650        assert!(!dot.contains("dynamic"), "unexpected dynamic jump table");
651    }
652
653    #[test]
654    fn abbreviate_hex_repeated() {
655        // 32 repeated ff bytes + suffix.
656        assert_eq!(
657            abbreviate_hex(
658                "PUSH32 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0"
659            ),
660            "PUSH32 0xff..e0",
661        );
662        assert_eq!(
663            abbreviate_hex(
664                "PUSH32 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffeeee"
665            ),
666            "PUSH32 0xff..eeee",
667        );
668        assert_eq!(
669            abbreviate_hex(
670                "PUSH32 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffeeeeee"
671            ),
672            "PUSH32 0xff..eeeeee",
673        );
674        // 32 repeated 00 bytes + suffix.
675        assert_eq!(
676            abbreviate_hex(
677                "PUSH32 0x0000000000000000000000000000000000000000000000000000000000000001"
678            ),
679            "PUSH32 0x00..01",
680        );
681        // All repeated, no suffix.
682        assert_eq!(
683            abbreviate_hex(
684                "PUSH32 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
685            ),
686            "PUSH32 0xff..",
687        );
688    }
689
690    #[test]
691    fn abbreviate_hex_short() {
692        // Too few repeated pairs (< 4).
693        assert_eq!(abbreviate_hex("PUSH3 0xffffff"), "PUSH3 0xffffff");
694        // No repetition.
695        assert_eq!(abbreviate_hex("PUSH4 0x30627b7c"), "PUSH4 0x30627b7c");
696        // Short value.
697        assert_eq!(abbreviate_hex("PUSH1 0x40"), "PUSH1 0x40");
698        // No hex at all.
699        assert_eq!(abbreviate_hex("STOP"), "STOP");
700    }
701}