ADR-0066 · Surface type system — a bidirectional checker targeting the kernel HasCTy (tested-superset, grades-deferred)
-
Status: Accepted
-
Summary: The surface gains a TYPE LAYER — a bidirectional type-checker (
check ⇐/synth ⇒) over the surface AST that produces typing conforming to the kernel's gradedHasVTy/HasCTyrelation. The relation stays the single source of truth; the checker is an ALGORITHM in the TESTED superset (its soundness vsHasCTyis differential-tested, not proven in v1). v1 checks type structure + effect rows; grades default toω(grade-checking is a separable refinement). Unblocks #5 (effect-typed signatures), #24 (lawful algebra — type-directed operator resolution), #21 (scoped capability types). -
Resolves: the "type-checking the surface is a later issue" deferral stated in
Bang/Frontend/Surface.lean -
Depends-on: 0019, 0020, 0028, 0029
-
Status: Accepted (architecture + staging locked; the type-expression grammar + checker are staged below).
-
Date: 2026-06-30
-
Layer: S (surface) / T (tooling) — the kernel
HasCTyrelation is UNTOUCHED; this adds the surface ALGORITHM that targets it. Not a K-ADR (no kernel change). -
Resolves: the surface's own deferral — "It produces no typing derivations … type-checking the surface is a later issue" (
Bang/Frontend/Surface.leanheader). This is that later issue, designed. -
Builds on: ADR-0019/0020 (graded de-Bruijn context —
GradeVec/TyCtx, theHasVTy/HasCTythe checker targets). ADR-0028 (verified-core / tested-superset stratification — the checker's tier). ADR-0029 (iso-recursive ADTs —sum/prod/μthe checker must handle). -
Reference: Dunfield–Krishnaswami, Bidirectional Typing (ACM CSUR 2021 — the survey; check/synth modes, annotation placement). Levy, Call-by-Push-Value (the value/computation split the checker respects). Atkey, Syntax and Semantics of Quantitative Type Theory (the QTT grade discipline, deferred here).
Context — the surface runs, but does not type
The pipeline today is String → parse → Surf → lower → Comp → Source.eval. The surface deliberately
produces NO typing (it lowers to grade-free Comp and runs). The kernel HAS a full graded typing
relation — HasVTy : GradeVec → TyCtx → Val → VTy and HasCTy for Comp (ADR-0019/0020), proven
sound (progress/preservation/type_safety) — but it is a declarative relation, not an
algorithm, and nothing connects it to surface programs.
This blocks the typed future. The northstar #24 (lawful algebra) needs a + b to resolve its
operator instance by the type of a/b, and its laws are typed propositions — neither is possible
without types flowing through the surface. #5 (effect-typed signatures) and #21 (scoped capability
types) are the same gap. So the type layer is the critical path; this ADR designs it.
Decision
1. Algorithm = bidirectional typing (check ⇐ / synth ⇒)
Two mutually-recursive modes over the surface AST:
synth : TyEnv → Surf → Except TypeError (Ty × Eff)— infer a term's type (and effect row).check : TyEnv → Surf → Ty → Except TypeError Eff— check a term against an expected type.
CBPV maps cleanly: values synth/check at VTy; computations at CTy; force/thunk cross the
adjunction (mode switch). Eliminators are checking-driven (the scrutinee synthesizes); introductions
check against the expected type (so Left(e) checks e against the A of an expected A + B).
Annotations carry the burden where inference can't: function parameters (fun (x : Int) => …) and
top-level (e : T); everything else is inferred. (Rejected: full Hindley–Milner — doesn't fit graded
CBPV + effects + future user-operators; bidirectional is the modern standard for rich systems. Rejected:
fully-annotated — too verbose.)
2. HasVTy/HasCTy is the SPEC; the checker is an algorithm that TARGETS it
Single source of truth: the kernel relation is the truth. The checker is a NEW algorithm whose
soundness is the contract — if check/synth accepts, the program's lowering is HasCTy-derivable.
We do not write a second surface typing relation (that would be two copies of the typing rules — the
SSoT violation ADR-0028's stratification engineers against). The surface Ty syntax is a thin sugar over
VTy/CTy; checking maps to a HasCTy claim on the lowered Comp.
3. Tested superset, NOT verified core (v1)
Per ADR-0028, the checker sits in the tested tier. Its agreement with HasCTy is differential-tested,
not proven in v1:
- soundness corpus — well-typed surface programs; assert the lowering is
HasCTy-derivable (the existingExamples/#guarddiscipline, extended with aHasCTywitness check). - rejection corpus — ill-typed programs the checker must reject (
3 + Left(0), applying a non-function, …). - (later) property test — generated terms:
checkaccepts ⟹HasCTyholds (Plausible, #80 harness).
A verified type-checker (machine-proven sound and complete vs HasCTy) is the gold standard and a
post-v1 aspiration — a large proof. v1 buys the capability at tested-rung cost, exactly the stratification
move (proof budget on the kernel relation; the algorithm rides differential-testing).
4. Grades default to ω; grade-checking is a separable refinement
HasCTy is graded (QTT = {zero,one,omega}; ADR-0019). omega = unrestricted use, so a term
typed with every binder at ω is HasCTy-derivable whenever it is type-correct modulo the resource
discipline. v1 therefore checks type structure + effect rows and assigns grades = ω uniformly;
grade-checking (linearity — the 0/1 distinctions, "used exactly once", erasure) is its own later
increment. (Rejected: full QTT grade inference in v1 — research-level; premature when the immediate
unblock (#24, #5) needs types, not linearity. The ω-default is sound: it produces valid HasCTy
derivations; it just doesn't yet enforce the resource discipline.)
5. Effect rows ARE inferred — this layer IS #5
synth/check return the computation's effect row (the union of labels its performs touch,
discharged by enclosing handlers). That is precisely #5 (effect-typed signatures + type display):
a function's inferred CTy shows its effect row (Int -> {throws} Int). So #5 is not a separate feature
— it is the natural output of this checker.
6. Surface type syntax (new grammar, staged)
Users write types: Int, Unit, A -> B (function CTy), A + B, A * B, Thunk C (the U
former), Cap ℓ, and effect rows on computations ({throws} Int, the #5 surface). Annotations:
fun (x : T) => e, let x : T = e in …, top-level e : T. A type-expression parser (a small Pratt-ish
grammar — note the synergy with #30) produces a surface Ty that maps to VTy/CTy.
Downstream — what this unblocks (and how they hook in)
- #5 — is this (decision 5): effect rows in inferred types + type display.
- #24 lawful algebra — once
synthexposesa : Vec, operator resolution looks up theAddCommGroup Vecinstance by that type. The typeclass/instance mechanism + law checking is a SEPARATE feature on top; this layer's job is only to make the type available at the operator site. - #21 scoped capability types —
Cap ℓtypes surface here; scoping rides the checker.
Consequences & staging (each gated; de-risk early)
① this ADR — architecture locked
② type-expression grammar + parser (Ty syntax + annotations on fun/let/top-level)
③ SPIKE — bidirectional checker for the PURE fragment (int·unit·let·lam·app·pair·sum, no effects/
grades) over Surf, + 3-5 diff-tests (checker-accepts ⟹ HasCTy on the lowering). De-risks the
architecture before breadth. ← do this BEFORE committing to the full build.
④ effects — infer/check effect rows (handlers discharge labels) = #5 shipped
⑤ corpora — soundness + rejection corpus as #guards; wire the HasCTy-witness check into the gate
⑥ (later, separable) grades (linearity) · instance resolution (#24) · a VERIFIED checker
Surface/tooling only; the kernel HasCTy and the verification spine are untouched (the checker is a leaf
consumer, like Surface.lean). The first real step after this ADR is the spike (③) — a minimal
bidirectional checker for the pure fragment, diff-tested against HasCTy, to validate the architecture
cheaply before the full grammar + effects.