geolog-zeta-fork/tests/examples_integration.rs
2026-02-26 11:50:51 +01:00

961 lines
36 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! 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<ReplState, String> {
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<geolog::core::ElaboratedTheory>) {
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"
);
}
}