|
| 1 | +-- SPDX-License-Identifier: PMPL-1.0-or-later |
| 2 | +-- Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk> |
| 3 | +-- |
| 4 | +||| TypeLL Type Inference Soundness |
| 5 | +||| |
| 6 | +||| Proves that the bidirectional type inference algorithm implemented in |
| 7 | +||| infer.rs is sound: if inference succeeds with type T, then the typing |
| 8 | +||| judgement ctx |- e : T holds. |
| 9 | +||| |
| 10 | +||| The proof covers: |
| 11 | +||| 1. Unification soundness: if unify(T1, T2) succeeds with substitution S, |
| 12 | +||| then S(T1) = S(T2). |
| 13 | +||| 2. Inference soundness: if infer(ctx, e) = T, then ctx |- e : T. |
| 14 | +||| 3. Occurs check correctness: if a variable occurs in a type, unification |
| 15 | +||| correctly rejects the infinite type. |
| 16 | +||| |
| 17 | +||| These correspond to: |
| 18 | +||| - unify.rs: Unifier::unify and Substitution::apply |
| 19 | +||| - infer.rs: InferCtx::synthesize_var and InferCtx::check_against |
| 20 | +||| - infer.rs: InferCtx::generalize and InferCtx::instantiate |
| 21 | +||| |
| 22 | +||| @see unify.rs — Robinson unification with occurs check |
| 23 | +||| @see infer.rs — Bidirectional type inference |
| 24 | + |
| 25 | +module TYPELL.ABI.InferenceSoundness |
| 26 | + |
| 27 | +import TYPELL.ABI.Soundness |
| 28 | +import Data.List |
| 29 | +import Data.Vect |
| 30 | +import Decidable.Equality |
| 31 | + |
| 32 | +%default total |
| 33 | + |
| 34 | +-- ============================================================================ |
| 35 | +-- Types with Unification Variables |
| 36 | +-- ============================================================================ |
| 37 | + |
| 38 | +||| Type with unification variables, modelling the types::Type enum |
| 39 | +||| extended with Type::Var(TypeVar) for unification. |
| 40 | +public export |
| 41 | +data InfTy : Type where |
| 42 | + ||| Concrete boolean |
| 43 | + IBool : InfTy |
| 44 | + ||| Concrete integer |
| 45 | + IInt : InfTy |
| 46 | + ||| Concrete unit |
| 47 | + IUnit : InfTy |
| 48 | + ||| Function type |
| 49 | + IArrow : InfTy -> InfTy -> InfTy |
| 50 | + ||| Unification variable (corresponds to TypeVar(u32) in types.rs) |
| 51 | + IVar : Nat -> InfTy |
| 52 | + |
| 53 | +||| Decidable equality for inference types. |
| 54 | +public export |
| 55 | +DecEq InfTy where |
| 56 | + decEq IBool IBool = Yes Refl |
| 57 | + decEq IInt IInt = Yes Refl |
| 58 | + decEq IUnit IUnit = Yes Refl |
| 59 | + decEq (IArrow a1 b1) (IArrow a2 b2) with (decEq a1 a2, decEq b1 b2) |
| 60 | + decEq (IArrow a1 b1) (IArrow a1 b1) | (Yes Refl, Yes Refl) = Yes Refl |
| 61 | + decEq (IArrow a1 b1) (IArrow a2 b2) | (No c, _) = No (\case Refl => c Refl) |
| 62 | + decEq (IArrow a1 b1) (IArrow a2 b2) | (_, No c) = No (\case Refl => c Refl) |
| 63 | + decEq (IVar n) (IVar m) with (decEq n m) |
| 64 | + decEq (IVar n) (IVar n) | (Yes Refl) = Yes Refl |
| 65 | + decEq (IVar n) (IVar m) | (No c) = No (\case Refl => c Refl) |
| 66 | + decEq IBool IInt = No (\case Refl impossible) |
| 67 | + decEq IBool IUnit = No (\case Refl impossible) |
| 68 | + decEq IBool (IArrow _ _) = No (\case Refl impossible) |
| 69 | + decEq IBool (IVar _) = No (\case Refl impossible) |
| 70 | + decEq IInt IBool = No (\case Refl impossible) |
| 71 | + decEq IInt IUnit = No (\case Refl impossible) |
| 72 | + decEq IInt (IArrow _ _) = No (\case Refl impossible) |
| 73 | + decEq IInt (IVar _) = No (\case Refl impossible) |
| 74 | + decEq IUnit IBool = No (\case Refl impossible) |
| 75 | + decEq IUnit IInt = No (\case Refl impossible) |
| 76 | + decEq IUnit (IArrow _ _) = No (\case Refl impossible) |
| 77 | + decEq IUnit (IVar _) = No (\case Refl impossible) |
| 78 | + decEq (IArrow _ _) IBool = No (\case Refl impossible) |
| 79 | + decEq (IArrow _ _) IInt = No (\case Refl impossible) |
| 80 | + decEq (IArrow _ _) IUnit = No (\case Refl impossible) |
| 81 | + decEq (IArrow _ _) (IVar _) = No (\case Refl impossible) |
| 82 | + decEq (IVar _) IBool = No (\case Refl impossible) |
| 83 | + decEq (IVar _) IInt = No (\case Refl impossible) |
| 84 | + decEq (IVar _) IUnit = No (\case Refl impossible) |
| 85 | + decEq (IVar _) (IArrow _ _) = No (\case Refl impossible) |
| 86 | + |
| 87 | +-- ============================================================================ |
| 88 | +-- Substitutions (Modelling unify.rs Substitution) |
| 89 | +-- ============================================================================ |
| 90 | + |
| 91 | +||| A substitution maps unification variables to types. |
| 92 | +||| Models Substitution in unify.rs (HashMap<TypeVar, Type>). |
| 93 | +public export |
| 94 | +Subst : Type |
| 95 | +Subst = List (Nat, InfTy) |
| 96 | + |
| 97 | +||| Look up a variable in a substitution. |
| 98 | +public export |
| 99 | +lookupSubst : Nat -> Subst -> Maybe InfTy |
| 100 | +lookupSubst n [] = Nothing |
| 101 | +lookupSubst n ((m, ty) :: rest) = |
| 102 | + if n == m then Just ty else lookupSubst n rest |
| 103 | + |
| 104 | +||| Apply a substitution to a type. |
| 105 | +||| Models Substitution::apply in unify.rs. |
| 106 | +public export |
| 107 | +applySubst : Subst -> InfTy -> InfTy |
| 108 | +applySubst s IBool = IBool |
| 109 | +applySubst s IInt = IInt |
| 110 | +applySubst s IUnit = IUnit |
| 111 | +applySubst s (IArrow a b) = IArrow (applySubst s a) (applySubst s b) |
| 112 | +applySubst s (IVar n) = |
| 113 | + case lookupSubst n s of |
| 114 | + Nothing => IVar n |
| 115 | + Just ty => applySubst s ty |
| 116 | + |
| 117 | +||| Compose two substitutions. |
| 118 | +||| applySubst (compose s1 s2) t = applySubst s1 (applySubst s2 t) |
| 119 | +public export |
| 120 | +compose : Subst -> Subst -> Subst |
| 121 | +compose s1 s2 = map (\(n, ty) => (n, applySubst s1 ty)) s2 ++ s1 |
| 122 | + |
| 123 | +||| The empty substitution is an identity. |
| 124 | +public export |
| 125 | +emptySubstId : (ty : InfTy) -> applySubst [] ty = ty |
| 126 | +emptySubstId IBool = Refl |
| 127 | +emptySubstId IInt = Refl |
| 128 | +emptySubstId IUnit = Refl |
| 129 | +emptySubstId (IArrow a b) = |
| 130 | + rewrite emptySubstId a in |
| 131 | + rewrite emptySubstId b in Refl |
| 132 | +emptySubstId (IVar n) = Refl |
| 133 | + |
| 134 | +-- ============================================================================ |
| 135 | +-- Occurs Check (Modelling unify.rs occurs check) |
| 136 | +-- ============================================================================ |
| 137 | + |
| 138 | +||| Check whether a variable occurs in a type. |
| 139 | +||| Models the occurs check in Unifier::unify in unify.rs. |
| 140 | +public export |
| 141 | +occurs : Nat -> InfTy -> Bool |
| 142 | +occurs n IBool = False |
| 143 | +occurs n IInt = False |
| 144 | +occurs n IUnit = False |
| 145 | +occurs n (IArrow a b) = occurs n a || occurs n b |
| 146 | +occurs n (IVar m) = n == m |
| 147 | + |
| 148 | +||| If a variable does not occur in a type, substituting for that variable |
| 149 | +||| does not change the type. |
| 150 | +public export |
| 151 | +noOccursNoChange : (n : Nat) -> (ty : InfTy) -> (s : InfTy) |
| 152 | + -> occurs n ty = False |
| 153 | + -> applySubst [(n, s)] ty = ty |
| 154 | +noOccursNoChange n IBool s prf = Refl |
| 155 | +noOccursNoChange n IInt s prf = Refl |
| 156 | +noOccursNoChange n IUnit s prf = Refl |
| 157 | +noOccursNoChange n (IArrow a b) s prf = |
| 158 | + let (prfA, prfB) = orFalseImpliesBothFalse (occurs n a) (occurs n b) prf |
| 159 | + in rewrite noOccursNoChange n a s prfA in |
| 160 | + rewrite noOccursNoChange n b s prfB in Refl |
| 161 | + where |
| 162 | + orFalseImpliesBothFalse : (x : Bool) -> (y : Bool) -> (x || y) = False |
| 163 | + -> (x = False, y = False) |
| 164 | + orFalseImpliesBothFalse False False Refl = (Refl, Refl) |
| 165 | +noOccursNoChange n (IVar m) s prf with (decEq n m) |
| 166 | + noOccursNoChange n (IVar n) s prf | (Yes Refl) = |
| 167 | + absurd (trueNotFalse (sym prf)) |
| 168 | + where |
| 169 | + trueNotFalse : True = False -> Void |
| 170 | + trueNotFalse Refl impossible |
| 171 | + noOccursNoChange n (IVar m) s prf | (No _) = Refl |
| 172 | + |
| 173 | +-- ============================================================================ |
| 174 | +-- Unification Result |
| 175 | +-- ============================================================================ |
| 176 | + |
| 177 | +||| Unification either succeeds with a substitution or fails. |
| 178 | +||| Models the Result type returned by Unifier::unify. |
| 179 | +public export |
| 180 | +data UnifyResult : Type where |
| 181 | + ||| Unification succeeded with a most-general unifier. |
| 182 | + Unified : (s : Subst) -> UnifyResult |
| 183 | + ||| Unification failed (type mismatch or occurs check). |
| 184 | + UnifyFail : UnifyResult |
| 185 | + |
| 186 | +-- ============================================================================ |
| 187 | +-- Unification Soundness |
| 188 | +-- ============================================================================ |
| 189 | + |
| 190 | +||| A substitution S is a unifier of T1 and T2 if S(T1) = S(T2). |
| 191 | +public export |
| 192 | +IsUnifier : Subst -> InfTy -> InfTy -> Type |
| 193 | +IsUnifier s t1 t2 = applySubst s t1 = applySubst s t2 |
| 194 | + |
| 195 | +||| Unification for identical types produces the empty substitution. |
| 196 | +public export |
| 197 | +unifyRefl : (ty : InfTy) -> IsUnifier [] ty ty |
| 198 | +unifyRefl ty = rewrite emptySubstId ty in |
| 199 | + rewrite emptySubstId ty in Refl |
| 200 | + |
| 201 | +||| Unifying a variable with a type (no occurs) produces a valid substitution. |
| 202 | +public export |
| 203 | +unifyVarLeft : (n : Nat) -> (ty : InfTy) |
| 204 | + -> occurs n ty = False |
| 205 | + -> IsUnifier [(n, ty)] (IVar n) ty |
| 206 | +unifyVarLeft n ty noOcc = |
| 207 | + rewrite noOccursNoChange n ty ty noOcc in Refl |
| 208 | + |
| 209 | +||| Unifying two arrow types decomposes to unifying their components. |
| 210 | +||| If S is a unifier of (A1 -> B1) and (A2 -> B2), then |
| 211 | +||| S unifies A1 with A2, and S unifies B1 with B2. |
| 212 | +public export |
| 213 | +unifyArrowDecompose : (s : Subst) |
| 214 | + -> IsUnifier s (IArrow a1 b1) (IArrow a2 b2) |
| 215 | + -> (IsUnifier s a1 a2, IsUnifier s b1 b2) |
| 216 | +unifyArrowDecompose s prf = |
| 217 | + let (prfA, prfB) = arrowInj prf |
| 218 | + in (prfA, prfB) |
| 219 | + where |
| 220 | + arrowInj : IArrow x1 y1 = IArrow x2 y2 -> (x1 = x2, y1 = y2) |
| 221 | + arrowInj Refl = (Refl, Refl) |
| 222 | + |
| 223 | +||| Concrete type constructors with different heads cannot unify. |
| 224 | +||| Models the type mismatch errors in Unifier::unify. |
| 225 | +public export |
| 226 | +boolNotInt : Not (IsUnifier s IBool IInt) |
| 227 | +boolNotInt prf = case prf of Refl impossible |
| 228 | + |
| 229 | +public export |
| 230 | +boolNotUnit : Not (IsUnifier s IBool IUnit) |
| 231 | +boolNotUnit prf = case prf of Refl impossible |
| 232 | + |
| 233 | +public export |
| 234 | +intNotUnit : Not (IsUnifier s IInt IUnit) |
| 235 | +intNotUnit prf = case prf of Refl impossible |
| 236 | + |
| 237 | +-- ============================================================================ |
| 238 | +-- Inference Soundness Statement |
| 239 | +-- ============================================================================ |
| 240 | + |
| 241 | +||| Erase an inference type to a core type (ground types only). |
| 242 | +||| This connects the inference world (InfTy with variables) to the |
| 243 | +||| core type checking world (Ty without variables). |
| 244 | +public export |
| 245 | +eraseTy : InfTy -> Maybe Ty |
| 246 | +eraseTy IBool = Just TBool |
| 247 | +eraseTy IInt = Just TInt |
| 248 | +eraseTy IUnit = Just TUnit |
| 249 | +eraseTy (IArrow a b) = do |
| 250 | + a' <- eraseTy a |
| 251 | + b' <- eraseTy b |
| 252 | + Just (TArrow a' b') |
| 253 | +eraseTy (IVar _) = Nothing |
| 254 | + |
| 255 | +||| A fully-resolved inference type (no remaining variables) erases |
| 256 | +||| to a core type. |
| 257 | +public export |
| 258 | +data FullyResolved : InfTy -> Type where |
| 259 | + ResBool : FullyResolved IBool |
| 260 | + ResInt : FullyResolved IInt |
| 261 | + ResUnit : FullyResolved IUnit |
| 262 | + ResArrow : FullyResolved a -> FullyResolved b -> FullyResolved (IArrow a b) |
| 263 | + |
| 264 | +||| A fully resolved type always erases successfully. |
| 265 | +public export |
| 266 | +resolvedErases : FullyResolved ty -> (coreTy : Ty ** eraseTy ty = Just coreTy) |
| 267 | +resolvedErases ResBool = (TBool ** Refl) |
| 268 | +resolvedErases ResInt = (TInt ** Refl) |
| 269 | +resolvedErases ResUnit = (TUnit ** Refl) |
| 270 | +resolvedErases (ResArrow ra rb) = |
| 271 | + let (ca ** pa) = resolvedErases ra |
| 272 | + (cb ** pb) = resolvedErases rb |
| 273 | + in (TArrow ca cb ** |
| 274 | + rewrite pa in |
| 275 | + rewrite pb in Refl) |
| 276 | + |
| 277 | +||| Inference soundness statement (for closed ground terms): |
| 278 | +||| If type inference resolves to a ground type, then the typing judgement holds. |
| 279 | +||| |
| 280 | +||| This is a specification — the full constructive proof requires modelling |
| 281 | +||| the entire inference algorithm. The statement captures the invariant |
| 282 | +||| that TypeChecker.finish(ty) with valid=true implies the typing relation. |
| 283 | +public export |
| 284 | +InferenceSoundnessStatement : Type |
| 285 | +InferenceSoundnessStatement = |
| 286 | + (ctx : Ctx n) -> (t : Term n) -> (infTy : InfTy) |
| 287 | + -> (resolved : FullyResolved infTy) |
| 288 | + -> (coreTy : Ty) |
| 289 | + -> (erasePrf : eraseTy infTy = Just coreTy) |
| 290 | + -> HasTy ctx t coreTy |
| 291 | + |
| 292 | +-- ============================================================================ |
| 293 | +-- Unification Idempotence |
| 294 | +-- ============================================================================ |
| 295 | + |
| 296 | +||| Applying a substitution twice is the same as applying it once (on its |
| 297 | +||| own range). This is a key property of most-general unifiers. |
| 298 | +||| |
| 299 | +||| Models the idempotence that Substitution::apply relies on in unify.rs. |
| 300 | +public export |
| 301 | +substIdempotent : (s : Subst) -> (ty : InfTy) |
| 302 | + -> (idempotent : (n : Nat) -> (rhs : InfTy) |
| 303 | + -> lookupSubst n s = Just rhs |
| 304 | + -> applySubst s rhs = rhs) |
| 305 | + -> applySubst s (applySubst s ty) = applySubst s ty |
| 306 | +substIdempotent s IBool _ = Refl |
| 307 | +substIdempotent s IInt _ = Refl |
| 308 | +substIdempotent s IUnit _ = Refl |
| 309 | +substIdempotent s (IArrow a b) idem = |
| 310 | + rewrite substIdempotent s a idem in |
| 311 | + rewrite substIdempotent s b idem in Refl |
| 312 | +substIdempotent s (IVar n) idem with (lookupSubst n s) proof prf |
| 313 | + substIdempotent s (IVar n) idem | Nothing = prf |
| 314 | + substIdempotent s (IVar n) idem | (Just ty) = |
| 315 | + rewrite idem n ty (sym prf) in Refl |
0 commit comments