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