Your verifier is probably lying to you about floats


Here is a promise that looks obviously true:



fn add_twice(x: Float) -> Float
    ensures result == x + 0.2
{
    return x + 0.1 + 0.1
}

Adding a tenth twice is the same as adding two tenths. Every algebra

teacher you have ever had agrees. Most program verifiers agree too.


They are wrong, and so is the promise.


Velaris refuses to prove it, and hands back the number that breaks it:



error[E700] promise cannot be kept: 'add_twice' ensures result == x + 0.2
  proven without running the program:
    x = -1.207290298954004637010939404717646539211273193359375
    gives result = -1.007290298954004459375255464692600071430206298828125

That is not a rounding display artifact. Those are two different

doubles, and the program really does produce the second one.


Why the lie is tempting


To prove things about numbers, a verifier translates your code into

formulas for a solver. The question is which *theory* to translate

into.


The comfortable choice is the theory of real numbers. Reals are

associative, commutative, infinitely precise, and solvers are fast at

them. Translate Float to Real and everything works beautifully:

proofs come back in milliseconds, and x + 0.1 + 0.1 == x + 0.2 is

trivially true.


It is also a statement about a machine that does not exist.


Your processor implements IEEE-754 binary64. In that world 0.1 is not

one tenth — it is the nearest double to one tenth, which is

0.1000000000000000055511151231257827021181583404541015625. Every

operation rounds to the nearest representable value. Addition is not

associative. Adding a tenth twice takes two rounding steps; adding two

tenths takes one. For many values of x those disagree in the last

bit, and one bit is all it takes for == to be false.


So a verifier that models floats as reals will happily certify code

that fails on the machine it is compiled for. The proof is valid. The

theorem is about the wrong object.


What honesty costs


Z3 has a floating-point theory that implements IEEE-754 exactly:

rounding modes, subnormals, infinities, NaN, signed zero. Velaris

translates Float into that theory rather than into Real.


The bill arrives immediately.


It is slow. The FP theory is decided by bit-blasting — expanding

64-bit values into circuits of individual bits and handing the result

to a SAT solver. The refutation above takes about fifteen seconds.

Integer proofs in the same compiler finish in milliseconds. Velaris

gives float queries a thirty second budget and everything else three

seconds, and only pays the larger cost for functions that actually

mention floats.


Fewer things are provable. Plenty of true-in-the-reals facts are

simply false in IEEE-754, and plenty of true-in-IEEE facts are too

expensive to establish. A verifier that pretends floats are reals has a

much better success rate on paper. It is winning a game nobody should

want to play.


Equality gets strange, correctly. Velaris compares floats with

fpEQ, not structural equality, which means NaN is not equal to itself

and positive zero equals negative zero. Both are IEEE behaviour, and

both surprise people. Using structural equality would have been faster

and easier to explain, and would have quietly produced false results at

the edges.


There is a bonus that only shows up once you are honest: comparison

constraints start doing real work. A precondition like `requires

x >= 0.0` silently rules out NaN, because NaN fails every comparison.

The prover knows that, so a promise you could not otherwise establish

sometimes becomes provable for free.


The rule underneath


Velaris has one commitment it will not trade away: **it never claims

something is proven unless the claim is literally true.**


That single rule decided the float design by itself. If you model

floats as reals, "proven" starts meaning "proven about an idealised

machine that does not exist," and the word has quietly been devalued.

Everything after that is negotiation.


The same rule shows up elsewhere in the compiler. Division and modulo

are not native-compiled, because a native fdiv by zero yields

infinity while the language promises a clean error, and two execution

modes that disagree are worse than one slow mode. A premise the solver

cannot translate aborts the whole proof rather than being silently

dropped, because proving with dropped premises manufactures false

counterexamples. Anything unprovable degrades to a runtime check rather

than being waved through.


None of these make the demo look better. All of them are the reason the

demo can be believed.


Try it


The compiler runs in your browser, no install:

<https://gowrishankar-infra.github.io/velaris-lang/playground.html>


Paste the function at the top of this page and watch it refuse. Then

change ensures result == x + 0.2 to something IEEE actually

guarantees — say ensures result >= x — and watch it go through.


If you can make Velaris say "proven" about something that is false at

runtime, that is a soundness bug, and this project treats those as

security reports.


<https://github.com/gowrishankar-infra/velaris-lang>