cubical-transport-hott-lean4/Topolei/Selection.lean
Maximus Gorog c2e3ecb3e3
Some checks are pending
Lean Action CI / build (push) Waiting to run
Initial commit: topolei — cubical-transport HoTT in Lean 4 + Rust FFI
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>
2026-04-27 20:40:45 -06:00

291 lines
12 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/-
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