Skip to main content

tensorlogic_ir/expr_serialize/
mod.rs

1//! Expression serialization for TLExpr and EinsumGraph.
2//!
3//! Provides custom S-expression text format and compact binary format for
4//! serializing/deserializing logical expressions and computation graphs.
5//! This enables saving/loading compiled expressions for caching, transfer, and debugging.
6
7mod binary;
8mod sexpr;
9
10use crate::TLExpr;
11
12pub use binary::{from_binary, graph_from_binary, graph_to_binary, to_binary};
13pub use sexpr::{from_sexpr, to_sexpr};
14
15// ============================================================================
16// Error type
17// ============================================================================
18
19/// Error type for serialization operations.
20#[derive(Debug, Clone)]
21pub enum ExprSerializeError {
22    /// I/O related error
23    IoError(String),
24    /// Format/parsing error
25    FormatError(String),
26    /// Unknown variant tag encountered
27    UnknownVariant(String),
28    /// Binary format version mismatch
29    VersionMismatch { expected: u32, got: u32 },
30    /// Invalid magic bytes in binary header
31    InvalidMagic,
32    /// Input was truncated (unexpected end)
33    TruncatedInput,
34    /// UTF-8 decoding error
35    Utf8Error(String),
36}
37
38impl std::fmt::Display for ExprSerializeError {
39    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40        match self {
41            Self::IoError(msg) => write!(f, "IO error: {msg}"),
42            Self::FormatError(msg) => write!(f, "Format error: {msg}"),
43            Self::UnknownVariant(v) => write!(f, "Unknown variant: {v}"),
44            Self::VersionMismatch { expected, got } => {
45                write!(f, "Version mismatch: expected {expected}, got {got}")
46            }
47            Self::InvalidMagic => write!(f, "Invalid magic bytes"),
48            Self::TruncatedInput => write!(f, "Truncated input"),
49            Self::Utf8Error(msg) => write!(f, "UTF-8 error: {msg}"),
50        }
51    }
52}
53
54impl std::error::Error for ExprSerializeError {}
55
56// ============================================================================
57// ExprFormat
58// ============================================================================
59
60/// Serialization format selector.
61#[derive(Debug, Clone, PartialEq)]
62pub enum ExprFormat {
63    /// S-expression text format: `(And (Not (Var "x")) (Pred "p" (Var "y")))`
64    SExpr,
65    /// Compact binary format with magic + version header
66    Binary,
67}
68
69// ============================================================================
70// Constants (shared between binary and sexpr modules)
71// ============================================================================
72
73/// Binary format magic bytes for single expressions: "TLEX"
74pub(crate) const TLEX_MAGIC: [u8; 4] = [0x54, 0x4C, 0x45, 0x58];
75/// Binary format magic bytes for batch expressions: "TLBX"
76pub(crate) const TLBX_MAGIC: [u8; 4] = [0x54, 0x4C, 0x42, 0x58];
77/// Binary format magic bytes for graphs: "TLGR"
78pub(crate) const TLGR_MAGIC: [u8; 4] = [0x54, 0x4C, 0x47, 0x52];
79/// Current binary format version
80pub(crate) const FORMAT_VER: u32 = 1;
81
82// ============================================================================
83// Tag assignments for TLExpr variants (u8)
84// ============================================================================
85
86pub(crate) const TAG_PRED: u8 = 0;
87pub(crate) const TAG_AND: u8 = 1;
88pub(crate) const TAG_OR: u8 = 2;
89pub(crate) const TAG_NOT: u8 = 3;
90pub(crate) const TAG_EXISTS: u8 = 4;
91pub(crate) const TAG_FORALL: u8 = 5;
92pub(crate) const TAG_IMPLY: u8 = 6;
93pub(crate) const TAG_SCORE: u8 = 7;
94pub(crate) const TAG_ADD: u8 = 8;
95pub(crate) const TAG_SUB: u8 = 9;
96pub(crate) const TAG_MUL: u8 = 10;
97pub(crate) const TAG_DIV: u8 = 11;
98pub(crate) const TAG_POW: u8 = 12;
99pub(crate) const TAG_MOD: u8 = 13;
100pub(crate) const TAG_MIN: u8 = 14;
101pub(crate) const TAG_MAX: u8 = 15;
102pub(crate) const TAG_ABS: u8 = 16;
103pub(crate) const TAG_FLOOR: u8 = 17;
104pub(crate) const TAG_CEIL: u8 = 18;
105pub(crate) const TAG_ROUND: u8 = 19;
106pub(crate) const TAG_SQRT: u8 = 20;
107pub(crate) const TAG_EXP: u8 = 21;
108pub(crate) const TAG_LOG: u8 = 22;
109pub(crate) const TAG_SIN: u8 = 23;
110pub(crate) const TAG_COS: u8 = 24;
111pub(crate) const TAG_TAN: u8 = 25;
112pub(crate) const TAG_EQ: u8 = 26;
113pub(crate) const TAG_LT: u8 = 27;
114pub(crate) const TAG_GT: u8 = 28;
115pub(crate) const TAG_LTE: u8 = 29;
116pub(crate) const TAG_GTE: u8 = 30;
117pub(crate) const TAG_IF_THEN_ELSE: u8 = 31;
118pub(crate) const TAG_CONSTANT: u8 = 32;
119pub(crate) const TAG_AGGREGATE: u8 = 33;
120pub(crate) const TAG_LET: u8 = 34;
121pub(crate) const TAG_BOX: u8 = 35;
122pub(crate) const TAG_DIAMOND: u8 = 36;
123pub(crate) const TAG_NEXT: u8 = 37;
124pub(crate) const TAG_EVENTUALLY: u8 = 38;
125pub(crate) const TAG_ALWAYS: u8 = 39;
126pub(crate) const TAG_UNTIL: u8 = 40;
127pub(crate) const TAG_TNORM: u8 = 41;
128pub(crate) const TAG_TCONORM: u8 = 42;
129pub(crate) const TAG_FUZZY_NOT: u8 = 43;
130pub(crate) const TAG_FUZZY_IMPLICATION: u8 = 44;
131pub(crate) const TAG_SOFT_EXISTS: u8 = 45;
132pub(crate) const TAG_SOFT_FORALL: u8 = 46;
133pub(crate) const TAG_WEIGHTED_RULE: u8 = 47;
134pub(crate) const TAG_PROBABILISTIC_CHOICE: u8 = 48;
135pub(crate) const TAG_RELEASE: u8 = 49;
136pub(crate) const TAG_WEAK_UNTIL: u8 = 50;
137pub(crate) const TAG_STRONG_RELEASE: u8 = 51;
138pub(crate) const TAG_LAMBDA: u8 = 52;
139pub(crate) const TAG_APPLY: u8 = 53;
140pub(crate) const TAG_SET_MEMBERSHIP: u8 = 54;
141pub(crate) const TAG_SET_UNION: u8 = 55;
142pub(crate) const TAG_SET_INTERSECTION: u8 = 56;
143pub(crate) const TAG_SET_DIFFERENCE: u8 = 57;
144pub(crate) const TAG_SET_CARDINALITY: u8 = 58;
145pub(crate) const TAG_EMPTY_SET: u8 = 59;
146pub(crate) const TAG_SET_COMPREHENSION: u8 = 60;
147pub(crate) const TAG_COUNTING_EXISTS: u8 = 61;
148pub(crate) const TAG_COUNTING_FORALL: u8 = 62;
149pub(crate) const TAG_EXACT_COUNT: u8 = 63;
150pub(crate) const TAG_MAJORITY: u8 = 64;
151pub(crate) const TAG_LEAST_FIXPOINT: u8 = 65;
152pub(crate) const TAG_GREATEST_FIXPOINT: u8 = 66;
153pub(crate) const TAG_NOMINAL: u8 = 67;
154pub(crate) const TAG_AT: u8 = 68;
155pub(crate) const TAG_SOMEWHERE: u8 = 69;
156pub(crate) const TAG_EVERYWHERE: u8 = 70;
157pub(crate) const TAG_ALL_DIFFERENT: u8 = 71;
158pub(crate) const TAG_GLOBAL_CARDINALITY: u8 = 72;
159pub(crate) const TAG_ABDUCIBLE: u8 = 73;
160pub(crate) const TAG_EXPLAIN: u8 = 74;
161pub(crate) const TAG_SYMBOL_LITERAL: u8 = 75;
162pub(crate) const TAG_MATCH: u8 = 76;
163pub(crate) const TAG_PATTERN_CONST_SYMBOL: u8 = 0;
164pub(crate) const TAG_PATTERN_CONST_NUMBER: u8 = 1;
165pub(crate) const TAG_PATTERN_WILDCARD: u8 = 2;
166
167// Term tags
168pub(crate) const TERM_TAG_VAR: u8 = 0;
169pub(crate) const TERM_TAG_CONST: u8 = 1;
170pub(crate) const TERM_TAG_TYPED: u8 = 2;
171
172// AggregateOp tags
173pub(crate) const AGG_COUNT: u8 = 0;
174pub(crate) const AGG_SUM: u8 = 1;
175pub(crate) const AGG_AVERAGE: u8 = 2;
176pub(crate) const AGG_MAX: u8 = 3;
177pub(crate) const AGG_MIN: u8 = 4;
178pub(crate) const AGG_PRODUCT: u8 = 5;
179pub(crate) const AGG_ANY: u8 = 6;
180pub(crate) const AGG_ALL: u8 = 7;
181
182// TNormKind tags
183pub(crate) const TNORM_MINIMUM: u8 = 0;
184pub(crate) const TNORM_PRODUCT: u8 = 1;
185pub(crate) const TNORM_LUKASIEWICZ: u8 = 2;
186pub(crate) const TNORM_DRASTIC: u8 = 3;
187pub(crate) const TNORM_NILPOTENT_MINIMUM: u8 = 4;
188pub(crate) const TNORM_HAMACHER: u8 = 5;
189
190// TCoNormKind tags
191pub(crate) const TCONORM_MAXIMUM: u8 = 0;
192pub(crate) const TCONORM_PROBABILISTIC_SUM: u8 = 1;
193pub(crate) const TCONORM_BOUNDED_SUM: u8 = 2;
194pub(crate) const TCONORM_DRASTIC: u8 = 3;
195pub(crate) const TCONORM_NILPOTENT_MAXIMUM: u8 = 4;
196pub(crate) const TCONORM_HAMACHER: u8 = 5;
197
198// FuzzyNegationKind tags
199pub(crate) const FNEG_STANDARD: u8 = 0;
200pub(crate) const FNEG_SUGENO: u8 = 1;
201pub(crate) const FNEG_YAGER: u8 = 2;
202
203// FuzzyImplicationKind tags
204pub(crate) const FIMP_GODEL: u8 = 0;
205pub(crate) const FIMP_LUKASIEWICZ: u8 = 1;
206pub(crate) const FIMP_REICHENBACH: u8 = 2;
207pub(crate) const FIMP_KLEENE_DIENES: u8 = 3;
208pub(crate) const FIMP_RESCHER: u8 = 4;
209pub(crate) const FIMP_GOGUEN: u8 = 5;
210
211// OpType tags for graph serialization
212pub(crate) const OP_EINSUM: u8 = 0;
213pub(crate) const OP_ELEM_UNARY: u8 = 1;
214pub(crate) const OP_ELEM_BINARY: u8 = 2;
215pub(crate) const OP_REDUCE: u8 = 3;
216
217// ============================================================================
218// Fingerprint (FNV-1a)
219// ============================================================================
220
221/// Compute a 64-bit FNV-1a hash/fingerprint of a `TLExpr` for caching.
222pub fn expr_fingerprint(expr: &TLExpr) -> u64 {
223    let bin = to_binary(expr);
224    fnv1a_hash(&bin[8..]) // skip magic + version, hash only payload
225}
226
227/// FNV-1a 64-bit hash implementation.
228fn fnv1a_hash(data: &[u8]) -> u64 {
229    const FNV_OFFSET_BASIS: u64 = 0xcbf29ce484222325;
230    const FNV_PRIME: u64 = 0x00000100000001B3;
231    let mut hash = FNV_OFFSET_BASIS;
232    for &byte in data {
233        hash ^= byte as u64;
234        hash = hash.wrapping_mul(FNV_PRIME);
235    }
236    hash
237}
238
239/// Compare two serialized binary forms for equality without deserializing.
240pub fn binary_equal(a: &[u8], b: &[u8]) -> bool {
241    a == b
242}
243
244// ============================================================================
245// SerializationStats
246// ============================================================================
247
248/// Statistics about the serialized form of a `TLExpr`.
249#[derive(Debug, Clone)]
250pub struct SerializationStats {
251    /// Size of the S-expression representation in bytes
252    pub sexpr_bytes: usize,
253    /// Size of the binary representation in bytes
254    pub binary_bytes: usize,
255    /// Compression ratio: sexpr_bytes / binary_bytes
256    pub compression_ratio: f64,
257    /// Number of AST nodes in the expression
258    pub node_count: usize,
259    /// Maximum nesting depth
260    pub max_depth: usize,
261}
262
263impl SerializationStats {
264    /// Compute serialization statistics for a `TLExpr`.
265    pub fn compute(expr: &TLExpr) -> Self {
266        let sexpr_str = to_sexpr(expr);
267        let bin = to_binary(expr);
268        let sexpr_bytes = sexpr_str.len();
269        let binary_bytes = bin.len();
270        let compression_ratio = if binary_bytes > 0 {
271            sexpr_bytes as f64 / binary_bytes as f64
272        } else {
273            0.0
274        };
275        let node_count = count_nodes(expr);
276        let max_depth = compute_depth(expr);
277        Self {
278            sexpr_bytes,
279            binary_bytes,
280            compression_ratio,
281            node_count,
282            max_depth,
283        }
284    }
285
286    /// Return a human-readable summary string.
287    pub fn summary(&self) -> String {
288        format!(
289            "sexpr={} bytes, binary={} bytes, ratio={:.2}, nodes={}, depth={}",
290            self.sexpr_bytes,
291            self.binary_bytes,
292            self.compression_ratio,
293            self.node_count,
294            self.max_depth
295        )
296    }
297}
298
299fn count_nodes(expr: &TLExpr) -> usize {
300    let mut count = 1usize;
301    visit_children(expr, &mut |child| count += count_nodes(child));
302    count
303}
304
305fn compute_depth(expr: &TLExpr) -> usize {
306    let mut max_child_depth = 0usize;
307    visit_children(expr, &mut |child| {
308        let d = compute_depth(child);
309        if d > max_child_depth {
310            max_child_depth = d;
311        }
312    });
313    1 + max_child_depth
314}
315
316/// Visit immediate children of a `TLExpr`.
317fn visit_children(expr: &TLExpr, f: &mut impl FnMut(&TLExpr)) {
318    match expr {
319        TLExpr::Pred { .. }
320        | TLExpr::Constant(_)
321        | TLExpr::EmptySet
322        | TLExpr::Nominal { .. }
323        | TLExpr::AllDifferent { .. }
324        | TLExpr::Abducible { .. } => {}
325
326        TLExpr::Not(e)
327        | TLExpr::Score(e)
328        | TLExpr::Abs(e)
329        | TLExpr::Floor(e)
330        | TLExpr::Ceil(e)
331        | TLExpr::Round(e)
332        | TLExpr::Sqrt(e)
333        | TLExpr::Exp(e)
334        | TLExpr::Log(e)
335        | TLExpr::Sin(e)
336        | TLExpr::Cos(e)
337        | TLExpr::Tan(e)
338        | TLExpr::Box(e)
339        | TLExpr::Diamond(e)
340        | TLExpr::Next(e)
341        | TLExpr::Eventually(e)
342        | TLExpr::Always(e) => f(e),
343
344        TLExpr::And(a, b)
345        | TLExpr::Or(a, b)
346        | TLExpr::Imply(a, b)
347        | TLExpr::Add(a, b)
348        | TLExpr::Sub(a, b)
349        | TLExpr::Mul(a, b)
350        | TLExpr::Div(a, b)
351        | TLExpr::Pow(a, b)
352        | TLExpr::Mod(a, b)
353        | TLExpr::Min(a, b)
354        | TLExpr::Max(a, b)
355        | TLExpr::Eq(a, b)
356        | TLExpr::Lt(a, b)
357        | TLExpr::Gt(a, b)
358        | TLExpr::Lte(a, b)
359        | TLExpr::Gte(a, b) => {
360            f(a);
361            f(b);
362        }
363
364        TLExpr::IfThenElse {
365            condition,
366            then_branch,
367            else_branch,
368        } => {
369            f(condition);
370            f(then_branch);
371            f(else_branch);
372        }
373
374        TLExpr::Exists { body, .. }
375        | TLExpr::ForAll { body, .. }
376        | TLExpr::Majority { body, .. }
377        | TLExpr::SetComprehension {
378            condition: body, ..
379        } => f(body),
380
381        TLExpr::Aggregate { body, .. } => f(body),
382
383        TLExpr::Let { value, body, .. } => {
384            f(value);
385            f(body);
386        }
387
388        TLExpr::Until { before, after } | TLExpr::WeakUntil { before, after } => {
389            f(before);
390            f(after);
391        }
392
393        TLExpr::Release { released, releaser } | TLExpr::StrongRelease { released, releaser } => {
394            f(released);
395            f(releaser);
396        }
397
398        TLExpr::TNorm { left, right, .. } | TLExpr::TCoNorm { left, right, .. } => {
399            f(left);
400            f(right);
401        }
402
403        TLExpr::FuzzyNot { expr: e, .. } => f(e),
404        TLExpr::FuzzyImplication {
405            premise,
406            conclusion,
407            ..
408        } => {
409            f(premise);
410            f(conclusion);
411        }
412
413        TLExpr::SoftExists { body, .. } | TLExpr::SoftForAll { body, .. } => f(body),
414        TLExpr::WeightedRule { rule, .. } => f(rule),
415
416        TLExpr::ProbabilisticChoice { alternatives } => {
417            for (_, alt_expr) in alternatives {
418                f(alt_expr);
419            }
420        }
421
422        TLExpr::Lambda { body, .. }
423        | TLExpr::LeastFixpoint { body, .. }
424        | TLExpr::GreatestFixpoint { body, .. } => f(body),
425
426        TLExpr::Apply { function, argument } => {
427            f(function);
428            f(argument);
429        }
430
431        TLExpr::SetMembership { element, set } => {
432            f(element);
433            f(set);
434        }
435        TLExpr::SetUnion { left, right }
436        | TLExpr::SetIntersection { left, right }
437        | TLExpr::SetDifference { left, right } => {
438            f(left);
439            f(right);
440        }
441
442        TLExpr::SetCardinality { set } => f(set),
443
444        TLExpr::CountingExists { body, .. }
445        | TLExpr::CountingForAll { body, .. }
446        | TLExpr::ExactCount { body, .. } => f(body),
447
448        TLExpr::At { formula, .. }
449        | TLExpr::Somewhere { formula }
450        | TLExpr::Everywhere { formula }
451        | TLExpr::Explain { formula } => f(formula),
452
453        TLExpr::GlobalCardinality { values, .. } => {
454            for v in values {
455                f(v);
456            }
457        }
458
459        TLExpr::SymbolLiteral(_) => {}
460
461        TLExpr::Match { scrutinee, arms } => {
462            f(scrutinee);
463            for (_, body) in arms {
464                f(body);
465            }
466        }
467    }
468}
469
470// ============================================================================
471// Batch serialization
472// ============================================================================
473
474/// Serialize multiple expressions efficiently into a single binary blob.
475pub fn batch_to_binary(exprs: &[TLExpr]) -> Vec<u8> {
476    let mut w = binary::BinWriter::new();
477    w.write_magic(&TLBX_MAGIC);
478    w.write_u32(FORMAT_VER);
479    w.write_u32(exprs.len() as u32);
480    for expr in exprs {
481        binary::write_expr_bin(expr, &mut w);
482    }
483    w.into_bytes()
484}
485
486/// Deserialize multiple expressions from a batch binary blob.
487pub fn batch_from_binary(bytes: &[u8]) -> Result<Vec<TLExpr>, ExprSerializeError> {
488    let mut r = binary::BinReader::new(bytes);
489    let magic = r.read_magic()?;
490    if magic != TLBX_MAGIC {
491        return Err(ExprSerializeError::InvalidMagic);
492    }
493    let version = r.read_u32()?;
494    if version != FORMAT_VER {
495        return Err(ExprSerializeError::VersionMismatch {
496            expected: FORMAT_VER,
497            got: version,
498        });
499    }
500    let count = r.read_u32()? as usize;
501    let mut exprs = Vec::with_capacity(count);
502    for _ in 0..count {
503        exprs.push(binary::read_expr_bin(&mut r)?);
504    }
505    Ok(exprs)
506}
507
508// ============================================================================
509// Tests
510// ============================================================================
511
512#[cfg(test)]
513mod tests {
514    use super::*;
515    use crate::{EinsumGraph, EinsumNode, TLExpr, TNormKind, Term};
516
517    fn simple_pred(name: &str, arg: &str) -> TLExpr {
518        TLExpr::Pred {
519            name: name.to_string(),
520            args: vec![Term::Var(arg.to_string())],
521        }
522    }
523
524    #[test]
525    fn test_sexpr_variable() {
526        let expr = simple_pred("P", "x");
527        let s = to_sexpr(&expr);
528        assert!(s.contains("x"));
529        assert!(s.contains("Pred"));
530    }
531
532    #[test]
533    fn test_sexpr_constant() {
534        let expr = TLExpr::Constant(3.15);
535        let s = to_sexpr(&expr);
536        assert!(s.contains("3.15"));
537        assert!(s.contains("Constant"));
538    }
539
540    #[test]
541    fn test_sexpr_not() {
542        let expr = TLExpr::Not(Box::new(simple_pred("P", "x")));
543        let s = to_sexpr(&expr);
544        assert!(s.contains("Not"));
545        assert!(s.contains("Pred"));
546    }
547
548    #[test]
549    fn test_sexpr_and() {
550        let a = simple_pred("P", "x");
551        let b = simple_pred("Q", "y");
552        let expr = TLExpr::And(Box::new(a), Box::new(b));
553        let s = to_sexpr(&expr);
554        assert!(s.contains("And"));
555        assert!(s.contains("\"P\""));
556        assert!(s.contains("\"Q\""));
557    }
558
559    #[test]
560    fn test_sexpr_roundtrip_simple() {
561        let expr = TLExpr::Constant(42.0);
562        let s = to_sexpr(&expr);
563        let parsed = from_sexpr(&s).expect("parse failed");
564        assert_eq!(parsed, expr);
565    }
566
567    #[test]
568    fn test_sexpr_roundtrip_nested() {
569        let inner = TLExpr::And(
570            Box::new(simple_pred("P", "x")),
571            Box::new(TLExpr::Not(Box::new(simple_pred("Q", "y")))),
572        );
573        let expr = TLExpr::ForAll {
574            var: "x".to_string(),
575            domain: "Entity".to_string(),
576            body: Box::new(inner),
577        };
578        let s = to_sexpr(&expr);
579        let parsed = from_sexpr(&s).expect("parse failed");
580        assert_eq!(parsed, expr);
581    }
582
583    #[test]
584    fn test_sexpr_parse_error() {
585        let result = from_sexpr("not valid sexpr )))");
586        assert!(result.is_err());
587    }
588
589    #[test]
590    fn test_binary_roundtrip_variable() {
591        let expr = simple_pred("P", "x");
592        let bin = to_binary(&expr);
593        let parsed = from_binary(&bin).expect("binary parse failed");
594        assert_eq!(parsed, expr);
595    }
596
597    #[test]
598    fn test_binary_roundtrip_constant() {
599        let expr = TLExpr::Constant(2.719);
600        let bin = to_binary(&expr);
601        let parsed = from_binary(&bin).expect("binary parse failed");
602        assert_eq!(parsed, expr);
603    }
604
605    #[test]
606    fn test_binary_roundtrip_not() {
607        let expr = TLExpr::Not(Box::new(TLExpr::Constant(1.0)));
608        let bin = to_binary(&expr);
609        let parsed = from_binary(&bin).expect("binary parse failed");
610        assert_eq!(parsed, expr);
611    }
612
613    #[test]
614    fn test_binary_roundtrip_and() {
615        let expr = TLExpr::And(
616            Box::new(TLExpr::Constant(1.0)),
617            Box::new(TLExpr::Constant(2.0)),
618        );
619        let bin = to_binary(&expr);
620        let parsed = from_binary(&bin).expect("binary parse failed");
621        assert_eq!(parsed, expr);
622    }
623
624    #[test]
625    fn test_binary_roundtrip_nested() {
626        let leaf = simple_pred("leaf", "z");
627        let nested = TLExpr::Imply(
628            Box::new(TLExpr::And(
629                Box::new(TLExpr::Exists {
630                    var: "x".to_string(),
631                    domain: "D".to_string(),
632                    body: Box::new(leaf.clone()),
633                }),
634                Box::new(TLExpr::Not(Box::new(leaf))),
635            )),
636            Box::new(TLExpr::Constant(99.9)),
637        );
638        let bin = to_binary(&nested);
639        let parsed = from_binary(&bin).expect("binary parse failed");
640        assert_eq!(parsed, nested);
641    }
642
643    #[test]
644    fn test_binary_magic_check() {
645        let expr = TLExpr::Constant(1.0);
646        let bin = to_binary(&expr);
647        assert_eq!(&bin[..4], &TLEX_MAGIC);
648    }
649
650    #[test]
651    fn test_binary_invalid_magic() {
652        let data = vec![0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x00, 0x00, 0x00];
653        let result = from_binary(&data);
654        assert!(matches!(result, Err(ExprSerializeError::InvalidMagic)));
655    }
656
657    #[test]
658    fn test_binary_truncated() {
659        let data = vec![0x54, 0x4C, 0x45]; // incomplete magic
660        let result = from_binary(&data);
661        assert!(matches!(result, Err(ExprSerializeError::TruncatedInput)));
662    }
663
664    #[test]
665    fn test_expr_fingerprint_same() {
666        let a = TLExpr::And(
667            Box::new(TLExpr::Constant(1.0)),
668            Box::new(TLExpr::Constant(2.0)),
669        );
670        let b = TLExpr::And(
671            Box::new(TLExpr::Constant(1.0)),
672            Box::new(TLExpr::Constant(2.0)),
673        );
674        assert_eq!(expr_fingerprint(&a), expr_fingerprint(&b));
675    }
676
677    #[test]
678    fn test_expr_fingerprint_different() {
679        let a = TLExpr::Constant(1.0);
680        let b = TLExpr::Constant(2.0);
681        assert_ne!(expr_fingerprint(&a), expr_fingerprint(&b));
682    }
683
684    #[test]
685    fn test_binary_equal_true() {
686        let expr = TLExpr::Or(
687            Box::new(TLExpr::Constant(1.0)),
688            Box::new(TLExpr::Constant(2.0)),
689        );
690        let a = to_binary(&expr);
691        let b = to_binary(&expr);
692        assert!(binary_equal(&a, &b));
693    }
694
695    #[test]
696    fn test_serialization_stats() {
697        let expr = TLExpr::And(
698            Box::new(simple_pred("P", "x")),
699            Box::new(TLExpr::Not(Box::new(simple_pred("Q", "y")))),
700        );
701        let stats = SerializationStats::compute(&expr);
702        assert!(stats.sexpr_bytes > 0);
703        assert!(stats.binary_bytes > 0);
704        assert!(stats.node_count > 0);
705        assert!(stats.max_depth > 0);
706        let summary = stats.summary();
707        assert!(summary.contains("bytes"));
708    }
709
710    #[test]
711    fn test_batch_roundtrip() {
712        let exprs = vec![
713            TLExpr::Constant(1.0),
714            TLExpr::Not(Box::new(TLExpr::Constant(2.0))),
715            TLExpr::And(
716                Box::new(simple_pred("P", "x")),
717                Box::new(simple_pred("Q", "y")),
718            ),
719        ];
720        let bin = batch_to_binary(&exprs);
721        let parsed = batch_from_binary(&bin).expect("batch parse failed");
722        assert_eq!(parsed.len(), exprs.len());
723        for (a, b) in exprs.iter().zip(parsed.iter()) {
724            assert_eq!(a, b);
725        }
726    }
727
728    #[test]
729    fn test_graph_binary_roundtrip() {
730        let mut graph = EinsumGraph::new();
731        let _a = graph.add_tensor("A");
732        let _b = graph.add_tensor("B");
733        let _c = graph.add_tensor("C");
734        graph
735            .add_node(EinsumNode::einsum("ik,kj->ij", vec![0, 1], vec![2]))
736            .expect("add node failed");
737        graph.add_output(2).expect("add output failed");
738
739        let bin = graph_to_binary(&graph);
740        let parsed = graph_from_binary(&bin).expect("graph parse failed");
741        assert_eq!(parsed.tensors, graph.tensors);
742        assert_eq!(parsed.inputs, graph.inputs);
743        assert_eq!(parsed.outputs, graph.outputs);
744        assert_eq!(parsed.nodes.len(), graph.nodes.len());
745    }
746
747    #[test]
748    fn test_sexpr_roundtrip_empty_set() {
749        let expr = TLExpr::EmptySet;
750        let s = to_sexpr(&expr);
751        let parsed = from_sexpr(&s).expect("parse failed");
752        assert_eq!(parsed, expr);
753    }
754
755    #[test]
756    fn test_binary_roundtrip_lambda() {
757        let expr = TLExpr::Lambda {
758            var: "x".to_string(),
759            var_type: Some("Int".to_string()),
760            body: Box::new(TLExpr::Constant(42.0)),
761        };
762        let bin = to_binary(&expr);
763        let parsed = from_binary(&bin).expect("binary parse failed");
764        assert_eq!(parsed, expr);
765    }
766
767    #[test]
768    fn test_binary_roundtrip_fuzzy() {
769        let expr = TLExpr::TNorm {
770            kind: TNormKind::Lukasiewicz,
771            left: Box::new(TLExpr::Constant(0.5)),
772            right: Box::new(TLExpr::Constant(0.7)),
773        };
774        let bin = to_binary(&expr);
775        let parsed = from_binary(&bin).expect("binary parse failed");
776        assert_eq!(parsed, expr);
777    }
778}