Numbers
The class-backed numeric primitives and the math that works on every number type.
Generated by
bin/build_library_doc.pyfrom the kernel sources. Do not edit by hand: change the generator, or the doc comments inkernel/src/, and re-run it.
Numeric types
UNION NumStore
number is an ergonomic unified numeric — an integer XOR a float, never
both and never neither — backed by this compiler-known value-class. It is
NEVER dev-instantiated (there is no CREATE Number); it is driven entirely
by the number primitive surface and by literals. It is copy-by-value (a
value-class, NOT opaque's move-only model).
Storage is a NumberKind tag (INTEGER | UNSIGNED | FLOAT) selecting one arm of
a raw NumStore UNION (int64 XOR uint64 XOR float64). The tag makes every access
safe — the unsafe union read is encapsulated entirely behind this value-class
surface (the north-star "relocate the cost inward" tiebreak). The UNSIGNED arm
(number-unsigned-arm-design.md, #38) is value-classified, not surface-typed: it
is reached only when an integer overflows the int64 arm into (int64_max,
uint64_max], giving full [int64_min, uint64_max] integer fidelity.
See number-and-complex-design.md and ENVZN_CONSTITUTION.md I.D.i(g). NumberKind (the INTEGER|FLOAT subtype tag) lives in enums.ev per the kernel convention that all ENUMs are declared there (E9031).
kernel/src/Number.ev:34
Fields
int64 iuint64 ufloat64 f
CLASS Number
kernel/src/Number.ev:41
Fields
NumberKind tagNumStore store
Constructors
INIT()
Default-initialize to zero, INTEGER subtype (constitution I.D.i(b): every
numeric primitive default-inits to zero). Without this the synthesized
zero-init would leave tag at 0 — not the INTEGER case — since kernel
enums start at 1.
INIT(int64 v)
INIT(uint64 v)
Widen-in from an unsigned 64-bit value. Value-classified by the ladder:
a magnitude that still fits the signed arm (v ≤ int64_max) is stored as
INTEGER so it compares/prints identically to the same int literal; only a
value in (int64_max, uint64_max] takes the UNSIGNED arm. This is the total
uint64 INTO number edge (no fallible AS needed — every uint64 fits).
INIT(float64 v)
Methods
METHOD kind() RETURNS NumberKind
The subtype this number currently holds — INTEGER, UNSIGNED, or FLOAT. The
V1 introspection surface (the typed, MATCH-able stand-in for V2 runtime
WHEN n IS int64). A pure tag read; never touches the union arm.
METHOD equals(number other) RETURNS boolean
Value-equality across subtypes (the Equatable surface a == b lowers to).
Any float-subtype operand compares by IEEE value (the integer arm promoted
to double) — so number(3) == number(3.0) is TRUE, number(0.1+0.2) ==
number(0.3) is FALSE. Two integer-kind operands are equal only when they
share an arm: INTEGER (≤ int64_max) and UNSIGNED ((int64_max, uint64_max])
hold DISJOINT value ranges, so a cross-arm compare is FALSE without ever
comparing signed to unsigned bits (no UB). Tolerance is ≈; bit-exact is
BEQUALS. (Decision 2026-06-23: float == = IEEE value.)
METHOD isLessThan(number other) RETURNS boolean
Ordering by mathematical value across arms (the a < b surface). Mirrors
equals: any float-subtype operand compares by IEEE value, so
number(2) < number(2.5) is TRUE. Two integer-kind operands compare in
the int128 domain, which unifies the INTEGER and UNSIGNED arms into one
signed-wide value — so a cross-arm compare is ORDERED (unlike equality,
where the disjoint ranges make it simply FALSE) and never compares signed
to unsigned bits.
NaN: the float path delegates to float64's own <, so every comparison
against a NaN is FALSE — the same answer float64 gives. That is why the
four operators derive by SWAPPING operands and never by negating: NOT (b
< a) would make NaN <= x TRUE. (I.D.g.v, ordering added 2026-09-02.)
METHOD isLessThanOrEqual(number other) RETURNS boolean
a <= b. A separate method rather than NOT isLessThan(b, a) for the
NaN reason in isLessThan above: negation flips NaN's correct FALSE into
a wrong TRUE. > and >= are the operand-swapped forms of these two.
METHOD __op_plus__(number other) RETURNS number
Arithmetic. Any float-subtype operand → float result (the integer arm
promoted to double — the common float path, unchanged). Otherwise both are
integer-kind: compute in int128 so int64 overflow is seen, then classify
down the ladder — so number overflow promotes (INTEGER→UNSIGNED→FLOAT)
instead of wrapping. */^ whose result exceeds int128 drop to FLOAT.
METHOD __op_minus__(number other) RETURNS number
METHOD negate() RETURNS number
Negation — -n lowers to this (number has no C++ unary operator-).
A FLOAT negates in float64, so -0.0 stays a signed zero; an integer kind
negates in int128 and classifies back down the ladder, so negating a
uint64 past int64's range promotes instead of wrapping — the same rule
0 - n follows.
METHOD __op_times__(number other) RETURNS number
Multiply. Integer-kind operands compute in int128; the one integer op whose
product can exceed int128 (near-uint64_max × near-uint64_max ≈ 2^128), so a
recover-and-check (a≠0 ∧ p/a≠b ⇒ the multiply wrapped) drops to FLOAT.
METHOD __op_divide__(number other) RETURNS number
METHOD __op_floordiv__(number other) RETURNS number
Floor division (~/) — floors toward negative infinity (vs / which
truncates). Integer-kind → int128 floor then classify; any float → float
floor. Division shrinks magnitude so it never overflows the int128 domain
(int64_min ~/ -1 lands at 2^63, which the ladder classifies UNSIGNED).
METHOD __op_power__(number other) RETURNS number
Power (^). Any float operand → float result. Integer-kind: a negative
exponent is fractional → FLOAT; otherwise iterate in int128, dropping to
FLOAT the moment a multiply would exceed int128. Bases 0/1/−1 are handled
directly so a huge exponent can't spin the loop (|base|≥2 overflows within
~127 steps).
METHOD isClose(number other, float64 rtol, float64 atol) RETURNS boolean
Approximate equality (the ≈ / ≉ operator lowers to this). Two
int-subtype operands compare EXACTLY — integers carry no representation
error, so ≈ adds nothing there. Otherwise (any float involved) the
principled tolerance applies: |a − b| ≤ max(rtol·max(|a|,|b|), atol).
The atol floor is what makes comparison to zero work. Operator defaults
come from the desugar; this method is also the explicit-tolerance surface.
CLASS Complex
The complex primitive's backing value-class.
complex is one complex-number type, backed by this compiler-known value-class
and NEVER dev-instantiated (no CREATE Complex); it is driven entirely by the
complex primitive surface and the real + coeff im literal form. Copy-by-value.
Internally it is a pair of numbers — the real and imaginary components — each
keeping its own int/float subtype, so a single complex spans both Gaussian-
integer complexes (3 + 4im, integer components) and float complexes
(3.0 + 4.0im), and even mixed ones (3 + 4.0im). Per-component arithmetic is
just number arithmetic, which is why complex is built ON number.
The field is named imag (not im) because im is the reserved imaginary-unit
token; the public accessors are .real / .imaginary (each a number), added
in a later cycle alongside .conjugate / .magnitude / .phase.
See number-and-complex-design.md Part 2 and ENVZN_CONSTITUTION.md I.D.i(g).
kernel/src/Complex.ev:38
Fields
number renumber imag
Constructors
INIT(number re, number imag)
Methods
METHOD equals(complex other) RETURNS boolean
Component-wise value-equality (the Equatable surface a == b lowers to).
Two complexes are equal iff both components are equal as numbers — so
(3 + 4im) == (3 + 4im). There is NO ordering on complex (</> are a
compile error): the complex field is not ordered.
METHOD __op_plus__(complex other) RETURNS complex
Addition / subtraction — component-wise, the reals with the reals and the
imaginaries with the imaginaries: (8 + 6im) = (3 + 4im) + (5 + 2im).
Each component op is ordinary number arithmetic; the result reassembles
through the rp + ip im literal form (fused at parse time, so this does
not recurse into the very operator being defined).
METHOD __op_minus__(complex other) RETURNS complex
METHOD __op_times__(complex other) RETURNS complex
Multiplication: (a + bi)(c + di) = (ac − bd) + (ad + bc)i. The cross terms
are what make im² = −1 fall out — (0 + 1im) * (0 + 1im) = (−1 + 0im).
METHOD real() RETURNS number
The real component (a in a + bi), as a number.
METHOD imaginary() RETURNS number
The imaginary component (b in a + bi), as a number.
METHOD conjugate() RETURNS complex
The complex conjugate — flips the sign of the imaginary part:
conjugate(a + bi) = a − bi. Multiplying a value by its conjugate gives a
real ((a+bi)(a−bi) = a² + b²), which is why division scales by it.
METHOD magnitude() RETURNS number
Magnitude (modulus) |a + bi| = √(a² + b²) — the distance from the origin,
always a non-negative real. Computed in float64 via Math.sqrt, so the
result is a float-subtype number. (Complex depends on Math here.)
METHOD phase() RETURNS number
Phase (argument) of a + bi = atan2(b, a) — the angle in radians from the
positive real axis, in (−π, π]. A float-subtype number.
METHOD negate() RETURNS complex
Negation — -c lowers to this: −(a + bi) = (−a) + (−b)i. (Unary minus on
a complex routes here; complex has no C++ unary operator-.)
METHOD __op_divide__(complex other) RETURNS complex
Division: (a + bi)/(c + di) = [(ac + bd) + (bc − ad)i] / (c² + d²).
Both components are scaled by the real denominator |other|². With integer
components this follows number's truncating integer /; float-component
complexes divide exactly — use those when an exact quotient is wanted.
CLASS Decimal128
The decimal128 primitive's backing value-class (V1, DESIGN_QUEUE #40).
decimal128 is an IEEE 754-2008 exact base-10 numeric, backed by this
compiler-known value-class. NEVER dev-instantiated (no CREATE Decimal128 in
user code); driven by the decimal128 surface, literals, and AS/INTO. Copy-by-
value, blittable. Exact + cohort-normalized ⇒ Comparable AND Hashable.
Storage is { int128 coefficient; int32 exponent }: value = coefficient × 10^exponent. int128 holds ~38 decimal digits, ≥ the IEEE decimal128 34-significant-digit requirement. The hard decimal machinery lives here (north-star "relocate the cost inward"); the dev surface is a plain primitive.
ROUNDING CONTEXT: a RoundingMode enum + the single decideUp decision
funnel parametrize the reducers (reduced/mulReduce/divReduce) by (precision,
mode); the bare operators forward (34, HALF_EVEN) so their behavior is
unchanged. Public surface: roundTo(places[, mode]) (decimal-place rounding)
and add/subtract/multiply/divide(other, precision, mode) (explicit
significant-digit context). See decimal128-design.md.
kernel/src/Decimal128.ev:30
Fields
int128 coefficientint32 exponent
Constructors
INIT()
Default-initialize to zero (constitution I.D.i(b)).
INIT(int128 coeff, int32 exp)
Construct from a (coefficient, exponent) pair: value = coefficient × 10^exponent. The form the literal/conversion/arithmetic lowering targets.
Methods
METHOD powTen(int32 n) RETURNS int128
10^n as int128, for 0 ≤ n ≤ 38 (callers keep n in range). n < 0 → 1.
METHOD digitCount(int128 v) RETURNS int32
Decimal digit count of |v| (zero counts as 1 digit). Overflow-safe (uses ~/).
METHOD decideUp(int32 cmpHalf, boolean nonzeroDropped, boolean coeffOdd, boolean neg, RoundingMode mode) RETURNS boolean
Single source of truth for "round the retained magnitude UP by one?" across all seven RoundingMode values, given a cohort-free description of what was dropped below the retained coefficient: - cmpHalf — the dropped fraction vs ½ ULP of the last retained digit: +1 above ½, 0 exactly ½ (a tie), −1 below ½. - nonzeroDropped — TRUE iff ANY nonzero digit was dropped (the directed modes UP/CEILING/FLOOR round on any remainder, not just at the half). - coeffOdd — parity of the retained coefficient (HALF_EVEN tie-break). - neg — sign of the value (CEILING/FLOOR are sign-directed). Every reducing path (reduced / mulReduce / divReduce / roundedDrop) funnels its rounding decision through here, so the bare operators' fixed (34, HALF_EVEN) behavior and the explicit-mode surface share one rule and cannot drift.
METHOD roundedDrop(boolean neg, int128 mag, int32 drop, RoundingMode mode) RETURNS int128
Drop the drop lowest decimal digits (drop ≥ 1) from an EXACT magnitude
mag, rounding per mode via decideUp, and return the rounded
magnitude. The dropped part has no sticky tail (mag is exact), so the
tie test is just remainder-vs-half. Used by roundTo (decimal-place
rounding); the sig-digit reducers compute cmpHalf inline because they
also carry a sticky direction.
METHOD reduced(boolean neg, int128 mag, int32 exp, int32 stickyDir, int32 precision, RoundingMode mode) RETURNS decimal128
Build a decimal128 from a sign + magnitude + exponent, rounding the
magnitude to ≤ precision significant digits with the given mode.
stickyDir carries information about digits below mag already dropped:
+1 = the true value is slightly ABOVE mag (round up at a tie), −1 =
slightly BELOW (round down at a tie), 0 = mag is exact (tie → per mode).
Bare +/− pass (34, HALF_EVEN) so their behavior is unchanged.
METHOD addMagnitudes(int128 ca, int32 ea, int128 cb, int32 eb, int32 precision, RoundingMode mode) RETURNS decimal128
Sum of two decimals by sign+magnitude. Aligns the larger-exponent operand UP exactly (bounded so it fits int128), rounds the smaller operand DOWN to the working exponent capturing a tri-state sticky, combines by sign, then reduces to 34 digits. Subtraction's guard-borrow is handled by the sticky sign (a rounded-off subtrahend makes the true result slightly smaller).
METHOD pairDigits(int128 hPart, int128 lPart) RETURNS int32
Product of two magnitudes (each < 10^34) at the given exponent, reduced to ≤ 34 digits with round-half-even. Avoids a 256-bit binary intermediate by splitting each coefficient at 10^17 into halves whose pairwise products all fit int128 (< 10^34 < 2^113); the exact 68-digit product is carried as a (high, low) decimal pair and reduced digit-by-digit with a sticky tail. Significant-digit count of a value carried as a (high, low) decimal pair hPart·10^34 + lPart, with lPart < 10^34. When the high part is nonzero its lowest digit sits at decimal position 34, so the pair spans digitCount(hPart) + 34 digits; otherwise the count is just lPart's. The 34 here is the structural split exponent (10^34), independent of the target precision.
METHOD mulReduce(boolean neg, int128 ma, int128 mb, int32 exp, int32 precision, RoundingMode mode) RETURNS decimal128
METHOD divReduce(boolean neg, int128 ma, int128 mb, int32 exp, int32 precision, RoundingMode mode) RETURNS decimal128
Quotient of two magnitudes (mb ≠ 0) at the given exponent, to 34
significant digits with round-half-even. Schoolbook long division: take the
integer quotient, then emit fractional digits one at a time (each step
rem*10 < 10^35 stays in int128) until 34 significant digits or an exact
remainder, then one guard digit + sticky for the final rounding.
METHOD equals(decimal128 other) RETURNS boolean
Cohort-normalized VALUE equality (the Equatable surface a == b): two
decimals are equal iff their numeric values match, regardless of cohort —
1.0 == 1.00 == 1. Computed as (a − b) having a zero coefficient (the
subtraction aligns + reduces exactly), so different representations of the
same value compare equal.
METHOD isLessThan(decimal128 other) RETURNS boolean
Strict less-than (the Comparable surface): the sign of (a − b).
METHOD hash() RETURNS uint64
Cohort-invariant hash (the Hashable surface): hashes the CANONICAL form (coefficient with trailing zeros stripped + adjusted exponent), so equal values hash equal — the invariant that makes decimal128 a sound Dictionary/ Set key. FNV-1a over the canonical (coefficient, exponent). The int128 → uint64 folds are inline unchecked narrowings (Decimal128 is a floor primitive, emitted before NumericUtilities, so it can't call it).
METHOD __op_plus__(decimal128 other) RETURNS decimal128
METHOD __op_minus__(decimal128 other) RETURNS decimal128
METHOD negate() RETURNS decimal128
Negation — -d lowers to this (decimal128 has no C++ unary operator-).
Flip the coefficient's sign and keep the exponent: exact, never rounded,
and the cohort survives — -(1.50) is -1.50, which 0 - d through
addMagnitudes would not promise.
METHOD checkedExp(int64 e) RETURNS int32
Narrow a working exponent (computed in int64 to dodge int32 overflow UB)
back to the int32 exponent field, raising a catchable MathError when the
scale overflows int32 — decision 2's "PANIC MathError on overflow".
Reachable: d = d * d doubles the exponent each step, so ~32 squarings
overflow. Decimal128 is emitted before the MathError class, so it cannot
PANIC MathError directly — it routes through the int128 floor-division
domain trap (INT64_MIN ~/ −1 → "floor division result out of range"), the
only MathError raiser reachable from a floor primitive. (The message is
the generic floor-div one; an exponent-specific message would need the
MathError class reordered ahead of Decimal128 — see decimal128-design.md.)
METHOD __op_times__(decimal128 other) RETURNS decimal128
METHOD __op_divide__(decimal128 other) RETURNS decimal128
METHOD clampPrecision(int32 p) RETURNS int32
Clamp a caller-supplied precision to the type's valid range [1, 34]: decimal128 carries at most 34 significant digits, and a precision below 1 is meaningless. Out-of-range requests are pinned to the nearest bound rather than rejected — the result is always a well-formed decimal128.
METHOD roundTo(int32 places, RoundingMode mode) RETURNS decimal128
Round to places digits after the decimal point, ties broken per mode.
Negative places rounds to tens / hundreds / … (place −2 → nearest 100).
A value already at this granularity or coarser is returned unchanged.
This is decimal-PLACE rounding (the everyday money/measurement operation);
significant-digit control is the precision arg on the arithmetic methods.
METHOD roundTo(int32 places) RETURNS decimal128
roundTo with the default HALF_EVEN (banker's) tie-break.
METHOD add(decimal128 other, int32 precision, RoundingMode mode) RETURNS decimal128
Sum reduced to precision significant digits, ties per mode — the
explicit-context form of +.
METHOD subtract(decimal128 other, int32 precision, RoundingMode mode) RETURNS decimal128
Difference reduced to precision significant digits, ties per mode —
the explicit-context form of −.
METHOD multiply(decimal128 other, int32 precision, RoundingMode mode) RETURNS decimal128
Product reduced to precision significant digits, ties per mode — the
explicit-context form of *.
METHOD divide(decimal128 other, int32 precision, RoundingMode mode) RETURNS decimal128
Quotient to precision significant digits, ties per mode — the
explicit-context form of /. Divide-by-zero raises MathError through
divReduce's floor-division, exactly as / does.
METHOD absOf(decimal128 v) RETURNS decimal128
|v| — magnitude as a decimal128 (negate the coefficient, keep the exponent). The cohort is irrelevant to closeTo (it compares values), so no normalization is needed.
METHOD closeTo(decimal128 other) RETURNS boolean
Approximate equality — the ≈ (U+2248) / ≉ (U+2249) surface. decimal128
is EXACT and 34 significant digits wide, so the number type's √machine-eps
epsilon (1.5e-8, sized for float64) is far too coarse here. closeTo is
RELATIVE (a fraction of the larger magnitude): the default tolerance agrees
to ~30 of 34 significant digits — it absorbs a few ULP of rounding noise
from /, AS, … without calling genuinely different values close. Use
->isClose(other, rtol, atol) for a caller-chosen tolerance.
METHOD isClose(decimal128 other, decimal128 rtol, decimal128 atol) RETURNS boolean
closeTo with caller-chosen relative + absolute tolerances (the principled-
tolerance companion, parallel to number->isClose): TRUE iff
|a − b| ≤ max(rtol·max(|a|,|b|), atol). rtol scales with the larger
operand magnitude (the relative term); atol is the absolute floor near
zero (where a relative test alone never matches a non-zero value).
CLASS DecimalText
kernel/src/DecimalText.ev:17
Methods
METHOD parseDecimalParts(REFERENCE String s) RETURNS | DecimalParts
Scan EXACT decimal text into its (coefficient, exponent) parts — an optional sign, integer digits, and an optional '.' with fractional digits ("19.99", "-0.01", "100", "+5.0"). The coefficient is every digit as a signed int128; the exponent is −(fractional digit count), so the text's scale is preserved ("1.00" → coefficient 100, exponent −2). Fallible: empty input, a stray character, a second '.', no digits, or int128-overflow of the coefficient.
Math
CLASS Math
NAMESPACE of standard math functions and constants.
Math is a stateless NAMESPACE (the mirror of a STRUCT: functions +
CONSTANT data, no instance), accessed via Math.methodName(args)
and Math.PI. The namespace declares the float64 constants
(PI, E, …) plus a wide method surface
covering absolute value, roots/exponentiation, logarithms,
trigonometry, rounding, sign, min/max/clamp, angle conversion,
overflow-aware integer arithmetic, and geometry helpers.
The standard math functions are bound to their C symbols in
<math.h> (and abs in <stdlib.h>) via FOREIGN BIND (V1
Part E) — see the bind block below. Domain validation (negative
roots, non-positive logarithms, out-of-range inverse-trig
arguments) stays in Envzn; each bind performs only the raw
computation.
────────────────────────────────────────────────────────────────── OVERLOAD CONVENTIONS ──────────────────────────────────────────────────────────────────
Envzn has no user generics, so methods that the spec describes
generically over a numeric T (min(T, T), wrappingAdd(T, T),
etc.) appear here as one concrete overload per primitive type:
- For
min/max/clamp: int32, int64, float32, float64 overloads (4 per method). - For
saturatingAdd/checkedAdd: every integer primitive — int8..int64, uint8..uint64 (16 per method). Float overflow follows IEEE 754 semantics; no checked variant. (WRAPPING arithmetic is not a method — it is the operators&+ &- &*of I.F.ii(a.iv).) - For
abs/sign: int32, int64, float32, float64 overloads.
To keep this design preview reasonable in length, only the int32 and float64 overloads are spelled out below for the multi-overload families; production Math.ev expands every primitive variant. The expansion is mechanical.
────────────────────────────────────────────────────────────────── ERROR HANDLING ──────────────────────────────────────────────────────────────────
Domain errors PANIC MathError:
- sqrt, cbrt of a negative real
- log, log2, log10 of a non-positive value
- asin, acos of a value outside [-1, 1]
- pow(0, x) for x < 0
Integer overflow on plain + / - / * PANICs (MathError,
I.F.ii(a.iii)) — it never silently wraps. The explicit alternatives
are the wrapping operators &+ &- &* (modular), and the
saturating / checked method variants below.
kernel/src/Math.ev:96
Fields
float64 PIfloat64 TAUfloat64 Efloat64 PHIfloat64 EMfloat64 SQRT2float64 LN2
Methods
METHOD abs(int32 x) RETURNS int32
ABSOLUTE VALUE — overloads per primitive
METHOD abs(float64 x) RETURNS float64
METHOD isNaN(float64 x) RETURNS boolean
TRUE iff x is NaN — the only value not equal to itself. The language
forbids authoring NaN as a literal (a NaN/Infinity is a computation
RESULT, never developer-assignable, I.C), so this is how a value that
can ARISE is TESTED — retiring the hand-rolled v != v idiom.
METHOD isNaN(float32 x) RETURNS boolean
METHOD isFinite(float64 x) RETURNS boolean
TRUE iff x is finite (neither ±infinity nor NaN). x - x is 0 for any
finite x and NaN for ±inf/NaN, so (x - x) == 0 is the finiteness test —
retiring the hand-rolled (f - f) == 0 idiom.
METHOD isFinite(float32 x) RETURNS boolean
METHOD squareRoot(float64 x) RETURNS complex
Square root over the reals AND into the complex plane — RETURNS complex
so a negative argument yields a pure-imaginary result instead of failing:
squareRoot(9) = (3 + 0im) (x ≥ 0 → real component)
squareRoot(-4) = (0 + 2im) (x < 0 → imaginary component)
This is the Math↔complex integration: complex c = Math.squareRoot(-4)
is a plain complex assignment. (Math depends on the complex type here; the
edge is one-way — Complex's own magnitude/phase use the bare FOREIGN math
binds, not this namespace, so there is no Math↔Complex cycle.)
METHOD cubeRoot(float64 x) RETURNS float64
METHOD exponent(float64 base, float64 exp) RETURNS | float64
METHOD eulersExponent(float64 x) RETURNS float64
METHOD loge(float64 x) RETURNS | float64
LOGARITHMS — x must be > 0; FAILURE STATUS otherwise.
METHOD log2(float64 x) RETURNS | float64
METHOD log10(float64 x) RETURNS | float64
METHOD sin(float64 x) RETURNS float64
TRIGONOMETRY — radians; asin/acos domain-checked.
METHOD cos(float64 x) RETURNS float64
METHOD tan(float64 x) RETURNS float64
METHOD asin(float64 x) RETURNS | float64
METHOD acos(float64 x) RETURNS | float64
METHOD atan(float64 x) RETURNS float64
METHOD atan2(float64 y, float64 x) RETURNS float64
METHOD sinh(float64 x) RETURNS float64
METHOD cosh(float64 x) RETURNS float64
METHOD tanh(float64 x) RETURNS float64
METHOD floor(float64 x) RETURNS float64
ROUNDING — float64 in, float64 out
METHOD ceiling(float64 x) RETURNS float64
METHOD round(float64 x) RETURNS float64
METHOD truncate(float64 x) RETURNS float64
METHOD sign(int32 x) RETURNS int32
SIGN — returns -1, 0, or 1 as int32 regardless of input type
METHOD sign(float64 x) RETURNS int32
METHOD min(int32 a, int32 b) RETURNS int32
MIN / MAX / CLAMP — overloads per numeric primitive
Spelled out for int32 + float64 in this preview; production Math.ev expands int64 / float32 variants identically.
METHOD min(float64 a, float64 b) RETURNS float64
METHOD max(int32 a, int32 b) RETURNS int32
METHOD max(float64 a, float64 b) RETURNS float64
METHOD clamp(int32 v, int32 lo, int32 hi) RETURNS int32
METHOD clamp(float64 v, float64 lo, float64 hi) RETURNS float64
METHOD toRadians(float64 deg) RETURNS float64
ANGLE CONVERSION
METHOD toDegrees(float64 rad) RETURNS float64
METHOD saturatingAdd(int32 a, int32 b) RETURNS int32
OVERFLOW-AWARE INTEGER ARITHMETIC
Two flavors per integer primitive (the WRAPPING flavor is now the
operators &+ &- & of I.F.ii(a.iv), lowered to the ev_wrap_
int32 overloads spelled out; production Math.ev expands all 8 integer primitives.
METHOD checkedAdd(int32 a, int32 b) RETURNS | int32
METHOD hypotenuseLength(float64 x, float64 y) RETURNS float64
GEOMETRY
CLASS NumericLimits
NAMESPACE of the numeric limits and the layout constants the conversion hosts read.
A conversion body used to spell its bounds as literals — v > 127,
v > 0x7F, iv > 9223372036854775807 — and a literal's type was decided
twice, once by the analyzer and once by the emitter's magnitude ladder
(gh #268). A CONSTANT has one declared type, so a bound read from here
arrives typed exactly once. The four conversion hosts
(PrimitiveConversions, NumberConversions, DecimalConversions,
ComplexConversions) contain no numeric literal at all; every value they
compare against or count with is a member of this namespace, reached as
NumericLimits.INT8_MAX.
The <stdint.h> limits keep the names C gave them and are BOUND, not
declared: INT8_MIN is a macro, so a member of that name would be
rewritten by the preprocessor before clang saw a declaration. The bind
mechanism of §17 reads each macro once into a member of this namespace
(I.K.ii.d). Everything else here is an ordinary CONSTANT, typed as the
site that reads it needs — a binary mask for a byte extraction, an
int32 bias for an int32 exponent, a float64 bound for a float
range check — so no site casts.
kernel/src/NumericLimits.ev:33
Fields
int8 INT8_MINint8 INT8_MAXint16 INT16_MINint16 INT16_MAXint32 INT32_MINint32 INT32_MAXint64 INT64_MINint64 INT64_MAXuint8 UINT8_MAXuint16 UINT16_MAXuint32 UINT32_MAXuint64 UINT64_MAXint128 INT128_MINint128 INT128_MAXuint32 CODE_POINT_MAXuint16 SURROGATE_FIRSTuint16 SURROGATE_LASTfloat64 INT32_MIN_F64float64 INT32_MAX_F64float64 UINT8_MAX_F64float64 UINT16_MAX_F64float64 CODE_POINT_MAX_F64int32 INT32_ZEROint32 INT32_ONEint64 INT64_ZEROint64 INT64_ONEuint64 UINT64_ZEROuint64 UINT64_ONEint128 INT128_ZEROint128 INT128_ONEint128 INT128_TENfloat64 FLOAT64_ZEROfloat64 FLOAT64_TENbinary BYTE_MASKint64 BITS_PER_BYTEint64 BYTES_PER_HALFint64 LAST_BYTE_INDEXint64 HALF_WIDTH_BITSint32 DECIMAL128_EXPONENT_BIASint64 DECIMAL128_EXPONENT_SHIFTint64 DECIMAL128_SIGN_SHIFTchar32 DIGIT_ZEROchar32 DECIMAL_POINTchar32 MINUS_SIGN
CLASS NumericUtilities
kernel/src/NumericUtilities.ev:30
Methods
METHOD floatHashBits(float64 v) RETURNS uint64
The hash-stable bit pattern of a float — the value's IEEE-754 binary64 encoding, with one canonicalisation.
A hasher may not simply widen a float into a word: that truncates the mantissa, so two distinct values collide. It takes the BITS instead, which is exact and injective — a float32 key widens into binary64 losslessly first, so both widths share this one path.
-0.0 is the single case where bits and equality disagree: IEEE says
-0.0 == 0.0, so the two MUST hash alike or a dictionary loses a key it
was handed. Both encodings therefore map to 0.
NaN needs no special case and gets none: it is equal to nothing, itself included, so no equal pair can hash differently. The consequence is worth stating plainly — a NaN inserted as a key can never be looked up again. That is IEEE's rule, not this function's.
METHOD toInt32(uint32 v) RETURNS int32
Saturating cast uint32 -> int32 (clamps anything above INT32_MAX).
METHOD truncateToUint64(uint128 v) RETURNS uint64
Low 64 bits of a 128-bit unsigned value.
METHOD truncateToUint64(int128 v) RETURNS uint64
Low 64 bits of a 128-bit SIGNED value (bit-reinterpret; no range check).
The signed sibling of the uint128 form — used by decimal128's cohort hash
and by number's int64↔uint64 reinterpret paths.
METHOD toUint64(int64 v) RETURNS uint64
Reinterpret a signed int64 as uint64 (value-preserving for the non-negative
inputs callers guarantee; bit-reinterpret otherwise). The int64 sibling of
toUint64(int32) — number AS uint64 uses it instead of an inline narrow.
METHOD truncateToUint32(uint64 v) RETURNS uint32
Low 32 bits of a 64-bit unsigned value.
METHOD truncateToInt32(int64 v) RETURNS int32
Low 32 bits of a 64-bit signed value, WRAPPING into int32's range.
Spelled out rather than cast (2026-08-25). BAND states which bits are
kept and narrows the type to uint32; the sign is then applied by
arithmetic, because the top half of uint32 has no int32 counterpart and
AS correctly refuses it. Masking the sign bit off first makes the AS
total — it can never fail — and subtracting the bias reproduces two's
complement exactly, in Envzn, with nothing reinterpreted behind the
developer's back.
METHOD truncateToInt32(uint64 v) RETURNS int32
Low 32 bits of a 64-bit unsigned value, WRAPPING into int32's range. The unsigned-source sibling of the int64 form above; same construction.
METHOD truncateToInt32(float32 v) RETURNS int32
Truncate a float toward zero into int32 (no NaN / range check — the caller is responsible for validating the value first).
FOREIGN BIND, not an Envzn expression (Brian, 2026-08-24). There is no
total float->int spelling in the language and this host cannot reach the
one fallible form: NumericUtilities is emitted BEFORE
PrimitiveConversions, so AS/INTO are undeclared identifiers here.
TRUNCATE refuses a float source correctly — round-toward-zero is not a
bit chop. The shim sits beside ev_convert_f64_to_bits, which this file
already binds, so the pattern is the file's own.
METHOD truncateToInt32(float64 v) RETURNS int32
METHOD toUint64(int32 v) RETURNS uint64
A signed int32 read as uint64 — value-preserving for the non-negative inputs callers guarantee. A NEGATIVE input does not survive: it maps into the top of the unsigned range (-1 becomes 18446744073709551615), so the sign is lost rather than the bits reinterpreted.
METHOD toInt64(uint64 v) RETURNS int64
A uint64 read as int64 — value-preserving for values <= INT64_MAX, which
callers guarantee. Above that the value wraps negative. The inverse of
toUint64(int64). It also underpins the uint64 INTO int128 conversion
(PrimitiveConversions), which splits the magnitude across two toInt64 calls
to widen positively — the direct cross-sign assign is not itself a widen.
METHOD exactScaledInt128(int128 mantissa, int32 power) RETURNS | int128
Exact integer value of mantissa × 10^power as int128, or FAILURE when the
result is not an exact integer (power < 0 leaving a remainder) or its
magnitude exceeds int128. The shared extraction behind every decimal128 AS
int*/uint*; each caller range-checks the int128 against its own width.
CLASS FloatFormat
Shortest-round-trippable float -> decimal string.
Portions derive from Ryu (Copyright 2018 Ulf Adams), used under the Boost Software License 1.0 — see LICENSE.md, Third-Party Notices.
Kernel-internal numeric formatter. Users never touch it directly;
FloatConverter's float→String operators delegate
here. Modelled on UTFCodec.ev — a self-contained numeric algorithm
in its own file behind a clean interface.
ALGORITHM
Implements the Ryu algorithm (Ulf Adams, "Ryū: fast float-to-string
conversion", PLDI 2018) — the shortest decimal string that round-
trips back to the exact input float. Pure Envzn: no FOREIGN, no
native shim. This replaces an earlier lossy std::to_string shim
(which gave a fixed 6 decimal places).
Re-implemented from the published algorithm rather than transcribed
from the reference C — with one exception: the precomputed constant
tables are copied verbatim, and carry per-table provenance notes in
the body. The portable RYU_OPTIMIZE_SIZE variant is the
model: it computes the pow5 / inverse-pow5 magic values at runtime
from a tiny base table rather than carrying ~1300 uint64 table
constants, and uses a hand-rolled 64x64->128 multiply (no uint128).
Upstream: github.com/ulfjack/ryu, offered under Apache-2.0 or Boost-1.0.
Envzn elects Boost-1.0 — see LICENSE.md, Third-Party Notices.
INTERFACE — bits in, String out
formatF64(uint64 bits) / formatF32(uint32 bits) take the raw
IEEE-754 bits of the float, never a float value. The float<->bits
reinterpret is the caller's job. Consequence: FloatFormat
is pure integer code with zero dependency on a float type or any
bitcast primitive.
kernel/src/FloatFormat.ev:55
Fields
uint32 LOG2_OF_5_MULTuint32 LOG2_OF_5_SHIFTint32 LOG2_POW5_MAX_Euint32 LOG10_OF_2_MULTuint32 LOG10_OF_2_SHIFTint32 LOG10_POW2_MAX_Euint32 LOG10_OF_5_MULTuint32 LOG10_OF_5_SHIFTint32 LOG10_POW5_MAX_Eint32 DOUBLE_MANTISSA_BITSint32 DOUBLE_EXPONENT_BITSint32 DOUBLE_BIASint32 FLOAT_MANTISSA_BITSint32 FLOAT_EXPONENT_BITSint32 FLOAT_BIASint32 DOUBLE_POW5_INV_BITCOUNTint32 DOUBLE_POW5_BITCOUNTint32 FLOAT_POW5_INV_BITCOUNTint32 FLOAT_POW5_BITCOUNTint32 POW5_TABLE_SIZE
Methods
METHOD float64toDecimal(uint64 bits) RETURNS FloatingDecimal64
Render the IEEE-754 double whose raw bits are bits as its
shortest round-trippable decimal String.
METHOD floatingDecimalToString(FloatingDecimal64 fd, boolean isNegative) RETURNS String
Turn a FloatingDecimal64 carrier into its final String, per the Phase 4 format-policy (floatformat-ryu-plan.md): Python-style threshold (sciExp < -4 OR >= 16), compact exponent (no '+', no leading zero), no trailing '.0', sign of zero dropped, special-case spellings "NaN"/"infinity"/"-infinity". MIRRORED by floatingDecimalToBytes — the two MUST stay byte-identical.
METHOD formatF64(uint64 bits) RETURNS String
One-shot: bits → FloatingDecimal64 → String. Convenience wrapper that chains float64toDecimal + floatingDecimalToString, extracting the sign bit from the raw bits.
METHOD floatingDecimalToBytes(MUTABLE REFERENCE binary[] out, FloatingDecimal64 fd, boolean isNegative) RETURNS void
MIRROR of floatingDecimalToString — appends the bytes to out.
METHOD appendF64Bits(MUTABLE REFERENCE binary[] out, uint64 bits) RETURNS void
MIRROR of formatF64 — appends the bytes for the float whose raw
bits are bits.
METHOD appendF64(MUTABLE REFERENCE binary[] out, float64 v) RETURNS void
appendF64Bits from a float64 value — the serializer's entry point.
METHOD decimalF64(float64 v) RETURNS uint64
The Ryu decomposition of a float64 value, without formatting: v == (isNegative ? -1 : 1) * mantissa * 10^exponent, the mantissa the shortest that round-trips. Specials keep float64toDecimal's sentinel exponents (0x7FFFFFFC..0x7FFFFFFF).
METHOD float32toDecimal(uint32 bits) RETURNS FloatingDecimal32
Render the IEEE-754 float whose raw bits are bits as its
shortest round-trippable decimal. Same structural shape as
float64toDecimal above — re-read its STEP 1..5 comments
for the algorithm narrative; this body only flags the f32
specifics (narrower mantissa, narrower magic constants,
32-bit arithmetic throughout).
METHOD floatingDecimal32ToString(FloatingDecimal32 fd, boolean isNegative) RETURNS String
f32 sibling of floatingDecimalToString — identical logic; only the carrier type differs (FloatingDecimal32 vs FloatingDecimal64, uint32 vs uint64 mantissa). The shared digit-walking helpers (appendDigits, appendMantissaWithPoint, appendChars) take uint64 — uint32 mantissas widen at the call site.
METHOD formatF32(uint32 bits) RETURNS String
One-shot: bits → FloatingDecimal32 → String.
Numeric contracts
INTERFACE Arithmetic
Arithmetic[T] — the contract that T is additive: an add(T),
subtract(T), and negate(), each closed over T. It is a named
constraint (e.g. for generic bounds), NOT an operator-derivation
mechanism.
The binary OPERATORS are a separate, explicit surface: a class
overloads + / - by declaring METHOD operator +(T) RETURNS T /
METHOD operator -(T) RETURNS T (mangled __op_plus__ /
__op_minus__), exactly as number and decimal128 do. Declaring
IMPLEMENTS Arithmetic[T] does NOT by itself make a + b compile —
the operator method is what the compiler dispatches. There is no
unary operator -; expose negation as the named negate().
Implementations should be closed (add(b) returns a T),
associative, and have subtraction undo add. Built-in for the
primitive numerics. A fully-numeric custom type typically declares
both this contract and the matching operator methods, and pairs
with Comparable[T] / Multiplier[T].
kernel/src/interfaces.ev:553
Methods
METHOD add(REFERENCE T other) RETURNS T
METHOD subtract(REFERENCE T other) RETURNS T
METHOD negate() RETURNS T
GROUP Floating
GROUP HashKey — the primitives admissible as a hash key: every WordKey,
plus decimal128.
decimal128 is NOT a WordKey — #key is forbidden for it (E2153) — but it
does not need to be. It carries its own exact
hash() (Decimal128.ev), so it takes a hasher's WHEN K IMPLEMENTS Hashable
arm and never reaches the widening. That is why the general hashers admit it
and FastIntHasher/InlineIntHasher, whose bodies ARE the widening, do not.
kernel/src/interfaces.ev:123
INTERFACE Multiplier
Multiplier[T] — the contract that T supports multiply(T) and
divide(T). Sister to Arithmetic[T]; separated because not every
additive type multiplies (e.g. Date types add a Duration but have no
meaningful multiplication). Like Arithmetic, this is a constraint,
NOT operator-derivation.
The * / / OPERATORS are declared explicitly: METHOD operator
*(T) RETURNS T / METHOD operator /(T) RETURNS T (mangled
__op_times__ / __op_divide__). A class may overload *// this
way (e.g. Matrix * Matrix); % is not overloadable.
Division by a zero-equivalent value should PANIC MathError if
undefined. Built-in for the primitive numerics. Arithmetic[T] +
Multiplier[T] + Comparable[T] is what makes a custom type satisfy
the implicit Numeric constraint.
kernel/src/interfaces.ev:577
Methods
METHOD multiply(REFERENCE T other) RETURNS T
METHOD divide(REFERENCE T other) RETURNS T
GROUP Numeric
Per the ENVZN module convention (CLAUDE.md), this file contains INTERFACEs (and may contain GROUPs / STRUCTs). Each interface here is "reserved" — recognized by the compiler for built-in cascade rules, operator dispatch, or similar.
This file currently contains the interfaces needed to support String, Array, Stack, and ArrayIterator. Expansion as more kernel collections land (Hashable for Set/Dictionary, Equatable / Comparable fully spec'd alongside the existing Operator Overloading interfaces, etc.) — out of scope for this initial draft. GROUP Numeric — sum type spanning every primitive numeric kind.
The constitution's reserved numeric category. Used by:
- Methods that accept "any number" without committing to a
specific width (Math.isLessThan(Numeric a, Numeric b)).
- Generic numeric algorithms expressed at the kernel layer.
- Operator dispatch on Numeric-typed variables (the four
arithmetic operators are spec-defined on Numeric; the
compiler dispatches to the underlying primitive at runtime).
Includes byte because byte is structurally an unsigned 8-bit
integer and participates in numeric arithmetic in the kernel
(e.g. byte arrays for binary protocols). Excludes char
because char represents a Unicode code point — comparison /
ordering apply, but arithmetic does not.
kernel/src/interfaces.ev:37
GROUP ValuePrimitive
GROUP Collections — sum type spanning every kernel collection class.
The compiler recognises this group as the canonical "is this a
collection?" check. Every member is parametric (Foo[T] or
Dictionary[K: V]) and is guaranteed by the kernel to:
- implement Cloneable with an AUTO METHOD clone() generated body
- default-construct to an empty container via
EMPTYorCREATE() - support
:=ownership transfer (per the canonical move semantics)
Drives Bug #23's AUTO clone synthesis when a class has a collection-
typed field: the body emits field->clone() without requiring the
element type to itself be Cloneable when the collection's clone()
internally handles its own elements.
Used by template qualifier expressions as the constraint for "this
type param accepts any kernel collection" (e.g. Dictionary's V).
GROUP ValuePrimitive — the POSITIVE form of PRIMITIVE(EXCEPT opaque,
boolean, number, complex). A negative list has to be remembered; a positive
one cannot be forgotten. The EXCEPT form was also silently WRONG: it still
admitted the four C.* boundary types, legal only inside a FOREIGN
declaration (I.D.x).
⚠ IT IS NOT A DROP-IN FOR THE ATOM, and the reason is structural. A comma in
a template qualifier is a UNION, not an intersection: V IS Cloneable,
PRIMITIVE(EXCEPT boolean) means "a Cloneable class OR such a primitive".
Writing V IS Cloneable, ValuePrimitive therefore ADDS an arm rather than
narrowing one, and PRIMITIVE is a reserved meta-category atom carrying
value semantics the analyzer reads — dropping it changed ownership analysis
and raised E3023 on CollisionNode. So this group replaces an EXCEPT list
only where the primitive constraint stands ALONE (GIVEN TYPE K IS
PRIMITIVE(EXCEPT …)), which is the hasher shape. Narrowing an atom BY a
group at a union site needs compiler support that does not exist yet.
kernel/src/interfaces.ev:79
ENUM NumberKind
NumberKind — the subtype tag carried by a number primitive: a signed
integer (int64) XOR an unsigned integer (uint64, reached only when a value
overflows the int64 arm into (int64_max, uint64_max]) XOR a float (float64).
number->kind() returns this (a typed, MATCH-able surface). Backs the
Number value-class (Number.ev, V1 Part D). UNSIGNED is appended last so the
shipped INTEGER/FLOAT ordinals are unchanged. See number-and-complex-design.md,
number-unsigned-arm-design.md / ENVZN_CONSTITUTION I.D.i(g).
kernel/src/enums.ev:245
| Case | Description |
|---|---|
? |
— |
? |
— |
? |
— |
ENUM RoundingMode
RoundingMode — how a decimal128 operation breaks at the boundary where
it must drop digits. The bare operators + - * / always round to 34
significant digits with HALF_EVEN; the explicit-context methods
(roundTo, and add/subtract/multiply/divide with an explicit
precision) take a mode. There is NO ambient/global rounding context —
every mode-taking call names its choice (the readable north-star).
The set is Java's RoundingMode minus UNNECESSARY; a superset of
IEEE 754's five rounding-direction attributes. HALF_EVEN is the
zero-default (it is listed first), matching the bare-operator behavior.
Backs the Decimal128 value-class (Decimal128.ev, V1 Part #40).
- HALF_EVEN — ties to the nearest even digit (banker's rounding). The IEEE default; minimizes cumulative bias. DEFAULT.
- HALF_UP — ties away from zero (0.5 → 1, −0.5 → −1). The rounding taught in schools; IEEE roundTiesToAway.
- HALF_DOWN — ties toward zero (0.5 → 0, −0.5 → 0).
- UP — always away from zero (round magnitude up on any nonzero dropped part).
- DOWN — always toward zero (truncate). IEEE roundTowardZero.
- CEILING — toward +∞ (round up for positives, truncate for negatives). IEEE roundTowardPositive.
- FLOOR — toward −∞ (truncate for positives, round up for negatives). IEEE roundTowardNegative.
kernel/src/enums.ev:291
| Case | Description |
|---|---|
? |
— |
? |
— |
? |
— |
? |
— |
? |
— |
? |
— |
? |
— |
STRUCT DecimalParts
DecimalParts — the (coefficient, exponent) a decimal text scans into, before
the compiler assembles them into a decimal128. Returned by
NumericUtilities.parseDecimalParts so the String INTO decimal128 lowering can
build Decimal128(coeff, exp) — the same construction the literal path emits —
without the kernel ever constructing a value-class primitive itself.
kernel/src/structs.ev:67
Fields
int128 coefficientint32 exponent