Check
On this page
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 thattmhas typetyand returns an elaborated term.infer : Ctx → Tm → Computation { term; type; }— synthesis mode. Infers the type oftmand returns the elaborated term with its type.checkType : Ctx → Tm → Computation Tm— verify a term is a type.checkTypeLevel : Ctx → Tm → Computation { term; level; }— likecheckTypebut 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 ontypeErroreffects.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
inferand usesconvto compare. - Non-cumulative universes: Tarski-style, exact-level —
U(i)does not subsume intoU(j)fori < j; conv compares levels byconvLevelwith 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 bBrackets 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 bEquivalent 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 bEquivalent 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 TmDispatches 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 TmcheckTypeLevel
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 : Ctxextend
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 -> Ctxinfer
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 -> ValrunCheck
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.