The base commit
This commit is contained in:
commit
ac2f202594
3
.gitattributes
vendored
Normal file
3
.gitattributes
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
|
||||
# Use bd merge for beads JSONL files
|
||||
.beads/issues.jsonl merge=beads
|
||||
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
target/
|
||||
.claude/
|
||||
.idea
|
||||
|
||||
1282
Cargo.lock
generated
Normal file
1282
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
31
Cargo.toml
Normal file
31
Cargo.toml
Normal file
@ -0,0 +1,31 @@
|
||||
[package]
|
||||
name = "geolog"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
chumsky = "0.9"
|
||||
ariadne = "0.4" # for nice error reporting
|
||||
uuid = { version = "1", features = ["v7"] }
|
||||
roaring = "0.10"
|
||||
nonminmax = "0.1"
|
||||
rkyv = { version = "0.7", features = ["validation", "uuid", "indexmap"] }
|
||||
tinyvec = { version = "1.6", features = ["alloc"] }
|
||||
indexmap = "2.0"
|
||||
memmap2 = "0.9"
|
||||
rustyline = "15" # readline for REPL
|
||||
toml = "0.8" # workspace.toml parsing
|
||||
serde = { version = "1", features = ["derive"] } # for toml
|
||||
egglog-union-find = "1.0" # union-find for congruence closure
|
||||
egglog-numeric-id = "1.0" # newtype IDs with define_id! macro
|
||||
itertools = "0.13" # Either type for zero-copy iterators
|
||||
|
||||
[dev-dependencies]
|
||||
insta = "1.40" # snapshot testing
|
||||
proptest = "1.4" # property-based testing
|
||||
rand = "0.9.2"
|
||||
tempfile = "3.10" # temp dirs for persistence tests
|
||||
|
||||
[[bin]]
|
||||
name = "geolog"
|
||||
path = "src/bin/geolog.rs"
|
||||
321
NOTES.md
Normal file
321
NOTES.md
Normal file
@ -0,0 +1,321 @@
|
||||
# Geolog Project Notes
|
||||
|
||||
## Overview
|
||||
|
||||
**Geolog** is a **Geometric Logic REPL** — a type theory with semantics in topoi, designed for formal specifications using geometric logic.
|
||||
|
||||
### Core Capabilities
|
||||
|
||||
- **Geometric logic programming** — encode mathematical structures, relationships, and constraints
|
||||
- **Database schema definition** — define sorts, functions, relations, and axioms
|
||||
- **Model/instance creation** — create concrete finite models satisfying theory axioms
|
||||
- **Automated inference** — chase algorithm for automatic fact derivation
|
||||
- **Version control** — git-like commits and tracking for instances
|
||||
- **Persistence** — append-only storage with optional disk persistence
|
||||
|
||||
### Use Cases
|
||||
|
||||
- Business process workflow orchestration
|
||||
- Formal verification via diagrammatic rewriting
|
||||
- Database query design
|
||||
- Petri net reachability and process modeling
|
||||
|
||||
---
|
||||
|
||||
## Tech Stack
|
||||
|
||||
**Primary Language**: Rust (2021 edition, Cargo-based)
|
||||
|
||||
### Key Dependencies
|
||||
|
||||
| Crate | Version | Purpose |
|
||||
|-------|---------|---------|
|
||||
| `chumsky` | 0.9 | Parser combinator library |
|
||||
| `ariadne` | 0.4 | Error reporting with source spans |
|
||||
| `rkyv` | 0.7 | Zero-copy serialization |
|
||||
| `rustyline` | 15 | REPL readline interface |
|
||||
| `egglog-union-find` | 1.0 | Union-find for congruence closure |
|
||||
| `roaring` | 0.10 | Bitmap library for sparse relations |
|
||||
| `indexmap` | 2.0 | Order-preserving hash maps |
|
||||
| `uuid` | 1 | UUID generation |
|
||||
| `memmap2` | 0.9 | Memory-mapped file I/O |
|
||||
|
||||
### Testing Frameworks
|
||||
|
||||
- `insta` — snapshot testing
|
||||
- `proptest` — property-based testing
|
||||
- `tempfile` — temporary directory management
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ USER INTERFACE │
|
||||
│ REPL (interactive CLI) | Batch file loading │
|
||||
├─────────────────────────────────────────────────────┤
|
||||
│ PARSING LAYER (Lexer → Parser → AST) │
|
||||
│ chumsky-based lexer & parser, source error reporting│
|
||||
├─────────────────────────────────────────────────────┤
|
||||
│ ELABORATION LAYER (AST → Core IR) │
|
||||
│ Type checking, name resolution, theory/instance │
|
||||
├─────────────────────────────────────────────────────┤
|
||||
│ CORE LAYER (Typed Representation) │
|
||||
│ Signature, Term, Formula, Structure, ElaboratedTheory│
|
||||
├─────────────────────────────────────────────────────┤
|
||||
│ STORAGE LAYER (Persistence) │
|
||||
│ Append-only GeologMeta store with version control │
|
||||
├─────────────────────────────────────────────────────┤
|
||||
│ QUERY & SOLVER LAYER (Execution) │
|
||||
│ Chase algorithm, congruence closure, relational │
|
||||
│ algebra compiler, SMT-style model enumeration │
|
||||
├─────────────────────────────────────────────────────┤
|
||||
│ TENSOR ALGEBRA (Axiom Checking) │
|
||||
│ Sparse tensor evaluation for axiom validation │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Directory Structure
|
||||
|
||||
| Path | Purpose |
|
||||
|------|---------|
|
||||
| `src/bin/geolog.rs` | CLI entry point |
|
||||
| `src/lib.rs` | Library root, exports `parse()` |
|
||||
| `src/repl.rs` | Interactive REPL state machine |
|
||||
| `src/lexer.rs` | Tokenization using chumsky |
|
||||
| `src/parser.rs` | Token stream → AST |
|
||||
| `src/ast.rs` | Abstract syntax tree types |
|
||||
| `src/core.rs` | Core IR: Signature, Term, Formula, Structure |
|
||||
| `src/elaborate/` | AST → Core elaboration |
|
||||
| `src/store/` | Persistence layer (append-only) |
|
||||
| `src/query/` | Chase algorithm, relational algebra |
|
||||
| `src/solver/` | SMT-style model enumeration |
|
||||
| `src/tensor/` | Sparse tensor algebra for axiom checking |
|
||||
| `src/cc.rs` | Congruence closure (union-find) |
|
||||
| `src/id.rs` | Luid/Slid identity system |
|
||||
| `src/universe.rs` | Global element registry |
|
||||
| `examples/geolog/` | 30+ example `.geolog` files |
|
||||
| `tests/` | 25+ test files |
|
||||
| `docs/` | ARCHITECTURE.md, SYNTAX.md |
|
||||
| `proofs/` | Lean4 formalization |
|
||||
| `fuzz/` | Fuzzing targets |
|
||||
|
||||
---
|
||||
|
||||
## Main Components
|
||||
|
||||
### Parsing & Syntax (~1,200 lines)
|
||||
|
||||
- `lexer.rs` — tokenization
|
||||
- `parser.rs` — token stream → AST
|
||||
- `ast.rs` — AST types (Theory, Instance, Axiom, etc.)
|
||||
- `error.rs` — error formatting with source spans
|
||||
- `pretty.rs` — Core → Geolog source roundtrip printing
|
||||
|
||||
### Elaboration (~2,200 lines)
|
||||
|
||||
- `elaborate/mod.rs` — coordination
|
||||
- `elaborate/theory.rs` — AST Theory → Core ElaboratedTheory
|
||||
- `elaborate/instance.rs` — AST Instance → Core Structure
|
||||
- `elaborate/env.rs` — environment with theory registry
|
||||
- `elaborate/types.rs` — type expression evaluation
|
||||
- `elaborate/error.rs` — type error reporting
|
||||
|
||||
### Core Representation
|
||||
|
||||
- `core.rs` — DerivedSort, Signature, Structure, Formula, Term, Sequent
|
||||
- `id.rs` — Luid (global unique ID) and Slid (structure-local ID)
|
||||
- `universe.rs` — global element registry with UUID ↔ Luid mapping
|
||||
- `naming.rs` — bidirectional name ↔ Luid mapping
|
||||
|
||||
### Storage Layer (~1,500 lines)
|
||||
|
||||
- `store/mod.rs` — main Store struct
|
||||
- `store/schema.rs` — cached sort/function/relation IDs
|
||||
- `store/append.rs` — low-level element append operations
|
||||
- `store/theory.rs` — theory CRUD
|
||||
- `store/instance.rs` — instance CRUD
|
||||
- `store/commit.rs` — git-like version control
|
||||
- `store/materialize.rs` — indexed views for fast lookups
|
||||
|
||||
### Query & Compilation (~3,500 lines)
|
||||
|
||||
- `query/compile.rs` — Query → RelAlgIR plan compilation
|
||||
- `query/to_relalg.rs` — Query → Relational Algebra IR
|
||||
- `query/from_relalg.rs` — RelAlgIR → Executable QueryOp
|
||||
- `query/chase.rs` — chase algorithm for fixpoint computation
|
||||
- `query/backend.rs` — naive QueryOp executor
|
||||
- `query/optimize.rs` — algebraic law rewriting
|
||||
|
||||
### Solver & Model Enumeration (~1,300 lines)
|
||||
|
||||
- `solver/mod.rs` — unified model enumeration API
|
||||
- `solver/tree.rs` — explicit search tree for partial models
|
||||
- `solver/tactics.rs` — automated search strategies:
|
||||
- CheckTactic: axiom validation
|
||||
- ForwardChainingTactic: Datalog-style inference
|
||||
- PropagateEquationsTactic: congruence closure
|
||||
- AutoTactic: composite fixpoint solver
|
||||
- `solver/types.rs` — SearchNode, Obligation, NodeStatus types
|
||||
|
||||
### Tensor Algebra (~2,600 lines)
|
||||
|
||||
- `tensor/expr.rs` — lazy tensor expression trees
|
||||
- `tensor/sparse.rs` — sparse tensor storage (RoaringBitmap-based)
|
||||
- `tensor/builder.rs` — expression builders
|
||||
- `tensor/compile.rs` — Formula → TensorExpr compilation
|
||||
- `tensor/check.rs` — axiom checking via tensor evaluation
|
||||
|
||||
---
|
||||
|
||||
## Key Entry Points
|
||||
|
||||
1. **CLI**: `src/bin/geolog.rs`
|
||||
```
|
||||
Usage: geolog [-d <workspace>] [source_files...]
|
||||
```
|
||||
|
||||
2. **Parse Entry**: `src/lib.rs` exports `parse(input: &str) → Result<File, String>`
|
||||
|
||||
3. **REPL State**: `src/repl.rs` — `ReplState::process_line()`
|
||||
|
||||
4. **Theory Elaboration**: `elaborate/theory.rs::elaborate_theory()`
|
||||
|
||||
5. **Instance Elaboration**: `elaborate/instance.rs::elaborate_instance_ctx()`
|
||||
|
||||
6. **Chase Algorithm**: `query/chase.rs::chase_fixpoint_with_cc()`
|
||||
|
||||
7. **Model Enumeration**: `solver/mod.rs::enumerate_models()`
|
||||
|
||||
---
|
||||
|
||||
## Design Decisions
|
||||
|
||||
### Geometric Logic Foundation
|
||||
|
||||
- **Axioms as Sequents**: `forall vars. premises |- conclusion`
|
||||
- **Positive Conclusions**: Can have existentials, disjunctions, but never negations
|
||||
- **Geometric Morphisms**: Preserved by design, enabling category-theoretic semantics
|
||||
|
||||
### Identity System
|
||||
|
||||
- **Luid** ("Local Universe ID"): Globally unique across all structures
|
||||
- **Slid** ("Structure-Local ID"): Index within a single structure
|
||||
- Bidirectional mapping enables persistent identity despite structure changes
|
||||
|
||||
### Append-Only Storage
|
||||
|
||||
- **GeologMeta**: Single homoiconic theory instance storing all data
|
||||
- **Patch-based Versioning**: Each commit is a delta from parent
|
||||
- **Never Delete**: Elements only tombstoned for perfect audit trails
|
||||
|
||||
### Type System
|
||||
|
||||
- **Postfix Application**: `x f` not `f(x)` — categorical style
|
||||
- **Derived Sorts**: Products of base sorts for record domains
|
||||
- **Product Domains**: Functions can take record arguments: `[x: M, y: M] -> M`
|
||||
- **Relations → Prop**: Relations are functions to `Prop` (boolean predicates)
|
||||
|
||||
### Chase Algorithm
|
||||
|
||||
- **Fixpoint Iteration**: Derives all consequences until closure
|
||||
- **Congruence Closure Integration**: Merges elements when axioms conclude `x = y`
|
||||
- **Termination for Unit Laws**: Categories with unit laws no longer loop forever
|
||||
- Uses tensor algebra for efficient axiom checking
|
||||
|
||||
### Solver Architecture
|
||||
|
||||
- **Explicit Search Tree**: Not implicit in call stack (AI-friendly for agent control)
|
||||
- **Refinement Preorder**: Structures can grow (carriers, functions, relations)
|
||||
- **Obligations vs Unsat**: Axiom obligation = need to witness conclusion (NOT failure)
|
||||
- **True Unsat**: Only when deriving `⊢ False` from instantiated axioms
|
||||
- **Tactics-based**: AutoTactic composes multiple tactics
|
||||
|
||||
### Relational Algebra Compilation
|
||||
|
||||
- **QueryOp Intermediate**: SQL-like operators (Scan, Filter, Join, Project, etc.)
|
||||
- **Optimization Passes**: Filter fusion, projection pushdown
|
||||
- **Store-aware**: Compiled directly to GeologMeta queries with indexing
|
||||
|
||||
### Tensor Algebra for Axiom Checking
|
||||
|
||||
- **Sparse Representation**: Roaring Bitmaps for efficient membership
|
||||
- **Lazy Expression Trees**: Tensor products fused with contractions
|
||||
- **Boolean Semiring**: AND for product, OR for sum
|
||||
|
||||
---
|
||||
|
||||
## REPL Commands
|
||||
|
||||
```
|
||||
:list, :inspect <name> - Introspection
|
||||
:add, :assert, :retract - Mutations
|
||||
:query, :explain, :compile - Query analysis
|
||||
:chase, :solve, :extend - Inference
|
||||
:commit, :history - Version control
|
||||
:source <file> - Load programs
|
||||
:help - Show help
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Parameterized Theories
|
||||
|
||||
Theories can be parameterized by other instances:
|
||||
|
||||
```geolog
|
||||
theory (N : PetriNet instance) Marking {
|
||||
token : Sort;
|
||||
token/of : token -> N/P;
|
||||
}
|
||||
```
|
||||
|
||||
This enables rich type-theoretic modeling (e.g., Petri net reachability as dependent types).
|
||||
|
||||
---
|
||||
|
||||
## Testing Infrastructure
|
||||
|
||||
- **Property-based tests** (`proptest`): naming, overlay, patches, queries, structure, tensor, universe, solver
|
||||
- **Unit tests**: parsing, elaboration, meta, pretty-printing, relations, version control, workspace
|
||||
- **Integration tests**: 30+ `.geolog` example files
|
||||
- **Fuzzing**: `fuzz/` directory with parser and REPL fuzzing targets
|
||||
|
||||
---
|
||||
|
||||
## Project Status
|
||||
|
||||
**Version**: 0.1.0 (Early production)
|
||||
|
||||
### Completed
|
||||
|
||||
- Core geometric logic implementation
|
||||
- Parser, elaborator, and core IR
|
||||
- Chase algorithm with equality saturation
|
||||
- Solver with SMT-like model enumeration
|
||||
- Persistence and version control
|
||||
- Comprehensive test coverage
|
||||
|
||||
### Active Development
|
||||
|
||||
- Nested instance elaboration
|
||||
- Homoiconic query plan representation
|
||||
- Disjunction variable alignment for tensor builder
|
||||
- Lean4 formalization of monotonic submodel proofs
|
||||
|
||||
---
|
||||
|
||||
## Key Files Reference
|
||||
|
||||
| File | Line Count (approx) | Description |
|
||||
|------|---------------------|-------------|
|
||||
| `src/core.rs` | ~800 | Core type definitions |
|
||||
| `src/parser.rs` | ~600 | Parser implementation |
|
||||
| `src/repl.rs` | ~1000 | REPL state machine |
|
||||
| `src/query/chase.rs` | ~500 | Chase algorithm |
|
||||
| `src/solver/mod.rs` | ~400 | Model enumeration API |
|
||||
| `src/tensor/sparse.rs` | ~600 | Sparse tensor storage |
|
||||
| `src/store/mod.rs` | ~400 | Storage coordination |
|
||||
227
architecture.dot
Normal file
227
architecture.dot
Normal file
@ -0,0 +1,227 @@
|
||||
digraph GeologArchitecture {
|
||||
rankdir=TB;
|
||||
compound=true;
|
||||
fontname="Helvetica";
|
||||
node [fontname="Helvetica", shape=box, style="rounded,filled", fillcolor="#f0f0f0"];
|
||||
edge [fontname="Helvetica"];
|
||||
|
||||
label="Geolog Architecture";
|
||||
labelloc="t";
|
||||
fontsize=24;
|
||||
|
||||
// User Interface Layer
|
||||
subgraph cluster_ui {
|
||||
label="User Interface";
|
||||
style="rounded,filled";
|
||||
fillcolor="#e3f2fd";
|
||||
|
||||
cli [label="CLI\n(bin/geolog.rs)", fillcolor="#bbdefb"];
|
||||
repl [label="REPL\n(repl.rs)", fillcolor="#bbdefb"];
|
||||
batch [label="Batch Loading\n(.geolog files)", fillcolor="#bbdefb"];
|
||||
}
|
||||
|
||||
// Parsing Layer
|
||||
subgraph cluster_parsing {
|
||||
label="Parsing Layer";
|
||||
style="rounded,filled";
|
||||
fillcolor="#e8f5e9";
|
||||
|
||||
lexer [label="Lexer\n(lexer.rs)", fillcolor="#c8e6c9"];
|
||||
parser [label="Parser\n(parser.rs)", fillcolor="#c8e6c9"];
|
||||
ast [label="AST\n(ast.rs)", fillcolor="#c8e6c9"];
|
||||
error [label="Error Reporting\n(error.rs)\nariadne", fillcolor="#c8e6c9"];
|
||||
pretty [label="Pretty Printer\n(pretty.rs)", fillcolor="#c8e6c9"];
|
||||
}
|
||||
|
||||
// Elaboration Layer
|
||||
subgraph cluster_elaboration {
|
||||
label="Elaboration Layer";
|
||||
style="rounded,filled";
|
||||
fillcolor="#fff3e0";
|
||||
|
||||
elab_theory [label="Theory Elaboration\n(elaborate/theory.rs)", fillcolor="#ffe0b2"];
|
||||
elab_instance [label="Instance Elaboration\n(elaborate/instance.rs)", fillcolor="#ffe0b2"];
|
||||
elab_env [label="Environment\n(elaborate/env.rs)", fillcolor="#ffe0b2"];
|
||||
elab_types [label="Type Evaluation\n(elaborate/types.rs)", fillcolor="#ffe0b2"];
|
||||
elab_error [label="Type Errors\n(elaborate/error.rs)", fillcolor="#ffe0b2"];
|
||||
}
|
||||
|
||||
// Core Layer
|
||||
subgraph cluster_core {
|
||||
label="Core Layer";
|
||||
style="rounded,filled";
|
||||
fillcolor="#fce4ec";
|
||||
|
||||
core [label="Core IR\n(core.rs)\nSignature, Term,\nFormula, Structure", fillcolor="#f8bbd9"];
|
||||
id [label="Identity System\n(id.rs)\nLuid, Slid", fillcolor="#f8bbd9"];
|
||||
universe [label="Universe\n(universe.rs)\nUUID <-> Luid", fillcolor="#f8bbd9"];
|
||||
naming [label="Naming\n(naming.rs)\nName <-> Luid", fillcolor="#f8bbd9"];
|
||||
cc [label="Congruence Closure\n(cc.rs)\nUnion-Find", fillcolor="#f8bbd9"];
|
||||
}
|
||||
|
||||
// Storage Layer
|
||||
subgraph cluster_storage {
|
||||
label="Storage Layer";
|
||||
style="rounded,filled";
|
||||
fillcolor="#e1f5fe";
|
||||
|
||||
store [label="Store\n(store/mod.rs)", fillcolor="#b3e5fc"];
|
||||
store_schema [label="Schema Cache\n(store/schema.rs)", fillcolor="#b3e5fc"];
|
||||
store_append [label="Append Operations\n(store/append.rs)", fillcolor="#b3e5fc"];
|
||||
store_theory [label="Theory CRUD\n(store/theory.rs)", fillcolor="#b3e5fc"];
|
||||
store_instance [label="Instance CRUD\n(store/instance.rs)", fillcolor="#b3e5fc"];
|
||||
store_commit [label="Version Control\n(store/commit.rs)", fillcolor="#b3e5fc"];
|
||||
store_materialize [label="Materialized Views\n(store/materialize.rs)", fillcolor="#b3e5fc"];
|
||||
geologmeta [label="GeologMeta\n(Homoiconic Store)", fillcolor="#81d4fa", style="rounded,filled,bold"];
|
||||
}
|
||||
|
||||
// Query Layer
|
||||
subgraph cluster_query {
|
||||
label="Query & Compilation Layer";
|
||||
style="rounded,filled";
|
||||
fillcolor="#f3e5f5";
|
||||
|
||||
query_compile [label="Query Compiler\n(query/compile.rs)", fillcolor="#e1bee7"];
|
||||
query_relalg [label="Relational Algebra IR\n(query/to_relalg.rs)\n(query/from_relalg.rs)", fillcolor="#e1bee7"];
|
||||
query_chase [label="Chase Algorithm\n(query/chase.rs)\nFixpoint + CC", fillcolor="#ce93d8", style="rounded,filled,bold"];
|
||||
query_backend [label="Query Backend\n(query/backend.rs)", fillcolor="#e1bee7"];
|
||||
query_optimize [label="Optimizer\n(query/optimize.rs)", fillcolor="#e1bee7"];
|
||||
}
|
||||
|
||||
// Solver Layer
|
||||
subgraph cluster_solver {
|
||||
label="Solver Layer";
|
||||
style="rounded,filled";
|
||||
fillcolor="#e0f2f1";
|
||||
|
||||
solver [label="Model Enumeration\n(solver/mod.rs)", fillcolor="#b2dfdb"];
|
||||
solver_tree [label="Search Tree\n(solver/tree.rs)", fillcolor="#b2dfdb"];
|
||||
solver_tactics [label="Tactics\n(solver/tactics.rs)\nCheck, Forward,\nPropagate, Auto", fillcolor="#80cbc4", style="rounded,filled,bold"];
|
||||
solver_types [label="Solver Types\n(solver/types.rs)", fillcolor="#b2dfdb"];
|
||||
}
|
||||
|
||||
// Tensor Layer
|
||||
subgraph cluster_tensor {
|
||||
label="Tensor Algebra Layer";
|
||||
style="rounded,filled";
|
||||
fillcolor="#fff8e1";
|
||||
|
||||
tensor_expr [label="Tensor Expressions\n(tensor/expr.rs)", fillcolor="#ffecb3"];
|
||||
tensor_sparse [label="Sparse Storage\n(tensor/sparse.rs)\nRoaringBitmap", fillcolor="#ffe082", style="rounded,filled,bold"];
|
||||
tensor_builder [label="Expression Builder\n(tensor/builder.rs)", fillcolor="#ffecb3"];
|
||||
tensor_compile [label="Formula Compiler\n(tensor/compile.rs)", fillcolor="#ffecb3"];
|
||||
tensor_check [label="Axiom Checker\n(tensor/check.rs)", fillcolor="#ffecb3"];
|
||||
}
|
||||
|
||||
// External Dependencies (simplified)
|
||||
subgraph cluster_deps {
|
||||
label="Key Dependencies";
|
||||
style="rounded,dashed";
|
||||
fillcolor="#fafafa";
|
||||
|
||||
chumsky [label="chumsky\n(parser combinators)", shape=ellipse, fillcolor="#e0e0e0"];
|
||||
rkyv [label="rkyv\n(zero-copy serde)", shape=ellipse, fillcolor="#e0e0e0"];
|
||||
roaring [label="roaring\n(bitmaps)", shape=ellipse, fillcolor="#e0e0e0"];
|
||||
unionfind [label="egglog-union-find", shape=ellipse, fillcolor="#e0e0e0"];
|
||||
}
|
||||
|
||||
// Data Flow Edges
|
||||
|
||||
// UI to Parsing
|
||||
cli -> repl;
|
||||
batch -> repl;
|
||||
repl -> lexer [lhead=cluster_parsing];
|
||||
|
||||
// Parsing flow
|
||||
lexer -> parser;
|
||||
parser -> ast;
|
||||
ast -> error [style=dashed, label="errors"];
|
||||
ast -> pretty [style=dashed, label="roundtrip"];
|
||||
|
||||
// Parsing to Elaboration
|
||||
ast -> elab_theory;
|
||||
ast -> elab_instance;
|
||||
|
||||
// Elaboration internal
|
||||
elab_theory -> elab_env;
|
||||
elab_instance -> elab_env;
|
||||
elab_env -> elab_types;
|
||||
elab_types -> elab_error [style=dashed];
|
||||
|
||||
// Elaboration to Core
|
||||
elab_theory -> core;
|
||||
elab_instance -> core;
|
||||
|
||||
// Core internal
|
||||
core -> id;
|
||||
id -> universe;
|
||||
id -> naming;
|
||||
core -> cc;
|
||||
|
||||
// Core to Storage
|
||||
core -> store [lhead=cluster_storage];
|
||||
|
||||
// Storage internal
|
||||
store -> store_schema;
|
||||
store -> store_append;
|
||||
store -> store_theory;
|
||||
store -> store_instance;
|
||||
store -> store_commit;
|
||||
store -> store_materialize;
|
||||
store_append -> geologmeta;
|
||||
store_theory -> geologmeta;
|
||||
store_instance -> geologmeta;
|
||||
store_commit -> geologmeta;
|
||||
store_materialize -> geologmeta;
|
||||
|
||||
// Query layer connections
|
||||
repl -> query_compile [label="queries"];
|
||||
query_compile -> query_relalg;
|
||||
query_relalg -> query_optimize;
|
||||
query_optimize -> query_backend;
|
||||
query_backend -> store [label="execute"];
|
||||
|
||||
// Chase
|
||||
repl -> query_chase [label=":chase"];
|
||||
query_chase -> cc [label="equality\nsaturation"];
|
||||
query_chase -> store;
|
||||
query_chase -> tensor_check [label="axiom\nchecking"];
|
||||
|
||||
// Solver connections
|
||||
repl -> solver [label=":solve\n:query"];
|
||||
solver -> solver_tree;
|
||||
solver_tree -> solver_tactics;
|
||||
solver_tactics -> solver_types;
|
||||
solver_tactics -> query_chase [label="forward\nchaining"];
|
||||
solver_tactics -> cc [label="propagate\nequations"];
|
||||
solver_tactics -> tensor_check [label="check\naxioms"];
|
||||
solver -> store;
|
||||
|
||||
// Tensor internal
|
||||
tensor_compile -> tensor_expr;
|
||||
tensor_expr -> tensor_builder;
|
||||
tensor_builder -> tensor_sparse;
|
||||
tensor_check -> tensor_compile;
|
||||
tensor_sparse -> core [label="read\nstructure"];
|
||||
|
||||
// Dependencies
|
||||
lexer -> chumsky [style=dotted];
|
||||
parser -> chumsky [style=dotted];
|
||||
store -> rkyv [style=dotted];
|
||||
tensor_sparse -> roaring [style=dotted];
|
||||
cc -> unionfind [style=dotted];
|
||||
|
||||
// Legend
|
||||
subgraph cluster_legend {
|
||||
label="Legend";
|
||||
style="rounded";
|
||||
fillcolor="white";
|
||||
|
||||
legend_data [label="Data Flow", shape=plaintext];
|
||||
legend_dep [label="Dependency", shape=plaintext];
|
||||
legend_key [label="Key Component", fillcolor="#80cbc4", style="rounded,filled,bold"];
|
||||
|
||||
legend_data -> legend_dep [style=invis];
|
||||
legend_dep -> legend_key [style=invis];
|
||||
}
|
||||
}
|
||||
770
architecture.svg
Normal file
770
architecture.svg
Normal file
@ -0,0 +1,770 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Generated by graphviz version 12.2.1 (0)
|
||||
-->
|
||||
<!-- Title: GeologArchitecture Pages: 1 -->
|
||||
<svg width="2545pt" height="1858pt"
|
||||
viewBox="0.00 0.00 2545.00 1857.98" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 1853.98)">
|
||||
<title>GeologArchitecture</title>
|
||||
<polygon fill="white" stroke="none" points="-4,4 -4,-1853.98 2541,-1853.98 2541,4 -4,4"/>
|
||||
<text text-anchor="middle" x="1268.5" y="-1823.18" font-family="Helvetica,sans-Serif" font-size="24.00">Geolog Architecture</text>
|
||||
<g id="clust1" class="cluster">
|
||||
<title>cluster_ui</title>
|
||||
<path fill="#e3f2fd" stroke="black" d="M700,-1630.98C700,-1630.98 943,-1630.98 943,-1630.98 949,-1630.98 955,-1636.98 955,-1642.98 955,-1642.98 955,-1793.48 955,-1793.48 955,-1799.48 949,-1805.48 943,-1805.48 943,-1805.48 700,-1805.48 700,-1805.48 694,-1805.48 688,-1799.48 688,-1793.48 688,-1793.48 688,-1642.98 688,-1642.98 688,-1636.98 694,-1630.98 700,-1630.98"/>
|
||||
<text text-anchor="middle" x="821.5" y="-1778.68" font-family="Helvetica,sans-Serif" font-size="24.00">User Interface</text>
|
||||
</g>
|
||||
<g id="clust2" class="cluster">
|
||||
<title>cluster_parsing</title>
|
||||
<path fill="#e8f5e9" stroke="black" d="M978,-523.73C978,-523.73 1218,-523.73 1218,-523.73 1224,-523.73 1230,-529.73 1230,-535.73 1230,-535.73 1230,-938.1 1230,-938.1 1230,-944.1 1224,-950.1 1218,-950.1 1218,-950.1 978,-950.1 978,-950.1 972,-950.1 966,-944.1 966,-938.1 966,-938.1 966,-535.73 966,-535.73 966,-529.73 972,-523.73 978,-523.73"/>
|
||||
<text text-anchor="middle" x="1098" y="-923.3" font-family="Helvetica,sans-Serif" font-size="24.00">Parsing Layer</text>
|
||||
</g>
|
||||
<g id="clust3" class="cluster">
|
||||
<title>cluster_elaboration</title>
|
||||
<path fill="#fff3e0" stroke="black" d="M2177,-96.3C2177,-96.3 2517,-96.3 2517,-96.3 2523,-96.3 2529,-102.3 2529,-108.3 2529,-108.3 2529,-615.35 2529,-615.35 2529,-621.35 2523,-627.35 2517,-627.35 2517,-627.35 2177,-627.35 2177,-627.35 2171,-627.35 2165,-621.35 2165,-615.35 2165,-615.35 2165,-108.3 2165,-108.3 2165,-102.3 2171,-96.3 2177,-96.3"/>
|
||||
<text text-anchor="middle" x="2347" y="-600.55" font-family="Helvetica,sans-Serif" font-size="24.00">Elaboration Layer</text>
|
||||
</g>
|
||||
<g id="clust4" class="cluster">
|
||||
<title>cluster_core</title>
|
||||
<path fill="#fce4ec" stroke="black" d="M70,-87.68C70,-87.68 362,-87.68 362,-87.68 368,-87.68 374,-93.68 374,-99.68 374,-99.68 374,-459.23 374,-459.23 374,-465.23 368,-471.23 362,-471.23 362,-471.23 70,-471.23 70,-471.23 64,-471.23 58,-465.23 58,-459.23 58,-459.23 58,-99.68 58,-99.68 58,-93.68 64,-87.68 70,-87.68"/>
|
||||
<text text-anchor="middle" x="216" y="-444.43" font-family="Helvetica,sans-Serif" font-size="24.00">Core Layer</text>
|
||||
</g>
|
||||
<g id="clust5" class="cluster">
|
||||
<title>cluster_storage</title>
|
||||
<path fill="#e1f5fe" stroke="black" d="M1210,-8C1210,-8 2145,-8 2145,-8 2151,-8 2157,-14 2157,-20 2157,-20 2157,-310.73 2157,-310.73 2157,-316.73 2151,-322.73 2145,-322.73 2145,-322.73 1210,-322.73 1210,-322.73 1204,-322.73 1198,-316.73 1198,-310.73 1198,-310.73 1198,-20 1198,-20 1198,-14 1204,-8 1210,-8"/>
|
||||
<text text-anchor="middle" x="1677.5" y="-295.93" font-family="Helvetica,sans-Serif" font-size="24.00">Storage Layer</text>
|
||||
</g>
|
||||
<g id="clust6" class="cluster">
|
||||
<title>cluster_query</title>
|
||||
<path fill="#f3e5f5" stroke="black" d="M579,-775.6C579,-775.6 897,-775.6 897,-775.6 903,-775.6 909,-781.6 909,-787.6 909,-787.6 909,-1225.48 909,-1225.48 909,-1231.48 903,-1237.48 897,-1237.48 897,-1237.48 579,-1237.48 579,-1237.48 573,-1237.48 567,-1231.48 567,-1225.48 567,-1225.48 567,-787.6 567,-787.6 567,-781.6 573,-775.6 579,-775.6"/>
|
||||
<text text-anchor="middle" x="738" y="-1210.68" font-family="Helvetica,sans-Serif" font-size="24.00">Query & Compilation Layer</text>
|
||||
</g>
|
||||
<g id="clust7" class="cluster">
|
||||
<title>cluster_solver</title>
|
||||
<path fill="#e0f2f1" stroke="black" d="M32,-1133.85C32,-1133.85 176,-1133.85 176,-1133.85 182,-1133.85 188,-1139.85 188,-1145.85 188,-1145.85 188,-1566.48 188,-1566.48 188,-1572.48 182,-1578.48 176,-1578.48 176,-1578.48 32,-1578.48 32,-1578.48 26,-1578.48 20,-1572.48 20,-1566.48 20,-1566.48 20,-1145.85 20,-1145.85 20,-1139.85 26,-1133.85 32,-1133.85"/>
|
||||
<text text-anchor="middle" x="104" y="-1551.68" font-family="Helvetica,sans-Serif" font-size="24.00">Solver Layer</text>
|
||||
</g>
|
||||
<g id="clust8" class="cluster">
|
||||
<title>cluster_tensor</title>
|
||||
<path fill="#fff8e1" stroke="black" d="M302,-523.73C302,-523.73 547,-523.73 547,-523.73 553,-523.73 559,-529.73 559,-535.73 559,-535.73 559,-1060.73 559,-1060.73 559,-1066.73 553,-1072.73 547,-1072.73 547,-1072.73 302,-1072.73 302,-1072.73 296,-1072.73 290,-1066.73 290,-1060.73 290,-1060.73 290,-535.73 290,-535.73 290,-529.73 296,-523.73 302,-523.73"/>
|
||||
<text text-anchor="middle" x="424.5" y="-1045.93" font-family="Helvetica,sans-Serif" font-size="24.00">Tensor Algebra Layer</text>
|
||||
</g>
|
||||
<g id="clust9" class="cluster">
|
||||
<title>cluster_deps</title>
|
||||
<path fill="none" stroke="black" stroke-dasharray="5,2" d="M394,-87.5C394,-87.5 1178,-87.5 1178,-87.5 1184,-87.5 1190,-93.5 1190,-99.5 1190,-99.5 1190,-188.1 1190,-188.1 1190,-194.1 1184,-200.1 1178,-200.1 1178,-200.1 394,-200.1 394,-200.1 388,-200.1 382,-194.1 382,-188.1 382,-188.1 382,-99.5 382,-99.5 382,-93.5 388,-87.5 394,-87.5"/>
|
||||
<text text-anchor="middle" x="786" y="-173.3" font-family="Helvetica,sans-Serif" font-size="24.00">Key Dependencies</text>
|
||||
</g>
|
||||
<g id="clust10" class="cluster">
|
||||
<title>cluster_legend</title>
|
||||
<path fill="none" stroke="black" d="M999,-1486.73C999,-1486.73 1117,-1486.73 1117,-1486.73 1123,-1486.73 1129,-1492.73 1129,-1498.73 1129,-1498.73 1129,-1790.23 1129,-1790.23 1129,-1796.23 1123,-1802.23 1117,-1802.23 1117,-1802.23 999,-1802.23 999,-1802.23 993,-1802.23 987,-1796.23 987,-1790.23 987,-1790.23 987,-1498.73 987,-1498.73 987,-1492.73 993,-1486.73 999,-1486.73"/>
|
||||
<text text-anchor="middle" x="1058" y="-1775.43" font-family="Helvetica,sans-Serif" font-size="24.00">Legend</text>
|
||||
</g>
|
||||
<!-- cli -->
|
||||
<g id="node1" class="node">
|
||||
<title>cli</title>
|
||||
<path fill="#bbdefb" stroke="black" d="M801.62,-1760.98C801.62,-1760.98 708.38,-1760.98 708.38,-1760.98 702.38,-1760.98 696.38,-1754.98 696.38,-1748.98 696.38,-1748.98 696.38,-1730.48 696.38,-1730.48 696.38,-1724.48 702.38,-1718.48 708.38,-1718.48 708.38,-1718.48 801.62,-1718.48 801.62,-1718.48 807.62,-1718.48 813.62,-1724.48 813.62,-1730.48 813.62,-1730.48 813.62,-1748.98 813.62,-1748.98 813.62,-1754.98 807.62,-1760.98 801.62,-1760.98"/>
|
||||
<text text-anchor="middle" x="755" y="-1743.68" font-family="Helvetica,sans-Serif" font-size="14.00">CLI</text>
|
||||
<text text-anchor="middle" x="755" y="-1726.43" font-family="Helvetica,sans-Serif" font-size="14.00">(bin/geolog.rs)</text>
|
||||
</g>
|
||||
<!-- repl -->
|
||||
<g id="node2" class="node">
|
||||
<title>repl</title>
|
||||
<path fill="#bbdefb" stroke="black" d="M808.38,-1681.48C808.38,-1681.48 761.62,-1681.48 761.62,-1681.48 755.62,-1681.48 749.62,-1675.48 749.62,-1669.48 749.62,-1669.48 749.62,-1650.98 749.62,-1650.98 749.62,-1644.98 755.62,-1638.98 761.62,-1638.98 761.62,-1638.98 808.38,-1638.98 808.38,-1638.98 814.38,-1638.98 820.38,-1644.98 820.38,-1650.98 820.38,-1650.98 820.38,-1669.48 820.38,-1669.48 820.38,-1675.48 814.38,-1681.48 808.38,-1681.48"/>
|
||||
<text text-anchor="middle" x="785" y="-1664.18" font-family="Helvetica,sans-Serif" font-size="14.00">REPL</text>
|
||||
<text text-anchor="middle" x="785" y="-1646.93" font-family="Helvetica,sans-Serif" font-size="14.00">(repl.rs)</text>
|
||||
</g>
|
||||
<!-- cli->repl -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>cli->repl</title>
|
||||
<path fill="none" stroke="black" d="M762.88,-1718.37C765.96,-1710.42 769.55,-1701.13 772.94,-1692.38"/>
|
||||
<polygon fill="black" stroke="black" points="776.17,-1693.73 776.52,-1683.14 769.65,-1691.2 776.17,-1693.73"/>
|
||||
</g>
|
||||
<!-- lexer -->
|
||||
<g id="node4" class="node">
|
||||
<title>lexer</title>
|
||||
<path fill="#c8e6c9" stroke="black" d="M1070.38,-905.6C1070.38,-905.6 1017.62,-905.6 1017.62,-905.6 1011.62,-905.6 1005.62,-899.6 1005.62,-893.6 1005.62,-893.6 1005.62,-875.1 1005.62,-875.1 1005.62,-869.1 1011.62,-863.1 1017.62,-863.1 1017.62,-863.1 1070.38,-863.1 1070.38,-863.1 1076.38,-863.1 1082.38,-869.1 1082.38,-875.1 1082.38,-875.1 1082.38,-893.6 1082.38,-893.6 1082.38,-899.6 1076.38,-905.6 1070.38,-905.6"/>
|
||||
<text text-anchor="middle" x="1044" y="-888.3" font-family="Helvetica,sans-Serif" font-size="14.00">Lexer</text>
|
||||
<text text-anchor="middle" x="1044" y="-871.05" font-family="Helvetica,sans-Serif" font-size="14.00">(lexer.rs)</text>
|
||||
</g>
|
||||
<!-- repl->lexer -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>repl->lexer</title>
|
||||
<path fill="none" stroke="black" d="M820.63,-1646.74C869.94,-1626.9 953,-1583.18 953,-1513.73 953,-1513.73 953,-1513.73 953,-1005.98 953,-989.54 958.45,-973.92 966.69,-959.71"/>
|
||||
<polygon fill="black" stroke="black" points="969.57,-961.69 971.99,-951.38 963.67,-957.94 969.57,-961.69"/>
|
||||
</g>
|
||||
<!-- query_compile -->
|
||||
<g id="node27" class="node">
|
||||
<title>query_compile</title>
|
||||
<path fill="#e1bee7" stroke="black" d="M856.75,-1184.35C856.75,-1184.35 737.25,-1184.35 737.25,-1184.35 731.25,-1184.35 725.25,-1178.35 725.25,-1172.35 725.25,-1172.35 725.25,-1153.85 725.25,-1153.85 725.25,-1147.85 731.25,-1141.85 737.25,-1141.85 737.25,-1141.85 856.75,-1141.85 856.75,-1141.85 862.75,-1141.85 868.75,-1147.85 868.75,-1153.85 868.75,-1153.85 868.75,-1172.35 868.75,-1172.35 868.75,-1178.35 862.75,-1184.35 856.75,-1184.35"/>
|
||||
<text text-anchor="middle" x="797" y="-1167.05" font-family="Helvetica,sans-Serif" font-size="14.00">Query Compiler</text>
|
||||
<text text-anchor="middle" x="797" y="-1149.8" font-family="Helvetica,sans-Serif" font-size="14.00">(query/compile.rs)</text>
|
||||
</g>
|
||||
<!-- repl->query_compile -->
|
||||
<g id="edge32" class="edge">
|
||||
<title>repl->query_compile</title>
|
||||
<path fill="none" stroke="black" d="M786.39,-1638.52C788.16,-1610.31 791,-1558.23 791,-1513.73 791,-1513.73 791,-1513.73 791,-1335.48 791,-1286.74 793.45,-1230.26 795.22,-1195.87"/>
|
||||
<polygon fill="black" stroke="black" points="798.7,-1196.35 795.73,-1186.18 791.71,-1195.98 798.7,-1196.35"/>
|
||||
<text text-anchor="middle" x="816.88" y="-1428.55" font-family="Helvetica,sans-Serif" font-size="14.00">queries</text>
|
||||
</g>
|
||||
<!-- query_chase -->
|
||||
<g id="node29" class="node">
|
||||
<title>query_chase</title>
|
||||
<path fill="#ce93d8" stroke="black" stroke-width="2" d="M694.75,-1192.98C694.75,-1192.98 587.25,-1192.98 587.25,-1192.98 581.25,-1192.98 575.25,-1186.98 575.25,-1180.98 575.25,-1180.98 575.25,-1145.23 575.25,-1145.23 575.25,-1139.23 581.25,-1133.23 587.25,-1133.23 587.25,-1133.23 694.75,-1133.23 694.75,-1133.23 700.75,-1133.23 706.75,-1139.23 706.75,-1145.23 706.75,-1145.23 706.75,-1180.98 706.75,-1180.98 706.75,-1186.98 700.75,-1192.98 694.75,-1192.98"/>
|
||||
<text text-anchor="middle" x="641" y="-1175.68" font-family="Helvetica,sans-Serif" font-size="14.00">Chase Algorithm</text>
|
||||
<text text-anchor="middle" x="641" y="-1158.43" font-family="Helvetica,sans-Serif" font-size="14.00">(query/chase.rs)</text>
|
||||
<text text-anchor="middle" x="641" y="-1141.18" font-family="Helvetica,sans-Serif" font-size="14.00">Fixpoint + CC</text>
|
||||
</g>
|
||||
<!-- repl->query_chase -->
|
||||
<g id="edge37" class="edge">
|
||||
<title>repl->query_chase</title>
|
||||
<path fill="none" stroke="black" d="M754.69,-1638.68C721.52,-1613.48 673,-1567.44 673,-1513.73 673,-1513.73 673,-1513.73 673,-1335.48 673,-1289.93 661.94,-1238.71 652.83,-1204.23"/>
|
||||
<polygon fill="black" stroke="black" points="656.23,-1203.4 650.23,-1194.67 649.47,-1205.24 656.23,-1203.4"/>
|
||||
<text text-anchor="middle" x="695.5" y="-1428.55" font-family="Helvetica,sans-Serif" font-size="14.00">:chase</text>
|
||||
</g>
|
||||
<!-- solver -->
|
||||
<g id="node32" class="node">
|
||||
<title>solver</title>
|
||||
<path fill="#b2dfdb" stroke="black" d="M167.5,-1533.98C167.5,-1533.98 40.5,-1533.98 40.5,-1533.98 34.5,-1533.98 28.5,-1527.98 28.5,-1521.98 28.5,-1521.98 28.5,-1503.48 28.5,-1503.48 28.5,-1497.48 34.5,-1491.48 40.5,-1491.48 40.5,-1491.48 167.5,-1491.48 167.5,-1491.48 173.5,-1491.48 179.5,-1497.48 179.5,-1503.48 179.5,-1503.48 179.5,-1521.98 179.5,-1521.98 179.5,-1527.98 173.5,-1533.98 167.5,-1533.98"/>
|
||||
<text text-anchor="middle" x="104" y="-1516.68" font-family="Helvetica,sans-Serif" font-size="14.00">Model Enumeration</text>
|
||||
<text text-anchor="middle" x="104" y="-1499.43" font-family="Helvetica,sans-Serif" font-size="14.00">(solver/mod.rs)</text>
|
||||
</g>
|
||||
<!-- repl->solver -->
|
||||
<g id="edge41" class="edge">
|
||||
<title>repl->solver</title>
|
||||
<path fill="none" stroke="black" d="M749.35,-1651.61C645.62,-1629.45 341.82,-1564.54 191,-1532.32"/>
|
||||
<polygon fill="black" stroke="black" points="191.96,-1528.94 181.45,-1530.28 190.5,-1535.79 191.96,-1528.94"/>
|
||||
<text text-anchor="middle" x="624.86" y="-1607.68" font-family="Helvetica,sans-Serif" font-size="14.00">:solve</text>
|
||||
<text text-anchor="middle" x="624.86" y="-1590.43" font-family="Helvetica,sans-Serif" font-size="14.00">:query</text>
|
||||
</g>
|
||||
<!-- batch -->
|
||||
<g id="node3" class="node">
|
||||
<title>batch</title>
|
||||
<path fill="#bbdefb" stroke="black" d="M934.5,-1760.98C934.5,-1760.98 843.5,-1760.98 843.5,-1760.98 837.5,-1760.98 831.5,-1754.98 831.5,-1748.98 831.5,-1748.98 831.5,-1730.48 831.5,-1730.48 831.5,-1724.48 837.5,-1718.48 843.5,-1718.48 843.5,-1718.48 934.5,-1718.48 934.5,-1718.48 940.5,-1718.48 946.5,-1724.48 946.5,-1730.48 946.5,-1730.48 946.5,-1748.98 946.5,-1748.98 946.5,-1754.98 940.5,-1760.98 934.5,-1760.98"/>
|
||||
<text text-anchor="middle" x="889" y="-1743.68" font-family="Helvetica,sans-Serif" font-size="14.00">Batch Loading</text>
|
||||
<text text-anchor="middle" x="889" y="-1726.43" font-family="Helvetica,sans-Serif" font-size="14.00">(.geolog files)</text>
|
||||
</g>
|
||||
<!-- batch->repl -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>batch->repl</title>
|
||||
<path fill="none" stroke="black" d="M861.4,-1718.16C849.3,-1709.14 834.91,-1698.42 821.9,-1688.73"/>
|
||||
<polygon fill="black" stroke="black" points="824.05,-1685.96 813.94,-1682.79 819.87,-1691.58 824.05,-1685.96"/>
|
||||
</g>
|
||||
<!-- parser -->
|
||||
<g id="node5" class="node">
|
||||
<title>parser</title>
|
||||
<path fill="#c8e6c9" stroke="black" d="M1076,-826.1C1076,-826.1 1012,-826.1 1012,-826.1 1006,-826.1 1000,-820.1 1000,-814.1 1000,-814.1 1000,-795.6 1000,-795.6 1000,-789.6 1006,-783.6 1012,-783.6 1012,-783.6 1076,-783.6 1076,-783.6 1082,-783.6 1088,-789.6 1088,-795.6 1088,-795.6 1088,-814.1 1088,-814.1 1088,-820.1 1082,-826.1 1076,-826.1"/>
|
||||
<text text-anchor="middle" x="1044" y="-808.8" font-family="Helvetica,sans-Serif" font-size="14.00">Parser</text>
|
||||
<text text-anchor="middle" x="1044" y="-791.55" font-family="Helvetica,sans-Serif" font-size="14.00">(parser.rs)</text>
|
||||
</g>
|
||||
<!-- lexer->parser -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>lexer->parser</title>
|
||||
<path fill="none" stroke="black" d="M1044,-862.99C1044,-855.22 1044,-846.17 1044,-837.59"/>
|
||||
<polygon fill="black" stroke="black" points="1047.5,-837.86 1044,-827.86 1040.5,-837.86 1047.5,-837.86"/>
|
||||
</g>
|
||||
<!-- chumsky -->
|
||||
<g id="node41" class="node">
|
||||
<title>chumsky</title>
|
||||
<ellipse fill="#e0e0e0" stroke="black" cx="855" cy="-125.55" rx="114.73" ry="30.05"/>
|
||||
<text text-anchor="middle" x="855" y="-129.5" font-family="Helvetica,sans-Serif" font-size="14.00">chumsky</text>
|
||||
<text text-anchor="middle" x="855" y="-112.25" font-family="Helvetica,sans-Serif" font-size="14.00">(parser combinators)</text>
|
||||
</g>
|
||||
<!-- lexer->chumsky -->
|
||||
<g id="edge54" class="edge">
|
||||
<title>lexer->chumsky</title>
|
||||
<path fill="none" stroke="black" stroke-dasharray="1,5" d="M1082.75,-876.93C1142.42,-865.93 1250,-841.2 1250,-805.85 1250,-805.85 1250,-805.85 1250,-642.98 1250,-589.31 1262.21,-567.39 1231,-523.73 1125.1,-375.57 968.14,-484.5 869,-331.73 836.89,-282.24 840.39,-211.16 846.92,-166.98"/>
|
||||
<polygon fill="black" stroke="black" points="850.34,-167.73 848.48,-157.31 843.43,-166.62 850.34,-167.73"/>
|
||||
</g>
|
||||
<!-- ast -->
|
||||
<g id="node6" class="node">
|
||||
<title>ast</title>
|
||||
<path fill="#c8e6c9" stroke="black" d="M1140.75,-713.1C1140.75,-713.1 1099.25,-713.1 1099.25,-713.1 1093.25,-713.1 1087.25,-707.1 1087.25,-701.1 1087.25,-701.1 1087.25,-682.6 1087.25,-682.6 1087.25,-676.6 1093.25,-670.6 1099.25,-670.6 1099.25,-670.6 1140.75,-670.6 1140.75,-670.6 1146.75,-670.6 1152.75,-676.6 1152.75,-682.6 1152.75,-682.6 1152.75,-701.1 1152.75,-701.1 1152.75,-707.1 1146.75,-713.1 1140.75,-713.1"/>
|
||||
<text text-anchor="middle" x="1120" y="-695.8" font-family="Helvetica,sans-Serif" font-size="14.00">AST</text>
|
||||
<text text-anchor="middle" x="1120" y="-678.55" font-family="Helvetica,sans-Serif" font-size="14.00">(ast.rs)</text>
|
||||
</g>
|
||||
<!-- parser->ast -->
|
||||
<g id="edge5" class="edge">
|
||||
<title>parser->ast</title>
|
||||
<path fill="none" stroke="black" d="M1058.12,-783.24C1069.74,-766.26 1086.38,-741.96 1099.48,-722.83"/>
|
||||
<polygon fill="black" stroke="black" points="1102.34,-724.84 1105.1,-714.61 1096.57,-720.88 1102.34,-724.84"/>
|
||||
</g>
|
||||
<!-- parser->chumsky -->
|
||||
<g id="edge55" class="edge">
|
||||
<title>parser->chumsky</title>
|
||||
<path fill="none" stroke="black" stroke-dasharray="1,5" d="M1034.64,-783.37C1006.25,-721.65 921.63,-540.65 898,-523.73 874.24,-506.71 853.05,-535 833,-513.73 743.01,-418.29 802.74,-243.85 836.42,-165.95"/>
|
||||
<polygon fill="black" stroke="black" points="839.54,-167.56 840.37,-157 833.13,-164.74 839.54,-167.56"/>
|
||||
</g>
|
||||
<!-- error -->
|
||||
<g id="node7" class="node">
|
||||
<title>error</title>
|
||||
<path fill="#c8e6c9" stroke="black" d="M1083.88,-591.48C1083.88,-591.48 986.12,-591.48 986.12,-591.48 980.12,-591.48 974.12,-585.48 974.12,-579.48 974.12,-579.48 974.12,-543.73 974.12,-543.73 974.12,-537.73 980.12,-531.73 986.12,-531.73 986.12,-531.73 1083.88,-531.73 1083.88,-531.73 1089.88,-531.73 1095.88,-537.73 1095.88,-543.73 1095.88,-543.73 1095.88,-579.48 1095.88,-579.48 1095.88,-585.48 1089.88,-591.48 1083.88,-591.48"/>
|
||||
<text text-anchor="middle" x="1035" y="-574.18" font-family="Helvetica,sans-Serif" font-size="14.00">Error Reporting</text>
|
||||
<text text-anchor="middle" x="1035" y="-556.93" font-family="Helvetica,sans-Serif" font-size="14.00">(error.rs)</text>
|
||||
<text text-anchor="middle" x="1035" y="-539.68" font-family="Helvetica,sans-Serif" font-size="14.00">ariadne</text>
|
||||
</g>
|
||||
<!-- ast->error -->
|
||||
<g id="edge6" class="edge">
|
||||
<title>ast->error</title>
|
||||
<path fill="none" stroke="black" stroke-dasharray="5,2" d="M1090.43,-670.14C1084.3,-664.91 1078.27,-658.96 1073.5,-652.6 1062.21,-637.55 1053.48,-618.67 1047.22,-602.11"/>
|
||||
<polygon fill="black" stroke="black" points="1050.63,-601.27 1043.96,-593.04 1044.04,-603.63 1050.63,-601.27"/>
|
||||
<text text-anchor="middle" x="1093.75" y="-639.3" font-family="Helvetica,sans-Serif" font-size="14.00">errors</text>
|
||||
</g>
|
||||
<!-- pretty -->
|
||||
<g id="node8" class="node">
|
||||
<title>pretty</title>
|
||||
<path fill="#c8e6c9" stroke="black" d="M1209.75,-582.85C1209.75,-582.85 1126.25,-582.85 1126.25,-582.85 1120.25,-582.85 1114.25,-576.85 1114.25,-570.85 1114.25,-570.85 1114.25,-552.35 1114.25,-552.35 1114.25,-546.35 1120.25,-540.35 1126.25,-540.35 1126.25,-540.35 1209.75,-540.35 1209.75,-540.35 1215.75,-540.35 1221.75,-546.35 1221.75,-552.35 1221.75,-552.35 1221.75,-570.85 1221.75,-570.85 1221.75,-576.85 1215.75,-582.85 1209.75,-582.85"/>
|
||||
<text text-anchor="middle" x="1168" y="-565.55" font-family="Helvetica,sans-Serif" font-size="14.00">Pretty Printer</text>
|
||||
<text text-anchor="middle" x="1168" y="-548.3" font-family="Helvetica,sans-Serif" font-size="14.00">(pretty.rs)</text>
|
||||
</g>
|
||||
<!-- ast->pretty -->
|
||||
<g id="edge7" class="edge">
|
||||
<title>ast->pretty</title>
|
||||
<path fill="none" stroke="black" stroke-dasharray="5,2" d="M1117.96,-670.13C1117.55,-659.51 1118.08,-646.5 1121.5,-635.35 1126.2,-620.05 1135.07,-604.86 1143.81,-592.34"/>
|
||||
<polygon fill="black" stroke="black" points="1146.42,-594.7 1149.51,-584.56 1140.78,-590.56 1146.42,-594.7"/>
|
||||
<text text-anchor="middle" x="1153.75" y="-639.3" font-family="Helvetica,sans-Serif" font-size="14.00">roundtrip</text>
|
||||
</g>
|
||||
<!-- elab_theory -->
|
||||
<g id="node9" class="node">
|
||||
<title>elab_theory</title>
|
||||
<path fill="#ffe0b2" stroke="black" d="M2318.5,-582.85C2318.5,-582.85 2185.5,-582.85 2185.5,-582.85 2179.5,-582.85 2173.5,-576.85 2173.5,-570.85 2173.5,-570.85 2173.5,-552.35 2173.5,-552.35 2173.5,-546.35 2179.5,-540.35 2185.5,-540.35 2185.5,-540.35 2318.5,-540.35 2318.5,-540.35 2324.5,-540.35 2330.5,-546.35 2330.5,-552.35 2330.5,-552.35 2330.5,-570.85 2330.5,-570.85 2330.5,-576.85 2324.5,-582.85 2318.5,-582.85"/>
|
||||
<text text-anchor="middle" x="2252" y="-565.55" font-family="Helvetica,sans-Serif" font-size="14.00">Theory Elaboration</text>
|
||||
<text text-anchor="middle" x="2252" y="-548.3" font-family="Helvetica,sans-Serif" font-size="14.00">(elaborate/theory.rs)</text>
|
||||
</g>
|
||||
<!-- ast->elab_theory -->
|
||||
<g id="edge8" class="edge">
|
||||
<title>ast->elab_theory</title>
|
||||
<path fill="none" stroke="black" d="M1153,-671.96C1163.58,-665.84 1175.31,-658.99 1186,-652.6 1198.55,-645.11 1200.08,-639.83 1214,-635.35 1389.19,-579.03 1942.93,-566.28 2161.76,-563.42"/>
|
||||
<polygon fill="black" stroke="black" points="2161.71,-566.92 2171.67,-563.3 2161.62,-559.92 2161.71,-566.92"/>
|
||||
</g>
|
||||
<!-- elab_instance -->
|
||||
<g id="node10" class="node">
|
||||
<title>elab_instance</title>
|
||||
<path fill="#ffe0b2" stroke="black" d="M2509.38,-582.85C2509.38,-582.85 2360.62,-582.85 2360.62,-582.85 2354.62,-582.85 2348.62,-576.85 2348.62,-570.85 2348.62,-570.85 2348.62,-552.35 2348.62,-552.35 2348.62,-546.35 2354.62,-540.35 2360.62,-540.35 2360.62,-540.35 2509.38,-540.35 2509.38,-540.35 2515.38,-540.35 2521.38,-546.35 2521.38,-552.35 2521.38,-552.35 2521.38,-570.85 2521.38,-570.85 2521.38,-576.85 2515.38,-582.85 2509.38,-582.85"/>
|
||||
<text text-anchor="middle" x="2435" y="-565.55" font-family="Helvetica,sans-Serif" font-size="14.00">Instance Elaboration</text>
|
||||
<text text-anchor="middle" x="2435" y="-548.3" font-family="Helvetica,sans-Serif" font-size="14.00">(elaborate/instance.rs)</text>
|
||||
</g>
|
||||
<!-- ast->elab_instance -->
|
||||
<g id="edge9" class="edge">
|
||||
<title>ast->elab_instance</title>
|
||||
<path fill="none" stroke="black" d="M1153.1,-690.2C1336.6,-686.41 2224.75,-666.2 2340,-627.35 2363.35,-619.48 2386.28,-604.14 2403.76,-590.37"/>
|
||||
<polygon fill="black" stroke="black" points="2405.65,-593.34 2411.2,-584.31 2401.24,-587.91 2405.65,-593.34"/>
|
||||
</g>
|
||||
<!-- elab_env -->
|
||||
<g id="node11" class="node">
|
||||
<title>elab_env</title>
|
||||
<path fill="#ffe0b2" stroke="black" d="M2400.5,-409.48C2400.5,-409.48 2285.5,-409.48 2285.5,-409.48 2279.5,-409.48 2273.5,-403.48 2273.5,-397.48 2273.5,-397.48 2273.5,-378.98 2273.5,-378.98 2273.5,-372.98 2279.5,-366.98 2285.5,-366.98 2285.5,-366.98 2400.5,-366.98 2400.5,-366.98 2406.5,-366.98 2412.5,-372.98 2412.5,-378.98 2412.5,-378.98 2412.5,-397.48 2412.5,-397.48 2412.5,-403.48 2406.5,-409.48 2400.5,-409.48"/>
|
||||
<text text-anchor="middle" x="2343" y="-392.18" font-family="Helvetica,sans-Serif" font-size="14.00">Environment</text>
|
||||
<text text-anchor="middle" x="2343" y="-374.93" font-family="Helvetica,sans-Serif" font-size="14.00">(elaborate/env.rs)</text>
|
||||
</g>
|
||||
<!-- elab_theory->elab_env -->
|
||||
<g id="edge10" class="edge">
|
||||
<title>elab_theory->elab_env</title>
|
||||
<path fill="none" stroke="black" d="M2262.98,-539.93C2278.83,-510.08 2308.24,-454.69 2326.65,-420.02"/>
|
||||
<polygon fill="black" stroke="black" points="2329.68,-421.78 2331.27,-411.31 2323.49,-418.5 2329.68,-421.78"/>
|
||||
</g>
|
||||
<!-- core -->
|
||||
<g id="node14" class="node">
|
||||
<title>core</title>
|
||||
<path fill="#f8bbd9" stroke="black" d="M353.5,-426.73C353.5,-426.73 232.5,-426.73 232.5,-426.73 226.5,-426.73 220.5,-420.73 220.5,-414.73 220.5,-414.73 220.5,-361.73 220.5,-361.73 220.5,-355.73 226.5,-349.73 232.5,-349.73 232.5,-349.73 353.5,-349.73 353.5,-349.73 359.5,-349.73 365.5,-355.73 365.5,-361.73 365.5,-361.73 365.5,-414.73 365.5,-414.73 365.5,-420.73 359.5,-426.73 353.5,-426.73"/>
|
||||
<text text-anchor="middle" x="293" y="-409.43" font-family="Helvetica,sans-Serif" font-size="14.00">Core IR</text>
|
||||
<text text-anchor="middle" x="293" y="-392.18" font-family="Helvetica,sans-Serif" font-size="14.00">(core.rs)</text>
|
||||
<text text-anchor="middle" x="293" y="-374.93" font-family="Helvetica,sans-Serif" font-size="14.00">Signature, Term,</text>
|
||||
<text text-anchor="middle" x="293" y="-357.68" font-family="Helvetica,sans-Serif" font-size="14.00">Formula, Structure</text>
|
||||
</g>
|
||||
<!-- elab_theory->core -->
|
||||
<g id="edge14" class="edge">
|
||||
<title>elab_theory->core</title>
|
||||
<path fill="none" stroke="black" d="M2173.13,-555.7C2048.55,-547.8 1798.65,-531.37 1587,-513.73 1130.07,-475.65 583.43,-419.6 377.23,-398.09"/>
|
||||
<polygon fill="black" stroke="black" points="377.72,-394.62 367.41,-397.06 376.99,-401.58 377.72,-394.62"/>
|
||||
</g>
|
||||
<!-- elab_instance->elab_env -->
|
||||
<g id="edge11" class="edge">
|
||||
<title>elab_instance->elab_env</title>
|
||||
<path fill="none" stroke="black" d="M2423.9,-539.93C2407.88,-510.08 2378.14,-454.69 2359.53,-420.02"/>
|
||||
<polygon fill="black" stroke="black" points="2362.67,-418.46 2354.85,-411.31 2356.5,-421.77 2362.67,-418.46"/>
|
||||
</g>
|
||||
<!-- elab_instance->core -->
|
||||
<g id="edge15" class="edge">
|
||||
<title>elab_instance->core</title>
|
||||
<path fill="none" stroke="black" d="M2389.97,-539.89C2374.45,-533.63 2356.76,-527.44 2340,-523.73 1956.21,-438.74 712.3,-400.32 376.95,-391.34"/>
|
||||
<polygon fill="black" stroke="black" points="377.48,-387.86 367.39,-391.09 377.29,-394.85 377.48,-387.86"/>
|
||||
</g>
|
||||
<!-- elab_types -->
|
||||
<g id="node12" class="node">
|
||||
<title>elab_types</title>
|
||||
<path fill="#ffe0b2" stroke="black" d="M2407.25,-278.23C2407.25,-278.23 2278.75,-278.23 2278.75,-278.23 2272.75,-278.23 2266.75,-272.23 2266.75,-266.23 2266.75,-266.23 2266.75,-247.73 2266.75,-247.73 2266.75,-241.73 2272.75,-235.73 2278.75,-235.73 2278.75,-235.73 2407.25,-235.73 2407.25,-235.73 2413.25,-235.73 2419.25,-241.73 2419.25,-247.73 2419.25,-247.73 2419.25,-266.23 2419.25,-266.23 2419.25,-272.23 2413.25,-278.23 2407.25,-278.23"/>
|
||||
<text text-anchor="middle" x="2343" y="-260.93" font-family="Helvetica,sans-Serif" font-size="14.00">Type Evaluation</text>
|
||||
<text text-anchor="middle" x="2343" y="-243.68" font-family="Helvetica,sans-Serif" font-size="14.00">(elaborate/types.rs)</text>
|
||||
</g>
|
||||
<!-- elab_env->elab_types -->
|
||||
<g id="edge12" class="edge">
|
||||
<title>elab_env->elab_types</title>
|
||||
<path fill="none" stroke="black" d="M2343,-366.75C2343,-346.05 2343,-313.76 2343,-289.74"/>
|
||||
<polygon fill="black" stroke="black" points="2346.5,-289.88 2343,-279.88 2339.5,-289.88 2346.5,-289.88"/>
|
||||
</g>
|
||||
<!-- elab_error -->
|
||||
<g id="node13" class="node">
|
||||
<title>elab_error</title>
|
||||
<path fill="#ffe0b2" stroke="black" d="M2403.88,-146.8C2403.88,-146.8 2282.12,-146.8 2282.12,-146.8 2276.12,-146.8 2270.12,-140.8 2270.12,-134.8 2270.12,-134.8 2270.12,-116.3 2270.12,-116.3 2270.12,-110.3 2276.12,-104.3 2282.12,-104.3 2282.12,-104.3 2403.88,-104.3 2403.88,-104.3 2409.88,-104.3 2415.88,-110.3 2415.88,-116.3 2415.88,-116.3 2415.88,-134.8 2415.88,-134.8 2415.88,-140.8 2409.88,-146.8 2403.88,-146.8"/>
|
||||
<text text-anchor="middle" x="2343" y="-129.5" font-family="Helvetica,sans-Serif" font-size="14.00">Type Errors</text>
|
||||
<text text-anchor="middle" x="2343" y="-112.25" font-family="Helvetica,sans-Serif" font-size="14.00">(elaborate/error.rs)</text>
|
||||
</g>
|
||||
<!-- elab_types->elab_error -->
|
||||
<g id="edge13" class="edge">
|
||||
<title>elab_types->elab_error</title>
|
||||
<path fill="none" stroke="black" stroke-dasharray="5,2" d="M2343,-235.47C2343,-214.74 2343,-182.41 2343,-158.35"/>
|
||||
<polygon fill="black" stroke="black" points="2346.5,-158.48 2343,-148.48 2339.5,-158.48 2346.5,-158.48"/>
|
||||
</g>
|
||||
<!-- id -->
|
||||
<g id="node15" class="node">
|
||||
<title>id</title>
|
||||
<path fill="#f8bbd9" stroke="black" d="M179.75,-286.85C179.75,-286.85 78.25,-286.85 78.25,-286.85 72.25,-286.85 66.25,-280.85 66.25,-274.85 66.25,-274.85 66.25,-239.1 66.25,-239.1 66.25,-233.1 72.25,-227.1 78.25,-227.1 78.25,-227.1 179.75,-227.1 179.75,-227.1 185.75,-227.1 191.75,-233.1 191.75,-239.1 191.75,-239.1 191.75,-274.85 191.75,-274.85 191.75,-280.85 185.75,-286.85 179.75,-286.85"/>
|
||||
<text text-anchor="middle" x="129" y="-269.55" font-family="Helvetica,sans-Serif" font-size="14.00">Identity System</text>
|
||||
<text text-anchor="middle" x="129" y="-252.3" font-family="Helvetica,sans-Serif" font-size="14.00">(id.rs)</text>
|
||||
<text text-anchor="middle" x="129" y="-235.05" font-family="Helvetica,sans-Serif" font-size="14.00">Luid, Slid</text>
|
||||
</g>
|
||||
<!-- core->id -->
|
||||
<g id="edge16" class="edge">
|
||||
<title>core->id</title>
|
||||
<path fill="none" stroke="black" d="M236.75,-349.41C224.75,-340.9 212.3,-331.71 201,-322.73 190.1,-314.06 178.72,-304.23 168.31,-294.89"/>
|
||||
<polygon fill="black" stroke="black" points="170.71,-292.33 160.95,-288.21 166,-297.52 170.71,-292.33"/>
|
||||
</g>
|
||||
<!-- cc -->
|
||||
<g id="node18" class="node">
|
||||
<title>cc</title>
|
||||
<path fill="#f8bbd9" stroke="black" d="M354.12,-286.85C354.12,-286.85 221.88,-286.85 221.88,-286.85 215.88,-286.85 209.88,-280.85 209.88,-274.85 209.88,-274.85 209.88,-239.1 209.88,-239.1 209.88,-233.1 215.88,-227.1 221.88,-227.1 221.88,-227.1 354.12,-227.1 354.12,-227.1 360.12,-227.1 366.12,-233.1 366.12,-239.1 366.12,-239.1 366.12,-274.85 366.12,-274.85 366.12,-280.85 360.12,-286.85 354.12,-286.85"/>
|
||||
<text text-anchor="middle" x="288" y="-269.55" font-family="Helvetica,sans-Serif" font-size="14.00">Congruence Closure</text>
|
||||
<text text-anchor="middle" x="288" y="-252.3" font-family="Helvetica,sans-Serif" font-size="14.00">(cc.rs)</text>
|
||||
<text text-anchor="middle" x="288" y="-235.05" font-family="Helvetica,sans-Serif" font-size="14.00">Union-Find</text>
|
||||
</g>
|
||||
<!-- core->cc -->
|
||||
<g id="edge19" class="edge">
|
||||
<title>core->cc</title>
|
||||
<path fill="none" stroke="black" d="M291.54,-349.5C290.92,-333.55 290.2,-314.94 289.57,-298.68"/>
|
||||
<polygon fill="black" stroke="black" points="293.08,-298.7 289.19,-288.84 286.08,-298.97 293.08,-298.7"/>
|
||||
</g>
|
||||
<!-- store -->
|
||||
<g id="node19" class="node">
|
||||
<title>store</title>
|
||||
<path fill="#b3e5fc" stroke="black" d="M1319.12,-278.23C1319.12,-278.23 1228.88,-278.23 1228.88,-278.23 1222.88,-278.23 1216.88,-272.23 1216.88,-266.23 1216.88,-266.23 1216.88,-247.73 1216.88,-247.73 1216.88,-241.73 1222.88,-235.73 1228.88,-235.73 1228.88,-235.73 1319.12,-235.73 1319.12,-235.73 1325.12,-235.73 1331.12,-241.73 1331.12,-247.73 1331.12,-247.73 1331.12,-266.23 1331.12,-266.23 1331.12,-272.23 1325.12,-278.23 1319.12,-278.23"/>
|
||||
<text text-anchor="middle" x="1274" y="-260.93" font-family="Helvetica,sans-Serif" font-size="14.00">Store</text>
|
||||
<text text-anchor="middle" x="1274" y="-243.68" font-family="Helvetica,sans-Serif" font-size="14.00">(store/mod.rs)</text>
|
||||
</g>
|
||||
<!-- core->store -->
|
||||
<g id="edge20" class="edge">
|
||||
<title>core->store</title>
|
||||
<path fill="none" stroke="black" d="M365.93,-377.62C543.07,-354.28 995.45,-294.68 1186.62,-269.49"/>
|
||||
<polygon fill="black" stroke="black" points="1187.04,-272.97 1196.5,-268.19 1186.13,-266.03 1187.04,-272.97"/>
|
||||
</g>
|
||||
<!-- universe -->
|
||||
<g id="node16" class="node">
|
||||
<title>universe</title>
|
||||
<path fill="#f8bbd9" stroke="black" d="M174.75,-155.43C174.75,-155.43 79.25,-155.43 79.25,-155.43 73.25,-155.43 67.25,-149.43 67.25,-143.43 67.25,-143.43 67.25,-107.68 67.25,-107.68 67.25,-101.68 73.25,-95.68 79.25,-95.68 79.25,-95.68 174.75,-95.68 174.75,-95.68 180.75,-95.68 186.75,-101.68 186.75,-107.68 186.75,-107.68 186.75,-143.43 186.75,-143.43 186.75,-149.43 180.75,-155.43 174.75,-155.43"/>
|
||||
<text text-anchor="middle" x="127" y="-138.13" font-family="Helvetica,sans-Serif" font-size="14.00">Universe</text>
|
||||
<text text-anchor="middle" x="127" y="-120.88" font-family="Helvetica,sans-Serif" font-size="14.00">(universe.rs)</text>
|
||||
<text text-anchor="middle" x="127" y="-103.63" font-family="Helvetica,sans-Serif" font-size="14.00">UUID <-> Luid</text>
|
||||
</g>
|
||||
<!-- id->universe -->
|
||||
<g id="edge17" class="edge">
|
||||
<title>id->universe</title>
|
||||
<path fill="none" stroke="black" d="M128.55,-226.61C128.28,-209.07 127.93,-186.53 127.63,-167.29"/>
|
||||
<polygon fill="black" stroke="black" points="131.13,-167.26 127.48,-157.32 124.13,-167.37 131.13,-167.26"/>
|
||||
</g>
|
||||
<!-- naming -->
|
||||
<g id="node17" class="node">
|
||||
<title>naming</title>
|
||||
<path fill="#f8bbd9" stroke="black" d="M317,-155.43C317,-155.43 217,-155.43 217,-155.43 211,-155.43 205,-149.43 205,-143.43 205,-143.43 205,-107.68 205,-107.68 205,-101.68 211,-95.68 217,-95.68 217,-95.68 317,-95.68 317,-95.68 323,-95.68 329,-101.68 329,-107.68 329,-107.68 329,-143.43 329,-143.43 329,-149.43 323,-155.43 317,-155.43"/>
|
||||
<text text-anchor="middle" x="267" y="-138.13" font-family="Helvetica,sans-Serif" font-size="14.00">Naming</text>
|
||||
<text text-anchor="middle" x="267" y="-120.88" font-family="Helvetica,sans-Serif" font-size="14.00">(naming.rs)</text>
|
||||
<text text-anchor="middle" x="267" y="-103.63" font-family="Helvetica,sans-Serif" font-size="14.00">Name <-> Luid</text>
|
||||
</g>
|
||||
<!-- id->naming -->
|
||||
<g id="edge18" class="edge">
|
||||
<title>id->naming</title>
|
||||
<path fill="none" stroke="black" d="M165.43,-226.81C175.52,-218.4 186.35,-209.08 196,-200.1 208.13,-188.82 220.86,-175.99 232.13,-164.24"/>
|
||||
<polygon fill="black" stroke="black" points="234.65,-166.66 239.01,-157 229.58,-161.84 234.65,-166.66"/>
|
||||
</g>
|
||||
<!-- unionfind -->
|
||||
<g id="node44" class="node">
|
||||
<title>unionfind</title>
|
||||
<ellipse fill="#e0e0e0" stroke="black" cx="624" cy="-125.55" rx="98.03" ry="18"/>
|
||||
<text text-anchor="middle" x="624" y="-120.88" font-family="Helvetica,sans-Serif" font-size="14.00">egglog-union-find</text>
|
||||
</g>
|
||||
<!-- cc->unionfind -->
|
||||
<g id="edge58" class="edge">
|
||||
<title>cc->unionfind</title>
|
||||
<path fill="none" stroke="black" stroke-dasharray="1,5" d="M366.38,-244.14C411.71,-235.52 469.09,-221.54 517,-200.1 545.55,-187.33 574.58,-166.8 595.19,-150.67"/>
|
||||
<polygon fill="black" stroke="black" points="597.22,-153.53 602.85,-144.55 592.85,-148.06 597.22,-153.53"/>
|
||||
</g>
|
||||
<!-- store_schema -->
|
||||
<g id="node20" class="node">
|
||||
<title>store_schema</title>
|
||||
<path fill="#b3e5fc" stroke="black" d="M1663.75,-146.8C1663.75,-146.8 1550.25,-146.8 1550.25,-146.8 1544.25,-146.8 1538.25,-140.8 1538.25,-134.8 1538.25,-134.8 1538.25,-116.3 1538.25,-116.3 1538.25,-110.3 1544.25,-104.3 1550.25,-104.3 1550.25,-104.3 1663.75,-104.3 1663.75,-104.3 1669.75,-104.3 1675.75,-110.3 1675.75,-116.3 1675.75,-116.3 1675.75,-134.8 1675.75,-134.8 1675.75,-140.8 1669.75,-146.8 1663.75,-146.8"/>
|
||||
<text text-anchor="middle" x="1607" y="-129.5" font-family="Helvetica,sans-Serif" font-size="14.00">Schema Cache</text>
|
||||
<text text-anchor="middle" x="1607" y="-112.25" font-family="Helvetica,sans-Serif" font-size="14.00">(store/schema.rs)</text>
|
||||
</g>
|
||||
<!-- store->store_schema -->
|
||||
<g id="edge21" class="edge">
|
||||
<title>store->store_schema</title>
|
||||
<path fill="none" stroke="black" d="M1331.45,-252.2C1385.21,-246.69 1466.38,-233.17 1529,-200.1 1549.83,-189.1 1569.13,-171 1583.32,-155.51"/>
|
||||
<polygon fill="black" stroke="black" points="1585.64,-158.17 1589.65,-148.37 1580.4,-153.53 1585.64,-158.17"/>
|
||||
</g>
|
||||
<!-- store_append -->
|
||||
<g id="node21" class="node">
|
||||
<title>store_append</title>
|
||||
<path fill="#b3e5fc" stroke="black" d="M1832.5,-146.8C1832.5,-146.8 1705.5,-146.8 1705.5,-146.8 1699.5,-146.8 1693.5,-140.8 1693.5,-134.8 1693.5,-134.8 1693.5,-116.3 1693.5,-116.3 1693.5,-110.3 1699.5,-104.3 1705.5,-104.3 1705.5,-104.3 1832.5,-104.3 1832.5,-104.3 1838.5,-104.3 1844.5,-110.3 1844.5,-116.3 1844.5,-116.3 1844.5,-134.8 1844.5,-134.8 1844.5,-140.8 1838.5,-146.8 1832.5,-146.8"/>
|
||||
<text text-anchor="middle" x="1769" y="-129.5" font-family="Helvetica,sans-Serif" font-size="14.00">Append Operations</text>
|
||||
<text text-anchor="middle" x="1769" y="-112.25" font-family="Helvetica,sans-Serif" font-size="14.00">(store/append.rs)</text>
|
||||
</g>
|
||||
<!-- store->store_append -->
|
||||
<g id="edge22" class="edge">
|
||||
<title>store->store_append</title>
|
||||
<path fill="none" stroke="black" d="M1331.38,-252.27C1429.08,-245.27 1623.15,-228.26 1685,-200.1 1707.84,-189.71 1729.05,-171.02 1744.45,-155.09"/>
|
||||
<polygon fill="black" stroke="black" points="1746.65,-157.86 1750.92,-148.17 1741.53,-153.08 1746.65,-157.86"/>
|
||||
</g>
|
||||
<!-- store_theory -->
|
||||
<g id="node22" class="node">
|
||||
<title>store_theory</title>
|
||||
<path fill="#b3e5fc" stroke="black" d="M1977.12,-146.8C1977.12,-146.8 1874.88,-146.8 1874.88,-146.8 1868.88,-146.8 1862.88,-140.8 1862.88,-134.8 1862.88,-134.8 1862.88,-116.3 1862.88,-116.3 1862.88,-110.3 1868.88,-104.3 1874.88,-104.3 1874.88,-104.3 1977.12,-104.3 1977.12,-104.3 1983.12,-104.3 1989.12,-110.3 1989.12,-116.3 1989.12,-116.3 1989.12,-134.8 1989.12,-134.8 1989.12,-140.8 1983.12,-146.8 1977.12,-146.8"/>
|
||||
<text text-anchor="middle" x="1926" y="-129.5" font-family="Helvetica,sans-Serif" font-size="14.00">Theory CRUD</text>
|
||||
<text text-anchor="middle" x="1926" y="-112.25" font-family="Helvetica,sans-Serif" font-size="14.00">(store/theory.rs)</text>
|
||||
</g>
|
||||
<!-- store->store_theory -->
|
||||
<g id="edge23" class="edge">
|
||||
<title>store->store_theory</title>
|
||||
<path fill="none" stroke="black" d="M1331.54,-255.4C1459.31,-253.32 1762.62,-243.8 1854,-200.1 1874.62,-190.24 1892.61,-171.88 1905.45,-155.97"/>
|
||||
<polygon fill="black" stroke="black" points="1908.13,-158.22 1911.47,-148.16 1902.59,-153.94 1908.13,-158.22"/>
|
||||
</g>
|
||||
<!-- store_instance -->
|
||||
<g id="node23" class="node">
|
||||
<title>store_instance</title>
|
||||
<path fill="#b3e5fc" stroke="black" d="M2137,-146.8C2137,-146.8 2019,-146.8 2019,-146.8 2013,-146.8 2007,-140.8 2007,-134.8 2007,-134.8 2007,-116.3 2007,-116.3 2007,-110.3 2013,-104.3 2019,-104.3 2019,-104.3 2137,-104.3 2137,-104.3 2143,-104.3 2149,-110.3 2149,-116.3 2149,-116.3 2149,-134.8 2149,-134.8 2149,-140.8 2143,-146.8 2137,-146.8"/>
|
||||
<text text-anchor="middle" x="2078" y="-129.5" font-family="Helvetica,sans-Serif" font-size="14.00">Instance CRUD</text>
|
||||
<text text-anchor="middle" x="2078" y="-112.25" font-family="Helvetica,sans-Serif" font-size="14.00">(store/instance.rs)</text>
|
||||
</g>
|
||||
<!-- store->store_instance -->
|
||||
<g id="edge24" class="edge">
|
||||
<title>store->store_instance</title>
|
||||
<path fill="none" stroke="black" d="M1331.62,-256.34C1481.24,-256.53 1878.71,-251.98 1998,-200.1 2020.34,-190.39 2040.49,-171.77 2054.98,-155.7"/>
|
||||
<polygon fill="black" stroke="black" points="2057.53,-158.09 2061.43,-148.24 2052.24,-153.51 2057.53,-158.09"/>
|
||||
</g>
|
||||
<!-- store_commit -->
|
||||
<g id="node24" class="node">
|
||||
<title>store_commit</title>
|
||||
<path fill="#b3e5fc" stroke="black" d="M1329.62,-146.8C1329.62,-146.8 1218.38,-146.8 1218.38,-146.8 1212.38,-146.8 1206.38,-140.8 1206.38,-134.8 1206.38,-134.8 1206.38,-116.3 1206.38,-116.3 1206.38,-110.3 1212.38,-104.3 1218.38,-104.3 1218.38,-104.3 1329.62,-104.3 1329.62,-104.3 1335.62,-104.3 1341.62,-110.3 1341.62,-116.3 1341.62,-116.3 1341.62,-134.8 1341.62,-134.8 1341.62,-140.8 1335.62,-146.8 1329.62,-146.8"/>
|
||||
<text text-anchor="middle" x="1274" y="-129.5" font-family="Helvetica,sans-Serif" font-size="14.00">Version Control</text>
|
||||
<text text-anchor="middle" x="1274" y="-112.25" font-family="Helvetica,sans-Serif" font-size="14.00">(store/commit.rs)</text>
|
||||
</g>
|
||||
<!-- store->store_commit -->
|
||||
<g id="edge25" class="edge">
|
||||
<title>store->store_commit</title>
|
||||
<path fill="none" stroke="black" d="M1274,-235.47C1274,-214.74 1274,-182.41 1274,-158.35"/>
|
||||
<polygon fill="black" stroke="black" points="1277.5,-158.48 1274,-148.48 1270.5,-158.48 1277.5,-158.48"/>
|
||||
</g>
|
||||
<!-- store_materialize -->
|
||||
<g id="node25" class="node">
|
||||
<title>store_materialize</title>
|
||||
<path fill="#b3e5fc" stroke="black" d="M1508,-146.8C1508,-146.8 1372,-146.8 1372,-146.8 1366,-146.8 1360,-140.8 1360,-134.8 1360,-134.8 1360,-116.3 1360,-116.3 1360,-110.3 1366,-104.3 1372,-104.3 1372,-104.3 1508,-104.3 1508,-104.3 1514,-104.3 1520,-110.3 1520,-116.3 1520,-116.3 1520,-134.8 1520,-134.8 1520,-140.8 1514,-146.8 1508,-146.8"/>
|
||||
<text text-anchor="middle" x="1440" y="-129.5" font-family="Helvetica,sans-Serif" font-size="14.00">Materialized Views</text>
|
||||
<text text-anchor="middle" x="1440" y="-112.25" font-family="Helvetica,sans-Serif" font-size="14.00">(store/materialize.rs)</text>
|
||||
</g>
|
||||
<!-- store->store_materialize -->
|
||||
<g id="edge26" class="edge">
|
||||
<title>store->store_materialize</title>
|
||||
<path fill="none" stroke="black" d="M1303.2,-235.44C1317.73,-225.06 1335.46,-212.14 1351,-200.1 1369.85,-185.5 1390.4,-168.58 1407,-154.66"/>
|
||||
<polygon fill="black" stroke="black" points="1409.23,-157.36 1414.62,-148.24 1404.72,-152 1409.23,-157.36"/>
|
||||
</g>
|
||||
<!-- rkyv -->
|
||||
<g id="node42" class="node">
|
||||
<title>rkyv</title>
|
||||
<ellipse fill="#e0e0e0" stroke="black" cx="1085" cy="-125.55" rx="97.23" ry="30.05"/>
|
||||
<text text-anchor="middle" x="1085" y="-129.5" font-family="Helvetica,sans-Serif" font-size="14.00">rkyv</text>
|
||||
<text text-anchor="middle" x="1085" y="-112.25" font-family="Helvetica,sans-Serif" font-size="14.00">(zero-copy serde)</text>
|
||||
</g>
|
||||
<!-- store->rkyv -->
|
||||
<g id="edge56" class="edge">
|
||||
<title>store->rkyv</title>
|
||||
<path fill="none" stroke="black" stroke-dasharray="1,5" d="M1244.05,-235.47C1214.34,-215.12 1168.31,-183.6 1133.4,-159.69"/>
|
||||
<polygon fill="black" stroke="black" points="1135.8,-157.1 1125.57,-154.34 1131.85,-162.87 1135.8,-157.1"/>
|
||||
</g>
|
||||
<!-- geologmeta -->
|
||||
<g id="node26" class="node">
|
||||
<title>geologmeta</title>
|
||||
<path fill="#81d4fa" stroke="black" stroke-width="2" d="M1830.62,-58.5C1830.62,-58.5 1707.38,-58.5 1707.38,-58.5 1701.38,-58.5 1695.38,-52.5 1695.38,-46.5 1695.38,-46.5 1695.38,-28 1695.38,-28 1695.38,-22 1701.38,-16 1707.38,-16 1707.38,-16 1830.62,-16 1830.62,-16 1836.62,-16 1842.62,-22 1842.62,-28 1842.62,-28 1842.62,-46.5 1842.62,-46.5 1842.62,-52.5 1836.62,-58.5 1830.62,-58.5"/>
|
||||
<text text-anchor="middle" x="1769" y="-41.2" font-family="Helvetica,sans-Serif" font-size="14.00">GeologMeta</text>
|
||||
<text text-anchor="middle" x="1769" y="-23.95" font-family="Helvetica,sans-Serif" font-size="14.00">(Homoiconic Store)</text>
|
||||
</g>
|
||||
<!-- store_append->geologmeta -->
|
||||
<g id="edge27" class="edge">
|
||||
<title>store_append->geologmeta</title>
|
||||
<path fill="none" stroke="black" d="M1769,-104.1C1769,-93.96 1769,-81.49 1769,-70.14"/>
|
||||
<polygon fill="black" stroke="black" points="1772.5,-70.37 1769,-60.37 1765.5,-70.37 1772.5,-70.37"/>
|
||||
</g>
|
||||
<!-- store_theory->geologmeta -->
|
||||
<g id="edge28" class="edge">
|
||||
<title>store_theory->geologmeta</title>
|
||||
<path fill="none" stroke="black" d="M1888.39,-103.88C1866.82,-92.02 1839.53,-77.02 1816.49,-64.35"/>
|
||||
<polygon fill="black" stroke="black" points="1818.35,-61.38 1807.9,-59.63 1814.97,-67.52 1818.35,-61.38"/>
|
||||
</g>
|
||||
<!-- store_instance->geologmeta -->
|
||||
<g id="edge29" class="edge">
|
||||
<title>store_instance->geologmeta</title>
|
||||
<path fill="none" stroke="black" d="M2038.01,-103.92C2025.44,-98.01 2011.35,-91.97 1998,-87.5 1951.44,-71.92 1897.85,-60.09 1854.23,-51.92"/>
|
||||
<polygon fill="black" stroke="black" points="1855.04,-48.51 1844.57,-50.15 1853.77,-55.4 1855.04,-48.51"/>
|
||||
</g>
|
||||
<!-- store_commit->geologmeta -->
|
||||
<g id="edge30" class="edge">
|
||||
<title>store_commit->geologmeta</title>
|
||||
<path fill="none" stroke="black" d="M1310.61,-103.88C1323.08,-97.68 1337.32,-91.48 1351,-87.5 1463.03,-54.93 1597.91,-43.83 1683.56,-40.09"/>
|
||||
<polygon fill="black" stroke="black" points="1683.69,-43.59 1693.54,-39.68 1683.4,-36.59 1683.69,-43.59"/>
|
||||
</g>
|
||||
<!-- store_materialize->geologmeta -->
|
||||
<g id="edge31" class="edge">
|
||||
<title>store_materialize->geologmeta</title>
|
||||
<path fill="none" stroke="black" d="M1484.66,-103.99C1498.65,-98.09 1514.28,-92.03 1529,-87.5 1579.52,-71.96 1637.53,-59.9 1683.87,-51.61"/>
|
||||
<polygon fill="black" stroke="black" points="1684.35,-55.08 1693.59,-49.9 1683.14,-48.18 1684.35,-55.08"/>
|
||||
</g>
|
||||
<!-- query_relalg -->
|
||||
<g id="node28" class="node">
|
||||
<title>query_relalg</title>
|
||||
<path fill="#e1bee7" stroke="black" d="M868.38,-1036.85C868.38,-1036.85 725.62,-1036.85 725.62,-1036.85 719.62,-1036.85 713.62,-1030.85 713.62,-1024.85 713.62,-1024.85 713.62,-989.1 713.62,-989.1 713.62,-983.1 719.62,-977.1 725.62,-977.1 725.62,-977.1 868.38,-977.1 868.38,-977.1 874.38,-977.1 880.38,-983.1 880.38,-989.1 880.38,-989.1 880.38,-1024.85 880.38,-1024.85 880.38,-1030.85 874.38,-1036.85 868.38,-1036.85"/>
|
||||
<text text-anchor="middle" x="797" y="-1019.55" font-family="Helvetica,sans-Serif" font-size="14.00">Relational Algebra IR</text>
|
||||
<text text-anchor="middle" x="797" y="-1002.3" font-family="Helvetica,sans-Serif" font-size="14.00">(query/to_relalg.rs)</text>
|
||||
<text text-anchor="middle" x="797" y="-985.05" font-family="Helvetica,sans-Serif" font-size="14.00">(query/from_relalg.rs)</text>
|
||||
</g>
|
||||
<!-- query_compile->query_relalg -->
|
||||
<g id="edge33" class="edge">
|
||||
<title>query_compile->query_relalg</title>
|
||||
<path fill="none" stroke="black" d="M797,-1141.64C797,-1118 797,-1078.53 797,-1048.59"/>
|
||||
<polygon fill="black" stroke="black" points="800.5,-1048.75 797,-1038.75 793.5,-1048.75 800.5,-1048.75"/>
|
||||
</g>
|
||||
<!-- query_optimize -->
|
||||
<g id="node31" class="node">
|
||||
<title>query_optimize</title>
|
||||
<path fill="#e1bee7" stroke="black" d="M866.38,-905.6C866.38,-905.6 741.62,-905.6 741.62,-905.6 735.62,-905.6 729.62,-899.6 729.62,-893.6 729.62,-893.6 729.62,-875.1 729.62,-875.1 729.62,-869.1 735.62,-863.1 741.62,-863.1 741.62,-863.1 866.38,-863.1 866.38,-863.1 872.38,-863.1 878.38,-869.1 878.38,-875.1 878.38,-875.1 878.38,-893.6 878.38,-893.6 878.38,-899.6 872.38,-905.6 866.38,-905.6"/>
|
||||
<text text-anchor="middle" x="804" y="-888.3" font-family="Helvetica,sans-Serif" font-size="14.00">Optimizer</text>
|
||||
<text text-anchor="middle" x="804" y="-871.05" font-family="Helvetica,sans-Serif" font-size="14.00">(query/optimize.rs)</text>
|
||||
</g>
|
||||
<!-- query_relalg->query_optimize -->
|
||||
<g id="edge34" class="edge">
|
||||
<title>query_relalg->query_optimize</title>
|
||||
<path fill="none" stroke="black" d="M798.69,-976.78C799.74,-958.82 801.07,-935.79 802.14,-917.34"/>
|
||||
<polygon fill="black" stroke="black" points="805.63,-917.67 802.72,-907.49 798.64,-917.27 805.63,-917.67"/>
|
||||
</g>
|
||||
<!-- query_chase->cc -->
|
||||
<g id="edge38" class="edge">
|
||||
<title>query_chase->cc</title>
|
||||
<path fill="none" stroke="black" d="M623.43,-1132.84C607.01,-1102.65 585,-1053.67 585,-1007.98 585,-1007.98 585,-1007.98 585,-803.85 585,-678.97 641.47,-620.88 563,-523.73 519.57,-469.97 482.17,-490.57 414,-479.23 402.92,-477.39 219.78,-479.33 212,-471.23 174.58,-432.3 193.69,-400.53 212,-349.73 219.28,-329.53 233.14,-310.63 247.06,-295.3"/>
|
||||
<polygon fill="black" stroke="black" points="249.39,-297.93 253.72,-288.26 244.31,-293.12 249.39,-297.93"/>
|
||||
<text text-anchor="middle" x="626.03" y="-752.3" font-family="Helvetica,sans-Serif" font-size="14.00">equality</text>
|
||||
<text text-anchor="middle" x="626.03" y="-735.05" font-family="Helvetica,sans-Serif" font-size="14.00">saturation</text>
|
||||
</g>
|
||||
<!-- query_chase->store -->
|
||||
<g id="edge39" class="edge">
|
||||
<title>query_chase->store</title>
|
||||
<path fill="none" stroke="black" d="M707.21,-1136.48C710.18,-1135.38 713.12,-1134.29 716,-1133.23 792.46,-1105.15 832.89,-1131.78 889,-1072.73 909.66,-1050.99 908,-1037.97 908,-1007.98 908,-1007.98 908,-1007.98 908,-642.98 908,-464.66 1116.8,-336.07 1218.98,-283.82"/>
|
||||
<polygon fill="black" stroke="black" points="1220.42,-287.02 1227.77,-279.39 1217.26,-280.77 1220.42,-287.02"/>
|
||||
</g>
|
||||
<!-- tensor_check -->
|
||||
<g id="node40" class="node">
|
||||
<title>tensor_check</title>
|
||||
<path fill="#ffecb3" stroke="black" d="M428.88,-1028.23C428.88,-1028.23 319.12,-1028.23 319.12,-1028.23 313.12,-1028.23 307.12,-1022.23 307.12,-1016.23 307.12,-1016.23 307.12,-997.73 307.12,-997.73 307.12,-991.73 313.12,-985.73 319.12,-985.73 319.12,-985.73 428.88,-985.73 428.88,-985.73 434.88,-985.73 440.88,-991.73 440.88,-997.73 440.88,-997.73 440.88,-1016.23 440.88,-1016.23 440.88,-1022.23 434.88,-1028.23 428.88,-1028.23"/>
|
||||
<text text-anchor="middle" x="374" y="-1010.93" font-family="Helvetica,sans-Serif" font-size="14.00">Axiom Checker</text>
|
||||
<text text-anchor="middle" x="374" y="-993.68" font-family="Helvetica,sans-Serif" font-size="14.00">(tensor/check.rs)</text>
|
||||
</g>
|
||||
<!-- query_chase->tensor_check -->
|
||||
<g id="edge40" class="edge">
|
||||
<title>query_chase->tensor_check</title>
|
||||
<path fill="none" stroke="black" d="M574.9,-1143.47C553.16,-1136.1 529.31,-1126.63 508.75,-1115.23 469.29,-1093.34 429.51,-1059.98 403.52,-1036.28"/>
|
||||
<polygon fill="black" stroke="black" points="406.08,-1033.89 396.36,-1029.68 401.33,-1039.03 406.08,-1033.89"/>
|
||||
<text text-anchor="middle" x="539.88" y="-1101.93" font-family="Helvetica,sans-Serif" font-size="14.00">axiom</text>
|
||||
<text text-anchor="middle" x="539.88" y="-1084.68" font-family="Helvetica,sans-Serif" font-size="14.00">checking</text>
|
||||
</g>
|
||||
<!-- query_backend -->
|
||||
<g id="node30" class="node">
|
||||
<title>query_backend</title>
|
||||
<path fill="#e1bee7" stroke="black" d="M868,-826.1C868,-826.1 744,-826.1 744,-826.1 738,-826.1 732,-820.1 732,-814.1 732,-814.1 732,-795.6 732,-795.6 732,-789.6 738,-783.6 744,-783.6 744,-783.6 868,-783.6 868,-783.6 874,-783.6 880,-789.6 880,-795.6 880,-795.6 880,-814.1 880,-814.1 880,-820.1 874,-826.1 868,-826.1"/>
|
||||
<text text-anchor="middle" x="806" y="-808.8" font-family="Helvetica,sans-Serif" font-size="14.00">Query Backend</text>
|
||||
<text text-anchor="middle" x="806" y="-791.55" font-family="Helvetica,sans-Serif" font-size="14.00">(query/backend.rs)</text>
|
||||
</g>
|
||||
<!-- query_backend->store -->
|
||||
<g id="edge36" class="edge">
|
||||
<title>query_backend->store</title>
|
||||
<path fill="none" stroke="black" d="M801.28,-783.3C790.8,-731.96 771.65,-596.98 840,-523.73 856.06,-506.52 874.69,-529.68 892,-513.73 954.09,-456.52 881.67,-388.75 943,-330.73 980.17,-295.56 1119.64,-274.6 1205.23,-264.77"/>
|
||||
<polygon fill="black" stroke="black" points="1205.37,-268.28 1214.92,-263.68 1204.59,-261.32 1205.37,-268.28"/>
|
||||
<text text-anchor="middle" x="867" y="-556.93" font-family="Helvetica,sans-Serif" font-size="14.00">execute</text>
|
||||
</g>
|
||||
<!-- query_optimize->query_backend -->
|
||||
<g id="edge35" class="edge">
|
||||
<title>query_optimize->query_backend</title>
|
||||
<path fill="none" stroke="black" d="M804.53,-862.99C804.73,-855.22 804.96,-846.17 805.18,-837.59"/>
|
||||
<polygon fill="black" stroke="black" points="808.67,-837.95 805.43,-827.86 801.68,-837.77 808.67,-837.95"/>
|
||||
</g>
|
||||
<!-- solver->store -->
|
||||
<g id="edge48" class="edge">
|
||||
<title>solver->store</title>
|
||||
<path fill="none" stroke="black" d="M179.65,-1505.49C224.24,-1497.2 272,-1478.07 272,-1434.23 272,-1434.23 272,-1434.23 272,-642.98 272,-589.62 247.54,-560.72 286,-523.73 296.28,-513.84 400.04,-516.66 414,-513.73 655.84,-463.02 701.16,-397.74 939,-330.73 1030.16,-305.04 1137.44,-283.18 1205.57,-270.31"/>
|
||||
<polygon fill="black" stroke="black" points="1205.91,-273.81 1215.09,-268.53 1204.61,-266.93 1205.91,-273.81"/>
|
||||
</g>
|
||||
<!-- solver_tree -->
|
||||
<g id="node33" class="node">
|
||||
<title>solver_tree</title>
|
||||
<path fill="#b2dfdb" stroke="black" d="M151,-1454.48C151,-1454.48 57,-1454.48 57,-1454.48 51,-1454.48 45,-1448.48 45,-1442.48 45,-1442.48 45,-1423.98 45,-1423.98 45,-1417.98 51,-1411.98 57,-1411.98 57,-1411.98 151,-1411.98 151,-1411.98 157,-1411.98 163,-1417.98 163,-1423.98 163,-1423.98 163,-1442.48 163,-1442.48 163,-1448.48 157,-1454.48 151,-1454.48"/>
|
||||
<text text-anchor="middle" x="104" y="-1437.18" font-family="Helvetica,sans-Serif" font-size="14.00">Search Tree</text>
|
||||
<text text-anchor="middle" x="104" y="-1419.93" font-family="Helvetica,sans-Serif" font-size="14.00">(solver/tree.rs)</text>
|
||||
</g>
|
||||
<!-- solver->solver_tree -->
|
||||
<g id="edge42" class="edge">
|
||||
<title>solver->solver_tree</title>
|
||||
<path fill="none" stroke="black" d="M104,-1491.37C104,-1483.6 104,-1474.54 104,-1465.96"/>
|
||||
<polygon fill="black" stroke="black" points="107.5,-1466.24 104,-1456.24 100.5,-1466.24 107.5,-1466.24"/>
|
||||
</g>
|
||||
<!-- solver_tactics -->
|
||||
<g id="node34" class="node">
|
||||
<title>solver_tactics</title>
|
||||
<path fill="#80cbc4" stroke="black" stroke-width="2" d="M160,-1374.98C160,-1374.98 48,-1374.98 48,-1374.98 42,-1374.98 36,-1368.98 36,-1362.98 36,-1362.98 36,-1309.98 36,-1309.98 36,-1303.98 42,-1297.98 48,-1297.98 48,-1297.98 160,-1297.98 160,-1297.98 166,-1297.98 172,-1303.98 172,-1309.98 172,-1309.98 172,-1362.98 172,-1362.98 172,-1368.98 166,-1374.98 160,-1374.98"/>
|
||||
<text text-anchor="middle" x="104" y="-1357.68" font-family="Helvetica,sans-Serif" font-size="14.00">Tactics</text>
|
||||
<text text-anchor="middle" x="104" y="-1340.43" font-family="Helvetica,sans-Serif" font-size="14.00">(solver/tactics.rs)</text>
|
||||
<text text-anchor="middle" x="104" y="-1323.18" font-family="Helvetica,sans-Serif" font-size="14.00">Check, Forward,</text>
|
||||
<text text-anchor="middle" x="104" y="-1305.93" font-family="Helvetica,sans-Serif" font-size="14.00">Propagate, Auto</text>
|
||||
</g>
|
||||
<!-- solver_tree->solver_tactics -->
|
||||
<g id="edge43" class="edge">
|
||||
<title>solver_tree->solver_tactics</title>
|
||||
<path fill="none" stroke="black" d="M104,-1411.67C104,-1404.17 104,-1395.33 104,-1386.42"/>
|
||||
<polygon fill="black" stroke="black" points="107.5,-1386.7 104,-1376.7 100.5,-1386.7 107.5,-1386.7"/>
|
||||
</g>
|
||||
<!-- solver_tactics->cc -->
|
||||
<g id="edge46" class="edge">
|
||||
<title>solver_tactics->cc</title>
|
||||
<path fill="none" stroke="black" d="M63.84,-1297.61C47.84,-1280.59 30.53,-1259.41 19,-1237.48 3.33,-1207.66 0,-1197.79 0,-1164.1 0,-1164.1 0,-1164.1 0,-387.23 0,-293.41 117.57,-365.65 201,-322.73 216.24,-314.89 231.63,-304.44 245.15,-294.23"/>
|
||||
<polygon fill="black" stroke="black" points="247.23,-297.05 252.99,-288.16 242.94,-291.52 247.23,-297.05"/>
|
||||
<text text-anchor="middle" x="35.25" y="-808.8" font-family="Helvetica,sans-Serif" font-size="14.00">propagate</text>
|
||||
<text text-anchor="middle" x="35.25" y="-791.55" font-family="Helvetica,sans-Serif" font-size="14.00">equations</text>
|
||||
</g>
|
||||
<!-- solver_tactics->query_chase -->
|
||||
<g id="edge45" class="edge">
|
||||
<title>solver_tactics->query_chase</title>
|
||||
<path fill="none" stroke="black" d="M172.28,-1313.69C272.55,-1281.69 459.06,-1222.17 564.05,-1188.66"/>
|
||||
<polygon fill="black" stroke="black" points="564.97,-1192.04 573.44,-1185.67 562.85,-1185.37 564.97,-1192.04"/>
|
||||
<text text-anchor="middle" x="402.12" y="-1266.68" font-family="Helvetica,sans-Serif" font-size="14.00">forward</text>
|
||||
<text text-anchor="middle" x="402.12" y="-1249.43" font-family="Helvetica,sans-Serif" font-size="14.00">chaining</text>
|
||||
</g>
|
||||
<!-- solver_types -->
|
||||
<g id="node35" class="node">
|
||||
<title>solver_types</title>
|
||||
<path fill="#b2dfdb" stroke="black" d="M145.62,-1184.35C145.62,-1184.35 40.38,-1184.35 40.38,-1184.35 34.38,-1184.35 28.38,-1178.35 28.38,-1172.35 28.38,-1172.35 28.38,-1153.85 28.38,-1153.85 28.38,-1147.85 34.38,-1141.85 40.38,-1141.85 40.38,-1141.85 145.62,-1141.85 145.62,-1141.85 151.62,-1141.85 157.62,-1147.85 157.62,-1153.85 157.62,-1153.85 157.62,-1172.35 157.62,-1172.35 157.62,-1178.35 151.62,-1184.35 145.62,-1184.35"/>
|
||||
<text text-anchor="middle" x="93" y="-1167.05" font-family="Helvetica,sans-Serif" font-size="14.00">Solver Types</text>
|
||||
<text text-anchor="middle" x="93" y="-1149.8" font-family="Helvetica,sans-Serif" font-size="14.00">(solver/types.rs)</text>
|
||||
</g>
|
||||
<!-- solver_tactics->solver_types -->
|
||||
<g id="edge44" class="edge">
|
||||
<title>solver_tactics->solver_types</title>
|
||||
<path fill="none" stroke="black" d="M101.59,-1297.87C99.63,-1267.35 96.9,-1224.8 95.04,-1195.95"/>
|
||||
<polygon fill="black" stroke="black" points="98.54,-1195.76 94.41,-1186 91.55,-1196.21 98.54,-1195.76"/>
|
||||
</g>
|
||||
<!-- solver_tactics->tensor_check -->
|
||||
<g id="edge47" class="edge">
|
||||
<title>solver_tactics->tensor_check</title>
|
||||
<path fill="none" stroke="black" d="M135.24,-1297.59C189.65,-1231.58 300.72,-1096.87 349.78,-1037.36"/>
|
||||
<polygon fill="black" stroke="black" points="352.48,-1039.59 356.14,-1029.64 347.08,-1035.13 352.48,-1039.59"/>
|
||||
<text text-anchor="middle" x="295" y="-1167.05" font-family="Helvetica,sans-Serif" font-size="14.00">check</text>
|
||||
<text text-anchor="middle" x="295" y="-1149.8" font-family="Helvetica,sans-Serif" font-size="14.00">axioms</text>
|
||||
</g>
|
||||
<!-- tensor_expr -->
|
||||
<g id="node36" class="node">
|
||||
<title>tensor_expr</title>
|
||||
<path fill="#ffecb3" stroke="black" d="M436.38,-826.1C436.38,-826.1 311.62,-826.1 311.62,-826.1 305.62,-826.1 299.62,-820.1 299.62,-814.1 299.62,-814.1 299.62,-795.6 299.62,-795.6 299.62,-789.6 305.62,-783.6 311.62,-783.6 311.62,-783.6 436.38,-783.6 436.38,-783.6 442.38,-783.6 448.38,-789.6 448.38,-795.6 448.38,-795.6 448.38,-814.1 448.38,-814.1 448.38,-820.1 442.38,-826.1 436.38,-826.1"/>
|
||||
<text text-anchor="middle" x="374" y="-808.8" font-family="Helvetica,sans-Serif" font-size="14.00">Tensor Expressions</text>
|
||||
<text text-anchor="middle" x="374" y="-791.55" font-family="Helvetica,sans-Serif" font-size="14.00">(tensor/expr.rs)</text>
|
||||
</g>
|
||||
<!-- tensor_builder -->
|
||||
<g id="node38" class="node">
|
||||
<title>tensor_builder</title>
|
||||
<path fill="#ffecb3" stroke="black" d="M433.88,-713.1C433.88,-713.1 312.12,-713.1 312.12,-713.1 306.12,-713.1 300.12,-707.1 300.12,-701.1 300.12,-701.1 300.12,-682.6 300.12,-682.6 300.12,-676.6 306.12,-670.6 312.12,-670.6 312.12,-670.6 433.88,-670.6 433.88,-670.6 439.88,-670.6 445.88,-676.6 445.88,-682.6 445.88,-682.6 445.88,-701.1 445.88,-701.1 445.88,-707.1 439.88,-713.1 433.88,-713.1"/>
|
||||
<text text-anchor="middle" x="373" y="-695.8" font-family="Helvetica,sans-Serif" font-size="14.00">Expression Builder</text>
|
||||
<text text-anchor="middle" x="373" y="-678.55" font-family="Helvetica,sans-Serif" font-size="14.00">(tensor/builder.rs)</text>
|
||||
</g>
|
||||
<!-- tensor_expr->tensor_builder -->
|
||||
<g id="edge50" class="edge">
|
||||
<title>tensor_expr->tensor_builder</title>
|
||||
<path fill="none" stroke="black" d="M373.81,-783.24C373.67,-766.8 373.46,-743.51 373.29,-724.69"/>
|
||||
<polygon fill="black" stroke="black" points="376.79,-724.84 373.2,-714.88 369.79,-724.91 376.79,-724.84"/>
|
||||
</g>
|
||||
<!-- tensor_sparse -->
|
||||
<g id="node37" class="node">
|
||||
<title>tensor_sparse</title>
|
||||
<path fill="#ffe082" stroke="black" stroke-width="2" d="M427.88,-591.48C427.88,-591.48 312.12,-591.48 312.12,-591.48 306.12,-591.48 300.12,-585.48 300.12,-579.48 300.12,-579.48 300.12,-543.73 300.12,-543.73 300.12,-537.73 306.12,-531.73 312.12,-531.73 312.12,-531.73 427.88,-531.73 427.88,-531.73 433.88,-531.73 439.88,-537.73 439.88,-543.73 439.88,-543.73 439.88,-579.48 439.88,-579.48 439.88,-585.48 433.88,-591.48 427.88,-591.48"/>
|
||||
<text text-anchor="middle" x="370" y="-574.18" font-family="Helvetica,sans-Serif" font-size="14.00">Sparse Storage</text>
|
||||
<text text-anchor="middle" x="370" y="-556.93" font-family="Helvetica,sans-Serif" font-size="14.00">(tensor/sparse.rs)</text>
|
||||
<text text-anchor="middle" x="370" y="-539.68" font-family="Helvetica,sans-Serif" font-size="14.00">RoaringBitmap</text>
|
||||
</g>
|
||||
<!-- tensor_sparse->core -->
|
||||
<g id="edge53" class="edge">
|
||||
<title>tensor_sparse->core</title>
|
||||
<path fill="none" stroke="black" d="M356.94,-531.53C345.39,-505.82 328.3,-467.8 314.7,-437.52"/>
|
||||
<polygon fill="black" stroke="black" points="317.96,-436.24 310.67,-428.55 311.57,-439.1 317.96,-436.24"/>
|
||||
<text text-anchor="middle" x="378.75" y="-500.43" font-family="Helvetica,sans-Serif" font-size="14.00">read</text>
|
||||
<text text-anchor="middle" x="378.75" y="-483.18" font-family="Helvetica,sans-Serif" font-size="14.00">structure</text>
|
||||
</g>
|
||||
<!-- roaring -->
|
||||
<g id="node43" class="node">
|
||||
<title>roaring</title>
|
||||
<ellipse fill="#e0e0e0" stroke="black" cx="449" cy="-125.55" rx="58.51" ry="30.05"/>
|
||||
<text text-anchor="middle" x="449" y="-129.5" font-family="Helvetica,sans-Serif" font-size="14.00">roaring</text>
|
||||
<text text-anchor="middle" x="449" y="-112.25" font-family="Helvetica,sans-Serif" font-size="14.00">(bitmaps)</text>
|
||||
</g>
|
||||
<!-- tensor_sparse->roaring -->
|
||||
<g id="edge57" class="edge">
|
||||
<title>tensor_sparse->roaring</title>
|
||||
<path fill="none" stroke="black" stroke-dasharray="1,5" d="M299.81,-550.68C216.06,-537.24 83.58,-510.33 54,-471.23 -11.45,-384.7 -19.47,-306.95 54,-227.1 102.92,-173.94 312.73,-231.08 378,-200.1 395.66,-191.72 411.27,-176.94 423.32,-162.81"/>
|
||||
<polygon fill="black" stroke="black" points="425.8,-165.31 429.39,-155.34 420.37,-160.89 425.8,-165.31"/>
|
||||
</g>
|
||||
<!-- tensor_builder->tensor_sparse -->
|
||||
<g id="edge51" class="edge">
|
||||
<title>tensor_builder->tensor_sparse</title>
|
||||
<path fill="none" stroke="black" d="M372.52,-670.25C372.09,-652.16 371.47,-625.32 370.94,-602.97"/>
|
||||
<polygon fill="black" stroke="black" points="374.45,-603.19 370.72,-593.27 367.45,-603.35 374.45,-603.19"/>
|
||||
</g>
|
||||
<!-- tensor_compile -->
|
||||
<g id="node39" class="node">
|
||||
<title>tensor_compile</title>
|
||||
<path fill="#ffecb3" stroke="black" d="M435.62,-905.6C435.62,-905.6 312.38,-905.6 312.38,-905.6 306.38,-905.6 300.38,-899.6 300.38,-893.6 300.38,-893.6 300.38,-875.1 300.38,-875.1 300.38,-869.1 306.38,-863.1 312.38,-863.1 312.38,-863.1 435.62,-863.1 435.62,-863.1 441.62,-863.1 447.62,-869.1 447.62,-875.1 447.62,-875.1 447.62,-893.6 447.62,-893.6 447.62,-899.6 441.62,-905.6 435.62,-905.6"/>
|
||||
<text text-anchor="middle" x="374" y="-888.3" font-family="Helvetica,sans-Serif" font-size="14.00">Formula Compiler</text>
|
||||
<text text-anchor="middle" x="374" y="-871.05" font-family="Helvetica,sans-Serif" font-size="14.00">(tensor/compile.rs)</text>
|
||||
</g>
|
||||
<!-- tensor_compile->tensor_expr -->
|
||||
<g id="edge49" class="edge">
|
||||
<title>tensor_compile->tensor_expr</title>
|
||||
<path fill="none" stroke="black" d="M374,-862.99C374,-855.22 374,-846.17 374,-837.59"/>
|
||||
<polygon fill="black" stroke="black" points="377.5,-837.86 374,-827.86 370.5,-837.86 377.5,-837.86"/>
|
||||
</g>
|
||||
<!-- tensor_check->tensor_compile -->
|
||||
<g id="edge52" class="edge">
|
||||
<title>tensor_check->tensor_compile</title>
|
||||
<path fill="none" stroke="black" d="M374,-985.24C374,-966.49 374,-938.59 374,-917.04"/>
|
||||
<polygon fill="black" stroke="black" points="377.5,-917.33 374,-907.33 370.5,-917.33 377.5,-917.33"/>
|
||||
</g>
|
||||
<!-- legend_data -->
|
||||
<g id="node45" class="node">
|
||||
<title>legend_data</title>
|
||||
<path fill="#f0f0f0" stroke="none" d="M1088.12,-1757.73C1088.12,-1757.73 1027.88,-1757.73 1027.88,-1757.73 1021.88,-1757.73 1015.88,-1751.73 1015.88,-1745.73 1015.88,-1745.73 1015.88,-1733.73 1015.88,-1733.73 1015.88,-1727.73 1021.88,-1721.73 1027.88,-1721.73 1027.88,-1721.73 1088.12,-1721.73 1088.12,-1721.73 1094.12,-1721.73 1100.12,-1727.73 1100.12,-1733.73 1100.12,-1733.73 1100.12,-1745.73 1100.12,-1745.73 1100.12,-1751.73 1094.12,-1757.73 1088.12,-1757.73"/>
|
||||
<text text-anchor="middle" x="1058" y="-1735.05" font-family="Helvetica,sans-Serif" font-size="14.00">Data Flow</text>
|
||||
</g>
|
||||
<!-- legend_dep -->
|
||||
<g id="node46" class="node">
|
||||
<title>legend_dep</title>
|
||||
<path fill="#f0f0f0" stroke="none" d="M1097.5,-1678.23C1097.5,-1678.23 1018.5,-1678.23 1018.5,-1678.23 1012.5,-1678.23 1006.5,-1672.23 1006.5,-1666.23 1006.5,-1666.23 1006.5,-1654.23 1006.5,-1654.23 1006.5,-1648.23 1012.5,-1642.23 1018.5,-1642.23 1018.5,-1642.23 1097.5,-1642.23 1097.5,-1642.23 1103.5,-1642.23 1109.5,-1648.23 1109.5,-1654.23 1109.5,-1654.23 1109.5,-1666.23 1109.5,-1666.23 1109.5,-1672.23 1103.5,-1678.23 1097.5,-1678.23"/>
|
||||
<text text-anchor="middle" x="1058" y="-1655.55" font-family="Helvetica,sans-Serif" font-size="14.00">Dependency</text>
|
||||
</g>
|
||||
<!-- legend_data->legend_dep -->
|
||||
<!-- legend_key -->
|
||||
<g id="node47" class="node">
|
||||
<title>legend_key</title>
|
||||
<path fill="#80cbc4" stroke="black" stroke-width="2" d="M1108.75,-1530.73C1108.75,-1530.73 1007.25,-1530.73 1007.25,-1530.73 1001.25,-1530.73 995.25,-1524.73 995.25,-1518.73 995.25,-1518.73 995.25,-1506.73 995.25,-1506.73 995.25,-1500.73 1001.25,-1494.73 1007.25,-1494.73 1007.25,-1494.73 1108.75,-1494.73 1108.75,-1494.73 1114.75,-1494.73 1120.75,-1500.73 1120.75,-1506.73 1120.75,-1506.73 1120.75,-1518.73 1120.75,-1518.73 1120.75,-1524.73 1114.75,-1530.73 1108.75,-1530.73"/>
|
||||
<text text-anchor="middle" x="1058" y="-1508.05" font-family="Helvetica,sans-Serif" font-size="14.00">Key Component</text>
|
||||
</g>
|
||||
<!-- legend_dep->legend_key -->
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 62 KiB |
255
docs/ARCHITECTURE.md
Normal file
255
docs/ARCHITECTURE.md
Normal file
@ -0,0 +1,255 @@
|
||||
# Geolog Architecture
|
||||
|
||||
Geolog is a language for geometric logic with semantics in topoi. This document describes the module structure and data flow.
|
||||
|
||||
## Module Overview
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ USER INTERFACE │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ repl.rs Interactive REPL with commands (:help, :inspect, etc.) │
|
||||
│ bin/geolog.rs CLI entry point │
|
||||
└───────────────────────────────┬─────────────────────────────────────────────┘
|
||||
│
|
||||
┌───────────────────────────────▼─────────────────────────────────────────────┐
|
||||
│ PARSING / SURFACE LAYER │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ lexer.rs Tokenization (chumsky-based) │
|
||||
│ parser.rs Token stream → AST (chumsky-based) │
|
||||
│ ast.rs Surface syntax AST types │
|
||||
│ pretty.rs Core → geolog source (inverse of parsing) │
|
||||
│ error.rs Error formatting with source spans │
|
||||
└───────────────────────────────┬─────────────────────────────────────────────┘
|
||||
│
|
||||
┌───────────────────────────────▼─────────────────────────────────────────────┐
|
||||
│ ELABORATION LAYER │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ elaborate/ │
|
||||
│ ├── mod.rs Re-exports │
|
||||
│ ├── env.rs Elaboration environment (theory registry) │
|
||||
│ ├── theory.rs AST theory → Core theory elaboration │
|
||||
│ ├── instance.rs AST instance → Core structure elaboration │
|
||||
│ └── error.rs Elaboration error types │
|
||||
│ │
|
||||
│ Transforms surface AST into typed core representation │
|
||||
└───────────────────────────────┬─────────────────────────────────────────────┘
|
||||
│
|
||||
┌───────────────────────────────▼─────────────────────────────────────────────┐
|
||||
│ CORE LAYER │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ core.rs Core IR: Signature, Term, Formula, Structure │
|
||||
│ - Signature: sorts + functions + relations │
|
||||
│ - Term: Var | App | Record | Project │
|
||||
│ - Formula: True | False | Eq | Rel | Conj | Disj | Exists │
|
||||
│ - Structure: carriers + function maps + relation storage │
|
||||
│ │
|
||||
│ id.rs Identity system (Luid = global, Slid = structure-local) │
|
||||
│ universe.rs Global element registry (Luid allocation) │
|
||||
│ naming.rs Bidirectional name ↔ Luid mapping │
|
||||
└───────────────────────────────┬─────────────────────────────────────────────┘
|
||||
│
|
||||
┌───────────────────────────────▼─────────────────────────────────────────────┐
|
||||
│ STORAGE LAYER │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ store/ │
|
||||
│ ├── mod.rs Store struct: unified GeologMeta persistence │
|
||||
│ ├── schema.rs Schema ID caches (sort_ids, func_ids, etc.) │
|
||||
│ ├── append.rs Append-only element/function/relation creation │
|
||||
│ ├── theory.rs Theory → Store integration │
|
||||
│ ├── instance.rs Instance → Store integration │
|
||||
│ ├── commit.rs Git-like commit/version control │
|
||||
│ └── bootstrap_queries.rs Hardcoded query patterns (being replaced) │
|
||||
│ │
|
||||
│ workspace.rs Legacy session management (deprecated, use Store) │
|
||||
│ patch.rs Patch-based structure modifications │
|
||||
│ version.rs Git-like version control for structures │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ QUERY LAYER │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ query/ │
|
||||
│ ├── mod.rs Re-exports and overview │
|
||||
│ ├── chase.rs Chase algorithm for existential/equality conclusions │
|
||||
│ │ - chase_fixpoint_with_cc(): main entry point │
|
||||
│ │ - Integrates CongruenceClosure for equality saturation│
|
||||
│ ├── compile.rs Query → QueryOp plan compilation │
|
||||
│ ├── backend.rs Naive QueryOp executor (reference impl) │
|
||||
│ ├── optimize.rs Algebraic law rewriting (filter fusion, etc.) │
|
||||
│ ├── pattern.rs Legacy Pattern API (deprecated) │
|
||||
│ └── store_queries.rs Store-level compiled query methods │
|
||||
│ │
|
||||
│ Relational query engine for GeologMeta and instance queries. │
|
||||
│ Query API: Query::scan(sort).filter_eq(func, col, val).compile() │
|
||||
│ Optimizer applies RelAlgIR laws: Filter(p, Filter(q, x)) → Filter(p∧q, x) │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ SOLVING LAYER (frontier) │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ cc.rs Congruence closure (shared by solver + chase) │
|
||||
│ - Element equivalence tracking with union-find │
|
||||
│ - Used for equality conclusion axioms │
|
||||
│ │
|
||||
│ solver/ │
|
||||
│ ├── mod.rs Unified model enumeration API + re-exports │
|
||||
│ │ - enumerate_models(): core unified function │
|
||||
│ │ - solve(): find models from scratch │
|
||||
│ │ - query(): extend existing models │
|
||||
│ ├── types.rs SearchNode, Obligation, NodeStatus (re-exports cc::*) │
|
||||
│ ├── tree.rs Explicit search tree with from_base() for extensions │
|
||||
│ └── tactics.rs Automated search tactics: │
|
||||
│ - CheckTactic: axiom checking, obligation reporting │
|
||||
│ - ForwardChainingTactic: Datalog-style forward chaining │
|
||||
│ - PropagateEquationsTactic: congruence closure propagation│
|
||||
│ - AutoTactic: composite fixpoint solver │
|
||||
│ │
|
||||
│ REPL commands: `:solve <theory>`, `:extend <instance> <theory>` │
|
||||
│ See examples/geolog/solver_demo.geolog for annotated examples. │
|
||||
│ │
|
||||
│ tensor/ │
|
||||
│ ├── mod.rs Re-exports │
|
||||
│ ├── expr.rs Lazy tensor expression trees │
|
||||
│ ├── sparse.rs Sparse tensor storage (RoaringTreemap) │
|
||||
│ ├── builder.rs Expression builders (conjunction, disjunction, exists) │
|
||||
│ ├── compile.rs Formula → TensorExpr compilation │
|
||||
│ └── check.rs Axiom checking via tensor evaluation │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ META LAYER (self-description) │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ meta.rs Rust codegen for GeologMeta theory │
|
||||
│ theories/GeologMeta.geolog Homoiconic theory representation │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Data Flow
|
||||
|
||||
### Parsing / Pretty-Printing Flow
|
||||
```
|
||||
Source text → lexer.rs → Token stream → parser.rs → ast::File
|
||||
↓
|
||||
core::Structure ← elaborate ←──────────────────────── ast::*
|
||||
↓
|
||||
pretty.rs → Source text (roundtrip!)
|
||||
```
|
||||
|
||||
### Elaboration Flow
|
||||
```
|
||||
ast::TheoryDecl → elaborate/theory.rs → core::Theory (Signature + Axioms)
|
||||
ast::InstanceDecl → elaborate/instance.rs → core::Structure
|
||||
```
|
||||
|
||||
### REPL Flow
|
||||
```
|
||||
User input → ReplState::process_line → MetaCommand | GeologInput
|
||||
↓
|
||||
GeologInput → parse → elaborate → workspace.add_*
|
||||
```
|
||||
|
||||
## Key Types
|
||||
|
||||
### Identity System
|
||||
|
||||
```rust
|
||||
Luid // "Local Universe ID" - globally unique across all structures
|
||||
Slid // "Structure-Local ID" - index within a single structure
|
||||
|
||||
// A Structure maps Slid → Luid for global identity
|
||||
structure.get_luid(slid) -> Luid
|
||||
```
|
||||
|
||||
### Core Representation
|
||||
|
||||
```rust
|
||||
// Signatures define the vocabulary
|
||||
Signature {
|
||||
sorts: Vec<String>, // Sort names by SortId
|
||||
functions: Vec<FunctionSymbol>, // f : A → B
|
||||
relations: Vec<RelationSymbol>, // R : A → Prop
|
||||
}
|
||||
|
||||
// Structures interpret signatures
|
||||
Structure {
|
||||
carriers: Vec<RoaringBitmap>, // Elements per sort (as Slid)
|
||||
functions: Vec<HashMap<...>>, // Function value maps
|
||||
relations: Vec<VecRelation>, // Relation extents
|
||||
local_to_global: Vec<Luid>, // Slid → Luid
|
||||
}
|
||||
```
|
||||
|
||||
### Axioms (Sequents)
|
||||
|
||||
```rust
|
||||
Sequent {
|
||||
context: Context, // Universally quantified variables
|
||||
premise: Formula, // Antecedent (conjunction of atomics)
|
||||
conclusion: Formula, // Consequent (positive geometric formula)
|
||||
}
|
||||
```
|
||||
|
||||
## Design Principles
|
||||
|
||||
1. **Postfix application**: `x f` not `f(x)` — matches categorical composition
|
||||
2. **Child pointers**: Parent → Child, not Child → Parent (no products in domains)
|
||||
3. **Upward binding**: Variables point to their binders (scoping is explicit)
|
||||
4. **Sparse storage**: Relations use RoaringBitmap for efficient membership
|
||||
5. **Patch-based updates**: Structures evolve via patches, enabling versioning
|
||||
6. **Explicit search tree**: Solver maintains tree in memory, not call stack
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
- **proptest**: Property-based tests for core operations (naming, patches, structure)
|
||||
- **unit tests**: Specific behaviors in `tests/unit_*.rs`
|
||||
- **integration tests**: Example .geolog files in `tests/examples_integration.rs`
|
||||
- **REPL testing**: Interactive exploration via `cargo run`
|
||||
|
||||
## Future Directions
|
||||
|
||||
See `bd ready` for current work items. Key frontiers:
|
||||
|
||||
- **Query engine** (`geolog-7tt`, `geolog-32x`): Chase algorithm and RelAlgIR compiler
|
||||
- **Nested instance elaboration** (`geolog-1d4`): Inline instance definitions
|
||||
- **Monotonic Submodel proofs** (`geolog-rgg`): Lean4 formalization
|
||||
- **Disjunction variable alignment** (`geolog-69b`): Extend tensor builder for heterogeneous disjuncts
|
||||
|
||||
## Recent Milestones
|
||||
|
||||
- **Unified model enumeration API** (`2026-01-19`): Consolidated `solve()`, `extend()`, and `query()`
|
||||
into single `enumerate_models()` function. REPL commands `:solve` and `:extend` now share underlying implementation.
|
||||
|
||||
- **Tensor compiler improvements** (`2026-01-20`):
|
||||
- Function application equalities: `f(x) = y`, `y = f(x)`, `f(x) = g(y)` now compile correctly
|
||||
- Empty-domain existential fix: `∃x. φ` on empty domain correctly returns false
|
||||
- Closed `geolog-dxr` (tensor compilation panics on function terms)
|
||||
|
||||
- **Bootstrap query migration** (`2026-01-20`): All 6 bootstrap_queries functions now delegate
|
||||
to compiled query engine (`store_queries.rs`). Net reduction of ~144 lines of handcoded iteration.
|
||||
|
||||
- **Proptest coverage** (`2026-01-20`): Added 6 solver proptests covering trivial theories,
|
||||
inconsistent theories, existential theories, and Horn clause propagation.
|
||||
|
||||
- **Theory extends fix** (`2026-01-20`): Fixed bug where function names like `Func/dom` (using `/`
|
||||
as naming convention) were incorrectly treated as grandparent-qualified names. RelAlgIR.geolog
|
||||
now loads correctly, unblocking homoiconic query plan work (`geolog-32x`).
|
||||
|
||||
- **:explain REPL command** (`2026-01-20`): Added `:explain <instance> <sort>` to show query
|
||||
execution plans, with Display impl for QueryOp using math notation (∫, δ, z⁻¹, ×, ∧, ∨).
|
||||
|
||||
- **Geometric logic solver complete** (`geolog-xj2`): Forward chaining, equation propagation,
|
||||
existential body processing, derivation search for False. Interactive via `:solve`.
|
||||
|
||||
- **Chase with equality saturation** (`2026-01-21`): Chase algorithm now integrates congruence
|
||||
closure (CC) for handling equality conclusion axioms like `R(x,y) |- x = y`. CC tracks
|
||||
element equivalences and canonicalizes structures after chase converges. This enables
|
||||
Category theory to terminate correctly: unit law axioms collapse infinite `id;id;...`
|
||||
compositions. Added `src/cc.rs` as shared module for both solver and chase.
|
||||
|
||||
- **Chase proptests** (`2026-01-21`): Added property-based tests for reflexivity, transitivity,
|
||||
existential conclusions, and equality conclusions. Multi-session persistence tests verify
|
||||
chase results survive REPL restart.
|
||||
|
||||
- **Fuzzing infrastructure** (`2026-01-21`): Added `fuzz/` directory with `fuzz_parser` and
|
||||
`fuzz_repl` targets for finding edge cases. Requires nightly Rust.
|
||||
336
docs/SYNTAX.md
Normal file
336
docs/SYNTAX.md
Normal file
@ -0,0 +1,336 @@
|
||||
# Geolog Surface Syntax Reference
|
||||
|
||||
This document describes the surface syntax of Geolog. For examples, see `examples/geolog/`.
|
||||
|
||||
## Lexical Elements
|
||||
|
||||
### Identifiers
|
||||
```
|
||||
identifier := [a-zA-Z_][a-zA-Z0-9_]*
|
||||
```
|
||||
|
||||
### Paths
|
||||
Paths use `/` as a separator (not `.`), which allows `.` for field projection:
|
||||
```
|
||||
path := identifier ('/' identifier)*
|
||||
```
|
||||
Examples: `P`, `in/src`, `ax/refl`
|
||||
|
||||
### Keywords
|
||||
```
|
||||
namespace theory instance query
|
||||
Sort Prop forall exists
|
||||
```
|
||||
|
||||
### Operators and Punctuation
|
||||
```
|
||||
: -> = |- \/ . , ;
|
||||
{ } [ ] ( )
|
||||
```
|
||||
|
||||
## Declarations
|
||||
|
||||
A Geolog file consists of declarations:
|
||||
|
||||
```
|
||||
file := declaration*
|
||||
declaration := namespace | theory | instance | query
|
||||
```
|
||||
|
||||
### Namespace
|
||||
```
|
||||
namespace identifier;
|
||||
```
|
||||
Currently a no-op; reserved for future module system.
|
||||
|
||||
### Theory
|
||||
|
||||
```ebnf
|
||||
theory := 'theory' params? identifier '{' theory_item* '}'
|
||||
params := param_group+
|
||||
param_group := '(' param (',' param)* ')'
|
||||
param := identifier ':' type_expr
|
||||
|
||||
theory_item := sort_decl | function_decl | axiom_decl | field_decl
|
||||
```
|
||||
|
||||
#### Sort Declaration
|
||||
```
|
||||
identifier ':' 'Sort' ';'
|
||||
```
|
||||
Example: `P : Sort;`
|
||||
|
||||
#### Function Declaration
|
||||
```
|
||||
path ':' type_expr '->' type_expr ';'
|
||||
```
|
||||
Examples:
|
||||
```
|
||||
src : E -> V; // Unary function
|
||||
mul : [x: M, y: M] -> M; // Binary function (product domain)
|
||||
```
|
||||
|
||||
#### Relation Declaration
|
||||
Relations are functions to `Prop`:
|
||||
```
|
||||
path ':' type_expr '->' 'Prop' ';'
|
||||
```
|
||||
Example:
|
||||
```
|
||||
leq : [x: X, y: X] -> Prop; // Binary relation
|
||||
```
|
||||
|
||||
#### Axiom Declaration
|
||||
```
|
||||
path ':' 'forall' quantified_vars '.' premises '|-' conclusion ';'
|
||||
|
||||
quantified_vars := (var_group (',' var_group)*)? // May be empty!
|
||||
var_group := identifier (',' identifier)* ':' type_expr
|
||||
premises := formula (',' formula)* // May be empty
|
||||
```
|
||||
|
||||
Examples:
|
||||
```
|
||||
// No premises (Horn clause with empty body)
|
||||
ax/refl : forall x : X. |- [x: x, y: x] leq;
|
||||
|
||||
// With premises
|
||||
ax/trans : forall x : X, y : X, z : X.
|
||||
[x: x, y: y] leq, [x: y, y: z] leq |- [x: x, y: z] leq;
|
||||
|
||||
// Empty quantifier - unconditional axiom
|
||||
// Useful for asserting existence without preconditions
|
||||
ax/nonempty : forall . |- exists x : X.;
|
||||
```
|
||||
|
||||
### Instance
|
||||
|
||||
```ebnf
|
||||
instance := 'instance' identifier ':' type_expr '=' instance_body
|
||||
instance_body := '{' instance_item* '}' | 'chase' '{' instance_item* '}'
|
||||
|
||||
instance_item := element_decl | equation | nested_instance
|
||||
```
|
||||
|
||||
Using `= chase { ... }` runs the chase algorithm during elaboration, automatically deriving facts from axioms.
|
||||
|
||||
The chase supports:
|
||||
- **Existential conclusions**: Creates fresh elements for `∃` in axiom conclusions
|
||||
- **Equality conclusions**: Uses congruence closure to track element equivalences
|
||||
- **Fixpoint iteration**: Runs until no new facts can be derived
|
||||
|
||||
Equality saturation enables termination for theories with unit laws (like Categories) that would otherwise loop forever.
|
||||
|
||||
#### Element Declaration
|
||||
```
|
||||
identifier ':' type_expr ';'
|
||||
```
|
||||
Example: `A : V;` — declares element `A` of sort `V`
|
||||
|
||||
#### Equation
|
||||
```
|
||||
term '=' term ';'
|
||||
```
|
||||
Example: `ab src = A;` — asserts that applying `src` to `ab` yields `A`
|
||||
|
||||
#### Nested Instance (syntax parsed but not fully elaborated)
|
||||
```
|
||||
identifier '=' '{' instance_item* '}' ';'
|
||||
```
|
||||
|
||||
## Type Expressions
|
||||
|
||||
```ebnf
|
||||
type_expr := 'Sort' | 'Prop' | path | record_type | app_type | arrow_type | instance_type
|
||||
|
||||
record_type := '[' (field (',' field)*)? ']'
|
||||
field := identifier ':' type_expr // Named field
|
||||
| type_expr // Positional: gets name "0", "1", etc.
|
||||
|
||||
app_type := type_expr type_expr // Juxtaposition
|
||||
arrow_type := type_expr '->' type_expr
|
||||
instance_type := type_expr 'instance'
|
||||
```
|
||||
|
||||
Examples:
|
||||
```
|
||||
Sort // The universe of sorts
|
||||
Prop // Propositions
|
||||
V // A named sort
|
||||
[x: M, y: M] // Product type with named fields
|
||||
[M, M] // Product type with positional fields ("0", "1")
|
||||
[M, on: M] // Mixed: first positional, second named
|
||||
M -> M // Function type
|
||||
PetriNet instance // Instance of a theory
|
||||
N PetriNet instance // Parameterized: N is a PetriNet instance
|
||||
```
|
||||
|
||||
## Terms
|
||||
|
||||
```ebnf
|
||||
term := path | record | paren_term | application | projection
|
||||
|
||||
record := '[' (entry (',' entry)*)? ']'
|
||||
entry := identifier ':' term // Named entry
|
||||
| term // Positional: gets name "0", "1", etc.
|
||||
|
||||
paren_term := '(' term ')'
|
||||
application := term term // Postfix! 'x f' means 'f(x)'
|
||||
projection := term '.' identifier // Record projection
|
||||
```
|
||||
|
||||
**Important**: Geolog uses **postfix** function application.
|
||||
|
||||
| Geolog | Traditional |
|
||||
|--------|-------------|
|
||||
| `x f` | `f(x)` |
|
||||
| `[x: a, y: b] mul` | `mul(a, b)` |
|
||||
| `x f g` | `g(f(x))` |
|
||||
|
||||
This matches categorical composition: morphisms compose left-to-right.
|
||||
|
||||
Examples:
|
||||
```
|
||||
A // Variable/element reference
|
||||
ab src // Apply src to ab
|
||||
[x: a, y: b] mul // Apply mul to record (named fields)
|
||||
[a, b] mul // Apply mul to record (positional)
|
||||
[a, on: b] rel // Mixed: positional first, named second
|
||||
x f g // Composition: g(f(x))
|
||||
r .field // Project field from record r
|
||||
```
|
||||
|
||||
**Note on positional fields**: Positional fields are assigned names "0", "1", etc.
|
||||
When matching against a relation defined with named fields (e.g., `rel : [x: M, y: M] -> Prop`),
|
||||
positional fields are matched by position: "0" matches the first field, "1" the second, etc.
|
||||
This allows mixing positional and named syntax: `[a, y: b] rel` is equivalent to `[x: a, y: b] rel`.
|
||||
|
||||
## Formulas
|
||||
|
||||
```ebnf
|
||||
formula := atomic | exists | disjunction | paren_formula
|
||||
|
||||
atomic := equality | relation_app
|
||||
equality := term '=' term
|
||||
relation_app := term identifier // 'x R' means R(x)
|
||||
|
||||
exists := 'exists' quantified_vars '.' formulas? // Body may be empty (= True)
|
||||
formulas := formula (',' formula)*
|
||||
disjunction := formula ('\/' formula)+
|
||||
paren_formula := '(' formula ')'
|
||||
```
|
||||
|
||||
**Conjunction** is implicit: premises in axioms separated by `,` form a conjunction.
|
||||
|
||||
Examples:
|
||||
```
|
||||
x = y // Equality
|
||||
[x: a, y: b] leq // Relation application
|
||||
exists z : X. [x: x, y: z] leq // Existential with condition
|
||||
exists z : X. // Existential with empty body (= exists z. True)
|
||||
phi \/ psi // Disjunction
|
||||
```
|
||||
|
||||
## Comments
|
||||
|
||||
Line comments start with `//`:
|
||||
```
|
||||
// This is a comment
|
||||
P : Sort; // Inline comment
|
||||
```
|
||||
|
||||
## Complete Example
|
||||
|
||||
```geolog
|
||||
// Directed graph theory
|
||||
theory Graph {
|
||||
V : Sort;
|
||||
E : Sort;
|
||||
src : E -> V;
|
||||
tgt : E -> V;
|
||||
}
|
||||
|
||||
// A triangle: A → B → C → A
|
||||
instance Triangle : Graph = {
|
||||
A : V;
|
||||
B : V;
|
||||
C : V;
|
||||
|
||||
ab : E;
|
||||
ab src = A;
|
||||
ab tgt = B;
|
||||
|
||||
bc : E;
|
||||
bc src = B;
|
||||
bc tgt = C;
|
||||
|
||||
ca : E;
|
||||
ca src = C;
|
||||
ca tgt = A;
|
||||
}
|
||||
```
|
||||
|
||||
## Grammar Summary (EBNF)
|
||||
|
||||
```ebnf
|
||||
file := declaration*
|
||||
|
||||
declaration := 'namespace' ident ';'
|
||||
| 'theory' params? ident '{' theory_item* '}'
|
||||
| 'instance' ident ':' type '=' '{' instance_item* '}'
|
||||
| 'query' ident ':' type '=' formula
|
||||
|
||||
params := ('(' param (',' param)* ')')+
|
||||
param := ident ':' type
|
||||
|
||||
theory_item := ident ':' 'Sort' ';'
|
||||
| path ':' type '->' type ';'
|
||||
| path ':' 'forall' qvars '.' formulas '|-' formula ';'
|
||||
|
||||
qvars := (ident (',' ident)* ':' type) (',' ...)*
|
||||
formulas := formula (',' formula)*
|
||||
|
||||
instance_item := ident ':' type ';'
|
||||
| term '=' term ';'
|
||||
| ident '=' '{' instance_item* '}' ';'
|
||||
|
||||
type := 'Sort' | 'Prop' | path | '[' fields ']' | type type | type '->' type | type 'instance'
|
||||
fields := (ident ':' type) (',' ...)*
|
||||
|
||||
term := path | '[' entries ']' | '(' term ')' | term term | term '.' ident
|
||||
entries := (ident ':' term) (',' ...)*
|
||||
|
||||
formula := term '=' term | term ident | 'exists' qvars '.' formula | formula '\/' formula | '(' formula ')'
|
||||
|
||||
path := ident ('/' ident)*
|
||||
ident := [a-zA-Z_][a-zA-Z0-9_]*
|
||||
```
|
||||
|
||||
## Example Files
|
||||
|
||||
The `examples/geolog/` directory contains working examples:
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `graph.geolog` | Simple directed graph theory with vertices and edges |
|
||||
| `preorder.geolog` | Preorder (reflexive, transitive relation) with discrete/chain instances |
|
||||
| `transitive_closure.geolog` | **Demonstrates chase algorithm** - computes reachability |
|
||||
| `monoid.geolog` | Algebraic monoid theory with associativity axiom |
|
||||
| `petri_net.geolog` | Petri net formalization with places, transitions, marking |
|
||||
| `petri_net_showcase.geolog` | **Full showcase** - parameterized theories, nested instances, cross-references |
|
||||
| `todo_list.geolog` | Task management example with dependencies |
|
||||
| `solver_demo.geolog` | Solver demonstration with reachability queries |
|
||||
| `relalg_simple.geolog` | Simple RelAlgIR query plan examples |
|
||||
|
||||
### Running Examples
|
||||
|
||||
```bash
|
||||
# Start REPL with an example
|
||||
cargo run -- examples/geolog/graph.geolog
|
||||
|
||||
# Or load interactively
|
||||
cargo run
|
||||
:source examples/geolog/transitive_closure.geolog
|
||||
:inspect Chain
|
||||
:chase Chain # Computes transitive closure!
|
||||
```
|
||||
168
examples/elaborate.rs
Normal file
168
examples/elaborate.rs
Normal file
@ -0,0 +1,168 @@
|
||||
use geolog::universe::Universe;
|
||||
use geolog::{
|
||||
elaborate::{ElaborationContext, Env, elaborate_instance_ctx, elaborate_theory},
|
||||
parse,
|
||||
repl::InstanceEntry,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
|
||||
fn main() {
|
||||
let input = r#"
|
||||
namespace VanillaPetriNets;
|
||||
|
||||
theory PetriNet {
|
||||
P : Sort;
|
||||
T : Sort;
|
||||
in : Sort;
|
||||
out : Sort;
|
||||
in/src : in -> P;
|
||||
in/tgt : in -> T;
|
||||
out/src : out -> T;
|
||||
out/tgt : out -> P;
|
||||
}
|
||||
|
||||
theory (N : PetriNet instance) Marking {
|
||||
token : Sort;
|
||||
token/of : token -> N/P;
|
||||
}
|
||||
|
||||
theory (X : Sort) (Y : Sort) Iso {
|
||||
fwd : X -> Y;
|
||||
bwd : Y -> X;
|
||||
fb : forall x : X. |- x fwd bwd = x;
|
||||
bf : forall y : Y. |- y bwd fwd = y;
|
||||
}
|
||||
|
||||
instance ExampleNet : PetriNet = {
|
||||
A : P;
|
||||
B : P;
|
||||
C : P;
|
||||
ab : T;
|
||||
ba : T;
|
||||
abc : T;
|
||||
ab_in : in;
|
||||
ab_in in/src = A;
|
||||
ab_in in/tgt = ab;
|
||||
ab_out : out;
|
||||
ab_out out/src = ab;
|
||||
ab_out out/tgt = B;
|
||||
ba_in : in;
|
||||
ba_in in/src = B;
|
||||
ba_in in/tgt = ba;
|
||||
ba_out : out;
|
||||
ba_out out/src = ba;
|
||||
ba_out out/tgt = A;
|
||||
abc_in1 : in;
|
||||
abc_in1 in/src = A;
|
||||
abc_in1 in/tgt = abc;
|
||||
abc_in2 : in;
|
||||
abc_in2 in/src = B;
|
||||
abc_in2 in/tgt = abc;
|
||||
abc_out : out;
|
||||
abc_out out/src = abc;
|
||||
abc_out out/tgt = C;
|
||||
}
|
||||
"#;
|
||||
|
||||
println!("=== PARSING ===");
|
||||
let file = match parse(input) {
|
||||
Ok(f) => f,
|
||||
Err(e) => {
|
||||
eprintln!("Parse error: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
println!("Parsed {} declarations\n", file.declarations.len());
|
||||
|
||||
println!("=== ELABORATING ===");
|
||||
let mut env = Env::new();
|
||||
let mut universe = Universe::new();
|
||||
|
||||
for decl in &file.declarations {
|
||||
match &decl.node {
|
||||
geolog::Declaration::Namespace(name) => {
|
||||
println!("Skipping namespace: {}", name);
|
||||
}
|
||||
geolog::Declaration::Theory(t) => {
|
||||
print!("Elaborating theory {}... ", t.name);
|
||||
match elaborate_theory(&mut env, t) {
|
||||
Ok(elab) => {
|
||||
println!("OK!");
|
||||
println!(
|
||||
" Params: {:?}",
|
||||
elab.params.iter().map(|p| &p.name).collect::<Vec<_>>()
|
||||
);
|
||||
println!(" Sorts: {:?}", elab.theory.signature.sorts);
|
||||
println!(
|
||||
" Functions: {:?}",
|
||||
elab.theory
|
||||
.signature
|
||||
.functions
|
||||
.iter()
|
||||
.map(|f| &f.name)
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
println!(" Axioms: {}", elab.theory.axioms.len());
|
||||
for (i, ax) in elab.theory.axioms.iter().enumerate() {
|
||||
println!(
|
||||
" [{i}] {} vars, premise -> conclusion",
|
||||
ax.context.vars.len()
|
||||
);
|
||||
}
|
||||
println!();
|
||||
|
||||
// Add to environment for dependent theories
|
||||
env.theories.insert(elab.theory.name.clone(), Rc::new(elab));
|
||||
}
|
||||
Err(e) => {
|
||||
println!("FAILED: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
geolog::Declaration::Instance(i) => {
|
||||
// Extract theory name from the type expression
|
||||
let theory_name = i.theory.as_single_path()
|
||||
.and_then(|p| p.segments.first().cloned())
|
||||
.unwrap_or_else(|| "?".to_string());
|
||||
print!("Elaborating instance {}... ", i.name);
|
||||
let instances: HashMap<String, InstanceEntry> = HashMap::new();
|
||||
let mut ctx = ElaborationContext {
|
||||
theories: &env.theories,
|
||||
instances: &instances,
|
||||
universe: &mut universe,
|
||||
siblings: HashMap::new(),
|
||||
};
|
||||
match elaborate_instance_ctx(&mut ctx, i) {
|
||||
Ok(result) => {
|
||||
let structure = &result.structure;
|
||||
println!("OK!");
|
||||
println!(" Theory: {}", theory_name);
|
||||
println!(" Elements: {} total", structure.len());
|
||||
for sort_id in 0..structure.carriers.len() {
|
||||
println!(
|
||||
" Sort {}: {} elements",
|
||||
sort_id,
|
||||
structure.carrier_size(sort_id)
|
||||
);
|
||||
}
|
||||
println!(" Functions defined:");
|
||||
for (fid, func_map) in structure.functions.iter().enumerate() {
|
||||
println!(" Func {}: {} mappings", fid, func_map.len());
|
||||
}
|
||||
println!();
|
||||
}
|
||||
Err(e) => {
|
||||
println!("FAILED: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
geolog::Declaration::Query(_) => {
|
||||
println!("Skipping query (not implemented yet)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("=== SUMMARY ===");
|
||||
println!("Elaborated {} theories", env.theories.len());
|
||||
}
|
||||
132
examples/full_petri.rs
Normal file
132
examples/full_petri.rs
Normal file
@ -0,0 +1,132 @@
|
||||
use geolog::parse;
|
||||
|
||||
fn main() {
|
||||
let input = r#"
|
||||
namespace VanillaPetriNets;
|
||||
|
||||
theory PetriNet {
|
||||
P : Sort;
|
||||
T : Sort;
|
||||
in : Sort;
|
||||
out : Sort;
|
||||
in/src : in -> P;
|
||||
in/tgt : in -> T;
|
||||
out/src : out -> T;
|
||||
out/tgt : out -> P;
|
||||
}
|
||||
|
||||
theory (N : PetriNet instance) Marking {
|
||||
token : Sort;
|
||||
token/of : token -> N/P;
|
||||
}
|
||||
|
||||
theory (N : PetriNet instance) ReachabilityProblem {
|
||||
initial_marking : N Marking instance;
|
||||
target_marking : N Marking instance;
|
||||
}
|
||||
|
||||
theory (N : PetriNet instance) Trace {
|
||||
F : Sort;
|
||||
F/of : F -> N/T;
|
||||
|
||||
W : Sort;
|
||||
W/src : W -> [firing : F, arc : N/out];
|
||||
W/tgt : W -> [firing : F, arc : N/in];
|
||||
|
||||
ax1 : forall w : W. |- w W/src .arc N/out/src = w W/src .firing F/of;
|
||||
ax2 : forall w : W. |- w W/tgt .arc N/in/tgt = w W/tgt .firing F/of;
|
||||
ax3 : forall w1, w2 : W. w1 W/src = w2 W/src |- w1 = w2;
|
||||
ax4 : forall w1, w2 : W. w1 W/tgt = w2 W/tgt |- w1 = w2;
|
||||
|
||||
input_terminal : Sort;
|
||||
output_terminal : Sort;
|
||||
input_terminal/of : input_terminal -> N/P;
|
||||
output_terminal/of : output_terminal -> N/P;
|
||||
input_terminal/tgt : input_terminal -> [firing : F, arc : N/in];
|
||||
output_terminal/src : output_terminal -> [firing : F, arc : N/out];
|
||||
|
||||
ax5 : forall f : F, arc : N/out. arc N/out/src = f F/of |-
|
||||
(exists w : W. w W/src = [firing: f, arc: arc]) \/
|
||||
(exists o : output_terminal. o output_terminal/src = [firing: f, arc: arc]);
|
||||
ax6 : forall f : F, arc : N/in. arc N/in/tgt = f F/of |-
|
||||
(exists w : W. w W/tgt = [firing: f, arc: arc]) \/
|
||||
(exists i : input_terminal. i input_terminal/tgt = [firing: f, arc: arc]);
|
||||
}
|
||||
|
||||
theory (X : Sort) (Y : Sort) Iso {
|
||||
fwd : X -> Y;
|
||||
bwd : Y -> X;
|
||||
fb : forall x : X. |- x fwd bwd = x;
|
||||
bf : forall y : Y. |- y bwd fwd = y;
|
||||
}
|
||||
|
||||
instance ExampleNet : PetriNet = {
|
||||
A : P;
|
||||
B : P;
|
||||
C : P;
|
||||
ab : T;
|
||||
ba : T;
|
||||
abc : T;
|
||||
ab_in : in;
|
||||
ab_in in/src = A;
|
||||
ab_in in/tgt = ab;
|
||||
ab_out : out;
|
||||
ab_out out/src = ab;
|
||||
ab_out out/tgt = B;
|
||||
ba_in : in;
|
||||
ba_in in/src = B;
|
||||
ba_in in/tgt = ba;
|
||||
ba_out : out;
|
||||
ba_out out/src = ba;
|
||||
ba_out out/tgt = A;
|
||||
abc_in1 : in;
|
||||
abc_in1 in/src = A;
|
||||
abc_in1 in/tgt = abc;
|
||||
abc_in2 : in;
|
||||
abc_in2 in/src = B;
|
||||
abc_in2 in/tgt = abc;
|
||||
abc_out : out;
|
||||
abc_out out/src = abc;
|
||||
abc_out out/tgt = C;
|
||||
}
|
||||
|
||||
// Reachability problem: can we get from A to B?
|
||||
instance problem0 : ExampleNet ReachabilityProblem = {
|
||||
initial_marking = {
|
||||
t : token;
|
||||
t token/of = ExampleNet/A;
|
||||
};
|
||||
target_marking = {
|
||||
t : token;
|
||||
t token/of = ExampleNet/B;
|
||||
};
|
||||
}
|
||||
|
||||
query findTrace {
|
||||
? : ExampleNet Trace instance;
|
||||
}
|
||||
"#;
|
||||
|
||||
match parse(input) {
|
||||
Ok(file) => {
|
||||
println!("Parsed successfully!");
|
||||
println!("Declarations: {}", file.declarations.len());
|
||||
for decl in &file.declarations {
|
||||
match &decl.node {
|
||||
geolog::Declaration::Namespace(n) => println!(" - namespace {}", n),
|
||||
geolog::Declaration::Theory(t) => {
|
||||
println!(" - theory {} ({} items)", t.name, t.body.len())
|
||||
}
|
||||
geolog::Declaration::Instance(i) => {
|
||||
println!(" - instance {} ({} items)", i.name, i.body.len())
|
||||
}
|
||||
geolog::Declaration::Query(q) => println!(" - query {}", q.name),
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Parse error: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
86
examples/geolog/category.geolog
Normal file
86
examples/geolog/category.geolog
Normal file
@ -0,0 +1,86 @@
|
||||
// Category theory in current geolog syntax
|
||||
//
|
||||
// This is the "desugared" version of the aspirational syntax in
|
||||
// loose_thoughts/2026-01-21_dependent_sorts_and_functional_relations.md
|
||||
|
||||
theory Category {
|
||||
ob : Sort;
|
||||
mor : Sort;
|
||||
|
||||
// Morphism source and target
|
||||
src : mor -> ob;
|
||||
tgt : mor -> ob;
|
||||
|
||||
// Composition: comp(f, g, h) means "h = f ; g" (f then g)
|
||||
// Domain constraint: f.tgt = g.src
|
||||
comp : [f: mor, g: mor, h: mor] -> Prop;
|
||||
|
||||
// Identity: id(a, f) means "f is the identity on a"
|
||||
id : [a: ob, f: mor] -> Prop;
|
||||
|
||||
// === Axioms ===
|
||||
|
||||
// Identity morphisms have matching source and target
|
||||
ax/id_src : forall x : ob, i : mor. [a: x, f: i] id |- i src = x;
|
||||
ax/id_tgt : forall x : ob, i : mor. [a: x, f: i] id |- i tgt = x;
|
||||
|
||||
// Composition domain constraint
|
||||
ax/comp_dom : forall p : mor, q : mor, r : mor.
|
||||
[f: p, g: q, h: r] comp |- p tgt = q src;
|
||||
|
||||
// Composition source/target
|
||||
ax/comp_src : forall p : mor, q : mor, r : mor.
|
||||
[f: p, g: q, h: r] comp |- r src = p src;
|
||||
ax/comp_tgt : forall p : mor, q : mor, r : mor.
|
||||
[f: p, g: q, h: r] comp |- r tgt = q tgt;
|
||||
|
||||
// Existence of identities (one per object)
|
||||
ax/id_exists : forall x : ob. |- exists i : mor. [a: x, f: i] id;
|
||||
|
||||
// Existence of composites (when composable)
|
||||
ax/comp_exists : forall p : mor, q : mor.
|
||||
p tgt = q src |- exists r : mor. [f: p, g: q, h: r] comp;
|
||||
|
||||
// Left unit: id_a ; f = f
|
||||
ax/unit_left : forall x : ob, i : mor, p : mor, r : mor.
|
||||
[a: x, f: i] id, p src = x, [f: i, g: p, h: r] comp |- r = p;
|
||||
|
||||
// Right unit: f ; id_b = f
|
||||
ax/unit_right : forall y : ob, i : mor, p : mor, r : mor.
|
||||
[a: y, f: i] id, p tgt = y, [f: p, g: i, h: r] comp |- r = p;
|
||||
|
||||
// Associativity: (f ; g) ; h = f ; (g ; h)
|
||||
ax/assoc : forall p : mor, q : mor, r : mor, pq : mor, qr : mor, pqr1 : mor, pqr2 : mor.
|
||||
[f: p, g: q, h: pq] comp, [f: pq, g: r, h: pqr1] comp,
|
||||
[f: q, g: r, h: qr] comp, [f: p, g: qr, h: pqr2] comp
|
||||
|- pqr1 = pqr2;
|
||||
|
||||
// Uniqueness of composition (functional)
|
||||
ax/comp_unique : forall p : mor, q : mor, r1 : mor, r2 : mor.
|
||||
[f: p, g: q, h: r1] comp, [f: p, g: q, h: r2] comp |- r1 = r2;
|
||||
|
||||
// Uniqueness of identity (one per object)
|
||||
ax/id_unique : forall x : ob, i1 : mor, i2 : mor.
|
||||
[a: x, f: i1] id, [a: x, f: i2] id |- i1 = i2;
|
||||
}
|
||||
|
||||
// The "walking arrow" category: A --f--> B
|
||||
//
|
||||
// Now we can declare just objects and non-identity morphisms!
|
||||
// The chase derives:
|
||||
// - Identity morphisms for each object (via ax/id_exists)
|
||||
// - Composition facts (via ax/comp_exists)
|
||||
// - Source/target for compositions (via ax/comp_src, ax/comp_tgt)
|
||||
//
|
||||
// The equality saturation (via congruence closure) collapses:
|
||||
// - id;id;id;... = id (via ax/unit_left and ax/unit_right)
|
||||
// - Duplicate compositions (via ax/comp_unique)
|
||||
// Without CC, the chase would loop forever creating id;id, id;id;id, ...
|
||||
instance Arrow : Category = chase {
|
||||
// Objects
|
||||
A : ob;
|
||||
B : ob;
|
||||
|
||||
// Non-identity morphism
|
||||
f : mor; f src = A; f tgt = B;
|
||||
}
|
||||
27
examples/geolog/field_projection_chase_test.geolog
Normal file
27
examples/geolog/field_projection_chase_test.geolog
Normal file
@ -0,0 +1,27 @@
|
||||
// Test: Field projection in chase
|
||||
|
||||
theory FieldProjectionChaseTest {
|
||||
A : Sort;
|
||||
B : Sort;
|
||||
|
||||
R : Sort;
|
||||
R/data : R -> [x: A, y: B];
|
||||
|
||||
// Marker sort for elements whose x field matches a given a
|
||||
XMatches : Sort;
|
||||
XMatches/r : XMatches -> R;
|
||||
XMatches/a : XMatches -> A;
|
||||
|
||||
// Axiom: if r's x field equals a, create an XMatches
|
||||
ax1 : forall r : R, a : A, b : B. r R/data = [x: a, y: b] |- exists m : XMatches. m XMatches/r = r, m XMatches/a = a;
|
||||
}
|
||||
|
||||
instance Test : FieldProjectionChaseTest = chase {
|
||||
a1 : A;
|
||||
a2 : A;
|
||||
b1 : B;
|
||||
r1 : R;
|
||||
r1 R/data = [x: a1, y: b1];
|
||||
r2 : R;
|
||||
r2 R/data = [x: a2, y: b1];
|
||||
}
|
||||
12
examples/geolog/field_projection_test.geolog
Normal file
12
examples/geolog/field_projection_test.geolog
Normal file
@ -0,0 +1,12 @@
|
||||
// Test: Field projection syntax
|
||||
|
||||
theory FieldProjectionTest {
|
||||
A : Sort;
|
||||
B : Sort;
|
||||
|
||||
R : Sort;
|
||||
R/data : R -> [x: A, y: B];
|
||||
|
||||
// Axiom using field projection: r R/data .x
|
||||
ax1 : forall r : R, a : A. r R/data .x = a |- true;
|
||||
}
|
||||
79
examples/geolog/graph.geolog
Normal file
79
examples/geolog/graph.geolog
Normal file
@ -0,0 +1,79 @@
|
||||
// Directed Graph: vertices and edges with source/target functions
|
||||
//
|
||||
// This is the canonical example of a "presheaf" - a functor from a small
|
||||
// category (the "walking arrow" • → •) to Set.
|
||||
|
||||
theory Graph {
|
||||
V : Sort; // Vertices
|
||||
E : Sort; // Edges
|
||||
|
||||
src : E -> V; // Source of an edge
|
||||
tgt : E -> V; // Target of an edge
|
||||
}
|
||||
|
||||
// A simple triangle graph: A → B → C → A
|
||||
instance Triangle : Graph = {
|
||||
// Vertices
|
||||
A : V;
|
||||
B : V;
|
||||
C : V;
|
||||
|
||||
// Edges
|
||||
ab : E;
|
||||
bc : E;
|
||||
ca : E;
|
||||
|
||||
// Edge endpoints
|
||||
ab src = A;
|
||||
ab tgt = B;
|
||||
bc src = B;
|
||||
bc tgt = C;
|
||||
ca src = C;
|
||||
ca tgt = A;
|
||||
}
|
||||
|
||||
// A self-loop: one vertex with an edge to itself
|
||||
instance Loop : Graph = {
|
||||
v : V;
|
||||
e : E;
|
||||
e src = v;
|
||||
e tgt = v;
|
||||
}
|
||||
|
||||
// The "walking arrow": two vertices, one edge
|
||||
instance Arrow : Graph = {
|
||||
s : V;
|
||||
t : V;
|
||||
f : E;
|
||||
f src = s;
|
||||
f tgt = t;
|
||||
}
|
||||
|
||||
// A more complex graph: diamond shape with two paths from top to bottom
|
||||
//
|
||||
// top
|
||||
// / \
|
||||
// left right
|
||||
// \ /
|
||||
// bottom
|
||||
//
|
||||
instance Diamond : Graph = {
|
||||
top : V;
|
||||
left : V;
|
||||
right : V;
|
||||
bottom : V;
|
||||
|
||||
top_left : E;
|
||||
top_right : E;
|
||||
left_bottom : E;
|
||||
right_bottom : E;
|
||||
|
||||
top_left src = top;
|
||||
top_left tgt = left;
|
||||
top_right src = top;
|
||||
top_right tgt = right;
|
||||
left_bottom src = left;
|
||||
left_bottom tgt = bottom;
|
||||
right_bottom src = right;
|
||||
right_bottom tgt = bottom;
|
||||
}
|
||||
29
examples/geolog/iso_instance_test.geolog
Normal file
29
examples/geolog/iso_instance_test.geolog
Normal file
@ -0,0 +1,29 @@
|
||||
// Multi-parameter theory instantiation test
|
||||
|
||||
theory (X : Sort) (Y : Sort) Iso {
|
||||
fwd : X -> Y;
|
||||
bwd : Y -> X;
|
||||
fb : forall x : X. |- x fwd bwd = x;
|
||||
bf : forall y : Y. |- y bwd fwd = y;
|
||||
}
|
||||
|
||||
theory A { a : Sort; }
|
||||
theory B { b : Sort; }
|
||||
|
||||
instance As : A = {
|
||||
a1 : a;
|
||||
a2 : a;
|
||||
}
|
||||
|
||||
instance Bs : B = {
|
||||
b1 : b;
|
||||
b2 : b;
|
||||
}
|
||||
|
||||
// Can we create an Iso instance with sort parameters?
|
||||
instance AB_Iso : As/a Bs/b Iso = {
|
||||
a1 fwd = Bs/b1;
|
||||
a2 fwd = Bs/b2;
|
||||
b1 bwd = As/a1;
|
||||
b2 bwd = As/a2;
|
||||
}
|
||||
9
examples/geolog/iso_theory_test.geolog
Normal file
9
examples/geolog/iso_theory_test.geolog
Normal file
@ -0,0 +1,9 @@
|
||||
// Multi-parameter theory test (Iso from vision)
|
||||
|
||||
// First just try sorts as parameters
|
||||
theory (X : Sort) (Y : Sort) Iso {
|
||||
fwd : X -> Y;
|
||||
bwd : Y -> X;
|
||||
// Axioms would need chained function application...
|
||||
// fb : forall x : X. |- x fwd bwd = x;
|
||||
}
|
||||
78
examples/geolog/monoid.geolog
Normal file
78
examples/geolog/monoid.geolog
Normal file
@ -0,0 +1,78 @@
|
||||
// Monoid: a set with an associative binary operation and identity element
|
||||
//
|
||||
// This is the simplest algebraic structure with interesting axioms.
|
||||
// Note: geolog uses postfix function application.
|
||||
|
||||
theory Monoid {
|
||||
M : Sort;
|
||||
|
||||
// Binary operation: M × M → M
|
||||
mul : [x: M, y: M] -> M;
|
||||
|
||||
// Identity element: we use a unary function from M to M that
|
||||
// "picks out" the identity (any x maps to e)
|
||||
// A cleaner approach would use Unit → M but that needs product support.
|
||||
id : M -> M;
|
||||
|
||||
// Left identity: id(x) * y = y (id(x) is always e)
|
||||
ax/left_id : forall x : M, y : M.
|
||||
|- [x: x id, y: y] mul = y;
|
||||
|
||||
// Right identity: x * id(y) = x
|
||||
ax/right_id : forall x : M, y : M.
|
||||
|- [x: x, y: y id] mul = x;
|
||||
|
||||
// Associativity: (x * y) * z = x * (y * z)
|
||||
ax/assoc : forall x : M, y : M, z : M.
|
||||
|- [x: [x: x, y: y] mul, y: z] mul = [x: x, y: [x: y, y: z] mul] mul;
|
||||
|
||||
// id is constant: id(x) = id(y) for all x, y
|
||||
ax/id_const : forall x : M, y : M.
|
||||
|- x id = y id;
|
||||
}
|
||||
|
||||
// Trivial monoid: single element, e * e = e
|
||||
instance Trivial : Monoid = {
|
||||
e : M;
|
||||
|
||||
// Multiplication table: e * e = e
|
||||
// Using positional syntax: [a, b] maps to [x: a, y: b]
|
||||
[e, e] mul = e;
|
||||
|
||||
// Identity: e is the identity element
|
||||
e id = e;
|
||||
}
|
||||
|
||||
// Boolean "And" monoid: {T, F} with T as identity
|
||||
// T and T = T, T and F = F, F and T = F, F and F = F
|
||||
instance BoolAnd : Monoid = {
|
||||
T : M;
|
||||
F : M;
|
||||
|
||||
// Identity: T is the identity element
|
||||
T id = T;
|
||||
F id = T;
|
||||
|
||||
// Multiplication table for "and":
|
||||
[x: T, y: T] mul = T;
|
||||
[x: T, y: F] mul = F;
|
||||
[x: F, y: T] mul = F;
|
||||
[x: F, y: F] mul = F;
|
||||
}
|
||||
|
||||
// Boolean "Or" monoid: {T, F} with F as identity
|
||||
// T or T = T, T or F = T, F or T = T, F or F = F
|
||||
instance BoolOr : Monoid = {
|
||||
T : M;
|
||||
F : M;
|
||||
|
||||
// Identity: F is the identity element
|
||||
T id = F;
|
||||
F id = F;
|
||||
|
||||
// Multiplication table for "or":
|
||||
[x: T, y: T] mul = T;
|
||||
[x: T, y: F] mul = T;
|
||||
[x: F, y: T] mul = T;
|
||||
[x: F, y: F] mul = F;
|
||||
}
|
||||
33
examples/geolog/nested_instance_test.geolog
Normal file
33
examples/geolog/nested_instance_test.geolog
Normal file
@ -0,0 +1,33 @@
|
||||
// Test: Nested instance declarations (following vision pattern)
|
||||
|
||||
theory Place {
|
||||
P : Sort;
|
||||
}
|
||||
|
||||
theory (Pl : Place instance) Token {
|
||||
token : Sort;
|
||||
token/of : token -> Pl/P;
|
||||
}
|
||||
|
||||
theory (Pl : Place instance) Problem {
|
||||
initial_marking : Pl Token instance;
|
||||
target_marking : Pl Token instance;
|
||||
}
|
||||
|
||||
// Create a place instance
|
||||
instance MyPlaces : Place = {
|
||||
p1 : P;
|
||||
p2 : P;
|
||||
}
|
||||
|
||||
// Test nested instance declarations
|
||||
instance TestProblem : MyPlaces Problem = {
|
||||
initial_marking = {
|
||||
t1 : token;
|
||||
t1 token/of = MyPlaces/p1;
|
||||
};
|
||||
target_marking = {
|
||||
t2 : token;
|
||||
t2 token/of = MyPlaces/p2;
|
||||
};
|
||||
}
|
||||
135
examples/geolog/petri_net.geolog
Normal file
135
examples/geolog/petri_net.geolog
Normal file
@ -0,0 +1,135 @@
|
||||
// Petri Net: a bipartite graph between places and transitions
|
||||
//
|
||||
// Petri nets model concurrent systems. Places hold tokens, transitions
|
||||
// fire when their input places have tokens, consuming inputs and
|
||||
// producing outputs.
|
||||
//
|
||||
// This encoding uses explicit "arc" sorts for input/output connections,
|
||||
// which is more faithful to the categorical semantics (a span).
|
||||
|
||||
theory PetriNet {
|
||||
P : Sort; // Places
|
||||
T : Sort; // Transitions
|
||||
In : Sort; // Input arcs (from place to transition)
|
||||
Out : Sort; // Output arcs (from transition to place)
|
||||
|
||||
// Input arc endpoints
|
||||
in/place : In -> P;
|
||||
in/trans : In -> T;
|
||||
|
||||
// Output arc endpoints
|
||||
out/trans : Out -> T;
|
||||
out/place : Out -> P;
|
||||
}
|
||||
|
||||
// A simple producer-consumer net:
|
||||
//
|
||||
// (ready) --[produce]--> (buffer) --[consume]--> (done)
|
||||
//
|
||||
instance ProducerConsumer : PetriNet = {
|
||||
// Places
|
||||
ready : P;
|
||||
buffer : P;
|
||||
done : P;
|
||||
|
||||
// Transitions
|
||||
produce : T;
|
||||
consume : T;
|
||||
|
||||
// Input arcs
|
||||
i1 : In;
|
||||
i1 in/place = ready;
|
||||
i1 in/trans = produce;
|
||||
|
||||
i2 : In;
|
||||
i2 in/place = buffer;
|
||||
i2 in/trans = consume;
|
||||
|
||||
// Output arcs
|
||||
o1 : Out;
|
||||
o1 out/trans = produce;
|
||||
o1 out/place = buffer;
|
||||
|
||||
o2 : Out;
|
||||
o2 out/trans = consume;
|
||||
o2 out/place = done;
|
||||
}
|
||||
|
||||
// Mutual exclusion: two processes competing for a shared resource
|
||||
//
|
||||
// (idle1) --[enter1]--> (crit1) --[exit1]--> (idle1)
|
||||
// ^ |
|
||||
// | (mutex) |
|
||||
// | v
|
||||
// (idle2) --[enter2]--> (crit2) --[exit2]--> (idle2)
|
||||
//
|
||||
instance MutualExclusion : PetriNet = {
|
||||
// Places for process 1
|
||||
idle1 : P;
|
||||
crit1 : P;
|
||||
|
||||
// Places for process 2
|
||||
idle2 : P;
|
||||
crit2 : P;
|
||||
|
||||
// Shared mutex token
|
||||
mutex : P;
|
||||
|
||||
// Transitions
|
||||
enter1 : T;
|
||||
exit1 : T;
|
||||
enter2 : T;
|
||||
exit2 : T;
|
||||
|
||||
// Process 1 enters: needs idle1 AND mutex
|
||||
i_enter1_idle : In;
|
||||
i_enter1_idle in/place = idle1;
|
||||
i_enter1_idle in/trans = enter1;
|
||||
|
||||
i_enter1_mutex : In;
|
||||
i_enter1_mutex in/place = mutex;
|
||||
i_enter1_mutex in/trans = enter1;
|
||||
|
||||
o_enter1 : Out;
|
||||
o_enter1 out/trans = enter1;
|
||||
o_enter1 out/place = crit1;
|
||||
|
||||
// Process 1 exits: releases mutex
|
||||
i_exit1 : In;
|
||||
i_exit1 in/place = crit1;
|
||||
i_exit1 in/trans = exit1;
|
||||
|
||||
o_exit1_idle : Out;
|
||||
o_exit1_idle out/trans = exit1;
|
||||
o_exit1_idle out/place = idle1;
|
||||
|
||||
o_exit1_mutex : Out;
|
||||
o_exit1_mutex out/trans = exit1;
|
||||
o_exit1_mutex out/place = mutex;
|
||||
|
||||
// Process 2 enters: needs idle2 AND mutex
|
||||
i_enter2_idle : In;
|
||||
i_enter2_idle in/place = idle2;
|
||||
i_enter2_idle in/trans = enter2;
|
||||
|
||||
i_enter2_mutex : In;
|
||||
i_enter2_mutex in/place = mutex;
|
||||
i_enter2_mutex in/trans = enter2;
|
||||
|
||||
o_enter2 : Out;
|
||||
o_enter2 out/trans = enter2;
|
||||
o_enter2 out/place = crit2;
|
||||
|
||||
// Process 2 exits: releases mutex
|
||||
i_exit2 : In;
|
||||
i_exit2 in/place = crit2;
|
||||
i_exit2 in/trans = exit2;
|
||||
|
||||
o_exit2_idle : Out;
|
||||
o_exit2_idle out/trans = exit2;
|
||||
o_exit2_idle out/place = idle2;
|
||||
|
||||
o_exit2_mutex : Out;
|
||||
o_exit2_mutex out/trans = exit2;
|
||||
o_exit2_mutex out/place = mutex;
|
||||
}
|
||||
195
examples/geolog/petri_net_full.geolog
Normal file
195
examples/geolog/petri_net_full.geolog
Normal file
@ -0,0 +1,195 @@
|
||||
// Full Petri Net Reachability - Type-Theoretic Encoding
|
||||
//
|
||||
// This demonstrates the complete type-theoretic encoding of Petri net
|
||||
// reachability from the original geolog design vision.
|
||||
//
|
||||
// Original design: loose_thoughts/2025-12-12_12:10_VanillaPetriNetRechability.md
|
||||
//
|
||||
// Key concepts:
|
||||
// - PetriNet: places, transitions, input/output arcs (with proper arc semantics)
|
||||
// - Marking: tokens in a net (parameterized by net)
|
||||
// - ReachabilityProblem: initial and target markings (nested instances)
|
||||
// - Trace: sequence of firings with wires connecting arcs
|
||||
// - Iso: isomorphism between two sorts (used for bijections)
|
||||
// - Solution: a trace with isomorphisms to markings
|
||||
//
|
||||
// This encoding is more type-theoretically precise than the simple
|
||||
// PlaceReachability: it tracks individual tokens and arc multiplicities,
|
||||
// enabling correct handling of "multi-token" transitions.
|
||||
|
||||
// ============================================================
|
||||
// THEORY: PetriNet
|
||||
// Basic structure: places, transitions, and arcs
|
||||
// ============================================================
|
||||
|
||||
theory PetriNet {
|
||||
P : Sort; // Places
|
||||
T : Sort; // Transitions
|
||||
|
||||
in : Sort; // Input arcs (place -> transition)
|
||||
out : Sort; // Output arcs (transition -> place)
|
||||
|
||||
// Each arc knows which place/transition it connects
|
||||
in/src : in -> P; // Input arc source place
|
||||
in/tgt : in -> T; // Input arc target transition
|
||||
out/src : out -> T; // Output arc source transition
|
||||
out/tgt : out -> P; // Output arc target place
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// THEORY: Marking (parameterized by N : PetriNet)
|
||||
// A marking assigns tokens to places
|
||||
// ============================================================
|
||||
|
||||
theory (N : PetriNet instance) Marking {
|
||||
token : Sort;
|
||||
token/of : token -> N/P;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// THEORY: ReachabilityProblem (parameterized by N : PetriNet)
|
||||
// Defines initial and target markings as nested instances
|
||||
// ============================================================
|
||||
|
||||
theory (N : PetriNet instance) ReachabilityProblem {
|
||||
initial_marking : N Marking instance;
|
||||
target_marking : N Marking instance;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// THEORY: Trace (parameterized by N : PetriNet)
|
||||
// A trace is a sequence of transition firings with "wires"
|
||||
// connecting input and output arcs
|
||||
// ============================================================
|
||||
|
||||
theory (N : PetriNet instance) Trace {
|
||||
// Firings
|
||||
F : Sort;
|
||||
F/of : F -> N/T;
|
||||
|
||||
// Wires connect output of one firing to input of another
|
||||
W : Sort;
|
||||
W/src : W -> [firing : F, arc : N/out]; // Wire source (firing, output arc)
|
||||
W/tgt : W -> [firing : F, arc : N/in]; // Wire target (firing, input arc)
|
||||
|
||||
// Wire coherence: output arc must belong to source firing's transition
|
||||
ax1 : forall w : W. |- w W/src .arc N/out/src = w W/src .firing F/of;
|
||||
// Wire coherence: input arc must belong to target firing's transition
|
||||
ax2 : forall w : W. |- w W/tgt .arc N/in/tgt = w W/tgt .firing F/of;
|
||||
|
||||
// Wire uniqueness: each (firing, out-arc) pair has at most one wire
|
||||
ax3 : forall w1, w2 : W. w1 W/src = w2 W/src |- w1 = w2;
|
||||
// Wire uniqueness: each (firing, in-arc) pair has at most one wire
|
||||
ax4 : forall w1, w2 : W. w1 W/tgt = w2 W/tgt |- w1 = w2;
|
||||
|
||||
// Terminals: for initial marking tokens (input) and final marking tokens (output)
|
||||
input_terminal : Sort;
|
||||
output_terminal : Sort;
|
||||
input_terminal/of : input_terminal -> N/P;
|
||||
output_terminal/of : output_terminal -> N/P;
|
||||
input_terminal/tgt : input_terminal -> [firing : F, arc : N/in];
|
||||
output_terminal/src : output_terminal -> [firing : F, arc : N/out];
|
||||
|
||||
// Every output arc of every firing must be wired OR be an output terminal
|
||||
ax5 : forall f : F, arc : N/out. arc N/out/src = f F/of |-
|
||||
(exists w : W. w W/src = [firing: f, arc: arc]) \/
|
||||
(exists o : output_terminal. o output_terminal/src = [firing: f, arc: arc]);
|
||||
|
||||
// Every input arc of every firing must be wired OR be an input terminal
|
||||
ax6 : forall f : F, arc : N/in. arc N/in/tgt = f F/of |-
|
||||
(exists w : W. w W/tgt = [firing: f, arc: arc]) \/
|
||||
(exists i : input_terminal. i input_terminal/tgt = [firing: f, arc: arc]);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// THEORY: Iso (parameterized by two sorts)
|
||||
// An isomorphism between two sorts
|
||||
// ============================================================
|
||||
|
||||
theory (X : Sort) (Y : Sort) Iso {
|
||||
fwd : X -> Y;
|
||||
bwd : Y -> X;
|
||||
fb : forall x : X. |- x fwd bwd = x;
|
||||
bf : forall y : Y. |- y bwd fwd = y;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// THEORY: Solution (parameterized by N and RP)
|
||||
// A solution to a reachability problem
|
||||
// ============================================================
|
||||
|
||||
theory (N : PetriNet instance) (RP : N ReachabilityProblem instance) Solution {
|
||||
// The witnessing trace
|
||||
trace : N Trace instance;
|
||||
|
||||
// Bijection between input terminals and initial marking tokens
|
||||
initial_marking_iso : (trace/input_terminal) (RP/initial_marking/token) Iso instance;
|
||||
|
||||
// Bijection between output terminals and target marking tokens
|
||||
target_marking_iso : (trace/output_terminal) (RP/target_marking/token) Iso instance;
|
||||
|
||||
// Initial marking commutes: token placement matches terminal placement
|
||||
initial_marking_P_comm : forall i : trace/input_terminal.
|
||||
|- i trace/input_terminal/of = i initial_marking_iso/fwd RP/initial_marking/token/of;
|
||||
|
||||
// Target marking commutes: token placement matches terminal placement
|
||||
target_marking_P_comm : forall o : trace/output_terminal.
|
||||
|- o trace/output_terminal/of = o target_marking_iso/fwd RP/target_marking/token/of;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// INSTANCE: ExampleNet - a small Petri net
|
||||
//
|
||||
// (A) --[ab]--> (B) --[bc]--> (C)
|
||||
// ^ |
|
||||
// +---[ba]------+
|
||||
// ============================================================
|
||||
|
||||
instance ExampleNet : PetriNet = {
|
||||
// Places
|
||||
A : P;
|
||||
B : P;
|
||||
C : P;
|
||||
|
||||
// Transitions
|
||||
ab : T;
|
||||
ba : T;
|
||||
bc : T;
|
||||
|
||||
// A -> B (via ab)
|
||||
ab_in : in;
|
||||
ab_in in/src = A;
|
||||
ab_in in/tgt = ab;
|
||||
ab_out : out;
|
||||
ab_out out/src = ab;
|
||||
ab_out out/tgt = B;
|
||||
|
||||
// B -> A (via ba)
|
||||
ba_in : in;
|
||||
ba_in in/src = B;
|
||||
ba_in in/tgt = ba;
|
||||
ba_out : out;
|
||||
ba_out out/src = ba;
|
||||
ba_out out/tgt = A;
|
||||
|
||||
// B -> C (via bc)
|
||||
bc_in : in;
|
||||
bc_in in/src = B;
|
||||
bc_in in/tgt = bc;
|
||||
bc_out : out;
|
||||
bc_out out/src = bc;
|
||||
bc_out out/tgt = C;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Example queries (once query solving is implemented):
|
||||
//
|
||||
// query can_reach_B_from_A {
|
||||
// ? : ExampleNet problem0 Solution instance;
|
||||
// }
|
||||
//
|
||||
// where problem0 : ExampleNet ReachabilityProblem = {
|
||||
// initial_marking = { tok : token; tok token/of = ExampleNet/A; };
|
||||
// target_marking = { tok : token; tok token/of = ExampleNet/B; };
|
||||
// }
|
||||
// ============================================================
|
||||
345
examples/geolog/petri_net_showcase.geolog
Normal file
345
examples/geolog/petri_net_showcase.geolog
Normal file
@ -0,0 +1,345 @@
|
||||
// Petri Net Reachability - Full Type-Theoretic Encoding
|
||||
//
|
||||
// This showcase demonstrates geolog's core capabilities through a
|
||||
// non-trivial domain: encoding Petri net reachability as dependent types.
|
||||
//
|
||||
// A solution to a reachability problem is NOT a yes/no boolean but a
|
||||
// CONSTRUCTIVE WITNESS: a diagrammatic proof that tokens can flow from
|
||||
// initial to target markings via a sequence of transition firings.
|
||||
//
|
||||
// Key concepts demonstrated:
|
||||
// - Parameterized theories (Marking depends on PetriNet instance)
|
||||
// - Nested instance types (ReachabilityProblem contains Marking instances)
|
||||
// - Sort-parameterized theories (Iso takes two sorts as parameters)
|
||||
// - Cross-instance references (solution's trace elements reference problem's tokens)
|
||||
//
|
||||
// Original design: loose_thoughts/2025-12-12_12:10_VanillaPetriNetRechability.md
|
||||
|
||||
// ============================================================
|
||||
// THEORY: PetriNet
|
||||
// Places, transitions, and arcs with proper arc semantics
|
||||
// ============================================================
|
||||
|
||||
theory PetriNet {
|
||||
P : Sort; // Places
|
||||
T : Sort; // Transitions
|
||||
in : Sort; // Input arcs (place -> transition)
|
||||
out : Sort; // Output arcs (transition -> place)
|
||||
|
||||
in/src : in -> P; // Input arc source place
|
||||
in/tgt : in -> T; // Input arc target transition
|
||||
out/src : out -> T; // Output arc source transition
|
||||
out/tgt : out -> P; // Output arc target place
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// THEORY: Marking (parameterized by N : PetriNet)
|
||||
// A marking assigns tokens to places
|
||||
// ============================================================
|
||||
|
||||
theory (N : PetriNet instance) Marking {
|
||||
token : Sort;
|
||||
token/of : token -> N/P;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// THEORY: ReachabilityProblem (parameterized by N : PetriNet)
|
||||
// Initial and target markings as nested instances
|
||||
// ============================================================
|
||||
|
||||
theory (N : PetriNet instance) ReachabilityProblem {
|
||||
initial_marking : N Marking instance;
|
||||
target_marking : N Marking instance;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// THEORY: Trace (parameterized by N : PetriNet)
|
||||
// A trace records transition firings and token flow via wires
|
||||
// ============================================================
|
||||
//
|
||||
// A trace is a diagrammatic proof of reachability:
|
||||
// - Firings represent transition occurrences
|
||||
// - Wires connect output arcs of firings to input arcs of other firings
|
||||
// - Terminals connect to the initial/target markings
|
||||
//
|
||||
// The completeness axiom (ax/must_be_fed) ensures every input arc
|
||||
// of every firing is accounted for - either wired from another firing
|
||||
// or fed by an input terminal.
|
||||
|
||||
theory (N : PetriNet instance) Trace {
|
||||
// Firings
|
||||
F : Sort;
|
||||
F/of : F -> N/T;
|
||||
|
||||
// Wires connect output arcs of firings to input arcs of other firings
|
||||
W : Sort;
|
||||
W/src_firing : W -> F;
|
||||
W/src_arc : W -> N/out;
|
||||
W/tgt_firing : W -> F;
|
||||
W/tgt_arc : W -> N/in;
|
||||
|
||||
// Wire coherence: source arc must belong to source firing's transition
|
||||
ax/wire_src_coherent : forall w : W.
|
||||
|- w W/src_arc N/out/src = w W/src_firing F/of;
|
||||
|
||||
// Wire coherence: target arc must belong to target firing's transition
|
||||
ax/wire_tgt_coherent : forall w : W.
|
||||
|- w W/tgt_arc N/in/tgt = w W/tgt_firing F/of;
|
||||
|
||||
// Wire place coherence: wire connects matching places
|
||||
ax/wire_place_coherent : forall w : W.
|
||||
|- w W/src_arc N/out/tgt = w W/tgt_arc N/in/src;
|
||||
|
||||
// Terminals
|
||||
input_terminal : Sort;
|
||||
output_terminal : Sort;
|
||||
input_terminal/of : input_terminal -> N/P;
|
||||
output_terminal/of : output_terminal -> N/P;
|
||||
|
||||
// Terminals connect to specific firings and arcs
|
||||
input_terminal/tgt_firing : input_terminal -> F;
|
||||
input_terminal/tgt_arc : input_terminal -> N/in;
|
||||
output_terminal/src_firing : output_terminal -> F;
|
||||
output_terminal/src_arc : output_terminal -> N/out;
|
||||
|
||||
// Terminal coherence axioms
|
||||
ax/input_terminal_coherent : forall i : input_terminal.
|
||||
|- i input_terminal/tgt_arc N/in/tgt = i input_terminal/tgt_firing F/of;
|
||||
|
||||
ax/output_terminal_coherent : forall o : output_terminal.
|
||||
|- o output_terminal/src_arc N/out/src = o output_terminal/src_firing F/of;
|
||||
|
||||
// Terminal place coherence
|
||||
ax/input_terminal_place : forall i : input_terminal.
|
||||
|- i input_terminal/of = i input_terminal/tgt_arc N/in/src;
|
||||
|
||||
ax/output_terminal_place : forall o : output_terminal.
|
||||
|- o output_terminal/of = o output_terminal/src_arc N/out/tgt;
|
||||
|
||||
// COMPLETENESS: Every arc of every firing must be accounted for.
|
||||
|
||||
// Input completeness: every input arc must be fed by a wire or input terminal
|
||||
ax/input_complete : forall f : F, arc : N/in.
|
||||
arc N/in/tgt = f F/of |-
|
||||
(exists w : W. w W/tgt_firing = f, w W/tgt_arc = arc) \/
|
||||
(exists i : input_terminal. i input_terminal/tgt_firing = f, i input_terminal/tgt_arc = arc);
|
||||
|
||||
// Output completeness: every output arc must be captured by a wire or output terminal
|
||||
ax/output_complete : forall f : F, arc : N/out.
|
||||
arc N/out/src = f F/of |-
|
||||
(exists w : W. w W/src_firing = f, w W/src_arc = arc) \/
|
||||
(exists o : output_terminal. o output_terminal/src_firing = f, o output_terminal/src_arc = arc);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// THEORY: Iso (parameterized by two sorts)
|
||||
// Isomorphism (bijection) between two sorts
|
||||
// ============================================================
|
||||
|
||||
theory (X : Sort) (Y : Sort) Iso {
|
||||
fwd : X -> Y;
|
||||
bwd : Y -> X;
|
||||
|
||||
// Roundtrip axioms ensure this is a true bijection
|
||||
fb : forall x : X. |- x fwd bwd = x;
|
||||
bf : forall y : Y. |- y bwd fwd = y;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// THEORY: Solution (parameterized by N and RP)
|
||||
// A constructive witness that target is reachable from initial
|
||||
// ============================================================
|
||||
|
||||
theory (N : PetriNet instance) (RP : N ReachabilityProblem instance) Solution {
|
||||
trace : N Trace instance;
|
||||
|
||||
// Bijection: input terminals <-> initial marking tokens
|
||||
initial_iso : (trace/input_terminal) (RP/initial_marking/token) Iso instance;
|
||||
|
||||
// Bijection: output terminals <-> target marking tokens
|
||||
target_iso : (trace/output_terminal) (RP/target_marking/token) Iso instance;
|
||||
|
||||
// Commutativity axioms (currently unchecked):
|
||||
// ax/init_comm : forall i : trace/input_terminal.
|
||||
// |- i trace/input_terminal/of = i initial_iso/fwd RP/initial_marking/token/of;
|
||||
// ax/target_comm : forall o : trace/output_terminal.
|
||||
// |- o trace/output_terminal/of = o target_iso/fwd RP/target_marking/token/of;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// INSTANCE: ExampleNet
|
||||
//
|
||||
// A Petri net with places A, B, C and transitions:
|
||||
// ab: consumes 1 token from A, produces 1 token in B
|
||||
// ba: consumes 1 token from B, produces 1 token in A
|
||||
// abc: consumes 1 token from A AND 1 from B, produces 1 token in C
|
||||
//
|
||||
// +---[ba]----+
|
||||
// v |
|
||||
// (A) --[ab]->(B) --+
|
||||
// | |
|
||||
// +----[abc]-------+--> (C)
|
||||
//
|
||||
// The abc transition is interesting: it requires BOTH an A-token
|
||||
// and a B-token to fire, producing a C-token.
|
||||
// ============================================================
|
||||
|
||||
instance ExampleNet : PetriNet = {
|
||||
A : P; B : P; C : P;
|
||||
ab : T; ba : T; abc : T;
|
||||
|
||||
// A -> B (via ab)
|
||||
ab_in : in; ab_in in/src = A; ab_in in/tgt = ab;
|
||||
ab_out : out; ab_out out/src = ab; ab_out out/tgt = B;
|
||||
|
||||
// B -> A (via ba)
|
||||
ba_in : in; ba_in in/src = B; ba_in in/tgt = ba;
|
||||
ba_out : out; ba_out out/src = ba; ba_out out/tgt = A;
|
||||
|
||||
// A + B -> C (via abc) - note: two input arcs!
|
||||
abc_in1 : in; abc_in1 in/src = A; abc_in1 in/tgt = abc;
|
||||
abc_in2 : in; abc_in2 in/src = B; abc_in2 in/tgt = abc;
|
||||
abc_out : out; abc_out out/src = abc; abc_out out/tgt = C;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// PROBLEM 0: Can we reach B from A with one token?
|
||||
// Initial: 1 token in A
|
||||
// Target: 1 token in B
|
||||
// ============================================================
|
||||
|
||||
instance problem0 : ExampleNet ReachabilityProblem = {
|
||||
initial_marking = {
|
||||
tok : token;
|
||||
tok token/of = ExampleNet/A;
|
||||
};
|
||||
target_marking = {
|
||||
tok : token;
|
||||
tok token/of = ExampleNet/B;
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// SOLUTION 0: Yes! Fire transition 'ab' once.
|
||||
//
|
||||
// This Solution instance is a CONSTRUCTIVE PROOF:
|
||||
// - The trace contains one firing (f1) of transition 'ab'
|
||||
// - The input terminal feeds the A-token into f1's input arc
|
||||
// - The output terminal captures f1's B-token output
|
||||
// - The isomorphisms prove the token counts match exactly
|
||||
// ============================================================
|
||||
|
||||
instance solution0 : ExampleNet problem0 Solution = {
|
||||
trace = {
|
||||
// One firing of transition 'ab'
|
||||
f1 : F;
|
||||
f1 F/of = ExampleNet/ab;
|
||||
|
||||
// Input terminal: feeds the initial A-token into f1
|
||||
it : input_terminal;
|
||||
it input_terminal/of = ExampleNet/A;
|
||||
it input_terminal/tgt_firing = f1;
|
||||
it input_terminal/tgt_arc = ExampleNet/ab_in;
|
||||
|
||||
// Output terminal: captures f1's B-token output
|
||||
ot : output_terminal;
|
||||
ot output_terminal/of = ExampleNet/B;
|
||||
ot output_terminal/src_firing = f1;
|
||||
ot output_terminal/src_arc = ExampleNet/ab_out;
|
||||
};
|
||||
|
||||
initial_iso = {
|
||||
trace/it fwd = problem0/initial_marking/tok;
|
||||
problem0/initial_marking/tok bwd = trace/it;
|
||||
};
|
||||
|
||||
target_iso = {
|
||||
trace/ot fwd = problem0/target_marking/tok;
|
||||
problem0/target_marking/tok bwd = trace/ot;
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// PROBLEM 2: Can we reach C from two A-tokens?
|
||||
// Initial: 2 tokens in A
|
||||
// Target: 1 token in C
|
||||
//
|
||||
// This is interesting because the only path to C is via 'abc',
|
||||
// which requires tokens in BOTH A and B simultaneously.
|
||||
// ============================================================
|
||||
|
||||
instance problem2 : ExampleNet ReachabilityProblem = {
|
||||
initial_marking = {
|
||||
t1 : token; t1 token/of = ExampleNet/A;
|
||||
t2 : token; t2 token/of = ExampleNet/A;
|
||||
};
|
||||
target_marking = {
|
||||
t : token;
|
||||
t token/of = ExampleNet/C;
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// SOLUTION 2: Yes! Fire 'ab' then 'abc'.
|
||||
//
|
||||
// Token flow diagram:
|
||||
//
|
||||
// [it1]--A-->[f1: ab]--B--wire-->[f2: abc]--C-->[ot]
|
||||
// [it2]--A-----------------^
|
||||
//
|
||||
// Step 1: Fire 'ab' to move one token A -> B
|
||||
// - it1 feeds A-token into f1 via ab_in
|
||||
// - f1 produces B-token via ab_out
|
||||
// Step 2: Fire 'abc' consuming one A-token and one B-token
|
||||
// - it2 feeds A-token into f2 via abc_in1
|
||||
// - Wire connects f1's ab_out to f2's abc_in2 (the B-input)
|
||||
// - f2 produces C-token via abc_out
|
||||
// ============================================================
|
||||
|
||||
instance solution2 : ExampleNet problem2 Solution = {
|
||||
trace = {
|
||||
// Two firings
|
||||
f1 : F; f1 F/of = ExampleNet/ab; // First: A -> B
|
||||
f2 : F; f2 F/of = ExampleNet/abc; // Second: A + B -> C
|
||||
|
||||
// Wire connecting f1's B-output to f2's B-input
|
||||
// This is the crucial connection that makes the trace valid!
|
||||
w1 : W;
|
||||
w1 W/src_firing = f1;
|
||||
w1 W/src_arc = ExampleNet/ab_out;
|
||||
w1 W/tgt_firing = f2;
|
||||
w1 W/tgt_arc = ExampleNet/abc_in2;
|
||||
|
||||
// Input terminal 1: feeds first A-token into f1
|
||||
it1 : input_terminal;
|
||||
it1 input_terminal/of = ExampleNet/A;
|
||||
it1 input_terminal/tgt_firing = f1;
|
||||
it1 input_terminal/tgt_arc = ExampleNet/ab_in;
|
||||
|
||||
// Input terminal 2: feeds second A-token into f2
|
||||
it2 : input_terminal;
|
||||
it2 input_terminal/of = ExampleNet/A;
|
||||
it2 input_terminal/tgt_firing = f2;
|
||||
it2 input_terminal/tgt_arc = ExampleNet/abc_in1;
|
||||
|
||||
// Output terminal: captures f2's C-token output
|
||||
ot : output_terminal;
|
||||
ot output_terminal/of = ExampleNet/C;
|
||||
ot output_terminal/src_firing = f2;
|
||||
ot output_terminal/src_arc = ExampleNet/abc_out;
|
||||
};
|
||||
|
||||
// Bijection: 2 input terminals <-> 2 initial tokens
|
||||
initial_iso = {
|
||||
trace/it1 fwd = problem2/initial_marking/t1;
|
||||
trace/it2 fwd = problem2/initial_marking/t2;
|
||||
problem2/initial_marking/t1 bwd = trace/it1;
|
||||
problem2/initial_marking/t2 bwd = trace/it2;
|
||||
};
|
||||
|
||||
// Bijection: 1 output terminal <-> 1 target token
|
||||
target_iso = {
|
||||
trace/ot fwd = problem2/target_marking/t;
|
||||
problem2/target_marking/t bwd = trace/ot;
|
||||
};
|
||||
}
|
||||
188
examples/geolog/petri_net_solution.geolog
Normal file
188
examples/geolog/petri_net_solution.geolog
Normal file
@ -0,0 +1,188 @@
|
||||
// Full Petri Net Reachability with Synthesized Solution
|
||||
//
|
||||
// This file contains the complete type-theoretic encoding of Petri net
|
||||
// reachability, plus a manually synthesized solution proving that place B
|
||||
// is reachable from place A in the example net.
|
||||
//
|
||||
// ============================================================
|
||||
// This instance was synthesized automatically by Claude Opus 4.5.
|
||||
// As was this entire file, and this entire project, really.
|
||||
// ============================================================
|
||||
|
||||
// ============================================================
|
||||
// THEORY: PetriNet - Basic structure with arc semantics
|
||||
// ============================================================
|
||||
|
||||
theory PetriNet {
|
||||
P : Sort; // Places
|
||||
T : Sort; // Transitions
|
||||
in : Sort; // Input arcs (place -> transition)
|
||||
out : Sort; // Output arcs (transition -> place)
|
||||
|
||||
in/src : in -> P; // Input arc source place
|
||||
in/tgt : in -> T; // Input arc target transition
|
||||
out/src : out -> T; // Output arc source transition
|
||||
out/tgt : out -> P; // Output arc target place
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// THEORY: Marking - Tokens parameterized by a net
|
||||
// ============================================================
|
||||
|
||||
theory (N : PetriNet instance) Marking {
|
||||
token : Sort;
|
||||
token/of : token -> N/P; // Which place each token is in
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// THEORY: ReachabilityProblem - Initial and target markings
|
||||
// ============================================================
|
||||
|
||||
theory (N : PetriNet instance) ReachabilityProblem {
|
||||
initial_marking : N Marking instance;
|
||||
target_marking : N Marking instance;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// THEORY: Trace - A sequence of transition firings with wires
|
||||
// ============================================================
|
||||
//
|
||||
// Simplified version for now - full version with product types commented out below.
|
||||
|
||||
theory (N : PetriNet instance) Trace {
|
||||
F : Sort; // Firings
|
||||
F/of : F -> N/T; // Which transition each fires
|
||||
|
||||
// Terminals for initial/final marking tokens
|
||||
input_terminal : Sort;
|
||||
output_terminal : Sort;
|
||||
input_terminal/of : input_terminal -> N/P;
|
||||
output_terminal/of : output_terminal -> N/P;
|
||||
}
|
||||
|
||||
// Full Trace theory with wires and product types (not yet fully supported):
|
||||
//
|
||||
// theory (N : PetriNet instance) Trace {
|
||||
// F : Sort; // Firings
|
||||
// F/of : F -> N/T; // Which transition each fires
|
||||
//
|
||||
// W : Sort; // Wires connecting firings
|
||||
// W/src : W -> [firing : F, arc : N/out]; // Wire source
|
||||
// W/tgt : W -> [firing : F, arc : N/in]; // Wire target
|
||||
//
|
||||
// // Wire coherence axioms
|
||||
// ax1 : forall w : W. |- w W/src .arc N/out/src = w W/src .firing F/of;
|
||||
// ax2 : forall w : W. |- w W/tgt .arc N/in/tgt = w W/tgt .firing F/of;
|
||||
//
|
||||
// // Wire uniqueness
|
||||
// ax3 : forall w1, w2 : W. w1 W/src = w2 W/src |- w1 = w2;
|
||||
// ax4 : forall w1, w2 : W. w1 W/tgt = w2 W/tgt |- w1 = w2;
|
||||
//
|
||||
// // Terminals for initial/final marking tokens
|
||||
// input_terminal : Sort;
|
||||
// output_terminal : Sort;
|
||||
// input_terminal/of : input_terminal -> N/P;
|
||||
// output_terminal/of : output_terminal -> N/P;
|
||||
// input_terminal/tgt : input_terminal -> [firing : F, arc : N/in];
|
||||
// output_terminal/src : output_terminal -> [firing : F, arc : N/out];
|
||||
//
|
||||
// // Coverage axioms
|
||||
// ax5 : forall f : F, arc : N/out. arc N/out/src = f F/of |-
|
||||
// (exists w : W. w W/src = [firing: f, arc: arc]) \/
|
||||
// (exists o : output_terminal. o output_terminal/src = [firing: f, arc: arc]);
|
||||
// ax6 : forall f : F, arc : N/in. arc N/in/tgt = f F/of |-
|
||||
// (exists w : W. w W/tgt = [firing: f, arc: arc]) \/
|
||||
// (exists i : input_terminal. i input_terminal/tgt = [firing: f, arc: arc]);
|
||||
// }
|
||||
|
||||
// ============================================================
|
||||
// THEORY: Iso - Isomorphism between two sorts
|
||||
// ============================================================
|
||||
|
||||
theory (X : Sort) (Y : Sort) Iso {
|
||||
fwd : X -> Y;
|
||||
bwd : Y -> X;
|
||||
fb : forall x : X. |- x fwd bwd = x;
|
||||
bf : forall y : Y. |- y bwd fwd = y;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// THEORY: Solution - A complete reachability witness
|
||||
// ============================================================
|
||||
|
||||
theory (N : PetriNet instance) (RP : N ReachabilityProblem instance) Solution {
|
||||
trace : N Trace instance;
|
||||
|
||||
initial_iso : (trace/input_terminal) (RP/initial_marking/token) Iso instance;
|
||||
target_iso : (trace/output_terminal) (RP/target_marking/token) Iso instance;
|
||||
|
||||
ax/init_comm : forall i : trace/input_terminal.
|
||||
|- i trace/input_terminal/of = i initial_iso/fwd RP/initial_marking/token/of;
|
||||
ax/target_comm : forall o : trace/output_terminal.
|
||||
|- o trace/output_terminal/of = o target_iso/fwd RP/target_marking/token/of;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// INSTANCE: ExampleNet - A small Petri net
|
||||
//
|
||||
// (A) --[ab]--> (B) --[bc]--> (C)
|
||||
// ^ |
|
||||
// +---[ba]------+
|
||||
// ============================================================
|
||||
|
||||
instance ExampleNet : PetriNet = {
|
||||
A : P; B : P; C : P;
|
||||
ab : T; ba : T; bc : T;
|
||||
|
||||
ab_in : in; ab_in in/src = A; ab_in in/tgt = ab;
|
||||
ab_out : out; ab_out out/src = ab; ab_out out/tgt = B;
|
||||
|
||||
ba_in : in; ba_in in/src = B; ba_in in/tgt = ba;
|
||||
ba_out : out; ba_out out/src = ba; ba_out out/tgt = A;
|
||||
|
||||
bc_in : in; bc_in in/src = B; bc_in in/tgt = bc;
|
||||
bc_out : out; bc_out out/src = bc; bc_out out/tgt = C;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// INSTANCE: problem0 - Can we reach B from A?
|
||||
// ============================================================
|
||||
|
||||
instance problem0 : ExampleNet ReachabilityProblem = {
|
||||
initial_marking = {
|
||||
tok : token;
|
||||
tok token/of = ExampleNet/A;
|
||||
};
|
||||
target_marking = {
|
||||
tok : token;
|
||||
tok token/of = ExampleNet/B;
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// INSTANCE: solution0 - YES! Here's the proof.
|
||||
// ============================================================
|
||||
// This instance was synthesized automatically by Claude Opus 4.5.
|
||||
// ============================================================
|
||||
|
||||
// The solution proves that place B is reachable from place A by firing
|
||||
// transition ab. This creates a trace with one firing and the necessary
|
||||
// input/output terminal mappings.
|
||||
|
||||
instance solution0 : ExampleNet problem0 Solution = {
|
||||
trace = {
|
||||
f1 : F;
|
||||
f1 F/of = ExampleNet/ab;
|
||||
|
||||
it : input_terminal;
|
||||
it input_terminal/of = ExampleNet/A;
|
||||
|
||||
ot : output_terminal;
|
||||
ot output_terminal/of = ExampleNet/B;
|
||||
};
|
||||
|
||||
// NOTE: Cross-instance references (e.g., trace/it in initial_iso)
|
||||
// are not yet fully supported. The iso instances would map:
|
||||
// - trace/it <-> problem0/initial_marking/tok
|
||||
// - trace/ot <-> problem0/target_marking/tok
|
||||
}
|
||||
164
examples/geolog/petri_reachability.geolog
Normal file
164
examples/geolog/petri_reachability.geolog
Normal file
@ -0,0 +1,164 @@
|
||||
// Petri Net Reachability - Full Example
|
||||
//
|
||||
// This demonstrates the core ideas from the original geolog design document:
|
||||
// modeling Petri net reachability using geometric logic with the chase algorithm.
|
||||
//
|
||||
// Original design: loose_thoughts/2025-12-12_12:10.md
|
||||
//
|
||||
// Key concepts:
|
||||
// - PetriNet: places, transitions, input/output arcs
|
||||
// - Marking: assignment of tokens to places (parameterized theory)
|
||||
// - Trace: sequence of transition firings connecting markings
|
||||
// - Reachability: computed via chase algorithm
|
||||
|
||||
// ============================================================
|
||||
// THEORY: PetriNet
|
||||
// ============================================================
|
||||
|
||||
theory PetriNet {
|
||||
P : Sort; // Places
|
||||
T : Sort; // Transitions
|
||||
In : Sort; // Input arcs (place -> transition)
|
||||
Out : Sort; // Output arcs (transition -> place)
|
||||
|
||||
// Arc structure
|
||||
in/place : In -> P;
|
||||
in/trans : In -> T;
|
||||
out/trans : Out -> T;
|
||||
out/place : Out -> P;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// THEORY: Marking (parameterized)
|
||||
// A marking assigns tokens to places in a specific net
|
||||
// ============================================================
|
||||
|
||||
theory (N : PetriNet instance) Marking {
|
||||
Token : Sort;
|
||||
of : Token -> N/P;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// THEORY: PlaceReachability
|
||||
// Simplified reachability at the place level
|
||||
// ============================================================
|
||||
|
||||
theory PlaceReachability {
|
||||
P : Sort;
|
||||
T : Sort;
|
||||
|
||||
// Which transition connects which places
|
||||
// Fires(t, from, to) means transition t can move a token from 'from' to 'to'
|
||||
Fires : [trans: T, from: P, to: P] -> Prop;
|
||||
|
||||
// Reachability relation (transitive closure)
|
||||
CanReach : [from: P, to: P] -> Prop;
|
||||
|
||||
// Reflexivity: every place can reach itself
|
||||
ax/refl : forall p : P.
|
||||
|- [from: p, to: p] CanReach;
|
||||
|
||||
// Transition firing creates reachability
|
||||
ax/fire : forall t : T, x : P, y : P.
|
||||
[trans: t, from: x, to: y] Fires |- [from: x, to: y] CanReach;
|
||||
|
||||
// Transitivity: reachability composes
|
||||
ax/trans : forall x : P, y : P, z : P.
|
||||
[from: x, to: y] CanReach, [from: y, to: z] CanReach |- [from: x, to: z] CanReach;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// INSTANCE: SimpleNet
|
||||
// A -> B -> C with bidirectional A <-> B
|
||||
//
|
||||
// (A) <--[ba]-- (B) --[bc]--> (C)
|
||||
// | ^
|
||||
// +---[ab]------+
|
||||
// ============================================================
|
||||
|
||||
// Uses chase to derive CanReach from axioms (reflexivity, fire, transitivity)
|
||||
instance SimpleNet : PlaceReachability = chase {
|
||||
// Places
|
||||
A : P;
|
||||
B : P;
|
||||
C : P;
|
||||
|
||||
// Transitions
|
||||
ab : T; // A -> B
|
||||
ba : T; // B -> A
|
||||
bc : T; // B -> C
|
||||
|
||||
// Firing relations
|
||||
[trans: ab, from: A, to: B] Fires;
|
||||
[trans: ba, from: B, to: A] Fires;
|
||||
[trans: bc, from: B, to: C] Fires;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// INSTANCE: MutexNet
|
||||
// Two processes competing for a mutex
|
||||
//
|
||||
// idle1 --[enter1]--> crit1 --[exit1]--> idle1
|
||||
// ^ |
|
||||
// | mutex |
|
||||
// | v
|
||||
// idle2 --[enter2]--> crit2 --[exit2]--> idle2
|
||||
// ============================================================
|
||||
|
||||
// Uses chase to derive reachability relation
|
||||
instance MutexNet : PlaceReachability = chase {
|
||||
// Places
|
||||
idle1 : P;
|
||||
crit1 : P;
|
||||
idle2 : P;
|
||||
crit2 : P;
|
||||
mutex : P;
|
||||
|
||||
// Transitions
|
||||
enter1 : T;
|
||||
exit1 : T;
|
||||
enter2 : T;
|
||||
exit2 : T;
|
||||
|
||||
// Process 1 acquires mutex: idle1 + mutex -> crit1
|
||||
// (simplified: we track place-level, not token-level)
|
||||
[trans: enter1, from: idle1, to: crit1] Fires;
|
||||
[trans: enter1, from: mutex, to: crit1] Fires;
|
||||
|
||||
// Process 1 releases mutex: crit1 -> idle1 + mutex
|
||||
[trans: exit1, from: crit1, to: idle1] Fires;
|
||||
[trans: exit1, from: crit1, to: mutex] Fires;
|
||||
|
||||
// Process 2 acquires mutex: idle2 + mutex -> crit2
|
||||
[trans: enter2, from: idle2, to: crit2] Fires;
|
||||
[trans: enter2, from: mutex, to: crit2] Fires;
|
||||
|
||||
// Process 2 releases mutex: crit2 -> idle2 + mutex
|
||||
[trans: exit2, from: crit2, to: idle2] Fires;
|
||||
[trans: exit2, from: crit2, to: mutex] Fires;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// INSTANCE: ProducerConsumerNet
|
||||
// Producer creates items, consumer processes them
|
||||
//
|
||||
// ready --[produce]--> buffer --[consume]--> done
|
||||
// ============================================================
|
||||
|
||||
// Uses chase to derive reachability relation
|
||||
instance ProducerConsumerNet : PlaceReachability = chase {
|
||||
// Places
|
||||
ready : P;
|
||||
buffer : P;
|
||||
done : P;
|
||||
|
||||
// Transitions
|
||||
produce : T;
|
||||
consume : T;
|
||||
|
||||
// Produce: ready -> buffer
|
||||
[trans: produce, from: ready, to: buffer] Fires;
|
||||
|
||||
// Consume: buffer -> done
|
||||
[trans: consume, from: buffer, to: done] Fires;
|
||||
}
|
||||
72
examples/geolog/petri_reachability_full_vision.geolog
Normal file
72
examples/geolog/petri_reachability_full_vision.geolog
Normal file
@ -0,0 +1,72 @@
|
||||
// Full Petri Net Reachability Vision Test
|
||||
// From 2025-12-12_12:10_VanillaPetriNetRechability.md
|
||||
|
||||
theory PetriNet {
|
||||
P : Sort;
|
||||
T : Sort;
|
||||
in : Sort;
|
||||
out : Sort;
|
||||
in/src : in -> P;
|
||||
in/tgt : in -> T;
|
||||
out/src : out -> T;
|
||||
out/tgt : out -> P;
|
||||
}
|
||||
|
||||
theory (N : PetriNet instance) Marking {
|
||||
token : Sort;
|
||||
token/of : token -> N/P;
|
||||
}
|
||||
|
||||
theory (N : PetriNet instance) ReachabilityProblem {
|
||||
initial_marking : N Marking instance;
|
||||
target_marking : N Marking instance;
|
||||
}
|
||||
|
||||
// Simplified Trace theory without disjunctions for now
|
||||
theory (N : PetriNet instance) SimpleTrace {
|
||||
F : Sort;
|
||||
F/of : F -> N/T;
|
||||
|
||||
input_terminal : Sort;
|
||||
output_terminal : Sort;
|
||||
input_terminal/of : input_terminal -> N/P;
|
||||
output_terminal/of : output_terminal -> N/P;
|
||||
input_terminal/tgt : input_terminal -> [firing : F, arc : N/in];
|
||||
output_terminal/src : output_terminal -> [firing : F, arc : N/out];
|
||||
|
||||
// Simplified ax5: every firing+arc gets an output terminal
|
||||
ax5 : forall f : F, arc : N/out. |- exists o : output_terminal. o output_terminal/src = [firing: f, arc: arc];
|
||||
|
||||
// Simplified ax6: every firing+arc gets an input terminal
|
||||
ax6 : forall f : F, arc : N/in. |- exists i : input_terminal. i input_terminal/tgt = [firing: f, arc: arc];
|
||||
}
|
||||
|
||||
instance ExampleNet : PetriNet = {
|
||||
A : P;
|
||||
B : P;
|
||||
ab : T;
|
||||
ab_in : in;
|
||||
ab_in in/src = A;
|
||||
ab_in in/tgt = ab;
|
||||
ab_out : out;
|
||||
ab_out out/src = ab;
|
||||
ab_out out/tgt = B;
|
||||
}
|
||||
|
||||
// Test nested instance elaboration
|
||||
instance problem0 : ExampleNet ReachabilityProblem = {
|
||||
initial_marking = {
|
||||
tok : token;
|
||||
tok token/of = ExampleNet/A;
|
||||
};
|
||||
target_marking = {
|
||||
tok : token;
|
||||
tok token/of = ExampleNet/B;
|
||||
};
|
||||
}
|
||||
|
||||
// Test chase with SimpleTrace
|
||||
instance trace0 : ExampleNet SimpleTrace = chase {
|
||||
f1 : F;
|
||||
f1 F/of = ExampleNet/ab;
|
||||
}
|
||||
94
examples/geolog/petri_reachability_vision.geolog
Normal file
94
examples/geolog/petri_reachability_vision.geolog
Normal file
@ -0,0 +1,94 @@
|
||||
// Petri Net Reachability Vision Test
|
||||
// Based on 2025-12-12 design document
|
||||
|
||||
// Basic Petri net structure
|
||||
theory PetriNet {
|
||||
// Places
|
||||
P : Sort;
|
||||
|
||||
// Transitions
|
||||
T : Sort;
|
||||
|
||||
// Arcs (input to transitions, output from transitions)
|
||||
in : Sort;
|
||||
out : Sort;
|
||||
in/src : in -> P;
|
||||
in/tgt : in -> T;
|
||||
out/src : out -> T;
|
||||
out/tgt : out -> P;
|
||||
}
|
||||
|
||||
// A marking is a multiset of tokens, each at a place
|
||||
theory (N : PetriNet instance) Marking {
|
||||
token : Sort;
|
||||
token/of : token -> N/P;
|
||||
}
|
||||
|
||||
// A reachability problem is: can we get from initial marking to target?
|
||||
theory (N : PetriNet instance) ReachabilityProblem {
|
||||
initial_marking : N Marking instance;
|
||||
target_marking : N Marking instance;
|
||||
}
|
||||
|
||||
// A trace is a sequence of firings connected by wires
|
||||
theory (N : PetriNet instance) Trace {
|
||||
// Firings of transitions
|
||||
F : Sort;
|
||||
F/of : F -> N/T;
|
||||
|
||||
// Wires connect firing outputs to firing inputs
|
||||
W : Sort;
|
||||
W/src : W -> [firing : F, arc : N/out];
|
||||
W/tgt : W -> [firing : F, arc : N/in];
|
||||
|
||||
// Terminals are unconnected arc endpoints (to/from the initial/target markings)
|
||||
input_terminal : Sort;
|
||||
output_terminal : Sort;
|
||||
input_terminal/of : input_terminal -> N/P;
|
||||
output_terminal/of : output_terminal -> N/P;
|
||||
input_terminal/tgt : input_terminal -> [firing : F, arc : N/in];
|
||||
output_terminal/src : output_terminal -> [firing : F, arc : N/out];
|
||||
}
|
||||
|
||||
// Example Petri net: A <--ab/ba--> B, (A,B) --abc--> C
|
||||
instance ExampleNet : PetriNet = {
|
||||
A : P;
|
||||
B : P;
|
||||
C : P;
|
||||
ab : T;
|
||||
ba : T;
|
||||
abc : T;
|
||||
ab_in : in;
|
||||
ab_in in/src = A;
|
||||
ab_in in/tgt = ab;
|
||||
ab_out : out;
|
||||
ab_out out/src = ab;
|
||||
ab_out out/tgt = B;
|
||||
ba_in : in;
|
||||
ba_in in/src = B;
|
||||
ba_in in/tgt = ba;
|
||||
ba_out : out;
|
||||
ba_out out/src = ba;
|
||||
ba_out out/tgt = A;
|
||||
abc_in1 : in;
|
||||
abc_in1 in/src = A;
|
||||
abc_in1 in/tgt = abc;
|
||||
abc_in2 : in;
|
||||
abc_in2 in/src = B;
|
||||
abc_in2 in/tgt = abc;
|
||||
abc_out : out;
|
||||
abc_out out/src = abc;
|
||||
abc_out out/tgt = C;
|
||||
}
|
||||
|
||||
// Reachability problem: Can we reach B from A?
|
||||
instance problem0 : ExampleNet ReachabilityProblem = {
|
||||
initial_marking = {
|
||||
t : token;
|
||||
t token/of = ExampleNet/A;
|
||||
};
|
||||
target_marking = {
|
||||
t : token;
|
||||
t token/of = ExampleNet/B;
|
||||
};
|
||||
}
|
||||
66
examples/geolog/petri_trace_axioms.geolog
Normal file
66
examples/geolog/petri_trace_axioms.geolog
Normal file
@ -0,0 +1,66 @@
|
||||
// Test Trace theory with axioms using product codomains
|
||||
|
||||
theory PetriNet {
|
||||
P : Sort;
|
||||
T : Sort;
|
||||
in : Sort;
|
||||
out : Sort;
|
||||
in/src : in -> P;
|
||||
in/tgt : in -> T;
|
||||
out/src : out -> T;
|
||||
out/tgt : out -> P;
|
||||
}
|
||||
|
||||
// Trace theory with axioms
|
||||
theory (N : PetriNet instance) Trace {
|
||||
F : Sort;
|
||||
F/of : F -> N/T;
|
||||
|
||||
W : Sort;
|
||||
W/src : W -> [firing : F, arc : N/out];
|
||||
W/tgt : W -> [firing : F, arc : N/in];
|
||||
|
||||
input_terminal : Sort;
|
||||
output_terminal : Sort;
|
||||
input_terminal/of : input_terminal -> N/P;
|
||||
output_terminal/of : output_terminal -> N/P;
|
||||
input_terminal/tgt : input_terminal -> [firing : F, arc : N/in];
|
||||
output_terminal/src : output_terminal -> [firing : F, arc : N/out];
|
||||
|
||||
// Axiom: wires are injective on source
|
||||
// forall w1, w2 : W. w1 W/src = w2 W/src |- w1 = w2;
|
||||
// (Commented out - requires product codomain equality in premises)
|
||||
|
||||
// Axiom: every arc endpoint must be wired or terminated
|
||||
// forall f : F, arc : N/out. arc N/out/src = f F/of |-
|
||||
// (exists w : W. w W/src = [firing: f, arc: arc]) \/
|
||||
// (exists o : output_terminal. o output_terminal/src = [firing: f, arc: arc]);
|
||||
// (Commented out - requires product codomain values in conclusions)
|
||||
}
|
||||
|
||||
// Simple net for testing
|
||||
instance SimpleNet : PetriNet = {
|
||||
A : P;
|
||||
B : P;
|
||||
t : T;
|
||||
arc_in : in;
|
||||
arc_in in/src = A;
|
||||
arc_in in/tgt = t;
|
||||
arc_out : out;
|
||||
arc_out out/src = t;
|
||||
arc_out out/tgt = B;
|
||||
}
|
||||
|
||||
// Test that the basic theory without axioms still works
|
||||
instance SimpleTrace : SimpleNet Trace = {
|
||||
f1 : F;
|
||||
f1 F/of = SimpleNet/t;
|
||||
|
||||
it : input_terminal;
|
||||
it input_terminal/of = SimpleNet/A;
|
||||
it input_terminal/tgt = [firing: f1, arc: SimpleNet/arc_in];
|
||||
|
||||
ot : output_terminal;
|
||||
ot output_terminal/of = SimpleNet/B;
|
||||
ot output_terminal/src = [firing: f1, arc: SimpleNet/arc_out];
|
||||
}
|
||||
36
examples/geolog/petri_trace_coverage_test.geolog
Normal file
36
examples/geolog/petri_trace_coverage_test.geolog
Normal file
@ -0,0 +1,36 @@
|
||||
// Test: Trace coverage axiom (simplified)
|
||||
|
||||
theory PetriNet {
|
||||
P : Sort;
|
||||
T : Sort;
|
||||
out : Sort;
|
||||
out/src : out -> T;
|
||||
out/tgt : out -> P;
|
||||
}
|
||||
|
||||
theory (N : PetriNet instance) Trace {
|
||||
F : Sort;
|
||||
F/of : F -> N/T;
|
||||
|
||||
output_terminal : Sort;
|
||||
output_terminal/src : output_terminal -> [firing : F, arc : N/out];
|
||||
|
||||
// Simplified ax5: for every arc and firing, if the arc's source is the firing's transition,
|
||||
// create an output terminal
|
||||
ax5 : forall f : F, arc : N/out. |- exists o : output_terminal. o output_terminal/src = [firing: f, arc: arc];
|
||||
}
|
||||
|
||||
instance SimpleNet : PetriNet = {
|
||||
A : P;
|
||||
B : P;
|
||||
t : T;
|
||||
arc_out : out;
|
||||
arc_out out/src = t;
|
||||
arc_out out/tgt = B;
|
||||
}
|
||||
|
||||
// Trace with just a firing - chase should create a terminal
|
||||
instance TestTrace : SimpleNet Trace = chase {
|
||||
f1 : F;
|
||||
f1 F/of = SimpleNet/t;
|
||||
}
|
||||
57
examples/geolog/petri_trace_full_vision.geolog
Normal file
57
examples/geolog/petri_trace_full_vision.geolog
Normal file
@ -0,0 +1,57 @@
|
||||
// Trace theory with wires and disjunctions
|
||||
// Testing the full vision from 2025-12-12
|
||||
|
||||
theory PetriNet {
|
||||
P : Sort;
|
||||
T : Sort;
|
||||
in : Sort;
|
||||
out : Sort;
|
||||
in/src : in -> P;
|
||||
in/tgt : in -> T;
|
||||
out/src : out -> T;
|
||||
out/tgt : out -> P;
|
||||
}
|
||||
|
||||
theory (N : PetriNet instance) Trace {
|
||||
F : Sort;
|
||||
F/of : F -> N/T;
|
||||
|
||||
W : Sort;
|
||||
W/src : W -> [firing : F, arc : N/out];
|
||||
W/tgt : W -> [firing : F, arc : N/in];
|
||||
|
||||
input_terminal : Sort;
|
||||
output_terminal : Sort;
|
||||
input_terminal/of : input_terminal -> N/P;
|
||||
output_terminal/of : output_terminal -> N/P;
|
||||
input_terminal/tgt : input_terminal -> [firing : F, arc : N/in];
|
||||
output_terminal/src : output_terminal -> [firing : F, arc : N/out];
|
||||
|
||||
// Every out arc of every firing: either wired or terminal
|
||||
ax5 : forall f : F, arc : N/out. |-
|
||||
(exists w : W. w W/src = [firing: f, arc: arc]) \/
|
||||
(exists o : output_terminal. o output_terminal/src = [firing: f, arc: arc]);
|
||||
|
||||
// Every in arc of every firing: either wired or terminal
|
||||
ax6 : forall f : F, arc : N/in. |-
|
||||
(exists w : W. w W/tgt = [firing: f, arc: arc]) \/
|
||||
(exists i : input_terminal. i input_terminal/tgt = [firing: f, arc: arc]);
|
||||
}
|
||||
|
||||
instance SimpleNet : PetriNet = {
|
||||
A : P;
|
||||
B : P;
|
||||
t : T;
|
||||
arc_in : in;
|
||||
arc_in in/src = A;
|
||||
arc_in in/tgt = t;
|
||||
arc_out : out;
|
||||
arc_out out/src = t;
|
||||
arc_out out/tgt = B;
|
||||
}
|
||||
|
||||
// Chase should create both wires AND terminals (naive chase adds all disjuncts)
|
||||
instance trace_test : SimpleNet Trace = chase {
|
||||
f1 : F;
|
||||
f1 F/of = SimpleNet/t;
|
||||
}
|
||||
58
examples/geolog/petri_trace_test.geolog
Normal file
58
examples/geolog/petri_trace_test.geolog
Normal file
@ -0,0 +1,58 @@
|
||||
// Test that Trace theory with product codomains works
|
||||
|
||||
theory PetriNet {
|
||||
P : Sort;
|
||||
T : Sort;
|
||||
in : Sort;
|
||||
out : Sort;
|
||||
in/src : in -> P;
|
||||
in/tgt : in -> T;
|
||||
out/src : out -> T;
|
||||
out/tgt : out -> P;
|
||||
}
|
||||
|
||||
// Simple Petri net: A --t--> B
|
||||
instance SimpleNet : PetriNet = {
|
||||
A : P;
|
||||
B : P;
|
||||
t : T;
|
||||
arc_in : in;
|
||||
arc_in in/src = A;
|
||||
arc_in in/tgt = t;
|
||||
arc_out : out;
|
||||
arc_out out/src = t;
|
||||
arc_out out/tgt = B;
|
||||
}
|
||||
|
||||
// Trace theory with product codomains for wire endpoints
|
||||
theory (N : PetriNet instance) Trace {
|
||||
F : Sort;
|
||||
F/of : F -> N/T;
|
||||
|
||||
W : Sort;
|
||||
W/src : W -> [firing : F, arc : N/out];
|
||||
W/tgt : W -> [firing : F, arc : N/in];
|
||||
|
||||
input_terminal : Sort;
|
||||
output_terminal : Sort;
|
||||
input_terminal/of : input_terminal -> N/P;
|
||||
output_terminal/of : output_terminal -> N/P;
|
||||
input_terminal/tgt : input_terminal -> [firing : F, arc : N/in];
|
||||
output_terminal/src : output_terminal -> [firing : F, arc : N/out];
|
||||
}
|
||||
|
||||
// A simple trace: one firing of t, with input/output terminals
|
||||
instance SimpleTrace : SimpleNet Trace = {
|
||||
f1 : F;
|
||||
f1 F/of = SimpleNet/t;
|
||||
|
||||
// Input terminal (token comes from external marking)
|
||||
it : input_terminal;
|
||||
it input_terminal/of = SimpleNet/A;
|
||||
it input_terminal/tgt = [firing: f1, arc: SimpleNet/arc_in];
|
||||
|
||||
// Output terminal (token goes to external marking)
|
||||
ot : output_terminal;
|
||||
ot output_terminal/of = SimpleNet/B;
|
||||
ot output_terminal/src = [firing: f1, arc: SimpleNet/arc_out];
|
||||
}
|
||||
42
examples/geolog/preorder.geolog
Normal file
42
examples/geolog/preorder.geolog
Normal file
@ -0,0 +1,42 @@
|
||||
// Preorder: a set with a reflexive, transitive relation
|
||||
//
|
||||
// This demonstrates RELATIONS (predicates) as opposed to functions.
|
||||
// A relation R : A -> Prop is a predicate on A.
|
||||
// For binary relations, we use a product domain: R : [x: A, y: A] -> Prop
|
||||
|
||||
theory Preorder {
|
||||
X : Sort;
|
||||
|
||||
// The ordering relation: x ≤ y
|
||||
leq : [x: X, y: X] -> Prop;
|
||||
|
||||
// Reflexivity: x ≤ x
|
||||
ax/refl : forall x : X.
|
||||
|- [x: x, y: x] leq;
|
||||
|
||||
// Transitivity: x ≤ y ∧ y ≤ z → x ≤ z
|
||||
ax/trans : forall x : X, y : X, z : X.
|
||||
[x: x, y: y] leq, [x: y, y: z] leq |- [x: x, y: z] leq;
|
||||
}
|
||||
|
||||
// The discrete preorder: only reflexive pairs
|
||||
// (no elements are comparable except to themselves)
|
||||
// Uses `chase` to automatically derive reflexive pairs from axiom ax/refl.
|
||||
instance Discrete3 : Preorder = chase {
|
||||
a : X;
|
||||
b : X;
|
||||
c : X;
|
||||
}
|
||||
|
||||
// A total order on 3 elements: a ≤ b ≤ c
|
||||
// Uses `chase` to derive reflexive and transitive closure.
|
||||
instance Chain3 : Preorder = chase {
|
||||
bot : X;
|
||||
mid : X;
|
||||
top : X;
|
||||
|
||||
// Assert the basic ordering; chase will add reflexive pairs
|
||||
// and transitive closure (bot ≤ top)
|
||||
[x: bot, y: mid] leq;
|
||||
[x: mid, y: top] leq;
|
||||
}
|
||||
23
examples/geolog/product_codomain_equality_test.geolog
Normal file
23
examples/geolog/product_codomain_equality_test.geolog
Normal file
@ -0,0 +1,23 @@
|
||||
// Test: Product codomain equality in premise (ax3 pattern)
|
||||
|
||||
theory ProductCodomainEqTest {
|
||||
A : Sort;
|
||||
B : Sort;
|
||||
|
||||
W : Sort;
|
||||
W/src : W -> [x: A, y: B];
|
||||
|
||||
// ax3 pattern: forall w1, w2 : W. w1 W/src = w2 W/src |- w1 = w2
|
||||
// This should make W injective on src
|
||||
ax_inj : forall w1 : W, w2 : W. w1 W/src = w2 W/src |- w1 = w2;
|
||||
}
|
||||
|
||||
// Instance with two wires that have the same src - should be identified by chase
|
||||
instance Test : ProductCodomainEqTest = {
|
||||
a1 : A;
|
||||
b1 : B;
|
||||
w1 : W;
|
||||
w1 W/src = [x: a1, y: b1];
|
||||
w2 : W;
|
||||
w2 W/src = [x: a1, y: b1];
|
||||
}
|
||||
51
examples/geolog/product_codomain_test.geolog
Normal file
51
examples/geolog/product_codomain_test.geolog
Normal file
@ -0,0 +1,51 @@
|
||||
// Test: Product Codomain Support
|
||||
//
|
||||
// This tests the new feature where functions can have product codomains,
|
||||
// allowing record literal assignments like:
|
||||
// elem func = [field1: v1, field2: v2];
|
||||
|
||||
theory ProductCodomainTest {
|
||||
A : Sort;
|
||||
B : Sort;
|
||||
C : Sort;
|
||||
|
||||
// Function with product codomain: maps A elements to (B, C) pairs
|
||||
pair_of : A -> [left: B, right: C];
|
||||
}
|
||||
|
||||
instance TestInstance : ProductCodomainTest = {
|
||||
// Elements
|
||||
a1 : A;
|
||||
b1 : B;
|
||||
b2 : B;
|
||||
c1 : C;
|
||||
|
||||
// Assign product codomain value using record literal
|
||||
a1 pair_of = [left: b1, right: c1];
|
||||
}
|
||||
|
||||
// A more realistic example: Edges in a graph
|
||||
theory DirectedGraph {
|
||||
V : Sort;
|
||||
E : Sort;
|
||||
|
||||
// Edge endpoints as a product codomain
|
||||
endpoints : E -> [src: V, tgt: V];
|
||||
}
|
||||
|
||||
instance TriangleGraph : DirectedGraph = {
|
||||
// Vertices
|
||||
v0 : V;
|
||||
v1 : V;
|
||||
v2 : V;
|
||||
|
||||
// Edges
|
||||
e01 : E;
|
||||
e12 : E;
|
||||
e20 : E;
|
||||
|
||||
// Assign edge endpoints using record literals
|
||||
e01 endpoints = [src: v0, tgt: v1];
|
||||
e12 endpoints = [src: v1, tgt: v2];
|
||||
e20 endpoints = [src: v2, tgt: v0];
|
||||
}
|
||||
18
examples/geolog/record_existential_test.geolog
Normal file
18
examples/geolog/record_existential_test.geolog
Normal file
@ -0,0 +1,18 @@
|
||||
// Test: Record literals in existential conclusions
|
||||
|
||||
theory RecordExistentialTest {
|
||||
A : Sort;
|
||||
B : Sort;
|
||||
|
||||
R : Sort;
|
||||
R/data : R -> [x: A, y: B];
|
||||
|
||||
// Axiom: given any a:A and b:B, there exists an R with that data
|
||||
ax1 : forall a : A, b : B. |-
|
||||
exists r : R. r R/data = [x: a, y: b];
|
||||
}
|
||||
|
||||
instance Test : RecordExistentialTest = chase {
|
||||
a1 : A;
|
||||
b1 : B;
|
||||
}
|
||||
12
examples/geolog/record_in_axiom_test.geolog
Normal file
12
examples/geolog/record_in_axiom_test.geolog
Normal file
@ -0,0 +1,12 @@
|
||||
// Test: Record literals in axioms
|
||||
|
||||
theory RecordAxiomTest {
|
||||
A : Sort;
|
||||
B : Sort;
|
||||
|
||||
R : Sort;
|
||||
R/data : R -> [x: A, y: B];
|
||||
|
||||
// Test axiom with record literal RHS
|
||||
ax1 : forall r : R, a : A, b : B. r R/data = [x: a, y: b] |- true;
|
||||
}
|
||||
23
examples/geolog/record_premise_chase_test.geolog
Normal file
23
examples/geolog/record_premise_chase_test.geolog
Normal file
@ -0,0 +1,23 @@
|
||||
// Test: Chase with record literals in premises
|
||||
|
||||
theory RecordPremiseTest {
|
||||
A : Sort;
|
||||
B : Sort;
|
||||
|
||||
R : Sort;
|
||||
R/data : R -> [x: A, y: B];
|
||||
|
||||
// Derived sort for processed items
|
||||
Processed : Sort;
|
||||
Processed/r : Processed -> R;
|
||||
|
||||
// Axiom: given r with data [x: a, y: b], create a Processed for it
|
||||
ax1 : forall r : R, a : A, b : B. r R/data = [x: a, y: b] |- exists p : Processed. p Processed/r = r;
|
||||
}
|
||||
|
||||
instance Test : RecordPremiseTest = {
|
||||
a1 : A;
|
||||
b1 : B;
|
||||
r1 : R;
|
||||
r1 R/data = [x: a1, y: b1];
|
||||
}
|
||||
130
examples/geolog/relalg_simple.geolog
Normal file
130
examples/geolog/relalg_simple.geolog
Normal file
@ -0,0 +1,130 @@
|
||||
// Example: RelAlgIR query plan instances
|
||||
//
|
||||
// This demonstrates creating query plans as RelAlgIR instances.
|
||||
// These show the string diagram representation of relational algebra.
|
||||
//
|
||||
// First we need to load both GeologMeta (for Srt, Func, etc.) and RelAlgIR.
|
||||
// This file just defines instances; load theories first in the REPL:
|
||||
// :load theories/GeologMeta.geolog
|
||||
// :load theories/RelAlgIR.geolog
|
||||
// :load examples/geolog/relalg_simple.geolog
|
||||
//
|
||||
// Note: RelAlgIR extends GeologMeta, so a RelAlgIR instance contains
|
||||
// elements from both GeologMeta sorts (Srt, Func, Elem) and RelAlgIR
|
||||
// sorts (Wire, Schema, ScanOp, etc.)
|
||||
|
||||
// ============================================================
|
||||
// Example 1: Simple Scan
|
||||
// ============================================================
|
||||
// Query: "scan all elements of sort V"
|
||||
// Plan: () --[ScanOp]--> Wire
|
||||
|
||||
instance ScanV : RelAlgIR = chase {
|
||||
// -- Schema (target theory) --
|
||||
target_theory : GeologMeta/Theory;
|
||||
target_theory GeologMeta/Theory/parent = target_theory;
|
||||
|
||||
v_srt : GeologMeta/Srt;
|
||||
v_srt GeologMeta/Srt/theory = target_theory;
|
||||
|
||||
// -- Query Plan --
|
||||
v_base_schema : BaseSchema;
|
||||
v_base_schema BaseSchema/srt = v_srt;
|
||||
|
||||
v_schema : Schema;
|
||||
v_base_schema BaseSchema/schema = v_schema;
|
||||
|
||||
scan_out : Wire;
|
||||
scan_out Wire/schema = v_schema;
|
||||
|
||||
scan : ScanOp;
|
||||
scan ScanOp/srt = v_srt;
|
||||
scan ScanOp/out = scan_out;
|
||||
|
||||
scan_op : Op;
|
||||
scan ScanOp/op = scan_op;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Example 2: Filter(Scan)
|
||||
// ============================================================
|
||||
// Query: "scan E, filter where src(e) = some vertex"
|
||||
// Plan: () --[Scan]--> w1 --[Filter]--> w2
|
||||
//
|
||||
// This demonstrates composition via wire sharing.
|
||||
|
||||
// Uses chase to derive relations.
|
||||
instance FilterScan : RelAlgIR = chase {
|
||||
// -- Schema (representing Graph theory) --
|
||||
target_theory : GeologMeta/Theory;
|
||||
target_theory GeologMeta/Theory/parent = target_theory;
|
||||
|
||||
// Sorts: V (vertices), E (edges)
|
||||
v_srt : GeologMeta/Srt;
|
||||
v_srt GeologMeta/Srt/theory = target_theory;
|
||||
|
||||
e_srt : GeologMeta/Srt;
|
||||
e_srt GeologMeta/Srt/theory = target_theory;
|
||||
|
||||
// Functions: src : E -> V
|
||||
// First create the DSort wrappers
|
||||
v_base_ds : GeologMeta/BaseDS;
|
||||
v_base_ds GeologMeta/BaseDS/srt = v_srt;
|
||||
|
||||
e_base_ds : GeologMeta/BaseDS;
|
||||
e_base_ds GeologMeta/BaseDS/srt = e_srt;
|
||||
|
||||
v_dsort : GeologMeta/DSort;
|
||||
v_base_ds GeologMeta/BaseDS/dsort = v_dsort;
|
||||
|
||||
e_dsort : GeologMeta/DSort;
|
||||
e_base_ds GeologMeta/BaseDS/dsort = e_dsort;
|
||||
|
||||
src_func : GeologMeta/Func;
|
||||
src_func GeologMeta/Func/theory = target_theory;
|
||||
src_func GeologMeta/Func/dom = e_dsort;
|
||||
src_func GeologMeta/Func/cod = v_dsort;
|
||||
|
||||
// NOTE: For a complete example, we'd also need an Instance element
|
||||
// and Elem elements. For simplicity, we use a simpler predicate structure.
|
||||
|
||||
// Using TruePred for now (matches all, demonstrating structure)
|
||||
|
||||
// -- Query Plan --
|
||||
// Schema for E
|
||||
e_base_schema : BaseSchema;
|
||||
e_base_schema BaseSchema/srt = e_srt;
|
||||
|
||||
e_schema : Schema;
|
||||
e_base_schema BaseSchema/schema = e_schema;
|
||||
|
||||
// Wire 1: output of Scan (E elements)
|
||||
w1 : Wire;
|
||||
w1 Wire/schema = e_schema;
|
||||
|
||||
// Wire 2: output of Filter (filtered E elements)
|
||||
w2 : Wire;
|
||||
w2 Wire/schema = e_schema;
|
||||
|
||||
// Scan operation
|
||||
scan : ScanOp;
|
||||
scan ScanOp/srt = e_srt;
|
||||
scan ScanOp/out = w1;
|
||||
|
||||
scan_op : Op;
|
||||
scan ScanOp/op = scan_op;
|
||||
|
||||
// Predicate: TruePred (matches all - demonstrates filter structure)
|
||||
true_pred : TruePred;
|
||||
pred_elem : Pred;
|
||||
true_pred TruePred/pred = pred_elem;
|
||||
|
||||
// Filter operation: w1 --[Filter(pred)]--> w2
|
||||
filter : FilterOp;
|
||||
filter FilterOp/in = w1;
|
||||
filter FilterOp/out = w2;
|
||||
filter FilterOp/pred = pred_elem;
|
||||
|
||||
filter_op : Op;
|
||||
filter FilterOp/op = filter_op;
|
||||
}
|
||||
132
examples/geolog/solver_demo.geolog
Normal file
132
examples/geolog/solver_demo.geolog
Normal file
@ -0,0 +1,132 @@
|
||||
// Solver Demo: Theories demonstrating the geometric logic solver
|
||||
//
|
||||
// Use the :solve command to find instances of these theories:
|
||||
// :source examples/geolog/solver_demo.geolog
|
||||
// :solve EmptyModel
|
||||
// :solve Inhabited
|
||||
// :solve Inconsistent
|
||||
//
|
||||
// The solver uses forward chaining to automatically:
|
||||
// - Add witness elements for existentials
|
||||
// - Assert relation tuples
|
||||
// - Detect unsatisfiability (derivation of False)
|
||||
|
||||
// ============================================================================
|
||||
// Theory 1: EmptyModel - Trivially satisfiable with empty carrier
|
||||
// ============================================================================
|
||||
//
|
||||
// A theory with no axioms is satisfied by the empty structure.
|
||||
// The solver should report SOLVED immediately with 0 elements.
|
||||
|
||||
theory EmptyModel {
|
||||
A : Sort;
|
||||
B : Sort;
|
||||
f : A -> B;
|
||||
R : A -> Prop;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Theory 2: UnconditionalExistential - Requires witness creation
|
||||
// ============================================================================
|
||||
//
|
||||
// Axiom: forall x : P. |- exists y : P. y R
|
||||
//
|
||||
// SUBTLE: This axiom's premise (True) and conclusion (∃y.R(y)) don't mention x!
|
||||
// So even though there's a "forall x : P", the check happens once for an empty
|
||||
// assignment. The premise True holds, but ∃y.R(y) doesn't hold for empty P
|
||||
// (no witnesses). The solver correctly detects this and adds a witness.
|
||||
//
|
||||
// This is correct geometric logic semantics! The universal over x doesn't
|
||||
// protect against empty P because x isn't used in the formulas.
|
||||
|
||||
theory UnconditionalExistential {
|
||||
P : Sort;
|
||||
R : P -> Prop;
|
||||
|
||||
// This effectively says "there must exist some y with R(y)"
|
||||
// because x is unused - the check happens once regardless of |P|
|
||||
ax : forall x : P. |- exists y : P. y R;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Theory 3: VacuouslyTrue - Axiom that IS vacuously true for empty carriers
|
||||
// ============================================================================
|
||||
//
|
||||
// Axiom: forall x : P. |- x R
|
||||
//
|
||||
// For every x, assert R(x). When P is empty, there are no x values to check,
|
||||
// so the axiom is vacuously satisfied. Compare with UnconditionalExistential!
|
||||
|
||||
theory VacuouslyTrue {
|
||||
P : Sort;
|
||||
R : P -> Prop;
|
||||
|
||||
// This truly IS vacuously true for empty P because x IS used in the conclusion
|
||||
ax : forall x : P. |- x R;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Theory 4: Inconsistent - UNSAT via derivation of False
|
||||
// ============================================================================
|
||||
//
|
||||
// Axiom: forall x. |- false
|
||||
//
|
||||
// For any element x, we derive False. This is immediately UNSAT.
|
||||
// The solver detects this and reports UNSAT.
|
||||
|
||||
theory Inconsistent {
|
||||
A : Sort;
|
||||
|
||||
// Contradiction: any element leads to False
|
||||
ax : forall a : A. |- false;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Theory 5: ReflexiveRelation - Forward chaining asserts reflexive tuples
|
||||
// ============================================================================
|
||||
//
|
||||
// Axiom: forall x. |- R(x, x)
|
||||
//
|
||||
// For every element x, the pair (x, x) is in relation R.
|
||||
// The solver will assert R(x, x) for each element added.
|
||||
|
||||
theory ReflexiveRelation {
|
||||
X : Sort;
|
||||
R : [a: X, b: X] -> Prop;
|
||||
|
||||
// Reflexivity: every element is related to itself
|
||||
ax/refl : forall x : X. |- [a: x, b: x] R;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Theory 6: ChainedWitness - Nested existential body processing
|
||||
// ============================================================================
|
||||
//
|
||||
// Axiom: forall x. |- exists y. exists z. E(x, y), E(y, z)
|
||||
//
|
||||
// For every x, there exist y and z such that E(x,y) and E(y,z).
|
||||
// Forward chaining creates witnesses and asserts the relations.
|
||||
|
||||
theory ChainedWitness {
|
||||
N : Sort;
|
||||
E : [src: N, tgt: N] -> Prop;
|
||||
|
||||
// Chain: every node has a two-step path out
|
||||
ax/chain : forall x : N. |- exists y : N. exists z : N. [src: x, tgt: y] E, [src: y, tgt: z] E;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Theory 7: EqualityCollapse - Equation handling via congruence closure
|
||||
// ============================================================================
|
||||
//
|
||||
// Axiom: forall x, y. |- x = y
|
||||
//
|
||||
// All elements of sort X are equal. The solver adds equations to the
|
||||
// congruence closure and merges equivalence classes.
|
||||
|
||||
theory EqualityCollapse {
|
||||
X : Sort;
|
||||
|
||||
// All elements are equal
|
||||
ax/all_equal : forall x : X, y : X. |- x = y;
|
||||
}
|
||||
31
examples/geolog/sort_param_simple.geolog
Normal file
31
examples/geolog/sort_param_simple.geolog
Normal file
@ -0,0 +1,31 @@
|
||||
// Simpler sort parameter test
|
||||
|
||||
theory (X : Sort) Container {
|
||||
elem : X; // not a Sort, but an element of X
|
||||
}
|
||||
|
||||
// Hmm, this doesn't quite work...
|
||||
// Let me try the actual vision pattern
|
||||
|
||||
theory Base {
|
||||
A : Sort;
|
||||
B : Sort;
|
||||
}
|
||||
|
||||
instance MyBase : Base = {
|
||||
a1 : A;
|
||||
a2 : A;
|
||||
b1 : B;
|
||||
b2 : B;
|
||||
}
|
||||
|
||||
// Now try a theory parameterized by an instance
|
||||
theory (Inst : Base instance) Map {
|
||||
map : Inst/A -> Inst/B;
|
||||
}
|
||||
|
||||
// Instance of Map parameterized by MyBase
|
||||
instance MyMap : MyBase Map = {
|
||||
a1 map = MyBase/b1;
|
||||
a2 map = MyBase/b2;
|
||||
}
|
||||
44
examples/geolog/todo_list.geolog
Normal file
44
examples/geolog/todo_list.geolog
Normal file
@ -0,0 +1,44 @@
|
||||
// TodoList: A simple relational model for tracking tasks
|
||||
//
|
||||
// This demonstrates geolog as a persistent relational database.
|
||||
// Elements represent tasks, and relations track their status.
|
||||
|
||||
theory TodoList {
|
||||
// The sort of todo items
|
||||
Item : Sort;
|
||||
|
||||
// Unary relations for item status (simple arrow syntax)
|
||||
completed : Item -> Prop;
|
||||
high_priority : Item -> Prop;
|
||||
blocked : Item -> Prop;
|
||||
|
||||
// Binary relation for dependencies
|
||||
depends : [item: Item, on: Item] -> Prop;
|
||||
|
||||
// Axiom: if an item depends on another, either it is blocked
|
||||
// or the dependency is completed
|
||||
ax/dep_blocked : forall x : Item, y : Item.
|
||||
[item: x, on: y] depends |- x blocked \/ y completed;
|
||||
}
|
||||
|
||||
// Example: An empty todo list ready for interactive use
|
||||
instance MyTodos : TodoList = {
|
||||
// Start empty - add items interactively with :add
|
||||
}
|
||||
|
||||
// Example: A pre-populated todo list
|
||||
instance SampleTodos : TodoList = {
|
||||
// Items
|
||||
buy_groceries : Item;
|
||||
cook_dinner : Item;
|
||||
do_laundry : Item;
|
||||
clean_house : Item;
|
||||
|
||||
// Status: unary relations use simple syntax
|
||||
buy_groceries completed;
|
||||
cook_dinner high_priority;
|
||||
|
||||
// Dependencies: cook_dinner depends on buy_groceries
|
||||
// Mixed syntax: first positional arg maps to 'item' field
|
||||
[cook_dinner, on: buy_groceries] depends;
|
||||
}
|
||||
77
examples/geolog/transitive_closure.geolog
Normal file
77
examples/geolog/transitive_closure.geolog
Normal file
@ -0,0 +1,77 @@
|
||||
// Transitive Closure Example
|
||||
//
|
||||
// This example demonstrates the chase algorithm computing transitive
|
||||
// closure of a relation. We define a Graph theory with Edge and Path
|
||||
// relations, where Path is the transitive closure of Edge.
|
||||
//
|
||||
// Run with:
|
||||
// cargo run -- examples/geolog/transitive_closure.geolog
|
||||
// Then:
|
||||
// :source examples/geolog/transitive_closure.geolog
|
||||
// :inspect Chain
|
||||
// :chase Chain
|
||||
//
|
||||
// The chase will derive Path tuples for all reachable pairs:
|
||||
// - Edge(a,b), Edge(b,c), Edge(c,d) as base facts
|
||||
// - Path(a,b), Path(b,c), Path(c,d) from base axiom
|
||||
// - Path(a,c), Path(b,d) from one step of transitivity
|
||||
// - Path(a,d) from two steps of transitivity
|
||||
|
||||
theory Graph {
|
||||
V : Sort;
|
||||
|
||||
// Direct edges in the graph
|
||||
Edge : [src: V, tgt: V] -> Prop;
|
||||
|
||||
// Reachability (transitive closure of Edge)
|
||||
Path : [src: V, tgt: V] -> Prop;
|
||||
|
||||
// Base case: every edge is a path
|
||||
ax/base : forall x, y : V.
|
||||
[src: x, tgt: y] Edge |- [src: x, tgt: y] Path;
|
||||
|
||||
// Inductive case: paths compose
|
||||
ax/trans : forall x, y, z : V.
|
||||
[src: x, tgt: y] Path, [src: y, tgt: z] Path |- [src: x, tgt: z] Path;
|
||||
}
|
||||
|
||||
// A linear chain: a -> b -> c -> d
|
||||
// Chase derives Path tuples from Edge via ax/base and ax/trans.
|
||||
instance Chain : Graph = chase {
|
||||
a : V;
|
||||
b : V;
|
||||
c : V;
|
||||
d : V;
|
||||
|
||||
// Edges form a chain
|
||||
[src: a, tgt: b] Edge;
|
||||
[src: b, tgt: c] Edge;
|
||||
[src: c, tgt: d] Edge;
|
||||
}
|
||||
|
||||
// A diamond: a -> b, a -> c, b -> d, c -> d
|
||||
// Chase derives all reachable paths.
|
||||
instance Diamond : Graph = chase {
|
||||
top : V;
|
||||
left : V;
|
||||
right : V;
|
||||
bottom : V;
|
||||
|
||||
// Two paths from top to bottom
|
||||
[src: top, tgt: left] Edge;
|
||||
[src: top, tgt: right] Edge;
|
||||
[src: left, tgt: bottom] Edge;
|
||||
[src: right, tgt: bottom] Edge;
|
||||
}
|
||||
|
||||
// A cycle: a -> b -> c -> a
|
||||
// Chase derives all reachable paths (full connectivity).
|
||||
instance Cycle : Graph = chase {
|
||||
x : V;
|
||||
y : V;
|
||||
z : V;
|
||||
|
||||
[src: x, tgt: y] Edge;
|
||||
[src: y, tgt: z] Edge;
|
||||
[src: z, tgt: x] Edge;
|
||||
}
|
||||
3
examples/main.rs
Normal file
3
examples/main.rs
Normal file
@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
println!("Hello world!")
|
||||
}
|
||||
216
examples/roundtrip.rs
Normal file
216
examples/roundtrip.rs
Normal file
@ -0,0 +1,216 @@
|
||||
use geolog::{parse, pretty_print};
|
||||
|
||||
fn main() {
|
||||
let input = r#"
|
||||
namespace VanillaPetriNets;
|
||||
|
||||
theory PetriNet {
|
||||
P : Sort;
|
||||
T : Sort;
|
||||
in : Sort;
|
||||
out : Sort;
|
||||
in/src : in -> P;
|
||||
in/tgt : in -> T;
|
||||
out/src : out -> T;
|
||||
out/tgt : out -> P;
|
||||
}
|
||||
|
||||
theory (N : PetriNet instance) Marking {
|
||||
token : Sort;
|
||||
token/of : token -> N/P;
|
||||
}
|
||||
|
||||
theory (N : PetriNet instance) ReachabilityProblem {
|
||||
initial_marking : N Marking instance;
|
||||
target_marking : N Marking instance;
|
||||
}
|
||||
|
||||
theory (N : PetriNet instance) Trace {
|
||||
F : Sort;
|
||||
F/of : F -> N/T;
|
||||
|
||||
W : Sort;
|
||||
W/src : W -> [firing : F, arc : N/out];
|
||||
W/tgt : W -> [firing : F, arc : N/in];
|
||||
|
||||
ax1 : forall w : W. |- w W/src .arc N/out/src = w W/src .firing F/of;
|
||||
ax2 : forall w : W. |- w W/tgt .arc N/in/tgt = w W/tgt .firing F/of;
|
||||
ax3 : forall w1, w2 : W. w1 W/src = w2 W/src |- w1 = w2;
|
||||
ax4 : forall w1, w2 : W. w1 W/tgt = w2 W/tgt |- w1 = w2;
|
||||
|
||||
input_terminal : Sort;
|
||||
output_terminal : Sort;
|
||||
input_terminal/of : input_terminal -> N/P;
|
||||
output_terminal/of : output_terminal -> N/P;
|
||||
input_terminal/tgt : input_terminal -> [firing : F, arc : N/in];
|
||||
output_terminal/src : output_terminal -> [firing : F, arc : N/out];
|
||||
|
||||
ax5 : forall f : F, arc : N/out. arc N/out/src = f F/of |-
|
||||
(exists w : W. w W/src = [firing: f, arc: arc]) \/
|
||||
(exists o : output_terminal. o output_terminal/src = [firing: f, arc: arc]);
|
||||
ax6 : forall f : F, arc : N/in. arc N/in/tgt = f F/of |-
|
||||
(exists w : W. w W/tgt = [firing: f, arc: arc]) \/
|
||||
(exists i : input_terminal. i input_terminal/tgt = [firing: f, arc: arc]);
|
||||
}
|
||||
|
||||
theory (X : Sort) (Y : Sort) Iso {
|
||||
fwd : X -> Y;
|
||||
bwd : Y -> X;
|
||||
fb : forall x : X. |- x fwd bwd = x;
|
||||
bf : forall y : Y. |- y bwd fwd = y;
|
||||
}
|
||||
|
||||
instance ExampleNet : PetriNet = {
|
||||
A : P;
|
||||
B : P;
|
||||
C : P;
|
||||
ab : T;
|
||||
ba : T;
|
||||
abc : T;
|
||||
ab_in : in;
|
||||
ab_in in/src = A;
|
||||
ab_in in/tgt = ab;
|
||||
ab_out : out;
|
||||
ab_out out/src = ab;
|
||||
ab_out out/tgt = B;
|
||||
ba_in : in;
|
||||
ba_in in/src = B;
|
||||
ba_in in/tgt = ba;
|
||||
ba_out : out;
|
||||
ba_out out/src = ba;
|
||||
ba_out out/tgt = A;
|
||||
abc_in1 : in;
|
||||
abc_in1 in/src = A;
|
||||
abc_in1 in/tgt = abc;
|
||||
abc_in2 : in;
|
||||
abc_in2 in/src = B;
|
||||
abc_in2 in/tgt = abc;
|
||||
abc_out : out;
|
||||
abc_out out/src = abc;
|
||||
abc_out out/tgt = C;
|
||||
}
|
||||
|
||||
instance problem0 : ExampleNet ReachabilityProblem = {
|
||||
initial_marking = {
|
||||
t : token;
|
||||
t token/of = ExampleNet/A;
|
||||
};
|
||||
target_marking = {
|
||||
t : token;
|
||||
t token/of = ExampleNet/B;
|
||||
};
|
||||
}
|
||||
|
||||
query findTrace {
|
||||
? : ExampleNet Trace instance;
|
||||
}
|
||||
"#;
|
||||
|
||||
println!("=== PARSING ORIGINAL ===");
|
||||
let ast1 = match parse(input) {
|
||||
Ok(f) => f,
|
||||
Err(e) => {
|
||||
eprintln!("Parse error: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
println!("Parsed {} declarations", ast1.declarations.len());
|
||||
|
||||
println!("\n=== PRETTY PRINTING ===");
|
||||
let printed = pretty_print(&ast1);
|
||||
println!("{}", printed);
|
||||
|
||||
println!("\n=== RE-PARSING ===");
|
||||
let ast2 = match parse(&printed) {
|
||||
Ok(f) => f,
|
||||
Err(e) => {
|
||||
eprintln!("Re-parse error: {}", e);
|
||||
eprintln!("\nPrinted output was:\n{}", printed);
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
println!("Re-parsed {} declarations", ast2.declarations.len());
|
||||
|
||||
println!("\n=== COMPARING ===");
|
||||
if ast1.declarations.len() != ast2.declarations.len() {
|
||||
eprintln!("Declaration count mismatch!");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
// Compare declaration types
|
||||
for (i, (d1, d2)) in ast1
|
||||
.declarations
|
||||
.iter()
|
||||
.zip(ast2.declarations.iter())
|
||||
.enumerate()
|
||||
{
|
||||
let type1 = match &d1.node {
|
||||
geolog::Declaration::Namespace(_) => "namespace",
|
||||
geolog::Declaration::Theory(_) => "theory",
|
||||
geolog::Declaration::Instance(_) => "instance",
|
||||
geolog::Declaration::Query(_) => "query",
|
||||
};
|
||||
let type2 = match &d2.node {
|
||||
geolog::Declaration::Namespace(_) => "namespace",
|
||||
geolog::Declaration::Theory(_) => "theory",
|
||||
geolog::Declaration::Instance(_) => "instance",
|
||||
geolog::Declaration::Query(_) => "query",
|
||||
};
|
||||
if type1 != type2 {
|
||||
eprintln!("Declaration {} type mismatch: {} vs {}", i, type1, type2);
|
||||
std::process::exit(1);
|
||||
}
|
||||
print!(" [{}] {} ", i, type1);
|
||||
|
||||
// Check names/details
|
||||
match (&d1.node, &d2.node) {
|
||||
(geolog::Declaration::Namespace(n1), geolog::Declaration::Namespace(n2)) => {
|
||||
if n1 != n2 {
|
||||
eprintln!("name mismatch: {} vs {}", n1, n2);
|
||||
std::process::exit(1);
|
||||
}
|
||||
println!("{} ✓", n1);
|
||||
}
|
||||
(geolog::Declaration::Theory(t1), geolog::Declaration::Theory(t2)) => {
|
||||
if t1.name != t2.name {
|
||||
eprintln!("name mismatch: {} vs {}", t1.name, t2.name);
|
||||
std::process::exit(1);
|
||||
}
|
||||
if t1.body.len() != t2.body.len() {
|
||||
eprintln!(
|
||||
"body length mismatch: {} vs {}",
|
||||
t1.body.len(),
|
||||
t2.body.len()
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
println!("{} ({} items) ✓", t1.name, t1.body.len());
|
||||
}
|
||||
(geolog::Declaration::Instance(i1), geolog::Declaration::Instance(i2)) => {
|
||||
if i1.name != i2.name {
|
||||
eprintln!("name mismatch: {} vs {}", i1.name, i2.name);
|
||||
std::process::exit(1);
|
||||
}
|
||||
if i1.body.len() != i2.body.len() {
|
||||
eprintln!(
|
||||
"body length mismatch: {} vs {}",
|
||||
i1.body.len(),
|
||||
i2.body.len()
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
println!("{} ({} items) ✓", i1.name, i1.body.len());
|
||||
}
|
||||
(geolog::Declaration::Query(q1), geolog::Declaration::Query(q2)) => {
|
||||
if q1.name != q2.name {
|
||||
eprintln!("name mismatch: {} vs {}", q1.name, q2.name);
|
||||
std::process::exit(1);
|
||||
}
|
||||
println!("{} ✓", q1.name);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
println!("\n=== ROUNDTRIP SUCCESS ===");
|
||||
}
|
||||
4
fuzz/.gitignore
vendored
Normal file
4
fuzz/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
target
|
||||
corpus
|
||||
artifacts
|
||||
coverage
|
||||
30
fuzz/Cargo.toml
Normal file
30
fuzz/Cargo.toml
Normal file
@ -0,0 +1,30 @@
|
||||
[package]
|
||||
name = "geolog-fuzz"
|
||||
version = "0.0.0"
|
||||
publish = false
|
||||
edition = "2024"
|
||||
|
||||
[package.metadata]
|
||||
cargo-fuzz = true
|
||||
|
||||
[dependencies]
|
||||
libfuzzer_sys = "0.4"
|
||||
|
||||
[dependencies.geolog]
|
||||
path = ".."
|
||||
|
||||
# Parser fuzzer - tests lexer/parser robustness
|
||||
[[bin]]
|
||||
name = "fuzz_parser"
|
||||
path = "fuzz_targets/fuzz_parser.rs"
|
||||
test = false
|
||||
doc = false
|
||||
bench = false
|
||||
|
||||
# REPL fuzzer - tests full execution pipeline
|
||||
[[bin]]
|
||||
name = "fuzz_repl"
|
||||
path = "fuzz_targets/fuzz_repl.rs"
|
||||
test = false
|
||||
doc = false
|
||||
bench = false
|
||||
60
fuzz/README.md
Normal file
60
fuzz/README.md
Normal file
@ -0,0 +1,60 @@
|
||||
# Fuzzing geolog
|
||||
|
||||
This directory contains fuzz targets for finding bugs and edge cases in geolog.
|
||||
|
||||
## Requirements
|
||||
|
||||
Fuzzing requires the nightly Rust compiler due to sanitizer support:
|
||||
|
||||
```bash
|
||||
rustup install nightly
|
||||
rustup default nightly # or use +nightly flag
|
||||
```
|
||||
|
||||
## Available Targets
|
||||
|
||||
- **fuzz_parser**: Exercises the lexer and parser with arbitrary UTF-8 input
|
||||
- **fuzz_repl**: Exercises the full REPL execution pipeline
|
||||
|
||||
## Running Fuzzers
|
||||
|
||||
```bash
|
||||
# List all fuzz targets
|
||||
cargo fuzz list
|
||||
|
||||
# Run the parser fuzzer
|
||||
cargo +nightly fuzz run fuzz_parser
|
||||
|
||||
# Run the REPL fuzzer
|
||||
cargo +nightly fuzz run fuzz_repl
|
||||
|
||||
# Run with a time limit (e.g., 60 seconds)
|
||||
cargo +nightly fuzz run fuzz_parser -- -max_total_time=60
|
||||
|
||||
# Run with a corpus directory
|
||||
cargo +nightly fuzz run fuzz_parser corpus/fuzz_parser
|
||||
```
|
||||
|
||||
## Corpus
|
||||
|
||||
Interesting inputs found during fuzzing are automatically saved to `corpus/<target_name>/`.
|
||||
These can be used to reproduce issues:
|
||||
|
||||
```bash
|
||||
# Reproduce a crash
|
||||
cargo +nightly fuzz run fuzz_parser corpus/fuzz_parser/<crash_file>
|
||||
```
|
||||
|
||||
## Minimizing Crashes
|
||||
|
||||
```bash
|
||||
cargo +nightly fuzz tmin fuzz_parser <crash_file>
|
||||
```
|
||||
|
||||
## Coverage
|
||||
|
||||
Generate coverage reports:
|
||||
|
||||
```bash
|
||||
cargo +nightly fuzz coverage fuzz_parser
|
||||
```
|
||||
17
fuzz/fuzz_targets/fuzz_parser.rs
Normal file
17
fuzz/fuzz_targets/fuzz_parser.rs
Normal file
@ -0,0 +1,17 @@
|
||||
//! Fuzz the geolog parser
|
||||
//!
|
||||
//! This target exercises the lexer and parser to find edge cases
|
||||
//! and potential panics in the parsing code.
|
||||
|
||||
#![no_main]
|
||||
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
|
||||
fuzz_target!(|data: &[u8]| {
|
||||
// Try to interpret the data as UTF-8
|
||||
if let Ok(input) = std::str::from_utf8(data) {
|
||||
// The parser should never panic, even on malformed input
|
||||
// It should return an error instead
|
||||
let _ = geolog::parse(input);
|
||||
}
|
||||
});
|
||||
22
fuzz/fuzz_targets/fuzz_repl.rs
Normal file
22
fuzz/fuzz_targets/fuzz_repl.rs
Normal file
@ -0,0 +1,22 @@
|
||||
//! Fuzz the geolog REPL execution
|
||||
//!
|
||||
//! This target exercises the full REPL pipeline: parsing, elaboration,
|
||||
//! and instance creation. It should never panic on any input.
|
||||
|
||||
#![no_main]
|
||||
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
use geolog::repl::ReplState;
|
||||
|
||||
fuzz_target!(|data: &[u8]| {
|
||||
// Try to interpret the data as UTF-8
|
||||
if let Ok(input) = std::str::from_utf8(data) {
|
||||
// Create a fresh REPL state for each fuzz input
|
||||
// (in-memory, no persistence)
|
||||
let mut state = ReplState::new();
|
||||
|
||||
// The REPL should never panic on any input
|
||||
// It should return a Result<_, String> error instead
|
||||
let _ = state.execute_geolog(input);
|
||||
}
|
||||
});
|
||||
2
proofs/.gitignore
vendored
Normal file
2
proofs/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
# Lake build artifacts
|
||||
.lake/
|
||||
1
proofs/GeologProofs.lean
Normal file
1
proofs/GeologProofs.lean
Normal file
@ -0,0 +1 @@
|
||||
import GeologProofs.MonotonicSubmodel
|
||||
1520
proofs/GeologProofs/MonotonicSubmodel.lean
Normal file
1520
proofs/GeologProofs/MonotonicSubmodel.lean
Normal file
File diff suppressed because it is too large
Load Diff
115
proofs/lake-manifest.json
Normal file
115
proofs/lake-manifest.json
Normal file
@ -0,0 +1,115 @@
|
||||
{"version": "1.1.0",
|
||||
"packagesDir": ".lake/packages",
|
||||
"packages":
|
||||
[{"url": "https://github.com/kyoDralliam/model-theory-topos.git",
|
||||
"type": "git",
|
||||
"subDir": null,
|
||||
"scope": "",
|
||||
"rev": "5d0c00af95ef89b0bf6774208c853e254dc1ce33",
|
||||
"name": "«model-theory-topos»",
|
||||
"manifestFile": "lake-manifest.json",
|
||||
"inputRev": "main",
|
||||
"inherited": false,
|
||||
"configFile": "lakefile.lean"},
|
||||
{"url": "https://github.com/PatrickMassot/checkdecls.git",
|
||||
"type": "git",
|
||||
"subDir": null,
|
||||
"scope": "",
|
||||
"rev": "3d425859e73fcfbef85b9638c2a91708ef4a22d4",
|
||||
"name": "checkdecls",
|
||||
"manifestFile": "lake-manifest.json",
|
||||
"inputRev": null,
|
||||
"inherited": true,
|
||||
"configFile": "lakefile.lean"},
|
||||
{"url": "https://github.com/leanprover-community/mathlib4.git",
|
||||
"type": "git",
|
||||
"subDir": null,
|
||||
"scope": "",
|
||||
"rev": "19f4ef2c52b278bd96626e02d594751e6e12ac98",
|
||||
"name": "mathlib",
|
||||
"manifestFile": "lake-manifest.json",
|
||||
"inputRev": "v4.22.0-rc3",
|
||||
"inherited": true,
|
||||
"configFile": "lakefile.lean"},
|
||||
{"url": "https://github.com/leanprover-community/plausible",
|
||||
"type": "git",
|
||||
"subDir": null,
|
||||
"scope": "leanprover-community",
|
||||
"rev": "61c44bec841faabd47d11c2eda15f57ec2ffe9d5",
|
||||
"name": "plausible",
|
||||
"manifestFile": "lake-manifest.json",
|
||||
"inputRev": "main",
|
||||
"inherited": true,
|
||||
"configFile": "lakefile.toml"},
|
||||
{"url": "https://github.com/leanprover-community/LeanSearchClient",
|
||||
"type": "git",
|
||||
"subDir": null,
|
||||
"scope": "leanprover-community",
|
||||
"rev": "6c62474116f525d2814f0157bb468bf3a4f9f120",
|
||||
"name": "LeanSearchClient",
|
||||
"manifestFile": "lake-manifest.json",
|
||||
"inputRev": "main",
|
||||
"inherited": true,
|
||||
"configFile": "lakefile.toml"},
|
||||
{"url": "https://github.com/leanprover-community/import-graph",
|
||||
"type": "git",
|
||||
"subDir": null,
|
||||
"scope": "leanprover-community",
|
||||
"rev": "140dc642f4f29944abcdcd3096e8ea9b4469c873",
|
||||
"name": "importGraph",
|
||||
"manifestFile": "lake-manifest.json",
|
||||
"inputRev": "main",
|
||||
"inherited": true,
|
||||
"configFile": "lakefile.toml"},
|
||||
{"url": "https://github.com/leanprover-community/ProofWidgets4",
|
||||
"type": "git",
|
||||
"subDir": null,
|
||||
"scope": "leanprover-community",
|
||||
"rev": "96c67159f161fb6bf6ce91a2587232034ac33d7e",
|
||||
"name": "proofwidgets",
|
||||
"manifestFile": "lake-manifest.json",
|
||||
"inputRev": "v0.0.67",
|
||||
"inherited": true,
|
||||
"configFile": "lakefile.lean"},
|
||||
{"url": "https://github.com/leanprover-community/aesop",
|
||||
"type": "git",
|
||||
"subDir": null,
|
||||
"scope": "leanprover-community",
|
||||
"rev": "a62ecd0343a2dcfbcac6d1e8243f5821879c0244",
|
||||
"name": "aesop",
|
||||
"manifestFile": "lake-manifest.json",
|
||||
"inputRev": "master",
|
||||
"inherited": true,
|
||||
"configFile": "lakefile.toml"},
|
||||
{"url": "https://github.com/leanprover-community/quote4",
|
||||
"type": "git",
|
||||
"subDir": null,
|
||||
"scope": "leanprover-community",
|
||||
"rev": "867d9dc77534341321179c9aa40fceda675c50d4",
|
||||
"name": "Qq",
|
||||
"manifestFile": "lake-manifest.json",
|
||||
"inputRev": "master",
|
||||
"inherited": true,
|
||||
"configFile": "lakefile.toml"},
|
||||
{"url": "https://github.com/leanprover-community/batteries",
|
||||
"type": "git",
|
||||
"subDir": null,
|
||||
"scope": "leanprover-community",
|
||||
"rev": "3cabaef23886b82ba46f07018f2786d9496477d6",
|
||||
"name": "batteries",
|
||||
"manifestFile": "lake-manifest.json",
|
||||
"inputRev": "main",
|
||||
"inherited": true,
|
||||
"configFile": "lakefile.toml"},
|
||||
{"url": "https://github.com/mhuisi/lean4-cli",
|
||||
"type": "git",
|
||||
"subDir": null,
|
||||
"scope": "",
|
||||
"rev": "e22ed0883c7d7f9a7e294782b6b137b783715386",
|
||||
"name": "Cli",
|
||||
"manifestFile": "lake-manifest.json",
|
||||
"inputRev": "main",
|
||||
"inherited": true,
|
||||
"configFile": "lakefile.toml"}],
|
||||
"name": "«geolog-proofs»",
|
||||
"lakeDir": ".lake"}
|
||||
15
proofs/lakefile.lean
Normal file
15
proofs/lakefile.lean
Normal file
@ -0,0 +1,15 @@
|
||||
import Lake
|
||||
open Lake DSL
|
||||
|
||||
package «geolog-proofs» where
|
||||
leanOptions := #[
|
||||
⟨`pp.unicode.fun, true⟩
|
||||
]
|
||||
|
||||
-- Import model-theory-topos from GitHub
|
||||
require «model-theory-topos» from git
|
||||
"https://github.com/kyoDralliam/model-theory-topos.git" @ "main"
|
||||
|
||||
@[default_target]
|
||||
lean_lib «GeologProofs» where
|
||||
globs := #[.submodules `GeologProofs]
|
||||
1
proofs/lean-toolchain
Normal file
1
proofs/lean-toolchain
Normal file
@ -0,0 +1 @@
|
||||
leanprover/lean4:v4.22.0-rc3
|
||||
331
src/ast.rs
Normal file
331
src/ast.rs
Normal file
@ -0,0 +1,331 @@
|
||||
//! Abstract Syntax Tree for Geolog
|
||||
//!
|
||||
//! Based on the syntax sketched in loose_thoughts/2025-12-12_12:10.md
|
||||
|
||||
use std::fmt;
|
||||
|
||||
/// A span in the source code, for error reporting
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct Span {
|
||||
pub start: usize,
|
||||
pub end: usize,
|
||||
}
|
||||
|
||||
impl Span {
|
||||
pub fn new(start: usize, end: usize) -> Self {
|
||||
Self { start, end }
|
||||
}
|
||||
}
|
||||
|
||||
/// A node with source location
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Spanned<T> {
|
||||
pub node: T,
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
impl<T> Spanned<T> {
|
||||
pub fn new(node: T, span: Span) -> Self {
|
||||
Self { node, span }
|
||||
}
|
||||
}
|
||||
|
||||
/// An identifier, possibly qualified with `/` (e.g., `N/P`, `W/src/arc`)
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Path {
|
||||
pub segments: Vec<String>,
|
||||
}
|
||||
|
||||
impl Path {
|
||||
pub fn single(name: String) -> Self {
|
||||
Self {
|
||||
segments: vec![name],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_single(&self) -> bool {
|
||||
self.segments.len() == 1
|
||||
}
|
||||
|
||||
pub fn as_single(&self) -> Option<&str> {
|
||||
if self.segments.len() == 1 {
|
||||
Some(&self.segments[0])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Path {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.segments.join("/"))
|
||||
}
|
||||
}
|
||||
|
||||
/// A complete source file
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct File {
|
||||
pub declarations: Vec<Spanned<Declaration>>,
|
||||
}
|
||||
|
||||
/// Top-level declarations
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum Declaration {
|
||||
/// `namespace Foo;`
|
||||
Namespace(String),
|
||||
|
||||
/// `theory (params) Name { body }`
|
||||
Theory(TheoryDecl),
|
||||
|
||||
/// `TypeExpr instance Name { body }`
|
||||
Instance(InstanceDecl),
|
||||
|
||||
/// `query Name { ? : Type; }`
|
||||
Query(QueryDecl),
|
||||
}
|
||||
|
||||
/// A theory declaration
|
||||
/// e.g., `theory (N : PetriNet instance) Marking { ... }`
|
||||
/// or `theory Foo extends Bar { ... }`
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct TheoryDecl {
|
||||
pub params: Vec<Param>,
|
||||
pub name: String,
|
||||
/// Optional parent theory to extend
|
||||
pub extends: Option<Path>,
|
||||
pub body: Vec<Spanned<TheoryItem>>,
|
||||
}
|
||||
|
||||
/// A parameter to a theory
|
||||
/// e.g., `N : PetriNet instance`
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Param {
|
||||
pub name: String,
|
||||
pub ty: TypeExpr,
|
||||
}
|
||||
|
||||
/// Items that can appear in a theory body
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum TheoryItem {
|
||||
/// `P : Sort;`
|
||||
Sort(String),
|
||||
|
||||
/// `in.src : in -> P;`
|
||||
Function(FunctionDecl),
|
||||
|
||||
/// `ax1 : forall w : W. hyps |- concl;`
|
||||
Axiom(AxiomDecl),
|
||||
|
||||
/// Inline instance (for nested definitions)
|
||||
/// `initial_marking : N Marking instance;`
|
||||
Field(String, TypeExpr),
|
||||
}
|
||||
|
||||
/// A function/morphism declaration
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct FunctionDecl {
|
||||
pub name: Path, // Can be dotted like `in.src`
|
||||
pub domain: TypeExpr,
|
||||
pub codomain: TypeExpr,
|
||||
}
|
||||
|
||||
/// An axiom declaration
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct AxiomDecl {
|
||||
pub name: Path, // Can be hierarchical like `ax/anc/base`
|
||||
pub quantified: Vec<QuantifiedVar>,
|
||||
pub hypotheses: Vec<Formula>,
|
||||
pub conclusion: Formula,
|
||||
}
|
||||
|
||||
/// A quantified variable in an axiom
|
||||
/// e.g., `w : W` or `w1, w2 : W`
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct QuantifiedVar {
|
||||
pub names: Vec<String>,
|
||||
pub ty: TypeExpr,
|
||||
}
|
||||
|
||||
/// A single token in a type expression stack program (concatenative parsing)
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum TypeToken {
|
||||
/// Push a path onto the stack (might be sort, instance ref, or theory name)
|
||||
Path(Path),
|
||||
|
||||
/// The `Sort` keyword - pushes the Sort kind
|
||||
Sort,
|
||||
|
||||
/// The `Prop` keyword - pushes the Prop kind
|
||||
Prop,
|
||||
|
||||
/// The `instance` keyword - pops top, wraps as instance type, pushes
|
||||
Instance,
|
||||
|
||||
/// Arrow - pops two types (domain, codomain), pushes function type
|
||||
/// Note: arrows are handled specially during parsing to maintain infix syntax
|
||||
Arrow,
|
||||
|
||||
/// Record type literal: `[field : Type, ...]`
|
||||
/// Contains nested TypeExprs for field types (evaluated recursively)
|
||||
Record(Vec<(String, TypeExpr)>),
|
||||
}
|
||||
|
||||
/// A type expression as a flat stack program (concatenative style)
|
||||
///
|
||||
/// Instead of a tree like `App(App(A, B), C)`, we store a flat sequence
|
||||
/// `[Path(A), Path(B), Path(C)]` that gets evaluated during elaboration
|
||||
/// when we have access to the symbol table (to know theory arities).
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct TypeExpr {
|
||||
pub tokens: Vec<TypeToken>,
|
||||
}
|
||||
|
||||
impl TypeExpr {
|
||||
/// Create a type expression from a single path
|
||||
pub fn single_path(p: Path) -> Self {
|
||||
Self {
|
||||
tokens: vec![TypeToken::Path(p)],
|
||||
}
|
||||
}
|
||||
|
||||
/// Create the Sort kind
|
||||
pub fn sort() -> Self {
|
||||
Self {
|
||||
tokens: vec![TypeToken::Sort],
|
||||
}
|
||||
}
|
||||
|
||||
/// Create the Prop kind
|
||||
pub fn prop() -> Self {
|
||||
Self {
|
||||
tokens: vec![TypeToken::Prop],
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if this is a single path (common case)
|
||||
pub fn as_single_path(&self) -> Option<&Path> {
|
||||
if self.tokens.len() == 1
|
||||
&& let TypeToken::Path(p) = &self.tokens[0] {
|
||||
return Some(p);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Check if this is the Sort kind
|
||||
pub fn is_sort(&self) -> bool {
|
||||
matches!(self.tokens.as_slice(), [TypeToken::Sort])
|
||||
}
|
||||
|
||||
/// Check if this ends with `instance`
|
||||
pub fn is_instance(&self) -> bool {
|
||||
self.tokens.last() == Some(&TypeToken::Instance)
|
||||
}
|
||||
|
||||
/// Get the inner type expression (without the trailing `instance` token)
|
||||
pub fn instance_inner(&self) -> Option<Self> {
|
||||
if self.is_instance() {
|
||||
Some(Self {
|
||||
tokens: self.tokens[..self.tokens.len() - 1].to_vec(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if this is the Prop kind
|
||||
pub fn is_prop(&self) -> bool {
|
||||
matches!(self.tokens.as_slice(), [TypeToken::Prop])
|
||||
}
|
||||
|
||||
/// Check if this is a record type
|
||||
pub fn as_record(&self) -> Option<&Vec<(String, TypeExpr)>> {
|
||||
if self.tokens.len() == 1
|
||||
&& let TypeToken::Record(fields) = &self.tokens[0] {
|
||||
return Some(fields);
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Terms (elements of types)
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum Term {
|
||||
/// A variable or path: `w`, `W/src/arc`
|
||||
/// `/` is namespace qualification
|
||||
Path(Path),
|
||||
|
||||
/// Function application (postfix style in surface syntax)
|
||||
/// `w W/src` means "apply W/src to w"
|
||||
App(Box<Term>, Box<Term>),
|
||||
|
||||
/// Field projection: `expr .field`
|
||||
/// Note the space before `.` to distinguish from path qualification
|
||||
Project(Box<Term>, String),
|
||||
|
||||
/// Record literal: `[firing: f, arc: arc]`
|
||||
Record(Vec<(String, Term)>),
|
||||
}
|
||||
|
||||
/// Formulas (geometric logic)
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum Formula {
|
||||
/// False/Bottom (⊥): inconsistency, empty disjunction
|
||||
False,
|
||||
|
||||
/// Relation application: `rel(term)` or `rel([field: value, ...])`
|
||||
RelApp(String, Term),
|
||||
|
||||
/// Equality: `t1 = t2`
|
||||
Eq(Term, Term),
|
||||
|
||||
/// Conjunction (often implicit in antecedents)
|
||||
And(Vec<Formula>),
|
||||
|
||||
/// Disjunction: `phi \/ psi`
|
||||
Or(Vec<Formula>),
|
||||
|
||||
/// Existential: `exists w : W. phi`
|
||||
Exists(Vec<QuantifiedVar>, Box<Formula>),
|
||||
|
||||
/// Truth
|
||||
True,
|
||||
}
|
||||
|
||||
/// An instance declaration
|
||||
/// e.g., `instance ExampleNet : PetriNet = { ... }`
|
||||
/// or `instance ExampleNet : PetriNet = chase { ... }` for chase-before-check
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct InstanceDecl {
|
||||
pub theory: TypeExpr,
|
||||
pub name: String,
|
||||
pub body: Vec<Spanned<InstanceItem>>,
|
||||
/// If true, run chase algorithm after elaboration before checking axioms.
|
||||
/// Syntax: `instance Name : Theory = chase { ... }`
|
||||
pub needs_chase: bool,
|
||||
}
|
||||
|
||||
/// Items in an instance body
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum InstanceItem {
|
||||
/// Element declaration: `A : P;` or `a, b, c : P;`
|
||||
Element(Vec<String>, TypeExpr),
|
||||
|
||||
/// Equation: `ab_in in.src = A;`
|
||||
Equation(Term, Term),
|
||||
|
||||
/// Nested instance: `initial_marking = N Marking instance { ... };`
|
||||
NestedInstance(String, InstanceDecl),
|
||||
|
||||
/// Relation assertion: `[item: buy_groceries] completed;`
|
||||
/// The Term should be a record with the relation's domain fields,
|
||||
/// and String is the relation name.
|
||||
RelationAssertion(Term, String),
|
||||
}
|
||||
|
||||
/// A query declaration
|
||||
/// e.g., `query query0 { ? : ExampleNet Problem0 ReachabilityProblemSolution; }`
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct QueryDecl {
|
||||
pub name: String,
|
||||
pub goal: TypeExpr,
|
||||
}
|
||||
1288
src/bin/geolog.rs
Normal file
1288
src/bin/geolog.rs
Normal file
File diff suppressed because it is too large
Load Diff
258
src/cc.rs
Normal file
258
src/cc.rs
Normal file
@ -0,0 +1,258 @@
|
||||
//! Congruence Closure for equality reasoning.
|
||||
//!
|
||||
//! This module provides a union-find based congruence closure implementation
|
||||
//! that can be used by both the solver (for model enumeration) and the chase
|
||||
//! (for computing derived relations with equality saturation).
|
||||
//!
|
||||
//! # Key Types
|
||||
//!
|
||||
//! - [`CongruenceClosure`]: Main struct wrapping union-find + pending equation queue
|
||||
//! - [`PendingEquation`]: An equation waiting to be processed
|
||||
//! - [`EquationReason`]: Why an equation was created (for debugging/explanation)
|
||||
//!
|
||||
//! # Usage
|
||||
//!
|
||||
//! ```ignore
|
||||
//! use geolog::cc::{CongruenceClosure, EquationReason};
|
||||
//!
|
||||
//! let mut cc = CongruenceClosure::new();
|
||||
//!
|
||||
//! // Add equation: a = b
|
||||
//! cc.add_equation(a, b, EquationReason::UserAsserted);
|
||||
//!
|
||||
//! // Process pending equations
|
||||
//! while let Some(eq) = cc.pop_pending() {
|
||||
//! cc.merge(eq.lhs, eq.rhs);
|
||||
//! // Check for function conflicts, add congruence equations...
|
||||
//! }
|
||||
//!
|
||||
//! // Query equivalence
|
||||
//! assert!(cc.are_equal(a, b));
|
||||
//! ```
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use egglog_union_find::UnionFind;
|
||||
|
||||
use crate::id::{NumericId, Slid};
|
||||
|
||||
/// A pending equation to be processed.
|
||||
///
|
||||
/// Equations arise from:
|
||||
/// 1. Function conflicts: `f(a) = x` and `f(a) = y` implies `x = y`
|
||||
/// 2. Axiom consequents: `∀x. P(x) → x = y`
|
||||
/// 3. Record projections: `[fst: a, snd: b].fst = a`
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct PendingEquation {
|
||||
/// Left-hand side element
|
||||
pub lhs: Slid,
|
||||
/// Right-hand side element
|
||||
pub rhs: Slid,
|
||||
/// Reason for the equation (for debugging/explanation)
|
||||
pub reason: EquationReason,
|
||||
}
|
||||
|
||||
/// Reason an equation was created
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum EquationReason {
|
||||
/// Function already maps domain to different values
|
||||
FunctionConflict { func_id: usize, domain: Slid },
|
||||
/// Axiom consequent required this equality
|
||||
AxiomConsequent { axiom_idx: usize },
|
||||
/// User asserted this equation
|
||||
UserAsserted,
|
||||
/// Congruence: f(a) = f(b) because a = b
|
||||
Congruence { func_id: usize },
|
||||
/// Chase-derived: equality conclusion in chase
|
||||
ChaseConclusion,
|
||||
}
|
||||
|
||||
/// Congruence closure state.
|
||||
///
|
||||
/// This wraps a union-find structure and pending equation queue,
|
||||
/// providing methods for merging elements and tracking equivalence classes.
|
||||
///
|
||||
/// Note: This struct handles the union-find bookkeeping but does NOT
|
||||
/// automatically propagate through function applications. The caller
|
||||
/// (solver or chase) is responsible for detecting function conflicts
|
||||
/// and adding congruence equations.
|
||||
#[derive(Clone)]
|
||||
pub struct CongruenceClosure {
|
||||
/// Union-find for tracking equivalence classes
|
||||
/// Uses Slid indices as keys
|
||||
pub uf: UnionFind<usize>,
|
||||
/// Pending equations to process
|
||||
pub pending: VecDeque<PendingEquation>,
|
||||
/// Number of merges performed (for statistics)
|
||||
pub merge_count: usize,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for CongruenceClosure {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("CongruenceClosure")
|
||||
.field("pending", &self.pending)
|
||||
.field("merge_count", &self.merge_count)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for CongruenceClosure {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl CongruenceClosure {
|
||||
/// Create a new congruence closure
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
uf: UnionFind::default(),
|
||||
pending: VecDeque::new(),
|
||||
merge_count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Find the canonical representative of an element
|
||||
/// Note: The UnionFind automatically reserves space as needed
|
||||
pub fn find(&mut self, slid: Slid) -> usize {
|
||||
self.uf.find(slid.index())
|
||||
}
|
||||
|
||||
/// Check if two elements are in the same equivalence class
|
||||
pub fn are_equal(&mut self, a: Slid, b: Slid) -> bool {
|
||||
self.find(a) == self.find(b)
|
||||
}
|
||||
|
||||
/// Add a pending equation
|
||||
pub fn add_equation(&mut self, lhs: Slid, rhs: Slid, reason: EquationReason) {
|
||||
self.pending.push_back(PendingEquation { lhs, rhs, reason });
|
||||
}
|
||||
|
||||
/// Pop the next pending equation, if any
|
||||
pub fn pop_pending(&mut self) -> Option<PendingEquation> {
|
||||
self.pending.pop_front()
|
||||
}
|
||||
|
||||
/// Check if there are pending equations
|
||||
pub fn has_pending(&self) -> bool {
|
||||
!self.pending.is_empty()
|
||||
}
|
||||
|
||||
/// Merge two elements, returning true if they were not already equal
|
||||
pub fn merge(&mut self, a: Slid, b: Slid) -> bool {
|
||||
let a_idx = a.index();
|
||||
let b_idx = b.index();
|
||||
|
||||
let ra = self.uf.find(a_idx);
|
||||
let rb = self.uf.find(b_idx);
|
||||
|
||||
if ra != rb {
|
||||
self.uf.union(ra, rb);
|
||||
self.merge_count += 1;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the canonical Slid for an element
|
||||
///
|
||||
/// Note: This returns a Slid with the canonical index, but the actual
|
||||
/// element in the Structure is still at the original Slid.
|
||||
pub fn canonical(&mut self, slid: Slid) -> Slid {
|
||||
let idx = self.find(slid);
|
||||
Slid::from_usize(idx)
|
||||
}
|
||||
|
||||
/// Get the number of elements tracked
|
||||
pub fn num_elements(&self) -> usize {
|
||||
self.merge_count + self.pending.len() // approximation
|
||||
}
|
||||
|
||||
/// Get statistics about the congruence closure: (merges, pending)
|
||||
pub fn stats(&self) -> (usize, usize) {
|
||||
(self.merge_count, self.pending.len())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_congruence_closure_basic() {
|
||||
let mut cc = CongruenceClosure::new();
|
||||
let a = Slid::from_usize(0);
|
||||
let b = Slid::from_usize(1);
|
||||
let c = Slid::from_usize(2);
|
||||
|
||||
// Initially all different
|
||||
assert!(!cc.are_equal(a, b));
|
||||
assert!(!cc.are_equal(b, c));
|
||||
assert!(!cc.are_equal(a, c));
|
||||
|
||||
// Merge a and b
|
||||
assert!(cc.merge(a, b));
|
||||
assert!(cc.are_equal(a, b));
|
||||
assert!(!cc.are_equal(b, c));
|
||||
|
||||
// Merge b and c (should transitively merge a and c)
|
||||
assert!(cc.merge(b, c));
|
||||
assert!(cc.are_equal(a, c));
|
||||
assert!(cc.are_equal(a, b));
|
||||
assert!(cc.are_equal(b, c));
|
||||
|
||||
// Merging already equal elements returns false
|
||||
assert!(!cc.merge(a, c));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_congruence_closure_pending() {
|
||||
let mut cc = CongruenceClosure::new();
|
||||
let a = Slid::from_usize(0);
|
||||
let b = Slid::from_usize(1);
|
||||
|
||||
assert!(!cc.has_pending());
|
||||
|
||||
cc.add_equation(a, b, EquationReason::UserAsserted);
|
||||
assert!(cc.has_pending());
|
||||
|
||||
let eq = cc.pop_pending().unwrap();
|
||||
assert_eq!(eq.lhs, a);
|
||||
assert_eq!(eq.rhs, b);
|
||||
assert!(!cc.has_pending());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_congruence_closure_stats() {
|
||||
let mut cc = CongruenceClosure::new();
|
||||
let a = Slid::from_usize(0);
|
||||
let b = Slid::from_usize(1);
|
||||
|
||||
assert_eq!(cc.stats(), (0, 0));
|
||||
|
||||
cc.merge(a, b);
|
||||
assert_eq!(cc.stats(), (1, 0));
|
||||
|
||||
cc.add_equation(a, b, EquationReason::UserAsserted);
|
||||
assert_eq!(cc.stats(), (1, 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_canonical() {
|
||||
let mut cc = CongruenceClosure::new();
|
||||
let a = Slid::from_usize(5);
|
||||
let b = Slid::from_usize(10);
|
||||
|
||||
// Before merge, each is its own canonical
|
||||
let ca = cc.canonical(a);
|
||||
let cb = cc.canonical(b);
|
||||
assert_ne!(ca, cb);
|
||||
|
||||
// After merge, both have same canonical
|
||||
cc.merge(a, b);
|
||||
let ca2 = cc.canonical(a);
|
||||
let cb2 = cc.canonical(b);
|
||||
assert_eq!(ca2, cb2);
|
||||
}
|
||||
}
|
||||
1511
src/core.rs
Normal file
1511
src/core.rs
Normal file
File diff suppressed because it is too large
Load Diff
315
src/elaborate/env.rs
Normal file
315
src/elaborate/env.rs
Normal file
@ -0,0 +1,315 @@
|
||||
//! Elaboration environment and basic elaboration functions.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::ast;
|
||||
use crate::core::*;
|
||||
|
||||
use super::error::{ElabError, ElabResult};
|
||||
|
||||
/// Environment for elaboration — tracks what's in scope
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Env {
|
||||
/// Known theories, by name
|
||||
pub theories: HashMap<String, Rc<ElaboratedTheory>>,
|
||||
/// Current theory being elaborated (if any)
|
||||
pub current_theory: Option<String>,
|
||||
/// Local signature being built
|
||||
pub signature: Signature,
|
||||
/// Parameters in scope (for parameterized theories)
|
||||
pub params: Vec<(String, Rc<ElaboratedTheory>)>,
|
||||
}
|
||||
|
||||
impl Env {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Resolve a path like "N/P" where N is a parameter and P is a sort in N's theory.
|
||||
///
|
||||
/// All param sorts are copied into the local signature with qualified names (e.g., "N/P"),
|
||||
/// so we just need to look up the joined path in the current signature.
|
||||
pub fn resolve_sort_path(&self, path: &ast::Path) -> ElabResult<DerivedSort> {
|
||||
// Join all segments with "/" — this handles both simple names like "F"
|
||||
// and qualified names like "N/P"
|
||||
let full_name = path.segments.join("/");
|
||||
if let Some(id) = self.signature.lookup_sort(&full_name) {
|
||||
return Ok(DerivedSort::Base(id));
|
||||
}
|
||||
Err(ElabError::UnknownSort(full_name))
|
||||
}
|
||||
|
||||
/// Resolve a function path like "N/in/src" or "F/of".
|
||||
///
|
||||
/// All param functions are copied into the local signature with qualified names,
|
||||
/// so we just need to look up the joined path.
|
||||
pub fn resolve_func_path(&self, path: &ast::Path) -> ElabResult<FuncId> {
|
||||
let full_name = path.segments.join("/");
|
||||
if let Some(id) = self.signature.lookup_func(&full_name) {
|
||||
return Ok(id);
|
||||
}
|
||||
Err(ElabError::UnknownFunction(full_name))
|
||||
}
|
||||
}
|
||||
|
||||
/// Elaborate a type expression into a DerivedSort
|
||||
///
|
||||
/// Uses the concatenative stack-based type evaluator.
|
||||
pub fn elaborate_type(env: &Env, ty: &ast::TypeExpr) -> ElabResult<DerivedSort> {
|
||||
use super::types::eval_type_expr;
|
||||
|
||||
let val = eval_type_expr(ty, env)?;
|
||||
val.as_derived_sort(env)
|
||||
}
|
||||
|
||||
/// Elaborate a term in a given context
|
||||
pub fn elaborate_term(env: &Env, ctx: &Context, term: &ast::Term) -> ElabResult<Term> {
|
||||
match term {
|
||||
ast::Term::Path(path) => {
|
||||
if path.segments.len() == 1 {
|
||||
// Simple variable
|
||||
let name = &path.segments[0];
|
||||
if let Some((_, sort)) = ctx.lookup(name) {
|
||||
return Ok(Term::Var(name.clone(), sort.clone()));
|
||||
}
|
||||
return Err(ElabError::UnknownVariable(name.clone()));
|
||||
}
|
||||
// Qualified path — could be a variable or a function reference
|
||||
// For now, treat as variable lookup failure
|
||||
Err(ElabError::UnknownVariable(path.to_string()))
|
||||
}
|
||||
ast::Term::App(base, func) => {
|
||||
// In surface syntax, application is postfix: `x f` means apply f to x
|
||||
// So App(base, func) where base is the argument and func is the function
|
||||
// First, elaborate the base (the argument)
|
||||
let elab_arg = elaborate_term(env, ctx, base)?;
|
||||
let arg_sort = elab_arg.sort(&env.signature);
|
||||
|
||||
// Then figure out what the function is
|
||||
match func.as_ref() {
|
||||
ast::Term::Path(path) => {
|
||||
let func_id = env.resolve_func_path(path)?;
|
||||
let func_sym = &env.signature.functions[func_id];
|
||||
|
||||
// Type check: argument sort must match function domain
|
||||
if arg_sort != func_sym.domain {
|
||||
return Err(ElabError::TypeMismatch {
|
||||
expected: func_sym.domain.clone(),
|
||||
got: arg_sort,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Term::App(func_id, Box::new(elab_arg)))
|
||||
}
|
||||
_ => {
|
||||
// Higher-order application — not supported yet
|
||||
Err(ElabError::UnsupportedFeature(
|
||||
"higher-order application".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
ast::Term::Project(base, field) => {
|
||||
let elab_base = elaborate_term(env, ctx, base)?;
|
||||
Ok(Term::Project(Box::new(elab_base), field.clone()))
|
||||
}
|
||||
ast::Term::Record(fields) => {
|
||||
let elab_fields: Result<Vec<_>, _> = fields
|
||||
.iter()
|
||||
.map(|(name, term)| elaborate_term(env, ctx, term).map(|t| (name.clone(), t)))
|
||||
.collect();
|
||||
Ok(Term::Record(elab_fields?))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Elaborate a formula
|
||||
pub fn elaborate_formula(env: &Env, ctx: &Context, formula: &ast::Formula) -> ElabResult<Formula> {
|
||||
match formula {
|
||||
ast::Formula::True => Ok(Formula::True),
|
||||
ast::Formula::False => Ok(Formula::False),
|
||||
ast::Formula::Eq(lhs, rhs) => {
|
||||
let elab_lhs = elaborate_term(env, ctx, lhs)?;
|
||||
let elab_rhs = elaborate_term(env, ctx, rhs)?;
|
||||
|
||||
// Type check: both sides must have the same sort
|
||||
let lhs_sort = elab_lhs.sort(&env.signature);
|
||||
let rhs_sort = elab_rhs.sort(&env.signature);
|
||||
if lhs_sort != rhs_sort {
|
||||
return Err(ElabError::TypeMismatch {
|
||||
expected: lhs_sort,
|
||||
got: rhs_sort,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Formula::Eq(elab_lhs, elab_rhs))
|
||||
}
|
||||
ast::Formula::And(conjuncts) => {
|
||||
let elab: Result<Vec<_>, _> = conjuncts
|
||||
.iter()
|
||||
.map(|f| elaborate_formula(env, ctx, f))
|
||||
.collect();
|
||||
Ok(Formula::Conj(elab?))
|
||||
}
|
||||
ast::Formula::Or(disjuncts) => {
|
||||
let elab: Result<Vec<_>, _> = disjuncts
|
||||
.iter()
|
||||
.map(|f| elaborate_formula(env, ctx, f))
|
||||
.collect();
|
||||
Ok(Formula::Disj(elab?))
|
||||
}
|
||||
ast::Formula::Exists(vars, body) => {
|
||||
// Extend context with quantified variables
|
||||
let mut extended_ctx = ctx.clone();
|
||||
for qv in vars {
|
||||
let sort = elaborate_type(env, &qv.ty)?;
|
||||
for name in &qv.names {
|
||||
extended_ctx = extended_ctx.extend(name.clone(), sort.clone());
|
||||
}
|
||||
}
|
||||
let elab_body = elaborate_formula(env, &extended_ctx, body)?;
|
||||
|
||||
// Build nested existentials (one for each variable)
|
||||
let mut result = elab_body;
|
||||
for qv in vars.iter().rev() {
|
||||
let sort = elaborate_type(env, &qv.ty)?;
|
||||
for name in qv.names.iter().rev() {
|
||||
result = Formula::Exists(name.clone(), sort.clone(), Box::new(result));
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
ast::Formula::RelApp(rel_name, arg) => {
|
||||
// Look up the relation
|
||||
let rel_id = env
|
||||
.signature
|
||||
.lookup_rel(rel_name)
|
||||
.ok_or_else(|| ElabError::UnknownRel(rel_name.clone()))?;
|
||||
|
||||
// Elaborate the argument
|
||||
let elab_arg = elaborate_term(env, ctx, arg)?;
|
||||
|
||||
// Type check: argument must match relation domain
|
||||
let rel_sym = &env.signature.relations[rel_id];
|
||||
let arg_sort = elab_arg.sort(&env.signature);
|
||||
if arg_sort != rel_sym.domain {
|
||||
return Err(ElabError::TypeMismatch {
|
||||
expected: rel_sym.domain.clone(),
|
||||
got: arg_sort,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Formula::Rel(rel_id, elab_arg))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Remap a DerivedSort for nested instance fields.
|
||||
///
|
||||
/// When copying sorts/functions from a nested instance field's theory into the local signature,
|
||||
/// we need different remapping rules:
|
||||
/// - Unqualified sorts (like "Token" in Marking) get prefixed with field_prefix (e.g., "RP/initial/Token")
|
||||
/// - Already-qualified sorts (like "N/P" in Marking) map to the parent param (e.g., just "N/P")
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `field_prefix` - The prefix for the nested field (e.g., "RP/initial")
|
||||
/// * `parent_param` - The parent parameter name (e.g., "RP"), used to strip when mapping qualified sorts
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn remap_derived_sort_for_nested(
|
||||
sort: &DerivedSort,
|
||||
source_sig: &Signature,
|
||||
target_sig: &Signature,
|
||||
field_prefix: &str,
|
||||
parent_param: &str,
|
||||
) -> DerivedSort {
|
||||
match sort {
|
||||
DerivedSort::Base(source_id) => {
|
||||
let sort_name = &source_sig.sorts[*source_id];
|
||||
let qualified_name = if sort_name.contains('/') {
|
||||
// Already qualified (e.g., "N/P" from a parameterized theory)
|
||||
// Try to find it directly in the target (e.g., "N/P" should exist from outer param)
|
||||
// If not found, try with parent param prefix (e.g., "RP/N/P")
|
||||
if target_sig.lookup_sort(sort_name).is_some() {
|
||||
sort_name.clone()
|
||||
} else {
|
||||
format!("{}/{}", parent_param, sort_name)
|
||||
}
|
||||
} else {
|
||||
// Unqualified sort from the field's theory - prefix with field_prefix
|
||||
format!("{}/{}", field_prefix, sort_name)
|
||||
};
|
||||
if let Some(target_id) = target_sig.lookup_sort(&qualified_name) {
|
||||
DerivedSort::Base(target_id)
|
||||
} else {
|
||||
// Fallback: just use the source ID (shouldn't happen in well-formed code)
|
||||
eprintln!(
|
||||
"Warning: could not remap sort '{}' (qualified: '{}') in nested field",
|
||||
sort_name, qualified_name
|
||||
);
|
||||
sort.clone()
|
||||
}
|
||||
}
|
||||
DerivedSort::Product(fields) => {
|
||||
let remapped_fields = fields
|
||||
.iter()
|
||||
.map(|(name, s)| {
|
||||
(
|
||||
name.clone(),
|
||||
remap_derived_sort_for_nested(s, source_sig, target_sig, field_prefix, parent_param),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
DerivedSort::Product(remapped_fields)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Remap a DerivedSort from one signature namespace to another.
|
||||
///
|
||||
/// When copying sorts/functions from a param theory into the local signature,
|
||||
/// the sort IDs need to be remapped. For example, if PetriNet has sort P at id=0,
|
||||
/// and we copy it as "N/P" into local signature at id=2, then any DerivedSort::Base(0)
|
||||
/// needs to become DerivedSort::Base(2).
|
||||
///
|
||||
/// The `preserve_existing_prefix` flag controls requalification behavior:
|
||||
/// - false (instance params): always prefix with param_name. N/X becomes M/N/X.
|
||||
/// - true (extends): preserve existing qualifier. N/X stays N/X.
|
||||
pub(crate) fn remap_derived_sort(
|
||||
sort: &DerivedSort,
|
||||
source_sig: &Signature,
|
||||
target_sig: &Signature,
|
||||
param_name: &str,
|
||||
preserve_existing_prefix: bool,
|
||||
) -> DerivedSort {
|
||||
match sort {
|
||||
DerivedSort::Base(source_id) => {
|
||||
// Look up the sort name in the source signature
|
||||
let sort_name = &source_sig.sorts[*source_id];
|
||||
// Find the corresponding qualified name in target signature
|
||||
let qualified_name = if preserve_existing_prefix && sort_name.contains('/') {
|
||||
// Extends case: already-qualified names keep their original qualifier
|
||||
sort_name.clone()
|
||||
} else {
|
||||
// Instance param case OR unqualified name: prefix with param_name
|
||||
format!("{}/{}", param_name, sort_name)
|
||||
};
|
||||
let target_id = target_sig
|
||||
.lookup_sort(&qualified_name)
|
||||
.expect("qualified sort should have been added");
|
||||
DerivedSort::Base(target_id)
|
||||
}
|
||||
DerivedSort::Product(fields) => {
|
||||
let remapped_fields = fields
|
||||
.iter()
|
||||
.map(|(name, s)| {
|
||||
(
|
||||
name.clone(),
|
||||
remap_derived_sort(s, source_sig, target_sig, param_name, preserve_existing_prefix),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
DerivedSort::Product(remapped_fields)
|
||||
}
|
||||
}
|
||||
}
|
||||
185
src/elaborate/error.rs
Normal file
185
src/elaborate/error.rs
Normal file
@ -0,0 +1,185 @@
|
||||
//! Elaboration error types.
|
||||
|
||||
use crate::core::DerivedSort;
|
||||
|
||||
/// A concrete counterexample showing which variable bindings violate an axiom.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CounterExample {
|
||||
/// (variable_name, element_name) pairs showing the violating assignment
|
||||
pub bindings: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for CounterExample {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let parts: Vec<String> = self
|
||||
.bindings
|
||||
.iter()
|
||||
.map(|(var, elem)| format!("{} = {}", var, elem))
|
||||
.collect();
|
||||
write!(f, "{{{}}}", parts.join(", "))
|
||||
}
|
||||
}
|
||||
|
||||
/// Elaboration errors
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum ElabError {
|
||||
UnknownSort(String),
|
||||
UnknownTheory(String),
|
||||
UnknownFunction(String),
|
||||
UnknownRel(String),
|
||||
UnknownVariable(String),
|
||||
TypeMismatch {
|
||||
expected: DerivedSort,
|
||||
got: DerivedSort,
|
||||
},
|
||||
NotASort(String),
|
||||
NotAFunction(String),
|
||||
NotARecord(String),
|
||||
NoSuchField {
|
||||
record: String,
|
||||
field: String,
|
||||
},
|
||||
InvalidPath(String),
|
||||
DuplicateDefinition(String),
|
||||
UnsupportedFeature(String),
|
||||
PartialFunction {
|
||||
func_name: String,
|
||||
missing_elements: Vec<String>,
|
||||
},
|
||||
/// Type error in function application: element's sort doesn't match function's domain
|
||||
DomainMismatch {
|
||||
func_name: String,
|
||||
element_name: String,
|
||||
expected_sort: String,
|
||||
actual_sort: String,
|
||||
},
|
||||
/// Type error in equation: RHS sort doesn't match function's codomain
|
||||
CodomainMismatch {
|
||||
func_name: String,
|
||||
element_name: String,
|
||||
expected_sort: String,
|
||||
actual_sort: String,
|
||||
},
|
||||
/// Axiom violation during instance checking
|
||||
AxiomViolation {
|
||||
axiom_index: usize,
|
||||
axiom_name: Option<String>,
|
||||
num_violations: usize,
|
||||
/// Concrete counterexamples (limited to first few for readability)
|
||||
counterexamples: Vec<CounterExample>,
|
||||
},
|
||||
/// Chase algorithm failed (e.g., didn't converge)
|
||||
ChaseFailed(String),
|
||||
|
||||
/// Not enough arguments for a parameterized theory
|
||||
NotEnoughArgs {
|
||||
name: String,
|
||||
expected: usize,
|
||||
got: usize,
|
||||
},
|
||||
|
||||
/// Type expression evaluation error
|
||||
TypeExprError(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ElabError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ElabError::UnknownSort(s) => write!(f, "unknown sort: {}", s),
|
||||
ElabError::UnknownTheory(s) => write!(f, "unknown theory: {}", s),
|
||||
ElabError::UnknownFunction(s) => write!(f, "unknown function: {}", s),
|
||||
ElabError::UnknownRel(s) => write!(f, "unknown relation: {}", s),
|
||||
ElabError::UnknownVariable(s) => write!(f, "unknown variable: {}", s),
|
||||
ElabError::TypeMismatch { expected, got } => {
|
||||
write!(f, "type mismatch: expected {}, got {}", expected, got)
|
||||
}
|
||||
ElabError::NotASort(s) => write!(f, "not a sort: {}", s),
|
||||
ElabError::NotAFunction(s) => write!(f, "not a function: {}", s),
|
||||
ElabError::NotARecord(s) => write!(f, "not a record type: {}", s),
|
||||
ElabError::NoSuchField { record, field } => {
|
||||
write!(f, "no field '{}' in record {}", field, record)
|
||||
}
|
||||
ElabError::InvalidPath(s) => write!(f, "invalid path: {}", s),
|
||||
ElabError::DuplicateDefinition(s) => write!(f, "duplicate definition: {}", s),
|
||||
ElabError::UnsupportedFeature(s) => write!(f, "unsupported feature: {}", s),
|
||||
ElabError::PartialFunction {
|
||||
func_name,
|
||||
missing_elements,
|
||||
} => {
|
||||
write!(
|
||||
f,
|
||||
"partial function '{}': missing definitions for {:?}",
|
||||
func_name, missing_elements
|
||||
)
|
||||
}
|
||||
ElabError::DomainMismatch {
|
||||
func_name,
|
||||
element_name,
|
||||
expected_sort,
|
||||
actual_sort,
|
||||
} => {
|
||||
write!(
|
||||
f,
|
||||
"type error: '{}' has sort '{}', but function '{}' expects domain sort '{}'",
|
||||
element_name, actual_sort, func_name, expected_sort
|
||||
)
|
||||
}
|
||||
ElabError::CodomainMismatch {
|
||||
func_name,
|
||||
element_name,
|
||||
expected_sort,
|
||||
actual_sort,
|
||||
} => {
|
||||
write!(
|
||||
f,
|
||||
"type error: '{}' has sort '{}', but function '{}' has codomain sort '{}'",
|
||||
element_name, actual_sort, func_name, expected_sort
|
||||
)
|
||||
}
|
||||
ElabError::AxiomViolation {
|
||||
axiom_index,
|
||||
axiom_name,
|
||||
num_violations,
|
||||
counterexamples,
|
||||
} => {
|
||||
let axiom_desc = if let Some(name) = axiom_name {
|
||||
format!("axiom '{}' (#{}) violated", name, axiom_index)
|
||||
} else {
|
||||
format!("axiom #{} violated", axiom_index)
|
||||
};
|
||||
|
||||
if counterexamples.is_empty() {
|
||||
write!(f, "{}: {} counterexample(s) found", axiom_desc, num_violations)
|
||||
} else {
|
||||
writeln!(f, "{}: {} counterexample(s) found", axiom_desc, num_violations)?;
|
||||
for (i, ce) in counterexamples.iter().enumerate() {
|
||||
writeln!(f, " #{}: {}", i + 1, ce)?;
|
||||
}
|
||||
if *num_violations > counterexamples.len() {
|
||||
write!(
|
||||
f,
|
||||
" ... and {} more",
|
||||
num_violations - counterexamples.len()
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
ElabError::ChaseFailed(msg) => write!(f, "chase failed: {}", msg),
|
||||
ElabError::NotEnoughArgs {
|
||||
name,
|
||||
expected,
|
||||
got,
|
||||
} => {
|
||||
write!(
|
||||
f,
|
||||
"'{}' expects {} argument(s), but only {} provided",
|
||||
name, expected, got
|
||||
)
|
||||
}
|
||||
ElabError::TypeExprError(msg) => write!(f, "type expression error: {}", msg),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type ElabResult<T> = Result<T, ElabError>;
|
||||
1431
src/elaborate/instance.rs
Normal file
1431
src/elaborate/instance.rs
Normal file
File diff suppressed because it is too large
Load Diff
17
src/elaborate/mod.rs
Normal file
17
src/elaborate/mod.rs
Normal file
@ -0,0 +1,17 @@
|
||||
//! Elaboration: surface syntax → typed core representation
|
||||
//!
|
||||
//! This module transforms the untyped AST into the typed core representation,
|
||||
//! performing name resolution and type checking along the way.
|
||||
|
||||
mod env;
|
||||
mod error;
|
||||
mod instance;
|
||||
mod theory;
|
||||
pub mod types;
|
||||
|
||||
// Re-export main types and functions
|
||||
pub use env::{elaborate_formula, elaborate_term, elaborate_type, Env};
|
||||
pub use error::{ElabError, ElabResult};
|
||||
pub use instance::{ElaborationContext, InstanceElaborationResult, InstanceEntry, elaborate_instance_ctx, elaborate_instance_ctx_partial};
|
||||
pub use theory::elaborate_theory;
|
||||
pub use types::{eval_type_expr, TypeValue};
|
||||
739
src/elaborate/theory.rs
Normal file
739
src/elaborate/theory.rs
Normal file
@ -0,0 +1,739 @@
|
||||
//! Theory elaboration.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::ast;
|
||||
use crate::core::*;
|
||||
|
||||
use super::env::{elaborate_formula, elaborate_type, remap_derived_sort, Env};
|
||||
use super::error::{ElabError, ElabResult};
|
||||
|
||||
/// Elaborate a theory declaration
|
||||
pub fn elaborate_theory(env: &mut Env, theory: &ast::TheoryDecl) -> ElabResult<ElaboratedTheory> {
|
||||
// Set up the environment for this theory
|
||||
let mut local_env = env.clone();
|
||||
local_env.current_theory = Some(theory.name.clone());
|
||||
local_env.signature = Signature::new();
|
||||
|
||||
// Track extended theories for transitive closure semantics
|
||||
let mut extends_chain: Vec<String> = Vec::new();
|
||||
|
||||
// Process extends clause (if any)
|
||||
// This is like a parameter, but:
|
||||
// 1. Uses the parent theory name as the qualifier (e.g., GeologMeta/Srt)
|
||||
// 2. Establishes an "is-a" relationship with transitive closure
|
||||
//
|
||||
// For transitive extends (A extends B extends C), we use "requalified" semantics:
|
||||
// - Sorts/funcs already qualified (from grandparents) keep their original qualifier
|
||||
// - Only unqualified items (parent's own) get the parent prefix
|
||||
// This gives A: { C/X, C/Y, B/Foo } rather than { B/C/X, B/C/Y, B/Foo }
|
||||
if let Some(ref parent_path) = theory.extends {
|
||||
let parent_name = parent_path.segments.join("/");
|
||||
if let Some(parent_theory) = env.theories.get(&parent_name) {
|
||||
// Record the extends relationship (including transitive parents)
|
||||
extends_chain.push(parent_name.clone());
|
||||
|
||||
// Helper: check if a name is already qualified from a grandparent
|
||||
// A name like "Grandparent/X" is grandparent-qualified if "Grandparent" is NOT
|
||||
// a sort in the parent theory (i.e., it's a theory name, not a naming convention).
|
||||
// Names like "Func/dom" where "Func" IS a sort use '/' as naming convention.
|
||||
let is_grandparent_qualified = |name: &str| -> bool {
|
||||
if let Some((prefix, _)) = name.split_once('/') {
|
||||
// If the prefix is a sort in parent, it's naming convention, not grandparent
|
||||
parent_theory.theory.signature.lookup_sort(prefix).is_none()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
// Helper: qualify a name - only prefix if not already qualified from grandparent
|
||||
let qualify = |name: &str| -> String {
|
||||
if is_grandparent_qualified(name) {
|
||||
// Already qualified from grandparent - keep as-is
|
||||
name.to_string()
|
||||
} else {
|
||||
// Parent's own item (possibly with naming convention '/') - add parent prefix
|
||||
format!("{}/{}", parent_name, name)
|
||||
}
|
||||
};
|
||||
|
||||
// Copy all sorts with requalified names
|
||||
for sort_name in &parent_theory.theory.signature.sorts {
|
||||
let qualified_name = qualify(sort_name);
|
||||
local_env.signature.add_sort(qualified_name);
|
||||
}
|
||||
|
||||
// Copy all functions with requalified names
|
||||
for func in &parent_theory.theory.signature.functions {
|
||||
let qualified_name = qualify(&func.name);
|
||||
// For domain/codomain remapping, always use parent_name because
|
||||
// the source signature uses the parent's namespace. The
|
||||
// preserve_existing_prefix flag handles grandparent-qualified sorts.
|
||||
let domain = remap_derived_sort(
|
||||
&func.domain,
|
||||
&parent_theory.theory.signature,
|
||||
&local_env.signature,
|
||||
&parent_name,
|
||||
true, // preserve_existing_prefix for extends
|
||||
);
|
||||
let codomain = remap_derived_sort(
|
||||
&func.codomain,
|
||||
&parent_theory.theory.signature,
|
||||
&local_env.signature,
|
||||
&parent_name,
|
||||
true, // preserve_existing_prefix for extends
|
||||
);
|
||||
local_env
|
||||
.signature
|
||||
.add_function(qualified_name, domain, codomain);
|
||||
}
|
||||
|
||||
// Copy all relations with requalified names
|
||||
for rel in &parent_theory.theory.signature.relations {
|
||||
let qualified_name = qualify(&rel.name);
|
||||
// Same as functions: always use parent_name for remapping
|
||||
let domain = remap_derived_sort(
|
||||
&rel.domain,
|
||||
&parent_theory.theory.signature,
|
||||
&local_env.signature,
|
||||
&parent_name,
|
||||
true, // preserve_existing_prefix for extends
|
||||
);
|
||||
local_env.signature.add_relation(qualified_name, domain);
|
||||
}
|
||||
|
||||
// Note: axioms are inherited but we don't copy them yet
|
||||
// (they reference the parent's sort/func IDs which need remapping)
|
||||
} else {
|
||||
return Err(ElabError::UnknownTheory(parent_name));
|
||||
}
|
||||
}
|
||||
|
||||
// Process parameters
|
||||
// When we have `theory (N : PetriNet instance) Trace { ... }`, we need to:
|
||||
// 1. Copy all sorts from PetriNet into local signature with qualified names (N/P, N/T, etc.)
|
||||
// 2. Copy all functions with qualified names (N/in/src, etc.)
|
||||
// This ensures all sort/func IDs are in a single namespace.
|
||||
let mut params = Vec::new();
|
||||
for param in &theory.params {
|
||||
// "T instance" parameters — the theory depends on an instance of another theory
|
||||
if param.ty.is_instance() {
|
||||
let inner = param.ty.instance_inner().unwrap();
|
||||
// Handle both simple (PetriNet instance) and parameterized (N ReachabilityProblem instance) cases
|
||||
let theory_name = extract_theory_name(&inner)?;
|
||||
if let Some(base_theory) = env.theories.get(&theory_name) {
|
||||
// Build mapping from base_theory's instance params to our type args
|
||||
// For `RP : N ReachabilityProblem instance`:
|
||||
// - collect_type_args returns ["N"] (all paths except the theory name)
|
||||
// - base_theory.params = [("N", "PetriNet")]
|
||||
// - mapping = {"N" -> "N"}
|
||||
let mut type_args = Vec::new();
|
||||
collect_type_args(&inner, &mut type_args);
|
||||
|
||||
// Build param substitution map: base_theory param name -> our type arg value
|
||||
let mut param_subst: HashMap<String, String> = HashMap::new();
|
||||
for (bp, arg) in base_theory.params.iter().zip(type_args.iter()) {
|
||||
if bp.theory_name != "Sort" {
|
||||
// Instance param - map its name to the type arg
|
||||
param_subst.insert(bp.name.clone(), arg.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Copy all sorts from param theory into local signature
|
||||
// But for sorts that come from a param that we're binding to an outer param,
|
||||
// reuse the outer param's sort instead of creating a duplicate.
|
||||
for sort_name in &base_theory.theory.signature.sorts {
|
||||
// Check if this sort starts with a param name that we're substituting
|
||||
let qualified_name = if let Some((prefix, suffix)) = sort_name.split_once('/') {
|
||||
if let Some(subst) = param_subst.get(prefix) {
|
||||
// This sort is from a param we're binding - use the substituted prefix
|
||||
let substituted_name = format!("{}/{}", subst, suffix);
|
||||
// If this sort already exists (from an outer param), don't add it again
|
||||
if local_env.signature.lookup_sort(&substituted_name).is_some() {
|
||||
continue;
|
||||
}
|
||||
substituted_name
|
||||
} else {
|
||||
// Not from a substituted param - prefix with our param name
|
||||
format!("{}/{}", param.name, sort_name)
|
||||
}
|
||||
} else {
|
||||
// Unqualified sort (the theory's own sort) - prefix with our param name
|
||||
format!("{}/{}", param.name, sort_name)
|
||||
};
|
||||
local_env.signature.add_sort(qualified_name);
|
||||
}
|
||||
|
||||
// Copy all functions from param theory with qualified names
|
||||
for func in &base_theory.theory.signature.functions {
|
||||
// Check if this function starts with a param name that we're substituting
|
||||
let qualified_name = if let Some((prefix, suffix)) = func.name.split_once('/') {
|
||||
if let Some(subst) = param_subst.get(prefix) {
|
||||
// This func is from a param we're binding - use the substituted prefix
|
||||
let substituted_name = format!("{}/{}", subst, suffix);
|
||||
// If this function already exists (from an outer param), don't add it again
|
||||
if local_env.signature.lookup_func(&substituted_name).is_some() {
|
||||
continue;
|
||||
}
|
||||
substituted_name
|
||||
} else {
|
||||
// Not from a substituted param - prefix with our param name
|
||||
format!("{}/{}", param.name, func.name)
|
||||
}
|
||||
} else {
|
||||
// Unqualified func - prefix with our param name
|
||||
format!("{}/{}", param.name, func.name)
|
||||
};
|
||||
// Remap domain and codomain to use local signature's sort IDs
|
||||
// We need to handle substitution for sorts too
|
||||
let domain = remap_derived_sort_with_subst(
|
||||
&func.domain,
|
||||
&base_theory.theory.signature,
|
||||
&local_env.signature,
|
||||
¶m.name,
|
||||
¶m_subst,
|
||||
);
|
||||
let codomain = remap_derived_sort_with_subst(
|
||||
&func.codomain,
|
||||
&base_theory.theory.signature,
|
||||
&local_env.signature,
|
||||
¶m.name,
|
||||
¶m_subst,
|
||||
);
|
||||
local_env
|
||||
.signature
|
||||
.add_function(qualified_name, domain, codomain);
|
||||
}
|
||||
|
||||
// Copy all relations from param theory with qualified names
|
||||
for rel in &base_theory.theory.signature.relations {
|
||||
// Check if this relation starts with a param name that we're substituting
|
||||
let qualified_name = if let Some((prefix, suffix)) = rel.name.split_once('/') {
|
||||
if let Some(subst) = param_subst.get(prefix) {
|
||||
let substituted_name = format!("{}/{}", subst, suffix);
|
||||
if local_env.signature.lookup_rel(&substituted_name).is_some() {
|
||||
continue;
|
||||
}
|
||||
substituted_name
|
||||
} else {
|
||||
format!("{}/{}", param.name, rel.name)
|
||||
}
|
||||
} else {
|
||||
format!("{}/{}", param.name, rel.name)
|
||||
};
|
||||
let domain = remap_derived_sort_with_subst(
|
||||
&rel.domain,
|
||||
&base_theory.theory.signature,
|
||||
&local_env.signature,
|
||||
¶m.name,
|
||||
¶m_subst,
|
||||
);
|
||||
local_env.signature.add_relation(qualified_name, domain);
|
||||
}
|
||||
|
||||
// NOTE: Instance field content (sorts/functions) is already included in
|
||||
// base_theory.theory.signature because it was added when that theory
|
||||
// was elaborated. We don't need to process instance fields again here.
|
||||
|
||||
params.push(TheoryParam {
|
||||
name: param.name.clone(),
|
||||
theory_name: theory_name.clone(),
|
||||
});
|
||||
local_env
|
||||
.params
|
||||
.push((param.name.clone(), base_theory.clone()));
|
||||
} else {
|
||||
return Err(ElabError::UnknownTheory(theory_name));
|
||||
}
|
||||
} else if param.ty.is_sort() {
|
||||
// "Sort" parameters — the theory is parameterized over a sort
|
||||
// Add the parameter as a sort in the local signature
|
||||
local_env.signature.add_sort(param.name.clone());
|
||||
// Also record it as a "sort parameter" for the theory
|
||||
params.push(TheoryParam {
|
||||
name: param.name.clone(),
|
||||
theory_name: "Sort".to_string(), // Special marker
|
||||
});
|
||||
} else {
|
||||
return Err(ElabError::UnsupportedFeature(format!(
|
||||
"parameter type {:?}",
|
||||
param.ty
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
// First pass: collect all sorts
|
||||
for item in &theory.body {
|
||||
if let ast::TheoryItem::Sort(name) = &item.node {
|
||||
local_env.signature.add_sort(name.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: collect all functions and relations
|
||||
for item in &theory.body {
|
||||
match &item.node {
|
||||
ast::TheoryItem::Function(f) => {
|
||||
// Check if codomain is Prop — if so, this is a relation declaration
|
||||
if f.codomain.is_prop() {
|
||||
let domain = elaborate_type(&local_env, &f.domain)?;
|
||||
local_env
|
||||
.signature
|
||||
.add_relation(f.name.to_string(), domain);
|
||||
} else {
|
||||
let domain = elaborate_type(&local_env, &f.domain)?;
|
||||
let codomain = elaborate_type(&local_env, &f.codomain)?;
|
||||
local_env
|
||||
.signature
|
||||
.add_function(f.name.to_string(), domain, codomain);
|
||||
}
|
||||
}
|
||||
// Legacy: A Field with a Record type is a relation declaration
|
||||
// (kept for backwards compatibility, may remove later)
|
||||
ast::TheoryItem::Field(name, ty) if ty.as_record().is_some() => {
|
||||
let domain = elaborate_type(&local_env, ty)?;
|
||||
local_env.signature.add_relation(name.clone(), domain);
|
||||
}
|
||||
// Instance-typed field declarations (nested instances)
|
||||
// e.g., `initial_marking : N Marking instance;`
|
||||
ast::TheoryItem::Field(name, ty) if ty.is_instance() => {
|
||||
let inner = ty.instance_inner().unwrap();
|
||||
// Store the theory type expression as a string
|
||||
let theory_type_str = format_type_expr(&inner);
|
||||
local_env
|
||||
.signature
|
||||
.add_instance_field(name.clone(), theory_type_str.clone());
|
||||
|
||||
// Also add the content (sorts, functions) from the field's theory
|
||||
// This enables accessing things like iso/fwd when we have `iso : X Y Iso instance`
|
||||
if let Ok(field_theory_name) = extract_theory_name(&inner)
|
||||
&& let Some(field_theory) = env.theories.get(&field_theory_name) {
|
||||
let field_prefix = name.clone();
|
||||
|
||||
// Build a mapping from source sort names to target sort names
|
||||
// - Sort parameters get substituted from type expression args
|
||||
// - Instance param sorts (e.g., "N/P") map to local sorts with same name
|
||||
// - Local sorts (e.g., "Token") get prefixed with field name
|
||||
let sort_param_map = collect_sort_params(&inner, field_theory);
|
||||
|
||||
// First, add any non-param sorts from the field's theory with prefix
|
||||
for sort_name in &field_theory.theory.signature.sorts {
|
||||
// Skip sorts that came from instance params (already qualified)
|
||||
if sort_name.contains('/') {
|
||||
continue;
|
||||
}
|
||||
// Skip Sort parameters (will be substituted)
|
||||
let is_sort_param = field_theory
|
||||
.params
|
||||
.iter()
|
||||
.any(|p| p.theory_name == "Sort" && p.name == *sort_name);
|
||||
if is_sort_param {
|
||||
continue;
|
||||
}
|
||||
// Add as prefixed sort
|
||||
let qualified_name = format!("{}/{}", field_prefix, sort_name);
|
||||
local_env.signature.add_sort(qualified_name);
|
||||
}
|
||||
|
||||
// Add functions from the field's theory
|
||||
for func in &field_theory.theory.signature.functions {
|
||||
// Skip functions that came from instance params (prefix matches param name)
|
||||
// But keep naming-convention functions like "input_terminal/of"
|
||||
let is_from_param = if let Some(prefix) = func.name.split('/').next() {
|
||||
field_theory.params.iter().any(|p| p.name == prefix)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
if is_from_param {
|
||||
continue;
|
||||
}
|
||||
let qualified_name = format!("{}/{}", field_prefix, func.name);
|
||||
let domain = remap_for_instance_field(
|
||||
&func.domain,
|
||||
&field_theory.theory.signature,
|
||||
&local_env.signature,
|
||||
&sort_param_map,
|
||||
&field_prefix,
|
||||
);
|
||||
let codomain = remap_for_instance_field(
|
||||
&func.codomain,
|
||||
&field_theory.theory.signature,
|
||||
&local_env.signature,
|
||||
&sort_param_map,
|
||||
&field_prefix,
|
||||
);
|
||||
if let (Some(d), Some(c)) = (domain, codomain) {
|
||||
local_env.signature.add_function(qualified_name, d, c);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Third pass: elaborate axioms
|
||||
let mut axioms = Vec::new();
|
||||
let mut axiom_names = Vec::new();
|
||||
for item in &theory.body {
|
||||
if let ast::TheoryItem::Axiom(ax) = &item.node {
|
||||
// Build context from quantified variables
|
||||
let mut ctx = Context::new();
|
||||
for qv in &ax.quantified {
|
||||
let sort = elaborate_type(&local_env, &qv.ty)?;
|
||||
for name in &qv.names {
|
||||
ctx = ctx.extend(name.clone(), sort.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Elaborate hypothesis (conjunction of all hypotheses)
|
||||
let premise = if ax.hypotheses.is_empty() {
|
||||
Formula::True
|
||||
} else {
|
||||
let hyps: Result<Vec<_>, _> = ax
|
||||
.hypotheses
|
||||
.iter()
|
||||
.map(|h| elaborate_formula(&local_env, &ctx, h))
|
||||
.collect();
|
||||
Formula::Conj(hyps?)
|
||||
};
|
||||
|
||||
// Elaborate conclusion
|
||||
let conclusion = elaborate_formula(&local_env, &ctx, &ax.conclusion)?;
|
||||
|
||||
// Collect axiom name (e.g., "ax/input_complete")
|
||||
axiom_names.push(ax.name.to_string());
|
||||
|
||||
axioms.push(Sequent {
|
||||
context: ctx,
|
||||
premise,
|
||||
conclusion,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ElaboratedTheory {
|
||||
params,
|
||||
theory: Theory {
|
||||
name: theory.name.clone(),
|
||||
signature: local_env.signature,
|
||||
axioms,
|
||||
axiom_names,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/// Remap a DerivedSort for an instance-typed field in a theory body.
|
||||
/// Handles both Sort parameters (substituted from type args) and instance param sorts.
|
||||
fn remap_for_instance_field(
|
||||
sort: &DerivedSort,
|
||||
source_sig: &Signature,
|
||||
target_sig: &Signature,
|
||||
sort_param_map: &HashMap<String, String>,
|
||||
field_prefix: &str,
|
||||
) -> Option<DerivedSort> {
|
||||
match sort {
|
||||
DerivedSort::Base(source_id) => {
|
||||
let sort_name = &source_sig.sorts[*source_id];
|
||||
|
||||
// Check Sort parameter substitution (e.g., X -> RP/initial/Token)
|
||||
if let Some(replacement) = sort_param_map.get(sort_name)
|
||||
&& let Some(target_id) = target_sig.lookup_sort(replacement) {
|
||||
return Some(DerivedSort::Base(target_id));
|
||||
}
|
||||
|
||||
// Check if it's an instance param sort (already qualified, e.g., N/P)
|
||||
if sort_name.contains('/')
|
||||
&& let Some(target_id) = target_sig.lookup_sort(sort_name) {
|
||||
return Some(DerivedSort::Base(target_id));
|
||||
}
|
||||
|
||||
// Check if it's a local sort (needs prefix, e.g., Token -> initial/Token)
|
||||
let prefixed = format!("{}/{}", field_prefix, sort_name);
|
||||
if let Some(target_id) = target_sig.lookup_sort(&prefixed) {
|
||||
return Some(DerivedSort::Base(target_id));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
DerivedSort::Product(fields) => {
|
||||
let remapped: Option<Vec<_>> = fields
|
||||
.iter()
|
||||
.map(|(n, s)| {
|
||||
remap_for_instance_field(s, source_sig, target_sig, sort_param_map, field_prefix)
|
||||
.map(|r| (n.clone(), r))
|
||||
})
|
||||
.collect();
|
||||
remapped.map(DerivedSort::Product)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Collect sort parameter mappings from a type expression.
|
||||
/// E.g., `RP/initial/Token RP/target/Token Iso` returns {"X" -> "RP/initial/Token", "Y" -> "RP/target/Token"}
|
||||
fn collect_sort_params(
|
||||
ty: &ast::TypeExpr,
|
||||
field_theory: &std::rc::Rc<ElaboratedTheory>,
|
||||
) -> HashMap<String, String> {
|
||||
let mut args = Vec::new();
|
||||
collect_type_args(ty, &mut args);
|
||||
|
||||
// Match args with sort parameters in order
|
||||
let mut map = HashMap::new();
|
||||
for (param, arg) in field_theory.params.iter().zip(args.iter()) {
|
||||
if param.theory_name == "Sort" {
|
||||
map.insert(param.name.clone(), arg.clone());
|
||||
}
|
||||
}
|
||||
map
|
||||
}
|
||||
|
||||
/// Recursively collect type arguments from an App chain.
|
||||
/// For `A B C Foo`, this returns ["A", "B", "C"] (Foo is the theory name).
|
||||
///
|
||||
/// With concatenative parsing, tokens are in order: [arg1, arg2, ..., theory_name]
|
||||
/// All path tokens except the last one are type arguments.
|
||||
pub fn collect_type_args(ty: &ast::TypeExpr, args: &mut Vec<String>) {
|
||||
use crate::ast::TypeToken;
|
||||
|
||||
// Collect all path tokens
|
||||
let paths: Vec<String> = ty
|
||||
.tokens
|
||||
.iter()
|
||||
.filter_map(|t| match t {
|
||||
TypeToken::Path(p) => Some(p.to_string()),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
// All but the last one are type arguments
|
||||
if paths.len() > 1 {
|
||||
args.extend(paths[..paths.len() - 1].iter().cloned());
|
||||
}
|
||||
}
|
||||
|
||||
/// Substitute sort parameters in a DerivedSort using a mapping.
|
||||
/// Returns None if the sort cannot be resolved in the target signature.
|
||||
#[allow(dead_code)]
|
||||
fn substitute_sort_params(
|
||||
sort: &DerivedSort,
|
||||
source_sig: &Signature,
|
||||
target_sig: &Signature,
|
||||
param_map: &HashMap<String, String>,
|
||||
) -> Option<DerivedSort> {
|
||||
match sort {
|
||||
DerivedSort::Base(source_id) => {
|
||||
let sort_name = &source_sig.sorts[*source_id];
|
||||
// Check if this sort is a parameter that should be substituted
|
||||
if let Some(replacement) = param_map.get(sort_name) {
|
||||
// Look up the replacement sort in the target signature
|
||||
if let Some(target_id) = target_sig.lookup_sort(replacement) {
|
||||
return Some(DerivedSort::Base(target_id));
|
||||
}
|
||||
// Couldn't find the replacement - this is an error case
|
||||
eprintln!(
|
||||
"Warning: sort param substitution failed for {} -> {}",
|
||||
sort_name, replacement
|
||||
);
|
||||
return None;
|
||||
}
|
||||
// Not a parameter - try to find in target as-is
|
||||
target_sig.lookup_sort(sort_name).map(DerivedSort::Base)
|
||||
}
|
||||
DerivedSort::Product(fields) => {
|
||||
let remapped_fields: Option<Vec<_>> = fields
|
||||
.iter()
|
||||
.map(|(name, s)| {
|
||||
substitute_sort_params(s, source_sig, target_sig, param_map)
|
||||
.map(|remapped| (name.clone(), remapped))
|
||||
})
|
||||
.collect();
|
||||
remapped_fields.map(DerivedSort::Product)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Remap a DerivedSort with instance parameter substitution.
|
||||
/// For sorts like "N/P" where N is being substituted for an outer param,
|
||||
/// we look up the substituted name instead of prefixing.
|
||||
fn remap_derived_sort_with_subst(
|
||||
sort: &DerivedSort,
|
||||
source_sig: &Signature,
|
||||
target_sig: &Signature,
|
||||
param_name: &str,
|
||||
param_subst: &HashMap<String, String>,
|
||||
) -> DerivedSort {
|
||||
match sort {
|
||||
DerivedSort::Base(source_id) => {
|
||||
let sort_name = &source_sig.sorts[*source_id];
|
||||
|
||||
// Check if this sort starts with a param name that we're substituting
|
||||
if let Some((prefix, suffix)) = sort_name.split_once('/')
|
||||
&& let Some(subst) = param_subst.get(prefix) {
|
||||
// Substitute the prefix
|
||||
let substituted_name = format!("{}/{}", subst, suffix);
|
||||
if let Some(target_id) = target_sig.lookup_sort(&substituted_name) {
|
||||
return DerivedSort::Base(target_id);
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, use the default prefixing behavior
|
||||
let qualified_name = format!("{}/{}", param_name, sort_name);
|
||||
if let Some(target_id) = target_sig.lookup_sort(&qualified_name) {
|
||||
DerivedSort::Base(target_id)
|
||||
} else if let Some(target_id) = target_sig.lookup_sort(sort_name) {
|
||||
// Fallback: try without prefix (for sorts that weren't duplicated)
|
||||
DerivedSort::Base(target_id)
|
||||
} else {
|
||||
panic!(
|
||||
"remap_derived_sort_with_subst: could not find sort {} or {}",
|
||||
qualified_name, sort_name
|
||||
);
|
||||
}
|
||||
}
|
||||
DerivedSort::Product(fields) => {
|
||||
let remapped_fields: Vec<_> = fields
|
||||
.iter()
|
||||
.map(|(name, s)| {
|
||||
(
|
||||
name.clone(),
|
||||
remap_derived_sort_with_subst(
|
||||
s,
|
||||
source_sig,
|
||||
target_sig,
|
||||
param_name,
|
||||
param_subst,
|
||||
),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
DerivedSort::Product(remapped_fields)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract the base theory name from a type expression.
|
||||
///
|
||||
/// With concatenative parsing, tokens are in order: [arg1, arg2, ..., theory_name]
|
||||
/// The last path token is the theory name.
|
||||
fn extract_theory_name(ty: &ast::TypeExpr) -> ElabResult<String> {
|
||||
use crate::ast::TypeToken;
|
||||
|
||||
// Find the last path token - that's the theory name
|
||||
for token in ty.tokens.iter().rev() {
|
||||
if let TypeToken::Path(path) = token {
|
||||
return Ok(path.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Err(ElabError::TypeExprError(format!(
|
||||
"cannot extract theory name from {:?}",
|
||||
ty
|
||||
)))
|
||||
}
|
||||
|
||||
/// Collect type arguments from a theory type string like "ExampleNet ReachabilityProblem".
|
||||
/// Returns the arguments (everything except the final theory name).
|
||||
pub fn collect_type_args_from_theory_type(theory_type: &str) -> Vec<String> {
|
||||
let tokens: Vec<&str> = theory_type.split_whitespace().collect();
|
||||
if tokens.len() <= 1 {
|
||||
vec![]
|
||||
} else {
|
||||
// All but the last token are arguments
|
||||
tokens[..tokens.len()-1].iter().map(|s| s.to_string()).collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a parameter substitution map for importing elements from a parameterized instance.
|
||||
///
|
||||
/// Given a param instance with a certain theory type (e.g., "ExampleNet ReachabilityProblem"),
|
||||
/// this builds a mapping from that theory's param names to the actual bindings.
|
||||
///
|
||||
/// For example, if:
|
||||
/// - `param_theory_type` = "ExampleNet ReachabilityProblem"
|
||||
/// - ReachabilityProblem has param `(N : PetriNet instance)`
|
||||
/// - The type args are ["ExampleNet"]
|
||||
///
|
||||
/// Returns: {"N" -> "ExampleNet"}
|
||||
pub fn build_param_subst(
|
||||
param_theory: &ElaboratedTheory,
|
||||
type_args: &[String],
|
||||
) -> HashMap<String, String> {
|
||||
let mut param_subst = HashMap::new();
|
||||
for (bp, arg) in param_theory.params.iter().zip(type_args.iter()) {
|
||||
if bp.theory_name != "Sort" {
|
||||
// Instance param - map its name to the type arg
|
||||
param_subst.insert(bp.name.clone(), arg.clone());
|
||||
}
|
||||
}
|
||||
param_subst
|
||||
}
|
||||
|
||||
/// Remap a sort name from a param instance to the local theory's sort namespace.
|
||||
///
|
||||
/// This handles the case where a param instance has sorts from its own params,
|
||||
/// and we need to figure out which local sorts they correspond to.
|
||||
///
|
||||
/// For example, when importing from `problem0` (an `ExampleNet ReachabilityProblem`)
|
||||
/// into `solution0` (an `ExampleNet problem0 Solution`):
|
||||
/// - problem0 has sort "N/P" where N = ExampleNet
|
||||
/// - solution0 has sort "N/P" where N = ExampleNet (from outer param)
|
||||
/// - So "N/P" from problem0 maps to "N/P" in solution0 (not "RP/N/P")
|
||||
///
|
||||
/// Arguments:
|
||||
/// - `sort_name`: The sort name in the param instance's signature (e.g., "N/P")
|
||||
/// - `param_name`: The local param name (e.g., "RP")
|
||||
/// - `param_subst`: Mapping from param instance's param names to their bindings (e.g., {"N" -> "ExampleNet"})
|
||||
/// - `local_arguments`: The local instance's param bindings (e.g., [("N", "ExampleNet"), ("RP", "problem0")])
|
||||
///
|
||||
/// Returns the sort name to use in the local signature.
|
||||
pub fn remap_sort_for_param_import(
|
||||
sort_name: &str,
|
||||
param_name: &str,
|
||||
param_subst: &HashMap<String, String>,
|
||||
local_arguments: &[(String, String)],
|
||||
) -> String {
|
||||
// Check if this sort starts with a param name that we're substituting
|
||||
if let Some((prefix, suffix)) = sort_name.split_once('/')
|
||||
&& let Some(bound_instance) = param_subst.get(prefix) {
|
||||
// This sort is from a param in the param instance.
|
||||
// Find which local param is bound to the same instance.
|
||||
for (local_param_name, local_instance) in local_arguments {
|
||||
if local_instance == bound_instance {
|
||||
// Found it! Use the local param's prefix instead.
|
||||
return format!("{}/{}", local_param_name, suffix);
|
||||
}
|
||||
}
|
||||
// Fallback: the instance isn't directly a local param,
|
||||
// just use param_name prefix
|
||||
return format!("{}/{}", param_name, sort_name);
|
||||
}
|
||||
|
||||
// Unqualified sort or no substitution applicable - prefix with param_name
|
||||
format!("{}/{}", param_name, sort_name)
|
||||
}
|
||||
|
||||
/// Format a type expression as a string (for storing instance field types)
|
||||
fn format_type_expr(ty: &ast::TypeExpr) -> String {
|
||||
use crate::ast::TypeToken;
|
||||
|
||||
let mut parts = Vec::new();
|
||||
|
||||
for token in &ty.tokens {
|
||||
match token {
|
||||
TypeToken::Path(path) => parts.push(path.to_string()),
|
||||
TypeToken::Sort => parts.push("Sort".to_string()),
|
||||
TypeToken::Prop => parts.push("Prop".to_string()),
|
||||
TypeToken::Instance => parts.push("instance".to_string()),
|
||||
TypeToken::Arrow => parts.push("->".to_string()),
|
||||
TypeToken::Record(fields) => {
|
||||
let field_strs: Vec<String> = fields
|
||||
.iter()
|
||||
.map(|(name, field_ty)| format!("{}: {}", name, format_type_expr(field_ty)))
|
||||
.collect();
|
||||
parts.push(format!("[{}]", field_strs.join(", ")));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parts.join(" ")
|
||||
}
|
||||
265
src/elaborate/types.rs
Normal file
265
src/elaborate/types.rs
Normal file
@ -0,0 +1,265 @@
|
||||
//! Type expression evaluation (concatenative stack-based)
|
||||
//!
|
||||
//! Evaluates flat TypeExpr token sequences into resolved types,
|
||||
//! using the symbol table to determine theory arities.
|
||||
|
||||
use crate::ast::{Path, TypeExpr, TypeToken};
|
||||
use crate::core::DerivedSort;
|
||||
use crate::elaborate::error::{ElabError, ElabResult};
|
||||
use crate::elaborate::Env;
|
||||
|
||||
/// A value on the type evaluation stack
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum TypeValue {
|
||||
/// The Sort kind (for parameter declarations like `X : Sort`)
|
||||
SortKind,
|
||||
|
||||
/// The Prop kind (for relation codomains)
|
||||
PropKind,
|
||||
|
||||
/// A resolved base sort (index into signature)
|
||||
Sort(DerivedSort),
|
||||
|
||||
/// An unresolved path (instance ref, sort path, or theory name)
|
||||
/// Will be resolved based on context
|
||||
Path(Path),
|
||||
|
||||
/// A theory applied to arguments
|
||||
AppliedTheory {
|
||||
theory_name: String,
|
||||
args: Vec<TypeValue>,
|
||||
},
|
||||
|
||||
/// Instance type: wraps another type value
|
||||
Instance(Box<TypeValue>),
|
||||
|
||||
/// Function/arrow type
|
||||
Arrow {
|
||||
domain: Box<TypeValue>,
|
||||
codomain: Box<TypeValue>,
|
||||
},
|
||||
|
||||
/// Record/product type
|
||||
Record(Vec<(String, TypeValue)>),
|
||||
}
|
||||
|
||||
impl TypeValue {
|
||||
/// Try to convert this type value to a DerivedSort
|
||||
pub fn as_derived_sort(&self, env: &Env) -> ElabResult<DerivedSort> {
|
||||
match self {
|
||||
TypeValue::Sort(s) => Ok(s.clone()),
|
||||
|
||||
TypeValue::Path(path) => {
|
||||
// Try to resolve as a sort path
|
||||
env.resolve_sort_path(path)
|
||||
}
|
||||
|
||||
TypeValue::Record(fields) => {
|
||||
let resolved: Result<Vec<_>, _> = fields
|
||||
.iter()
|
||||
.map(|(name, val)| val.as_derived_sort(env).map(|s| (name.clone(), s)))
|
||||
.collect();
|
||||
Ok(DerivedSort::Product(resolved?))
|
||||
}
|
||||
|
||||
TypeValue::SortKind => Err(ElabError::NotASort(
|
||||
"Sort is a kind, not a type".to_string(),
|
||||
)),
|
||||
|
||||
TypeValue::PropKind => Err(ElabError::NotASort(
|
||||
"Prop is a kind, not a type".to_string(),
|
||||
)),
|
||||
|
||||
TypeValue::AppliedTheory { theory_name, .. } => Err(ElabError::NotASort(format!(
|
||||
"applied theory '{}' is not a sort",
|
||||
theory_name
|
||||
))),
|
||||
|
||||
TypeValue::Instance(_) => Err(ElabError::NotASort(
|
||||
"instance type is not a sort".to_string(),
|
||||
)),
|
||||
|
||||
TypeValue::Arrow { .. } => Err(ElabError::NotASort(
|
||||
"arrow type is not a sort".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if this is the Sort kind
|
||||
pub fn is_sort_kind(&self) -> bool {
|
||||
matches!(self, TypeValue::SortKind)
|
||||
}
|
||||
|
||||
/// Check if this is an instance type
|
||||
pub fn is_instance(&self) -> bool {
|
||||
matches!(self, TypeValue::Instance(_))
|
||||
}
|
||||
|
||||
/// Get the inner type if this is an instance type
|
||||
pub fn instance_inner(&self) -> Option<&TypeValue> {
|
||||
match self {
|
||||
TypeValue::Instance(inner) => Some(inner),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the theory name and args if this is an applied theory
|
||||
pub fn as_applied_theory(&self) -> Option<(&str, &[TypeValue])> {
|
||||
match self {
|
||||
TypeValue::AppliedTheory { theory_name, args } => Some((theory_name, args)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Evaluate a type expression using the environment
|
||||
///
|
||||
/// This is the core stack-based evaluator. It processes tokens left-to-right,
|
||||
/// using the symbol table to determine theory arities.
|
||||
pub fn eval_type_expr(expr: &TypeExpr, env: &Env) -> ElabResult<TypeValue> {
|
||||
let mut stack: Vec<TypeValue> = Vec::new();
|
||||
|
||||
for token in &expr.tokens {
|
||||
match token {
|
||||
TypeToken::Sort => {
|
||||
stack.push(TypeValue::SortKind);
|
||||
}
|
||||
|
||||
TypeToken::Prop => {
|
||||
stack.push(TypeValue::PropKind);
|
||||
}
|
||||
|
||||
TypeToken::Path(path) => {
|
||||
// Check if this is a theory name with known arity
|
||||
let path_str = path.to_string();
|
||||
|
||||
if let Some(theory) = env.theories.get(&path_str) {
|
||||
let arity = theory.params.len();
|
||||
if arity > 0 {
|
||||
// Theory takes arguments - pop them from stack
|
||||
if stack.len() < arity {
|
||||
return Err(ElabError::NotEnoughArgs {
|
||||
name: path_str,
|
||||
expected: arity,
|
||||
got: stack.len(),
|
||||
});
|
||||
}
|
||||
let args = stack.split_off(stack.len() - arity);
|
||||
stack.push(TypeValue::AppliedTheory {
|
||||
theory_name: path_str,
|
||||
args,
|
||||
});
|
||||
} else {
|
||||
// Zero-arity theory
|
||||
stack.push(TypeValue::AppliedTheory {
|
||||
theory_name: path_str,
|
||||
args: vec![],
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Not a theory - could be a sort path or instance reference
|
||||
// Push as unresolved path
|
||||
stack.push(TypeValue::Path(path.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
TypeToken::Instance => {
|
||||
let top = stack.pop().ok_or_else(|| {
|
||||
ElabError::TypeExprError("'instance' with empty stack".to_string())
|
||||
})?;
|
||||
stack.push(TypeValue::Instance(Box::new(top)));
|
||||
}
|
||||
|
||||
TypeToken::Arrow => {
|
||||
// Pop codomain first (right-associative)
|
||||
let codomain = stack.pop().ok_or_else(|| {
|
||||
ElabError::TypeExprError("'->' missing codomain".to_string())
|
||||
})?;
|
||||
let domain = stack.pop().ok_or_else(|| {
|
||||
ElabError::TypeExprError("'->' missing domain".to_string())
|
||||
})?;
|
||||
stack.push(TypeValue::Arrow {
|
||||
domain: Box::new(domain),
|
||||
codomain: Box::new(codomain),
|
||||
});
|
||||
}
|
||||
|
||||
TypeToken::Record(fields) => {
|
||||
// Evaluate each field's type expression recursively
|
||||
let mut resolved_fields = Vec::new();
|
||||
for (name, field_expr) in fields {
|
||||
let field_val = eval_type_expr(field_expr, env)?;
|
||||
resolved_fields.push((name.clone(), field_val));
|
||||
}
|
||||
stack.push(TypeValue::Record(resolved_fields));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stack should have exactly one element
|
||||
if stack.is_empty() {
|
||||
return Err(ElabError::TypeExprError("empty type expression".to_string()));
|
||||
}
|
||||
if stack.len() > 1 {
|
||||
return Err(ElabError::TypeExprError(format!(
|
||||
"type expression left {} values on stack (expected 1)",
|
||||
stack.len()
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(stack.pop().unwrap())
|
||||
}
|
||||
|
||||
/// Convenience: evaluate a type expression and convert to DerivedSort
|
||||
pub fn eval_as_sort(expr: &TypeExpr, env: &Env) -> ElabResult<DerivedSort> {
|
||||
let val = eval_type_expr(expr, env)?;
|
||||
val.as_derived_sort(env)
|
||||
}
|
||||
|
||||
/// Extract the theory name from a type expression (for simple cases)
|
||||
///
|
||||
/// This is used when we just need the theory name without full evaluation.
|
||||
/// Returns None if the expression is more complex than a simple path or applied theory.
|
||||
pub fn extract_theory_name(expr: &TypeExpr) -> Option<String> {
|
||||
// Look for the last path token that isn't followed by Instance
|
||||
let mut last_theory_candidate: Option<&Path> = None;
|
||||
|
||||
for token in &expr.tokens {
|
||||
match token {
|
||||
TypeToken::Path(p) => {
|
||||
last_theory_candidate = Some(p);
|
||||
}
|
||||
TypeToken::Instance => {
|
||||
// The previous path was the theory name
|
||||
if let Some(p) = last_theory_candidate {
|
||||
return Some(p.to_string());
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// If no Instance token, the last path is the theory name
|
||||
last_theory_candidate.map(|p| p.to_string())
|
||||
}
|
||||
|
||||
/// Check if a type expression represents the Sort kind
|
||||
pub fn is_sort_kind(expr: &TypeExpr) -> bool {
|
||||
expr.tokens.len() == 1 && matches!(expr.tokens[0], TypeToken::Sort)
|
||||
}
|
||||
|
||||
/// Check if a type expression ends with `instance`
|
||||
pub fn is_instance_type(expr: &TypeExpr) -> bool {
|
||||
expr.tokens.last() == Some(&TypeToken::Instance)
|
||||
}
|
||||
|
||||
/// Get all path tokens from a type expression (useful for parameter extraction)
|
||||
pub fn get_paths(expr: &TypeExpr) -> Vec<&Path> {
|
||||
expr.tokens
|
||||
.iter()
|
||||
.filter_map(|t| match t {
|
||||
TypeToken::Path(p) => Some(p),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
211
src/error.rs
Normal file
211
src/error.rs
Normal file
@ -0,0 +1,211 @@
|
||||
//! Error formatting for Geolog
|
||||
//!
|
||||
//! Provides user-friendly error messages using ariadne for nice formatting.
|
||||
|
||||
use ariadne::{Color, Label, Report, ReportKind, Source};
|
||||
use chumsky::prelude::Simple;
|
||||
use std::ops::Range;
|
||||
|
||||
use crate::lexer::Token;
|
||||
|
||||
/// Format lexer errors into a user-friendly string
|
||||
pub fn format_lexer_errors(source: &str, errors: Vec<Simple<char>>) -> String {
|
||||
let mut output = Vec::new();
|
||||
|
||||
for error in errors {
|
||||
let span = error.span();
|
||||
let report = Report::build(ReportKind::Error, (), span.start)
|
||||
.with_message("Lexical error")
|
||||
.with_label(
|
||||
Label::new(span.clone())
|
||||
.with_message(format_lexer_error(&error))
|
||||
.with_color(Color::Red),
|
||||
);
|
||||
|
||||
report
|
||||
.finish()
|
||||
.write(Source::from(source), &mut output)
|
||||
.expect("Failed to write error report");
|
||||
}
|
||||
|
||||
String::from_utf8(output).unwrap_or_else(|_| "Error formatting failed".to_string())
|
||||
}
|
||||
|
||||
/// Format a single lexer error into a readable message
|
||||
fn format_lexer_error(error: &Simple<char>) -> String {
|
||||
let found = error
|
||||
.found()
|
||||
.map(|c| format!("'{}'", c))
|
||||
.unwrap_or_else(|| "end of input".to_string());
|
||||
|
||||
if let Some(_expected) = error.expected().next() {
|
||||
format!(
|
||||
"Unexpected {}, expected {}",
|
||||
found,
|
||||
format_char_set(error.expected())
|
||||
)
|
||||
} else {
|
||||
format!("Unexpected character {}", found)
|
||||
}
|
||||
}
|
||||
|
||||
/// Format parser errors into a user-friendly string
|
||||
pub fn format_parser_errors(
|
||||
source: &str,
|
||||
errors: Vec<Simple<Token>>,
|
||||
token_spans: &[(Token, Range<usize>)],
|
||||
) -> String {
|
||||
let mut output = Vec::new();
|
||||
|
||||
for error in errors {
|
||||
let span = error.span();
|
||||
|
||||
// Map token span to character span
|
||||
// The span could be either:
|
||||
// 1. A token index (0, 1, 2, ..., n-1 for n tokens) - look up in token_spans
|
||||
// 2. Already a character position (from custom errors that captured spans)
|
||||
//
|
||||
// Best heuristic: check if the span matches a token's character range.
|
||||
// If so, it's a character position. Otherwise, treat as token index.
|
||||
let is_char_position = token_spans
|
||||
.iter()
|
||||
.any(|(_, char_range)| char_range.start == span.start && char_range.end == span.end);
|
||||
|
||||
let char_span = if is_char_position {
|
||||
// Span exactly matches a token's character range - use as-is
|
||||
span.clone()
|
||||
} else if span.start < token_spans.len() {
|
||||
// Span.start is a valid token index - use token's character range
|
||||
token_spans[span.start].1.clone()
|
||||
} else if span.start == token_spans.len() {
|
||||
// End of input marker - use the end of the last token
|
||||
if let Some((_, last_range)) = token_spans.last() {
|
||||
last_range.end..last_range.end
|
||||
} else {
|
||||
0..0
|
||||
}
|
||||
} else {
|
||||
// Fallback: treat as character position
|
||||
let start = span.start.min(source.len());
|
||||
let end = span.end.min(source.len());
|
||||
start..end
|
||||
};
|
||||
|
||||
let report = Report::build(ReportKind::Error, (), char_span.start)
|
||||
.with_message("Parse error")
|
||||
.with_label(
|
||||
Label::new(char_span.clone())
|
||||
.with_message(format_parser_error(&error))
|
||||
.with_color(Color::Red),
|
||||
);
|
||||
|
||||
report
|
||||
.finish()
|
||||
.write(Source::from(source), &mut output)
|
||||
.expect("Failed to write error report");
|
||||
}
|
||||
|
||||
String::from_utf8(output).unwrap_or_else(|_| "Error formatting failed".to_string())
|
||||
}
|
||||
|
||||
/// Format a single parser error into a readable message
|
||||
fn format_parser_error(error: &Simple<Token>) -> String {
|
||||
use chumsky::error::SimpleReason;
|
||||
|
||||
let found = error
|
||||
.found()
|
||||
.map(|t| format!("'{}'", format_token(t)))
|
||||
.unwrap_or_else(|| "end of input".to_string());
|
||||
|
||||
// Check for custom error messages first (from Simple::custom())
|
||||
if let SimpleReason::Custom(msg) = error.reason() {
|
||||
return msg.clone();
|
||||
}
|
||||
|
||||
let expected = format_token_set(error.expected());
|
||||
|
||||
if !expected.is_empty() {
|
||||
// Check for common patterns and provide helpful messages
|
||||
let expected_str = expected.join(", ");
|
||||
|
||||
// Detect common mistakes
|
||||
if expected.contains(&"';'".to_string()) && error.found() == Some(&Token::Colon) {
|
||||
return format!(
|
||||
"Expected semicolon ';' to end declaration, found '{}'",
|
||||
format_token(error.found().unwrap())
|
||||
);
|
||||
}
|
||||
|
||||
if expected.contains(&"':'".to_string()) && error.found() == Some(&Token::Semicolon) {
|
||||
return format!(
|
||||
"Expected colon ':' before type, found '{}'",
|
||||
format_token(error.found().unwrap())
|
||||
);
|
||||
}
|
||||
|
||||
format!("Unexpected {}, expected one of: {}", found, expected_str)
|
||||
} else if let Some(label) = error.label() {
|
||||
label.to_string()
|
||||
} else {
|
||||
format!("Unexpected token {}", found)
|
||||
}
|
||||
}
|
||||
|
||||
/// Format a token for display
|
||||
fn format_token(token: &Token) -> String {
|
||||
match token {
|
||||
Token::Namespace => "namespace".to_string(),
|
||||
Token::Theory => "theory".to_string(),
|
||||
Token::Instance => "instance".to_string(),
|
||||
Token::Query => "query".to_string(),
|
||||
Token::Sort => "Sort".to_string(),
|
||||
Token::Prop => "Prop".to_string(),
|
||||
Token::Forall => "forall".to_string(),
|
||||
Token::Exists => "exists".to_string(),
|
||||
Token::True => "true".to_string(),
|
||||
Token::False => "false".to_string(),
|
||||
Token::Ident(s) => s.clone(),
|
||||
Token::LBrace => "{".to_string(),
|
||||
Token::RBrace => "}".to_string(),
|
||||
Token::LParen => "(".to_string(),
|
||||
Token::RParen => ")".to_string(),
|
||||
Token::LBracket => "[".to_string(),
|
||||
Token::RBracket => "]".to_string(),
|
||||
Token::Colon => ":".to_string(),
|
||||
Token::Semicolon => ";".to_string(),
|
||||
Token::Comma => ",".to_string(),
|
||||
Token::Dot => ".".to_string(),
|
||||
Token::Slash => "/".to_string(),
|
||||
Token::Arrow => "->".to_string(),
|
||||
Token::Eq => "=".to_string(),
|
||||
Token::Turnstile => "|-".to_string(),
|
||||
Token::And => r"/\".to_string(),
|
||||
Token::Or => r"\/".to_string(),
|
||||
Token::Question => "?".to_string(),
|
||||
Token::Chase => "chase".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Format a set of expected tokens
|
||||
fn format_token_set<'a>(expected: impl Iterator<Item = &'a Option<Token>>) -> Vec<String> {
|
||||
expected
|
||||
.filter_map(|opt| opt.as_ref())
|
||||
.map(|t| format!("'{}'", format_token(t)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Format a set of expected characters
|
||||
fn format_char_set<'a>(expected: impl Iterator<Item = &'a Option<char>>) -> String {
|
||||
let chars: Vec<String> = expected
|
||||
.filter_map(|opt| opt.as_ref())
|
||||
.map(|c| format!("'{}'", c))
|
||||
.collect();
|
||||
|
||||
if chars.is_empty() {
|
||||
"valid character".to_string()
|
||||
} else if chars.len() == 1 {
|
||||
chars[0].clone()
|
||||
} else {
|
||||
chars.join(" or ")
|
||||
}
|
||||
}
|
||||
114
src/id.rs
Normal file
114
src/id.rs
Normal file
@ -0,0 +1,114 @@
|
||||
//! ID types for geolog, following chit's multi-level ID design
|
||||
//!
|
||||
//! The key insight is that different operations benefit from different ID granularities:
|
||||
//! - UUIDs for global identity (persistence, version control, cross-structure references)
|
||||
//! - Luids for installation-wide identity (stable across structures, persisted)
|
||||
//! - Slids for structure-local computation (cache-friendly, compact)
|
||||
//!
|
||||
//! We use egglog's `define_id!` macro to create newtype wrappers around usize,
|
||||
//! giving us type safety (can't mix up Slid with Luid) and nice Debug output.
|
||||
|
||||
// Re-export NumericId trait and IdVec for typed indexing
|
||||
pub use egglog_numeric_id::{define_id, IdVec, NumericId};
|
||||
pub use nonminmax::NonMaxUsize;
|
||||
pub use uuid::Uuid;
|
||||
|
||||
// We define our own macro that wraps egglog's define_id! and adds rkyv derives
|
||||
macro_rules! define_id_with_rkyv {
|
||||
($v:vis $name:ident, $repr:ty, $doc:tt) => {
|
||||
#[doc = $doc]
|
||||
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
#[derive(rkyv::Archive, rkyv::Deserialize, rkyv::Serialize)]
|
||||
#[archive(check_bytes)]
|
||||
#[repr(transparent)]
|
||||
$v struct $name {
|
||||
/// The underlying representation (public for zero-copy archived access)
|
||||
pub rep: $repr,
|
||||
}
|
||||
|
||||
impl NumericId for $name {
|
||||
type Rep = $repr;
|
||||
type Atomic = std::sync::atomic::AtomicUsize;
|
||||
|
||||
fn new(val: $repr) -> Self {
|
||||
Self { rep: val }
|
||||
}
|
||||
|
||||
fn from_usize(index: usize) -> Self {
|
||||
Self { rep: index as $repr }
|
||||
}
|
||||
|
||||
fn index(self) -> usize {
|
||||
self.rep as usize
|
||||
}
|
||||
|
||||
fn rep(self) -> $repr {
|
||||
self.rep
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for $name {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}({})", stringify!($name), self.rep)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for $name {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.rep)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
define_id_with_rkyv!(
|
||||
pub Luid,
|
||||
usize,
|
||||
"Locally Universal ID: index into the global universe of UUIDs. Stable across installation, persisted."
|
||||
);
|
||||
|
||||
define_id_with_rkyv!(
|
||||
pub Slid,
|
||||
usize,
|
||||
"Structure-Local ID: index within a structure's element universe. Primary working ID."
|
||||
);
|
||||
|
||||
define_id_with_rkyv!(
|
||||
pub SortSlid,
|
||||
usize,
|
||||
"Sort-Local ID: index within a particular sort's carrier. Computed on-demand."
|
||||
);
|
||||
|
||||
/// A Slid that can be stored in Option without doubling size.
|
||||
/// Uses `NonMaxUsize` so that `Option<NonMaxUsize>` is the same size as `usize`,
|
||||
/// with `usize::MAX` serving as the niche for `None`.
|
||||
pub type OptSlid = Option<NonMaxUsize>;
|
||||
|
||||
/// Convert a Slid to OptSlid.
|
||||
/// Returns None if slid == usize::MAX (which would be an astronomically large structure).
|
||||
#[inline]
|
||||
pub fn some_slid(slid: Slid) -> OptSlid {
|
||||
NonMaxUsize::new(slid.index())
|
||||
}
|
||||
|
||||
/// Extract a Slid from OptSlid.
|
||||
#[inline]
|
||||
pub fn get_slid(opt: OptSlid) -> Option<Slid> {
|
||||
opt.map(|n| Slid::from_usize(n.get()))
|
||||
}
|
||||
|
||||
/// A Luid that can be stored in Option without doubling size.
|
||||
/// Analogous to OptSlid but for cross-instance references.
|
||||
pub type OptLuid = Option<NonMaxUsize>;
|
||||
|
||||
/// Convert a Luid to OptLuid.
|
||||
#[inline]
|
||||
pub fn some_luid(luid: Luid) -> OptLuid {
|
||||
NonMaxUsize::new(luid.index())
|
||||
}
|
||||
|
||||
/// Extract a Luid from OptLuid.
|
||||
#[inline]
|
||||
pub fn get_luid(opt: OptLuid) -> Option<Luid> {
|
||||
opt.map(|n| Luid::from_usize(n.get()))
|
||||
}
|
||||
143
src/lexer.rs
Normal file
143
src/lexer.rs
Normal file
@ -0,0 +1,143 @@
|
||||
//! Lexer for Geolog
|
||||
//!
|
||||
//! Tokenizes source into a stream for the parser.
|
||||
|
||||
use chumsky::prelude::*;
|
||||
use std::ops::Range;
|
||||
|
||||
/// Token types for Geolog
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum Token {
|
||||
// Keywords
|
||||
Namespace,
|
||||
Theory,
|
||||
Instance,
|
||||
Query,
|
||||
Sort,
|
||||
Prop,
|
||||
Forall,
|
||||
Exists,
|
||||
True,
|
||||
False,
|
||||
Chase,
|
||||
|
||||
// Identifiers
|
||||
Ident(String),
|
||||
|
||||
// Punctuation
|
||||
LBrace, // {
|
||||
RBrace, // }
|
||||
LParen, // (
|
||||
RParen, // )
|
||||
LBracket, // [
|
||||
RBracket, // ]
|
||||
Colon, // :
|
||||
Semicolon, // ;
|
||||
Comma, // ,
|
||||
Dot, // .
|
||||
Slash, // /
|
||||
Arrow, // ->
|
||||
Eq, // =
|
||||
Turnstile, // |-
|
||||
And, // /\
|
||||
Or, // \/
|
||||
Question, // ?
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Token {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Token::Namespace => write!(f, "namespace"),
|
||||
Token::Theory => write!(f, "theory"),
|
||||
Token::Instance => write!(f, "instance"),
|
||||
Token::Query => write!(f, "query"),
|
||||
Token::Sort => write!(f, "Sort"),
|
||||
Token::Prop => write!(f, "Prop"),
|
||||
Token::Forall => write!(f, "forall"),
|
||||
Token::Exists => write!(f, "exists"),
|
||||
Token::True => write!(f, "true"),
|
||||
Token::False => write!(f, "false"),
|
||||
Token::Chase => write!(f, "chase"),
|
||||
Token::Ident(s) => write!(f, "{}", s),
|
||||
Token::LBrace => write!(f, "{{"),
|
||||
Token::RBrace => write!(f, "}}"),
|
||||
Token::LParen => write!(f, "("),
|
||||
Token::RParen => write!(f, ")"),
|
||||
Token::LBracket => write!(f, "["),
|
||||
Token::RBracket => write!(f, "]"),
|
||||
Token::Colon => write!(f, ":"),
|
||||
Token::Semicolon => write!(f, ";"),
|
||||
Token::Comma => write!(f, ","),
|
||||
Token::Dot => write!(f, "."),
|
||||
Token::Slash => write!(f, "/"),
|
||||
Token::Arrow => write!(f, "->"),
|
||||
Token::Eq => write!(f, "="),
|
||||
Token::Turnstile => write!(f, "|-"),
|
||||
Token::And => write!(f, r"/\"),
|
||||
Token::Or => write!(f, r"\/"),
|
||||
Token::Question => write!(f, "?"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Type alias for spans
|
||||
pub type Span = Range<usize>;
|
||||
|
||||
/// Create a lexer for Geolog
|
||||
pub fn lexer() -> impl Parser<char, Vec<(Token, Span)>, Error = Simple<char>> {
|
||||
let keyword_or_ident = text::ident().map(|s: String| match s.as_str() {
|
||||
"namespace" => Token::Namespace,
|
||||
"theory" => Token::Theory,
|
||||
"instance" => Token::Instance,
|
||||
"query" => Token::Query,
|
||||
"Sort" => Token::Sort,
|
||||
"Prop" => Token::Prop,
|
||||
"forall" => Token::Forall,
|
||||
"exists" => Token::Exists,
|
||||
"true" => Token::True,
|
||||
"false" => Token::False,
|
||||
"chase" => Token::Chase,
|
||||
_ => Token::Ident(s),
|
||||
});
|
||||
|
||||
let punctuation = choice((
|
||||
just("->").to(Token::Arrow),
|
||||
just("|-").to(Token::Turnstile),
|
||||
just(r"/\").to(Token::And),
|
||||
just(r"\/").to(Token::Or),
|
||||
just('{').to(Token::LBrace),
|
||||
just('}').to(Token::RBrace),
|
||||
just('(').to(Token::LParen),
|
||||
just(')').to(Token::RParen),
|
||||
just('[').to(Token::LBracket),
|
||||
just(']').to(Token::RBracket),
|
||||
just(':').to(Token::Colon),
|
||||
just(';').to(Token::Semicolon),
|
||||
just(',').to(Token::Comma),
|
||||
just('.').to(Token::Dot),
|
||||
just('/').to(Token::Slash),
|
||||
just('=').to(Token::Eq),
|
||||
just('?').to(Token::Question),
|
||||
));
|
||||
|
||||
// Comments: // to end of line (handles both mid-file and end-of-file)
|
||||
// IMPORTANT: Must check for // BEFORE single / to avoid tokenizing as two Slash tokens
|
||||
let line_comment = just("//")
|
||||
.then(none_of('\n').repeated())
|
||||
.then(just('\n').or_not()) // Either newline or EOF
|
||||
.ignored();
|
||||
|
||||
// Token OR comment - comments produce None, tokens produce Some
|
||||
let token_or_skip = line_comment
|
||||
.to(None)
|
||||
.or(keyword_or_ident.or(punctuation).map(Some));
|
||||
|
||||
token_or_skip
|
||||
.map_with_span(|opt_tok, span| opt_tok.map(|tok| (tok, span)))
|
||||
.padded()
|
||||
.repeated()
|
||||
.then_ignore(end())
|
||||
.map(|items| items.into_iter().flatten().collect())
|
||||
}
|
||||
|
||||
// Unit tests moved to tests/unit_parsing.rs
|
||||
51
src/lib.rs
Normal file
51
src/lib.rs
Normal file
@ -0,0 +1,51 @@
|
||||
//! Geolog: A language for geometric logic
|
||||
//!
|
||||
//! Geolog is a type theory with semantics in topoi and geometric morphisms,
|
||||
//! designed as a unified language for database schemas, queries, and migrations.
|
||||
|
||||
pub mod ast;
|
||||
pub mod cc;
|
||||
pub mod core;
|
||||
pub mod elaborate;
|
||||
pub mod error;
|
||||
pub mod id;
|
||||
pub mod lexer;
|
||||
pub mod meta;
|
||||
pub mod naming;
|
||||
pub mod overlay;
|
||||
pub mod parser;
|
||||
pub mod patch;
|
||||
pub mod pretty;
|
||||
pub mod query;
|
||||
pub mod repl;
|
||||
pub mod serialize;
|
||||
pub mod solver;
|
||||
pub mod store;
|
||||
pub mod tensor;
|
||||
pub mod universe;
|
||||
pub mod version;
|
||||
pub mod zerocopy;
|
||||
|
||||
pub use ast::*;
|
||||
pub use lexer::lexer;
|
||||
pub use parser::parser;
|
||||
pub use pretty::pretty_print;
|
||||
|
||||
/// Parse a Geolog source string into an AST
|
||||
pub fn parse(input: &str) -> Result<File, String> {
|
||||
use chumsky::prelude::*;
|
||||
|
||||
let tokens = lexer::lexer()
|
||||
.parse(input)
|
||||
.map_err(|errs| error::format_lexer_errors(input, errs))?;
|
||||
|
||||
let token_stream: Vec<_> = tokens.iter().map(|(t, s)| (t.clone(), s.clone())).collect();
|
||||
let len = input.len();
|
||||
|
||||
parser::parser()
|
||||
.parse(chumsky::Stream::from_iter(
|
||||
len..len + 1,
|
||||
token_stream.into_iter(),
|
||||
))
|
||||
.map_err(|errs| error::format_parser_errors(input, errs, &tokens))
|
||||
}
|
||||
1106
src/meta.rs
Normal file
1106
src/meta.rs
Normal file
File diff suppressed because it is too large
Load Diff
355
src/naming.rs
Normal file
355
src/naming.rs
Normal file
@ -0,0 +1,355 @@
|
||||
//! Global naming index for human-readable names
|
||||
//!
|
||||
//! Names are purely a UI concern - all data in structures is identified by UUID.
|
||||
//! This index maps UUIDs to human-readable names for display and provides
|
||||
//! reverse lookup for parsing.
|
||||
//!
|
||||
//! Following chit's design: "namings are purely a user interface (input/output
|
||||
//! for humans and large language models)"
|
||||
//!
|
||||
//! ## Suffix-based lookup via ReversedPath
|
||||
//!
|
||||
//! To efficiently look up names by suffix (e.g., find all `*/A` when given just `A`),
|
||||
//! we store paths reversed in a BTreeMap. For example:
|
||||
//! - `["PetriNet", "P"]` is stored as `ReversedPath(["P", "PetriNet"])`
|
||||
//! - A prefix scan for `["A"]` finds all paths ending in `A`
|
||||
//!
|
||||
//! This enables O(log n + k) suffix lookups where k is the number of matches.
|
||||
|
||||
use crate::id::Uuid;
|
||||
use indexmap::IndexMap;
|
||||
use memmap2::Mmap;
|
||||
use rkyv::ser::Serializer;
|
||||
use rkyv::ser::serializers::AllocSerializer;
|
||||
use rkyv::{Archive, Deserialize, Serialize, check_archived_root};
|
||||
use std::collections::BTreeMap;
|
||||
use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// A qualified name path (e.g., ["PetriNet", "P"] for sort P in theory PetriNet)
|
||||
pub type QualifiedName = Vec<String>;
|
||||
|
||||
/// A path stored with segments reversed for efficient suffix-based lookup.
|
||||
///
|
||||
/// `["PetriNet", "P"]` becomes `ReversedPath(["P", "PetriNet"])`.
|
||||
/// This allows BTreeMap range queries to find all paths with a given suffix.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct ReversedPath(Vec<String>);
|
||||
|
||||
impl ReversedPath {
|
||||
/// Create a reversed path from a qualified name.
|
||||
pub fn from_qualified(segments: &[String]) -> Self {
|
||||
Self(segments.iter().rev().cloned().collect())
|
||||
}
|
||||
|
||||
/// Convert back to a qualified name (forward order).
|
||||
pub fn to_qualified(&self) -> QualifiedName {
|
||||
self.0.iter().rev().cloned().collect()
|
||||
}
|
||||
|
||||
/// Create a prefix for range queries (just the suffix segments, reversed).
|
||||
/// For looking up all paths ending in `["A"]`, create `ReversedPath(["A"])`.
|
||||
pub fn from_suffix(suffix: &[String]) -> Self {
|
||||
// Suffix is already in forward order, just reverse it
|
||||
Self(suffix.iter().rev().cloned().collect())
|
||||
}
|
||||
|
||||
/// Check if this path starts with the given prefix (for range iteration).
|
||||
pub fn starts_with(&self, prefix: &ReversedPath) -> bool {
|
||||
self.0.len() >= prefix.0.len() && self.0[..prefix.0.len()] == prefix.0[..]
|
||||
}
|
||||
|
||||
/// Get the inner segments (reversed order).
|
||||
pub fn segments(&self) -> &[String] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Serializable form of the naming index
|
||||
#[derive(Archive, Deserialize, Serialize, Default)]
|
||||
#[archive(check_bytes)]
|
||||
struct NamingData {
|
||||
/// UUID → qualified name mapping
|
||||
entries: Vec<(Uuid, QualifiedName)>,
|
||||
}
|
||||
|
||||
/// Global naming index
|
||||
///
|
||||
/// Provides bidirectional mapping between UUIDs and human-readable names.
|
||||
/// Names are qualified paths like ["PetriNet", "P"] for sort P in theory PetriNet.
|
||||
///
|
||||
/// ## Lookup modes
|
||||
/// - **By UUID**: O(1) via `uuid_to_name`
|
||||
/// - **By exact path**: O(log n) via `path_to_uuid`
|
||||
/// - **By suffix**: O(log n + k) via BTreeMap range query on reversed paths
|
||||
#[derive(Debug, Default)]
|
||||
pub struct NamingIndex {
|
||||
/// UUID → qualified name (for display)
|
||||
uuid_to_name: IndexMap<Uuid, QualifiedName>,
|
||||
/// Reversed path → UUIDs (for suffix-based lookup)
|
||||
/// Paths are stored reversed so that suffix queries become prefix scans.
|
||||
/// Multiple UUIDs can share the same path (ambiguous names).
|
||||
path_to_uuid: BTreeMap<ReversedPath, Vec<Uuid>>,
|
||||
/// Persistence path
|
||||
path: Option<PathBuf>,
|
||||
/// Dirty flag
|
||||
dirty: bool,
|
||||
}
|
||||
|
||||
impl NamingIndex {
|
||||
/// Create a new empty naming index
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Create a naming index with a persistence path
|
||||
pub fn with_path(path: impl Into<PathBuf>) -> Self {
|
||||
Self {
|
||||
uuid_to_name: IndexMap::new(),
|
||||
path_to_uuid: BTreeMap::new(),
|
||||
path: Some(path.into()),
|
||||
dirty: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Load a naming index from disk
|
||||
pub fn load(path: impl Into<PathBuf>) -> Result<Self, String> {
|
||||
let path = path.into();
|
||||
|
||||
if !path.exists() {
|
||||
return Ok(Self::with_path(path));
|
||||
}
|
||||
|
||||
let file = File::open(&path).map_err(|e| format!("Failed to open naming index: {}", e))?;
|
||||
|
||||
let mmap = unsafe { Mmap::map(&file) }
|
||||
.map_err(|e| format!("Failed to mmap naming index: {}", e))?;
|
||||
|
||||
if mmap.is_empty() {
|
||||
return Ok(Self::with_path(path));
|
||||
}
|
||||
|
||||
let archived = check_archived_root::<NamingData>(&mmap)
|
||||
.map_err(|e| format!("Failed to validate naming index: {}", e))?;
|
||||
|
||||
let data: NamingData = archived
|
||||
.deserialize(&mut rkyv::Infallible)
|
||||
.map_err(|_| "Failed to deserialize naming index")?;
|
||||
|
||||
let mut index = Self::with_path(path);
|
||||
for (uuid, name) in data.entries {
|
||||
index.insert_internal(uuid, name);
|
||||
}
|
||||
|
||||
Ok(index)
|
||||
}
|
||||
|
||||
/// Save the naming index to disk
|
||||
pub fn save(&mut self) -> Result<(), String> {
|
||||
let path = self
|
||||
.path
|
||||
.as_ref()
|
||||
.ok_or("Naming index has no persistence path")?;
|
||||
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.map_err(|e| format!("Failed to create naming directory: {}", e))?;
|
||||
}
|
||||
|
||||
let data = NamingData {
|
||||
entries: self
|
||||
.uuid_to_name
|
||||
.iter()
|
||||
.map(|(k, v)| (*k, v.clone()))
|
||||
.collect(),
|
||||
};
|
||||
|
||||
let mut serializer = AllocSerializer::<4096>::default();
|
||||
serializer
|
||||
.serialize_value(&data)
|
||||
.map_err(|e| format!("Failed to serialize naming index: {}", e))?;
|
||||
let bytes = serializer.into_serializer().into_inner();
|
||||
|
||||
let temp_path = path.with_extension("tmp");
|
||||
{
|
||||
let mut file = File::create(&temp_path)
|
||||
.map_err(|e| format!("Failed to create temp file: {}", e))?;
|
||||
file.write_all(&bytes)
|
||||
.map_err(|e| format!("Failed to write naming index: {}", e))?;
|
||||
file.sync_all()
|
||||
.map_err(|e| format!("Failed to sync naming index: {}", e))?;
|
||||
}
|
||||
|
||||
fs::rename(&temp_path, path)
|
||||
.map_err(|e| format!("Failed to rename naming index: {}", e))?;
|
||||
|
||||
self.dirty = false;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Internal insert without setting dirty flag
|
||||
fn insert_internal(&mut self, uuid: Uuid, name: QualifiedName) {
|
||||
// Add to reverse index (reversed path → UUIDs)
|
||||
let reversed = ReversedPath::from_qualified(&name);
|
||||
self.path_to_uuid
|
||||
.entry(reversed)
|
||||
.or_default()
|
||||
.push(uuid);
|
||||
self.uuid_to_name.insert(uuid, name);
|
||||
}
|
||||
|
||||
/// Register a name for a UUID
|
||||
pub fn insert(&mut self, uuid: Uuid, name: QualifiedName) {
|
||||
self.insert_internal(uuid, name);
|
||||
self.dirty = true;
|
||||
}
|
||||
|
||||
/// Register a simple (unqualified) name for a UUID
|
||||
pub fn insert_simple(&mut self, uuid: Uuid, name: String) {
|
||||
self.insert(uuid, vec![name]);
|
||||
}
|
||||
|
||||
/// Get the qualified name for a UUID
|
||||
pub fn get(&self, uuid: &Uuid) -> Option<&QualifiedName> {
|
||||
self.uuid_to_name.get(uuid)
|
||||
}
|
||||
|
||||
/// Get the simple (last component) name for a UUID
|
||||
pub fn get_simple(&self, uuid: &Uuid) -> Option<&str> {
|
||||
self.uuid_to_name
|
||||
.get(uuid)
|
||||
.and_then(|name| name.last())
|
||||
.map(|s| s.as_str())
|
||||
}
|
||||
|
||||
/// Get the display name for a UUID (simple name, or UUID if unnamed)
|
||||
pub fn display_name(&self, uuid: &Uuid) -> String {
|
||||
self.get_simple(uuid)
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_else(|| format!("{}", uuid))
|
||||
}
|
||||
|
||||
/// Look up all UUIDs whose qualified name ends with the given suffix.
|
||||
///
|
||||
/// Examples:
|
||||
/// - `lookup_suffix(&["A"])` returns UUIDs for "ExampleNet/A", "OtherNet/A", etc.
|
||||
/// - `lookup_suffix(&["ExampleNet", "A"])` returns just "ExampleNet/A"
|
||||
///
|
||||
/// Returns an iterator over matching UUIDs.
|
||||
pub fn lookup_suffix<'a>(&'a self, suffix: &[String]) -> impl Iterator<Item = Uuid> + 'a {
|
||||
let prefix = ReversedPath::from_suffix(suffix);
|
||||
self.path_to_uuid
|
||||
.range(prefix.clone()..)
|
||||
.take_while(move |(k, _)| k.starts_with(&prefix))
|
||||
.flat_map(|(_, uuids)| uuids.iter().copied())
|
||||
}
|
||||
|
||||
/// Look up UUID by exact qualified path.
|
||||
/// Returns None if ambiguous (multiple UUIDs share the exact path).
|
||||
pub fn lookup_exact(&self, path: &[String]) -> Option<Uuid> {
|
||||
let reversed = ReversedPath::from_qualified(path);
|
||||
match self.path_to_uuid.get(&reversed) {
|
||||
Some(uuids) if uuids.len() == 1 => Some(uuids[0]),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve a path to a UUID.
|
||||
/// - If exact match exists, return it.
|
||||
/// - If suffix matches exactly one UUID, return it.
|
||||
/// - Otherwise return Err with all candidates (empty if not found, multiple if ambiguous).
|
||||
pub fn resolve(&self, path: &[String]) -> Result<Uuid, Vec<Uuid>> {
|
||||
// First try exact match
|
||||
if let Some(uuid) = self.lookup_exact(path) {
|
||||
return Ok(uuid);
|
||||
}
|
||||
|
||||
// Fall back to suffix match
|
||||
let candidates: Vec<Uuid> = self.lookup_suffix(path).collect();
|
||||
match candidates.len() {
|
||||
1 => Ok(candidates[0]),
|
||||
_ => Err(candidates),
|
||||
}
|
||||
}
|
||||
|
||||
/// Look up UUIDs by simple (single-segment) name.
|
||||
/// This is a convenience wrapper around `lookup_suffix` for single names.
|
||||
pub fn lookup(&self, name: &str) -> Vec<Uuid> {
|
||||
self.lookup_suffix(&[name.to_string()]).collect()
|
||||
}
|
||||
|
||||
/// Look up a unique UUID by simple name (returns None if ambiguous or not found)
|
||||
pub fn lookup_unique(&self, name: &str) -> Option<Uuid> {
|
||||
let results: Vec<Uuid> = self.lookup_suffix(&[name.to_string()]).collect();
|
||||
if results.len() == 1 {
|
||||
Some(results[0])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if dirty
|
||||
pub fn is_dirty(&self) -> bool {
|
||||
self.dirty
|
||||
}
|
||||
|
||||
/// Number of entries
|
||||
pub fn len(&self) -> usize {
|
||||
self.uuid_to_name.len()
|
||||
}
|
||||
|
||||
/// Check if empty
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.uuid_to_name.is_empty()
|
||||
}
|
||||
|
||||
/// Iterate over all (UUID, name) pairs
|
||||
pub fn iter(&self) -> impl Iterator<Item = (&Uuid, &QualifiedName)> {
|
||||
self.uuid_to_name.iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for NamingIndex {
|
||||
fn drop(&mut self) {
|
||||
if self.dirty && self.path.is_some() {
|
||||
let _ = self.save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the global naming index path
|
||||
pub fn global_naming_path() -> Option<PathBuf> {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
std::env::var("HOME").ok().map(|h| {
|
||||
let mut p = PathBuf::from(h);
|
||||
p.push(".config");
|
||||
p.push("geolog");
|
||||
p.push("names.bin");
|
||||
p
|
||||
})
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
std::env::var("APPDATA").ok().map(|mut p| {
|
||||
p.push("geolog");
|
||||
p.push("names.bin");
|
||||
p
|
||||
})
|
||||
}
|
||||
#[cfg(not(any(unix, windows)))]
|
||||
{
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Load or create the global naming index
|
||||
pub fn global_naming_index() -> NamingIndex {
|
||||
match global_naming_path() {
|
||||
Some(path) => NamingIndex::load(&path).unwrap_or_else(|_| NamingIndex::with_path(path)),
|
||||
None => NamingIndex::new(),
|
||||
}
|
||||
}
|
||||
|
||||
// Unit tests moved to tests/proptest_naming.rs
|
||||
574
src/overlay.rs
Normal file
574
src/overlay.rs
Normal file
@ -0,0 +1,574 @@
|
||||
//! Overlay structures: patch-on-write semantics for efficient mutations.
|
||||
//!
|
||||
//! Instead of copying a structure to mutate it, we layer changes on top of an
|
||||
//! immutable base. The base is memory-mapped (zero-copy), and mutations accumulate
|
||||
//! in a thin delta layer. Cost of mutation is O(Δ), never O(base).
|
||||
//!
|
||||
//! # Architecture
|
||||
//!
|
||||
//! ```text
|
||||
//! ┌────────────────────────────────────────────────────────┐
|
||||
//! │ MappedStructure (immutable, mmap'd, potentially huge) │
|
||||
//! └────────────────────────────────────────────────────────┘
|
||||
//! ↑ read fallthrough
|
||||
//! ┌────────────────────────────────────────────────────────┐
|
||||
//! │ StructureDelta (tiny: just the changes) │
|
||||
//! └────────────────────────────────────────────────────────┘
|
||||
//! ```
|
||||
//!
|
||||
//! # Slid Addressing
|
||||
//!
|
||||
//! Base elements have Slids `0..base_len`. New overlay elements get Slids
|
||||
//! `base_len..base_len+delta_len`, so the address space is contiguous.
|
||||
//!
|
||||
//! # Usage
|
||||
//!
|
||||
//! ```ignore
|
||||
//! // Load base (fast, zero-copy)
|
||||
//! let base = MappedStructure::open(path)?;
|
||||
//!
|
||||
//! // Create overlay for mutations
|
||||
//! let mut overlay = OverlayStructure::new(Arc::new(base));
|
||||
//!
|
||||
//! // Mutate (changes go to delta)
|
||||
//! let elem = overlay.add_element(luid, sort_id);
|
||||
//! overlay.assert_relation(rel_id, vec![elem, other]);
|
||||
//!
|
||||
//! // Read (checks delta first, falls back to base)
|
||||
//! let sort = overlay.get_sort(elem);
|
||||
//!
|
||||
//! // Commit (materialize to new immutable structure)
|
||||
//! let new_base = overlay.commit(new_path)?;
|
||||
//!
|
||||
//! // Or rollback (instant - just clears delta)
|
||||
//! overlay.rollback();
|
||||
//! ```
|
||||
|
||||
use std::collections::{BTreeSet, HashMap};
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::core::{SortId, Structure};
|
||||
use crate::id::{Luid, NumericId, Slid};
|
||||
use crate::serialize::save_structure;
|
||||
use crate::zerocopy::{MappedRelation, MappedStructure};
|
||||
|
||||
// ============================================================================
|
||||
// DELTA TYPES
|
||||
// ============================================================================
|
||||
|
||||
/// A delta/patch representing changes to a structure.
|
||||
///
|
||||
/// This is the runtime-efficient analog of `Patch` (which uses UUIDs for
|
||||
/// persistence). `StructureDelta` uses Slids for fast in-memory operations.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct StructureDelta {
|
||||
/// New elements: (Luid, SortId). Slids start at base.len().
|
||||
pub new_elements: Vec<(Luid, SortId)>,
|
||||
|
||||
/// Per-relation deltas (indexed by rel_id)
|
||||
pub relations: Vec<RelationDelta>,
|
||||
|
||||
/// Per-function deltas (indexed by func_id)
|
||||
pub functions: Vec<FunctionDelta>,
|
||||
}
|
||||
|
||||
impl StructureDelta {
|
||||
/// Create a new empty delta with the given number of relations and functions.
|
||||
pub fn new(num_relations: usize, num_functions: usize) -> Self {
|
||||
Self {
|
||||
new_elements: Vec::new(),
|
||||
relations: vec![RelationDelta::default(); num_relations],
|
||||
functions: vec![FunctionDelta::default(); num_functions],
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the delta is empty (no changes).
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.new_elements.is_empty()
|
||||
&& self.relations.iter().all(|r| r.is_empty())
|
||||
&& self.functions.iter().all(|f| f.is_empty())
|
||||
}
|
||||
}
|
||||
|
||||
/// Delta for a single relation: assertions and retractions.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct RelationDelta {
|
||||
/// New tuples to assert (by content)
|
||||
pub assertions: BTreeSet<Vec<Slid>>,
|
||||
|
||||
/// Tuples to retract (by content, not by ID)
|
||||
pub retractions: BTreeSet<Vec<Slid>>,
|
||||
}
|
||||
|
||||
impl RelationDelta {
|
||||
/// Check if empty.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.assertions.is_empty() && self.retractions.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// Delta for a single function: updated mappings.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct FunctionDelta {
|
||||
/// Updated mappings: domain Slid -> codomain Slid.
|
||||
/// Only supports local functions in this version.
|
||||
pub updates: HashMap<Slid, Slid>,
|
||||
}
|
||||
|
||||
impl FunctionDelta {
|
||||
/// Check if empty.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.updates.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// OVERLAY STRUCTURE
|
||||
// ============================================================================
|
||||
|
||||
/// A mutable overlay on top of an immutable base structure.
|
||||
///
|
||||
/// All reads check the delta first, then fall back to the base.
|
||||
/// All writes go to the delta. The base is never modified.
|
||||
pub struct OverlayStructure {
|
||||
/// The immutable base (memory-mapped, zero-copy)
|
||||
base: Arc<MappedStructure>,
|
||||
|
||||
/// Accumulated changes
|
||||
delta: StructureDelta,
|
||||
}
|
||||
|
||||
impl OverlayStructure {
|
||||
/// Create a new overlay on top of a base structure.
|
||||
pub fn new(base: Arc<MappedStructure>) -> Self {
|
||||
let num_relations = base.num_relations();
|
||||
let num_functions = base.num_functions();
|
||||
Self {
|
||||
base,
|
||||
delta: StructureDelta::new(num_relations, num_functions),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the immutable base.
|
||||
pub fn base(&self) -> &MappedStructure {
|
||||
&self.base
|
||||
}
|
||||
|
||||
/// Get the accumulated delta.
|
||||
pub fn delta(&self) -> &StructureDelta {
|
||||
&self.delta
|
||||
}
|
||||
|
||||
/// Check if clean (no changes from base).
|
||||
pub fn is_clean(&self) -> bool {
|
||||
self.delta.is_empty()
|
||||
}
|
||||
|
||||
/// Discard all changes, returning to base state.
|
||||
pub fn rollback(&mut self) {
|
||||
self.delta = StructureDelta::new(
|
||||
self.base.num_relations(),
|
||||
self.base.num_functions(),
|
||||
);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// ELEMENT OPERATIONS
|
||||
// ========================================================================
|
||||
|
||||
/// Total number of elements (base + overlay).
|
||||
pub fn len(&self) -> usize {
|
||||
self.base.len() + self.delta.new_elements.len()
|
||||
}
|
||||
|
||||
/// Check if empty.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
|
||||
/// Number of sorts.
|
||||
pub fn num_sorts(&self) -> usize {
|
||||
self.base.num_sorts()
|
||||
}
|
||||
|
||||
/// Number of relations.
|
||||
pub fn num_relations(&self) -> usize {
|
||||
self.base.num_relations()
|
||||
}
|
||||
|
||||
/// Number of functions.
|
||||
pub fn num_functions(&self) -> usize {
|
||||
self.base.num_functions()
|
||||
}
|
||||
|
||||
/// Add a new element. Returns its Slid (starts at base.len()).
|
||||
pub fn add_element(&mut self, luid: Luid, sort_id: SortId) -> Slid {
|
||||
let slid = Slid::from_usize(self.base.len() + self.delta.new_elements.len());
|
||||
self.delta.new_elements.push((luid, sort_id));
|
||||
slid
|
||||
}
|
||||
|
||||
/// Get the Luid for an element.
|
||||
pub fn get_luid(&self, slid: Slid) -> Option<Luid> {
|
||||
let idx = slid.index();
|
||||
let base_len = self.base.len();
|
||||
if idx < base_len {
|
||||
self.base.get_luid(slid)
|
||||
} else {
|
||||
self.delta
|
||||
.new_elements
|
||||
.get(idx - base_len)
|
||||
.map(|(luid, _)| *luid)
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the sort for an element.
|
||||
pub fn get_sort(&self, slid: Slid) -> Option<SortId> {
|
||||
let idx = slid.index();
|
||||
let base_len = self.base.len();
|
||||
if idx < base_len {
|
||||
self.base.get_sort(slid)
|
||||
} else {
|
||||
self.delta
|
||||
.new_elements
|
||||
.get(idx - base_len)
|
||||
.map(|(_, sort)| *sort)
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterate over all elements (base + overlay).
|
||||
pub fn elements(&self) -> impl Iterator<Item = (Slid, Luid, SortId)> + '_ {
|
||||
let base_iter = self.base.elements();
|
||||
let base_len = self.base.len();
|
||||
let overlay_iter = self
|
||||
.delta
|
||||
.new_elements
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(move |(i, (luid, sort))| {
|
||||
(Slid::from_usize(base_len + i), *luid, *sort)
|
||||
});
|
||||
base_iter.chain(overlay_iter)
|
||||
}
|
||||
|
||||
/// Iterate over elements of a specific sort.
|
||||
pub fn elements_of_sort(&self, sort_id: SortId) -> impl Iterator<Item = Slid> + '_ {
|
||||
let base_iter = self.base.elements_of_sort(sort_id);
|
||||
let base_len = self.base.len();
|
||||
let overlay_iter = self
|
||||
.delta
|
||||
.new_elements
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(move |(_, (_, s))| *s == sort_id)
|
||||
.map(move |(i, _)| Slid::from_usize(base_len + i));
|
||||
base_iter.chain(overlay_iter)
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// RELATION OPERATIONS
|
||||
// ========================================================================
|
||||
|
||||
/// Assert a relation tuple.
|
||||
pub fn assert_relation(&mut self, rel_id: usize, tuple: Vec<Slid>) {
|
||||
// If this tuple was previously retracted, un-retract it
|
||||
self.delta.relations[rel_id].retractions.remove(&tuple);
|
||||
// Add to assertions
|
||||
self.delta.relations[rel_id].assertions.insert(tuple);
|
||||
}
|
||||
|
||||
/// Retract a relation tuple (by content).
|
||||
pub fn retract_relation(&mut self, rel_id: usize, tuple: Vec<Slid>) {
|
||||
// If this tuple was asserted in the overlay, just remove it
|
||||
if self.delta.relations[rel_id].assertions.remove(&tuple) {
|
||||
return;
|
||||
}
|
||||
// Otherwise, mark it as retracted from base
|
||||
self.delta.relations[rel_id].retractions.insert(tuple);
|
||||
}
|
||||
|
||||
/// Get an overlay view of a relation.
|
||||
pub fn relation(&self, rel_id: usize) -> Option<OverlayRelation<'_>> {
|
||||
let base_rel = self.base.relation(rel_id)?;
|
||||
let delta = self.delta.relations.get(rel_id)?;
|
||||
Some(OverlayRelation {
|
||||
base: base_rel,
|
||||
delta,
|
||||
})
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// FUNCTION OPERATIONS
|
||||
// ========================================================================
|
||||
|
||||
/// Set a function value.
|
||||
pub fn set_function(&mut self, func_id: usize, domain: Slid, value: Slid) {
|
||||
self.delta.functions[func_id].updates.insert(domain, value);
|
||||
}
|
||||
|
||||
/// Get a function value.
|
||||
pub fn get_function(&self, func_id: usize, domain: Slid) -> Option<Slid> {
|
||||
// Check delta first
|
||||
if let Some(&value) = self.delta.functions[func_id].updates.get(&domain) {
|
||||
return Some(value);
|
||||
}
|
||||
// Fall back to base (only for base elements)
|
||||
if domain.index() < self.base.len() {
|
||||
// Need to convert Slid to sort-local index for base lookup
|
||||
// This requires knowing the sort of the domain element
|
||||
if let Some(sort_id) = self.base.get_sort(domain) {
|
||||
// Count how many elements of this sort come before this one
|
||||
let sort_local_idx = self
|
||||
.base
|
||||
.elements_of_sort(sort_id)
|
||||
.take_while(|&s| s.index() < domain.index())
|
||||
.count();
|
||||
return self.base.function(func_id)?.get_local(sort_local_idx);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// COMMIT / MATERIALIZE
|
||||
// ========================================================================
|
||||
|
||||
/// Materialize the overlay into an owned Structure.
|
||||
///
|
||||
/// This combines the base and delta into a single Structure that can be
|
||||
/// saved to disk.
|
||||
pub fn materialize(&self) -> Structure {
|
||||
// Start with a fresh structure
|
||||
let mut structure = Structure::new(self.num_sorts());
|
||||
|
||||
// Copy base elements (we need to create them fresh since Structure wants to own them)
|
||||
// For now, we'll iterate and add. In production, we'd want a more efficient bulk copy.
|
||||
let mut slid_map: HashMap<Slid, Slid> = HashMap::new();
|
||||
|
||||
// We need a universe to add elements, but we're materializing so we'll
|
||||
// reuse the Luids from the overlay. Create elements with existing Luids.
|
||||
for (old_slid, luid, sort_id) in self.elements() {
|
||||
let new_slid = structure.add_element_with_luid(luid, sort_id);
|
||||
slid_map.insert(old_slid, new_slid);
|
||||
}
|
||||
|
||||
// Initialize relations with correct arities
|
||||
let arities: Vec<usize> = (0..self.num_relations())
|
||||
.map(|rel_id| {
|
||||
self.base
|
||||
.relation(rel_id)
|
||||
.map(|r| r.arity())
|
||||
.unwrap_or(0)
|
||||
})
|
||||
.collect();
|
||||
structure.init_relations(&arities);
|
||||
|
||||
// Copy relation tuples (applying the slid remapping)
|
||||
for rel_id in 0..self.num_relations() {
|
||||
if let Some(rel) = self.relation(rel_id) {
|
||||
for tuple in rel.live_tuples() {
|
||||
let remapped: Vec<Slid> = tuple
|
||||
.iter()
|
||||
.map(|&old_slid| slid_map.get(&old_slid).copied().unwrap_or(old_slid))
|
||||
.collect();
|
||||
structure.assert_relation(rel_id, remapped);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Copy functions (more complex, skip for now)
|
||||
|
||||
structure
|
||||
}
|
||||
|
||||
/// Commit the overlay: materialize and save to a new file, returning the new MappedStructure.
|
||||
pub fn commit(&self, path: &Path) -> Result<MappedStructure, String> {
|
||||
let structure = self.materialize();
|
||||
save_structure(&structure, path)?;
|
||||
MappedStructure::open(path)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// OVERLAY RELATION VIEW
|
||||
// ============================================================================
|
||||
|
||||
/// A read-only view of a relation through an overlay.
|
||||
pub struct OverlayRelation<'a> {
|
||||
base: MappedRelation<'a>,
|
||||
delta: &'a RelationDelta,
|
||||
}
|
||||
|
||||
impl<'a> OverlayRelation<'a> {
|
||||
/// Relation arity.
|
||||
pub fn arity(&self) -> usize {
|
||||
self.base.arity()
|
||||
}
|
||||
|
||||
/// Approximate count of live tuples.
|
||||
///
|
||||
/// This is approximate because checking retractions against base tuples
|
||||
/// would require iterating. For exact count, iterate `live_tuples()`.
|
||||
pub fn live_count_approx(&self) -> usize {
|
||||
// Base count + assertions - retractions (approximate)
|
||||
self.base.live_count() + self.delta.assertions.len()
|
||||
- self.delta.retractions.len().min(self.base.live_count())
|
||||
}
|
||||
|
||||
/// Check if a tuple is live (in base or assertions, not retracted).
|
||||
pub fn contains(&self, tuple: &[Slid]) -> bool {
|
||||
// Check if retracted
|
||||
if self.delta.retractions.contains(tuple) {
|
||||
return false;
|
||||
}
|
||||
// Check assertions
|
||||
if self.delta.assertions.contains(tuple) {
|
||||
return true;
|
||||
}
|
||||
// Check base - need to iterate base tuples to check
|
||||
// This is O(n) which is unfortunate, but we don't have a hash index
|
||||
for base_tuple in self.base.live_tuples() {
|
||||
let base_vec: Vec<Slid> = base_tuple.collect();
|
||||
if base_vec.as_slice() == tuple {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Iterate over live tuples (base filtered by retractions, plus assertions).
|
||||
///
|
||||
/// Returns tuples as `Vec<Slid>` for simplicity. Each vec is one tuple.
|
||||
pub fn live_tuples(&self) -> impl Iterator<Item = Vec<Slid>> + '_ {
|
||||
// Collect base tuples, filtering out retracted ones
|
||||
let base_filtered = self
|
||||
.base
|
||||
.live_tuples()
|
||||
.map(|t| t.collect::<Vec<_>>())
|
||||
.filter(|tuple| !self.delta.retractions.contains(tuple));
|
||||
|
||||
// Chain with assertions
|
||||
let assertions = self.delta.assertions.iter().cloned();
|
||||
|
||||
base_filtered.chain(assertions)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TESTS
|
||||
// ============================================================================
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::universe::Universe;
|
||||
use crate::serialize::save_structure;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[test]
|
||||
fn test_overlay_add_elements() {
|
||||
let dir = tempdir().unwrap();
|
||||
let path = dir.path().join("base.structure");
|
||||
|
||||
// Create and save a base structure
|
||||
let mut universe = Universe::new();
|
||||
let mut base_structure = Structure::new(2);
|
||||
let (a, _) = base_structure.add_element(&mut universe, 0);
|
||||
let (b, _) = base_structure.add_element(&mut universe, 1);
|
||||
save_structure(&base_structure, &path).unwrap();
|
||||
|
||||
// Load as mapped and create overlay
|
||||
let mapped = MappedStructure::open(&path).unwrap();
|
||||
let mut overlay = OverlayStructure::new(Arc::new(mapped));
|
||||
|
||||
assert_eq!(overlay.len(), 2);
|
||||
assert!(overlay.is_clean());
|
||||
|
||||
// Add elements through overlay
|
||||
let luid_c = universe.intern(crate::id::Uuid::now_v7());
|
||||
let c = overlay.add_element(luid_c, 0);
|
||||
|
||||
assert_eq!(overlay.len(), 3);
|
||||
assert!(!overlay.is_clean());
|
||||
assert_eq!(c.index(), 2); // New element gets Slid after base
|
||||
|
||||
// Check element lookups
|
||||
assert_eq!(overlay.get_sort(a), Some(0));
|
||||
assert_eq!(overlay.get_sort(b), Some(1));
|
||||
assert_eq!(overlay.get_sort(c), Some(0));
|
||||
|
||||
// Rollback
|
||||
overlay.rollback();
|
||||
assert_eq!(overlay.len(), 2);
|
||||
assert!(overlay.is_clean());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_overlay_relations() {
|
||||
let dir = tempdir().unwrap();
|
||||
let path = dir.path().join("base.structure");
|
||||
|
||||
// Create base with a relation
|
||||
let mut universe = Universe::new();
|
||||
let mut base_structure = Structure::new(1);
|
||||
let (a, _) = base_structure.add_element(&mut universe, 0);
|
||||
let (b, _) = base_structure.add_element(&mut universe, 0);
|
||||
base_structure.init_relations(&[2]); // binary relation
|
||||
base_structure.assert_relation(0, vec![a, b]);
|
||||
save_structure(&base_structure, &path).unwrap();
|
||||
|
||||
// Load and overlay
|
||||
let mapped = MappedStructure::open(&path).unwrap();
|
||||
let mut overlay = OverlayStructure::new(Arc::new(mapped));
|
||||
|
||||
// Check base relation
|
||||
let rel = overlay.relation(0).unwrap();
|
||||
assert_eq!(rel.arity(), 2);
|
||||
assert!(rel.contains(&[a, b]));
|
||||
assert!(!rel.contains(&[b, a]));
|
||||
|
||||
// Assert new tuple
|
||||
overlay.assert_relation(0, vec![b, a]);
|
||||
let rel = overlay.relation(0).unwrap();
|
||||
assert!(rel.contains(&[a, b]));
|
||||
assert!(rel.contains(&[b, a]));
|
||||
|
||||
// Retract original tuple
|
||||
overlay.retract_relation(0, vec![a, b]);
|
||||
let rel = overlay.relation(0).unwrap();
|
||||
assert!(!rel.contains(&[a, b]));
|
||||
assert!(rel.contains(&[b, a]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_overlay_materialize() {
|
||||
let dir = tempdir().unwrap();
|
||||
let base_path = dir.path().join("base.structure");
|
||||
let new_path = dir.path().join("new.structure");
|
||||
|
||||
// Create base
|
||||
let mut universe = Universe::new();
|
||||
let mut base_structure = Structure::new(1);
|
||||
let (a, _) = base_structure.add_element(&mut universe, 0);
|
||||
base_structure.init_relations(&[1]); // unary relation
|
||||
base_structure.assert_relation(0, vec![a]);
|
||||
save_structure(&base_structure, &base_path).unwrap();
|
||||
|
||||
// Load, modify, commit
|
||||
let mapped = MappedStructure::open(&base_path).unwrap();
|
||||
let mut overlay = OverlayStructure::new(Arc::new(mapped));
|
||||
|
||||
let luid_b = universe.intern(crate::id::Uuid::now_v7());
|
||||
let b = overlay.add_element(luid_b, 0);
|
||||
overlay.assert_relation(0, vec![b]);
|
||||
|
||||
let new_mapped = overlay.commit(&new_path).unwrap();
|
||||
|
||||
// Verify new structure
|
||||
assert_eq!(new_mapped.len(), 2);
|
||||
assert_eq!(new_mapped.num_relations(), 1);
|
||||
let rel = new_mapped.relation(0).unwrap();
|
||||
assert_eq!(rel.live_count(), 2);
|
||||
}
|
||||
}
|
||||
761
src/parser.rs
Normal file
761
src/parser.rs
Normal file
@ -0,0 +1,761 @@
|
||||
//! Parser for Geolog
|
||||
//!
|
||||
//! Parses token streams into AST.
|
||||
|
||||
use chumsky::prelude::*;
|
||||
|
||||
use crate::ast::*;
|
||||
use crate::lexer::{Span, Token};
|
||||
|
||||
/// Create a parser for a complete Geolog file
|
||||
pub fn parser() -> impl Parser<Token, File, Error = Simple<Token>> + Clone {
|
||||
declaration()
|
||||
.map_with_span(|decl, span| Spanned::new(decl, to_span(span)))
|
||||
.repeated()
|
||||
.then_ignore(end())
|
||||
.map(|declarations| File { declarations })
|
||||
}
|
||||
|
||||
fn to_span(span: Span) -> crate::ast::Span {
|
||||
crate::ast::Span::new(span.start, span.end)
|
||||
}
|
||||
|
||||
/// Assign positional names ("0", "1", ...) to unnamed fields in a record
|
||||
/// Only unnamed fields consume positional indices, so named fields can be reordered freely:
|
||||
/// `[a, on: b, c]` → `[("0", a), ("on", b), ("1", c)]`
|
||||
/// `[on: b, a, c]` → `[("on", b), ("0", a), ("1", c)]`
|
||||
///
|
||||
/// Returns Err with the duplicate field name if duplicates are found.
|
||||
fn assign_positional_names_checked<T>(
|
||||
fields: Vec<(Option<String>, T)>,
|
||||
) -> Result<Vec<(String, T)>, String> {
|
||||
let mut positional_idx = 0usize;
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
let mut result = Vec::with_capacity(fields.len());
|
||||
|
||||
for (name, val) in fields {
|
||||
let field_name = match name {
|
||||
Some(n) => n,
|
||||
None => {
|
||||
let n = positional_idx.to_string();
|
||||
positional_idx += 1;
|
||||
n
|
||||
}
|
||||
};
|
||||
|
||||
if !seen.insert(field_name.clone()) {
|
||||
return Err(field_name);
|
||||
}
|
||||
result.push((field_name, val));
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Helpers
|
||||
// ============================================================================
|
||||
|
||||
fn ident() -> impl Parser<Token, String, Error = Simple<Token>> + Clone {
|
||||
select! {
|
||||
Token::Ident(s) => s,
|
||||
// Allow keywords to be used as identifiers (e.g., in paths like ax/child/exists)
|
||||
Token::Namespace => "namespace".to_string(),
|
||||
Token::Theory => "theory".to_string(),
|
||||
Token::Instance => "instance".to_string(),
|
||||
Token::Query => "query".to_string(),
|
||||
Token::Sort => "Sort".to_string(),
|
||||
Token::Prop => "Prop".to_string(),
|
||||
Token::Forall => "forall".to_string(),
|
||||
Token::Exists => "exists".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a path: `foo` or `foo/bar/baz`
|
||||
/// Uses `/` for namespace qualification
|
||||
fn path() -> impl Parser<Token, Path, Error = Simple<Token>> + Clone {
|
||||
ident()
|
||||
.separated_by(just(Token::Slash))
|
||||
.at_least(1)
|
||||
.map(|segments| Path { segments })
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Types (Concatenative Stack-Based Parsing)
|
||||
// ============================================================================
|
||||
|
||||
/// Parse a full type expression with arrows (concatenative style)
|
||||
///
|
||||
/// `A B -> C D -> E` becomes tokens: [A, B, C, D, E, Arrow, Arrow]
|
||||
/// which evaluates right-to-left: A B -> (C D -> E)
|
||||
///
|
||||
/// Uses a single recursive() to handle mutual recursion between type expressions
|
||||
/// (for parentheses and record fields) and atomic type tokens.
|
||||
fn type_expr_impl() -> impl Parser<Token, TypeExpr, Error = Simple<Token>> + Clone {
|
||||
recursive(|type_expr_rec| {
|
||||
// === Atomic type tokens (non-recursive) ===
|
||||
let sort = just(Token::Sort).to(TypeToken::Sort);
|
||||
let prop = just(Token::Prop).to(TypeToken::Prop);
|
||||
let instance = just(Token::Instance).to(TypeToken::Instance);
|
||||
let path_tok = path().map(TypeToken::Path);
|
||||
|
||||
// Record type: [field: Type, ...] or [Type, ...] or mixed
|
||||
// Named field: `name: Type`
|
||||
let named_type_field = ident()
|
||||
.then_ignore(just(Token::Colon))
|
||||
.then(type_expr_rec.clone())
|
||||
.map(|(name, ty)| (Some(name), ty));
|
||||
// Positional field: `Type`
|
||||
let positional_type_field = type_expr_rec.clone().map(|ty| (None, ty));
|
||||
let record_field = choice((named_type_field, positional_type_field));
|
||||
|
||||
let record = record_field
|
||||
.separated_by(just(Token::Comma))
|
||||
.delimited_by(just(Token::LBracket), just(Token::RBracket))
|
||||
.try_map(|fields, span| {
|
||||
assign_positional_names_checked(fields)
|
||||
.map(TypeToken::Record)
|
||||
.map_err(|dup| Simple::custom(span, format!("duplicate field name: {}", dup)))
|
||||
});
|
||||
|
||||
// Single atomic token
|
||||
let single_token = choice((sort, prop, instance, record, path_tok)).map(|t| vec![t]);
|
||||
|
||||
// Parenthesized expression - flatten tokens into parent sequence
|
||||
let paren_expr = type_expr_rec
|
||||
.delimited_by(just(Token::LParen), just(Token::RParen))
|
||||
.map(|expr: TypeExpr| expr.tokens);
|
||||
|
||||
// A "chunk item" is either a paren group or a single token
|
||||
let chunk_item = choice((paren_expr, single_token));
|
||||
|
||||
// A "chunk" is one or more items (before an arrow or end)
|
||||
let chunk = chunk_item
|
||||
.repeated()
|
||||
.at_least(1)
|
||||
.map(|items: Vec<Vec<TypeToken>>| items.into_iter().flatten().collect::<Vec<_>>());
|
||||
|
||||
// Full type expression: chunks separated by arrows
|
||||
chunk
|
||||
.separated_by(just(Token::Arrow))
|
||||
.at_least(1)
|
||||
.map(|chunks: Vec<Vec<TypeToken>>| {
|
||||
// For right-associative arrows:
|
||||
// chunks: [[A, B], [C, D], [E]]
|
||||
// result: [A, B, C, D, E, Arrow, Arrow]
|
||||
//
|
||||
// The evaluator processes Arrow tokens right-to-left:
|
||||
// Stack after all tokens pushed: [A, B, C, D, E]
|
||||
// Arrow 1: pop C,D -> push Arrow{C,D} -> [A, B, Arrow{C,D}, E]
|
||||
// Wait, that's not right either...
|
||||
//
|
||||
// Actually the order should be:
|
||||
// [A, B, Arrow, C, D, Arrow, E] for left-to-right application
|
||||
// But we want (A B) -> ((C D) -> E) for right-associative
|
||||
//
|
||||
// For postfix arrows:
|
||||
// [A, B, C, D, E, Arrow, Arrow] means:
|
||||
// - Push A, B, C, D, E
|
||||
// - Arrow: pop E, pop D -> push Arrow{D,E}
|
||||
// - Arrow: pop Arrow{D,E}, pop C -> push Arrow{C, Arrow{D,E}}
|
||||
// Hmm, this also doesn't work well for multi-token chunks.
|
||||
//
|
||||
// Actually, let's just flatten all and append arrows.
|
||||
// The evaluator will be responsible for parsing chunks correctly.
|
||||
|
||||
let num_arrows = chunks.len() - 1;
|
||||
let mut tokens: Vec<TypeToken> = chunks.into_iter().flatten().collect();
|
||||
|
||||
// Add Arrow tokens at end
|
||||
for _ in 0..num_arrows {
|
||||
tokens.push(TypeToken::Arrow);
|
||||
}
|
||||
|
||||
TypeExpr { tokens }
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse a type expression (full, with arrows)
|
||||
fn type_expr() -> impl Parser<Token, TypeExpr, Error = Simple<Token>> + Clone {
|
||||
type_expr_impl()
|
||||
}
|
||||
|
||||
/// Parse a type expression without top-level arrows (for function domain position)
|
||||
///
|
||||
/// This parses a single "chunk" - type tokens without arrows at the top level.
|
||||
/// Used for places like function domain where we don't want `A -> B` to be ambiguous.
|
||||
fn type_expr_no_arrow() -> impl Parser<Token, TypeExpr, Error = Simple<Token>> + Clone {
|
||||
recursive(|_type_expr_rec| {
|
||||
// Atomic type tokens
|
||||
let sort = just(Token::Sort).to(TypeToken::Sort);
|
||||
let prop = just(Token::Prop).to(TypeToken::Prop);
|
||||
let instance = just(Token::Instance).to(TypeToken::Instance);
|
||||
let path_tok = path().map(TypeToken::Path);
|
||||
|
||||
// Record type: [field: Type, ...] or [Type, ...] or mixed
|
||||
// Named field: `name: Type`
|
||||
let named_type_field = ident()
|
||||
.then_ignore(just(Token::Colon))
|
||||
.then(type_expr_impl())
|
||||
.map(|(name, ty)| (Some(name), ty));
|
||||
// Positional field: `Type`
|
||||
let positional_type_field = type_expr_impl().map(|ty| (None, ty));
|
||||
let record_field = choice((named_type_field, positional_type_field));
|
||||
|
||||
let record = record_field
|
||||
.separated_by(just(Token::Comma))
|
||||
.delimited_by(just(Token::LBracket), just(Token::RBracket))
|
||||
.try_map(|fields, span| {
|
||||
assign_positional_names_checked(fields)
|
||||
.map(TypeToken::Record)
|
||||
.map_err(|dup| Simple::custom(span, format!("duplicate field name: {}", dup)))
|
||||
});
|
||||
|
||||
// Single atomic token
|
||||
let single_token = choice((sort, prop, instance, record, path_tok)).map(|t| vec![t]);
|
||||
|
||||
// Parenthesized expression - can contain full type expr with arrows
|
||||
let paren_expr = type_expr_impl()
|
||||
.delimited_by(just(Token::LParen), just(Token::RParen))
|
||||
.map(|expr: TypeExpr| expr.tokens);
|
||||
|
||||
// A "chunk item" is either a paren group or a single token
|
||||
let chunk_item = choice((paren_expr, single_token));
|
||||
|
||||
// One or more items, no arrows
|
||||
chunk_item
|
||||
.repeated()
|
||||
.at_least(1)
|
||||
.map(|items: Vec<Vec<TypeToken>>| {
|
||||
TypeExpr {
|
||||
tokens: items.into_iter().flatten().collect(),
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Terms
|
||||
// ============================================================================
|
||||
|
||||
fn term() -> impl Parser<Token, Term, Error = Simple<Token>> + Clone {
|
||||
recursive(|term| {
|
||||
let path_term = path().map(Term::Path);
|
||||
|
||||
// Record literal: [field: term, ...] or [term, ...] or mixed
|
||||
// Named field: `name: value`
|
||||
// Positional field: `value` (gets name "0", "1", etc.)
|
||||
let named_field = ident()
|
||||
.then_ignore(just(Token::Colon))
|
||||
.then(term.clone())
|
||||
.map(|(name, val)| (Some(name), val));
|
||||
let positional_field = term.clone().map(|val| (None, val));
|
||||
let record_field = choice((named_field, positional_field));
|
||||
|
||||
let record_term = record_field
|
||||
.separated_by(just(Token::Comma))
|
||||
.delimited_by(just(Token::LBracket), just(Token::RBracket))
|
||||
.try_map(|fields, span| {
|
||||
assign_positional_names_checked(fields)
|
||||
.map(Term::Record)
|
||||
.map_err(|dup| Simple::custom(span, format!("duplicate field name: {}", dup)))
|
||||
});
|
||||
|
||||
// Parenthesized term
|
||||
let paren_term = term
|
||||
.clone()
|
||||
.delimited_by(just(Token::LParen), just(Token::RParen));
|
||||
|
||||
let atom = choice((record_term, paren_term, path_term));
|
||||
|
||||
// Postfix operations:
|
||||
// - Application (juxtaposition): `w W/src` means "apply W/src to w"
|
||||
// - Field projection: `.field` projects a field from a record
|
||||
atom.clone()
|
||||
.then(
|
||||
choice((
|
||||
// Field projection: .field
|
||||
just(Token::Dot)
|
||||
.ignore_then(ident())
|
||||
.map(TermPostfix::Project),
|
||||
// Application: another atom
|
||||
atom.clone().map(TermPostfix::App),
|
||||
))
|
||||
.repeated(),
|
||||
)
|
||||
.foldl(|acc, op| match op {
|
||||
TermPostfix::Project(field) => Term::Project(Box::new(acc), field),
|
||||
TermPostfix::App(arg) => Term::App(Box::new(acc), Box::new(arg)),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum TermPostfix {
|
||||
Project(String),
|
||||
App(Term),
|
||||
}
|
||||
|
||||
/// Parse a record term specifically: [field: term, ...] or [term, ...] or mixed
|
||||
/// Used for relation assertions where we need a standalone record parser.
|
||||
fn record_term() -> impl Parser<Token, Term, Error = Simple<Token>> + Clone {
|
||||
recursive(|rec_term| {
|
||||
let path_term = path().map(Term::Path);
|
||||
let inner_term = choice((rec_term.clone(), path_term.clone()));
|
||||
|
||||
// Named field: `name: value`
|
||||
let named_field = ident()
|
||||
.then_ignore(just(Token::Colon))
|
||||
.then(inner_term.clone())
|
||||
.map(|(name, val)| (Some(name), val));
|
||||
// Positional field: `value`
|
||||
let positional_field = inner_term.map(|val| (None, val));
|
||||
let record_field = choice((named_field, positional_field));
|
||||
|
||||
record_field
|
||||
.separated_by(just(Token::Comma))
|
||||
.delimited_by(just(Token::LBracket), just(Token::RBracket))
|
||||
.try_map(|fields, span| {
|
||||
assign_positional_names_checked(fields)
|
||||
.map(Term::Record)
|
||||
.map_err(|dup| Simple::custom(span, format!("duplicate field name: {}", dup)))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Formulas
|
||||
// ============================================================================
|
||||
|
||||
fn formula() -> impl Parser<Token, Formula, Error = Simple<Token>> + Clone {
|
||||
recursive(|formula| {
|
||||
let quantified_var = ident()
|
||||
.separated_by(just(Token::Comma))
|
||||
.at_least(1)
|
||||
.then_ignore(just(Token::Colon))
|
||||
.then(type_expr())
|
||||
.map(|(names, ty)| QuantifiedVar { names, ty });
|
||||
|
||||
// Existential: exists x : T. phi1, phi2, ...
|
||||
// The body is a conjunction of formulas (comma-separated).
|
||||
// An empty body (exists x : X.) is interpreted as True.
|
||||
// This is standard geometric logic syntax.
|
||||
let exists = just(Token::Exists)
|
||||
.ignore_then(
|
||||
quantified_var
|
||||
.clone()
|
||||
.separated_by(just(Token::Comma))
|
||||
.at_least(1),
|
||||
)
|
||||
.then_ignore(just(Token::Dot))
|
||||
.then(formula.clone().separated_by(just(Token::Comma)))
|
||||
.map(|(vars, body_conjuncts)| {
|
||||
let body = match body_conjuncts.len() {
|
||||
0 => Formula::True,
|
||||
1 => body_conjuncts.into_iter().next().unwrap(),
|
||||
_ => Formula::And(body_conjuncts),
|
||||
};
|
||||
Formula::Exists(vars, Box::new(body))
|
||||
});
|
||||
|
||||
// Parenthesized formula
|
||||
let paren_formula = formula
|
||||
.clone()
|
||||
.delimited_by(just(Token::LParen), just(Token::RParen));
|
||||
|
||||
// Term-based formulas: either equality (term = term) or relation application (term rel)
|
||||
// Since term() greedily parses `base rel` as App(base, Path(rel)),
|
||||
// we detect that pattern when not followed by `=` and convert to RelApp
|
||||
let term_based = term()
|
||||
.then(just(Token::Eq).ignore_then(term()).or_not())
|
||||
.try_map(|(t, opt_rhs), span| {
|
||||
match opt_rhs {
|
||||
Some(rhs) => Ok(Formula::Eq(t, rhs)),
|
||||
None => {
|
||||
// Not equality - check for relation application pattern: term rel
|
||||
match t {
|
||||
Term::App(base, rel_term) => {
|
||||
match *rel_term {
|
||||
Term::Path(path) if path.segments.len() == 1 => {
|
||||
Ok(Formula::RelApp(path.segments[0].clone(), *base))
|
||||
}
|
||||
_ => Err(Simple::custom(span, "expected relation name (single identifier)"))
|
||||
}
|
||||
}
|
||||
_ => Err(Simple::custom(span, "expected relation application (term rel) or equality (term = term)"))
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Literals
|
||||
let true_lit = just(Token::True).to(Formula::True);
|
||||
let false_lit = just(Token::False).to(Formula::False);
|
||||
|
||||
let atom = choice((true_lit, false_lit, exists, paren_formula, term_based));
|
||||
|
||||
// Conjunction: phi /\ psi (binds tighter than disjunction)
|
||||
let conjunction = atom
|
||||
.clone()
|
||||
.then(just(Token::And).ignore_then(atom.clone()).repeated())
|
||||
.foldl(|a, b| {
|
||||
// Flatten into a single And with multiple conjuncts
|
||||
match a {
|
||||
Formula::And(mut conjuncts) => {
|
||||
conjuncts.push(b);
|
||||
Formula::And(conjuncts)
|
||||
}
|
||||
_ => Formula::And(vec![a, b]),
|
||||
}
|
||||
});
|
||||
|
||||
// Disjunction: phi \/ psi
|
||||
conjunction
|
||||
.clone()
|
||||
.then(just(Token::Or).ignore_then(conjunction.clone()).repeated())
|
||||
.foldl(|a, b| {
|
||||
// Flatten into a single Or with multiple disjuncts
|
||||
match a {
|
||||
Formula::Or(mut disjuncts) => {
|
||||
disjuncts.push(b);
|
||||
Formula::Or(disjuncts)
|
||||
}
|
||||
_ => Formula::Or(vec![a, b]),
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Axioms
|
||||
// ============================================================================
|
||||
|
||||
fn axiom_decl() -> impl Parser<Token, AxiomDecl, Error = Simple<Token>> + Clone {
|
||||
let quantified_var = ident()
|
||||
.separated_by(just(Token::Comma))
|
||||
.at_least(1)
|
||||
.then_ignore(just(Token::Colon))
|
||||
.then(type_expr())
|
||||
.map(|(names, ty)| QuantifiedVar { names, ty });
|
||||
|
||||
// Allow empty quantifier list: `forall .` means no universally quantified variables
|
||||
// This is useful for "unconditional" axioms like `forall . |- exists x : X. ...`
|
||||
let quantified_vars = just(Token::Forall)
|
||||
.ignore_then(quantified_var.separated_by(just(Token::Comma)))
|
||||
.then_ignore(just(Token::Dot));
|
||||
|
||||
// Hypotheses before |- (optional, comma separated)
|
||||
let hypotheses = formula()
|
||||
.separated_by(just(Token::Comma))
|
||||
.then_ignore(just(Token::Turnstile));
|
||||
|
||||
// name : forall vars. hyps |- conclusion
|
||||
// Name can be a path like `ax/anc/base`
|
||||
path()
|
||||
.then_ignore(just(Token::Colon))
|
||||
.then(quantified_vars)
|
||||
.then(hypotheses)
|
||||
.then(formula())
|
||||
.map(|(((name, quantified), hypotheses), conclusion)| AxiomDecl {
|
||||
name,
|
||||
quantified,
|
||||
hypotheses,
|
||||
conclusion,
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Theory items
|
||||
// ============================================================================
|
||||
|
||||
fn theory_item() -> impl Parser<Token, TheoryItem, Error = Simple<Token>> + Clone {
|
||||
// Sort declaration: P : Sort;
|
||||
let sort_decl = ident()
|
||||
.then_ignore(just(Token::Colon))
|
||||
.then_ignore(just(Token::Sort))
|
||||
.then_ignore(just(Token::Semicolon))
|
||||
.map(TheoryItem::Sort);
|
||||
|
||||
// Function declaration: name : domain -> codomain;
|
||||
// Name can be a path like `in.src`
|
||||
// Domain is parsed without arrows to avoid ambiguity
|
||||
let function_decl = path()
|
||||
.then_ignore(just(Token::Colon))
|
||||
.then(type_expr_no_arrow())
|
||||
.then_ignore(just(Token::Arrow))
|
||||
.then(type_expr())
|
||||
.then_ignore(just(Token::Semicolon))
|
||||
.map(|((name, domain), codomain)| {
|
||||
TheoryItem::Function(FunctionDecl {
|
||||
name,
|
||||
domain,
|
||||
codomain,
|
||||
})
|
||||
});
|
||||
|
||||
// Axiom: name : forall ... |- ...;
|
||||
let axiom = axiom_decl()
|
||||
.then_ignore(just(Token::Semicolon))
|
||||
.map(TheoryItem::Axiom);
|
||||
|
||||
// Field declaration (catch-all for parameterized theories): name : type;
|
||||
let field_decl = ident()
|
||||
.then_ignore(just(Token::Colon))
|
||||
.then(type_expr())
|
||||
.then_ignore(just(Token::Semicolon))
|
||||
.map(|(name, ty)| TheoryItem::Field(name, ty));
|
||||
|
||||
// Order matters: try more specific patterns first
|
||||
// axiom starts with "ident : forall"
|
||||
// function has "ident : type ->"
|
||||
// sort has "ident : Sort"
|
||||
// field is catch-all "ident : type"
|
||||
choice((axiom, function_decl, sort_decl, field_decl))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Declarations
|
||||
// ============================================================================
|
||||
|
||||
fn param() -> impl Parser<Token, Param, Error = Simple<Token>> + Clone {
|
||||
ident()
|
||||
.then_ignore(just(Token::Colon))
|
||||
.then(type_expr())
|
||||
.map(|(name, ty)| Param { name, ty })
|
||||
}
|
||||
|
||||
fn theory_decl() -> impl Parser<Token, TheoryDecl, Error = Simple<Token>> + Clone {
|
||||
// Optional `extends ParentTheory`
|
||||
let extends_clause = ident()
|
||||
.try_map(|s, span| {
|
||||
if s == "extends" {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Simple::custom(span, "expected 'extends'"))
|
||||
}
|
||||
})
|
||||
.ignore_then(path())
|
||||
.or_not();
|
||||
|
||||
// A param group in parens: (X : Type, Y : Type)
|
||||
let param_group = param()
|
||||
.separated_by(just(Token::Comma))
|
||||
.at_least(1)
|
||||
.delimited_by(just(Token::LParen), just(Token::RParen));
|
||||
|
||||
// After 'theory', we may have:
|
||||
// 1. One or more param groups followed by an identifier: (X:T) (Y:U) Name
|
||||
// 2. Just an identifier (no params): Name
|
||||
// 3. Just '{' (missing name - ERROR)
|
||||
//
|
||||
// Strategy: Parse by looking at the first token after 'theory':
|
||||
// - If '(' -> parse params, then expect name
|
||||
// - If identifier -> that's the name, no params
|
||||
// - If '{' -> error: missing name
|
||||
|
||||
// Helper to parse params then name
|
||||
let params_then_name = param_group
|
||||
.repeated()
|
||||
.at_least(1)
|
||||
.map(|groups: Vec<Vec<Param>>| groups.into_iter().flatten().collect::<Vec<Param>>())
|
||||
.then(ident())
|
||||
.map(|(params, name)| (params, name));
|
||||
|
||||
// No params, just a name
|
||||
let just_name = ident().map(|name| (Vec::<Param>::new(), name));
|
||||
|
||||
// Error case: '{' with no name - emit error at the '{' token's location
|
||||
// Use `just` to peek at '{' and capture its position, then emit a helpful error
|
||||
// We DON'T consume the '{' because we need it for the body parser
|
||||
let missing_name = just(Token::LBrace)
|
||||
.map_with_span(|_, span: Span| span) // Capture '{' token's span
|
||||
.rewind() // Rewind to not consume '{' - we need it for the body
|
||||
.validate(|brace_span, _, emit| {
|
||||
emit(Simple::custom(
|
||||
brace_span,
|
||||
"expected theory name - anonymous theories are not allowed. \
|
||||
Use: theory MyTheoryName { ... }",
|
||||
));
|
||||
// Return dummy values for error recovery
|
||||
(Vec::<Param>::new(), "_anonymous_".to_string())
|
||||
});
|
||||
|
||||
// Parse theory keyword, then params+name in one of the three ways
|
||||
// Order matters: try params first (if '('), then name (if ident), then error (if '{')
|
||||
just(Token::Theory)
|
||||
.ignore_then(choice((params_then_name, just_name, missing_name)))
|
||||
.then(extends_clause)
|
||||
.then(
|
||||
theory_item()
|
||||
.map_with_span(|item, span| Spanned::new(item, to_span(span)))
|
||||
.repeated()
|
||||
.delimited_by(just(Token::LBrace), just(Token::RBrace)),
|
||||
)
|
||||
.map(|(((params, name), extends), body)| TheoryDecl {
|
||||
params,
|
||||
name,
|
||||
extends,
|
||||
body,
|
||||
})
|
||||
}
|
||||
|
||||
fn instance_item() -> impl Parser<Token, InstanceItem, Error = Simple<Token>> + Clone {
|
||||
recursive(|instance_item| {
|
||||
// Nested instance: name = { ... };
|
||||
// Type is inferred from the field declaration in the theory
|
||||
let nested = ident()
|
||||
.then_ignore(just(Token::Eq))
|
||||
.then(
|
||||
instance_item
|
||||
.map_with_span(|item, span| Spanned::new(item, to_span(span)))
|
||||
.repeated()
|
||||
.delimited_by(just(Token::LBrace), just(Token::RBrace)),
|
||||
)
|
||||
.then_ignore(just(Token::Semicolon))
|
||||
.map(|(name, body)| {
|
||||
InstanceItem::NestedInstance(
|
||||
name,
|
||||
InstanceDecl {
|
||||
// Type will be inferred during elaboration
|
||||
theory: TypeExpr::single_path(Path::single("_inferred".to_string())),
|
||||
name: String::new(),
|
||||
body,
|
||||
needs_chase: false,
|
||||
},
|
||||
)
|
||||
});
|
||||
|
||||
// Element declaration: A : P; or a, b, c : P;
|
||||
let element = ident()
|
||||
.separated_by(just(Token::Comma))
|
||||
.at_least(1)
|
||||
.then_ignore(just(Token::Colon))
|
||||
.then(type_expr())
|
||||
.then_ignore(just(Token::Semicolon))
|
||||
.map(|(names, ty)| InstanceItem::Element(names, ty));
|
||||
|
||||
// Equation: term = term;
|
||||
let equation = term()
|
||||
.then_ignore(just(Token::Eq))
|
||||
.then(term())
|
||||
.then_ignore(just(Token::Semicolon))
|
||||
.map(|(l, r)| InstanceItem::Equation(l, r));
|
||||
|
||||
// Relation assertion: [field: value, ...] relation_name; (multi-ary)
|
||||
// or: element relation_name; (unary)
|
||||
// Multi-ary with explicit record
|
||||
let relation_assertion_record = record_term()
|
||||
.then(ident())
|
||||
.then_ignore(just(Token::Semicolon))
|
||||
.map(|(term, rel)| InstanceItem::RelationAssertion(term, rel));
|
||||
|
||||
// Unary relation: element relation_name;
|
||||
// This parses as: path followed by another ident, then semicolon
|
||||
// We wrap the element in a single-field record for uniform handling
|
||||
let relation_assertion_unary = path()
|
||||
.map(Term::Path)
|
||||
.then(ident())
|
||||
.then_ignore(just(Token::Semicolon))
|
||||
.map(|(elem, rel)| InstanceItem::RelationAssertion(elem, rel));
|
||||
|
||||
// Try nested first (ident = {), then element (ident :), then record relation ([ ...),
|
||||
// then unary relation (ident ident ;), then equation (fallback with =)
|
||||
choice((nested, element, relation_assertion_record, relation_assertion_unary, equation))
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse a single type token without 'instance' (for instance declaration headers)
|
||||
fn type_token_no_instance() -> impl Parser<Token, TypeToken, Error = Simple<Token>> + Clone {
|
||||
let sort = just(Token::Sort).to(TypeToken::Sort);
|
||||
let prop = just(Token::Prop).to(TypeToken::Prop);
|
||||
// No instance token here!
|
||||
|
||||
let path_tok = path().map(TypeToken::Path);
|
||||
|
||||
// Record type with full type expressions inside
|
||||
let record_field = ident()
|
||||
.then_ignore(just(Token::Colon))
|
||||
.then(type_expr_impl());
|
||||
|
||||
let record = record_field
|
||||
.separated_by(just(Token::Comma))
|
||||
.delimited_by(just(Token::LBracket), just(Token::RBracket))
|
||||
.map(TypeToken::Record);
|
||||
|
||||
choice((sort, prop, record, path_tok))
|
||||
}
|
||||
|
||||
/// Parse a type expression without the `instance` suffix (for instance declaration headers)
|
||||
fn type_expr_no_instance() -> impl Parser<Token, TypeExpr, Error = Simple<Token>> + Clone {
|
||||
// Parenthesized type - parse inner full type expr
|
||||
let paren_expr = type_expr_impl()
|
||||
.delimited_by(just(Token::LParen), just(Token::RParen))
|
||||
.map(|expr| expr.tokens);
|
||||
|
||||
// Single token (no instance allowed)
|
||||
let single = type_token_no_instance().map(|t| vec![t]);
|
||||
|
||||
// Either paren group or single token
|
||||
let item = choice((paren_expr, single));
|
||||
|
||||
// Collect all tokens
|
||||
item.repeated()
|
||||
.at_least(1)
|
||||
.map(|items| TypeExpr {
|
||||
tokens: items.into_iter().flatten().collect(),
|
||||
})
|
||||
}
|
||||
|
||||
fn instance_decl() -> impl Parser<Token, InstanceDecl, Error = Simple<Token>> + Clone {
|
||||
// Syntax: instance Name : Type = { ... }
|
||||
// or: instance Name : Type = chase { ... }
|
||||
just(Token::Instance)
|
||||
.ignore_then(ident())
|
||||
.then_ignore(just(Token::Colon))
|
||||
.then(type_expr_no_instance())
|
||||
.then_ignore(just(Token::Eq))
|
||||
.then(just(Token::Chase).or_not())
|
||||
.then(
|
||||
instance_item()
|
||||
.map_with_span(|item, span| Spanned::new(item, to_span(span)))
|
||||
.repeated()
|
||||
.delimited_by(just(Token::LBrace), just(Token::RBrace)),
|
||||
)
|
||||
.map(|(((name, theory), needs_chase), body)| InstanceDecl {
|
||||
theory,
|
||||
name,
|
||||
body,
|
||||
needs_chase: needs_chase.is_some(),
|
||||
})
|
||||
}
|
||||
|
||||
fn query_decl() -> impl Parser<Token, QueryDecl, Error = Simple<Token>> + Clone {
|
||||
just(Token::Query)
|
||||
.ignore_then(ident())
|
||||
.then(
|
||||
just(Token::Question)
|
||||
.ignore_then(just(Token::Colon))
|
||||
.ignore_then(type_expr())
|
||||
.then_ignore(just(Token::Semicolon))
|
||||
.delimited_by(just(Token::LBrace), just(Token::RBrace)),
|
||||
)
|
||||
.map(|(name, goal)| QueryDecl { name, goal })
|
||||
}
|
||||
|
||||
fn namespace_decl() -> impl Parser<Token, String, Error = Simple<Token>> + Clone {
|
||||
just(Token::Namespace)
|
||||
.ignore_then(ident())
|
||||
.then_ignore(just(Token::Semicolon))
|
||||
}
|
||||
|
||||
fn declaration() -> impl Parser<Token, Declaration, Error = Simple<Token>> + Clone {
|
||||
choice((
|
||||
namespace_decl().map(Declaration::Namespace),
|
||||
theory_decl().map(Declaration::Theory),
|
||||
instance_decl().map(Declaration::Instance),
|
||||
query_decl().map(Declaration::Query),
|
||||
))
|
||||
}
|
||||
|
||||
// Unit tests moved to tests/unit_parsing.rs
|
||||
688
src/patch.rs
Normal file
688
src/patch.rs
Normal file
@ -0,0 +1,688 @@
|
||||
//! Patch types for version control of geolog structures
|
||||
//!
|
||||
//! A Patch represents the changes between two versions of a Structure.
|
||||
//! Patches are the fundamental unit of version history - each commit
|
||||
//! creates a new patch that can be applied to recreate the structure.
|
||||
|
||||
use crate::core::SortId;
|
||||
use crate::id::{NumericId, Slid, Uuid};
|
||||
use rkyv::{Archive, Deserialize, Serialize};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
/// Changes to the element universe (additions and deletions)
|
||||
///
|
||||
/// Note: Element names are tracked separately in NamingPatch.
|
||||
#[derive(Default, Clone, Debug, PartialEq, Eq, Archive, Deserialize, Serialize)]
|
||||
#[archive(check_bytes)]
|
||||
pub struct ElementPatch {
|
||||
/// Elements removed from structure (by UUID)
|
||||
pub deletions: BTreeSet<Uuid>,
|
||||
/// Elements added: Uuid → sort_id
|
||||
pub additions: BTreeMap<Uuid, SortId>,
|
||||
}
|
||||
|
||||
impl ElementPatch {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.deletions.is_empty() && self.additions.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// Changes to element names (separate from structural changes)
|
||||
///
|
||||
/// Names can change independently of structure (renames), and new elements
|
||||
/// need names. This keeps patches self-contained for version control.
|
||||
#[derive(Default, Clone, Debug, PartialEq, Eq, Archive, Deserialize, Serialize)]
|
||||
#[archive(check_bytes)]
|
||||
pub struct NamingPatch {
|
||||
/// Names removed (by UUID) - typically when element is deleted
|
||||
pub deletions: BTreeSet<Uuid>,
|
||||
/// Names added or changed: UUID → qualified_name path
|
||||
pub additions: BTreeMap<Uuid, Vec<String>>,
|
||||
}
|
||||
|
||||
impl NamingPatch {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.deletions.is_empty() && self.additions.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// Changes to function definitions
|
||||
///
|
||||
/// We track both old and new values to support inversion (for undo).
|
||||
/// The structure uses UUIDs rather than Slids since Slids are unstable
|
||||
/// across different structure versions.
|
||||
#[derive(Default, Clone, Debug, PartialEq, Eq, Archive, Deserialize, Serialize)]
|
||||
#[archive(check_bytes)]
|
||||
pub struct FunctionPatch {
|
||||
/// func_id → (domain_uuid → old_codomain_uuid)
|
||||
/// None means was undefined before
|
||||
pub old_values: BTreeMap<usize, BTreeMap<Uuid, Option<Uuid>>>,
|
||||
/// func_id → (domain_uuid → new_codomain_uuid)
|
||||
pub new_values: BTreeMap<usize, BTreeMap<Uuid, Uuid>>,
|
||||
}
|
||||
|
||||
impl FunctionPatch {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.new_values.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// Changes to relation assertions (tuples added/removed)
|
||||
///
|
||||
/// Tuples are stored as `Vec<Uuid>` since element Slids are unstable across versions.
|
||||
/// We track both assertions and retractions to support inversion.
|
||||
#[derive(Default, Clone, Debug, PartialEq, Eq, Archive, Deserialize, Serialize)]
|
||||
#[archive(check_bytes)]
|
||||
pub struct RelationPatch {
|
||||
/// rel_id → set of tuples retracted (as UUID vectors)
|
||||
pub retractions: BTreeMap<usize, BTreeSet<Vec<Uuid>>>,
|
||||
/// rel_id → set of tuples asserted (as UUID vectors)
|
||||
pub assertions: BTreeMap<usize, BTreeSet<Vec<Uuid>>>,
|
||||
}
|
||||
|
||||
impl RelationPatch {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.assertions.is_empty() && self.retractions.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// A complete patch between two structure versions
|
||||
///
|
||||
/// Patches form a linked list via source_commit → target_commit.
|
||||
/// The initial commit has source_commit = None.
|
||||
///
|
||||
/// Note: Theory reference is stored as a Luid in the Structure, not here.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Archive, Deserialize, Serialize)]
|
||||
#[archive(check_bytes)]
|
||||
pub struct Patch {
|
||||
/// The commit this patch is based on (None for initial commit)
|
||||
pub source_commit: Option<Uuid>,
|
||||
/// The commit this patch creates
|
||||
pub target_commit: Uuid,
|
||||
/// Number of sorts in the theory (needed to rebuild structure)
|
||||
pub num_sorts: usize,
|
||||
/// Number of functions in the theory (needed to rebuild structure)
|
||||
pub num_functions: usize,
|
||||
/// Number of relations in the theory (needed to rebuild structure)
|
||||
pub num_relations: usize,
|
||||
/// Element changes (additions/deletions)
|
||||
pub elements: ElementPatch,
|
||||
/// Function value changes
|
||||
pub functions: FunctionPatch,
|
||||
/// Relation tuple changes (assertions/retractions)
|
||||
pub relations: RelationPatch,
|
||||
/// Name changes (for self-contained patches)
|
||||
pub names: NamingPatch,
|
||||
}
|
||||
|
||||
impl Patch {
|
||||
/// Create a new patch
|
||||
pub fn new(
|
||||
source_commit: Option<Uuid>,
|
||||
num_sorts: usize,
|
||||
num_functions: usize,
|
||||
num_relations: usize,
|
||||
) -> Self {
|
||||
Self {
|
||||
source_commit,
|
||||
target_commit: Uuid::now_v7(),
|
||||
num_sorts,
|
||||
num_functions,
|
||||
num_relations,
|
||||
elements: ElementPatch::default(),
|
||||
functions: FunctionPatch::default(),
|
||||
relations: RelationPatch::default(),
|
||||
names: NamingPatch::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if this patch makes any changes
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.elements.is_empty()
|
||||
&& self.functions.is_empty()
|
||||
&& self.relations.is_empty()
|
||||
&& self.names.is_empty()
|
||||
}
|
||||
|
||||
/// Invert this patch (swap old/new, additions/deletions)
|
||||
///
|
||||
/// Note: Inversion of element additions requires knowing the sort_id of deleted elements,
|
||||
/// which we don't track in deletions. This is a known limitation - sort info is lost on invert.
|
||||
/// Names are fully invertible since we track the full qualified name.
|
||||
/// Relations are fully invertible (assertions ↔ retractions).
|
||||
pub fn invert(&self) -> Patch {
|
||||
Patch {
|
||||
source_commit: Some(self.target_commit),
|
||||
target_commit: self.source_commit.unwrap_or_else(Uuid::now_v7),
|
||||
num_sorts: self.num_sorts,
|
||||
num_functions: self.num_functions,
|
||||
num_relations: self.num_relations,
|
||||
elements: ElementPatch {
|
||||
deletions: self.elements.additions.keys().copied().collect(),
|
||||
additions: self
|
||||
.elements
|
||||
.deletions
|
||||
.iter()
|
||||
.map(|uuid| (*uuid, 0)) // Note: loses sort info on invert
|
||||
.collect(),
|
||||
},
|
||||
functions: FunctionPatch {
|
||||
old_values: self
|
||||
.functions
|
||||
.new_values
|
||||
.iter()
|
||||
.map(|(func_id, changes)| {
|
||||
(
|
||||
*func_id,
|
||||
changes.iter().map(|(k, v)| (*k, Some(*v))).collect(),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
new_values: self
|
||||
.functions
|
||||
.old_values
|
||||
.iter()
|
||||
.filter_map(|(func_id, changes)| {
|
||||
let filtered: BTreeMap<_, _> = changes
|
||||
.iter()
|
||||
.filter_map(|(k, v)| v.map(|v| (*k, v)))
|
||||
.collect();
|
||||
if filtered.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some((*func_id, filtered))
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
},
|
||||
relations: RelationPatch {
|
||||
// Swap assertions ↔ retractions
|
||||
retractions: self.relations.assertions.clone(),
|
||||
assertions: self.relations.retractions.clone(),
|
||||
},
|
||||
names: NamingPatch {
|
||||
deletions: self.names.additions.keys().copied().collect(),
|
||||
additions: self
|
||||
.names
|
||||
.deletions
|
||||
.iter()
|
||||
.map(|uuid| (*uuid, vec![])) // Note: loses name on invert (would need old_names tracking)
|
||||
.collect(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Diff and Apply operations ============
|
||||
|
||||
use crate::core::{RelationStorage, Structure};
|
||||
use crate::id::{Luid, get_slid, some_slid};
|
||||
use crate::naming::NamingIndex;
|
||||
use crate::universe::Universe;
|
||||
|
||||
/// Create a patch representing the difference from `old` to `new`.
|
||||
///
|
||||
/// The resulting patch, when applied to `old`, produces `new`.
|
||||
/// Requires Universe for UUID lookup and NamingIndex for name changes.
|
||||
pub fn diff(
|
||||
old: &Structure,
|
||||
new: &Structure,
|
||||
universe: &Universe,
|
||||
old_naming: &NamingIndex,
|
||||
new_naming: &NamingIndex,
|
||||
) -> Patch {
|
||||
let mut patch = Patch::new(
|
||||
None, // Will be set by caller if needed
|
||||
new.num_sorts(),
|
||||
new.num_functions(),
|
||||
new.relations.len(),
|
||||
);
|
||||
|
||||
// Find element deletions: elements in old but not in new
|
||||
for &luid in old.luids.iter() {
|
||||
if !new.luid_to_slid.contains_key(&luid)
|
||||
&& let Some(uuid) = universe.get(luid) {
|
||||
patch.elements.deletions.insert(uuid);
|
||||
// Also mark name as deleted
|
||||
patch.names.deletions.insert(uuid);
|
||||
}
|
||||
}
|
||||
|
||||
// Find element additions: elements in new but not in old
|
||||
for (slid, &luid) in new.luids.iter().enumerate() {
|
||||
if !old.luid_to_slid.contains_key(&luid)
|
||||
&& let Some(uuid) = universe.get(luid) {
|
||||
patch.elements.additions.insert(uuid, new.sorts[slid]);
|
||||
// Also add name from new_naming
|
||||
if let Some(name) = new_naming.get(&uuid) {
|
||||
patch.names.additions.insert(uuid, name.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find name changes for elements that exist in both
|
||||
for &luid in new.luids.iter() {
|
||||
if old.luid_to_slid.contains_key(&luid) {
|
||||
// Element exists in both - check for name change
|
||||
if let Some(uuid) = universe.get(luid) {
|
||||
let old_name = old_naming.get(&uuid);
|
||||
let new_name = new_naming.get(&uuid);
|
||||
if old_name != new_name
|
||||
&& let Some(name) = new_name {
|
||||
patch.names.additions.insert(uuid, name.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find function value changes
|
||||
// We need to compare function values for elements that exist in both
|
||||
for func_id in 0..new.num_functions() {
|
||||
if func_id >= old.num_functions() {
|
||||
// New function added to schema - all its values are additions
|
||||
// Record each defined value with old_value = None
|
||||
let Some(new_func_col) = new.functions[func_id].as_local() else { continue };
|
||||
for (sort_slid, opt_codomain) in new_func_col.iter().enumerate() {
|
||||
if let Some(new_codomain_slid) = get_slid(*opt_codomain) {
|
||||
// Find UUIDs for domain and codomain
|
||||
let domain_uuid = find_uuid_by_sort_slid(new, universe, func_id, sort_slid);
|
||||
if let Some(domain_uuid) = domain_uuid {
|
||||
let new_codomain_luid = new.luids[new_codomain_slid.index()];
|
||||
if let Some(new_codomain_uuid) = universe.get(new_codomain_luid) {
|
||||
// Record: this domain element now maps to this codomain element
|
||||
// (was undefined before since function didn't exist)
|
||||
patch.functions.old_values
|
||||
.entry(func_id)
|
||||
.or_default()
|
||||
.insert(domain_uuid, None);
|
||||
patch.functions.new_values
|
||||
.entry(func_id)
|
||||
.or_default()
|
||||
.insert(domain_uuid, new_codomain_uuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut old_vals: BTreeMap<Uuid, Option<Uuid>> = BTreeMap::new();
|
||||
let mut new_vals: BTreeMap<Uuid, Uuid> = BTreeMap::new();
|
||||
|
||||
// Iterate over elements in the new structure's function domain
|
||||
// Note: patches only work with local functions currently
|
||||
let Some(new_func_col) = new.functions[func_id].as_local() else { continue };
|
||||
let Some(old_func_col) = old.functions[func_id].as_local() else { continue };
|
||||
|
||||
for (sort_slid, opt_codomain) in new_func_col.iter().enumerate() {
|
||||
// Find the UUID for this domain element
|
||||
if let Some(new_codomain_slid) = get_slid(*opt_codomain) {
|
||||
let domain_uuid = find_uuid_by_sort_slid(new, universe, func_id, sort_slid);
|
||||
if let Some(domain_uuid) = domain_uuid {
|
||||
let new_codomain_luid = new.luids[new_codomain_slid.index()];
|
||||
let new_codomain_uuid = universe.get(new_codomain_luid);
|
||||
|
||||
if let Some(new_codomain_uuid) = new_codomain_uuid {
|
||||
// Check if this element existed in old (by looking up its luid)
|
||||
let domain_luid = find_luid_by_sort_slid(new, func_id, sort_slid);
|
||||
if let Some(domain_luid) = domain_luid {
|
||||
if let Some(&old_domain_slid) = old.luid_to_slid.get(&domain_luid) {
|
||||
let old_sort_slid = old.sort_local_id(old_domain_slid);
|
||||
let old_codomain = get_slid(old_func_col[old_sort_slid.index()]);
|
||||
|
||||
match old_codomain {
|
||||
Some(old_codomain_slid) => {
|
||||
let old_codomain_luid = old.luids[old_codomain_slid.index()];
|
||||
if let Some(old_codomain_uuid) =
|
||||
universe.get(old_codomain_luid)
|
||||
&& old_codomain_uuid != new_codomain_uuid {
|
||||
// Value changed
|
||||
old_vals
|
||||
.insert(domain_uuid, Some(old_codomain_uuid));
|
||||
new_vals.insert(domain_uuid, new_codomain_uuid);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// Was undefined, now defined
|
||||
old_vals.insert(domain_uuid, None);
|
||||
new_vals.insert(domain_uuid, new_codomain_uuid);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Domain element is new - function value is part of the addition
|
||||
new_vals.insert(domain_uuid, new_codomain_uuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !new_vals.is_empty() {
|
||||
patch.functions.old_values.insert(func_id, old_vals);
|
||||
patch.functions.new_values.insert(func_id, new_vals);
|
||||
}
|
||||
}
|
||||
|
||||
// Find relation changes
|
||||
// Compare tuples in each relation between old and new
|
||||
let num_relations = new.relations.len().min(old.relations.len());
|
||||
for rel_id in 0..num_relations {
|
||||
let old_rel = &old.relations[rel_id];
|
||||
let new_rel = &new.relations[rel_id];
|
||||
|
||||
// Helper: convert a Slid tuple to UUID tuple
|
||||
let slid_tuple_to_uuids = |tuple: &[Slid], structure: &Structure| -> Option<Vec<Uuid>> {
|
||||
tuple
|
||||
.iter()
|
||||
.map(|&slid| {
|
||||
let luid = structure.luids[slid.index()];
|
||||
universe.get(luid)
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
|
||||
// Find tuples in old but not in new (retractions)
|
||||
let mut retractions: BTreeSet<Vec<Uuid>> = BTreeSet::new();
|
||||
for tuple in old_rel.iter() {
|
||||
// Check if this tuple (by UUID) exists in new
|
||||
if let Some(uuid_tuple) = slid_tuple_to_uuids(tuple, old) {
|
||||
// See if we can find the same UUID tuple in new
|
||||
let exists_in_new = new_rel.iter().any(|new_tuple| {
|
||||
slid_tuple_to_uuids(new_tuple, new)
|
||||
.map(|new_uuids| new_uuids == uuid_tuple)
|
||||
.unwrap_or(false)
|
||||
});
|
||||
if !exists_in_new {
|
||||
retractions.insert(uuid_tuple);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find tuples in new but not in old (assertions)
|
||||
let mut assertions: BTreeSet<Vec<Uuid>> = BTreeSet::new();
|
||||
for tuple in new_rel.iter() {
|
||||
if let Some(uuid_tuple) = slid_tuple_to_uuids(tuple, new) {
|
||||
let exists_in_old = old_rel.iter().any(|old_tuple| {
|
||||
slid_tuple_to_uuids(old_tuple, old)
|
||||
.map(|old_uuids| old_uuids == uuid_tuple)
|
||||
.unwrap_or(false)
|
||||
});
|
||||
if !exists_in_old {
|
||||
assertions.insert(uuid_tuple);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !retractions.is_empty() {
|
||||
patch.relations.retractions.insert(rel_id, retractions);
|
||||
}
|
||||
if !assertions.is_empty() {
|
||||
patch.relations.assertions.insert(rel_id, assertions);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle new relations in new that don't exist in old
|
||||
for rel_id in num_relations..new.relations.len() {
|
||||
let new_rel = &new.relations[rel_id];
|
||||
let mut assertions: BTreeSet<Vec<Uuid>> = BTreeSet::new();
|
||||
|
||||
for tuple in new_rel.iter() {
|
||||
let uuid_tuple: Option<Vec<Uuid>> = tuple
|
||||
.iter()
|
||||
.map(|&slid| {
|
||||
let luid = new.luids[slid.index()];
|
||||
universe.get(luid)
|
||||
})
|
||||
.collect();
|
||||
if let Some(uuids) = uuid_tuple {
|
||||
assertions.insert(uuids);
|
||||
}
|
||||
}
|
||||
|
||||
if !assertions.is_empty() {
|
||||
patch.relations.assertions.insert(rel_id, assertions);
|
||||
}
|
||||
}
|
||||
|
||||
patch
|
||||
}
|
||||
|
||||
/// Helper to find the Luid of an element given its func_id and sort_slid in a structure
|
||||
fn find_luid_by_sort_slid(structure: &Structure, func_id: usize, sort_slid: usize) -> Option<Luid> {
|
||||
let func_col_len = structure.functions[func_id].len();
|
||||
for (slid_idx, &_sort_id) in structure.sorts.iter().enumerate() {
|
||||
let slid = Slid::from_usize(slid_idx);
|
||||
let elem_sort_slid = structure.sort_local_id(slid);
|
||||
if elem_sort_slid.index() == sort_slid && func_col_len > sort_slid {
|
||||
return Some(structure.luids[slid_idx]);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Helper to find the UUID of an element given its func_id and sort_slid in a structure
|
||||
fn find_uuid_by_sort_slid(
|
||||
structure: &Structure,
|
||||
universe: &Universe,
|
||||
func_id: usize,
|
||||
sort_slid: usize,
|
||||
) -> Option<Uuid> {
|
||||
find_luid_by_sort_slid(structure, func_id, sort_slid).and_then(|luid| universe.get(luid))
|
||||
}
|
||||
|
||||
/// Apply a patch to create a new structure and update naming index.
|
||||
///
|
||||
/// Returns Ok(new_structure) on success, or Err with a description of what went wrong.
|
||||
/// Requires a Universe to convert UUIDs from the patch to Luids.
|
||||
/// The naming parameter is updated with name changes from the patch.
|
||||
pub fn apply_patch(
|
||||
base: &Structure,
|
||||
patch: &Patch,
|
||||
universe: &mut Universe,
|
||||
naming: &mut NamingIndex,
|
||||
) -> Result<Structure, String> {
|
||||
// Create a new structure
|
||||
let mut result = Structure::new(patch.num_sorts);
|
||||
|
||||
// Build a set of deleted UUIDs for quick lookup
|
||||
let deleted_uuids: std::collections::HashSet<Uuid> =
|
||||
patch.elements.deletions.iter().copied().collect();
|
||||
|
||||
// Copy elements from base that weren't deleted
|
||||
for (slid, &luid) in base.luids.iter().enumerate() {
|
||||
let uuid = universe.get(luid).ok_or("Unknown luid in base structure")?;
|
||||
if !deleted_uuids.contains(&uuid) {
|
||||
result.add_element_with_luid(luid, base.sorts[slid]);
|
||||
}
|
||||
}
|
||||
|
||||
// Add new elements from the patch (register UUIDs in universe)
|
||||
for (uuid, sort_id) in &patch.elements.additions {
|
||||
result.add_element_with_uuid(universe, *uuid, *sort_id);
|
||||
}
|
||||
|
||||
// Apply naming changes
|
||||
for uuid in &patch.names.deletions {
|
||||
// Note: NamingIndex doesn't have a remove method yet, skip for now
|
||||
let _ = uuid;
|
||||
}
|
||||
for (uuid, name) in &patch.names.additions {
|
||||
naming.insert(*uuid, name.clone());
|
||||
}
|
||||
|
||||
// Initialize function storage
|
||||
let domain_sort_ids: Vec<Option<SortId>> = (0..patch.num_functions)
|
||||
.map(|func_id| {
|
||||
if func_id < base.functions.len() && !base.functions[func_id].is_empty() {
|
||||
let func_len = base.functions[func_id].len();
|
||||
for (sort_id, carrier) in base.carriers.iter().enumerate() {
|
||||
if carrier.len() as usize == func_len {
|
||||
return Some(sort_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
})
|
||||
.collect();
|
||||
|
||||
result.init_functions(&domain_sort_ids);
|
||||
|
||||
// Copy function values from base (for non-deleted elements)
|
||||
// Note: patches only work with local functions currently
|
||||
for func_id in 0..base.num_functions().min(result.num_functions()) {
|
||||
let Some(base_func_col) = base.functions[func_id].as_local() else { continue };
|
||||
if !result.functions[func_id].is_local() { continue };
|
||||
|
||||
// Collect all the updates we need to make (to avoid borrow checker issues)
|
||||
let mut updates: Vec<(usize, Slid)> = Vec::new();
|
||||
|
||||
for (old_sort_slid, opt_codomain) in base_func_col.iter().enumerate() {
|
||||
if let Some(old_codomain_slid) = get_slid(*opt_codomain) {
|
||||
// Find the domain element's Luid
|
||||
let domain_luid = find_luid_by_sort_slid(base, func_id, old_sort_slid);
|
||||
if let Some(domain_luid) = domain_luid {
|
||||
// Check if domain element still exists in result
|
||||
if let Some(&new_domain_slid) = result.luid_to_slid.get(&domain_luid) {
|
||||
// Check if codomain element still exists
|
||||
let codomain_luid = base.luids[old_codomain_slid.index()];
|
||||
if let Some(&new_codomain_slid) = result.luid_to_slid.get(&codomain_luid) {
|
||||
let new_sort_slid = result.sort_local_id(new_domain_slid);
|
||||
updates.push((new_sort_slid.index(), new_codomain_slid));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply updates
|
||||
if let Some(result_func_col) = result.functions[func_id].as_local_mut() {
|
||||
for (idx, codomain_slid) in updates {
|
||||
if idx < result_func_col.len() {
|
||||
result_func_col[idx] = some_slid(codomain_slid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply function value changes from patch (using UUIDs → Luids)
|
||||
// Note: patches only work with local functions currently
|
||||
for (func_id, changes) in &patch.functions.new_values {
|
||||
if *func_id < result.num_functions() && result.functions[*func_id].is_local() {
|
||||
// Collect updates first to avoid borrow checker issues
|
||||
let mut updates: Vec<(usize, Slid)> = Vec::new();
|
||||
for (domain_uuid, codomain_uuid) in changes {
|
||||
let domain_luid = universe.lookup(domain_uuid);
|
||||
let codomain_luid = universe.lookup(codomain_uuid);
|
||||
if let (Some(domain_luid), Some(codomain_luid)) = (domain_luid, codomain_luid)
|
||||
&& let (Some(&domain_slid), Some(&codomain_slid)) = (
|
||||
result.luid_to_slid.get(&domain_luid),
|
||||
result.luid_to_slid.get(&codomain_luid),
|
||||
)
|
||||
{
|
||||
let sort_slid = result.sort_local_id(domain_slid);
|
||||
updates.push((sort_slid.index(), codomain_slid));
|
||||
}
|
||||
}
|
||||
|
||||
// Apply updates
|
||||
if let Some(result_func_col) = result.functions[*func_id].as_local_mut() {
|
||||
for (idx, codomain_slid) in updates {
|
||||
if idx < result_func_col.len() {
|
||||
result_func_col[idx] = some_slid(codomain_slid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize relation storage
|
||||
// Infer arities from base if available, otherwise from patch assertions
|
||||
let relation_arities: Vec<usize> = (0..patch.num_relations)
|
||||
.map(|rel_id| {
|
||||
// Try base first
|
||||
if rel_id < base.relations.len() {
|
||||
base.relations[rel_id].arity()
|
||||
} else if let Some(assertions) = patch.relations.assertions.get(&rel_id) {
|
||||
// Infer from first assertion
|
||||
assertions.iter().next().map(|t| t.len()).unwrap_or(0)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
result.init_relations(&relation_arities);
|
||||
|
||||
// Copy relation tuples from base (for non-deleted elements)
|
||||
for rel_id in 0..base.relations.len().min(patch.num_relations) {
|
||||
let base_rel = &base.relations[rel_id];
|
||||
|
||||
for tuple in base_rel.iter() {
|
||||
// Convert Slid tuple to UUID tuple to check if still valid
|
||||
let uuid_tuple: Option<Vec<Uuid>> = tuple
|
||||
.iter()
|
||||
.map(|&slid| {
|
||||
let luid = base.luids[slid.index()];
|
||||
universe.get(luid)
|
||||
})
|
||||
.collect();
|
||||
|
||||
if let Some(uuid_tuple) = uuid_tuple {
|
||||
// Check if this tuple should be retracted
|
||||
let should_retract = patch
|
||||
.relations
|
||||
.retractions
|
||||
.get(&rel_id)
|
||||
.map(|r| r.contains(&uuid_tuple))
|
||||
.unwrap_or(false);
|
||||
|
||||
if !should_retract {
|
||||
// Check all elements still exist and convert to new Slids
|
||||
let new_tuple: Option<Vec<Slid>> = uuid_tuple
|
||||
.iter()
|
||||
.map(|uuid| {
|
||||
universe
|
||||
.lookup(uuid)
|
||||
.and_then(|luid| result.luid_to_slid.get(&luid).copied())
|
||||
})
|
||||
.collect();
|
||||
|
||||
if let Some(new_tuple) = new_tuple {
|
||||
result.assert_relation(rel_id, new_tuple);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply relation assertions from patch
|
||||
for (rel_id, assertions) in &patch.relations.assertions {
|
||||
if *rel_id < patch.num_relations {
|
||||
for uuid_tuple in assertions {
|
||||
let slid_tuple: Option<Vec<Slid>> = uuid_tuple
|
||||
.iter()
|
||||
.map(|uuid| {
|
||||
universe
|
||||
.lookup(uuid)
|
||||
.and_then(|luid| result.luid_to_slid.get(&luid).copied())
|
||||
})
|
||||
.collect();
|
||||
|
||||
if let Some(slid_tuple) = slid_tuple {
|
||||
result.assert_relation(*rel_id, slid_tuple);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Create a patch representing a structure from empty (initial commit)
|
||||
pub fn to_initial_patch(structure: &Structure, universe: &Universe, naming: &NamingIndex) -> Patch {
|
||||
let empty = Structure::new(structure.num_sorts());
|
||||
let empty_naming = NamingIndex::new();
|
||||
diff(&empty, structure, universe, &empty_naming, naming)
|
||||
}
|
||||
|
||||
// Unit tests moved to tests/proptest_patch.rs
|
||||
424
src/pretty.rs
Normal file
424
src/pretty.rs
Normal file
@ -0,0 +1,424 @@
|
||||
//! Pretty-printer for Geolog AST
|
||||
//!
|
||||
//! Renders AST back to source syntax for round-trip testing.
|
||||
|
||||
use crate::ast::*;
|
||||
|
||||
/// Pretty-print configuration
|
||||
pub struct PrettyConfig {
|
||||
pub indent: usize,
|
||||
}
|
||||
|
||||
impl Default for PrettyConfig {
|
||||
fn default() -> Self {
|
||||
Self { indent: 2 }
|
||||
}
|
||||
}
|
||||
|
||||
/// A pretty-printer with indentation tracking
|
||||
pub struct Pretty {
|
||||
output: String,
|
||||
indent_level: usize,
|
||||
config: PrettyConfig,
|
||||
}
|
||||
|
||||
impl Default for Pretty {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Pretty {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
output: String::new(),
|
||||
indent_level: 0,
|
||||
config: PrettyConfig::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn finish(self) -> String {
|
||||
self.output
|
||||
}
|
||||
|
||||
fn indent(&mut self) {
|
||||
for _ in 0..(self.indent_level * self.config.indent) {
|
||||
self.output.push(' ');
|
||||
}
|
||||
}
|
||||
|
||||
fn write(&mut self, s: &str) {
|
||||
self.output.push_str(s);
|
||||
}
|
||||
|
||||
fn writeln(&mut self, s: &str) {
|
||||
self.output.push_str(s);
|
||||
self.output.push('\n');
|
||||
}
|
||||
|
||||
fn newline(&mut self) {
|
||||
self.output.push('\n');
|
||||
}
|
||||
|
||||
fn inc_indent(&mut self) {
|
||||
self.indent_level += 1;
|
||||
}
|
||||
|
||||
fn dec_indent(&mut self) {
|
||||
self.indent_level = self.indent_level.saturating_sub(1);
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Pretty-printing implementations ============
|
||||
|
||||
impl Pretty {
|
||||
pub fn file(&mut self, file: &File) {
|
||||
for (i, decl) in file.declarations.iter().enumerate() {
|
||||
if i > 0 {
|
||||
self.newline();
|
||||
}
|
||||
self.declaration(&decl.node);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn declaration(&mut self, decl: &Declaration) {
|
||||
match decl {
|
||||
Declaration::Namespace(name) => {
|
||||
self.write("namespace ");
|
||||
self.write(name);
|
||||
self.writeln(";");
|
||||
}
|
||||
Declaration::Theory(t) => self.theory_decl(t),
|
||||
Declaration::Instance(i) => self.instance_decl(i),
|
||||
Declaration::Query(q) => self.query_decl(q),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn theory_decl(&mut self, t: &TheoryDecl) {
|
||||
self.write("theory ");
|
||||
for param in &t.params {
|
||||
self.write("(");
|
||||
self.write(¶m.name);
|
||||
self.write(" : ");
|
||||
self.type_expr(¶m.ty);
|
||||
self.write(") ");
|
||||
}
|
||||
self.write(&t.name);
|
||||
self.writeln(" {");
|
||||
self.inc_indent();
|
||||
for item in &t.body {
|
||||
self.indent();
|
||||
self.theory_item(&item.node);
|
||||
self.newline();
|
||||
}
|
||||
self.dec_indent();
|
||||
self.writeln("}");
|
||||
}
|
||||
|
||||
pub fn theory_item(&mut self, item: &TheoryItem) {
|
||||
match item {
|
||||
TheoryItem::Sort(name) => {
|
||||
self.write(name);
|
||||
self.write(" : Sort;");
|
||||
}
|
||||
TheoryItem::Function(f) => {
|
||||
self.write(&f.name.to_string());
|
||||
self.write(" : ");
|
||||
self.type_expr(&f.domain);
|
||||
self.write(" -> ");
|
||||
self.type_expr(&f.codomain);
|
||||
self.write(";");
|
||||
}
|
||||
TheoryItem::Axiom(a) => self.axiom_decl(a),
|
||||
TheoryItem::Field(name, ty) => {
|
||||
self.write(name);
|
||||
self.write(" : ");
|
||||
self.type_expr(ty);
|
||||
self.write(";");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn axiom_decl(&mut self, a: &AxiomDecl) {
|
||||
self.write(&a.name.to_string());
|
||||
self.write(" : forall ");
|
||||
for (i, qv) in a.quantified.iter().enumerate() {
|
||||
if i > 0 {
|
||||
self.write(", ");
|
||||
}
|
||||
self.write(&qv.names.join(", "));
|
||||
self.write(" : ");
|
||||
self.type_expr(&qv.ty);
|
||||
}
|
||||
self.write(". ");
|
||||
|
||||
// Hypotheses (if any)
|
||||
if !a.hypotheses.is_empty() {
|
||||
for (i, hyp) in a.hypotheses.iter().enumerate() {
|
||||
if i > 0 {
|
||||
self.write(", ");
|
||||
}
|
||||
self.formula(hyp);
|
||||
}
|
||||
self.write(" ");
|
||||
}
|
||||
|
||||
self.write("|- ");
|
||||
self.formula(&a.conclusion);
|
||||
self.write(";");
|
||||
}
|
||||
|
||||
pub fn type_expr(&mut self, ty: &TypeExpr) {
|
||||
use crate::ast::TypeToken;
|
||||
|
||||
let mut need_space = false;
|
||||
|
||||
for token in &ty.tokens {
|
||||
match token {
|
||||
TypeToken::Sort => {
|
||||
if need_space {
|
||||
self.write(" ");
|
||||
}
|
||||
self.write("Sort");
|
||||
need_space = true;
|
||||
}
|
||||
TypeToken::Prop => {
|
||||
if need_space {
|
||||
self.write(" ");
|
||||
}
|
||||
self.write("Prop");
|
||||
need_space = true;
|
||||
}
|
||||
TypeToken::Path(p) => {
|
||||
if need_space {
|
||||
self.write(" ");
|
||||
}
|
||||
self.write(&p.to_string());
|
||||
need_space = true;
|
||||
}
|
||||
TypeToken::Instance => {
|
||||
self.write(" instance");
|
||||
need_space = true;
|
||||
}
|
||||
TypeToken::Arrow => {
|
||||
// Arrows are inserted between chunks
|
||||
// This simplistic approach just prints " -> " when we see Arrow
|
||||
self.write(" -> ");
|
||||
need_space = false;
|
||||
}
|
||||
TypeToken::Record(fields) => {
|
||||
if need_space {
|
||||
self.write(" ");
|
||||
}
|
||||
self.write("[");
|
||||
for (i, (name, field_ty)) in fields.iter().enumerate() {
|
||||
if i > 0 {
|
||||
self.write(", ");
|
||||
}
|
||||
self.write(name);
|
||||
self.write(" : ");
|
||||
self.type_expr(field_ty);
|
||||
}
|
||||
self.write("]");
|
||||
need_space = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Print a type expression that might need parentheses
|
||||
#[allow(dead_code)]
|
||||
fn type_expr_atom(&mut self, ty: &TypeExpr) {
|
||||
use crate::ast::TypeToken;
|
||||
|
||||
// Check if this needs parentheses (has arrows or multiple paths)
|
||||
let has_arrow = ty.tokens.iter().any(|t| matches!(t, TypeToken::Arrow));
|
||||
let path_count = ty
|
||||
.tokens
|
||||
.iter()
|
||||
.filter(|t| matches!(t, TypeToken::Path(_)))
|
||||
.count();
|
||||
|
||||
if has_arrow || path_count > 1 {
|
||||
self.write("(");
|
||||
self.type_expr(ty);
|
||||
self.write(")");
|
||||
} else {
|
||||
self.type_expr(ty);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn term(&mut self, t: &Term) {
|
||||
match t {
|
||||
Term::Path(p) => self.write(&p.to_string()),
|
||||
Term::App(f, a) => {
|
||||
self.term(f);
|
||||
self.write(" ");
|
||||
self.term_atom(a);
|
||||
}
|
||||
Term::Project(t, field) => {
|
||||
self.term(t);
|
||||
self.write(" .");
|
||||
self.write(field);
|
||||
}
|
||||
Term::Record(fields) => {
|
||||
self.write("[");
|
||||
for (i, (name, val)) in fields.iter().enumerate() {
|
||||
if i > 0 {
|
||||
self.write(", ");
|
||||
}
|
||||
self.write(name);
|
||||
self.write(": ");
|
||||
self.term(val);
|
||||
}
|
||||
self.write("]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Print a term that might need parentheses
|
||||
fn term_atom(&mut self, t: &Term) {
|
||||
match t {
|
||||
Term::App(_, _) | Term::Project(_, _) => {
|
||||
self.write("(");
|
||||
self.term(t);
|
||||
self.write(")");
|
||||
}
|
||||
_ => self.term(t),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn formula(&mut self, f: &Formula) {
|
||||
match f {
|
||||
Formula::True => self.write("true"),
|
||||
Formula::False => self.write("false"),
|
||||
Formula::RelApp(rel_name, arg) => {
|
||||
// Postfix relation application: term rel
|
||||
self.term(arg);
|
||||
self.write(" ");
|
||||
self.write(rel_name);
|
||||
}
|
||||
Formula::Eq(l, r) => {
|
||||
self.term(l);
|
||||
self.write(" = ");
|
||||
self.term(r);
|
||||
}
|
||||
Formula::And(conjuncts) => {
|
||||
for (i, c) in conjuncts.iter().enumerate() {
|
||||
if i > 0 {
|
||||
self.write(", ");
|
||||
}
|
||||
self.formula(c);
|
||||
}
|
||||
}
|
||||
Formula::Or(disjuncts) => {
|
||||
for (i, d) in disjuncts.iter().enumerate() {
|
||||
if i > 0 {
|
||||
self.write(" \\/ ");
|
||||
}
|
||||
self.formula_atom(d);
|
||||
}
|
||||
}
|
||||
Formula::Exists(vars, body) => {
|
||||
self.write("(exists ");
|
||||
for (i, qv) in vars.iter().enumerate() {
|
||||
if i > 0 {
|
||||
self.write(", ");
|
||||
}
|
||||
self.write(&qv.names.join(", "));
|
||||
self.write(" : ");
|
||||
self.type_expr(&qv.ty);
|
||||
}
|
||||
self.write(". ");
|
||||
self.formula(body);
|
||||
self.write(")");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Print a formula that might need parentheses
|
||||
fn formula_atom(&mut self, f: &Formula) {
|
||||
match f {
|
||||
Formula::Or(_) | Formula::And(_) => {
|
||||
self.write("(");
|
||||
self.formula(f);
|
||||
self.write(")");
|
||||
}
|
||||
_ => self.formula(f),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn instance_decl(&mut self, i: &InstanceDecl) {
|
||||
self.write("instance ");
|
||||
self.write(&i.name);
|
||||
self.write(" : ");
|
||||
self.type_expr(&i.theory);
|
||||
self.writeln(" = {");
|
||||
self.inc_indent();
|
||||
for item in &i.body {
|
||||
self.indent();
|
||||
self.instance_item(&item.node);
|
||||
self.newline();
|
||||
}
|
||||
self.dec_indent();
|
||||
self.writeln("}");
|
||||
}
|
||||
|
||||
pub fn instance_item(&mut self, item: &InstanceItem) {
|
||||
match item {
|
||||
InstanceItem::Element(names, ty) => {
|
||||
self.write(&names.join(", "));
|
||||
self.write(" : ");
|
||||
self.type_expr(ty);
|
||||
self.write(";");
|
||||
}
|
||||
InstanceItem::Equation(lhs, rhs) => {
|
||||
self.term(lhs);
|
||||
self.write(" = ");
|
||||
self.term(rhs);
|
||||
self.write(";");
|
||||
}
|
||||
InstanceItem::NestedInstance(name, inner) => {
|
||||
self.write(name);
|
||||
self.writeln(" = {");
|
||||
self.inc_indent();
|
||||
for item in &inner.body {
|
||||
self.indent();
|
||||
self.instance_item(&item.node);
|
||||
self.newline();
|
||||
}
|
||||
self.dec_indent();
|
||||
self.indent();
|
||||
self.write("};");
|
||||
}
|
||||
InstanceItem::RelationAssertion(term, rel) => {
|
||||
self.term(term);
|
||||
self.write(" ");
|
||||
self.write(rel);
|
||||
self.write(";");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn query_decl(&mut self, q: &QueryDecl) {
|
||||
self.write("query ");
|
||||
self.write(&q.name);
|
||||
self.writeln(" {");
|
||||
self.inc_indent();
|
||||
self.indent();
|
||||
self.write("? : ");
|
||||
self.type_expr(&q.goal);
|
||||
self.writeln(";");
|
||||
self.dec_indent();
|
||||
self.writeln("}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience function to pretty-print a file
|
||||
pub fn pretty_print(file: &File) -> String {
|
||||
let mut p = Pretty::new();
|
||||
p.file(file);
|
||||
p.finish()
|
||||
}
|
||||
|
||||
// Unit tests moved to tests/unit_pretty.rs
|
||||
1650
src/query/backend.rs
Normal file
1650
src/query/backend.rs
Normal file
File diff suppressed because it is too large
Load Diff
710
src/query/chase.rs
Normal file
710
src/query/chase.rs
Normal file
@ -0,0 +1,710 @@
|
||||
//! Chase algorithm for computing derived relations.
|
||||
//!
|
||||
//! The chase takes a structure and a set of axioms (sequents) and repeatedly
|
||||
//! applies the axioms until a fixpoint is reached. This is the standard database
|
||||
//! chase algorithm adapted for geometric logic.
|
||||
//!
|
||||
//! # Implementation
|
||||
//!
|
||||
//! This implementation uses the tensor subsystem to evaluate premises:
|
||||
//! 1. Compile premise to TensorExpr (handles existentials, conjunctions, etc.)
|
||||
//! 2. Materialize to get all satisfying variable assignments
|
||||
//! 3. For each assignment, fire the conclusion (add relations, create elements)
|
||||
//!
|
||||
//! This approach is strictly more powerful than query-based chase because
|
||||
//! the tensor system naturally handles existential quantification in premises
|
||||
//! via tensor contraction.
|
||||
//!
|
||||
//! # Supported Axiom Patterns
|
||||
//!
|
||||
//! **Premises** (anything the tensor system can compile):
|
||||
//! - Relations: `R(x,y)`
|
||||
//! - Conjunctions: `R(x,y), S(y,z)`
|
||||
//! - Existentials: `∃e. f(e) = x ∧ g(e) = y`
|
||||
//! - Equalities: `f(x) = y`, `f(x) = g(y)`
|
||||
//! - Disjunctions: `R(x) ∨ S(x)`
|
||||
//!
|
||||
//! **Conclusions**:
|
||||
//! - Relations: `⊢ R(x,y)` — add tuple to relation
|
||||
//! - Existentials: `⊢ ∃b. f(b) = y` — create element with function binding
|
||||
//! - Conjunctions: `⊢ R(x,y), f(x) = z` — multiple effects
|
||||
//!
|
||||
//! # Usage
|
||||
//!
|
||||
//! ```ignore
|
||||
//! use geolog::query::chase::chase_fixpoint;
|
||||
//!
|
||||
//! // Run chase to fixpoint
|
||||
//! let iterations = chase_fixpoint(
|
||||
//! &theory.theory.axioms,
|
||||
//! &mut structure,
|
||||
//! &mut universe,
|
||||
//! &theory.theory.signature,
|
||||
//! 100,
|
||||
//! )?;
|
||||
//! ```
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::cc::{CongruenceClosure, EquationReason};
|
||||
use crate::core::{DerivedSort, Formula, RelationStorage, Sequent, Signature, Structure, Term};
|
||||
use crate::id::{NumericId, Slid};
|
||||
use crate::tensor::{check_sequent, CheckResult};
|
||||
use crate::universe::Universe;
|
||||
|
||||
/// Error type for chase operations
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ChaseError {
|
||||
/// Unsupported formula in conclusion
|
||||
UnsupportedConclusion(String),
|
||||
/// Variable not bound
|
||||
UnboundVariable(String),
|
||||
/// Function conflict (different values for same input)
|
||||
FunctionConflict(String),
|
||||
/// Chase did not converge
|
||||
MaxIterationsExceeded(usize),
|
||||
/// Tensor compilation failed
|
||||
TensorCompilationFailed(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ChaseError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::UnsupportedConclusion(s) => write!(f, "Unsupported conclusion: {s}"),
|
||||
Self::UnboundVariable(s) => write!(f, "Unbound variable: {s}"),
|
||||
Self::FunctionConflict(s) => write!(f, "Function conflict: {s}"),
|
||||
Self::MaxIterationsExceeded(n) => write!(f, "Chase did not converge after {n} iterations"),
|
||||
Self::TensorCompilationFailed(s) => write!(f, "Tensor compilation failed: {s}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ChaseError {}
|
||||
|
||||
/// Variable binding: maps variable names to Slids
|
||||
pub type Binding = HashMap<String, Slid>;
|
||||
|
||||
/// Execute one step of the chase algorithm.
|
||||
///
|
||||
/// Iterates over all axioms, evaluates premises using the tensor system,
|
||||
/// and fires conclusions for each satisfying assignment.
|
||||
///
|
||||
/// Returns `true` if any changes were made.
|
||||
pub fn chase_step(
|
||||
axioms: &[Sequent],
|
||||
structure: &mut Structure,
|
||||
cc: &mut CongruenceClosure,
|
||||
universe: &mut Universe,
|
||||
sig: &Signature,
|
||||
) -> Result<bool, ChaseError> {
|
||||
let mut changed = false;
|
||||
|
||||
for axiom in axioms {
|
||||
changed |= fire_axiom(axiom, structure, cc, universe, sig)?;
|
||||
}
|
||||
|
||||
Ok(changed)
|
||||
}
|
||||
|
||||
/// Fire a single axiom: find violations using tensor system, fire conclusion only for violations.
|
||||
///
|
||||
/// This is the key to correct chase semantics: we only create fresh elements when
|
||||
/// the tensor system confirms there is NO existing witness for the conclusion.
|
||||
fn fire_axiom(
|
||||
axiom: &Sequent,
|
||||
structure: &mut Structure,
|
||||
cc: &mut CongruenceClosure,
|
||||
universe: &mut Universe,
|
||||
sig: &Signature,
|
||||
) -> Result<bool, ChaseError> {
|
||||
// Check the axiom - if compilation fails due to unsupported patterns, skip silently
|
||||
let violations = match check_sequent(axiom, structure, sig) {
|
||||
Ok(CheckResult::Satisfied) => {
|
||||
// Axiom is already satisfied - nothing to fire
|
||||
return Ok(false);
|
||||
}
|
||||
Ok(CheckResult::Violated(vs)) => vs,
|
||||
Err(_) => {
|
||||
// Tensor compilation failed (unsupported pattern)
|
||||
// Skip this axiom silently
|
||||
return Ok(false);
|
||||
}
|
||||
};
|
||||
|
||||
if violations.is_empty() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// Build index→Slid lookup for each context variable
|
||||
let index_to_slid: Vec<Vec<Slid>> = axiom.context.vars.iter()
|
||||
.map(|(_, sort)| carrier_to_slid_vec(structure, sort))
|
||||
.collect();
|
||||
|
||||
// Map from variable name to its position in the context
|
||||
let var_to_ctx_idx: HashMap<&str, usize> = axiom.context.vars.iter()
|
||||
.enumerate()
|
||||
.map(|(i, (name, _))| (name.as_str(), i))
|
||||
.collect();
|
||||
|
||||
let mut changed = false;
|
||||
|
||||
// Fire conclusion ONLY for violations (where premise holds but conclusion doesn't)
|
||||
for violation in violations {
|
||||
// Build binding from violation assignment
|
||||
let binding: Binding = violation.variable_names.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(tensor_idx, var_name)| {
|
||||
let ctx_idx = var_to_ctx_idx.get(var_name.as_str())?;
|
||||
let slid_vec = &index_to_slid[*ctx_idx];
|
||||
let tensor_val = violation.assignment.get(tensor_idx)?;
|
||||
let slid = slid_vec.get(*tensor_val)?;
|
||||
Some((var_name.clone(), *slid))
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Fire conclusion with this binding
|
||||
match fire_conclusion(&axiom.conclusion, &binding, structure, cc, universe, sig) {
|
||||
Ok(c) => changed |= c,
|
||||
Err(_) => {
|
||||
// Unsupported conclusion pattern - skip this axiom silently
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(changed)
|
||||
}
|
||||
|
||||
/// Convert a carrier to a Vec of Slids for index→Slid lookup
|
||||
fn carrier_to_slid_vec(structure: &Structure, sort: &DerivedSort) -> Vec<Slid> {
|
||||
match sort {
|
||||
DerivedSort::Base(sort_id) => {
|
||||
structure.carriers[*sort_id]
|
||||
.iter()
|
||||
.map(|u| Slid::from_usize(u as usize))
|
||||
.collect()
|
||||
}
|
||||
DerivedSort::Product(_) => {
|
||||
// Product sorts: would need to enumerate all combinations
|
||||
// For now, return empty (these are rare in practice)
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Fire a conclusion formula given a variable binding.
|
||||
/// Returns true if any changes were made.
|
||||
fn fire_conclusion(
|
||||
formula: &Formula,
|
||||
binding: &Binding,
|
||||
structure: &mut Structure,
|
||||
cc: &mut CongruenceClosure,
|
||||
universe: &mut Universe,
|
||||
sig: &Signature,
|
||||
) -> Result<bool, ChaseError> {
|
||||
match formula {
|
||||
Formula::True => Ok(false),
|
||||
|
||||
Formula::False => {
|
||||
// Contradiction - this shouldn't happen in valid chase
|
||||
Err(ChaseError::UnsupportedConclusion("False in conclusion".to_string()))
|
||||
}
|
||||
|
||||
Formula::Rel(rel_id, term) => {
|
||||
// Add tuple to relation
|
||||
let tuple = eval_term_to_tuple(term, binding, structure)?;
|
||||
|
||||
// Check if already present (using canonical representatives)
|
||||
let canonical_tuple: Vec<Slid> = tuple.iter()
|
||||
.map(|&s| cc.canonical(s))
|
||||
.collect();
|
||||
|
||||
// Check if a canonically-equivalent tuple exists
|
||||
let exists = structure.relations[*rel_id].iter().any(|existing| {
|
||||
if existing.len() != canonical_tuple.len() {
|
||||
return false;
|
||||
}
|
||||
existing.iter().zip(canonical_tuple.iter()).all(|(e, c)| {
|
||||
cc.canonical(*e) == *c
|
||||
})
|
||||
});
|
||||
|
||||
if exists {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
structure.relations[*rel_id].insert(tuple);
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
Formula::Conj(formulas) => {
|
||||
let mut changed = false;
|
||||
for f in formulas {
|
||||
changed |= fire_conclusion(f, binding, structure, cc, universe, sig)?;
|
||||
}
|
||||
Ok(changed)
|
||||
}
|
||||
|
||||
Formula::Disj(formulas) => {
|
||||
// Naive parallel chase: fire all disjuncts
|
||||
// (sound but potentially adds more facts than necessary)
|
||||
let mut changed = false;
|
||||
for f in formulas {
|
||||
changed |= fire_conclusion(f, binding, structure, cc, universe, sig)?;
|
||||
}
|
||||
Ok(changed)
|
||||
}
|
||||
|
||||
Formula::Eq(left, right) => {
|
||||
fire_equality(left, right, binding, structure, cc, sig)
|
||||
}
|
||||
|
||||
Formula::Exists(var_name, sort, body) => {
|
||||
fire_existential(var_name, sort, body, binding, structure, cc, universe, sig)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Evaluate a term to a tuple of Slids (for relation arguments)
|
||||
fn eval_term_to_tuple(
|
||||
term: &Term,
|
||||
binding: &Binding,
|
||||
structure: &Structure,
|
||||
) -> Result<Vec<Slid>, ChaseError> {
|
||||
match term {
|
||||
Term::Var(name, _) => {
|
||||
let slid = binding.get(name)
|
||||
.ok_or_else(|| ChaseError::UnboundVariable(name.clone()))?;
|
||||
Ok(vec![*slid])
|
||||
}
|
||||
Term::Record(fields) => {
|
||||
let mut tuple = Vec::new();
|
||||
for (_, field_term) in fields {
|
||||
tuple.extend(eval_term_to_tuple(field_term, binding, structure)?);
|
||||
}
|
||||
Ok(tuple)
|
||||
}
|
||||
Term::App(_, _) => {
|
||||
// Delegate to eval_term_to_slid which handles function application
|
||||
let result = eval_term_to_slid(term, binding, structure)?;
|
||||
Ok(vec![result])
|
||||
}
|
||||
Term::Project(_, _) => {
|
||||
Err(ChaseError::UnsupportedConclusion(
|
||||
"Projection in relation argument".to_string()
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Evaluate a term to a single Slid
|
||||
fn eval_term_to_slid(
|
||||
term: &Term,
|
||||
binding: &Binding,
|
||||
structure: &Structure,
|
||||
) -> Result<Slid, ChaseError> {
|
||||
match term {
|
||||
Term::Var(name, _) => {
|
||||
binding.get(name)
|
||||
.copied()
|
||||
.ok_or_else(|| ChaseError::UnboundVariable(name.clone()))
|
||||
}
|
||||
Term::App(func_idx, arg) => {
|
||||
let arg_slid = eval_term_to_slid(arg, binding, structure)?;
|
||||
let local_id = structure.sort_local_id(arg_slid);
|
||||
|
||||
structure.get_function(*func_idx, local_id)
|
||||
.ok_or_else(|| ChaseError::UnboundVariable(
|
||||
format!("Function {} undefined at {:?}", func_idx, arg_slid)
|
||||
))
|
||||
}
|
||||
Term::Project(base, field) => {
|
||||
let _base_slid = eval_term_to_slid(base, binding, structure)?;
|
||||
// Product projection - would need more structure info
|
||||
Err(ChaseError::UnsupportedConclusion(
|
||||
format!("Projection .{} not yet supported in chase", field)
|
||||
))
|
||||
}
|
||||
Term::Record(_) => {
|
||||
Err(ChaseError::UnsupportedConclusion(
|
||||
"Record term in scalar position".to_string()
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Fire an equality in conclusion: f(x) = y, x = y, etc.
|
||||
fn fire_equality(
|
||||
left: &Term,
|
||||
right: &Term,
|
||||
binding: &Binding,
|
||||
structure: &mut Structure,
|
||||
cc: &mut CongruenceClosure,
|
||||
sig: &Signature,
|
||||
) -> Result<bool, ChaseError> {
|
||||
match (left, right) {
|
||||
// f(arg) = value
|
||||
(Term::App(func_idx, arg), value) | (value, Term::App(func_idx, arg)) => {
|
||||
let arg_slid = eval_term_to_slid(arg, binding, structure)?;
|
||||
let local_id = structure.sort_local_id(arg_slid);
|
||||
|
||||
// Check if dealing with product codomain
|
||||
let func_info = &sig.functions[*func_idx];
|
||||
match &func_info.codomain {
|
||||
DerivedSort::Base(_) => {
|
||||
// Simple codomain
|
||||
let value_slid = eval_term_to_slid(value, binding, structure)?;
|
||||
|
||||
// Check if already defined
|
||||
if let Some(existing) = structure.get_function(*func_idx, local_id) {
|
||||
// Check if values are equal (using CC)
|
||||
if cc.are_equal(existing, value_slid) {
|
||||
return Ok(false); // Already set to equivalent value
|
||||
}
|
||||
// Function conflict: add equation to CC instead of error
|
||||
// (this is how we propagate equalities through functions)
|
||||
cc.add_equation(existing, value_slid, EquationReason::FunctionConflict {
|
||||
func_id: *func_idx,
|
||||
domain: arg_slid,
|
||||
});
|
||||
return Ok(true); // Changed (added equation)
|
||||
}
|
||||
|
||||
structure.define_function(*func_idx, arg_slid, value_slid)
|
||||
.map_err(|e| ChaseError::FunctionConflict(format!("{:?}", e)))?;
|
||||
Ok(true)
|
||||
}
|
||||
DerivedSort::Product(_fields) => {
|
||||
// Product codomain: f(x) = [field1: v1, ...]
|
||||
if let Term::Record(value_fields) = value {
|
||||
let codomain_values: Vec<(&str, Slid)> = value_fields.iter()
|
||||
.map(|(name, term)| {
|
||||
let slid = eval_term_to_slid(term, binding, structure)?;
|
||||
Ok((name.as_str(), slid))
|
||||
})
|
||||
.collect::<Result<Vec<_>, ChaseError>>()?;
|
||||
|
||||
// Check if already defined
|
||||
if let Some(existing) = structure.get_function_product_codomain(*func_idx, local_id) {
|
||||
let all_match = codomain_values.iter().all(|(name, expected)| {
|
||||
existing.iter().any(|(n, v)| n == name && cc.are_equal(*v, *expected))
|
||||
});
|
||||
if all_match {
|
||||
return Ok(false);
|
||||
}
|
||||
return Err(ChaseError::FunctionConflict(
|
||||
format!("Function {} already defined at {:?} with different values", func_idx, arg_slid)
|
||||
));
|
||||
}
|
||||
|
||||
structure.define_function_product_codomain(*func_idx, arg_slid, &codomain_values)
|
||||
.map_err(|e| ChaseError::FunctionConflict(format!("{:?}", e)))?;
|
||||
Ok(true)
|
||||
} else {
|
||||
Err(ChaseError::UnsupportedConclusion(
|
||||
format!("Expected record for product codomain function, got {:?}", value)
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// x = y (variable equality) - add to congruence closure!
|
||||
(Term::Var(name1, _), Term::Var(name2, _)) => {
|
||||
let slid1 = binding.get(name1)
|
||||
.ok_or_else(|| ChaseError::UnboundVariable(name1.clone()))?;
|
||||
let slid2 = binding.get(name2)
|
||||
.ok_or_else(|| ChaseError::UnboundVariable(name2.clone()))?;
|
||||
|
||||
// Check if already equal in CC
|
||||
if cc.are_equal(*slid1, *slid2) {
|
||||
Ok(false) // Already equivalent
|
||||
} else {
|
||||
// Add equation to congruence closure
|
||||
cc.add_equation(*slid1, *slid2, EquationReason::ChaseConclusion);
|
||||
Ok(true) // Changed!
|
||||
}
|
||||
}
|
||||
|
||||
_ => Err(ChaseError::UnsupportedConclusion(
|
||||
format!("Unsupported equality pattern: {:?} = {:?}", left, right)
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a formula is satisfied given a variable binding.
|
||||
/// This is used for witness search in existential conclusions.
|
||||
/// Uses CC for canonical relation lookups and equality checks.
|
||||
fn check_formula_satisfied(
|
||||
formula: &Formula,
|
||||
binding: &Binding,
|
||||
structure: &Structure,
|
||||
cc: &mut CongruenceClosure,
|
||||
) -> bool {
|
||||
match formula {
|
||||
Formula::True => true,
|
||||
Formula::False => false,
|
||||
|
||||
Formula::Rel(rel_id, term) => {
|
||||
// Check if the tuple is in the relation (using canonical representatives)
|
||||
if let Ok(tuple) = eval_term_to_tuple(term, binding, structure) {
|
||||
let canonical_tuple: Vec<Slid> = tuple.iter()
|
||||
.map(|&s| cc.canonical(s))
|
||||
.collect();
|
||||
|
||||
// Check if a canonically-equivalent tuple exists
|
||||
structure.relations[*rel_id].iter().any(|existing| {
|
||||
if existing.len() != canonical_tuple.len() {
|
||||
return false;
|
||||
}
|
||||
existing.iter().zip(canonical_tuple.iter()).all(|(e, c)| {
|
||||
cc.canonical(*e) == *c
|
||||
})
|
||||
})
|
||||
} else {
|
||||
false // Couldn't evaluate term (unbound variable)
|
||||
}
|
||||
}
|
||||
|
||||
Formula::Conj(fs) => {
|
||||
fs.iter().all(|f| check_formula_satisfied(f, binding, structure, cc))
|
||||
}
|
||||
|
||||
Formula::Disj(fs) => {
|
||||
fs.iter().any(|f| check_formula_satisfied(f, binding, structure, cc))
|
||||
}
|
||||
|
||||
Formula::Eq(t1, t2) => {
|
||||
// Check if both terms evaluate to equivalent values (using CC)
|
||||
match (eval_term_to_slid(t1, binding, structure), eval_term_to_slid(t2, binding, structure)) {
|
||||
(Ok(s1), Ok(s2)) => cc.are_equal(s1, s2),
|
||||
_ => false // Couldn't evaluate (unbound variable or undefined function)
|
||||
}
|
||||
}
|
||||
|
||||
Formula::Exists(inner_var, inner_sort, inner_body) => {
|
||||
// Check if any witness exists in the carrier
|
||||
let DerivedSort::Base(sort_idx) = inner_sort else {
|
||||
return false; // Product sorts not supported
|
||||
};
|
||||
|
||||
structure.carriers[*sort_idx].iter().any(|w_u64| {
|
||||
let witness = Slid::from_usize(w_u64 as usize);
|
||||
let mut extended = binding.clone();
|
||||
extended.insert(inner_var.clone(), witness);
|
||||
check_formula_satisfied(inner_body, &extended, structure, cc)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Fire an existential in conclusion: ∃x:S. body
|
||||
/// This creates a new element if no witness exists.
|
||||
///
|
||||
/// The algorithm:
|
||||
/// 1. Search the carrier of S for an existing witness w where body[x↦w] holds
|
||||
/// 2. If found, do nothing (witness exists)
|
||||
/// 3. If not found, create a fresh element w and fire body as conclusion with x↦w
|
||||
fn fire_existential(
|
||||
var_name: &str,
|
||||
sort: &DerivedSort,
|
||||
body: &Formula,
|
||||
binding: &Binding,
|
||||
structure: &mut Structure,
|
||||
cc: &mut CongruenceClosure,
|
||||
universe: &mut Universe,
|
||||
sig: &Signature,
|
||||
) -> Result<bool, ChaseError> {
|
||||
let DerivedSort::Base(sort_idx) = sort else {
|
||||
return Err(ChaseError::UnsupportedConclusion(
|
||||
"Existential with product sort not yet supported".to_string()
|
||||
));
|
||||
};
|
||||
|
||||
// Search for existing witness by checking if body is satisfied (using CC for canonical lookups)
|
||||
let carrier = &structure.carriers[*sort_idx];
|
||||
let witness_found = carrier.iter().any(|elem_u64| {
|
||||
let elem_slid = Slid::from_usize(elem_u64 as usize);
|
||||
let mut extended_binding = binding.clone();
|
||||
extended_binding.insert(var_name.to_string(), elem_slid);
|
||||
check_formula_satisfied(body, &extended_binding, structure, cc)
|
||||
});
|
||||
|
||||
if witness_found {
|
||||
return Ok(false); // Witness already exists, nothing to do
|
||||
}
|
||||
|
||||
// No witness exists - create a fresh element
|
||||
let (new_elem, _) = structure.add_element(universe, *sort_idx);
|
||||
|
||||
// Fire body as conclusion with the new element bound to var_name
|
||||
let mut extended_binding = binding.clone();
|
||||
extended_binding.insert(var_name.to_string(), new_elem);
|
||||
|
||||
// Use fire_conclusion to make the body true
|
||||
// This handles relations, equalities, conjunctions uniformly
|
||||
fire_conclusion(body, &extended_binding, structure, cc, universe, sig)?;
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Run the chase algorithm until a fixpoint is reached, with congruence closure.
|
||||
///
|
||||
/// Repeatedly applies [`chase_step`] and propagates equations until no more changes occur.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `axioms` - The sequents (axioms) to apply
|
||||
/// * `structure` - The structure to modify
|
||||
/// * `cc` - Congruence closure for equality reasoning
|
||||
/// * `universe` - The universe for element creation
|
||||
/// * `sig` - The signature
|
||||
/// * `max_iterations` - Safety limit to prevent infinite loops
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The number of iterations taken to reach the fixpoint.
|
||||
pub fn chase_fixpoint_with_cc(
|
||||
axioms: &[Sequent],
|
||||
structure: &mut Structure,
|
||||
cc: &mut CongruenceClosure,
|
||||
universe: &mut Universe,
|
||||
sig: &Signature,
|
||||
max_iterations: usize,
|
||||
) -> Result<usize, ChaseError> {
|
||||
let mut iterations = 0;
|
||||
|
||||
loop {
|
||||
if iterations >= max_iterations {
|
||||
return Err(ChaseError::MaxIterationsExceeded(max_iterations));
|
||||
}
|
||||
|
||||
// Fire axiom conclusions
|
||||
let axiom_changed = chase_step(axioms, structure, cc, universe, sig)?;
|
||||
|
||||
// Propagate pending equations in CC
|
||||
let eq_changed = propagate_equations(structure, cc, sig);
|
||||
|
||||
iterations += 1;
|
||||
|
||||
if !axiom_changed && !eq_changed {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(iterations)
|
||||
}
|
||||
|
||||
/// Propagate pending equations in the congruence closure.
|
||||
///
|
||||
/// This merges equivalence classes and detects function conflicts
|
||||
/// (which add new equations via congruence).
|
||||
fn propagate_equations(
|
||||
structure: &Structure,
|
||||
cc: &mut CongruenceClosure,
|
||||
sig: &Signature,
|
||||
) -> bool {
|
||||
let mut changed = false;
|
||||
|
||||
while let Some(eq) = cc.pop_pending() {
|
||||
// Merge the equivalence classes
|
||||
if cc.merge(eq.lhs, eq.rhs) {
|
||||
changed = true;
|
||||
|
||||
// Check for function conflicts (congruence propagation)
|
||||
// If f(a) = x and f(b) = y, and a = b (just merged), then x = y
|
||||
for func_id in 0..sig.functions.len() {
|
||||
if func_id >= structure.functions.len() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let lhs_local = structure.sort_local_id(eq.lhs);
|
||||
let rhs_local = structure.sort_local_id(eq.rhs);
|
||||
|
||||
let lhs_val = structure.get_function(func_id, lhs_local);
|
||||
let rhs_val = structure.get_function(func_id, rhs_local);
|
||||
|
||||
if let (Some(lv), Some(rv)) = (lhs_val, rhs_val)
|
||||
&& !cc.are_equal(lv, rv) {
|
||||
// Congruence: f(a) = lv, f(b) = rv, a = b implies lv = rv
|
||||
cc.add_equation(lv, rv, EquationReason::Congruence { func_id });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
changed
|
||||
}
|
||||
|
||||
/// Canonicalize the structure based on the congruence closure.
|
||||
///
|
||||
/// After the chase, some elements may have been merged in the CC but the
|
||||
/// structure still contains distinct elements. This function:
|
||||
/// 1. Removes non-canonical elements from carriers
|
||||
/// 2. Replaces relation tuples with their canonical forms
|
||||
fn canonicalize_structure(structure: &mut Structure, cc: &mut CongruenceClosure) {
|
||||
use crate::core::{RelationStorage, VecRelation};
|
||||
|
||||
// 1. Canonicalize carriers: keep only canonical representatives
|
||||
for carrier in &mut structure.carriers {
|
||||
let elements: Vec<u64> = carrier.iter().collect();
|
||||
carrier.clear();
|
||||
for elem in elements {
|
||||
let slid = Slid::from_usize(elem as usize);
|
||||
let canonical = cc.canonical(slid);
|
||||
// Only keep if this element is its own canonical representative
|
||||
if canonical == slid {
|
||||
carrier.insert(elem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Canonicalize relations: replace tuples with canonical forms
|
||||
for rel in &mut structure.relations {
|
||||
let canonical_tuples: Vec<Vec<Slid>> = rel.iter()
|
||||
.map(|tuple| tuple.iter().map(|&s| cc.canonical(s)).collect())
|
||||
.collect();
|
||||
|
||||
let arity = rel.arity();
|
||||
let mut new_rel = VecRelation::new(arity);
|
||||
for tuple in canonical_tuples {
|
||||
new_rel.insert(tuple);
|
||||
}
|
||||
|
||||
*rel = new_rel;
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the chase algorithm until a fixpoint is reached.
|
||||
///
|
||||
/// This is a convenience wrapper that creates a fresh congruence closure.
|
||||
/// Use [`chase_fixpoint_with_cc`] if you need to provide your own CC.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `axioms` - The sequents (axioms) to apply
|
||||
/// * `structure` - The structure to modify
|
||||
/// * `universe` - The universe for element creation
|
||||
/// * `sig` - The signature
|
||||
/// * `max_iterations` - Safety limit to prevent infinite loops
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The number of iterations taken to reach the fixpoint.
|
||||
pub fn chase_fixpoint(
|
||||
axioms: &[Sequent],
|
||||
structure: &mut Structure,
|
||||
universe: &mut Universe,
|
||||
sig: &Signature,
|
||||
max_iterations: usize,
|
||||
) -> Result<usize, ChaseError> {
|
||||
let mut cc = CongruenceClosure::new();
|
||||
let iterations = chase_fixpoint_with_cc(axioms, structure, &mut cc, universe, sig, max_iterations)?;
|
||||
|
||||
// Canonicalize structure to reflect CC merges before returning
|
||||
canonicalize_structure(structure, &mut cc);
|
||||
|
||||
Ok(iterations)
|
||||
}
|
||||
|
||||
// Tests are in tests/unit_chase.rs
|
||||
702
src/query/compile.rs
Normal file
702
src/query/compile.rs
Normal file
@ -0,0 +1,702 @@
|
||||
//! Query compiler: high-level queries → QueryOp plans.
|
||||
//!
|
||||
//! This module compiles query specifications into executable QueryOp plans.
|
||||
//! It supports:
|
||||
//! - Single-sort queries (like `Pattern`)
|
||||
//! - Multi-sort queries with joins
|
||||
//! - Function application and projection
|
||||
//!
|
||||
//! # Query Styles
|
||||
//!
|
||||
//! **∀-style (open sorts):** Elements determined by constraints.
|
||||
//! Compiled to relational algebra (scan, filter, join, project).
|
||||
//!
|
||||
//! **∃-style (closed sorts):** Elements are declared constants.
|
||||
//! Compiled to constraint satisfaction (witness enumeration).
|
||||
//! [Not yet implemented]
|
||||
//!
|
||||
//! # Design
|
||||
//!
|
||||
//! Query compilation is currently direct (Query → QueryOp).
|
||||
//! A future homoiconic version would compile to RelAlgIR instances,
|
||||
//! which would then be interpreted by the backend.
|
||||
|
||||
use crate::id::Slid;
|
||||
use super::backend::{JoinCond, Predicate, QueryOp};
|
||||
|
||||
/// A query specification that can involve multiple sorts and joins.
|
||||
///
|
||||
/// This generalizes `Pattern` to handle:
|
||||
/// - Multiple source sorts
|
||||
/// - Joins between sorts
|
||||
/// - Complex constraints across sorts
|
||||
///
|
||||
/// # Example: Find all Func where Func/theory == target
|
||||
///
|
||||
/// ```ignore
|
||||
/// let query = Query::scan(func_sort)
|
||||
/// .filter_eq(theory_func, 0, target_slid)
|
||||
/// .build();
|
||||
/// ```
|
||||
///
|
||||
/// # Example: Find all (Srt, Func) pairs where Srt/theory == Func/theory
|
||||
///
|
||||
/// ```ignore
|
||||
/// let query = Query::scan(srt_sort)
|
||||
/// .join_scan(func_sort)
|
||||
/// .join_on_func(srt_theory_func, 0, func_theory_func, 1)
|
||||
/// .build();
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Query {
|
||||
/// Sources: each is (sort_idx, alias). Alias is used in constraints.
|
||||
sources: Vec<Source>,
|
||||
/// Constraints to apply (filters and join conditions)
|
||||
constraints: Vec<Constraint>,
|
||||
/// Projection: which columns to return
|
||||
projection: Projection,
|
||||
}
|
||||
|
||||
/// A source in the query (a sort to scan).
|
||||
#[derive(Debug, Clone)]
|
||||
struct Source {
|
||||
/// Sort index to scan
|
||||
sort_idx: usize,
|
||||
/// Column offset in the combined tuple
|
||||
/// (each source adds 1 column for its element)
|
||||
#[allow(dead_code)] // Used for tracking, will be needed for complex projections
|
||||
col_offset: usize,
|
||||
}
|
||||
|
||||
/// A constraint in the query.
|
||||
#[derive(Debug, Clone)]
|
||||
enum Constraint {
|
||||
/// func(col) == constant
|
||||
FuncEqConst {
|
||||
func_idx: usize,
|
||||
arg_col: usize,
|
||||
expected: Slid,
|
||||
},
|
||||
/// func1(col1) == func2(col2)
|
||||
FuncEqFunc {
|
||||
func1_idx: usize,
|
||||
arg1_col: usize,
|
||||
func2_idx: usize,
|
||||
arg2_col: usize,
|
||||
},
|
||||
/// col1 == col2 (direct element equality)
|
||||
ColEq {
|
||||
col1: usize,
|
||||
col2: usize,
|
||||
},
|
||||
/// col == constant
|
||||
ColEqConst {
|
||||
col: usize,
|
||||
expected: Slid,
|
||||
},
|
||||
}
|
||||
|
||||
/// Projection specification.
|
||||
#[derive(Debug, Clone)]
|
||||
enum Projection {
|
||||
/// Return all columns
|
||||
All,
|
||||
/// Return specific columns
|
||||
Cols(Vec<usize>),
|
||||
/// Return specific columns with function applications
|
||||
FuncCols(Vec<FuncCol>),
|
||||
}
|
||||
|
||||
/// A column in projection, possibly with function application.
|
||||
#[derive(Debug, Clone)]
|
||||
struct FuncCol {
|
||||
/// Column to use as argument
|
||||
arg_col: usize,
|
||||
/// Function to apply (None = just the element)
|
||||
func_idx: Option<usize>,
|
||||
}
|
||||
|
||||
impl Query {
|
||||
/// Create a new query scanning a single sort.
|
||||
pub fn scan(sort_idx: usize) -> QueryBuilder {
|
||||
QueryBuilder {
|
||||
sources: vec![Source { sort_idx, col_offset: 0 }],
|
||||
constraints: vec![],
|
||||
projection: Projection::All,
|
||||
next_col: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for constructing queries fluently.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct QueryBuilder {
|
||||
sources: Vec<Source>,
|
||||
constraints: Vec<Constraint>,
|
||||
projection: Projection,
|
||||
next_col: usize,
|
||||
}
|
||||
|
||||
impl QueryBuilder {
|
||||
/// Add another sort to scan (creates a cross join, to be constrained).
|
||||
pub fn join_scan(mut self, sort_idx: usize) -> Self {
|
||||
let col_offset = self.next_col;
|
||||
self.sources.push(Source { sort_idx, col_offset });
|
||||
self.next_col += 1;
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a filter: func(col) == expected.
|
||||
///
|
||||
/// `col` is 0-indexed, referring to which source's element.
|
||||
pub fn filter_eq(mut self, func_idx: usize, arg_col: usize, expected: Slid) -> Self {
|
||||
self.constraints.push(Constraint::FuncEqConst {
|
||||
func_idx,
|
||||
arg_col,
|
||||
expected,
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a join condition: func1(col1) == func2(col2).
|
||||
///
|
||||
/// Used to join two scanned sorts by comparing function values.
|
||||
pub fn join_on_func(
|
||||
mut self,
|
||||
func1_idx: usize,
|
||||
arg1_col: usize,
|
||||
func2_idx: usize,
|
||||
arg2_col: usize,
|
||||
) -> Self {
|
||||
self.constraints.push(Constraint::FuncEqFunc {
|
||||
func1_idx,
|
||||
arg1_col,
|
||||
func2_idx,
|
||||
arg2_col,
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
/// Add an element equality constraint: col1 == col2.
|
||||
pub fn where_eq(mut self, col1: usize, col2: usize) -> Self {
|
||||
self.constraints.push(Constraint::ColEq { col1, col2 });
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a constant equality constraint: col == expected.
|
||||
pub fn where_const(mut self, col: usize, expected: Slid) -> Self {
|
||||
self.constraints.push(Constraint::ColEqConst { col, expected });
|
||||
self
|
||||
}
|
||||
|
||||
/// Project to specific columns.
|
||||
pub fn project(mut self, cols: Vec<usize>) -> Self {
|
||||
self.projection = Projection::Cols(cols);
|
||||
self
|
||||
}
|
||||
|
||||
/// Project with function applications.
|
||||
pub fn project_funcs(mut self, func_cols: Vec<(usize, Option<usize>)>) -> Self {
|
||||
self.projection = Projection::FuncCols(
|
||||
func_cols
|
||||
.into_iter()
|
||||
.map(|(arg_col, func_idx)| FuncCol { arg_col, func_idx })
|
||||
.collect(),
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the final Query.
|
||||
pub fn build(self) -> Query {
|
||||
Query {
|
||||
sources: self.sources,
|
||||
constraints: self.constraints,
|
||||
projection: self.projection,
|
||||
}
|
||||
}
|
||||
|
||||
/// Compile directly to QueryOp (skipping Query intermediate).
|
||||
pub fn compile(self) -> QueryOp {
|
||||
self.build().compile()
|
||||
}
|
||||
}
|
||||
|
||||
impl Query {
|
||||
/// Compile the query to a QueryOp plan.
|
||||
///
|
||||
/// The compilation strategy:
|
||||
/// 1. Scan each source sort
|
||||
/// 2. Join scans together (cross join if >1)
|
||||
/// 3. Handle FuncEqFunc constraints by applying functions, then filtering
|
||||
/// 4. Apply other constraints as filters
|
||||
/// 5. Apply projection
|
||||
pub fn compile(&self) -> QueryOp {
|
||||
if self.sources.is_empty() {
|
||||
return QueryOp::Empty;
|
||||
}
|
||||
|
||||
// Step 1: Build base plan from sources
|
||||
let mut plan = QueryOp::Scan {
|
||||
sort_idx: self.sources[0].sort_idx,
|
||||
};
|
||||
|
||||
// Track current column count (each source adds 1 column)
|
||||
let mut current_cols = 1;
|
||||
|
||||
// If multiple sources, join them
|
||||
for source in &self.sources[1..] {
|
||||
let right = QueryOp::Scan {
|
||||
sort_idx: source.sort_idx,
|
||||
};
|
||||
plan = QueryOp::Join {
|
||||
left: Box::new(plan),
|
||||
right: Box::new(right),
|
||||
cond: JoinCond::Cross, // Start with cross join, constraints will filter
|
||||
};
|
||||
current_cols += 1;
|
||||
}
|
||||
|
||||
// Step 2: Separate FuncEqFunc constraints (need Apply) from others
|
||||
let mut func_eq_func_constraints = Vec::new();
|
||||
let mut simple_constraints = Vec::new();
|
||||
|
||||
for constraint in &self.constraints {
|
||||
match constraint {
|
||||
Constraint::FuncEqFunc { .. } => func_eq_func_constraints.push(constraint),
|
||||
_ => simple_constraints.push(constraint),
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: Handle FuncEqFunc constraints
|
||||
// For each, apply both functions, track the added columns, then filter on equality
|
||||
for constraint in func_eq_func_constraints {
|
||||
if let Constraint::FuncEqFunc {
|
||||
func1_idx,
|
||||
arg1_col,
|
||||
func2_idx,
|
||||
arg2_col,
|
||||
} = constraint
|
||||
{
|
||||
// Apply func1 to arg1_col, result goes in current_cols
|
||||
plan = QueryOp::Apply {
|
||||
input: Box::new(plan),
|
||||
func_idx: *func1_idx,
|
||||
arg_col: *arg1_col,
|
||||
};
|
||||
let col1_result = current_cols;
|
||||
current_cols += 1;
|
||||
|
||||
// Apply func2 to arg2_col, result goes in current_cols
|
||||
plan = QueryOp::Apply {
|
||||
input: Box::new(plan),
|
||||
func_idx: *func2_idx,
|
||||
arg_col: *arg2_col,
|
||||
};
|
||||
let col2_result = current_cols;
|
||||
current_cols += 1;
|
||||
|
||||
// Filter where the two result columns are equal
|
||||
plan = QueryOp::Filter {
|
||||
input: Box::new(plan),
|
||||
pred: Predicate::ColEqCol {
|
||||
left: col1_result,
|
||||
right: col2_result,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Apply simple constraints as filters
|
||||
for constraint in simple_constraints {
|
||||
let pred = match constraint {
|
||||
Constraint::FuncEqConst {
|
||||
func_idx,
|
||||
arg_col,
|
||||
expected,
|
||||
} => Predicate::FuncEqConst {
|
||||
func_idx: *func_idx,
|
||||
arg_col: *arg_col,
|
||||
expected: *expected,
|
||||
},
|
||||
Constraint::FuncEqFunc { .. } => {
|
||||
unreachable!("FuncEqFunc already handled")
|
||||
}
|
||||
Constraint::ColEq { col1, col2 } => Predicate::ColEqCol {
|
||||
left: *col1,
|
||||
right: *col2,
|
||||
},
|
||||
Constraint::ColEqConst { col, expected } => Predicate::ColEqConst {
|
||||
col: *col,
|
||||
val: *expected,
|
||||
},
|
||||
};
|
||||
plan = QueryOp::Filter {
|
||||
input: Box::new(plan),
|
||||
pred,
|
||||
};
|
||||
}
|
||||
|
||||
// Step 5: Apply projection
|
||||
match &self.projection {
|
||||
Projection::All => {
|
||||
// No projection needed, return all columns
|
||||
}
|
||||
Projection::Cols(cols) => {
|
||||
plan = QueryOp::Project {
|
||||
input: Box::new(plan),
|
||||
columns: cols.clone(),
|
||||
};
|
||||
}
|
||||
Projection::FuncCols(func_cols) => {
|
||||
// Apply each function, then project
|
||||
let base_col = current_cols; // Start adding func results here
|
||||
for fc in func_cols.iter() {
|
||||
if let Some(func_idx) = fc.func_idx {
|
||||
plan = QueryOp::Apply {
|
||||
input: Box::new(plan),
|
||||
func_idx,
|
||||
arg_col: fc.arg_col,
|
||||
};
|
||||
current_cols += 1;
|
||||
}
|
||||
}
|
||||
// Project to the added columns
|
||||
if current_cols > base_col {
|
||||
let columns: Vec<usize> = (base_col..current_cols).collect();
|
||||
plan = QueryOp::Project {
|
||||
input: Box::new(plan),
|
||||
columns,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
plan
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Convenience functions for common query patterns
|
||||
// ============================================================================
|
||||
|
||||
/// Compile a simple single-sort query: scan sort, filter by func == value.
|
||||
///
|
||||
/// This is equivalent to `Pattern::new(sort).filter(func, value).compile()`
|
||||
/// but uses the new Query API.
|
||||
pub fn compile_simple_filter(sort_idx: usize, func_idx: usize, expected: Slid) -> QueryOp {
|
||||
Query::scan(sort_idx)
|
||||
.filter_eq(func_idx, 0, expected)
|
||||
.compile()
|
||||
}
|
||||
|
||||
/// Compile a query that returns func(elem) for matching elements.
|
||||
///
|
||||
/// scan(sort) |> filter(filter_func(elem) == expected) |> project(project_func(elem))
|
||||
pub fn compile_filter_project(
|
||||
sort_idx: usize,
|
||||
filter_func: usize,
|
||||
expected: Slid,
|
||||
project_func: usize,
|
||||
) -> QueryOp {
|
||||
// scan → filter → apply → project
|
||||
let scan = QueryOp::Scan { sort_idx };
|
||||
let filter = QueryOp::Filter {
|
||||
input: Box::new(scan),
|
||||
pred: Predicate::FuncEqConst {
|
||||
func_idx: filter_func,
|
||||
arg_col: 0,
|
||||
expected,
|
||||
},
|
||||
};
|
||||
let apply = QueryOp::Apply {
|
||||
input: Box::new(filter),
|
||||
func_idx: project_func,
|
||||
arg_col: 0,
|
||||
};
|
||||
// Now we have (elem, func(elem)), project to just column 1
|
||||
QueryOp::Project {
|
||||
input: Box::new(apply),
|
||||
columns: vec![1],
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::id::NumericId;
|
||||
|
||||
#[test]
|
||||
fn test_simple_scan_compiles() {
|
||||
let plan = Query::scan(0).compile();
|
||||
assert!(matches!(plan, QueryOp::Scan { sort_idx: 0 }));
|
||||
}
|
||||
|
||||
/// Test that Query-compiled plans produce same results as Pattern.
|
||||
///
|
||||
/// This validates that the new Query API is equivalent to the
|
||||
/// existing Pattern API for simple queries.
|
||||
#[test]
|
||||
fn test_query_matches_pattern() {
|
||||
use crate::core::Structure;
|
||||
use crate::query::backend::execute;
|
||||
use crate::query::{Pattern, Projection as PatternProjection};
|
||||
|
||||
// Create a structure with some data
|
||||
let mut structure = Structure::new(2);
|
||||
// Sort 0: elements 0, 1, 2
|
||||
structure.carriers[0].insert(0);
|
||||
structure.carriers[0].insert(1);
|
||||
structure.carriers[0].insert(2);
|
||||
// Sort 1: elements 10, 11
|
||||
structure.carriers[1].insert(10);
|
||||
structure.carriers[1].insert(11);
|
||||
|
||||
// Test 1: Simple scan
|
||||
let pattern_plan = Pattern {
|
||||
source_sort: 0,
|
||||
constraints: vec![],
|
||||
projection: PatternProjection::Element,
|
||||
}
|
||||
.compile();
|
||||
|
||||
let query_plan = Query::scan(0).compile();
|
||||
|
||||
let pattern_result = execute(&pattern_plan, &structure);
|
||||
let query_result = execute(&query_plan, &structure);
|
||||
|
||||
assert_eq!(
|
||||
pattern_result.len(),
|
||||
query_result.len(),
|
||||
"Scan should return same number of results"
|
||||
);
|
||||
|
||||
// Test 2: Scan with filter (using ColEqConst since we don't have functions)
|
||||
let pattern_plan = QueryOp::Filter {
|
||||
input: Box::new(QueryOp::Scan { sort_idx: 0 }),
|
||||
pred: Predicate::ColEqConst {
|
||||
col: 0,
|
||||
val: Slid::from_usize(1),
|
||||
},
|
||||
};
|
||||
|
||||
let query_plan = Query::scan(0)
|
||||
.where_const(0, Slid::from_usize(1))
|
||||
.compile();
|
||||
|
||||
let pattern_result = execute(&pattern_plan, &structure);
|
||||
let query_result = execute(&query_plan, &structure);
|
||||
|
||||
assert_eq!(pattern_result.len(), 1);
|
||||
assert_eq!(query_result.len(), 1);
|
||||
}
|
||||
|
||||
/// Test FuncEqFunc constraint: func1(col1) == func2(col2)
|
||||
#[test]
|
||||
fn test_func_eq_func_join() {
|
||||
use crate::core::Structure;
|
||||
use crate::query::backend::execute;
|
||||
use crate::universe::Universe;
|
||||
|
||||
// Create a structure with two sorts
|
||||
let mut structure = Structure::new(2);
|
||||
let mut universe = Universe::new();
|
||||
|
||||
// Sort 0: elements a, b
|
||||
let (a, _) = structure.add_element(&mut universe, 0);
|
||||
let (b, _) = structure.add_element(&mut universe, 0);
|
||||
|
||||
// Sort 1: elements x, y, z
|
||||
let (x, _) = structure.add_element(&mut universe, 1);
|
||||
let (y, _) = structure.add_element(&mut universe, 1);
|
||||
let (z, _) = structure.add_element(&mut universe, 1);
|
||||
|
||||
// Common target for function results
|
||||
let target1 = Slid::from_usize(100);
|
||||
let target2 = Slid::from_usize(200);
|
||||
|
||||
// Initialize functions
|
||||
// func0: Sort0 -> targets (a→100, b→200)
|
||||
// func1: Sort1 -> targets (x→100, y→200, z→100)
|
||||
structure.init_functions(&[Some(0), Some(1)]);
|
||||
|
||||
structure.define_function(0, a, target1).unwrap();
|
||||
structure.define_function(0, b, target2).unwrap();
|
||||
structure.define_function(1, x, target1).unwrap();
|
||||
structure.define_function(1, y, target2).unwrap();
|
||||
structure.define_function(1, z, target1).unwrap();
|
||||
|
||||
// Query: Find all (s0, s1) where func0(s0) == func1(s1)
|
||||
// Expected matches:
|
||||
// - (a, x) because func0(a)=100 == func1(x)=100
|
||||
// - (a, z) because func0(a)=100 == func1(z)=100
|
||||
// - (b, y) because func0(b)=200 == func1(y)=200
|
||||
|
||||
let plan = Query::scan(0)
|
||||
.join_scan(1)
|
||||
.join_on_func(0, 0, 1, 1) // func0(col0) == func1(col1)
|
||||
.compile();
|
||||
|
||||
let result = execute(&plan, &structure);
|
||||
|
||||
// Should have exactly 3 matching pairs
|
||||
assert_eq!(
|
||||
result.len(),
|
||||
3,
|
||||
"Expected 3 matching pairs, got {}",
|
||||
result.len()
|
||||
);
|
||||
}
|
||||
|
||||
/// Integration test: validate compiled queries against bootstrap_queries.
|
||||
///
|
||||
/// This test creates a real theory using the REPL, then verifies that
|
||||
/// queries compiled with the Query API produce the same results as
|
||||
/// the handcoded bootstrap_queries methods.
|
||||
#[test]
|
||||
fn test_query_matches_bootstrap_queries() {
|
||||
use crate::repl::ReplState;
|
||||
|
||||
// Create a theory via REPL
|
||||
let source = r#"
|
||||
theory Graph {
|
||||
V : Sort;
|
||||
E : Sort;
|
||||
src : E -> V;
|
||||
tgt : E -> V;
|
||||
}
|
||||
"#;
|
||||
|
||||
let mut repl = ReplState::new();
|
||||
let _ = repl.execute_geolog(source);
|
||||
|
||||
// Get the theory slid
|
||||
let theory_slid = match repl.store.resolve_name("Graph") {
|
||||
Some((slid, _)) => slid,
|
||||
None => panic!("Theory 'Graph' not found"),
|
||||
};
|
||||
|
||||
// Get bootstrap_queries result
|
||||
let bootstrap_sorts = repl.store.query_theory_sorts(theory_slid);
|
||||
|
||||
// Now compile a Query that does the same thing:
|
||||
// "Find all Srt where Srt/theory == theory_slid"
|
||||
let srt_sort = repl.store.sort_ids.srt.expect("Srt sort not found");
|
||||
let theory_func = repl
|
||||
.store
|
||||
.func_ids
|
||||
.srt_theory
|
||||
.expect("Srt/theory func not found");
|
||||
|
||||
// Compile the query
|
||||
let plan = compile_simple_filter(srt_sort, theory_func, theory_slid);
|
||||
|
||||
// Execute against the store's meta structure
|
||||
let result = crate::query::backend::execute(&plan, &repl.store.meta);
|
||||
|
||||
// Compare: should have same number of sorts
|
||||
assert_eq!(
|
||||
bootstrap_sorts.len(),
|
||||
result.len(),
|
||||
"Query should return same number of sorts as bootstrap_queries.\n\
|
||||
Bootstrap returned {} sorts: {:?}\n\
|
||||
Compiled query returned {} tuples",
|
||||
bootstrap_sorts.len(),
|
||||
bootstrap_sorts.iter().map(|s| &s.name).collect::<Vec<_>>(),
|
||||
result.len()
|
||||
);
|
||||
|
||||
// Verify we got V and E
|
||||
assert!(
|
||||
bootstrap_sorts.len() >= 2,
|
||||
"Graph theory should have at least V and E sorts"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filter_compiles() {
|
||||
let plan = Query::scan(0)
|
||||
.filter_eq(1, 0, Slid::from_usize(42))
|
||||
.compile();
|
||||
|
||||
// Should be Filter(Scan)
|
||||
if let QueryOp::Filter { input, pred } = plan {
|
||||
assert!(matches!(*input, QueryOp::Scan { sort_idx: 0 }));
|
||||
assert!(matches!(
|
||||
pred,
|
||||
Predicate::FuncEqConst {
|
||||
func_idx: 1,
|
||||
arg_col: 0,
|
||||
..
|
||||
}
|
||||
));
|
||||
} else {
|
||||
panic!("Expected Filter, got {:?}", plan);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_join_compiles() {
|
||||
let plan = Query::scan(0)
|
||||
.join_scan(1)
|
||||
.compile();
|
||||
|
||||
// Should be Join(Scan, Scan)
|
||||
if let QueryOp::Join { left, right, .. } = plan {
|
||||
assert!(matches!(*left, QueryOp::Scan { sort_idx: 0 }));
|
||||
assert!(matches!(*right, QueryOp::Scan { sort_idx: 1 }));
|
||||
} else {
|
||||
panic!("Expected Join, got {:?}", plan);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compile_simple_filter() {
|
||||
let plan = compile_simple_filter(5, 3, Slid::from_usize(100));
|
||||
|
||||
if let QueryOp::Filter { input, pred } = plan {
|
||||
assert!(matches!(*input, QueryOp::Scan { sort_idx: 5 }));
|
||||
if let Predicate::FuncEqConst {
|
||||
func_idx,
|
||||
arg_col,
|
||||
expected,
|
||||
} = pred
|
||||
{
|
||||
assert_eq!(func_idx, 3);
|
||||
assert_eq!(arg_col, 0);
|
||||
assert_eq!(expected, Slid::from_usize(100));
|
||||
} else {
|
||||
panic!("Expected FuncEqConst predicate");
|
||||
}
|
||||
} else {
|
||||
panic!("Expected Filter");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compile_filter_project() {
|
||||
let plan = compile_filter_project(0, 1, Slid::from_usize(42), 2);
|
||||
|
||||
// Should be Project(Apply(Filter(Scan)))
|
||||
if let QueryOp::Project { input, columns } = plan {
|
||||
assert_eq!(columns, vec![1]);
|
||||
if let QueryOp::Apply {
|
||||
input,
|
||||
func_idx,
|
||||
arg_col,
|
||||
} = *input
|
||||
{
|
||||
assert_eq!(func_idx, 2);
|
||||
assert_eq!(arg_col, 0);
|
||||
if let QueryOp::Filter { input, .. } = *input {
|
||||
assert!(matches!(*input, QueryOp::Scan { sort_idx: 0 }));
|
||||
} else {
|
||||
panic!("Expected Filter inside Apply");
|
||||
}
|
||||
} else {
|
||||
panic!("Expected Apply inside Project");
|
||||
}
|
||||
} else {
|
||||
panic!("Expected Project");
|
||||
}
|
||||
}
|
||||
}
|
||||
243
src/query/exec.rs
Normal file
243
src/query/exec.rs
Normal file
@ -0,0 +1,243 @@
|
||||
//! Query execution against a Store.
|
||||
//!
|
||||
//! This module executes Pattern queries against the GeologMeta store,
|
||||
//! computing the unique maximal element (cofree model) for ∀-style queries.
|
||||
|
||||
use crate::id::Slid;
|
||||
use crate::store::Store;
|
||||
use crate::store::append::AppendOps;
|
||||
|
||||
use super::{Pattern, Projection};
|
||||
|
||||
/// Result of a pattern query.
|
||||
///
|
||||
/// For ∀-style queries (open sorts), this is the cofree model:
|
||||
/// all elements satisfying the constraints.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum QueryResult {
|
||||
/// List of matching elements
|
||||
Elements(Vec<Slid>),
|
||||
/// List of projected values
|
||||
Values(Vec<Slid>),
|
||||
/// List of projected tuples
|
||||
Tuples(Vec<Vec<Slid>>),
|
||||
}
|
||||
|
||||
impl QueryResult {
|
||||
/// Get as elements (panics if not Elements variant).
|
||||
pub fn into_elements(self) -> Vec<Slid> {
|
||||
match self {
|
||||
QueryResult::Elements(e) => e,
|
||||
_ => panic!("QueryResult is not Elements"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get as values (panics if not Values variant).
|
||||
pub fn into_values(self) -> Vec<Slid> {
|
||||
match self {
|
||||
QueryResult::Values(v) => v,
|
||||
_ => panic!("QueryResult is not Values"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get as tuples (panics if not Tuples variant).
|
||||
pub fn into_tuples(self) -> Vec<Vec<Slid>> {
|
||||
match self {
|
||||
QueryResult::Tuples(t) => t,
|
||||
_ => panic!("QueryResult is not Tuples"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the result is empty.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
match self {
|
||||
QueryResult::Elements(e) => e.is_empty(),
|
||||
QueryResult::Values(v) => v.is_empty(),
|
||||
QueryResult::Tuples(t) => t.is_empty(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the number of results.
|
||||
pub fn len(&self) -> usize {
|
||||
match self {
|
||||
QueryResult::Elements(e) => e.len(),
|
||||
QueryResult::Values(v) => v.len(),
|
||||
QueryResult::Tuples(t) => t.len(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute a pattern query against a store.
|
||||
///
|
||||
/// This is the ∀-style query executor: scans all elements of source_sort,
|
||||
/// filters by constraints, and projects the result.
|
||||
///
|
||||
/// In terms of query semantics: computes the unique maximal element
|
||||
/// (cofree model) of the theory extension.
|
||||
pub fn execute_pattern(store: &Store, pattern: &Pattern) -> QueryResult {
|
||||
// Scan all elements of source sort
|
||||
let candidates = store.elements_of_sort(pattern.source_sort);
|
||||
|
||||
// Filter by constraints
|
||||
let matching: Vec<Slid> = candidates
|
||||
.into_iter()
|
||||
.filter(|&elem| {
|
||||
pattern.constraints.iter().all(|c| {
|
||||
store.get_func(c.func, elem) == Some(c.expected)
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Project
|
||||
match &pattern.projection {
|
||||
Projection::Element => QueryResult::Elements(matching),
|
||||
Projection::Func(func) => {
|
||||
let values: Vec<Slid> = matching
|
||||
.into_iter()
|
||||
.filter_map(|elem| store.get_func(*func, elem))
|
||||
.collect();
|
||||
QueryResult::Values(values)
|
||||
}
|
||||
Projection::Tuple(funcs) => {
|
||||
let tuples: Vec<Vec<Slid>> = matching
|
||||
.into_iter()
|
||||
.filter_map(|elem| {
|
||||
let tuple: Vec<Slid> = funcs
|
||||
.iter()
|
||||
.filter_map(|f| store.get_func(*f, elem))
|
||||
.collect();
|
||||
// Only include if all projections succeeded
|
||||
if tuple.len() == funcs.len() {
|
||||
Some(tuple)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
QueryResult::Tuples(tuples)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience methods on Store for pattern queries.
|
||||
impl Store {
|
||||
/// Execute a pattern query.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```ignore
|
||||
/// // Find all Srt where Srt.theory == theory_slid
|
||||
/// let result = store.query(
|
||||
/// Pattern::new(store.sort_ids.srt.unwrap())
|
||||
/// .filter(store.func_ids.srt_theory.unwrap(), theory_slid)
|
||||
/// );
|
||||
/// ```
|
||||
pub fn query(&self, pattern: &Pattern) -> QueryResult {
|
||||
execute_pattern(self, pattern)
|
||||
}
|
||||
|
||||
/// Execute a pattern query and return just the matching elements.
|
||||
pub fn query_elements(&self, pattern: &Pattern) -> Vec<Slid> {
|
||||
execute_pattern(self, pattern).into_elements()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Typed query helpers that replace bootstrap_queries
|
||||
// ============================================================================
|
||||
|
||||
/// Information about a sort (mirrors bootstrap_queries::SortInfo)
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SortInfo {
|
||||
pub name: String,
|
||||
pub slid: Slid,
|
||||
}
|
||||
|
||||
impl Store {
|
||||
/// Query all sorts belonging to a theory using Pattern API.
|
||||
///
|
||||
/// This is the Pattern-based equivalent of bootstrap_queries::query_theory_sorts.
|
||||
/// Both should return identical results.
|
||||
pub fn query_sorts_of_theory(&self, theory_slid: Slid) -> Vec<SortInfo> {
|
||||
let Some(srt_sort) = self.sort_ids.srt else {
|
||||
return vec![];
|
||||
};
|
||||
let Some(theory_func) = self.func_ids.srt_theory else {
|
||||
return vec![];
|
||||
};
|
||||
|
||||
// The core pattern: find all Srt where Srt.theory == theory_slid
|
||||
let pattern = Pattern::new(srt_sort)
|
||||
.filter(theory_func, theory_slid);
|
||||
|
||||
// Execute and post-process
|
||||
self.query_elements(&pattern)
|
||||
.into_iter()
|
||||
.map(|slid| {
|
||||
let name = self.get_element_name(slid);
|
||||
let short_name = name.rsplit('/').next().unwrap_or(&name).to_string();
|
||||
SortInfo { name: short_name, slid }
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
/// Test that Pattern-based query matches bootstrap_queries.
|
||||
///
|
||||
/// This is a sanity test to ensure the new query engine gives
|
||||
/// identical results to the hand-coded queries.
|
||||
#[test]
|
||||
fn test_query_sorts_matches_bootstrap() {
|
||||
// Parse and elaborate a theory via REPL
|
||||
let source = r#"
|
||||
theory Graph {
|
||||
V : Sort;
|
||||
E : Sort;
|
||||
src : E -> V;
|
||||
tgt : E -> V;
|
||||
}
|
||||
"#;
|
||||
|
||||
// Use ReplState to execute
|
||||
let mut repl = crate::repl::ReplState::new();
|
||||
let _ = repl.execute_geolog(source);
|
||||
|
||||
// Get the theory slid
|
||||
if let Some((theory_slid, _)) = repl.store.resolve_name("Graph") {
|
||||
// Query using bootstrap method
|
||||
let bootstrap_result = repl.store.query_theory_sorts(theory_slid);
|
||||
|
||||
// Query using Pattern method
|
||||
let pattern_result = repl.store.query_sorts_of_theory(theory_slid);
|
||||
|
||||
// Should have same number of results
|
||||
assert_eq!(
|
||||
bootstrap_result.len(),
|
||||
pattern_result.len(),
|
||||
"Different number of sorts returned: bootstrap={}, pattern={}",
|
||||
bootstrap_result.len(),
|
||||
pattern_result.len()
|
||||
);
|
||||
|
||||
// Should have same sort names (V and E)
|
||||
let bootstrap_names: std::collections::HashSet<_> =
|
||||
bootstrap_result.iter().map(|s| &s.name).collect();
|
||||
let pattern_names: std::collections::HashSet<_> =
|
||||
pattern_result.iter().map(|s| &s.name).collect();
|
||||
|
||||
assert_eq!(
|
||||
bootstrap_names,
|
||||
pattern_names,
|
||||
"Different sort names returned"
|
||||
);
|
||||
|
||||
// Verify we got the expected sorts
|
||||
assert!(bootstrap_names.contains(&"V".to_string()), "Missing sort V");
|
||||
assert!(bootstrap_names.contains(&"E".to_string()), "Missing sort E");
|
||||
} else {
|
||||
panic!("Theory 'Graph' not found after execution");
|
||||
}
|
||||
}
|
||||
}
|
||||
1239
src/query/from_relalg.rs
Normal file
1239
src/query/from_relalg.rs
Normal file
File diff suppressed because it is too large
Load Diff
43
src/query/mod.rs
Normal file
43
src/query/mod.rs
Normal file
@ -0,0 +1,43 @@
|
||||
//! Query engine for geolog.
|
||||
//!
|
||||
//! **Semantics:** Queries are theory extensions. The result is the set of maximal
|
||||
//! elements in the posetal reflection of well-formed Ext_M(T') — the category
|
||||
//! of T'-extensions of base model M.
|
||||
//!
|
||||
//! See `loose_thoughts/2026-01-19_18:15_query_semantics.md` for full design.
|
||||
//!
|
||||
//! # Query Styles
|
||||
//!
|
||||
//! - **∃-style (closed sorts):** New sorts with declared constants.
|
||||
//! Well-formedness requires exactly those constants exist.
|
||||
//! Maximal elements = one per valid witness assignment.
|
||||
//! Implementation: constraint satisfaction.
|
||||
//!
|
||||
//! - **∀-style (open sorts):** New sorts with no constants, constrained by
|
||||
//! universal axioms. Bounded by constraint, posetal reflection identifies
|
||||
//! observationally-equivalent duplicates.
|
||||
//! Unique maximal element = cofree model.
|
||||
//! Implementation: relational algebra / Datalog.
|
||||
//!
|
||||
//! # Implementation Phases
|
||||
//!
|
||||
//! 1. **Open sort computation** - what bootstrap_queries does manually
|
||||
//! 2. **Closed sort enumeration** - constraint satisfaction
|
||||
//! 3. **Chase for derived relations** - semi-naive fixpoint
|
||||
//! 4. **Mixed queries** - combine both
|
||||
|
||||
mod pattern;
|
||||
mod exec;
|
||||
pub mod backend;
|
||||
pub mod optimize;
|
||||
pub mod compile;
|
||||
mod store_queries;
|
||||
pub mod to_relalg;
|
||||
pub mod from_relalg;
|
||||
pub mod chase;
|
||||
|
||||
pub use pattern::{Pattern, Constraint, Projection};
|
||||
pub use exec::{QueryResult, execute_pattern};
|
||||
pub use backend::{Bag, QueryOp, Predicate, JoinCond, execute, execute_optimized, StreamContext, execute_stream};
|
||||
pub use optimize::optimize;
|
||||
pub use compile::{Query, QueryBuilder, compile_simple_filter, compile_filter_project};
|
||||
308
src/query/optimize.rs
Normal file
308
src/query/optimize.rs
Normal file
@ -0,0 +1,308 @@
|
||||
//! Query optimizer using algebraic laws.
|
||||
//!
|
||||
//! Applies rewrite rules corresponding to the algebraic laws defined in
|
||||
//! RelAlgIR.geolog to transform query plans into more efficient forms.
|
||||
//!
|
||||
//! This is a simple "obviously correct" optimizer:
|
||||
//! - Single-pass bottom-up rewriting
|
||||
//! - No cost model (just simplification)
|
||||
//! - Validated by proptests against the naive backend
|
||||
//!
|
||||
//! Key rewrites:
|
||||
//! - Filter(True, x) → x
|
||||
//! - Filter(False, x) → Empty
|
||||
//! - Filter(p, Filter(q, x)) → Filter(And(p, q), x)
|
||||
//! - Distinct(Distinct(x)) → Distinct(x)
|
||||
//! - Union(x, Empty) → x
|
||||
//! - Union(Empty, x) → x
|
||||
//! - Negate(Negate(x)) → x
|
||||
//! - Join(x, Empty) → Empty
|
||||
//! - Join(Empty, x) → Empty
|
||||
|
||||
use super::backend::{Predicate, QueryOp};
|
||||
|
||||
/// Optimize a query plan by applying algebraic laws.
|
||||
///
|
||||
/// Returns an equivalent plan that may be more efficient to execute.
|
||||
/// The optimization is semantics-preserving: optimize(p) produces the
|
||||
/// same results as p for any structure.
|
||||
pub fn optimize(plan: &QueryOp) -> QueryOp {
|
||||
// Bottom-up: optimize children first, then apply rules
|
||||
let optimized_children = optimize_children(plan);
|
||||
apply_rules(optimized_children)
|
||||
}
|
||||
|
||||
/// Recursively optimize all children of a plan node.
|
||||
fn optimize_children(plan: &QueryOp) -> QueryOp {
|
||||
match plan {
|
||||
QueryOp::Scan { sort_idx } => QueryOp::Scan { sort_idx: *sort_idx },
|
||||
|
||||
QueryOp::ScanRelation { rel_id } => QueryOp::ScanRelation { rel_id: *rel_id },
|
||||
|
||||
QueryOp::Filter { input, pred } => QueryOp::Filter {
|
||||
input: Box::new(optimize(input)),
|
||||
pred: pred.clone(),
|
||||
},
|
||||
|
||||
QueryOp::Project { input, columns } => QueryOp::Project {
|
||||
input: Box::new(optimize(input)),
|
||||
columns: columns.clone(),
|
||||
},
|
||||
|
||||
QueryOp::Join { left, right, cond } => QueryOp::Join {
|
||||
left: Box::new(optimize(left)),
|
||||
right: Box::new(optimize(right)),
|
||||
cond: cond.clone(),
|
||||
},
|
||||
|
||||
QueryOp::Union { left, right } => QueryOp::Union {
|
||||
left: Box::new(optimize(left)),
|
||||
right: Box::new(optimize(right)),
|
||||
},
|
||||
|
||||
QueryOp::Distinct { input } => QueryOp::Distinct {
|
||||
input: Box::new(optimize(input)),
|
||||
},
|
||||
|
||||
QueryOp::Negate { input } => QueryOp::Negate {
|
||||
input: Box::new(optimize(input)),
|
||||
},
|
||||
|
||||
QueryOp::Constant { tuple } => QueryOp::Constant { tuple: tuple.clone() },
|
||||
|
||||
QueryOp::Empty => QueryOp::Empty,
|
||||
|
||||
QueryOp::Apply { input, func_idx, arg_col } => QueryOp::Apply {
|
||||
input: Box::new(optimize(input)),
|
||||
func_idx: *func_idx,
|
||||
arg_col: *arg_col,
|
||||
},
|
||||
|
||||
QueryOp::ApplyField { input, func_idx, arg_col, field_name } => QueryOp::ApplyField {
|
||||
input: Box::new(optimize(input)),
|
||||
func_idx: *func_idx,
|
||||
arg_col: *arg_col,
|
||||
field_name: field_name.clone(),
|
||||
},
|
||||
|
||||
// DBSP temporal operators: optimize children, preserve state_id
|
||||
QueryOp::Delay { input, state_id } => QueryOp::Delay {
|
||||
input: Box::new(optimize(input)),
|
||||
state_id: *state_id,
|
||||
},
|
||||
|
||||
QueryOp::Diff { input, state_id } => QueryOp::Diff {
|
||||
input: Box::new(optimize(input)),
|
||||
state_id: *state_id,
|
||||
},
|
||||
|
||||
QueryOp::Integrate { input, state_id } => QueryOp::Integrate {
|
||||
input: Box::new(optimize(input)),
|
||||
state_id: *state_id,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply algebraic rewrite rules to a plan node.
|
||||
/// Assumes children are already optimized.
|
||||
fn apply_rules(plan: QueryOp) -> QueryOp {
|
||||
match plan {
|
||||
// ============================================================
|
||||
// Filter Laws
|
||||
// ============================================================
|
||||
|
||||
// Filter(True, x) → x
|
||||
QueryOp::Filter { input, pred: Predicate::True } => *input,
|
||||
|
||||
// Filter(False, x) → Empty
|
||||
QueryOp::Filter { pred: Predicate::False, .. } => QueryOp::Empty,
|
||||
|
||||
// Filter(p, Filter(q, x)) → Filter(And(p, q), x)
|
||||
QueryOp::Filter { input, pred: outer_pred } => {
|
||||
if let QueryOp::Filter { input: inner_input, pred: inner_pred } = *input {
|
||||
QueryOp::Filter {
|
||||
input: inner_input,
|
||||
pred: Predicate::And(
|
||||
Box::new(outer_pred),
|
||||
Box::new(inner_pred),
|
||||
),
|
||||
}
|
||||
} else {
|
||||
QueryOp::Filter {
|
||||
input: Box::new(*input),
|
||||
pred: outer_pred,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Distinct Laws
|
||||
// ============================================================
|
||||
|
||||
// Distinct(Distinct(x)) → Distinct(x)
|
||||
QueryOp::Distinct { input } => {
|
||||
if matches!(*input, QueryOp::Distinct { .. }) {
|
||||
*input
|
||||
} else {
|
||||
QueryOp::Distinct { input }
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Union Laws
|
||||
// ============================================================
|
||||
|
||||
// Union(x, Empty) → x
|
||||
// Union(Empty, x) → x
|
||||
QueryOp::Union { left, right } => {
|
||||
match (&*left, &*right) {
|
||||
(QueryOp::Empty, _) => *right,
|
||||
(_, QueryOp::Empty) => *left,
|
||||
_ => QueryOp::Union { left, right },
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Negate Laws
|
||||
// ============================================================
|
||||
|
||||
// Negate(Negate(x)) → x
|
||||
QueryOp::Negate { input } => {
|
||||
if let QueryOp::Negate { input: inner } = *input {
|
||||
*inner
|
||||
} else {
|
||||
QueryOp::Negate { input }
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Join Laws
|
||||
// ============================================================
|
||||
|
||||
// Join(x, Empty) → Empty
|
||||
// Join(Empty, x) → Empty
|
||||
QueryOp::Join { left, right, cond } => {
|
||||
if matches!(*left, QueryOp::Empty) || matches!(*right, QueryOp::Empty) {
|
||||
QueryOp::Empty
|
||||
} else {
|
||||
QueryOp::Join { left, right, cond }
|
||||
}
|
||||
}
|
||||
|
||||
// No rewrite applies
|
||||
other => other,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::query::backend::JoinCond;
|
||||
use crate::id::{NumericId, Slid};
|
||||
|
||||
#[test]
|
||||
fn test_filter_true_elimination() {
|
||||
let scan = QueryOp::Scan { sort_idx: 0 };
|
||||
let filter = QueryOp::Filter {
|
||||
input: Box::new(scan.clone()),
|
||||
pred: Predicate::True,
|
||||
};
|
||||
let optimized = optimize(&filter);
|
||||
assert!(matches!(optimized, QueryOp::Scan { sort_idx: 0 }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filter_false_to_empty() {
|
||||
let scan = QueryOp::Scan { sort_idx: 0 };
|
||||
let filter = QueryOp::Filter {
|
||||
input: Box::new(scan),
|
||||
pred: Predicate::False,
|
||||
};
|
||||
let optimized = optimize(&filter);
|
||||
assert!(matches!(optimized, QueryOp::Empty));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filter_fusion() {
|
||||
let scan = QueryOp::Scan { sort_idx: 0 };
|
||||
let filter1 = QueryOp::Filter {
|
||||
input: Box::new(scan),
|
||||
pred: Predicate::ColEqConst { col: 0, val: Slid::from_usize(1) },
|
||||
};
|
||||
let filter2 = QueryOp::Filter {
|
||||
input: Box::new(filter1),
|
||||
pred: Predicate::ColEqConst { col: 0, val: Slid::from_usize(2) },
|
||||
};
|
||||
let optimized = optimize(&filter2);
|
||||
|
||||
// Should be a single filter with And predicate
|
||||
if let QueryOp::Filter { pred: Predicate::And(_, _), .. } = optimized {
|
||||
// Good!
|
||||
} else {
|
||||
panic!("Expected fused filter with And predicate, got {:?}", optimized);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_distinct_idempotent() {
|
||||
let scan = QueryOp::Scan { sort_idx: 0 };
|
||||
let distinct1 = QueryOp::Distinct {
|
||||
input: Box::new(scan),
|
||||
};
|
||||
let distinct2 = QueryOp::Distinct {
|
||||
input: Box::new(distinct1.clone()),
|
||||
};
|
||||
let optimized = optimize(&distinct2);
|
||||
|
||||
// Should be single distinct
|
||||
if let QueryOp::Distinct { input } = optimized {
|
||||
assert!(matches!(*input, QueryOp::Scan { .. }));
|
||||
} else {
|
||||
panic!("Expected Distinct, got {:?}", optimized);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_union_empty_elimination() {
|
||||
let scan = QueryOp::Scan { sort_idx: 0 };
|
||||
let union = QueryOp::Union {
|
||||
left: Box::new(scan.clone()),
|
||||
right: Box::new(QueryOp::Empty),
|
||||
};
|
||||
let optimized = optimize(&union);
|
||||
assert!(matches!(optimized, QueryOp::Scan { sort_idx: 0 }));
|
||||
|
||||
// Also test left empty
|
||||
let union2 = QueryOp::Union {
|
||||
left: Box::new(QueryOp::Empty),
|
||||
right: Box::new(scan),
|
||||
};
|
||||
let optimized2 = optimize(&union2);
|
||||
assert!(matches!(optimized2, QueryOp::Scan { sort_idx: 0 }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_negate_involution() {
|
||||
let scan = QueryOp::Scan { sort_idx: 0 };
|
||||
let negate1 = QueryOp::Negate {
|
||||
input: Box::new(scan),
|
||||
};
|
||||
let negate2 = QueryOp::Negate {
|
||||
input: Box::new(negate1),
|
||||
};
|
||||
let optimized = optimize(&negate2);
|
||||
assert!(matches!(optimized, QueryOp::Scan { sort_idx: 0 }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_join_empty_elimination() {
|
||||
let scan = QueryOp::Scan { sort_idx: 0 };
|
||||
let join = QueryOp::Join {
|
||||
left: Box::new(scan),
|
||||
right: Box::new(QueryOp::Empty),
|
||||
cond: JoinCond::Cross,
|
||||
};
|
||||
let optimized = optimize(&join);
|
||||
assert!(matches!(optimized, QueryOp::Empty));
|
||||
}
|
||||
}
|
||||
171
src/query/pattern.rs
Normal file
171
src/query/pattern.rs
Normal file
@ -0,0 +1,171 @@
|
||||
//! Pattern-based query representation.
|
||||
//!
|
||||
//! This represents the common pattern from bootstrap_queries:
|
||||
//! "find all X : Sort where X.func₁ = Y₁ ∧ X.func₂ = Y₂ ∧ ..."
|
||||
//!
|
||||
//! In query semantics terms, this is an ∀-style query with an open result sort:
|
||||
//! ```text
|
||||
//! theory Query extends Base {
|
||||
//! Result : Sort; // Open (no constants)
|
||||
//! elem : Result → Sort; // Projection to base
|
||||
//! axiom { r : Result ⊢ elem(r).func₁ = Y₁ ∧ elem(r).func₂ = Y₂ }
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! The unique maximal element (cofree model) is the set of all elements
|
||||
//! satisfying the constraint.
|
||||
|
||||
use crate::id::Slid;
|
||||
|
||||
/// A pattern query: find all elements of a sort matching constraints.
|
||||
///
|
||||
/// Equivalent to SQL: `SELECT elem FROM Sort WHERE func₁(elem) = v₁ AND ...`
|
||||
///
|
||||
/// Uses `usize` for sort/function IDs (internal indices) and `Slid` for
|
||||
/// element values (external references).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Pattern {
|
||||
/// The sort to scan (sort index)
|
||||
pub source_sort: usize,
|
||||
/// Constraints: each is (func_index, expected_value)
|
||||
pub constraints: Vec<Constraint>,
|
||||
/// What to project/return
|
||||
pub projection: Projection,
|
||||
}
|
||||
|
||||
/// A constraint: func(elem) must equal expected_value
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Constraint {
|
||||
/// Function index to apply to the scanned element
|
||||
pub func: usize,
|
||||
/// Expected value (must match)
|
||||
pub expected: Slid,
|
||||
}
|
||||
|
||||
/// What to return from the query
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Projection {
|
||||
/// Return the element itself
|
||||
Element,
|
||||
/// Return the value of a function applied to the element
|
||||
Func(usize),
|
||||
/// Return a tuple of function values
|
||||
Tuple(Vec<usize>),
|
||||
}
|
||||
|
||||
impl Pattern {
|
||||
/// Create a new pattern query.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```ignore
|
||||
/// // Find all Srt where Srt.theory == theory_slid
|
||||
/// let pattern = Pattern::new(store.sort_ids.srt.unwrap())
|
||||
/// .filter(store.func_ids.srt_theory.unwrap(), theory_slid);
|
||||
/// ```
|
||||
pub fn new(source_sort: usize) -> Self {
|
||||
Self {
|
||||
source_sort,
|
||||
constraints: Vec::new(),
|
||||
projection: Projection::Element,
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a constraint: func(elem) must equal value.
|
||||
pub fn filter(mut self, func: usize, value: Slid) -> Self {
|
||||
self.constraints.push(Constraint {
|
||||
func,
|
||||
expected: value,
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
/// Project a function value instead of the element.
|
||||
pub fn project(mut self, func: usize) -> Self {
|
||||
self.projection = Projection::Func(func);
|
||||
self
|
||||
}
|
||||
|
||||
/// Project a tuple of function values.
|
||||
pub fn project_tuple(mut self, funcs: Vec<usize>) -> Self {
|
||||
self.projection = Projection::Tuple(funcs);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Pattern → QueryOp Compilation
|
||||
// ============================================================================
|
||||
|
||||
use super::backend::{QueryOp, Predicate};
|
||||
|
||||
impl Pattern {
|
||||
/// Compile a Pattern into a QueryOp for the naive backend.
|
||||
///
|
||||
/// A Pattern query:
|
||||
/// 1. Scans all elements of source_sort
|
||||
/// 2. Filters by constraints: func(elem) = expected for each constraint
|
||||
/// 3. Projects according to projection type
|
||||
///
|
||||
/// We implement this as:
|
||||
/// - Scan → single-column tuples (elem)
|
||||
/// - For each constraint, use FuncEqConst predicate
|
||||
/// - Project to requested columns
|
||||
pub fn compile(&self) -> QueryOp {
|
||||
// Start with a scan of the sort
|
||||
let mut plan = QueryOp::Scan { sort_idx: self.source_sort };
|
||||
|
||||
// Apply constraints as filters
|
||||
// Each constraint checks: func(elem) = expected
|
||||
for constraint in &self.constraints {
|
||||
plan = QueryOp::Filter {
|
||||
input: Box::new(plan),
|
||||
pred: Predicate::FuncEqConst {
|
||||
func_idx: constraint.func,
|
||||
arg_col: 0, // The scanned element is always in column 0
|
||||
expected: constraint.expected,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Apply projection
|
||||
match &self.projection {
|
||||
Projection::Element => {
|
||||
// Already have the element in col 0, no change needed
|
||||
}
|
||||
Projection::Func(func_idx) => {
|
||||
// Apply function to element, return that instead
|
||||
// This requires an Apply operation
|
||||
plan = QueryOp::Apply {
|
||||
input: Box::new(plan),
|
||||
func_idx: *func_idx,
|
||||
arg_col: 0,
|
||||
};
|
||||
// Now we have (elem, func(elem)), project to just col 1
|
||||
plan = QueryOp::Project {
|
||||
input: Box::new(plan),
|
||||
columns: vec![1],
|
||||
};
|
||||
}
|
||||
Projection::Tuple(func_indices) => {
|
||||
// Apply each function in sequence, then project
|
||||
for func_idx in func_indices.iter() {
|
||||
plan = QueryOp::Apply {
|
||||
input: Box::new(plan),
|
||||
func_idx: *func_idx,
|
||||
arg_col: 0, // Always apply to original element
|
||||
};
|
||||
}
|
||||
// Now we have (elem, f1(elem), f2(elem), ...), project to func results
|
||||
// Columns 1, 2, ... are the func results
|
||||
let columns: Vec<usize> = (1..=func_indices.len()).collect();
|
||||
plan = QueryOp::Project {
|
||||
input: Box::new(plan),
|
||||
columns,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
plan
|
||||
}
|
||||
}
|
||||
672
src/query/store_queries.rs
Normal file
672
src/query/store_queries.rs
Normal file
@ -0,0 +1,672 @@
|
||||
//! Store query integration: using compiled queries to replace bootstrap_queries.
|
||||
//!
|
||||
//! This module provides query methods on Store that use the compiled Query API
|
||||
//! instead of handcoded iterations. It demonstrates that the Query compiler
|
||||
//! can replace bootstrap_queries.rs.
|
||||
//!
|
||||
//! # Migration Path
|
||||
//!
|
||||
//! 1. First, create query versions here that match bootstrap_queries behavior
|
||||
//! 2. Add tests that validate both produce same results
|
||||
//! 3. Once validated, swap implementations in bootstrap_queries
|
||||
//! 4. Eventually deprecate bootstrap_queries in favor of these
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```ignore
|
||||
//! // Old: bootstrap_queries.rs
|
||||
//! for srt_slid in self.elements_of_sort(srt_sort) {
|
||||
//! if self.get_func(theory_func, srt_slid) == Some(theory_slid) { ... }
|
||||
//! }
|
||||
//!
|
||||
//! // New: store_queries.rs using Query compiler
|
||||
//! let plan = Query::scan(srt_sort)
|
||||
//! .filter_eq(theory_func, 0, theory_slid)
|
||||
//! .compile();
|
||||
//! let result = execute(&plan, &store.meta);
|
||||
//! ```
|
||||
|
||||
use crate::core::DerivedSort;
|
||||
use crate::id::{NumericId, Slid, Uuid};
|
||||
use crate::store::Store;
|
||||
use crate::store::append::AppendOps;
|
||||
use crate::store::bootstrap_queries::{SortInfo, FuncInfo, RelInfo, ElemInfo, FuncValInfo, RelTupleInfo};
|
||||
use super::backend::execute;
|
||||
use super::compile::compile_simple_filter;
|
||||
|
||||
impl Store {
|
||||
/// Get the UUID for an element in GeologMeta by its Slid.
|
||||
/// Used for deterministic ordering: UUIDs v7 are time-ordered.
|
||||
pub fn get_element_uuid(&self, slid: Slid) -> Uuid {
|
||||
if let Some(&luid) = self.meta.luids.get(slid.index()) {
|
||||
self.universe.get(luid).unwrap_or(Uuid::nil())
|
||||
} else {
|
||||
Uuid::nil()
|
||||
}
|
||||
}
|
||||
|
||||
/// Query all sorts belonging to a theory (using compiled query engine).
|
||||
///
|
||||
/// This is equivalent to `query_theory_sorts` in bootstrap_queries.rs,
|
||||
/// but uses the Query compiler instead of handcoded iteration.
|
||||
pub fn query_theory_sorts_compiled(&self, theory_slid: Slid) -> Vec<SortInfo> {
|
||||
let Some(srt_sort) = self.sort_ids.srt else {
|
||||
return vec![];
|
||||
};
|
||||
let Some(theory_func) = self.func_ids.srt_theory else {
|
||||
return vec![];
|
||||
};
|
||||
|
||||
// Compile and execute the query
|
||||
let plan = compile_simple_filter(srt_sort, theory_func, theory_slid);
|
||||
let result = execute(&plan, &self.meta);
|
||||
|
||||
// Convert query results to SortInfo
|
||||
let mut infos = Vec::new();
|
||||
for tuple in result.tuples.keys() {
|
||||
if let Some(&srt_slid) = tuple.first() {
|
||||
let name = self.get_element_name(srt_slid);
|
||||
let short_name = name.rsplit('/').next().unwrap_or(&name).to_string();
|
||||
infos.push(SortInfo {
|
||||
name: short_name,
|
||||
slid: srt_slid,
|
||||
});
|
||||
}
|
||||
}
|
||||
// Sort by UUID to ensure deterministic order matching original creation order
|
||||
// (UUIDs v7 are time-ordered, so earlier-created elements come first)
|
||||
infos.sort_by_key(|info| self.get_element_uuid(info.slid));
|
||||
infos
|
||||
}
|
||||
|
||||
/// Query all functions belonging to a theory (using compiled query engine).
|
||||
///
|
||||
/// This is equivalent to `query_theory_funcs` in bootstrap_queries.rs,
|
||||
/// but uses the Query compiler for the initial scan+filter.
|
||||
pub fn query_theory_funcs_compiled(&self, theory_slid: Slid) -> Vec<FuncInfo> {
|
||||
let Some(func_sort) = self.sort_ids.func else {
|
||||
return vec![];
|
||||
};
|
||||
let Some(theory_func) = self.func_ids.func_theory else {
|
||||
return vec![];
|
||||
};
|
||||
let Some(dom_func) = self.func_ids.func_dom else {
|
||||
return vec![];
|
||||
};
|
||||
let Some(cod_func) = self.func_ids.func_cod else {
|
||||
return vec![];
|
||||
};
|
||||
|
||||
// Compile and execute the query to find matching functions
|
||||
let plan = compile_simple_filter(func_sort, theory_func, theory_slid);
|
||||
let result = execute(&plan, &self.meta);
|
||||
|
||||
// Convert query results to FuncInfo (with domain/codomain lookups)
|
||||
let mut infos = Vec::new();
|
||||
for tuple in result.tuples.keys() {
|
||||
if let Some(&func_slid) = tuple.first() {
|
||||
let name = self.get_element_name(func_slid);
|
||||
let short_name = name.rsplit('/').next().unwrap_or(&name).to_string();
|
||||
|
||||
// Get domain and codomain DSorts (using bootstrap logic)
|
||||
let domain = self
|
||||
.get_func(dom_func, func_slid)
|
||||
.map(|ds| self.resolve_dsort(ds))
|
||||
.unwrap_or(DerivedSort::Product(vec![]));
|
||||
let codomain = self
|
||||
.get_func(cod_func, func_slid)
|
||||
.map(|ds| self.resolve_dsort(ds))
|
||||
.unwrap_or(DerivedSort::Product(vec![]));
|
||||
|
||||
infos.push(FuncInfo {
|
||||
name: short_name,
|
||||
slid: func_slid,
|
||||
domain,
|
||||
codomain,
|
||||
});
|
||||
}
|
||||
}
|
||||
// Sort by UUID to ensure deterministic order matching original creation order
|
||||
infos.sort_by_key(|info| self.get_element_uuid(info.slid));
|
||||
infos
|
||||
}
|
||||
|
||||
/// Query all relations belonging to a theory (using compiled query engine).
|
||||
///
|
||||
/// This is equivalent to `query_theory_rels` in bootstrap_queries.rs,
|
||||
/// but uses the Query compiler for the initial scan+filter.
|
||||
pub fn query_theory_rels_compiled(&self, theory_slid: Slid) -> Vec<RelInfo> {
|
||||
let Some(rel_sort) = self.sort_ids.rel else {
|
||||
return vec![];
|
||||
};
|
||||
let Some(theory_func) = self.func_ids.rel_theory else {
|
||||
return vec![];
|
||||
};
|
||||
let Some(dom_func) = self.func_ids.rel_dom else {
|
||||
return vec![];
|
||||
};
|
||||
|
||||
// Compile and execute the query to find matching relations
|
||||
let plan = compile_simple_filter(rel_sort, theory_func, theory_slid);
|
||||
let result = execute(&plan, &self.meta);
|
||||
|
||||
// Convert query results to RelInfo (with domain lookup)
|
||||
let mut infos = Vec::new();
|
||||
for tuple in result.tuples.keys() {
|
||||
if let Some(&rel_slid) = tuple.first() {
|
||||
let name = self.get_element_name(rel_slid);
|
||||
let short_name = name.rsplit('/').next().unwrap_or(&name).to_string();
|
||||
|
||||
// Get domain DSort (using bootstrap logic)
|
||||
let domain = self
|
||||
.get_func(dom_func, rel_slid)
|
||||
.map(|ds| self.resolve_dsort(ds))
|
||||
.unwrap_or(DerivedSort::Product(vec![]));
|
||||
|
||||
infos.push(RelInfo {
|
||||
name: short_name,
|
||||
slid: rel_slid,
|
||||
domain,
|
||||
});
|
||||
}
|
||||
}
|
||||
// Sort by UUID to ensure deterministic order matching original creation order
|
||||
infos.sort_by_key(|info| self.get_element_uuid(info.slid));
|
||||
infos
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Instance queries (compiled versions)
|
||||
// ========================================================================
|
||||
|
||||
/// Query all elements belonging to an instance (using compiled query engine).
|
||||
///
|
||||
/// This is equivalent to `query_instance_elems` in bootstrap_queries.rs,
|
||||
/// but uses the Query compiler for the initial scan+filter.
|
||||
pub fn query_instance_elems_compiled(&self, instance_slid: Slid) -> Vec<ElemInfo> {
|
||||
let Some(elem_sort) = self.sort_ids.elem else {
|
||||
return vec![];
|
||||
};
|
||||
let Some(instance_func) = self.func_ids.elem_instance else {
|
||||
return vec![];
|
||||
};
|
||||
let Some(sort_func) = self.func_ids.elem_sort else {
|
||||
return vec![];
|
||||
};
|
||||
|
||||
// Compile and execute the query to find matching elements
|
||||
let plan = compile_simple_filter(elem_sort, instance_func, instance_slid);
|
||||
let result = execute(&plan, &self.meta);
|
||||
|
||||
// Convert query results to ElemInfo
|
||||
let mut infos = Vec::new();
|
||||
for tuple in result.tuples.keys() {
|
||||
if let Some(&elem_slid) = tuple.first() {
|
||||
let name = self.get_element_name(elem_slid);
|
||||
let short_name = name.rsplit('/').next().unwrap_or(&name).to_string();
|
||||
let srt_slid = self.get_func(sort_func, elem_slid);
|
||||
|
||||
infos.push(ElemInfo {
|
||||
name: short_name,
|
||||
slid: elem_slid,
|
||||
srt_slid,
|
||||
});
|
||||
}
|
||||
}
|
||||
// Sort by UUID to preserve original creation order
|
||||
infos.sort_by_key(|info| self.get_element_uuid(info.slid));
|
||||
infos
|
||||
}
|
||||
|
||||
/// Query all function values in an instance (using compiled query engine).
|
||||
///
|
||||
/// This is equivalent to `query_instance_func_vals` in bootstrap_queries.rs,
|
||||
/// but uses the Query compiler for the initial scan+filter.
|
||||
pub fn query_instance_func_vals_compiled(&self, instance_slid: Slid) -> Vec<FuncValInfo> {
|
||||
let Some(fv_sort) = self.sort_ids.func_val else {
|
||||
return vec![];
|
||||
};
|
||||
let Some(instance_func) = self.func_ids.func_val_instance else {
|
||||
return vec![];
|
||||
};
|
||||
let Some(func_func) = self.func_ids.func_val_func else {
|
||||
return vec![];
|
||||
};
|
||||
let Some(arg_func) = self.func_ids.func_val_arg else {
|
||||
return vec![];
|
||||
};
|
||||
let Some(result_func) = self.func_ids.func_val_result else {
|
||||
return vec![];
|
||||
};
|
||||
|
||||
// Compile and execute the query
|
||||
let plan = compile_simple_filter(fv_sort, instance_func, instance_slid);
|
||||
let result = execute(&plan, &self.meta);
|
||||
|
||||
// Convert query results to FuncValInfo
|
||||
let mut infos = Vec::new();
|
||||
for tuple in result.tuples.keys() {
|
||||
if let Some(&fv_slid) = tuple.first() {
|
||||
infos.push(FuncValInfo {
|
||||
slid: fv_slid,
|
||||
func_slid: self.get_func(func_func, fv_slid),
|
||||
arg_slid: self.get_func(arg_func, fv_slid),
|
||||
result_slid: self.get_func(result_func, fv_slid),
|
||||
});
|
||||
}
|
||||
}
|
||||
// Sort by UUID to preserve original creation order
|
||||
infos.sort_by_key(|info| self.get_element_uuid(info.slid));
|
||||
infos
|
||||
}
|
||||
|
||||
/// Query all relation tuples in an instance.
|
||||
///
|
||||
/// NOTE: Relation tuples are now stored in columnar batches (see `store::columnar`),
|
||||
/// not as individual GeologMeta elements. This function returns empty until
|
||||
/// columnar batch loading is implemented.
|
||||
///
|
||||
/// TODO: Implement columnar batch loading for relation tuples.
|
||||
pub fn query_instance_rel_tuples_compiled(&self, _instance_slid: Slid) -> Vec<RelTupleInfo> {
|
||||
// Relation tuples are stored in columnar batches, not GeologMeta elements.
|
||||
// Return empty until columnar batch loading is implemented.
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::repl::ReplState;
|
||||
|
||||
/// Test that compiled query matches bootstrap query results.
|
||||
#[test]
|
||||
fn test_compiled_matches_bootstrap_sorts() {
|
||||
let source = r#"
|
||||
theory TestTheory {
|
||||
A : Sort;
|
||||
B : Sort;
|
||||
C : Sort;
|
||||
f : A -> B;
|
||||
}
|
||||
"#;
|
||||
|
||||
let mut repl = ReplState::new();
|
||||
let _ = repl.execute_geolog(source);
|
||||
|
||||
let theory_slid = repl.store.resolve_name("TestTheory")
|
||||
.expect("Theory should exist").0;
|
||||
|
||||
// Compare bootstrap vs compiled
|
||||
let bootstrap = repl.store.query_theory_sorts(theory_slid);
|
||||
let compiled = repl.store.query_theory_sorts_compiled(theory_slid);
|
||||
|
||||
// Same number of results
|
||||
assert_eq!(
|
||||
bootstrap.len(), compiled.len(),
|
||||
"Bootstrap returned {} sorts, compiled returned {}",
|
||||
bootstrap.len(), compiled.len()
|
||||
);
|
||||
|
||||
// Same names (order may differ)
|
||||
let mut bootstrap_names: Vec<_> = bootstrap.iter().map(|s| &s.name).collect();
|
||||
let mut compiled_names: Vec<_> = compiled.iter().map(|s| &s.name).collect();
|
||||
bootstrap_names.sort();
|
||||
compiled_names.sort();
|
||||
|
||||
assert_eq!(bootstrap_names, compiled_names, "Sort names should match");
|
||||
}
|
||||
|
||||
/// Test compiled query with theory that has no sorts.
|
||||
#[test]
|
||||
fn test_compiled_empty_theory() {
|
||||
let source = r#"
|
||||
theory EmptyTheory {
|
||||
}
|
||||
"#;
|
||||
|
||||
let mut repl = ReplState::new();
|
||||
let _ = repl.execute_geolog(source);
|
||||
|
||||
let theory_slid = repl.store.resolve_name("EmptyTheory")
|
||||
.expect("Theory should exist").0;
|
||||
|
||||
let bootstrap = repl.store.query_theory_sorts(theory_slid);
|
||||
let compiled = repl.store.query_theory_sorts_compiled(theory_slid);
|
||||
|
||||
assert_eq!(bootstrap.len(), 0);
|
||||
assert_eq!(compiled.len(), 0);
|
||||
}
|
||||
|
||||
/// Test that multiple theories have independent sorts.
|
||||
#[test]
|
||||
fn test_compiled_multiple_theories() {
|
||||
let source = r#"
|
||||
theory Theory1 {
|
||||
X : Sort;
|
||||
Y : Sort;
|
||||
}
|
||||
theory Theory2 {
|
||||
P : Sort;
|
||||
Q : Sort;
|
||||
R : Sort;
|
||||
}
|
||||
"#;
|
||||
|
||||
let mut repl = ReplState::new();
|
||||
let _ = repl.execute_geolog(source);
|
||||
|
||||
let theory1_slid = repl.store.resolve_name("Theory1")
|
||||
.expect("Theory1 should exist").0;
|
||||
let theory2_slid = repl.store.resolve_name("Theory2")
|
||||
.expect("Theory2 should exist").0;
|
||||
|
||||
// Theory1 should have X, Y
|
||||
let t1_bootstrap = repl.store.query_theory_sorts(theory1_slid);
|
||||
let t1_compiled = repl.store.query_theory_sorts_compiled(theory1_slid);
|
||||
|
||||
assert_eq!(t1_bootstrap.len(), 2);
|
||||
assert_eq!(t1_compiled.len(), 2);
|
||||
|
||||
// Theory2 should have P, Q, R
|
||||
let t2_bootstrap = repl.store.query_theory_sorts(theory2_slid);
|
||||
let t2_compiled = repl.store.query_theory_sorts_compiled(theory2_slid);
|
||||
|
||||
assert_eq!(t2_bootstrap.len(), 3);
|
||||
assert_eq!(t2_compiled.len(), 3);
|
||||
|
||||
// Names should be independent
|
||||
let t1_names: std::collections::HashSet<_> =
|
||||
t1_compiled.iter().map(|s| &s.name).collect();
|
||||
let t2_names: std::collections::HashSet<_> =
|
||||
t2_compiled.iter().map(|s| &s.name).collect();
|
||||
|
||||
assert!(t1_names.contains(&"X".to_string()));
|
||||
assert!(t1_names.contains(&"Y".to_string()));
|
||||
assert!(t2_names.contains(&"P".to_string()));
|
||||
assert!(t2_names.contains(&"Q".to_string()));
|
||||
assert!(t2_names.contains(&"R".to_string()));
|
||||
}
|
||||
|
||||
/// Test that compiled query matches bootstrap query for functions.
|
||||
#[test]
|
||||
fn test_compiled_matches_bootstrap_funcs() {
|
||||
let source = r#"
|
||||
theory FuncTheory {
|
||||
A : Sort;
|
||||
B : Sort;
|
||||
C : Sort;
|
||||
f : A -> B;
|
||||
g : B -> C;
|
||||
h : A -> C;
|
||||
}
|
||||
"#;
|
||||
|
||||
let mut repl = ReplState::new();
|
||||
let _ = repl.execute_geolog(source);
|
||||
|
||||
let theory_slid = repl.store.resolve_name("FuncTheory")
|
||||
.expect("Theory should exist").0;
|
||||
|
||||
// Compare bootstrap vs compiled
|
||||
let bootstrap = repl.store.query_theory_funcs(theory_slid);
|
||||
let compiled = repl.store.query_theory_funcs_compiled(theory_slid);
|
||||
|
||||
// Same number of results
|
||||
assert_eq!(
|
||||
bootstrap.len(), compiled.len(),
|
||||
"Bootstrap returned {} funcs, compiled returned {}",
|
||||
bootstrap.len(), compiled.len()
|
||||
);
|
||||
|
||||
// Same names (order may differ)
|
||||
let mut bootstrap_names: Vec<_> = bootstrap.iter().map(|f| &f.name).collect();
|
||||
let mut compiled_names: Vec<_> = compiled.iter().map(|f| &f.name).collect();
|
||||
bootstrap_names.sort();
|
||||
compiled_names.sort();
|
||||
|
||||
assert_eq!(bootstrap_names, compiled_names, "Function names should match");
|
||||
|
||||
// Verify we have the expected functions
|
||||
assert!(compiled_names.contains(&&"f".to_string()));
|
||||
assert!(compiled_names.contains(&&"g".to_string()));
|
||||
assert!(compiled_names.contains(&&"h".to_string()));
|
||||
}
|
||||
|
||||
/// Test that compiled query matches bootstrap query for relations.
|
||||
#[test]
|
||||
fn test_compiled_matches_bootstrap_rels() {
|
||||
let source = r#"
|
||||
theory RelTheory {
|
||||
Node : Sort;
|
||||
Source : Node -> Prop;
|
||||
Sink : Node -> Prop;
|
||||
Connected : [x: Node, y: Node] -> Prop;
|
||||
}
|
||||
"#;
|
||||
|
||||
let mut repl = ReplState::new();
|
||||
let _ = repl.execute_geolog(source);
|
||||
|
||||
let theory_slid = repl.store.resolve_name("RelTheory")
|
||||
.expect("Theory should exist").0;
|
||||
|
||||
// Compare bootstrap vs compiled
|
||||
let bootstrap = repl.store.query_theory_rels(theory_slid);
|
||||
let compiled = repl.store.query_theory_rels_compiled(theory_slid);
|
||||
|
||||
// Same number of results
|
||||
assert_eq!(
|
||||
bootstrap.len(), compiled.len(),
|
||||
"Bootstrap returned {} rels, compiled returned {}",
|
||||
bootstrap.len(), compiled.len()
|
||||
);
|
||||
|
||||
// Same names (order may differ)
|
||||
let mut bootstrap_names: Vec<_> = bootstrap.iter().map(|r| &r.name).collect();
|
||||
let mut compiled_names: Vec<_> = compiled.iter().map(|r| &r.name).collect();
|
||||
bootstrap_names.sort();
|
||||
compiled_names.sort();
|
||||
|
||||
assert_eq!(bootstrap_names, compiled_names, "Relation names should match");
|
||||
|
||||
// Verify we have the expected relations
|
||||
assert!(compiled_names.contains(&&"Source".to_string()));
|
||||
assert!(compiled_names.contains(&&"Sink".to_string()));
|
||||
assert!(compiled_names.contains(&&"Connected".to_string()));
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Instance query tests
|
||||
// ========================================================================
|
||||
|
||||
/// Test that compiled query matches bootstrap for instance elements.
|
||||
#[test]
|
||||
fn test_compiled_matches_bootstrap_instance_elems() {
|
||||
let source = r#"
|
||||
theory Graph {
|
||||
V : Sort;
|
||||
E : Sort;
|
||||
src : E -> V;
|
||||
tgt : E -> V;
|
||||
}
|
||||
|
||||
instance SimpleGraph : Graph = {
|
||||
a : V;
|
||||
b : V;
|
||||
c : V;
|
||||
e1 : E;
|
||||
e2 : E;
|
||||
e1 src = a;
|
||||
e1 tgt = b;
|
||||
e2 src = b;
|
||||
e2 tgt = c;
|
||||
}
|
||||
"#;
|
||||
|
||||
let mut repl = ReplState::new();
|
||||
let _ = repl.execute_geolog(source);
|
||||
|
||||
let instance_slid = repl.store.resolve_name("SimpleGraph")
|
||||
.expect("Instance should exist").0;
|
||||
|
||||
// Compare bootstrap vs compiled
|
||||
let bootstrap = repl.store.query_instance_elems(instance_slid);
|
||||
let compiled = repl.store.query_instance_elems_compiled(instance_slid);
|
||||
|
||||
// Same number of results
|
||||
assert_eq!(
|
||||
bootstrap.len(), compiled.len(),
|
||||
"Bootstrap returned {} elems, compiled returned {}",
|
||||
bootstrap.len(), compiled.len()
|
||||
);
|
||||
|
||||
// Should have 5 elements: a, b, c, e1, e2
|
||||
assert_eq!(compiled.len(), 5, "Expected 5 elements");
|
||||
|
||||
// Same names (order may differ)
|
||||
let mut bootstrap_names: Vec<_> = bootstrap.iter().map(|e| &e.name).collect();
|
||||
let mut compiled_names: Vec<_> = compiled.iter().map(|e| &e.name).collect();
|
||||
bootstrap_names.sort();
|
||||
compiled_names.sort();
|
||||
|
||||
assert_eq!(bootstrap_names, compiled_names, "Element names should match");
|
||||
}
|
||||
|
||||
/// Test that compiled query matches bootstrap for function values.
|
||||
#[test]
|
||||
fn test_compiled_matches_bootstrap_func_vals() {
|
||||
let source = r#"
|
||||
theory Graph {
|
||||
V : Sort;
|
||||
E : Sort;
|
||||
src : E -> V;
|
||||
tgt : E -> V;
|
||||
}
|
||||
|
||||
instance TwoEdges : Graph = {
|
||||
v1 : V;
|
||||
v2 : V;
|
||||
v3 : V;
|
||||
edge1 : E;
|
||||
edge2 : E;
|
||||
edge1 src = v1;
|
||||
edge1 tgt = v2;
|
||||
edge2 src = v2;
|
||||
edge2 tgt = v3;
|
||||
}
|
||||
"#;
|
||||
|
||||
let mut repl = ReplState::new();
|
||||
let _ = repl.execute_geolog(source);
|
||||
|
||||
let instance_slid = repl.store.resolve_name("TwoEdges")
|
||||
.expect("Instance should exist").0;
|
||||
|
||||
// Compare bootstrap vs compiled
|
||||
let bootstrap = repl.store.query_instance_func_vals(instance_slid);
|
||||
let compiled = repl.store.query_instance_func_vals_compiled(instance_slid);
|
||||
|
||||
// Same number of results
|
||||
assert_eq!(
|
||||
bootstrap.len(), compiled.len(),
|
||||
"Bootstrap returned {} func_vals, compiled returned {}",
|
||||
bootstrap.len(), compiled.len()
|
||||
);
|
||||
|
||||
// Should have 4 function values: edge1.src, edge1.tgt, edge2.src, edge2.tgt
|
||||
assert_eq!(compiled.len(), 4, "Expected 4 function values");
|
||||
}
|
||||
|
||||
/// Test that compiled query matches bootstrap for relation tuples.
|
||||
///
|
||||
/// NOTE: Relation tuples are now stored in columnar batches (see store::columnar),
|
||||
/// not as individual GeologMeta elements. The bootstrap and compiled queries
|
||||
/// for RelTuple elements return empty since we no longer create those elements.
|
||||
///
|
||||
/// Relation tuple data is now accessed via `Store::load_instance_data_batches()`.
|
||||
#[test]
|
||||
fn test_compiled_matches_bootstrap_rel_tuples() {
|
||||
let source = r#"
|
||||
theory NodeMarking {
|
||||
Node : Sort;
|
||||
Marked : [n: Node] -> Prop;
|
||||
}
|
||||
|
||||
instance ThreeNodes : NodeMarking = {
|
||||
n1 : Node;
|
||||
n2 : Node;
|
||||
n3 : Node;
|
||||
[n: n1] Marked;
|
||||
[n: n3] Marked;
|
||||
}
|
||||
"#;
|
||||
|
||||
let mut repl = ReplState::new();
|
||||
let _ = repl.execute_geolog(source);
|
||||
|
||||
let instance_slid = repl.store.resolve_name("ThreeNodes")
|
||||
.expect("Instance should exist").0;
|
||||
|
||||
// Compare bootstrap vs compiled - both should return empty now
|
||||
// since relation tuples are stored in columnar batches, not GeologMeta
|
||||
let bootstrap = repl.store.query_instance_rel_tuples(instance_slid);
|
||||
let compiled = repl.store.query_instance_rel_tuples_compiled(instance_slid);
|
||||
|
||||
// Same number of results (both empty)
|
||||
assert_eq!(
|
||||
bootstrap.len(), compiled.len(),
|
||||
"Bootstrap returned {} rel_tuples, compiled returned {}",
|
||||
bootstrap.len(), compiled.len()
|
||||
);
|
||||
|
||||
// Relation tuples are no longer stored as GeologMeta elements
|
||||
// They're in columnar batches accessed via load_instance_data_batches()
|
||||
assert_eq!(compiled.len(), 0, "RelTuple elements are not created (tuples in columnar batches)");
|
||||
|
||||
// Note: In in-memory mode (no store path), columnar batches aren't persisted.
|
||||
// The in-memory Structure still has the relation tuples - they're just not
|
||||
// serialized to disk. For tests with persistence, use a temp dir.
|
||||
//
|
||||
// The relation tuples are accessible via the in-memory Structure:
|
||||
use crate::core::RelationStorage;
|
||||
let entry = repl.instances.get("ThreeNodes").expect("Instance entry should exist");
|
||||
let rel_count: usize = entry.structure.relations.iter()
|
||||
.map(|r| r.len())
|
||||
.sum();
|
||||
assert_eq!(rel_count, 2, "Expected 2 relation tuples in in-memory Structure");
|
||||
}
|
||||
|
||||
/// Test compiled query with empty instance.
|
||||
#[test]
|
||||
fn test_compiled_empty_instance() {
|
||||
let source = r#"
|
||||
theory Simple {
|
||||
T : Sort;
|
||||
}
|
||||
|
||||
instance EmptyInst : Simple = {
|
||||
}
|
||||
"#;
|
||||
|
||||
let mut repl = ReplState::new();
|
||||
let _ = repl.execute_geolog(source);
|
||||
|
||||
let instance_slid = repl.store.resolve_name("EmptyInst")
|
||||
.expect("Instance should exist").0;
|
||||
|
||||
let bootstrap_elems = repl.store.query_instance_elems(instance_slid);
|
||||
let compiled_elems = repl.store.query_instance_elems_compiled(instance_slid);
|
||||
assert_eq!(bootstrap_elems.len(), 0);
|
||||
assert_eq!(compiled_elems.len(), 0);
|
||||
|
||||
let bootstrap_fvs = repl.store.query_instance_func_vals(instance_slid);
|
||||
let compiled_fvs = repl.store.query_instance_func_vals_compiled(instance_slid);
|
||||
assert_eq!(bootstrap_fvs.len(), 0);
|
||||
assert_eq!(compiled_fvs.len(), 0);
|
||||
|
||||
let bootstrap_rts = repl.store.query_instance_rel_tuples(instance_slid);
|
||||
let compiled_rts = repl.store.query_instance_rel_tuples_compiled(instance_slid);
|
||||
assert_eq!(bootstrap_rts.len(), 0);
|
||||
assert_eq!(compiled_rts.len(), 0);
|
||||
}
|
||||
}
|
||||
1386
src/query/to_relalg.rs
Normal file
1386
src/query/to_relalg.rs
Normal file
File diff suppressed because it is too large
Load Diff
1659
src/repl.rs
Normal file
1659
src/repl.rs
Normal file
File diff suppressed because it is too large
Load Diff
294
src/serialize.rs
Normal file
294
src/serialize.rs
Normal file
@ -0,0 +1,294 @@
|
||||
//! Structure serialization and deserialization.
|
||||
//!
|
||||
//! Provides rkyv-based serialization for `Structure` with both:
|
||||
//! - `save_structure` / `load_structure`: heap-allocated deserialization
|
||||
//! - `load_structure_mapped`: zero-copy memory-mapped access
|
||||
|
||||
use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
|
||||
use memmap2::Mmap;
|
||||
use rkyv::ser::serializers::AllocSerializer;
|
||||
use rkyv::ser::Serializer;
|
||||
use rkyv::{check_archived_root, Archive, Deserialize, Serialize};
|
||||
|
||||
use crate::core::{FunctionColumn, ProductStorage, RelationStorage, SortId, Structure, TupleId, VecRelation};
|
||||
use crate::id::{get_luid, get_slid, some_luid, some_slid, Luid, NumericId, Slid};
|
||||
|
||||
// ============================================================================
|
||||
// SERIALIZABLE DATA TYPES
|
||||
// ============================================================================
|
||||
|
||||
/// Serializable form of a relation
|
||||
#[derive(Archive, Deserialize, Serialize)]
|
||||
#[archive(check_bytes)]
|
||||
pub struct RelationData {
|
||||
pub arity: usize,
|
||||
pub tuples: Vec<Vec<Slid>>,
|
||||
pub extent: Vec<TupleId>,
|
||||
}
|
||||
|
||||
/// Serializable form of a function column
|
||||
#[derive(Archive, Deserialize, Serialize)]
|
||||
#[archive(check_bytes)]
|
||||
pub enum FunctionColumnData {
|
||||
Local(Vec<Option<usize>>),
|
||||
External(Vec<Option<usize>>),
|
||||
/// Product domain: maps tuples of sort-local indices to result Slid index,
|
||||
/// along with the field sort IDs for reconstruction
|
||||
ProductLocal {
|
||||
entries: Vec<(Vec<usize>, usize)>,
|
||||
field_sorts: Vec<usize>,
|
||||
},
|
||||
/// Product codomain: base domain maps to multiple fields
|
||||
ProductCodomain {
|
||||
/// One column per field - each Vec<Option<usize>> is indexed by domain sort-local index
|
||||
field_columns: Vec<Vec<Option<usize>>>,
|
||||
field_names: Vec<String>,
|
||||
field_sorts: Vec<usize>,
|
||||
domain_sort: usize,
|
||||
},
|
||||
}
|
||||
|
||||
/// Serializable form of a Structure
|
||||
#[derive(Archive, Deserialize, Serialize)]
|
||||
#[archive(check_bytes)]
|
||||
pub struct StructureData {
|
||||
pub num_sorts: usize,
|
||||
pub luids: Vec<Luid>,
|
||||
pub sorts: Vec<SortId>,
|
||||
pub functions: Vec<FunctionColumnData>,
|
||||
pub relations: Vec<RelationData>,
|
||||
}
|
||||
|
||||
impl StructureData {
|
||||
pub fn from_structure(structure: &Structure) -> Self {
|
||||
let functions = structure
|
||||
.functions
|
||||
.iter()
|
||||
.map(|func_col| match func_col {
|
||||
FunctionColumn::Local(col) => FunctionColumnData::Local(
|
||||
col.iter()
|
||||
.map(|&opt| get_slid(opt).map(|s| s.index()))
|
||||
.collect(),
|
||||
),
|
||||
FunctionColumn::External(col) => FunctionColumnData::External(
|
||||
col.iter()
|
||||
.map(|&opt| get_luid(opt).map(|l| l.index()))
|
||||
.collect(),
|
||||
),
|
||||
FunctionColumn::ProductLocal {
|
||||
storage,
|
||||
field_sorts,
|
||||
} => {
|
||||
let entries: Vec<(Vec<usize>, usize)> = storage
|
||||
.iter_defined()
|
||||
.map(|(tuple, result)| (tuple, result.index()))
|
||||
.collect();
|
||||
FunctionColumnData::ProductLocal {
|
||||
entries,
|
||||
field_sorts: field_sorts.clone(),
|
||||
}
|
||||
}
|
||||
FunctionColumn::ProductCodomain {
|
||||
field_columns,
|
||||
field_names,
|
||||
field_sorts,
|
||||
domain_sort,
|
||||
} => {
|
||||
let serialized_columns: Vec<Vec<Option<usize>>> = field_columns
|
||||
.iter()
|
||||
.map(|col| {
|
||||
col.iter()
|
||||
.map(|&opt| get_slid(opt).map(|s| s.index()))
|
||||
.collect()
|
||||
})
|
||||
.collect();
|
||||
FunctionColumnData::ProductCodomain {
|
||||
field_columns: serialized_columns,
|
||||
field_names: field_names.clone(),
|
||||
field_sorts: field_sorts.clone(),
|
||||
domain_sort: *domain_sort,
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let relations = structure
|
||||
.relations
|
||||
.iter()
|
||||
.map(|rel| RelationData {
|
||||
arity: rel.arity(),
|
||||
tuples: rel.tuples.clone(),
|
||||
extent: rel.iter_ids().collect(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
num_sorts: structure.num_sorts(),
|
||||
luids: structure.luids.clone(),
|
||||
sorts: structure.sorts.clone(),
|
||||
functions,
|
||||
relations,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_structure(&self) -> Structure {
|
||||
use crate::id::NumericId;
|
||||
|
||||
let mut structure = Structure::new(self.num_sorts);
|
||||
|
||||
for (slid_idx, (&luid, &sort_id)) in self.luids.iter().zip(self.sorts.iter()).enumerate() {
|
||||
let added_slid = structure.add_element_with_luid(luid, sort_id);
|
||||
debug_assert_eq!(added_slid, Slid::from_usize(slid_idx));
|
||||
}
|
||||
|
||||
structure.functions = self
|
||||
.functions
|
||||
.iter()
|
||||
.map(|func_data| match func_data {
|
||||
FunctionColumnData::Local(col) => FunctionColumn::Local(
|
||||
col.iter()
|
||||
.map(|&opt| opt.map(Slid::from_usize).and_then(some_slid))
|
||||
.collect(),
|
||||
),
|
||||
FunctionColumnData::External(col) => FunctionColumn::External(
|
||||
col.iter()
|
||||
.map(|&opt| opt.map(Luid::from_usize).and_then(some_luid))
|
||||
.collect(),
|
||||
),
|
||||
FunctionColumnData::ProductLocal {
|
||||
entries,
|
||||
field_sorts,
|
||||
} => {
|
||||
let mut storage = ProductStorage::new_general();
|
||||
for (tuple, result) in entries {
|
||||
storage
|
||||
.set(tuple, Slid::from_usize(*result))
|
||||
.expect("no conflicts in serialized data");
|
||||
}
|
||||
FunctionColumn::ProductLocal {
|
||||
storage,
|
||||
field_sorts: field_sorts.clone(),
|
||||
}
|
||||
}
|
||||
FunctionColumnData::ProductCodomain {
|
||||
field_columns,
|
||||
field_names,
|
||||
field_sorts,
|
||||
domain_sort,
|
||||
} => {
|
||||
let restored_columns: Vec<Vec<crate::id::OptSlid>> = field_columns
|
||||
.iter()
|
||||
.map(|col| {
|
||||
col.iter()
|
||||
.map(|&opt| opt.map(Slid::from_usize).and_then(some_slid))
|
||||
.collect()
|
||||
})
|
||||
.collect();
|
||||
FunctionColumn::ProductCodomain {
|
||||
field_columns: restored_columns,
|
||||
field_names: field_names.clone(),
|
||||
field_sorts: field_sorts.clone(),
|
||||
domain_sort: *domain_sort,
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
structure.relations = self
|
||||
.relations
|
||||
.iter()
|
||||
.map(|rel_data| {
|
||||
let mut rel = VecRelation::new(rel_data.arity);
|
||||
for tuple in &rel_data.tuples {
|
||||
rel.tuple_to_id.insert(tuple.clone(), rel.tuples.len());
|
||||
rel.tuples.push(tuple.clone());
|
||||
}
|
||||
for &tuple_id in &rel_data.extent {
|
||||
rel.extent.insert(tuple_id as u64);
|
||||
}
|
||||
rel
|
||||
})
|
||||
.collect();
|
||||
|
||||
structure
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SAVE / LOAD FUNCTIONS
|
||||
// ============================================================================
|
||||
|
||||
/// Save a Structure to a file
|
||||
pub fn save_structure(structure: &Structure, path: &Path) -> Result<(), String> {
|
||||
let data = StructureData::from_structure(structure);
|
||||
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent).map_err(|e| format!("Failed to create directory: {}", e))?;
|
||||
}
|
||||
|
||||
let mut serializer = AllocSerializer::<4096>::default();
|
||||
serializer
|
||||
.serialize_value(&data)
|
||||
.map_err(|e| format!("Failed to serialize structure: {}", e))?;
|
||||
let bytes = serializer.into_serializer().into_inner();
|
||||
|
||||
let temp_path = path.with_extension("tmp");
|
||||
{
|
||||
let mut file =
|
||||
File::create(&temp_path).map_err(|e| format!("Failed to create temp file: {}", e))?;
|
||||
file.write_all(&bytes)
|
||||
.map_err(|e| format!("Failed to write file: {}", e))?;
|
||||
file.sync_all()
|
||||
.map_err(|e| format!("Failed to sync file: {}", e))?;
|
||||
}
|
||||
|
||||
fs::rename(&temp_path, path).map_err(|e| format!("Failed to rename file: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Load a Structure from a file (deserializes into heap-allocated Structure)
|
||||
///
|
||||
/// Use this when you need a mutable Structure or when access patterns involve
|
||||
/// heavy computation on the data. For read-only access to large structures,
|
||||
/// prefer `load_structure_mapped` which is ~100-400x faster.
|
||||
pub fn load_structure(path: &Path) -> Result<Structure, String> {
|
||||
let file = File::open(path).map_err(|e| format!("Failed to open file: {}", e))?;
|
||||
|
||||
let mmap = unsafe { Mmap::map(&file) }.map_err(|e| format!("Failed to mmap file: {}", e))?;
|
||||
|
||||
if mmap.is_empty() {
|
||||
return Err("Empty structure file".to_string());
|
||||
}
|
||||
|
||||
let archived = check_archived_root::<StructureData>(&mmap)
|
||||
.map_err(|e| format!("Failed to validate archive: {}", e))?;
|
||||
|
||||
let data: StructureData = archived
|
||||
.deserialize(&mut rkyv::Infallible)
|
||||
.map_err(|_| "Failed to deserialize structure")?;
|
||||
|
||||
Ok(data.to_structure())
|
||||
}
|
||||
|
||||
/// Load a Structure from a file with zero-copy access (memory-mapped)
|
||||
///
|
||||
/// This is ~100-400x faster than `load_structure` for large structures because
|
||||
/// it doesn't deserialize the data - it accesses the archived format directly
|
||||
/// from the memory map.
|
||||
///
|
||||
/// Use this for:
|
||||
/// - Read-only access to large structures
|
||||
/// - Fast startup when you just need to query existing data
|
||||
/// - Reducing memory footprint (only the mmap exists, no heap copies)
|
||||
///
|
||||
/// Trade-offs:
|
||||
/// - Read-only (cannot modify the structure)
|
||||
/// - Slightly different API (returns `MappedStructure` instead of `Structure`)
|
||||
/// - File must remain valid for lifetime of `MappedStructure`
|
||||
pub fn load_structure_mapped(path: &Path) -> Result<crate::zerocopy::MappedStructure, String> {
|
||||
crate::zerocopy::MappedStructure::open(path)
|
||||
}
|
||||
415
src/solver/mod.rs
Normal file
415
src/solver/mod.rs
Normal file
@ -0,0 +1,415 @@
|
||||
//! Solver infrastructure for instance synthesis
|
||||
//!
|
||||
//! This module provides the search tree and tactics for finding instances
|
||||
//! of geometric theories. The architecture is designed for AI-agent-driven
|
||||
//! search: the agent manipulates an explicit search tree, running automated
|
||||
//! tactics for bounded time and providing strategic guidance.
|
||||
//!
|
||||
//! # Key Concepts
|
||||
//!
|
||||
//! - **Search Tree**: Explicit tree of partial models, not implicit in call stack
|
||||
//! - **Partial Model**: A `Structure` where carriers can grow, functions can become
|
||||
//! more defined, and relations can have more tuples asserted
|
||||
//! - **Refinement**: Natural preorder on Structures (really a category of partial
|
||||
//! models with refinement morphisms)
|
||||
//! - **Obligation**: When an axiom's premise is satisfied but conclusion isn't,
|
||||
//! we have an obligation to witness the conclusion (not a failure!)
|
||||
//! - **Tactic**: Automated search strategy that runs for bounded time
|
||||
//! - **Agent Loop**: AI decides which nodes to explore, provides hints, estimates
|
||||
//! success probabilities, allocates resources
|
||||
//!
|
||||
//! # The Refinement Order
|
||||
//!
|
||||
//! A Structure S₁ refines to S₂ (S₁ ≤ S₂) when:
|
||||
//! - All carriers in S₁ are subsets of corresponding carriers in S₂
|
||||
//! - All defined function values in S₁ are preserved in S₂
|
||||
//! - All asserted relation tuples in S₁ are preserved in S₂
|
||||
//!
|
||||
//! A search node conjectures: "∃ complete, axiom-satisfying Structure above this one"
|
||||
//!
|
||||
//! # Obligations, Equations, and Derivations
|
||||
//!
|
||||
//! In geometric logic, axiom consequents are always positive (existentials,
|
||||
//! disjunctions, atomic relations, equations). The refinement order on partial
|
||||
//! models includes not just adding facts, but also **quotienting by equations**
|
||||
//! (merging elements). This means:
|
||||
//!
|
||||
//! - **Obligation**: Premise satisfied, conclusion not yet witnessed → need to
|
||||
//! witness. Can always potentially be done by refinement.
|
||||
//!
|
||||
//! - **Pending Equation**: Two terms must be equal. Resolved by merging elements
|
||||
//! and propagating consequences (congruence closure).
|
||||
//!
|
||||
//! - **Unsat**: The ONLY way to get true unsatisfiability is if there exists a
|
||||
//! **Derivation** of `⊢ False` from the instantiated axioms. This is
|
||||
//! proof-theoretic: we need to actually derive False, not just have "conflicts".
|
||||
//!
|
||||
//! For example, "function f already maps a to b, but we need f(a) = c" is NOT
|
||||
//! unsat—it's a pending equation `b = c`. We merge b and c, propagate, and only
|
||||
//! if this leads to deriving False (via some axiom like `R(x), S(x) ⊢ False`)
|
||||
//! do we have true unsatisfiability.
|
||||
//!
|
||||
//! This is essentially SMT solving with EUF (equality + uninterpreted functions)
|
||||
//! plus geometric theory axioms, where the goal is to either:
|
||||
//! 1. Find a complete model satisfying all axioms, or
|
||||
//! 2. Derive `⊢ False` proving no such model exists
|
||||
//!
|
||||
//! # Unified Model Enumeration API
|
||||
//!
|
||||
//! The high-level API unifies `:solve` and `:query` under a common abstraction:
|
||||
//! finding maximal elements of the posetal reflection of the category of models.
|
||||
//!
|
||||
//! - [`solve`]: Find models from scratch (base = empty structure)
|
||||
//! - [`query`]: Find extensions of an existing model to a larger theory
|
||||
//! - [`enumerate_models`]: Core unified function (both above are wrappers)
|
||||
//!
|
||||
//! ```ignore
|
||||
//! // Find any model of a theory
|
||||
//! let result = solve(theory, Budget::quick());
|
||||
//!
|
||||
//! // Extend an existing model to satisfy additional axioms
|
||||
//! let result = query(base_structure, universe, extended_theory, budget);
|
||||
//! ```
|
||||
|
||||
mod tactics;
|
||||
mod tree;
|
||||
mod types;
|
||||
|
||||
// Re-export main types
|
||||
pub use tactics::{AutoTactic, Budget, CheckTactic, EnumerateFunctionTactic, ForwardChainingTactic, PropagateEquationsTactic, Tactic, TacticResult};
|
||||
pub use tree::SearchTree;
|
||||
pub use types::{
|
||||
AxiomCheckResult, ConflictClause, CongruenceClosure, EquationReason, NodeDetail, NodeId,
|
||||
NodeStatus, Obligation, PendingEquation, SearchNode, SearchSummary,
|
||||
};
|
||||
|
||||
// Unified model enumeration API (see below)
|
||||
// - enumerate_models: core unified function
|
||||
// - solve: convenience for :solve (find models from scratch)
|
||||
// - query: convenience for :query (extend existing models)
|
||||
// - EnumerationResult: result type
|
||||
|
||||
// Re-export union-find for convenience
|
||||
pub use egglog_union_find::UnionFind;
|
||||
|
||||
// ============================================================================
|
||||
// UNIFIED MODEL ENUMERATION API
|
||||
// ============================================================================
|
||||
|
||||
use std::rc::Rc;
|
||||
use crate::core::{DerivedSort, ElaboratedTheory, Structure};
|
||||
use crate::universe::Universe;
|
||||
|
||||
/// Result of model enumeration.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum EnumerationResult {
|
||||
/// Found a complete model satisfying all axioms.
|
||||
Found {
|
||||
/// The witness structure (model).
|
||||
model: Structure,
|
||||
/// Time taken in milliseconds.
|
||||
time_ms: f64,
|
||||
},
|
||||
/// Proved no model exists (derived False).
|
||||
Unsat {
|
||||
/// Time taken in milliseconds.
|
||||
time_ms: f64,
|
||||
},
|
||||
/// Search incomplete (budget exhausted or still has obligations).
|
||||
Incomplete {
|
||||
/// Partial structure so far.
|
||||
partial: Structure,
|
||||
/// Time taken in milliseconds.
|
||||
time_ms: f64,
|
||||
/// Description of why incomplete.
|
||||
reason: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// Unified model enumeration: find models of `theory` extending `base`.
|
||||
///
|
||||
/// This is the core API that unifies `:solve` and `:query`:
|
||||
/// - `:solve T` = `enumerate_models(empty, T, budget)`
|
||||
/// - `:query M T'` = `enumerate_models(M, T', budget)` where T' extends M's theory
|
||||
///
|
||||
/// # Arguments
|
||||
/// - `base`: Starting structure (empty for `:solve`, existing model for `:query`)
|
||||
/// - `universe`: Universe for Luid allocation (should contain Luids from base)
|
||||
/// - `theory`: The theory to satisfy
|
||||
/// - `budget`: Resource limits for the search
|
||||
///
|
||||
/// # Returns
|
||||
/// - `Found` if a complete model was found
|
||||
/// - `Unsat` if no model exists (derived False)
|
||||
/// - `Incomplete` if budget exhausted or search blocked
|
||||
pub fn enumerate_models(
|
||||
base: Structure,
|
||||
universe: Universe,
|
||||
theory: Rc<ElaboratedTheory>,
|
||||
budget: Budget,
|
||||
) -> EnumerationResult {
|
||||
let start = std::time::Instant::now();
|
||||
let sig = &theory.theory.signature;
|
||||
|
||||
// Create search tree from base
|
||||
let mut tree = SearchTree::from_base(theory.clone(), base, universe);
|
||||
|
||||
// Initialize function and relation storage at root (if not already initialized)
|
||||
// This preserves any function values that were imported from param instances.
|
||||
let num_funcs = sig.functions.len();
|
||||
let num_rels = sig.relations.len();
|
||||
|
||||
// Only init functions if not already initialized (or wrong size)
|
||||
if tree.nodes[0].structure.functions.len() != num_funcs {
|
||||
let domain_sort_ids: Vec<Option<usize>> = sig
|
||||
.functions
|
||||
.iter()
|
||||
.map(|f| match &f.domain {
|
||||
DerivedSort::Base(sid) => Some(*sid),
|
||||
DerivedSort::Product(_) => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
if tree.init_functions(0, &domain_sort_ids).is_err() {
|
||||
return EnumerationResult::Incomplete {
|
||||
partial: tree.nodes[0].structure.clone(),
|
||||
time_ms: start.elapsed().as_secs_f64() * 1000.0,
|
||||
reason: "Failed to initialize function storage".to_string(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Only init relations if not already initialized (or wrong size)
|
||||
if tree.nodes[0].structure.relations.len() != num_rels {
|
||||
let arities: Vec<usize> = sig
|
||||
.relations
|
||||
.iter()
|
||||
.map(|r| match &r.domain {
|
||||
DerivedSort::Base(_) => 1,
|
||||
DerivedSort::Product(fields) => fields.len(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
if tree.init_relations(0, &arities).is_err() {
|
||||
return EnumerationResult::Incomplete {
|
||||
partial: tree.nodes[0].structure.clone(),
|
||||
time_ms: start.elapsed().as_secs_f64() * 1000.0,
|
||||
reason: "Failed to initialize relation storage".to_string(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Run AutoTactic
|
||||
let result = AutoTactic.run(&mut tree, 0, &budget);
|
||||
let time_ms = start.elapsed().as_secs_f64() * 1000.0;
|
||||
|
||||
match result {
|
||||
TacticResult::Solved => EnumerationResult::Found {
|
||||
model: tree.nodes[0].structure.clone(),
|
||||
time_ms,
|
||||
},
|
||||
TacticResult::Unsat(_) => EnumerationResult::Unsat { time_ms },
|
||||
TacticResult::HasObligations(obs) => EnumerationResult::Incomplete {
|
||||
partial: tree.nodes[0].structure.clone(),
|
||||
time_ms,
|
||||
reason: format!("Has {} unfulfilled obligations", obs.len()),
|
||||
},
|
||||
TacticResult::Progress { steps_taken, .. } => EnumerationResult::Incomplete {
|
||||
partial: tree.nodes[0].structure.clone(),
|
||||
time_ms,
|
||||
reason: format!("Made progress ({} steps) but not complete", steps_taken),
|
||||
},
|
||||
TacticResult::Timeout { steps_taken } => EnumerationResult::Incomplete {
|
||||
partial: tree.nodes[0].structure.clone(),
|
||||
time_ms,
|
||||
reason: format!("Timeout after {} steps", steps_taken),
|
||||
},
|
||||
TacticResult::Error(e) => EnumerationResult::Incomplete {
|
||||
partial: tree.nodes[0].structure.clone(),
|
||||
time_ms,
|
||||
reason: format!("Error: {}", e),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience: solve a theory from scratch (find any model).
|
||||
///
|
||||
/// Equivalent to `enumerate_models(empty_structure, Universe::new(), theory, budget)`.
|
||||
pub fn solve(theory: Rc<ElaboratedTheory>, budget: Budget) -> EnumerationResult {
|
||||
let num_sorts = theory.theory.signature.sorts.len();
|
||||
let base = Structure::new(num_sorts);
|
||||
enumerate_models(base, Universe::new(), theory, budget)
|
||||
}
|
||||
|
||||
/// Convenience: query/extend an existing model.
|
||||
///
|
||||
/// Equivalent to `enumerate_models(base, universe, extension_theory, budget)`.
|
||||
pub fn query(
|
||||
base: Structure,
|
||||
universe: Universe,
|
||||
extension_theory: Rc<ElaboratedTheory>,
|
||||
budget: Budget,
|
||||
) -> EnumerationResult {
|
||||
enumerate_models(base, universe, extension_theory, budget)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod unified_api_tests {
|
||||
use super::*;
|
||||
use crate::core::{Context, Formula, RelationStorage, Sequent, Signature, Term, Theory};
|
||||
|
||||
fn make_existential_theory() -> Rc<ElaboratedTheory> {
|
||||
// Theory: Node sort, R relation
|
||||
// Axiom: True |- ∃x:Node. R(x)
|
||||
let mut sig = Signature::new();
|
||||
let node = sig.add_sort("Node".to_string());
|
||||
sig.add_relation("R".to_string(), DerivedSort::Base(node));
|
||||
|
||||
let axiom = Sequent {
|
||||
context: Context::new(),
|
||||
premise: Formula::True,
|
||||
conclusion: Formula::Exists(
|
||||
"x".to_string(),
|
||||
DerivedSort::Base(node),
|
||||
Box::new(Formula::Rel(0, Term::Var("x".to_string(), DerivedSort::Base(node)))),
|
||||
),
|
||||
};
|
||||
|
||||
Rc::new(ElaboratedTheory {
|
||||
params: vec![],
|
||||
theory: Theory {
|
||||
name: "ExistsR".to_string(),
|
||||
signature: sig,
|
||||
axioms: vec![axiom],
|
||||
axiom_names: vec!["ax/exists_r".to_string()],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_solve_finds_model() {
|
||||
// solve = enumerate_models with empty base
|
||||
let theory = make_existential_theory();
|
||||
let result = solve(theory, Budget::quick());
|
||||
|
||||
match result {
|
||||
EnumerationResult::Found { model, .. } => {
|
||||
// Should have at least one element with R
|
||||
assert!(model.carrier_size(0) >= 1);
|
||||
assert!(!model.relations[0].is_empty());
|
||||
}
|
||||
other => panic!("Expected Found, got {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_query_extends_base() {
|
||||
// query = enumerate_models with existing base
|
||||
let theory = make_existential_theory();
|
||||
|
||||
// Create base with one element, R not yet holding
|
||||
let mut universe = Universe::new();
|
||||
let mut base = Structure::new(1);
|
||||
let (_elem, _) = base.add_element(&mut universe, 0);
|
||||
base.init_relations(&[1]);
|
||||
|
||||
// query should extend the base to satisfy the axiom
|
||||
let result = query(base, universe, theory, Budget::quick());
|
||||
|
||||
match result {
|
||||
EnumerationResult::Found { model, .. } => {
|
||||
// R should now have at least one tuple
|
||||
assert!(!model.relations[0].is_empty());
|
||||
}
|
||||
other => panic!("Expected Found, got {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unification_equivalence() {
|
||||
// Demonstrate: solve(T) = enumerate_models(empty, T)
|
||||
let theory = make_existential_theory();
|
||||
let budget = Budget::quick();
|
||||
|
||||
// Method 1: solve
|
||||
let result1 = solve(theory.clone(), budget.clone());
|
||||
|
||||
// Method 2: enumerate_models with empty base
|
||||
let num_sorts = theory.theory.signature.sorts.len();
|
||||
let empty_base = Structure::new(num_sorts);
|
||||
let result2 = enumerate_models(empty_base, Universe::new(), theory, budget);
|
||||
|
||||
// Both should succeed (find a model)
|
||||
match (&result1, &result2) {
|
||||
(EnumerationResult::Found { .. }, EnumerationResult::Found { .. }) => {
|
||||
// Both found models - the unification works!
|
||||
}
|
||||
_ => panic!(
|
||||
"Expected both to find models, got {:?} and {:?}",
|
||||
result1, result2
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_solve_unsat_theory() {
|
||||
// Theory that derives False: forall a:A. |- false
|
||||
let mut sig = Signature::new();
|
||||
let _sort_a = sig.add_sort("A".to_string());
|
||||
|
||||
// Axiom: forall a:A. |- false
|
||||
let axiom = Sequent {
|
||||
context: Context::new(),
|
||||
premise: Formula::True,
|
||||
conclusion: Formula::False,
|
||||
};
|
||||
|
||||
let theory = Rc::new(ElaboratedTheory {
|
||||
params: vec![],
|
||||
theory: Theory {
|
||||
name: "Inconsistent".to_string(),
|
||||
signature: sig,
|
||||
axioms: vec![axiom],
|
||||
axiom_names: vec!["ax/inconsistent".to_string()],
|
||||
},
|
||||
});
|
||||
|
||||
let result = solve(theory, Budget::quick());
|
||||
|
||||
match result {
|
||||
EnumerationResult::Unsat { .. } => {
|
||||
// Correctly detected UNSAT
|
||||
}
|
||||
other => panic!("Expected Unsat, got {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_solve_trivial_theory() {
|
||||
// Theory with no axioms - should be trivially satisfied by empty structure
|
||||
let mut sig = Signature::new();
|
||||
sig.add_sort("A".to_string());
|
||||
sig.add_sort("B".to_string());
|
||||
|
||||
let theory = Rc::new(ElaboratedTheory {
|
||||
params: vec![],
|
||||
theory: Theory {
|
||||
name: "Trivial".to_string(),
|
||||
signature: sig,
|
||||
axioms: vec![],
|
||||
axiom_names: vec![],
|
||||
},
|
||||
});
|
||||
|
||||
let result = solve(theory, Budget::quick());
|
||||
|
||||
match result {
|
||||
EnumerationResult::Found { model, .. } => {
|
||||
// Empty structure is a valid model
|
||||
assert_eq!(model.carrier_size(0), 0);
|
||||
assert_eq!(model.carrier_size(1), 0);
|
||||
}
|
||||
other => panic!("Expected Found with empty model, got {:?}", other),
|
||||
}
|
||||
}
|
||||
}
|
||||
1398
src/solver/tactics.rs
Normal file
1398
src/solver/tactics.rs
Normal file
File diff suppressed because it is too large
Load Diff
465
src/solver/tree.rs
Normal file
465
src/solver/tree.rs
Normal file
@ -0,0 +1,465 @@
|
||||
//! Search tree for instance synthesis.
|
||||
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::core::{ElaboratedTheory, RelationStorage, Signature, Structure};
|
||||
use crate::id::{Luid, Slid, Uuid};
|
||||
use crate::tensor::{CheckResult, Violation};
|
||||
use crate::universe::Universe;
|
||||
|
||||
use super::types::{
|
||||
ConflictClause, CongruenceClosure, NodeDetail, NodeId, NodeStatus, SearchNode, SearchSummary,
|
||||
};
|
||||
|
||||
/// The search tree
|
||||
#[derive(Debug)]
|
||||
pub struct SearchTree {
|
||||
/// All nodes, indexed by NodeId
|
||||
pub(crate) nodes: Vec<SearchNode>,
|
||||
/// The theory we're trying to instantiate
|
||||
pub theory: Rc<ElaboratedTheory>,
|
||||
/// Universe for Luid allocation
|
||||
pub universe: Universe,
|
||||
}
|
||||
|
||||
impl SearchTree {
|
||||
/// Create a new search tree for instantiating a theory
|
||||
///
|
||||
/// The root node contains an empty Structure with the right number of
|
||||
/// sorts but no elements.
|
||||
///
|
||||
/// This is equivalent to `SearchTree::from_base(theory, empty_structure)`.
|
||||
/// Use this for `:solve` (finding models from scratch).
|
||||
pub fn new(theory: Rc<ElaboratedTheory>) -> Self {
|
||||
let num_sorts = theory.theory.signature.sorts.len();
|
||||
let root_structure = Structure::new(num_sorts);
|
||||
Self::from_base_inner(theory, root_structure, Universe::new())
|
||||
}
|
||||
|
||||
/// Create a search tree starting from an existing base structure.
|
||||
///
|
||||
/// This enables the unified model-finding API:
|
||||
/// - `:solve T` = `SearchTree::new(T)` = find models of T from scratch
|
||||
/// - `:query M T'` = `SearchTree::from_base(T', M)` = find extensions of M to T'
|
||||
///
|
||||
/// The base structure's elements, function values, and relation tuples are
|
||||
/// preserved as "frozen" facts. The solver will only add new facts, not remove
|
||||
/// existing ones (the refinement order).
|
||||
///
|
||||
/// # Arguments
|
||||
/// - `theory`: The theory to satisfy (may extend the base structure's theory)
|
||||
/// - `base`: The starting structure (may already have elements, functions, relations)
|
||||
/// - `universe`: The universe for Luid allocation (should contain Luids from base)
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if the base structure has more sorts than the theory signature.
|
||||
pub fn from_base(theory: Rc<ElaboratedTheory>, base: Structure, universe: Universe) -> Self {
|
||||
let num_sorts = theory.theory.signature.sorts.len();
|
||||
assert!(
|
||||
base.carriers.len() <= num_sorts,
|
||||
"Base structure has {} sorts but theory only has {}",
|
||||
base.carriers.len(),
|
||||
num_sorts
|
||||
);
|
||||
Self::from_base_inner(theory, base, universe)
|
||||
}
|
||||
|
||||
/// Internal constructor shared by `new` and `from_base`.
|
||||
fn from_base_inner(theory: Rc<ElaboratedTheory>, root_structure: Structure, universe: Universe) -> Self {
|
||||
let root = SearchNode {
|
||||
id: 0,
|
||||
parent: None,
|
||||
children: Vec::new(),
|
||||
structure: root_structure,
|
||||
cc: CongruenceClosure::new(),
|
||||
status: NodeStatus::Open,
|
||||
p_success: 0.5, // Prior: 50% chance of solution existing
|
||||
conflicts: Vec::new(),
|
||||
label: Some("root".to_string()),
|
||||
};
|
||||
|
||||
Self {
|
||||
nodes: vec![root],
|
||||
theory,
|
||||
universe,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the root node ID
|
||||
pub fn root(&self) -> NodeId {
|
||||
0
|
||||
}
|
||||
|
||||
/// Get a node by ID
|
||||
pub fn get(&self, id: NodeId) -> Option<&SearchNode> {
|
||||
self.nodes.get(id)
|
||||
}
|
||||
|
||||
/// Get a mutable reference to a node
|
||||
pub fn get_mut(&mut self, id: NodeId) -> Option<&mut SearchNode> {
|
||||
self.nodes.get_mut(id)
|
||||
}
|
||||
|
||||
/// Get the signature of the theory
|
||||
pub fn signature(&self) -> &Signature {
|
||||
&self.theory.theory.signature
|
||||
}
|
||||
|
||||
/// Get all open frontier nodes
|
||||
pub fn frontier(&self) -> Vec<NodeId> {
|
||||
self.nodes
|
||||
.iter()
|
||||
.filter(|n| n.status == NodeStatus::Open && n.children.is_empty())
|
||||
.map(|n| n.id)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Get frontier nodes sorted by p_success (descending)
|
||||
pub fn frontier_by_probability(&self) -> Vec<NodeId> {
|
||||
let mut frontier = self.frontier();
|
||||
frontier.sort_by(|&a, &b| {
|
||||
let pa = self.nodes[a].p_success;
|
||||
let pb = self.nodes[b].p_success;
|
||||
pb.partial_cmp(&pa).unwrap_or(std::cmp::Ordering::Equal)
|
||||
});
|
||||
frontier
|
||||
}
|
||||
|
||||
/// Create a child node by cloning the parent's structure
|
||||
///
|
||||
/// Returns the new node's ID. The child starts with the same structure
|
||||
/// as the parent (will be refined by subsequent operations).
|
||||
pub fn branch(&mut self, parent: NodeId, label: Option<String>) -> NodeId {
|
||||
let parent_node = &self.nodes[parent];
|
||||
let child_structure = parent_node.structure.clone();
|
||||
let child_cc = parent_node.cc.clone();
|
||||
let child_p = parent_node.p_success;
|
||||
|
||||
let child_id = self.nodes.len();
|
||||
let child = SearchNode {
|
||||
id: child_id,
|
||||
parent: Some(parent),
|
||||
children: Vec::new(),
|
||||
structure: child_structure,
|
||||
cc: child_cc,
|
||||
status: NodeStatus::Open,
|
||||
p_success: child_p,
|
||||
conflicts: Vec::new(),
|
||||
label,
|
||||
};
|
||||
|
||||
self.nodes.push(child);
|
||||
self.nodes[parent].children.push(child_id);
|
||||
child_id
|
||||
}
|
||||
|
||||
/// Mark a node as solved (found valid instance)
|
||||
pub fn mark_solved(&mut self, id: NodeId) {
|
||||
if let Some(node) = self.nodes.get_mut(id) {
|
||||
node.status = NodeStatus::Solved;
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark a node as unsatisfiable
|
||||
pub fn mark_unsat(&mut self, id: NodeId, conflict: Option<ConflictClause>) {
|
||||
if let Some(node) = self.nodes.get_mut(id) {
|
||||
node.status = NodeStatus::Unsat;
|
||||
if let Some(c) = conflict {
|
||||
node.conflicts.push(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark a node as pruned (agent decided not to explore)
|
||||
pub fn mark_pruned(&mut self, id: NodeId) {
|
||||
if let Some(node) = self.nodes.get_mut(id) {
|
||||
node.status = NodeStatus::Pruned;
|
||||
}
|
||||
}
|
||||
|
||||
/// Update a node's success probability estimate
|
||||
pub fn set_probability(&mut self, id: NodeId, p: f64) {
|
||||
if let Some(node) = self.nodes.get_mut(id) {
|
||||
node.p_success = p.clamp(0.0, 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if any node has been solved
|
||||
pub fn has_solution(&self) -> Option<NodeId> {
|
||||
self.nodes
|
||||
.iter()
|
||||
.find(|n| n.status == NodeStatus::Solved)
|
||||
.map(|n| n.id)
|
||||
}
|
||||
|
||||
/// Get the path from root to a node (list of NodeIds)
|
||||
pub fn path_to(&self, id: NodeId) -> Vec<NodeId> {
|
||||
let mut path = Vec::new();
|
||||
let mut current = Some(id);
|
||||
while let Some(nid) = current {
|
||||
path.push(nid);
|
||||
current = self.nodes[nid].parent;
|
||||
}
|
||||
path.reverse();
|
||||
path
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// REFINEMENT OPERATIONS
|
||||
// ============================================================================
|
||||
|
||||
/// Operations for refining a partial model (moving up in the refinement order)
|
||||
impl SearchTree {
|
||||
/// Add a new element to a sort in a node's structure
|
||||
///
|
||||
/// Returns the (Slid, Luid) of the new element.
|
||||
pub fn add_element(&mut self, node: NodeId, sort_id: usize) -> Result<(Slid, Luid), String> {
|
||||
let node = self.nodes.get_mut(node).ok_or("Invalid node ID")?;
|
||||
if node.status != NodeStatus::Open {
|
||||
return Err("Cannot refine a non-open node".to_string());
|
||||
}
|
||||
Ok(node.structure.add_element(&mut self.universe, sort_id))
|
||||
}
|
||||
|
||||
/// Add a new element with a specific UUID
|
||||
pub fn add_element_with_uuid(
|
||||
&mut self,
|
||||
node: NodeId,
|
||||
uuid: Uuid,
|
||||
sort_id: usize,
|
||||
) -> Result<(Slid, Luid), String> {
|
||||
let node = self.nodes.get_mut(node).ok_or("Invalid node ID")?;
|
||||
if node.status != NodeStatus::Open {
|
||||
return Err("Cannot refine a non-open node".to_string());
|
||||
}
|
||||
Ok(node
|
||||
.structure
|
||||
.add_element_with_uuid(&mut self.universe, uuid, sort_id))
|
||||
}
|
||||
|
||||
/// Define a function value: f(domain) = codomain
|
||||
///
|
||||
/// The function must not already be defined at this domain element
|
||||
/// (that would be a conflict, not a refinement).
|
||||
pub fn define_function(
|
||||
&mut self,
|
||||
node: NodeId,
|
||||
func_id: usize,
|
||||
domain_slid: Slid,
|
||||
codomain_slid: Slid,
|
||||
) -> Result<(), String> {
|
||||
let node = self.nodes.get_mut(node).ok_or("Invalid node ID")?;
|
||||
if node.status != NodeStatus::Open {
|
||||
return Err("Cannot refine a non-open node".to_string());
|
||||
}
|
||||
node.structure
|
||||
.define_function(func_id, domain_slid, codomain_slid)
|
||||
}
|
||||
|
||||
/// Assert a relation tuple: R(tuple) = true
|
||||
pub fn assert_relation(
|
||||
&mut self,
|
||||
node: NodeId,
|
||||
rel_id: usize,
|
||||
tuple: Vec<Slid>,
|
||||
) -> Result<bool, String> {
|
||||
let node = self.nodes.get_mut(node).ok_or("Invalid node ID")?;
|
||||
if node.status != NodeStatus::Open {
|
||||
return Err("Cannot refine a non-open node".to_string());
|
||||
}
|
||||
Ok(node.structure.assert_relation(rel_id, tuple))
|
||||
}
|
||||
|
||||
/// Initialize function storage for a node (call after adding elements)
|
||||
pub fn init_functions(
|
||||
&mut self,
|
||||
node: NodeId,
|
||||
domain_sort_ids: &[Option<usize>],
|
||||
) -> Result<(), String> {
|
||||
let node = self.nodes.get_mut(node).ok_or("Invalid node ID")?;
|
||||
node.structure.init_functions(domain_sort_ids);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Initialize relation storage for a node
|
||||
pub fn init_relations(&mut self, node: NodeId, arities: &[usize]) -> Result<(), String> {
|
||||
let node = self.nodes.get_mut(node).ok_or("Invalid node ID")?;
|
||||
node.structure.init_relations(arities);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Add a pending equation to a node's congruence closure
|
||||
///
|
||||
/// Equations arise from axiom consequents, function conflicts, etc.
|
||||
/// They are processed later during propagation.
|
||||
pub fn add_pending_equation(
|
||||
&mut self,
|
||||
node: NodeId,
|
||||
lhs: Slid,
|
||||
rhs: Slid,
|
||||
reason: super::types::EquationReason,
|
||||
) {
|
||||
if let Some(node) = self.nodes.get_mut(node) {
|
||||
node.cc.add_equation(lhs, rhs, reason);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CONSTRAINT CHECKING
|
||||
// ============================================================================
|
||||
|
||||
impl SearchTree {
|
||||
/// Check all axioms against a node's current structure
|
||||
///
|
||||
/// Returns Ok(()) if all axioms are satisfied, or Err with violations.
|
||||
pub fn check_axioms(&self, node: NodeId) -> Result<(), Vec<(usize, Vec<Violation>)>> {
|
||||
let node = self.nodes.get(node).ok_or_else(Vec::new)?;
|
||||
let violations = crate::tensor::check_theory_axioms(
|
||||
&self.theory.theory.axioms,
|
||||
&node.structure,
|
||||
&self.theory.theory.signature,
|
||||
);
|
||||
if violations.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(violations)
|
||||
}
|
||||
}
|
||||
|
||||
/// Check a single axiom
|
||||
pub fn check_axiom(&self, node: NodeId, axiom_idx: usize) -> CheckResult {
|
||||
let node = match self.nodes.get(node) {
|
||||
Some(n) => n,
|
||||
None => return CheckResult::Satisfied, // Invalid node = vacuously true?
|
||||
};
|
||||
let axiom = match self.theory.theory.axioms.get(axiom_idx) {
|
||||
Some(a) => a,
|
||||
None => return CheckResult::Satisfied,
|
||||
};
|
||||
// Return Satisfied on compile error (unsupported patterns handled elsewhere)
|
||||
crate::tensor::check_sequent(axiom, &node.structure, &self.theory.theory.signature)
|
||||
.unwrap_or(CheckResult::Satisfied)
|
||||
}
|
||||
|
||||
/// Check if a structure is "complete" (all functions total, all axioms satisfied)
|
||||
///
|
||||
/// A complete structure is a valid model of the theory.
|
||||
pub fn is_complete(&self, node: NodeId) -> Result<bool, String> {
|
||||
let node = self.nodes.get(node).ok_or("Invalid node ID")?;
|
||||
let sig = &self.theory.theory.signature;
|
||||
|
||||
// Check all functions are total
|
||||
for (func_id, func_sym) in sig.functions.iter().enumerate() {
|
||||
if func_id >= node.structure.functions.len() {
|
||||
return Ok(false); // Function storage not initialized
|
||||
}
|
||||
|
||||
// Get domain cardinality (works for base and product sorts)
|
||||
let domain_size = func_sym.domain.cardinality(&node.structure);
|
||||
|
||||
// Check all domain elements have values (local functions only for now)
|
||||
let func_col = &node.structure.functions[func_id];
|
||||
if func_col.len() < domain_size {
|
||||
return Ok(false);
|
||||
}
|
||||
if let Some(local_col) = func_col.as_local() {
|
||||
for opt in local_col {
|
||||
if opt.is_none() {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check all axioms
|
||||
match self.check_axioms(node.id) {
|
||||
Ok(()) => Ok(true),
|
||||
Err(_) => Ok(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// AGENT INTERFACE
|
||||
// ============================================================================
|
||||
|
||||
impl SearchTree {
|
||||
/// Get a summary of the search state
|
||||
pub fn summary(&self, top_k: usize) -> SearchSummary {
|
||||
let frontier = self.frontier_by_probability();
|
||||
let top_frontier: Vec<_> = frontier
|
||||
.iter()
|
||||
.take(top_k)
|
||||
.map(|&id| {
|
||||
let node = &self.nodes[id];
|
||||
(id, node.p_success, node.label.clone())
|
||||
})
|
||||
.collect();
|
||||
|
||||
SearchSummary {
|
||||
total_nodes: self.nodes.len(),
|
||||
frontier_size: frontier.len(),
|
||||
solved_count: self
|
||||
.nodes
|
||||
.iter()
|
||||
.filter(|n| n.status == NodeStatus::Solved)
|
||||
.count(),
|
||||
unsat_count: self
|
||||
.nodes
|
||||
.iter()
|
||||
.filter(|n| n.status == NodeStatus::Unsat)
|
||||
.count(),
|
||||
top_frontier,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get detailed info about a node (for agent inspection)
|
||||
pub fn node_detail(&self, id: NodeId) -> Option<NodeDetail> {
|
||||
let node = self.nodes.get(id)?;
|
||||
Some(NodeDetail {
|
||||
id: node.id,
|
||||
parent: node.parent,
|
||||
children: node.children.clone(),
|
||||
status: node.status.clone(),
|
||||
p_success: node.p_success,
|
||||
label: node.label.clone(),
|
||||
carrier_sizes: node
|
||||
.structure
|
||||
.carriers
|
||||
.iter()
|
||||
.map(|c| c.len() as usize)
|
||||
.collect(),
|
||||
num_function_values: node
|
||||
.structure
|
||||
.functions
|
||||
.iter()
|
||||
.map(|f| match f {
|
||||
crate::core::FunctionColumn::Local(col) => {
|
||||
col.iter().filter(|opt| opt.is_some()).count()
|
||||
}
|
||||
crate::core::FunctionColumn::External(col) => {
|
||||
col.iter().filter(|opt| opt.is_some()).count()
|
||||
}
|
||||
crate::core::FunctionColumn::ProductLocal { storage, .. } => {
|
||||
storage.defined_count()
|
||||
}
|
||||
crate::core::FunctionColumn::ProductCodomain { field_columns, .. } => {
|
||||
// Count elements where ALL fields are defined
|
||||
if field_columns.is_empty() {
|
||||
0
|
||||
} else {
|
||||
let len = field_columns[0].len();
|
||||
(0..len)
|
||||
.filter(|&i| field_columns.iter().all(|col| col.get(i).is_some_and(|opt| opt.is_some())))
|
||||
.count()
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
num_relation_tuples: node.structure.relations.iter().map(|r| r.len()).collect(),
|
||||
conflicts: node.conflicts.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
131
src/solver/types.rs
Normal file
131
src/solver/types.rs
Normal file
@ -0,0 +1,131 @@
|
||||
//! Core types for the solver infrastructure.
|
||||
|
||||
use crate::core::Structure;
|
||||
use crate::id::{Luid, Slid};
|
||||
|
||||
// Re-export congruence closure types from shared module
|
||||
pub use crate::cc::{CongruenceClosure, EquationReason, PendingEquation};
|
||||
|
||||
/// Unique identifier for a search node
|
||||
pub type NodeId = usize;
|
||||
|
||||
/// A node in the search tree
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SearchNode {
|
||||
/// Unique ID for this node
|
||||
pub id: NodeId,
|
||||
/// Parent node (None for root)
|
||||
pub parent: Option<NodeId>,
|
||||
/// Children (branches from this node)
|
||||
pub children: Vec<NodeId>,
|
||||
/// The partial model at this node
|
||||
pub structure: Structure,
|
||||
/// Congruence closure for tracking element equivalences
|
||||
pub cc: CongruenceClosure,
|
||||
/// Status of this node
|
||||
pub status: NodeStatus,
|
||||
/// Agent's estimate of success probability (0.0 to 1.0)
|
||||
pub p_success: f64,
|
||||
/// Conflict clauses learned at or below this node
|
||||
pub conflicts: Vec<ConflictClause>,
|
||||
/// Debug/display name for this node
|
||||
pub label: Option<String>,
|
||||
}
|
||||
|
||||
/// Status of a search node
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum NodeStatus {
|
||||
/// Still exploring (frontier node)
|
||||
Open,
|
||||
/// Found a valid complete instance
|
||||
Solved,
|
||||
/// Proved unsatisfiable from this point
|
||||
Unsat,
|
||||
/// Agent decided not to explore further
|
||||
Pruned,
|
||||
}
|
||||
|
||||
/// A learned conflict clause (derivation of False)
|
||||
///
|
||||
/// Records a combination of commitments from which `⊢ False` was derived.
|
||||
/// Used for CDCL-style pruning: if a node's commitments subsume a conflict
|
||||
/// clause, that node can be immediately marked Unsat (since False is derivable).
|
||||
///
|
||||
/// Note: This represents a PROOF of unsatisfiability, not mere "conflicts".
|
||||
/// Even apparent conflicts (like function defined with two different values)
|
||||
/// just create pending equations—only if propagating those equations leads
|
||||
/// to deriving False do we have a true conflict clause.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ConflictClause {
|
||||
/// Elements that must exist (sort_id, luid)
|
||||
pub required_elements: Vec<(usize, Luid)>,
|
||||
/// Function values that must hold (func_id, domain_luid, codomain_luid)
|
||||
pub required_functions: Vec<(usize, Luid, Luid)>,
|
||||
/// Relation tuples that must be asserted (rel_id, tuple as Luids)
|
||||
pub required_relations: Vec<(usize, Vec<Luid>)>,
|
||||
/// Which axiom was violated (index into theory's axiom list)
|
||||
pub violated_axiom: Option<usize>,
|
||||
/// Human-readable explanation
|
||||
pub explanation: Option<String>,
|
||||
}
|
||||
|
||||
/// An obligation to fulfill
|
||||
///
|
||||
/// Geometric logic consequents are positive (existentials, disjunctions, relations).
|
||||
/// When an axiom's premise is satisfied but conclusion isn't, we have an OBLIGATION
|
||||
/// to make the conclusion true. This can always potentially be done by refinement
|
||||
/// (adding elements, defining functions, asserting relations).
|
||||
///
|
||||
/// Only when fulfilling the obligation would CONFLICT with existing commitments
|
||||
/// is the node truly unsatisfiable.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Obligation {
|
||||
/// Which axiom generated this obligation
|
||||
pub axiom_idx: usize,
|
||||
/// The variable assignment where premise holds but conclusion doesn't
|
||||
/// Maps variable name to (sort_id, slid) in the current structure
|
||||
pub assignment: Vec<(String, usize, Slid)>,
|
||||
/// Human-readable description of what needs to be witnessed
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
/// Result of checking axioms: either all satisfied, or obligations remain
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum AxiomCheckResult {
|
||||
/// All axioms satisfied for all substitutions
|
||||
AllSatisfied,
|
||||
/// Some axioms have unsatisfied consequents (obligations to fulfill)
|
||||
Obligations(Vec<Obligation>),
|
||||
}
|
||||
|
||||
/// Summary of the current search state (for agent inspection)
|
||||
#[derive(Debug)]
|
||||
pub struct SearchSummary {
|
||||
/// Total nodes in tree
|
||||
pub total_nodes: usize,
|
||||
/// Open frontier nodes
|
||||
pub frontier_size: usize,
|
||||
/// Solved nodes
|
||||
pub solved_count: usize,
|
||||
/// Unsat nodes
|
||||
pub unsat_count: usize,
|
||||
/// Top-k frontier nodes by probability
|
||||
pub top_frontier: Vec<(NodeId, f64, Option<String>)>,
|
||||
}
|
||||
|
||||
/// Detailed information about a search node
|
||||
#[derive(Debug)]
|
||||
pub struct NodeDetail {
|
||||
pub id: NodeId,
|
||||
pub parent: Option<NodeId>,
|
||||
pub children: Vec<NodeId>,
|
||||
pub status: NodeStatus,
|
||||
pub p_success: f64,
|
||||
pub label: Option<String>,
|
||||
pub carrier_sizes: Vec<usize>,
|
||||
pub num_function_values: Vec<usize>,
|
||||
pub num_relation_tuples: Vec<usize>,
|
||||
pub conflicts: Vec<ConflictClause>,
|
||||
}
|
||||
|
||||
// Congruence closure types and tests are now in crate::cc
|
||||
31
src/store/append.rs
Normal file
31
src/store/append.rs
Normal file
@ -0,0 +1,31 @@
|
||||
//! Low-level append operations for the Store.
|
||||
//!
|
||||
//! These are the primitive operations that all higher-level operations use.
|
||||
//! Note: We use a trait to document the interface, but the actual implementations
|
||||
//! are on Store directly to avoid borrow checker issues.
|
||||
|
||||
use crate::id::Slid;
|
||||
|
||||
/// Low-level operations on the meta structure.
|
||||
///
|
||||
/// This trait documents the interface that Store implements for low-level
|
||||
/// element manipulation. The actual implementations are on Store directly.
|
||||
pub trait AppendOps {
|
||||
/// Add an element to a sort in the meta structure with a simple name
|
||||
fn add_element(&mut self, sort_id: usize, name: &str) -> Slid;
|
||||
|
||||
/// Add an element with a qualified name path
|
||||
fn add_element_qualified(&mut self, sort_id: usize, path: Vec<String>) -> Slid;
|
||||
|
||||
/// Define a function value in the meta structure
|
||||
fn define_func(&mut self, func_id: usize, domain: Slid, codomain: Slid) -> Result<(), String>;
|
||||
|
||||
/// Get a function value from the meta structure
|
||||
fn get_func(&self, func_id: usize, domain: Slid) -> Option<Slid>;
|
||||
|
||||
/// Get all elements of a sort
|
||||
fn elements_of_sort(&self, sort_id: usize) -> Vec<Slid>;
|
||||
|
||||
/// Get the name of an element
|
||||
fn get_element_name(&self, slid: Slid) -> String;
|
||||
}
|
||||
355
src/store/batch.rs
Normal file
355
src/store/batch.rs
Normal file
@ -0,0 +1,355 @@
|
||||
//! Atomic batch creation for elements.
|
||||
//!
|
||||
//! This module enforces the Monotonic Submodel Property by requiring all facts
|
||||
//! involving an element to be defined atomically at element creation time.
|
||||
//!
|
||||
//! # Design Principles
|
||||
//!
|
||||
//! 1. **All facts defined at creation**: When element `a` is created, all facts
|
||||
//! involving `a` (function values `f(a)=b`, relation tuples `R(a,c)`) must be
|
||||
//! defined in the same atomic batch.
|
||||
//!
|
||||
//! 2. **No post-hoc fact addition**: After an element's batch is committed, no new
|
||||
//! facts involving that element can be added. This ensures existing submodels
|
||||
//! remain valid as new elements are added.
|
||||
//!
|
||||
//! 3. **Relations are boolean functions**: Relations `R: A × B → Bool` are treated
|
||||
//! as total functions. When element `a` is created, all `R(a, _)` and `R(_, a)`
|
||||
//! values are implicitly `false` unless explicitly asserted as `true`.
|
||||
|
||||
use crate::id::{NumericId, Slid};
|
||||
|
||||
use super::Store;
|
||||
|
||||
/// An atomic batch of changes for creating a single new element.
|
||||
///
|
||||
/// All facts involving the new element must be defined in this batch.
|
||||
/// After the batch is committed, no new facts can be added.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ElementBatch {
|
||||
/// The instance this element belongs to
|
||||
pub instance: Slid,
|
||||
|
||||
/// The sort (from the theory) of this element
|
||||
pub sort: Slid,
|
||||
|
||||
/// Human-readable name for the element
|
||||
pub name: String,
|
||||
|
||||
/// Function values where this element is in the domain: f(elem) = value
|
||||
pub func_vals: Vec<(Slid, Slid)>, // (func, codomain_value)
|
||||
|
||||
/// Relation assertions where this element appears: R(..., elem, ...) = true
|
||||
/// Only the TRUE tuples are listed; everything else is implicitly false.
|
||||
pub rel_tuples: Vec<(Slid, Slid)>, // (rel, arg) - for unary relations or when elem is the arg
|
||||
}
|
||||
|
||||
impl ElementBatch {
|
||||
/// Create an empty/invalid batch (for use with mem::replace)
|
||||
fn empty() -> Self {
|
||||
Self {
|
||||
instance: Slid::from_usize(0),
|
||||
sort: Slid::from_usize(0),
|
||||
name: String::new(),
|
||||
func_vals: Vec::new(),
|
||||
rel_tuples: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ElementBatch {
|
||||
/// Create a new element batch
|
||||
pub fn new(instance: Slid, sort: Slid, name: impl Into<String>) -> Self {
|
||||
Self {
|
||||
instance,
|
||||
sort,
|
||||
name: name.into(),
|
||||
func_vals: Vec::new(),
|
||||
rel_tuples: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a function value: f(this_element) = value
|
||||
pub fn with_func(mut self, func: Slid, value: Slid) -> Self {
|
||||
self.func_vals.push((func, value));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a relation tuple: R(this_element) = true (for unary relations)
|
||||
/// or R(arg) = true where this element is part of arg
|
||||
pub fn with_rel(mut self, rel: Slid, arg: Slid) -> Self {
|
||||
self.rel_tuples.push((rel, arg));
|
||||
self
|
||||
}
|
||||
|
||||
/// Define a function value: f(this_element) = value
|
||||
pub fn define_func(&mut self, func: Slid, value: Slid) {
|
||||
self.func_vals.push((func, value));
|
||||
}
|
||||
|
||||
/// Assert a relation tuple as true
|
||||
pub fn assert_rel(&mut self, rel: Slid, arg: Slid) {
|
||||
self.rel_tuples.push((rel, arg));
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for creating elements with all their facts defined atomically.
|
||||
///
|
||||
/// This enforces the Monotonic Submodel Property by ensuring all facts
|
||||
/// are defined before the element is committed.
|
||||
pub struct ElementBuilder<'a> {
|
||||
store: &'a mut Store,
|
||||
batch: ElementBatch,
|
||||
committed: bool,
|
||||
}
|
||||
|
||||
impl<'a> ElementBuilder<'a> {
|
||||
/// Create a new element builder
|
||||
pub fn new(store: &'a mut Store, instance: Slid, sort: Slid, name: impl Into<String>) -> Self {
|
||||
Self {
|
||||
store,
|
||||
batch: ElementBatch::new(instance, sort, name),
|
||||
committed: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Define a function value: f(this_element) = value
|
||||
pub fn define_func(&mut self, func: Slid, value: Slid) -> &mut Self {
|
||||
self.batch.define_func(func, value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Assert a relation tuple as true: R(arg) = true
|
||||
pub fn assert_rel(&mut self, rel: Slid, arg: Slid) -> &mut Self {
|
||||
self.batch.assert_rel(rel, arg);
|
||||
self
|
||||
}
|
||||
|
||||
/// Commit the element batch and return the new element's Slid.
|
||||
///
|
||||
/// This atomically creates the element and all its facts.
|
||||
/// After this, no new facts involving this element can be added.
|
||||
pub fn commit(mut self) -> Result<Slid, String> {
|
||||
self.committed = true;
|
||||
let batch = std::mem::replace(&mut self.batch, ElementBatch::empty());
|
||||
self.store.add_element_batch(batch)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Drop for ElementBuilder<'a> {
|
||||
fn drop(&mut self) {
|
||||
if !self.committed {
|
||||
// Log a warning if the builder was dropped without committing
|
||||
// In debug builds, this could panic to catch bugs
|
||||
#[cfg(debug_assertions)]
|
||||
eprintln!(
|
||||
"Warning: ElementBuilder for '{}' was dropped without committing",
|
||||
self.batch.name
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Store {
|
||||
/// Create an element builder for atomic element creation.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```ignore
|
||||
/// let elem = store.build_element(instance, sort, "my_element")
|
||||
/// .define_func(f, target)
|
||||
/// .assert_rel(r, arg)
|
||||
/// .commit()?;
|
||||
/// ```
|
||||
pub fn build_element(
|
||||
&mut self,
|
||||
instance: Slid,
|
||||
sort: Slid,
|
||||
name: impl Into<String>,
|
||||
) -> ElementBuilder<'_> {
|
||||
ElementBuilder::new(self, instance, sort, name)
|
||||
}
|
||||
|
||||
/// Add an element with all its facts atomically.
|
||||
///
|
||||
/// This is the low-level API; prefer `build_element()` for a builder pattern.
|
||||
pub fn add_element_batch(&mut self, batch: ElementBatch) -> Result<Slid, String> {
|
||||
// 1. Create the element
|
||||
let elem_slid = self.add_elem(batch.instance, batch.sort, &batch.name)?;
|
||||
|
||||
// 2. Add all function values
|
||||
for (func, value) in batch.func_vals {
|
||||
self.add_func_val(batch.instance, func, elem_slid, value)?;
|
||||
}
|
||||
|
||||
// 3. Add all relation tuples (sparse: only the true ones)
|
||||
for (rel, arg) in batch.rel_tuples {
|
||||
self.add_rel_tuple(batch.instance, rel, arg)?;
|
||||
}
|
||||
|
||||
Ok(elem_slid)
|
||||
}
|
||||
|
||||
/// Create multiple elements atomically within a closure.
|
||||
///
|
||||
/// This allows defining elements that reference each other within the same batch.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```ignore
|
||||
/// store.create_elements(instance, |ctx| {
|
||||
/// let a = ctx.add_element(sort_a, "a")?;
|
||||
/// let b = ctx.add_element(sort_b, "b")?;
|
||||
///
|
||||
/// ctx.define_func(f, a, b)?; // f(a) = b
|
||||
/// ctx.assert_rel(r, a)?; // R(a) = true
|
||||
///
|
||||
/// Ok(vec![a, b])
|
||||
/// })?;
|
||||
/// ```
|
||||
pub fn create_elements<F, R>(&mut self, instance: Slid, f: F) -> Result<R, String>
|
||||
where
|
||||
F: FnOnce(&mut ElementCreationContext<'_>) -> Result<R, String>,
|
||||
{
|
||||
let mut ctx = ElementCreationContext::new(self, instance);
|
||||
let result = f(&mut ctx)?;
|
||||
ctx.commit()?;
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
/// Context for creating multiple elements atomically.
|
||||
///
|
||||
/// All elements and facts created within this context are committed together.
|
||||
pub struct ElementCreationContext<'a> {
|
||||
store: &'a mut Store,
|
||||
instance: Slid,
|
||||
/// Elements created but not yet committed to GeologMeta
|
||||
pending_elements: Vec<(Slid, Slid, String)>, // (sort, slid, name)
|
||||
/// Function values to add
|
||||
pending_func_vals: Vec<(Slid, Slid, Slid)>, // (func, arg, result)
|
||||
/// Relation tuples to add
|
||||
pending_rel_tuples: Vec<(Slid, Slid)>, // (rel, arg)
|
||||
committed: bool,
|
||||
}
|
||||
|
||||
impl<'a> ElementCreationContext<'a> {
|
||||
fn new(store: &'a mut Store, instance: Slid) -> Self {
|
||||
Self {
|
||||
store,
|
||||
instance,
|
||||
pending_elements: Vec::new(),
|
||||
pending_func_vals: Vec::new(),
|
||||
pending_rel_tuples: Vec::new(),
|
||||
committed: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a new element (returns Slid immediately for use in defining facts)
|
||||
pub fn add_element(&mut self, sort: Slid, name: impl Into<String>) -> Result<Slid, String> {
|
||||
let name = name.into();
|
||||
let elem_slid = self.store.add_elem(self.instance, sort, &name)?;
|
||||
self.pending_elements.push((sort, elem_slid, name));
|
||||
Ok(elem_slid)
|
||||
}
|
||||
|
||||
/// Define a function value: f(arg) = result
|
||||
pub fn define_func(&mut self, func: Slid, arg: Slid, result: Slid) -> Result<(), String> {
|
||||
self.pending_func_vals.push((func, arg, result));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Assert a relation tuple as true: R(arg) = true
|
||||
pub fn assert_rel(&mut self, rel: Slid, arg: Slid) -> Result<(), String> {
|
||||
self.pending_rel_tuples.push((rel, arg));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Commit all pending elements and facts
|
||||
fn commit(&mut self) -> Result<(), String> {
|
||||
// Add all function values
|
||||
for (func, arg, result) in std::mem::take(&mut self.pending_func_vals) {
|
||||
self.store.add_func_val(self.instance, func, arg, result)?;
|
||||
}
|
||||
|
||||
// Add all relation tuples
|
||||
for (rel, arg) in std::mem::take(&mut self.pending_rel_tuples) {
|
||||
self.store.add_rel_tuple(self.instance, rel, arg)?;
|
||||
}
|
||||
|
||||
self.committed = true;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Drop for ElementCreationContext<'a> {
|
||||
fn drop(&mut self) {
|
||||
if !self.committed && !self.pending_elements.is_empty() {
|
||||
#[cfg(debug_assertions)]
|
||||
eprintln!(
|
||||
"Warning: ElementCreationContext with {} pending elements was dropped without committing",
|
||||
self.pending_elements.len()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_element_batch_builder() {
|
||||
let mut store = Store::new();
|
||||
|
||||
// Create a theory with a sort
|
||||
let theory = store.create_theory("TestTheory").unwrap();
|
||||
let sort = store.add_sort(theory, "Node").unwrap();
|
||||
let sort_ds = store.make_base_dsort(sort).unwrap();
|
||||
|
||||
// Create a function
|
||||
let _func = store.add_function(theory, "label", sort_ds, sort_ds).unwrap();
|
||||
|
||||
// Create an instance
|
||||
let instance = store.create_instance("TestInstance", theory).unwrap();
|
||||
|
||||
// Create an element using the batch API
|
||||
let elem = store
|
||||
.build_element(instance, sort, "node1")
|
||||
.commit()
|
||||
.unwrap();
|
||||
|
||||
// Verify element was created
|
||||
let view = store.materialize(instance);
|
||||
assert!(view.elements.contains(&elem));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_elements_context() {
|
||||
let mut store = Store::new();
|
||||
|
||||
// Create a theory with a sort and relation
|
||||
let theory = store.create_theory("TestTheory").unwrap();
|
||||
let sort = store.add_sort(theory, "Node").unwrap();
|
||||
let sort_ds = store.make_base_dsort(sort).unwrap();
|
||||
let rel = store.add_relation(theory, "connected", sort_ds).unwrap();
|
||||
|
||||
// Create an instance
|
||||
let instance = store.create_instance("TestInstance", theory).unwrap();
|
||||
|
||||
// Create multiple elements atomically
|
||||
let (a, b) = store
|
||||
.create_elements(instance, |ctx| {
|
||||
let a = ctx.add_element(sort, "a")?;
|
||||
let b = ctx.add_element(sort, "b")?;
|
||||
ctx.assert_rel(rel, a)?;
|
||||
Ok((a, b))
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
// Verify elements were created
|
||||
let view = store.materialize(instance);
|
||||
assert!(view.elements.contains(&a));
|
||||
assert!(view.elements.contains(&b));
|
||||
}
|
||||
}
|
||||
1017
src/store/bootstrap_queries.rs
Normal file
1017
src/store/bootstrap_queries.rs
Normal file
File diff suppressed because it is too large
Load Diff
208
src/store/columnar.rs
Normal file
208
src/store/columnar.rs
Normal file
@ -0,0 +1,208 @@
|
||||
//! Columnar batch format for efficient storage and wire transfer.
|
||||
//!
|
||||
//! This module defines the physical representation for instance-level data
|
||||
//! (elements, function values, relation tuples). The logical model is still
|
||||
//! GeologMeta (with Elem, FuncVal, RelTupleArg sorts), but the physical
|
||||
//! encoding uses columnar batches for efficiency.
|
||||
//!
|
||||
//! # EDB vs IDB Batches
|
||||
//!
|
||||
//! Batches are tagged as either EDB (extensional) or IDB (intensional):
|
||||
//!
|
||||
//! - **EDB batches**: User-declared facts. Persisted locally AND transmitted over wire.
|
||||
//! - **IDB batches**: Chase-derived facts. Persisted locally but NOT transmitted over wire.
|
||||
//!
|
||||
//! When receiving patches over the network, only EDB batches are included.
|
||||
//! The recipient runs the chase locally to regenerate IDB tuples.
|
||||
//!
|
||||
//! Each patch can have up to 2 batches per instance:
|
||||
//! - One EDB batch (if user manually added tuples)
|
||||
//! - One IDB batch (if chase produced conclusions)
|
||||
|
||||
use rkyv::{Archive, Deserialize, Serialize};
|
||||
|
||||
use crate::id::Uuid;
|
||||
|
||||
/// Distinguishes between user-declared (EDB) and chase-derived (IDB) data.
|
||||
///
|
||||
/// This determines whether the batch is transmitted over the wire during sync.
|
||||
#[derive(Archive, Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
#[archive(check_bytes)]
|
||||
pub enum BatchKind {
|
||||
/// Extensional database: user-declared facts.
|
||||
/// Persisted locally AND transmitted over wire.
|
||||
#[default]
|
||||
Edb,
|
||||
/// Intensional database: chase-derived facts.
|
||||
/// Persisted locally but NOT transmitted over wire.
|
||||
Idb,
|
||||
}
|
||||
|
||||
/// A batch of elements added to an instance.
|
||||
///
|
||||
/// Logically equivalent to a collection of Elem elements in GeologMeta.
|
||||
#[derive(Archive, Serialize, Deserialize, Debug, Clone)]
|
||||
#[archive(check_bytes)]
|
||||
pub struct ElementBatch {
|
||||
/// Which instance these elements belong to
|
||||
pub instance: Uuid,
|
||||
/// Sort UUID for each element (parallel array)
|
||||
pub sorts: Vec<Uuid>,
|
||||
/// UUID for each element (parallel array, same length as sorts)
|
||||
pub elements: Vec<Uuid>,
|
||||
}
|
||||
|
||||
/// A batch of function values in an instance.
|
||||
///
|
||||
/// Logically equivalent to a collection of FuncVal elements in GeologMeta.
|
||||
#[derive(Archive, Serialize, Deserialize, Debug, Clone)]
|
||||
#[archive(check_bytes)]
|
||||
pub struct FunctionValueBatch {
|
||||
/// Which instance these function values belong to
|
||||
pub instance: Uuid,
|
||||
/// Which function
|
||||
pub func: Uuid,
|
||||
/// Domain elements (parallel array)
|
||||
pub args: Vec<Uuid>,
|
||||
/// Codomain elements (parallel array, same length as args)
|
||||
pub results: Vec<Uuid>,
|
||||
}
|
||||
|
||||
/// A batch of relation tuples in an instance.
|
||||
///
|
||||
/// Logically equivalent to a collection of RelTuple + RelTupleArg elements
|
||||
/// in GeologMeta, but stored columnar for efficiency.
|
||||
///
|
||||
/// For a relation `R : [from: A, to: B] -> Prop`, this stores:
|
||||
/// - columns[0] = all "from" field values (UUIDs of A elements)
|
||||
/// - columns[1] = all "to" field values (UUIDs of B elements)
|
||||
///
|
||||
/// Row i represents the tuple (columns[0][i], columns[1][i]).
|
||||
#[derive(Archive, Serialize, Deserialize, Debug, Clone)]
|
||||
#[archive(check_bytes)]
|
||||
pub struct RelationTupleBatch {
|
||||
/// Which instance these tuples belong to
|
||||
pub instance: Uuid,
|
||||
/// Which relation
|
||||
pub rel: Uuid,
|
||||
/// Field UUIDs for each column (from the relation's domain ProdDS/Field)
|
||||
pub field_ids: Vec<Uuid>,
|
||||
/// Columnar data: columns[field_idx][row_idx] = element UUID
|
||||
/// All columns have the same length (number of tuples).
|
||||
pub columns: Vec<Vec<Uuid>>,
|
||||
}
|
||||
|
||||
impl RelationTupleBatch {
|
||||
/// Create a new empty batch for a relation
|
||||
pub fn new(instance: Uuid, rel: Uuid, field_ids: Vec<Uuid>) -> Self {
|
||||
let num_fields = field_ids.len();
|
||||
Self {
|
||||
instance,
|
||||
rel,
|
||||
field_ids,
|
||||
columns: vec![Vec::new(); num_fields],
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a tuple to the batch
|
||||
pub fn push(&mut self, tuple: &[Uuid]) {
|
||||
assert_eq!(tuple.len(), self.columns.len(), "tuple arity mismatch");
|
||||
for (col, &val) in self.columns.iter_mut().zip(tuple.iter()) {
|
||||
col.push(val);
|
||||
}
|
||||
}
|
||||
|
||||
/// Number of tuples in this batch
|
||||
pub fn len(&self) -> usize {
|
||||
self.columns.first().map(|c| c.len()).unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Whether the batch is empty
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
|
||||
/// Iterate over tuples as slices
|
||||
pub fn iter(&self) -> impl Iterator<Item = Vec<Uuid>> + '_ {
|
||||
(0..self.len()).map(|i| {
|
||||
self.columns.iter().map(|col| col[i]).collect()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A complete instance data snapshot in columnar format.
|
||||
///
|
||||
/// This is the efficient representation for storage and wire transfer.
|
||||
/// Logically equivalent to the Elem, FuncVal, RelTuple, RelTupleArg
|
||||
/// portions of a GeologMeta instance.
|
||||
#[derive(Archive, Serialize, Deserialize, Debug, Clone, Default)]
|
||||
#[archive(check_bytes)]
|
||||
pub struct InstanceDataBatch {
|
||||
/// Whether this batch contains EDB (user-declared) or IDB (chase-derived) data.
|
||||
/// IDB batches are persisted locally but NOT transmitted over wire.
|
||||
pub kind: BatchKind,
|
||||
/// All element additions
|
||||
pub elements: Vec<ElementBatch>,
|
||||
/// All function value definitions
|
||||
pub function_values: Vec<FunctionValueBatch>,
|
||||
/// All relation tuple assertions
|
||||
pub relation_tuples: Vec<RelationTupleBatch>,
|
||||
}
|
||||
|
||||
impl InstanceDataBatch {
|
||||
/// Create a new empty EDB batch (default for user-declared data)
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Create a new empty IDB batch (for chase-derived data)
|
||||
pub fn new_idb() -> Self {
|
||||
Self {
|
||||
kind: BatchKind::Idb,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if this batch should be transmitted over the wire
|
||||
pub fn is_wire_transmittable(&self) -> bool {
|
||||
self.kind == BatchKind::Edb
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_relation_tuple_batch() {
|
||||
let instance = Uuid::nil();
|
||||
let rel = Uuid::nil();
|
||||
let field_a = Uuid::nil();
|
||||
let field_b = Uuid::nil();
|
||||
|
||||
let mut batch = RelationTupleBatch::new(
|
||||
instance,
|
||||
rel,
|
||||
vec![field_a, field_b],
|
||||
);
|
||||
|
||||
assert!(batch.is_empty());
|
||||
|
||||
// Add some tuples
|
||||
let elem1 = Uuid::nil();
|
||||
let elem2 = Uuid::nil();
|
||||
let elem3 = Uuid::nil();
|
||||
|
||||
batch.push(&[elem1, elem2]);
|
||||
batch.push(&[elem2, elem3]);
|
||||
batch.push(&[elem1, elem3]);
|
||||
|
||||
assert_eq!(batch.len(), 3);
|
||||
|
||||
let tuples: Vec<_> = batch.iter().collect();
|
||||
assert_eq!(tuples.len(), 3);
|
||||
assert_eq!(tuples[0], vec![elem1, elem2]);
|
||||
assert_eq!(tuples[1], vec![elem2, elem3]);
|
||||
assert_eq!(tuples[2], vec![elem1, elem3]);
|
||||
}
|
||||
}
|
||||
209
src/store/commit.rs
Normal file
209
src/store/commit.rs
Normal file
@ -0,0 +1,209 @@
|
||||
//! Commit operations for the Store.
|
||||
//!
|
||||
//! Version control through commits and name bindings.
|
||||
|
||||
use crate::id::{NumericId, Slid};
|
||||
|
||||
use super::append::AppendOps;
|
||||
use super::{BindingKind, Store};
|
||||
|
||||
impl Store {
|
||||
/// Create a new commit
|
||||
pub fn commit(&mut self, message: Option<&str>) -> Result<Slid, String> {
|
||||
let sort_id = self.sort_ids.commit.ok_or("Commit sort not found")?;
|
||||
let commit_slid = self.add_element(sort_id, message.unwrap_or("commit"));
|
||||
|
||||
// Set parent if there's a head
|
||||
if let Some(head) = self.head {
|
||||
let parent_func = self.func_ids.commit_parent.ok_or("Commit/parent not found")?;
|
||||
self.define_func(parent_func, commit_slid, head)?;
|
||||
}
|
||||
|
||||
// Create NameBindings for all uncommitted changes
|
||||
let nb_sort = self.sort_ids.name_binding.ok_or("NameBinding sort not found")?;
|
||||
let commit_func = self.func_ids.name_binding_commit.ok_or("NameBinding/commit not found")?;
|
||||
let theory_func = self.func_ids.name_binding_theory.ok_or("NameBinding/theory not found")?;
|
||||
let instance_func = self.func_ids.name_binding_instance.ok_or("NameBinding/instance not found")?;
|
||||
|
||||
// Collect uncommitted to avoid borrow issues
|
||||
let uncommitted: Vec<_> = self.uncommitted.drain().collect();
|
||||
for (name, binding) in uncommitted {
|
||||
let nb_slid = self.add_element(nb_sort, &format!("nb_{}_{}", name, commit_slid.index()));
|
||||
self.define_func(commit_func, nb_slid, commit_slid)?;
|
||||
|
||||
match binding.kind {
|
||||
BindingKind::Theory => {
|
||||
self.define_func(theory_func, nb_slid, binding.target)?;
|
||||
}
|
||||
BindingKind::Instance => {
|
||||
self.define_func(instance_func, nb_slid, binding.target)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update head
|
||||
self.head = Some(commit_slid);
|
||||
|
||||
// Auto-save
|
||||
self.save()?;
|
||||
|
||||
Ok(commit_slid)
|
||||
}
|
||||
|
||||
/// Get the current binding for a name (from HEAD commit or uncommitted)
|
||||
pub fn resolve_name(&self, name: &str) -> Option<(Slid, BindingKind)> {
|
||||
// Check uncommitted first
|
||||
if let Some(binding) = self.uncommitted.get(name) {
|
||||
return Some((binding.target, binding.kind));
|
||||
}
|
||||
|
||||
// Search through name bindings from HEAD backwards (if we have commits)
|
||||
if let (Some(head), Some(nb_sort), Some(commit_func), Some(theory_func), Some(instance_func)) = (
|
||||
self.head,
|
||||
self.sort_ids.name_binding,
|
||||
self.func_ids.name_binding_commit,
|
||||
self.func_ids.name_binding_theory,
|
||||
self.func_ids.name_binding_instance,
|
||||
) {
|
||||
let mut current = Some(head);
|
||||
while let Some(commit) = current {
|
||||
// Find all NameBindings for this commit
|
||||
for nb_slid in self.elements_of_sort(nb_sort) {
|
||||
if self.get_func(commit_func, nb_slid) == Some(commit) {
|
||||
// Check if this binding is for our name
|
||||
let nb_name = self.get_element_name(nb_slid);
|
||||
if nb_name.starts_with(&format!("nb_{}_", name)) {
|
||||
// Found it! Return the target
|
||||
if let Some(theory) = self.get_func(theory_func, nb_slid) {
|
||||
return Some((theory, BindingKind::Theory));
|
||||
}
|
||||
if let Some(instance) = self.get_func(instance_func, nb_slid) {
|
||||
return Some((instance, BindingKind::Instance));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Move to parent commit
|
||||
if let Some(parent_func) = self.func_ids.commit_parent {
|
||||
current = self.get_func(parent_func, commit);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: search directly in meta Structure for uncommitted theories/instances
|
||||
// This handles the case where data exists in meta.bin but no commit was made yet
|
||||
if let Some(theory_sort) = self.sort_ids.theory {
|
||||
for slid in self.elements_of_sort(theory_sort) {
|
||||
if self.get_element_name(slid) == name {
|
||||
return Some((slid, BindingKind::Theory));
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(instance_sort) = self.sort_ids.instance {
|
||||
for slid in self.elements_of_sort(instance_sort) {
|
||||
if self.get_element_name(slid) == name {
|
||||
return Some((slid, BindingKind::Instance));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Get all commits in order (oldest to newest)
|
||||
pub fn commit_history(&self) -> Vec<Slid> {
|
||||
let Some(head) = self.head else {
|
||||
return vec![];
|
||||
};
|
||||
|
||||
let mut chain = Vec::new();
|
||||
let mut current = Some(head);
|
||||
|
||||
while let Some(commit) = current {
|
||||
chain.push(commit);
|
||||
current = self
|
||||
.func_ids
|
||||
.commit_parent
|
||||
.and_then(|f| self.get_func(f, commit));
|
||||
}
|
||||
|
||||
chain.reverse();
|
||||
chain
|
||||
}
|
||||
|
||||
/// List all committed bindings (theories and instances)
|
||||
///
|
||||
/// Returns (name, kind, target_slid) for each binding visible from HEAD.
|
||||
/// Names may appear multiple times if rebound in different commits.
|
||||
pub fn list_bindings(&self) -> Vec<(String, BindingKind, Slid)> {
|
||||
let Some(head) = self.head else {
|
||||
return vec![];
|
||||
};
|
||||
|
||||
let Some(nb_sort) = self.sort_ids.name_binding else {
|
||||
return vec![];
|
||||
};
|
||||
let Some(commit_func) = self.func_ids.name_binding_commit else {
|
||||
return vec![];
|
||||
};
|
||||
let Some(theory_func) = self.func_ids.name_binding_theory else {
|
||||
return vec![];
|
||||
};
|
||||
let Some(instance_func) = self.func_ids.name_binding_instance else {
|
||||
return vec![];
|
||||
};
|
||||
|
||||
let mut bindings = Vec::new();
|
||||
let mut seen_names = std::collections::HashSet::new();
|
||||
|
||||
// Walk commits from head backwards
|
||||
let mut current = Some(head);
|
||||
while let Some(commit) = current {
|
||||
// Find all NameBindings for this commit
|
||||
for nb_slid in self.elements_of_sort(nb_sort) {
|
||||
if self.get_func(commit_func, nb_slid) == Some(commit) {
|
||||
// Extract name from "nb_{name}_{commit_id}"
|
||||
let nb_name = self.get_element_name(nb_slid);
|
||||
if let Some(name) = extract_binding_name(&nb_name) {
|
||||
// Only include first (most recent) binding for each name
|
||||
if seen_names.insert(name.clone()) {
|
||||
if let Some(theory) = self.get_func(theory_func, nb_slid) {
|
||||
bindings.push((name, BindingKind::Theory, theory));
|
||||
} else if let Some(instance) = self.get_func(instance_func, nb_slid) {
|
||||
bindings.push((name, BindingKind::Instance, instance));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Move to parent commit
|
||||
current = self
|
||||
.func_ids
|
||||
.commit_parent
|
||||
.and_then(|f| self.get_func(f, commit));
|
||||
}
|
||||
|
||||
bindings
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract the name from a binding element name like "nb_Graph_2"
|
||||
fn extract_binding_name(nb_name: &str) -> Option<String> {
|
||||
// Format: "nb_{name}_{commit_id}"
|
||||
if !nb_name.starts_with("nb_") {
|
||||
return None;
|
||||
}
|
||||
let rest = &nb_name[3..]; // Skip "nb_"
|
||||
// Find the last underscore (before commit_id)
|
||||
if let Some(last_underscore) = rest.rfind('_') {
|
||||
// Verify the part after underscore is a number
|
||||
if rest[last_underscore + 1..].parse::<usize>().is_ok() {
|
||||
return Some(rest[..last_underscore].to_string());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
356
src/store/instance.rs
Normal file
356
src/store/instance.rs
Normal file
@ -0,0 +1,356 @@
|
||||
//! Instance operations for the Store.
|
||||
//!
|
||||
//! Creating, extending, and modifying instances in the GeologMeta structure.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::core::{RelationStorage, Structure};
|
||||
use crate::id::{NumericId, Slid, Uuid};
|
||||
|
||||
use super::append::AppendOps;
|
||||
use super::columnar::{InstanceDataBatch, RelationTupleBatch};
|
||||
use super::{BindingKind, Store, UncommittedBinding};
|
||||
|
||||
impl Store {
|
||||
/// Create a new instance (version 0, no parent)
|
||||
pub fn create_instance(&mut self, name: &str, theory: Slid) -> Result<Slid, String> {
|
||||
let sort_id = self.sort_ids.instance.ok_or("Instance sort not found")?;
|
||||
let instance_slid = self.add_element(sort_id, name);
|
||||
|
||||
// Set theory
|
||||
let func_id = self.func_ids.instance_theory.ok_or("Instance/theory not found")?;
|
||||
self.define_func(func_id, instance_slid, theory)?;
|
||||
|
||||
// Register uncommitted binding
|
||||
self.uncommitted.insert(
|
||||
name.to_string(),
|
||||
UncommittedBinding {
|
||||
target: instance_slid,
|
||||
kind: BindingKind::Instance,
|
||||
},
|
||||
);
|
||||
|
||||
Ok(instance_slid)
|
||||
}
|
||||
|
||||
/// Create a new version of an existing instance
|
||||
pub fn extend_instance(&mut self, parent: Slid, name: &str) -> Result<Slid, String> {
|
||||
let sort_id = self.sort_ids.instance.ok_or("Instance sort not found")?;
|
||||
|
||||
// Get the theory from the parent
|
||||
let theory_func = self.func_ids.instance_theory.ok_or("Instance/theory not found")?;
|
||||
let theory = self.get_func(theory_func, parent).ok_or("Parent has no theory")?;
|
||||
|
||||
let instance_slid = self.add_element(
|
||||
sort_id,
|
||||
&format!("{}@v{}", name, self.meta.carriers[sort_id].len()),
|
||||
);
|
||||
|
||||
// Set parent and theory
|
||||
let parent_func = self.func_ids.instance_parent.ok_or("Instance/parent not found")?;
|
||||
self.define_func(parent_func, instance_slid, parent)?;
|
||||
self.define_func(theory_func, instance_slid, theory)?;
|
||||
|
||||
// Update uncommitted binding
|
||||
self.uncommitted.insert(
|
||||
name.to_string(),
|
||||
UncommittedBinding {
|
||||
target: instance_slid,
|
||||
kind: BindingKind::Instance,
|
||||
},
|
||||
);
|
||||
|
||||
Ok(instance_slid)
|
||||
}
|
||||
|
||||
/// Add an element to an instance
|
||||
pub fn add_elem(&mut self, instance: Slid, srt: Slid, name: &str) -> Result<Slid, String> {
|
||||
let sort_id = self.sort_ids.elem.ok_or("Elem sort not found")?;
|
||||
let elem_slid = self.add_element_qualified(
|
||||
sort_id,
|
||||
vec![self.get_element_name(instance), name.to_string()],
|
||||
);
|
||||
|
||||
let instance_func = self.func_ids.elem_instance.ok_or("Elem/instance not found")?;
|
||||
let sort_func = self.func_ids.elem_sort.ok_or("Elem/sort not found")?;
|
||||
|
||||
self.define_func(instance_func, elem_slid, instance)?;
|
||||
self.define_func(sort_func, elem_slid, srt)?;
|
||||
|
||||
Ok(elem_slid)
|
||||
}
|
||||
|
||||
/// Retract an element from an instance
|
||||
pub fn retract_elem(&mut self, instance: Slid, elem: Slid) -> Result<Slid, String> {
|
||||
let sort_id = self.sort_ids.elem_retract.ok_or("ElemRetract sort not found")?;
|
||||
let retract_slid = self.add_element(sort_id, &format!("retract_{}", self.get_element_name(elem)));
|
||||
|
||||
let instance_func = self.func_ids.elem_retract_instance.ok_or("ElemRetract/instance not found")?;
|
||||
let elem_func = self.func_ids.elem_retract_elem.ok_or("ElemRetract/elem not found")?;
|
||||
|
||||
self.define_func(instance_func, retract_slid, instance)?;
|
||||
self.define_func(elem_func, retract_slid, elem)?;
|
||||
|
||||
Ok(retract_slid)
|
||||
}
|
||||
|
||||
/// Define a function value in an instance
|
||||
pub fn add_func_val(
|
||||
&mut self,
|
||||
instance: Slid,
|
||||
func: Slid,
|
||||
arg: Slid,
|
||||
result: Slid,
|
||||
) -> Result<Slid, String> {
|
||||
let sort_id = self.sort_ids.func_val.ok_or("FuncVal sort not found")?;
|
||||
let fv_slid = self.add_element(
|
||||
sort_id,
|
||||
&format!("fv_{}_{}", self.get_element_name(func), self.get_element_name(arg)),
|
||||
);
|
||||
|
||||
let instance_func = self.func_ids.func_val_instance.ok_or("FuncVal/instance not found")?;
|
||||
let func_func = self.func_ids.func_val_func.ok_or("FuncVal/func not found")?;
|
||||
let arg_func = self.func_ids.func_val_arg.ok_or("FuncVal/arg not found")?;
|
||||
let result_func = self.func_ids.func_val_result.ok_or("FuncVal/result not found")?;
|
||||
|
||||
self.define_func(instance_func, fv_slid, instance)?;
|
||||
self.define_func(func_func, fv_slid, func)?;
|
||||
self.define_func(arg_func, fv_slid, arg)?;
|
||||
self.define_func(result_func, fv_slid, result)?;
|
||||
|
||||
Ok(fv_slid)
|
||||
}
|
||||
|
||||
// NOTE: No retract_func_val - function values are IMMUTABLE (Monotonic Submodel Property)
|
||||
|
||||
/// Assert a relation tuple in an instance.
|
||||
///
|
||||
/// NOTE: This is a legacy stub. Relation tuples should be persisted via columnar
|
||||
/// batches (see `store::columnar`). This method is kept for API compatibility
|
||||
/// but silently succeeds without persisting to storage.
|
||||
///
|
||||
/// TODO: Migrate callers to use columnar batch persistence.
|
||||
#[allow(unused_variables)]
|
||||
pub fn add_rel_tuple(&mut self, instance: Slid, rel: Slid, arg: Slid) -> Result<Slid, String> {
|
||||
// Relation tuples are now stored in columnar batches, not as individual
|
||||
// GeologMeta elements. This method is a no-op that returns a dummy Slid.
|
||||
//
|
||||
// The actual persistence should happen via InstanceDataBatch in columnar.rs.
|
||||
// For now, return the arg as a placeholder to avoid breaking callers.
|
||||
Ok(arg)
|
||||
}
|
||||
|
||||
// NOTE: No retract_rel_tuple - relation tuples are IMMUTABLE (Monotonic Submodel Property)
|
||||
|
||||
/// Persist all instance data (elements, function values, relation tuples) to GeologMeta.
|
||||
///
|
||||
/// This takes a Structure and persists its contents to the Store, creating Elem,
|
||||
/// FuncVal, and RelTuple elements in GeologMeta.
|
||||
///
|
||||
/// Returns a mapping from Structure Slids to GeologMeta Elem Slids.
|
||||
pub fn persist_instance_data(
|
||||
&mut self,
|
||||
instance_slid: Slid,
|
||||
theory_slid: Slid,
|
||||
structure: &Structure,
|
||||
element_names: &HashMap<Slid, String>,
|
||||
) -> Result<InstancePersistResult, String> {
|
||||
// Get theory's sorts to map sort indices to Srt Slids
|
||||
let sort_infos = self.query_theory_sorts(theory_slid);
|
||||
let func_infos = self.query_theory_funcs(theory_slid);
|
||||
let rel_infos = self.query_theory_rels(theory_slid);
|
||||
|
||||
// Build sort index -> Srt Slid mapping
|
||||
let sort_idx_to_srt: HashMap<usize, Slid> = sort_infos
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(idx, info)| (idx, info.slid))
|
||||
.collect();
|
||||
|
||||
// Build func index -> Func Slid mapping
|
||||
let func_idx_to_func: HashMap<usize, Slid> = func_infos
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(idx, info)| (idx, info.slid))
|
||||
.collect();
|
||||
|
||||
// Build rel index -> Rel Slid mapping
|
||||
let rel_idx_to_rel: HashMap<usize, Slid> = rel_infos
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(idx, info)| (idx, info.slid))
|
||||
.collect();
|
||||
|
||||
// Mapping from Structure Slid to GeologMeta Elem Slid
|
||||
let mut elem_slid_map: HashMap<Slid, Slid> = HashMap::new();
|
||||
|
||||
// 1. Persist all elements
|
||||
for (sort_idx, carrier) in structure.carriers.iter().enumerate() {
|
||||
let srt_slid = sort_idx_to_srt
|
||||
.get(&sort_idx)
|
||||
.copied()
|
||||
.ok_or_else(|| format!("Unknown sort index: {}", sort_idx))?;
|
||||
|
||||
for structure_slid_u64 in carrier.iter() {
|
||||
let structure_slid = Slid::from_usize(structure_slid_u64 as usize);
|
||||
let elem_name = element_names
|
||||
.get(&structure_slid)
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or_else(|| "elem");
|
||||
|
||||
let elem_slid = self.add_elem(instance_slid, srt_slid, elem_name)?;
|
||||
elem_slid_map.insert(structure_slid, elem_slid);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Persist function values
|
||||
// For now, only handle base domain functions (not product domains)
|
||||
for (func_idx, func_col) in structure.functions.iter().enumerate() {
|
||||
let func_slid = match func_idx_to_func.get(&func_idx) {
|
||||
Some(s) => *s,
|
||||
None => continue, // Skip if no corresponding Func in theory
|
||||
};
|
||||
|
||||
match func_col {
|
||||
crate::core::FunctionColumn::Local(values) => {
|
||||
for (local_idx, opt_result) in values.iter().enumerate() {
|
||||
if let Some(result_slid) = crate::id::get_slid(*opt_result) {
|
||||
// Find the structure Slid for this local index
|
||||
// The local index corresponds to position in the domain sort's carrier
|
||||
if let Some(domain_sort_idx) = self.get_func_domain_sort(func_slid)
|
||||
&& let Some(carrier) = structure.carriers.get(domain_sort_idx)
|
||||
&& let Some(arg_u64) = carrier.iter().nth(local_idx) {
|
||||
let arg_slid = Slid::from_usize(arg_u64 as usize);
|
||||
if let (Some(&arg_elem), Some(&result_elem)) =
|
||||
(elem_slid_map.get(&arg_slid), elem_slid_map.get(&result_slid))
|
||||
{
|
||||
self.add_func_val(instance_slid, func_slid, arg_elem, result_elem)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
crate::core::FunctionColumn::External(_) => {
|
||||
// External functions reference elements from other instances
|
||||
// TODO: Handle external references
|
||||
}
|
||||
crate::core::FunctionColumn::ProductLocal { .. } => {
|
||||
// Product domain functions need special handling
|
||||
// TODO: Handle product domains
|
||||
}
|
||||
crate::core::FunctionColumn::ProductCodomain { .. } => {
|
||||
// Product codomain functions need special handling
|
||||
// TODO: Handle product codomains (store each field value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Persist relation tuples via columnar batches
|
||||
// Build InstanceDataBatch with all relation tuples
|
||||
let mut batch = InstanceDataBatch::new();
|
||||
|
||||
// Get instance UUID for the batch
|
||||
let instance_uuid = self.get_element_uuid(instance_slid);
|
||||
|
||||
// Build a map from Structure Slid to element UUID
|
||||
let struct_slid_to_uuid: HashMap<Slid, Uuid> = elem_slid_map
|
||||
.iter()
|
||||
.map(|(&struct_slid, &elem_slid)| {
|
||||
(struct_slid, self.get_element_uuid(elem_slid))
|
||||
})
|
||||
.collect();
|
||||
|
||||
for (rel_idx, relation) in structure.relations.iter().enumerate() {
|
||||
let rel_slid = match rel_idx_to_rel.get(&rel_idx) {
|
||||
Some(s) => *s,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
if relation.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the relation UUID
|
||||
let rel_uuid = self.get_element_uuid(rel_slid);
|
||||
|
||||
// Get field UUIDs for this relation's domain
|
||||
let rel_info = rel_infos.get(rel_idx);
|
||||
let arity = rel_info.map(|r| r.domain.arity()).unwrap_or(1);
|
||||
|
||||
// For field_ids, we use the field UUIDs from the relation's domain
|
||||
// For now, use placeholder UUIDs since we need to query Field elements
|
||||
// TODO: Query Field elements from GeologMeta for proper UUIDs
|
||||
let field_ids: Vec<Uuid> = (0..arity).map(|_| Uuid::nil()).collect();
|
||||
|
||||
let mut rel_batch = RelationTupleBatch::new(
|
||||
instance_uuid,
|
||||
rel_uuid,
|
||||
field_ids,
|
||||
);
|
||||
|
||||
// Add all tuples
|
||||
for tuple in relation.iter() {
|
||||
// Convert Structure Slids to UUIDs
|
||||
let uuid_tuple: Vec<Uuid> = tuple
|
||||
.iter()
|
||||
.filter_map(|struct_slid| struct_slid_to_uuid.get(struct_slid).copied())
|
||||
.collect();
|
||||
|
||||
if uuid_tuple.len() == tuple.len() {
|
||||
rel_batch.push(&uuid_tuple);
|
||||
}
|
||||
}
|
||||
|
||||
if !rel_batch.is_empty() {
|
||||
batch.relation_tuples.push(rel_batch);
|
||||
}
|
||||
}
|
||||
|
||||
// Save the batch if we have any relation tuples
|
||||
if !batch.relation_tuples.is_empty() {
|
||||
// Determine version number (count existing batches for this instance)
|
||||
let existing_batches = self.load_instance_data_batches(instance_uuid)
|
||||
.unwrap_or_default();
|
||||
let version = existing_batches.len() as u64;
|
||||
|
||||
self.save_instance_data_batch(instance_uuid, version, &batch)?;
|
||||
}
|
||||
|
||||
Ok(InstancePersistResult { elem_slid_map })
|
||||
}
|
||||
|
||||
/// Helper to get the domain sort index for a function.
|
||||
fn get_func_domain_sort(&self, func_slid: Slid) -> Option<usize> {
|
||||
let dom_func = self.func_ids.func_dom?;
|
||||
let dsort_slid = self.get_func(dom_func, func_slid)?;
|
||||
|
||||
// Check if it's a base dsort
|
||||
let base_ds_sort = self.sort_ids.base_ds?;
|
||||
let srt_func = self.func_ids.base_ds_srt?;
|
||||
let dsort_func = self.func_ids.base_ds_dsort?;
|
||||
|
||||
for base_slid in self.elements_of_sort(base_ds_sort) {
|
||||
if self.get_func(dsort_func, base_slid) == Some(dsort_slid)
|
||||
&& let Some(srt_slid) = self.get_func(srt_func, base_slid) {
|
||||
// Find this Srt's index in the theory
|
||||
let srt_theory_func = self.func_ids.srt_theory?;
|
||||
if let Some(theory_slid) = self.get_func(srt_theory_func, srt_slid) {
|
||||
let sorts = self.query_theory_sorts(theory_slid);
|
||||
for (idx, info) in sorts.iter().enumerate() {
|
||||
if info.slid == srt_slid {
|
||||
return Some(idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Result of persisting instance data to GeologMeta.
|
||||
#[derive(Debug)]
|
||||
pub struct InstancePersistResult {
|
||||
/// Mapping from Structure Slids to GeologMeta Elem Slids
|
||||
pub elem_slid_map: HashMap<Slid, Slid>,
|
||||
}
|
||||
238
src/store/materialize.rs
Normal file
238
src/store/materialize.rs
Normal file
@ -0,0 +1,238 @@
|
||||
//! Materialized views for the Store.
|
||||
//!
|
||||
//! A MaterializedView is an indexed snapshot of an instance at a specific version,
|
||||
//! computed by walking the version chain and applying all additions/retractions.
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use crate::id::{NumericId, Slid};
|
||||
|
||||
use super::append::AppendOps;
|
||||
use super::Store;
|
||||
|
||||
/// A materialized view of an instance at a specific version.
|
||||
///
|
||||
/// This is the "rendered" state of an instance after applying all patches
|
||||
/// from the root to a particular version. It can be incrementally updated
|
||||
/// when a new child version is created.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MaterializedView {
|
||||
/// The instance version this view is materialized at
|
||||
pub instance: Slid,
|
||||
|
||||
/// Live elements (not tombstoned)
|
||||
pub elements: HashSet<Slid>,
|
||||
|
||||
/// Live relation tuples: tuple_slid -> (rel, arg)
|
||||
pub rel_tuples: HashMap<Slid, (Slid, Slid)>,
|
||||
|
||||
/// Live function values: fv_slid -> (func, arg, result)
|
||||
pub func_vals: HashMap<Slid, (Slid, Slid, Slid)>,
|
||||
|
||||
/// Element tombstones (for delta computation)
|
||||
/// NOTE: Only elements can be tombstoned; FuncVals and RelTuples are immutable
|
||||
pub elem_tombstones: HashSet<Slid>,
|
||||
}
|
||||
|
||||
impl MaterializedView {
|
||||
/// Create an empty materialized view
|
||||
pub fn empty(instance: Slid) -> Self {
|
||||
Self {
|
||||
instance,
|
||||
elements: HashSet::new(),
|
||||
rel_tuples: HashMap::new(),
|
||||
func_vals: HashMap::new(),
|
||||
elem_tombstones: HashSet::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the number of live elements
|
||||
pub fn element_count(&self) -> usize {
|
||||
self.elements.len()
|
||||
}
|
||||
|
||||
/// Check if an element is live
|
||||
pub fn has_element(&self, elem: Slid) -> bool {
|
||||
self.elements.contains(&elem)
|
||||
}
|
||||
|
||||
/// Check if a relation tuple is live
|
||||
pub fn has_rel_tuple(&self, tuple: Slid) -> bool {
|
||||
self.rel_tuples.contains_key(&tuple)
|
||||
}
|
||||
|
||||
/// Get all elements of a particular sort (requires Store for lookup)
|
||||
pub fn elements_of_sort<'a>(
|
||||
&'a self,
|
||||
store: &'a Store,
|
||||
sort: Slid,
|
||||
) -> impl Iterator<Item = Slid> + 'a {
|
||||
self.elements.iter().copied().filter(move |&elem| {
|
||||
store
|
||||
.func_ids
|
||||
.elem_sort
|
||||
.and_then(|f| store.get_func(f, elem))
|
||||
.map(|s| s == sort)
|
||||
.unwrap_or(false)
|
||||
})
|
||||
}
|
||||
|
||||
/// Get all relation tuples for a particular relation
|
||||
pub fn tuples_of_relation(&self, rel: Slid) -> impl Iterator<Item = (Slid, Slid)> + '_ {
|
||||
self.rel_tuples
|
||||
.iter()
|
||||
.filter(move |(_, (r, _))| *r == rel)
|
||||
.map(|(&tuple_slid, (_, arg))| (tuple_slid, *arg))
|
||||
}
|
||||
|
||||
/// Get all function values for a particular function
|
||||
pub fn values_of_function(&self, func: Slid) -> impl Iterator<Item = (Slid, Slid)> + '_ {
|
||||
self.func_vals
|
||||
.iter()
|
||||
.filter(move |(_, (f, _, _))| *f == func)
|
||||
.map(|(_, (_, arg, result))| (*arg, *result))
|
||||
}
|
||||
}
|
||||
|
||||
impl Store {
|
||||
/// Materialize an instance from scratch by walking the parent chain.
|
||||
///
|
||||
/// This collects all additions and retractions from root to the specified
|
||||
/// version, producing a complete view of the instance state.
|
||||
pub fn materialize(&self, instance: Slid) -> MaterializedView {
|
||||
let mut view = MaterializedView::empty(instance);
|
||||
|
||||
// Collect version chain (from instance back to root)
|
||||
let mut chain = Vec::new();
|
||||
let mut version = Some(instance);
|
||||
while let Some(v) = version {
|
||||
chain.push(v);
|
||||
version = self.func_ids.instance_parent.and_then(|f| self.get_func(f, v));
|
||||
}
|
||||
|
||||
// Process from oldest to newest (reverse the chain)
|
||||
for v in chain.into_iter().rev() {
|
||||
self.apply_version_delta(&mut view, v);
|
||||
}
|
||||
|
||||
view.instance = instance;
|
||||
view
|
||||
}
|
||||
|
||||
/// Apply the delta from a single instance version to a materialized view.
|
||||
///
|
||||
/// This is the core of incremental materialization: given a view at version N,
|
||||
/// we can efficiently update it to version N+1 by applying only the changes
|
||||
/// introduced in N+1.
|
||||
pub fn apply_version_delta(&self, view: &mut MaterializedView, version: Slid) {
|
||||
// 1. Process element additions
|
||||
if let Some(elem_sort) = self.sort_ids.elem
|
||||
&& let Some(instance_func) = self.func_ids.elem_instance {
|
||||
for elem in self.elements_of_sort(elem_sort) {
|
||||
if self.get_func(instance_func, elem) == Some(version) {
|
||||
// Don't add if already tombstoned
|
||||
if !view.elem_tombstones.contains(&elem) {
|
||||
view.elements.insert(elem);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Process element retractions
|
||||
if let Some(retract_sort) = self.sort_ids.elem_retract
|
||||
&& let Some(instance_func) = self.func_ids.elem_retract_instance
|
||||
&& let Some(elem_func) = self.func_ids.elem_retract_elem {
|
||||
for retract in self.elements_of_sort(retract_sort) {
|
||||
if self.get_func(instance_func, retract) == Some(version)
|
||||
&& let Some(elem) = self.get_func(elem_func, retract) {
|
||||
view.elements.remove(&elem);
|
||||
view.elem_tombstones.insert(elem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Process relation tuple additions
|
||||
// NOTE: Relation tuples are now stored in columnar batches (see `store::columnar`),
|
||||
// not as individual GeologMeta elements. This section is a no-op until
|
||||
// columnar batch loading is implemented.
|
||||
//
|
||||
// TODO: Load relation tuples from columnar batches into view.rel_tuples
|
||||
|
||||
// 4. Process function value additions (IMMUTABLE - no retractions)
|
||||
if let Some(fv_sort) = self.sort_ids.func_val
|
||||
&& let (Some(instance_func), Some(func_func), Some(arg_func), Some(result_func)) = (
|
||||
self.func_ids.func_val_instance,
|
||||
self.func_ids.func_val_func,
|
||||
self.func_ids.func_val_arg,
|
||||
self.func_ids.func_val_result,
|
||||
) {
|
||||
for fv in self.elements_of_sort(fv_sort) {
|
||||
if self.get_func(instance_func, fv) == Some(version)
|
||||
&& let (Some(func), Some(arg), Some(result)) = (
|
||||
self.get_func(func_func, fv),
|
||||
self.get_func(arg_func, fv),
|
||||
self.get_func(result_func, fv),
|
||||
) {
|
||||
view.func_vals.insert(fv, (func, arg, result));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Incrementally update a materialized view to a new version.
|
||||
///
|
||||
/// The new version must be a direct child of the view's current version,
|
||||
/// or this will return an error.
|
||||
pub fn update_view(
|
||||
&self,
|
||||
view: &mut MaterializedView,
|
||||
new_version: Slid,
|
||||
) -> Result<(), String> {
|
||||
// Verify that new_version is a direct child of view.instance
|
||||
let parent = self
|
||||
.func_ids
|
||||
.instance_parent
|
||||
.and_then(|f| self.get_func(f, new_version));
|
||||
|
||||
if parent != Some(view.instance) {
|
||||
return Err(format!(
|
||||
"Cannot incrementally update: {} is not a direct child of {}",
|
||||
new_version.index(),
|
||||
view.instance.index()
|
||||
));
|
||||
}
|
||||
|
||||
// Apply the delta
|
||||
self.apply_version_delta(view, new_version);
|
||||
view.instance = new_version;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create a new instance version extending an existing view, and update the view.
|
||||
///
|
||||
/// This is the preferred way to modify instances: create the extension,
|
||||
/// add elements/tuples/values to it, then update the view.
|
||||
pub fn extend_instance_with_view(
|
||||
&mut self,
|
||||
view: &mut MaterializedView,
|
||||
name: &str,
|
||||
) -> Result<Slid, String> {
|
||||
let new_version = self.extend_instance(view.instance, name)?;
|
||||
|
||||
// The view can be updated after mutations are done
|
||||
// For now, just update the instance reference
|
||||
view.instance = new_version;
|
||||
|
||||
Ok(new_version)
|
||||
}
|
||||
|
||||
/// Materialize and cache a view for an instance.
|
||||
///
|
||||
/// This stores the view in a view cache for efficient reuse.
|
||||
/// The cache is invalidated when the instance is extended.
|
||||
pub fn get_or_create_view(&mut self, instance: Slid) -> MaterializedView {
|
||||
// For now, just materialize (cache can be added later)
|
||||
self.materialize(instance)
|
||||
}
|
||||
}
|
||||
585
src/store/mod.rs
Normal file
585
src/store/mod.rs
Normal file
@ -0,0 +1,585 @@
|
||||
//! Append-only store for GeologMeta elements.
|
||||
//!
|
||||
//! This module provides the foundation for geolog's persistent, versioned data model.
|
||||
//! All data (theories, instances, elements, function values, relation tuples) is stored
|
||||
//! as elements in a single GeologMeta Structure that is append-only.
|
||||
//!
|
||||
//! # Key design principles
|
||||
//!
|
||||
//! - **Append-only**: Elements are never deleted, only tombstoned
|
||||
//! - **Patch-based versioning**: Each theory/instance version is a delta from its parent
|
||||
//! - **Incremental materialization**: Views are updated efficiently as patches arrive
|
||||
//! - **Eternal format**: Once GeologMeta schema is v1.0, it never changes
|
||||
//!
|
||||
//! # Module structure
|
||||
//!
|
||||
//! - [`schema`]: Cached sort and function IDs from GeologMeta
|
||||
//! - [`append`]: Low-level element append operations
|
||||
//! - [`theory`]: Theory CRUD (create, extend, add sorts/functions/relations)
|
||||
//! - [`instance`]: Instance CRUD (create, extend, add elements, retractions)
|
||||
//! - [`commit`]: Version control (commits, name bindings, history)
|
||||
//! - [`query`]: Query operations (walking version chains)
|
||||
//! - [`materialize`]: Materialized views for fast indexed access
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::core::{DerivedSort, ElaboratedTheory, Structure};
|
||||
use crate::id::{NumericId, Slid};
|
||||
use crate::meta::geolog_meta;
|
||||
use crate::naming::NamingIndex;
|
||||
use crate::universe::Universe;
|
||||
|
||||
pub mod append;
|
||||
pub mod batch;
|
||||
pub mod bootstrap_queries;
|
||||
pub mod columnar;
|
||||
pub mod commit;
|
||||
pub mod instance;
|
||||
pub mod materialize;
|
||||
pub mod query;
|
||||
pub mod schema;
|
||||
pub mod theory;
|
||||
|
||||
pub use batch::{ElementBatch, ElementBuilder, ElementCreationContext};
|
||||
pub use materialize::MaterializedView;
|
||||
pub use schema::{FuncIds, SortIds};
|
||||
|
||||
// ============================================================================
|
||||
// STORE
|
||||
// ============================================================================
|
||||
|
||||
/// The append-only store: a single GeologMeta Structure plus indexing.
|
||||
///
|
||||
/// This is the "source of truth" for all geolog data. Theories and instances
|
||||
/// are represented as elements within this structure, along with their
|
||||
/// components (sorts, functions, relations, elements, values, etc.).
|
||||
pub struct Store {
|
||||
/// The GeologMeta instance containing all data
|
||||
pub meta: Structure,
|
||||
|
||||
/// The GeologMeta theory (for signature lookups)
|
||||
pub meta_theory: Arc<ElaboratedTheory>,
|
||||
|
||||
/// Universe for UUID <-> Luid mapping
|
||||
pub universe: Universe,
|
||||
|
||||
/// Human-readable names for UUIDs
|
||||
pub naming: NamingIndex,
|
||||
|
||||
/// Current HEAD commit (None if no commits yet)
|
||||
pub head: Option<Slid>,
|
||||
|
||||
/// Uncommitted changes (name -> target slid)
|
||||
/// These become NameBindings on commit
|
||||
pub uncommitted: HashMap<String, UncommittedBinding>,
|
||||
|
||||
/// Cached sort IDs for quick lookup
|
||||
pub(crate) sort_ids: SortIds,
|
||||
|
||||
/// Cached function IDs for quick lookup
|
||||
pub(crate) func_ids: FuncIds,
|
||||
|
||||
/// Path for persistence (None = in-memory only)
|
||||
pub path: Option<PathBuf>,
|
||||
|
||||
/// Whether there are unsaved changes
|
||||
dirty: bool,
|
||||
}
|
||||
|
||||
/// An uncommitted name binding
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UncommittedBinding {
|
||||
/// The target (Theory or Instance slid in meta)
|
||||
pub target: Slid,
|
||||
/// Whether this binds to a theory or instance
|
||||
pub kind: BindingKind,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum BindingKind {
|
||||
Theory,
|
||||
Instance,
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// APPEND TRAIT IMPLEMENTATION
|
||||
// ============================================================================
|
||||
|
||||
impl append::AppendOps for Store {
|
||||
fn add_element(&mut self, sort_id: usize, name: &str) -> Slid {
|
||||
let (slid, luid) = self.meta.add_element(&mut self.universe, sort_id);
|
||||
let uuid = self.universe.get(luid).expect("freshly created luid should have uuid");
|
||||
self.naming.insert(uuid, vec![name.to_string()]);
|
||||
self.dirty = true;
|
||||
slid
|
||||
}
|
||||
|
||||
fn add_element_qualified(&mut self, sort_id: usize, path: Vec<String>) -> Slid {
|
||||
let (slid, luid) = self.meta.add_element(&mut self.universe, sort_id);
|
||||
let uuid = self.universe.get(luid).expect("freshly created luid should have uuid");
|
||||
self.naming.insert(uuid, path);
|
||||
self.dirty = true;
|
||||
slid
|
||||
}
|
||||
|
||||
fn define_func(&mut self, func_id: usize, domain: Slid, codomain: Slid) -> Result<(), String> {
|
||||
self.meta.define_function(func_id, domain, codomain)?;
|
||||
self.dirty = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_func(&self, func_id: usize, domain: Slid) -> Option<Slid> {
|
||||
let sort_slid = self.meta.sort_local_id(domain);
|
||||
self.meta.get_function(func_id, sort_slid)
|
||||
}
|
||||
|
||||
fn elements_of_sort(&self, sort_id: usize) -> Vec<Slid> {
|
||||
if sort_id >= self.meta.carriers.len() {
|
||||
return vec![];
|
||||
}
|
||||
self.meta.carriers[sort_id]
|
||||
.iter()
|
||||
.map(|x| Slid::from_usize(x as usize))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn get_element_name(&self, slid: Slid) -> String {
|
||||
let luid = self.meta.get_luid(slid);
|
||||
if let Some(uuid) = self.universe.get(luid) {
|
||||
self.naming.display_name(&uuid)
|
||||
} else {
|
||||
format!("#{}", slid.index())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// STORE IMPL
|
||||
// ============================================================================
|
||||
|
||||
impl Store {
|
||||
/// Create a new empty store
|
||||
pub fn new() -> Self {
|
||||
let meta_theory = geolog_meta();
|
||||
let num_sorts = meta_theory.theory.signature.sorts.len();
|
||||
let mut meta = Structure::new(num_sorts);
|
||||
|
||||
// Initialize function storage for all functions in GeologMeta
|
||||
let domain_sort_ids: Vec<Option<usize>> = meta_theory
|
||||
.theory
|
||||
.signature
|
||||
.functions
|
||||
.iter()
|
||||
.map(|f| match &f.domain {
|
||||
DerivedSort::Base(sort_id) => Some(*sort_id),
|
||||
DerivedSort::Product(_) => None,
|
||||
})
|
||||
.collect();
|
||||
meta.init_functions(&domain_sort_ids);
|
||||
|
||||
// Initialize relation storage
|
||||
let arities: Vec<usize> = meta_theory
|
||||
.theory
|
||||
.signature
|
||||
.relations
|
||||
.iter()
|
||||
.map(|r| match &r.domain {
|
||||
DerivedSort::Base(_) => 1,
|
||||
DerivedSort::Product(fields) => fields.len(),
|
||||
})
|
||||
.collect();
|
||||
meta.init_relations(&arities);
|
||||
|
||||
let sort_ids = SortIds::from_theory(&meta_theory);
|
||||
let func_ids = FuncIds::from_theory(&meta_theory);
|
||||
|
||||
Self {
|
||||
meta,
|
||||
meta_theory,
|
||||
universe: Universe::new(),
|
||||
naming: NamingIndex::new(),
|
||||
head: None,
|
||||
uncommitted: HashMap::new(),
|
||||
sort_ids,
|
||||
func_ids,
|
||||
path: None,
|
||||
dirty: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a store with a persistence path
|
||||
pub fn with_path(path: impl Into<PathBuf>) -> Self {
|
||||
let path = path.into();
|
||||
|
||||
// Create directory if needed
|
||||
let _ = std::fs::create_dir_all(&path);
|
||||
|
||||
// Create store with paths for all components
|
||||
let mut store = Self::new();
|
||||
store.path = Some(path.clone());
|
||||
store.universe = Universe::with_path(path.join("universe"));
|
||||
store.naming = NamingIndex::with_path(path.join("naming"));
|
||||
store
|
||||
}
|
||||
|
||||
/// Load a store from disk, or create new if doesn't exist
|
||||
pub fn load_or_create(path: impl Into<PathBuf>) -> Self {
|
||||
let path = path.into();
|
||||
if path.exists() {
|
||||
Self::load(&path).unwrap_or_else(|_| Self::with_path(path))
|
||||
} else {
|
||||
Self::with_path(path)
|
||||
}
|
||||
}
|
||||
|
||||
/// Load a store from disk
|
||||
pub fn load(path: &Path) -> Result<Self, String> {
|
||||
// Load meta structure
|
||||
let meta_path = path.join("meta.bin");
|
||||
let meta = crate::serialize::load_structure(&meta_path)?;
|
||||
|
||||
// Load universe
|
||||
let universe_path = path.join("universe");
|
||||
let universe = Universe::load(&universe_path)?;
|
||||
|
||||
// Load naming
|
||||
let naming_path = path.join("naming");
|
||||
let naming = NamingIndex::load(&naming_path)?;
|
||||
|
||||
// Load HEAD commit reference
|
||||
let head_path = path.join("HEAD");
|
||||
let head = if head_path.exists() {
|
||||
let content = std::fs::read_to_string(&head_path)
|
||||
.map_err(|e| format!("Failed to read HEAD: {}", e))?;
|
||||
let index: usize = content
|
||||
.trim()
|
||||
.parse()
|
||||
.map_err(|e| format!("Invalid HEAD format: {}", e))?;
|
||||
Some(Slid::from_usize(index))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Get meta theory and build IDs (same as new())
|
||||
let meta_theory = geolog_meta();
|
||||
let sort_ids = SortIds::from_theory(&meta_theory);
|
||||
let func_ids = FuncIds::from_theory(&meta_theory);
|
||||
|
||||
Ok(Self {
|
||||
meta,
|
||||
meta_theory,
|
||||
universe,
|
||||
naming,
|
||||
head,
|
||||
uncommitted: HashMap::new(),
|
||||
sort_ids,
|
||||
func_ids,
|
||||
path: Some(path.to_path_buf()),
|
||||
dirty: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Save the store to disk
|
||||
pub fn save(&mut self) -> Result<(), String> {
|
||||
if !self.dirty {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let Some(path) = &self.path else {
|
||||
return Ok(()); // In-memory store, nothing to save
|
||||
};
|
||||
|
||||
// Ensure parent directory exists
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)
|
||||
.map_err(|e| format!("Failed to create directory: {}", e))?;
|
||||
}
|
||||
|
||||
// Save universe
|
||||
self.universe.save()?;
|
||||
|
||||
// Save naming
|
||||
self.naming.save()?;
|
||||
|
||||
// Save meta structure
|
||||
let meta_path = path.join("meta.bin");
|
||||
crate::serialize::save_structure(&self.meta, &meta_path)?;
|
||||
|
||||
// Save head commit reference
|
||||
if let Some(head) = self.head {
|
||||
let head_path = path.join("HEAD");
|
||||
std::fs::write(&head_path, format!("{}", head.index()))
|
||||
.map_err(|e| format!("Failed to write HEAD: {}", e))?;
|
||||
}
|
||||
|
||||
self.dirty = false;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if the store has uncommitted changes
|
||||
pub fn is_dirty(&self) -> bool {
|
||||
self.dirty || !self.uncommitted.is_empty()
|
||||
}
|
||||
|
||||
/// Get the number of elements in the meta structure
|
||||
pub fn len(&self) -> usize {
|
||||
self.meta.len()
|
||||
}
|
||||
|
||||
/// Check if the store is empty
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.meta.is_empty()
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// COLUMNAR BATCH STORAGE
|
||||
// ========================================================================
|
||||
|
||||
/// Get the directory for instance data (columnar batches)
|
||||
fn instance_data_dir(&self) -> Option<PathBuf> {
|
||||
self.path.as_ref().map(|p| p.join("instance_data"))
|
||||
}
|
||||
|
||||
/// Save instance data batch for a specific patch version.
|
||||
///
|
||||
/// Each patch can have up to 2 batches per instance:
|
||||
/// - One EDB batch (user-declared facts)
|
||||
/// - One IDB batch (chase-derived facts)
|
||||
///
|
||||
/// The batch kind is encoded in the filename to allow both to coexist.
|
||||
pub fn save_instance_data_batch(
|
||||
&self,
|
||||
instance_uuid: crate::id::Uuid,
|
||||
patch_version: u64,
|
||||
batch: &columnar::InstanceDataBatch,
|
||||
) -> Result<(), String> {
|
||||
use rkyv::ser::serializers::AllocSerializer;
|
||||
use rkyv::ser::Serializer;
|
||||
|
||||
let Some(dir) = self.instance_data_dir() else {
|
||||
return Ok(()); // In-memory store, nothing to save
|
||||
};
|
||||
|
||||
// Ensure directory exists
|
||||
std::fs::create_dir_all(&dir)
|
||||
.map_err(|e| format!("Failed to create instance_data dir: {}", e))?;
|
||||
|
||||
// Serialize batch with rkyv
|
||||
let mut serializer = AllocSerializer::<4096>::default();
|
||||
serializer.serialize_value(batch)
|
||||
.map_err(|e| format!("Failed to serialize instance data batch: {}", e))?;
|
||||
let bytes = serializer.into_serializer().into_inner();
|
||||
|
||||
// Write to file named by instance UUID, patch version, and batch kind
|
||||
// EDB batches: {uuid}_v{version}_edb.batch.bin
|
||||
// IDB batches: {uuid}_v{version}_idb.batch.bin
|
||||
let kind_suffix = match batch.kind {
|
||||
columnar::BatchKind::Edb => "edb",
|
||||
columnar::BatchKind::Idb => "idb",
|
||||
};
|
||||
let filename = format!("{}_v{}_{}.batch.bin", instance_uuid, patch_version, kind_suffix);
|
||||
let file_path = dir.join(filename);
|
||||
std::fs::write(&file_path, &bytes)
|
||||
.map_err(|e| format!("Failed to write instance data batch: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Load all instance data batches for an instance (across all patch versions).
|
||||
///
|
||||
/// Returns batches in version order so they can be applied sequentially.
|
||||
/// Both EDB and IDB batches are loaded; use `batch.kind` to filter if needed.
|
||||
pub fn load_instance_data_batches(
|
||||
&self,
|
||||
instance_uuid: crate::id::Uuid,
|
||||
) -> Result<Vec<columnar::InstanceDataBatch>, String> {
|
||||
use rkyv::Deserialize;
|
||||
|
||||
let Some(dir) = self.instance_data_dir() else {
|
||||
return Ok(vec![]); // In-memory store, no data
|
||||
};
|
||||
|
||||
if !dir.exists() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
// (version, is_idb, batch) - sort so EDB comes before IDB at same version
|
||||
let mut version_batches: Vec<(u64, bool, columnar::InstanceDataBatch)> = Vec::new();
|
||||
let prefix = format!("{}_v", instance_uuid);
|
||||
|
||||
// Read all matching batch files
|
||||
let entries = std::fs::read_dir(&dir)
|
||||
.map_err(|e| format!("Failed to read instance_data dir: {}", e))?;
|
||||
|
||||
for entry in entries {
|
||||
let entry = entry.map_err(|e| format!("Failed to read dir entry: {}", e))?;
|
||||
let path = entry.path();
|
||||
|
||||
if let Some(name) = path.file_name().and_then(|n| n.to_str())
|
||||
&& name.starts_with(&prefix) && name.ends_with(".batch.bin") {
|
||||
// Parse filename: {uuid}_v{version}_{edb|idb}.batch.bin
|
||||
// or legacy format: {uuid}_v{version}.batch.bin
|
||||
let suffix = name
|
||||
.strip_prefix(&prefix)
|
||||
.and_then(|s| s.strip_suffix(".batch.bin"))
|
||||
.ok_or_else(|| format!("Invalid batch filename: {}", name))?;
|
||||
|
||||
// Check for new format with _edb or _idb suffix
|
||||
let (version_str, is_idb) = if let Some(v) = suffix.strip_suffix("_edb") {
|
||||
(v, false)
|
||||
} else if let Some(v) = suffix.strip_suffix("_idb") {
|
||||
(v, true)
|
||||
} else {
|
||||
// Legacy format without kind suffix - assume EDB
|
||||
(suffix, false)
|
||||
};
|
||||
|
||||
let version: u64 = version_str.parse()
|
||||
.map_err(|_| format!("Invalid version in filename: {}", name))?;
|
||||
|
||||
let bytes = std::fs::read(&path)
|
||||
.map_err(|e| format!("Failed to read batch {}: {}", name, e))?;
|
||||
|
||||
let archived = rkyv::check_archived_root::<columnar::InstanceDataBatch>(&bytes)
|
||||
.map_err(|e| format!("Failed to validate batch {}: {}", name, e))?;
|
||||
|
||||
let batch: columnar::InstanceDataBatch = archived.deserialize(&mut rkyv::Infallible)
|
||||
.map_err(|_| format!("Failed to deserialize batch {}", name))?;
|
||||
|
||||
version_batches.push((version, is_idb, batch));
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by version, then EDB before IDB at same version
|
||||
version_batches.sort_by_key(|(v, is_idb, _)| (*v, *is_idb));
|
||||
Ok(version_batches.into_iter().map(|(_, _, b)| b).collect())
|
||||
}
|
||||
|
||||
/// Load only EDB (wire-transmittable) batches for an instance.
|
||||
///
|
||||
/// This is what would be sent over the network during sync.
|
||||
pub fn load_edb_batches(
|
||||
&self,
|
||||
instance_uuid: crate::id::Uuid,
|
||||
) -> Result<Vec<columnar::InstanceDataBatch>, String> {
|
||||
let all = self.load_instance_data_batches(instance_uuid)?;
|
||||
Ok(all.into_iter().filter(|b| b.is_wire_transmittable()).collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Store {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TESTS
|
||||
// ============================================================================
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_create_store() {
|
||||
let store = Store::new();
|
||||
assert!(store.head.is_none());
|
||||
assert!(store.uncommitted.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_theory() {
|
||||
let mut store = Store::new();
|
||||
let _theory = store.create_theory("TestTheory").unwrap();
|
||||
assert!(store.uncommitted.contains_key("TestTheory"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_instance() {
|
||||
let mut store = Store::new();
|
||||
let theory = store.create_theory("TestTheory").unwrap();
|
||||
let _instance = store.create_instance("TestInstance", theory).unwrap();
|
||||
assert!(store.uncommitted.contains_key("TestInstance"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_commit() {
|
||||
let mut store = Store::new();
|
||||
let _theory = store.create_theory("TestTheory").unwrap();
|
||||
let commit = store.commit(Some("Initial commit")).unwrap();
|
||||
assert_eq!(store.head, Some(commit));
|
||||
assert!(store.uncommitted.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_materialize_empty_instance() {
|
||||
let mut store = Store::new();
|
||||
let theory = store.create_theory("TestTheory").unwrap();
|
||||
let instance = store.create_instance("TestInstance", theory).unwrap();
|
||||
|
||||
let view = store.materialize(instance);
|
||||
assert_eq!(view.instance, instance);
|
||||
assert!(view.elements.is_empty());
|
||||
assert!(view.rel_tuples.is_empty());
|
||||
assert!(view.func_vals.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_materialize_with_elements() {
|
||||
let mut store = Store::new();
|
||||
let theory = store.create_theory("TestTheory").unwrap();
|
||||
let instance = store.create_instance("TestInstance", theory).unwrap();
|
||||
|
||||
// We'd need a sort in the theory to add elements, so this test is limited
|
||||
let view = store.materialize(instance);
|
||||
assert_eq!(view.instance, instance);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_incremental_view_update() {
|
||||
let mut store = Store::new();
|
||||
let theory = store.create_theory("TestTheory").unwrap();
|
||||
let v1 = store.create_instance("TestInstance", theory).unwrap();
|
||||
|
||||
let mut view = store.materialize(v1);
|
||||
assert_eq!(view.instance, v1);
|
||||
|
||||
// Extend the instance
|
||||
let v2 = store.extend_instance(v1, "TestInstance_v2").unwrap();
|
||||
|
||||
// Update view incrementally
|
||||
let result = store.update_view(&mut view, v2);
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(view.instance, v2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_incremental_update_invalid_parent() {
|
||||
let mut store = Store::new();
|
||||
let theory = store.create_theory("TestTheory").unwrap();
|
||||
let v1 = store.create_instance("Instance1", theory).unwrap();
|
||||
let v2 = store.create_instance("Instance2", theory).unwrap();
|
||||
|
||||
let mut view = store.materialize(v1);
|
||||
|
||||
// v2 is not a child of v1, so this should fail
|
||||
let result = store.update_view(&mut view, v2);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_commit_history() {
|
||||
let mut store = Store::new();
|
||||
let _theory = store.create_theory("TestTheory").unwrap();
|
||||
let c1 = store.commit(Some("First")).unwrap();
|
||||
|
||||
store.create_theory("Theory2").unwrap();
|
||||
let c2 = store.commit(Some("Second")).unwrap();
|
||||
|
||||
let history = store.commit_history();
|
||||
assert_eq!(history, vec![c1, c2]);
|
||||
}
|
||||
}
|
||||
127
src/store/query.rs
Normal file
127
src/store/query.rs
Normal file
@ -0,0 +1,127 @@
|
||||
//! Query operations for the Store.
|
||||
//!
|
||||
//! Walking instance version chains to collect elements, function values, and relation tuples.
|
||||
//!
|
||||
//! NOTE: FuncVals and RelTuples are IMMUTABLE (Monotonic Submodel Property).
|
||||
//! Only elements can be retracted.
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::id::Slid;
|
||||
|
||||
use super::append::AppendOps;
|
||||
use super::Store;
|
||||
|
||||
impl Store {
|
||||
/// Get all elements of an instance (including from parent chain)
|
||||
pub fn get_instance_elements(&self, instance: Slid) -> Vec<Slid> {
|
||||
let mut elements = Vec::new();
|
||||
let mut retractions = HashSet::new();
|
||||
|
||||
// Collect retractions first (from all versions in chain)
|
||||
let mut version = Some(instance);
|
||||
while let Some(v) = version {
|
||||
if let Some(retract_sort) = self.sort_ids.elem_retract
|
||||
&& let Some(instance_func) = self.func_ids.elem_retract_instance
|
||||
&& let Some(elem_func) = self.func_ids.elem_retract_elem {
|
||||
for retract in self.elements_of_sort(retract_sort) {
|
||||
if self.get_func(instance_func, retract) == Some(v)
|
||||
&& let Some(elem) = self.get_func(elem_func, retract) {
|
||||
retractions.insert(elem);
|
||||
}
|
||||
}
|
||||
}
|
||||
version = self.func_ids.instance_parent.and_then(|f| self.get_func(f, v));
|
||||
}
|
||||
|
||||
// Now collect elements (filtering out retracted ones)
|
||||
let mut version = Some(instance);
|
||||
while let Some(v) = version {
|
||||
if let Some(elem_sort) = self.sort_ids.elem
|
||||
&& let Some(instance_func) = self.func_ids.elem_instance {
|
||||
for elem in self.elements_of_sort(elem_sort) {
|
||||
if self.get_func(instance_func, elem) == Some(v)
|
||||
&& !retractions.contains(&elem) {
|
||||
elements.push(elem);
|
||||
}
|
||||
}
|
||||
}
|
||||
version = self.func_ids.instance_parent.and_then(|f| self.get_func(f, v));
|
||||
}
|
||||
|
||||
elements
|
||||
}
|
||||
|
||||
/// Get all relation tuples of an instance (including from parent chain)
|
||||
///
|
||||
/// NOTE: Relation tuples are now stored in columnar batches (see `store::columnar`),
|
||||
/// not as individual GeologMeta elements. This function returns empty until
|
||||
/// columnar batch loading is implemented.
|
||||
///
|
||||
/// TODO: Implement columnar batch loading for relation tuples.
|
||||
pub fn get_instance_rel_tuples(&self, _instance: Slid) -> Vec<(Slid, Slid, Slid)> {
|
||||
// Relation tuples are stored in columnar batches, not GeologMeta elements.
|
||||
// Return empty until columnar batch loading is implemented.
|
||||
vec![]
|
||||
}
|
||||
|
||||
/// Get all function values of an instance (including from parent chain)
|
||||
///
|
||||
/// Returns (fv_slid, func_slid, arg_slid, result_slid) tuples.
|
||||
/// NOTE: FuncVals are IMMUTABLE - no retractions (Monotonic Submodel Property)
|
||||
pub fn get_instance_func_vals(&self, instance: Slid) -> Vec<(Slid, Slid, Slid, Slid)> {
|
||||
let mut vals = Vec::new();
|
||||
|
||||
// Collect function values from all versions in the chain
|
||||
let mut version = Some(instance);
|
||||
while let Some(v) = version {
|
||||
if let Some(fv_sort) = self.sort_ids.func_val
|
||||
&& let (
|
||||
Some(instance_func),
|
||||
Some(func_func),
|
||||
Some(arg_func),
|
||||
Some(result_func),
|
||||
) = (
|
||||
self.func_ids.func_val_instance,
|
||||
self.func_ids.func_val_func,
|
||||
self.func_ids.func_val_arg,
|
||||
self.func_ids.func_val_result,
|
||||
) {
|
||||
for fv in self.elements_of_sort(fv_sort) {
|
||||
if self.get_func(instance_func, fv) == Some(v)
|
||||
&& let (Some(func), Some(arg), Some(result)) = (
|
||||
self.get_func(func_func, fv),
|
||||
self.get_func(arg_func, fv),
|
||||
self.get_func(result_func, fv),
|
||||
) {
|
||||
vals.push((fv, func, arg, result));
|
||||
}
|
||||
}
|
||||
}
|
||||
version = self.func_ids.instance_parent.and_then(|f| self.get_func(f, v));
|
||||
}
|
||||
|
||||
vals
|
||||
}
|
||||
|
||||
/// Get the theory for an instance
|
||||
pub fn get_instance_theory(&self, instance: Slid) -> Option<Slid> {
|
||||
self.func_ids
|
||||
.instance_theory
|
||||
.and_then(|f| self.get_func(f, instance))
|
||||
}
|
||||
|
||||
/// Get the parent of an instance (for versioning)
|
||||
pub fn get_instance_parent(&self, instance: Slid) -> Option<Slid> {
|
||||
self.func_ids
|
||||
.instance_parent
|
||||
.and_then(|f| self.get_func(f, instance))
|
||||
}
|
||||
|
||||
/// Get an element's sort
|
||||
pub fn get_elem_sort(&self, elem: Slid) -> Option<Slid> {
|
||||
self.func_ids
|
||||
.elem_sort
|
||||
.and_then(|f| self.get_func(f, elem))
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user