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