|
| 1 | +# SPDX-License-Identifier: PMPL-1.0-or-later |
| 2 | +# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk> |
| 3 | + |
| 4 | +# Lithoglyph/GQL System Specification |
| 5 | + |
| 6 | +## Overview |
| 7 | + |
| 8 | +Lithoglyph is a graph query language database built on a polyglot stack: |
| 9 | +Forth (storage), Factor (query runtime), Zig (bridge), Elixir (control |
| 10 | +plane), and Lean 4 (normalizer). |
| 11 | + |
| 12 | +## Memory Model |
| 13 | + |
| 14 | +### Forth Storage Layer |
| 15 | + |
| 16 | +The storage engine uses fixed-size blocks (4 KiB default, configurable at |
| 17 | +init). All writes are append-only: mutations produce new journal entries |
| 18 | +rather than overwriting existing blocks. Block addresses are immutable |
| 19 | +once allocated. Freed blocks are reclaimed only during explicit compaction. |
| 20 | + |
| 21 | +### Zig Block Allocator |
| 22 | + |
| 23 | +Zig manages the physical block pool via a slab allocator. Each slab |
| 24 | +corresponds to a memory-mapped region of the journal file. The allocator |
| 25 | +maintains a free-list of reclaimed blocks after compaction. A write-ahead |
| 26 | +log (WAL) in Zig ensures crash recovery: every block write is first |
| 27 | +recorded in the WAL, then flushed to the journal. WAL entries are |
| 28 | +checksummed (xxHash64) and trimmed after journal sync. |
| 29 | + |
| 30 | +### Factor Query Execution |
| 31 | + |
| 32 | +Factor executes queries using stack-based evaluation with no heap garbage |
| 33 | +collector. Intermediate query results live on the data stack or in |
| 34 | +explicitly allocated retain stacks. Large result sets spill to Zig-managed |
| 35 | +temporary blocks rather than growing unbounded in-process memory. Stack |
| 36 | +frames are released deterministically when a query completes. |
| 37 | + |
| 38 | +### Lean 4 Normalizer |
| 39 | + |
| 40 | +The Lean 4 normalizer operates on an in-memory graph representation |
| 41 | +received via serialized messages from the Elixir control plane. It |
| 42 | +performs graph rewriting in pure Lean (no IO monad) and returns the |
| 43 | +normalized form. Memory is managed by Lean's reference-counted runtime; |
| 44 | +the normalizer process is short-lived per invocation. |
| 45 | + |
| 46 | +## Concurrency Model |
| 47 | + |
| 48 | +### Elixir/OTP Session Management |
| 49 | + |
| 50 | +Each client connection is supervised by an OTP GenServer. The supervisor |
| 51 | +tree uses `one_for_one` strategy: a crashed session does not affect |
| 52 | +others. Sessions hold no shared mutable state; all coordination happens |
| 53 | +through message passing to the storage coordinator (a singleton GenServer |
| 54 | +that serializes journal writes). |
| 55 | + |
| 56 | +### Factor Cooperative Multitasking |
| 57 | + |
| 58 | +Query execution within Factor uses cooperative multitasking via explicit |
| 59 | +yield points. Long-running traversals yield after processing each batch |
| 60 | +of edges (default batch size: 1024). This prevents any single query from |
| 61 | +starving the runtime. Factor threads are M:1 (multiplexed onto the OTP |
| 62 | +scheduler thread that owns the Factor NIF). |
| 63 | + |
| 64 | +### Write Serialization |
| 65 | + |
| 66 | +All journal mutations are serialized through the Elixir storage |
| 67 | +coordinator. Reads are lock-free against the append-only journal: |
| 68 | +readers see a consistent snapshot defined by the journal offset at |
| 69 | +query start (snapshot isolation via offset bookmarking). |
| 70 | + |
| 71 | +## Effect System |
| 72 | + |
| 73 | +### Provenance Effect (Mandatory) |
| 74 | + |
| 75 | +Every mutation carries a provenance record as a mandatory effect. The |
| 76 | +provenance includes: actor identity (session ID + authenticated principal), |
| 77 | +timestamp (monotonic + wall-clock), rationale (client-supplied text or |
| 78 | +`"implicit"` default), and causal predecessor (previous journal offset |
| 79 | +for the affected subgraph). Provenance records are stored inline in the |
| 80 | +journal block, not in a side table. Queries may filter or project on |
| 81 | +provenance fields. |
| 82 | + |
| 83 | +### Reversibility Effect |
| 84 | + |
| 85 | +Every mutation also stores its inverse operation in the journal. For |
| 86 | +node insertion, the inverse is a tombstone marker. For edge creation, |
| 87 | +the inverse is edge removal. For property updates, the inverse stores |
| 88 | +the prior value. Reversal is triggered by issuing a `REVERT` command |
| 89 | +referencing a journal offset range. The reversal itself produces new |
| 90 | +journal entries (with their own provenance), preserving the append-only |
| 91 | +invariant. |
| 92 | + |
| 93 | +### Effect Composition |
| 94 | + |
| 95 | +Provenance and reversibility compose: reverting a mutation produces a |
| 96 | +new provenance record attributing the revert to the requesting actor, |
| 97 | +and the revert entry itself is reversible (enabling undo-of-undo). |
| 98 | + |
| 99 | +## Module System |
| 100 | + |
| 101 | +### Forth Word Definitions |
| 102 | + |
| 103 | +Storage operations are defined as Forth words in `.4th` files loaded at |
| 104 | +engine startup. Custom words extend the storage vocabulary (e.g., |
| 105 | +`BLOCK-ALLOC`, `WAL-APPEND`, `JOURNAL-SYNC`). Words are organized into |
| 106 | +wordlists by functional area. |
| 107 | + |
| 108 | +### Factor Vocabularies |
| 109 | + |
| 110 | +Query operations are organized as Factor vocabularies: `gql.parser`, |
| 111 | +`gql.planner`, `gql.executor`, `gql.results`. Each vocabulary declares |
| 112 | +its imports explicitly. Vocabularies are loaded on demand by the runtime. |
| 113 | + |
| 114 | +### Elixir OTP Applications |
| 115 | + |
| 116 | +The control plane is structured as an OTP umbrella application with |
| 117 | +child apps: `lithoglyph_session`, `lithoglyph_storage`, |
| 118 | +`lithoglyph_normalizer` (Lean 4 port wrapper), and `lithoglyph_api` |
| 119 | +(external interface). Dependencies between apps are declared in |
| 120 | +`mix.exs` and enforced by the release build. |
| 121 | + |
| 122 | +### Cross-Language Boundaries |
| 123 | + |
| 124 | +Zig serves as the bridge between Forth, Factor, and Elixir via NIF |
| 125 | +bindings. The Zig bridge exposes a C ABI consumed by Erlang NIFs and |
| 126 | +Factor FFI. Message formats crossing the bridge are length-prefixed |
| 127 | +binary with a 1-byte tag discriminator. |
0 commit comments