Skip to content

Math (std::math)

std::math brings together the essential numeric operations — rounding, trigonometry, interpolation, random-number generation, constants and arbitrary-precision decimal types. Import with use std::math.

Directed rounding

math.floor rounds toward −∞; math.ceil rounds toward +∞. An exact integer has floor == ceil.

pages(total, per_page) uses ceil to calculate the required number of pages.

01-floor-ceil.zolo
Playground
// Feature: math.floor / math.ceil — directed rounding

// When to use: round down/up to the nearest integer.


use std::math

// floor — rounds down (toward -inf).

print(math.floor(3.2))  // expected: 3

print(math.floor(3.9))  // expected: 3

print(math.floor(-1.2))  // expected: -2


// ceil — rounds up (toward +inf).

print(math.ceil(3.1))  // expected: 4

print(math.ceil(3.0))  // expected: 3

print(math.ceil(-1.7))  // expected: -1


// Exact integer — floor == ceil.

print(math.floor(5.0))  // expected: 5

print(math.ceil(5.0))  // expected: 5


// Application: split N items across fixed-size pages.

fn pages(total: int, per_page: int) -> int {
  return math.ceil(total / (per_page * 1.0))
}

print(pages(100, 10))  // expected: 10

print(pages(101, 10))  // expected: 11

print(pages(1, 10))  // expected: 1

Square root and exponentiation

math.sqrt and math.pow cover most geometric formulas.

2-D Euclidean distance built with sqrt and direct multiplication.

02-sqrt-and-pow.zolo
Playground
// Feature: math.sqrt / math.pow — square root and exponentiation

// When to use: distances, vector magnitudes, math formulas.


use std::math

print(math.sqrt(9.0))  // expected: 3.0

print(math.sqrt(2.0))  // ~1.4142...

print(math.sqrt(0.0))  // expected: 0.0


// pow — base ^ exp.

print(math.pow(2.0, 10.0))  // expected: 1024.0

print(math.pow(3.0, 2.0))  // expected: 9.0

print(math.pow(10.0, 0.0))  // expected: 1.0


// 2D Euclidean distance.

fn dist(x1: float, y1: float, x2: float, y2: float) -> float {
  let dx = x2 - x1
  let dy = y2 - y1
  return math.sqrt(dx * dx + dy * dy)
}

print(dist(0.0, 0.0, 3.0, 4.0))  // expected: 5.0

print(dist(1.0, 1.0, 4.0, 5.0))  // expected: 5.0

Trigonometry

Trigonometric functions operate in radians. Use math.rad and math.deg to convert between degrees and radians.

Notable values of sin/cos/tan; built-in degree converter.

03-trig.zolo
Playground
// Feature: math.sin / math.cos / math.tan — trigonometry (radians)

// When to use: rotations, animations, geometry, physics.


use std::math

const PI = 3.14159265358979

// Notable values.

print("{math.sin(0.0):.2f}")  // expected: 0.00

print("{math.cos(0.0):.2f}")  // expected: 1.00

print("{math.sin(PI / 2.0):.2f}")  // expected: 1.00

print("{math.cos(PI):.2f}")  // expected: -1.00


// Degrees -> radians conversion.

fn rad(deg: float) -> float {
  return deg * (PI / 180.0)
}

// 30 degrees.

print("{math.sin(rad(30.0)):.2f}")  // expected: 0.50

print("{math.cos(rad(60.0)):.2f}")  // expected: 0.50


// math.deg / math.rad — built-in converters.

print("{math.rad(180.0):.2f}")  // expected: 3.14

print("{math.deg(PI):.0f}")  // expected: 180


// tan(45 degrees) ~ 1.

print("{math.tan(rad(45.0)):.2f}")  // expected: 1.00

Random numbers

math.randomseed fixes the seed for deterministic results — useful in tests. math.random() returns a float in [0, 1); math.random(N) returns an integer in [1, N].

Fixed seed + random element selection from an array.

04-random.zolo
Playground
// Feature: math.random / math.randomseed — pseudo-random numbers

// When to use: pick elements, simple simulations, games.


use std::math

// A fixed seed makes output deterministic — useful for tests.

math.randomseed(42)

// random() with no args — float in [0.0, 1.0).

let x = math.random()
print(x >= 0.0 && x < 1.0)  // expected: true


let y = math.random()
print(y >= 0.0 && y < 1.0)  // expected: true


// random(N) — integer in [1, N].

let die = math.random(6)
print(die >= 1 && die <= 6)  // expected: true


// random(lo, hi) — integer in [lo, hi].

let between = math.random(10, 20)
print(between >= 10 && between <= 20)

// expected: true


// Pick a random element from an array.

let colors = ["red", "green", "blue", "yellow"]
let i = math.random(1, colors.len()) - 1  // 0..len-1

let chosen = colors[i]
print(chosen.len() > 0)  // expected: true

Absolute value, minimum and maximum

abs, min, max; manual clamp composed with min(max(v, lo), hi).

05-abs-min-max.zolo
Playground
// Feature: math.abs / math.min / math.max — numeric utilities

// When to use: scalar distance, clamp, comparisons.


use std::math

// abs — absolute value.

print(math.abs(5))  // expected: 5

print(math.abs(-5))  // expected: 5

print(math.abs(-3.14))  // expected: 3.14

print(math.abs(0))  // expected: 0


// min / max — between two values.

print(math.min(3, 7))  // expected: 3

print(math.max(3, 7))  // expected: 7

print(math.min(-1, -5))  // expected: -5

print(math.max(-1, -5))  // expected: -1


// Pattern: manual clamp.

fn clamp(v: int, lo: int, hi: int) -> int {
  return math.min(math.max(v, lo), hi)
}

print(clamp(15, 0, 10))  // expected: 10

print(clamp(-3, 0, 10))  // expected: 0

print(clamp(5, 0, 10))  // expected: 5


// Absolute difference.

fn diff(a: int, b: int) -> int {
  return math.abs(a - b)
}

print(diff(10, 3))  // expected: 7

print(diff(3, 10))  // expected: 7

Interpolation and smoothing

math.mix / math.lerp interpolate linearly; math.remap converts between ranges; math.step is a binary threshold; math.smoothstep smooths the transition.

clamp, mix/lerp, inverse_lerp, remap, step, smoothstep — animation and shader primitives.

06-clamp-mix-step.zolo
Playground
// Feature: math.clamp / mix / step / smoothstep / remap — blending helpers

// When to use: animation, easing, color blending, creative coding,

// graphics — wherever you'd reach for GLSL's interpolation primitives.


use std::math

// clamp — limit a value to [lo, hi].

print(math.clamp(15, 0, 10))   // expected: 10

print(math.clamp(-5, 0, 10))   // expected: 0

print(math.clamp(5, 0, 10))    // expected: 5


// mix / lerp — linear interpolation.

// `mix(a, b, t)` = a + (b - a) * t. Both names are aliases.

print(math.mix(0.0, 100.0, 0.5))      // expected: 50

print(math.lerp(0.0, 100.0, 0.25))    // expected: 25

print(math.mix(0.0, 100.0, 1.5))      // expected: 150  (extrapolates — does NOT clamp t)


// inverse_lerp — given v in [a, b], return t such that mix(a, b, t) == v.

print(math.inverse_lerp(0.0, 100.0, 50.0))   // expected: 0.5

print(math.inverse_lerp(0.0, 100.0, 25.0))   // expected: 0.25

print(math.inverse_lerp(10.0, 20.0, 25.0))   // expected: 1.5  (extrapolates)


// remap — convert a value from one range to another.

//   remap(0.5, 0, 1, 0, 100)   ≡  50

print(math.remap(0.5, 0.0, 1.0, 0.0, 100.0))   // expected: 50

print(math.remap(75.0, 0.0, 100.0, -1.0, 1.0)) // expected: 0.5


// step — hard threshold: 0 if x < edge, 1 otherwise.

print(math.step(0.5, 0.3))    // expected: 0

print(math.step(0.5, 0.5))    // expected: 1

print(math.step(0.5, 0.7))    // expected: 1


// smoothstep — Hermite smooth interpolation. Like step, but smooth.

// Useful for soft transitions, antialiasing, eased fades.

print(math.smoothstep(0.0, 1.0, -0.5))    // expected: 0  (below edge0)

print(math.smoothstep(0.0, 1.0, 0.5))     // expected: 0.5 (center)

print(math.smoothstep(0.0, 1.0, 1.5))     // expected: 1  (above edge1)

Sign, fraction, truncation and powers of two

math.sign, math.fract and math.trunc follow GLSL semantics. math.exp2/math.log2 are useful in audio and mip-maps; math.is_power_of_two and math.next_power_of_two help with circular-buffer sizing.

fract(-0.3) returns 0.7 (GLSL semantics, not C); next_power_of_two(63) returns 64.

07-sign-fract-trunc.zolo
Playground
// Feature: math.sign / fract / trunc / exp2 / log2 / power-of-two helpers

// When to use: signal processing, packing, hash table sizing, fast

// power-of-two arithmetic, GLSL-style numeric primitives.


use std::math

// sign — returns -1, 0, or 1. Matches GLSL: sign(0) == 0.

print(math.sign(-5.0))    // expected: -1

print(math.sign(0.0))     // expected: 0

print(math.sign(3.0))     // expected: 1


// fract — fractional part, always in [0, 1). Differs from `x % 1`

// for negatives: GLSL semantics, not C semantics.

print(math.fract(3.7))    // expected: 0.7

print(math.fract(-0.3))   // expected: 0.7  (not -0.3!)

print(math.fract(2.0))    // expected: 0


// trunc — drop fractional part toward zero. Differs from floor for

// negatives.

print(math.trunc(3.7))    // expected: 3

print(math.trunc(-3.7))   // expected: -3   (floor(-3.7) would be -4)

print(math.trunc(2.0))    // expected: 2


// exp2 / log2 — base-2 power and logarithm. Useful for octave-based

// audio, dB conversions, mip-map levels.

print(math.exp2(3.0))     // expected: 8

print(math.log2(1024.0))  // expected: 10

print(math.log2(0.5))     // expected: -1


// is_power_of_two — handy for asserting hash table sizes, ring buffers.

print(math.is_power_of_two(64))   // expected: true

print(math.is_power_of_two(63))   // expected: false

print(math.is_power_of_two(1))    // expected: true

print(math.is_power_of_two(0))    // expected: false  (0 is NOT a power of two)


// next_power_of_two — smallest 2^k >= n. Saturates rather than

// overflowing — `next_power_of_two(1e100)` returns `Inf`, not garbage.

print(math.next_power_of_two(63))    // expected: 64

print(math.next_power_of_two(64))    // expected: 64

print(math.next_power_of_two(65))    // expected: 128

print(math.next_power_of_two(1))     // expected: 1

Constants

math.pi, math.tau, math.e, math.phi, math.epsilon, math.infinity and math.nan avoid hardcoding. math.tau (= 2π) is preferred for full-turn angles.

Using math.tau for turn fractions; math.epsilon for manual tolerance.

08-constants.zolo
Playground
// Feature: math constants — pi, tau, e, phi, epsilon, infinity, nan

// When to use: any code that needs these named values without

// hardcoding them. Tau (2π) is preferred for full-turn angles;

// epsilon for "is this close enough" comparisons; infinity/nan for

// sentinel checks.


use std::math

// Already-existing: pi.

print(math.pi)         // expected: 3.14...


// New constants:

print(math.tau)        // expected: 6.28...  (= 2 * pi)

print(math.e)          // expected: 2.718...

print(math.phi)        // expected: 1.618...  (golden ratio)

print(math.epsilon)    // expected: 2.22e-16  (f64 machine epsilon)

print(math.infinity)   // expected: inf

print(math.nan)        // expected: nan


// Tau is sometimes more natural than pi for full rotations:

//   full circle = 1 * tau = tau radians.

//   half circle = 0.5 * tau = pi radians.

fn full_turn(fraction: float) -> float {
  return math.tau * fraction
}
print(full_turn(0.5))    // expected: 3.14... (half turn)

print(full_turn(0.25))   // expected: 1.57... (quarter turn)


// Epsilon for approximate equality (sketch — proper FP comparison is

// trickier; this works for values near 1.0).

fn approx_eq(a: float, b: float) -> bool {
  return math.abs(a - b) < math.epsilon * 16.0
}
print(approx_eq(0.1 + 0.2, 0.3))   // expected: true (despite FP noise)

Approximate float comparison — the ~= operator

Comparing floats directly with == triggers the float-equality warning because 0.1 + 0.2 ≠ 0.3 in IEEE 754. The ~= operator uses adaptive tolerance by default; the within tol, within tol relative and within n ulps variants give precise control.

Plain ~=, absolute tolerance, relative tolerance and within; math.is_nan and math.is_infinite; safe_divide.

09-approx-eq.zolo
Playground
// Feature: approximate float equality via ~= operator or math.approx_eq* functions.

// When to use: any comparison between floats produced by arithmetic.

// Direct `==` triggers the `float-equality` lint warning because

// `0.1 + 0.2 ≠ 0.3` in IEEE 754. The ~= operator solves that for default tolerance.

// Default adaptive tolerance — combines absolute (near zero) and

// relative (large magnitude). Good for ~80% of cases.

use std::math

print(0.1 + 0.2 ~= 0.3)  // expected: true

print(1.0 + 0.1 - 0.1 ~= 1.0)  // expected: true (FP rearrangement noise)

print(1.0 ~= 2.0)  // expected: false


// Explicit absolute tolerance — for near-zero checks or known scale.

fn velocity_sample() -> float {
  return 0.0000001
}

print(velocity_sample() ~= 0.0 within 1e-6)  // expected: true

// Explicit relative tolerance — for values that span orders of

// magnitude. `0.001` ≈ 0.1% relative tolerance.

print(1e10 ~= 1.0000001e10 within 0.001 relative)  // expected: true

print(1.0 ~= 1.5 within 0.001 relative)  // expected: false

// NaN semantics — `~=` returns false for any NaN comparison.

// Use `math.is_nan` to check explicitly.

let nan = 0.0 / 0.0
print(nan ~= nan)  // expected: false

print(math.is_nan(nan))  // expected: true

// Infinity semantics — same sign approx-equal, opposite signs not.

print(math.huge ~= math.huge)  // expected: true

print(math.huge ~= -math.huge)  // expected: false

print(math.is_infinite(math.huge))  // expected: true


// Typical usage in a guard clause:

fn safe_divide(a: float, b: float) -> float? {
  if b ~= 0.0 within 1e-12 { return nil }
  return a / b
}

print(safe_divide(10.0, 2.0))  // expected: 5

print(safe_divide(10.0, 0.0))  // expected: nil

The complementary operator !~= inverts the result — ideal for guards and convergence loops:

Newton loop with !~= to detect non-convergence; assert_close function.

10-approx-eq-not.zolo
Playground
// Feature: `!~=` operator — approximate "not equal" between floats.

// When to use: guard clauses where you want to *act* when two floats

// have drifted apart, or assertion failures when convergence didn't happen.

//

// Semantically: `a !~= b` is exactly `not (a ~= b)`. Same tolerance rules.

// Use `!~=` for direct readability — `if x !~= y { ... }` reads better

// than `if not (x ~= y) { ... }` in conditionals.


use std::math

// Bare form (default adaptive tolerance)

print(1.0 !~= 2.0)                              // expected: true (clearly different)

print(1.0 !~= 1.0)                              // expected: false (exactly equal)

print((0.1 + 0.2) !~= 0.3)                      // expected: false (FP noise still within default)


// With absolute tolerance

print(1.0 !~= 1.0001 within 1e-9)               // expected: true (diff exceeds 1e-9)

print(1.0 !~= 1.0001 within 0.001)              // expected: false (within tolerance, so NOT not-eq)


// With relative tolerance

print(100.0 !~= 101.0 within 0.001 relative)    // expected: true (1% > 0.1% rtol)

print(100.0 !~= 100.05 within 0.001 relative)   // expected: false (0.05% < 0.1% rtol)


// Use case 1: detect when iterative process hasn't converged yet

var x = 1.0
var prev = 0.0
var iter = 0
while prev !~= x within 1e-10 {
  prev = x
  x = (x + 2.0 / x) * 0.5   // Newton step toward sqrt(2)

  iter = iter + 1
}
print("sqrt(2) ≈ {x} after {iter} iterations")    // expected: ≈ 1.4142... after a few iters


// Use case 2: assert convergence in a test/sanity check

fn assert_close(a: float, b: float, label: str) {
  if a !~= b within 1e-9 {
    print("FAIL {label}: {a} differs from {b}")
  } else {
    print("OK {label}")
  }
}
assert_close(0.1 + 0.2, 0.3, "fp_noise")           // expected: OK fp_noise

assert_close(math.sqrt(2.0) * math.sqrt(2.0), 2.0, "sqrt_roundtrip")  // expected: OK

When the comparison needs to be passed as a first-class value (e.g. to filter), use the equivalent functions:

math.approx_eq, math.approx_eq_abs, math.approx_eq_rel as arguments to filter.

11-approx-eq-functions.zolo
Playground
// Feature: `math.approx_eq*` function family — the function-call form

// of approximate float equality, parallel to the `~=` operator.

//

// When to use functions instead of the operator:

//   - **Higher-order usage**: pass the function as a value to map/filter/reduce.

//   - **Explicit naming in code review**: `math.approx_eq_rel(...)` makes the

//     tolerance mode obvious at the call site.

//   - **Compatibility / migration**: code coming from Python's `math.isclose`

//     or Julia's `isapprox` feels familiar.

//

// The operator (`~=`) is preferred for inline conditional checks. The functions

// are preferred when the comparison is a "first-class" operation in your code.


use std::math
use std::Array

// Equivalence: every operator form has a function form.

print((0.1 + 0.2) ~= 0.3)                              // operator

print(math.approx_eq(0.1 + 0.2, 0.3))                  // function — same result


print(1.0 ~= 1.0001 within 0.001)                      // operator

print(math.approx_eq_abs(1.0, 1.0001, 0.001))          // function — same result


print(100.0 ~= 100.05 within 0.001 relative)           // operator

print(math.approx_eq_rel(100.0, 100.05, 0.001))        // function — same result


// First-class usage: filter a list using the function form

let measurements = [0.301, 0.299, 0.5, 0.30000001, 0.7]
let near_target = measurements.filter(|m| math.approx_eq_abs(m, 0.3, 0.01))
print(near_target)    // expected: [0.301, 0.299, 0.30000001]


// Reduce / find: detect the first sample that matches a target within tolerance

fn first_matching(samples: [float], target: float, tol: float) -> float? {
  for s in samples {
    if math.approx_eq_abs(s, target, tol) { return s }
  }
  return nil
}
print(first_matching([1.0, 1.5, 1.999, 2.0001, 3.0], 2.0, 0.01))   // expected: 1.999


// Side-by-side semantic note: NaN behavior is identical between operator and function.

let nan = 0.0 / 0.0
print(nan ~= nan)                          // false

print(math.approx_eq(nan, nan))            // false

print(math.is_nan(nan))                    // true (use this for NaN checks)

Special float predicates — NaN, infinite, finite

Never test x == x for NaN or x == math.huge for infinity — use the dedicated predicates:

is_nan, is_infinite, is_finite; defensive pattern in area_of_circle.

12-nan-finite-infinite.zolo
Playground
// Feature: float predicates — `math.is_nan`, `math.is_finite`, `math.is_infinite`.

// When to use: classify a float without falling into the IEEE 754 traps:

//   - `x == x` is `false` when `x` is NaN — that's the only reliable NaN test.

//   - `x == math.huge` works for +infinity but you'd miss -infinity.

//   - `x !~= y within tol` doesn't tell you if `x` is special (NaN/Inf).

//

// These predicates encapsulate the right check for each case.


use std::math

// ── NaN detection ───────────────────────────────────────────────────

// NaN is produced by indeterminate operations: 0/0, sqrt(-1), inf - inf.

let nan_from_div = 0.0 / 0.0
let nan_from_sqrt = math.sqrt(-1.0)
let nan_from_inf = math.huge - math.huge

print(math.is_nan(nan_from_div))         // expected: true

print(math.is_nan(nan_from_sqrt))        // expected: true

print(math.is_nan(nan_from_inf))         // expected: true

print(math.is_nan(42.0))                 // expected: false

print(math.is_nan(math.huge))            // expected: false (infinity is NOT NaN)


// NaN propagates through every arithmetic operation:

print(math.is_nan(nan_from_div + 1.0))   // expected: true

print(math.is_nan(nan_from_div * 0.0))   // expected: true


// ── Infinity detection ──────────────────────────────────────────────

// `math.huge` is +infinity. `-math.huge` is -infinity.

// `math.is_infinite` matches both signs; `math.is_inf` is an alias kept

// for backward compatibility (introduced before the canonical name).

print(math.is_infinite(math.huge))       // expected: true

print(math.is_infinite(-math.huge))      // expected: true

print(math.is_inf(math.huge))            // expected: true (alias)

print(math.is_infinite(42.0))            // expected: false

print(math.is_infinite(nan_from_div))    // expected: false (NaN is not Inf)


// Operations that produce infinity:

print(math.is_infinite(1.0 / 0.0))       // expected: true (+inf)

print(math.is_infinite(-1.0 / 0.0))      // expected: true (-inf)


// ── Finite detection (the "everything is well-behaved" check) ───────

// `is_finite(x)` ≡ `not is_nan(x) and not is_infinite(x)`.

print(math.is_finite(42.0))              // expected: true

print(math.is_finite(0.0))               // expected: true

print(math.is_finite(-1e308))            // expected: true (large but representable)

print(math.is_finite(math.huge))         // expected: false

print(math.is_finite(nan_from_div))      // expected: false


// ── Typical defensive-coding pattern ────────────────────────────────

// A function that should never receive special values: validate up front.

fn area_of_circle(radius: float) -> float? {
  if !math.is_finite(radius) { return nil }   // reject NaN, ±inf

  if radius < 0.0 { return nil }
  return math.pi * radius * radius
}
print(area_of_circle(2.0))                   // expected: 12.566...

print(area_of_circle(-1.0))                  // expected: nil

print(area_of_circle(math.huge))             // expected: nil (would overflow)

print(area_of_circle(0.0 / 0.0))             // expected: nil (NaN input)


// ── Constants for direct comparison/return ──────────────────────────

// `math.nan` and `math.huge` are the constants. They're often useful

// as sentinel return values or "unset" markers in numeric APIs.

let unset: float = math.nan
print(math.is_nan(unset))                // expected: true (sentinel detected)

ULP comparison

For testing math implementations (sqrt, sin…) where the exact bit-distance matters:

within n ulps — strictest mode; 0.0 ~= -0.0 within 0 ulps returns true (IEEE 754).

13-approx-eq-ulps.zolo
Playground
// Feature: `~= within <n> ulps` — ULP-based approximate equality.

// When to use: when testing implementations of math operations

// (sqrt, sin, etc.) where you want bit-exact-modulo-N-ULPs comparison.

// "ULP" = "units in the last place" = bit-pattern distance for same-sign

// floats. This is the strictest of the three tolerance modes:

//   - within tol           (absolute: |a - b| <= tol)

//   - within tol relative  (relative: |a - b| <= tol * max(|a|, |b|))

//   - within n ulps        (bit-pattern: differ by at most n in their f64 bits)


use std::math

// ── Exact match: differs by 0 ULPs ──────────────────────────────────

print(1.0 ~= 1.0 within 0 ulps)                   // expected: true

print(2.5 ~= 2.5 within 0 ulps)                   // expected: true


// ── 1 ULP away ──────────────────────────────────────────────────────

// (0.1 + 0.2) is the classic FP noise case. The result differs from

// 0.3 by 1 ULP (~5.55e-17 at that magnitude). `within 0 ulps` fails,

// `within 1 ulps` (or more) succeeds.

print((0.1 + 0.2) ~= 0.3 within 0 ulps)           // expected: false (NOT bit-exact)

print((0.1 + 0.2) ~= 0.3 within 1 ulps)           // expected: true

print((0.1 + 0.2) ~= 0.3 within 4 ulps)           // expected: true (4 ulps is plenty)


// ── Distinctly different values ─────────────────────────────────────

print(1.0 ~= 2.0 within 4 ulps)                   // expected: false

print(1.0 ~= 1.001 within 100 ulps)               // expected: false (1.0 vs 1.001 is ~4.5e12 ULPs away)

print(1.0 ~= 1.0 + math.epsilon within 1 ulps)    // expected: true (1 ULP step at magnitude 1.0)


// ── NaN: always false ───────────────────────────────────────────────

let nan = 0.0 / 0.0
print(nan ~= nan within 1000 ulps)                // expected: false

print(nan ~= 1.0 within 1000 ulps)                // expected: false


// ── Opposite signs: never within ULPs ───────────────────────────────

// Bit patterns of negative floats are far from positive ones in u64 space,

// so opposite signs never match. (Exception: -0.0 == 0.0 is caught by the

// early `a == b` short-circuit.)

print(1.0 ~= -1.0 within 1000 ulps)               // expected: false

print(0.0 ~= -0.0 within 0 ulps)                  // expected: true (0 and -0 are equal in IEEE 754)


// ── Negation (!~=) ──────────────────────────────────────────────────

print(1.0 !~= 2.0 within 4 ulps)                  // expected: true (distinctly unequal)

print((0.1 + 0.2) !~= 0.3 within 4 ulps)          // expected: false (within tol → NOT not-eq)


// ── Comparison vs other tolerance modes ─────────────────────────────

// The three modes can give different verdicts on the same comparison.

// Example: 0.1 + 0.2 vs 0.3 (diff ≈ 5.55e-17 ≈ 1 ULP near 0.3):

print((0.1 + 0.2) ~= 0.3 within 1e-10)            // true (very loose absolute)

print((0.1 + 0.2) ~= 0.3 within 1e-18)            // false (tighter than the diff)

print((0.1 + 0.2) ~= 0.3 within 0.001 relative)   // true (very loose relative)

print((0.1 + 0.2) ~= 0.3 within 1 ulps)           // true (bit-exact 1 ULP)

print((0.1 + 0.2) ~= 0.3 within 0 ulps)           // false (strictest)


// ── Use case: sqrt round-trip accuracy test ─────────────────────────

// sqrt(2)^2 should equal 2.0 to within a handful of ULPs.

let two = math.sqrt(2.0) * math.sqrt(2.0)
print(two ~= 2.0 within 4 ulps)                   // expected: true

print(two ~= 2.0 within 0 ulps)                   // expected: probably false

                                                   // (the round-trip introduces tiny error)

decimal type — exact decimal arithmetic (~28 digits)

decimal (suffix d) is backed by rust_decimal. Exact arithmetic for money and accounting — no binary floating-point noise.

0.1d + 0.2d == 0.3d returns true; conversions with as; invoice total with decimal.

14-decimal-type.zolo
Playground
// Feature: `decimal` primitive type — exact decimal arithmetic.

// When to use: any context where binary float precision is wrong:

//   - money / financial calculations

//   - bookkeeping

//   - any code where `0.1 + 0.2 == 0.3` matters

//

// Decimal is backed by `rust_decimal` (~96-bit fixed-point). Arithmetic

// is EXACT for representable decimal values (up to ~28 significant digits).


// ── Literals: `d` suffix ────────────────────────────────────────────

let a: decimal = 0.1d
let b: decimal = 0.2d
print(a + b)        // expected: "0.3"  (NOT 0.30000000000000004 like float!)

print(a + b == 0.3d) // expected: true (no float-equality lint warning)


// Integer-form decimal literal

let price: decimal = 100d
print(price)        // expected: "100"


// ── Arithmetic operators ────────────────────────────────────────────

let x: decimal = 2.5d
let y: decimal = 1.5d
print(x + y)        // expected: "4.0"

print(x - y)        // expected: "1.0"

print(x * y)        // expected: "3.75"

print(x / y)        // expected: "1.6666..."  (default precision)


// ── Comparison operators (exact, no ~= needed) ──────────────────────

print(0.1d + 0.2d == 0.3d)       // expected: true (EXACT)

print(0.1d + 0.2d != 0.3d)       // expected: false

print(0.5d < 1.0d)               // expected: true

print(0.5d <= 0.5d)              // expected: true

print(1.0d > 0.5d)               // expected: true


// ── Conversions via `as` ────────────────────────────────────────────

// Decimal → float: precision may be lost (decimal has more digits than f64)

let from_d: float = 0.3d as float
print(from_d)                    // expected: 0.3


// Float → decimal: REVEALS the binary FP noise

// (0.1 in f64 isn't actually 0.1 — it's 0.10000000000000000555...)

let from_f: decimal = 0.1 as decimal
print(from_f)                    // expected: 0.1000000000000000055511151231


// Int → decimal: always exact

let from_i: decimal = 42 as decimal
print(from_i)                    // expected: "42"


// Decimal → int: truncates (no rounding)

let to_i: int = 99.9d as int
print(to_i)                      // expected: 99 (truncated, not rounded)


// ── Real-world example: simple invoice total ────────────────────────

fn invoice_total(items: [decimal], tax_rate: decimal) -> decimal {
  var subtotal: decimal = 0d
  for item in items {
    subtotal = subtotal + item
  }
  return subtotal + subtotal * tax_rate
}

let items: [decimal] = [19.99d, 4.50d, 12.00d]
let total = invoice_total(items, 0.08d)
print(total)                     // expected: exact arithmetic, e.g., "39.7892"

bigdecimal type — arbitrary precision

bigdecimal (suffix bd) has no fixed digit limit — only memory. Suitable for values beyond the ~28 digits of decimal.

40-digit integers; as bigdecimal reveals the binary noise of float.

15-bigdecimal-type.zolo
Playground
// Feature: `bigdecimal` primitive type — arbitrary-precision decimal arithmetic.

// When to use: when you need MORE than rust_decimal's ~28 significant digits:

//   - cryptographic / scientific computations

//   - financial calculations involving enormous sums (> 10^28)

//   - any code that needs to represent values like

//     1234567890123456789012345678901234567890 exactly

//

// BigDecimal is backed by the `bigdecimal` crate (arbitrary precision, heap-allocated).

// Unlike `decimal` (rust_decimal, ~96-bit / 28 digits), `bigdecimal` has no fixed

// upper limit — just heap memory.


// ── Literals: `bd` suffix ───────────────────────────────────────────

let a: bigdecimal = 0.1bd
let b: bigdecimal = 0.2bd
print(a + b)          // expected: "0.3"  (exact, no floating-point noise)

print(a + b == 0.3bd) // expected: true


// Integer-form bigdecimal literal

let price: bigdecimal = 100bd
print(price)          // expected: "100"


// ── Arbitrary precision — beyond rust_decimal's 28-digit limit ──────

// rust_decimal would panic or lose precision here. bigdecimal handles it.

let big: bigdecimal = 1234567890123456789012345678901234567890bd
let one: bigdecimal = 1bd
print(big + one)      // expected: "1234567890123456789012345678901234567891"


// ── Arithmetic operators ─────────────────────────────────────────────

let x: bigdecimal = 2.5bd
let y: bigdecimal = 1.5bd
print(x + y)          // expected: "4"

print(x - y)          // expected: "1"

print(x * y)          // expected: "3.75"

print(x / y)          // expected: "1.6666..." (extended precision)


// ── Comparison operators (exact, no ~= needed) ───────────────────────

print(0.1bd + 0.2bd == 0.3bd)    // expected: true (EXACT)

print(0.1bd + 0.2bd != 0.3bd)    // expected: false

print(0.5bd < 1.0bd)             // expected: true

print(0.5bd <= 0.5bd)            // expected: true

print(1.0bd > 0.5bd)             // expected: true


// ── Conversions via `as` ─────────────────────────────────────────────

// BigDecimal → float: precision may be lost

let from_bd: float = 0.3bd as float
print(from_bd)                   // expected: 0.3


// Float → bigdecimal: REVEALS the binary FP noise

let from_f: bigdecimal = 0.1 as bigdecimal
print(from_f)                    // expected: something like 0.1000000000000000055511...


// Int → bigdecimal: always exact

let from_i: bigdecimal = 42 as bigdecimal
print(from_i)                    // expected: "42"


// BigDecimal → int: truncates (no rounding)

let to_i: int = 99.9bd as int
print(to_i)                      // expected: 99 (truncated)


// BigDecimal ↔ Decimal cross-cast

let d: decimal = 3.14d
let bd_from_d: bigdecimal = d as bigdecimal
print(bd_from_d)                 // expected: "3.14"

Methods on decimal and bigdecimal

Both types expose .round(n), .ceil(), .floor(), .trunc() and .scale() directly on the value:

d.round(2), neg.ceil(), bd.floor() — controlled rounding on both types.

16-decimal-methods.zolo
Playground
// Feature: decimal/bigdecimal methods — round, ceil, floor, trunc, scale.


let d: decimal = 3.14159d
print(d.round(2))    // expected: "3.14"

print(d.round(4))    // expected: "3.1416"

print(d.ceil())      // expected: "4"

print(d.floor())     // expected: "3"

print(d.trunc())     // expected: "3"

print(d.scale())     // expected: 5


let neg: decimal = -2.7d
print(neg.ceil())    // expected: "-2"

print(neg.floor())   // expected: "-3"

print(neg.trunc())   // expected: "-2"


let bd: bigdecimal = 99.999999bd
print(bd.round(2))   // expected: "100.00" or "100"

print(bd.ceil())     // expected: "100"

print(bd.floor())    // expected: "99"

print(bd.trunc())    // expected: "99"

print(bd.scale())    // expected: 6


let neg_bd: bigdecimal = -1.5bd
print(neg_bd.ceil())   // expected: "-1"

print(neg_bd.floor())  // expected: "-2"

print(neg_bd.trunc())  // expected: "-1"

Method syntax on the float type

The predicates and comparison functions from the math module are also available as methods directly on a float value:

x.is_nan(), (-3.14).abs(), (0.1 + 0.2).approx_eq(0.3) — chained form.

17-float-methods.zolo
Playground
// Feature: methods on float type (Phase 3 reduced).

// Same semantics as math.* stdlib functions; method syntax for readability.

// x.is_nan()         → math.is_nan(x)

// x.is_finite()      → math.is_finite(x)

// x.is_infinite()    → math.is_infinite(x)

// x.abs()            → math.abs(x)

// x.abs_diff(y)      → math.abs_diff(x, y)

// x.approx_eq(y)     → math.approx_eq(x, y)

// x.approx_eq_abs(y, tol)   → math.approx_eq_abs(x, y, tol)

// x.approx_eq_rel(y, rtol)  → math.approx_eq_rel(x, y, rtol)

// x.approx_eq_ulps(y, n)    → math.approx_eq_ulps(x, y, n)


use std::math

let x: float = 1.0
let nan: float = 0.0 / 0.0

// Predicates

print(x.is_nan())          // false

print(nan.is_nan())        // true

print(x.is_finite())       // true

print(nan.is_finite())     // false

print(math.huge.is_infinite())  // true


// Arithmetic

print((-3.14).abs())       // 3.14

print(x.abs_diff(2.0))     // 1.0


// Approximate equality

print((0.1 + 0.2).approx_eq(0.3))               // true (default tol)

print((0.1 + 0.2).approx_eq_abs(0.3, 1e-9))     // true

print(1.0.approx_eq_rel(1.01, 0.001))            // false (1% > 0.1%)

print((0.1 + 0.2).approx_eq_ulps(0.3, 4))       // true

Challenge

Implement a function normalize_score(raw: float) -> float that maps raw from [0, 100] to [0, 1] using math.remap, then verify with ~= that normalize_score(50.0) ~= 0.5.

enespt-br