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