Chapter 6 Numerics
SourceSilo takes numbers seriously. There are four distinct numeric representations, three arithmetic-variant families for when a plain + isn't enough, and a full set of bitwise operations. This chapter walks through all three layers and the reasoning behind each.
Four representations, one axis
Silo's numbers split along one axis you already care about even if you don't realise it: exact versus inexact.
| Representation | Shape | Exact? |
|---|---|---|
Int |
Arbitrary-precision integer | Yes |
Fraction |
Exact rational (p/q) |
Yes |
Decimal |
Unbounded decimal | Yes |
Float |
IEEE 754 binary floating point | No |
Three of the four are exact. The one that isn't — Float — is the familiar IEEE 754 representation with all the usual surprises: 0.1 0.2 + is not exactly 0.3, rounding modes matter, arithmetic isn't associative. Everything else in the number tower avoids those pitfalls.
42 # Int
-7 # Int
2 100 pow # Int — still exact, even at 2^100
3/4 # Fraction
1/3 1/4 + # Fraction — exactly 7/12
3.14 # Float (F64)
19.99:D64 # Decimal (D64)
Two of these types come in a family of widths:
- Float:
F16,F32,F64,F128,F256. The default for a bare literal like3.14isF64. - Decimal:
D32,D64,D128. Written with a:D64suffix on a literal:19.99:D64.
Int and Fraction don't have width variants because they're arbitrary-precision. Int and Decimal can grow as large as the host allows; the host decides whether overflow is "never" or "bounded by available memory".
Contamination: exact meets inexact
When two operands have different representations, the result goes to whichever one is less precise:
1 3.0 + # ⌊4.0⌉ Int + Float → Float
1/3 0.5 + # ⌊0.833…⌉ Fraction + Float → Float
42 1.0:D64 + # ⌊43.0⌉ Int + Decimal → Decimal
1.0:D64 1.0:F64 + # ⌊2.0⌉ Decimal + Float → Float
The rule is: any Float in the input produces a Float in the output. Exactness is preserved only when every operand is exact. This is the contamination rule. It makes it impossible to accidentally lose exactness without a Float literal appearing somewhere visible.
If you want to intermix exact and inexact deliberately, you coerce: my-int (F64 .from) pulls an Int into F64 explicitly.
Bounded-integer types
A plain Int is unbounded. For fixed-width integers — which most low-level code wants — Silo uses the dependent type machinery from chapter 6: an integer type parameterised by its range.
( Int 0 .. 256 ) # unsigned byte
( Int -2 ^63 .. 2 ^63 ) # signed 64-bit
( Int .. ) # unbounded signed
Standard aliases are defined for the common widths:
| Unsigned | Signed |
|---|---|
U8 |
I8 |
U16 |
I16 |
U32 |
I32 |
U64 |
I64 |
I128 |
A literal gets a specific width via a suffix: 42:U8, -7:I32, 0xFF:U64.
Because the range lives in the type, the compiler knows what fits and what doesn't. 300:U8 is a compile error — not a wrap-around, not a silent truncation, a compile error.
Arithmetic variants
The plain +, -, *, / operators produce results of the same type as their inputs. On bounded integer types that means something has to happen when the mathematical result doesn't fit. Silo lets you choose, by providing four variants of each operator.
Saturating (default)
+, -, *, / on bounded types saturate: they clamp at the type's min or max rather than overflow:
255 :U8 1 :U8 + # ⌊255⌉ clamped at U8 max
0 :U8 1 :U8 - # ⌊0⌉ clamped at U8 min
42 1 + # ⌊43⌉ unbounded Int, no limit
Saturation is the conservative default: the program keeps running, no silent wrap, no panic — you just don't get values outside the type's range.
Checked — +? -? *? /?
Returns a (Result T Str):
127 :I8 1 :I8 + ? # ⌊Err("overflow: 127 + 1 exceeds I8 max")⌉
42 :U8 1 :U8 + ? # ⌊Ok(43:U8)⌉
10 0 /? # ⌊Err("division by zero")⌉
Use checked arithmetic when you want to react to overflow programmatically, for example to return a structured error from the enclosing word.
Wrapping — +! -! *!
Modular arithmetic — the two's-complement wrap that low-level code sometimes wants explicitly:
255 :U8 1 :U8 + ! # ⌊0⌉ wraps at U8 max
127 :I8 1 :I8 + ! # ⌊-128⌉ wraps at I8 max
The ! is pronounced "bang" by tradition and signals "I know what I'm doing, please wrap". Nothing silent here — the ! is the opt-in.
Widening — +_ -_ *_
Widens the result to the next-larger bounded type so it always fits:
200 :U8 200 :U8 + _ # ⌊400:U16⌉ widened
127 :I8 127 :I8 + _ # ⌊254:I16⌉ widened
Useful when you're going to feed the result into a further calculation that can handle the wider type. The _ suffix indicates "give me the widened result".
Summary table
| Variant | Suffix | What happens on overflow |
|---|---|---|
| Saturating | (none) | Clamp to min/max |
| Checked | ? |
Returns (Result T Str) with Err on overflow |
| Wrapping | ! |
Two's-complement wrap |
| Widening | _ |
Result is the next-wider type |
Pick the one that matches what you actually want; don't settle for "I hope it doesn't overflow".
Bitwise operations
Every integer type — bounded or unbounded — implements the bitwise traits. Each operation is an ordinary method:
0xFF :U8 0x0F :U8 .bit-and # ⌊0x0F:U8⌉
0xF0 :U8 0x0F :U8 .bit-or # ⌊0xFF:U8⌉
0xFF :U8 0x0F :U8 .bit-xor # ⌊0xF0:U8⌉
0xF0 :U8 .bit-not # ⌊0x0F:U8⌉
1 :U8 4 .shl # ⌊0x10:U8⌉ 1 << 4
0x10 :U8 2 .shr # ⌊0x04:U8⌉ 16 >> 2
The six traits are BitAnd, BitOr, BitXor, BitNot, Shl, Shr. A convenience trait Bitwise bundles all six, so a generic word that needs the whole bitwise vocabulary can depend on a single bound:
:fn popcount ( a -> Int ) { ( Bitwise a ) }
.. .
:end
Shift amounts accept any unsigned integer. The host handles any out-of-range shift according to its platform; you can reason about the operation as "shift by this many bits, clamped to the width".
Key points
- Four representations —
Int,Fraction,Decimal,Float— with exact/inexact contamination: aFloatanywhere in the inputs makes the output aFloat. - Bounded integer types are a dependent-typed refinement of
Int. Common widths have aliases (U8…U64,I8…I128). - Four arithmetic variants cover the "what happens on overflow" question: saturating (default), checked
?, wrapping!, widening_. Pick deliberately; the defaults never silently wrap. - Bitwise operations are methods on the
BitAnd/BitOr/BitXor/BitNot/Shl/Shrtraits; theBitwiseconvenience trait bundles all six.
Next: pattern matching, including the Never type and existential downcasting.