Add scaffolding for SQL support

This commit is contained in:
Hassan Abedi 2026-04-09 12:38:43 +02:00
parent 0f705a3fbd
commit 0986e13669
51 changed files with 3963 additions and 11 deletions

View File

@ -6,7 +6,7 @@ This file provides guidance to coding agents collaborating on this repository.
Query Engine is an experimental Rust project for building query-engine Query Engine is an experimental Rust project for building query-engine
components. The current implementation is centered on a chase-based reasoning components. The current implementation is centered on a chase-based reasoning
core plus lightweight interactive frontends. core, lightweight interactive frontends, and an early relational/SQL scaffold.
Priorities, in order: Priorities, in order:
@ -52,8 +52,14 @@ Quick examples:
- `rule.rs`: TGDs, EGDs, equalities, and builders. - `rule.rs`: TGDs, EGDs, equalities, and builders.
- `substitution.rs`: variable bindings and unification. - `substitution.rs`: variable bindings and unification.
- `engine.rs`: chase execution and configuration. - `engine.rs`: chase execution and configuration.
- `inference.rs`: shared matching and provenance-aware materialization helpers.
- `union_find.rs`: equality merging support. - `union_find.rs`: equality merging support.
- `src/frontend/`: lightweight interactive surface for scripts, REPL, and local web UI. - `src/frontend/`: lightweight interactive surface for scripts, REPL, and local web UI.
- `src/relational/`: schemas, values, rows, and result sets for relational execution.
- `src/catalog/`: predicate-to-table schema inference and catalog access.
- `src/sql/`: narrow SQL AST and parser support.
- `src/planner/`: logical plan structures and SQL-to-plan translation.
- `src/execution/`: execution of the current logical plan subset.
- `tests/`: integration, regression, and property-based tests. - `tests/`: integration, regression, and property-based tests.
## Architecture Constraints ## Architecture Constraints
@ -64,6 +70,8 @@ Quick examples:
- The chase engine should remain largely stateless; pass execution state explicitly. - The chase engine should remain largely stateless; pass execution state explicitly.
- New chase variants should be composable with existing infrastructure. - New chase variants should be composable with existing infrastructure.
- Existential variables generate labeled nulls (`Term::Null`). - Existential variables generate labeled nulls (`Term::Null`).
- The current SQL support is intentionally narrow: single-table `SELECT-FROM-WHERE` with positional column names such as `c0` and `c1`.
- Relational and SQL modules should build on explicit schemas and logical plans, not call frontend helpers directly.
- If you add parser, planner, or executor layers, keep their responsibilities separate. - If you add parser, planner, or executor layers, keep their responsibilities separate.
- Public docs and interfaces should reflect the implemented state of the repository accurately. - Public docs and interfaces should reflect the implemented state of the repository accurately.
@ -105,6 +113,7 @@ Example scopes that are good first tasks:
- Implement a new utility method on `Instance` or `Atom`. - Implement a new utility method on `Instance` or `Atom`.
- Tighten frontend wording so it matches actual behavior. - Tighten frontend wording so it matches actual behavior.
- Introduce a small planning-oriented type without changing execution semantics. - Introduce a small planning-oriented type without changing execution semantics.
- Extend the SQL slice with a narrow, well-tested feature such as aliases or named columns.
## Testing Expectations ## Testing Expectations
@ -113,6 +122,7 @@ Example scopes that are good first tasks:
- Integration tests go in `tests/integration_tests.rs`. - Integration tests go in `tests/integration_tests.rs`.
- Regression tests for bug fixes go in `tests/regression_tests.rs`. - Regression tests for bug fixes go in `tests/regression_tests.rs`.
- Property-based tests go in `tests/property_tests.rs`. - Property-based tests go in `tests/property_tests.rs`.
- SQL/planner/execution flow tests go in `tests/sql_pipeline_tests.rs`.
- Do not merge code that breaks existing tests. - Do not merge code that breaks existing tests.
Minimal unit-test checklist for chase-related behavior: Minimal unit-test checklist for chase-related behavior:
@ -151,6 +161,7 @@ Before coding:
2. Identify affected tests. 2. Identify affected tests.
3. Consider impact on API stability. 3. Consider impact on API stability.
4. Avoid overstating roadmap progress in code comments or docs. 4. Avoid overstating roadmap progress in code comments or docs.
5. Keep the supported SQL subset explicit when touching `sql`, `planner`, or `execution`.
Before submitting: Before submitting:
@ -186,6 +197,7 @@ Use this review format:
- If you detect contradictory repository conventions, follow existing code and update docs accordingly. - If you detect contradictory repository conventions, follow existing code and update docs accordingly.
- When uncertain about correctness, add or extend tests first, then optimize. - When uncertain about correctness, add or extend tests first, then optimize.
- When adding non-chase engine pieces, define clean interfaces before broadening functionality. - When adding non-chase engine pieces, define clean interfaces before broadening functionality.
- Keep `frontend` presentation-only when possible; shared reasoning logic belongs in `chase`, relational logic in `relational`/`planner`/`execution`.
- Keep user-facing naming consistent with the repository name: `query-engine` / `query_engine`. - Keep user-facing naming consistent with the repository name: `query-engine` / `query_engine`.
## Commit and PR Hygiene ## Commit and PR Hygiene

View File

@ -23,6 +23,12 @@ This document tracks the current state and next steps for the repository.
- [x] Equality merging with union-find support - [x] Equality merging with union-find support
- [x] REPL, script runner, and local web UI - [x] REPL, script runner, and local web UI
- [x] Provenance-oriented explanation support - [x] Provenance-oriented explanation support
- [x] Relational schema, value, row, and result-set scaffolding
- [x] Predicate-backed catalog inference
- [x] Minimal SQL AST and parser
- [x] Logical plan scaffolding
- [x] Logical-plan execution for the first SQL slice
- [x] Single-table `SELECT-FROM-WHERE` support with positional columns (`c0`, `c1`, ...)
### Near-Term Cleanup ### Near-Term Cleanup
@ -30,27 +36,31 @@ This document tracks the current state and next steps for the repository.
- [ ] Remove remaining stale terminology in comments and help text - [ ] Remove remaining stale terminology in comments and help text
- [ ] Expand examples for the current rule-engine workflow - [ ] Expand examples for the current rule-engine workflow
- [ ] Add rustdoc coverage for the main public types - [ ] Add rustdoc coverage for the main public types
- [ ] Document the current SQL subset and its limits
### Query-Engine Structure ### Query-Engine Structure
- [ ] Introduce a dedicated logical representation module - [x] Introduce a dedicated logical representation module
- [ ] Define clear front-end, planning, and execution boundaries - [x] Define clear front-end, planning, and execution boundaries
- [ ] Add engine-level abstractions that are not chase-specific - [ ] Add engine-level abstractions that are not chase-specific
- [ ] Establish common schema and typed-value representations - [x] Establish common schema and typed-value representations
- [ ] Design a source boundary for future scans and pushdown - [ ] Design a source boundary for future scans and pushdown
### Front End and Planning ### Front End and Planning
- [ ] Add a parser-oriented module beyond the current rule REPL language - [x] Add a parser-oriented module beyond the current rule REPL language
- [ ] Add AST types for a structured query front end - [x] Add AST types for a structured query front end
- [ ] Add logical plan node types - [x] Add logical plan node types
- [ ] Add name resolution and schema validation hooks - [x] Add name resolution and schema validation hooks
- [ ] Add expression typing and nullability tracking - [x] Add expression typing and nullability tracking
- [ ] Add aliases and richer projection expressions
- [ ] Add joins across multiple predicate-backed tables
- [ ] Add a catalog path for stable column naming beyond `c0`, `c1`, ...
### Execution and Optimization ### Execution and Optimization
- [ ] Introduce physical operator abstractions - [ ] Introduce physical operator abstractions
- [ ] Add a planning step from logical operators to executable operators - [x] Add a planning step from logical operators to executable operators
- [ ] Add basic rule-based logical rewrites - [ ] Add basic rule-based logical rewrites
- [ ] Add statistics and cost-model scaffolding - [ ] Add statistics and cost-model scaffolding
- [ ] Add indexing and access-path abstractions - [ ] Add indexing and access-path abstractions
@ -73,7 +83,7 @@ This document tracks the current state and next steps for the repository.
- [ ] Fact import/export - [ ] Fact import/export
- [ ] File-backed data source experiments - [ ] File-backed data source experiments
- [ ] Table-like row or batch abstractions - [x] Table-like row or batch abstractions
- [ ] Stable script/query file format - [ ] Stable script/query file format
- [ ] Integration with external storage or file formats - [ ] Integration with external storage or file formats
@ -91,6 +101,7 @@ This document tracks the current state and next steps for the repository.
- [x] Integration tests - [x] Integration tests
- [x] Property-based tests - [x] Property-based tests
- [x] Regression tests - [x] Regression tests
- [x] Initial SQL pipeline tests
- [ ] Benchmark coverage - [ ] Benchmark coverage
- [ ] Snapshot-style frontend tests - [ ] Snapshot-style frontend tests
- [ ] More planner/executor tests as those layers are added - [ ] More planner/executor tests as those layers are added

529
examples/geolog/README.md Normal file
View File

@ -0,0 +1,529 @@
# Geolog DSL Structure
This directory contains example `.geolog` files that use a richer DSL than the
minimal `.chase` script language in `examples/scripts/`.
This README summarizes the Geolog DSL structure as it appears in the examples in
this directory. It should be read as a practical, example-driven reference, not
as a formal or complete language specification.
## At a Glance
- Top-level declarations are `theory` and `instance`.
- The main kind of type is `Sort`.
- Symbols can denote sorts, constants, functions, predicates, or nested
instances.
- Axioms use sequent-style syntax: `premise |- conclusion`.
- Instance bodies can be plain literals (`= { ... }`) or chase blocks
(`= chase { ... }`).
- Function application is postfix, and qualified names use `/`.
## Informal File Shape
```text
file := { comment | theory_decl | instance_decl }
comment := // ...
theory_decl :=
theory theory_params? Name {
theory_item*
}
instance_decl :=
instance Name : theory_app = {
instance_item*
}
| instance Name : theory_app = chase {
instance_item*
}
```
In practice, a file is usually a sequence of theory declarations followed by one
or more example instances.
## Top-Level Declarations
### `theory`
A theory introduces sorts, symbols, and axioms.
```text
theory Graph {
V : Sort;
Edge : [src: V, tgt: V] -> Prop;
}
```
Theories may also be parameterized.
```text
theory (X : Sort) (Y : Sort) Iso {
fwd : X -> Y;
bwd : Y -> X;
}
theory (N : PetriNet instance) Marking {
token : Sort;
token/of : token -> N/P;
}
```
Observed parameter forms:
- sort parameters: `(X : Sort)`
- instance parameters: `(N : PetriNet instance)`
- dependent instance parameters: `(RP : N ReachabilityProblem instance)`
### `instance`
An instance provides concrete elements and assignments for a theory.
```text
instance Triangle : Graph = {
A : V;
B : V;
C : V;
}
```
Instances may target parameterized theories by writing the arguments before the
theory name.
```text
instance MyMap : MyBase Map = { ... }
instance solution0 : ExampleNet problem0 Solution = { ... }
instance AB_Iso : As/a Bs/b Iso = { ... }
```
## Theory Body Items
The examples show the following declaration forms inside a theory body.
### Sort declarations
```text
V : Sort;
E : Sort;
```
This introduces carrier sorts.
### Constants or distinguished elements
```text
elem : X;
```
This introduces a named element of an existing sort.
### Functions
```text
src : E -> V;
fwd : X -> Y;
token/of : token -> N/P;
```
Names may contain `/`, which is used heavily for structured or qualified names.
### Predicates and relations
Unary predicates:
```text
completed : Item -> Prop;
```
Record-shaped predicates:
```text
Edge : [src: V, tgt: V] -> Prop;
depends : [item: Item, on: Item] -> Prop;
```
### Product-valued functions
Functions may return record-shaped values.
```text
pair_of : A -> [left: B, right: C];
W/src : W -> [firing: F, arc: N/out];
```
### Nested instance slots
Theories can contain fields whose values are themselves instances.
```text
initial_marking : N Marking instance;
trace : N Trace instance;
initial_iso : (trace/input_terminal) (RP/initial_marking/token) Iso instance;
```
### Axioms
Axioms use a sequent-like form.
```text
ax/trans : forall x, y, z : V.
[src: x, tgt: y] Path, [src: y, tgt: z] Path |- [src: x, tgt: z] Path;
```
An axiom declaration has the observed structure:
```text
name : forall binders. premise |- conclusion;
```
## Formula and Term Syntax
The examples use the following building blocks inside axioms and assignments.
### Quantifiers
Universal binders appear after `forall`.
```text
forall x : X, y : Y.
```
Existentials appear directly inside conclusions or disjuncts.
```text
exists r : R. r R/data = [x: a, y: b]
exists w : W. w W/src_firing = f, w W/src_arc = arc
```
### Sequents
The central connective is `|-`, separating premise from conclusion.
```text
premise1, premise2 |- conclusion1, conclusion2;
```
In the examples:
- commas act as conjunctions
- the left side is the premise
- the right side is the conclusion
### Equality
Equality is used for ordinary values and compound values.
```text
ab src = A
r R/data = [x: a, y: b]
x fwd bwd = x
```
### Truth constants
```text
|- true;
|- false;
```
### Disjunction
Disjunction is written as `\/`.
```text
[item: x, on: y] depends |- x blocked \/ y completed;
```
### Predicate atoms
Unary predicate atoms:
```text
x blocked
y completed
```
Record-shaped predicate atoms:
```text
[src: x, tgt: y] Edge
[a: x, f: i] id
```
### Postfix application
Function application is postfix and can be chained.
```text
e src
x id
x fwd bwd
w W/src_arc N/out/tgt
```
This is one of the most distinctive traits of the DSL.
### Qualified paths
The slash `/` is used for qualification and nested access.
Observed examples include:
```text
R/data
N/P
N/out/src
trace/input_terminal
RP/initial_marking/token
ExampleNet/A
```
### Record literals
Records are written with brackets and named fields.
```text
[x: a, y: b]
[firing: f, arc: arc]
```
The examples also show positional shorthand for record arguments when the field
order is known:
```text
[e, e] mul = e;
[cook_dinner, on: buy_groceries] depends;
```
### Field projection
Record-valued terms support field projection.
```text
r R/data .x = a
```
## Instance Body Items
The examples show the following forms inside instance bodies.
### Element declarations
```text
a : V;
tok : token;
ab : T;
```
### Function assignments
```text
ab src = A;
tok token/of = ExampleNet/A;
a1 map = MyBase/b1;
```
### Predicate facts
Unary facts:
```text
buy_groceries completed;
```
Record-shaped facts:
```text
[src: a, tgt: b] Edge;
[item: x, on: y] depends;
```
### Product-valued assignments
```text
a1 pair_of = [left: b1, right: c1];
ot output_terminal/src = [firing: f1, arc: SimpleNet/arc_out];
```
### Nested instance literals
Instance-valued fields can be assigned inline blocks.
```text
initial_marking = {
tok : token;
tok token/of = ExampleNet/A;
};
```
## `= { ... }` vs `= chase { ... }`
The examples consistently use two instance forms.
### Plain instance
```text
instance Triangle : Graph = {
...
}
```
This gives a direct structure with only the explicitly listed elements and
assignments.
### Chase instance
```text
instance Chain : Graph = chase {
...
}
```
This marks an instance whose contents should be extended by the theory axioms.
Examples use this for transitive closure, existential witness generation, and
trace/reachability constructions.
## Observed Design Patterns
### Parameterized theories
Theories can be abstract over:
- a sort, such as `theory (X : Sort) Container`
- an instance, such as `theory (N : PetriNet instance) Marking`
- multiple arguments, such as `theory (N : PetriNet instance) (RP : N ReachabilityProblem instance) Solution`
### Theory application by adjacency
Theory arguments are written before the theory name.
```text
N Marking instance
MyBase Map
ExampleNet problem0 Solution
```
### Nested structure via qualified names
Many examples model dependent structure by naming into earlier declarations.
```text
token/of : token -> N/P;
initial_iso : (trace/input_terminal) (RP/initial_marking/token) Iso instance;
```
## What Seems Outside the File DSL
Some example comments refer to interactive commands such as:
```text
:source
:load
:inspect
:chase
:solve
```
Those commands appear to belong to an external REPL or tool environment rather
than to the `.geolog` file grammar itself.
## Querying
The examples do **not** show a stable, implemented query form inside `.geolog`
files in the same way they show `theory` and `instance` declarations.
What they do show is:
### 1. External interactive commands
Some files suggest that querying or inspection happens through an external tool
or REPL:
```text
:source examples/geolog/transitive_closure.geolog
:inspect Chain
:chase Chain
:solve EmptyModel
```
Based on the example comments, these commands appear to mean roughly:
- `:source` or `:load`: load Geolog definitions from files
- `:inspect`: inspect a declared instance
- `:chase`: materialize or display the closure of a `= chase` instance
- `:solve`: ask a solver to construct a satisfying instance for a theory
These are tool commands, not part of the confirmed `.geolog` declaration syntax.
### 2. A sketched future query form
One file contains a commented-out example of a possible query block:
```text
query can_reach_B_from_A {
? : ExampleNet problem0 Solution instance;
}
```
This suggests an aspirational style where querying is expressed as asking for a
witness inhabiting some instance type. However, in the current examples this is
only a comment, not an observed live construct.
### 3. Current implemented querying elsewhere in the repo
The currently implemented query syntax in this repository belongs to the minimal
frontend language, not to Geolog. That language supports forms such as:
```text
query Ancestor(?X, ?Y)?
explain Ancestor(alice, carol)?
```
So the safest summary is:
- Geolog examples define theories, instances, and chase-oriented structure.
- Querying appears to be external, REPL-driven, or still in design.
- The only clearly implemented query syntax in this repo today is the minimal
`.chase` frontend query language.
## Best Example Files by Feature
- `transitive_closure.geolog`: basic theory, axiom, and `= chase` usage
- `graph.geolog`: plain structural instances
- `monoid.geolog`: postfix application and positional record syntax
- `todo_list.geolog`: unary predicates and disjunction
- `sort_param_simple.geolog`: sort and instance parameters
- `nested_instance_test.geolog`: nested instance-valued fields
- `product_codomain_test.geolog`: record-valued function codomains
- `field_projection_test.geolog`: field projection syntax
- `record_existential_test.geolog`: existential witness creation with records
- `petri_net_showcase.geolog`: large multi-theory composition
- `solver_demo.geolog`: satisfiability-oriented axiom patterns
## Minimal Example
```text
theory Graph {
V : Sort;
Edge : [src: V, tgt: V] -> Prop;
Path : [src: V, tgt: V] -> Prop;
ax/base : forall x, y : V.
[src: x, tgt: y] Edge |- [src: x, tgt: y] Path;
}
instance Chain : Graph = chase {
a : V;
b : V;
[src: a, tgt: b] Edge;
}
```
That small example already shows the core structure of the DSL:
- a `theory`
- sort and predicate declarations
- an axiom written as a sequent
- an `instance`
- a `chase` block that asks for closure under the theory axioms

View 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;
}

View 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];
}

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View 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;
};
}

View 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;
}

View 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; };
// }
// ============================================================

View 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;
};
}

View 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
}

View 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;
}

View 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;
}

View 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;
};
}

View 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];
}

View 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;
}

View 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;
}

View 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];
}

View 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;
}

View 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];
}

View 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];
}

View 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;
}

View 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;
}

View 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];
}

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View File

@ -0,0 +1,13 @@
# Example Scripts
These scripts can be executed with:
```bash
make script SCRIPT=examples/scripts/ancestor.chase
```
Available examples:
- `ancestor.chase`: transitive closure over `Parent/2`
- `employee_departments.chase`: existential rule that creates labeled nulls
- `same_team.chase`: conjunctive query with a self-join

View File

@ -0,0 +1,11 @@
# Derive ancestors from parent relationships.
fact Parent(alice, bob).
fact Parent(bob, carol).
fact Parent(carol, dave).
rule Parent(?X, ?Y) -> Ancestor(?X, ?Y).
rule Ancestor(?X, ?Y), Parent(?Y, ?Z) -> Ancestor(?X, ?Z).
run.
query Ancestor(?X, ?Y)?

View File

@ -0,0 +1,11 @@
# Existential rule example: each employee works in some department.
fact Employee(alice).
fact Employee(bob).
fact Employee(carol).
rule Employee(?X) -> WorksIn(?X, ?Dept).
run.
show facts
query WorksIn(?X, ?Dept)?

View File

@ -0,0 +1,11 @@
# Query people who share a manager.
fact ManagedBy(alice, eve).
fact ManagedBy(bob, eve).
fact ManagedBy(carol, frank).
rule ManagedBy(?X, ?M), ManagedBy(?Y, ?M) -> SameTeam(?X, ?Y).
run.
query SameTeam(?X, ?Y)?
query SameTeam(alice, bob)?

133
src/catalog/mod.rs Normal file
View File

@ -0,0 +1,133 @@
//! Minimal catalog support for mapping predicates to relational schemas.
use std::collections::HashMap;
use std::error::Error;
use std::fmt;
use crate::chase::{Instance, Term};
use crate::relational::{DataType, Field, Schema};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CatalogError {
UnknownTable(String),
InconsistentArity {
table: String,
expected: usize,
found: usize,
},
}
impl fmt::Display for CatalogError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UnknownTable(table) => write!(f, "unknown table `{}`", table),
Self::InconsistentArity {
table,
expected,
found,
} => write!(
f,
"table `{}` has inconsistent arity: expected {}, found {}",
table, expected, found
),
}
}
}
impl Error for CatalogError {}
#[derive(Debug, Clone, Default)]
pub struct PredicateCatalog {
schemas: HashMap<String, Schema>,
}
impl PredicateCatalog {
pub fn new() -> Self {
Self::default()
}
pub fn register_table(&mut self, table: impl Into<String>, schema: Schema) {
self.schemas.insert(table.into(), schema);
}
pub fn schema_for(&self, table: &str) -> Result<&Schema, CatalogError> {
self.schemas
.get(table)
.ok_or_else(|| CatalogError::UnknownTable(table.to_string()))
}
pub fn from_instance(instance: &Instance) -> Result<Self, CatalogError> {
let mut arities = HashMap::new();
let mut nullable_positions: HashMap<String, Vec<bool>> = HashMap::new();
for atom in instance.iter() {
let arity = atom.arity();
match arities.get(&atom.predicate) {
Some(expected) if *expected != arity => {
return Err(CatalogError::InconsistentArity {
table: atom.predicate.clone(),
expected: *expected,
found: arity,
});
}
Some(_) => {}
None => {
arities.insert(atom.predicate.clone(), arity);
nullable_positions.insert(atom.predicate.clone(), vec![false; arity]);
}
}
if let Some(positions) = nullable_positions.get_mut(&atom.predicate) {
for (index, term) in atom.terms.iter().enumerate() {
if matches!(term, Term::Null(_)) {
positions[index] = true;
}
}
}
}
let mut catalog = Self::new();
for (table, arity) in arities {
let nullable = nullable_positions.remove(&table).unwrap_or_default();
let fields = (0..arity)
.map(|index| {
Field::new(
format!("c{}", index),
DataType::Text,
nullable.get(index).copied().unwrap_or(false),
)
})
.collect();
catalog.register_table(table, Schema::new(fields));
}
Ok(catalog)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::chase::Atom;
#[test]
fn infers_predicate_schemas_from_instance() {
let instance: Instance = vec![
Atom::new(
"Parent",
vec![Term::constant("alice"), Term::constant("bob")],
),
Atom::new("Parent", vec![Term::constant("bob"), Term::null(0)]),
]
.into_iter()
.collect();
let catalog = PredicateCatalog::from_instance(&instance).unwrap();
let schema = catalog.schema_for("Parent").unwrap();
assert_eq!(schema.len(), 2);
assert_eq!(schema.fields()[0].name(), "c0");
assert_eq!(schema.fields()[1].name(), "c1");
assert!(schema.fields()[1].nullable());
}
}

112
src/execution/mod.rs Normal file
View File

@ -0,0 +1,112 @@
//! Minimal execution support for the first SQL slice.
use std::error::Error;
use std::fmt;
use crate::chase::{Instance, Term};
use crate::planner::logical::{LogicalExpr, LogicalPlan};
use crate::relational::{ResultSet, Row, Value};
#[derive(Debug)]
pub enum ExecutionError {
UnknownColumn(String),
NonGroundScanTerm,
}
impl fmt::Display for ExecutionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UnknownColumn(column) => write!(f, "unknown column `{}`", column),
Self::NonGroundScanTerm => {
write!(f, "cannot scan non-ground terms into relational rows")
}
}
}
}
impl Error for ExecutionError {}
pub fn execute(plan: &LogicalPlan, instance: &Instance) -> Result<ResultSet, ExecutionError> {
match plan {
LogicalPlan::Scan { table, schema } => {
let mut rows = Vec::new();
for fact in instance.facts_for_predicate(table) {
let values = fact
.terms
.iter()
.map(value_from_term)
.collect::<Result<Vec<_>, _>>()?;
rows.push(Row::new(values));
}
Ok(ResultSet::new(schema.clone(), rows))
}
LogicalPlan::Filter { input, predicate } => {
let result = execute(input, instance)?;
let filtered_rows = result
.rows()
.iter()
.filter(|row| eval_predicate(predicate, row, result.schema()).unwrap_or(false))
.cloned()
.collect();
Ok(ResultSet::new(result.schema().clone(), filtered_rows))
}
LogicalPlan::Project {
input,
expressions,
schema,
} => {
let result = execute(input, instance)?;
let mut rows = Vec::new();
for row in result.rows() {
let values = expressions
.iter()
.map(|expr| eval_expr(&expr.expr, row, result.schema()))
.collect::<Result<Vec<_>, _>>()?;
rows.push(Row::new(values));
}
Ok(ResultSet::new(schema.clone(), rows))
}
}
}
fn eval_predicate(
expr: &LogicalExpr,
row: &Row,
schema: &crate::relational::Schema,
) -> Result<bool, ExecutionError> {
match expr {
LogicalExpr::Eq(left, right) => Ok(eval_expr(left, row, schema)?
.sql_eq(&eval_expr(right, row, schema)?)
.unwrap_or(false)),
_ => Ok(false),
}
}
fn eval_expr(
expr: &LogicalExpr,
row: &Row,
schema: &crate::relational::Schema,
) -> Result<Value, ExecutionError> {
match expr {
LogicalExpr::Column(name) => {
let index = schema
.index_of(name)
.ok_or_else(|| ExecutionError::UnknownColumn(name.clone()))?;
Ok(row.get(index).cloned().unwrap_or(Value::Null))
}
LogicalExpr::Literal(value) => Ok(value.clone()),
LogicalExpr::Eq(left, right) => {
let left = eval_expr(left, row, schema)?;
let right = eval_expr(right, row, schema)?;
Ok(Value::Boolean(left.sql_eq(&right).unwrap_or(false)))
}
}
}
fn value_from_term(term: &Term) -> Result<Value, ExecutionError> {
match term {
Term::Constant(value) => Ok(Value::text(value.clone())),
Term::Null(_) => Ok(Value::Null),
Term::Variable(_) => Err(ExecutionError::NonGroundScanTerm),
}
}

View File

@ -4,8 +4,13 @@
//! lightweight frontends for experimenting with rule-driven query answering. //! lightweight frontends for experimenting with rule-driven query answering.
//! It is not yet a full SQL engine with logical and physical planning layers. //! It is not yet a full SQL engine with logical and physical planning layers.
pub mod catalog;
pub mod chase; pub mod chase;
pub mod execution;
pub mod frontend; pub mod frontend;
pub mod planner;
pub mod relational;
pub mod sql;
// Curated convenience re-exports for the current public crate surface. // Curated convenience re-exports for the current public crate surface.
// Lower-level reasoning and provenance APIs remain under `query_engine::chase`. // Lower-level reasoning and provenance APIs remain under `query_engine::chase`.

47
src/planner/logical.rs Normal file
View File

@ -0,0 +1,47 @@
use crate::relational::{ResultSet, Schema, Value};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LogicalExpr {
Column(String),
Literal(Value),
Eq(Box<LogicalExpr>, Box<LogicalExpr>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NamedExpr {
pub name: String,
pub expr: LogicalExpr,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LogicalPlan {
Scan {
table: String,
schema: Schema,
},
Filter {
input: Box<LogicalPlan>,
predicate: LogicalExpr,
},
Project {
input: Box<LogicalPlan>,
expressions: Vec<NamedExpr>,
schema: Schema,
},
}
impl LogicalPlan {
pub fn output_schema(&self) -> &Schema {
match self {
Self::Scan { schema, .. } => schema,
Self::Filter { input, .. } => input.output_schema(),
Self::Project { schema, .. } => schema,
}
}
}
impl From<ResultSet> for LogicalPlan {
fn from(_: ResultSet) -> Self {
unreachable!("result sets are execution output, not logical plans")
}
}

4
src/planner/mod.rs Normal file
View File

@ -0,0 +1,4 @@
//! Logical planning scaffolding.
pub mod logical;
pub mod sql;

149
src/planner/sql.rs Normal file
View File

@ -0,0 +1,149 @@
use std::error::Error;
use std::fmt;
use crate::catalog::{CatalogError, PredicateCatalog};
use crate::planner::logical::{LogicalExpr, LogicalPlan, NamedExpr};
use crate::relational::{Field, Schema, Value};
use crate::sql::ast::{BinaryOp, Expr, Literal, Select, SelectItem};
#[derive(Debug)]
pub enum PlannerError {
Catalog(CatalogError),
UnknownColumn(String),
UnsupportedProjection,
}
impl fmt::Display for PlannerError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Catalog(err) => write!(f, "catalog error: {}", err),
Self::UnknownColumn(column) => write!(f, "unknown column `{}`", column),
Self::UnsupportedProjection => {
write!(f, "only wildcard and column projections are supported")
}
}
}
}
impl Error for PlannerError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Catalog(err) => Some(err),
Self::UnknownColumn(_) | Self::UnsupportedProjection => None,
}
}
}
impl From<CatalogError> for PlannerError {
fn from(value: CatalogError) -> Self {
Self::Catalog(value)
}
}
pub fn plan_select(
select: &Select,
catalog: &PredicateCatalog,
) -> Result<LogicalPlan, PlannerError> {
let scan_schema = catalog.schema_for(&select.from)?.clone();
let mut plan = LogicalPlan::Scan {
table: select.from.clone(),
schema: scan_schema.clone(),
};
if let Some(selection) = &select.selection {
let predicate = plan_expr(selection, &scan_schema)?;
plan = LogicalPlan::Filter {
input: Box::new(plan),
predicate,
};
}
if is_wildcard_projection(&select.projection) {
return Ok(plan);
}
let mut expressions = Vec::new();
let mut fields = Vec::new();
for item in &select.projection {
match item {
SelectItem::Expr { expr, alias } => match expr {
Expr::Identifier(name) => {
let index = scan_schema
.index_of(name)
.ok_or_else(|| PlannerError::UnknownColumn(name.clone()))?;
let input_field = &scan_schema.fields()[index];
let output_name = alias.clone().unwrap_or_else(|| name.clone());
expressions.push(NamedExpr {
name: output_name.clone(),
expr: LogicalExpr::Column(name.clone()),
});
fields.push(Field::new(
output_name,
input_field.data_type().clone(),
input_field.nullable(),
));
}
_ => return Err(PlannerError::UnsupportedProjection),
},
SelectItem::Wildcard => return Err(PlannerError::UnsupportedProjection),
}
}
Ok(LogicalPlan::Project {
input: Box::new(plan),
expressions,
schema: Schema::new(fields),
})
}
fn is_wildcard_projection(items: &[SelectItem]) -> bool {
matches!(items, [SelectItem::Wildcard])
}
fn plan_expr(expr: &Expr, schema: &Schema) -> Result<LogicalExpr, PlannerError> {
match expr {
Expr::Identifier(name) => {
if schema.index_of(name).is_none() {
return Err(PlannerError::UnknownColumn(name.clone()));
}
Ok(LogicalExpr::Column(name.clone()))
}
Expr::Literal(literal) => Ok(LogicalExpr::Literal(plan_literal(literal))),
Expr::Binary { left, op, right } => match op {
BinaryOp::Eq => Ok(LogicalExpr::Eq(
Box::new(plan_expr(left, schema)?),
Box::new(plan_expr(right, schema)?),
)),
},
}
}
fn plan_literal(literal: &Literal) -> Value {
match literal {
Literal::String(value) => Value::text(value.clone()),
Literal::Null => Value::Null,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::catalog::PredicateCatalog;
use crate::chase::{Atom, Instance, Term};
use crate::sql::parser::parse_select;
#[test]
fn plans_projection_and_filter() {
let instance: Instance = vec![Atom::new(
"Parent",
vec![Term::constant("alice"), Term::constant("bob")],
)]
.into_iter()
.collect();
let catalog = PredicateCatalog::from_instance(&instance).unwrap();
let select = parse_select("SELECT c0 FROM Parent WHERE c1 = 'bob'").unwrap();
let plan = plan_select(&select, &catalog).unwrap();
assert_eq!(plan.output_schema().len(), 1);
}
}

9
src/relational/mod.rs Normal file
View File

@ -0,0 +1,9 @@
//! Relational data model scaffolding for future SQL and planner work.
mod row;
mod schema;
mod value;
pub use row::{ResultSet, Row};
pub use schema::{DataType, Field, Schema};
pub use value::Value;

40
src/relational/row.rs Normal file
View File

@ -0,0 +1,40 @@
use super::{Schema, Value};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Row {
values: Vec<Value>,
}
impl Row {
pub fn new(values: Vec<Value>) -> Self {
Self { values }
}
pub fn values(&self) -> &[Value] {
&self.values
}
pub fn get(&self, index: usize) -> Option<&Value> {
self.values.get(index)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResultSet {
schema: Schema,
rows: Vec<Row>,
}
impl ResultSet {
pub fn new(schema: Schema, rows: Vec<Row>) -> Self {
Self { schema, rows }
}
pub fn schema(&self) -> &Schema {
&self.schema
}
pub fn rows(&self) -> &[Row] {
&self.rows
}
}

92
src/relational/schema.rs Normal file
View File

@ -0,0 +1,92 @@
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DataType {
Text,
Boolean,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Field {
name: String,
data_type: DataType,
nullable: bool,
}
impl Field {
pub fn new(name: impl Into<String>, data_type: DataType, nullable: bool) -> Self {
Self {
name: name.into(),
data_type,
nullable,
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn data_type(&self) -> &DataType {
&self.data_type
}
pub fn nullable(&self) -> bool {
self.nullable
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Schema {
fields: Vec<Field>,
}
impl Schema {
pub fn new(fields: Vec<Field>) -> Self {
Self { fields }
}
pub fn fields(&self) -> &[Field] {
&self.fields
}
pub fn len(&self) -> usize {
self.fields.len()
}
pub fn is_empty(&self) -> bool {
self.fields.is_empty()
}
pub fn index_of(&self, name: &str) -> Option<usize> {
self.fields.iter().position(|field| field.name() == name)
}
}
impl fmt::Display for Schema {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let fields = self
.fields
.iter()
.map(|field| field.name().to_string())
.collect::<Vec<_>>()
.join(", ");
write!(f, "[{}]", fields)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn schema_resolves_columns_by_name() {
let schema = Schema::new(vec![
Field::new("c0", DataType::Text, false),
Field::new("c1", DataType::Text, true),
]);
assert_eq!(schema.index_of("c0"), Some(0));
assert_eq!(schema.index_of("c1"), Some(1));
assert_eq!(schema.index_of("missing"), None);
}
}

37
src/relational/value.rs Normal file
View File

@ -0,0 +1,37 @@
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Value {
Text(String),
Boolean(bool),
Null,
}
impl Value {
pub fn text(value: impl Into<String>) -> Self {
Self::Text(value.into())
}
pub fn is_null(&self) -> bool {
matches!(self, Self::Null)
}
pub fn sql_eq(&self, other: &Self) -> Option<bool> {
match (self, other) {
(Self::Null, _) | (_, Self::Null) => None,
(Self::Text(left), Self::Text(right)) => Some(left == right),
(Self::Boolean(left), Self::Boolean(right)) => Some(left == right),
(Self::Text(_), Self::Boolean(_)) | (Self::Boolean(_), Self::Text(_)) => Some(false),
}
}
}
impl fmt::Display for Value {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Text(value) => write!(f, "{}", value),
Self::Boolean(value) => write!(f, "{}", value),
Self::Null => write!(f, "NULL"),
}
}
}

34
src/sql/ast.rs Normal file
View File

@ -0,0 +1,34 @@
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Select {
pub projection: Vec<SelectItem>,
pub from: String,
pub selection: Option<Expr>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SelectItem {
Wildcard,
Expr { expr: Expr, alias: Option<String> },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Expr {
Identifier(String),
Literal(Literal),
Binary {
left: Box<Expr>,
op: BinaryOp,
right: Box<Expr>,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Literal {
String(String),
Null,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BinaryOp {
Eq,
}

4
src/sql/mod.rs Normal file
View File

@ -0,0 +1,4 @@
//! Minimal SQL front-end scaffolding.
pub mod ast;
pub mod parser;

275
src/sql/parser.rs Normal file
View File

@ -0,0 +1,275 @@
use std::error::Error;
use std::fmt;
use super::ast::{BinaryOp, Expr, Literal, Select, SelectItem};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ParseError {
UnexpectedEnd,
ExpectedToken(&'static str),
ExpectedIdentifier,
UnexpectedToken(String),
UnterminatedString,
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UnexpectedEnd => write!(f, "unexpected end of input"),
Self::ExpectedToken(token) => write!(f, "expected `{}`", token),
Self::ExpectedIdentifier => write!(f, "expected identifier"),
Self::UnexpectedToken(token) => write!(f, "unexpected token `{}`", token),
Self::UnterminatedString => write!(f, "unterminated string literal"),
}
}
}
impl Error for ParseError {}
#[derive(Debug, Clone, PartialEq, Eq)]
enum Token {
Select,
From,
Where,
Null,
Identifier(String),
String(String),
Star,
Comma,
Eq,
}
pub fn parse_select(input: &str) -> Result<Select, ParseError> {
let tokens = tokenize(input)?;
let mut parser = Parser::new(tokens);
parser.parse_select()
}
struct Parser {
tokens: Vec<Token>,
index: usize,
}
impl Parser {
fn new(tokens: Vec<Token>) -> Self {
Self { tokens, index: 0 }
}
fn parse_select(&mut self) -> Result<Select, ParseError> {
self.expect_keyword(Token::Select, "SELECT")?;
let projection = self.parse_projection()?;
self.expect_keyword(Token::From, "FROM")?;
let from = self.expect_identifier()?;
let selection = if self.peek() == Some(&Token::Where) {
self.index += 1;
Some(self.parse_expr()?)
} else {
None
};
if let Some(token) = self.peek() {
return Err(ParseError::UnexpectedToken(render_token(token)));
}
Ok(Select {
projection,
from,
selection,
})
}
fn parse_projection(&mut self) -> Result<Vec<SelectItem>, ParseError> {
let mut items = Vec::new();
loop {
let item = match self.next().ok_or(ParseError::UnexpectedEnd)? {
Token::Star => SelectItem::Wildcard,
Token::Identifier(name) => SelectItem::Expr {
expr: Expr::Identifier(name),
alias: None,
},
other => return Err(ParseError::UnexpectedToken(render_token(&other))),
};
items.push(item);
if self.peek() == Some(&Token::Comma) {
self.index += 1;
continue;
}
break;
}
Ok(items)
}
fn parse_expr(&mut self) -> Result<Expr, ParseError> {
let left = self.parse_operand()?;
match self.next().ok_or(ParseError::UnexpectedEnd)? {
Token::Eq => {
let right = self.parse_operand()?;
Ok(Expr::Binary {
left: Box::new(left),
op: BinaryOp::Eq,
right: Box::new(right),
})
}
other => Err(ParseError::UnexpectedToken(render_token(&other))),
}
}
fn parse_operand(&mut self) -> Result<Expr, ParseError> {
match self.next().ok_or(ParseError::UnexpectedEnd)? {
Token::Identifier(name) => Ok(Expr::Identifier(name)),
Token::String(value) => Ok(Expr::Literal(Literal::String(value))),
Token::Null => Ok(Expr::Literal(Literal::Null)),
other => Err(ParseError::UnexpectedToken(render_token(&other))),
}
}
fn expect_keyword(&mut self, token: Token, label: &'static str) -> Result<(), ParseError> {
let next = self.next().ok_or(ParseError::UnexpectedEnd)?;
if next == token {
Ok(())
} else {
Err(ParseError::ExpectedToken(label))
}
}
fn expect_identifier(&mut self) -> Result<String, ParseError> {
match self.next().ok_or(ParseError::UnexpectedEnd)? {
Token::Identifier(name) => Ok(name),
_ => Err(ParseError::ExpectedIdentifier),
}
}
fn peek(&self) -> Option<&Token> {
self.tokens.get(self.index)
}
fn next(&mut self) -> Option<Token> {
let token = self.tokens.get(self.index).cloned();
if token.is_some() {
self.index += 1;
}
token
}
}
fn tokenize(input: &str) -> Result<Vec<Token>, ParseError> {
let mut chars = input.chars().peekable();
let mut tokens = Vec::new();
while let Some(ch) = chars.peek().copied() {
if ch.is_whitespace() {
chars.next();
continue;
}
match ch {
'*' => {
chars.next();
tokens.push(Token::Star);
}
',' => {
chars.next();
tokens.push(Token::Comma);
}
'=' => {
chars.next();
tokens.push(Token::Eq);
}
'\'' => tokens.push(Token::String(parse_string(&mut chars)?)),
ch if is_identifier_start(ch) => {
let ident = parse_identifier(&mut chars);
let token = match ident.to_ascii_uppercase().as_str() {
"SELECT" => Token::Select,
"FROM" => Token::From,
"WHERE" => Token::Where,
"NULL" => Token::Null,
_ => Token::Identifier(ident),
};
tokens.push(token);
}
other => return Err(ParseError::UnexpectedToken(other.to_string())),
}
}
Ok(tokens)
}
fn parse_string<I>(chars: &mut std::iter::Peekable<I>) -> Result<String, ParseError>
where
I: Iterator<Item = char>,
{
let mut value = String::new();
let quote = chars.next();
if quote != Some('\'') {
return Err(ParseError::ExpectedToken("'"));
}
while let Some(ch) = chars.next() {
if ch == '\'' {
if chars.peek() == Some(&'\'') {
chars.next();
value.push('\'');
continue;
}
return Ok(value);
}
value.push(ch);
}
Err(ParseError::UnterminatedString)
}
fn parse_identifier<I>(chars: &mut std::iter::Peekable<I>) -> String
where
I: Iterator<Item = char>,
{
let mut ident = String::new();
while let Some(ch) = chars.peek().copied() {
if is_identifier_part(ch) {
ident.push(ch);
chars.next();
} else {
break;
}
}
ident
}
fn is_identifier_start(ch: char) -> bool {
ch.is_ascii_alphabetic() || ch == '_'
}
fn is_identifier_part(ch: char) -> bool {
ch.is_ascii_alphanumeric() || ch == '_'
}
fn render_token(token: &Token) -> String {
match token {
Token::Select => "SELECT".to_string(),
Token::From => "FROM".to_string(),
Token::Where => "WHERE".to_string(),
Token::Null => "NULL".to_string(),
Token::Identifier(name) => name.clone(),
Token::String(value) => format!("'{}'", value),
Token::Star => "*".to_string(),
Token::Comma => ",".to_string(),
Token::Eq => "=".to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_select_with_filter() {
let select = parse_select("SELECT c0 FROM Parent WHERE c1 = 'bob'").unwrap();
assert_eq!(select.from, "Parent");
assert_eq!(select.projection.len(), 1);
assert!(select.selection.is_some());
}
}

View File

@ -0,0 +1,67 @@
use query_engine::catalog::PredicateCatalog;
use query_engine::execution::execute;
use query_engine::planner::sql::plan_select;
use query_engine::sql::parser::parse_select;
use query_engine::{Atom, Instance, Term};
fn parent_instance() -> Instance {
vec![
Atom::new(
"Parent",
vec![Term::constant("alice"), Term::constant("bob")],
),
Atom::new(
"Parent",
vec![Term::constant("bob"), Term::constant("carol")],
),
]
.into_iter()
.collect()
}
#[test]
fn select_star_scans_predicate_as_table() {
let instance = parent_instance();
let catalog = PredicateCatalog::from_instance(&instance).unwrap();
let select = parse_select("SELECT * FROM Parent").unwrap();
let plan = plan_select(&select, &catalog).unwrap();
let result = execute(&plan, &instance).unwrap();
assert_eq!(result.schema().len(), 2);
assert_eq!(result.rows().len(), 2);
assert_eq!(result.rows()[0].values().len(), 2);
}
#[test]
fn select_projection_keeps_requested_columns() {
let instance = parent_instance();
let catalog = PredicateCatalog::from_instance(&instance).unwrap();
let select = parse_select("SELECT c0 FROM Parent").unwrap();
let plan = plan_select(&select, &catalog).unwrap();
let result = execute(&plan, &instance).unwrap();
assert_eq!(result.schema().len(), 1);
assert_eq!(result.rows().len(), 2);
let mut values = result
.rows()
.iter()
.map(|row| format!("{}", row.values()[0]))
.collect::<Vec<_>>();
values.sort();
assert_eq!(values, vec!["alice".to_string(), "bob".to_string()]);
}
#[test]
fn select_where_filters_rows() {
let instance = parent_instance();
let catalog = PredicateCatalog::from_instance(&instance).unwrap();
let select = parse_select("SELECT c0 FROM Parent WHERE c1 = 'bob'").unwrap();
let plan = plan_select(&select, &catalog).unwrap();
let result = execute(&plan, &instance).unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(format!("{}", result.rows()[0].values()[0]), "alice");
}