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 fn target_block(&self, target: Inst) -> Option<Block> {
15 self.cfg.inst_to_block.get(target).copied().flatten()
16 }
17
18 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 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 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 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 while header.len() < 2 {
85 header.push(' ');
86 }
87 lines.push((header, comment));
88
89 for inst in block.insts() {
91 let data = self.inst(inst);
92 if data.is_dead_code() {
93 continue;
94 }
95
96 inst_lines[inst] = lines.len() as u32 + 1;
98
99 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 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 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 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
215const 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
229fn 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
252fn 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 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 STOP | RETURN | REVERT | INVALID | SELFDESTRUCT => RED,
269 JUMP
271 | JUMPI
272 | JUMPDEST
273 | CREATE
274 | CALL
275 | CALLCODE
276 | DELEGATECALL
277 | CREATE2
278 | STATICCALL
279 | LOG0..=LOG4 => GREEN,
280 ADD..=SIGNEXTEND | LT..=CLZ | KECCAK256 => WHITE,
282 ADDRESS..=SLOTNUM | MLOAD..=MCOPY => BLUE,
284 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
315mod 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 pub(super) const NODE_FILL: &str = DARK_BLUE;
335 pub(super) const NODE_BORDER: &str = BLUE;
336 pub(super) const REVERT_FILL: &str = DARK_RED;
338 pub(super) const REVERT_BORDER: &str = RED;
339 pub(super) const EXIT_FILL: &str = DARK_GREEN;
341 pub(super) const EXIT_BORDER: &str = GREEN;
342 pub(super) const SUSPEND_FILL: &str = DARK_ORANGE;
344 pub(super) const SUSPEND_BORDER: &str = ORANGE;
345 pub(super) const BRANCH_FILL: &str = DARK_TEAL;
347 pub(super) const BRANCH_BORDER: &str = TEAL;
348 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 #[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 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 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 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 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 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 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 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 #[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
500fn 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 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 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 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 assert!(dot.contains("SSTORE"), "missing SSTORE");
638 assert!(dot.contains("[g=7]"), "missing first gas section");
640 assert!(dot.contains("[g=16]"), "missing second gas section");
641 assert!(dot.contains("CALL"), "missing CALL");
643 assert!(dot.contains("[g=121]"), "missing CALL gas section");
644 assert!(dot.contains("bb0 -> bb1"), "missing jump edge");
646 assert!(dot.contains("bb1 -> bb1"), "missing loop back-edge");
648 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 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 assert_eq!(
676 abbreviate_hex(
677 "PUSH32 0x0000000000000000000000000000000000000000000000000000000000000001"
678 ),
679 "PUSH32 0x00..01",
680 );
681 assert_eq!(
683 abbreviate_hex(
684 "PUSH32 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
685 ),
686 "PUSH32 0xff..",
687 );
688 }
689
690 #[test]
691 fn abbreviate_hex_short() {
692 assert_eq!(abbreviate_hex("PUSH3 0xffffff"), "PUSH3 0xffffff");
694 assert_eq!(abbreviate_hex("PUSH4 0x30627b7c"), "PUSH4 0x30627b7c");
696 assert_eq!(abbreviate_hex("PUSH1 0x40"), "PUSH1 0x40");
698 assert_eq!(abbreviate_hex("STOP"), "STOP");
700 }
701}