kernel
the trusted type checker for eidos. a nox program of 9 CIC typing rules (one per term constructor) encoded as Layer 1 patterns. this is the TCB — the only component whose correctness must be assumed. everything outside the kernel is untrusted shell.
interface
the kernel exposes two operations, each implemented as a nox formula noun:
infer : (Σ, Γ, term) → type | Error
check : (Σ, Γ, term, type) → Ok | Error
- Σ — global environment (inductive descriptors, accessed via the
lookpattern) - Γ — local context (list of CTX_VAR / CTX_LET entries; see terms)
- term — a term noun to type-check
- type — an expected type noun
infer derives the type of a term. check verifies that a term has a given type, using infer and definitional equality. check is the entry point for proof verification.
inputs and outputs are nox nouns following the encoding in terms.
nox program structure
the kernel formula is a depth-first case dispatch on the term tag (axis 2 of the term noun), implemented with nested branch patterns. each branch reads operands via axis, recurses via compose, and constructs result nouns via cons.
the kernel input noun shape:
input = cons(cons(Σ_ref, Γ), term)
-- axis(2, input) = cons(Σ_ref, Γ)
-- axis(3, input) = term
-- axis(4, input) = Σ_ref
-- axis(5, input) = Γ
-- axis(6, input) = term tag
-- axis(7, input) = term payload
Σ_ref is a pointer to the global environment, read via look (namespace 0). Γ is the local context noun.
typing rules
each rule below corresponds to one branch in the kernel dispatch. the rule name matches the term tag.
var
ctx_entry = ctx_lookup(Γ, i)
infer(Σ, Γ, VAR(i)) =
| ctx_entry is CTX_VAR(A) → shift(A, i+1)
| ctx_entry is CTX_LET(A,_) → shift(A, i+1)
shift(A, i+1) adjusts A's free indices for the i+1 binders crossed during lookup. the kernel implements ctx_lookup as a tail-recursive compose chain over the context list, decrementing i at each VAR/LET entry.
sort
infer(Σ, Γ, SORT(u)) =
require: u < p − 1 -- p = 2⁶⁴ − 2³² + 1; overflow would wrap to SORT(0)
SORT(u + 1)
implemented via lt(axis(7, term), atom(p−1)) before add(..., atom(1)). if the check fails, return Error(expected_sort).
pi
rule (Π-form):
u = infer(Σ, Γ, A)
require: u = SORT(s₁) -- else Error(expected_sort)
v = infer(Σ, Γ:VAR(A), B)
require: v = SORT(s₂) -- else Error(expected_sort)
infer(Σ, Γ, PI(A, B)) = SORT(prop_max(s₁, s₂))
the kernel extends the context by prepending CTX_VAR(A) before inferring B. prop_max is computed via a branch: if s₂ = 0, return SORT(0); else return SORT(max(s₁, s₂)).
lam
rule (Π-intro):
u = infer(Σ, Γ, A)
require: u = SORT(_) -- else Error(expected_sort): A must be a type
T = infer(Σ, Γ:VAR(A), t)
infer(Σ, Γ, LAM(A, t)) = PI(A, T)
result type is constructed via cons(atom(0x02), cons(A, T)).
app
rule (Π-elim):
F = infer(Σ, Γ, f)
require: whnf(Σ, Γ, F) = PI(A, B) -- else Error(expected_pi)
check(Σ, Γ, a, A)
infer(Σ, Γ, APP(f, a)) = subst(B, a)
the kernel calls whnf on F and checks the head tag is PI (0x02). if not, returns Error(expected_pi). result is subst(B, a) — the body type with the argument substituted.
let
rule (Let):
check(Σ, Γ, v, A) -- value must have declared type
T = infer(Σ, Γ:LET(A,v), b) -- body inferred under extended context
infer(Σ, Γ, LET(A, v, b)) = subst(T, v)
the context extension is CTX_LET(A, v) — the bound variable unfolds to v during whnf inside the body.
ind
rule (Ind-form):
desc = env_lookup(Σ, id) -- IND_DESC(arity, sort, param_tel, constructors)
require: len(params) = arity -- else Error(type_mismatch)
-- param_tel is a telescope PI(P₀, PI(P₁, ... PI(Pₙ₋₁, _) ...))
-- check each param against its type in the telescope (with prior params substituted):
for (pᵢ, Pᵢ) in zip(params, unroll_telescope(desc.param_tel, params)):
check(Σ, Γ, pᵢ, Pᵢ)
infer(Σ, Γ, IND(id, params)) = SORT(sort)
the sort of the inductive type comes directly from its descriptor. unroll_telescope(tel, params) steps through the PI-chain, substituting prior params into each successive type. Substitution is innermost-first (most recent param substituted first): P₀, subst(P₁, p₀), subst(subst(P₂, p₁), p₀), etc. The innermost-first order is required because subst replaces VAR(0), which after peeling i binders refers to the i-th (most recent) parameter, not the first.
ctor
rule (Ind-intro):
desc = env_lookup(Σ, id)
ctor_type = instantiate(desc.constructors[k], params)
infer(Σ, Γ, CTOR(id, k, args)) = apply_args(ctor_type, args)
instantiate substitutes the inductive's parameters into the constructor type telescope. apply_args applies the resulting function type to each argument, returning the final type after all arguments are consumed.
elim
rule (Ind-elim):
desc = env_lookup(Σ, id) -- else Error(unknown_inductive)
T_target = infer(Σ, Γ, target)
require: whnf(Σ, Γ, T_target) = IND(id, params) -- else Error(expected_ind)
-- motive must map the target type to some sort
T_motive = infer(Σ, Γ, motive)
require: whnf(Σ, Γ, T_motive) = PI(IND(id, params'), SORT(s)) -- else Error(type_mismatch)
require: def_eq(Σ, Γ, params, params') -- motive params must match target params
-- large elimination gate: Prop inductives may only eliminate into Type if proof-irrelevant
-- "field types" means constructor argument types AFTER parameter instantiation
if desc.sort = 0 and s > 0:
require: len(desc.constructors) ≤ 1
-- for each constructor k, check all field types (post-instantiate) are in Prop:
for k in 0..len(desc.constructors):
tel = instantiate(desc.constructors[k], params)
for each PI(A, _) in tel: require: infer(Σ, Γ, A) = SORT(0)
-- admitted: False (0 ctors), True (1 ctor, 0 fields), And (1 ctor, 2 Prop fields),
-- Eq (1 ctor, 0 post-param fields), Accessible (1 ctor, Prop field)
-- rejected: Or (2 ctors), Nat.le (2 ctors)
-- each case[k] must have the type case_type computes
for k in 0..len(desc.constructors):
check(Σ, Γ, cases[k], case_type(desc, k, motive, params))
infer(Σ, Γ, ELIM(id, motive, cases, target)) = APP(motive, target)
case_type is defined in § helper functions below. the large elimination gate prevents extracting data from proofs: inductives in Prop (desc.sort = 0) may only eliminate into Type (s > 0) when they have at most one constructor and all fields are Props — the proof-irrelevance condition. False, True, And, and Eq satisfy this; Or and Nat.le do not.
helper functions
the following functions are called from the typing rules. each is a nox formula composition in the kernel.
instantiate
substitutes the inductive's actual parameters into a constructor telescope. constructor types are stored with the inductive's a parameters as the outermost free variables (VAR(a-1) = first param, VAR(0) = last param). params is ordered outermost-first.
IND_DESC now includes a param_tel field:
IND_DESC(arity, sort, param_tel, constructors)
param_tel is a PI-telescope of length arity encoding the types of the inductive's parameters. this is used by the ind typing rule to check parameter arguments. constructor telescopes in constructors have the same arity outermost free variables (the parameters) before any constructor-specific fields.
instantiate(ctor_tel, params):
t = ctor_tel
for p in reverse(params): -- p_{a-1} first (replaces VAR(0)), then p_{a-2}, ...
t = subst(t, p)
return t
-- result: constructor type with no parameter free variables;
-- remaining free variables are the constructor's own field types
apply_args
steps a constructor type telescope through its arguments, checking each one:
apply_args(Σ, Γ, ctor_type, args):
-- Σ and Γ are threaded from the enclosing infer/check invocation
t = ctor_type
for a in args:
require whnf(Σ, Γ, t) = PI(A, B) -- else Error(type_mismatch)
check(Σ, Γ, a, A)
t = subst(B, a)
return t -- the final return type after all args are substituted in
recargs
for ι-reduction: for each constructor argument whose type is the same inductive, produce the recursive eliminator call. this is the recargs referenced in the whnf ι step in terms.
recargs(Σ, Γ, id, motive, cases, ctor_tel_inst, args):
-- Σ, Γ threaded from enclosing invocation
result = []
t = ctor_tel_inst
for aᵢ in args:
require whnf(Σ, Γ, t) = PI(Aᵢ, rest)
if whnf(Σ, Γ, Aᵢ) = IND(id, _):
result = result ++ [ELIM(id, motive, cases, aᵢ)]
t = subst(rest, aᵢ)
return result
case_type
computes the expected type for the k-th case function. for each constructor field, emits a PI-binder; for each field of recursive inductive type IND(id, _), also emits an induction hypothesis binder APP(motive, field).
case_type(desc, k, motive, params):
tel = instantiate(desc.constructors[k], params)
-- tel = Π(a₀:A₀)(a₁:A₁)...(aₙ₋₁:Aₙ₋₁). IND(id, params) [named notation]
-- result type structure (named notation):
-- Π(a₀:A₀), [Π(_ : motive a₀),]
-- Π(a₁:A₁), [Π(_ : motive a₁),]
-- ...,
-- APP(motive, CTOR(id, k, [a₀, ..., aₙ₋₁]))
-- where [Π(_ : motive aᵢ)] is present iff whnf(Aᵢ) = IND(id, _)
return build(tel, motive, id, k, rev_fields=[])
build(tel, motive, id, k, rev_fields):
match tel with
| PI(Aᵢ, rest), whnf(Aᵢ) = IND(id, _) →
-- recursive field: emit field binder then IH binder
-- field aᵢ = VAR(0); IH type = APP(motive_shifted_1, VAR(0))
ih = APP(shift(motive, 1), VAR(0))
inner = build(shift(rest, 1), shift(motive, 2), id, k,
[VAR(1)] ++ shift_all(rev_fields, 2))
return PI(Aᵢ, PI(ih, inner))
| PI(Aᵢ, rest) →
-- non-recursive field: emit field binder only
inner = build(rest, shift(motive, 1), id, k,
[VAR(0)] ++ shift_all(rev_fields, 1))
return PI(Aᵢ, inner)
| _ →
-- all fields collected; rev_fields = [aₙ₋₁, ..., a₀] (innermost first)
return APP(motive, CTOR(id, k, reverse(rev_fields)))
shift_all(list, n) applies shift(_, n) to each element. the shift(motive, d) calls keep motive valid under d new PI-binders introduced above it.
check
check reduces to infer plus definitional equality:
check(Σ, Γ, term, expected) =
actual = infer(Σ, Γ, term)
if def_eq(Σ, Γ, actual, expected) then Ok
else Error(type_mismatch, actual, expected)
definitional equality (def_eq) is defined in terms. the kernel calls whnf on both sides before structural comparison.
whnf in the kernel
the kernel's whnf implementation uses the look pattern for δ-unfolding of CTX_LET entries, and recursive compose chains for β and ι steps. whnf results are memoized by noun id using the call pattern (tag 0x01 = optimization hint): the prover caches (input_id → whnf_id) so repeated whnf calls on the same noun are O(1).
whnf terminates because:
- β reduces the λ count in the head
- ι reduces the structural size of the target (by the inductive's well-founded order)
- δ unfolds each definition at most once per whnf call (guarded by memo)
error encoding
errors are atom nouns with a high tag bit:
| error | atom value |
|---|---|
| unbound_variable | 0xE000 |
| expected_sort | 0xE001 |
| expected_pi | 0xE002 |
| expected_ind | 0xE003 |
| type_mismatch | 0xE004 |
| unknown_inductive | 0xE005 |
| constructor_out_of_range | 0xE006 |
| budget_exhausted | 0xE007 |
the kernel propagates errors upward via branch: any Error atom from a recursive call short-circuits and returns the error from the enclosing rule.
nox pattern usage
each CIC rule maps to a composition of nox Layer 1 patterns:
| rule | primary patterns used |
|---|---|
| var | axis (navigate context list), compose (chain lookup steps) |
| sort | axis (read level), add (increment) |
| pi | axis, compose (two infer calls), branch (prop_max), cons (build result) |
| lam | axis, compose (infer A, extend ctx, infer t), cons |
| app | axis, compose (infer f, whnf, branch on tag, check a), compose (subst) |
| let | axis, compose (check v, extend ctx, infer b), compose (subst) |
| ind | look (env_lookup), axis, compose |
| ctor | look, axis, compose (instantiate, apply_args) |
| elim | look, axis, compose (case_type loop), cons |
| whnf | compose + branch on tag; call (memo) |
| def_eq | compose (two whnf calls), eq (noun equality), branch |
| subst | compose + branch per constructor; recursive |
the kernel is a single formula noun of approximately 2,500 nox IR nodes. it uses no call for search (only for memoization) and no look for BBG state — only the Σ lookup namespace.
budget
the kernel runs under nox budget. each reduce() call costs 1 unit; multi-row patterns (inv, lt, bitwise, hash) cost more. for a well-typed term of size N (noun node count), the kernel budget is bounded by O(N²) in the worst case (deeply nested definitional equality with large terms). practical proofs run well within O(N log N).
budget is tracked by nox's built-in metering. if the budget is exhausted, the kernel returns Error(budget_exhausted). the prover may increase the budget and retry — exhaustion is not a soundness failure.