cubical-transport-hott-lean4/Topolei/GPU/Spec.lean
Maximus Gorog c2e3ecb3e3
Some checks are pending
Lean Action CI / build (push) Waiting to run
Initial commit: topolei — cubical-transport HoTT in Lean 4 + Rust FFI
Implements the cells-spec vision: a computation space that preserves
auditability, correctness, interactivity. Phase 1 (Lean kernel +
naga-IR Rust backend) is closed; foundation hypothesis stack
(Selection H1+H2, Subobject H3, Trace H5, Obs.Ctx C2, Cubical.Trace)
landed.

Highlights:
- Cubical-HoTT syntax + value/eval/readback in Lean
- naga-IR pipeline (no GLSL string crosses FFI; 17/17 probes pass)
- Honesty audit: every non-transport (sealed cells, vertex shader,
  Y-flip, presentation conventions) is documented as such
- Polymorphic Trace α as free monoid; Cubical.Trace gives
  CTerm → Trace CTerm by structural fold (homomorphism = definition)
- Selection as Huet zipper; Subobject as Boolean algebra over WCell
- All theorems proven; the proof IS the implementation

See STATUS.md for the resume guide.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 20:40:45 -06:00

448 lines
21 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

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

/-
Topolei.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]
-- ── EMLPathGPU 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.