zax.io

A guide to the Zax programming language

View project on GitHub

Zax fixed-point scalars

Field Value
Status Current conceptual design
Audience Human developers choosing, storing, converting, or calculating with fixed-point values
Applies To Finite binary-scaled fixed-point specializations of Zax integers; not a formal grammar or specification
Implementation State Not established by this repository
Owns Fixed-point mental model; coefficient width and bounded fractional-bit position; exact and Q names; range, quantum, initialization, literals, type-owned mathematical constants, arithmetic, full-precision product, bitwise behavior, numeric conversion, local representation relationships, costs, diagnostics, and portability
Does Not Own The F = 0 integer surface (integers); cross-family endian semantics (endianness); aggregate compatibility and coercive conversion syntax (structural shapes and compatibility); exact generic/factory syntax; complete operator forms; or real-literal source and raw-pattern grammar (literal source and operators)
Source / Provenance Legacy numeric input in basics, refined against current integer, endian, structural, literal, and operator design

Start with a coefficient and a fixed scale

A fixed-point value stores an integer coefficient while its type fixes where the binary point belongs:

myTemperature : I16F8 = 21.5

I16F8 has:

  • a signed 16-bit coefficient;
  • eight fractional bits;
  • a quantum of 1 / 256; and
  • the represented value coefficient * 2^-8.

myTemperature therefore stores coefficient 5504:

5504 / 256 = 21.5

The coefficient and fractional-bit count are both fixed by the concrete type. No value carries a runtime scale field.

Fixed-point and binary floating-point values are separate scalar families. A fixed-point value has no moving exponent, NaN, infinity, or subnormal class. See binary floating-point scalars for that model.

Width, fractional bits, and range

Write:

  • W for total coefficient width; and
  • F for the number of low coefficient bits interpreted fractionally.

The represented value is:

coefficient * 2^-F

The smallest step between adjacent values is the quantum:

quantum = 2^-F

For an unsigned coefficient:

minimum = 0
maximum = (2^W - 1) * 2^-F

For a signed two’s-complement coefficient:

minimum = -2^(W-1) * 2^-F
maximum = (2^(W-1) - 1) * 2^-F

For I16F8:

coefficient range = -32768 through 32767
value range       = -128 through 127.99609375
quantum           = 0.00390625

F remains within the represented coefficient:

signed:   0 <= F <= W - 1
unsigned: 0 <= F <= W

Zax does not currently use negative F or place the binary point beyond those bounds. Those forms would create a more general scaled-integer family with much larger scale factors rather than the bounded fixed-point model taught here.

Every coefficient bit pattern is valid. Signed coefficients retain the two’s-complement representation defined by Zax integers.

Exact names

Zax names an exact fixed-point specialization by total coefficient width and fractional-bit count:

I<W>F<FractionBits>
U<W>F<FractionBits>

Examples:

Signed Unsigned Coefficient width Fractional bits
I16F8 U16F8 16 8
I32F16 U32F16 32 16
I32F8 U32F8 32 8
I64F32 U64F32 64 32
I16F14 U16F14 16 14
I16F12 U16F12 16 12

The family root is the canonical current-execution-environment catalog:

Scalars.Fixed.I16F8
Scalars.Fixed.U32F16

Short names are transparent aliases of those public declarations. Explicit environment paths include Scalars.Fixed.Target.I16F8 and Scalars.Fixed.CompilerHost.I16F8. The later endianness section gives the absolute canonical little/big paths that complete concrete identity.

Zero fractional bits are ordinary integers

F = 0 is exactly the corresponding integer specialization:

I16F0 = I16
U16F0 = U16

This is one intrinsic family relationship, not a conversion between two coincidentally represented types. The integer owner defines the complete F = 0 surface.

Q aliases

Zax also provides transparent Q aliases for familiar source. Its convention is explicit: the integer count excludes the sign bit.

QI<i>F<f> => signed width   = 1 + i + f
QU<i>F<f> => unsigned width = i + f

The initial aliases are:

Zax exact names Q aliases
I16F8 / U16F8 QI7F8 / QU8F8
I32F16 / U32F16 QI15F16 / QU16F16
I32F8 / U32F8 QI23F8 / QU24F8
I64F32 / U64F32 QI31F32 / QU32F32
I16F14 / U16F14 QI1F14 / QU2F14
I16F12 / U16F12 QI3F12 / QU4F12

For example, QI7F8 and I16F8 are two names for one type. Q conventions outside Zax sometimes count the sign differently; the I16F8 form remains the unambiguous Zax-native spelling.

Zax does not provide domain names such as AudioFixed32 or ControlFixed16. Audio, control, simulation, and inference systems use several incompatible range/precision choices. A domain library or application can define a meaningful identity after validating its own requirements.

Endianness and storage

Endianness is part of the concrete intrinsic specialization:

Scalars.Fixed.Little.I16F8
Scalars.Fixed.Big.I16F8

An unqualified I16F8 selects the active execution environment’s concrete endianness. Explicit Target.I16F8 and CompilerHost.I16F8 paths select their named environments.

Numeric behavior always follows the declared type. Operating on a value whose endianness differs from the CPU may require byte swaps, endian-aware instructions, or software support. Complete cross-family behavior is defined by Zax endianness.

Storage uses the integer coefficient’s byte envelope. When W is not a multiple of eight:

  • the used part of the highest participating byte is its low-order portion;
  • unused high bits are non-value bits;
  • their contents are unspecified; and
  • they cannot affect defined fixed-point behavior.

Required alignment remains a target layout fact. Container padding remains separate from non-value bits inside the scalar’s own envelope.

Initialization and real-number source

Default initialization produces coefficient zero and numeric zero. Its logical representation is all zero.

A typed real number is realized directly in the selected fixed-point type:

myExact : I16F8 = 1.5
myRounded : I16F8 = 0.1

1.5 is represented exactly. Decimal 0.1 is not on the I16F8 grid, so ordinary realization rounds it to the nearest representable value, with ties to even. Ordinary precision loss is not a diagnostic.

A sufficiently small nonzero source may round to zero. A finite source whose magnitude exceeds the destination range is a compile-time error:

myTooLarge : I16F8 = 128.0 // error: outside the I16F8 range

The compiler computes the destination result independently of compiler-host floating arithmetic.

Decimal real source accepts e or E followed by an optionally signed decimal exponent:

myLarge : I32F8 = 1.5e+4
mySmall : I16F14 = 1E-3

The exponent is a power of ten applied to the exact mathematical source before fixed-point realization. Its presence makes 1e3 real-number source even without a fractional point. Bare e3 is not a number literal. Ordinary real source without another destination defaults to Float. Uncommitted exact-real arithmetic calculates before the fixed-point commitment:

myResult : I16F8 = 0.1 + 0.2
// Exact rational 0.3 is realized once as I16F8.

myConcrete : I16F8 =
  (: I16F8 = 0.1) + 0.2
// The inner declaration commits before +, so concrete I16F8 arithmetic occurs.

Binary and hexadecimal payloads may use a decimal p/P exponent denoting a power of two:

myBinary : I16F8 = b'1.1p-3'
myHex : I16F8 = h'1.8p+1'
h'1.8' // equivalent to h'1.8p0'

Exact token, default, prefix, and exponent behavior is owned by literal source and operators.

An exact fixed-point type also supplies type-qualified raw logical-pattern literals:

myRaw := I16F8.h'0180'
// Coefficient 384, represented value 1.5.

The payload supplies coefficient bits, not a mathematical hexadecimal real. Logical digit order is independent of storage endianness. Complete raw-pattern rules are defined by literal source and operators.

A future exact-realization request remains separate from raw-pattern source.

Type-owned mathematical constants

Every fixed-point type exposes an applicable constant only when the correctly rounded mathematical value fits:

myPi := I16F8.pi
myRoot := I32F16.sqrt2

The shared catalog is pi, tau, e, phi, sqrt2, sqrt3, ln2, ln10, log2e, log10e, invPi, twoOverPi, invSqrtPi, halfPi, and quarterPi.

Each value is rounded directly from its mathematical definition rather than derived from another already rounded constant. Fixed-point representation limits remain minimum, maximum, and quantum; the family has no NaN, infinity, signed zero, subnormal class, or floating epsilon.

Complete cross-family constant naming and source behavior is routed through literal source and operators.

Arithmetic keeps one concrete type

Ordinary fixed-point arithmetic uses operands of the same exact identity. Zax does not implicitly rescale independently typed operands:

myFine : I16F8 = 1.5
myCoarse : I16F4 = 2.0

myResult := myFine + myCoarse
// error: I16F8 and I16F4 are different fixed-point identities

Convert deliberately to the scale and range in which the operation should occur.

Addition and subtraction

Addition and subtraction apply to coefficients at the already shared scale. Their mathematical result retains W and F.

Required arithmetic checks whether the exact result fits. Overflow follows the applicable required, optional, wrapping, saturating, or reporting policy rather than silently widening the type.

Multiplication

Multiplying two coefficients doubles the apparent scale. An ordinary same-type product rescales once and rounds back to the existing grid:

resultCoefficient =
  roundToNearestEven((leftCoefficient * rightCoefficient) / 2^F)

For I16F8, 1.5 has coefficient 384:

384 * 384 / 256 = 576
576 / 256       = 2.25

An implementation may use a wider intermediate or a native fractional multiply instruction. The observable result remains I16F8.

A tiny exact product may round to zero without panicking:

myQuantum : I16F8 = 0.00390625
myRoundedToZero := myQuantum * myQuantum
// I16F8 value 0: the exact product is below half of one I16F8 quantum

Reporting operations may expose that precision was discarded. Overflow is a separate condition governed by the selected arithmetic policy.

Full-precision product

Use full precision product when the complete coefficient product and doubled fractional precision must remain available:

myLeft : I16F8 = 1.5
myRight : I16F8 = 2.25

myFull := myLeft full precision product myRight
// I32F16, exact value 3.375

For two like W, F operands, the associated result has sufficient coefficient width for the full product and 2F fractional bits, conceptually 2W, 2F. Exact generic associated-type declaration syntax remains future work.

Division

Ordinary division also retains the operand type:

resultCoefficient =
  roundToNearestEven((leftCoefficient * 2^F) / rightCoefficient)

Required division by zero fails. A finite quotient not lying on the existing grid rounds to nearest, ties to even. There is no universally exact finite fixed-point quotient type: values such as one third require unbounded fractional precision.

Remainder

Remainder applies to one fixed-point identity and uses a quotient truncated toward zero:

myValue : I16F8 = 10.5
myRemainder := myValue % 4
// The integer source becomes I16F8 value 4.0; result is I16F8 value 2.5.

Because both coefficients have the same F, coefficient remainder directly produces the fixed-point remainder. A zero divisor fails. Bare uncommitted 10.5 % 4 still depends on future default real-type selection because neither operand has selected a concrete identity.

Bitwise operations inspect the coefficient

Fixed-point complement, AND, OR, XOR, AND-NOT, shifts, rotations, counts, masks, and positions operate on or inspect the finite coefficient representation.

This is a deliberate fixed-point surface. It does not imply that every integer-specific numeric relationship transfers merely because fixed-point is implemented by the intrinsic integer family.

Left shift has the numeric effect of multiplication by a power of two when the result fits. Right shift may discard coefficient bits and therefore follows its defined signed rounding behavior. Rotations are representation operations rather than arithmetic scaling.

An integer source beside a concrete fixed-point operand realizes as that same fixed-point identity:

myBits : U32F8 = 3.0
myResult := myBits ^ 1
// `1` becomes U32F8 value 1.0, coefficient 256.

The literal does not silently become a raw U32F0 mask. To toggle the lowest coefficient bit, use one quantum:

myLowestChanged := myBits ^ 0.00390625
// U32F8 quantum, coefficient 1

or use an explicit same-endian U32 representation view for mask-oriented work.

Numeric conversion

Conversion preserves represented numeric value unless an explicit rounding or modular policy says otherwise.

Exact and optional conversion

Increasing F shifts the coefficient left and is exact when the destination coefficient fits. Decreasing F is exact only when every discarded low bit is zero.

myFraction : I16F8 = 1.5
myWhole := myFraction as I16?
// absent: 1.5 is not exactly representable as I16

myExact : I16F8 = 2.0
myInteger := myExact as I16
// exact value 2

Integer-to-fixed conversion is the reverse F = 0 relationship and also checks range after scaling.

Plain as Destination is available only when type facts or a compile-time-known value prove exact representability. It introduces no runtime panic. as Destination? reports any value-dependent range or fractional nonrepresentability through absence.

Rounded conversion

Rounded conversion selects a rounding rule and then checks the destination range. Precision loss and a tiny nonzero value rounding to zero do not panic. When the rounded result is outside a fixed/integer destination range, a required form may panic, an optional form reports absence, and a saturating form chooses the applicable endpoint.

Its exact source phrase remains operator work and examples using future wording must mark it as illustrative.

Integer modular narrowing as does not mean “discard fractional bits.” Modular coefficient conversion and numeric rounding are separate policies.

Representation and coercion

An exact representation operation can expose the coefficient independently of numeric conversion. Its final source spelling remains unsettled.

Safe scalar compatibility requires equality of:

  • coefficient logical width;
  • signedness and coefficient encoding;
  • fractional-bit position;
  • concrete endianness;
  • logical-bit placement;
  • storage extent and alignment;
  • non-value-bit placement and behavior; and
  • normalization and valid-pattern rules.

Local coercive conversion uses a reduced relationship. Equal coefficient width, compatible exact storage geometry, and a valid destination permit a zero-translation reinterpretation of signedness or F:

myValue : I16F8 = -1.5

myRawView :=
  myValue as coercive layout U16F4 &

The same bytes now have another known coefficient-and-scale interpretation. Numeric value need not be preserved.

Every integer-backed fixed-point bit pattern is valid, so matching-geometry integer/fixed coercion is safe even when it changes numeric interpretation. Integer/fixed cross-endian coercion may likewise expose a precisely known valid byte-order reinterpretation. Numeric endian conversion instead preserves value and may rearrange bytes.

Coercion is local and produces a reference view, not an independent copy. It never grants access authority. Existing mutable, writable, or varying authority may be preserved, but readonly, immutable, or final input cannot gain stronger access. An enclosing aggregate may still require unsafe as coercive when another participating scalar relation has value-dependent validity or writable invariant risk.

Complete aggregate, anchor, qualification, and same-storage view behavior is defined by Zax structural shapes and compatibility. unsafe cast is defined by Zax conversions and casts.

Availability and cost

A concrete fixed-point identity exists independently of whether one target provides every numeric operation. Its representation-level size, alignment, zero initialization, copying, and raw access remain known.

An available operation may use:

  • one or more native integer or fractional instructions;
  • wider registers or intermediates;
  • automatically supplied software;
  • runtime selection permitted by the target contract; or
  • compile-time evaluation.

The language promises the selected result and failure policy, not one lowering. Unsupported operations produce compiler diagnostics instead of silently returning an incomplete result.

Important visible costs include:

  • scale adjustment and rounding;
  • overflow or zero-divisor checks;
  • wider intermediates;
  • endian adaptation;
  • software emulation;
  • reporting or optional results; and
  • raw/coercive portability responsibility.

Diagnostics

Diagnostics should identify:

  • public and canonical fixed-point names;
  • coefficient width, signedness, F, quantum, and range;
  • concrete endianness and storage envelope;
  • the source and destination grids for a failed conversion;
  • exact, optional, rounded, wrapping, saturating, reporting, or coercive policy;
  • discarded precision separately from overflow;
  • division by zero;
  • mixed identities requiring explicit conversion;
  • unavailable target operation and support classification; and
  • invalid size, alignment, or representation relationship for coercion.

For example:

cannot exactly convert I16F8 value 1.5 to I16
I16 has no fractional values; use optional or explicitly rounded conversion

Portability and maturity

  • Exact fixed-point value sets and arithmetic results are stable.
  • An unqualified name selects the active environment’s concrete endianness; choose an absolute-endian path for stable stored byte order.
  • Profile and provider changes may change cost or operation availability, but not the meaning of an exact supported operation.
  • Q aliases are transparent names under Zax’s fixed convention.
  • Changing W, F, signedness, endianness, rounding, or overflow policy can change values, layout, source validity, and compatibility.

This document is current conceptual design, not formal grammar, a CPU-provider format, an ABI/wire contract, or an implementation mapping. Exact generic declarations, reflection APIs, rounded-conversion wording, and exhaustive protected operator forms remain future focused work and must preserve the programmer-visible behavior established here.