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>
9.3 KiB
WASM.md — Wasm Target Integration
Phase D.wasm deliverable, 2026-04-24. Describes the
cubical-transport crate's wasm32 build, its import/export contract, and
how to embed it in a Lean-wasm composite artifact.
1. Build
cd native/cubical
cargo build --release --target wasm32-unknown-unknown
# Output: target/wasm32-unknown-unknown/release/cubical_transport.wasm
Requirements: the wasm32-unknown-unknown Rust target installed
(rustup target add wasm32-unknown-unknown). No emscripten, no C
toolchain needed for the Rust side — build.rs skips the shim.c
compilation on wasm targets.
Current artifact size: ~20KB (no_std, full cubical evaluator).
2. Wasm ABI contract
The compiled module has these wasm-level imports and exports.
2.1 Exports (14 + memory + globals)
All 14 cubical-evaluator entry points plus wasm runtime bookkeeping:
| Export | Role |
|---|---|
cubical_transport_eval |
Main evaluator: (CEnv, CTerm) → CVal |
cubical_transport_vapp |
Function application |
cubical_transport_vpapp |
Dim application |
cubical_transport_vtransp |
Value-level transport |
cubical_transport_vhcomp |
Homogeneous composition |
cubical_transport_vcomp_term |
Hetero comp at term level |
cubical_transport_vcompn_term |
Multi-clause comp |
cubical_transport_vfst |
Σ first projection |
cubical_transport_vsnd |
Σ second projection |
cubical_transport_readback |
NbE reification |
cubical_transport_readback_neu |
Neutral reification |
cubical_transport_step |
readback ∘ eval .nil (Option B) |
cubical_transport_dimexpr_normalize |
DimExpr canonicalisation |
cubical_transport_face_normalize |
FaceFormula canonicalisation |
memory |
Linear memory (shared with host) |
__heap_base |
Address of first allocatable byte |
__data_end |
Address of end of static data |
Every cubical_transport_* export takes lean_object* (i.e. i32 wasm
pointers) and returns lean_object*. The Lean object layout on
wasm32 is:
struct lean_object {
uint32_t m_rc; // offset 0, 4 bytes
uint16_t m_cs_sz; // offset 4, 2 bytes
uint8_t m_other; // offset 6, 1 byte (num_objs for ctors)
uint8_t m_tag; // offset 7, 1 byte
// fields follow at offset 8; each pointer is 4 bytes on wasm32.
};
Scalar encoding: (value << 1) | 1. Heap objects have low bit 0.
2.2 Imports (in env namespace)
Eight shim functions the host must provide. These are Lean's
inline-only helpers (lean_obj_tag, lean_ctor_get, etc.) exposed as
real wasm imports:
| Import | Lean equivalent | Purpose |
|---|---|---|
cubical_transport_shim_obj_tag |
lean_obj_tag |
(o: i32) → i32 — constructor tag |
cubical_transport_shim_ctor_get |
lean_ctor_get |
(o, i) → field pointer |
cubical_transport_shim_ctor_set |
lean_ctor_set |
(o, i, v) → () |
cubical_transport_shim_alloc_ctor |
lean_alloc_ctor |
(tag, num_objs, scalar_sz) → new ctor |
cubical_transport_shim_inc |
lean_inc |
refcount +1 (no-op on scalars) |
cubical_transport_shim_dec |
lean_dec |
refcount –1 |
cubical_transport_shim_string_eq |
lean_string_eq |
(a, b) → bool |
cubical_transport_shim_mk_string |
lean_mk_string |
Allocate Lean String from null-terminated C string |
A Lean-wasm runtime must satisfy these imports. Two paths:
-
Natively via C compilation. Compile
shim.calongside the Lean runtime in the wasm build (emscripten, say, or clang wasm32). The shim delegates to Lean's inlines; Lean's own runtime provideslean_alloc_small,lean_dec_ref_cold, etc. -
Via runtime binding. When instantiating the wasm module in a host (Node/browser/wasmtime), provide JS/Rust implementations of the 8 shim functions that interact with the shared wasm memory.
The harness at test/wasm_harness.mjs uses approach (2) with a
minimal JS implementation — enough to demonstrate the module works.
3. Integration with a Lean-wasm composite artifact
The cells-spec vision is a Lean 4 wasm binary with topolei cubical support, running in a browser or wasmtime. The composite artifact comprises:
lean-wasm-composite/
├── lean-kernel.wasm # Lean's own runtime compiled to wasm
├── cubical-transport.wasm # This crate
├── glue.js / glue.wasm # Import/export wiring
└── main.{js,html} # Entry point
Two approaches to wiring them:
3.1 Host-side multiplexing (recommended)
Instantiate both modules in the host (JS or another wasm runtime).
The host holds a single WebAssembly.Memory instance and shares it
between modules. Imports on cubical-transport resolve to functions
exported by lean-kernel.wasm (the shim wrappers that call Lean's
inlines).
Advantages: no linker required, modules are swappable, each can be updated independently. Clean separation of concerns.
Pseudo-code:
const lean = await WebAssembly.instantiate(leanKernelWasm, {});
const topolei = await WebAssembly.instantiate(topoleiCubicalWasm, {
env: {
cubical_transport_shim_obj_tag: lean.instance.exports.cubical_transport_shim_obj_tag,
cubical_transport_shim_ctor_get: lean.instance.exports.cubical_transport_shim_ctor_get,
// ... etc
},
});
// Share memory: either use lean's memory (topolei declares memory import)
// or bridge via copy routines.
3.2 Static link via wasm-ld
Link both modules at build time using wasm-ld --whole-archive or
similar. Produces a single wasm file. Simpler deployment but less
flexible for updates.
Requirements: both modules must be compiled with compatible calling conventions and share a single memory segment. wasm-ld handles this automatically for reactors (non-main wasm).
4. Verification harness
test/wasm_harness.mjs is a standalone Node.js script that loads the
wasm module and runs 10 correctness tests:
- Scalar passthrough (4 tests) —
.zero,.one,.bot,.topare tagged-pointer scalars; their normalizers return identity. - Heap reductions (5 tests) —
.inv .zero → .one,.inv .one → .zero, double-negation, face absorption (.meet .top .bot → .bot), recursive.meetnormalization. - Structural inspection (1 test) — verifies a
.meetof two.invs normalizes to a.meetof scalars (Rust traverses and allocates correctly).
Run:
cd native/cubical
node test/wasm_harness.mjs
Expected: ── 10 / 10 tests passed ──.
The harness provides its own minimal allocator (bump-pointer from
__heap_base) and Lean-layout helpers — it is NOT a full Lean
runtime, but it is sufficient to exercise every Rust code path that
doesn't require strings.
5. Limitations and TODOs
5.1 String handling
The harness does not implement lean_string_eq or lean_mk_string.
Tests that exercise env lookup (eval .nil (.var "x")) or string
messages would require a full String-object implementation in the
host. A real Lean-wasm runtime provides these automatically.
5.2 Memory growth
The wasm module declares an initial memory size. Complex
computations may exceed it; the host should monitor
memory.buffer.byteLength and call memory.grow() as needed.
cubical_transport_shim_alloc_ctor's allocator implementation decides when to
grow.
5.3 Pointer size (wasm32 vs wasm64)
Currently targeting wasm32-unknown-unknown — 32-bit pointers. A
future wasm64 build would change the field offset calculations in
the harness (FIELD_SIZE = 8 instead of 4). The Rust code adapts
automatically via core::mem::size_of::<*const _>(), but consumers
of WASM.md should be aware.
5.4 Refcount correctness
The JS harness stubs cubical_transport_shim_inc / _dec as no-ops (we don't
GC). This is fine for one-shot test runs but would leak in a
long-running application. A real host must implement refcounting.
6. Related documents
FFI_DESIGN.md— full C ABI contract (includes wasm as a target).FFI_COMPLETENESS.md— per-function axiom-coverage audit.KERNEL_BOUNDARY.md— what this delivers vs. Lean kernel limits.native/cubical/README.md— crate-level build + development notes.