geolog-zeta-fork/tests/examples_integration.rs

961 lines
36 KiB
Rust
Raw Permalink Normal View History

2026-02-26 11:50:51 +01:00
//! 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"
);
}
}