Float refinements: NaN, infinities and the other IEEE hazards in the types
Status: feasibility analysis and proposal for review, 2026-10-30. The user:
“it’d be great if you could achieve the same thing for float/double re NaN,
dividing by 0 (other cursed things), +Inf, -Inf. Not sure how feasible it
is”. Companions: design/no-faults.md, design/refinement-types.md,
design/no-faults-evidence-map.md, specifications/primitive-types-specification.md
section 5. No em dashes.
1 Where Cinch already stands
Floats never fault, and the specification already refuses the classic
traps (primitive-types-specification.md sections 5 and 7):
F32/F64have noEqand noOrdinstance, because IEEE equality is not an equivalence relation. Comparisons are the named IEEE predicates (feq,flt, …) ortotalOrder.- Float-to-integer conversion is only
checkedToT(Noneon NaN and out of range) orsaturatingToT. The codegen selects around LLVM’s poisonousfptosi/fptouiresults (codegen.rs~4070), so no poison escapes. - No transformation may reassociate, contract or re-round float code.
So the problem is not crashes. NaN, the infinities and negative zero flow
through programs silently as values, and a type cannot say “this is a real
number”. The goal is to let types say it and have the compiler prove it,
with the same machinery as the integer work (refinements over a type
parameter, D413; decide; facts from case rows, D412 and its rework).
2 The hazards, exactly (IEEE 754 binary64, round to nearest)
| Operation | NaN when | Infinite when (operands not NaN) |
|---|---|---|
x + y |
an operand is NaN, or {+Inf, -Inf} |
an operand is infinite, or overflow |
x - y |
an operand is NaN, or both infinite with the same sign | as above |
x * y |
an operand is NaN, or zero times infinity | an operand is infinite, or overflow |
x / y |
an operand is NaN, 0 / 0, or Inf / Inf |
y is zero and x is not, x is infinite, or overflow |
sqrt x |
x is NaN or x < 0 (sqrt (-0) = -0) |
x = +Inf |
rem x y, fmod |
an operand is NaN, x infinite, or y zero |
never |
log x |
x is NaN or x < 0 |
x = +-0 gives -Inf; x = +Inf |
pow x y |
negative finite x with non-integer finite y, NaN operands except pow x 0 = 1 and pow 1 y = 1 |
pow (+-0) y for y < 0, overflow |
Other hazards:
- Negative zero.
-0 == +0under IEEE equality, yet they are distinguishable:1 / -0 = -Inf,signBit,totalOrder. Hashing must not separate values equality identifies. - Negated comparisons.
not (x < y)does not implyx >= y: NaN makes both false. Facts from float comparisons must use IEEE-correct negation. - Non-closure. Even when both operands are finite,
+,-,*and/can overflow to an infinity.Finiteis not closed under arithmetic without magnitude bounds; “not NaN” is not closed under+either (Inf + -Inf). - Rounding.
(a + b) + canda + (b + c)differ;x + 1 == xfor largex. Already handled: no reassociation without a contract. - Platform differences. Transcendental functions (
sin,exp,log) are not correctly rounded in most libms, so results differ across platforms. That is a determinism question, not a value hazard; it belongs with the platform contract. - Flush-to-zero and denormals-are-zero modes change semantics. Cinch never enables them, and generated code must never set them.
3 Predicates
Extending the closed refinement vocabulary, all decidable by one or two float comparisons:
| Predicate | Meaning | Runtime test |
|---|---|---|
NotNaN x |
x is not NaN |
fcmp ord x, x |
Finite x |
not NaN and not infinite | isFinite |
NonZero x |
strictly below or above zero, so neither zero and not NaN | `flt x 0 |
Positive x, Negative x |
x > 0, x < 0 (excludes both zeros and NaN) |
one fcmp |
NonNegative x |
x >= 0 or x is -0 (the sqrt domain; excludes NaN) |
one fcmp |
InRange lo hi x |
lo <= x <= hi for literal bounds (excludes NaN) |
two fcmp |
type Real64 = refine (x :: F64) with Finite x (name open) is the type most
code wants: an actual real number.
Facts from case rows (the D412 rework) produce these predicates from the
existing IEEE tests, with IEEE-correct negation. The false row of flt x y
gives not (x < y), which implies x >= y only when both are NotNaN. The
true row of isNaN x gives nothing useful, and its false row gives
NotNaN x.
4 Operations whose types state their guarantees
Because Finite is not closed under arithmetic, refined operands give a
weaker result, and the type says exactly what holds:
| Operation | Operands | Result |
|---|---|---|
+, -, * |
Finite, Finite |
NotNaN (may overflow to an infinity) |
/ |
Finite, NonZero and Finite |
NotNaN |
sqrt |
NonNegative |
NotNaN (and Finite if the operand is) |
log |
Positive |
NotNaN |
rem |
Finite, NonZero |
Finite |
| conversion to an integer type | InRange lo hi within the target’s range |
the integer, total, no Option |
To get Finite back after arithmetic there are two routes:
- Interval reasoning (proposed).
InRangepredicates with literal bounds compose: ifxis in[a, b]andyin[c, d], thenx + yis in[a + c, b + d], computed with outward rounding so the bound is sound. When the result interval is inside the finite range, the result isFiniteandInRange, with no runtime check. The checker already has literal-interval implication (types.rs,interval_atom). This extends it from comparisons to arithmetic over refined operands: abstract interpretation of the expression over intervals. Physical quantities, probabilities, normalized coordinates and money in a bounded range all fit this pattern. - Decide at a boundary. Where bounds are unknown (input, accumulation
over unbounded loops),
decide (Finite x)once, which is one comparison, and the fact carries forward. This is the same discipline as validating input.
5 What becomes lawful, and what gets faster
Eq,OrdandHashonNotNaN F64. Without NaN, a total order exists. Two choices, which differ only on zeros:- IEEE equality (
-0 == +0, as Rust’sordered_float::NotNandoes, with hashing normalizing-0to+0). This is an equivalence relation, but not a congruence:1 / xseparates the two zeros, so a rewrite that substitutes equals for equals would be unsound. totalOrderequality (-0 /= +0), which IS a congruence. Proposed, because Cinch’s rewrites and laws assume equal values are interchangeable. IEEEfeqstays available by name.
- IEEE equality (
- Sound fast-math. LLVM’s
nnanandninfflags let the optimizer assume no NaN or infinity, and make the result poison if the assumption is wrong, so most languages cannot use them safely. When the refinements prove operands and resultNotNaNorFinite, the compiler can emit them soundly, with the proof as the justification (the unifying justification rule,design/layouts-and-specialization.mdsection 7).reassocandcontractchange results and stay forbidden without a contract.
6 Feasibility
| Tier | What | Cost | Builds on |
|---|---|---|---|
| 1 | The predicates of section 3, decide, total front doors (finite :: F64 -> Option Real64), facts from IEEE tests with correct negation |
small: the integer machinery reused | D412, D413 |
| 2 | The refined operation signatures of section 4; Eq/Ord/Hash on NotNaN F64; total float-to-integer conversion from InRange |
medium: a signature table and instance work | tier 1 |
| 3 | Interval reasoning over arithmetic (section 4, route 1) | medium to large: outward-rounded interval arithmetic in the checker | NF4’s linear arithmetic slot in types.rs |
| 4 | Sound nnan/ninf emission |
small once tier 3 exists | the justification rule |
| 5 | Error-bound proofs (how far a result is from the real value) | research (Flocq-level) | proof automation, last |
Tiers 1 and 2 are straightforward, and tier 3 is what makes it pleasant. None
of it needs IEEE semantics to change: plain F64 stays exact IEEE, and the
refinements say what is known about a value.
7 Decisions (user, 2026-10-30)
F64(andF32) stay the only float types, unboxed, with exact IEEE semantics. The refinements of section 3 carry the proofs. A refinement is representation-identical (D31: the refined value is the container’s machine word), soFinite,NotNaNandInRangecost nothing at runtime. A name such asReal64is only an alias forrefine (x :: F64) with Finite x, never a distinct boxed type. This settles “opt-in or default”: there is one type, and what is known about a value is in its refinement.- The IEEE
Eq/Ordinstances onF64live in an unsafe module and are never implicit. They breakEq’s laws (NaNis not equal to itself), so they cannot be ordinary instances. Proposed mechanism, extending the existingunsafekeyword (prim bodies, D213/D221): anunsafe instanceis exempt from the law layer’s checks for the laws it names as violated, is recorded as an assumption in the artifact manifest, may never be declaredimplicit(animplicit unsafe instanceis a compile error), and is reachable only by explicit selection (using/with). It lives in a module whose name says so (Unsafe.Float, orData.Float.Ieeewith the unsafe marker on the instance; the name is open).
8 Open decisions
- Whether the LAWFUL instances on
NotNaN F64(totalOrderequality, a congruence, section 5) are implicit, or also explicit-only. - The module name for the unsafe IEEE instances.
- Names:
Real64/Real32aliases; theInRangespelling.
