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