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