Skip to main content

soldb_repl/
render.rs

1//! Text for a terminal, from [`Output`].
2//!
3//! One line says where the debugger is: the source position, then the step, program
4//! counter, opcode, and gas in parentheses, so a person reads the location first and a
5//! script finds the numbers in one place. Everything is plain text unless colors are
6//! asked for, and nothing is printed twice.
7
8use std::fmt::Write as _;
9
10use crate::response::{
11    BreakpointEvent, Level, Output, StateInfo, Stop, StopReason, ValueStatus, VariableInfo,
12};
13
14/// Renders [`Output`] as lines of text.
15#[derive(Debug, Clone, Copy, Default)]
16pub struct Renderer {
17    /// Whether to color the text with ANSI escapes.
18    pub color: bool,
19}
20
21impl Renderer {
22    #[must_use]
23    pub const fn new(color: bool) -> Self {
24        Self { color }
25    }
26
27    /// The text for one item, each line ended by a newline; empty for items that print
28    /// nothing.
29    #[must_use]
30    pub fn render(&self, output: &Output) -> String {
31        let mut text = String::new();
32        for line in self.lines(output) {
33            text.push_str(&line);
34            text.push('\n');
35        }
36        text
37    }
38
39    /// The lines for one item.
40    #[must_use]
41    pub fn lines(&self, output: &Output) -> Vec<String> {
42        match output {
43            Output::Loaded { steps, contracts } => {
44                let mut lines = vec![format!("Loaded trace with {} steps.", self.number(steps))];
45                if contracts.is_empty() {
46                    lines.push(self.dim(
47                        "No sources matched the executed contracts; stepping is by instruction.",
48                    ));
49                } else {
50                    let listed = contracts
51                        .iter()
52                        .map(|contract| match &contract.address {
53                            Some(address) => {
54                                format!("{} ({})", self.name(&contract.name), self.dim(address))
55                            }
56                            None => self.name(&contract.name),
57                        })
58                        .collect::<Vec<_>>();
59                    lines.push(format!(
60                        "{} {}",
61                        self.info("Sources loaded for:"),
62                        listed.join(", ")
63                    ));
64                }
65                lines
66            }
67            Output::Stop(stop) => self.stop_lines(stop),
68            Output::Message { level, text, note } => {
69                let mut lines = vec![match level {
70                    Level::Info => self.info_message(text),
71                    Level::Warning => self.warning(text),
72                    Level::Note => format!("{} {text}", self.warning("Note:")),
73                }];
74                if let Some(note) = note {
75                    lines.push(format!("{} {note}", self.dim("note:")));
76                }
77                lines
78            }
79            Output::Breakpoint { event, id, label } => vec![match event {
80                BreakpointEvent::Set => format!(
81                    "{} {label}",
82                    self.success(format!("Breakpoint #{id} set at"))
83                ),
84                BreakpointEvent::Cleared => format!(
85                    "{} {label}",
86                    self.info(format!("Breakpoint #{id} cleared at"))
87                ),
88            }],
89            Output::Breakpoints { breakpoints } => {
90                if breakpoints.is_empty() {
91                    vec![self.dim("No breakpoints set.")]
92                } else {
93                    breakpoints
94                        .iter()
95                        .map(|breakpoint| {
96                            format!("#{} {}", self.number(breakpoint.id), breakpoint.label)
97                        })
98                        .collect()
99                }
100            }
101            Output::Backtrace {
102                warning,
103                note,
104                frames,
105            } => {
106                let mut lines = Vec::new();
107                if let Some(warning) = warning {
108                    lines.push(self.warning(format!("warning: {warning}")));
109                }
110                if let Some(note) = note {
111                    lines.push(format!("{} {note}", self.dim("note:")));
112                }
113                for frame in frames {
114                    let mut line = format!("#{:<2} ", frame.index);
115                    if let Some(kind) = &frame.kind {
116                        let _ = write!(line, "[{kind}] ");
117                    }
118                    line.push_str(&self.name(&frame.name));
119                    if !frame.arguments.is_empty() {
120                        let arguments = frame
121                            .arguments
122                            .iter()
123                            .map(|argument| format!("{} = {}", argument.name, argument.value))
124                            .collect::<Vec<_>>()
125                            .join(", ");
126                        let _ = write!(line, "({arguments})");
127                    }
128                    if let Some(location) = &frame.location {
129                        let _ = write!(line, " at {location}");
130                    }
131                    if let Some(address) = &frame.address {
132                        let _ = write!(line, " ({})", self.address(address));
133                    }
134                    line.push_str(&self.dim(format!("  step {}, PC {}", frame.step, frame.pc)));
135                    lines.push(line);
136                }
137                lines
138            }
139            Output::Listing {
140                path,
141                current_line,
142                lines,
143            } => {
144                let mut out = vec![format!("{}:{}", self.info(path), self.number(current_line))];
145                for line in lines {
146                    if line.current {
147                        out.push(format!(
148                            "{} {}",
149                            self.bold(format!("=> {:>5} |", line.line)),
150                            self.bold(&line.text)
151                        ));
152                    } else {
153                        out.push(format!(
154                            "{} {}",
155                            self.dim(format!("   {:>5} |", line.line)),
156                            line.text
157                        ));
158                    }
159                }
160                out
161            }
162            Output::Stack { words, shown } => {
163                if words.is_empty() {
164                    return vec![self.dim("Stack: empty")];
165                }
166                let mut lines = vec![format!(
167                    "{} {} words, top first",
168                    self.info("Stack:"),
169                    self.number(words.len())
170                )];
171                for (index, word) in words.iter().enumerate().rev().take(*shown) {
172                    lines.push(format!("  [{index}] {}", self.number(word)));
173                }
174                if *shown < words.len() {
175                    lines.push(self.dim(format!("  ... {} more below", words.len() - shown)));
176                }
177                lines
178            }
179            Output::Memory(memory) => {
180                let range = if memory.start > 0 || memory.end < memory.total {
181                    format!(", showing bytes {}..{}", memory.start, memory.end)
182                } else {
183                    String::new()
184                };
185                let mut lines = vec![format!(
186                    "{} {} bytes{range}",
187                    self.info("Memory:"),
188                    self.number(memory.total)
189                )];
190                for word in &memory.words {
191                    lines.push(format!(
192                        "{} {}",
193                        self.dim(format!("0x{:04x}:", word.offset)),
194                        word.hex
195                    ));
196                }
197                lines
198            }
199            Output::Storage { address, slots } => {
200                if slots.is_empty() {
201                    return vec![self.dim("Storage: no slots read or written yet.")];
202                }
203                let mut lines = vec![match address {
204                    Some(address) => {
205                        format!(
206                            "{} {}",
207                            self.info("Storage:"),
208                            self.dim(format!("of {address}"))
209                        )
210                    }
211                    None => self.info("Storage:"),
212                }];
213                for slot in slots {
214                    let was = slot
215                        .was
216                        .as_ref()
217                        .map(|was| self.dim(format!("  (was {was})")))
218                        .unwrap_or_default();
219                    lines.push(format!("  {} = {}{was}", slot.slot, slot.value));
220                }
221                lines
222            }
223            Output::TransientStorage { address, slots } => {
224                if slots.is_empty() {
225                    return vec![self.dim(
226                        "Transient storage: nothing read or written yet; every slot is zero.",
227                    )];
228                }
229                let mut lines = vec![match address {
230                    Some(address) => format!(
231                        "{} {}",
232                        self.info("Transient storage:"),
233                        self.dim(format!("of {address}"))
234                    ),
235                    None => self.info("Transient storage:"),
236                }];
237                for slot in slots {
238                    lines.push(format!("  {} = {}", slot.slot, slot.value));
239                }
240                lines
241            }
242            Output::Calldata { bytes, data } => vec![
243                format!("{} {} bytes", self.info("Calldata:"), self.number(bytes)),
244                data.clone(),
245            ],
246            Output::ReturnData { bytes: 0, .. } => vec![self.dim("Return data: empty.")],
247            Output::ReturnData { bytes, data } => vec![
248                format!("{} {} bytes", self.info("Return data:"), self.number(bytes)),
249                data.clone(),
250            ],
251            Output::Variables {
252                warning,
253                pc,
254                locals,
255                unavailable,
256                state,
257                transient,
258            } => {
259                let mut lines = Vec::new();
260                if let Some(warning) = warning {
261                    lines.push(self.warning(format!("warning: {warning}")));
262                }
263                if locals.is_empty() {
264                    // The difference matters: locals that cannot be read here at all is
265                    // not the same as none being live at this program counter.
266                    lines.push(match unavailable {
267                        Some(reason) => format!(
268                            "{} locals are unavailable here: {reason}",
269                            self.dim("Variables:")
270                        ),
271                        None => format!(
272                            "{} no variables in scope at PC {}",
273                            self.dim("Variables:"),
274                            self.number(pc)
275                        ),
276                    });
277                }
278                for variable in locals {
279                    lines.push(self.variable(variable));
280                }
281                match state {
282                    StateInfo::Variables { variables } => {
283                        lines.push(self.dim("State:"));
284                        for variable in variables {
285                            lines.push(self.variable(variable));
286                        }
287                    }
288                    // Transient variables are state variables too; they follow under `State:`.
289                    StateInfo::None if !transient.is_empty() => lines.push(self.dim("State:")),
290                    StateInfo::None => {
291                        lines.push(self.dim("State: this contract declares no state variables"));
292                    }
293                    StateInfo::NoLayout => lines.push(self.dim(
294                        "State: no storage layout loaded; compile with `--storage-layout` to \
295                         read state variables",
296                    )),
297                    StateInfo::NoStorage => {
298                        lines.push(self.dim("State: no storage was recorded for this step"));
299                    }
300                }
301                for variable in transient {
302                    lines.push(self.variable(variable));
303                }
304                lines
305            }
306            Output::Variable { warning, variable } => {
307                let mut lines = Vec::new();
308                if let Some(warning) = warning {
309                    lines.push(self.warning(format!("warning: {warning}")));
310                }
311                lines.push(self.variable(variable));
312                lines
313            }
314            Output::Resources { contracts, json } => {
315                if *json {
316                    let document = serde_json::json!({ "contracts": contracts });
317                    return serde_json::to_string_pretty(&document)
318                        .unwrap_or_default()
319                        .lines()
320                        .map(str::to_owned)
321                        .collect();
322                }
323                if contracts.is_empty() {
324                    return vec![self.warning("No contract sources are loaded.")];
325                }
326                let mut lines = Vec::new();
327                for contract in contracts {
328                    lines.push(format!(
329                        "{} {} {}",
330                        self.name(&contract.name),
331                        self.dim(contract.address.as_deref().unwrap_or("<any address>")),
332                        self.dim(format!("[{}]", contract.environment))
333                    ));
334                    for path in &contract.sources {
335                        lines.push(format!("  {path}"));
336                    }
337                }
338                lines
339            }
340            Output::Help { lines } => lines.clone(),
341            Output::Tui | Output::Quit => Vec::new(),
342        }
343    }
344
345    /// The stop: the reason when there is one, then the location line and the source
346    /// line, or the stack in assembly mode.
347    fn stop_lines(&self, stop: &Stop) -> Vec<String> {
348        let mut lines = Vec::new();
349        match &stop.reason {
350            StopReason::Initial | StopReason::Moved => {}
351            StopReason::Breakpoint { id, label } => lines.push(format!(
352                "{} {}, {label}",
353                self.success(format!("Breakpoint #{id} hit at step")),
354                self.number(stop.step)
355            )),
356            StopReason::End => lines.push(format!(
357                "{} {}",
358                self.info("End of trace at step"),
359                self.number(stop.step)
360            )),
361            StopReason::Start => lines.push(format!(
362                "{} {}",
363                self.info("Start of trace at step"),
364                self.number(stop.step)
365            )),
366        }
367        let status = format!(
368            "step {}/{}, pc {}, {}, gas {}",
369            self.number(stop.step),
370            self.number(stop.last_step),
371            self.number(stop.pc),
372            self.opcode(&stop.op),
373            self.number(stop.gas)
374        );
375        match &stop.location {
376            Some(location) => {
377                let function = location
378                    .function
379                    .as_deref()
380                    .map(|name| format!(" in {}", self.name(name)))
381                    .unwrap_or_default();
382                let generated = if location.generated {
383                    self.dim("  (compiler-generated code for this line)")
384                } else {
385                    String::new()
386                };
387                lines.push(format!(
388                    "{}:{}{function}{generated}  {}",
389                    self.info(&location.path),
390                    self.number(location.line),
391                    self.dim(format!("({status})"))
392                ));
393                if stop.stack.is_none() {
394                    if let Some(text) = &location.text {
395                        lines.push(format!(
396                            "{} {text}",
397                            self.dim(format!("{:>5} |", location.line))
398                        ));
399                    }
400                }
401            }
402            None if stop.has_source => {
403                let address = stop
404                    .address
405                    .as_deref()
406                    .map(|address| format!(" in {}", self.address(address)))
407                    .unwrap_or_default();
408                lines.push(format!(
409                    "{}  {}",
410                    self.dim(format!("no source for this step{address}")),
411                    self.dim(format!("({status})"))
412                ));
413            }
414            None => lines.push(status),
415        }
416        if let Some(stack) = &stop.stack {
417            lines.push(format!(
418                "{} {}",
419                self.info("Stack:"),
420                self.stack_summary(stack)
421            ));
422        }
423        if let Some(note) = &stop.note {
424            lines.push(format!("{} {note}", self.warning("Note:")));
425        }
426        lines
427    }
428
429    /// The top of the stack on one line, the words numbered from the bottom.
430    fn stack_summary(&self, top_first: &[String]) -> String {
431        if top_first.is_empty() {
432            return self.dim("empty");
433        }
434        let total = top_first.len();
435        let mut parts = top_first
436            .iter()
437            .take(4)
438            .enumerate()
439            .map(|(offset, word)| {
440                format!(
441                    "{} {}",
442                    self.dim(format!("[{}]", total - 1 - offset)),
443                    self.number(shorten_hex(word))
444                )
445            })
446            .collect::<Vec<_>>();
447        if total > 4 {
448            parts.push(self.dim(format!("... +{} more", total - 4)));
449        }
450        parts.join(" ")
451    }
452
453    fn variable(&self, variable: &VariableInfo) -> String {
454        let value = match variable.status {
455            ValueStatus::Unavailable => self.warning(&variable.value),
456            _ => self.success(&variable.value),
457        };
458        let place = variable
459            .place
460            .as_ref()
461            .map(|place| self.dim(format!(" [{place}]")))
462            .unwrap_or_default();
463        format!(
464            "{} {} = {value}{place}",
465            self.info(&variable.ty),
466            self.bold(&variable.name)
467        )
468    }
469
470    fn info_message(&self, text: &str) -> String {
471        // `Label: rest` colors the label; a bare sentence is printed as it is.
472        match text.split_once(": ") {
473            Some((label, rest)) if !label.contains(' ') || label.starts_with("Mode") => {
474                format!("{} {rest}", self.info(format!("{label}:")))
475            }
476            _ => self.info(text),
477        }
478    }
479
480    fn paint(&self, text: impl std::fmt::Display, code: &str) -> String {
481        if self.color {
482            format!("\x1b[{code}m{text}\x1b[0m")
483        } else {
484            text.to_string()
485        }
486    }
487
488    fn bold(&self, text: impl std::fmt::Display) -> String {
489        self.paint(text, "1")
490    }
491
492    fn dim(&self, text: impl std::fmt::Display) -> String {
493        self.paint(text, "2")
494    }
495
496    fn info(&self, text: impl std::fmt::Display) -> String {
497        self.paint(text, "96")
498    }
499
500    fn success(&self, text: impl std::fmt::Display) -> String {
501        self.paint(text, "92")
502    }
503
504    fn warning(&self, text: impl std::fmt::Display) -> String {
505        self.paint(text, "93")
506    }
507
508    fn number(&self, text: impl std::fmt::Display) -> String {
509        self.paint(text, "93")
510    }
511
512    fn opcode(&self, text: impl std::fmt::Display) -> String {
513        self.paint(text, "94")
514    }
515
516    fn name(&self, text: impl std::fmt::Display) -> String {
517        self.paint(text, "95")
518    }
519
520    fn address(&self, text: impl std::fmt::Display) -> String {
521        self.paint(text, "95")
522    }
523}
524
525/// A long hex word as its first digits: `0x1234...`.
526#[must_use]
527pub fn shorten_hex(value: &str) -> String {
528    let Some(digits) = value.strip_prefix("0x") else {
529        return value.to_owned();
530    };
531    if value.len() <= 10 {
532        return value.to_owned();
533    }
534    let head = digits.chars().take(4).collect::<String>();
535    format!("0x{head}...")
536}
537
538#[cfg(test)]
539mod tests {
540    use super::{shorten_hex, Renderer};
541    use crate::response::{
542        Level, Output, SlotInfo, StateInfo, Stop, StopLocation, StopReason, ValueStatus,
543        VariableInfo,
544    };
545
546    #[test]
547    fn transient_storage_lists_its_slots_or_says_every_one_is_zero() {
548        let renderer = Renderer::new(false);
549        assert_eq!(
550            renderer.render(&Output::TransientStorage {
551                address: Some("0xabc".to_owned()),
552                slots: vec![SlotInfo {
553                    slot: "0x0".to_owned(),
554                    value: "0x2".to_owned(),
555                    was: None,
556                }],
557            }),
558            "Transient storage: of 0xabc\n  0x0 = 0x2\n"
559        );
560        assert_eq!(
561            renderer.render(&Output::TransientStorage {
562                address: None,
563                slots: Vec::new(),
564            }),
565            "Transient storage: nothing read or written yet; every slot is zero.\n"
566        );
567    }
568
569    #[test]
570    fn return_data_is_its_size_and_hex_or_says_it_is_empty() {
571        let renderer = Renderer::new(false);
572        assert_eq!(
573            renderer.render(&Output::ReturnData {
574                bytes: 1,
575                data: "0x2a".to_owned()
576            }),
577            "Return data: 1 bytes\n0x2a\n"
578        );
579        assert_eq!(
580            renderer.render(&Output::ReturnData {
581                bytes: 0,
582                data: "0x".to_owned()
583            }),
584            "Return data: empty.\n"
585        );
586    }
587
588    #[test]
589    fn transient_variables_follow_the_state_and_keep_its_reason() {
590        let renderer = Renderer::new(false);
591        let variables = |state| Output::Variables {
592            warning: None,
593            pc: 7,
594            locals: vec![VariableInfo {
595                name: "a".to_owned(),
596                ty: "uint256".to_owned(),
597                value: "1".to_owned(),
598                raw: None,
599                status: ValueStatus::Decoded,
600                place: Some("stack+1".to_owned()),
601            }],
602            unavailable: None,
603            state,
604            transient: vec![VariableInfo {
605                name: "locked".to_owned(),
606                ty: "bool".to_owned(),
607                value: "true".to_owned(),
608                raw: None,
609                status: ValueStatus::Decoded,
610                place: Some("transient slot 0x0".to_owned()),
611            }],
612        };
613        // A layout with no storage variables does not say the contract has none.
614        assert_eq!(
615            renderer.render(&variables(StateInfo::None)),
616            "uint256 a = 1 [stack+1]\nState:\nbool locked = true [transient slot 0x0]\n"
617        );
618        // A missing storage layout is still reported.
619        assert!(renderer
620            .render(&variables(StateInfo::NoLayout))
621            .contains("no storage layout loaded"));
622    }
623
624    #[test]
625    fn a_stop_is_one_location_line_and_the_source_line() {
626        let renderer = Renderer::new(false);
627        let stop = Stop {
628            reason: StopReason::Breakpoint {
629                id: 1,
630                label: "Counter.sol:7".to_owned(),
631            },
632            step: 12,
633            last_step: 99,
634            pc: 34,
635            op: "PUSH1".to_owned(),
636            gas: 5000,
637            location: Some(StopLocation {
638                path: "Counter.sol".to_owned(),
639                line: 7,
640                function: Some("increment".to_owned()),
641                generated: false,
642                text: Some("        count += 1;".to_owned()),
643            }),
644            address: None,
645            has_source: true,
646            stack: None,
647            note: Some("something".to_owned()),
648        };
649        assert_eq!(
650            renderer.render(&Output::Stop(stop)),
651            "Breakpoint #1 hit at step 12, Counter.sol:7\n\
652             Counter.sol:7 in increment  (step 12/99, pc 34, PUSH1, gas 5000)\n    \
653             7 |         count += 1;\n\
654             Note: something\n"
655        );
656        let bare = Stop {
657            reason: StopReason::Moved,
658            step: 0,
659            last_step: 3,
660            pc: 0,
661            op: "PUSH1".to_owned(),
662            gas: 1,
663            location: None,
664            address: Some("0xabc".to_owned()),
665            has_source: false,
666            stack: Some(vec!["0x2".to_owned(), "0x1".to_owned()]),
667            note: None,
668        };
669        assert_eq!(
670            renderer.render(&Output::Stop(bare)),
671            "step 0/3, pc 0, PUSH1, gas 1\nStack: [1] 0x2 [0] 0x1\n"
672        );
673    }
674
675    #[test]
676    fn messages_and_hex_are_plain_without_color() {
677        let renderer = Renderer::new(false);
678        assert_eq!(
679            renderer.render(&Output::Message {
680                level: Level::Warning,
681                text: "Unknown command: x".to_owned(),
682                note: Some("try help".to_owned()),
683            }),
684            "Unknown command: x\nnote: try help\n"
685        );
686        assert_eq!(shorten_hex("0x1234"), "0x1234");
687        assert_eq!(shorten_hex("0x123456789abcdef0"), "0x1234...");
688        assert_eq!(shorten_hex("plain"), "plain");
689        assert!(Renderer::new(true)
690            .render(&Output::Help {
691                lines: vec!["x".to_owned()]
692            })
693            .starts_with('x'));
694    }
695}