Dependent core kernel design
Design note for the Cinch toolchain, roadmap slice P7 in
compiler/CONFORMANCE.md. Companion to the core language specification,
which states obligations; this note fixes the kernel shape and its mapping
onto the existing pipeline.
Status: design draft for implementation. Items marked open are listed in section 9.
1 Shape of the system
A small dependently typed core with bidirectional elaboration. The surface elaborates into the core; the core is what the erasure pass and the code generator see. The existing Hindley-Milner layer does not go away: it is the elaborator’s inference engine for the simply typed fragment and for effect rows and multiplicities. Dependent checking is layered as an explicit judgment on top, so every slice keeps working while the core grows.
surface -> elaboration -> core terms (typed) -> erasure -> core IR -> LLVM ^ | | conversion +-- evidence checked here (proofs, totality) +-- unification (metas for inference)2 Kinds and universes
Typeis the universe of computational types;Propis the universe of erased propositions (section 7).Type : Typeis NOT admitted; a universe hierarchyType 0 : Type 1 : ...is implicit (predicative, cumulative in a limited form:Type iembeds intoType jfori <= j).- Kinds, types and values share term syntax (draft 0.2); the checker assigns kinds. Universe levels are inferred with the usual max rule for Pi.
NatandIntare inductive types inType 0with mathematical semantics (section 6); machine types are primitives inType 0whose runtime representation is fixed by the primitive types specification.
3 Core terms and judgments
Terms: variables, Pi (x : A) -> B, Sigma (x : A). B, lambda, application,
let, constructors, dependent pattern matching, Type i, Prop, proof
terms (erased), and the existing effect-row and multiplicity annotations on
arrows.
Judgments (bidirectional):
Gamma |- t => A inferGamma |- t <= A checkGamma |- A type well-formed typesA == B conversion (definitional equality)Gamma |- t : A ok usage/multiplicity check (P5)Inference forms are the usual ones (variables, application of an inferable head, annotations). Everything else checks against an expected type; the expected type of a pattern match is propagated into branches.
4 Conversion
Definitional equality is beta (for dependent application), delta (transparent
definitions only), and iota (constructor case reduction on constructor
applications), plus proof irrelevance for Prop terms (open whether this
is definitional or propositional) and eta for records/units where useful
(open). Conversion is a terminating procedure under the termination
discipline of section 8: it never unfolds recursive definitions without a
structural decrease argument; unchecked unfolding is an error, not a loop.
Machine arithmetic is NOT part of conversion: 2 + 2 == 4 is proved, not
reduced (the primitive types specification makes wrapping/checked families
explicit; the mathematical Nat/Int operations reduce only on numerals
where a total decision procedure applies, see decide in section 8).
5 Inductive families
Declaration:
data Vec (n :: Nat) (a :: Type) where Nil :: Vec 0 a Cons :: forall k. a -> Vec k a -> Vec (k + 1) a- Constructors declare their indices explicitly after elaboration (the surface suppresses constructor-local implicit binders; the elaborator re-inserts them).
- Positivity is checked syntactically (strict positivity for the bootstrap; positivity-preserving nesting is open).
- The runtime representation of an inductive family is the ADT block already
in use: indices that are computational (like
n) are RETAINED as fields when execution needs them (core specification section 3: erasure must not drop witnesses the runtime needs); erased indices are dropped in the erasure pass.
6 Mathematical numbers
Nat and Int are inductive (or primitive-backed) unbounded numbers whose
semantics are exact. Implementation: the Rust runtime’s bignum Nat/Int
(P4) provides the representation; the core treats them as abstract types
with total operations (+, -, * on Nat with truncated subtraction
matching the inductive definition; Int exact). Literals elaborate to
numeral constructors; decide (i < n) uses the runtime comparison and
returns evidence scoped to the examined values (core specification
section 4), never a general proof.
7 Propositions and erasure
Propis a universe of erased propositions.Eq x y,n <= metc. live inProp;refl,congrand userproofdeclarations construct evidence inProp.- Erasure rules:
- Terms in
Prop,erasedbinders anderasedrecord fields are erased before code generation; they occupy no runtime representation. - Erasure must not remove computation with observable effects or divergence; an erased proof of a total fact can vanish entirely.
- No elimination of
PropintoTypeexcept through the allowed singleton/decidable paths; computational decisions from erased evidence are rejected (proof impossible = impossiblestyle cycles are rejected by termination, andFin 7constructions by index checking). - Erased evidence may appear in types (indices, refinements) but never in the runtime representation of a value that execution inspects.
- Terms in
- The erasure pass runs on core terms and is checked: it may only remove terms the typing judgment marks erased.
8 Totality, evidence, refinements
- Total functions: structural recursion (a recursive call must decrease a
named argument structurally) or a checked well-founded measure. The
checker returns a checkable result; automation may suggest, never assume.
Divergence is explicit (
Divergein the effect row or aspec/partial form); a looping function cannot inhabit aProp. - Branch evidence:
if/caseintroduce facts into scope on their branches (the scrutinee equals the pattern). Refinements are the surface for these facts; the core records them asEq-style hypotheses, and flow-sensitive invalidation drops facts when mutable state changes (core specification section 5). - As built (branch facts from matches): every branch of a match binds
what its test proves about the column’s value, by one rule. The
decision tree’s branches are exact, so the fact holds on every path into
the branch: a default branch knows the value is none of the literals,
and an earlier guarded row never removes values from a later branch (a
failing guard falls through inside its own branch). The readings:
- a
Boolscrutinee the checker reads as a predicate: itsTruebranch gets the predicate, itsFalsebranch the refutation (a wildcard row afterTruesits in theFalsebranch). Readable predicates are a comparison (<,<=,>,>=,==,/=) whose operator is the prelude’sOrd/Eqmethod and whose operands areNat,Int, machine integers orChar(never floats: under IEEE a NaN makesnot (x < y)differ fromx >= y); a structural predicate written as the condition (if NonZero d then); and&&,||, and the prelude’snotover those; - a numeric scrutinee: a literal branch states
x == k, the default branchx /= kfor every literal; comparing k x y(the prelude’s three-way comparison;Orditself has only the four boolean methods) at a numeric key:LT,EQ,GTstatek x < k y,k x == k y,k x > k y;- a list-shaped scrutinee (the
NotNullvocabulary): a non-empty constructor’s branch statesNotNull x, the empty onenot (NotNull x). Other constructor types have no vocabulary predicate and state nothing (a predicate per constructor,IsJust-style, would be needed). The value readings state their fact about the scrutinee and about every name a row binds to the column (e -> nat-div n eafter a0row, or a function equation’s parameter after a literal equation). A scrutinee is read only when a fact may name it: numerals, locals, borrow reads, arithmetic (not at machine integers, whose arithmetic wraps while index arithmetic is mathematical),bytes-len, and pure global functions taking no borrow, pointer or byte buffer (a buffer’s contents change underbytes-setwith no tracked footprint).lengthandtoNatare excluded: the index reading takes them as the baked measures whatever they resolve to.if c then a else biscase c of True -> a; False -> b: the sameBoolreading and branch rule, and acasewhoseBoolscrutinee no pattern names branches on the test itself, so both compile to the same compare-and-branch. A guard is aBoolcolumn over its row (its body is theTruebranch, the fallthrough to later rows theFalsebranch, reaching only rows whose names denote the same values).p && qchecksqin theTruebranch ofpand holds both (or justpwhenqhas no reading);p || qchecksqin theFalsebranch ofpand refutes both. ABoolthe checker cannot read carries no fact. - Mechanism: the facts are native to the checker, not a desugaring to
case decide c of. Each fact conjunct is a hiddenPropbinder in the branch scope (its name is no identifier, so source can neither name nor forge it), so the existing discharge (discharge_oblig) and the D192 staleness tracker (ev_derivs/ev_uses, the same write log) read it exactly as aYes hbinder. - Reading never changes typing: a condition elaborates exactly as a
plain one (a comparison is the ordinary application of the prelude’s
Ord/Eqmethod, with the same test and cost), and the fact is read off that elaboration only when the method’s class argument came out a total order and both operands are terms a fact may name. A condition whose reading would need other typing simply has no fact. - Staleness: a fact is bound when its branch is elaborated, so writes in
sibling branches (not on its path) do not count, and it also carries
the window between its own test and the end of its condition, so a
write later in the condition (
copyOut m >= 5 || poke m) does. - Cost: the plain
if’s, one comparison and a branch. NoDecvalue is built, and a fact-bearingif,case c of True/Falseand literalcaseallocate exactly what a plainifdoes. (case decide c of Yes/Nodoes not allocate either:Yes/Noare static nullary words, but it materializes the word and dispatches on its header; the branch-fact path skips both.) - Use sites: a
usingobligation at a call, and a plain value checked against a refinement whose container it is (nat-div n dwithd :: NatagainstNonZero Nat): the predicate instantiated at the value’s index reading is decided statically or entailed by evidence in scope; the coercion is the same word. - Entailment is
pred_entails:pred_implies’s rows over the caller’s own names, never up to renaming (a < bsays nothing aboutb < a). One item, or the items over the obligation’s names together, must entail the obligation; an item whose names are rebound after it (an innerlet x) is out of scope. What discharges is what the table proves: syntactic identity, conjunct projection, interval containment and disjointness over literal bounds,x /= 0and a zero-excluding range forNonZero x, and a range missingkforx /= k. When the table fails, every fact in scope together (over any names) is decided by difference logic (linear_entails,types.rs):i < nentailsi + 1 <= n, andi <= j,j < nentaili < n(design/refinement-types.md, “linear facts over variables”). Its proof is a negative-cycle certificate checked before the discharge accepts it; the facts the certificate cites are the ones the staleness check reads.
- a
decide Pexists only with a supplied decision procedure; its result isYes evidence | No refutationscoped to the examined values.
9 Open decisions
- Proof irrelevance definitional or propositional; eta for which types.
- Strict positivity vs positivity-preserving nesting.
- Whether
Nat/Intare inductive types with runtime specialization or primitive abstract types with total semantics (implementation may pick the latter if the former blocks performance work; the surface cannot tell). - Universe cumulativity: full or only at
Type 0boundaries. - Sized/guarded discipline choice for coinduction (core specification section 9) and its interaction with the totality checker.
10 Mapping onto the current pipeline
types.rsgainsTy::Pi,Ty::Sigma,Ty::Prop,Ty::Univ(level), indexedTy::Adtwith index arguments; unification gets a conversion fallback (try definitional equality when syntactic unification fails).check_terms.rsgrows a dependent checking mode; the simply typed path remains the fast path when no dependent forms appear.- The core IR (
core.rs) stays the erasure output; erased slots are simply absent there, so codegen needs no changes for erasure beyond what P2/P4 already provide for numbers.
