1315 lines
32 KiB
Markdown
1315 lines
32 KiB
Markdown
# Geolog
|
||
|
||
> This README was synthesized automatically by Claude Opus 4.5.
|
||
> As was this entire project, really.
|
||
|
||
**Geometric Logic REPL** - A language and runtime for formal specifications using geometric logic.
|
||
|
||
Geolog aims to provide a highly customizable, efficient, concurrent, append-only, persistent memory and query infrastructure for everything from business process workflow orchestration to formal verification via diagrammatic rewriting.
|
||
|
||
## Quick Start
|
||
|
||
```bash
|
||
~/dev/geolog$ cargo install --path .
|
||
Compiling geolog v0.1.0 (/home/dev/geolog)
|
||
Finished release [optimized] target(s) in 12.34s
|
||
Installing ~/.cargo/bin/geolog
|
||
Installed package `geolog v0.1.0` (executable `geolog`)
|
||
|
||
# Session 1: Define a theory
|
||
~/dev/geolog$ geolog -d foo
|
||
Workspace: foo
|
||
geolog> theory Graph {
|
||
V : Sort;
|
||
E : Sort;
|
||
src : E -> V;
|
||
tgt : E -> V;
|
||
reachable : [from: V, to: V] -> Prop;
|
||
|
||
ax/edge : forall e : E. |- [from: e src, to: e tgt] reachable;
|
||
ax/trans : forall x,y,z : V.
|
||
[from: x, to: y] reachable, [from: y, to: z] reachable
|
||
|- [from: x, to: z] reachable;
|
||
}
|
||
Defined theory Graph (2 sorts, 2 functions, 1 relations, 2 axioms)
|
||
|
||
geolog> :quit
|
||
Goodbye!
|
||
|
||
# Session 2: Create an instance with chase (theory auto-persisted!)
|
||
~/dev/geolog$ geolog -d foo
|
||
Workspace: foo
|
||
geolog> instance G : Graph = chase {
|
||
a, b, c : V;
|
||
e1, e2 : E;
|
||
e1 src = a; e1 tgt = b;
|
||
e2 src = b; e2 tgt = c;
|
||
}
|
||
Defined instance G : Graph (5 elements)
|
||
|
||
geolog> :inspect G
|
||
instance G : Graph = {
|
||
// V (3):
|
||
a : V;
|
||
b : V;
|
||
c : V;
|
||
// E (2):
|
||
e1 : E;
|
||
e2 : E;
|
||
// src:
|
||
e1 src = a;
|
||
e2 src = b;
|
||
// tgt:
|
||
e1 tgt = b;
|
||
e2 tgt = c;
|
||
// reachable (3 tuples):
|
||
[from: a, to: b] reachable;
|
||
[from: b, to: c] reachable;
|
||
[from: a, to: c] reachable;
|
||
}
|
||
|
||
geolog> :quit
|
||
Goodbye!
|
||
|
||
# Session 3: Everything persisted automatically!
|
||
~/dev/geolog$ geolog -d foo
|
||
Workspace: foo
|
||
geolog> :list
|
||
Theories:
|
||
Graph (2 sorts, 2 functions, 1 relations, 2 axioms)
|
||
Instances:
|
||
G : Graph (5 elements)
|
||
|
||
geolog> :inspect G
|
||
instance G : Graph = {
|
||
// V (3):
|
||
a : V;
|
||
b : V;
|
||
c : V;
|
||
// E (2):
|
||
e1 : E;
|
||
e2 : E;
|
||
// src:
|
||
e1 src = a;
|
||
e2 src = b;
|
||
// tgt:
|
||
e1 tgt = b;
|
||
e2 tgt = c;
|
||
// reachable (3 tuples):
|
||
[from: a, to: b] reachable;
|
||
[from: b, to: c] reachable;
|
||
[from: a, to: c] reachable;
|
||
}
|
||
|
||
# Category theory with equality saturation
|
||
~/dev/geolog$ geolog examples/geolog/category.geolog
|
||
geolog> :show Arrow
|
||
instance Arrow : Category = {
|
||
// ob (2):
|
||
A : ob;
|
||
B : ob;
|
||
// mor (3):
|
||
f : mor;
|
||
#3 : mor;
|
||
#4 : mor;
|
||
// src:
|
||
f src = A;
|
||
#3 src = A;
|
||
#4 src = B;
|
||
// tgt:
|
||
f tgt = B;
|
||
#3 tgt = A;
|
||
#4 tgt = B;
|
||
// comp (4 tuples):
|
||
[f: f, g: #4, h: f] comp;
|
||
[f: #3, g: f, h: f] comp;
|
||
[f: #3, g: #3, h: #3] comp;
|
||
[f: #4, g: #4, h: #4] comp;
|
||
// id (2 tuples):
|
||
[a: A, f: #3] id;
|
||
[a: B, f: #4] id;
|
||
}
|
||
```
|
||
|
||
The `Arrow` instance declares only objects A, B and one morphism f : A → B.
|
||
The chase derives identity morphisms (#3 = idA, #4 = idB) and all compositions,
|
||
while **equality saturation** collapses infinite self-compositions via unit laws:
|
||
- `[f: #3, g: f, h: f]` means idA;f = f (left unit)
|
||
- `[f: f, g: #4, h: f]` means f;idB = f (right unit)
|
||
- `[f: #3, g: #3, h: #3]` means idA;idA = idA (collapsed by unit law)
|
||
|
||
## Features
|
||
|
||
- **Theories**: Define sorts (types), functions, relations, and axioms
|
||
- **Instances**: Create concrete models of theories
|
||
- **Parameterized Theories**: Theories can depend on instances of other theories
|
||
- **Nested Instances**: Inline instance definitions within instances
|
||
- **Relations**: Binary and n-ary predicates with product domains
|
||
- **Axioms**: Geometric sequents, automatically checked with tensor algebra
|
||
- **Chase Algorithm**: Automatic inference of derived facts
|
||
- **Interactive REPL**: Explore and modify instances dynamically
|
||
- **Version Control**: Commit and track changes to instances
|
||
|
||
---
|
||
|
||
## Showcase: Petri Net Reachability as Dependent Types
|
||
|
||
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)
|
||
|
||
> **Note**: This showcase is tested by `cargo test test_petri_net_showcase` and
|
||
> matches `examples/geolog/petri_net_showcase.geolog` exactly.
|
||
|
||
### The Type-Theoretic Encoding
|
||
|
||
```geolog
|
||
// ============================================================
|
||
// THEORY: PetriNet - Places, transitions, and arcs
|
||
// ============================================================
|
||
|
||
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
|
||
// ============================================================
|
||
|
||
theory (N : PetriNet instance) Trace {
|
||
F : Sort; // Firings
|
||
F/of : F -> N/T; // Which transition each firing corresponds to
|
||
|
||
// 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 axioms (source/target arcs must match firing transitions)
|
||
ax/wire_src_coherent : forall w : W. |- w W/src_arc N/out/src = w W/src_firing F/of;
|
||
ax/wire_tgt_coherent : forall w : W. |- w W/tgt_arc N/in/tgt = w W/tgt_firing F/of;
|
||
ax/wire_place_coherent : forall w : W. |- w W/src_arc N/out/tgt = w W/tgt_arc N/in/src;
|
||
|
||
// Terminals connect initial/target markings to firings
|
||
input_terminal : Sort;
|
||
output_terminal : Sort;
|
||
input_terminal/of : input_terminal -> N/P;
|
||
output_terminal/of : output_terminal -> N/P;
|
||
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;
|
||
}
|
||
```
|
||
|
||
### Problem 0: Can we reach B from A with one token?
|
||
|
||
```geolog
|
||
// ============================================================
|
||
// The Petri Net:
|
||
// +---[ba]----+
|
||
// v |
|
||
// (A) --[ab]->(B) --+
|
||
// | |
|
||
// +----[abc]-------+--> (C)
|
||
// ============================================================
|
||
|
||
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;
|
||
}
|
||
|
||
// 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.
|
||
// ============================================================
|
||
|
||
instance solution0 : ExampleNet problem0 Solution = {
|
||
trace = {
|
||
f1 : F;
|
||
f1 F/of = ExampleNet/ab;
|
||
|
||
// Input terminal feeds A-token into f1's ab_in arc
|
||
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 via ab_out arc
|
||
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?
|
||
|
||
This is a more interesting case: the only path to C is via `abc`, which requires
|
||
tokens in BOTH A and B simultaneously. Starting with 2 tokens in A, we must
|
||
first move one to B, then fire `abc`.
|
||
|
||
```geolog
|
||
// Initial: 2 tokens in A, Target: 1 token in C
|
||
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: 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
|
||
// Step 2: Fire 'abc' consuming one A-token and one B-token
|
||
// ============================================================
|
||
|
||
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
|
||
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;
|
||
};
|
||
}
|
||
```
|
||
|
||
Each `Solution` instance is a **constructive diagrammatic proof**:
|
||
- The trace contains firing(s) of specific transitions
|
||
- Input terminals witness that initial tokens feed into firings
|
||
- Output terminals witness that firings produce target tokens
|
||
- The isomorphisms prove the token counts match exactly
|
||
|
||
---
|
||
|
||
## Table of Contents
|
||
|
||
1. [Basic Concepts](#basic-concepts)
|
||
2. [Theory Definitions](#theory-definitions)
|
||
3. [Instance Definitions](#instance-definitions)
|
||
4. [Relations and Axioms](#relations-and-axioms)
|
||
5. [The Chase Algorithm](#the-chase-algorithm)
|
||
6. [REPL Commands](#repl-commands)
|
||
7. [Complete Examples](#complete-examples)
|
||
|
||
---
|
||
|
||
## Basic Concepts
|
||
|
||
Geolog is based on **geometric logic**, a fragment of first-order logic that:
|
||
- Allows existential quantification in conclusions
|
||
- Allows disjunctions in conclusions
|
||
- Is preserved by geometric morphisms (structure-preserving maps)
|
||
|
||
A **theory** defines:
|
||
- **Sorts**: Types of elements
|
||
- **Function symbols**: Function-typed variables with domain and codomain derived from sorts
|
||
- **Relation symbols**: Predicate-typed variables with domain derived from sorts, and codomain `-> Prop`
|
||
- **Axioms**: Geometric sequents (first universal quantifiers, then an implication between two propositions which are then purely positive)
|
||
|
||
An **instance** is a concrete finite model, which means it assigns to each sort a finite set, to each function a finite function, and to each relation a Boolean-valued tensor, such that all axioms evaluate to true.
|
||
|
||
---
|
||
|
||
## Theory Definitions
|
||
|
||
### Simple Theory with Sorts and Functions
|
||
|
||
```geolog
|
||
// Directed Graph: vertices and edges with source/target functions
|
||
theory Graph {
|
||
V : Sort; // Vertices
|
||
E : Sort; // Edges
|
||
|
||
src : E -> V; // Source of an edge
|
||
tgt : E -> V; // Target of an edge
|
||
}
|
||
```
|
||
|
||
### Theory with Product Domain Functions
|
||
|
||
```geolog
|
||
// Monoid: a set with an associative binary operation
|
||
theory Monoid {
|
||
M : Sort;
|
||
|
||
// Binary operation: M × M → M
|
||
mul : [x: M, y: M] -> M;
|
||
|
||
// Identity element
|
||
id : M -> M;
|
||
|
||
// 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;
|
||
}
|
||
```
|
||
|
||
### REPL Session: Defining a Theory Inline
|
||
|
||
```
|
||
geolog> theory Counter {
|
||
...... C : Sort;
|
||
...... next : C -> C;
|
||
...... }
|
||
Defined theory Counter (1 sorts, 1 functions)
|
||
|
||
geolog> :inspect Counter
|
||
theory Counter {
|
||
C : Sort;
|
||
next : C -> C;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Instance Definitions
|
||
|
||
### Basic Instance
|
||
|
||
```geolog
|
||
// 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 (function definitions)
|
||
ab src = A;
|
||
ab tgt = B;
|
||
bc src = B;
|
||
bc tgt = C;
|
||
ca src = C;
|
||
ca tgt = A;
|
||
}
|
||
```
|
||
|
||
### Instance with Product Domain Functions
|
||
|
||
```geolog
|
||
// Boolean "And" monoid: {T, F} with T as identity
|
||
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;
|
||
}
|
||
```
|
||
|
||
### REPL Session: Loading and Inspecting
|
||
|
||
```
|
||
geolog> :source examples/geolog/graph.geolog
|
||
Loading examples/geolog/graph.geolog...
|
||
Defined theory Graph (2 sorts, 2 functions)
|
||
|
||
geolog> :list
|
||
Theories:
|
||
Graph (2 sorts, 2 functions)
|
||
Instances:
|
||
Diamond : Graph (8 elements)
|
||
Arrow : Graph (3 elements)
|
||
Loop : Graph (2 elements)
|
||
Triangle : Graph (6 elements)
|
||
|
||
geolog> :inspect Triangle
|
||
instance Triangle : Graph = {
|
||
// V (3):
|
||
A : V;
|
||
B : V;
|
||
C : V;
|
||
// E (3):
|
||
ab : E;
|
||
bc : E;
|
||
ca : E;
|
||
// src:
|
||
ab src = A;
|
||
bc src = B;
|
||
ca src = C;
|
||
// tgt:
|
||
ab tgt = B;
|
||
bc tgt = C;
|
||
ca tgt = A;
|
||
}
|
||
|
||
geolog> :query Triangle V
|
||
Elements of V in Triangle:
|
||
A
|
||
B
|
||
C
|
||
```
|
||
|
||
---
|
||
|
||
## Relations and Axioms
|
||
|
||
Relations are predicates on sorts, declared with `-> Prop`.
|
||
|
||
### Unary Relations
|
||
|
||
```geolog
|
||
theory TodoList {
|
||
Item : Sort;
|
||
|
||
// Unary relations use simple arrow syntax
|
||
completed : Item -> Prop;
|
||
high_priority : Item -> Prop;
|
||
blocked : Item -> Prop;
|
||
}
|
||
```
|
||
|
||
### Binary Relations
|
||
|
||
```geolog
|
||
theory Preorder {
|
||
X : Sort;
|
||
|
||
// Binary relation: x ≤ y (field names document the relation)
|
||
leq : [lo: X, hi: X] -> Prop;
|
||
|
||
// Reflexivity axiom: x ≤ x
|
||
ax/refl : forall x : X.
|
||
|- [lo: x, hi: x] leq;
|
||
|
||
// Transitivity axiom: x ≤ y ∧ y ≤ z → x ≤ z
|
||
ax/trans : forall x : X, y : X, z : X.
|
||
[lo: x, hi: y] leq, [lo: y, hi: z] leq |- [lo: x, hi: z] leq;
|
||
}
|
||
```
|
||
|
||
### Asserting Relation Tuples in Instances
|
||
|
||
```geolog
|
||
instance SampleTodos : TodoList = {
|
||
buy_groceries : Item;
|
||
cook_dinner : Item;
|
||
do_laundry : Item;
|
||
clean_house : Item;
|
||
|
||
// Assert unary relation: buy_groceries is completed
|
||
buy_groceries completed;
|
||
|
||
// Assert unary relation: cook_dinner is high priority
|
||
cook_dinner high_priority;
|
||
|
||
// Binary relation using mixed positional/named syntax:
|
||
// First positional arg maps to 'item' field, named arg for 'on'
|
||
[cook_dinner, on: buy_groceries] depends;
|
||
}
|
||
```
|
||
|
||
### REPL Session: Asserting Relations Dynamically
|
||
|
||
```
|
||
geolog> :source examples/geolog/todo_list.geolog
|
||
Loading examples/geolog/todo_list.geolog...
|
||
Defined theory TodoList (1 sorts, 4 relations)
|
||
|
||
geolog> :inspect SampleTodos
|
||
instance SampleTodos : TodoList = {
|
||
// Item (4):
|
||
buy_groceries : Item;
|
||
cook_dinner : Item;
|
||
do_laundry : Item;
|
||
clean_house : Item;
|
||
// completed (1 tuples):
|
||
[buy_groceries] completed;
|
||
// high_priority (1 tuples):
|
||
[cook_dinner] high_priority;
|
||
// depends (1 tuples):
|
||
[cook_dinner, buy_groceries] depends;
|
||
}
|
||
|
||
geolog> :assert SampleTodos completed cook_dinner
|
||
Asserted completed(cook_dinner) in instance 'SampleTodos'
|
||
|
||
geolog> :inspect SampleTodos
|
||
instance SampleTodos : TodoList = {
|
||
// Item (4):
|
||
buy_groceries : Item;
|
||
cook_dinner : Item;
|
||
do_laundry : Item;
|
||
clean_house : Item;
|
||
// completed (2 tuples):
|
||
[buy_groceries] completed;
|
||
[cook_dinner] completed;
|
||
// high_priority (1 tuples):
|
||
[cook_dinner] high_priority;
|
||
// depends (1 tuples):
|
||
[cook_dinner, buy_groceries] depends;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## The Chase Algorithm
|
||
|
||
The **chase algorithm** computes the closure of an instance under the theory's axioms. It derives all facts that logically follow from the base facts and axioms.
|
||
|
||
### Transitive Closure Example
|
||
|
||
```geolog
|
||
// Graph with reachability (transitive closure)
|
||
theory Graph {
|
||
V : Sort;
|
||
|
||
// Direct edges
|
||
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
|
||
// Using `= chase { ... }` to automatically apply axioms during elaboration.
|
||
instance Chain : Graph = chase {
|
||
a : V;
|
||
b : V;
|
||
c : V;
|
||
d : V;
|
||
|
||
// Initial edges (chase derives Path tuples)
|
||
[src: a, tgt: b] Edge;
|
||
[src: b, tgt: c] Edge;
|
||
[src: c, tgt: d] Edge;
|
||
}
|
||
```
|
||
|
||
### REPL Session: Running the Chase
|
||
|
||
When using `= chase { ... }` syntax, the chase runs automatically during elaboration:
|
||
|
||
```
|
||
geolog> :source examples/geolog/transitive_closure.geolog
|
||
Loading examples/geolog/transitive_closure.geolog...
|
||
Defined theory Graph (1 sorts, 2 relations)
|
||
Defined instance Chain : Graph (4 elements) [chase: 6 Path tuples derived]
|
||
|
||
geolog> :inspect Chain
|
||
instance Chain : Graph = {
|
||
// V (4):
|
||
a : V;
|
||
b : V;
|
||
c : V;
|
||
d : V;
|
||
// Edge (3 tuples):
|
||
[a, b] Edge;
|
||
[b, c] Edge;
|
||
[c, d] Edge;
|
||
// Path (6 tuples):
|
||
[a, b] Path;
|
||
[b, c] Path;
|
||
[c, d] Path;
|
||
[a, c] Path; // Derived: a->b + b->c
|
||
[b, d] Path; // Derived: b->c + c->d
|
||
[a, d] Path; // Derived: a->c + c->d (or a->b + b->d)
|
||
}
|
||
```
|
||
|
||
You can also run chase manually with `:chase` on non-chase instances:
|
||
|
||
```
|
||
geolog> :chase MyInstance
|
||
Running chase on instance 'MyInstance' (theory 'Graph')...
|
||
✓ Chase completed in 3 iterations (0.15ms)
|
||
```
|
||
|
||
The chase derived:
|
||
- **3 base paths** from the Edge → Path axiom
|
||
- **2 one-step transitive paths**: (a,c) and (b,d)
|
||
- **1 two-step transitive path**: (a,d)
|
||
|
||
---
|
||
|
||
## REPL Commands
|
||
|
||
### General Commands
|
||
|
||
| Command | Description |
|
||
|---------|-------------|
|
||
| `:help [topic]` | Show help (topics: syntax, examples) |
|
||
| `:quit` | Exit the REPL |
|
||
| `:list [target]` | List theories/instances |
|
||
| `:inspect <name>` | Show details of a theory or instance |
|
||
| `:source <file>` | Load and execute a .geolog file |
|
||
| `:clear` | Clear the screen |
|
||
| `:reset` | Reset all state |
|
||
|
||
### Instance Mutation
|
||
|
||
| Command | Description |
|
||
|---------|-------------|
|
||
| `:add <inst> <elem> <sort>` | Add element to instance |
|
||
| `:assert <inst> <rel> [args]` | Assert relation tuple |
|
||
| `:retract <inst> <elem>` | Retract element |
|
||
|
||
### Query Commands
|
||
|
||
| Command | Description |
|
||
|---------|-------------|
|
||
| `:query <inst> <sort>` | List all elements of a sort |
|
||
| `:explain <inst> <sort>` | Show query execution plan |
|
||
| `:compile <inst> <sort>` | Show RelAlgIR compilation |
|
||
| `:chase <inst> [max_iter]` | Run chase algorithm |
|
||
|
||
### Version Control
|
||
|
||
| Command | Description |
|
||
|---------|-------------|
|
||
| `:commit [msg]` | Commit current changes |
|
||
| `:history` | Show commit history |
|
||
|
||
### Solver
|
||
|
||
| Command | Description |
|
||
|---------|-------------|
|
||
| `:solve <theory> [budget_ms]` | Find model of theory |
|
||
| `:extend <inst> <theory> [budget_ms]` | Extend instance to theory |
|
||
|
||
### REPL Session: Query Explanation
|
||
|
||
```
|
||
geolog> :source examples/geolog/graph.geolog
|
||
Loading examples/geolog/graph.geolog...
|
||
Defined theory Graph (2 sorts, 2 functions)
|
||
|
||
geolog> :explain Triangle V
|
||
Query plan for ':query Triangle V':
|
||
|
||
Scan(sort=0)
|
||
|
||
Sort: V (index 0)
|
||
Instance: Triangle (theory: Graph)
|
||
|
||
geolog> :explain Triangle E
|
||
Query plan for ':query Triangle E':
|
||
|
||
Scan(sort=1)
|
||
|
||
Sort: E (index 1)
|
||
Instance: Triangle (theory: Graph)
|
||
```
|
||
|
||
---
|
||
|
||
## Complete Examples
|
||
|
||
### Example 1: Directed Graphs
|
||
|
||
**File: `examples/geolog/graph.geolog`**
|
||
|
||
```geolog
|
||
// Directed Graph: vertices and edges with source/target functions
|
||
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 = {
|
||
A : V;
|
||
B : V;
|
||
C : V;
|
||
|
||
ab : E;
|
||
bc : E;
|
||
ca : E;
|
||
|
||
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;
|
||
}
|
||
|
||
// Diamond shape with two paths from top to 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;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### Example 2: Algebraic Structures (Monoids)
|
||
|
||
**File: `examples/geolog/monoid.geolog`**
|
||
|
||
```geolog
|
||
// Monoid: a set with an associative binary operation and identity
|
||
theory Monoid {
|
||
M : Sort;
|
||
|
||
// Binary operation: M × M → M
|
||
mul : [x: M, y: M] -> M;
|
||
|
||
// Identity element selector
|
||
id : M -> M;
|
||
|
||
// Left identity: id(x) * y = y
|
||
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;
|
||
}
|
||
|
||
// Trivial monoid: single element
|
||
instance Trivial : Monoid = {
|
||
e : M;
|
||
[x: e, y: e] mul = e;
|
||
e id = e;
|
||
}
|
||
|
||
// Boolean "And" monoid
|
||
instance BoolAnd : Monoid = {
|
||
T : M;
|
||
F : M;
|
||
|
||
T id = T;
|
||
F id = T;
|
||
|
||
[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
|
||
instance BoolOr : Monoid = {
|
||
T : M;
|
||
F : M;
|
||
|
||
T id = F;
|
||
F id = F;
|
||
|
||
[x: T, y: T] mul = T;
|
||
[x: T, y: F] mul = T;
|
||
[x: F, y: T] mul = T;
|
||
[x: F, y: F] mul = F;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### Example 3: Preorders with Chase
|
||
|
||
**File: `examples/geolog/preorder.geolog`**
|
||
|
||
```geolog
|
||
// Preorder: reflexive and transitive relation
|
||
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;
|
||
}
|
||
|
||
// Discrete preorder: only reflexive pairs
|
||
// Uses `chase` to automatically derive reflexive pairs from ax/refl.
|
||
instance Discrete3 : Preorder = chase {
|
||
a : X;
|
||
b : X;
|
||
c : X;
|
||
}
|
||
|
||
// A total order on 3 elements: bot ≤ mid ≤ top
|
||
instance Chain3 : Preorder = chase {
|
||
bot : X;
|
||
mid : X;
|
||
top : X;
|
||
|
||
[x: bot, y: mid] leq;
|
||
[x: mid, y: top] leq;
|
||
// Chase derives: (bot,bot), (mid,mid), (top,top) + (bot,top)
|
||
}
|
||
```
|
||
|
||
**REPL Session:**
|
||
|
||
```
|
||
geolog> :source examples/geolog/preorder.geolog
|
||
Defined theory Preorder (1 sorts, 1 relations)
|
||
Defined instance Discrete3 : Preorder (3 elements) [chase: 3 leq tuples derived]
|
||
Defined instance Chain3 : Preorder (3 elements) [chase: 6 leq tuples derived]
|
||
|
||
geolog> :inspect Discrete3
|
||
leq: 3 tuple(s) // (a,a), (b,b), (c,c) - reflexivity only
|
||
|
||
geolog> :inspect Chain3
|
||
leq: 6 tuple(s) // reflexive pairs + given + transitive (bot,top)
|
||
```
|
||
|
||
---
|
||
|
||
### Example 4: Task Management
|
||
|
||
**File: `examples/geolog/todo_list.geolog`**
|
||
|
||
```geolog
|
||
// TodoList: relational model for task tracking
|
||
theory TodoList {
|
||
Item : Sort;
|
||
|
||
// Status relations (unary, simple arrow syntax)
|
||
completed : Item -> Prop;
|
||
high_priority : Item -> Prop;
|
||
blocked : Item -> Prop;
|
||
|
||
// Dependencies (binary, with named fields)
|
||
depends : [item: Item, on: Item] -> Prop;
|
||
|
||
// Axiom: blocked items depend on incomplete items
|
||
ax/dep_blocked : forall x : Item, y : Item.
|
||
[item: x, on: y] depends |- x blocked \/ y completed;
|
||
}
|
||
|
||
instance SampleTodos : TodoList = {
|
||
buy_groceries : Item;
|
||
cook_dinner : Item;
|
||
do_laundry : Item;
|
||
clean_house : Item;
|
||
|
||
// Unary relations: simple syntax
|
||
buy_groceries completed;
|
||
cook_dinner high_priority;
|
||
|
||
// Binary relation: mixed positional/named syntax
|
||
// First positional arg -> 'item', named arg for 'on'
|
||
[cook_dinner, on: buy_groceries] depends;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### Example 5: Transitive Closure (Chase Demo)
|
||
|
||
**File: `examples/geolog/transitive_closure.geolog`**
|
||
|
||
```geolog
|
||
// Transitive Closure - demonstrates the chase algorithm
|
||
theory Graph {
|
||
V : Sort;
|
||
|
||
Edge : [src: V, tgt: V] -> Prop;
|
||
Path : [src: V, tgt: V] -> Prop;
|
||
|
||
// Base: edges are paths
|
||
ax/base : forall x, y : V.
|
||
[src: x, tgt: y] Edge |- [src: x, tgt: y] Path;
|
||
|
||
// Transitivity: paths compose
|
||
ax/trans : forall x, y, z : V.
|
||
[src: x, tgt: y] Path, [src: y, tgt: z] Path |- [src: x, tgt: z] Path;
|
||
}
|
||
|
||
// Linear chain: a -> b -> c -> d (chase runs automatically)
|
||
instance Chain : Graph = chase {
|
||
a : V;
|
||
b : V;
|
||
c : V;
|
||
d : V;
|
||
|
||
[src: a, tgt: b] Edge;
|
||
[src: b, tgt: c] Edge;
|
||
[src: c, tgt: d] Edge;
|
||
}
|
||
|
||
// Diamond: two paths from top to bottom
|
||
instance Diamond : Graph = chase {
|
||
top : V;
|
||
left : V;
|
||
right : V;
|
||
bottom : V;
|
||
|
||
[src: top, tgt: left] Edge;
|
||
[src: top, tgt: right] Edge;
|
||
[src: left, tgt: bottom] Edge;
|
||
[src: right, tgt: bottom] Edge;
|
||
}
|
||
|
||
// Cycle: x -> y -> z -> x (chase computes all 9 pairs!)
|
||
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;
|
||
}
|
||
```
|
||
|
||
**REPL Session** (chase runs during `:source`):
|
||
|
||
```
|
||
geolog> :source examples/geolog/transitive_closure.geolog
|
||
Defined theory Graph (1 sorts, 2 relations)
|
||
Defined instance Chain : Graph (4 elements) [chase: 6 Path tuples]
|
||
Defined instance Diamond : Graph (4 elements) [chase: 5 Path tuples]
|
||
Defined instance Cycle : Graph (3 elements) [chase: 9 Path tuples]
|
||
```
|
||
|
||
---
|
||
|
||
### Example 6: Inline Definitions
|
||
|
||
You can define theories and instances directly in the REPL:
|
||
|
||
```
|
||
geolog> theory Counter {
|
||
...... C : Sort;
|
||
...... next : C -> C;
|
||
...... }
|
||
Defined theory Counter (1 sorts, 1 functions)
|
||
|
||
geolog> instance Mod3 : Counter = {
|
||
...... zero : C;
|
||
...... one : C;
|
||
...... two : C;
|
||
...... zero next = one;
|
||
...... one next = two;
|
||
...... two next = zero;
|
||
...... }
|
||
Defined instance Mod3 : Counter (3 elements)
|
||
|
||
geolog> :inspect Mod3
|
||
instance Mod3 : Counter = {
|
||
// C (3):
|
||
zero : C;
|
||
one : C;
|
||
two : C;
|
||
// next:
|
||
zero next = one;
|
||
one next = two;
|
||
two next = zero;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Syntax Reference
|
||
|
||
### Sorts
|
||
```
|
||
identifier : Sort;
|
||
```
|
||
|
||
### Functions
|
||
```
|
||
// Unary function
|
||
name : Domain -> Codomain;
|
||
|
||
// Binary function (product domain)
|
||
name : [field1: Sort1, field2: Sort2] -> Codomain;
|
||
```
|
||
|
||
### Relations
|
||
```
|
||
// Unary relation
|
||
name : [field: Sort] -> Prop;
|
||
|
||
// Binary relation
|
||
name : [x: Sort1, y: Sort2] -> Prop;
|
||
```
|
||
|
||
### Axioms
|
||
```
|
||
// No premises (fact)
|
||
name : forall vars. |- conclusion;
|
||
|
||
// With premises
|
||
name : forall vars. premise1, premise2 |- conclusion;
|
||
|
||
// With disjunction in conclusion
|
||
name : forall vars. premise |- conclusion1 \/ conclusion2;
|
||
```
|
||
|
||
### Instance Elements
|
||
```
|
||
elem_name : Sort;
|
||
```
|
||
|
||
### Function Values
|
||
```
|
||
// Unary
|
||
elem func = value;
|
||
|
||
// Product domain
|
||
[field1: val1, field2: val2] func = value;
|
||
```
|
||
|
||
### Relation Assertions
|
||
```
|
||
// Unary relation
|
||
elem relation;
|
||
|
||
// Binary relation
|
||
[field1: val1, field2: val2] relation;
|
||
```
|
||
|
||
---
|
||
|
||
## Architecture
|
||
|
||
> TODO: greatly expand this section
|
||
|
||
Geolog is built with several key components:
|
||
|
||
- **Parser**: Converts `.geolog` source to AST
|
||
- **Elaborator**: Type-checks and converts AST to core representations
|
||
- **Structure**: In-memory model representation with carriers and functions
|
||
- **Chase Engine**: Fixpoint computation for derived relations
|
||
- **Query Engine**: Relational algebra for querying instances
|
||
- **Store**: Persistent, append-only storage with version control
|
||
|
||
---
|
||
|
||
## License
|
||
|
||
MIT License - see LICENSE file for details.
|
||
|
||
---
|
||
|
||
## Contributing
|
||
|
||
Contributions welcome! See CLAUDE.md for development guidelines and the `loose_thoughts/` directory for design discussions.
|