GuideThe Silo HandbookNumerics

Chapter 6 Numerics

Source

Silo 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 like 3.14 is F64.
  • Decimal: D32, D64, D128. Written with a :D64 suffix 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: a Float anywhere in the inputs makes the output a Float.
  • Bounded integer types are a dependent-typed refinement of Int. Common widths have aliases (U8U64, I8I128).
  • 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/Shr traits; the Bitwise convenience trait bundles all six.

Next: pattern matching, including the Never type and existential downcasting.