Explain how refinement type checking reduces to SMT queries, using the example of checking that a function call div(x, y) is safe where div expects {v : Int | v != 0} as its second argument.
Think about your answer, then reveal below.
Model answer: At the call site div(x, y), the type checker must verify that y has type {v : Int | v != 0}. It collects the path conditions (e.g., from an enclosing 'if y != 0' branch) and the refinements of y's declared type, forming the context. It then asks the SMT solver: given the context, is y != 0 valid? If the solver says yes (valid), the call type-checks. If the solver says no (with a counterexample), the type checker reports an error showing a concrete scenario where y could be zero.
This reduction is what makes refinement types practical. Every subtyping check {v : T | P} <: {v : T | Q} reduces to the implication P => Q, which is an SMT query. Path-sensitivity comes from collecting predicates from conditional branches: inside 'if y != 0 then div(x, y)', the path condition y != 0 is added to the context, making the SMT query trivially valid. The programmer writes normal code with type annotations; the tool handles the rest.