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, self.name(&frame.name));
115                    if !frame.arguments.is_empty() {
116                        let arguments = frame
117                            .arguments
118                            .iter()
119                            .map(|argument| format!("{} = {}", argument.name, argument.value))
120                            .collect::<Vec<_>>()
121                            .join(", ");
122                        let _ = write!(line, "({arguments})");
123                    }
124                    if let Some(location) = &frame.location {
125                        let _ = write!(line, " at {location}");
126                    }
127                    if let Some(address) = &frame.address {
128                        let _ = write!(line, " ({})", self.address(address));
129                    }
130                    line.push_str(&self.dim(format!("  step {}, PC {}", frame.step, frame.pc)));
131                    lines.push(line);
132                }
133                lines
134            }
135            Output::Listing {
136                path,
137                current_line,
138                lines,
139            } => {
140                let mut out = vec![format!("{}:{}", self.info(path), self.number(current_line))];
141                for line in lines {
142                    if line.current {
143                        out.push(format!(
144                            "{} {}",
145                            self.bold(format!("=> {:>5} |", line.line)),
146                            self.bold(&line.text)
147                        ));
148                    } else {
149                        out.push(format!(
150                            "{} {}",
151                            self.dim(format!("   {:>5} |", line.line)),
152                            line.text
153                        ));
154                    }
155                }
156                out
157            }
158            Output::Stack { words, shown } => {
159                if words.is_empty() {
160                    return vec![self.dim("Stack: empty")];
161                }
162                let mut lines = vec![format!(
163                    "{} {} words, top first",
164                    self.info("Stack:"),
165                    self.number(words.len())
166                )];
167                for (index, word) in words.iter().enumerate().rev().take(*shown) {
168                    lines.push(format!("  [{index}] {}", self.number(word)));
169                }
170                if *shown < words.len() {
171                    lines.push(self.dim(format!("  ... {} more below", words.len() - shown)));
172                }
173                lines
174            }
175            Output::Memory(memory) => {
176                let range = if memory.start > 0 || memory.end < memory.total {
177                    format!(", showing bytes {}..{}", memory.start, memory.end)
178                } else {
179                    String::new()
180                };
181                let mut lines = vec![format!(
182                    "{} {} bytes{range}",
183                    self.info("Memory:"),
184                    self.number(memory.total)
185                )];
186                for word in &memory.words {
187                    lines.push(format!(
188                        "{} {}",
189                        self.dim(format!("0x{:04x}:", word.offset)),
190                        word.hex
191                    ));
192                }
193                lines
194            }
195            Output::Storage { address, slots } => {
196                if slots.is_empty() {
197                    return vec![self.dim("Storage: no slots read or written yet.")];
198                }
199                let mut lines = vec![match address {
200                    Some(address) => {
201                        format!(
202                            "{} {}",
203                            self.info("Storage:"),
204                            self.dim(format!("of {address}"))
205                        )
206                    }
207                    None => self.info("Storage:"),
208                }];
209                for slot in slots {
210                    let was = slot
211                        .was
212                        .as_ref()
213                        .map(|was| self.dim(format!("  (was {was})")))
214                        .unwrap_or_default();
215                    lines.push(format!("  {} = {}{was}", slot.slot, slot.value));
216                }
217                lines
218            }
219            Output::Calldata { bytes, data } => vec![
220                format!("{} {} bytes", self.info("Calldata:"), self.number(bytes)),
221                data.clone(),
222            ],
223            Output::Variables {
224                warning,
225                pc,
226                locals,
227                unavailable,
228                state,
229            } => {
230                let mut lines = Vec::new();
231                if let Some(warning) = warning {
232                    lines.push(self.warning(format!("warning: {warning}")));
233                }
234                if locals.is_empty() {
235                    // The difference matters: locals that cannot be read here at all is
236                    // not the same as none being live at this program counter.
237                    lines.push(match unavailable {
238                        Some(reason) => format!(
239                            "{} locals are unavailable here: {reason}",
240                            self.dim("Variables:")
241                        ),
242                        None => format!(
243                            "{} no variables in scope at PC {}",
244                            self.dim("Variables:"),
245                            self.number(pc)
246                        ),
247                    });
248                }
249                for variable in locals {
250                    lines.push(self.variable(variable));
251                }
252                match state {
253                    StateInfo::Variables { variables } => {
254                        lines.push(self.dim("State:"));
255                        for variable in variables {
256                            lines.push(self.variable(variable));
257                        }
258                    }
259                    StateInfo::None => {
260                        lines.push(self.dim("State: this contract declares no state variables"));
261                    }
262                    StateInfo::NoLayout => lines.push(self.dim(
263                        "State: no storage layout loaded; compile with `--storage-layout` to \
264                         read state variables",
265                    )),
266                    StateInfo::NoStorage => {
267                        lines.push(self.dim("State: no storage was recorded for this step"));
268                    }
269                }
270                lines
271            }
272            Output::Variable { warning, variable } => {
273                let mut lines = Vec::new();
274                if let Some(warning) = warning {
275                    lines.push(self.warning(format!("warning: {warning}")));
276                }
277                lines.push(self.variable(variable));
278                lines
279            }
280            Output::Resources { contracts, json } => {
281                if *json {
282                    let document = serde_json::json!({ "contracts": contracts });
283                    return serde_json::to_string_pretty(&document)
284                        .unwrap_or_default()
285                        .lines()
286                        .map(str::to_owned)
287                        .collect();
288                }
289                if contracts.is_empty() {
290                    return vec![self.warning("No contract sources are loaded.")];
291                }
292                let mut lines = Vec::new();
293                for contract in contracts {
294                    lines.push(format!(
295                        "{} {} {}",
296                        self.name(&contract.name),
297                        self.dim(contract.address.as_deref().unwrap_or("<any address>")),
298                        self.dim(format!("[{}]", contract.environment))
299                    ));
300                    for path in &contract.sources {
301                        lines.push(format!("  {path}"));
302                    }
303                }
304                lines
305            }
306            Output::Help { lines } => lines.clone(),
307            Output::Tui | Output::Quit => Vec::new(),
308        }
309    }
310
311    /// The stop: the reason when there is one, then the location line and the source
312    /// line, or the stack in assembly mode.
313    fn stop_lines(&self, stop: &Stop) -> Vec<String> {
314        let mut lines = Vec::new();
315        match &stop.reason {
316            StopReason::Initial | StopReason::Moved => {}
317            StopReason::Breakpoint { id, label } => lines.push(format!(
318                "{} {}, {label}",
319                self.success(format!("Breakpoint #{id} hit at step")),
320                self.number(stop.step)
321            )),
322            StopReason::End => lines.push(format!(
323                "{} {}",
324                self.info("End of trace at step"),
325                self.number(stop.step)
326            )),
327            StopReason::Start => lines.push(format!(
328                "{} {}",
329                self.info("Start of trace at step"),
330                self.number(stop.step)
331            )),
332        }
333        let status = format!(
334            "step {}/{}, pc {}, {}, gas {}",
335            self.number(stop.step),
336            self.number(stop.last_step),
337            self.number(stop.pc),
338            self.opcode(&stop.op),
339            self.number(stop.gas)
340        );
341        match &stop.location {
342            Some(location) => {
343                let function = location
344                    .function
345                    .as_deref()
346                    .map(|name| format!(" in {}", self.name(name)))
347                    .unwrap_or_default();
348                let generated = if location.generated {
349                    self.dim("  (compiler-generated code for this line)")
350                } else {
351                    String::new()
352                };
353                lines.push(format!(
354                    "{}:{}{function}{generated}  {}",
355                    self.info(&location.path),
356                    self.number(location.line),
357                    self.dim(format!("({status})"))
358                ));
359                if stop.stack.is_none() {
360                    if let Some(text) = &location.text {
361                        lines.push(format!(
362                            "{} {text}",
363                            self.dim(format!("{:>5} |", location.line))
364                        ));
365                    }
366                }
367            }
368            None if stop.has_source => {
369                let address = stop
370                    .address
371                    .as_deref()
372                    .map(|address| format!(" in {}", self.address(address)))
373                    .unwrap_or_default();
374                lines.push(format!(
375                    "{}  {}",
376                    self.dim(format!("no source for this step{address}")),
377                    self.dim(format!("({status})"))
378                ));
379            }
380            None => lines.push(status),
381        }
382        if let Some(stack) = &stop.stack {
383            lines.push(format!(
384                "{} {}",
385                self.info("Stack:"),
386                self.stack_summary(stack)
387            ));
388        }
389        if let Some(note) = &stop.note {
390            lines.push(format!("{} {note}", self.warning("Note:")));
391        }
392        lines
393    }
394
395    /// The top of the stack on one line, the words numbered from the bottom.
396    fn stack_summary(&self, top_first: &[String]) -> String {
397        if top_first.is_empty() {
398            return self.dim("empty");
399        }
400        let total = top_first.len();
401        let mut parts = top_first
402            .iter()
403            .take(4)
404            .enumerate()
405            .map(|(offset, word)| {
406                format!(
407                    "{} {}",
408                    self.dim(format!("[{}]", total - 1 - offset)),
409                    self.number(shorten_hex(word))
410                )
411            })
412            .collect::<Vec<_>>();
413        if total > 4 {
414            parts.push(self.dim(format!("... +{} more", total - 4)));
415        }
416        parts.join(" ")
417    }
418
419    fn variable(&self, variable: &VariableInfo) -> String {
420        let value = match variable.status {
421            ValueStatus::Unavailable => self.warning(&variable.value),
422            _ => self.success(&variable.value),
423        };
424        let place = variable
425            .place
426            .as_ref()
427            .map(|place| self.dim(format!(" [{place}]")))
428            .unwrap_or_default();
429        format!(
430            "{} {} = {value}{place}",
431            self.info(&variable.ty),
432            self.bold(&variable.name)
433        )
434    }
435
436    fn info_message(&self, text: &str) -> String {
437        // `Label: rest` colors the label; a bare sentence is printed as it is.
438        match text.split_once(": ") {
439            Some((label, rest)) if !label.contains(' ') || label.starts_with("Mode") => {
440                format!("{} {rest}", self.info(format!("{label}:")))
441            }
442            _ => self.info(text),
443        }
444    }
445
446    fn paint(&self, text: impl std::fmt::Display, code: &str) -> String {
447        if self.color {
448            format!("\x1b[{code}m{text}\x1b[0m")
449        } else {
450            text.to_string()
451        }
452    }
453
454    fn bold(&self, text: impl std::fmt::Display) -> String {
455        self.paint(text, "1")
456    }
457
458    fn dim(&self, text: impl std::fmt::Display) -> String {
459        self.paint(text, "2")
460    }
461
462    fn info(&self, text: impl std::fmt::Display) -> String {
463        self.paint(text, "96")
464    }
465
466    fn success(&self, text: impl std::fmt::Display) -> String {
467        self.paint(text, "92")
468    }
469
470    fn warning(&self, text: impl std::fmt::Display) -> String {
471        self.paint(text, "93")
472    }
473
474    fn number(&self, text: impl std::fmt::Display) -> String {
475        self.paint(text, "93")
476    }
477
478    fn opcode(&self, text: impl std::fmt::Display) -> String {
479        self.paint(text, "94")
480    }
481
482    fn name(&self, text: impl std::fmt::Display) -> String {
483        self.paint(text, "95")
484    }
485
486    fn address(&self, text: impl std::fmt::Display) -> String {
487        self.paint(text, "95")
488    }
489}
490
491/// A long hex word as its first digits: `0x1234...`.
492#[must_use]
493pub fn shorten_hex(value: &str) -> String {
494    let Some(digits) = value.strip_prefix("0x") else {
495        return value.to_owned();
496    };
497    if value.len() <= 10 {
498        return value.to_owned();
499    }
500    let head = digits.chars().take(4).collect::<String>();
501    format!("0x{head}...")
502}
503
504#[cfg(test)]
505mod tests {
506    use super::{shorten_hex, Renderer};
507    use crate::response::{Level, Output, Stop, StopLocation, StopReason};
508
509    #[test]
510    fn a_stop_is_one_location_line_and_the_source_line() {
511        let renderer = Renderer::new(false);
512        let stop = Stop {
513            reason: StopReason::Breakpoint {
514                id: 1,
515                label: "Counter.sol:7".to_owned(),
516            },
517            step: 12,
518            last_step: 99,
519            pc: 34,
520            op: "PUSH1".to_owned(),
521            gas: 5000,
522            location: Some(StopLocation {
523                path: "Counter.sol".to_owned(),
524                line: 7,
525                function: Some("increment".to_owned()),
526                generated: false,
527                text: Some("        count += 1;".to_owned()),
528            }),
529            address: None,
530            has_source: true,
531            stack: None,
532            note: Some("something".to_owned()),
533        };
534        assert_eq!(
535            renderer.render(&Output::Stop(stop)),
536            "Breakpoint #1 hit at step 12, Counter.sol:7\n\
537             Counter.sol:7 in increment  (step 12/99, pc 34, PUSH1, gas 5000)\n    \
538             7 |         count += 1;\n\
539             Note: something\n"
540        );
541        let bare = Stop {
542            reason: StopReason::Moved,
543            step: 0,
544            last_step: 3,
545            pc: 0,
546            op: "PUSH1".to_owned(),
547            gas: 1,
548            location: None,
549            address: Some("0xabc".to_owned()),
550            has_source: false,
551            stack: Some(vec!["0x2".to_owned(), "0x1".to_owned()]),
552            note: None,
553        };
554        assert_eq!(
555            renderer.render(&Output::Stop(bare)),
556            "step 0/3, pc 0, PUSH1, gas 1\nStack: [1] 0x2 [0] 0x1\n"
557        );
558    }
559
560    #[test]
561    fn messages_and_hex_are_plain_without_color() {
562        let renderer = Renderer::new(false);
563        assert_eq!(
564            renderer.render(&Output::Message {
565                level: Level::Warning,
566                text: "Unknown command: x".to_owned(),
567                note: Some("try help".to_owned()),
568            }),
569            "Unknown command: x\nnote: try help\n"
570        );
571        assert_eq!(shorten_hex("0x1234"), "0x1234");
572        assert_eq!(shorten_hex("0x123456789abcdef0"), "0x1234...");
573        assert_eq!(shorten_hex("plain"), "plain");
574        assert!(Renderer::new(true)
575            .render(&Output::Help {
576                lines: vec!["x".to_owned()]
577            })
578            .starts_with('x'));
579    }
580}