The public root exports ieee, scalar, vector, complex, quaternion,
matrix, linalg, geometry, and algebra, their shared result types, and
the C23 source surface in the scalar contract.
zmath is a dependency-free Zig 0.17.0 mathematics library. Public fixed-size operations do not allocate.
Public real formats are f16, f32, and f64 for the current IEEE and scalar primitives. Public type and function names use lower-case h, f, and d suffixes. q, f128, and bfloat16 are outside this contract and have no public API.
The API uses one structural generic, Mat. Complex values, vectors, quaternions, geometry, factorizations, and geometric algebras use named concrete types.
Root re-exports provide convenience names for scalar and IEEE operations.
zmath.ieee
zmath.scalar
zmath.vector
zmath.complex
zmath.quaternion
zmath.matrix
zmath.linalg
zmath.geometry
zmath.algebra
src/
ieee.zig
scalar.zig
vector.zig
vector_value.zig
complex.zig
quaternion.zig
matrix.zig
linalg.zig
geometry.zig
algebra.zig
zmath.vector is implemented across src/vector*.zig: src/vector.zig plus the five modules it re-exports, as described in Vector API.
The dependency direction is:
ieee -> scalar -> complex
+-> vector -> quaternion
complex + vector + quaternion -> matrix -> linalg
vector + quaternion + matrix -> geometry
complex + vector + quaternion + matrix -> algebra
Geometry and algebra do not depend on each other.
See README.md for dependency setup and build commands. The module carries its private C kernels and link settings. The C headers are not a standalone C API.
Checked geometry returns NonFiniteInput for invalid supplied coordinates
and NonFiniteResult for arithmetic range failure. Fixed-size values need no cleanup.
Other numerical APIs use IEEE values, status fields, or null as documented below.
zmath.vector provides @Vector overloads for the elementwise scalar
operations: 417 public entry points across six modules. It is a distinct
layer from the linear-algebra Vec* value types described in
Vectors and quaternions, which the same module
also owns and which are implemented in src/vector_value.zig. The two layers
share this module path and nothing else.
Every vector operation applies the scalar kernel to each lane. Lane i of
a vector result has the scalar result for lane i of the input.
src/vector_surface_test.zig guards this surface, and src/vector_test.zig carries the per-lane bit-identity tests, including an exhaustive pass over all 65,536 binary16 bit patterns.
The layer is portable. @Vector is lowered by the compiler for the selected
target. There is no runtime CPU detection, no hand-written per-ISA kernel, and
no per-format selection beyond the lane type itself. What the layer provides
is a uniform lane-parallel API, not a numerical speedup over a scalar loop.
| Module | Source | Covers |
|---|---|---|
zmath.vector |
src/vector.zig |
The 214 float-to-float entry points, where a lane maps to a lane of the same float type |
zmath.vector.struct_family |
src/vector_struct.zig |
The 13 operations whose scalar kernel returns a result struct |
zmath.vector.intarg |
src/vector_intarg.zig |
The 11 operations that take a non-float argument |
zmath.vector.narrow |
src/vector_narrow.zig |
The ternary fma and the 6 C23 narrowing operations |
zmath.vector.ieee_family |
src/vector_ieee.zig |
The remaining IEEE and miscellaneous operations, including the integer-returning and boolean-returning ones |
zmath.vector.core |
src/vector_core.zig |
The shared per-lane mapping core behind intarg, narrow, and ieee_family |
The four families and core are re-exported from zmath.vector, so the whole @Vector
surface is reachable as zmath.vector.*. core is public so that a caller
can write a further lane-parallel entry point from the same helpers, with the
same per-lane bit-identity. struct_family does not use it, because every
core helper returns a @Vector; it carries its own per-lane helpers instead.
The six Vec* value types are re-exported from zmath.vector as well, so the module path serves
both layers. The table above is the @Vector surface only; src/vector_value.zig is not a
lane-parallel module and its operations carry no per-lane bit-identity argument.
The two counts are disjoint and are taken the same way. 417 is the number of
pub fn declarations across the six @Vector modules in the table above. 158 is
the number of pub fn declarations the six Vec* structs publish in
src/vector_value.zig, counting their instance methods, their constructors, and
their static factories. The two numbers share a module path and nothing else, and
neither is a subset of the other.
The root of zmath publishes 103 runtime callables: 97 pub fn declarations and
6 pub const function aliases, which are fmaximum_mag, fminimum_mag,
fmaximum_num, fminimum_num, fmaximum_mag_num, and fminimum_mag_num.
96 of the 103 accept a @Vector and route to this layer, which is 90 of the 97
pub fn declarations plus all 6 aliases. The seven that do not are:
nan, whose type argument is acomptimetype and therefore has no lane to map over. The vector spelling of a NaN is@splatat the call site.fadd,fsub,fmul,fdiv,ffma, andfsqrt, whose root signatures take concretef64arguments and returnf32. A concretef64parameter cannot accept a@Vector, so those six are scalar-only at the root. Their vector forms exist and belong to this layer, aszmath.vector.narrow.faddv,fsubv,fmulv,fdivv,ffmav, andfsqrtv, which take a@Vector(N, f64)and return@Vector(N, f32).
A fixed f64 parameter by itself does not make a root operation scalar-only.
nextToward(value, toward: f64) still takes a @Vector value and broadcasts its
f64 comparator, and it is one of the 96.
Each operation is available in these forms:
- As a top-level generic overload:
zmath.sin(x)wherex: @Vector(N, T)withTone off16,f32, orf64. The element type selects the scalar kernel, and the vector width is inferred from the argument. - As a generic vector entry point:
zmath.vector.sinv(x), which selects the scalar kernel from the lane type. - As specialized entry points that pin the lane type:
zmath.vector.sinfvandzmath.vector.sindvforf32andf64, pluszmath.vector.exphvforf16, which is the spelling wherever thef16name is not already used by another generic entry point.
The specialized name is the operation name, the format letter, and v
(for example sinfv for the sine of a @Vector(N, f32)).
Where a specialized f16 name would collide with another operation's generic
entry point, the specialized name is omitted and the generic opv entry point
serves binary16 as well. This affects the bare trigonometry operations
sin, cos, tan, asin, acos, and atan, because their binary16
specialization would otherwise collide with the generic entry point of the
corresponding hyperbolic operation (sinhv, coshv, tanhv, asinhv,
acoshv, atanhv). No operation loses coverage; binary16 callers use the
generic opv form.
zmath.vector.sinhv is therefore the binary16 HYPERBOLIC sine applied to
@Vector(N, f16) lanes, not the binary16 SINE. The bare trigonometric
generics sinv, cosv, tanv, asinv, acosv, and atanv keep the f16
binding of the root dispatch, where sin maps to zmath.scalar.sinh (the
binary16 sine), cos to zmath.scalar.cosh, and tan to zmath.scalar.tanh,
while sinh maps to zmath.scalar.sinhh. See
Generic dispatch.
zmath.sin(@Vector(4, f32)) // top-level generic overload
zmath.vector.sinv(@Vector(4, f32)) // generic vector entry point
zmath.vector.sinfv(@Vector(4, f32)) // binary32 specialization
zmath.vector.sindv(@Vector(2, f64)) // binary64 specialization
zmath.vector.powv(a, b) // elementwise binary
zmath.vector.sinhv(@Vector(4, f16)) // binary16 HYPERBOLIC sine
Most results are a real @Vector(N, R):
| Operations | Result |
|---|---|
| Float-to-float, and the narrowing operations | @Vector(N, T), and @Vector(N, f32) for @Vector(N, f64) narrowing input |
ilogb and llogb |
@Vector(N, i32) and @Vector(N, i64) |
totalOrder and totalOrderMag |
@Vector(N, bool) |
The 13 result-struct operations cannot be a @Vector. Zig forbids a
struct as a vector element type and reports "expected integer, float, bool, or
pointer for the vector element type", so atan2, atan2pi, lgamma,
frexp, modf, remquo, canonicalize, setPayload, setPayloadSig,
lrint, llrint, lround, and llround return a lane-ordered array
[N]Result instead, and so do pown, compoundn, and rootn, which also
return a result struct. Element i of that array is the value the scalar
kernel itself returned for lane i, moved whole and in lane order, so the
per-lane bit-identity guarantee is unchanged. This is a language limitation,
not an oversight.
The array shape buys no throughput. The kernel is applied once per lane, exactly as a scalar loop would, so the array form is a uniform lane-parallel API and not SIMD acceleration.
Result structs in scalar_c23_types.zig are bare struct values with auto
layout. They are never extern and never packed, so @bitCast and
@offsetOf do not apply to them. Every field is assigned explicitly and
nothing is reinterpreted.
zmath.vector.intarg holds the 11 operations that take a non-float argument.
Each extra argument has two forms:
- A broadcast runtime scalar applied to every lane, for example
ldexpv(x, exponent: i32)andscalbnv(x, exponent: i32),scalblnv(x, exponent: i64), the power family, andnextTowardv(x, toward: f64). Thefromfpfamily takes two broadcast scalars, aFromFpRounddirection and au32width. - A per-lane integer vector under a
Lanessuffix, holding a@Vector(N, i32)or@Vector(N, i64)of exponents, for exampleldexpvLanes(x, e)andpownvLanes(x, e).
nextToward has no Lanes form, because its toward argument is always
f64 and each per-format kernel applies its own internal tolerance.
pownv, compoundnv, and rootnv return the lane-ordered
[N]PowerResult(T) array described above, in both the broadcast and the
Lanes form.
zmath.vector.ldexpv(x, 3) // one broadcast exponent
zmath.vector.ldexpvLanes(x, e) // one exponent per lane
zmath.vector.pownvLanes(x, e) // [N]PowerResult(T), lane-ordered
zmath.vector.ilogbv(x) // @Vector(N, i32)
zmath.vector.totalOrderv(a, b) // @Vector(N, bool)
zmath.vector.atan2v(y, x) // [N]Atan2Result(T), lane-ordered
ulpDistance is a zmath extension. It is not a C23 scalar operation.
nextAfterh, nextAfterf, and nextAfterd choose the first NaN operand.
If it is signaling, they set its quiet bit and preserve its sign and payload.
They return a quiet NaN unchanged.
totalOrderh, totalOrderf, and totalOrderd return true when the left operand precedes or equals the right operand in IEEE total order.
The target root API uses one generic operation name for f16, f32, and f64.
zmath.abs(value)
zmath.sin(value)
zmath.sinh(value)
zmath.atan2(y, x)
zmath.compoundn(value, exponent)The floating input type selects one exact scalar implementation:
| Input type | Scalar suffix |
|---|---|
f16 |
h |
f32 |
f |
f64 |
d |
For example, zmath.sin(x) selects zmath.scalar.sinh, sinf, or sind.
zmath.sinh(x) selects zmath.scalar.sinhh, sinhf, or sinhd.
zmath.cos(x) selects zmath.scalar.cosh, cosf, or cosd.
zmath.cosh(x) selects zmath.scalar.coshh, coshf, or coshd.
zmath.tan(x) selects tanh, tanf, or tand.
zmath.tanh(x) selects tanhh, tanhf, or tanhd.
The scalar name is the operation name followed by the type suffix. The suffix rule produces sinhh, coshh, tanhh, asinhh, acoshh, and atanhh for binary16 hyperbolic operations.
All floating arguments to a normal binary or ternary generic must have the same type. Dispatch does not widen, narrow, or coerce a floating argument.
The first floating argument selects the family when an operation also takes an integer or an output pointer. Integer-result operations keep the suffix of their floating input.
zmath.abs is the C23 fabs equivalent. Its scalar symbols remain absh, absf, and absd.
The API does not add fabs, fabsh, fabsf, or fabsd aliases.
The target has no q, f128, bfloat16, or bfloat suffix family. These types do not participate in generic dispatch.
The scalar and IEEE APIs use a pure-value profile.
A call communicates only through its return value, including an explicit result struct when required.
No result contract uses errno, floating-point exception flags, or hidden mutable status.
zmath does not read or write errno.
It does not inspect, clear, report, or restore floating-point exception flags.
Incidental processor flags are outside this contract.
Callers must keep IEEE round-to-nearest, ties-to-even active for each scalar call.
Scalar functions do not take a general RoundMode argument.
The only public functions that do are the zmath.ieee scaleB and roundIntegral families, which name the rounding of their own result.
An operation-defined direction parameter, such as the fromfp direction, does not change the process environment.
zmath does not read, validate, set, or restore the process rounding mode.
Behavior with another active rounding mode is unsupported.
This caller precondition also applies to narrowing operations. They compute the exact mathematical operation and round once to the declared destination type.
In the tables below, op{h,f,d} names three exact zmath.scalar symbols. For example, exp{h,f,d} means exph, expf, and expd.
Each operation also has one target root generic named zmath.<op>, unless a table states an exception. These tables define target names only.
| Root generic | Scalar implementation family |
|---|---|
abs |
abs{h,f,d} |
ceil |
ceil{h,f,d} |
floor |
floor{h,f,d} |
nearbyint |
nearbyint{h,f,d} |
rint |
rint{h,f,d} |
round |
round{h,f,d} |
roundeven |
roundeven{h,f,d} |
trunc |
trunc{h,f,d} |
lrint |
lrint{h,f,d} |
llrint |
llrint{h,f,d} |
lround |
lround{h,f,d} |
llround |
llround{h,f,d} |
fromfp |
fromfp{h,f,d} |
ufromfp |
ufromfp{h,f,d} |
fromfpx |
fromfpx{h,f,d} |
ufromfpx |
ufromfpx{h,f,d} |
The lrint, llrint, lround, and llround families return this shared result type:
pub const IntegerResult = struct { value: i64, ok: bool };A valid conversion returns .{ .value = result, .ok = true }.
A NaN, either infinity, or a rounded value outside the i64 range returns .{ .value = 0, .ok = false }.
Invalid input does not trap and does not use a numeric sentinel.
The fromfp, ufromfp, fromfpx, and ufromfpx families return an integral value in the input floating type T.
Each family has the target signature (value: T, round: FromFpRound, width: u32) T.
The u32 width maps the C unsigned int argument.
pub const FromFpRound = enum {
nearest_even,
nearest_from_zero,
toward_zero,
toward_positive,
toward_negative,
};| Root generic | Scalar implementation family |
|---|---|
exp |
exp{h,f,d} |
exp2 |
exp2{h,f,d} |
exp10 |
exp10{h,f,d} |
expm1 |
expm1{h,f,d} |
exp2m1 |
exp2m1{h,f,d} |
exp10m1 |
exp10m1{h,f,d} |
log |
log{h,f,d} |
log2 |
log2{h,f,d} |
log10 |
log10{h,f,d} |
log1p |
log1p{h,f,d} |
logp1 |
logp1{h,f,d} |
log2p1 |
log2p1{h,f,d} |
log10p1 |
log10p1{h,f,d} |
log1p and logp1 are distinct C23 public spellings with the same mathematical operation.
| Root generic | Scalar implementation family |
|---|---|
sin |
sin{h,f,d} |
cos |
cos{h,f,d} |
tan |
tan{h,f,d} |
asin |
asin{h,f,d} |
acos |
acos{h,f,d} |
atan |
atan{h,f,d} |
atan2 |
atan2{h,f,d} |
sinpi |
sinpi{h,f,d} |
cospi |
cospi{h,f,d} |
tanpi |
tanpi{h,f,d} |
asinpi |
asinpi{h,f,d} |
acospi |
acospi{h,f,d} |
atanpi |
atanpi{h,f,d} |
atan2pi |
atan2pi{h,f,d} |
atan2 and atan2pi return Atan2Result(T) { value: T, resolved: bool }. |
|
| The root generics and all suffix variants use this result shape. | |
| The public symbols are wrappers, and direct CORE-MATH kernel symbols remain private. | |
| The wrappers own all NaN, axis, zero, and infinity results. | |
The value field is an operation result only when resolved=true. |
|
Every ordinary result and every documented special result sets resolved=true. |
|
| This includes a quiet NaN selected from a NaN input. | |
If a finite kernel cannot prove one nearest-even result, the call sets resolved=false. |
|
The value field is then the format's positive canonical quiet NaN. |
|
| This false status reports an unresolved computation, not a mathematical NaN or a domain error. | |
| No valid input may trap or terminate the process. | |
| The wrappers do not guess a result bit when a kernel cannot decide. | |
They do not call host libm or use fenv or errno. |
| Root generic | Scalar implementation family |
|---|---|
sinh |
sinh{h,f,d} |
cosh |
cosh{h,f,d} |
tanh |
tanh{h,f,d} |
asinh |
asinh{h,f,d} |
acosh |
acosh{h,f,d} |
atanh |
atanh{h,f,d} |
The family notation expands mechanically. Thus, sinh{h,f,d} means sinhh, sinhf, and sinhd.
| Root generic | Scalar implementation family |
|---|---|
pow |
pow{h,f,d} |
pown |
pown{h,f,d} |
powr |
powr{h,f,d} |
compoundn |
compoundn{h,f,d} |
sqrt |
sqrt{h,f,d} |
cbrt |
cbrt{h,f,d} |
rsqrt |
rsqrt{h,f,d} |
rootn |
rootn{h,f,d} |
hypot |
hypot{h,f,d} |
compoundn, pown, and rootn take an i64 exponent or degree.
They return PowerResult(T) { value: T, ok: bool }.
The root generics and all suffix variants use this result shape.
The accepted integer range is -64 through +64, inclusive.
When n is below -64 or above +64, the range check precedes all input-value rules.
The value field is then the format's positive canonical quiet NaN, and ok=false.
Every call with -64 <= n and n <= 64 sets ok=true.
The value field then holds the specified result, including an in-range domain or NaN result.
Thus, ok reports only whether n is in the accepted range.
The C23 compoundn(x, n) operation computes (1 + x)^n.
A CORE-MATH compound source with a floating exponent is not a direct implementation of this operation.
The power families use the ordered IEC 60559 and glibc value precedence below.
An earlier rule wins.
For the three PowerResult families, each result below describes value when ok=true.
This project adopts the return values, but it does not adopt fenv or errno effects.
For pow, signaling-NaN quieting precedes an identity that would otherwise hide that signaling NaN.
Quiet NaNs do not override pow(x, +/-0)=+1 or pow(+1, y)=+1.
pow(-1, +/-infinity) is +1.
After these identities, the first remaining NaN in (x, y) parameter order wins.
For an infinite exponent, abs(x) relative to one selects +0 or +infinity.
For a zero or infinite base and a finite nonzero exponent, the exponent sign selects zero or infinity.
Such a result is negative only when the base is negative and the exponent is an odd integer.
A finite negative base with a finite noninteger exponent returns the positive canonical quiet NaN.
For pown, a signaling NaN is quieted before the n=0 identity.
Then n=0 returns +1, including for a quiet NaN base.
A remaining NaN is quieted.
Zero and infinity results use the exponent sign, and a negative sign occurs only for an odd n.
For powr, four domain cases precede ordinary NaN selection.
They are a negative non-NaN base, 0^0, 1^infinity, and infinity^0.
Each domain case returns the positive canonical quiet NaN.
After these cases, the first NaN in (x, y) parameter order wins.
For compoundn, a signaling NaN is quieted first.
A non-NaN x < -1 then returns the positive canonical quiet NaN.
Next, n=0 returns +1, including for a quiet NaN.
A remaining NaN is quieted.
For x=-1, a positive n returns +0, and a negative n returns +infinity.
For x=+infinity, a positive n returns +infinity, and a negative n returns +0.
For rootn, n=0 returns the positive canonical quiet NaN before NaN selection.
A remaining NaN is quieted.
A negative nonzero value with an even n returns the positive canonical quiet NaN.
Zero and infinity results preserve a negative sign only for odd n.
The sign of n selects the zero or infinity magnitude.
Each finite direct result from a power, root, or pi-scaled family must be correctly rounded in nearest_even.
For a result-container function, this rule applies to a finite float only when its status field is true.
For pown, compoundn, and rootn, the rule applies only when -64 <= n and n <= 64.
A false status supplies no mathematical result and makes no accuracy claim for its sentinel float.
This requirement does not claim that a current implementation satisfies it.
A composition of separately rounded operations is not final evidence.
This includes post-scaling by an approximation of 1/pi, reciprocal-exponent power, and target-format power-by-squaring.
The general bounded power cases use fixed, format-specific integer storage.
They use 32 u64 limbs for f16, 256 limbs for f32, and 2,048 limbs for f64.
The exact pown and compoundn paths keep finite dyadic arithmetic exact and round once.
The exact rootn path uses candidate and midpoint comparisons instead of a rounded reciprocal-power composition.
These storage bounds prevent allocation. They do not specify a universal execution-time bound.
For binary64 exact rationals with a unit denominator, rounding extracts the significand bits and uses guard, sticky, and parity bits instead of long division. Binary64 scaled-integer comparisons read virtually shifted limbs without materializing shifted operands. Both paths retain the same exact rounding and candidate decisions; the binary16 and binary32 integer paths are unchanged.
Four binary64 cases use a single strict IEEE operation instead of the integer path:
pownd(x, -1) and rootnd(x, -1) use 1 / x,
rootnd(x, 2) uses @sqrt(x), and compoundnd(x, 1) uses 1 + x.
The exponent bounds and exceptional-value rules above precede these paths.
Each finite result rounds once in nearest_even.
The general reciprocal-square-root case, rootnd(x, -2), retains exact comparisons;
it is not a separately rounded 1 / @sqrt(x) composition.
The f16 and f32 paths are unchanged.
| Root generic | Scalar implementation family |
|---|---|
erf |
erf{h,f,d} |
erfc |
erfc{h,f,d} |
lgamma |
lgamma{h,f,d} |
tgamma |
tgamma{h,f,d} |
lgamma returns LgammaResult(T) { value: T, sign: i2 }.
The root generic and all suffix variants use this result shape.
For a finite non-pole input, value is the rounded value of log(abs(Gamma(x))).
The sign field is the sign of Gamma(x) and is either -1 or +1.
No public result uses sign=0.
At each finite negative-integer pole and at either zero, value is +infinity.
The sign field is -1 at each finite negative-integer pole and at -0.
It is +1 at +0, either infinity, and every NaN.
For either infinity, value is +infinity.
For a NaN, value follows the common same-width NaN policy.
lgamma follows the pure-value and caller-RNE profile.
It returns only value and sign and exposes no errno or floating-point exception status.
It does not inspect, clear, report, or restore floating-point exception flags.
Incidental processor flags remain outside this contract.
The API has no global signgam value and no hidden mutable gamma-sign state.
| Root generic | Scalar implementation family |
|---|---|
fmod |
fmod{h,f,d} |
remainder |
remainder{h,f,d} |
remquo |
remquo{h,f,d} |
frexp |
frexp{h,f,d} |
modf |
modf{h,f,d} |
ilogb |
ilogb{h,f,d} |
llogb |
llogb{h,f,d} |
logb |
logb{h,f,d} |
ldexp |
ldexp{h,f,d} |
scalbn |
scalbn{h,f,d} |
scalbln |
scalbln{h,f,d} |
frexp returns FrexpResult(T) { fraction: T, exponent: i32 }.
modf returns ModfResult(T) { fractional: T, integral: T }.
remquo returns RemquoResult(T) { value: T, quotient: i32 }.
These structs replace C output pointers.
remquo uses the nearest integer quotient, with ties to even.
Let n be that quotient.
Its quotient field is sign(x/y) * (abs(n) mod 8).
Thus, the contract exposes exactly three low quotient bits.
A selected NaN, an infinite dividend, or a zero divisor sets quotient=0.
The NaN result follows the common NaN policy.
An infinite dividend or zero divisor without a NaN returns the positive canonical quiet NaN.
A zero dividend with a nonzero divisor, or a finite dividend with an infinite divisor, returns the dividend and quotient=0.
frexp returns exponent=0 for zero, infinity, and NaN.
Its fraction preserves a zero or infinity bit pattern.
For a NaN, its fraction preserves the sign and payload and sets the quiet bit.
modf returns the input zero in both fields.
For a finite value with magnitude below one, the integral field is a zero with the input sign.
For an integral finite value, the fractional field is a zero with the input sign.
For infinity, the fractional field is a zero with the input sign, and the integral field is the input infinity.
For a NaN, both fields contain the same sign-preserving, payload-preserving quiet NaN.
ilogb returns minInt(i32) for either zero and maxInt(i32) for infinity or NaN.
llogb uses the corresponding i64 limits.
logb returns -infinity for either zero, +infinity for either infinity, and a quieted input NaN for NaN.
The ldexp, scalbn, and scalbln families preserve a NaN sign and payload.
A quiet NaN is bit-identical.
A signaling NaN sets only the format quiet bit.
These rules freeze the current deterministic remquo and decomposition outputs.
They are not delegated to host library behavior.
| Root generic | Scalar implementation family |
|---|---|
fma |
fma{h,f,d} |
fdim |
fdim{h,f,d} |
fmax |
fmax{h,f,d} |
fmin |
fmin{h,f,d} |
fmaximum |
fmaximum{h,f,d} |
fminimum |
fminimum{h,f,d} |
fmaximum_mag |
fmaximum_mag{h,f,d} |
fminimum_mag |
fminimum_mag{h,f,d} |
fmaximum_num |
fmaximum_num{h,f,d} |
fminimum_num |
fminimum_num{h,f,d} |
fmaximum_mag_num |
fmaximum_mag_num{h,f,d} |
fminimum_mag_num |
fminimum_mag_num{h,f,d} |
The binary16 fmah kernel multiplies and adds exact integer dyadics.
It performs one final round-to-nearest, ties-to-even packing step.
It does not use a widened host FMA followed by a second target-format rounding.
The legacy fmax and fmin families use a deterministic local policy.
This policy is a zmath contract, not a portable C guarantee.
With exactly one NaN operand, they return the numeric operand.
With two NaN operands, they return the first operand after preserving its sign and payload and setting its quiet bit.
For an opposite signed-zero tie, fmax returns +0 and fmin returns -0.
Narrowing operations are the exception to one-operation-name dispatch. C23 encodes the source and destination format in each IEC 60559 symbol.
C23 7.12.14 defines these public binary-format signatures:
pub fn fadd(x: f64, y: f64) f32;
pub fn fsub(x: f64, y: f64) f32;
pub fn fmul(x: f64, y: f64) f32;
pub fn fdiv(x: f64, y: f64) f32;
pub fn ffma(x: f64, y: f64, z: f64) f32;
pub fn fsqrt(x: f64) f32;The target also includes the C23 Annex H _FloatM forms for the supported public formats:
| Destination | Source | Add | Subtract | Multiply | Divide | Fused multiply-add | Square root |
|---|---|---|---|---|---|---|---|
f16 |
f32 |
f16addf32(x: f32, y: f32) f16 |
f16subf32(x: f32, y: f32) f16 |
f16mulf32(x: f32, y: f32) f16 |
f16divf32(x: f32, y: f32) f16 |
f16fmaf32(x: f32, y: f32, z: f32) f16 |
f16sqrtf32(x: f32) f16 |
f16 |
f64 |
f16addf64(x: f64, y: f64) f16 |
f16subf64(x: f64, y: f64) f16 |
f16mulf64(x: f64, y: f64) f16 |
f16divf64(x: f64, y: f64) f16 |
f16fmaf64(x: f64, y: f64, z: f64) f16 |
f16sqrtf64(x: f64) f16 |
f32 |
f64 |
f32addf64(x: f64, y: f64) f32 |
f32subf64(x: f64, y: f64) f32 |
f32mulf64(x: f64, y: f64) f32 |
f32divf64(x: f64, y: f64) f32 |
f32fmaf64(x: f64, y: f64, z: f64) f32 |
f32sqrtf64(x: f64) f32 |
These format-pair declarations are Annex H forms. They are not Annex F declarations.
These names are target scalar implementation names. They are not aliases for a same-type operation.
The target has no f128, q, bfloat16, decimal, or long-double source or destination form.
The legacy fadd, fsub, fmul, fdiv, ffma, and fsqrt forms are root-level C23 names with the signatures above.
The format-pair forms are available from zmath.scalar and use the exact names in the table.
Each narrowing operation computes as if to infinite precision and rounds once to the declared destination type. The implementation must form the exact intermediate in portable software and round only at the destination. Target-dependent extended precision, contraction, or a direct narrowing shortcut is not final correctness evidence.
For every cross-width narrowing operation, the first NaN in parameter order selects the result. The result preserves its sign and the high-order diagnostic payload bits that fit the destination. It discards lower payload bits and then sets the destination quiet bit.
The scalar module does not duplicate representation and successor operations. The target zmath.ieee module owns these C23 equivalents:
canonicalize{h,f,d}
copySign{h,f,d}
nan{h,f,d}
nextAfter{h,f,d}
nextToward{h,f,d}
nextUp{h,f,d}
nextDown{h,f,d}
For input type T, canonicalize{h,f,d}(value: T) returns CanonicalizeResult(T) { value: T, ok: bool }.
This value-returning local adaptation replaces the C output pointer and integer success result.
Each supported h, f, or d input returns ok=true.
A non-NaN value is bit-identical to the input, including zero and infinity.
A quiet NaN is bit-identical to the input.
A signaling NaN preserves its sign and diagnostic payload and sets only the format quiet bit.
The supported fixed formats never return ok=false.
No supported fixed-format call can produce a failure result, so no fixed-format failure value exists.
That status is reserved for a future representation which the API cannot canonicalize.
The nan family has the target signature nan{h,f,d}(tag: []const u8) T.
The tag grammar is one or more ASCII hexadecimal digits: [0-9A-Fa-f]+.
The parser accepts no sign, 0x prefix, separator, whitespace, empty tag, or non-ASCII byte.
It interprets the tag as an unsigned diagnostic-payload integer.
The value must fit 9 bits for h, 22 bits for f, or 51 bits for d.
A valid tag produces a positive quiet NaN with that diagnostic payload.
An invalid, empty, or out-of-range tag produces the positive canonical quiet NaN.
The nextToward family has the target signature nextToward{h,f,d}(value: T, toward: f64) T.
Its comparator is f64 because f64 is the widest public format.
C leaves this cross-format NaN mapping implementation-defined.
When toward is an f64 NaN, nextTowardh and nextTowardf preserve its sign.
They retain the high-order diagnostic payload bits that fit the target, discard lower bits, and set the target quiet bit.
For f64 to f16, source payload bits 50 through 42 become destination payload bits 8 through 0.
For f64 to f32, source payload bits 50 through 29 become destination payload bits 21 through 0.
This mapping is a zmath contract, not a portable C guarantee.
Classification predicates, signaling tests, ordered comparisons, and bit conversion also remain in zmath.ieee.
C23 Annex F defines these optional IEC 60559 operations. They are target names, but they are not baseline C23 7.12 scalar names:
| Root generic | zmath.ieee implementation family |
|---|---|
totalOrder |
totalOrder{h,f,d} |
totalOrderMag |
totalOrderMag{h,f,d} |
getPayload |
getPayload{h,f,d} |
setPayload |
setPayload{h,f,d} |
setPayloadSig |
setPayloadSig{h,f,d} |
setPayload and setPayloadSig return PayloadResult(T) { value: T, ok: bool }.
This struct replaces the C result pointer with a value and success flag.
These optional operations remain a by-value API fixed to the h, f, and d formats.
They do not expose C output pointers or a dynamic format parameter.
C leaves the accepted NaN payload range implementation-defined.
This project accepts a fixed-format floating argument only when it represents an allowed numeric integer payload.
For setPayload, both signed zeros represent q=0.
The exact success ranges and result bits are:
| Format | Accepted q |
setPayload result bits |
Accepted s |
setPayloadSig result bits |
|---|---|---|---|---|
h |
0 through 511 |
0x7e00 OR q |
1 through 511 |
0x7c00 OR s |
f |
0 through 4,194,303 |
0x7fc00000 OR q |
1 through 4,194,303 |
0x7f800000 OR s |
d |
0 through 2,251,799,813,685,247 |
0x7ff8000000000000 OR q |
1 through 2,251,799,813,685,247 |
0x7ff0000000000000 OR s |
Each successful setPayload call returns the exact positive quiet NaN in the table with ok=true.
Each successful setPayloadSig call returns the exact positive signaling NaN in the table with ok=true.
Zero, including -0, is invalid for setPayloadSig because it would encode infinity.
A negative nonzero, fractional, subnormal, infinite, NaN, or out-of-range argument is invalid for either setter.
Each failure returns the exact PayloadResult(T) { value=+0, ok=false }.
This policy is a zmath contract, not a portable C guarantee.
zmath.ieee owns the two families that name the rounding of their own result instead of reading the process rounding mode.
scaleB takes any i32 exponent, and both families take the same four directions at every width.
| Family | Signatures |
|---|---|
scaleB |
scaleBh(mode: RoundMode, value: f16, exponent: i32) f16, scaleBf(mode: RoundMode, value: f32, exponent: i32) f32, scaleBd(mode: RoundMode, value: f64, exponent: i32) f64 |
roundIntegral |
roundIntegralh(mode: RoundMode, value: f16) f16, roundIntegralf(mode: RoundMode, value: f32) f32, roundIntegrald(mode: RoundMode, value: f64) f64 |
RoundMode declares exactly four values: nearest_even, toward_zero, toward_positive, and toward_negative.
Those four are the modes both families accept, with no default and no fifth direction.
nearest_even is ties to an even least-significant significand bit.
scaleB returns value scaled by 2^exponent, rounded once in the named mode.
An exact scale never rounds: the significand is unchanged, so only a right shift into the subnormal range can discard anything.
An exponent beyond the representable range is not an error, because the whole i32 range is a valid argument.
A result that leaves the format saturates at its boundary: infinity when the mode lets the magnitude grow, and the largest finite value when it does not.
A result that rounds to zero keeps the sign of the input, so the underflow of a negative value returns -0.
roundIntegral returns value rounded to an integer in the named mode, with the same tie, direction, and sign rules.
An integral value is returned bit-identical.
A finite fractional value is rounded in the named mode, including values with magnitude greater than one.
A result of zero retains the input sign.
Both families use gradual underflow. A subnormal input is normalized before it is scaled, a subnormal result is never flushed to zero, and a zero and an infinity pass through at any exponent. A NaN input preserves its sign and diagnostic payload and sets only the format quiet bit.
This contract previously named no signature for either family, and only the binary16 forms existed.
The f and d forms are now implemented with the signatures in the table, over the same four RoundMode values.
The three widths share one width-generic implementation, so a change to one width is a change to all three.
Each manifest-accepted operation-format pair must define exact output bits for zeros, subnormals, infinities, and NaNs.
The format encodings are:
| Format | +0 |
-0 |
+1 |
+infinity |
-infinity |
Canonical quiet NaN |
|---|---|---|---|---|---|---|
h |
0000 |
8000 |
3c00 |
7c00 |
fc00 |
7e00 |
f |
00000000 |
80000000 |
3f800000 |
7f800000 |
ff800000 |
7fc00000 |
d |
0000000000000000 |
8000000000000000 |
3ff0000000000000 |
7ff0000000000000 |
fff0000000000000 |
7ff8000000000000 |
When an operation returns a same-width input NaN, it preserves the sign and diagnostic payload.
It then sets the format quiet bit.
The quiet-bit masks are 0200, 00400000, and 0008000000000000 for h, f, and d.
A quiet NaN remains unchanged.
A signaling NaN becomes a quiet NaN.
When an operation returns a narrower NaN, let S and D be the source and destination diagnostic-payload widths.
The destination payload is source_payload >> (S - D).
The operation preserves the sign and then sets the destination quiet bit.
Unless an operation has a stricter rule, the first NaN argument in parameter order selects the result.
An invalid non-NaN input returns the positive canonical quiet NaN for its format.
An operation-specific documented rule takes precedence.
This includes the local fmax and fmin policy and the C23 number-preferring variants.
Signed-zero and infinity results follow each operation's documented policy. That policy can be local, C23, or IEC 60559. Implementations must not replace those results with a host-library choice.
The manifest must record every operation-specific exception before acceptance. Target status or source availability cannot substitute for this evidence.
zmath.complex owns the canonical Complexf and Complexd types.
pub const Complexf = struct { re: f32, im: f32 };
pub const Complexd = struct { re: f64, im: f64 };The complete C99/C23 complex surface is implemented for both types. The module functions use an explicit type suffix:
pub fn realf(value: Complexf) f32;
pub fn reald(value: Complexd) f64;
pub fn imagf(value: Complexf) f32;
pub fn imagd(value: Complexd) f64;
pub fn conjf(value: Complexf) Complexf;
pub fn conjd(value: Complexd) Complexd;
pub fn projf(value: Complexf) Complexf;
pub fn projd(value: Complexd) Complexd;
pub fn absf(value: Complexf) f32;
pub fn absd(value: Complexd) f64;
pub fn argf(value: Complexf) f32;
pub fn argd(value: Complexd) f64;Both suffixes implement add, sub, mul, div, exp, log, log10, pow, sqrt, sin, cos, tan, asin, acos, atan, sinh, cosh, tanh, asinh, acosh, and atanh.
The manifest names only complexAdd and complexMul, so the other nineteen pairs are implemented without oracle evidence.
Each entry defines its Zig name, inputs, result, branch cut, principal range, signed-zero result, NaN result, infinity result, pole result, overflow result, and underflow result.
Finite complex results use the scalar manifest accuracy contract for each component after range reduction. A branch-cut or special-value case uses an exact expected component-bit pattern. Complex functions do not return null for C mathematical poles.
zmath.vector owns Vec2f, Vec3f, Vec4f, Vec2d, Vec3d, and Vec4d. Each is a value type with named components.
These are implemented in src/vector_value.zig and re-exported from zmath.vector. They are not
the @Vector layer of Vector API, which reuses the same module path for a different
purpose.
zmath.quaternion owns raw Hamilton types and validated rotation types:
pub const Quatf = struct { x: f32, y: f32, z: f32, w: f32 };
pub const Quatd = struct { x: f64, y: f64, z: f64, w: f64 };
pub const UnitQuatf = struct { raw: Quatf };
pub const UnitQuatd = struct { raw: Quatd };w is the scalar component. Quatf.mul(a, b) is Hamilton multiplication. A UnitQuatf represents an active, right-handed rotation of a column vector.
UnitQuatf.rotate(v) applies q * (v, 0) * conjugate(q). a.then(b) means apply a first, then b, and equals b * a.
UnitQuatf.init(raw) returns ?UnitQuatf. It rejects a zero or non-finite input, normalizes a
finite nonzero one, and validates that the normalized result lands in the unit band, so init is
the constructor that maintains the unit norm. Raw Quatf supports algebraic operations and does
not expose a total rotation operation.
length on Vec2f through Vec4d and on Quatf and Quatd is an ordered fold
through the accepted hypot kernel over the components rather than a square root
of a sum of squares, so it avoids the spurious overflow and the flush to zero that
squaring a component can cause. The first component enters the fold as the exact
value hypot(+0, x) returns for it: the magnitude of x, or, when x is a NaN,
that NaN quieted with its sign and payload preserved. Every later component goes
through the same kernel in the same order. Nesting the kernel is not a
single-rounded multicomponent norm. It is the accepted kernel applied again, never
a re-derived polynomial, and it carries no acceptance of its own here.
fromAxisAngle measures its axis once. It rejects a non-finite axis, rejects a
zero or non-finite length, and divides the three components by that length. It
does not normalize the quotient and measure it a second time: a largest component
divided by the accepted finite, nonzero length cannot round to zero, so the
quotient is not the zero vector and a second norm check decides nothing the length
has not already decided. The accepted and rejected calls are the same calls.
raw is a public field. Zig cannot make a struct field private, and
UnitQuatf is a plain struct with no opaque handle, so UnitQuatf{ .raw = q } compiles for any
consumer and can store any quaternion at all. The unit norm is therefore an invariant the
constructors maintain and the consumer preserves, not one the language or the type enforces:
init is the only validating constructor, fromAxisAngle is the second way in and routes through
init, and raw is the read path back to the algebra. A consumer must not assign arbitrary values
to raw and must not bypass validation to obtain a UnitQuatf. rotate and the composition
methods rely on that unit-norm precondition, so a forged non-unit value has no rotation guarantee:
the rotation it performs scales by the norm squared, and every rotate result from it is
meaningless rather than merely inaccurate. There is no raw() accessor method: Zig puts fields and
declarations in one namespace per struct, so a field named raw and a method named raw cannot
both exist.
zmath.matrix owns the sole public structural generic.
pub fn Mat(
comptime rows: usize,
comptime columns: usize,
comptime Scalar: type,
) type;
pub const Mat2f = Mat(2, 2, f32);
pub const Mat3f = Mat(3, 3, f32);
pub const Mat4f = Mat(4, 4, f32);
pub const Mat5f = Mat(5, 5, f32);
pub const Mat2d = Mat(2, 2, f64);
pub const Mat3d = Mat(3, 3, f64);
pub const Mat4d = Mat(4, 4, f64);
pub const Mat5d = Mat(5, 5, f64);Scalar is one of f32, f64, Complexf, or Complexd. Other user-defined scalar types are unsupported.
Mat(rows, columns, Scalar) stores data: [columns][rows]Scalar. The first index is the column. The second index is the row. at(row, column) returns data[column][row].
Vectors are columns. M * v applies matrix M to vector v. Composition is right-to-left. (A * B) * v applies B first and A second.
Mat.mulVec applies the matrix to a column vector: it takes exactly columns
lanes and returns exactly rows. The input is a named vector when a named vector of
that lane count and entry type exists, so Vec2f, Vec3f, or Vec4f for a real
f32 matrix and Vec2d, Vec3d, or Vec4d for a real f64 matrix. Every other
real column count, and every Complexf or Complexd column count, takes the plain
array [columns]Scalar. The result is Mat.vector_type, which stays row-sized: a
three-row f32 matrix returns Vec3f, a five-row one returns [5]f32, and a
complex matrix returns [rows]Complexf or [rows]Complexd. A square Mat is
unchanged by that split, so Mat3f.mulVec still takes and returns Vec3f and
Mat4d.mulVec still takes and returns Vec4d, and vector_type is still the
right-hand side type solveLu and solveCholesky accept.
const rect = Mat(2, 4, f32).init(.{
.{ 1, 3, 5, 7 },
.{ 2, 4, 6, 8 },
});
const applied: zmath.vector.Vec2f =
rect.mulVec(zmath.vector.Vec4f.init(1, 1, 1, 1)); // x = 16, y = 20A rectangular matrix no longer takes a row-sized argument. Handing Mat(5, 4, f32)
or Mat(2, 3, f32) the five-lane or two-lane vector the old signature accepted is
rejected by type checking at compile time rather than by a checked-build panic, and
there is no compatibility overload for that spelling. The arithmetic does not move
with the shape: each row still seeds from its column-zero product and adds the
remaining columns in increasing order, strictly, with no reassociation and no fused
multiply-add.
pub fn mul(
comptime output_columns: usize,
left: Mat(rows, columns, Scalar),
right: Mat(columns, output_columns, Scalar),
) Mat(rows, output_columns, Scalar);Specialized operations are module functions, not alias methods.
pub fn determinantMat3f(value: Mat3f) f32;
pub fn inverseMat4d(value: Mat4d) ?Mat4d;
pub fn mulMat4fVec4f(matrix: Mat4f, vector: Vec4f) Vec4f;Mat4f and Mat4d represent homogeneous 3D transforms and linear 4D transforms. Mat5f and Mat5d represent homogeneous 4D affine or projective transforms.
The API has no dynamic matrix, expression template, or tensor type.
The implementation supplies the size-paired forms of the three named operations rather than only
Mat3f, Mat4d, and Mat4f: determinantMat2f through determinantMat5d, inverseMat2f
through inverseMat4d, and mulMat4fVec4f with mulMat4dVec4d.
The fixed relative pivot thresholds are inverse_pivot_threshold_f = 0x1p-21
and inverse_pivot_threshold_d = 0x1p-50.
They derive from unit roundoff and the reduction's element-growth bound.
Callers can inspect these constants. The API has no per-call threshold option.
inverse writes only the cells a later step or the result can still read. The
reduction holds [ value | identity ] in one row-major work array of a comptime
extent, so nothing is allocated. Step k reads column k of the left block for
the pivot search and for the multiplier, and writes every column from k + 1 onward.
Column j of the left block is therefore read by the pivot search of step j and
by nothing else: after step j no cell of column j is read again whatever the
entries are, and the result loop reads only the right block. The normalization and
the elimination skip the settled left-block columns, and a row interchange moves
only the live columns, because the columns before k are settled in every row.
This is not a value-dependent claim. An update skipped for a settled column would
have written a cell that nothing reads, including a NaN that an overflow would
otherwise have left behind. Every live operation keeps its position in the
sequence, the reduction stays in strict float mode, and null still means exactly
the three things it means above: the pre-scan found a non-finite entry, the pivot
magnitude failed the threshold test against inverse_pivot_threshold_f or
inverse_pivot_threshold_d times the largest entry magnitude, or the reduction
left a non-finite entry in the right block. The threshold, the pivot order, and
the returned value are not changed by the skipped cells.
zmath.matrix.mul is declared with anytype parameters so that both operand shapes are checked
while the output width is inferred. Zig has no function overloading, so a separate literal
declaration per size pair is not expressible; the returned type is still exactly
Mat(rows, output_columns, Scalar).
zmath.linalg owns named factorization types for named matrix sizes.
pub const LuMat2f = struct { lu: Mat2f, permutation: [2]u8, parity: i1 };
pub const LuMat3f = struct { lu: Mat3f, permutation: [3]u8, parity: i1 };
pub const LuMat4f = struct { lu: Mat4f, permutation: [4]u8, parity: i1 };
pub const CholeskyMat4f = struct { lower: Mat4f };The factorization types are plain value structs with no methods. Their operations are module
functions: lu and cholesky produce a factorization, solveLu and solveCholesky return
?Vec2f through ?Vec4d, solveLuMultiple and solveCholeskyMultiple carry a right-hand side
matrix, and reconstructLu, determinantLu, reconstructCholesky, and determinantCholesky
read a factorization back. lu and cholesky return null for a singular, non-positive-definite,
or non-finite input, as applicable. A solve returns null for a zero or non-finite
stored pivot. It does not reject non-finite computed entries: finite right-hand sides
can overflow to a successful result containing infinity or NaN.
Single right-hand sides must have the matrix's exact vector_type.
Multiple right-hand sides must have the factorization's row count and scalar type.
The result is a new value. A failure writes no partial result into caller storage.
The implementation extends this to every named size in both precisions: LuMat2f through
LuMat4d and CholeskyMat2f through CholeskyMat4d, twelve factorization types rather than the
four named above. lu and cholesky take anytype for the same reason zmath.matrix.mul does.
There is also a multiple-right-hand-side solve for each factorization family. lu is Doolittle
with partial pivoting, so the stored diagonal of U is the pivot sequence and the determinant is
parity times the product of that diagonal; zmath.linalg reuses the pivot thresholds
zmath.matrix publishes instead of declaring a second spelling of the same constant.
zmath.geometry owns concrete f/d families in each dimension.
- 2D: point, vector, line, ray, segment, circle, and AABB.
- 3D: point, vector, line, ray, segment, plane, sphere, and AABB.
- 4D: point, vector, line, ray, segment, hyperplane, hypersphere, and AABB.
A point and a vector are distinct types. A line, ray, plane, or hyperplane constructor normalizes its direction or normal. A circle, sphere, or hypersphere requires a finite nonnegative radius. An AABB requires minimum[i] <= maximum[i].
Each dimension and precision has a concrete intersection union. It distinguishes none, point, two_points, segment, line, plane, hyperplane, coincident, and contained when applicable. It does not use null to combine these cases.
Predicates use one explicit policy:
pub const PredicatePolicy = enum { fast, adaptive_exact };fast reports the sign of the rounded scalar polynomial: infinity retains its sign,
while NaN returns error.NonFiniteResult.
adaptive_exact reports the exact polynomial sign for finite f32 or f64 inputs.
It can resolve a sign when fast rounds to zero or fails.
Floating constructions can still fail under either policy.
Predicates do not use an implicit epsilon.
Checked geometry rejects non-finite arguments and receiver fields with error.NonFiniteInput.
It rejects non-finite computed results or required construction intermediates with error.NonFiniteResult.
These checks apply in every build mode and return no partial result.
Infallible queries and conversions (isFinite, eq, toArray) and Bivector4.neg
do not perform checked validation and can inspect or preserve non-finite fields.
Geometry fields are writable. Constructors establish finite coordinates, unit directions or normals, nonnegative radii, and ordered bounds where applicable. Callers must preserve the geometric invariants after direct field writes. Checked calls validate finiteness. They do not re-run every constructor invariant check.
The implementation adds three tags beyond the nine listed above, because the nine do not name
every result the families can produce. box carries the common AABB of two AABBs.
circle and hypersphere carry center/radius summaries of a flat's ball cross-section.
Use the original flat to recover its placement and orientation; these payloads alone
do not represent an oriented disk or three-dimensional ball in ambient space.
Applicability follows the dimension. The 2D union has no flat tags.
The 3D union has no three-flat tag.
A coincident pair of two-sided infinite flats returns its span tag.
Identical rays, segments, balls, and boxes return coincident.
A ray's supporting line is not its whole common set.
contained identifies which entire operand is the common set.
A solid ball intersected by a ray returns its entire closed chord as segment,
including an inside ray's origin; a zero-length chord is point.
Proper ball/ball overlaps retain two_points boundary summaries. In 2D these are
the circumference crossings, not the common solid lens. In 3D and 4D they are
selected boundary points, not the complete boundary or solid intersection.
adaptive_exact is exact integer arithmetic, not an error-bounded approximation with a fast path.
Every finite f64 is m * 2^e with |m| < 2^53 and -1074 <= e <= 971, so scaling by 2^1074
reaches an exact integer; the predicate is then evaluated with schoolbook multiplication in a
fixed [132]u64 limb magnitude plus a separate sign, 8,448 bits on the stack, and the sign of
that integer is the sign of the real value. An f32 widens to f64 exactly, so one
representation covers both precisions. There is no epsilon, no value-dependent limb count, and
no fallback to the fast answer.
Coordinate subtraction happens after integer conversion, so rounding or overflow in the corresponding
floating difference cannot change the exact sign. Mixed-degree ball and ray comparisons align their
power-of-two scales before comparison. The fixed scratch bound covers these rescaled predicates.
One limit is stated rather than hidden. In 3D and 4D the affine intersections decide coplanarity
with the exact predicate on the ORIGINAL coordinates: in 3D that gate is the exact orientation
determinant, and in 4D it is an exact rank test on three difference vectors. They then project
into a plane and run the exact 2D predicate on the PROJECTED coordinates. The coplanarity gate,
which is the decision a plain evaluation gets wrong, is exact on the inputs as given.
Intersection PARAMETERS are f32/f64 and their range tests are ordinary comparisons.
zmath.algebra owns Cl3f/d, Cl4f/d, Pga3f/d, Pga4f/d, Cga3f/d, and Cga4f/d.
| Module | Ordered basis | Metric | Coefficients |
|---|---|---|---|
| Cl3 | e1, e2, e3 | +1, +1, +1 | 8 |
| Cl4 | e1, e2, e3, e4 | +1, +1, +1, +1 | 16 |
| Pga3 | e0, e1, e2, e3 | 0, +1, +1, +1 | 16 |
| Pga4 | e0, e1, e2, e3, e4 | 0, +1, +1, +1, +1 | 32 |
| Cga3 | e1, e2, e3, e_plus, e_minus | +1, +1, +1, +1, -1 | 32 |
| Cga4 | e1, e2, e3, e4, e_plus, e_minus | +1, +1, +1, +1, +1, -1 | 64 |
The basis order assigns a bit to each basis vector. A full multivector stores coefficients in ascending unsigned blade-mask order. The coefficient at index mask multiplies the ascending canonical blade represented by mask.
Reverse maps grade k to (-1)^(k * (k - 1) / 2). Cl and CGA use the right dual A * inverse(I). PGA uses the signed complementary Poincare dual.
For CGA, n_o = (e_minus - e_plus) / 2 and n_inf = e_minus + e_plus. Their inner product is -1. A Euclidean vector x embeds as n_o + x + (x dot x) * n_inf / 2.
Each algebra module publishes a normalization table before it becomes public. The table defines scale, sign, preconditions, and failure behavior for every named rotor, motor, versor, point, flat, round, and full multivector operation.
No public Algebra(p, q, r, T), generic vector, generic quaternion, or anytype product exists.
The signed complementary Poincare dual is implemented as: a grade k blade of mask m maps to
the blade of mask (top & ~m) with the sign (-1)^(k * (n - 1 - k)), where n is the basis
count. The double dual then carries the sign
(-1)^(k(n-1-k) + (n-k)(k-1)) = (-1)^(2k(n-k) - n) = (-1)^n, so dual(dual(A)) is A for Pga3
and -A for Pga4. That involution is the test that pins the sign rule.
Blade product tables are generated at comptime from the metric vector rather than written out, so
the Cga4 table is not a hand-maintained 64 by 64 constant. Each of the twelve types is a distinct
concrete type built from one private comptime generic, so no public signature carries anytype or
a type parameter. Storage is [2^n]T and is published per type as storage_bytes: 32 bytes for
Cl3f up to 512 for Cga4d. Those are stack value types, not allocations.
dot is scalarProduct(a, b.reverse()), and scalarProduct reads the diagonal
of the generated product table rather than all coefficient_count squared entries
of it. A blade pair lands on the scalar coefficient only when it multiplies with
itself, so every other pair lands above grade 0 and is discarded by the scalar
form. The surviving terms are the same terms, in the same ascending mask order,
with the same +0 seed, and with the same coeff * a * b operand order that
geometricProduct uses when it accumulates into its scalar slot, so the value is
what a.geometricProduct(b).c[0] is. Both skips of the full product are kept. A
zero left coefficient contributes nothing, and a zero metric entry contributes
nothing, which is the degenerate-metric rule: the Pga basis vector e0 has a
zero entry on the diagonal, so a Pga dot product still ignores it exactly as the
full product does. dot stays symmetric and norm stays dot(self, self).
inverseChecked uses the even Clifford conjugate, forms its product with self,
and checks that product for a finite, nonzero scalar sandwich. It rejects non-finite
input and a non-scalar sandwich. This is not a general invertibility test: an
odd-only blade returns null. Final scaled coefficients are not checked and can
overflow. The unchecked inverse requires the same scalar-sandwich precondition.
evenPart retains all even grades, including grade 6 where supported.
unitRotor, unitVersor, and unitFlat normalize candidate values; they do not
prove rotor membership, versor membership, or blade decomposability. The caller
must establish those properties. A single grade or an even grade alone is not
such a proof.
zig build test runs the numerical unit suite. zig build test-build installs
the same executable without running it. The optional cross-test host tool
uses cross-test.profile.sh to build that executable
for its selected targets. x86 targets use baseline+fma.
Oracle generation, stored-corpus replay, fuzzing, and benchmarks are in zmath-testing. Its scalar contract defines the corpus schema and target profile. Source exports and unit tests do not establish whole-domain accuracy. See the harness documentation for its commands.