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            if let ast::Term::Atom(atom) = premise {
125                all_preds.insert(atom.sym);
126            } else if let ast::Term::NegAtom(atom) = premise {
127                all_preds.insert(atom.sym);
128            }
129        }
130    }
131
132    for pred in all_preds {
133        if !idb_preds.contains(&pred) {
134            program.ext_preds.push(pred);
135        }
136    }
137
138    let stratified = program.stratify().map_err(|e| anyhow!(e))?;
139
140    let ctx = LoweringContext::new(arena);
141    let ir = ctx.lower_unit(unit);
142
143    Ok((ir, stratified))
144}
145
146/// Compiles the Intermediate Representation (IR) into a WebAssembly (WASM) module.
147///
148/// This uses the default `WasmImportsBackend` which expects certain host functions
149/// to be available for data access.
150pub fn compile_to_wasm(ir: &mut Ir, stratified: &StratifiedProgram) -> Vec<u8> {
151    let mut codegen = Codegen::new_with_stratified(ir, stratified, WasmImportsBackend);
152    codegen.generate()
153}
154
155/// Executes a compiled Mangle program using the pure Rust interpreter.
156///
157/// This function:
158/// 1.  Iterates through the strata defined in `StratifiedProgram`.
159/// 2.  Identifies recursive predicates within each stratum.
160/// 3.  Executes non-recursive strata once.
161/// 4.  Executes recursive strata using a semi-naive evaluation loop.
162///
163/// Returns the `Interpreter` instance, which holds the final state (facts) of the execution.
164pub fn execute<'a>(
165    ir: &'a mut Ir,
166    stratified: &StratifiedProgram<'a>,
167    store: Box<dyn Store + 'a>,
168) -> Result<Interpreter<'a>> {
169    let arena = stratified.arena();
170
171    // 1. Pre-plan everything that needs mutable access to IR
172    let mut strata_plans = Vec::new();
173
174    for stratum in stratified.strata() {
175        let mut stratum_pred_names = FxHashSet::default();
176        for pred in &stratum {
177            if let Some(name) = arena.predicate_name(*pred) {
178                stratum_pred_names.insert(name);
179            }
180        }
181
182        // Identify rules for this stratum
183        let mut rule_ids = Vec::new();
184        for (i, inst) in ir.insts.iter().enumerate() {
185            if let Inst::Rule { head, .. } = inst
186                && let Inst::Atom { predicate, .. } = ir.get(*head)
187            {
188                let head_name = ir.resolve_name(*predicate);
189                if stratum_pred_names.contains(head_name) {
190                    rule_ids.push(InstId::new(i));
191                }
192            }
193        }
194
195        if rule_ids.is_empty() {
196            strata_plans.push(None);
197            continue;
198        }
199
200        // Check if any rule in the stratum is recursive
201        let mut is_recursive = false;
202        for &rule_id in &rule_ids {
203            if let Inst::Rule { premises, .. } = ir.get(rule_id) {
204                for &premise in premises {
205                    if let Inst::Atom { predicate, .. } = ir.get(premise) {
206                        let pred_name = ir.resolve_name(*predicate);
207                        if stratum_pred_names.contains(pred_name) {
208                            is_recursive = true;
209                            break;
210                        }
211                    }
212                }
213            }
214            if is_recursive {
215                break;
216            }
217        }
218
219        if !is_recursive {
220            let mut ops = Vec::new();
221            for rule_id in rule_ids {
222                let planner = Planner::new(ir);
223                ops.push(planner.plan_rule(rule_id)?);
224            }
225            strata_plans.push(Some(StratumPlan::NonRecursive(ops)));
226        } else {
227            let mut initial_ops = Vec::new();
228            for &rule_id in &rule_ids {
229                let planner = Planner::new(ir);
230                initial_ops.push(planner.plan_rule(rule_id)?);
231            }
232
233            let mut delta_plans = Vec::new();
234            for &rule_id in &rule_ids {
235                let premises = if let Inst::Rule { premises, .. } = ir.get(rule_id) {
236                    premises.clone()
237                } else {
238                    continue;
239                };
240
241                for &premise in &premises {
242                    let (predicate, pred_name) =
243                        if let Inst::Atom { predicate, .. } = ir.get(premise) {
244                            (*predicate, ir.resolve_name(*predicate).to_string())
245                        } else {
246                            continue;
247                        };
248
249                    if stratum_pred_names.contains(pred_name.as_str()) {
250                        let planner = Planner::new(ir).with_delta(predicate);
251                        delta_plans.push(planner.plan_rule(rule_id)?);
252                    }
253                }
254            }
255            strata_plans.push(Some(StratumPlan::Recursive {
256                initial_ops,
257                delta_plans,
258            }));
259        }
260    }
261
262    // 2. Now execute using the interpreter
263    let mut interpreter = Interpreter::new(ir, store);
264
265    // Initialize EDB relations
266    for pred in stratified.extensional_preds() {
267        if let Some(name) = arena.predicate_name(pred) {
268            interpreter.store_mut().create_relation(name);
269        }
270    }
271
272    for plan in strata_plans {
273        match plan {
274            Some(StratumPlan::NonRecursive(ops)) => {
275                for op in ops {
276                    interpreter.execute(&op)?;
277                }
278            }
279            Some(StratumPlan::Recursive {
280                initial_ops,
281                delta_plans,
282            }) => {
283                for op in initial_ops {
284                    interpreter.execute(&op)?;
285                }
286                interpreter.store_mut().merge_deltas();
287
288                loop {
289                    let mut changes = 0;
290                    for op in &delta_plans {
291                        changes += interpreter.execute(op)?;
292                    }
293                    if changes == 0 {
294                        break;
295                    }
296                    interpreter.store_mut().merge_deltas();
297                }
298            }
299            None => {}
300        }
301        interpreter.store_mut().merge_deltas();
302    }
303
304    Ok(interpreter)
305}
306
307enum StratumPlan {
308    NonRecursive(Vec<mangle_ir::physical::Op>),
309    Recursive {
310        initial_ops: Vec<mangle_ir::physical::Op>,
311        delta_plans: Vec<mangle_ir::physical::Op>,
312    },
313}
314
315#[cfg(test)]
316mod tests {
317    use super::*;
318    use mangle_interpreter::{MemStore, Value};
319
320    #[test]
321    fn test_driver_e2e() -> Result<()> {
322        let arena = Arena::new_with_global_interner();
323        let source = r#"
324            p(1).
325            p(2).
326            q(X) :- p(X).
327        "#;
328
329        let (mut ir, stratified) = compile(source, &arena)?;
330        let store = Box::new(MemStore::new());
331        let interpreter = execute(&mut ir, &stratified, store)?;
332
333        // Check results
334        let facts: Vec<_> = interpreter
335            .store()
336            .scan("q")
337            .expect("relation q not found")
338            .collect();
339        assert!(!facts.is_empty(), "relation q not found");
340
341        let mut values: Vec<i64> = facts
342            .iter()
343            .map(|t| match t[0] {
344                Value::Number(n) => n,
345                _ => panic!("expected number"),
346            })
347            .collect();
348        values.sort();
349
350        assert_eq!(values, vec![1, 2]);
351
352        Ok(())
353    }
354
355    #[test]
356    fn test_driver_e2e_with_package() -> Result<()> {
357        let arena = Arena::new_with_global_interner();
358        let source = r#"
359            Package pkg!
360            p(1).
361            q(X) :- p(X).
362        "#;
363
364        let (mut ir, stratified) = compile(source, &arena)?;
365        let store = Box::new(MemStore::new());
366        let interpreter = execute(&mut ir, &stratified, store)?;
367
368        // Check results - predicates should be prefixed with "pkg."
369        let facts: Vec<_> = interpreter
370            .store()
371            .scan("pkg.q")
372            .expect("relation pkg.q not found")
373            .collect();
374        assert!(!facts.is_empty(), "relation pkg.q not found");
375
376        let values: Vec<i64> = facts
377            .iter()
378            .map(|t| match t[0] {
379                Value::Number(n) => n,
380                _ => panic!("expected number"),
381            })
382            .collect();
383        assert_eq!(values, vec![1]);
384
385        Ok(())
386    }
387
388    #[test]
389    fn test_driver_let_transform() -> Result<()> {
390        let arena = Arena::new_with_global_interner();
391        let source = r#"
392            p(1).
393            p(2).
394            q(Y) :- p(X) |> let Y = fn:plus(X, 10).
395        "#;
396
397        let (mut ir, stratified) = compile(source, &arena)?;
398        let store = Box::new(MemStore::new());
399        let interpreter = execute(&mut ir, &stratified, store)?;
400
401        let facts: Vec<_> = interpreter
402            .store()
403            .scan("q")
404            .expect("relation q not found")
405            .collect();
406        let mut values: Vec<i64> = facts
407            .iter()
408            .map(|t| match t[0] {
409                Value::Number(n) => n,
410                _ => panic!("expected number"),
411            })
412            .collect();
413        values.sort();
414
415        assert_eq!(values, vec![11, 12]);
416        Ok(())
417    }
418
419    #[test]
420    fn test_driver_aggregation() -> Result<()> {
421        let arena = Arena::new_with_global_interner();
422        let source = r#"
423            p(1, 10).
424            p(1, 20).
425            p(2, 30).
426            q(K, S) :- p(K, V) |> do fn:group_by(K); let S = fn:sum(V).
427        "#;
428
429        let (mut ir, stratified) = compile(source, &arena)?;
430        let store = Box::new(MemStore::new());
431        let interpreter = execute(&mut ir, &stratified, store)?;
432
433        let facts: Vec<_> = interpreter
434            .store()
435            .scan("q")
436            .expect("relation q not found")
437            .collect();
438        let mut results: Vec<(i64, i64)> = facts
439            .iter()
440            .map(|t| {
441                if let (Value::Number(k), Value::Number(s)) = (&t[0], &t[1]) {
442                    (*k, *s)
443                } else {
444                    panic!("expected numbers");
445                }
446            })
447            .collect();
448        results.sort();
449
450        assert_eq!(results, vec![(1, 30), (2, 30)]);
451        Ok(())
452    }
453
454    #[test]
455    fn test_driver_aggregation_count() -> Result<()> {
456        let arena = Arena::new_with_global_interner();
457        let source = r#"
458            p(1, 10).
459            p(1, 20).
460            p(2, 30).
461            q(K, C) :- p(K, V) |> do fn:group_by(K); let C = fn:count(V).
462        "#;
463
464        let (mut ir, stratified) = compile(source, &arena)?;
465        let store = Box::new(MemStore::new());
466        let interpreter = execute(&mut ir, &stratified, store)?;
467
468        let facts: Vec<_> = interpreter
469            .store()
470            .scan("q")
471            .expect("relation q not found")
472            .collect();
473        let mut results: Vec<(i64, i64)> = facts
474            .iter()
475            .map(|t| {
476                if let (Value::Number(k), Value::Number(c)) = (&t[0], &t[1]) {
477                    (*k, *c)
478                } else {
479                    panic!("expected numbers");
480                }
481            })
482            .collect();
483        results.sort();
484
485        assert_eq!(results, vec![(1, 2), (2, 1)]);
486        Ok(())
487    }
488
489    #[test]
490    fn test_driver_reachability() -> Result<()> {
491        let arena = Arena::new_with_global_interner();
492        let source = r#"
493            edge(1, 2).
494            edge(2, 3).
495            edge(3, 4).
496            edge(4, 5).
497            reachable(X, Y) :- edge(X, Y).
498            reachable(X, Z) :- reachable(X, Y), edge(Y, Z).
499        "#;
500
501        let (mut ir, stratified) = compile(source, &arena)?;
502        let store = Box::new(MemStore::new());
503        let interpreter = execute(&mut ir, &stratified, store)?;
504
505        let facts: Vec<_> = interpreter
506            .store()
507            .scan("reachable")
508            .expect("reachable relation not found")
509            .collect();
510        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)
511
512        let mut reachable_from_1: Vec<i64> = facts
513            .iter()
514            .filter(|t| t[0] == Value::Number(1))
515            .map(|t| match t[1] {
516                Value::Number(n) => n,
517                _ => panic!("expected number"),
518            })
519            .collect();
520        reachable_from_1.sort();
521        assert_eq!(reachable_from_1, vec![2, 3, 4, 5]);
522
523        Ok(())
524    }
525
526    #[test]
527    fn test_name_constants() -> Result<()> {
528        let arena = Arena::new_with_global_interner();
529        let source = r#"
530            role(/role/admin).
531            role(/role/user).
532            role(/role/application).
533        "#;
534
535        let (mut ir, stratified) = compile(source, &arena)?;
536        let store = Box::new(MemStore::new());
537        let interpreter = execute(&mut ir, &stratified, store)?;
538
539        let facts: Vec<_> = interpreter
540            .store()
541            .scan("role")
542            .expect("relation role not found")
543            .collect();
544        assert_eq!(facts.len(), 3);
545
546        let mut names: Vec<String> = facts
547            .iter()
548            .map(|t| match &t[0] {
549                Value::String(s) => s.clone(),
550                _ => panic!("expected string"),
551            })
552            .collect();
553        names.sort();
554        assert_eq!(
555            names,
556            vec!["/role/admin", "/role/application", "/role/user"]
557        );
558
559        Ok(())
560    }
561
562    #[test]
563    fn test_inequality() -> Result<()> {
564        let arena = Arena::new_with_global_interner();
565        // Note: name constants like /role/application cannot appear immediately
566        // before `.` because the scanner treats `.` as a name_char. Use string
567        // constants or ensure a `)` separates the name from the clause terminator.
568        let source = r#"
569            role("admin").
570            role("user").
571            role("application").
572            non_app_role(R) :- role(R), R != "application".
573        "#;
574
575        let (mut ir, stratified) = compile(source, &arena)?;
576        let store = Box::new(MemStore::new());
577        let interpreter = execute(&mut ir, &stratified, store)?;
578
579        let facts: Vec<_> = interpreter
580            .store()
581            .scan("non_app_role")
582            .expect("relation non_app_role not found")
583            .collect();
584        assert_eq!(facts.len(), 2);
585
586        let mut names: Vec<String> = facts
587            .iter()
588            .map(|t| match &t[0] {
589                Value::String(s) => s.clone(),
590                _ => panic!("expected string"),
591            })
592            .collect();
593        names.sort();
594        assert_eq!(names, vec!["admin", "user"]);
595
596        Ok(())
597    }
598
599    #[test]
600    fn test_negation() -> Result<()> {
601        let arena = Arena::new_with_global_interner();
602        let source = r#"
603            service("web").
604            service("api").
605            service("db").
606            has_dep("web").
607            has_dep("api").
608            no_dep(S) :- service(S), !has_dep(S).
609        "#;
610
611        let (mut ir, stratified) = compile(source, &arena)?;
612        let store = Box::new(MemStore::new());
613        let interpreter = execute(&mut ir, &stratified, store)?;
614
615        let facts: Vec<_> = interpreter
616            .store()
617            .scan("no_dep")
618            .expect("relation no_dep not found")
619            .collect();
620        assert_eq!(facts.len(), 1);
621        assert_eq!(facts[0][0], Value::String("db".to_string()));
622
623        Ok(())
624    }
625
626    #[test]
627    fn test_combined_name_ineq_negation() -> Result<()> {
628        // Mini devops-like program exercising all features together
629        let arena = Arena::new_with_global_interner();
630        let source = r#"
631            container("web", /status/running).
632            container("api", /status/running).
633            container("db", /status/stopped).
634            depends_on("web", "db").
635            depends_on("api", "db").
636
637            running(Name) :- container(Name, /status/running).
638            stopped(Name) :- container(Name, /status/stopped).
639            has_running_dep(Name) :- depends_on(Name, Dep), running(Dep).
640            needs_attention(Name) :- depends_on(Name, Dep), stopped(Dep).
641            independent(Name) :- running(Name), !has_running_dep(Name).
642        "#;
643
644        let (mut ir, stratified) = compile(source, &arena)?;
645        let store = Box::new(MemStore::new());
646        let interpreter = execute(&mut ir, &stratified, store)?;
647
648        // Check running containers
649        let running: Vec<_> = interpreter
650            .store()
651            .scan("running")
652            .expect("relation running not found")
653            .collect();
654        assert_eq!(running.len(), 2);
655
656        // Check stopped
657        let stopped: Vec<_> = interpreter
658            .store()
659            .scan("stopped")
660            .expect("relation stopped not found")
661            .collect();
662        assert_eq!(stopped.len(), 1);
663        assert_eq!(stopped[0][0], Value::String("db".to_string()));
664
665        // Both web and api depend on db which is stopped
666        let needs_attention: Vec<_> = interpreter
667            .store()
668            .scan("needs_attention")
669            .expect("relation needs_attention not found")
670            .collect();
671        assert_eq!(needs_attention.len(), 2);
672
673        // db is not running so nobody has a running dep
674        // Both web and api are running and have no running deps
675        let independent: Vec<_> = interpreter
676            .store()
677            .scan("independent")
678            .expect("relation independent not found")
679            .collect();
680        assert_eq!(independent.len(), 2);
681
682        Ok(())
683    }
684
685    #[test]
686    fn test_join_with_constants_in_second_atom() -> Result<()> {
687        // Regression: fresh_var used ir.insts.len() as counter, producing
688        // duplicate NameIds for scan variables. This caused the second body
689        // atom's columns to overwrite each other during IndexLookup execution.
690        let arena = Arena::new_with_global_interner();
691        let source = r#"
692            p("a", "x").
693            q("a", "y").
694            test(E) :- p(E, "x"), q(E, "y").
695        "#;
696
697        let (mut ir, stratified) = compile(source, &arena)?;
698        let store = Box::new(MemStore::new());
699        let interpreter = execute(&mut ir, &stratified, store)?;
700
701        let facts: Vec<_> = interpreter
702            .store()
703            .scan("test")
704            .expect("relation test not found")
705            .collect();
706
707        assert_eq!(facts.len(), 1, "expected 1 result, got {:?}", facts);
708        assert_eq!(facts[0][0], Value::String("a".to_string()));
709
710        Ok(())
711    }
712
713    #[test]
714    fn test_join_constant_only_in_second_atom() -> Result<()> {
715        // Simpler variant: constant only in second atom
716        let arena = Arena::new_with_global_interner();
717        let source = r#"
718            p("a", "x").
719            q("a", "y").
720            test(E, V) :- p(E, V), q(E, "y").
721        "#;
722
723        let (mut ir, stratified) = compile(source, &arena)?;
724        let store = Box::new(MemStore::new());
725        let interpreter = execute(&mut ir, &stratified, store)?;
726
727        let facts: Vec<_> = interpreter
728            .store()
729            .scan("test")
730            .expect("relation test not found")
731            .collect();
732
733        assert_eq!(facts.len(), 1, "expected 1 result, got {:?}", facts);
734        assert_eq!(facts[0][0], Value::String("a".to_string()));
735        assert_eq!(facts[0][1], Value::String("x".to_string()));
736
737        Ok(())
738    }
739
740    #[test]
741    fn test_compile_units_package_use() -> Result<()> {
742        let arena = Arena::new_with_global_interner();
743
744        let schema = r#"
745            Package config_schema !
746            Decl server_port(Port).
747            Decl programs_dir(Path).
748        "#;
749
750        let config = r#"
751            Use config_schema !
752            config_schema.server_port(8090).
753            config_schema.programs_dir("/programs").
754        "#;
755
756        let (mut ir, stratified) = compile_units(&[schema, config], &arena)?;
757        let store = Box::new(MemStore::new());
758        let interpreter = execute(&mut ir, &stratified, store)?;
759
760        // Query the qualified predicate
761        let port_facts: Vec<_> = interpreter
762            .store()
763            .scan("config_schema.server_port")
764            .expect("relation config_schema.server_port not found")
765            .collect();
766        assert_eq!(port_facts.len(), 1);
767        assert_eq!(port_facts[0][0], Value::Number(8090));
768
769        let dir_facts: Vec<_> = interpreter
770            .store()
771            .scan("config_schema.programs_dir")
772            .expect("relation config_schema.programs_dir not found")
773            .collect();
774        assert_eq!(dir_facts.len(), 1);
775        assert_eq!(dir_facts[0][0], Value::String("/programs".to_string()));
776
777        Ok(())
778    }
779
780    #[test]
781    fn test_compile_to_wasm() -> Result<()> {
782        let arena = Arena::new_with_global_interner();
783        let source = r#"
784            p(1).
785            q(X) :- p(X).
786        "#;
787
788        let (mut ir, stratified) = compile(source, &arena)?;
789        let wasm_bytes = compile_to_wasm(&mut ir, &stratified);
790
791        // Basic check that we generated something that looks like WASM
792        assert!(!wasm_bytes.is_empty());
793        assert_eq!(&wasm_bytes[0..4], b"\0asm"); // WASM magic header
794
795        Ok(())
796    }
797}