Thrust: A Prophecy-Based Refinement Type System for Rust
(authored by agents unless marked đ§) Tag: ogawa2025thrust, paper
what Thrust contributes
Thrust is an automated Rust verifier that combines dependent refinement types, prophecy variables for mutable borrows, and constrained Horn clause (CHC) solving. The abstract states that it supports âsynthesis of inductive invariants for loops and recursive functionsâ and âprecise verification, especially for programs involving pointer aliasing and borrowing, without sacrificing the benefits of automated verificationâ (abstract, p. 230:1). It targets safe Rust and delegates âaliasing XOR mutabilityâ and well-borrowedness (in the stacked borrows sense) to Rustâs type system. The authors prove soundness of the type system for a core calculus λCOR under that assumption, implement Thrust as a Rust compiler plugin, and evaluate it against RustHorn, Flux, and Prusti.
technique 1: infer refinement types with CHCs
What it is. A refinement type is a base type constrained by a logical predicate. The paperâs incr example has the type (x : i32) â {Μ : i32 | Μ = x + 1} and annotations #[thrust::requires(true)] and #[thrust::ensures(result == x + 1)] (Fig. 1, p. 230:6). Thrust infers missing predicates by generating CHCs and solving them with Spacer, a solver based on property directed reachability (PDR). The implementation âprepares refinement type templates, generates type derivations based on them, and collects subtyping constraints to form a system of CHC constraintsâ (Section 5, p. 230:17).
When to use it. Inference: use inference for arithmetic postconditions, loop invariants, recursive summaries, and functional correctness where a solver can synthesize predicates; use annotations when synthesis fails. The paper says annotations serve âas hints to its backend CHC solverâ (Section 1.1, p. 230:3).
How to apply it. Inference: the paper gives no step list; these steps are reconstructed from Sections 1.1 and 5. (1) Write Rust and optional requires/ensures refinement annotations. (2) Compile to the optimized MIR the Rust compiler provides âafter desugaring, type checking, borrow checking, and certain optimizationsâ (Section 5, p. 230:17). (3) Prepare refinement type templates. (4) Generate type derivations and collect subtyping constraints as CHCs. (5) Solve them with Spacer. (6) If solving fails, add function-signature or subexpression annotations and rerun. The experiments use a 180-second time limit (Section 5.1, p. 230:17).
Verus form (Inference: Thrust does not use Verus). Translate a refinement to requires and ensures on an executable fn; express loop facts with invariant declarations and arithmetic lemmas in proof fns. A Verus spec fn can define a pure predicate, but the executable function carries the contract. CHC inference is not a Verus construct; the analogous workflow is solver-assisted invariant discovery followed by explicit Verus proof. Keep the contract on the executable function, and use proof functions only in verification code.
Evidence and limits. Table 2 (p. 230:18) has 32 rows: 4 categories (prec, ho-funs, adt, mod) Ă 4 programs Ă a safe and an unsafe variant. Thrust returns the expected result on all 32 rows (16 safe, 16 unsafe), while RustHorn, Flux, and Prusti each return unknown, abort, or timeout on some rows. This is a small benchmark set, not a claim about all Rust. The unsafe results carry a dagger: âWe manually confirmed that the program is indeed unsafe when Thrust concludes that the program is unsafe, since we have not formally proved the completeness of Thrustâ (Table 2 footnote, p. 230:18). Inference: completeness is conjectured, not proved, so an âunsafeâ answer from Thrust is not a proved counterexample.
technique 2: strong updates from Rustâs aliasing XOR mutability
What it is. Rust guarantees that a memory location has either multiple immutable references or a single mutable reference, not both (Section 1, p. 230:2). Thrust uses this to update a pointed-to value precisely (a strong update). In incr_max, the type system guarantees the two &mut i32 arguments p and q do not alias; the function increments only the referent of the larger one. The return type records both cases: âp â„ âq â§ âŠp = âp + 1 â§ âŠq = âq, or âp < âq â§ âŠp = âp â§ âŠq = âq + 1, where â is the value at call time and ⊠the value at return (Fig. 2, pp. 230:6â7). The paper says Thrust can also synthesize this predicate automatically by type inference (Section 2, pp. 230:6â7) and achieves precision âwithout sacrificing full automationâ (Section 1.1, p. 230:3).
How to apply it. Inference: steps reconstructed from Section 2. (1) Keep mutable references as ordinary &mut. (2) Rely on the borrow checker for non-aliasing. (3) At a write through a mutable reference, update the current value in that referenceâs logical representation. (4) Infer or annotate the functionâs dependent return type. Dynamic selection of a mutable reference is the stress case: Flux âcannot assign precise types to programs that dynamically select the target for writing through mutable references based on runtime valuesâ (Section 2, p. 230:7), because its strong references âare tracked as singleton typesâ (Section 5.2, p. 230:19).
Verus form (Inference). Use &mut T in executable signatures and maintain a ghost snapshot of each referent. State branch-specific postconditions with ensures implications or a disjunction. A proof of non-aliasing must come from Verusâs accepted mutable-reference rules; do not invent a modifies clause. Update ghost values in proof code and prove the executable assignment preserves them. If references are stored in an enum or collection, the representation and aliasing facts need explicit invariants.
Evidence and limits. In Table 2 (p. 230:18), prec-1-inc-max-plain-safe is verified safe by Thrust (0 annotation lines, 0.05 s), RustHorn (0.04 s), and Prusti (0 annotation lines, 3.77 s); Flux reports unknown (0.07 s). In the prec category the Flux and Prusti inputs were inlined âin order to avoid the need for annotationsâ (Section 5.2, p. 230:18). This single row shows the targeted precision, not general superiority. The design covers safe Rust only; the conclusion calls precise, automated verification of interior mutability and unsafe code âan important challengeâ (Section 7, p. 230:22).
technique 3: prophecy variables for borrow release
What it is. A mutable borrow is represented by its current value and a prophecy for its value when the borrow is released. The paper explains: âFrom the time the borrow is created until it is released, the prophecy refers to the âfuture valueâ of the mutable borrowâ (Section 1.2, p. 230:5). In Fig. 3âs main, x starts as âš2â©; creating m = &mut *x gives m the representation âš2, lâ© with a fresh prophecy l and changes x to âšlâ©. Inside decr, the argument starts as âšx0, ââ© and becomes âšx0 â 1, ââ© after the decrement; at the end of its lifetime â is fixed to x0 â 1, so the return type gives âŠx = âx â 1. At the call site this yields l = 2 â 1, which proves assert!(*x == 1) (Fig. 3 and Section 2, pp. 230:6â7).
How to apply it. Steps follow rules T_MutBorrow, T_Assign, and T_DropMut (Section 4.2, pp. 230:12â14). (1) At a mutable borrow, create a fresh prophecy variable y. (2) Give the new reference the representation âšcurrent, yâ© and replace the borrowed part of the owner by y. (3) When the reference is released by drop, fix its future value to its current value; λCOR releases a pointer after assignment, so an assignment fixes the future value to the assigned value. (4) Continue checking with the ownerâs value equal to the prophecy. For a reborrow n = &mut *m (Fig. 4, p. 230:8), create l2, change m from âš2, l1â© to âšl2, l1â©, give n âš2, l2â©; *n -= 1 fixes l2 = 2 â 1, then *m -= 1 fixes l1 = l2 â 1.
Verus form (Inference). Model the future value with a ghost variable or a ghost cell associated with the borrow lifetime. Use old(x) in contracts for the entry value and an explicit ghost result for the value transferred back at release. A proof function can establish the relation between the current value and the prophecy; executable code must not call it. The precise encoding depends on Verusâs borrow support, so this is a conceptual translation, not a claim that Verus exposes Thrustâs prophecy primitive.
Evidence and limits. The paper says âThrust propagates update information to the original owner upon mutable borrow releaseâ (Section 1.1, p. 230:3). Theorem 4.1 (Section 4.3, p. 230:16) shows a well-typed program either finishes, steps, or hits BorrowError (a stacked-borrows violation); the paper then relies on the fact that âRustâs borrow checker guarantees that BorrowError never happensâ. Inference: prophecy reasoning itself does not establish well-borrowedness; it trusts the borrow checker. The system also separates program variables from logical variables because changing a variable referenced by a refinement could otherwise make the refinement invalid (Section 2, p. 230:8).
technique 4: immutable logical snapshots and modular verification
What it is. Refinements depend only on implicit logical variables, not mutable program variables. The paper says, âLogical variables represent static values, that is, their meaning does not change even as the program evaluation proceedsâ (Section 2, p. 230:8). Without this, a refinement {y : i32 | y = âx} would become invalid after decr changes the referent of x, even though y is not modified (Section 2, p. 230:8). Formally, typing contexts Î hold implicit variables and place contexts Î map program variables to logical forms (Section 4.1, p. 230:10). Thrust also supports âfunction- and subexpression-level modular verificationâ with user annotations as solver hints (Section 1.1, p. 230:3).
How to apply it. Inference: steps reconstructed from Sections 2 and 4. (1) At each binding (rule T_Let), introduce implicit variables for the bound value. (2) Write refinements only over implicit and prophecy variables. (3) On mutation, change the logical form assigned to the program variable, not old implicit variables. (4) At a function call, use the calleeâs inferred or annotated type as its summary. (5) For nonlinear arithmetic or hard recursive invariants, add annotations rather than rely on inference.
Verus form (Inference). Use old(...) and ghost snapshots in requires/ensures; use loop invariant clauses to carry stable facts. Define pure relations in spec fns and prove them in proof fns. The key change is that Verus requires explicit ghost state and lemmas where Thrust inserts implicit variables. This technique is directly applicable to avoiding accidental dependence on mutable state.
Evidence and limits. In Table 2 (p. 230:18), Thrust verifies all four mod safe programs in 0.05â0.08 s; the two nonlinear-arithmetic programs (mod-3-nonlin-sum, mod-4-nonlin-mult-mut) use 2 annotation lines each, and RustHorn times out on both safe variants. The âAnnot.â column counts âthe number of lines used for user-specified invariants in function signaturesâ (Section 5.2, p. 230:18). Section 5.3 (p. 230:20) reports a cost: introducing implicit variables at each binding over MIRâs simple operands creates many implicit variables, which increases the load on the CHC solver.
technique 5: refine higher-order functions and algebraic data
Thrustâs type-based approach assigns refinement types to higher-order functions and to elements of algebraic data types. The paper says this âallows compositional verification by inferring refinement types for higher-order functions and data elements within algebraic data typesâ (Section 1.1, p. 230:4). For polymorphic collections such as lists, Thrust introduces âa predicate variable that enables per-element refinement for the type parameterâ (Section 5.2, p. 230:19), so the CHC solver need not synthesize recursive functions over the data type.
How to apply it. Inference: steps reconstructed from Sections 1.1 and 5.2. (1) Give a function parameter a refinement type describing its input/output relation. (2) Check each actual function against it by subtyping (rule S_Fun, Fig. 9, p. 230:15). (3) For a list or tree, attach a predicate variable to the element type. (4) Let CHC solving find the predicates.
Verus form (Inference). Encode higher-order contracts as spec fn relations passed as ghost arguments, or prove each closureâs contract separately. For recursive datatypes, define a recursive spec fn predicate and prove list/tree lemmas by induction. Verus does not infer Thrustâs refinement types automatically, so the user supplies the predicate and lemmas.
Evidence and limits. In Table 2 (p. 230:18), Thrust verifies all eight ho-funs rows (four safe, four unsafe) in 0.04â0.12 s with 0 annotation lines; RustHorn aborts on all eight. On the four adt safe programs Thrust takes 0.09â4.09 s; adt-2-dict-pos needs 2 annotation lines. The formal calculus λCOR does not support general recursive ADTs, and its functions âmust not contain free variablesâ; introducing existential types for boxed closures âis left for future workâ (Section 4.4, pp. 230:16â17).
implementation, trust, and product implications
The implementation is a Rust compiler plugin over optimized MIR with the CHC solver Spacer. Experiments ran on Ubuntu 24.04 with 32 GB RAM and 16 logical CPU cores on an AMD EPYC 7R13, using Spacer bundled with Z3 4.13.0 and a 180-second time limit (Section 5.1, p. 230:17). The paper says MIR gives âstability against changes in the source codeâ (Section 5, p. 230:17), but Thrust âinterprets MIR more rigorously compared to RustHorn, resulting in more complex constraints being generatedâ (Section 5.3, p. 230:20), and is âsometimes slowerâ than RustHorn on the RustHorn benchmark set. The RustHorn-benchmark results appear only as a scatter plot (Fig. 10, p. 230:20); the paper defers the full results to the supplementary material, which this study does not check.
The proved result is soundness of the type system for λCOR assuming well-borrowed executions (Theorem 4.1, Section 4.3, p. 230:16). Inference: the trusted base includes Rustâs borrow checker, the correspondence between λCOR and MIR (discussed only in the supplementary material, Section 5, p. 230:17), the CHC solver, and the implementation. Inference: the threat to VL is moderate and direct. Prophecy and immutable snapshots address the hardest mutation/borrow precision problems, while CHC inference could reduce annotation burden. VL should reuse the explicit soundness boundary and avoid claiming that solver success proves completeness. Existing Verus support for mutable references, old, invariant, requires, ensures, and proof functions should be checked before importing any Thrust notation.
techniques for the catalog
- CHC refinement inference: generate Horn constraints from refinement templates and solve them to infer loop, recursion, and function predicates
- aliasing-aware strong updates: use Rustâs exclusive mutable-reference guarantee to update the selected referent precisely
- prophecy borrow summaries: track a borrowâs future release value and propagate it back to the owner
- immutable logical snapshots: keep refinements over stable implicit variables while program values mutate
- modular higher-order and ADT refinement: attach predicates to function types and data elements for compositional proofs Checked 2026-10-17 against the full paper by a Claude agent (mid-paper quote: âBecause Rustâs borrow checker guarantees that BorrowError never happens, we can ensure that (safe) Rust programs well-typed in our refinement type system never get stuck.â): checked every quotation, number, section and page reference, and technique step against the paper text, including every Table 2 row cited; changed the Table 2 counts (4 safe programs per category, not 8; 32 rows are 16 safe/unsafe pairs), the
prec-1times (RustHorn 0.04 s, Prusti 3.77 s, RustHorn has no annotation column), themodsafe time range (0.05â0.08 s), theho-funsrow count, the Table 2 page (p. 230:18 only), several section/page references (Flux singleton types in Section 5.2; propagation quote from Section 1.1), the Fig. 3 and Fig. 4 walk-throughs, the Section 4.4 limits (they apply to λCOR), marked reconstructed steps and the trusted-base list âInference:â, and deleted unsupported claims (a branch-join disjunction step, a claim that modular summaries may hide incorrect contracts, a claim about the memory logic unsafe code needs); did not check the supplementary material, the Fig. 10 scatter-plot values, or any Verus statement.
Last edited: