foundry_debugger/tui/
draw.rs

1//! TUI draw implementation.
2
3use super::context::TUIContext;
4use crate::op::OpcodeParam;
5use foundry_compilers::artifacts::sourcemap::SourceElement;
6use foundry_evm_core::buffer::{get_buffer_accesses, BufferKind};
7use foundry_evm_traces::debug::SourceData;
8use ratatui::{
9    layout::{Alignment, Constraint, Direction, Layout, Rect},
10    style::{Color, Modifier, Style},
11    text::{Line, Span, Text},
12    widgets::{Block, Borders, List, ListItem, ListState, Paragraph, Wrap},
13    Frame,
14};
15use revm_inspectors::tracing::types::CallKind;
16use std::{collections::VecDeque, fmt::Write, io};
17
18impl TUIContext<'_> {
19    /// Draws the TUI layout and subcomponents to the given terminal.
20    pub(crate) fn draw(&self, terminal: &mut super::DebuggerTerminal) -> io::Result<()> {
21        terminal.draw(|f| self.draw_layout(f)).map(drop)
22    }
23
24    #[inline]
25    fn draw_layout(&self, f: &mut Frame<'_>) {
26        // We need 100 columns to display a 32 byte word in the memory and stack panes.
27        let area = f.area();
28        let min_width = 100;
29        let min_height = 16;
30        if area.width < min_width || area.height < min_height {
31            self.size_too_small(f, min_width, min_height);
32            return;
33        }
34
35        // The horizontal layout draws these panes at 50% width.
36        let min_column_width_for_horizontal = 200;
37        if area.width >= min_column_width_for_horizontal {
38            self.horizontal_layout(f);
39        } else {
40            self.vertical_layout(f);
41        }
42    }
43
44    fn size_too_small(&self, f: &mut Frame<'_>, min_width: u16, min_height: u16) {
45        let mut lines = Vec::with_capacity(4);
46
47        let l1 = "Terminal size too small:";
48        lines.push(Line::from(l1));
49
50        let area = f.area();
51        let width_color = if area.width >= min_width { Color::Green } else { Color::Red };
52        let height_color = if area.height >= min_height { Color::Green } else { Color::Red };
53        let l2 = vec![
54            Span::raw("Width = "),
55            Span::styled(area.width.to_string(), Style::new().fg(width_color)),
56            Span::raw(" Height = "),
57            Span::styled(area.height.to_string(), Style::new().fg(height_color)),
58        ];
59        lines.push(Line::from(l2));
60
61        let l3 = "Needed for current config:";
62        lines.push(Line::from(l3));
63        let l4 = format!("Width = {min_width} Height = {min_height}");
64        lines.push(Line::from(l4));
65
66        let paragraph =
67            Paragraph::new(lines).alignment(Alignment::Center).wrap(Wrap { trim: true });
68        f.render_widget(paragraph, area)
69    }
70
71    /// Draws the layout in vertical mode.
72    ///
73    /// ```text
74    /// |-----------------------------|
75    /// |             op              |
76    /// |-----------------------------|
77    /// |            stack            |
78    /// |-----------------------------|
79    /// |             buf             |
80    /// |-----------------------------|
81    /// |                             |
82    /// |             src             |
83    /// |                             |
84    /// |-----------------------------|
85    /// ```
86    fn vertical_layout(&self, f: &mut Frame<'_>) {
87        let area = f.area();
88        let h_height = if self.show_shortcuts { 4 } else { 0 };
89
90        // NOTE: `Layout::split` always returns a slice of the same length as the number of
91        // constraints, so the `else` branch is unreachable.
92
93        // Split off footer.
94        let [app, footer] = Layout::new(
95            Direction::Vertical,
96            [Constraint::Ratio(100 - h_height, 100), Constraint::Ratio(h_height, 100)],
97        )
98        .split(area)[..] else {
99            unreachable!()
100        };
101
102        // Split the app in 4 vertically to construct all the panes.
103        let [op_pane, stack_pane, memory_pane, src_pane] = Layout::new(
104            Direction::Vertical,
105            [
106                Constraint::Ratio(1, 6),
107                Constraint::Ratio(1, 6),
108                Constraint::Ratio(1, 6),
109                Constraint::Ratio(3, 6),
110            ],
111        )
112        .split(app)[..] else {
113            unreachable!()
114        };
115
116        if self.show_shortcuts {
117            self.draw_footer(f, footer);
118        }
119        self.draw_src(f, src_pane);
120        self.draw_op_list(f, op_pane);
121        self.draw_stack(f, stack_pane);
122        self.draw_buffer(f, memory_pane);
123    }
124
125    /// Draws the layout in horizontal mode.
126    ///
127    /// ```text
128    /// |-----------------|-----------|
129    /// |        op       |   stack   |
130    /// |-----------------|-----------|
131    /// |                 |           |
132    /// |       src       |    buf    |
133    /// |                 |           |
134    /// |-----------------|-----------|
135    /// ```
136    fn horizontal_layout(&self, f: &mut Frame<'_>) {
137        let area = f.area();
138        let h_height = if self.show_shortcuts { 4 } else { 0 };
139
140        // Split off footer.
141        let [app, footer] = Layout::new(
142            Direction::Vertical,
143            [Constraint::Ratio(100 - h_height, 100), Constraint::Ratio(h_height, 100)],
144        )
145        .split(area)[..] else {
146            unreachable!()
147        };
148
149        // Split app in 2 horizontally.
150        let [app_left, app_right] =
151            Layout::new(Direction::Horizontal, [Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)])
152                .split(app)[..]
153        else {
154            unreachable!()
155        };
156
157        // Split left pane in 2 vertically to opcode list and source.
158        let [op_pane, src_pane] =
159            Layout::new(Direction::Vertical, [Constraint::Ratio(1, 4), Constraint::Ratio(3, 4)])
160                .split(app_left)[..]
161        else {
162            unreachable!()
163        };
164
165        // Split right pane horizontally to construct stack and memory.
166        let [stack_pane, memory_pane] =
167            Layout::new(Direction::Vertical, [Constraint::Ratio(1, 4), Constraint::Ratio(3, 4)])
168                .split(app_right)[..]
169        else {
170            unreachable!()
171        };
172
173        if self.show_shortcuts {
174            self.draw_footer(f, footer);
175        }
176        self.draw_src(f, src_pane);
177        self.draw_op_list(f, op_pane);
178        self.draw_stack(f, stack_pane);
179        self.draw_buffer(f, memory_pane);
180    }
181
182    fn draw_footer(&self, f: &mut Frame<'_>, area: Rect) {
183        let l1 = "[q]: quit | [k/j]: prev/next op | [a/s]: prev/next jump | [c/C]: prev/next call | [g/G]: start/end | [b]: cycle memory/calldata/returndata buffers";
184        let l2 = "[t]: stack labels | [m]: buffer decoding | [shift + j/k]: scroll stack | [ctrl + j/k]: scroll buffer | ['<char>]: goto breakpoint | [h] toggle help";
185        let dimmed = Style::new().add_modifier(Modifier::DIM);
186        let lines =
187            vec![Line::from(Span::styled(l1, dimmed)), Line::from(Span::styled(l2, dimmed))];
188        let paragraph =
189            Paragraph::new(lines).alignment(Alignment::Center).wrap(Wrap { trim: false });
190        f.render_widget(paragraph, area);
191    }
192
193    fn draw_src(&self, f: &mut Frame<'_>, area: Rect) {
194        let (text_output, source_name) = self.src_text(area);
195        let call_kind_text = match self.call_kind() {
196            CallKind::Create | CallKind::Create2 => "Contract creation",
197            CallKind::Call => "Contract call",
198            CallKind::StaticCall => "Contract staticcall",
199            CallKind::CallCode => "Contract callcode",
200            CallKind::DelegateCall => "Contract delegatecall",
201            CallKind::AuthCall => "Contract authcall",
202        };
203        let title = format!(
204            "{} {} ",
205            call_kind_text,
206            source_name.map(|s| format!("| {s}")).unwrap_or_default()
207        );
208        let block = Block::default().title(title).borders(Borders::ALL);
209        let paragraph = Paragraph::new(text_output).block(block).wrap(Wrap { trim: false });
210        f.render_widget(paragraph, area);
211    }
212
213    fn src_text(&self, area: Rect) -> (Text<'_>, Option<&str>) {
214        let (source_element, source) = match self.src_map() {
215            Ok(r) => r,
216            Err(e) => return (Text::from(e), None),
217        };
218
219        // We are handed a vector of SourceElements that give us a span of sourcecode that is
220        // currently being executed. This includes an offset and length.
221        // This vector is in instruction pointer order, meaning the location of the instruction
222        // minus `sum(push_bytes[..pc])`.
223        let offset = source_element.offset() as usize;
224        let len = source_element.length() as usize;
225        let max = source.source.len();
226
227        // Split source into before, relevant, and after chunks, split by line, for formatting.
228        let actual_start = offset.min(max);
229        let actual_end = (offset + len).min(max);
230
231        let mut before: Vec<_> = source.source[..actual_start].split_inclusive('\n').collect();
232        let actual: Vec<_> =
233            source.source[actual_start..actual_end].split_inclusive('\n').collect();
234        let mut after: VecDeque<_> = source.source[actual_end..].split_inclusive('\n').collect();
235
236        let num_lines = before.len() + actual.len() + after.len();
237        let height = area.height as usize;
238        let needed_highlight = actual.len();
239        let mid_len = before.len() + actual.len();
240
241        // adjust what text we show of the source code
242        let (start_line, end_line) = if needed_highlight > height {
243            // highlighted section is more lines than we have available
244            let start_line = before.len().saturating_sub(1);
245            (start_line, before.len() + needed_highlight)
246        } else if height > num_lines {
247            // we can fit entire source
248            (0, num_lines)
249        } else {
250            let remaining = height - needed_highlight;
251            let mut above = remaining / 2;
252            let mut below = remaining / 2;
253            if below > after.len() {
254                // unused space below the highlight
255                above += below - after.len();
256            } else if above > before.len() {
257                // we have unused space above the highlight
258                below += above - before.len();
259            } else {
260                // no unused space
261            }
262
263            // since above is subtracted from before.len(), and the resulting
264            // start_line is used to index into before, above must be at least
265            // 1 to avoid out-of-range accesses.
266            if above == 0 {
267                above = 1;
268            }
269            (before.len().saturating_sub(above), mid_len + below)
270        };
271
272        // Unhighlighted line number: gray.
273        let u_num = Style::new().fg(Color::Gray);
274        // Unhighlighted text: default, dimmed.
275        let u_text = Style::new().add_modifier(Modifier::DIM);
276        // Highlighted line number: cyan.
277        let h_num = Style::new().fg(Color::Cyan);
278        // Highlighted text: cyan, bold.
279        let h_text = Style::new().fg(Color::Cyan).add_modifier(Modifier::BOLD);
280
281        let mut lines = SourceLines::new(start_line, end_line);
282
283        // We check if there is other text on the same line before the highlight starts.
284        if let Some(last) = before.pop() {
285            let last_has_nl = last.ends_with('\n');
286
287            if last_has_nl {
288                before.push(last);
289            }
290            for line in &before[start_line..] {
291                lines.push(u_num, line, u_text);
292            }
293
294            let first = if !last_has_nl {
295                lines.push_raw(h_num, &[Span::raw(last), Span::styled(actual[0], h_text)]);
296                1
297            } else {
298                0
299            };
300
301            // Skip the first line if it has already been handled above.
302            for line in &actual[first..] {
303                lines.push(h_num, line, h_text);
304            }
305        } else {
306            // No text before the current line.
307            for line in &actual {
308                lines.push(h_num, line, h_text);
309            }
310        }
311
312        // Fill in the rest of the line as unhighlighted.
313        if let Some(last) = actual.last() {
314            if !last.ends_with('\n') {
315                if let Some(post) = after.pop_front() {
316                    if let Some(last) = lines.lines.last_mut() {
317                        last.spans.push(Span::raw(post));
318                    }
319                }
320            }
321        }
322
323        // Add after highlighted text.
324        while mid_len + after.len() > end_line {
325            after.pop_back();
326        }
327        for line in after {
328            lines.push(u_num, line, u_text);
329        }
330
331        // pad with empty to each line to ensure the previous text is cleared
332        for line in &mut lines.lines {
333            // note that the \n is not included in the line length
334            if area.width as usize > line.width() + 1 {
335                line.push_span(Span::raw(" ".repeat(area.width as usize - line.width() - 1)));
336            }
337        }
338
339        (Text::from(lines.lines), source.path.to_str())
340    }
341
342    /// Returns source map, source code and source name of the current line.
343    fn src_map(&self) -> Result<(SourceElement, &SourceData), String> {
344        let address = self.address();
345        let Some(contract_name) = self.debugger_context.identified_contracts.get(address) else {
346            return Err(format!("Unknown contract at address {address}"));
347        };
348
349        self.debugger_context
350            .contracts_sources
351            .find_source_mapping(
352                contract_name,
353                self.current_step().pc as u32,
354                self.debug_call().kind.is_any_create(),
355            )
356            .ok_or_else(|| format!("No source map for contract {contract_name}"))
357    }
358
359    fn draw_op_list(&self, f: &mut Frame<'_>, area: Rect) {
360        let debug_steps = self.debug_steps();
361        let max_pc = debug_steps.iter().map(|step| step.pc).max().unwrap_or(0);
362        let max_pc_len = hex_digits(max_pc);
363
364        let items = debug_steps
365            .iter()
366            .enumerate()
367            .map(|(i, step)| {
368                let mut content = String::with_capacity(64);
369                write!(content, "{:0>max_pc_len$x}|", step.pc).unwrap();
370                if let Some(op) = self.opcode_list.get(i) {
371                    content.push_str(op);
372                }
373                ListItem::new(Span::styled(content, Style::new().fg(Color::White)))
374            })
375            .collect::<Vec<_>>();
376
377        let title = format!(
378            "Address: {} | PC: {} | Gas used in call: {} | Code section: {}",
379            self.address(),
380            self.current_step().pc,
381            self.current_step().gas_used,
382            self.current_step().code_section_idx,
383        );
384        let block = Block::default().title(title).borders(Borders::ALL);
385        let list = List::new(items)
386            .block(block)
387            .highlight_symbol("▶")
388            .highlight_style(Style::new().fg(Color::White).bg(Color::DarkGray))
389            .scroll_padding(1);
390        let mut state = ListState::default().with_selected(Some(self.current_step));
391        f.render_stateful_widget(list, area, &mut state);
392    }
393
394    fn draw_stack(&self, f: &mut Frame<'_>, area: Rect) {
395        let step = self.current_step();
396        let stack = step.stack.as_ref();
397        let stack_len = stack.map_or(0, |s| s.len());
398
399        let min_len = decimal_digits(stack_len).max(2);
400
401        let params = OpcodeParam::of(step.op.get(), step.immediate_bytes.as_ref());
402
403        let text: Vec<Line<'_>> = stack
404            .map(|stack| {
405                stack
406                    .iter()
407                    .rev()
408                    .enumerate()
409                    .skip(self.draw_memory.current_stack_startline)
410                    .map(|(i, stack_item)| {
411                        let param = params
412                            .as_ref()
413                            .and_then(|params| params.iter().find(|param| param.index == i));
414
415                        let mut spans = Vec::with_capacity(1 + 32 * 2 + 3);
416
417                        // Stack index.
418                        spans.push(Span::styled(
419                            format!("{i:0min_len$}| "),
420                            Style::new().fg(Color::White),
421                        ));
422
423                        // Item hex bytes.
424                        hex_bytes_spans(&stack_item.to_be_bytes::<32>(), &mut spans, |_, _| {
425                            if param.is_some() {
426                                Style::new().fg(Color::Cyan)
427                            } else {
428                                Style::new().fg(Color::White)
429                            }
430                        });
431
432                        if self.stack_labels {
433                            if let Some(param) = param {
434                                spans.push(Span::raw("| "));
435                                spans.push(Span::raw(param.name));
436                            }
437                        }
438
439                        spans.push(Span::raw("\n"));
440
441                        Line::from(spans)
442                    })
443                    .collect()
444            })
445            .unwrap_or_default();
446
447        let title = format!("Stack: {stack_len}");
448        let block = Block::default().title(title).borders(Borders::ALL);
449        let paragraph = Paragraph::new(text).block(block).wrap(Wrap { trim: true });
450        f.render_widget(paragraph, area);
451    }
452
453    fn draw_buffer(&self, f: &mut Frame<'_>, area: Rect) {
454        let call = self.debug_call();
455        let step = self.current_step();
456        let buf = match self.active_buffer {
457            BufferKind::Memory => step.memory.as_ref().unwrap().as_ref(),
458            BufferKind::Calldata => call.calldata.as_ref(),
459            BufferKind::Returndata => step.returndata.as_ref(),
460        };
461
462        let min_len = hex_digits(buf.len());
463
464        // Color memory region based on read/write.
465        let mut offset = None;
466        let mut len = None;
467        let mut write_offset = None;
468        let mut write_size = None;
469        let mut color = None;
470        let stack_len = step.stack.as_ref().map_or(0, |s| s.len());
471        if stack_len > 0 {
472            if let Some(stack) = step.stack.as_ref() {
473                if let Some(accesses) = get_buffer_accesses(step.op.get(), stack) {
474                    if let Some(read_access) = accesses.read {
475                        offset = Some(read_access.1.offset);
476                        len = Some(read_access.1.len);
477                        color = Some(Color::Cyan);
478                    }
479                    if let Some(write_access) = accesses.write {
480                        if self.active_buffer == BufferKind::Memory {
481                            write_offset = Some(write_access.offset);
482                            write_size = Some(write_access.len);
483                        }
484                    }
485                }
486            }
487        }
488
489        // color word on previous write op
490        // TODO: technically it's possible for this to conflict with the current op, ie, with
491        // subsequent MCOPYs, but solc can't seem to generate that code even with high optimizer
492        // settings
493        if self.current_step > 0 {
494            let prev_step = self.current_step - 1;
495            let prev_step = &self.debug_steps()[prev_step];
496            if let Some(stack) = prev_step.stack.as_ref() {
497                if let Some(write_access) =
498                    get_buffer_accesses(prev_step.op.get(), stack).and_then(|a| a.write)
499                {
500                    if self.active_buffer == BufferKind::Memory {
501                        offset = Some(write_access.offset);
502                        len = Some(write_access.len);
503                        color = Some(Color::Green);
504                    }
505                }
506            }
507        }
508
509        let height = area.height as usize;
510        let end_line = self.draw_memory.current_buf_startline + height;
511
512        let text: Vec<Line<'_>> = buf
513            .chunks(32)
514            .enumerate()
515            .skip(self.draw_memory.current_buf_startline)
516            .take_while(|(i, _)| *i < end_line)
517            .map(|(i, buf_word)| {
518                let mut spans = Vec::with_capacity(1 + 32 * 2 + 1 + 32 / 4 + 1);
519
520                // Buffer index.
521                spans.push(Span::styled(
522                    format!("{:0min_len$x}| ", i * 32),
523                    Style::new().fg(Color::White),
524                ));
525
526                // Word hex bytes.
527                hex_bytes_spans(buf_word, &mut spans, |j, _| {
528                    let mut byte_color = Color::White;
529                    let mut end = None;
530                    let idx = i * 32 + j;
531                    if let (Some(offset), Some(len), Some(color)) = (offset, len, color) {
532                        end = Some(offset + len);
533                        if (offset..offset + len).contains(&idx) {
534                            // [offset, offset + len] is the memory region to be colored.
535                            // If a byte at row i and column j in the memory panel
536                            // falls in this region, set the color.
537                            byte_color = color;
538                        }
539                    }
540                    if let (Some(write_offset), Some(write_size)) = (write_offset, write_size) {
541                        // check for overlap with read region
542                        let write_end = write_offset + write_size;
543                        if let Some(read_end) = end {
544                            let read_start = offset.unwrap();
545                            if (write_offset..write_end).contains(&read_end) {
546                                // if it contains end, start from write_start up to read_end
547                                if (write_offset..read_end).contains(&idx) {
548                                    return Style::new().fg(Color::Yellow);
549                                }
550                            } else if (write_offset..write_end).contains(&read_start) {
551                                // otherwise if it contains read start, start from read_start up to
552                                // write_end
553                                if (read_start..write_end).contains(&idx) {
554                                    return Style::new().fg(Color::Yellow);
555                                }
556                            }
557                        }
558                        if (write_offset..write_end).contains(&idx) {
559                            byte_color = Color::Red;
560                        }
561                    }
562
563                    Style::new().fg(byte_color)
564                });
565
566                if self.buf_utf {
567                    spans.push(Span::raw("|"));
568                    for utf in buf_word.chunks(4) {
569                        if let Ok(utf_str) = std::str::from_utf8(utf) {
570                            spans.push(Span::raw(utf_str.replace('\0', ".")));
571                        } else {
572                            spans.push(Span::raw("."));
573                        }
574                    }
575                }
576
577                spans.push(Span::raw("\n"));
578
579                Line::from(spans)
580            })
581            .collect();
582
583        let title = self.active_buffer.title(buf.len());
584        let block = Block::default().title(title).borders(Borders::ALL);
585        let paragraph = Paragraph::new(text).block(block).wrap(Wrap { trim: true });
586        f.render_widget(paragraph, area);
587    }
588}
589
590/// Wrapper around a list of [`Line`]s that prepends the line number on each new line.
591struct SourceLines<'a> {
592    lines: Vec<Line<'a>>,
593    start_line: usize,
594    max_line_num: usize,
595}
596
597impl<'a> SourceLines<'a> {
598    fn new(start_line: usize, end_line: usize) -> Self {
599        Self { lines: Vec::new(), start_line, max_line_num: decimal_digits(end_line) }
600    }
601
602    fn push(&mut self, line_number_style: Style, line: &'a str, line_style: Style) {
603        self.push_raw(line_number_style, &[Span::styled(line, line_style)]);
604    }
605
606    fn push_raw(&mut self, line_number_style: Style, spans: &[Span<'a>]) {
607        let mut line_spans = Vec::with_capacity(4);
608
609        let line_number = format!(
610            "{number: >width$} ",
611            number = self.start_line + self.lines.len() + 1,
612            width = self.max_line_num
613        );
614        line_spans.push(Span::styled(line_number, line_number_style));
615
616        // Space between line number and line text.
617        line_spans.push(Span::raw("  "));
618
619        line_spans.extend_from_slice(spans);
620
621        self.lines.push(Line::from(line_spans));
622    }
623}
624
625fn hex_bytes_spans(bytes: &[u8], spans: &mut Vec<Span<'_>>, f: impl Fn(usize, u8) -> Style) {
626    for (i, &byte) in bytes.iter().enumerate() {
627        if i > 0 {
628            spans.push(Span::raw(" "));
629        }
630        spans.push(Span::styled(alloy_primitives::hex::encode([byte]), f(i, byte)));
631    }
632}
633
634/// Returns the number of decimal digits in the given number.
635///
636/// This is the same as `n.to_string().len()`.
637fn decimal_digits(n: usize) -> usize {
638    n.checked_ilog10().unwrap_or(0) as usize + 1
639}
640
641/// Returns the number of hexadecimal digits in the given number.
642///
643/// This is the same as `format!("{n:x}").len()`.
644fn hex_digits(n: usize) -> usize {
645    n.checked_ilog(16).unwrap_or(0) as usize + 1
646}
647
648#[cfg(test)]
649mod tests {
650    #[test]
651    fn decimal_digits() {
652        assert_eq!(super::decimal_digits(0), 1);
653        assert_eq!(super::decimal_digits(1), 1);
654        assert_eq!(super::decimal_digits(2), 1);
655        assert_eq!(super::decimal_digits(9), 1);
656        assert_eq!(super::decimal_digits(10), 2);
657        assert_eq!(super::decimal_digits(11), 2);
658        assert_eq!(super::decimal_digits(50), 2);
659        assert_eq!(super::decimal_digits(99), 2);
660        assert_eq!(super::decimal_digits(100), 3);
661        assert_eq!(super::decimal_digits(101), 3);
662        assert_eq!(super::decimal_digits(201), 3);
663        assert_eq!(super::decimal_digits(999), 3);
664        assert_eq!(super::decimal_digits(1000), 4);
665        assert_eq!(super::decimal_digits(1001), 4);
666    }
667
668    #[test]
669    fn hex_digits() {
670        assert_eq!(super::hex_digits(0), 1);
671        assert_eq!(super::hex_digits(1), 1);
672        assert_eq!(super::hex_digits(2), 1);
673        assert_eq!(super::hex_digits(9), 1);
674        assert_eq!(super::hex_digits(10), 1);
675        assert_eq!(super::hex_digits(11), 1);
676        assert_eq!(super::hex_digits(15), 1);
677        assert_eq!(super::hex_digits(16), 2);
678        assert_eq!(super::hex_digits(17), 2);
679        assert_eq!(super::hex_digits(0xff), 2);
680        assert_eq!(super::hex_digits(0x100), 3);
681        assert_eq!(super::hex_digits(0x101), 3);
682    }
683}