Plain-C reference core#

Can I get aether’s arithmetic as standalone C that gives the same bits?

Yes: aether_dsc.c_core is a pure-Python generator of C99 functions that reproduce, bit for bit, what aether’s expression templates evaluate on the host. A consumer such as hawk requests the functions it needs and inlines the generated text into a standalone C file, with no aether header and no C++ compiler involved.

What it generates#

Each function is a static inline C99 function over small value structs. The generator covers vectors (vec_dot, vec_norm, vec_sum, vec_cross, the componentwise operators and math functions), matrices (their elementwise mirrors plus mat_mv, mat_mm, mat_transpose and vec_outer), quaternions (quat_mul, quat_conj, quat_recip, quat_rotate and the pure/vec3 conversions) and four scalar formulas (rsqrt, sign, clip, remainder). Every kind exists at both real widths, f64 (double) and f32 (float); f32 text uses the f-suffixed libm and never promotes to double. c_core.kinds() lists the kinds. The format version tag is aether-c/1 (c_core.CORE_VERSION).

The contract#

The generated functions follow the evaluation order in the Numerics contract: right-nested folds with no zero seed, division by one reciprocal, neg as x * -1, norm as sqrt of the sum of squares, no promotion. The page also holds the table of generated arithmetic and the exact extent of the bit guarantee.

Build conditions#

The bit-identity holds under two conditions: compile the C translation unit with -ffp-contract=off, and compare against the templates’ default std:: host route (no AETHER_HOST_VECTOR_MATH, no fast-math). The sign of a zero from min or max of +0 and -0 is the one exception. The reasons are in Numerics contract.

Consuming it from Python#

The module uses only the standard library. Ask for a function by kind, shape and width, then emit the C text for it and everything it depends on:

from aether_dsc import c_core as C

f = C.function("vec_dot", shape=(3,), width="f64")
print(C.emit([f.name]))

emit writes the width typedef and struct types first, then the functions. The typedefs are guarded (#ifndef AETHER_C_F64_VEC3_T), but the functions are bare static inline and are emitted once per emit() call, so a consumer concatenating several calls must de-duplicate by name (or make one call with every name). f.deps lists the typedef and struct names a function needs, and f.libm the libm functions it calls, so a consumer can resolve dependencies and link libm only when required.

How it is pinned#

Three checks keep the generated text and the templates from drifting apart:

  • The CCore gtest suite (tests/test_CCore.cpp and .cu) compiles the corpus header and asserts that every generated function is memcmp-equal to the corresponding template expression.

  • tests/c_core/check_c99.sh compiles the corpus under a strict C99 gate: the umbrella header, and every part on its own.

  • tools/c_core/mint_golden.py --check verifies that the committed corpus (tests/c_core/aether_c_corpus.h and the md5-fenced parts under tests/c_core/corpus/) is exactly what the generator produces, and that no stale part is left behind. CI runs the last two before building; the CCore suite runs in CI’s normal build-and-test job.

Regenerating the corpus#

The corpus is committed as one small header per width and shape (corpus/f64/vec3.h, corpus/f32/mat2x3.h, …), with an umbrella aether_c_corpus.h that includes them all. It changes only when the core does. After changing the generator, re-mint the corpus and commit it with the change:

PYTHONPATH=dsc python3 tools/c_core/mint_golden.py

Minting is refused under $CI: a gate that regenerates its own expected value proves nothing.