aether-dsc 0.2.6__tar.gz → 0.2.7__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (23) hide show
  1. {aether_dsc-0.2.6/aether_dsc.egg-info → aether_dsc-0.2.7}/PKG-INFO +12 -1
  2. {aether_dsc-0.2.6 → aether_dsc-0.2.7}/README.md +11 -0
  3. {aether_dsc-0.2.6 → aether_dsc-0.2.7}/aether_dsc/__init__.py +1 -1
  4. aether_dsc-0.2.7/aether_dsc/c_core.py +421 -0
  5. {aether_dsc-0.2.6 → aether_dsc-0.2.7/aether_dsc.egg-info}/PKG-INFO +12 -1
  6. {aether_dsc-0.2.6 → aether_dsc-0.2.7}/aether_dsc.egg-info/SOURCES.txt +2 -0
  7. {aether_dsc-0.2.6 → aether_dsc-0.2.7}/pyproject.toml +1 -1
  8. aether_dsc-0.2.7/tests/test_c_core.py +221 -0
  9. {aether_dsc-0.2.6 → aether_dsc-0.2.7}/LICENSE +0 -0
  10. {aether_dsc-0.2.6 → aether_dsc-0.2.7}/NOTICE +0 -0
  11. {aether_dsc-0.2.6 → aether_dsc-0.2.7}/aether_dsc/_core.py +0 -0
  12. {aether_dsc-0.2.6 → aether_dsc-0.2.7}/aether_dsc/_payload/ee303363b05593f7617a9bbd46fd051fdba1ab5ed91498b223c1a2cd9c369790.bin +0 -0
  13. {aether_dsc-0.2.6 → aether_dsc-0.2.7}/aether_dsc/_rtc_check.py +0 -0
  14. {aether_dsc-0.2.6 → aether_dsc-0.2.7}/aether_dsc/_seal_impl.py +0 -0
  15. {aether_dsc-0.2.6 → aether_dsc-0.2.7}/aether_dsc/payload_headers.txt +0 -0
  16. {aether_dsc-0.2.6 → aether_dsc-0.2.7}/aether_dsc/payload_headers_host.txt +0 -0
  17. {aether_dsc-0.2.6 → aether_dsc-0.2.7}/aether_dsc/seal.py +0 -0
  18. {aether_dsc-0.2.6 → aether_dsc-0.2.7}/aether_dsc.egg-info/dependency_links.txt +0 -0
  19. {aether_dsc-0.2.6 → aether_dsc-0.2.7}/aether_dsc.egg-info/requires.txt +0 -0
  20. {aether_dsc-0.2.6 → aether_dsc-0.2.7}/aether_dsc.egg-info/top_level.txt +0 -0
  21. {aether_dsc-0.2.6 → aether_dsc-0.2.7}/setup.cfg +0 -0
  22. {aether_dsc-0.2.6 → aether_dsc-0.2.7}/tests/test_aether_dsc.py +0 -0
  23. {aether_dsc-0.2.6 → aether_dsc-0.2.7}/tests/test_ft_payload.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aether-dsc
3
- Version: 0.2.6
3
+ Version: 0.2.7
4
4
  Summary: aether-dsc — the sealed AETHER header payload: opaque, digest-named, no readable source in the wheel
5
5
  Author: Alessandro Masat
6
6
  License: Apache-2.0
@@ -59,6 +59,17 @@ The blob is a zlib-compressed tar — not obfuscation against a determined
59
59
  reader, just enough that `strings`/`cat` on the wheel does not hand back
60
60
  readable C++ source.
61
61
 
62
+ ## The C reference core
63
+
64
+ `aether_dsc.c_core` is a pure-Python (standard library only) generator of
65
+ plain C99 functions that reproduce, bit for bit, what aether's expression
66
+ templates evaluate on the host: vector, matrix, quaternion and scalar
67
+ kinds at `f64` and `f32`. `c_core.function(kind, shape=..., width=...)`
68
+ returns one function (its name, text, `deps` and `libm`), and
69
+ `c_core.emit(names)` writes the typedefs and functions for those names and
70
+ everything they depend on as one block of C text. Compile it with
71
+ `-ffp-contract=off`.
72
+
62
73
  ## Threading
63
74
 
64
75
  Free-threaded CPython (3.13t/3.14t) is supported: the compiled modules declare GIL-free operation and the GIL stays disabled after import. Any number of threads may call module-level functions, build, compile, plan and cache concurrently. Distinct objects may be used from distinct threads without synchronisation. One stateful object (a stream, capture, launcher, graph, composer, plan, pipeline, active set, host kernel or arg block) shared by several threads is memory-safe — its calls serialise and a consumed object raises — but the ORDER of those calls is the caller's responsibility, exactly as for a NumPy array or a CuPy stream. CUDA adds two rules of its own: a stream capture is begun, filled and ended by one thread, and while any capture is open no thread may synchronise the whole device (stream-level synchronisation is fine). GPU routes are supported on free-threaded 3.14; on 3.13t the CPU route runs.
@@ -40,6 +40,17 @@ The blob is a zlib-compressed tar — not obfuscation against a determined
40
40
  reader, just enough that `strings`/`cat` on the wheel does not hand back
41
41
  readable C++ source.
42
42
 
43
+ ## The C reference core
44
+
45
+ `aether_dsc.c_core` is a pure-Python (standard library only) generator of
46
+ plain C99 functions that reproduce, bit for bit, what aether's expression
47
+ templates evaluate on the host: vector, matrix, quaternion and scalar
48
+ kinds at `f64` and `f32`. `c_core.function(kind, shape=..., width=...)`
49
+ returns one function (its name, text, `deps` and `libm`), and
50
+ `c_core.emit(names)` writes the typedefs and functions for those names and
51
+ everything they depend on as one block of C text. Compile it with
52
+ `-ffp-contract=off`.
53
+
43
54
  ## Threading
44
55
 
45
56
  Free-threaded CPython (3.13t/3.14t) is supported: the compiled modules declare GIL-free operation and the GIL stays disabled after import. Any number of threads may call module-level functions, build, compile, plan and cache concurrently. Distinct objects may be used from distinct threads without synchronisation. One stateful object (a stream, capture, launcher, graph, composer, plan, pipeline, active set, host kernel or arg block) shared by several threads is memory-safe — its calls serialise and a consumed object raises — but the ORDER of those calls is the caller's responsibility, exactly as for a NumPy array or a CuPy stream. CUDA adds two rules of its own: a stream capture is begun, filled and ended by one thread, and while any capture is open no thread may synchronise the whole device (stream-level synchronisation is fine). GPU routes are supported on free-threaded 3.14; on 3.13t the CPU route runs.
@@ -50,7 +50,7 @@ __all__ = ["Payload", "seal", "payload", "version", "PAYLOAD_DIR"]
50
50
  #: `constexpr version()`), because a sealed payload speaks for exactly one
51
51
  #: aether checkout and a version skew between the two would silently claim
52
52
  #: otherwise.
53
- version = "0.2.6"
53
+ version = "0.2.7"
54
54
  __version__ = version # the conventional spelling
55
55
 
56
56
  _PAYLOAD_CACHE: Payload | None = None
@@ -0,0 +1,421 @@
1
+ # Copyright 2026 Alessandro Masat
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """aether's C reference core: a generator of plain C99 functions.
5
+
6
+ Each function reproduces, bit for bit, what aether's expression templates
7
+ evaluate on the host (see "Bit-identity formula table" in
8
+ docs/superpowers/specs/2026-10-16-embedded-c-target-design.md). The rules
9
+ that make that true:
10
+
11
+ * folds (dot, norm, sum) are right-nested, highest index outermost, with no
12
+ zero seed: ``e[N-1] + (e[N-2] + (... + e[0]))``;
13
+ * ``x / s`` is ``r = 1/s`` once, then ``r * x_k``;
14
+ * ``neg`` is ``x * -1``; ``norm`` is ``sqrt(ssq)`` (no hypot);
15
+ * nothing promotes: a ``f32`` text uses the ``f``-suffixed libm and only
16
+ ``aether_c_f32_t`` values.
17
+
18
+ Consumers: aether's gtest suite pins the corpus header minted from these
19
+ texts against the C++ templates; hawk imports this module, requests
20
+ functions by ``(kind, shape, width)``, resolves ``deps`` transitively and
21
+ inlines the texts verbatim into standalone C files. The module only uses the
22
+ standard library. Every ``function``/``struct``/``real_typedef`` result is
23
+ memoised by name in one registry, which ``emit`` reads.
24
+ """
25
+
26
+ from dataclasses import dataclass
27
+ from typing import Callable, Iterable
28
+
29
+ CORE_VERSION = "aether-c/1"
30
+ WIDTHS = ("f64", "f32")
31
+
32
+ # width -> (C type, libm suffix)
33
+ _WIDTH = {"f64": ("double", ""), "f32": ("float", "f")}
34
+
35
+
36
+ @dataclass(frozen=True)
37
+ class CFunction:
38
+ name: str
39
+ text: str
40
+ deps: tuple[str, ...]
41
+ libm: tuple[str, ...]
42
+
43
+
44
+ _REGISTRY: dict[str, CFunction] = {}
45
+
46
+
47
+ def _check_width(width: str) -> None:
48
+ if width not in _WIDTH:
49
+ raise ValueError(f"unknown width {width!r}; expected one of {WIDTHS}")
50
+
51
+
52
+ _RANK: dict[str, int] = {} # 0 width typedef, 1 struct typedef, 2 function
53
+
54
+
55
+ def _memo(item: CFunction, rank: int) -> CFunction:
56
+ _RANK.setdefault(item.name, rank)
57
+ return _REGISTRY.setdefault(item.name, item)
58
+
59
+
60
+ def real_type(width: str) -> str:
61
+ _check_width(width)
62
+ return _WIDTH[width][0]
63
+
64
+
65
+ def _t(width: str) -> str:
66
+ return f"aether_c_{width}_t"
67
+
68
+
69
+ def real_typedef(width: str) -> CFunction:
70
+ _check_width(width)
71
+ name = _t(width)
72
+ guard = name.upper()
73
+ text = f"#ifndef {guard}\n#define {guard}\ntypedef {_WIDTH[width][0]} {name};\n#endif\n"
74
+ return _memo(CFunction(name, text, (), ()), 0)
75
+
76
+
77
+ def _check_shape(shape) -> None:
78
+ """Every extent an ``int`` (not ``bool``) >= 1; raised before anything is memoised."""
79
+ if not all(type(n) is int and n >= 1 for n in shape):
80
+ raise ValueError(f"invalid shape {shape!r}: every extent must be an int >= 1")
81
+
82
+
83
+ def _struct_name(shape: tuple[int, ...], width: str) -> str:
84
+ _check_shape(shape)
85
+ if len(shape) == 1:
86
+ return f"aether_c_{width}_vec{shape[0]}_t"
87
+ if len(shape) == 2:
88
+ return f"aether_c_{width}_mat{shape[0]}x{shape[1]}_t"
89
+ raise ValueError(f"no struct for shape {shape!r}")
90
+
91
+
92
+ def struct(shape: tuple[int, ...], width: str) -> CFunction:
93
+ shape = tuple(shape)
94
+ name = _struct_name(shape, width) # validates the shape before anything is memoised
95
+ real = real_typedef(width)
96
+ if len(shape) == 1:
97
+ body = f"{real.name} v[{shape[0]}];"
98
+ else:
99
+ body = f"{real.name} m[{shape[0]}][{shape[1]}];"
100
+ guard = name.upper()
101
+ text = f"#ifndef {guard}\n#define {guard}\ntypedef struct {{ {body} }} {name};\n#endif\n"
102
+ return _memo(CFunction(name, text, (real.name,), ()), 1)
103
+
104
+
105
+ # ---- per-family formatters; each returns (body lines, libm) ----
106
+ # ``cells`` are the component accessors in row-major order ("v[k]" or "m[r][c]"),
107
+ # so one formatter serves a vector kind and its matrix mirror; ``x`` names the
108
+ # operand of a unary kind.
109
+
110
+ def _fold(terms: list[str]) -> str:
111
+ """Right-nested, highest index outermost, no seed."""
112
+ if len(terms) == 1:
113
+ return terms[0]
114
+ return f"({terms[-1]} + {_fold(terms[:-1])})"
115
+
116
+
117
+ def _cells(shape: tuple[int, ...]) -> list[str]:
118
+ if len(shape) == 1:
119
+ return [f"v[{k}]" for k in range(shape[0])]
120
+ return [f"m[{r}][{c}]" for r in range(shape[0]) for c in range(shape[1])]
121
+
122
+
123
+ def _fold_kind(op: str, cells: list[str], w: str, x: str) -> tuple[list[str], tuple[str, ...]]:
124
+ if op == "vec_dot":
125
+ return [f"return {_fold([f'(a.{c} * b.{c})' for c in cells])};"], ()
126
+ if op == "vec_norm":
127
+ sq = _fold([f"({x}.{c} * {x}.{c})" for c in cells])
128
+ return [f"return sqrt{_WIDTH[w][1]}({sq});"], (f"sqrt{_WIDTH[w][1]}",)
129
+ return [f"return {_fold([f'{x}.{c}' for c in cells])};"], ()
130
+
131
+
132
+ def _out_body(cells: list[str], exprs: list[str]) -> list[str]:
133
+ return [f"out.{c} = {e};" for c, e in zip(cells, exprs)]
134
+
135
+
136
+ def _cwise_binary(kind: str, cells: list[str], w: str, x: str) -> tuple[list[str], tuple[str, ...]]:
137
+ s = _WIDTH[w][1]
138
+ if kind in _ARITH:
139
+ op = _ARITH[kind]
140
+ return _out_body(cells, [f"(a.{c} {op} b.{c})" for c in cells]), ()
141
+ fn = f"{_LIBM_BINARY[kind]}{s}"
142
+ return _out_body(cells, [f"{fn}(a.{c}, b.{c})" for c in cells]), (fn,)
143
+
144
+
145
+ def _cwise_unary(kind: str, cells: list[str], w: str, x: str) -> tuple[list[str], tuple[str, ...]]:
146
+ t = _t(w)
147
+ if kind == "vec_neg":
148
+ return _out_body(cells, [f"({x}.{c} * ({t})-1.0)" for c in cells]), ()
149
+ if kind == "vec_rsqrt":
150
+ fn = f"sqrt{_WIDTH[w][1]}"
151
+ return _out_body(cells, [f"(({t})1.0 / {fn}({x}.{c}))" for c in cells]), (fn,)
152
+ fn = f"{_LIBM_UNARY[kind]}{_WIDTH[w][1]}"
153
+ return _out_body(cells, [f"{fn}({x}.{c})" for c in cells]), (fn,)
154
+
155
+
156
+ def _scale(kind: str, cells: list[str], w: str, x: str) -> tuple[list[str], tuple[str, ...]]:
157
+ t = _t(w)
158
+ if kind == "vec_scale":
159
+ return _out_body(cells, [f"(s * a.{c})" for c in cells]), ()
160
+ pre = [f"const {t} r = (({t})1.0 / s);"]
161
+ return pre + _out_body(cells, [f"(r * a.{c})" for c in cells]), ()
162
+
163
+
164
+ def _cross(kind: str, cells: list[str], w: str, x: str) -> tuple[list[str], tuple[str, ...]]:
165
+ return _out_body(cells, [
166
+ "((a.v[1] * b.v[2]) - (a.v[2] * b.v[1]))",
167
+ "((a.v[2] * b.v[0]) - (a.v[0] * b.v[2]))",
168
+ "((a.v[0] * b.v[1]) - (a.v[1] * b.v[0]))",
169
+ ]), ()
170
+
171
+
172
+ _ARITH = {"vec_add": "+", "vec_sub": "-", "vec_mul": "*", "vec_cwise_div": "/"}
173
+ _LIBM_BINARY = {"vec_min": "fmin", "vec_max": "fmax", "vec_pow": "pow"}
174
+ _LIBM_UNARY = {"vec_abs": "fabs", "vec_sqrt": "sqrt", "vec_exp": "exp", "vec_log": "log",
175
+ "vec_sin": "sin", "vec_cos": "cos", "vec_tan": "tan", "vec_tanh": "tanh",
176
+ "vec_asin": "asin", "vec_acos": "acos", "vec_atan": "atan"}
177
+
178
+ # kind -> (arg layout, result, body formatter); layout "fold2"/"bin"=(a,b), "un"=(a) or (m),
179
+ # "scalar"=(a, s); result "vec" or "real". Insertion order here IS the order kinds() reports.
180
+ _VEC_KINDS: dict[str, tuple[str, str, Callable]] = {}
181
+
182
+
183
+ def _register(names: Iterable[str], layout: str, result: str, fmt: Callable) -> None:
184
+ for n in names:
185
+ _VEC_KINDS[n] = (layout, result, fmt)
186
+
187
+
188
+ _register(("vec_dot",), "fold2", "real", _fold_kind)
189
+ _register(("vec_norm", "vec_sum"), "un", "real", _fold_kind)
190
+ _register(("vec_cross",), "fold2", "vec", _cross)
191
+ _register(tuple(_ARITH), "fold2", "vec", _cwise_binary)
192
+ _register(("vec_scale", "vec_div_s"), "scalar", "vec", _scale)
193
+ _register(("vec_min", "vec_max", "vec_pow"), "fold2", "vec", _cwise_binary)
194
+ _register(("vec_neg", "vec_abs", "vec_sqrt", "vec_rsqrt", "vec_exp", "vec_log", "vec_sin",
195
+ "vec_cos", "vec_tan", "vec_tanh", "vec_asin", "vec_acos", "vec_atan"),
196
+ "un", "vec", _cwise_unary)
197
+ assert set(_LIBM_BINARY) | set(_ARITH) | set(_LIBM_UNARY) <= set(_VEC_KINDS)
198
+
199
+ _VEC_ORDER = tuple(_VEC_KINDS)
200
+ # matrix mirrors: every elementwise vector kind (not the folds or cross) over m[r][c]
201
+ _MAT_CWISE = tuple("mat_" + k[4:] for k, v in _VEC_KINDS.items()
202
+ if v[1] == "vec" and k != "vec_cross")
203
+ _MAT_OTHER = ("mat_sum", "mat_mv", "mat_mm", "mat_transpose", "vec_outer")
204
+ _QUAT = ("quat_mul", "quat_conj", "quat_recip", "quat_rotate", "quat_as_pure", "quat_as_vec3")
205
+ _SCALARS = ("rsqrt", "sign", "clip", "remainder")
206
+
207
+
208
+ def kinds() -> tuple[str, ...]:
209
+ return _VEC_ORDER + _MAT_CWISE + _MAT_OTHER + _QUAT + _SCALARS
210
+
211
+
212
+ def _render(kind: str, shape: tuple[int, ...], width: str, ret: str, name: str,
213
+ params: str, body: list[str], deps: tuple[str, ...], libm: tuple[str, ...]) -> CFunction:
214
+ head = f"/* aether-c: {kind} {shape!r} {width} {CORE_VERSION} */\n"
215
+ lines = "".join(f" {ln}\n" for ln in body)
216
+ text = f"{head}static inline {ret} {name}({params})\n{{\n{lines}}}\n"
217
+ return _memo(CFunction(name, text, deps, tuple(sorted(set(libm)))), 2)
218
+
219
+
220
+ def _deps(width: str, *shapes: tuple[int, ...]) -> tuple[str, ...]:
221
+ """Width typedef first, then each struct type in order of first use."""
222
+ out = [real_typedef(width).name]
223
+ for sh in shapes:
224
+ n = struct(sh, width).name
225
+ if n not in out:
226
+ out.append(n)
227
+ return tuple(out)
228
+
229
+
230
+ def _require_rank(kind: str, shape: tuple[int, ...], rank: int) -> None:
231
+ if len(shape) != rank:
232
+ raise ValueError(f"{kind} needs a rank-{rank} shape, got {shape!r}")
233
+
234
+
235
+ def _vector_function(kind: str, shape: tuple[int, ...], width: str) -> CFunction:
236
+ """A vector kind, or (kind ``mat_*``) its elementwise matrix mirror."""
237
+ is_mat = kind.startswith("mat_")
238
+ base = "vec_" + kind[4:] if is_mat else kind
239
+ _require_rank(kind, shape, 2 if is_mat else 1)
240
+ if kind == "vec_cross" and shape != (3,):
241
+ raise ValueError(f"vec_cross is defined only for shape (3,), got {shape!r}")
242
+ layout, result, fmt = _VEC_KINDS[base]
243
+ t, st = _t(width), _struct_name(shape, width)
244
+ x = "m" if is_mat else "a"
245
+ body, libm = fmt(base, _cells(shape), width, x)
246
+ params = {"fold2": f"{st} a, {st} b", "un": f"{st} {x}", "scalar": f"{st} a, {t} s"}[layout]
247
+ if result == "vec":
248
+ body = [f"{st} out;"] + body + ["return out;"]
249
+ prefix = st[len(f"aether_c_{width}_"):-len("_t")]
250
+ name = f"aether_c_{width}_{prefix}_{kind[4:]}"
251
+ return _render(kind, shape, width, st if result == "vec" else t, name, params, body,
252
+ _deps(width, shape), libm)
253
+
254
+
255
+ def _matrix_function(kind: str, shape: tuple[int, ...], shape2, width: str) -> CFunction:
256
+ t, w = _t(width), width
257
+ if kind == "vec_outer":
258
+ _require_rank(kind, shape, 1)
259
+ if shape2 is None or len(shape2) != 1:
260
+ raise ValueError(f"vec_outer needs shape2 of rank 1, got shape={shape!r} shape2={shape2!r}")
261
+ out_shape = (shape[0], shape2[0])
262
+ ut, vt, ot = _struct_name(shape, w), _struct_name(shape2, w), _struct_name(out_shape, w)
263
+ body = [f"{ot} out;"] + _out_body(_cells(out_shape), [
264
+ f"(u.v[{r}] * v.v[{c}])" for r in range(shape[0]) for c in range(shape2[0])]) + ["return out;"]
265
+ name = f"aether_c_{w}_vec{shape[0]}_outer_vec{shape2[0]}"
266
+ return _render(kind, shape, w, ot, name, f"{ut} u, {vt} v", body,
267
+ _deps(w, shape, shape2, out_shape), ())
268
+ _require_rank(kind, shape, 2)
269
+ rows, cols = shape
270
+ mt = _struct_name(shape, w)
271
+ if kind == "mat_sum":
272
+ terms = [f"(m.{c} * ({t})1.0)" for c in _cells(shape)]
273
+ return _render(kind, shape, w, t, f"aether_c_{w}_mat{rows}x{cols}_sum", f"{mt} m",
274
+ [f"return {_fold(terms)};"], _deps(w, shape), ())
275
+ if kind == "mat_transpose":
276
+ out_shape = (cols, rows)
277
+ ot = _struct_name(out_shape, w)
278
+ body = [f"{ot} out;"] + _out_body(_cells(out_shape), [
279
+ f"m.m[{r}][{c}]" for c in range(cols) for r in range(rows)]) + ["return out;"]
280
+ return _render(kind, shape, w, ot, f"aether_c_{w}_mat{rows}x{cols}_transpose", f"{mt} m",
281
+ body, _deps(w, shape, out_shape), ())
282
+ if kind == "mat_mv":
283
+ vt, ot = _struct_name((cols,), w), _struct_name((rows,), w)
284
+ body = [f"{ot} out;"] + _out_body(_cells((rows,)), [
285
+ _fold([f"(m.m[{r}][{c}] * v.v[{c}])" for c in range(cols)]) for r in range(rows)]) + ["return out;"]
286
+ return _render(kind, shape, w, ot, f"aether_c_{w}_mat{rows}x{cols}_mv", f"{mt} m, {vt} v",
287
+ body, _deps(w, shape, (cols,), (rows,)), ())
288
+ # mat_mm
289
+ if shape2 is None or len(shape2) != 2 or shape2[0] != cols:
290
+ raise ValueError(f"mat_mm needs shape2 (K, C) with K == {cols}, got shape={shape!r} shape2={shape2!r}")
291
+ out_shape = (rows, shape2[1])
292
+ rt, ot = _struct_name(shape2, w), _struct_name(out_shape, w)
293
+ body = [f"{ot} out;"] + _out_body(_cells(out_shape), [
294
+ _fold([f"(l.m[{i}][{k}] * r.m[{k}][{j}])" for k in range(cols)])
295
+ for i in range(rows) for j in range(shape2[1])]) + ["return out;"]
296
+ name = f"aether_c_{w}_mat{rows}x{cols}_mm_mat{shape2[0]}x{shape2[1]}"
297
+ return _render(kind, shape, w, ot, name, f"{mt} l, {rt} r", body,
298
+ _deps(w, shape, shape2, out_shape), ())
299
+
300
+
301
+ def _quat_function(kind: str, shape, shape2, width: str) -> CFunction:
302
+ """Quaternions are vec4 values laid out (w, x, y, z); a 3-vector is a vec3."""
303
+ if shape is not None or shape2 is not None:
304
+ raise ValueError(f"{kind} takes no shape, got shape={shape!r} shape2={shape2!r}")
305
+ t, w = _t(width), width
306
+ q4, q3 = (4,), (3,)
307
+ st4, st3 = _struct_name(q4, w), _struct_name(q3, w)
308
+ conj = lambda k: f"(q.v[{k}] * ({t})-1.0)"
309
+ pre: list[str] = []
310
+ if kind == "quat_mul":
311
+ # (l_i * r_j) term indices per output component, in aether's left-to-right order
312
+ pairs = [[(0, 0), (1, 1), (2, 2), (3, 3)], [(0, 1), (1, 0), (2, 3), (3, 2)],
313
+ [(0, 2), (1, 3), (2, 0), (3, 1)], [(0, 3), (1, 2), (2, 1), (3, 0)]]
314
+ signs = [("-", "-", "-"), ("+", "+", "-"), ("-", "+", "+"), ("+", "-", "+")]
315
+ exprs = []
316
+ for ps, sg in zip(pairs, signs):
317
+ e = f"(l.v[{ps[0][0]}] * r.v[{ps[0][1]}])"
318
+ for (i, j), op in zip(ps[1:], sg):
319
+ e = f"({e} {op} (l.v[{i}] * r.v[{j}]))"
320
+ exprs.append(e)
321
+ params, ret, shapes, cells = f"{st4} l, {st4} r", st4, (q4,), _cells(q4)
322
+ elif kind == "quat_conj":
323
+ exprs = ["q.v[0]"] + [conj(k) for k in (1, 2, 3)]
324
+ params, ret, shapes, cells = f"{st4} q", st4, (q4,), _cells(q4)
325
+ elif kind == "quat_recip":
326
+ ssq = _fold([f"(q.v[{k}] * q.v[{k}])" for k in range(4)])
327
+ pre = [f"const {t} r = (({t})1.0 / {ssq});"]
328
+ exprs = ["(r * q.v[0])"] + [f"(r * {conj(k)})" for k in (1, 2, 3)]
329
+ params, ret, shapes, cells = f"{st4} q", st4, (q4,), _cells(q4)
330
+ elif kind == "quat_rotate":
331
+ # component i uses j = i+1, k = i+2 (mod 3); u_j is q.v[j+1]
332
+ for i, n in enumerate("xyz"):
333
+ j, k = (i + 1) % 3, (i + 2) % 3
334
+ pre.append(f"const {t} t{n} = (({t})2.0 * ((q.v[{j + 1}] * v.v[{k}]) - (q.v[{k + 1}] * v.v[{j}])));")
335
+ exprs = []
336
+ for i in range(3):
337
+ j, k = (i + 1) % 3, (i + 2) % 3
338
+ exprs.append(f"((v.v[{i}] + (q.v[0] * t{'xyz'[i]})) + "
339
+ f"((q.v[{j + 1}] * t{'xyz'[k]}) - (q.v[{k + 1}] * t{'xyz'[j]})))")
340
+ params, ret, shapes, cells = f"{st4} q, {st3} v", st3, (q4, q3), _cells(q3)
341
+ elif kind == "quat_as_pure":
342
+ exprs = [f"({t})0.0"] + [f"v.v[{k}]" for k in range(3)]
343
+ params, ret, shapes, cells = f"{st3} v", st4, (q3, q4), _cells(q4)
344
+ else: # quat_as_vec3
345
+ exprs = [f"q.v[{k}]" for k in (1, 2, 3)]
346
+ params, ret, shapes, cells = f"{st4} q", st3, (q4, q3), _cells(q3)
347
+ body = pre + [f"{ret} out;"] + _out_body(cells, exprs) + ["return out;"]
348
+ return _render(kind, q4, w, ret, f"aether_c_{w}_quat_{kind[5:]}", params, body,
349
+ _deps(w, *shapes), ())
350
+
351
+
352
+ def _scalar_function(kind: str, width: str) -> CFunction:
353
+ t, s = _t(width), _WIDTH[width][1]
354
+ real = real_typedef(width)
355
+ sq, fm, cs = f"sqrt{s}", f"fmod{s}", f"copysign{s}"
356
+ if kind == "rsqrt":
357
+ params, body, libm = f"{t} x", [f"return (({t})1.0 / {sq}(x));"], (sq,)
358
+ elif kind == "sign":
359
+ params, libm = f"{t} x", ()
360
+ body = [f"return (x > ({t})0.0) ? ({t})1.0 : ((x < ({t})0.0) ? ({t})-1.0 : ({t})0.0);"]
361
+ elif kind == "clip":
362
+ params, libm = f"{t} x, {t} lo, {t} hi", ()
363
+ body = [f"const {t} t = ((x >= lo) || (x != x)) ? x : lo;",
364
+ "return ((t <= hi) || (t != t)) ? t : hi;"]
365
+ else:
366
+ params, libm = f"{t} a, {t} b", (fm, cs)
367
+ body = [f"{t} m = {fm}(a, b);",
368
+ f"if (m != ({t})0.0) {{",
369
+ f" if ((b < ({t})0.0) != (m < ({t})0.0)) {{",
370
+ " m += b;",
371
+ " }",
372
+ "} else {",
373
+ f" m = {cs}(({t})0.0, b);",
374
+ "}",
375
+ "return m;"]
376
+ return _render(kind, (), width, t, f"aether_c_{width}_{kind}", params, body, (real.name,), libm)
377
+
378
+
379
+ def function(kind: str, *, shape=None, shape2=None, width: str = "f64") -> CFunction:
380
+ _check_width(width)
381
+ if kind in _SCALARS:
382
+ if shape is not None or shape2 is not None:
383
+ raise ValueError(f"{kind} takes no shape, got shape={shape!r} shape2={shape2!r}")
384
+ return _scalar_function(kind, width)
385
+ if kind not in kinds():
386
+ raise ValueError(f"unknown kind {kind!r}; known kinds: {', '.join(kinds())}")
387
+ if kind in _QUAT:
388
+ return _quat_function(kind, shape, shape2, width)
389
+ if shape is None:
390
+ raise ValueError(f"{kind} needs a shape")
391
+ shape = tuple(shape)
392
+ shape2 = None if shape2 is None else tuple(shape2)
393
+ _check_shape(shape)
394
+ if shape2 is not None:
395
+ _check_shape(shape2)
396
+ if kind not in ("mat_mm", "vec_outer"):
397
+ raise ValueError(f"{kind} takes no shape2, got shape2={shape2!r}")
398
+ if kind in _VEC_KINDS or kind in _MAT_CWISE:
399
+ return _vector_function(kind, shape, width)
400
+ return _matrix_function(kind, shape, shape2, width)
401
+
402
+
403
+ def emit(names: Iterable[str]) -> str:
404
+ """The closure of ``names``: typedefs, then structs, then functions, each once."""
405
+ order: list[str] = []
406
+ seen: set[str] = set()
407
+
408
+ def visit(name: str) -> None:
409
+ if name in seen:
410
+ return
411
+ if name not in _REGISTRY:
412
+ raise KeyError(name)
413
+ seen.add(name)
414
+ for dep in _REGISTRY[name].deps:
415
+ visit(dep)
416
+ order.append(name)
417
+
418
+ for name in names:
419
+ visit(name)
420
+ order.sort(key=_RANK.__getitem__) # stable: dependency order kept within a class
421
+ return "\n".join(_REGISTRY[n].text for n in order)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aether-dsc
3
- Version: 0.2.6
3
+ Version: 0.2.7
4
4
  Summary: aether-dsc — the sealed AETHER header payload: opaque, digest-named, no readable source in the wheel
5
5
  Author: Alessandro Masat
6
6
  License: Apache-2.0
@@ -59,6 +59,17 @@ The blob is a zlib-compressed tar — not obfuscation against a determined
59
59
  reader, just enough that `strings`/`cat` on the wheel does not hand back
60
60
  readable C++ source.
61
61
 
62
+ ## The C reference core
63
+
64
+ `aether_dsc.c_core` is a pure-Python (standard library only) generator of
65
+ plain C99 functions that reproduce, bit for bit, what aether's expression
66
+ templates evaluate on the host: vector, matrix, quaternion and scalar
67
+ kinds at `f64` and `f32`. `c_core.function(kind, shape=..., width=...)`
68
+ returns one function (its name, text, `deps` and `libm`), and
69
+ `c_core.emit(names)` writes the typedefs and functions for those names and
70
+ everything they depend on as one block of C text. Compile it with
71
+ `-ffp-contract=off`.
72
+
62
73
  ## Threading
63
74
 
64
75
  Free-threaded CPython (3.13t/3.14t) is supported: the compiled modules declare GIL-free operation and the GIL stays disabled after import. Any number of threads may call module-level functions, build, compile, plan and cache concurrently. Distinct objects may be used from distinct threads without synchronisation. One stateful object (a stream, capture, launcher, graph, composer, plan, pipeline, active set, host kernel or arg block) shared by several threads is memory-safe — its calls serialise and a consumed object raises — but the ORDER of those calls is the caller's responsibility, exactly as for a NumPy array or a CuPy stream. CUDA adds two rules of its own: a stream capture is begun, filled and ended by one thread, and while any capture is open no thread may synchronise the whole device (stream-level synchronisation is fine). GPU routes are supported on free-threaded 3.14; on 3.13t the CPU route runs.
@@ -6,6 +6,7 @@ aether_dsc/__init__.py
6
6
  aether_dsc/_core.py
7
7
  aether_dsc/_rtc_check.py
8
8
  aether_dsc/_seal_impl.py
9
+ aether_dsc/c_core.py
9
10
  aether_dsc/payload_headers.txt
10
11
  aether_dsc/payload_headers_host.txt
11
12
  aether_dsc/seal.py
@@ -16,4 +17,5 @@ aether_dsc.egg-info/requires.txt
16
17
  aether_dsc.egg-info/top_level.txt
17
18
  aether_dsc/_payload/ee303363b05593f7617a9bbd46fd051fdba1ab5ed91498b223c1a2cd9c369790.bin
18
19
  tests/test_aether_dsc.py
20
+ tests/test_c_core.py
19
21
  tests/test_ft_payload.py
@@ -14,7 +14,7 @@ name = "aether-dsc"
14
14
  # aether-dsc ships a SEALED payload of that exact checkout, so a version skew
15
15
  # between the two would silently claim a payload speaks for a different
16
16
  # aether than it does.
17
- version = "0.2.6"
17
+ version = "0.2.7"
18
18
  description = "aether-dsc — the sealed AETHER header payload: opaque, digest-named, no readable source in the wheel"
19
19
  readme = "README.md"
20
20
  requires-python = ">=3.9"
@@ -0,0 +1,221 @@
1
+ # Copyright 2026 Alessandro Masat
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ import re, subprocess, shutil, pytest
4
+ from aether_dsc import c_core as C
5
+
6
+ F64_DOT3 = ("static inline aether_c_f64_t aether_c_f64_vec3_dot(aether_c_f64_vec3_t a, aether_c_f64_vec3_t b)\n"
7
+ "{\n return ((a.v[2] * b.v[2]) + ((a.v[1] * b.v[1]) + (a.v[0] * b.v[0])));\n}\n")
8
+
9
+ def test_vec3_dot_is_right_nested_with_no_seed():
10
+ f = C.function("vec_dot", shape=(3,), width="f64")
11
+ assert f.name == "aether_c_f64_vec3_dot" and f.text.endswith(F64_DOT3)
12
+ assert f.text.startswith("/* aether-c: vec_dot (3,) f64 aether-c/1 */\n")
13
+ assert f.deps == ("aether_c_f64_t", "aether_c_f64_vec3_t") and f.libm == ()
14
+
15
+ def test_vec3_norm_is_sqrt_of_right_nested_squares():
16
+ assert "return sqrt(((a.v[2] * a.v[2]) + ((a.v[1] * a.v[1]) + (a.v[0] * a.v[0]))));" in C.function("vec_norm", shape=(3,)).text
17
+
18
+ def test_vec1_dot_is_one_product():
19
+ assert "return (a.v[0] * b.v[0]);" in C.function("vec_dot", shape=(1,)).text
20
+
21
+ def test_div_s_multiplies_by_one_reciprocal():
22
+ t = C.function("vec_div_s", shape=(2,)).text
23
+ assert "const aether_c_f64_t r = ((aether_c_f64_t)1.0 / s);" in t and "out.v[1] = (r * a.v[1]);" in t
24
+
25
+ def test_neg_multiplies_by_minus_one():
26
+ assert "out.v[0] = (a.v[0] * (aether_c_f64_t)-1.0);" in C.function("vec_neg", shape=(1,)).text
27
+
28
+ def test_min_keeps_operand_order():
29
+ assert "out.v[0] = fmin(a.v[0], b.v[0]);" in C.function("vec_min", shape=(1,)).text
30
+
31
+ def test_cross_formulas_and_shape_guard():
32
+ t = C.function("vec_cross", shape=(3,)).text
33
+ assert "out.v[0] = ((a.v[1] * b.v[2]) - (a.v[2] * b.v[1]));" in t
34
+ assert "out.v[1] = ((a.v[2] * b.v[0]) - (a.v[0] * b.v[2]));" in t
35
+ assert "out.v[2] = ((a.v[0] * b.v[1]) - (a.v[1] * b.v[0]));" in t
36
+ with pytest.raises(ValueError, match=r"\(4,\)"):
37
+ C.function("vec_cross", shape=(4,))
38
+
39
+ def test_scalar_formulas():
40
+ assert "return ((aether_c_f64_t)1.0 / sqrt(x));" in C.function("rsqrt").text
41
+ assert "const aether_c_f64_t t = ((x >= lo) || (x != x)) ? x : lo;" in C.function("clip").text
42
+ assert "return ((t <= hi) || (t != t)) ? t : hi;" in C.function("clip").text
43
+ r = C.function("remainder").text
44
+ assert "aether_c_f64_t m = fmod(a, b);" in r and "m = copysign((aether_c_f64_t)0.0, b);" in r
45
+ assert C.function("remainder").libm == ("copysign", "fmod")
46
+
47
+ def test_f32_never_promotes():
48
+ t = C.function("vec_norm", shape=(3,), width="f32").text
49
+ assert "sqrtf(" in t and "aether_c_f32_t" in t and "double" not in t
50
+ assert C.function("vec_norm", shape=(3,), width="f32").libm == ("sqrtf",)
51
+
52
+ def test_struct_typedefs_are_guarded():
53
+ s = C.struct((3,), "f64")
54
+ assert s.text == "#ifndef AETHER_C_F64_VEC3_T\n#define AETHER_C_F64_VEC3_T\ntypedef struct { aether_c_f64_t v[3]; } aether_c_f64_vec3_t;\n#endif\n"
55
+
56
+ MAT_SHAPES = ((2, 2), (3, 3), (4, 4), (2, 3), (3, 2), (1, 3))
57
+ MM_PAIRS = (((2, 3), (3, 2)), ((3, 3), (3, 3)), ((2, 2), (2, 2)))
58
+ OUTER_PAIRS = (((2,), (3,)), ((3,), (3,)))
59
+
60
+
61
+ def _requests(width):
62
+ """Every (kind, kwargs) the broad tests exercise, derived from kinds()."""
63
+ for kind in C.kinds():
64
+ if kind == "vec_outer":
65
+ for s, s2 in OUTER_PAIRS:
66
+ yield kind, dict(shape=s, shape2=s2, width=width)
67
+ elif kind == "mat_mm":
68
+ for s, s2 in MM_PAIRS:
69
+ yield kind, dict(shape=s, shape2=s2, width=width)
70
+ elif kind.startswith("mat_"):
71
+ for s in MAT_SHAPES:
72
+ yield kind, dict(shape=s, width=width)
73
+ elif kind.startswith("quat_"):
74
+ yield kind, dict(width=width)
75
+ elif kind.startswith("vec_"):
76
+ for s in ((1,), (2,), (3,), (4,), (6,)):
77
+ if kind == "vec_cross" and s != (3,): continue
78
+ yield kind, dict(shape=s, width=width)
79
+ else:
80
+ yield kind, dict(width=width)
81
+
82
+ def test_texts_are_deterministic_c99_and_c_plus_plus_safe():
83
+ for width in C.WIDTHS:
84
+ for kind, kw in _requests(width):
85
+ a, b = C.function(kind, **kw), C.function(kind, **kw)
86
+ assert a == b and "//" not in a.text and "static inline" in a.text
87
+ assert a.deps[0] == f"aether_c_{width}_t"
88
+
89
+ def test_emit_orders_the_closure_typedefs_first_each_once():
90
+ names = [C.function("vec_norm", shape=(3,)).name, C.function("vec_dot", shape=(3,)).name]
91
+ text = C.emit(names)
92
+ assert text.index("typedef double aether_c_f64_t;") < text.index("aether_c_f64_vec3_t;") < text.index("aether_c_f64_vec3_norm(")
93
+ assert text.count("typedef struct { aether_c_f64_t v[3]; }") == 1
94
+
95
+ @pytest.mark.skipif(shutil.which("cc") is None, reason="no C compiler")
96
+ def test_emitted_closure_compiles_as_strict_c99(tmp_path):
97
+ names = [C.function(k, **kw).name for w in C.WIDTHS for k, kw in _requests(w)]
98
+ src = tmp_path / "t.c"
99
+ src.write_text("#include <math.h>\n" + C.emit(names))
100
+ subprocess.run(["cc", "-std=c99", "-pedantic", "-Wall", "-Wextra", "-Wconversion", "-Wsign-conversion",
101
+ "-Wdouble-promotion", "-Wno-unused-function", "-Werror", "-fsyntax-only", str(src)], check=True)
102
+
103
+ def test_mat2x2_sum_is_the_ones_matdot_fold():
104
+ t = C.function("mat_sum", shape=(2, 2)).text
105
+ assert "return ((m.m[1][1] * (aether_c_f64_t)1.0) + ((m.m[1][0] * (aether_c_f64_t)1.0) + ((m.m[0][1] * (aether_c_f64_t)1.0) + (m.m[0][0] * (aether_c_f64_t)1.0))));" in t
106
+
107
+ def test_mat2x2_mv_row_fold():
108
+ t = C.function("mat_mv", shape=(2, 2)).text
109
+ assert "out.v[0] = ((m.m[0][1] * v.v[1]) + (m.m[0][0] * v.v[0]));" in t
110
+
111
+ def test_mat1x3_mv():
112
+ assert "out.v[0] = ((m.m[0][2] * v.v[2]) + ((m.m[0][1] * v.v[1]) + (m.m[0][0] * v.v[0])));" in C.function("mat_mv", shape=(1, 3)).text
113
+
114
+ def test_mat_mm_names_both_shapes_and_folds_over_k():
115
+ f = C.function("mat_mm", shape=(2, 3), shape2=(3, 2))
116
+ assert f.name == "aether_c_f64_mat2x3_mm_mat3x2"
117
+ assert "out.m[0][0] = ((l.m[0][2] * r.m[2][0]) + ((l.m[0][1] * r.m[1][0]) + (l.m[0][0] * r.m[0][0])));" in f.text
118
+ assert set(f.deps) >= {"aether_c_f64_mat2x3_t", "aether_c_f64_mat3x2_t", "aether_c_f64_mat2x2_t"}
119
+ assert f.deps[0] == "aether_c_f64_t"
120
+
121
+ def test_outer_and_transpose():
122
+ assert "out.m[1][0] = (u.v[1] * v.v[0]);" in C.function("vec_outer", shape=(2,), shape2=(3,)).text
123
+ assert C.function("vec_outer", shape=(2,), shape2=(3,)).name == "aether_c_f64_vec2_outer_vec3"
124
+ assert "out.m[2][0] = m.m[0][2];" in C.function("mat_transpose", shape=(1, 3)).text
125
+
126
+ def test_mat_cwise_mirror_the_vector_templates():
127
+ assert "out.m[1][0] = (s * a.m[1][0]);" in C.function("mat_scale", shape=(2, 2)).text
128
+ t = C.function("mat_div_s", shape=(2, 2)).text
129
+ assert "const aether_c_f64_t r = ((aether_c_f64_t)1.0 / s);" in t and "out.m[1][1] = (r * a.m[1][1]);" in t
130
+ assert "out.m[0][1] = (m.m[0][1] * (aether_c_f64_t)-1.0);" in C.function("mat_neg", shape=(1, 3)).text
131
+ assert "out.m[0][0] = ((aether_c_f32_t)1.0 / sqrtf(m.m[0][0]));" in C.function("mat_rsqrt", shape=(1, 1), width="f32").text
132
+ assert "out.m[0][0] = fmin(a.m[0][0], b.m[0][0]);" in C.function("mat_min", shape=(1, 3)).text
133
+ assert "out.m[0][2] = fabsf(m.m[0][2]);" in C.function("mat_abs", shape=(1, 3), width="f32").text
134
+ assert "out.m[0][0] = (a.m[0][0] + b.m[0][0]);" in C.function("mat_add", shape=(2, 3)).text
135
+
136
+ def test_matrix_shape_errors():
137
+ with pytest.raises(ValueError, match=r"\(3,\)"):
138
+ C.function("mat_sum", shape=(3,))
139
+ with pytest.raises(ValueError, match=r"\(2, 3\).*\(2, 3\)"):
140
+ C.function("mat_mm", shape=(2, 3), shape2=(2, 3))
141
+ with pytest.raises(ValueError, match=r"\(2, 2\)"):
142
+ C.function("mat_mm", shape=(2, 2))
143
+ with pytest.raises(ValueError, match=r"\(2,\)"):
144
+ C.function("vec_outer", shape=(2,))
145
+
146
+ def test_kinds_order_matrix_between_vectors_and_scalars():
147
+ k = C.kinds()
148
+ assert k.index("vec_atan") < k.index("mat_add") < k.index("mat_atan") < k.index("mat_sum") < k.index("rsqrt")
149
+ assert k[-4:] == ("rsqrt", "sign", "clip", "remainder") and k[-11] == "vec_outer" and k[-10:-4] == tuple(x for x in k if x.startswith("quat_"))
150
+ assert len(set(k)) == len(k)
151
+
152
+ def test_unknown_kind_and_name():
153
+ with pytest.raises(ValueError, match="vec_dot"):
154
+ C.function("nope")
155
+ with pytest.raises(KeyError, match="no_such"):
156
+ C.emit(["no_such"])
157
+
158
+
159
+ def test_quat_mul_hamilton_left_to_right():
160
+ t = C.function("quat_mul").text
161
+ assert "out.v[0] = ((((l.v[0] * r.v[0]) - (l.v[1] * r.v[1])) - (l.v[2] * r.v[2])) - (l.v[3] * r.v[3]));" in t
162
+ assert "out.v[1] = ((((l.v[0] * r.v[1]) + (l.v[1] * r.v[0])) + (l.v[2] * r.v[3])) - (l.v[3] * r.v[2]));" in t
163
+ assert "out.v[2] = ((((l.v[0] * r.v[2]) - (l.v[1] * r.v[3])) + (l.v[2] * r.v[0])) + (l.v[3] * r.v[1]));" in t
164
+ assert "out.v[3] = ((((l.v[0] * r.v[3]) + (l.v[1] * r.v[2])) - (l.v[2] * r.v[1])) + (l.v[3] * r.v[0]));" in t
165
+
166
+ def test_quat_rotate_expands_as_aether_does():
167
+ t = C.function("quat_rotate").text
168
+ assert "const aether_c_f64_t tx = ((aether_c_f64_t)2.0 * ((q.v[2] * v.v[2]) - (q.v[3] * v.v[1])));" in t
169
+ assert "out.v[0] = ((v.v[0] + (q.v[0] * tx)) + ((q.v[2] * tz) - (q.v[3] * ty)));" in t
170
+
171
+ def test_quat_recip_scales_the_conjugate_by_one_reciprocal():
172
+ t = C.function("quat_recip").text
173
+ assert "const aether_c_f64_t r = ((aether_c_f64_t)1.0 / ((q.v[3] * q.v[3]) + ((q.v[2] * q.v[2]) + ((q.v[1] * q.v[1]) + (q.v[0] * q.v[0])))));" in t
174
+ assert "out.v[0] = (r * q.v[0]);" in t and "out.v[1] = (r * (q.v[1] * (aether_c_f64_t)-1.0));" in t
175
+
176
+ def test_quat_as_pure_and_as_vec3():
177
+ assert "out.v[0] = (aether_c_f64_t)0.0;" in C.function("quat_as_pure").text
178
+ assert "out.v[2] = q.v[3];" in C.function("quat_as_vec3").text
179
+
180
+ def test_quat_names_types_deps_and_shape_rejection():
181
+ f = C.function("quat_rotate", width="f32")
182
+ assert f.name == "aether_c_f32_quat_rotate"
183
+ assert f.text.startswith("/* aether-c: quat_rotate (4,) f32 aether-c/1 */\n")
184
+ assert "static inline aether_c_f32_vec3_t aether_c_f32_quat_rotate(aether_c_f32_vec4_t q, aether_c_f32_vec3_t v)" in f.text
185
+ assert f.deps == ("aether_c_f32_t", "aether_c_f32_vec4_t", "aether_c_f32_vec3_t")
186
+ assert C.function("quat_as_pure").deps == ("aether_c_f64_t", "aether_c_f64_vec3_t", "aether_c_f64_vec4_t")
187
+ assert "const aether_c_f32_t r = ((aether_c_f32_t)1.0 /" in C.function("quat_recip", width="f32").text
188
+ with pytest.raises(ValueError):
189
+ C.function("quat_mul", shape=(4,))
190
+ with pytest.raises(ValueError):
191
+ C.function("quat_mul", shape2=(4,))
192
+
193
+
194
+ _LIBM_CALL = re.compile(r"\b(sqrt|fmin|fmax|pow|fabs|exp|log|sin|cos|tan|tanh|asin|acos|atan|fmod|copysign)f?\(")
195
+
196
+ def test_every_registered_name_audits_deps_libm_and_width_purity():
197
+ for width in C.WIDTHS:
198
+ for kind, kw in _requests(width):
199
+ f = C.function(kind, **kw)
200
+ for dep in f.deps:
201
+ C.emit([dep]) # KeyError if a dep is not registered
202
+ calls = set(re.findall(r"\b((?:sqrt|fmin|fmax|pow|fabs|exp|log|sin|cos|tan|tanh|asin|acos|atan|fmod|copysign)f?)\(", f.text))
203
+ assert set(f.libm) == calls and len(f.libm) == len(set(f.libm)), (f.name, f.libm, calls)
204
+ if width == "f32":
205
+ assert all(c.endswith("f") for c in calls), f.name
206
+ assert not re.search(r"\bdouble\b", f.text), f.name
207
+ else:
208
+ assert not re.search(r"\bfloat\b", f.text), f.name
209
+
210
+ def test_invalid_shapes_raise_and_register_nothing():
211
+ before = dict(C._REGISTRY)
212
+ for kind, kw in (("vec_add", dict(shape=(0,))), ("vec_add", dict(shape=(-2,))),
213
+ ("vec_dot", dict(shape=(0,))), ("mat_mv", dict(shape=(0, 3))),
214
+ ("vec_add", dict(shape=(True,))), ("vec_add", dict(shape=(2.0,))),
215
+ ("rsqrt", dict(shape=(3,))), ("clip", dict(shape2=(3,))),
216
+ ("vec_add", dict(shape=(3,), shape2=(3,)))):
217
+ with pytest.raises(ValueError):
218
+ C.function(kind, **kw)
219
+ with pytest.raises(ValueError):
220
+ C.struct((0,), "f32")
221
+ assert C._REGISTRY == before
File without changes
File without changes
File without changes