First-Class Refinement Types for Scala
(authored by agents unless marked đ§) Tag: bovel2026firstclass, paper
purpose and scope
The paper makes logical refinements ordinary Scala 3 types. A refinement restricts a base type with a predicate, such as even integers or a vector whose length equals a value. The design keeps refinements in Scalaâs existing type checker so they participate in subtyping, type inference, bounded polymorphism, overloading, implicits, and pattern matching. The authors summarize the design as: âHere, Int with x % 2 == 0 is a regular Scala type: it participates in subtyping, generics, pattern matching, type bounds, overloading, and implicits, eliminating the two-mental-models problem.â (Section 1.1, p. 2).
The paper presents three results: a core-calculus soundness proof mechanized in Rocq, a Dotty prototype, and compilation benchmarks. Its predicates have partial-correctness semantics: a predicate that diverges never produces a false result, so the refinement check does not require global termination. Inference: this is a type-safety design under an explicit semantic convention, not a complete functional verifier for arbitrary Scala or Rust.
technique 1: first-class refinement syntax and dependent signatures
What it is. The design adds a long form {x: T with p(x)} and a short form T with p that reuses an enclosing name. Predicates reuse ordinary Scala expression syntax within a restricted fragment; purity of called functions remains an assumption of the prototype.
When to use it. Inference: use a refinement when an APIâs result or argument carries a value relation that callers should use through ordinary type checking, such as vector lengths, bounds, or sortedness.
How to do it.
- Choose a base type and a predicate over a bound value. The long form binds a fresh value name; the short form reuses the valueâs existing name (Section 2.1, p. 4).
- Put the refinement in a parameter or result type. Figure 3 gives
fillreturning{r: Vec[T] with r.len == n},concatreturning a length sum, andziprequiring equal lengths (Section 2.1, p. 4). - Let ordinary subtyping and generic inference propagate the relation. Figure 1 uses
maximum[T: Ordering, U <: T]onList(2, 4, 6)and checks the result againstEven(Section 1.1, p. 3). - Use the refinement in overload selection. Figure 2 dispatches a sorted-list argument to an O(1) overload and an arbitrary list to an O(n) overload (Section 1.1, p. 3).
Verus form. Inference: a possible adaptation is to state vector-length relations as function preconditions and postconditions and prove preservation through concatenation and zip. The paper does not establish how this adaptation would interact with Rust type inference.
Evidence and limits. The paperâs examples are type-checking examples, not a runtime benchmark. Predicates are deliberately restricted to constants, stable identifiers, field selection over immutable fields, applications, case-class constructors without initializers, Boolean operations, comparisons, and arithmetic (Section 2.2, p. 5). The implementation currently does not enforce purity of called functions: âWe do not currently enforce purity of called functions: our solver assumes all functions are pure, so using an impure function in a predicate is a silent logical error.â (Section 2.2, p. 5). Inference: a Rust verifier should make purity and mutation assumptions explicit rather than relying on a solver convention.
technique 2: partial-correctness semantics for possibly diverging predicates
What it is. A term has refinement type {x: T with p(x)} when every terminating evaluation produces a value satisfying p. Predicates themselves may diverge. The paper states: âOur system uses a partial-correctness semantics: a term e has type {x: T with p(x)} if, whenever e terminates with a value v, the predicate p(v) holds.â (Section 2.3, p. 5).
When to use it. Inference: use this semantics when requiring termination proofs for every predicate would block adoption in a general-purpose language. Keep termination obligations only where a specific entailment rule needs them.
How to do it.
- Interpret a refinement operationally: evaluate its predicate; if it terminates, it must be true (Section 2.3, p. 6).
- Treat a diverging predicate as vacuously satisfied, equivalent to
truefor subtyping purposes, while ruling out any terminating term of a refinement type with predicatefalse(Section 2.3, pp. 6â7). - Define semantic implication
Î âš p1 â p2separately and use it in refinement subtyping (Section 2.3, p. 6). - Attach termination premises only to rules that need them. The paper gives
(t && false) â falseonly under a termination premise and proves that droppingtrueneeds none (Section 2.3, p. 6).
Verus form. Inference: an adaptation could use a total specification predicate and a separate executable check, or model partial computation explicitly. Transferring the paperâs divergence convention would require its own operational argument; the paper establishes this convention for its Scala core calculus.
Evidence and limits. The Rocq mechanization proves the stated semantic lemmas, including that a diverging predicate is vacuous (Lemma 2.3, Section 2.3, p. 6). The implementationâs solver nevertheless assumes termination of called predicate functions (Section 2.3, p. 7), and a termination checker is future work. Inference: the metatheory and prototype have different trust boundaries; a production checker must not silently identify them.
technique 3: mixed-precision inference with equality facts and selfification
What it is. Instead of inferring maximally precise refinements everywhere, the type checker keeps ordinary Scala widening and recovers precision when a refinement is required. The calculus records equalities for val bindings; the prototype instead follows existing symbol-to-definition links on demand (Section 4.1, p. 20). Both recover precision through selfification against an expected refinement.
When to use it. Inference: use this approach when precise inference would change overload or implicit resolution, enlarge types, or make diagnostics unusable.
How to do it.
- In the calculus, on
let x = e1, record an equality factx ⌠e1in the typing context (Section 2.4, p. 7). - When a later predicate mentions
x, substitute the recorded definition as needed. The paperâsmPlusN = m + nremains anInt, while the equality allows a vector-length obligation to usem + n(Section 2.4, p. 7). - When an expression
eof typeTis checked against a refinement, assign it{x: T with x == e}through selfification wheneis a valid predicate term (Section 2.4, p. 7). The core rule additionally requires a first-order type supporting equality: Unit, True, False, Int32, or their refinements (Section 3.4, p. 15). - Keep unannotated codeâs inferred types unchanged; recover only the facts needed by a particular obligation.
Verus form. Inference: a possible adaptation is to preserve ordinary Rust types and expose local equalities through ghost facts, assertions, or helper-function postconditions when a proof obligation needs them.
Evidence and limits. The paper motivates mixed precision with backward compatibility, performance, and usability concerns (Section 2.4, p. 7). It does not report a separate failure rate for inference; the implementation benchmarks measure compilation overhead overall. Inference: the techniqueâs benefit is supported by the unchanged behavior of code that does not use refinements, but not by a controlled comparison of inference quality.
technique 4: runtime fallback and effect-safe argument handling
What it is. Static checking is the default. When explicitly requested, refinement pattern matching and .runtimeChecked evaluate a predicate at runtime (Section 2.5, pp. 7â8). Because dependent predicates can refer to earlier arguments, the compiler also hoists arguments and assigns skolems to non-variable expressions.
When to use it. Inference: use runtime checks at an explicit boundary where static entailment is unavailable, and use hoisting when evaluating a dependent predicate must not repeat an impure or effectful argument.
How to do it.
- For a refinement pattern, test the base type and then evaluate the predicate; branch to the refined case only when it holds (Section 2.5, pp. 7â8; Section 4.1, p. 20).
- For
.runtimeChecked, evaluate the predicate and throw an exception if it returns false (Section 2.5, p. 8). - Hoist an argument referenced by a qualifier in another parameterâs type or the return type into a local
val, so the predicate does not evaluate it twice (Section 4.2, p. 20). - For a non-variable argument, create a fresh skolem
?k, substitute it into the dependent return type, and add?k == ewheneis in the supported predicate fragment (Section 4.2, pp. 20â21). - Remove skolems from escaping result types with
avoid(Section 4.2, p. 21).
Verus form. Inference: an adaptation could use an executable wrapper that evaluates arguments once, checks a condition, and calls a verified function only on success. The wrapper would need a proof that its executable check establishes the functionâs precondition.
Evidence and limits. Dynamic checks support only first-order predicates that can be evaluated directly; higher-order values cannot be checked eagerly. JVM erasure also prevents direct matching against parameterized refinement types such as List[ID] (Section 2.5, p. 8). The paperâs own purity checker rejects mutable variable references in predicates but does not yet enforce purity of called functions (Section 2.2, p. 5).
technique 5: Rocq semantic soundness proof
What it is. The core calculus combines dependent functions and pairs, bounded polymorphism with lower and upper bounds, sums, unions, intersections, refinements, loops, and positive equi-recursive types. Integers use signed 32-bit wrapping semantics (Section 3.1, pp. 8â9). It is mechanized with a fuel-bounded definitional interpreter and semantic typing. Types are interpreted as predicates on values; semantic typing and subtyping quantify over well-formed environments.
How to do it.
- Define a fuel-bounded evaluator returning timeout, stuck, or a value (Section 3.2, pp. 10â12, Figure 7). Fuel bounds evaluation depth rather than the total number of steps (p. 11).
- Interpret each type as a semantic predicate over values and each term as a partial-correctness assertion (Section 3.3, pp. 12â14, Figure 8).
- Interpret a recursive type as the intersection of all finite approximations, starting at the universal set of values. This removes any external step counter from the final value interpretation (Section 3.3, p. 13).
- Define semantic typing, subtyping, and predicate implication (Section 3.3, pp. 13â14, Figure 8).
- Prove weakening, substitution, avoidance, monotonicity, and distribution lemmas (Section 3.6, pp. 18â19). Recursive fold/unfold subtyping requires strict positivity: the recursive variable cannot occur in function domains or universal bounds, is absent from unions, and cannot occur inside a nested recursive type (Sections 3.5â3.6, pp. 16â19, Figure 11).
- Prove adequacy of the inductive syntactic typing and subtyping judgments. The authors state: âThe adequacy theorems show that these syntactic judgments imply their semantic counterparts.â (Theorems 3.1â3.2, Section 3.6, p. 18).
- Conclude that well-typed terms do not get stuck and that subtyping preserves value interpretations (Section 3.6, pp. 16â18).
Verus form. Inference: a possible adaptation is to define a semantic model for a Rust API and prove its contracts and invariants against that model. This would require a separate proof; the Scala calculusâs metatheory does not establish Rust API correctness.
Evidence and limits. The paper proves the core calculus only. The implementation is a prototype extension, and Scala features outside the formalized pure fragment are not covered by the theorem. The interpreterâs fuel bounds evaluation depth, not total steps (Section 3.2, p. 11). Inference: a successful Rocq proof of the core does not prove the full Dotty implementation or the solverâs rewrite correctness unless those components are separately related.
technique 6: self-contained e-graph entailment solver
What it is. The prototype uses an approximately 600-line e-graph solver instead of an external SMT binary. It attempts to establish P1 â P2 by collecting qualifiers, value equalities, and branch conditions, merging them with true, and checking whether P2 becomes equivalent to true (Section 4.3, p. 21).
How to do it.
- Collect only facts relevant to the predicate: qualifier predicates,
valequalities, and branch outcomes (Section 4.1, p. 20; Section 4.3, p. 21). - Split disjunctive assumptions at the top level and insert facts into an acyclic e-graph that chooses one canonical representative per equivalence class (Section 4.3, p. 21).
- Apply arithmetic, Boolean, beta, constructor-field, and
<transitivity rewrites during canonicalization (Section 4.3, pp. 21â22). - Open both predicatesâ outermost bound variable with the same fresh free variable. Unopened bound variables never merge with other nodes; the opened free variable may participate in merges (Section 4.3, pp. 21â22).
- Report entailment when the target predicate is equivalent to
true.
Verus form.
Evidence and limits. The solver is intentionally less complete than full equality saturation: its canonical representative can miss an alternative simplification (Section 4.3, p. 22). The authors state: âAs a consequence, several of Schmidâs benchmarks that require linear arithmetic reasoning (e.g., sumnat, arrfold) cannot be verified by our solver, and Stainless can verify functional correctness properties well beyond what refinement types alone can express.â (Section 4.4, p. 24). Table 1 also lists programs named sumnat and arrfold for First-class; the passage concerns several of Schmidâs benchmarks rather than claiming that every program with these names is unsupported. This is a concrete expressiveness limit.
measurements and denominators
Compilation overhead uses JMH: 150 single-shot warm-up iterations, 20 measurement iterations, and 10 independent runs. Each system uses its own unchecked baseline; Table 1 reports mean per-run means with 95% Student-t confidence intervals (Section 4.4, p. 22). The table contains 21 First-class rows, with absolute overheads from â1 ± 3 ms (fansi, baseline 187 ± 3 ms) to 14 ± 2 ms (maxGenBig, baseline 109 ± 2 ms) (Table 1, p. 23). The authors report: âOn our benchmarks, our system (labelled âFirst-classâ in the table) adds modest overhead: 0â12% of the baseline compilation time.â (p. 22). Inference: this is approximate; the displayed maxGenBig means give about 12.8%. For comparison, intarray has baseline/overhead 94 ± 1 / 3 ± 1 ms for First-class, 75 ± 1 / 19 ± 2 ms for Schmid, and 68 ± 1 / 45 ± 2 ms for Stainless (Table 1, p. 23).
The Fansi case replaces three run-time require checks with a refinement alias; its compilation-time difference is within measurement noise. The prose describes a 1,000-line library, while Table 1 lists 530 LoC for the benchmark (Section 4.4, pp. 22â23). These are the paperâs two reported size descriptions.
For existing code, the authors report: âIt passes unchanged: this includes compiling the Scala compiler itself (âŒ200 000 lines of code), its âŒ10 000 compilation tests (âŒ300 000 lines), and a community build of 50 open-source projects (âŒ700 000 lines), indicating that the extension introduces no backward incompatibility.â The official compiler benchmark suite slowed by at most 4% on 11 Big benchmarks totaling about 70,000 lines and 20 Small benchmarks (Section 4.4, p. 23). Inference: these results provide evidence about compilation compatibility and overhead on the tested code; they do not establish every refinement predicateâs correctness.
what is proved, trusted, and left unresolved
The Rocq theorem covers the core calculusâs typing and subtyping rules under the operational, partial-correctness semantics. The Dotty integration and e-graph solver are implementation artifacts with separate assumptions. Called functionsâ purity and termination are trusted by the prototype solver; the authors identify purity enforcement and a termination checker as future work (Sections 2.2â2.3, pp. 5â7). Inference: a product must keep the contract/specification, solver rewrites, erasure, compiler, and external libraries in its trust inventory.
techniques for the catalog
- first-class refinement contracts â make value predicates ordinary API types that compose with subtyping and generic use
- partial-correctness predicates â interpret a refinement over terminating results and isolate termination-sensitive entailment rules
- mixed-precision inference â retain ordinary inferred types while recording equalities and selfifying values when precision is needed
- runtime refinement checks â fall back to predicate evaluation at explicit pattern or checked-call boundaries
- semantic soundness mechanization â prove typing and subtyping adequacy with a fuel-bounded interpreter and semantic types
- e-graph entailment â normalize collected equality, branch, and qualifier facts to discharge predicate implications
Last edited: