Evidence Map: Refinement/Dependent Machinery for Proven-Total Operations
Status: survey taken 2026-10-30 by a read-only scout; line numbers drift.
It feeds the migration slices of design/no-faults.md.
Summary
The checker already has a working, tested evidence pipeline: refine/Dec/decide, builtin Fin/Vec/Sigma, using-context obligation discharge with D192 staleness, and a table-driven implication/weakening/narrowing lattice (forget/narrow). What it does not have: (1) any evidence narrowing from a plain if/boolean comparison into its branches (only case on Dec’s Yes/No constructors binds usable evidence); (2) linear arithmetic over two variables (only literal-bounded interval containment); (3) any Fin/refined-index alternative wired onto Bytes, which is the type bytes-get/bytes-set actually index. Literal-divisor NonZero obligations already fold at compile time for free. This asymmetry drives the recommended order at the end: cheapest wins first, then the two missing primitives that gate the two largest call-site families.
Files
design/refinement-types.md(whole file): refinement shape, predicate vocabulary, weakening/narrowing/forget/compound-predicate design,using-context constraint reading.design/dependent-core-kernel.md(section 6, 8):Nat/Intas abstract totals;decidescoped to examined values, never a general proof; branch evidence design (not fully implemented for plainif).design/proof-automation.md(section 1, 3, 4): trusted-kernel-checks-untrusted-search architecture; catalogue naming linear arithmetic (“omega-like”) and induction as still-needed, not yet built.design/no-faults.md,design/no-faults-inventory.md: the constraint and the current fault census (bytes-get/set 190+, div/rem families 170+, nat-sub 15+).compiler/CONFORMANCE.md:830-996: implementation status ledger for D248-D261 (refinements/decide/Fin/Vec, narrowing/forget, compound predicates,usingobligations).compiler/src/check_terms.rs:decide_shape(2456),decide_compound(2325-2460),decide_operand/baked measures (4523-4600),Forget/Narrowelaboration (897-1010),using-obligation discharge (690-820, 7609-7748), evidence invalidation write/use tracking (2306-2311, 2983-2987),ObligStatus/static_obligation/pred_obligation(10-14, 3492-3602), plainifelaboration with no evidence capture (869-885, 1438-1441).compiler/src/types.rs:838-1023:pred_conjuncts,pred_implies/pred_implies_canon, theInterval/interval_atom/atom_implied_bytable (literal-numeral bounds only, viabound_value).compiler/src/check_decls.rs:1453-1560: builtinVec/Fin/Decfamily registration (install_index_families).compiler/prelude/Data/NonZero.cinch(whole file, 27-50): the one implemented refinement example.compiler/prelude/Data/Nat.cinch:22-80,Data/Int.cinch:19-99,Data/Bytes.cinch:19-93: today’s partial signatures and class instances.compiler/prelude/Text/Lex.cinch:196-199, 315-316, 1029-1031;compiler/prelude/Text/Show.cinch:85-99;compiler/prelude/Data/Date.cinch:159-373,Data/Time.cinch:250-365: representative call sites.compiler/prelude/Effect/Http.cinch:213-217,compiler/prelude/Text/Toml.cinch:205-214: the only existing bounds-guard wrappers, written with a plainif, notdecide/Fin.compiler/tests/dependent.rs:62-93, 313-322;compiler/tests/lattice.rs:682-830: working examples ofdecide/Fin/Sigmaandusing-obligation discharge.examples/source/01-bounded-index.cinch,02-vector-filter.cinch: the canonicalFin/Vec/Sigmaidiom at prose-example scale.
Architecture
Surface terms elaborate through check_terms.rs into core terms; decide is a special application form recognized by decide_shape/decide_compound/decide_operand, producing a Dec p value (Yes/No, both builtin constructors registered alongside Vec/Fin in check_decls.rs). A case match on Yes h/No h binds h as an ordinary pattern variable carrying Prop-erased evidence; this is the only place evidence becomes usable in a branch. Refinements (refine (x :: T) with P) are a predicate-indexed family whose runtime representation is the container itself; pred_implies (interval-containment plus syntactic rows) decides implication for implicit weakening, narrow requires an explicit Dec for strengthening, forget drops named top-level conjuncts. using-context obligations on signatures and instances discharge at the call’s own arguments through the same Dec path, re-checked for staleness whenever check_terms.rs’s mutable-write tracking (note_ev_write_loc/record_ev_use) sees the underlying location written. None of this touches plain if: its elaboration (869-885, 1438-1441) checks the condition against Bool and infers/checks the two branches in isolation, recording nothing from the condition.
1. Refinement types (D248-D267)
Introduction. type NonZero = refine (n :: Int) with NonZero n (compiler/prelude/Data/NonZero.cinch:27). The transparent constructor (NonZero n w) takes the container value and a Dec-discharged witness; the total front door is:
non-zero :: Int -> Option NonZeronon-zero n = case decide (NonZero n) of Yes w -> Some (NonZero n w) No _ -> None(NonZero.cinch:29-31.)
Elimination. Rep-identity: a refined value is its container at runtime (zero-cost witness erasure, D31). unrefine is the explicit forgetful projection back to the base type (design doc, “Projection is free”).
Predicate vocabulary (closed, decidable). NotNull/NonZero/Ascii/Sorted(Ordered) plus Dec atoms (bounded comparisons, index equalities, Not), with &&/|| added by D256. Quantifiers are refused (design/refinement-types.md, “Compound predicates” section; check.rs kind registration for Prop).
Implication and weakening (D260/D261 read as D258). pred_implies/pred_implies_canon (compiler/src/types.rs:899-920) implements: syntactic identity, p && q implies p, and an interval-containment table for comparison/measure atoms (compiler/src/types.rs:950-1007). Weakening along this order is implicit and free; strengthening always needs a narrow cast carrying a Dec of the stronger predicate (check_terms.rs:962-1010).
using-obligation discharge at a call site (the Dec path, D192 staleness). A signature like merge2 :: using (Sorted xs, Sorted ys) => (xs :: List Nat) -> (ys :: List Nat) -> List Nat records its obligations as PropObligs (check.rs:2012-2016); at the call, check_terms.rs:690-820 discharges each named obligation against evidence already in scope through the Dec path. This is tested end-to-end in compiler/tests/lattice.rs:682-830 (using_obligations_discharge_through_the_dec_evidence_path, signature_obligations_constrain_callers_identically). D192 staleness re-checks: a mutable write to the region an evidence item’s statement reads invalidates it (note_ev_write_loc/note_opaque_write_from, check_terms.rs:709-712, 2306-2311; use-site re-check record_ev_use, check_terms.rs:2983-2987).
MWE (refine introduction/elimination/discharge, all working today):
non-zero 10 -- Yes w folds at compile time (see section 3); Option NonZero, Somecase decide (NonZero n) of Yes w -> NonZero n w -- introduction: transparent constructor + witness No _ -> ...let plain = unrefine nz -- elimination: free projection to Int2. Dec/decide
Signature and shape. decide accepts a comparison (<,<=,>,>=,==/=,/=/!=) or Eq x y (decide_shape, compiler/src/check_terms.rs:2456-2473), plus compound &&/||/Not forms recognized syntactically off the surface if-sugar (decide_compound, check_terms.rs:2325-2460). It returns Dec p = Yes (h :: p) | No (h :: Not p), both builtin constructors (check_decls.rs:1518-1537). The length/toNat builtins are specially recognized operands so decide (i < length b) lowers to the real BytesOp::Len comparison rather than a source function call (check_terms.rs:4523-4600).
MWE (compiler/tests/dependent.rs:65-69):
safeAt :: (n :: Nat) -> Nat -> Option (Fin n)safeAt n i = case decide (i < n) of Yes bound -> Some (Fin i bound) No _ -> NoneDoes evidence flow from a case on a comparison into the branch? Yes, exactly and only through pattern-matching on Dec’s Yes/No constructors: bound above is an ordinary bound variable carrying the erased proof i < n, usable to build Fin i bound.
Does evidence flow from a plain if condition? No. ast::Term::If elaboration (check_terms.rs:869-885 for inference, 1438-1441 for checking) checks cond :: Bool and infers/checks the two branches independently; nothing about cond’s truth is recorded as evidence in either branch. Concretely, Effect/Http.cinch:213-217’s one existing bounds-guard wrapper:
byte-at b i = case i >= bytes-len b of True -> None False -> Some (bytes-get b i)type-checks today only because bytes-get’s own signature carries no precondition at all (section 4); if bytes-get required i < bytes-len b evidence, this exact wrapper would stop compiling, because the False branch gets no narrowed fact from the guard.
Path-sensitive narrowing: none beyond the Dec constructor match. Every obligation is scoped “to the examined values” (design/dependent-core-kernel.md section 6); there is no flow-typing pass that derives i < n from a Bool-typed if/comparison outside decide.
3. NonZero, Fin, Vec n, Sigma, and the dependent kernel
NonZero: compiler/prelude/Data/NonZero.cinch:27 (refinement over Int only; no Nat alias exists in the prelude, confirmed by directory listing: Data/NonZero.cinch is the only NonZero/Fin/Vec/Sigma prelude file; Fin/Vec/Sigma have no prelude files at all, only compiler builtins and test/example usage).
Fin: builtin family, compiler/src/check_decls.rs:1497-1520:
data Fin (n :: Nat) where Fin :: (i :: Nat) -> (bound :: i < n) -> Fin nbound is erased; i is the runtime index.
Vec n a: builtin family, check_decls.rs:1456-1495:
data Vec (n :: Nat) (a :: Type) where Nil :: Vec 0 a Cons :: forall k. a -> Vec k a -> Vec (k + 1) aSigma: ast.rs:126-127 (Sigma(Name, Box<Type>, Box<Type>, Span)), types.rs:1378-1381, surface in parser.rs:2997-3001. MWE (compiler/tests/dependent.rs:75-80):
pack :: forall (n :: Nat). Vec n Int -> Sigma (m :: Nat). Vec m Intpack xs = (n, xs)unpack :: (Sigma (m :: Nat). Vec m Int) -> Natunpack p = case p of (m, v) -> mDoes 10 :: NonZero Nat (or Int) get accepted at compile time? There is no direct ascription syntax tested for this, but the underlying mechanism already statically folds it: pred_obligation (check_terms.rs:3590-3602) evaluates NonZero n over a numeral via eval_idx, returning ObligStatus::Checked/Disproved with zero runtime check. So decide (NonZero 10) folds to Yes at compile time already; construction today still goes through non-zero :: Int -> Option NonZero (a runtime-shaped Option, even though the underlying decision was compile-time) or the transparent constructor fed that statically-folded witness directly.
Does the kernel run decide at compile time on literals? Linear-arithmetic reasoning, or only syntactic atom-set inclusion? static_obligation (check_terms.rs:3492-3566) folds Cmp/Eq over numerals via eval_idx, and And/Or/Not combine the sub-results; pred_obligation folds the structural vocabulary the same way. Beyond that, pred_implies (types.rs:899-920) is not general linear arithmetic: it is syntactic atom-set inclusion plus one interval-containment table (Interval/interval_atom/atom_implied_by, types.rs:950-1007) that requires one side of each comparison to be a numeral (bound_value, types.rs:986-1000, folds only Num/+/-/* over numerals). A comparison between two variables (i < n where n is itself a variable, e.g. bytes-len b) produces no interval fact at all; interval_atom returns None when neither side is a numeral. This is the exact gap design/proof-automation.md section 4 item 2 names (“omega-like” linear arithmetic) as needed and not yet built.
4. Current signatures
| name | signature | site | instance |
|---|---|---|---|
bytes-get |
Bytes -> Nat -> Nat |
compiler/prelude/Data/Bytes.cinch:33 |
: |
bytes-set |
Bytes -> Nat -> Nat -> Unit |
Bytes.cinch:34 |
: |
bytes-len |
Bytes -> Nat |
Bytes.cinch:32 |
: |
nat-sub |
Nat -> Nat -> Nat |
compiler/prelude/Data/Nat.cinch:28 |
Sub_Nat :: Sub Nat, (-) x y = nat-sub x y (Nat.cinch:49-50) |
nat-div |
Nat -> Nat -> Nat |
Nat.cinch:30 |
Div_Nat (Nat.cinch:58-59) |
nat-rem |
Nat -> Nat -> Nat |
Nat.cinch:31 |
Rem_Nat (Nat.cinch:55-56) |
int-div |
Int -> Int -> Int |
compiler/prelude/Data/Int.cinch:24 |
Div_Int (Int.cinch:68-69) |
int-rem |
Int -> Int -> Int |
Int.cinch:25 |
Rem_Int (Int.cinch:65-66) |
- on Nat resolves through ordinary class dispatch (Sub typeclass) straight to the faulting foreign nat-sub; no static precondition exists on any of these today. int-sub (Int.cinch:22) is total already (Int is unbounded signed), so it is not in the fault inventory.
5. Three call sites each
(a) Byte indexing in a loop (lexer).
compiler/prelude/Text/Lex.cinch:196-199,bytes-eq: recursesi+1up tobytes-len x, readingbytes-get x i/bytes-get y i.Lex.cinch:315-316,render-pieces:nat-cmp i (bytes-len b)guard thenutf8-at b i.Lex.cinch:1029-1031,trim-attr-end:bytes-get b (j - 1)on a decrementing index.
Evidence needed: i < bytes-len x (and i < bytes-len y for site 1), re-derived every recursive step. Today’s machinery (decide+Fin) can express it (section 2/3), but each call site needs its own case decide (i < length x) of Yes bound -> Fin i bound; No _ -> ... inline, replacing the plain Nat index with Fin, at one decide + one case + one Fin construction per loop iteration per indexed buffer (two per call for site 1, which compares two buffers).
(b) Division by a literal (hex/decimal rendering).
compiler/prelude/Text/Show.cinch:92,nat-digits-into:bytes-set b (pos + k - 1) (48 + nat-rem n 10).Show.cinch:93, same function:nat-div n 10.Show.cinch:99,nat-digit-count:nat-div n 10.
Evidence needed: NonZero 10. This folds statically for free (pred_obligation, section 3): if nat-div/nat-rem took a NonZero Nat divisor, these three sites would need zero syntactic change at the literal 10; the checker’s existing numeral-folding already discharges it.
(c) Nat subtraction.
compiler/prelude/Data/Date.cinch:160:mp = if m > 2 then nat-sub m 3 else nat-add m 9.Date.cinch:373,digits-value-acc:nat-sub (char-code c) 48.compiler/prelude/Data/Time.cinch:251:nat-sub 9 (list-length cs), guarded two lines earlier byif list-length cs > 9 then None else ...(Time.cinch:250).
Evidence needed: b <= a for each a - b. Site 1 and 3 are guarded by a plain if/boolean comparison whose truth would supply exactly the needed fact, but per section 2 that guard gives no narrowed evidence to the subtraction; today’s machinery cannot express “the if’s condition proves the subtraction safe” without rewriting the guard itself as decide+case. Site 2 is worse: char-code c being an ASCII digit (hence >= 48) is established by a different function entirely (all-digits, called by the site’s own caller); there is no mechanism today to carry that cross-function invariant as scoped evidence to this call site short of a refined Char/digit parameter type.
6. What is missing
- Evidence narrowing from plain
if/boolean conditions.dependent-core-kernel.mdsection 8 designs this (“if/case introduce facts into scope on their branches”), but the implementation (check_terms.rs:869-885, 1438-1441) does not do it: only matchingDec’sYes/Noconstructors binds usable evidence. Nearly every existing guard in the prelude (byte-at, theDate.cinch/Time.cinchsubtraction guards) is written as a plainif/caseonBool, notdecide. This is the single highest-leverage gap, since fixing it would let an existing guard discharge its matching refinement obligation with no rewrite. Closed 2026-10-30: every match branch now binds what its test proves (if,caseon a comparison, on a numeric value with literal rows, oncomparing, on a list’s constructors, guards,&&/||/not), dischargingusingobligations and letting a plain value enter a refinement; seedesign/dependent-core-kernel.mdsection 8 (“As built”). - Linear arithmetic over variables.
i < nimplyingi + 1 <= nwhennis a variable (e.g.bytes-len b,list-length cs) is not decidable bypred_implies:interval_atom/bound_valuerequire a numeral on one side of the comparison (types.rs:986-1007).design/proof-automation.mdsection 4 item 2 names this (“omega-like”) as needed next; no code implements it. This gates almost every indexing loop (section 5a), since the bound advances by a non-literal amount each iteration. Closed 2026-10-30: the discharge reads every fact in scope together as difference constraints (x - y <= coverNat,Int, machine integers andChar) and refutes them with the obligation’s negation by negative-cycle detection, soi < n(withna local,bytes-len b,length xsor any pure measure) dischargesi + 1 <= n,1 <= n - i,NonZero (n - i),i <= nand0 < n, and bounds chain through other variables. Machine-integer arithmetic stays opaque (a user+may wrap). Each proof carries a checked negative-cycle certificate; seedesign/refinement-types.md(“linear facts over variables”) anddesign/proof-automation.mdsection 5. Item 4 (carrying an invariant across a recursion) remains: the procedure reads the facts in scope at each step, and a loop’s bound is in scope only where a branch tests it. - Literal discharge for
NonZero/comparison divisors already works (section 3,pred_obligation), so it is not missing for the literal-divisor pattern (section 5b); it is missing for a non-literal divisor (nat-div n dwithdan arbitrary expression), which needs a threadedNonZerowitness or falls back toOption-returning division. - Loop-invariant/structural-recursion automation.
decideis explicitly “scoped to the examined values… never a general proof” (dependent-core-kernel.mdsection 6); there is no mechanism to prove an index bound once and have it automatically carry forward through a structural recursion’s remaining steps (the catalogue’s “induction and case analysis” tactic layer,proof-automation.mdsection 4 item 1, is design-only; no tactic/reflection layer exists incheck_terms.rs). Each iteration must re-derive its ownDecby hand. - Compound-predicate refinement declarations (
refine (xs :: T) with p && q, D256/D257’s predicate-indexed-family generalization) are explicitly “implementation queued behind the language-tail slice” / “rides the P28 continuation” (compiler/CONFORMANCE.md:854-885), even thoughdecideon a source-level&&/||expression already folds viadecide_compound. The type-level compound-refinement syntax fromdesign/refinement-types.md’sSizedAsciiexample is not buildable today. - No
Fin/refined-index alternative onBytesat all.design/no-faults-inventory.md:100confirms this directly;Fin/Vecexist only as compiler builtins exercised by tests/examples, never wired toBytes, the one container dominating the fault call-site count.
Recommended migration order
NonZerodivisor for literal-divisornat-div/nat-rem/int-div/int-remsites first (section 5b). The checker already statically foldsNonZeroover numeral literals (pred_obligation), so changing these primitives’ signatures to take aNonZero Nat/NonZero Intdivisor costs zero proof burden at every literal-divisor call site (the hex/decimal rendering family acrossShow.cinch,Base64.cinch, etc.). This is the cheapest available win against the densest fault families (170+ sites), with no new language feature required; non-literal-divisor sites fall back toOption-returning division.- Add branch narrowing from plain
if/comparison conditions before touchingbytes-get/bytes-setornat-subin earnest. This is the smallest missing feature (already specified independent-core-kernel.mdsection 8, just unimplemented) and unlocks the largest number of call sites, because nearly every existing prelude guard (byte-at, theDate.cinch/Time.cinchsubtraction guards) is a plainif, not adecide. Without it, migratingnat-suborbytes-getforces every one of those guards to be hand-rewritten todecide+casesyntax, multiplying the touched call-site count for no added safety over what the guard already proves informally. Natsubtraction (a - brequiringb <= a): once (2) lands, the majority of existingif (b <= a-implying condition) then nat-sub a b else ...guards discharge for free. The remaining cross-function-invariant sites (thechar-code cASCII-digit pattern indigits-value-acc) need either a refinedChar/digit-range parameter type or the already-namedsaturating-subescape (design/no-faults.mdsection 7) rather than a proof.- Variable-bound linear arithmetic (
i < nimpliesi + 1 <= nfor variablen) before attemptingbytes-get/bytes-setat scale. This is required by essentially every indexing loop (section 5a) once the index advances past a singledecidecall;proof-automation.mdnames it as the next needed automation (section 4 item 2), but no interval/omega reasoning over two variables exists intypes.rstoday. Without it, migratingbytes-get/bytes-setcompiles (theFin/decidemachinery can express each step by hand) but at a per-iteration verbosity (decide+case+Finconstruction, per buffer, per loop step) that makes a 190+-site mass migration impractical. - A
Fin/Vec-style indexed alternative wired ontoByteslast. This closes the largest fault family (190+ sites,bytes-get/bytes-set) but should come after (2) and (4):bytes-get/bytes-setsit inside recursive loops in almost every call site found (Lex.cinch, and by extensionBase64.cinch,Digest.cinch,Json.cinch,Regex.cinch), and without loop-carried narrowing and variable-bound arithmetic the per-site proof burden is one hand-writtendecide+Finconstruction per call: expressible today (Finis already a compiler builtin,check_decls.rs:1497-1520), but at a verbosity that defeats mass migration until (2) and (4) exist.
The order above puts the smallest missing language feature (if-condition narrowing) ahead of the headline call-site counts (bytes-get/bytes-set, variable-divisor nat-div), because those counts are gated behind two still-missing primitives, while the literal-NonZero win is available immediately against the checker as it stands today.
