Skip to content

Commit b694d02

Browse files
authored
Basic support for floats (f16, f32, f64, f128) (#995)
This PR adds basic support for [IEEE 754 Floating Point Numbers](https://en.wikipedia.org/wiki/IEEE_754), specifically `f16`, `f32`, `f64`, `f128`. These are already supported in K through builtins (see [domains.md](https://github.com/runtimeverification/k/blob/master/k-distribution/include/kframework/builtin/domains.md#ieee-754-floating-point-numbers)). This work is derivative of the implementation in the c-semantics (see ieee754.k in that repository). In particular: - 6 tests are added that tests each type for equality, negation, comparison, casting, arithmetic, and special values (NaN and Infinity) - Documentation and helpers for floats are added to numbers.md - Decoding is added for floats from bytes in numbers.md (see note on haskell backend below) - Semantic rules for arithmetic are extended for floats in data.md ### Haskell Backend The haskell backend has no `Float` builtins (no Float.hs in [kore/src/Kore/Builtin/](https://github.com/runtimeverification/haskell-backend/tree/master/kore/src/Kore/Builtin)). This means `kore-exec` crashes with "missing hook FLOAT.int2float" when attempting to evaluate a float. The booster avoids this by delegating Float evaluation to the LLVM shared library via `simplifyTerm` in [booster/library/Booster/LLVM.hs](https://github.com/runtimeverification/haskell-backend/blob/master/booster/library/Booster/LLVM.hs). Prior to being able to decode floats, they were left as `UnableToDecode` which did not throw and error in the exec-smir test suite for `--haskell-backend`. Now that they are able to be decoded, the haskell backend throws on these decoded values. So I am now skipping any tests with decoded floats for exec-smir with haskell backend.
1 parent f0257bc commit b694d02

12 files changed

Lines changed: 535 additions & 11 deletions

File tree

kmir/src/kmir/kdist/mir-semantics/rt/data.md

Lines changed: 85 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1421,7 +1421,41 @@ Boolean values can also be cast to Integers (encoding `true` as `1`).
14211421
[preserves-definedness] // ensures #numTypeOf is defined
14221422
```
14231423

1424-
Casts involving `Float` values are currently not implemented.
1424+
Casts involving `Float` values: `IntToFloat`, `FloatToInt`, and `FloatToFloat`.
1425+
1426+
```k
1427+
// IntToFloat: convert integer to float with the target float type's precision
1428+
rule <k> #cast(Integer(VAL, _WIDTH, _SIGNEDNESS), castKindIntToFloat, _, TY)
1429+
=> Float(
1430+
Int2Float(VAL,
1431+
#significandBits(#floatTypeOf(lookupTy(TY))),
1432+
#exponentBits(#floatTypeOf(lookupTy(TY)))),
1433+
#bitWidth(#floatTypeOf(lookupTy(TY)))
1434+
)
1435+
...
1436+
</k>
1437+
[preserves-definedness]
1438+
1439+
// FloatToInt: truncate float towards zero and convert to integer
1440+
rule <k> #cast(Float(VAL, _WIDTH), castKindFloatToInt, _, TY)
1441+
=> #intAsType(Float2Int(VAL), 128, #intTypeOf(lookupTy(TY)))
1442+
...
1443+
</k>
1444+
requires #isIntType(lookupTy(TY))
1445+
[preserves-definedness]
1446+
1447+
// FloatToFloat: round float to the target float type's precision
1448+
rule <k> #cast(Float(VAL, _WIDTH), castKindFloatToFloat, _, TY)
1449+
=> Float(
1450+
roundFloat(VAL,
1451+
#significandBits(#floatTypeOf(lookupTy(TY))),
1452+
#exponentBits(#floatTypeOf(lookupTy(TY)))),
1453+
#bitWidth(#floatTypeOf(lookupTy(TY)))
1454+
)
1455+
...
1456+
</k>
1457+
[preserves-definedness]
1458+
```
14251459

14261460
### Casts between pointer types
14271461

@@ -2015,6 +2049,20 @@ are correct.
20152049
rule onInt(binOpRem, X, Y) => X %Int Y requires Y =/=Int 0 [preserves-definedness]
20162050
// operation undefined otherwise
20172051
2052+
// performs the given operation on IEEE 754 floats
2053+
// Note: Rust's float % is truncating remainder: x - trunc(x/y) * y
2054+
// This differs from K's %Float which is IEEE 754 remainder (round to nearest).
2055+
syntax Float ::= onFloat( BinOp, Float, Float ) [function]
2056+
// -------------------------------------------------------
2057+
rule onFloat(binOpAdd, X, Y) => X +Float Y [preserves-definedness]
2058+
rule onFloat(binOpAddUnchecked, X, Y) => X +Float Y [preserves-definedness]
2059+
rule onFloat(binOpSub, X, Y) => X -Float Y [preserves-definedness]
2060+
rule onFloat(binOpSubUnchecked, X, Y) => X -Float Y [preserves-definedness]
2061+
rule onFloat(binOpMul, X, Y) => X *Float Y [preserves-definedness]
2062+
rule onFloat(binOpMulUnchecked, X, Y) => X *Float Y [preserves-definedness]
2063+
rule onFloat(binOpDiv, X, Y) => X /Float Y [preserves-definedness]
2064+
rule onFloat(binOpRem, X, Y) => X -Float (Y *Float truncFloat(X /Float Y)) [preserves-definedness]
2065+
20182066
// error cases for isArithmetic(BOP):
20192067
// * arguments must be Numbers
20202068
@@ -2083,6 +2131,18 @@ are correct.
20832131
// infinite precision result must equal truncated result
20842132
andBool truncate(onInt(BOP, ARG1, ARG2), WIDTH, Unsigned) ==Int onInt(BOP, ARG1, ARG2)
20852133
[preserves-definedness]
2134+
2135+
// Float arithmetic: Rust never emits CheckedBinaryOp for floats (only BinaryOp),
2136+
// so the checked flag is always false here. See rustc_const_eval/src/interpret/operator.rs:
2137+
// binary_float_op returns a plain value, not a (value, overflow) pair.
2138+
rule #applyBinOp(
2139+
BOP,
2140+
Float(ARG1, WIDTH),
2141+
Float(ARG2, WIDTH),
2142+
false)
2143+
=> Float(onFloat(BOP, ARG1, ARG2), WIDTH)
2144+
requires isArithmetic(BOP)
2145+
[preserves-definedness]
20862146
```
20872147

20882148
#### Comparison operations
@@ -2119,6 +2179,14 @@ The argument types must be the same for all comparison operations, however this
21192179
rule cmpOpBool(binOpGe, X, Y) => cmpOpBool(binOpLe, Y, X)
21202180
rule cmpOpBool(binOpGt, X, Y) => cmpOpBool(binOpLt, Y, X)
21212181
2182+
syntax Bool ::= cmpOpFloat ( BinOp, Float, Float ) [function]
2183+
rule cmpOpFloat(binOpEq, X, Y) => X ==Float Y
2184+
rule cmpOpFloat(binOpLt, X, Y) => X <Float Y
2185+
rule cmpOpFloat(binOpLe, X, Y) => X <=Float Y
2186+
rule cmpOpFloat(binOpNe, X, Y) => X =/=Float Y
2187+
rule cmpOpFloat(binOpGe, X, Y) => X >=Float Y
2188+
rule cmpOpFloat(binOpGt, X, Y) => X >Float Y
2189+
21222190
// error cases for isComparison and binOpCmp:
21232191
// * arguments must be numbers or Bool
21242192
@@ -2146,9 +2214,19 @@ The argument types must be the same for all comparison operations, however this
21462214
BoolVal(cmpOpBool(OP, VAL1, VAL2))
21472215
requires isComparison(OP)
21482216
[priority(60), preserves-definedness] // OP known to be a comparison
2217+
2218+
rule #applyBinOp(OP, Float(VAL1, WIDTH), Float(VAL2, WIDTH), _)
2219+
=>
2220+
BoolVal(cmpOpFloat(OP, VAL1, VAL2))
2221+
requires isComparison(OP)
2222+
[preserves-definedness] // OP known to be a comparison
21492223
```
21502224

2151-
The `binOpCmp` operation returns `-1`, `0`, or `+1` (the behaviour of Rust's `std::cmp::Ordering as i8`), indicating `LE`, `EQ`, or `GT`.
2225+
Types that are equivlance relations can implement [Eq](https://doc.rust-lang.org/std/cmp/trait.Eq.html),
2226+
and then they may implement [Ord](https://doc.rust-lang.org/std/cmp/trait.Ord.html) for a total ordering.
2227+
For types that implement `Ord` the `cmp` method must be implemented which can compare any two elements respective to their total ordering.
2228+
Here we provide the `binOpCmp` for `Bool` and `Int` operation which returns `-1`, `0`, or `+1` (the behaviour of Rust's `std::cmp::Ordering as i8`),
2229+
indicating `LE`, `EQ`, or `GT`.
21522230

21532231
```k
21542232
syntax Int ::= cmpInt ( Int , Int ) [function , total]
@@ -2183,7 +2261,11 @@ The semantics of the operation in this case is to wrap around (with the given bi
21832261
...
21842262
</k>
21852263
2186-
// TODO add rule for Floats once they are supported.
2264+
rule <k> #applyUnOp(unOpNeg, Float(VAL, WIDTH))
2265+
=>
2266+
Float(--Float VAL, WIDTH)
2267+
...
2268+
</k>
21872269
```
21882270

21892271
The `unOpNot` operation works on boolean and integral values, with the usual semantics for booleans and a bitwise semantics for integral values (overflows cannot occur).

kmir/src/kmir/kdist/mir-semantics/rt/decoding.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,13 @@ and arrays (where layout is trivial).
5252
requires #isIntType(TYPEINFO) andBool lengthBytes(BYTES) ==Int #elemSize(TYPEINFO)
5353
[preserves-definedness]
5454
55+
// Float: handled in separate module for numeric operations
56+
rule #decodeValue(BYTES, TYPEINFO) => #decodeFloat(BYTES, #floatTypeOf(TYPEINFO))
57+
requires #isFloatType(TYPEINFO) andBool lengthBytes(BYTES) ==Int #elemSize(TYPEINFO)
58+
[preserves-definedness]
59+
5560
// TODO Char type
5661
// rule #decodeConstant(constantKindAllocated(allocation(BYTES, _, _, _)), typeInfoPrimitiveType(primTypeChar)) => typedValue(Str(...), TY, mutabilityNot)
57-
58-
// TODO Float decoding: not supported natively in K
5962
```
6063

6164

kmir/src/kmir/kdist/mir-semantics/rt/numbers.md

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ The code in this file implements functionality for `Integer` and `Float` values
55
```k
66
requires "./value.md"
77
requires "../ty.md"
8+
requires "rat.md"
89
910
module RT-NUMBERS
1011
imports TYPES
@@ -13,6 +14,8 @@ module RT-NUMBERS
1314
imports BOOL
1415
imports BYTES
1516
imports INT
17+
imports FLOAT
18+
imports RAT
1619
```
1720

1821
## Helpers and Constants for Integer Operations
@@ -38,6 +41,15 @@ module RT-NUMBERS
3841
rule #isIntType(typeInfoPrimitiveType(primTypeInt(_))) => true
3942
rule #isIntType(typeInfoPrimitiveType(primTypeUint(_))) => true
4043
rule #isIntType(_) => false [owise]
44+
45+
syntax Bool ::= #isFloatType ( TypeInfo ) [function, total]
46+
// --------------------------------------------------------
47+
rule #isFloatType(typeInfoPrimitiveType(primTypeFloat(_))) => true
48+
rule #isFloatType(_) => false [owise]
49+
50+
syntax FloatTy ::= #floatTypeOf ( TypeInfo ) [function]
51+
// ----------------------------------------------------
52+
rule #floatTypeOf(typeInfoPrimitiveType(primTypeFloat(FLOATTY))) => FLOATTY
4153
```
4254

4355
Constants used for overflow-checking and truncation are defined here as macros.
@@ -110,6 +122,197 @@ This truncation function is instrumental in the implementation of Integer arithm
110122
[preserves-definedness]
111123
```
112124

125+
## Helpers and Constants for Float Operations
126+
127+
Rust supports four fixed-width IEEE 754 float types: `f16`, `f32`, `f64`, and `f128`.
128+
The helpers below extract format parameters for each type. First, an overview of the format.
129+
130+
### IEEE 754 Binary Format
131+
132+
An IEEE 754 binary floating-point word has three fields stored left-to-right:
133+
134+
```
135+
MSB LSB
136+
+---------+----------------+----------------------+
137+
| sign | exponent | fraction |
138+
| (1 bit) | (EB bits) | (SB - 1 bits) |
139+
+---------+----------------+----------------------+
140+
total bits = 1 + EB + (SB - 1)
141+
```
142+
143+
The **significand** (also called **precision**) is the total number of significant bits
144+
in the represented value, including an implicit leading 1 that is not stored in the
145+
fraction field. For a normal number, the mathematical value is:
146+
147+
value = (-1)^sign * 2^(exponent - bias) * 1.fraction
148+
149+
The "1." prefix is the implicit bit, so the significand has `SB` bits of precision
150+
even though only `SB - 1` fraction bits are stored. For example, f64 stores 52 fraction
151+
bits but has 53 bits of significand precision.
152+
153+
K's built-in `FLOAT` module uses this convention: `Int2Float(x, precision, exponentBits)`
154+
takes `precision = SB` (total significand bits including the implicit 1) and `exponentBits = EB`.
155+
See [IEEE 754 on Wikipedia](https://en.wikipedia.org/wiki/IEEE_754) for full details.
156+
157+
The exponent is stored as an unsigned integer in
158+
[excess-M encoding](https://en.wikipedia.org/wiki/Offset_binary) with `bias = 2^(EB-1) - 1`,
159+
so that the actual exponent is `stored - bias`. For f64, bias = 1023: a stored value of 1023
160+
means exponent 0, 1024 means +1, and 1 means -1022. Stored values 0 and `2^EB - 1` are
161+
reserved for zero/subnormals and infinity/NaN respectively.
162+
163+
| Type | Total bits | Sign | Exponent (EB) | Fraction (SB-1) | Significand (SB) | Bias |
164+
|------|------------|------|---------------|-----------------|------------------|------------|
165+
| f16 | 16 | 1 | 5 | 10 | 11 | 15 |
166+
| f32 | 32 | 1 | 8 | 23 | 24 | 127 |
167+
| f64 | 64 | 1 | 11 | 52 | 53 | 1023 |
168+
| f128 | 128 | 1 | 15 | 112 | 113 | 16383 |
169+
170+
```k
171+
syntax Int ::= #significandBits ( FloatTy ) [function, total]
172+
// ----------------------------------------------------------
173+
rule #significandBits(floatTyF16) => 11
174+
rule #significandBits(floatTyF32) => 24
175+
rule #significandBits(floatTyF64) => 53
176+
rule #significandBits(floatTyF128) => 113
177+
178+
syntax Int ::= #exponentBits ( FloatTy ) [function, total]
179+
// -------------------------------------------------------
180+
rule #exponentBits(floatTyF16) => 5
181+
rule #exponentBits(floatTyF32) => 8
182+
rule #exponentBits(floatTyF64) => 11
183+
rule #exponentBits(floatTyF128) => 15
184+
185+
syntax Int ::= #bias ( FloatTy ) [function, total]
186+
// -----------------------------------------------
187+
rule #bias(FLOATTY) => (1 <<Int (#exponentBits(FLOATTY) -Int 1)) -Int 1
188+
```
189+
190+
### IEEE 754 Special Values
191+
192+
When the exponent field is all 1s (`2^EB - 1`), the value is either infinity
193+
(fraction = 0) or NaN (fraction != 0). K's `FLOAT-SYNTAX` module in
194+
[domains.md](https://github.com/runtimeverification/k/blob/master/k-distribution/include/kframework/builtin/domains.md#ieee-754-floating-point-numbers)
195+
defines float literals with a `p<SB>x<EB>` suffix to specify precision and exponent
196+
bits. See IEEE 754 Binary Format above for values of SB and EB.
197+
198+
For example, `Infinityp53x11` is f64 positive infinity, `NaNp24x8` is f32 NaN.
199+
200+
```k
201+
syntax Float ::= #posInfFloat ( FloatTy ) [function, total]
202+
// --------------------------------------------------------
203+
rule #posInfFloat(floatTyF16) => Infinityp11x5
204+
rule #posInfFloat(floatTyF32) => Infinityp24x8
205+
rule #posInfFloat(floatTyF64) => Infinityp53x11
206+
rule #posInfFloat(floatTyF128) => Infinityp113x15
207+
208+
syntax Float ::= #nanFloat ( FloatTy ) [function, total]
209+
// -----------------------------------------------------
210+
rule #nanFloat(floatTyF16) => NaNp11x5
211+
rule #nanFloat(floatTyF32) => NaNp24x8
212+
rule #nanFloat(floatTyF64) => NaNp53x11
213+
rule #nanFloat(floatTyF128) => NaNp113x15
214+
```
215+
216+
## Decoding Float values from `Bytes` for `OperandConstant`
217+
218+
The `#decodeFloat` function reconstructs a `Float` value from its IEEE 754 byte representation.
219+
The bytes are first converted to a raw integer, then the sign, biased exponent, and stored significand
220+
are extracted. The value is reconstructed using K's `Int2Float` and float arithmetic, with a
221+
high-precision intermediate to avoid overflow when reconstructing subnormals and small normal values.
222+
223+
```k
224+
syntax Value ::= #decodeFloat ( Bytes, FloatTy ) [function]
225+
// --------------------------------------------------------
226+
rule #decodeFloat(BYTES, FLOATTY) => #decodeFloatRaw(Bytes2Int(BYTES, LE, Unsigned), FLOATTY)
227+
requires lengthBytes(BYTES) ==Int #bitWidth(FLOATTY) /Int 8
228+
[preserves-definedness]
229+
230+
syntax Value ::= #decodeFloatRaw ( Int, FloatTy ) [function, total]
231+
// ----------------------------------------------------------------
232+
rule #decodeFloatRaw(RAW, FLOATTY)
233+
=> #decodeFloatParts(
234+
RAW >>Int (#significandBits(FLOATTY) +Int #exponentBits(FLOATTY) -Int 1),
235+
(RAW >>Int (#significandBits(FLOATTY) -Int 1)) &Int ((1 <<Int #exponentBits(FLOATTY)) -Int 1),
236+
RAW &Int ((1 <<Int (#significandBits(FLOATTY) -Int 1)) -Int 1),
237+
FLOATTY
238+
)
239+
240+
syntax Value ::= #decodeFloatParts ( sign: Int, biasedExp: Int, storedSig: Int, FloatTy ) [function, total]
241+
// --------------------------------------------------------------------------------------------------------
242+
243+
// Zero (positive or negative)
244+
rule #decodeFloatParts(SIGN, 0, 0, FLOATTY)
245+
=> Float(#applyFloatSign(Int2Float(0, #significandBits(FLOATTY), #exponentBits(FLOATTY)), SIGN), #bitWidth(FLOATTY))
246+
247+
// Subnormal: no implicit leading 1, exponent is 1 - bias
248+
rule #decodeFloatParts(SIGN, 0, SIG, FLOATTY)
249+
=> Float(
250+
#applyFloatSign(
251+
#reconstructFloat(SIG, 2 -Int #bias(FLOATTY) -Int #significandBits(FLOATTY), FLOATTY),
252+
SIGN
253+
),
254+
#bitWidth(FLOATTY)
255+
)
256+
requires SIG =/=Int 0
257+
258+
// Normal: implicit leading 1 in significand
259+
rule #decodeFloatParts(SIGN, EXP, SIG, FLOATTY)
260+
=> Float(
261+
#applyFloatSign(
262+
#reconstructFloat(
263+
SIG |Int (1 <<Int (#significandBits(FLOATTY) -Int 1)),
264+
EXP -Int #bias(FLOATTY) -Int #significandBits(FLOATTY) +Int 1,
265+
FLOATTY
266+
),
267+
SIGN
268+
),
269+
#bitWidth(FLOATTY)
270+
)
271+
requires EXP >Int 0 andBool EXP <Int ((1 <<Int #exponentBits(FLOATTY)) -Int 1)
272+
273+
// Infinity
274+
rule #decodeFloatParts(SIGN, EXP, 0, FLOATTY)
275+
=> Float(#applyFloatSign(#posInfFloat(FLOATTY), SIGN), #bitWidth(FLOATTY))
276+
requires EXP ==Int ((1 <<Int #exponentBits(FLOATTY)) -Int 1)
277+
278+
// NaN
279+
rule #decodeFloatParts(_SIGN, EXP, SIG, FLOATTY)
280+
=> Float(#nanFloat(FLOATTY), #bitWidth(FLOATTY))
281+
requires EXP ==Int ((1 <<Int #exponentBits(FLOATTY)) -Int 1) andBool SIG =/=Int 0
282+
283+
// Owise case should not be reachable, returning a string value that should get stuck in the semantics
284+
rule #decodeFloatParts(_, _, _, _) => StringVal( "ERRORFailedToDecodeFloat" ) [owise]
285+
286+
```
287+
288+
Reconstruct a float from its integer significand and adjusted exponent.
289+
For positive exponents, shift the significand left and convert directly.
290+
For negative exponents, use `Rat2Float` to convert the exact rational
291+
`SIG / 2^|AEXP|` to the target float precision.
292+
293+
```k
294+
syntax Float ::= #reconstructFloat ( sig: Int, adjExp: Int, FloatTy ) [function]
295+
// -------------------------------------------------------------------------------
296+
rule #reconstructFloat(SIG, AEXP, FLOATTY)
297+
=> Int2Float(SIG <<Int AEXP, #significandBits(FLOATTY), #exponentBits(FLOATTY))
298+
requires AEXP >=Int 0
299+
[preserves-definedness]
300+
301+
rule #reconstructFloat(SIG, AEXP, FLOATTY)
302+
=> Rat2Float(SIG /Rat (1 <<Int (0 -Int AEXP)),
303+
#significandBits(FLOATTY),
304+
#exponentBits(FLOATTY))
305+
requires AEXP <Int 0
306+
[preserves-definedness]
307+
308+
// Apply the sign bit to a float value
309+
syntax Float ::= #applyFloatSign ( Float, Int ) [function, total]
310+
// ---------------------------------------------------------------
311+
rule #applyFloatSign(F, 0) => F
312+
rule #applyFloatSign(F, 1) => --Float F
313+
rule #applyFloatSign(F, _) => F [owise]
314+
```
315+
113316
## Type Casts Between Different Numeric Types
114317

115318

kmir/src/tests/integration/data/exec-smir/structs-tuples/struct_field_update.state

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
ListItem ( newLocal ( ty ( 1 ) , mutabilityMut ) )
2929
ListItem ( typedValue ( Aggregate ( variantIdx ( 0 ) , ListItem ( Integer ( 1 , 32 , true ) )
3030
ListItem ( BoolVal ( true ) )
31-
ListItem ( thunk ( UnableToDecode ( b"33333sE@" , typeInfoPrimitiveType ( primTypeFloat ( floatTyF64 ) ) ) ) )
31+
ListItem ( Float ( 0.42899999999999999e2 , 64 ) )
3232
ListItem ( Aggregate ( variantIdx ( 0 ) , ListItem ( Integer ( 1 , 32 , true ) )
3333
ListItem ( Integer ( 10 , 32 , true ) ) ) ) ) , ty ( 28 ) , mutabilityMut ) )
3434
ListItem ( typedValue ( Moved , ty ( 27 ) , mutabilityMut ) )

0 commit comments

Comments
 (0)