# nix-effects — Full Documentation ## Manual ### Guide #### Introduction Nix libraries often grow into small languages. A user declares entities, reusable aspects, target classes, dependencies, and policies; a pipeline turns that declarative graph into concrete Nix modules, files, packages, or checks. Without a shared structure, every layer writes its own walker: one for validation, one for documentation, one for dependency ordering, one for rendering, one for diagnostics. nix-effects is a typed, description-backed programming substrate for pure Nix. Effects are the execution model; generated descriptions are the shared structure; the kernel checks boundaries; generic tools, diagnostics, proofs, and ornaments reuse the same shape. Everything runs at `nix eval` time, before anything builds or ships. The unusual part is that the kernel is hosted inside Nix evaluation itself. Nix normally gives libraries functions, attrsets, assertions, and module option types. nix-effects adds typed descriptions that generic programs can inspect. Validation is therefore not a hand-written walker: it is one interpretation of a type description, next to schemas, documentation, dependency extraction, and DSL interpretation. The type layer is backed by a Martin-Löf dependent type checker in `src/tc/` with Pi, Sigma, identity types with J, explicit universe levels, HOAS elaboration, generated datatypes, and verified extraction of plain Nix functions from proof terms. Descriptions provide reusable datatype shapes, so domain DSLs can be validated, interpreted, documented, transformed, or extracted by generic tools instead of one-off traversals. The bidirectional checker sends `typeCheck` effects carrying a field-path context, so type errors in deeply nested terms come back localized to the field that broke. ## What it looks like A target class is one of a small set of strings. In nix-effects, that is a refinement type: ```nix let inherit (fx.types) String refined; TargetClass = refined "TargetClass" String (x: builtins.elem x [ "module" "file" "package" "check" ]); in { ok = TargetClass.check "module"; # true bad = TargetClass.check "fleet"; # false } ``` Behind the scenes, `.check` runs the MLTT kernel's decision procedure for the base type, then evaluates the refinement predicate. For a refinement type like `TargetClass`, this is fast: the kernel confirms a string, the guard confirms membership in the known target classes. You write normal Nix and the kernel runs behind the scenes. But checking individual values is only the starting point. The same kernel-backed structure supports derived validation for compound shapes and can also verify entire functions, then extract them as ordinary Nix functions. ## Verified functions over real data Write an implementation in HOAS (Higher-Order Abstract Syntax), the kernel type-checks it, and `v.verify` extracts a callable Nix function. Here is a small aspect declaration predicate. The kernel verifies a function that takes a record with `name`, `target`, and `requires` fields, then checks that the target is one of the classes this pipeline knows how to render. The check uses string comparison inside the kernel — `strEq` is a kernel primitive, not a Nix-level hack. ```nix let H = fx.types.hoas; v = fx.types.verified; AspectDecl = H.record [ { name = "name"; type = H.string; } { name = "target"; type = H.string; } { name = "requires"; type = H.listOf H.string; } ]; targets = H.cons H.string (v.str "module") (H.cons H.string (v.str "file") (H.cons H.string (v.str "package") (H.cons H.string (v.str "check") (H.nil H.string)))); validateAspect = v.verify (H.forall "a" AspectDecl (_: H.bool)) (v.fn "a" AspectDecl (a: v.strElem (v.field AspectDecl "target" a) targets)); in { ok = validateAspect { name = "workspace-shell"; target = "module"; requires = [ "toolchain" ]; }; # true bad = validateAspect { name = "workspace-aspect"; target = "fleet"; requires = [ ]; }; # false } ``` `validateAspect` is a plain Nix function. You call it with a plain Nix attrset. But the implementation was verified by the MLTT kernel before extraction — the kernel confirmed that the function matches its type (`AspectDecl → Bool`), that field projections are well-typed, and that the string membership check composes correctly. If you made a type error in the implementation — say, compared a `Bool` where a `String` was expected — the kernel would reject it at `nix eval` time. The record type (`H.record`) elaborates to nested Sigma in the kernel. `v.field` desugars to the right chain of first/second projections. `v.strEq` is a kernel primitive that reduces `strEq "foo" "foo"` to `true` during normalization. `v.strElem` folds over a list with `strEq`. None of this is Nix-level string comparison — it's computation inside the proof checker. ## Proofs as programs The same kernel that verifies functions also checks mathematical proofs. Both are the same judgment — `Γ ⊢ t : T` — applied differently. A verified function proves that an implementation inhabits its type. An equality proof proves that two expressions reduce to the same normal form. ```nix let H = fx.types.hoas; v = fx.types.verified; # Verified addition: Nat → Nat → Nat by structural recursion add = v.verify (H.forall "m" H.nat (_: H.forall "n" H.nat (_: H.nat))) (v.fn "m" H.nat (m: v.fn "n" H.nat (n: v.match H.nat m { zero = n; succ = _k: ih: H.succ ih; }))); in { five = add 2 3; # 5 # Prove 3 + 5 = 8: the kernel normalizes both sides, Refl witnesses equality proof = (H.checkHoas (H.eq H.nat (add (H.natLit 3) (H.natLit 5)) (H.natLit 8)) H.refl).tag == "refl"; # true } ``` The `add` function is extracted exactly like `validateAspect` — write in HOAS, kernel checks, extract a Nix function. The equality proof goes one step further: the kernel normalizes `add(3, 5)` by running the structural recursion, arrives at `8`, and confirms `Refl` witnesses `8 = 8`. This is computational proof — the kernel computes the answer and verifies that computation agrees with the claim. ## The effect system The "effects" in nix-effects are algebraic effects implemented via a freer monad (Kiselyov & Ishii 2015). A computation is a tree of effects with continuations. A handler walks the tree, interpreting each effect: ```nix let inherit (fx) pure bind run; inherit (fx.effects) get put; # Read state, double it, write it back doubleState = bind get (s: bind (put (s * 2)) (_: pure s)); result = run doubleState fx.effects.state.handler 21; # result.value = 21, result.state = 42 in result ``` This matters for type checking because it separates *what* to check from *how* to report. When `DepRecord.validate` finds a type error, it sends a `typeCheck` effect. The handler decides the policy: - **Strict** — abort on the first error - **Collecting** — gather all errors, keep checking - **Logging** — record every check, pass or fail Same validation logic, different handler. The checker reports through the same effect substrate, so validation policy composes with the rest of the effect system. ## The verification spectrum Not everything needs proofs. nix-effects supports four levels of assurance, and you pick the one that fits: **Level 1 — Contract.** Write normal Nix. Types check values via `.check`. The kernel runs behind the scenes. Zero cost to adopt. ```nix TargetClass = refined "TargetClass" String (x: builtins.elem x [ "module" "file" "package" "check" ]); TargetClass.check "module" # true ``` **Level 2 — Boundary.** Data is checked by the kernel at module interfaces. Every type has a `kernelType` and `.check` is derived from the kernel's `decide` procedure. This is what all built-in types do by default — `(ListOf String).check ["a" "b"]` elaborates the list into a kernel term and type-checks it. **Level 3 — Property.** Write proof terms in HOAS that the kernel verifies. Prove that `3 + 5 = 8`, or that double negation on booleans is the identity, or that `append([1,2], [3]) = [1,2,3]`. **Level 4 — Full.** The implementation IS the proof term. Write in HOAS, the kernel verifies, `extract` produces a Nix function correct by construction. The `validateAspect` example above is Level 4 — the kernel verified the validator before extracting it as a callable function. Most users will stay at levels 1 and 2. The kernel is there when you need it. With record types and string comparison now in the kernel, Level 4 handles real-world validation — not just arithmetic on natural numbers. ## How this document is organized The rest of the guide builds up from here: - **[Getting Started](/nix-effects/guide/getting-started)** sets up the library and shows the first type, effect, and generated datatype. - **[Effects and Handlers](/nix-effects/guide/effects-and-handlers)** explains the execution model: computations send operations, handlers choose policy. - **[Typed Validation](/nix-effects/guide/typed-validation)** covers primitive, refined, structured, and dependent validation boundaries. - **[Generated Datatypes](/nix-effects/guide/generated-datatypes)** introduces description-backed data as the shared structure. - **[Generic Programming](/nix-effects/guide/generic-programming)** shows how schemas, dependency graphs, diagnostics, and views derive from that structure. - **[Ornaments and Description-Backed Data](/nix-effects/guide/ornaments)** refines generated shapes while preserving forgetful maps. - **[Proof Guide](/nix-effects/guide/proof-guide)** builds proofs and verified implementations from kernel-checked HOAS terms. - **[Theory](/nix-effects/concepts/theory)** explains the ideas behind the model. - The internals chapters document the trampoline, architecture, kernel implementation, and formal specification for contributors. ## References 1. Martin-Lof, P. (1984). *Intuitionistic Type Theory*. Bibliopolis. 2. Kiselyov, O., & Ishii, H. (2015). *Freer Monads, More Extensible Effects*. Haskell Symposium 2015. [[pdf](https://okmij.org/ftp/Haskell/extensible/more.pdf)] 3. Plotkin, G., & Pretnar, M. (2009). *Handlers of Algebraic Effects*. ESOP 2009. [[doi](https://doi.org/10.1007/978-3-642-00590-9_7)] 4. Findler, R., & Felleisen, M. (2002). *Contracts for Higher-Order Functions*. ICFP 2002. [[doi](https://doi.org/10.1145/581478.581484)] #### Getting Started ## Installation Add nix-effects as a flake input: ```nix { inputs.nix-effects.url = "github:kleisli-io/nix-effects"; outputs = { nix-effects, nixpkgs, ... }: let fx = nix-effects.lib; in { # fx.types, fx.run, fx.send, fx.bind, fx.effects, fx.stream ... }; } ``` Or import directly without flakes: ```nix let fx = import ./path/to/nix-effects { lib = nixpkgs.lib; }; in ... ``` ## Your first type Define a type with `fx.types.refined`: ```nix let inherit (fx.types) String refined; TargetClass = refined "TargetClass" String (x: builtins.elem x [ "module" "file" "package" "check" ]); in { # Kernel decision procedure — fast boolean check ok = TargetClass.check "module"; # true bad = TargetClass.check "fleet"; # false # Effectful validate — runs through the trampoline, produces blame context result = fx.run (TargetClass.validate "fleet") fx.effects.typecheck.collecting []; # result.state = [ { context = "TargetClass"; ... } ] } ``` ## Your first dependent type One field's type depends on another field's value — this is a genuine dependent type, checked by the MLTT proof-checking kernel: ```nix let inherit (fx.types) Bool String ListOf DepRecord refined; TargetClass = refined "TargetClass" String (x: builtins.elem x [ "module" "file" "package" "check" ]); AspectDecl = DepRecord [ { name = "generated"; type = Bool; } { name = "target"; type = self: if self.generated then TargetClass else String; } { name = "requires"; type = _: ListOf String; } ]; in { ok = AspectDecl.checkFlat { generated = true; target = "module"; requires = [ "toolchain" ]; }; # true bad = AspectDecl.checkFlat { generated = true; target = "fleet"; requires = [ ]; }; # false } ``` ## Your first effect Write a computation, then choose the handler: ```nix let inherit (fx) pure bind run; inherit (fx.effects) get put; # Double the state doubleState = bind get (s: bind (put (s * 2)) (_: pure s)); result = run doubleState fx.effects.state.handler 21; # result.value = 21 (old state returned) # result.state = 42 (state doubled) in result ``` ## A first generated datatype Generated datatypes are the next step after validation. They give a DSL its constructors, its type, and reusable structural metadata: ```nix let H = fx.types.hoas; Aspect = H.datatype "Aspect" [ (H.con "aspect" [ (H.field "name" H.string) (H.field "target" H.string) (H.field "requires" (H.listOf H.string)) ]) ]; in Aspect ``` The generated `Aspect` shape can be checked, viewed, reviewed, documented, traversed for dependencies, interpreted by handlers, and ornamented with derived fields. ## Running checks The repo includes runnable tests and examples: ```bash git clone https://github.com/kleisli-io/nix-effects cd nix-effects # Run all tests nix flake check ``` ## The kernel behind every type Every `.check` call runs the MLTT type-checking kernel. When you write `TargetClass.check "module"`, the kernel elaborates `"module"` into a term, type-checks it, and returns a boolean. This is not a separate system — it is what `.check` does. You can also write verified implementations using HOAS combinators: ```nix let H = fx.types.hoas; v = fx.types.verified; # Write a function in HOAS, kernel type-checks it, extract as Nix function succ = v.verify (H.forall "x" H.nat (_: H.nat)) (v.fn "x" H.nat (x: H.succ x)); in succ 5 # 6 — a certified Nix function ``` The kernel checks the implementation against its type at `nix eval` time. If the types don't match, you get an error before anything builds. See the [Kernel Architecture](/nix-effects/internals/kernel-architecture) chapter for the full pipeline. ## What's in the box The `fx` attrset is the entire public API: | Namespace | Contents | |-----------|---------| | `fx.pure`, `fx.bind`, `fx.send`, `fx.map`, `fx.seq` | Freer monad kernel | | `fx.run`, `fx.handle` | Trampoline interpreter | | `fx.adapt`, `fx.adaptHandlers` | Handler composition | | `fx.types.*` | Type system (primitives, constructors, dependent, refinement, universe) | | `fx.types.hoas` | HOAS surface combinators for the kernel | | `fx.types.elaborateType`, etc. | Elaboration bridge: fx.types ↔ kernel | | `fx.types.verified` | Convenience combinators for writing verified implementations | | `fx.effects.*` | Built-in effects (state, error, reader, writer, acc, choice, conditions, typecheck, linear) | | `fx.stream.*` | Effectful lazy sequences | #### Effects and Handlers nix-effects separates program intent from execution policy. A program sends operations such as "read state", "write state", or "report a type error". A handler decides what those operations mean. That split is the execution model for the rest of the library. Typed validation, diagnostics, streams, resource tracking, and the checker all use the same effect substrate. The validator does not decide whether an error aborts immediately or gets collected with the next ten errors. The handler decides. ## Computations A computation is either finished or waiting for a handler: ```nix Pure value Impure effect continuation ``` The public constructors are `pure`, `send`, and `bind`: ```nix let inherit (fx) pure bind send; in bind (send "get" null) (state: bind (send "put" (state + 1)) (_: pure state)) ``` Most code uses effect modules rather than raw `send`: ```nix let inherit (fx) pure bind run; inherit (fx.effects) state; increment = bind state.get (n: bind (state.put (n + 1)) (_: pure n)); in run increment state.handler 41 ``` The result contains the returned value and final handler state: ```nix { value = 41; state = 42; } ``` ## Handlers are policy A handler is an attrset of operations. Each operation receives the effect parameter, the current handler state, and the continuation protocol. It can resume the computation or abort it: ```nix { get = { param, state }: { resume = state; inherit state; }; put = { param, state }: { resume = null; state = param; }; } ``` The computation above never mentions how state is represented. It only sends `get` and `put`. That is why the same validation logic can run under a strict handler in CI, a collecting handler in a documentation test, or a logging handler in a development shell. ## Type checking as an effect Validation uses the same shape. A type can expose both: - `.check value` for a fast boolean boundary. - `.validate value` for an effectful check with diagnostics. ```nix let inherit (fx.types) String refined; TargetClass = refined "TargetClass" String (x: builtins.elem x [ "module" "file" "package" "check" ]); in fx.run (TargetClass.validate "fleet") fx.effects.typecheck.collecting [] ``` The type sends a `typeCheck` request with context. The handler turns that request into an error list, a thrown exception, or a trace. The type does not need separate implementations for each policy. ## Composition Handlers compose because computations are ordinary values. `adapt` and `adaptHandlers` let a local computation run under a different handler view without rewriting the computation itself. Streams use the same substrate to request the next value lazily. Linear resources use it to count consumption. The kernel uses it to report type errors without placing diagnostics inside the trusted evaluator. The implementation details live in the [Trampoline](/nix-effects/internals/trampoline) chapter. For day-to-day use, the rule is simple: write computations in terms of operations, then choose the handler that matches the boundary where the computation runs. #### Typed Validation Typed validation is the first user-facing layer built on the effect model. A type describes the values a boundary accepts. The same type can answer a fast yes/no question or produce structured diagnostics through a handler. The point is not only that validation exists. In nix-effects, validation is derived from the type description. Users write the shape and any domain refinements; they do not write a recursive validator for every record, list, variant, or generated datatype. That derive-style capability is hosted inside Nix itself, not delegated to an external schema engine or macro system. This chapter uses value-level types. Later chapters show how generated datatypes carry reusable descriptions that the same checker, diagnostic tools, interpreters, and generic derivations consume. ## Primitive and refined types Primitive types wrap Nix value predicates with kernel-backed type information: ```nix let inherit (fx.types) Int String Bool; in { okInt = Int.check 3; okString = String.check "aspect"; badBool = Bool.check "true"; } ``` Refinement types narrow an existing type with a predicate: ```nix let inherit (fx.types) String refined; TargetClass = refined "TargetClass" String (x: builtins.elem x [ "module" "file" "package" "check" ]); in { ok = TargetClass.check "module"; bad = TargetClass.check "fleet"; } ``` Use refinements for named boundary conditions: target classes, non-empty names, supported dialects, allowed protocol versions, and similar concrete constraints. ## Structured values Records and lists compose types into larger boundaries: ```nix let inherit (fx.types) Record String ListOf refined; TargetClass = refined "TargetClass" String (x: builtins.elem x [ "module" "file" "package" "check" ]); AspectDecl = Record { name = String; target = TargetClass; requires = ListOf String; }; in AspectDecl.check { name = "workspace-shell"; target = "module"; requires = [ "toolchain" ]; } ``` The check is structural and derived. Each field is checked with its own type, and the composed type preserves enough context for diagnostics to point at the field that failed. Adding another field or nesting another record changes the type description; the validator follows automatically. ## Dependent records Some fields depend on earlier fields. `DepRecord` evaluates field types left to right, so later field types can inspect earlier values: ```nix let inherit (fx.types) Bool String ListOf DepRecord refined; TargetClass = refined "TargetClass" String (x: builtins.elem x [ "module" "file" "package" "check" ]); AspectDecl = DepRecord [ { name = "generated"; type = Bool; } { name = "target"; type = self: if self.generated then TargetClass else String; } { name = "requires"; type = _: ListOf String; } ]; in { ok = AspectDecl.checkFlat { generated = true; target = "module"; requires = [ "toolchain" ]; }; bad = AspectDecl.checkFlat { generated = true; target = "fleet"; requires = [ ]; }; } ``` The dependent field type is never evaluated until the fields it depends on have passed. That ordering keeps diagnostics local and avoids running domain logic on malformed input. ## Check vs validate Use `.check` when you need a boolean: ```nix AspectDecl.checkFlat value ``` Use `.validate` when the caller needs diagnostics: ```nix fx.run (AspectDecl.validate value) fx.effects.typecheck.collecting [] ``` The validation path reports through effects, so choosing a handler chooses policy. A CLI can collect every field error. CI can abort on the first fatal error. Tests can inspect the emitted diagnostics as data. This separates two concerns that are often mixed together in Nix DSLs: the type describes the accepted shape, while the handler decides whether a failed check becomes an exception, a list of diagnostics, or a trace. ## Where the kernel fits Every public type carries a kernel representation. Primitive and first-order types can use fast decidable checks. Higher-order and proof oriented boundaries use HOAS terms that the kernel checks directly. This is one model, not two. Value-level validation is the practical entry point; generated datatypes and verified functions reuse the same kernel-backed structure when the boundary needs more evidence than a predicate can provide. #### Generated Datatypes Generated datatypes make structure explicit. A datatype is not only a constructor API; it also carries the description and metadata that the kernel, generic walkers, diagnostics, schemas, dependency extraction, and ornaments reuse. Use generated datatypes when a domain value should be more than an untyped attrset. The generated value has constructors for building data, a type for checking data, and public structural evidence for tools. ## A first datatype ```nix let H = fx.types.hoas; Aspect = H.datatype "Aspect" [ (H.con "aspect" [ (H.field "name" H.string) (H.field "target" H.string) (H.field "requires" (H.listOf H.string)) ]) ]; in Aspect ``` The generated `Aspect` object exposes: - `Aspect.D` — the description used by the kernel. - `Aspect.T` — the generated type. - `Aspect.aspect` — the constructor. - `_dtypeMeta` — constructor and field metadata. The description is the important part. It is the common shape that lets multiple consumers agree on what an aspect declaration is. ## View and review Application code usually works with named constructor records: ```nix let G = fx.types.generic; aspect = G.value.review Aspect.T { _con = "aspect"; name = "workspace-shell"; target = "module"; requires = [ "toolchain" ]; }; in G.value.view Aspect.T aspect ``` `review` turns a constructor record into the generated value. `view` turns the generated value back into a constructor record. This is the bridge between ergonomic Nix data and the description-backed representation the kernel can inspect. ## Generated families The datatype layer supports several shapes: - `datatype` for ordinary generated datatypes. - `datatypeP` when constructors are parameterized. - `datatypeI` when the family is indexed. - `datatypePI` when both parameters and indices are needed. The more indexed the family, the more precisely the type can state what the value means. The public model is still the same: a description, a generated type, constructors, and metadata. ## Stack-safe elaboration Generated constructor trees can be large. nix-effects elaborates them through the same stack-safe machinery used by the effect interpreter and kernel normalizer. Deep values should remain ordinary data, not a reason to avoid the generated layer. That is why generated datatypes are suitable for infrastructure DSLs: they give the checker real structure without moving the user out of pure Nix evaluation. ## What comes next The next chapter shows what the metadata buys. Once a datatype carries a description, generic tools can derive schemas, dependency graphs, diagnostics, and ornaments without each tool inventing its own walker. #### Generic Programming Generic programming is the payoff for description-backed data. Once a datatype exposes its constructors, fields, and description, tools can consume that structure directly instead of maintaining hand-written walkers for every domain shape. The same generated datatype can feed a checker, a documentation surface, a dependency extractor, a schema generator, and an ornament. Those tools agree because they read the same metadata. This is the consequence of hosting the typed description language inside Nix. There is no separate schema file to keep synchronized with the DSL, and no per-datatype validator to write. A new user datatype participates because the generic programs consume the levitated description, not a closed set of special cases known in advance. ## Inspecting metadata ```nix let G = fx.types.generic; info = G.datatype.datatypeInfo Aspect.T; in { name = info.name; constructors = map (c: c.name) info.constructors; } ``` `datatypeInfo` is the stable entry point for generated datatype metadata. It returns the public structure: datatype name, constructors, field names, field types, and description data needed by derivations. ## Deriving descriptors `deriveDescriptor` turns datatype metadata into a neutral structural description suitable for tools that do not want to depend on the HOAS representation directly: ```nix let G = fx.types.generic; in G.derive.deriveDescriptor Aspect.T ``` A descriptor is not a second source of truth. It is a view of the datatype's existing structure. ## Deriving schemas Schema derivation uses the same fields and constructor tags: ```nix G.derive.deriveSchema Aspect.T ``` Use this for external validation surfaces, generated documentation, or tooling that needs to explain the shape to systems outside the kernel. The schema follows the datatype; changing the datatype changes the derived schema. ## Deriving dependencies Dependency extraction is another consumer of the same shape: ```nix G.derive.deriveDeps Aspect.T aspect ``` This is useful when a domain value references other values by name or path. For an aspect declaration, the `requires` field can become the dependency edge list for a topological interpreter. The generic walker handles traversal; domain-specific metadata decides which fields count as references. ## Diagnostics from shape Structured diagnostics become easier when every field has a stable path. The validator can report: ```nix [ "constructors" "aspect" "fields" "requires" 0 ] ``` instead of a string-only error. That path is reusable in CLI output, HTML documentation, editor integrations, and test assertions. The path is not guessed by a custom recursive function. It is produced by the same generic descent that reads constructor and field metadata. ## Avoid ad hoc walkers The design rule is direct: if a tool needs to traverse a generated datatype, start from `datatypeInfo`, `view`, `review`, and the derive helpers. Do not duplicate the datatype's shape in a separate hand-written walker. The description is already the shared structure. The ornaments chapter builds on this. An ornament refines one generated shape into another while preserving a forgetful map, and functional ornaments add a canonical way to rebuild the enriched value. #### Ornaments and Description-Backed Data Generated datatypes in nix-effects are description-backed. A datatype is not only a bundle of constructors; it also carries a `Desc` tree and metadata that generic tools can inspect. That is what makes the same datatype usable by the checker, value walkers, schema derivation, dependency extraction, and ornaments. An ornament refines one generated datatype into another while preserving a forgetful map back to the base datatype. Use it when one layer needs more information than another layer, but the enriched value should still be usable wherever the base value was expected. ## Description-backed datatypes Start with an ordinary generated datatype: ```nix let fx = import ./nix/nix-effects {}; H = fx.types.hoas; G = fx.types.generic; Aspect = H.datatype "Aspect" [ (H.con "aspect" [ (H.field "name" H.string) (H.field "target" H.string) (H.field "requires" (H.listOf H.string)) ]) ]; in Aspect ``` `Aspect` exposes: - `Aspect.D`: the description used by the kernel. - `Aspect.T`: the generated type. - `Aspect.aspect`: the constructor. - `_dtypeMeta`: constructor and field metadata used by generic tools. The generic layer is the usual interface for description-backed programming: ```nix let info = G.datatype.datatypeInfo Aspect.T; value = G.value.review Aspect.T { _con = "aspect"; name = "workspace-shell"; target = "module"; requires = [ "toolchain" ]; }; in { constructors = map (c: c.name) info.constructors; roundtrip = G.value.view Aspect.T value; } ``` `review` turns a constructor record into a HOAS value. `view` turns the HOAS value back into a named constructor record. Derivation helpers such as `G.derive.deriveDescriptor`, `G.derive.deriveSchema`, and `G.derive.deriveDeps` consume the same metadata. ## Plain ornaments A plain ornament adds fields while retaining enough structure to forget back to the base datatype: ```nix let ResolvedAspect = G.ornaments.ornament Aspect { name = "ResolvedAspect"; constructors.aspect.fields = [ { keep = "name"; } { keep = "target"; } { keep = "requires"; } { insert = "resolvedPath"; type = H.string; } ]; }; resolved = G.value.review ResolvedAspect.T { _con = "aspect"; name = "workspace-shell"; target = "module"; requires = [ "toolchain" ]; resolvedPath = "modules/workspace-shell.nix"; }; in G.value.view Aspect.T (G.ornaments.forget ResolvedAspect resolved) ``` The result is: ```nix { _con = "aspect"; name = "workspace-shell"; target = "module"; requires = [ "toolchain" ]; } ``` The important property is one-way. Many resolved aspects may forget to the same aspect. The ornament says how to drop the inserted structure without changing the base shape. Plain ornaments are useful when the enriched value already exists. They give you checked forgetful maps, composition, and pullback transport: ```nix G.ornaments.forget ResolvedAspect resolved G.ornaments.compose Outer Inner G.ornaments.pullback ResolvedAspect baseFunction resolved ``` ## Functional ornaments A functional ornament adds the forward direction: a section of the forgetful map. ```nix section : base -> ornamented forget (section x) = x ``` In the generic API, build a functional ornament from a base datatype, an ornament spec, and explicit synthesis functions for inserted fields: ```nix let ResolvedAspect = G.ornaments.functional { base = Aspect; spec = { name = "ResolvedAspect"; constructors.aspect.fields = [ { keep = "name"; } { keep = "target"; } { keep = "requires"; } { insert = "resolvedPath"; type = H.string; } ]; }; synth.constructors.aspect.fields.resolvedPath = ctx: "modules/${ctx.baseRecord.name}.nix"; }; aspect = G.value.review Aspect.T { _con = "aspect"; name = "workspace-shell"; target = "module"; requires = [ "toolchain" ]; }; resolved = G.ornaments.build ResolvedAspect aspect; in { enriched = G.value.view ResolvedAspect.meta.ornamented.T resolved; forgotten = G.value.view Aspect.T (G.ornaments.forget ResolvedAspect resolved); } ``` The enriched record contains the synthesized resolvedPath: ```nix { _con = "aspect"; name = "workspace-shell"; target = "module"; requires = [ "toolchain" ]; resolvedPath = "modules/workspace-shell.nix"; } ``` The forgotten record is the original aspect. This gives a practical workflow: construct the small base value first, then let the functional ornament add canonical metadata. ## Composing enrichments Functional ornaments compose when the outer ornament starts from the inner ornament's generated datatype: ```nix let MaterializedAspect = G.ornaments.functional { base = ResolvedAspect.meta.ornamented; spec = { name = "MaterializedAspect"; constructors.aspect.fields = [ { keep = "name"; } { keep = "target"; } { keep = "requires"; } { keep = "resolvedPath"; } { insert = "order"; type = H.nat; } ]; }; synth.constructors.aspect.fields.order = _ctx: 1; }; Materialized = G.ornaments.composeFunctional MaterializedAspect ResolvedAspect; in G.ornaments.build Materialized aspect ``` The composed section builds the final enriched value directly from the base aspect. Forgetting the result through the composed ornament returns the base aspect. ## Lifting producers and transforms Producer lifting runs a base producer, then enriches its output through the functional section: ```nix let makeAspect = H.ann (H.lam "aspect" Aspect.T (_: G.value.review Aspect.T { _con = "aspect"; name = "workspace-shell"; target = "module"; requires = [ "toolchain" ]; })) (H.forall "_" Aspect.T (_: Aspect.T)); lifted = G.ornaments.liftProducer Materialized makeAspect aspect; in G.value.view MaterializedAspect.meta.ornamented.T lifted ``` The base function still only knows about `Aspect`. The lifted producer returns the canonical materialized aspect. Forgetting the lifted result gives the base producer's result. Transform lifting is similar, but starts from an ornamented input: ```nix G.ornaments.liftTransform { input = ResolvedAspect; output = MaterializedAspect; fn = baseTransform; } resolved ``` The input is forgotten to the base, `fn` runs on the base value, and the output section rebuilds the canonical enriched result. ## Obligations and diagnostics Inserted fields must have a builder, a declared measure, or an explicit proof builder. Validation is total and returns structured diagnostics: ```nix G.ornaments.validateFunctional { base = Aspect; spec = { name = "BrokenResolvedAspect"; constructors.aspect.fields = [ { keep = "name"; } { keep = "target"; } { keep = "requires"; } { insert = "resolvedPath"; type = H.string; } ]; }; synth = {}; } ``` The diagnostic identifies the missing field builder: ```nix { ok = false; diagnostics = [{ code = "functional.missing-builder"; path = [ "functional" "constructors" "aspect" "fields" "resolvedPath" ]; message = "inserted field needs builder"; severity = "error"; }]; } ``` Proof-marked inserted fields use `proof = true` or `role = "proof"` and report `functional.unresolved-proof` until a proof builder is supplied. Measure-derived inserted fields use `measure = ""` and require a matching entry under `measures`. ## Algebraic ornaments Algebraic ornaments are the case where inserted indices or fields are computed from a fold over the base value. The canonical example is ornamenting a list into a vector by inserting its length. The public helper is `G.ornaments.algOrn`. It validates the supported description/algebra fragment before constructing the ornament. The current supported fragment covers `ret`, `arg`, `rec`, keep-only `pi`, and `plus`. Unsupported function-domain aggregation remains explicit: validation reports a shape diagnostic instead of guessing an algebra. Use algebraic ornaments when the extra information is determined by the base structure itself. Use functional ornaments when the extra information comes from a user-provided synthesis function, proof builder, or declared measure. ## Ornament composition Plain, functional, and algebraic ornaments all refine a single base datatype at the whole-type level. Three additional constructions refine ornaments themselves, so the refinement *at one field position* can be an ornament rather than the identity: - **Leaf functional ornaments** lift a Nix-meta function `f : B → A` into an ornament of a primitive HOAS type former. They are the base case for refinements on non-μ types (`H.string`, `H.derivation`, `H.thunk`, …) where there is no constructor structure to walk. - **Functorial container lifts** carry an ornament `O : Ornament A A'` through a strictly-positive container functor (`List`, `Attrs`, `Maybe`, or a single record field) to produce an ornament between the lifted containers, with forget defined by the standard functorial action. - **Sub-ornament composition at `keep`** is the spec verb `{ keep = "x"; sub = O; }` inside a `G.ornaments.ornament` call. It declares that the kept field carries the *refined* type at the source and forgets through `O.forget` at that field on the way to the base. These compose: a `keep + sub` may use a `lift.list` of a `thunkOrnament`, and so on. ### Leaf functional ornaments A leaf functional ornament refines a primitive HOAS type former by attaching a Nix-meta function pair `(forget, section)`. Construction requires the primitive to satisfy `_htag ≠ "mu"`; μ-encoded bases use `H.ornament` or `H.functionalOrnament` instead. ```nix let fx = import ./nix/nix-effects {}; H = fx.types.hoas; IdString = H.leafOrnament { primitive = H.string; forget = x: x; section = x: x; }; in IdString.forget "hello" ``` The identity is the trivial case; the construction earns its keep when `forget` is a non-trivial Nix function such as `forceThunk`. The literature anchor is Dagand and McBride, *Transporting Functions across Ornaments* (JFP 2014), which gives the general construction of a functional ornament over `A` induced by a function `f : B → A`. The leaf case specialises that construction to primitive type formers. The derived constructor `H.thunkOrnament` packages the lift of `forceThunk : Thunk a → a` and its section `mkThunk : a → Thunk a`: ```nix H.thunkOrnament inner ≡ H.leafOrnament { primitive = H.thunk inner; forget = fx.state.thunk.forceThunk; section = fx.state.thunk.mkThunk; meta = { name = "Thunk"; baseHtag = "thunk"; inner = inner; }; } ``` The `Thunk` carrier is the kernel's deepSeq-safe wrapper for transport of cyclic or otherwise force-unsafe values through handler state; see `src/state/thunk.nix` for the carrier shape and discipline. `H.thunk` is the leaf functional ornament whose forget is `forceThunk` — the ornament algebra is the principled home for the `Thunk → a` story that the runtime already follows. ### Functorial container lifts Given an ornament `O : Ornament A A'`, each strictly-positive container functor `F` extends `O` to an ornament `F(O) : Ornament F(A) F(A')` whose forget is `F`'s functorial action on `O.forget`. The kernel exposes four lifts under `G.ornaments.lift`: ```nix G.ornaments.lift.list : Ornament A A' -> Ornament (List A) (List A') G.ornaments.lift.attrs : Ornament A A' -> Ornament (Attrs A) (Attrs A') G.ornaments.lift.maybe : Ornament A A' -> Ornament (Maybe A) (Maybe A') G.ornaments.lift.field : String -> Ornament A A' -> Ornament Record Record' ``` Forget is `map o.forget`, `mapAttrs (_: o.forget)`, the null-passthrough of `o.forget`, and the single-field rewrite `r // { ${name} = o.forget r.${name}; }` respectively. Section mirrors forget on the dual side. ```nix let fx = import ./nix/nix-effects {}; H = fx.types.hoas; G = fx.types.generic; drvA = { type = "derivation"; name = "a"; outPath = "/nix/store/a"; }; drvB = { type = "derivation"; name = "b"; outPath = "/nix/store/b"; }; thunked = H.thunkOrnament H.derivation; paths = G.ornaments.lift.list thunked; in paths.forget [ (thunked.section drvA) (thunked.section drvB) ] # ≡ [ drvA drvB ] ``` The literature anchor is Dagand, *A Cosmology of Datatypes* (PhD thesis, Strathclyde 2013), chapter 6 "Functoriality of refinements." The four lifts are siblings, not derived from one another at the kernel boundary: each is the functorial action of an ornament along the corresponding container functor. `lift.field` is the elementary product-component move; the other three are the analogous moves for `List`, `Attrs`, and `Maybe`. The container lifts decompose through `lift.field` via the μ-encoding of containers, but the kernel ships them all directly so consumers do not pay μ-unfolding cost at every use site. ### Sub-ornament composition at `keep` The spec language of `G.ornaments.ornament` recognises `{ keep = "fieldName"; sub = O; }` as a directive that the named field carries `O`'s *refined* type on the ornamented side and forgets through `O.forget` at that field. The plain `{ keep = "fieldName"; }` form remains the categorical identity at the kept position; the `sub` verb adds the refinement. ```nix let fx = import ./nix/nix-effects {}; H = fx.types.hoas; G = fx.types.generic; drv = { type = "derivation"; name = "x"; outPath = "/nix/store/x"; }; Box = H.datatype "Box" [ (H.con "box" [ (H.field "value" H.derivation) ]) ]; TaggedThunkBox = G.ornaments.ornament Box { name = "TaggedThunkBox"; constructors.box.fields = [ { keep = "value"; sub = H.thunkOrnament H.derivation; } { insert = "tag"; type = H.bool; } ]; }; state = { _con = "box"; value = (H.thunkOrnament H.derivation).section drv; tag = true; }; in G.ornaments.forget TaggedThunkBox state # ≡ { _con = "box"; value = drv; } ``` The forget walker copies plain `keep` fields, drops `insert` fields, and dispatches `keep + sub` fields through the sub-ornament's forget. Constructor `_con` is preserved because the kernel reuses the base constructor's name for the refined branch. Sub-type agreement is checked at spec time. A `sub` whose forward type disagrees with the kept field's type produces an `ornament.sub-type-mismatch` diagnostic; a `sub` that is not a recognised ornament produces `ornament.sub-not-ornament`. ### A single forget, two evaluations `G.ornaments.forget : Ornamented → Value → Value` is one morphism. The HOAS-term path and the Nix-meta path are two evaluations of the same morphism; the commutation `eval (forgetHoas t) ≡ forgetMeta (eval t)` is a theorem of the framework. Dispatch is structural on the well-formed sum `_htag` (HOAS term) ⊕ `_con` (Nix-meta μ-value): ```nix G.ornaments.forget ornamented value # value._htag -> HOAS-term path (returns HOAS-applied forget) # value._con -> Nix-meta path (returns Nix attrset) # both / neither / non-attrset -> throws ``` Both internal paths are reachable as `G.ornaments._internal.{forgetHoas, forgetMeta}` for tests that need to pin the evaluator, but application code calls `G.ornaments.forget` and lets the dispatcher pick. ## API summary HOAS layer: ```nix H.ornament base spec H.ornDesc ornament H.ornForget ornament H.ornCompose outer inner H.ornPullback ornament resultTy baseFn H.ornLiftFold ornament resultTy baseFold H.functionalOrnament { ornament; chooseIndex; section; ... } H.ornBuild functional index baseValue H.ornLiftProducer functional baseFn index baseInput H.ornLiftTransform { input; output; fn; } outputIndex inputIndex ornamentedInput H.leafOrnament { primitive; forget; section; sectionProof?; meta?; } H.thunkOrnament inner ``` Generic layer: ```nix G.ornaments.ornament base spec G.ornaments.forget ornamented value G.ornaments.compose outer inner G.ornaments.pullback ornamented baseFn value G.ornaments.functional { base; spec; synth; ... } G.ornaments.validateFunctional { base; spec; synth; ... } G.ornaments.build functional baseValue G.ornaments.composeFunctional outer inner G.ornaments.liftProducer functional baseFn baseInput G.ornaments.liftTransform { input; output; fn; } ornamentedInput G.ornaments.lift.list o G.ornaments.lift.attrs o G.ornaments.lift.maybe o G.ornaments.lift.field name o ``` The generic layer is the best default for application code. Drop to HOAS when you need direct indexed terms, explicit proof terms, or kernel-level transport. #### Proof Guide Nix DSL values are concrete at eval time. Every declaration, field, and dependency edge is known before anything builds. The nix-effects dependent type checker exploits this: it normalizes both sides of an equation via NbE, and if they reduce to the same value, `Refl` proves them equal. No symbolic reasoning, no induction over unknowns — just computation on concrete data, checked through the freer-monad effect layer in pure Nix. This chapter builds proofs incrementally, from `0 + 0 = 0` through the J eliminator to verified extraction of plain Nix functions from kernel-checked HOAS terms. Every example is runnable. The code comes from three files in the repository: [`proof-basics.nix`](https://github.com/kleisli-io/nix-effects/blob/main/examples/proof-basics.nix), [`equality-proofs.nix`](https://github.com/kleisli-io/nix-effects/blob/main/examples/equality-proofs.nix), and [`verified-functions.nix`](https://github.com/kleisli-io/nix-effects/blob/main/examples/verified-functions.nix). **Prerequisites.** You should know what a function is and what `let` bindings do in Nix. Familiarity with the Getting Started chapter helps but isn't required. You do not need to know type theory. ## Your first proof A proof in nix-effects is a term that type-checks against an equality type. The simplest equality type is `Eq(Nat, 0+0, 0)` — the claim that adding zero to zero produces zero. The proof term is `Refl`, which says "both sides are the same." The kernel checks this by normalizing `0 + 0`, arriving at `0`, and confirming that `Refl` witnesses `0 = 0`. ```nix let H = fx.types.hoas; inherit (H) nat eq zero refl checkHoas; # Addition by structural recursion on the first argument add = m: n: H.ind (H.lam "_" nat (_: nat)) n (H.lam "k" nat (_: H.lam "ih" nat (ih: H.succ ih))) m; in # Prove: 0 + 0 = 0 (checkHoas (eq nat (add zero zero) zero) refl).tag == "refl" # → true ``` `checkHoas` is the kernel's entry point. It takes a type and a term, runs bidirectional type checking with normalization by evaluation, and returns a result. If the result's `tag` is `"refl"`, the proof was accepted. If it has an `error` field, the kernel rejected it. The kernel doesn't pattern-match on `0 + 0 = 0` as a special case. It evaluates `add(zero, zero)` by running the generated natural eliminator behind `H.ind` — the base case fires, returns `n` (which is `zero`), and the kernel sees `Eq(Nat, zero, zero)`. `Refl` witnesses any `Eq(A, x, x)`, so the proof goes through. Larger numbers work the same way. The kernel unrolls the recursion: ```nix # 3 + 5 = 8 (checkHoas (eq nat (add (H.natLit 3) (H.natLit 5)) (H.natLit 8)) refl).tag == "refl" # 10 + 7 = 17 (checkHoas (eq nat (add (H.natLit 10) (H.natLit 7)) (H.natLit 17)) refl).tag == "refl" ``` Both reduce to `true`. The kernel normalizes `add(3, 5)` step by step — three `succ` peels, then the base case returns `5`, then three `succ` wrappers are reapplied — and confirms the result is `8`. ## Dependent witnesses A computational equality says "these two things are the same." A dependent witness says "here is a value, and here is evidence that it has a property." The Sigma type `Σ(x:A).P(x)` packages both: a value `x` of type `A`, and a proof that `P(x)` holds. ```nix let H = fx.types.hoas; inherit (H) nat eq sigma zero pair refl checkHoas; in { # "There exists x : Nat such that x = 0" — witnessed by (0, Refl) witness = let ty = sigma "x" nat (x: eq nat x zero); tm = pair zero refl; in (checkHoas ty tm).tag == "pair"; # → true } ``` The type `Σ(x:Nat). Eq(Nat, x, 0)` says "a natural number equal to zero." The term `(0, Refl)` inhabits it: `0` for the value, `Refl` for the proof that `0 = 0`. The kernel checks both components — it confirms `0 : Nat` and `Refl : Eq(Nat, 0, 0)`. Witnesses get more interesting when the property involves computation: ```nix # "There exists x such that 3+5 = x" — witnessed by (8, Refl) witnessAdd = let add = m: n: H.ind (H.lam "_" nat (_: nat)) n (H.lam "k" nat (_: H.lam "ih" nat (ih: H.succ ih))) m; ty = sigma "x" nat (x: eq nat (add (H.natLit 3) (H.natLit 5)) x); tm = pair (H.natLit 8) refl; in (checkHoas ty tm).tag == "pair"; ``` The kernel normalizes `add(3, 5)` to `8`, checks that `8` matches the witness value, and accepts the proof. If you claimed the witness was `7`, the kernel would reject it — `Refl` can't witness `8 = 7`. ## Eliminators Eliminators are how you compute over inductive types in type theory. Where Nix uses `if`/`else` and list folds, the kernel uses eliminators: structured recursion with a *motive* that declares what type the result has. The motive is what makes these dependently typed — the return type can vary based on the input. ### Booleans `H.boolElim k motive trueCase falseCase scrutinee` — case analysis on a derived boolean. `H.bool` is `μ ⊤ (plus (retI tt) (retI tt)) tt`, and `H.boolElim` is defined in terms of `desc-ind` on that description (see `src/tc/hoas/combinators.nix`). The user-facing behavior is the standard boolean eliminator: with a constant motive (return type doesn't depend on the boolean), it's equivalent to an if/else. ```nix let H = fx.types.hoas; inherit (H) nat bool eq zero refl boolElim checkHoas; in { # if true then 42 else 0 = 42 trueCase = let result = boolElim 0 (H.lam "_" bool (_: nat)) (H.natLit 42) zero H.true_; in (checkHoas (eq nat result (H.natLit 42)) refl).tag == "refl"; # if false then 42 else 0 = 0 falseCase = let result = boolElim 0 (H.lam "_" bool (_: nat)) (H.natLit 42) zero H.false_; in (checkHoas (eq nat result zero) refl).tag == "refl"; } ``` ### Natural numbers `H.ind k motive base step n` — structural recursion over the generated natural datatype. The base case handles zero, the step case takes the predecessor `k` and the inductive hypothesis `ih` (the result for `k`) and produces the result for `S(k)`: ```nix let H = fx.types.hoas; inherit (H) nat eq refl checkHoas; # double(n): double(0) = 0, double(S(k)) = S(S(double(k))) double = n: H.ind (H.lam "_" nat (_: nat)) H.zero (H.lam "k" nat (_: H.lam "ih" nat (ih: H.succ (H.succ ih)))) n; in # double(4) = 8 (checkHoas (eq nat (double (H.natLit 4)) (H.natLit 8)) refl).tag == "refl" ``` The kernel unrolls four steps: `double(4) = S(S(double(3))) = ... = 8`. ### Lists `H.listElim k elemType motive nilCase consCase list` — structural recursion over the generated list datatype. The nil case provides the base value, the cons case takes the head, tail, and inductive hypothesis: ```nix let H = fx.types.hoas; inherit (H) nat eq refl checkHoas; list123 = H.cons nat (H.natLit 1) (H.cons nat (H.natLit 2) (H.cons nat (H.natLit 3) (H.nil nat))); # sum(xs): fold with addition sumList = xs: H.listElim nat (H.lam "_" (H.listOf nat) (_: nat)) H.zero (H.lam "h" nat (h: H.lam "t" (H.listOf nat) (_: H.lam "ih" nat (ih: H.ind (H.lam "_" nat (_: nat)) ih (H.lam "k" nat (_: H.lam "ih2" nat (ih2: H.succ ih2))) h)))) xs; in # sum([1, 2, 3]) = 6 (checkHoas (eq nat (sumList list123) (H.natLit 6)) refl).tag == "refl" ``` ### Sums (coproducts) `H.sumElim k L R motive leftCase rightCase scrutinee` — case analysis over the generated sum datatype: ```nix let H = fx.types.hoas; inherit (H) nat bool sum eq zero refl checkHoas; in { # case Left(5) of { Left n → n; Right _ → 0 } = 5 leftCase = let scrut = H.inl nat bool (H.natLit 5); result = H.sumElim nat bool (H.lam "_" (sum nat bool) (_: nat)) (H.lam "n" nat (n: n)) (H.lam "b" bool (_: zero)) scrut; in (checkHoas (eq nat result (H.natLit 5)) refl).tag == "refl"; } ``` ## The J eliminator Everything above uses `Refl` on equalities that the kernel verifies by computation — normalize both sides, confirm they match. But what if you want to reason *about* equalities? Prove that equality is symmetric, or that applying a function to equal inputs gives equal outputs? That requires the J eliminator, the fundamental proof principle for identity types in Martin-Löf type theory [1]. J says: if you can prove something about `x = x` (the reflexive case), you can prove it about any `x = y` where the equality is witnessed. ``` J(A, a, P, pr, b, eq) A : type a : left side of the equality P : λ(y:A). λ(_:Eq(A,a,y)). Type — the motive pr : P(a, refl) — the base case (when y = a) b : right side eq : Eq(A, a, b) — proof that a = b Returns: P(b, eq) Computation rule: J(A, a, P, pr, a, refl) = pr ``` When the equality proof is `Refl`, J returns the base case directly. The kernel reduces `J(..., refl)` to `pr`, and the proof goes through. ### Congruence If `x = y`, then `f(x) = f(y)` for any function `f`. This is the standard *cong* combinator, derived from J: ```nix let H = fx.types.hoas; inherit (H) nat eq u forall refl checkHoas; congType = forall "A" (u 0) (a: forall "B" (u 0) (b: forall "f" (forall "_" a (_: b)) (f: forall "x" a (x: forall "y" a (y: forall "_" (eq a x y) (_: eq b (H.app f x) (H.app f y))))))); congTerm = H.lam "A" (u 0) (a: H.lam "B" (u 0) (b: H.lam "f" (forall "_" a (_: b)) (f: H.lam "x" a (x: H.lam "y" a (y: H.lam "p" (eq a x y) (p: H.j a x (H.lam "y'" a (y': H.lam "_" (eq a x y') (_: eq b (H.app f x) (H.app f y')))) refl y p)))))); in (checkHoas congType congTerm).tag == "lam" ``` The derivation: J eliminates the proof `p : Eq(A, x, y)`. The motive says "given `y'` equal to `x`, produce `Eq(B, f(x), f(y'))`." In the base case, `y' = x`, so the goal is `Eq(B, f(x), f(x))` — which `Refl` proves. J then transports this to `Eq(B, f(x), f(y))`. This generic combinator type-checks with abstract variables `A`, `B`, `f`, `x`, `y` — the kernel verifies the reasoning is valid for all inputs. On concrete data, J receives `Refl` (since concrete equalities reduce by computation), and the kernel simplifies: ```nix # Concrete: from add(2,1) = 3, derive succ(add(2,1)) = succ(3) congConcrete = let add21 = add (H.natLit 2) (H.succ H.zero); three = H.natLit 3; in (checkHoas (eq nat (H.succ add21) (H.succ three)) (H.j nat add21 (H.lam "y" nat (y: H.lam "_" (eq nat add21 y) (_: eq nat (H.succ add21) (H.succ y)))) refl three refl)).tag == "j"; ``` ### Symmetry If `x = y`, then `y = x`. The motive is `λy'.λ_. Eq(A, y', x)` — when `y' = x`, the goal is `Eq(A, x, x)`, proved by `Refl`: ```nix # sym : Π(A:U₀). Π(x:A). Π(y:A). Eq(A,x,y) → Eq(A,y,x) symTerm = H.lam "A" (u 0) (a: H.lam "x" a (x: H.lam "y" a (y: H.lam "p" (eq a x y) (p: H.j a x (H.lam "y'" a (y': H.lam "_" (eq a x y') (_: eq a y' x))) refl y p)))); ``` ### Transitivity If `x = y` and `y = z`, then `x = z`. Fix `p : Eq(A, x, y)`, then eliminate `q` with J. The motive is `λz'.λ_. Eq(A, x, z')` — when `z' = y`, the goal is `Eq(A, x, y)`, proved by `p`: ```nix # trans : Π(A:U₀). Π(x:A). Π(y:A). Π(z:A). Eq(A,x,y) → Eq(A,y,z) → Eq(A,x,z) transTerm = H.lam "A" (u 0) (a: H.lam "x" a (x: H.lam "y" a (y: H.lam "z" a (z: H.lam "p" (eq a x y) (p: H.lam "q" (eq a y z) (q: H.j a y (H.lam "z'" a (z': H.lam "_" (eq a y z') (_: eq a x z'))) p z q)))))); ``` ### Transport The most general form. If `x = y` and `P(x)` holds, then `P(y)` holds. Congruence, symmetry, and transitivity are all special cases. ```nix # transport : Π(A:U₀). Π(P:A→U₀). Π(x:A). Π(y:A). Eq(A,x,y) → P(x) → P(y) transportTerm = H.lam "A" (u 0) (a: H.lam "P" (forall "_" a (_: u 0)) (bigP: H.lam "x" a (x: H.lam "y" a (y: H.lam "p" (eq a x y) (p: H.lam "px" (H.app bigP x) (px: H.j a x (H.lam "y'" a (y': H.lam "_" (eq a x y') (_: H.app bigP y'))) px y p)))))); ``` ### Chaining proofs J applications compose. Here we chain congruence (lift through `succ`) with symmetry (reverse the equality) — two J applications, the output of the first feeding as the equality proof to the second: ```nix # From Eq(Nat, add(2,1), 3): # Step 1 (cong succ): Eq(Nat, S(add(2,1)), S(3)) # Step 2 (sym): Eq(Nat, S(3), S(add(2,1))) combinedProof = let add21 = add (H.natLit 2) (H.succ H.zero); three = H.natLit 3; sadd21 = H.succ add21; sthree = H.succ three; # Step 1: cong succ congStep = H.j nat add21 (H.lam "y" nat (y: H.lam "_" (eq nat add21 y) (_: eq nat sadd21 (H.succ y)))) refl three refl; # Step 2: sym on the cong result in (checkHoas (eq nat sthree sadd21) (H.j nat sadd21 (H.lam "y" nat (y: H.lam "_" (eq nat sadd21 y) (_: eq nat y sadd21))) refl sthree congStep)).tag == "j"; ``` ## Verified extraction Proofs establish that properties hold. Verified extraction goes further: write an implementation in HOAS, the kernel type-checks it against a specification, and `v.verify` extracts a callable Nix function. The result is an ordinary Nix value — an integer, a boolean, a function, a list — but one whose implementation was machine-checked before use. ### The simplest case ```nix let H = fx.types.hoas; v = fx.types.verified; # Kernel-verified successor: Nat → Nat succFn = v.verify (H.forall "x" H.nat (_: H.nat)) (v.fn "x" H.nat (x: H.succ x)); in succFn 5 # → 6 ``` `v.verify` does three things: elaborates the HOAS into kernel terms, type-checks the implementation against the type, and extracts the result as a Nix value. The extracted function is plain Nix — no kernel overhead at call time. `v.fn` is a convenience wrapper around `H.lam` that threads the extraction metadata. You could write raw `H.lam` instead, but `v.fn` handles the plumbing for multi-argument functions and pattern matching. ### Pattern matching `v.match` builds an `H.ind` with a constant motive. You provide the result type, the scrutinee, and branches for `zero` and `succ`: ```nix # Verified addition: Nat → Nat → Nat addFn = v.verify (H.forall "m" H.nat (_: H.forall "n" H.nat (_: H.nat))) (v.fn "m" H.nat (m: v.fn "n" H.nat (n: v.match H.nat m { zero = n; succ = _k: ih: H.succ ih; }))); addFn 2 3 # → 5 addFn 0 7 # → 7 ``` The `succ` branch receives two arguments: `k` (the predecessor) and `ih` (the inductive hypothesis — the result for `k`). For addition, `ih` is `add(k, n)`, so wrapping it with `succ` gives `add(S(k), n)`. ### Boolean and cross-type elimination `v.if_` elaborates to `H.boolElim` on the derived `H.bool`: ```nix # Verified not: Bool → Bool notFn = v.verify (H.forall "b" H.bool (_: H.bool)) (v.fn "b" H.bool (b: v.if_ H.bool b { then_ = v.false_; else_ = v.true_; })); notFn true # → false notFn false # → true ``` Cross-type elimination — scrutinize one type, return another — works by specifying a different result type: ```nix # Verified isZero: Nat → Bool isZeroFn = v.verify (H.forall "n" H.nat (_: H.bool)) (v.fn "n" H.nat (n: v.match H.bool n { zero = v.true_; succ = _k: _ih: v.false_; })); isZeroFn 0 # → true isZeroFn 5 # → false ``` ### List operations `v.map`, `v.filter`, and `v.fold` are verified list combinators. Each takes HOAS terms, not Nix functions — the kernel verifies the entire pipeline: ```nix # Composed pipeline: filter zeros, then sum # Input: [0, 3, 0, 2, 1] → Filter: [3, 2, 1] → Sum: 6 composedResult = let input = H.cons H.nat (v.nat 0) (H.cons H.nat (v.nat 3) (H.cons H.nat (v.nat 0) (H.cons H.nat (v.nat 2) (H.cons H.nat (v.nat 1) (H.nil H.nat))))); nonZero = v.fn "n" H.nat (n: v.match H.bool n { zero = v.false_; succ = _k: _ih: v.true_; }); addCombine = v.fn "a" H.nat (a: v.fn "acc" H.nat (acc: v.match H.nat a { zero = acc; succ = _k: ih: H.succ ih; })); in v.verify H.nat (v.fold H.nat H.nat (v.nat 0) addCombine (v.filter H.nat nonZero input)); # → 6 ``` The kernel verifies the filter predicate (`Nat → Bool`), the fold combinator (`Nat → Nat → Nat`), and their composition before extracting the result. A type error in any component — say, returning a `Nat` where the filter expects a `Bool` — fails at `nix eval`, not at runtime. ### Aspect declarations and string operations The kernel supports record types (elaborated as nested Sigma) and string equality (`strEq` is a kernel primitive). Together they verify functions over the same aspect declarations introduced earlier: ```nix let H = fx.types.hoas; v = fx.types.verified; AspectDecl = H.record [ { name = "name"; type = H.string; } { name = "target"; type = H.string; } { name = "requires"; type = H.listOf H.string; } ]; targets = H.cons H.string (v.str "module") (H.cons H.string (v.str "file") (H.cons H.string (v.str "package") (H.cons H.string (v.str "check") (H.nil H.string)))); validateAspect = v.verify (H.forall "a" AspectDecl (_: H.bool)) (v.fn "a" AspectDecl (a: v.strElem (v.field AspectDecl "target" a) targets)); in { ok = validateAspect { name = "workspace-shell"; target = "module"; requires = [ "toolchain" ]; }; # → true bad = validateAspect { name = "workspace-aspect"; target = "fleet"; requires = [ ]; }; # → false } ``` `v.field` desugars to the right chain of `fst`/`snd` projections for the field's position in the Sigma chain. `v.strEq` reduces in the kernel via the `StrEq` primitive — it compares string literals during normalization, producing `true` or `false` as kernel values. `v.strElem` folds that primitive over a generated list of allowed target classes. ## What the kernel can and cannot prove The nix-effects kernel implements Martin-Löf type theory with universes, dependent functions, dependent pairs, identity types, natural numbers, lists, sums, unit, the empty type, an indexed- description family (`Desc I`, `μ`, `desc-ind`), and seven axiomatized Nix primitives (String, Int, Float, Attrs, Path, Function, Any). Booleans are derived — `H.bool` as `μ ⊤ (plus (retI tt) (retI tt)) tt` with eliminator `H.boolElim` via `desc-ind`. `Empty` is a kernel primitive (the initial-object dual of `Unit`) with eliminator `H.absurd : Π(P:U(k)). Empty → P`. The kernel can prove any property that reduces to a comparison of normal forms. **It can prove:** - Equalities between computed values: `add(3, 5) = 8`, `length([1,2,3]) = 3`, `append([1,2], [3]) = [1,2,3]` - Properties of concrete data: "this declaration field is in the allowed set," "this target class is valid," "these two strings match" - Generic combinators: `cong`, `sym`, `trans`, and `transport` type-check with abstract variables - Verified function extraction: any function expressible with the kernel's eliminators can be verified and extracted **It cannot prove automatically by conversion:** - **Symbolic induction by `refl`.** `forall n, n + 0 = n` requires induction over an abstract variable. The generated natural eliminator reduces on concrete generated constructor values, so `3 + 0 = 3` and `100 + 0 = 100` are witnessed by `refl`. For a bound `n`, `n + 0` stays neutral because addition recurses on its first argument; it is not definitionally equal to `n`. The universal statement is still provable as propositional equality with an explicit induction proof. See `examples/category-theory/arithmetic.nix` for `addRightZero`. - **Properties of Nix builtins.** The kernel axiomatizes `String`, `Int`, `Float`, etc. as opaque types. The kernel has `strEq` (equality) and `strLen` (length) for strings, plus list membership, but operations beyond these — `builtins.substring`, `builtins.match`, concatenation — are not kernel functions, so their properties are not provable. - **Eta-expansion.** The kernel does not identify `f` with `λx.f(x)`. Functions that are extensionally equal but intensionally different are not convertible. - **User-defined recursive types.** The kernel has an indexed-description family (`Desc I` / `μ` / `desc-ind`) that the macro layer uses to build public inductives such as `Nat`, `List`, `Sum`, `Bool`, `Fin`, `Vec`, and `Eq`-as-description. Arbitrary user-defined inductive families (binary trees, red-black trees, etc.) use the same description-macro layer; they are not written directly against primitive per-type kernel nodes. The macro layer exposes four user-facing entry points for defining inductive types. `H.datatype name cons` compiles a monomorphic, ⊤-indexed datatype from a list of `H.con name fields` specs (`H.field`, `H.fieldD`, `H.recField`, `H.piField`, `H.piFieldD` for the field shapes). `H.datatypeP name params mkCtors` adds a parameter layer, threading each parameter through an outer Π binder. `H.datatypeI name I consList` adds an arbitrary index type `I : U`; constructors use `H.conI name fields targetIdx` to specify their target index as a function of earlier field markers, and recursive fields at non-default indices use `H.recFieldAt name idxFn` (plain `H.recField` is rejected at `I ≠ ⊤`). `H.datatypePI name params indexFn mkCtors` combines parameters and indexing — the index type itself may depend on parameters, which is what `Eq A a : A → U` requires. Each macro returns a record exposing `.D : Desc I`, `.T : Π(i:I). U` (or `μ ⊤ D tt` at the ⊤-sugar path), per-constructor fields, and `.elim` built on `desc-ind`. The prelude's `FinDT`, `VecDT`, and `EqDT` are the canonical indexed instances and drive the surface `H.fin` / `H.vec` / `H.eqDT` bindings as thin forwarders. For Nix, the "concrete data" restriction is less of a limitation than it sounds. Nix evaluates declarations completely before building — every aspect, generated module, dependency edge, and package attribute is a concrete value at eval time. The kernel verifies all computable properties of that concrete data. What it gives up is proving things about *all possible* declarations generically. In practice, you prove properties of the specific graph being built, which is the one that matters. ## Quick reference | Pattern | Type | Proof term | |---------|------|------------| | Computational equality | `Eq(A, x, y)` where `x`, `y` normalize to same value | `Refl` | | Dependent witness | `Σ(x:A). P(x)` | `(value, proof)` | | Case analysis (bool, derived) | `H.boolElim k motive true_case false_case b` | Result of elimination | | Structural recursion (nat) | `H.ind k motive base step n` | Result of elimination | | List recursion | `H.listElim k elem motive nil_case cons_case xs` | Result of elimination | | Sum dispatch | `H.sumElim k L R motive left_case right_case s` | Result of elimination | | Congruence | `Eq(A,x,y) → Eq(B, f(x), f(y))` | `J(A, x, λy'.λ_. Eq(B,f(x),f(y')), Refl, y, p)` | | Symmetry | `Eq(A,x,y) → Eq(A,y,x)` | `J(A, x, λy'.λ_. Eq(A,y',x), Refl, y, p)` | | Transitivity | `Eq(A,x,y) → Eq(A,y,z) → Eq(A,x,z)` | `J(A, y, λz'.λ_. Eq(A,x,z'), p, z, q)` | | Transport | `Eq(A,x,y) → P(x) → P(y)` | `J(A, x, λy'.λ_. P(y'), px, y, p)` | | Ex falso (derived `H.void = Fin 0`) | `H.void → A` | `H.absurd A x` (routes through `absurdFin0`) | | Verified function | `v.verify type impl` | Extracted Nix function | ## References 1. Martin-Löf, P. (1984). *Intuitionistic Type Theory*. Bibliopolis. 2. The Univalent Foundations Program (2013). *Homotopy Type Theory: Univalent Foundations of Mathematics*. Institute for Advanced Study. [[pdf](https://homotopytypetheory.org/book/)] 3. Norell, U. (2007). *Towards a practical programming language based on dependent type theory*. PhD thesis, Chalmers. [[pdf](https://www.cse.chalmers.se/~ulfn/papers/thesis.pdf)] #### Sugar nix-effects ships an opt-in syntax layer. The kernel doesn't import it, nothing in the effect interpreter depends on it, and removing it leaves the library unchanged. What it buys is readability. A three-step state computation without sugar: ```nix bind state.get (n: bind (state.put (n + 1)) (_: bind state.get (n2: pure n2))) ``` and with sugar, two forms: ```nix # Combinator (sequence effects, discard intermediates) steps [ (_: state.get) (n: state.put (n + 1)) (_: state.get) ] # Operator state.get / (n: state.put (n + 1)) / (_: state.get) ``` Both evaluate to the same value under the state handler. `do` is the companion combinator: rather than producing a `Comp` directly it returns a Kleisli arrow `(a -> M b)` that you apply to a seed — trading one keystroke (`(... ) null`) for composability, point-free use, and auto-lifting of plain functions. The two are covered side-by-side in [Effect combinators](#effect-combinators). A third form — `letM` — applies to parallel effects whose results you want under named bindings: ```nix # Without sugar bind (reader.asks (e: e.host)) (host: bind (reader.asks (e: e.port)) (port: pure "${host}:${toString port}")) # With letM letM { host = reader.asks (e: e.host); port = reader.asks (e: e.port); } (b: pure "${b.host}:${toString b.port}") ``` `letM` evaluates its attrs independently and passes the result attrset to the continuation. Use it when the effects don't depend on each other's values. ## Opting in Sugar is a hybrid namespace. Effect combinators sit at the top level of `fx.sugar`, so `with fx.sugar;` brings `do`, `steps`, `letM`, `pure`, `bind`, `run`, and `handle` into scope immediately. Division and types are one level deeper, under `operators` and `types` respectively. ``` fx.sugar ├── do, steps, letM ├── pure, bind, map, seq, pipe, kleisli ├── run, handle ├── operators │ └── __div └── types ├── wrap └── Int, String, Bool, Float, Path, Null, Unit, Any ``` The combinator-only form is safe under every Nix dialect. No operator magic, no `with`, no chance of surprising anyone: ```nix let inherit (fx.sugar) steps letM; in steps [ (_: state.get) (n: state.put (n * 3)) (_: state.get) ] ``` Adding `/` as left-associative bind turns long `bind` chains into pipelines: ```nix let inherit (fx.sugar.operators) __div; in state.get / (s: state.put (s + 7)) / (_: state.get) ``` For a file that's mostly computation, reach for `with fx.sugar;` and pair it with the `__div` inherit: ```nix let inherit (fx.sugar.operators) __div; in with fx.sugar; state.get / (s: state.put (s * 2)) / (_: state.get) ``` The division operator is always nested under `operators`. `with fx.sugar;` alone will not activate `/` — you have to reach for `operators` explicitly. That nesting is the entire reason the namespace is hybrid. ## Effect combinators ### `do`: composable Kleisli arrow `do` takes a list of functions and returns a Kleisli arrow `(a -> M b)`. Apply it to a seed value to obtain the computation: ```nix (do [ (x: x + 1) # plain function — auto-lifted via `pure` (x: pure (x * 10)) # already monadic — passes through ]) 1 # runs to 20 ``` Three properties follow from returning an arrow rather than a `Comp` directly. **Composable.** Two pipelines glue via `kleisli` without re-wrapping: ```nix kleisli (do [ f g ]) (do [ h i ]) == do [ f g h i ] ``` **Data-last.** The seed is the final argument, so `do` slots into `map` point-free: ```nix map (do [ validate enrich ]) userIds ``` **Auto-lifting.** Each step may be plain `(a -> b)` or monadic `(a -> M b)`. The runtime dispatches via `isComp`: a `Comp` result passes through `bind`, anything else is wrapped in `pure`. Pure and effectful steps mix freely without manual lifting. The empty list gives the identity arrow `x: pure x`; the singleton list applies its single step. ### `steps`: sequence of effects `steps` is the original `do` semantics, renamed. It takes a list of functions, threads each through `bind`, and returns a `Comp` directly: ```nix steps [ (_: state.get) (n: state.put (n + 1)) (_: state.get) ] ``` The seed is `pure null`, so the first step receives `null`. Empty lists produce `pure null`. Use `steps` when the intent is "sequence these effects" rather than "thread a value through a pipeline" — analogous to Haskell's `sequence_`. When the first step is producer- shaped (`_: pure x`), the leading `null` is harmless boilerplate; for anything more elaborate, reach for `do` so the seed is explicit. ### `letM`: named results `letM` collects an attrset of computations, evaluates each one, and hands the result attrset to a continuation. The Reader-pattern example from the test suite: ```nix letM { host = reader.asks (e: e.host); port = reader.asks (e: e.port); } (b: pure "${b.host}:${toString b.port}") # runs to "example.com:443" ``` The continuation receives `{ host; port; }`. When a computation and its continuation don't need sequencing by intermediate values but do need named results in scope, `letM` is cleaner than nested `bind`. ### `__div`: operator-style bind `__div` is a magic attribute name. When both operands of `/` are non-numeric and `__div` is lexically in scope, Nix dispatches the operator through it. `fx.sugar.operators.__div` is `fx.bind` under another name. ```nix let inherit (fx.sugar.operators) __div; in state.get / (n: pure (n + 1)) / (n: pure (n * 2)) ``` The form is left-associative: `a / f / g` is `bind (bind a f) g`. This matches the usual reading of a pipeline. ### Re-exports For convenience, `fx.sugar` re-exports `pure`, `bind`, `map`, `seq`, `pipe`, `kleisli`, `run`, and `handle` verbatim from `fx`. `with fx.sugar;` gives you everything the effect layer exposes without a second `inherit` line. ## Type sugar ### Primitives and refinement `fx.sugar.types` pre-wraps the eight zero-ary primitives — `Int`, `String`, `Bool`, `Float`, `Path`, `Null`, `Unit`, `Any` — with a `__functor` that builds a refinement when you apply a predicate. A target class, written without sugar: ```nix let inherit (fx.types) String refined; in refined "TargetClass" String (x: builtins.elem x [ "module" "file" "package" "check" ]) ``` and with sugar: ```nix let inherit (fx.sugar.types) String; in String (x: builtins.elem x [ "module" "file" "package" "check" ]) ``` Both produce a kernel-identical type. The difference is readability when you're composing several predicates. ### Name cascading Every refinement appends a `?` to the base type's name. Repeated refinement cascades: ```nix let inherit (fx.sugar.types) Int; P0 = Int; # "Int" P1 = Int (x: x >= 0); # "Int?" P2 = Int (x: x >= 0) (x: x < 10); # "Int??" in builtins.toString P2 # "Int??" ``` The name is what shows up in error messages. If `Int??` isn't descriptive enough, drop back to `fx.types.refined` and give the type an explicit name: ```nix let inherit (fx.types) refined String; in refined "RendererClass" String (x: builtins.elem x [ "module" "file" "package" "check" ]) ``` ### `wrap` for user-defined types Types built with `fx.types.mkType` don't get sugar by default. Wrap them with `fx.sugar.types.wrap` to opt in: ```nix let inherit (fx.types) mkType hoas; inherit (fx.sugar.types) wrap; UserInt = mkType { name = "UserInt"; kernelType = hoas.int_; }; Sugared = wrap UserInt; in (Sugared (x: x > 0)).check 5 # true ``` Wrapping is purely additive. It only adds `__functor` (for refinement application) and `__toString` (for the name). The base type's kernel, check, description, universe, and every other field stay untouched — so a sugared type is interchangeable with the desugared original everywhere the kernel looks at it. ### Sugar inside Record fields Constructors like `Record`, `ListOf`, `Maybe`, and `Either` already consume a first argument — their schema. Wrapping them with `__functor` would collide with that call shape, so `fx.sugar.types` doesn't wrap them. It doesn't need to: a sugared field-type inside a Record schema composes for free, because Record reads only the kernel, which sugar preserves: ```nix let inherit (fx.types) Record; inherit (fx.sugar.types) Int String Bool; in Record { age = Int (x: x >= 0); name = String (s: builtins.stringLength s > 0); active = Bool; } ``` This Record has the same `_kernel` as the hand-refined version. The sugar is pushed *into* the schema, where it needs no special support from the constructor. ## Caveats A few details worth knowing before you reach for sugar. ### `+` can't be overloaded Nix's `+` operator is `ExprConcatStrings` in the parser 1. The runtime dispatches on operand types (string, path, number) without consulting any magic attribute. There's no `__plus` to implement. Applied to two types, `+` will either concatenate (if they're strings), add (if they're numbers), or error out. It is not a hook. For the same reason, there's no way to overload `==`, `<`, or most other operators. Sugar uses what Nix already dispatches through: `__functor` for callable attrsets, `__toString` for string coercion, and `__div` for `/`. ### `with` does not activate `__div` Nix's `/` operator looks up `__div` by name in the enclosing lexical scope. It does not search `with`-scoped values. This surprises people who expect the two forms to be interchangeable: ```nix # Works: let inherit (fx.sugar.operators) __div; in (state.get / f) # Does NOT work — raises an arithmetic division error at runtime: with fx.sugar.operators; (state.get / f) ``` The reason is that `with` only extends the free-variable lookup chain — it does not introduce `__div` as a bound name in the scope that `/` consults. The full-sugar form above wraps `inherit (fx.sugar.operators) __div;` in the same `let` that brings in the combinators for exactly this reason. A witness test lives at `tests/sugar-effects-test.nix` under `withOperatorsDoesNotActivateDiv`. It asserts that `with fx.sugar.operators; (6 / 2) == 3` — plain arithmetic, not `__div` dispatch. ### Lix 2.92+ rejects `__div` shadow Lix 2 deprecated the pattern of binding a name prefixed with `__` that shadows a builtin-reserved operator slot. As of Lix 2.92, `let inherit (ops) __div; in ...` produces `shadow-internal-symbols` errors during parse. If your codebase targets Lix, stick to `do`, `steps`, and `letM` — they don't touch this mechanism. This is not a CppNix limitation. `__div` works under CppNix 2.18+ and 2.31 (the release we test against). The Lix deprecation is a deliberate policy choice in that fork. ### Scope pollution with `with` `with fx.sugar.operators;` brings `__div` into the lookup chain as a value, not as an operator hook. As just noted, it won't make `/` dispatch to it. But it does make the name `__div` available for reference — a minor footgun if you were relying on shadowing. Prefer `inherit` over `with` for operator opt-in. ### Name cascading versus explicit names Chained refinements produce names like `Int??`. That's intentional ("you refined this twice") but not always helpful in error messages. If your domain has a real name, use `fx.types.refined` directly: ```nix let inherit (fx.types) refined Int; Even = refined "Even" Int (x: builtins.bitAnd x 1 == 0); Positive = refined "Positive" Int (x: x > 0); EvenPositive = refined "EvenPositive" Even (x: x > 0); in EvenPositive.name # "EvenPositive" ``` Sugar is for in-place predicates, not named types that outlive their definition. ## Forward-compat notes Sugar is strictly additive and never references anything the type system marks for retirement. Three commitments hold across future changes to the kernel and type modules. **Kernel preservation.** A sugared type has the same `_kernel` as its base. Constructors (`Record`, `ListOf`, `Maybe`, `Either`, `Variant`) read only `_kernel` — so a sugared field is indistinguishable from a desugared one to every kernel consumer. **Refinement delegation.** Sugar never constructs refined types directly. Every `sugared T (pred)` call goes through `fx.types.refined`, which is the user-facing API point guaranteed to survive kernel-internal reorganizations. If `refined` changes shape, sugar follows automatically. **No diagnostic emission.** Sugar never builds values from `src/diag/positions.nix` or `src/diag/error.nix`. Error annotation happens via the base type's `description` and `name`, which propagate through `refined` without sugar-specific code. When the diagnostic layer gains structure, sugar will inherit it. Active witness tests for each of these live in `tests/sugar-compat-test.nix`. Running the test file as part of `nix flake check` keeps the commitments observable. ## When to reach for which form `steps` is the default for "run these effects, in order, for their side effects." `do` is the default when a pipeline threads a value through several steps, when the pipeline needs to compose with another pipeline, or when you want point-free use under `map` and friends. `letM` covers the case where bound values are siblings rather than a left-to-right pipeline. Reach for `__div` when a pipeline has three or more obvious-effect steps and the parentheses are hurting readability. Reach for `with fx.sugar;` when you're writing a file that's mostly computation, not mostly plumbing. For types, use `fx.sugar.types` for one-off refinements inside Record schemas. Drop back to `fx.types.refined` when the type deserves a name you'll reference elsewhere. --- 1 `src/libexpr/parser.y` in the Nix source, handling `ExprConcatStrings`. 2 Lix is a community fork of Nix. Relevant deprecation: `shadow-internal-symbols` in Lix 2.92 release notes. ### Concepts #### Theory Several papers shaped nix-effects. Here's how each one maps to code. ## Algebraic effects and the freer monad A computation is a tree of effects with continuations. A handler walks the tree, interpreting each effect — either resuming the continuation with a value or aborting it. That's the handler model from Plotkin & Pretnar (2009), and nix-effects implements it directly. A computation is either: - `Pure value` — finished, returning a value - `Impure effect continuation` — suspended, waiting for a handler to interpret `effect` and feed the result to `continuation` `send` creates an `Impure` node: ```nix send "get" null # Impure { effect = { name = "get"; param = null; }; queue = [k]; } ``` `bind` appends to the continuation queue: ```nix bind (send "get" null) (s: pure (s * 2)) # Impure { effect = get; queue = [k1, k2] } — O(1) per bind ``` Handlers provide the interpretation: ```nix handlers = { get = { param, state }: { resume = state; inherit state; }; put = { param, state }: { resume = null; state = param; }; }; ``` `resume` feeds a value to the continuation. `abort` discards it and halts. ## FTCQueue: O(1) bind Naïve free monads have O(n²) bind chains. The problem is reassociation: ``` (m >>= f) >>= g ≡ m >>= (f >=> g) ``` Each reassociation traverses the whole tree. Kiselyov & Ishii (2015) solved this by storing continuations in a catenable queue (FTCQueue) instead of a list. `snoc` is O(1); queue application (`qApp`) amortizes the reassociation across traversal. Total cost: O(n) for n bind operations, regardless of nesting depth. This matters in practice — a `DepRecord` with 100 fields sends 100 effects, each of which binds. Without the queue, validation time would be quadratic in the number of fields. The interpreter that processes these queued continuations uses defunctionalization (Reynolds 1972): the recursive handler becomes a data structure — effect name, parameter, handler result — and a worklist loop (`builtins.genericClosure`) iterates over steps instead of recursing. This is the pattern Van Horn & Might (2010) identified in *Abstracting Abstract Machines*: store-allocated continuations plus worklist iteration give you bounded stack depth. The [Trampoline](/nix-effects/internals/trampoline) chapter covers the implementation: how `genericClosure` becomes a trampoline, why `deepSeq` prevents thunk accumulation, and which automated tests pin stack-safety behavior. ## Value-dependent types Martin-Löf (1984) is where types that depend on values come from. In nix-effects, all types bottom out in the MLTT kernel (`src/tc/`), which handles type checking, universe level computation, and proof verification. The user-facing API provides convenience constructors on top. **Sigma (Σ)** — the dependent pair. The second component's type is a function of the first component's value: ```nix Σ(generated : Bool). if generated then TargetClass else String ``` In nix-effects: ```nix Sigma { fst = Bool; snd = b: if b then TargetClass else String; } ``` `Sigma.validate` decomposes the check: validate `fst` first, then — only if it passes — evaluate `snd fst-value` and validate that. The dependent expression is never evaluated on a wrong-typed input. That ordering is the whole point. **Pi (Π)** — dependent function type. The return type depends on the argument: ```nix Pi { domain = String; codomain = _: Int; } ``` The kernel's decision procedure checks `isFunction` — closures are opaque, so that's all it can verify at introduction. Full verification happens at elimination via the kernel's type-checking judgment. **Universe hierarchy.** Types themselves have types, stratified from `Type_0` through `Type_4` to guard against Russell's paradox: ```nix (typeAt 0).check Int # true — Int lives at universe 0 level String # 0 (typeAt 1).check (typeAt 0) # true — Type_0 lives at universe 1 ``` Universe levels are computed by the kernel's `checkTypeLevel`: `level(Pi(A,B)) = max(level(A), level(B))`, `level(U(i)) = i+1`. Self-containing universes (`U(i) : U(i)`) are rejected — `level(U(i)) = i+1 > i`, so the check fails. This prevents both accidental and adversarial paradoxes for every kernel-backed type. ## Refinement types Sometimes you need a type that's narrower than `String` but wider than an enum hard-coded into every caller. Freeman & Pfenning (1991) introduced the refinement type: given a base type T and a predicate P, the type {x:T | P(x)} admits only values of T that satisfy P. `refined` is the direct implementation — `refined "TargetClass" String (x: builtins.elem x [ "module" "file" "package" "check" ])` is a string whose value is one of the renderer classes, with a name attached. Rondon et al. (2008) later scaled the idea with SMT-based inference under the name *Liquid Types*. We skip the solver and use runtime predicate checking: ```nix TargetClass = refined "TargetClass" String (x: builtins.elem x [ "module" "file" "package" "check" ]); NonEmpty = refined "NonEmpty" String (s: builtins.stringLength s > 0); Nat = refined "Nat" Int (x: x >= 0); ``` `TargetClass.check` composes the kernel's decision (`String`) with the refinement predicate. Combinators for building compound predicates: ```nix allOf [ pred1 pred2 ] # conjunction anyOf [ pred1 pred2 ] # disjunction negate pred # negation ``` ## Soundness and what Nix provides The kernel's soundness is standard MLTT metatheory. Martin-Löf (1984) set out the rules, and the Mini-TT lineage (Coquand et al. 2009, Kovács 2022, and the elaboration-zoo and pi-forall tutorials) gives the bidirectional elaboration and normalization-by-evaluation recipe the kernel follows. Nothing in the kernel is novel on the metatheory side, and none of the soundness argument routes through anything Nix-specific. What Nix contributes is a faithful runtime for definitional equality. NbE requires that reducing open terms is deterministic and side effect free, and pure Nix evaluation gives you exactly that. `builtins.trace` and `builtins.throw` are not observable through definitional equality, so they cannot perturb conversion checking. The effect layer sits above the kernel as meta-level freer monad data. `Impure` and `Pure` attrsets are values walked by pure handlers. They are not object-language effects in the kernel's grammar, and the kernel has no constructor for them. This is how the effect layer can surface kernel errors as `typeCheck` effects carrying context paths without widening the trusted core. ## Graded linear types Orchard, Liepelt & Eades (2019) introduced a type system where each variable carries a usage grade from a resource semiring. We implement three points on that spectrum: `Linear` (exactly one use), `Affine` (at most one), and `Graded` (exactly n uses). In practice, the handler maintains a resource map counting each `consume` call against a `maxUses` bound. At handler exit, a finalizer checks that every resource was consumed the expected number of times. The grade discipline is enforced at runtime through the effect system, not statically — so you get usage tracking without a custom type checker, but violations show up at eval time rather than before it. That's a real trade-off, and for eval-time DSL validation we're comfortable with it. ## Higher-order contracts and blame Findler & Felleisen (2002) solved a problem that shows up immediately when you try to check function types: you can't test a function contract at the point of definition. A function is a closure — opaque. The only way to check it is to wrap it and verify at application boundaries. In nix-effects, this is exactly what happens. `decide(H.forall ..., f)` can only confirm `builtins.isFunction f` — the kernel can't look inside a Nix closure. For full verification, you write the implementation in HOAS, the kernel type-checks the term, and `extract` wraps the result as a Nix function that elaborates its arguments at every call boundary. The contract is enforced at application, not definition. That's Findler & Felleisen. Their other contribution is blame tracking. When a check fails, the error needs to say *which* contract was violated and *where*. In nix-effects, `.validate` sends `typeCheck` effects carrying blame context — type name, field path, rejected value — and the handler decides the error policy: `strict` throws immediately, `collecting` accumulates all failures, `logging` records every check. Same kernel judgment, different reporting strategy — the handler pattern (Plotkin & Pretnar) composes with the contract pattern (Findler & Felleisen) to separate what to check from how to report. ## References 1. Plotkin, G., & Pretnar, M. (2009). *Handlers of Algebraic Effects*. ESOP 2009. [[doi](https://doi.org/10.1007/978-3-642-00590-9_7)] 2. Kiselyov, O., & Ishii, H. (2015). *Freer Monads, More Extensible Effects*. Haskell Symposium 2015. [[pdf](https://okmij.org/ftp/Haskell/extensible/more.pdf)] 3. Martin-Löf, P. (1984). *Intuitionistic Type Theory*. Bibliopolis. 4. Rondon, P., Kawaguchi, M., & Jhala, R. (2008). *Liquid Types*. PLDI 2008. [[doi](https://doi.org/10.1145/1375581.1375602)] 5. Findler, R., & Felleisen, M. (2002). *Contracts for Higher-Order Functions*. ICFP 2002. [[doi](https://doi.org/10.1145/581478.581484)] 6. Van Horn, D., & Might, M. (2010). *Abstracting Abstract Machines*. ICFP 2010. (See [Trampoline](/nix-effects/internals/trampoline)) 7. Freeman, T., & Pfenning, F. (1991). *Refinement Types for ML*. PLDI 1991. [[doi](https://doi.org/10.1145/113445.113468)] 8. Orchard, D., Liepelt, V., & Eades, H. (2019). *Quantitative Program Reasoning with Graded Modal Types*. ICFP 2019. [[doi](https://doi.org/10.1145/3341714)] ## Prior art - Borja, V. (2026). *nfx: Nix Algebraic Effects System with Handlers*. [[github](https://github.com/vic/nfx)] — Implements algebraic effects in pure Nix using a context-passing model with `immediate`/`pending` constructors. nix-effects adopted nfx's `adapt` handler combinator, `mk { doc, value, tests }` API pattern, and effect module vocabulary (`state`, `acc`, `conditions`, `choice`, streams), while building a new core on the freer monad encoding from Kiselyov & Ishii (2015) and adding value-dependent types and a type-checking kernel that nfx does not attempt. ### Internals #### Trampoline The trampoline is how nix-effects interprets freer monad computations with O(1) stack depth in a language with no iteration primitives and no tail-call optimization. ## The problem Nix is a pure, lazy, functional language. It has no loops. Every "iteration" is recursion. A naïve free monad interpreter using mutual recursion would build a call stack proportional to the computation length: ``` run (bind (bind (bind ... (send "get" null) ...) ...) ...) → run step1 → run step2 → run step3 → ... (N frames deep) ``` For validation of a large DSL value — say, an aspect graph with hundreds of declarations — this would blow the stack. ## The solution: `builtins.genericClosure` Nix's `builtins.genericClosure` is the only built-in iterative primitive. It implements a worklist algorithm: ``` genericClosure { startSet = [ initialNode ]; operator = node -> [ ...nextNodes ]; } ``` `operator` is called on each node. New nodes returned by `operator` are added to the worklist if their `key` hasn't been seen before. The result is the set of all reachable nodes. nix-effects repurposes this as a trampoline: each step of computation is a node. The `operator` function handles one effect and produces the next step as a singleton list. The computation terminates when `operator` returns `[]` (i.e., when we reach a `Pure` node). ```nix steps = builtins.genericClosure { startSet = [{ key = 0; _comp = comp; _state = initialState; }]; operator = step: if isPure step._comp then [] # halt else [ nextStep ]; # one more step }; ``` Stack depth: **O(1)**. `genericClosure` handles its own iteration internally; the `operator` function is never deeply nested. ## The thunk problem and `deepSeq` `genericClosure` only forces the `key` field of each node (for deduplication). All other fields — including `_state` and `_comp` — are lazy thunks. Without intervention, after N steps the `_state` field would be: ``` f(f(f(... f(initialState) ...))) # N thunks deep ``` Forcing the final `_state` would then rebuild the entire call stack in thunk evaluation, defeating the purpose. The fix: make `key` depend on `builtins.deepSeq newState`: ```nix key = builtins.deepSeq newState (step.key + 1) ``` Since `genericClosure` forces `key`, it also forces `deepSeq newState`, which eagerly evaluates the state at each step. No thunk chain builds up. The test suite validates deep effect chains and pure bind chains so the stack-safety contract stays covered by automated checks. ### State-shape contract `builtins.deepSeq newState` imposes a contract on handler-state shape: every value reachable through state must be deepSeq-tolerant. Scalars, finite records, and lists of scalars satisfy this trivially. Functions are also safe — `deepSeq` on a closure forces it to WHNF and stops, never recursing into the captured environment. `builtins.deepSeq` detects cycles by object identity: `forceValueDeep` keeps a seen-set of already-forced values, so a self-referential attrset terminates. That guard has two gaps. A lazy graph that regenerates a fresh object on each force — as a derivation's `passthru` can — is never recognized as seen and overflows. And a traversal that keeps no seen-set at all — `builtins.toJSON`, or the `api.extractValue` walker — descends any cyclic value until the evaluator overflows. Deep-forcing a real derivation's full attribute closure at every step is also prohibitively expensive even where it terminates. None of these failures is recoverable: a stack overflow and `toJSON`'s "cannot convert a function to JSON" both escape `tryEval`; only `throw` and `assert false` are catchable, which is why a fuel-bounded walker that throws on exhaustion is the one usable divergence signal. This behavior is identical on every evaluator probed — Nix 2.3.18, 2.18.8, 2.24.8, Lix 2.91.1, and 2.35pre — so the contract rests on stable language semantics, not an evaluator-specific quirk. For this case the library ships `fx.state.mkThunk` / `forceThunk` (`src/state/thunk.nix`). The carrier wraps any value as `{ _tag = "Thunk"; _force = _: value; }` — a closure shields the value from deepSeq. The companion kernel type former `H.thunk : Hoas → Hoas` is decided by a *lazy structural* walker: it verifies `is attrset ∧ has _force closure` and does NOT recurse into `_force`. Inner-type validation runs at forget time, post-forced. Forcing in the validator would defeat the deepSeq-shielding the whole construct exists for. Effect descriptions that carry derivations through state type their payload fields as `H.thunk H.derivation`, not `H.derivation`, so `fx.send`-time validation rejects unwrapped drvs before they reach the trampoline. The inner type is parametric: any value category that needs trampoline transit can ride through `H.thunk a` with no bespoke primitive. ## Defunctionalization The interpreter defunctionalizes (**Reynolds 1972**) the recursive handler: the continuation moves from the call stack into an explicit data structure (the FTCQueue). The worklist loop processes these continuations iteratively rather than recursively — the same pattern identified by **Van Horn & Might (2010)** in *Abstracting Abstract Machines*. **Gibbons (2022)** *Continuation-Passing Style, Defunctionalization, Accumulations, and Associativity* shows the hidden precondition: this transformation is valid when the accumulated operation is associative. For nix-effects, the handler state transformations compose associatively because function composition is associative. ## References - Reynolds, J. C. (1972). *Definitional Interpreters for Higher-Order Programming Languages*. ACM Annual Conference. - Van Horn, D., & Might, M. (2010). *Abstracting Abstract Machines*. ICFP 2010. - Gibbons, J. (2022). *Continuation-Passing Style, Defunctionalization, Accumulations, and Associativity*. The Art, Science, and Engineering of Programming, 6(2). [[doi](https://doi.org/10.22152/programming-journal.org/2022/6/7)] - Kiselyov, O., & Ishii, H. (2015). *Freer Monads, More Extensible Effects*. #### Kernel Architecture This chapter describes the type-checking kernel: its pipeline, its primitives, and how to write verified implementations that the kernel checks and extracts back to usable Nix functions. ## Two kernels nix-effects combines an effects kernel with a type-checking kernel: - The **effects kernel** (`src/kernel.nix`, `src/comp.nix`, `src/queue.nix`) implements the freer monad with FTCQueue. It defines the `Computation` ADT (`Pure a | Impure (Effect x) (FTCQueue x a)`) and the monadic operations `pure`, `impure`, `send`, `bind`, `map`, `seq`, `pipe`, `kleisli`. - The **type-checking kernel** (`src/tc/`) implements Martin-Löf type theory with normalization by evaluation and bidirectional checking. Core modules define terms, values, evaluation, quotation, conversion, and checking; adjacent HOAS, elaboration, generic, and ornament modules provide the user-facing structure. The type-checking kernel's higher layers use the effects kernel for error reporting. When `check` or `infer` rejects a term, it does not throw — it calls `send "typeError" { msg; expected; got; term; }`, producing an `Impure` computation. Handlers in `src/effects/typecheck.nix` (`strict`, `collecting`, and others) interpret that request with different strategies: `strict` throws on the first error, `collecting` accumulates errors into handler state. The TCB (`eval`, `quote`, `conv`) never sends effects — it only throws on kernel-invariant violations. ``` Type system API (src/types/) Record, ListOf, DepRecord, refined, Pi, Sigma, ... | | elaboration (src/tc/elaborate/, src/tc/hoas/) v Type-checking kernel (MLTT, src/tc/) | | typeError sent as effect request v Effects kernel (freer monad + FTCQueue, src/kernel.nix) | | handler (strict / collecting / ...) interprets effects v Pure Nix ``` Every `fx.types` type carries a `_kernel` field — a HOAS tree that elaborates to a kernel type. `.check` is derived from `decide(_kernel, v)`; `.validate` wraps `decide` in a `typeCheck` effect so handlers can do blame-annotated reporting; `.prove` type-checks HOAS proof terms; `verifyAndExtract` runs the full pipeline (check → eval → extract) to produce a Nix value from a HOAS implementation. Refinement types add a `guard` predicate that runs alongside the kernel check (`check = kernelDecide(v) ∧ guard(v)`), handling constraints the kernel cannot express. ## The kernel pipeline The kernel implements normalization by evaluation (NbE) with bidirectional type checking. The core pipeline has one responsibility per module family: ``` term.nix --> eval/ --> value.nix | quote.nix --> term.nix | conv.nix | check/ ``` | Module | Function | Signature | |--------|----------|-----------| | `term.nix` | Term constructors | `mkVar`, `mkPi`, `mkLam`, `mkApp`, ... | | `eval/` | Evaluation | `Env × Tm -> Val` | | `value.nix` | Value constructors | `VLam`, `VPi`, `VPair`, `VMu`, ... | | `quote.nix` | Quotation | `ℕ × Val -> Tm` | | `conv.nix` | Conversion checking | `ℕ × Val × Val -> Bool` | | `check/` | Type checking | `Ctx × Tm × Val -> Tm` / `Ctx × Tm -> Tm × Val` | **Terms** (`Tm`) are the syntax — de Bruijn indexed expressions with explicit binding structure. **Values** (`Val`) are the semantics — fully normalized forms where lambdas carry defunctionalized closures. The TCB does not use Nix lambdas as semantic closures. **Evaluation** converts terms to values. **Quotation** reads values back to terms. **Conversion** checks whether two values are definitionally equal by comparing their semantic structure. The type checker is bidirectional: - `check(Γ, t, T)` — check that term `t` has type `T` (type-directed) - `infer(Γ, t)` — infer the type of term `t` (term-directed) - `checkTypeLevel(Γ, T)` — compute the universe level of a type ### Trust model The kernel has three layers with decreasing trust requirements: **Layer 0 — Trusted Computing Base.** `eval`, `quote`, `conv`. Pure functions. No side effects. No imports from the effect system. Bugs here compromise soundness. **Layer 1 — Semi-trusted.** `check`, `infer`, `checkTypeLevel`. Uses the TCB and sends effects for error reporting. Bugs may produce wrong error messages or reject valid terms, but cannot cause unsoundness. **Layer 2 — Untrusted.** The elaborator (`hoas/`, `elaborate/`). Translates surface syntax to core terms. Can have arbitrary bugs without compromising safety — the kernel verifies the output. ## Axiomatized primitives The kernel understands eight Nix primitive types as axioms. Each has a type former, a literal constructor, and a typing rule. None have eliminators — the kernel says "String is a type at level 0" and "a string literal inhabits String" but cannot structurally decompose these values. | Nix type | Kernel type | Literal | Level | |----------|-------------|---------|-------| | `string` | `String` | `StringLit(s)` | 0 | | `int` | `Int` | `IntLit(n)` | 0 | | `float` | `Float` | `FloatLit(f)` | 0 | | `set` | `Attrs` | `AttrsLit` | 0 | | `path` | `Path` | `PathLit` | 0 | | `derivation` | `Derivation` | `DerivationLit` | 0 | | `lambda` | `Function` | `FnLit` | 0 | | (any) | `Any` | `AnyLit` | 0 | `Path` is decided by `builtins.isPath`; `Derivation` is decided by `(v: builtins.isAttrs v && (v.type or null) == "derivation")`. The two share no values: `Path` rejects attrsets, `Derivation` rejects strings and non-derivation attrsets. The structural type formers with kernel introduction and elimination rules are `Unit`, `Sigma`, `Pi`, bootstrap identity/coproduct infrastructure, propositional truncation (`Squash`), universe lifting, function extensionality as an axiom, and the indexed-description family (`Desc I`, `μ`, `interpD`, `allD`, `everywhereD`, `desc-ind`). Public `Nat`, `List`, `Sum`, `Bool`, `Eq`, `Fin`, `Vec`, and `W` are generated through descriptions and the datatype macro. Their eliminators are generated adapters over `desc-ind`. This gives the kernel enough structure to compute with generated inductive families, pairs, and functions, while treating strings, integers, and other Nix-native types as opaque tokens. Axiomatized primitives are critical for real-world use. Without them, verified modules can only work over `Nat`/`Bool`/`List`/`Sigma`/`Sum`. With them, modules can handle target classes (`String`), dependency counts (`Int`), declaration records (nested `Sigma`/`Attrs`), and so on. ## HOAS: the surface API Writing de Bruijn indexed terms by hand is error-prone. The HOAS (Higher-Order Abstract Syntax) layer lets you use Nix lambdas for variable binding. The public API is `fx.types.hoas`. ### Type combinators ```nix H = fx.types.hoas; H.nat # Nat H.bool # Bool H.string # String H.int_ # Int H.float_ # Float H.unit # Unit (nullary product) H.void # Void (empty type) H.listOf H.nat # List Nat H.sum H.nat H.bool # Nat + Bool H.forall "x" H.nat (_: H.bool) # Π(x : Nat). Bool H.sigma "x" H.nat (_: H.bool) # Σ(x : Nat). Bool H.u 0 # U₀ (universe of small types) ``` ### Term combinators ```nix # Lambda: λ(x : Nat). x H.lam "x" H.nat (x: x) # Application H.app f arg # Natural number literals H.zero # 0 H.succ H.zero # 1 H.natLit 42 # 42 (sugar for 42 succs) # Boolean literals H.true_ H.false_ # Pairs H.pair fst snd # Projections H.fst_ p H.snd_ p # Sum injections H.inl leftTy rightTy term H.inr leftTy rightTy term # String / Int / Float literals H.stringLit "hello" H.intLit 42 H.floatLit 3.14 # Type annotation H.ann term type ``` ### Eliminators ```nix # Natural number induction H.ind motive base step scrut # Boolean elimination (k : Level is the motive's universe level) H.boolElim k motive onTrue onFalse scrut # List elimination H.listElim elemType motive onNil onCons scrut # Sum elimination H.sumElim leftTy rightTy motive onLeft onRight scrut # Equality elimination (J) H.j type lhs motive base rhs eq ``` ### How HOAS compiles Binding combinators produce HOAS markers — lightweight attrsets that stand for bound variables at a specific depth. The `elaborate` function (in `hoas.nix`) converts these to de Bruijn indexed `Tm` terms: ``` H.lam "x" H.nat (x: H.succ x) │ │ HOAS: x is a marker { _hoas = true; level = 0; } │ ▼ elaborate (depth=0) │ │ marker at level 0 -> T.mkVar(0 - 0 - 1) = T.mkVar(0) │ ▼ Lam("x", Nat, Succ(Var(0))) ← de Bruijn term ``` The elaboration of nested binding forms is trampolined via `builtins.genericClosure` for stack safety on deeply nested terms. ## The elaboration bridge The elaboration bridge (`elaborate.nix`) connects the user-facing type system to the kernel. It has six operations: | Operation | Signature | Direction | |-----------|-----------|-----------| | `elaborateType` | `FxType -> HoasTree` | type system -> kernel | | `elaborateValue` | `HoasTree × NixVal -> HoasTree` | Nix value -> kernel term | | `extract` | `HoasTree × Val -> NixValue` | kernel value -> Nix value | | `decide` | `HoasTree × NixVal -> Bool` | decision procedure | | `decideType` | `FxType × NixVal -> Bool` | elaborate type, then decide | | `verifyAndExtract` | `HoasTree × HoasTree -> NixValue` | full pipeline | ### elaborateType Converts an `fx.types` type into a HOAS tree. Dispatches on three things, in order: 1. The `_kernel` field (types built via `mkType` with `kernelType`) 2. Structural fields (Pi: `domain`/`codomain`, Sigma: `fstType`/`sndFamily`) 3. Name convention (`"Bool"` -> `H.bool`, `"String"` -> `H.string`, etc.) ### elaborateValue Converts a Nix value into a HOAS term, guided by a HOAS type. For example, given `H.nat` and the Nix integer `3`, produces `H.succ (H.succ (H.succ H.zero))`. Given `H.string` and `"hello"`, produces `H.stringLit "hello"`. ### extract — the reverse direction `extract` converts kernel values back to Nix values. It is the reverse of `elaborateValue`. This is where the verification story becomes interesting: you write an implementation as a HOAS term, the kernel verifies it, `eval` produces a kernel value, and `extract` converts the result to a usable Nix value. ```nix # extract : HoasTree -> Val -> NixValue extract H.nat (VDescCon ... ) # -> 2 extract H.bool (VDescCon ... ) # -> true # H.bool is derived extract H.string (VStringLit "hi") # -> "hi" extract (H.listOf H.nat) (VDescCon ... ) # -> [1 2 3] extract (H.forall "x" ...) (VLam ...) # -> Nix function (!) ``` The Pi case is the most important. Extracting a verified function produces a Nix function that: 1. Elaborates its argument into a kernel value (Nix -> kernel) 2. Applies the kernel-verified closure 3. Extracts the result back (kernel -> Nix) Correct by construction — the kernel verified the term, `eval` produced the closure, `extract` wraps with value conversion at the boundaries. ### decide The decision procedure. Returns `true` iff both elaboration and kernel type-checking succeed: ```nix decide = hoasTy: value: let result = builtins.tryEval ( let hoasVal = elaborateValue hoasTy value; checked = H.checkHoas hoasTy hoasVal; in !(checked ? error) ); in result.success && result.value; ``` This is the function that `mkType` uses to derive `.check`. Every type's `.check` is `v: decide(kernelType, v)` — no hand-written predicates. ### verifyAndExtract — the full pipeline Type-check a HOAS term against a HOAS type, then extract the result: ```nix verifyAndExtract = hoasTy: hoasImpl: let checked = H.checkHoas hoasTy hoasImpl; in if checked ? error then throw "verifyAndExtract: type check failed" else let tm = H.elab hoasImpl; # HOAS -> de Bruijn val = E.eval [] tm; # evaluate to Val in extract hoasTy val; # Val -> Nix value ``` ## Convenience combinators Raw HOAS is verbose. The convenience combinator layer (`fx.types.verified`, accessed as `v`) provides sugar that produces valid HOAS term trees: ```nix v = fx.types.verified; H = fx.types.hoas; ``` ### Literals ```nix v.nat 5 # H.natLit 5 v.str "hello" # H.stringLit "hello" v.int_ 42 # H.intLit 42 v.float_ 3.14 # H.floatLit 3.14 v.true_ # H.true_ v.false_ # H.false_ v.null_ # H.tt (unit) ``` ### Binding forms ```nix # Lambda: λ(x : Nat). body v.fn "x" H.nat (x: body) # Let: let x : Nat = 5 in body v.let_ "x" H.nat (v.nat 5) (x: body) ``` ### Eliminators with inferred motives The convenience combinators construct the required motive automatically from the result type. The motive is always constant (non-dependent): `λ_. resultTy`. ```nix # Boolean: if b then 1 else 0 v.if_ H.nat v.true_ { then_ = v.nat 1; else_ = v.nat 0; } # Natural number: pattern match on n v.match H.nat n { zero = v.nat 42; succ = k: ih: H.succ ih; } # List: fold over elements v.matchList H.nat H.nat list { nil = v.nat 0; cons = h: t: ih: H.succ ih; # count elements } # Sum: case split v.matchSum H.nat H.bool H.nat scrut { left = x: H.succ x; right = _: v.nat 0; } ``` ### Derived combinators ```nix # Map: apply f to each element v.map H.nat H.nat succFn myList # Fold: combine elements with accumulator v.fold H.nat H.nat (v.nat 0) addFn myList # Filter: keep elements matching predicate v.filter H.nat isZeroFn myList ``` ### Pairs and sums ```nix v.pair fstTerm sndTerm sigmaType v.fst p v.snd p v.inl leftTy rightTy term v.inr leftTy rightTy term v.app f arg ``` ### The verify pipeline `v.verify` wraps `verifyAndExtract` — type-check and extract in one call: ```nix v.verify type implementation # = verifyAndExtract type implementation ``` ## Writing verified implementations The key insight: instead of writing Nix functions and trying to elaborate them into kernel terms (which fails for closures), write implementations as HOAS terms and **extract** Nix functions out. This is the approach taken by Coq (extraction), Idris (compilation), and F\*. ### Example: verified addition ```nix let H = fx.types.hoas; v = fx.types.verified; addTy = H.forall "m" H.nat (_: H.forall "n" H.nat (_: H.nat)); addImpl = v.fn "m" H.nat (m: v.fn "n" H.nat (n: v.match H.nat m { zero = n; succ = _k: ih: H.succ ih; })); # Kernel verifies: addImpl : Π(m:Nat). Π(n:Nat). Nat # Then extracts a 2-argument Nix function add = v.verify addTy addImpl; in add 2 3 # -> 5 ``` What happens step by step: 1. `v.verify` calls `H.checkHoas addTy addImpl` — the kernel type-checks the HOAS term against the HOAS type 2. `H.elab addImpl` converts HOAS to de Bruijn indexed `Tm` 3. `E.eval [] tm` evaluates to a `VLam` value (a Nix closure via NbE) 4. `extract addTy val` wraps the `VLam` as a Nix function that elaborates arguments at call boundaries The resulting `add` is an ordinary Nix function. Call it with Nix integers. At each call, the argument is elaborated into a kernel value, the verified closure runs, and the result is extracted back. ### Example: verified list operations ```nix let H = fx.types.hoas; v = fx.types.verified; # Successor function: Nat -> Nat succFn = v.fn "x" H.nat (x: H.succ x); # Map successor over a list input = H.cons H.nat (v.nat 0) (H.cons H.nat (v.nat 1) (H.cons H.nat (v.nat 2) (H.nil H.nat))); result = v.verify (H.listOf H.nat) (v.map H.nat H.nat succFn input); in result # -> [1 2 3] ``` ### Example: verified filter ```nix let H = fx.types.hoas; v = fx.types.verified; # isZero : Nat -> Bool isZero = v.fn "n" H.nat (n: v.match H.bool n { zero = v.true_; succ = _k: _ih: v.false_; }); input = H.cons H.nat (v.nat 0) (H.cons H.nat (v.nat 1) (H.cons H.nat (v.nat 0) (H.cons H.nat (v.nat 2) (H.nil H.nat)))); result = v.verify (H.listOf H.nat) (v.filter H.nat isZero input); in result # -> [0 0] ``` ## The verification spectrum The architecture supports a spectrum of assurance levels. Each level offers a different trade-off between verification strength and implementation cost. ### Level 1: Contract The baseline. Types check values at introduction via `.check` (which calls `decide` under the hood). No HOAS, no proof terms — just write normal Nix code and let the type system validate data at boundaries. ```nix let inherit (fx.types) String refined; TargetClass = refined "TargetClass" String (x: builtins.elem x [ "module" "file" "package" "check" ]); # Refinement with string guard — kernel checks String, guard checks membership LogLevel = refined "LogLevel" String (x: builtins.elem x [ "debug" "info" "warn" "error" ]); in { module = TargetClass.check "module"; # true fleet = TargetClass.check "fleet"; # false info = LogLevel.check "info"; # true trace = LogLevel.check "trace"; # false — not in the allowed set # Effectful validation with blame context result = fx.run (TargetClass.validate "fleet") fx.effects.typecheck.collecting []; # result.state = [ { context = "TargetClass"; ... } ] } ``` **Cost:** Zero — write normal Nix. The kernel runs behind the scenes. Refinement types compose: `ListOf TargetClass` checks that every element is a known renderer class. The kernel elaborates the list, the guard runs per element. ### Level 2: Boundary Data values are checked by the kernel at module interfaces. Types carry `kernelType` and `.check` is derived from the kernel's `decide` procedure. This is what every type does by default. ```nix let inherit (fx.types) Bool String ListOf DepRecord refined; TargetClass = refined "TargetClass" String (x: builtins.elem x [ "module" "file" "package" "check" ]); # Dependent record: generated aspects must target a known renderer class. # The kernel elaborates the record to a Sigma chain, checks each field's type # against its kernelType, and the guard on TargetClass validates membership. AspectDecl = DepRecord [ { name = "generated"; type = Bool; } { name = "target"; type = self: if self.generated then TargetClass else String; } { name = "requires"; type = _: ListOf String; } ]; in { ok = AspectDecl.checkFlat { generated = true; target = "module"; requires = [ "toolchain" ]; }; # true bad = AspectDecl.checkFlat { generated = true; target = "fleet"; requires = [ ]; }; # false external = AspectDecl.checkFlat { generated = false; target = "external-renderer"; requires = [ ]; }; # true } ``` **Cost:** Low — add `kernelType` to custom types (built-in types already have it). The dependent record pattern shows how boundary checking scales: the kernel handles structural verification, guards handle domain predicates, and the dependency between fields is resolved at check time. ### Level 3: Property Universal properties verified via proof terms. Write proofs in HOAS that the kernel checks. The proof term is separate from the implementation — you write separate Nix code alongside, and the kernel verifies that the stated property holds. ```nix let H = fx.types.hoas; inherit (H) nat bool eq forall refl checkHoas; # Define addition by structural recursion on the first argument add = m: n: H.ind (H.lam "_" nat (_: nat)) n (H.lam "k" nat (_: H.lam "ih" nat (ih: H.succ ih))) m; not_ = b: H.boolElim 0 (H.lam "_" bool (_: bool)) H.false_ H.true_ b; in { # Prove: 3 + 5 = 8 # The kernel normalizes add(3,5) via generated natural induction, # then confirms Refl witnesses Eq(Nat, 8, 8). arithmetic = (checkHoas (eq nat (add (H.natLit 3) (H.natLit 5)) (H.natLit 8)) refl).tag == "refl"; # Prove: not(not(true)) = true # The kernel evaluates two BoolElim steps and confirms the result. doubleNeg = (checkHoas (eq bool (not_ (not_ H.true_)) H.true_) refl).tag == "refl"; # Prove: append([1,2], [3]) = [1,2,3] # Generated list induction unfolds the first list onto [3]. listAppend = let list12 = H.cons nat (H.natLit 1) (H.cons nat (H.natLit 2) (H.nil nat)); list3 = H.cons nat (H.natLit 3) (H.nil nat); list123 = H.cons nat (H.natLit 1) (H.cons nat (H.natLit 2) (H.cons nat (H.natLit 3) (H.nil nat))); append = xs: ys: H.listElim nat (H.lam "_" (H.listOf nat) (_: H.listOf nat)) ys (H.lam "h" nat (h: H.lam "t" (H.listOf nat) (_: H.lam "ih" (H.listOf nat) (ih: H.cons nat h ih)))) xs; in (checkHoas (eq (H.listOf nat) (append list12 list3) list123) refl).tag == "refl"; } ``` **Cost:** Medium — write proofs in HOAS. The proofs are separate from production code, so you can add them incrementally to an existing codebase without rewriting anything. ### Level 4: Full The implementation IS the proof term. Write the entire implementation in HOAS, the kernel verifies it, and `extract` produces a Nix function that is correct by construction. The extracted function is plain Nix — no kernel overhead at call time. ```nix let H = fx.types.hoas; v = fx.types.verified; AspectDecl = H.record [ { name = "name"; type = H.string; } { name = "target"; type = H.string; } { name = "requires"; type = H.listOf H.string; } ]; targets = H.cons H.string (v.str "module") (H.cons H.string (v.str "file") (H.cons H.string (v.str "package") (H.cons H.string (v.str "check") (H.nil H.string)))); # Verified aspect validator: checks that the target class is supported. # The kernel type-checks the implementation against AspectDecl -> Bool, # verifies field projections, and confirms strElem composes correctly. validateAspect = v.verify (H.forall "a" AspectDecl (_: H.bool)) (v.fn "a" AspectDecl (a: v.strElem (v.field AspectDecl "target" a) targets)); # Verified addition: structural recursion extracted as Nix function add = v.verify (H.forall "m" H.nat (_: H.forall "n" H.nat (_: H.nat))) (v.fn "m" H.nat (m: v.fn "n" H.nat (n: v.match H.nat m { zero = n; succ = _k: ih: H.succ ih; }))); in { sum = add 2 3; # -> 5, correct by construction ok = validateAspect { name = "workspace-shell"; target = "module"; requires = [ "toolchain" ]; }; # -> true bad = validateAspect { name = "workspace-aspect"; target = "fleet"; requires = [ ]; }; # -> false } ``` **Cost:** High — write the implementation in HOAS. Best reserved for code where the cost is justified by the assurance. See the [Proof Guide](/nix-effects/guide/proof-guide) for a progressive tutorial from simple proofs through the J eliminator to verified extraction of plain Nix functions. ## How mkType derives .check Every type is built by `mkType` (in `foundation.nix`). The kernel type IS the type. `.check` is its decision procedure, derived mechanically: ``` _kernel : HoasType ← the type IS this check : Value -> Bool ← derived from decide(kernelType, value) kernelCheck : Value -> Bool ← same as check (legacy alias) prove : HoasTerm -> Bool ← kernel proof checking universe : Int ← computed from checkTypeLevel(kernelType) ``` For refinement types, an optional `guard` adds a runtime predicate on top of the kernel check: `check = decide(kernelType, v) && guard(v)`. The guard handles predicates the kernel cannot express — for example, `x >= 0` for natural numbers, or membership in a finite set for validated strings. ## Limitations **Nix closures are opaque.** `decide(H.forall ..., f)` can only check `builtins.isFunction f`. For full function verification, write the function in HOAS and use `v.verify` to extract. **Refinement predicates are opaque.** The kernel cannot represent `x >= 0` as a type-level assertion. Refinement types always need a hand-written guard predicate. **`builtins.tryEval` is limited.** It only catches `throw` and `assert false`. Cross-type comparison errors, boolean coercion errors, and missing attribute access crash Nix uncatchably. This affects `decide` for types whose elaboration might trigger such errors. **Dependent extraction is limited.** Extracting a dependent Pi or Sigma requires a sentinel test to detect non-dependence. If the type family is truly dependent, extraction throws and requires explicit type annotation. **Opaque types cannot be extracted.** `Attrs`, `Path`, `Derivation`, `Function`, and `Any` are axiomatized — the kernel knows they exist but discards their payloads. Extracting a value of these types throws. They work for type-checking (deciding membership) but not for the full verify-and-extract pipeline. **Extraction has boundary cost.** Extracted functions elaborate their arguments at every call (Nix -> kernel value -> apply -> extract -> Nix). For hot paths, the contract layer's `.check` fast path is more efficient. #### Kernel Formal Specification This document is the contract the implementation must satisfy. Every typing rule, compute rule, and conversion rule is stated precisely. Every test is derived from this spec. Every invariant the kernel must maintain is listed. The spec uses standard type-theoretic notation. No Nix code appears here — this document is reviewable by anyone who reads dependent type theory, regardless of implementation language. --- ## 1. Trust Model The kernel has three layers with strictly decreasing trust requirements. **Layer 0 — Trusted Computing Base (TCB).** The evaluator, quotation, and conversion checker. Pure functions. No side effects. No imports from the effect system. Bugs here compromise soundness. Every line must be auditable. - `eval : Env × Tm → Val` - `quote : ℕ × Val → Tm` - `conv : ℕ × Val × Val → Bool` **Layer 1 — Semi-trusted.** The bidirectional type checker. Uses the TCB and sends effects for error reporting. Bugs here may produce wrong error messages or reject valid terms, but cannot cause unsoundness (the TCB rejects ill-typed terms independently). - `check : Ctx × Tm × Val → Tm` - `infer : Ctx × Tm → Tm × Val` - `checkTypeLevel : Ctx × Tm → Tm × ℕ` **Layer 2 — Untrusted.** The elaborator. Translates surface syntax (named variables, implicit arguments, level inference, eta-insertion) into fully explicit core terms. Can have arbitrary bugs without compromising safety — the kernel verifies the output. ### Failure modes | Condition | Response | Rationale | |-----------|----------|-----------| | Kernel invariant violation | `throw` (crash) | TCB may be buggy; cannot trust own output | | User type error | Effect `typeError` | Normal operation; handler decides policy | | Normalization budget exceeded | `throw` (crash) | Layer 0 has no effect access; `tryEval` catches it | | Unknown term tag | `throw` (crash) | Exhaustiveness violation = kernel bug | --- ## 2. Syntax ### 2.1 Terms (Tm) The core term language. All binding uses de Bruijn indices. Name annotations are cosmetic (for error messages only). ``` Tm ::= -- Variables and binding | Var(i : ℕ) -- de Bruijn index | Let(n : Name, A : Tm, t : Tm, u : Tm) -- let n : A = t in u -- Functions | Pi(n : Name, A : Tm, B : Tm) -- Π(n : A). B | Lam(n : Name, A : Tm, t : Tm) -- λ(n : A). t | App(t : Tm, u : Tm) -- t u -- Pairs | Sigma(n : Name, A : Tm, B : Tm) -- Σ(n : A). B | Pair(a : Tm, b : Tm) -- (a, b) | Fst(t : Tm) -- π₁ t | Snd(t : Tm) -- π₂ t -- Unit | Unit -- ⊤ | Tt -- tt -- Empty | Empty -- ⊥ | Absurd(P : Tm, x : Tm) -- empty-type eliminator -- Bootstrap coproduct, private to descPlus interpretation | BootSum(A : Tm, B : Tm) -- A + B | BootInl(A : Tm, B : Tm, t : Tm) -- inl t | BootInr(A : Tm, B : Tm, t : Tm) -- inr t | BootSumElim(A : Tm, B : Tm, P : Tm, l : Tm, r : Tm, s : Tm) -- private eliminator for descPlus payloads -- Bootstrap identity, private to descRet/Lift/index transport | BootEq(A : Tm, a : Tm, b : Tm) -- internal Id_A(a, b) | BootRefl -- internal refl | BootJ(A : Tm, a : Tm, P : Tm, pr : Tm, b : Tm, eq : Tm) -- private identity eliminator | Funext -- function extensionality axiom -- Propositional truncation | Squash(A : Tm) -- proof-irrelevant truncation | SquashIntro(a : Tm) -- introduction into Squash | SquashElim(A : Tm, B : Tm, f : Tm, x : Tm) -- recTrunc: A -> Squash B, Squash A -> Squash B -- Levels (Tarski-style sort of universe levels — see §6.6, §8.5) | Level -- the Level sort itself, lives at U(0) | LevelZero -- 0 | LevelSuc(k : Tm) -- successor: k+1 | LevelMax(a : Tm, b : Tm) -- join in the level semilattice -- Universes (level-indexed, k : Level) | U(k : Tm) -- Type_k -- Descriptions (universe-polymorphic; see §7.6) | Desc(K : Tm, I : Tm) -- Desc^K I — descriptions over index type I | DescRet(j : Tm) -- ret(j) — leaf returning at index j | DescArg(K : Tm, S : Tm, T : Tm) -- arg^K S T — non-recursive Π over S : U(K) | DescRec(j : Tm, D : Tm) -- rec(j) D — recursive child at index j, then D | DescPi(K : Tm, S : Tm, f : Tm, D : Tm) -- π^K S f D — recursive Π over S : U(K), indexed by f | DescPlus(A : Tm, B : Tm) -- A + B — first-class binary coproduct of descriptions | DescDescApp(I : Tm, L : Tm) -- canonical descDesc I L reference | InterpD(level : Tm, I : Tm, D : Tm, X : Tm, i : Tm) -- interpretation of D at index i | AllD(level : Tm, I : Tm, D : Tm, K : Tm, X : Tm, M : Tm, i : Tm, d : Tm) -- all recursive positions satisfy M | EverywhereD(level : Tm, I : Tm, D : Tm, K : Tm, X : Tm, M : Tm, ih : Tm, i : Tm, d : Tm) -- builds AllD evidence from ih | DescInd(D : Tm, motive : Tm, step : Tm, i : Tm, scrut : Tm) -- generic μ-induction over D at index i -- μ-types (description-induced datatypes) | Mu(I : Tm, D : Tm, i : Tm) -- μ I D i — the i-th type in the family classified by D | DescCon(D : Tm, i : Tm, payload : Tm) -- introduction at index i with payload : interp(D, i) -- Annotations | Ann(t : Tm, A : Tm) -- (t : A); may carry trusted/label sidecars -- Lift | Lift(l : Tm, m : Tm, eq : Tm, A : Tm) | LiftIntro(l : Tm, m : Tm, eq : Tm, A : Tm, a : Tm) | LiftElim(l : Tm, m : Tm, eq : Tm, A : Tm, x : Tm) -- Axiomatized primitive types | String -- string type | Int -- integer type | Float -- float type | Attrs -- attribute set type | Path -- path type | Derivation -- derivation attrset type | Function -- opaque function type | Any -- dynamic/any type -- String operations | StrEq(lhs : Tm, rhs : Tm) -- string equality: lhs == rhs → H.bool (derived) -- Primitive literals | StringLit(s) -- string literal | IntLit(n) -- integer literal | FloatLit(f) -- float literal | AttrsLit -- attribute set literal | PathLit -- path literal | DerivationLit -- derivation attrset literal | FnLit -- opaque function literal | AnyLit -- any literal -- Opaque lambda trust boundary for extracted Nix functions | OpaqueLam(fnBox, piTy : Tm) -- Closed-Val splice (two-level TT reflection) | LitVal(v : Val) -- opaque carrier; eval is identity on v ``` Public `Nat`, `List`, `Sum`, `Bool`, `Fin`, `Vec`, `Eq`, and `W` are not primitive core syntax. The HOAS prelude generates them as descriptions: ``` H.nat = Mu(Unit, NatDT.D, Tt) H.listOf A = Mu(Unit, ListDT.D A, Tt) H.sum A B = Mu(Unit, SumDT.D 0 A B, Tt) H.eq A a b = Mu(A, EqDT.D A a, b) ``` Their constructors and eliminators elaborate to `DescCon` and `DescInd` applications through the datatype macro layer. The kernel therefore exposes one public induction principle for data: `DescInd`. The bootstrap coproduct and bootstrap identity above remain internal support for description interpretation, `DescRet`, `Lift`, and index transport. ### 2.2 Binding convention In `Pi(n, A, B)`, `Lam(n, A, t)`, `Sigma(n, A, B)`, and `Let(n, A, t, u)`: the body (`B`, `t`, or `u`) binds one variable. Index 0 in the body refers to the bound variable. All other indices shift by 1. Eliminators take their motives as ordinary function terms, not as implicit binders. Generated public eliminators build typed HOAS applications to datatype-specific eliminator functions; the core eliminator underneath is `DescInd`. ### 2.3 De Bruijn index conventions Indices count inward from the use site: 0 = most recent binder. Example: `λ(x : A). λ(y : B). x` is `Lam(x, A, Lam(y, B, Var(1)))`. --- ## 3. Values (Semantic Domain) Values are the result of evaluation. They use de Bruijn **levels** (counting outward from the top of the context) instead of indices. ``` Val ::= -- Functions | VPi(n : Name, A : Val, cl : Closure) -- Π type | VLam(n : Name, A : Val, cl : Closure) -- λ abstraction -- Pairs | VSigma(n : Name, A : Val, cl : Closure) -- Σ type | VPair(a : Val, b : Val) -- pair value -- Unit | VUnit | VTt -- Empty | VEmpty -- Bootstrap coproduct, private to descPlus interpretation | VBootSum(A : Val, B : Val) | VBootInl(A : Val, B : Val, v : Val) | VBootInr(A : Val, B : Val, v : Val) -- Bootstrap identity, private to descRet/Lift/index transport | VBootEq(A : Val, a : Val, b : Val) | VBootRefl | VFunext -- Propositional truncation | VSquash(A : Val) | VSquashIntro(a : Val) -- Levels (Tarski-style sort of universe levels — see §6.6, §8.5) | VLevel -- the Level sort itself | VLevelZero -- 0 | VLevelSuc(pred : Val) -- successor | VLevelMax(lhs : Val, rhs : Val) -- join -- Universes (level-indexed, k : VLevel) | VU(k : Val) -- Descriptions (universe-polymorphic; see §7.6) | VDesc(K : Val, I : Val) -- Desc^K I -- Description constructors are encoded as VDescCon inhabitants of -- μ Unit (descDesc I K) tt. Evaluation projects them through the private -- DViewRet/DViewArg/DViewRec/DViewPi/DViewPlus semantic view. -- μ-types | VMu(I : Val, D : Val, i : Val) -- μ I D i | VDescCon(D : Val, i : Val, d : Val) -- introduction at index i with payload d | VInterpD(level : Val, I : Val, D : Val, X : Val, i : Val) | VAllD(level : Val, I : Val, D : Val, K : Val, X : Val, M : Val, i : Val, d : Val) | VEverywhereD(level : Val, I : Val, D : Val, K : Val, X : Val, M : Val, ih : Val, i : Val, d : Val) -- Lift | VLift(l : Val, m : Val, eq : Val, A : Val) | VLiftIntro(l : Val, m : Val, eq : Val, A : Val, a : Val) -- Axiomatized primitive types | VString | VInt | VFloat | VAttrs | VPath | VDerivation | VFunction | VAny -- Primitive literal values | VStringLit(s) | VIntLit(n) | VFloatLit(f) | VAttrsLit | VPathLit | VDerivationLit | VFnLit | VAnyLit | VOpaqueLam(fnBox, piTy : Val) -- Neutrals (stuck computations) | VNe(level : ℕ, spine : [Elim]) Elim ::= | EApp(v : Val) | EFst | ESnd | EBootSumElim(A : Val, B : Val, P : Val, l : Val, r : Val) | EBootJ(A : Val, a : Val, P : Val, pr : Val, b : Val) | EStrEq(arg : Val) | EAbsurd(P : Val) | EDescInd(D : Val, motive : Val, step : Val, i : Val) | EInterpD(level : Val, I : Val, X : Val, i : Val) | EAllD(level : Val, I : Val, K : Val, X : Val, M : Val, i : Val, d : Val) | EEverywhereD(level : Val, I : Val, K : Val, X : Val, M : Val, ih : Val, i : Val, d : Val) | ELiftElim(l : Val, m : Val, eq : Val, A : Val) | ESquashElim(A : Val, B : Val, f : Val) Closure ::= (env : Env, body : Tm) Env ::= [Val] -- list indexed by de Bruijn index ``` ### 3.1 Level/index relationship De Bruijn levels count from the outermost binder: 0 = first-ever bound variable. Levels are stable under context extension. Conversion between index and level: ``` index = depth - level - 1 level = depth - index - 1 ``` where `depth` is the current binding depth (length of the context). ### 3.2 Fresh variables A fresh variable at depth `d` is `VNe(d, [])` — a neutral with level `d` and empty spine. Used in conversion checking to compare under binders. ### 3.3 Closure instantiation ``` instantiate((env, body), v) = eval([v] ++ env, body) ``` --- ## 4. Evaluation Rules `eval(ρ, t)` interprets term `t` in environment `ρ`, producing a value. All rules are deterministic. ### 4.1 Variables and let ``` eval(ρ, Var(i)) = ρ[i] eval(ρ, Let(n, A, t, u)) = eval([eval(ρ, t)] ++ ρ, u) eval(ρ, Ann(t, A)) = eval(ρ, t) ``` ### 4.2 Functions ``` eval(ρ, Pi(n, A, B)) = VPi(n, eval(ρ, A), (ρ, B)) eval(ρ, Lam(n, A, t)) = VLam(n, eval(ρ, A), (ρ, t)) eval(ρ, App(t, u)) = vApp(eval(ρ, t), eval(ρ, u)) ``` where `vApp` performs beta reduction or accumulates: ``` vApp(VLam(n, A, cl), v) = instantiate(cl, v) vApp(VNe(l, sp), v) = VNe(l, sp ++ [EApp(v)]) vApp(_, _) = THROW "kernel bug: vApp on non-function" ``` ### 4.3 Pairs ``` eval(ρ, Sigma(n, A, B)) = VSigma(n, eval(ρ, A), (ρ, B)) eval(ρ, Pair(a, b)) = VPair(eval(ρ, a), eval(ρ, b)) eval(ρ, Fst(t)) = vFst(eval(ρ, t)) eval(ρ, Snd(t)) = vSnd(eval(ρ, t)) ``` where: ``` vFst(VPair(a, b)) = a vFst(VNe(l, sp)) = VNe(l, sp ++ [EFst]) vFst(_) = THROW "kernel bug: vFst on non-pair" vSnd(VPair(a, b)) = b vSnd(VNe(l, sp)) = VNe(l, sp ++ [ESnd]) vSnd(_) = THROW "kernel bug: vSnd on non-pair" ``` ### 4.4 Generated data families Natural numbers, lists, public sums, and public equality are prelude datatypes generated from descriptions. Their types evaluate to `VMu I D i`; their constructors evaluate to `VDescCon D i payload`; their eliminators elaborate to `DescInd` over the generated description. ```text H.nat == μ Unit NatDT.D tt H.zero == descCon NatDT.D tt H.succ n == descCon NatDT.D tt H.listOf A == μ Unit (ListDT.D A) tt H.nil A == descCon (ListDT.D A) tt H.cons A h t == descCon (ListDT.D A) tt H.sum A B == μ Unit (SumDT.D 0 A B) tt H.eq A a b == μ A (EqDT.D A a) b ``` The generated constructor payloads still use private bootstrap coproduct/identity values where the description interpretation requires them. These are representation details, not public eliminators. Deep generated natural/list values are stack-safe through the constructor flattening and `desc-con` trampoline paths, rather than through primitive nat/list evaluator cases. ### 4.5 Description interpretation, Lift, Squash, and Funext The current kernel includes several small primitives used by generated datatypes and proof-oriented APIs: ```text eval(ρ, DescDescApp(I,L)) = tagged descDesc value eval(ρ, InterpD(level,I,D,X,i)) = vInterpD(level,I,D,X,i) eval(ρ, AllD(level,I,D,K,X,M,i,d)) = vAllD(level,I,D,K,X,M,i,d) eval(ρ, EverywhereD(level,I,D,K,X,M,ih,i,d)) = vEverywhereD(level,I,D,K,X,M,ih,i,d) eval(ρ, Lift(l,m,eq,A)) = vLiftF(l,m,eq,A) eval(ρ, LiftIntro(l,m,eq,A,a)) = vLiftIntroF(l,m,eq,A,a) eval(ρ, LiftElim(l,m,eq,A,x)) = vLiftElimF(l,m,eq,A,x) eval(ρ, Squash(A)) = VSquash(eval(ρ,A)) eval(ρ, SquashIntro(a)) = VSquashIntro(eval(ρ,a)) eval(ρ, SquashElim(A,B,f,x)) = vSquashElim(eval(ρ,A), eval(ρ,B), eval(ρ,f), eval(ρ,x)) eval(ρ, Funext) = VFunext eval(ρ, OpaqueLam(fnBox, piTy)) = VOpaqueLam(fnBox, eval(ρ,piTy)) eval(ρ, LitVal(v)) = v ``` `LitVal v` is the splice operator of two-level type theory (Kovács, "Staged Compilation with Two-Level Type Theory", POPL 2024; Annenkov–Capriotti–Kraus–Sattler 2019): a closed semantic value reflected into the syntactic domain. The eval rule discards the environment, so the carried `v` must be closed (free de Bruijn levels would never resolve). Quotation reads through to the underlying value — `LitVal` is invisible at the Val layer, so conv, quote, and the ι/β/η rules operate on `v` directly without any new transparency rule. The kernel uses `LitVal` as the canonical Val→Tm lift on paths that would otherwise call `quote 0 v` on a value destined for re-evaluation (notably the effect-handler bridge in `src/experimental/desc-interp/trampoline.nix`). Reflection avoids the O(size v) structural walk of `quote`, which would compound across iterated bridge steps. `Lift l m eq A` collapses definitionally to `A` when `l` and `m` are level-convertible; nested Lifts compose; `lower (lift a)` reduces to `a`; and `lift (lower x)` eta-reduces on stuck neutrals. The `eq` witness is irrelevant for conversion once the levels and underlying type match. `Squash A` is proof-irrelevant. Any two `SquashIntro` inhabitants at a shared `Squash A` type are definitionally equal, and neutral inhabitants of `Squash A` convert against introductions by the shared-type invariant. `DescDescApp` stamps a canonical reference on the generated `descDesc I L` value so quotation and conversion can avoid descending into the self-describing universe spiral. `InterpD`, `AllD`, and `EverywhereD` are the kernel primitives behind description interpretation and the generated `desc-ind` iota rule. ### 4.6 Unit ``` eval(ρ, Unit) = VUnit eval(ρ, Tt) = VTt ``` Unit has no eliminator in the core. The kernel implements ⊤-η: any neutral of type ⊤ converts against `VTt` (see §6.3). Sound in the type-free conv because conv is always called on values sharing a type; if one side is `VTt`, the shared type is ⊤ and the neutral's only inhabitant is `Tt`. ### 4.6′ Empty ``` eval(ρ, Empty) = VEmpty ``` Empty has no introduction. The eliminator `Absurd` (§4.6″) discharges any neutral of type Empty. ### 4.6″ Absurd ``` eval(ρ, Absurd(P, x)) = vAbsurd(eval(ρ, P), eval(ρ, x)) vAbsurd(P, VNe(l, sp)) = VNe(l, sp ++ [EAbsurd(P)]) vAbsurd(P, _) = THROW "kernel bug: vAbsurd on non-neutral" ``` `Absurd(P, x)` is the unique map from the initial object to any type `P` at any universe level. It is well-typed only when `x : Empty`; since `Empty` has no canonical inhabitants, `x` is always neutral in sound code. The non-neutral case is a kernel invariant violation — THROW rather than silently propagate, matching `vFst`/`vBootJ` hygiene. There is no β-rule for `Absurd`: it only fires on neutrals, and no canonical inhabitant of `Empty` exists to trigger reduction. Two `Absurd` redexes on the same neutral with definitionally equal `P` are conv-equal via the `EAbsurd` spine frame (§6.4). ### 4.7 Bootstrap coproduct ``` eval(ρ, BootSum(A, B)) = VBootSum(eval(ρ, A), eval(ρ, B)) eval(ρ, BootInl(A, B, t)) = VBootInl(eval(ρ, A), eval(ρ, B), eval(ρ, t)) eval(ρ, BootInr(A, B, t)) = VBootInr(eval(ρ, A), eval(ρ, B), eval(ρ, t)) eval(ρ, BootSumElim(A,B,P,l,r,s)) = vBootSumElim(eval(ρ,A), eval(ρ,B), eval(ρ,P), eval(ρ,l), eval(ρ,r), eval(ρ,s)) ``` where: ``` vBootSumElim(A, B, P, l, r, VBootInl(_, _, v)) = vApp(l, v) vBootSumElim(A, B, P, l, r, VBootInr(_, _, v)) = vApp(r, v) vBootSumElim(A, B, P, l, r, VNe(k, sp)) = VNe(k, sp ++ [EBootSumElim(A, B, P, l, r)]) vBootSumElim(_, _, _, _, _, _) = THROW "kernel bug: vBootSumElim on non-boot-sum" ``` This coproduct is private to `descPlus` interpretation. Public `H.sum`, `H.inl`, `H.inr`, and `H.sumElim` route through generated `SumDT`. ### 4.8 Bootstrap identity ``` eval(ρ, BootEq(A, a, b)) = VBootEq(eval(ρ, A), eval(ρ, a), eval(ρ, b)) eval(ρ, BootRefl) = VBootRefl eval(ρ, BootJ(A, a, P, pr, b, eq)) = vBootJ(eval(ρ,A), eval(ρ,a), eval(ρ,P), eval(ρ,pr), eval(ρ,b), eval(ρ,eq)) ``` where: ``` vBootJ(A, a, P, pr, b, VBootRefl) = pr vBootJ(A, a, P, pr, b, VNe(l,sp)) = VNe(l, sp ++ [EBootJ(A, a, P, pr, b)]) vBootJ(_, _, _, _, _, _) = THROW "kernel bug: vBootJ on non-refl" ``` This identity substrate is private to `descRet`, Lift witnesses, index transport, and no-confusion helpers. Public `H.eq`, `H.refl`, and `H.j` route through generated `EqDT`. ### 4.9 Universes ``` eval(ρ, U(i)) = VU(i) ``` ### 4.10 Axiomatized primitives Type formers evaluate to their corresponding values. Literals carry their payload through. No computation, no recursion — these are axiomatized constants. ``` eval(ρ, String) = VString eval(ρ, Int) = VInt eval(ρ, Float) = VFloat eval(ρ, Attrs) = VAttrs eval(ρ, Path) = VPath eval(ρ, Derivation) = VDerivation eval(ρ, Function) = VFunction eval(ρ, Any) = VAny eval(ρ, StringLit(s)) = VStringLit(s) eval(ρ, IntLit(n)) = VIntLit(n) eval(ρ, FloatLit(f)) = VFloatLit(f) eval(ρ, AttrsLit) = VAttrsLit eval(ρ, PathLit) = VPathLit eval(ρ, DerivationLit) = VDerivationLit eval(ρ, FnLit) = VFnLit eval(ρ, AnyLit) = VAnyLit ``` Most primitives have no eliminators. They exist to integrate Nix's native types into the kernel's type system as opaque, axiomatized constants. The exception is String, which has `StrEq` (§4.11). ### 4.11 String equality (StrEq) ``` eval(ρ, StrEq(lhs, rhs)) = vStrEq(eval(ρ, lhs), eval(ρ, rhs)) ``` where: ``` -- trueV / falseV are the plus-encoded derived booleans: -- trueV = VDescCon boolDescV VTt (VBootInl eqTtV eqTtV VBootRefl) -- falseV = VDescCon boolDescV VTt (VBootInr eqTtV eqTtV VBootRefl) -- where boolDescV is the generated encoded BoolDT description. vStrEq(VStringLit(s₁), VStringLit(s₂)) = if s₁ == s₂ then trueV else falseV vStrEq(VNe(l, sp), rhs) = VNe(l, sp ++ [EStrEq(rhs)]) vStrEq(lhs, VNe(l, sp)) = VNe(l, sp ++ [EStrEq(lhs)]) vStrEq(_, _) = THROW "kernel bug: vStrEq on non-string" ``` `StrEq` is a binary predicate on strings. Both arguments must be of type `String`. The result type is the derived `H.bool` — `μ ⊤ (plus (retI tt) (retI tt)) tt` — which is the kernel representation of booleans after their retirement as primitives. Unlike other eliminators, StrEq has no motive: it always returns `H.bool`, not a dependent type. When both arguments are concrete string literals, `vStrEq` reduces to the plus-encoded `true_` or `false_` value by Nix-level string comparison. When either argument is neutral, the neutral's spine is extended with `EStrEq` carrying the other argument. This is sound because `StrEq` is symmetric: `StrEq(a, b) ≡ StrEq(b, a)` for all `a, b : String`. --- ## 5. Quotation Rules `quote(d, v)` converts a value back to a term, converting levels to indices. `d` is the current binding depth. ``` quote(d, VPi(n, A, cl)) = Pi(n, quote(d, A), quote(d+1, instantiate(cl, fresh(d)))) quote(d, VLam(n, A, cl)) = Lam(n, quote(d, A), quote(d+1, instantiate(cl, fresh(d)))) quote(d, VSigma(n, A, cl)) = Sigma(n, quote(d, A), quote(d+1, instantiate(cl, fresh(d)))) quote(d, VPair(a, b)) = Pair(quote(d, a), quote(d, b), _) quote(d, VUnit) = Unit quote(d, VTt) = Tt quote(d, VEmpty) = Empty quote(d, VBootSum(A, B)) = BootSum(quote(d, A), quote(d, B)) quote(d, VBootInl(A, B, v)) = BootInl(quote(d, A), quote(d, B), quote(d, v)) quote(d, VBootInr(A, B, v)) = BootInr(quote(d, A), quote(d, B), quote(d, v)) quote(d, VBootEq(A, a, b)) = BootEq(quote(d, A), quote(d, a), quote(d, b)) quote(d, VBootRefl) = BootRefl quote(d, VU(i)) = U(i) quote(d, VString) = String quote(d, VInt) = Int quote(d, VFloat) = Float quote(d, VAttrs) = Attrs quote(d, VPath) = Path quote(d, VDerivation) = Derivation quote(d, VFunction) = Function quote(d, VAny) = Any quote(d, VStringLit(s)) = StringLit(s) quote(d, VIntLit(n)) = IntLit(n) quote(d, VFloatLit(f)) = FloatLit(f) quote(d, VAttrsLit) = AttrsLit quote(d, VPathLit) = PathLit quote(d, VDerivationLit) = DerivationLit quote(d, VFnLit) = FnLit quote(d, VAnyLit) = AnyLit quote(d, VNe(l, sp)) = quoteSp(d, Var(d - l - 1), sp) quoteSp(d, head, []) = head quoteSp(d, head, [EApp(v) | rest]) = quoteSp(d, App(head, quote(d, v)), rest) quoteSp(d, head, [EFst | rest]) = quoteSp(d, Fst(head), rest) quoteSp(d, head, [ESnd | rest]) = quoteSp(d, Snd(head), rest) quoteSp(d, head, [EBootSumElim(A,B,P,l,r) | rest]) = quoteSp(d, BootSumElim(quote(d,A), quote(d,B), quote(d,P), quote(d,l), quote(d,r), head), rest) quoteSp(d, head, [EBootJ(A,a,P,pr,b) | rest]) = quoteSp(d, BootJ(quote(d,A), quote(d,a), quote(d,P), quote(d,pr), quote(d,b), head), rest) quoteSp(d, head, [EStrEq(arg) | rest]) = quoteSp(d, StrEq(head, quote(d, arg)), rest) quoteSp(d, head, [EAbsurd(P) | rest]) = quoteSp(d, Absurd(quote(d, P), head), rest) fresh(d) = VNe(d, []) ``` --- ## 6. Conversion Rules `conv(d, v₁, v₂)` checks definitional equality of two values at binding depth `d`. Returns boolean. **No type information is used** — conversion is purely structural on normalized values. ### 6.1 Structural rules ``` conv(d, VU(i), VU(j)) = (i == j) conv(d, VUnit, VUnit) = true conv(d, VTt, VTt) = true conv(d, VEmpty, VEmpty) = true conv(d, VBootRefl, VBootRefl) = true conv(d, VString, VString) = true conv(d, VInt, VInt) = true conv(d, VFloat, VFloat) = true conv(d, VAttrs, VAttrs) = true conv(d, VPath, VPath) = true conv(d, VDerivation, VDerivation) = true conv(d, VFunction, VFunction) = true conv(d, VAny, VAny) = true conv(d, VStringLit(s₁), VStringLit(s₂)) = (s₁ == s₂) conv(d, VIntLit(n₁), VIntLit(n₂)) = (n₁ == n₂) conv(d, VFloatLit(f₁), VFloatLit(f₂)) = (f₁ == f₂) conv(d, VAttrsLit, VAttrsLit) = true conv(d, VPathLit, VPathLit) = true conv(d, VDerivationLit, VDerivationLit) = true conv(d, VFnLit, VFnLit) = true conv(d, VAnyLit, VAnyLit) = true ``` ### 6.2 Binding forms To compare under binders, generate a fresh variable and instantiate: ``` conv(d, VPi(_, A₁, cl₁), VPi(_, A₂, cl₂)) = conv(d, A₁, A₂) ∧ conv(d+1, instantiate(cl₁, fresh(d)), instantiate(cl₂, fresh(d))) conv(d, VLam(_, _, cl₁), VLam(_, _, cl₂)) = conv(d+1, instantiate(cl₁, fresh(d)), instantiate(cl₂, fresh(d))) conv(d, VLam(_, _, cl), v) = -- Π-η conv(d+1, instantiate(cl, fresh(d)), vApp(v, fresh(d))) -- only fires when v is not a VLam conv(d, v, VLam(_, _, cl)) = -- Π-η conv(d+1, vApp(v, fresh(d)), instantiate(cl, fresh(d))) -- only fires when v is not a VLam conv(d, VSigma(_, A₁, cl₁), VSigma(_, A₂, cl₂)) = conv(d, A₁, A₂) ∧ conv(d+1, instantiate(cl₁, fresh(d)), instantiate(cl₂, fresh(d))) ``` The two Π-η rules fire when exactly one side is a VLam; both sides being VLam falls through to the symmetric VLam/VLam rule above. Sound because conv is always called on values sharing a type — if one side is VLam, that type is VPi, and the other side's only inhabitants up to definitional equality are its η-expansions. Termination: each rule strictly decreases VLam-depth on the side it fires on, so no nested firing can loop. ### 6.3 Compound values ``` conv(d, VPair(a₁, b₁), VPair(a₂, b₂)) = conv(d, a₁, a₂) ∧ conv(d, b₁, b₂) conv(d, VPair(a, b), VNe(l, sp)) = conv(d, a, vFst(VNe(l, sp))) ∧ conv(d, b, vSnd(VNe(l, sp))) -- Σ-η conv(d, VNe(l, sp), VPair(a, b)) = conv(d, vFst(VNe(l, sp)), a) ∧ conv(d, vSnd(VNe(l, sp)), b) -- Σ-η conv(d, VTt, VNe(_, _)) = true -- ⊤-η conv(d, VNe(_, _), VTt) = true -- ⊤-η conv(d, VBootSum(A₁, B₁), VBootSum(A₂, B₂)) = conv(d, A₁, A₂) ∧ conv(d, B₁, B₂) conv(d, VBootInl(A₁, B₁, v₁), VBootInl(A₂, B₂, v₂)) = conv(d, A₁, A₂) ∧ conv(d, B₁, B₂) ∧ conv(d, v₁, v₂) conv(d, VBootInr(A₁, B₁, v₁), VBootInr(A₂, B₂, v₂)) = conv(d, A₁, A₂) ∧ conv(d, B₁, B₂) ∧ conv(d, v₁, v₂) conv(d, VBootEq(A₁, a₁, b₁), VBootEq(A₂, a₂, b₂)) = conv(d, A₁, A₂) ∧ conv(d, a₁, a₂) ∧ conv(d, b₁, b₂) ``` ### 6.4 Neutrals ``` conv(d, VNe(l₁, sp₁), VNe(l₂, sp₂)) = (l₁ == l₂) ∧ convSp(d, sp₁, sp₂) convSp(d, [], []) = true convSp(d, [e₁|r₁], [e₂|r₂]) = convElim(d, e₁, e₂) ∧ convSp(d, r₁, r₂) convSp(d, _, _) = false -- different lengths convElim(d, EApp(v₁), EApp(v₂)) = conv(d, v₁, v₂) convElim(d, EFst, EFst) = true convElim(d, ESnd, ESnd) = true convElim(d, EBootSumElim(A₁,B₁,P₁,l₁,r₁), EBootSumElim(A₂,B₂,P₂,l₂,r₂)) = conv(d, A₁, A₂) ∧ conv(d, B₁, B₂) ∧ conv(d, P₁, P₂) ∧ conv(d, l₁, l₂) ∧ conv(d, r₁, r₂) convElim(d, EBootJ(A₁,a₁,P₁,pr₁,b₁), EBootJ(A₂,a₂,P₂,pr₂,b₂)) = conv(d, A₁, A₂) ∧ conv(d, a₁, a₂) ∧ conv(d, P₁, P₂) ∧ conv(d, pr₁, pr₂) ∧ conv(d, b₁, b₂) convElim(d, EStrEq(arg₁), EStrEq(arg₂)) = conv(d, arg₁, arg₂) convElim(d, EAbsurd(P₁), EAbsurd(P₂)) = conv(d, P₁, P₂) convElim(_, _, _) = false ``` ### 6.5 Catch-all ``` conv(d, _, _) = false ``` Any pair of values not matching the above rules is not definitionally equal. **Π-eta, Σ-eta, and ⊤-eta are applied** (see §6.2 for Π-η, §6.3 for Σ-η and ⊤-η): `f` converts against `λx. f x` under a fresh binder; a pair `⟨a, b⟩` converts against a neutral `x : Σ` by projecting both sides; and any neutral of type `⊤` converts against `tt`. All three η-rules are sound in the type-free conv because conv is always called on two values sharing a type — the side carrying the canonical form (VLam, VPair, VTt) witnesses the shared type's shape (VPi, VSigma, ⊤), and the other side's η-expansion is its only inhabitant up to definitional equality. **Π-η rationale.** Π-η matches the standard semantic models of MLTT (PER, presheaf, simplicial sets) and is the η-rule consistent with funext: `f ≡ λx. f x` definitionally. Without it, definitional equality on Pi-typed values diverges from the equality the surface language must reason about — every elaborator that produces a function value would have to maintain its own η-normal form before submitting to conv. With it, conv handles the canonical-vs-neutral case by descending under one binder and continues structurally; subsequent ⊤-η, Σ-η, and neutral-vs-neutral rules fire as usual on the body. This composition in particular is what closes assemblies that pair a surface `descPi k S (λ_:S. tt) D` (a 3-arg combinator that fills the kernel's `f : S → ⊤` slot with a constant lambda) against a kernel-reconstructed `descPi k S f D` whose `f` is a case-bound variable. ### 6.6 Level conversion (convLevel) Level expressions form a join-semilattice with `zero` as bottom and `max` as join. `convLevel(d, k₁, k₂)` checks definitional equality of two Level values modulo the semilattice laws. **Fast path** (syntactic equality): ``` convLevel(d, k, k) = true ``` When the same Level value reaches both sides of conv (e.g. a description's level is reused unchanged across recursive children), the syntactic-equality check skips the normaliser entirely. Allocations from re-normalising structurally-identical levels dominate hot CHECK loops, so this short-circuit is non-optional. **Normalisation.** Each Level value is reduced to its canonical form before structural comparison: - `max(k, zero) = max(zero, k) = k` (zero absorption) - `max(k, k) = k` (idempotence) - `suc(max(a, b)) = max(suc(a), suc(b))` (suc distributes over max) - `max` operands are sorted to a canonical order The canonical form is `max(s₁, …, sₙ)` where each `sᵢ = sucᵐ(zero)` or `sᵢ = sucᵐ(VNe(_, _))`, sorted lexicographically. After normalisation the comparison is structural: ``` convLevel(d, k₁, k₂) = (normLevel(k₁) ≡_struct normLevel(k₂)) ``` Used by description and universe CHECK rules (§7.6) to verify that two level expressions denote the same level. --- ## 7. Typing Rules (Bidirectional) ### 7.1 Contexts ``` Ctx ::= { env : Env, -- values for evaluation types : [Val], -- types of bound variables (indexed by de Bruijn) depth : ℕ -- current binding depth } emptyCtx = { env = [], types = [], depth = 0 } extend(Γ, n, A) = { env = [fresh(Γ.depth)] ++ Γ.env, types = [A] ++ Γ.types, depth = Γ.depth + 1 } lookupType(Γ, i) = Γ.types[i] -- THROW if i >= length(Γ.types) ``` ### 7.2 Notation ``` Γ ⊢ t ⇐ A ↝ t' checking mode: check(Γ, t, A) = t' Γ ⊢ t ⇒ A ↝ t' synthesis mode: infer(Γ, t) = (t', A) Γ ⊢ T type ↝ T' type formation: checkType(Γ, T) = T' Γ ⊢ T type@i ↝ T' type + level: checkTypeLevel(Γ, T) = (T', i) ``` The output `t'` is the elaborated core term (fully annotated). ### 7.3 Synthesis rules (infer) **Var** ``` lookupType(Γ, i) = A ────────────────────── Γ ⊢ Var(i) ⇒ A ↝ Var(i) ``` **Ann** (annotation) ``` Γ ⊢ A type ↝ A' Â = eval(Γ.env, A') Γ ⊢ t ⇐ Â ↝ t' ────────────────────── Γ ⊢ Ann(t, A) ⇒ Â ↝ Ann(t', A') ``` **App** (application) ``` Γ ⊢ f ⇒ fTy ↝ f' whnf(fTy) = VPi(n, A, cl) Γ ⊢ u ⇐ A ↝ u' B = instantiate(cl, eval(Γ.env, u')) ────────────────────── Γ ⊢ App(f, u) ⇒ B ↝ App(f', u') ``` **CRITICAL**: `whnf(fTy)` must normalize `fTy` to weak head normal form before pattern matching. If `fTy` is a let-unfolding or a neutral that reduces further, the match will fail spuriously. In this kernel, `eval` already produces WHNF, so `whnf(v) = v` for all values. But this invariant must be maintained if the value representation changes. **Fst** (first projection) ``` Γ ⊢ t ⇒ tTy ↝ t' whnf(tTy) = VSigma(n, A, cl) ────────────────────── Γ ⊢ Fst(t) ⇒ A ↝ Fst(t') ``` **Snd** (second projection) ``` Γ ⊢ t ⇒ tTy ↝ t' whnf(tTy) = VSigma(n, A, cl) B = instantiate(cl, vFst(eval(Γ.env, t'))) ────────────────────── Γ ⊢ Snd(t) ⇒ B ↝ Snd(t') ``` **Eliminator motive checking (checkMotive).** All eliminators require a motive `P : domTy → U(k)` for some `k`. The implementation provides a shared `checkMotive` helper that handles two forms: - Lambda motives (`P = λx. body`): extend the context with `x : domTy` and verify `body` is a type via `checkType`. - Non-lambda motives: infer the type and verify it has shape `VPi(_, domTy, _ → VU(k))` for some `k`. The `k` is not fixed — motives may target any universe level, enabling **large elimination** (eliminators whose return type is a type, not a value). Generated datatype eliminators use this same motive checker through `DescInd`. **Generated eliminators.** Public eliminators for natural numbers, lists, sums, equality, vectors, finite sets, W-types, and user-defined datatypes are generated as applications of `DescInd` to their datatype description. The checker validates the generated motive and branch terms against the generic indexed-description eliminator. **Public identity elimination.** `H.j A a P pr b eq` is a HOAS adapter that preserves the usual J-shaped arguments, but elaborates to `EqDT.elim`. The adapter checks `pr` against `P a (EqDT.refl A a)` and checks `eq` against `EqDT.T A a b` before emitting the generated eliminator spine. **Bootstrap identity elimination.** Internal description machinery may still use `BootJ`: ``` Γ ⊢ A type ↝ A' Â = eval(Γ.env, A') Γ ⊢ a ⇐ Â ↝ a' â = eval(Γ.env, a') Γ ⊢ P ⇐ <Π(y : A). Π(e : BootEq A a y). U(k)> ↝ P' P̂ = eval(Γ.env, P') Γ ⊢ pr ⇐ vApp(vApp(P̂, â), VBootRefl) ↝ pr' Γ ⊢ b ⇐ Â ↝ b' b̂ = eval(Γ.env, b') Γ ⊢ eq ⇐ VBootEq(Â, â, b̂) ↝ eq' ────────────────────── Γ ⊢ BootJ(A, a, P, pr, b, eq) ⇒ vApp(vApp(P̂, b̂), eval(Γ.env, eq')) ↝ BootJ(A', a', P', pr', b', eq') ``` **Bootstrap J motive verification.** For non-lambda motives, the implementation structurally verifies all three components: 1. Outer Pi domain matches `A` (conversion check) 2. Inner Pi domain matches `BootEq(A, a, y)` (conversion check) 3. Innermost codomain is `VU(k)` for some `k` For lambda motives (`P = λy. body`), the body is checked via `checkMotive` against `BootEq(A, a, y)`, which performs the same verification on the inner structure. This catches motive errors at the motive itself rather than deferring to the base case. **Axiomatized primitive type formers** (synthesis) Primitive type formers are synthesized directly — they infer as inhabitants of `U(0)`: ``` ────────────────────── Γ ⊢ String ⇒ VU(0) ↝ String ────────────────────── Γ ⊢ Int ⇒ VU(0) ↝ Int ``` (Similarly for Float, Attrs, Path, Derivation, Function, Any — all at level 0.) **Primitive literals** (synthesis) Literals synthesize their corresponding type: ``` ────────────────────── Γ ⊢ StringLit(s) ⇒ VString ↝ StringLit(s) ────────────────────── Γ ⊢ IntLit(n) ⇒ VInt ↝ IntLit(n) ────────────────────── Γ ⊢ FloatLit(f) ⇒ VFloat ↝ FloatLit(f) ``` (Similarly for AttrsLit → VAttrs, PathLit → VPath, DerivationLit → VDerivation, FnLit → VFunction, AnyLit → VAny.) **StrEq** (string equality) ``` boolDescV = eval [] (elab BoolDT.D) boolV = VMu VUnit boolDescV VTt Γ ⊢ lhs ⇐ VString ↝ lhs' Γ ⊢ rhs ⇐ VString ↝ rhs' ────────────────────── Γ ⊢ StrEq(lhs, rhs) ⇒ boolV ↝ StrEq(lhs', rhs') ``` Both arguments are checked against `VString`. The result type is the derived `H.bool` — `μ ⊤ (plus (retI tt) (retI tt)) tt` — written `boolV` above. StrEq is not a dependent eliminator: it has no motive parameter. ### 7.4 Checking rules (check) **Lam** (lambda introduction) ``` whnf(A) = VPi(n, dom, cl) Γ' = extend(Γ, n, dom) cod = instantiate(cl, fresh(Γ.depth)) Γ' ⊢ t ⇐ cod ↝ t' ────────────────────── Γ ⊢ Lam(n, _, t) ⇐ A ↝ Lam(n, quote(Γ.depth, dom), t') ``` **Pair** (pair introduction) ``` whnf(T) = VSigma(n, A, cl) Γ ⊢ a ⇐ A ↝ a' B = instantiate(cl, eval(Γ.env, a')) Γ ⊢ b ⇐ B ↝ b' ────────────────────── Γ ⊢ Pair(a, b, _) ⇐ T ↝ Pair(a', b', quote(Γ.depth, T)) ``` **Generated datatype constructors.** Public constructors such as `H.zero`, `H.succ`, `H.nil`, `H.cons`, `H.inl`, `H.inr`, and user-defined datatype constructors check as `DescCon` introductions against their generated `VMu` type. Their payloads are checked against the interpretation of the constructor's description. Deep generated natural/list constructor chains are handled by flat constructor elaboration and `desc-con` trampolines. **Tt** ``` whnf(A) = VUnit ────────────────────── Γ ⊢ Tt ⇐ A ↝ Tt ``` **BootRefl** ``` whnf(T) = VBootEq(A, a, b) conv(Γ.depth, a, b) = true ────────────────────── Γ ⊢ BootRefl ⇐ T ↝ BootRefl ``` If `conv(Γ.depth, a, b) = false`, this is a **type error**: the two sides of the equation are not definitionally equal, and `BootRefl` cannot prove the equation. Public `H.refl` is resolved by HOAS check mode against generated `EqDT.T A a a`, elaborating to `EqDT.refl A a`; unrestricted inference for bare public `refl` is rejected. **Primitive literals** (checked against their corresponding types) ``` whnf(A) = VString ────────────────────── Γ ⊢ StringLit(s) ⇐ A ↝ StringLit(s) whnf(A) = VInt ────────────────────── Γ ⊢ IntLit(n) ⇐ A ↝ IntLit(n) whnf(A) = VFloat ────────────────────── Γ ⊢ FloatLit(f) ⇐ A ↝ FloatLit(f) whnf(A) = VAttrs ────────────────────── Γ ⊢ AttrsLit ⇐ A ↝ AttrsLit whnf(A) = VPath ────────────────────── Γ ⊢ PathLit ⇐ A ↝ PathLit whnf(A) = VDerivation ────────────────────── Γ ⊢ DerivationLit ⇐ A ↝ DerivationLit whnf(A) = VFunction ────────────────────── Γ ⊢ FnLit ⇐ A ↝ FnLit whnf(A) = VAny ────────────────────── Γ ⊢ AnyLit ⇐ A ↝ AnyLit ``` **Let** ``` Γ ⊢ A type ↝ A' Â = eval(Γ.env, A') Γ ⊢ t ⇐ Â ↝ t' t̂ = eval(Γ.env, t') Γ' = { env = [t̂] ++ Γ.env, types = [Â] ++ Γ.types, depth = Γ.depth + 1 } Γ' ⊢ u ⇐ B ↝ u' ────────────────────── Γ ⊢ Let(n, A, t, u) ⇐ B ↝ Let(n, A', t', u') ``` Note: `Let` in checking mode — the expected type `B` is for the body `u`, not for the definition `t`. **Sub** (mode switch: fall through to synthesis) ``` Γ ⊢ t ⇒ A ↝ t' conv(Γ.depth, A, B) = true ────────────────────── Γ ⊢ t ⇐ B ↝ t' ``` This is the catch-all. If no other checking rule applies, try synthesis and verify the inferred type matches the expected type. ### 7.5 Type formation (checkType / checkTypeLevel) The implementation provides two variants: `checkType(Γ, T)` returns only the elaborated term, while `checkTypeLevel(Γ, T)` returns both the elaborated term and the universe level. `checkType` is a thin wrapper: `checkType(Γ, T) = checkTypeLevel(Γ, T).term`. Universe levels are computed structurally during the type formation check (see §8.2), not by post-hoc inspection of evaluated values. ``` ────────────────────── Γ ⊢ Unit type ↝ Unit ────────────────────── Γ ⊢ Empty type ↝ Empty Γ ⊢ P type ↝ P' level(P') = k Γ ⊢ x ⇐ Empty ↝ x' ────────────────────── Γ ⊢ Absurd(P, x) ⇒ P̂ (P̂ = eval(Γ.env, P')) ↝ Absurd(P', x') ────────────────────── Γ ⊢ String type ↝ String (Similarly for Int, Float, Attrs, Path, Derivation, Function, Any) ────────────────────── Γ ⊢ U(i) type ↝ U(i) Γ ⊢ A type ↝ A' Γ ⊢ B type ↝ B' ────────────────────── Γ ⊢ BootSum(A, B) type ↝ BootSum(A', B') Γ ⊢ A type ↝ A' Â = eval(Γ.env, A') Γ ⊢ a ⇐ Â ↝ a' Γ ⊢ b ⇐ Â ↝ b' ────────────────────── Γ ⊢ BootEq(A, a, b) type ↝ BootEq(A', a', b') Γ ⊢ A type ↝ A' Â = eval(Γ.env, A') Γ' = extend(Γ, n, Â) Γ' ⊢ B type ↝ B' ────────────────────── Γ ⊢ Pi(n, A, B) type ↝ Pi(n, A', B') Γ ⊢ A type ↝ A' Â = eval(Γ.env, A') Γ' = extend(Γ, n, Â) Γ' ⊢ B type ↝ B' ────────────────────── Γ ⊢ Sigma(n, A, B) type ↝ Sigma(n, A', B') -- Fallback: infer and check it's a universe Γ ⊢ T ⇒ A ↝ T' whnf(A) = VU(i) ────────────────────── Γ ⊢ T type ↝ T' ``` Public generated data types enter this judgment through the fallback: their expanded `Mu(...)` terms infer a universe. ### 7.6 Descriptions: typing rules Descriptions classify strictly-positive datatype signatures over an index type `I`. A description `D : Desc^k I` quantifies over at most universe level `k`: `descArg^k S T` requires `S : U(k)`, and `descPi^k S f D` requires `S : U(k)` and `f : S → I`. The description's level `k` is recovered from the surrounding `μ` (via `mu I D i`) at elaboration time; CHECK rules thread `k` through recursive children without re-synthesising it ("homogeneity by typing", below). **Notation.** `Desc^k I` is the kernel value `VDesc(K, Î)` where `K : Level` (§8.5). `μ I D i` is the kernel value `VMu(Î, D̂, î)`, the `i`-th type in the family classified by `D`. #### 7.6.1 Description eliminators are encoded at the HOAS layer There is no `DescElim` core term in the current kernel. The HOAS surface exposes description elimination by applying the generated `encodeDescElim` program to a `descDesc I K` value. The trusted core operations involved are `DescDescApp`, `InterpD`, `AllD`, `EverywhereD`, and `DescInd`. The level agreement once handled by a primitive `DescElim` rule is now checked by the ordinary typing rules for `Desc I K`, `interpD`, and the encoded eliminator's Pi type. `DescDescApp(I, K)` carries a canonical reference so quotation and conversion can recognize the self-describing description universe without forcing the recursive encoding. #### 7.6.2 Homogeneity by typing (desc CHECK rules) When CHECKing a description against `Desc^K I`, recursive sub-checks inherit `K` directly from the surrounding type rather than synthesising their own. The principle: > A description's recursive children inhabit the same description type > as the parent. Reconstructing `VDesc(K, Î)` per recursive call to > thread the type — only to have the recursive CHECK pattern-match > it back open — is wasted allocation. The CHECK rules pass the surrounding type directly: ``` whnf(ty) = VDesc(K, Î) Γ ⊢ S ⇐ VU(K) ↝ S' Ŝ = eval(Γ.env, S') Γ ⊢ T ⇐ VPi(_, Ŝ, ([], ty)) ↝ T' -- recursive, same ty ────────────────────── Γ ⊢ DescArg(K, S, T) ⇐ ty ↝ DescArg(K, S', T') whnf(ty) = VDesc(K, Î) Γ ⊢ S ⇐ VU(K) ↝ S' Ŝ = eval(Γ.env, S') Γ ⊢ f ⇐ VPi(_, Ŝ, ([], Î)) ↝ f' Γ ⊢ D ⇐ VPi(_, Ŝ, ([], ty)) ↝ D' -- recursive, same ty ────────────────────── Γ ⊢ DescPi(K, S, f, D) ⇐ ty ↝ DescPi(K, S', f', D') whnf(ty) = VDesc(K, Î) Γ ⊢ A ⇐ ty ↝ A' -- recursive, same ty Γ ⊢ B ⇐ ty ↝ B' -- recursive, same ty ────────────────────── Γ ⊢ DescPlus(A, B) ⇐ ty ↝ DescPlus(A', B') whnf(ty) = VDesc(K, Î) Γ ⊢ j ⇐ Î ↝ j' Γ ⊢ D ⇐ ty ↝ D' -- recursive, same ty ────────────────────── Γ ⊢ DescRec(j, D) ⇐ ty ↝ DescRec(j', D') whnf(ty) = VDesc(K, Î) Γ ⊢ j ⇐ Î ↝ j' ────────────────────── Γ ⊢ DescRet(j) ⇐ ty ↝ DescRet(j') ``` The `K` and `Î` are recovered once at the outermost CHECK, then reused unchanged by every recursive sub-check. The rules' written shape mirrors the implementation: `ty` (the surrounding `VDesc` value) flows into recursive positions verbatim, and `convLevel` on the inner description's level reduces to `convLevel(K, K)` — the syntactic-equality fast-path of §6.6 fires. #### 7.6.3 desc-con CHECK with checkDescAtAnyLevel A `μ I D i` introduction `descCon D i d` checks the description `D` against `Desc^K I`, where `K` is recovered from the surrounding `μ`'s classifier. Because `μ` at indexed positions is checked against `VMu(Î, D̂, î)` whose `D̂` field carries no externally-visible level, the CHECK rule infers the level from the type-of-`D̂` at elaboration: ``` whnf(ty) = VMu(Î, D̂, î) checkDescAtAnyLevel(Γ, D) = (D', K) conv(Γ.depth, eval(Γ.env, D'), D̂) = true Γ ⊢ i ⇐ Î ↝ i' conv(Γ.depth, eval(Γ.env, i'), î) = true Γ ⊢ payload ⇐ ↝ payload' ────────────────────── Γ ⊢ DescCon(D, i, payload) ⇐ ty ↝ DescCon(D', i', payload') ``` `checkDescAtAnyLevel(Γ, D)` is the bidirectional bridge: it elaborates `D` at the most specific admissible level synthesised from `D`'s structure, returning both the elaborated term and the inferred `K`. Equivalent to `∃K. Γ ⊢ D ⇐ Desc^K Î`. The bidirectional discipline at index positions is preserved: canonical intros (`tt`, `zero`, `refl`, …) at the index slot remain checkable-only. --- ## 8. Universe Rules ### 8.1 Universe formation ``` U(i) : U(i + 1) for all i ≥ 0 ``` ### 8.2 Type former levels Universe levels are computed by `checkTypeLevel`, which returns `{ term; level; }` from the **typing derivation**, not from post-hoc value inspection. This avoids the problem of unknown levels for neutral type variables. We write `level(A)` as shorthand for `checkTypeLevel(Γ, A).level`. ``` checkTypeLevel(Γ, Unit) = { Unit, 0 } checkTypeLevel(Γ, Empty) = { Empty, 0 } checkTypeLevel(Γ, String) = { String, 0 } checkTypeLevel(Γ, Int) = { Int, 0 } checkTypeLevel(Γ, Float) = { Float, 0 } checkTypeLevel(Γ, Attrs) = { Attrs, 0 } checkTypeLevel(Γ, Path) = { Path, 0 } checkTypeLevel(Γ, Derivation) = { Derivation, 0 } checkTypeLevel(Γ, Function) = { Function, 0 } checkTypeLevel(Γ, Any) = { Any, 0 } checkTypeLevel(Γ, BootSum(A,B))= { BootSum(A',B'), max(level(A), level(B)) } checkTypeLevel(Γ, Pi(n, A, B)) = { Pi(n,A',B'), max(level(A), level(B)) } checkTypeLevel(Γ, Sigma(n,A,B))= { Sigma(n,A',B'), max(level(A), level(B)) } checkTypeLevel(Γ, BootEq(A,a,b)) = { BootEq(A',a',b'), level(A) } checkTypeLevel(Γ, U(i)) = { U(i), i + 1 } -- Fallback: infer type, require VU(i), extract i checkTypeLevel(Γ, T) = { T', i } where Γ ⊢ T ⇒ VU(i) ``` The fallback handles neutral type expressions (variables, applications) by inferring their type and requiring it to be a universe. This correctly propagates levels through type variables: if `B : U(1)`, then `checkTypeLevel` on `B` infers `VU(1)` and returns level 1. ### 8.3 Non-cumulativity The kernel is non-cumulative: a type `A` at level `i` is **not** automatically a type at any level `j > i`. The Sub rule (§7.4) compares the inferred universe against the expected one with the ordinary structural `conv`, which decides `VU(i)` against `VU(j)` by exact level equality — `convLevel(i, j)` modulo the §6.6 semilattice laws, never by `i ≤ j`. To move a type up the hierarchy you apply the explicit `Lift` former (§4.5); there is no implicit subsumption. Conversion therefore stays a decidable equivalence rather than a preorder, and the bidirectional discipline keeps a single CHECK-to-INFER bridge (the `conv` round-trip) with no cumulativity side-channel. ### 8.4 Universe consistency The kernel MUST reject `U(i) : U(i)`. This is guaranteed by the level computation: `level(U(i)) = i + 1`, so `U(i)` lives at level `i + 1`, not `i`. Self-containing universes cannot be constructed. This prevents Girard's paradox (Girard 1972), which requires a type that contains itself. Hurkens (1995) gives the compact MLTT rendering of the inconsistency proof. Universe stratification is the standard fix, and it is why the kernel enforces `level(U(i)) = i + 1`. ### 8.5 Level sort `Level` is a Tarski-style sort of universe levels, with constructors `zero`, `suc`, and `max`. It inhabits the lowest universe: ``` ────────────────────── Γ ⊢ Level type ↝ Level ────────────────────── Γ ⊢ Level ⇒ VU(0) ↝ Level ────────────────────── Γ ⊢ LevelZero ⇐ Level ↝ LevelZero Γ ⊢ k ⇐ Level ↝ k' ────────────────────── Γ ⊢ LevelSuc(k) ⇐ Level ↝ LevelSuc(k') Γ ⊢ a ⇐ Level ↝ a' Γ ⊢ b ⇐ Level ↝ b' ────────────────────── Γ ⊢ LevelMax(a, b) ⇐ Level ↝ LevelMax(a', b') ``` Conversion modulo the semilattice laws (idempotence of `max`, `suc` distribution over `max`, zero absorption) is delegated to `convLevel` (§6.6); structural conversion on Level values without normalisation would be too coarse (e.g. `max(zero, k)` and `k` would not compare equal). The `Level` sort enables predicative universe polymorphism: the description type `Desc^k I` and universe `U(k)` quantify over arbitrary levels via `Π(k:Level). …`. Rank-1 only — `Level` itself has no eliminator, and admitting one would break parametricity over the semilattice quotient (any eliminator would be observably sensitive to the canonical form chosen by `convLevel`). --- ## 9. Fuel Mechanism ### 9.1 Evaluation fuel Every call to `evalF` receives a fuel parameter and decrements it by one before evaluating the term. When fuel reaches 0: ``` evalF(fuel=0, ρ, t) = THROW "normalization budget exceeded" ``` The kernel aborts via `throw`. Layer 0 (TCB) has no access to the effect system by design, so fuel exhaustion and kernel invariant violations both manifest as Nix-level throws caught by `tryEval`. Callers should treat any throw from the evaluator as "term not verified" — the distinction between fuel exhaustion and a kernel bug is in the error message text, not the failure mechanism. ### 9.2 Default budget The default fuel budget is 10,000,000 reduction steps. This is configurable by the caller via `evalF`. No minimum is enforced — callers may pass arbitrarily low fuel, which will cause immediate `throw` on the first eval step. ### 9.3 Fuel accounting Fuel is **per-path**, not a global counter. Each call to `evalF` captures `f = fuel - 1` and passes `f` to all sub-evaluations of that term. When evaluating `App(t, u)`, both `evalF(f, ρ, t)` and `evalF(f, ρ, u)` receive the same `f`. This means fuel bounds the **depth** of any single evaluation path, not the total work across all paths. For a balanced binary tree of N applications, the total work is O(2^depth × fuel), not O(fuel). This is inherent to pure Nix — there is no mutable global counter. The fuel mechanism guarantees termination (every path eventually hits 0) but does not bound total computation time. All fuel consumption flows through `evalF`: - Direct term evaluation (each `evalF` call decrements fuel by 1) - Beta-reduction in `vApp` consumes fuel indirectly via `instantiateF`, which calls `evalF` - Iota-reduction in generated recursive eliminators consumes fuel through `vDescIndF`, `vAllDF`, `vEverywhereDF`, and `vAppF` `BootJ` completes in O(1) on `BootRefl`. `BootSumElim` is non-recursive, but branch selection calls `vAppF` on the selected branch. Structural operations (building values, pattern matching on tags) do not consume fuel. ### 9.4 Fuel threading in generated eliminators Generated natural/list eliminators route through datatype-specific wrappers and the generic `DescInd` evaluator. Deep constructor chains are not handled by hard-coded nat/list cases: `DescCon` evaluation recognizes homogeneous linear recursive payloads from the description profile and flattens them into `builtins.foldl'` loops. Each fold step threads fuel through the accumulator: ``` foldl'(λ{acc, fuel}. λi. if fuel ≤ 0 then THROW "normalization budget exceeded" else { acc = step(fuel, acc, chain[i]); fuel = fuel - 1; }) {acc = base; fuel = fuel} [1..n] ``` This ensures that an N-element chain consumes N units of fuel from the fold, plus whatever fuel each step application consumes internally. Without this threading, each step would get the original fuel budget, giving an effective budget of N × fuel. The worst-case complexity of a threaded fold is O(fuel²): at step *i*, the inner `vAppF` receives `fuel - i` as its own per-path budget. Summing over all steps gives Σ(fuel - i) ≈ fuel²/2. To achieve O(fuel), `vAppF` would need to return remaining fuel — an invasive signature change. The quadratic residual is inherent to per-path fuel semantics and is a strict improvement over the pre-threading O(N × fuel) with unbounded N. ### 9.5 Fuel consumption in constructor chains Generated constructor evaluation flattens chains of n `DescCon` layers when the description's linear profile proves the recursion is structurally homogeneous. The evaluator deducts n fuel units from the budget before rebuilding the semantic value. Generated natural and list constructors are instances of this generic path, so the 5000-deep stress tests are evidence for the description-level trampoline rather than for datatype-specific hard-coding. --- ## 10. Properties the Implementation Must Satisfy ### 10.1 Soundness (non-negotiable) If the kernel accepts `Γ ⊢ t : A`, then `t` is a valid term of type `A` in MLTT with the specified type formers and universe hierarchy. Formally: **If `check(Γ, t, A)` succeeds, then `Γ ⊢ t : A` is derivable in the declarative typing rules of MLTT.** Equivalently: the kernel never accepts an ill-typed term. ### 10.2 Determinism For any input `(Γ, t, A)`, the kernel produces the same result on every invocation. There is no randomness, no system-dependent behavior, no sensitivity to evaluation order (beyond fuel exhaustion, which always rejects). ### 10.3 Termination For any input `(Γ, t, A)`, the kernel terminates. It either: - Accepts (returns the elaborated term) - Rejects with a type error (via effect) - Rejects with fuel exhaustion - Crashes with a kernel bug diagnostic (throw) It never loops. The fuel mechanism guarantees this. ### 10.4 Evaluation roundtrip For any well-typed term `t` and environment `ρ` consistent with the context: ``` quote(d, eval(ρ, quote(d, eval(ρ, t)))) = quote(d, eval(ρ, t)) ``` Evaluation followed by quotation is idempotent. The result is a normal form. ### 10.5 Conversion reflexivity For any value `v`: ``` conv(d, v, v) = true ``` ### 10.6 Conversion symmetry For any values `v₁, v₂`: ``` conv(d, v₁, v₂) = conv(d, v₂, v₁) ``` ### 10.7 Conversion transitivity For any values `v₁, v₂, v₃`: ``` conv(d, v₁, v₂) ∧ conv(d, v₂, v₃) ⟹ conv(d, v₁, v₃) ``` ### 10.8 Type preservation under evaluation If `Γ ⊢ t : A` and `eval(Γ.env, t) = v`, then `v` represents a value of type `A`. This is not directly testable (values don't carry types) but is ensured by the correctness of the evaluation rules. ### 10.9 Strong normalization (for well-typed terms) For any well-typed term `t`, `eval` terminates without exhausting fuel for a sufficiently large fuel budget. The fuel mechanism is a practical safeguard, not a theoretical necessity for well-typed terms. --- ## 11. Derived Test Cases Every rule in this spec generates at least one positive test (the rule applies and succeeds) and one negative test (the rule's premises are violated and the kernel rejects). ### 11.1 Required positive tests (kernel must ACCEPT) ``` -- Public generated identity ⊢ H.refl : H.eq H.nat H.zero H.zero -- Function type ⊢ Lam(x, H.nat, Var(0)) : Pi(x, H.nat, H.nat) -- Application f : Pi(x, H.nat, H.nat) ⊢ App(f, H.zero) : H.nat -- Dependent function ⊢ Lam(A, U(0), Lam(x, Var(0), Var(0))) : Pi(A, U(0), Pi(x, A, A)) -- Sigma pair ⊢ Pair(H.zero, Tt, Sigma(x, H.nat, Unit)) : Sigma(x, H.nat, Unit) -- Generated natural induction: 0 + 0 = 0 ⊢ H.refl : H.eq H.nat (H.ind ... H.zero) H.zero -- List ⊢ H.cons H.nat H.zero (H.nil H.nat) : H.listOf H.nat -- Sum injection ⊢ H.inl H.nat Unit H.zero : H.sum H.nat Unit -- Universe hierarchy ⊢ U(0) : U(1) ⊢ U(1) : U(2) ⊢ H.nat : U(0) ⊢ Pi(x, H.nat, H.nat) : U(0) -- Let binding ⊢ Let(x, H.nat, H.zero, Var(0)) : H.nat -- StrEq: type inference returns the derived H.bool -- (= μ ⊤ (plus (retI tt) (retI tt)) tt; see §4.11) ⊢ StrEq(StringLit("a"), StringLit("b")) : H.bool -- StrEq reduction: equal strings reduce to the derived true_ value; -- unequal strings reduce to false_. Both witnessed via H.refl over the -- derived-bool form. Expressing this rule at the Tm level requires -- the plus/μ machinery; see the examples/verified-functions.nix -- fixture `recordStrEqMatch` for an executable test. ``` ### 11.2 Required negative tests (kernel must REJECT) ``` -- Type mismatch ⊢ H.zero : Unit REJECT -- Universe violation ⊢ U(0) : U(0) REJECT -- Non-cumulativity: a U(0) type is not accepted at U(1) ⊢ H.nat : U(1) REJECT (no subsumption; use Lift, §8.3) -- H.refl on unequal terms ⊢ H.refl : H.eq H.nat H.zero (H.succ H.zero) REJECT -- Application of non-function ⊢ App(H.zero, H.zero) REJECT -- Projection of non-pair ⊢ Fst(H.zero) REJECT -- Wrong eliminator scrutinee ⊢ H.ind ... Tt REJECT (Tt : Unit, not H.nat) -- Unbound variable ⊢ Var(0) (in empty context) REJECT -- StrEq on non-string ⊢ StrEq(H.zero, StringLit("foo")) REJECT (lhs is H.nat, expected String) -- Ill-typed pair under expected Sigma ⊢ Pair(H.zero, H.zero) ⇐ Sigma(x, H.nat, Unit) REJECT ``` ### 11.3 Required stress tests ``` -- Large H.nat: H.succ^5000(H.zero) : H.nat ACCEPT (trampoline) -- Large H.listOf: H.cons^5000 : H.listOf H.nat ACCEPT (trampoline) -- H.ind on H.succ^5000(H.zero) ACCEPT (trampoline) -- H.listElim on H.cons^5000 ACCEPT (trampoline) -- Succ elaboration: elab-succ-5000 ACCEPT (trampoline) -- Cons elaboration: elab-cons-5000 ACCEPT (trampoline) -- Deeply nested Pi: Pi(x₁, ..., Pi(xₙ, H.nat, H.nat)...) for n=500 ACCEPT -- Fuel exhaustion: artificially low fuel on complex term REJECT (fuel) -- Fuel threading: generated natural fold decrements fuel per step ACCEPT -- Fuel threading: generated list fold decrements fuel per step ACCEPT ``` ### 11.4 Required roundtrip tests For each value form, verify: ``` quote(d, eval(ρ, t)) = normal_form(t) ``` where `normal_form(t)` is the expected normal form. --- ## 12. Notation Index | Symbol | Meaning | |--------|---------| | Γ | Typing context | | ρ | Value environment | | d | Binding depth (for levels ↔ indices) | | ⊢ | Typing judgment | | ⇐ | Checking mode | | ⇒ | Synthesis mode | | ↝ | Elaborates to | | ≡ | Definitional equality | | Π | Dependent function type | | Σ | Dependent pair type | | ℕ | Natural numbers | | 𝔹 | Booleans (derived: `μ ⊤ (plus (retI tt) (retI tt)) tt`) | | ⊤ | Unit type | | ⊥ | Empty type | | U(i) | Universe at level i | | Id_A(a,b) | Identity type | | TCB | Trusted computing base | | WHNF | Weak head normal form | | NbE | Normalization by evaluation | | THROW | Kernel invariant violation (crash) | | REJECT | Term rejected (via effect or fuel) | --- ## 13. Known Limitations The following are documented implementation choices or limitations, not bugs. They are recorded here so auditors do not rediscover them. ### 13.1 Trusted annotation sidecars are semantically erased `Ann` terms may carry implementation sidecars such as `trusted`, `_descRef`, `_label`, and `_conLabel`. Evaluation may propagate those sidecars to values for performance, source maps, or generated datatype metadata, but conversion ignores them. They must never affect definitional equality. ### 13.2 Lambda domain annotations discarded in checking mode When checking `Lam(n, A, t)` against `VPi(n, dom, cl)`, the lambda's domain annotation `A` is discarded and replaced by `dom` from the Pi type. This is standard bidirectional type checking (Dunfield & Krishnaswami 2021, §4): in checking mode, the expected type provides the domain, not the term. The elaborated output uses `quote(d, dom)`, making the original annotation unrecoverable. ### 13.3 Term constructors do not validate argument types Term constructors (`mkVar`, `mkApp`, etc.) accept arbitrary Nix values without type validation. `mkVar "hello"` produces `{ tag = "var"; idx = "hello"; }`, which crashes at eval time. The trust boundary is the HOAS layer (`src/tc/hoas/`), which is the public API — direct term construction is internal to the kernel. ### 13.4 `tryEval` only catches `throw` and `assert false` `builtins.tryEval` in the elaborator's `isConstantFamily` sentinel detection only catches explicit `throw` and `assert false`. Nix coercion errors (e.g., "cannot convert a function to JSON"), missing attribute access, and type comparison errors are uncatchable. The elaborator uses `builtins.typeOf` in error paths to avoid triggering coercion errors. ### 13.5 HOAS sentinel comparison The `isConstantFamily` sentinel test in the elaborator applies two distinct sentinel values and compares the results to detect whether a binding body is dependent. Both Pi and Sigma paths compare **elaborated kernel terms** (`H.elab r1.value == H.elab r2.value`) rather than raw HOAS trees. This avoids false negatives from Nix's function identity comparison (`==` on lambdas). However, if `H.elab` itself produces structurally different terms for semantically equivalent types (e.g., through different elaboration paths), false negatives remain possible. This is a safe failure mode — the kernel still type-checks correctly, but elaboration may require explicit `_kernel` annotations unnecessarily. ### 13.6 StrEq neutral canonicalization When one argument to `vStrEq` is neutral and the other is a literal, the neutral's spine is extended with `EStrEq(literal)`. When both arguments are neutral, the **left** neutral's spine is extended with `EStrEq(right)`. This means `StrEq(x, y)` and `StrEq(y, x)` (where both are neutral) produce different normal forms: `VNe(x, [EStrEq(y)])` vs `VNe(y, [EStrEq(x)])`. Therefore `conv` will report them as **not** definitionally equal, even though `StrEq` is semantically symmetric. This is a safe conservatism: the kernel may reject some provable equalities but never accepts a false one. ### 13.7 Extract uses type value threading (not sentinels) The `extract` function threads kernel type values (`tyVal`) through recursive extraction, rather than using sentinel-based non-dependence tests. For Pi extraction, the codomain type is computed per-invocation via `instantiate(tyVal.closure, kernelArg)`, supporting both dependent and non-dependent function extraction. For Sigma extraction (records), the second component's type is computed via `instantiate(tyVal.closure, val.fst)`. A `reifyType : Val → HoasTree` fallback converts kernel type values back to HOAS when the HOAS body cannot be applied (e.g., when the body accesses record fields from a neutral). `reifyType` loses sugar (VSigma → `H.sigma`, not `H.record`) so the HOAS body is preferred when available. ### 13.8 Spine comparison complexity `convSp` uses `builtins.elemAt` in a fold to compare neutral spines. In Nix, `builtins.elemAt` on lists is O(1) (Nix lists are internally vectors/arrays), so the actual complexity is O(n), not O(n²). This was incorrectly flagged in an earlier audit. --- ## References 1. Coquand, T. et al. (2009). *A simple type-theoretic language: Mini-TT.* 2. Dunfield, J. & Krishnaswami, N. (2021). *Bidirectional Typing.* ACM Computing Surveys. 3. Kovács, A. (2022). *Generalized Universe Hierarchies.* CSL 2022. 4. Abel, A. & Chapman, J. (2014). *Normalization by Evaluation in the Delay Monad.* 5. Girard, J.-Y. (1972). *Interprétation fonctionnelle et élimination des coupures de l'arithmétique d'ordre supérieur.* Thèse d'État, Université Paris 7. 6. Hurkens, A. J. C. (1995). *A Simplification of Girard's Paradox.* TLCA 1995. 7. de Bruijn, N. (1972). *Lambda Calculus Notation with Nameless Dummies.* 8. Martin-Löf, P. (1984). *Intuitionistic Type Theory.* Bibliopolis. 9. Felicissimo, T. (2023). *Generic Bidirectional Typing for Dependent Type Theories.* ## Examples ### Overview ### Proofs #### Proof Basics The first proof examples stay close to computation. You write a HOAS proposition, give `refl` as evidence, and let the kernel normalize both sides. When the normal forms match, the checker accepts the proof. Later sections use the same checker with dependent pairs and eliminators. The full source lives at `examples/proof-basics.nix`. ## Computational equality Addition, boolean negation, list length, and append are defined with eliminators. The equality proof is still `refl`; the work happens in normalization. ```nix add = m: n: ind 0 (lam "_" nat (_: nat)) n (lam "k" nat (_: lam "ih" nat (ih: succ ih))) m; addThreeFive = (checkHoas (eq nat (add (natLit 3) (natLit 5)) (natLit 8)) refl) .tag == "desc-con"; doubleNegTrue = (checkHoas (eq bool (not_ (not_ true_)) true_) refl).tag == "desc-con"; ``` ## Dependent witnesses A sigma value packages a witness with proof that the witness has the requested property. These examples use concrete witnesses whose proof component reduces to reflexivity. ```nix witnessAddResult = let ty = sigma "x" nat (x: eq nat (add (natLit 3) (natLit 5)) x); tm = pair (natLit 8) refl; in (checkHoas ty tm).tag == "pair"; ``` ## Eliminators as programs Natural, boolean, list, and sum eliminators let the proof examples define small programs inside HOAS. Each checked assertion ties the computed result back to an equality. ```nix listSum = let sumList = xs: listElim 0 nat (lam "_" (listOf nat) (_: nat)) zero (lam "h" nat (h: lam "t" (listOf nat) (_: lam "ih" nat (ih: add h ih)))) xs; in (checkHoas (eq nat (sumList list123) (natLit 6)) refl).tag == "desc-con"; ``` ## Polymorphism and impossibility The final examples leave concrete computation and check reusable functions: a universe-polymorphic identity and the eliminator from `Void`. ```nix polyId = let ty = forall "A" (u 0) (a: forall "x" a (_: a)); tm = lam "A" (u 0) (a: lam "x" a (x: x)); in (checkHoas ty tm).tag == "lam"; exFalso = let ty = forall "A" (u 0) (a: forall "x" void (_: a)); tm = lam "A" (u 0) (a: lam "x" void (x: absurd a x)); in (checkHoas ty tm).tag == "lam"; ``` #### Equality Proofs The J eliminator is the primitive way to reason from equality. These examples build the familiar combinators by checking generic theorem terms, then apply the same shapes to concrete values that reduce by computation. ## Congruence Congruence says that equal inputs stay equal after applying the same function. The generic term is checked for arbitrary `A`, `B`, `f`, `x`, and `y`; the concrete term exercises the same pattern after normalizing arithmetic. ```nix congType = let ty = forall "A" (u 0) (a: forall "B" (u 0) (b: forall "f" (forall "_" a (_: b)) (f: forall "x" a (x: forall "y" a (y: forall "_" (eq a x y) (_: eq b (app f x) (app f y))))))); tm = lam "A" (u 0) (a: lam "B" (u 0) (b: lam "f" (forall "_" a (_: b)) (f: lam "x" a (x: lam "y" a (y: lam "p" (eq a x y) (p: j a x (lam "y'" a (y': lam "_" (eq a x y') (_: eq b (app f x) (app f y')))) refl y p)))))); in (checkHoas ty tm).tag == "lam"; ``` ## Symmetry and transitivity Symmetry flips an equality. Transitivity composes two equalities by eliminating over the second proof and carrying the first proof as the base case. ```nix symType = let ty = forall "A" (u 0) (a: forall "x" a (x: forall "y" a (y: forall "_" (eq a x y) (_: eq a y x)))); tm = lam "A" (u 0) (a: lam "x" a (x: lam "y" a (y: lam "p" (eq a x y) (p: j a x (lam "y'" a (y': lam "_" (eq a x y') (_: eq a y' x))) refl y p)))); in (checkHoas ty tm).tag == "lam"; ``` ## Transport Transport moves evidence through an equality in a dependent family. The concrete example uses a boolean-indexed family that chooses between `Nat` and `Bool`. ```nix transportConcrete = let motiveP = b: boolElim 1 (lam "_" bool (_: u 0)) nat bool b; proofTm = j bool true_ (lam "y" bool (y: lam "_" (eq bool true_ y) (_: motiveP y))) zero true_ refl; in (checkHoas nat proofTm).tag == "app"; ``` ## Combining proof steps The final example feeds a congruence proof into symmetry. It is a small proof pipeline, and the useful invariant is that both intermediate proof terms are still checked by the same kernel. ```nix combinedProof = let add21 = add (natLit 2) (succ zero); three = natLit 3; sadd21 = succ add21; sthree = succ three; congStep = j nat add21 (lam "y" nat (y: lam "_" (eq nat add21 y) (_: eq nat sadd21 (succ y)))) refl three refl; proofTy = eq nat sthree sadd21; proofTm = j nat sadd21 (lam "y" nat (y: lam "_" (eq nat sadd21 y) (_: eq nat y sadd21))) refl sthree congStep; in (checkHoas proofTy proofTm).tag == "app"; ``` #### Verified Functions `fx.types.verified.verify` checks a HOAS implementation against a type, evaluates it, and extracts a Nix value. The result can be called like an ordinary function, but it first passed through the kernel. ## First functions The smallest verified programs are ordinary unary functions. The type says what the extracted Nix function accepts and returns; the implementation is written with verified HOAS constructors. ```nix succFn = v.verify (H.forall "x" H.nat (_: H.nat)) (v.fn "x" H.nat (x: H.succ x)); succApply5 = succFn 5 == 6; notFn = v.verify (H.forall "b" H.bool (_: H.bool)) (v.fn "b" H.bool (b: v.if_ H.bool b { then_ = v.false_; else_ = v.true_; })); ``` ## Recursion over naturals `v.match` builds the eliminator for naturals. Addition uses the first argument as the scrutinee and returns a function awaiting the second argument. ```nix addFn = v.verify (H.forall "m" H.nat (_: H.forall "n" H.nat (_: H.nat))) (v.fn "m" H.nat (m: v.fn "n" H.nat (n: v.match H.nat m { zero = n; succ = _k: ih: H.succ ih; }))); add2and3 = addFn 2 3 == 5; ``` ## Lists and pipelines List combinators remain checked HOAS terms until extraction. The composed example filters zeros out of a list and folds the remaining values into a sum. ```nix mapSuccResult = v.verify (H.listOf H.nat) (v.map H.nat H.nat (v.fn "x" H.nat (x: H.succ x)) (H.cons (v.nat 0) (H.cons (v.nat 1) (H.cons (v.nat 2) H.nil)))); composedResult = let input = H.cons (v.nat 0) (H.cons (v.nat 3) (H.cons (v.nat 0) (H.cons (v.nat 2) (H.cons (v.nat 1) H.nil)))); nonZero = v.fn "n" H.nat (n: v.match H.bool n { zero = v.false_; succ = _k: _ih: v.true_; }); in v.verify H.nat (v.fold H.nat H.nat (v.nat 0) addCombine (v.filter H.nat nonZero input)); ``` ## Sums, lets, pairs, and records Verified extraction also covers the richer HOAS constructors used by nix-effects types: coproduct elimination, local bindings, sigma pairs, and record field projection. ```nix sumLeftResult = v.verify H.nat (v.matchSum H.nat H.bool H.nat (v.inl H.nat H.bool (v.nat 5)) { left = x: H.succ x; right = b: v.if_ H.nat b { then_ = v.nat 1; else_ = v.nat 0; }; }); recordGetX = v.verify (H.forall "r" (H.record [{ name = "x"; type = H.nat; } { name = "y"; type = H.bool; }]) (_: H.nat)) (v.fn "r" (H.record [{ name = "x"; type = H.nat; } { name = "y"; type = H.bool; }]) (r: v.field (H.record [{ name = "x"; type = H.nat; } { name = "y"; type = H.bool; }]) "x" r)); ``` ## Strings and records The final examples use kernel-verified string predicates directly and then combine them with record projection. ```nix strEqFn = v.verify (H.forall "a" H.string (_: H.forall "b" H.string (_: H.bool))) (v.fn "a" H.string (a: v.fn "b" H.string (b: v.strEq a b))); recordStrEqFn = let recTy = H.record [ { name = "name"; type = H.string; } { name = "target"; type = H.string; } ]; in v.verify (H.forall "r" recTy (_: H.bool)) (v.fn "r" recTy (r: v.strEq (v.field recTy "name" r) (v.field recTy "target" r))); ``` ### Effects and Validation #### Handler-Swap Validation The computation is built once from a typed record validator. Only the handler changes. That makes the effect boundary visible: collecting accumulates every validation error, logging records each check, and strict aborts at the first failure. ## Define a typed boundary The type describes a network configuration with positive numeric fields. `badConfig` intentionally violates multiple checks. ```nix Pos = refined "Pos" Int (x: x > 0); Network = Record { hostName = String; port = Pos; interfaces = ListOf (Record { name = String; mtu = Pos; }); }; badConfig = { hostName = "kleisli.io"; port = (-1); interfaces = [ { name = "eth0"; mtu = (-50); } { name = 42; mtu = 1500; } { name = "eth2"; mtu = "big"; } ]; }; ``` ## Swap the handler `Network.validate badConfig` returns an effectful computation. The same computation can be interpreted by collecting, logging, or strict handlers without rebuilding the validator. ```nix comp = Network.validate badConfig; runWith = handlers: state: fx.handle { inherit handlers state; } comp; collecting = let r = runWith fx.effects.typecheck.collecting [ ]; line = e: " ${renderPath e} :: expected ${e.typeName}, got ${e.actual}"; in "${toString (builtins.length r.state)} error(s):\n" + builtins.concatStringsSep "\n" (map line r.state); logging = let r = runWith fx.effects.typecheck.logging [ ]; line = e: " ${if e.passed then "pass" else "fail"} ${renderPath e} : ${e.typeName}"; in builtins.concatStringsSep "\n" (map line r.state); ``` ### Surface Languages #### Surface STLC The STLC examples build a source language incrementally over the HOAS checker. Each child page introduces one extension, with the full source available under `examples/stlc/`. ## Layered surfaces The core surface defines functions and application. Later examples import that vocabulary and extend it with products, sums, recursive lists, refinements, and diagnostics. ```nix core = import ./core.nix { inherit fx lib; }; sumsProducts = import ./sums-products.nix { inherit fx lib core; }; recursiveLists = import ./recursive-lists.nix { inherit fx lib core; }; refinementsDiagnostics = import ./refinements-diagnostics.nix { inherit fx lib core; }; ``` ## More walkthroughs - [STLC Core Surface](/nix-effects/surface-examples/stlc/core) - [STLC Sums and Products](/nix-effects/surface-examples/stlc/sumsProducts) - [STLC Recursive Lists](/nix-effects/surface-examples/stlc/recursiveLists) - [STLC Refinements and Diagnostics](/nix-effects/surface-examples/stlc/refinementsDiagnostics) #### STLC Core Surface This example builds a small source language over the existing HOAS checker. The surface adds syntax and source positions; the kernel still owns typing and normalization. ## Define the surface `defineSurface` maps each source constructor to a HOAS expression. The core STLC fragment has Pi types, lambdas, annotations, and applications. ```nix surface = S.defineSurface { name = "STLC"; description = "Small typed lambda-calculus surface example."; constructors = { lam = { tag = "stlc.lam"; handler = { depth, h, lower, hoas, ... }: lower depth (hoas.lam h.name h.domain h.body); }; app = { tag = "stlc.app"; handler = { depth, h, lower, hoas, ... }: lower depth (hoas.app h.fn h.arg); }; }; }; ``` ## Smart constructors Users construct source terms with small Nix functions. The values are surface nodes until `check` elaborates them into HOAS. ```nix arrow = domain: codomain: pi "_" domain (_: codomain); lam = name: domain: body: surface.mk "lam" { inherit name domain body; }; ann = term: type: surface.mk "ann" { inherit term type; }; app = fn: arg: surface.mk "app" { inherit fn arg; }; idNatTy = arrow H.nat H.nat; idNat = lam "x" H.nat (x: var x); idNatAnn = ann idNat idNatTy; ``` ## Expected-type holes `holeLam` omits a lambda domain. The handler reads the expected type, solves a surface implicit metavariable with the Pi domain, and then elaborates an ordinary typed lambda. ```nix holeLambdaFromExpected = { context, name, body, hoas ? H }: let implicit = context.implicitMeta { type = { ctx = C.emptyCtx; ty = V.vU V.vLevelZero; }; label = "stlc.lambda-domain"; }; expectedType = context.expectedType or null; domain = if expectedType == null then null else piDomain expectedType; in if expectedType == null then S.unsolvedImplicitError { metas = [{ id = implicit.id; }]; } else if domain == null then expectedFunctionError context else { term = hoas.lam name domain body; solvedState = M.solveMeta implicit.id (E.eval [ ] (hoas.elab domain)) implicit.state; }; ``` ## Implicit insertion Plicity-style implicit binders are carried as sidecars on Pi and lambda nodes. During application, the elaborator inserts fresh metas for implicit arguments and solves them from explicit arguments or the expected residual type. ```nix polyIdTy = implicitPi "A" universe0 (A: arrow A A); polyId = implicitLam "A" universe0 (A: lam "x" A (x: var x)); polyIdAnn = ann polyId polyIdTy; stlcPolyIdAppliedSolvesImplicit = let position = { path = [ "poly" "id" "zero" ]; }; r = check (app polyIdAnn H.zero) H.nat position; in !(r ? error) && r.tag == "app"; ``` #### STLC Sums and Products This extension adds ordinary product and coproduct forms. The surface layer still only translates syntax: products elaborate to Sigma, sums elaborate to the generated sum datatype, and case analysis elaborates to `sumElim`. ## Products A product type is a non-dependent Sigma. Pair introduction and projections reuse the existing HOAS pair, fst, and snd terms. ```nix sigma = name: fst: snd: surface.mk "sigma" { inherit name fst snd; }; prod = fst: snd: sigma "_" fst (_: snd); pair = fst: snd: surface.mk "pair" { inherit fst snd; }; fst_ = pair: surface.mk "fst" { inherit pair; }; snd_ = pair: surface.mk "snd" { inherit pair; }; natBoolProduct = prod H.nat H.bool; natBoolPair = pair H.zero H.true_; natBoolPairAnn = Core.ann natBoolPair natBoolProduct; ``` ## Sums Sum injections keep both sides of the type explicit so the generated datatype constructors are inferable. Case analysis uses a constant motive for the STLC fragment. ```nix sum = left: right: surface.mk "sum" { inherit left right; }; inl = left: right: term: surface.mk "inl" { inherit left right term; }; inr = left: right: term: surface.mk "inr" { inherit left right term; }; case_ = left: right: result: scrut: onLeft: onRight: surface.mk "case" { inherit left right result scrut onLeft onRight; }; stlcSumCaseChecks = let term = case_ H.nat H.bool H.nat leftZeroAnn (n: n) (_: H.zero); r = check term H.nat { path = [ "sum" "case" ]; }; in !(r ? error); ``` #### STLC Recursive Lists Lists are already available in the HOAS layer as a generated recursive datatype. This surface gives them ordinary STLC syntax and shows how omitted element types can be solved from an expected `List A`. ## List constructors `List A`, `nil A`, and `cons A h t` translate to the generated HOAS list carrier and constructors. ```nix list = elem: surface.mk "list" { inherit elem; }; nil = elem: surface.mk "nil" { inherit elem; }; cons = elem: head: tail: surface.mk "cons" { inherit elem head tail; }; explicitNilNat = nil H.nat; oneTwoList = cons H.nat one (cons H.nat (H.natLit 2) explicitNilNat); ``` ## Folding lists The fold form elaborates to `listElim` with a constant result type. The checked example computes the sum of `[1, 1, 1]` and proves the result is `3`. ```nix listFold = elem: result: onNil: onCons: scrut: surface.mk "listFold" { inherit elem result onNil onCons scrut; }; sumList = xs: listFold H.nat H.nat H.zero (head: _tail: ih: add head ih) xs; stlcListFoldSumChecks = (H.checkHoas (H.eq H.nat (sumList ones) three) H.refl).tag == "desc-con"; ``` ## Element-type holes `implicitNil` and `implicitCons` allocate a surface metavariable for the missing element type. Checking against `List Nat` solves that metavariable as `Nat`. ```nix implicitElementFromExpected = { context, label, hoas ? H }: let implicit = context.implicitMeta { type = { ctx = C.emptyCtx; ty = V.vU V.vLevelZero; }; inherit label; }; expectedType = context.expectedType or null; elem = if expectedType == null then null else listElement expectedType; in if expectedType == null then S.unsolvedImplicitError { metas = [{ id = implicit.id; }]; } else if elem == null then expectedListError context else { inherit elem implicit; solvedState = M.solveMeta implicit.id (E.eval [ ] (hoas.elab elem)) implicit.state; }; ``` #### STLC Refinements and Diagnostics The refinement surface gives a source-language spelling to the HOAS refinement carrier and preserves diagnostic intent. A missing annotation, an ordinary type mismatch, and a failed refinement proof are distinct failures. ## Refinement values A refinement type is represented as a dependent pair whose second component is a positive decision. The surface `refine` form packages the witness and proof into that carrier. ```nix atLeastOneNat = refinement "AtLeastOne" H.nat (n: H.le one n); oneLeOne = H.leSS H.leZ; oneAtLeastOne = refine atLeastOneNat one oneLeOne; stlcRefinementValueChecks = let r = check oneAtLeastOne atLeastOneNat { path = [ "refinement" "value" ]; }; in !(r ? error) && r.tag == "pair"; ``` ## Refinement failure If the witness and proof do not match, the surface reports a refinement-specific diagnostic instead of exposing the raw Sigma mismatch as the main error. ```nix zeroWithOneProof = refine atLeastOneNat H.zero oneLeOne; stlcBadRefinementHasSurfaceDiagnostic = let position = { path = [ "bad" "refinement" ]; }; r = check zeroWithOneProof atLeastOneNat position; in (r ? error) && (r.kind or null) == "stlc.refinement-failure" && (r.position or null) == position; ``` ## Diagnostic classes The examples keep three failure classes separate: unsolved implicit information, ordinary checker errors, and refinement failures. That gives editors and command-line tools enough structure to suggest the right repair. ```nix stlcDiagnosticUnsolvedImplicitIsDistinct = let position = { path = [ "diagnostic" "implicit" ]; }; r = elaborate (Core.holeLam "x" (x: Core.var x)) position; in (r ? error) && (r.kind or null) == "surface.unsolved-implicit" && (r.position or null) == position; stlcDiagnosticRefinementFailureIsDistinct = let position = { path = [ "diagnostic" "refinement" ]; }; r = check zeroWithOneProof atLeastOneNat position; in (r ? error) && (r.kind or null) == "stlc.refinement-failure"; ``` ### Applications #### Category Theory This example is a larger proof library, organized as a guided tour. Each file builds on the previous one: proof combinators, arithmetic lemmas, algebraic structures, functors, and a Yoneda-style round trip. The public `api` exposes extracted Nix functions. The `hoas` attrset keeps the typed HOAS terms available for users who want to inspect or extend the proofs. ## Arithmetic proof layer The first layer derives equality combinators from `J`, defines addition by Nat elimination, and proves the standard addition laws. Computational facts are extracted as ordinary Nix functions. ```nix cat = import ./examples/category-theory { inherit fx; }; cat.api.add 3 5 == 8 cat.tests.addComm ``` ## Algebra and functors The algebra files package the arithmetic proofs as a monoid and a one-object category. The functor file reuses the same doubling map as both a monoid homomorphism and an endofunctor. ```nix cat.tests.natAddMonoid cat.tests.natCategory cat.tests.doubleFunctor ``` ## Yoneda round trips The final file states the evaluate/lift pair for a small types-as-groupoids presentation and checks both round-trip laws. ```nix cat.tests.yonedaEval cat.tests.yonedaLift cat.tests.evalLift cat.tests.liftEval ``` #### Expression Interpreter This example implements a strict expression language with arithmetic, booleans, let bindings, lambdas, application, and recursive bindings. The evaluator emits effects for environment lookup, local scope, and failure; handlers decide how those requests run. The same module also exports scalable expression generators used by the benchmark suite. ## Expression syntax Expressions are plain tagged Nix attrsets. Constructors keep the evaluator independent from any parser or source format. ```nix num = n: { _tag = "Expr"; _variant = "Num"; inherit n; }; add = l: r: { _tag = "Expr"; _variant = "Add"; inherit l r; }; lam = param: body: { _tag = "Expr"; _variant = "Lam"; inherit param body; }; app = fn: arg: { _tag = "Expr"; _variant = "App"; inherit fn arg; }; ``` ## Handler boundary Variable lookup, scoped execution, and failure are effects. Recursive bindings work by building a closure whose environment refers back to itself, while the handler owns the actual environment map. ```nix lookup = name: send "lookup" name; getEnv = send "getEnv" null; fail = msg: send "fail" msg; run = expr: let result = handle { handlers = mkHandler { }; state = { env = { }; }; } (eval expr); in if result ? error then throw "eval error: ${result.error}" else result.value; ``` ## Benchmark generators `exprs.nix` generates large recursive and nested expressions. The benchmark suite imports these generators from `examples/interp`. ```nix interp.exprs.benchmarks.fib10 interp.exprs.benchmarks.lets500 interp.exprs.benchmarks.sum1000 ``` #### Build Simulator This example models a small build planner. Nodes depend on other nodes, builders consume dependency outputs, and handlers provide cache, configuration, logging, and failure behavior. The graph generators produce the scalable workloads used by the benchmark suite. ## Graph nodes Nodes are plain Nix records. A builder receives the evaluated dependency results and configuration, then returns either a value or an error marker. ```nix leaf = name: value: { inherit name; deps = [ ]; builder = { deps, config }: value; }; sumBuilder = { deps, config }: builtins.foldl' (acc: v: acc + v) (config.base or 0) (builtins.attrValues deps); ``` ## Effectful evaluation The evaluator requests cache reads, cache writes, configuration, logging, and failure through effects. Swapping the handler changes observability without changing graph traversal. ```nix eval = graph: handle { handlers = handlersWithLogging; state = mkState graph; } (buildNode graph.root); evalQuiet = graph: handle { handlers = handlersQuiet; state = mkState graph; } (buildNode graph.root); ``` ## Benchmark generators `graphs.nix` generates linear, wide, diamond, tree, mixed, and failing graphs. The benchmark suite imports them from `examples/build-sim`. ```nix buildSim.graphs.benchmarks.linear500 buildSim.graphs.benchmarks.diamond10 buildSim.graphs.benchmarks.mixed_large ``` ## API Reference ### Core API #### Kernel Freer monad kernel: Return/OpCall ADT with FTCQueue bind, `send`, `map`, `seq`, `pipe`, `kleisli`. ## `bind` _bind: sequence two computations; if the first is `Pure`, apply `f` to its value; otherwise snoc `f` onto the FTCQueue for O(1) per-step composition._ ``` bind : Computation a -> (a -> Computation b) -> Computation b ``` Monadic bind: ``` bind comp f = case comp of Pure a -> f a Impure e q -> Impure e (snoc q f) ``` O(1) per bind via FTCQueue snoc (Kiselyov & Ishii 2015, section 3.1). ## `impure` _impure: re-export of `fx.comp.impure` for kernel consumers._ ``` impure : Effect -> FTCQueue -> Computation a ``` Re-export of the computation `Impure` constructor. ## `kleisli` _kleisli: compose two Kleisli arrows `(a -> M b)` and `(b -> M c)` into a single `(a -> M c)`; the arrow product of the freer-monad Kleisli category._ ``` kleisli : (a -> Computation b) -> (b -> Computation c) -> a -> Computation c ``` Kleisli composition. Compose two Kleisli arrows into a single arrow. The associative `>=>` operator in Haskell terminology. ## `map` _mapComp (exported as `map`): apply `f` to the eventual result of a computation (Functor instance); implemented as `bind comp (x: pure (f x))`._ ``` map : (a -> b) -> Computation a -> Computation b ``` Map a function over the result of a computation (Functor instance). Exposed as `map` at the module's top-level. ## `pipe` _pipe: chain a computation through a list of Kleisli arrows, threading each result into the next via bind; the empty arrow list yields `init` unchanged._ ``` pipe : Computation a -> [(a -> Computation b)] -> Computation b ``` Chain a computation through a list of Kleisli arrows. Each arrow's input is the previous arrow's output. ## `pure` _pure: re-export of `fx.comp.pure` for kernel consumers._ ``` pure : a -> Computation a ``` Re-export of the computation `Pure` constructor. ## `queue` _queue: re-export of the FTCQueue namespace for advanced handler composition and adaptation._ ``` queue : Namespace ``` Re-export of `fx.queue` for advanced use. ## `send` _send: lift an effect request named `name` carrying `param` into a computation suspended at that effect; the handler's response resumes via the continuation queue._ ``` send : String -> a -> Computation b ``` Send an effect request. Returns an `Impure` computation whose continuation queue resolves to the handler's response. ## `seq` _seq: thread a list of computations left-to-right via bind, discarding intermediate values and returning only the last; the empty list yields `pure null`._ ``` seq : [Computation a] -> Computation a ``` Sequence a list of computations, threading effects via `bind`. Returns the last result; intermediate values are discarded. #### Comp Computation ADT: introduction and elimination forms for `Pure | Impure`. ## `impure` _impure: build a suspended computation (`Impure` constructor) carrying an effect request and a continuation queue to resume against._ ``` impure : { name, param } -> FTCQueue -> Computation a ``` Create a suspended computation. The OpCall constructor of the freer monad — pairs an effect with the continuation queue. ## `isComp` _isComp: test whether a value is a computation (has `_tag` of `Pure` or `Impure`); returns `false` for any other Nix value._ ``` isComp : a -> Bool ``` Test whether a value is a computation. Returns `true` iff `_tag` is `Pure` or `Impure`. ## `isPure` _isPure: hot-path predicate for `_tag == "Pure"`; cheaper than `match` for branching since it avoids the case-record allocation._ ``` isPure : Computation a -> Bool ``` Test whether a computation is `Pure`. For hot-path conditionals where `match` would allocate a case record. ## `match` _match: eliminate a computation by cases, dispatching to `pure` or `impure` clauses; consumers should use this instead of inspecting `_tag` directly._ ``` match : Computation a -> { pure : a -> b, impure : Effect -> FTCQueue -> b } -> b ``` Eliminate a computation by cases: ``` match comp { pure = a: ...; impure = effect: queue: ...; } ``` Every function that consumes a `Computation` should go through `match` or `isPure` — never inspect `_tag` directly. ## `pure` _pure: lift a value into a pure computation (`Pure` constructor); the trivial computation that returns the value without performing any effect._ ``` pure : a -> Computation a ``` Lift a value into a pure computation. The Return constructor of the freer monad. #### Binds Idiomatic Nix bind helpers: `bindAttrs`, `bindComp`, `bindFn`, plus the `optionalArg` sentinel. ## `bindAttrs` _bindAttrs: sequence an attrset of effectful or send-able values; non-computation values become `send name value`, `optionalArg` probes via `has-handler` first._ ``` bindAttrs : { = Computation a | OptionalArg | Param; __sort? = [String] -> [String] } -> Computation { = a } ``` Like a bind-chain but operates over named attrset of required-effects. ```nix bind.attrs { foo = 99; bar = pure 22; baz = asks (env: env.baz); } ``` Values that are non-effects become send params: `send "foo" 99`. Pass the `optionalArg` sentinel to mark a key as optional: bindAttrs probes via `has-handler` first and omits the key when no handler is installed (so a Nix function's default value can take over). Result has same attr-keys with corresponding effect result. See also: `bind.comp`, `bind.fn` for which this is the foundation. # NOTE: Ordering of chained effects. Since an attrSet has no order, this function chains effects in same order as `builtins.attrNames` (alphabetical). If you need an special order for computations that might be order senstive, specify a `__sort = names => names` function. ## `bindComp` _bindComp: turn an effectful function into an effect chain via `bindAttrs`; required args become required sends, optional args (with Nix defaults) probe via `has-handler`._ ``` bindComp : { = Computation a | Param } -> ({ }: Computation b) -> Computation b ``` Turns a Nix effectful function into an effect chain via bindAttrs. ```nix bindComp { bar = pure 22; } ({ foo, bar }: pure (foo * bar)) ``` The function sees bar as the result of `pure 22` and `foo` as the result of `send "foo" false` -- false comes directly from using `lib.functionArgs f`, the handler can know if "foo" is optional in f. Optional args (those with defaults in the Nix function) are probed via has-handler before sending. If no handler exists, the arg is skipped and the Nix default kicks in. This works by using `bindAttrs` on the intersection of function args and attrs. ## `bindFn` _bindFn: like `bindComp` but for pure Nix functions; lifts the function's result into `pure` while still resolving its arguments through the effect system._ ``` bindFn : { = Computation a | Param } -> ({ }: b) -> Computation b ``` Like bindComp but works on normal Nix functions and turns its result into a pure-effect. ```nix bindFn { bar = pure 22; } ({ foo, bar }: foo * bar) ``` ## `optionalArg` _optionalArg: sentinel for `bindAttrs`/`bindComp` optional effect arguments; absent handlers omit the attr so Nix defaults can apply._ ``` optionalArg : OptionalArg ``` Sentinel value marking an attr as handler-conditional in `bindAttrs`. #### Trampoline Trampolined interpreter using `builtins.genericClosure` for O(1) stack depth. ## `handle` _handle: trampolined handler combinator with a custom `return` clause; interprets the computation through `handlers` from initial `state` then folds the pair via `return`._ ``` handle : { return ? Identity, handlers, state ? null } -> Computation a -> { value, state } ``` Trampolined handler combinator with `return` clause. Follows Kiselyov & Ishii's `handle_relay` pattern but trampolined via `genericClosure` for O(1) stack depth. **Arguments** (attrset): - `return` — `value -> state -> { value, state }`. How to transform the final Pure value. Default: identity. - `handlers` — `{ effectName = { param, state }: { resume | abort, state }; }`. Each must return `{ resume; state; }` or `{ abort; state; }`. - `state` — initial handler state. Default: null. **Handler state and closure-valued fields** — the trampoline `deepSeq`-forces handler state at each step. Derivations and other values that hang under `deepSeq` must be wrapped with `fx.state.thunk.mkThunk` before being stored in handler state, and unwrapped with `fx.state.thunk.forceThunk` after `handle` returns. Closure-valued fields are opaque to `deepSeq` and don't need wrapping, but any attrset field reachable from state that contains a derivation or pointer-cyclic value does. ## `rotate` _rotate: selectively handle known effects and rotate unknown ones outward; matches the Kyo-style handler-rotation law for nested scopes._ ``` rotate : { return ? Identity, handlers, state ? null } -> Computation a -> Computation b ``` Selectively handle known effects and rotate unknown effects outward. If the current effect has a matching handler, the handler is applied. If it does not match, the effect is re-suspended and its continuation is wrapped so handling resumes after that effect is interpreted by an outer handler. This corresponds to the Kyo-style handler rotation law from https://gist.github.com/vic/3a7f52974a28675dbaf40b34bec74787: ``` handle(tag1, suspend(tag2, i, k), f) = suspend(tag2, i, x => handle(tag1, k(x), f))` for `tag1 != tag2 ``` ## `run` _run: drive a computation through the `genericClosure` trampoline with a handler attrset and initial state; returns `{ value, state }` at O(1) stack depth._ ``` run : Computation a -> Handlers -> State -> { value : a, state : State } ``` Run a computation through the `genericClosure` trampoline. **Arguments:** - `comp` — the freer monad computation to interpret - `handlers` — `{ effectName = { param, state }: { resume | abort, state }; ... }` - `initialState` — starting state passed to handlers Handlers must return one of: ``` { resume = value; state = newState; } -- invoke continuation with value { abort = value; state = newState; } -- discard continuation, halt ``` This is the defunctionalized encoding of Plotkin & Pretnar (2009): `resume` ≡ invoke continuation k(v), `abort` ≡ discard k. Stack depth: O(1) — constant regardless of computation length. Time: O(n) where n = number of effects in the computation. **Handler state and closure-valued fields** — the trampoline `deepSeq`-forces handler state at each step. Derivations and other values that hang under `deepSeq` must be wrapped with `fx.state.thunk.mkThunk` before being stored in handler state, and unwrapped with `fx.state.thunk.forceThunk` after `run` returns. Closure-valued fields are opaque to `deepSeq` and don't need wrapping, but any attrset field reachable from state that contains a derivation or pointer-cyclic value does. #### Queue FTCQueue (catenable queue, after Kiselyov & Ishii 2015). O(1) `snoc`/`append`, amortized O(1) `viewl`. ## `append` _append: concatenate two queues in O(1); identity queues short-circuit and the `__rawResume` rotation flag is propagated when present._ ``` append : FTCQueue a b -> FTCQueue b c -> FTCQueue a c ``` Concatenate two queues. O(1). Identity queues short-circuit (return the other side untouched). The `__rawResume` flag is preserved so `fx.bind` chains around `scope.provide` retain deep-handler semantics. ## `leaf` _leaf: build a singleton FTCQueue containing one continuation; the leaf of the catenable-tree representation._ ``` leaf : (a -> Computation b) -> FTCQueue a b ``` Create a singleton queue containing one continuation function. ## `node` _node: join two FTCQueues into a balanced tree node; O(1) concatenation that defers traversal cost to `viewl`._ ``` node : FTCQueue a x -> FTCQueue x b -> FTCQueue a b ``` Join two queues. O(1) — just creates a tree node; the cost is amortised by `viewl` during deconstruction. ## `qApp` _qApp: apply a queue of continuations to a starting value; trampolines pure continuations via `genericClosure` and halts at the first `Impure` result._ ``` qApp : FTCQueue a b -> a -> Computation b ``` Apply a queue of continuations to a value. Processes continuations left-to-right: if a continuation returns `Pure`, feed the value to the next continuation. If it returns `Impure`, append the remaining queue to the effect's own queue and return. ## `singleton` _singleton: alias for `leaf`; build an FTCQueue from a single continuation function with no nesting._ ``` singleton : (a -> Computation b) -> FTCQueue a b ``` Create a queue with a single continuation. O(1). Synonym for `leaf`. ## `snoc` _snoc: append one continuation to the right end of a queue in O(1); preserves the `__rawResume` rotation flag for deep-handler semantics._ ``` snoc : FTCQueue a b -> (b -> Computation c) -> FTCQueue a c ``` Append a continuation to the right of the queue. O(1). Preserves the `__rawResume` flag through extension so deep-handler rotation continuations keep routing effectful resumes back through inner-scope handlers. ## `viewl` _viewl: extract the leftmost continuation from a queue with amortised O(1) cost via `viewlGo` rotation; returns `{ head, tail }`, `tail = null` for singletons._ ``` viewl : FTCQueue a b -> { head : (a -> Computation b), tail : FTCQueue a b | null } ``` Extract the leftmost continuation from the queue. Amortized O(1). Returns `{ head = fn; tail = queue | null; }` — `tail` is `null` when the queue had only one element. #### Adapt Handler context transformation. Contravariant on context, covariant on continuation. ## `adapt` _adapt: lift a child-state handler through a `get`/`set` lens onto parent state; contravariant on context extraction, covariant on result incorporation._ ``` adapt : { get : P -> S, set : P -> S -> P } -> Handler S -> Handler P ``` Transform a handler's state context. Wraps a handler that works with child state `S` so it works with parent state `P`, using a `get`/`set` lens. Propagates both `resume` and `abort`. ```nix counterHandler = { param, state }: { resume = null; state = state + param; }; adapted = adapt { get = s: s.counter; set = s: c: s // { counter = c; }; } counterHandler; # adapted now works with { counter = 0; logs = []; } state ``` ## `adaptHandlers` _adaptHandlers: lift an entire handler set through the same `get`/`set` lens; equivalent to mapping `adapt` over each handler in the attrset._ ``` adaptHandlers : { get : P -> S, set : P -> S -> P } -> Handlers S -> Handlers P ``` Adapt an entire handler set (attrset of handlers) to a different state context. Applies the same `get`/`set` lens to every handler in the set. ```nix stateHandlers = { get = { param, state }: { value = state; inherit state; }; put = { param, state }: { value = null; state = param; }; }; adapted = adaptHandlers { get = s: s.data; set = s: d: s // { data = d; }; } stateHandlers; ``` #### Pipeline Typed pipeline framework with composable stages. Stages are composable transformations executed with reader (immutable environment), error (collecting validation errors), and acc (non-fatal warnings) effects. The run function wires up all handlers and returns { value, errors, warnings, typeErrors }. ```nix let stage1 = pipeline.mkStage { name = "discover"; transform = data: bind (pipeline.asks (env: env.config)) (cfg: pure (data // { config = cfg; })); }; result = pipeline.run { config = "prod"; } [ stage1 ]; in result # => { config = "prod"; } ``` ## `ask` _ask: reader-effect helper returning the full pipeline environment._ ``` ask : Computation Env ``` Convenience re-export of `fx.effects.reader.ask`. ## `asks` _asks: reader-effect helper applying a projection to the pipeline environment._ ``` asks : (Env -> a) -> Computation a ``` Convenience re-export of `fx.effects.reader.asks`. ## `bind` _bind: re-export of `fx.kernel.bind` for pipeline stage implementations._ ``` bind : Computation a -> (a -> Computation b) -> Computation b ``` Re-export of `fx.kernel.bind`. ## `compose` _compose: chain a list of stages into a single computation; each stage's transform receives the previous output and returns the next stage's input wrapped in a computation._ ``` compose : [Stage] -> Data -> Computation Data ``` Chain stages into a single computation. Each stage's transform receives the output of the previous stage and returns a computation producing the next stage's input. Initial data seeds the pipeline. ## `map` _map: re-export of `fx.kernel.map` for pipeline stage implementations._ ``` map : (a -> b) -> Computation a -> Computation b ``` Re-export of `fx.kernel.map`. ## `mkStage` _mkStage: build a named pipeline stage carrying a `transform`, optional `inputType`/`outputType` schemas, and a `description`; stages chain through `compose`._ ``` mkStage : { name, description ? "", transform, inputType ? null, outputType ? null } -> Stage ``` Create a named pipeline stage. `transform : Data -> Computation Data` Takes current pipeline data, uses effects (ask, raise, warn), returns computation producing updated pipeline data. inputType/outputType : optional type schemas for validation at stage boundaries (checked when provided). Validation uses fx.types.validate which sends typeCheck effects. ## `pure` _pure: re-export of `fx.kernel.pure` for pipeline stage implementations._ ``` pure : a -> Computation a ``` Re-export of `fx.kernel.pure`. ## `raise` _raise: collecting-error helper for pipeline stages._ ``` raise : String -> Computation a ``` Convenience re-export of `fx.effects.error.raise`. ## `raiseWith` _raiseWith: collecting-error helper with context for pipeline stages._ ``` raiseWith : Context -> String -> Computation a ``` Convenience re-export of `fx.effects.error.raiseWith`. ## `run` _run: execute a pipeline with reader/error/acc/typecheck handlers wired up; returns `{ value, errors, warnings, typeErrors }` from the final state._ ``` run : Args -> [Stage] -> { value : Data, errors : [Err], warnings : [Warn], typeErrors : [Err] } ``` Execute a pipeline with effect handling. `args : { ... }` Becomes the reader environment -- stages access via ask/asks. stages : [Stage] Ordered list of stages to execute. Returns: value -- final pipeline data from last stage errors -- list of { message, context } from validation failures warnings -- list of non-fatal warning items typeErrors -- list of type validation errors ## `warn` _warn: accumulator helper for non-fatal pipeline warnings._ ``` warn : a -> Computation null ``` Convenience re-export of `fx.effects.acc.emit`. #### State The trampoline deepSeq-forces handler state at each step; derivations and other cyclic attrsets hang. `mkThunk` wraps a value as `{ _tag = "Thunk"; _force = _: value; }` — Nix never recurses into a closure environment, so deepSeq sees only the inert tag and closure. Wrap before storing; unwrap with `forceThunk` after `fx.run`/`fx.handle` returns. ## Sub-namespaces - [`thunk`](/nix-effects/core-api/state/thunk) ## Source - [`src/state/thunk.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/state/thunk.nix) #### Thunk Runtime carrier module. Three exports: mkThunk : a -> Thunk a forceThunk : Thunk a -> a isThunk : Value -> Bool A `Thunk` is `{ _tag = "Thunk"; _force = _: value; }`. The value is captured in the closure's environment, where `builtins.deepSeq` cannot reach it. The companion HOAS combinator `H.thunk inner` provides the structurally-lazy kernel type former. This carrier is intentionally runtime-only: the HOAS `thunk` combinator supplies the typed surface, and handlers use the carrier to keep state transport lazy under trampoline evaluation. ## `forceThunk` _forceThunk: recover the value hidden inside a Thunk carrier; rejects raw values and malformed carriers._ ``` forceThunk : Thunk a -> a ``` Recover the value captured inside a `Thunk` carrier. ## `isThunk` _isThunk: predicate recognizing Thunk carriers by their `_force` closure._ ``` isThunk : Value -> Bool ``` Return true when a value has the Thunk carrier shape. ## `mkThunk` _Thunk: deepSeq-safe carrier for transporting values through trampoline-threaded handler state via a closure that hides the value from `builtins.deepSeq`._ ``` mkThunk : a -> Thunk a ``` A `Thunk` is `{ _tag = "Thunk"; _force = _: value; }`. The value is captured in the closure's environment, where `builtins.deepSeq` cannot reach it. The companion HOAS combinator `H.thunk inner` provides the structurally-lazy kernel type former. This carrier is intentionally runtime-only: the HOAS `thunk` combinator supplies the typed surface, and handlers use the carrier to keep state transport lazy under trampoline evaluation. #### Sugar Opt-in syntax-livability layer for nix-effects. - `fx.sugar.do` / `fx.sugar.letM` — combinator forms - `fx.sugar.operators.__div` — `/` as reverse-apply (bind) - re-exports of `pure bind run handle map seq pipe kleisli` See `book/src/sugar.md` for the opt-in matrix and caveats. ## `bind` _Re-export of fx.kernel.bind. See fx.kernel for details._ ## `do` _do: build a composable Kleisli arrow `(a -> M b)` from a list of steps; plain functions `(a -> b)` are auto-lifted via `pure`, monadic functions `(a -> M b)` pass through. Apply the result to a seed to obtain a `Comp b`._ ``` do : [a -> b | a -> M b] -> (a -> M b) ``` Use when steps form a linear pipeline that should remain point-free and composable. Because `do` returns a Kleisli arrow, two pipelines glue without lambdas: `kleisli (do [f g]) (do [h i]) == do [f g h i]`. Data-last argument order makes `map (do [validate enrich]) xs` natural. Auto-lifting lets pure and effectful steps mix freely — runtime dispatch via `isComp` decides whether to thread through `map` or `bind`. An empty list gives the identity arrow `x: pure x`. Prefer `steps` when discarding intermediate values is intentional (effect sequencing). Prefer `kleisli` for a single binary composition. Use `letM` when bound values are siblings rather than a left-to-right pipeline. ## `handle` _Re-export of fx.trampoline.handle. See fx.trampoline for details._ ## `kleisli` _Re-export of fx.kernel.kleisli. See fx.kernel for details._ ## `letM` _letM: `attrs`-based monadic binding — runs `bindAttrs attrs` to gather a record of results, then passes the record to continuation `k` for the next step._ ``` letM : { name = Comp a } -> ({ name = a } -> Comp b) -> Comp b ``` Use when several independent computations must complete before a single dependent step runs — `letM { a = ca; b = cb; } ({ a, b }: ...)` replaces a nested `bind ca (a: bind cb (b: ...))` chain with a flat record. Field order in the resulting record is unspecified; ordering of side effects across fields is determined by `bindAttrs`, not by the caller's attribute layout. Prefer over `do` when the bound values are siblings rather than a left-to-right pipeline. For a single bind, plain `bind` is clearer. ## `map` _Re-export of fx.kernel.map. See fx.kernel for details._ ## `operators` _operators: sugar operator overloads — currently `__div` aliasing `fx.kernel.bind` so `c / k` reads as `bind c k` in expression context._ Opt-in operator-style monadic composition. The attrset's `__div` field is invoked by Nix's `/` operator when the LHS is an attrset carrying `__div`. Use sparingly: operator overloading is non-idiomatic in Nix and obscures dataflow for readers unfamiliar with the convention. The combinator forms (`bind`, `do`, `kleisli`) remain the canonical entry points. ## `pipe` _Re-export of fx.kernel.pipe. See fx.kernel for details._ ## `pure` _Re-export of fx.kernel.pure. See fx.kernel for details._ ## `run` _Re-export of fx.trampoline.run. See fx.trampoline for details._ ## `seq` _Re-export of fx.kernel.seq. See fx.kernel for details._ ## `steps` _steps: sequence a list of `Comp` steps left-to-right via `bind`, returning a single composed `Comp`; each step receives the previous result, the seed is `pure null`._ ``` steps : [a -> Comp b] -> Comp b ``` Use when only the side effects of each step matter — analogous to Haskell's `sequence_`. The seed value is `pure null`, so the first step's argument is `null`; discard it if the first step is producer-shaped (`_: pure x`). An empty list returns `pure null`. Prefer `do` for composable, point-free Kleisli pipelines that thread values. Prefer `kleisli` when composing two Kleisli arrows without an initial value, and `pipe` when threading a non-monadic seed through effectful transforms. ## `types` _types: refinement-applicable type wrappers — each primitive becomes a `__functor`-bearing record so `Int (n: n > 0)` chains another `refined` layer; `wrap` lifts an arbitrary type into the same callable form._ Sugar layer over `fx.types.refinement.refined`. Each entry carries `__functor self pred = sugared (refined "name?" self pred)`, so calling a type as a function tightens it with a predicate. `__toString self = self.name` makes interpolated names readable. The wrapped primitives (`Int`, `String`, `Bool`, `Float`, `Path`, `Null`, `Unit`, `Any`) come from `fx.types.primitives`; `wrap` lets callers lift any other type into the same chainable shape. ## Source - [`src/sugar/effects.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/sugar/effects.nix) - [`src/sugar/operators.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/sugar/operators.nix) - [`src/sugar/types.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/sugar/types.nix) #### Build `plan` validates a sequence of `BuildStep` records via `fx.pipeline.run` (reader/error/acc effects). `materialize` lowers a validated plan to a `pkgs.runCommand` derivation; shell generation is pure and tested inline. ## `materialize` _materialize: convert a validated `BuildPlan` into a `pkgs.runCommand` derivation; copies sources, scopes per-step env vars, runs steps under `set -euo pipefail`._ ``` materialize : { pkgs, plan, native ? [] } -> Derivation ``` Materialize a validated `BuildPlan` into a derivation. Converts the eval-time plan (validated by `fx.build.plan`) into a `pkgs.runCommand` derivation. Sources are copied into a working directory, per-step environment variables are scoped, and steps execute sequentially under `set -euo pipefail`. The `plan` argument is the `.plan` field from `fx.build.plan`'s output. Shell generation helpers (`mkStepScript`, `mkSourceSetup`, `mkBuildScript`) are pure functions tested inline. ## `plan` _plan: validate build steps against `BuildStep`, filter by `when` predicates against reader context, and collect errors/warnings without throwing._ ``` plan : { name, steps, sources ? {}, context ? {} } -> { plan : BuildPlan, errors : [Err], warnings : [Warn], typeErrors : [Err] } ``` Validate and process build steps into a `BuildPlan`. Runs an eval-time pipeline that validates each step against `BuildStep`, filters steps by `when` predicates using reader context, and collects all errors without throwing. ## Sub-namespaces - [`types`](/nix-effects/core-api/build/types) ## Source - [`src/build/materialize.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/build/materialize.nix) - [`src/build/plan.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/build/plan.nix) - [`src/build/types.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/build/types.nix) #### Types Build types for effects-powered builders. `BuildStep` and `BuildPlan` describe build pipelines at the type level, enabling validation before materialization into derivations. ## `BuildPlan` _BuildPlan: open-record type for a complete build pipeline; requires non-empty `name` and a `[BuildStep]` list, permits `sources` and `context`._ Build plan: a complete build pipeline. Required: `name` (non-empty), `steps` (list of `BuildStep`). Optional (open record): `sources`, `context`. ## `BuildStep` _BuildStep: open-record type for a single shell step in a build pipeline; requires non-empty `name` and `run`, permits `tools`/`env`/`when`/etc._ Build step: a single step in a build pipeline. Required: `name` (non-empty), `run` (shell fragment). Optional (open record): `description`, `tools`, `env`, `inputType`, `outputType`, `when`. #### Experimental Names, shapes, and laws under this namespace may change without deprecation cycles. Sub-modules are promoted out (and renamed) once they stabilise. ## Sub-namespaces - [`desc-interp`](/nix-effects/core-api/experimental/desc-interp) #### Desc-interp `desc.nix` encodes FreeFx as `plus pureSummand impureSummand`; continuations live in an FTCQueue value (Kiselyov & Ishii) rather than meta-level Nix functions. `kernel.nix` mirrors `fx.kernel`'s `pure`/`send`/`bind`; `bind` snocs into the Impure summand's queue without recursing on the μ-tree. `trampoline.nix` drives programs via `builtins.genericClosure` at O(1) host stack depth. ## Handler shortcut layering Each canonical handler op admits a partial-evaluation residual that bypasses kernel `vApp` per Impure step. Soundness splits: - **kernel-side** (`effects/*-shortcut-laws.nix`): `H.refl` lemma `handle_X op s ≡_kernel mkResumeAt … r s` (resp. `mkAbortAt`) certifying the per-op rewrite under ι+β. - **emitter-side** (`extract.nix`): per-primitive Val-conv test `eval (HOAS witness) ≡_Val extract (Primitive …)`. Sugars (`Resume`/`Abort`/`PairRaw`) fold into primitives. - **composed-side** (`compose.nix` + `composed-shortcut.nix` + `composed-shortcut-laws.nix`): `composeHandlers` over UniRet closes the algebra under `+`. Six lemmas chain `composeHandlersInl/InrLemma` with per-effect uniform-shortcut lemmas; `composedHandlerShortcut` mirrors the kernel-composed path by direct Val construction. ## Adding a new effect with a handler shortcut Recipe — apply in order: 1. **Identify canonical RHS shapes.** Reduce `handle_X op s` per constructor via ι+β. Each canonical op should normalise to `mkResumeAt … r s` or `mkAbortAt … v s` (or a bare `H.pair _ _` outside the UniRet shape). 2. **Extend `extract.nix` only if needed.** Existing sugars cover the UniRet and bare-pair shapes; primitives (`DescCon`, `BootInl`, `BootInr`, `Pair`, `Tt`, `BootRefl`) suffice for anything else. Add a sugar only when a non-trivial shape repeats across multiple effects. 3. **Write `effects/x-shortcut-laws.nix`.** One `H.refl` lemma per canonical op asserting `handle_X op s ≡_kernel mkXAt …`. Mirror `effects/state-shortcut-laws.nix`'s `withPrefix`/ `lamPrefix` helper layout. If `uniformOf_X` differs from `handle_X` (error's three strategies), add `effects/x-uniform-shortcut-laws.nix` too. 4. **Wire `handlerShortcut` in `atType`.** Pre-compute metadata Vals (`leftSig`, `rightSig`, `sumDescVal`) once per instantiation; in `handlerShortcut op stateVal`, dispatch on `op._opTag` and return `extract (Resume …)` / `extract (Abort …)` / `extract (PairRaw …)`. Smart constructors tag canonical ops via `_opTag = "-"`. If `uniformOf_X` is needed for composition, expose `uniformOfShort` (or `uniformOfShortAt A` if A varies post-`atType`). 5. **Add parity tests in `trampoline.nix`.** For each canonical op, compare `runX { handler; dispatch; }` vs `runX { handler; dispatch; handlerShortcut; }` via `fx.tc.conv.conv 0` on both `.value` and `.state`. For ops that abort with a Nix throw (strict-style), force `.state.tag` under `builtins.tryEval` and assert both paths trip the throw. Composition with another effect via `composeHandlers` reuses the per-effect `uniformOfShort` plus `composedHandlerShortcut H_short_A H_short_B metaFor`; soundness is anchored kernel-side by `composed-shortcut-laws.nix` without any per-effect-pair work. ## Sub-namespaces - [`compose`](/nix-effects/core-api/experimental/desc-interp/compose) - [`compose-laws`](/nix-effects/core-api/experimental/desc-interp/compose-laws) - [`composed-shortcut`](/nix-effects/core-api/experimental/desc-interp/composed-shortcut) - [`composed-shortcut-laws`](/nix-effects/core-api/experimental/desc-interp/composed-shortcut-laws) - [`desc`](/nix-effects/core-api/experimental/desc-interp/desc) - [`descind-laws`](/nix-effects/core-api/experimental/desc-interp/descind-laws) - [`effects`](/nix-effects/core-api/experimental/desc-interp/effects) - [`extract`](/nix-effects/core-api/experimental/desc-interp/extract) - [`kernel`](/nix-effects/core-api/experimental/desc-interp/kernel) - [`trampoline`](/nix-effects/core-api/experimental/desc-interp/trampoline) ## Source - [`src/experimental/desc-interp/compose-laws.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/experimental/desc-interp/compose-laws.nix) - [`src/experimental/desc-interp/compose.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/experimental/desc-interp/compose.nix) - [`src/experimental/desc-interp/composed-shortcut-laws.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/experimental/desc-interp/composed-shortcut-laws.nix) - [`src/experimental/desc-interp/composed-shortcut.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/experimental/desc-interp/composed-shortcut.nix) - [`src/experimental/desc-interp/desc.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/experimental/desc-interp/desc.nix) - [`src/experimental/desc-interp/descind-laws.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/experimental/desc-interp/descind-laws.nix) - [`src/experimental/desc-interp/extract.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/experimental/desc-interp/extract.nix) - [`src/experimental/desc-interp/kernel.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/experimental/desc-interp/kernel.nix) - [`src/experimental/desc-interp/trampoline.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/experimental/desc-interp/trampoline.nix) #### Compose fx.experimental.desc-interp.compose: universal handler-result type UniRet and its smart constructors. Every kernel-resident handler can be re-shaped into a UniRet-returning form; composeHandlers operates uniformly on that shape. ## `UniRet` _UniRet : `Π (S:U) (Op:U) (Resp:Op→U) (op:Op) (A:U). U` — universal handler-result type. Left summand `Σ _r:(Resp op). S` = resume (response + new state). Right summand `Σ _a:A. S` = abort (final value + new state). State's `handle_StateTy` is conv-equal to `Π S A. Π op:EffState S. Π _s:S. UniRet S (EffState S) (Resp_State S) op A` — see the inline conv test below._ ## `UniRetTy` _Π-type of UniRet: `Π (S:U) (Op:U) (Resp:Op→U) (op:Op) (A:U). U`._ ## `composeHandlers` _composeHandlers S_A S_B Op_A Op_B Resp_A Resp_B A H_A H_B — kernel-resident combinator taking two UniRet-shaped handlers and producing a UniRet-shaped handler over the sum effect. Body dispatches outer `sumElim` on `op`, projects the matching state slot, runs the per-effect handler, then inner-`sumElim`s on the UniRet result to re-package into the composed shape with the threaded product state. The composed-law witness lifts per-effect laws through this shape._ ## `composeHandlersTy` _Π-type of composeHandlers: `Π (S_A S_B Op_A Op_B : U). Π (Resp_A : Op_A → U). Π (Resp_B : Op_B → U). Π (A : U). (Π op:Op_A. Π _s:S_A. UniRet S_A Op_A Resp_A op A) → (Π op:Op_B. Π _s:S_B. UniRet S_B Op_B Resp_B op A) → Π op:(Sum Op_A Op_B). Π _s:(Σ S_A S_B). UniRet (Σ S_A S_B) (Sum Op_A Op_B) (composedResp Op_A Op_B Resp_A Resp_B) op A`._ ## `composedResp` _composedResp Op_A Op_B Resp_A Resp_B op — paired response family for the sum effect. sumElim-driven so `composedResp … (inl opA) ≡ Resp_A opA` and `composedResp … (inr opB) ≡ Resp_B opB` by iota; composeHandlers relies on this conv-equality to retype the mkResume r-argument._ ## `composedRespAt` _composedRespAt Op_A Op_B Resp_A Resp_B — Nix-level helper for the 4-arg composedResp spine; result has type `Π op:(Sum Op_A Op_B). U`._ ## `composedRespTy` _Π-type of composedResp: `Π (Op_A Op_B : U). Π (Resp_A : Op_A → U). Π (Resp_B : Op_B → U). Π op:(Sum Op_A Op_B). U`._ ## `mkAbort` _mkAbort : `Π S Op Resp op A. A → S → UniRet S Op Resp op A` — right-injection smart constructor._ ## `mkAbortAt` _mkAbortAt S Op Resp op A a s — Nix-level helper for the fully-applied mkAbort spine._ ## `mkAbortTy` _Π-type of mkAbort._ ## `mkResume` _mkResume : `Π S Op Resp op A. (Resp op) → S → UniRet S Op Resp op A` — left-injection smart constructor._ ## `mkResumeAt` _mkResumeAt S Op Resp op A r s — Nix-level helper for the fully-applied mkResume spine._ ## `mkResumeTy` _Π-type of mkResume._ ## `uniRetAt` _uniRetAt S Op Resp op A — Nix-level helper for the fully-applied UniRet spine; consumers avoid hand-rolling the 5-arg app each time._ #### Compose-laws fx.experimental.desc-interp.compose-laws: kernel-checked one-shot meta-theorem for composeHandlers. Two clauses (inl/inr); both discharge by H.refl. ## `composeHandlersInlLemma` _Inl clause proof: 12 lams + H.refl. checkHoas succeeds because kernel conv decides composeHandlers' iota universally._ ## `composeHandlersInlLemmaTy` _Inl clause Π-type: composeHandlers on (inl op_A, pair s_A s_B) ≡ sumElim … (H_A op_A s_A) (mkResume_composed …) (mkAbort_composed …) with s_B threaded._ ## `composeHandlersInrLemma` _Inr clause proof: 12 lams + H.refl._ ## `composeHandlersInrLemmaTy` _Inr clause Π-type: mirror of the inl clause with the state-slot roles swapped._ #### Composed-shortcut Pipeline per Impure step on a composed op: read `op._side` / `op._inner`, project the composed state with `extract.Project`, recurse via the matching per-side shortcut, then `extract.OfElim` on the sub-UniRet — Resume → composed Resume; Abort → composed Abort — repacking the sibling state slot via `extract.PairRaw`. Soundness: kernel-side `composedHandlerShortcutLemma` (`composed-shortcut-laws.nix`) chains `composeHandlersInl/InrLemma` with per-effect uniform-shortcut lemmas; emitter-side, the `_self.tests` below conv-check kernel-composed vs shortcut-emitted Vals at concrete canonical ops. ## `composedHandlerShortcut` _composedHandlerShortcut H_short_A H_short_B metaFor op stateVal — composed UniRet Val mirroring `eval (composeHandlers … H_A H_B opVal stateVal)`. `op` carries `_side`/`_inner` sidecars from `composedInl`/`composedInr`; `metaFor` is the composed-UniRet metadata lookup keyed by inner-op tag; per-side shortcuts must return UniRet Vals. `null` propagates from any of the inputs (kernel fallback)._ ## `composedInl` _composedInl Op_A Op_B subOp — `H.inl Op_A Op_B subOp` with `_side = "composed-inl"`, `_inner = subOp`. Sidecars are ignored by elab and survive `K.send`'s `impureCon` wrap._ ## `composedInr` _composedInr Op_A Op_B subOp — `H.inr` counterpart of `composedInl`._ ## `mkComposedMeta` _mkComposedMeta { SigmaSHoas; A; respHoas; } — precompute `{ sumDescVal; leftSigVal; rightSigVal; }` for the composed UniRet at one canonical inner op (response type already ι-reduced)._ #### Composed-shortcut-laws fx.experimental.desc-interp.composed-shortcut-laws: six H.refl lemmas — state's three canonical ops on inl and error's three strategies on inr — anchoring `composedHandlerShortcut` soundness. Each chains `composeHandlersInl/InrLemma`, the per-effect uniform-shortcut lemma, and inner `sumElim` ι; all three reductions are conv-decidable. ## `errorCollectingInrLemma` _H.refl proof of `errorCollectingInrLemmaTy`. Resume payload := `tt`; new s_B := `cons E payload s_B`._ ## `errorCollectingInrLemmaTy` _`Π E S_A A. Π H_A. Π payload:E. Π s_A:S_A. Π s_B:List E. composeHandlers … H_A uniformOf_collecting (inr (error E payload)) (pair s_A s_B) ≡ mkResumeAt … tt (pair s_A (cons E payload s_B))`._ ## `errorResultInrLemma` _H.refl proof of `errorResultInrLemmaTy`. Abort channel `Sum E A_inner` carries `inl payload`._ ## `errorResultInrLemmaTy` _`Π E S_A S_B A_inner. Π H_A. Π payload:E. Π s_A:S_A. Π s_B:S_B. composeHandlers … H_A uniformOf_result (inr (error E payload)) (pair s_A s_B) ≡ mkAbortAt … (Sum E A_inner) (inl E A_inner payload) (pair s_A s_B)`._ ## `errorStrictInrLemma` _H.refl proof of `errorStrictInrLemmaTy`. Strict always aborts; abort payload := `payload`._ ## `errorStrictInrLemmaTy` _`Π E S_A S_B. Π H_A. Π payload:E. Π s_A:S_A. Π s_B:S_B. composeHandlers … H_A uniformOf_strict (inr (error E payload)) (pair s_A s_B) ≡ mkAbortAt … E payload (pair s_A s_B)`._ ## `stateGetInlLemma` _H.refl proof of `stateGetInlLemmaTy`. `handle_State (get S) s_A ι→ inl(pair s_A s_A)`; inner sumElim ι folds to the composed mkResumeAt._ ## `stateGetInlLemmaTy` _`Π E S_A A. Π H_B. Π s_A:S_A. Π s_B:List E. composeHandlers … handle_State H_B (inl (get S_A)) (pair s_A s_B) ≡ mkResumeAt … (inl (get S_A)) A s_A (pair s_A s_B)`._ ## `stateModifyInlLemma` _H.refl proof of `stateModifyInlLemmaTy`. New sub-state := `fn s_A`; resume payload := `tt`._ ## `stateModifyInlLemmaTy` _`Π E S_A A. Π H_B. Π fn:S_A→S_A. Π s_A:S_A. Π s_B:List E. composeHandlers … handle_State H_B (inl (modify S_A fn)) (pair s_A s_B) ≡ mkResumeAt … tt (pair (fn s_A) s_B)`._ ## `statePutInlLemma` _H.refl proof of `statePutInlLemmaTy`. New sub-state := `param`; resume payload := `tt`._ ## `statePutInlLemmaTy` _`Π E S_A A. Π H_B. Π param s_A:S_A. Π s_B:List E. composeHandlers … handle_State H_B (inl (put S_A param)) (pair s_A s_B) ≡ mkResumeAt … tt (pair param s_B)`._ #### Desc fx.experimental.desc-interp.desc: freer monad encoded as a levitated Desc (freeFx = pure + impure plus continuation queue); the substrate descInterp runs. ## `freeFx` _freeFx Eff Resp A : Desc¹ ⊤ — freer monad as a plus-of-two-summands description; the Impure summand's continuation slot is a queue value `μI U² (kontQueueApp Eff Resp) (Resp op, A)` rather than a meta-level fn._ ``` freeFx : Hoas U -> Hoas (U -> U) -> Hoas U -> Hoas (Desc¹ ⊤) ``` ## `freeFxApp` _freeFxApp Eff Resp A — identity-tagged HOAS form of `freeFx Eff Resp A`; canonApp wrapper that stamps the resulting VDescCon with `_canonRef = { id = "freeFx"; params = [Eff, Resp, A]; }` so conv/quote short-circuit on the canonical identity._ ``` freeFxApp : Hoas U -> Hoas (U -> U) -> Hoas U -> Hoas (Desc¹ ⊤) ``` ## `impureCon` _impureCon Eff Resp A op q — smart constructor for the Impure summand; `q : μI U² (kontQueueApp Eff Resp) (Resp op, A)` is a queue value carrying the continuation. Emits a desc-con HOAS form with a Desc¹ inr payload storing `op` and the ret witness through `LiftAt 0 1`._ ``` impureCon : Hoas U -> Hoas (U -> U) -> Hoas U -> Hoas Eff -> Hoas (μI U² kontQueueApp …) -> Hoas (FreeFx Eff Resp A) ``` ## `kontQueue` _kontQueue Eff Resp : IDesc(U × U) — description-side FTCQueue (Kiselyov & Ishii 2015, section 3.1) at the indexed slice; three-summand plus encoding Identity / Leaf / Node where ∃M existentials are reified as descArgAt-bound index witnesses (CDMM 2010, section 6). Levitation transposition of `nix/nix-effects/src/queue.nix`._ ``` kontQueue : Hoas U -> Hoas (U -> U) -> Hoas (IDesc (U × U)) ``` ## `kontQueueApp` _kontQueueApp Eff Resp — identity-tagged HOAS form of `kontQueue Eff Resp`; canonApp wrapper that stamps the resulting VDescCon with `_canonRef = { id = "kontQueue"; params = [Eff, Resp]; }` so conv on μI(kontQueueApp …) self-equality short-circuits without forcing `.D`. X and A are dropped from the wrapper's params — the indexing is intrinsic to `IDesc(U × U)`._ ``` kontQueueApp : Hoas U -> Hoas (U -> U) -> Hoas (IDesc (U × U)) ``` ## `pureCon` _pureCon Eff Resp A v — smart constructor for the Pure summand; emits a desc-con HOAS form with a Desc¹ inl payload storing `v` and the ret witness through `LiftAt 0 1`._ ``` pureCon : Hoas U -> Hoas (U -> U) -> Hoas U -> Hoas A -> Hoas (FreeFx Eff Resp A) ``` ## `qIdentity` _qIdentity Eff Resp X — smart constructor for the Identity summand of `kontQueue`; emits a desc-con HOAS form at index (X, X) with payload `bootInl (X, refl)` of the outer plus. Carries `_index = { X; A = X; }` sidecar._ ``` qIdentity : Hoas U -> Hoas (U -> U) -> Hoas U -> Hoas (μI U² kontQueueApp …) ``` ## `qLeaf` _qLeaf Eff Resp X A fn — smart constructor for the Leaf summand; `fn : X → μ(freeFxApp Eff Resp A)` is a HOAS lam with `fn.domain == X` (host-level `==` is the principled Nix-meta surrogate for the descArgAt(X)-instantiation equality the kernel checker would enforce, mirroring qNode's seam check on `_index`). Emits a desc-con HOAS form at index (X, A) with payload `bootInr (bootInl (X, (A, (fn, refl))))` of the nested plus. Carries `_index = { X; A; }` sidecar._ ``` qLeaf : Hoas U -> Hoas (U -> U) -> Hoas U -> Hoas U -> Hoas (X -> μ freeFx) -> Hoas (μI U² kontQueueApp …) ``` ## `qNode` _qNode Eff Resp l r — smart constructor for the Node summand; `l` and `r` are queue values whose `_index` sidecars supply the index witnesses. Seam M = l._index.A is asserted equal to r._index.X via Nix-level == (kernel has no surface metavariables; the host-level check is the principled enforcement). Emits a desc-con HOAS form at index (l._index.X, r._index.A) with payload `bootInr (bootInr (X, (M, (A, (l, (r, refl))))))` of the nested plus. Carries `_index = { X; A; }` sidecar._ ``` qNode : Hoas U -> Hoas (U -> U) -> Hoas (μI U² kontQueueApp …) -> Hoas (μI U² kontQueueApp …) -> Hoas (μI U² kontQueueApp …) ``` #### Descind-laws fx.experimental.desc-interp.descind-laws: kernel-level laws derived via `descInd kontQueueApp` at the indexed slice. Hosts qAppKernel (kernel-resident transposition of trampoline.qApp) and qAppIdLaw (qApp qIdentity x ≡ pure x as a kernel conv-witness). ## `qAppIdLaw` _qAppIdLaw : Π(X:U). Π(x:X). bootEq (μ freeFx X) (qAppKernel (X,X) (qIdentity X) x) (pureCon X x). The Identity-collapse law as a kernel conv-witness; the proof is `bootRefl`, justified by descInd-on-descCon β through the Identity step body. Demonstrates that the indexed inductive hypothesis is operationally usable — the lemma's well-typedness depends essentially on `descInd kontQueueApp` projecting `fst_(i)` / `snd_(i)`._ ``` qAppIdLaw : Π(X:U). Π(x:X). bootEq (μ freeFx X) (qAppKernel (X,X) (qIdentity X) x) (pureCon X x) ``` ## `qAppIdLawTy` _qAppIdLawTy Eff Resp: Π-type generated for the qApp identity law at one effect signature._ ## `qAppKernel` _qAppKernel Eff Resp i q x — kernel-resident `qApp` defined via `descInd kontQueueApp`. Identity branch returns `pureCon X x` (X = fst_(i) = snd_(i) forced by index); Leaf branch returns `fn x`; Node branch returns `bind (ih_l x) ih_r` cashing in both indexed IHs at (X,M) and (M,A). Mirrors `trampoline.qApp` but at the kernel layer._ ``` qAppKernel : Hoas U -> Hoas (U -> U) -> Hoas U² -> Hoas (μI U² kontQueueApp i) -> Hoas (fst_(i)) -> Hoas (μ freeFxApp Eff Resp (snd_(i)) tt) ``` #### Effects `error` exposes one op with `strict`/`collecting`/`result` handler strategies. `state` declares `EffState : Π(S:U). U` with `get`/`put`/`modify` and ships a kernel-term `handle_State` head-normalised at run-entry by the trampoline bridge. `typecheck` declares `EffTypeCheck : Π(R:U). U` with one `report` op and six policy handlers: five fold a stream of membership decisions, and `strict` throws on the first failure. ## Sub-namespaces - [`error`](/nix-effects/core-api/experimental/desc-interp/effects/error) - [`error-shortcut-laws`](/nix-effects/core-api/experimental/desc-interp/effects/error-shortcut-laws) - [`error-uniform-shortcut-laws`](/nix-effects/core-api/experimental/desc-interp/effects/error-uniform-shortcut-laws) - [`state`](/nix-effects/core-api/experimental/desc-interp/effects/state) - [`state-shortcut-laws`](/nix-effects/core-api/experimental/desc-interp/effects/state-shortcut-laws) - [`typecheck`](/nix-effects/core-api/experimental/desc-interp/effects/typecheck) - [`typecheck-shortcut-laws`](/nix-effects/core-api/experimental/desc-interp/effects/typecheck-shortcut-laws) ## Source - [`src/experimental/desc-interp/effects/error-shortcut-laws.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/experimental/desc-interp/effects/error-shortcut-laws.nix) - [`src/experimental/desc-interp/effects/error-uniform-shortcut-laws.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/experimental/desc-interp/effects/error-uniform-shortcut-laws.nix) - [`src/experimental/desc-interp/effects/error.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/experimental/desc-interp/effects/error.nix) - [`src/experimental/desc-interp/effects/state-shortcut-laws.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/experimental/desc-interp/effects/state-shortcut-laws.nix) - [`src/experimental/desc-interp/effects/state.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/experimental/desc-interp/effects/state.nix) - [`src/experimental/desc-interp/effects/typecheck-shortcut-laws.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/experimental/desc-interp/effects/typecheck-shortcut-laws.nix) - [`src/experimental/desc-interp/effects/typecheck.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/experimental/desc-interp/effects/typecheck.nix) #### Error Pair with `experimental.desc-interp.trampoline.run`: ```nix let er = error.atType_strict H.string H.nat; in run er.eff er.resp H.nat returnTy { handler = er.handler; dispatch = er.dispatch; } program initialState ``` Strategies differ in handler return shape and dispatch action: strict : Σ State E → action="throw" collecting : Σ (List E) Unit → action="resume", response=tt result : Σ State (Sum E A_inner)→ action="abort" ## `EffError` _EffError : Π(E:U₀). U₀ — kernel datatype with one constructor `error(payload:E)`. Built via `H.datatypeP`; macro-derived `.T`, `.D`, `.elim`, and the `error` introducer._ ## `EffErrorTy` _Π-type of error's op-identity family: `Π(E:U₀). U₀`._ ## `Resp_collecting` _Collecting error response family; errors resume with Unit while accumulating payloads in state._ ## `Resp_collectingTy` _Π-type of collecting-error's response family: `Π(E:U₀). Π(_op:EffError E). U₀`. Body returns `Unit` — collecting always resumes with `tt`._ ## `Resp_result` _Result-channel error response family; errors abort into Sum E A_inner._ ## `Resp_resultTy` _Π-type of result-error's response family: `Π(E:U₀). Π(_op:EffError E). U₀`. Body returns `Void` — result never resumes (abort lives in the program's typed result channel)._ ## `Resp_strict` _Strict error response family; every error op has Void response because strict errors never resume._ ## `Resp_strictTy` _Π-type of strict-error's response family: `Π(E:U₀). Π(_op:EffError E). U₀`. Body returns `Void` at every op — strict never resumes._ ## `atType_collecting` _`atType_collecting E` — per-`E` monomorphisation of `handle_collecting` (State specialised to `List E`). Exposes the `State` field as a convenience for callers needing `H.nil E` initial values._ ## `atType_result` _`atType_result E State A_inner` — per-`(E, State, A_inner)` monomorphisation of `handle_result` with the `Sum E A_inner` result channel pre-built._ ## `atType_strict` _`atType_strict E State` — per-`(E, State)` monomorphisation of `handle_strict` with a `raise` smart constructor and `dispatch` interpreter pre-built. Pass `{ handler; dispatch; }` to the trampoline._ ## `handle_collecting` _Kernel collecting error handler; conses payloads into List E state and resumes with Unit._ ## `handle_collectingTy` _Π-type of the collecting error handler: `Π(E:U₀). Π(op:EffError E). Π(_s:List E). Σ (List E) Unit`. Always resumes — handler accumulates errors into `List E` state, signals resume via `tt`. Dispatch action = `"resume"`._ ## `handle_result` _Kernel result error handler; aborts with Sum E A_inner while preserving state._ ## `handle_resultTy` _Π-type of the result error handler: `Π(E State A_inner:U₀). Π(op:EffError E). Π(_s:State). Σ State (Sum E A_inner)`. Always aborts — handler returns `inl payload` in the Sum-typed result channel. Dispatch action = `"abort"`._ ## `handle_strict` _Kernel strict error handler; maps raise to throw-shaped Σ State E._ ## `handle_strictTy` _Π-type of the strict error handler: `Π(E State:U₀). Π(op:EffError E). Π(_s:State). Σ State E`. Non-resumable — handler surrenders the error payload with the final state. Dispatch action = `"throw"`._ ## `uniformOf_collecting` _`uniformOf_collecting E A`: UniRet-shaped adapter for `handle_collecting`. Dispatches via EffError.elim so the resume payload `tt : Unit ≡ Resp_collecting E (error E payload)` types in each branch._ ## `uniformOf_collectingTy` _Π-type of `uniformOf_collecting`: UniRet at S := List E, with A as a phantom parameter (collecting never aborts)._ ## `uniformOf_result` _`uniformOf_result E State A_inner`: UniRet-shaped adapter for `handle_result`. Projects `Σ State (Sum E A_inner)` into `mkAbort … sumPayload state`._ ## `uniformOf_resultTy` _Π-type of `uniformOf_result`: UniRet at A := Sum E A_inner (handle_result's typed result channel becomes the abort value)._ ## `uniformOf_strict` _`uniformOf_strict E State`: UniRet-shaped adapter for `handle_strict`. Projects `Σ State E` into `mkAbort … e s`; always inhabits the right summand._ ## `uniformOf_strictTy` _Π-type of `uniformOf_strict`: UniRet at A := E (the surrender value rides the abort channel)._ #### Error-shortcut-laws fx.experimental.desc-interp.effects.error-shortcut-laws: kernel-checked one-shot lemmas for each `handle_*` on the canonical EffError raise. Each lemma discharges by `H.refl` — ι on EffError.elim fires at the single `error` constructor, then β reduces the onError branch to a bare `H.pair _ _`. RHSes match `extract.PairRaw` emitter output, anchoring per-strategy shortcut soundness at the kernel layer. ## `collectingRaiseLemma` _Proof of collecting-raise lemma: 3 lams + H.refl. State specialised to `List E`; onError cons-es payload onto the prior list and signals resume via `tt`._ ## `collectingRaiseLemmaTy` _Π-type of collecting-raise lemma: `Π E. Π payload:E. Π s:List E. handle_collecting E (error E payload) s ≡ H.pair (H.cons E payload s) H.tt : Σ (List E) Unit`._ ## `resultRaiseLemma` _Proof of result-raise lemma: 5 lams + H.refl. Always aborts; the Sum E A_inner result channel always carries the inl-injected payload (handler never produces inr)._ ## `resultRaiseLemmaTy` _Π-type of result-raise lemma: `Π E State A_inner. Π payload:E. Π s:State. handle_result E State A_inner (error E payload) s ≡ H.pair s (H.inl E A_inner payload) : Σ State (Sum E A_inner)`._ ## `strictRaiseLemma` _Proof of strict-raise lemma: 4 lams + H.refl. EffError has one constructor, so ι on `EffError.elim 0` fires immediately at `error`; β on the onError λs (`H.lam payload. H.lam _s. H.pair _s payload`) yields `H.pair s payload`._ ## `strictRaiseLemmaTy` _Π-type of strict-raise lemma: `Π E State. Π payload:E. Π s:State. handle_strict E State (error E payload) s ≡ H.pair s payload : Σ State E`._ #### Error-uniform-shortcut-laws fx.experimental.desc-interp.effects.error-uniform-shortcut-laws: kernel-checked H.refl lemmas certifying that each `uniformOf_*` on the canonical EffError raise reduces to the appropriate `mkResumeAt`/`mkAbortAt` UniRet form. All three `uniformOf_X` are defined as direct `EffError.elim 0`-dispatches; at the canonical `error E payload` op, ι on the elim fires (single constructor → onError branch) and β on the onError λs delivers the RHS in one normal-form step. So all three lemmas discharge by H.refl uniformly. ## `collectingUniformLemma` _Proof of collecting-uniform lemma: single H.refl over `uniformOf_collecting`'s direct `EffError.elim 0` dispatch at the canonical `error` constructor. ι + two β. Kernel typing of mkResume's r-slot additionally requires `Resp_collecting E op ≡_ι Unit`, which the same ι firing delivers._ ## `collectingUniformLemmaTy` _Π-type of collecting-uniform lemma: `Π E A. Π payload:E. Π s:List E. uniformOf_collecting E A (error E payload) s ≡ mkResumeAt (List E) Ee Resp_collecting_E op A tt (cons E payload s) : UniRet (List E) Ee Resp_collecting_E op A`._ ## `resultUniformLemma` _Proof of result-uniform lemma: single H.refl over `uniformOf_result`'s direct `EffError.elim 0` dispatch at the canonical `error` constructor. ι + two β; the onError branch's body emits `mkAbort … (inl E A_inner payload) _s` directly._ ## `resultUniformLemmaTy` _Π-type of result-uniform lemma: `Π E State A_inner. Π payload:E. Π s:State. uniformOf_result E State A_inner (error E payload) s ≡ mkAbortAt State Ee Resp_result_E op (Sum E A_inner) (inl E A_inner payload) s : UniRet State Ee Resp_result_E op (Sum E A_inner)`._ ## `strictUniformLemma` _Proof of strict-uniform lemma: single H.refl over `uniformOf_strict`'s direct `EffError.elim 0` dispatch at the canonical `error` constructor. ι + two β._ ## `strictUniformLemmaTy` _Π-type of strict-uniform lemma: `Π E State. Π payload:E. Π s:State. uniformOf_strict E State (error E payload) s ≡ mkAbortAt State Ee Resp_strict_E op E payload s : UniRet State Ee Resp_strict_E op E`._ #### State Pair with `experimental.desc-interp.trampoline.run`: ```nix let st = state.atType H.nat H.nat; in run st.eff st.resp H.nat { inherit (st) handler dispatch; } program initialState ``` ## `EffState` _EffState : Π(S:U₀). U₀ — kernel datatype with three constructors (`get`, `put(param:S)`, `modify(fn:S→S)`). Built via `H.datatypeP`; macro-derived `.T`, `.D`, `.elim`, and per-constructor introducers._ ## `EffStateTy` _Π-type of state's op-identity family: `Π(S:U₀). U₀`._ ## `Resp_State` _Resp_State S op: response type family for state operations; get returns S, put/modify return Unit._ ## `Resp_StateTy` _Π-type of state's per-op response family: `Π(S:U₀). Π(_op:EffState S). U₀`._ ## `atType` _`atType S A` — per-`(S, A)` monomorphisation of `(EffState, Resp_State)` shipping `send`-wrapped smart constructors `get`/`put`/`modify`, the kernel-resident `handler` (= `handle_State` applied to `S` and `A`), and the Nix-side `dispatch` interpreter that reads the kernel-eval'd Sum return value into a `{ action; newState; response?; value?; }` step decision for the trampoline._ ## `elimAt` _elimAt k S — EffState.elim k with hidden state type supplied._ ## `getAt` _getAt S — EffState.get with hidden state type supplied._ ## `handle_State` _handle_State S A op s: kernel-resident state handler returning UniRet left/resume results for get, put, and modify._ ## `handle_StateTy` _Π-type of the state handler: `Π(S A:U₀). Π(op:EffState S). Π(_s:S). Sum (Σ _r:Resp_State[S,op], S) (Σ _a:A, S)`. Always returns the left (resume) summand — state effects never abort._ ## `modifyAt` _modifyAt S fn — EffState.modify with hidden state type and explicit function parameter supplied._ ## `modifyPrefixAt` _modifyPrefixAt S — EffState.modify with hidden state type supplied, leaving the explicit function parameter._ ## `putAt` _putAt S param — EffState.put with hidden state type and explicit state parameter supplied._ ## `putPrefixAt` _putPrefixAt S — EffState.put with hidden state type supplied, leaving the explicit state parameter._ #### State-shortcut-laws fx.experimental.desc-interp.effects.state-shortcut-laws: kernel-checked one-shot lemmas for handle_State on each canonical EffState op (get/put/modify). Each lemma discharges by `H.refl` — kernel conv decides reduction universally via ι on EffState.elim + Resp_State. Lemma RHSes match the `extract.Resume` emitter output, anchoring the shortcut path's soundness at the kernel layer. ## `getLemma` _Proof of get-lemma: 3 lams + H.refl. Reduction at canonical `get` is by ι on `EffState.elim` plus β on the onGet branch; `Resp_State S (get S) ≡ S` by ι makes the resume payload `s` well-typed._ ## `getLemmaTy` _Π-type of get-lemma: `Π S A. Π s:S. handle_State S A (get S) s ≡ mkResumeAt S Op Resp (get S) A s s`._ ## `modifyLemma` _Proof of modify-lemma: 4 lams + H.refl. `Resp_State S (modify S fn) ≡ Unit` by ι; new state := `fn s`._ ## `modifyLemmaTy` _Π-type of modify-lemma: `Π S A. Π fn:S→S. Π s:S. handle_State S A (modify S fn) s ≡ mkResumeAt S Op Resp (modify S fn) A tt (fn s)`._ ## `putLemma` _Proof of put-lemma: 4 lams + H.refl. `Resp_State S (put S param) ≡ Unit` by ι; the prior state `s` is discarded and resume carries new state `param`._ ## `putLemmaTy` _Π-type of put-lemma: `Π S A. Π param s:S. handle_State S A (put S param) s ≡ mkResumeAt S Op Resp (put S param) A tt param`._ #### Typecheck Pair with `experimental.desc-interp.trampoline.run`: ```nix let tc = typecheck.atType_collecting fx.tc.kernel.failure.DiagError; in run tc.eff tc.resp H.unit { inherit (tc) handler dispatch; } program initialState ``` A program is a `bind` chain of `tc.report reason path carrier passed` ops; `reason` is the closed five-token enum, `path` the internalized structural blame site (a `Path = List Position` over the value-side alphabet `Field`/`Elem`/`Tag`/`Tuple`), `carrier` the opaque host residue, `passed` the host-precomputed decision. Every op resumes with `tt`; the handler folds the decision into its state and never inspects the carrier. ## `ByReason` _Per-reason counter tuple in constructor order (a five-Nat Σ-chain)._ ## `EffTypeCheck` _EffTypeCheck : Π(R:U₀). U₀ — one constructor `report(reason:Reason, path:Path, carrier:R, passed:Bool)`._ ## `EffTypeCheckTy` _Π-type of the op-identity family: `Π(R:U₀). U₀`._ ## `Path` _`Path = List Position` — the structural blame site carried by `report`, mirroring the producer's typeCheck `path`._ ## `Position` _Value-side descent position: `Field name` / `Elem idx` / `Tag name` / `Tuple idx`. The internalized core of `fx.diag.positions` for value-validation paths (`name` a strEq-comparable carrier, `idx` a Nat); rendered segment text stays host-side._ ## `Reason` _Closed five-token diagnostic enum (shapeMismatch/missingField/extraField/predicateFailed/deferredPi)._ ## `Resp` _Response family: every `report` op resumes with Unit (the decision rides in the op, not the resume)._ ## `atType_collecting` _`atType_collecting R` — monomorphises the collecting handler and ships `{ eff; resp; handler; dispatch; report; evalOp; handlerShortcut }`. The shortcut rebuilds the resume residual directly (certified by `typecheck-shortcut-laws.nix`)._ ## `atType_firstN` _`atType_firstN R` — monomorphised first-N bridge record._ ## `atType_logging` _`atType_logging R` — monomorphised logging bridge record._ ## `atType_pretty` _`atType_pretty R` — monomorphised pretty bridge record._ ## `atType_strict` _`atType_strict R` — monomorphises the strict handler and ships `{ eff; resp; handler; dispatch; report; evalOp; handlerShortcut }` with a conditional-throw dispatch (resume on pass, throw on fail); the shortcut emits the Sum directly (`Resume`/`Abort`)._ ## `atType_summarize` _`atType_summarize R` — monomorphised summarize bridge record._ ## `atType_summarizeAssoc` _`atType_summarizeAssoc R` — monomorphised summarize-assoc bridge record (assoc-list byReason)._ ## `bumpReason` _bumpReason r b — increment the counter for reason `r` via the Reason eliminator (no string comparison)._ ## `handle_collecting` _Collecting handler: a fail conses the residue, a pass keeps the list. Returns `Σ (List R) Unit`._ ## `handle_firstN` _First-N handler: an O(1) down-counter records failures until it reaches zero, then drops. Returns `Σ (Σ (List R) Nat) Unit`._ ## `handle_logging` _Logging handler: every decision is recorded as `(passed, residue)`. Returns `Σ (List (Σ Bool R)) Unit`._ ## `handle_pretty` _Pretty handler: conses the host-rendered failure line (carried opaquely) on a fail. Same shape as collecting._ ## `handle_strict` _Strict handler: a pass resumes (left summand), a fail surrenders the residue (right summand). Returns `Sum (Σ Unit Unit) (Σ R Unit)`; the right summand drives a `throw`._ ## `handle_summarize` _Summarize handler: groups failures by the reason enum, tracking (passed, failed) totals. Returns `Σ (Σ ByReason (Σ Nat Nat)) Unit`._ ## `handle_summarizeAssoc` _Summarize-assoc handler: groups failures into a `List (Σ String Nat)` byReason assoc list via `insertOrIncrement` (a `listElim`+`strEq` fold), tracking (passed, failed) totals. Returns `Σ (Σ (List (Σ String Nat)) (Σ Nat Nat)) Unit`. The production-faithful `byReason` attrset shape, alongside the O(1) closed-enum variant._ ## `insertOrIncrement` _insertOrIncrement key assoc — a `listElim` fold over a `List (Σ String Nat)` byReason assoc list: a `strEq` key match bumps the entry's count in place, otherwise a new `(key, 1)` is appended._ ## `reasonName` _reasonName r — render the reason enum to its production `byReason` key token (shape-mismatch/missing-field/extra-field/predicate-failed/deferred-pi) via the Reason eliminator._ #### Typecheck-shortcut-laws fx.experimental.desc-interp.effects.typecheck-shortcut-laws: kernel-checked one-shot lemmas certifying each typecheck handler on the canonical `report` op reduces to its shortcut residual (`pair newState tt` for the resume strategies; a `Sum` inl/inr for strict). Each discharges by `H.refl` — ι on EffTypeCheck.elim fires the single `report` branch, β binds the four fields + state, and the step's `boolElim` at a concrete `passed` selects the arm. RHSes match the `extract.PairRaw`/`Resume`/`Abort` emitter outputs, anchoring per-strategy shortcut soundness at the kernel layer. firstN-fail splits on the stuck down-counter (`suc`/`zero`); summarize/summarize-assoc fail keep the reason symbolic (one lemma per variant covers all tokens). ## `collectingFail` _Proof of collecting-fail: 5 lams + H.refl. A fail conses the residue (RHS via consAtExplicit)._ ## `collectingFailTy` _Π-type of collecting-fail: `… (report … false) s ≡ H.pair (cons carrier s) tt`._ ## `collectingPass` _Proof of collecting-pass: 5 lams + H.refl. A pass keeps the accumulator._ ## `collectingPassTy` _Π-type of collecting-pass: `Π R reason path carrier. Π s:List R. handle_collecting R (report reason path carrier true) s ≡ H.pair s tt`._ ## `firstNFailSuc` _Proof of firstN-fail-suc: 6 lams + H.refl. The recorded list is annotated (the `ind` step body is synthesized, so a bare cons carries no element type)._ ## `firstNFailSucTy` _Π-type of firstN-fail at `remaining = suc rp`: `… (report … false) (pair coll (suc rp)) ≡ H.pair (pair (cons carrier coll) rp) tt` — record and decrement._ ## `firstNFailZero` _Proof of firstN-fail-zero: 5 lams + H.refl._ ## `firstNFailZeroTy` _Π-type of firstN-fail at `remaining = zero`: `… (report … false) (pair coll zero) ≡ H.pair (pair coll zero) tt` — drop and hold (the abort state)._ ## `firstNPass` _Proof of firstN-pass: 5 lams + H.refl._ ## `firstNPassTy` _Π-type of firstN-pass: `… handle_firstN R (report … true) s ≡ H.pair s tt` (no `ind`; the state is returned)._ ## `loggingFail` _Proof of logging-fail: 5 lams + H.refl._ ## `loggingFailTy` _Π-type of logging-fail: `… (report … false) s ≡ H.pair (cons (pair false carrier) s) tt`._ ## `loggingPass` _Proof of logging-pass: 5 lams + H.refl. Every decision is recorded; the outcome rides in the record._ ## `loggingPassTy` _Π-type of logging-pass: `… handle_logging R (report … true) s ≡ H.pair (cons (pair true carrier) s) tt`._ ## `prettyFail` _Proof of pretty-fail: 5 lams + H.refl._ ## `prettyFailTy` _Π-type of pretty-fail: `… (report … false) s ≡ H.pair (cons carrier s) tt` (the host-rendered line rides in carrier)._ ## `prettyPass` _Proof of pretty-pass: 5 lams + H.refl._ ## `prettyPassTy` _Π-type of pretty-pass: same shape as collecting-pass (`H.pair s tt`)._ ## `strictFail` _Proof of strict-fail: 5 lams + H.refl._ ## `strictFailTy` _Π-type of strict-fail: `… (report … false) s ≡ inr strictLeft (strictRight R) (pair carrier s)` — surrender the residue down the right summand (the throw channel)._ ## `strictPass` _Proof of strict-pass: 5 lams + H.refl._ ## `strictPassTy` _Π-type of strict-pass: `… handle_strict R (report … true) s ≡ inl strictLeft (strictRight R) (pair tt s)` — resume down the left summand._ ## `summarizeAssocFail` _Proof of summarize-assoc-fail: 7 lams + H.refl._ ## `summarizeAssocFailTy` _Π-type of summarize-assoc-fail (reason symbolic): `… (report reason … false) (pair br (pair pt ft)) ≡ H.pair (pair (insertOrIncrement (reasonName reason) br) (pair pt (suc ft))) tt`._ ## `summarizeAssocPass` _Proof of summarize-assoc-pass: 7 lams + H.refl._ ## `summarizeAssocPassTy` _Π-type of summarize-assoc-pass: `… handle_summarizeAssoc R (report … true) (pair br (pair pt ft)) ≡ H.pair (pair br (pair (suc pt) ft)) tt`._ ## `summarizeFail` _Proof of summarize-fail: 7 lams + H.refl._ ## `summarizeFailTy` _Π-type of summarize-fail (reason symbolic): `… (report reason … false) (pair br (pair pt ft)) ≡ H.pair (pair (bumpReason reason br) (pair pt (suc ft))) tt`. `bumpReason reason br` stays neutral on both sides, so one lemma covers every reason._ ## `summarizePass` _Proof of summarize-pass: 7 lams + H.refl._ ## `summarizePassTy` _Π-type of summarize-pass: `… handle_summarize R (report … true) (pair br (pair pt ft)) ≡ H.pair (pair br (pair (suc pt) ft)) tt` — bump the passed total._ #### Extract ## Soundness layering - **kernel-side (HOAS)**: per-canonical-op lemma `handle_X op s ≡_kernel mkResumeAt … r s` proved by `H.refl` + ι (mirrors `compose-laws.nix`). - **emitter-side (Val)**: per-primitive conv test `eval (HOAS witness) ≡_Val extract (Primitive …)` shown via `fx.tc.conv.conv 0`. Sugar tests are consequences of the primitive tests + composition. - **chain**: kernel-path Val ≡ shortcut-path Val at every canonical op. ## Algebra - **Primitives** — `DescCon`, `BootInl`, `BootInr`, `Pair`, `Tt`, `BootRefl`. One-to-one mirrors of `V.vDescCon` / `vBootInl` / `vBootInr` / `vPair` / `vTt` / `vBootRefl`. - **Sugars** — `Resume`, `Abort`, `PairRaw`. Folded shapes rewriting into nested primitive RF trees; no capability beyond the primitives. - **Consumers** — `Project`, `OfElim`. Read a Val (or RF, via polymorphic `extract`) back into a Val; used by composed shortcuts. Caller supplies `sumDescVal` / `leftSig` / `rightSig` once per `atType` instance. `sumDescValOf leftSig rightSig` recovers the VDescCon `.D` slot from a HOAS sum type by evaluating a canonical `H.inl` skeleton. ## `Abort` _Abort sumDescVal leftSig rightSig value state — UniRet right summand sugar. Folds to `DescCon sumDescVal Tt (BootInr leftSig rightSig (Pair (Pair value state) BootRefl))`. Used by error-strict (surrender), error-result (Sum-typed channel), and composed abort paths._ ## `BootInl` _BootInl left right val — primitive mirror of `V.vBootInl left right val`. Left-summand selector inside a `DescCon` for a plus-of-args description._ ## `BootInr` _BootInr left right val — primitive mirror of `V.vBootInr left right val`. Right-summand counterpart of `BootInl`._ ## `BootRefl` _BootRefl — primitive mirror of `V.vBootRefl`. Structural marker for the `ret` slot of descArg._ ## `DescCon` _DescCon D i d — primitive mirror of `V.vDescCon D i d`. `D` is a descriptor Val, `i` the index Val, `d` the constructor payload._ ## `OfElim` _OfElim scrutinee onResume onAbort — Val-side pattern match on a UniRet-shaped scrutinee. `onResume`/`onAbort` are Nix `Val → Val` functions receiving the bootInl/bootInr payload (a `(payload, state)` VPair). Used by composed shortcuts to dispatch on inner sub-handler results._ ## `Pair` _Pair fst_ snd_ — primitive mirror of `V.vPair fst snd`._ ## `PairRaw` _PairRaw fst_ snd_ — alias for `Pair`. Documents intent as a bare `H.pair _ _` residual (as opposed to UniRet-inner pair). Used by error's three strategies whose reduced RHS isn't UniRet-wrapped._ ## `Project` _Project side of_ — first or second projection of a VPair-typed Val. `side ∈ {"fst","snd"}`. Used by composed-shortcut state-slot repack._ ## `Resume` _Resume sumDescVal leftSig rightSig resp state — UniRet left summand sugar. Folds to `DescCon sumDescVal Tt (BootInl leftSig rightSig (Pair (Pair resp state) BootRefl))`. Mirrors `eval (mkResumeAt S Op Resp op A resp state)`._ ## `Tt` _Tt — primitive mirror of `V.vTt`. Unit Val constant._ ## `extract` _extract : ResidualForm → Val. Direct attrset construction targeting the VDescCon/VBootInl|VBootInr/VPair encoding produced by `eval` on the canonical mkResumeAt / mkAbortAt / fst_ / snd_ / sumElim spines. Per-constructor conv tests under `_self.tests` discharge emitter-side soundness._ ## `sumDescValOf` _sumDescValOf leftSigHoas rightSigHoas — recovers the VDescCon `.D` slot for a `H.sum leftSig rightSig` instantiation by evaluating a canonical `H.inl` skeleton and reading its `.D`. Caller-side helper for building `Resume`/`Abort` metadata Vals once per `atType` instance._ #### Kernel fx.experimental.desc-interp.kernel: pure/send/bind constructors producing FreeFx Desc values directly as `μ freeFxApp` data, not Nix-host computations. ## `bind` _bind Eff Resp A B m f — sequence two computations. Pure case applies `f` to the payload; Impure case snocs `f` into the queue. Linear continuation chains stay flat in the queue, sidestepping Nix's call-depth ceiling._ ``` bind : Hoas U -> Hoas (U -> U) -> Hoas U -> Hoas U -> Hoas (μ freeFxApp Eff Resp A) -> (Hoas A -> Hoas (μ freeFxApp Eff Resp B)) -> Hoas (μ freeFxApp Eff Resp B) ``` ## `pure` _pure Eff Resp A v — lift v into the Pure summand of FreeFx; alias for `D.pureCon`._ ``` pure : Hoas U -> Hoas (U -> U) -> Hoas U -> Hoas A -> Hoas (μ freeFxApp Eff Resp A) ``` ## `qSnoc` _qSnoc Eff Resp B q f — append the Nix-meta continuation `f` (Hoas q._index.A → Hoas (μ freeFxApp Eff Resp B)) to the right of queue `q`. The chain's input type is q._index.X and its current output is q._index.A. Identity short-circuits to a fresh `qLeaf` at (q._index.A, B); otherwise builds `qNode q (qLeaf q._index.A B fn)` with the seam M = q._index.A asserted via the sidecar invariant on qNode._ ``` qSnoc : Hoas U -> Hoas (U -> U) -> Hoas U -> Hoas (μI U² kontQueueApp …) -> (Hoas A -> Hoas (μ freeFxApp Eff Resp B)) -> Hoas (μI U² kontQueueApp …) ``` ## `send` _send Eff Resp op — issue an effect request with the identity continuation; emits an impureCon whose queue slot is `qIdentity Eff Resp (Resp op)` at index `(Resp op, Resp op)`._ ``` send : Hoas U -> Hoas (U -> U) -> Hoas Eff -> Hoas (μ freeFxApp Eff Resp (Resp op)) ``` #### Trampoline fx.experimental.desc-interp.trampoline: qApp/run/handle interpreters trampolining FreeFx Desc values via genericClosure for O(1) stack depth on long chains. ## `handle` _handle Eff Resp A { return?; handler; dispatch; state } prog — thin wrapper over `run` with a custom `return` clause folding the final (value, state) pair._ ``` handle : Hoas U -> Hoas (U -> U) -> Hoas U -> { return?, handler, dispatch, state } -> Hoas (μ freeFxApp Eff Resp A) -> b ``` ## `qApp` _qApp Eff Resp q x — apply queue `q` to value `x : Hoas q._index.X`; trampolines Pure-returning leaves via genericClosure; on Impure splices the inner queue with the remaining tail. The queue's `_index` sidecar carries the (X, A) indexing that the kernel's μI U² slot witnesses._ ``` qApp : Hoas U -> Hoas (U -> U) -> Hoas (μI U² kontQueueApp …) -> Hoas X -> Hoas (μ freeFxApp Eff Resp A) ``` ## `qAppend` _qAppend Eff Resp l r — concatenate two queues. Identity short-circuits on either side mirror `queue.nix:append`; otherwise builds `qNode l r` with the seam M = `l._index.A` (= `r._index.X`, asserted host-level by qNode). The composite queue inhabits μI U² kontQueueApp (l._index.X, r._index.A)._ ``` qAppend : Hoas U -> Hoas (U -> U) -> Hoas (μI U² kontQueueApp …) -> Hoas (μI U² kontQueueApp …) -> Hoas (μI U² kontQueueApp …) ``` ## `run` _run Eff Resp A { handler; dispatch } prog initialState — drive `prog : μ(freeFxApp Eff Resp A)` to completion via a `genericClosure` worklist; per Impure node, the kernel-resident `handler` term is applied to the op + threaded state via `vApp`, and the Nix-side `dispatch` interpreter reads the kernel-eval'd result to produce a step decision._ ``` run : Hoas U -> Hoas (U -> U) -> Hoas U -> { handler; dispatch } -> Hoas (μ freeFxApp Eff Resp A) -> Hoas State -> { value; state } ``` ### Diagnostics #### Positions Shared diagnostic alphabet. Pure data. A Position names the blame location in a structural descent through a Desc or through raw MLTT structure (Π / Σ / Ann / μ / App). The alphabet is description-centric: names such as `DArgSort`, `DPlusL`, and `PiDom` identify sub-positions by their meaning in the structure, not by the code path that happens to visit them. Each Position carries four fields: - `tag` : variant discriminator (used by hint-key lookup); - `segment` : short rendered path label (e.g. `"arg.S"`, `"Π.dom"`); - `intent` : semantic role description, rendered by the pretty printer as a "what was expected at this position" gloss alongside the segment; - `rule` : optional descent-rule annotation; null on every constant. `withRule` produces a decorated variant and `bindPR` populates it at the emission site. `intent` and `rule` are opaque to the hint resolver: keys are built from `tag` only, so decorating Positions does not affect lookup. Two kinds of consumer: - A kernel enrichment layer that wraps rule delegations, emitting a child error tagged with the `Position` of the sub-call that failed. - A value-level validator (record / list / variant field walkers) that emits `Field` / `Elem` / `Tag` positions from its per- component blame traversal. Both consumers produce `Error` trees whose children are keyed by `Position`, allowing errors from either source to compose into one tree. ## `AnnTerm` _AnnTerm: canonical Position constant for the term being annotated._ ``` Position ``` Canonical diagnostic Position constant. Segment: `ann.term`. ## `AnnType` _AnnType: canonical Position constant for the type annotation supplied._ ``` Position ``` Canonical diagnostic Position constant. Segment: `ann.type`. ## `AppArg` _AppArg: canonical Position constant for the argument supplied to the application._ ``` Position ``` Canonical diagnostic Position constant. Segment: `app.arg`. ## `AppHead` _AppHead: canonical Position constant for the function applied._ ``` Position ``` Canonical diagnostic Position constant. Segment: `app.head`. ## `Case` _Case: parameterised position naming an eliminator's case handler by `name` — generated constructor names, `"inl"`/`"inr"`, `"base"`, `"onRet"`/`"onArg"`/`"onRec"`/`"onPi"`/`"onPlus"`, `"step"`._ ``` Case : String -> Position ``` ## `DArgBody` _DArgBody: canonical Position constant for the description body produced by `arg`'s family._ ``` Position ``` Canonical diagnostic Position constant. Segment: `arg.T`. ## `DArgEq` _DArgEq: canonical Position constant for the equality witness paired with `arg`'s sort._ ``` Position ``` Canonical diagnostic Position constant. Segment: `arg.eq`. ## `DArgLevel` _DArgLevel: canonical Position constant for the universe level at which `arg`'s sort lives._ ``` Position ``` Canonical diagnostic Position constant. Segment: `arg.k`. ## `DArgSort` _DArgSort: canonical Position constant for the sort inhabited by `arg`'s argument._ ``` Position ``` Canonical diagnostic Position constant. Segment: `arg.S`. ## `DConLayer` _DConLayer: parameterised position naming the `layer`-th step in a peeled linear-recursive `desc-con` trampoline chain (0 = outermost, n = base); renders as `con[]`; quotient representative of homogeneous μ-unfolding paths so per-blame chain depth stays constant regardless of trampoline depth._ ``` DConLayer : Int -> Position ``` ## `DElimLevel` _DElimLevel: canonical Position constant for the universe level at which the eliminator's motive lives._ ``` Position ``` Canonical diagnostic Position constant. Segment: `elim.k`. ## `DPiBody` _DPiBody: canonical Position constant for the description body produced by `pi`'s family._ ``` Position ``` Canonical diagnostic Position constant. Segment: `pi.T`. ## `DPiEq` _DPiEq: canonical Position constant for the equality witness paired with `pi`'s sort._ ``` Position ``` Canonical diagnostic Position constant. Segment: `pi.eq`. ## `DPiFn` _DPiFn: canonical Position constant for the index selector function of `pi`._ ``` Position ``` Canonical diagnostic Position constant. Segment: `pi.f`. ## `DPiLevel` _DPiLevel: canonical Position constant for the universe level at which `pi`'s domain lives._ ``` Position ``` Canonical diagnostic Position constant. Segment: `pi.k`. ## `DPiSort` _DPiSort: canonical Position constant for the sort inhabited by `pi`'s domain._ ``` Position ``` Canonical diagnostic Position constant. Segment: `pi.S`. ## `DPlusL` _DPlusL: canonical Position constant for the left summand of `plus`._ ``` Position ``` Canonical diagnostic Position constant. Segment: `plus.L`. ## `DPlusR` _DPlusR: canonical Position constant for the right summand of `plus`._ ``` Position ``` Canonical diagnostic Position constant. Segment: `plus.R`. ## `DRecIndex` _DRecIndex: canonical Position constant for the index value supplied to `rec`._ ``` Position ``` Canonical diagnostic Position constant. Segment: `rec.j`. ## `DRecTail` _DRecTail: canonical Position constant for the description tail spliced onto `rec`._ ``` Position ``` Canonical diagnostic Position constant. Segment: `rec.D`. ## `DRetIndex` _DRetIndex: canonical Position constant for the index value supplied to `ret`._ ``` Position ``` Canonical diagnostic Position constant. Segment: `ret.j`. ## `Elem` _Elem: parameterised position naming a list element by `idx`; renders as `[]` in blame paths and carries `idx` for downstream lookup by integer index._ ``` Elem : Int -> Position ``` ## `Field` _Field: parameterised position naming a record field by `name`; renders as `.` in blame paths and carries `name` for downstream lookup by field-key._ ``` Field : String -> Position ``` ## `JEq` _JEq: canonical Position constant for the equality witness consumed by `J`._ ``` Position ``` Canonical diagnostic Position constant. Segment: `J.eq`. ## `JLhs` _JLhs: canonical Position constant for the left endpoint of `J`._ ``` Position ``` Canonical diagnostic Position constant. Segment: `J.a`. ## `JRhs` _JRhs: canonical Position constant for the right endpoint of `J`._ ``` Position ``` Canonical diagnostic Position constant. Segment: `J.b`. ## `JType` _JType: canonical Position constant for the type carrying `J`'s two endpoints._ ``` Position ``` Canonical diagnostic Position constant. Segment: `J.A`. ## `LamBody` _LamBody: canonical Position constant for the body of the lambda under its bound variable._ ``` Position ``` Canonical diagnostic Position constant. Segment: `lam.body`. ## `LetBody` _LetBody: canonical Position constant for the body of the let-binding._ ``` Position ``` Canonical diagnostic Position constant. Segment: `let.body`. ## `LevelMaxLhs` _LevelMaxLhs: canonical Position constant for the left operand of `max`._ ``` Position ``` Canonical diagnostic Position constant. Segment: `max.L`. ## `LevelMaxRhs` _LevelMaxRhs: canonical Position constant for the right operand of `max`._ ``` Position ``` Canonical diagnostic Position constant. Segment: `max.R`. ## `LevelSucPred` _LevelSucPred: canonical Position constant for the predecessor of `suc`._ ``` Position ``` Canonical diagnostic Position constant. Segment: `suc.pred`. ## `Motive` _Motive: canonical Position constant for the motive of the eliminator._ ``` Position ``` Canonical diagnostic Position constant. Segment: `motive`. ## `MuDesc` _MuDesc: canonical Position constant for the description argument of `con`._ ``` Position ``` Canonical diagnostic Position constant. Segment: `con.D`. ## `MuIndex` _MuIndex: canonical Position constant for the index value supplied to `con`._ ``` Position ``` Canonical diagnostic Position constant. Segment: `con.i`. ## `MuPayload` _MuPayload: canonical Position constant for the payload of `con` at its index._ ``` Position ``` Canonical diagnostic Position constant. Segment: `con.d`. ## `MuUnroll` _MuUnroll: canonical Position constant for an unrolling step of μ._ ``` Position ``` Canonical diagnostic Position constant. Segment: `μ`. ## `OpaqueType` _OpaqueType: canonical Position constant for the Π-type annotation of the opaque lambda._ ``` Position ``` Canonical diagnostic Position constant. Segment: `opaque.type`. ## `PiCod` _PiCod: canonical Position constant for the codomain family of Π._ ``` Position ``` Canonical diagnostic Position constant. Segment: `Π.cod`. ## `PiDom` _PiDom: canonical Position constant for the domain type of Π._ ``` Position ``` Canonical diagnostic Position constant. Segment: `Π.dom`. ## `Scrut` _Scrut: canonical Position constant for the scrutinee of the eliminator._ ``` Position ``` Canonical diagnostic Position constant. Segment: `scrut`. ## `SigmaFst` _SigmaFst: canonical Position constant for the first component type of Σ._ ``` Position ``` Canonical diagnostic Position constant. Segment: `Σ.fst`. ## `SigmaSnd` _SigmaSnd: canonical Position constant for the second component type of Σ._ ``` Position ``` Canonical diagnostic Position constant. Segment: `Σ.snd`. ## `Sub` _Sub: canonical Position constant for the subsumption bridge from CHECK to INFER._ ``` Position ``` Canonical diagnostic Position constant. Segment: `sub`. ## `Tag` _Tag: parameterised position naming a tagged-union arm by `name`; renders as `#` in blame paths and carries `name` for downstream lookup by variant tag._ ``` Tag : String -> Position ``` ## `Tuple` _Tuple: parameterised position naming a tuple component by static `idx`; renders identically to `Elem` (`[]`) but stays distinct so consumers tell n-ary tuple from list descent._ ``` Tuple : Int -> Position ``` ## `ULevel` _ULevel: canonical Position constant for the level argument of `U`._ ``` Position ``` Canonical diagnostic Position constant. Segment: `U.k`. ## `eq` _eq: structural equality on `Position` values; relies on Nix's attrset-by-content comparison so equal positions compare equal regardless of construction path._ ``` eq : Position -> Position -> Bool ``` ## `isPosition` _isPosition: predicate recognising the `Position` ADT — checks `_tag == "Position"`; complements the `Layer`/`Error` predicates from `fx.diag.error`._ ``` isPosition : Any -> Bool ``` ## `renderSegment` _renderSegment: render a `Position` as its short human-readable segment (`"arg.S"`, `"plus.L"`, `".age"`); single field read since the segment is carried on the Position itself._ ``` renderSegment : Position -> String ``` ## `withRule` _withRule: decorate a `Position` with a descent-rule annotation, returning a structurally distinct Position whose `rule` field is set to the supplied string; leaves the original canonical constant intact for sharing._ ``` withRule : String -> Position -> Position ``` Use at the kernel-rule call site to record which rule emitted the descent — paired with `bindPR` to wrap the inner computation under a Position that carries both the structural coordinate (`tag`/`segment`/`intent`) and the rule identity. The hint resolver ignores `rule` (keys come from `tag` only), so decorating Positions never changes lookup. #### Error Diagnostic Error ADT. An Error has a Layer (Kernel | Generic | Contract), a layer-discriminated Detail record, a short msg, an optional hint, and a list of children. A leaf has `children = []`; a branch has a non-empty children list whose entries are `{ position, error }` pairs. Sibling failures produce many children; a chained descent produces one child; a leaf has none. Constructors: mkKernelError { position?, rule, msg, expected?, got?, ctx_depth?, hint? } mkGenericError { type?, context?, value, desc?, index?, guard?, msg, hint? } mkContractError { type?, context?, value, guard, msg, hint? } Per-layer Detail builders: mkKernelDetail / mkGenericDetail / mkContractDetail Combinators: nestUnder : Position -> Error -> Error addChild : Position -> Error -> Error -> Error setLeafHint : Hint -> Error -> Error Layer constants: Kernel, Generic, Contract. Predicates: isError, isLayer, isDetail, isKernel, isGeneric, isContract. Equality: eq (structural). Pure data. No dependencies on kernel, trampoline, effects, tc, or types modules. ## `Contract` _Contract: Layer constant tagging a value with the correct shape that violates a refinement predicate (`guard`)._ Paired with a `ContractDetail` carrying `value`, the mandatory `guard = { predicate = "…"; }`, and optionally the surface `type` and outer `context`. Renderer formats Contract failures as predicate violations distinct from shape mismatches. Pre-allocated; never construct via `{ _tag = ...; }`. ## `Generic` _Generic: Layer constant tagging a value whose shape fails to inhabit a description._ Emitted by sugar validators (`fx.types.*`) and the generic `descElim`-driven shape checker. Paired with a `GenericDetail` carrying `value` (the failing input) and usually `type` / `desc` identifying the violated contract. Pure shape failures (record-field type mismatch, wrong variant tag) live here; refinement-predicate failures belong in `Contract`. ## `Kernel` _Kernel: Layer constant tagging a kernel-layer diagnostic error; `Error.layer` is set to this for typechecker/elaborator failures._ Emitted by `checkHoas` / `infer` / elaborator passes. Paired with a `KernelDetail` carrying `rule` (the failing kernel rule name) and typically `expected` / `got` carrying normalised value-domain types. Pre-allocated; never construct via `{ _tag = ...; }`. ## `addChild` _addChild: append a keyed child `Error` to a branching `parent`; used by collecting handlers that gather many sibling failures under one root without flattening the blame paths._ ``` addChild : Position -> Error -> Error -> Error -- position, parent, child ``` Use for sibling collection (record fields, variant cases, list elements all rejected against the same root contract). Order of insertion is preserved in the resulting children list, so callers control reporting order. The parent's `layer` / `detail` / `msg` are not overwritten — they describe the outer-shape failure, while each child describes one element's failure. For descent (one-deep hop), use `nestUnder` instead; `addChild` is the multi-sibling counterpart. ## `appendTrace` _appendTrace: append a `{ rule, position }` entry to a Kernel-layer error's `detail.trace`; non-Kernel errors pass through unchanged. Used by `bindP`/`bindPR` to auto-capture the descent chain as the error unwinds._ ``` appendTrace : (String | null) -> Position -> Error -> Error ``` ## `captureFrame` _captureFrame: project a typing context into a `{ depth, env, types, names }` Frame suitable for `KernelDetail.frame`; reads `ctx.names` defensively so contexts without name tracking still produce a well-formed frame._ ``` captureFrame : Ctx -> Frame ``` ## `eq` _eq: structural equality on `Error` values — relies on Nix's attrset-by-content comparison so equal trees compare equal regardless of construction path._ ``` eq : Error -> Error -> Bool ``` Equality is content-based, so two distinct constructions of the same shape (`mkKernelError { rule = "r"; msg = "m"; }` twice) compare equal — useful for test assertions and for deduplicating siblings in a collecting handler. Position ADT values inside `children` participate in the comparison, so chains with reordered hops are not equal. ## `isContract` _isContract: `Layer` variant predicate — true iff the value is the `Contract` constant._ ``` isContract : Any -> Bool ``` ## `isDetail` _isDetail: predicate recognising the `Detail` ADT — checks `_tag == "Detail"` plus the presence of a `layer` self-discriminator._ ``` isDetail : Any -> Bool ``` ## `isError` _isError: predicate recognising the `Error` ADT — checks `_tag == "Error"` plus the required fields `layer`, `detail`, `msg`, `children`._ ``` isError : Any -> Bool ``` Use at module boundaries where input could be anything; internal code that constructs Errors via the per-layer constructors doesn't need to check. The predicate rejects bare attrsets that happen to have `_tag = "Error"` but are missing required fields — guards against accidental partial construction more than against type confusion. ## `isGeneric` _isGeneric: `Layer` variant predicate — true iff the value is the `Generic` constant._ ``` isGeneric : Any -> Bool ``` ## `isKernel` _isKernel: `Layer` variant predicate — true iff the value is the `Kernel` constant._ ``` isKernel : Any -> Bool ``` ## `isLayer` _isLayer: predicate recognising the `Layer` ADT — checks `_tag == "Layer"` plus the presence of `tag`; accepts `Kernel`, `Generic`, and `Contract`._ ``` isLayer : Any -> Bool ``` Does not distinguish the variants — pair with a check on `.tag` for layer-specific dispatch, or use `isKernel` / `isGeneric` / `isContract`. Rejects plain attrsets `{ tag = "Kernel"; }` that lack `_tag`, so renderer branching can trust a positive result to mean a canonical constant. ## `mkContractDetail` _mkContractDetail: build a Contract-layer `Detail` record from optional field overrides; defaults set every field to null and carry the layer self-discriminator. Callers should always populate `guard`._ ``` mkContractDetail : { type?, value?, context?, guard? } -> Detail ``` ## `mkContractError` _mkContractError: build a contract-layer leaf `Error` from `{ value, guard, msg, type?, context?, hint? }`; `value` has the correct shape but `guard` rejected it._ ``` mkContractError : { value, guard, msg, type?, context?, hint? } -> Error ``` `guard` is mandatory — a `{ predicate = "…"; }` record naming the refinement that rejected `value`. Without a guard the failure is structural, not contractual, and belongs in `mkGenericError`. `type` and `context` describe the surrounding contract for the renderer. ## `mkGenericDetail` _mkGenericDetail: build a Generic-layer `Detail` record from optional field overrides; defaults set every field to null and carry the layer self-discriminator._ ``` mkGenericDetail : { type?, desc?, value?, context?, index?, guard? } -> Detail ``` ## `mkGenericError` _mkGenericError: build a generic-layer leaf `Error` from `{ value, msg, type?, context?, desc?, index?, guard?, hint? }`; `value` is the thing whose shape failed to inhabit the description._ ``` mkGenericError : { value, msg, type?, context?, desc?, index?, guard?, hint? } -> Error ``` `type` is the surface name presented to the user (`"PersonT"`, `"NonEmptyList"`); `desc` is the underlying `Desc I` shape if the producer has it; `context` is an outer-scope name used in nested record/variant failures. The `guard` slot is retained for callers that have not yet migrated refinement failures to `mkContractError`; prefer Contract for new producers. ## `mkKernelDetail` _mkKernelDetail: build a Kernel-layer `Detail` record from optional field overrides; defaults set every field to null (trace to []) and carry the layer self-discriminator._ ``` mkKernelDetail : { rule?, expected?, got?, ctx_depth?, term?, frame?, trace? } -> Detail ``` ## `mkKernelError` _mkKernelError: build a kernel-layer leaf `Error` from `{ rule, msg, position?, expected?, got?, ctx_depth?, term?, frame?, trace?, hint? }`; when `position` is supplied, wraps the leaf in `nestUnder` so the rule emits its own descent coordinate._ ``` mkKernelError : { rule, msg, position?, expected?, got?, ctx_depth?, term?, frame?, trace?, hint? } -> Error ``` `rule` is the kernel-rule identifier — `hints.nix` keys its Hint table off `rule` plus blame-path-suffix, so use the canonical name (`"check"`, `"desc-arg"`, `"univ-poly"`, …) not an ad-hoc string. `expected` / `got` should be value-domain terms (post-`eval`), not unelaborated HOAS — the diff renderer expects normalised shape. Supplying `position` is equivalent to `nestUnder p (mkKernelError { … })`; use it when the rule emits a single descent hop, not when the caller will add hops of its own. `term` names the failing surface-term shape; `frame` is a Ctx snapshot via `captureFrame`; `trace` is usually left empty at emission and populated by `bindP` / `bindPR` as the error unwinds. ## `nestUnder` _nestUnder: add a positional hop above `inner`, producing a new branch whose single child carries `position` as its edge label; pass-through of layer/detail/msg/hint preserves leaf rendering at any depth._ ``` nestUnder : Position -> Error -> Error ``` Pass-through invariant: the wrapper's `layer`, `detail`, `msg`, and `hint` are copied from `inner`. This is what lets the renderer pick its branch (Kernel vs Generic) and pick the leaf's payload at any depth without descending the chain — the entire single-child chain reports the same diagnostic, just with a longer blame path. Stacking is by repeated application: `nestUnder pOuter (nestUnder pInner leaf)` yields a chain whose outer edge is `pOuter`, inner edge `pInner`. ## `setLeafHint` _setLeafHint: walk the single-child chain to its leaf and overwrite `hint` on the endpoint, returning a structurally-equivalent tree; stack-safe via `splitChainFast`/`splitChainSlow` to kernel-descent depth._ ``` setLeafHint : Hint -> Error -> Error ``` Only mutates the chain endpoint. If the endpoint is branching (children count > 1), returns the tree unchanged — sibling- specific hint attachment is the caller's responsibility, the function is intentionally a no-op there to avoid clobbering ambiguity. Stack-safe to arbitrary depth: switches from direct recursion to a `genericClosure` worklist past `fastPathLimit` (500) frames. Use after `hints.resolve` has produced a Hint for the failure, not before. #### Hints Hint resolver for diagnostic Errors. Exports: resolve : Error -> Hint | null classify : Error -> String hints : { = Hint; } A Hint is `{ _tag = "Hint"; text; category; severity; docLink; }`. The `_tag` marker keeps it terminal for `api.extractValue`, and the remaining fields are plain data consumable by renderers, LSPs, docs, and linters. Severity is `"error"` at this layer; `docLink` resolves to a dedicated per-key page on `docs.kleisli.io` of the form `/nix-effects/diag-hints/`, where `` matches the docs-site heading-id slugification rule applied to the key. Each per-key page is also exposed as an MCP resource at `docs://kleisli/nix-effects/diag-hints/`, so a compiler Hint dereferences to focused agent-readable content in one fetch. Keys encode a leaf-anchored suffix of the blame path plus the classifier pattern: `"....::"`. A key matches when its positions equal the last N tags of the blame path; `resolve` returns the hint under the longest matching suffix. Single-position keys are the 1-hop special case. Hint text is position-semantic: no kernel-rule strings, no source-file references. Chain walking recurses directly up to 500 frames, then falls through to a `builtins.genericClosure` slow path that WHNF-forces the next node. ## `classify` _classify: derive the classifier pattern string from an `Error`'s detail and leaf position — yields the right-hand side of suffix-keys consumed by `resolve` (e.g. `"universe-mismatch"`)._ ``` classify : Error -> String ``` ## `hints` _hints: the closed `{ key = Hint }` registry indexed by `....::` keys; each value carries `text`, `category`, `severity`, `docLink` to the per-key `/nix-effects/diag-hints/` page._ ## `mkHint` _mkHint: structured `Hint` constructor from a `category` and `text` — sets `_tag = "Hint"` (terminal for `extractValue`), `severity = "error"`, and the default `docLink` pointing at the diag-hints section root._ ``` mkHint : Category -> String -> Hint ``` ## `resolve` _resolve: walk a blame path inward from leaf to root, returning the `Hint` under the longest matching suffix-key — the canonical lookup for surfacing position-semantic guidance on a kernel `Error`._ ``` resolve : Error -> Hint | null ``` ## `taxonomy` _taxonomy: closed list of allowed `Hint.category` values (`"universe"`, `"sort"`, `"description"`, `"arity"`, `"indexing"`, `"inhabitation"`, …); enforced by the `hints-categories-in-taxonomy` test._ ## Hint registry Diagnostic hints give stable names to recurring checker and validation failures. Use the per-hint pages when an error includes a `Hint::` token, or scan the registry below to see the available hint keys. ### DArgSort::universe-mismatch Category: **universe** · Severity: **error** the sort position of `arg` must live in U(0); descriptions only carry small types. Pass `u 0`, or factor the dependency through `descRec` / `descPi` if a larger type is genuinely needed. ### DPiSort::universe-mismatch Category: **universe** · Severity: **error** the sort position of `pi` must live in U(0); `descPi` takes a small domain. Use `u 0`, or encode the dependency through an index instead of the Pi domain. ### LevelMaxLhs::type-mismatch Category: **universe** · Severity: **error** the left operand of `max` must be a Level ### LevelMaxRhs::type-mismatch Category: **universe** · Severity: **error** the right operand of `max` must be a Level ### LevelSucPred::type-mismatch Category: **universe** · Severity: **error** the predecessor of `suc` must be a Level ### ULevel::type-mismatch Category: **universe** · Severity: **error** the level argument of `U` must be a Level ### AnnType::not-a-type Category: **sort** · Severity: **error** the annotation position must be a type (live in some U(k)), not a term. Write a type expression such as `nat`, `bool`, `u 0`, or a user-defined datatype. ### JType::not-a-type Category: **sort** · Severity: **error** the type parameter of `J` must be a type (live in some U(k)), not a term. Pass a type expression like `nat`, `u 0`, or the type shared by J's two endpoints. ### Motive.PiDom::not-a-type Category: **sort** · Severity: **error** the motive's domain must be a type (live in some U(k)). The motive receives the scrutinee's type as its domain and returns a type; supply a concrete type such as `nat`, `u 0`, or the datatype being eliminated. ### Motive::not-a-type Category: **sort** · Severity: **error** an eliminator's motive must return a type (live in some U(k)) ### PiCod::not-a-type Category: **sort** · Severity: **error** the codomain family of Π must return a type for each argument, not an ordinary value. Provide a function whose body inhabits some `U k`. ### PiDom::not-a-type Category: **sort** · Severity: **error** the domain of Π must be a type (live in some U(k)), not a term or value. Supply a type expression like `nat`, `bool`, `u 0`, or a user-defined datatype. ### DArgBody::not-a-desc Category: **description** · Severity: **error** the body of `arg` must produce a description (Desc I), not an ordinary value. Build one with `descRet`, `descArg`, `descRec`, `descPi`, or `descPlus`. ### DPiBody::not-a-desc Category: **description** · Severity: **error** the body of `pi` must produce a description for each input, not a plain term. Return a Desc I via `descRet`, `descArg`, `descRec`, `descPi`, or `descPlus`. ### DPlusL::not-a-desc Category: **description** · Severity: **error** the left summand of `plus` must be a description (Desc I) ### DPlusR::not-a-desc Category: **description** · Severity: **error** the right summand of `plus` must be a description at the same index type as the left summand ### DRecTail::not-a-desc Category: **description** · Severity: **error** the tail position of `rec` must itself be a description, not an ordinary term. Continue the spine with `descRet`, `descArg`, `descRec`, `descPi`, or `descPlus`. ### MuDesc::not-a-desc Category: **description** · Severity: **error** the description argument of μ must be a Desc I term, not an ordinary value. Construct it with `descRet`, `descArg`, `descRec`, `descPi`, or `descPlus`. ### AppHead::not-a-function Category: **arity** · Severity: **error** the head of an application must have a function type (Pi) ### DPiFn::not-a-function Category: **arity** · Severity: **error** the index selector `f` of `pi` must be a function `S -> I` ### Motive::not-a-function Category: **arity** · Severity: **error** the motive must be a function from the scrutinee's type into a type, not a bare type or value. Supply a one-argument function whose body lives in some `U k`. ### OpaqueType::not-a-function Category: **arity** · Severity: **error** the annotation on an opaque lambda must be a Pi type ### DPiFn::type-mismatch Category: **indexing** · Severity: **error** the index selector's domain must match the declared sort `S` ### DRecIndex::type-mismatch Category: **indexing** · Severity: **error** the index position of `rec` must match the Desc's declared index type. Pass a term of that index type, or adjust the enclosing `μ I ...` to match. ### DRetIndex::type-mismatch Category: **indexing** · Severity: **error** the index position of `ret` must match the Desc's declared index type. Supply a term of that index type, or redefine the enclosing `μ I ...` over the index you actually have. ### MuIndex::type-mismatch Category: **indexing** · Severity: **error** the index passed to `con` must have the description's index type ### MuPayload::type-mismatch Category: **indexing** · Severity: **error** the payload of `con` must inhabit the description's interpretation at the given index ### Elem::inhabitation-failed Category: **inhabitation** · Severity: **error** the element does not inhabit the list's element type ### Field::inhabitation-failed Category: **inhabitation** · Severity: **error** the field's value does not inhabit the declared field type ### SigmaFst::inhabitation-failed Category: **inhabitation** · Severity: **error** the first component does not inhabit the declared `fst` type ### SigmaSnd::inhabitation-failed Category: **inhabitation** · Severity: **error** the second component does not inhabit the dependent `snd` type ### Tag::inhabitation-failed Category: **inhabitation** · Severity: **error** the variant's payload does not inhabit the branch type ### Elem::refinement-failed Category: **refinement** · Severity: **error** the element violates the element type's refinement predicate ### Field::refinement-failed Category: **refinement** · Severity: **error** the field's value violates the field type's refinement predicate ### SigmaFst::refinement-failed Category: **refinement** · Severity: **error** the first component violates the `fst` type's refinement predicate ### SigmaSnd::refinement-failed Category: **refinement** · Severity: **error** the second component violates the `snd` type's refinement predicate ### Tag::refinement-failed Category: **refinement** · Severity: **error** the variant's payload violates the branch type's refinement predicate ### Case::type-mismatch Category: **elimination** · Severity: **error** this case-body's inferred type does not match the type the eliminator's motive requires ### Scrut::type-mismatch Category: **elimination** · Severity: **error** the scrutinee's type must match the eliminator's expected shape. Annotate the scrutinee via `ann`, or switch to the eliminator that matches its inferred type. ### AnnTerm::type-mismatch Category: **type-mismatch** · Severity: **error** the annotated term does not match its declared type ### AppArg::type-mismatch Category: **type-mismatch** · Severity: **error** the argument does not match the function's domain ### JType::type-mismatch Category: **type-mismatch** · Severity: **error** the type parameter of `J` must match the type of its two endpoints ### OpaqueType::type-mismatch Category: **type-mismatch** · Severity: **error** the opaque lambda's declared domain does not match the expected domain ### ::unhandled-boot-inl Category: **shape** · Severity: **error** the left injection `inl x` has no inference rule — the sum type's right summand is not determined by the syntax of `x` alone. Annotate with `ann (inl x) (sum A B)` to fix the expected sum, or place it where checking already provides one. ### ::unhandled-boot-inr Category: **shape** · Severity: **error** the right injection `inr y` has no inference rule — the sum type's left summand is not determined by the syntax of `y` alone. Annotate with `ann (inr y) (sum A B)` to fix the expected sum, or place it where checking already provides one. ### ::unhandled-boot-refl Category: **shape** · Severity: **error** `refl` has no inference rule — the type of the equated endpoint is not determined by its syntax. Annotate with `ann refl (eq A x x)`, or use `refl` where the surrounding rule already supplies an expected equality type. ### ::unhandled-lam Category: **shape** · Severity: **error** this lambda has no inference rule — its domain and codomain types are not determined by its syntax. Annotate with `ann (lam …) (pi A B)` to drive checking, or place it where the surrounding rule already supplies an expected Pi type. ### ::unhandled-other Category: **shape** · Severity: **error** this intro form has no inference rule — its type is not determined by its syntax alone. Either annotate the term with `ann ` to switch the kernel into checking mode, or move it inside a context where an expected type is already known. ### ::unhandled-pair Category: **shape** · Severity: **error** this pair has no inference rule — its component types are not determined by its syntax. Annotate with `ann (pair …) (sigma A B)` to drive checking, or place it where the surrounding rule already supplies an expected Sigma type. ### ::unhandled-tt Category: **shape** · Severity: **error** the unit value `tt` has no inference rule — there is exactly one type it can inhabit. The kernel still asks for an annotation to keep inference syntax-directed. Use `ann tt unit` at the use site, or check `tt` against an expected `unit` type. #### Pretty Pretty-printing for diagnostic Errors. Exports: pathSegments : Error -> [String] pathString : Error -> String oneLine : Error -> String multiLine : Error -> String Chain walkers recurse directly up to 500 frames, then fall through to a `builtins.genericClosure` slow path that WHNF-forces the next node at each step. Pure data -> string; no effects. ## `multiLine` _multiLine: render `Error` as a multi-line block — header line, blame path, detail fields rendered with `renderValue`, and the optional hint text indented under the leaf._ ``` multiLine : Error -> String ``` ## `oneLine` _oneLine: render `Error` as a single-line diagnostic combining `pathString`, layer tag, msg, and (when present) hint text — suited for editor squigglies and terse log output._ ``` oneLine : Error -> String ``` ## `pathSegments` _pathSegments: walk an `Error` from root to leaf collecting position tags as strings; stack-safe to kernel-descent depth via the same fast/slow split as the other chain walkers._ ``` pathSegments : Error -> [String] ``` ## `pathString` _pathString: render `Error`'s root-to-leaf positions as a dotted path (e.g. `"DArgSort.PiCod.AppArg"`); useful for log lines and one-line diagnostic summaries._ ``` pathString : Error -> String ``` ## `renderValue` _renderValue: render an arbitrary detail value (term/type/payload) as a string suitable for inclusion in diagnostic output; centralises the formatting policy across `oneLine`/`multiLine`._ ``` renderValue : Any -> String ``` ### Effects #### State Mutable state effect: get/put/modify with standard handler. ## `get` _get: read the current state threaded by the state handler; impure request whose response IS the handler state._ ``` get : Computation s ``` Read the current state. Returns a Computation that, when handled, yields the current state value. ## `gets` _gets: read a projection of the current state via a user function; sugar for `bind get (s: pure (f s))`._ ``` gets : (s -> a) -> Computation a ``` Read a projection of the current state. ## `handler` _state.handler: interprets get/put/modify over a single state value; pair with `trampoline.handle` and the initial state._ Standard state handler. Interprets get/put/modify effects. Use with `trampoline.handle`: ```nix handle { handlers = state.handler; state = initialState; } comp ``` - `get`: returns current state as value - `put`: replaces state with param, returns null - `modify`: applies param (a function) to state, returns null ## `modify` _modify: apply a function to the current state in place; impure request resuming with null after the handler runs the transformer._ ``` modify : (s -> s) -> Computation null ``` Apply a function to the current state. Returns a Computation that, when handled, transforms the state via f and returns null. ## `put` _put: replace the current state with the supplied value; impure request resuming with null._ ``` put : s -> Computation null ``` Replace the current state. Returns a Computation that, when handled, sets the state to the given value and returns null. ## `update` _update: read the state, run a user computation against it to produce `{ state, value }`, put the new state, return the value._ ``` update : (s -> Computation { state, value }) -> Computation value ``` Apply a computation to the current state. Returns a Computation that, when handled, updates the state and returns value. #### Reader Read-only environment effect: ask/asks/local with standard handler. ## `ask` _ask: read the current environment threaded by the reader handler; impure request whose response IS the handler state._ ``` ask : Computation env ``` Read the current environment. ## `asks` _asks: read a projection of the environment via a user-supplied function; sugar for `bind ask (env: pure (f env))`._ ``` asks : (env -> a) -> Computation a ``` Read a projection of the environment. ## `handler` _reader.handler: interprets ask/local effects with state IS the (immutable) environment; ask resumes with state, local replaces it._ Standard reader handler. Interprets ask effects. The state IS the environment (immutable through the computation). ```nix handle { handlers = reader.handler; state = myEnv; } comp ``` ## `local` _local: run a sub-computation under a modifier-transformed environment; sends `local` so handlers can install the modified env._ ``` local : (env -> env) -> Computation a -> Computation a ``` Run a computation with a modified environment. Returns a new computation that transforms the environment before executing the inner computation. Since handlers are pure functions, local is implemented by wrapping the inner computation's ask effects with the modifier. In practice, use separate handler installation with the modified env. #### Writer Append-only output effect: tell/tellAll with list-collecting handler. ## `handler` _writer.handler: collects tell/tellAll output as a list in handler state, starting at `[]`; pair with `trampoline.handle`._ Standard writer handler. Collects tell output in state as a list. Initial state: `[]` ```nix handle { handlers = writer.handler; state = []; } comp ``` ## `tell` _tell: append a single value to the writer effect's output log; impure request resuming with null._ ``` tell : w -> Computation null ``` Append a value to the output log. ## `tellAll` _tellAll: append a list of values to the writer effect's output log in a single impure request; one bind instead of N from mapping tell._ ``` tellAll : [w] -> Computation null ``` Append a list of values to the output log. #### Acc Accumulator effect: emit/emitAll/collect for incremental list building. ## `collect` _collect: read the acc effect's currently accumulated items as a list; impure request returning the contents captured so far._ ``` collect : Computation [a] ``` Read the current accumulated items as a list. Returns whatever has been emitted within the surrounding handled scope. ## `emit` _emit: append a single item to the acc effect's accumulator; issues an impure effect request whose result is null._ ``` emit : a -> Computation null ``` Append a single item to the accumulator. Pairs with `collect` to read back all items emitted within the surrounding handled scope. ## `emitAll` _emitAll: append a list of items to the acc effect's accumulator in a single impure request; one bind instead of N from mapping emit._ ``` emitAll : [a] -> Computation null ``` Append a list of items to the accumulator in a single effect request. Equivalent to mapping `emit` over the list but cheaper at runtime. ## `handler` _acc.handler: standard accumulator handler implementing emit/emitAll/collect over a list state with `[]` as the initial value._ Standard accumulator handler. State is the list of accumulated items, starting at `[]`. ```nix handle { handlers = acc.handler; state = []; } comp ``` #### Error Error effect with contextual messages and multiple handler strategies. ## `collecting` _error.collecting: handler that accumulates every error into state as a list of `{ message, context }` and resumes computation with null._ Collecting error handler: accumulates errors in state as a list. Resumes computation with null so subsequent effects still execute. Use when you want all errors, not just the first. State shape: list of { message, context } ## `raise` _raise: send an `error` effect carrying a message and empty context; the handler decides whether to throw, collect, or recover._ ``` raise : string -> Computation a ``` Raise an error. Returns a Computation that sends an "error" effect. The handler determines what happens: throw, collect, or recover. ## `raiseWith` _raiseWith: raise an error with a context string; handlers can collect contexts to assemble stack-trace-style reports._ ``` raiseWith : string -> string -> Computation a ``` Raise an error with context. The context string describes where in the computation the error occurred, enabling stack-trace-like error reports when used with the collecting handler. ## `result` _error.result: handler that aborts with a tagged `{ _tag = "Error"; message; context; }` value; uses the non-resumption protocol._ Result error handler: aborts computation with tagged Error value. Uses the non-resumption protocol to discard the continuation. Returns `{ _tag = "Error"; message; context; }` on error. ## `strict` _error.strict: handler that throws on the first error via `builtins.throw`, prefixing context when present; halts evaluation immediately._ Strict error handler: throws on first error via builtins.throw. Use when errors should halt evaluation immediately. Includes context in the thrown message when available. #### Conditions CL-style condition system: signal/warn with restart-based recovery. ## `collectConditions` _conditions.collectConditions: handler that accumulates each condition into state as `{ name, data }` and resumes with `continue`._ Collecting handler: accumulates conditions in state, resumes with continue. State shape: list of { name, data } Initial state: [] ## `fail` _conditions.fail: last-resort handler that throws on any condition (via `builtins.throw`); ignores available restarts._ Fail handler: throws on any condition. Ignores available restarts. Use as a last-resort handler. ## `ignore` _conditions.ignore: handler that silently discards every condition by resuming with `{ restart = "continue"; value = null; }`._ Ignore handler: resumes with null for any condition. All conditions are silently discarded. ## `signal` _signal: raise a CL-style condition with name, data, and available restart names; the handler returns `{ restart, value }` to choose recovery._ ``` signal : string -> any -> [string] -> Computation any ``` Signal a condition. The handler chooses a restart strategy. **Arguments:** - `name` — condition name (e.g. `"division-by-zero"`, `"file-not-found"`) - `data` — condition data (error details, context) - `restarts` — list of available restart names The handler receives `{ name, data, restarts }` and returns a `{ restart, value }` attrset. The continuation receives this choice. ## `warn` _warn: raise a warning condition with the conventional `muffle-warning` restart; if the handler doesn't muffle, computation continues._ ``` warn : string -> any -> Computation null ``` Signal a warning condition. Like signal but with a conventional `"muffle-warning"` restart. If the handler doesn't muffle, the computation continues normally. ## `withRestart` _withRestart: handler factory invoking a named restart with a given value for one matched condition; throws on every other condition._ ``` withRestart : string -> string -> any -> handler ``` Create a handler that invokes a specific restart for a named condition. For all other conditions, falls through (throws). **Arguments:** - `condName` — condition name to match - `restartName` — restart to invoke - `restartVal` — value to pass via the restart #### Choice Non-deterministic choice effect: choose/fail/guard with list handler. ## `choose` _choose: non-deterministic selection from a list of alternatives; the handler determines exploration strategy (e.g. listAll for all branches)._ ``` choose : [a] -> Computation a ``` Non-deterministic choice from a list of alternatives. The handler determines how alternatives are explored. ## `fail` _fail: abort the current non-deterministic branch; equivalent to `choose []` with an empty-alternatives short-circuit._ ``` fail : Computation a ``` Fail the current branch of non-deterministic computation. Equivalent to `choose []`. ## `guard` _guard: continue when the predicate is true, fail the branch when false; threads boolean predicates into non-deterministic search._ ``` guard : bool -> Computation null ``` Guard a condition: continue if true, fail if false. ## `initialState` _choice.initialState: starting state `{ results = []; pending = []; }` for the listAll handler; pair with `handle` to run._ Initial state for the listAll handler. ## `listAll` _choice.listAll: handler exploring every non-deterministic branch and accumulating results into `state.results`; list-monad semantics._ Handler that explores all non-deterministic branches and returns a list of all results. Empty choices abort that branch. State is `{ results : [a], pending : [Computation a] }`. After handling, results are in `state.results`. ```nix let r = handle { handlers = choice.listAll; state = choice.initialState; } comp; in r.state.results ``` #### Scope Computation-scoped handlers via effect rotation. ## `handlersFromAttrs` _scope.handlersFromAttrs: lift an attrset of handlers, functions, or constants into named effect handlers; preserves shape and state._ ``` handlersFromAttrs : { = handler | (param -> a) | a; ... } -> handlers ``` Helper to transform an attrset into named handlers. If attrValue is a function `{ param, state }` it is used directly as handler; If attrValue is a function, resume is `f param` and preserves state; Otherwise a constant handler always resumes with attrValue, preserving state. ## `provide` _scope.provide: install stateless handlers for a sub-computation's dynamic extent (reader/val pattern); outer handler state survives unchanged._ ``` scope.provide : handlers -> Computation a -> Computation a ``` Install handlers for a computation's dynamic extent without touching state. Unhandled effects rotate outward; outer handler state mutations survive unaffected. This is the reader/val handler pattern (Koka's `val`, Haskell's `runReader`, Scheme's `parameterize`) — use it when handlers are stateless (resume with a constant, pass state through). For handlers that need their own state, use scope.run or scope.stateful instead. ## `run` _scope.run: handle named effects inside a sub-computation and hide the scope's own state from the caller; rotates unknown effects outward._ ``` scope.run : { handlers, state? } -> Computation a -> Computation a ``` Run a computation with scoped handlers. Effects matching `handlers` are handled inside the scope. Unknown effects rotate outward. The scope's internal state is hidden — caller sees only the body's value. ## `runWith` _scope.runWith: like scope.run but surfaces the scope's final `{ value, state }` to the caller; raw rotation without state hiding._ ``` scope.runWith : { handlers, state? } -> Computation a -> Computation { value, state } ``` Like scope.run but exposes the scope's final state alongside the value. ## `stateful` _scope.stateful: run a sub-computation under scoped handlers while preserving outer state around the rotation point; wraps state.update._ ``` stateful : handlers -> Computation a -> Computation a ``` Run a computation with scoped handlers while preserving state around effect rotation. ## `val` _scope.val: provide constant values as named effect handlers for a sub-computation; sugar for `provide (handlersFromAttrs bindings)`._ ``` scope.val : { = value; ... } -> Computation a -> Computation a ``` Provide constant values as named effect handlers for a computation's dynamic extent. Each key in the bindings attrset becomes an effect that resumes with the corresponding value. Built on scope.provide via handlersFromAttrs. Named after Koka's `val` effect handler. For the traditional single-environment reader (ask/asks/local), see fx.effects.reader. #### Linear Graded linear resource tracking: acquire/consume/release with usage enforcement. Each resource gets a capability token at acquire time. The graded handler covers linear (exactly once), affine (at most once via release), exact(n), and unlimited usage through a single maxUses parameter. Quick start: ```nix let comp = bind (linear.acquireLinear "secret") (token: bind (linear.consume token) (val: pure val)); in linear.run comp ``` For composition with other handlers, use handler/return/initialState with `adaptHandlers`. ## `acquire` _acquire: take a graded linear resource and obtain a capability token; `maxUses` (or null) sets the linearity bound enforced by the handler._ ``` acquire : { resource : a, maxUses : Int | null } -> Computation Token ``` Acquire a graded linear resource. Returns a capability token. The token wraps the resource with an ID for tracking. The handler maintains a resource map in its state, counting each consume call against the maxUses bound. - `maxUses = 1` — Linear: exactly one consume required - `maxUses = n` — Exact: exactly n consumes required - `maxUses = null` — Unlimited: any number of consumes allowed Tokens should be consumed exactly maxUses times, or explicitly released. At handler exit, the return clause (finalizer) checks: released → always OK, `maxUses = null` → always OK, otherwise → `currentUses` must equal `maxUses`. ## `acquireExact` _acquireExact: acquire a resource that must be consumed exactly `n` times; encodes graded linearity beyond pure linear/affine._ ``` acquireExact : a -> Int -> Computation Token ``` Acquire a resource that must be consumed exactly n times. ## `acquireLinear` _acquireLinear: acquire a strictly linear resource (maxUses = 1); the finalizer fails unless exactly one consume happens before scope exit._ ``` acquireLinear : a -> Computation Token ``` Acquire a linear resource (exactly one consume required). ## `acquireUnlimited` _acquireUnlimited: acquire a resource with `maxUses = null`; the finalizer never reports usage mismatches for unlimited tokens._ ``` acquireUnlimited : a -> Computation Token ``` Acquire an unlimited resource (any number of consumes allowed). ## `consume` _consume: spend a use of a capability token and return the wrapped resource; aborts with `LinearityError` on use-after-release or bound-exceeded._ ``` consume : Token -> Computation a ``` Consume a capability token, returning the wrapped resource value. Increments the token's usage counter. Aborts with `LinearityError` if: - Token was already released (`"consume-after-release"`) - Usage would exceed maxUses bound (`"exceeded-bound"`) The returned value is the original resource passed to acquire. ## `handler` _linear.handler: interprets linearAcquire/linearConsume/linearRelease over a `{ nextId, resources }` state; emits tagged LinearityError on misuse._ Graded linear resource handler. Interprets linearAcquire, linearConsume, and linearRelease effects. Tracks resource usage in handler state. Use with `trampoline.handle`: ```nix handle { handlers = linear.handler; return = linear.return; state = linear.initialState; } comp ``` Or use the convenience: `linear.run comp` - `linearAcquire`: creates token, adds resource entry to state - `linearConsume`: increments usage counter, returns resource value - `linearRelease`: marks resource as released (finalizer skips it) ## `initialState` _linear.initialState: `{ nextId = 0; resources = {}; }`; monotonic ID counter plus an empty resource map indexed by stringified ID._ Initial handler state for the linear resource handler. ```nix { nextId = 0; resources = {}; } ``` - `nextId`: monotonic counter for generating unique resource IDs. - `resources`: map from ID (string) to resource tracking entry. ## `release` _release: drop a capability token without consuming it; the finalizer then skips it (affine usage). Aborts on double-release._ ``` release : Token -> Computation null ``` Explicitly release a capability token without consuming it. Marks the resource as released. The finalizer skips released resources, so this allows affine usage (acquire then drop). Aborts with `LinearityError` on double-release. ## `return` _linear.return: finalizer checking every non-released, finite-bound resource was consumed exactly `maxUses` times; wraps mismatches in LinearityError._ ``` return : a -> State -> { value : a | LinearityError, state : State } ``` Finalizer return clause for the linear handler. Checks each resource in handler state: - `released` → OK (explicitly dropped) - `maxUses = null` → OK (unlimited) - otherwise → `currentUses` must equal `maxUses` On violation, wraps the original value in a `LinearityError` with details of each mismatched resource. On success, passes through unchanged. Runs on both normal return and abort paths. ## `run` _linear.run: convenience wrapper bundling handler, return, and initialState; runs a computation under the graded linear discipline._ ``` run : Computation a -> { value : a | LinearityError, state : State } ``` Run a computation with the graded linear handler. Bundles handler, return clause, and initial state into one call. To compose with other handlers, use handler/return/initialState separately with `adaptHandlers`. ```nix let comp = bind (acquireLinear "secret") (token: bind (consume token) (val: pure "got:${val}")); in linear.run comp # => { value = "got:secret"; state = { nextId = 1; resources = { ... }; }; } ``` #### Typecheck Reusable typeCheck handlers. Use `policy.` for the canonical surface; legacy `strict`/`collecting`/`logging` attributes are preserved for back-compat. ## `collecting` _policy.collecting: accumulates every failing typeCheck into state as a list of `{ context, typeName, actual, message, path, reason }`._ Collecting typeCheck handler: accumulates errors in state. Resumes with `true` on success, `false` on failure (computation continues). State shape: list of `{ context, typeName, actual, message, path, reason }` Initial state: `[]` ## `firstN` _policy.firstN N: bounded-collection handler keeping up to N failures, then setting `aborted = true` and dropping the rest. State stays bounded._ ``` firstN : Int -> Handler ``` Bounded-collection handler: collects up to N failures, then drops the rest. Useful for early-termination policies where the consumer only needs a handful of representative errors. After the Nth failure, `aborted` flips true and subsequent failures are silently dropped (state size stays bounded at N entries). State shape: `{ collected :: [errorRecord]; aborted :: Bool; }` Initial state: `{ collected = []; aborted = false; }` ## `logging` _policy.logging: records every typeCheck (pass and fail) in state with `passed` boolean; useful for tracing without aborting computation._ Logging typeCheck handler: records every check (pass or fail) in state. Always resumes with the actual check result (boolean). State shape: list of `{ context, typeName, passed, path, reason }` Initial state: `[]` ## `policy` _policy: canonical grouped surface for typecheck handlers._ Grouped `strict`/`collecting`/`logging`/`firstN`/`summarize`/`pretty` handlers. ## `pretty` _policy.pretty cfg: emits one pre-formatted `[reason] expected T at , got ` line per failure; renders the Position-list path with per-segment separators._ ``` pretty : { sourceMap? } -> Handler ``` Display-rendering handler: emits one pre-formatted line per failure. Renders the blame location by concatenating each Position's `segment` field (`.field`, `[i]`, `#Tag` carry their own separator). Falls back to `context` when the path is empty. Each line is shaped: [reason] expected typeName at , got `cfg.sourceMap` is reserved for future source-span annotation; not consumed yet. State shape: `[String]` (one entry per failure; consumer joins with newline for display). Initial state: `[]` ## `strict` _policy.strict: throws on the first failing typeCheck via `builtins.throw`; resumes with true on success. Halts evaluation on error._ Strict typeCheck handler: throws on first type error. Resumes with true on success (check passed). Use when type errors should halt evaluation immediately. State: unused (pass null). ## `summarize` _policy.summarize: bounded-memory grouping by `reason`; tracks counts per reason plus pass/fail totals. State is O(K) in distinct reasons, not O(N)._ Bounded-memory grouping handler: counts failures by `reason`, drops per-failure data (value/path/context/message) so memory stays O(K) in the number of distinct reasons rather than O(N) in the number of effects. Use for high-volume validation where individual error records would blow up state — only the aggregate matters. State shape: `{ byReason :: { = Int; ... }; passed :: Int; failed :: Int; }` Initial state: `{ byReason = {}; passed = 0; failed = 0; }` #### HasHandler Check if a handler with given name exists in current scope. ### Types #### Foundation Type system foundation: Type constructor, check, validate, make, refine. ## `bind` _bind: re-export of `fx.kernel.bind` for dependent contract modules._ Re-export of `fx.kernel.bind`. ## `check` _check: predicate that asks whether `value` inhabits `type`; returns the type's guarded kernel decision as a Bool, never throws._ ``` check : Type -> Value -> Bool ``` Check whether a value inhabits a type. Pure — returns a Bool. The dual of `make`, which throws on failure. ## `defEq` _defEq: definitional equality on types; true iff the kernel-conversion judgment `Γ ⊢ A._kernel ≡ B._kernel` holds under β/η/ι/μ reduction._ ``` defEq : Type -> Type -> Bool ``` Definitional equality on types. This is the type-theoretic equality that decides when two type expressions denote the same type: conv 0 (eval [] (elab A._kernel)) (eval [] (elab B._kernel)) Strictly stronger than meta-language `==` on `_kernel`: Nix `==` only coincides with conv when the encoding contains no closures. After the description-backed migration of Record/Variant/Certified, `(H.datatype …).T` carries Pi-binder closures and per-call fresh thunks; `==` on those kernels is no longer a sound proxy for type equality. `defEq` is the correct predicate. Grounded in Martin-Löf (1984), section 6, and standard NbE conversion (Abel et al. 2007). ## `make` _make: assert-and-return; runs `type.check` on the value, returning it on success or throwing a `nix-effects type error` on failure._ ``` make : Type -> Value -> Value ``` Validate a value and return it, or throw on failure. The throwing dual of `check`. ## `mkType` _mkType: foundation type constructor; builds a `nix-effects` type from a kernel HOAS representation plus optional guard/verify/universe/approximate flags._ ``` mkType : { name, kernelType ? null, guard ? null, verify ? null, description ? name, universe ? null, approximate ? false } -> Type ``` Create a type from its kernel representation. A nix-effects type is defined by its `kernelType` — an HOAS type tree representing the type in the MLTT kernel. All fields are derived: - `.check` = `decide(kernelType, v)` — the decision procedure - `.universe` = `checkTypeLevel(kernelType)` — computed universe level - `.kernelCheck` = same as `.check` - `.prove` = kernel proof checking for HOAS terms Arguments: - `name` — Human-readable type name - `kernelType` — HOAS type tree (required — this IS the type) - `guard` — Optional runtime predicate for refinement types. When present, `.check = kernelDecide(v) && guard(v)` (conjunction — kernel catches structural errors, guard handles residual constraints). The guard handles constraints the kernel can't express (e.g., x >= 0). - `verify` — Optional custom verifier (`self → fuel → path → value → Computation`). `fuel` is the native-recursion budget: recursive verifiers descend natively while it is positive and defer the sub-walk via a `deriveBounce` effect when it runs out, keeping deep structures stack-safe. Non-recursive verifiers ignore it. `path` is a list of `fx.diag.positions` Position records describing the structural descent from the validation root (e.g. `[(P.Field "a") (P.Field "b")]` for a nested field, `[(P.Elem 0) (P.Field "mtu")]` for a list element's field). When null (default), `validate` is auto-derived by wrapping `check` in a `typeCheck` effect. Supply a custom `verify` for types that decompose checking (e.g. Record sends separate effects per field for blame tracking). - `description` — Documentation string (default = `name`) - `universe` — Optional universe level. When null (default), the level is computed from `checkTypeLevel(kernelType)`. The computed level is total: `.universe` throws — rather than fabricating a level — when the kernel type is not a type, depends on a term, or is level-polymorphic. A supplied `universe` is enforced against the kernel minimum: it may over-approximate it (e.g. for a fallback `kernelType`) but under-approximation is inconsistent and throws, and it cannot pin a level on a type that has none. - `approximate` — When true, the kernelType is a sound but lossy approximation (e.g., `H.function_` for Pi, `H.any` for Sigma). Suppresses `_kernel`, `kernelCheck`, and `prove` on the result, since the kernel representation doesn't precisely capture this type. The kernelType is still used internally for universe computation. ## `pure` _pure: re-export of `fx.kernel.pure` for dependent contract modules._ Re-export of `fx.kernel.pure`. ## `refine` _refine: narrow a base type with an extra predicate; returns a refined `Type` whose `check` conjoins kernel decision with the supplied guard._ ``` refine : Type -> (Value -> Bool) -> Type ``` Narrow a type with an additional predicate. Creates a refinement type whose check = kernelDecide(v) ∧ guard(v) (conjunction). The base type's kernel provides structural checking; the guard handles the refinement predicate the kernel cannot express. Grounded in Freeman & Pfenning (1991) "Refinement Types for ML" and Rondon et al. (2008) "Liquid Types". ## `refineGuard` _refineGuard: build a refinement type's guard slot from a base type and a predicate; a KernelPred witness yields a kernel-derived (authoritative) guard composed with the base's witness, a raw predicate stays an opaque lambda conjoined with `base.check`._ ``` refineGuard : Type -> (Value -> Bool | KernelPred) -> (Value -> Bool | KernelPred) ``` Shared by `refine` and `refinement.refined` so both compose guards identically. A `KernelPred` predicate is threaded through (and `andKP`-composed with `base._kernelPred`); a raw predicate becomes `v: base.check v && predicate v`. ## `send` _send: re-export of `fx.kernel.send` for dependent contract modules._ Re-export of `fx.kernel.send`. ## `validate` _validate: emit a standalone `typeCheck` effect with an explicit `context` string for ad-hoc validation; prefer `type.validate` unless overriding context._ ``` validate : Type -> Value -> String -> Computation Bool ``` Standalone effectful validation with explicit context string. Sends a `typeCheck` effect with the given type, value, and context. The handler receives `{ type, context, value }` and determines the response: throw, collect error, log, or offer restarts. For typical use, prefer `type.validate` (auto-derived by `mkType`, uses the type's name as context). This 3-arg form is for cases where a custom context string is needed. #### Primitives Primitive types: String, Int, Bool, Float, Attrs, Path, Derivation, Function, Null, Unit, Any. ## `Any` _Any: top type; every Nix value inhabits it, backed by `H.any` — used as the lossy fallback kernel for approximate types._ Top type. Every value inhabits `Any`. Approximate types fall back to `Any` for their kernel slot. ## `Attrs` _Attrs: primitive type for any Nix attribute set; backed by `H.attrs` — accepts every attrset including `{}`, never checks field shape._ Attribute set type. Any attrset, including `{}`. Use `Record` for declared-field shape. ## `Bool` _Bool: primitive type whose only inhabitants are `true` and `false`; backed by the `H.bool` kernel type with precise kernel decision._ Boolean type. Inhabited by `true` and `false`. ## `Derivation` _Derivation: primitive type for Nix derivation values — attrsets carrying `type = "derivation"`. The store-producing irreducible value category that makes Nix Nix; rejects plain attrsets, strings, and paths._ Derivation type. Nix derivation values (built via `mkDerivation` / `runCommand` / `stdenv.mkDerivation` etc.). Membership is decided structurally: any attrset with `type = "derivation"` qualifies. Bare attrsets (no `type` field), strings (e.g. `"pkgs.hello"`), and paths (e.g. `./foo`) are rejected — derivations are a distinct value category. ## `Float` _Float: primitive type whose values are Nix floating-point numbers; backed by `H.float_` and decided by `builtins.isFloat` (excludes ints)._ Float type. Inhabited by Nix floats; rejects integers. ## `Function` _Function: primitive type for Nix lambdas; backed by `H.function_` and decided by `builtins.isFunction` — argument/result shape is not introspected._ Function type. Any Nix lambda. The arrow shape (`a -> b`) is not checked at this level — use `H.forall` for that. ## `Int` _Int: primitive type whose values are Nix integers; backed by the `H.int_` kernel type and decided by `builtins.isInt` (excludes floats)._ Integer type. Inhabited by Nix integer values; rejects floats. ## `Null` _Null: primitive type whose only inhabitant is `null`; isomorphic to `Unit` and backed by the `H.unit` kernel type._ Null type. Only `null` inhabits it. Isomorphic to `Unit`. ## `Path` _Path: primitive type for Nix path values; rejects strings — paths and strings are distinct value categories in Nix._ Path type. Nix path values (e.g. `./foo`); not interchangeable with strings. ## `String` _String: primitive type whose values are Nix strings; backed by the `H.string` kernel type and decided by `builtins.isString` at the elaborator._ String type. Inhabited by Nix string values. ## `Unit` _Unit: primitive type with one inhabitant `null`; isomorphic to `Null`, backed by the `H.unit` kernel type — the trivial / terminal type._ Unit type. Trivial type with one inhabitant. Isomorphic to `Null`. #### Constructors Type constructors: Record, RecordOpen, ListOf, Maybe, Either, Variant. ## `Either` _Either: tagged sum type constructor; `Either L R` accepts `{ _tag = "Left"; value : L }` or `{ _tag = "Right"; value : R }`._ ``` Either : Type -> Type -> Type ``` Tagged union of two types. Accepts `{ _tag = "Left"; value = a; }` or `{ _tag = "Right"; value = b; }`. ## `ListOf` _ListOf: homogeneous list type constructor; `ListOf T` checks every element has type `T`, blames per-index, never short-circuits — handler picks error policy._ ``` ListOf : Type -> Type ``` Homogeneous list type. `ListOf Type` checks that all elements have the given type. Custom verifier sends per-element `typeCheck` effects with indexed context strings (e.g. `List[Int][2]`) for blame tracking. Unlike Sigma, elements are independent — no short-circuit. All elements are checked; the handler decides error policy (strict aborts on first, collecting gathers all). ## `Maybe` _Maybe: option type constructor; `Maybe T` accepts null or any value of type `T`; kernel precision and sufficiency inherit from the inner type._ ``` Maybe : Type -> Type ``` Option type. Maybe Type accepts null or a value of Type. ## `Record` _Record: closed record type constructor; `Record { f = T; ... }` checks that values carry exactly the declared fields with matching types and rejects extras._ ``` Record : { = Type; ... } -> Type ``` Closed record type constructor. Takes a schema `{ field = Type; ... }` and checks that a value has exactly the declared fields with correct types. Unknown fields are rejected — for open semantics use `RecordOpen`. Verify is per-field and emits one `typeCheck` effect per blamed field, threading a `Field name` Position so handlers can recover the structural path. ## `RecordOpen` _RecordOpen: open-record type constructor; like `Record` but undeclared fields are accepted untouched, useful for records carrying optional metadata slots._ ``` RecordOpen : { = Type; ... } -> Type ``` Open record type constructor. Like `Record`, but undeclared fields are permitted (and ignored by kernel and verify). Use for types whose values carry intentional metadata slots beyond the declared schema — e.g. build steps with optional `tools` / `env` / `when` annotations. The kernel datatype tags `openExtras = true` in `_dtypeMeta` so downstream type-directed walks know to allow them. ## `Variant` _Variant: discriminated-union type constructor; `Variant { tag = T; ... }` accepts `{ _tag = name; value }` checked against the named branch._ ``` Variant : { = Type; ... } -> Type ``` Discriminated union. Takes `{ tag = Type; ... }` schema. Accepts `{ _tag = "tag"; value = ...; }` where value has the corresponding type. #### Refinement Refinement types and predicate combinators. Grounded in Freeman & Pfenning (1991) and Rondon et al. (2008). ## `allOf` _allOf: conjoin a list of predicates. A non-empty list of all-KernelPred members folds into one KernelPred (the conjoined refinement internalizes); any raw lambda, or an empty list, yields a plain conjoined guard that holds when every member holds (empty = constant `true`)._ ``` allOf : [(KernelPred | (Value -> Bool))] -> (KernelPred | (Value -> Bool)) ``` Combine predicates with conjunction: `(allOf [p1 p2]) v = p1 v && p2 v`. All-KernelPred input folds to a KernelPred so the refinement internalizes; a raw-lambda member demotes to a plain guard. Empty list returns constant `true`. ## `anyOf` _anyOf: disjoin a list of predicates into one that holds when any member holds; the empty list yields a constant `false`._ ``` anyOf : [(Value -> Bool)] -> Value -> Bool ``` Combine predicates with disjunction: `(anyOf [p1 p2]) v = p1 v || p2 v`. Empty list returns `false`. ## `eqInt` _eqInt: kernel-internalizing factory predicate `x == k` over Int; internalizes into the kernel `ktype`._ ``` eqInt : Int -> KernelPred ``` KernelPred witness factory over the signed-int carrier deciding `x == k`. ## `inRange` _inRange: factory predicate asserting that a numeric value lies within `[lo, hi]`; both endpoints are inclusive._ ``` inRange : Number -> Number -> Number -> Bool ``` Predicate factory: `(inRange lo hi) v = lo <= v <= hi`. Both endpoints inclusive. ## `inRangeInt` _inRangeInt: kernel-internalizing factory predicate `lo <= x <= hi` over Int; both endpoints inclusive, internalizes into the kernel `ktype`._ ``` inRangeInt : Int -> Int -> KernelPred ``` KernelPred witness factory over the signed-int carrier deciding `lo <= x <= hi`. ## `matching` _matching: factory predicate that holds when a value is a string fully matched by the supplied regex pattern; non-strings are rejected._ ``` matching : String -> String -> Bool ``` Predicate factory: `(matching pattern) s = s matches regex pattern`. Full-match semantics — anchor not needed. ## `negate` _negate: flip a predicate's polarity; `negate p` accepts exactly the values `p` rejects, and vice versa._ ``` negate : (Value -> Bool) -> Value -> Bool ``` Negate a predicate: `(negate p) v = !(p v)`. ## `nonEmpty` _nonEmpty: predicate asserting that a string or list has at least one element/character; values of other types are rejected._ ``` nonEmpty : (String | List) -> Bool ``` Predicate: string or list is non-empty. Rejects non-string/non-list inputs. ## `nonEmptyStr` _nonEmptyStr: kernel-internalizing refinement predicate deciding String non-emptiness via the host-backed `strLen` (`1 <= length x`). As the predicate of `refined`/`refine` it internalizes into the kernel `ktype`, unlike the raw `nonEmpty` which also covers the list carrier._ ``` nonEmptyStr : KernelPred ``` KernelPred witness over the string carrier deciding `length x >= 1` through `strLen`. Use in place of `nonEmpty` on String to internalize the refinement (non-null `.ktype`). ## `nonNegative` _nonNegative: predicate asserting that a numeric value is greater than or equal to zero; accepts zero, rejects negatives._ ``` nonNegative : Number -> Bool ``` Predicate: `value >= 0`. Zero accepted. ## `nonNegativeInt` _nonNegativeInt: kernel-internalizing refinement predicate `x >= 0` over Int; internalizes into the kernel `ktype` when used with `refined`/`refine`._ ``` nonNegativeInt : KernelPred ``` KernelPred witness over the signed-int carrier deciding `x >= 0`. ## `oneOfStr` _oneOfStr: kernel-internalizing factory predicate deciding membership in a fixed String literal set, via the kernel's decidable `strEq`; a singleton list is equality-against-literal. As the predicate of `refined`/`refine` it internalizes into the kernel `ktype`. Decides by literal equality — substring/match stay outside the kernel._ ``` oneOfStr : [String] -> KernelPred ``` KernelPred witness factory over the string carrier deciding `x ∈ {lits…}` as a strEq disjunction. Unlike `matching` (a raw lambda needing string introspection the kernel lacks), this internalizes. ## `positive` _positive: predicate asserting that a numeric value is strictly greater than zero; rejects zero, negatives, and non-numerics by extension._ ``` positive : Number -> Bool ``` Predicate: `value > 0`. Strict — zero is rejected. ## `positiveInt` _positiveInt: kernel-internalizing refinement predicate `x > 0` over Int; as the predicate of `refined`/`refine` it yields a type whose check is kernel-decided and whose `.ktype` is non-null._ ``` positiveInt : KernelPred ``` KernelPred witness over the signed-int carrier deciding `x > 0`. Unlike `positive` (a raw lambda), this internalizes into the kernel `ktype`. ## `refined` _refined: build a named refinement type narrowing `base` with an extra predicate; the resulting type's `check` conjoins kernel decision with the guard._ ``` refined : String -> Type -> (Value -> Bool) -> Type ``` Create a named refinement type. The supplied predicate runs in addition to the base type's check — kernel handles structural validation, the predicate handles residual constraints. #### Dependent Dependent contracts: Pi (Π), Sigma (Σ), Certified, Vector, DepRecord. Grounded in Martin-Löf (1984) "Intuitionistic Type Theory". ## `Certified` Subset type `Σ(x:A).P(x)` with `P(x)` a mere proposition. The witness is an inhabitant of `P(x)`, not the host Bool. Two formers, one type: - decidable `predicate` (a `reflect` KernelPred) → `P x = KT.P(decide t x)` (`Unit`/`Void` ≡ `KT.El t`); witness is the Unit inhabitant (`null`), synthesized by `certify`. `_kernelSufficient = true`; `_kernel = El t`. - general `{ family; bridge; }` (`family : A → U`, or a `mkPropFamily` bundle passed as `family`) → `P x = squash(family x)`, proof irrelevance definitional; witness is `squashIntro` of a checked HOAS proof supplied to `certifyProof v p`. `_kernelSufficient = false`. A predicate that is neither (a raw host lambda) yields no proof, so it is not a `Certified` — construction throws. For an un-proven runtime guard use `fx.types.refinement.refined`. Construction: - `.certify v` — decidable: pure, fail-closed, synthesizes the witness. - `.certifyProof v p` — general: checks `p : family(v)`, then truncates. - `.certifyE v` — decidable effectful dual (sends `typeCheck` on failure). - `.certifyProofE v p` — general effectful dual (sends `typeCheck` on failure). - `.check` / `.validate` — inherited from Sigma (pair check / effectful intro). - `.prove term` — checks a term against the genuine subset `_kernel`. Membership decides component-wise: `.check`/`.validate` ride a structural `Σ x:A. Unit` kernelType (host-decidable without normalizing), with the predicate decided at the concrete fst; `_kernel` exposes the real `El t`. ## `DepRecord` Dependent record type built on nested Sigma. Schema is an ordered list of `{ name; type; }` where `type` can be: - A Type (static field) - A function (`partial-record → Type`) for dependent fields Isomorphic to nested Sigma types: ``` { a : A, b : B(a) } ≅ Σ(a:A).B(a) { a : A, b : B(a), c : C(a,b) } ≅ Σ(a:A).Σ(b:B(a)).C(a,b) ``` Values are nested Sigma pairs: ```nix { fst = a; snd = { fst = b; snd = c; }; } ``` Inherits from Sigma: `.validate` (effectful), `.proj1`, `.proj2`, `.pair`, `.pairE`, `.curry`, `.uncurry`. Use `.pack` to convert flat attrset → nested Sigma value. Use `.unpack` to convert nested Sigma value → flat attrset. ## `Pi` Dependent function type `Π(x:A).B(x)`. Arguments: - `domain` — Type A - `codomain` — A-value → Type (type family B indexed by domain values) - `universe` — Universe level (explicit parameter — see below) - `name` — optional display name == Higher-order contract with algebraic effects == Pi is a HIGHER-ORDER CONTRACT (Findler & Felleisen 2002). Higher-order contracts check function values differently from data values: a data contract is verified immediately and completely, but a function contract is verified incrementally at each application site. This is the standard, correct strategy for function contracts — not a deficit. The (Specification, Guard, Verifier) triple for Pi: ``` Guard (check): builtins.isFunction — the immediate first-order part of the contract. Soundly rejects non-functions. Verifier (validate): effectful guard (auto-derived, 1 arg) — wraps the guard in a typeCheck effect for blame tracking. Elimination (checkAt): deferred contract check (2 args) — verifies a specific application f(arg) by sending typeCheck effects for both domain (arg : A) and codomain (f(arg) : B(arg)). ``` This is precisely the Findler-Felleisen decomposition: the immediate part (`isFunction`) is checked at introduction; the deferred part (domain + codomain) is checked at each elimination site via `checkAt`. == Adequacy == ``` check f ⟺ all typeCheck effects in (validate f) pass ``` Both `check` and `validate` verify the introduction form (is it a function?). `checkAt` verifies individual applications — the deferred contract. == Universe level == Universe level is an explicit parameter. In MLTT, the level is computed as `max(i, sup_{a:A} level(B(a)))` by inspecting the syntax of B. For types with explicit kernelType, the kernel computes and verifies levels via checkTypeLevel. The explicit universe parameter provides the level for the surface API's `.universe` field. == MLTT rule mapping == ``` Formation: Pi { domain, codomain, universe } Introduction check: .check (guard: isFunction) Introduction verify: .validate (effectful guard, auto-derived) Elimination: .apply (pure), .checkAt (effectful, deferred contract) Computation: β-reduction (Nix evaluation) ``` Operations: - `.checkAt f arg` — deferred contract check at elimination site - `.apply arg` — pure elimination: compute codomain type B(arg) - `.compose f other` — compose Pi types (requires witness function) - `.domain` — the domain type A - `.codomain` — the type family B ## `Sigma` Dependent pair type `Σ(x:A).B(x)`. Arguments: - `fst` — Type A (type of the first component) - `snd` — A-value → Type (type family for the second component) - `universe` — Universe level (explicit parameter) - `name` — optional display name - `kernelType` — optional explicit HOAS kernel form (see below) Values are `{ fst; snd; }` where `fst : A` and `snd : B(fst)`. == Kernel form: explicit vs. approximate == `snd : A-value → Type` lives at the Nix-meta level — it operates on Nix values. The kernel form `H.sigma name fst._kernel (a: ...)` needs a closure operating on **HOAS variables**, not Nix values. The two categories disagree: ``` snd : NixVal → Type (surface, Nix-meta) kernel snd : HoasVar → HoasType (kernel, HOAS-level) ``` For genuinely-dependent `snd` (e.g., `x: if x > 0 then Int else String`), `snd` cannot be applied to an HOAS variable — the test on the variable would crash. So the library does not attempt automatic derivation; omitting `kernelType` produces `_kernel = H.any` with `approximate = true`. Downstream consumers that take Sigma through `elaborateType` recover the structure at the surface→kernel boundary; consumers that use `_kernel` directly (kernel walkers, the generic `deriveCheck` dispatcher) see `H.any`. Pass `kernelType` explicitly when you need the kernel form to **be** a Sigma — for example when piping the Type into another datatype constructor whose kernel walker dispatches on `_htag == "sigma"`: ```nix Prod = Sigma { fst = Int; snd = _: String; universe = 0; kernelType = H.sigma "x" Int._kernel (_: String._kernel); }; ``` For the non-dependent case (snd ignores its argument) the explicit form is mechanical and could in principle be derived; the library treats both cases uniformly to keep the dependent/non-dependent distinction out of the surface API. == First-order contract — guard is exact == Sigma is a FIRST-ORDER CONTRACT: both components are concrete data, so the contract is checked immediately and completely. The guard (`check`) IS full membership — there is no over-approximation. ``` Guard (check): fst:A ∧ snd:B(fst) — exact. G = ⟦Σ(x:A).B(x)⟧. Verifier (verify): decomposed effectful check — sends separate typeCheck effects for fst and snd for blame tracking. ``` This contrasts with Pi where the guard over-approximates (`isFunction`) because functions are higher-order. Sigma pairs are data — the dependent relationship (snd's type depends on fst's value) can be fully verified because both values are available. Adequacy: ``` T.check v ⟺ all typeCheck effects in T.validate v pass ``` Under the all-pass handler. The guard is exact and the decomposed verifier sends individual `typeCheck` effects per component — the all-pass handler's boolean state tracks whether all passed. Totality: if the input is structurally malformed (not an attrset, missing `fst`/`snd`), verify falls back to a single `typeCheck` for the whole type — failure goes through the effect system, never crashes Nix. Universe level is an explicit parameter (computing `sup_{a:A} snd(a).universe` requires evaluating the type family on all domain values, same as Pi). == MLTT rule mapping == ``` Formation: Sigma { fst, snd, universe } Introduction: .check (exact guard), .validate (effectful, decomposed) Elimination: .proj1 (π₁), .proj2 (π₂) Computation: π₁(a,b) ≡ a, π₂(a,b) ≡ b ``` Operations: - `.proj1 pair` — first projection π₁ - `.proj2 pair` — second projection π₂ - `.pair a b` — smart constructor (throws on invalid) - `.validate v` — effectful: decomposed typeCheck effects for blame - `.pairE a b` — effectful smart constructor - `.pullback f g` — contravariant predicate pullback (see below) - `.curry` / `.uncurry` — standard Sigma adjunction - `.fstType` — the type A - `.sndFamily` — the type family B ## `Vector` Length-indexed list type family, built on Pi. ``` Vector(A) = Π(n:Nat).{xs : List(A) | |xs| = n} ``` This is the correct Martin-Löf encoding: Vector IS a Pi type. It inherits `.validate` (effectful), `.compose`, `.apply`, `.domain`, `.codomain` from Pi. Usage: ```nix Vector elemType # the Pi type family (Nat → SizedList) (Vector elemType).apply 3 # specific type for length 3 ``` ## `mkPropFamily` Bundle a propositional `family` (`A → U`) with its `bridge` into one handle for `Certified`'s general former. Pass it via the `family` argument: `Certified { base; family = mkPropFamily { family; bridge; }; }`. The two-argument `{ family; bridge; }` form remains valid. #### Linear Linear type constructors: structural guards for capability tokens. Pure type predicates that check token structure without consuming. Usage enforcement is in effects/linear.nix (separate concerns). Linear(T) — exactly one consume required Affine(T) — at most one consume (release allowed) Graded(n, T) — exactly n consumes (generalizes Linear/Affine) See Orchard et al. (2019) for graded modal types. ## `Affine` _Affine: structural type constructor for capability tokens that may be consumed at most once; identical shape to `Linear`, release is permitted._ ``` Affine : Type -> Type ``` Affine type: capability token that may be consumed at most once. Structurally identical to `Linear(T)`. The name communicates that the resource may be explicitly released (dropped) via `effects/linear.release` without consuming it — "at most once" vs Linear's "exactly once." The structural guard is the same: both check for a valid capability token with inner type T. The usage distinction (exactly-once vs at-most-once) is enforced by the effect handler, not the type system. Operations: - `.check v` — pure guard: is v a valid affine token wrapping T? - `.validate v` — effectful: sends `typeCheck` for blame tracking - `.innerType` — the wrapped type T ## `Graded` _Graded: structural type constructor for capability tokens with declared usage multiplicity; generalises Linear/Affine via the `maxUses` parameter._ ``` Graded : { maxUses : Int | null, innerType : Type } -> Type ``` Graded type: capability token with usage multiplicity annotation. Generalizes Linear and Affine via a `maxUses` parameter: ```nix Graded { maxUses = 1; innerType = T; } # ≡ Linear(T) Graded { maxUses = null; innerType = T; } # ≡ Unlimited(T) Graded { maxUses = n; innerType = T; } # ≡ Exact(n, T) ``` The structural guard is the same as Linear and Affine — token structure with inner type check. The `maxUses` appears in the type name for documentation but is NOT checked by the guard (the grade lives in handler state, not the token). The name uses ω for null (unlimited): `Graded(1, Int)`, `Graded(5, String)`, `Graded(ω, Bool)` From Orchard et al. (2019) "Quantitative Program Reasoning with Graded Modal Types" — semiring-indexed usage annotations where + models branching, × models sequencing, 1 = linear, ω = unlimited. Operations: - `.check v` — pure guard: is v a valid graded token wrapping T? - `.validate v` — effectful: sends `typeCheck` for blame tracking - `.innerType` — the wrapped type T - `.maxUses` — the declared usage multiplicity ## `Linear` _Linear: structural type constructor for capability tokens that must be consumed exactly once; checks token shape, leaves usage tracking to the linear effect handler._ ``` Linear : Type -> Type ``` Linear type: capability token that must be consumed exactly once. Creates a type whose `check` verifies the capability token structure: ```nix { _linear = true, id = Int, resource = innerType } ``` Pure structural guard — checking does not consume the token. `effects/linear.nix` tracks consumption separately. Adequacy invariant: ``` Linear(T).check v ⟺ all typeCheck effects in Linear(T).validate v pass ``` Holds by construction via `mkType`'s auto-derived `validate`. Operations: - `.check v` — pure guard: is v a valid linear token wrapping T? - `.validate v` — effectful: sends `typeCheck` for blame tracking - `.innerType` — the wrapped type T #### Universe Universe hierarchy: Type_0 : Type_1 : Type_2 : ... Lazy infinite non-cumulative tower. ## `Type_0` _Type_0: first universe in the non-cumulative tower._ Predefined `Type_0` universe. ## `Type_1` _Type_1: second universe in the non-cumulative tower._ Predefined `Type_1` universe. ## `Type_2` _Type_2: third universe in the non-cumulative tower._ Predefined `Type_2` universe. ## `Type_3` _Type_3: fourth universe in the non-cumulative tower._ Predefined `Type_3` universe. ## `Type_4` _Type_4: fifth universe in the non-cumulative tower._ Predefined `Type_4` universe. ## `level` _level: read a type's universe level as an `Int`; level 0 covers atomic types, level 1 contains `Type_0`, and so on up the stratified tower. Throws (via `.universe`) when the type's level is term-dependent or level-polymorphic._ ``` level : Type -> Int ``` Get the universe level of a type. Equivalent to `.universe` field access; provided for explicit calls. Like `.universe`, it throws rather than fabricating a level when the type's universe is term-dependent or level-polymorphic (no ground `suc^n zero`). ## `lift` _lift: raise a type by one universe — `lift t = liftTo (t.universe + 1) t`, preserving its values._ ``` lift : Type -> Type ``` Raise a type by one universe level. `lift t = liftTo (t.universe + 1) t`. See `liftTo`. ## `liftTo` _liftTo: explicit cross-level coercion — `liftTo m t` reindexes type `t` to universe `m` (require `m >= t.universe`), preserving its values; idempotent at `m == t.universe`, throws when `m` is below `t`'s level._ ``` liftTo : Int -> Type -> Type ``` Reindex a type to a higher universe. `liftTo m t` has universe `m` and accepts exactly the values `t` accepts (`check` is preserved); its `_kernel` is the kernel `LiftAt` of `t`'s kernel type. Requires `m >= t.universe`; idempotent at `m == t.universe`. The non-cumulative tower has no implicit subsumption, so this is how a lower-level type becomes a member of a higher universe. ## `typeAt` _typeAt: factory producing the non-cumulative universe type `Type_n`; values of `Type_n` are types of universe exactly n; `Type_n` itself has universe `n+1`._ ``` typeAt : Int -> Type ``` Create universe type at level n (non-cumulative). `Type_n` contains exactly the types with universe `n` — a lower-level type is not subsumed, use `lift`/`liftTo` for the explicit coercion. `Type_n` itself has universe `n + 1`, enforcing `Type_n : Type_(n+1)` for all n and avoiding Russell's paradox. ### Streams #### Core Stream primitives: done/more/fromList/iterate/range/replicate. ## `done` _done: terminate a stream with a final value; produces a pure `Step` tagged `Done` carrying that value as the stream's result._ ``` done : a -> Computation (Step a b) ``` Terminate a stream with a final value. Returns a pure Computation whose `_tag` is `"Done"` and whose `value` is the supplied result. ## `fromList` _fromList: convert a Nix list into a stream; emits each element as `More` and terminates with `Done null`._ ``` fromList : [a] -> Computation (Step null a) ``` Create a stream from a list. The empty list collapses to `done null`. ## `iterate` _iterate: build an infinite stream by repeated application: `[x, f x, f (f x), ...]`; must be paired with a limiting combinator to terminate._ ``` iterate : (a -> a) -> a -> Computation (Step r a) ``` Create an infinite stream by repeated application: ``` iterate f x = [x, f(x), f(f(x)), ...] ``` Must be consumed with a limiting combinator (`take`, `takeWhile`). ## `more` _more: yield one head element followed by a continuation stream; produces a pure `Step` tagged `More` with `head` and `tail` fields._ ``` more : a -> Computation (Step r a) -> Computation (Step r a) ``` Yield an element and a continuation stream. The `tail` argument is a Computation, so the rest of the stream stays lazy. ## `range` _range: build a stream of integers from `start` inclusive to `end` exclusive; empty when `start >= end`._ ``` range : Int -> Int -> Computation (Step null Int) ``` Create a stream of integers from `start` (inclusive) to `end` (exclusive). Empty when `start >= end`. ## `replicate` _replicate: build a stream of `n` copies of value `x`; empty when `n <= 0`, otherwise emits `n` `More` steps then `Done null`._ ``` replicate : Int -> a -> Computation (Step null a) ``` Create a stream of `n` copies of a value. Empty when `n <= 0`. #### Transform Stream transformations: map, filter, scanl, flatMap. ## `filter` _sfilter (exported as `filter`): keep only elements that satisfy the predicate; failing elements are dropped silently with no blame._ ``` sfilter : (a -> Bool) -> Computation (Step r a) -> Computation (Step r a) ``` Keep only elements satisfying a predicate. Exposed as `filter` at the module's top-level. ## `flatMap` _flatMap: apply `f` returning a stream to each element and flatten via `concat`; expands one input element into zero or more outputs._ ``` flatMap : (a -> Computation (Step r b)) -> Computation (Step r a) -> Computation (Step r b) ``` Apply a function that returns a stream to each element, then flatten the resulting streams with `concat`. ## `map` _smap (exported as `map`): map a function over each element of a stream; the structure of `More`/`Done` steps is preserved._ ``` smap : (a -> b) -> Computation (Step r a) -> Computation (Step r b) ``` Map a function over each element of a stream. Exposed as `map` at the module's top-level. ## `scanl` _scanl: emit a running left-fold; for each input element, emit the accumulator before combining with the element to advance._ ``` scanl : (b -> a -> b) -> b -> Computation (Step r a) -> Computation (Step r b) ``` Accumulate a running fold over the stream, yielding each intermediate accumulator value. #### Limit Stream limiting: take, takeWhile, drop. ## `drop` _drop: skip the first `n` elements and forward the remainder unchanged; non-positive `n` is a no-op._ ``` drop : Int -> Computation (Step r a) -> Computation (Step r a) ``` Skip the first `n` elements of a stream. Non-positive `n` is a no-op. ## `take` _take: yield at most the first `n` elements of a stream, then `Done null`; non-positive `n` yields the empty stream immediately._ ``` take : Int -> Computation (Step r a) -> Computation (Step null a) ``` Take the first `n` elements of a stream. Non-positive `n` yields the empty stream. ## `takeWhile` _takeWhile: yield prefix elements while the predicate holds; terminates on the first element that fails the predicate._ ``` takeWhile : (a -> Bool) -> Computation (Step r a) -> Computation (Step null a) ``` Take elements while a predicate holds. The first element that fails the predicate is discarded along with everything after. #### Combine Stream combination: concat, interleave, zip, zipWith. ## `concat` _concat: yield all elements of `s1`, then all elements of `s2`; if `s1` ends immediately we forward `s2` directly without rewrapping._ ``` concat : Computation (Step r a) -> Computation (Step s a) -> Computation (Step s a) ``` Concatenate two streams: all elements of the first, then all of the second. Sequential, not interleaved. ## `interleave` _interleave: alternate elements between two streams; when the leading stream ends, the trailing stream takes over uninterrupted._ ``` interleave : Computation (Step r a) -> Computation (Step s a) -> Computation (Step null a) ``` Interleave two streams: alternate elements from each. When one stream ends the other continues to completion. ## `zip` _zip: pair elements positionally into `{ fst, snd }` records; stops when either input stream ends._ ``` zip : Computation (Step r a) -> Computation (Step s b) -> Computation (Step null { fst : a, snd : b }) ``` Zip two streams into a stream of pairs. Stops when either stream ends. ## `zipWith` _zipWith: pair elements positionally and combine each pair with `f`; stops when either input stream ends._ ``` zipWith : (a -> b -> c) -> Computation (Step r a) -> Computation (Step s b) -> Computation (Step null c) ``` Zip two streams with a combining function. Stops when either stream ends. #### Reduce Stream reduction: fold, toList, length, sum, signal, signalOn, any, all. ## `all` _all: return `true` if every element satisfies the predicate; short-circuits on first miss via lazy evaluation of the stream tail._ ``` all : (a -> Bool) -> Computation (Step r a) -> Computation Bool ``` Check if all elements satisfy a predicate. Short-circuits on first failing element. ## `any` _any: return `true` if any element satisfies the predicate; short-circuits on first match via lazy evaluation of the stream tail._ ``` any : (a -> Bool) -> Computation (Step r a) -> Computation Bool ``` Check if any element satisfies a predicate. Short-circuits on first match — the rest of the stream is never forced. ## `fold` _fold: left-fold a stream into a single value with initial accumulator `z`; the canonical terminal combinator other reducers delegate to._ ``` fold : (b -> a -> b) -> b -> Computation (Step r a) -> Computation b ``` Left fold over a stream. Drains the stream, threading the accumulator through `f` for each element. ## `length` _length: count the number of elements in a stream; equivalent to `fold (n: _: n + 1) 0` over the stream's element steps._ ``` length : Computation (Step r a) -> Computation Int ``` Count the number of elements in a stream. ## `signal` _signal: emit `z` then forward only values not structurally equal to the previous emission; specialisation of `signalOn` over `==`._ ``` signal : a -> Computation (Step r a) -> Computation (Step r a) ``` Return a stream that emits only when the incoming values change, using structural equality to detect duplicates. Equivalent to `signalOn z (x: y: x == y)`. ## `signalOn` _signalOn: emit `z` then forward only values the comparator deems different from the previous emission; suppresses runs of equivalent inputs._ ``` signalOn : a -> (a -> a -> Bool) -> Computation (Step r a) -> Computation (Step r a) ``` Return a stream that emits only when the incoming values change. The comparator receives the current value and the next stream value; if they compare equal, the next value is skipped. The returned stream begins with the provided initial value `z`. ## `sum` _sum: sum all numeric elements in a stream starting from 0; equivalent to `fold (acc: x: acc + x) 0`._ ``` sum : Computation (Step r Number) -> Computation Number ``` Sum all numeric elements in a stream. Initial accumulator is `0`. ## `toList` _toList: collect all stream elements into a list in emission order; equivalent to `fold (acc: x: acc ++ [x]) []`._ ``` toList : Computation (Step r a) -> Computation [a] ``` Collect all stream elements into a list, preserving emission order. ### Type Checker #### Term Syntax of the kernel's term language. All 48 constructors produce attrsets with a `tag` field (not `_tag`, to distinguish kernel terms from effect system nodes). Binding is de Bruijn indexed: `mkVar i` refers to the i-th enclosing binder (0 = innermost). Name annotations (`name` parameter on `mkPi`, `mkLam`, `mkSigma`, `mkLet`) are cosmetic — used only in error messages, never in equality checking. ## Constructors ### Variables and Binding - `mkVar : Int → Tm` — variable by de Bruijn index - `mkLet : String → Tm → Tm → Tm → Tm` — `let name : type = val in body` - `mkAnn : Tm → Tm → Tm` — type annotation `(term : type)` ### Functions - `mkPi : String → Tm → Tm → Tm` — dependent function type `Π(name : domain). codomain` - `mkLam : String → Tm → Tm → Tm` — lambda `λ(name : domain). body` - `mkApp : Tm → Tm → Tm` — application `fn arg` ### Pairs - `mkSigma : String → Tm → Tm → Tm` — dependent pair type `Σ(name : fst). snd` - `mkPair : Tm → Tm → Tm` — pair constructor `(fst, snd)` - `mkFst : Tm → Tm` — first projection - `mkSnd : Tm → Tm` — second projection ### Inductive Types - `mkUnit`, `mkTt` — unit type and value - `mkBootSum`, `mkBootInl`, `mkBootInr`, `mkBootSumElim` — bootstrap coproduct for `descPlus` - `mkBootEq`, `mkBootRefl`, `mkBootJ` — identity type with J eliminator ### Universes - `mkU : (Int | Tm) → Tm` — universe `U(level)`. Accepts either a concrete Int (wrapped via `mkLevelLit`) or a Level-typed Tm directly. - `mkLevelLit : Int → Tm` — builds `suc^n zero` as a Level term. ### Axiomatized Primitives - `mkString`, `mkInt`, `mkFloat`, `mkAttrs`, `mkPath`, `mkDerivation`, `mkFunction`, `mkAny` — type formers - `mkStringLit`, `mkIntLit`, `mkFloatLit`, `mkAttrsLit`, `mkPathLit`, `mkDerivationLit`, `mkFnLit`, `mkAnyLit` — literal values ## `funextTypeTm` _funextTypeTm: pre-elaborated kernel term for the funext axiom's type `∀(j,k,A,B,f,g). (∀a. Eq (B a) (f a) (g a)) -> Eq (Π a:A. B a) f g`._ ## `mkAbsurd` _mkAbsurd: empty-type eliminator — `absurd P x` discharges a stuck `x : Empty` to produce a value of any type `P`; well-typed only when `x` is a neutral (Empty has no canonical inhabitants)._ ``` mkAbsurd : Tm -> Tm -> Tm -- type (P), term (x : Empty) ``` ## `mkAllD` _mkAllD: induction-hypothesis collector `All D X P i payload` — given motive `P : (i:I) -> X i -> U(k)`, threads `P` through every recursive child in the payload._ ``` mkAllD : Tm -> Tm -> Tm -> Tm -> Tm -> Tm -> Tm -> Tm -- k, I, D, level, X, P, i, payload ``` ## `mkAnn` _mkAnn: type annotation `(term : type)` — fixes the checking direction at the kernel level; consumed by `Sub` and elaboration._ ``` mkAnn : Tm -> Tm -> Tm -- term, type ``` ## `mkAnnTrusted` _mkAnnTrusted: type annotation marked as elaborator-trusted — `check` skips re-validation of `term` against `type` since elaboration has already proved well-typedness._ ``` mkAnnTrusted : Tm -> Tm -> Tm -- term, type ``` ## `mkAnnTrustedWithDescRef` _mkAnnTrustedWithDescRef: trusted annotation carrying a `_descRef` sidecar — used by the HOAS elaborator to retain levitated description provenance across eval/quote round-trips._ ``` mkAnnTrustedWithDescRef : Tm -> Tm -> Any -> Tm -- term, type, descRef ``` ## `mkAnnTrustedWithLabels` _mkAnnTrustedWithLabels: trusted annotation carrying `_label` / `_conLabel` sidecars — used by `H.withDescLabel` / `H.withConLabel` to surface presentation labels on `descView`._ ``` mkAnnTrustedWithLabels : Tm -> Tm -> { label?, conLabel? } -> Tm ``` ## `mkAny` _mkAny: axiomatised top primitive type `Any` — accepts every Nix value; used as the lossy-fallback kernel for approximate types._ ## `mkAnyLit` _mkAnyLit: kernel literal for an arbitrary Nix value `v : Any` — used by approximate types whose kernel slot is `mkAny`._ ``` mkAnyLit : Any -> Tm ``` ## `mkApp` _mkApp: function application `fn arg` — head `fn` is checked first to infer Π type, then `arg` is checked against the domain._ ``` mkApp : Tm -> Tm -> Tm -- fn, arg ``` ## `mkAttrs` _mkAttrs: axiomatised primitive type `Attrs` — type former at U(0) inhabited by any Nix attribute set, including `{}`._ ## `mkAttrsLit` _mkAttrsLit: kernel literal for a Nix attribute set `a : Attrs`; the attrs are carried opaquely (no per-field validation at the kernel level)._ ``` mkAttrsLit : Attrs -> Tm ``` ## `mkBootEq` _mkBootEq: bootstrap identity type `Eq(A, a, b)` — propositional equality used by `descRet`'s level transport and by the J eliminator._ ``` mkBootEq : Tm -> Tm -> Tm -> Tm -- A, a, b ``` ## `mkBootInl` _mkBootInl: left-injection of `mkBootSum` — `inl(a) : A + B`; carries both `A` and `B` for elaboration shape recovery._ ``` mkBootInl : Tm -> Tm -> Tm -> Tm -- leftTy, rightTy, value ``` ## `mkBootInr` _mkBootInr: right-injection of `mkBootSum` — `inr(b) : A + B`; carries both `A` and `B` for elaboration shape recovery._ ``` mkBootInr : Tm -> Tm -> Tm -> Tm -- leftTy, rightTy, value ``` ## `mkBootJ` _mkBootJ: J eliminator on `mkBootEq` — transports a property `motive` along a proof of equality, yielding a term at the other endpoint._ ``` mkBootJ : Tm -> Tm -> Tm -> Tm -> Tm -> Tm -> Tm -- A, a, motive, identity, b, eq ``` ## `mkBootRefl` _mkBootRefl: bootstrap reflexivity `refl : Eq(A, a, a)` — the canonical inhabitant of every reflexive identity type; check-mode only at elaboration._ ## `mkBootSum` _mkBootSum: bootstrap coproduct type `A + B` — used by `descPlus` to encode sum-of-descriptions before generic sums become available._ ``` mkBootSum : Tm -> Tm -> Tm -- left, right ``` ## `mkBootSumElim` _mkBootSumElim: bootstrap sum eliminator — case-splits a `A + B` scrutinee through `onLeft`/`onRight` arms at motive `(_:A+B) -> Q _`._ ``` mkBootSumElim : Tm -> Tm -> Tm -> Tm -> Tm -> Tm -> Tm -- leftTy, rightTy, motive, onLeft, onRight, scrut ``` ## `mkCanonApp` _mkCanonApp: generic identity-tagged application — `canon-app id params body` evaluates by currying-applying `body` to `params` and stamps the result `VDescCon` with `_canonRef = { id; params; }`; conv/quote short-circuit on the canonical identity instead of forcing `.D`._ ``` mkCanonApp : String -> [Tm] -> Tm -> Tm -- id, params, body ``` ## `mkDerivation` _mkDerivation: axiomatised primitive type `Derivation` — type former at U(0) inhabited by Nix derivation values (attrsets with `type = "derivation"`); the irreducible Nix-store-producing value category._ ## `mkDerivationLit` _mkDerivationLit: kernel literal for a Nix derivation `d : Derivation`; the value is carried opaquely (kernel never inspects derivation attrs)._ ``` mkDerivationLit : Derivation -> Tm ``` ## `mkDesc` _mkDesc: level-zero description type `Desc I k` at index sort `I : U(0)` and universe level `k` — the levitated algebra of constructors for datatypes._ ``` mkDesc : Tm -> Tm -> Tm -- k, I ``` ## `mkDescAt` _mkDescAt: `Desc^k I` carrying an explicit `iLev` for the universe of `I`. The kernel synthesises the desc-formation level as `U(suc (max k iLev))`._ ``` mkDescAt : Tm -> Tm -> Tm -> Tm -- iLev, k, I ``` ## `mkDescCon` _mkDescCon: constructor introduction for `μ I D i` — takes a payload typed by `interpD D (μ I D) i` and returns the corresponding `μ` value._ ``` mkDescCon : Tm -> Tm -> Tm -> Tm -- D, i, payload ``` ## `mkDescConChain` _mkDescConChain: flat-form linear-chain dual of an N-deep mkDescCon. `layers` is a flat outer-first Nix-list of `{ i; heads }` records; `base = { D; i; d }` at the terminator. The consumer pass-graph (evalF, conv, extract, quote) walks the list iteratively, so libnix `forceValueDeep` depth is O(1) regardless of N. Bijective dual of the chain-form Val (`_shape == "linearChain"`)._ ``` mkDescConChain : { layers : [{i:Tm; heads:[Tm]}]; base : {D:Tm; i:Tm; d:Tm}; outerD : Tm; payloadTag : String; payloadLeft : Tm; payloadRight : Tm; } -> Tm ``` ## `mkDescConWithCert` _mkDescConWithCert: `mkDescCon` carrying a `Squash`-truncated guard certificate — threads a refinement guard's decision proof through the kernel for description-backed Record/Variant constructors (`descConCertified`)._ ``` mkDescConWithCert : Tm -> Tm -> Tm -> Tm -> Tm -- D, i, payload, cert ``` ## `mkDescDescApp` _mkDescDescApp: level-zero applied form of `descDesc` — `Desc^(suc L) ⊤` whose mu-fixpoint is `Desc^L I` for `I : U(0)`; bootstraps generic programming over descriptions themselves._ ``` mkDescDescApp : Tm -> Tm -> Tm -- I, L ``` ## `mkDescDescAppAt` _mkDescDescAppAt: applied form of `descDesc` carrying an explicit `ℓ` for the universe of `I` — generalised outer signature `λℓ:Level. λI:U(ℓ). λL:Level. Desc^(suc (max L ℓ)) ⊤`._ ``` mkDescDescAppAt : Tm -> Tm -> Tm -> Tm -- ℓ, I, L ``` ## `mkDescInd` _mkDescInd: levitated induction principle on `μ I D` — given a motive and step function, produces a generic recursor over the data described by `D`._ ``` mkDescInd : Tm -> Tm -> Tm -> Tm -> Tm -> Tm -- I, D, motive, step, scrut ``` ## `mkEmpty` _mkEmpty: empty type `Empty` — initial type at universe level 0; no constructors._ ## `mkEverywhereD` _mkEverywhereD: payload-traversal combinator over a description — applies a per-node `f` at every recursive position, producing a derived payload of the same shape._ ``` mkEverywhereD : Tm -> Tm -> Tm -> Tm -> Tm -> Tm -> Tm -> Tm -> Tm ``` ## `mkFloat` _mkFloat: axiomatised primitive type `Float` — type former at U(0) inhabited by Nix floats (excludes integers)._ ## `mkFloatLit` _mkFloatLit: kernel literal for a Nix float `x : Float`._ ``` mkFloatLit : Float -> Tm ``` ## `mkFnLit` _mkFnLit: kernel literal for an opaque Nix function `f : Function` — wraps the function in an `fnBox` for thunk-identity-preserving conversion._ ``` mkFnLit : Function -> Tm ``` ## `mkFst` _mkFst: first-projection eliminator on a Σ-typed term — yields the dependent `fst` component; Σ-eta is exercised in `conv`._ ``` mkFst : Tm -> Tm ``` ## `mkFunction` _mkFunction: axiomatised primitive type `Function` — type former at U(0) inhabited by opaque Nix-level functions wrapped via `mkOpaqueLam`._ ## `mkFunext` _mkFunext: function-extensionality axiom — given pointwise-equal `f`, `g` at every argument, produces an equality proof `Eq (Π a:A. B a) f g`._ ``` mkFunext : Tm -> Tm -> Tm -> Tm -> Tm -> Tm -- A, B, f, g, hypothesis ``` ## `mkInt` _mkInt: axiomatised primitive type `Int` — type former at U(0) inhabited by Nix integers (excludes floats)._ ## `mkIntEq` _mkIntEq: host `==` on `Int` literals `intEq a b : Bool` (parallel to `mkStrEq`)._ ``` mkIntEq : Tm -> Tm -> Tm -- a, b ``` ## `mkIntLe` _mkIntLe: host `<=` on `Int` literals `intLe a b : Bool` (parallel to `mkStrEq`). Non-symmetric — operand order preserved on a neutral spine._ ``` mkIntLe : Tm -> Tm -> Tm -- a, b ``` ## `mkIntLit` _mkIntLit: kernel literal for a Nix integer `n : Int`._ ``` mkIntLit : Int -> Tm ``` ## `mkInterpD` _mkInterpD: interpret a description `D : Desc I k` against a recursive carrier `X : I -> U(k)` at index `i`, yielding the payload type `interpD D X i`._ ``` mkInterpD : Tm -> Tm -> Tm -> Tm -> Tm -- k, I, D, X, i ``` ## `mkLam` _mkLam: lambda abstraction `λ(name : domain). body` — domain annotation is optional at check time (overridden by expected Π's domain)._ ``` mkLam : String -> Tm -> Tm -> Tm -- name, domain, body ``` ## `mkLet` _mkLet: let-binding `let name : type = val in body` — `name` is cosmetic, the binder is introduced into body's de Bruijn context as index `0`._ ``` mkLet : String -> Tm -> Tm -> Tm -> Tm -- name, type, val, body ``` ## `mkLevel` _mkLevel: universe-level sort `Level : U(0)` — the type former whose inhabitants are level expressions used in `mkU`._ ## `mkLevelLit` _mkLevelLit: concrete Level literal from an `Int` — builds `suc^n zero` as a Level term; entry point for level-polymorphism-free code._ ``` mkLevelLit : Int -> Tm ``` ## `mkLevelMax` _mkLevelMax: pointwise max of two levels `max(l, r) : Level` — used to compute the universe of `Σ` / `Π` types whose components inhabit distinct universes._ ``` mkLevelMax : Tm -> Tm -> Tm -- l, r ``` ## `mkLevelSuc` _mkLevelSuc: successor `suc(level) : Level` — increment a Level expression by one._ ``` mkLevelSuc : Tm -> Tm ``` ## `mkLevelZero` _mkLevelZero: level-zero literal `0 : Level` — base case of `Level`'s inductive structure._ ## `mkLift` _mkLift: Tarski lift `LiftAt l m A : U(m)` with `l ≤ m` — non-cumulative cross-level transport of a type at level `l` into level `m`._ ``` mkLift : Tm -> Tm -> Tm -> Tm -- l, m, A ``` ## `mkLiftElim` _mkLiftElim: elimination of `Lift l m A` — lowers a lifted term back to its original level; the inverse pairing with `mkLiftIntro`._ ``` mkLiftElim : Tm -> Tm -> Tm -> Tm -> Tm -- l, m, A, x ``` ## `mkLiftIntro` _mkLiftIntro: introduction of `Lift l m A` — lifts a term `a : A` at level `l` to a term at level `m`; eq witness is auto-emitted via `mkBootRefl`._ ``` mkLiftIntro : Tm -> Tm -> Tm -> Tm -> Tm -- l, m, A, a ``` ## `mkLitVal` _mkLitVal: closed-Val splice — opaque Val carrier whose eval is identity on the carried value. O(1) Val→Tm reflection; sound iff val is closed._ ``` mkLitVal : Val -> Tm ``` ## `mkMu` _mkMu: levitated fixpoint `μ I D i` — carrier type of values whose constructors are described by `D : Desc I k` at index `i`._ ``` mkMu : Tm -> Tm -> Tm -> Tm -- I, D, i ``` ## `mkOpaqueLam` _mkOpaqueLam: lambda over an opaque Nix function — kernel never inspects or applies it; `fnBox` thunk identity preserves conv reflexivity across eval/quote rounds._ ``` mkOpaqueLam : FnBox -> Tm -> Tm -- fnBox, piType ``` ## `mkPair` _mkPair: pair constructor `(fst, snd)` — both components are checked against the corresponding Σ slots at the expected type._ ``` mkPair : Tm -> Tm -> Tm -- fst, snd ``` ## `mkPath` _mkPath: axiomatised primitive type `Path` — type former at U(0) inhabited by Nix path values._ ## `mkPathLit` _mkPathLit: kernel literal for a Nix path `p : Path`._ ``` mkPathLit : Path -> Tm ``` ## `mkPi` _mkPi: dependent function type `Π(name : domain). codomain` — `name` is cosmetic, the binder is introduced into codomain's de Bruijn context as index `0`._ ``` mkPi : String -> Tm -> Tm -> Tm -- name, domain, codomain ``` ## `mkSigma` _mkSigma: dependent pair type `Σ(name : fst). snd` — `name` is cosmetic, the binder is introduced into snd's de Bruijn context as index `0`._ ``` mkSigma : String -> Tm -> Tm -> Tm -- name, fst, snd ``` ## `mkSnd` _mkSnd: second-projection eliminator on a Σ-typed term — yields the `snd` component with `fst` substituted; Σ-eta is exercised in `conv`._ ``` mkSnd : Tm -> Tm ``` ## `mkSquash` _mkSquash: propositional truncation `Squash A` — quotient of `A` collapsing all inhabitants to one, used for proof-irrelevant fields._ ``` mkSquash : Tm -> Tm ``` ## `mkSquashElim` _mkSquashElim: eliminator for `Squash` restricted to `Squash`-typed motives — preserves proof irrelevance by forbidding motives that distinguish inhabitants._ ``` mkSquashElim : Tm -> Tm -> Tm -> Tm -> Tm -- A, motive, fn, scrut ``` ## `mkSquashIntro` _mkSquashIntro: introduction of `Squash A` — lifts any `a : A` to a single inhabitant of `Squash A`._ ``` mkSquashIntro : Tm -> Tm ``` ## `mkStrEq` _mkStrEq: decidable equality on `String` literals `strEq a b : Bool` — used by indexed datatypes whose constructor selection branches on string keys._ ``` mkStrEq : Tm -> Tm -> Tm -- a, b ``` ## `mkStrLen` _mkStrLen: host string length `strLen s : Int` on a `String` literal; a stuck string operand keeps it neutral. Internalizes string-length refinements (e.g. non-emptiness)._ ``` mkStrLen : Tm -> Tm -- s ``` ## `mkString` _mkString: axiomatised primitive type `String` — type former at U(0) inhabited by Nix string values._ ## `mkStringLit` _mkStringLit: kernel literal for a Nix string `s : String`._ ``` mkStringLit : String -> Tm ``` ## `mkTt` _mkTt: unit value `tt` — sole inhabitant of `mkUnit`; eta-converts every term of type `Unit` to itself._ ## `mkU` _mkU: universe type `U(level)` at the given level expression — accepts either a concrete `Int` (wrapped via `mkLevelLit`) or a Level-typed `Tm`._ ``` mkU : (Int | Tm) -> Tm ``` ## `mkUnit` _mkUnit: unit type `Unit` — terminal type with single inhabitant `tt`; backs `fx.types.Unit` at universe level 0._ ## `mkVar` _mkVar: variable reference by de Bruijn index — `0` is the innermost binder; higher indices reach outer binders._ ``` mkVar : Int -> Tm ``` #### Value Values are the semantic domain produced by evaluation. They use de Bruijn *levels* (counting outward from the top of the context), not indices, which makes weakening trivial. ## Closures `mkClosure : Env → Tm → Closure` — defunctionalized closure. No Nix lambdas in the TCB; a closure is `{ env, body }` where `body` is a kernel Tm evaluated by `eval.instantiate`. ## Value Constructors Each `v*` constructor mirrors a term constructor: - `vLam`, `vPi` — function values/types (carry name, domain, closure) - `vSigma`, `vPair` — pair types/values - `vUnit`, `vTt` — unit - `vBootSum`, `vBootInl`, `vBootInr` — bootstrap coproduct values - `vBootEq`, `vBootRefl` — identity values - `vU` — universe values - `vString`, `vInt`, `vFloat`, `vAttrs`, `vPath`, `vDerivation`, `vFunction`, `vAny` — primitive types - `vStringLit`, `vIntLit`, `vFloatLit`, `vAttrsLit`, `vPathLit`, `vDerivationLit`, `vFnLit`, `vAnyLit` — primitive literals ## Neutrals `vNe : Level → Spine → Val` — a stuck computation: a variable (identified by de Bruijn level) applied to a spine of eliminators. `freshVar : Depth → Val` — neutral with empty spine at the given depth. Used during type-checking to introduce fresh variables under binders. ## Elimination Frames (Spine Entries) - `eApp`, `eFst`, `eSnd` — function/pair eliminators - `eBootSumElim`, `eBootJ` — inductive eliminators ## `eAbsurd` _eAbsurd: elimination frame for `absurd` on a neutral `Empty`-typed scrutinee — carries the target type `P`; sound because `Empty` has no canonical inhabitants, so this frame can only arise on a stuck spine._ ``` eAbsurd : Val -> SpineEntry -- type P ``` ## `eAllD` _eAllD: elimination frame for `allD` on a neutral description — carries motive `P` plus shape parameters._ ``` eAllD : Val -> Val -> Val -> Val -> Val -> Val -> SpineEntry ``` ## `eApp` _eApp: elimination frame for function application — pushes an argument onto a neutral spine._ ``` eApp : Val -> SpineEntry ``` ## `eBootJ` _eBootJ: elimination frame for the J eliminator on a neutral identity proof — carries A, a, motive, refl-case, b._ ``` eBootJ : Val -> Val -> Val -> Val -> Val -> SpineEntry ``` ## `eBootSumElim` _eBootSumElim: elimination frame for `bootSumElim` on a neutral sum scrutinee — carries motive and case arms._ ``` eBootSumElim : Val -> Val -> Val -> Val -> Val -> SpineEntry ``` ## `eDescInd` _eDescInd: elimination frame for `descInd` on a neutral `μ`-typed scrutinee — carries `I`, `D`, motive, step._ ``` eDescInd : Val -> Val -> Val -> Val -> SpineEntry ``` ## `eEverywhereD` _eEverywhereD: elimination frame for `everywhereD` on a neutral description — carries per-node `f` plus shape parameters._ ``` eEverywhereD : Val -> Val -> Val -> Val -> Val -> Val -> Val -> SpineEntry ``` ## `eFst` _eFst: elimination frame for first-projection on a Σ neutral._ ## `eIntEq` _eIntEq: `intEq` frame on a neutral int operand — carries the other operand (symmetric)._ ``` eIntEq : Val -> SpineEntry ``` ## `eIntLeL` _eIntLeL: `intLe` frame where the neutral operand is the lhs — carries the rhs._ ``` eIntLeL : Val -> SpineEntry ``` ## `eIntLeR` _eIntLeR: `intLe` frame where the neutral operand is the rhs — carries the lhs._ ``` eIntLeR : Val -> SpineEntry ``` ## `eInterpD` _eInterpD: elimination frame for `interpD` on a neutral description — carries `k`, `I`, `X`, `i` for completion when the neutral resolves._ ``` eInterpD : Val -> Val -> Val -> Val -> SpineEntry ``` ## `eLiftElim` _eLiftElim: elimination frame for `liftElim` on a neutral `Lift l m A` — carries `l`, `m`, `A` for level lowering._ ``` eLiftElim : Val -> Val -> Val -> SpineEntry ``` ## `eSnd` _eSnd: elimination frame for second-projection on a Σ neutral._ ## `eSquashElim` _eSquashElim: elimination frame for `squashElim` on a neutral `Squash`-typed scrutinee — carries motive shape (`A`, `B`) and case function `f`._ ``` eSquashElim : Val -> Val -> Val -> SpineEntry ``` ## `eStrEq` _eStrEq: elimination frame for `strEq` on a neutral string operand — carries the other operand for completion when the neutral resolves._ ``` eStrEq : Val -> Val -> SpineEntry ``` ## `eStrLen` _eStrLen: elimination frame for `strLen` on a neutral string operand — nullary; the stuck operand is the spine head, so the frame carries nothing._ ``` eStrLen : SpineEntry ``` ## `envCons` _envCons: O(1) environment extension — prepend a value as index 0; no list copy, no cached field._ ``` envCons : Val -> Env -> Env ``` ## `envFromList` _envFromList: normalize a Nix-list environment to cons cells (index 0 = list head); idempotent on cons (isList guard) so evaluator entry points normalize in O(1) on already-cons inputs._ ``` envFromList : [Val] | Env -> Env ``` ## `envLen` _envLen: environment length (binder depth) via an iterative genericClosure spine walk — O(N) time, O(1) stack (overflow-free); no cached field, which would recurse N-deep at read or build._ ``` envLen : Env -> Int ``` ## `envNil` _envNil: empty de Bruijn environment (the cons-cell nil)._ ## `envNth` _envNth: iterative de Bruijn lookup (foldl' over a range) — clears the C-stack and max-call-depth on deep environments; a plain Nix list is indexed verbatim (isList guard) so elaborate-layer list-contexts pass through unchanged._ ``` envNth : Env -> Int -> Val ``` ## `envPrepend` _envPrepend: prepend a short Nix list of values (index 0 first) onto an environment._ ``` envPrepend : [Val] -> Env -> Env ``` ## `envToList` _envToList: materialize a de Bruijn spine into an index-ordered Nix list (index 0 = most recent) via an iterative genericClosure walk — O(N) time, O(1) stack (overflow-free). For whole-context reads (`any`/`map`/`filter` over every binding); a plain Nix list passes through verbatim (isList guard), null yields the empty list. Inverse of envFromList on cons inputs._ ``` envToList : Env -> [Val] ``` ## `freshVar` _freshVar: introduce a fresh neutral variable at the given depth — used during type-checking to bind a fresh witness under Π / Σ / let binders._ ``` freshVar : Int -> Val -- depth ``` ## `mkClosure` _mkClosure: defunctionalised closure `{ env, body }` — captures the evaluation environment and the kernel `Tm` body; instantiated by `eval.instantiate` without Nix lambdas in the TCB._ ``` mkClosure : Env -> Tm -> Closure ``` ## `vAllD` _vAllD: value-domain induction-hypothesis collector — threads motive `P` through every recursive position in a payload._ ``` vAllD : Val -> Val -> Val -> Val -> Val -> Val -> Val -> Val ``` ## `vAny` _vAny: value-domain axiomatised top primitive `Any : U(0)` — accepts every Nix value._ ## `vAnyLit` _vAnyLit: value-domain literal carrying an arbitrary Nix value `v : Any` — used by approximate types whose kernel slot is `vAny`._ ``` vAnyLit : Any -> Val ``` ## `vAttrs` _vAttrs: value-domain axiomatised primitive `Attrs : U(0)` — inhabited by any Nix attrset._ ## `vAttrsLit` _vAttrsLit: value-domain literal carrying an opaque Nix attrset `a : Attrs`._ ``` vAttrsLit : Attrs -> Val ``` ## `vBootEq` _vBootEq: value-domain bootstrap identity type `Eq(A, a, b)` — propositional equality used by `descRet`'s level transport and by the J eliminator._ ``` vBootEq : Val -> Val -> Val -> Val -- A, a, b ``` ## `vBootInl` _vBootInl: value-domain left-injection `inl(a) : A + B` — carries `leftTy` and `rightTy` for elaboration shape recovery._ ``` vBootInl : Val -> Val -> Val -> Val -- leftTy, rightTy, value ``` ## `vBootInr` _vBootInr: value-domain right-injection `inr(b) : A + B` — carries `leftTy` and `rightTy` for elaboration shape recovery._ ``` vBootInr : Val -> Val -> Val -> Val -- leftTy, rightTy, value ``` ## `vBootRefl` _vBootRefl: value-domain reflexivity `refl : Eq(A, a, a)` — canonical inhabitant of every reflexive identity; conv collapses all proofs of refl to this._ ## `vBootSum` _vBootSum: value-domain bootstrap coproduct type `A + B` — used by `descPlus`'s sum-of-descriptions before generic sums become available._ ``` vBootSum : Val -> Val -> Val ``` ## `vDerivation` _vDerivation: value-domain axiomatised primitive `Derivation : U(0)` — Nix derivation values; the store-producing irreducible value category._ ## `vDerivationLit` _vDerivationLit: value-domain literal carrying a Nix derivation `d : Derivation` opaquely._ ``` vDerivationLit : Derivation -> Val ``` ## `vDesc` _vDesc: value-domain level-zero description type `Desc I k` at index sort `I : U(0)` and universe level `k`._ ``` vDesc : Val -> Val -> Val -- level, I ``` ## `vDescAt` _vDescAt: value-domain `Desc^k I` carrying an explicit `iLev` for the universe of `I`. The conv unfolding rule reads `iLev` to construct the expected `mkDescDescAppV` D-slot._ ``` vDescAt : Val -> Val -> Val -> Val -- level, iLev, I ``` ## `vDescCon` _vDescCon: value-domain constructor `descCon(D, i, payload)` — the canonical introducer for `μ I D i` values._ ``` vDescCon : Val -> Val -> Val -> Val -- D, i, payload ``` ## `vDescConChain` _vDescConChain: flat-chain VDescCon for linearChain Descs. `.d` is stub `vTt`; chain lives in `_layers` (Nix list, outer-first) + `_base`. Chain-wide BootSum wrapper info on `_payloadTag`/`_payloadLeft`/`_payloadRight`. Outer cert is `_layers[0].cert`. O(1) libnix stack on deep force; non-chain-aware consumers crash on `.d.fst` rather than silently mis-reading a degenerate view._ ``` vDescConChain : Val -> Val -> String -> Val -> Val -> [LayerRec] -> Val -> Val -- D, i, payloadTag, payloadLeft, payloadRight, layers, base ``` ## `vDescConTagged` _vDescConTagged: value-domain `descCon` stamped with a canonical-reference identity `_canonRef = { id; params }` for conv and quote short-circuiting; skips forcing `.D` on known descriptions, breaking universe-level descent loops. Carries no proof or certificate — `_canonRef` is a conv/quote identity tag only._ ``` vDescConTagged : Val -> Val -> Val -> Val -> Val -- D, i, payload, _canonRef ``` ## `vEmpty` _vEmpty: value-domain empty type — initial type with no inhabitants; conv-equal in one step by tag identity._ ## `vEverywhereD` _vEverywhereD: value-domain payload-traversal combinator — applies per-node `f` at every recursive position, producing a same-shape derived payload._ ``` vEverywhereD : Val -> Val -> Val -> Val -> Val -> Val -> Val -> Val -> Val ``` ## `vFloat` _vFloat: value-domain axiomatised primitive `Float : U(0)`._ ## `vFloatLit` _vFloatLit: value-domain literal carrying a Nix float `x : Float`._ ``` vFloatLit : Float -> Val ``` ## `vFnLit` _vFnLit: value-domain literal carrying an opaque Nix function — `fnBox` preserves thunk identity for conv reflexivity._ ``` vFnLit : FnBox -> Val ``` ## `vFunction` _vFunction: value-domain axiomatised primitive `Function : U(0)` — opaque-function carrier._ ## `vFunext` _vFunext: value-domain funext axiom — given pointwise equality, produces equality of functions at `Π(a:A). B a`._ ``` vFunext : Val -> Val -> Val -> Val -> Val -> Val ``` ## `vInt` _vInt: value-domain axiomatised primitive `Int : U(0)`._ ## `vIntLit` _vIntLit: value-domain literal carrying a Nix integer `n : Int`._ ``` vIntLit : Int -> Val ``` ## `vInterpD` _vInterpD: value-domain interpretation `interpD D X i` — yields the payload type for a constructor described by `D` against carrier `X`._ ``` vInterpD : Val -> Val -> Val -> Val -> Val -> Val -- k, I, D, X, i ``` ## `vLam` _vLam: value-domain lambda `λ(name : domain). body` — carries a defunctionalised closure rather than a Nix function, keeping the TCB Nix-lambda-free._ ``` vLam : String -> Val -> Closure -> Val ``` ## `vLazyDescIndAccLayer` _vLazyDescIndAccLayer: MACHINE-INTERNAL deferred accumulator layer of a `descInd` linear chain. Applying expands via four kAppVV frames (step i d (vPair prevAcc vTt) arg); never escapes `runMachineAtF`._ ``` vLazyDescIndAccLayer : Val -> Val -> Val -> Val -> Val -- step, i, d, prevAcc ``` ## `vLevel` _vLevel: value-domain Level type `Level : U(0)`._ ## `vLevelLit` _vLevelLit: value-domain concrete Level literal `n : Level` — derived from a Nix integer at evaluation time._ ``` vLevelLit : Int -> Val ``` ## `vLevelMax` _vLevelMax: value-domain pointwise max `max(l, r) : Level` — used for universes of dependent products / pairs across distinct levels._ ``` vLevelMax : Val -> Val -> Val -- l, r ``` ## `vLevelSuc` _vLevelSuc: value-domain successor of a Level expression `suc(l) : Level`._ ``` vLevelSuc : Val -> Val ``` ## `vLevelZero` _vLevelZero: value-domain level-zero literal `0 : Level`._ ## `vLift` _vLift: value-domain Tarski lift `Lift l m A` — non-cumulative cross-level transport of type `A : U(l)` into `U(m)` with `l ≤ m`._ ``` vLift : Val -> Val -> Val -> Val -- l, m, A ``` ## `vLiftIntro` _vLiftIntro: value-domain introduction of `Lift l m A` — lifts a term `a : A` at level `l` to a term at level `m`._ ``` vLiftIntro : Val -> Val -> Val -> Val -> Val -- l, m, A, a ``` ## `vMu` _vMu: value-domain levitated fixpoint `μ I D i` — carrier type of values whose constructors are described by `D` at index `i`._ ``` vMu : Val -> Val -> Val -> Val -- I, D, i ``` ## `vNe` _vNe: neutral value — stuck computation `var^lvl `; head is a de Bruijn level, spine is a list of elimination frames awaiting reduction._ ``` vNe : Int -> Spine -> Val -- level, spine ``` ## `vNeSnoc` _vNeSnoc: O(1) neutral-spine extension — append one elimination frame via the skew-binary RAL backing `_ral`, keeping `.spine` a materialized in-order list. Use in place of `vNe lvl (n.spine ++ [frame])` to avoid the O(N²)/overflow-prone Nix-list snoc._ ``` vNeSnoc : Val -> SpineEntry -> Val -- neutral, frame ``` ## `vOpaqueLam` _vOpaqueLam: value-domain opaque lambda over a Nix function — kernel never inspects it; `fnBox` thunk identity preserves conv reflexivity._ ``` vOpaqueLam : FnBox -> Val -> Val -- fnBox, piType ``` ## `vPair` _vPair: value-domain pair `(fst, snd)` — components held in WHNF, projected by `eFst` / `eSnd` spine frames._ ``` vPair : Val -> Val -> Val ``` ## `vPath` _vPath: value-domain axiomatised primitive `Path : U(0)`._ ## `vPathLit` _vPathLit: value-domain literal carrying a Nix path `p : Path`._ ``` vPathLit : Path -> Val ``` ## `vPi` _vPi: value-domain dependent function type `Π(name : domain). codomain` — carries a closure for the codomain to permit semantic substitution._ ``` vPi : String -> Val -> Closure -> Val ``` ## `vSigma` _vSigma: value-domain dependent pair type `Σ(name : fst). snd` — carries a closure for the snd component to permit semantic substitution._ ``` vSigma : String -> Val -> Closure -> Val ``` ## `vSquash` _vSquash: value-domain propositional truncation `Squash A` — quotient of `A` collapsing all inhabitants to one for proof-irrelevant fields._ ``` vSquash : Val -> Val ``` ## `vSquashIntro` _vSquashIntro: value-domain introduction of `Squash A` — lifts any inhabitant of `A` to the sole inhabitant of `Squash A`._ ``` vSquashIntro : Val -> Val ``` ## `vString` _vString: value-domain axiomatised primitive `String : U(0)`._ ## `vStringLit` _vStringLit: value-domain literal carrying a Nix string `s : String`._ ``` vStringLit : String -> Val ``` ## `vThunkTm` _vThunkTm: MACHINE-INTERNAL deferred-Tm Val produced by `ev` on non-atomic Tms. Captures `{ env; tm }`; the driver's `Done` handler forces top-level VThunkTm before returning, so external code observes it only in stored sub-Val fields._ ``` vThunkTm : Env -> Tm -> Val ``` ## `vTt` _vTt: value-domain unit value `tt` — sole inhabitant of `vUnit`; conv collapses all Unit-typed values to this._ ## `vU` _vU: value-domain universe `U(level)` — the type of types at a given Level value._ ``` vU : Val -> Val ``` ## `vUnit` _vUnit: value-domain unit type — terminal type with single inhabitant `vTt`; eta-converted in `conv`._ #### Quote Converts values back to terms, translating de Bruijn levels to indices. Pure function — part of the TCB. ## Core Functions - `quote : Depth → Val → Tm` — read back a value at binding depth d. Level-to-index conversion: `index = depth - level - 1`. - `quoteSp : Depth → Tm → Spine → Tm` — quote a spine of eliminators applied to a head term (folds left over the spine). - `quoteElim : Depth → Tm → Elim → Tm` — quote a single elimination frame applied to a head term. - `nf : Env → Tm → Tm` — normalize: `eval` then `quote`. Useful for testing roundtrip idempotency (`nf env (nf env tm) == nf env tm`). - `lvl2Ix : Depth → Level → Index` — level-to-index helper. ## Trampolining Linear VDescCon chains are quoted iteratively via `genericClosure` for O(1) stack depth on deep generated data (5000+ elements). ## Binder Quotation For VPi, VLam, VSigma: instantiates the closure with a fresh variable at the current depth, then quotes the body at `depth + 1`. ## `lvl2Ix` _lvl2Ix: convert a de Bruijn level to an index at binding `depth` — `index = depth - level - 1`; helper exposed for downstream tooling that interleaves with quotation._ ``` lvl2Ix : Depth -> Level -> Index ``` ## `nf` _nf: normalise a term to its canonical form by `eval` then `quote` — useful for testing round-trip idempotency (`nf env (nf env tm) == nf env tm`)._ ``` nf : Env -> Tm -> Tm ``` ## `quote` _quote: read-back a value `Val` to a term `Tm` at binding `depth` — translates de Bruijn levels to indices via `index = depth - level - 1`. Pure TCB function._ ``` quote : Depth -> Val -> Tm ``` ## `quoteElim` _quoteElim: quote a single elimination frame applied to a head term — dispatches on frame tag, recursively quoting each carried argument._ ``` quoteElim : Depth -> Tm -> Elim -> Tm ``` ## `quoteSp` _quoteSp: quote a spine of elimination frames applied to a head term — folds left over the spine, calling `quoteElim` at each step._ ``` quoteSp : Depth -> Tm -> Spine -> Tm ``` #### Conv Checks whether two values are definitionally equal at a given binding depth. Purely structural — no type information used, no eta expansion. Pure function — part of the TCB. ## Core Functions - `conv : Depth → Val → Val → Bool` — check definitional equality. - `convSp : Depth → Spine → Spine → Bool` — check spine equality (same length, pairwise `convElim`). - `convElim : Depth → Elim → Elim → Bool` — check elimination frame equality (same tag, recursively conv on carried values). ## Conversion Rules - **Structural**: same-constructor values with matching fields. Universe levels compared by `==`. Primitive literals by value. - **Binding forms**: Pi, Lam, Sigma compared under a fresh variable at depth d (instantiate both closures, compare at d+1). - **Compound values**: recursive on all components. - **Neutrals**: same head level and convertible spines. - **Catch-all**: different constructors → false. ## Trampolining Deep ordinary data is represented by generated `VDescCon` values. Conversion stays structural except for explicitly documented eta/unfolding rules. ## No Eta `conv` does not perform eta expansion: a neutral `f` and `λx. f(x)` are **not** definitionally equal. Cumulativity (`U(i) ≤ U(j)`) is handled in check.nix, not here. ## `admitLevel` _admitLevel: is a Level term-independent? — every `normLevel` summand rests on a `zero` or empty-spine `var` base; the shared predicate behind every universe-level soundness gate. Rejects applied-neutral bases, admits level variables._ ``` admitLevel : Val -> Bool ``` ## `cPeelArm` _cPeelArm: deep-arm classifier for `cPeel` — peels VNe spines and VMu/VDesc/VBootEq towers onto the machine frontier via the shared `elimGoals` decomposition. Returns a layer record or null._ ``` cPeelArm : Depth -> Val -> Val -> ({ kind; goals; na; nb; nd; } | null) ``` ## `cPeelBinder` _cPeelBinder: binder-arm classifier for `cPeel` — mirrors convStep's VPi/VLam/eta/VSigma arms (and the preceding Lift-collapse guard) so binder layers peel flat. Returns a layer record or null._ ``` cPeelBinder : Depth -> Val -> Val -> ({ kind; goals; na; nb; nd; } | null) ``` ## `conv` _conv: definitional equality on values at binding `depth` — purely structural with Σ/Unit/Π-eta; foundation of `Sub` in `check` and the kernel TCB._ ``` conv : Depth -> Val -> Val -> Bool ``` ## `convElim` _convElim: elimination-frame equality at binding `depth` — same tag and recursively `conv` on every carried value; building block of `convSp`._ ``` convElim : Depth -> Elim -> Elim -> Bool ``` ## `convLevel` _convLevel: definitional equality on Level expressions — `normLevel` both sides, then structural compare; required because `convLevel` is non-trivial under `levelMax` associativity._ ``` convLevel : Val -> Val -> Bool ``` ## `convSp` _convSp: spine equality at binding `depth` — same length plus pairwise `convElim` on each frame; used to compare two neutral-value spines._ ``` convSp : Depth -> Spine -> Spine -> Bool ``` ## `convStep` _convStep: single conversion step over forced values — the conv dispatch body, called by the `runConvF` machine for non-structural goals._ ``` convStep : Depth -> Val -> Val -> Bool ``` ## `normLevel` _normLevel: normalise a Level expression to canonical form — `levelMax`/`levelSuc` collapsed via the algebraic laws so two equivalent forms compare equal under `convLevel`._ ``` normLevel : Val -> Val ``` #### Eval Pure evaluator: interprets kernel terms in an environment of values. Zero effect system imports — part of the trusted computing base (TCB). ## Core Functions - `eval : Env → Tm → Val` — evaluate with default fuel (10M steps) - `evalF : Int → Env → Tm → Val` — evaluate with explicit fuel budget - `instantiate : Closure → Val → Val` — apply a closure to an argument ## Elimination Helpers - `vApp : Val → Val → Val` — apply a function value (beta-reduces VLam, extends spine for VNe) - `vFst`, `vSnd` — pair projections - `vBootSumElim` — sum elimination - `vBootJ` — identity elimination (computes to base on VBootRefl) ## Trampolining Generated `desc-con` chains use `builtins.genericClosure` to flatten recursive structures iteratively, guaranteeing O(1) stack depth on deep generated recursive data. ## Fuel Mechanism Each `evalF` call decrements a fuel counter. When fuel reaches 0, evaluation throws `"normalization budget exceeded"`. This bounds total work and prevents unbounded computation in the Nix evaluator. Default budget: 10,000,000 steps. ## `descView` _descView: default-fuel one-step semantic view of a description value — return `{ tag = "DView{Ret,Arg,Rec,Pi,Plus}"; idx; ...payload }` selecting the description's outer constructor._ ``` descView : Val -> { tag : DViewTag; idx : Int; ... } | null ``` Dispatches on both primitive `VDescX` shapes and encoded `VDescCon` shapes uniformly. The `idx` field gives the summand position (`0` = ret, `1` = arg, `2` = rec, `3` = pi, `4` = plus). Primary consumer is `check.nix`'s `desc-con` trampoline, which uses the view to detect plus-coproduct shapes with linear recursion. Returns `null` for `VNe` or values whose `_canonRef` is opaque. ## `eval` _eval: default-fuel wrapper around `evalF`; spends from the `defaultFuel` (10M-step) budget. The canonical evaluator entry for kernel consumers._ ``` eval : Env -> Tm -> Val ``` ## `evalF` _evalF: kernel-term evaluator. Routes through the depth-budgeted hybrid in `tc/eval/direct.nix`: direct `mkValueF` recursion for shallow structural terms, the CEK machine (`tc/eval/machine.nix`) at budget exhaustion and for the tags that must see machine-internal Val tags (notably `VLazyDescIndAccLayer`). `mkValueF` remains exported for overlays (notably `tc/elaborate/eval-overlay.nix`) that compose their own self-table with VMeta-aware dispatch._ ``` evalF : Int -> Env -> Tm -> Val ``` ## `instantiate` _instantiate: default-fuel closure application — given `Closure { env; body; }` and an argument `Val`, evaluate the body in the extended environment._ ``` instantiate : Closure -> Val -> Val ``` ## `linearProfile` _linearProfile: default-fuel linear-recursion classifier — given a description value `D`, return a list of pre-rec field types if `D` is a `descArg`-chain ending in `descRec descRet`; `null` if `D` is non-linear (tree, multi-rec, or non-plus)._ ``` linearProfile : Val -> [{ S : Val; ... }] | null ``` Walks the `descArg`/`descRec` chain via `descView`; succeeds when every pre-rec field is `descArg` and the tail is exactly `descRec descRet`. Used by `check.nix`'s `desc-con` trampoline to detect when a plus-coproduct `A + B` has exactly one linear-recursive summand — the case where chain peeling applies and 5000+ layers can be checked without recursion. ## `machine` _fx.tc.eval.machine: CEK abstract machine. `runMachineF` is the defunctionalized form of `evalF`; `runQuoteF` is the symmetric read-back driver consumed by `tc/quote.nix`._ ## `mkDescDescAppV` _mkDescDescAppV: default-fuel canonical `descDesc iLev I L`-value constructor — produces the levitated description-of-descriptions value, cached and shared across the kernel for index `I` at universe `iLev` and level `L`._ ``` mkDescDescAppV : Val -> Val -> Val -> Val -- iLev, I, L ``` ## `mkValueF` _mkValueF: dispatch-algebra-parameterized core evaluator body. Kernel instantiates `mkValueF self`; overlays (notably `tc/elaborate/eval-overlay.nix`) instantiate with their own self-table to thread VMeta-aware dispatch through closure bodies, preserving kernel-purity (Abel-Pientka 2011, section 2; see `tc/elaborate/value.nix:13-17`)._ ``` mkValueF : Self -> Int -> Env -> Tm -> Val ``` ## `sumPayloadTmView` _sumPayloadTmView: term-level sum-payload view — given a `Tm` that is `boot-inl`/`boot-inr` or a sum-shaped `desc-con` with `_descConCert`, return `{ side; value; rebuild; rebuildVal; }` to drive the `desc-con` trampoline's payload walker; `null` for non-sum shapes._ ``` sumPayloadTmView : Tm -> { side : "inl" | "inr"; value : Tm; rebuild : Tm -> Tm; rebuildVal : (Tm -> Val) -> Val -> Val; } | null ``` ## `sumPayloadValView` _sumPayloadValView: value-level sum-payload view — given a `Val` that is `VBootInl`/`VBootInr` or a sum-shaped `VDescCon` with `_descRef`, return `{ side; value; rebuild; }` for use in the `desc-con` trampoline; `null` for non-sum shapes._ ``` sumPayloadValView : Val -> { side : "inl" | "inr"; value : Val; rebuild : Val -> Val; } | null ``` ## `vAllD` _vAllD: default-fuel `allD L I D K X M i d` — the type expressing that every recursive position in description-payload `d` (at index `i`) satisfies motive `M`._ ``` vAllD : Val -> Val -> Val -> Val -> Val -> Val -> Val -> Val -> Val ``` ## `vApp` _vApp: default-fuel kernel function-application value — beta-reduces `VLam`, extends the spine for `VNe`, threads through `VDescViewFn`._ ``` vApp : Val -> Val -> Val ``` ## `vBootJ` _vBootJ: J-eliminator over `VBootEq` — on `VBootRefl` returns the base case (checker has already verified the sides match); on `VNe` extends the spine with an `eBootJ` frame._ ``` vBootJ : Val -> Val -> Val -> Val -> Val -> Val -> Val ``` Arguments are `type lhs motive base rhs eq`. When `eq` is `VBootRefl`, returns `base` directly — the checker has already verified `lhs ≡ rhs`, so `rhs` is unused. When `eq` is `VNe`, preserves `rhs` in the `EBootJ` spine frame so quotation can reconstruct the stuck term. Any other shape is rejected with an internal error. ## `vBootSumElim` _vBootSumElim: default-fuel boot-sum eliminator — dispatches `VBootInl` to `onLeft` and `VBootInr` to `onRight`; extends the spine on `VNe`._ ``` vBootSumElim : Val -> Val -> Val -> Val -> Val -> Val -> Val ``` ## `vDescInd` _vDescInd: default-fuel generic eliminator for description-based indexed inductives — `ind D P step i (con d) = step i d (everywhere D D P (lam j x. ind D P step j x) i d)`._ ``` vDescInd : Val -> Val -> Val -> Val -> Val -> Val ``` Arguments: description `D`, motive `P : (i:I) -> mu D i -> U`, step function `step`, target index `i`, scrutinee (a `VDescCon` or `VNe`). On `VDescCon`, builds the inductive hypothesis as a self-reference closure over `step`/`motive`/`D`/`I`, feeds the canonical `mu D` family to `vEverywhereDF`, and applies `step i d (everywhereResult)`. On `VNe`, extends the spine with `eDescInd D motive step i`. The motive must be a `VLam` so `I` is recoverable from its domain annotation. ## `vEverywhereD` _vEverywhereD: default-fuel `everywhereD L I D K X M ih i d` — apply inductive hypothesis `ih` at every recursive position of `d`, producing the `allD` witness consumed by `vDescInd`._ ``` vEverywhereD : Val -> Val -> Val -> Val -> Val -> Val -> Val -> Val -> Val -> Val ``` ## `vFst` _vFst: kernel pair-projection — return the first component of a `VPair`; extend the spine with `eFst` on a stuck `VNe`. Forces a deferred `VThunkTm` first (the VThunkTm read-site invariant), as `vAppF` does, so the direct evaluator opening a machine-built closure env never reads a thunk's `.tag`._ ``` vFst : Val -> Val ``` ## `vInterpD` _vInterpD: default-fuel description interpretation — given `L_val I_val D X i`, compute the type `interp I D X i` whose values are layers of the recursive datatype determined by `D`._ ``` vInterpD : Val -> Val -> Val -> Val -> Val -> Val ``` ## `vLiftElimF` _vLiftElimF: kernel `lower` eliminator — idempotent at `convLevel l m`; β-reduces `lower _ (lift _ a) -> a` on `VLiftIntro`; appends `ELiftElim` to a stuck `VNe` spine._ ``` vLiftElimF : Val -> Val -> Val -> Val -> Val -> Val ``` ## `vLiftF` _vLiftF: kernel `Lift l m eq A` type-former value — the type of values of `A : U(l)` transported up to `U(m)`; collapses idempotently when `convLevel l m` and composes nested Lifts._ ``` vLiftF : Val -> Val -> Val -> Val -> Val ``` Idempotence: `Lift l l _ A ≡ A` for homogeneous code — returns `A` directly when `convLevel l m`. Composition: `Lift l m _ (Lift l' l _ A') ≡ Lift l' m _ A'` — flattens by lowering the inner level. The witness slot `eq` is irrelevant on collapse since both bound conditions hold by transitivity; emit `vBootRefl` for the composed form. ## `vLiftIntroF` _vLiftIntroF: kernel `liftIntro` value former — wraps `a : A` as a `VLift l m _ A`-typed value; idempotent at `convLevel l m`, and η-reduces a stuck `lower`-spine via `lift _ (lower _ x) ≡ x`._ ``` vLiftIntroF : Val -> Val -> Val -> Val -> Val -> Val ``` Witness-irrelevance is enforced structurally: levels are compared via `convLevel` and the carried `A` via syntactic equality. The spine's `eq` field is not consulted. The η-reduction inspects the tail of a `VNe` spine for an `ELiftElim` frame whose parameters match; on hit, drops the frame to yield the inner stuck term. ## `vSnd` _vSnd: kernel pair-projection — return the second component of a `VPair`; extend the spine with `eSnd` on a stuck `VNe`. Forces a deferred `VThunkTm` first (the VThunkTm read-site invariant), as `vAppF` does, so the direct evaluator opening a machine-built closure env never reads a thunk's `.tag`._ ``` vSnd : Val -> Val ``` ## Sub-namespaces - [`_internal`](/nix-effects/type-checker/eval/_internal) - [`dispatch`](/nix-effects/type-checker/eval/dispatch) ## Source - [`src/tc/eval/core.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/eval/core.nix) - [`src/tc/eval/desc.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/eval/desc.nix) - [`src/tc/eval/direct.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/eval/direct.nix) - [`src/tc/eval/machine.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/eval/machine.nix) #### _internal fx.tc.eval._internal: cross-part evaluator helpers reachable from sibling parts via the self-fixpoint; not part of the stable consumer surface. ## `mkCanonAppVF` _mkCanonAppVF: value-level constructor for `canon-app id params body`-as-tagged-`VDescCon` — currying-applies `body` to `params` and stamps the result with `_canonRef = { id; params; body; }` so conv/quote short-circuit on the canonical identity instead of forcing `.D`._ ``` mkCanonAppVF : Int -> String -> [Val] -> Val -> Val ``` Generic counterpart of `mkDescDescAppVF` for user-registered canonical descriptions. `bodyVal` is expected to be a curried chain of `VLam`s that, after applying every element of `paramVals`, yields a `VDescCon`. The raw result's `.D`/`.i`/`.d` fields are reused; the `_canonRef` stamp takes precedence in conv and quote so the recursive `.D` slot is never forced. `bodyVal` is preserved on the stamp so `quote` can emit `T.mkCanonApp id params body`. `canonRefConv` compares stamps by `(id, params)` only; `body` is conv-irrelevant. Throws if the curried application does not produce a `VDescCon` — the smart-form's static contract. ## `mkDescDescAppVF` _mkDescDescAppVF: value-level constructor for `descDesc I L`-as-tagged-`VDescCon` — builds the tagged shell before forcing the recursively computed fields so conv/quote can recognise the canonical reference without descending into the strong-levitation spiral._ ``` mkDescDescAppVF : Int -> Val -> Val -> Val ``` Returns a `VDescCon` whose `_canonRef = { id = "descDesc"; I; L; }` marker lets conv/quote treat the value opaquely. Eliminators (`descView`, `vInterpDF`, ...) walk through `descViewF`'s one-step semantic view of the same reference rather than forcing `.D` directly. The underlying `raw` value is `descDescVal I L` evaluated via `vAppF`; its fields are reused but the canonical marker takes precedence in conv and quote. This separation enables sharing a single `descDesc` value across every recursive position in the kernel without re-evaluating the spiral. #### Dispatch fx.tc.eval.dispatch: full kernel evaluator self-fixpoint. Consumed by overlay constructions (notably `tc/elaborate/eval-overlay.nix`) that need to build a meta-aware self-table replacing selected dispatch attrs while inheriting the rest. ## `descView` _descView: default-fuel one-step semantic view of a description value — return `{ tag = "DView{Ret,Arg,Rec,Pi,Plus}"; idx; ...payload }` selecting the description's outer constructor._ ``` descView : Val -> { tag : DViewTag; idx : Int; ... } | null ``` Dispatches on both primitive `VDescX` shapes and encoded `VDescCon` shapes uniformly. The `idx` field gives the summand position (`0` = ret, `1` = arg, `2` = rec, `3` = pi, `4` = plus). Primary consumer is `check.nix`'s `desc-con` trampoline, which uses the view to detect plus-coproduct shapes with linear recursion. Returns `null` for `VNe` or values whose `_canonRef` is opaque. ## `eval` _eval: default-fuel wrapper around `evalF`; spends from the `defaultFuel` (10M-step) budget. The canonical evaluator entry for kernel consumers._ ``` eval : Env -> Tm -> Val ``` ## `evalF` _evalF: kernel-term evaluator. Routes through the depth-budgeted hybrid in `tc/eval/direct.nix`: direct `mkValueF` recursion for shallow structural terms, the CEK machine (`tc/eval/machine.nix`) at budget exhaustion and for the tags that must see machine-internal Val tags (notably `VLazyDescIndAccLayer`). `mkValueF` remains exported for overlays (notably `tc/elaborate/eval-overlay.nix`) that compose their own self-table with VMeta-aware dispatch._ ``` evalF : Int -> Env -> Tm -> Val ``` ## `instantiate` _instantiate: default-fuel closure application — given `Closure { env; body; }` and an argument `Val`, evaluate the body in the extended environment._ ``` instantiate : Closure -> Val -> Val ``` ## `linearProfile` _linearProfile: default-fuel linear-recursion classifier — given a description value `D`, return a list of pre-rec field types if `D` is a `descArg`-chain ending in `descRec descRet`; `null` if `D` is non-linear (tree, multi-rec, or non-plus)._ ``` linearProfile : Val -> [{ S : Val; ... }] | null ``` Walks the `descArg`/`descRec` chain via `descView`; succeeds when every pre-rec field is `descArg` and the tail is exactly `descRec descRet`. Used by `check.nix`'s `desc-con` trampoline to detect when a plus-coproduct `A + B` has exactly one linear-recursive summand — the case where chain peeling applies and 5000+ layers can be checked without recursion. ## `mkCanonAppVF` _mkCanonAppVF: value-level constructor for `canon-app id params body`-as-tagged-`VDescCon` — currying-applies `body` to `params` and stamps the result with `_canonRef = { id; params; body; }` so conv/quote short-circuit on the canonical identity instead of forcing `.D`._ ``` mkCanonAppVF : Int -> String -> [Val] -> Val -> Val ``` Generic counterpart of `mkDescDescAppVF` for user-registered canonical descriptions. `bodyVal` is expected to be a curried chain of `VLam`s that, after applying every element of `paramVals`, yields a `VDescCon`. The raw result's `.D`/`.i`/`.d` fields are reused; the `_canonRef` stamp takes precedence in conv and quote so the recursive `.D` slot is never forced. `bodyVal` is preserved on the stamp so `quote` can emit `T.mkCanonApp id params body`. `canonRefConv` compares stamps by `(id, params)` only; `body` is conv-irrelevant. Throws if the curried application does not produce a `VDescCon` — the smart-form's static contract. ## `mkDescDescAppV` _mkDescDescAppV: default-fuel canonical `descDesc iLev I L`-value constructor — produces the levitated description-of-descriptions value, cached and shared across the kernel for index `I` at universe `iLev` and level `L`._ ``` mkDescDescAppV : Val -> Val -> Val -> Val -- iLev, I, L ``` ## `mkDescDescAppVF` _mkDescDescAppVF: value-level constructor for `descDesc I L`-as-tagged-`VDescCon` — builds the tagged shell before forcing the recursively computed fields so conv/quote can recognise the canonical reference without descending into the strong-levitation spiral._ ``` mkDescDescAppVF : Int -> Val -> Val -> Val ``` Returns a `VDescCon` whose `_canonRef = { id = "descDesc"; I; L; }` marker lets conv/quote treat the value opaquely. Eliminators (`descView`, `vInterpDF`, ...) walk through `descViewF`'s one-step semantic view of the same reference rather than forcing `.D` directly. The underlying `raw` value is `descDescVal I L` evaluated via `vAppF`; its fields are reused but the canonical marker takes precedence in conv and quote. This separation enables sharing a single `descDesc` value across every recursive position in the kernel without re-evaluating the spiral. ## `mkValueF` _mkValueF: dispatch-algebra-parameterized core evaluator body. Kernel instantiates `mkValueF self`; overlays (notably `tc/elaborate/eval-overlay.nix`) instantiate with their own self-table to thread VMeta-aware dispatch through closure bodies, preserving kernel-purity (Abel-Pientka 2011, section 2; see `tc/elaborate/value.nix:13-17`)._ ``` mkValueF : Self -> Int -> Env -> Tm -> Val ``` ## `sumPayloadTmView` _sumPayloadTmView: term-level sum-payload view — given a `Tm` that is `boot-inl`/`boot-inr` or a sum-shaped `desc-con` with `_descConCert`, return `{ side; value; rebuild; rebuildVal; }` to drive the `desc-con` trampoline's payload walker; `null` for non-sum shapes._ ``` sumPayloadTmView : Tm -> { side : "inl" | "inr"; value : Tm; rebuild : Tm -> Tm; rebuildVal : (Tm -> Val) -> Val -> Val; } | null ``` ## `sumPayloadValView` _sumPayloadValView: value-level sum-payload view — given a `Val` that is `VBootInl`/`VBootInr` or a sum-shaped `VDescCon` with `_descRef`, return `{ side; value; rebuild; }` for use in the `desc-con` trampoline; `null` for non-sum shapes._ ``` sumPayloadValView : Val -> { side : "inl" | "inr"; value : Val; rebuild : Val -> Val; } | null ``` ## `vAllD` _vAllD: default-fuel `allD L I D K X M i d` — the type expressing that every recursive position in description-payload `d` (at index `i`) satisfies motive `M`._ ``` vAllD : Val -> Val -> Val -> Val -> Val -> Val -> Val -> Val -> Val ``` ## `vApp` _vApp: default-fuel kernel function-application value — beta-reduces `VLam`, extends the spine for `VNe`, threads through `VDescViewFn`._ ``` vApp : Val -> Val -> Val ``` ## `vBootJ` _vBootJ: J-eliminator over `VBootEq` — on `VBootRefl` returns the base case (checker has already verified the sides match); on `VNe` extends the spine with an `eBootJ` frame._ ``` vBootJ : Val -> Val -> Val -> Val -> Val -> Val -> Val ``` Arguments are `type lhs motive base rhs eq`. When `eq` is `VBootRefl`, returns `base` directly — the checker has already verified `lhs ≡ rhs`, so `rhs` is unused. When `eq` is `VNe`, preserves `rhs` in the `EBootJ` spine frame so quotation can reconstruct the stuck term. Any other shape is rejected with an internal error. ## `vBootSumElim` _vBootSumElim: default-fuel boot-sum eliminator — dispatches `VBootInl` to `onLeft` and `VBootInr` to `onRight`; extends the spine on `VNe`._ ``` vBootSumElim : Val -> Val -> Val -> Val -> Val -> Val -> Val ``` ## `vDescInd` _vDescInd: default-fuel generic eliminator for description-based indexed inductives — `ind D P step i (con d) = step i d (everywhere D D P (lam j x. ind D P step j x) i d)`._ ``` vDescInd : Val -> Val -> Val -> Val -> Val -> Val ``` Arguments: description `D`, motive `P : (i:I) -> mu D i -> U`, step function `step`, target index `i`, scrutinee (a `VDescCon` or `VNe`). On `VDescCon`, builds the inductive hypothesis as a self-reference closure over `step`/`motive`/`D`/`I`, feeds the canonical `mu D` family to `vEverywhereDF`, and applies `step i d (everywhereResult)`. On `VNe`, extends the spine with `eDescInd D motive step i`. The motive must be a `VLam` so `I` is recoverable from its domain annotation. ## `vEverywhereD` _vEverywhereD: default-fuel `everywhereD L I D K X M ih i d` — apply inductive hypothesis `ih` at every recursive position of `d`, producing the `allD` witness consumed by `vDescInd`._ ``` vEverywhereD : Val -> Val -> Val -> Val -> Val -> Val -> Val -> Val -> Val -> Val ``` ## `vFst` _vFst: kernel pair-projection — return the first component of a `VPair`; extend the spine with `eFst` on a stuck `VNe`. Forces a deferred `VThunkTm` first (the VThunkTm read-site invariant), as `vAppF` does, so the direct evaluator opening a machine-built closure env never reads a thunk's `.tag`._ ``` vFst : Val -> Val ``` ## `vInterpD` _vInterpD: default-fuel description interpretation — given `L_val I_val D X i`, compute the type `interp I D X i` whose values are layers of the recursive datatype determined by `D`._ ``` vInterpD : Val -> Val -> Val -> Val -> Val -> Val ``` ## `vLiftElimF` _vLiftElimF: kernel `lower` eliminator — idempotent at `convLevel l m`; β-reduces `lower _ (lift _ a) -> a` on `VLiftIntro`; appends `ELiftElim` to a stuck `VNe` spine._ ``` vLiftElimF : Val -> Val -> Val -> Val -> Val -> Val ``` ## `vLiftF` _vLiftF: kernel `Lift l m eq A` type-former value — the type of values of `A : U(l)` transported up to `U(m)`; collapses idempotently when `convLevel l m` and composes nested Lifts._ ``` vLiftF : Val -> Val -> Val -> Val -> Val ``` Idempotence: `Lift l l _ A ≡ A` for homogeneous code — returns `A` directly when `convLevel l m`. Composition: `Lift l m _ (Lift l' l _ A') ≡ Lift l' m _ A'` — flattens by lowering the inner level. The witness slot `eq` is irrelevant on collapse since both bound conditions hold by transitivity; emit `vBootRefl` for the composed form. ## `vLiftIntroF` _vLiftIntroF: kernel `liftIntro` value former — wraps `a : A` as a `VLift l m _ A`-typed value; idempotent at `convLevel l m`, and η-reduces a stuck `lower`-spine via `lift _ (lower _ x) ≡ x`._ ``` vLiftIntroF : Val -> Val -> Val -> Val -> Val -> Val ``` Witness-irrelevance is enforced structurally: levels are compared via `convLevel` and the carried `A` via syntactic equality. The spine's `eq` field is not consulted. The η-reduction inspects the tail of a `VNe` spine for an `ELiftElim` frame whose parameters match; on hit, drops the frame to yield the inner stuck term. ## `vSnd` _vSnd: kernel pair-projection — return the second component of a `VPair`; extend the spine with `eSnd` on a stuck `VNe`. Forces a deferred `VThunkTm` first (the VThunkTm read-site invariant), as `vAppF` does, so the direct evaluator opening a machine-built closure env never reads a thunk's `.tag`._ ``` vSnd : Val -> Val ``` #### Check Semi-trusted (Layer 1): uses the TCB (eval/quote/conv) and reports type errors via `send "typeError"`. Bugs here may produce wrong error messages but cannot cause unsoundness. ## Core Functions - `check : Ctx → Tm → Val → Computation Tm` — checking mode. Verifies that `tm` has type `ty` and returns an elaborated term. - `infer : Ctx → Tm → Computation { term; type; }` — synthesis mode. Infers the type of `tm` and returns the elaborated term with its type. - `checkType : Ctx → Tm → Computation Tm` — verify a term is a type. - `checkTypeLevel : Ctx → Tm → Computation { term; level; }` — like `checkType` but also returns the universe level. ## Context Operations - `emptyCtx` — empty typing context `{ env = []; types = []; depth = 0; }` - `extend : Ctx → String → Val → Ctx` — add a binding (index 0 = most recent) - `lookupType : Ctx → Int → Val` — look up a variable's type by index ## Test Helpers - `runCheck : Computation → Value` — run a computation through the trampoline handler, aborting on `typeError` effects. - `checkTm : Ctx → Tm → Val → Tm|Error` — check and unwrap. - `inferTm : Ctx → Tm → { term; type; }|Error` — infer and unwrap. ## Key Behaviors - **Sub rule**: when checking mode doesn't match (e.g., checking a variable), falls through to `infer` and uses `conv` to compare. - **Non-cumulative universes**: Tarski-style, exact-level — `U(i)` does not subsume into `U(j)` for `i < j`; conv compares levels by `convLevel` with no cumulativity coercion. - **Large elimination**: motives may return any universe, enabling type-computing eliminators (`checkMotive`). - **Trampolining**: Succ and Cons chains checked iteratively. ## `_blame` __blame: shared blame-frame discipline for bindP — { handlers, fold, empty } installed by every trampoline that runs a kernel check Computation so position wrapping is reconstructed at the single top-level typeError handler. `blame` is an opaque cons list (deepSeq-opaque ⇒ O(1)/step, structurally shared); `empty` is its nil for state init._ ``` _blame : { handlers : Handlers, fold : Blame -> Error -> Error, empty : Blame } ``` ## `_yield` __yield: tcYield defer discipline — { handlers, wrap } installed alongside _blame by every trampoline running a kernel check Computation. A head `wrap` makes a recursive checker entry effect-first (O(1) WHNF), so the bindP `isPure` fast path stays flat on recursive arms while leaving syntactic leaves Pure. `tcYield` carries no state and no blame frame — observationally invisible._ ``` _yield : { handlers : Handlers, wrap : Computation a -> Computation a } ``` ## `bindP` _bindP: position-tagged bind for kernel rule bodies — brackets the inner computation with a push/pop blame frame so any typeError it raises is wrapped under the given Position before reaching the top-level handler._ ``` bindP : Position -> Computation a -> (a -> Computation b) -> Computation b ``` Brackets an impure inner computation `m` with a `tcBlamePush`/`tcBlamePop` frame whose wrap calls `D.nestUnder position` on every `diag.Error` `m` raises. A pure `m` skips the frame and threads its value to `k` (it raises nothing, so push-then-pop is a no-op). The push is emitted before an impure `m`, so the combinator is `Impure` in O(1) and forces `m` only to WHNF — recursion in `m` defers into the trampoline. The wrapping records the descent coordinate at the caller site — precision that downstream generic paths (the `check → infer` catch-all, deep conv failures) cannot supply. The continuation `k` runs after the pop, so errors it raises are not wrapped by this frame. Frames are reconstructed onto the leaf error by the single top-level handler (see `_blame`). Use over `K.bind` whenever the failing site has a definite positional identity in the surface syntax; pair with `bindPChain` to thread N positions through one bracket. ## `bindPChain` _bindPChain: fused sequential variant of `bindP` — threads a list of positions through a single shared typeError handler so the emitted blame chain has `positions[0]` as the outermost edge._ ``` bindPChain : [Position] -> Computation a -> (a -> Computation b) -> Computation b ``` Equivalent to nested `bindP p_1 (bindP p_2 (... (bindP p_n m) k_pure) k_pure) k` when intermediate continuations are pure passthroughs, but pushes a single composite frame nesting the positions outermost-first. `positions` is forced only on the error path; empty `positions` push an identity frame. ## `bindPR` _bindPR: rule-annotated variant of `bindP` — wraps the inner computation under `withRule rule position` so the blame edge records both the structural coordinate and the kernel-rule identity that emitted the descent._ ``` bindPR : Position -> String -> Computation a -> (a -> Computation b) -> Computation b ``` Equivalent to `bindP (fx.diag.positions.withRule rule position) m k`. The hint resolver consults only `position.tag`, so the rule annotation never changes hint lookup — it surfaces in pretty-printed output and is available to any consumer reading `Position.rule` directly. ## `check` _check: bidirectional checking-mode entry — verify `tm : ty` and return the elaborated kernel term; dispatches intro-form rules against their type-formers and falls through to synthesis plus structural conversion._ ``` check : Ctx -> Tm -> Val -> Comp Tm ``` Dispatches intro forms against their corresponding type formers: `lam` vs `VPi`, `pair` vs `VSigma`, `tt` vs `VUnit`, `boot-inl`/`boot-inr` vs `VBootSum`, `boot-refl` vs `VBootEq`, `squash-intro` vs `VSquash`, `string-lit`/`int-lit`/... vs their primitive value types, and the trampolined `desc-con` vs `VMu`. Anything not matched falls through to `infer` plus a structural `C.conv` round-trip — the sole CHECK-to-INFER bridge in the bidirectional rules. The kernel is Tarski-style and non-cumulative: a term checked against `U(k)` must have inferred type exactly `U(k)` modulo `convLevel`. No universe-cumulativity coercion fires here. Per-summand level mixing in `desc-arg` / `desc-pi` is handled through the bound-witness slot at synthesis time. The `desc-con` branch is trampolined for deep recursive data (5000+ layers): it peels homogeneous linear-recursive chains along a single recursive position when the description is a plus-coproduct `A + B` with exactly one linear-recursive summand. Non-linear shapes (tree, mutual recursion, multiple recursive constructors, non-plus `D`) fall through to per-layer checking via the degenerate `n = 0` branch. Constructor certificates (`_descConCert`) accelerate the non-recursive case by skipping the chain walk entirely. Failure modes emit `D.mkKernelError` via the `typeError` effect; positions and rule keys identify the failure site for the diagnostic renderer. Cross-ref: `infer` for the synthesis side, `checkMotive` for eliminator motive validation. ## `checkTm` _checkTm: unwrapped variant of `check` — runs `runCheck (self.check ctx tm ty)` so callers get the elaborated term or a flat error record without manual trampoline handling._ ``` checkTm : Ctx -> Tm -> Val -> Tm | { error; msg; expected; got } ``` ## `checkType` _checkType: thin wrapper around `checkTypeLevel` that discards the level — verifies `tm` is a type and returns the elaborated term only._ ``` checkType : Ctx -> Tm -> Computation Tm ``` ## `checkTypeLevel` _checkTypeLevel: type-formation judgement — verifies that `tm` is a type and returns both the elaborated term and the universe Level value it inhabits._ ``` checkTypeLevel : Ctx -> Tm -> Computation { term; level } ``` `level` is a kernel Level *value* (`V.vLevelZero`, `V.vLevelSuc`, `V.vLevelMax`) — not a Nix integer — so level-polymorphic types (`U(k)` for a variable `k : Level`) flow through without ad-hoc integer machinery. Levels come from the typing derivation, not post-hoc value inspection (e.g., `Π(x:A). B` computes its level as the `vLevelMax` of domain/codomain levels). The fallback path delegates to `infer` and succeeds iff the inferred type is a universe; in that case `.type.level` is already a Level value and is forwarded verbatim. ## `emptyCtx` _emptyCtx: empty typing context `{ env = envNil; types = envNil; names = envNil; depth = 0; }` — the zero of `extend`; starting point for top-level `check`/`infer` invocations. env/types/names are de Bruijn cons-list spines (index 0 = most recent binding) walked iteratively by `envNth`, so deep contexts stay host-stack- and call-depth-flat._ ``` emptyCtx : Ctx ``` ## `extend` _extend: append a binding to a typing context — pushes a fresh de Bruijn variable at depth `ctx.depth`, the new type at index 0, and the name at index 0 of `names`; depth increments by one. `depth`/`eb` are forced at each extend so the scalar counters stay plain ints, never deferred `+1`/`or`-thunk chains that recurse N-deep on the C-stack when finally forced (cf. value.nix env-spine memo). The entry-yield budget `eb` carries through unchanged (consumed only at `check`/`infer` heads)._ ``` extend : Ctx -> String -> Val -> Ctx ``` ## `infer` _infer: bidirectional synthesis-mode entry — given a term, return both the elaborated kernel term and a `Val` representing its inferred type; covers variables, annotations, application, projections, eliminators, the universe hierarchy, primitive type formers, and Desc/Mu operations._ ``` infer : Ctx -> Tm -> Comp { term : Tm; type : Val; } ``` Dispatches on `tm.tag` to one rule per term shape. Type formers (`pi`, `sigma`, `list`, `boot-sum`, `boot-eq`, `mu`, `squash`) delegate to `checkTypeLevel` and lift the returned level into a `VU` type. Eliminators (`bool-elim`, `list-elim`, `sum-elim`, `desc-ind`, `j`, `squash-elim`, ...) are the most intricate dispatches: each builds expected motive/step types by quoting the motive at the appropriate de Bruijn depth, accounting for the fresh binders introduced by each step lambda. Variables look up their type in `ctx.types` by index. `ann` elaborates the type annotation first, then checks the body against the resulting `Val` — with one optimisation: when `_descRef` is present and the body is `trusted` (emitted only by `T.mkAnnTrusted` inside the kernel itself), the body is accepted without re-checking. This avoids quadratic blowup on deep recursive-data CHECK where every layer carries the same encoded element description. `app` infers the function side, validates the argument against the domain, and instantiates the codomain closure with the argument's value. Failure to find an applicable rule emits a `typeError` with rule `"infer"` and message `"cannot infer type"`. Per-rule positions and rules identify the specific failure for the diagnostic renderer. Cross-ref: `check` for the checking-mode side, `checkTypeLevel` for type-former level extraction, `checkMotive` for motive validation in eliminator rules. ## `inferTm` _inferTm: unwrapped variant of `infer` — runs `runCheck (self.infer ctx tm)` so callers get `{ term; type }` or a flat error record without manual trampoline handling._ ``` inferTm : Ctx -> Tm -> { term; type } | { error; msg; expected; got } ``` ## `lookupType` _lookupType: read a variable's type from a context by de Bruijn index — index 0 is the most recent binding; throws on out-of-range index with a descriptive message. Indexes the `types` cons-list spine via the iterative `envNth` (host-stack- and call-depth-flat); the bound check uses the O(1) `depth` counter (= spine length) instead of re-walking the spine._ ``` lookupType : Ctx -> Int -> Val ``` ## `runCheck` _runCheck: discharge a checking computation through the trampoline handler — collapses `typeError` into a flat `{ error; msg; expected; got }` record; returns the success value on the happy path._ ``` runCheck : Computation a -> a | { error; msg; expected; got } ``` Installs a `typeError` handler that aborts the computation on the first emission, exposing the structured `diag.Error` as `error` plus convenience projections `msg`, `expected`, and `got` (the leaf detail fields). The success branch returns whatever the computation yielded; only the post-handle `result.value` is exposed (state is discarded). Pair with `checkTm` / `inferTm` for the unwrapped form, or with `fx.tc.check.diag.runCheckD` / `runCheckDLazy` for hint-decorated failures. ## Sub-namespaces - [`_internal`](/nix-effects/type-checker/check/_internal) - [`diag`](/nix-effects/type-checker/check/diag) ## Source - [`src/tc/check/bindP.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/check/bindP.nix) - [`src/tc/check/check.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/check/check.nix) - [`src/tc/check/ctx.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/check/ctx.nix) - [`src/tc/check/infer.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/check/infer.nix) - [`src/tc/check/type.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/check/type.nix) #### Diag Outside the trust boundary. Routes kernel check/infer results through the trusted core and decorates failures with a resolved hint + a SourceMap-sourced surface origin. No new effects. ## API - `sourceMap` — SourceMap data type and combinators. See the module's own doc for the full surface. - `checkD : Ctx -> Tm -> Val -> SourceMap -> Any` - `inferD : Ctx -> Tm -> SourceMap -> Any` - `runCheckD : SourceMap -> Computation -> Any` - `runCheckDLazy : (Unit -> SourceMap) -> Computation -> Any` On success, these return what the trusted core returned (the elaborated Tm for checkD, `{term; type;}` for inferD). On failure, they return `{ error; msg; expected; got; hint; surface; }`. `runCheckDLazy` defers `mkSm null` into the failure branch, so the success path pays one closure allocation instead of the full SM walker. Kernel HOAS entry points (`checkHoas`/`inferHoas`) use this variant. `hint` is a `Hint` record (`{ _tag="Hint"; text; category; severity; docLink; }`) from `fx.diag.hints.resolve`, or null. `surface` is the SourceMap's hoas payload at the blame chain's leaf, or null when the chain exits the mapped region. Soundness audit: this module contains no kernel rule logic. A bug here can produce wrong hint text or an incorrect surface back-map; it cannot cause an ill-typed term to be accepted. ## `checkD` _checkD: `check` with diagnostic decoration — shorthand for `runCheckD sm (C.check ctx tm ty)`; returns the elaborated term on success, decorated error record on failure._ ``` checkD : Ctx -> Tm -> Val -> SourceMap -> Tm | { error; msg; expected; got; hint; surface } ``` ## `inferD` _inferD: `infer` with diagnostic decoration — shorthand for `runCheckD sm (C.infer ctx tm)`; returns `{ term; type }` on success, decorated error record on failure._ ``` inferD : Ctx -> Tm -> SourceMap -> { term; type } | { error; msg; expected; got; hint; surface } ``` ## `runCheckD` _runCheckD: kernel-result decorator — runs `C.runCheck` then, on failure, attaches a resolved hint (via `fx.diag.hints.resolve`) and a SourceMap-sourced surface back-mapping; success path untouched._ ``` runCheckD : SourceMap -> Computation -> Tm | { error; msg; expected; got; hint; surface } ``` Lives outside the trust boundary: no new effect handlers, no rule logic. The trusted `typeError` handler from `ctx.nix` runs first; this combinator is a value-level transform on the failure attrset. On success, returns whatever the trusted core returned (elaborated `Tm`). On failure, augments the flat record with `hint` (a `Hint` record or `null`) and `surface` (the SourceMap's hoas payload at the blame chain's leaf, or `null` when the chain exits the mapped sub-tree). ## `runCheckDLazy` _runCheckDLazy: deferred-SourceMap variant of `runCheckD` — takes a thunk that's forced only on failure, sparing the success path the full SourceMap walker allocation._ ``` runCheckDLazy : (Unit -> SourceMap) -> Computation -> Tm | { error; msg; expected; got; hint; surface } ``` Used by kernel HOAS entry points (`checkHoas` / `inferHoas`) where the SourceMap is a pure byproduct of elaboration and is consulted only when resolving surface positions on error. The success path pays one closure allocation instead of the full SourceMap construction. ## `sourceMap` _sourceMap: parallel structure threaded alongside elaborated `Tm` — back-maps a kernel `Error`'s `Position`-chain blame to the HOAS surface node that produced the offending sub-term._ A SourceMap mirrors the shape of an elaborated `Tm`, carrying at each node an opaque HOAS-origin reference plus a map from `Position` keys to child SourceMaps. Its sole purpose is back-mapping: given an error raised by the kernel during `check`/`infer` — whose blame is expressed as a `Position` chain threaded through `Error.children` by `bindP` — resolve that chain to the HOAS surface node that produced the offending sub-Tm. Structure: ``` SourceMap = { _tag = "SourceMap"; hoas : Any | null; # HOAS-origin reference (opaque) subs : AttrSet SourceMap; # keyed by positionKey(Position) } ``` Surface: constructors (`leaf`, `node`, `opaque`), descent (`descend`, `descendChain`, `hoasAt`, `hoasAtError`), chain extraction (`chainPositions`), key derivation (`positionKey`), and the type predicate (`isSourceMap`). The key alphabet is whatever `src/diag/positions.nix` emits via `positionKey`; chain walking uses the same fast/slow split as `src/diag/pretty.nix` (`builtins.genericClosure` beyond 500 steps). ## Source - [`src/tc/check/diag/shell.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/check/diag/shell.nix) - [`src/tc/check/diag/source_map.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/check/diag/source_map.nix) #### _internal fx.tc.check._internal: cross-part checker helpers reachable from sibling parts via the self-fixpoint; not part of the stable consumer surface. ## `checkDescAtAnyLevel` _checkDescAtAnyLevel: description checking at any universe level — accepts both primitive `VDesc` results and encoded `VMu` descriptions (the `VDesc ↔ μ_⊤(descDesc I L)` correspondence) and threads the universe level back to the caller for downstream encoding decisions._ ``` checkDescAtAnyLevel : Ctx -> Tm -> Val -> Computation { term; level } ``` Trusted-annotation fast path: when `dTm` is a `T.mkAnnTrusted` with a complete `_descRef`, build a canonical description term directly (skipping the unfolding scan) and reuse the carried level. Otherwise fall through to inference and dispatch on the inferred type's tag: - `VDesc`: the level is already on the type; conv-check that the index type matches `iTyVal` and forward. - `VMu`: the description is encoded — scan a bounded list of candidate universe levels (the prelude exercises `L = 0..3`) and ask `conv` whether `V.vDesc lev iTyVal` unifies with the inferred type. Conv fires the symmetric `VDesc ↔ VMu` unfolding internally (same mechanism as `conv.nix:344-355`). - Anything else: emit a `typeError` — not a description. Used by `desc-con` checking (`check.nix`) for `_descConCert` validation, by `infer.nix` for `desc-ind` motive and branch checking, and by `type.nix:mu` to thread the description level into the `μ` type's universe level. ## `checkMotive` _checkMotive: eliminator-motive validation — walks the lambda layers of a motive against an expected domain chain `D_1 → … → D_n → U(k)` and returns the elaborated motive term together with the universe level `k` of its codomain._ ``` checkMotive : Ctx -> Tm -> Chain -> Comp { term; level } ``` The domain chain is a `{ head : Val; tail : Val → Chain } | null` sequence so each layer's domain may depend on the previously-bound value (required by `desc-ind`, whose motive is `(i : I) → μ D i → U(k)`). 1-argument call sites use the `singleton` helper to build a one-element chain. - Lambda motive: extend the context with the current layer's domain, recurse into the body with `chain.tail freshV`, and wrap the result in `T.mkLam`. The level threads up unchanged through each lam wrapper so the innermost codomain's universe flows back to eliminators that care about it (e.g., `desc-ind`'s `allTy`). - Non-lambda motive: fall through to `infer`, then walk the inferred Π-chain in lock-step with the expected `chain`, conv-checking each domain and instantiating each codomain through a fresh variable. The innermost codomain must be a universe; its level is the motive's level. Failure modes emit `D.mkKernelError` under the `P.Motive` position for the diagnostic renderer. Cross-ref: `singleton` for the unary-chain helper, `infer` for the synthesis fallback, `checkTypeLevel` for the innermost-universe leaf. ## `singleton` _singleton: build a one-element motive chain — `{ head = dom; tail = _: null; }` — the trivial telescope used by `checkMotive` call sites with a single domain and no nested binder dependency._ ``` singleton : Val -> Chain ``` The motive chain consumed by `checkMotive` is a nested `{ head : Val; tail : Val → Chain } | null` structure so each layer's domain may depend on prior binder values. Most eliminator sites (Sum, Eq, Squash) have only one motive argument and no inter-binder dependency; `singleton dom` packs that into the chain shape with a constant `tail` that ignores its argument and returns `null` — closing the chain after a single layer. #### Elaborate Bridges the fx.types layer to the kernel's term representation via the HOAS combinator layer. Provides the Nix ↔ kernel boundary. ## Type Elaboration - `elaborateType : FxType → Hoas` — convert an fx.types type descriptor to an HOAS tree. Dispatches on: (1) `_kernel` annotation, (2) structural fields (Pi: domain/codomain, Sigma: fstType/sndFamily), (3) name convention (Bool, Nat, String, Int, Float, ...). Dependent Pi/Sigma require explicit `_kernel` annotation. ## Value Elaboration - `elaborateValue : Hoas → NixVal → Hoas` — convert a Nix value to an HOAS term tree given its HOAS type. Bool→true_/false_, Int→natLit, List→cons chain, Sum→inl/inr, Sigma→pair. Trampolined for large lists. ## Structural Validation - `validateValue : Path → Hoas → NixVal → [{ type; context; value; path; reason; }]` — collecting-handler bridge over `fx.tc.generic.check.deriveCheck`. Accumulates every `typeCheck` effect emission from the canonical structural validator into a list of typed error records carrying structured `Path` (Position-list) descents to each failure site. Empty list ↔ `elaborateValue` would succeed (soundness invariant). ## Value Extraction - `extract : Hoas → Val → NixValue` — reverse of `elaborateValue`. Converts kernel values back to Nix values. Generated Nat/List `VDescCon` chains become Nix ints/lists; generated sums become tagged unions. Pi extraction wraps the VLam as a Nix function with boundary conversion. Opaque types (Attrs, Path, Function, Any) throw — kernel discards payloads. - `extractInner : Hoas → Val → Val → NixValue` — three-argument extraction with kernel type value threading. Supports dependent Pi/Sigma via closure instantiation instead of sentinel tests. - `reifyType : Val → Hoas` — converts a kernel type value back to HOAS. Fallback for when HOAS body application fails (dependent types). Loses sugar (VSigma→sigma, not record). ## Decision Procedure - `decide : Hoas → NixVal → Bool` — returns true iff elaboration and kernel type-checking both succeed. Uses `tryEval` for safety. - `decideType : FxType → NixVal → Bool` — elaborate type then decide. ## Full Pipeline - `verifyAndExtract : Hoas → Hoas → NixValue` — type-check an HOAS implementation against an HOAS type, evaluate, extract to Nix value. Throws on type error. ## Value Embedding - `embedVal : Val → Hoas` — lift a kernel value back into HOAS. `quote 0` reads the value to a closed `Tm`; `H.embedTm` wraps it via the `pre-elab` rule. Use when a Val produced by kernel evaluation (e.g. an effect handler's response) needs to flow into a surrounding HOAS expression that is itself about to be elaborated and re-evaluated. ## `decide` _decide: boolean decision procedure — returns `true` iff `elaborateValue` succeeds and the kernel type-checker accepts the resulting HOAS value; `tryEval` catches all elaboration throws._ ``` decide : Hoas -> NixVal -> Bool ``` ## `decideType` _decideType: `decide` lifted to `fx.types` descriptors — runs `elaborateType` then `decide`. Convenience for users working with `fx.types` rather than raw HOAS types._ ``` decideType : FxType -> NixVal -> Bool ``` ## `elaborateType` _elaborateType: convert an `fx.types` type descriptor to an HOAS type tree — dispatches on `_kernel` annotation, structural fields (Pi: domain/codomain, Sigma: fstType/sndFamily), then name convention (Bool, Nat, Unit, ...). Dependent Pi/Sigma require explicit `_kernel`._ ``` elaborateType : FxType -> Hoas ``` Three dispatch tiers, tried in order: 1. `_kernel` annotation: returned directly. This is the authoritative path for types built via `mkType` with an explicit `kernelType` (covers refinement, dependent, linear, levitated descriptions). 2. Structural fields: `Pi { domain, codomain, checkAt }` → `H.forall`; `Sigma { fstType, sndFamily, proj1 }` → `H.sigma`. Only non-dependent families (constant-codomain / constant-sndFamily) are accepted at this tier — dependent families throw and demand the `_kernel` path. 3. Name convention: primitives (`Bool`, `Nat`, `Unit`, `Null`, `String`, `Int`, `Float`, `Attrs`, `Path`, `Function`, `Any`) map to their HOAS prelude entries. Unknown types throw with a message directing the author to add `_kernel`. Cross-ref: `elaborateValue` for the dual on values, `decideType` for the boolean pipeline. ## `elaborateValue` _elaborateValue: strict-handler bridge over `fx.tc.generic.check.deriveElaborate` — produces the HOAS term that witnesses the value's typing, throwing on the first shape mismatch (catchable by `tryEval`)._ ``` elaborateValue : Hoas -> NixVal -> Hoas ``` Composes the canonical structural walker `fx.tc.generic.check.deriveElaborate` with the strict handler from `fx.effects.typecheck.strict`. The walker is the same fold as `validateValue` instantiated at carrier `Hoas`; the strict handler throws on the first emitted typeCheck failure. Per-tag construction is owned by the hoasAlg algebra inside `tc/generic/check.nix`: `Bool` → `true_`/`false_`; `Nat` → `natLit`; `String`/`Int`/`Float` → primitive literals; `List` → cons chain via an O(1)-per-step continuation accumulator; `Sum` → `inl`/`inr`; `Sigma` → `pair`; constructor records → prev-threaded `H.app` chain via `D.fieldType` (typeFn-aware for dependent fields). Fails fast on the first shape mismatch via `builtins.throw`. Soundness invariant: `validateValue [] hoasTy v` is `[]` iff `elaborateValue hoasTy v` does not throw on the same input. ## `embedVal` _embedVal: Val-to-HOAS lift — `embedVal v` reflects a closed kernel `Val` into HOAS via `H.litVal`; the elaborator emits `T.mkLitVal` and eval returns the value verbatim. O(1) regardless of value depth._ ``` embedVal : Val -> Hoas ``` Use when a kernel value needs to flow into a surrounding HOAS expression that will be re-elaborated and re-evaluated. The canonical example is an effect handler's response handed to a user continuation: the continuation receives a Val but wants to build further HOAS like `H.app H.succ response` for the next program fragment. Without the lift, `H.elab` recurses into the Val (which lacks `_htag`) and throws. Soundness: the splice rule `eval ρ (LitVal v) = v` discards the environment, so v must be closed (no free de Bruijn levels). The bridge guarantees this — values reaching the embed site are evaluation results of closed handler programs. Cost: O(1). The prior `H.embedTm (quote 0 v)` composition walked v structurally each call, producing quadratic blow-up in iterative bridge use (a chain of N `bind get (n: …embedVal n…)` steps quoted progressively deeper state Vals). Reference: two-level type theory splice (Kovács, "Staged Compilation with Two-Level Type Theory", POPL 2024; Annenkov–Capriotti–Kraus–Sattler 2019). ## `evalOverlay` _evalOverlay: default-fuel wrapper around `evalOverlayF`._ ``` evalOverlay : Env -> Tm -> ElabVal ``` ## `evalOverlayF` _evalOverlayF fuel env tm: meta-aware kernel-term evaluator. Substitute for `evalF` at sites that may evaluate closure bodies whose environment contains `VMeta` values._ ``` evalOverlayF : Fuel -> Env -> Tm -> ElabVal ``` ## `extract` _extract: kernel-value to Nix-value extraction — reverse of `elaborateValue`; computes the kernel type from the HOAS type once, then delegates to `extractInner` for the recursive walk with kernel type-value threading._ ``` extract : Hoas -> Val -> NixValue ``` ## `extractInner` _extractInner: three-argument kernel value extraction — takes an HOAS type (for dispatch and sugar), a kernel type value (for dependent codomain/snd computation), and the kernel value to extract._ ``` extractInner : Hoas -> Val -> Val -> NixValue ``` Inner workhorse for `fx.tc.elaborate.extract`. Threads the kernel type value alongside the HOAS form so dependent Pi / Sigma codomains can be instantiated through closure application rather than sentinel-test heuristics. The Pi branch wraps Nix arguments via `self.elaborateValue` before feeding them back into the kernel; the mutual recursion closes through `self`. Decoding strategy varies by type tag: Nat / List / Sum generated shapes are walked via `VDescCon`-chain decomposition (trampolined via `genericClosure` for stack safety on deep values); Pi extraction wraps a `VLam` as a Nix function with boundary conversion; opaque types (Attrs, Path, Function, Any) throw because the kernel discards their payloads. ## `instantiateOverlay` _instantiateOverlay: default-fuel wrapper around `instantiateOverlayF`._ ``` instantiateOverlay : Closure -> ElabVal -> ElabVal ``` ## `instantiateOverlayF` _instantiateOverlayF fuel cl arg: meta-aware closure instantiation. Where kernel `instantiateF` crashes when the closure environment contains a `VMeta` (kernel `vAppF` reads `.tag` which `VMeta` lacks), this overlay routes app/elim dispatch through `VMeta`-aware variants and threads overlay evaluation transitively through closure bodies._ ``` instantiateOverlayF : Fuel -> Closure -> ElabVal -> ElabVal ``` ## `reifyType` _reifyType: kernel-to-HOAS type rebuild — converts a kernel type `Val` back to an HOAS type for extract dispatch; loses sugar (VSigma → H.sigma rather than H.record)._ ``` reifyType : Val -> Hoas ``` Used as a fallback when the HOAS body cannot be applied (dependent-type instantiation, polymorphic app-spine reduction). The HOAS body is preferred when available since it preserves record/variant/maybe structure. Generated `nat` / `list` / `sum` shapes reify to raw-tag HOAS attrsets rather than the module-level `H.nat` / `H.listOf` / `H.sum` combinators — the public combinators carry app-spines or `_dtypeMeta`, while raw tags dispatch directly to `extractInner`'s decoders and avoid re-entering type reify. ## `validateValue` _validateValue: collecting-handler bridge over `fx.tc.generic.check.deriveCheck` — accumulates every `typeCheck` effect emission produced by the canonical structural validator._ ``` validateValue : Path -> Hoas -> NixVal -> [{ type; context; value; path; reason; }] ``` Composes the canonical structural validator `fx.tc.generic.check.deriveCheck` with the collecting handler from `fx.effects.typecheck.collecting` to accumulate every typed failure across the value tree. The path argument is a structured Position list from `fx.tc.generic.path` (alias of `fx.diag.positions`); each emitted error record carries the descent path to its failure site under `reason ∈ { shape-mismatch, missing-field, predicate-failed, deferred-pi }`. Returns `[]` iff `elaborateValue` would not throw on the same input. Use for upfront validation in build-time gates and structural diagnostics; use `elaborateValue` directly when only the elaborated value is needed. ## `verifyAndExtract` _verifyAndExtract: full type-check + evaluate + extract pipeline — given an HOAS type and an HOAS implementation, type-check the impl against the type, evaluate to a kernel value, and extract to a Nix value; throws on type error._ ``` verifyAndExtract : Hoas -> Hoas -> NixValue ``` The canonical entry point for closing a verified-impl pipeline back to ordinary Nix data. Evaluates the term returned by `checkHoas` — not a fresh elaboration of the impl — so the extracted value is the one that was verified, with all metas (e.g. a polymorphic list's element type, solved only by checking against the expected type) already pinned. The kernel type value is the direct elaboration of `hoasTy`; `extractInner` reads descriptions through `descView`, which treats primitive and encoded shapes uniformly, so the type's representation need not match the checked term's. On type-check failure throws `"verifyAndExtract: type check failed"` — callers needing structured diagnostics should split the pipeline: `H.checkHoas` for the error attrset, then `extract`/`extractInner` only on success. ## Sub-namespaces - [`_internal`](/nix-effects/type-checker/elaborate/_internal) - [`meta`](/nix-effects/type-checker/elaborate/meta) ## Source - [`src/tc/elaborate/check.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/elaborate/check.nix) - [`src/tc/elaborate/conv.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/elaborate/conv.nix) - [`src/tc/elaborate/core.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/elaborate/core.nix) - [`src/tc/elaborate/effects.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/elaborate/effects.nix) - [`src/tc/elaborate/eval-overlay.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/elaborate/eval-overlay.nix) - [`src/tc/elaborate/extract.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/elaborate/extract.nix) - [`src/tc/elaborate/infer.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/elaborate/infer.nix) - [`src/tc/elaborate/insertion.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/elaborate/insertion.nix) - [`src/tc/elaborate/meta-core.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/elaborate/meta-core.nix) - [`src/tc/elaborate/quote.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/elaborate/quote.nix) - [`src/tc/elaborate/runElab.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/elaborate/runElab.nix) - [`src/tc/elaborate/value.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/elaborate/value.nix) #### Meta fx.tc.elaborate.meta: meta-aware overlay — `VMeta`, overlay check/infer, overlay eliminators, quote, `elabConv`, five scoped meta-effects (force/getMetas/assignMeta/emitConstraint/getConstraints), `runElab` handler. ## `addAndSimplifyConstraint` _addAndSimplifyConstraint c state: simplify a constraint, allocate its id, append it to 𝒦, and update mentions._ ``` addAndSimplifyConstraint : Constraint -> ElabState -> { id : Int; state : ElabState; } ``` ## `addConstraint` _addConstraint c state: allocate a constraint id, append to 𝒦, and register watcher mentions._ ``` addConstraint : Constraint -> ElabState -> { id : Int; state : ElabState; } ``` ## `assignMeta` _assignMeta id tm: send-emitter extending Δ with `id ↦ tm`. Wakes any watchers registered in `mentions[id]` (per Abel-Pientka, section 7) when the assignment lands. Response shape: unit._ ``` assignMeta : Int -> Tm -> Comp Unit ``` ## `coerce` _coerce: embed a kernel `Val` into `ElabVal`. Identity at the Nix level — the coproduct `ElabVal = Val ⊎ VMeta` is structural (kernel `Val`s are already valid `ElabVal` inhabitants because `VMeta` has a disjoint tag)._ ``` coerce : Val -> ElabVal ``` ## `descendImplicitPi` _Wrap a body in implicit lambdas peeling leading implicit binders from an expected type._ ``` descendImplicitPi : Ctx -> Val -> (Ctx -> Val -> Comp Tm) -> Comp Tm ``` ## `dispatchMeta` _dispatchMeta: bridge `dispatch` for the 5 meta-effects — selects the appropriate step on `ctx.op._opTag` and threads `ctx.state` (Nix-side Δ/𝒦/mentions attrset). Throws on unknown tags so accidental effect-set drift fails at build time._ ``` dispatchMeta : { op; outputVal; state } -> { action; newState; response?; } ``` ## `elabAllDF` _elabAllDF fuel level I D K X M i d: meta-aware allD. `VMeta` in the description position appends `EAllD`._ ``` elabAllDF : Fuel -> Val -> Val -> ElabVal -> Val -> Val -> Val -> Val -> Val -> ElabVal ``` ## `elabAppF` _elabAppF fuel fn arg: meta-aware application helper. Rigid `fn` delegates to the kernel evaluator; `VMeta` appends `EApp arg` to its spine._ ``` elabAppF : Fuel -> ElabVal -> ElabVal -> ElabVal ``` ## `elabBootJ` _elabBootJ type lhs motive base rhs eq: meta-aware J eliminator. `VBootRefl` reduces to `base`; `VMeta` appends `EBootJ`._ ``` elabBootJ : Val -> Val -> Val -> Val -> Val -> ElabVal -> ElabVal ``` ## `elabBootSumElimF` _elabBootSumElimF fuel left right motive onLeft onRight scrut: meta-aware bootstrap sum eliminator. `VMeta` appends `EBootSumElim`._ ``` elabBootSumElimF : Fuel -> Val -> Val -> Val -> Val -> Val -> ElabVal -> ElabVal ``` ## `elabCheck` _elabCheck ctx tm ty: elaborator checking wrapper. Rigid cases delegate to the rigid checker; meta-involving Sub-rule cases synthesize then call `elabConv`, emitting postponed constraints instead of rigid type errors._ ``` elabCheck : Ctx -> ElabTm -> ElabVal -> ElabComp ElabTm ``` ## `elabCheckTm` _elabCheckTm ctx tm ty: run `elabCheck` under `runElab` and return the value branch._ ``` elabCheckTm : Ctx -> ElabTm -> ElabVal -> ElabTm | Error ``` ## `elabConv` _elabConv d ty lhs rhs: meta-aware conversion for elaboration. Rigid comparisons delegate to kernel conversion; comparisons involving `VMeta` emit postponed `conv` constraints; Pi/Sigma types are compared structurally using overlay eliminators so metas retain their spines._ ``` elabConv : Depth -> Val -> ElabVal -> ElabVal -> Comp Bool ``` ## `elabDescIndF` _elabDescIndF fuel D motive step i scrut: meta-aware descInd eliminator. `VMeta` appends `EDescInd`._ ``` elabDescIndF : Fuel -> Val -> Val -> Val -> Val -> ElabVal -> ElabVal ``` ## `elabEverywhereDF` _elabEverywhereDF fuel level I D K X M ih i d: meta-aware everywhereD. `VMeta` in the description position appends `EEverywhereD`._ ``` elabEverywhereDF : Fuel -> Val -> Val -> ElabVal -> Val -> Val -> Val -> Val -> Val -> Val -> ElabVal ``` ## `elabFst` _elabFst p: meta-aware first projection. Rigid pairs/neutrals delegate to the kernel evaluator; `VMeta` appends `EFst`._ ``` elabFst : ElabVal -> ElabVal ``` ## `elabInfer` _elabInfer ctx tm: elaborator synthesis wrapper. Rigid terms delegate to the rigid checker; overlay meta terms carrying a type annotation synthesize that annotation without entering the rigid checker._ ``` elabInfer : Ctx -> ElabTm -> ElabComp { term : ElabTm; type : ElabVal } ``` ## `elabInferApp` _elabInferApp ctx tm: App-mode synthesis. Infers fn type, peels leading implicit Pis via `insertImplicits` when the call site is explicit, then checks the argument against the resulting explicit domain._ ``` elabInferApp : Ctx -> ElabTm -> Comp ({ term; type } | Error) ``` ## `elabInferTm` _elabInferTm ctx tm: run `elabInfer` under `runElab` and return the value branch._ ``` elabInferTm : Ctx -> ElabTm -> { term; type } | Error ``` ## `elabInterpDF` _elabInterpDF fuel level I D X i: meta-aware interpD. `VMeta` in the description position appends `EInterpD`._ ``` elabInterpDF : Fuel -> Val -> Val -> ElabVal -> Val -> Val -> ElabVal ``` ## `elabLiftElimF` _elabLiftElimF l m eq A x: meta-aware Lift eliminator. `VMeta` appends `ELiftElim`._ ``` elabLiftElimF : Val -> Val -> Val -> Val -> ElabVal -> ElabVal ``` ## `elabSnd` _elabSnd p: meta-aware second projection. Rigid pairs/neutrals delegate to the kernel evaluator; `VMeta` appends `ESnd`._ ``` elabSnd : ElabVal -> ElabVal ``` ## `elabSquashElimF` _elabSquashElimF fuel A B f x: meta-aware propositional-truncation eliminator. `VMeta` appends `ESquashElim`._ ``` elabSquashElimF : Fuel -> Val -> Val -> Val -> ElabVal -> ElabVal ``` ## `emitConstraint` _emitConstraint c: send-emitter appending `c` to the constraint queue 𝒦. Distinct from `typeError` (a type-error is proof of unsolvability; a postponed constraint is a monotone wait per Optimist's Lemma, GMM section 6.2 Lemma 4). Response shape: the allocated constraint id._ ``` emitConstraint : Constraint -> Comp Int ``` ## `emptyState` _emptyState: initial `runElab` state — Δ, 𝒦, watcher index, ordered meta ids, and monotone fresh counters._ ``` emptyState : ElabState ``` ## `evalElab` _evalElab state env tm: zonk tm against state.delta then delegate to kernel eval. Unsolved metas yield a stuck VMeta value._ ``` evalElab : ElabState -> Env -> Tm -> Val ``` ## `extendMeta` _extendMeta id type state: add a Hole entry to Δ and advance the fresh-id counter past it._ ``` extendMeta : Int -> MetaType -> ElabState -> ElabState ``` ## `force` _force v: send-emitter requesting Δ-aware reduction of `v`. Kernel `Val`s pass through unchanged; `VMeta { id; spine; type }` is reduced via `delta[id] · spine` when `id` is solved in Δ, else returned unchanged. Response shape: `ElabVal`._ ``` force : ElabVal -> Comp ElabVal ``` ## `forceMeta` _forceMeta v state: if `v` is solved in Δ, replay its captured spine over the solution._ ``` forceMeta : VMeta -> ElabState -> ElabVal ``` ## `freshMeta` _freshMeta type: allocate a new metavariable hole in Δ of the given type. Response shape: VMeta._ ``` freshMeta : MetaType -> Comp VMeta ``` ## `freshMetaInState` _freshMetaInState type state: allocate a new `VMeta` and append a matching Hole entry to Δ._ ``` freshMetaInState : MetaType -> ElabState -> { meta : VMeta; state : ElabState; } ``` ## `getConstraints` _getConstraints: send-emitter reading the current constraint queue 𝒦. Response shape: `𝒦 : [Constraint]`._ ``` getConstraints : Comp [Constraint] ``` ## `getMetas` _getMetas: send-emitter reading the current meta-context Δ. Response shape: `Δ : { = { tm; type } | null }`._ ``` getMetas : Comp Delta ``` ## `handle_Meta` _handle_Meta: kernel HOAS shell `λ_op. λ_s. tt` for the meta-effects bridge. Paired with `runElab`'s `dispatchMeta`, which carries the actual per-op interpretation Nix-side._ ## `handle_MetaTy` _handle_MetaTy: `Π(_op:metaEff). Π(_s:Unit). Unit`. Unit codomain reflects that `outputVal` carries no info for meta-effects — Δ updates and responses flow Nix-side via `dispatchMeta`._ ## `insertImplicits` _Peel leading implicit Pi binders of a function's inferred type by fresh-meta insertion. Returns Comp { term; type } with the explicit head._ ``` insertImplicits : Ctx -> Tm -> Val -> Comp { term : Tm; type : Val } ``` ## `isHole` _isHole m: true when a metavariable-context entry is unsolved._ ``` isHole : MetaEntry -> Bool ``` ## `isImplicitPi` _Predicate: value is a VPi with implicit plicity sidecar._ ``` isImplicitPi : Val -> Bool ``` ## `isSolved` _isSolved m: true when a metavariable-context entry carries a solution._ ``` isSolved : MetaEntry -> Bool ``` ## `isVMeta` _isVMeta: predicate distinguishing the overlay `VMeta` from kernel `Val`s — true iff `v._vTag == "VMeta"`. Kernel `Val`s use `tag` (not `_vTag`) for ADT discrimination, so the predicate is decidable on the disjoint union._ ``` isVMeta : ElabVal -> Bool ``` ## `isVMetaTy` _Predicate: value is a VMeta (overlay representation)._ ``` isVMetaTy : Val -> Bool ``` ## `levelsVal` _levelsVal v: collect neutral variable levels referenced by an elaborator value._ ``` levelsVal : ElabVal -> [Level] ``` ## `lookupMeta` _lookupMeta state id: read a Δ entry by metavariable id, returning null when absent._ ``` lookupMeta : ElabState -> Int -> MetaEntry | Null ``` ## `markConstraint` _markConstraint cid status extra state: update one constraint status and merge extra diagnostic fields._ ``` markConstraint : Int -> String -> AttrSet -> ElabState -> ElabState ``` ## `mentionsOf` _mentionsOf vals: collect unique metavariable ids referenced by a list of elaborator values._ ``` mentionsOf : [ElabVal] -> [Int] ``` ## `metaEff` _metaEff: kernel-typed effect signature for the elaborator's 5 meta-effects — 5-element `H.variant` over tags `force` / `getMetas` / `assignMeta` / `emitConstraint` / `getConstraints`. Payload types are uniformly `H.unit`; semantic payload data rides in the Nix-host sentinel attached to the `op` argument._ ``` metaEff : Hoas U ``` ## `metaIdsVal` _metaIdsVal v: collect metavariable ids referenced by an elaborator value and its spine payloads._ ``` metaIdsVal : ElabVal -> [Int] ``` ## `metaResp` _metaResp: response-type function for `metaEff` — kernel-typed `λ_op : metaEff. H.unit`. All five meta-effects return Nix-host opaque values; the kernel-level response is uniformly unit._ ``` metaResp : Hoas (metaEff -> U) ``` ## `mkConstraint` _mkConstraint c: normalize a constraint record with default position, mentions, and status fields._ ``` mkConstraint : AttrSet -> Constraint ``` ## `mkHole` _mkHole id type: construct an unsolved metavariable-context entry._ ``` mkHole : Int -> MetaType -> MetaEntry ``` ## `mkMeta` _mkMeta: overlay term head for quoted metavariables. This is not a kernel `Tm` constructor; it lives only in `fx.tc.elaborate`'s meta-aware term overlay._ ``` mkMeta : Int -> [ElabTm] -> ElabTm ``` ## `mkSolved` _mkSolved id tm type: construct a solved metavariable-context entry._ ``` mkSolved : Int -> ElabVal -> MetaType -> MetaEntry ``` ## `mkVMeta` _mkVMeta: construct an overlay metavariable value — `{ _vTag = "VMeta"; id; spine; type = { ctx; ty } }`. `id` is the globally-unique meta identifier allocated by `runElab`; `spine` is the local-variable spine captured at construction (Abel-Pientka, section 2 σ); `type` carries the meta's allocation-site typing context plus its expected type (paper's `A[Φ]` annotation)._ ``` mkVMeta : Int -> [Val] -> { ctx : Ctx; ty : Val } -> ElabVal ``` ## `nf` _nf env tm: kernel eval followed by meta-aware quote._ ``` nf : Env -> Tm -> ElabTm ``` ## `occurs` _occurs id v: true when `v` references metavariable `id`._ ``` occurs : Int -> ElabVal -> Bool ``` ## `patternSpine` _patternSpine spine: true for application spines whose arguments are distinct bound variables._ ``` patternSpine : Spine -> Bool ``` ## `plicityAwait` _Emit a postponed plicity-await constraint awaiting the named meta; re-awakens when the meta solves._ ``` plicityAwait : Ctx -> Int -> Val -> Val -> Comp Int ``` ## `processActiveConstraints` _processActiveConstraints state: rerun local simplification for active constraints and preserve non-active constraints._ ``` processActiveConstraints : ElabState -> ElabState ``` ## `quote` _quote d v: meta-aware read-back. Rigid values delegate to `fx.tc.quote`; `VMeta` quotes to `mkMeta id []` with its elimination spine replayed as ordinary term eliminators._ ``` quote : Depth -> ElabVal -> ElabTm ``` ## `quoteElim` _quoteElim d head frame: replay one kernel spine frame against an overlay term head, recursively using meta-aware `quote` for frame payloads._ ``` quoteElim : Depth -> ElabTm -> SpineEntry -> ElabTm ``` ## `quoteSp` _quoteSp d head spine: fold `quoteElim` over a stored elimination spine._ ``` quoteSp : Depth -> ElabTm -> [SpineEntry] -> ElabTm ``` ## `reawakenMentions` _reawakenMentions ids state: mark postponed constraints watching solved metavariables active._ ``` reawakenMentions : [Int] -> ElabState -> ElabState ``` ## `registerMentions` _registerMentions mentions cid index: add a constraint id to every mentioned metavariable watcher list._ ``` registerMentions : [Int] -> Int -> MentionsIndex -> MentionsIndex ``` ## `runElab` _runElab A program: discharge an elaborator computation through the descInterp trampoline. Threads `emptyState` Nix-side via the bridge's `state` channel; `handle_Meta` (kernel shell) plus `dispatchMeta` (Nix-side interpreter) discharge the 5 meta-effects._ ``` runElab : Hoas U -> Hoas (μ freeFxApp metaEff metaResp A) -> { value; state : ElabState; } ``` ## `sigmaFlatten` _sigmaFlatten id state: replace an unsolved Sigma-typed meta with a pair of freshly allocated component metas._ ``` sigmaFlatten : Int -> ElabState -> ElabState ``` ## `simplifyConstraint` _simplifyConstraint c state: locally simplify one constraint by rigid conversion, direct meta solving, occurs-check failure, or postponement._ ``` simplifyConstraint : Constraint -> ElabState -> { state : ElabState; constraint : Constraint; } ``` ## `solveMeta` _solveMeta id tm state: replace a Δ entry with a Solved entry and reawaken constraints watching that id._ ``` solveMeta : Int -> ElabVal -> ElabState -> ElabState ``` ## `updateConstraint` _updateConstraint cid f state: update one constraint in 𝒦 by id._ ``` updateConstraint : Int -> (Constraint -> Constraint) -> ElabState -> ElabState ``` ## `zonkTm` _zonkTm depth state tm: substitute solved metas in `tm`. Returns `{ error = { unsolved-meta; id; ctx; } }` if any `Hole` is in `state.delta`; else `{ value = tm' }` with thunked sub-fields (stack-safe, forcing deferred to consumer access)._ ``` zonkTm : Depth -> ElabState -> Tm -> Result Tm ``` ## Source - [`src/tc/elaborate/meta/constraints.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/elaborate/meta/constraints.nix) - [`src/tc/elaborate/meta/context.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/elaborate/meta/context.nix) - [`src/tc/elaborate/meta/eval.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/elaborate/meta/eval.nix) - [`src/tc/elaborate/meta/meta.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/elaborate/meta/meta.nix) - [`src/tc/elaborate/meta/unify.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/elaborate/meta/unify.nix) - [`src/tc/elaborate/meta/zonk.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/elaborate/meta/zonk.nix) #### _internal fx.tc.elaborate._internal: cross-part elaboration helpers reachable from sibling parts via the self-fixpoint; not part of the stable consumer surface. ## `reifyDesc` _reifyDesc: kernel-to-HOAS description rebuild — converts a description `Val` back to an HOAS description; counterpart to `reifyType` used internally by `extractInner` for `mu` decoding fallbacks._ ``` reifyDesc : Val -> Hoas ``` Reverses `evalDesc`: walks a description Val via `descView` and rebuilds the corresponding HOAS form (`H.descRet`, `H.descArg`, `H.descRec`, `H.descPi`, `H.plus`). Recursion is in `self` so descRec / plus sub-descriptions traverse uniformly. Used by `reifyType`'s `VMu` fallback when no sugared shape (Bool / Nat / List / Sum) matches the description: the result is an anonymous `H.mu (reifyDesc D) H.tt` that `extractInner`'s mu branch can then decode against an optional `_dtypeMeta`. #### Hoas Higher-Order Abstract Syntax layer that lets you write kernel terms using Nix lambdas for variable binding. The `lower` function compiles HOAS trees to de Bruijn indexed Tm terms. ## Example ```nix # Π(A:U₀). A → A H.forall "A" (H.u 0) (A: H.forall "x" A (_: A)) ``` ## Type Combinators - `nat`, `bool`, `unit`, `void` — base types - `string`, `int_`, `float_`, `attrs`, `path`, `derivation`, `function_`, `any` — primitive types - `thunk` (parametric: `thunk : Hoas -> Hoas`) — generic deepSeq-safe carrier - `listOf : Hoas → Hoas` — List(elem) - `sum : Hoas → Hoas → Hoas` — Sum(left, right) - `eq : Hoas → Hoas → Hoas → Hoas` — generated EqDT(type, lhs, rhs) - `u : Int → Hoas` — Universe at level - `forall : String → Hoas → (Hoas → Hoas) → Hoas` — Π-type (Nix lambda for body) - `sigma : String → Hoas → (Hoas → Hoas) → Hoas` — Σ-type ## Compound Types (Sugar) - `record : [{ name; type; }] → Hoas` — nested Sigma (sorted fields) - `maybe : Hoas → Hoas` — Sum(inner, Unit) - `variant : [{ tag; type; }] → Hoas` — nested Sum (sorted tags) - `product : String → [Field] → DataSpec` — named single-constructor μ-datatype ## Term Combinators - `lam : String → Hoas → (Hoas → Hoas) → Hoas` — λ-abstraction - `let_ : String → Hoas → Hoas → (Hoas → Hoas) → Hoas` — let binding - `zero`, `succ`, `true_`, `false_`, `tt`, `refl` — intro forms; `refl` is check-mode only - `nil`, `cons`, `pair`, `inl`, `inr` — data constructors - `stringLit`, `intLit`, `floatLit`, `attrsLit`, `pathLit`, `derivationLit`, `fnLit`, `anyLit` — primitive literals - `absurd`, `ann`, `app`, `fst_`, `snd_` — elimination/annotation ## Eliminators - `ind` — generated natural eliminator adapter - `boolElim` — (k : Level) → (Q : bool → U(k)) → Q true_ → Q false_ → (b : bool) → Q b - `listElim` — generated list eliminator adapter - `sumElim` — generated sum eliminator adapter - `j` — EqDT eliminator adapter with J-shaped arguments ## Ornaments - `ornI`, `ornDesc`, `ornForget` — first-class ornaments compiled to existing `Desc`, `mu`, and `descInd` programs; ornaments are not kernel primitives - `ornPullback`, `ornLiftFold` — transport base programs to ornamented datatypes by composing with `ornForget` - `ornLiftProducer`, `ornLiftTransform` — lift base producers/transforms through functional ornament output sections - `algOrn` — algebraic ornament builder for `descRet`/`descArg`/`descRec`/keep-only `descPi`/`descPlus`, generating an ornament indexed by an algebra result - `functionalOrnament`, `ornBuild` — manual sections of `ornForget` for explicit base-to-ornamented construction - `validateFunctionalLaws`, `functionalCompose` — law metadata checks and composition for sectioned ornaments - `validateOrnament`, `tryOrnament`, `validateAlgOrn`, `tryAlgOrn` — total structured diagnostics for user-facing ornament construction ## Lowering - `lower : Int → Hoas → Tm` — compile at given depth - `elab : Hoas → Tm` — compile from depth 0 ## Convenience - `checkHoas : Hoas → Hoas → Tm|Error` — elaborate type+term, type-check - `inferHoas : Hoas → { term; type; }|Error` — elaborate and infer - `natLit : Int → Hoas` — build S^n(zero) ## Stack Safety Binding chains (pi/lam/sigma/let), succ chains, and cons chains are elaborated iteratively via `genericClosure` — safe to 8000+ depth. ## `False` _False: propositional falsehood — alias for the kernel-primitive `void` (`empty`) per HoTT Book, section 1.7 (`False = ⊥`); the initial-object dual of `True`._ ``` False : Hoas ``` ## `True` _True: propositional truth — alias for `unit` per HoTT Book, section 1.7 (`True = ⊤`). Distinct from `BoolDT`'s data-level `true_`._ ``` True : Hoas ``` ## `WDT` _WDT: W-type description macro — packages shape sort + position family into a `DataSpec` for arbitrary-branching inductive trees outside the description layer._ ``` WDT : Hoas -> (Hoas -> Hoas) -> DataSpec ``` ## `absurd` _absurd: HOAS empty-type eliminator — `absurd P x` discharges `x : Empty` to a value of any type `P` at any universe level; the unique map from the initial object._ ``` absurd : Hoas -> Hoas -> Hoas -- targetType P, scrutinee x ``` ## `absurdFin0` _absurdFin0: vacuous-`fin 0` eliminator — `absurdFin0 P x` discharges `x : fin 0` to any type `P` via no-confusion on the impossible `Eq Nat (succ m) zero`._ ``` absurdFin0 : Hoas -> Hoas -> Hoas -- P, x ``` `fin 0` is vacuous because both fzero and fsuc constructor payloads carry `Eq Nat (succ m) i` leaves, which at `i = 0` give `Eq Nat (succ m) zero` — uninhabited. The implementation uses a single J transport at motive `λx _. natCaseU P Unit x`, landing at `Unit` for succ cases (filled by `tt`) and at `P` at the goal case `i = zero`. Used by `void` and surface `absurd`. ## `absurdPrim` _absurdPrim: HOAS kernel-primitive empty-type eliminator — `absurdPrim P x` discharges a stuck `x : Empty` to produce a value of any type `P` at any universe level; the unique map from the initial object._ ``` absurdPrim : Hoas -> Hoas -> Hoas -- targetType P, scrutinee x ``` ## `algArg` _algArg: arg algebra — consumes the `descArg`'s payload via `body : s -> Algebra`, recursing into the rest of the description through the resulting algebra._ ``` algArg : (Tm -> Algebra) -> Algebra ``` ## `algOrn` _algOrn: build an `Ornament` from an algebra over a base description — indexed by the algebra's result, so each ornamented value carries its algebra trace as the J-index._ ``` algOrn : { I?, J, baseD | D, erase, algebra, resultTy?, pack?, proof?, level?, meta? } -> Ornament ``` ## `algOrnDiagnosticRecords` _algOrnDiagnosticRecords: total structured diagnostics describing every shape mismatch between an algebra and its target description; returns `[]` on success._ ``` algOrnDiagnosticRecords : Attrs -> [Diagnostic] ``` ## `algOrnDiagnostics` _algOrnDiagnostics: human-readable text forms of `algOrnDiagnosticRecords` for inclusion in error messages and test assertions._ ``` algOrnDiagnostics : Attrs -> [String] ``` ## `algPiKeep` _algPiKeep: pi-keep algebra — `body` consumes the Π-quantified branch using `branchIndex` (or full `{ branchIndex, ... }` record), generating an algebra per branch._ ``` algPiKeep : (Tm | { branchIndex, ... }) -> (Tm -> Algebra) -> Algebra ``` ## `algPlus` _algPlus: plus algebra — sum of two sub-algebras, mirroring `descPlus`; routes the algebra into `left` or `right` depending on the constructor arm taken._ ``` algPlus : Algebra -> Algebra -> Algebra ``` ## `algRec` _algRec: rec algebra — handles a `descRec` arm by giving `body : recResult -> Algebra` over the recursive child's algebra result; threads results upward._ ``` algRec : (Tm -> Algebra) -> Algebra ``` ## `algRet` _algRet: ret algebra — terminates an algebraic ornament arm by returning a constant `result` value for the leaf description; used over `descRet` shapes._ ``` algRet : Tm -> Algebra ``` ## `allD` _allD: induction-hypothesis collector — `allD level I D K X M i d` produces the type `All D X M i d`, threading motive `M` through every recursive position in payload `d`._ ``` allD : Level -> Hoas -> Hoas -> Level -> Hoas -> Hoas -> Hoas -> Hoas -> Hoas ``` ## `and` _and: propositional conjunction — `and P Q := Σ (_:P). Q` per HoTT Book, section 1.7. Elaborates to a sigma._ ``` and : Hoas -> Hoas -> Hoas ``` ## `ann` _ann: HOAS type annotation — `ann term type` produces `(term : type)`; fixes the checking direction so the kernel verifies `term` against `type` rather than inferring._ ``` ann : Hoas -> Hoas -> Hoas ``` Essential at every position where the kernel would otherwise need to infer a type from a checkable form (e.g. lambda bodies producing data constructors). The elaborator emits `mkAnn`; `check` consumes the annotation and validates the term against the declared type. Default — re-checks the body at every use site. For named-definition / δ-rule discipline on smart constructors whose bodies are well-typed by construction (and where re-checking would diverge through mutual recursion), see `annTrusted`. ## `annTrusted` _annTrusted: HOAS type annotation that propagates the declared type without re-checking the body — the δ-rule analogue. `annTrusted term type` declares `(term : type)` as a named definition whose body is well-typed by construction._ ``` annTrusted : Hoas -> Hoas -> Hoas ``` Use this for smart constructors that wrap closed expressions whose well-typedness follows compositionally from the kernel-typed combinators used to build them. The infer rule's `trusted` branch skips the body re-check that `ann` performs, making this the only viable annotation form for mutually recursive smart constructors (e.g. `freeFxApp` ↔ `kontQueueApp` in `experimental/desc-interp/desc.nix`, where each body references the other through `μ(…App …)`). Discipline: the caller is responsible for verifying the body against the declared type at definition time (manually, or via a separately-checkable test fixture). At use sites the kernel propagates the declared type unconditionally. Elaborates to `mkAnnTrusted`. Equivalent in spirit to Coq's `Definition foo : T := body` or Agda's signature-then-body form — the type is looked up at use sites, not re-derived. ## `any` _any: kernel-primitive `Any` type — top type covering every Nix value; used as a fallback when no narrower type fits._ ``` any : Hoas ``` ## `anyLit` _anyLit: HOAS Any-literal marker — placeholder term checkable against `any`; covers any kernel-opaque Nix value._ ``` anyLit : Hoas ``` ## `app` _app: HOAS function application — `app f arg` builds the redex; β-reduces during normalisation when `f` is a lambda._ ``` app : Hoas -> Hoas -> Hoas ``` ## `attrs` _attrs: kernel-primitive `Attrs` type — opaque Nix attrset axiomatised at the kernel level; literal-only entry via `attrsLit`._ ``` attrs : Hoas ``` ## `attrsLit` _attrsLit: HOAS Attrs-literal marker — placeholder term checkable against `attrs`; the actual Nix attrset is not embedded in the kernel term._ ``` attrsLit : Hoas ``` ## `bool` _bool: HOAS `Bool` type former — generated by `BoolDT.T`; isomorphic to `Sum Unit Unit` under the description encoding._ ``` bool : Hoas ``` ## `boolElim` _boolElim: HOAS `Bool` eliminator — `boolElim k Q onT onF b` runs `BoolDT.elim k`; the motive `Q : bool -> U(k)` may depend on the scrutinee._ ``` boolElim : Level -> Hoas -> Hoas -> Hoas -> Hoas -> Hoas -- k, motive, onTrue, onFalse, scrut ``` For constant motives use `tc.verified.if_` which auto- generates `λ_:bool.resultTy`. Dependent motives are needed when the result type differs between true and false cases — e.g. dispatching to different datatypes via `natCaseU`. ## `canonApp` _canonApp: generic identity-tagged HOAS application — `canonApp id params body` produces a `VDescCon` stamped with `_canonRef = { id; params; }` at eval time, so conv/quote short-circuit on the canonical identity instead of forcing the recursive `.D` slot. Use to add cycle-safe outer references for user-defined recursive descriptions (freer monads, FTCQueues, ...)._ ``` canonApp : String -> [Hoas] -> Hoas -> Hoas ``` ## `checkFunctionalLaws` _checkFunctionalLaws: identity on `F` when every law-check passes, otherwise throws the concatenated diagnostic text — the assertive sibling of `validateFunctionalLaws`._ ``` checkFunctionalLaws : FunctionalOrnament -> FunctionalOrnament -- throws on failure ``` ## `checkHoas` _checkHoas: HOAS-driven type-checker — `checkHoas typeHoas termHoas` elaborates both, runs the kernel `check` rule, and returns the checked term or a structured Error._ ``` checkHoas : Hoas -> Hoas -> Tm | Error ``` The principal entry for verifying a HOAS term against a HOAS type. Elaborates type and term in tandem (so binders align), then invokes the kernel `check` rule which produces a verified `Tm` or a structured `Error` carrying the failing rule and contextual `Detail`. Returns the Error directly (does not throw); callers route through `?error` for fast dispatch. ## `con` _con: HOAS constructor declarator — `con name fields` packages an ordered list of field declarations into a constructor specification consumed by `datatype`._ ``` con : String -> [Field] -> Constructor ``` ## `conI` _conI: indexed-constructor declarator — `conI name fields targetIndex` declares a constructor whose carrier targets a specific index of the datatype's index family._ ``` conI : String -> [Field] -> (Hoas -> Hoas) -> Constructor ``` ## `cong` _cong: User-Eq congruence — proof of `∀A B (f:A→B) x y. Eq A x y -> Eq B (f x) (f y)`; one J application transporting refl through the function. Composes with `trans` for diagram-chase proofs on derived equalities._ ``` cong : Hoas -> Hoas -> Hoas -> Hoas -> Hoas -> Hoas -> Hoas -- A, B, f, x, y, p ``` ## `congSuc` _congSuc: Level-suc congruence — proof of `∀a b. Eq Level a b -> Eq Level (suc a) (suc b)`; derived via J transporting refl along the supplied equality._ ``` congSuc : Hoas ``` ## `cons` _cons: HOAS list-cons constructor — `cons h t` prepends `h : A` to `t : listOf A`; element type inferred._ ``` cons : Hoas -> Hoas -> Hoas -- head, tail ``` ## `datatype` _datatype: HOAS datatype macro — `datatype name [con …]` declares a named ⊤-slice datatype, emitting `{ D, T, elim, }` with `_dtypeMeta` for walker dispatch._ ``` datatype : String -> [Constructor] -> DataSpec ``` The macro builds the description from constructor field lists, synthesises the type former (`T = mu D tt`), generated constructors that introduce values, and an eliminator specialised for the description. The result attrset's `_dtypeMeta` field carries the constructor list for walker dispatch (used by `recoverConstructor` / `walkAttrsetDatatype`). Surface entry point for declaring new datatypes — prefer over manual `mu` + `descCon` construction. ## `datatypeI` _datatypeI: indexed-datatype macro — `datatypeI name indexSort [conI …]` declares a datatype indexed by `indexSort`; each constructor must declare its target index._ ``` datatypeI : String -> Hoas -> [ConI] -> DataSpec ``` Indexed counterpart to `datatype`. The index sort `I` parametrises the carrier family, and each constructor's `conI` declares the index it produces. The resulting `T : I -> U` is the family type former; `app T idx` is the carrier at a specific index. Prelude `fin`, `vec` use this form. ## `datatypeP` _datatypeP: parametric-datatype macro — `datatypeP name [param …] [con …]` declares a datatype parametric in zero or more sort parameters._ ``` datatypeP : String -> [Param] -> (Hoas -> [Constructor]) -> DataSpec ``` Parameters are sorts external to the datatype; the constructor list is a function of the parameter binders. Resulting `T` takes parameters before producing the carrier type. Prelude `listOf`, `sum` would be expressed via this macro (in practice they're macro-generated via `ListDT`, `SumDT` at fixed parameter shapes). ## `datatypePI` _datatypePI: parametric + indexed datatype — `datatypePI name [param …] indexSortFn [conI …]` combines parameters and indices in one macro._ ``` datatypePI : String -> [Param] -> (Hoas -> Hoas) -> (Hoas -> [ConI]) -> DataSpec ``` The most general datatype declarator: takes external parameters AND an index sort that can depend on those parameters, AND constructors whose index decisions depend on both. Used by `vec` (parameter A, index n : nat) and similar prelude families. ## `dec` _dec: decidability proposition — `dec P := P ⊎ ¬ P` per Agda `Relation.Nullary.Dec`. Defined as `sum P (not P)`; inhabitants built via `yes` / `no` and eliminated via `decElim`._ ``` dec : Hoas -> Hoas ``` ## `decAnd` _decAnd: conjunction decidability — `decAnd P Q dp dq : dec (and P Q)` given `dp : dec P` and `dq : dec Q`. Both yes ⇒ yes (pair); either no ⇒ no (refute via fst_/snd_)._ ``` decAnd : Hoas -> Hoas -> Hoas -> Hoas -> Hoas -- P, Q, decP, decQ ``` ## `decElim` _decElim: eliminator for `dec P` — `decElim P motive oy on d` discharges `d : dec P` against motive `M : dec P -> U(0)` with branches `oy : (p:P) -> M (yes _ p)` and `on : (r: not P) -> M (no _ r)`. Forwards to `sumElim` at level 0._ ``` decElim : Hoas -> Hoas -> Hoas -> Hoas -> Hoas -> Hoas -- P, motive, oyes, ono, d ``` ## `decNot` _decNot: negation decidability — `decNot P dp : dec (not P)`. yes ⇒ no (negation contradicts the proof); no ⇒ yes (the refutation IS the negation witness)._ ``` decNot : Hoas -> Hoas -> Hoas -- P, decP ``` ## `decOr` _decOr: disjunction decidability — `decOr P Q dp dq : dec (or_ P Q)`. Either yes ⇒ yes (inl/inr); both no ⇒ no (refute via sumElim)._ ``` decOr : Hoas -> Hoas -> Hoas -> Hoas -> Hoas -- P, Q, decP, decQ ``` ## `decideEqIntZ` _decideEqIntZ: decidability of IntZ equality — `decideEqIntZ m n : dec (Eq IntZ m n)`. Four-case `intzElim` sign cascade; same-sign quadrants delegate to `decideEqNat` and lift via cong / injectivity; cross-sign quadrants refute via `signsDiffer` / `signsDifferRev`. Agda `Data.Integer.Properties._≟_`._ ``` decideEqIntZ : Hoas -- closed kernel function Π m. Π n. dec (Eq IntZ m n) ``` ## `decideEqNat` _decideEqNat: decidability of Nat equality — `decideEqNat m n : dec (Eq Nat m n)`. Closed kernel `lam`-term performing simultaneous structural recursion on both arguments. Agda `Data.Nat.Properties._≟_`._ ``` decideEqNat : Hoas -- closed kernel function Π m. Π n. dec (Eq Nat m n) ``` ## `decideLeIntZ` _decideLeIntZ: decidability of `intzLe` — `decideLeIntZ m n : dec (intzLe m n)`. Four-case `intzElim` sign cascade following `intzLe`'s quadrant table; pos-pos delegates to `decideLeNat`, pos-negSucc returns `no` (target reduces to `void`), negSucc-pos returns `yes tt` (target reduces to `unit`), negSucc-negSucc delegates to `decideLeNat` at flipped arguments. Agda `Data.Integer.Properties._≤?_`._ ``` decideLeIntZ : Hoas -- closed kernel function Π m. Π n. dec (intzLe m n) ``` ## `decideLeNat` _decideLeNat: decidability of `Le` — `decideLeNat m n : dec (le m n)`. Closed kernel `lam`-term following Agda `Data.Nat.Properties._≤?_` four-case recipe (zero-zero, zero-suc, suc-zero, suc-suc)._ ``` decideLeNat : Hoas -- closed kernel function Π m. Π n. dec (le m n) ``` ## `derivation` _derivation: kernel-primitive `Derivation` type — Nix derivation values (attrsets carrying `type = "derivation"`); the store-producing irreducible Nix value category, axiomatised at the kernel level with literal-only entry via `derivationLit`._ ``` derivation : Hoas ``` ## `derivationLit` _derivationLit: HOAS Derivation-literal marker — placeholder term checkable against `derivation`; the Nix derivation is not embedded in the kernel term._ ``` derivationLit : Hoas ``` ## `desc` _desc: ⊤-slice description type former — alias for `descI unitPrim`; descriptions over the kernel-primitive unit index type._ ``` desc : Hoas ``` ## `descArg` _descArg: argument-position description-encoder — `descArg I k S T` extends a description with a non-recursive field of sort `S`, with `T` the continuation under the bound value._ ``` descArg : Hoas -> Level -> Hoas -> Hoas -> Hoas ``` ## `descCataBool` _descCataBool: constant-Bool-motive description catamorphism — `descCataBool { D, carrier, onArg }` folds `onArg`'s per-field Boolean decisions over `μ ⊤ D`, conjoining every `descArg` field and recursive child. The non-dependent specialisation of `descInd`._ ``` descCataBool : { D : Hoas; carrier : Hoas; onArg : Int -> Hoas -> Hoas -> Hoas } -> Hoas -- result : carrier -> Bool ``` ## `descCon` _descCon: description-constructor introduction — `descCon D i d` builds an element of `μ I D i` from payload `d` matching `D`'s shape at index `i`._ ``` descCon : Hoas -> Hoas -> Hoas -> Hoas -- D, index, payload ``` Used internally by the elaborator to emit `mu` carriers from interpreted payloads. Surface code building inductive values typically routes through generated constructors (`zero`/`succ`/`true_`/`nil`/etc.) which call `descCon` with the right payload shape. Direct use is for advanced ornament-bridging code. ## `descDesc` _descDesc: levitated description-of-descriptions — `descDesc I k` is the description whose μ-carrier is `Desc^k I`; foundational for description encoding._ ``` descDesc : Hoas -> Level -> Hoas -- I, k ``` The plus-tree of `descDesc` has five summands corresponding to the description constructors (`retI`, `descArg`, `recI`, `piI`, `plusI`). Every encoded HOAS description elaborates through `descDesc` so each `VDescCon` value carries its constructor tag explicitly. Universe-polymorphic over `k`. ## `descElim` _descElim: encoded description eliminator — `descElim I k L motive onRet onArg onRec onPi onPlus scrut` walks the cascade over `descDesc I k`'s plus-tree to dispatch on `scrut`'s constructor summand._ ``` descElim : Hoas -> Level -> Level -> Hoas -> Hoas -> Hoas -> Hoas -> Hoas -> Hoas -> Hoas -> Hoas ``` The deep machinery for analysing description shapes. Each `on*` callback handles one summand of the description encoding. Surface code rarely needs this directly — `descInd` is the friendlier face for value-level induction; `descElim` is needed for type-level dispatch over description shape (e.g. inside ornament machinery). ## `descInd` _descInd: description-induction principle — `descInd D motive step i scrut` eliminates `scrut : μ I D i` against motive `Q : ∀i:I. μ I D i -> U`, using `step` to handle each summand with its inductive hypotheses._ ``` descInd : Hoas -> Hoas -> Hoas -> Hoas -> Hoas -> Hoas -- D, motive, step, index, scrut ``` Generic indexed induction. The `step` argument receives the index, the payload, and the inductive hypotheses (`allD`- wrapped). Generated eliminators like `NatDT.elim`, `ListDT.elim` are pre-applied specialisations of `descInd`. ## `descPi` _descPi: ⊤-slice piI alias — `descPi k S D` quantifies over sort `S` and continues with `D` under a constant-index function; the index-of-payload defaults to `ttPrim`._ ``` descPi : Level -> Hoas -> Hoas -> Hoas -- k, sort, continuation ``` ## `descRec` _descRec: ⊤-slice recI alias — `recI unitPrim 0 ttPrim D`; adds a ⊤-indexed recursive child to a description._ ``` descRec : Hoas -> Hoas -- continuation ``` ## `descRet` _descRet: ⊤-slice retI alias — `retI unitPrim 0 ttPrim`; the standard leaf of a ⊤-indexed description, used by prelude descriptions like `natDesc`._ ``` descRet : Hoas ``` ## `elab` _elab: closed-term HOAS-to-Tm compiler — lowers a HOAS term from depth 0, runs meta-aware synthesis + zonking, and surfaces unsolved metas as a throw at the elaborator boundary._ ``` elab : Hoas -> Tm ``` ## `elab2` _elab2: pair-producing elaborator — runs `elab` and `sourceMapOf` together, returning `{ tm, sourceMap }`; used by the diagnostic shell which consumes both outputs._ ``` elab2 : Hoas -> { tm : Tm, sourceMap : SourceMap } ``` ## `embedTm` _embedTm: opaque HOAS carrier for a closed kernel `Tm` — `embedTm tm` wraps an already-elaborated term so it can sit inside a surrounding HOAS tree; `elaborate` returns the carried `tm` verbatim regardless of binding depth._ ``` embedTm : Tm -> Hoas ``` Use when a pre-built Tm needs to flow back through the HOAS surface. For Val embedding, prefer `H.litVal` (or `fx.tc.elaborate.embedVal`), which reflects the Val directly in O(1) without the `quote 0` walk. Soundness depends on the carried `Tm` being closed under the binders surrounding the embed site. Elaborates via the `pre-elab` rule in `tc/hoas/lower.nix`. ## `empty` _empty: HOAS empty type — alias for the kernel-primitive `emptyPrim`; the initial-object dual of `unit`._ ``` empty : Hoas ``` ## `eq` _eq: HOAS propositional equality — `eq A a b` builds `EqDT A a b`, the levitated identity type over a sort `A`; `refl` introduces, `j` eliminates._ ``` eq : Hoas -> Hoas -> Hoas -> Hoas ``` ## `eqCongSucc` _eqCongSucc: congruence of `suc` — `eqCongSucc m n e : Eq Nat (suc m) (suc n)` given `e : Eq Nat m n`. bootJ at motive `λx _. Eq Nat (suc m) (suc x)`; base case satisfied by `bootRefl`._ ``` eqCongSucc : Hoas -> Hoas -> Hoas -> Hoas -- m, n, eq ``` ## `eqDT` _eqDT: prelude equality type — `eqDT A a b` is `μ A (eqDesc A a) b`, the indexed datatype carrier of equality at sort `A`._ ``` eqDT : Hoas -> Hoas -> Hoas -> Hoas -- A, lhs, rhs ``` ## `eqDTToEq` _eqDTToEq: datatype equality → bootstrap equality — `eqDTToEq A a b x` extracts a `bootEq A a b` witness from `x : eqDT A a b` via descInd at motive `Q i x = bootEq A a i`._ ``` eqDTToEq : Hoas -> Hoas -> Hoas -> Hoas -> Hoas -- A, lhs, rhs, x ``` ## `eqDesc` _eqDesc: prelude equality description — `eqDesc A a` builds the single-constructor retI-only description of `EqDT A a : A -> U`, used as the carrier description of equality at sort `A`._ ``` eqDesc : Hoas -> Hoas -> Hoas -- A, lhs ``` ## `eqInjSucc` _eqInjSucc: injectivity of `suc` — `eqInjSucc m n e : Eq Nat m n` given `e : Eq Nat (suc m) (suc n)`. bootJ at motive `λx _. Eq Nat m (predNat x)`; base case `Eq Nat m m` via β-reduction of `predNat (suc m)`._ ``` eqInjSucc : Hoas -> Hoas -> Hoas -> Hoas -- m, n, eqSucc ``` ## `eqIsoBwd` _eqIsoBwd: backward leg of the eq ↔ eqDT iso — proves `bootEq (eqDT A a b) (eqToEqDT (eqDTToEq x)) x`; descInd on `x` reduces to the diagonal via inner J transport._ ``` eqIsoBwd : Hoas -> Hoas -> Hoas -> Hoas -- A, lhs, rhs ``` ## `eqIsoFwd` _eqIsoFwd: forward leg of the eq ↔ eqDT iso — proves `bootEq (bootEq A a b) (eqDTToEq (eqToEqDT e)) e`; J on `e` collapses both sides at the base case._ ``` eqIsoFwd : Hoas -> Hoas -> Hoas -> Hoas -- A, lhs, rhs ``` ## `eqRefutSuccZero` _eqRefutSuccZero: McBride no-confusion — `eqRefutSuccZero m e : void` refutes `e : Eq Nat (suc m) zero`. bootJ at motive `λx _. natCaseU void unit x`; base `tt` at unit, result `void` at zero._ ``` eqRefutSuccZero : Hoas -> Hoas -> Hoas -- m, eq ``` ## `eqRefutZeroSucc` _eqRefutZeroSucc: symmetric McBride no-confusion — `eqRefutZeroSucc n e : void` refutes `e : Eq Nat zero (suc n)`. bootJ at motive `λx _. natCaseU unit void x`._ ``` eqRefutZeroSucc : Hoas -> Hoas -> Hoas -- n, eq ``` ## `eqToEqDT` _eqToEqDT: bootstrap equality → datatype equality — `eqToEqDT A a b e` converts `e : bootEq A a b` to `eqDT A a b` via J transporting `reflDT` along the witness._ ``` eqToEqDT : Hoas -> Hoas -> Hoas -> Hoas -> Hoas -- A, lhs, rhs, eq ``` ## `everywhereD` _everywhereD: induction-hypothesis constructor — `everywhereD level I D K X M ih i d` builds the `allD …` witness by applying `ih` at every recursive subposition._ ``` everywhereD : Level -> Hoas -> Hoas -> Level -> Hoas -> Hoas -> Hoas -> Hoas -> Hoas -> Hoas ``` ## `false_` _false_: HOAS `Bool` constructor `false` — generated by `BoolDT`; corresponds to the left summand of bool-as-`Sum Unit Unit`._ ``` false_ : Hoas ``` ## `field` _field: HOAS datatype field declarator — `field name type` declares a non-recursive constructor field; the macro routes through `descArg`._ ``` field : String -> Hoas -> { name; type; } ``` ## `fieldAt` _fieldAt: universe-polymorphic field declarator — `fieldAt name level type` declares a field at an explicit sort level._ ``` fieldAt : String -> Level -> Hoas -> { name; type; level; } ``` ## `fieldD` _fieldD: dependent-field declarator — `fieldD name type indexFn` declares a field whose type can depend on prior fields via the index function._ ``` fieldD : String -> Hoas -> (Hoas -> Hoas) -> { name; type; indexFn; } ``` ## `fin` _fin: prelude `Fin` type family — forwarder for `FinDT.T`; `app fin n` is the type of natural numbers strictly less than `n`._ ``` fin : Hoas -- application yields fin n : Hoas ``` ## `finDesc` _finDesc: prelude `Fin` description — forwarder for `FinDT.D`, the indexed description of the finite-natural family `Fin : nat -> U`._ ``` finDesc : Hoas ``` ## `finElim` _finElim: prelude `Fin` eliminator — `finElim k P Pz Ps n x` discharges `x : fin n` against motive `P : ∀n:nat. fin n -> U(k)` with branches for fzero / fsuc._ ``` finElim : Level -> Hoas -> Hoas -> Hoas -> Hoas -> Hoas -> Hoas ``` ## `floatLit` _floatLit: HOAS Float literal — `floatLit f` lifts a Nix float to a kernel `floatLit` Tm checkable against `float_`._ ``` floatLit : Float -> Hoas ``` ## `float_` _float_: kernel-primitive `Float` type — Nix-meta floats axiomatised at the kernel level; literals enter via `floatLit`._ ``` float_ : Hoas ``` ## `fnLit` _fnLit: HOAS Function-literal marker — placeholder term checkable against `function_`; the Nix function is not embedded in the kernel term._ ``` fnLit : Hoas ``` ## `forall` _forall: HOAS Π-type former — `forall name dom body` builds `Π(name:dom). body` with `body` a Nix function receiving the bound variable as a HOAS term._ ``` forall : String -> Hoas -> (Hoas -> Hoas) -> Hoas ``` The body is a Nix function so the binder is a real Nix-level variable inside the user's scope. `elaborate` converts it to a de Bruijn `Tm` with the correct index. Chains of `forall` are elaborated iteratively via `genericClosure`, so deeply-nested Π-types are stack-safe to depths of 8000+. The `name` is cosmetic — used only in error messages and never in equality checking. ## `fst_` _fst_: HOAS Σ-pair first projection — extracts the left component; reduces by π₁ during normalisation when the argument is a `pair`._ ``` fst_ : Hoas -> Hoas ``` ## `fsuc` _fsuc: prelude `Fin` successor constructor — `fsuc k` lifts `k : fin m` to `succ k : fin (succ m)`; predecessor inferred._ ``` fsuc : Hoas -> Hoas -- fin-predecessor ``` ## `function_` _function_: kernel-primitive `Function` type — opaque Nix function axiomatised at the kernel level; literal-only entry via `fnLit`._ ``` function_ : Hoas ``` ## `functionalCompose` _functionalCompose: compose two functional ornaments `outer` over `inner`, producing a functional ornament whose section threads through both `chooseIndex`/`section` pipelines._ ``` functionalCompose : FunctionalOrnament -> FunctionalOrnament -> FunctionalOrnament ``` ## `functionalLawDiagnosticRecords` _functionalLawDiagnosticRecords: run every check in `F.laws.checks`, collecting structured diagnostics for every law that fails to evaluate or returns non-`true`; total._ ``` functionalLawDiagnosticRecords : FunctionalOrnament -> [Diagnostic] ``` ## `functionalLawDiagnostics` _functionalLawDiagnostics: human-readable string forms of `functionalLawDiagnosticRecords` for surfacing law-check failures in test output and error reports._ ``` functionalLawDiagnostics : FunctionalOrnament -> [String] ``` ## `functionalOrnament` _functionalOrnament: package `{ ornament, chooseIndex, section, indexProof?, laws?, meta? }` into a validated `FunctionalOrnament` record, the section/builder bundle that bridges base producers to ornamented outputs._ ``` functionalOrnament : { ornament, chooseIndex, section, indexProof?, laws?, meta? } -> FunctionalOrnament ``` ## `functionalOrnamentDiagnosticRecords` _functionalOrnamentDiagnosticRecords: total structured diagnostics describing every missing / ill-typed field of a candidate `functionalOrnament` spec; returns `[]` when the spec is valid._ ``` functionalOrnamentDiagnosticRecords : Attrs -> [Diagnostic] ``` ## `functionalOrnamentDiagnostics` _functionalOrnamentDiagnostics: human-readable strings derived from `functionalOrnamentDiagnosticRecords` for surfacing in error messages and test assertions._ ``` functionalOrnamentDiagnostics : Attrs -> [String] ``` ## `fzero` _fzero: prelude `Fin` zero constructor — `fzero : fin (succ m)`; predecessor inferred from the expected type._ ``` fzero : Hoas ``` ## `iff` _iff: propositional biconditional — `iff P Q := (P → Q) × (Q → P)` per HoTT Book, section 1.7. Built as `and (P→Q) (Q→P)`._ ``` iff : Hoas -> Hoas -> Hoas ``` ## `implicitApp` _Implicit application — caller passes implicit explicitly. Same kernel Tm as `app`, plus `_plicity` sidecar._ ``` Hoas -> Hoas -> Hoas ``` ## `implicitForall` _Implicit Π-binder. Same kernel Tm as `forall`, plus `_plicity` sidecar._ ``` String -> Hoas -> (Hoas -> Hoas) -> Hoas ``` ## `implicitLam` _Implicit λ-binder. Same kernel Tm as `lam`, plus `_plicity` sidecar._ ``` String -> Hoas -> (Hoas -> Hoas) -> Hoas ``` ## `ind` _ind: HOAS `Nat` induction principle wrapper — `ind k P B S n` runs `NatDT.elim k` after binding motive `P`, base `B`, and step `S` at their required types._ ``` ind : Level -> Hoas -> Hoas -> Hoas -> Hoas -> Hoas -- k, motive, base, step, scrut ``` The motive `P : nat -> U(k)` may depend on the scrutinee; for constant motives drop to `tc.verified.match` which auto- generates `λ_:nat.resultTy`. The wrapper inserts three `let_` bindings before the application spine so the kernel can infer each leg of the dependent application chain. Level `k` accepts a Nix Int, a HOAS Level term, or a kernel Tm. ## `inferHoas` _inferHoas: HOAS-driven type inference — `inferHoas termHoas` elaborates and runs the kernel `infer` rule, returning `{ term, type }` or a structured Error._ ``` inferHoas : Hoas -> { term : Tm, type : Val } | Error ``` Complement to `checkHoas` for the synthesis direction. Many HOAS forms are inference-friendly (annotated values, applications with inferable heads); pure inference fails on checking-only forms like bare lambdas or data constructors without an enclosing annotation. ## `inl` _inl: HOAS sum left-injection — `inl v` builds `Left v : Sum A B`; level, leftTy, rightTy inferred. Alias of `inlAt` post-implicit-migration._ ``` inl : Hoas -> Hoas -- value ``` ## `inr` _inr: HOAS sum right-injection — `inr v` builds `Right v : Sum A B`; level, leftTy, rightTy inferred. Alias of `inrAt` post-implicit-migration._ ``` inr : Hoas -> Hoas -- value ``` ## `intEq` _intEq: HOAS kernel int equality — `intEq a b` produces a `bool` HOAS term; reflects the `mkIntEq` primitive (host `==`)._ ``` intEq : Hoas -> Hoas -> Hoas ``` ## `intLe` _intLe: HOAS kernel int order — `intLe a b` produces a `bool` HOAS term; reflects the `mkIntLe` primitive (host `<=`)._ ``` intLe : Hoas -> Hoas -> Hoas ``` ## `intLit` _intLit: HOAS Int literal — `intLit n` lifts a Nix integer to a kernel `intLit` Tm checkable against `int_`._ ``` intLit : Int -> Hoas ``` ## `int_` _int_: kernel-primitive `Int` type — Nix-meta integers axiomatised at the kernel level; distinct from `nat`, which is the description-derived `Nat`._ ``` int_ : Hoas ``` ## `interpD` _interpD: description interpreter — `interpD level I D X i` produces the payload type `⟦D⟧ X i` at level `level`, the fibre over index `i` when the recursive position is filled by `X`._ ``` interpD : Level -> Hoas -> Hoas -> Hoas -> Hoas -> Hoas -- level, I, D, X, i ``` ## `intz` _intz: prelude `IntZ` type — `IntZDT.T`. Literature-canonical 2-constructor inductive integer with unique representation._ ``` intz : Hoas ``` ## `intzDecode` _intzDecode: sign-erasing payload extractor `intz -> Nat`. `intzPos n` and `intzNegSucc n` both yield `n`. Used as the bootJ motive discriminator in `intzPosInjective` / `intzNegSuccInjective`._ ``` intzDecode : Hoas ``` ## `intzDesc` _intzDesc: prelude `IntZ` description — `IntZDT.D`. Two-summand description with one Nat field per constructor (`pos` / `negSucc`)._ ``` intzDesc : Hoas ``` ## `intzElim` _intzElim: prelude `IntZ` eliminator — `intzElim k Q onPos onNegSucc z` runs `IntZDT.elim k`; the motive `Q : intz -> U(k)` may depend on the scrutinee. Mirrors `boolElim`'s shape with two payload-carrying case branches over a Nat field each._ ``` intzElim : Level -> Hoas -> Hoas -> Hoas -> Hoas -> Hoas -- k, motive, onPos, onNegSucc, scrut ``` ## `intzLe` _intzLe: prelude `IntZ` ordering type-family — `intzLe m n : U(0)` follows Agda `Data.Integer.Base._≤_` four cases: pos-pos delegates to Nat `le`; pos-negSucc is `void` (positives never ≤ negatives); negSucc-pos is `unit` (negatives always ≤ positives); negSucc-negSucc flips arguments and delegates back to Nat `le` (negSucc is monotonically decreasing in its argument)._ ``` intzLe : Hoas -> Hoas -> Hoas -- m, n ``` ## `intzLit` _intzLit: Nix-meta bridge from a Nix integer to `IntZ` — `intzLit n` returns `intzPos (natLit n)` for `n >= 0` and `intzNegSucc (natLit (-n - 1))` for `n < 0`. Boundary cases: `intzLit 0 = intzPos 0`; `intzLit (-1) = intzNegSucc 0`._ ``` intzLit : Int -> Hoas ``` ## `intzNegSucc` _intzNegSucc: prelude `IntZ` negative constructor — `intzNegSucc n : intz` encodes the integer `-(n+1)` for `n : Nat`. `intzNegSucc 0` is `-1`._ ``` intzNegSucc : Hoas -> Hoas -- n ``` ## `intzNegSuccCong` _intzNegSuccCong: congruence of `intzNegSucc` — `intzNegSuccCong m n e : Eq IntZ (negSucc m) (negSucc n)` lifts `e : Eq Nat m n`._ ``` intzNegSuccCong : Hoas -> Hoas -> Hoas -> Hoas -- m, n, natEq ``` ## `intzNegSuccInjective` _intzNegSuccInjective: symmetric injectivity of `intzNegSucc` via the same `intzDecode` motive._ ``` intzNegSuccInjective : Hoas -> Hoas -> Hoas -> Hoas -- m, n, intzEq ``` ## `intzPos` _intzPos: prelude `IntZ` non-negative constructor — `intzPos n : intz` encodes the integer `n` for `n : Nat`. `intzPos 0` is the canonical zero._ ``` intzPos : Hoas -> Hoas -- n ``` ## `intzPosCong` _intzPosCong: congruence of `intzPos` — `intzPosCong m n e : Eq IntZ (pos m) (pos n)` lifts `e : Eq Nat m n`. bootJ at motive `λx _. Eq IntZ (pos m) (pos x)`; mirrors `eqCongSucc`._ ``` intzPosCong : Hoas -> Hoas -> Hoas -> Hoas -- m, n, natEq ``` ## `intzPosInjective` _intzPosInjective: injectivity of `intzPos` — `intzPosInjective m n e : Eq Nat m n` from `e : Eq IntZ (pos m) (pos n)`. bootJ at motive `λx _. Eq Nat m (intzDecode x)`; β on the decoder collapses base case to `Eq Nat m m`._ ``` intzPosInjective : Hoas -> Hoas -> Hoas -> Hoas -- m, n, intzEq ``` ## `isLeafOrn` _isLeafOrn: predicate identifying leaf functional ornaments via the `_leafOrnTag = "leaf-ornament"` tag._ ``` isLeafOrn : Value -> Bool ``` ## `j` _j: HOAS J-eliminator for `EqDT` — `j A a P base b e` discharges `e : EqDT A a b` against motive `P : ∀x:A. EqDT A a x -> U`, computing the goal type at `b`._ ``` j : Hoas -> Hoas -> Hoas -> Hoas -> Hoas -> Hoas -> Hoas -- type, lhs, motive, base, rhs, eq ``` The classic Martin-Löf J: given a base case at the diagonal (`base : P a refl`), transports along any equality. Used by `transNat`, `congSuc`, `maxSucDom`, `eqToEqDT`, etc. The motive must accept both the right-hand side and the equality term; for substitution-only patterns, write a constant-in-eq motive `λx _. ...`. ## `just` _just: HOAS Maybe just-injection — `just innerTy v` is `inl v : maybe innerTy`._ ``` just : Hoas -> Hoas -> Hoas -- innerTy, value ``` ## `justAt` _justAt: universe-polymorphic Maybe just-injection — `justAt k innerTy v` is `inl v : maybeAt k innerTy`._ ``` justAt : Level -> Hoas -> Hoas -> Hoas -- level, innerTy, value ``` ## `lam` _lam: HOAS lambda — `lam name dom body` builds `λ(name:dom). body` with `body` a Nix function receiving the bound variable; the principal term-binding form._ ``` lam : String -> Hoas -> (Hoas -> Hoas) -> Hoas ``` Body is a Nix function, so the variable binding flows through Nix's own scope. `elaborate` produces a de Bruijn `Tm` with index 0 for the innermost binder. Chains of `lam` are elaborated iteratively (stack-safe to 8000+). Use `opaqueLam` when wrapping a non-HOAS Nix function for which kernel checking would otherwise require recovering structure. ## `le` _le: prelude `Le` curried type family — `le m n` is the type of proofs that `m ≤ n` over `Nat`. The Agda `Data.Nat.Base._≤_` inductive predicate (z≤n / s≤s constructors)._ ``` le : Hoas -> Hoas -> Hoas -- m, n ``` ## `leDesc` _leDesc: prelude `Le` description — forwarder for `LeDT.D`, the indexed description of the order family `Le : Σ Nat (_: Nat) -> U` (curried at surface as `le m n`)._ ``` leDesc : Hoas ``` ## `leElim` _leElim: prelude `Le` eliminator with curried-motive adapter — `leElim K P Pz Ps m n pf` discharges `pf : le m n` against motive `P : (m n : nat) -> le m n -> U(K)` with branches for `leZ` / `leSS`. Internally adapts the Σ-indexed motive of `LeDT.elim`._ ``` leElim : Level -> Hoas -> Hoas -> Hoas -> Hoas -> Hoas -> Hoas -> Hoas ``` ## `leInjSS` _leInjSS: injectivity of `leSS` — `leInjSS m n pf : Le m n` given `pf : Le (suc m) (suc n)`. leElim at motive `λm' n' _. Le (predNat m') (predNat n')`; leZ fills via `leZ ∘ predNat`, leSS fills with the recursive witness directly (via β on `predNat (suc _)`)._ ``` leInjSS : Hoas -> Hoas -> Hoas -> Hoas -- m, n, leSuccSucc ``` ## `leRefutSuccZero` _leRefutSuccZero: refutation of `Le (suc m) zero` — leElim at motive `λm' n' _. natCaseU unit (natCaseU void unit n') m'`; both leZ and leSS case targets collapse to `unit`, while the refutation target at (suc m, 0) collapses to `void`._ ``` leRefutSuccZero : Hoas -> Hoas -> Hoas -- m, leProof ``` ## `leSS` _leSS: prelude `Le` step constructor (s≤s) — `leSS lemn : Le (suc m) (suc n)` for `lemn : Le m n`; `m`, `n` inferred. Lifts ≤-witnesses through simultaneous successor._ ``` leSS : Hoas -> Hoas -- lemn ``` ## `leZ` _leZ: prelude `Le` base constructor (z≤n) — `leZ : Le 0 n`; bound `n` inferred. Decidability witness for `decideLeNat 0 n`._ ``` leZ : Hoas ``` ## `leafOrnament` _leafOrnament: constructor for a *leaf functional ornament* — a refinement of a primitive HOAS type former (one with `_htag ≠ "mu"`) carrying Nix-meta `forget : Refined → Base` and `section : Base → Refined`. The leaf case is the literature-faithful specialisation of Dagand–McBride 2014 (JFP) functional ornaments to type formers without a μ-encoded description. The canonical instance is `thunkOrnament`._ ``` leafOrnament : { primitive : HoasType, forget : Refined -> Base, section : Base -> Refined, sectionProof? : Refined -> Bool, meta? : Attrs } -> LeafOrnament ``` ## `leafOrnamentDiagnosticRecords` _leafOrnamentDiagnosticRecords: total structured diagnostics for a candidate leaf-ornament spec — codes `leaf.invalid-spec | leaf.missing-{primitive,forget,section} | leaf.invalid-{primitive,forget,section,section-proof}`._ ``` leafOrnamentDiagnosticRecords : Attrs -> [Diagnostic] ``` ## `leafOrnamentDiagnostics` _leafOrnamentDiagnostics: human-readable text forms of `leafOrnamentDiagnosticRecords` for surfacing in error messages and test assertions._ ``` leafOrnamentDiagnostics : Attrs -> [String] ``` ## `let_` _let_: HOAS let binding — `let_ name ty val body` builds `let name : ty = val in body` with `body` a Nix function receiving the bound variable._ ``` let_ : String -> Hoas -> Hoas -> (Hoas -> Hoas) -> Hoas ``` Used internally by eliminator wrappers (`ind`, `listElim`, `sumElim`) to bind motive / base / step at their required types before the application spine, making each subterm inferable. User code typically uses `let_` to share subexpressions or to name intermediate values for clarity. ## `level` _level: HOAS universe-level type former — inhabits `U(0)`; lets users quantify over universes via `forall "k" level (k: …)` and build level expressions inline._ ``` level : Hoas ``` ## `levelMax` _levelMax: HOAS Level join — `levelMax l m` produces `max(l, m)`; the semilattice operation used by `descArg` / `descPi` and the universe of dependent products._ ``` levelMax : Hoas -> Hoas -> Hoas ``` ## `levelSuc` _levelSuc: HOAS Level successor — `levelSuc l` produces `l + 1`; used to build closed level expressions and inside `congSuc` for Eq-on-Level transport._ ``` levelSuc : Hoas -> Hoas ``` ## `levelZero` _levelZero: HOAS Level constant `0` — the base universe level; combines with `levelSuc` / `levelMax` to form arbitrary closed level expressions._ ``` levelZero : Hoas ``` ## `listDesc` _listDesc: prelude `List` description constructor — `listDesc A` produces the two-summand description `descRet + descArg A (_: descRec descRet)` of `List A`._ ``` listDesc : Hoas -> Hoas ``` ## `listElim` _listElim: HOAS list eliminator wrapper — `listElim k A P N C xs` runs `ListDT.elim k A` after binding motive / nil-branch / cons-step at their required types._ ``` listElim : Level -> Hoas -> Hoas -> Hoas -> Hoas -> Hoas -> Hoas -- k, A, motive, onNil, onCons, scrut ``` The cons step has type `∀h:A. ∀t:listOf A. ∀_:P t. P (cons h t)` — the inductive hypothesis is the recursive call's result. For constant motives use `tc.verified.matchList`. The level argument threads into the motive's codomain universe. ## `listOf` _listOf: HOAS list type former — `listOf elemTy` is `μ Unit listDesc tt` at element type `elemTy`; the description-derived counterpart to Nix-native lists._ ``` listOf : Hoas -> Hoas ``` ## `listOfAt` _listOfAt: universe-polymorphic list type former — `listOfAt level A` builds `List A` at the given universe level, used when the homogeneous-level `listOf` cannot carry an element type above U(0)._ ``` listOfAt : Hoas -> Hoas -> Hoas -- level, A ``` ## `litVal` _litVal: closed-Val splice — `litVal v` reflects a closed kernel Val into HOAS without structural reconstruction; `eval ρ (mkLitVal v) = v` independent of ρ. O(1) Val→HOAS lift, contrasted with `embedTm (quote 0 v)` which is O(size v)._ ``` litVal : Val -> Hoas ``` Splice in the sense of two-level type theory (Kovács, POPL 2024; Annenkov–Capriotti–Kraus–Sattler 2019): reflects a value from the semantic domain back into syntax with eval as the identity. Sound iff the carried Val is closed — `eval` discards the environment, so any free de Bruijn level would never resolve. The canonical Val→HOAS lift in the bridge; replaces the `embedTm (quote 0 v)` composition when v will only be re-evaluated. ## `lower` _lower: depth-parameterised HOAS-to-Tm compiler — `lower depth h` converts a HOAS term to its de Bruijn `Tm` representation; `depth` is the binding level at the call site._ ``` lower : Int -> Hoas -> Tm ``` The principal HOAS-side compilation entry. Binding chains are lowered iteratively via `genericClosure` for stack safety to 8000+ depth. Use directly when controlling the binding depth (e.g. when re-lowering an open HOAS subterm). For the closed-term case, prefer `elab` which fixes `depth = 0`. ## `maxSucDom` _maxSucDom: Level-max-suc transport — proof of `∀a b. Eq Level (max a b) b -> Eq Level (max a (suc b)) (suc b)`; lifts max-bound witness through suc on the right operand._ ``` maxSucDom : Hoas ``` ## `maybe` _maybe: HOAS optional type — `maybe inner` is `Sum inner Unit`; left payload carries a value, right marks absence._ ``` maybe : Hoas -> Hoas ``` ## `maybeAt` _maybeAt: universe-polymorphic optional — `maybeAt k inner` is `Sum inner (LiftAt 0 k Unit)` at level k; `maybe` is the level-zero default._ ``` maybeAt : Level -> Hoas -> Hoas -- level, inner ``` ## `mu` _mu: ⊤-slice inductive carrier — alias for `muI unitPrim D i`; the unit-indexed special case used by prelude datatypes like nat, list, bool._ ``` mu : Hoas -> Hoas -> Hoas -- D, i ``` ## `nat` _nat: HOAS `Nat` type former — generated by `NatDT.T` from the macro-derived natural-number datatype; serves as the index sort for vector / list-length families._ ``` nat : Hoas ``` ## `natCaseU` _natCaseU: nat-case at the universe level — `natCaseU A B` is `λn:nat. case n { zero => A; succ _ => B; }`; built on `descInd nat.D` with sum-branching on the per-constructor payload._ ``` natCaseU : Hoas -> Hoas -> Hoas -- zeroBranch, succBranch ``` The discriminator's result type is `U(0)` regardless of scrutinee; the inductive hypothesis is discarded since discrimination is value-shape-driven, not recursive. Used by `vhead` to make the vnil/vcons branches target different types (`unit` vs the element type), and by `absurdFin0` to target the goal type at zero and `Unit` at succ. ## `natDesc` _natDesc: prelude `Nat` description — `descRet + descRec descRet`; the canonical two-summand description of natural numbers as zero + succ(Nat)._ ``` natDesc : Hoas ``` ## `natLit` _natLit: Nix integer to HOAS Nat — `natLit n` builds `succ^n zero` as a HOAS term; convenience wrapper around iterated `succ` application._ ``` natLit : Int -> Hoas ``` ## `natPredCase` _natPredCase: nat-case with predecessor-dependent succ branch — `natPredCase A n` returns `unit` at zero and `vec A m` at `succ m`; generalises `natCaseU` to payload-dependent succ cases._ ``` natPredCase : Hoas -> Hoas -- elemTy ``` Used by `vtail` to build the vecElim motive `P n xs = natPredCase A n` so the vnil branch targets `unit` (filled by `tt`) and the vcons branch targets `vec A pred` (filled by the tail). Implementation extracts the predecessor via `fst_ r` on the inr summand. ## `natToLevel` _natToLevel: meta-level integer-to-Level helper — `natToLevel n` produces `levelSuc^n levelZero`; throws on non-Int input; emits closed Level syntax only._ ``` natToLevel : Int -> Hoas ``` ## `nil` _nil: HOAS empty-list constructor — `nil : listOf A`; element type inferred from the expected type._ ``` nil : Hoas ``` ## `no` _no: negative decidability witness — `no P r : dec P` for a refutation `r : not P`. Routes through the right injection of `P ⊎ ¬ P`._ ``` no : Hoas -> Hoas -> Hoas -- P, refutation ``` ## `not` _not: propositional negation — `not P := P → ⊥` per HoTT Book, section 1.7. Elaborates to a pi with `void` codomain._ ``` not : Hoas -> Hoas ``` ## `nothing` _nothing: HOAS Maybe none-injection — `nothing innerTy` is `inr tt : maybe innerTy`._ ``` nothing : Hoas -> Hoas -- innerTy ``` ## `nothingAt` _nothingAt: universe-polymorphic Maybe none-injection — `nothingAt k innerTy` is `inr (lift tt) : maybeAt k innerTy`._ ``` nothingAt : Level -> Hoas -> Hoas -- level, innerTy ``` ## `opaqueLam` _opaqueLam: HOAS opaque-lambda wrapper — `opaqueLam nixFn piHoas` packages a non-HOAS Nix function with its declared Π-type; the kernel treats the body as an unverified trust boundary._ ``` opaqueLam : (Any -> Any) -> Hoas -> Hoas ``` Use only when the body cannot be expressed as HOAS — e.g. wrapping a recursion-irregular helper produced outside the kernel. The elaborator emits `mkOpaqueLam`; the type-checker cannot recover the body's HOAS shape, so it accepts the declared `piHoas` without re-validation. Prefer `verifiedFn` from `tc.verified` when the body IS expressible as HOAS — that path keeps body verification. ## `or_` _or_: propositional disjunction — `or_ P Q := P + Q` per HoTT Book, section 1.7. Trailing underscore avoids the Nix `or` keyword in identifier position (mirrors `true_` / `false_`)._ ``` or_ : Hoas -> Hoas -> Hoas ``` ## `ornArgInsert` _ornArgInsert: argInsert-node spec for `ornI.node` — inserts a fresh `arg S` field absent from the base description; body binds the inserted witness recursively._ ``` ornArgInsert : Tm -> (Tm -> OrnNode) -> { tag = "argInsert"; S; body } ``` ## `ornArgKeep` _ornArgKeep: argKeep-node spec for `ornI.node` — keeps a base `descArg S body` field unchanged in the ornament; body re-binds the same `s : S` recursively._ ``` ornArgKeep : Tm -> (Tm -> OrnNode) -> { tag = "argKeep"; S; body } ``` ## `ornBuild` _ornBuild: build the ornamented value at index `i` from `baseValue` by invoking `F.section`; the canonical entry point for ornament construction from a base witness._ ``` ornBuild : FunctionalOrnament -> Tm -> Tm -> Tm ``` ## `ornCompose` _ornCompose: vertical composition of ornaments — given `inner : I -> J` and `outer : J -> K`, produce `outer ∘ inner : I -> K`; sequential refinement._ ``` ornCompose : Ornament -> Ornament -> Ornament -- outer, inner ``` ## `ornDesc` _ornDesc: compile an `Ornament` to its annotated `Desc J` term — the levitated description of the ornamented datatype, ready for `muI` / `interpD`._ ``` ornDesc : Ornament -> Tm -- Desc J ``` ## `ornForget` _ornForget: produce the forgetting morphism `mu (ornDesc O) ~> mu (baseD O)`, mapping every ornamented value back to its underlying base-description value._ ``` ornForget : Ornament -> Tm -- J -> ornMu -> baseMu ``` ## `ornI` _ornI: master constructor of an `Ornament` over a base description, packaging `{ I, J, erase, baseD, node }` and optional level/meta into the canonical ornament record consumed by `ornDesc` / `ornMu` / `ornForget`._ ``` ornI : { I, J, erase, baseD, node, level?, meta? } -> Ornament ``` ## `ornId` _ornId: identity ornament on `D : Desc I` — `forget` is the identity at every index; the unit of `ornCompose`. Recovers `D` as its own ornament._ ``` ornId : Tm -> Tm -> Ornament -- I, D ``` ## `ornIndexProof` _ornIndexProof: extract the `indexProof` slot of a functional ornament — the proof `i -> baseValue -> erase (chooseIndex i baseValue) ≡ i` certifying the section commutes with forget._ ``` ornIndexProof : FunctionalOrnament -> (Tm -> Tm -> Tm) ``` ## `ornLiftFold` _ornLiftFold: alias for `ornPullback` specialised to folds — composes a base fold with `ornForget` so it runs on ornamented carriers without re-deriving the algebra._ ``` ornLiftFold : Ornament -> (Tm -> Tm) -> Tm -> Tm -- resultTy, baseFold -> lifted ``` ## `ornLiftProducer` _ornLiftProducer: lift a base producer `baseFn` through a functional ornament `F` — run the producer on the base input, then build the ornamented output via `F.section`._ ``` ornLiftProducer : FunctionalOrnament -> (Tm -> Tm) -> Tm -> Tm -> Tm ``` ## `ornLiftTransform` _ornLiftTransform: lift a base transform through paired input/output functional ornaments — forget the ornamented input, run the base transform, build the ornamented output._ ``` ornLiftTransform : { input : OrnLike, output : FunctionalOrnament, fn } -> Tm -> Tm -> Tm -> Tm ``` ## `ornMu` _ornMu: build the ornamented `mu` at index `j`, i.e. `muI J (ornDesc O) j` — the carrier type of values living over the ornament at that index._ ``` ornMu : Ornament -> Tm -> Tm -- index in J yields kernel type ``` ## `ornPiKeep` _ornPiKeep: piKeep-node spec for `ornI.node` — keeps a Π-quantified field over `S`; `branch` supplies the base/ornament index functions and proof, defaulting to identity._ ``` ornPiKeep : Tm -> (Tm | { baseF, ornF, proof }) -> OrnNode -> { tag = "piKeep"; S; tail; baseF; ornF; proof; l } ``` ## `ornPlus` _ornPlus: plus-node spec for `ornI.node` — sum of two ornament arms `left` and `right`, mirroring `descPlus` at the ornament level._ ``` ornPlus : OrnNode -> OrnNode -> { tag = "plus"; left; right } ``` ## `ornPullback` _ornPullback: transport a base program `baseFn : I -> baseMu -> R(i)` along `ornForget`, yielding the same program over ornamented inputs at every J index._ ``` ornPullback : Ornament -> (Tm -> Tm) -> Tm -> Tm -- resultTy, baseFn -> lifted ``` ## `ornRec` _ornRec: rec-node spec for `ornI.node` — a recursive child at index `j` with proof `erase j ≡ baseIndex` and a `tail` ornament for the remainder of the constructor._ ``` ornRec : Tm -> (Tm | { proof, baseIndex }) -> OrnNode -> { tag = "rec"; j; proof; baseIndex; tail } ``` ## `ornRet` _ornRet: ret-node spec for `ornI.node` — terminates an ornament arm at index `j` in J with a base-index witness `baseIndex` and proof `erase j ≡ baseIndex`._ ``` ornRet : Tm -> Tm -> Tm -> { tag = "ret"; j; proof; baseIndex } ``` ## `ornSection` _ornSection: extract the `section` builder from a functional ornament — the function `i -> baseValue -> ornamentedValue` that realises the section morphism._ ``` ornSection : FunctionalOrnament -> (Tm -> Tm -> Tm) ``` ## `ornTargetIndex` _ornTargetIndex: extract the `chooseIndex` slot of a functional ornament — the function `i -> baseValue -> J` that picks the ornamented index for each base input._ ``` ornTargetIndex : FunctionalOrnament -> (Tm -> Tm -> Tm) ``` ## `ornament` _ornament: master surface — given a monomorphic generated `DataSpec` (`base`) and a constructor-by-constructor `spec`, produce the ornamented `Datatype` plus `forget` morphism._ ``` ornament : DataSpec -> OrnamentSpec -> Datatype ``` ## `ornamentDiagnosticRecords` _ornamentDiagnosticRecords: total structured diagnostics covering every surface error in an ornament spec — missing constructors, unknown arms, malformed fields, out-of-order keeps._ ``` ornamentDiagnosticRecords : DataSpec -> OrnamentSpec -> [Diagnostic] ``` ## `ornamentDiagnostics` _ornamentDiagnostics: human-readable text forms of `ornamentDiagnosticRecords` for surfacing in error messages and test assertions._ ``` ornamentDiagnostics : DataSpec -> OrnamentSpec -> [String] ``` ## `pair` _pair: HOAS Σ-pair introduction — `pair fst snd` packages two HOAS values; the surrounding type annotation pins which Σ-type the pair inhabits._ ``` pair : Hoas -> Hoas -> Hoas ``` ## `path` _path: kernel-primitive `Path` type — opaque Nix path axiomatised at the kernel level; literal-only entry via `pathLit`._ ``` path : Hoas ``` ## `pathLit` _pathLit: HOAS Path-literal marker — placeholder term checkable against `path`; the Nix path is not embedded in the kernel term._ ``` pathLit : Hoas ``` ## `piField` _piField: Π-typed field declarator — `piField name sort body` declares a function-typed field where every element of `sort` indexes into the body description._ ``` piField : String -> Hoas -> (Hoas -> Hoas) -> { name; sort; body; pi; } ``` ## `piFieldD` _piFieldD: dependent Π-typed field — `piFieldD` extends `piField` with the ability for the body to depend on prior constructor fields._ ``` piFieldD : String -> Hoas -> (Hoas -> Hoas -> Hoas) -> { name; sort; body; pi; indexFn; } ``` ## `plicity` _Plicity tag namespace: explicit/implicit values + predicates._ ``` { explicit : Plicity; implicit : Plicity; isPlicity : Any -> Bool; isImplicit : Plicity -> Bool; } ``` ## `plus` _plus: ⊤-slice description sum — alias for `plusI unitPrim 0 A B`; the standard form for combining prelude descriptions like nat = retI + recI(retI)._ ``` plus : Hoas -> Hoas -> Hoas ``` ## `predNat` _predNat: saturating Nat predecessor — `predNat zero ≡ zero`, `predNat (suc m) ≡ m`. Built via `ind` with constant `nat` motive. Consumed by `eqInjSucc` and `leInjSS`._ ``` predNat : Hoas -- closed function nat -> nat ``` ## `product` _product: HOAS named single-constructor μ-datatype — `product name [H.field …]` is sugar for `datatype name [(con name fields)]`; returns the full DataSpec so deriveSchema/deriveDescriptor and validateValue work unchanged._ ``` product : String -> [Field] -> DataSpec ``` The named parallel to `record` (anonymous, `.T`-only) and `variant` (n-con sum). The single constructor's name is the type name, since a product has no constructor to disambiguate. Fields project flat, with no inner `value` wrapper. Use when the domain type has a stable user-supplied name and consumers need the full DataSpec (`.T`, `.cons`, `.` ctor, `.elim`). ## `recField` _recField: recursive-field declarator — `recField name indexFn` declares a recursive position whose target index is computed from prior fields._ ``` recField : String -> (Hoas -> Hoas) -> { name; indexFn; recursive; } ``` Marks the field as a recursive child via `_recursive = true`, routing through `descRec` rather than `descArg` in the description-emission step. The `indexFn` extracts the target index from prior fields, supporting indexed datatypes whose recursive children reference different indices. ## `recFieldAt` _recFieldAt: universe-polymorphic recursive-field declarator — `recFieldAt name level indexFn` with explicit level for the recursive position._ ``` recFieldAt : String -> Level -> (Hoas -> Hoas) -> { name; indexFn; recursive; level; } ``` ## `record` _record: HOAS record type — `record [{name; type}…]` builds a mono-constructor μ-datatype whose single constructor takes the listed fields in order; surface for `v.field` projection._ ``` record : [{ name : String; type : Hoas; }] -> Hoas ``` Compiled to a mono-constructor μ-datatype carrying `_dtypeMeta` with one constructor named `mk` whose fields match the input list. The type's canonical name is `Record{fieldName1, …}` sorted alphabetically. Use `v.field recordTy "name" record` to project a field by name; the eliminator is derived once and reused across projections. ## `refinementPred` _refinementPred: Σ-encoded refinement-predicate carrier `refinementPred dom predFn = Σ (x : dom) (Dec (predFn x))`. McBride & McKinna 2004 'The view from the left'. `predFn` is a Nix-meta `Hoas -> Hoas` predicate following the surface convention of `sigma` / `forall` / `lam`. Inhabitants pair a value with a decision proof; the decision procedure is supplied at the value level rather than encoded in the type._ ``` refinementPred : Hoas -> (Hoas -> Hoas) -> Hoas -- domain, predFn ``` ## `refl` _refl: HOAS reflexivity introduction for `EqDT` — emitted in check-mode against a goal `EqDT A a a`; the elaborator handles the level/index inference._ ``` refl : Hoas ``` ## `reflDT` _reflDT: prelude equality reflexivity — `reflDT A a : eqDT A a a`; the canonical inhabitant of the diagonal._ ``` reflDT : Hoas -> Hoas -> Hoas -- A, value ``` ## `reifyLevel` _reifyLevel: HOAS Level → kernel Level Tm — converts a HOAS-side level expression (Int, level term, or already-reified Tm) to the kernel's canonical Level representation._ ``` reifyLevel : Level -> Tm ``` ## `retI` _retI: indexed retI-constructor — `retI I k j` builds a `Desc^k I` leaf returning index `j`; level-polymorphic over `k`._ ``` retI : Hoas -> Level -> Hoas -> Hoas -- I, k, targetIndex ``` ## `sigma` _sigma: HOAS Σ-type former — `sigma name fst body` builds `Σ(name:fst). body`; the dependent-pair counterpart to `forall`._ ``` sigma : String -> Hoas -> (Hoas -> Hoas) -> Hoas ``` Stack-safe like `forall` (iterative `genericClosure` elaboration). Pairs enter via `pair`, project via `fst_` / `snd_`. For records of more than two fields, prefer `record` which is encoded as a mono-constructor μ-datatype with named projections via `v.field`. ## `signsDiffer` _signsDiffer: no-confusion refutation `(m n : Nat) -> Eq IntZ (pos m) (negSucc n) -> void`. McBride 2000 PhD, section 3.5 discriminator-motive technique applied to the IntZ sign discriminator (`unit` on `pos`, `void` on `negSucc`) plus `bootJ` transport._ ``` signsDiffer : Hoas -> Hoas -> Hoas -> Hoas -- m, n, e ``` ## `signsDifferRev` _signsDifferRev: symmetric no-confusion refutation `(m n : Nat) -> Eq IntZ (negSucc m) (pos n) -> void`. Same discriminator-motive technique as `signsDiffer` with the `pos` / `negSucc` targets swapped._ ``` signsDifferRev : Hoas -> Hoas -> Hoas -> Hoas -- m, n, e ``` ## `snd_` _snd_: HOAS Σ-pair second projection — extracts the right component; reduces by π₂ during normalisation when the argument is a `pair`._ ``` snd_ : Hoas -> Hoas ``` ## `sourceMapOf` _sourceMapOf: HOAS surface → SourceMap walker — produces a structural map from the HOAS term's positions to source-form metadata; consumed by the diagnostic shell to associate errors with source positions._ ``` sourceMapOf : Hoas -> SourceMap ``` ## `squash` _squash: HOAS propositional truncation — `squash A` is the type `‖A‖` whose elements are all conv-equal; collapses A's content to mere inhabitation._ ``` squash : Hoas -> Hoas ``` Two `squashIntro _` values inhabiting the same `squash A` are conv-equal by definitional irrelevance — equality holds without inspecting payloads. Use to express subsingleton propositions or to enforce proof-irrelevance on otherwise relevant data. ## `squashElim` _squashElim: HOAS truncation eliminator — `squashElim A B f x` lifts `f : A -> squash B` over `x : squash A`; restricted to `squash`-typed motives so irrelevance is preserved._ ``` squashElim : Hoas -> Hoas -> Hoas -> Hoas -> Hoas -- A, B, f, x ``` Restricted to motives whose target is `squash B` — eliminating out of a squash into a relevant type is forbidden (would violate irrelevance). For relevant-target elimination, the only path is constructing a derivation that requires no inspection of the squashed value. ## `squashIntro` _squashIntro: HOAS truncation introduction — `squashIntro a` wraps `a : A` into `squash A`; the resulting witness ignores the underlying value under conv._ ``` squashIntro : Hoas -> Hoas ``` ## `strEq` _strEq: HOAS kernel string equality — `strEq a b` produces a `bool` HOAS term; reflects the `mkStrEq` primitive of the kernel._ ``` strEq : Hoas -> Hoas -> Hoas ``` ## `strLen` _strLen: HOAS kernel string length — `strLen s` produces an `int_` HOAS term; reflects the `mkStrLen` primitive (host string length)._ ``` strLen : Hoas -> Hoas ``` ## `string` _string: kernel-primitive `String` type — axiomatised; literals enter via `stringLit`, equality is decidable via `strEq`._ ``` string : Hoas ``` ## `stringLit` _stringLit: HOAS String literal — `stringLit s` lifts a Nix string to a kernel `stringLit` Tm checkable against `string`._ ``` stringLit : String -> Hoas ``` ## `succ` _succ: HOAS `Nat` successor — `succ n` builds `n + 1`; introduces `nat` at the `descRec descRet` summand._ ``` succ : Hoas -> Hoas ``` ## `sum` _sum: HOAS coproduct type former — `sum A B` builds `A + B` via `SumDT.T` at the implicit base level; inhabitants enter via `inl` / `inr`._ ``` sum : Hoas -> Hoas -> Hoas ``` ## `sumDesc` _sumDesc: prelude `Sum` description constructor — `sumDesc A B` produces the two-summand description `descArg A (_: descRet) + descArg B (_: descRet)` of `Sum A B`._ ``` sumDesc : Hoas -> Hoas -> Hoas ``` ## `sumElim` _sumElim: HOAS sum eliminator wrapper — `sumElim k A B P L R s` runs `SumDT.elim k` at the base universe level; `L` and `R` are dependent on the injected payload._ ``` sumElim : Level -> Hoas -> Hoas -> Hoas -> Hoas -> Hoas -> Hoas -> Hoas -- k, A, B, motive, onLeft, onRight, scrut ``` Calls `sumElimAt levelZero …` for the homogeneous-level common case. For sums constructed via `inlAt` / `inrAt` at a non-zero level, use `sumElimAt` directly with the matching level. For constant motives use `tc.verified.matchSum`. ## `sup` _sup: HOAS W-type constructor `sup s t` — supplies the node-shape `s` and the recursive-children family `t : pos s → W`._ ``` sup : Hoas -> Hoas -> Hoas ``` ## `surfacePlicity` _Read the plicity sidecar, defaulting to explicit._ ``` Any -> Plicity ``` ## `thunk` _thunk: generic deepSeq-safe carrier type former — `thunk a` is `{ _tag = "Thunk"; _force = _: a }`. Lazy structural check: walker verifies `_force` is a closure but does NOT invoke it; inner-type validation runs post-forget. Forget map: `t._force null`. Use as a payload type wherever values must survive trampoline `deepSeq` while remaining recoverable._ ``` thunk : Hoas -> Hoas ``` ## `thunkOrnament` _thunkOrnament: derived leaf-functional-ornament constructor for `H.thunk inner` with `forget = forceThunk` and `section = mkThunk`. The functional ornament induced by `forceThunk : Thunk A → A` per Dagand–McBride 2014 (JFP), section 3, specialised to the primitive Thunk carrier from `fx.state.thunk`._ ``` thunkOrnament : HoasType -> LeafOrnament ``` ## `trans` _trans: User-Eq transitivity — proof of `∀A x y z. Eq A x y -> Eq A y z -> Eq A x z`; one J application transporting `pxy` along `pyz`. Chains with `cong` to build diagram-chase witnesses without expanding either equality's RHS through the kernel._ ``` trans : Hoas -> Hoas -> Hoas -> Hoas -> Hoas -> Hoas -> Hoas -- A, x, y, z, pxy, pyz ``` ## `true_` _true_: HOAS `Bool` constructor `true` — generated by `BoolDT`; corresponds to the right summand of bool-as-`Sum Unit Unit`._ ``` true_ : Hoas ``` ## `tryAlgOrn` _tryAlgOrn: total constructor — returns `{ ok, diagnostics, value? }` where `value` is the `algOrn`-built ornament when `ok`, otherwise diagnostics-only._ ``` tryAlgOrn : Attrs -> { ok : Bool, diagnostics : [Diagnostic], value? : Ornament } ``` ## `tryFunctionalOrnament` _tryFunctionalOrnament: total constructor — returns `{ ok, diagnostics, value? }` where `value` is the built functional ornament when `ok`, otherwise diagnostics-only._ ``` tryFunctionalOrnament : Attrs -> { ok : Bool, diagnostics : [Diagnostic], value? : FunctionalOrnament } ``` ## `tryLeafOrnament` _tryLeafOrnament: total constructor — returns `{ ok, diagnostics, value? }` where `value` is the built leaf ornament when `ok`, otherwise diagnostics-only._ ``` tryLeafOrnament : Attrs -> { ok : Bool, diagnostics : [Diagnostic], value? : LeafOrnament } ``` ## `tryOrnament` _tryOrnament: total constructor — returns `{ ok, diagnostics, value? }` where `value` is the built ornamented datatype when `ok`, otherwise diagnostics-only._ ``` tryOrnament : DataSpec -> OrnamentSpec -> { ok : Bool, diagnostics : [Diagnostic], value? : Datatype } ``` ## `tt` _tt: HOAS unit value — the unique inhabitant of `unit`; ⊤-slice convention places this at every retI leaf of description scaffolding._ ``` tt : Hoas ``` ## `u` _u: HOAS universe former — `u level` builds `U(level)`, the universe of types at the given level; `level` accepts a Nix Int, a HOAS Level term, or a kernel Tm._ ``` u : Level -> Hoas ``` ## `unit` _unit: HOAS unit type — alias for the kernel-primitive `unitPrim`; the ⊤-slice index sort for description machinery and refinement bound markers._ ``` unit : Hoas ``` ## `validateAlgOrn` _validateAlgOrn: total predicate `{ ok, diagnostics }` over a candidate `algOrn` spec — checks every algebra arm against its description shape without throwing._ ``` validateAlgOrn : Attrs -> { ok : Bool, diagnostics : [Diagnostic] } ``` ## `validateFunctionalLaws` _validateFunctionalLaws: total predicate `{ ok, diagnostics }` over a functional ornament's law-check bundle; reports which checks failed without throwing._ ``` validateFunctionalLaws : FunctionalOrnament -> { ok : Bool, diagnostics : [Diagnostic] } ``` ## `validateFunctionalOrnament` _validateFunctionalOrnament: total predicate over candidate functional-ornament specs returning `{ ok, diagnostics }`; never throws, intended for upstream `try*` and surface validators._ ``` validateFunctionalOrnament : Attrs -> { ok : Bool, diagnostics : [Diagnostic] } ``` ## `validateLeafOrnament` _validateLeafOrnament: total predicate `{ ok, diagnostics }` over a candidate leaf-ornament spec; rejects μ-encoded primitives, already-ornamented primitives, and missing/malformed forget/section/sectionProof fields._ ``` validateLeafOrnament : Attrs -> { ok : Bool, diagnostics : [Diagnostic] } ``` ## `validateOrnament` _validateOrnament: total predicate `{ ok, diagnostics }` over `(base, spec)` for the user-facing `ornament` surface; never throws._ ``` validateOrnament : DataSpec -> OrnamentSpec -> { ok : Bool, diagnostics : [Diagnostic] } ``` ## `variant` _variant: HOAS tagged-union type — `variant [{tag; type}…]` builds an n-constructor μ-datatype whose tags become single-field constructor names._ ``` variant : [{ tag : String; type : Hoas; }] -> Hoas ``` Each branch becomes a single-field constructor named after the tag, so a value `{ _tag = "Left"; value = v; }` walks via the recovery `_tag` branch finding constructor "Left" and reading its `value` field. The canonical type name is `Variant{tag1, tag2, …}` with tags sorted alphabetically. ## `variantAt` _variantAt: universe-polymorphic tagged-union — `variantAt k [{tag; type}…]` nests `sumAt k` over branches already at U(k); `variant` is the level-zero default._ ``` variantAt : Level -> [{ tag : String; type : Hoas; }] -> Hoas ``` ## `variantInject` _variantInject: HOAS variant-value injection — `variantInject ty tag inner` produces the nested `inl/inr` chain that injects `inner` into `tag`'s branch of `ty`._ ``` variantInject : Hoas -> String -> Hoas -> Hoas -- variantTy, tag, value ``` ## `variantInjectAt` _variantInjectAt: universe-polymorphic variant-value injection — `variantInjectAt k ty tag inner` nests `inl/inr` at level k to inject `inner` into `tag`'s branch of `ty`._ ``` variantInjectAt : Level -> Hoas -> String -> Hoas -> Hoas -- level, variantTy, tag, value ``` ## `vcons` _vcons: prelude `Vec` cons constructor — `vcons x xs` prepends `x : A` to `xs : vec A m`, producing `vec A (succ m)`; element type and predecessor inferred._ ``` vcons : Hoas -> Hoas -> Hoas -- head, tail ``` ## `vec` _vec: prelude `Vec` type family — forwarder for `app VecDT.T A`; `app (vec A) n` is the type of vectors of length `n` carrying elements of type `A`._ ``` vec : Hoas -> Hoas -- elemTy ``` ## `vecDesc` _vecDesc: prelude `Vec` description — forwarder for `app VecDT.D A`; the indexed description of length-indexed vectors over element type `A`._ ``` vecDesc : Hoas -> Hoas -- elemTy ``` ## `vecElim` _vecElim: prelude `Vec` eliminator — `vecElim k A P Pn Pc n xs` discharges `xs : vec A n` against motive `P : ∀n:nat. vec A n -> U(k)`._ ``` vecElim : Level -> Hoas -> Hoas -> Hoas -> Hoas -> Hoas -> Hoas -> Hoas ``` ## `vhead` _vhead: prelude `Vec` head extractor — `vhead A n xs` returns the first element of `xs : vec A (succ n)`; eliminator-driven via the `natCaseU unit A` motive._ ``` vhead : Hoas -> Hoas ``` ## `vnil` _vnil: prelude `Vec` empty constructor — `vnil : vec A zero`; element type inferred._ ``` vnil : Hoas ``` ## `void` _void: HOAS empty type — alias for `empty` / `emptyPrim`; the initial-object dual of `unit`, eliminated via `absurd`._ ``` void : Hoas ``` ## `vtail` _vtail: prelude `Vec` tail extractor — `vtail A n xs` returns the tail of `xs : vec A (succ n)` at type `vec A n`; eliminator-driven via the `natPredCase A` motive._ ``` vtail : Hoas -> Hoas ``` ## `w` _w: W-type former — generalised inductive type for trees with arbitrary branching; foundational for encoding finitely-branching datatypes outside the description layer._ ``` w : Hoas -> Hoas -> Hoas -- shapeTy, posFn ``` ## `wDesc` _wDesc: W-type description forwarder — alias for `WDT.D`, the description of W-type trees over shape S and position family pos._ ``` wDesc : Hoas -> (Hoas -> Hoas) -> Hoas ``` ## `wElim` _wElim: W-type eliminator forwarder — alias for `WDT.elim`, the dependent recursor for W-type trees._ ``` wElim : Level -> Hoas -> (Hoas -> Hoas) -> Hoas -> Hoas -> Hoas -> Hoas ``` ## `withConLabel` _withConLabel: attach a surrounding-constructor label to a description-encoding HOAS form — `withConLabel label form` adds `_conLabel = label`; conv-irrelevant._ ``` withConLabel : String -> Hoas -> Hoas ``` Set by the `datatype` macro at each constructor's spine site, so each summand carries its constructor name. Surfaces through `descView`'s `.conLabel` field. Orthogonal to `withDescLabel` (separate slot) so labeling a field-labeled description with a constructor name doesn't overwrite the field's identity. ## `withDescLabel` _withDescLabel: attach a presentation label to a description-encoding HOAS form — `withDescLabel label form` adds `_label = label`; conv-irrelevant, idempotent under re-labeling._ ``` withDescLabel : String -> Hoas -> Hoas ``` Surfaces back through `descView`'s `.label` field. Used by renderers (pretty-printers, doc generators) to surface field names without affecting type equality — two descriptions differing only in labels are conv-equal. Orthogonal to `withConLabel`, which lives in a separate `_conLabel` slot. ## `yes` _yes: positive decidability witness — `yes P p : dec P` for a proof `p : P`. Routes through the left injection of `P ⊎ ¬ P`._ ``` yes : Hoas -> Hoas -> Hoas -- P, proof ``` ## `zero` _zero: HOAS `Nat` constructor — the natural-number zero; introduces `nat` at the `descRet` summand of `natDesc`._ ``` zero : Hoas ``` ## Sub-namespaces - [`_internal`](/nix-effects/type-checker/hoas/_internal) ## Source - [`src/tc/hoas/combinators.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/hoas/combinators.nix) - [`src/tc/hoas/datatype.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/hoas/datatype.nix) - [`src/tc/hoas/decidable.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/hoas/decidable.nix) - [`src/tc/hoas/desc.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/hoas/desc.nix) - [`src/tc/hoas/forced.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/hoas/forced.nix) - [`src/tc/hoas/lower.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/hoas/lower.nix) - [`src/tc/hoas/ornament.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/hoas/ornament.nix) - [`src/tc/hoas/plicity.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/hoas/plicity.nix) - [`src/tc/hoas/source_map.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/hoas/source_map.nix) #### _internal Unstable internal surface — boot-sum/boot-eq helpers, kernel-Tm encoders, and indexed-variant scaffolding; prefer SumDT/EqDT-generated forms in user code. ## `bootEq` _bootEq: bootstrap propositional-equality type former at the description layer — `bootEq T a b` is the kernel-internal `Eq` used before EqDT is available. Prefer the SumDT/EqDT-generated `eq` for end-user code._ ``` bootEq : Hoas -> Hoas -> Hoas -> Hoas -- type, lhs, rhs ``` ## `bootInl` _bootInl: bootstrap left-injection at the description layer — `bootInl L R v` introduces a `boot-sum L R` from `v : L`. Used by the freer-monad-as-description encoding to build μ-values where no SumDT-generated `inl` applies._ ``` bootInl : Hoas -> Hoas -> Hoas -> Hoas -- L, R, v ``` ## `bootInr` _bootInr: bootstrap right-injection at the description layer — `bootInr L R v` introduces a `boot-sum L R` from `v : R`. See `bootInl`._ ``` bootInr : Hoas -> Hoas -> Hoas -> Hoas -- L, R, v ``` ## `bootJ` _bootJ: bootstrap J-eliminator at the description layer — `bootJ T a P b c eq` transports `b : P a a refl` along `eq : bootEq T a c` to `P a c eq`. Prefer EqDT-generated `j` for end-user code._ ``` bootJ : Hoas -> Hoas -> Hoas -> Hoas -> Hoas -> Hoas -> Hoas -- T, a, motive, base, c, eq ``` ## `bootRefl` _bootRefl: bootstrap reflexivity witness at the description layer — checkable against any `bootEq T a a`. Prefer EqDT-generated `refl` for end-user code._ ``` bootRefl : Hoas ``` ## `bootSum` _bootSum: bootstrap binary coproduct type former at the description layer — `bootSum L R` is the kernel-internal `Sum` used before SumDT is available; arises inside `descPlus` interpretation._ ``` bootSum : Hoas -> Hoas -> Hoas -- L, R ``` ## `bootSumElim` _bootSumElim: bootstrap sum eliminator at the description layer — `bootSumElim L R P onL onR scrut` discharges `scrut : bootSum L R` to `P scrut`. Prefer SumDT-generated `sumElim` for end-user code._ ``` bootSumElim : Hoas -> Hoas -> Hoas -> Hoas -> Hoas -> Hoas -> Hoas -- L, R, motive, onLeft, onRight, scrut ``` ## Sub-namespaces - [`_encoders`](/nix-effects/type-checker/hoas/_internal/_encoders) - [`_forced`](/nix-effects/type-checker/hoas/_internal/_forced) - [`_indexed`](/nix-effects/type-checker/hoas/_internal/_indexed) #### _encoders Kernel-Tm and Val-level encoders for surface description combinators; consumed by `tc/eval` (descDescVal) and `tc/generic` (encodeDescXTm pre-evaluations). ## `__descDesc` ___descDesc: private internal-use alias for `descDesc` — exposed for the encoder cascade; surface code should use `descDesc`._ ``` __descDesc : Hoas -> Level -> Hoas ``` ## `descDescApp` _descDescApp: applied `descDesc` form — `descDescApp I k` produces `descDesc I k` as a closed Tm; convenience for sites that need the pre-applied form._ ``` descDescApp : Hoas -> Level -> Hoas ``` ## `descDescAppAtI` _descDescAppAtI: applied `descDesc` form with explicit index-universe level — `descDescAppAtI iLev I k` evaluates to a canonical stamp with params `[iLev, I, k]`._ ``` descDescAppAtI : Level -> Hoas -> Level -> Hoas ``` ## `descDescTm` _descDescTm: pre-elaborated `descDesc` term — closed kernel `Tm` form usable directly by kernel rules that need the description-of-descriptions without re-elaborating._ ``` descDescTm : Tm ``` ## `descDescVal` _descDescVal: pre-evaluated `descDesc` value — the kernel `Val` form, ready for `interpD` / `descInd` consumption without re-evaluation._ ``` descDescVal : Val ``` ## `encodeDescArg` _encodeDescArg: HOAS encoder for `descArg` — `encodeDescArg I k S T` builds the structural encoding of a `descArg I k S T` description._ ``` encodeDescArg : Hoas -> Level -> Hoas -> Hoas -> Hoas ``` ## `encodeDescArgAt` _encodeDescArgAt: HOAS encoder for `descArgAt` — universe-polymorphic encoder for `descArgAt I l k S T`._ ``` encodeDescArgAt : Hoas -> Level -> Level -> Hoas -> Hoas -> Hoas ``` ## `encodeDescArgAtTm` _encodeDescArgAtTm: pre-elaborated `encodeDescArgAt` — closed kernel `Tm`._ ``` encodeDescArgAtTm : Tm ``` ## `encodeDescArgTm` _encodeDescArgTm: pre-elaborated `encodeDescArg` — closed kernel `Tm`._ ``` encodeDescArgTm : Tm ``` ## `encodeDescElim` _encodeDescElim: HOAS encoder for `descElim` — produces the cascade application that walks `descDesc`'s plus-tree to dispatch on a `VDescCon` value's constructor._ ``` encodeDescElim : Hoas -> Level -> Level -> Hoas -> Hoas -> Hoas -> Hoas -> Hoas -> Hoas -> Hoas -> Hoas ``` ## `encodeDescElimTm` _encodeDescElimTm: pre-elaborated `encodeDescElim` — closed kernel `Tm` form of the eliminator cascade._ ``` encodeDescElimTm : Tm ``` ## `encodeDescElimVal` _encodeDescElimVal: pre-evaluated `encodeDescElim` — kernel `Val` form, ready for direct consumption without re-evaluation._ ``` encodeDescElimVal : Val ``` ## `encodeDescPi` _encodeDescPi: HOAS encoder for `piI` — `encodeDescPi I k S f D` builds the structural encoding of a `piI I k S f D` description._ ``` encodeDescPi : Hoas -> Level -> Hoas -> Hoas -> Hoas -> Hoas ``` ## `encodeDescPiAt` _encodeDescPiAt: HOAS encoder for `piIAt` — universe-polymorphic encoder for `piIAt I l k S f D`._ ``` encodeDescPiAt : Hoas -> Level -> Level -> Hoas -> Hoas -> Hoas -> Hoas ``` ## `encodeDescPiAtTm` _encodeDescPiAtTm: pre-elaborated `encodeDescPiAt` — closed kernel `Tm`._ ``` encodeDescPiAtTm : Tm ``` ## `encodeDescPiTm` _encodeDescPiTm: pre-elaborated `encodeDescPi` — closed kernel `Tm`._ ``` encodeDescPiTm : Tm ``` ## `encodeDescPlus` _encodeDescPlus: HOAS encoder for `plusI` — `encodeDescPlus I k A B` builds the structural encoding of a `plusI I k A B` description._ ``` encodeDescPlus : Hoas -> Level -> Hoas -> Hoas -> Hoas ``` ## `encodeDescPlusTm` _encodeDescPlusTm: pre-elaborated `encodeDescPlus` — closed kernel `Tm`._ ``` encodeDescPlusTm : Tm ``` ## `encodeDescRec` _encodeDescRec: HOAS encoder for `recI` — `encodeDescRec I k j D` builds the structural encoding of a `recI I k j D` description._ ``` encodeDescRec : Hoas -> Level -> Hoas -> Hoas -> Hoas ``` ## `encodeDescRecTm` _encodeDescRecTm: pre-elaborated `encodeDescRec` — closed kernel `Tm`._ ``` encodeDescRecTm : Tm ``` ## `encodeDescRet` _encodeDescRet: HOAS encoder for `retI` — `encodeDescRet I k j` builds the `μ ⊤ (descDesc I k) tt` value structurally encoding a `retI I k j` description._ ``` encodeDescRet : Hoas -> Level -> Hoas -> Hoas -- I, k, targetIdx ``` ## `encodeDescRetTm` _encodeDescRetTm: pre-elaborated `encodeDescRet` — closed kernel `Tm` lambda; consumed by `interpD`'s encoded-desc view branch._ ``` encodeDescRetTm : Tm ``` ## `natDescTm` _natDescTm: pre-elaborated `Nat` description term — closed kernel `Tm` encoding `natDesc`; used by the kernel where the HOAS form would re-elaborate._ ``` natDescTm : Tm ``` #### _forced Forced-argument analysis helpers for datatype constructors; consumed by datatype elaboration and tests that inspect recoverable constructor fields. ## `forcedFieldNames` _McBride-forced subset of a constructor's fields: a field is forced iff its sentinel marker occurs in `targetIdx prevFull` or in `fieldTyOf f_j prev_ i. Returns field names in declaration order._ ``` { fields : [Field]; targetIdx : Prev -> Hoas; fieldTyOf : ?Field -> Prev -> Hoas } -> [String] ``` ## `forcedFieldSet` _forcedFieldNames re-keyed as `{ = true; }` for O(1) membership._ ``` { fields; targetIdx; fieldTyOf? } -> { = true; } ``` ## `isFieldForced` _Predicate form: whether the named field appears in the forced set._ ``` { fields; targetIdx; fieldTyOf? } -> String -> Bool ``` ## `mentionsOf` _Collect `_signatureField` marker names occurring in a HOAS term. Binder bodies are descended by applying their closure to a `forced-probe` sentinel._ ``` Hoas -> [String] ``` #### _indexed Indexed/equality-aligned combinators (`muI`, `piI`, `recI`, `plusI`, `inrAt`, `fieldAt`) consumed by ornament construction and indexed-datatype test fixtures. ## `LiftAt` _LiftAt: HOAS cross-level type former — `LiftAt l m A : U(m)` for `A : U(l)` with `l ≤ m`; bound witness auto-emitted as `mkBootRefl` when convLevel decides `Eq Level (max l m) m`._ ``` LiftAt : Level -> Level -> Hoas -> Hoas -- l, m, A ``` Idempotent at equal levels: `LiftAt l l A ≡ A` (no wrapping when `sameLevelSyntax l l`). Use to transport a type from a lower universe to a higher one without changing inhabitation. For level-polymorphic binders where convLevel cannot decide, use `LiftAtWithEq` and supply the proof explicitly. ## `LiftAtWithEq` _LiftAtWithEq: LiftAt variant carrying an explicit bound-witness — `LiftAtWithEq l m eq A` supplies `eq : Eq Level (max l m) m` when convLevel cannot decide it._ ``` LiftAtWithEq : Level -> Level -> Hoas -> Hoas -> Hoas -- l, m, eq, A ``` ## `consAtExplicit` _consAtExplicit: internal List cons with hidden level + element type supplied explicitly for rigid/raw evaluation sites._ ``` consAtExplicit : Level -> Hoas -> Hoas -> Hoas -> Hoas -- level, elemTy, head, tail ``` ## `datatypeAt` _datatypeAt: universe-polymorphic ⊤-slice datatype — `datatypeAt name level [con …]` emits a datatype at sort level `level`._ ``` datatypeAt : String -> Level -> [Constructor] -> DataSpec ``` ## `datatypePAt` _datatypePAt: universe-polymorphic parametric datatype — `datatypePAt` extends `datatypeP` with explicit sort levels for parameters._ ``` datatypePAt : String -> Level -> [Param] -> (Hoas -> [Constructor]) -> DataSpec ``` ## `descArgAt` _descArgAt: universe-polymorphic descArg — `descArgAt I l k S T` builds the same with explicit sort level `l`._ ``` descArgAt : Hoas -> Level -> Level -> Hoas -> Hoas -> Hoas ``` ## `descArgAtAtI` _descArgAtAtI: fully universe-explicit indexed descArg — `descArgAtAtI iLev I l k S T` threads both the index-universe level and payload sort level._ ``` descArgAtAtI : Level -> Hoas -> Level -> Level -> Hoas -> Hoas -> Hoas ``` ## `descArgAtI` _descArgAtI: indexed descArg with explicit index-universe level — `descArgAtI iLev I k S T` for index types `I : U(iLev)`._ ``` descArgAtI : Level -> Hoas -> Level -> Hoas -> Hoas -> Hoas ``` ## `descArgWithEq` _descArgWithEq: descArgAt variant with explicit bound-witness — supplies the `eq : Eq Level (max l k) k` proof for level-polymorphic positions._ ``` descArgWithEq : Hoas -> Level -> Level -> Hoas -> Hoas -> Hoas -> Hoas ``` ## `descAt` _descAt: universe-polymorphic ⊤-slice description — alias for `descIAt k unitPrim`; descriptions at level `k` over the unit index type._ ``` descAt : Level -> Hoas ``` ## `descI` _descI: indexed description type former — `descI I` builds `Desc I`, the universe of descriptions over index sort `I`; level defaults to 0._ ``` descI : Hoas -> Hoas ``` ## `descIAt` _descIAt: universe-polymorphic indexed description — `descIAt k I` builds `Desc^k I`; the level-omitting `descI` defaults to level 0._ ``` descIAt : Level -> Hoas -> Hoas ``` ## `descIAtAtI` _descIAtAtI: fully universe-explicit indexed description — `descIAtAtI iLev k I` builds `Desc^k I` for `I : U(iLev)`._ ``` descIAtAtI : Level -> Level -> Hoas -> Hoas ``` ## `descIAtI` _descIAtI: indexed description with explicit index-universe level — `descIAtI iLev I` builds `Desc I` for `I : U(iLev)`._ ``` descIAtI : Level -> Hoas -> Hoas ``` ## `descPiAt` _descPiAt: universe-polymorphic ⊤-slice descPi — `descPiAt l k S D` with explicit sort level `l`._ ``` descPiAt : Level -> Level -> Hoas -> Hoas -> Hoas ``` ## `descPiWithEq` _descPiWithEq: descPiAt variant with explicit bound-witness — supplies the `eq : Eq Level (max l k) k` proof for level-polymorphic descriptions._ ``` descPiWithEq : Level -> Level -> Hoas -> Hoas -> Hoas -> Hoas ``` ## `fieldAt` _fieldAt: universe-polymorphic field declarator — `fieldAt name level type` declares a field at an explicit sort level._ ``` fieldAt : String -> Level -> Hoas -> { name; type; level; } ``` ## `fieldAtWithEq` _fieldAtWithEq: fieldAt variant carrying an explicit bound-witness — supplies the `eq : Eq Level (max l k) k` proof for level-polymorphic positions._ ``` fieldAtWithEq : String -> Level -> Hoas -> Hoas -> { name; type; level; eq; } ``` ## `fieldDAt` _fieldDAt: universe-polymorphic dependent-field declarator — combines `fieldAt`'s explicit level with `fieldD`'s index dependence._ ``` fieldDAt : String -> Level -> Hoas -> (Hoas -> Hoas) -> { name; type; level; indexFn; } ``` ## `fieldDAtWithEq` _fieldDAtWithEq: universe-polymorphic dependent-field with explicit bound-witness — combines fieldDAt with explicit eq._ ``` fieldDAtWithEq : String -> Level -> Hoas -> Hoas -> (Hoas -> Hoas) -> { name; type; level; eq; indexFn; } ``` ## `inlAt` _inlAt: universe-polymorphic left-injection — `inlAt v` builds `Left v : Sum A B`; level, leftTy, rightTy inferred._ ``` inlAt : Hoas -> Hoas -- value ``` ## `inlAtExplicit` _inlAtExplicit: internal Sum left-injection with hidden parameters supplied explicitly for raw evaluation sites._ ``` inlAtExplicit : Level -> Hoas -> Hoas -> Hoas -> Hoas -- level, leftTy, rightTy, value ``` ## `inrAt` _inrAt: universe-polymorphic right-injection — `inrAt v` builds `Right v : Sum A B`; level, leftTy, rightTy inferred._ ``` inrAt : Hoas -> Hoas -- value ``` ## `inrAtExplicit` _inrAtExplicit: internal Sum right-injection with hidden parameters supplied explicitly for raw evaluation sites._ ``` inrAtExplicit : Level -> Hoas -> Hoas -> Hoas -> Hoas -- level, leftTy, rightTy, value ``` ## `liftAt` _liftAt: HOAS lift introduction — `liftAt l m A a` produces `LiftAt l m A` from `a : A`; idempotent at equal levels (no introducer when `l = m`)._ ``` liftAt : Level -> Level -> Hoas -> Hoas -> Hoas -- l, m, A, a ``` ## `liftAtWithEq` _liftAtWithEq: explicit-witness lift introduction — `liftAtWithEq l m eq A a` with the caller-provided `eq` term derived via `congSuc` / `maxSucDom` for level-polymorphic positions._ ``` liftAtWithEq : Level -> Level -> Hoas -> Hoas -> Hoas -> Hoas -- l, m, eq, A, a ``` ## `lowerAt` _lowerAt: HOAS lift elimination — `lowerAt l m A x` extracts `a : A` from `x : LiftAt l m A`; β-reduces with `liftAt`, idempotent at equal levels._ ``` lowerAt : Level -> Level -> Hoas -> Hoas -> Hoas -- l, m, A, x ``` ## `lowerAtWithEq` _lowerAtWithEq: explicit-witness lift elimination — `lowerAtWithEq l m eq A x` mirroring `liftAtWithEq`; uses the supplied `eq` to discharge the level bound._ ``` lowerAtWithEq : Level -> Level -> Hoas -> Hoas -> Hoas -> Hoas -- l, m, eq, A, x ``` ## `muI` _muI: indexed inductive carrier former — `muI I D i` builds `μ I D i`, the value type at index `i` of the description `D : Desc I`._ ``` muI : Hoas -> Hoas -> Hoas -> Hoas -- I, D, i ``` The carrier is constructed by `descCon D i d` from interpreted payloads. Eliminated via `descInd D Q step i x` for indexed induction. The kernel routes `mu` through the description- rule path, never as a bare `mu` constructor — every `muI` flow eventually applies the encoder cascade. ## `muIAtI` _muIAtI: indexed inductive carrier with explicit index-universe level — `muIAtI iLev I D i` builds `μ I D i` for `I : U(iLev)`._ ``` muIAtI : Level -> Hoas -> Hoas -> Hoas -> Hoas ``` ## `nilAtExplicit` _nilAtExplicit: internal List nil with hidden level + element type supplied explicitly for rigid/raw evaluation sites._ ``` nilAtExplicit : Level -> Hoas -> Hoas -- level, elemTy ``` ## `piFieldAt` _piFieldAt: universe-polymorphic Π-typed field — `piFieldAt name level sort body` with explicit level._ ``` piFieldAt : String -> Level -> Hoas -> (Hoas -> Hoas) -> { ... } ``` ## `piFieldAtIndex` _piFieldAtIndex: piFieldAt variant emitting an explicit-target-index — used when the recursive Π must specify the index of the resulting recursive child._ ``` piFieldAtIndex : String -> Level -> Hoas -> (Hoas -> Hoas) -> (Hoas -> Hoas) -> { ... } ``` ## `piFieldAtIndexWithEq` _piFieldAtIndexWithEq: piFieldAtIndex with explicit bound-witness._ ``` piFieldAtIndexWithEq : String -> Level -> Hoas -> Hoas -> (Hoas -> Hoas) -> (Hoas -> Hoas) -> { ... } ``` ## `piFieldAtWithEq` _piFieldAtWithEq: piFieldAt with explicit bound-witness — supplies the `eq` proof for level-polymorphic positions._ ``` piFieldAtWithEq : String -> Level -> Hoas -> Hoas -> (Hoas -> Hoas) -> { ... } ``` ## `piFieldDAt` _piFieldDAt: universe-polymorphic dependent Π-typed field — combines piFieldAt with piFieldD's dependence on prior fields._ ``` piFieldDAt : String -> Level -> Hoas -> (Hoas -> Hoas -> Hoas) -> { ... } ``` ## `piFieldDAtIndex` _piFieldDAtIndex: piFieldDAt variant emitting an explicit-target-index._ ``` piFieldDAtIndex : String -> Level -> Hoas -> (Hoas -> Hoas -> Hoas) -> (Hoas -> Hoas -> Hoas) -> { ... } ``` ## `piFieldDAtIndexWithEq` _piFieldDAtIndexWithEq: piFieldDAtIndex with explicit bound-witness._ ``` piFieldDAtIndexWithEq : String -> Level -> Hoas -> Hoas -> (Hoas -> Hoas -> Hoas) -> (Hoas -> Hoas -> Hoas) -> { ... } ``` ## `piFieldDAtWithEq` _piFieldDAtWithEq: universe-polymorphic dependent Π-typed field with explicit bound-witness._ ``` piFieldDAtWithEq : String -> Level -> Hoas -> Hoas -> (Hoas -> Hoas -> Hoas) -> { ... } ``` ## `piI` _piI: indexed piI-constructor — `piI I k S f D` builds a description that quantifies over `S` then continues with `D` parametrised by the chosen element via `f : S -> Tm` (the index function)._ ``` piI : Hoas -> Level -> Hoas -> Hoas -> Hoas -> Hoas -- I, k, sort, indexFn, continuation ``` ## `piIAt` _piIAt: universe-polymorphic piI — `piIAt I l k S f D` builds the same shape as `piI` but with explicit sort level `l` and continuation level `k`, used when level decisions cannot be left to convLevel._ ``` piIAt : Hoas -> Level -> Level -> Hoas -> Hoas -> Hoas -> Hoas ``` ## `piIAtAtI` _piIAtAtI: fully universe-explicit indexed piI — `piIAtAtI iLev I l k S f D` threads the index-universe level and payload sort level._ ``` piIAtAtI : Level -> Hoas -> Level -> Level -> Hoas -> Hoas -> Hoas -> Hoas ``` ## `piIAtI` _piIAtI: indexed piI with explicit index-universe level — `piIAtI iLev I k S f D` for descriptions over `I : U(iLev)`._ ``` piIAtI : Level -> Hoas -> Level -> Hoas -> Hoas -> Hoas -> Hoas ``` ## `piIWithEq` _piIWithEq: piIAt variant carrying an explicit bound-witness — `piIWithEq I l k S f eq D` supplies the `eq : Eq Level (max l k) k` proof when the elaborator cannot auto-decide via `convLevel`._ ``` piIWithEq : Hoas -> Level -> Level -> Hoas -> Hoas -> Hoas -> Hoas -> Hoas ``` ## `plusI` _plusI: indexed description sum — `plusI I k A B` builds `A + B` at index sort `I` and level `k`; both `A` and `B` must share index sort and level._ ``` plusI : Hoas -> Level -> Hoas -> Hoas -> Hoas ``` ## `plusIAtI` _plusIAtI: indexed description sum with explicit index-universe level — `plusIAtI iLev I k A B` for descriptions over `I : U(iLev)`._ ``` plusIAtI : Level -> Hoas -> Level -> Hoas -> Hoas -> Hoas ``` ## `recI` _recI: indexed recI-constructor — `recI I k j D` adds a recursive child at target index `j` to the continuation description `D`; the recursive position contributes payload at `μ I D' j` for the inner D'._ ``` recI : Hoas -> Level -> Hoas -> Hoas -> Hoas -- I, k, targetIndex, continuation ``` ## `recIAtI` _recIAtI: indexed recI-constructor with explicit index-universe level — `recIAtI iLev I k j D` for `I : U(iLev)`._ ``` recIAtI : Level -> Hoas -> Level -> Hoas -> Hoas -> Hoas ``` ## `retIAtI` _retIAtI: indexed retI-constructor with explicit index-universe level — `retIAtI iLev I k j` for leaves over `I : U(iLev)`._ ``` retIAtI : Level -> Hoas -> Level -> Hoas -> Hoas ``` ## `sumAt` _sumAt: universe-polymorphic coproduct — `sumAt level A B` builds `A + B` at the given universe level, used when the homogeneous-level `sum` cannot decide the bound witness._ ``` sumAt : Hoas -> Hoas -> Hoas -> Hoas -- level, A, B ``` ## `sumElimAt` _sumElimAt: universe-polymorphic sum eliminator — `sumElimAt level k A B P L R s` runs the sum eliminator at the given universe level, matching `inlAt` / `inrAt` construction._ ``` sumElimAt : Hoas -> Level -> Hoas -> Hoas -> Hoas -> Hoas -> Hoas -> Hoas -> Hoas -- level, k, A, B, motive, onLeft, onRight, scrut ``` #### Surface Surface specifications define constructor tags and handlers. HOAS elaboration consults the attached registry only for nodes that carry `_surfaceRegistry`, so existing HOAS terms continue through the built-in dispatcher. ## `collectImplicitMetas` _Collect surface-created implicit metavariable terms from an elaborated overlay term._ ``` ElabTm -> [SurfaceMeta] ``` ## `collectSurfaceMetas` _Collect explicitly marked overlay metavariable terms from an elaborated surface result._ ``` ElabTm -> [SurfaceMeta] ``` ## `containsImplicitMeta` _Predicate for unresolved surface-created implicit metavariable terms._ ``` ElabTm -> Bool ``` ## `containsSurfaceMeta` _Predicate for any overlay metavariable term inside an elaborated surface result._ ``` ElabTm -> Bool ``` ## `defineOrnament` _Package a surface-to-target mapping with section and forget morphisms for elaborator derivation._ ``` { source, target, mapping?, section?, forget? } -> SurfaceOrnament ``` ## `defineSurface` _Define a named surface language with constructor metadata and elaboration handlers._ ``` { name, description?, scope?, constructors } -> SurfaceSpec ``` ## `deriveElaborator` _Derive an elaborator entry point from a surface specification and optional ornament section. Expected type and source position are threaded into surface handlers._ ``` { surface, ornament? } -> { registry, elaborate } ``` ## `deriveParser` _Derive a parser shell for a surface specification._ ``` { surface, parse? } -> { parse } ``` ## `derivePrinter` _Derive a small printer for surface AST nodes._ ``` { surface, render? } -> { print } ``` ## `elaborate` _Elaborate one surface term, optionally checking it against an expected HOAS type and source position._ ``` { surface, term, expectedType?, position?, ornament? } -> Tm | Error ``` ## `finalizeSurfaceElab` _Return a structured unsolved-implicit error when elaboration leaves surface implicit metas unresolved._ ``` { result, state?, term?, surface?, position? } -> ElabTm | Error ``` ## `framework` _Convenience bundle for the surface-language definition, ornament, elaboration, parser, and printer APIs._ ## `handlerContext` _Build the argument record passed to surface elaboration handlers, adding expected type, source position, and implicit-meta helpers._ ``` { h, depth, lower, hoas, fx, ... } -> HandlerArgs ``` ## `implicitMeta` _Allocate an implicit metavariable using the Phase 6 meta-context shape and return both overlay value and term forms._ ``` { type?, state?, position?, surface?, label? } -> { id, value, term, state } ``` ## `parse` _Parse one input value with a derived parser shell or supplied parser._ ``` { surface, input, parse? } -> Hoas ``` ## `prelude` _Minimal surface prelude with List and an implicit-argument nil proof fixture._ ## `print` _Print one surface AST node with a derived or supplied renderer._ ``` { surface, term, render? } -> String ``` ## `surfaceTerm` _Construct one term from a surface specification._ ``` SurfaceSpec -> String -> Attrs -> Hoas ``` ## `toy` _Toy boolean surface used as an end-to-end elaboration fixture._ ## `unsolvedImplicitError` _Build the structured surface error for unresolved implicit metavariables._ ``` { metas, term?, surface?, position? } -> Error ``` ## `withSurfaceContext` _Attach root expected-type and position metadata to a surface AST node before elaboration._ ``` { term, expectedType?, position? } -> Hoas ``` ## Sub-namespaces - [`registry`](/nix-effects/type-checker/surface/registry) ## Source - [`src/tc/surface/define-ornament.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/surface/define-ornament.nix) - [`src/tc/surface/define-surface.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/surface/define-surface.nix) - [`src/tc/surface/derive-elaborator.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/surface/derive-elaborator.nix) - [`src/tc/surface/derive-parser.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/surface/derive-parser.nix) - [`src/tc/surface/derive-printer.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/surface/derive-printer.nix) - [`src/tc/surface/framework.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/surface/framework.nix) - [`src/tc/surface/handler-context.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/surface/handler-context.nix) - [`src/tc/surface/implicit.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/surface/implicit.nix) - [`src/tc/surface/prelude.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/surface/prelude.nix) - [`src/tc/surface/registry.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/surface/registry.nix) #### Registry Surface elaborator registry operations. ## `empty` _Empty surface elaboration registry._ ``` Registry ``` ## `emptyRegistry` _Alias for the empty surface elaboration registry._ ``` Registry ``` ## `fromHandlers` _Build a surface registry from an attrset of constructor-tag handlers._ ``` { tag : Handler } -> Registry ``` ## `handlerFor` _Alias for lookup._ ``` Registry -> String -> Handler | Null ``` ## `isRegistry` _Predicate for surface elaboration registries._ ``` Any -> Bool ``` ## `lookup` _Return the handler registered for a surface constructor tag, or null._ ``` Registry -> String -> Handler | Null ``` ## `merge` _Merge two surface registries, rejecting duplicate constructor handlers._ ``` Registry -> Registry -> Registry ``` ## `node` _Construct a surface AST node carrying its registry._ ``` Registry -> String -> Attrs -> Hoas ``` ## `normalize` _Normalize nullable registry inputs to a registry record._ ``` Registry | Null -> Registry ``` ## `register` _Register a handler for one surface constructor tag._ ``` Registry -> String -> Handler -> Registry ``` ## `withRegistry` _Attach a surface elaboration registry to an existing node._ ``` Registry -> Attrs -> Hoas ``` #### Generic Reflection over levitated descriptions and datatype macro outputs. Datatype consumers should use this layer instead of raw `_dtypeMeta`. Generic algebraic ornament helpers check algebra descriptors against `generic.desc.shape`; the supported fragment is ret/arg/rec/plus. Ornament validation exposes total structured diagnostics for expected surface errors; semantic ornament maps remain pure over validated inputs. Functional ornaments expose manual sections for canonical base-to-ornamented construction; synthesis is layered on later. The first synthesis layer accepts `{ base; spec; synth; }`, reuses the ordinary ornament spec, and requires explicit builders for inserted fields. Proof-marked inserted fields use `prove` or `synth.constructors..proofs` and report unresolved proof obligations as structured diagnostics. Declared measures feed algebraic/measure-derived inserted fields through the synthesis builder context and produce missing-measure obligations. Function transport lifts base producers and transforms through canonical functional output sections while existing pullback remains forget-then-run. ## `path` _path: list-level operations on a `[Position]` descent chain — `empty`, `extend`, `render`, `renderAll`. Position segments themselves are constructed via `fx.diag.positions` (`P.Field name`, `P.Elem i`, etc.); this module only handles the list assembly and pretty-rendering._ ``` path : { empty, extend, render, renderAll } ``` Operations on a path (a list of `Position` segments naming a structural descent from a validation root to a failure site): - `empty = []` is the root path. - `extend : Path -> Position -> Path` appends a segment. - `render : Position -> String` pretty-renders one segment. - `renderAll : Path -> String` concatenates rendered segments. Position segments are produced by `fx.diag.positions` (the canonical curried constructors `Field`, `Elem`, `Tag`, `Tuple`, plus the ~30 nullary description/MLTT positions). Handlers consume the same `Position` records regardless of whether they come from kernel descent or value-side `verify=` walks. ## Sub-namespaces - [`check`](/nix-effects/type-checker/generic/check) - [`checkD`](/nix-effects/type-checker/generic/checkD) - [`datatype`](/nix-effects/type-checker/generic/datatype) - [`derive`](/nix-effects/type-checker/generic/derive) - [`desc`](/nix-effects/type-checker/generic/desc) - [`ornaments`](/nix-effects/type-checker/generic/ornaments) - [`value`](/nix-effects/type-checker/generic/value) ## Source - [`src/tc/generic/check.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/generic/check.nix) - [`src/tc/generic/checkD.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/generic/checkD.nix) - [`src/tc/generic/datatype.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/generic/datatype.nix) - [`src/tc/generic/derive.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/generic/derive.nix) - [`src/tc/generic/desc.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/generic/desc.nix) - [`src/tc/generic/ornaments.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/generic/ornaments.nix) - [`src/tc/generic/path.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/generic/path.nix) - [`src/tc/generic/value.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/generic/value.nix) #### Desc A uniform interface to levitated descriptions (`Desc I k`) that hides the encoder details behind a small algebra of views, folds, and predicates. Every entry accepts either HOAS or evaluated `Val` descriptions — `evalDesc` normalises to `Val` idempotently. `descView` returns a one-step semantic view exposing the outer constructor as both an integer `idx` (0=ret, 1=arg, 2=rec, 3=pi, 4=plus) and a human-readable `kind` string, alongside the constructor-specific fields (`I`, `k`, `j`, `sTy`, `tFn`, `sub`, `A`, `B`, plus optional `label`/`conLabel` sidecar metadata). `foldDesc` is the catamorphism: pass per-constructor handler functions, each receiving a record with the recursed sub-values already materialised. `foldDescM` is the monadic dual — each handler returns a `Computation R` and the combinator binds sub-results before invoking the matching case; `paraDM` is the paramorphic variant that additionally exposes the original sub-description `Val` at each recursive site (for callers that consult kernel-level shape without paying for a monadic descent). `foldDescWithPath` mirrors `foldDescM` and threads a `[fx.diag.positions]` chain extended at each descent so handlers can construct positionally-blamed errors directly from inside the fold. `mapDesc` is the structural map — handlers return either replacement HOAS, an explicit `{ _replaceChildren = …; }` payload, or a `Val` for direct substitution; identity mappers reconstruct definitionally-equal outputs via the canonical encoder chain. `deepEqualDesc` decides definitional equality by delegating to `fx.tc.conv.conv`; presentation labels (`_label`, `_conLabel`) are conv-irrelevant. `children`, `shape`, and the `isRet` / `isArg` / `isRec` / `isPi` / `isPlus` predicates round out the introspection surface for callers that want lightweight structural queries without writing a full fold. ## `children` _children: direct sub-descriptions list — empty for `ret`, `[body]` for `arg`/`rec`/`pi` (arg applies the body to a `vTt` placeholder), `[A, B]` for `plus`._ ``` children : Hoas | Val -> [Val] ``` ## `deepEqualDesc` _deepEqualDesc: definitional equality on description values — evaluates both arguments via `evalDesc`, then runs `fx.tc.conv.conv 0` for full conv-equality._ ``` deepEqualDesc : Hoas | Val -> Hoas | Val -> Bool ``` ## `descView` _descView: one-step semantic view of a description value — returns `{ idx, kind, I, k, ... }` selecting the outer constructor (`0` = ret, `1` = arg, `2` = rec, `3` = pi, `4` = plus); throws on malformed inputs._ ``` descView : Hoas | Val -> { idx : 0..4; kind : String; I : Val; k : Val; ... } ``` Friendly wrapper around `fx.tc.eval.descView` that adds a human-readable `kind` field alongside the integer `idx`. Returns the raw view fields (`I`, `k`, `j`, `sTy`, `tFn`, `sub`, `A`, `B`, optional `label`/`conLabel`) so callers can peel apart any description shape uniformly. Use this entry as the canonical view for generic programming over levitated descriptions; the lower-level `fx.tc.eval.descView` is identical in semantics but skips the kind-naming step. ## `evalDesc` _evalDesc: idempotent description normaliser — returns evaluated `Val` descriptions unchanged, evaluates HOAS descriptions via `H.elab` + `E.eval []`; throws on inputs that are neither._ ``` evalDesc : Hoas | Val -> Val ``` ## `foldDesc` _foldDesc: catamorphism over description structure — recursively decomposes a description into its ret/arg/rec/pi/plus shape and invokes the matching case in `cases` with the materialised sub-values._ ``` foldDesc : { ret? ; arg? ; rec? ; pi? ; plus? ; default? } -> Hoas | Val -> R ``` ## `foldDescM` _foldDescM: monadic catamorphism over description structure — recurses through ret/arg/rec/pi/plus and binds each sub-`Computation R` before invoking the matching handler, so handlers receive already-bound `R` carriers and the combinator owns the threading._ ``` foldDescM : { ret? ; arg? ; rec? ; pi? ; plus? ; default? } -> Hoas | Val -> Computation R ``` Monadic dual of `foldDesc`. Each handler returns a `Computation R`; recursed sub-computations are sequenced via `fx.kernel.bind` before the handler runs, so a handler observes bound `R` values (not raw computations). Per-summand handler shapes: - `ret` : `{ view; j }` — no sub-recursion. - `arg` : `{ view; sTy; body; sample }` where `body : arg -> Computation R` defers recursion at the body's parameter and `sample : R` is the bound result of recursing on the placeholder-instantiated body (mirrors the pure `foldDesc` arg convention). - `rec` : `{ view; j; sub }` with `sub : R`. - `pi` : `{ view; sTy; fn; sub }` with `sub : R` (the selector `fn` stays raw — it's a `Val`, not a description). - `plus` : `{ view; left; right }` with both bound. The `default` handler is used when a per-summand handler is absent; default-of-defaults is `_: K.pure null`. Use this fold to drive `checkD`/`inferD`-shaped walkers where each kernel-CHECK sub-delegation lives inside a `Computation Tm` (typeError handler, `bindP`-style position wrapping). The combinator preserves the pure-bind discipline: recursing at a summand whose sub-walk resolves to `K.pure …` does not install a handler. ## `foldDescWithPath` _foldDescWithPath: monadic catamorphism that threads a `path` of `fx.diag.positions` segments AND uses `bindP` at each descent so emitted typeErrors auto-wrap under the structural position before re-raising._ ``` foldDescWithPath : [Position] -> { ret? ; arg? ; rec? ; pi? ; plus? ; default? } -> Hoas | Val -> Computation R ``` Variant of `foldDescM` with two-pronged structural blame: every descent installs a `fx.tc.check.bindP` handler under the descent's position, and the current `path` is exposed to handlers as data. The blame alphabet at each descent: arg.T → `P.DArgBody` rec.D → `P.DRecTail` pi.T → `P.DPiBody` plus.L → `P.DPlusL` plus.R → `P.DPlusR` Because the descent uses `bindP`, a sub-handler that emits `typeError` produces an error tree whose outermost edge is the descent position, with the original error nested under it. Nested descents stack — a typeError emitted at the leaf of a plus(rec(ret)) descent surfaces with the edge chain `[DPlusL, DRecTail]` outermost-first, matching the kernel's existing positional-blame convention. Handlers that prefer explicit position wrapping can use the `path` argument and `D.nestUnder` directly; both routes compose because the alphabet is identical. Each handler receives the *current* `path` (the chain leading to its node, not yet extended by its own descent) and, for non-leaf summands, helper extenders matching what the descent will install: - `ret` : `{ view; path; j }`. - `arg` : `{ view; path; sTy; body; sample; bodyPath }` — `bodyPath = path ++ [DArgBody]`. - `rec` : `{ view; path; j; sub; subPath }` — `subPath = path ++ [DRecTail]`. - `pi` : `{ view; path; sTy; fn; sub; subPath }` — `subPath = path ++ [DPiBody]`. - `plus` : `{ view; path; left; right; leftPath; rightPath }`. Pass `[]` as the root path for a top-level walk. Nested callers pass the outer chain so positions compose end-to-end. ## `isArg` _isArg: predicate — `true` iff the description's outer constructor is `descArg`._ ``` isArg : Hoas | Val -> Bool ``` ## `isPi` _isPi: predicate — `true` iff the description's outer constructor is `descPi`._ ``` isPi : Hoas | Val -> Bool ``` ## `isPlus` _isPlus: predicate — `true` iff the description's outer constructor is `plusI` (binary sum)._ ``` isPlus : Hoas | Val -> Bool ``` ## `isRec` _isRec: predicate — `true` iff the description's outer constructor is `descRec`._ ``` isRec : Hoas | Val -> Bool ``` ## `isRet` _isRet: predicate — `true` iff the description's outer constructor is `descRet`._ ``` isRet : Hoas | Val -> Bool ``` ## `mapDesc` _mapDesc: structurally rewrite each layer of a description via per-shape mapper functions, then reconstruct using the canonical encoder chain — the result is conv-equivalent to the original when mappers are identities._ ``` mapDesc : { ret? ; arg? ; rec? ; pi? ; plus? } -> Hoas | Val -> Val ``` ## `paraDM` _paraDM: monadic paramorphism over description structure — each handler additionally receives the original sub-description `Val` alongside the bound recursed result, enabling certificate-aware fast paths and reflective elaborators._ ``` paraDM : { ret? ; arg? ; rec? ; pi? ; plus? ; default? } -> Hoas | Val -> Computation R ``` Paramorphic variant of `foldDescM`. Where `foldDescM` exposes only the recursed `R` carrier at each sub-position, `paraDM` also exposes the raw sub-description `Val` — required by callers that inspect the kernel-level description shape without paying for a monadic descent (certificate consultation, ornament-section selection, reflection). Per-summand handler shapes: - `ret` : `{ view; j }`. - `arg` : `{ view; sTy; body; bodyDesc; sample; sampleDesc }` — `body : arg -> Computation R` and the parallel `bodyDesc : arg -> Val` return the sub-description without recursing. `sample` and `sampleDesc` are the bound result and raw `Val` at the placeholder respectively. - `rec` : `{ view; j; sub; subDesc }` — `subDesc : Val` is the original sub-description `view.sub`. - `pi` : `{ view; sTy; fn; sub; subDesc }`. - `plus` : `{ view; left; right; leftDesc; rightDesc }`. `default` and identity-on-failure semantics match `foldDescM`. ## `shape` _shape: tagged-record skeleton of a description — returns `{ kind = "ret" | "arg" | "rec" | "pi" | "plus"; ...payload }` with only the shape-relevant fields; payload depends on `kind`._ ``` shape : Hoas | Val -> { kind : String; ... } ``` #### Datatype Operates on the `_dtypeMeta` attrset attached to every datatype produced by `H.datatype` / `H.datatypeI` / `H.datatypeP`, and on `H.app`-spines whose head carries `_dtypeMeta` (instantiated polymorphic datatypes). `datatypeInfo` is the canonical entry: it normalises a raw meta or applies `meta.instantiate` to the spine args before returning a fully-defaulted record (`indexed`, `params`, `paramArgs`, `indexTy`, plus both `constructors` and the legacy `cons` alias). Lookup helpers stay close to caller intent: `constructors`, `fields`, `constructorByName`, and `constructorByIx` accept either pre-normalised meta or a raw `DataSpec`. Mismatched names and out-of-range indices throw with descriptive messages; nothing fails silently. Field-level resolution threads `prev` — the record of previously-materialised dependent-field values — so `fieldType field prev` returns the kernel HOAS type for both concrete fields (`field.type`) and dependent fields (`field.typeFn prev`). `targetIndex con prev` resolves a constructor's target index in the datatype's index type the same way. `fieldRole` exposes the optional `role` annotation used by generic derivations to distinguish payload from proof-bearing fields. ## `constructorByIx` _constructorByIx: lookup a constructor by zero-based declaration-order index; throws on out-of-range or non-integer index._ ``` constructorByIx : Meta | DataSpec -> Int -> Constructor ``` ## `constructorByName` _constructorByName: lookup a constructor by name in a datatype's meta/`DataSpec`; throws on unknown name with a descriptive message._ ``` constructorByName : Meta | DataSpec -> String -> Constructor ``` ## `constructors` _constructors: extract the constructor list from a meta or `DataSpec` — accepts either a pre-normalised meta with `constructors` or a raw `DataSpec`, calling `datatypeInfo` for the latter._ ``` constructors : Meta | DataSpec -> [Constructor] ``` ## `datatypeInfo` _datatypeInfo: produce normalised metadata from either a `DataSpec` (record with `_dtypeMeta`) or an `H.app` spine whose head is a polymorphic datatype; for polymorphic heads, applies `meta.instantiate` to the spine args._ ``` datatypeInfo : DataSpec | Hoas -> Meta ``` ## `fieldRole` _fieldRole: extract the `role` annotation from a field, or `null` if unset. Used by generic derivations to distinguish e.g. proof-bearing fields from ordinary payload._ ``` fieldRole : Field -> String | null ``` ## `fieldType` _fieldType: resolve a field's HOAS type — prefers `field.typeFn prev` (dependent fields) over `field.type` (concrete); throws if neither slot is filled._ ``` fieldType : Field -> AttrSet -> Hoas ``` ## `fields` _fields: extract the field list from a constructor record; defaults to `[]` for constructors with no fields slot._ ``` fields : Constructor -> [Field] ``` ## `instantiate` _instantiate: apply parameter arguments to a polymorphic datatype `DataSpec`, returning the normalised meta for that instantiation; throws for non-polymorphic datatypes given non-empty `paramArgs`._ ``` instantiate : DataSpec -> [Hoas] -> Meta ``` ## `isDatatype` _isDatatype: predicate identifying values that carry `_dtypeMeta` directly, or app-spines whose head carries `_dtypeMeta` (instantiated polymorphic datatypes)._ ``` isDatatype : Any -> Bool ``` ## `normalizeMetadata` _normalizeMetadata: canonicalise a raw `_dtypeMeta` attrset — requires `constructors`, defaults `indexed`/`params`/`paramArgs`/`indexTy`, and pre-fills every field with `level`/`eq`/`proof`/`type`/`typeFn`/`S`/`SFn`/`idxFn`/`branchIdxFn`/`role`/`source` slots._ ``` normalizeMetadata : Meta -> Meta ``` ## `targetIndex` _targetIndex: compute a constructor's target index in the datatype's index type — applies `con.targetIdx prev` where `prev` is the materialised previous-field record; throws if no `targetIdx` function is present._ ``` targetIndex : Constructor -> AttrSet -> Hoas ``` #### Value Two-way conversion between HOAS values and Nix constructor-records, plus generic structural traversal. The type argument accepts either an HOAS type directly or a datatype-output record carrying `.T`; everything else is normalised by `typeOf` internally. `view` is the HOAS-or-Nix → tagged-record direction: record/variant datatypes return `{ _con = "name"; …fields }`, primitives flatten to their native Nix form. The function is idempotent on values already in HOAS form. `review` is the inverse, kernel-elaborating a Nix value back to HOAS via `Elab.elaborateValue`. `toConstructorRecord` and `fromConstructorRecord` are intent-named aliases preferred in datatype-generic code. `fold` consumes a `{ = Fields -> R; default? }` map and dispatches on `_con`; `recAt` fields are recursively folded before being passed in, all other fields appear raw. Missing constructors fall through to `cases.default constructor` or throw. `mapChildren` rewrites a single layer: each `recAt` field is replaced by `childMapper field child`; non-recursive fields pass through unchanged. The result is normalised by `view` so it round-trips with `review`. ## `fold` _fold: structural fold over a generated datatype value — dispatches on the constructor's name in `cases`, recursing into `recAt` fields before invoking the case function with the record of materialised fields._ ``` fold : Hoas -> { = Fields -> R; default? : Constructor -> Fields -> R; } -> NixVal -> R ``` Each case in `cases` receives a record of materialised fields keyed by field name; `recAt` fields have already been folded recursively, all other fields appear as raw Nix values. Missing constructors fall through to `cases.default constructor` if present, else throw `"missing case ''"`. Implementation note: walks via `view` + `constructorByName`, so the input must be either a constructor-record or a value elaborable via `review`. Non-attrset value views (e.g., raw `Nat`) bypass this function — caller folds over the primitive directly. ## `fromConstructorRecord` _fromConstructorRecord: alias of `review` — the datatype-generic name for the record-to-value direction; preferred when expressing intent over generated datatypes._ ``` fromConstructorRecord : Hoas | { T : Hoas; ... } -> NixVal -> Hoas ``` ## `mapChildren` _mapChildren: rewrite a single layer of a generated datatype — for each `recAt` field, apply `childMapper field child`; non-recursive fields pass through unchanged. The result is `view`-normalised so it round-trips with `review`._ ``` mapChildren : Hoas -> (Field -> NixVal -> NixVal) -> NixVal -> NixVal ``` ## `review` _review: Nix-value → HOAS-value — friendly alias of `Elab.elaborateValue` taking the type either as `Hoas` directly or via a `.T` field (datatype outputs); inverse of `view`._ ``` review : Hoas | { T : Hoas; ... } -> NixVal -> Hoas ``` ## `toConstructorRecord` _toConstructorRecord: alias of `view` — the datatype-generic name for the value-to-record direction; preferred when expressing intent over generated datatypes._ ``` toConstructorRecord : Hoas | { T : Hoas; ... } -> Hoas | NixVal -> NixVal ``` ## `view` _view: HOAS-value or Nix-value → Nix constructor-record — kernel-elaborates if given a Nix value, then `Elab.extract`s back to a tagged record (`_con` for record/variant datatypes, primitive for leaf types)._ ``` view : Hoas | { T : Hoas; ... } -> Hoas | NixVal -> NixVal ``` Idempotent on the HOAS form: when `value` is already an HOAS tree (`_htag` present), it skips re-elaboration. Otherwise round-trips through `review` to produce an HOAS value, then evaluates and extracts. Returns a constructor-record `{ _con = "name"; ...fields }` for record/variant datatypes. Primitive values (Nat, String, ...) flatten to their native Nix form. Cross-ref: `fromConstructorRecord` is the alias name used in the datatype-generic API surface. #### Derive Generic, codegen-friendly summaries of a datatype's shape. Each helper consumes a `DataSpec` (or pre-normalised meta) and produces a pure-data record that downstream tooling — schema validators, doc renderers, codegen, dependency analysers — can consume without re-walking the kernel HOAS. `typeDescriptor` projects an arbitrary HOAS or `Val` type to `{ kind; … }` with a finite tag set covering primitives (`nat`/`bool`/`int`/`string`/`float`/`attrs`/`path`/`function`/`any`/`unit`/`universe`), algebraic forms (`list { elem }`, `sum { left; right }`, `datatype { name; args? }`), recursive sites (`recursive`), and unknown fallthrough (`hoas { tag }`). `deriveDescriptor` is the base artefact: name, constructors, per-field records with kind / index / level / role / source / proof / recursive-index / branch-index / dependent-type / type-descriptor. `deriveSchema` projects to JSON-Schema-flavoured `{ title; oneOf }`. `deriveDocs` re-shapes for Markdown / HTML rendering. `deriveFold` produces the fold-skeleton record for code generators. `deriveDeps` walks a constructor record, emitting a `{ nodes; edges }` graph that records constructor descents (`recAt` recursion sites), role-tagged dependency fields (matched against a fixed role policy), and proof-bearing fields. The output is consumable by the generic graph renderer and by build-time dependency analysers. ## `deriveDeps` _deriveDeps: build a node-and-edge dependency graph from a value — walks each constructor record, emitting nodes for constructors and edges for `recAt` recursion sites, role-tagged dependency fields, and proof-bearing fields._ ``` deriveDeps : DataSpec -> NixVal -> { datatype, root, rootPath, indexed, index, nodes, edges } ``` Traversal seeds at path `\"$\"` and recurses into each `recAt` field, accumulating constructor nodes and per-field edges. Emitted edge kinds: - `recAt` field on a constructor: edge from parent to child constructor, carrying `field`, `role`, `source`, and the resolved child type. - Dependency-role field (matching the role-policy predicate): edge to a referenced datatype. - Proof-bearing field (`proof = true`): edge tagging the unresolved obligation. The returned `nodes` / `edges` shape is consumable by the generic graph renderer and by build-time dependency analysers. ## `deriveDescriptor` _deriveDescriptor: produce a full structured descriptor of a datatype — name, constructors, fields with kinds/types/roles, and source provenance. The base artefact other `derive*` helpers consume._ ``` deriveDescriptor : DataSpec -> { name : String; constructors : [ConstructorDescriptor]; ... } ``` ## `deriveDocs` _deriveDocs: produce a documentation-friendly descriptor — same `name` and `constructors` as `deriveDescriptor` plus a `heading` field for use in Markdown/HTML rendering._ ``` deriveDocs : DataSpec -> { heading : String; name : String; constructors : [...] } ``` ## `deriveFold` _deriveFold: produce a fold-skeleton descriptor — `{ datatype; cases = [{ name; fields = [{ name; kind; type; }] }] }`; consumed by code generators emitting per-datatype fold scaffolds._ ``` deriveFold : DataSpec -> { datatype : String; cases : [Object] } ``` ## `deriveSchema` _deriveSchema: produce a JSON-Schema-flavoured `{ title, oneOf : [ConstructorSchema] }` from a datatype — each constructor schema records its name, field name+kind+type, and metadata flags (target index, proof, etc.)._ ``` deriveSchema : DataSpec -> { title : String; oneOf : [Object] } ``` ## `typeDescriptor` _typeDescriptor: convert a kernel type (HOAS or `Val`) into a structured descriptor record — `{ kind; name?; constructors?; ... }` summarising the shape; used downstream by schema, docs, and dependency derivations._ ``` typeDescriptor : Hoas | Val | null -> { kind : String; ... } ``` #### Check The single polymorphic fold over the full HOAS type algebra, parameterised by an `Algebra A`. Two canonical algebras live alongside it: `unitAlg` returns `null` at every node and is used for validation (the walker emits `typeCheck` effects on failure); `hoasAlg` constructs the corresponding HOAS term at every node and is used for elaboration. Validation and elaboration are not two functors — they are one fold instantiated at two carriers. `deriveCheck` and `deriveElaborate` thread `unitAlg` / `hoasAlg` respectively. Both accept `(ty, path, value)` and return a `Computation A`; the walker owns shape inspection, `_htag` dispatch, `fx.kernel` effect emissions, path threading, and the dependent-Σ snd-type derivation via an internal strict-handler trampoline on fst. Algebras own per-shape success-case construction and the field-walker (so `unitAlg` matches the legacy `f.type`-only traversal while `hoasAlg` threads `prev` through `fieldType` for dependent-field resolution). `checkWithGuard` composes `deriveCheck` with a refinement predicate: shape-check via the unit walker first, predicate second (the two cannot run in parallel — the predicate's domain is exactly the values that pass shape). When the eventual Σ-with-Decide-snd encoding lands, this special case collapses into `deriveCheck` over Σ-types and the helper deletes. Failure reasons carry one of `shape-mismatch`, `missing-field`, `extra-field`, `predicate-failed`, or `deferred-pi`, routed by handlers under `fx.effects.typecheck.*`. ## `checkWithGuard` _checkWithGuard: refinement-predicate-aware variant of `deriveCheck` — runs shape check first via `unitAlg`, then composes the type's refinement predicate when present; required while refined types are encoded outside the canonical Σ-with-Decide-snd form._ ``` checkWithGuard : Type -> Path -> Value -> Computation Null ``` ## `deriveCheck` _deriveCheck: canonical typed walker over the HOAS algebra threading `unitAlg` — emits `fx.effects.typecheck` failures with structured `reason` (shape-mismatch, missing-field, extra-field, predicate-failed, deferred-pi) and `path` (a `fx.diag.positions` chain)._ ``` deriveCheck : Type -> Path -> Value -> Computation Null ``` ## `deriveElaborate` _deriveElaborate: canonical typed walker over the HOAS algebra threading `hoasAlg` — reconstructs the corresponding HOAS term at every successfully-checked node; shares path threading and dispatch with `deriveCheck`._ ``` deriveElaborate : Type -> Path -> Value -> Computation Hoas ``` #### CheckD checkD: generic bidirectional checker for Desc payloads — validates terms against `interpD level I D X i` by walking the description. ## `checkD` _checkD: check a payload term against the interpretation of a description at a concrete index and recursive family._ ``` checkD : Ctx -> Level -> I -> D -> X -> i -> Tm -> Computation Tm ``` ## `checkDAt` _checkDAt: spec-record variant of checkD; spec contains `{ level; I; D; X; i; }`._ ``` checkDAt : Ctx -> { level; I; D; X; i; } -> Tm -> Computation Tm ``` ## `inferD` _inferD: synthesize the checked payload and its `interpD level I D X i` type._ ``` inferD : Ctx -> Level -> I -> D -> X -> i -> Tm -> Computation { term; type; } ``` ## `inferDAt` _inferDAt: spec-record variant of inferD; spec contains `{ level; I; D; X; i; }`._ ``` inferDAt : Ctx -> { level; I; D; X; i; } -> Tm -> Computation { term; type; } ``` #### Ornaments Three families of ornament construction over kernel descriptions: raw structural ornaments (`ornament`, `compose`, `forget`, `forgetHoas`, `pullback`, `pullbackHoas`), algebraic ornaments derived from algebras over the supported fragment (`algOrn`, `algShape`, `algSupportedFragment`, `algShapeDiagnostics`, `checkAlgShape`, `liftFold`), and functional ornaments for lifting producers / transforms through canonical output sections (`functional`, `functionalSection`, `functionalTargetIndex`, `functionalBuildIndexed` / `functionalBuild`, `liftProducerIndexed` / `liftProducer`, `liftTransformIndexed` / `liftTransform`, `composeFunctional`). Validation is uniform across families: every constructor has a `validate` / `try` / `diagnose` triple (`validateSpec` / `tryOrnament` / `diagnoseSpec`, `validateAlgOrn` / `tryAlgOrn` / `diagnoseAlgOrn`, `validateFunctional` / `tryFunctional` / `diagnoseFunctional`, plus the law-level `validateFunctionalLaws` / `diagnoseFunctionalLaws`). `validate*` throws on rejection with a structured diagnostic, `try*` returns `{ success; value | diagnostic }`, `diagnose*` always returns the diagnostic record. The supported algebraic fragment is `[ "ret" "arg" "rec" "pi" "plus" ]`. Convenience aliases `section`, `targetIndex`, `buildIndexed`, and `build` re-export the corresponding `functional*` entries for callers writing in the functional-ornament idiom. ## `algOrn` _algOrn: build an algebraic ornament from `args`, running `checkAlgShape` first so unsupported algebra shapes are rejected with structured diagnostics before delegating to `H.algOrn`._ ``` algOrn : { I?, J, baseD | D, erase, algebra, ... } -> Ornament ``` ## `algShape` _algShape: total predicate `{ ok, kind, expected }` over an algebra value's shape — surfaces the offending `kind` when the algebra falls outside `algSupportedFragment`._ ``` algShape : Algebra -> { ok : Bool, kind? : String, expected? : [String] } ``` ## `algShapeDiagnostics` _algShapeDiagnostics: human-readable diagnostic strings for an algebra whose shape falls outside the supported `ret/arg/rec/pi/plus` fragment._ ``` algShapeDiagnostics : Attrs -> [String] ``` ## `algSupportedFragment` _algSupportedFragment: the constructor-tag whitelist `[ "ret" "arg" "rec" "pi" "plus" ]` accepted by `algOrn`; surfaced as the `expected` field of `algShape`'s diagnostic when an algebra falls outside this fragment._ ``` algSupportedFragment : [String] ``` ## `build` _functionalBuild: build the ornamented value from `baseValue` at the unit index `tt` — convenience wrapper for J = `Unit` functional ornaments._ ``` functionalBuild : FunctionalOrnament -> Tm -> Tm ``` ## `buildIndexed` _functionalBuildIndexed: build the ornamented value at an explicit `index` from `baseValue` — wraps `H.ornBuild` for the indexed surface._ ``` functionalBuildIndexed : FunctionalOrnament -> Tm -> Tm -> Tm ``` ## `checkAlgShape` _checkAlgShape: return `algebra` unchanged when it lies in the supported fragment, otherwise throw with the concatenated `algShapeDiagnostics` text._ ``` checkAlgShape : Attrs -> Algebra -- throws on unsupported shape ``` ## `checkSpec` _checkSpec: surface `_ornMeta` from a built ornament — exposes `{ base, spec, core, cfg }` for downstream introspection of the ornament's provenance and shape._ ``` checkSpec : DataSpec -> OrnamentSpec -> Attrs ``` ## `compose` _compose: vertical composition of ornaments at the generic surface — `outer ∘ inner` lifts the inner ornament's J through the outer's K, automatically unpacking `.ornament` cores._ ``` compose : Ornament -> Ornament -> Ornament -- outer, inner ``` ## `composeFunctional` _composeFunctional: vertical composition of two functional ornaments — threads `inner.chooseIndex` and `inner.section` into `outer` to produce a stacked section pipeline._ ``` composeFunctional : FunctionalOrnament -> FunctionalOrnament -> FunctionalOrnament ``` ## `diagnoseAlgOrn` _diagnoseAlgOrn: human-readable diagnostic strings for a candidate algebraic-ornament spec — derived from `H.algOrnDiagnostics`, suitable for error messages and tests._ ``` diagnoseAlgOrn : Attrs -> [String] ``` ## `diagnoseFunctional` _diagnoseFunctional: human-readable diagnostic strings for a candidate functional-ornament spec — derived from `validateFunctional` for surfacing in errors._ ``` diagnoseFunctional : Attrs -> [String] ``` ## `diagnoseFunctionalLaws` _diagnoseFunctionalLaws: human-readable diagnostic strings for failed law checks on a functional ornament — surfaces which law-check returned non-`true` or failed to evaluate._ ``` diagnoseFunctionalLaws : FunctionalOrnament -> [String] ``` ## `diagnoseSpec` _diagnoseSpec: human-readable diagnostic strings for a candidate `(base, spec)` — derived from `H.ornamentDiagnostics`, suitable for error messages and tests._ ``` diagnoseSpec : DataSpec -> OrnamentSpec -> [String] ``` ## `forget` _forget: apply the forgetting morphism to an ornamented `value`, returning the underlying base-description value at the same J-index._ ``` forget : OrnamentedDatatype -> Tm -> Tm ``` ## `forgetHoas` _forgetHoas: yield the HOAS-level `forget` term `J -> ornMu -> baseMu` for an ornamented datatype or raw ornament; prefers `forget0` when the J-index is `Unit`._ ``` forgetHoas : OrnamentedDatatype -> Tm ``` ## `functional` _functional: total smart constructor for a functional ornament — validates `args` via `validateFunctional` and throws with diagnostics on failure, otherwise returns the built record._ ``` functional : Attrs -> FunctionalOrnament -- throws on invalid spec ``` ## `functionalBuild` _functionalBuild: build the ornamented value from `baseValue` at the unit index `tt` — convenience wrapper for J = `Unit` functional ornaments._ ``` functionalBuild : FunctionalOrnament -> Tm -> Tm ``` ## `functionalBuildIndexed` _functionalBuildIndexed: build the ornamented value at an explicit `index` from `baseValue` — wraps `H.ornBuild` for the indexed surface._ ``` functionalBuildIndexed : FunctionalOrnament -> Tm -> Tm -> Tm ``` ## `functionalSection` _functionalSection: extract the `section` slot of a functional ornament — the builder `i -> baseValue -> ornamentedValue` realising the section morphism._ ``` functionalSection : FunctionalOrnament -> (Tm -> Tm -> Tm) ``` ## `functionalTargetIndex` _functionalTargetIndex: extract the `chooseIndex` slot of a functional ornament — the function `i -> baseValue -> J` that picks the ornamented index per base input._ ``` functionalTargetIndex : FunctionalOrnament -> (Tm -> Tm -> Tm) ``` ## `lift` _lift: functorial container lifts — given an ornament `O : Ornament A A'`, `lift.list` / `lift.attrs` / `lift.maybe` produce `Ornament (F A) (F A')` whose forget is the standard functorial action of `F` on `O.forget`; `lift.field name O` is the elementary product-component move. Inputs may be leaf or decorated μ-ornaments; the μ-case delegates element forget to the meta walker._ ``` lift : { list : Ornament -> Ornament, attrs : Ornament -> Ornament, maybe : Ornament -> Ornament, field : String -> Ornament -> Ornament } ``` ## `liftFold` _liftFold: alias for `pullbackHoas` specialised to folds — composes a base fold with `forget` so it runs on ornamented carriers without re-deriving the algebra._ ``` liftFold : OrnamentedDatatype -> (Tm -> Tm) -> Tm -> Tm ``` ## `liftProducer` _liftProducer: lift a base producer through a functional ornament at unit index `tt` — convenience wrapper for J = `Unit`._ ``` liftProducer : FunctionalOrnament -> (Tm -> Tm) -> Tm -> Tm ``` ## `liftProducerIndexed` _liftProducerIndexed: lift a base producer `baseFn : baseInput -> baseValue` through a functional ornament at an explicit J-index, returning the ornamented output._ ``` liftProducerIndexed : FunctionalOrnament -> Tm -> (Tm -> Tm) -> Tm -> Tm ``` ## `liftTransform` _liftTransform: lift a base transform through paired input/output functional ornaments at unit indices — convenience wrapper for J = `Unit` on both sides._ ``` liftTransform : { input, output : FunctionalOrnament, fn } -> Tm -> Tm ``` ## `liftTransformIndexed` _liftTransformIndexed: lift a base transform through paired input/output functional ornaments at explicit indices — auto-unpacks `.ornament` cores from the input side._ ``` liftTransformIndexed : { input, output : FunctionalOrnament, fn } -> Tm -> Tm -> Tm -> Tm ``` ## `ornament` _ornament: user-facing entry — given a generated `DataSpec` (`base`) and a constructor-by-constructor `spec`, produce the ornamented `Datatype` with `forget` morphism baked in._ ``` ornament : DataSpec -> OrnamentSpec -> Datatype ``` ## `pullback` _pullback: apply a base function `baseFn` to a forgotten ornamented value — the value-level companion to `pullbackHoas`, running entirely in HOAS application._ ``` pullback : OrnamentedDatatype -> Tm -> Tm -> Tm ``` ## `pullbackHoas` _pullbackHoas: HOAS-level pullback of a base function `baseFn : I -> baseMu -> R(i)` through an ornamented carrier — auto-unpacks `.ornament` cores from generated datatypes._ ``` pullbackHoas : OrnamentedDatatype -> (Tm -> Tm) -> Tm -> Tm ``` ## `section` _functionalSection: extract the `section` slot of a functional ornament — the builder `i -> baseValue -> ornamentedValue` realising the section morphism._ ``` functionalSection : FunctionalOrnament -> (Tm -> Tm -> Tm) ``` ## `tryAlgOrn` _tryAlgOrn: total constructor — returns `{ ok, diagnostics, value? }` where `value` is the algebraic ornament when valid, otherwise diagnostics-only._ ``` tryAlgOrn : Attrs -> { ok : Bool, diagnostics : [Diagnostic], value? : Ornament } ``` ## `tryFunctional` _tryFunctional: total constructor — returns `{ ok, diagnostics, value? }` where `value` is the built functional ornament when valid, otherwise diagnostics-only._ ``` tryFunctional : Attrs -> { ok : Bool, diagnostics : [Diagnostic], value? : FunctionalOrnament } ``` ## `tryOrnament` _tryOrnament: total constructor — returns `{ ok, diagnostics, value? }` where `value` is the built ornamented datatype when valid, otherwise diagnostics-only._ ``` tryOrnament : DataSpec -> OrnamentSpec -> { ok : Bool, diagnostics : [Diagnostic], value? : Datatype } ``` ## `validateAlgOrn` _validateAlgOrn: total predicate `{ ok, diagnostics }` over candidate `algOrn` args — checks each algebra arm against its description shape without throwing._ ``` validateAlgOrn : Attrs -> { ok : Bool, diagnostics : [Diagnostic] } ``` ## `validateFunctional` _validateFunctional: total predicate `{ ok, diagnostics }` over candidate functional-ornament `args` — `coreOf` is applied to `.ornament` first so generated datatypes pass through._ ``` validateFunctional : Attrs -> { ok : Bool, diagnostics : [Diagnostic] } ``` ## `validateFunctionalLaws` _validateFunctionalLaws: delegates to `H.validateFunctionalLaws` — total predicate `{ ok, diagnostics }` over a functional ornament's law-check bundle._ ``` validateFunctionalLaws : FunctionalOrnament -> { ok : Bool, diagnostics : [Diagnostic] } ``` ## `validateSpec` _validateSpec: total predicate `{ ok, diagnostics }` over an `(base, spec)` candidate for the user-facing `ornament` surface — never throws._ ``` validateSpec : DataSpec -> OrnamentSpec -> { ok : Bool, diagnostics : [Diagnostic] } ``` #### Verified High-level combinators for writing kernel-checked implementations. Write programs with these combinators, then call `v.verify` to type-check and extract a Nix function that is correct by construction. ## Example ```nix # Verified successor: Nat → Nat v.verify (H.forall "x" H.nat (_: H.nat)) (v.fn "x" H.nat (x: H.succ x)) # → Nix function: n → n + 1 ``` ## Literals - `nat : Int → Hoas` — natural number literal (S^n(zero)) - `str : String → Hoas` — string literal - `int_ : Int → Hoas` — integer literal - `float_ : Float → Hoas` — float literal - `true_`, `false_` — boolean literals - `null_` — unit value (tt) ## Binding - `fn : String → Hoas → (Hoas → Hoas) → Hoas` — lambda abstraction - `let_ : String → Hoas → Hoas → (Hoas → Hoas) → Hoas` — let binding ## Data Operations - `pair`, `fst`, `snd` — Σ-type construction and projection - `field : Hoas → String → Hoas → Hoas` — record field projection by name - `inl`, `inr` — Sum injection - `app` — function application ## Eliminators (Constant Motive) These auto-generate the motive `λ_.resultTy`, so you only supply the result type and the branches: - `if_ : Hoas → Hoas → { then_; else_; } → Hoas` — Bool elimination - `match : Hoas → Hoas → { zero; succ : k → ih → Hoas; } → Hoas` — Nat elimination - `matchList : Hoas → Hoas → Hoas → { nil; cons : h → t → ih → Hoas; } → Hoas` — List elimination - `matchSum : Hoas → Hoas → Hoas → Hoas → { left; right; } → Hoas` — Sum elimination - `matchData : Datatype → Hoas → Hoas → { ; } → Hoas` — any non-parametric `H.datatype` - `elimData : Level → Datatype → Hoas → Hoas → { ; } → Hoas` — `matchData` with a dependent motive ## Derived Combinators - `map : Hoas → Hoas → Hoas → Hoas → Hoas` — map f over a list - `fold : Hoas → Hoas → Hoas → Hoas → Hoas → Hoas` — fold over a list - `filter : Hoas → Hoas → Hoas → Hoas` — filter a list by predicate ## Pipeline - `verify : Hoas → Hoas → NixValue` — type-check + eval + extract - `verifiedFn : Hoas → Hoas → VerifiedValue` — callable value with `_hoasImpl` for full kernel body verification in parent types ## `app` _app: HOAS function application — `app f arg` builds the redex; β-reduces during normalisation when `f` is a lambda._ ``` app : Hoas -> Hoas -> Hoas ``` ## `elimData` _elimData: generic non-parametric user-datatype eliminator with dependent motive — builds branch binders from `_dtypeMeta` and ann-wraps each body against the meta-β-reduced motive image, so branch bodies are checked rather than synthesised. Handlers keyed by ctor name, curried over fields then one IH per `recAt` field._ ``` elimData : Level -> Datatype -> Hoas -> Hoas -> { : field… -> ih… -> Hoas; } -> Hoas ``` ## `false_` _false_: HOAS literal — the `False` constructor of `H.bool` as `inl tt`; reflects the bool-as-sum levitation discipline._ ``` false_ : Hoas ``` ## `field` _field: HOAS record field-projection by name — derives the universal-property eliminator for a mono-constructor datatype and applies it to extract the named field._ ``` field : Hoas -> String -> Hoas -> Hoas ``` Requires `recordTy` to carry `_dtypeMeta` with exactly one constructor (records are mono-constructor datatypes via `H.record`). Throws at build time if the type is multi-constructor, has no fields, or the requested field name is absent. The 1-field special case reduces via ι (`elim P (λa.a) (mk a) ≡ a`); no surface branching is needed. Field positions are read from `meta.constructors[0].fields` in declaration order, so renaming a field in source is a breaking change. ## `filter` _filter: HOAS list-filter combinator — keeps elements where `pred : elemTy -> Bool` returns `true_`; built on `listElim` plus per-element `boolElim`._ ``` filter : Hoas -> Hoas -> Hoas -> Hoas ``` Annotates `pred` with `H.forall "_" elemTy (_: H.bool)` so the application infers. `pred` must be a HOAS function term producing a `bool` HOAS value (e.g. via `if_`, `match`, or a direct `true_`/`false_`). Element order is preserved; the accumulator threads via the inductive hypothesis. ## `float_` _float_: HOAS literal — lift a Nix float to a `floatLit` HOAS term checkable against `H.float_`._ ``` float_ : Float -> Hoas ``` ## `fn` _fn: HOAS lambda — `fn name domTy body` builds `λ(name:domTy). body`, with `body` a Nix function receiving the bound variable as a HOAS term._ ``` fn : String -> Hoas -> (Hoas -> Hoas) -> Hoas ``` ## `fold` _fold: HOAS list-fold combinator — combines elements right-to-left using `f : elemTy -> resultTy -> resultTy` starting from `init`._ ``` fold : Hoas -> Hoas -> Hoas -> Hoas -> Hoas -> Hoas ``` Annotates `f` with `H.forall "_" elemTy (_: H.forall "_" resultTy (_: resultTy))` so the applications `f h ih` infer correctly. `f` must be a HOAS function term built with `fn` (or nested `fn` for the curried two-argument case). The accumulator threads through `ih`; the empty-list case returns `init`. ## `fst` _fst: first projection on a HOAS Σ-pair; reduces by π₁ during normalisation._ ``` fst : Hoas -> Hoas ``` ## `if_` _if_: bool-elimination wrapper — supplies a constant motive `λ_.resultTy` so callers write only the result type and the two branches._ ``` if_ : Hoas -> Hoas -> { then_ : Hoas; else_ : Hoas; } -> Hoas ``` Use when the branch result type does not depend on the scrutinee. For a dependent motive (different result type per branch), drop to `H.boolElim` directly. The synthesised motive is `λ_:bool.resultTy`; `boolElim`'s level argument is fixed at 0 here, so cross-universe branching also needs `H.boolElim`. ## `inl` _inl: HOAS sum left-injection — `inl leftTy rightTy term` builds an `A + B` term carrying `term : A` in the left branch._ ``` inl : Hoas -> Hoas -> Hoas -> Hoas ``` ## `inr` _inr: HOAS sum right-injection — `inr leftTy rightTy term` builds an `A + B` term carrying `term : B` in the right branch._ ``` inr : Hoas -> Hoas -> Hoas -> Hoas ``` ## `int_` _int_: HOAS literal — lift a Nix integer to an `intLit` HOAS term checkable against `H.int_` (the kernel `Int` axiom, distinct from `nat`)._ ``` int_ : Int -> Hoas ``` ## `let_` _let_: HOAS let binding — `let_ name ty val body` builds `let name : ty = val in body`; `body` is a Nix function receiving the bound variable._ ``` let_ : String -> Hoas -> Hoas -> (Hoas -> Hoas) -> Hoas ``` ## `makeRecord` _makeRecord: HOAS record construction by name — applies the mono-constructor of a μ-encoded record type to the field values in declaration order, dual to `field`'s named projection._ ``` makeRecord : Hoas -> { = Hoas; ... } -> Hoas ``` Dual to `field`. Given a record type built by `H.record` (or any mono-constructor datatype carrying `_dtypeMeta`) and an attrset keyed by field name, produces the HOAS term `mk a₀ … aₙ₋₁` where each `aᵢ` is read from `argsAttrs` by field name, and field order follows `meta.constructors[0].fields` (for `H.record`, alphabetical by name). Throws at build time on a multi-constructor datatype, a missing required field, or an unknown extra field. The constructor term used is `meta.constructors[0].ctor`, exposed on the kernel datatype attrset by `_datatypeImpl`. Use this in verified implementations that return records, so consumer code never needs to reach for `builtins.foldl' H.app DT.mk [...]` or know the μ-encoding. ## `map` _map: HOAS list-map combinator built on `listElim` — applies `f : elemTy -> resultTy` to every element, threading the inductive hypothesis to accumulate the output list._ ``` map : Hoas -> Hoas -> Hoas -> Hoas -> Hoas ``` Annotates `f` with `H.forall "_" elemTy (_: resultTy)` so the kernel can infer the application type. `f` must be a HOAS function term (build one with `fn`), not a Nix function. For element transformations that aren't expressible as a uniform HOAS function (e.g. type-dependent on element shape), drop to `H.listElim` directly. ## `match` _match: nat-elimination wrapper — supplies a constant motive `λ_.resultTy`; the `succ` callback receives both predecessor `k` and inductive hypothesis `ih`._ ``` match : Hoas -> Hoas -> { zero : Hoas; succ : Hoas -> Hoas -> Hoas; } -> Hoas ``` The succ callback receives two HOAS values: `k` (the predecessor binder, of type `nat`) and `ih` (the inductive hypothesis, of type `resultTy`). Use when result type is non-dependent on the scrutinee; drop to `H.ind` for dependent motives. Level is fixed at 0 by the synthesised motive. ## `matchData` _matchData: `elimData` with constant motive `λ_.resultTy` — the `matchSum`/`matchList` analogue for arbitrary non-parametric `H.datatype`s._ ``` matchData : Datatype -> Hoas -> Hoas -> { : field… -> ih… -> Hoas; } -> Hoas ``` ## `matchList` _matchList: list-elimination wrapper — constant motive `λ_.resultTy`; the `cons` callback binds head, tail, and inductive hypothesis._ ``` matchList : Hoas -> Hoas -> Hoas -> { nil : Hoas; cons : Hoas -> Hoas -> Hoas -> Hoas; } -> Hoas ``` The cons callback receives three HOAS values: `h` (the head element of type `elemTy`), `t` (the tail list of type `listOf elemTy`), and `ih` (the inductive hypothesis of type `resultTy`). For dependent motives, drop to `H.listElim`. Level fixed at 0. ## `matchSum` _matchSum: sum-elimination wrapper — constant motive `λ_.resultTy`; the left and right callbacks receive their respective payloads._ ``` matchSum : Hoas -> Hoas -> Hoas -> Hoas -> { left : Hoas -> Hoas; right : Hoas -> Hoas; } -> Hoas ``` Each callback receives one HOAS value: the left payload for `left`, the right payload for `right`. Use when the result type doesn't depend on which case fires; drop to `H.sumElim` for dependent motives. Level fixed at 0. ## `nat` _nat: HOAS literal — wrap a Nix `Int` as a `succ^n zero` natural-number HOAS term checkable against `H.nat`._ ``` nat : Int -> Hoas ``` ## `null_` _null_: HOAS literal — the unique inhabitant `tt` of `H.unit`; the conventional empty / placeholder value in verified code._ ``` null_ : Hoas ``` ## `pair` _pair: HOAS Σ-pair constructor — `pair fst snd` packages two HOAS values; the surrounding annotation fixes which Σ-type the pair inhabits._ ``` pair : Hoas -> Hoas -> Hoas ``` ## `snd` _snd: second projection on a HOAS Σ-pair; reduces by π₂ during normalisation._ ``` snd : Hoas -> Hoas ``` ## `str` _str: HOAS literal — lift a Nix `String` to a `stringLit` HOAS term checkable against `H.string`._ ``` str : String -> Hoas ``` ## `strElem` _strElem: HOAS membership check on a `List String` — folds `strEq target` across the list, accumulating `true_` if any element matches._ ``` strElem : Hoas -> Hoas -> Hoas ``` Built on `fold` plus `strEq`: each list element is compared to `target`; the accumulator starts at `false_` and stays `true_` once a match is seen. Use when verified code needs a membership predicate over strings; for raw kernel-level equality on a single pair, use `strEq`. ## `strEq` _strEq: kernel string equality returning a `Bool` HOAS term; reflects the `mkStrEq` primitive of the kernel._ ``` strEq : Hoas -> Hoas -> Hoas ``` ## `true_` _true_: HOAS literal — the `True` constructor of `H.bool` as `inr tt`; reflects the bool-as-sum levitation discipline._ ``` true_ : Hoas ``` ## `verifiedFn` _verifiedFn: extract a kernel-verified callable carrying `_hoasImpl` — `elaborateValue`'s Pi case uses the HOAS body for full re-verification instead of falling back to an opaque trust boundary._ ``` verifiedFn : Hoas -> Hoas -> { __functor; _hoasImpl } ``` Returns an attrset callable via `__functor` (so `(verifiedFn piTy body) arg` works) and carrying `_hoasImpl = body` so parent elaboration can re-check the body against a more general type instead of treating it as an opaque lambda. Use when the verified function will be embedded inside another verified type-check (e.g. as a field of a verified record); for standalone use, `verify` is simpler. ## `verify` _verify: full pipeline — kernel-check a HOAS implementation against a HOAS type, then evaluate and extract a Nix value witnessing the body._ ``` verify : Hoas -> Hoas -> NixValue ``` Calls `fx.tc.elaborate.verifyAndExtract` internally. Returns the Nix value (typically a function or data structure) that the type-checked HOAS body denotes; consume via normal Nix call or attribute access. Use as the final step of a verified-implementation pipeline. For a callable wrapper that retains the HOAS implementation so parent-type elaboration can re-check the body, use `verifiedFn` instead. #### Kernel Kernel-internal `validate` for the internalizable fragment of `mkType`-buildable types. A code is a base carrier type (any `U_0` type — a mu-encoded inductive or a primitive) paired with a finite stack of predicates over that carrier; the decider is the conjunction of the stack. ## Surface (`ktype`) - `KType : U_1` — a base carrier type paired with a predicate stack - `beta t : U_0` — the base carrier of a code (constant along refinement) - `psi t : beta t -> Bool` — the accumulated membership predicate - `El t` — the derived refinement subtype `Sigma x:beta t. P (psi t x)` - `P` — the `Bool -> U_0` membership decoder (`P true ~> Unit`, `P false ~> Void`) - `betaFn`/`decideFn`/`ElFn` — the same functions as closed kernel terms - `andB`, `iota`/`refine` constructor helpers ## Decision (`decide`) - `decide t : beta t -> Bool` — membership in `El t`; `decide = psi` ## `decide` _fx.tc.kernel.decide: the membership decision procedure decide = psi : (t:KType) -> beta t -> Bool._ ## `failure` _fx.tc.kernel.failure: the kernel transcription of the diagnostic Error ADT (DiagError — a mono-constructor record over Layer/Detail/msg/hint and an opaque Unit children slot) and the Failure of a rejected check as a dependent Sigma-chain (FailureTy = Sigma ktp:KType. Sigma x:(beta ktp). Sigma ctx. Sigma rsn. Sigma pth. DiagError; FailureTheory the base-only Sigma). Layer/Detail sub-shapes, the layerOf/detailOf/mkDiag/mkFailure constructors, the fst_/snd_-spine Failure projections, and the named DiagError field projections._ ## `handlers` _fx.tc.kernel.handlers: the six typecheck-policy handlers (strict/collecting/logging/firstN/summarize/pretty) as closed kernel step terms folding a stream of membership decisions into accumulated state. A decision is a host-known boolean `passed` plus an opaque host-rendered residue Rec; each step is `... -> Σ State payload` whose reduction is the state transition. firstN carries an O(1) down-counter; summarize renders the reason enum to its production key token via `reasonName` and groups failures into a `List (Σ String Nat)` assoc list via `insertOrIncrement` (a `listElim`+`strEq` fold). Exports the step terms, their result shapes, and the Reason/assoc grouping vocabulary._ ## `ktype` _fx.tc.kernel.ktype: flat predicate-stack datum — KType : U_1 (a base carrier type paired with a predicate stack over it), beta (the carrier), the accumulator-fold decider psi/decide, derived El, the membership decoder P, andB, and the iota/refine constructors._ ## `reflect` _fx.tc.kernel.reflect: reflect a Nix-side carrier type into its KType code (reflect/reflectRefine), the checkable Boolean predicate vocabulary for refinement arms — Int over the primitive carrier via the host-backed intLe/intEq (positiveInt, nonNegativeInt, inRangeInt, eqInt) and String literal-set membership (oneOfStrTerm, via strEq) plus non-emptiness (strNonEmptyTerm, via strLen) — and the KernelPred witness layer (mkKernelPred, andKP, isKernelPred, sealed, kernelExpressible, ktypeOf, deriveGuard) with ready-made witnesses: Int (intPositive, intNonNegative, intInRange, intEq) and String (strOneOf, strNonEmpty), O(1)-bridged and guard-derived from their kernel terms._ ## `validate` _fx.tc.kernel.validate: the membership-decision arm of typechecking. validateClosed t v context reason path diagError decides membership with the kernel oracle (elaborate.decide) and either returns `pure v` or raises the host `typeCheck` effect with the caller-supplied diagnostics — the closed-input generalization of the auto-derived validateAt. validateK R t reason path carrier x is the kernel-internal report producer: it emits one kernel `report` op whose decision is the KType decider applied to the carrier (`decide t x`), returning `freeFx (EffTypeCheck R) Resp Unit` for the kernel handlers to fold. validateEl R t payload x is the kernel-internal certifier — the membership-witness dual of validateK: it produces the dependent witness `El t` when `decide t x` accepts, and otherwise aborts on EffError's strict (Void-response) raise carrying the opaque payload, so no witness is ever forged; returns `freeFx (EffError R) Resp_strict (El t)`._ ## Source - [`src/tc/kernel/decide.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/kernel/decide.nix) - [`src/tc/kernel/failure.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/kernel/failure.nix) - [`src/tc/kernel/flat-faithfulness.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/kernel/flat-faithfulness.nix) - [`src/tc/kernel/handlers.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/kernel/handlers.nix) - [`src/tc/kernel/ktype.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/kernel/ktype.nix) - [`src/tc/kernel/level-soundness.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/kernel/level-soundness.nix) - [`src/tc/kernel/reflect.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/kernel/reflect.nix) - [`src/tc/kernel/soundness.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/kernel/soundness.nix) - [`src/tc/kernel/validate.nix`](https://github.com/kleisli-io/nix-effects/blob/main/src/tc/kernel/validate.nix) ### Diagnostic Hints #### DArgSort::universe-mismatch **Key:** `DArgSort::universe-mismatch` **Category:** universe · **Severity:** error **Source:** [`src/diag/hints.nix:844`](https://github.com/kleisli-io/nix-effects/blob/main/src/diag/hints.nix#L844) the sort position of `arg` must live in U(0); descriptions only carry small types. Pass `u 0`, or factor the dependency through `descRec` / `descPi` if a larger type is genuinely needed. ## Example `Hint::DArgSort::universe-mismatch` means the `DArgSort` position uses a universe level that is too large for that slot. The example shows the failing form first, then one way to give the checker the missing structure. ```nix # Bad: description argument sorts must stay in U0. descArg (u 1) (_: descRet tt) # Better: keep the argument sort small. descArg (u 0) (_: descRet tt) ``` --- [← All diagnostic hints](/nix-effects/diag-hints) #### DPiSort::universe-mismatch **Key:** `DPiSort::universe-mismatch` **Category:** universe · **Severity:** error **Source:** [`src/diag/hints.nix:848`](https://github.com/kleisli-io/nix-effects/blob/main/src/diag/hints.nix#L848) the sort position of `pi` must live in U(0); `descPi` takes a small domain. Use `u 0`, or encode the dependency through an index instead of the Pi domain. ## Example `Hint::DPiSort::universe-mismatch` means the `DPiSort` position uses a universe level that is too large for that slot. The example shows the failing form first, then one way to give the checker the missing structure. ```nix # Bad: descPi's domain sort is too large. descPi (u 1) f (_: descRet tt) # Better: use a small domain sort. descPi (u 0) f (_: descRet tt) ``` --- [← All diagnostic hints](/nix-effects/diag-hints) #### LevelMaxLhs::type-mismatch **Key:** `LevelMaxLhs::type-mismatch` **Category:** universe · **Severity:** error **Source:** [`src/diag/hints.nix:922`](https://github.com/kleisli-io/nix-effects/blob/main/src/diag/hints.nix#L922) the left operand of `max` must be a Level ## Example `Hint::LevelMaxLhs::type-mismatch` means the `LevelMaxLhs` position supplies a term whose type does not match the expected type. The example shows the failing form first, then one way to give the checker the missing structure. ```nix # Bad: max's left operand is not a Level. levelMax nat level0 # Better: both operands are levels. levelMax level0 level1 ``` --- [← All diagnostic hints](/nix-effects/diag-hints) #### LevelMaxRhs::type-mismatch **Key:** `LevelMaxRhs::type-mismatch` **Category:** universe · **Severity:** error **Source:** [`src/diag/hints.nix:924`](https://github.com/kleisli-io/nix-effects/blob/main/src/diag/hints.nix#L924) the right operand of `max` must be a Level ## Example `Hint::LevelMaxRhs::type-mismatch` means the `LevelMaxRhs` position supplies a term whose type does not match the expected type. The example shows the failing form first, then one way to give the checker the missing structure. ```nix # Bad: max's right operand is not a Level. levelMax level0 nat # Better: both operands are levels. levelMax level0 level1 ``` --- [← All diagnostic hints](/nix-effects/diag-hints) #### LevelSucPred::type-mismatch **Key:** `LevelSucPred::type-mismatch` **Category:** universe · **Severity:** error **Source:** [`src/diag/hints.nix:920`](https://github.com/kleisli-io/nix-effects/blob/main/src/diag/hints.nix#L920) the predecessor of `suc` must be a Level ## Example `Hint::LevelSucPred::type-mismatch` means the `LevelSucPred` position supplies a term whose type does not match the expected type. The example shows the failing form first, then one way to give the checker the missing structure. ```nix # Bad: suc expects a Level. levelSuc nat # Better: pass a level expression. levelSuc level0 ``` --- [← All diagnostic hints](/nix-effects/diag-hints) #### ULevel::type-mismatch **Key:** `ULevel::type-mismatch` **Category:** universe · **Severity:** error **Source:** [`src/diag/hints.nix:926`](https://github.com/kleisli-io/nix-effects/blob/main/src/diag/hints.nix#L926) the level argument of `U` must be a Level ## Example `Hint::ULevel::type-mismatch` means the `ULevel` position supplies a term whose type does not match the expected type. The example shows the failing form first, then one way to give the checker the missing structure. ```nix # Bad: U expects a Level argument. u nat # Better: pass a level. u level0 ``` --- [← All diagnostic hints](/nix-effects/diag-hints) #### AnnType::not-a-type **Key:** `AnnType::not-a-type` **Category:** sort · **Severity:** error **Source:** [`src/diag/hints.nix:862`](https://github.com/kleisli-io/nix-effects/blob/main/src/diag/hints.nix#L862) the annotation position must be a type (live in some U(k)), not a term. Write a type expression such as `nat`, `bool`, `u 0`, or a user-defined datatype. ## Example `Hint::AnnType::not-a-type` means the `AnnType` position uses a term or value where the checker needs a type. The example shows the failing form first, then one way to give the checker the missing structure. ```nix # Bad: the annotation is a term. ann (natLit 0) (natLit 1) # Better: annotate with a type. ann (natLit 0) nat ``` --- [← All diagnostic hints](/nix-effects/diag-hints) #### JType::not-a-type **Key:** `JType::not-a-type` **Category:** sort · **Severity:** error **Source:** [`src/diag/hints.nix:870`](https://github.com/kleisli-io/nix-effects/blob/main/src/diag/hints.nix#L870) the type parameter of `J` must be a type (live in some U(k)), not a term. Pass a type expression like `nat`, `u 0`, or the type shared by J's two endpoints. ## Example `Hint::JType::not-a-type` means the `JType` position uses a term or value where the checker needs a type. The example shows the failing form first, then one way to give the checker the missing structure. ```nix # Bad: J's type argument is a term. J (natLit 0) motive refl x y p # Better: J's type argument is the type of both endpoints. J nat motive refl x y p ``` --- [← All diagnostic hints](/nix-effects/diag-hints) #### Motive.PiDom::not-a-type **Key:** `Motive.PiDom::not-a-type` **Category:** sort · **Severity:** error **Source:** [`src/diag/hints.nix:931`](https://github.com/kleisli-io/nix-effects/blob/main/src/diag/hints.nix#L931) the motive's domain must be a type (live in some U(k)). The motive receives the scrutinee's type as its domain and returns a type; supply a concrete type such as `nat`, `u 0`, or the datatype being eliminated. ## Example `Hint::Motive.PiDom::not-a-type` means the `PiDom` position under `Motive` uses a term or value where the checker needs a type. The example shows the failing form first, then one way to give the checker the missing structure. ```nix # Bad: the motive's Pi domain is a term. motive = pi (natLit 0) (_: u 0) # Better: the motive's Pi domain is a type. motive = pi nat (_: u 0) ``` --- [← All diagnostic hints](/nix-effects/diag-hints) #### Motive::not-a-type **Key:** `Motive::not-a-type` **Category:** sort · **Severity:** error **Source:** [`src/diag/hints.nix:894`](https://github.com/kleisli-io/nix-effects/blob/main/src/diag/hints.nix#L894) an eliminator's motive must return a type (live in some U(k)) ## Example `Hint::Motive::not-a-type` means the `Motive` position uses a term or value where the checker needs a type. The example shows the failing form first, then one way to give the checker the missing structure. ```nix # Bad: the motive returns a term. motive = x: natLit 0 # Better: the motive returns a type. motive = x: nat ``` --- [← All diagnostic hints](/nix-effects/diag-hints) #### PiCod::not-a-type **Key:** `PiCod::not-a-type` **Category:** sort · **Severity:** error **Source:** [`src/diag/hints.nix:860`](https://github.com/kleisli-io/nix-effects/blob/main/src/diag/hints.nix#L860) the codomain family of Π must return a type for each argument, not an ordinary value. Provide a function whose body inhabits some `U k`. ## Example `Hint::PiCod::not-a-type` means the `PiCod` position uses a term or value where the checker needs a type. The example shows the failing form first, then one way to give the checker the missing structure. ```nix # Bad: the codomain family returns a term. pi nat (_: natLit 0) # Better: the codomain family returns a type. pi nat (_: nat) ``` --- [← All diagnostic hints](/nix-effects/diag-hints) #### PiDom::not-a-type **Key:** `PiDom::not-a-type` **Category:** sort · **Severity:** error **Source:** [`src/diag/hints.nix:858`](https://github.com/kleisli-io/nix-effects/blob/main/src/diag/hints.nix#L858) the domain of Π must be a type (live in some U(k)), not a term or value. Supply a type expression like `nat`, `bool`, `u 0`, or a user-defined datatype. ## Example `Hint::PiDom::not-a-type` means the `PiDom` position uses a term or value where the checker needs a type. The example shows the failing form first, then one way to give the checker the missing structure. ```nix # Bad: a Pi domain must be a type, not a term. pi (natLit 0) (_: nat) # Better: put a type in the domain. pi nat (_: nat) ``` --- [← All diagnostic hints](/nix-effects/diag-hints) #### DArgBody::not-a-desc **Key:** `DArgBody::not-a-desc` **Category:** description · **Severity:** error **Source:** [`src/diag/hints.nix:846`](https://github.com/kleisli-io/nix-effects/blob/main/src/diag/hints.nix#L846) the body of `arg` must produce a description (Desc I), not an ordinary value. Build one with `descRet`, `descArg`, `descRec`, `descPi`, or `descPlus`. ## Example `Hint::DArgBody::not-a-desc` means the `DArgBody` position uses an ordinary term where the checker needs a Desc. The example shows the failing form first, then one way to give the checker the missing structure. ```nix # Bad: the body returns an ordinary term. descArg nat (_: natLit 0) # Better: the body returns a description. descArg nat (_: descRet tt) ``` --- [← All diagnostic hints](/nix-effects/diag-hints) #### DPiBody::not-a-desc **Key:** `DPiBody::not-a-desc` **Category:** description · **Severity:** error **Source:** [`src/diag/hints.nix:850`](https://github.com/kleisli-io/nix-effects/blob/main/src/diag/hints.nix#L850) the body of `pi` must produce a description for each input, not a plain term. Return a Desc I via `descRet`, `descArg`, `descRec`, `descPi`, or `descPlus`. ## Example `Hint::DPiBody::not-a-desc` means the `DPiBody` position uses an ordinary term where the checker needs a Desc. The example shows the failing form first, then one way to give the checker the missing structure. ```nix # Bad: the dependent body returns a term. descPi nat f (_: natLit 0) # Better: the dependent body returns a Desc. descPi nat f (_: descRet tt) ``` --- [← All diagnostic hints](/nix-effects/diag-hints) #### DPlusL::not-a-desc **Key:** `DPlusL::not-a-desc` **Category:** description · **Severity:** error **Source:** [`src/diag/hints.nix:876`](https://github.com/kleisli-io/nix-effects/blob/main/src/diag/hints.nix#L876) the left summand of `plus` must be a description (Desc I) ## Example `Hint::DPlusL::not-a-desc` means the `DPlusL` position uses an ordinary term where the checker needs a Desc. The example shows the failing form first, then one way to give the checker the missing structure. ```nix # Bad: the left summand is not a description. descPlus (natLit 0) rightDesc # Better: both summands are descriptions. descPlus (descRet tt) rightDesc ``` --- [← All diagnostic hints](/nix-effects/diag-hints) #### DPlusR::not-a-desc **Key:** `DPlusR::not-a-desc` **Category:** description · **Severity:** error **Source:** [`src/diag/hints.nix:878`](https://github.com/kleisli-io/nix-effects/blob/main/src/diag/hints.nix#L878) the right summand of `plus` must be a description at the same index type as the left summand ## Example `Hint::DPlusR::not-a-desc` means the `DPlusR` position uses an ordinary term where the checker needs a Desc. The example shows the failing form first, then one way to give the checker the missing structure. ```nix # Bad: the right summand is not a description. descPlus leftDesc (natLit 0) # Better: both summands are descriptions. descPlus leftDesc (descRet tt) ``` --- [← All diagnostic hints](/nix-effects/diag-hints) #### DRecTail::not-a-desc **Key:** `DRecTail::not-a-desc` **Category:** description · **Severity:** error **Source:** [`src/diag/hints.nix:856`](https://github.com/kleisli-io/nix-effects/blob/main/src/diag/hints.nix#L856) the tail position of `rec` must itself be a description, not an ordinary term. Continue the spine with `descRet`, `descArg`, `descRec`, `descPi`, or `descPlus`. ## Example `Hint::DRecTail::not-a-desc` means the `DRecTail` position uses an ordinary term where the checker needs a Desc. The example shows the failing form first, then one way to give the checker the missing structure. ```nix # Bad: the tail after a recursive position is a term. descRec i (natLit 0) # Better: continue with another description. descRec i (descRet i) ``` --- [← All diagnostic hints](/nix-effects/diag-hints) #### MuDesc::not-a-desc **Key:** `MuDesc::not-a-desc` **Category:** description · **Severity:** error **Source:** [`src/diag/hints.nix:864`](https://github.com/kleisli-io/nix-effects/blob/main/src/diag/hints.nix#L864) the description argument of μ must be a Desc I term, not an ordinary value. Construct it with `descRet`, `descArg`, `descRec`, `descPi`, or `descPlus`. ## Example `Hint::MuDesc::not-a-desc` means the `MuDesc` position uses an ordinary term where the checker needs a Desc. The example shows the failing form first, then one way to give the checker the missing structure. ```nix # Bad: mu expects a Desc, not a term. mu nat (natLit 0) # Better: construct the Desc explicitly. mu nat (descRet zero) ``` --- [← All diagnostic hints](/nix-effects/diag-hints) #### AppHead::not-a-function **Key:** `AppHead::not-a-function` **Category:** arity · **Severity:** error **Source:** [`src/diag/hints.nix:882`](https://github.com/kleisli-io/nix-effects/blob/main/src/diag/hints.nix#L882) the head of an application must have a function type (Pi) ## Example `Hint::AppHead::not-a-function` means the `AppHead` position uses a non-function where the checker needs a Pi-typed term. The example shows the failing form first, then one way to give the checker the missing structure. ```nix # Bad: the application head is not a function. app (natLit 0) (natLit 1) # Better: apply a lambda or another Pi-typed term. app (lam "x" nat (x: x)) (natLit 1) ``` --- [← All diagnostic hints](/nix-effects/diag-hints) #### DPiFn::not-a-function **Key:** `DPiFn::not-a-function` **Category:** arity · **Severity:** error **Source:** [`src/diag/hints.nix:872`](https://github.com/kleisli-io/nix-effects/blob/main/src/diag/hints.nix#L872) the index selector `f` of `pi` must be a function `S -> I` ## Example `Hint::DPiFn::not-a-function` means the `DPiFn` position uses a non-function where the checker needs a Pi-typed term. The example shows the failing form first, then one way to give the checker the missing structure. ```nix # Bad: the index selector is not a function. descPi nat expectedIndex (_: descRet expectedIndex) # Better: provide a selector from the domain into the index type. descPi nat (x: expectedIndex) (_: descRet expectedIndex) ``` --- [← All diagnostic hints](/nix-effects/diag-hints) #### Motive::not-a-function **Key:** `Motive::not-a-function` **Category:** arity · **Severity:** error **Source:** [`src/diag/hints.nix:868`](https://github.com/kleisli-io/nix-effects/blob/main/src/diag/hints.nix#L868) the motive must be a function from the scrutinee's type into a type, not a bare type or value. Supply a one-argument function whose body lives in some `U k`. ## Example `Hint::Motive::not-a-function` means the `Motive` position uses a non-function where the checker needs a Pi-typed term. The example shows the failing form first, then one way to give the checker the missing structure. ```nix # Bad: an eliminator motive is a bare type. natElim nat zero succCase n # Better: make the motive a function from the scrutinee. natElim (x: nat) zero succCase n ``` --- [← All diagnostic hints](/nix-effects/diag-hints) #### OpaqueType::not-a-function **Key:** `OpaqueType::not-a-function` **Category:** arity · **Severity:** error **Source:** [`src/diag/hints.nix:890`](https://github.com/kleisli-io/nix-effects/blob/main/src/diag/hints.nix#L890) the annotation on an opaque lambda must be a Pi type ## Example `Hint::OpaqueType::not-a-function` means the `OpaqueType` position uses a non-function where the checker needs a Pi-typed term. The example shows the failing form first, then one way to give the checker the missing structure. ```nix # Bad: an opaque lambda annotation must be a Pi type. opaqueLam nat body # Better: annotate it with a function type. opaqueLam (pi nat (_: nat)) body ``` --- [← All diagnostic hints](/nix-effects/diag-hints) #### DPiFn::type-mismatch **Key:** `DPiFn::type-mismatch` **Category:** indexing · **Severity:** error **Source:** [`src/diag/hints.nix:874`](https://github.com/kleisli-io/nix-effects/blob/main/src/diag/hints.nix#L874) the index selector's domain must match the declared sort `S` ## Example `Hint::DPiFn::type-mismatch` means the `DPiFn` position supplies a term whose type does not match the expected type. The example shows the failing form first, then one way to give the checker the missing structure. ```nix # Bad: the selector domain does not match the declared sort. descPi nat (s: stringIndex) (_: descRet stringIndex) # Better: the selector consumes the same sort descPi declares. descPi nat (n: natIndex n) (_: descRet (natIndex n)) ``` --- [← All diagnostic hints](/nix-effects/diag-hints) #### DRecIndex::type-mismatch **Key:** `DRecIndex::type-mismatch` **Category:** indexing · **Severity:** error **Source:** [`src/diag/hints.nix:854`](https://github.com/kleisli-io/nix-effects/blob/main/src/diag/hints.nix#L854) the index position of `rec` must match the Desc's declared index type. Pass a term of that index type, or adjust the enclosing `μ I ...` to match. ## Example `Hint::DRecIndex::type-mismatch` means the `DRecIndex` position supplies a term whose type does not match the expected type. The example shows the failing form first, then one way to give the checker the missing structure. ```nix # Bad: the recursive position points at the wrong index type. descRec wrongIndex tailDesc # Better: recurse at an index from the Desc index type. descRec expectedIndex tailDesc ``` --- [← All diagnostic hints](/nix-effects/diag-hints) #### DRetIndex::type-mismatch **Key:** `DRetIndex::type-mismatch` **Category:** indexing · **Severity:** error **Source:** [`src/diag/hints.nix:852`](https://github.com/kleisli-io/nix-effects/blob/main/src/diag/hints.nix#L852) the index position of `ret` must match the Desc's declared index type. Supply a term of that index type, or redefine the enclosing `μ I ...` over the index you actually have. ## Example `Hint::DRetIndex::type-mismatch` means the `DRetIndex` position supplies a term whose type does not match the expected type. The example shows the failing form first, then one way to give the checker the missing structure. ```nix # Bad: the return index is not in the Desc index type. descRet wrongIndex # Better: return at an index of the declared index type. descRet expectedIndex ``` --- [← All diagnostic hints](/nix-effects/diag-hints) #### MuIndex::type-mismatch **Key:** `MuIndex::type-mismatch` **Category:** indexing · **Severity:** error **Source:** [`src/diag/hints.nix:886`](https://github.com/kleisli-io/nix-effects/blob/main/src/diag/hints.nix#L886) the index passed to `con` must have the description's index type ## Example `Hint::MuIndex::type-mismatch` means the `MuIndex` position supplies a term whose type does not match the expected type. The example shows the failing form first, then one way to give the checker the missing structure. ```nix # Bad: the constructor index has the wrong type. con wrongIndex payload # Better: construct at an index from the declared index type. con expectedIndex payload ``` --- [← All diagnostic hints](/nix-effects/diag-hints) #### MuPayload::type-mismatch **Key:** `MuPayload::type-mismatch` **Category:** indexing · **Severity:** error **Source:** [`src/diag/hints.nix:888`](https://github.com/kleisli-io/nix-effects/blob/main/src/diag/hints.nix#L888) the payload of `con` must inhabit the description's interpretation at the given index ## Example `Hint::MuPayload::type-mismatch` means the `MuPayload` position supplies a term whose type does not match the expected type. The example shows the failing form first, then one way to give the checker the missing structure. ```nix # Bad: the payload does not match the constructor description. con expectedIndex wrongPayload # Better: build a payload matching the description at that index. con expectedIndex expectedPayload ``` --- [← All diagnostic hints](/nix-effects/diag-hints) #### Elem::inhabitation-failed **Key:** `Elem::inhabitation-failed` **Category:** inhabitation · **Severity:** error **Source:** [`src/diag/hints.nix:902`](https://github.com/kleisli-io/nix-effects/blob/main/src/diag/hints.nix#L902) the element does not inhabit the list's element type ## Example `Hint::Elem::inhabitation-failed` means the `Elem` position contains a value that does not inhabit the generated type. The example shows the failing form first, then one way to give the checker the missing structure. ```nix # Bad: a list element does not inhabit the element type. IntList.validate [ 1 "two" 3 ] # Better: every element inhabits the element type. IntList.validate [ 1 2 3 ] ``` --- [← All diagnostic hints](/nix-effects/diag-hints) #### Field::inhabitation-failed **Key:** `Field::inhabitation-failed` **Category:** inhabitation · **Severity:** error **Source:** [`src/diag/hints.nix:898`](https://github.com/kleisli-io/nix-effects/blob/main/src/diag/hints.nix#L898) the field's value does not inhabit the declared field type ## Example `Hint::Field::inhabitation-failed` means the `Field` position contains a value that does not inhabit the generated type. The example shows the failing form first, then one way to give the checker the missing structure. ```nix # Bad: the field value has the wrong shape. User.validate { name = 42; } # Better: use a value inhabiting the field type. User.validate { name = "alice"; } ``` --- [← All diagnostic hints](/nix-effects/diag-hints) #### SigmaFst::inhabitation-failed **Key:** `SigmaFst::inhabitation-failed` **Category:** inhabitation · **Severity:** error **Source:** [`src/diag/hints.nix:912`](https://github.com/kleisli-io/nix-effects/blob/main/src/diag/hints.nix#L912) the first component does not inhabit the declared `fst` type ## Example `Hint::SigmaFst::inhabitation-failed` means the `SigmaFst` position contains a value that does not inhabit the generated type. The example shows the failing form first, then one way to give the checker the missing structure. ```nix # Bad: the first component does not inhabit fst's type. pair "not-a-nat" proof # Better: fst inhabits its declared type. pair (natLit 3) proof ``` --- [← All diagnostic hints](/nix-effects/diag-hints) #### SigmaSnd::inhabitation-failed **Key:** `SigmaSnd::inhabitation-failed` **Category:** inhabitation · **Severity:** error **Source:** [`src/diag/hints.nix:914`](https://github.com/kleisli-io/nix-effects/blob/main/src/diag/hints.nix#L914) the second component does not inhabit the dependent `snd` type ## Example `Hint::SigmaSnd::inhabitation-failed` means the `SigmaSnd` position contains a value that does not inhabit the generated type. The example shows the failing form first, then one way to give the checker the missing structure. ```nix # Bad: the second component does not inhabit snd's dependent type. pair (natLit 3) "not-a-proof" # Better: snd inhabits the type determined by fst. pair (natLit 3) proofFor3 ``` --- [← All diagnostic hints](/nix-effects/diag-hints) #### Tag::inhabitation-failed **Key:** `Tag::inhabitation-failed` **Category:** inhabitation · **Severity:** error **Source:** [`src/diag/hints.nix:906`](https://github.com/kleisli-io/nix-effects/blob/main/src/diag/hints.nix#L906) the variant's payload does not inhabit the branch type ## Example `Hint::Tag::inhabitation-failed` means the `Tag` position contains a value that does not inhabit the generated type. The example shows the failing form first, then one way to give the checker the missing structure. ```nix # Bad: the variant payload does not inhabit the branch type. Result.validate { tag = "Ok"; value = false; } # Better: the payload matches the selected branch. Result.validate { tag = "Ok"; value = 200; } ``` --- [← All diagnostic hints](/nix-effects/diag-hints) #### Elem::refinement-failed **Key:** `Elem::refinement-failed` **Category:** refinement · **Severity:** error **Source:** [`src/diag/hints.nix:904`](https://github.com/kleisli-io/nix-effects/blob/main/src/diag/hints.nix#L904) the element violates the element type's refinement predicate ## Example `Hint::Elem::refinement-failed` means the `Elem` position contains a value that fails the refinement predicate. The example shows the failing form first, then one way to give the checker the missing structure. ```nix # Bad: one element fails the element refinement. PositiveList.validate [ 1 -2 3 ] # Better: every element satisfies the refinement. PositiveList.validate [ 1 2 3 ] ``` --- [← All diagnostic hints](/nix-effects/diag-hints) #### Field::refinement-failed **Key:** `Field::refinement-failed` **Category:** refinement · **Severity:** error **Source:** [`src/diag/hints.nix:900`](https://github.com/kleisli-io/nix-effects/blob/main/src/diag/hints.nix#L900) the field's value violates the field type's refinement predicate ## Example `Hint::Field::refinement-failed` means the `Field` position contains a value that fails the refinement predicate. The example shows the failing form first, then one way to give the checker the missing structure. ```nix # Bad: the field has the right base type but fails its predicate. User.validate { age = -1; } # Better: satisfy the refinement predicate. User.validate { age = 42; } ``` --- [← All diagnostic hints](/nix-effects/diag-hints) #### SigmaFst::refinement-failed **Key:** `SigmaFst::refinement-failed` **Category:** refinement · **Severity:** error **Source:** [`src/diag/hints.nix:916`](https://github.com/kleisli-io/nix-effects/blob/main/src/diag/hints.nix#L916) the first component violates the `fst` type's refinement predicate ## Example `Hint::SigmaFst::refinement-failed` means the `SigmaFst` position contains a value that fails the refinement predicate. The example shows the failing form first, then one way to give the checker the missing structure. ```nix # Bad: fst has the right base type but fails its refinement. pair (-1) proof # Better: fst satisfies the refinement. pair 1 proof ``` --- [← All diagnostic hints](/nix-effects/diag-hints) #### SigmaSnd::refinement-failed **Key:** `SigmaSnd::refinement-failed` **Category:** refinement · **Severity:** error **Source:** [`src/diag/hints.nix:918`](https://github.com/kleisli-io/nix-effects/blob/main/src/diag/hints.nix#L918) the second component violates the `snd` type's refinement predicate ## Example `Hint::SigmaSnd::refinement-failed` means the `SigmaSnd` position contains a value that fails the refinement predicate. The example shows the failing form first, then one way to give the checker the missing structure. ```nix # Bad: snd fails the refinement depending on fst. pair 3 (-1) # Better: snd satisfies the dependent refinement. pair 3 4 ``` --- [← All diagnostic hints](/nix-effects/diag-hints) #### Tag::refinement-failed **Key:** `Tag::refinement-failed` **Category:** refinement · **Severity:** error **Source:** [`src/diag/hints.nix:908`](https://github.com/kleisli-io/nix-effects/blob/main/src/diag/hints.nix#L908) the variant's payload violates the branch type's refinement predicate ## Example `Hint::Tag::refinement-failed` means the `Tag` position contains a value that fails the refinement predicate. The example shows the failing form first, then one way to give the checker the missing structure. ```nix # Bad: the variant payload fails the branch refinement. Port.validate { tag = "Tcp"; value = -1; } # Better: the payload satisfies the branch refinement. Port.validate { tag = "Tcp"; value = 443; } ``` --- [← All diagnostic hints](/nix-effects/diag-hints) #### Case::type-mismatch **Key:** `Case::type-mismatch` **Category:** elimination · **Severity:** error **Source:** [`src/diag/hints.nix:910`](https://github.com/kleisli-io/nix-effects/blob/main/src/diag/hints.nix#L910) this case-body's inferred type does not match the type the eliminator's motive requires ## Example `Hint::Case::type-mismatch` means the `Case` position supplies a term whose type does not match the expected type. The example shows the failing form first, then one way to give the checker the missing structure. ```nix # Bad: one case branch returns the wrong type. caseOf scrutinee { left = x: true_; right = y: natLit 0; } # Better: all branches return the motive's type. caseOf scrutinee { left = x: natLit 1; right = y: natLit 0; } ``` --- [← All diagnostic hints](/nix-effects/diag-hints) #### Scrut::type-mismatch **Key:** `Scrut::type-mismatch` **Category:** elimination · **Severity:** error **Source:** [`src/diag/hints.nix:866`](https://github.com/kleisli-io/nix-effects/blob/main/src/diag/hints.nix#L866) the scrutinee's type must match the eliminator's expected shape. Annotate the scrutinee via `ann`, or switch to the eliminator that matches its inferred type. ## Example `Hint::Scrut::type-mismatch` means the `Scrut` position supplies a term whose type does not match the expected type. The example shows the failing form first, then one way to give the checker the missing structure. ```nix # Bad: the eliminator is used on a scrutinee of the wrong type. natElim motive zero succCase true_ # Better: use a scrutinee with the eliminator's type. natElim motive zero succCase (natLit 3) ``` --- [← All diagnostic hints](/nix-effects/diag-hints) #### AnnTerm::type-mismatch **Key:** `AnnTerm::type-mismatch` **Category:** type-mismatch · **Severity:** error **Source:** [`src/diag/hints.nix:880`](https://github.com/kleisli-io/nix-effects/blob/main/src/diag/hints.nix#L880) the annotated term does not match its declared type ## Example `Hint::AnnTerm::type-mismatch` means the `AnnTerm` position supplies a term whose type does not match the expected type. The example shows the failing form first, then one way to give the checker the missing structure. ```nix # Bad: the annotated term does not inhabit the annotation. ann true_ nat # Better: the term matches the annotation. ann (natLit 1) nat ``` --- [← All diagnostic hints](/nix-effects/diag-hints) #### AppArg::type-mismatch **Key:** `AppArg::type-mismatch` **Category:** type-mismatch · **Severity:** error **Source:** [`src/diag/hints.nix:884`](https://github.com/kleisli-io/nix-effects/blob/main/src/diag/hints.nix#L884) the argument does not match the function's domain ## Example `Hint::AppArg::type-mismatch` means the `AppArg` position supplies a term whose type does not match the expected type. The example shows the failing form first, then one way to give the checker the missing structure. ```nix # Bad: the argument does not match the function domain. app (lam "x" nat (x: x)) true_ # Better: pass an argument from the domain. app (lam "x" nat (x: x)) (natLit 1) ``` --- [← All diagnostic hints](/nix-effects/diag-hints) #### JType::type-mismatch **Key:** `JType::type-mismatch` **Category:** type-mismatch · **Severity:** error **Source:** [`src/diag/hints.nix:896`](https://github.com/kleisli-io/nix-effects/blob/main/src/diag/hints.nix#L896) the type parameter of `J` must match the type of its two endpoints ## Example `Hint::JType::type-mismatch` means the `JType` position supplies a term whose type does not match the expected type. The example shows the failing form first, then one way to give the checker the missing structure. ```nix # Bad: J's type argument does not match the endpoints. J bool motive refl zero zero p # Better: use the endpoints' shared type. J nat motive refl zero zero p ``` --- [← All diagnostic hints](/nix-effects/diag-hints) #### OpaqueType::type-mismatch **Key:** `OpaqueType::type-mismatch` **Category:** type-mismatch · **Severity:** error **Source:** [`src/diag/hints.nix:892`](https://github.com/kleisli-io/nix-effects/blob/main/src/diag/hints.nix#L892) the opaque lambda's declared domain does not match the expected domain ## Example `Hint::OpaqueType::type-mismatch` means the `OpaqueType` position supplies a term whose type does not match the expected type. The example shows the failing form first, then one way to give the checker the missing structure. ```nix # Bad: the opaque lambda domain disagrees with the expected domain. opaqueLam (pi bool (_: nat)) body # Better: align the declared domain with the expected Pi domain. opaqueLam (pi nat (_: nat)) body ``` --- [← All diagnostic hints](/nix-effects/diag-hints) #### ::unhandled-boot-inl **Key:** `::unhandled-boot-inl` **Category:** shape · **Severity:** error **Source:** [`src/diag/hints.nix:945`](https://github.com/kleisli-io/nix-effects/blob/main/src/diag/hints.nix#L945) the left injection `inl x` has no inference rule — the sum type's right summand is not determined by the syntax of `x` alone. Annotate with `ann (inl x) (sum A B)` to fix the expected sum, or place it where checking already provides one. ## Example `Hint::unhandled-boot-inl` means `inl` is in inference mode without an expected Sum type. The example shows the failing form first, then one way to give the checker the missing structure. ```nix # Bad: inl does not determine the right summand by itself. infer emptyCtx (inl (natLit 1)) # Better: annotate the intended sum type. infer emptyCtx (ann (inl (natLit 1)) (sum nat bool)) ``` --- [← All diagnostic hints](/nix-effects/diag-hints) #### ::unhandled-boot-inr **Key:** `::unhandled-boot-inr` **Category:** shape · **Severity:** error **Source:** [`src/diag/hints.nix:947`](https://github.com/kleisli-io/nix-effects/blob/main/src/diag/hints.nix#L947) the right injection `inr y` has no inference rule — the sum type's left summand is not determined by the syntax of `y` alone. Annotate with `ann (inr y) (sum A B)` to fix the expected sum, or place it where checking already provides one. ## Example `Hint::unhandled-boot-inr` means `inr` is in inference mode without an expected Sum type. The example shows the failing form first, then one way to give the checker the missing structure. ```nix # Bad: inr does not determine the left summand by itself. infer emptyCtx (inr true_) # Better: annotate the intended sum type. infer emptyCtx (ann (inr true_) (sum nat bool)) ``` --- [← All diagnostic hints](/nix-effects/diag-hints) #### ::unhandled-boot-refl **Key:** `::unhandled-boot-refl` **Category:** shape · **Severity:** error **Source:** [`src/diag/hints.nix:949`](https://github.com/kleisli-io/nix-effects/blob/main/src/diag/hints.nix#L949) `refl` has no inference rule — the type of the equated endpoint is not determined by its syntax. Annotate with `ann refl (eq A x x)`, or use `refl` where the surrounding rule already supplies an expected equality type. ## Example `Hint::unhandled-boot-refl` means `refl` is in inference mode without an expected equality type. The example shows the failing form first, then one way to give the checker the missing structure. ```nix # Bad: refl does not determine the equality type by itself. infer emptyCtx refl # Better: annotate the equality being proven. infer emptyCtx (ann refl (eq nat zero zero)) ``` --- [← All diagnostic hints](/nix-effects/diag-hints) #### ::unhandled-lam **Key:** `::unhandled-lam` **Category:** shape · **Severity:** error **Source:** [`src/diag/hints.nix:939`](https://github.com/kleisli-io/nix-effects/blob/main/src/diag/hints.nix#L939) this lambda has no inference rule — its domain and codomain types are not determined by its syntax. Annotate with `ann (lam …) (pi A B)` to drive checking, or place it where the surrounding rule already supplies an expected Pi type. ## Example `Hint::unhandled-lam` means a lambda is in inference mode without an expected Pi type. The example shows the failing form first, then one way to give the checker the missing structure. ```nix # Bad: a lambda reaches inference without an expected Pi type. infer emptyCtx (lam "x" nat (x: x)) # Better: annotate it so checking mode knows the Pi type. infer emptyCtx (ann (lam "x" nat (x: x)) (pi nat (_: nat))) ``` --- [← All diagnostic hints](/nix-effects/diag-hints) #### ::unhandled-other **Key:** `::unhandled-other` **Category:** shape · **Severity:** error **Source:** [`src/diag/hints.nix:951`](https://github.com/kleisli-io/nix-effects/blob/main/src/diag/hints.nix#L951) this intro form has no inference rule — its type is not determined by its syntax alone. Either annotate the term with `ann ` to switch the kernel into checking mode, or move it inside a context where an expected type is already known. ## Example `Hint::unhandled-other` means an introduction form is in inference mode without enough expected type information. The example shows the failing form first, then one way to give the checker the missing structure. ```nix # Bad: an introduction form reaches inference without context. infer emptyCtx introForm # Better: annotate the expected type at the boundary. infer emptyCtx (ann introForm expectedType) ``` --- [← All diagnostic hints](/nix-effects/diag-hints) #### ::unhandled-pair **Key:** `::unhandled-pair` **Category:** shape · **Severity:** error **Source:** [`src/diag/hints.nix:941`](https://github.com/kleisli-io/nix-effects/blob/main/src/diag/hints.nix#L941) this pair has no inference rule — its component types are not determined by its syntax. Annotate with `ann (pair …) (sigma A B)` to drive checking, or place it where the surrounding rule already supplies an expected Sigma type. ## Example `Hint::unhandled-pair` means a pair is in inference mode without an expected Sigma type. The example shows the failing form first, then one way to give the checker the missing structure. ```nix # Bad: a pair reaches inference without an expected Sigma type. infer emptyCtx (pair (natLit 1) true_) # Better: annotate it with the Sigma type it should inhabit. infer emptyCtx (ann (pair (natLit 1) true_) (sigma "x" nat (_: bool))) ``` --- [← All diagnostic hints](/nix-effects/diag-hints) #### ::unhandled-tt **Key:** `::unhandled-tt` **Category:** shape · **Severity:** error **Source:** [`src/diag/hints.nix:943`](https://github.com/kleisli-io/nix-effects/blob/main/src/diag/hints.nix#L943) the unit value `tt` has no inference rule — there is exactly one type it can inhabit. The kernel still asks for an annotation to keep inference syntax-directed. Use `ann tt unit` at the use site, or check `tt` against an expected `unit` type. ## Example `Hint::unhandled-tt` means `tt` is in inference mode without an expected Unit type. The example shows the failing form first, then one way to give the checker the missing structure. ```nix # Bad: tt reaches inference without an expected Unit type. infer emptyCtx tt # Better: annotate tt or check it against unit. infer emptyCtx (ann tt unit) ``` --- [← All diagnostic hints](/nix-effects/diag-hints) ## Additional pages (including unstable internal surfaces) ### Applications Application examples are full modules rather than isolated snippets. They expose ordinary Nix APIs, include prose walkthroughs, and provide workload generators consumed by the benchmark suite. The source for this section lives under [examples/](https://github.com/kleisli-io/nix-effects/tree/main/examples). ## Walkthroughs - [Category Theory](/nix-effects/application-examples/categoryTheory): Kernel-checked arithmetic, algebra, functors, and Yoneda-style constructions. - [Expression Interpreter](/nix-effects/application-examples/interp): A small expression language interpreted with lookup, scoped environment, and failure effects. - [Build Simulator](/nix-effects/application-examples/buildSim): A dependency-graph evaluator with cache, configuration, logging, and failure effects. ### Concepts Concepts collects the theory that shapes the library. It is useful when you want the design vocabulary behind the implementation: algebraic effects, freer monads, queue-backed binds, normalization by evaluation, and levitated descriptions. ### Diagnostic Hints Diagnostic hints are stable labels for recurring checker and validation failures. When an error includes a `Hint::` token, the matching page explains what the key means and where to start debugging. Use this section when the raw error is too local and you need the larger shape of the failure: which kind of invariant was violated, what the checker was trying to establish, and what sort of change is usually relevant. Each hint page also points back to the source location that emits the hint. ### Effects and Validation Effect examples keep the computation fixed and change the handler. This makes policy choices explicit: collect validation errors, log each check, or stop at the first failure. The source for this section lives under [examples/](https://github.com/kleisli-io/nix-effects/tree/main/examples). ## Walkthroughs - [Handler-Swap Validation](/nix-effects/effect-examples/handlerSwapValidation): Run one validation computation with collecting, logging, and strict handlers. ### Examples The examples show nix-effects in complete, small programs. They move from proof construction, to effect-handler policy, to surface languages built over the HOAS kernel, to application-sized interpreters and graph evaluators. The same examples live in the source tree under `examples/` if you want to run or adapt them locally. ## Where to start Start with proof examples when you want to see values checked by the kernel. Use the effect example to compare validation policies without changing the computation. Use the surface-language examples when you want to build a small syntax layer over HOAS. Use the application examples when you want complete programs that also feed the benchmark suite. ```nix examples/ proof-basics.nix equality-proofs.nix verified-functions.nix handler-swap-validation.nix stlc/ category-theory/ interp/ build-sim/ ``` ## Walkthrough groups - [Proofs](/nix-effects/proof-examples) - Kernel-checked proof walkthroughs: computation, equality reasoning, and verified extraction. - [Effects and Validation](/nix-effects/effect-examples) - Effect-handler walkthroughs that show one computation running under multiple validation policies. - [Surface Languages](/nix-effects/surface-examples) - Small source-language walkthroughs built over HOAS, refinements, diagnostics, and generated data. - [Applications](/nix-effects/application-examples) - Complete example programs that double as benchmark workloads. ### Guide The guide is the shortest path from using nix-effects to understanding the pieces that make it useful. Start with setup and handler basics, then follow the later chapters when you need typed validation, description-backed datatypes, ornaments, proofs, or syntax helpers. ### Internals Internals explains how nix-effects is put together. These pages cover the trampoline, the relationship between the effect layer and the type-checking kernel, the trusted computing boundary, and the formal contract maintained by the kernel. ### Proofs Proof examples show how HOAS terms become kernel-checked evidence and usable Nix values. Start with computational equality, then move to reusable equality combinators and verified function extraction. The source for this section lives under [examples/](https://github.com/kleisli-io/nix-effects/tree/main/examples). ## Walkthroughs - [Proof Basics](/nix-effects/proof-examples/proofBasics): Computational proof examples checked by the HOAS kernel and exposed as ordinary Nix values. - [Equality Proofs](/nix-effects/proof-examples/equalityProofs): Derive reusable equality combinators from the J eliminator and check them through the kernel. - [Verified Functions](/nix-effects/proof-examples/verifiedFunctions): Kernel-checked HOAS programs extracted into plain Nix functions. ### Surface Languages Surface-language examples build a simply typed lambda calculus in layers. The core syntax introduces functions and application; later pages add products, sums, recursive lists, refinements, and diagnostics. The source for this section lives under [examples/](https://github.com/kleisli-io/nix-effects/tree/main/examples). ## Walkthroughs - [Surface STLC](/nix-effects/surface-examples/stlc): Surface-language walkthroughs for a simply typed lambda calculus.