cubical-transport-hott-lean4/docs/FFI_DESIGN.md
Maximus Gorog 19928d040a
Some checks failed
Lean Action CI / build (push) Has been cancelled
REL2 universe stratification + topolei naming cleanup + Rust ABI v4
Two structural changes landed together as one coherent body of work.

## 1. Engine is name-clean from higher-order projects

The engine no longer carries "topolei" in its own naming surface.
Higher-order projects depend on the engine, not vice versa, so the
engine should be self-named.

  topolei-cubical (Cargo)            → cubical-transport
  libtopolei_cubical.a               → libcubical_transport.a
  topolei_cubical.h                  → cubical_transport.h
  TOPOLEI_FFI_ABI_VERSION            → CUBICAL_TRANSPORT_ABI_VERSION
  topolei_cubical_*  (14 FFI fns)    → cubical_transport_*
  topolei_shim_*     (9 shim fns)    → cubical_transport_shim_*

Inter-repo references describing topolei as a downstream consumer
(README, KERNEL_BOUNDARY.md, INDUCTIVE_TYPES.md, etc.) are preserved
as legitimate dependency-direction descriptions.

## 2. Universe-stratified, dependently-typed CType

  CType : ULevel → Type (genuinely indexed inductive)

with dependent pi/sigma carrying a binder name, a lift constructor
for cumulativity, and parameter lists of Σ-packaged types.

Per CCHM rules:
  · univ ℓ        : CType (ℓ.succ)
  · pi/sigma      : CType (max ℓ_A ℓ_B), with named binder
  · path A        : at A's level
  · glue T A      : T and A at same level
  · ind           : at user-chosen level (heterogeneous-level params)
  · interval      : CType .zero
  · lift          : CType (ℓ.succ), data-preserving

Every existing engine module cascades through {ℓ : ULevel} implicits
on functions/theorems, pi/sigma binder updates, and Σ-packaged params
lists.  CTerm stays un-indexed (universe lives on CType).

## 3. Substrate machinery for the cascade

  Universe.lean — ULevel inductive + max algebra (assoc, comm, etc.),
                  all theorems proven structurally.

  Syntax.lean — adds SkeletalCType enum + CType.skeleton level-erasure
                projection + per-constructor skeleton_* simp lemmas +
                CType.ind_skeleton_ne_pi disjointness lemma.  Used to
                discharge cross-level HEq cases in TransportLaws/CompLaws
                without invoking K.

## 4. Rust ABI v3 → v4

Lean 4 keeps implicit {ℓ : ULevel} parameters at runtime as constructor
fields, in declaration order interleaved with explicit args (verified
via probeLayout instrumentation).  Layout for level-bearing constructors
documented in cubical_transport.h §"v4 layout tables".

  CType.pi      : 5 fields — [ℓ_d, ℓ_c, var, A, B]
  CType.path    : 4 fields — [ℓ, A, a, b]
  CType.glue    : 9 fields — [ℓ, φ, T, f, fInv, sec, ret, coh, A]
  CType.ind     : 3 fields — [ℓ, S, params]
  CType.lift    : 2 fields — [ℓ, A]
  CTerm.transp  : 5 fields — [i, ℓ, A, φ, t]   (i precedes ℓ)
  CVal.vCompFun : 9 fields — [ℓ_d, ℓ_c, env, i, dom, cod, φ, u, t]
  ... etc

All Rust marshalling (value.rs, eval.rs, transport.rs, composition.rs,
glue.rs, beta.rs, dim_absent.rs, readback.rs, subst.rs, ffi.rs, tags.rs)
updated to match.

## Discipline

  · Zero sorry in CubicalTransport/.
  · Zero noncomputable instances; zero Classical.propDecidable shortcuts.
  · No CType.level projection (the level lives in the inductive's index).
  · No parallel CTypeU type.
  · No stub substrate types (def Ω := CType.univ etc.).
  · Tests restored to full coverage (EvalTest 623 lines, FFITest 351
    lines with classifier-runtime tests intact).

## Verification

  cd cubical-transport-hott-lean4
  lake build                 # 48 jobs OK
  ./.lake/build/bin/cubical-test
                             # ── 49/49 passed ──
                             # ── 46/46 properties passed ──
                             # PASS: all smoke + property tests

  cd ../topolei
  lake build                 # 90 jobs OK
  ./.lake/build/bin/probe-test
                             # ── 7/7 probes passed ──
                             # PASS: GPU output matches Lean ShaderSemantic

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 00:21:14 -06:00

14 KiB
Raw Blame History

FFI_DESIGN.md — Lean ↔ Rust C ABI Contract for Cubical HoTT

Stage 4.5 deliverable (2026-04-23). The design contract Rust implements to extend Lean 4 with kernel-level cubical-transport HoTT reduction.


1. Purpose and framing

Topolei's Rust backend is not a separate application that happens to use Lean as a library. It is a Lean kernel extension: Rust functions are linked via @[extern] / @[implemented_by] so that cubical terms reduce in Lean's kernel at native speed.

The contract is bidirectional:

  • Lean-side spec: every reduction rule Rust must implement is stated as an axiom (the Rust-discharge axioms catalogued in STATUS.md's "Axiom provenance spectrum"). The axioms are the Boolean-valued contract; Rust satisfies them structurally.
  • Rust-side implementation: Rust functions expose a C ABI that Lean's compiled code calls for reduction. The FFI boundary passes lean_object* pointers and returns lean_obj_res results, following Lean 4's runtime conventions.

2. FFI surface — the functions Rust implements

Lean function Rust symbol Arity Notes
eval cubical_transport_eval CEnv → CTerm → CVal Main evaluator entry
vApp cubical_transport_vapp CVal → CVal → CVal Function application
vPApp cubical_transport_vpapp CVal → DimExpr → CVal Dimension application
vTransp cubical_transport_vtransp DimVar → CType → FaceFormula → CVal → CVal Value-level transport
vHCompValue cubical_transport_vhcomp CType → FaceFormula → CVal → CVal → CVal Homogeneous composition
vCompAtTerm cubical_transport_vcomp_term CEnv → DimVar → CType → FaceFormula → CTerm → CTerm → CVal Heterogeneous comp at term
vCompNAtTerm cubical_transport_vcompn_term CEnv → DimVar → CType → List (FaceFormula × CTerm) → CTerm → CVal Multi-clause comp
vFst cubical_transport_vfst CVal → CVal First projection (Σ)
vSnd cubical_transport_vsnd CVal → CVal Second projection (Σ)
readback cubical_transport_readback CVal → CTerm NbE reification
readbackNeu cubical_transport_readback_neu CNeu → CTerm Neutral reification
CTerm.step (optional) cubical_transport_step CTerm → CTerm One-step reduction; see §8

3. Inductive object layout

All Lean inductives cross the boundary as lean_object* pointers with constructor tags accessed via lean_ctor_get_tag / lean_ctor_get_uint8. Rust recurses on the inductive shape using these accessors — no bindgen-generated structs, because Lean's runtime layout encodes small-value optimisations that bindgen cannot capture.

3.1 DimVar

structure DimVar where
  name : String

Single-field structure. At the C ABI: lean_ctor_get(obj, 0) returns the String object. Use lean_string_cstr to get a borrowed *const c_char. Hygiene convention: $-prefixed names are reserved; Rust should treat them as fresh-generated binders and avoid shadowing.

3.2 DimExpr

6 constructors, tagged 05:

Tag Constructor Fields
0 .zero
1 .one
2 .var 1 × DimVar
3 .inv 1 × DimExpr
4 .meet 2 × DimExpr
5 .join 2 × DimExpr

Rust evaluates / normalises via recursive descent. DimExpr.normalize (Stage 4.1, Interval.lean) defines the canonical form; Rust's implementation must produce normalize-equivalent output.

3.3 FaceFormula

6 constructors:

Tag Constructor Fields
0 .bot
1 .top
2 .eq0 1 × DimVar
3 .eq1 1 × DimVar
4 .meet 2 × FaceFormula
5 .join 2 × FaceFormula

FaceFormula.normalize (Stage 4.3, Face.lean) is the canonical form Rust must produce after .substDim for deterministic face dispatch.

3.4 CType

5 constructors:

Tag Constructor Fields
0 .univ
1 .pi 2 × CType
2 .path 1 × CType, 2 × CTerm
3 .sigma 2 × CType
4 .glue 1 × FaceFormula, 1 × CType, 5 × CTerm, 1 × CType

3.5 CTerm

12 constructors (11 after .step is derived). See Cubical/Syntax.lean for field order. Constructor tags are assigned in definition order: var, lam, app, plam, papp, transp, comp, compN, glueIn, unglue, pair, fst, snd.

3.6 CVal / CNeu

Mutual inductives. See Cubical/Value.lean for field order. Notable constructors that carry CEnv (closures):

  • vlam, vplam — λ-/dim-closures.
  • vCompFun, vPathTransp — hold their capture env for later reduction.

The env is itself an inductive cons-list of (String, CVal); Rust traverses via lookup_nil / lookup_cons tag dispatch.


4. Memory ownership conventions

Follows Lean 4's runtime:

  • b_lean_obj_arg (borrowed): Rust reads the pointer but does not manage its refcount. Default for read-only arguments (CTerm, CType, FaceFormula, etc.).
  • lean_obj_arg (owned): Rust takes ownership; must release via lean_dec unless returned. Used for arguments that will be consumed / transformed and returned as the result.
  • lean_obj_res (result): returned value; caller takes ownership.

Convention for this project:

  • All read-only inputs (CTerm, CType, FaceFormula, CEnv, DimVar, etc.) are passed as b_lean_obj_arg.
  • Results are lean_obj_res — Rust allocates via lean_alloc_ctor etc. and returns.
  • The Cubical/FFI.lean module (Stage 4.6) declares every function with @& annotations matching these conventions.

5. Recursion strategy

Rust implements each FFI entry point natively via tag dispatch. Examples:

// cubical_transport_eval: eval : CEnv → CTerm → CVal
#[no_mangle]
pub extern "C" fn cubical_transport_eval(
    env: *const c_void,      // b_lean_obj_arg — borrowed CEnv
    t: *const c_void,        // b_lean_obj_arg — borrowed CTerm
) -> *mut c_void {           // lean_obj_res — owned CVal
    let tag = unsafe { lean_ctor_get_tag(t) };
    match tag {
        0 => eval_var(env, t),
        1 => eval_lam(env, t),
        2 => eval_app(env, t),
        // ... 11 arms total for CTerm
        _ => unreachable!(),
    }
}

Callback to Lean? Generally no — Rust implements everything natively to avoid the FFI round-trip cost. The one exception: helper functions like CTerm.substDim, FaceFormula.substDim may be called back if Rust chooses to keep them Lean-implemented. Recommendation: Rust implements its own subst_dim matching the Lean spec — avoids FFI overhead and keeps the evaluator self-contained.


6. Error and divergence conventions

Lean's partial def for eval produces marker neutrals like .vneu (.nvar "<vApp: vplam applied as function>") for ill-typed states. Rust mirrors this: on unreachable arms (type errors in input that should have been caught by the typechecker), allocate a vneu neutral whose nvar name carries the error tag. Do not panic or abort — Lean's kernel tolerates stuck forms.

For genuine divergence (the partial def's unrestricted recursion), Rust uses a configurable fuel counter. Default: 1 000 000 reductions. On exhaustion: return a vneu (.nvar "<divergence>") neutral and log. Rust never crashes Lean.


7. @[extern] vs @[implemented_by]

Lean 4 offers two ways to bind a Lean name to a Rust implementation:

  • @[extern "symbol"] opaque — the Lean declaration has no Lean- native body; the symbol resolves at link time. Best for functions that have no meaningful fallback (IO primitives, hardware access).
  • @[implemented_by rustImpl] def original := leanFallback — the Lean has a slow fallback; Rust runs at runtime. Best when the fallback is useful for testing without Rust linked.

Our choice: @[implemented_by] for all cubical-core functions. Rationale:

  • The existing partial def bodies in Eval.lean / Transport.lean / Readback.lean are correct Lean implementations (slow but usable for unit tests and axiom-consistency checks). Keeping them as fallbacks lets us run the project without the Rust crate (in a fresh clone or before Rust builds).
  • @[implemented_by] preserves the ability to test Lean-only behaviour (e.g. in EvalTest.lean) without CI needing Cargo.
  • The Rust-implemented path is taken when the compiled artifact links to the Rust library; otherwise the fallback runs.

@[extern] for types:

  • GPUContext, ShaderHandle in GPU/Spec.lean remain opaque extern types — no Lean-native representation. Pattern already in use.

8. CTerm.step design choice

Per Stage 4.4 decision, CTerm.step remains opaque at the Lean spec level. Rust has two valid implementation strategies:

Option A — native step

#[no_mangle]
pub extern "C" fn cubical_transport_step(t: *const c_void) -> *mut c_void {
    // Direct pattern-match on CTerm constructor; emit CCHM comp-shaped
    // body for path-typed transp-of-plam; identity-ish otherwise.
    ...
}

Pros: single FFI call per step; simplest Rust surface. Cons: duplicates logic with eval + readback.

Option B — derived step

#[no_mangle]
pub extern "C" fn cubical_transport_step(t: *const c_void) -> *mut c_void {
    let empty_env = lean_alloc_ctor(0, 0, 0);  // CEnv.nil
    let v = cubical_transport_eval(empty_env, t);
    cubical_transport_readback(v)
}

Pros: no duplicated logic; step is a pure composition. Cons: extra allocation overhead per step (negligible in practice).

Recommendation: Option B. It's semantically equivalent to the readback ∘ eval .nil definition implied by the step↔eval bridge, and reduces Rust maintenance surface. The T4 axiom (transp_plam_is_plam_path) is satisfied by Option B via the .vPathTransp.plam j (.compN …) readback arm.


9. Lake + Cargo integration

9.1 Directory layout

topolei/
├── lakefile.toml                   -- Lean build
├── native/                         -- top-level native code dir
│   ├── CMakeLists.txt              -- existing C++ GPU/Canvas (glfw, gl, glew)
│   ├── include/topolei/...         -- C++ headers
│   ├── src/canvas.cpp              -- existing C++ GPU/Canvas
│   └── cubical/                    -- Rust cubical backend (THIS CRATE)
│       ├── Cargo.toml              -- no_std, dual native+wasm targets
│       ├── README.md
│       ├── include/
│       │   └── cubical_transport.h   -- C header, ABI v1
│       ├── src/
│       │   ├── lib.rs              -- #![no_std], panic_handler
│       │   ├── lean_runtime.rs     -- hand-rolled Lean C ABI
│       │   ├── ffi.rs              -- #[no_mangle] exports
│       │   └── (future Phase BC)  -- cubical/, subst/ modules
│       └── target/                 -- Cargo output (gitignored)
└── Topolei/
    └── Cubical/
        ├── FFI.lean                -- @[extern] + @[implemented_by] (Stage 4.6)
        └── (existing files)

The Rust crate is isolated at native/cubical/ — separate build system from the C++ GPU/Canvas legacy code at native/ top-level. Each has its own toolchain (Cargo vs CMake) and own artifacts.

9.2 native/Cargo.toml

[package]
name = "topolei-native"
version = "0.1.0"
edition = "2024"

[lib]
crate-type = ["staticlib"]  # For Lean to link against.

[dependencies]
lean-sys = { version = "0.1", features = ["runtime"] }

[profile.release]
lto = true
codegen-units = 1

9.3 lakefile.toml additions

[[external_lib]]
name = "cubical_transport_native"
extra_link_args = ["-L", "native/target/release", "-ltopolei_native"]

# Build step: invoke `cargo build --release -p topolei-native`
# before Lean compilation; see lakefile script pre-build hook.

(Exact lakefile syntax depends on the Lean toolchain version; verify at implementation time.)

9.4 C header

// native/include/cubical_transport.h
#pragma once
#include <lean/lean.h>

lean_obj_res cubical_transport_eval(b_lean_obj_arg env, b_lean_obj_arg t);
lean_obj_res cubical_transport_vapp(b_lean_obj_arg f, b_lean_obj_arg arg);
lean_obj_res cubical_transport_vpapp(b_lean_obj_arg v, b_lean_obj_arg r);
// ... one declaration per FFI function

The header is generated from Cubical/FFI.lean's @[extern] / @[implemented_by] declarations (Stage 4.6 produces the Lean side; the header is hand-maintained in parallel).


10. Testing strategy

  • Unit tests (Lean-only fallback): EvalTest.lean already tests the Lean partial def implementations. With Rust linked, these tests run Rust's impl transparently via @[implemented_by].
  • Axiom consistency: for every Rust-discharge axiom, a CI step verifies the Rust implementation matches by running representative term reductions and comparing against the Lean fallback's output.
  • Differential fuzzing: random CTerm generation → compare rust_eval env t vs lean_fallback_eval env t for thousands of terms. Divergence is a bug.

11. Versioning and ABI stability

The FFI contract is versioned:

// C header
#define CUBICAL_TRANSPORT_ABI_VERSION 1

Any change to a function signature, inductive layout, or ownership convention increments this number. Rust and Lean both embed the version; link-time mismatch is a hard error.


12. Non-goals

  • The Rust backend does not implement any of:
    • Zigzag n-category engine (Lean port; ZIGZAG_PORT.md).
    • Numerical layer (separate stream, NUMERICAL.md).
    • Cell-layer semantics (Phase 2, pure Lean).
    • Boundary / Security modal model (Phase 5b, pure Lean).
  • These are all Lean-native; adding Rust to them would violate the "one Rust component" discipline (STATUS.md architecture section).

End of FFI_DESIGN.md. Companion document: FFI_COMPLETENESS.md (Stage 4.7) — the per-function axiom-completeness audit.