Some checks are pending
Lean Action CI / build (push) Waiting to run
Implements the cells-spec vision: a computation space that preserves auditability, correctness, interactivity. Phase 1 (Lean kernel + naga-IR Rust backend) is closed; foundation hypothesis stack (Selection H1+H2, Subobject H3, Trace H5, Obs.Ctx C2, Cubical.Trace) landed. Highlights: - Cubical-HoTT syntax + value/eval/readback in Lean - naga-IR pipeline (no GLSL string crosses FFI; 17/17 probes pass) - Honesty audit: every non-transport (sealed cells, vertex shader, Y-flip, presentation conventions) is documented as such - Polymorphic Trace α as free monoid; Cubical.Trace gives CTerm → Trace CTerm by structural fold (homomorphism = definition) - Selection as Huet zipper; Subobject as Boolean algebra over WCell - All theorems proven; the proof IS the implementation See STATUS.md for the resume guide. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
291 lines
12 KiB
Text
291 lines
12 KiB
Text
/-
|
||
Topolei.Selection
|
||
=================
|
||
Foundational selection abstraction — hypothesis stack H1 + H2.
|
||
|
||
## What's a hypothesis here
|
||
|
||
The user's design intent:
|
||
|
||
"Selection has to live in cell space as a cell abstraction.
|
||
We want a stack of options... navigate a selection path.
|
||
Observation returns results we use the observation on again.
|
||
The space [we navigate] must match the space we are in."
|
||
|
||
My natural-transformation reading: a Selection is a *focus inside
|
||
a cell-tree with history*. The tree structure is the cell space;
|
||
the focus is "where we currently are"; the history is "how we got
|
||
here". Mathematically: a **zipper** (Huet 1997).
|
||
|
||
## The two hypotheses this file commits to
|
||
|
||
**H1 — Selection is a focused sub-cell with breadcrumb history.**
|
||
A `Selection` carries a `focus : WCell` and a `crumbs : List Crumb`
|
||
trail. `read` returns the focus. Round-trip: `descend i` then
|
||
`ascend` is the identity (when descend succeeded), so the trail
|
||
faithfully encodes the path back to the root.
|
||
|
||
**H2 — Path application is associative.** Building selections by
|
||
composing `Move`s is associative: `applyPath s (p₁ ++ p₂) =
|
||
(applyPath s p₁) >>= (·.applyPath p₂)`. Identity move-list is
|
||
the unit. Together these say `(Selection, applyPath, [], ++)` is
|
||
a partial monoid action by the move-monoid.
|
||
|
||
## What this is NOT yet
|
||
|
||
- H3 (Boolean algebra ∩, ∪, ¬ over `σ`-predicates) — selections as
|
||
sub-objects beyond focus;
|
||
- H4 (horizontal lifts) — selections that follow the connection on
|
||
the fibration;
|
||
- H5 (trace map) — inverse projection from rendered elements back
|
||
to source morphisms.
|
||
|
||
Each of those is a separate file once H1+H2 are stable.
|
||
|
||
## Why no Rust
|
||
|
||
This is structural reasoning about a Lean inductive. The
|
||
zipper's correctness is decided by Lean's kernel from the
|
||
definitions; nothing here needs to execute on a GPU. When
|
||
selections eventually drive the renderer, the existing
|
||
`compileEMLPath` pipeline consumes the *focus* of a selection
|
||
(a single cell) and runs unchanged.
|
||
-/
|
||
|
||
namespace Topolei.Selection
|
||
|
||
-- ── Workspace cell ─────────────────────────────────────────────────────────
|
||
--
|
||
-- A minimal labeled tree. Stand-in for the broader cell calculus —
|
||
-- when the cubical Cell type stabilises, concrete cells (EMLPath,
|
||
-- CTerm, …) project into this structure for selection purposes via
|
||
-- a `toWCell` function we'll add per-cell-type. The selection
|
||
-- algebra here doesn't care about the cell's interior, only its
|
||
-- tree shape.
|
||
|
||
inductive WCell where
|
||
| mk : String → List WCell → WCell
|
||
deriving Inhabited
|
||
|
||
namespace WCell
|
||
|
||
def data : WCell → String
|
||
| .mk d _ => d
|
||
|
||
def children : WCell → List WCell
|
||
| .mk _ c => c
|
||
|
||
@[simp] theorem mk_data (d : String) (cs : List WCell) :
|
||
(WCell.mk d cs).data = d := rfl
|
||
|
||
@[simp] theorem mk_children (d : String) (cs : List WCell) :
|
||
(WCell.mk d cs).children = cs := rfl
|
||
|
||
@[simp] theorem eta : ∀ c : WCell, WCell.mk c.data c.children = c
|
||
| .mk _ _ => rfl
|
||
|
||
end WCell
|
||
|
||
-- ── Crumb (one step of breadcrumb trail) ──────────────────────────────────
|
||
--
|
||
-- When we descend from a parent into its i-th child, we leave a
|
||
-- breadcrumb that records: the parent's data + the index we took +
|
||
-- the parent's full children list. Reconstructing the parent from
|
||
-- the (possibly modified) child = `set`-replacing the i-th slot
|
||
-- with the focus.
|
||
--
|
||
-- Storing the whole `parentChildren` list is more memory than
|
||
-- splitting into (left, right) but makes reconstruction equationally
|
||
-- clean and the round-trip proof a one-liner via `List.set_get?_eq`.
|
||
|
||
structure Crumb where
|
||
parentData : String
|
||
index : Nat
|
||
parentChildren : List WCell
|
||
deriving Inhabited
|
||
|
||
namespace Crumb
|
||
|
||
/-- Reconstruct the parent cell from a focused child + this breadcrumb. -/
|
||
def reconstruct (cr : Crumb) (child : WCell) : WCell :=
|
||
WCell.mk cr.parentData (cr.parentChildren.set cr.index child)
|
||
|
||
end Crumb
|
||
|
||
-- ── Selection ─────────────────────────────────────────────────────────────
|
||
|
||
/-- A Selection: a focused cell + a breadcrumb trail back to the root.
|
||
Trail's head is the immediate parent (most recent crumb); trail's
|
||
last element is the root's parent (none — i.e., focus is root —
|
||
when the trail is empty).
|
||
|
||
Invariant we will *never* state in the type but is true by
|
||
construction: `crumbs.head?.parentChildren[crumbs.head?.index]?` is
|
||
the position the focus was descended into. We don't carry this
|
||
invariant in the type because it makes manipulation awkward; the
|
||
`descend_ascend` theorem below proves it implicitly. -/
|
||
structure Selection where
|
||
focus : WCell
|
||
crumbs : List Crumb
|
||
deriving Inhabited
|
||
|
||
namespace Selection
|
||
|
||
/-- The trivial selection at the root of `c`: focus = c, empty trail. -/
|
||
def atRoot (c : WCell) : Selection := { focus := c, crumbs := [] }
|
||
|
||
/-- Read the currently-focused cell. -/
|
||
def read (s : Selection) : WCell := s.focus
|
||
|
||
-- ── H1.1: round-trip on `atRoot` ──────────────────────────────────────────
|
||
|
||
/-- Reading the at-root selection of `c` returns `c`. This is the
|
||
most basic round-trip: the trivial selection of a cell faithfully
|
||
represents the cell. -/
|
||
@[simp] theorem atRoot_read (c : WCell) : (atRoot c).read = c := rfl
|
||
|
||
-- ── Navigation: descend / ascend ──────────────────────────────────────────
|
||
|
||
/-- Descend into the i-th child of the focus. Returns `none` if `i`
|
||
is out of range — the user can then handle the failure
|
||
however the calling layer prefers. -/
|
||
def descend (s : Selection) (i : Nat) : Option Selection :=
|
||
match s.focus.children[i]? with
|
||
| none => none
|
||
| some child =>
|
||
some { focus := child
|
||
crumbs := { parentData := s.focus.data
|
||
index := i
|
||
parentChildren := s.focus.children } :: s.crumbs }
|
||
|
||
/-- Ascend back to the parent. Returns `none` if the focus IS the
|
||
root (empty crumbs). -/
|
||
def ascend (s : Selection) : Option Selection :=
|
||
match s.crumbs with
|
||
| [] => none
|
||
| cr :: rest => some { focus := cr.reconstruct s.focus, crumbs := rest }
|
||
|
||
-- ── H1.2: descend-then-ascend = identity ──────────────────────────────────
|
||
|
||
/-- The key list-set lemma we need for `descend_ascend`: if `l[i]? =
|
||
some x`, then `l.set i x = l`. Replacing an element at a position
|
||
with the same element it already had is a no-op. Proved by
|
||
induction; standalone because the exact name in the Lean stdlib
|
||
has churned across versions. -/
|
||
private theorem List.set_self_of_getElem? {α : Type _}
|
||
: ∀ {l : List α} {i : Nat} {x : α}, l[i]? = some x → l.set i x = l
|
||
| [], _, _, h => by simp at h
|
||
| _ :: _, 0, _, h => by simp at h; subst h; rfl
|
||
| _ :: tl, i+1, _, h => by
|
||
simp [List.set]
|
||
exact List.set_self_of_getElem? (l := tl) (by simpa using h)
|
||
|
||
/-- **H1.2 — descend-then-ascend round-trip.** If descending into
|
||
child `i` succeeded, ascending from the result returns the
|
||
original selection.
|
||
|
||
The proof: `descend` produces a selection whose focus is the
|
||
i-th child and whose top crumb stores the parent's children
|
||
list. `ascend` reconstructs the parent by `set`-replacing
|
||
position `i` with the focus. Since the focus IS the i-th
|
||
child (it's what we descended into), `set i child` is a no-op
|
||
on `parentChildren`, giving back the original parent. -/
|
||
theorem descend_ascend (s : Selection) (i : Nat) (s' : Selection)
|
||
(h : s.descend i = some s') : s'.ascend = some s := by
|
||
-- Unpack `descend` to extract the child + the structure of s'.
|
||
unfold descend at h
|
||
match hChild : s.focus.children[i]? with
|
||
| none =>
|
||
rw [hChild] at h
|
||
contradiction
|
||
| some child =>
|
||
rw [hChild] at h
|
||
-- Now h : some {focus := child, crumbs := newCrumb :: s.crumbs} = some s'
|
||
injection h with h'
|
||
subst h'
|
||
-- Goal: ascend (the_descended_selection) = some s
|
||
simp only [ascend, Crumb.reconstruct]
|
||
-- The goal reduces to:
|
||
-- { focus := WCell.mk s.focus.data (s.focus.children.set i child),
|
||
-- crumbs := s.crumbs } = s
|
||
-- which follows from `set i child = s.focus.children` (since
|
||
-- `child = s.focus.children[i]`) plus WCell.eta on s.focus.
|
||
rw [List.set_self_of_getElem? hChild]
|
||
simp [WCell.eta]
|
||
|
||
-- ── Composition: Move + Path + applyPath ──────────────────────────────────
|
||
|
||
/-- A single navigation step. -/
|
||
inductive Move where
|
||
| descend : Nat → Move
|
||
| ascend : Move
|
||
deriving Repr, Inhabited
|
||
|
||
/-- Apply a single move. `descend i` may fail if `i` is out of
|
||
range; `ascend` may fail if focus is root. -/
|
||
def applyMove (s : Selection) : Move → Option Selection
|
||
| .descend i => s.descend i
|
||
| .ascend => s.ascend
|
||
|
||
/-- Apply a sequence of moves left-to-right. Threads `Option`
|
||
through the fold — any failed move aborts the whole path. -/
|
||
def applyPath : Selection → List Move → Option Selection
|
||
| s, [] => some s
|
||
| s, m :: ms => (applyMove s m).bind (·.applyPath ms)
|
||
|
||
-- ── H2.1: identity ────────────────────────────────────────────────────────
|
||
|
||
/-- The empty path is the identity. -/
|
||
@[simp] theorem applyPath_nil (s : Selection) : applyPath s [] = some s := rfl
|
||
|
||
-- ── H2.2: associativity ───────────────────────────────────────────────────
|
||
|
||
/-- **H2 — applying a concatenated path = applying the parts in
|
||
order.** This is the partial-monoid associativity for the
|
||
selection action. The proof is induction on the first list,
|
||
pushing the bind through. -/
|
||
theorem applyPath_append (s : Selection) (p₁ p₂ : List Move) :
|
||
applyPath s (p₁ ++ p₂) = (applyPath s p₁).bind (·.applyPath p₂) := by
|
||
induction p₁ generalizing s with
|
||
| nil => simp [applyPath]
|
||
| cons m ms ih =>
|
||
simp only [List.cons_append, applyPath]
|
||
cases applyMove s m with
|
||
| none => simp
|
||
| some s' => simp [ih]
|
||
|
||
-- ── Concrete demo (operational sanity check via `#eval`) ──────────────────
|
||
|
||
/-- A small example tree:
|
||
root [ inner [ leaf-A, leaf-B ], leaf-C ]
|
||
Used by `#eval`s below. We use `#eval`-style introspection
|
||
rather than `decide`-backed example proofs because the
|
||
Decidable instance for `Selection` doesn't reduce in the
|
||
elaborator (it's defined on a recursive `WCell.mk` and the
|
||
elaborator gets stuck on `sorry`-mocking unwound recursion).
|
||
The abstract theorems above are what actually verify the
|
||
abstraction; these `#eval`s just let a human eyeball that
|
||
operations behave as expected. -/
|
||
def demoTree : WCell :=
|
||
WCell.mk "root"
|
||
[ WCell.mk "inner"
|
||
[ WCell.mk "leaf-A" [], WCell.mk "leaf-B" [] ]
|
||
, WCell.mk "leaf-C" []
|
||
]
|
||
|
||
/-- Descend twice into the "inner" child then "leaf-A". The focused
|
||
cell's data should be `"leaf-A"`. -/
|
||
def demoFocus : Option String :=
|
||
((applyPath (atRoot demoTree) [.descend 0, .descend 0]).map (·.read.data))
|
||
|
||
#eval demoFocus -- expected: some "leaf-A"
|
||
|
||
/-- Round-trip check: descend into the inner cell, ascend, focus's
|
||
data should be `"root"` (the original root). -/
|
||
def demoRoundTrip : Option String :=
|
||
(((atRoot demoTree).descend 0).bind (·.ascend)).map (·.read.data)
|
||
|
||
#eval demoRoundTrip -- expected: some "root"
|
||
|
||
end Selection
|
||
end Topolei.Selection
|