Skip to main content

nomiscript/runtime/
format.rs

1use base64::Engine;
2use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
3
4use super::value::{Pair, Value};
5
6const BYTES_INLINE_THRESHOLD: usize = 32;
7
8#[must_use]
9pub fn format_value(value: &Value) -> String {
10    let mut out = String::new();
11    write_value(&mut out, value);
12    out
13}
14
15fn write_value(out: &mut String, value: &Value) {
16    match value {
17        Value::Nil => out.push_str("nil"),
18        Value::T => out.push('t'),
19        Value::Number(n) if *n.denom() == 1 => {
20            out.push_str(&n.numer().to_string());
21        }
22        Value::Number(n) => {
23            out.push_str(&format!("{}/{}", n.numer(), n.denom()));
24        }
25        Value::String(s) => write_string_literal(out, s),
26        Value::Symbol(s) => out.push_str(s),
27        Value::Bytes(b) => write_bytes_literal(out, b),
28        Value::Pair(pair) => write_pair_or_list(out, pair),
29        Value::Vector(items) => write_vector(out, items),
30        Value::Closure(c) => {
31            out.push_str(&format!("#<closure:{}>", c.code_id));
32        }
33        Value::Struct { name, fields } => write_struct(out, name, fields),
34        Value::Commodity {
35            amount,
36            commodity_id,
37        } => {
38            // Plist envelope keeps the wire shape uniform with native
39            // results: emacs `(read)` recovers `(:commodity <ratio> :id "<uuid>")`.
40            out.push_str("(:commodity ");
41            if *amount.denom() == 1 {
42                out.push_str(&amount.numer().to_string());
43            } else {
44                out.push_str(&format!("{}/{}", amount.numer(), amount.denom()));
45            }
46            out.push_str(" :id \"");
47            out.push_str(&commodity_id.to_string());
48            out.push_str("\")");
49        }
50    }
51}
52
53fn write_string_literal(out: &mut String, s: &str) {
54    out.push('"');
55    for ch in s.chars() {
56        match ch {
57            '\\' => out.push_str("\\\\"),
58            '"' => out.push_str("\\\""),
59            '\n' => out.push_str("\\n"),
60            '\t' => out.push_str("\\t"),
61            '\r' => out.push_str("\\r"),
62            other => out.push(other),
63        }
64    }
65    out.push('"');
66}
67
68fn write_bytes_literal(out: &mut String, bytes: &[u8]) {
69    if bytes.len() <= BYTES_INLINE_THRESHOLD {
70        out.push_str("#u8(");
71        let mut first = true;
72        for byte in bytes {
73            if first {
74                first = false;
75            } else {
76                out.push(' ');
77            }
78            out.push_str(&byte.to_string());
79        }
80        out.push(')');
81    } else {
82        out.push_str("#\"");
83        out.push_str(&BASE64_STANDARD.encode(bytes));
84        out.push('"');
85    }
86}
87
88fn write_pair_or_list(out: &mut String, pair: &Pair) {
89    out.push('(');
90    write_value(out, &pair.car);
91    let mut cdr = &pair.cdr;
92    loop {
93        match cdr {
94            Value::Nil => break,
95            Value::Pair(next) => {
96                out.push(' ');
97                write_value(out, &next.car);
98                cdr = &next.cdr;
99            }
100            other => {
101                out.push_str(" . ");
102                write_value(out, other);
103                break;
104            }
105        }
106    }
107    out.push(')');
108}
109
110fn write_vector(out: &mut String, items: &[Value]) {
111    out.push_str("#(");
112    let mut first = true;
113    for item in items {
114        if first {
115            first = false;
116        } else {
117            out.push(' ');
118        }
119        write_value(out, item);
120    }
121    out.push(')');
122}
123
124fn write_struct(out: &mut String, name: &str, fields: &[Value]) {
125    out.push_str("#S(");
126    out.push_str(name);
127    for field in fields {
128        out.push(' ');
129        write_value(out, field);
130    }
131    out.push(')');
132}
133
134#[cfg(test)]
135mod tests {
136    use super::super::value::{Closure, Fraction, Pair};
137    use super::*;
138    use crate::ast::Expr;
139    use crate::reader::Reader;
140
141    #[test]
142    fn formats_nil() {
143        assert_eq!(format_value(&Value::Nil), "nil");
144    }
145
146    #[test]
147    fn formats_t() {
148        assert_eq!(format_value(&Value::T), "t");
149    }
150
151    #[test]
152    fn a_list_ending_in_nil_prints_as_a_proper_list() {
153        let list = Pair::cons(Value::Symbol("A".into()), Value::Nil);
154        assert_eq!(format_value(&list), "(A)");
155    }
156
157    #[test]
158    fn formats_integer_number() {
159        assert_eq!(
160            format_value(&Value::Number(Fraction::from_integer(42))),
161            "42"
162        );
163    }
164
165    #[test]
166    fn formats_negative_integer() {
167        assert_eq!(
168            format_value(&Value::Number(Fraction::from_integer(-7))),
169            "-7"
170        );
171    }
172
173    #[test]
174    fn formats_proper_fraction() {
175        assert_eq!(format_value(&Value::Number(Fraction::new(3, 4))), "3/4");
176    }
177
178    #[test]
179    fn formats_string_with_escapes() {
180        let v = Value::String("a\nb\"c\\d".to_string());
181        assert_eq!(format_value(&v), "\"a\\nb\\\"c\\\\d\"");
182    }
183
184    #[test]
185    fn formats_symbol_verbatim() {
186        assert_eq!(format_value(&Value::Symbol("FOO".into())), "FOO");
187    }
188
189    #[test]
190    fn formats_short_bytes_as_u8_literal() {
191        let v = Value::Bytes(vec![0, 1, 255]);
192        assert_eq!(format_value(&v), "#u8(0 1 255)");
193    }
194
195    #[test]
196    fn formats_empty_bytes_as_u8_literal() {
197        assert_eq!(format_value(&Value::Bytes(Vec::new())), "#u8()");
198    }
199
200    #[test]
201    fn formats_bytes_at_inline_threshold() {
202        let bytes: Vec<u8> = (0..BYTES_INLINE_THRESHOLD as u8).collect();
203        let formatted = format_value(&Value::Bytes(bytes.clone()));
204        assert!(formatted.starts_with("#u8("));
205        assert!(formatted.ends_with(')'));
206    }
207
208    #[test]
209    fn formats_long_bytes_as_base64_literal() {
210        let bytes: Vec<u8> = (0..=BYTES_INLINE_THRESHOLD as u8).collect();
211        let formatted = format_value(&Value::Bytes(bytes.clone()));
212        assert!(formatted.starts_with("#\""));
213        assert!(formatted.ends_with('"'));
214        let payload = &formatted[2..formatted.len() - 1];
215        let decoded = BASE64_STANDARD.decode(payload).unwrap();
216        assert_eq!(decoded, bytes);
217    }
218
219    #[test]
220    fn formats_proper_list() {
221        let list = Pair::cons(
222            Value::Number(Fraction::from_integer(1)),
223            Pair::cons(
224                Value::Number(Fraction::from_integer(2)),
225                Pair::cons(Value::Number(Fraction::from_integer(3)), Value::Nil),
226            ),
227        );
228        assert_eq!(format_value(&list), "(1 2 3)");
229    }
230
231    #[test]
232    fn formats_dotted_pair() {
233        let pair = Pair::cons(Value::Symbol("A".into()), Value::Symbol("B".into()));
234        assert_eq!(format_value(&pair), "(A . B)");
235    }
236
237    #[test]
238    fn formats_improper_list() {
239        let list = Pair::cons(
240            Value::Number(Fraction::from_integer(1)),
241            Pair::cons(
242                Value::Number(Fraction::from_integer(2)),
243                Value::Number(Fraction::from_integer(3)),
244            ),
245        );
246        assert_eq!(format_value(&list), "(1 2 . 3)");
247    }
248
249    #[test]
250    fn formats_vector() {
251        let v = Value::Vector(vec![Value::Number(Fraction::from_integer(1)), Value::T]);
252        assert_eq!(format_value(&v), "#(1 t)");
253    }
254
255    #[test]
256    fn formats_closure() {
257        let c = Value::Closure(Closure::new(7, vec![]));
258        assert_eq!(format_value(&c), "#<closure:7>");
259    }
260
261    #[test]
262    fn formats_struct() {
263        let s = Value::Struct {
264            name: "ACCOUNT".into(),
265            fields: vec![
266                Value::Symbol("ID".into()),
267                Value::Number(Fraction::from_integer(42)),
268            ],
269        };
270        assert_eq!(format_value(&s), "#S(ACCOUNT ID 42)");
271    }
272
273    #[test]
274    fn round_trip_short_bytes_through_reader() {
275        let v = Value::Bytes(vec![0xCA, 0xFE, 0xBA, 0xBE]);
276        let formatted = format_value(&v);
277        let parsed = Reader::parse(&formatted).unwrap();
278        assert_eq!(
279            parsed.exprs,
280            vec![Expr::Bytes(vec![0xCA, 0xFE, 0xBA, 0xBE])]
281        );
282    }
283
284    #[test]
285    fn round_trip_long_bytes_through_reader() {
286        let bytes: Vec<u8> = (0..=255).chain(0..=255).collect();
287        let formatted = format_value(&Value::Bytes(bytes.clone()));
288        let parsed = Reader::parse(&formatted).unwrap();
289        assert_eq!(parsed.exprs, vec![Expr::Bytes(bytes)]);
290    }
291
292    #[test]
293    fn round_trip_full_byte_range_through_reader() {
294        let bytes: Vec<u8> = (0..=255).collect();
295        let formatted = format_value(&Value::Bytes(bytes.clone()));
296        let parsed = Reader::parse(&formatted).unwrap();
297        assert_eq!(parsed.exprs, vec![Expr::Bytes(bytes)]);
298    }
299
300    #[test]
301    fn round_trip_rational_through_reader() {
302        for (num, den) in [(1i64, 2i64), (-3, 4), (1, 3), (7, 11)] {
303            let v = Value::Number(Fraction::new(num, den));
304            let formatted = format_value(&v);
305            let parsed = Reader::parse(&formatted).unwrap();
306            assert_eq!(parsed.exprs, vec![Expr::Number(Fraction::new(num, den))]);
307        }
308    }
309}