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