diff --git a/CubicalBench.lean b/CubicalBench.lean index bf55e16..f83160d 100644 --- a/CubicalBench.lean +++ b/CubicalBench.lean @@ -1,5 +1,5 @@ -import Topolei.Cubical.Readback -import Topolei.Cubical.FFI +import CubicalTransport.Readback +import CubicalTransport.FFI /-! CubicalBench.lean — Phase D.2 performance benchmarks. diff --git a/CubicalTest.lean b/CubicalTest.lean index 16e019e..a463b14 100644 --- a/CubicalTest.lean +++ b/CubicalTest.lean @@ -1,10 +1,10 @@ -import Topolei.Cubical.FFITest -import Topolei.Cubical.PropertyTest +import CubicalTransport.FFITest +import CubicalTransport.PropertyTest def main : IO UInt32 := do - let smokeFails ← TopoleiCubicalFFITest.runSmokeTests + let smokeFails ← CubicalTransportFFITest.runSmokeTests IO.println "" - let propFails ← TopoleiCubicalPropertyTest.runProperties + let propFails ← CubicalTransportPropertyTest.runProperties let total := smokeFails + propFails IO.println "" if total > 0 then diff --git a/CubicalTransport.lean b/CubicalTransport.lean new file mode 100644 index 0000000..829fad9 --- /dev/null +++ b/CubicalTransport.lean @@ -0,0 +1,22 @@ +import CubicalTransport.Interval +import CubicalTransport.Face +import CubicalTransport.Syntax +import CubicalTransport.Subst +import CubicalTransport.DimLine +import CubicalTransport.Typing +import CubicalTransport.Equiv +import CubicalTransport.Glue +import CubicalTransport.Value +import CubicalTransport.Transport +import CubicalTransport.Line +import CubicalTransport.Eval +import CubicalTransport.EvalTest +import CubicalTransport.Readback +import CubicalTransport.FFI +import CubicalTransport.FFITest +import CubicalTransport.ValueTyping +import CubicalTransport.TransportLaws +import CubicalTransport.System +import CubicalTransport.CompLaws +import CubicalTransport.Soundness +import CubicalTransport.PropertyTest diff --git a/Topolei/Cubical/CompLaws.lean b/CubicalTransport/CompLaws.lean similarity index 95% rename from Topolei/Cubical/CompLaws.lean rename to CubicalTransport/CompLaws.lean index b503536..e0895dc 100644 --- a/Topolei/Cubical/CompLaws.lean +++ b/CubicalTransport/CompLaws.lean @@ -23,9 +23,9 @@ preservation lemma on `eval`/`readback` (Stream B #2a). -/ -import Topolei.Cubical.System -import Topolei.Cubical.TransportLaws -import Topolei.Cubical.ValueTyping +import CubicalTransport.System +import CubicalTransport.TransportLaws +import CubicalTransport.ValueTyping -- ── Subject reduction for composition ──────────────────────────────────────── diff --git a/Topolei/Cubical/DimLine.lean b/CubicalTransport/DimLine.lean similarity index 99% rename from Topolei/Cubical/DimLine.lean rename to CubicalTransport/DimLine.lean index 41de917..8001eb1 100644 --- a/Topolei/Cubical/DimLine.lean +++ b/CubicalTransport/DimLine.lean @@ -18,7 +18,7 @@ value at i selects the correct endpoint type. -/ -import Topolei.Cubical.Subst +import CubicalTransport.Subst -- ── DimLine ─────────────────────────────────────────────────────────────────── diff --git a/Topolei/Cubical/Equiv.lean b/CubicalTransport/Equiv.lean similarity index 99% rename from Topolei/Cubical/Equiv.lean rename to CubicalTransport/Equiv.lean index b432a86..8df91f8 100644 --- a/Topolei/Cubical/Equiv.lean +++ b/CubicalTransport/Equiv.lean @@ -29,7 +29,7 @@ "Priority order" item 3). -/ -import Topolei.Cubical.Typing +import CubicalTransport.Typing -- ── Equivalence data ───────────────────────────────────────────────────────── diff --git a/Topolei/Cubical/Eval.lean b/CubicalTransport/Eval.lean similarity index 99% rename from Topolei/Cubical/Eval.lean rename to CubicalTransport/Eval.lean index e8f46fe..a0ae439 100644 --- a/Topolei/Cubical/Eval.lean +++ b/CubicalTransport/Eval.lean @@ -22,8 +22,8 @@ metric. For now, `partial def` is the honest choice. -/ -import Topolei.Cubical.Value -import Topolei.Cubical.Transport +import CubicalTransport.Value +import CubicalTransport.Transport -- ── Rust FFI declarations (Phase C.2) ────────────────────────────────────── -- `@[extern "topolei_cubical_*"] opaque *Rust ...` declares the Rust diff --git a/Topolei/Cubical/EvalTest.lean b/CubicalTransport/EvalTest.lean similarity index 99% rename from Topolei/Cubical/EvalTest.lean rename to CubicalTransport/EvalTest.lean index fae1c26..32c9397 100644 --- a/Topolei/Cubical/EvalTest.lean +++ b/CubicalTransport/EvalTest.lean @@ -16,7 +16,7 @@ · transport / composition terms produce the expected neutrals. -/ -import Topolei.Cubical.Eval +import CubicalTransport.Eval -- ── Free variable ─────────────────────────────────────────────────────────── diff --git a/Topolei/Cubical/FFI.lean b/CubicalTransport/FFI.lean similarity index 98% rename from Topolei/Cubical/FFI.lean rename to CubicalTransport/FFI.lean index 47782b3..f81ebc7 100644 --- a/Topolei/Cubical/FFI.lean +++ b/CubicalTransport/FFI.lean @@ -60,4 +60,4 @@ - `KERNEL_BOUNDARY.md` — what this delivers vs. what requires kernel work. -/ -import Topolei.Cubical.Readback +import CubicalTransport.Readback diff --git a/Topolei/Cubical/FFITest.lean b/CubicalTransport/FFITest.lean similarity index 97% rename from Topolei/Cubical/FFITest.lean rename to CubicalTransport/FFITest.lean index 2e3aee0..75f31d3 100644 --- a/Topolei/Cubical/FFITest.lean +++ b/CubicalTransport/FFITest.lean @@ -14,14 +14,14 @@ them inside a compiled binary where Rust IS linked. Invoke from a compiled executable. `Main.lean` can optionally - route to `TopoleiCubicalFFITest.runSmokeTests` when passed + route to `CubicalTransportFFITest.runSmokeTests` when passed `--cubical-test`. Or a dedicated test exe target. -/ -import Topolei.Cubical.Readback -import Topolei.Cubical.FFI +import CubicalTransport.Readback +import CubicalTransport.FFI -namespace TopoleiCubicalFFITest +namespace CubicalTransportFFITest -- ── Summarisers ──────────────────────────────────────────────────────────── @@ -151,4 +151,4 @@ def runSmokeTests : IO UInt32 := do IO.println s!"── {tests.length - fails.toNat} / {tests.length} passed ──" return fails -end TopoleiCubicalFFITest +end CubicalTransportFFITest diff --git a/Topolei/Cubical/Face.lean b/CubicalTransport/Face.lean similarity index 99% rename from Topolei/Cubical/Face.lean rename to CubicalTransport/Face.lean index 4fc8bb7..829eca5 100644 --- a/Topolei/Cubical/Face.lean +++ b/CubicalTransport/Face.lean @@ -11,7 +11,7 @@ The key invariant: (i=0) and (i=1) are mutually exclusive and jointly exhaustive. -/ -import Topolei.Cubical.Interval +import CubicalTransport.Interval -- ── Face formulas ───────────────────────────────────────────────────────────── diff --git a/Topolei/Cubical/Glue.lean b/CubicalTransport/Glue.lean similarity index 99% rename from Topolei/Cubical/Glue.lean rename to CubicalTransport/Glue.lean index 9ef811f..95d3dbf 100644 --- a/Topolei/Cubical/Glue.lean +++ b/CubicalTransport/Glue.lean @@ -53,8 +53,8 @@ CType-function level (`DimExpr → CType`) instead. -/ -import Topolei.Cubical.Eval -import Topolei.Cubical.Equiv +import CubicalTransport.Eval +import CubicalTransport.Equiv -- ── Ergonomic glue-type construction from EquivData ───────────────────────── diff --git a/Topolei/Cubical/Interval.lean b/CubicalTransport/Interval.lean similarity index 100% rename from Topolei/Cubical/Interval.lean rename to CubicalTransport/Interval.lean diff --git a/Topolei/Cubical/Line.lean b/CubicalTransport/Line.lean similarity index 99% rename from Topolei/Cubical/Line.lean rename to CubicalTransport/Line.lean index 6ec7adf..873e044 100644 --- a/Topolei/Cubical/Line.lean +++ b/CubicalTransport/Line.lean @@ -45,7 +45,7 @@ *where* the obligation is discharged. -/ -import Topolei.Cubical.Transport +import CubicalTransport.Transport -- ── DimLine.inv ────────────────────────────────────────────────────────────── -- Reversed line via DimExpr substitution. `.inv (.var i)` flips the diff --git a/Topolei/Cubical/PropertyTest.lean b/CubicalTransport/PropertyTest.lean similarity index 98% rename from Topolei/Cubical/PropertyTest.lean rename to CubicalTransport/PropertyTest.lean index 33ad4ff..7ce5d44 100644 --- a/Topolei/Cubical/PropertyTest.lean +++ b/CubicalTransport/PropertyTest.lean @@ -16,10 +16,10 @@ `cubical-test` exe; see `CubicalTest.lean` for wiring. -/ -import Topolei.Cubical.Readback -import Topolei.Cubical.FFI +import CubicalTransport.Readback +import CubicalTransport.FFI -namespace TopoleiCubicalPropertyTest +namespace CubicalTransportPropertyTest -- ── Summarisers (reuse from FFITest but private to this module) ──────────── @@ -246,4 +246,4 @@ def runProperties : IO UInt32 := do IO.println s!"── {totalRun - totalFails.toNat} / {totalRun} properties passed ──" return totalFails -end TopoleiCubicalPropertyTest +end CubicalTransportPropertyTest diff --git a/Topolei/Cubical/Readback.lean b/CubicalTransport/Readback.lean similarity index 99% rename from Topolei/Cubical/Readback.lean rename to CubicalTransport/Readback.lean index 950dda0..a9254ce 100644 --- a/Topolei/Cubical/Readback.lean +++ b/CubicalTransport/Readback.lean @@ -54,7 +54,7 @@ env shadowing provides capture-avoidance. -/ -import Topolei.Cubical.Eval +import CubicalTransport.Eval -- ── Inhabited instance for CTerm ──────────────────────────────────────────── -- Needed for `partial def` elaboration: Lean's partial-fixpoint compilation diff --git a/Topolei/Cubical/Soundness.lean b/CubicalTransport/Soundness.lean similarity index 99% rename from Topolei/Cubical/Soundness.lean rename to CubicalTransport/Soundness.lean index 77e46f6..2189d2b 100644 --- a/Topolei/Cubical/Soundness.lean +++ b/CubicalTransport/Soundness.lean @@ -49,9 +49,9 @@ cells-layer work; stated briefly below for completeness. -/ -import Topolei.Cubical.TransportLaws -import Topolei.Cubical.CompLaws -import Topolei.Cubical.Glue +import CubicalTransport.TransportLaws +import CubicalTransport.CompLaws +import CubicalTransport.Glue namespace Soundness diff --git a/Topolei/Cubical/Subst.lean b/CubicalTransport/Subst.lean similarity index 99% rename from Topolei/Cubical/Subst.lean rename to CubicalTransport/Subst.lean index 18cabd1..25d9cdb 100644 --- a/Topolei/Cubical/Subst.lean +++ b/CubicalTransport/Subst.lean @@ -25,7 +25,7 @@ it requires DimExpr.subst commutativity, which needs its own treatment. -/ -import Topolei.Cubical.Syntax +import CubicalTransport.Syntax -- ── CTerm.substDimBool ──────────────────────────────────────────────────────── diff --git a/Topolei/Cubical/Syntax.lean b/CubicalTransport/Syntax.lean similarity index 99% rename from Topolei/Cubical/Syntax.lean rename to CubicalTransport/Syntax.lean index 800a829..0f8ddba 100644 --- a/Topolei/Cubical/Syntax.lean +++ b/CubicalTransport/Syntax.lean @@ -22,7 +22,7 @@ `CompLaws.lean`. -/ -import Topolei.Cubical.Face +import CubicalTransport.Face -- ── Syntax ──────────────────────────────────────────────────────────────────── diff --git a/Topolei/Cubical/System.lean b/CubicalTransport/System.lean similarity index 99% rename from Topolei/Cubical/System.lean rename to CubicalTransport/System.lean index bc8c23b..ca38b7c 100644 --- a/Topolei/Cubical/System.lean +++ b/CubicalTransport/System.lean @@ -19,7 +19,7 @@ · System.Typed — packages the typing judgment on the body -/ -import Topolei.Cubical.Typing +import CubicalTransport.Typing -- (Typing.lean is below System in the import chain; System cannot be imported -- from Typing. The HasType.comp rule uses raw components. This file provides -- the System.Valid → HasType.comp convenience bridge.) diff --git a/Topolei/Cubical/Transport.lean b/CubicalTransport/Transport.lean similarity index 98% rename from Topolei/Cubical/Transport.lean rename to CubicalTransport/Transport.lean index 66e14d2..88b6da2 100644 --- a/Topolei/Cubical/Transport.lean +++ b/CubicalTransport/Transport.lean @@ -33,8 +33,8 @@ reversal. -/ -import Topolei.Cubical.Value -import Topolei.Cubical.DimLine -- for CType.dimAbsent and substDimExpr +import CubicalTransport.Value +import CubicalTransport.DimLine -- for CType.dimAbsent and substDimExpr -- ── Rust FFI declaration (Phase C.2) ────────────────────────────────────── diff --git a/Topolei/Cubical/TransportLaws.lean b/CubicalTransport/TransportLaws.lean similarity index 99% rename from Topolei/Cubical/TransportLaws.lean rename to CubicalTransport/TransportLaws.lean index 9755f6c..c8b3073 100644 --- a/Topolei/Cubical/TransportLaws.lean +++ b/CubicalTransport/TransportLaws.lean @@ -25,7 +25,7 @@ `readback_transp_plam_general` in `Readback.lean` (Stream B #2c). -/ -import Topolei.Cubical.ValueTyping +import CubicalTransport.ValueTyping -- ── Subject reduction for transport ────────────────────────────────────────── diff --git a/Topolei/Cubical/Typing.lean b/CubicalTransport/Typing.lean similarity index 99% rename from Topolei/Cubical/Typing.lean rename to CubicalTransport/Typing.lean index 9dd9a67..a1deade 100644 --- a/Topolei/Cubical/Typing.lean +++ b/CubicalTransport/Typing.lean @@ -22,7 +22,7 @@ Dependent Π is deferred until we have a term evaluator. -/ -import Topolei.Cubical.DimLine +import CubicalTransport.DimLine -- ── Context ─────────────────────────────────────────────────────────────────── diff --git a/Topolei/Cubical/Value.lean b/CubicalTransport/Value.lean similarity index 99% rename from Topolei/Cubical/Value.lean rename to CubicalTransport/Value.lean index eec98e7..3f3ac72 100644 --- a/Topolei/Cubical/Value.lean +++ b/CubicalTransport/Value.lean @@ -21,7 +21,7 @@ evaluator will grow a companion `evalType` returning a `VType`. -/ -import Topolei.Cubical.Syntax +import CubicalTransport.Syntax mutual /-- Name-keyed environment: a cons-list of `(name, value)` bindings. The diff --git a/Topolei/Cubical/ValueTyping.lean b/CubicalTransport/ValueTyping.lean similarity index 98% rename from Topolei/Cubical/ValueTyping.lean rename to CubicalTransport/ValueTyping.lean index 6fa29d9..45961db 100644 --- a/Topolei/Cubical/ValueTyping.lean +++ b/CubicalTransport/ValueTyping.lean @@ -60,8 +60,8 @@ surface scales O(1) in type-formers, not O(n). -/ -import Topolei.Cubical.Typing -import Topolei.Cubical.Readback +import CubicalTransport.Typing +import CubicalTransport.Readback -- ── Semantic typing (declarative stubs) ───────────────────────────────────── diff --git a/HYPOTHESES.md b/HYPOTHESES.md deleted file mode 100644 index 1db0157..0000000 --- a/HYPOTHESES.md +++ /dev/null @@ -1,132 +0,0 @@ -# Topolei — Hypothesis Log - -Scientific tracking for the cells interface project. -Each hypothesis has: statement, prediction, falsification condition, test protocol, status. - ---- - -## H1 — EML as Computational Cell Primitive - -**Statement:** `eml(x,y) = exp(x) − ln(y)` with constant `1`, embedded as a single -primitive 1-cell, is sufficient to generate all elementary function cells via transport -composition. No other arithmetic primitives are needed in the computational layer. - -**Prediction:** sin, cos, +, ×, √, π can each be type-checked as EML tree cells in -Lean 4 with verified derivation proofs. - -**Falsification:** Any elementary function requiring a primitive outside the EML grammar -`S → 1 | eml(S,S)`. - -**Test protocol:** Formalize each derivation in `Topolei/EML/Derive.lean`. A failed -`#check` or `sorry`-free proof attempt falsifies. - -**Status:** Supported by Odrzywolek (2026) constructive proof. Not yet formalized in Lean. - ---- - -## H2 — Uniform Tree Structure Enables Intuitive Interface - -**Statement:** Because all EML expressions share the grammar `S → 1 | eml(S,S)`, -the interface can present math manipulation as composing identical nodes — subjectively -more intuitive than a multi-button calculator or ad-hoc function syntax. - -**Prediction:** After N interactive sessions, user reports EML-tree manipulation feels -natural for expressing the math they care about (calculus, ODEs, geometry). - -**Falsification:** User finds EML trees opaque or mechanical for expressions of -practical interest, even after familiarity. Specific failure case: depth > 4 trees -feel unmanageable without additional abstraction. - -**Test protocol:** Subjective sessions recorded in this file under H2-Sessions. -Scale: 1 (not intuitive, not powerful) to 5 (intuitive and powerful). - -**Status:** Untested. - -### H2-Sessions - - - ---- - -## H3 — Text and Graph Rendering Are Co-Projections of EML Cells - -**Statement:** Text layout and graph rendering are both sections of the same cell -fibration — `RenderCell` parameterized by a projection map — not architecturally -distinct subsystems. EML provides the shared computational substrate for both. - -**Prediction:** A single `RenderCell` type with two projection instances (text, graph) -compiles and renders correctly without duplicated primitives. - -**Falsification:** Text rendering requires primitives (glyph rasterization, string -indexing, Unicode) with no EML analog, forcing a separate non-cell subsystem. - -**Test protocol:** Implement `RenderCell` in Phase 4. If the text projection requires -falling outside the EML/cell framework, H3 is falsified. Partial falsification -(glyph lookup is external but layout is EML) is recorded as a refinement. - -**Status:** Partially supported by cells-spec §1.5 (rendering context as a cell). -EML connection is new and untested. - ---- - -## H4 — FM^fr as the Mathematical Foundation for Mathematical Notation - -**Statement:** Mathematical notation should be modeled as factorization homology -∫_M A over a framed syntactic space M with an E_n-algebra A encoding local -mathematical operations. Under this model: - -- A mathematical expression is an element of ∫_M A, where M is a framed manifold - (1-manifold for linear text; 2-manifold for 2D layout — fractions, matrices, integrals) -- A **rendering** (typeset, graph, interactive widget) is a choice of framing on M, - not a separate compilation pipeline -- A **transport between framings** is a provably structure-preserving language - transformation — syntax ↔ geometry ↔ interactive manipulation -- **Custom functionality** = changing A while holding M fixed (same syntactic space, - different algebra — e.g. symbolic vs. numeric vs. proof-term interpretation) -- **Custom language transformations** = changing the framing of M (same expression, - different geometric embedding — e.g. inline text vs. displayed equation vs. 3D surface) - -LaTeX, under this model, is not a compiler but a particular choice of framing with -a fixed algebra. "Parsing" is not a pipeline but section selection. The ⊗-excision -property of FM^fr guarantees that local composition rules are globally consistent — -which is exactly what LaTeX currently enforces by convention and fragile macros. - -**Prediction:** The `RenderCell` type from H3 is correctly typed as a section of the -FM^fr fibration ∫_{M^fr} A, where: -- M^fr is the framed syntactic manifold of the expression -- A is an E_n-algebra in the cells/EML framework -- Different renderings are provably related by framing transports - -**Falsification:** Mathematical notation requires operations that cannot be expressed -as local E_n-algebra data over any framing of a finite-dimensional manifold — i.e., -some notational structure is genuinely global and not ⊗-excisive. - -**Connection to existing framework:** -- Refines H3: FM^fr is the *reason* text and graph are co-sections, not just an - architectural claim -- Fits cells-spec §1.6: the Grothendieck fibration whose sections are programs is - precisely ∫_{(−)} A as M varies over framed manifolds -- Lives in the ∞-operad / (∞,1)-category world that the cubical embedding targets -- EML is the computational algebra A; the framed syntactic space M is what the - cells-spec calls the "rendering context" (§1.5) - -**Test protocol:** Formalize a minimal FM^fr structure in Lean 4: -1. Define a framed 1-manifold type (for linear text) -2. Define an E_1-algebra over EMLExpr -3. Show that ∫_M A recovers the standard left-to-right evaluation of a formula -4. Show that a framing change (1-manifold → 2-manifold embedding) recovers a - 2D layout (e.g., fraction bar as a geometric separator) - -**Status:** Proposed. Not yet formalized. Depends on Phase 1 (EML evaluator) and -the cubical core (transport, composition) being in place first. - ---- - -## Open Questions - -- OQ1: Does the EML framework extend to complex-valued rendering (i.e. shaders that - need ℂ arithmetic natively, not just as derived operations)? -- OQ2: Can EML tree depth be bounded for all functions needed in practical math - visualization, or do some require unbounded depth (infinite series)? -- OQ3: Is there a ternary EML variant (hinted at in the paper) that removes the - need for the distinguished constant `1`, and does that simplify the cell primitive? diff --git a/Main.lean b/Main.lean deleted file mode 100644 index 86b201a..0000000 --- a/Main.lean +++ /dev/null @@ -1,94 +0,0 @@ -import Topolei - -/-! Entry point for the topolei live transport renderer. - - Each subcommand opens a window showing **one fixed fiber** of a - cubical 1-cell. No host-side time-driven animation — fibers are - selected at render time and stay put. Re-run with a different - subcommand to see a different fiber. - - Default subcommand (`mid`) shows `plotT` at `pathParam = 0.5`: - `plotT.body = var "t"`, so every pixel evaluates to `0.5` — a - solid 50% grey panel. `at0` shows it at `0.0` (solid black), - `at1` at `1.0` (solid white). `endpoints` shows `at0 | at1` - side-by-side: solid black on the left, solid white on the right - — two genuinely-distinct fibers of the same 1-cell. - - `gradient` shows `plotPx`, a constant 1-cell whose body is the - spatial coordinate `px`. Every fiber is the same: a black→white - horizontal gradient. Useful as a sanity check that fibers - coincide on a constant 1-cell (`EMLPath.const_endpoints`). - - `transp` (and the older `plotExp` / `plotLn`) are kept reachable - but their bodies leave `[0, 1]` and so their greyscale renders - saturate to white / clamp to black at the display. The probe - test verifies they still match the spec — they're just - visually less informative without a normalized colorspace. --/ - -def main (args : List String) : IO Unit := do - match args with - | "at0" :: _ => - canvasRunPath plotT.toEMLPath 0.0 700 700 - "topolei — plotT at pathParam = 0 (solid black, the at0 fiber of var t)" - | "at1" :: _ => - canvasRunPath plotT.toEMLPath 1.0 700 700 - "topolei — plotT at pathParam = 1 (solid white, the at1 fiber of var t)" - | "mid" :: _ | [] => - canvasRunPath plotT.toEMLPath 0.5 700 700 - "topolei — plotT at pathParam = 0.5 (solid 50% grey, the midpoint fiber)" - | "qtr" :: _ => - canvasRunPath plotT.toEMLPath 0.25 700 700 - "topolei — plotT at pathParam = 0.25 (solid 25% grey)" - | "tqr" :: _ => - canvasRunPath plotT.toEMLPath 0.75 700 700 - "topolei — plotT at pathParam = 0.75 (solid 75% grey)" - | "endpoints" :: _ => - -- Side-by-side boundary fibers of the SAME 1-cell `plotT`. - -- Left panel: pathParam = 0 → solid black. - -- Right panel: pathParam = 1 → solid white. - -- Each panel has its own uniform buffer with its own - -- pathParam — the two fibers are genuinely distinct, and - -- no visualization adapter is aliasing them. - canvasRunPath2 - plotT.toEMLPath 0.0 - plotT.toEMLPath 1.0 - 1200 600 - "topolei — plotT at0 | at1 (boundary fibers: solid black | solid white)" - | "gradient" :: _ => - -- Constant 1-cell — body is `var "px"` so the image is fixed - -- regardless of pathParam. Two panels at different pathParams - -- should be pixel-identical (`EMLPath.const_endpoints` - -- realised on the GPU). - canvasRunPath2 - plotPx.toEMLPath 0.0 - plotPx.toEMLPath 1.0 - 1200 600 - "topolei — plotPx (constant 1-cell): two panels at different pathParam coincide" - | "transp" :: _ => - -- The original `plotTransp` 1-cell (body = exp(px) - t). Its - -- image leaves [0, 1] for these inputs, so most pixels saturate - -- to white at the display — but the spec and the probes still - -- agree on the underlying float values. - canvasRunPath2 - plotTransp.toEMLPath 0.0 - plotTransp.toEMLPath 1.0 - 1200 600 - "topolei — plotTransp at0 | at1 (image leaves [0,1]; expect saturation)" - | _ => - IO.println "topolei — usage:" - IO.println " topolei show plotT at pathParam = 0.5 (default; solid 50% grey)" - IO.println " topolei mid same as default" - IO.println " topolei at0 plotT at pathParam = 0 (solid black)" - IO.println " topolei at1 plotT at pathParam = 1 (solid white)" - IO.println " topolei qtr plotT at pathParam = 0.25" - IO.println " topolei tqr plotT at pathParam = 0.75" - IO.println " topolei endpoints plotT at0 | at1 (black | white side-by-side)" - IO.println " topolei gradient plotPx (constant 1-cell): same gradient on both panels" - IO.println " topolei transp plotTransp at0 | at1 (saturates; spec is honest)" - IO.println "" - IO.println "Each window renders ONE fiber of the cubical 1-cell, statically." - IO.println "There is no host-side time-to-parameter animation — that would" - IO.println "not be a transport. The `Float → RGB` step is identity" - IO.println "(greyscale), not a hue cycle, so fibers that genuinely differ" - IO.println "show distinct pixel output." diff --git a/NAGA_IR_PLAN.md b/NAGA_IR_PLAN.md deleted file mode 100644 index 6992743..0000000 --- a/NAGA_IR_PLAN.md +++ /dev/null @@ -1,473 +0,0 @@ -# NAGA_IR_PLAN.md — direct naga IR construction from `EMLPath` - -*A pick-up-in-one-session plan for eliminating the last text-format -intermediary in the render pipeline: the GLSL string that currently -sits between `Rust EMLExpr` and `naga::Module`. Future author: read -§1–§4 before writing any code; §5 is the staging; §6–§8 are the -traps.* - ---- - -## 0. TL;DR - -**Today** (after P6): `Lean EMLPath → FFI → Rust EMLPath → Rust GLSL -string → naga-glsl parser → naga::Module → SPIR-V → GPU`. - -**Target**: `Lean EMLPath → FFI → Rust EMLPath → naga::Module (direct) → -SPIR-V → GPU`. No GLSL text format. No parser. - -Why: the text stage is the last unverified intermediate in the pipeline. -naga-glsl is a third-party GLSL parser; its semantic interpretation of -our shader strings is trust-based. Eliminating it shortens the trust -boundary from "naga's GLSL frontend plus Lean+Rust emitters must all -agree" to "Lean and Rust emitters must produce equivalent naga modules". - -It is not an axiom-killer on its own — `compileEMLPath_correct` -remains an axiom — but it removes a 10⁴-line dependency from the trust -surface and brings a Lean-side naga-IR model (future work) within -reach. - ---- - -## 1. Prerequisite reading - -Read these **before** writing any code. Skip any item at your peril: -each pitfall in §6 is a trap you'll step into without the background. - -### 1.1 Upstream (naga + wgpu) - -Match the version pinned in `native/canvas-rs/Cargo.toml` (currently -`wgpu = "22.1"` → `naga 22.1.0`). Paths given are on this machine's -cargo registry; repoint as needed. - -| File | What to read it for | -|------|---------------------| -| `$CARGO_HOME/registry/src/*/naga-22.1.0/src/lib.rs` | `Module`, `Function`, `EntryPoint`, `Expression`, `Statement`, `Literal`, `MathFunction`, `Type`, `TypeInner`. Top-level IR vocabulary. | -| `$CARGO_HOME/registry/src/*/naga-22.1.0/src/arena.rs` | `Arena`, `UniqueArena`, `Handle`. Why every expression lives behind a handle, why types must dedupe. | -| `$CARGO_HOME/registry/src/*/naga-22.1.0/src/valid/mod.rs` | `Validator`, `ValidationFlags`, `Capabilities`. Validation is a **hard precondition** for SPIR-V emission — unvalidated modules panic the writer. | -| `$CARGO_HOME/registry/src/*/naga-22.1.0/src/back/spv/mod.rs` | `write_vec(&Module, &ModuleInfo, &Options, Option<&PipelineOptions>) -> Result, Error>`. One call; needs `ModuleInfo` from the validator. | -| `$CARGO_HOME/registry/src/*/naga-22.1.0/src/front/glsl/*` | Reference implementation of a frontend building a `Module`. Good place to copy patterns from. | -| `$CARGO_HOME/registry/src/*/wgpu-22.1.0/src/lib.rs` (search `ShaderSource`) | Three variants: `SpirV(Cow<[u32]>)`, `Glsl{...}`, `Naga(Cow<'static, Module>)`. The `Naga` variant takes a module directly — we can pass our constructed `Module` without round-tripping through SPIR-V, though SPIR-V is the default and more widely exercised path. | - -### 1.2 In-repo context - -| Doc / source | Read for | -|--------------|----------| -| `RENDER_BRIDGE_GAP.md` §4, §6 | The original framing of the rendering-stack axioms. §6 describes the multi-layer Projection/Observation pipeline this plan is a prerequisite for. | -| `Topolei/GPU/Spec.lean` | `ShaderSemantic`, `shaderVar`, `shaderVarWithDim`, `EMLPath.toColor`, the axioms (`compileEMLPath`, `compileEMLPath_correct`, `render_faithful`). The semantic the naga module must realise. | -| `Topolei/EML.lean` + `Topolei/EML/Path.lean` | `EMLExpr`, `PlotConfig`, `EMLPath`, `EMLPath.toFragShaderProbe`. The Lean-side shader emitter — the naga builder must produce a module whose SPIR-V behaves identically on the seven P7 probes. | -| `native/canvas-rs/src/eml.rs` | Rust-side `EMLExpr` / `EMLPath` + `emlexpr_from_lean` walker + `to_frag_shader_probe` string emitter. The input side of the naga builder (walker is unchanged); the output side is what's being replaced. | -| `native/canvas-rs/src/lib.rs::offscreen_render_pixel` | Where the shader-module handoff happens. The swap-in point. | -| `Topolei/Render/Probe.lean` | `probeEmitterDiff` (string diff Lean vs Rust GLSL) + pixel probes. The test surface that must stay at 10/10 after the swap. | - -### 1.3 Background concepts - -- **Arena-based IR**. Every `Expression` and `Type` is stored by - handle in an arena owned by the enclosing `Function` / `Module`. - Construct nodes strictly bottom-up: a handle is only valid once all - its subexpressions exist in the same arena. -- **Emit statements**. Computing an expression isn't the same as - making its value available to later statements. Non-`const` - expressions must be wrapped in `Statement::Emit(range)` entries in - the function body, naming the ranges of expression handles that are - now "live" at this point in control flow. Forgetting this is a - silent-mis-emit trap (emits a module that validates but produces - wrong code). -- **`UniqueArena` vs `Arena`**. Types go in `UniqueArena` so `f32` - is allocated once and reused. Expressions go in `Arena` (each - `Literal(1.0)` is a distinct node). Mixing these up is a - validation-time error. -- **Bindings**. Entry-point args/results need `Binding`s - (`Location` for varyings, `BuiltIn` for special inputs like - `Position`). Uniform globals need `ResourceBinding { group, binding }` - matching the wgpu bind-group layout. - ---- - -## 2. Current state — what's wired today - -`native/canvas-rs/src/lib.rs::offscreen_render_pixel` builds a -`wgpu::ShaderModule` via `ShaderSource::Glsl { shader: fragment_glsl, -stage: Fragment, defines: ... }`. Behind the scenes wgpu calls -naga-glsl, parses the string, produces a `naga::Module`, then writes -SPIR-V via `naga::back::spv::write_vec`. - -The `fragment_glsl` string comes from `eml::EMLPath::to_frag_shader_probe` -(or `PlotConfig::toFragShader` for the decorated live-render path). - -### Emission structure (reference) - -The probe shader emitted today is a small fixed scaffold plus one EML -expression. Reproduced here in the order a naga builder will need to -materialise the pieces: - -```glsl -#version 450 -layout(location=0) in vec2 uv; -layout(location=0) out vec4 fragColor; -layout(set=0, binding=0) uniform Uniforms { - float u_time; // offset 0 - float u_pathParam; // offset 4 - vec2 u_resolution; // offset 8 -}; -void main() { - float px = uv.x; - float py = uv.y; - float {dim} = u_pathParam; - float v = {body}; // the EML expression - float r = 0.5 + 0.5 * cos(6.2832 * v); - float g = 0.5 + 0.5 * cos(6.2832 * v + 2.094); - float b = 0.5 + 0.5 * cos(6.2832 * v + 4.189); - fragColor = vec4(r, g, b, 1.0); -} -``` - -Every line above has a direct naga IR counterpart; §5 is the -translation. - ---- - -## 3. Target state — what the naga IR builder looks like - -A new module `native/canvas-rs/src/emit_naga.rs` exposes: - -```rust -pub fn build_probe_module(path: &crate::eml::EMLPath) -> naga::Module; -``` - -Internal organisation: - -```rust -// Top-level orchestrator. Produces a fully-validated Module. -pub fn build_probe_module(path: &EMLPath) -> Module { ... } - -// The fragment entry-point's body + its expression/statement arenas. -fn build_main_fn(module: &mut Module, types: &ProbeTypes, - globals: &ProbeGlobals, path: &EMLPath) -> Function { ... } - -// The EML-specific piece: EMLExpr → Handle under an -// extending function arena. This is the structural heart. -fn emit_emlexpr( - expr: &EMLExpr, - module: &Module, - function: &mut Function, - env: &EmitEnv, -) -> Handle { ... } - -// The scaffolding types referenced by multiple stages. -struct ProbeTypes { - f32: Handle, - vec2_f32: Handle, - vec4_f32: Handle, - uniforms: Handle, // struct { f32, f32, vec2 } -} - -struct ProbeGlobals { - uniforms_buf: Handle, // @group(0) @binding(0) - uv_in: Handle, // @location(0) input - frag_out: Handle, // @location(0) output -} - -// Compile-time env mapping EMLExpr variable names to the naga -// expression handle that evaluates to that variable's value at the -// current point. `path.dimName` maps to the `u_pathParam` field of -// the uniform; "px" / "py" map to uv.x / uv.y; everything else is -// a "bound to 0.0" fallback (matching shaderVar's fallback). -struct EmitEnv { - px: Handle, - py: Handle, - dim_name: String, - dim_value: Handle, -} -``` - -Outside the builder: - -```rust -// In offscreen_render_pixel (and the live renderer later): -let module = emit_naga::build_probe_module(&path); -let shader_module = device.create_shader_module( - wgpu::ShaderModuleDescriptor { - label: Some("probe-fragment"), - source: wgpu::ShaderSource::Naga(Cow::Owned(module)), - } -); -``` - ---- - -## 4. The EMLExpr translation table - -Every variant maps to one or two naga expression nodes. The table is -small because EMLExpr is small — this is why the "maximally correct" -pathway is tractable. - -| EMLExpr | naga Expression | -|---------|------------------------------------------------------| -| `One` | `Expression::Literal(Literal::F32(1.0))` | -| `Var "px"` | `env.px` (reuse handle) | -| `Var "py"` | `env.py` (reuse handle) | -| `Var name` where `name == path.dimName` | `env.dim_value` (reuse handle) | -| `Var name` (other) | `Expression::Literal(Literal::F32(0.0))` — matches `shaderVar`'s fallback | -| `Eml(l, r)` | `exp(l) - log(r)`: two `Math` calls + one `Binary::Subtract` | - -And the main-body scaffolding above `v = eml(...)`: - -| GLSL line | naga expressions | -|-----------|------------------| -| `float px = uv.x` | `AccessIndex { base: uv_in, index: 0 }` | -| `float py = uv.y` | `AccessIndex { base: uv_in, index: 1 }` | -| `float dim = u_pathParam` | `AccessIndex { base: Expression::Load(uniforms_buf), index: 1 }` | -| `0.5 * cos(6.2832 * v + phase)` | Literal + Math(Cos) + Binary(Mul/Add) chains | -| `fragColor = vec4(r, g, b, 1.0)` | `Expression::Compose { ty: vec4_f32, components: [r, g, b, Literal(1.0)] }`; then `Statement::Store { pointer: frag_out, value: composed }` | - -Every intermediate expression must be reachable via `Statement::Emit` -before the `Store` that uses them. - ---- - -## 5. Staging — seven commits, each independently verifiable - -Each stage adds strictly more and ships green regression. Do not skip -stages — they're ordered so validation catches each class of mistake -in isolation. - -**Status:** Stages 1–6 done as of 2026-04-25. All 17 probes pass on -the Vulkan-Intel-Iris-Xe target. Stage 7 (cutover / drop the GLSL -feature for the probe path) is the only remaining follow-up. - -### Stage 1 — hardcoded red pixel — ✅ done - -Goal: prove the SPIR-V pipeline works end-to-end with zero EML content. - -- `build_probe_module` ignores `path`; returns a module whose - fragment main writes `vec4(1.0, 0.0, 0.0, 1.0)`. -- Wire it into `offscreen_render_pixel` behind a `#[cfg(feature = - "naga_ir")]` or a boolean. -- Run `probe-test`. Expected: the seven pixel probes **fail** - (pixels are red, not the EML color), but the adapter line prints - and no panic occurs. The failure mode confirms wiring; a panic - would mean module validation failed. -- Commit. - -### Stage 2 — uniforms bound — ✅ done - -Goal: confirm the bind-group layout matches. - -- Build the `Uniforms` struct type in the module. -- Add `uniforms_buf` global with `@group(0) @binding(0)`. -- Output `vec4(u_pathParam, u_pathParam, u_pathParam, 1.0)`. -- Confirm probe pixels equal `(u_pathParam, u_pathParam, - u_pathParam)` — for `pathParam = 0.5` every channel is ≈ 0.5. -- Commit. - -### Stage 3 — varyings wired — ✅ done - -Goal: reach `px = uv.x`, `py = uv.y`. - -- Add `uv_in` global with `@location(0)` input binding. -- Output `vec4(uv.x, uv.y, 0.0, 1.0)`. -- Confirm pixel `(x, y)` in framebuffer has `r = (x+0.5)/w`, `g = 1 - - (y+0.5)/h` (NDC y-flip). -- Commit. - -### Stage 4 — minimal EML emission — ✅ done - -Goal: walk the `EMLExpr` tree. - -- Implement `emit_emlexpr` covering `One`, `Var`, `Eml`. -- Output `v = eml_body(path)`; write `vec4(v, v, v, 1.0)`. -- For `plotExp` (body `exp(px) - log(1.0)`), pixel `(64, 64)` should - give `v ≈ exp(0.504) - log(1.0) ≈ 1.655`. Stored as `1.655` - clamped by the format. Since Rgba32Float doesn't clamp, the value - is literal. -- Commit. - -### Stage 5 — full probe-shader equivalent — ✅ done - -Goal: match `EMLPath.toColor` pixel-for-pixel. - -- Add the cos-cycle: `r = 0.5 + 0.5 * cos(6.2832 * v)`, g with phase - `2.094`, b with phase `4.189`. -- All seven pixel probes pass. -- Commit. - -### Stage 6 — emitter-drift test extension — ✅ done - -Goal: the naga-IR and GLSL-string emitters must agree on pixel -output. - -- Keep both `build_probe_module` (naga) and `to_frag_shader_probe` - (GLSL) usable. -- Add a third probe row to `ProbeTest`: render the same path via both - paths, assert the pixel outputs agree. -- Runs 3 emitter-drift + 7 naga-pixel + 7 glsl-pixel = 17 probes. -- Commit. - -### Stage 7 — cutover — pending - -Goal: make naga-IR the default, drop naga-glsl from the probe. - -- `offscreen_render_pixel` now uses `ShaderSource::Naga` - unconditionally. -- Keep `to_frag_shader_probe` only for the `rustEmitProbeShader` FFI - (used by the emitter-drift test on the Lean side — Lean's string - emitter still has to agree with something human-readable). -- Remove `features = ["glsl"]` from `wgpu` in `Cargo.toml` if no - remaining code path needs it. (Check: `PlotConfig::toFragShader` - is still a GLSL string used by the interactive render loop — - keeping `glsl` for that path is reasonable until a separate pass - converts the decorated plot shader to naga IR too.) -- Commit. - ---- - -## 6. Known pitfalls - -These are the traps the naga-glsl frontend handles silently and a -hand-built module must get right. - -1. **Expression-before-use**. If you add - `Expression::Binary { left, right }` to the function arena before - `left` is in the arena, validation fails with a cryptic - `InvalidExpression`. Always append subexpressions first. -2. **`Statement::Emit`**. Non-constant expressions must be wrapped - in an `Emit` statement or they're not computed at runtime. Track - the last-emitted handle as you build; emit a fresh range on each - "new live handle". See `naga/src/front/glsl/builtins.rs` for the - pattern. -3. **`UniqueArena` for types**. `module.types.insert(ty, Span::UNDEFINED)` - deduplicates — calling `insert(f32, _)` twice returns the same - handle. Do not call `Arena::append` on the types arena; that API - isn't exposed on `UniqueArena`. -4. **Struct layout must match Rust `Uniforms`**. naga's struct-layout - validator rejects mismatched offsets/strides. Enforce - `Layout::Std140` (Vulkan UBO convention) and specify each member's - `offset` / `span` explicitly. Cross-check against - `native/canvas-rs/src/lib.rs::Uniforms` — `time: f32 @ 0`, - `path_param: f32 @ 4`, `resolution: [f32; 2] @ 8`. -5. **`ResourceBinding { group: 0, binding: 0 }`** on the uniform - global. Mismatch with wgpu's `BindGroupLayout` is a runtime error - in `create_render_pipeline`. -6. **Entry-point arguments and result**. Fragment entry-point - takes `uv: vec2` at `Location(0)` and returns - `vec4` at `Location(0)`. Naga's `EntryPoint` - encodes these on the `Function::arguments` / `result` directly, - not as globals — differs from the GLSL surface where they're top- - level `in`/`out`. Two idioms coexist in naga modules; the - argument-binding idiom is cleaner. Study - `naga/src/front/glsl/functions.rs` for the translation. -7. **NDC y-flip**. The GLSL emitter produces a shader that reads - `uv.y` matching the vertex-shader's `pos * 0.5 + 0.5` convention. - wgpu+Vulkan flip Y in the rasterizer. A naga-IR fragment shader - inherits the same conventions — don't add a compensating flip. -8. **Validation capabilities**. `ValidationFlags::all()` is usually - what you want. Some naga features (e.g., subgroup ops) require - enabling specific `Capabilities`; our probe shader uses none of - these. Keep `Capabilities::empty()` initially. -9. **Debug names**. `module.types[h].name = Some(...)` and - `Function::named_expressions` are optional but make SPIR-V debuggers - (RenderDoc) vastly more usable. Worth the 10 lines. -10. **`naga::Module::Default`**. `Module` doesn't implement `Default`; - construct explicitly with every arena as `::default()` and - `entry_points: Vec::new()`. Typo'd field initialisers are - silent. - ---- - -## 7. Verification strategy - -Each stage is verifiable independently, but the composite check is: - -### 7.1 Static - -- `naga::valid::Validator::new(ValidationFlags::all(), - Capabilities::empty()).validate(&module)?` - must succeed. Run on every module before SPIR-V emission. - Validation failures are informative (point at the handle + issue). - -### 7.2 Dynamic - -- **All 10 probes pass** (3 emitter-drift + 7 pixel). This is the - authoritative end-to-end check. If pixels diverge from the GLSL - path by more than tolerance, the naga emitter has a bug. -- **Byte-level SPIR-V diff** (optional). Dump the SPIR-V bytes from - the naga-IR path and the naga-glsl path; compare. Byte-equality is - unlikely but semantic equivalence is checked by the pixel probes. - -### 7.3 Stage-by-stage check - -Run `probe-test` after each stage. At Stage 5 onward all 10 pass; -earlier stages will have pixel-probe failures by design but still -exercise adapter selection + module validation + render pass. - ---- - -## 8. Open questions (decide before Stage 1) - -1. **Keep the GLSL path for `PlotConfig::toFragShader` (decorated - plot shader)?** That shader has grid/axes/curve overlay code not - representable in `EMLExpr`. Options: - - (a) Keep GLSL for decorated plots; naga-IR only for probe shader. - Simpler. Keeps `features = ["glsl"]`. - - (b) Extend `EMLExpr` with a "decoration" AST for grid/axes/curve. - Principled, larger scope. - - (c) Let the decorated plot be a fixed naga-IR template with an - `EMLExpr`-hole for the body. Middle ground. - *Recommendation: (a) for the first pass; revisit (c) once the - probe is direct.* -2. **Pipeline caching.** Each probe call recreates the wgpu device - (seconds per call). Separate concern from naga IR, but if we're - touching the same pipeline setup code, worth a paragraph. -3. **wgpu `ShaderSource::Naga` vs round-trip via `ShaderSource::SpirV`.** - `Naga` hands the module directly to wgpu's internal SPIR-V writer; - `SpirV` accepts pre-written bytes. Functionally equivalent for - our use; `Naga` is one less step but `SpirV` gives byte-level - inspectability for debugging. *Recommendation: use `Naga` for - the production path and expose a `--dump-spirv` debug switch for - the bytes.* -4. **Error-path UX.** naga validation errors are structured - (`ValidationError`); surface them in `offscreen_render_pixel`'s - return as `Err(format!(...))` rather than via `panic!`. A - mis-built probe module should produce a sentinel, not a crash. - ---- - -## 9. Formal status after this pass - -This push does **not** eliminate `compileEMLPath_correct` as an -axiom. The axiom's content is still "the shader handle produced by -Lean's `compileEMLPath` has semantic equal to `EMLPath.toColor`". -What it eliminates is the unverified text stage: - -| Pipeline layer | Before | After | -|----------------|--------|-------| -| Lean → Rust AST | structured (P6) | structured (unchanged) | -| Rust AST → shader input | Rust emits GLSL text | Rust builds naga IR | -| shader input → naga Module | naga-glsl parses ~10⁴ LOC | same arena we built | -| naga Module → SPIR-V | naga::back::spv (unchanged) | unchanged | -| SPIR-V → GPU | wgpu + driver (unchanged) | unchanged | - -The "same arena we built" line is the correctness gain: the module -the SPIR-V writer sees is the exact structure we constructed from -`EMLExpr`, with no parse step in between. A future Lean model of -naga IR could then close the remaining gap to a theorem — but that -is a separate project. - ---- - -## 10. Session budget - -Realistic one-session target: **Stages 1–5** (hardcoded red → full -probe parity). That's ~500–800 Rust lines, most of it in the new -`emit_naga.rs`, with careful arena bookkeeping. Expect two hours of -naga-docs reading before writing one stage. - -Stages 6–7 (drift test + cutover) are ~100 lines each and can go in -a follow-up. - ---- - -*End of NAGA_IR_PLAN.md. Update §8 if the decisions change. Update -§5 as stages land — tick them here rather than spreading status across -session summaries.* diff --git a/PLAN.md b/PLAN.md deleted file mode 100644 index 4188f5e..0000000 --- a/PLAN.md +++ /dev/null @@ -1,199 +0,0 @@ -# Topolei — Architecture Plan - -## Core Premise - -Topolei is a **Lean 4 extension** that adds cubical-transport homotopy type -theory to Lean 4 via a Rust FFI module. On top of that foundation, it -builds a unified rendering interface where text and graphs are co-projections -of the same computational primitive: the EML cell. - -Process discipline: every layer that will ultimately cross the FFI boundary -is *first* formalized in Lean as axioms and data structures. The Rust -component then discharges those axioms at runtime via `@[extern]` / -`@[implemented_by]`. The cubical core (Phase 1) exemplifies this — its -axiom set (eval-level equations for `eval`/`vApp`/`vPApp`/`vTransp`/…, -six glueIn/unglue face axioms, Glue-transport axioms) is complete in Lean -with zero Rust written yet. - -**Step-level axioms have been collapsed.** The Phase 1 Week 7 -step↔eval bridge (`Cubical/Readback.lean`, `STATUS.md` § Week 7) -provides `CTerm.readback := readback ∘ eval .nil` and derives NbE -analogues of each step axiom. Stream B #2d (2026-04-23) completed -the cleanup by physically deleting the now-redundant axioms. Current -status: -- ✅ Removed from source (NbE theorems in `Readback.lean`): - T1 `transp_id`, T2 `transp_const_id`, C1 `comp_full`, C2 `comp_empty`, - `step_papp_plam`. -- ✅ T4 NbE coverage complete for path-typed lines via - `readback_transp_plam_general` (Stream B #2c, 2026-04-23) — combines - the full-face, constant-line, and varying-path cases. Non-path - varying lines are vacuous in well-typed code. -- ✅ T5 promoted to eval level via `eval_transp_face_congr` (Stream B - #2b, 2026-04-23); NbE form `readback_transp_face_congr`. Step-level - T5 axiom removed. -- ⚠️ Residual axioms (genuinely need extra machinery): T3 and C4 - (subject reduction, need typing-preservation lemmas). Step-level T4 - retained as syntactic fallback. - -Rust's obligation set is the eval-level equations plus readback -equations (now including `readback_vPathTransp_plam` / `_other`) plus -the residual step axioms — not the full step axiom list. - -EML operator (Odrzywolek 2026, arXiv:2603.21852): - - eml(x, y) = exp(x) − ln(y) - -Grammar: S → 1 | eml(S, S) - -This is the continuous analogue of NAND — a Sheffer operator for all elementary -functions. Every sin, cos, +, ×, √, π is a particular binary tree of eml nodes. -See `exp-log.pdf` for the full constructive proof. - ---- - -## Rendering Stack - -``` -Lean 4 - EMLTree S → 1 | eml(S,S) - ↓ verified compile - ShaderIR typed binary IR; proofs attached here - ↓ emit (primary) - SPIR-V bytes binary, formal semantics, direct to Vulkan driver - ↓ (FFI: C ABI) -Rust (wgpu / Vulkan) - GPU context - draw(uniformBuffer) - ↓ -Native window (X11/Wayland) — or WebGPU (browser) via the same wgpu -``` - -### Browser / WASM interoperability - -When browser deployment is needed, add a WGSL emitter from the same ShaderIR node. -The proof layer and the IR are unchanged; only the emit step differs. - -``` -ShaderIR - ├─ emit/SPIRV.lean → SPIR-V bytes (native Vulkan — primary dev target) - └─ emit/WGSL.lean → WGSL text (WebGPU / browser — secondary target) -``` - -WGSL text is compiled to native GPU instructions by the browser (Metal/DX12/Vulkan -under the hood), so runtime performance is identical. The format difference is -parse-time only and immaterial once shaders are uploaded. - -**Decision:** Develop against SPIR-V + Vulkan. Add WGSL emitter before any browser -demo. Both share the same ShaderIR, so the WGSL path costs one emitter file, not -an architectural change. - -### Why not WASM-compile Lean itself - -Lean 4's WASM backend exists (via its C output + emscripten) but produces -large bundles (~30–80MB with elaborator + stdlib). For the primary -deployment target — "browser-runnable shader demo" — keep Lean as a -native ahead-of-time compiler that emits shader strings/bytes + discharged -proof terms at build time; ship only the Rust FFI runtime as the `.wasm` -module. A secondary "prove-in-browser" artifact can be built later if -interactive theorem exploration is wanted. - ---- - -## Window Interface - -Three surfaces exposed to the window — nothing more: - -1. `uploadShader(spirv: ByteArray) → ShaderHandle` - Compiled EML tree arrives as SPIR-V bytes. - -2. `setUniform(handle: ShaderHandle, name: String, value: Float) → Unit` - Moving a slider = transport along a path in parameter space. - -3. `onInput(event: InputEvent) → CellDeformation` - Click/drag lifts screen coordinates back to cell space (fiber selection). - -The window is dumb. All homotopy structure is resolved in Lean before bytes cross -the FFI. Rust manages GPU context lifecycle and — on the other FFI surface — -the cubical evaluator kernel linked to Lean's `eval`/`readback` axioms -(with `step` derived — see Phase 4 below). - ---- - -## Phase Roadmap - -### Phase 1 — EML Core (Lean 4) -- `Topolei/EML/Tree.lean` — inductive `EMLTree` (S → 1 | eml S S) -- `Topolei/EML/Eval.lean` — evaluator `EMLTree → ℂ` -- `Topolei/EML/Derive.lean` — prove sin, cos, +, ×, π as EML trees (verified) -- `Topolei/EML/Compile.lean` — EMLTree → ShaderIR - -### Phase 2 — ShaderIR + Emitters -- `Topolei/Shader/IR.lean` — typed intermediate representation -- `Topolei/Shader/SPIRV.lean` — ShaderIR → SPIR-V bytes (primary) -- `Topolei/Shader/WGSL.lean` — ShaderIR → WGSL text (browser compat) - -### Phase 3 — Zigzag Engine Lean Port -Port the n-category combinatorial engine from Rust reference into Lean 4. -See `ZIGZAG_PORT.md` for the step-by-step plan. Delivers `Topolei/Zigzag/`: -`Monotone`, `Core`, `Diagram`, `Signature`, `Degeneracy`, `Pullback`, -`Normalise`, `Typecheck`, `Tests`. Plus `Cell/Zigzag.lean` bridging to the -cubical core. **Pure Lean — no Rust dependency for this phase.** The Rust -implementation at `zigzag-engine/` is reference material only; see its -README. Delivers dimension-general normalisation (past the homotopy.io -4D cap), provable essential-identity preservation, and the combinatorial -backend for higher cells. - -### Phase 4 — Rust FFI: Cubical Evaluator Backend (the *one* Rust component) -Rust implementations of `eval`, `vApp`, `vPApp`, `vTransp`, `vHCompValue`, -`vCompAtTerm`, `vCompNAtTerm`, `readback`, `readbackNeu`, and the -face-disjoint reductions for transp/comp/glue. Linked via `@[extern]` + -`@[implemented_by]` to the axioms already stated in `Cubical/Eval.lean`, -`Cubical/Readback.lean`, `TransportLaws.lean` (residual), `CompLaws.lean` -(residual), `Soundness.lean`, and `Glue.lean`. Turns Lean's -reasoning-only cubical core into a kernel-speed reducer. - -**Step is largely derived, not implemented.** The Week 7 step↔eval -bridge (Sessions 1–4 + Session 5 cleanup landed 2026-04-23) gives -`CTerm.readback := readback ∘ eval .nil` and NbE-level analogues of T1, -T2, C1, C2, `step_papp_plam` (+ partial T4) as Lean theorems — the -former axiom statements have been physically removed from the source. -Four step-level axioms remain on Rust's plate: T3, T5, C4 (each blocked -on separate machinery, see STATUS.md Week 7 table), and the general T4 -case. - -**This is the only Rust component in topolei.** It exists solely to -extend Lean 4 with computational cubical-transport HoTT — everything -else is Lean. - -### Phase 5 — GPU Runtime (still Rust, but within Phase 4's crate) -wgpu (Vulkan/Metal/DX12/WebGPU) context, shader upload, framebuffer, -uniform buffers. Three-surface window FFI (upload / setUniform / -onInput) plus the render loop. Lives in the same Rust crate as Phase 4 -for convenience (shared `lean-sys` interop) but is a distinct FFI -surface (effects vs. reductions). - -### Phase 3.5 — FM^fr Notation Layer (H4) -- Model mathematical notation as ∫_M A (factorization homology, framed) -- M : framed syntactic manifold (1-manifold = linear text, 2-manifold = 2D layout) -- A : E_n-algebra over EMLExpr — local composition rules -- Framing transports = language transformations (syntax ↔ geometry ↔ interactive) -- LaTeX becomes one framing choice; other framings give graph/interactive/proof renderings -- Depends on: EML evaluator (Phase 1) + cubical transport (cells-spec Phase 1) - -### Phase 4 — Cells-EML Bridge -- Connect EMLTree nodes into cells-spec CType/CTerm framework -- EML node = 1-cell; tree composition = path concatenation - -### Phase 5 — Subjective Testing Loop -- Minimal interactive window: one EML tree → one rendered cell -- User manipulates parameters; intuition scores recorded in HYPOTHESES.md -- Iterate on interface based on H2 and H3 test results - ---- - -## Constraints (from cells-spec) - -1. Zero external HoTT dependencies — own everything from interval algebra up -2. Lean 4 kernel compatibility — cubical calculus deeply embedded as data -3. Self-maintainable — single developer buildable, no external package ecosystem -4. Practical GPU target — proof layer and performance layer separated by narrow FFI diff --git a/ProbeTest.lean b/ProbeTest.lean deleted file mode 100644 index dd8da3c..0000000 --- a/ProbeTest.lean +++ /dev/null @@ -1,10 +0,0 @@ -import Topolei.RenderProbe - -def main : IO UInt32 := do - let fails ← TopoleiProbe.runProbes - if fails > 0 then - IO.println s!"FAIL: {fails} probe(s) diverged" - return 1 - else - IO.println "PASS: GPU output matches Lean ShaderSemantic on all probes" - return 0 diff --git a/README.md b/README.md index 3dc457f..9fafc68 100644 --- a/README.md +++ b/README.md @@ -1,51 +1,58 @@ -# topolei +# cubical-transport-hott-lean4 -A Lean 4 extension adding cubical-transport homotopy type theory to Lean 4 -via a Rust FFI module. +A Lean 4 implementation of cubical-transport homotopy type theory +(CCHM-flavor), with a fast Rust kernel exposed through C ABI. -## Documents +The Lean side defines the syntax, semantics, and soundness theorems. +The Rust side discharges the per-step β-rules of the evaluator. +Lean axioms are routed through `@[implemented_by]` to Rust functions +that return Lean objects in the same shape Lean would have produced; +the soundness layer (`CubicalTransport/Soundness.lean`) certifies the +backend at the boundary, so the kernel speed of the Rust code +preserves the Lean-level proofs. -- **`STATUS.md`** — current formal status, Phase 1 closure, open - obligations, three-stream priority order. -- **`PLAN.md`** — architecture plan: rendering stack, window FFI - surfaces, phase roadmap. -- **`cells-spec.md`** — full system specification: cubical core, cells, - shader pipeline, runtime, boundary, self-hosting. -- **`TRANSPORT_PLAN.md`** — step-by-step cubical evaluator formalization - plan (Phase 1 history). -- **`ZIGZAG_PORT.md`** — step-by-step Lean port plan for the n-category - combinatorial engine. Parallel to TRANSPORT_PLAN but for Phase 2+ - higher-cell backend. -- **`NAGA_IR_PLAN.md`** — staged plan for direct `naga::Module` - construction from `EMLPath` (eliminating the last text-format stage - in the render pipeline). Seven-stage roadmap with reading list and - known pitfalls; pick up in a fresh session. -- **`NUMERICAL.md`** — principles for numerical implementations: - separation of mathematical content from execution context, - contracts, registry, construction faults to avoid. -- **`HYPOTHESES.md`** — H1–H4 hypotheses about the approach. -- **`REFERENCES.md`** — papers and code references (CCHM, cubicaltt, - Agda Cubical, EML). -- **`zigzag-engine/`** — reference Rust implementation of the - n-category engine (~11K LOC) + papers. Port-from material, NOT a - dependency. See its own README. +## What's here -## Core framing +- `CubicalTransport/` — 22 Lean modules for syntax, substitution, + dimensional structure, faces, typing, evaluation (eval / value / + readback), transport, Glue, composition, and the soundness theorems. +- `native/cubical/` — Rust kernel (`#![no_std]`, dual-target native + staticlib + cdylib, wasm32 cdylib). +- `CubicalTest.lean`, `CubicalBench.lean` — engine smoke + property + tests (62/62 passing) and microbenchmarks. -**Topolei is a Lean 4 extension.** Everything external to Lean is -*first* specified as Lean axioms; the one Rust FFI component later -discharges those axioms via `@[extern]` + `@[implemented_by]`. +## Reusing this engine -**The one Rust component** is the cubical evaluator backend (plus GPU -runtime within the same crate). Its purpose is extending Lean 4 with -computational cubical-transport HoTT. +Add as a Lake dependency from another Lean 4 project: -**Everything else is Lean**, including the zigzag n-category engine -(being ported in) and the numerical layer. The medium-term goal is to -maximise what can be reasoned about inside Lean; Rust is used only -where fundamentally required (effects) or as a post-spec optimisation -target. +```toml +[[require]] +name = "cubicalTransport" +path = "../cubical-transport-hott-lean4" # or git = "..." +``` -The cubical core (Phase 1) is closed with zero Rust dependency. Many -subsequent phases (Cells, Reactive, Color, Shader IR, Boundary models, -Meta, Zigzag) are pure-Lean extensions of the axiom base. +Then `import CubicalTransport.Syntax`, `import CubicalTransport.Eval`, +etc. Link against `native/cubical/target/release/libtopolei_cubical.a` +in your own `moreLinkArgs` so the FFI symbols resolve. + +## Build + +```bash +(cd native/cubical && cargo build --release) +lake build +./.lake/build/bin/cubical-test # 62/62 tests pass +``` + +## Reference + +- `FFI_DESIGN.md` — C ABI contract. +- `FFI_COMPLETENESS.md` — per-function axiom audit. +- `KERNEL_BOUNDARY.md` — what this delivers in unmodified Lean 4 vs. + what would need upstream Lean kernel work. +- `NUMERICAL.md` — numerical implementation principles. +- `TRANSPORT_PLAN.md` — formalization plan (history of Phase 1). + +## Used by + +- [`max/topolei`](../topolei) — interactive cells-spec workspace + front-end built on this engine. diff --git a/REFERENCES.md b/REFERENCES.md deleted file mode 100644 index 1e17ff2..0000000 --- a/REFERENCES.md +++ /dev/null @@ -1,152 +0,0 @@ -# Topolei — References - -Papers and code referenced for implementation. Organized by subsystem. - ---- - -## Cubical Type Theory — Foundational Papers - -**CCHM — the primary reference for our cubical core.** -Cohen, Coquand, Huber, Mörtberg (2016) -"Cubical Type Theory: a constructive interpretation of the univalence axiom" -arXiv:1611.02108 -https://arxiv.org/abs/1611.02108 -> De Morgan interval algebra, face formulas, hcomp, transport, Glue types, univalence. -> This is the spec for Cells/Cubical/*.lean. - -**De Morgan Implementation Tutorial — how to actually build it.** -Mörtberg (2022) -"A tutorial on implementing De Morgan cubical type theory" -arXiv:2210.08232 -https://arxiv.org/abs/2210.08232 -> Type-checking algorithms, cofibration handling, evaluator structure. -> Closest thing to a recipe for our Eval.lean and Transport.lean. - -**ABCFHL — Cartesian cubical variant (alternative to de Morgan).** -Angiuli, Brunerie, Coquand, Favonia, Harper, Licata -"Syntax and Models of Cartesian Cubical Type Theory" -https://www.cs.cmu.edu/~rwh/papers/uniform/uniform.pdf -> Read if de Morgan interval causes problems with Lean's kernel; Cartesian -> variant has different composition rules that may embed more cleanly. - -**Univalence in cubical sets.** -Bezem, Coquand, Huber (2017) -arXiv:1710.10941 -https://arxiv.org/abs/1710.10941 - -**Axioms for cubical type theory in a topos.** -Orton, Pitts (2017) -arXiv:1712.04864 -https://arxiv.org/abs/1712.04864 - -**Unifying cubical and multimodal type theory.** -Aagaard, Kristensen, Gratzer, Birkedal (2022) -arXiv:2203.13000 -https://arxiv.org/abs/2203.13000 - ---- - -## Cubical Type Theory — Reference Code - -**cubicaltt — original Haskell implementation by Mörtberg et al.** -https://github.com/mortberg/cubicaltt -> Reference for hcomp algorithm, face formula solver, evaluator structure. -> Read the source, do not depend on it. - -**Agda Cubical Library — target architecture for our Lean embedding.** -https://github.com/agda/cubical -> Key files to read: -> Cubical/Core/Primitives.agda — interval, face, transport primitives -> Cubical/Foundations/Transport.agda — transport lemmas -> Cubical/Core/Glue.agda — Glue type and univalence -> We are reimplementing this structure in Lean 4 as a deep embedding. -> Do not import; use as architectural reference only. - -**Ground Zero — Lean 4 synthetic HoTT library.** -https://github.com/rzrn/ground_zero -> Shows how to avoid Lean's native equality and build HoTT synthetically in Lean 4. -> Read for: eliminator construction patterns, HIT techniques via quotients. -> cells-spec constraint: do not take it as a dependency. - ---- - -## EML — Exp-Minus-Log Binary Primitive - -**The EML paper. Primary computational reference for this project.** -Odrzywolek, Andrzej (2026) -"All elementary functions from a single operator" -arXiv:2603.21852 -https://arxiv.org/abs/2603.21852 -Local copy: exp-log.pdf -> eml(x,y) = exp(x) − ln(y) with constant 1 generates all elementary functions. -> Grammar: S → 1 | eml(S,S). Constructive proof for sin, cos, +, ×, π, etc. -> Foundation for H1. See HYPOTHESES.md. - ---- - -## Factorization Homology — FM^fr Notation Layer - -**The primer — best entry point.** -Ayala, Francis (2019) -"A factorization homology primer" -arXiv:1903.10961 -https://arxiv.org/abs/1903.10961 -> E_n-algebras, framed manifolds, ⊗-excision. Read this first for H4. - -**Original paper.** -Ayala, Francis (2012) -"Factorization homology of topological manifolds" -arXiv:1206.5522 -https://arxiv.org/abs/1206.5522 - -**Higher categories.** -Ayala, Francis (2015) -"Factorization homology I: higher categories" -arXiv:1504.04007 -https://arxiv.org/abs/1504.04007 - -**Stratified spaces (for mixed text/graph layouts).** -Ayala, Francis, Tanaka (2014) -"Factorization homology of stratified spaces" -arXiv:1409.0848 -https://arxiv.org/abs/1409.0848 -> Relevant when notation mixes 1D (text) and 2D (diagram) regions. - -**Traces in dimension 1 (for linear/sequential syntax).** -arXiv:2105.01143 -https://arxiv.org/abs/2105.01143 -> Circle-invariant traces; relevant for cyclic/recursive notation structures. - ---- - -## Verified Compiler / Shader IR - -**Lean4Lean — verified Lean typechecker in Lean. Pattern reference.** -Carneiro (2024) -"Lean4Lean: Verifying a Typechecker for Lean, in Lean" -arXiv:2403.14064 -https://arxiv.org/abs/2403.14064 -> Reference for: how to structure a verified evaluator/compiler in Lean 4. -> Our EML → ShaderIR compiler should follow similar patterns. - -**SPIR-V specification.** -Khronos Group -https://www.khronos.org/spirv/ -> Binary format target for native GPU path. Formal grammar maps onto our ShaderIR. - -**MLIR SPIR-V dialect — IR structure reference.** -https://mlir.llvm.org/docs/Dialects/SPIR-V/ -> Reference for what a typed shader IR looks like before binary encoding. -> Informs the design of Topolei/Shader/IR.lean. - ---- - -## Recommended Reading Order - -1. CCHM (1611.02108) — understand the cubical core we are embedding -2. De Morgan tutorial (2210.08232) — implementation recipe -3. cubicaltt source — see the evaluator and hcomp in action -4. Agda Cubical library — see the Lean-side target architecture -5. EML paper (2603.21852) — already read; revisit Sect. 3 for derivation tables -6. Ayala-Francis primer (1903.10961) — FM^fr foundation for H4 -7. Lean4Lean (2403.14064) — verified compiler patterns before writing ShaderIR diff --git a/RENDER_BRIDGE_GAP.md b/RENDER_BRIDGE_GAP.md deleted file mode 100644 index 16e30fa..0000000 --- a/RENDER_BRIDGE_GAP.md +++ /dev/null @@ -1,458 +0,0 @@ -# Render Bridge Gap — Formal Plan for N-Dimensional Projection Backend - -*Living document. Inventories the correctness gaps between the current -Lean spec + wgpu render pipeline and a complete formal rendering backend -for n-dimensional projection with cubical-transport semantics. Status as -of 2026-04-24.* - -## Short version - -The rendering pipeline **runs end-to-end** — `CVal`/`EMLPath` → GLSL → naga -→ SPIR-V → wgpu → Vulkan — but the **Lean-side formal model of the GPU** -(`Topolei/GPU/Spec.lean`) is narrower than what the pipeline actually -does. Specifically, the formal model assumes a pixel-shader that depends -only on `(PixelCoord, FrameUniforms = (time, resWidth, resHeight))`; the -pipeline also supports **a cubical path parameter** and (eventually) **an -n-dim scene encoded via `Projection n k`**. - -Closing the gap means extending the semantic model so that every stage -has a Lean-level counterpart whose correctness axiom constrains the Rust -implementation. Once the model is complete, any shader produced by -`compileEML`, any wgpu render call, any Rust rasterizer — all are -witnesses of the same Lean-level specification. - ---- - -## 1. The current pipeline and where it's unbridged - -``` -Lean │ Bridge (axiom/theorem) │ Rust / GPU -─────────────────────────┼─────────────────────────────────────┼───────────── -CVal (cubical value) │ cvalPathToEMLPath ← NOT WRITTEN │ — -EMLPath { dim, body } │ body.toGLSL ← concrete def │ — -String (GLSL source) │ compileEML ← AXIOM │ naga-glsl -ShaderHandle │ compileEML_correct ← AXIOM │ wgpu pipeline -ShaderSemantic │ render_faithful ← AXIOM stubbed │ Vulkan exec -PixelColor │ — │ framebuffer -``` - -Five rows, three gaps (NOT WRITTEN, AXIOM, stub). The pipeline works -*computationally* end-to-end; the gaps are where Lean's model stops -describing what the GPU actually does. - ---- - -## 2. `FrameUniforms` is too narrow - -**What the shader uses:** -```glsl -uniform float u_time; -uniform vec2 u_resolution; -// (generated locally, driven by u_time) -float t = 0.5 + 0.5 * sin(u_time * 0.7); -float px = (uv.x * 2.0 - 1.0) * xR; -float py = (uv.y * 2.0 - 1.0) * yR; -``` - -**What the model has:** -```lean -structure FrameUniforms where - time : Float - resWidth : Float - resHeight : Float - -def shaderVar (name : String) (p : PixelCoord) (u : FrameUniforms) : Float := - match name with - | "px" | "py" | "u_time" | "u_resWidth" | "u_resHeight" => … - | _ => 0.0 -``` - -**The gap:** `shaderVar "t" = 0.0` unconditionally. The existing theorem -`render_eq_at1` requires `shaderVar path.dimName = 1.0` — an -*uninhabited* hypothesis for parametric paths, so the theorem says -nothing about them. - -**The fix:** - -```lean -structure FrameUniforms where - time : Float - resWidth : Float - resHeight : Float - pathParam : Float := 0.0 -- NEW - -def shaderVarWithDim - (dimName : String) (name : String) - (p : PixelCoord) (u : FrameUniforms) : Float := - if name = dimName then u.pathParam - else shaderVar name p u -``` - -Then every EMLPath has a correctness theorem: - -```lean -theorem compileEMLPath_correct_at - (path : EMLPath) (p : PixelCoord) (u : FrameUniforms) - (h : u.pathParam = τ) : - (compileEMLPath path).semantic p u = - (path.body.toColorWithEnv - (shaderVarWithDim path.dimName · p { u with pathParam := τ })) -``` - -This is the pointwise statement that the shader output at path parameter -`τ` equals the EML body evaluated at `dimName ↦ τ`. - -**Scope:** ~60 lines in `GPU/Spec.lean`. No Rust changes required — this -is a specification pass. The existing Rust shader already behaves this -way; we're just saying so formally. - ---- - -## 3. The sine-sweep driver is unspecified - -**What the shader does:** `t = 0.5 + 0.5 * sin(u_time * 0.7)` — chosen on -the Lean side by `EML.lean`'s shader emitter, not axiomatised. - -**Why it matters:** the end-to-end correctness theorem we want is - -> "At any frame with `u.time = t₀`, the GPU pixel equals the EMLPath body -> evaluated at `dimName ↦ driver(t₀)`." - -Without an explicit `driver : Float → Float`, this theorem can't be -stated. The current model has `path.at1` (driver ≡ 1) and `path.at0` -(driver ≡ 0) — two points of the sweep. The sweep itself is implicit. - -**The fix:** - -```lean -structure PathDriver where - fn : Float → Float -- time → path param - range01 : ∀ t, 0 ≤ fn t ∧ fn t ≤ 1 -- stays in [0,1] - -def sineSweep (freq : Float) : PathDriver where - fn t := 0.5 + 0.5 * Float.sin (t * freq) - range01 := … -- IEEE Float axiom - -theorem rendered_pixel_at_time - (path : EMLPath) (driver : PathDriver) - (p : PixelCoord) (u : FrameUniforms) - (h : u.pathParam = driver.fn u.time) : - (compileEMLPath path).semantic p u = … -``` - -`sineSweep 0.7` becomes the specific driver the current Lean emitter -chooses. A renderer can swap drivers (linear sawtooth, ramp-and-hold, -externally controlled) without breaking the theorem — only the *driver -instance* changes. - -**Scope:** ~40 lines in `GPU/Spec.lean` + one IEEE Float axiom -(`Float.sin` ∈ `[-1, 1]`). - ---- - -## 4. `render_faithful` is `True` - -**The current axiom:** -```lean -axiom render_faithful (ctx : GPUContext) (h : ShaderHandle) (u : FrameUniforms) : - True -``` - -This says *nothing* — it's a placeholder for the theorem "when the render -loop runs with handle `h` under uniforms `u`, the pixel at coord `p` on -screen equals `h.semantic p u`." - -**The gap:** without a real body, there's no formal link between -`ShaderHandle.semantic` (what Lean *says* the shader computes) and actual -GPU pixel output (what ends up on screen). - -**The fix — two parts:** - -1. **A pixel-readback FFI**, added to `native/canvas-rs` (or C++-side - wrapper). Entry: `topolei_read_pixel(ctx, x, y) -> [f32; 4]`. Lean - side: - ```lean - @[extern "topolei_read_pixel"] - opaque readPixel (ctx : GPUContext) (x y : UInt32) : IO (Float × Float × Float × Float) - ``` - -2. **Replace `True` with a checkable theorem:** - ```lean - axiom render_faithful - (ctx : GPUContext) (h : ShaderHandle) (u : FrameUniforms) - (p : PixelCoord) : - let c := h.semantic p u - -- Rust-discharged: after a render cycle, readPixel matches `c` - True -- refine: ⟨c.r, c.g, c.b, c.a⟩ = readPixel (round p.x) (round p.y) - ``` - - Pixel-readback tests in the integration suite discharge it: compile a - known shader, render, read pixel, assert equal-within-ε to the - semantic value. - -**Scope:** ~60 lines C++/Rust + ~40 Lean. Closes the spec loop: after -this, a shader that evaluates wrong is caught by a test, not by human -inspection. - ---- - -## 5. `cvalPathToEMLPath` does not exist - -**The role:** bridge `CVal` (arbitrary cubical value) to `EMLPath` -(renderable parametric scalar field). Without it, only hand-crafted -`PlotConfig`s go through the render pipeline — arbitrary cubical -transports can't. - -**The signature:** -```lean -/-- Extract a renderable path from a cubical value. Succeeds when `v` - is a `vplam` closure whose body is expressible as an EMLExpr - (built from Float-valued variables, `exp`, `log`, and `1.0`). -/ -def cvalPathToEMLPath : CVal → Option EMLPath - | .vplam env i body => Option.map (EMLPath.mk i.name) (ctermToEML env body) - | _ => none -``` - -where `ctermToEML : CEnv → CTerm → Option EMLExpr` is the restricted -translator: it supports `var` (free), `app` only for known builtins -(`Float.exp`, `Float.log`), `plam`/`papp` for dim handling, rejects -anything else. Returns `none` for out-of-scope terms. - -**Correctness theorem (wanted):** -```lean -theorem cvalPathToEMLPath_at_endpoint - (v : CVal) (path : EMLPath) - (h : cvalPathToEMLPath v = some path) - (baseEnv : String → Float) : - -- value-level: what vPApp of `v` at endpoint-0 evaluates to - -- matches the EML at0 computation - … = path.at0 baseEnv -``` - -and similarly for `at1`. - -**Why this matters:** after this bridge, the demo is **any cubical path -`Path A a₀ a₁` in the scalar-field universe → continuous GPU animation -of the 1-cell** — no manual EML construction. Writing a transport in -Lean and running `renderCVal` renders it. - -**Scope:** ~120 lines Lean + ~80 for the correctness theorems. No Rust. -Entirely a Lean-internal translator. - ---- - -## 6. N-dim scenes don't exist in the GPU spec - -**What we have in Lean:** -- `ProjPoint n` — homogeneous coords for ℝℙⁿ. -- `Projection n k` — `apply`, `undefinedLocus`, `horizonImage`. -- `ObservationPrimitive n k` — atomic or fractal-split. -- `Camera = atom Projection ViewAugment` (via `Observation.lean`). - -**What the GPU spec has:** -- Nothing about `ProjPoint`, `Projection`, or `ObservationPrimitive`. -- `ShaderSemantic = PixelCoord → FrameUniforms → PixelColor` — - strictly 2-in, 1-out. - -**The gap (three layers):** - -### 6a. Projection compilation - -We need `compileProjection : Projection n k → ShaderFragmentHandle`. -Running the projection on the GPU should fold the n-dim coords through -the chart's map (stereographic, gnomonic, perspective) into the output -coords, and report the `horizonImage` predicate as a uniform. - -**Representation at pixel level:** the final screen is still `PixelCoord`, -but the *intermediate* ProjPoint values need GPU representation. Options: -(a) marshal n-dim coords as an `Array Float` uniform buffer, -(b) specialise shaders per-n at compile time, -(c) fix `n ≤ 4` and use `vec4` slots. - -Option (c) matches graphics conventions and is probably the right first -pass: ℝℙ⁰, ℝℙ¹, ℝℙ², ℝℙ³, ℝℙ⁴ all fit in `vec4`. - -### 6b. Observation compilation - -```lean -compileObservation : ObservationPrimitive n k → ShaderHandle - | .atom proj aug => compileAtomic proj aug - | .compose children => compileBranch children -``` - -The tree structure of `compose` becomes a series of GPU branch tests: -at each pixel, evaluate each child's scene-region predicate and -dispatch. Atomic observations compile to one pass; composite ones to a -flat chain of predicate-gated passes. - -**Correctness:** `(compileObservation obs).semantic p u` equals the -pointwise result of walking `obs`'s tree on the scene point -`projToScene u.pathParam p`. - -### 6c. Camera chain - -A camera chain is `View n := Projection n 2` built by -`Projection.compose`. On the GPU: -- A chain of 4D → 3D → 2D projections is a chain of shader fragments. -- Each fragment consumes the previous's output (a `vec4` of homogeneous - coords). -- The last one writes `PixelColor`. - -Compilation: `compileView : Projection n 2 → ViewAugment n → ShaderHandle`. -Correctness: the chain's `apply` composition ↔ the shader pipeline's -composition. - -**Scope:** Substantial. ~400 Lean lines + ~500 Rust for the -marshalling and shader synthesis. Probably 2–3 focused sessions. - ---- - -## 7. Float semantics axioms — still carry a debt - -The current pipeline relies on ~8 IEEE 754 axioms scattered across -`GPU/Spec.lean` and `Render/ProjScene.lean`: `Float.log_one`, -`Float.max_one_ge_eps`, `Float.sub_zero`, `Float.one_ne_zero_beq`, -`Float.neg_one_mul_neg_one`, `Float.one_mul`, `Float.mul_assoc`. - -**Status:** uncontroversial by construction, but each is an unverified -assumption about the GPU's Float behaviour. Rust's `f32` is bit-exact -IEEE; Vulkan's driver *mostly* is but allows some fast-math modes. - -**The closure:** bundle these into one module (`Topolei/Float/IEEE.lean`) -with a comment stating the driver-level expectations, and add one more: -`Float.sin_range : ∀ t, -1 ≤ Float.sin t ≤ 1` (needed for `sineSweep`). - -**Scope:** ~20 lines consolidation + 1 new axiom. - ---- - -## 8. The `topolei_run2` two-panel path is unimplemented - -`canvasRun2` (side-by-side two-shader panel) was handled by canvas.cpp's -`topolei_run2_internal` via OpenGL `glScissor`. The wgpu port's -`topolei_run2` currently falls back to rendering only the left panel. - -**The fix:** one render pass with two sub-pipelines and a scissor rect -per panel, matching the C++ logic. wgpu's -`RenderPass::set_scissor_rect(x, y, w, h)` is the equivalent of -`glScissor`. - -**Scope:** ~120 lines Rust. No Lean changes — the FFI signature is -already in place. - ---- - -## 9. `canvas.cpp` + `CMakeLists.txt` should be retired - -Once the formal bridge is closed and the wgpu path is proved -equivalent, the OpenGL canvas can be deleted from the tree. Until then, -they remain as: - -- a working baseline if the wgpu path regresses; -- reference for the `glScissor` logic we need to port to #8. - -**Scope:** `git rm` + update of `native/include/` + any doc crossrefs, -once done. - ---- - -## 10. Render-crate static-link collision - -`libtopolei_render.a` and `libtopolei_canvas.a` both statically embed -the Rust runtime (`rust_eh_personality`, `std::sys::args`, etc.). -Linking both causes multi-definition errors. - -**Current state:** render crate is unlinked (nothing in Lean calls it). - -**Options to restore render linkage:** -- **(A) Merge render into canvas-rs.** One crate, one Rust runtime. - Simplest; preferred if the render crate's planned SDF rasterizer and - `compileEML` Rust impl can coexist with the canvas code. -- **(B) Convert one to `cdylib`.** Links dynamically at runtime; may - complicate Lean-wasm composite target. -- **(C) `-Wl,--allow-multiple-definition`.** Works but brittle — the - two runtimes may differ in subtle ways. - -Option (A) is recommended. Fold `native/render/` into `native/canvas-rs/` -as a `render` module; re-wire `@[extern]` names. - -**Scope:** ~2 hours of file-moving and symbol renaming. - ---- - -## 11. Nix-store paths in `lakefile.toml` are hardcoded - -Current `lakefile.toml` contains paths like -`/nix/store/wb6rhpznjfczwlwx23zmdrrw74bayxw4-glibc-2.42-47/lib/...` -baked in for: -- glibc (`libc.so.6` + `ld-linux-x86-64.so.2`, for `__tls_get_addr`) -- vulkan-loader -- libX11, libxcb, libxkbcommon -- libXcursor, libXi, libXrandr, libXext, nix-ld - -These break on any machine with different nix-store hashes. - -**The fix:** generate them from `pkg-config` or `nix-shell`'s -`NIX_LDFLAGS` at build time. A small `build.sh` preprocessing pass that -populates `lakefile.toml` from a template using `pkg-config --libs`. - -**Scope:** ~80 lines bash + one `lakefile.toml.in` template. Doesn't -change Lean or Rust. - ---- - -## 12. WebGPU / wasm32 target - -The cubical crate already builds for `wasm32-unknown-unknown` -(see `native/cubical/WASM.md`). Canvas does not. - -**What it needs:** -- Compile `topolei-canvas` with `--target wasm32-unknown-unknown`. -- `winit` wasm feature + `wgpu-web`. -- Lean-wasm composite artifact (per cells-spec §4). - -**Blockers at this layer:** -- `std::fs` / threading assumptions must be feature-gated. -- winit's `any_thread(true)` is X11-specific; for wasm, the event loop - is browser-driven. - -**Scope:** 1–2 full sessions. Depends on the cubical-crate's wasm -harness already working. - ---- - -## 13. Summary table of work by item - -| # | Component | Lines | Lean | Rust | Session count | -|----|------------------------------------|------:|:----:|:----:|:-------------:| -| 2 | `FrameUniforms.pathParam` | 60 | ✓ | | 0.5 | -| 3 | `PathDriver` + `sineSweep` | 40 | ✓ | | 0.3 | -| 4 | `render_faithful` + `readPixel` | 100 | ✓ | ✓ | 1 | -| 5 | `cvalPathToEMLPath` | 200 | ✓ | | 1–2 | -| 6 | Projection / Observation → GPU | 900 | ✓ | ✓ | 3–4 | -| 7 | Float axiom consolidation | 20 | ✓ | | 0.2 | -| 8 | `topolei_run2` scissor panes | 120 | | ✓ | 0.5 | -| 9 | Retire canvas.cpp | — | | | 0.1 | -| 10 | Merge render into canvas-rs | — | | ✓ | 0.5 | -| 11 | Dynamic nix paths in lakefile | 80 | | | 0.3 | -| 12 | wasm32 target | 150 | | ✓ | 1–2 | - -**Critical path to a complete n-dim backend:** items 2 → 3 → 4 → 5 → 6. -Items 7–12 are polish / portability / cleanup. - ---- - -## 14. Design principle to keep - -Every row in the table in §1 that currently reads AXIOM or NOT WRITTEN -should resolve to **either a theorem (provable in Lean) or a `@[extern]` -opaque with `@[implemented_by]` wiring** (a Rust function whose -correctness is specified by remaining axioms about its behaviour). In -all cases the Lean kernel reasons through *specs*, not Rust. Rust -provides *speed and effect*; correctness stays in Lean. - -Any time we're tempted to shortcut by making something computable via -a Float constant or a hardcoded value (a "driver without spec", a -"shader without semantic axiom", a "projection without composition -theorem"), that's a bridge gap in the making. Adding the spec up front -is cheaper than forensic reconstruction later. diff --git a/STATUS.md b/STATUS.md deleted file mode 100644 index 5ffa898..0000000 --- a/STATUS.md +++ /dev/null @@ -1,1163 +0,0 @@ -# Topolei — Formal Status - -*Last updated: 2026-04-24 (Phase 1 closed; Stages 1–4 delivered; -Rust backend Phases A–D all shipped. Rust cubical evaluator is -live in both native and wasm32 builds, wired via @[implemented_by], -verified by 55/55 smoke+property tests and baseline benchmarks.)* - ---- - -## Resume guide (start here on a new session) - -This document combines architecture, axiom inventory, historical logs, and -the next-action menu. For resuming work, read in this order: - -1. **Architecture at a glance** (immediately below) — the Lean-as-host / - Rust-as-FFI-backend framing. Skip if already familiar. -2. **What is done** (jump to `## What is done` heading) — current module - inventory. -3. **Axiom discharge obligations** (`## Axiom discharge obligations`) — - the live list of what Rust must compute. -4. **Priority order** (`## Priority order`) — the menu of next-action - entry points across Streams A/B/C, with ✅ marks on completed items. -5. **Concrete "next session" suggestions** (last section) — curated - single/multi-session candidates. - -Skip the per-week historical logs (the long block of "Phase 1 Week N" -sub-sections) unless tracing a specific past decision. - -**Process discipline (do not violate):** -- Lean-first, Rust-second: every Rust-implemented behavior is *first* - stated in Lean as an axiom, *then* discharged. Adding eval/readback - axioms in Lean is the right move; writing Rust ahead of the spec is not. -- Face-disjoint axiom partitions: when extending a sub-case (e.g. Glue - transport), add new axioms with a precondition disjoint from existing - ones rather than rewriting the existing axiom. This keeps `transp_ua` - and similar consumers stable. -- NbE-first reductions: prefer eval-level + readback-level axioms over - step-level. After Stage 2.3 (subject reduction), **T3 and C4 are - theorems**, not step-level axioms; only T4 (`transp_plam_is_plam`) - remains as a syntactic fallback — all other step-level axioms are gone. -- **Axiom provenance discipline (Stage 3):** every axiom carries one of - three provenance tags — *Rust-discharge* (evaluator implementation), - *Lean-discharge* (future Lean proof from inductive definition), or - *IEEE/external* (Float / GPU spec). See "Axiom provenance spectrum" - section below. - ---- - -## Architecture at a glance - -**Topolei is a Lean 4 extension that adds cubical-transport HoTT to Lean 4 -via a Rust FFI module.** The process discipline is: - -1. **Formalize in Lean first.** Every layer that will eventually be - implemented in Rust — the cubical evaluator's reduction rules, the GPU - runtime's IO behavior, numerical kernels' input-output specs — is - *first* stated in Lean as axioms and data structures. No Rust is - written until the Lean-side axiom set is stable. -2. **Discharge via FFI later.** Rust functions, exposed through a C ABI, - are linked back into Lean via `@[extern]` + `@[implemented_by]`. Each - axiom becomes a kernel-trusted computation. -3. **Phase 1 (Cubical Core) is closed in Lean.** The six-week cubical - specification (Interval, Face, Syntax, Subst, DimLine, Typing, Value, - Eval, Transport, Comp, Equiv, Glue, Soundness, plus the EML and - GPU/Spec bridges) is complete with zero Rust dependency. Its axiom - set is the contract the Rust FFI will eventually satisfy. -4. **Many Lean-side phases don't need Rust.** Phase 2 (Cells), Phase 3a - (Reactive), Phase 3b (Color), Phase 4 Shader IR/emit, Phase 5b - Boundary/Security models, and Phase 6 Meta can all proceed as more - Lean — extending the axiom base. The Rust FFI unblocks *execution* - (GPU, window, OS), not *formalization*. - -**The one Rust component has two distinct surfaces within it:** - -- **(a) Cubical evaluator backend** — Rust implements `step`, `eval`, - `vApp`, `vPApp`, `vTransp`, `vHCompValue`, `vCompAtTerm`, `vCompNAtTerm` - + the face-disjoint reduction rules for transp/comp/glue. Gives Lean's - kernel the ability to *reduce* cubical terms to values. **This is the - defining purpose of topolei's Rust FFI** — it exists to extend Lean 4 - with computational cubical-transport HoTT. -- **(b) GPU / effectful runtime** — wgpu-backed GPU context, shader - upload/dispatch, window/input, OS primitives. Hosted in the same - Rust crate for build convenience; distinct FFI surface. Specs live - in `GPU/Spec.lean` + Phase 5 Runtime modules. - -**Zigzag engine — Lean, not Rust.** The combinatorial n-category -engine (normalisation, degeneracy theory, type-checking against -signatures) is being ported into Lean. The Rust reference at -`zigzag-engine/` is a port-from template, not a dependency. See -`ZIGZAG_PORT.md` for the full 10-step plan. This preserves the -Lean-as-host discipline: reasoning inside Lean about n-categorical -normalisation requires structural access, which FFI hides. - -**Numerical scheme discipline:** see `NUMERICAL.md` for the -context-from-content separation principle applied to numerical -implementations. Every scheme is a pair (mathematical content × -execution context); they must not leak into each other, and §10 -defines the minimum contract for first-class registry status. - ---- - -## Audit summary - -Post-review pass addressed a kernel-level soundness issue and cleaned up -abstractions across Phase 1. Changes: - -- **Soundness:** `CTerm.step` is now `opaque` (was a concrete `def` with an - identity wildcard arm). Every axiom of the form `step (.transp …) = …` or - `step (.comp …) = …` was silently inconsistent with `CTerm.noConfusion` - under the old definition; opacity restores consistency. The path β-rule - is stated as an explicit axiom `step_papp_plam`. -- **CCHM alignment:** dropped the non-CCHM axiom `transp_as_comp` (T3) and - the misleading `transp_id_from_comp` derived theorem. `comp_top_const_id` - now requires `CTerm.dimAbsent i t = true` and is reproved via `comp_full` + - `substDimBool_of_absent`, with no appeal to T3. -- **Dead code:** collapsed identical `if j = i` branches in - `CTerm.substDim` (Syntax) and `CTerm.dimAbsent` (DimLine). Removed - tautology theorems `substDim_false_is_at0`/`substDim_true_is_at1`. -- **Abstraction:** removed useless hypotheses on `comp_full` - (`(φ : FaceFormula) (hφ : φ = .top)`) and `comp_empty` - (`Entails .bot .bot`). `comp_full_typed` now *actually* uses `comp_full` - (states typing for `substDimBool L.binder true u`, not for `step`). - `path_at_face_zero`/`one` removed (they took unused face hypotheses; - the unconditional `CTerm.step_papp_zero`/`one` in Syntax already - capture the content). `typing_plam_boundaries` renamed to - `plam_boundaries` and stripped of its unused typing hypothesis. - `System.typed_bot_vacuous` renamed to `System.typed_bot`. - -### GPU bridge pass - -- **Soundness (GPU side):** the old `compile_correct` axiom universally - quantified over both `expr` and `h`, so instantiating with two different - exprs at a shared `h` forced `expr₁.toColor = expr₂.toColor`. Replaced with - an abstract compile function `compileEML : EMLExpr → ShaderHandle` and a - per-handle correctness axiom `compileEML_correct`. -- **Rendering bridge:** added `EMLPath.evalAt_body_eq_at1` (scalar), - `EMLPath.toColor_body_eq_at1_toColor` (color), and `render_eq_at1` - (end-to-end). These close the cubical → Float → color → pixel chain. -- **Plot paths:** added `PlotConfig.dimName` and `PlotConfig.toEMLPath` in - EML/Path.lean. `plotExp` and `plotLn` are proved *constant paths* (their - bodies don't mention `"t"`); a genuinely parametric `plotExpT` demo was - added with proved endpoint values. -- **Last sorry discharged:** `evalAt_expOf` now closes against three IEEE 754 - Float axioms (`Float.log_one`, `Float.max_one_ge_eps`, `Float.sub_zero`). - -### Phase 1 Week 2: evaluator (cells-spec §5.4) - -- **`Cubical/Value.lean`:** mutual `CEnv` / `CVal` / `CNeu` inductives - (named-variable adaptation of the spec's de-Bruijn `Env`/`Val`/`Neutral`). - `CVal` covers `vlam` closures, `vplam` dim-closures, and embedded `CNeu`; - `CNeu` covers free variables, stuck (p)apps, and stuck transport/comp. -- **`Cubical/Eval.lean`:** mutual `partial def` `eval` / `vApp` / `vPApp` - covering the λ-calculus fragment plus dimension application (β-reduces - via `CTerm.substDim`). Transport and composition produce `vneu` stuck - values (real reduction rules are Phase 1 Weeks 3–4). Eleven axiom - equations mirror the match arms so theorems can reduce through `eval` - without relying on `partial def`'s opaque kernel form — same pattern as - `opaque CTerm.step` + `step_papp_plam`. -- **`Cubical/EvalTest.lean`:** eight roundtrip tests — free variable, - identity β, constant-function β, K combinator (two β-reductions), stuck - application, path-abstraction closure, path β via `substDim`, transport - and composition neutralisation. All proved from the axiom equations. - -### Phase 1 Week 3: transport at eval level (cells-spec §5.5) - -**Full CCHM Π transport is implemented** — both constant-domain and -varying-domain cases. Delivers the "transport along refl = id" test -target (§13 Week 3) and the per-type-former Π rule. - -#### Prerequisite: CType substitution by general DimExpr - -- **`Cubical/Subst.lean`:** added `CType.substDimExpr : DimVar → DimExpr → CType → CType` - — generalises the existing Bool-only `CType.substDim` to arbitrary - `DimExpr`. Needed to build the reversed line `A[i := inv i]` for - inverse transport. Reduction lemmas `substDimExpr_univ/pi/path` and the - bridge `substDim_eq_substDimExpr` relating the two. -- **`Cubical/DimLine.lean`:** generalised `CTerm.substDim_of_absent` to - arbitrary `DimExpr` (the proof is the same as the old Bool case); added - `CType.substDimExpr_of_absent`. The old `substDimBool_of_absent` is now - a corollary. - -#### Value-level machinery - -- **`Cubical/Value.lean`:** `CVal.vTranspFun` now carries *both* domain - and codomain: `vTranspFun : DimVar → CType → CType → FaceFormula → - CVal → CVal`. A single uniform constructor handles the full CCHM Π - rule; the constant-domain specialisation falls out automatically from - `vTranspInv_const`. -- **`Cubical/Transport.lean`:** - - `vTransp i A φ v` dispatches in four priority-ordered arms: - 1. `φ = .top` → `v` (T1). - 2. `CType.dimAbsent i A = true` → `v` (T2). - 3. `A = pi domA codA` → `vTranspFun i domA codA φ v` (**unified - full CCHM Π rule** — no longer gated by `dimAbsent i domA`). - 4. Otherwise → stuck `vneu (.ntransp i A φ v)`. - - `vTranspInv i A φ v := vTransp i (A.substDimExpr i (.inv (.var i))) φ v` - — inverse transport as forward transport along the reversed line. - - Theorem `vTranspInv_const`: when `CType.dimAbsent i A = true`, the - reversed line equals the original (by `substDimExpr_of_absent`), so - `vTranspInv` reduces to `v` via T2. - - Theorem `vTranspInv_top`: `vTranspInv i A .top v = v`. - - Four axioms: `vTransp_top`, `vTransp_const`, `vTransp_pi` (new; - replaces the earlier `vTransp_pi_dom_const`), `vTransp_stuck` - (simplified precondition: "A is not a pi" replaces the earlier - "not a pi-with-constant-domain"). - -#### CCHM Π β-rule at the value level - -- **`Cubical/Eval.lean`:** `vApp` on a transported function now performs - the full three-step CCHM reduction: - ``` - vApp (.vTranspFun i domA codA φ f) arg = - vTransp i codA φ (vApp f (vTranspInv i domA φ arg)) - ``` - i.e. inverse-transport through the domain, apply the function, - forward-transport through the codomain. The axiom `vApp_vTranspFun` - is updated to match. Derived theorem `eval_transp_pi` replaces the - earlier `eval_transp_pi_dom_const`. - -#### Tests - -- `eval_transp_top_id`, `eval_transp_const_univ`, `eval_transp_const_pi` - — Week 3 "transport along refl = id". -- `eval_transp_pi_const_dom_example` — const-domain Π transport produces - `vTranspFun i .univ codA φ f`. -- `vApp_vTranspFun_const_dom_reduces` — applying it reduces through - `vTranspInv_const` (identity) + outer `vTransp_stuck`. -- `eval_transp_pi_varying_dom` — **varying-domain** Π transport also - produces `vTranspFun` (same constructor, no special case). -- `vApp_vTranspFun_varying_dom_reduces` — applying it cascades three - stuck neutrals: the inverse transport through the varying domain, the - stuck application, and the stuck forward transport through the - varying codomain. - -### Correctness audit - -The axiom set is provably disjoint by preconditions (top / const / pi / -stuck-non-pi), so no two axioms can fire with contradictory conclusions -on the same input. The unified `vTranspFun` constructor is -semantically well-behaved at every edge case: - -- **Fully constant line** (caught by arm 2): `vTranspFun` isn't produced; - if manually constructed, `vApp` reduces it to `vApp f y` via the - two-sided `vTransp_const`/`vTranspInv_const` collapse. -- **Constant domain, varying codomain**: `vTranspInv` reduces to - identity, recovering the simpler formula `vTransp i codA φ (vApp f arg)`. -- **Varying domain**: the full CCHM three-step reduction runs; when - pieces stall, they stall consistently into nested `ntransp`/`napp` - neutrals. -- **Under `φ = .top`** (if manually constructed): reduces to `vApp f y` - via `vTransp_top` + `vTranspInv_top` — correct, since identity - transport of a function is the function. - -### Phase 1 Week 4: composition at eval level - -Heterogeneous composition at eval level plus homogeneous composition on -Π types — CCHM §5.6 delivered with working reductions for the cases that -don't require Glue. - -#### Value-level machinery - -- **`Cubical/Value.lean`:** two new `CVal` constructors + one `CNeu`: - - `vHCompFun : CType → FaceFormula → CVal → CVal → CVal` — result of - `hcomp (pi A B) φ tube base` at the value level. No domain stored - (homogeneous comp doesn't transport through the domain). - - `vTubeApp : CVal → CVal → CVal` — represents `λj. (tube @ j) arg` - as a dim-abstraction value. Needed to thread the outer hcomp's - tube into the inner hcomp on the codomain. - - `CNeu.nhcomp : CType → FaceFormula → CVal → CVal → CNeu` — stuck - hcomp (separate from `ncomp` because the type is fixed, not a line). - -#### Evaluator extensions - -- **`Cubical/Eval.lean`:** mutual block extended with two partial defs: - - `vHCompValue A φ tube base` — homogeneous composition on a fixed - type. Three disjoint arms: `.top → vPApp tube .one`, `.pi → vHCompFun`, - stuck otherwise. - - `vCompAtTerm env i A φ u t` — heterogeneous composition at the *term - level*. Takes `u` and `t` as `CTerm`s so that the `comp_full` case - can substitute `i := 1` before evaluating (this is genuinely different - from `vPApp`-ing the evaluated `u`). Four disjoint arms: - `.top → eval env (u[i := 1])` (C1), `.bot → eval env (.transp i A .bot t)` (C2), - `dimAbsent i A → vHCompValue A φ (vplam env i u) (eval env t)` - (hetero-comp ≡ hcomp on constant lines), stuck otherwise. - - `eval`'s `.comp` arm now routes through `vCompAtTerm`. - - `vApp` gains a `vHCompFun` case unfolding per the CCHM Π hcomp rule: - `vApp (.vHCompFun codA φ tube base) arg = vHCompValue codA φ (.vTubeApp tube arg) (vApp base arg)`. - - `vPApp` gains a `vTubeApp` case: `vPApp (.vTubeApp tube arg) r = vApp (vPApp tube r) arg`. - - Exhaustiveness cases for `vApp` on `vTubeApp` (type error) and - `vPApp` on `vHCompFun` (type error). - -#### Axioms (all disjoint by precondition) - -Old coarse `eval_comp` axiom replaced by four disjoint case-axioms: - - `eval_comp_top` — C1: `eval env (u.substDim i .one)`. - - `eval_comp_bot` — C2: `eval env (.transp i A .bot t)`. - - `eval_comp_const` — `dimAbsent i A → vHCompValue A φ (vplam env i u) (eval env t)`. - - `eval_comp_stuck` — fallback. - -Three `vHCompValue_*`: `vHCompValue_top`, `vHCompValue_pi`, `vHCompValue_stuck`. -One each for `vApp` on `vHCompFun` (`vApp_vHCompFun`) and `vPApp` on -`vTubeApp` (`vPApp_vTubeApp`). - -#### Tests - -- `eval_comp_top_example` — C1 with a body with no dim dep → stripped to neutral. -- `eval_comp_top_dim_subst` — C1 with body `papp (var "p") i`; the `i := 1` - substitution hits `DimExpr.subst (var i)` which collapses to `DimExpr.one`, - yielding `npapp (nvar "p") .one`. **Exercises real dim substitution.** -- `eval_comp_bot_univ` — C2 chained with T2: `comp i .univ .bot u t` → `eval t`. -- `eval_comp_const_line` — constant-line comp reduces to hcomp; on `.univ` - the hcomp stalls into `nhcomp`. -- `eval_comp_neu` — stuck comp form (free vars, non-const non-endpoint face). -- `vHCompValue_top_reduces` — `hcomp .top` on a vplam tube returns the - tube-body at dim 1. -- `vApp_vHCompFun_reduces` — **CCHM Π hcomp β-rule** runs with neutral - tube/base/argument; cascades cleanly into `nhcomp` + `napp` neutrals. -- `vPApp_vTubeApp_reduces` — `(λj. tube@j arg) @ r` reduces to - `(tube@r) arg` using a vplam tube. - -### C1 / C2 now discharged at eval level - -`eval_comp_top` and `eval_comp_bot` are the eval-level analogues of the -step-level axioms `comp_full` (C1) and `comp_empty` (C2). Once a -`step ↔ eval` bridge is built, the step-level axioms can be derived from -these — one less foundational axiom each. - -### Phase 1 Week 4+: Path transport (cells-spec §5.5 Path case) - -Path transport of a path element `p : Path A(0) a(0) b(0)` along a line -`i. Path A(i) a(i) b(i)` produces an element of `Path A(1) a(1) b(1)`. -CCHM's rule uses a 3-clause heterogeneous comp with the system -`[φ ↦ p@j, j=0 ↦ a(i), j=1 ↦ b(i)]`. Full multi-clause comp is an -involved refactor; this pass delivers the **endpoint-accurate -reductions** (which cover the common case of evaluating paths at their -boundaries) and a **structured stuck form** for generic-dim applications -that preserves the transport data for possible future unsticking. - -#### Value-level machinery - -- **`Cubical/Value.lean`:** - - `CVal.vPathTransp : CEnv → DimVar → CType → CTerm → CTerm → FaceFormula → CVal → CVal` - — value-level representation of `transp^i (Path A(i) a(i) b(i)) φ p`. - Stores env (for later substDim on `a`, `b`), binder, base type, - endpoint CTerms, face, and the already-evaluated path value. - - `CNeu.npathTranspApp : ... → DimExpr → CNeu` — generic-dim stall, - structurally preserves the full transport data. - -#### Evaluator refactor - -- **`Cubical/Eval.lean`:** eval's `.transp` arm now dispatches in four - priority-ordered arms: - 1. `φ = .top` → `eval env t` (T1, unconditional). - 2. `CType.dimAbsent i A = true` → `eval env t` (T2, covers constant - lines of *any* head — univ, pi, path, sigma-when-we-have-it). - 3. `A = .path A₀ a b` (line varies) → `vPathTransp` closure. - 4. Otherwise → value-level `vTransp` (which handles `.pi` via - `vTranspFun`). - `vPApp` gains three new arms for `vPathTransp`: - · At `.zero` → `eval env (a.substDim i .one)` (= a(1) from CCHM - (j=0) clause). - · At `.one` → `eval env (b.substDim i .one)` (= b(1) from (j=1)). - · At generic DimExpr → `vneu (.npathTranspApp …)`. - `vApp` gains a `vPathTransp` case (type error — path values aren't - functions). - -#### Axioms (all disjoint by precondition) - -Old coarse `eval_transp` axiom replaced by four disjoint case-axioms: - - `eval_transp_top` (φ = .top). - - `eval_transp_const` (φ ≠ .top ∧ dimAbsent i A). - - `eval_transp_path` (φ ≠ .top ∧ path-line varies). - - `eval_transp_nonpath` (φ ≠ .top ∧ non-constant non-path). - -Three axioms for `vPApp` on `vPathTransp`: `vPApp_vPathTransp_zero`, -`vPApp_vPathTransp_one`, `vPApp_vPathTransp_stuck` (r ≠ .zero ∧ r ≠ .one). - -#### Tests - -- `eval_transp_path_example` — transport along a varying path line - `path univ (var "a_line") (papp (var "b_pt") i)` produces the - expected `vPathTransp` closure. -- `vPApp_vPathTransp_zero_reduces` — endpoint at `.zero` yields `a(1)`. - Exercises `substDim i .one` on a constant endpoint term. -- `vPApp_vPathTransp_one_reduces` — endpoint at `.one` yields `b(1)`. - The `.papp (.var "b_pt") (DimExpr.var i)` endpoint has its `i` replaced - with `.one`, producing `npapp (nvar "b_pt") .one` — **real CCHM - endpoint correction firing through multiple layers**. -- `vPApp_vPathTransp_stuck_reduces` — at a fresh dim var, stalls at - `npathTranspApp` preserving all transport data. -- `eval_transp_constant_path` — when the path line is fully constant, - T2 fires and the transport is identity (no `vPathTransp` produced). - -### Correctness audit for path transport - -- **Axiom disjointness** — eval's four `.transp` axioms have preconditions - that partition the input space: `.top` / (non-top ∧ const) / (non-top - ∧ non-const ∧ path) / (non-top ∧ non-const ∧ non-path). Similarly the - three `vPApp vPathTransp` axioms partition by `.zero`/`.one`/else. -- **Endpoint correctness** — the CCHM `(j=0)` clause specifies the value - at `j=0` to be `a(i)`; at the lid `i=1`, this is `a(1)`. Not - `transp^i(a(0))`, which is what a naïve approach would compute. Our - axiom directly gives the CCHM-correct value `eval env (a.substDim i .one)`. -- **Constant-line short-circuit** — when `dimAbsent i A = true` for - `A = .path A₀ a b`, CCHM transport is identity (T2). Our eval checks - this before the path routing, so no `vPathTransp` is constructed and - the result is plainly `eval env t`. Avoids producing a `vPathTransp` - that would need additional reduction machinery to recognize as identity. -- **φ = .top short-circuit** — similarly, the top face is caught before - path routing, so transport under `.top` is identity (T1) for path - types as for any other type. No `vPathTransp` is produced. -- **Stall preservation** — at generic dims, the `npathTranspApp` neutral - stores env, binder, types, endpoints, face, and path value. A later - substitution of a fresh dim with an endpoint can be propagated through - this neutral via the endpoint axioms — no data is lost. - -### Phase 1 Week 4++: Multi-clause comp + full path transport - -The **full CCHM machinery** for path transport is now in place — generic-dim -applications no longer stall blindly but produce a multi-clause `compN` that -genuinely unsticks when any clause face resolves to `.top` (via the new -`FaceFormula.dimExprEq0/1` helpers). - -#### New machinery - -**`Cubical/Face.lean`**: -- `FaceFormula.dimAbsent` (moved up from DimLine so it's visible at Face level). -- `FaceFormula.dimExprEq0 / dimExprEq1 : DimExpr → FaceFormula` — encoding - "r = 0" and "r = 1" as face formulas for composite DimExprs. Mutual - recursion (inv swaps them; meet/join de Morgan-dualise). - `_eval` theorems prove semantic correctness. -- `FaceFormula.substDim : DimVar → DimExpr → FaceFormula → FaceFormula` — - general substitution. Uses `dimExprEq0/1` for `eq0 j[j:=r]`/`eq1 j[j:=r]`. -- Supporting theorems: `substDim_zero/substDim_one` (agree with `restrict`), - `substDim_of_absent`, `substDim_comm`, `dimExprEq0_dimAbsent`/`dimExprEq1_dimAbsent`, - `dimExprEq0_substDim_of_absent`/`dimExprEq1_substDim_of_absent`, - `dimAbsent_after_substDim`. - -**`Cubical/Interval.lean`**: `DimExpr.dimAbsent` relocated here. - -**`Cubical/Syntax.lean`**: -- New CTerm constructor `compN : DimVar → CType → List (FaceFormula × CTerm) → CTerm → CTerm`. -- `CTerm.substDim` extended for `compN` via a mutual helper - `substDim.clauses` (so Lean can see structural termination through the - clause list). -- `transp` and `comp` now substitute into `FaceFormula` too (were previously - no-ops on `φ` — an approximation that could produce subtle bugs with - nested dim substitutions). - -**`Cubical/DimLine.lean`**: -- `CTerm.dimAbsent` extended for `compN`, mutual helper `dimAbsent.clauses`. -- `CTerm.substDim_absent_aux`, `CTerm.dimAbsent_after_substDim_aux`, - `CTerm.substDim_comm_aux` all extended to handle `compN` via mutual - helpers for the clause-list case. -- Transp/comp proofs updated for the new face-formula substitution. - -**`Cubical/Value.lean`**: -- `CNeu.ncompN` — stuck multi-clause comp neutral. -- `CVal.vPathTransp` changed to store the path as a `CTerm` (not CVal) — - needed so `.papp p r` terms can be constructed for the multi-clause - reduction. -- `CNeu.npathTranspApp` removed (no longer produced — path transport now - always reduces to `vCompNAtTerm`, which handles the stuck case itself). - -**`Cubical/Eval.lean`**: -- Mutual block now has **six** partial defs: `eval`, `vApp`, `vPApp`, - `vHCompValue`, `vCompAtTerm`, `vCompNAtTerm`. -- `eval`'s `.compN` arm delegates to `vCompNAtTerm`. -- `vPApp` on `vPathTransp` at a generic DimExpr now routes through - `vCompNAtTerm` with the CCHM 3-clause system - `[(φ, p@r), (dimExprEq0 r, a), (dimExprEq1 r, b)]` and base `p@r`. - **This is the genuine CCHM path transport reduction, not a stall.** -- `vCompNAtTerm` priority-order arms: - · forcing `.top` clause anywhere in list → fires (substDim i .one in body). - · empty list → reduces to plain transport (C2). - · single live clause → delegates to `vCompAtTerm`. - · else → stuck `ncompN`. -- New axioms: `eval_compN`, `vCompNAtTerm_def` (compound axiom exposing - the full case analysis — users pattern-match on clause list). -- `vPApp_vPathTransp_general` replaces the old `vPApp_vPathTransp_stuck` - (now uses `vCompNAtTerm` reduction). - -#### New tests - -- `eval_compN_empty` — empty system compN reduces to transport. -- `eval_compN_top_fires` — top-clause at head fires (C1-like). -- `eval_compN_top_later` — top-clause deeper in list still fires - (`find?` scans past non-top clauses). -- `vPApp_vPathTransp_inv_zero` — **key correctness test**: applying path - transport at the composite DimExpr `.inv .zero` (semantically = 1) - exercises: - 1. `dimExprEq0 (inv 0)` computes via the de Morgan case to - `dimExprEq1 .zero = .bot`. - 2. `dimExprEq1 (inv 0)` computes to `dimExprEq0 .zero = .top`. - 3. compN system `[(φ, _), (.bot, a), (.top, b)]` scanned by `find?`. - 4. `.top`-faced clause fires → returns `b(1)` = `b.substDim i .one`. - 5. `pathLine_b.substDim i_dim .one` = `papp (var "b_pt") .one`. - 6. Evaluates to `vneu (npapp (nvar "b_pt") .one)`. - This is real CCHM reduction chaining through every new piece. - -### Correctness audit for this pass - -- **`FaceFormula.substDim` preserves semantic equality with `restrict` - at endpoints** — `substDim_zero/one = restrict false/true`. Full - semantic correctness proven via `dimExprEq0_eval` / `dimExprEq1_eval` - theorems (`dimExprEq0 r` evaluates to true iff `r` evaluates to false). -- **compN clause-list substitution commutes with disjoint dim vars** — - via `CTerm.substDim.clauses_comm_aux` (mutual with the CTerm case). -- **`.compN` dim-absence preserved by substitution** — via - `CTerm.dimAbsent.clauses_after_substDim` (mutual). -- **Face formulas now substituted in `.transp` and `.comp`** — closes a - prior gap where `transp i A φ t`'s dim-substitution left `φ` unchanged - even when `φ` mentioned the substituted var. -- **Path transport at endpoints still via specific axioms** - (`vPApp_vPathTransp_zero/one`) — semantic-equivalent shortcut to the - general compN reduction, skipping list-scan overhead. - -### Phase 1 Week 5: Equivalences and Glue (cells-spec §5.7–§5.8) - -**Equiv** (`Cubical/Equiv.lean`): - -- `EquivData` — half-adjoint equivalence structure (HoTT §4.2) carrying - `f`, `fInv`, `sec`, `ret`, `coh` as raw `CTerm`s rather than a Σ-type - (sigma types aren't in `CType` yet; `fiberTy` / contractibility-of-fibers - deferred). -- `idEquiv : CType → EquivData` — trivial equivalence on any type; - uses reserved hygienic names `"$x"`, `⟨"$e"⟩`, `⟨"$eo"⟩`, `⟨"$ei"⟩` - for its bound vars (consistent with `"$y"`/`⟨"$fj"⟩` in `vApp vCompFun`). -- Five typing theorems proving `idEquiv` components are well-typed in any - context at their *propositionally-reduced* types (e.g. `sec : A → Path A x x` - rather than `A → Path A (f (fInv x)) x`). Same compromise as the - non-dependent Π restriction — β-equivalence isn't part of `HasType`. - -**Glue** (`Cubical/Glue.lean` + extensions): - -- New `CType` constructor `.glue (φ : FaceFormula) (T : CType) - (f fInv sec ret coh : CTerm) (A : CType)` — equivalence's five CTerms - inlined directly so the `CType`/`CTerm` mutual block remains closed - (not dependent on `EquivData`). -- New `CTerm` constructors `.glueIn (φ t a)` and `.unglue (φ f g)`. -- Threaded through the full pipeline: `CTerm.substDim` extended for both - new term constructors; `CType.substDim` / `CType.substDimExpr` / all - four `_aux` mutual defs in `DimLine.lean` extended for `.glue` on the - CType side and `.glueIn`/`.unglue` on the CTerm side. -- Two new `CNeu` constructors: `nglueIn` and `nunglue` — structured stuck - forms preserving both face-on and face-off evaluated sub-values, so - later dim substitution into `φ` can unstick them. -- `eval` on `.glueIn` / `.unglue` dispatches by face priority (top/bot/ - stuck), mirrored by six face-disjoint axioms: - - `eval_glueIn_top` — `.glueIn .top t a → eval t`. - - `eval_glueIn_bot` — `.glueIn .bot t a → eval a`. - - `eval_glueIn_stuck` — non-top-non-bot produces `nglueIn`. - - `eval_unglue_top` — `.unglue .top f g → vApp f g` (forward map). - - `eval_unglue_bot` — `.unglue .bot f g → eval g` (identity). - - `eval_unglue_stuck` — non-top-non-bot produces `nunglue`. -- `EquivData.toGlueType (φ T e A)` — ergonomic wrapper inlining `e`'s - five CTerms into the `.glue` constructor. -- `uaLine e A B : DimExpr → CType` — single-face CCHM univalence line - `Glue [dimExprEq0 r ↦ (A, e)] B`. At `r = .zero`, `dimExprEq0 .zero = .top`, - so the glue is `A` via `e`; at `r = .one`, `dimExprEq0 .one = .bot`, so - the glue is `B`. Not a two-face symmetric ua (`Glue [i=0 ↦ (A, e), - i=1 ↦ (B, idEquiv)] B`); semantically equivalent but simpler. -- Endpoint rfl-lemmas `uaLine_zero / one / var`, plus computational - content theorems `uaLine_zero/one_glueIn/unglue_reduces` that derive - A-behaviour at `r=0` and B-behaviour at `r=1` from the face-disjoint - axioms. - -### Correctness audit for Week 5 - -- **Axiom disjointness** — glueIn's three axioms partition by `.top` / - `.bot` / (non-top ∧ non-bot); unglue's three axioms similarly. No two - axioms for the same head can fire together. -- **Structured stucks** — `nglueIn` / `nunglue` preserve sub-values as - `CVal`s (not erased to neutrals). When dim substitution resolves the - stored `φ` to `.top` or `.bot`, a later pass can unstick by re-evaluating - through the endpoint axioms. -- **`CType.glue` in substitution machinery** — extended for `substDim`, - `substDimExpr`, `substDim_of_absent`, `substDimExpr_of_absent`, - `dimAbsent_after_substDim`, and `substDim_comm_aux`. All five threads - uniformly descend into the 5 inlined CTerms + T + A + φ. -- **Partial: Glue transport** — a restricted-form axiom - `eval_transp_glue_const_at_bot` (in `Glue.lean`) covers the case of an - empty outer face, constant-in-i fiber/base/equivalence components, and - inner glue face collapsing to `.bot` at `i := 1`. This discharges - `transp_ua` as a *theorem* (not axiom). The generalisation to varying - equivalences, varying base type, and non-`.bot` outer faces remains - Week 6+ work — extending this one axiom into a full CCHM §6.2 rule is - the first step toward a complete Glue reduction system. -- **Deferred: multi-face Glue** — a `.glueN` with a list of `(φ_k, T_k, e_k)` - clauses would let us write the full two-face CCHM ua. Single-face is - semantically equivalent and enough for Phase 1. - -### Phase 1 Week 6 (2026-04-23): Glue transport — constant-component fragment - -**`transp_ua` is now a theorem.** Glue transport is closed for the -"constant-non-φ-components" case — equivalences supplied from outside the -transport binder — modulo a single future-work gap (the `φ[i:=1] = .top` -sub-case, which needs the equivalence's half-adjoint hcomp construction). - -Changes: - -- **Eval.lean — `.transp` dispatch on `.glue`.** The `eval` partial def's - `.transp` arm now has a dedicated `.glue` case that produces a stuck - neutral as a runtime placeholder (the same shape `vTransp` would have - produced for glue). This separates `.glue` from the `_ => vTransp` - fallback so the new Glue-specific axioms don't collide with - `eval_transp_nonpath`. -- **Eval.lean — `eval_transp_nonpath` restricted.** Added an `h_not_glue` - precondition excluding `.glue` type heads. Without this, the new Glue - axioms would directly contradict `eval_transp_nonpath` + `vTransp_stuck` - on the `.glue` case. Downstream theorems `eval_transp_pi`, - `eval_transp_stuck`, and `EvalTest.eval_transp_neu` all propagate the - precondition. -- **Glue.lean — `eval_transp_glue_const_at_bot`.** When all non-φ - components are dim-absent from `i`, the outer face `ψ ≠ .top`, and the - inner face restricted to `i := 1` collapses to `.bot`, glue transport - reduces to `transp i A ψ (unglue (φ[i:=0]) f t)`. In the `uaLine` - setting (constant `A`, constant equivalence, `φ = eq0 i`), this chains - through `eval_transp_const` (T2) and `eval_unglue_top` to yield `e.f t`. -- **Glue.lean — `eval_transp_glue_const_stuck`.** When the inner face - restricted to `i := 1` is neither `.top` nor `.bot` (so the glueIn at - `i = 1` has a non-trivial face and the reduction can't collapse), the - result is a structured stuck neutral preserving the full glue data, - matching the partial def's runtime behavior. Disjoint from - `_at_bot` by the `φ[i:=1] ≠ .bot` precondition. - -**Case-analysis status for constant non-φ components:** - -| `φ.substDim i .one` | status | axiom / mechanism | -|---------------------|-------------|----------------------------------------------| -| `.bot` | ✅ covered | `eval_transp_glue_const_at_bot` | -| `.top` | ✅ covered | `eval_transp_glue_const_at_top` (Stream B #1a, 2026-04-23) | -| otherwise | ✅ covered | `eval_transp_glue_const_stuck` (stuck form) | - -**Constant-component fragment is closed.** All three sub-cases of -`φ.substDim i .one` are face-disjoint axioms with documented discharge -obligations on the Rust backend. `transp_ua` and the new -`transp_ua_inverse` (Soundness.lean) demonstrate the `_at_bot` and -`_at_top` axioms firing through the canonical univalence-line shapes. - -**New proof obligation on the Rust backend:** the full CCHM §6.2 Glue -transport formula, whose restriction to each covered sub-case must -produce the stated RHS. In particular the `_at_top` axiom states the -T-side witness as the *naïve* `app fInv (transp i A ψ (unglue …))`; the -full CCHM formula adds an hcomp-in-T correction using `sec` to enforce -ψ-boundary agreement, which trivialises under the constant-component -hypothesis up to the equivalence's propositional coherence (a -well-typedness obligation the caller already discharges). - -**Remaining Glue-transport generalisations** (require more machinery): -- Varying base type `A`: replace the outer transport with a genuine - comp/fill through the base line (CCHM §6.2's first fill). -- Varying equivalence components `f(i)`, `fInv(i)`, etc.: use - `transp^i T` to transport witnesses along the equivalence line. -- Hcomp-correction wrapper around the `_at_top` RHS: requires T-side - hcomp infrastructure to state the formula precisely. - -### Phase 1 Week 7 (2026-04-23): step↔eval bridge — Session 1 - -**Goal of the bridge.** The project currently carries two parallel -axiom sets: step-level (T1–T5, C1/C2/C4, `step_papp_plam`) and eval-level -(`eval_*` mirrors of the partial def). They must agree but nothing -enforces this. The bridge defines `step` as a normalisation-by-evaluation -composition — `step t := readback 0 (eval .nil t)` — so step-level axioms -become theorems derivable from eval-level axioms alone. Net effect: ~8 -fundamental axioms (step-level) become derivable, and the Rust backend's -obligation list halves. - -**Why multiple sessions.** The bridge requires (a) defining readback -(NbE reification) as a partial def, (b) proving a readback/eval -correspondence lemma, (c) redefining step and deriving each existing -step axiom as a theorem. Each piece is substantial; this is a five-session -plan. - -**Session 1 (this session) — `Topolei/Cubical/Readback.lean` landed:** - -- `partial def readback : Nat → CVal → CTerm` and - `partial def readbackNeu : Nat → CNeu → CTerm` in a mutual block, - covering every `CVal` and `CNeu` constructor (including the cubical - closures — `vTranspFun`, `vCompFun`, `vHCompFun`, `vTubeApp`, - `vPathTransp` — and every neutral shape). -- Depth-indexed fresh-name generation: `$rb_` for term binders, - `$rd_` for dim binders. Extends the `$`-prefixed hygiene - convention already used by the evaluator. -- Correct α-renaming semantics for `vlam`: extend env at the *original* - binder name mapped to `vneu (nvar fresh)`, so body lookups return the - fresh neutral. (First implementation attempt extended at the fresh - name, yielding `λx. x ↝ λ$rb_0. x` — caught by the test suite.) -- Face-disjoint reduction axioms mirroring every partial-def arm, same - pattern as `eval_*` in Eval.lean. -- `CTerm.readback : CTerm → CTerm := readback 0 ∘ eval .nil` — the - convenience wrapper that Session 3 will use to redefine step. -- Three sanity tests in `ReadbackTest`: free variable (`var_readback`), - identity function (`id_lambda_readback`), constant function - (`const_fn_readback`). All exercise the axioms end-to-end and verify - α-renaming works correctly. - -**Sessions 2–4 delivered (same day):** - -| Session | Delivered | -|---------|-----------| -| 2 ✅ | Switched from depth-indexed fresh-name generation (`$rb_`) to **original-binder preservation** — env shadowing handles capture, which means `readback (eval .nil t) = t` for closed normal forms in the λ-fragment. Axioms simplified (no depth parameter). Correspondence lemmas `readback_nvar`, `CTerm.readback_var`, `CTerm.readback_lam`, `CTerm.readback_plam` prove readback/eval behavior on canonical λ-fragment forms. | -| 3 ✅ | Derived five step-level axioms as theorems under NbE: `CTerm.readback_papp_plam` (path β), `readback_transp_id` (T1), `readback_transp_const_id` (T2), `readback_comp_full` (C1), `readback_comp_empty` (C2). Each is a one-to-three-step `rw` chain through the corresponding eval-level axiom. | -| 4 ✅ | Partial T4 coverage: `readback_transp_plam_top` (full face) and `readback_transp_plam_const` (constant line) — the two cases where NbE reduces through. General T4 (arbitrary line/face) and T3/T5/C4 are documented as requiring machinery beyond NbE reification (typing-preservation for T3/C4, face-formula normalisation for T5, richer `vPathTransp` readback for general T4). | - -**Session 5 (deferred — independent refactor):** The five NbE-derivable -step axioms are now provably redundant, but physical removal from the -codebase requires refactoring clients of `step_papp_plam` -(`step_papp_zero`, `step_papp_one`, `plam_witnesses_boundaries`) and of -the derived step-level theorems in TransportLaws/CompLaws. That -refactor is decoupled from the bridge's soundness benefit — the bridge -works as-is; removal just shortens the source. - -**Net Week 7 result**: Rust's obligation list is down from "step axioms -+ eval axioms + readback axioms" to "eval axioms + readback axioms + -three semantic step axioms (T3, T5, C4) that truly are beyond NbE". -Five step axioms eliminated from the fundamental obligation list. See -the updated Axiom discharge obligations table below. - -## What is done - -### Cubical core (all 8 steps of TRANSPORT_PLAN.md) - -| File | Contents | State | -|------|----------|-------| -| `Cubical/Interval.lean` | `DimVar`, `DimExpr`, de Morgan algebra, `DimExpr.subst`, `eval_subst`. **Stage 4.1**: `DimExpr.normalize` + `normalize_preserves_eval` + literal-reduction lemmas (`.inv .zero → .one`, `.inv .one → .zero`, double-negation). `deriving Inhabited` added (for FFI `opaque` requirement). | ✅ Stage 4.1 | -| `Cubical/Face.lean` | `FaceFormula`, face lattice laws, `restrict`, `Entails`, endpoint lemmas. **Stage 4.3**: `FaceFormula.normalize` + `normalize_preserves_eval` covering the meet/join × top/bot absorption laws — ensures deterministic face dispatch for Rust. `deriving Inhabited` added. | ✅ Stage 4.3 | -| `Cubical/Syntax.lean` | `CType` (`univ`/`pi`/`path`/**`sigma`**/**`glue`**) and `CTerm` (λ-fragment + transp/comp/compN + `glueIn`/`unglue` + **`pair`/`fst`/`snd`**) mutual inductives, `substDim`, **opaque** `step`. **Stage 1.2**: Σ types added. **Stage 4.4**: step-representation decision documented — Rust may implement natively (Option A) or derive as `readback ∘ eval .nil` (Option B); both satisfy the T4 axiom. | ✅ Stages 1.2 + 4.4 | -| `Cubical/Line.lean` | **NEW (Stage 1.1)**. `DimLine.inv` (reversed line), `DimLine.concat` (CCHM universe-hcomp, axiomatic), `transp_concat` theorem (cells-spec §14 Critical), `transp_concat_const_{left,right}` unit-law corollaries. Explicit Rust-discharge vs Lean-discharge provenance discipline codified. | ✅ Stage 1.1 | -| `Cubical/ValueTyping.lean` | **NEW (Stage 2.3)**. Semantic typing relations `HasVal : CVal → CType → Prop` / `HasNeu` / `EnvHasType` (opaque stubs, full inductive definition is Lean-discharge future work). Preservation axioms: `eval_preserves_type`, `readback_preserves_type`, `EnvHasType.nil`, consolidated `CTerm.step_preserves_type`. **T3 and C4 are now theorems** derived from `step_preserves_type` — axiom scales O(1) in type-formers, not O(n). | ✅ Stage 2.3 | -| `Cubical/FFI.lean` | **NEW (Stage 4.6)**. `@[extern]` `opaque` declarations for every Rust cubical-HoTT entry point: `evalRust`, `vAppRust`, `vPAppRust`, `vTranspRust`, `vHCompValueRust`, `vCompAtTermRust`, `vCompNAtTermRust`, `vFstRust`, `vSndRust`, `readbackRust`, `readbackNeuRust`, `stepRust`, `DimExprNormalizeRust`, `FaceFormulaNormalizeRust`. Companion doc: `FFI_DESIGN.md` (memory + marshalling) + `FFI_COMPLETENESS.md` (axiom audit). | ✅ Stage 4.6 | -| `Cubical/Subst.lean` | `CTerm.substDimBool`, `CType.substDim` (Bool), `CType.substDimExpr` (general DimExpr) — **extended for `.glue`** — `substDim_eq_substDimExpr` bridge, reduction lemmas | ✅ complete | -| `Cubical/DimLine.lean` | `DimLine`, endpoints `at0`/`at1`, `dimAbsent` (extended for `.glue`/`.glueIn`/`.unglue`), general-DimExpr absent-subst, Bool-case corollaries, `substDim_idem`, `substDim_comm` — all four `_aux` mutual defs extended | ✅ complete | -| `Cubical/Typing.lean` | `HasType` judgment (var/lam/app/plam/papp/transp/comp), inversion lemmas, `weaken`, `plam_boundaries` | ✅ complete | -| `Cubical/TransportLaws.lean` | **Stage 2.3**: T3 `transp_step_preserves` is now a *theorem* derived from `CTerm.step_preserves_type` (ValueTyping.lean). **Stage 4.2**: T4 `transp_plam_is_plam` replaced by constructive `transp_plam_is_plam_path` — path-restricted with explicit body `.plam j (.compN i A [(φ, body), (.eq0 j, a), (.eq1 j, b)] body)` mirroring `readback_vPathTransp_plam`. Rust discharges concretely. Derived: `transp_plam_body_path`, `transp_plam_body_path_eq`, `transp_plam_step_typed_path`. | ✅ Stages 2.3 + 4.2 | -| `Cubical/System.lean` | `System`, `CompatAt0`, compat lemmas, `System.Valid`, `HasType.comp_of_valid` bridge | ✅ complete | -| `Cubical/CompLaws.lean` | **Stage 2.3**: C4 `comp_step_preserves` is now a *theorem* derived from `CTerm.step_preserves_type`. Zero axioms in this file. C3 intentionally dropped (see file header). | ✅ Stage 2.3 | -| `Cubical/Value.lean` | Mutual `CEnv` / `CVal` / `CNeu` extended with `nglueIn`/`nunglue` + **`vpair`** (CVal) + **`nfst`/`nsnd`** (CNeu), `CEnv.lookup`/`extend`, `Inhabited` instances. **Stage 1.2**: Σ values added. | ✅ Stage 1.2 | -| `Cubical/Eval.lean` | Mutual block of 6 partial defs: `eval`, `vApp`, `vPApp`, `vHCompValue`, `vCompAtTerm`, `vCompNAtTerm`. λ-fragment + plam/papp + priority-ordered `.transp` dispatch (top / const / path / **glue (stuck placeholder)** / non-path-non-glue via `vTransp`) + `vCompAtTerm` for `.comp` + `vCompNAtTerm` for `.compN` + face-disjoint dispatch for `.glueIn` (top/bot/stuck) and `.unglue` (top/bot/stuck). `vApp` performs full CCHM Π β-rules on `vTranspFun`, `vHCompFun`, and `vCompFun`. `vPApp` reduces `vPathTransp` at `.zero`/`.one` endpoints via CCHM multi-clause (j=0/j=1) logic and routes generic dims through `vCompNAtTerm`. `eval_transp_nonpath` now carries an `h_not_glue` precondition so the Glue transport axioms (in `Glue.lean`) have an uncontested slot. | ✅ complete | -| `Cubical/Readback.lean` | Mutual `readback : CVal → CTerm` / `readbackNeu : CNeu → CTerm` as partial defs covering every CVal/CNeu constructor. **Original-binder preservation** — env shadowing provides capture-avoidance without needing fresh names. Face-disjoint `readback_*` / `readbackNeu_*` axioms mirroring every arm; `vPathTransp` arm splits face-disjointly on `.plam` input (Stream B #2c). `CTerm.readback` wrapper for NbE composition. **Seven NbE theorems** (`readback_papp_plam`, `readback_transp_id`, `readback_transp_const_id`, `readback_comp_full`, `readback_comp_empty`, `readback_transp_plam_path`, `readback_transp_face_congr`) derive the corresponding step-level axioms; `readback_transp_plam_general` combines `_top` / `_const` / `_path` to cover every well-typed path-line input shape. Correspondence lemmas for λ-fragment canonical forms. `ReadbackTest` sanity tests. **Step↔eval bridge Sessions 1–4 + Stream B #2b/#2c.** | ✅ Week 7 + Stream B #2b/#2c | -| `Cubical/Equiv.lean` | Half-adjoint `EquivData` (f/fInv/sec/ret/coh as CTerms), `idEquiv : CType → EquivData`, rfl-projection lemmas, five typing theorems (`hasType_f/fInv/sec_refl/ret_refl/coh_refl_refl`) at propositionally-reduced types | ✅ Week 5 | -| `Cubical/Glue.lean` | `EquivData.toGlueType` wrapper, `uaLine` (single-face CCHM ua), endpoint rfl-lemmas, computational content theorems, `idEquiv` specialisations. **Glue-transport axiom family (9 total):** 6 constant-equivalence axioms partitioning `{const-A, varying-A} × {bot, top, stuck}` + 2 **hcomp-corrected `_at_top` variants** (Stage 2.1 — full CCHM §6.2 formula with hcomp-in-T using `ret` for definitional ψ-boundary agreement) + 1 **varying-equivalence stuck axiom** (Stage 2.4 — structural stuck form when any of f/fInv/sec/ret/coh varies in i). `eval_transp_glue_const_at_top_from_hcomp` derives the naked form from the hcomp form at ψ = .bot. | ✅ Stages 2.1 + 2.4 | -| `Cubical/Transport.lean` | `vTransp` (4 priority-ordered arms, unified Π); `vTranspInv` (line reversal); `vTranspInv_const` / `vTranspInv_top` theorems; four `vTransp` axioms + `vTranspLine` wrapper | ✅ Weeks 3–4 (full Π) | -| `Cubical/EvalTest.lean` | Tests: λ-fragment β, path β, stuck forms, Week 3 transport-reduction cases, Π-const-domain and Π-varying-domain transport β-unfoldings, Week 4 hetero-comp reductions (C1/C2/const-line), Π hcomp β-rule, vTubeApp reductions | ✅ complete | - -### Soundness theorems (Stages 1.3 + 2.3) - -| Theorem | Statement | State | -|---------|-----------|-------| -| `Soundness.glue_beta` | `eval env (.unglue φ f (.glueIn φ t a)) = eval env a` under overlap `h_overlap : eval env (.app f t) = eval env a` | **Stage 1.3**: promoted from axiom to theorem. Follows from `eval_unglue_of_glueIn`. | -| `Soundness.glue_eta` | `eval env (.glueIn φ t (.unglue φ f g)) = eval env g` under overlap `h_overlap : eval env t = eval env (.app f g)` | **Stage 1.3**: promoted from axiom to theorem. Follows from `eval_glueIn_of_unglue`. | -| `Soundness.transp_ua` | transport along `uaLine e A B (.var i)` at ψ = .bot = `vApp (eval e.f) (eval t)` | Proved — uses `eval_transp_glue_const_at_bot`. | -| `Soundness.transp_ua_inverse` | transport along `uaLine e A B (.inv (.var i))` at ψ = .bot = `vApp (eval e.fInv) (eval t)` | Proved — uses `eval_transp_glue_const_at_top`. | -| `TransportLaws.transp_step_preserves` (T3) | `HasType Γ t L.at0 → HasType Γ (CTerm.step (.transp L.binder L.body φ t)) L.at1` | **Stage 2.3**: promoted from axiom to theorem. Follows from `HasType.transp` + `CTerm.step_preserves_type`. | -| `CompLaws.comp_step_preserves` (C4) | `HasType Γ t₀ L.at0 → HasType Γ u L.at1 → (compat) → HasType Γ (CTerm.step (.comp L.binder L.body φ u t₀)) L.at1` | **Stage 2.3**: promoted from axiom to theorem. Follows from `HasType.comp` + `CTerm.step_preserves_type`. | -| `Line.transp_concat` | `vTranspLine (concat L M h) .bot v = vTranspLine M .bot (vTranspLine L .bot v)` | **Stage 1.1** (cells-spec §14 Critical, previously unstated). Restatement of the eval-level `vTranspLine_concat` axiom. | -| `Line.transp_concat_const_{left,right}` | unit-law corollaries via T2 | **Stage 1.1**. | - -### EML and GPU layer - -| File | Contents | State | -|------|----------|-------| -| `EML.lean` | `EMLExpr` AST, GLSL codegen, `PlotConfig`, demo expressions | ✅ complete | -| `EML/Path.lean` | `boolToFloat`, `EMLExpr.evalWithEnv`, `EMLPath`, `at0`/`at1`, `const_endpoints`, `expPath` example | ✅ complete | -| `GPU/Spec.lean` | Opaque GPU types, `EMLExpr.evalAt`/`toColor`, `compileEML` + per-handle `compileEML_correct` axiom, `render_faithful` stub, IEEE Float axioms, EMLPath rendering bridge (`evalAt_body_eq_at1`, `toColor_body_eq_at1_toColor`, `render_eq_at1`), `evalAt_expOf` discharged | ✅ complete | -| `Canvas.lean` | FFI bindings for GLFW/OpenGL (`canvasRun`, `canvasRun2`) | ✅ complete | - ---- - -## Open sorries - -None. `evalAt_expOf` is now discharged against three IEEE 754 Float axioms -(`Float.log_one`, `Float.max_one_ge_eps`, `Float.sub_zero`) that serve as -verification obligations on the Rust FFI runtime. - ---- - -## Deferred formal work - -### Dependent Π types (`Typing.lean`, comment) - -`HasType` has non-dependent `pi A B` only. Full dependent `(x:A) → B x` requires a -term evaluator (to apply `B` to a term). Noted as deferred. - -### Full `φ`-substitution in `CTerm.substDim` (`Syntax.lean`, comment) - -**RESOLVED** (Phase 1 Week 4++): `CTerm.substDim` now substitutes into face -formulas via `FaceFormula.substDim`. The approximation note no longer -applies. - ---- - -## Phase 1 Week 4+++: Heterogeneous Π composition (fully reduced) - -The previously-stuck heterogeneous Π comp case now reduces via a -CCHM-faithful fill-based β-rule. - -### Machinery - -- **`CVal.vCompFun : CEnv → DimVar → CType → CType → FaceFormula → CTerm → CTerm → CVal`** — - closure storing env, line binder, domA, codA, face, and tube/base CTerms. -- **`vCompAtTerm`**'s `.pi`-with-varying-line arm produces `vCompFun` instead - of the prior stuck `ncomp` neutral. -- **`vApp` on `vCompFun`** runs the CCHM fill β-rule: - - Construct `y_at_i = transp^{fj} (A[i := (inv fj) ∨ i]) φ y` — the fill. - - Construct `y_at_0 = transp^{fj} (A[i := inv fj]) φ y` — inverse-endpoint. - - Inner comp: `comp^i B(i) φ (u y_at_i) (t y_at_0)`. - - Evaluated in env extended with reserved `"$y"` bound to the argument. -- Reserved names `"$y"` (arg) and `⟨"$fj"⟩` (fill dim) are documented - hygiene assumptions. - -### Fill endpoint correctness (verified in comments) - -- At `fj = 0`: line-slice is `A[i := 1 ∨ i] = A(1)`. Source correct (`y : A(1)`). ✓ -- At `fj = 1`: line-slice is `A[i := 0 ∨ i] = A(i)`. Target correct. ✓ -- At `i = 0`: reduces to the inverse-transport formula. ✓ -- At `i = 1`: line collapses to `A(1)` constant, T2 gives `y` as expected. ✓ - -### Degenerate case: const domain - -When `dimAbsent i domA = true`, `substDimExpr` is identity, the transport -line in the fill is constant, T2 fires and the fills reduce to `y`. The -β-rule collapses to `comp^i B(i) φ (u y) (t y)` — no special-case logic -needed, just by following the reductions. - -### Axioms - -- `eval_comp_pi` — routes `comp^i (pi A B) φ u t` (non-top, non-bot, - varying line) to `vCompFun`. -- `vApp_vCompFun` — the full β-rule as an equation. -- `eval_comp_stuck` gets a new `h_not_pi` precondition (excluded by - `eval_comp_pi` for any pi-headed type). - -### Tests - -- `eval_comp_pi_example` — hetero comp on a varying pi produces `vCompFun`. -- `vApp_vCompFun_const_dom_example` — const-domain β fires. -- `vApp_vCompFun_varying_dom_fires` — varying-domain β fires, producing - the genuinely non-trivial fill terms. -- Spot-check `CType.substDimExpr` computes the expected composite - domain type `path univ "a0" (papp "p" ((inv fj) ∨ i))`. - ---- - -## Axiom discharge obligations - -These are intentional axioms — formal specs for what the Rust evaluator -(linked via `@[extern]` + `@[implemented_by]`) must compute. Topolei is -structured as a Lean 4 extension: the external-to-Lean layer is always -*specified as axioms in Lean first*, then *discharged* by the Rust module. -Each becomes a proof obligation when implementation is written. - -### Cubical evaluator (TransportLaws + CompLaws + Readback) - -The Phase 1 Week 7 step↔eval bridge promoted five step-level axioms to -NbE theorems in `Cubical/Readback.lean`. Stream B #2d (Session 5 -cleanup, 2026-04-23) **physically removed** those five axioms and their -client cascade — they no longer appear as axioms in the Lean source. - -| Axiom (former) | Obligation | Current status | -|----------------|------------|----------------| -| `step_papp_plam` | `step ((⟨i⟩ t) @ r) = t[i := r]` | ✅ Removed; theorem `CTerm.readback_papp_plam` | -| T1 `transp_id` | `transpⁱ A 1_F t` reduces to `t` | ✅ Removed; theorem `CTerm.readback_transp_id` | -| T2 `transp_const_id` | transport under constant (i-absent) type is identity | ✅ Removed; theorem `CTerm.readback_transp_const_id` | -| C1 `comp_full` | `comp ... 1_F u t₀` reduces to `u[i:=1]` | ✅ Removed; theorem `CTerm.readback_comp_full` | -| C2 `comp_empty` | `comp ... 0_F u t₀` reduces same as `transp ... 0_F t₀` | ✅ Removed; theorem `CTerm.readback_comp_empty` | - -| Axiom (residual) | Obligation | Status | -|------------------|------------|--------| -| T3 `transp_step_preserves` | transport is type-safe (subject reduction) | ⚠️ Stays step-level; needs typing-preservation machinery (Stream B #2a) | -| T4 `transp_plam_is_plam` | transport of `⟨j⟩ body` produces `⟨j⟩ body'` | ✅ NbE coverage complete for path-typed lines via `readback_transp_plam_general` (combines `_top` / `_const` / `_path`); see `Cubical/Readback.lean`. Non-path varying lines are vacuous in well-typed code (Stream B #2c, 2026-04-23). Step axiom retained for syntactic-only consumers. | -| T5 `transp_face_congr` | transport inspects face formulas only by truth value | ✅ Promoted to eval-level axiom `eval_transp_face_congr` in `Eval.lean`; NbE theorem `CTerm.readback_transp_face_congr` in `Readback.lean`. Step-level axiom removed (Stream B #2b, 2026-04-23). | -| C4 `comp_step_preserves` | composition is type-safe | ⚠️ Stays step-level; same typing-preservation gap as T3 | - -**Remaining true obligations on Rust for the cubical evaluator**: -- All eval-level equations (mirroring `eval` / `vApp` / `vPApp` / `vTransp` - / `vHCompValue` / `vCompAtTerm` / `vCompNAtTerm` arms). -- All readback-level equations (mirroring `readback` / `readbackNeu` arms). -- Glue-transport axioms (`eval_transp_glue_const_at_bot` / `_at_top` / `_stuck`). -- The six glueIn/unglue face-disjoint axioms. -- The eval-level T5 axiom `eval_transp_face_congr` (face normalisation). -- The two residual step-level axioms T3 and C4 (subject reduction — - needs typing-preservation machinery, Stream B #2a). -- The step-level T4 `transp_plam_is_plam` retained as a syntactic - fallback (NbE coverage for path-typed lines is complete via - `readback_transp_plam_general`). - -*(CCHM C3 — "transport is a specialised composition" — is intentionally -not stated here; see the note at the top of `CompLaws.lean`. It only holds -with side-conditions that would duplicate `transp_const_id` in content, and -will be recovered via the real `transp = hcomp + fill` reduction when the -evaluator is written in Phase 1 Week 4.)* - -### Evaluator equations (Eval.lean + Transport.lean + Glue.lean) - -Axioms mirroring the `partial def` arms of `eval`, `vApp`, `vPApp`: -`eval_var`, `eval_lam`, `eval_app`, `eval_plam`, `eval_papp`, `eval_transp`, -`eval_comp`, `vApp_vlam`, `vApp_vneu`, `vPApp_vplam`, `vPApp_vneu`. Plus -three for `vTransp`: `vTransp_top`, `vTransp_const`, `vTransp_stuck`. All -will become provable theorems once the evaluator is rewritten as a total -function with a proper termination measure. `eval_transp_top`, -`eval_transp_const`, `eval_transp_stuck` are already theorems, derived from -the corresponding `vTransp_*` axioms. - -**Glue-transport axioms** (Glue.lean, complete for constant-equivalence -sub-cases — both constant-A and varying-A; varying-equivalence is Stream -B #1c, future): - -| Axiom | A varies? | `φ[i:=1]` | Obligation | -|-------|-----------|-----------|------------| -| `eval_transp_glue_const_at_bot` | no | `.bot` | reduces to `transp i A ψ (unglue (φ[i:=0]) f t)` | -| `eval_transp_glue_const_at_top` | no | `.top` | reduces to `app fInv (transp i A ψ (unglue (φ[i:=0]) f t))` | -| `eval_transp_glue_const_stuck` | no | residual | structured `ntransp` neutral | -| `eval_transp_glue_varA_at_bot` | yes | `.bot` | reduces to `compN i A [(ψ, unglue φ f t), (φ, app f t)] (unglue (φ[i:=0]) f t)` | -| `eval_transp_glue_varA_at_top` | yes | `.top` | reduces to `app fInv (compN i A [(ψ, unglue φ f t), (φ, app f t)] (unglue (φ[i:=0]) f t))` | -| `eval_transp_glue_varA_stuck` | yes | residual | structured `ntransp` neutral | - -The six axioms partition `(A.dimAbsent i, φ[i:=1])` into 6 disjoint -cells — face-disjoint by hypothesis (`hA : … = true / false` and the -`φ[i:=1]` partition). The Rust backend must reduce CCHM §6.2 Glue -transport to the matching cell. Hcomp-correction note (Stream B #1a -refined): the `_at_top` axioms state the principal T-side witness; -their full CCHM form adds a T-side hcomp using `sec` for ψ-boundary -agreement, which trivialises under constant-equivalence hypotheses. - -### GPU layer (GPU/Spec) - -| Axiom | Obligation | -|-------|------------| -| `ShaderHandle.semantic` | opaque semantic function carried by every shader handle | -| `compileEML` | abstract EML-to-shader compilation function | -| `compileEML_correct` | `(compileEML expr).semantic = expr.toColor` (scoped per-expr; avoids the earlier unsound universally-quantified version) | -| `render_faithful` | GPU writes `h.semantic p u` at pixel `p` (needs readback tests; currently `True`) | -| `Float.log_one` | IEEE 754 `log 1 = 0` | -| `Float.max_one_ge_eps` | IEEE 754 `max 1.0 1e-9 = 1.0` | -| `Float.sub_zero` | IEEE 754 `a − 0 = a` for finite `a` | - ---- - -## Axiom provenance spectrum (Stage 3 audit, 2026-04-23) - -Every axiom in topolei carries one of three provenance tags that -determine *where* it is ultimately discharged. The axiom count -(~100) is large mostly because the Lean side *specifies* the Rust -evaluator's behaviour one arm at a time; the set of open Lean-side -proof obligations is small. - -### A. Rust-discharge axioms (~90 of ~100) - -These axioms state, arm-by-arm, what the Rust cubical evaluator must -compute. They cannot be eliminated without changing the partial-def -structure — they ARE the Lean-as-spec surface of the FFI contract. -Grouped by file: - -- `Eval.lean` (48 axioms): one per arm of `eval`, `vApp`, `vPApp`, - `vHCompValue`, `vCompAtTerm`, `vCompNAtTerm`, `vFst`, `vSnd`. Plus - the eval-level β/η rules for glue (`eval_unglue_of_glueIn`, - `eval_glueIn_of_unglue` — Stage 1.3) and T5 face congruence - (`eval_transp_face_congr` — Stream B #2b). -- `Transport.lean` (4 axioms): `vTransp_top/_const/_pi/_stuck`. -- `Readback.lean` (19 axioms): one per arm of `readback` / - `readbackNeu`, including the face-disjoint `vPathTransp_plam` / - `_other` pair (Stream B #2c) and the Σ additions (Stage 1.2). -- `Glue.lean` (9 axioms): the 6 `_const_*` / `_varA_*` face-disjoint - partition + 2 hcomp-corrected `_at_top` forms (Stage 2.1) + 1 - varying-equivalence stuck axiom (Stage 2.4). -- `Line.lean` (4 axioms): `DimLine.concat` + `concat_at0` + `concat_at1` - + `vTranspLine_concat` (Stage 1.1 — CCHM universe-hcomp discharge). -- `GPU/Spec.lean` (2–3 axioms): `ShaderHandle` / `GPUContext` opaque - types, `compileEML` / `compileEML_correct`, `render_faithful`. - -### B. Lean-discharge axioms (~9 of ~100) - -These axioms state properties that a future *Lean* development will -prove. No Rust involved. - -- `ValueTyping.lean` (4 axioms): `eval_preserves_type`, - `readback_preserves_type`, `EnvHasType.nil`, `CTerm.step_preserves_type`. - All discharge once `HasVal` / `HasNeu` / `EnvHasType` are given - inductive definitions. Unblocks T3 / C4 theorems (already done - modulo these axioms). -- `Line.lean` (3 axioms): `DimLine.inv_at0`, `inv_at1`, `inv_inv`. - Discharge once `DimExpr.normalize` lands (reducing `.inv .zero = .one` - etc.). -- `TransportLaws.lean` (1 axiom): T4 `transp_plam_is_plam` — syntactic - fallback; path-typed inputs are fully NbE-covered. Becomes - theorem or redundant once a total evaluator is introduced. -- Reserved for future extension (e.g. Multi-face Glue). - -### C. IEEE / external axioms (~5 of ~100) - -- `GPU/Spec.lean` (3 Float axioms): IEEE 754 `log 1 = 0`, `max 1 ε = 1`, - `a − 0 = a`. Pragmatic; discharged by IEEE 754 specification. -- Opaque `GPU` types (2). - -### Key audit findings - -1. **Only ~9 of ~100 axioms are open Lean-side proof obligations.** The - rest are either Rust-discharge (intrinsic to the Lean-spec / Rust- - backend contract) or external-spec. Topolei's formalisation is - nearly closed at the Lean level. - -2. **Stage 2.3 consolidation delivered O(1) scaling.** Before: each - new type-former with a transp/comp rule needed its own - step-preserves axiom. After: subject reduction scales in the - single `CTerm.step_preserves_type` axiom — adding new type-formers - (Σ done, `.glueN` future) automatically inherits preservation. - -3. **Face-disjoint partitioning is the pervasive pattern.** Recurring - in Glue transport (9-way partition), glueIn/unglue (3-way + β/η - redex), transp face cases (4-way), Σ β/neu (2-way). Every axiom - family consumers' non-overlap by matching/mismatched preconditions. - This is what lets adding new cases grow the axiom set without - perturbing proofs that consume existing axioms (e.g. `transp_ua`). - -4. **Step-level axioms are nearly gone.** After Stages 2.3 + 1.3: - T1/T2/C1/C2/`step_papp_plam`/T5/glue_β/η are all theorems. T3/C4 - are theorems (via `step_preserves_type`). Only T4 - (`transp_plam_is_plam`) remains — a syntactic fallback that is - vacuous in well-typed code. - ---- - -## Missing connections - -### `Canvas.lean` ↔ formal spec - -`canvasRun` / `canvasRun2` are FFI calls with no connection to `render_faithful` -or `compileEML_correct`. No theorem links the window loop to the GPU spec. - -### `Basic.lean` - -Currently `def hello := "world"`. Intended role not yet defined. - ---- - -## Priority order - -**Phase 1 (Cubical Core) is closed.** Stages 1–3 (post-Phase-1 -refinement, 2026-04-23) are closed: Line primitives, Sigma types, -β/η-aware glue rules, hcomp-corrected `_at_top`, T3/C4 subject -reduction, varying-equiv stuck, poet-restructure audit. Three -mostly-independent work streams remain. Pick by intent, not by -dependency. - -**Stream A: Zigzag Engine Lean Port (Phase 2 prerequisite, pure Lean):** -See `ZIGZAG_PORT.md` for the full 10-step plan. Ports the n-category -combinatorics (dimension-general normalisation preserving essential -identities, type-checking against signatures) from Rust reference into -Lean. Unlocks `Cell/` higher-cell semantics. 6–8 weeks. Rust -reference at `zigzag-engine/` is for porting from, not a dependency. - -**Stream B: Pure-Lean extensions of the cubical core.** - -Each sub-item below is an independent entry point — pick by intent, -not by dependency. Entry point for each is marked 📍. ✅ items -landed in Stages 1–3 (2026-04-23). - -1. **Glue transport — CCHM §6.2 sub-cases.** - - ✅ **1a. Hcomp-correction wrapper for `_at_top`** (Stage 2.1). - `eval_transp_glue_const_at_top_hcomp` and `_varA_at_top_hcomp` - state the full CCHM formula with hcomp-in-T using `ret` for - definitional ψ-boundary agreement. Naked form derived as theorem - at ψ = .bot (`eval_transp_glue_const_at_top_from_hcomp`). - - ✅ **1b. Varying base type `A`** (Stream B #1b). - - ✅ **1c. Varying-equivalence (structurally stuck)** (Stage 2.4). - `eval_transp_glue_varEquiv` produces a structured `ntransp` - neutral when any of `f/fInv/sec/ret/coh` vary in `i`. Full - reduction formula is future Rust refinement (the axiom becomes - more informative without Lean-side changes). - -2. **step↔eval bridge — all residuals now closed or consolidated.** - - ✅ **2a. T3 / C4 subject reduction** (Stage 2.3). Consolidated into - `CTerm.step_preserves_type` (ValueTyping.lean); T3 and C4 are - theorems. - - ✅ **2b. T5 face congruence** (Stream B #2b). - - ✅ **2c. General T4 NbE for path-typed lines** (Stream B #2c). - - ✅ **2d. Physical removal of redundant step axioms** (Stream B #2d). - - ✅ **Glue β/η on general faces** (Stage 1.3). `glue_beta`, - `glue_eta` (Soundness.lean) promoted to theorems via eval-level - `eval_unglue_of_glueIn` / `eval_glueIn_of_unglue`. - -3. ✅ **Sigma types** (Stage 1.2). `CType.sigma` + `CTerm.pair/.fst/.snd` - + value-level `vpair` / `nfst` / `nsnd` + full propagation through - substDim/dimAbsent/Typing/Value/Eval/Readback. Unlocks Cell.par - (§6.2), Color triples (§8.1), Shader pair/fst/snd (§9.2). - `fiberTy` still needs **dependent** Σ (genuinely deferred). - -4. 📍 **Multi-face Glue** (`.glueN`). *Intentionally deferred.* Enables - the symmetric two-face ua (cells-spec §5.7); single-face Glue - (present) is computationally sufficient. Not load-bearing for - Phase 2 cells. Future work. - -5. ✅ **Line module** (Stage 1.1). `DimLine.inv` + `DimLine.concat` + - `transp_concat` theorem (cells-spec §14 Critical, previously - unstated) + `transp_concat_const_{left,right}` unit-law corollaries. - Foundation for `Cell.seq` / `Cell.inv` in Phase 2. - -6. **Hygienic fresh-name generation** (low priority). Current - convention: user code must avoid `$`-prefixed names. **Entry**: - add a gensym counter to `CEnv` (or state monad in eval). Can be - deferred indefinitely. - -**Stream C: Rust FFI — the one Rust component.** - -Cubical evaluator backend implementing the eval-level, readback-level, -and Glue-transport axioms. Linked via `@[extern]` + -`@[implemented_by]`. Gives Lean's kernel the ability to *reduce* -cubical terms at native speed. Also hosts the GPU runtime (wgpu) when -Phase 5 begins. **This is the only Rust component in topolei**; no -part of the zigzag engine, numerical layer, cell layer, or any other -phase belongs here. - -📍 **Entry: minimum Rust obligation list to start implementation** (see -the Axiom discharge obligations table for the full list): - -- **Eval arms** (Eval.lean): `eval_var`, `eval_lam`, `eval_app`, - `eval_plam`, `eval_papp`, the four `eval_transp_*` face-cases, the - five `eval_comp_*` cases, `eval_compN`, the three `eval_glueIn_*` - face-cases, the three `eval_unglue_*` face-cases. -- **vApp / vPApp / vTransp / vHCompValue / vCompAtTerm / vCompNAtTerm - arms**: as listed in Eval.lean's axiom block. -- **Readback arms** (Readback.lean): `readback_*` (9 axioms — including - the face-disjoint `readback_vPathTransp_plam` / `_other` pair) + - `readbackNeu_*` (9 axioms). -- **Glue transport** (Glue.lean, 9 axioms after Stages 2.1 + 2.4): the - six `_const_*` / `_varA_*` face-disjoint axioms + 2 hcomp-corrected - `_at_top` variants (Stage 2.1) + 1 varying-equivalence stuck axiom - (Stage 2.4). -- **Σ eval/readback** (Eval.lean + Readback.lean, Stage 1.2): - `eval_pair/fst/snd`, `vFst_vpair/vneu`, `vSnd_vpair/vneu`, - `readback_vpair`, `readbackNeu_nfst/nsnd`. -- **Line transport** (Line.lean, Stage 1.1): `DimLine.concat` + endpoints - + `vTranspLine_concat` (CCHM universe-hcomp). -- **Glue β/η** (Eval.lean, Stage 1.3): `eval_unglue_of_glueIn`, - `eval_glueIn_of_unglue`. Soundness-level `glue_beta`/`glue_eta` are - theorems. -- **T5 face congruence** (Eval.lean): `eval_transp_face_congr`. -- **Residual step-level axioms**: only T4 `transp_plam_is_plam` - (TransportLaws.lean — syntactic fallback). T3 and C4 are theorems - (via `CTerm.step_preserves_type` in ValueTyping.lean). -- **Float axioms** (GPU/Spec.lean): `Float.log_one`, - `Float.max_one_ge_eps`, `Float.sub_zero`. -- **GPU bridge**: `compileEML`, `compileEML_correct`, - `render_faithful` (after pixel-readback tests replace `True`). - ---- - -## Concrete "next session" suggestions (2026-04-23, post-Stages 1–3) - -With Stages 1–3 closed, the cubical core is minimal and well-shaped. -Remaining choices are all forward-looking rather than closure-oriented: - -| Suggestion | Size | Impact | -|------------|------|--------| -| **Start Zigzag Lean port** (Stream A) | 6–8 weeks | Unblocks Phase 2 Cell/Higher.lean (n-cells). Independent of cubical-core extensions. | -| **Start Phase 2 Cell/Basic.lean + Cell/Compose.lean** | 2–3 sessions | Consumes the Line module (Stage 1.1) — `Cell.seq` / `Cell.inv` via `DimLine.concat` / `DimLine.inv`. cell_left_unit / cell_right_unit via `transp_concat_const_{left,right}`. Non-blocked. | -| **Discharge HasVal / HasNeu inductively** (Stage 2.3 completion) | multi-session | Replaces the four Lean-discharge axioms in ValueTyping.lean with theorems. Unlocks full subject-reduction machinery and dependent typing extensions. | -| **Multi-face Glue (`.glueN`)** (Stream B #4, deferred) | multi-session | Enables the symmetric two-face ua (cells-spec §5.7). Single-face Glue is sufficient for Phase 2 cells, so this is backlog. | -| **Varying-equivalence Glue full reduction** (Stage 2.4 refinement) | multi-session | Upgrade `eval_transp_glue_varEquiv` from stuck form to full CCHM computational content (needs `transp^i T` on witness terms). | -| **`DimExpr.normalize`** | 1–2 sessions | Reduces `.inv .zero = .one` etc. syntactically. Discharges three Lean-side axioms (`DimLine.inv_at0`, `inv_at1`, `inv_inv`). | -| **Start Rust implementation** (Stream C) | multi-session | **Unblocked by Stage 4.** See `FFI_DESIGN.md` (C ABI contract) + `FFI_COMPLETENESS.md` (per-function axiom audit) + `KERNEL_BOUNDARY.md` (scope contract vs. Lean kernel). `Cubical/FFI.lean` declares the extern entry points; Rust implements them to satisfy the ~90 Rust-discharge axioms. Orthogonal — can run in parallel with any Stream B work. | -| **Dependent Π / Σ types** | multi-session | Would require a term evaluator to apply `B` to a term. Unlocks proper `fiberTy` (Equiv.lean's "propositionally-reduced types" compromise). | diff --git a/Topolei.lean b/Topolei.lean deleted file mode 100644 index 86bab18..0000000 --- a/Topolei.lean +++ /dev/null @@ -1,31 +0,0 @@ -import Topolei.Basic -import Topolei.Canvas -import Topolei.EML -import Topolei.EML.Path -import Topolei.Cubical.Interval -import Topolei.Cubical.Face -import Topolei.Cubical.Syntax -import Topolei.Cubical.Subst -import Topolei.Cubical.DimLine -import Topolei.Cubical.Typing -import Topolei.Cubical.Equiv -import Topolei.Cubical.Glue -import Topolei.Cubical.Value -import Topolei.Cubical.Transport -import Topolei.Cubical.Line -import Topolei.Cubical.Eval -import Topolei.Cubical.EvalTest -import Topolei.Cubical.Readback -import Topolei.Cubical.FFI -import Topolei.Cubical.FFITest -import Topolei.Cubical.ValueTyping -import Topolei.Cubical.TransportLaws -import Topolei.Cubical.System -import Topolei.Cubical.CompLaws -import Topolei.Cubical.Soundness -import Topolei.GPU.Spec -import Topolei.Selection -import Topolei.Subobject -import Topolei.Trace -import Topolei.Cubical.Trace -import Topolei.Obs.Ctx diff --git a/Topolei/Basic.lean b/Topolei/Basic.lean deleted file mode 100644 index 99415d9..0000000 --- a/Topolei/Basic.lean +++ /dev/null @@ -1 +0,0 @@ -def hello := "world" diff --git a/Topolei/Canvas.lean b/Topolei/Canvas.lean deleted file mode 100644 index 29f0c80..0000000 --- a/Topolei/Canvas.lean +++ /dev/null @@ -1,46 +0,0 @@ -import Topolei.EML.Path - -/-! - Topolei.Canvas - ============== - Lean bindings to the live wgpu canvas. Both entry points take an - `EMLPath` (a structured cubical-1-cell) and a fixed `pathParam : Float`, - not a shader-source string and not a time-driven driver. The - fragment shader is built directly as a `naga::Module` on the Rust - side from the Lean inductive walk; the rendering is **static** — - every frame is the 1-cell's fiber at exactly `pathParam`. - - ## Why no animation curve? - - An earlier iteration animated `pathParam` host-side via a sine - sweep of `u_time`. That sweep is not a cubical transport — it's a - free-standing time-to-parameter function chosen for visual effect. - Driving rendering by something that is not itself a transport - violates the cells-spec discipline that "every continuous function - in the visible pipeline is a transport". The animated form belongs - to a 2-cell (a homotopy of 1-cells parameterised by a second - interval); we don't have 2-cell infrastructure yet, so we render - fixed fibers and leave time-driven motion for the 2-cell pass. - - See `NAGA_IR_PLAN.md` for the IR-builder plan; `compileEMLPath_correct` - in `Topolei.GPU.Spec` is the contract the Rust IR builder satisfies. --/ - -/-- Live render of one fiber of an `EMLPath`. `pathParam` chooses - the fiber; the rendering is static (the same fiber persists for - the lifetime of the window). -/ -@[extern "topolei_run_path"] -opaque canvasRunPath - (path : @& EMLPath) (pathParam : Float) - (width height : UInt32) (title : @& String) : IO Unit - -/-- Two-panel side-by-side variant: `pathL` rendered at fiber `ppL` - on the left, `pathR` at fiber `ppR` on the right. Each panel has - its own uniform buffer, so the two fibers are independent — pass - distinct `ppL`/`ppR` to display two different fibers (e.g. `at0` - vs `at1` of the same 1-cell). -/ -@[extern "topolei_run_path2"] -opaque canvasRunPath2 - (pathL : @& EMLPath) (ppL : Float) - (pathR : @& EMLPath) (ppR : Float) - (width height : UInt32) (title : @& String) : IO Unit diff --git a/Topolei/Cubical/Trace.lean b/Topolei/Cubical/Trace.lean deleted file mode 100644 index 6becb41..0000000 --- a/Topolei/Cubical/Trace.lean +++ /dev/null @@ -1,207 +0,0 @@ -/- - Topolei.Cubical.Trace - ===================== - The trace map at the cubical-syntax level. - - ## What this file does - - Given a cubical term `t : CTerm`, `traceOf t : Trace CTerm` returns - the list of *all sub-terms encountered* in walking `t` (including - `t` itself). This is the **provenance fold**: every constructor - visited, every variable referenced, every face-conditional clause - walked. - - Every rendered output produced from `t` traces back to `traceOf t`. - No instrumentation of the renderer is required — the trace is a - property of the *cubical structure*, computable in pure Lean before - the term ever leaves the host for the GPU. The Rust side renders - whatever single concrete shader Lean hands it; the trace was already - extracted upstream. - - ## Why this is the Euler-elegant move - - Composition of cubical terms (via `comp`, `compN`, `glueIn`, - `unglue`) automatically composes their traces — this is forced by - `traceOf`'s structural recursion + `Trace`'s monoid structure. The - homomorphism theorems below say: for any constructor `C` with - sub-terms `s₁..sₙ`, - - traceOf (C s₁ … sₙ) = single (C s₁ … sₙ) ∪ traceOf s₁ ∪ … ∪ traceOf sₙ - - Every one is `rfl`. The homomorphism IS the definition. No - external machinery, no enumerated cases, just structural recursion - realised as a fold + Trace's free-monoid algebra. - - ## What this gets us, semantically - - Per-pixel traces (the user's "different pixels carrying projections - of different fibers") become straightforward once we add a face- - pruning version `traceOfAt : DimAssignment → CTerm → Trace CTerm` - that, before recursing into `compN`'s clauses, evaluates each face - formula at the given assignment and skips clauses whose face is - inactive. That's a sibling function we can land later. - - Coherence between fibers (the differential / sheaf / bundle - question) is then a *predicate over `Trace CTerm`*: "the traces at - adjacent pixels share a long prefix," "the traces vary smoothly - along the rendered path," etc. All landed via simple `Prop`s, no - new types. - - ## Why no namespace wrap on `traceOf` - - `CTerm` lives at the root namespace (Syntax.lean has no - `namespace` declaration), so `CTerm.traceOf` must too — that's - what makes the dot notation `t.traceOf` resolve for any `t : CTerm`. - Theorems are in a namespace below; the function is at root. --/ - -import Topolei.Cubical.Syntax -import Topolei.Trace - -open Topolei.Trace - --- ── traceOf : structural fold over CTerm ────────────────────────────────── - --- The trace of a cubical term: itself, plus the union of traces of --- its immediate sub-terms (recursively). --- --- Mutual with `traceOf.clauses` to handle `compN`'s list of face- --- conditional sub-terms — same pattern as `CTerm.substDim` / --- `CTerm.substDim.clauses` in `Syntax.lean`. -mutual - /-- The trace of a cubical term: itself, plus the union of traces of - its immediate sub-terms (recursively). -/ - def CTerm.traceOf : CTerm → Trace CTerm - | t@(.var _) => Trace.single t - | t@(.lam _ body) => - (Trace.single t).union body.traceOf - | t@(.app f a) => - (Trace.single t).union (f.traceOf.union a.traceOf) - | t@(.plam _ body) => - (Trace.single t).union body.traceOf - | t@(.papp body _) => - (Trace.single t).union body.traceOf - | t@(.transp _ _ _ body) => - (Trace.single t).union body.traceOf - | t@(.comp _ _ _ u v) => - (Trace.single t).union (u.traceOf.union v.traceOf) - | t@(.compN _ _ clauses v) => - (Trace.single t).union ((CTerm.traceOf.clauses clauses).union v.traceOf) - | t@(.glueIn _ a b) => - (Trace.single t).union (a.traceOf.union b.traceOf) - | t@(.unglue _ f g) => - (Trace.single t).union (f.traceOf.union g.traceOf) - | t@(.pair a b) => - (Trace.single t).union (a.traceOf.union b.traceOf) - | t@(.fst a) => - (Trace.single t).union a.traceOf - | t@(.snd a) => - (Trace.single t).union a.traceOf - - /-- Walk a `compN`'s face-conditional clauses, unioning each - sub-term's trace. The face formulas themselves contribute - *no* trace items — they're metadata about *when* the - sub-term participates, not source items. A future - `traceOfAt` will use the formulas to prune; the - unrestricted `traceOf` records all clauses unconditionally. -/ - def CTerm.traceOf.clauses : List (FaceFormula × CTerm) → Trace CTerm - | [] => Trace.empty - | (_, c) :: rest => c.traceOf.union (CTerm.traceOf.clauses rest) -end - --- ── Theorems live in a sub-namespace ────────────────────────────────────── - -namespace Topolei.Cubical.Trace - --- ── Homomorphism theorems (the construction-language equations) ────────── --- --- For each cubical constructor C with sub-terms s₁..sₙ: --- traceOf (C s₁ … sₙ) = single (C s₁ … sₙ) ∪ traceOf s₁ ∪ … ∪ traceOf sₙ --- --- Every one of these is `rfl` by the definition above. This is the --- "Euler-elegant" core: the homomorphism IS the definition; we don't --- need separate proofs. The theorems exist as named references for --- downstream code, and as a stable contract that future refactors --- of `traceOf` must preserve. - -@[simp] theorem traceOf_var (x : String) : - (CTerm.var x).traceOf = Trace.single (CTerm.var x) := rfl - -@[simp] theorem traceOf_lam (x : String) (body : CTerm) : - (CTerm.lam x body).traceOf = - (Trace.single (CTerm.lam x body)).union body.traceOf := rfl - -@[simp] theorem traceOf_app (f a : CTerm) : - (CTerm.app f a).traceOf = - (Trace.single (CTerm.app f a)).union (f.traceOf.union a.traceOf) := rfl - -@[simp] theorem traceOf_plam (i : DimVar) (body : CTerm) : - (CTerm.plam i body).traceOf = - (Trace.single (CTerm.plam i body)).union body.traceOf := rfl - -@[simp] theorem traceOf_papp (body : CTerm) (r : DimExpr) : - (CTerm.papp body r).traceOf = - (Trace.single (CTerm.papp body r)).union body.traceOf := rfl - -@[simp] theorem traceOf_transp (i : DimVar) (A : CType) - (φ : FaceFormula) (body : CTerm) : - (CTerm.transp i A φ body).traceOf = - (Trace.single (CTerm.transp i A φ body)).union body.traceOf := rfl - -@[simp] theorem traceOf_comp (i : DimVar) (A : CType) (φ : FaceFormula) - (u v : CTerm) : - (CTerm.comp i A φ u v).traceOf = - (Trace.single (CTerm.comp i A φ u v)).union - (u.traceOf.union v.traceOf) := rfl - -@[simp] theorem traceOf_glueIn (φ : FaceFormula) (a b : CTerm) : - (CTerm.glueIn φ a b).traceOf = - (Trace.single (CTerm.glueIn φ a b)).union (a.traceOf.union b.traceOf) := - rfl - -@[simp] theorem traceOf_unglue (φ : FaceFormula) (f g : CTerm) : - (CTerm.unglue φ f g).traceOf = - (Trace.single (CTerm.unglue φ f g)).union (f.traceOf.union g.traceOf) := - rfl - -@[simp] theorem traceOf_pair (a b : CTerm) : - (CTerm.pair a b).traceOf = - (Trace.single (CTerm.pair a b)).union (a.traceOf.union b.traceOf) := rfl - -@[simp] theorem traceOf_fst (a : CTerm) : - (CTerm.fst a).traceOf = - (Trace.single (CTerm.fst a)).union a.traceOf := rfl - -@[simp] theorem traceOf_snd (a : CTerm) : - (CTerm.snd a).traceOf = - (Trace.single (CTerm.snd a)).union a.traceOf := rfl - --- ── Length / non-emptiness ──────────────────────────────────────────────── --- --- A trivial but useful corollary: every term's trace is non-empty --- (it always contains at least the term itself). The user's --- introspection guarantee depends on this — "every rendered element --- has *some* provenance" is a typed property, not a runtime hope. - -theorem traceOf_nonempty (t : CTerm) : t.traceOf.items ≠ [] := by - cases t <;> simp [CTerm.traceOf, Trace.single, Trace.union] - -end Topolei.Cubical.Trace - --- ── Operational sanity ──────────────────────────────────────────────────── - -/-- A demo: the trace of `λx. x` (an identity term) contains the lam - AND the var. -/ -def demoIdentity : CTerm := .lam "x" (.var "x") - -#eval demoIdentity.traceOf.items.length -- expected: 2 (lam + var) - -/-- A demo: the trace of `(a, b)` contains pair, var "a", var "b" → 3. -/ -def demoPair : CTerm := .pair (.var "a") (.var "b") - -#eval demoPair.traceOf.items.length -- expected: 3 - -/-- A demo: the trace of an application contains app + f-trace + a-trace. -/ -def demoApp : CTerm := .app (.var "f") (.var "a") - -#eval demoApp.traceOf.items.length -- expected: 3 diff --git a/Topolei/EML.lean b/Topolei/EML.lean deleted file mode 100644 index 7118d29..0000000 --- a/Topolei/EML.lean +++ /dev/null @@ -1,90 +0,0 @@ --- EML tree: S → 1 | eml(S, S) --- eml(x, y) = exp(x) − ln(y) --- --- Single binary primitive that generates all elementary functions. --- (Odrzywolek 2026, arXiv:2603.21852) - --- ── Core inductive ──────────────────────────────────────────────────────────── - -inductive EMLExpr where - | one : EMLExpr - | var (name : String) : EMLExpr -- free variable resolved in the rendering env - | eml (l r : EMLExpr) : EMLExpr - --- ── Derived forms ───────────────────────────────────────────────────────────── - --- exp(x) = eml(x, 1) since exp(x) − ln(1) = exp(x) − 0 = exp(x) -def EMLExpr.expOf (x : EMLExpr) : EMLExpr := .eml x .one - --- ln(x) = eml(1, eml(eml(1, x), 1)) -def EMLExpr.lnOf (x : EMLExpr) : EMLExpr := - .eml .one (.eml (.eml .one x) .one) - --- ── Plot configuration ──────────────────────────────────────────────────────── - -/-- A path config: an EML expression plus its distinguished path-dimension - variable. `dimName` is the name that ranges over `{0, 1}` when the - config is interpreted as an `EMLPath` (see `EML/Path.lean`'s - `PlotConfig.toEMLPath`). When `dimName` does not occur in `expr`, - the path is *constant* (its value is the same at both endpoints); - when it does, the path is genuinely parametric. -/ -structure PlotConfig where - expr : EMLExpr - dimName : String := "t" - --- ── Named demo expressions (probe test fixtures) ───────────────────────────── - --- exp(x): depth-1 EML tree, one node. -def plotExp : PlotConfig := { - expr := EMLExpr.expOf (.var "px") -} - --- ln(x): depth-3 EML tree, three nodes. --- --- Historical note: an earlier `plotLn` used the variable name --- `"max(px, 0.001)"` to clamp negative inputs on the GPU side. That --- made the GPU shader evaluate `max(px, 0.001)` as a GLSL expression --- but left Lean's `shaderVar` hitting the fallback `0.0` — the two --- sides disagreed on the semantic of the variable. The probe test --- surfaced the divergence; the fix is to use a real variable `px` --- and accept that `ln` of negative `px` produces `NaN` on both sides --- (the two sides agree, which is what `render_faithful` cares about). -def plotLn : PlotConfig := { - expr := EMLExpr.lnOf (.var "px") -} - --- ── Continuous-homotopy demo: a genuinely parametric path ──────────────────── --- `exp(px) − t` translates the exponential curve down by `t`. At `t=0` --- the curve is `y = exp(x)`; at `t=1` it's `y = exp(x) − 1`; in between it --- smoothly slides. EML-expressible because `exp(px) - log(exp(t))` reduces --- (via `log ∘ exp = id`) to `exp(px) - t`; body is --- `eml(var "px", eml(var "t", one))`. - -def plotTransp : PlotConfig := { - expr := .eml (.var "px") (.eml (.var "t") .one) - dimName := "t" -} - --- ── Clean fibers in [0, 1] for greyscale demos ────────────────────────────── --- These bodies are picked specifically so their image lies in `[0, 1]`, --- matching the framebuffer's natural display range. Each is a 1-cell; --- their shape under different `pathParam` values shows the transport --- in action with no clamping artifacts. - -/-- The 1-cell whose body IS the dim variable: at `pathParam = c` every - pixel evaluates to `c`. Different fibers display as solid-color - panels with brightness equal to the fiber's parameter. - `at0 = solid black`, `at1 = solid white`, `mid = solid 50% grey`. -/ -def plotT : PlotConfig := { - expr := .var "t" - dimName := "t" -} - -/-- The 1-cell whose body is `px`: image is the horizontal coordinate - itself, a black-to-white left-to-right gradient. Constant - 1-cell (no `t` dependence) — every fiber is the same gradient. - Useful as a sanity check: fibers should NOT differ. -/ -def plotPx : PlotConfig := { - expr := .var "px" - dimName := "t" -} diff --git a/Topolei/EML/Path.lean b/Topolei/EML/Path.lean deleted file mode 100644 index d192814..0000000 --- a/Topolei/EML/Path.lean +++ /dev/null @@ -1,243 +0,0 @@ -/- - Topolei.EML.Path - ================ - Connection between EML expressions and the cubical interval. - - An EML expression with a free "dimension variable" (a named string) - that is evaluated at 0.0 or 1.0 is a path in the Float domain. - This is the bridge between: - - · the cubical interval I (Bool endpoints: false = 0, true = 1) - · EML evaluation (Float-valued) - - Key definitions: - · EMLExpr.evalWithEnv — generalised evaluator with custom var resolver - · EMLExpr.varAbsent — syntactic check that a variable does not appear - · EMLPath — an EML expression with a distinguished dim var - · EMLPath.atBool/at0/at1 — evaluation at Bool endpoints - · EMLPath.const_endpoints — absent dim var → at0 = at1 - - The GPU-layer connection (linking baseEnv to shaderVar) lives in - GPU/Spec.lean rather than here, to avoid a circular import. --/ - -import Topolei.EML -import Topolei.Cubical.DimLine - --- ── Bool → Float endpoint map ───────────────────────────────────────────────── - -/-- Map Bool to its Float interval endpoint: false ↦ 0.0, true ↦ 1.0 -/ -def boolToFloat : Bool → Float - | false => 0.0 - | true => 1.0 - -@[simp] theorem boolToFloat_false : boolToFloat false = 0.0 := rfl -@[simp] theorem boolToFloat_true : boolToFloat true = 1.0 := rfl - --- ── Generalised EML evaluator ───────────────────────────────────────────────── - -/-- Evaluate an EML expression using a custom variable resolver. - This generalises EMLExpr.evalAt (which uses shaderVar) so we can - override the dimension variable without touching shader-layer types. -/ -def EMLExpr.evalWithEnv (env : String → Float) : EMLExpr → Float - | .one => 1.0 - | .var name => env name - | .eml l r => - let lv := l.evalWithEnv env - let rv := r.evalWithEnv env - Float.exp lv - Float.log (max rv 1e-9) - --- ── Variable absence predicate ──────────────────────────────────────────────── - -/-- Syntactic check: named variable does not appear in the expression. -/ -def EMLExpr.varAbsent (name : String) : EMLExpr → Bool - | .one => true - | .var n => n != name - | .eml l r => l.varAbsent name && r.varAbsent name - -/-- Two environments that agree on all names except `name` give the same - evaluation on expressions that don't mention `name`. -/ -theorem EMLExpr.evalWithEnv_congr - (e : EMLExpr) (name : String) - (habs : e.varAbsent name = true) - (env1 env2 : String → Float) - (henv : ∀ n, n ≠ name → env1 n = env2 n) : - e.evalWithEnv env1 = e.evalWithEnv env2 := by - induction e with - | one => rfl - | var n => - simp only [varAbsent, bne_iff_ne] at habs - simp [evalWithEnv, henv n habs] - | eml l r ihl ihr => - simp only [varAbsent, Bool.and_eq_true] at habs - simp [evalWithEnv, ihl habs.1, ihr habs.2] - --- ── EMLPath ─────────────────────────────────────────────────────────────────── - -/-- An EML path: an expression parametric in a named dimension variable. - The `dimName` variable ranges over {0.0, 1.0} ≅ Bool. -/ -structure EMLPath where - dimName : String -- dimension variable (e.g. "t") - body : EMLExpr -- parametric expression - -/-- Evaluate an EMLPath at a Bool endpoint, given a base variable resolver for - all variables other than dimName. -/ -def EMLPath.atBool (path : EMLPath) (b : Bool) (baseEnv : String → Float) : Float := - path.body.evalWithEnv (fun name => - if name = path.dimName then boolToFloat b else baseEnv name) - -def EMLPath.at0 (path : EMLPath) (baseEnv : String → Float) : Float := - path.atBool false baseEnv - -def EMLPath.at1 (path : EMLPath) (baseEnv : String → Float) : Float := - path.atBool true baseEnv - --- ── Endpoint reduction lemmas ───────────────────────────────────────────────── - -@[simp] theorem EMLPath.at0_def (path : EMLPath) (baseEnv : String → Float) : - path.at0 baseEnv = - path.body.evalWithEnv (fun name => - if name = path.dimName then 0.0 else baseEnv name) := by - simp [at0, atBool, boolToFloat] - -@[simp] theorem EMLPath.at1_def (path : EMLPath) (baseEnv : String → Float) : - path.at1 baseEnv = - path.body.evalWithEnv (fun name => - if name = path.dimName then 1.0 else baseEnv name) := by - simp [at1, atBool, boolToFloat] - --- ── Constant path ───────────────────────────────────────────────────────────── - -/-- When the body does not mention dimName, at0 = at1 for any baseEnv. -/ -theorem EMLPath.const_endpoints (path : EMLPath) - (habs : path.body.varAbsent path.dimName = true) - (baseEnv : String → Float) : - path.at0 baseEnv = path.at1 baseEnv := by - simp only [at0_def, at1_def] - apply EMLExpr.evalWithEnv_congr _ _ habs - intro n hn - simp [if_neg hn] - --- ── Example: exp path ──────────────────────────────────────────────────────── - -/-- The exponential path: f(t) = exp(t) for t ∈ {0, 1}. -/ -def expPath : EMLPath := - { dimName := "t" - body := EMLExpr.expOf (.var "t") } - -/-- At t = 0, expPath evaluates to exp(0) - log(max 1.0 1e-9). -/ -theorem expPath_at0 (baseEnv : String → Float) : - expPath.at0 baseEnv = - Float.exp 0.0 - Float.log (max 1.0 1e-9) := by - simp [expPath, EMLExpr.expOf, EMLExpr.evalWithEnv] - -/-- At t = 1, expPath evaluates to exp(1) - log(max 1.0 1e-9). -/ -theorem expPath_at1 (baseEnv : String → Float) : - expPath.at1 baseEnv = - Float.exp 1.0 - Float.log (max 1.0 1e-9) := by - simp [expPath, EMLExpr.expOf, EMLExpr.evalWithEnv] - --- ── Connection to DimLine ───────────────────────────────────────────────────── -/- - Structural parallel: - - DimLine (cubical, CType-valued) ↔ EMLPath (rendering, Float-valued) - ─────────────────────────────────────────────────────────────────────── - DimLine.binder : DimVar ↔ EMLPath.dimName : String - DimLine.at0 : CType ↔ EMLPath.at0 : Float - DimLine.at1 : CType ↔ EMLPath.at1 : Float - transp_const_id (T2) ↔ EMLPath.const_endpoints - - Transport along a constant DimLine is the identity (T2). - EML analogue: evaluation of a constant EMLPath (dimName absent) gives - the same Float value at both endpoints. - - The rendering correctness claim: - "If the GPU evaluates shader at the 1-end of a DimLine, - the Float result equals EMLPath.at1 baseEnv." - This is the bridge axiom in GPU/Spec.lean (to be added there). --/ - -theorem EMLPath_const_mirrors_T2 - (path : EMLPath) - (habs : path.body.varAbsent path.dimName = true) - (baseEnv : String → Float) : - path.at0 baseEnv = path.at1 baseEnv := - EMLPath.const_endpoints path habs baseEnv - --- ── PlotConfig → EMLPath ────────────────────────────────────────────────────── -/- - A `PlotConfig` carries an `EMLExpr` and a distinguished `dimName` which is - the path dimension. Viewing a plot as an `EMLPath` is a direct projection: - drop the display metadata, keep the expression and the dimension. - - When the plot's expression does not mention `dimName`, the resulting path - is constant (T2 analogue); when it does, the plot is genuinely time-varying - and `at0`, `at1` differ. --/ - -/-- Project a `PlotConfig` to its `EMLPath` view. -/ -def PlotConfig.toEMLPath (cfg : PlotConfig) : EMLPath := - { dimName := cfg.dimName - body := cfg.expr } - -@[simp] theorem PlotConfig.toEMLPath_dimName (cfg : PlotConfig) : - cfg.toEMLPath.dimName = cfg.dimName := rfl - -@[simp] theorem PlotConfig.toEMLPath_body (cfg : PlotConfig) : - cfg.toEMLPath.body = cfg.expr := rfl - -/-- A plot is a *constant path* exactly when its expression does not mention - the plot's declared dimension variable. -/ -theorem PlotConfig.const_path_of_varAbsent - (cfg : PlotConfig) - (habs : cfg.expr.varAbsent cfg.dimName = true) - (baseEnv : String → Float) : - cfg.toEMLPath.at0 baseEnv = cfg.toEMLPath.at1 baseEnv := - EMLPath.const_endpoints cfg.toEMLPath habs baseEnv - --- ── Demo expressions as paths ───────────────────────────────────────────────── -/- - `plotExp` and `plotLn` (in `EML.lean`) use `px` as their free variable, - not the dimension variable `t`. They are therefore *constant paths* — the - same value at both endpoints — under the default `dimName := "t"`. --/ - -theorem plotExp_body_varAbsent : plotExp.expr.varAbsent "t" = true := by decide - -theorem plotExp_is_const_path (baseEnv : String → Float) : - plotExp.toEMLPath.at0 baseEnv = plotExp.toEMLPath.at1 baseEnv := - plotExp.const_path_of_varAbsent plotExp_body_varAbsent baseEnv - -theorem plotLn_body_varAbsent : plotLn.expr.varAbsent "t" = true := by decide - -theorem plotLn_is_const_path (baseEnv : String → Float) : - plotLn.toEMLPath.at0 baseEnv = plotLn.toEMLPath.at1 baseEnv := - plotLn.const_path_of_varAbsent plotLn_body_varAbsent baseEnv - --- ── Parametric example (a genuinely non-constant path) ─────────────────────── - -/-- A parametric plot that actually uses the dimension variable `t`: - `eml(t, 1) = exp(t)` — a path from `exp(0)` at `t=0` to `exp(1)` at `t=1`. -/ -def plotExpT : PlotConfig := - { expr := EMLExpr.expOf (.var "t") - dimName := "t" } - -theorem plotExpT_at0 (baseEnv : String → Float) : - plotExpT.toEMLPath.at0 baseEnv = - Float.exp 0.0 - Float.log (max 1.0 1e-9) := by - simp [plotExpT, PlotConfig.toEMLPath, EMLPath.at0, EMLPath.atBool, - EMLExpr.expOf, EMLExpr.evalWithEnv, boolToFloat] - -theorem plotExpT_at1 (baseEnv : String → Float) : - plotExpT.toEMLPath.at1 baseEnv = - Float.exp 1.0 - Float.log (max 1.0 1e-9) := by - simp [plotExpT, PlotConfig.toEMLPath, EMLPath.at1, EMLPath.atBool, - EMLExpr.expOf, EMLExpr.evalWithEnv, boolToFloat] - --- The shader whose semantic IS `EMLPath.toColor` is now built directly --- as a `naga::Module` (no GLSL string intermediary) on the Rust side --- by `native/canvas-rs/src/emit_naga.rs::build_probe_module`. See --- `NAGA_IR_PLAN.md` for the construction; `Topolei.GPU.Spec`'s --- `compileEMLPath_correct` axiom states the contract that builder --- must satisfy. diff --git a/Topolei/GPU/Spec.lean b/Topolei/GPU/Spec.lean deleted file mode 100644 index bb10566..0000000 --- a/Topolei/GPU/Spec.lean +++ /dev/null @@ -1,448 +0,0 @@ -/- - Topolei.GPU.Spec - ================ - Axiomatic specification of the GPU layer. - - Everything declared `axiom` here is a CLAIM about what the Rust/GPU - implementation must satisfy. The Lean math is proved correct *assuming* - these axioms. Before writing Rust, we verify the math is consistent with - the specs. When the Rust ships, we verify it against them. - - Proof obligations: - - Lean side: proved below (no axioms needed) - - Rust/GPU side: declared as axioms (discharged by `native/canvas-rs/`) - - ## Honesty audit — non-transports the rendering pipeline depends on - - Every continuous function reaching the GPU should ideally be a - cubical transport. In practice the pipeline relies on a finite - set of non-transports. This block enumerates them. They split - into three classes: - - ### A. Sealed cells (cells-spec §1.7) - GPU intrinsics whose interiors are sealed at the IEEE/spec level. - We consume their boundaries (the IEEE 754 contract for `exp`, - `log`, `cos`, `+`, `-`, `*`) and trust the hardware to satisfy - them. Discharge obligation = the IEEE 754 spec on the chip. - · `Float.exp`, `Float.log` (for `eml(l, r) = exp(l) - log(r)`) - · IEEE 754 `+`, `-`, `*` on `Float` - · Rasterizer interpolation of `uv` (perspective barycentric - across the fullscreen triangle) - · WGSL vertex shader's coordinate algebra - (`pos = vec2(select(-1, 3, …)…); uv = pos * 0.5 + 0.5`) - - ### B. Visualization adapter (frozen by `compileEMLPath_correct`) - The cosine cycle in `EMLExpr.toColor` (`r,g,b = 0.5 + - 0.5·cos(2π·v + φ)`). NOT a cubical transport — see the docstring - on `EMLExpr.toColor` below. Removing it requires re-axiomatizing - the spec or modeling color spaces as `CType` cells. - - ### C. Presentation conventions - Window-side choices the renderer makes that aren't part of any - semantic claim: - · Window dimensions (caller's choice) - · Aspect ratio (uv²ⁿ → window's W×H rectangle; bodies that use - `py` will be visibly stretched if W ≠ H) - · Y-flip in `pixelUV` (`uv.y = 1 - (y+0.5)/h`) to match Vulkan's - rasterizer Y-flip - · Floating-point tolerance `5e-3` in the probe (CPU vs GPU - IEEE 754 disagreement) - · The two-panel viewport split - - ## What is NOT in this list (by design) - - Things that *are* transports in our calculus: - · `EMLExpr.eml(l, r) = exp(l) - log(r)` — generates elementary - functions per the EML primitive (Odrzywolek 2026) - · The `EMLPath` body's value as `pathParam` varies — this is - genuinely the transport we're rendering --/ - -import Topolei.EML -import Topolei.EML.Path - --- ── Opaque GPU types ────────────────────────────────────────────────────────── --- C++ objects; Lean sees them only through their axioms. - -axiom ShaderHandle : Type -axiom GPUContext : Type - --- ── Semantic domain (pure Lean) ─────────────────────────────────────────────── - -structure PixelCoord where - x : Float - y : Float - -structure FrameUniforms where - time : Float - resWidth : Float - resHeight : Float - /-- The path parameter — a fixed value chosen per render. The shader - reads this uniform and binds its cubical-path dim variable to it; - see `shaderVarWithDim` below. Earlier the canvas-rs runtime - animated this from `u.time` via a sine sweep; that was removed - because a host-side `Float → Float` is not a transport. Default - `0.0` keeps existing construction sites (record literals and - `sorry`/`default`-derived values) well-typed. -/ - pathParam : Float := 0.0 - -structure PixelColor where - r : Float - g : Float - b : Float - a : Float - -/-- The semantic type of a shader: pure function from pixel + frame state to color. -/ -def ShaderSemantic := PixelCoord → FrameUniforms → PixelColor - --- ── EML reference evaluator (pure Lean) ────────────────────────────────────── --- Defines what the GPU *should* compute. The axioms below say it does. - -/-- Look up the value of a free variable name in shader context. -/ -def shaderVar (name : String) (p : PixelCoord) (u : FrameUniforms) : Float := - match name with - | "px" => p.x - | "py" => p.y - | "u_time" => u.time - | "u_resWidth" => u.resWidth - | "u_resHeight" => u.resHeight - | _ => 0.0 - -/-- Dim-aware variable resolver. Routes the path's distinguished - `dimName` to `u.pathParam`; every other name passes through to - `shaderVar`. This is the semantic-side analogue of the GPU shader - referencing the dim variable via `u_pathParam` rather than a - GLSL-local computation. - - Named so the rewrite `shaderVarWithDim d d p u = u.pathParam` is - `rfl` on the dim-hit case; the miss case delegates cleanly. -/ -def shaderVarWithDim (dimName : String) (name : String) - (p : PixelCoord) (u : FrameUniforms) : Float := - if name = dimName then u.pathParam - else shaderVar name p u - -@[simp] theorem shaderVarWithDim_dim - (dimName : String) (p : PixelCoord) (u : FrameUniforms) : - shaderVarWithDim dimName dimName p u = u.pathParam := by - simp [shaderVarWithDim] - -@[simp] theorem shaderVarWithDim_other - (dimName name : String) (p : PixelCoord) (u : FrameUniforms) - (h : name ≠ dimName) : - shaderVarWithDim dimName name p u = shaderVar name p u := by - simp [shaderVarWithDim, h] - -/-- Evaluate an EML expression to a Float at a given pixel and frame state. - This is the reference semantics — the ground truth for what the shader computes. -/ -def EMLExpr.evalAt (p : PixelCoord) (u : FrameUniforms) : EMLExpr → Float - | .one => 1.0 - | .var name => shaderVar name p u - | .eml l r => - let lv := l.evalAt p u - let rv := r.evalAt p u - -- exp(l) − ln(r), clamping rv > 0 for real-valued rendering - Float.exp lv - Float.log (max rv 1e-9) - -/-- Direct greyscale projection: write the EML value into all three - color channels. - - This is the minimal-honest `Float → Color` mapping — it's the - identity injection of the Float-valued transport's image into - the framebuffer's three channels. No `cos`, no normalization, - no period-wrapping. Negative `v` displays as black after the - sealed-cell sRGB clamp at the display; `v > 1` displays as - white. - - A previous version applied a cosine cycle - (`r = 0.5 + 0.5·cos(2π·v + φ)` etc.) here to make any value - visible, but the cycle has period 1 in `v` and so *aliased* - fibers that differ by 1 (e.g. `plotTransp.at0` vs - `plotTransp.at1` differ by exactly 1) into pixel-identical - output. That made it visually impossible to distinguish two - genuinely-distinct transports. The cycle is removed. - - Greyscale is still not a "transport" in the cubical sense — - the framebuffer-clamp on overflow / underflow is a sealed-cell - behavior at the display, not a derived cell. Building a real - `Float ↔ Color` transport (cells-spec §8: color spaces as - `CType`, conversions as cells via `ua` of an equivalence) is - on the roadmap. -/ -def EMLExpr.toColor (p : PixelCoord) (u : FrameUniforms) (expr : EMLExpr) : PixelColor := - let v := expr.evalAt p u - { r := v, g := v, b := v, a := 1.0 } - --- ── GPU axioms ──────────────────────────────────────────────────────────────── - -/-- Axiom G1: Every ShaderHandle carries a semantic — the function it computes. -/ -axiom ShaderHandle.semantic : ShaderHandle → ShaderSemantic - -/-- Axiom G2: An abstract EML-to-shader compiler. The C++ side is obliged to - implement something that matches this spec. -/ -axiom compileEML : EMLExpr → ShaderHandle - -/-- Axiom G3: `compileEML` is correct — the compiled shader's semantic - function agrees with the Lean reference evaluator `toColor`. - - Scoped to the handle produced by `compileEML expr`; this avoids the - earlier unsound formulation `∀ expr h, h.semantic = expr.toColor`, which - forced `expr₁.toColor = expr₂.toColor` for any pair of exprs via a - shared `h`. -/ -axiom compileEML_correct (expr : EMLExpr) : - (compileEML expr).semantic = expr.toColor - -/-- Axiom G4: The render loop is faithful. For a compiled shader `h` - running under uniforms `u`, the pixel written at screen coord `p` - equals `h.semantic p u` (within IEEE 754 float tolerance — bit- - exact only on driver+arch combinations that expose it). - - The axiom body is `True` because Lean cannot evaluate an IO action - in a pure proof. The **empirical discharge** happens in - `Topolei.Render.Probe` via `renderProbePixel` — a Rust FFI that - renders a shader offscreen into an `Rgba32Float` texture and reads - one pixel back. See `ProbeTest.lean` + the `probe-test` lake exe. -/ -axiom render_faithful (ctx : GPUContext) (h : ShaderHandle) (u : FrameUniforms) : - True -- empirical discharge via Topolei.Render.Probe + probe-test exe - --- ── Bridge theorems (proved in Lean, assuming the axioms) ───────────────────── - -/-- The compiled-shader semantic agrees with the Lean reference evaluator at - every pixel and frame, for the handle `compileEML` produced. -/ -theorem compileEML_semantic_eq_toColor - (expr : EMLExpr) (p : PixelCoord) (u : FrameUniforms) : - (compileEML expr).semantic p u = expr.toColor p u := by - rw [compileEML_correct] - -/-- Legacy hypothesis-carrying form: given a per-handle correctness witness, - `semantic = toColor` pointwise. Useful for handles produced by code - paths other than `compileEML` (e.g. externally provided shaders). -/ -theorem compiled_semantic_eq_eval - (expr : EMLExpr) (h : ShaderHandle) - (hc : h.semantic = expr.toColor) - (p : PixelCoord) (u : FrameUniforms) : - h.semantic p u = expr.toColor p u := by - rw [hc] - --- ── Evaluator correctness (pure Lean proofs) ────────────────────────────────── - --- ── IEEE 754 Float-arithmetic obligations ──────────────────────────────────── --- Lean's `Float` is axiomatic; it has no DecidableEq matching propositional --- equality, so `native_decide` cannot discharge even simple numeric identities. --- The three axioms below are required for `evalAt_expOf` and are uncontroversial --- IEEE 754 facts. They become verification obligations on the C++ runtime. - -/-- `log 1 = 0` in IEEE 754 double precision. -/ -axiom Float.log_one : Float.log 1.0 = 0.0 - -/-- `max 1.0 1e-9 = 1.0` (1.0 > 1e-9 in IEEE 754). -/ -axiom Float.max_one_ge_eps : max (1.0 : Float) 1e-9 = 1.0 - -/-- IEEE 754 subtraction by zero is identity (for non-NaN operands; - the `evalAt` call site always feeds a finite `Float.exp` result, - which is non-NaN except at `Float.exp +∞`). -/ -axiom Float.sub_zero (a : Float) : a - 0.0 = a - --- The earlier `PathDriver` / `sineSweep` / `canonicalDriver` / --- `Float.sin_range` / `sineSweep_range01_of` block was removed --- because no part of it was a transport in the cells-spec sense: --- it described a host-side `Float → Float` function used to --- animate `u.pathParam` from `u.time`, which is not derived from --- any cubical path or type family. The `canvas-rs` runtime no --- longer animates `pathParam`; each window renders one fiber of --- the 1-cell at a fixed `pathParam` chosen by the caller. --- --- Animated rendering — a continuous deformation of fibers over --- time — is properly a 2-cell (a homotopy of 1-cells parameterised --- by a second interval). When 2-cell infrastructure lands, the --- spec for it goes here, derived from the cubical calculus, not as --- a free-standing `Float → Float`. - -/-- exp(x) = eml(x, 1): evaluates to Float.exp p.x. - - Proof chain: - evalAt p u (eml (var "px") one) - = Float.exp p.x − Float.log (max 1.0 1e-9) [unfold evalAt, shaderVar] - = Float.exp p.x − Float.log 1.0 [Float.max_one_ge_eps] - = Float.exp p.x − 0.0 [Float.log_one] - = Float.exp p.x [Float.sub_zero]. -/ -theorem evalAt_expOf (p : PixelCoord) (u : FrameUniforms) : - EMLExpr.evalAt p u (EMLExpr.expOf (.var "px")) = Float.exp p.x := by - simp only [EMLExpr.expOf, EMLExpr.evalAt, shaderVar, - Float.max_one_ge_eps, Float.log_one, Float.sub_zero] - -/-- The depth-1 EML tree is the exponential — H1 first concrete instance. -/ -theorem h1_exp_instance (p : PixelCoord) (u : FrameUniforms) - (h : ShaderHandle) - (hc : h.semantic = fun p u => EMLExpr.toColor p u (EMLExpr.expOf (.var "px"))) : - h.semantic p u = EMLExpr.toColor p u (EMLExpr.expOf (.var "px")) := by - rw [hc] - --- ── EMLPath–GPU bridge ──────────────────────────────────────────────────────── -/- - An EMLPath with dimName = "t" represents a shader that varies along a - dimension variable t ∈ {0, 1}. The GPU evaluates it at the 1-end (t = 1.0). - The baseEnv here is (shaderVar · p u): the standard pixel+frame variable map. - - This is the link between: - · EMLPath.at1 (cubical/EML: what the 1-end value should be) - · EMLExpr.evalAt (GPU/Spec: what the reference evaluator computes) - - The condition "dimName variable in baseEnv" is handled by the override in - atBool; the rest of the variables use shaderVar as usual. --/ - -/-- evalAt agrees with evalWithEnv when using shaderVar as the resolver. -/ -theorem EMLExpr.evalAt_eq_evalWithEnv (p : PixelCoord) (u : FrameUniforms) - (e : EMLExpr) : - e.evalAt p u = e.evalWithEnv (fun name => shaderVar name p u) := by - induction e with - | one => rfl - | var name => simp [evalAt, evalWithEnv, shaderVar] - | eml l r ihl ihr => - simp only [evalAt, evalWithEnv, ihl, ihr] - -/-- An EMLPath evaluated at the 1-end (b = true) agrees with the standard - evaluator when the dimension variable is overridden to 1.0 in the env. -/ -theorem EMLPath.at1_eq_evalAt_override - (path : EMLPath) (p : PixelCoord) (u : FrameUniforms) : - path.at1 (fun name => shaderVar name p u) = - path.body.evalWithEnv (fun name => - if name = path.dimName then 1.0 - else shaderVar name p u) := - rfl - -/-- If the dimension variable is not `px` and not in shaderVar's domain, - and the body of the path does not use the dim variable, - then EMLPath.at1 = EMLExpr.evalAt for the body. -/ -theorem EMLPath.at1_of_absent_eq_evalAt - (path : EMLPath) (p : PixelCoord) (u : FrameUniforms) - (habs : path.body.varAbsent path.dimName = true) : - path.at1 (fun name => shaderVar name p u) = - path.body.evalAt p u := by - rw [EMLExpr.evalAt_eq_evalWithEnv] - apply EMLExpr.evalWithEnv_congr _ _ habs - intro n hn - simp [if_neg hn] - --- ── Rendering bridge: dim uniform overridden to 1.0 ⇒ shader = EMLPath.at1 ── - -/-- Scalar bridge: when `shaderVar` already resolves the path's dim variable - to 1.0, the GPU evaluator on the path body equals `EMLPath.at1` computed - against the standard `shaderVar`-based env. Proof: rewrite `evalAt` via - `evalAt_eq_evalWithEnv`, then use `evalWithEnv_congr` on the two envs - which agree everywhere (the override matches the baseline at dimName). -/ -theorem EMLPath.evalAt_body_eq_at1 - (path : EMLPath) (p : PixelCoord) (u : FrameUniforms) - (hu : shaderVar path.dimName p u = 1.0) : - path.body.evalAt p u = - path.at1 (fun name => shaderVar name p u) := by - rw [EMLExpr.evalAt_eq_evalWithEnv, EMLPath.at1_eq_evalAt_override] - -- Both sides are evalWithEnv against envs that agree pointwise: - -- env1 n = shaderVar n p u - -- env2 n = if n = dimName then 1.0 else shaderVar n p u - -- At n = dimName: env1 = 1.0 (by hu), env2 = 1.0. Else: equal by construction. - congr 1 - funext n - by_cases h : n = path.dimName - · subst h; simp [hu] - · simp [if_neg h] - -/-- Color bridge: under the same dim-uniform assumption, `toColor` of the - path body equals `toColor` of the `EMLPath.at1` value. This is the - formal "rendering at t=1 equals EMLPath.at1" statement promised in - `EML/Path.lean`. -/ -theorem EMLPath.toColor_body_eq_at1_toColor - (path : EMLPath) (p : PixelCoord) (u : FrameUniforms) - (hu : shaderVar path.dimName p u = 1.0) : - EMLExpr.toColor p u path.body = - let v := path.at1 (fun name => shaderVar name p u) - ({ r := v, g := v, b := v, a := 1.0 } : PixelColor) := by - simp only [EMLExpr.toColor, EMLPath.evalAt_body_eq_at1 path p u hu] - -/-- End-to-end rendering bridge: the compiled shader for `path.body`, - under a frame whose `shaderVar` resolves the path's dim variable to 1.0, - produces the `toColor` of `EMLPath.at1`. - - This closes the cubical-to-pixel loop: - DimLine.at1 ↔ EMLPath.at1 ↔ compileEML(body) @ {dim uniform = 1.0}. - - **Note (P3+P5 pass).** The hypothesis `shaderVar path.dimName p u = 1.0` - is *uninhabited* for path dims that aren't in `shaderVar`'s match - (e.g. "t") — `shaderVar` returns `0.0` as fallback, never `1.0`. - The theorem therefore says nothing about parametric shaders that run - in the `canvas-rs` runtime. The inhabited successor is - `render_eq_at_pathParam` below, built on `compileEMLPath` + - `shaderVarWithDim`. -/ -theorem render_eq_at1 - (path : EMLPath) (p : PixelCoord) (u : FrameUniforms) - (hu : shaderVar path.dimName p u = 1.0) : - (compileEML path.body).semantic p u = - let v := path.at1 (fun name => shaderVar name p u) - ({ r := v, g := v, b := v, a := 1.0 } : PixelColor) := by - rw [compileEML_correct] - exact EMLPath.toColor_body_eq_at1_toColor path p u hu - --- ── Dim-aware shader semantic: inhabited rendering theorems (P3+P5) ───────── -/- - The semantic of a compiled *path* shader. Uses `shaderVarWithDim` so - the path's distinguished `dimName` is resolved via `u.pathParam` — - exactly how the `canvas-rs` runtime binds the uniform after its host- - side `PathDriver` computes the parameter from `u.time`. --/ - -/-- Rendering color of an `EMLPath` at a pixel + frame. Uses - `shaderVarWithDim` so the path's dim name resolves to `u.pathParam`. - - Greyscale projection: the EML body's Float value goes into all - three channels. See `EMLExpr.toColor` for the rationale (the - cosine cycle was removed because it aliased fibers differing by - integer multiples of 1 into pixel-identical output). -/ -def EMLPath.toColor (path : EMLPath) (p : PixelCoord) (u : FrameUniforms) : PixelColor := - let v := path.body.evalWithEnv (fun n => shaderVarWithDim path.dimName n p u) - { r := v, g := v, b := v, a := 1.0 } - -/-- **Axiom G2′**: compile an `EMLPath` to a shader handle. The path's - `dimName` determines which scalar uniform the shader references for - the path parameter; the caller binds that uniform to a fixed - `pathParam` value (no host-side animation curve — see the audit - block at the top of this file). -/ -axiom compileEMLPath : EMLPath → ShaderHandle - -/-- **Axiom G3′**: `compileEMLPath` is correct — the compiled shader's - semantic agrees with `EMLPath.toColor`. Like `compileEML_correct` - but dim-aware: the shader's semantic function uses `u.pathParam` in - place of `0.0` at `path.dimName`. -/ -axiom compileEMLPath_correct (path : EMLPath) : - (compileEMLPath path).semantic = EMLPath.toColor path - -/-- The compiled-path semantic equals `EMLPath.toColor path` pointwise — - the inhabited successor of `compileEML_semantic_eq_toColor` for - parametric shaders. -/ -theorem compileEMLPath_semantic_eq_toColor - (path : EMLPath) (p : PixelCoord) (u : FrameUniforms) : - (compileEMLPath path).semantic p u = EMLPath.toColor path p u := by - rw [compileEMLPath_correct] - -/-- **Inhabited rendering theorem.** The compiled path shader at - `(p, u)` equals the path body evaluated with the dim name resolved - to `u.pathParam` via `shaderVarWithDim`. No uninhabited hypotheses. - - For a fixed-point instance: when `u.pathParam = 1.0`, the RHS - agrees with the earlier `render_eq_at1` form (modulo - `evalWithEnv_congr` between `shaderVarWithDim` and the ad-hoc env - used by `EMLPath.at1`). For a sweep: `u.pathParam` varies - continuously and the RHS traces the path's image. -/ -theorem render_eq_at_pathParam - (path : EMLPath) (p : PixelCoord) (u : FrameUniforms) : - (compileEMLPath path).semantic p u = EMLPath.toColor path p u := - compileEMLPath_semantic_eq_toColor path p u - --- The earlier `shaderVarWithDim_eq_driverEnv`, `EMLPath.toColor_of_driver`, --- and `render_eq_at_driver` theorems were removed alongside the --- `PathDriver` scaffolding above: they all said "if the host computes --- pathParam by some `Float → Float` function, the rendering equals --- the path body evaluated at that function's output". That's a --- trivial restatement of `compileEMLPath_correct` once the function --- is fixed — and the fixed function was a sine sweep, which is not a --- transport. Use `render_eq_at_pathParam` directly (see above) for --- the abstract form, and `EMLPath.toColor_body_eq_at1_toColor` for --- the `pathParam = 1` reduction. diff --git a/Topolei/Obs/Ctx.lean b/Topolei/Obs/Ctx.lean deleted file mode 100644 index 8555dd8..0000000 --- a/Topolei/Obs/Ctx.lean +++ /dev/null @@ -1,198 +0,0 @@ -/- - Topolei.Obs.Ctx - =============== - C2 of the categories-from-the-interface stack: - **the observation category for peripherals.** - - Observations are typed peripheral configurations — they describe - *what surface the rendering lands on*, not the rendering itself. - Window dimensions, pixel format, eventually GPU adapter / mouse - position / clock tick. None of these are transports in the - cubical sense (cells-spec §1.7 calls them "sealed cells whose - interior cannot be deformed from above"); but they do organise - themselves into a category, and that category is what every - transport in the cell calculus eventually has to live over. - - ## What this file proves - - We define `Ctx` as a record (the typed peripheral) and `resize` / - `reformat` as the basic ops. We prove the *laws* that say these - ops compose like morphisms in a category: - - - **Identity**: `c.resize c.width c.height = c`, - `c.reformat c.pixelFmt = c` - - **Idempotence on overwrite**: - `(c.resize w₁ h₁).resize w₂ h₂ = c.resize w₂ h₂` - (the second resize wins; the first is forgotten) - - **Commutativity** of independent ops: - `(c.resize w h).reformat fmt = (c.reformat fmt).resize w h` - - These laws *make `Ctx` a category* — objects are values of `Ctx`, - morphisms are equivalence classes of resize/reformat sequences, - and the laws above quotient the free monoid down to the right - thing. We don't materialise the category as a Lean structure - here because the laws-as-`@[simp]`-rewrites are enough for the - reasoning we'll need; if H4 (horizontal lifts of transports - along observation arrows) needs the explicit category, we add it - then. - - ## What this file does NOT do - - - It does not define **the fibration** `p : Cells → Obs` that - the cells-spec promises. That's C5, derived from C2 + C3. - Once `Ctx` is in place, we lift `compileEMLPath` to take a - `Ctx` explicitly instead of having `width`/`height`/format - floating as bare arguments. - - - It does not define **arrows as a typed inductive** (the - `Arrow : Ctx → Ctx → Type` form I sketched earlier). That - would force us to either prove laws *up to a quotient* or - use a higher-inductive type — overhead that buys nothing - new at this layer. Instead, we use the *equational - presentation*: ops + laws. When H4 needs to talk about - "an arrow `f : c → c'`" abstractly, we'll add the typed - inductive then. - - - It does not yet bridge to `Topolei.Selection.Selection`. A - `WiredSelection` would pair `(c : Ctx) (s : Selection)` with - a compatibility predicate; deferred until we know which - compatibility actually matters for rendering. - - ## Reference - - Cells-spec §1.5 ("Rendering Context as a Cell"), §15.2 - ("Presheaf of Potential Cells"). --/ - -namespace Topolei.Obs - --- ── Pixel format ────────────────────────────────────────────────────────── --- --- The "type" of a framebuffer. Sealed-cell — its IEEE / sRGB / --- linear-RGB semantics are determined by the GPU's hardware spec, --- not by our calculus. We only carry it as a typed tag so --- rendering pipelines can refuse to bind into a context whose --- format they don't support. - -inductive PixelFormat where - /-- 32-bit float per channel; no display clamp. Used by the - `render_faithful` probe so CPU/GPU agreement isn't - dominated by quantisation. -/ - | rgbaF32 : PixelFormat - /-- 8-bit per channel sRGB. Display surface; values clamp - to [0, 1] and gamma-encode. Standard for live windows. -/ - | rgbaSrgb : PixelFormat -deriving Repr, DecidableEq, Inhabited - --- ── Observation context ─────────────────────────────────────────────────── - -/-- An observation context: the typed peripheral configuration - a render lands on. Objects of the observation category. -/ -structure Ctx where - width : Nat - height : Nat - pixelFmt : PixelFormat -deriving Repr, DecidableEq, Inhabited - -namespace Ctx - --- ── Operations (morphisms in the implicit category) ────────────────────── - -/-- Resize the observation surface. Width × height update. -/ -def resize (c : Ctx) (w h : Nat) : Ctx := - { c with width := w, height := h } - -/-- Change the pixel format. -/ -def reformat (c : Ctx) (fmt : PixelFormat) : Ctx := - { c with pixelFmt := fmt } - --- ── Identity laws: ops with the current value are no-ops ───────────────── - -/-- Resizing a context to its current dimensions is the identity. -/ -@[simp] theorem resize_self (c : Ctx) : - c.resize c.width c.height = c := by - rcases c with ⟨_, _, _⟩ - rfl - -/-- Reformatting a context to its current format is the identity. -/ -@[simp] theorem reformat_self (c : Ctx) : - c.reformat c.pixelFmt = c := by - rcases c with ⟨_, _, _⟩ - rfl - --- ── Idempotence on overwrite: the last value wins ──────────────────────── - -/-- Composed resizes collapse: only the outer dimensions matter. -/ -@[simp] theorem resize_resize (c : Ctx) (w₁ h₁ w₂ h₂ : Nat) : - (c.resize w₁ h₁).resize w₂ h₂ = c.resize w₂ h₂ := by - rcases c with ⟨_, _, _⟩ - rfl - -/-- Composed reformats collapse: only the outer format matters. -/ -@[simp] theorem reformat_reformat (c : Ctx) (f₁ f₂ : PixelFormat) : - (c.reformat f₁).reformat f₂ = c.reformat f₂ := by - rcases c with ⟨_, _, _⟩ - rfl - --- ── Commutativity of independent ops ───────────────────────────────────── - -/-- Resize and reformat commute — they touch independent fields, so - order doesn't matter. -/ -@[simp] theorem resize_reformat (c : Ctx) (w h : Nat) (fmt : PixelFormat) : - (c.resize w h).reformat fmt = (c.reformat fmt).resize w h := by - rcases c with ⟨_, _, _⟩ - rfl - -/-- Symmetric form: reformat-then-resize = resize-then-reformat. -/ -theorem reformat_resize (c : Ctx) (fmt : PixelFormat) (w h : Nat) : - (c.reformat fmt).resize w h = (c.resize w h).reformat fmt := - (Ctx.resize_reformat c w h fmt).symm - --- ── Read-back of the field updates ─────────────────────────────────────── --- These are field-update lemmas that downstream proofs will lean on. - -@[simp] theorem resize_width (c : Ctx) (w h : Nat) : - (c.resize w h).width = w := by - rcases c with ⟨_, _, _⟩ - rfl - -@[simp] theorem resize_height (c : Ctx) (w h : Nat) : - (c.resize w h).height = h := by - rcases c with ⟨_, _, _⟩ - rfl - -@[simp] theorem resize_pixelFmt (c : Ctx) (w h : Nat) : - (c.resize w h).pixelFmt = c.pixelFmt := by - rcases c with ⟨_, _, _⟩ - rfl - -@[simp] theorem reformat_width (c : Ctx) (fmt : PixelFormat) : - (c.reformat fmt).width = c.width := by - rcases c with ⟨_, _, _⟩ - rfl - -@[simp] theorem reformat_height (c : Ctx) (fmt : PixelFormat) : - (c.reformat fmt).height = c.height := by - rcases c with ⟨_, _, _⟩ - rfl - -@[simp] theorem reformat_pixelFmt (c : Ctx) (fmt : PixelFormat) : - (c.reformat fmt).pixelFmt = fmt := by - rcases c with ⟨_, _, _⟩ - rfl - -end Ctx - --- ── Concrete demo (operational sanity check via `#eval`) ────────────────── - -/-- Default rendering context: 800×600, sRGB. -/ -def defaultCtx : Ctx := - { width := 800, height := 600, pixelFmt := PixelFormat.rgbaSrgb } - -#eval defaultCtx.width -- expected: 800 -#eval defaultCtx.resize 1024 768 |>.width -- expected: 1024 -#eval (defaultCtx.resize 1024 768).reformat PixelFormat.rgbaF32 |>.pixelFmt - -- expected: PixelFormat.rgbaF32 -#eval ((defaultCtx.resize 100 100).resize 200 200).width -- expected: 200 - -end Topolei.Obs diff --git a/Topolei/RenderProbe.lean b/Topolei/RenderProbe.lean deleted file mode 100644 index def957d..0000000 --- a/Topolei/RenderProbe.lean +++ /dev/null @@ -1,158 +0,0 @@ -/- - Topolei.Render.Probe - ==================== - Empirical check for `render_faithful` — the axiom in `Topolei.GPU.Spec` - that claims the GPU's runtime output equals the compiled shader's - `ShaderSemantic` at every pixel and uniform setting. - - The Rust FFI `topolei_canvas_render_probe_path_pixel` accepts a Lean - `EMLPath` inductive structurally, builds the fragment shader as a - `naga::Module` directly via `emit_naga::build_probe_module` (no GLSL - text intermediary), submits the module to wgpu's SPIR-V backend, and - reads back a single pixel from an `Rgba32Float` offscreen target. - This module binds that FFI and exercises it on the demo paths - `plotExp` / `plotLn` / `plotTransp`. - - ## Tolerance - - IEEE 754 rounding differs slightly between the CPU (Lean-side - `ShaderSemantic` evaluation via `Float.exp`, `Float.log`, `Float.cos`) - and the GPU (wgpu → Vulkan → driver → shader unit). The comparison - therefore uses an absolute tolerance rather than bit-exact equality. - `5e-3` is generous; a bit-exact probe on known IEEE 754 drivers - (e.g. SwiftShader) might tighten it to 1e-6 or better. - - ## Running - - The probe requires a Vulkan/Metal/DX12 adapter (or a software - rasterizer like SwiftShader) on the host. When no adapter is - available the FFI returns a sentinel `RGBA { r := -1, g := -1, ... }` - and the test skips that comparison rather than asserting failure. - - Run with: `lake build probe-test && ./.lake/build/bin/probe-test`. --/ - -import Topolei.EML -import Topolei.EML.Path -import Topolei.GPU.Spec - -namespace TopoleiProbe - -/-- Rendered-pixel RGBA as four Lean `Float` values. Layout matches - the `topolei_canvas_shim_io_ok_rgba` helper in `canvas-rs/shim.c`: - tag 0, 4 boxed-Float fields. -/ -structure RGBA where - r : Float - g : Float - b : Float - a : Float -deriving Repr, Inhabited - -/-- **Structured probe.** `renderProbePath path width height time - pathParam x y` — passes the `EMLPath` inductive *structurally* to - the Rust side, which walks the object tree - (`eml::emlpath_from_lean`), builds a `naga::Module` directly via - `emit_naga::build_probe_module`, and runs the offscreen probe. - - No shader-source string crosses the FFI boundary. See - `NAGA_IR_PLAN.md` for the construction; `compileEMLPath_correct` - in `Topolei.GPU.Spec` is the contract the IR builder must satisfy. -/ -@[extern "topolei_canvas_render_probe_path_pixel"] -opaque renderProbePath - (path : @& EMLPath) - (width height : UInt32) - (time pathParam : Float) - (x y : UInt32) : IO RGBA - -/-- True when `renderProbePath` returned its GPU-unavailable sentinel. - A channel of `-1.0` is outside the valid RGBA range `[0, 1]`, so - tests can use this as a reliable skip signal. -/ -def RGBA.isSentinel (c : RGBA) : Bool := - c.r < 0.0 && c.g < 0.0 && c.b < 0.0 && c.a < 0.0 - -/-- Bridge to `GPU/Spec.lean`: the CPU-side `PixelColor` that the GPU - is expected to match. One-to-one translation. -/ -def RGBA.ofPixelColor (c : PixelColor) : RGBA := - { r := c.r, g := c.g, b := c.b, a := c.a } - -/-- Absolute-tolerance comparison for `Float`. Both arguments must - differ by no more than `tol`. -/ -def floatClose (a b tol : Float) : Bool := - let d := a - b - let ad := if d < 0.0 then -d else d - ad <= tol - -/-- Compare two RGBA values channel-wise under `tol`. -/ -def rgbaClose (expected actual : RGBA) (tol : Float) : Bool := - floatClose expected.r actual.r tol && - floatClose expected.g actual.g tol && - floatClose expected.b actual.b tol && - floatClose expected.a actual.a tol - -/-- Convert framebuffer pixel `(x, y)` at resolution `(w, h)` into the - `uv ∈ [0, 1]²` coordinates the probe shader sees at that pixel's - fragment center. Framebuffer y is top-down (wgpu origin) while - the vertex shader's uv tracks NDC, so we flip: `uv.y = 1 - - (y+0.5)/h`. `uv.x = (x+0.5)/w`. The `+0.5` lands us on the pixel - center where the rasterizer samples. -/ -def pixelUV (x y width height : UInt32) : PixelCoord := - let w := width.toNat.toFloat - let h := height.toNat.toFloat - let px := (x.toNat.toFloat + 0.5) / w - let py := 1.0 - (y.toNat.toFloat + 0.5) / h - { x := px, y := py } - -/-- Run a single probe: pass the `EMLPath` directly to the Rust - offscreen probe; compare the GPU-returned pixel `(x, y)` against - Lean's `EMLPath.toColor path (pixelUV ...) u`. -/ -def runProbe - (path : EMLPath) (label : String) - (width height : UInt32) - (time pathParam : Float) - (x y : UInt32) - (tol : Float := 5e-3) : IO Bool := do - let actual ← renderProbePath path width height time pathParam x y - if actual.isSentinel then - IO.println s!" ⏭ SKIP [{label}] at ({x}, {y}) — no GPU adapter" - return true - let p : PixelCoord := pixelUV x y width height - let u : FrameUniforms := - { time, resWidth := width.toNat.toFloat, resHeight := height.toNat.toFloat, - pathParam } - let expectedColor := EMLPath.toColor path p u - let expected := RGBA.ofPixelColor expectedColor - if rgbaClose expected actual tol then - IO.println s!" ✅ [{label}] pixel ({x}, {y}): GPU matches Lean (r={expected.r} g={expected.g} b={expected.b})" - return true - else - IO.println s!" ❌ [{label}] pixel ({x}, {y}) divergent" - IO.println s!" expected: r={expected.r} g={expected.g} b={expected.b} a={expected.a}" - IO.println s!" actual: r={actual.r} g={actual.g} b={actual.b} a={actual.a}" - return false - -/-- Full probe battery. Returns number of failures. - - Each entry asserts `EMLPath.toColor path p u ≈ GPU_pixel(p, u)` - within tolerance — i.e. the inhabited form of `render_faithful` - on the naga-IR pipeline. -/ -def runProbes : IO UInt32 := do - IO.println "── Topolei render_faithful probes ──" - IO.println " Naga-IR pixel probes (EMLPath → naga::Module → SPIR-V → GPU):" - -- Constant path (dim variable absent from body): `pathParam` is - -- irrelevant and the two endpoints coincide. - let n1 ← runProbe plotExp.toEMLPath "plotExp" 128 128 0.0 0.0 64 64 - let n2 ← runProbe plotExp.toEMLPath "plotExp" 128 128 0.0 1.0 64 64 - let n3 ← runProbe plotLn.toEMLPath "plotLn" 128 128 0.0 0.5 32 64 - -- Parametric path: `pathParam` changes the color — sample across - -- values to cover the sweep. - let n4 ← runProbe plotTransp.toEMLPath "plotTransp" 128 128 0.0 0.0 64 64 - let n5 ← runProbe plotTransp.toEMLPath "plotTransp" 128 128 0.0 0.5 64 64 - let n6 ← runProbe plotTransp.toEMLPath "plotTransp" 128 128 0.0 1.0 64 64 - let n7 ← runProbe plotTransp.toEMLPath "plotTransp" 128 128 0.0 0.5 32 96 - let mut fails : UInt32 := 0 - for ok in [n1, n2, n3, n4, n5, n6, n7] do - if !ok then fails := fails + 1 - IO.println s!"── {7 - fails.toNat} / 7 probes passed ──" - return fails - -end TopoleiProbe diff --git a/Topolei/Selection.lean b/Topolei/Selection.lean deleted file mode 100644 index 45f22ee..0000000 --- a/Topolei/Selection.lean +++ /dev/null @@ -1,291 +0,0 @@ -/- - 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 diff --git a/Topolei/Subobject.lean b/Topolei/Subobject.lean deleted file mode 100644 index 9fc0cfc..0000000 --- a/Topolei/Subobject.lean +++ /dev/null @@ -1,258 +0,0 @@ -/- - Topolei.Subobject - ================= - H3 of the selection-foundation stack. - - ## What this file is - - A `Subobject` of `WCell` is a *characteristic function*: - `σ : WCell → Bool`, identifying which cells are "in" the - subobject. Subobjects form a **Boolean algebra** under ∩, ∪, ¬ - with units ⊤, ⊥ — every law follows from `Bool`'s algebra by - pointwise reasoning, so the file is mostly `funext + simp + Bool`. - - ## Why this layer - - H1+H2 gave us *focused* selections — one cell, with a path - back to the root. H3 gives us *scoped* selections — a - *family* of candidate cells, with a focus picked from inside. - The Boolean algebra then lets us: - - - intersect two scopes (∩) — "things in both selections"; - - union two scopes (∪) — "things in either"; - - complement (¬) — "things outside this selection". - - These operations are what peripheral observations need in - order to *combine* or *refine* the cells they're looking at, - without descending to ad-hoc selection tools per peripheral - type (the VFX problem the user named: "their transports are - forgetful, so they have many selector tools"). Here, ONE - abstraction (Subobject + Boolean algebra) covers all of it. - - ## What this file does NOT contain - - - The *focused-subobject* combination (Selection × Subobject). - That goes in `Topolei.Selection` once H1+H2's focus type - is extended with a scope field. - - - The *action* of cell-endomorphisms on Subobjects. That's - `Subobject.preimage` below — actually, it IS in this file. - The thing NOT here is the action on `Selection` (which - requires the focused-subobject layer to be in place). - - - **Heyting / intuitionistic refinement** to `WCell → Type` for - proof-relevant subobjects (where membership tracks *why* a - cell is in scope). That's a future H7 — when H5 (the trace - map) needs provenance, we lift `σ`'s codomain from `Bool` to - `Type`. For now, classical Boolean is enough. - - ## Reference - - - Cells-spec §15.4 ("Lawvere-Tierney topology") — Subobjects - are exactly the level-0 instance of the LT-topology hierarchy - the cells-spec uses for accessibility / security. - - Cells-spec §1.5 ("Rendering Context as a Cell") — peripheral - observations as fiber selectors. --/ - -namespace Topolei.Subobject - --- We don't import `Topolei.Selection` to keep this file independent; --- both `Subobject` and `Selection` are foundational, neither --- depends on the other. The bridge between them lives in a --- third file. -private inductive WCell where - | mk : String → List WCell → WCell - -namespace WCell -def data : WCell → String | .mk d _ => d -def children : WCell → List WCell | .mk _ c => c -end WCell - --- ── The Subobject type ──────────────────────────────────────────────────── - -/-- A subobject of `WCell`: a Boolean-valued characteristic function - saying which cells are "in" the subobject. Equivalence between - subobjects is pointwise (function-extensional). -/ -structure Subobject where - σ : WCell → Bool -deriving Inhabited - -namespace Subobject - -/-- Two subobjects are equal iff their characteristic functions - are pointwise equal. Function extensionality is `funext`. -/ -@[ext] theorem ext {a b : Subobject} (h : ∀ c, a.σ c = b.σ c) : a = b := by - cases a; cases b - congr 1 - funext c - exact h c - --- ── Constants: ⊤ (everywhere) and ⊥ (nowhere) ──────────────────────────── - -/-- The total subobject — every cell is in it. -/ -def top : Subobject := { σ := fun _ => true } - -/-- The empty subobject — no cell is in it. -/ -def bot : Subobject := { σ := fun _ => false } - --- ── Pointwise operations ───────────────────────────────────────────────── - -/-- Intersection (AND of characteristic functions). -/ -def inter (a b : Subobject) : Subobject := { σ := fun c => a.σ c && b.σ c } - -/-- Union (OR of characteristic functions). -/ -def union (a b : Subobject) : Subobject := { σ := fun c => a.σ c || b.σ c } - -/-- Complement (NOT of characteristic function). -/ -def compl (a : Subobject) : Subobject := { σ := fun c => !(a.σ c) } - --- ── Boolean algebra laws (every one follows from `Bool` algebra) ────────── - --- The pattern: `ext c; simp [Subobject.inter, Subobject.union, Subobject.compl, --- Subobject.top, Subobject.bot, Bool.]`. - --- ── Commutativity ───────────────────────────────────────────────────────── - -@[simp] theorem inter_comm (a b : Subobject) : a.inter b = b.inter a := by - ext c; simp [inter, Bool.and_comm] - -@[simp] theorem union_comm (a b : Subobject) : a.union b = b.union a := by - ext c; simp [union, Bool.or_comm] - --- ── Associativity ───────────────────────────────────────────────────────── - -theorem inter_assoc (a b c : Subobject) : - (a.inter b).inter c = a.inter (b.inter c) := by - ext x; simp [inter, Bool.and_assoc] - -theorem union_assoc (a b c : Subobject) : - (a.union b).union c = a.union (b.union c) := by - ext x; simp [union, Bool.or_assoc] - --- ── Idempotence ─────────────────────────────────────────────────────────── - -@[simp] theorem inter_self (a : Subobject) : a.inter a = a := by - ext c; simp [inter] - -@[simp] theorem union_self (a : Subobject) : a.union a = a := by - ext c; simp [union] - --- ── Identity laws (top is unit of ∩, bot is unit of ∪) ─────────────────── - -@[simp] theorem inter_top (a : Subobject) : a.inter top = a := by - ext c; simp [inter, top] - -@[simp] theorem top_inter (a : Subobject) : top.inter a = a := by - ext c; simp [inter, top] - -@[simp] theorem union_bot (a : Subobject) : a.union bot = a := by - ext c; simp [union, bot] - -@[simp] theorem bot_union (a : Subobject) : bot.union a = a := by - ext c; simp [union, bot] - --- ── Annihilation (top is absorber of ∪, bot of ∩) ──────────────────────── - -@[simp] theorem inter_bot (a : Subobject) : a.inter bot = bot := by - ext c; simp [inter, bot] - -@[simp] theorem bot_inter (a : Subobject) : bot.inter a = bot := by - ext c; simp [inter, bot] - -@[simp] theorem union_top (a : Subobject) : a.union top = top := by - ext c; simp [union, top] - -@[simp] theorem top_union (a : Subobject) : top.union a = top := by - ext c; simp [union, top] - --- ── Distributivity ─────────────────────────────────────────────────────── - -theorem inter_distrib_union (a b c : Subobject) : - a.inter (b.union c) = (a.inter b).union (a.inter c) := by - ext x; simp [inter, union, Bool.and_or_distrib_left] - -theorem union_distrib_inter (a b c : Subobject) : - a.union (b.inter c) = (a.union b).inter (a.union c) := by - ext x; simp [union, inter, Bool.or_and_distrib_left] - --- ── Complement laws ────────────────────────────────────────────────────── - -@[simp] theorem compl_compl (a : Subobject) : a.compl.compl = a := by - ext c; simp [compl] - -@[simp] theorem inter_compl_self (a : Subobject) : a.inter a.compl = bot := by - ext c; simp [inter, compl, bot] - -@[simp] theorem union_compl_self (a : Subobject) : a.union a.compl = top := by - ext c; simp [union, compl, top] - -@[simp] theorem compl_top : (top : Subobject).compl = bot := by - ext c; simp [compl, top, bot] - -@[simp] theorem compl_bot : (bot : Subobject).compl = top := by - ext c; simp [compl, top, bot] - --- ── De Morgan ──────────────────────────────────────────────────────────── - -theorem compl_inter (a b : Subobject) : - (a.inter b).compl = a.compl.union b.compl := by - ext c; simp [inter, union, compl, Bool.not_and] - -theorem compl_union (a b : Subobject) : - (a.union b).compl = a.compl.inter b.compl := by - ext c; simp [union, inter, compl, Bool.not_or] - --- ── Absorption ─────────────────────────────────────────────────────────── - -@[simp] theorem inter_union_self (a b : Subobject) : - a.inter (a.union b) = a := by - ext c; cases h : a.σ c <;> simp [inter, union, h] - -@[simp] theorem union_inter_self (a b : Subobject) : - a.union (a.inter b) = a := by - ext c; cases h : a.σ c <;> simp [inter, union, h] - --- ── Bridge to construction: scope-preserving endomorphisms ─────────────── --- --- A peripheral observation produces a `Subobject` (the cells the --- user is "looking at"). A constructor — i.e. an endomorphism of --- WCell — should respect the scope: cells inside the selection --- map to cells inside the selection. This is the type-level --- guarantee that "applying a tool to a selection produces a new --- valid selection". --- --- Note: this is a *property* of an endomorphism, not a structure --- on Subobjects. The action on Selections — `applyEndo` — uses --- this property as a precondition; it lives in --- `Topolei.Selection.Scoped` (next module to land). - -/-- `f` preserves the subobject `a`: every cell in `a` maps to a - cell in `a`. This is the scope-preservation precondition - for actions on focused-subobject selections. -/ -def Preserves (a : Subobject) (f : WCell → WCell) : Prop := - ∀ c, a.σ c = true → a.σ (f c) = true - -/-- The identity always preserves any subobject. -/ -theorem Preserves.id (a : Subobject) : Preserves a (fun c => c) := fun _ h => h - -/-- Composition of preserving endomorphisms is preserving. -/ -theorem Preserves.comp {a : Subobject} {f g : WCell → WCell} - (hf : Preserves a f) (hg : Preserves a g) : - Preserves a (fun c => f (g c)) := fun c hc => hf (g c) (hg c hc) - -end Subobject - --- ── Operational sanity check ────────────────────────────────────────────── - -/-- A demo subobject: cells whose data starts with `"leaf"`. -/ -def demoLeaves : Subobject := { σ := fun c => c.data.startsWith "leaf" } - -#eval demoLeaves.σ (WCell.mk "leaf-A" []) -- expected: true -#eval demoLeaves.σ (WCell.mk "root" []) -- expected: false -#eval (demoLeaves.inter demoLeaves).σ (WCell.mk "leaf-A" []) -- expected: true -#eval (demoLeaves.compl).σ (WCell.mk "root" []) -- expected: true -#eval (Subobject.top : Subobject).σ (WCell.mk "anything" []) -- expected: true -#eval (Subobject.bot : Subobject).σ (WCell.mk "anything" []) -- expected: false - -end Topolei.Subobject diff --git a/Topolei/Trace.lean b/Topolei/Trace.lean deleted file mode 100644 index 09a742a..0000000 --- a/Topolei/Trace.lean +++ /dev/null @@ -1,188 +0,0 @@ -/- - Topolei.Trace - ============= - H5 of the foundation stack: the **trace map** (inverse projection), - in its polymorphic form. - - ## Polymorphic from day 1 - - A `Trace α` is a list of contributing items of type `α`. The - parameter `α` is what the trace is *about*: - - - `Trace WCell` — workspace-tree-level traces (rendered → workspace cells) - - `Trace CTerm` — cubical-syntax-level traces (rendered → CTerm sub-terms) - - `Trace SourceLoc` — file/line provenance traces - - `Trace HandleId` — GPU resource provenance - - All four are the same algebraic structure (a free monoid on `α`) with - the same theorems. Polymorphism captures the "consolidate - abstractions" principle: ONE Trace type, instantiated wherever - needed. - - ## Why this layer - - H1+H2 (Selection) gave us *focus + history* — pointing at one cell - with a path back to the root. H3 (Subobject) gave us the *algebra - of scopes*. C2 (Obs.Ctx) gave us the typed peripheral category. - - H5 closes the loop: every rendered element carries a typed pointer - back to the source items that contributed to producing it. This is - the **inverse projection**: given an output (a pixel, region, curve - on the screen), recover the items whose values projected to it. - - ## Geometry, sheaves, bundles, differential structure: ALL derived - - Differential geometry is NOT a separate framework bolted onto cells. - It is a *property of trace coherence* — particular shapes of how - `Trace`s relate across the rendered space: - - - Trace varies smoothly across adjacent rendered elements → - differential structure (Jacobian / connection / parallel transport). - - Trace shares open sets in coherent overlaps → sheaf structure. - - Multiple sources project to one rendered point → fiber bundle's - preimage at that point. - - The same `Trace α` carries all three; the geometry is in the - *predicates* / *projections* over traces, not in the type itself. - - ## What this file does NOT contain - - - **The cubical-trace function** `CTerm → Trace CTerm`. That's a - sibling file `Topolei.Cubical.Trace`, which uses *this* `Trace`. - - **DecidableEq-dependent operations** (diff, intersect). Add when - needed; they require `DecidableEq α`. - - ## Reference - - Cells-spec §1.5 ("Rendering Context as a Cell"), §17.1 - ("Vulnerabilities as Topological Failures": side channels are - unwanted traces — same abstraction, security layer). --/ - -namespace Topolei.Trace - --- ── The Trace structure ─────────────────────────────────────────────────── - -/-- A trace: the typed list of items that contributed to producing a - rendered element. Polymorphic in the item type — instantiate - with `WCell` for workspace-tree traces, `CTerm` for cubical- - syntax traces, etc. Plural because at singularities, multiple - sources project to one rendered point — and even at non-singular - points the trace can include intermediate items (the sub-things - visited en route to the final output). - - The list order records *contribution order*: the first item is - the deepest contributor, the last is the most recently seen. This - order matters when stacking traces during rendering. -/ -structure Trace (α : Type) where - items : List α -deriving Repr, Inhabited - -namespace Trace - -/-- The empty trace — no items contributed. Identity element for - `union`, witness that some rendered element has no source-item - provenance (e.g., a clear-color background pixel). -/ -def empty {α : Type} : Trace α := { items := [] } - -/-- The single-item trace — exactly one source-item contributed. - Used when lifting a primitive into a traced render. -/ -def single {α : Type} (a : α) : Trace α := { items := [a] } - -/-- Combine two traces by concatenating their item lists. Order is - preserved: the second trace's items append after the first's. - This is *not* deduplicating — if an item appears in both traces - we keep both occurrences (the list is multi-set-like). - Deduplication is a separate operation that requires `DecidableEq`. -/ -def union {α : Type} (t₁ t₂ : Trace α) : Trace α := - { items := t₁.items ++ t₂.items } - --- ── Monoid laws (empty is unit, union is associative) ──────────────────── - -@[simp] theorem empty_union {α : Type} (t : Trace α) : - Trace.empty.union t = t := by - cases t; simp [union, empty] - -@[simp] theorem union_empty {α : Type} (t : Trace α) : - t.union Trace.empty = t := by - cases t; simp [union, empty] - -theorem union_assoc {α : Type} (t₁ t₂ t₃ : Trace α) : - (t₁.union t₂).union t₃ = t₁.union (t₂.union t₃) := by - cases t₁; cases t₂; cases t₃ - simp [union, List.append_assoc] - -end Trace - --- ── TracedRender: rendered element + its trace ──────────────────────────── - -/-- A `TracedRender R α` is a rendered element of type `R` paired - with its `Trace α`. Every rendered output carries its provenance. - - `R` is the rendered-element type (e.g. `PixelColor`, a region - label, a frame). `α` is the trace-item type (e.g. `WCell`, - `CTerm`, …). Polymorphism lets the same abstraction work for - every rendering granularity and every provenance shape. -/ -structure TracedRender (R α : Type) where - render : R - trace : Trace α -deriving Repr, Inhabited - -namespace TracedRender - -/-- Lift a rendered value with a single-item trace. -/ -def lift {R α : Type} (r : R) (a : α) : TracedRender R α := - { render := r, trace := Trace.single a } - -/-- Lift a rendered value with no trace — for outputs without source- - item provenance (clear color, vertex-stage outputs, sealed-cell - rasterizer products). -/ -def liftEmpty {R α : Type} (r : R) : TracedRender R α := - { render := r, trace := Trace.empty } - -/-- Combine two traced renders by combining their traces (left-biased - on the rendered value). -/ -def merge {R α : Type} (a b : TracedRender R α) : TracedRender R α := - { render := a.render, trace := a.trace.union b.trace } - -@[simp] theorem merge_liftEmpty {R α : Type} (a : TracedRender R α) (r' : R) : - a.merge (liftEmpty r') = a := by - cases a; simp [merge, liftEmpty, Trace.union_empty] - -@[simp] theorem liftEmpty_merge {R α : Type} (r : R) (b : TracedRender R α) : - (liftEmpty r).merge b = { render := r, trace := b.trace } := by - simp [merge, liftEmpty, Trace.empty_union] - -end TracedRender - --- ── Operational sanity (polymorphic on `String` items as the simplest demo) ─ - -/-- Demo trace over `String` items. -/ -def demoTrace : Trace String := - (Trace.single "alpha").union (Trace.single "beta") - -#eval demoTrace.items.length -- expected: 2 -#eval demoTrace.items -- expected: ["alpha", "beta"] - -/-- Empty-union round-trip. -/ -example : (Trace.empty : Trace String).union demoTrace = demoTrace := - Trace.empty_union _ - -/-- Union associativity exercised on three traces. -/ -example : - let t₁ := (Trace.single "a" : Trace String) - let t₂ := Trace.single "b" - let t₃ := Trace.empty - (t₁.union t₂).union t₃ = t₁.union (t₂.union t₃) := - Trace.union_assoc _ _ _ - -/-- `TracedRender` lift carries the trace it's given. -/ -example : (TracedRender.lift (5 : Nat) "src").trace = Trace.single "src" := rfl - -/-- Merging into a no-trace render leaves the trace alone. -/ -example : - let a : TracedRender Nat String := TracedRender.lift 5 "src" - a.merge (TracedRender.liftEmpty 7) = a := - TracedRender.merge_liftEmpty _ _ - -end Topolei.Trace diff --git a/ZIGZAG_PORT.md b/ZIGZAG_PORT.md deleted file mode 100644 index e2f714d..0000000 --- a/ZIGZAG_PORT.md +++ /dev/null @@ -1,343 +0,0 @@ -# Zigzag Engine — Lean Port Plan - -*Parallel to `TRANSPORT_PLAN.md` (which guided Phase 1's cubical -formalisation). This document plans the step-by-step port of the -zigzag engine from its Rust reference implementation into Lean 4, -as the combinatorial n-category backend for topolei's cell layer.* - ---- - -## Decision (2026-04-22) - -**The zigzag engine will be reimplemented in Lean 4.** The existing -Rust implementation at `zigzag-engine/zigzag-engine/` is **reference -material only** — a structural template for the port, not a -dependency. This matches topolei's Lean-as-host discipline and -maximises the project's medium-term goal of Lean-native reasoning -(see below). - -**The only Rust component in topolei is the cubical evaluator FFI -backend** — the module that discharges the Phase 1 axioms (`step`, -`eval`, `vApp`, `vPApp`, `vTransp`, etc.) via `@[extern]` + -`@[implemented_by]`. That one Rust crate exists to extend Lean 4 with -computational cubical-transport HoTT; it is topolei's reason for any -FFI whatsoever. Everything else lives in Lean. - -## Why Lean, not the Rust backend (Option A over Option B) - -Axiomatic Rust backends give `axiom normalise_idempotent : ...` — -statements we can *use* but cannot *prove*. Porting to Lean makes -each such statement a **theorem** the kernel checks. The project's -medium-term goal is to maximise what can be reasoned about in Lean; -that forecloses FFI-backed hiding of mathematical content. - -The Rust implementation was itself AI-assisted and is not -hand-polished artefact we are throwing away; it is a -test-oracle-quality scaffold that the Lean port can match against. - -## Reference materials - -- `zigzag-engine/papers/zigzag-normalisation-2205.08952.pdf` — - Heidemann-Reutter-Vicary, LICS 2022. The algorithm (Construction 17) - and correctness (Proposition 19). -- `zigzag-engine/papers/layout-algorithm-2305.06938.pdf` — - Tataru-Vicary, 2024. Explosion / k-points / layout. -- `zigzag-engine/papers/homotopy-io-2402.13179.pdf` — - Corbyn et al., FSCD 2024. The parent proof assistant. -- `zigzag-engine/zigzag-engine/src/*.rs` — reference Rust - implementation (11,003 lines across 13 modules). -- `zigzag-engine/zigzag-engine-spec/zigzag-engine-spec.md` — original - spec for the reference implementation. - -## Port destination - -All Lean modules land under `Topolei/Zigzag/`: - -| Lean module | Rust reference | Approx. size | -|-------------|----------------|--------------| -| `Zigzag/Monotone.lean` | `src/monotone.rs` (325 LOC) | ~150 LOC + proofs | -| `Zigzag/Core.lean` | `src/zigzag.rs` (291 LOC) | ~150 LOC | -| `Zigzag/Diagram.lean` | `src/diagram.rs` (1484 LOC) | ~600 LOC | -| `Zigzag/Signature.lean` | `src/signature.rs` (200 LOC) | ~100 LOC | -| `Zigzag/Degeneracy.lean` | `src/degeneracy.rs` (1284 LOC) | ~500 LOC + proofs | -| `Zigzag/Normalise.lean` | `src/normalise.rs` (849 LOC) | ~400 LOC + proofs | -| `Zigzag/Typecheck.lean` | `src/typecheck.rs` (597 LOC) | ~250 LOC | -| `Zigzag/Explosion.lean` | `src/explosion.rs` (1414 LOC) | ~500 LOC | -| `Zigzag/Tests.lean` | `tests/` + `examples/` | ~200 LOC `#eval` regressions | - -**Intentionally not ported:** -- `src/import.rs` (1491 LOC) — homotopy.io interop, not needed. -- `src/discover.rs` (1981 LOC) — search over diagrams; decide later. -- `src/python.rs` (716 LOC) — Python bindings, not needed. -- `src/layout.rs` (320 LOC) — geometric layout; deferred to Phase 4 - Interaction, may be a Lean module or may defer to a Rust - `@[implemented_by]` optimisation later. - -Core port size: roughly **2,500–3,000 Lean lines** to match the -algorithmic core of the Rust implementation, with proofs adding -perhaps another 1,000–2,000 depending on how far the correctness -theorems are pursued (Step 9 below). - ---- - -## Steps - -### Step 1 — `Zigzag/Monotone.lean` (foundation) - -**Content**: -- `MonotoneMap (n m : Nat)` structure with `entries : List (Fin m)` - and `is_monotone` proof. -- Composition, identity, face maps `dᵢ`. -- Wraith's R equivalence `Δ₊ → Δ₌ᵒᵖ` as a pure function. -- Preimage computation. - -**Proofs**: -- `MonotoneMap.compose_assoc` -- `MonotoneMap.wraith_r_involution` (R² = id on the nose) -- `MonotoneMap.face_map_image` — face maps omit exactly one element. - -**Deliverable test**: `#eval` the `inspect_half_braid` example's -monotone substructure; compare to the Rust engine's output on the -same input. - ---- - -### Step 2 — `Zigzag/Core.lean` (zigzags themselves) - -**Content**: -- `Zigzag (T : Type) : Type` — `{ regular : Vec T, singular : Vec T, - forward : Vec Morphism, backward : Vec Morphism }`. -- `ZigzagMap` — singular map `fˢ : n → m` in `Δ₊` with regular/singular - slices and the commutativity conditions as `Prop`-valued fields. -- Composition of zigzag maps. - -**Proofs**: -- `ZigzagMap.compose_respects_commutativity` — composition preserves - the commutativity predicates. -- `Zigzag.identity_is_length_zero` — the identity zigzag is trivially - a zero-length zigzag. - ---- - -### Step 3 — `Zigzag/Diagram.lean` (the main data structure) - -**Content**: -- Mutual inductive `Diagram` / `DiagramN` / `Cospan` / `Rewrite` / - `Cone`. Same shape as Rust's `pub enum Diagram { Diagram0(Generator) - | DiagramN(DiagramN) }`. -- Smart constructors: `Diagram.identity`, `Diagram.attach`, - `Diagram.compose`. -- Dimension predicate `Diagram.dimension : Diagram → Nat`. -- Source/target extractors. -- Regular-slice / singular-slice computation (mirrors - `DiagramN.regular_slice` in the Rust). - -**Proofs**: -- `Diagram.dimension_of_attach` — attaching a generator of dimension `k` - produces a diagram of dimension `k`. -- `Diagram.source_source` / `Diagram.target_target` boundary - consistency. -- Globularity predicate + decidability. - ---- - -### Step 4 — `Zigzag/Signature.lean` - -**Content**: -- `Generator` structure: id, dimension, invertibility. -- `GeneratorData` with source / target diagrams. -- `Signature` as a list/hashmap of `GeneratorData`. -- Well-formedness: every `GeneratorData`'s source / target dimension - = `generator.dimension - 1`. - -**Proofs**: -- `Signature.well_formed` is decidable. - ---- - -### Step 5 — `Zigzag/Degeneracy.lean` - -**Content**: -- Predicates: `IsSimpleDegeneracy`, `IsParallelDegeneracy`, - `IsDegeneracy` (closure under composition of the first two). -- Constructors: `Degeneracy.insert_identity_cospan` (the basic simple - degeneracy). -- Factorisation: every degeneracy factors as simple ∘ parallel - (Lemma 7 from the paper). - -**Proofs**: -- `Degeneracy.isomorphisms_are_degeneracies` (Lemma 6). -- `Degeneracy.factorisation_unique_up_to_iso` (Lemma 7). -- `Degeneracy.is_monomorphism` (Lemma 8). -- `Degeneracy.left_cancellation` (Lemma 10). -- `Degeneracy.finite_subobjects` (Lemma 14). - -**This step is where the bulk of the Phase 1-style proof work sits.** -Some of these may start as `axiom` and promote to `theorem` as the -infrastructure firms up — same pattern as how T1/T2/C1/C2 worked in -`Cubical/TransportLaws.lean`. - ---- - -### Step 6 — `Zigzag/Pullback.lean` (Proposition 13) - -**Content**: -- Pullback construction for degeneracy maps. -- `pullback_is_degeneracy` statement. - -**Proofs**: -- `Degeneracy.pullback_exists` — the construction terminates. -- `Degeneracy.pullback_legs_are_degeneracies` (Proposition 13). - -**Note**: Proposition 13 is the most algorithmically dense piece. **OK -to start as an axiom**. Pattern to follow: state the axiom, write the -construction as a `partial def` with test-case regression, upgrade to -a total def + theorem when the proof is clearer. Exactly how -`step`/`eval` were handled in Phase 1. - ---- - -### Step 7 — `Zigzag/Normalise.lean` (Construction 17) - -**Content**: -- `NormalisationResult` structure: `normal_form`, `degeneracy`, - `factorisations`. -- `Sink` structure for relative normalisation. -- `normalise : Diagram → NormalisationResult` (absolute case). -- `normalise_sink : Sink → NormalisationResult` (relative case). -- Termination: structural recursion on `Diagram.dimension`. - -**Proofs**: -- `normalise_idempotent` — the headline result (easy, structural). -- `normalise_preserves_globularity` (Proposition 23). -- `normalise_correctness` (Proposition 19) — relative to the axiom - set from Steps 5–6. - -**Test**: port Rust unit tests from `tests/integration_tests.rs` to -Lean `#eval` regressions (Eckmann-Hilton dim 3, syllepsis dim 5, -Figure 6 dim 4 essential-identity). - ---- - -### Step 8 — `Zigzag/Typecheck.lean` - -**Content**: -- `SingularContent` extraction. -- Piece decomposition. -- `type_check : Diagram → Signature → Except TypeError Unit`. - -**Proofs**: -- `type_check_sound` — if `type_check D Σ` returns `ok`, then all - pieces' normalisations are in `Σ`. - ---- - -### Step 9 — `Zigzag/Tests.lean` (regression battery) - -Port the Rust test cases: -- `tests/integration_tests.rs` — normalisation regressions. -- `tests/nontrivial_constructors.rs` — diagram construction. -- `examples/inspect_half_braid.rs` — the Eckmann-Hilton braiding. -- `examples/render_braiding.rs` — braiding as a 3-diagram. -- `examples/scaffold_analysis.rs` / `trace_scaffold.rs` / `trace_merge.rs` — reduction traces. - -Each becomes a Lean `#eval` or `example` proving the expected output. -These are the correctness gradient that catches porting errors early. - ---- - -### Step 10 — `Cell/Zigzag.lean` (bridge to cubical core) - -**Content**: -- Translator: `CType → Option Diagram` for the dimensions where both - make sense (0-cells, 1-cells, 2-cells-via-Path). -- Translator: `Diagram → Option CType` for the inverse. -- Identity / compose / whisker operations at the `Cell` layer that - dispatch to the right backend: cubical for low dimensions (where - univalence matters), zigzag for higher dimensions (where - combinatorial composition dominates). - -**This is where the two formalisms meet.** Cubical Phase 1 gives us -equivalence and transport; Zigzag gives us higher-composition and -normalisation; `Cell/` combines them. - ---- - -## Explosion and layout (post-core) - -Steps 11+ (not critical for the n-category reasoning goal): -- `Zigzag/Explosion.lean` — k-points, poset structure. Lean-native - port of `src/explosion.rs` (1414 Rust LOC). -- `Zigzag/Layout.lean` — constraint system. May remain pure Lean or - may defer the QP solver to a Rust `@[implemented_by]` optimisation. - **Decided later** once performance requirements are known. - ---- - -## Axiom discipline (from Phase 1 experience) - -The port follows the same axiom-first pattern established in Phase 1: - -1. **First pass**: data structures pure; algorithm as `def` (maybe - `partial def`); key correctness statements as `axiom`. -2. **Second pass**: tighten `partial def` into `def` with structural - termination; promote axioms to theorems where the proof is - mechanical. -3. **Third pass**: prove the hard theorems (Proposition 13, correctness - of Construction 17 relative to the degeneracy axioms). - -At every stage, axioms are **formal specs for what the algorithm must -satisfy**, not blanket assumptions. The Rust reference implementation -tests each axiom via example; the Lean port must match those tests. - ---- - -## Relationship to topolei's other phases - -- **Phase 1 (Cubical Core)** — complete. Not touched by this port. -- **Phase 2 (Cells)** — the zigzag Lean port *is* a prerequisite for - cells-spec §6.3 "Higher Cells". `Cell/Basic.lean` can begin using - cubical-only semantics for 0/1/2-cells; higher cells use the zigzag - backend from Step 10 above. -- **Rust FFI (cubical evaluator)** — independent work stream. The - zigzag port does not depend on it. When the Rust FFI lands, it - backs the cubical axioms; the zigzag Lean code becomes a consumer - of the now-computational cubical layer. -- **Numerical layer** (`NUMERICAL.md`) — independent. Schemes can use - zigzag diagrams as structural source / target types once the port - is complete. - ---- - -## Sizing - -- Steps 1–4: ~2 weeks (data structures + basic algorithms). -- Steps 5–7: ~3–4 weeks (degeneracy + normalisation + proofs; this - is the heart of the port). -- Step 8: ~3 days. -- Step 9: ~1 week (regression battery). -- Step 10: ~1 week (bridge). -- **Total: 6–8 weeks** for the core port with correctness theorems. - -Comparable to Phase 1 in size; same single-developer feasibility. - ---- - -## Success criteria - -The port is **complete** when: - -1. All regression tests from `zigzag-engine/tests/` pass as Lean - `#eval`s or `example`s. -2. `normalise_idempotent` is a theorem (not an axiom). -3. The Eckmann-Hilton (dim 3), syllepsis (dim 5), and Figure 6 (dim 4) - examples type-check and normalise to their documented results. -4. `Cell/Zigzag.lean` (Step 10) compiles and bridges to the cubical - core without circular dependencies. -5. `STATUS.md` can claim "Phase 2 Higher-Cell backend: closed in Lean" - with zero Rust dependency (beyond the cubical-evaluator FFI, which - is a separate work stream). - -At that point, topolei has a Lean-native combinatorial n-category -engine, provably correct where proven, with the Rust zigzag engine -demoted from reference to archive. diff --git a/build.sh b/build.sh index bed2dec..aafd501 100755 --- a/build.sh +++ b/build.sh @@ -2,30 +2,13 @@ set -e cd "$(dirname "$0")" -GLFW_PC="/nix/store/hpdf5fwl5arkc8d625cxba604i8dwnvp-glfw-3.4/lib/pkgconfig" -GL_PC="/nix/store/q9fb1ps2fxa8p4n13mbsijz9w0svhsd4-libglvnd-1.7.0-dev/lib/pkgconfig" -GLEW_PC="/nix/store/nw97c9lkxpzmpq99sgda8aa8xp9q9q4f-glew-2.2.0-dev/lib/pkgconfig" -GLU_PC="/nix/store/vrfv132mqnh44001g96iczc31n1rpgc8-glu-9.0.3-dev/lib/pkgconfig" -export PKG_CONFIG_PATH="$GLFW_PC:$GL_PC:$GLEW_PC:$GLU_PC:$PKG_CONFIG_PATH" - -echo "── building Rust canvas (wgpu + winit) ──" -# Replaces the old C++ canvas.cpp — targets Vulkan/Metal/DX12/WebGPU -# via wgpu, with cross-platform window via winit, shader translation -# via naga-glsl. The C++ canvas.cpp + CMakeLists.txt are retained as -# reference but no longer linked. -(cd native/canvas-rs && cargo build --release) - -echo "── building Rust cubical backend ──" +echo "── building Rust cubical kernel ──" # Native staticlib for Lean linkage. Wasm build is a separate step # invoked by `cargo build --target wasm32-unknown-unknown` on demand. (cd native/cubical && cargo build --release) -echo "── building Rust render backend ──" -# Scaffolding crate for future render-side FFI work. -(cd native/render && cargo build --release) - -echo "── building Lean ──" +echo "── building Lean library + tests ──" lake build echo "── done ──" -echo "run: ./.lake/build/bin/topolei" +echo "run: ./.lake/build/bin/cubical-test" diff --git a/cells-spec.md b/cells-spec.md deleted file mode 100644 index ba209ff..0000000 --- a/cells-spec.md +++ /dev/null @@ -1,2391 +0,0 @@ -# Cells: A Cubical Transport Calculus for Interactive Computation - -## Specification and Rendering Context for Lean 4 - -**Version:** 0.1.0-draft -**Status:** Foundational specification — no external dependencies - ---- - -## 1. Motivating Principle - -### 1.1 Why Not Buffers - -Emacs was built on a single primitive: the **buffer** — a mutable sequence of characters with a cursor position, plus metadata. The genius was that everything reduced to this: files, REPLs, shell sessions, mail, version control, games. A buffer is simultaneously the data, the view, and the interaction surface. Its key properties are that it's linear, mutable in place, addressable by position, and composable. - -But buffers encode a fundamental assumption: **the primary object of thought is a linear text stream.** That was right for decades. It isn't anymore. The objects people work with now are graphs, not streams — dependency graphs, type hierarchies, dataflow, conversation trees, knowledge graphs, proof trees, UI component trees, API call chains. The operations people want are not "insert character at point" but "transform this structure while preserving these invariants." The feedback loop is not "edit text, run command, read output" but "manipulate a live structure and watch consequences propagate." - -### 1.2 The Cell as Primitive - -If buffers were the right primitive for the era of linear text, what is the right primitive for the era of structured, interactive, type-aware computation? - -A **cell**. But not a spreadsheet cell — a typed, reactive, addressable, composable unit of computation with bidirectional connections. A cell is: - -- **A value** of some type (could be text, a function, a type itself, a diagram, a proof term, an image, a running process, a shader). -- **A boundary** — its interface to the outside world, defined by its type. This is what other cells can see and connect to. -- **An interior** — its implementation, which can itself be a network of sub-cells. This is where the fractal self-similarity comes from, analogous to how an Emacs buffer can contain the output of a process that is itself running Emacs. -- **Reactive bindings** — when cells connected to it change, it can recompute. But unlike a spreadsheet, the dataflow graph is first-class and manipulable. -- **A projection** — how the cell renders itself for human interaction. A single cell can have multiple projections (text, graphical, diagrammatic, tabular), and projections are themselves cells. - -Just as every Emacs command operates on buffers and returns buffers, every operation in this system operates on cells and returns cells. A cell whose value is a transformation on cells is a macro/command. A cell whose value is a type is a schema/mode. A cell whose value is a projection function is a renderer/view. A cell whose boundary has no free ports is a complete program. A cell whose boundary has free ports is a component/library/API. - -The self-referential closure — the equivalent of Emacs being written in Elisp operating on buffers — is that the IDE itself is a cell, its configuration is sub-cells, its commands are cells whose types are endomorphisms on the workspace cell, and modifying the IDE is just refining cells like modifying anything else. - -### 1.3 The Foundational Collapse: Cells ARE Transports - -The deep realization is that a cell isn't a thing that *has* transport. A cell **is** a transport. - -A transport is a map induced by a path in a type family: - -``` -i : 𝕀 ⊢ A(i) type -tr[i.A(i), 0→1] : A(0) → A(1) -``` - -A cell from `A` to `B` is a line of types whose endpoints are `A` and `B`. The cell's computation is transport along this line. The cell's rendering is a projection of this line. The cell's interaction surface is the fiber over any point selected along it. - -This identification has immediate consequences: - -- **Composition** of cells is composition of transports, which is path concatenation. -- **Identity cells** are reflexivity — transport along a constant path, the identity function. -- **Parallel composition** is product transport. -- **The monad laws are the groupoid laws on paths** — associativity of concatenation, identity cancellation — which hold definitionally in cubical type theory. -- **Higher cells are higher transports.** A 2-cell between two cells is a homotopy. A user dragging a slider and watching a shader change continuously is tracing a 2-cell. An n-cell is an n-cube of transports. - -The dimension hierarchy: - -``` -0-cell : a value — a point, a constant, a color -1-cell : a transport — a function, a shader pass, a wire -2-cell : a homotopy — a continuous deformation, a parameter sweep -3-cell : a homotopy of — a second-order variation, - homotopies an animation of an animation -n-cell : ... — full generality -``` - -### 1.4 The Surreal Direct-Manipulation Vision - -The system this specification describes is one where you see a rendered form (a surface, a flow field, a material), grab a point on it (selecting a fiber of the rendering projection), deform it (tracing a path in parameter space), and watch consequences propagate through the cell network at frame rate because the compilation cell emits GPU code. The type of the thing you're manipulating is visible as its shape. The type system prevents ill-typed deformations. The rendering is a projection of the computation, and manipulation of the rendering is transported back to a deformation of the computation. - -### 1.5 The Rendering Context as a Cell - -The screen is a (discretized) 2-manifold `S`. At each pixel `p ∈ S`, the fiber is the space of possible visual outputs — color, transparency, depth. A rendering context is a section of this fibration — an assignment of visual data to every pixel. Mouse position is a fiber selector: the mouse at `p` evaluates `π⁻¹(p)`, the set of computational cells projecting to that screen point. Clicking selects a section. Dragging transports a section along a path in screen space, lifted back to the computational space. - -The fiber doesn't have to be selected by a point. Area selection, lasso, type-based selection — each is a different sub-presheaf of the projection sheaf. The number of distinct generic projections of an n-cell onto the 2D screen is classified by singularity theory (Whitney, Thom-Boardman), and each singularity type — fold curves, cusps, swallowtails — is itself a natural interaction handle in the interface. - -### 1.6 Expanding the Cell Space: Pullbacks and Kan Extensions - -The cell system begins in a small subspace of all possible computation. External capabilities — GPU APIs, audio engines, physics solvers, network protocols — live in their own type systems. The process of absorbing an external tool into the cell formalism is a **pullback**: finding the universal type that maps into both the cell calculus and the external API, agreeing on shared computational semantics. - -But a single pullback doesn't capture the full story. The external tool may have capabilities the cell calculus doesn't yet express (requiring a **right Kan extension** to construct new cell types that map to the external features), and the cell calculus has structure the external tool can't see (transport, composition, higher coherence — a **left Kan extension** in the other direction). The iterative process of absorbing tools is building a diagram of these extensions, and the total system at any point is the **homotopy limit** of the resulting diagram — a Grothendieck fibration whose sections are programs. - -### 1.7 The Ground Floor: Hardware and Kernel as Sealed Cells - -The registers, ALU operations, pipeline stages, cache hierarchy, kernel syscall interface, and MMU page tables are all cells. But they are cells whose interiors have been sealed — you can see their boundaries (the ISA, the ABI, the syscall convention) but you cannot deform their interiors. They are opaque cells, and the opacity itself is a modal property classified by the accessibility topology. - -This is not merely an analogy. The ISA boundary is a sheaf condition: it promises that two different CPUs implementing the same ISA produce the same observable behavior for the same instructions. When this fails — Spectre, Meltdown, Rowhammer — the sheaf condition is violated. A microarchitectural side channel is literally information leaking through a topology that should have hidden it. The cell formalism makes this structure explicit and formal, enabling security analysis as a natural consequence of the mathematical framework rather than an afterthought. - ---- - -## 2. Design Constraints - -1. **Zero external HoTT dependencies.** No Ground Zero, no Mathlib, no upstream that can archive or rot. Every line from the interval algebra to the shader pipeline is owned by this project. - -2. **Lean 4 kernel compatibility.** Lean 4's kernel has `propext`, `Quot.sound`, and proof irrelevance in `Prop`, which contradicts native univalence. The cubical calculus is therefore **deeply embedded** as data — an `inductive` type family interpreted by a verified evaluator. Lean's equality is never asked to be cubical. - -3. **Self-maintainability.** The system must be buildable, testable, and modifiable by a single developer or small team with no dependency on external package ecosystems beyond Lean's `Init` and the Rust standard library. - -4. **Practical GPU target.** The cubical formalism must compile, through a verified pipeline, to executable GPU shader code. The proof layer and the execution layer are separated by a narrow FFI boundary. - ---- - -## 3. Architecture Overview — Topolei as a Lean 4 Extension - -**Core framing:** Topolei is a Lean 4 project that **extends Lean 4 with cubical-transport HoTT** via a Rust FFI module. Everything — the cubical evaluator, the cell layer, the shader pipeline, the GPU runtime — is *first* formalized in Lean as axiom sets and data structures. The Rust component then discharges those axioms at runtime via `@[extern]` / `@[implemented_by]`, giving the Lean kernel both (a) kernel-speed reduction for cubical terms and (b) access to effects Lean cannot perform natively (GPU, window, OS). - -``` -┌─────────────────────────────────────────────────────────────┐ -│ Pure Lean 4 │ -│ │ -│ ┌──────────────┐ ┌──────────────┐ ┌─────────────────┐ │ -│ │ Cubical │ │ Cell │ │ Shader IR │ │ -│ │ Core │──▶│ Graph │──▶│ Compiler │ │ -│ │ (+axioms) │ │ & Reactive │ │ (verified) │ │ -│ └──────────────┘ └──────────────┘ └────────┬────────┘ │ -│ │ │ │ -│ │ ── every axiom is a proof obligation ─┤ │ -│ ▼ ▼ │ -├────────────────────────────────────────────────────────────┤ -│ FFI Boundary (C ABI) │ -├────────────────────────────────────────────────────────────┤ -│ Rust (FFI backend) │ -│ │ -│ ┌──────────────┐ ┌──────────────┐ ┌─────────────────┐│ -│ │ Cubical │ │ GPU │ │ Shader ││ -│ │ Evaluator │ │ Runtime │◀──│ Emitter ││ -│ │ (axiom │ │ (wgpu / │ │ (SPIR-V / ││ -│ │ discharge) │ │ Vulkan) │ │ WGSL bytes) ││ -│ └──────────────┘ └──────────────┘ └─────────────────┘│ -└────────────────────────────────────────────────────────────┘ -``` - -**Two distinct roles of the Rust layer:** - -1. **Cubical evaluator backend** — Rust implements `eval`, `vApp`, `vPApp`, `vTransp`, `vHCompValue`, `vCompAtTerm`, `vCompNAtTerm`, `readback`, `readbackNeu`, and the face-disjoint reduction rules for transport, composition, and glue. Each Rust function is linked via `@[extern]` + `@[implemented_by]` to the matching Lean axiom in `Cubical/Eval.lean`, `Cubical/Readback.lean`, `Cubical/Glue.lean`, and the (residual) `TransportLaws.lean` / `CompLaws.lean` / `Soundness.lean`. Effect: Lean's kernel *reduces* cubical terms, not just *reasons* about them. (`CTerm.step` is defined as `readback 0 ∘ eval .nil` via the Phase 1 Week 7 step↔eval bridge — see `STATUS.md`; Rust therefore does not implement step separately.) - -2. **Effectful runtime** — GPU context, shader upload, uniforms, render, window/input, OS primitives. Effects Lean fundamentally cannot perform; their interface specs live in `GPU/Spec.lean` + the Phase 5 Runtime modules as IO axioms. - -**Invariant:** Proof obligations remain in Lean. Performance and effect obligations go to Rust. The FFI boundary carries opaque types whose invariants are stated and verified in Lean but whose implementations are in Rust. - -**Process discipline:** Every phase formalizes its external-to-Lean needs as Lean axioms *before* the Rust side is written. Phase 1 (Cubical Core) exemplifies this: the eval-level equations (`eval_*`, `vApp_*`, `vPApp_*`, `vTransp_*`, …), the readback equations (`readback_*`, `readbackNeu_*`), the face-disjoint glueIn/unglue/Glue-transport axioms, and the three IEEE Float axioms are all stated in Lean with zero Rust code present. The Rust module, when built, turns this axiom set into a satisfied contract. Five step-level axioms (T1, T2, C1, C2, `step_papp_plam`) are now Lean theorems via the Week 7 bridge and are not separate Rust obligations. Four remain on Rust's plate: T3 and C4 (subject reduction — need typing-preservation machinery), T5 (face congruence — needs face normalisation), and the general case of T4 (plam-shape preservation — needs richer `vPathTransp` readback). See STATUS.md § Week 7 for the per-axiom table. - ---- - -## 4. Package Structure - -``` -cells/ -├── lakefile.lean -├── lean-toolchain -├── Cells.lean -- root import -│ -├── Cells/ -│ ├── Cubical/ -- Phase 1: core calculus -│ │ ├── Interval.lean -- de Morgan algebra 𝕀 -│ │ ├── Face.lean -- face formulas φ -│ │ ├── Syntax.lean -- CType, CTerm mutual inductive -│ │ ├── Subst.lean -- substitution on terms and dims -│ │ ├── Value.lean -- normal forms / values -│ │ ├── Eval.lean -- evaluator / normalizer -│ │ ├── Transport.lean -- transp for each type former -│ │ ├── Comp.lean -- hcomp / composition -│ │ ├── Glue.lean -- Glue types, univalence -│ │ ├── Equiv.lean -- equivalences, fibers -│ │ └── Soundness.lean -- metatheorems -│ │ -│ ├── Cell/ -- Phase 2: cells-as-transports -│ │ ├── Basic.lean -- Cell structure -│ │ ├── Compose.lean -- sequential and parallel comp -│ │ ├── Higher.lean -- 2-cells, n-cells -│ │ ├── Fiber.lean -- fiber selection, sections -│ │ ├── Projection.lean -- projections to lower dims -│ │ └── Graph.lean -- cell networks / workspaces -│ │ -│ ├── Reactive/ -- Phase 3: incremental propagation -│ │ ├── Depend.lean -- dependency tracking -│ │ ├── Incremental.lean -- incremental re-typechecking -│ │ └── Propagate.lean -- change propagation through graph -│ │ -│ ├── Color/ -- Phase 3b: color as a cell -│ │ ├── Space.lean -- color space types -│ │ ├── Transport.lean -- gamut mapping as transport -│ │ └── Blend.lean -- compositing algebra -│ │ -│ ├── Shader/ -- Phase 4: verified compilation -│ │ ├── IR.lean -- shader intermediate representation -│ │ ├── Compile.lean -- CTerm → ShaderIR (verified) -│ │ ├── Optimize.lean -- algebraic simplification -│ │ ├── Emit.lean -- ShaderIR → GLSL string (Lean side) -│ │ └── Numeric.lean -- numerical method cells -│ │ -│ ├── Runtime/ -- Phase 5: GPU + interaction -│ │ ├── GPU.lean -- opaque types + FFI declarations -│ │ ├── Input.lean -- input event types -│ │ ├── Window.lean -- window management -│ │ └── Loop.lean -- render-input-deform cycle -│ │ -│ ├── Boundary/ -- Phase 5b: accessibility + security -│ │ ├── Access.lean -- AccessLevel, ModalCell, CellEvidence -│ │ ├── Guard.lean -- GuardCell, BoundaryWire, SecurityPolicy -│ │ ├── ProCell.lean -- ProCell, CellSpec, SpecSource -│ │ ├── Classifier.lean -- boundary classifier as a cell -│ │ ├── Hardware.lean -- RegisterCell, ALU ops, sealed tower -│ │ ├── Kernel.lean -- KernelCell, PageTableCell, syscalls -│ │ ├── Security.lean -- vulnerability types, security theorems -│ │ └── Expansion.lean -- ToolIntegration, Grothendieck construction -│ │ -│ └── Meta/ -- Phase 6: self-hosting -│ ├── DSL.lean -- syntax extensions for cell notation -│ ├── Tactic.lean -- custom tactics (transport, compose) -│ └── Widget.lean -- editor-as-cell-network -│ -├── native/ -- Rust crate (linked via C ABI FFI) -│ ├── Cargo.toml -│ ├── include/ -│ │ ├── cells/gpu.h -│ │ ├── cells/emit.h -│ │ └── cells/numeric.h -│ ├── src/ -│ │ ├── gpu/ -│ │ │ ├── context.cpp -- GL/Vulkan context lifecycle -│ │ │ ├── shader.cpp -- shader compilation + linking -│ │ │ ├── framebuffer.cpp -- FBO management -│ │ │ └── input.cpp -- GLFW input polling -│ │ ├── emit/ -│ │ │ ├── glsl.cpp -- ShaderIR → GLSL 4.50 -│ │ │ ├── wgsl.cpp -- ShaderIR → WGSL (WebGPU) -│ │ │ └── spirv.cpp -- ShaderIR → SPIR-V (Vulkan) -│ │ └── numeric/ -│ │ ├── blas.cpp -- OpenBLAS wrapper -│ │ ├── mesh.cpp -- mesh generation / refinement -│ │ └── solver.cpp -- PDE discretization kernels -│ └── deps/ -- vendored headers (GLFW, glad, etc.) -│ -├── tests/ -│ ├── Cubical/ -- unit tests for evaluator -│ ├── Cell/ -- cell composition tests -│ ├── Shader/ -- compilation round-trip tests -│ ├── Boundary/ -- access level, guard, security tests -│ └── Integration/ -- end-to-end render tests -│ -└── examples/ - ├── HelloCell.lean -- minimal: one cell, one shader, one window - ├── Gradient.lean -- color gradient as transport - ├── NoiseField.lean -- procedural noise as cell network - └── Interactive.lean -- mouse-driven deformation -``` - ---- - -## 5. Cubical Core — Detailed Specification - -### 5.1 The Interval (de Morgan Algebra) - -The interval `𝕀` is a bounded distributive lattice with an involution. It is represented as a free algebra on named dimension variables. - -```lean --- Cells/Cubical/Interval.lean - -/-- Dimension expressions: the free de Morgan algebra - on dimension variables. -/ -inductive Dim : Type where - | i0 : Dim -- the endpoint 0 - | i1 : Dim -- the endpoint 1 - | var : Nat → Dim -- dimension variable (de Bruijn level) - | meet : Dim → Dim → Dim -- min(r, s) = r ∧ s - | join : Dim → Dim → Dim -- max(r, s) = r ∨ s - | inv : Dim → Dim -- 1 - r - deriving Repr, BEq, Hashable - -namespace Dim - -/-- Substitute dimension variable `n` with value `val`. -/ -def subst (n : Nat) (val : Dim) : Dim → Dim - | .i0 => .i0 - | .i1 => .i1 - | .var m => if m == n then val else .var m - | .meet a b => .meet (a.subst n val) (b.subst n val) - | .join a b => .join (a.subst n val) (b.subst n val) - | .inv a => .inv (a.subst n val) - -/-- Evaluate to a Boolean when all variables are instantiated. -/ -def eval : Dim → Option Bool - | .i0 => some false - | .i1 => some true - | .var _ => none - | .meet a b => do pure ((← a.eval) && (← b.eval)) - | .join a b => do pure ((← a.eval) || (← b.eval)) - | .inv a => do pure (!(← a.eval)) - -/-- Normalize: apply de Morgan laws, double negation, absorption. -/ -def normalize : Dim → Dim - | .inv (.inv a) => normalize a - | .inv .i0 => .i1 - | .inv .i1 => .i0 - | .inv (.meet a b) => .join (.inv (normalize a)) (.inv (normalize b)) - | .inv (.join a b) => .meet (.inv (normalize a)) (.inv (normalize b)) - | .meet a .i0 => .i0 - | .meet .i0 a => .i0 - | .meet a .i1 => normalize a - | .meet .i1 a => normalize a - | .join a .i0 => normalize a - | .join .i0 a => normalize a - | .join a .i1 => .i1 - | .join .i1 a => .i1 - | .meet a b => .meet (normalize a) (normalize b) - | .join a b => .join (normalize a) (normalize b) - | .inv a => .inv (normalize a) - | d => d - -end Dim -``` - -**Laws that must hold (proved as theorems):** - -| Law | Statement | -|-----|-----------| -| Idempotence | `meet r r = r`, `join r r = r` | -| Commutativity | `meet r s = meet s r` | -| Associativity | `meet r (meet s t) = meet (meet r s) t` | -| Absorption | `meet r (join r s) = r` | -| Distributivity | `meet r (join s t) = join (meet r s) (meet r t)` | -| De Morgan | `inv (meet r s) = join (inv r) (inv s)` | -| Involution | `inv (inv r) = r` | -| Boundary | `meet r (inv r)` is NOT required to be `i0` (CCHM style) | - -### 5.2 Face Formulas - -Face formulas are propositions about dimension variables. They define the "boundary" of a cube — the faces on which partial elements are defined. - -```lean --- Cells/Cubical/Face.lean - -/-- A face formula: a proposition constraining dimensions. -/ -inductive Face : Type where - | eq0 : Nat → Face -- variable i = 0 - | eq1 : Nat → Face -- variable i = 1 - | and : Face → Face → Face -- φ ∧ ψ - | or : Face → Face → Face -- φ ∨ ψ - | top : Face -- ⊤ (always satisfied) - | bot : Face -- ⊥ (never satisfied) - deriving Repr, BEq - -namespace Face - -/-- Evaluate a face formula given a dimension assignment. -/ -def eval (assign : Nat → Option Bool) : Face → Option Bool - | .eq0 n => do let v ← assign n; pure (v == false) - | .eq1 n => do let v ← assign n; pure (v == true) - | .and φ ψ => do pure ((← φ.eval assign) && (← ψ.eval assign)) - | .or φ ψ => do pure ((← φ.eval assign) || (← ψ.eval assign)) - | .top => some true - | .bot => some false - -/-- Check if one face implies another (conservative). -/ -def implies (φ ψ : Face) : Bool := - sorry -- decision procedure for propositional lattice - -/-- The face generated by a dimension being an endpoint. -/ -def ofDim (r : Dim) : Face := - match r.normalize with - | .i0 => .top - | .i1 => .top - | .var n => .or (.eq0 n) (.eq1 n) -- tautology: i=0 ∨ i=1 - | _ => .bot -- not a simple constraint - -end Face - -/-- A system of partial elements: values defined on faces. - On overlapping faces, values must agree (the coherence condition). -/ -structure System (α : Type) where - clauses : List (Face × α) -``` - -### 5.3 Syntax — Types and Terms - -The core type theory, represented as mutually inductive data types. - -```lean --- Cells/Cubical/Syntax.lean - -/-- Universe levels. -/ -abbrev Level := Nat - -mutual - -/-- Types in the cubical calculus. -/ -inductive CType : Type where - | univ : Level → CType - | el : CTerm → CType -- El(a) where a : Type_n - | pi : CType → CType → CType -- Π(x : A). B - | sigma : CType → CType → CType -- Σ(x : A). B - | pathTy : CType → CTerm → CTerm → CType -- Path_A(a, b) - | glue : Face → CType → CTerm → CType → CType - -- Glue [φ ↦ (T, e)] A - -- On face φ: type is T with equiv e : T ≃ A - -- Off face φ: type is A - deriving Repr - -/-- Terms in the cubical calculus. -/ -inductive CTerm : Type where - -- Standard λ-calculus - | var : Nat → CTerm -- de Bruijn index - | lam : CTerm → CTerm -- λx. t - | app : CTerm → CTerm → CTerm -- f a - | pair : CTerm → CTerm → CTerm -- (a, b) - | fst : CTerm → CTerm -- π₁ - | snd : CTerm → CTerm -- π₂ - - -- Type annotation - | ann : CTerm → CType → CTerm -- (t : A) - - -- Dimension abstraction and application - | dimLam : CTerm → CTerm -- λ(i : 𝕀). t - | dimApp : CTerm → Dim → CTerm -- t @ r - - -- Cubical operations - | transp : CTerm → Face → CTerm → CTerm - -- transp (i. A(i)) φ u₀ : A(1) - -- Precondition: when φ = ⊤, A is constant in i - -- and transp computes to identity - -- u₀ : A(0) - | hcomp : CType → Face → CTerm → CTerm → CTerm - -- hcomp A [φ ↦ u] u₀ : A - -- u : (i : 𝕀) → Partial φ A (the tube) - -- u₀ : A (the base) - -- u₀ agrees with u(i0) on φ - - -- Glue introduction and elimination - | glueIn : Face → CTerm → CTerm → CTerm - -- glue [φ ↦ t] a : Glue [φ ↦ (T, e)] A - | unglue : Face → CTerm → CTerm - -- unglue φ g : A - - -- Literals for base types (shader-relevant) - | floatLit : Float → CTerm - | vecLit : Array Float → CTerm -- vec2, vec3, vec4 - | boolLit : Bool → CTerm - | intLit : Int → CTerm - deriving Repr - -end -``` - -### 5.4 Values and Evaluation - -The evaluator reduces `CTerm` to `Val` (weak head normal form). This is where the computational content of cubical type theory lives. - -```lean --- Cells/Cubical/Value.lean - -/-- An environment: a list of values for bound variables. -/ -abbrev Env := Array Val - -mutual - -/-- Values: weak-head normal forms. -/ -inductive Val : Type where - | vlam : Env → CTerm → Val -- closure - | vdimLam : Env → CTerm → Val -- dim closure - | vpair : Val → Val → Val - | vpi : Val → Env → CTerm → Val -- Π type value - | vsigma : Val → Env → CTerm → Val -- Σ type value - | vpathTy : Val → Val → Val → Val -- Path type value - | vglue : Face → Val → Val → Val → Val -- Glue type value - | vuniv : Level → Val -- Type_n - | vneutral : Neutral → Val -- stuck term - | vfloat : Float → Val - | vvec : Array Float → Val - | vbool : Bool → Val - | vint : Int → Val - deriving Repr - -/-- Neutral terms: stuck computations. -/ -inductive Neutral : Type where - | nvar : Nat → Neutral - | napp : Neutral → Val → Neutral - | nfst : Neutral → Neutral - | nsnd : Neutral → Neutral - | ndimApp : Neutral → Dim → Neutral - | ntransp : Val → Face → Neutral → Neutral - | nhcomp : Val → Face → Val → Neutral → Neutral - deriving Repr - -end -``` - -```lean --- Cells/Cubical/Eval.lean - -/-- Evaluate a term in an environment. -/ -partial def eval (env : Env) : CTerm → Val - | .var n => env[n]! - | .lam body => .vlam env body - | .app f a => vApp (eval env f) (eval env a) - | .pair a b => .vpair (eval env a) (eval env b) - | .fst t => vFst (eval env t) - | .snd t => vSnd (eval env t) - | .dimLam body => .vdimLam env body - | .dimApp t r => vDimApp (eval env t) r - | .transp line φ base => - vTransp (eval env line) φ (eval env base) - | .hcomp A φ tube base => - vHComp (evalType env A) φ (eval env tube) (eval env base) - | .glueIn φ t a => vGlueIn φ (eval env t) (eval env a) - | .unglue φ g => vUnglue φ (eval env g) - | .floatLit f => .vfloat f - | .vecLit v => .vvec v - | .boolLit b => .vbool b - | .intLit i => .vint i - | .ann t _ => eval env t - -/-- Apply a value to an argument. -/ -partial def vApp : Val → Val → Val - | .vlam env body, arg => eval (env.push arg) body - | .vneutral n, arg => .vneutral (.napp n arg) - | _, _ => panic! "vApp: not a function" - -/-- Project first component. -/ -partial def vFst : Val → Val - | .vpair a _ => a - | .vneutral n => .vneutral (.nfst n) - | _ => panic! "vFst: not a pair" - -/-- Project second component. -/ -partial def vSnd : Val → Val - | .vpair _ b => b - | .vneutral n => .vneutral (.nsnd n) - | _ => panic! "vSnd: not a pair" -``` - -### 5.5 Transport - -Transport is the central computational operation. For each type former, transport has specific behavior. - -```lean --- Cells/Cubical/Transport.lean - -/-- Transport along a line of types. - - transp (i. A(i)) φ u₀ : A(1) - - When φ = ⊤, A is constant and transp is the identity. - Otherwise, case-split on the head constructor of A. -/ -partial def vTransp (lineA : Val) (φ : Face) (u₀ : Val) : Val := - -- If φ is satisfied, A is constant → identity - if φ == .top then u₀ - else match lineA with - -- - -- Π-type: transp contravariantly in domain, covariantly in codomain - -- transp (i. Π(x : A(i)). B(i,x)) φ f - -- = λ(x₁ : A(1)). transp (i. B(i, fill⁻¹(i,x₁))) φ (f (transp⁻¹ x₁)) - -- - | .vpi domLine codLine => sorry -- implemented per CCHM - - -- - -- Σ-type: transport componentwise with correction term - -- transp (i. Σ(x : A(i)). B(i,x)) φ (a₀, b₀) - -- = (a₁, transp (i. B(i, fill(i,a₀))) φ b₀) - -- where a₁ = transp (i. A(i)) φ a₀ - -- - | .vsigma fstLine sndLine => sorry -- implemented per CCHM - - -- - -- Path type: adjust endpoints - -- transp (i. Path (A(i)) (a(i)) (b(i))) φ p - -- = λ(j : 𝕀). comp (i. A(i)) [j=0 ↦ a(i), j=1 ↦ b(i)] (p @ j) - -- - | .vpathTy lineA' lineA0 lineA1 => sorry -- implemented per CCHM - - -- - -- Glue type: the key case giving univalence - -- Uses the equivalence to transport on the base, - -- then adjusts the fibers - -- - | .vglue ψ lineT lineE lineBase => sorry -- implemented per CCHM - - -- - -- Universe: transp (i. Type) φ A = A - -- (transport in the universe is the identity because - -- univalence is given by Glue, not by transport in Type) - -- - | .vuniv _ => u₀ - - -- - -- Neutral: stuck, produce neutral transport - -- - | .vneutral n => .vneutral (.ntransp lineA φ (.nvar 0 /- placeholder -/)) - - | _ => panic! "vTransp: unexpected type" -``` - -### 5.6 Composition (hcomp) - -Homogeneous composition fills open boxes at a fixed type. - -```lean --- Cells/Cubical/Comp.lean - -/-- Homogeneous composition. - - hcomp A [φ ↦ u] u₀ : A - - Given: - A : Type (fixed) - φ : Face (the constrained face) - u : (i : 𝕀) → Partial φ A (the tube: walls of the box) - u₀ : A (the base, agreeing with u(0) on φ) - Produces: - the lid of the box at i = 1 --/ -partial def vHComp (tyA : Val) (φ : Face) (tube : Val) (base : Val) : Val := - if φ == .top then - -- Tube covers everything → just evaluate tube at i=1 - vDimApp tube .i1 - else match tyA with - | .vpi _ _ => sorry -- per CCHM: pointwise hcomp in codomain - | .vsigma _ _ => sorry -- per CCHM: componentwise with correction - | .vpathTy _ _ _ => sorry -- per CCHM: becomes 2-dimensional comp - | .vglue _ _ _ _ => sorry -- per CCHM: the hard case - | .vneutral n => .vneutral (.nhcomp tyA φ tube (.nvar 0)) - | _ => panic! "vHComp: unexpected type" - -/-- Heterogeneous composition (derived from transp + hcomp). - - comp (i. A(i)) [φ ↦ u] u₀ : A(1) - - = hcomp A(1) [φ ↦ transp (j. A(j)) u(i)] (transp (i. A(i)) u₀) --/ -def vComp (lineA : Val) (φ : Face) (tube : Val) (base : Val) : Val := - sorry -- derived operation -``` - -### 5.7 Glue Types and Univalence - -Glue types are the mechanism by which univalence becomes a theorem rather than an axiom. - -```lean --- Cells/Cubical/Glue.lean - -/-- Glue type formation. - - Glue [φ ↦ (T, e)] A - - On face φ: the type is T, with e : T ≃ A witnessing equivalence. - Off face φ: the type is A. - The boundary is "glued" by the equivalence. --/ - -/-- glue introduction: given t : T on face φ and a : A off face φ, - with e(t) = a on the overlap, produce a term of Glue type. -/ -partial def vGlueIn (φ : Face) (t : Val) (a : Val) : Val := - if φ == .top then t -- on the face, it's just t - else if φ == .bot then a -- off the face, it's just a - else sorry -- general case - -/-- unglue: extract the A-component from a Glue term. -/ -partial def vUnglue (φ : Face) (g : Val) : Val := - if φ == .top then - sorry -- apply the equivalence: e(g) - else g -- off the face, g is already in A - -/-- The univalence map: given e : A ≃ B, construct Path Type A B. - - ua(e) := λ(i : 𝕀). Glue [i=0 ↦ (A, e), i=1 ↦ (B, idEquiv)] B - - At i=0: Glue [⊤ ↦ (A, e)] B = A (via e) - At i=1: Glue [⊤ ↦ (B, id)] B = B (trivially) --/ -def ua (equiv : CTerm) (tyA tyB : CType) : CTerm := - .dimLam sorry -- the Glue line described above - -/-- Theorem: ua computes correctly at endpoints. -/ -theorem ua_zero (e : CTerm) (A B : CType) : - evalType [] (substDim (ua e A B) .i0) = evalType [] A := - sorry - -theorem ua_one (e : CTerm) (A B : CType) : - evalType [] (substDim (ua e A B) .i1) = evalType [] B := - sorry - -/-- Theorem: transport along ua(e) computes as e. - This is the computational content of univalence. -/ -theorem transp_ua (e : CTerm) (a : CTerm) : - eval [] (.transp (ua e sorry sorry) .bot a) = - eval [] (.app e a) := - sorry -``` - -### 5.8 Equivalences and Fibers - -```lean --- Cells/Cubical/Equiv.lean - -/-- The fiber of f over b: Σ(a : A), f(a) =_B b -/ -def fiberTy (A B : CType) (f : CTerm) (b : CTerm) : CType := - .sigma A (.pathTy (weakenType B) (.app (weakenTerm f) (.var 0)) (weakenTerm b)) - -/-- An equivalence e : A ≃ B consists of: - - a map f : A → B - - for every b : B, a contractible fiber -/ -structure EquivData where - f : CTerm -- the forward map - fInv : CTerm -- the inverse (for computation) - sec : CTerm -- section: f(fInv(b)) = b - ret : CTerm -- retraction: fInv(f(a)) = a - coh : CTerm -- coherence (half-adjoint) - -/-- The identity equivalence on a type. -/ -def idEquiv (A : CType) : EquivData where - f := .lam (.var 0) -- id - fInv := .lam (.var 0) -- id - sec := .lam (.dimLam (.var 1)) -- refl - ret := .lam (.dimLam (.var 1)) -- refl - coh := sorry -- trivial coherence -``` - ---- - -## 6. Cells as Transports - -### 6.1 The Cell Structure - -```lean --- Cells/Cell/Basic.lean - -/-- A cell is a line of types: a transport between its boundary faces. - This is the fundamental object of the system. -/ -structure Cell where - /-- The line of types: (i : 𝕀) ⊢ A(i) type. - Represented as a dim-lambda returning a type term. -/ - line : CTerm - - /-- The input boundary: A(0). -/ - input : CType - - /-- The output boundary: A(1). -/ - output : CType - - /-- Proof that input matches the line evaluated at 0. -/ - input_spec : evalType #[] (dimSubst line .i0) = evalType #[] (.ann (.var 0) input) - - /-- Proof that output matches the line evaluated at 1. -/ - output_spec : evalType #[] (dimSubst line .i1) = evalType #[] (.ann (.var 0) output) - -/-- The transport function induced by a cell. -/ -def Cell.asTransport (c : Cell) : CTerm := - .lam (.transp c.line .bot (.var 0)) - -/-- The identity cell on a type: the constant line. -/ -def Cell.id (A : CType) : Cell where - line := .dimLam (.ann (.var 0) A) -- A for all i - input := A - output := A - input_spec := rfl - output_spec := rfl - -/-- A cell from an equivalence (via univalence). -/ -def Cell.ofEquiv (e : EquivData) (A B : CType) : Cell where - line := ua e.f A B - input := A - output := B - input_spec := ua_zero e.f A B - output_spec := ua_one e.f A B -``` - -### 6.2 Cell Composition - -```lean --- Cells/Cell/Compose.lean - -/-- Sequential composition: concatenate lines. - c₁ : A ↔ B and c₂ : B ↔ C gives c₁ ∘ c₂ : A ↔ C -/ -def Cell.seq (c₁ c₂ : Cell) (h : c₁.output = c₂.input) : Cell where - line := concatDimLines c₁.line c₂.line h - input := c₁.input - output := c₂.output - input_spec := sorry - output_spec := sorry - -/-- Parallel composition: product of lines. - c₁ : A₁ ↔ B₁ and c₂ : A₂ ↔ B₂ gives c₁ × c₂ : A₁×A₂ ↔ B₁×B₂ -/ -def Cell.par (c₁ c₂ : Cell) : Cell where - line := .dimLam (.ann (.var 0) (.sigma - (dimSubstType c₁.line (.var 0)) - (dimSubstType c₂.line (.var 0)))) - input := .sigma c₁.input c₂.input - output := .sigma c₁.output c₂.output - input_spec := sorry - output_spec := sorry - -/-- Inverse cell: reverse the line. - c : A ↔ B gives c⁻¹ : B ↔ A -/ -def Cell.inv (c : Cell) : Cell where - line := .dimLam (dimSubst c.line (.inv (.var 0))) - input := c.output - output := c.input - input_spec := sorry - output_spec := sorry - -/-- Monad structure on cells. - return = Cell.id - bind = Cell.seq - Laws hold by groupoid laws on paths. -/ - -theorem cell_left_unit (c : Cell) : - Cell.seq (Cell.id c.input) c sorry ≈ c := - sorry -- follows from transp along constant line = id - -theorem cell_right_unit (c : Cell) : - Cell.seq c (Cell.id c.output) sorry ≈ c := - sorry - -theorem cell_assoc (c₁ c₂ c₃ : Cell) (h₁₂ h₂₃ : _) : - Cell.seq (Cell.seq c₁ c₂ h₁₂) c₃ h₂₃ ≈ - Cell.seq c₁ (Cell.seq c₂ c₃ sorry) sorry := - sorry -- follows from path concatenation associativity -``` - -### 6.3 Higher Cells - -```lean --- Cells/Cell/Higher.lean - -/-- A 2-cell is a homotopy between two 1-cells. - It is a square: a term (i, j : 𝕀) ⊢ A(i,j) type -/ -structure Cell2 where - /-- The two 1-cells being related. -/ - top : Cell -- the cell at j = 1 - bottom : Cell -- the cell at j = 0 - /-- Side cells connecting endpoints. -/ - left : Cell -- input side: top.input ↔ bottom.input - right : Cell -- output side: top.output ↔ bottom.output - /-- The filling: a square of types. -/ - square : CTerm -- (i, j : 𝕀) ⊢ A(i,j) type - /-- Boundary coherence. -/ - face_top : dimSubst (dimSubst square (.var 1)) .i1 = top.line - face_bottom : dimSubst (dimSubst square (.var 1)) .i0 = bottom.line - face_left : dimSubst (dimSubst square (.var 0)) .i0 = left.line - face_right : dimSubst (dimSubst square (.var 0)) .i1 = right.line - -/-- An n-cell is an n-cube of transports. -/ -inductive CellN : Nat → Type where - | point : CType → CellN 0 - | arrow : Cell → CellN 1 - | cube : {n : Nat} → - (boundary : Fin (2 * (n + 1)) → CellN n) → - (filler : CTerm) → - CellN (n + 1) - -/-- The dimension hierarchy: - 0-cell = a value (a type, a constant, a color) - 1-cell = a transport (a function, a shader pass, a wire) - 2-cell = a homotopy (a continuous deformation, a parameter sweep) - 3-cell = a homotopy of homotopies (an animation of an animation) - n-cell = full generality -/ -``` - -### 6.4 Fibers and Projections - -```lean --- Cells/Cell/Fiber.lean - -/-- A projection from an n-cell to a 2D screen manifold. -/ -structure Projection (n : Nat) where - /-- The cell being projected. -/ - source : CellN n - /-- The projection map: from the n-cell to ℝ². -/ - proj : CTerm -- source → (Float × Float) - -/-- The fiber of a projection at a screen point: - the set of computational elements that map to that pixel. -/ -def Projection.fiberAt (π : Projection n) (screenPos : Float × Float) : CType := - fiberTy sorry sorry π.proj (.pair (.floatLit screenPos.1) (.floatLit screenPos.2)) - -/-- Fiber selection: choosing an element of the fiber. - Mouse click = evaluating a section of the projection sheaf. -/ -structure FiberSelection (n : Nat) where - proj : Projection n - pos : Float × Float - element : CTerm -- an element of fiberAt proj pos - -/-- Continuous fiber deformation: dragging = transporting a section - along a path in screen space. -/ -structure Drag (n : Nat) where - proj : Projection n - path : CTerm -- 𝕀 → (Float × Float), the mouse path - start : FiberSelection n - -- The section being transported: - -- for each point along the drag path, - -- a fiber element connected to the previous one by transport - section_ : CTerm -- (i : 𝕀) → fiber(path(i)) -``` - ---- - -## 7. Cell Graph and Reactivity - -### 7.1 The Cell Network - -```lean --- Cells/Cell/Graph.lean - -/-- Identifier for a cell in a graph. -/ -abbrev CellId := Nat - -/-- A port is one face of a cell. -/ -structure Port where - cellId : CellId - face : Fin 2 -- 0 = input, 1 = output - -/-- A wire connects two ports with a proof of type compatibility. -/ -structure Wire where - source : Port - target : Port - -- Type at source face must equal type at target face - compat : CTerm -- proof term (in the cubical calculus) - -/-- A cell graph: the workspace. -/ -structure CellGraph where - cells : Array Cell - wires : Array Wire - -- No dangling wires - wires_valid : ∀ w ∈ wires, - w.source.cellId < cells.size ∧ - w.target.cellId < cells.size - -/-- Get the type at a port. -/ -def CellGraph.portType (g : CellGraph) (p : Port) : CType := - let cell := g.cells[p.cellId]! - if p.face == 0 then cell.input else cell.output - -/-- Flatten a cell graph to a single composite cell. - Topological sort, then compose sequentially. - Parallel branches become Σ-type transports. -/ -def CellGraph.flatten (g : CellGraph) : Cell := - sorry -- topological sort + fold with Cell.seq and Cell.par - -/-- Find all cells downstream of a changed cell. -/ -def CellGraph.downstream (g : CellGraph) (changed : CellId) : Array CellId := - sorry -- BFS/DFS on the wire graph from `changed` -``` - -### 7.2 Incremental Re-typechecking - -```lean --- Cells/Reactive/Incremental.lean - -/-- When a cell is modified, determine which wires need rechecking. -/ -def recheckNeeded (g : CellGraph) (modified : CellId) : Array Nat := - g.wires.zipWithIndex.filterMap fun ⟨w, i⟩ => - if w.source.cellId == modified || w.target.cellId == modified - then some i - else none - -/-- Result of incremental typechecking. -/ -inductive RecheckResult where - | ok : CellGraph → RecheckResult -- all wires still valid - | broken : Array Nat → RecheckResult -- these wire indices failed - -/-- Incrementally recheck after modification. -/ -def incrementalRecheck (g : CellGraph) (modified : CellId) - (newCell : Cell) : RecheckResult := - let g' := { g with cells := g.cells.set! modified newCell } - let toCheck := recheckNeeded g' modified - let broken := toCheck.filter fun i => - let w := g'.wires[i]! - g'.portType w.source != g'.portType w.target - if broken.isEmpty then .ok g' else .broken broken -``` - ---- - -## 8. Color as a Cell - -### 8.1 Color Spaces as Types - -```lean --- Cells/Color/Space.lean - -/-- A color space is a type in the cell calculus. -/ -inductive ColorSpace : Type where - | sRGB : ColorSpace -- standard RGB (gamma-encoded) - | linearRGB : ColorSpace -- linear light RGB - | oklch : ColorSpace -- perceptual: lightness, chroma, hue - | oklab : ColorSpace -- perceptual: lightness, a*, b* - | hsl : ColorSpace -- hue, saturation, lightness - | xyz : ColorSpace -- CIE XYZ - deriving Repr, BEq - -/-- Each color space has a representation as a triple of floats. -/ -def ColorSpace.toCType : ColorSpace → CType - | _ => .sigma (.el (.ann (.floatLit 0) sorry)) - (.sigma (.el (.ann (.floatLit 0) sorry)) - (.el (.ann (.floatLit 0) sorry))) - -/-- A color is a cell with a specific color space type. -/ -structure Color where - space : ColorSpace - value : CTerm -- a triple of floats in that space - -/-- A color transform is a cell between color spaces: - the transport IS the color conversion. -/ -def colorConvert (from to : ColorSpace) : Cell := - Cell.ofEquiv (colorConvertEquiv from to) from.toCType to.toCType - -/-- sRGB ↔ linear RGB conversion, as an equivalence. -/ -def srgbLinearEquiv : EquivData where - f := sorry -- gamma decode: c < 0.04045 ? c/12.92 : ((c+0.055)/1.055)^2.4 - fInv := sorry -- gamma encode: c < 0.0031308 ? 12.92*c : 1.055*c^(1/2.4)-0.055 - sec := sorry - ret := sorry - coh := sorry -``` - -### 8.2 Gamut Mapping as Transport - -```lean --- Cells/Color/Transport.lean - -/-- Gamut mapping: projecting an out-of-gamut color into the displayable set. - This is a SECTION of the inclusion Display ↪ Perceptual. - Not an equivalence — it's a retraction, hence lossy. - But it IS a cell: it transports along a line in color space - that moves from the intended color to the nearest displayable color. -/ -def gamutMap (display : ColorSpace) (color : Color) : Cell := - sorry -- line from color.value to nearest in-gamut point -``` - ---- - -## 9. Shader Compilation - -### 9.1 Shader IR - -```lean --- Cells/Shader/IR.lean - -/-- Types available in shader code. -/ -inductive ShaderType : Type where - | float : ShaderType - | vec2 : ShaderType - | vec3 : ShaderType - | vec4 : ShaderType - | mat2 : ShaderType - | mat3 : ShaderType - | mat4 : ShaderType - | int : ShaderType - | bool : ShaderType - | sampler : ShaderType -- sampler2D - deriving Repr, BEq - -/-- Shader expressions (no side effects, no control flow). -/ -inductive ShaderExpr : Type where - | litF : Float → ShaderExpr - | litI : Int → ShaderExpr - | litB : Bool → ShaderExpr - | var : String → ShaderExpr - | binop : String → ShaderExpr → ShaderExpr → ShaderExpr - | unop : String → ShaderExpr → ShaderExpr - | call : String → List ShaderExpr → ShaderExpr - | swizzle : ShaderExpr → String → ShaderExpr - | ternary : ShaderExpr → ShaderExpr → ShaderExpr → ShaderExpr - | vecCon : List ShaderExpr → ShaderExpr -- vec3(x,y,z) - | matCon : List ShaderExpr → ShaderExpr -- mat3(...) - | texSample : ShaderExpr → ShaderExpr → ShaderExpr -- texture(s, uv) - deriving Repr - -/-- Shader statements (sequential, with control flow). -/ -inductive ShaderStmt : Type where - | decl : ShaderType → String → ShaderExpr → ShaderStmt - | assign : String → ShaderExpr → ShaderStmt - | forLoop : String → ShaderExpr → ShaderExpr → List ShaderStmt → ShaderStmt - | ifThen : ShaderExpr → List ShaderStmt → List ShaderStmt → ShaderStmt - | ret : ShaderExpr → ShaderStmt - | discard : ShaderStmt - deriving Repr - -/-- A complete shader program. -/ -structure ShaderProgram where - version : String := "450" - uniforms : List (ShaderType × String) - inputs : List (ShaderType × String) -- varyings in - outputs : List (ShaderType × String) -- varyings out / fragColor - body : List ShaderStmt - deriving Repr -``` - -### 9.2 Verified Compilation - -```lean --- Cells/Shader/Compile.lean - -/-- Judgment: a cell term is shader-compilable. - This restricts to the fragment of the cubical calculus - that maps onto GPU operations. -/ -inductive Compilable : CTerm → Prop where - | float_lit : ∀ f, Compilable (.floatLit f) - | vec_lit : ∀ v, Compilable (.vecLit v) - | var : ∀ n, Compilable (.var n) - | app : Compilable f → Compilable a → Compilable (.app f a) - | lam : Compilable body → Compilable (.lam body) - | pair : Compilable a → Compilable b → Compilable (.pair a b) - | fst : Compilable t → Compilable (.fst t) - | snd : Compilable t → Compilable (.snd t) - -- No transp, hcomp, glue, dimLam — these must be - -- reduced away before compilation - -/-- Compile a compilable cell term to shader IR. - - The key invariant: the compiled shader computes the - same function as the cell's transport, at the level - of floating-point semantics. -/ -def compileCell (t : CTerm) (h : Compilable t) - (ctx : CompileContext) : ShaderProgram := - sorry -- structural recursion on the compilability proof - -/-- Attempt to make a term compilable by reducing away - all cubical operations (transp, hcomp, Glue) to - their concrete computational content. -/ -def reduceToConcrete (t : CTerm) : Option CTerm := - sorry -- run the evaluator, read back, check Compilable - -/-- The full pipeline: - Cell → reduce cubical ops → compile to ShaderIR → emit GLSL -/ -def cellToGLSL (c : Cell) : Option String := do - let reduced ← reduceToConcrete c.asTransport - let h : Compilable reduced := sorry -- check - let program := compileCell reduced h {} - pure (emitGLSL program) -``` - -### 9.3 GLSL Emission (Lean side) - -```lean --- Cells/Shader/Emit.lean - -/-- Emit a shader expression as a GLSL string. -/ -def emitExpr : ShaderExpr → String - | .litF f => toString f - | .litI i => toString i - | .litB true => "true" - | .litB false => "false" - | .var name => name - | .binop op l r => s!"({emitExpr l} {op} {emitExpr r})" - | .unop op e => s!"({op}{emitExpr e})" - | .call fn args => - s!"{fn}({", ".intercalate (args.map emitExpr)})" - | .swizzle e c => s!"{emitExpr e}.{c}" - | .ternary c t f => - s!"({emitExpr c} ? {emitExpr t} : {emitExpr f})" - | .vecCon cs => - s!"vec{cs.length}({", ".intercalate (cs.map emitExpr)})" - | .matCon cs => - s!"mat{cs.length}({", ".intercalate (cs.map emitExpr)})" - | .texSample s uv => - s!"texture({emitExpr s}, {emitExpr uv})" - -/-- Emit a shader type as GLSL. -/ -def emitType : ShaderType → String - | .float => "float" - | .vec2 => "vec2" - | .vec3 => "vec3" - | .vec4 => "vec4" - | .mat2 => "mat2" - | .mat3 => "mat3" - | .mat4 => "mat4" - | .int => "int" - | .bool => "bool" - | .sampler => "sampler2D" - -/-- Emit a complete shader program as GLSL source. -/ -def emitGLSL (prog : ShaderProgram) : String := - let header := s!"#version {prog.version}\n\n" - let uniforms := prog.uniforms.map fun ⟨ty, name⟩ => - s!"uniform {emitType ty} {name};\n" - let inputs := prog.inputs.map fun ⟨ty, name⟩ => - s!"in {emitType ty} {name};\n" - let outputs := prog.outputs.map fun ⟨ty, name⟩ => - s!"out {emitType ty} {name};\n" - let body := emitStmts prog.body 1 - header ++ - String.join uniforms ++ "\n" ++ - String.join inputs ++ - String.join outputs ++ "\n" ++ - "void main() {\n" ++ body ++ "}\n" - -where - emitStmts (stmts : List ShaderStmt) (indent : Nat) : String := - String.join (stmts.map (emitStmt · indent)) - - emitStmt (stmt : ShaderStmt) (indent : Nat) : String := - let pad := String.mk (List.replicate (indent * 4) ' ') - match stmt with - | .decl ty name expr => - s!"{pad}{emitType ty} {name} = {emitExpr expr};\n" - | .assign name expr => - s!"{pad}{name} = {emitExpr expr};\n" - | .forLoop var lo hi body => - s!"{pad}for (int {var} = {emitExpr lo}; {var} < {emitExpr hi}; {var}++) \{\n" ++ - emitStmts body (indent + 1) ++ - s!"{pad}}\n" - | .ifThen cond then_ else_ => - s!"{pad}if ({emitExpr cond}) \{\n" ++ - emitStmts then_ (indent + 1) ++ - s!"{pad}} else \{\n" ++ - emitStmts else_ (indent + 1) ++ - s!"{pad}}\n" - | .ret expr => - s!"{pad}fragColor = {emitExpr expr};\n" - | .discard => - s!"{pad}discard;\n" -``` - ---- - -## 10. Runtime — FFI Boundary - -### 10.1 Opaque GPU Types - -```lean --- Cells/Runtime/GPU.lean - -/-- Opaque pointer to the Rust-backed GPU context. - Invariant: a valid OpenGL/Vulkan context is active. -/ -opaque GPUContextPointer : NonemptyType -def GPUContext : Type := GPUContextPointer.type -instance : Nonempty GPUContext := GPUContextPointer.property - -/-- Opaque handle to a compiled shader program on GPU. -/ -opaque ShaderHandlePointer : NonemptyType -def ShaderHandle : Type := ShaderHandlePointer.type -instance : Nonempty ShaderHandle := ShaderHandlePointer.property - -/-- Create a GPU context with a window. -/ -@[extern "cells_gpu_create"] -opaque GPU.create (width height : UInt32) (title : @& String) : IO GPUContext - -/-- Destroy a GPU context. -/ -@[extern "cells_gpu_destroy"] -opaque GPU.destroy (ctx : @& GPUContext) : IO Unit - -/-- Compile a GLSL source string to a GPU shader program. -/ -@[extern "cells_gpu_compile_shader"] -opaque GPU.compileShader (ctx : @& GPUContext) (glsl : @& String) : IO ShaderHandle - -/-- Set uniform values on a shader program. -/ -@[extern "cells_gpu_set_uniforms"] -opaque GPU.setUniforms (ctx : @& GPUContext) (handle : @& ShaderHandle) - (uniforms : @& FloatArray) : IO Unit - -/-- Render a fullscreen quad with the given shader. -/ -@[extern "cells_gpu_render_quad"] -opaque GPU.renderQuad (ctx : @& GPUContext) (handle : @& ShaderHandle) : IO Unit - -/-- Swap buffers (present the rendered frame). -/ -@[extern "cells_gpu_swap"] -opaque GPU.swap (ctx : @& GPUContext) : IO Unit - -/-- Check if the window should close. -/ -@[extern "cells_gpu_should_close"] -opaque GPU.shouldClose (ctx : @& GPUContext) : IO Bool -``` - -### 10.2 Input Events - -```lean --- Cells/Runtime/Input.lean - -/-- Raw input events from the windowing system. -/ -inductive InputEvent : Type where - | mouseMove : Float → Float → InputEvent -- x, y in [0,1]² - | mouseDown : Float → Float → Nat → InputEvent -- x, y, button - | mouseUp : Float → Float → Nat → InputEvent - | mouseDrag : Float → Float → Float → Float → InputEvent -- x, y, dx, dy - | keyDown : UInt32 → InputEvent - | keyUp : UInt32 → InputEvent - | scroll : Float → Float → InputEvent -- dx, dy - | resize : UInt32 → UInt32 → InputEvent -- width, height - | quit : InputEvent - | none : InputEvent - deriving Repr - -/-- Poll for the next input event. -/ -@[extern "cells_gpu_poll_input"] -opaque GPU.pollInput (ctx : @& GPUContext) : IO InputEvent -``` - -### 10.3 The Interaction Loop - -```lean --- Cells/Runtime/Loop.lean - -/-- The workspace state: a cell graph + current projection + selection. -/ -structure WorkspaceState where - graph : CellGraph - projection : Projection 1 -- current view - selection : Option (FiberSelection 1) - uniforms : FloatArray -- current parameter values - -/-- The main interaction loop. - - This is a 3-cell in disguise: the session is a path - through workspace states, and the rendering at each - moment is the projection of the current state. -/ -partial def mainLoop (ctx : GPUContext) (handle : ShaderHandle) - (state : WorkspaceState) : IO Unit := do - -- Check exit - if ← GPU.shouldClose ctx then return - - -- Poll input - let event ← GPU.pollInput ctx - - -- Process event - let state' ← match event with - | .mouseDown x y _ => - -- Fiber selection: find which cell projects to (x, y) - let fiber := selectFiber state.graph state.projection (x, y) - pure { state with selection := fiber } - - | .mouseDrag x y dx dy => - match state.selection with - | some sel => - -- Transport the selection along the drag direction - let newParams := applyDeformation state.uniforms sel dx dy - GPU.setUniforms ctx handle newParams - pure { state with uniforms := newParams } - | none => pure state - - | .mouseUp _ _ _ => - pure { state with selection := none } - - | .keyDown key => - -- Keyboard: cell manipulation commands - handleKeyCommand state key - - | .resize w h => - -- Recompute projection for new window size - pure state -- TODO - - | _ => pure state - - -- Render - GPU.renderQuad ctx handle - GPU.swap ctx - - -- Continue - mainLoop ctx handle state' - -/-- Entry point. -/ -def main (args : List String) : IO Unit := do - -- Create context - let ctx ← GPU.create 1280 720 "Cells" - - -- Build initial cell graph (a trivial example) - let graph := initialGraph - let shader ← compileGraph graph - let handle ← GPU.compileShader ctx shader - - -- Initial state - let state : WorkspaceState := { - graph := graph - projection := defaultProjection - selection := none - uniforms := FloatArray.mk #[0.0, 0.0, 1.0] -- time, mouse_x, mouse_y - } - - -- Run - mainLoop ctx handle state - GPU.destroy ctx -``` - ---- - -## 11. Rust Native Backend Specification - -> **Note on code samples below.** The C++ code examples retained from an -> earlier draft of this spec are illustrative of the *Lean FFI pattern* -> (taking `b_lean_obj_arg`, returning `lean_obj_res`, the -> `register_class` + finalizer dance). Rust's `extern "C"` functions -> mirror these shapes 1:1 via the `lean-sys` crate (bindgen-generated -> bindings to `lean/lean.h`), so the interop story is structurally -> identical. When the actual Rust crate is written, these C++ snippets -> become Rust equivalents without changes to the FFI boundary or the -> Lean-side `@[extern]` declarations. - -### 11.1 GPU Context (context.cpp) - -```cpp -// native/src/gpu/context.cpp - -#include -#include -// Include glad or similar for GL function loading - -struct CellsGPUContext { - GLFWwindow* window; - uint32_t width; - uint32_t height; - GLuint fullscreenVAO; // VAO for fullscreen quad - GLuint fullscreenVBO; -}; - -// Destructor for Lean's external object system -static void cells_gpu_finalize(void* ptr) { - auto* ctx = static_cast(ptr); - if (ctx->window) { - glfwDestroyWindow(ctx->window); - } - delete ctx; -} - -// For Lean GC traversal (no Lean objects inside) -static void cells_gpu_foreach(void* ptr, b_lean_obj_arg fn) { - // No Lean objects to traverse -} - -extern "C" LEAN_EXPORT lean_obj_res cells_gpu_create( - uint32_t width, uint32_t height, - b_lean_obj_arg title, lean_obj_arg world -) { - if (!glfwInit()) { - return lean_io_result_mk_error( - lean_mk_io_user_error( - lean_mk_string("Failed to initialize GLFW"))); - } - - glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4); - glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 5); - glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); - - const char* title_str = lean_string_cstr(title); - GLFWwindow* win = glfwCreateWindow( - width, height, title_str, nullptr, nullptr); - - if (!win) { - glfwTerminate(); - return lean_io_result_mk_error( - lean_mk_io_user_error( - lean_mk_string("Failed to create window"))); - } - - glfwMakeContextCurrent(win); - - // Load GL functions (glad, GLEW, etc.) - // gladLoadGLLoader((GLADloadproc)glfwGetProcAddress); - - // Create fullscreen quad VAO - auto* ctx = new CellsGPUContext{win, width, height, 0, 0}; - setup_fullscreen_quad(ctx); - - lean_object* obj = lean_alloc_external( - cells_gpu_finalize, cells_gpu_foreach, ctx); - return lean_io_result_mk_ok(obj); -} - -extern "C" LEAN_EXPORT lean_obj_res cells_gpu_destroy( - b_lean_obj_arg ctx_obj, lean_obj_arg world -) { - // Destructor will be called by Lean GC - // But we can force cleanup here - auto* ctx = static_cast( - lean_get_external_data(ctx_obj)); - if (ctx->window) { - glfwDestroyWindow(ctx->window); - ctx->window = nullptr; - } - glfwTerminate(); - return lean_io_result_mk_ok(lean_box(0)); -} -``` - -### 11.2 Shader Compilation (shader.cpp) - -```cpp -// native/src/gpu/shader.cpp - -#include - -// Minimal vertex shader for fullscreen quad -static const char* FULLSCREEN_VERT = R"( -#version 450 -out vec2 fragCoord; -void main() { - vec2 pos = vec2(gl_VertexID & 1, (gl_VertexID >> 1) & 1) * 2.0 - 1.0; - fragCoord = pos * 0.5 + 0.5; - gl_Position = vec4(pos, 0.0, 1.0); -} -)"; - -extern "C" LEAN_EXPORT lean_obj_res cells_gpu_compile_shader( - b_lean_obj_arg ctx_obj, - b_lean_obj_arg glsl_src, - lean_obj_arg world -) { - const char* frag_src = lean_string_cstr(glsl_src); - - // Compile vertex shader - GLuint vert = glCreateShader(GL_VERTEX_SHADER); - glShaderSource(vert, 1, &FULLSCREEN_VERT, nullptr); - glCompileShader(vert); - // ... check errors ... - - // Compile fragment shader - GLuint frag = glCreateShader(GL_FRAGMENT_SHADER); - glShaderSource(frag, 1, &frag_src, nullptr); - glCompileShader(frag); - // ... check errors ... - - // Link program - GLuint program = glCreateProgram(); - glAttachShader(program, vert); - glAttachShader(program, frag); - glLinkProgram(program); - // ... check errors ... - - glDeleteShader(vert); - glDeleteShader(frag); - - // Return program handle as external object - // (or boxed uint if handles fit in lean_box) - return lean_io_result_mk_ok(lean_box(program)); -} - -extern "C" LEAN_EXPORT lean_obj_res cells_gpu_set_uniforms( - b_lean_obj_arg ctx_obj, - b_lean_obj_arg handle_obj, - b_lean_obj_arg uniforms, - lean_obj_arg world -) { - uint32_t program = lean_unbox(handle_obj); - glUseProgram(program); - - // Walk the FloatArray and set uniforms by index - size_t n = lean_sarray_size(uniforms); - double* data = lean_float_array_cptr(uniforms); - - for (size_t i = 0; i < n; i++) { - // Convention: uniform locations are sequential - glUniform1f(i, (float)data[i]); - } - - return lean_io_result_mk_ok(lean_box(0)); -} - -extern "C" LEAN_EXPORT lean_obj_res cells_gpu_render_quad( - b_lean_obj_arg ctx_obj, - b_lean_obj_arg handle_obj, - lean_obj_arg world -) { - auto* ctx = static_cast( - lean_get_external_data(ctx_obj)); - uint32_t program = lean_unbox(handle_obj); - - glClear(GL_COLOR_BUFFER_BIT); - glUseProgram(program); - glBindVertexArray(ctx->fullscreenVAO); - glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); - - return lean_io_result_mk_ok(lean_box(0)); -} -``` - -### 11.3 Input Handling (input.cpp) - -```cpp -// native/src/gpu/input.cpp - -#include -#include - -// InputEvent constructors match the Lean inductive: -// 0 = mouseMove, 1 = mouseDown, 2 = mouseUp, -// 3 = mouseDrag, 4 = keyDown, 5 = keyUp, -// 6 = scroll, 7 = resize, 8 = quit, 9 = none - -extern "C" LEAN_EXPORT lean_obj_res cells_gpu_poll_input( - b_lean_obj_arg ctx_obj, lean_obj_arg world -) { - auto* ctx = static_cast( - lean_get_external_data(ctx_obj)); - - glfwPollEvents(); - - if (glfwWindowShouldClose(ctx->window)) { - // Return InputEvent.quit (tag 8, no fields) - lean_object* ev = lean_alloc_ctor(8, 0, 0); - return lean_io_result_mk_ok(ev); - } - - // Check mouse position - double mx, my; - glfwGetCursorPos(ctx->window, &mx, &my); - double nx = mx / ctx->width; - double ny = my / ctx->height; - - // Return InputEvent.mouseMove (tag 0, 2 float fields) - lean_object* ev = lean_alloc_ctor(0, 0, 2 * sizeof(double)); - lean_ctor_set_float(ev, 0, nx); - lean_ctor_set_float(ev, sizeof(double), ny); - - return lean_io_result_mk_ok(ev); -} -``` - ---- - -## 12. Build Configuration - -### 12.1 lakefile.lean - -```lean -import Lake -open Lake DSL System - -def nativeDir : FilePath := "native" - -def gpuLinkArgs : Array String := - if System.Platform.isOSX then - #["-framework", "OpenGL", "-lglfw", - "-L/opt/homebrew/lib", "-I/opt/homebrew/include"] - else if System.Platform.isWindows then - #["-lopengl32", "-lglfw3"] - else -- Linux - #["-lGL", "-lglfw", "-lm", "-ldl", "-lpthread"] - -package cells where - moreLinkArgs := gpuLinkArgs - -@[default_target] -lean_lib Cells where - roots := #[`Cells] - -lean_exe cellsApp where - root := `Main - moreLinkArgs := gpuLinkArgs -``` - -### 12.2 CMakeLists.txt (native/) - -```cmake -cmake_minimum_required(VERSION 3.16) -project(cells_native C CXX) - -set(CMAKE_CXX_STANDARD 17) - -find_package(OpenGL REQUIRED) -find_package(glfw3 3.3 REQUIRED) - -add_library(cells_native STATIC - src/gpu/context.cpp - src/gpu/shader.cpp - src/gpu/framebuffer.cpp - src/gpu/input.cpp - src/emit/glsl.cpp - src/emit/wgsl.cpp - src/numeric/blas.cpp - src/numeric/mesh.cpp - src/numeric/solver.cpp -) - -target_include_directories(cells_native PUBLIC - include/ - ${LEAN_INCLUDE_DIR} -) - -target_link_libraries(cells_native - OpenGL::GL - glfw -) -``` - ---- - -## 13. Development Roadmap - -### Phase 1: Cubical Core (weeks 1–6) - -| Week | Deliverable | Test | -|------|-------------|------| -| 1 | `Interval.lean`, `Face.lean`, `Syntax.lean` | de Morgan laws, substitution | -| 2 | `Value.lean`, `Eval.lean` (λ-calculus fragment) | eval roundtrip for simple terms | -| 3 | `Transport.lean` (Π, Σ cases) | transport along refl = id | -| 4 | `Transport.lean` (Path case), `Comp.lean` | composition fills boxes | -| 5 | `Glue.lean`, `Equiv.lean` | ua computes at endpoints | -| 6 | `Soundness.lean` | transp_ua, monad laws | - -### Phase 2: Cells (weeks 7–9) - -| Week | Deliverable | Test | -|------|-------------|------| -| 7 | `Cell/Basic.lean`, `Compose.lean` | id is neutral, seq is assoc | -| 8 | `Cell/Higher.lean`, `Fiber.lean` | 2-cells compose, fibers compute | -| 9 | `Cell/Graph.lean`, `Reactive/` | graph flatten matches manual comp | - -**Phase 2 higher-cell backend — Zigzag Lean Port.** `Cell/Higher.lean` -depends on the n-category combinatorial engine: dimension-general -normalisation that correctly preserves essential identities past -dimension 4 (where homotopy.io historically caps out). This engine -is **ported into Lean** from the Rust reference at -`zigzag-engine/` — not consumed as an FFI dependency. See -`ZIGZAG_PORT.md` for the 10-step port plan. The port adds -`Topolei/Zigzag/{Monotone, Core, Diagram, Signature, Degeneracy, -Pullback, Normalise, Typecheck, Tests}.lean` + `Cell/Zigzag.lean` -bridge. Deliverables are Lean-native, pure functional, kernel-checked. -Independent of the Rust evaluator FFI; can proceed in parallel with it. - -### Phase 3: Shader Pipeline (weeks 10–13) - -| Week | Deliverable | Test | -|------|-------------|------| -| 10 | `Shader/IR.lean`, `Emit.lean` | emit a hardcoded ShaderProgram → valid GLSL | -| 11 | `Shader/Compile.lean` | compile a float-arithmetic cell to ShaderIR | -| 12 | `native/gpu/` + FFI wrappers | open window, render solid color | -| 13 | End-to-end: cell → shader → GPU | render a gradient from a cell definition | - -### Phase 4: Interaction (weeks 14–17) - -| Week | Deliverable | Test | -|------|-------------|------| -| 14 | `Runtime/Input.lean`, `Loop.lean` | mouse events reach Lean | -| 15 | Fiber selection + drag deformation | drag changes uniform, visual updates | -| 16 | `Color/Space.lean`, color cells | color space transport renders correctly | -| 17 | `examples/Interactive.lean` | mouse-driven procedural shader | - -### Phase 5: Boundary and Security (weeks 18–22) - -| Week | Deliverable | Test | -|------|-------------|------| -| 18 | `Boundary/Access.lean`, `ProCell.lean` | modal cells annotate correctly | -| 19 | `Boundary/Guard.lean`, `Security.lean` | guard cells block invalid crossings | -| 20 | `Boundary/Hardware.lean`, `Kernel.lean` | hardware model compiles, sealed tower types check | -| 21 | `Boundary/Classifier.lean`, `Expansion.lean` | boundary transport moves access levels | -| 22 | `SecureCellGraph`, wgpu `ToolIntegration` | cross-boundary wires require guards, wgpu pullback works | - -### Phase 6: Self-Hosting (weeks 23+) - -| Milestone | Deliverable | -|-----------|-------------| -| 23–25 | `Meta/DSL.lean`: syntax extensions for cell notation | -| 26–29 | `Meta/Widget.lean`: cell graph editor as a cell graph | -| 30+ | The editor edits itself. The system is complete. | - ---- - -## 14. Key Theorems (Proof Obligations) - -These are the theorems that must be proved (or deliberately `sorry`'d with documented intent) for the system to be sound. - -### Cubical Core - -| Theorem | Statement | Priority | -|---------|-----------|----------| -| `transp_refl` | Transport along a constant line is the identity | Critical | -| `transp_concat` | Transport along a concatenation is composition of transports | Critical | -| `hcomp_faces` | Composition agrees with the tube on constrained faces | Critical | -| `ua_endpoints` | `ua(e)` evaluates to `A` at 0 and `B` at 1 | Critical | -| `transp_ua` | Transport along `ua(e)` computes as `e` | Critical | -| `glue_beta` | `unglue (glue t a) = a` | High | -| `glue_eta` | `glue (unglue g) = g` on appropriate faces | High | - -### Cell Layer - -| Theorem | Statement | Priority | -|---------|-----------|----------| -| `cell_left_unit` | `id ∘ c ≈ c` | High | -| `cell_right_unit` | `c ∘ id ≈ c` | High | -| `cell_assoc` | `(c₁ ∘ c₂) ∘ c₃ ≈ c₁ ∘ (c₂ ∘ c₃)` | High | -| `cell_inv_left` | `c⁻¹ ∘ c ≈ id` | Medium | -| `par_seq_interchange` | `(c₁ × c₂) ∘ (c₃ × c₄) ≈ (c₁ ∘ c₃) × (c₂ ∘ c₄)` | Medium | - -### Shader Compilation - -| Theorem | Statement | Priority | -|---------|-----------|----------| -| `compile_sound` | Compiled shader computes same function as cell transport | Critical | -| `reduce_complete` | If a cell has no free dim variables, `reduceToConcrete` succeeds | High | -| `emit_parse_roundtrip` | Emitted GLSL, if parsed, gives back the same ShaderIR | Medium | - -### Boundary and Security - -| Theorem | Statement | Priority | -|---------|-----------|----------| -| `guard_completeness` | Every cross-boundary wire has a guard cell | Critical | -| `deny_is_empty` | A `deny` guard allows no data through | Critical | -| `process_isolation` | Disjoint address spaces cannot observe each other | Critical | -| `isa_sheaf_condition` | ISA-level observations are invariant under microarch change | High | -| `boundary_transport` | Tool absorption moves access levels monotonically upward | High | -| `pullback_agreement` | Pullback cells agree with external tool on shared semantics | High | -| `kan_extension_faithful` | Kan-extended cell types faithfully represent native capabilities | Medium | -| `modal_composition` | Composing modal cells preserves the minimum access level | Medium | - ---- - -## 15. Computational Boundaries and the Accessibility Topology - -This section formalizes the mathematical object that describes the border between what the cell system can access and what it cannot — and how to represent unknown, inaccessible, or adversarial computation as cells before the system has absorbed it. - -### 15.1 The Problem of Separated Computational Spaces - -The cell system inhabits a small subspace `C₀` of the full computational universe `U`. Outside `C₀` lie GPU compute APIs, audio engines, physics solvers, network protocols, other processes, other machines, other architectures. The boundary between `C₀` and `U \ C₀` is not a sharp wall — it has graded structure. Some external capabilities are one pullback away (wgpu), some are many abstractions away (quantum hardware), and some are adversarial (untrusted network inputs). - -Below the cell system lie layers that are sealed by design: the language runtime, the kernel, the ISA, the microarchitecture, the physics of transistors. These are cells whose interiors cannot be inspected or deformed from above, but whose boundaries (the ABI, the syscall interface, the instruction encoding) are the surfaces the cell system touches. - -The mathematical object that encodes all of this — the border, the grading, the accessibility structure, and the security model — is a **stratified Lawvere-Tierney topology on the subobject classifier of the cell topos**. - -### 15.2 The Presheaf of Potential Cells - -Before considering accessibility, define the presheaf of all potential cells. The base category `Ctx` has as objects the computational contexts in which cells can execute — "local CPU," "GPU via wgpu," "remote server," "sandboxed WASM runtime," etc. A presheaf on `Ctx` assigns to each context `c` the type of cells that *could* exist there: - -``` -P : Ctx^op → Type -``` - -The restriction maps `P(c) → P(c')` for an inclusion `c' ↪ c` express: if a cell works in a richer context, which cells still work in a poorer one. - -Not every potential cell is actually available. A GPU compute cell might be well-typed in the cubical calculus but lack an FFI binding. A cell from an external library might exist but be unverified. The gap between "potential" and "actual" is the accessibility structure. - -### 15.3 The Accessibility Modality - -A **Lawvere-Tierney topology** `j` on the topos of presheaves over `Ctx` is an endomorphism on the subobject classifier: - -``` -j : Ω → Ω -``` - -satisfying idempotence (`j ∘ j = j`), preservation of truth (`j(⊤) = ⊤`), and preservation of conjunction (`j(p ∧ q) = j(p) ∧ j(q)`). - -In the cell system, `Ω` classifies propositions about cells — "this cell exists," "this cell is well-typed," "this cell's transport is verified," "this cell's compiled shader is correct." The topology `j` selects which of these propositions count as actually accessible from the system's current vantage point. - -A proposition `φ` is **`j`-true** when `j(φ) = ⊤`. The `j`-sheaves are the presheaves whose sections are determined by their `j`-local data — these are the cells the system can fully see and manipulate. - -The non-sheaves are the cells at the boundary. The **sheafification** functor `L : PSh(Ctx) → Sh_j(Ctx)` is the process of absorbing external capability — pulling it into the cell system. The cells you have are the sheaves. The cells you could have are the presheaves. The boundary is the gap, and sheafification is how you close it. - -### 15.4 The Accessibility Lattice - -Different levels of access form a lattice of topologies, each coarser (more permissive) than the last: - -``` -j_verified ≤ j_typed ≤ j_compiled ≤ j_accessible ≤ j_exists ≤ j_unknown -``` - -In Lean 4: - -```lean --- Cells/Boundary/Access.lean - -/-- Levels of accessibility for cells. - Each level corresponds to a Lawvere-Tierney topology - on the cell topos. -/ -inductive AccessLevel : Type where - | verified : AccessLevel -- transport proof completed - | typed : AccessLevel -- well-typed in cubical calculus - | compiled : AccessLevel -- shader compiles, proof incomplete - | accessible : AccessLevel -- FFI binding exists, not compiled - | exists : AccessLevel -- capability known to exist externally - | unknown : AccessLevel -- beyond the border - deriving Repr, BEq, Ord - -/-- A modal cell: a cell annotated with its accessibility level. -/ -structure ModalCell where - cell : Cell - level : AccessLevel - evidence : CellEvidence cell level - -/-- Evidence appropriate to each access level. -/ -inductive CellEvidence : Cell → AccessLevel → Type where - | verified : TransportProof c → CellEvidence c .verified - | typed : TypeCheckResult c → CellEvidence c .typed - | compiled : ShaderProgram → CellEvidence c .compiled - | accessible : FFIBinding → CellEvidence c .accessible - | exists : CellSpec → CellEvidence c .exists - | unknown : CellEvidence c .unknown -``` - -### 15.5 Trust Boundaries and Guard Cells - -Cells that cross modality boundaries have their wires annotated with the trust gap. A **guard cell** sits on a trust boundary and validates data crossing it. The guard IS a transport — it either passes data through (if valid) or blocks it: - -```lean --- Cells/Boundary/Guard.lean - -/-- A wire across a trust boundary. -/ -structure BoundaryWire extends Wire where - trustedSide : Port - trustedLevel : AccessLevel - untrustedSide : Port - untrustedLevel : AccessLevel - gap : trustedLevel > untrustedLevel - guard : GuardCell trustedLevel untrustedLevel - -/-- The type of validation required at a trust boundary. -/ -inductive GuardType where - | none : GuardType -- within trust zone, no guard needed - | typecheck : GuardType -- dynamic type check at boundary - | sanitize : GuardType -- input sanitization / normalization - | sandbox : GuardType -- execute in isolated context - | verify : GuardType -- full formal verification required - | deny : GuardType -- connection forbidden - -/-- A security policy: a choice of which trust gaps are - permitted and what guards they require. -/ -structure SecurityPolicy where - maxGap : Nat - guardFor : (gap : Nat) → (gap ≤ maxGap) → GuardType - trustTyped : Bool -- can verified cells receive from typed-only? - trustFFI : Bool -- can compiled cells receive from FFI-only? - trustExtern : Bool -- can any cell receive from 'exists' level? - -/-- A guard cell: its transport IS the validation function. -/ -def mkGuardCell (policy : SecurityPolicy) (gap : Nat) - (h : gap ≤ policy.maxGap) : Cell := - match policy.guardFor gap h with - | .typecheck => { - line := .dimLam (.pi (.base "UntrustedInput") (.base "Result")) - input := .base "UntrustedInput" - output := .base "ValidatedInput" - input_spec := sorry - output_spec := sorry - } - | .sandbox => sorry -- transport executes in isolated context - | .deny => sorry -- transport is the empty function (⊥ → A) - | _ => sorry -``` - -### 15.6 Pro-Cells: Modeling the Unknown Exterior - -For capabilities beyond the border — external systems you don't yet have access to and may not fully understand — the right object is a **pro-cell**: a formal cofiltered limit of increasingly refined approximations. - -You can't see quantum computing cells, but you can describe increasingly refined guesses about what they would look like. Each approximation is a `CellSpec` at level `exists` or `unknown`. The actual external system is the limit of the tower — which you never reach, but you can work with any finite stage. - -```lean --- Cells/Boundary/ProCell.lean - -/-- A partial description of a cell whose interior is unknown. -/ -structure CellSpec where - inputHint : Option CType -- what we know about input - outputHint : Option CType -- what we know about output - constraints : List CellConstraint -- behavioral constraints - source : SpecSource -- where this spec came from - -/-- Provenance of a cell specification. -/ -inductive SpecSource where - | documentation : String → SpecSource - | observed : List (CTerm × CTerm) → SpecSource -- I/O pairs - | inferred : CTerm → SpecSource -- from related cells - | adversarial : SpecSource -- assume worst case - -/-- A pro-cell: a cell represented by a tower of - increasingly refined approximations. -/ -structure ProCell where - approx : Nat → CellSpec - refine : ∀ n, Refinement (approx (n+1)) (approx n) - level : AccessLevel - level_bound : level ≤ .exists - -/-- Refinement: a later approximation is more specific. -/ -structure Refinement (fine coarse : CellSpec) where - input_refines : fine.inputHint.isSome → coarse.inputHint.isSome - output_refines : fine.outputHint.isSome → coarse.outputHint.isSome - constraints_extend : coarse.constraints ⊆ fine.constraints -``` - -### 15.7 The Boundary Classifier as a Cell - -The boundary between accessible and inaccessible computation is not a wall — it's a **classifier**. In topos theory, the subobject classifier `Ω` is an object in the topos. Since the computational universe is (modeled as) a topos, the classifier of accessibility is itself a cell. - -There is a cell `∂ : CType` whose elements are propositions about accessibility. A section of `∂` over the workspace assigns to each position in the cell graph a truth value saying "is this accessible?" The boundary is the frontier of this section — the set of positions where accessibility changes. - -```lean --- Cells/Boundary/Classifier.lean - -/-- The accessibility classifier as a cell type. -/ -def AccessClassifier : CType := - .pi (.base "CellId") (.base "AccessLevel") - -/-- A boundary is a cell whose type is the accessibility - classifier. Its transport moves the boundary: as you - absorb new tools, the accessible region grows. -/ -structure Boundary where - current : CTerm -- AccessClassifier value (current state) - expansion : Cell -- transport: current → new state after absorption -``` - -The transport of the boundary cell IS the process of absorbing a new tool. When you perform a pullback to integrate wgpu, you are applying a transport to the boundary cell that moves some cells from `exists` to `accessible` or `compiled`. The evolution of the boundary is a path in the space of topologies on the topos, which is itself a transport, which is itself a cell. - ---- - -## 16. Hardware and Kernel as Sealed Cells - -### 16.1 The Sealed Cell Tower - -The computational infrastructure beneath the cell system forms a tower, each level a cell, each boundary a trust topology: - -``` -Level 7: Cell calculus (your system) - ↑ guard: the type checker - ↑ topology: j_cell - -Level 6: Lean 4 runtime - ↑ guard: the compiler - ↑ topology: j_language - -Level 5: Userspace process - ↑ guard: the kernel's syscall handler - ↑ topology: j_userspace - -Level 4: Kernel - ↑ guard: interrupt handlers, MMU - ↑ topology: j_kernel - -Level 3: ISA (instruction set architecture) - ↑ guard: decoder, retirement unit - ↑ topology: j_isa - -Level 2: Microarchitecture - ↑ guard: fabrication masks - ↑ topology: j_microarch - -Level 1: Physics (transistors, electrons) - ↑ guard: the laws of physics - ↑ topology: j_physics -``` - -Each level is a cell. Each boundary is a guard cell. Each topology says what's visible from above. The whole tower is a cosimplicial object — a sequence of cells with face maps (projections that forget a level) and degeneracy maps (insertions that add a vacuous level). - -### 16.2 Registers as Cells - -A CPU register is the simplest hardware cell. Its transport maps "value written" to "value read," but this transport passes through time (clock cycles) and through hazards (pipeline forwarding, speculative execution rollback). - -```lean --- Cells/Boundary/Hardware.lean - -/-- A register cell: transport from write-time to read-time. -/ -structure RegisterCell where - index : RegId - wordType : MachineType -- .u64, .f64, .v128 - cell : Cell - /-- Identity when no hazards intervene -/ - hazardFree : HazardCondition → cell.asTransport = Cell.id wordType.toCType - /-- Forwarding path when hazards exist -/ - forwarded : Hazard → cell.asTransport = forwardingPath - -/-- From userspace, registers are opaque modal cells: - you can use them but cannot inspect their transport. -/ -def userspaceRegister (r : RegId) : ModalCell where - cell := registerTransport r - level := .accessible - evidence := .accessible (ffiBinding r) -``` - -### 16.3 ALU Operations as Cells - -An ALU operation (integer addition, multiplication, etc.) is a cell whose transport maps `(a, b)` to the result. The implementation — carry-lookahead adder, transistor-level logic — is sealed behind multiple layers of opacity. Each sealing IS a Lawvere-Tierney topology that hides the level below: - -```lean --- Each hardware abstraction level defines a topology - -/-- The ISA topology: instructions visible, microarch hidden. - THIS is the ISA guarantee: same instructions, different - implementations, same observable behavior. -/ -def j_isa : Topology where - isLocal := fun φ => φ ∈ isaSpecPropositions - -/-- The ABI topology: calling convention visible, encoding hidden. -/ -def j_abi : Topology where - isLocal := fun φ => φ ∈ abiSpecPropositions - -/-- The topology chain -/ --- j_physics ≤ j_microarch ≤ j_isa ≤ j_abi ≤ j_language ≤ j_cell -``` - -### 16.4 The Kernel as a Mediating Cell - -The operating system kernel is a cell that mediates between userspace and hardware. Its transport maps userspace requests to hardware operations, factored through permission checks: - -```lean -/-- The kernel cell: mediation between userspace and hardware. -/ -structure KernelCell where - syscallType : CType - cell : Cell - /-- The transport factors through permission checking -/ - factors : cell.asTransport = permissionCheck ∘ hardwareDispatch ∘ validateArgs - /-- The permission check is a guard cell -/ - guard : GuardCell kernelPolicy trustGap sorry -``` - -### 16.5 Virtual Memory as a Fibration - -Virtual memory is a fiber bundle over virtual address space. The base space is the virtual address space. The fiber over each virtual page is the set of physical frames it could map to. The page table is a section — a choice of mapping. The sheaf condition says: adjacent pages with compatible permissions can be mapped to arbitrary physical frames. - -```lean -/-- Virtual memory modeled as a sheaf over address space. -/ -structure PageTableCell where - virtualSpace : CType -- Fin (2^48) - physicalSpace : CType -- Fin (2^52) - mapping : CTerm -- virtualSpace → physicalSpace - permissions : CTerm -- virtualSpace → Permission - /-- The security property: process isolation -/ - isolation : ∀ pid₁ pid₂, pid₁ ≠ pid₂ → - disjoint (mapping pid₁) (mapping pid₂) -``` - -Process isolation is the sheaf condition that two processes' sections of the address fibration don't interfere. A kernel exploit that breaks isolation is a proof that the sheaf condition fails. - -### 16.6 The Hardware Model from the Cell System - -The cell system models the entire sealed tower as pro-cells and opaque modal cells: - -```lean -/-- The hardware beneath the cell system. -/ -structure HardwareModel where - isa : CellSpec -- ISA-level: what you can see - microarch : ProCell -- microarch: what you guess - physics : ProCell -- physics: pure speculation - trustISA : AccessLevel := .compiled - trustSilicon : AccessLevel := .exists - -/-- The kernel beneath the cell system. -/ -structure KernelModel where - syscalls : Array CellSpec -- syscall interface - scheduler : ProCell -- scheduling: opaque - mmu : ProCell -- memory management: opaque - trustLevel : AccessLevel := .accessible -``` - ---- - -## 17. Security as Cell Topology - -### 17.1 Vulnerabilities as Topological Failures - -The accessibility topology gives every class of vulnerability a precise type-theoretic description: - -A **buffer overflow** is a cell whose transport writes past its declared output boundary — the fiber over the output port is larger than the type claims. - -A **privilege escalation** is a transport that crosses a guard cell without passing through the guard — the factorization property of the kernel cell is violated. - -A **side channel** is information leaking through a topology — a proposition that should be `j`-hidden is actually `j`-observable. The sheaf condition for the hiding topology fails. - -A **sandbox escape** is a section of the accessibility sheaf that extends past its declared boundary — a cell at level `accessible` behaves as though it's at level `compiled` for resources outside its sandbox. - -Each of these can be stated as a formal property of the cell graph, and the security policy's guard cells are the mechanisms that prevent them. - -### 17.2 Security Properties as Theorems - -```lean --- Cells/Boundary/Security.lean - -/-- No data crosses a trust boundary without passing through a guard. -/ -theorem guard_completeness (g : CellGraph) (policy : SecurityPolicy) : - ∀ w ∈ g.wires, - accessLevel (g.portCell w.source) > accessLevel (g.portCell w.target) → - ∃ guard ∈ g.cells, isGuardOn guard w := - sorry - -/-- A guard cell at level `deny` allows no data through. -/ -theorem deny_is_empty (guard : GuardCell p .deny) : - ∀ input, guard.cell.asTransport input = ⊥ := - sorry - -/-- Process isolation: disjoint address spaces - cannot observe each other's state. -/ -theorem process_isolation (pt : PageTableCell) - (pid₁ pid₂ : ProcessId) (h : pid₁ ≠ pid₂) : - ∀ (obs : Observable), - obs ∈ visibleTo pid₁ pt → - obs ∉ visibleTo pid₂ pt := - sorry - -/-- The ISA sheaf condition: if the microarchitecture satisfies - the ISA spec, then ISA-level observations are invariant - under microarchitectural change. -/ -theorem isa_sheaf_condition (impl₁ impl₂ : MicroarchImpl) - (h₁ : satisfiesISA impl₁) (h₂ : satisfiesISA impl₂) : - ∀ (prog : Program) (obs : ISAObservable), - observe obs (run impl₁ prog) = observe obs (run impl₂ prog) := - sorry - -- When this theorem FAILS for a specific obs, - -- that obs is a side channel (Spectre, Meltdown, etc.) -``` - -### 17.3 Trust-Annotated Cell Graphs - -A workspace with security is a cell graph where every wire is annotated with its trust level and every cross-boundary wire has a guard: - -```lean -/-- A security-aware cell graph. -/ -structure SecureCellGraph extends CellGraph where - /-- Access level for each cell -/ - levels : Array AccessLevel - levels_size : levels.size = cells.size - /-- Every cross-boundary wire has a guard -/ - guarded : ∀ i, i < wires.size → - let w := wires[i]! - let srcLevel := levels[w.source.cellId]! - let tgtLevel := levels[w.target.cellId]! - srcLevel ≠ tgtLevel → - ∃ g, g < cells.size ∧ isGuardBetween cells[g]! w - /-- The security policy in effect -/ - policy : SecurityPolicy - /-- All guards conform to the policy -/ - policyConformance : ∀ g, isGuard cells[g]! → - guardConformsToPolicy cells[g]! policy - -/-- Check if adding a new wire would violate the security policy. -/ -def SecureCellGraph.canConnect (g : SecureCellGraph) - (src tgt : Port) : Bool := - let srcLevel := g.levels[src.cellId]! - let tgtLevel := g.levels[tgt.cellId]! - if srcLevel == tgtLevel then true - else - let gap := accessGap srcLevel tgtLevel - gap ≤ g.policy.maxGap && - g.policy.guardFor gap sorry != .deny -``` - ---- - -## 18. Expanding the Cell Space — The Grothendieck Construction - -### 18.1 Tools as a Diagram - -As the system absorbs more external capabilities, each tool defines a "spoke" in a diagram: - -``` - CellTypes (current) - / | | \ - / | | \ - / | | \ - WGPU Audio Physics Editor - | | | | - ↓ ↓ ↓ ↓ - GPUOps SndOps SimOps IOOps -``` - -The total system at any point is the wide pullback (limit) over all spokes. In ∞-categorical language, this is the homotopy limit of the diagram. - -### 18.2 The Grothendieck Fibration - -The long-term architecture is a Grothendieck fibration. The base category is `Tools` (wgpu, audio, physics, network, editor, etc.). The fiber over each tool is the category of cell types that interface with it. The total category is the full system. - -Sections of this fibration are programs — a coherent choice of cells, one for each tool the program uses, such that the compositions agree. The cell graph (workspace) IS a section. The type system enforces that sections are coherent. - -The expansion process — absorbing a new tool — is extending the base category by one object and constructing the fiber over it via right Kan extension: - -```lean --- Cells/Boundary/Expansion.lean - -/-- A tool integration: the data needed to absorb an - external capability into the cell system. -/ -structure ToolIntegration where - /-- Name of the tool -/ - name : String - /-- The tool's native type system -/ - nativeTypes : List CType - /-- FFI bindings (Rust functions exposed via C ABI) -/ - bindings : List FFIBinding - /-- The pullback: cell types that map to native types -/ - cellTypes : List CType - /-- Agreement: each cell type compiles to the native type -/ - agreement : ∀ i, i < cellTypes.length → - compilesTo cellTypes[i]! nativeTypes[i]! - /-- New cell types constructed by Kan extension: - native capabilities without existing cell counterparts -/ - extensions : List CType - /-- Evidence that extensions faithfully represent - the native capabilities -/ - extensionSpec : ∀ i, i < extensions.length → - ∃ native, faithfulRepresentation extensions[i]! native - -/-- Apply a tool integration: expand the cell graph - to include the new tool's capabilities. -/ -def expandCellSpace (g : SecureCellGraph) - (tool : ToolIntegration) : SecureCellGraph := - -- 1. Add the pullback cells (known correspondence) - -- 2. Add the Kan extension cells (new capabilities) - -- 3. Add guard cells at the trust boundary - -- 4. Set access levels for new cells - -- 5. Verify policy conformance - sorry - -/-- The wgpu integration as a concrete example. -/ -def wgpuIntegration : ToolIntegration where - name := "wgpu" - nativeTypes := [ - .base "WGPUDevice", - .base "WGPURenderPipeline", - .base "WGPUBuffer", - .base "WGPUTexture", - .base "WGPUComputePipeline" - ] - bindings := [ - ffi "cells_wgpu_create_device", - ffi "cells_wgpu_create_pipeline", - ffi "cells_wgpu_create_buffer", - -- ... - ] - cellTypes := [ - .base "GPUContext", -- maps to WGPUDevice - .base "ShaderHandle", -- maps to WGPURenderPipeline - -- ... - ] - agreement := sorry - extensions := [ - .base "ComputeCell", -- Kan extension: no prior cell type - .base "StorageBuffer", -- Kan extension: new capability - ] - extensionSpec := sorry -``` - -### 18.3 The Complete Boundary Object - -The full mathematical object encoding the cell system's relationship to everything outside it: - -``` -(Sh_{j_policy}(Ctx), ∂, {j_level}_{level ∈ AccessLevel}, ∫F) -``` - -where: - -- `Ctx` is the category of computational contexts -- `j_policy` is the active security policy topology -- `∂ ∈ Sh_{j_policy}(Ctx)` is the boundary classifier (itself a cell) -- `{j_level}` is the lattice of accessibility topologies -- `∫F` is the Grothendieck construction over the tool diagram `F : Tools^op → Cat` - -The cells you have are the `j_verified`-sheaves. The cells you're absorbing are moving from `j_exists`-sheaves to `j_accessible`-sheaves via pullback + Kan extension. The cells at the boundary are presheaves that aren't yet sheaves for your current topology. The security model is the choice of topology. The evolution of the boundary is a path in the space of topologies — itself a transport, itself a cell. It is cells all the way up, and the sealed hardware tower is cells all the way down. - ---- - -## 19. Notation and DSL (Future) - -The `Meta/DSL.lean` module will provide syntax extensions for ergonomic cell definition. Target notation: - -```lean --- Define a cell with sugared syntax -cell gradient : Color.sRGB ↔ Color.sRGB := - transport (i : 𝕀) => - Color.mix (Color.red) (Color.blue) (Dim.toFloat i) - --- Compose cells with ∘ -cell pipeline := geometry ∘ lighting ∘ tonemapping - --- Parallel composition with × -cell stereo := left_eye × right_eye - --- 2-cell (deformation) with ≃ -deform fade : shader_a ≃ shader_b := - homotopy (i j : 𝕀) => - Color.mix (shader_a @ i) (shader_b @ i) (Dim.toFloat j) - --- Interactive manipulation -interactive myShader := - manip (noise_cell >>= color_map_cell) - project: rasterize - deform: closest_param -``` - ---- - -## 20. Glossary - -| Term | Definition in this system | -|------|--------------------------| -| **Cell** | A transport: a line of types `(i : 𝕀) ⊢ A(i) type` with its induced map `A(0) → A(1)`. | -| **Transport** | The map `A(0) → A(1)` induced by a line of types. Synonymous with "cell." | -| **Fiber** | Given a projection `π : X → S`, the fiber over `s ∈ S` is `{x : X | π(x) = s}`. Mouse position selects a fiber. | -| **Projection** | A map from a computational cell to screen space. Rendering is evaluation of a projection. | -| **Section** | A continuous choice of one fiber element for each screen point. A rendered image is a section. A program is a section of the Grothendieck fibration over tools. | -| **Composition** | Path concatenation. Sequential wiring of cells. Monadic bind. | -| **Glue type** | The type former that constructs paths between equivalent types, giving univalence. | -| **Face formula** | A proposition constraining dimension variables, defining on which faces of a cube a partial element is specified. | -| **hcomp** | Homogeneous composition: filling an open box at a fixed type. | -| **transp** | Transport: moving a value from one end of a type line to the other. | -| **ShaderIR** | A restricted intermediate representation of cell computations that maps to GPU shader languages. | -| **Workspace** | A cell graph: a network of cells connected by wires. The top-level object of the system. | -| **AccessLevel** | A grade in the lattice of accessibility topologies, from `verified` (fully proven) to `unknown` (beyond the border). | -| **ModalCell** | A cell annotated with its accessibility level and the evidence appropriate to that level. | -| **GuardCell** | A cell sitting on a trust boundary whose transport validates data crossing between access levels. | -| **ProCell** | A cell represented by a tower of increasingly refined approximations, for capabilities beyond the accessible border. | -| **Boundary** | The frontier of the accessibility classifier — where the access level changes. Itself a cell whose transport is tool absorption. | -| **Lawvere-Tierney topology** | An endomorphism `j : Ω → Ω` on the subobject classifier selecting which propositions about cells count as "locally true." Different topologies define different trust levels. | -| **Sealed cell** | A cell whose interior cannot be inspected or deformed from the current access level — registers, ALU ops, kernel internals. | -| **Sheaf condition** | The requirement that local data (individual cell behaviors) agreeing on overlaps (shared state) glue to global data (program behavior). Violation = vulnerability. | -| **Pullback** | The universal type mapping into both the cell calculus and an external tool's type system, agreeing on shared semantics. The basic move for absorbing a new tool. | -| **Kan extension** | The construction of new cell types to cover external capabilities that have no existing cell counterpart. How the cell system grows. | -| **Grothendieck fibration** | The total system: a fibration over the category of tools, whose fiber over each tool is the cell types that interface with it. Sections are programs. | - ---- - -*This document is the complete specification for the Cells system as a Lean 4 extension. It depends on nothing outside itself, Lean 4's `Init`, and the Rust standard library (reached via a C ABI FFI). Everything described here — from the de Morgan interval algebra through the sealed cell tower of hardware and kernel, to the Grothendieck fibration of tool absorption — is buildable, testable, and maintainable by a single developer. The process discipline is absolute: every layer is formalized in Lean as axioms before the Rust FFI discharges them. The boundary is a cell. The expansion of the boundary is a transport. It is cells all the way down, and cells all the way up.* diff --git a/exp-log.pdf b/exp-log.pdf deleted file mode 100644 index 33c2a59..0000000 Binary files a/exp-log.pdf and /dev/null differ diff --git a/lakefile.toml b/lakefile.toml index 8994ea8..47b47bc 100644 --- a/lakefile.toml +++ b/lakefile.toml @@ -1,14 +1,14 @@ -name = "topolei" +name = "cubicalTransport" version = "0.1.0" -defaultTargets = ["topolei", "cubical-test"] +defaultTargets = ["cubical-test"] [[lean_lib]] -name = "Topolei" +name = "CubicalTransport" [[lean_exe]] name = "cubical-test" root = "CubicalTest" -# Runs Phase C.3 smoke tests + Phase D.1 property tests on the +# Phase C.3 smoke tests + Phase D.1 property tests on the # Rust-backed cubical evaluator. No GPU dependencies. moreLinkArgs = [ "./native/cubical/target/release/libtopolei_cubical.a", @@ -21,91 +21,3 @@ root = "CubicalBench" moreLinkArgs = [ "./native/cubical/target/release/libtopolei_cubical.a", ] - -[[lean_exe]] -name = "topolei" -root = "Main" -moreLinkArgs = [ - # Rust canvas (wgpu + winit + naga-glsl) — replaces the OpenGL/GLFW C++ - # canvas.cpp. Provides topolei_run / topolei_run2 declared in - # Topolei/Canvas.lean. Targets Vulkan/Metal/DX12/WebGPU under wgpu. - "./native/canvas-rs/target/release/libtopolei_canvas.a", - # Rust cubical-HoTT backend (topolei-cubical) built by build.sh via cargo. - # Provides topolei_cubical_* symbols declared in Topolei/Cubical/FFI.lean. - "./native/cubical/target/release/libtopolei_cubical.a", - # NOTE: the render crate (libtopolei_render.a) is not linked here — it - # and canvas-rs each embed their own copy of the Rust runtime, which - # collides at static-link time (rust_eh_personality et al.). The render - # crate will be merged into canvas-rs, or one of the two made a cdylib, - # when render actually has load-bearing FFI. - # System libs needed by wgpu (Vulkan driver loader + X11 for winit) and - # glibc (TLS symbols via __tls_get_addr required by wgpu's dlopen path). - # Match Lean's toolchain glibc path so __tls_get_addr resolves from the - # same DSO ld-linux-x86-64.so.2 the final executable will load. - # Explicitly include libc.so.6 + ld-linux-x86-64.so.2 — wgpu's statically- - # linked Rust code references __tls_get_addr, resolved only by the - # dynamic linker DSO. Paths point at Lean's toolchain glibc. - # Avoid -L/run/current-system/sw/lib because its libc.a leaks - # __open_nocancel/_setjmp into the static link. Point -L at each lib's - # nix-store dir individually. - "-Wl,--no-as-needed", - "/nix/store/wb6rhpznjfczwlwx23zmdrrw74bayxw4-glibc-2.42-47/lib/libc.so.6", - "/nix/store/wb6rhpznjfczwlwx23zmdrrw74bayxw4-glibc-2.42-47/lib/ld-linux-x86-64.so.2", - "-L/nix/store/7h3p5dm7p8wrbdm7ssb3mybvcjm5f79p-vulkan-loader-1.4.328.0/lib", - "-L/nix/store/2yvh4kwhfd65dcd3r6y6bgdwclfndvzr-libX11-1.8.12/lib", - "-L/nix/store/xwd1s74zk3bwilv4p02284ckyy319vhz-libxcb-1.17.0/lib", - "-L/nix/store/k9ab9lfy15l7br6iagxiwdgdi9kkby88-libxkbcommon-1.8.1/lib", - "-lvulkan", - "-lX11", - "-lxcb", - "-lxkbcommon", - "-ldl", - "-lpthread", - "-lm", - "-lgcc_s", - # RPATH: bake library search paths into the binary so winit's runtime - # dlopen() calls find libXcursor.so.1, libXi.so.6, libXrandr.so.2, - # libXext.so, etc. without the user setting LD_LIBRARY_PATH. - "-Wl,-rpath,/nix/store/7h3p5dm7p8wrbdm7ssb3mybvcjm5f79p-vulkan-loader-1.4.328.0/lib", - "-Wl,-rpath,/nix/store/c0z5kfib8j6xcmbkdknwkkqy38nwph4c-libXcursor-1.2.3/lib", - "-Wl,-rpath,/nix/store/frfb398wg8imfw5r0ac18gy389by0vap-libXi-1.8.2/lib", - "-Wl,-rpath,/nix/store/fxw37cf0j4zp5xagyq0j144536qwc9q4-libXrandr-1.5.4/lib", - "-Wl,-rpath,/nix/store/yzd9jj5q0ad2dzpmxhfs0ssp4ddq2j2r-ld-library-path/share/nix-ld/lib", - "-Wl,-rpath,/nix/store/2v2nlnxm34grn5iq1s1n4di9vsn3k4si-libXext-1.3.6/lib", -] - -[[lean_exe]] -name = "probe-test" -root = "ProbeTest" -# Empirical check for `render_faithful` (Topolei/GPU/Spec.lean). -# Uses the `topolei_canvas_render_probe_pixel` FFI to render a handful -# of known shaders offscreen and compare GPU pixel output against the -# Lean-side ShaderSemantic. Requires a GPU adapter or software -# rasterizer at runtime; the probe gracefully returns a sentinel and -# the test SKIPs when none is available. Link args mirror the -# interactive `topolei` exe since canvas-rs pulls the same wgpu / -# Vulkan / X11 stack (winit is compiled in but not used at probe time). -moreLinkArgs = [ - "./native/canvas-rs/target/release/libtopolei_canvas.a", - "-Wl,--no-as-needed", - "/nix/store/wb6rhpznjfczwlwx23zmdrrw74bayxw4-glibc-2.42-47/lib/libc.so.6", - "/nix/store/wb6rhpznjfczwlwx23zmdrrw74bayxw4-glibc-2.42-47/lib/ld-linux-x86-64.so.2", - "-L/nix/store/7h3p5dm7p8wrbdm7ssb3mybvcjm5f79p-vulkan-loader-1.4.328.0/lib", - "-L/nix/store/2yvh4kwhfd65dcd3r6y6bgdwclfndvzr-libX11-1.8.12/lib", - "-L/nix/store/xwd1s74zk3bwilv4p02284ckyy319vhz-libxcb-1.17.0/lib", - "-L/nix/store/k9ab9lfy15l7br6iagxiwdgdi9kkby88-libxkbcommon-1.8.1/lib", - "-lvulkan", - "-lX11", - "-lxcb", - "-lxkbcommon", - "-ldl", - "-lpthread", - "-lm", - "-lgcc_s", - "-Wl,-rpath,/nix/store/7h3p5dm7p8wrbdm7ssb3mybvcjm5f79p-vulkan-loader-1.4.328.0/lib", - "-Wl,-rpath,/nix/store/c0z5kfib8j6xcmbkdknwkkqy38nwph4c-libXcursor-1.2.3/lib", - "-Wl,-rpath,/nix/store/frfb398wg8imfw5r0ac18gy389by0vap-libXi-1.8.2/lib", - "-Wl,-rpath,/nix/store/fxw37cf0j4zp5xagyq0j144536qwc9q4-libXrandr-1.5.4/lib", - "-Wl,-rpath,/nix/store/yzd9jj5q0ad2dzpmxhfs0ssp4ddq2j2r-ld-library-path/share/nix-ld/lib", - "-Wl,-rpath,/nix/store/2v2nlnxm34grn5iq1s1n4di9vsn3k4si-libXext-1.3.6/lib", -] diff --git a/native/CMakeLists.txt b/native/CMakeLists.txt deleted file mode 100644 index adc86dd..0000000 --- a/native/CMakeLists.txt +++ /dev/null @@ -1,27 +0,0 @@ -cmake_minimum_required(VERSION 3.20) -project(topolei_native CXX) - -set(CMAKE_CXX_STANDARD 17) - -find_package(PkgConfig REQUIRED) -pkg_check_modules(GLFW REQUIRED glfw3) -pkg_check_modules(GL REQUIRED gl) -pkg_check_modules(GLEW REQUIRED glew) - -add_library(topolei_native STATIC src/canvas.cpp) - -target_include_directories(topolei_native PUBLIC - ${CMAKE_CURRENT_SOURCE_DIR}/include - ${GLFW_INCLUDE_DIRS} - ${GL_INCLUDE_DIRS} - ${GLEW_INCLUDE_DIRS} - "$ENV{HOME}/.elan/toolchains/leanprover--lean4---v4.30.0-rc2/include" -) - -target_link_libraries(topolei_native PUBLIC - ${GLFW_LIBRARIES} - ${GL_LIBRARIES} - ${GLEW_LIBRARIES} -) - -target_compile_options(topolei_native PRIVATE ${GLFW_CFLAGS_OTHER}) diff --git a/native/canvas-rs/Cargo.lock b/native/canvas-rs/Cargo.lock deleted file mode 100644 index 88e8464..0000000 --- a/native/canvas-rs/Cargo.lock +++ /dev/null @@ -1,2221 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - -[[package]] -name = "android-activity" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f2a1bb052857d5dd49572219344a7332b31b76405648eabac5bc68978251bcd" -dependencies = [ - "android-properties", - "bitflags 2.11.1", - "cc", - "jni", - "libc", - "log", - "ndk", - "ndk-context", - "ndk-sys 0.6.0+11769913", - "num_enum", - "thiserror 2.0.18", -] - -[[package]] -name = "android-properties" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "anstream" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" - -[[package]] -name = "anstyle-parse" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" -dependencies = [ - "anstyle", - "once_cell_polyfill", - "windows-sys 0.61.2", -] - -[[package]] -name = "arrayvec" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" - -[[package]] -name = "as-raw-xcb-connection" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" - -[[package]] -name = "ash" -version = "0.38.0+1.3.281" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" -dependencies = [ - "libloading", -] - -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - -[[package]] -name = "bit-set" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0481a0e032742109b1133a095184ee93d88f3dc9e0d28a5d033dc77a073f44f" -dependencies = [ - "bit-vec", -] - -[[package]] -name = "bit-vec" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2c54ff287cfc0a34f38a6b832ea1bd8e448a330b3e40a50859e6488bee07f22" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" - -[[package]] -name = "block" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" - -[[package]] -name = "block2" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" -dependencies = [ - "objc2", -] - -[[package]] -name = "bumpalo" -version = "3.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" - -[[package]] -name = "bytemuck" -version = "1.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" -dependencies = [ - "bytemuck_derive", -] - -[[package]] -name = "bytemuck_derive" -version = "1.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "bytes" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" - -[[package]] -name = "calloop" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" -dependencies = [ - "bitflags 2.11.1", - "log", - "polling", - "rustix 0.38.44", - "slab", - "thiserror 1.0.69", -] - -[[package]] -name = "cc" -version = "1.2.61" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" -dependencies = [ - "find-msvc-tools", - "jobserver", - "libc", - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "cfg_aliases" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" - -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - -[[package]] -name = "codespan-reporting" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" -dependencies = [ - "termcolor", - "unicode-width", -] - -[[package]] -name = "colorchoice" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" - -[[package]] -name = "com" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e17887fd17353b65b1b2ef1c526c83e26cd72e74f598a8dc1bee13a48f3d9f6" -dependencies = [ - "com_macros", -] - -[[package]] -name = "com_macros" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d375883580a668c7481ea6631fc1a8863e33cc335bf56bfad8d7e6d4b04b13a5" -dependencies = [ - "com_macros_support", - "proc-macro2", - "syn 1.0.109", -] - -[[package]] -name = "com_macros_support" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad899a1087a9296d5644792d7cb72b8e34c1bec8e7d4fbc002230169a6e8710c" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "combine" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = [ - "bytes", - "memchr", -] - -[[package]] -name = "concurrent-queue" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "core-graphics" -version = "0.23.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" -dependencies = [ - "bitflags 1.3.2", - "core-foundation", - "core-graphics-types", - "foreign-types", - "libc", -] - -[[package]] -name = "core-graphics-types" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" -dependencies = [ - "bitflags 1.3.2", - "core-foundation", - "libc", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "cursor-icon" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" - -[[package]] -name = "d3d12" -version = "22.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdbd1f579714e3c809ebd822c81ef148b1ceaeb3d535352afc73fd0c4c6a0017" -dependencies = [ - "bitflags 2.11.1", - "libloading", - "winapi", -] - -[[package]] -name = "dispatch" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" - -[[package]] -name = "dlib" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" -dependencies = [ - "libloading", -] - -[[package]] -name = "document-features" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" -dependencies = [ - "litrs", -] - -[[package]] -name = "dpi" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" - -[[package]] -name = "env_filter" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef" -dependencies = [ - "log", - "regex", -] - -[[package]] -name = "env_logger" -version = "0.11.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" -dependencies = [ - "anstream", - "anstyle", - "env_filter", - "jiff", - "log", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "find-msvc-tools" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" - -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[package]] -name = "foreign-types" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" -dependencies = [ - "foreign-types-macros", - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-macros" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "foreign-types-shared" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" - -[[package]] -name = "futures-core" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" - -[[package]] -name = "futures-task" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" - -[[package]] -name = "futures-util" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" -dependencies = [ - "futures-core", - "futures-task", - "pin-project-lite", - "slab", -] - -[[package]] -name = "gethostname" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" -dependencies = [ - "rustix 1.1.4", - "windows-link", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasip2", -] - -[[package]] -name = "gl_generator" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" -dependencies = [ - "khronos_api", - "log", - "xml-rs", -] - -[[package]] -name = "glow" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd348e04c43b32574f2de31c8bb397d96c9fcfa1371bd4ca6d8bdc464ab121b1" -dependencies = [ - "js-sys", - "slotmap", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "glutin_wgl_sys" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e" -dependencies = [ - "gl_generator", -] - -[[package]] -name = "gpu-alloc" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" -dependencies = [ - "bitflags 2.11.1", - "gpu-alloc-types", -] - -[[package]] -name = "gpu-alloc-types" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" -dependencies = [ - "bitflags 2.11.1", -] - -[[package]] -name = "gpu-allocator" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdd4240fc91d3433d5e5b0fc5b67672d771850dc19bbee03c1381e19322803d7" -dependencies = [ - "log", - "presser", - "thiserror 1.0.69", - "winapi", - "windows", -] - -[[package]] -name = "gpu-descriptor" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" -dependencies = [ - "bitflags 2.11.1", - "gpu-descriptor-types", - "hashbrown 0.15.5", -] - -[[package]] -name = "gpu-descriptor-types" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" -dependencies = [ - "bitflags 2.11.1", -] - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash", -] - -[[package]] -name = "hashbrown" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" - -[[package]] -name = "hassle-rs" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af2a7e73e1f34c48da31fb668a907f250794837e08faa144fd24f0b8b741e890" -dependencies = [ - "bitflags 2.11.1", - "com", - "libc", - "libloading", - "thiserror 1.0.69", - "widestring", - "winapi", -] - -[[package]] -name = "hermit-abi" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" - -[[package]] -name = "hexf-parse" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" - -[[package]] -name = "indexmap" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" -dependencies = [ - "equivalent", - "hashbrown 0.17.0", -] - -[[package]] -name = "is_terminal_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" - -[[package]] -name = "jiff" -version = "0.2.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f00b5dbd620d61dfdcb6007c9c1f6054ebd75319f163d886a9055cec1155073d" -dependencies = [ - "jiff-static", - "log", - "portable-atomic", - "portable-atomic-util", - "serde_core", -] - -[[package]] -name = "jiff-static" -version = "0.2.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e000de030ff8022ea1da3f466fbb0f3a809f5e51ed31f6dd931c35181ad8e6d7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "jni" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" -dependencies = [ - "cfg-if", - "combine", - "jni-macros", - "jni-sys 0.4.1", - "log", - "simd_cesu8", - "thiserror 2.0.18", - "walkdir", - "windows-link", -] - -[[package]] -name = "jni-macros" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" -dependencies = [ - "proc-macro2", - "quote", - "rustc_version", - "simd_cesu8", - "syn 2.0.117", -] - -[[package]] -name = "jni-sys" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" -dependencies = [ - "jni-sys 0.4.1", -] - -[[package]] -name = "jni-sys" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" -dependencies = [ - "jni-sys-macros", -] - -[[package]] -name = "jni-sys-macros" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" -dependencies = [ - "quote", - "syn 2.0.117", -] - -[[package]] -name = "jobserver" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" -dependencies = [ - "getrandom", - "libc", -] - -[[package]] -name = "js-sys" -version = "0.3.95" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" -dependencies = [ - "cfg-if", - "futures-util", - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "khronos-egl" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" -dependencies = [ - "libc", - "libloading", - "pkg-config", -] - -[[package]] -name = "khronos_api" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" - -[[package]] -name = "libc" -version = "0.2.186" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" - -[[package]] -name = "libloading" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" -dependencies = [ - "cfg-if", - "windows-link", -] - -[[package]] -name = "libredox" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" -dependencies = [ - "bitflags 2.11.1", - "libc", - "plain", - "redox_syscall 0.7.4", -] - -[[package]] -name = "linux-raw-sys" -version = "0.4.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" - -[[package]] -name = "linux-raw-sys" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" - -[[package]] -name = "litrs" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" - -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" - -[[package]] -name = "malloc_buf" -version = "0.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" -dependencies = [ - "libc", -] - -[[package]] -name = "memchr" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" - -[[package]] -name = "metal" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ecfd3296f8c56b7c1f6fbac3c71cefa9d78ce009850c45000015f206dc7fa21" -dependencies = [ - "bitflags 2.11.1", - "block", - "core-graphics-types", - "foreign-types", - "log", - "objc", - "paste", -] - -[[package]] -name = "naga" -version = "22.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bd5a652b6faf21496f2cfd88fc49989c8db0825d1f6746b1a71a6ede24a63ad" -dependencies = [ - "arrayvec", - "bit-set", - "bitflags 2.11.1", - "cfg_aliases 0.1.1", - "codespan-reporting", - "hexf-parse", - "indexmap", - "log", - "rustc-hash", - "spirv", - "termcolor", - "thiserror 1.0.69", - "unicode-xid", -] - -[[package]] -name = "ndk" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" -dependencies = [ - "bitflags 2.11.1", - "jni-sys 0.3.1", - "log", - "ndk-sys 0.6.0+11769913", - "num_enum", - "raw-window-handle", - "thiserror 1.0.69", -] - -[[package]] -name = "ndk-context" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" - -[[package]] -name = "ndk-sys" -version = "0.5.0+25.2.9519653" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" -dependencies = [ - "jni-sys 0.3.1", -] - -[[package]] -name = "ndk-sys" -version = "0.6.0+11769913" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" -dependencies = [ - "jni-sys 0.3.1", -] - -[[package]] -name = "num_enum" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" -dependencies = [ - "num_enum_derive", - "rustversion", -] - -[[package]] -name = "num_enum_derive" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "objc" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" -dependencies = [ - "malloc_buf", -] - -[[package]] -name = "objc-sys" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" - -[[package]] -name = "objc2" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" -dependencies = [ - "objc-sys", - "objc2-encode", -] - -[[package]] -name = "objc2-app-kit" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" -dependencies = [ - "bitflags 2.11.1", - "block2", - "libc", - "objc2", - "objc2-core-data", - "objc2-core-image", - "objc2-foundation", - "objc2-quartz-core", -] - -[[package]] -name = "objc2-cloud-kit" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" -dependencies = [ - "bitflags 2.11.1", - "block2", - "objc2", - "objc2-core-location", - "objc2-foundation", -] - -[[package]] -name = "objc2-contacts" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" -dependencies = [ - "block2", - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-data" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" -dependencies = [ - "bitflags 2.11.1", - "block2", - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-image" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" -dependencies = [ - "block2", - "objc2", - "objc2-foundation", - "objc2-metal", -] - -[[package]] -name = "objc2-core-location" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" -dependencies = [ - "block2", - "objc2", - "objc2-contacts", - "objc2-foundation", -] - -[[package]] -name = "objc2-encode" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" - -[[package]] -name = "objc2-foundation" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" -dependencies = [ - "bitflags 2.11.1", - "block2", - "dispatch", - "libc", - "objc2", -] - -[[package]] -name = "objc2-link-presentation" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" -dependencies = [ - "block2", - "objc2", - "objc2-app-kit", - "objc2-foundation", -] - -[[package]] -name = "objc2-metal" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" -dependencies = [ - "bitflags 2.11.1", - "block2", - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-quartz-core" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" -dependencies = [ - "bitflags 2.11.1", - "block2", - "objc2", - "objc2-foundation", - "objc2-metal", -] - -[[package]] -name = "objc2-symbols" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" -dependencies = [ - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-ui-kit" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" -dependencies = [ - "bitflags 2.11.1", - "block2", - "objc2", - "objc2-cloud-kit", - "objc2-core-data", - "objc2-core-image", - "objc2-core-location", - "objc2-foundation", - "objc2-link-presentation", - "objc2-quartz-core", - "objc2-symbols", - "objc2-uniform-type-identifiers", - "objc2-user-notifications", -] - -[[package]] -name = "objc2-uniform-type-identifiers" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" -dependencies = [ - "block2", - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-user-notifications" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" -dependencies = [ - "bitflags 2.11.1", - "block2", - "objc2", - "objc2-core-location", - "objc2-foundation", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "once_cell_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" - -[[package]] -name = "orbclient" -version = "0.3.53" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12c6933ddbbd16539a7672e697bb8d41ac3a4e99ac43eeb40c07236bd7fcb2dd" -dependencies = [ - "libc", - "libredox", -] - -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall 0.5.18", - "smallvec", - "windows-link", -] - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "pin-project" -version = "1.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] -name = "pkg-config" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" - -[[package]] -name = "plain" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" - -[[package]] -name = "polling" -version = "3.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" -dependencies = [ - "cfg-if", - "concurrent-queue", - "hermit-abi", - "pin-project-lite", - "rustix 1.1.4", - "windows-sys 0.61.2", -] - -[[package]] -name = "pollster" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22686f4785f02a4fcc856d3b3bb19bf6c8160d103f7a99cc258bddd0251dc7f2" - -[[package]] -name = "portable-atomic" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" - -[[package]] -name = "portable-atomic-util" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" -dependencies = [ - "portable-atomic", -] - -[[package]] -name = "presser" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" - -[[package]] -name = "proc-macro-crate" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" -dependencies = [ - "toml_edit", -] - -[[package]] -name = "proc-macro2" -version = "1.0.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "profiling" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" - -[[package]] -name = "quote" -version = "1.0.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "range-alloc" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca45419789ae5a7899559e9512e58ca889e41f04f1f2445e9f4b290ceccd1d08" - -[[package]] -name = "raw-window-handle" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" - -[[package]] -name = "redox_syscall" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags 2.11.1", -] - -[[package]] -name = "redox_syscall" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a" -dependencies = [ - "bitflags 2.11.1", -] - -[[package]] -name = "regex" -version = "1.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" - -[[package]] -name = "renderdoc-sys" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" - -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustix" -version = "0.38.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" -dependencies = [ - "bitflags 2.11.1", - "errno", - "libc", - "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", -] - -[[package]] -name = "rustix" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" -dependencies = [ - "bitflags 2.11.1", - "errno", - "libc", - "linux-raw-sys 0.12.1", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "semver" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "simd_cesu8" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" -dependencies = [ - "rustc_version", - "simdutf8", -] - -[[package]] -name = "simdutf8" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" - -[[package]] -name = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - -[[package]] -name = "slotmap" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" -dependencies = [ - "version_check", -] - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "smol_str" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" -dependencies = [ - "serde", -] - -[[package]] -name = "spirv" -version = "0.3.0+sdk-1.3.268.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" -dependencies = [ - "bitflags 2.11.1", -] - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.117" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "termcolor" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" -dependencies = [ - "thiserror-impl 2.0.18", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "toml_datetime" -version = "1.1.1+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_edit" -version = "0.25.11+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" -dependencies = [ - "indexmap", - "toml_datetime", - "toml_parser", - "winnow", -] - -[[package]] -name = "toml_parser" -version = "1.1.2+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" -dependencies = [ - "winnow", -] - -[[package]] -name = "topolei-canvas" -version = "0.1.0" -dependencies = [ - "bytemuck", - "cc", - "env_logger", - "log", - "pollster", - "raw-window-handle", - "wgpu", - "winit", -] - -[[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "pin-project-lite", - "tracing-core", -] - -[[package]] -name = "tracing-core" -version = "0.1.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "unicode-segmentation" -version = "1.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" - -[[package]] -name = "unicode-width" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" - -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - -[[package]] -name = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "wasip2" -version = "1.0.3+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.118" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.68" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.118" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.118" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn 2.0.117", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.118" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "web-sys" -version = "0.3.95" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "wgpu" -version = "22.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d1c4ba43f80542cf63a0a6ed3134629ae73e8ab51e4b765a67f3aa062eb433" -dependencies = [ - "arrayvec", - "cfg_aliases 0.1.1", - "document-features", - "js-sys", - "log", - "naga", - "parking_lot", - "profiling", - "raw-window-handle", - "smallvec", - "static_assertions", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "wgpu-core", - "wgpu-hal", - "wgpu-types", -] - -[[package]] -name = "wgpu-core" -version = "22.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0348c840d1051b8e86c3bcd31206080c5e71e5933dabd79be1ce732b0b2f089a" -dependencies = [ - "arrayvec", - "bit-vec", - "bitflags 2.11.1", - "cfg_aliases 0.1.1", - "document-features", - "indexmap", - "log", - "naga", - "once_cell", - "parking_lot", - "profiling", - "raw-window-handle", - "rustc-hash", - "smallvec", - "thiserror 1.0.69", - "wgpu-hal", - "wgpu-types", -] - -[[package]] -name = "wgpu-hal" -version = "22.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6bbf4b4de8b2a83c0401d9e5ae0080a2792055f25859a02bf9be97952bbed4f" -dependencies = [ - "android_system_properties", - "arrayvec", - "ash", - "bit-set", - "bitflags 2.11.1", - "block", - "cfg_aliases 0.1.1", - "core-graphics-types", - "d3d12", - "glow", - "glutin_wgl_sys", - "gpu-alloc", - "gpu-allocator", - "gpu-descriptor", - "hassle-rs", - "js-sys", - "khronos-egl", - "libc", - "libloading", - "log", - "metal", - "naga", - "ndk-sys 0.5.0+25.2.9519653", - "objc", - "once_cell", - "parking_lot", - "profiling", - "range-alloc", - "raw-window-handle", - "renderdoc-sys", - "rustc-hash", - "smallvec", - "thiserror 1.0.69", - "wasm-bindgen", - "web-sys", - "wgpu-types", - "winapi", -] - -[[package]] -name = "wgpu-types" -version = "22.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc9d91f0e2c4b51434dfa6db77846f2793149d8e73f800fa2e41f52b8eac3c5d" -dependencies = [ - "bitflags 2.11.1", - "js-sys", - "web-sys", -] - -[[package]] -name = "widestring" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" -dependencies = [ - "windows-core", - "windows-targets", -] - -[[package]] -name = "windows-core" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "winit" -version = "0.30.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6755fa58a9f8350bd1e472d4c3fcc25f824ec358933bba33306d0b63df5978d" -dependencies = [ - "android-activity", - "atomic-waker", - "bitflags 2.11.1", - "block2", - "bytemuck", - "calloop", - "cfg_aliases 0.2.1", - "concurrent-queue", - "core-foundation", - "core-graphics", - "cursor-icon", - "dpi", - "js-sys", - "libc", - "ndk", - "objc2", - "objc2-app-kit", - "objc2-foundation", - "objc2-ui-kit", - "orbclient", - "percent-encoding", - "pin-project", - "raw-window-handle", - "redox_syscall 0.4.1", - "rustix 0.38.44", - "smol_str", - "tracing", - "unicode-segmentation", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "web-time", - "windows-sys 0.52.0", - "x11-dl", - "x11rb", - "xkbcommon-dl", -] - -[[package]] -name = "winnow" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" -dependencies = [ - "memchr", -] - -[[package]] -name = "wit-bindgen" -version = "0.57.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" - -[[package]] -name = "x11-dl" -version = "2.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" -dependencies = [ - "libc", - "once_cell", - "pkg-config", -] - -[[package]] -name = "x11rb" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" -dependencies = [ - "as-raw-xcb-connection", - "gethostname", - "libc", - "libloading", - "once_cell", - "rustix 1.1.4", - "x11rb-protocol", -] - -[[package]] -name = "x11rb-protocol" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" - -[[package]] -name = "xkbcommon-dl" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" -dependencies = [ - "bitflags 2.11.1", - "dlib", - "log", - "once_cell", - "xkeysym", -] - -[[package]] -name = "xkeysym" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" - -[[package]] -name = "xml-rs" -version = "0.8.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" diff --git a/native/canvas-rs/Cargo.toml b/native/canvas-rs/Cargo.toml deleted file mode 100644 index 89dabe3..0000000 --- a/native/canvas-rs/Cargo.toml +++ /dev/null @@ -1,39 +0,0 @@ -[package] -name = "topolei-canvas" -version = "0.1.0" -edition = "2021" -rust-version = "1.76" -description = "wgpu + winit canvas for topolei. Builds fragment shaders as `naga::Module` directly from Lean `EMLPath` inductives — no GLSL string intermediary on the path-render side." -license = "MIT" -publish = false - -[lib] -name = "topolei_canvas" -crate-type = ["staticlib"] - -[dependencies] -# `naga-ir` enables `wgpu::ShaderSource::Naga(...)` for the fragment -# stage. Default features cover WGSL (used for the small -# fullscreen-triangle vertex shader) + Vulkan / Metal / DX12 backends. -wgpu = { version = "22.1", features = ["naga-ir"] } -# X11-only winit — disables Wayland backend to sidestep dlopen(libwayland) -# TLS symbols that collide with Lean's static linker on NixOS. -winit = { version = "0.30", default-features = false, features = ["x11", "rwh_06"] } -pollster = "0.3" -bytemuck = { version = "1.15", features = ["derive"] } -log = "0.4" -env_logger = "0.11" -raw-window-handle = "0.6" - -[build-dependencies] -cc = "1.0" - -[profile.release] -opt-level = 3 -lto = true -codegen-units = 1 -panic = "abort" - -[profile.dev] -opt-level = 0 -panic = "abort" diff --git a/native/canvas-rs/ProbeTest.lean b/native/canvas-rs/ProbeTest.lean deleted file mode 100644 index e69de29..0000000 diff --git a/native/canvas-rs/build.rs b/native/canvas-rs/build.rs deleted file mode 100644 index 8666fa4..0000000 --- a/native/canvas-rs/build.rs +++ /dev/null @@ -1,26 +0,0 @@ -//! Build script: compile shim.c exposing Lean's inline runtime helpers. - -fn main() { - let target = std::env::var("TARGET").unwrap_or_default(); - if target.starts_with("wasm32") { - return; - } - - let lean_include = std::env::var("LEAN_INCLUDE").unwrap_or_else(|_| { - let prefix = std::process::Command::new("lean") - .arg("--print-prefix") - .output() - .expect("failed to run `lean --print-prefix`; set LEAN_INCLUDE instead"); - let prefix = String::from_utf8(prefix.stdout).unwrap(); - format!("{}/include", prefix.trim()) - }); - - cc::Build::new() - .file("shim.c") - .include(&lean_include) - .flag("-Wno-unused-parameter") - .compile("topolei_canvas_shim"); - - println!("cargo:rerun-if-changed=shim.c"); - println!("cargo:rerun-if-env-changed=LEAN_INCLUDE"); -} diff --git a/native/canvas-rs/shim.c b/native/canvas-rs/shim.c deleted file mode 100644 index 04e8923..0000000 --- a/native/canvas-rs/shim.c +++ /dev/null @@ -1,64 +0,0 @@ -/* shim.c — expose Lean's static inline runtime helpers used by the - * canvas crate's FFI (lean_string_cstr, lean_io_result_mk_ok, etc.). - * - * Mirror of `native/cubical/shim.c` + `native/render/shim.c`. - * Names prefixed `topolei_canvas_shim_` to avoid collisions at link. - */ - -#include -#include -#include - -const char* topolei_canvas_shim_string_cstr(b_lean_obj_arg s) { - return lean_string_cstr(s); -} - -lean_obj_res topolei_canvas_shim_io_ok_unit(void) { - return lean_io_result_mk_ok(lean_box(0)); -} - -/* Build a Lean `RGBA` structure (tag 0) and wrap in `IO.Result.ok`. - * - * Representation: Lean compiles a structure whose fields are all - * `Float` (f64) using **inline unboxed scalar storage** — the four - * doubles are packed directly into the ctor's scalar area as 4 × 8 = - * 32 bytes. `lean_alloc_ctor(tag, num_objs=0, scalar_bytes=32)` + - * writes via `lean_ctor_scalar_cptr` match that layout. - * - * An earlier version of this shim used `lean_alloc_ctor(0, 4, 0)` - * with `lean_box_float` per field — that produces a ctor with 4 - * boxed Float objects, which does not match the compiled layout. - * Lean-side reads then saw uninitialised scalar memory (all zeros) - * regardless of what the GPU rendered. If the Lean `RGBA` structure - * ever gains a non-Float field, this shim must be revisited. - */ -lean_obj_res topolei_canvas_shim_io_ok_rgba(double r, double g, double b, double a) { - lean_object* ctor = lean_alloc_ctor(0, 0, 32); - uint8_t* scalars = lean_ctor_scalar_cptr(ctor); - memcpy(scalars + 0, &r, 8); - memcpy(scalars + 8, &g, 8); - memcpy(scalars + 16, &b, 8); - memcpy(scalars + 24, &a, 8); - return lean_io_result_mk_ok(ctor); -} - -/* Inductive-walk helpers — expose Lean's static-inline object accessors - * so Rust can traverse inductive data (EMLExpr, EMLPath, etc.) without - * a separate `lean-sys` dependency. Mirror of the same helpers in - * `native/cubical/shim.c`, scoped under the `topolei_canvas_shim_` - * prefix to avoid link collisions when a single binary pulls in both - * crates. - */ -uint32_t topolei_canvas_shim_obj_tag(b_lean_obj_arg o) { - return (uint32_t)lean_obj_tag(o); -} - -b_lean_obj_res topolei_canvas_shim_ctor_get(b_lean_obj_arg o, uint32_t idx) { - return lean_ctor_get(o, idx); -} - -/* Wrap a C NUL-terminated string in an `IO String` result object. */ -lean_obj_res topolei_canvas_shim_mk_string_io(const char* s) { - lean_object* str = lean_mk_string(s); - return lean_io_result_mk_ok(str); -} diff --git a/native/canvas-rs/src/emit_naga.rs b/native/canvas-rs/src/emit_naga.rs deleted file mode 100644 index 28770d7..0000000 --- a/native/canvas-rs/src/emit_naga.rs +++ /dev/null @@ -1,435 +0,0 @@ -//! # emit_naga — direct naga IR construction from `EMLPath` -//! -//! Replaces the GLSL-text intermediary between Rust `EMLPath` and -//! `naga::Module`. The naga-glsl frontend (~10⁴ LOC) drops out of the -//! trust surface; what the SPIR-V backend writes is exactly the module -//! we built here. -//! -//! See `NAGA_IR_PLAN.md` (top-level) for staging. This file lands in -//! seven commits; consult §5 of the plan for which behaviour each stage -//! locks in. -//! -//! ## Greyscale projection (post-cos-strip) -//! -//! Fragment writes `vec4(v, v, v, 1.0)` where `v = body`. Direct -//! identity mapping into the framebuffer's three channels. -//! -//! An earlier version applied a cosine cycle -//! (`r = 0.5 + 0.5*cos(2π·v + φ)`) here to make any value visible. -//! That cycle has period 1 in `v`, so two fibers of a 1-cell that -//! differ by 1 (e.g. `plotTransp.at0` vs `plotTransp.at1`) rendered -//! to pixel-identical output — visually destroying the transport's -//! content. The cycle is removed; the spec was updated in tandem -//! (`Topolei.GPU.Spec::EMLPath.toColor`). -//! -//! ## Module shape -//! -//! - `entry_points[0]`: fragment, `name = "main"`. -//! - `arguments`: `uv: vec2 @location(0)` — kept on the entry -//! point even when unused, because Stages 3+ need it and reusing the -//! same shape across stages keeps the bind-group layout stable. -//! - `result`: `vec4 @location(0)`. -//! - `body`: a single `Return` of a Compose expression. -//! -//! ## Pitfalls handled (NAGA_IR_PLAN.md §6) -//! -//! 1. **Expression-before-use** — composition uses handles already in -//! the arena. Order of `append` calls matters; we read each handle -//! only after the call that produced it. -//! 2. **`Statement::Emit`** — non-literal expressions (here just the -//! `Compose`) sit inside an `Emit` range covering them. -//! 3. **`UniqueArena` for types** — types go in `module.types` via -//! `insert(_, Span::UNDEFINED)`; identical types dedupe. -//! 8. **Validation capabilities** — `Capabilities::empty()` is enough -//! for the probe shader. -//! -//! Pitfalls 4–7 (struct layout, `ResourceBinding`, `Binding` shape, -//! NDC y-flip) become live in Stages 2–3. - -use wgpu::naga::{ - self, AddressSpace, BinaryOperator, Binding, EntryPoint, Expression, Function, - FunctionArgument, FunctionResult, GlobalVariable, Handle, Literal, MathFunction, Module, - ResourceBinding, Scalar, ScalarKind, ShaderStage, Span, Statement, StructMember, Type, - TypeInner, VectorSize, -}; - -use crate::eml::{EMLExpr, EMLPath}; - -// ── Type interning helpers ───────────────────────────────────────────────── -// -// `module.types` is a `UniqueArena`: identical `TypeInner` values -// produce identical handles. Helpers below keep the call site short. - -#[inline] -fn ty_f32(module: &mut Module) -> Handle { - module.types.insert( - Type { - name: None, - inner: TypeInner::Scalar(Scalar { - kind: ScalarKind::Float, - width: 4, - }), - }, - Span::UNDEFINED, - ) -} - -#[inline] -fn ty_vec2_f32(module: &mut Module) -> Handle { - module.types.insert( - Type { - name: None, - inner: TypeInner::Vector { - size: VectorSize::Bi, - scalar: Scalar { - kind: ScalarKind::Float, - width: 4, - }, - }, - }, - Span::UNDEFINED, - ) -} - -#[inline] -fn ty_vec4_f32(module: &mut Module) -> Handle { - module.types.insert( - Type { - name: None, - inner: TypeInner::Vector { - size: VectorSize::Quad, - scalar: Scalar { - kind: ScalarKind::Float, - width: 4, - }, - }, - }, - Span::UNDEFINED, - ) -} - -/// Pre-built type handles, deduped through `module.types`. -/// -/// `uniforms` matches `Uniforms` in `lib.rs` and `FrameUniforms` in -/// `Topolei.GPU.Spec`: `{ time: f32 @ 0, path_param: f32 @ 4, -/// resolution: vec2 @ 8 }` with span 16. Validation rejects -/// mismatched offsets / spans, so this is single-source-of-truth on -/// the IR side. -struct ProbeTypes { - #[allow(dead_code)] // Stage 4+ - f32: Handle, - vec2_f32: Handle, - vec4_f32: Handle, - uniforms: Handle, -} - -impl ProbeTypes { - fn build(module: &mut Module) -> Self { - let f32_h = ty_f32(module); - let vec2_f32_h = ty_vec2_f32(module); - let vec4_f32_h = ty_vec4_f32(module); - let uniforms_h = module.types.insert( - Type { - name: Some("Uniforms".to_string()), - inner: TypeInner::Struct { - members: vec![ - StructMember { - name: Some("time".to_string()), - ty: f32_h, - binding: None, - offset: 0, - }, - StructMember { - name: Some("path_param".to_string()), - ty: f32_h, - binding: None, - offset: 4, - }, - StructMember { - name: Some("resolution".to_string()), - ty: vec2_f32_h, - binding: None, - offset: 8, - }, - ], - span: 16, - }, - }, - Span::UNDEFINED, - ); - Self { - f32: f32_h, - vec2_f32: vec2_f32_h, - vec4_f32: vec4_f32_h, - uniforms: uniforms_h, - } - } -} - -/// Globals used by the probe shader. Each global is a `naga::Module` -/// global variable; the entry-point body refers to them via -/// `Expression::GlobalVariable` (which produces a *pointer* to the -/// value, except for `AddressSpace::Handle` which produces the value -/// directly). -struct ProbeGlobals { - uniforms_buf: Handle, -} - -impl ProbeGlobals { - fn build(module: &mut Module, types: &ProbeTypes) -> Self { - let uniforms_buf = module.global_variables.append( - GlobalVariable { - name: Some("uniforms".to_string()), - space: AddressSpace::Uniform, - binding: Some(ResourceBinding { - group: UNIFORM_GROUP, - binding: UNIFORM_BINDING, - }), - ty: types.uniforms, - init: None, - }, - Span::UNDEFINED, - ); - Self { uniforms_buf } - } -} - -const FRAGMENT_LOCATION: u32 = 0; -const UNIFORM_GROUP: u32 = 0; -const UNIFORM_BINDING: u32 = 0; -/// `Uniforms.path_param` field index. Must match the Rust -/// `Uniforms` struct layout (`time, path_param, resolution`). -#[allow(dead_code)] // Stage 4+ -const UNIFORMS_FIELD_PATH_PARAM: u32 = 1; - -// ── build_probe_module — top-level orchestrator ──────────────────────────── - -/// Build a fully-validated fragment-shader `naga::Module` whose pixel -/// output (eventually) equals `EMLPath.toColor path p u`. -/// -/// At Stage 1 the path is ignored and every pixel is `(1, 0, 0, 1)`. -/// Pixel probes therefore fail by design; the module is still expected -/// to validate and round-trip through wgpu's SPIR-V writer. -pub fn build_probe_module(path: &EMLPath) -> Module { - let mut module = Module::default(); - let types = ProbeTypes::build(&mut module); - let globals = ProbeGlobals::build(&mut module, &types); - - let entry = build_main_fn(path, &types, &globals); - module.entry_points.push(entry); - - module -} - -// ── build_main_fn — fragment entry point ─────────────────────────────────── - -fn build_main_fn(path: &EMLPath, types: &ProbeTypes, globals: &ProbeGlobals) -> EntryPoint { - let mut function = Function::default(); - function.name = Some("main".to_string()); - - // Argument: uv: vec2 @location(0). Naga's fragment-stage - // pattern keeps varyings on the function arguments rather than on - // top-level globals; this is the idiom the GLSL frontend produces - // and what the SPIR-V backend expects. - function.arguments.push(FunctionArgument { - name: Some("uv".to_string()), - ty: types.vec2_f32, - binding: Some(Binding::Location { - location: FRAGMENT_LOCATION, - second_blend_source: false, - // Match the WGSL vertex shader's default interpolation - // (`perspective`) + sampling (`center`). Mismatching the - // sampling causes wgpu to reject the pipeline at - // `create_render_pipeline` time with "Input sampling - // doesn't match provided Some(Center)". - interpolation: Some(naga::Interpolation::Perspective), - sampling: Some(naga::Sampling::Center), - }), - }); - - // Result: vec4 @location(0). - function.result = Some(FunctionResult { - ty: types.vec4_f32, - binding: Some(Binding::Location { - location: FRAGMENT_LOCATION, - second_blend_source: false, - interpolation: None, - sampling: None, - }), - }); - - // Body — Stage 4: walk the EMLExpr tree, build `v = body`, - // emit `vec4(v, v, v, 1.0)`. - // - // Expression order rules (NAGA_IR_PLAN.md §6 #1, #2): - // - Every handle must be appended before any expression that - // references it. - // - `Literal`, `Constant`, `ZeroValue`, `FunctionArgument`, - // `GlobalVariable`, `LocalVariable` are *implicitly* evaluated - // and must NOT sit inside an `Emit` range. Naga rejects them - // with "Expression [n] can't be introduced — it's already in - // scope". - // - All other expressions need to be covered by an `Emit` whose - // range is `expressions.range_from(len_before_first_emittable)`. - // - // The strategy here: append every implicit (FunctionArgument, - // GlobalVariable, all Literals) up front, then snapshot the arena - // length, then run the rest of the body (AccessIndex, Load, - // Math, Binary, Compose) inside one continuous Emit range. The - // `emit_emlexpr` walker only ever appends emittable expressions — - // it reuses the pre-allocated `one` / `zero` literals when the - // tree calls for them. - let uniforms_ptr = function.expressions.append( - Expression::GlobalVariable(globals.uniforms_buf), - Span::UNDEFINED, - ); - let uv_arg = function - .expressions - .append(Expression::FunctionArgument(0), Span::UNDEFINED); - let zero = function - .expressions - .append(Expression::Literal(Literal::F32(0.0)), Span::UNDEFINED); - let one = function - .expressions - .append(Expression::Literal(Literal::F32(1.0)), Span::UNDEFINED); - - let pre_chain = function.expressions.len(); - let px = function.expressions.append( - Expression::AccessIndex { - base: uv_arg, - index: 0, - }, - Span::UNDEFINED, - ); - let py = function.expressions.append( - Expression::AccessIndex { - base: uv_arg, - index: 1, - }, - Span::UNDEFINED, - ); - let path_param_ptr = function.expressions.append( - Expression::AccessIndex { - base: uniforms_ptr, - index: UNIFORMS_FIELD_PATH_PARAM, - }, - Span::UNDEFINED, - ); - let dim_value = function.expressions.append( - Expression::Load { - pointer: path_param_ptr, - }, - Span::UNDEFINED, - ); - - let env = EmitEnv { - px, - py, - dim_name: &path.dim_name, - dim_value, - one, - zero, - }; - let v = emit_emlexpr(&path.body, &mut function, &env); - - // Greyscale projection: write `v` to all three channels. No cos - // cycle, no normalization — see the file-level comment. - let composed = function.expressions.append( - Expression::Compose { - ty: types.vec4_f32, - components: vec![v, v, v, one], - }, - Span::UNDEFINED, - ); - - function.body.push( - Statement::Emit(function.expressions.range_from(pre_chain)), - Span::UNDEFINED, - ); - function.body.push( - Statement::Return { - value: Some(composed), - }, - Span::UNDEFINED, - ); - - EntryPoint { - name: "main".to_string(), - stage: ShaderStage::Fragment, - early_depth_test: None, - workgroup_size: [0, 0, 0], - function, - } -} - -// ── EMLExpr → naga Expression handle ─────────────────────────────────────── - -/// Bindings for the leaves of an `EMLExpr`. All four handles must be -/// already allocated in `function.expressions` *and* either implicit -/// (the literal pair) or already covered by the open `Emit` range -/// (the uv-derived px/py and the loaded dim value). -struct EmitEnv<'a> { - px: Handle, - py: Handle, - dim_name: &'a str, - dim_value: Handle, - one: Handle, - zero: Handle, -} - -/// Walk an `EMLExpr` and return the `Handle` for its -/// value. Only appends emittable expressions (`Math`, `Binary`); the -/// caller wraps the chain in a single `Statement::Emit`. -/// -/// Mirrors `Topolei.EML.EMLExpr.toGLSL` semantically (and `eml.rs` -/// `to_glsl` literally) — `Var` falls back to `0.0` when the name is -/// neither `px`/`py` nor the path's dim variable (matches -/// `shaderVar`'s fallback in `GPU/Spec.lean`). -fn emit_emlexpr(expr: &EMLExpr, function: &mut Function, env: &EmitEnv) -> Handle { - match expr { - EMLExpr::One => env.one, - EMLExpr::Var(name) => { - if name == "px" { - env.px - } else if name == "py" { - env.py - } else if name == env.dim_name { - env.dim_value - } else { - env.zero - } - } - EMLExpr::Eml(l, r) => { - let lh = emit_emlexpr(l, function, env); - let rh = emit_emlexpr(r, function, env); - let exp_l = function.expressions.append( - Expression::Math { - fun: MathFunction::Exp, - arg: lh, - arg1: None, - arg2: None, - arg3: None, - }, - Span::UNDEFINED, - ); - let log_r = function.expressions.append( - Expression::Math { - fun: MathFunction::Log, - arg: rh, - arg1: None, - arg2: None, - arg3: None, - }, - Span::UNDEFINED, - ); - function.expressions.append( - Expression::Binary { - op: BinaryOperator::Subtract, - left: exp_l, - right: log_r, - }, - Span::UNDEFINED, - ) - } - } -} diff --git a/native/canvas-rs/src/eml.rs b/native/canvas-rs/src/eml.rs deleted file mode 100644 index 7353243..0000000 --- a/native/canvas-rs/src/eml.rs +++ /dev/null @@ -1,112 +0,0 @@ -//! # eml -//! -//! Rust mirror of `Topolei.EML.EMLExpr` and `Topolei.EML.Path.EMLPath`. -//! Used by the probe and (future) live-render pipelines to accept a -//! **structured** path from Lean over FFI — not a pre-emitted GLSL -//! string — and then emit GPU shader source on this side. -//! -//! Having the AST on both sides means: -//! -//! - The shader emitter lives in Rust where it runs, not in Lean -//! where it would need to produce a string that Rust then parses. -//! - Lean's `EMLPath.toFragShaderProbe` becomes a **reference -//! emitter**: the two sides are expected to produce the same -//! shader for the same path, and any divergence is a spec-vs-impl -//! bug that can be caught by a string diff. -//! - Future work (direct naga IR construction, SPIR-V emission) has -//! a typed starting point instead of a string. -//! -//! The Lean object layout this module consumes: -//! -//! ```text -//! inductive EMLExpr where -//! | one : EMLExpr -- tag 0, 0 fields -//! | var (name : String) : EMLExpr -- tag 1, 1 field -//! | eml (l r : EMLExpr) : EMLExpr -- tag 2, 2 fields -//! -//! structure EMLPath where -//! dimName : String -//! body : EMLExpr -- tag 0, 2 fields -//! ``` - -use std::ffi::{c_char, c_void, CStr}; - -// ── Shim bindings (from `native/canvas-rs/shim.c`) ────────────────────────── - -extern "C" { - fn topolei_canvas_shim_obj_tag(o: *const c_void) -> u32; - fn topolei_canvas_shim_ctor_get(o: *const c_void, idx: u32) -> *const c_void; - fn topolei_canvas_shim_string_cstr(s: *const c_void) -> *const c_char; -} - -// ── Rust EMLExpr / EMLPath ───────────────────────────────────────────────── - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum EMLExpr { - One, - Var(String), - Eml(Box, Box), -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct EMLPath { - pub dim_name: String, - pub body: EMLExpr, -} - -// ── Lean object walkers ──────────────────────────────────────────────────── - -/// Walk a Lean `EMLExpr` object and build the Rust mirror. Caller -/// must keep `obj` alive for the duration — we only read fields -/// (`ctor_tag` / `ctor_get` / `string_cstr`), never retain anything. -/// -/// # Safety -/// -/// `obj` must be a valid `lean_object*` pointing at an `EMLExpr`. On -/// an unknown tag this returns `EMLExpr::One` rather than panic — the -/// caller can detect divergence from Lean state by string-diffing the -/// emitted shader, which is a stronger check than a panic path. -pub unsafe fn emlexpr_from_lean(obj: *const c_void) -> EMLExpr { - let tag = topolei_canvas_shim_obj_tag(obj); - match tag { - 0 => EMLExpr::One, - 1 => { - let s = topolei_canvas_shim_ctor_get(obj, 0); - EMLExpr::Var(read_lean_string(s)) - } - 2 => { - let l = topolei_canvas_shim_ctor_get(obj, 0); - let r = topolei_canvas_shim_ctor_get(obj, 1); - EMLExpr::Eml(Box::new(emlexpr_from_lean(l)), Box::new(emlexpr_from_lean(r))) - } - _ => EMLExpr::One, - } -} - -/// Walk a Lean `EMLPath` object (single-ctor structure with two -/// fields). Uses [`emlexpr_from_lean`] on the body. -/// -/// # Safety -/// -/// See [`emlexpr_from_lean`]. -pub unsafe fn emlpath_from_lean(obj: *const c_void) -> EMLPath { - let dim_name_obj = topolei_canvas_shim_ctor_get(obj, 0); - let body_obj = topolei_canvas_shim_ctor_get(obj, 1); - EMLPath { - dim_name: read_lean_string(dim_name_obj), - body: emlexpr_from_lean(body_obj), - } -} - -unsafe fn read_lean_string(s: *const c_void) -> String { - let ptr = topolei_canvas_shim_string_cstr(s); - if ptr.is_null() { - String::new() - } else { - CStr::from_ptr(ptr).to_string_lossy().into_owned() - } -} - -// GLSL emission removed — the path-based probe builds a `naga::Module` -// directly via `crate::emit_naga::build_probe_module`. See -// NAGA_IR_PLAN.md. diff --git a/native/canvas-rs/src/lib.rs b/native/canvas-rs/src/lib.rs deleted file mode 100644 index e8cafbe..0000000 --- a/native/canvas-rs/src/lib.rs +++ /dev/null @@ -1,830 +0,0 @@ -//! # topolei-canvas -//! -//! wgpu + winit canvas for the topolei transport renderer. Both the -//! live render loop and the headless `render_faithful` probe build the -//! fragment shader as a `naga::Module` directly from a Lean `EMLPath` -//! inductive — no GLSL string intermediary. -//! -//! ## FFI -//! -//! - `topolei_run_path(path, width, height, title, world) -> IO Unit` -//! Open a window, animate one path with the canonical driver -//! (sine-sweep of `u_time` mapped onto `u_pathParam`). Returns -//! when the user closes the window. -//! -//! - `topolei_run_path2(pathL, pathR, width, height, title, world) -> IO Unit` -//! Side-by-side two-panel variant. Both panels share the uniform -//! buffer (same `u_time`, `u_pathParam`). -//! -//! - `topolei_canvas_render_probe_path_pixel(path, w, h, time, -//! pathParam, x, y, world) -> IO RGBA` -//! Headless one-pixel probe used by `Topolei.Render.Probe` to -//! verify `compileEMLPath_correct`. -//! -//! Each panel/probe builds its `naga::Module` via -//! `emit_naga::build_probe_module(&path)` and hands it to wgpu via -//! `ShaderSource::Naga(Cow::Owned(module))`. The vertex stage is a -//! tiny WGSL fullscreen-triangle shader; no GLSL frontend is involved. - -use std::borrow::Cow; -use std::ffi::{c_char, CStr, c_void}; -use std::sync::Arc; - -use bytemuck::{Pod, Zeroable}; -use wgpu::util::DeviceExt; -use winit::application::ApplicationHandler; -use winit::dpi::LogicalSize; -use winit::event::WindowEvent; -use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop}; -use winit::platform::x11::EventLoopBuilderExtX11; -use winit::window::{Window, WindowAttributes, WindowId}; - -mod eml; -mod emit_naga; - -// ── Uniform layout ────────────────────────────────────────────────────────── - -#[repr(C)] -#[derive(Copy, Clone, Debug, Pod, Zeroable)] -struct Uniforms { - // std140 layout: scalars are 4-byte aligned; vec2 is 8-byte aligned. - // - // Layout contract — must match the UBO declared by the naga IR - // emitter (`emit_naga::ProbeTypes::uniforms`) and the semantic - // spec in `Topolei.GPU.Spec.FrameUniforms`: - // - // offset 0: float u_time - // offset 4: float u_pathParam ← host-driven cubical path parameter - // offset 8: vec2 u_resolution (16-byte boundary is implicit) - // - // `path_param` is fixed at construction. We do NOT animate it - // host-side: a host-chosen time→pathParam function is not itself - // a transport in the cells-spec sense (it lives outside the - // cubical calculus). Each frame renders the EMLPath at exactly - // one fiber chosen by the caller. An animated rendering would - // be a 2-cell (a homotopy of 1-cells parameterised by a second - // interval) — that requires 2-cell infrastructure we don't have - // yet. - time: f32, - path_param: f32, - resolution: [f32; 2], -} - -// ── Vertex shader (WGSL fullscreen triangle) ─────────────────────────────── -// -// One large triangle covering NDC `[-1, 1]²`; gives the fragment stage -// `uv ∈ [0, 1]²` across the panel. WGSL is wgpu's first-class shader -// language and is in the default feature set; nothing depends on -// naga-glsl here. - -const VERTEX_WGSL: &str = r#" -struct VsOut { - @builtin(position) pos: vec4, - @location(0) uv: vec2, -}; - -@vertex -fn main(@builtin(vertex_index) idx: u32) -> VsOut { - let x = select(-1.0, 3.0, idx == 1u); - let y = select(-1.0, 3.0, idx == 2u); - var out: VsOut; - out.pos = vec4(x, y, 0.0, 1.0); - out.uv = vec2(x, y) * 0.5 + 0.5; - return out; -} -"#; - -// ── Pipeline construction ────────────────────────────────────────────────── -// -// One fragment shader + one render pipeline per panel, sharing the -// vertex module + uniform bind group with every other panel. The -// fragment module is always a `naga::Module` we build ourselves from -// an `EMLPath` — no GLSL. - -/// Build one naga-IR fragment-shader render pipeline against the -/// shared vertex module + pipeline layout. -fn make_panel_pipeline_from_path( - device: &wgpu::Device, - format: wgpu::TextureFormat, - vert_module: &wgpu::ShaderModule, - pipeline_layout: &wgpu::PipelineLayout, - path: &eml::EMLPath, - label: &str, -) -> wgpu::RenderPipeline { - let module = emit_naga::build_probe_module(path); - let frag_module = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some(label), - source: wgpu::ShaderSource::Naga(Cow::Owned(module)), - }); - device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some(label), - layout: Some(pipeline_layout), - vertex: wgpu::VertexState { - module: vert_module, - entry_point: "main", - buffers: &[], - compilation_options: Default::default(), - }, - fragment: Some(wgpu::FragmentState { - module: &frag_module, - entry_point: "main", - targets: &[Some(wgpu::ColorTargetState { - format, - // `blend: None` is universally valid; `Some(REPLACE)` - // fails validation on non-blendable formats like - // `Rgba32Float` used by the offscreen probe. - blend: None, - write_mask: wgpu::ColorWrites::ALL, - })], - compilation_options: Default::default(), - }), - primitive: wgpu::PrimitiveState { - topology: wgpu::PrimitiveTopology::TriangleList, - ..Default::default() - }, - depth_stencil: None, - multisample: wgpu::MultisampleState::default(), - multiview: None, - cache: None, - }) -} - -// ── Live-render state ────────────────────────────────────────────────────── -// -// N-panel engine. Per frame: begin one render pass (clear once), then -// for each panel set its viewport and draw its pipeline. Viewports -// partition the window width evenly left-to-right; each panel's -// fragment shader sees `uv ∈ [0, 1]²` across its own pane rather than -// across the whole window. -// -// 1 panel → one full-width viewport (topolei_run_path). -// 2 panels → side-by-side halves (topolei_run_path2). - -struct GpuState { - surface: wgpu::Surface<'static>, - device: wgpu::Device, - queue: wgpu::Queue, - config: wgpu::SurfaceConfiguration, - panels: Vec, - size: winit::dpi::PhysicalSize, -} - -/// One panel's render state: pipeline + per-panel uniform buffer + -/// bind group. Each panel binds its own `pathParam` because the -/// fiber a panel shows is a property of *that* panel, not the -/// window. Sharing one uniform across panels would conflate two -/// independent fibers. -struct PanelState { - pipeline: wgpu::RenderPipeline, - uniform_buf: wgpu::Buffer, - uniform_bg: wgpu::BindGroup, - path_param: f32, -} - -impl GpuState { - /// Initialise the GPU state for `paths.len()` panels. Requires - /// at least one path; panics otherwise (an empty window has no - /// useful meaning). `path_params[i]` selects the fiber of the - /// `i`th 1-cell to render — fixed for the lifetime of the window. - /// `path_params.len()` must equal `paths.len()`. - fn new( - window: Arc, - paths: &[eml::EMLPath], - path_params: &[f32], - ) -> Result { - assert_eq!( - paths.len(), - path_params.len(), - "GpuState::new: per-panel path_params length must match paths length" - ); - assert!(!paths.is_empty(), "GpuState::new needs at least one path"); - let size = window.inner_size(); - - let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { - backends: wgpu::Backends::VULKAN | wgpu::Backends::METAL | wgpu::Backends::DX12, - ..Default::default() - }); - - let surface = instance - .create_surface(window.clone()) - .map_err(|e| format!("create_surface: {e:?}"))?; - - let adapter = pollster::block_on(instance.request_adapter( - &wgpu::RequestAdapterOptions { - power_preference: wgpu::PowerPreference::LowPower, - compatible_surface: Some(&surface), - force_fallback_adapter: false, - }, - )) - .ok_or("no GPU adapter found")?; - - let (device, queue) = pollster::block_on(adapter.request_device( - &wgpu::DeviceDescriptor { - label: Some("topolei-canvas device"), - required_features: wgpu::Features::empty(), - required_limits: wgpu::Limits::downlevel_defaults(), - memory_hints: wgpu::MemoryHints::Performance, - }, - None, - )) - .map_err(|e| format!("request_device: {e:?}"))?; - - let surface_caps = surface.get_capabilities(&adapter); - let format = surface_caps - .formats - .iter() - .find(|f| f.is_srgb()) - .copied() - .unwrap_or(surface_caps.formats[0]); - - let config = wgpu::SurfaceConfiguration { - usage: wgpu::TextureUsages::RENDER_ATTACHMENT, - format, - width: size.width.max(1), - height: size.height.max(1), - present_mode: wgpu::PresentMode::Fifo, - desired_maximum_frame_latency: 2, - alpha_mode: surface_caps.alpha_modes[0], - view_formats: vec![], - }; - surface.configure(&device, &config); - - // Shared vertex module (fullscreen triangle, WGSL). - let vert_module = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("vertex"), - source: wgpu::ShaderSource::Wgsl(VERTEX_WGSL.into()), - }); - - // Bind-group layout (shared — every panel binds the same - // shape, just to a different buffer). - let uniform_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("uniform-bgl"), - entries: &[wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: None, - }, - count: None, - }], - }); - let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("pipeline-layout"), - bind_group_layouts: &[&uniform_bgl], - push_constant_ranges: &[], - }); - - // One panel per path — each gets its own uniform buffer with - // its own pathParam, its own bind group, and its own pipeline. - // Sharing one uniform across panels would conflate fibers - // from different 1-cells, so we don't. - let panels: Vec = paths - .iter() - .zip(path_params.iter()) - .enumerate() - .map(|(i, (path, &pp))| { - let uniform_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some(&format!("uniforms-{i}")), - contents: bytemuck::bytes_of(&Uniforms { - time: 0.0, - path_param: pp, - resolution: [size.width as f32, size.height as f32], - }), - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - }); - let uniform_bg = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some(&format!("uniform-bg-{i}")), - layout: &uniform_bgl, - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: uniform_buf.as_entire_binding(), - }], - }); - let pipeline = make_panel_pipeline_from_path( - &device, - config.format, - &vert_module, - &pipeline_layout, - path, - &format!("panel-{i}"), - ); - PanelState { pipeline, uniform_buf, uniform_bg, path_param: pp } - }) - .collect(); - - Ok(Self { - surface, - device, - queue, - config, - panels, - size, - }) - } - - fn resize(&mut self, new_size: winit::dpi::PhysicalSize) { - if new_size.width > 0 && new_size.height > 0 { - self.size = new_size; - self.config.width = new_size.width; - self.config.height = new_size.height; - self.surface.configure(&self.device, &self.config); - // Update only the resolution field on each panel; pathParam - // stays fixed because it parameterises the 1-cell, not the - // window. - for panel in &self.panels { - self.queue.write_buffer( - &panel.uniform_buf, - 0, - bytemuck::bytes_of(&Uniforms { - time: 0.0, - path_param: panel.path_param, - resolution: [self.size.width as f32, self.size.height as f32], - }), - ); - } - } - } - - fn render(&mut self) -> Result<(), wgpu::SurfaceError> { - let frame = self.surface.get_current_texture()?; - let view = frame - .texture - .create_view(&wgpu::TextureViewDescriptor::default()); - let mut encoder = self - .device - .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); - - // Compute per-panel viewports. Width is sliced evenly; the - // final panel extends to the window edge to cover any rounding. - let n = self.panels.len() as u32; - let total_w = self.config.width; - let full_h = self.config.height as f32; - let slice_w = total_w / n.max(1); - - { - let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("main"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: &view, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Clear(wgpu::Color { - r: 0.0, - g: 0.0, - b: 0.0, - a: 1.0, - }), - store: wgpu::StoreOp::Store, - }, - })], - depth_stencil_attachment: None, - timestamp_writes: None, - occlusion_query_set: None, - }); - - for (i, panel) in self.panels.iter().enumerate() { - let x = (i as u32) * slice_w; - let w = if (i as u32) + 1 == n { - total_w - x - } else { - slice_w - }; - pass.set_viewport(x as f32, 0.0, w as f32, full_h, 0.0, 1.0); - pass.set_pipeline(&panel.pipeline); - pass.set_bind_group(0, &panel.uniform_bg, &[]); - pass.draw(0..3, 0..1); - } - } - self.queue.submit(Some(encoder.finish())); - frame.present(); - Ok(()) - } -} - -// ── winit App ────────────────────────────────────────────────────────────── - -struct App { - window: Option>, - state: Option, - paths: Vec, - path_params: Vec, - title: String, - initial_size: LogicalSize, - init_error: Option, -} - -impl App { - fn new( - paths: Vec, - path_params: Vec, - title: String, - w: u32, - h: u32, - ) -> Self { - Self { - window: None, - state: None, - paths, - path_params, - title, - initial_size: LogicalSize::new(w, h), - init_error: None, - } - } -} - -impl ApplicationHandler for App { - fn resumed(&mut self, event_loop: &ActiveEventLoop) { - if self.window.is_some() { - return; - } - let attrs = WindowAttributes::default() - .with_title(&self.title) - .with_inner_size(self.initial_size); - let window = match event_loop.create_window(attrs) { - Ok(w) => Arc::new(w), - Err(e) => { - self.init_error = Some(format!("create_window: {e:?}")); - event_loop.exit(); - return; - } - }; - match GpuState::new(window.clone(), &self.paths, &self.path_params) { - Ok(s) => { - self.window = Some(window); - self.state = Some(s); - } - Err(e) => { - self.init_error = Some(e); - event_loop.exit(); - } - } - } - - fn window_event( - &mut self, - event_loop: &ActiveEventLoop, - _window_id: WindowId, - event: WindowEvent, - ) { - let Some(state) = self.state.as_mut() else { return; }; - match event { - WindowEvent::CloseRequested => event_loop.exit(), - WindowEvent::Resized(sz) => state.resize(sz), - WindowEvent::RedrawRequested => { - match state.render() { - Ok(_) => {} - Err(wgpu::SurfaceError::Lost | wgpu::SurfaceError::Outdated) => { - state.resize(state.size) - } - Err(wgpu::SurfaceError::OutOfMemory) => event_loop.exit(), - Err(e) => eprintln!("render error: {e:?}"), - } - if let Some(w) = self.window.as_ref() { - w.request_redraw(); - } - } - _ => {} - } - } -} - -fn run_path_loop( - paths: Vec, - path_params: Vec, - title: String, - w: u32, - h: u32, -) -> i32 { - if paths.is_empty() { - eprintln!("topolei-canvas: run_path_loop called with zero paths"); - return -4; - } - if paths.len() != path_params.len() { - eprintln!( - "topolei-canvas: paths.len() = {} but path_params.len() = {}", - paths.len(), - path_params.len() - ); - return -5; - } - let _ = env_logger::builder().is_test(false).try_init(); - - // `any_thread(true)` lets us create the event loop from whichever - // thread Lean called into — Lean's runtime doesn't guarantee we're - // on the "main thread" as winit normally wants on Linux. The X11 - // backend tolerates this. - let event_loop = match EventLoop::builder().with_any_thread(true).build() { - Ok(el) => el, - Err(e) => { - eprintln!("topolei-canvas: EventLoop::new failed: {e:?}"); - return -1; - } - }; - event_loop.set_control_flow(ControlFlow::Poll); - let mut app = App::new(paths, path_params, title, w, h); - if let Err(e) = event_loop.run_app(&mut app) { - eprintln!("topolei-canvas: run_app failed: {e:?}"); - return -2; - } - if let Some(err) = app.init_error.as_ref() { - eprintln!("topolei-canvas: initialization failed: {err}"); - return -3; - } - 0 -} - -// ── Offscreen probe: render-faithful empirical check ─────────────────────── -// -// Headless one-pixel render, used by `Topolei.Render.Probe`. Same -// fragment-naga construction as the live render; output read back from -// an `Rgba32Float` offscreen texture. Each call creates fresh GPU -// resources; this is deliberately slow and deliberately independent -// of the live loop. - -fn offscreen_render_pixel_path( - path: &eml::EMLPath, - width: u32, - height: u32, - time: f32, - path_param: f32, - x: u32, - y: u32, -) -> Result<[f32; 4], String> { - if x >= width || y >= height { - return Err(format!( - "pixel ({}, {}) out of bounds for ({}, {})", - x, y, width, height - )); - } - - let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { - backends: wgpu::Backends::VULKAN | wgpu::Backends::METAL | wgpu::Backends::DX12, - ..Default::default() - }); - let adapter = pollster::block_on(instance.request_adapter( - &wgpu::RequestAdapterOptions { - power_preference: wgpu::PowerPreference::default(), - compatible_surface: None, - force_fallback_adapter: false, - }, - )) - .ok_or_else(|| "no GPU adapter available for probe".to_string())?; - let adapter_info = adapter.get_info(); - eprintln!( - "topolei-probe: adapter = {} ({:?}, {:?})", - adapter_info.name, adapter_info.backend, adapter_info.device_type - ); - let (device, queue) = pollster::block_on(adapter.request_device( - &wgpu::DeviceDescriptor { - label: Some("topolei-probe"), - required_features: wgpu::Features::empty(), - required_limits: wgpu::Limits::downlevel_defaults(), - memory_hints: wgpu::MemoryHints::Performance, - }, - None, - )) - .map_err(|e| format!("request_device: {:?}", e))?; - - let format = wgpu::TextureFormat::Rgba32Float; - let texture = device.create_texture(&wgpu::TextureDescriptor { - label: Some("probe-target"), - size: wgpu::Extent3d { width, height, depth_or_array_layers: 1 }, - mip_level_count: 1, - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - format, - usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC, - view_formats: &[], - }); - let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); - - let vert_module = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("probe-vertex"), - source: wgpu::ShaderSource::Wgsl(VERTEX_WGSL.into()), - }); - - let uniform_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("probe-uniforms"), - contents: bytemuck::bytes_of(&Uniforms { - time, - path_param, - resolution: [width as f32, height as f32], - }), - usage: wgpu::BufferUsages::UNIFORM, - }); - let uniform_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("probe-uniform-bgl"), - entries: &[wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: None, - }, - count: None, - }], - }); - let uniform_bg = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("probe-uniform-bg"), - layout: &uniform_bgl, - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: uniform_buf.as_entire_binding(), - }], - }); - let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("probe-pipeline-layout"), - bind_group_layouts: &[&uniform_bgl], - push_constant_ranges: &[], - }); - let pipeline = make_panel_pipeline_from_path( - &device, format, &vert_module, &pipeline_layout, path, "probe-pipeline", - ); - - let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("probe-encoder"), - }); - { - let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("probe-pass"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: &view, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Clear(wgpu::Color { - r: 0.0, g: 0.0, b: 0.0, a: 1.0, - }), - store: wgpu::StoreOp::Store, - }, - })], - depth_stencil_attachment: None, - timestamp_writes: None, - occlusion_query_set: None, - }); - pass.set_pipeline(&pipeline); - pass.set_bind_group(0, &uniform_bg, &[]); - pass.draw(0..3, 0..1); - } - - // Copy texture → staging buffer. wgpu requires bytes_per_row to - // be a multiple of 256. - let bytes_per_pixel: u32 = 16; // Rgba32Float - let align: u32 = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT; - let bytes_per_row_unaligned = width * bytes_per_pixel; - let bytes_per_row = ((bytes_per_row_unaligned + align - 1) / align) * align; - let buffer_size = (bytes_per_row as u64) * (height as u64); - let staging = device.create_buffer(&wgpu::BufferDescriptor { - label: Some("probe-staging"), - size: buffer_size, - usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ, - mapped_at_creation: false, - }); - encoder.copy_texture_to_buffer( - wgpu::ImageCopyTexture { - texture: &texture, - mip_level: 0, - origin: wgpu::Origin3d::ZERO, - aspect: wgpu::TextureAspect::All, - }, - wgpu::ImageCopyBuffer { - buffer: &staging, - layout: wgpu::ImageDataLayout { - offset: 0, - bytes_per_row: Some(bytes_per_row), - rows_per_image: Some(height), - }, - }, - wgpu::Extent3d { width, height, depth_or_array_layers: 1 }, - ); - queue.submit(Some(encoder.finish())); - - let slice = staging.slice(..); - let (tx, rx) = std::sync::mpsc::channel(); - slice.map_async(wgpu::MapMode::Read, move |r| { - let _ = tx.send(r); - }); - device.poll(wgpu::Maintain::Wait); - rx.recv() - .map_err(|e| format!("map_async recv: {:?}", e))? - .map_err(|e| format!("map_async: {:?}", e))?; - - let data = slice.get_mapped_range(); - let row_offset = (y as usize) * (bytes_per_row as usize); - let pixel_offset = row_offset + (x as usize) * (bytes_per_pixel as usize); - let bytes = &data[pixel_offset..pixel_offset + 16]; - let r = f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]); - let g = f32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]); - let b = f32::from_le_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]); - let a = f32::from_le_bytes([bytes[12], bytes[13], bytes[14], bytes[15]]); - drop(data); - staging.unmap(); - - Ok([r, g, b, a]) -} - -// ── FFI glue ─────────────────────────────────────────────────────────────── - -extern "C" { - fn topolei_canvas_shim_string_cstr(s: *const c_void) -> *const c_char; - fn topolei_canvas_shim_io_ok_unit() -> *mut c_void; - fn topolei_canvas_shim_io_ok_rgba(r: f64, g: f64, b: f64, a: f64) -> *mut c_void; -} - -fn lean_string_to_string(s: *const c_void) -> String { - unsafe { - let ptr = topolei_canvas_shim_string_cstr(s); - if ptr.is_null() { - String::new() - } else { - CStr::from_ptr(ptr).to_string_lossy().into_owned() - } - } -} - -/// `topolei_run_path(path, pathParam, width, height, title, world) -> IO Unit`. -/// -/// Single-panel render of one fiber of the cubical 1-cell `path`. -/// `pathParam` selects which fiber; the rendering is static — every -/// frame is the same fiber, the GPU just keeps it on screen until -/// the user closes the window. No host-side animation curve is -/// applied; that would not be a transport. -#[no_mangle] -pub extern "C" fn topolei_run_path( - path_obj: *const c_void, - path_param: f64, - width: u32, - height: u32, - title: *const c_void, - _world: *const c_void, -) -> *mut c_void { - let path = unsafe { eml::emlpath_from_lean(path_obj) }; - let title_s = lean_string_to_string(title); - let _ = run_path_loop(vec![path], vec![path_param as f32], title_s, width, height); - unsafe { topolei_canvas_shim_io_ok_unit() } -} - -/// `topolei_run_path2(pathL, ppL, pathR, ppR, width, height, title, world) -> IO Unit`. -/// -/// Two-panel side-by-side variant. `pathL` is rendered at fiber -/// `ppL` on the left; `pathR` at fiber `ppR` on the right. Each -/// panel binds its own uniform buffer with its own pathParam — the -/// two panels do not share fibers. Use this when the visual demand -/// is "show two specific fibers side-by-side" (e.g. `at0` vs `at1` -/// of the same 1-cell to display its boundary). -#[no_mangle] -pub extern "C" fn topolei_run_path2( - path_l_obj: *const c_void, - pp_l: f64, - path_r_obj: *const c_void, - pp_r: f64, - width: u32, - height: u32, - title: *const c_void, - _world: *const c_void, -) -> *mut c_void { - let path_l = unsafe { eml::emlpath_from_lean(path_l_obj) }; - let path_r = unsafe { eml::emlpath_from_lean(path_r_obj) }; - let title_s = lean_string_to_string(title); - let _ = run_path_loop( - vec![path_l, path_r], - vec![pp_l as f32, pp_r as f32], - title_s, - width, - height, - ); - unsafe { topolei_canvas_shim_io_ok_unit() } -} - -/// `topolei_canvas_render_probe_path_pixel(path, w, h, time, pathParam, x, y, world) -> IO RGBA`. -/// -/// Headless one-pixel readback for `render_faithful`. Same -/// fragment-naga construction as the live render. On GPU / adapter -/// failure, returns the sentinel `RGBA { -1, -1, -1, -1 }`. -#[no_mangle] -pub extern "C" fn topolei_canvas_render_probe_path_pixel( - path_obj: *const c_void, - width: u32, - height: u32, - time: f64, - path_param: f64, - x: u32, - y: u32, - _world: *const c_void, -) -> *mut c_void { - let path = unsafe { eml::emlpath_from_lean(path_obj) }; - match offscreen_render_pixel_path( - &path, width, height, time as f32, path_param as f32, x, y, - ) { - Ok([r, g, b, a]) => unsafe { - topolei_canvas_shim_io_ok_rgba(r as f64, g as f64, b as f64, a as f64) - }, - Err(e) => { - eprintln!("topolei-canvas: render_probe_path_pixel: {}", e); - unsafe { topolei_canvas_shim_io_ok_rgba(-1.0, -1.0, -1.0, -1.0) } - } - } -} diff --git a/native/include/topolei/canvas.h b/native/include/topolei/canvas.h deleted file mode 100644 index b1514d7..0000000 --- a/native/include/topolei/canvas.h +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once -#include -#include - -#ifdef __cplusplus -extern "C" { -#endif - -// Single shader fullscreen. -lean_obj_res topolei_run(lean_obj_arg shader, uint32_t width, uint32_t height, - lean_obj_arg title, lean_obj_arg world); - -// Two shaders side by side in one window. -lean_obj_res topolei_run2(lean_obj_arg shaderL, lean_obj_arg shaderR, - uint32_t width, uint32_t height, - lean_obj_arg title, lean_obj_arg world); - -#ifdef __cplusplus -} -#endif diff --git a/native/render/Cargo.lock b/native/render/Cargo.lock deleted file mode 100644 index 8ab096d..0000000 --- a/native/render/Cargo.lock +++ /dev/null @@ -1,32 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "cc" -version = "1.2.61" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" -dependencies = [ - "find-msvc-tools", - "shlex", -] - -[[package]] -name = "find-msvc-tools" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "topolei-render" -version = "0.1.0" -dependencies = [ - "cc", -] diff --git a/native/render/Cargo.toml b/native/render/Cargo.toml deleted file mode 100644 index 85f18f7..0000000 --- a/native/render/Cargo.toml +++ /dev/null @@ -1,27 +0,0 @@ -[package] -name = "topolei-render" -version = "0.1.0" -edition = "2021" -rust-version = "1.76" -description = "CPU SDF rasterizer for topolei's graph renderer. Native-only (std), sits beside the no_std cubical crate." -license = "MIT" -publish = false - -[lib] -name = "topolei_render" -crate-type = ["staticlib"] - -[dependencies] - -[build-dependencies] -cc = "1.0" - -[profile.release] -opt-level = 3 -lto = true -codegen-units = 1 -panic = "abort" - -[profile.dev] -opt-level = 0 -panic = "abort" diff --git a/native/render/build.rs b/native/render/build.rs deleted file mode 100644 index 2a2e7c5..0000000 --- a/native/render/build.rs +++ /dev/null @@ -1,29 +0,0 @@ -//! Build script — compiles the C shim that exposes Lean's inline -//! runtime helpers as real extern symbols (see shim.c). -//! -//! Mirrors `native/cubical/build.rs`. Native targets only. - -fn main() { - let target = std::env::var("TARGET").unwrap_or_default(); - if target.starts_with("wasm32") { - return; - } - - let lean_include = std::env::var("LEAN_INCLUDE").unwrap_or_else(|_| { - let prefix = std::process::Command::new("lean") - .arg("--print-prefix") - .output() - .expect("failed to run `lean --print-prefix`; set LEAN_INCLUDE instead"); - let prefix = String::from_utf8(prefix.stdout).unwrap(); - format!("{}/include", prefix.trim()) - }); - - cc::Build::new() - .file("shim.c") - .include(&lean_include) - .flag("-Wno-unused-parameter") - .compile("topolei_render_shim"); - - println!("cargo:rerun-if-changed=shim.c"); - println!("cargo:rerun-if-env-changed=LEAN_INCLUDE"); -} diff --git a/native/render/shim.c b/native/render/shim.c deleted file mode 100644 index 84583f2..0000000 --- a/native/render/shim.c +++ /dev/null @@ -1,72 +0,0 @@ -/* shim.c — expose Lean's static inline runtime helpers as real extern - * symbols for the render crate's future Rust FFI to link against. - * - * Mirror of `native/cubical/shim.c`, kept separate so the render crate - * can link its own shim without dragging the cubical crate's object - * files into its build. - * - * Lean 4's `lean_obj_tag` / `lean_ctor_get` / `lean_ctor_set` / - * `lean_alloc_ctor` / `lean_inc` / `lean_dec` / `lean_string_cstr` / - * `lean_mk_string` are all `static inline` in ``. A Rust - * staticlib that calls them via `extern "C"` produces unresolved - * references at link time. The wrappers below have real ELF symbols - * with the `topolei_render_shim_*` prefix. Zero overhead — the - * compiler should inline the calls. - * - * Compiled by `build.rs` via the `cc` crate. Native targets only; - * wasm builds don't link against Lean's runtime. - */ - -#include -#include - -uint32_t topolei_render_shim_obj_tag(b_lean_obj_arg o) { - return lean_obj_tag(o); -} - -lean_obj_res topolei_render_shim_ctor_get(b_lean_obj_arg o, unsigned i) { - return lean_ctor_get(o, i); -} - -void topolei_render_shim_ctor_set(lean_object* o, unsigned i, lean_obj_arg v) { - lean_ctor_set(o, i, v); -} - -lean_obj_res topolei_render_shim_alloc_ctor(unsigned tag, unsigned num_objs, unsigned scalar_sz) { - return lean_alloc_ctor(tag, num_objs, scalar_sz); -} - -void topolei_render_shim_inc(b_lean_obj_arg o) { - lean_inc(o); -} - -void topolei_render_shim_dec(b_lean_obj_arg o) { - lean_dec(o); -} - -const char* topolei_render_shim_string_cstr(b_lean_obj_arg s) { - return lean_string_cstr(s); -} - -lean_obj_res topolei_render_shim_mk_string(const char* s) { - return lean_mk_string(s); -} - -/* ByteArray (lean_sarray) helpers — used by future bytes-based FFI. - * These aren't in the cubical shim because cubical operates on ctor - * objects, not byte buffers; for render we'll pass serialised - * primitive lists + shader bytes through ByteArrays. */ - -size_t topolei_render_shim_sarray_size(b_lean_obj_arg a) { - return lean_sarray_size(a); -} - -const uint8_t* topolei_render_shim_sarray_cptr(b_lean_obj_arg a) { - return lean_sarray_cptr(a); -} - -lean_obj_res topolei_render_shim_alloc_sarray1(size_t size, size_t capacity) { - /* Element size 1 (bytes); result has size==capacity elements ready - * to be filled by the caller. */ - return lean_alloc_sarray(1, size, capacity); -} diff --git a/native/render/src/lib.rs b/native/render/src/lib.rs deleted file mode 100644 index 54139f3..0000000 --- a/native/render/src/lib.rs +++ /dev/null @@ -1,41 +0,0 @@ -//! # topolei-render -//! -//! Scaffolding crate for future Rust-side rendering work. Kept separate -//! from `native/cubical/` because the `no_std` + wasm-targeted cubical -//! crate shouldn't grow `std::fs` or heap allocation pressure unrelated -//! to cubical evaluation. -//! -//! ## Current state -//! -//! Contains one entry point — `topolei_render_version` — used as a -//! link-liveness check. The `shim.c` (in `shim.c` alongside this file) -//! mirrors `native/cubical/shim.c` so future FFI work can read Lean -//! objects properly instead of relying on hardcoded values. -//! -//! ## Planned entries (not yet implemented) -//! -//! - `topolei_render_sdf(prim, point) -> f32` — Rust-speed -//! implementation of `RenderPrim.sdf`. Wires via `@[extern]` + -//! `@[implemented_by]` on the Lean side. Equational laws -//! (`sdf_union_eq_min` etc.) are already `rfl` theorems in Lean; -//! the Rust impl must satisfy them by construction. -//! -//! - `topolei_render_compile_eml(expr_bytes) -> shader_bytes` — -//! Rust-side `compileEML` via `naga` (WGSL / SPIR-V / GLSL triple -//! target). Discharges `compileEML` + `compileEML_correct` in -//! `Topolei/GPU/Spec.lean`. -//! -//! ## FFI surface (today) -//! -//! - `topolei_render_version() -> u32` -//! Returns the crate's ABI version. Calling this from Lean and -//! getting `1` back confirms the staticlib is correctly linked. - -// ── ABI version check ─────────────────────────────────────────────────────── - -/// Returns the render crate's ABI version. Bumped whenever the FFI -/// signatures below change incompatibly. -#[no_mangle] -pub extern "C" fn topolei_render_version() -> u32 { - 1 -} diff --git a/native/src/canvas.cpp b/native/src/canvas.cpp deleted file mode 100644 index d1fab50..0000000 --- a/native/src/canvas.cpp +++ /dev/null @@ -1,210 +0,0 @@ -#include "topolei/canvas.h" - -// Lean C API — must come before any GL header. -#include - -// GLEW must be included before any GL header. -#include -#define GLFW_INCLUDE_NONE -#include -#include -#include -#include - -// ── shader helpers ──────────────────────────────────────────────────────────── - -static const char* VERT_SRC = R"glsl( -#version 330 core -out vec2 uv; -void main() { - vec2 pos = vec2((gl_VertexID == 1) ? 3.0 : -1.0, - (gl_VertexID == 2) ? 3.0 : -1.0); - uv = pos * 0.5 + 0.5; - gl_Position = vec4(pos, 0.0, 1.0); -} -)glsl"; - -// Vertex shader that clips to the left or right half of the window. -// u_panel: 0 = left half, 1 = right half -static const char* VERT_PANEL_SRC = R"glsl( -#version 330 core -out vec2 uv; -uniform int u_panel; // 0 = left, 1 = right -void main() { - vec2 pos = vec2((gl_VertexID == 1) ? 3.0 : -1.0, - (gl_VertexID == 2) ? 3.0 : -1.0); - uv = pos * 0.5 + 0.5; - // Remap x into the panel's half: left=[−1,0], right=[0,1] - pos.x = pos.x * 0.5 + (u_panel == 0 ? -0.5 : 0.5); - gl_Position = vec4(pos, 0.0, 1.0); -} -)glsl"; - -static GLuint compile_shader(GLenum type, const char* src) { - GLuint s = glCreateShader(type); - glShaderSource(s, 1, &src, NULL); - glCompileShader(s); - GLint ok; glGetShaderiv(s, GL_COMPILE_STATUS, &ok); - if (!ok) { - char log[2048]; glGetShaderInfoLog(s, sizeof(log), NULL, log); - fprintf(stderr, "shader compile error:\n%s\n", log); - return 0; - } - return s; -} - -static GLuint link_program(const char* vert_src, const char* frag_src) { - GLuint vert = compile_shader(GL_VERTEX_SHADER, vert_src); - GLuint frag = compile_shader(GL_FRAGMENT_SHADER, frag_src); - if (!vert || !frag) return 0; - - GLuint prog = glCreateProgram(); - glAttachShader(prog, vert); - glAttachShader(prog, frag); - glLinkProgram(prog); - glDeleteShader(vert); - glDeleteShader(frag); - - GLint ok; glGetProgramiv(prog, GL_LINK_STATUS, &ok); - if (!ok) { - char log[2048]; glGetProgramInfoLog(prog, sizeof(log), NULL, log); - fprintf(stderr, "program link error:\n%s\n", log); - return 0; - } - return prog; -} - -static GLFWwindow* make_window(int width, int height, const char* title) { - if (!glfwInit()) { fprintf(stderr, "glfwInit failed\n"); return nullptr; } - glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); - glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); - glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); - GLFWwindow* win = glfwCreateWindow(width, height, title, NULL, NULL); - if (!win) { fprintf(stderr, "glfwCreateWindow failed\n"); glfwTerminate(); return nullptr; } - glfwMakeContextCurrent(win); - glfwSwapInterval(1); - glewExperimental = GL_TRUE; - if (glewInit() != GLEW_OK) { - fprintf(stderr, "glewInit failed\n"); - glfwDestroyWindow(win); glfwTerminate(); return nullptr; - } - return win; -} - -// ── single fullscreen shader ────────────────────────────────────────────────── - -static int topolei_run_internal(const char* shader_src, int width, int height, const char* title) { - GLFWwindow* win = make_window(width, height, title); - if (!win) return 1; - - GLuint prog = link_program(VERT_SRC, shader_src); - if (!prog) { glfwDestroyWindow(win); glfwTerminate(); return 1; } - - GLuint vao; glGenVertexArrays(1, &vao); glBindVertexArray(vao); - GLint time_loc = glGetUniformLocation(prog, "u_time"); - GLint res_loc = glGetUniformLocation(prog, "u_resolution"); - - while (!glfwWindowShouldClose(win)) { - int w, h; glfwGetFramebufferSize(win, &w, &h); - glViewport(0, 0, w, h); - glUseProgram(prog); - if (time_loc >= 0) glUniform1f(time_loc, (float)glfwGetTime()); - if (res_loc >= 0) glUniform2f(res_loc, (float)w, (float)h); - glDrawArrays(GL_TRIANGLES, 0, 3); - glfwSwapBuffers(win); - glfwPollEvents(); - } - - glDeleteVertexArrays(1, &vao); - glDeleteProgram(prog); - glfwDestroyWindow(win); - glfwTerminate(); - return 0; -} - -// ── two panels side by side ─────────────────────────────────────────────────── - -static int topolei_run2_internal( - const char* srcL, const char* srcR, - int width, int height, const char* title) -{ - GLFWwindow* win = make_window(width, height, title); - if (!win) return 1; - - GLuint progL = link_program(VERT_PANEL_SRC, srcL); - GLuint progR = link_program(VERT_PANEL_SRC, srcR); - if (!progL || !progR) { glfwDestroyWindow(win); glfwTerminate(); return 1; } - - GLuint vao; glGenVertexArrays(1, &vao); glBindVertexArray(vao); - - auto bind_uniforms = [](GLuint prog, int panel, float t, float w, float h) { - glUseProgram(prog); - GLint pl = glGetUniformLocation(prog, "u_panel"); - GLint tl = glGetUniformLocation(prog, "u_time"); - GLint rl = glGetUniformLocation(prog, "u_resolution"); - if (pl >= 0) glUniform1i(pl, panel); - if (tl >= 0) glUniform1f(tl, t); - // Each panel sees only its half-width - if (rl >= 0) glUniform2f(rl, w * 0.5f, h); - }; - - while (!glfwWindowShouldClose(win)) { - int w, h; glfwGetFramebufferSize(win, &w, &h); - float t = (float)glfwGetTime(); - - glViewport(0, 0, w, h); - glClear(GL_COLOR_BUFFER_BIT); - - // Left panel - glEnable(GL_SCISSOR_TEST); - glScissor(0, 0, w/2, h); - bind_uniforms(progL, 0, t, (float)w, (float)h); - glDrawArrays(GL_TRIANGLES, 0, 3); - - // Right panel - glScissor(w/2, 0, w - w/2, h); - bind_uniforms(progR, 1, t, (float)w, (float)h); - glDrawArrays(GL_TRIANGLES, 0, 3); - - glDisable(GL_SCISSOR_TEST); - - // Divider line - glScissor(w/2 - 1, 0, 2, h); - glEnable(GL_SCISSOR_TEST); - glClearColor(0.5f, 0.5f, 0.6f, 1.0f); - glClear(GL_COLOR_BUFFER_BIT); - glClearColor(0.0f, 0.0f, 0.0f, 1.0f); - glDisable(GL_SCISSOR_TEST); - - glfwSwapBuffers(win); - glfwPollEvents(); - } - - glDeleteVertexArrays(1, &vao); - glDeleteProgram(progL); - glDeleteProgram(progR); - glfwDestroyWindow(win); - glfwTerminate(); - return 0; -} - -// ── Lean FFI ────────────────────────────────────────────────────────────────── - -extern "C" lean_obj_res topolei_run( - lean_obj_arg shader, uint32_t width, uint32_t height, - lean_obj_arg title, lean_obj_arg /* world */) -{ - topolei_run_internal(lean_string_cstr(shader), (int)width, (int)height, lean_string_cstr(title)); - return lean_io_result_mk_ok(lean_box(0)); -} - -extern "C" lean_obj_res topolei_run2( - lean_obj_arg shaderL, lean_obj_arg shaderR, - uint32_t width, uint32_t height, - lean_obj_arg title, lean_obj_arg /* world */) -{ - topolei_run2_internal( - lean_string_cstr(shaderL), lean_string_cstr(shaderR), - (int)width, (int)height, lean_string_cstr(title)); - return lean_io_result_mk_ok(lean_box(0)); -}