Skip to main content

doge_runtime/
display.rs

1use std::fmt;
2
3use crate::value::Value;
4
5/// The receiver's name in a bound method's display form: a `many` instance shows
6/// its class (`<method Shibe.speak>`), a collection its type (`<method
7/// List.append>`).
8fn receiver_label(v: &Value) -> String {
9    match v {
10        Value::Object(o) => o.borrow().class_name.to_string(),
11        other => other.type_name().to_string(),
12    }
13}
14
15/// String form of a value as it appears *nested* inside a container: strings
16/// gain quotes, everything else prints as it would on its own (the `b"..."` form
17/// bytes already print in is self-quoting, so it needs no nested special case).
18fn repr(v: &Value) -> String {
19    match v {
20        Value::Str(s) => format!("\"{s}\""),
21        other => other.to_string(),
22    }
23}
24
25/// The printable `b"..."` form of raw bytes: printable ASCII shown literally
26/// (with `"` and `\` escaped), every other byte as a `\xNN` hex escape. Total and
27/// UTF-8-safe, so `bark` and `str(bytes)` can render any bytes without decoding.
28fn bytes_repr(bytes: &[u8]) -> String {
29    let mut out = String::from("b\"");
30    for &byte in bytes {
31        match byte {
32            b'"' => out.push_str("\\\""),
33            b'\\' => out.push_str("\\\\"),
34            0x20..=0x7e => out.push(byte as char),
35            _ => out.push_str(&format!("\\x{byte:02x}")),
36        }
37    }
38    out.push('"');
39    out
40}
41
42impl fmt::Display for Value {
43    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44        match self {
45            Value::Int(n) => write!(f, "{n}"),
46            Value::Float(x) => {
47                // Always show a decimal point so Floats never look like Ints:
48                // 3.0 prints "3.0", 2.5 prints "2.5".
49                if x.is_finite() && x.fract() == 0.0 {
50                    write!(f, "{x:.1}")
51                } else {
52                    write!(f, "{x}")
53                }
54            }
55            // A Decimal prints at its own scale, so `dec("0.10")` shows "0.10" —
56            // the exact value the user wrote, trailing zeros and all.
57            Value::Decimal(d) => write!(f, "{d}"),
58            Value::Str(s) => write!(f, "{s}"),
59            Value::Bytes(b) => write!(f, "{}", bytes_repr(b)),
60            Value::Bool(b) => write!(f, "{b}"),
61            Value::None => write!(f, "none"),
62            Value::List(items) => {
63                let items = items.borrow();
64                let inner = items.iter().map(repr).collect::<Vec<_>>().join(", ");
65                write!(f, "[{inner}]")
66            }
67            Value::Dict(entries) => {
68                let entries = entries.borrow();
69                let inner = entries
70                    .iter()
71                    .map(|(k, v)| format!("\"{k}\": {}", repr(v)))
72                    .collect::<Vec<_>>()
73                    .join(", ");
74                write!(f, "{{{inner}}}")
75            }
76            Value::Object(o) => write!(f, "<{}>", o.borrow().class_name),
77            Value::Function(func) => write!(f, "<function {}>", func.name),
78            Value::Class(class) => write!(f, "<class {}>", class.name),
79            Value::BoundMethod(m) => {
80                write!(f, "<method {}.{}>", receiver_label(&m.receiver), m.method)
81            }
82            Value::Error(e) => write!(f, "{}", e.message),
83            Value::Socket(_) => write!(f, "<socket>"),
84            Value::Pup(_) => write!(f, "<pup>"),
85            Value::Bowl(_) => write!(f, "<bowl>"),
86        }
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn scalars_format_python_style() {
96        assert_eq!(Value::int(7).to_string(), "7");
97        assert_eq!(Value::Float(2.5).to_string(), "2.5");
98        assert_eq!(Value::Float(3.0).to_string(), "3.0");
99        assert_eq!(Value::str("kabosu").to_string(), "kabosu");
100        assert_eq!(Value::Bool(true).to_string(), "true");
101        assert_eq!(Value::None.to_string(), "none");
102    }
103
104    #[test]
105    fn decimals_print_at_their_own_scale() {
106        use std::str::FromStr;
107        // Trailing zeros are kept — the exact value the user wrote.
108        let d = Value::decimal(bigdecimal::BigDecimal::from_str("0.10").unwrap());
109        assert_eq!(d.to_string(), "0.10");
110        // Nested in a container, a Decimal is bare (it is not a Str).
111        assert_eq!(Value::list(vec![d]).to_string(), "[0.10]");
112    }
113
114    #[test]
115    fn strings_are_bare_at_top_level_but_quoted_when_nested() {
116        assert_eq!(Value::str("wow").to_string(), "wow");
117        let list = Value::list(vec![Value::str("a"), Value::int(1)]);
118        assert_eq!(list.to_string(), "[\"a\", 1]");
119    }
120
121    #[test]
122    fn dict_formats_with_quoted_keys_in_insertion_order() {
123        let mut map = crate::ordered_map::OrderedMap::new();
124        map.insert("name".to_string(), Value::str("kabosu"));
125        map.insert("age".to_string(), Value::int(7));
126        assert_eq!(
127            Value::dict(map).to_string(),
128            "{\"name\": \"kabosu\", \"age\": 7}"
129        );
130    }
131
132    #[test]
133    fn object_prints_its_class_in_angle_brackets() {
134        assert_eq!(Value::object(0, "Shibe").to_string(), "<Shibe>");
135    }
136
137    #[test]
138    fn function_prints_its_name_in_angle_brackets() {
139        assert_eq!(
140            Value::function(0, "greet", vec![]).to_string(),
141            "<function greet>"
142        );
143    }
144
145    #[test]
146    fn bound_method_prints_its_receiver_and_name() {
147        let obj = Value::object(0, "Shibe");
148        assert_eq!(
149            Value::bound_method(obj, "speak").to_string(),
150            "<method Shibe.speak>"
151        );
152        let list = Value::list(vec![]);
153        assert_eq!(
154            Value::bound_method(list, "append").to_string(),
155            "<method List.append>"
156        );
157    }
158
159    #[test]
160    fn bytes_print_in_b_quote_form_with_hex_escapes() {
161        assert_eq!(Value::bytes("hi").to_string(), "b\"hi\"");
162        // Non-printable and high bytes become \xNN; quotes and backslashes escape.
163        assert_eq!(
164            Value::bytes([0x00, 0xff, b'"', b'\\']).to_string(),
165            "b\"\\x00\\xff\\\"\\\\\""
166        );
167        // Self-quoting, so it stays the same nested in a container.
168        assert_eq!(
169            Value::list(vec![Value::bytes("hi")]).to_string(),
170            "[b\"hi\"]"
171        );
172    }
173
174    #[test]
175    fn error_prints_its_message() {
176        let err = crate::error::error_value(
177            &crate::error::DogeError::type_error("much wrong"),
178            "s.doge",
179            2,
180        );
181        assert_eq!(err.to_string(), "much wrong");
182        // Nested in a container it stays bare (it is not a Str), like objects.
183        assert_eq!(Value::list(vec![err]).to_string(), "[much wrong]");
184    }
185}