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            format_string(s.as_str(), buf);
81        }
82        Value::Symbol(s) => {
83            buf.push(':');
84            buf.push_str(s.as_str());
85        }
86        Value::Variant(v) => {
87            format_variant(v, config, depth, buf);
88        }
89        Value::Map(m) => {
90            format_map(m, config, depth, buf);
91        }
92        Value::Quotation { .. } => {
93            buf.push_str("<quotation>");
94        }
95        Value::Closure { .. } => {
96            buf.push_str("<closure>");
97        }
98        Value::Channel(_) => {
99            buf.push_str("<channel>");
100        }
101        Value::WeaveCtx { .. } => {
102            buf.push_str("<weave-ctx>");
103        }
104    }
105}
106
107/// Format a string with proper escaping
108fn format_string(s: &str, buf: &mut String) {
109    buf.push('"');
110    for c in s.chars() {
111        match c {
112            '"' => buf.push_str("\\\""),
113            '\\' => buf.push_str("\\\\"),
114            '\n' => buf.push_str("\\n"),
115            '\r' => buf.push_str("\\r"),
116            '\t' => buf.push_str("\\t"),
117            '\x08' => buf.push_str("\\b"),
118            '\x0C' => buf.push_str("\\f"),
119            c if c.is_control() => {
120                buf.push_str(&format!("\\u{:04x}", c as u32));
121            }
122            c => buf.push(c),
123        }
124    }
125    buf.push('"');
126}
127
128/// Format a variant (includes List as special case)
129fn format_variant(v: &VariantData, config: &SonConfig, depth: usize, buf: &mut String) {
130    let tag = v.tag.as_str();
131
132    // Special case: List variant uses list-of/lv syntax
133    if tag == "List" {
134        format_list(&v.fields, config, depth, buf);
135    } else {
136        // General variant: :Tag field1 field2 wrap-N
137        buf.push(':');
138        buf.push_str(tag);
139
140        let field_count = v.fields.len();
141
142        if config.pretty && !v.fields.is_empty() {
143            for field in v.fields.iter() {
144                newline_at_indent(buf, depth + 1, config);
145                format_value(field, config, depth + 1, buf);
146            }
147            newline_at_indent(buf, depth, config);
148        } else {
149            for field in v.fields.iter() {
150                buf.push(' ');
151                format_value(field, config, depth, buf);
152            }
153        }
154
155        buf.push_str(&format!(" wrap-{}", field_count));
156    }
157}
158
159/// Format a list using list-of/lv syntax
160fn format_list(fields: &[Value], config: &SonConfig, depth: usize, buf: &mut String) {
161    buf.push_str("list-of");
162
163    if fields.is_empty() {
164        return;
165    }
166
167    if config.pretty {
168        for field in fields.iter() {
169            newline_at_indent(buf, depth + 1, config);
170            format_value(field, config, depth + 1, buf);
171            buf.push_str(" lv");
172        }
173    } else {
174        for field in fields.iter() {
175            buf.push(' ');
176            format_value(field, config, depth, buf);
177            buf.push_str(" lv");
178        }
179    }
180}
181
182/// Format a map using map-of/kv syntax
183fn format_map(map: &HashMap<MapKey, Value>, config: &SonConfig, depth: usize, buf: &mut String) {
184    buf.push_str("map-of");
185
186    if map.is_empty() {
187        return;
188    }
189
190    // Sort keys for deterministic output (important for testing/debugging)
191    let mut entries: Vec<_> = map.iter().collect();
192    entries.sort_by(|(k1, _), (k2, _)| {
193        let s1 = map_key_sort_string(k1);
194        let s2 = map_key_sort_string(k2);
195        s1.cmp(&s2)
196    });
197
198    if config.pretty {
199        for (key, value) in entries {
200            newline_at_indent(buf, depth + 1, config);
201            format_map_key(key, buf);
202            buf.push(' ');
203            format_value(value, config, depth + 1, buf);
204            buf.push_str(" kv");
205        }
206    } else {
207        for (key, value) in entries {
208            buf.push(' ');
209            format_map_key(key, buf);
210            buf.push(' ');
211            format_value(value, config, depth, buf);
212            buf.push_str(" kv");
213        }
214    }
215}
216
217/// Get a sort key string for a MapKey
218fn map_key_sort_string(key: &MapKey) -> String {
219    match key {
220        MapKey::Int(n) => format!("0_{:020}", n), // Prefix with 0 for ints
221        MapKey::Bool(b) => format!("1_{}", b),    // Prefix with 1 for bools
222        MapKey::String(s) => format!("2_{}", s.as_str()), // Prefix with 2 for strings
223    }
224}
225
226/// Format a map key
227fn format_map_key(key: &MapKey, buf: &mut String) {
228    match key {
229        MapKey::Int(n) => buf.push_str(&n.to_string()),
230        MapKey::Bool(b) => buf.push_str(if *b { "true" } else { "false" }),
231        MapKey::String(s) => format_string(s.as_str(), buf),
232    }
233}
234
235/// Push indentation spaces
236fn push_indent(buf: &mut String, depth: usize, indent_size: usize) {
237    for _ in 0..(depth * indent_size) {
238        buf.push(' ');
239    }
240}
241
242/// Start a new line and indent to the given depth (pretty-print helper).
243fn newline_at_indent(buf: &mut String, depth: usize, config: &SonConfig) {
244    buf.push('\n');
245    push_indent(buf, depth, config.indent);
246}
247
248// ============================================================================
249// Runtime Builtins
250// ============================================================================
251
252/// son.dump: Serialize top of stack to SON string (compact)
253/// Stack effect: ( Value -- String )
254///
255/// # Safety
256/// - The stack must be a valid stack pointer
257/// - The stack must contain at least one value
258#[unsafe(no_mangle)]
259pub unsafe extern "C" fn patch_seq_son_dump(stack: Stack) -> Stack {
260    unsafe { son_dump_impl(stack, false) }
261}
262
263/// son.dump-pretty: Serialize top of stack to SON string (pretty-printed)
264/// Stack effect: ( Value -- String )
265///
266/// # Safety
267/// - The stack must be a valid stack pointer
268/// - The stack must contain at least one value
269#[unsafe(no_mangle)]
270pub unsafe extern "C" fn patch_seq_son_dump_pretty(stack: Stack) -> Stack {
271    unsafe { son_dump_impl(stack, true) }
272}
273
274/// Implementation for both dump variants
275unsafe fn son_dump_impl(stack: Stack, pretty: bool) -> Stack {
276    let (rest, value) = unsafe { pop(stack) };
277
278    let config = if pretty {
279        SonConfig::pretty()
280    } else {
281        SonConfig::compact()
282    };
283
284    let result = value_to_son(&value, &config);
285    let result_str = SeqString::from(result);
286
287    unsafe { push(rest, Value::String(result_str)) }
288}
289
290// ============================================================================
291// Tests
292// ============================================================================
293
294#[cfg(test)]
295mod tests {
296    use super::*;
297    use crate::seqstring::global_string;
298    use std::sync::Arc;
299
300    #[test]
301    fn test_int() {
302        let v = Value::Int(42);
303        assert_eq!(value_to_son(&v, &SonConfig::default()), "42");
304    }
305
306    #[test]
307    fn test_negative_int() {
308        let v = Value::Int(-123);
309        assert_eq!(value_to_son(&v, &SonConfig::default()), "-123");
310    }
311
312    #[test]
313    fn test_float() {
314        let v = Value::Float(2.5);
315        assert_eq!(value_to_son(&v, &SonConfig::default()), "2.5");
316    }
317
318    #[test]
319    fn test_float_whole_number() {
320        let v = Value::Float(42.0);
321        let s = value_to_son(&v, &SonConfig::default());
322        assert!(s.contains('.'), "Float should contain decimal point: {}", s);
323    }
324
325    #[test]
326    fn test_bool_true() {
327        let v = Value::Bool(true);
328        assert_eq!(value_to_son(&v, &SonConfig::default()), "true");
329    }
330
331    #[test]
332    fn test_bool_false() {
333        let v = Value::Bool(false);
334        assert_eq!(value_to_son(&v, &SonConfig::default()), "false");
335    }
336
337    #[test]
338    fn test_string_simple() {
339        let v = Value::String(global_string("hello".to_string()));
340        assert_eq!(value_to_son(&v, &SonConfig::default()), r#""hello""#);
341    }
342
343    #[test]
344    fn test_string_escaping() {
345        let v = Value::String(global_string("hello\nworld".to_string()));
346        assert_eq!(value_to_son(&v, &SonConfig::default()), r#""hello\nworld""#);
347    }
348
349    #[test]
350    fn test_string_quotes() {
351        let v = Value::String(global_string(r#"say "hi""#.to_string()));
352        assert_eq!(value_to_son(&v, &SonConfig::default()), r#""say \"hi\"""#);
353    }
354
355    #[test]
356    fn test_symbol() {
357        let v = Value::Symbol(global_string("my-symbol".to_string()));
358        assert_eq!(value_to_son(&v, &SonConfig::default()), ":my-symbol");
359    }
360
361    #[test]
362    fn test_empty_list() {
363        let list = Value::Variant(Arc::new(VariantData::new(
364            global_string("List".to_string()),
365            vec![],
366        )));
367        assert_eq!(value_to_son(&list, &SonConfig::default()), "list-of");
368    }
369
370    #[test]
371    fn test_list() {
372        let list = Value::Variant(Arc::new(VariantData::new(
373            global_string("List".to_string()),
374            vec![Value::Int(1), Value::Int(2), Value::Int(3)],
375        )));
376        assert_eq!(
377            value_to_son(&list, &SonConfig::default()),
378            "list-of 1 lv 2 lv 3 lv"
379        );
380    }
381
382    #[test]
383    fn test_list_pretty() {
384        let list = Value::Variant(Arc::new(VariantData::new(
385            global_string("List".to_string()),
386            vec![Value::Int(1), Value::Int(2)],
387        )));
388        let expected = "list-of\n  1 lv\n  2 lv";
389        assert_eq!(value_to_son(&list, &SonConfig::pretty()), expected);
390    }
391
392    #[test]
393    fn test_empty_map() {
394        let m: HashMap<MapKey, Value> = HashMap::new();
395        let v = Value::Map(Box::new(m));
396        assert_eq!(value_to_son(&v, &SonConfig::default()), "map-of");
397    }
398
399    #[test]
400    fn test_map() {
401        let mut m = HashMap::new();
402        m.insert(
403            MapKey::String(global_string("key".to_string())),
404            Value::Int(42),
405        );
406        let v = Value::Map(Box::new(m));
407        assert_eq!(
408            value_to_son(&v, &SonConfig::default()),
409            r#"map-of "key" 42 kv"#
410        );
411    }
412
413    #[test]
414    fn test_variant_no_fields() {
415        let v = Value::Variant(Arc::new(VariantData::new(
416            global_string("None".to_string()),
417            vec![],
418        )));
419        assert_eq!(value_to_son(&v, &SonConfig::default()), ":None wrap-0");
420    }
421
422    #[test]
423    fn test_variant_with_fields() {
424        let v = Value::Variant(Arc::new(VariantData::new(
425            global_string("Point".to_string()),
426            vec![Value::Int(10), Value::Int(20)],
427        )));
428        assert_eq!(
429            value_to_son(&v, &SonConfig::default()),
430            ":Point 10 20 wrap-2"
431        );
432    }
433
434    #[test]
435    fn test_variant_pretty() {
436        let v = Value::Variant(Arc::new(VariantData::new(
437            global_string("Point".to_string()),
438            vec![Value::Int(10), Value::Int(20)],
439        )));
440        let expected = ":Point\n  10\n  20\n wrap-2";
441        assert_eq!(value_to_son(&v, &SonConfig::pretty()), expected);
442    }
443
444    #[test]
445    fn test_nested_list_in_map() {
446        let list = Value::Variant(Arc::new(VariantData::new(
447            global_string("List".to_string()),
448            vec![Value::Int(1), Value::Int(2)],
449        )));
450        let mut m = HashMap::new();
451        m.insert(MapKey::String(global_string("items".to_string())), list);
452        let v = Value::Map(Box::new(m));
453        assert_eq!(
454            value_to_son(&v, &SonConfig::default()),
455            r#"map-of "items" list-of 1 lv 2 lv kv"#
456        );
457    }
458
459    #[test]
460    fn test_quotation() {
461        let v = Value::Quotation {
462            wrapper: 0,
463            impl_: 0,
464        };
465        assert_eq!(value_to_son(&v, &SonConfig::default()), "<quotation>");
466    }
467
468    #[test]
469    fn test_closure() {
470        let v = Value::Closure {
471            fn_ptr: 0,
472            env: Arc::new([]),
473        };
474        assert_eq!(value_to_son(&v, &SonConfig::default()), "<closure>");
475    }
476}