4.5. Floating-point types
Morloc’s floating-point types are IEEE 754 binary formats. Real is the
default; F32 and F64 exist when you need to control precision explicitly.
| Type | Width | Use case |
|---|---|---|
|
Language-dependent (typically 64-bit IEEE 754) |
Default floating point. |
|
32 bits (IEEE 754 binary32) |
Tensors, GPU code, memory-constrained numerics. |
|
64 bits (IEEE 754 binary64) |
Default-precision scientific computation. |
Each maps to its host-language equivalent:
| Morloc type | C++ | Python | R |
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
4.5.1. Literal forms
Real literals need a decimal point or an exponent:
pi :: Real
pi = 3.14159265358979
-- scientific notation (upper or lowercase 'e')
avogadro :: F64
avogadro = 6.022e23
-- negative exponent
boltzmann :: Real
boltzmann = 1.380649e-23
$ ./floats pi
3.14159265358979
$ ./floats avogadro
6.022e+23
$ ./floats boltzmann
1.380649e-23
Note the explicit + on the printed exponent.
A literal with neither a decimal point nor an exponent is an Int, not a
Real. Write 1.0 or 1e0 when you want a floating-point one.
4.5.2. IEEE 754 and non-finite values
Real follows IEEE 754 in full, which means its value space is the finite
reals representable at the target precision plus three classes of non-finite
value:
-
+Infinity -
-Infinity -
NaN(Not-a-Number)
Ordinary arithmetic produces these: dividing by zero, overflowing the finite
range, or evaluating an indeterminate form such as Inf - Inf or 0 * Inf.
They are not error states. They are values, and they propagate through later
computation by rules the standard fixes.
Source-level literals
Each has a dedicated literal, capitalized to match Morloc’s other keyword-like
values (True, False, Null):
posInf :: Real
posInf = Inf
negInf :: Real
negInf = -Inf
notANumber :: Real
notANumber = NaN
-Inf lexes as a single atomic token, the same way -1.5 is one token rather
than negate 1.5, so it works in pure-Morloc contexts where negate is not in
scope. The same holds for -NaN, though the sign of a NaN collapses at the
wire boundary: both NaN and -NaN come back as the canonical nan.
Arithmetic on non-finite values
All three target languages follow IEEE 754 here, so these results do not depend on which pool the computation lands in. Every row below was run:
| Expression | Result | Why |
|---|---|---|
|
|
Same-sign infinity addition |
|
|
Invalid op: opposite-sign cancellation |
|
|
Invalid op: same-sign cancellation |
|
|
Invalid op: zero times infinity |
|
|
Magnitude preservation |
|
|
Sign rule on multiplication |
|
|
Like-sign product |
|
|
Mixed-sign product |
|
|
NaN absorption (additive) |
|
|
NaN beats zero |
|
|
NaN beats infinity |
|
|
Sign-bit flip |
|
|
Sign flip stays NaN |
4.5.3. Compile-time literal overflow
A real literal is bounds-checked against the precision it is written into. As
with integer literals, the check runs during code generation, so typecheck
passes and make rejects it.
For Real and F64, the maximum magnitude is about 1.8e308:
tooBig :: Real
tooBig = 1e500
$ morloc make fbig.loc
fbig.loc:6:10: error:
Float literal 1.0e500 overflows F64 (|x| > 1.8e308)
|
6 | tooBig = 1e500
| ^
The check is per-precision, so a literal that fits F64 can still overflow
F32 (maximum magnitude about 3.4e38):
tooBigF32 :: F32
tooBigF32 = 1e100
$ morloc make fbig32.loc
fbig32.loc:6:13: error:
Float literal 1.0e100 overflows F32 (|x| > 3.4e38)
|
6 | tooBigF32 = 1e100
| ^
Negative literals are checked symmetrically:
$ morloc make fneg.loc
fneg.loc:6:10: error:
Float literal -1.0e500 overflows F64 (|x| > 1.8e308)
|
6 | tooNeg = -1e500
| ^
Inf, -Inf, and NaN bypass the bounds check by construction. They are
explicit non-finite values, not finite literals that happened to overflow.
4.5.4. Wire format and JSON interop
The JSON wire format is RFC 8259 compliant, and standard JSON has no syntax for non-finite numbers. The specification’s recommended workaround is strings, so Morloc emits them as quoted lowercase strings:
| Value | JSON form |
|---|---|
|
|
|
|
|
|
Finite |
The numeric form ( |
You can see this in the output of the literals above:
$ ./floats posInf
"inf"
$ ./floats negInf
"-inf"
$ ./floats notANumber
"nan"
So a Real-typed field can arrive as either a JSON number or a JSON string.
Consumers need to accept both.
Internal cross-language boundaries do not use JSON. Morloc-to-pool calls use a binary format that preserves IEEE 754 bytes verbatim, so non-finite values round-trip with no loss. Only the JSON boundary — usually the program’s final output — uses the string form.
|
|
Cross-language gotcha: division by zero in Python
The three languages agree on IEEE 754 arithmetic, but they disagree on one
point of language design: Python raises That difference is visible from inside Morloc.
If a program depends on |
4.5.5. F32 precision considerations
F32 halves memory against F64, which matters for large numerical arrays — tensors, image buffers, GPU input — where the extra precision is not needed.
The tradeoffs:
-
The significand carries about 7 decimal digits of precision, against about
- 15 to 17 for
F64. A literal such as `0.1 -
F32` rounds to the nearest representable binary32 value; it is not exact.
- 15 to 17 for
-
Maximum magnitude is about 3.4e38, against 1.8e308 for
F64. The compile-time bounds check enforces this for literals. -
All
F32arithmetic runs at single precision, including the overflow-to-infinity threshold.
For most application code Real is the right default. Reach for F32
deliberately, when memory or single-precision hardware demands it.
4.5.6. Converting to and from floating point
The TotalInto and PartialInto classes from Integer types extend to
floats. into covers the conversions that cannot fail: widening an integer
whose full range fits the target mantissa (24 bits for F32, 53 for F64),
F32 to F64, and Real to and from F64 in both directions — they are
representationally identical in every current backend.
Integer-to-float conversions that may lose precision get their own class:
class RealLike a where
toReal :: a -> Real
toReal never fails but can lose precision above 2^53. Every numeric type has
an instance. The canonical use is a mean:
mean :: [Real] -> Real
mean xs = sum xs / toReal (size xs)
$ ./floats mean '[1,2,3,4]'
2.5
size returns U64 and toReal bridges it into the Real denominator. The
precision loss is theoretical at any realistic container size, but naming it
keeps the lossy step visible.
Float-to-integer conversion goes through tryInto, which raises rather than
returning a value it cannot represent. It fails on NaN, on Inf, on
non-integer values, and on values outside the target integer’s range:
approx :: Real -> I32
approx x = tryInto x
$ ./floats approx 3.0
3
$ ./floats approx 3.5
Error: run failed
cannot convert non-integer float 3.5 to integer
at approx [py] (mid=8, floats.loc:1:75)
$ ./floats approx 1e20
Error: run failed
value 100000000000000000000 out of range [-2147483648, 2147483647]
at approx [py] (mid=8, floats.loc:1:75)
To round to a nearby integer instead of failing, apply round, floor, ceil,
or trunc from the math module first, then tryInto the result.
Narrowing F64 to F32, and Real to F32, are deliberately not provided
as TotalInto instances — they lose precision on every input. If you need one,
source an explicit foreign function, so the lossy step is visible at the call
site.
4.5.7. Negation of Real values
Negation works on Real, F32, and F64 exactly as it does on integers, via
the Negatable typeclass; see Integer types for the full unary-minus
rules. Three IEEE 754 specifics:
-
-Infand-NaNare atomic source literals. Nonegatelookup happens, so they work in pure-Morloc contexts. -
negate Infis-Inf, andnegate NaNisNaN— the sign bit flips, but the value is still NaN. -
negate 0.0is-0.0. The two compare equal under==but have different bit patterns. The binary cross-language format preserves the distinction; the JSON output does not.