Skip to main content

mangle_driver/
lib.rs

1// Copyright 2025 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! The Mangle Driver.
16//!
17//! This crate acts as the orchestrator for the Mangle compiler pipeline.
18//! It connects parsing, analysis, and execution components to provide a
19//! high-level API for running Mangle programs.
20//!
21//! # Execution Architecture
22//!
23//! Mangle supports multiple execution strategies:
24//!
25//! 1.  **Reference Implementation (Legacy)**: A naive bottom-up evaluator that operates directly on the AST.
26//!     This is implemented in the `mangle-engine` crate and serves as a correctness baseline.
27//!     It is not used by this driver.
28//!
29//! 2.  **Interpreter (Default)**: A high-performance interpreter that executes the Mangle Intermediate Representation (IR).
30//!     The driver compiles source code to IR and then executes it using `mangle-interpreter`.
31//!
32//! 3.  **WASM Compilation**: The IR can be compiled to WebAssembly (WASM) for execution in browsers or
33//!     WASM runtimes. This is handled by `mangle-codegen`.
34//!
35//! # Key Responsibilities
36//!
37//! *   **Compilation**: Parsing source code and lowering it to the Intermediate Representation (IR).
38//! *   **Stratification**: Analyzing dependencies between predicates to determine the correct
39//!     evaluation order (handling negation and recursion). This is implemented in [`Program`].
40//! *   **Execution**: Running the compiled plan using the [`mangle_interpreter`].
41//! *   **Codegen**: Generating WASM modules from the IR.
42//!
43//! # Example
44//!
45//! ```rust
46//! use mangle_ast::Arena;
47//! use mangle_driver::{compile, execute};
48//!
49//! let arena = Arena::new_with_global_interner();
50//! let source = "p(1). q(X) :- p(X).";
51//!
52//! // 1. Compile
53//! let (mut ir, stratified) = compile(source, &arena).expect("compilation failed");
54//!
55//! // 2. Execute
56//! let store = Box::new(mangle_interpreter::MemStore::new());
57//! let interpreter = execute(&mut ir, &stratified, store).expect("execution failed");
58//! ```
59
60use anyhow::{Result, anyhow};
61use ast::Arena;
62use fxhash::FxHashSet;
63use mangle_analysis::{LoweringContext, Planner, Program, StratifiedProgram, rewrite_unit};
64use mangle_ast as ast;
65use mangle_codegen::{Codegen, WasmImportsBackend};
66use mangle_interpreter::{Interpreter, Store};
67use mangle_ir::{Inst, InstId, Ir};
68use mangle_parse::Parser;
69
70/// Compiles source code into the Mangle Intermediate Representation (IR).
71///
72/// This function performs:
73/// 1.  Parsing of the source string into an AST.
74/// 2.  **Renaming**: Applies package rewrites to support module namespacing.
75/// 3.  **Stratification**: Orders the evaluation of rules.
76/// 4.  **Lowering**: Converts the AST into the flat IR.
77///
78/// Returns a tuple containing the IR and the stratification info (which dictates
79/// the order of execution).
80pub fn compile<'a>(source: &str, arena: &'a Arena) -> Result<(Ir, StratifiedProgram<'a>)> {
81    compile_units(&[source], arena)
82}
83
84/// Compiles multiple source units into the Mangle Intermediate Representation (IR).
85///
86/// Each source string is parsed into a separate AST unit, renamed independently
87/// (handling Package/Use directives), and then merged into a single unit for
88/// stratification and lowering.
89///
90/// This enables multi-unit compilation where one unit can declare a `Package`
91/// and another can `Use` it with qualified predicate references.
92pub fn compile_units<'a>(sources: &[&str], arena: &'a Arena) -> Result<(Ir, StratifiedProgram<'a>)> {
93    // Parse and rename each source unit independently
94    let mut all_decls: Vec<&'a ast::Decl<'a>> = Vec::new();
95    let mut all_clauses: Vec<&'a ast::Clause<'a>> = Vec::new();
96
97    for (i, source) in sources.iter().enumerate() {
98        let label = format!("source_{}", i);
99        let mut parser = Parser::new(arena, source.as_bytes(), arena.alloc_str(&label));
100        parser.next_token().map_err(|e| anyhow!(e))?;
101        let unit = parser.parse_unit()?;
102
103        let rewritten = rewrite_unit(arena, unit);
104        all_decls.extend_from_slice(rewritten.decls);
105        all_clauses.extend_from_slice(rewritten.clauses);
106    }
107
108    // Build the merged unit
109    let merged_unit = ast::Unit {
110        decls: arena.alloc_slice_copy(&all_decls),
111        clauses: arena.alloc_slice_copy(&all_clauses),
112    };
113    let unit = &merged_unit;
114
115    let mut program = Program::new(arena);
116    let mut all_preds = FxHashSet::default();
117    let mut idb_preds = FxHashSet::default();
118
119    for clause in unit.clauses {
120        program.add_clause(arena, clause);
121        idb_preds.insert(clause.head.sym);
122        all_preds.insert(clause.head.sym);
123        for premise in clause.premises {
124            match premise {
125                ast::Term::Atom(atom) => { all_preds.insert(atom.sym); }
126                ast::Term::NegAtom(atom) => { all_preds.insert(atom.sym); }
127                ast::Term::TemporalAtom(atom, _) => { all_preds.insert(atom.sym); }
128                _ => {}
129            }
130        }
131    }
132
133    for pred in all_preds {
134        if !idb_preds.contains(&pred) {
135            program.ext_preds.push(pred);
136        }
137    }
138
139    let stratified = program.stratify().map_err(|e| anyhow!(e))?;
140
141    let ctx = LoweringContext::new(arena);
142    let ir = ctx.lower_unit(unit);
143
144    Ok((ir, stratified))
145}
146
147/// Compiles the Intermediate Representation (IR) into a WebAssembly (WASM) module.
148///
149/// This uses the default `WasmImportsBackend` which expects certain host functions
150/// to be available for data access. Returns a `CompiledModule` containing the WASM
151/// bytecode and string/name tables needed by the host runtime.
152pub fn compile_to_wasm(
153    ir: &mut Ir,
154    stratified: &StratifiedProgram,
155) -> mangle_codegen::CompiledModule {
156    let mut codegen = Codegen::new_with_stratified(ir, stratified, WasmImportsBackend);
157    codegen.generate()
158}
159
160/// Executes a compiled Mangle program using the pure Rust interpreter.
161///
162/// This function:
163/// 1.  Iterates through the strata defined in `StratifiedProgram`.
164/// 2.  Identifies recursive predicates within each stratum.
165/// 3.  Executes non-recursive strata once.
166/// 4.  Executes recursive strata using a semi-naive evaluation loop.
167///
168/// Returns the `Interpreter` instance, which holds the final state (facts) of the execution.
169pub fn execute<'a>(
170    ir: &'a mut Ir,
171    stratified: &StratifiedProgram<'a>,
172    store: Box<dyn Store + 'a>,
173) -> Result<Interpreter<'a>> {
174    let arena = stratified.arena();
175
176    // 1. Pre-plan everything that needs mutable access to IR
177    let mut strata_plans = Vec::new();
178
179    for stratum in stratified.strata() {
180        let mut stratum_pred_names = FxHashSet::default();
181        for pred in &stratum {
182            if let Some(name) = arena.predicate_name(*pred) {
183                stratum_pred_names.insert(name);
184            }
185        }
186
187        // Identify rules for this stratum
188        let mut rule_ids = Vec::new();
189        for (i, inst) in ir.insts.iter().enumerate() {
190            if let Inst::Rule { head, .. } = inst
191                && let Inst::Atom { predicate, .. } = ir.get(*head)
192            {
193                let head_name = ir.resolve_name(*predicate);
194                if stratum_pred_names.contains(head_name) {
195                    rule_ids.push(InstId::new(i));
196                }
197            }
198        }
199
200        if rule_ids.is_empty() {
201            strata_plans.push(None);
202            continue;
203        }
204
205        // Check if any rule in the stratum is recursive
206        let mut is_recursive = false;
207        for &rule_id in &rule_ids {
208            if let Inst::Rule { premises, .. } = ir.get(rule_id) {
209                for &premise in premises {
210                    if let Inst::Atom { predicate, .. } = ir.get(premise) {
211                        let pred_name = ir.resolve_name(*predicate);
212                        if stratum_pred_names.contains(pred_name) {
213                            is_recursive = true;
214                            break;
215                        }
216                    }
217                }
218            }
219            if is_recursive {
220                break;
221            }
222        }
223
224        if !is_recursive {
225            let mut ops = Vec::new();
226            for rule_id in rule_ids {
227                let planner = Planner::new(ir);
228                ops.push(planner.plan_rule(rule_id)?);
229            }
230            strata_plans.push(Some(StratumPlan::NonRecursive(ops)));
231        } else {
232            let mut initial_ops = Vec::new();
233            for &rule_id in &rule_ids {
234                let planner = Planner::new(ir);
235                initial_ops.push(planner.plan_rule(rule_id)?);
236            }
237
238            let mut delta_plans = Vec::new();
239            for &rule_id in &rule_ids {
240                let premises = if let Inst::Rule { premises, .. } = ir.get(rule_id) {
241                    premises.clone()
242                } else {
243                    continue;
244                };
245
246                for &premise in &premises {
247                    let (predicate, pred_name) =
248                        if let Inst::Atom { predicate, .. } = ir.get(premise) {
249                            (*predicate, ir.resolve_name(*predicate).to_string())
250                        } else {
251                            continue;
252                        };
253
254                    if stratum_pred_names.contains(pred_name.as_str()) {
255                        let planner = Planner::new(ir).with_delta(predicate);
256                        delta_plans.push(planner.plan_rule(rule_id)?);
257                    }
258                }
259            }
260            strata_plans.push(Some(StratumPlan::Recursive {
261                initial_ops,
262                delta_plans,
263            }));
264        }
265    }
266
267    // Collect temporal predicate names for coalescing
268    let temporal_pred_names: Vec<String> = ir
269        .temporal_predicates
270        .iter()
271        .map(|name_id| ir.resolve_name(*name_id).to_string())
272        .collect();
273
274    // 2. Now execute using the interpreter
275    let mut interpreter = Interpreter::new(ir, store);
276
277    // Initialize EDB relations
278    for pred in stratified.extensional_preds() {
279        if let Some(name) = arena.predicate_name(pred) {
280            interpreter.store_mut().create_relation(name);
281        }
282    }
283
284    for plan in strata_plans {
285        match plan {
286            Some(StratumPlan::NonRecursive(ops)) => {
287                for op in ops {
288                    interpreter.execute(&op)?;
289                }
290            }
291            Some(StratumPlan::Recursive {
292                initial_ops,
293                delta_plans,
294            }) => {
295                for op in initial_ops {
296                    interpreter.execute(&op)?;
297                }
298                interpreter.store_mut().merge_deltas();
299
300                loop {
301                    let mut changes = 0;
302                    for op in &delta_plans {
303                        changes += interpreter.execute(op)?;
304                    }
305                    if changes == 0 {
306                        break;
307                    }
308                    interpreter.store_mut().merge_deltas();
309                }
310                // Coalesce temporal intervals after fixpoint converges
311                for name in &temporal_pred_names {
312                    interpreter.store_mut().coalesce_temporal(name);
313                }
314            }
315            None => {}
316        }
317        interpreter.store_mut().merge_deltas();
318        for name in &temporal_pred_names {
319            interpreter.store_mut().coalesce_temporal(name);
320        }
321    }
322
323    Ok(interpreter)
324}
325
326enum StratumPlan {
327    NonRecursive(Vec<mangle_ir::physical::Op>),
328    Recursive {
329        initial_ops: Vec<mangle_ir::physical::Op>,
330        delta_plans: Vec<mangle_ir::physical::Op>,
331    },
332}
333
334#[cfg(test)]
335mod tests {
336    use super::*;
337    use mangle_interpreter::{MemStore, Value};
338
339    #[test]
340    fn test_driver_e2e() -> Result<()> {
341        let arena = Arena::new_with_global_interner();
342        let source = r#"
343            p(1).
344            p(2).
345            q(X) :- p(X).
346        "#;
347
348        let (mut ir, stratified) = compile(source, &arena)?;
349        let store = Box::new(MemStore::new());
350        let interpreter = execute(&mut ir, &stratified, store)?;
351
352        // Check results
353        let facts: Vec<_> = interpreter
354            .store()
355            .scan("q")
356            .expect("relation q not found")
357            .collect();
358        assert!(!facts.is_empty(), "relation q not found");
359
360        let mut values: Vec<i64> = facts
361            .iter()
362            .map(|t| match t[0] {
363                Value::Number(n) => n,
364                _ => panic!("expected number"),
365            })
366            .collect();
367        values.sort();
368
369        assert_eq!(values, vec![1, 2]);
370
371        Ok(())
372    }
373
374    #[test]
375    fn test_driver_e2e_with_package() -> Result<()> {
376        let arena = Arena::new_with_global_interner();
377        let source = r#"
378            Package pkg!
379            p(1).
380            q(X) :- p(X).
381        "#;
382
383        let (mut ir, stratified) = compile(source, &arena)?;
384        let store = Box::new(MemStore::new());
385        let interpreter = execute(&mut ir, &stratified, store)?;
386
387        // Check results - predicates should be prefixed with "pkg."
388        let facts: Vec<_> = interpreter
389            .store()
390            .scan("pkg.q")
391            .expect("relation pkg.q not found")
392            .collect();
393        assert!(!facts.is_empty(), "relation pkg.q not found");
394
395        let values: Vec<i64> = facts
396            .iter()
397            .map(|t| match t[0] {
398                Value::Number(n) => n,
399                _ => panic!("expected number"),
400            })
401            .collect();
402        assert_eq!(values, vec![1]);
403
404        Ok(())
405    }
406
407    #[test]
408    fn test_driver_let_transform() -> Result<()> {
409        let arena = Arena::new_with_global_interner();
410        let source = r#"
411            p(1).
412            p(2).
413            q(Y) :- p(X) |> let Y = fn:plus(X, 10).
414        "#;
415
416        let (mut ir, stratified) = compile(source, &arena)?;
417        let store = Box::new(MemStore::new());
418        let interpreter = execute(&mut ir, &stratified, store)?;
419
420        let facts: Vec<_> = interpreter
421            .store()
422            .scan("q")
423            .expect("relation q not found")
424            .collect();
425        let mut values: Vec<i64> = facts
426            .iter()
427            .map(|t| match t[0] {
428                Value::Number(n) => n,
429                _ => panic!("expected number"),
430            })
431            .collect();
432        values.sort();
433
434        assert_eq!(values, vec![11, 12]);
435        Ok(())
436    }
437
438    #[test]
439    fn test_driver_aggregation() -> Result<()> {
440        let arena = Arena::new_with_global_interner();
441        let source = r#"
442            p(1, 10).
443            p(1, 20).
444            p(2, 30).
445            q(K, S) :- p(K, V) |> do fn:group_by(K); let S = fn:sum(V).
446        "#;
447
448        let (mut ir, stratified) = compile(source, &arena)?;
449        let store = Box::new(MemStore::new());
450        let interpreter = execute(&mut ir, &stratified, store)?;
451
452        let facts: Vec<_> = interpreter
453            .store()
454            .scan("q")
455            .expect("relation q not found")
456            .collect();
457        let mut results: Vec<(i64, i64)> = facts
458            .iter()
459            .map(|t| {
460                if let (Value::Number(k), Value::Number(s)) = (&t[0], &t[1]) {
461                    (*k, *s)
462                } else {
463                    panic!("expected numbers");
464                }
465            })
466            .collect();
467        results.sort();
468
469        assert_eq!(results, vec![(1, 30), (2, 30)]);
470        Ok(())
471    }
472
473    #[test]
474    fn test_driver_aggregation_count() -> Result<()> {
475        let arena = Arena::new_with_global_interner();
476        let source = r#"
477            p(1, 10).
478            p(1, 20).
479            p(2, 30).
480            q(K, C) :- p(K, V) |> do fn:group_by(K); let C = fn:count(V).
481        "#;
482
483        let (mut ir, stratified) = compile(source, &arena)?;
484        let store = Box::new(MemStore::new());
485        let interpreter = execute(&mut ir, &stratified, store)?;
486
487        let facts: Vec<_> = interpreter
488            .store()
489            .scan("q")
490            .expect("relation q not found")
491            .collect();
492        let mut results: Vec<(i64, i64)> = facts
493            .iter()
494            .map(|t| {
495                if let (Value::Number(k), Value::Number(c)) = (&t[0], &t[1]) {
496                    (*k, *c)
497                } else {
498                    panic!("expected numbers");
499                }
500            })
501            .collect();
502        results.sort();
503
504        assert_eq!(results, vec![(1, 2), (2, 1)]);
505        Ok(())
506    }
507
508    #[test]
509    fn test_driver_reachability() -> Result<()> {
510        let arena = Arena::new_with_global_interner();
511        let source = r#"
512            edge(1, 2).
513            edge(2, 3).
514            edge(3, 4).
515            edge(4, 5).
516            reachable(X, Y) :- edge(X, Y).
517            reachable(X, Z) :- reachable(X, Y), edge(Y, Z).
518        "#;
519
520        let (mut ir, stratified) = compile(source, &arena)?;
521        let store = Box::new(MemStore::new());
522        let interpreter = execute(&mut ir, &stratified, store)?;
523
524        let facts: Vec<_> = interpreter
525            .store()
526            .scan("reachable")
527            .expect("reachable relation not found")
528            .collect();
529        assert_eq!(facts.len(), 10); // (1,2),(1,3),(1,4),(1,5), (2,3),(2,4),(2,5), (3,4),(3,5), (4,5)
530
531        let mut reachable_from_1: Vec<i64> = facts
532            .iter()
533            .filter(|t| t[0] == Value::Number(1))
534            .map(|t| match t[1] {
535                Value::Number(n) => n,
536                _ => panic!("expected number"),
537            })
538            .collect();
539        reachable_from_1.sort();
540        assert_eq!(reachable_from_1, vec![2, 3, 4, 5]);
541
542        Ok(())
543    }
544
545    #[test]
546    fn test_name_constants() -> Result<()> {
547        let arena = Arena::new_with_global_interner();
548        let source = r#"
549            role(/role/admin).
550            role(/role/user).
551            role(/role/application).
552        "#;
553
554        let (mut ir, stratified) = compile(source, &arena)?;
555        let store = Box::new(MemStore::new());
556        let interpreter = execute(&mut ir, &stratified, store)?;
557
558        let facts: Vec<_> = interpreter
559            .store()
560            .scan("role")
561            .expect("relation role not found")
562            .collect();
563        assert_eq!(facts.len(), 3);
564
565        let mut names: Vec<String> = facts
566            .iter()
567            .map(|t| match &t[0] {
568                Value::String(s) => s.clone(),
569                _ => panic!("expected string"),
570            })
571            .collect();
572        names.sort();
573        assert_eq!(
574            names,
575            vec!["/role/admin", "/role/application", "/role/user"]
576        );
577
578        Ok(())
579    }
580
581    #[test]
582    fn test_inequality() -> Result<()> {
583        let arena = Arena::new_with_global_interner();
584        // Note: name constants like /role/application cannot appear immediately
585        // before `.` because the scanner treats `.` as a name_char. Use string
586        // constants or ensure a `)` separates the name from the clause terminator.
587        let source = r#"
588            role("admin").
589            role("user").
590            role("application").
591            non_app_role(R) :- role(R), R != "application".
592        "#;
593
594        let (mut ir, stratified) = compile(source, &arena)?;
595        let store = Box::new(MemStore::new());
596        let interpreter = execute(&mut ir, &stratified, store)?;
597
598        let facts: Vec<_> = interpreter
599            .store()
600            .scan("non_app_role")
601            .expect("relation non_app_role not found")
602            .collect();
603        assert_eq!(facts.len(), 2);
604
605        let mut names: Vec<String> = facts
606            .iter()
607            .map(|t| match &t[0] {
608                Value::String(s) => s.clone(),
609                _ => panic!("expected string"),
610            })
611            .collect();
612        names.sort();
613        assert_eq!(names, vec!["admin", "user"]);
614
615        Ok(())
616    }
617
618    #[test]
619    fn test_negation() -> Result<()> {
620        let arena = Arena::new_with_global_interner();
621        let source = r#"
622            service("web").
623            service("api").
624            service("db").
625            has_dep("web").
626            has_dep("api").
627            no_dep(S) :- service(S), !has_dep(S).
628        "#;
629
630        let (mut ir, stratified) = compile(source, &arena)?;
631        let store = Box::new(MemStore::new());
632        let interpreter = execute(&mut ir, &stratified, store)?;
633
634        let facts: Vec<_> = interpreter
635            .store()
636            .scan("no_dep")
637            .expect("relation no_dep not found")
638            .collect();
639        assert_eq!(facts.len(), 1);
640        assert_eq!(facts[0][0], Value::String("db".to_string()));
641
642        Ok(())
643    }
644
645    #[test]
646    fn test_combined_name_ineq_negation() -> Result<()> {
647        // Mini devops-like program exercising all features together
648        let arena = Arena::new_with_global_interner();
649        let source = r#"
650            container("web", /status/running).
651            container("api", /status/running).
652            container("db", /status/stopped).
653            depends_on("web", "db").
654            depends_on("api", "db").
655
656            running(Name) :- container(Name, /status/running).
657            stopped(Name) :- container(Name, /status/stopped).
658            has_running_dep(Name) :- depends_on(Name, Dep), running(Dep).
659            needs_attention(Name) :- depends_on(Name, Dep), stopped(Dep).
660            independent(Name) :- running(Name), !has_running_dep(Name).
661        "#;
662
663        let (mut ir, stratified) = compile(source, &arena)?;
664        let store = Box::new(MemStore::new());
665        let interpreter = execute(&mut ir, &stratified, store)?;
666
667        // Check running containers
668        let running: Vec<_> = interpreter
669            .store()
670            .scan("running")
671            .expect("relation running not found")
672            .collect();
673        assert_eq!(running.len(), 2);
674
675        // Check stopped
676        let stopped: Vec<_> = interpreter
677            .store()
678            .scan("stopped")
679            .expect("relation stopped not found")
680            .collect();
681        assert_eq!(stopped.len(), 1);
682        assert_eq!(stopped[0][0], Value::String("db".to_string()));
683
684        // Both web and api depend on db which is stopped
685        let needs_attention: Vec<_> = interpreter
686            .store()
687            .scan("needs_attention")
688            .expect("relation needs_attention not found")
689            .collect();
690        assert_eq!(needs_attention.len(), 2);
691
692        // db is not running so nobody has a running dep
693        // Both web and api are running and have no running deps
694        let independent: Vec<_> = interpreter
695            .store()
696            .scan("independent")
697            .expect("relation independent not found")
698            .collect();
699        assert_eq!(independent.len(), 2);
700
701        Ok(())
702    }
703
704    #[test]
705    fn test_join_with_constants_in_second_atom() -> Result<()> {
706        // Regression: fresh_var used ir.insts.len() as counter, producing
707        // duplicate NameIds for scan variables. This caused the second body
708        // atom's columns to overwrite each other during IndexLookup execution.
709        let arena = Arena::new_with_global_interner();
710        let source = r#"
711            p("a", "x").
712            q("a", "y").
713            test(E) :- p(E, "x"), q(E, "y").
714        "#;
715
716        let (mut ir, stratified) = compile(source, &arena)?;
717        let store = Box::new(MemStore::new());
718        let interpreter = execute(&mut ir, &stratified, store)?;
719
720        let facts: Vec<_> = interpreter
721            .store()
722            .scan("test")
723            .expect("relation test not found")
724            .collect();
725
726        assert_eq!(facts.len(), 1, "expected 1 result, got {:?}", facts);
727        assert_eq!(facts[0][0], Value::String("a".to_string()));
728
729        Ok(())
730    }
731
732    #[test]
733    fn test_join_constant_only_in_second_atom() -> Result<()> {
734        // Simpler variant: constant only in second atom
735        let arena = Arena::new_with_global_interner();
736        let source = r#"
737            p("a", "x").
738            q("a", "y").
739            test(E, V) :- p(E, V), q(E, "y").
740        "#;
741
742        let (mut ir, stratified) = compile(source, &arena)?;
743        let store = Box::new(MemStore::new());
744        let interpreter = execute(&mut ir, &stratified, store)?;
745
746        let facts: Vec<_> = interpreter
747            .store()
748            .scan("test")
749            .expect("relation test not found")
750            .collect();
751
752        assert_eq!(facts.len(), 1, "expected 1 result, got {:?}", facts);
753        assert_eq!(facts[0][0], Value::String("a".to_string()));
754        assert_eq!(facts[0][1], Value::String("x".to_string()));
755
756        Ok(())
757    }
758
759    #[test]
760    fn test_compile_units_package_use() -> Result<()> {
761        let arena = Arena::new_with_global_interner();
762
763        let schema = r#"
764            Package config_schema !
765            Decl server_port(Port).
766            Decl programs_dir(Path).
767        "#;
768
769        let config = r#"
770            Use config_schema !
771            config_schema.server_port(8090).
772            config_schema.programs_dir("/programs").
773        "#;
774
775        let (mut ir, stratified) = compile_units(&[schema, config], &arena)?;
776        let store = Box::new(MemStore::new());
777        let interpreter = execute(&mut ir, &stratified, store)?;
778
779        // Query the qualified predicate
780        let port_facts: Vec<_> = interpreter
781            .store()
782            .scan("config_schema.server_port")
783            .expect("relation config_schema.server_port not found")
784            .collect();
785        assert_eq!(port_facts.len(), 1);
786        assert_eq!(port_facts[0][0], Value::Number(8090));
787
788        let dir_facts: Vec<_> = interpreter
789            .store()
790            .scan("config_schema.programs_dir")
791            .expect("relation config_schema.programs_dir not found")
792            .collect();
793        assert_eq!(dir_facts.len(), 1);
794        assert_eq!(dir_facts[0][0], Value::String("/programs".to_string()));
795
796        Ok(())
797    }
798
799    #[test]
800    fn test_less_than_comparison() -> Result<()> {
801        let arena = Arena::new_with_global_interner();
802        let source = r#"
803            num(8.1). num(42). num(99.5).
804            big(X) :- num(X), 85 < X .
805        "#;
806
807        let (mut ir, stratified) = compile(source, &arena)?;
808        let store = Box::new(MemStore::new());
809        let interpreter = execute(&mut ir, &stratified, store)?;
810
811        let facts: Vec<_> = interpreter
812            .store()
813            .scan("big")
814            .expect("relation big not found")
815            .collect();
816        assert_eq!(facts.len(), 1, "expected 1 result, got {:?}", facts);
817        assert_eq!(facts[0][0], Value::Float(99.5));
818
819        Ok(())
820    }
821
822    #[test]
823    fn test_less_equal_comparison() -> Result<()> {
824        let arena = Arena::new_with_global_interner();
825        let source = r#"
826            num(10). num(50). num(85). num(99).
827            up_to_85(X) :- num(X), X <= 85 .
828        "#;
829
830        let (mut ir, stratified) = compile(source, &arena)?;
831        let store = Box::new(MemStore::new());
832        let interpreter = execute(&mut ir, &stratified, store)?;
833
834        let facts: Vec<_> = interpreter
835            .store()
836            .scan("up_to_85")
837            .expect("relation up_to_85 not found")
838            .collect();
839        assert_eq!(facts.len(), 3, "expected 3 results, got {:?}", facts);
840
841        Ok(())
842    }
843
844    #[test]
845    fn test_compile_to_wasm() -> Result<()> {
846        let arena = Arena::new_with_global_interner();
847        let source = r#"
848            p(1).
849            q(X) :- p(X).
850        "#;
851
852        let (mut ir, stratified) = compile(source, &arena)?;
853        let compiled = compile_to_wasm(&mut ir, &stratified);
854
855        // Basic check that we generated something that looks like WASM
856        assert!(!compiled.wasm.is_empty());
857        assert_eq!(&compiled.wasm[0..4], b"\0asm"); // WASM magic header
858
859        Ok(())
860    }
861
862    #[test]
863    fn test_greater_than_comparison() -> Result<()> {
864        let arena = Arena::new_with_global_interner();
865        let source = r#"
866            num(10). num(50). num(85). num(99).
867            big(X) :- num(X), X > 50 .
868        "#;
869
870        let (mut ir, stratified) = compile(source, &arena)?;
871        let store = Box::new(MemStore::new());
872        let interpreter = execute(&mut ir, &stratified, store)?;
873
874        let facts: Vec<_> = interpreter
875            .store()
876            .scan("big")
877            .expect("relation big not found")
878            .collect();
879        assert_eq!(facts.len(), 2, "expected 2 results, got {:?}", facts);
880
881        let mut values: Vec<i64> = facts
882            .iter()
883            .map(|t| match t[0] {
884                Value::Number(n) => n,
885                _ => panic!("expected number"),
886            })
887            .collect();
888        values.sort();
889        assert_eq!(values, vec![85, 99]);
890
891        Ok(())
892    }
893
894    #[test]
895    fn test_greater_equal_comparison() -> Result<()> {
896        let arena = Arena::new_with_global_interner();
897        let source = r#"
898            num(10). num(50). num(85). num(99).
899            at_least_85(X) :- num(X), X >= 85 .
900        "#;
901
902        let (mut ir, stratified) = compile(source, &arena)?;
903        let store = Box::new(MemStore::new());
904        let interpreter = execute(&mut ir, &stratified, store)?;
905
906        let facts: Vec<_> = interpreter
907            .store()
908            .scan("at_least_85")
909            .expect("relation at_least_85 not found")
910            .collect();
911        assert_eq!(facts.len(), 2, "expected 2 results, got {:?}", facts);
912
913        Ok(())
914    }
915
916    #[test]
917    fn test_variadic_arithmetic() -> Result<()> {
918        let arena = Arena::new_with_global_interner();
919        let source = r#"
920            p(1).
921            p(2).
922            q(Y) :- p(X) |> let Y = fn:plus(X, 10, 100).
923        "#;
924
925        let (mut ir, stratified) = compile(source, &arena)?;
926        let store = Box::new(MemStore::new());
927        let interpreter = execute(&mut ir, &stratified, store)?;
928
929        let facts: Vec<_> = interpreter
930            .store()
931            .scan("q")
932            .expect("relation q not found")
933            .collect();
934        let mut values: Vec<i64> = facts
935            .iter()
936            .map(|t| match t[0] {
937                Value::Number(n) => n,
938                _ => panic!("expected number"),
939            })
940            .collect();
941        values.sort();
942        assert_eq!(values, vec![111, 112]);
943        Ok(())
944    }
945
946    #[test]
947    fn test_unary_minus() -> Result<()> {
948        let arena = Arena::new_with_global_interner();
949        let source = r#"
950            p(5).
951            q(Y) :- p(X) |> let Y = fn:minus(X).
952        "#;
953
954        let (mut ir, stratified) = compile(source, &arena)?;
955        let store = Box::new(MemStore::new());
956        let interpreter = execute(&mut ir, &stratified, store)?;
957
958        let facts: Vec<_> = interpreter
959            .store()
960            .scan("q")
961            .expect("relation q not found")
962            .collect();
963        assert_eq!(facts.len(), 1);
964        assert_eq!(facts[0][0], Value::Number(-5));
965        Ok(())
966    }
967
968    #[test]
969    fn test_string_concat() -> Result<()> {
970        let arena = Arena::new_with_global_interner();
971        let source = r#"
972            p("hello", "world").
973            q(R) :- p(A, B) |> let R = fn:string:concat(A, " ", B).
974        "#;
975
976        let (mut ir, stratified) = compile(source, &arena)?;
977        let store = Box::new(MemStore::new());
978        let interpreter = execute(&mut ir, &stratified, store)?;
979
980        let facts: Vec<_> = interpreter
981            .store()
982            .scan("q")
983            .expect("relation q not found")
984            .collect();
985        assert_eq!(facts.len(), 1);
986        assert_eq!(facts[0][0], Value::String("hello world".to_string()));
987        Ok(())
988    }
989
990    #[test]
991    fn test_string_replace() -> Result<()> {
992        let arena = Arena::new_with_global_interner();
993        let source = r#"
994            p("foo-bar-baz").
995            q(R) :- p(S) |> let R = fn:string:replace(S, "-", "_", -1).
996        "#;
997
998        let (mut ir, stratified) = compile(source, &arena)?;
999        let store = Box::new(MemStore::new());
1000        let interpreter = execute(&mut ir, &stratified, store)?;
1001
1002        let facts: Vec<_> = interpreter
1003            .store()
1004            .scan("q")
1005            .expect("relation q not found")
1006            .collect();
1007        assert_eq!(facts.len(), 1);
1008        assert_eq!(facts[0][0], Value::String("foo_bar_baz".to_string()));
1009        Ok(())
1010    }
1011
1012    #[test]
1013    fn test_number_to_string() -> Result<()> {
1014        let arena = Arena::new_with_global_interner();
1015        let source = r#"
1016            p(42).
1017            q(R) :- p(X) |> let R = fn:number:to_string(X).
1018        "#;
1019
1020        let (mut ir, stratified) = compile(source, &arena)?;
1021        let store = Box::new(MemStore::new());
1022        let interpreter = execute(&mut ir, &stratified, store)?;
1023
1024        let facts: Vec<_> = interpreter
1025            .store()
1026            .scan("q")
1027            .expect("relation q not found")
1028            .collect();
1029        assert_eq!(facts.len(), 1);
1030        assert_eq!(facts[0][0], Value::String("42".to_string()));
1031        Ok(())
1032    }
1033
1034    #[test]
1035    fn test_float64_to_string() -> Result<()> {
1036        let arena = Arena::new_with_global_interner();
1037        let source = r#"
1038            p(3.14).
1039            q(R) :- p(X) |> let R = fn:float64:to_string(X).
1040        "#;
1041
1042        let (mut ir, stratified) = compile(source, &arena)?;
1043        let store = Box::new(MemStore::new());
1044        let interpreter = execute(&mut ir, &stratified, store)?;
1045
1046        let facts: Vec<_> = interpreter
1047            .store()
1048            .scan("q")
1049            .expect("relation q not found")
1050            .collect();
1051        assert_eq!(facts.len(), 1);
1052        assert_eq!(facts[0][0], Value::String("3.14".to_string()));
1053        Ok(())
1054    }
1055
1056    #[test]
1057    fn test_float_promotion_in_sqrt() -> Result<()> {
1058        let arena = Arena::new_with_global_interner();
1059        let source = r#"
1060            p(16).
1061            q(R) :- p(X) |> let R = fn:sqrt(X).
1062        "#;
1063
1064        let (mut ir, stratified) = compile(source, &arena)?;
1065        let store = Box::new(MemStore::new());
1066        let interpreter = execute(&mut ir, &stratified, store)?;
1067
1068        let facts: Vec<_> = interpreter
1069            .store()
1070            .scan("q")
1071            .expect("relation q not found")
1072            .collect();
1073        assert_eq!(facts.len(), 1);
1074        assert_eq!(facts[0][0], Value::Float(4.0));
1075        Ok(())
1076    }
1077
1078    #[test]
1079    fn test_negative_number_literals() -> Result<()> {
1080        let arena = Arena::new_with_global_interner();
1081        let source = r#"
1082            temp(-10).
1083            temp(5).
1084            temp(20).
1085            below_zero(X) :- temp(X), X < 0 .
1086            offset(X, Y) :- temp(X) |> let Y = fn:float:plus(X, -0.5).
1087        "#;
1088
1089        let (mut ir, stratified) = compile(source, &arena)?;
1090        let store = Box::new(MemStore::new());
1091        let interpreter = execute(&mut ir, &stratified, store)?;
1092
1093        let facts: Vec<_> = interpreter
1094            .store()
1095            .scan("below_zero")
1096            .expect("relation below_zero not found")
1097            .collect();
1098        assert_eq!(facts.len(), 1);
1099        assert_eq!(facts[0][0], Value::Number(-10));
1100
1101        let offset_facts: Vec<_> = interpreter
1102            .store()
1103            .scan("offset")
1104            .expect("relation offset not found")
1105            .collect();
1106        assert_eq!(offset_facts.len(), 3);
1107
1108        Ok(())
1109    }
1110
1111    #[test]
1112    fn test_builtin_string_predicates() -> Result<()> {
1113        let arena = Arena::new_with_global_interner();
1114        let source = r#"
1115            path("/api/users").
1116            path("/api/posts").
1117            path("/home").
1118            path("/api/users/admin").
1119            api_path(P) :- path(P), :string:starts_with(P, "/api").
1120            users_path(P) :- path(P), :string:contains(P, "users").
1121            html_path(P) :- path(P), :string:ends_with(P, "admin").
1122        "#;
1123
1124        let (mut ir, stratified) = compile(source, &arena)?;
1125        let store = Box::new(MemStore::new());
1126        let interpreter = execute(&mut ir, &stratified, store)?;
1127
1128        let api_facts: Vec<_> = interpreter
1129            .store()
1130            .scan("api_path")
1131            .expect("relation api_path not found")
1132            .collect();
1133        assert_eq!(api_facts.len(), 3, "api_path: {:?}", api_facts);
1134
1135        let users_facts: Vec<_> = interpreter
1136            .store()
1137            .scan("users_path")
1138            .expect("relation users_path not found")
1139            .collect();
1140        assert_eq!(users_facts.len(), 2, "users_path: {:?}", users_facts);
1141
1142        let html_facts: Vec<_> = interpreter
1143            .store()
1144            .scan("html_path")
1145            .expect("relation html_path not found")
1146            .collect();
1147        assert_eq!(html_facts.len(), 1, "html_path: {:?}", html_facts);
1148
1149        Ok(())
1150    }
1151
1152    #[test]
1153    fn test_match_prefix() -> Result<()> {
1154        let arena = Arena::new_with_global_interner();
1155        let source = r#"
1156            name("/role/admin").
1157            name("/role").
1158            name("/other").
1159            under_role(N) :- name(N), :match_prefix(N, "/role").
1160        "#;
1161
1162        let (mut ir, stratified) = compile(source, &arena)?;
1163        let store = Box::new(MemStore::new());
1164        let interpreter = execute(&mut ir, &stratified, store)?;
1165
1166        let facts: Vec<_> = interpreter
1167            .store()
1168            .scan("under_role")
1169            .expect("relation under_role not found")
1170            .collect();
1171        // "/role" itself should NOT match (must be strictly longer)
1172        assert_eq!(facts.len(), 1, "under_role: {:?}", facts);
1173        assert_eq!(facts[0][0], Value::String("/role/admin".to_string()));
1174
1175        Ok(())
1176    }
1177
1178    #[test]
1179    fn test_timestamp_literals() -> Result<()> {
1180        let arena = Arena::new_with_global_interner();
1181        let source = r#"
1182            event(2024-01-15T10:30:00Z).
1183            event(2024-06-01T00:00:00Z).
1184            has_event(X) :- event(X).
1185        "#;
1186
1187        let (mut ir, stratified) = compile(source, &arena)?;
1188        let store = Box::new(MemStore::new());
1189        let interpreter = execute(&mut ir, &stratified, store)?;
1190
1191        let facts: Vec<_> = interpreter
1192            .store()
1193            .scan("has_event")
1194            .expect("relation has_event not found")
1195            .collect();
1196        assert_eq!(facts.len(), 2);
1197        // Both should be Time values
1198        for fact in &facts {
1199            assert!(matches!(fact[0], Value::Time(_)), "expected Time, got {:?}", fact[0]);
1200        }
1201        Ok(())
1202    }
1203
1204    #[test]
1205    fn test_duration_literals() -> Result<()> {
1206        let arena = Arena::new_with_global_interner();
1207        let source = r#"
1208            timeout(30s).
1209            timeout(500ms).
1210            has_timeout(X) :- timeout(X).
1211        "#;
1212
1213        let (mut ir, stratified) = compile(source, &arena)?;
1214        let store = Box::new(MemStore::new());
1215        let interpreter = execute(&mut ir, &stratified, store)?;
1216
1217        let facts: Vec<_> = interpreter
1218            .store()
1219            .scan("has_timeout")
1220            .expect("relation has_timeout not found")
1221            .collect();
1222        assert_eq!(facts.len(), 2);
1223        for fact in &facts {
1224            assert!(matches!(fact[0], Value::Duration(_)), "expected Duration, got {:?}", fact[0]);
1225        }
1226        Ok(())
1227    }
1228
1229    #[test]
1230    fn test_time_arithmetic() -> Result<()> {
1231        let arena = Arena::new_with_global_interner();
1232        let source = r#"
1233            start(2024-01-15T10:00:00Z).
1234            result(Y) :- start(X) |> let Y = fn:time:add(X, 1h).
1235        "#;
1236
1237        let (mut ir, stratified) = compile(source, &arena)?;
1238        let store = Box::new(MemStore::new());
1239        let interpreter = execute(&mut ir, &stratified, store)?;
1240
1241        let facts: Vec<_> = interpreter
1242            .store()
1243            .scan("result")
1244            .expect("relation result not found")
1245            .collect();
1246        assert_eq!(facts.len(), 1);
1247        // 2024-01-15T10:00:00Z + 1h = 2024-01-15T11:00:00Z
1248        match &facts[0][0] {
1249            Value::Time(nanos) => {
1250                let expected = 1705276800_000_000_000i64 + (11 * 3600) * 1_000_000_000;
1251                assert_eq!(*nanos, expected, "time should be 2024-01-15T11:00:00Z");
1252            }
1253            v => panic!("expected Time, got {v:?}"),
1254        }
1255        Ok(())
1256    }
1257
1258    #[test]
1259    fn test_time_sub() -> Result<()> {
1260        let arena = Arena::new_with_global_interner();
1261        let source = r#"
1262            pair(2024-01-15T12:00:00Z, 2024-01-15T10:00:00Z).
1263            diff(D) :- pair(A, B) |> let D = fn:time:sub(A, B).
1264        "#;
1265
1266        let (mut ir, stratified) = compile(source, &arena)?;
1267        let store = Box::new(MemStore::new());
1268        let interpreter = execute(&mut ir, &stratified, store)?;
1269
1270        let facts: Vec<_> = interpreter
1271            .store()
1272            .scan("diff")
1273            .expect("relation diff not found")
1274            .collect();
1275        assert_eq!(facts.len(), 1);
1276        match &facts[0][0] {
1277            Value::Duration(nanos) => {
1278                assert_eq!(*nanos, 2 * 3600 * 1_000_000_000, "diff should be 2h");
1279            }
1280            v => panic!("expected Duration, got {v:?}"),
1281        }
1282        Ok(())
1283    }
1284
1285    #[test]
1286    fn test_time_components() -> Result<()> {
1287        let arena = Arena::new_with_global_interner();
1288        let source = r#"
1289            ts(2024-06-15T14:30:45Z).
1290            year_of(Y) :- ts(T) |> let Y = fn:time:year(T).
1291            month_of(M) :- ts(T) |> let M = fn:time:month(T).
1292            day_of(D) :- ts(T) |> let D = fn:time:day(T).
1293            hour_of(H) :- ts(T) |> let H = fn:time:hour(T).
1294            minute_of(Min) :- ts(T) |> let Min = fn:time:minute(T).
1295            second_of(S) :- ts(T) |> let S = fn:time:second(T).
1296        "#;
1297
1298        let (mut ir, stratified) = compile(source, &arena)?;
1299        let store = Box::new(MemStore::new());
1300        let interpreter = execute(&mut ir, &stratified, store)?;
1301
1302        let check = |rel: &str, expected: i64| {
1303            let facts: Vec<_> = interpreter.store().scan(rel).expect(rel).collect();
1304            assert_eq!(facts.len(), 1, "{rel}");
1305            assert_eq!(facts[0][0], Value::Number(expected), "{rel}");
1306        };
1307        check("year_of", 2024);
1308        check("month_of", 6);
1309        check("day_of", 15);
1310        check("hour_of", 14);
1311        check("minute_of", 30);
1312        check("second_of", 45);
1313        Ok(())
1314    }
1315
1316    #[test]
1317    fn test_duration_components() -> Result<()> {
1318        let arena = Arena::new_with_global_interner();
1319        let source = r#"
1320            dur(90s).
1321            dur_seconds(S) :- dur(D) |> let S = fn:duration:seconds(D).
1322            dur_nanos(N) :- dur(D) |> let N = fn:duration:nanos(D).
1323        "#;
1324
1325        let (mut ir, stratified) = compile(source, &arena)?;
1326        let store = Box::new(MemStore::new());
1327        let interpreter = execute(&mut ir, &stratified, store)?;
1328
1329        let secs: Vec<_> = interpreter.store().scan("dur_seconds").expect("dur_seconds").collect();
1330        assert_eq!(secs.len(), 1);
1331        assert_eq!(secs[0][0], Value::Float(90.0));
1332
1333        let nanos: Vec<_> = interpreter.store().scan("dur_nanos").expect("dur_nanos").collect();
1334        assert_eq!(nanos.len(), 1);
1335        assert_eq!(nanos[0][0], Value::Number(90_000_000_000));
1336        Ok(())
1337    }
1338
1339    #[test]
1340    fn test_time_comparison_predicates() -> Result<()> {
1341        let arena = Arena::new_with_global_interner();
1342        let source = r#"
1343            event(2024-01-01T00:00:00Z).
1344            event(2024-06-15T00:00:00Z).
1345            event(2024-12-31T00:00:00Z).
1346            recent(T) :- event(T), :time:gt(T, 2024-06-01T00:00:00Z).
1347        "#;
1348
1349        let (mut ir, stratified) = compile(source, &arena)?;
1350        let store = Box::new(MemStore::new());
1351        let interpreter = execute(&mut ir, &stratified, store)?;
1352
1353        let facts: Vec<_> = interpreter
1354            .store()
1355            .scan("recent")
1356            .expect("relation recent not found")
1357            .collect();
1358        assert_eq!(facts.len(), 2, "recent: {:?}", facts);
1359        Ok(())
1360    }
1361
1362    #[test]
1363    fn test_date_only_timestamp() -> Result<()> {
1364        let arena = Arena::new_with_global_interner();
1365        let source = r#"
1366            d(2024-01-15).
1367            has(X) :- d(X).
1368        "#;
1369
1370        let (mut ir, stratified) = compile(source, &arena)?;
1371        let store = Box::new(MemStore::new());
1372        let interpreter = execute(&mut ir, &stratified, store)?;
1373
1374        let facts: Vec<_> = interpreter
1375            .store()
1376            .scan("has")
1377            .expect("relation has not found")
1378            .collect();
1379        assert_eq!(facts.len(), 1);
1380        match &facts[0][0] {
1381            Value::Time(nanos) => {
1382                // 2024-01-15T00:00:00Z
1383                assert_eq!(*nanos, 1705276800_000_000_000);
1384            }
1385            v => panic!("expected Time, got {v:?}"),
1386        }
1387        Ok(())
1388    }
1389
1390    #[test]
1391    fn test_time_trunc() -> Result<()> {
1392        let arena = Arena::new_with_global_interner();
1393        let source = r#"
1394            ts(2024-06-15T14:30:45Z).
1395            truncated(Y) :- ts(T) |> let Y = fn:time:trunc(T, /hour).
1396        "#;
1397
1398        let (mut ir, stratified) = compile(source, &arena)?;
1399        let store = Box::new(MemStore::new());
1400        let interpreter = execute(&mut ir, &stratified, store)?;
1401
1402        let facts: Vec<_> = interpreter
1403            .store()
1404            .scan("truncated")
1405            .expect("relation truncated not found")
1406            .collect();
1407        assert_eq!(facts.len(), 1);
1408        match &facts[0][0] {
1409            Value::Time(nanos) => {
1410                // Should be truncated to 2024-06-15T14:00:00Z
1411                let display = format!("{}", Value::Time(*nanos));
1412                assert_eq!(display, "2024-06-15T14:00:00Z");
1413            }
1414            v => panic!("expected Time, got {v:?}"),
1415        }
1416        Ok(())
1417    }
1418
1419    #[test]
1420    fn test_duration_arithmetic() -> Result<()> {
1421        let arena = Arena::new_with_global_interner();
1422        let source = r#"
1423            d(1h, 30m).
1424            total(T) :- d(A, B) |> let T = fn:duration:add(A, B).
1425            doubled(T) :- d(A, _) |> let T = fn:duration:mult(A, 2).
1426        "#;
1427
1428        let (mut ir, stratified) = compile(source, &arena)?;
1429        let store = Box::new(MemStore::new());
1430        let interpreter = execute(&mut ir, &stratified, store)?;
1431
1432        let facts: Vec<_> = interpreter
1433            .store()
1434            .scan("total")
1435            .expect("relation total not found")
1436            .collect();
1437        assert_eq!(facts.len(), 1);
1438        match &facts[0][0] {
1439            Value::Duration(nanos) => {
1440                assert_eq!(*nanos, 90 * 60 * 1_000_000_000, "1h + 30m = 90m");
1441            }
1442            v => panic!("expected Duration, got {v:?}"),
1443        }
1444
1445        let facts: Vec<_> = interpreter
1446            .store()
1447            .scan("doubled")
1448            .expect("relation doubled not found")
1449            .collect();
1450        assert_eq!(facts.len(), 1);
1451        match &facts[0][0] {
1452            Value::Duration(nanos) => {
1453                assert_eq!(*nanos, 2 * 3600 * 1_000_000_000, "2 * 1h = 2h");
1454            }
1455            v => panic!("expected Duration, got {v:?}"),
1456        }
1457        Ok(())
1458    }
1459
1460    #[test]
1461    fn test_list_construction() -> Result<()> {
1462        let arena = Arena::new_with_global_interner();
1463        let source = r#"
1464            data(1). data(2). data(3).
1465            result(L) :- data(X) |> let L = fn:list(X).
1466        "#;
1467        let (mut ir, stratified) = compile(source, &arena)?;
1468        let store = Box::new(MemStore::new());
1469        let interpreter = execute(&mut ir, &stratified, store)?;
1470
1471        let facts: Vec<_> = interpreter
1472            .store()
1473            .scan("result")
1474            .expect("relation result not found")
1475            .collect();
1476        // Each data fact produces a single-element list
1477        assert_eq!(facts.len(), 3);
1478        for fact in &facts {
1479            match &fact[0] {
1480                Value::Compound(_, elems) => assert_eq!(elems.len(), 1),
1481                v => panic!("expected Compound, got {v:?}"),
1482            }
1483        }
1484        Ok(())
1485    }
1486
1487    #[test]
1488    fn test_list_literal_syntax() -> Result<()> {
1489        let arena = Arena::new_with_global_interner();
1490        // The parser desugars [1, 2, 3] to fn:list(1, 2, 3)
1491        let source = r#"
1492            result([1, 2, 3]).
1493        "#;
1494        let (mut ir, stratified) = compile(source, &arena)?;
1495        let store = Box::new(MemStore::new());
1496        let interpreter = execute(&mut ir, &stratified, store)?;
1497
1498        let facts: Vec<_> = interpreter
1499            .store()
1500            .scan("result")
1501            .expect("relation result not found")
1502            .collect();
1503        assert_eq!(facts.len(), 1);
1504        match &facts[0][0] {
1505            Value::Compound(_, elems) => {
1506                assert_eq!(elems.len(), 3);
1507                assert_eq!(elems[0], Value::Number(1));
1508                assert_eq!(elems[1], Value::Number(2));
1509                assert_eq!(elems[2], Value::Number(3));
1510            }
1511            v => panic!("expected Compound, got {v:?}"),
1512        }
1513        Ok(())
1514    }
1515
1516    #[test]
1517    fn test_struct_construction() -> Result<()> {
1518        let arena = Arena::new_with_global_interner();
1519        let source = r#"
1520            result({/name: "alice", /age: 30}).
1521        "#;
1522        let (mut ir, stratified) = compile(source, &arena)?;
1523        let store = Box::new(MemStore::new());
1524        let interpreter = execute(&mut ir, &stratified, store)?;
1525
1526        let facts: Vec<_> = interpreter
1527            .store()
1528            .scan("result")
1529            .expect("relation result not found")
1530            .collect();
1531        assert_eq!(facts.len(), 1);
1532        match &facts[0][0] {
1533            Value::Compound(_, elems) => {
1534                // Interleaved: ["/name", "alice", "/age", 30]
1535                assert_eq!(elems.len(), 4);
1536                assert_eq!(elems[0], Value::String("/name".to_string()));
1537                assert_eq!(elems[1], Value::String("alice".to_string()));
1538                assert_eq!(elems[2], Value::String("/age".to_string()));
1539                assert_eq!(elems[3], Value::Number(30));
1540            }
1541            v => panic!("expected Compound, got {v:?}"),
1542        }
1543        Ok(())
1544    }
1545
1546    #[test]
1547    fn test_pair_construction() -> Result<()> {
1548        let arena = Arena::new_with_global_interner();
1549        let source = r#"
1550            data("key", 42).
1551            result(P) :- data(K, V) |> let P = fn:pair(K, V).
1552        "#;
1553        let (mut ir, stratified) = compile(source, &arena)?;
1554        let store = Box::new(MemStore::new());
1555        let interpreter = execute(&mut ir, &stratified, store)?;
1556
1557        let facts: Vec<_> = interpreter
1558            .store()
1559            .scan("result")
1560            .expect("relation result not found")
1561            .collect();
1562        assert_eq!(facts.len(), 1);
1563        match &facts[0][0] {
1564            Value::Compound(_, elems) => {
1565                assert_eq!(elems.len(), 2);
1566                assert_eq!(elems[0], Value::String("key".to_string()));
1567                assert_eq!(elems[1], Value::Number(42));
1568            }
1569            v => panic!("expected Compound, got {v:?}"),
1570        }
1571        Ok(())
1572    }
1573
1574    #[test]
1575    fn test_list_get_and_len() -> Result<()> {
1576        let arena = Arena::new_with_global_interner();
1577        let source = r#"
1578            data([10, 20, 30]).
1579            first(F) :- data(L) |> let F = fn:list:get(L, 0).
1580            length(N) :- data(L) |> let N = fn:len(L).
1581        "#;
1582        let (mut ir, stratified) = compile(source, &arena)?;
1583        let store = Box::new(MemStore::new());
1584        let interpreter = execute(&mut ir, &stratified, store)?;
1585
1586        let facts: Vec<_> = interpreter
1587            .store()
1588            .scan("first")
1589            .expect("relation first not found")
1590            .collect();
1591        assert_eq!(facts.len(), 1);
1592        assert_eq!(facts[0][0], Value::Number(10));
1593
1594        let facts: Vec<_> = interpreter
1595            .store()
1596            .scan("length")
1597            .expect("relation length not found")
1598            .collect();
1599        assert_eq!(facts.len(), 1);
1600        assert_eq!(facts[0][0], Value::Number(3));
1601        Ok(())
1602    }
1603
1604    #[test]
1605    fn test_struct_get() -> Result<()> {
1606        let arena = Arena::new_with_global_interner();
1607        let source = r#"
1608            data({/name: "alice", /age: 30}).
1609            name(N) :- data(S) |> let N = fn:struct:get(S, /name).
1610            age(A) :- data(S) |> let A = fn:struct:get(S, /age).
1611        "#;
1612        let (mut ir, stratified) = compile(source, &arena)?;
1613        let store = Box::new(MemStore::new());
1614        let interpreter = execute(&mut ir, &stratified, store)?;
1615
1616        let facts: Vec<_> = interpreter
1617            .store()
1618            .scan("name")
1619            .expect("relation name not found")
1620            .collect();
1621        assert_eq!(facts.len(), 1);
1622        assert_eq!(facts[0][0], Value::String("alice".to_string()));
1623
1624        let facts: Vec<_> = interpreter
1625            .store()
1626            .scan("age")
1627            .expect("relation age not found")
1628            .collect();
1629        assert_eq!(facts.len(), 1);
1630        assert_eq!(facts[0][0], Value::Number(30));
1631        Ok(())
1632    }
1633
1634    #[test]
1635    fn test_pair_accessors() -> Result<()> {
1636        let arena = Arena::new_with_global_interner();
1637        // Use intermediate relation since multiple |> chains not supported
1638        let source = r#"
1639            data(42, "hello").
1640            pair_data(P) :- data(A, B) |> let P = fn:pair(A, B).
1641            result_first(F) :- pair_data(P) |> let F = fn:pair:first(P).
1642            result_second(S) :- pair_data(P) |> let S = fn:pair:second(P).
1643        "#;
1644        let (mut ir, stratified) = compile(source, &arena)?;
1645        let store = Box::new(MemStore::new());
1646        let interpreter = execute(&mut ir, &stratified, store)?;
1647
1648        let facts: Vec<_> = interpreter
1649            .store()
1650            .scan("result_first")
1651            .expect("relation result_first not found")
1652            .collect();
1653        assert_eq!(facts.len(), 1);
1654        assert_eq!(facts[0][0], Value::Number(42));
1655
1656        let facts: Vec<_> = interpreter
1657            .store()
1658            .scan("result_second")
1659            .expect("relation result_second not found")
1660            .collect();
1661        assert_eq!(facts.len(), 1);
1662        assert_eq!(facts[0][0], Value::String("hello".to_string()));
1663        Ok(())
1664    }
1665
1666    #[test]
1667    fn test_map_operations() -> Result<()> {
1668        let arena = Arena::new_with_global_interner();
1669        let source = r#"
1670            data([/a: 10, /b: 20]).
1671            val_a(V) :- data(M) |> let V = fn:map:get(M, /a).
1672            val_b(V) :- data(M) |> let V = fn:map:get(M, /b).
1673            nkeys(N) :- data(M) |> let N = fn:map:len(M).
1674        "#;
1675        let (mut ir, stratified) = compile(source, &arena)?;
1676        let store = Box::new(MemStore::new());
1677        let interpreter = execute(&mut ir, &stratified, store)?;
1678
1679        let facts: Vec<_> = interpreter
1680            .store()
1681            .scan("val_a")
1682            .expect("relation val_a not found")
1683            .collect();
1684        assert_eq!(facts.len(), 1);
1685        assert_eq!(facts[0][0], Value::Number(10));
1686
1687        let facts: Vec<_> = interpreter
1688            .store()
1689            .scan("val_b")
1690            .expect("relation val_b not found")
1691            .collect();
1692        assert_eq!(facts.len(), 1);
1693        assert_eq!(facts[0][0], Value::Number(20));
1694
1695        let facts: Vec<_> = interpreter
1696            .store()
1697            .scan("nkeys")
1698            .expect("relation nkeys not found")
1699            .collect();
1700        assert_eq!(facts.len(), 1);
1701        assert_eq!(facts[0][0], Value::Number(2));
1702        Ok(())
1703    }
1704
1705    #[test]
1706    fn test_nested_compound() -> Result<()> {
1707        let arena = Arena::new_with_global_interner();
1708        // A list of pairs
1709        let source = r#"
1710            data("a", 1).
1711            data("b", 2).
1712            pairs(P) :- data(K, V) |> let P = fn:pair(K, V).
1713            first_key(K) :- pairs(P) |> let K = fn:pair:first(P).
1714        "#;
1715        let (mut ir, stratified) = compile(source, &arena)?;
1716        let store = Box::new(MemStore::new());
1717        let interpreter = execute(&mut ir, &stratified, store)?;
1718
1719        let facts: Vec<_> = interpreter
1720            .store()
1721            .scan("first_key")
1722            .expect("relation first_key not found")
1723            .collect();
1724        assert_eq!(facts.len(), 2);
1725        let mut keys: Vec<String> = facts
1726            .iter()
1727            .map(|t| match &t[0] {
1728                Value::String(s) => s.clone(),
1729                v => panic!("expected string, got {v:?}"),
1730            })
1731            .collect();
1732        keys.sort();
1733        assert_eq!(keys, vec!["a", "b"]);
1734        Ok(())
1735    }
1736
1737    // -----------------------------------------------------------------------
1738    // Temporal facts tests
1739    // -----------------------------------------------------------------------
1740
1741    /// Non-recursive: temporal facts with point annotations.
1742    #[test]
1743    fn test_temporal_point_facts() -> Result<()> {
1744        let arena = Arena::new_with_global_interner();
1745        let source = r#"
1746            Decl link(X, Y) temporal bound [/name, /name].
1747            link(/a, /b)@[2024-01-01].
1748            link(/a, /c)@[2024-01-02].
1749        "#;
1750
1751        let (mut ir, stratified) = compile(source, &arena)?;
1752        let store = Box::new(MemStore::new());
1753        let interpreter = execute(&mut ir, &stratified, store)?;
1754
1755        // link is temporal so each fact has 4 columns: X, Y, start, end
1756        let facts: Vec<_> = interpreter
1757            .store()
1758            .scan("link")
1759            .expect("relation link not found")
1760            .collect();
1761        assert_eq!(facts.len(), 2, "expected 2 temporal link facts, got {:?}", facts);
1762
1763        // Each fact should have 4 columns (2 regular + 2 temporal)
1764        for fact in &facts {
1765            assert_eq!(fact.len(), 4, "temporal fact should have 4 columns, got {:?}", fact);
1766        }
1767
1768        Ok(())
1769    }
1770
1771    /// Non-recursive: simple temporal rule copying facts.
1772    #[test]
1773    fn test_temporal_simple_rule() -> Result<()> {
1774        let arena = Arena::new_with_global_interner();
1775        let source = r#"
1776            Decl link(X, Y) temporal bound [/name, /name].
1777            Decl reachable(X, Y) temporal bound [/name, /name].
1778            link(/a, /b)@[2024-01-01].
1779            reachable(X, Y)@[T] :- link(X, Y)@[T].
1780        "#;
1781
1782        let (mut ir, stratified) = compile(source, &arena)?;
1783        let store = Box::new(MemStore::new());
1784        let interpreter = execute(&mut ir, &stratified, store)?;
1785
1786        let facts: Vec<_> = interpreter
1787            .store()
1788            .scan("reachable")
1789            .expect("relation reachable not found")
1790            .collect();
1791        assert_eq!(facts.len(), 1, "expected 1 reachable fact, got {:?}", facts);
1792        assert_eq!(facts[0].len(), 4); // X, Y, start, end
1793        assert_eq!(facts[0][0], Value::String("/a".to_string()));
1794        assert_eq!(facts[0][1], Value::String("/b".to_string()));
1795
1796        Ok(())
1797    }
1798
1799    /// Recursive temporal graph reachability with point-in-time intervals.
1800    /// Based on mangle-go/examples/temporal_graph_points.mg
1801    #[test]
1802    fn test_temporal_graph_points() -> Result<()> {
1803        let arena = Arena::new_with_global_interner();
1804        let source = r#"
1805            Decl link(X, Y) temporal bound [/name, /name].
1806            Decl reachable(X, Y) temporal bound [/name, /name].
1807
1808            link(/a, /b)@[2024-01-01].
1809            link(/b, /c)@[2024-01-01].
1810
1811            link(/a, /c)@[2024-01-02].
1812            link(/c, /d)@[2024-01-02].
1813
1814            reachable(X, Y)@[T] :- link(X, Y)@[T].
1815            reachable(X, Z)@[T] :- reachable(X, Y)@[T], link(Y, Z)@[T].
1816        "#;
1817
1818        let (mut ir, stratified) = compile(source, &arena)?;
1819
1820        let store = Box::new(MemStore::new());
1821        let interpreter = execute(&mut ir, &stratified, store)?;
1822
1823        let facts: Vec<_> = interpreter
1824            .store()
1825            .scan("reachable")
1826            .expect("relation reachable not found")
1827            .collect();
1828
1829        let t1: i64 = 1704067200_000_000_000; // 2024-01-01 UTC
1830        let t2: i64 = 1704153600_000_000_000; // 2024-01-02 UTC
1831
1832        // Collect as (from, to, time) tuples for easier comparison
1833        let mut results: Vec<(String, String, i64)> = facts
1834            .iter()
1835            .map(|f| {
1836                let from = match &f[0] {
1837                    Value::String(n) => n.clone(),
1838                    v => panic!("expected name, got {v:?}"),
1839                };
1840                let to = match &f[1] {
1841                    Value::String(n) => n.clone(),
1842                    v => panic!("expected name, got {v:?}"),
1843                };
1844                let time = match &f[2] {
1845                    Value::Time(t) => *t,
1846                    v => panic!("expected time, got {v:?}"),
1847                };
1848                (from, to, time)
1849            })
1850            .collect();
1851        results.sort();
1852
1853        // Expected:
1854        // T1: a->b, b->c, a->c (derived)
1855        // T2: a->c, c->d, a->d (derived)
1856        let expected = vec![
1857            ("/a".to_string(), "/b".to_string(), t1),
1858            ("/a".to_string(), "/c".to_string(), t1),
1859            ("/a".to_string(), "/c".to_string(), t2),
1860            ("/a".to_string(), "/d".to_string(), t2),
1861            ("/b".to_string(), "/c".to_string(), t1),
1862            ("/c".to_string(), "/d".to_string(), t2),
1863        ];
1864        assert_eq!(results, expected, "temporal graph reachability mismatch");
1865
1866        Ok(())
1867    }
1868
1869    /// Test temporal interval coalescing: overlapping intervals merge.
1870    #[test]
1871    fn test_temporal_coalescing() -> Result<()> {
1872        let arena = Arena::new_with_global_interner();
1873        // Two overlapping intervals for the same fact should be coalesced
1874        let source = r#"
1875            Decl link(X, Y) temporal bound [/name, /name].
1876            Decl reach(X, Y) temporal bound [/name, /name].
1877            link(/a, /b)@[2024-01-01, 2024-01-05].
1878            link(/a, /b)@[2024-01-03, 2024-01-10].
1879            reach(X, Y)@[S, E] :- link(X, Y)@[S, E].
1880        "#;
1881
1882        let (mut ir, stratified) = compile(source, &arena)?;
1883        let store = Box::new(MemStore::new());
1884        let interpreter = execute(&mut ir, &stratified, store)?;
1885
1886        let facts: Vec<_> = interpreter
1887            .store()
1888            .scan("reach")
1889            .expect("relation reach not found")
1890            .collect();
1891
1892        // After coalescing, the two overlapping intervals [Jan 1-5] and [Jan 3-10]
1893        // should merge into a single [Jan 1, Jan 10] interval.
1894        assert_eq!(facts.len(), 1, "expected 1 coalesced fact, got {:?}", facts);
1895        assert_eq!(facts[0][0], Value::String("/a".to_string()));
1896        assert_eq!(facts[0][1], Value::String("/b".to_string()));
1897
1898        // Verify the merged interval spans Jan 1 to Jan 10
1899        let start = match &facts[0][2] {
1900            Value::Time(t) => *t,
1901            v => panic!("expected time, got {v:?}"),
1902        };
1903        let end = match &facts[0][3] {
1904            Value::Time(t) => *t,
1905            v => panic!("expected time, got {v:?}"),
1906        };
1907        let jan1: i64 = 1704067200_000_000_000;
1908        let jan10: i64 = 1704844800_000_000_000;
1909        assert_eq!(start, jan1, "start should be Jan 1");
1910        assert_eq!(end, jan10, "end should be Jan 10");
1911
1912        Ok(())
1913    }
1914
1915    // -----------------------------------------------------------------------
1916    // Ported from Go: temporal_integration_test.go
1917    // -----------------------------------------------------------------------
1918
1919    /// Go: TestIntegration_TemporalCoalesce - three overlapping intervals coalesce to one
1920    #[test]
1921    fn test_temporal_coalesce_three_overlapping() -> Result<()> {
1922        let arena = Arena::new_with_global_interner();
1923        let source = r#"
1924            Decl status(X) temporal bound [/name].
1925            status(/active)@[2024-01-01, 2024-01-15].
1926            status(/active)@[2024-01-10, 2024-01-25].
1927            status(/active)@[2024-01-20, 2024-01-31].
1928        "#;
1929
1930        let (mut ir, stratified) = compile(source, &arena)?;
1931        let store = Box::new(MemStore::new());
1932        let interpreter = execute(&mut ir, &stratified, store)?;
1933
1934        let facts: Vec<_> = interpreter
1935            .store()
1936            .scan("status")
1937            .expect("relation status not found")
1938            .collect();
1939        // After coalescing, should be 1 fact spanning Jan 1 to Jan 31
1940        assert_eq!(facts.len(), 1, "expected 1 coalesced fact, got {:?}", facts);
1941        assert_eq!(facts[0][0], Value::String("/active".to_string()));
1942
1943        Ok(())
1944    }
1945
1946    /// Go: TestIntegration_DurationBoundScenarios - temporal recursion reachability
1947    /// (more complex variant with 3 link facts, some at different timestamps)
1948    #[test]
1949    fn test_temporal_reachability_mixed_timestamps() -> Result<()> {
1950        let arena = Arena::new_with_global_interner();
1951        let source = r#"
1952            Decl link(X, Y) temporal bound [/name, /name].
1953            Decl reachable(X, Y) temporal bound [/name, /name].
1954
1955            link(/a, /b)@[2024-01-01].
1956            link(/b, /c)@[2024-01-01].
1957            link(/c, /d)@[2024-01-02].
1958
1959            reachable(X, Y)@[T] :- link(X, Y)@[T].
1960            reachable(X, Z)@[T] :- reachable(X, Y)@[T], link(Y, Z)@[T].
1961        "#;
1962
1963        let (mut ir, stratified) = compile(source, &arena)?;
1964        let store = Box::new(MemStore::new());
1965        let interpreter = execute(&mut ir, &stratified, store)?;
1966
1967        let facts: Vec<_> = interpreter
1968            .store()
1969            .scan("reachable")
1970            .expect("relation reachable not found")
1971            .collect();
1972
1973        // Collect as (from, to) ignoring time for uniqueness check
1974        let mut pairs: Vec<(String, String)> = facts
1975            .iter()
1976            .map(|f| {
1977                let from = match &f[0] { Value::String(s) => s.clone(), v => panic!("{v:?}") };
1978                let to = match &f[1] { Value::String(s) => s.clone(), v => panic!("{v:?}") };
1979                (from, to)
1980            })
1981            .collect();
1982        pairs.sort();
1983        pairs.dedup();
1984
1985        // Expected unique pairs (same as Go test):
1986        // a->b, b->c, a->c (all at t1), c->d (at t2)
1987        // a->c is derived transitively at t1
1988        // No a->d because c->d is at t2 but a->c is at t1
1989        let expected = vec![
1990            ("/a".to_string(), "/b".to_string()),
1991            ("/a".to_string(), "/c".to_string()),
1992            ("/b".to_string(), "/c".to_string()),
1993            ("/c".to_string(), "/d".to_string()),
1994        ];
1995        assert_eq!(pairs, expected, "temporal reachability mismatch");
1996
1997        Ok(())
1998    }
1999
2000    /// Go example: temporal_graph_intervals.mg
2001    /// Tests interval intersection with 4 cases for deriving reachability.
2002    #[test]
2003    fn test_temporal_graph_intervals() -> Result<()> {
2004        let arena = Arena::new_with_global_interner();
2005        let source = r#"
2006            Decl link(X, Y) temporal bound [/name, /name].
2007            Decl reachable(X, Y) temporal bound [/name, /name].
2008
2009            link(/a, /b)@[2024-01-01, 2024-01-10].
2010            link(/b, /c)@[2024-01-05, 2024-01-15].
2011            link(/c, /d)@[2024-01-12, 2024-01-20].
2012
2013            reachable(X, Y)@[S, E] :- link(X, Y)@[S, E].
2014
2015            reachable(X, Z)@[S1, E1] :-
2016                reachable(X, Y)@[S1, E1], link(Y, Z)@[S2, E2],
2017                :time:ge(S1, S2), :time:le(E1, E2), :time:le(S1, E1).
2018
2019            reachable(X, Z)@[S1, E2] :-
2020                reachable(X, Y)@[S1, E1], link(Y, Z)@[S2, E2],
2021                :time:ge(S1, S2), :time:lt(E2, E1), :time:le(S1, E2).
2022
2023            reachable(X, Z)@[S2, E1] :-
2024                reachable(X, Y)@[S1, E1], link(Y, Z)@[S2, E2],
2025                :time:gt(S2, S1), :time:le(E1, E2), :time:le(S2, E1).
2026
2027            reachable(X, Z)@[S2, E2] :-
2028                reachable(X, Y)@[S1, E1], link(Y, Z)@[S2, E2],
2029                :time:gt(S2, S1), :time:lt(E2, E1), :time:le(S2, E2).
2030        "#;
2031
2032        let (mut ir, stratified) = compile(source, &arena)?;
2033        let store = Box::new(MemStore::new());
2034        let interpreter = execute(&mut ir, &stratified, store)?;
2035
2036        let facts: Vec<_> = interpreter
2037            .store()
2038            .scan("reachable")
2039            .expect("relation reachable not found")
2040            .collect();
2041
2042        // Collect unique (from, to) pairs
2043        let mut pairs: Vec<(String, String)> = facts
2044            .iter()
2045            .map(|f| {
2046                let from = match &f[0] { Value::String(s) => s.clone(), v => panic!("{v:?}") };
2047                let to = match &f[1] { Value::String(s) => s.clone(), v => panic!("{v:?}") };
2048                (from, to)
2049            })
2050            .collect();
2051        pairs.sort();
2052        pairs.dedup();
2053
2054        // Expected (from the Go example comments):
2055        // a->b, b->c, c->d (direct)
2056        // a->c (intersection of [Jan1-10] and [Jan5-15] = [Jan5-10])
2057        // b->d (intersection of [Jan5-15] and [Jan12-20] = [Jan12-15])
2058        // a->d NOT derived (a->c is [Jan5-10], c->d is [Jan12-20] — no overlap)
2059        let expected = vec![
2060            ("/a".to_string(), "/b".to_string()),
2061            ("/a".to_string(), "/c".to_string()),
2062            ("/b".to_string(), "/c".to_string()),
2063            ("/b".to_string(), "/d".to_string()),
2064            ("/c".to_string(), "/d".to_string()),
2065        ];
2066        assert_eq!(pairs, expected, "interval intersection reachability mismatch");
2067
2068        // Verify specific intervals for derived facts
2069        for f in &facts {
2070            let from = match &f[0] { Value::String(s) => s.as_str(), _ => "" };
2071            let to = match &f[1] { Value::String(s) => s.as_str(), _ => "" };
2072            let start = match &f[2] { Value::Time(t) => *t, _ => 0 };
2073            let end = match &f[3] { Value::Time(t) => *t, _ => 0 };
2074
2075            if from == "/a" && to == "/c" {
2076                // Intersection of [Jan1,Jan10] and [Jan5,Jan15] = [Jan5,Jan10]
2077                let jan5: i64 = 1704412800_000_000_000;
2078                let jan10: i64 = 1704844800_000_000_000;
2079                assert_eq!(start, jan5, "a->c start should be Jan 5");
2080                assert_eq!(end, jan10, "a->c end should be Jan 10");
2081            }
2082            if from == "/b" && to == "/d" {
2083                // Intersection of [Jan5,Jan15] and [Jan12,Jan20] = [Jan12,Jan15]
2084                let jan12: i64 = 1705017600_000_000_000;
2085                let jan15: i64 = 1705276800_000_000_000;
2086                assert_eq!(start, jan12, "b->d start should be Jan 12");
2087                assert_eq!(end, jan15, "b->d end should be Jan 15");
2088            }
2089        }
2090
2091        Ok(())
2092    }
2093
2094    /// Go example: temporal_sequence.mg (adapted for mangle-rs syntax)
2095    /// Event sequence detection: A followed by B within 10 minutes.
2096    /// Uses fn:time:sub to compute duration and :duration:le for comparison.
2097    /// Split into two rules since mangle-rs doesn't support inline assignments in body.
2098    #[test]
2099    fn test_temporal_sequence_detection() -> Result<()> {
2100        let arena = Arena::new_with_global_interner();
2101        // Strategy: compute the time difference via transform, then filter.
2102        // Rule 1: compute (U, Diff) pairs where B happens after A
2103        // Rule 2: filter where Diff <= 10 minutes (600 seconds = 600_000_000_000 ns)
2104        let source = r#"
2105            Decl event_a(Name) temporal bound [/name].
2106            Decl event_b(Name) temporal bound [/name].
2107
2108            event_a(/u1)@[2024-01-01T10:00:00Z].
2109            event_b(/u1)@[2024-01-01T10:05:00Z].
2110
2111            event_a(/u2)@[2024-01-01T10:00:00Z].
2112            event_b(/u2)@[2024-01-01T10:15:00Z].
2113
2114            seq_pair(U, Diff) :-
2115                event_b(U)@[Tb],
2116                event_a(U)@[Ta],
2117                :time:lt(Ta, Tb)
2118                |> let Diff = fn:time:sub(Tb, Ta).
2119
2120            match_seq(U) :-
2121                seq_pair(U, Diff),
2122                :duration:le(Diff, 600000000000).
2123        "#;
2124
2125        let (mut ir, stratified) = compile(source, &arena)?;
2126        let store = Box::new(MemStore::new());
2127        let interpreter = execute(&mut ir, &stratified, store)?;
2128
2129        let facts: Vec<_> = interpreter
2130            .store()
2131            .scan("match_seq")
2132            .expect("relation match_seq not found")
2133            .collect();
2134
2135        // Only /u1 should match (5 min gap = 300s), /u2 has 15 min gap = 900s
2136        assert_eq!(facts.len(), 1, "expected 1 match, got {:?}", facts);
2137        assert_eq!(facts[0][0], Value::String("/u1".to_string()));
2138
2139        Ok(())
2140    }
2141
2142    /// Test: non-temporal programs are unaffected by temporal machinery
2143    #[test]
2144    fn test_temporal_backward_compat_reachability() -> Result<()> {
2145        let arena = Arena::new_with_global_interner();
2146        let source = r#"
2147            edge(/a, /b).
2148            edge(/b, /c).
2149            edge(/c, /d).
2150            path(X, Y) :- edge(X, Y).
2151            path(X, Z) :- edge(X, Y), path(Y, Z).
2152        "#;
2153
2154        let (mut ir, stratified) = compile(source, &arena)?;
2155        let store = Box::new(MemStore::new());
2156        let interpreter = execute(&mut ir, &stratified, store)?;
2157
2158        let facts: Vec<_> = interpreter
2159            .store()
2160            .scan("path")
2161            .expect("relation path not found")
2162            .collect();
2163
2164        let mut pairs: Vec<(String, String)> = facts
2165            .iter()
2166            .map(|f| {
2167                let from = match &f[0] { Value::String(s) => s.clone(), v => panic!("{v:?}") };
2168                let to = match &f[1] { Value::String(s) => s.clone(), v => panic!("{v:?}") };
2169                (from, to)
2170            })
2171            .collect();
2172        pairs.sort();
2173
2174        let expected = vec![
2175            ("/a".to_string(), "/b".to_string()),
2176            ("/a".to_string(), "/c".to_string()),
2177            ("/a".to_string(), "/d".to_string()),
2178            ("/b".to_string(), "/c".to_string()),
2179            ("/b".to_string(), "/d".to_string()),
2180            ("/c".to_string(), "/d".to_string()),
2181        ];
2182        assert_eq!(pairs, expected);
2183
2184        Ok(())
2185    }
2186
2187    /// Test: negation with temporal still works
2188    #[test]
2189    fn test_temporal_backward_compat_negation() -> Result<()> {
2190        let arena = Arena::new_with_global_interner();
2191        let source = r#"
2192            all(/a). all(/b). all(/c).
2193            excluded(/a).
2194            included(X) :- all(X), !excluded(X).
2195        "#;
2196
2197        let (mut ir, stratified) = compile(source, &arena)?;
2198        let store = Box::new(MemStore::new());
2199        let interpreter = execute(&mut ir, &stratified, store)?;
2200
2201        let facts: Vec<_> = interpreter
2202            .store()
2203            .scan("included")
2204            .expect("relation included not found")
2205            .collect();
2206
2207        let mut values: Vec<String> = facts
2208            .iter()
2209            .map(|f| match &f[0] { Value::String(s) => s.clone(), v => panic!("{v:?}") })
2210            .collect();
2211        values.sort();
2212        assert_eq!(values, vec!["/b", "/c"]);
2213
2214        Ok(())
2215    }
2216}