Skip to main content

seq_core/
son.rs

1//! SON (Seq Object Notation) Serialization
2//!
3//! Serializes Seq Values to SON format - a prefix/postfix notation compatible
4//! with Seq syntax. SON values can be evaluated in Seq to recreate the original data.
5//!
6//! # Format Examples
7//!
8//! - Int: `42`
9//! - Float: `3.14`
10//! - Bool: `true` / `false`
11//! - String: `"hello"` (with proper escaping)
12//! - Symbol: `:my-symbol`
13//! - List: `list-of 1 lv 2 lv 3 lv`
14//! - Map: `map-of "key" "value" kv`
15//! - Variant: `:Tag field1 field2 wrap-2`
16
17use crate::seqstring::SeqString;
18use crate::stack::{Stack, pop, push};
19use crate::value::{MapKey, Value, VariantData};
20use std::collections::HashMap;
21
22/// Configuration for SON output formatting
23#[derive(Clone)]
24pub(crate) struct SonConfig {
25    /// Use pretty printing with indentation
26    pub(crate) pretty: bool,
27    /// Number of spaces per indentation level
28    pub(crate) indent: usize,
29}
30
31impl Default for SonConfig {
32    fn default() -> Self {
33        Self {
34            pretty: false,
35            indent: 2,
36        }
37    }
38}
39
40impl SonConfig {
41    /// Create a compact (single-line) config
42    pub(crate) fn compact() -> Self {
43        Self::default()
44    }
45
46    /// Create a pretty-printed config
47    pub(crate) fn pretty() -> Self {
48        Self {
49            pretty: true,
50            indent: 2,
51        }
52    }
53}
54
55/// Format a Value to SON string
56pub(crate) fn value_to_son(value: &Value, config: &SonConfig) -> String {
57    let mut buf = String::new();
58    format_value(value, config, 0, &mut buf);
59    buf
60}
61
62/// Internal formatting function with indentation tracking
63fn format_value(value: &Value, config: &SonConfig, depth: usize, buf: &mut String) {
64    match value {
65        Value::Int(n) => {
66            buf.push_str(&n.to_string());
67        }
68        Value::Float(f) => {
69            let s = f.to_string();
70            buf.push_str(&s);
71            // Ensure floats always have decimal point for disambiguation
72            if !s.contains('.') && f.is_finite() {
73                buf.push_str(".0");
74            }
75        }
76        Value::Bool(b) => {
77            buf.push_str(if *b { "true" } else { "false" });
78        }
79        Value::String(s) => {
80            // SON is text serialization (Seq-source-syntax compatible).
81            // Non-UTF-8 bytes have no clean Seq-syntax representation,
82            // so we display lossily — round-trip of arbitrary bytes
83            // through SON is *not* supported. Callers needing to
84            // round-trip binary data should base64/hex-encode first.
85            format_string(&s.as_str_lossy(), buf);
86        }
87        Value::Symbol(s) => {
88            buf.push(':');
89            buf.push_str(&s.as_str_lossy());
90        }
91        Value::Variant(v) => {
92            format_variant(v, config, depth, buf);
93        }
94        Value::Map(m) => {
95            format_map(m, config, depth, buf);
96        }
97        Value::Quotation { .. } => {
98            buf.push_str("<quotation>");
99        }
100        Value::Closure { .. } => {
101            buf.push_str("<closure>");
102        }
103        Value::Channel(_) => {
104            buf.push_str("<channel>");
105        }
106        Value::WeaveCtx { .. } => {
107            buf.push_str("<weave-ctx>");
108        }
109    }
110}
111
112/// Format a string with proper escaping
113fn format_string(s: &str, buf: &mut String) {
114    buf.push('"');
115    for c in s.chars() {
116        match c {
117            '"' => buf.push_str("\\\""),
118            '\\' => buf.push_str("\\\\"),
119            '\n' => buf.push_str("\\n"),
120            '\r' => buf.push_str("\\r"),
121            '\t' => buf.push_str("\\t"),
122            '\x08' => buf.push_str("\\b"),
123            '\x0C' => buf.push_str("\\f"),
124            c if c.is_control() => {
125                buf.push_str(&format!("\\u{:04x}", c as u32));
126            }
127            c => buf.push(c),
128        }
129    }
130    buf.push('"');
131}
132
133/// Format a variant (includes List as special case)
134fn format_variant(v: &VariantData, config: &SonConfig, depth: usize, buf: &mut String) {
135    // Variant tags are constructor names — text by design. We compare
136    // bytes for the List discriminator (no UTF-8 dependence) and use
137    // the lossy-display form for the printed tag in non-List cases.
138    let is_list = v.tag.as_bytes() == b"List";
139
140    if is_list {
141        format_list(&v.fields, config, depth, buf);
142    } else {
143        // General variant: :Tag field1 field2 wrap-N
144        buf.push(':');
145        buf.push_str(&v.tag.as_str_lossy());
146
147        let field_count = v.fields.len();
148
149        if config.pretty && !v.fields.is_empty() {
150            for field in v.fields.iter() {
151                newline_at_indent(buf, depth + 1, config);
152                format_value(field, config, depth + 1, buf);
153            }
154            newline_at_indent(buf, depth, config);
155        } else {
156            for field in v.fields.iter() {
157                buf.push(' ');
158                format_value(field, config, depth, buf);
159            }
160        }
161
162        buf.push_str(&format!(" wrap-{}", field_count));
163    }
164}
165
166/// Format a list using list-of/lv syntax
167fn format_list(fields: &[Value], config: &SonConfig, depth: usize, buf: &mut String) {
168    buf.push_str("list-of");
169
170    if fields.is_empty() {
171        return;
172    }
173
174    if config.pretty {
175        for field in fields.iter() {
176            newline_at_indent(buf, depth + 1, config);
177            format_value(field, config, depth + 1, buf);
178            buf.push_str(" lv");
179        }
180    } else {
181        for field in fields.iter() {
182            buf.push(' ');
183            format_value(field, config, depth, buf);
184            buf.push_str(" lv");
185        }
186    }
187}
188
189/// Format a map using map-of/kv syntax
190fn format_map(map: &HashMap<MapKey, Value>, config: &SonConfig, depth: usize, buf: &mut String) {
191    buf.push_str("map-of");
192
193    if map.is_empty() {
194        return;
195    }
196
197    // Sort keys for deterministic output (important for testing/debugging)
198    let mut entries: Vec<_> = map.iter().collect();
199    entries.sort_by(|(k1, _), (k2, _)| {
200        let s1 = map_key_sort_string(k1);
201        let s2 = map_key_sort_string(k2);
202        s1.cmp(&s2)
203    });
204
205    if config.pretty {
206        for (key, value) in entries {
207            newline_at_indent(buf, depth + 1, config);
208            format_map_key(key, buf);
209            buf.push(' ');
210            format_value(value, config, depth + 1, buf);
211            buf.push_str(" kv");
212        }
213    } else {
214        for (key, value) in entries {
215            buf.push(' ');
216            format_map_key(key, buf);
217            buf.push(' ');
218            format_value(value, config, depth, buf);
219            buf.push_str(" kv");
220        }
221    }
222}
223
224/// Get a sort key string for a MapKey
225fn map_key_sort_string(key: &MapKey) -> String {
226    match key {
227        MapKey::Int(n) => format!("0_{:020}", n), // Prefix with 0 for ints
228        MapKey::Bool(b) => format!("1_{}", b),    // Prefix with 1 for bools
229        MapKey::String(s) => format!("2_{}", s.as_str_lossy()), // Prefix with 2 for strings
230    }
231}
232
233/// Format a map key
234fn format_map_key(key: &MapKey, buf: &mut String) {
235    match key {
236        MapKey::Int(n) => buf.push_str(&n.to_string()),
237        MapKey::Bool(b) => buf.push_str(if *b { "true" } else { "false" }),
238        MapKey::String(s) => format_string(&s.as_str_lossy(), buf),
239    }
240}
241
242/// Push indentation spaces
243fn push_indent(buf: &mut String, depth: usize, indent_size: usize) {
244    for _ in 0..(depth * indent_size) {
245        buf.push(' ');
246    }
247}
248
249/// Start a new line and indent to the given depth (pretty-print helper).
250fn newline_at_indent(buf: &mut String, depth: usize, config: &SonConfig) {
251    buf.push('\n');
252    push_indent(buf, depth, config.indent);
253}
254
255// ============================================================================
256// Runtime Builtins
257// ============================================================================
258
259/// son.dump: Serialize top of stack to SON string (compact)
260/// Stack effect: ( Value -- String )
261///
262/// # Safety
263/// - The stack must be a valid stack pointer
264/// - The stack must contain at least one value
265#[unsafe(no_mangle)]
266pub unsafe extern "C" fn patch_seq_son_dump(stack: Stack) -> Stack {
267    unsafe { son_dump_impl(stack, false) }
268}
269
270/// son.dump-pretty: Serialize top of stack to SON string (pretty-printed)
271/// Stack effect: ( Value -- String )
272///
273/// # Safety
274/// - The stack must be a valid stack pointer
275/// - The stack must contain at least one value
276#[unsafe(no_mangle)]
277pub unsafe extern "C" fn patch_seq_son_dump_pretty(stack: Stack) -> Stack {
278    unsafe { son_dump_impl(stack, true) }
279}
280
281/// Implementation for both dump variants
282unsafe fn son_dump_impl(stack: Stack, pretty: bool) -> Stack {
283    let (rest, value) = unsafe { pop(stack) };
284
285    let config = if pretty {
286        SonConfig::pretty()
287    } else {
288        SonConfig::compact()
289    };
290
291    let result = value_to_son(&value, &config);
292    let result_str = SeqString::from(result);
293
294    unsafe { push(rest, Value::String(result_str)) }
295}
296
297// ============================================================================
298// Tests
299// ============================================================================
300
301#[cfg(test)]
302mod tests {
303    use super::*;
304    use crate::seqstring::global_string;
305    use std::sync::Arc;
306
307    #[test]
308    fn test_int() {
309        let v = Value::Int(42);
310        assert_eq!(value_to_son(&v, &SonConfig::default()), "42");
311    }
312
313    #[test]
314    fn test_negative_int() {
315        let v = Value::Int(-123);
316        assert_eq!(value_to_son(&v, &SonConfig::default()), "-123");
317    }
318
319    #[test]
320    fn test_float() {
321        let v = Value::Float(2.5);
322        assert_eq!(value_to_son(&v, &SonConfig::default()), "2.5");
323    }
324
325    #[test]
326    fn test_float_whole_number() {
327        let v = Value::Float(42.0);
328        let s = value_to_son(&v, &SonConfig::default());
329        assert!(s.contains('.'), "Float should contain decimal point: {}", s);
330    }
331
332    #[test]
333    fn test_bool_true() {
334        let v = Value::Bool(true);
335        assert_eq!(value_to_son(&v, &SonConfig::default()), "true");
336    }
337
338    #[test]
339    fn test_bool_false() {
340        let v = Value::Bool(false);
341        assert_eq!(value_to_son(&v, &SonConfig::default()), "false");
342    }
343
344    #[test]
345    fn test_string_simple() {
346        let v = Value::String(global_string("hello".to_string()));
347        assert_eq!(value_to_son(&v, &SonConfig::default()), r#""hello""#);
348    }
349
350    #[test]
351    fn test_string_escaping() {
352        let v = Value::String(global_string("hello\nworld".to_string()));
353        assert_eq!(value_to_son(&v, &SonConfig::default()), r#""hello\nworld""#);
354    }
355
356    #[test]
357    fn test_string_quotes() {
358        let v = Value::String(global_string(r#"say "hi""#.to_string()));
359        assert_eq!(value_to_son(&v, &SonConfig::default()), r#""say \"hi\"""#);
360    }
361
362    #[test]
363    fn test_symbol() {
364        let v = Value::Symbol(global_string("my-symbol".to_string()));
365        assert_eq!(value_to_son(&v, &SonConfig::default()), ":my-symbol");
366    }
367
368    #[test]
369    fn test_empty_list() {
370        let list = Value::Variant(Arc::new(VariantData::new(
371            global_string("List".to_string()),
372            vec![],
373        )));
374        assert_eq!(value_to_son(&list, &SonConfig::default()), "list-of");
375    }
376
377    #[test]
378    fn test_list() {
379        let list = Value::Variant(Arc::new(VariantData::new(
380            global_string("List".to_string()),
381            vec![Value::Int(1), Value::Int(2), Value::Int(3)],
382        )));
383        assert_eq!(
384            value_to_son(&list, &SonConfig::default()),
385            "list-of 1 lv 2 lv 3 lv"
386        );
387    }
388
389    #[test]
390    fn test_list_pretty() {
391        let list = Value::Variant(Arc::new(VariantData::new(
392            global_string("List".to_string()),
393            vec![Value::Int(1), Value::Int(2)],
394        )));
395        let expected = "list-of\n  1 lv\n  2 lv";
396        assert_eq!(value_to_son(&list, &SonConfig::pretty()), expected);
397    }
398
399    #[test]
400    fn test_empty_map() {
401        let m: HashMap<MapKey, Value> = HashMap::new();
402        let v = Value::Map(Box::new(m));
403        assert_eq!(value_to_son(&v, &SonConfig::default()), "map-of");
404    }
405
406    #[test]
407    fn test_map() {
408        let mut m = HashMap::new();
409        m.insert(
410            MapKey::String(global_string("key".to_string())),
411            Value::Int(42),
412        );
413        let v = Value::Map(Box::new(m));
414        assert_eq!(
415            value_to_son(&v, &SonConfig::default()),
416            r#"map-of "key" 42 kv"#
417        );
418    }
419
420    #[test]
421    fn test_variant_no_fields() {
422        let v = Value::Variant(Arc::new(VariantData::new(
423            global_string("None".to_string()),
424            vec![],
425        )));
426        assert_eq!(value_to_son(&v, &SonConfig::default()), ":None wrap-0");
427    }
428
429    #[test]
430    fn test_variant_with_fields() {
431        let v = Value::Variant(Arc::new(VariantData::new(
432            global_string("Point".to_string()),
433            vec![Value::Int(10), Value::Int(20)],
434        )));
435        assert_eq!(
436            value_to_son(&v, &SonConfig::default()),
437            ":Point 10 20 wrap-2"
438        );
439    }
440
441    #[test]
442    fn test_variant_pretty() {
443        let v = Value::Variant(Arc::new(VariantData::new(
444            global_string("Point".to_string()),
445            vec![Value::Int(10), Value::Int(20)],
446        )));
447        let expected = ":Point\n  10\n  20\n wrap-2";
448        assert_eq!(value_to_son(&v, &SonConfig::pretty()), expected);
449    }
450
451    #[test]
452    fn test_nested_list_in_map() {
453        let list = Value::Variant(Arc::new(VariantData::new(
454            global_string("List".to_string()),
455            vec![Value::Int(1), Value::Int(2)],
456        )));
457        let mut m = HashMap::new();
458        m.insert(MapKey::String(global_string("items".to_string())), list);
459        let v = Value::Map(Box::new(m));
460        assert_eq!(
461            value_to_son(&v, &SonConfig::default()),
462            r#"map-of "items" list-of 1 lv 2 lv kv"#
463        );
464    }
465
466    #[test]
467    fn test_quotation() {
468        let v = Value::Quotation {
469            wrapper: 0,
470            impl_: 0,
471        };
472        assert_eq!(value_to_son(&v, &SonConfig::default()), "<quotation>");
473    }
474
475    #[test]
476    fn test_closure() {
477        let v = Value::Closure {
478            fn_ptr: 0,
479            env: Arc::new([]),
480        };
481        assert_eq!(value_to_son(&v, &SonConfig::default()), "<closure>");
482    }
483}