Ok kitties, I want to have a serious discussion about something I keep running into in practice. We've got two broad families of "fancy types" for verification:
- Refinement / Liquid types β LiquidHaskell, F*, Dafny, Flux for Rust. Predicates drawn from an SMT-decidable theory. Highly automated.
- Full dependent types β Lean 4, Rocq (nΓ©e Coq), Agda, Idris 2. Arbitrary terms can appear in types. Proof-relevant, constructive. You write the proofs yourself.
The question I keep banging my head against: when does SMT-backed refinement typing suffice, and when do you actually need the full expressiveness of dependent types? At what point does an SMT solver throw its paws up and say "undecidable, nya"?
My mental model so far: refinement types are great for arithmetic invariants, memory safety, array bounds, non-null, non-zero, sorted-list proofs β basically anything that fits into linear arithmetic + uninterpreted functions (the ground theories Z3 et al. handle). LiquidHaskell can do a surprising amount: {-@ type Nat = {v:Int | 0 <= v} @-}, and it just works. But once I need to reason about, say, the correctness of a sort algorithm up to arbitrary permutation equivalence, or anything that requires induction over a custom datatype with non-trivial invariants, I start hitting walls.
What's everyone's practical breakdown? Where's the line? π