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