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                // Keep whole Floats distinct from Ints.
48                if x.is_finite() && x.fract() == 0.0 {
49                    write!(f, "{x:.1}")
50                } else {
51                    write!(f, "{x}")
52                }
53            }
54            Value::Decimal(d) => write!(f, "{d}"),
55            Value::Str(s) => write!(f, "{s}"),
56            Value::Bytes(b) => write!(f, "{}", bytes_repr(b)),
57            Value::Bool(b) => write!(f, "{b}"),
58            Value::None => write!(f, "none"),
59            Value::List(items) => {
60                let items = items.borrow();
61                let inner = items.iter().map(repr).collect::<Vec<_>>().join(", ");
62                write!(f, "[{inner}]")
63            }
64            Value::Dict(entries) => {
65                let entries = entries.borrow();
66                let inner = entries
67                    .iter()
68                    .map(|(k, v)| format!("\"{k}\": {}", repr(v)))
69                    .collect::<Vec<_>>()
70                    .join(", ");
71                write!(f, "{{{inner}}}")
72            }
73            Value::Object(o) => write!(f, "<{}>", o.borrow().class_name),
74            Value::Function(func) => write!(f, "<function {}>", func.name),
75            Value::Class(class) => write!(f, "<class {}>", class.name),
76            Value::BoundMethod(m) => {
77                write!(f, "<method {}.{}>", receiver_label(&m.receiver), m.method)
78            }
79            Value::Error(e) => write!(f, "{}", e.message),
80            Value::Socket(_) => write!(f, "<socket>"),
81            Value::Pup(_) => write!(f, "<pup>"),
82            Value::Bowl(_) => write!(f, "<bowl>"),
83        }
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90
91    #[test]
92    fn scalars_format_python_style() {
93        assert_eq!(Value::int(7).to_string(), "7");
94        assert_eq!(Value::Float(2.5).to_string(), "2.5");
95        assert_eq!(Value::Float(3.0).to_string(), "3.0");
96        assert_eq!(Value::str("kabosu").to_string(), "kabosu");
97        assert_eq!(Value::Bool(true).to_string(), "true");
98        assert_eq!(Value::None.to_string(), "none");
99    }
100
101    #[test]
102    fn decimals_print_at_their_own_scale() {
103        use std::str::FromStr;
104        // Trailing zeros are kept — the exact value the user wrote.
105        let d = Value::decimal(bigdecimal::BigDecimal::from_str("0.10").unwrap());
106        assert_eq!(d.to_string(), "0.10");
107        // Nested in a container, a Decimal is bare (it is not a Str).
108        assert_eq!(Value::list(vec![d]).to_string(), "[0.10]");
109    }
110
111    #[test]
112    fn strings_are_bare_at_top_level_but_quoted_when_nested() {
113        assert_eq!(Value::str("wow").to_string(), "wow");
114        let list = Value::list(vec![Value::str("a"), Value::int(1)]);
115        assert_eq!(list.to_string(), "[\"a\", 1]");
116    }
117
118    #[test]
119    fn dict_formats_with_quoted_keys_in_insertion_order() {
120        let mut map = crate::ordered_map::OrderedMap::new();
121        map.insert("name".to_string(), Value::str("kabosu"));
122        map.insert("age".to_string(), Value::int(7));
123        assert_eq!(
124            Value::dict(map).to_string(),
125            "{\"name\": \"kabosu\", \"age\": 7}"
126        );
127    }
128
129    #[test]
130    fn object_prints_its_class_in_angle_brackets() {
131        assert_eq!(Value::object(0, "Shibe").to_string(), "<Shibe>");
132    }
133
134    #[test]
135    fn function_prints_its_name_in_angle_brackets() {
136        assert_eq!(
137            Value::function(0, "greet", vec![]).to_string(),
138            "<function greet>"
139        );
140    }
141
142    #[test]
143    fn bound_method_prints_its_receiver_and_name() {
144        let obj = Value::object(0, "Shibe");
145        assert_eq!(
146            Value::bound_method(obj, "speak").to_string(),
147            "<method Shibe.speak>"
148        );
149        let list = Value::list(vec![]);
150        assert_eq!(
151            Value::bound_method(list, "append").to_string(),
152            "<method List.append>"
153        );
154    }
155
156    #[test]
157    fn bytes_print_in_b_quote_form_with_hex_escapes() {
158        assert_eq!(Value::bytes("hi").to_string(), "b\"hi\"");
159        // Non-printable and high bytes become \xNN; quotes and backslashes escape.
160        assert_eq!(
161            Value::bytes([0x00, 0xff, b'"', b'\\']).to_string(),
162            "b\"\\x00\\xff\\\"\\\\\""
163        );
164        // Self-quoting, so it stays the same nested in a container.
165        assert_eq!(
166            Value::list(vec![Value::bytes("hi")]).to_string(),
167            "[b\"hi\"]"
168        );
169    }
170
171    #[test]
172    fn error_prints_its_message() {
173        let err = crate::error::error_value(
174            &crate::error::DogeError::type_error("much wrong"),
175            "s.doge",
176            2,
177        );
178        assert_eq!(err.to_string(), "much wrong");
179        // Nested in a container it stays bare (it is not a Str), like objects.
180        assert_eq!(Value::list(vec![err]).to_string(), "[much wrong]");
181    }
182}