loopty 0.1.0__tar.gz → 0.2.0__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.
- {loopty-0.1.0 → loopty-0.2.0}/CHANGELOG.md +185 -0
- {loopty-0.1.0 → loopty-0.2.0}/PKG-INFO +170 -87
- {loopty-0.1.0 → loopty-0.2.0}/README.md +168 -85
- {loopty-0.1.0 → loopty-0.2.0}/docs/loopy-notes.md +149 -35
- {loopty-0.1.0 → loopty-0.2.0}/docs/quickstart.md +2 -1
- {loopty-0.1.0 → loopty-0.2.0}/examples/README.md +11 -11
- {loopty-0.1.0 → loopty-0.2.0}/pyproject.toml +5 -2
- {loopty-0.1.0 → loopty-0.2.0}/src/loopty/__init__.py +1 -1
- {loopty-0.1.0 → loopty-0.2.0}/src/loopty/compose.py +7 -43
- {loopty-0.1.0 → loopty-0.2.0}/src/loopty/domain.py +14 -2
- {loopty-0.1.0 → loopty-0.2.0}/src/loopty/executor.py +69 -13
- {loopty-0.1.0 → loopty-0.2.0}/src/loopty/flow.py +19 -7
- {loopty-0.1.0 → loopty-0.2.0}/src/loopty/idx.py +20 -4
- {loopty-0.1.0 → loopty-0.2.0}/src/loopty/interpret.py +50 -22
- {loopty-0.1.0 → loopty-0.2.0}/src/loopty/lower.py +334 -39
- loopty-0.2.0/src/loopty/names.py +235 -0
- {loopty-0.1.0 → loopty-0.2.0}/src/loopty/operations.py +41 -2
- {loopty-0.1.0 → loopty-0.2.0}/src/loopty/promotion.py +58 -18
- loopty-0.2.0/src/loopty/refusal.py +756 -0
- {loopty-0.1.0 → loopty-0.2.0}/src/loopty/term.py +61 -0
- {loopty-0.1.0 → loopty-0.2.0}/src/loopty/trace.py +332 -41
- {loopty-0.1.0 → loopty-0.2.0}/tests/test_arithmetic.py +1060 -4
- {loopty-0.1.0 → loopty-0.2.0}/tests/test_examples.py +9 -3
- {loopty-0.1.0 → loopty-0.2.0}/tests/test_isl_reading.py +302 -0
- {loopty-0.1.0 → loopty-0.2.0}/tests/test_lower.py +94 -0
- loopty-0.2.0/tests/test_names.py +301 -0
- {loopty-0.1.0 → loopty-0.2.0}/tests/test_program.py +3 -2
- {loopty-0.1.0 → loopty-0.2.0}/tests/test_smoke.py +1 -1
- {loopty-0.1.0 → loopty-0.2.0}/.gitignore +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/LICENSE +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/docs/device-runs/device_demos.py +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/docs/device-runs/h200-cuda-commands.log +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/docs/device-runs/h200-cuda-device-demos.log +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/docs/device-runs/h200-cuda-reduction-probe.log +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/docs/device-runs/pocl-epyc-cpu-commands.log +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/docs/device-runs/pocl-epyc-cpu-device-demos.log +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/docs/device-runs/pocl-epyc-cpu-reduction-probe.log +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/docs/device-runs/reduction_probe.py +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/docs/device-runs.md +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/examples/composition.py +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/examples/fusion.py +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/examples/p2p.py +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/examples/pairs.py +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/examples/reshape_layouts.py +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/examples/spmv.py +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/examples/stencil_skew.py +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/examples/travel.py +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/examples/wavefront_acoustic.py +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/scripts/refresh_example_outputs.py +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/src/loopty/arr.py +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/src/loopty/cli.py +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/src/loopty/contract.py +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/src/loopty/faithful.py +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/src/loopty/hypotheses.py +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/src/loopty/isl_reading.py +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/src/loopty/kernel.py +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/src/loopty/oracle.py +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/src/loopty/plugin.py +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/src/loopty/py.typed +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/src/loopty/reduction.py +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/src/loopty/schedule.py +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/src/loopty/tolerance.py +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/src/loopty/typing.py +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/tests/conftest.py +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/tests/hand_terms.py +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/tests/kernels/axpy.py +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/tests/kernels/domains.py +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/tests/kernels/out_of_bounds.py +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/tests/kernels/spmv_min.py +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/tests/kernels/stencil.py +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/tests/kernels/travel.py +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/tests/kernels/travel_wrong.py +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/tests/test_adversarial.py +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/tests/test_arr.py +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/tests/test_buildable.py +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/tests/test_cli.py +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/tests/test_contract_bounds.py +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/tests/test_contraction.py +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/tests/test_declared_layout.py +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/tests/test_domains.py +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/tests/test_executor.py +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/tests/test_executor_c.py +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/tests/test_faithful.py +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/tests/test_flow.py +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/tests/test_fusion.py +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/tests/test_idx.py +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/tests/test_interpret.py +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/tests/test_kernel.py +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/tests/test_lower_traced.py +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/tests/test_oracle.py +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/tests/test_refresh_script.py +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/tests/test_schedule.py +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/tests/test_storage.py +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/tests/test_term.py +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/tests/test_trace.py +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/tests/test_travel.py +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/tests/test_typing.py +0 -0
- {loopty-0.1.0 → loopty-0.2.0}/tests/test_work_items.py +0 -0
|
@@ -6,6 +6,191 @@ All notable changes to loopty are recorded here. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.2.0] - 2026-10-11
|
|
10
|
+
|
|
11
|
+
The second release, on lanky 0.2.0, which checks each theorem's reading of
|
|
12
|
+
its annotations and gives it a row of its own in the ledger, and whose `abs`
|
|
13
|
+
of a term compares (lanky#94); loopty requires it, so that the ledgers its
|
|
14
|
+
documentation shows are the ones an install prints. The rest is two waves of
|
|
15
|
+
fixes where the compiled run computed otherwise than numpy, or did not build:
|
|
16
|
+
subscripts, storage and names (#164: an index sum in a dividing subscript is
|
|
17
|
+
64 bits wide, a narrow subscript stays in its type, complex `==` builds on
|
|
18
|
+
OpenCL, a program's arrays are off the C stack, and every isl dimension
|
|
19
|
+
loopty names has a name of its own), and arithmetic and messages (#169: a
|
|
20
|
+
sum is computed as the body wrote it, `abs` of a real lowers in a guard, the
|
|
21
|
+
compiled run is refused before it starts where the native run is refused,
|
|
22
|
+
and loopy's refusal of a guard it cannot read names the fix). CI also tests
|
|
23
|
+
loopty against the released lanky (#160). The README's Known limits section
|
|
24
|
+
lists what is still open.
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- **The names of isl dimensions** (`loopty.names`, #161). Every isl object
|
|
29
|
+
loopty names the dimensions of keeps namedisl's invariants: each
|
|
30
|
+
dimension has a name, no two share one, and none is named like a
|
|
31
|
+
parameter. isl's reader breaks them silently, reading a tuple variable
|
|
32
|
+
spelled like a parameter as that parameter (#110). `check_names` checks
|
|
33
|
+
them where such an object is made, a statement's and a sum's domain when
|
|
34
|
+
the term records it, the loops of a nest, an access relation (its cells
|
|
35
|
+
are positional), the index tuples of a shape, a dense layout's map and a
|
|
36
|
+
polyhedral domain's points, and raises `NamingError`, a bug of loopty's,
|
|
37
|
+
naming the object and the clash. `rename` is the one way loopty renames
|
|
38
|
+
dimensions and replaces parameters, all at once: composing a program
|
|
39
|
+
(`compose.rename_set`), the binders of a nested sum in the lowering, and
|
|
40
|
+
the native run fixing a parameter at the value it reads use it.
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
|
|
44
|
+
- **Requires lanky 0.2.0** (`lanky>=0.2.0`, was `>=0.1.0`). Its ledger has
|
|
45
|
+
a reading row under each theorem (the next entry), which the `lanky check`
|
|
46
|
+
transcripts in the README, the quickstart and the examples' README show,
|
|
47
|
+
and its `abs` of a term is lanky's `Abs`, so a comparison of it traces,
|
|
48
|
+
`when(abs(k[i]) > 2)` and `127 == abs(c[i])` (lanky#94); the test of that
|
|
49
|
+
(#165) no longer skips on an older lanky. `[tool.uv.sources]` still
|
|
50
|
+
resolves lanky from `../lanky` in a checkout, and the built distributions
|
|
51
|
+
ask for it from PyPI.
|
|
52
|
+
- **A theorem in a kernel file has its reading under it** (lanky #91, #155).
|
|
53
|
+
lanky now checks each claim's reading of its annotations: the annotations
|
|
54
|
+
are run again as plain Python at drawn values, the term is evaluated at the
|
|
55
|
+
same values, and the two have to agree. Each theorem gets a second fact,
|
|
56
|
+
`tested python ... the term computes what the annotations compute`, on
|
|
57
|
+
which its pass or proof rests. The `lanky check` transcripts of
|
|
58
|
+
`examples/spmv.py` and `examples/travel.py`, each of which has a
|
|
59
|
+
`scan_monotone`, show the new row, and are regenerated with
|
|
60
|
+
`scripts/refresh_example_outputs.py`; `examples/README.md` says what the
|
|
61
|
+
row is. A kernel's and a program's facts are loopty's, and have no such
|
|
62
|
+
row: their trace has its own check, `trace-faithful`. (This entry was
|
|
63
|
+
filed under 0.1.0 by mistake; it was merged after that release.)
|
|
64
|
+
- CI also tests loopty as `pip install loopty` gets it: a second job installs
|
|
65
|
+
it without `[tool.uv.sources]`, so with lanky from PyPI, and runs the suite
|
|
66
|
+
and the worked example's `lanky check` and `loopty run` (#147).
|
|
67
|
+
- loopy's refusal of an access under a guard its bounds check does not read,
|
|
68
|
+
a product of loop variables, a guard computed in floating point or an
|
|
69
|
+
array's entry, names the statement, where the body wrote it, the guard,
|
|
70
|
+
and a guard over integers that keeps the access in bounds, which loopy
|
|
71
|
+
reads, with a size as the body reads it: `with when(i <= y.dom.size - 5):`
|
|
72
|
+
for `x[i + 4]` under `when(i * 0.5 < 2)` (#148). It is
|
|
73
|
+
`loopty.refusal.GuardNotRead`, a `LoopyIndexError` still, with loopy's
|
|
74
|
+
message at its end. A guard that calls a function no statement's value
|
|
75
|
+
calls, which loopy never resolves, is refused with a `LoweringError`
|
|
76
|
+
naming the fix, where loopy failed with `KeyError` and the function's
|
|
77
|
+
name.
|
|
78
|
+
- An array a program makes is off the C stack (#62). On the C target loopy
|
|
79
|
+
declared it a variable-length array on the stack of the call, `double
|
|
80
|
+
f[n];`, so the thread's stack bounded how large an intermediate could be,
|
|
81
|
+
and a program whose intermediate was larger, eight megabytes on a thread
|
|
82
|
+
with four, died with no diagnostic. `LoopyExecutor` now allocates it on
|
|
83
|
+
the heap, zeroed, and passes it after the program's own arguments,
|
|
84
|
+
`double *__restrict__ f`, and `emit_code` and `loopty run --emit-code`
|
|
85
|
+
print it so (`lower.heap_temporaries`). The term and its schedules keep it
|
|
86
|
+
a temporary, which `Schedule.substitute` computes away; an empty one stays
|
|
87
|
+
where it takes no room, since loopy's C invoker cannot pass an empty
|
|
88
|
+
array. The OpenCL target's global temporary is unchanged.
|
|
89
|
+
|
|
90
|
+
### Fixed
|
|
91
|
+
|
|
92
|
+
- A sum is computed as the body wrote it (#151). pymbolic builds `a - b` as
|
|
93
|
+
`a + -1 * b` and `u - 1` as `u + -1`, the same nodes `a + -b` and `u + -1`
|
|
94
|
+
build, and the trace, the plan and the interpreter read every such sum as
|
|
95
|
+
the difference, which numpy computes otherwise: `c[i] + -a[i]` of an `int8`
|
|
96
|
+
`a` is `c[i] + -128` at `a[i] = -128` natively and was `c[i] + 128`
|
|
97
|
+
compiled, and `c[i] + -u[i]` of a `uint8` `u` adds `256 - u[i]` natively
|
|
98
|
+
and subtracted `u[i]`, at every `u[i]` but zero. The trace builds every `-`
|
|
99
|
+
the body writes as a `loopty.term.Difference`, a sum whose last term is
|
|
100
|
+
subtracted, and any other sum is read as written. So `u[i] + -1` of an
|
|
101
|
+
unsigned `u`, `k[i] - 2**31` and `k[i] + 2**31` of an `int32` `k`, `u[i] -
|
|
102
|
+
-7`, `c[i] + -b[i]` and `b[i] - True` of a truth value `b`, which numpy
|
|
103
|
+
refuses at every point and the compiled run computed as the other
|
|
104
|
+
spelling, are `TraceError`s naming the fix, and `k[i] + -(2**31)` and
|
|
105
|
+
`c[i] - -32768` of an `int16` `c` are computed in the type numpy computes
|
|
106
|
+
them in. A numpy scalar subtracted is kept as written, where pymbolic negates it
|
|
107
|
+
in its own type: `k[i] - np.uint8(3)` was `k[i] + 253` compiled. The
|
|
108
|
+
smallest `int32` and `int64` are written as C writes `INT_MIN`,
|
|
109
|
+
`(-2147483647 - 1)`: `-2147483648` is the negation of a `long` in C, and
|
|
110
|
+
`k[i] + -2147483648` of an `int32` `k` was computed in 64 bits, never
|
|
111
|
+
wrapping round as numpy does; `-9223372036854775808` is the negation of a
|
|
112
|
+
constant no signed type holds, which GCC reads in 128 bits with a warning
|
|
113
|
+
and clang as an `unsigned long`. That is on the C target, built with
|
|
114
|
+
`-fwrapv`; OpenCL C has none, and its compilers fold `k[i] + (-2147483647 -
|
|
115
|
+
1)` into `k[i] | (-2147483647 - 1)`, so there the literal is left as loopy
|
|
116
|
+
writes it. A real less an integer keeps the sign of zero: `x[i] - 0` and
|
|
117
|
+
`x[i] - i`, `-0.0` natively at `x[i] = -0.0` and `i = 0`, were `0.0`
|
|
118
|
+
compiled, since pymbolic builds them as `x[i] + 0` and `x[i] + -1 * i`; the
|
|
119
|
+
zero is written `-0.0`, and `i` is converted into the real's type before it
|
|
120
|
+
is negated. A truth value subtracted, `k[i] - True`, which pymbolic takes as
|
|
121
|
+
no operand, is a `TraceError` naming the integer, `k[i] - 1`. `k[i] - 2**63`
|
|
122
|
+
and `b[i] - 2**63` of a truth value `b`, which numpy refuses in `int64`, are
|
|
123
|
+
refused by the trace, naming a real, `2.0 ** 63`.
|
|
124
|
+
- `abs` of a real in a guard lowers (#154): loopy's type inference visits a
|
|
125
|
+
statement's value and never its guard, so `abs`, which only the guard
|
|
126
|
+
called, was never resolved, and loopy failed with `KeyError: 'abs'`. In a
|
|
127
|
+
guard it is written `x < 0 ? -x : (x == 0 ? 0 : x)`, which is numpy's
|
|
128
|
+
`abs(-0.0)` too, `0.0`.
|
|
129
|
+
- Loop arithmetic beside a numpy integer whose type does not hold it at a
|
|
130
|
+
point of the loops, `u[i] + (i - 3)` of a `uint32` `u` at `i = 0`, `a[i] *
|
|
131
|
+
(i * 100)` of an `int8` `a` from `i = 2`, and a loop variable stored into an
|
|
132
|
+
`int8` array from `i = 128`, is refused by numpy natively there, and the
|
|
133
|
+
compiled run computed it in C's type (#153). Where the call's sizes reach
|
|
134
|
+
such a point, isl finds it, and the compiled run is refused before it
|
|
135
|
+
starts (`loopty.refusal.RefusedNatively`, an `OverflowError`), naming the
|
|
136
|
+
point and `(i - 3) * np.int64(1)` as the fix. Arithmetic isl cannot read,
|
|
137
|
+
or a ragged row's loop, is left to the native refusal (#166). A sum's body
|
|
138
|
+
is asked about where the generator's `if` clause holds, which skips it
|
|
139
|
+
natively: `reduce_sum(a[j] + j for j in a.dom if j < 100)` of an `int8`
|
|
140
|
+
`a` is computed at any size. A program's call after a checked point is
|
|
141
|
+
refused once the run has shown that the points before it passed, whatever
|
|
142
|
+
a point after it says; where one before it failed, that point's error is
|
|
143
|
+
raised, as natively. A literal stored into an integer array that does not
|
|
144
|
+
hold it, `y[i] = 300` of a `uint8` `y`, is a `TraceError`, as #141's
|
|
145
|
+
literals are; under a guard, which masks the store, it is refused where the
|
|
146
|
+
call's sizes reach a point of the guard, and `with when(i < 0): y[i] = 300`
|
|
147
|
+
stores nothing in either run.
|
|
148
|
+
- A regression test for #152, which #156 fixed: a literal beside a loop
|
|
149
|
+
variable under a double power in a single-precision statement is written as
|
|
150
|
+
an integer, `(double) (i + 16777216)`, where loopy wrote `1.0f` and C
|
|
151
|
+
rounded the loop variable past `2**24`.
|
|
152
|
+
- A sum of loop variables and sizes in a dividing subscript is computed in
|
|
153
|
+
64 bits (#149). The plan leaves such a sum in 32 bits, as loopy computes a
|
|
154
|
+
loop bound, and a remainder or a floor division in a subscript used the
|
|
155
|
+
wrapped value once the sum passed `2**31`: `x[(i + k) % m]` at `k = 2**31 -
|
|
156
|
+
1` read cell `(-2**31) % 3` at `i = 1`, where numpy reads `2**31 % 3`, and
|
|
157
|
+
loopy wrote `x[(i + n) // 2]`, which it reads as affine with a division,
|
|
158
|
+
as `x[(i + n) / 2]`. In a subscript loopy does not read as affine with no
|
|
159
|
+
division the sum's first operand is widened now,
|
|
160
|
+
`x[loopty_mod_int64((int64_t) (i) + k, (int64_t) (m))]`, and `abs` of one
|
|
161
|
+
takes its sign from bit 63. The same sum in a value or a guard on the
|
|
162
|
+
loops is still 32 bits wide (#162).
|
|
163
|
+
- A subscript over a narrow numpy integer type is computed in it, as numpy
|
|
164
|
+
computes it (#157). The conversion back into `int8`, `uint8`, `int16` or
|
|
165
|
+
`uint16` that an operation in such a type gets elsewhere (#122) was left
|
|
166
|
+
out of a subscript, so `x[s + i]` of a `uint8` `s`, which loopy reads as
|
|
167
|
+
affine, read cell 256 where numpy takes `250 + 6` round to 0, and
|
|
168
|
+
`x[abs(s) % 7]` of an `int8` `-128` read cell 2, where numpy's `abs` keeps
|
|
169
|
+
`-128`, whose remainder is 5. It is kept now, `x[(uint8_t) (s + i)]`, and
|
|
170
|
+
loopy's reader declines the cast, so the subscript is written as it is.
|
|
171
|
+
- `==` and `!=` of complex values build on the OpenCL target (#158).
|
|
172
|
+
pyopencl's complex types are structs, which OpenCL C does not compare, and
|
|
173
|
+
loopy wrote `z[i] == w[i]` as it is, so the kernel did not build. They are
|
|
174
|
+
compared part by part, as numpy compares, in the complex type of the two,
|
|
175
|
+
a real operand with a zero imaginary part: `cdouble_real(z[i]) ==
|
|
176
|
+
cdouble_real(w[i]) && cdouble_imag(z[i]) == cdouble_imag(w[i])`. The C
|
|
177
|
+
targets compare C99's complex types with C's operators, as before.
|
|
178
|
+
- The index tuples of a shape whose size is named `i0`, `idx.to_set((Fin[i0],
|
|
179
|
+
...))`, had no points: their dimensions were named `i0, i1, ...` whatever
|
|
180
|
+
the sizes are called, and isl read the dimension `i0` as the size, `0 <= i0
|
|
181
|
+
< i0`. They are named clear of the sizes now, as a polyhedral domain's
|
|
182
|
+
already were (#161).
|
|
183
|
+
- A sum by a binder named like a size or an integer parameter of the kernel,
|
|
184
|
+
`reduce_sum(x[j] for j in x.dom)` over `x: Arr[Fin[j], Real]`, is refused
|
|
185
|
+
at the trace, naming the fix (#161). Its domain was `{ [j] : 0 <= j < j }`,
|
|
186
|
+
a set with a well-named dimension and no points, which the name check
|
|
187
|
+
could not see: the compiled sum was `0` where numpy's was not, the
|
|
188
|
+
in-bounds fact of `x[j + 5]` was decided over the empty domain, and a sum
|
|
189
|
+
by `i` inside a loop bounded by a size `i` did not build. A loop variable
|
|
190
|
+
so named was already given a name of its own. `flow.domain_set` and a
|
|
191
|
+
polyhedral domain's points now also refuse a dimension named like a size
|
|
192
|
+
their bounds meant as a parameter (`check_names(..., meant=...)`).
|
|
193
|
+
|
|
9
194
|
## [0.1.0] - 2026-10-09
|
|
10
195
|
|
|
11
196
|
The first release on PyPI, where only the 0.0.1 placeholder that reserved the
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: loopty
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: loopy, with types. A typed polyhedral layer over loopy.
|
|
5
5
|
Project-URL: Homepage, https://github.com/xywei/loopty
|
|
6
6
|
Project-URL: Repository, https://github.com/xywei/loopty
|
|
@@ -25,7 +25,7 @@ Classifier: Topic :: Software Development :: Compilers
|
|
|
25
25
|
Classifier: Typing :: Typed
|
|
26
26
|
Requires-Python: >=3.12
|
|
27
27
|
Requires-Dist: islpy<2026,>=2025.2.5
|
|
28
|
-
Requires-Dist: lanky>=0.
|
|
28
|
+
Requires-Dist: lanky>=0.2.0
|
|
29
29
|
Requires-Dist: loopy<2025.3,>=2025.2
|
|
30
30
|
Requires-Dist: numpy>=2
|
|
31
31
|
Requires-Dist: pymbolic>=2025.1
|
|
@@ -90,7 +90,7 @@ decided decided type spmv.py:112 spmv the accumulation
|
|
|
90
90
|
tested tested interpreter spmv.py:102 spmv the traced term computes what the body computes
|
|
91
91
|
decided tested call spmv.py:115 solve after scan(...) in solve: off[0] == 0 and (forall r in Fin(n). off[r ...
|
|
92
92
|
...
|
|
93
|
-
|
|
93
|
+
22 facts: 16 decided, 6 tested
|
|
94
94
|
```
|
|
95
95
|
|
|
96
96
|
Look at the `x[col[r, j]]` row, and at what decided it. That indirection is the
|
|
@@ -254,17 +254,19 @@ table of row starts. Both compiled runs agree with the native one.
|
|
|
254
254
|
|
|
255
255
|
## Status
|
|
256
256
|
|
|
257
|
-
This is 0.
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
ledger
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
the
|
|
266
|
-
|
|
267
|
-
|
|
257
|
+
This is 0.2.0. The core works: kernels and programs run natively, trace,
|
|
258
|
+
state their facts, lower and run compiled on the C target, and every schedule
|
|
259
|
+
step is checked; the edges are sharp. Below is what works, what works in part
|
|
260
|
+
and what does not work yet. After it, Known limits lists the open issues you
|
|
261
|
+
are most likely to meet, each with what to do about it. None of them makes the
|
|
262
|
+
ledger call a false fact `decided`, but in several the compiled run computes
|
|
263
|
+
something other than the native run, and the ledger catches that only on the
|
|
264
|
+
inputs it tries: read that section before you rely on a compiled run. CI runs
|
|
265
|
+
the suite on every change, on Python 3.12 and 3.13 with lanky's `main` and on
|
|
266
|
+
3.13 with the released lanky, and reruns every command whose output this
|
|
267
|
+
README, the quickstart and the examples' README show, holding them to what it
|
|
268
|
+
prints. It runs no OpenCL device. Until 1.0 an interface can change in a minor
|
|
269
|
+
release; the CHANGELOG says when one does.
|
|
268
270
|
|
|
269
271
|
**Works.**
|
|
270
272
|
|
|
@@ -350,7 +352,11 @@ says when one does.
|
|
|
350
352
|
loop variables, a float literal or a `Real` scalar (`when(i * 0.5 < 2)`,
|
|
351
353
|
`when(i * 2.0 < n)`), it checks an access at every point of the loop, and
|
|
352
354
|
refuses one it cannot show in bounds there, one the guard keeps in bounds
|
|
353
|
-
included
|
|
355
|
+
included. The refusal (`loopty.refusal.GuardNotRead`, a `LoopyIndexError`)
|
|
356
|
+
names the statement, the guard, and a guard over integers that keeps the
|
|
357
|
+
access in bounds, as the body writes it, `when(i <= y.dom.size - 5)` for
|
|
358
|
+
`x[i + 4]` (#148; note 23 in
|
|
359
|
+
`docs/loopy-notes.md`). A guard has to be a truth value, and one whose
|
|
354
360
|
value is an integer is a `TraceError`, on a native run and under tracing
|
|
355
361
|
alike. Natively
|
|
356
362
|
that is what `~(i > 0)` is (`~` on a Python bool is bitwise), while the trace
|
|
@@ -486,10 +492,25 @@ says when one does.
|
|
|
486
492
|
back into it, and an unsigned integer beside a signed one is computed in
|
|
487
493
|
the signed type numpy computes in (`int64` for a `uint32` and an `int32`),
|
|
488
494
|
where C's would be unsigned; integers compare exactly, as numpy's do, a
|
|
489
|
-
negative one with an unsigned one included.
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
495
|
+
negative one with an unsigned one included. A sum is computed as the body
|
|
496
|
+
wrote it: `c[i] - a[i]` as the difference, and `c[i] + -a[i]` as the sum of
|
|
497
|
+
`a[i]` negated in its own type, which differ for an `int8` `a` at `-128` and
|
|
498
|
+
an unsigned one at every value but zero; `u[i] + -1` of an unsigned `u` and
|
|
499
|
+
`k[i] - 2**31` of an `int32` `k`, which numpy refuses at every point, are
|
|
500
|
+
refused by the trace (#151). A real less an integer keeps the sign of zero,
|
|
501
|
+
`x[i] - 0` and `x[i] - i` at `x[i] = -0.0`, and a truth value subtracted,
|
|
502
|
+
`k[i] - True`, which pymbolic takes no operand of, is refused naming the
|
|
503
|
+
integer. Loop arithmetic beside a numpy integer whose
|
|
504
|
+
type does not hold it at some point, `u[i] + (i - 3)` of a `uint32` `u` at
|
|
505
|
+
`i = 0`, is refused by numpy natively there, and the compiled run is refused
|
|
506
|
+
before it starts where the call's sizes reach such a point, naming it
|
|
507
|
+
(`loopty.refusal.RefusedNatively`, #153). `abs` of an integer is numpy's,
|
|
508
|
+
the smallest value of its type included, and `abs` of a real in a guard is
|
|
509
|
+
written without a call, since loopy resolves no function only a guard calls
|
|
510
|
+
(#154); another function only a guard calls is refused, naming the fix.
|
|
511
|
+
`abs` of a term compares on either side, `when(abs(k[i]) > 2)` and `127 ==
|
|
512
|
+
abs(c[i])`, since lanky 0.2.0 (xywei/lanky#94). An integer literal past 64
|
|
513
|
+
bits is written as a real where numpy computes in one (`x[i] * 2**70`), or as a
|
|
493
514
|
`uint64` beside one (`u[i] + 2**63`), and refused elsewhere. A kernel named
|
|
494
515
|
like a function, a macro or a type loopy, the C headers or OpenCL C know
|
|
495
516
|
(`floor`, `pow`, `cpow`, `get_global_id`, `clamp`, `NULL`, `size_t`) is
|
|
@@ -636,11 +657,12 @@ says when one does.
|
|
|
636
657
|
what the compiled temporary holds: `float64` for `Real`, the dtype itself
|
|
637
658
|
for a numpy one such as `np.complex128`, `bool` for `Bool`, `int64` for
|
|
638
659
|
`Nat` and `Int`, and a signed integer of 32 bits or more for `Fin[m]`
|
|
639
|
-
(`int64` for an array a checked point reads). On the C target
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
660
|
+
(`int64` for an array a checked point reads). On the C target the executor
|
|
661
|
+
allocates a program's array on the heap and passes it after the program's
|
|
662
|
+
own arguments, `double *__restrict__ f`, where loopy would declare it on
|
|
663
|
+
the stack of the call, whose size bounded it (#62; note 16 in
|
|
664
|
+
`docs/loopy-notes.md`); on OpenCL it is a global temporary, which the
|
|
665
|
+
PyOpenCL host code allocates.
|
|
644
666
|
- Only a two-axis (row, fiber) ragged array, its rows counted by an array of
|
|
645
667
|
one axis, is built and lowered; tracing refuses any other ragged type, a
|
|
646
668
|
fiber after two dense axes, a dense axis after the fiber, or counts of two
|
|
@@ -651,28 +673,43 @@ says when one does.
|
|
|
651
673
|
which is exact: `1.0 * i ** 5` past `i = 6208` is a wider number natively
|
|
652
674
|
and wraps compiled, which the differential fact reports (#139).
|
|
653
675
|
A loop bound, and a subscript loopy reads as affine with no division, are
|
|
654
|
-
loopy's index arithmetic, 32 bits wide
|
|
655
|
-
sizes and small literals elsewhere; under `-fwrapv` such a subscript's
|
|
676
|
+
loopy's index arithmetic, 32 bits wide; under `-fwrapv` such a subscript's
|
|
656
677
|
sums and products give the cell it names. Any other subscript is computed
|
|
657
|
-
in 64 bits, as the rest is
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
a
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
678
|
+
in 64 bits, as the rest is, its sums of loop variables and sizes included:
|
|
679
|
+
`x[(i * i) % n]` (#129), `x[(i * 499999) // 1000000]`, which loopy wrote
|
|
680
|
+
with a division of the wrapped `499999 * i`, and `x[(i + n) // 2]`, whose
|
|
681
|
+
sum wrapped round past `2**30` (#149). A sum of loop variables, sizes and
|
|
682
|
+
small literals in a value or a guard on the loops is still 32 bits wide,
|
|
683
|
+
`y[i] = i + n` past `2**31` (#162). A subscript over a narrow numpy integer
|
|
684
|
+
type is computed in it, as numpy computes it: `x[s + i]` of a `uint8` `s`
|
|
685
|
+
wraps round at 256 in both runs (#157). One of a signed type that wraps
|
|
686
|
+
round below zero is refused natively, as any negative index is, and read
|
|
687
|
+
before the array compiled; its in-bounds fact is not decided, since isl
|
|
688
|
+
reads a narrow scalar or element as any integer, and is refuted or
|
|
689
|
+
assumed. A `Fin[m]` array the kernel writes may be an `int32` one
|
|
690
|
+
natively, whose entries the native run reads as `int64`, as the compiled
|
|
691
|
+
run computes with them, unless a program's checked point reads it, which
|
|
692
|
+
takes an `int64` one. An integer to a negative power whose exponent is not
|
|
693
|
+
a literal is left to numpy's refusal natively, and computed as an integer
|
|
694
|
+
compiled. Loop arithmetic beside a numpy integer that does not hold it is
|
|
695
|
+
refused compiled where isl reads it and the call's sizes fix its loops
|
|
696
|
+
(#153); a product of loop variables, a ragged row's loop, a store under a
|
|
697
|
+
guard isl does not read, and a sum's body under both a guard and the
|
|
698
|
+
generator's `if` clause where the guard is false, are left to the native
|
|
699
|
+
refusal and computed compiled (#166). So is a value stored into a signed
|
|
700
|
+
integer array that does not hold it, a `uint64` past `2**63` into `Int`,
|
|
701
|
+
an `Int` into an `np.int8` array, or a real past `2**63`, or a NaN, into
|
|
702
|
+
`Int`, whose value only the compiled kernel knows (#159). The written `-1 *
|
|
703
|
+
u[i]` of an unsigned `u`, which numpy refuses, is read as `-u[i]`, which
|
|
704
|
+
it computes (#168). A numpy scalar on the left of a term is traced as the
|
|
705
|
+
Python number it equals: `np.float64(0.1) * x[i]` as `0.1 * x[i]`, single
|
|
706
|
+
precision beside a `float32`, and `x[np.uint8(200) + i]` as `x[200 + i]`,
|
|
707
|
+
not taken round (#167). Two statements of one kernel whose lowered
|
|
708
|
+
arithmetic differs only by the dtype of an equal literal are generated
|
|
709
|
+
with the first one's literal, since loopy maps a subexpression to the
|
|
710
|
+
result of an equal one, and pymbolic finds a numpy scalar equal to any
|
|
711
|
+
number of its value (#170). See notes 19, 20, 23 and 25 in
|
|
712
|
+
`docs/loopy-notes.md`.
|
|
676
713
|
- A polyhedral domain is an array's whole index set, so it cannot sit beside
|
|
677
714
|
a dense axis (`Arr[Fin[k], Where[...], Real]` is refused; write the axis as
|
|
678
715
|
a binder of the domain). The pieces of a union have the same number of axes,
|
|
@@ -819,51 +856,97 @@ says when one does.
|
|
|
819
856
|
|
|
820
857
|
## Known limits
|
|
821
858
|
|
|
822
|
-
The open issues
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
`
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
`
|
|
859
|
+
The open issues you are most likely to meet, and what to do about each. Most
|
|
860
|
+
show in `lanky check` or `loopty run` as a `refuted` or `assumed` fact or as
|
|
861
|
+
an error, but `loopty run` compares the compiled run with the native one only
|
|
862
|
+
on the inputs it runs.
|
|
863
|
+
|
|
864
|
+
- **A compiled run does not check indices**
|
|
865
|
+
([#171](https://github.com/xywei/loopty/issues/171)). `loopty run` and
|
|
866
|
+
`LoopyExecutor.run` compile a kernel whatever its in-bounds facts say. Where
|
|
867
|
+
an index leaves its array, the native run raises `IndexError` and the
|
|
868
|
+
compiled run reads or writes outside the buffer, with no error. `lanky
|
|
869
|
+
check` shows where this can happen: the access's row, `x[i + 4] is in bounds
|
|
870
|
+
for every instance of S0`, reads `refuted` or `assumed`. Put the access
|
|
871
|
+
under a guard isl can read, which decides the fact: `with when(i + 4 <
|
|
872
|
+
x.dom.size):` before `y[i] = x[i + 4]`.
|
|
873
|
+
- **A numpy scalar on the left of an operation loses its type**
|
|
874
|
+
([#167](https://github.com/xywei/loopty/issues/167)). `np.float64(0.1) *
|
|
875
|
+
x[i]` is traced as `0.1 * x[i]`, so beside a `float32` `x` the compiled run
|
|
876
|
+
multiplies in single precision where numpy uses double. The difference is
|
|
877
|
+
within the tolerance `loopty run` allows a `float32` input, so it reads
|
|
878
|
+
`tested`. Put the scalar on the right: `x[i] * np.float64(0.1)`.
|
|
879
|
+
- **Loop arithmetic outside a subscript is 32 bits wide compiled**
|
|
880
|
+
([#162](https://github.com/xywei/loopty/issues/162)). A sum of loop
|
|
881
|
+
variables and sizes used as a value, `y[i] = i + n`, `(i + n) // 2` or
|
|
882
|
+
`1.0 * (i + n)`, wraps round compiled once it passes `2**31`, which takes a
|
|
883
|
+
size past about `2**30`; natively it is exact. `loopty run` reports it only
|
|
884
|
+
on an input that large. Make the first operand a 64-bit integer:
|
|
885
|
+
`y[i] = i * np.int64(1) + n`. A loop bound and a guard on the loops,
|
|
886
|
+
`when(i + n > 5)`, stay 32 bits wide: keep their sums below `2**31`.
|
|
887
|
+
- **Integers past 64 bits**
|
|
888
|
+
([#139](https://github.com/xywei/loopty/issues/139)). Arithmetic of loop
|
|
889
|
+
variables, sizes and literals alone is exact natively, as Python's integers
|
|
890
|
+
are, and 64 bits wide compiled: `1.0 * i ** 5` wraps round compiled past
|
|
891
|
+
`i = 6208`, which `loopty run` reports on an input that reaches it. Where
|
|
892
|
+
you want a real, compute in one from the start: `(1.0 * i) ** 5`.
|
|
893
|
+
- **Values too large for a narrow numpy integer**
|
|
894
|
+
([#166](https://github.com/xywei/loopty/issues/166),
|
|
895
|
+
[#159](https://github.com/xywei/loopty/issues/159),
|
|
896
|
+
[#177](https://github.com/xywei/loopty/issues/177)). numpy raises when a
|
|
897
|
+
value does not fit an integer type such as `np.int16`, and loopty refuses a
|
|
898
|
+
compiled run before it starts wherever isl finds such a value at the call's
|
|
899
|
+
sizes. The check misses a product of loop variables, `a[i] + i * i` of an
|
|
900
|
+
`np.int16` `a`, loop arithmetic in a ragged row's loop, and a `uint64` past
|
|
901
|
+
`2**63` stored into an `Int` array: the compiled run wraps these round, and
|
|
902
|
+
`loopty run` raises the native run's error only on an input that reaches
|
|
903
|
+
one. It also refuses one case numpy completes, a sum whose `if` clause
|
|
904
|
+
repeats the statement's guard. In each case, widen the narrow value first,
|
|
905
|
+
`a[i] * np.int64(1) + i * i`, which both runs compute alike, and store a
|
|
906
|
+
`uint64` into an `np.uint64` array.
|
|
907
|
+
- **A few spellings fail without naming the fix**
|
|
908
|
+
([#172](https://github.com/xywei/loopty/issues/172),
|
|
909
|
+
[#173](https://github.com/xywei/loopty/issues/173),
|
|
910
|
+
[#175](https://github.com/xywei/loopty/issues/175)). A sum in a guard,
|
|
911
|
+
`when(reduce_sum(x[j] for j in x.dom) > 1)`, fails inside loopy with `name
|
|
912
|
+
already used as iname`: compute the sum into an array in a statement of its
|
|
913
|
+
own and read that array in the guard. A truth value first in arithmetic,
|
|
914
|
+
`True - k[i]`, fails while tracing with Python's `TypeError`: write the
|
|
915
|
+
integer, `1 - k[i]`. `k[i] - -(2**63)` of an `Int` is refused as a literal
|
|
916
|
+
past 64 bits, though numpy computes it: write `k[i] + -(2**63)`, the same
|
|
917
|
+
value in `int64`.
|
|
918
|
+
- **A refused program has written its outputs**
|
|
919
|
+
([#176](https://github.com/xywei/loopty/issues/176)). When a compiled
|
|
920
|
+
program raises `RefusedNatively` for a call that follows one of its checks
|
|
921
|
+
between calls, the whole program has already run: that call and the calls
|
|
922
|
+
after it have written their outputs, where natively the program stopped
|
|
923
|
+
inside that call. After the error, do not trust the arrays, and pass copies
|
|
924
|
+
if you need them as they were.
|
|
842
925
|
- **What passes between calls is stored in full or not at all**
|
|
843
|
-
([#132](https://github.com/xywei/loopty/issues/132),
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
a
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
extra, generates OpenCL and runs it through pyopencl, and nothing in CI or in
|
|
854
|
-
the test suite runs a device. The demos were run on two device classes by
|
|
926
|
+
([#132](https://github.com/xywei/loopty/issues/132),
|
|
927
|
+
[#133](https://github.com/xywei/loopty/issues/133)). `Schedule.substitute`
|
|
928
|
+
computes an intermediate again at every read; storing only the cells live at
|
|
929
|
+
once, a window of three cells of `f` after the Burgers fusion, is not done.
|
|
930
|
+
A substitution through two intermediates, one computed from the other, is
|
|
931
|
+
refused: substitute one of them.
|
|
932
|
+
- **The OpenCL target is not run in CI**
|
|
933
|
+
([#174](https://github.com/xywei/loopty/issues/174)). `--target opencl`,
|
|
934
|
+
with the `opencl` extra, generates OpenCL and runs it through pyopencl, and
|
|
935
|
+
nothing in CI runs a device. The demos were run on two device classes by
|
|
855
936
|
hand, and
|
|
856
937
|
[docs/device-runs.md](https://github.com/xywei/loopty/blob/main/docs/device-runs.md)
|
|
857
|
-
has the commands and the transcripts.
|
|
858
|
-
|
|
938
|
+
has the commands and the transcripts. Signed integer overflow is undefined
|
|
939
|
+
in OpenCL C, which has no `-fwrapv`, so a signed integer sum that
|
|
940
|
+
overflows, which numpy wraps round, can come out otherwise there: run such
|
|
941
|
+
a kernel on the C target.
|
|
859
942
|
- **A theorem's or a postcondition's statement is lanky's reading of it.**
|
|
860
|
-
loopty traces a kernel's body itself
|
|
861
|
-
|
|
862
|
-
lanky
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
943
|
+
loopty traces a kernel's body itself and checks that trace, the
|
|
944
|
+
`trace-faithful` fact. A theorem, and the claim after a kernel's `->`, are
|
|
945
|
+
annotations lanky reads. lanky 0.2.0 checks a theorem's reading at drawn
|
|
946
|
+
values, the row `the term computes what the annotations compute` under it,
|
|
947
|
+
and its [Known limits](https://github.com/xywei/lanky#known-limits) say what
|
|
948
|
+
that check can miss. A postcondition has no such row: read its `STATEMENT`
|
|
949
|
+
column in the ledger.
|
|
867
950
|
|
|
868
951
|
The Partial and Not yet lists under Status have the rest, and the
|
|
869
952
|
[open issues](https://github.com/xywei/loopty/issues) have every one.
|
|
@@ -878,7 +961,7 @@ loopty runs on Python 3.12 and 3.13: the islpy it is held to has no wheels for
|
|
|
878
961
|
3.14. CI, and every run this README reports, is on Linux. In a uv project,
|
|
879
962
|
`uv add loopty`, and the same for the extra below. The install pulls in:
|
|
880
963
|
|
|
881
|
-
- [lanky](https://github.com/xywei/lanky) 0.
|
|
964
|
+
- [lanky](https://github.com/xywei/lanky) 0.2.0 or later, the proof host:
|
|
882
965
|
`lanky check` prints the ledger, and loopty plugs into it;
|
|
883
966
|
- [loopy](https://github.com/inducer/loopy) 2025.2, which generates the code.
|
|
884
967
|
loopty builds on loopy's internals, so a release of loopty is held to the
|