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 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}