Numbers

The class-backed numeric primitives and the math that works on every number type.

Generated by bin/build_library_doc.py from the kernel sources. Do not edit by hand: change the generator, or the doc comments in kernel/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

CLASS Number

kernel/src/Number.ev:41

Fields

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

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

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:

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

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_ intrinsics — not a method): - saturatingAdd: clamps at the type's min/max - checkedAdd: returns (value, STATUS); FAILURE on overflow

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

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

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:

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).

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