Status: shipping, always on (graduated in v0.33.0; the
refinedexperiment gate is gone). No flag is needed. See contract-types-guide.md for the runtime half, and ../archive/refinement-types-plan.md for the design.
Contract types (#refine{ x : T | p }) check their predicate at runtime.
Refinement types go one step further: the compiler tries to prove the
predicate at compile time, and emits a runtime check only where the proof
fails.
The single fact that shapes the whole feature: every refinement already has a runtime meaning. So the static discharger is allowed to give up on any obligation and stay sound -- the obligation just falls back to the check it would have had anyway. That is why a partial, hand-rolled solver is a real feature rather than a broken one, and why static discharge can never make a correct program wrong.
;; Proved: x > 0 entails 2x > 0, so no runtime check is emitted for the return.
(defn double-pos [x : #refine{ v : int | (> v 0) }] : #refine{ r : int | (> r 0) }
(* x 2))
Nothing to turn on -- static discharge runs on every compile. Write a
#refine{...} and the compiler tries to prove it.
It graduated from the refined experiment in v0.33.0. If you opted in
earlier, the old spellings must now be deleted -- their compatibility shims
were retired in 0.38.0, one minor line after graduation, and each is a hard
error again:
| Old spelling | Through 0.37.0 | From 0.38.0 |
|---|---|---|
--enable=refined |
accepted, TUR-W0063, no effect |
TUR-E0310 |
:experiments [:refined] in build.tur |
accepted, TUR-W0063, no effect |
TUR-E0310 |
~/.config/turmeric/experiments.tur :enable [:refined] |
accepted, TUR-W0063, no effect |
TUR-E0310 |
#lang turmeric refined (first line of a file) |
accepted, TUR-W0064, no effect |
TUR-E0330 |
Refinement checking itself is unaffected: #refine{...} has been unconditional
since 0.33.0 and needs no flag or layer.
These compatibility shims age out one minor line after graduation, so drop the flag when convenient rather than relying on it.
The one user-visible consequence of graduation: a refinement that is
violated on every execution reaching it is now a compile error (TUR-E0371)
rather than a runtime contract failure. Everything the solver cannot decide is
unchanged -- it still falls back to the runtime check.
--strict-refine--strict-refine is a diagnostic-strictness knob, not an experiment. It turns
every obligation the solver could not prove into a hard compile error instead of
a warning plus a runtime check. Use it when you want a build in which every
refinement is discharged statically:
tur build --strict-refine src/main.tur
Two things become hypotheses -- facts the prover may assume:
:pre predicate.Three things become goals -- things it must prove:
: #refine{ r : T | p };:post predicate;(defn index-ok [v : int, n : int, i : int] : #refine{ r : int | (< r (size-of v)) }
:pre (and (= (size-of v) n) (>= i 0) (< i n))
i)
Here the :pre clause supplies three hypotheses, and the return refinement is
the goal. It is discharged statically, so the return check disappears; the
:pre clause itself is still checked at runtime, since it constrains the
caller rather than the body.
When a return or postcondition goal is proved, no runtime check is emitted for it. When it is not, the check stays exactly where it was.
Passing a value where the parameter declares a refinement is a crossing into it, so the caller owes the proof:
(load "stdlib/refine.tur")
(defn safe-div [n : int, d : NonZero] : int
(/ n d))
(safe-div 10 2) ; proved: 2 != 0
(safe-div 10 0) ; error[TUR-E0371] -- a compile failure, not a runtime panic
Definition order does not matter. Crossings are resolved after the whole compilation unit is elaborated, so a call to a function defined later in the file is checked exactly like a call to one defined earlier.
A predicate may mention a sibling parameter, and the call site substitutes the argument in that slot rather than leaving a free variable:
(defn at [n : int, i : #refine{ j : int | (and (>= j 0) (< j n)) }] : int ...)
(at 10 3) ; proved: 0 <= 3 < 10
(at 3 10) ; error[TUR-E0371]
An argument the solver cannot prove is not an error by default. Only an
argument that is definitely wrong -- one whose goal is closed, so it
evaluates to false on the values written at the site -- fails the build. That
distinction is deliberate: the callee still checks its parameters on entry, and
erroring on every argument whose value is not statically known would make the
experiment impossible to adopt incrementally. --strict-refine opts into the
stricter reading, where any undischarged argument is a hard error.
The callee's entry check is always emitted, even when every visible call site is proved. Eliding it would need whole-program knowledge of the call graph (including exported and indirect callers), and getting that wrong drops a check that was protecting something. The call-site layer is a diagnostic on top of that guard, not a licence to remove it.
A call does not have to name a global function to be checked. Two shapes carry their parameter refinements to the call site:
;; a lambda with contract parameters
(let [f (fn [x : #refine{ v : int | (> v 0) }] : int (* x 2))]
(f 21)) ; proved
;; (f 0) -- error[TUR-E0371]
;; an alias of a global function
(let [g safe-div]
(g 10 0)) ; error[TUR-E0371] -- the alias resolves to safe-div
A lambda's contract parameter gets an entry check exactly like a defn's, and
a captured value's own refinement is in scope as a hypothesis inside the
lambda body.
A typeclass method is checked too, when the dispatch resolves to a known instance -- and what is checked is that instance's parameter predicate, not the class's:
(defclass Scaler [a]
(scale-by [self : a, k : #refine{ v : int | (> v 0) }] : int))
;; an instance that RESTATES the class demand
(definstance Scaler [int]
(scale-by [self : int, k : #refine{ v : int | (> v 0) }] : int (* self k)))
(.scale-by 3 4) ; proved
(.scale-by 3 0) ; error[TUR-E0371] on argument 2 of 'scale-by'
Both the dotted (.m x ...) and bare (m x ...) forms go through the same
resolution and are checked identically.
An instance parameter with no annotation inherits the class's refinement, the same way an unannotated result inherits the class's promise:
(definstance Scaler [int]
(scale-by [self k] (* self k))) ; k inherits (> v 0)
(.scale-by 3 0) ; error[TUR-E0371] -- and the entry check enforces it too
Writing an explicit annotation is how an instance demands less, which is
legal (TUR-E0374 only forbids demanding more). A bare : int carries no
predicate, so it opts the parameter out entirely:
(definstance Scaler [float]
(scale-by [self : float, k : int] : int k)) ; demands nothing of k
(.scale-by 1.5 0) ; allowed -- correct for the instance that runs
; warning[TUR-W0377]: relies on instance leniency
That call is allowed because a statically-resolved dispatch knows which
implementation runs, and is checked against that implementation's contract --
the more precise of the two. It is linted because the leniency is not part of
the interface: the same argument fails the moment dispatch goes dynamic, or a
stricter instance appears. Only a definite violation warns -- an argument
the class predicate rejects outright, not one it merely cannot prove -- so
(.scale-by 1.5 n) for an unconstrained n is silent.
A dynamic dispatch -- an abstract receiver, so no instance is selected -- is checked against the class signature instead:
(defn use-dynamic [^Scaler a x : a] : int
(.scale-by x 5)) ; proved from the CLASS predicate alone
That is sound by the same variance argument that licenses result propagation,
run in the other direction. TUR-E0374 rejects an instance that demands more
than its class, so every instance's parameter predicate is implied by the
class's -- which makes the class predicate the strongest demand true of every
instance, and an argument satisfying it acceptable to whichever instance runs.
It is also the only honest choice available there. With no resolved instance the dispatch falls back to an arbitrary carrier-compatible one, which is not necessarily the instance that will run, and whose predicate is weaker than the class's -- checking against it would demand less than the contract while appearing to check the contract.
An instance may accept more than its class signature promises, but not less. The class signature is the contract callers program against, so an instance that demanded more would reject an argument a generic caller was entitled to pass:
(defclass Scaler [a]
(scale-by [self : a, k : #refine{ v : int | (>= v 0) }] : int))
;; error[TUR-E0374]: rejects 0, which the class admits
(definstance Scaler [int]
(scale-by [self : int, k : #refine{ v : int | (> v 0) }] : int ...))
;; fine: accepts everything the class promises, and more
(definstance Scaler [int]
(scale-by [self : int, k : int] : int ...))
A class parameter with no refinement promises nothing, so any instance
refinement on it is a strengthening. The check is class_pred |- instance_pred
through the same solver seam, and it reports only on a refutation -- an
undecidable pair keeps the runtime check.
A method's result refinement varies the other way. A parameter refinement is something the method demands, so an instance may demand less. A result refinement is something it delivers, so an instance must deliver at least as much -- a caller programming against the class signature is relying on it:
(defclass Boxed [a]
(unbox [self : a, k : int] : #refine{ r : int | (>= r 0) }))
;; fine: no annotation, so the class's promise is INHERITED and checked
(definstance Boxed [int]
(unbox [self : int, k : int] : int (* self k)))
;; fine: delivers more than the class promises
(definstance Boxed [int]
(unbox [self : int, k : int] : #refine{ r : int | (> r 0) } ...))
;; error[TUR-E0374]: promises less about its result than the class does
(definstance Boxed [int]
(unbox [self : int, k : int] : #refine{ r : int | (>= r -5) } ...))
| position | obligation | an instance may... |
|---|---|---|
| parameter | class_pred(p) \|- instance_pred(p) |
demand LESS (accept more) |
| result | instance_pred(r) \|- class_pred(r) |
deliver MORE (promise more) |
Because of that rule, the class's result refinement propagates to callers even though which instance runs is unknown at the site -- it is the one promise true of every instance:
(defn use-it [] : #refine{ r : int | (>= r 0) }
(.unbox 3 4)) ; proved from the CLASS promise alone
An instance that restates a predicate the solver cannot prove implies the
class's gets the class predicate checked alongside its own ("Class result
contract violated"). That is what keeps the propagation honest: the variance
check reports only on a refutation, so an undecidable pair emits no error,
and without the extra check a caller would be relying on a promise nothing
enforced. Under --no-contracts nothing enforces it either, so nothing is
propagated.
What is not checked is a genuinely higher-order callee -- see Limits.
A call is an opaque term to the solver -- but when the callee has a return refinement, that predicate is a fact about the value this call produced, so it is asserted. Refined code composes instead of going opaque at every call:
(defn double-pos [x : Pos] : #refine{ r : int | (> r 0) }
(* x 2))
(defn twice [y : Pos] : int (* y 2))
(defn use [p : Pos] : int
(twice (double-pos p))) ; proved -- double-pos's result is known positive
Assuming a declared return refinement is sound because something enforces it:
either it was proved statically, or the runtime check that guarantees it would
have panicked first. When neither is true -- contracts stripped by
--no-contracts, or a release build without --keep-contracts -- the
refinement is not published, and call sites go back to treating the result
as opaque.
A function with no declared return refinement gets one tried for it. RT4 runs a
small fixed vocabulary of shapes (> 0, < 0, >= 0, <= 0, != 0) against
the body and keeps the first a backend proves:
(defn inc-pos [x : Pos] : int
(+ x 1)) ; (> result 0) is inferred and published to call sites
(defn use [p : Pos] : int
(twice (inc-pos p))) ; proved, with no annotation on inc-pos at all
This is not general refinement inference. There is no search over a recursive constraint system -- the layer that makes a LiquidHaskell-style backend hard is exactly the one Turmeric deletes by making refinements written. These are a handful of guesses, each individually checked, so a wrong guess costs a discarded proof attempt and never a wrong answer. A function whose result genuinely satisfies none of them simply gets nothing:
(defn lower [x : Pos] : int (- x 5)) ; can be negative -- nothing inferred
(defn use [p : Pos] : int (twice (lower p))) ; correctly stays undischarged
Scope, deliberately narrow: single-expression bodies, a numeric result, at most
four refined parameters. Branching bodies need a path-sensitive join at the
merge point and are deferred. TUR_REFINE_STATS=1 reports how many
refinements were inferred and how many probes it took.
The supported fragment is quantifier-free linear integer/real arithmetic with equality and uninterpreted functions.
pred ::= (= e e) | (not= e e) | (< e e) | (<= e e) | (> e e) | (>= e e)
| (and pred ...) | (or pred ...) | (not pred) | (=> pred pred)
| (measure e ...) ; a bool-returning measure IS a proposition
| true | false
expr ::= <int literal> | <float literal>
| <the refinement's bound variable>
| <any parameter in scope>
| (+ e e) | (- e e) | (* e <literal>) | (/ e <literal>) | (mod e <literal>)
| (measure e ...) ; an int/float-returning measure is a value
(/ e <literal>) and (mod e <literal>) on integers mean what the runtime
means: C's truncating / and % (the remainder takes the dividend's sign, so
(mod -7 2) is -1). The solver knows this -- it asserts e = k*q + r and
the sign clause for the two terms -- so (= (mod n 2) 0) on a parameter proves
(= (mod (+ n 2) 2) 0) on the result, and (>= n 0) proves
(<= 0 (/ n 2) n). Division or remainder by a variable is nonlinear and
falls under the rule below.
Two rules keep this on the cheap side of the solver cliff. They are rules, not accidents:
Named measures are uninterpreted functions. Any call the encoder does not
recognise -- len, size-of, your own helper -- becomes an opaque symbol that
congruence closure reasons about but never unfolds. This is what makes the
equality theory tractable, and it is why (= (size-of v) n) above is usable as
a hypothesis without the solver knowing anything about size-of's body.
A measure is sorted by its return type. :bool denotes a proposition,
:float a real, everything else an integer -- so the domain predicate you
actually want to write works as written:
(defn alive? [w : int e : int] #fx{} : bool (= w e))
(defn use-it [w : int e : #refine{ x : int | (alive? w x) }] : int e)
(defn guarded [w : int e : int] : int
(if (alive? w e) (use-it w e) 0)) ; the guard IS the proof
The sorting matters for soundness as well as expressiveness: a bool measure
mis-sorted as an integer would push people to the (= (alive-i w x) 1)
spelling -- the exact :int stand-in CLAUDE.md forbids -- and a float
measure mis-sorted as an integer would let integer tightening (e < 4 implies
e <= 3, valid only over the integers) prove an unprovable goal and elide a
check that should fire. refine-bool-measure, refine-float-measure, and
errors/refine-float-measure-not-tightened pin all three behaviors.
An abstract measure -- a name that resolves to no function at all -- has no
return type to read, so its position decides: a proposition where the grammar
requires one (the goal itself, or an operand of and/or/not/=>), a value
everywhere else. Equality is neutral, since (= (alive? w x) (alive? w y)) is
as legitimate as (= (len v) n). A name genuinely used at both sorts in one
verification condition is rejected outright rather than resolved by guessing:
one symbol meaning two things across a hypothesis and a goal is how a
congruence bug is written. The obligation falls to unknown with a stated
reason, and the runtime check is kept -- see
errors/refine-measure-sort-conflict.
A measure must be provably pure. Congruence -- treating two occurrences of
(size-of v) as the same value -- is only valid for a pure function, and the
compiler decides that for itself by walking the callee's body:
(defn size-of [v : int] #fx{} : int
(* v 2))
The walk is default-deny. It admits literals, reads of immutable bindings,
if / let / do / return / match, the arithmetic, comparison, and
logical builtins, and direct calls to functions that are themselves pure --
recursion included, so a recursive measure still gets congruence. Everything
else is impure: inline C, set!, perform, dereferences, field reads,
closures, indirect and rank-2-polymorphic calls, the println family, raw
memory, and any callee whose body is not available (an extern, or a forward
reference not yet elaborated). Without a purity proof each occurrence is a
distinct opaque value and the proof does not go through.
match is classified from its scrutinee and every arm, joined -- nothing
about dispatching on a constructor is observable, but one impure arm makes the
whole form impure. Pattern binders are not walked: they are introduced by the
pattern rather than evaluated, so an arm that merely reads one stays pure. Both
halves are pinned -- refine-match-pure-congruent (a match-bodied measure is
congruent) and refine-match-impure-arm (one arm calling a counter is not, and
the surviving check fires).
A declared effect row is a veto, not evidence. #fx{Log} rules purity out;
#fx{} on its own proves nothing, because the effect system tracks algebraic
effects and infers nothing from set!, from a mutable global, or from inline C.
A function can declare an empty row, keep a static counter in a C block, and
return a different value on every call.
The veto is applied where a function's verdict is memoized, so it holds on
every later lookup and it is transitive: a function with no row of its own
whose body calls a #fx{Bt} (or #fx{Log}, or #fx{e}) function is not
proven pure either. It used to be applied once at the top of the walk and
skipped on a memo hit, and the memo was usually written first by another
walk -- so in practice it never fired. sx1-bt-row-checked pins both the
direct and the transitive case, with an unannotated control that still proves.
The rule has teeth. Given a tick that counts up, (- (tick) (tick)) is -1,
never 0 -- but encoded congruently it becomes t - t, and a refinement of
(>= r 0) on it would be "proved" and its check elided. Three separate routes
into that hole are pinned by regression fixtures:
errors/refine-impure-not-congruent (an unannotated callee),
errors/refine-impure-fx-empty (a callee declaring #fx{} and lying), and
refine-typeclass-not-congruent (a typeclass method, which has no global
binding under its bare name and so used to be mistaken for an abstract
measure).
A typeclass method is never congruent. Which instance runs is not known at
the encoder, and an instance body can do anything, so both (m x) and (.m x)
get a distinct symbol per occurrence.
A field read is as pure as its receiver, so an ordinary getter is congruent:
(defn width-of [b : Box] : int (.width b))
There is no per-field mut marker in the language and none is needed: set!
on (.f s) requires s to be bound ^mut, which is the same
declaration-level guarantee that already makes a non-mut variable read
congruent. Computing the receiver still counts -- (.w (next-box)) is as
impure as next-box.
Behind a reference it is declined. With rc<T>, ref<T>, or a borrow, a
caller can hold a ^mut handle to the very object the callee reads through a
non-mut one and mutate it between two calls -- exactly the aliasing congruence
assumes away. A by-value receiver has no second handle to mutate through
(:copy copies; a moved value leaves the caller nothing), so the vector closes
by construction. rc getters lose precision and nothing else.
Note this does not make width-of and .width interchangeable: they are two
different uninterpreted symbols and nothing unfolds one into the other.
Congruence is about repeated occurrences of the same call.
A measure about mutable state can be made congruent within a scope where the
state cannot change -- a predicate like alive? or open? that reads a
generation counter or a socket flag is impure and gets a fresh symbol per
occurrence by the rule above, but a frozen region plus a #reads annotation
recovers congruence for it, soundly, without eliding the callee's kept entry
check. That is a separate feature (implemented, experimental); see the
Stateful Refinements guide.
match arm knowsA body that is a match is proved one arm at a time, and each arm is proved
under everything its own selection implies. There are four sources:
| Pattern | Hypothesis |
|---|---|
(Circle r) |
(= (#dt/tag s) 2) -- the constructor's discriminant |
(Circle r), record field radius |
(= r (.radius s)) |
0 |
(= s 0) |
(Pos n) when (> n 100) |
(> n 100) |
That is enough for a getter to state a postcondition about the field it returns:
(defdata Box (Box [width : int height : int]))
(defn get-width [b : Box] : #refine{ v : int | (= v (.width b)) }
(match b (Box w h) w))
w and (.width b) are the same value, and saying so is what discharges the
obligation. Without the field hypothesis w is an unconstrained name and
(.width b) an unrelated opaque term.
There is still no datatype sort in the VC. These are ordinary equations
over uninterpreted functions, which congruence closure already decides -- the
theory is synthesized before encoding rather than built into the solver.
#dt/tag is a total function from the datatype to its discriminant; a field
selector is total too, taking some unspecified value off its own constructor,
which is exactly what an uninterpreted symbol does. Both facts are true on the
path being proved.
The one thing the tag buys on its own is dead arms. Two matches on the same scrutinee pin its tag to two different constructors, and the inner arm's hypotheses become contradictory -- so its obligation holds vacuously, which is correct, because that arm cannot run:
(match s
(Pos n) (match s
(Pos m) 0
(Neg m) -1) ; unreachable, and proved so
(Neg n) 0)
A guard is a necessary condition for its arm, never a sufficient one -- an arm also requires every earlier arm to have failed, which is not asserted, so a guarded arm knows its guard and nothing about the arms above it.
The arm hypotheses run one way: a binder is the field it destructures. The defining equation of the constructor runs the other way, and is asserted once per constructor application in the obligation:
(= (.width (Box p 3)) p)
It is universally true, not path-dependent. Together with the arm's field hypothesis it closes the build-then-destructure round trip:
(defn roundtrip [p : int] : #refine{ r : int | (= r p) }
(let [b (Box p 3)]
(match b (Box w h) w)))
b = Box(p,3) from the let split, w = (.width b) from the arm, and
(.width (Box p 3)) = p from the axiom -- congruence closure does the rest.
Only record constructors take part: a positional variant ((Just :int))
has no field name, so there is no accessor to write the equation about.
A data constructor is treated as pure. It stores its arguments and runs no
user code, so two applications to equal arguments hold equal fields, which is
the only thing the VC ever asks -- a constructor term is reached only through a
selector. Object identity does differ between two applications, but nothing in
the predicate language can observe it. This says nothing about the constructor's
arguments: (Box (tick) 3) twice is two different terms, exactly as (tick)
twice is.
Arm hypotheses are scoped to their own arm. Sibling arms assert different tags
for the same scrutinee, so letting them accumulate would be a contradiction that
proves everything -- refine-match-arm-hyps-not-shared pins that.
The asymmetry is the point: a case the walk has not learned costs one runtime check, while a wrong purity guess elides a check that was protecting something. A name that resolves to no function at all is still treated as congruent: that is an abstract measure, a mathematical function by definition rather than code that runs.
Variable * variable is uninterpreted. (* x 2) is linear and fully decided;
(* x y) with both sides variable is abstracted to an opaque term and reported
with TUR-W0373. Congruence closure still relates two occurrences of the same
product, so nothing becomes unsound -- the arithmetic facts are simply gone, and
the obligation falls back to its runtime check.
;; TUR-W0373 + TUR-W0372: true, but not provable in the linear fragment.
(defn mul-pos [x : #refine{ v : int | (> v 0) }, y : #refine{ w : int | (> w 0) }]
: #refine{ r : int | (> r 0) }
(* x y))
We do not climb the nonlinear wall. A genuinely nonlinear obligation gets a runtime check, and that is the intended outcome.
stdlib/refine.turA deftype bound to a contract type is a refinement alias:
(deftype Pos #refine{ x : int | (> x 0) })
A parameter declared [n : Pos] takes the base int at the C level, gets
(> n 0) as its entry check, and contributes that predicate as a hypothesis.
stdlib/refine.tur ships the common ones. It is load-on-demand, not
auto-loaded -- names like Byte and Percent do not belong in every program's
type namespace by default:
(load "stdlib/refine.tur")
(defn safe-div [n : int, d : NonZero] : int
(/ n d))
| Alias | Predicate |
|---|---|
Nat |
(>= x 0) |
Pos |
(> x 0) |
Neg |
(< x 0) |
NonZero |
(not= x 0) |
Byte |
0 <= x <= 255 |
Percent |
0 <= x <= 100 |
NonNegFloat |
(>= x 0.0) |
PosFloat |
(> x 0.0) |
UnitFloat |
0.0 <= x <= 1.0 |
A refinement alias takes no type parameters in this prototype.
There is no heavyweight solver dependency. The shipped compiler carries a small, staged, in-house decision procedure, which is also why the WASM playground gets static checking at zero download cost. Obligations run through the stages in ascending cost and stop at the first one that decides:
| Stage | What it decides |
|---|---|
| S0 trivial | constant goals, a goal that is syntactically a hypothesis, contradictory hypotheses |
| S1 congruence closure (EUF) | equality and uninterpreted functions -- measures, abstracted products |
| S2 linear arithmetic | conjunctions of linear constraints, by Fourier-Motzkin elimination over exact rationals (a superset of difference logic) |
| S3 Nelson-Oppen | mixed goals, by exchanging entailed equalities between S1 and S2 |
Anything none of them decides is answered unknown and keeps its runtime check. Every internal cap -- cube count, variable count, constraint growth -- degrades the same way. The invariant the whole design rests on is one-directional: a stage may never say "valid" for something that is not, but saying "unknown" is always allowed.
Integers get one extra step: a strict constraint over integral data is tightened
(e < 0 becomes e <= -1), which is what lets x > 0 entail 2x > 0. Full
integer completeness (branch-and-bound, Omega) is deliberately not attempted.
Failing to prove something proves nothing, so a separate bounded search tries to refute the obligation: it enumerates a small candidate assignment space and evaluates the formula exactly. A satisfying assignment is a real counterexample, so it is reported with a model rather than a shrug.
The search declines VCs containing uninterpreted symbols -- a measure has no fixed interpretation to evaluate, so guessing one would be dishonest.
A failing obligation reports in three parts: the claim, a witness against it, and a remedy.
error[TUR-E0371]: refinement on the return value of 'wrong' cannot be proved statically
note: the predicate (> r 0) does not hold for every input here
note: counterexample: x = -2
help: (> x 0) would discharge it -- e.g. declare x : #refine{ v : int | (> v 0) }
The help: line is not a heuristic. It is a second query through the same
solver seam: a candidate fact is asserted as a hypothesis and the chain is
asked again, so only a candidate that genuinely discharges the goal is ever
offered. Two families are tried -- comparisons against the literals the code
already mentions, and relations between two variables, which is what produces
an index bound:
error[TUR-E0371]: refinement on the return value of 'at' cannot be proved statically
note: the predicate (< r n) does not hold for every input here
help: (< i n) would discharge it -- e.g. declare i : #refine{ v : int | (< v n) }
A candidate that contradicts what is already known is rejected explicitly.
Without that check a contradictory hypothesis would discharge the goal by ex
falso, and the compiler would cheerfully suggest constraining a variable to be
both negative and positive. When nothing consistent helps, no help: line is
printed -- silence beats a nonsense suggestion:
(defn impossible [x : #refine{ v : int | (< v 0) }] : #refine{ r : int | (> r 0) }
x) ; reported, with a counterexample, and NO hint -- none exists
When the values are written right at the site, the wording sharpens accordingly -- this is not "not for every input", it is "not for this one":
error[TUR-E0371]: refinement on argument 2 of 'safe-div' in 'main' cannot be proved statically
note: the predicate (not= x 0) is false for the value given here
| Code | Meaning |
|---|---|
TUR-E0371 |
the predicate genuinely does not hold: a function's own claim is falsifiable, or an argument is definitely wrong at its call site |
TUR-W0372 |
nothing decided it; the runtime check is kept |
TUR-W0373 |
a nonlinear subterm was abstracted; arithmetic reasoning is incomplete for it |
TUR-E0310 / TUR-E0330 |
a lingering --enable=refined / #lang turmeric refined; both shims retired in 0.38.0, so both are errors -- delete them |
TUR-E0371 and TUR-W0372 both leave the program safe -- the runtime check
survives in each case. Under --strict-refine both become hard errors.
Run tur explain TUR-W0372 (or any of the codes above) for the long form.
Two environment variables:
TUR_REFINE_STATS=1 tur build main.tur
# refine: 3 obligation(s): 2 proven, 0 refuted, 1 unknown (7 backend call(s))
TUR_REFINE_DUMP=1 tur emit-c main.tur
# --- refinement VC (return value of double-pos) ---
# (set-logic QF_UFLIA)
# (declare-const x Int)
# (assert (< 0 x))
# (assert (not (< 0 (* x 2))))
# (check-sat)
The dump is SMT-LIB2 with the goal negated -- unsat means valid. It is
there to read, and to paste into an external solver when you want a second
opinion; the in-house stages consume the internal representation directly and
never go through this text.
An obligation reported unknown may never have reached a backend at all: if it
escapes the supported fragment there is no VC to dump, and TUR_REFINE_DUMP=1
prints nothing for it. Stats names those separately, with the reason:
TUR_REFINE_STATS=1 tur check main.tur
# refine: not encoded (measure 'ready' is used both as a proposition and as a
# value): the return value of 'f'
This is a note about the fragment, not a defect in your program -- which is
why it rides on the stats switch rather than being a diagnostic. A call-site
crossing is runtime_guarded, so it does not warn by default at all; without
this line an obligation the encoder dropped and one the solver could not decide
were indistinguishable.
A development build used to be able to link a system Z3 as a correctness
oracle, decide every obligation twice, and report disagreements as
TUR-I0379. That scaffold has been retired: there is no
TUR_REFINE_Z3_ORACLE option, no tur_refine_fuzz binary, and no way to link
a solver into tur. The compiler ships no solver dependency and now has no
build mode that adds one.
The standing replacement is tests/corpus/smtlib/, replayed by the
tur_refine_corpus ctest target: 125 benchmarks whose sat/unsat labels
live in the repo as data, checked against the in-house chain in every build
rather than only on a machine with Z3 installed.
One caveat the oracle era taught, still worth knowing when you check a VC
against an external solver by hand (dump it with TUR_REFINE_DUMP=1): sat is
only a counterexample when the VC contains no uninterpreted symbols.
Abstraction is sound in one direction only -- unsat of an abstracted VC
implies the concrete obligation holds, but a model that assigns an opaque
symbol a convenient value proves nothing about the function it stands for.
The deleted tur_refine_fuzz started at the VC, below the encoder, so it was
blind to every bug in the translation from Turmeric source into a VC. Both
soundness bugs found in this work lived exactly there, and neither was visible
to it -- which is why its deletion costs less coverage than it appears to.
tests/refine-fuzz-src.py starts at the top instead. It generates whole
programs, compiles and runs each one with the gate off and on, and compares
what actually happened. It never needed Z3 -- the oracle is the gate-off build
-- so it is unaffected by the retirement and is now the primary fuzzer.
python3 tests/refine-fuzz-src.py --n 500 --seed 3
bash tests/run-refine-fuzz-src.sh # smoke size, also ctest
The property it enforces is the one the design turns on:
gate-off aborted on a contract => gate-on must not run to completion
plus identical stdout when both run clean, and no gate-on abort on a program
that ran clean without it. A fourth bucket, SUSPICIOUS_over_refute, collects
programs that run clean with the gate off and are rejected with it on. Those
are usually correct: TUR-E0371 is universally quantified over a function's
inputs while the program only exercises the arguments it happens to pass, so a
sound refutation and a clean run coexist routinely. They are counted and saved
for triage, never failed on.
The solver ships in the WebAssembly build, so it is checked there too --
bash tests/run-refine-wasm.sh (ctest tur_refine_wasm) compiles every
refine source under Emscripten with the real tur_wasm flags AND runs the
solver's unit checks at 32-bit pointers under node. Compiling is the easy
half; agreeing is the point, since S2 is Fourier-Motzkin over exact rationals
with overflow guards and the hash-cons table keys off integer widths. The
script skips cleanly when emcc is not installed.
A fuzzer nobody has watched fail is a fuzzer that reports zero because it is
broken. Two one-line sabotages are documented in the script header, both
verified to be caught where the shipped build reports zero: breaking purity
(rt_binding_is_pure returns true), and eliding a callee's entry checks
under the gate (rt_inject_param_checks returns early). The second simulates
the whole-program entry-check elision feature built unsoundly, so rerunning it
is the acceptance test for that work when it lands.
Known and deliberate, in rough order of how likely you are to hit them.
Each carries a tag, because "not checked" covers two very different promises and the difference matters if you are deciding whether to depend on this:
| tag | meaning |
|---|---|
| [by design] | A deliberate decision, usually forced by soundness or by the adoption philosophy. It will not change. |
| [prototype] | Needs a design change this prototype excludes -- refinements in function types, refinements on type parameters. Not planned. |
| [incomplete] | Could be improved, nobody is working on it. Safe: the effect is always an obligation that falls back to its runtime check, never a wrong answer. |
| [deferred] | Has a written plan and a trigger condition; waiting on demand rather than on effort. |
Nothing here is a bug being worked around. Every one of them lands on the safe side of the one-directional invariant: the worst outcome is an obligation the solver declines to prove, which keeps the runtime check it would have had anyway.
(defn apply2 [f : (fn [int int] int) ...]) cannot know what f's arguments
must satisfy, and neither can a call to apply2. Passing a refined function
as a value is legal and the callee's own entry checks still run, so nothing
unsound follows -- only the static crossing is lost.What does work, and is worth knowing before assuming the limit is wider
than it is: a direct call, a call through an alias of a global
((let [g safe-div] (g 10 0))), and a call to a lambda with refined
parameters are all checked. The gap is specifically a value reached through a
function-typed parameter.
Writing the refinement into the function type is rejected with
TUR-E0378, not ignored:
turmeric
(defn apply2 [f : (fn [int #refine{ v : int | (not= v 0) }] int)] : int ...)
;; error[TUR-E0378]: a refinement cannot be written on the parameter of a
;; (fn ...) type; function types do not carry refinements
Closing the gap needs refinements to be part of function types, with the
contravariant subtyping check that implies -- a type-system change the
prototype excludes.
- [by design] An instance that explicitly demands less is checked against its own,
weaker, predicate at a statically-resolved site -- deliberately, since it
is the implementation that will actually run. TUR-W0377 marks a call that
depends on that leniency; it is a warning rather than an error because the
call is correct for the instance it resolved to. See
docs/archive/class-param-refinement-not-demanded-of-callers.md
for why that reading was chosen over making the class signature binding on
callers.
- [by design] An argument that cannot be PROVED is not an error; one that is DEFINITELY
wrong is. A crossing reports TUR-E0371 when the goal mentions no variable
and evaluates false -- (safe-div 10 0) -- because every execution reaching
that call violates it. When the goal depends on a variable, the
counterexample only says "not for every input", the runtime check is kept,
and nothing is reported unless --strict-refine is on. Note that closedness
is a property of the goal, not of what is in scope: an unrelated parameter on
the caller does not make a literal violation unreportable.
if, let, match, and do. A branching body is
discharged per path -- c |- pred[then/r] and (not c) |- pred[else/r] --
and a let contributes x = v. A let whose binding shadows a name in
scope is alpha-renamed first, so the hypothesis relates a fresh name rather
than asserting the contradiction x = x - 1. A body that rebinds the same
name a second time declines to split. A branching let VALUE splits too, so
(let [m (if c a b)] ...) gives the body m = a on one path and m = b on
the other rather than an unconstrained m. A match arm contributes
everything its selection implies (see below); a pattern binder that shadows a
name in scope declines the split. A do block is proved through its last
form, but only when the preceding statements contain no assignment -- an
assignment can stale a hypothesis about a parameter, and carrying that
hypothesis across it would prove a function that violates its own refinement.if, let, and match.
A crossing is resolved after the whole unit, which is what lets it see every
callee's refinement; the branches that had to be taken to reach it are
recovered from the caller's body, so (if (= n 0) 0 (+ 1 (f (- n 1))))
discharges its recursive crossing from n >= 0 and n != 0 together. A
let contributes x = v, a match arm contributes a literal pattern's
equation and its guard. The caller's whole body is searched, so a call in
any body form keeps its guards -- not only one in the form the function
returns.Four things are deliberately left out, and all four cost a diagnostic rather
than soundness -- the callee's own entry check always remains:
a caller whose body assigns anywhere (a condition naming a reassigned
variable may no longer hold at the call); a constructor tag or field
selector, since those arrive with pattern binders; a let that binds a
function, which is not an arithmetic fact; and a call reachable by more
than one route, which a macro sharing a node can produce.
- [by design] A crossing under a shadowing binder is abandoned, not answered. The
encoder has one flat namespace, so an argument naming a shadowed variable
would inherit the outer one's hypotheses -- (let [x (- x x)] (sdiv 10 x))
under x > 0 once "proved" x != 0 of a value that is zero. Dropping the
binding's equation is not enough, because the collision is in the name rather
than the fact, so the whole crossing is skipped.
- [deferred] A while loop is not analysed. An accumulator built by a loop is
Unknown regardless of what the loop does. There is no invariant inference
and none is planned -- inferring facts is the thing this design deliberately
does not do. A user-written :invariant, which would be checking rather than
inference, is a plausible future addition but is not in the prototype; see
docs/upcoming/hold/loop-invariants-plan.md.
- [prototype] No refinements on type parameters or higher-order predicates. These are
rejected or fall through to runtime. (Typeclass method signatures are
supported now, on parameters and results alike -- see above.)
- [by design] Nonlinear arithmetic is uninterpreted, as described above.
One sign fact survives the abstraction: a square (* x x) is known to be
non-negative, so (>= (* x x) 0) and (> (+ (* x x) 1) 0) prove while
(* x y) with distinct operands stays Unknown (TUR-W0373 is emitted
either way).
- [incomplete] Integer reasoning is exact for equations, not yet for every
inequality system. 2v = 2x + 1 is recognised as having no integer
solution (parity), an equation is substituted away through any variable
with coefficient +/-1, and each inequality is rounded to its integer hull
(2q >= -1 gives q >= 0). What is left is the long tail: an all-integer
equation with no unit coefficient (2x + 3y = 1) is read as two
inequalities, and a system that is only infeasible over the integers
without any equation in it is not decided. Both fall to Unknown and keep
the runtime check; see docs/upcoming/solver-integer-tail-plan.md.
- [incomplete] A counterexample needs the search to fit its budget. The
bounded search that produces TUR-E0371's witness enumerates at most
131072 assignments over at most 8 integer variables; an obligation wider
than that is reported Unknown even when it is plainly false. (The limit was
three variables until 2026-09-05, which made every four-parameter function
with a refined return unrefutable.) TUR_REFINE_STATS=1 reports both
declines.
- [by design] A predicate that calls an effectful function is rejected (TUR-E0375),
in all four positions: a refined parameter, :pre, :post, and a refined
return. Whether a check runs depends on the build -- --no-contracts strips
them, a release build drops them, and this feature elides the ones it can
prove -- so an effectful predicate makes behaviour depend on whether its own
contracts were compiled in. Reported only on PROVEN impurity: a predicate
calling a function whose body the purity walk does not model (a field read, a
loop) is left alone, since a wrong "impure" would reject working code.
- [by design] Decisions are memoized within a compilation unit, keyed by a fingerprint
of the normalized VC under alpha-renaming, and every hit is confirmed by
structural comparison before its verdict is reused. Repeating the same
refinement across many functions therefore costs one decision, not many.
There is no cross-build cache.
- [by design] Purity is a syntactic whitelist, not an analysis. A function whose body
steps outside the admitted forms is impure even when it is in fact pure --
a struct field read through a computed receiver or a loop is enough.
Its calls are then not congruent and measure-style reasoning over it
does not go through. This costs completeness, never soundness. Widening the
whitelist further is the natural next increment; the effect row cannot
substitute for it, because an empty row is not a purity claim.
- [by design] The #reads stateful slice is TRUSTED, not checked -- and its region
guarantee is against ordinary code, not adversarial code. #reads w is a
declared promise the purity walk cannot verify for an inline-C body, and a
frozen region's mutator lockout can be stepped around by a deliberate
::-cast or inline C (an opaque carrier handle is reconstructable). The
worst adversarial outcome is the forgiving default semantics -- a stale read
of in-bounds memory -- wearing a proven badge; never an elided check (an
impure measure's entry contract is unemittable, so compile-time rejection
was always the enforcement, and that remains intact for ordinary code). A
hard guarantee needs module-private construction / a ::-sealed newtype --
an independent language feature, tracked in
docs/archive/frozen-region-aliasing-via-coercing-cast.md. Trust is no
longer the whole story, though: a demonstrably broken frame (a direct read
of a mutable global, or of state rooted in a parameter the frame omits)
draws TUR-W0383 and is refused the congruence grant, and
--dump-read-frames shows the verification walk's verdict, which can stamp
a walkable body's frame VERIFIED. An inline-C body stays exactly as trusted
as this entry describes. See the
Stateful Refinements guide.
Every one of these fails toward a runtime check, never toward a wrong answer
-- except the trusted #reads slice, which fails toward the forgiving
default semantics under a deliberately false declaration, as its entry above
states.
--enable= mechanism#lang layers