//! Integration tests for example .geolog files //! //! These tests ensure that the example files in `examples/geolog/` remain //! valid as the language evolves. They serve as living documentation. use geolog::repl::ReplState; use std::fs; use std::path::Path; /// Helper to load and execute a .geolog file, returning the REPL state fn load_geolog_file(path: &Path) -> Result { let content = fs::read_to_string(path) .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?; let mut state = ReplState::new(); // Use execute_geolog which handles everything correctly state .execute_geolog(&content) .map_err(|e| format!("Error in {}: {}", path.display(), e))?; Ok(state) } // ============================================================================ // Graph examples // ============================================================================ #[test] fn test_graph_example_parses() { let path = Path::new("examples/geolog/graph.geolog"); let state = load_geolog_file(path).expect("graph.geolog should parse and elaborate"); // Check theory let graph = state.theories.get("Graph").expect("Graph theory should exist"); assert_eq!(graph.theory.signature.sorts.len(), 2, "Graph should have 2 sorts (V, E)"); assert_eq!(graph.theory.signature.functions.len(), 2, "Graph should have 2 functions (src, tgt)"); // Check instances assert!(state.instances.contains_key("Triangle"), "Triangle instance should exist"); assert!(state.instances.contains_key("Loop"), "Loop instance should exist"); assert!(state.instances.contains_key("Arrow"), "Arrow instance should exist"); assert!(state.instances.contains_key("Diamond"), "Diamond instance should exist"); } #[test] fn test_graph_triangle_structure() { let path = Path::new("examples/geolog/graph.geolog"); let state = load_geolog_file(path).unwrap(); let triangle = state.instances.get("Triangle").unwrap(); // Triangle has 3 vertices + 3 edges = 6 elements assert_eq!(triangle.structure.len(), 6, "Triangle should have 6 elements"); // Check carrier sizes: V has 3, E has 3 assert_eq!(triangle.structure.carrier_size(0), 3, "Triangle should have 3 vertices"); assert_eq!(triangle.structure.carrier_size(1), 3, "Triangle should have 3 edges"); } #[test] fn test_graph_diamond_structure() { let path = Path::new("examples/geolog/graph.geolog"); let state = load_geolog_file(path).unwrap(); let diamond = state.instances.get("Diamond").unwrap(); // Diamond has 4 vertices + 4 edges = 8 elements assert_eq!(diamond.structure.len(), 8, "Diamond should have 8 elements"); assert_eq!(diamond.structure.carrier_size(0), 4, "Diamond should have 4 vertices"); assert_eq!(diamond.structure.carrier_size(1), 4, "Diamond should have 4 edges"); } // ============================================================================ // Petri net examples // ============================================================================ #[test] fn test_petri_net_example_parses() { let path = Path::new("examples/geolog/petri_net.geolog"); let state = load_geolog_file(path).expect("petri_net.geolog should parse and elaborate"); // Check theory let petri = state.theories.get("PetriNet").expect("PetriNet theory should exist"); assert_eq!(petri.theory.signature.sorts.len(), 4, "PetriNet should have 4 sorts (P, T, In, Out)"); assert_eq!(petri.theory.signature.functions.len(), 4, "PetriNet should have 4 functions"); // Check instances assert!(state.instances.contains_key("ProducerConsumer")); assert!(state.instances.contains_key("MutualExclusion")); } // ============================================================================ // Petri Net Showcase - Full Type-Theoretic Encoding (from 2025-12-12 vision) // ============================================================================ #[test] fn test_petri_net_showcase_loads() { let path = Path::new("examples/geolog/petri_net_showcase.geolog"); let state = load_geolog_file(path).expect("petri_net_showcase.geolog should parse and elaborate"); // Check theories assert!(state.theories.contains_key("PetriNet"), "PetriNet theory should exist"); assert!(state.theories.contains_key("Marking"), "Marking theory should exist"); assert!(state.theories.contains_key("ReachabilityProblem"), "ReachabilityProblem theory should exist"); assert!(state.theories.contains_key("Trace"), "Trace theory should exist"); assert!(state.theories.contains_key("Iso"), "Iso theory should exist"); assert!(state.theories.contains_key("Solution"), "Solution theory should exist"); // Check PetriNet theory structure let petri = state.theories.get("PetriNet").unwrap(); assert_eq!(petri.theory.signature.sorts.len(), 4, "PetriNet should have 4 sorts"); assert_eq!(petri.theory.signature.functions.len(), 4, "PetriNet should have 4 functions"); // Check parameterized theories have correct parameter structure let marking = state.theories.get("Marking").unwrap(); assert_eq!(marking.params.len(), 1, "Marking should have 1 parameter (N : PetriNet instance)"); assert_eq!(marking.params[0].name, "N"); let reach_prob = state.theories.get("ReachabilityProblem").unwrap(); assert_eq!(reach_prob.params.len(), 1, "ReachabilityProblem should have 1 parameter"); let trace = state.theories.get("Trace").unwrap(); assert_eq!(trace.params.len(), 1, "Trace should have 1 parameter"); let iso = state.theories.get("Iso").unwrap(); assert_eq!(iso.params.len(), 2, "Iso should have 2 parameters (X : Sort, Y : Sort)"); let solution = state.theories.get("Solution").unwrap(); assert_eq!(solution.params.len(), 2, "Solution should have 2 parameters (N, RP)"); // Check instances assert!(state.instances.contains_key("ExampleNet"), "ExampleNet instance should exist"); assert!(state.instances.contains_key("problem0"), "problem0 instance should exist"); assert!(state.instances.contains_key("solution0"), "solution0 instance should exist"); assert!(state.instances.contains_key("problem2"), "problem2 instance should exist"); assert!(state.instances.contains_key("solution2"), "solution2 instance should exist"); } #[test] fn test_petri_net_showcase_example_net_structure() { let path = Path::new("examples/geolog/petri_net_showcase.geolog"); let state = load_geolog_file(path).unwrap(); let example_net = state.instances.get("ExampleNet").unwrap(); // ExampleNet has: // - 3 places (A, B, C) // - 3 transitions (ab, ba, abc) // - 4 input arcs (ab_in, ba_in, abc_in1, abc_in2) // - 3 output arcs (ab_out, ba_out, abc_out) // Total: 3 + 3 + 4 + 3 = 13 elements assert_eq!(example_net.structure.len(), 13, "ExampleNet should have 13 elements"); // Check carrier sizes by sort index (P=0, T=1, in=2, out=3) assert_eq!(example_net.structure.carrier_size(0), 3, "ExampleNet should have 3 places"); assert_eq!(example_net.structure.carrier_size(1), 3, "ExampleNet should have 3 transitions"); assert_eq!(example_net.structure.carrier_size(2), 4, "ExampleNet should have 4 input arcs"); assert_eq!(example_net.structure.carrier_size(3), 3, "ExampleNet should have 3 output arcs"); } #[test] fn test_petri_net_showcase_problem0_structure() { let path = Path::new("examples/geolog/petri_net_showcase.geolog"); let state = load_geolog_file(path).unwrap(); // problem0: 1 token in A -> 1 token in B let problem0 = state.instances.get("problem0").unwrap(); // ReachabilityProblem structure includes the nested Marking instances // This test verifies the cross-references work correctly assert!(!problem0.structure.is_empty(), "problem0 should have elements"); } #[test] fn test_petri_net_showcase_solution0_structure() { let path = Path::new("examples/geolog/petri_net_showcase.geolog"); let state = load_geolog_file(path).unwrap(); // solution0 proves A -> B reachability by firing 'ab' once let solution0 = state.instances.get("solution0").unwrap(); // Solution structure includes nested Trace and Iso instances // The trace has: 1 firing, 1 input terminal, 1 output terminal // Plus the isomorphism mappings assert!(!solution0.structure.is_empty(), "solution0 should have elements"); } #[test] fn test_petri_net_showcase_solution2_structure() { let path = Path::new("examples/geolog/petri_net_showcase.geolog"); let state = load_geolog_file(path).unwrap(); // solution2 proves 2A -> C reachability by firing 'ab' then 'abc' // This is the complex case: requires firing ab to move one A-token to B, // then abc consumes both an A-token and the new B-token to produce C let solution2 = state.instances.get("solution2").unwrap(); assert!(!solution2.structure.is_empty(), "solution2 should have elements"); } /// NEGATIVE TEST: Verify that an incomplete solution correctly fails axiom checking. /// /// This test ensures that the Trace theory's wire axioms properly catch /// solutions that are missing required wires between firings. #[test] fn test_solution2_incomplete_fails_axiom_check() { let path = Path::new("tests/negative/solution2_incomplete_negative_test.geolog"); let result = load_geolog_file(path); // This file should FAIL to load because it's missing a wire // connecting f1's output to f2's input match result { Ok(_) => panic!("Incomplete solution2 should fail axiom checking, but it succeeded"), Err(err) => { assert!( err.contains("axiom") && err.contains("violated"), "Error should mention axiom violation, got: {}", err ); } } } #[test] fn test_petri_net_producer_consumer() { let path = Path::new("examples/geolog/petri_net.geolog"); let state = load_geolog_file(path).unwrap(); let pc = state.instances.get("ProducerConsumer").unwrap(); // ProducerConsumer: 3 places + 2 transitions + 2 input arcs + 2 output arcs = 9 assert_eq!(pc.structure.len(), 9, "ProducerConsumer should have 9 elements"); } #[test] fn test_petri_net_mutual_exclusion() { let path = Path::new("examples/geolog/petri_net.geolog"); let state = load_geolog_file(path).unwrap(); let mutex = state.instances.get("MutualExclusion").unwrap(); // MutualExclusion: 5 places + 4 transitions + 6 input arcs + 6 output arcs = 21 assert_eq!(mutex.structure.len(), 21, "MutualExclusion should have 21 elements"); } // ============================================================================ // Monoid example (with product domain function support) // ============================================================================ #[test] fn test_monoid_example_parses() { let path = Path::new("examples/geolog/monoid.geolog"); let state = load_geolog_file(path).expect("monoid.geolog should parse and elaborate"); // Check theory let monoid = state.theories.get("Monoid").expect("Monoid theory should exist"); assert_eq!(monoid.theory.signature.sorts.len(), 1, "Monoid should have 1 sort (M)"); assert_eq!(monoid.theory.signature.functions.len(), 2, "Monoid should have 2 functions (mul, id)"); assert_eq!(monoid.theory.axioms.len(), 4, "Monoid should have 4 axioms"); // Check instances (product domain support via geolog-ulh) assert!(state.instances.contains_key("Trivial"), "Trivial monoid should exist"); assert!(state.instances.contains_key("BoolAnd"), "BoolAnd monoid should exist"); assert!(state.instances.contains_key("BoolOr"), "BoolOr monoid should exist"); } #[test] fn test_monoid_trivial_structure() { let path = Path::new("examples/geolog/monoid.geolog"); let state = load_geolog_file(path).unwrap(); let trivial = state.instances.get("Trivial").unwrap(); // Trivial monoid has 1 element assert_eq!(trivial.structure.carrier_size(0), 1, "Trivial monoid should have 1 element"); // Check id function (base domain: M -> M) // id: e -> e assert!(trivial.structure.functions[1].as_local().is_some(), "id should be a local function"); let id_col = trivial.structure.functions[1].as_local().unwrap(); assert_eq!(id_col.len(), 1, "id should have 1 entry"); assert!(id_col[0].is_some(), "id(e) should be defined"); // Check mul function (product domain: M × M -> M) // mul: (e,e) -> e if let geolog::core::FunctionColumn::ProductLocal { storage, field_sorts } = &trivial.structure.functions[0] { assert_eq!(field_sorts.len(), 2, "mul should have 2-element domain"); assert_eq!(storage.defined_count(), 1, "mul should have 1 entry defined"); } else { panic!("mul should be a ProductLocal function"); } } #[test] fn test_monoid_bool_and_structure() { let path = Path::new("examples/geolog/monoid.geolog"); let state = load_geolog_file(path).unwrap(); let bool_and = state.instances.get("BoolAnd").unwrap(); // BoolAnd has 2 elements (T, F) assert_eq!(bool_and.structure.carrier_size(0), 2, "BoolAnd should have 2 elements"); // Check mul function (product domain): all 4 entries should be defined if let geolog::core::FunctionColumn::ProductLocal { storage, .. } = &bool_and.structure.functions[0] { assert_eq!(storage.defined_count(), 4, "mul should have all 4 entries defined (2×2)"); // Verify it's total assert!(storage.is_total(&[2, 2]), "mul should be total on 2×2 domain"); } else { panic!("mul should be a ProductLocal function"); } // Check id function (base domain): both entries defined let id_col = bool_and.structure.functions[1].as_local().unwrap(); assert_eq!(id_col.len(), 2, "id should have 2 entries"); assert!(id_col.iter().all(|opt| opt.is_some()), "id should be total"); } #[test] fn test_monoid_bool_or_structure() { let path = Path::new("examples/geolog/monoid.geolog"); let state = load_geolog_file(path).unwrap(); let bool_or = state.instances.get("BoolOr").unwrap(); // BoolOr has 2 elements (T, F) assert_eq!(bool_or.structure.carrier_size(0), 2, "BoolOr should have 2 elements"); // Check mul function is total if let geolog::core::FunctionColumn::ProductLocal { storage, .. } = &bool_or.structure.functions[0] { assert!(storage.is_total(&[2, 2]), "mul should be total on 2×2 domain"); } else { panic!("mul should be a ProductLocal function"); } } // ============================================================================ // Preorder example // ============================================================================ #[test] fn test_preorder_example_parses() { let path = Path::new("examples/geolog/preorder.geolog"); let state = load_geolog_file(path).expect("preorder.geolog should parse and elaborate"); // Check theory let preorder = state.theories.get("Preorder").expect("Preorder theory should exist"); assert_eq!(preorder.theory.signature.sorts.len(), 1, "Preorder should have 1 sort (X)"); assert_eq!(preorder.theory.signature.relations.len(), 1, "Preorder should have 1 relation (leq)"); assert_eq!(preorder.theory.axioms.len(), 2, "Preorder should have 2 axioms (refl, trans)"); // Check instances assert!(state.instances.contains_key("Discrete3")); assert!(state.instances.contains_key("Chain3")); } // ============================================================================ // Transitive closure example (demonstrates chase algorithm) // ============================================================================ #[test] fn test_transitive_closure_example_parses() { let path = Path::new("examples/geolog/transitive_closure.geolog"); let state = load_geolog_file(path).expect("transitive_closure.geolog should parse and elaborate"); // Check theory let graph = state.theories.get("Graph").expect("Graph theory should exist"); assert_eq!(graph.theory.signature.sorts.len(), 1, "Graph should have 1 sort (V)"); assert_eq!(graph.theory.signature.relations.len(), 2, "Graph should have 2 relations (Edge, Path)"); assert_eq!(graph.theory.axioms.len(), 2, "Graph should have 2 axioms (base, trans)"); // Check instances assert!(state.instances.contains_key("Chain"), "Chain instance should exist"); assert!(state.instances.contains_key("Diamond"), "Diamond instance should exist"); assert!(state.instances.contains_key("Cycle"), "Cycle instance should exist"); } #[test] fn test_transitive_closure_chain_structure() { let path = Path::new("examples/geolog/transitive_closure.geolog"); let state = load_geolog_file(path).unwrap(); let chain = state.instances.get("Chain").unwrap(); // Chain has 4 vertices assert_eq!(chain.structure.carrier_size(0), 4, "Chain should have 4 vertices"); // With `= chase { ... }`, axioms are applied during elaboration. // Path now has 6 tuples (transitive closure computed automatically). use geolog::core::RelationStorage; assert_eq!(chain.structure.relations[0].len(), 3, "Chain should have 3 Edge tuples"); assert_eq!(chain.structure.relations[1].len(), 6, "Chain should have 6 Path tuples after chase: 3 base + 2 one-step + 1 two-step"); } #[test] fn test_transitive_closure_chase() { use geolog::core::RelationStorage; use geolog::query::chase::chase_fixpoint; use geolog::universe::Universe; let path = Path::new("examples/geolog/transitive_closure.geolog"); let mut state = load_geolog_file(path).unwrap(); let chain = state.instances.get_mut("Chain").unwrap(); let theory = state.theories.get("Graph").unwrap(); // Chase already ran during elaboration (instance uses `= chase { ... }`), // so Path already has 6 tuples. assert_eq!(chain.structure.relations[1].len(), 6, "Chain should have 6 Path tuples after elaboration with chase"); // Running chase again should be idempotent (1 iteration, no changes) let mut universe = Universe::new(); let iterations = chase_fixpoint( &theory.theory.axioms, &mut chain.structure, &mut universe, &theory.theory.signature, 100, ).unwrap(); // Should converge immediately (already at fixpoint) assert_eq!(iterations, 1, "Chase should converge in 1 iteration when already at fixpoint"); // Still have 6 Path tuples assert_eq!(chain.structure.relations[1].len(), 6, "Chain should still have 6 Path tuples"); } // ============================================================================ // Theories: GeologMeta and RelAlgIR // ============================================================================ #[test] fn test_geolog_meta_loads() { let path = Path::new("theories/GeologMeta.geolog"); let state = load_geolog_file(path).expect("GeologMeta.geolog should parse and elaborate"); let meta = state.theories.get("GeologMeta").expect("GeologMeta theory should exist"); // GeologMeta is a large theory: 41 sorts, 78 functions, 3 relations, 16 axioms assert_eq!(meta.theory.signature.sorts.len(), 41, "GeologMeta should have 41 sorts"); assert_eq!(meta.theory.signature.functions.len(), 78, "GeologMeta should have 78 functions"); assert_eq!(meta.theory.signature.relations.len(), 3, "GeologMeta should have 3 relations"); assert_eq!(meta.theory.axioms.len(), 16, "GeologMeta should have 16 axioms"); // Check some key sorts exist assert!(meta.theory.signature.lookup_sort("Theory").is_some(), "Theory sort should exist"); assert!(meta.theory.signature.lookup_sort("Srt").is_some(), "Srt sort should exist"); assert!(meta.theory.signature.lookup_sort("Func").is_some(), "Func sort should exist"); assert!(meta.theory.signature.lookup_sort("Elem").is_some(), "Elem sort should exist"); } #[test] fn test_relalg_ir_loads() { // First load GeologMeta (RelAlgIR extends it) let meta_content = fs::read_to_string("theories/GeologMeta.geolog") .expect("Failed to read GeologMeta.geolog"); let ir_content = fs::read_to_string("theories/RelAlgIR.geolog") .expect("Failed to read RelAlgIR.geolog"); let mut state = ReplState::new(); state.execute_geolog(&meta_content) .expect("GeologMeta should load"); state.execute_geolog(&ir_content) .expect("RelAlgIR should load"); let ir = state.theories.get("RelAlgIR").expect("RelAlgIR theory should exist"); // RelAlgIR has 80 sorts (41 from GeologMeta + 39 own) assert_eq!(ir.theory.signature.sorts.len(), 80, "RelAlgIR should have 80 sorts"); // Check GeologMeta sorts are correctly qualified assert!(ir.theory.signature.lookup_sort("GeologMeta/Srt").is_some(), "GeologMeta/Srt should exist (inherited sort)"); assert!(ir.theory.signature.lookup_sort("GeologMeta/Func").is_some(), "GeologMeta/Func should exist (inherited sort)"); // Check RelAlgIR's own sorts exist (no prefix) assert!(ir.theory.signature.lookup_sort("Wire").is_some(), "Wire sort should exist"); assert!(ir.theory.signature.lookup_sort("Op").is_some(), "Op sort should exist"); assert!(ir.theory.signature.lookup_sort("ScanOp").is_some(), "ScanOp sort should exist"); // Check functions are correctly qualified // GeologMeta's "Func/dom" should become "GeologMeta/Func/dom" assert!(ir.theory.signature.lookup_func("GeologMeta/Func/dom").is_some(), "GeologMeta/Func/dom should exist (inherited function)"); assert!(ir.theory.signature.lookup_func("GeologMeta/Func/cod").is_some(), "GeologMeta/Func/cod should exist (inherited function)"); // RelAlgIR's own functions assert!(ir.theory.signature.lookup_func("Wire/schema").is_some(), "Wire/schema should exist"); assert!(ir.theory.signature.lookup_func("ScanOp/out").is_some(), "ScanOp/out should exist"); // Check functions referencing inherited sorts have correct domain/codomain // ScanOp/srt : ScanOp -> GeologMeta/Srt let scan_srt = ir.theory.signature.lookup_func("ScanOp/srt") .expect("ScanOp/srt should exist"); let func_info = &ir.theory.signature.functions[scan_srt]; match &func_info.codomain { geolog::core::DerivedSort::Base(sort_id) => { let sort_name = &ir.theory.signature.sorts[*sort_id]; assert_eq!(sort_name, "GeologMeta/Srt", "ScanOp/srt codomain should be GeologMeta/Srt"); } _ => panic!("ScanOp/srt codomain should be a base sort"), } } // ============================================================================ // RelAlgIR query plan examples // ============================================================================ /// Test that RelAlgIR instances can be created and represent query plans /// /// These instances use `= chase { ... }` to derive relations from axioms. /// The chase handles function applications in premises (e.g., `s ScanOp/out = w`) /// and universal conclusions (e.g., `forall x. |- R(x,x)`). #[test] fn test_relalg_simple_examples() { // Load theories first let meta_content = fs::read_to_string("theories/GeologMeta.geolog") .expect("Failed to read GeologMeta.geolog"); let ir_content = fs::read_to_string("theories/RelAlgIR.geolog") .expect("Failed to read RelAlgIR.geolog"); let examples_content = fs::read_to_string("examples/geolog/relalg_simple.geolog") .expect("Failed to read relalg_simple.geolog"); let mut state = ReplState::new(); state.execute_geolog(&meta_content) .expect("GeologMeta should load"); state.execute_geolog(&ir_content) .expect("RelAlgIR should load"); state.execute_geolog(&examples_content) .expect("relalg_simple.geolog should load"); // Check ScanV instance let scan_v = state.instances.get("ScanV") .expect("ScanV instance should exist"); assert_eq!(scan_v.structure.len(), 7, "ScanV should have 7 elements"); // Check FilterScan instance let filter_scan = state.instances.get("FilterScan") .expect("FilterScan instance should exist"); assert_eq!(filter_scan.structure.len(), 18, "FilterScan should have 18 elements"); // Verify FilterScan has the expected sorts populated // Get RelAlgIR theory for sort lookups let ir = state.theories.get("RelAlgIR").expect("RelAlgIR should exist"); // Check Wire sort has 2 elements (w1, w2) let wire_sort = ir.theory.signature.lookup_sort("Wire").expect("Wire sort"); assert_eq!( filter_scan.structure.carrier_size(wire_sort), 2, "FilterScan should have 2 Wire elements" ); // Check FilterOp sort has 1 element let filter_sort = ir.theory.signature.lookup_sort("FilterOp").expect("FilterOp sort"); assert_eq!( filter_scan.structure.carrier_size(filter_sort), 1, "FilterScan should have 1 FilterOp element" ); // Check ScanOp sort has 1 element let scan_sort = ir.theory.signature.lookup_sort("ScanOp").expect("ScanOp sort"); assert_eq!( filter_scan.structure.carrier_size(scan_sort), 1, "FilterScan should have 1 ScanOp element" ); } // ============================================================================ // RelAlgIR compile → execute roundtrip // ============================================================================ /// Helper to load RelAlgIR theory for tests fn load_relalg_for_test() -> (ReplState, std::rc::Rc) { let meta_content = fs::read_to_string("theories/GeologMeta.geolog") .expect("Failed to read GeologMeta.geolog"); let ir_content = fs::read_to_string("theories/RelAlgIR.geolog") .expect("Failed to read RelAlgIR.geolog"); let mut state = ReplState::new(); state .execute_geolog(&meta_content) .expect("GeologMeta should load"); state .execute_geolog(&ir_content) .expect("RelAlgIR should load"); let relalg_theory = state .theories .get("RelAlgIR") .expect("RelAlgIR should exist") .clone(); (state, relalg_theory) } /// Helper to verify roundtrip: direct execution == RelAlgIR execution fn verify_roundtrip( plan: &geolog::query::backend::QueryOp, target: &geolog::core::Structure, relalg_theory: &geolog::core::ElaboratedTheory, description: &str, ) { use geolog::query::backend::execute; use geolog::query::from_relalg::execute_relalg; use geolog::query::to_relalg::compile_to_relalg; use geolog::universe::Universe; // Execute directly let direct_result = execute(plan, target); // Compile to RelAlgIR let mut universe = Universe::new(); let relalg_instance = compile_to_relalg(plan, &std::rc::Rc::new(relalg_theory.clone()), &mut universe) .unwrap_or_else(|e| panic!("{}: Compilation failed: {}", description, e)); // Execute via RelAlgIR interpreter let relalg_result = execute_relalg(&relalg_instance, relalg_theory, target, None) .unwrap_or_else(|e| panic!("{}: RelAlgIR execution failed: {}", description, e)); // Compare results assert_eq!( direct_result.len(), relalg_result.len(), "{}: Length mismatch ({} vs {})", description, direct_result.len(), relalg_result.len() ); for (tuple, mult) in direct_result.iter() { assert_eq!( relalg_result.tuples.get(tuple), Some(mult), "{}: Tuple {:?} has wrong multiplicity", description, tuple ); } } /// Tests that we can compile a query to RelAlgIR and then execute it, /// getting the same results as direct execution. #[test] fn test_relalg_compile_execute_roundtrip() { use geolog::core::Structure; use geolog::query::backend::QueryOp; let (_, relalg_theory) = load_relalg_for_test(); // Create a simple test structure with 3 elements in sort 0 let mut target = Structure::new(1); target.carriers[0].insert(0); target.carriers[0].insert(1); target.carriers[0].insert(2); // Test Scan let scan_plan = QueryOp::Scan { sort_idx: 0 }; verify_roundtrip(&scan_plan, &target, &relalg_theory, "Scan"); } #[test] fn test_relalg_roundtrip_filter() { use geolog::core::Structure; use geolog::id::{NumericId, Slid}; use geolog::query::backend::{Predicate, QueryOp}; let (_, relalg_theory) = load_relalg_for_test(); // Create structure with 5 elements let mut target = Structure::new(1); for i in 0..5 { target.carriers[0].insert(i); } // Filter with True predicate (should keep all) let filter_true = QueryOp::Filter { input: Box::new(QueryOp::Scan { sort_idx: 0 }), pred: Predicate::True, }; verify_roundtrip(&filter_true, &target, &relalg_theory, "Filter(True)"); // Filter with False predicate (should keep none) let filter_false = QueryOp::Filter { input: Box::new(QueryOp::Scan { sort_idx: 0 }), pred: Predicate::False, }; verify_roundtrip(&filter_false, &target, &relalg_theory, "Filter(False)"); // Filter with ColEqConst let filter_const = QueryOp::Filter { input: Box::new(QueryOp::Scan { sort_idx: 0 }), pred: Predicate::ColEqConst { col: 0, val: Slid::from_usize(2), }, }; verify_roundtrip(&filter_const, &target, &relalg_theory, "Filter(ColEqConst)"); } #[test] fn test_relalg_roundtrip_join() { use geolog::core::Structure; use geolog::query::backend::{JoinCond, QueryOp}; let (_, relalg_theory) = load_relalg_for_test(); // Create structure with 2 sorts let mut target = Structure::new(2); target.carriers[0].insert(0); target.carriers[0].insert(1); target.carriers[1].insert(10); target.carriers[1].insert(11); target.carriers[1].insert(12); // Cross join let cross_join = QueryOp::Join { left: Box::new(QueryOp::Scan { sort_idx: 0 }), right: Box::new(QueryOp::Scan { sort_idx: 1 }), cond: JoinCond::Cross, }; verify_roundtrip(&cross_join, &target, &relalg_theory, "Join(Cross)"); } #[test] fn test_relalg_roundtrip_union() { use geolog::core::Structure; use geolog::query::backend::QueryOp; let (_, relalg_theory) = load_relalg_for_test(); // Create structure let mut target = Structure::new(2); target.carriers[0].insert(0); target.carriers[0].insert(1); target.carriers[1].insert(2); target.carriers[1].insert(3); // Union of two scans let union_plan = QueryOp::Union { left: Box::new(QueryOp::Scan { sort_idx: 0 }), right: Box::new(QueryOp::Scan { sort_idx: 1 }), }; verify_roundtrip(&union_plan, &target, &relalg_theory, "Union"); } #[test] fn test_relalg_roundtrip_distinct_negate() { use geolog::core::Structure; use geolog::query::backend::QueryOp; let (_, relalg_theory) = load_relalg_for_test(); // Create structure let mut target = Structure::new(1); target.carriers[0].insert(0); target.carriers[0].insert(1); target.carriers[0].insert(2); // Distinct let distinct_plan = QueryOp::Distinct { input: Box::new(QueryOp::Scan { sort_idx: 0 }), }; verify_roundtrip(&distinct_plan, &target, &relalg_theory, "Distinct"); // Negate let negate_plan = QueryOp::Negate { input: Box::new(QueryOp::Scan { sort_idx: 0 }), }; verify_roundtrip(&negate_plan, &target, &relalg_theory, "Negate"); } // ============================================================================ // Meta-test: all examples should parse // ============================================================================ /// Tests that all standalone .geolog example files parse and elaborate. /// /// Note: Some examples require loading theories first (e.g., relalg_simple.geolog /// requires GeologMeta and RelAlgIR). These are tested separately. #[test] fn test_all_examples_parse() { let examples_dir = Path::new("examples/geolog"); if !examples_dir.exists() { panic!("examples/geolog directory does not exist"); } // Examples that require loading theories first (tested separately) let requires_theories = [ "relalg_simple.geolog", ]; let mut failures = Vec::new(); for entry in fs::read_dir(examples_dir).unwrap() { let entry = entry.unwrap(); let path = entry.path(); // Skip files that require loading theories first if path.file_name() .and_then(|f| f.to_str()) .is_some_and(|file_name| requires_theories.contains(&file_name)) { continue; } if path.extension().is_some_and(|ext| ext == "geolog") && let Err(e) = load_geolog_file(&path) { failures.push(format!("{}: {}", path.display(), e)); } } if !failures.is_empty() { panic!( "The following example files failed to parse/elaborate:\n{}", failures.join("\n") ); } } // ============================================================================ // Multi-session persistence tests // ============================================================================ /// Tests that theories and instances survive REPL restarts /// /// This is a critical test for persistence: create data in one "session", /// then verify it's still accessible after creating a new ReplState with /// the same persistence path. #[test] fn test_persistence_survives_restart() { use tempfile::tempdir; let dir = tempdir().expect("Failed to create temp directory"); let db_path = dir.path().join("test.geolog"); // Session 1: Create a theory and instance { let mut state = ReplState::with_path(&db_path); let theory_def = r#" theory Counter { C : Sort; next : C -> C; } "#; state.execute_geolog(theory_def).expect("Theory should define"); let instance_def = r#" instance Mod3 : Counter = { zero : C; one : C; two : C; zero next = one; one next = two; two next = zero; } "#; state.execute_geolog(instance_def).expect("Instance should define"); // Verify it's in the current session assert!(state.theories.contains_key("Counter"), "Counter theory should exist in session 1"); assert!(state.instances.contains_key("Mod3"), "Mod3 instance should exist in session 1"); // Explicitly save before dropping state.store.save().expect("Save should succeed"); } // Session 2: Load from same path and verify data persists { let state = ReplState::with_path(&db_path); // Theory should be reconstructed assert!( state.theories.contains_key("Counter"), "Counter theory should persist across sessions" ); // Instance should be reconstructed assert!( state.instances.contains_key("Mod3"), "Mod3 instance should persist across sessions" ); // Verify instance structure has correct element count let mod3 = state.instances.get("Mod3").expect("Mod3 should exist"); assert_eq!( mod3.structure.len(), 3, "Mod3 should have 3 elements after reload" ); } } /// Tests that chase-derived data persists correctly #[test] fn test_persistence_with_chase() { use tempfile::tempdir; let dir = tempdir().expect("Failed to create temp directory"); let db_path = dir.path().join("chase_test.geolog"); // Session 1: Create a theory with chase { let mut state = ReplState::with_path(&db_path); let content = r#" theory Preorder { X : Sort; leq : [lo: X, hi: X] -> Prop; ax/refl : forall x : X. |- [lo: x, hi: x] leq; } instance Three : Preorder = chase { a : X; b : X; c : X; } "#; state.execute_geolog(content).expect("Should define theory and chase instance"); // Verify chase added diagonal tuples let three = state.instances.get("Three").expect("Three should exist"); assert_eq!(three.structure.relations[0].tuples.len(), 3, "Should have 3 reflexive tuples"); // Explicitly save before dropping state.store.save().expect("Save should succeed"); } // Session 2: Verify chase results persist { let state = ReplState::with_path(&db_path); assert!(state.theories.contains_key("Preorder"), "Theory should persist"); assert!(state.instances.contains_key("Three"), "Instance should persist"); let three = state.instances.get("Three").expect("Three should exist"); assert_eq!( three.structure.relations[0].tuples.len(), 3, "Chase-derived tuples should persist" ); } }