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.
Files changed (98) hide show
  1. {loopty-0.1.0 → loopty-0.2.0}/CHANGELOG.md +185 -0
  2. {loopty-0.1.0 → loopty-0.2.0}/PKG-INFO +170 -87
  3. {loopty-0.1.0 → loopty-0.2.0}/README.md +168 -85
  4. {loopty-0.1.0 → loopty-0.2.0}/docs/loopy-notes.md +149 -35
  5. {loopty-0.1.0 → loopty-0.2.0}/docs/quickstart.md +2 -1
  6. {loopty-0.1.0 → loopty-0.2.0}/examples/README.md +11 -11
  7. {loopty-0.1.0 → loopty-0.2.0}/pyproject.toml +5 -2
  8. {loopty-0.1.0 → loopty-0.2.0}/src/loopty/__init__.py +1 -1
  9. {loopty-0.1.0 → loopty-0.2.0}/src/loopty/compose.py +7 -43
  10. {loopty-0.1.0 → loopty-0.2.0}/src/loopty/domain.py +14 -2
  11. {loopty-0.1.0 → loopty-0.2.0}/src/loopty/executor.py +69 -13
  12. {loopty-0.1.0 → loopty-0.2.0}/src/loopty/flow.py +19 -7
  13. {loopty-0.1.0 → loopty-0.2.0}/src/loopty/idx.py +20 -4
  14. {loopty-0.1.0 → loopty-0.2.0}/src/loopty/interpret.py +50 -22
  15. {loopty-0.1.0 → loopty-0.2.0}/src/loopty/lower.py +334 -39
  16. loopty-0.2.0/src/loopty/names.py +235 -0
  17. {loopty-0.1.0 → loopty-0.2.0}/src/loopty/operations.py +41 -2
  18. {loopty-0.1.0 → loopty-0.2.0}/src/loopty/promotion.py +58 -18
  19. loopty-0.2.0/src/loopty/refusal.py +756 -0
  20. {loopty-0.1.0 → loopty-0.2.0}/src/loopty/term.py +61 -0
  21. {loopty-0.1.0 → loopty-0.2.0}/src/loopty/trace.py +332 -41
  22. {loopty-0.1.0 → loopty-0.2.0}/tests/test_arithmetic.py +1060 -4
  23. {loopty-0.1.0 → loopty-0.2.0}/tests/test_examples.py +9 -3
  24. {loopty-0.1.0 → loopty-0.2.0}/tests/test_isl_reading.py +302 -0
  25. {loopty-0.1.0 → loopty-0.2.0}/tests/test_lower.py +94 -0
  26. loopty-0.2.0/tests/test_names.py +301 -0
  27. {loopty-0.1.0 → loopty-0.2.0}/tests/test_program.py +3 -2
  28. {loopty-0.1.0 → loopty-0.2.0}/tests/test_smoke.py +1 -1
  29. {loopty-0.1.0 → loopty-0.2.0}/.gitignore +0 -0
  30. {loopty-0.1.0 → loopty-0.2.0}/LICENSE +0 -0
  31. {loopty-0.1.0 → loopty-0.2.0}/docs/device-runs/device_demos.py +0 -0
  32. {loopty-0.1.0 → loopty-0.2.0}/docs/device-runs/h200-cuda-commands.log +0 -0
  33. {loopty-0.1.0 → loopty-0.2.0}/docs/device-runs/h200-cuda-device-demos.log +0 -0
  34. {loopty-0.1.0 → loopty-0.2.0}/docs/device-runs/h200-cuda-reduction-probe.log +0 -0
  35. {loopty-0.1.0 → loopty-0.2.0}/docs/device-runs/pocl-epyc-cpu-commands.log +0 -0
  36. {loopty-0.1.0 → loopty-0.2.0}/docs/device-runs/pocl-epyc-cpu-device-demos.log +0 -0
  37. {loopty-0.1.0 → loopty-0.2.0}/docs/device-runs/pocl-epyc-cpu-reduction-probe.log +0 -0
  38. {loopty-0.1.0 → loopty-0.2.0}/docs/device-runs/reduction_probe.py +0 -0
  39. {loopty-0.1.0 → loopty-0.2.0}/docs/device-runs.md +0 -0
  40. {loopty-0.1.0 → loopty-0.2.0}/examples/composition.py +0 -0
  41. {loopty-0.1.0 → loopty-0.2.0}/examples/fusion.py +0 -0
  42. {loopty-0.1.0 → loopty-0.2.0}/examples/p2p.py +0 -0
  43. {loopty-0.1.0 → loopty-0.2.0}/examples/pairs.py +0 -0
  44. {loopty-0.1.0 → loopty-0.2.0}/examples/reshape_layouts.py +0 -0
  45. {loopty-0.1.0 → loopty-0.2.0}/examples/spmv.py +0 -0
  46. {loopty-0.1.0 → loopty-0.2.0}/examples/stencil_skew.py +0 -0
  47. {loopty-0.1.0 → loopty-0.2.0}/examples/travel.py +0 -0
  48. {loopty-0.1.0 → loopty-0.2.0}/examples/wavefront_acoustic.py +0 -0
  49. {loopty-0.1.0 → loopty-0.2.0}/scripts/refresh_example_outputs.py +0 -0
  50. {loopty-0.1.0 → loopty-0.2.0}/src/loopty/arr.py +0 -0
  51. {loopty-0.1.0 → loopty-0.2.0}/src/loopty/cli.py +0 -0
  52. {loopty-0.1.0 → loopty-0.2.0}/src/loopty/contract.py +0 -0
  53. {loopty-0.1.0 → loopty-0.2.0}/src/loopty/faithful.py +0 -0
  54. {loopty-0.1.0 → loopty-0.2.0}/src/loopty/hypotheses.py +0 -0
  55. {loopty-0.1.0 → loopty-0.2.0}/src/loopty/isl_reading.py +0 -0
  56. {loopty-0.1.0 → loopty-0.2.0}/src/loopty/kernel.py +0 -0
  57. {loopty-0.1.0 → loopty-0.2.0}/src/loopty/oracle.py +0 -0
  58. {loopty-0.1.0 → loopty-0.2.0}/src/loopty/plugin.py +0 -0
  59. {loopty-0.1.0 → loopty-0.2.0}/src/loopty/py.typed +0 -0
  60. {loopty-0.1.0 → loopty-0.2.0}/src/loopty/reduction.py +0 -0
  61. {loopty-0.1.0 → loopty-0.2.0}/src/loopty/schedule.py +0 -0
  62. {loopty-0.1.0 → loopty-0.2.0}/src/loopty/tolerance.py +0 -0
  63. {loopty-0.1.0 → loopty-0.2.0}/src/loopty/typing.py +0 -0
  64. {loopty-0.1.0 → loopty-0.2.0}/tests/conftest.py +0 -0
  65. {loopty-0.1.0 → loopty-0.2.0}/tests/hand_terms.py +0 -0
  66. {loopty-0.1.0 → loopty-0.2.0}/tests/kernels/axpy.py +0 -0
  67. {loopty-0.1.0 → loopty-0.2.0}/tests/kernels/domains.py +0 -0
  68. {loopty-0.1.0 → loopty-0.2.0}/tests/kernels/out_of_bounds.py +0 -0
  69. {loopty-0.1.0 → loopty-0.2.0}/tests/kernels/spmv_min.py +0 -0
  70. {loopty-0.1.0 → loopty-0.2.0}/tests/kernels/stencil.py +0 -0
  71. {loopty-0.1.0 → loopty-0.2.0}/tests/kernels/travel.py +0 -0
  72. {loopty-0.1.0 → loopty-0.2.0}/tests/kernels/travel_wrong.py +0 -0
  73. {loopty-0.1.0 → loopty-0.2.0}/tests/test_adversarial.py +0 -0
  74. {loopty-0.1.0 → loopty-0.2.0}/tests/test_arr.py +0 -0
  75. {loopty-0.1.0 → loopty-0.2.0}/tests/test_buildable.py +0 -0
  76. {loopty-0.1.0 → loopty-0.2.0}/tests/test_cli.py +0 -0
  77. {loopty-0.1.0 → loopty-0.2.0}/tests/test_contract_bounds.py +0 -0
  78. {loopty-0.1.0 → loopty-0.2.0}/tests/test_contraction.py +0 -0
  79. {loopty-0.1.0 → loopty-0.2.0}/tests/test_declared_layout.py +0 -0
  80. {loopty-0.1.0 → loopty-0.2.0}/tests/test_domains.py +0 -0
  81. {loopty-0.1.0 → loopty-0.2.0}/tests/test_executor.py +0 -0
  82. {loopty-0.1.0 → loopty-0.2.0}/tests/test_executor_c.py +0 -0
  83. {loopty-0.1.0 → loopty-0.2.0}/tests/test_faithful.py +0 -0
  84. {loopty-0.1.0 → loopty-0.2.0}/tests/test_flow.py +0 -0
  85. {loopty-0.1.0 → loopty-0.2.0}/tests/test_fusion.py +0 -0
  86. {loopty-0.1.0 → loopty-0.2.0}/tests/test_idx.py +0 -0
  87. {loopty-0.1.0 → loopty-0.2.0}/tests/test_interpret.py +0 -0
  88. {loopty-0.1.0 → loopty-0.2.0}/tests/test_kernel.py +0 -0
  89. {loopty-0.1.0 → loopty-0.2.0}/tests/test_lower_traced.py +0 -0
  90. {loopty-0.1.0 → loopty-0.2.0}/tests/test_oracle.py +0 -0
  91. {loopty-0.1.0 → loopty-0.2.0}/tests/test_refresh_script.py +0 -0
  92. {loopty-0.1.0 → loopty-0.2.0}/tests/test_schedule.py +0 -0
  93. {loopty-0.1.0 → loopty-0.2.0}/tests/test_storage.py +0 -0
  94. {loopty-0.1.0 → loopty-0.2.0}/tests/test_term.py +0 -0
  95. {loopty-0.1.0 → loopty-0.2.0}/tests/test_trace.py +0 -0
  96. {loopty-0.1.0 → loopty-0.2.0}/tests/test_travel.py +0 -0
  97. {loopty-0.1.0 → loopty-0.2.0}/tests/test_typing.py +0 -0
  98. {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.1.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.1.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
- 21 facts: 16 decided, 5 tested
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.1.0, the first release. The core works: kernels and programs run
258
- natively, trace, state their facts, lower and run compiled on the C target,
259
- and every schedule step is checked; the edges are sharp. Below is what works,
260
- what works in part and what does not work yet, and after it, under Known
261
- limits, the open issues you are most likely to meet, one of them a fact the
262
- ledger calls `decided` that is false: read that before you rely on a ledger.
263
- CI runs the suite on every change, on Python 3.12 and 3.13 and against lanky's
264
- `main`, and reruns every command whose output this README, the quickstart and
265
- the examples' README show, holding them to what it prints. It runs no OpenCL
266
- device. Until 1.0 an interface can change in a minor release; the CHANGELOG
267
- says when one does.
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 (#148; note 23 in `docs/loopy-notes.md`). A guard has to be a truth value, and one whose
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. `abs` of an integer is numpy's,
490
- the smallest value of its type included; a comparison of one does not
491
- trace yet (`abs(k[i]) > 2`, xywei/lanky#94). An integer literal past 64 bits
492
- is written as a real where numpy computes in one (`x[i] * 2**70`), or as a
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 a
640
- temporary is a variable-length array on the stack of the call, which bounds
641
- its size (note 16 in `docs/loopy-notes.md`); on OpenCL it is a global
642
- temporary, which is generated but, like every device path, not run from a
643
- development machine.
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, and so is a sum of loop variables,
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: `x[(i * i) % n]` (#129), and `x[(i * 499999)
658
- // 1000000]`, which loopy wrote with a division of the wrapped `499999 *
659
- i`. A sum the plan leaves in 32 bits stays so in a division too, `x[(i +
660
- n) // 2]` past `2**30` (#149). A subscript over a narrow numpy integer
661
- type is not converted back into it (#157). A `Fin[m]` array the kernel
662
- writes may be an `int32` one natively, whose entries the native run reads
663
- as `int64`, as the compiled run computes with them, unless a program's
664
- checked point reads it, which takes an `int64` one. An integer to a
665
- negative power whose exponent is not a literal is left to numpy's refusal
666
- natively, and computed as an integer compiled. pymbolic builds `u[i] - 1`
667
- and `u[i] + -1` as one term, read as the difference: of an unsigned `u`
668
- numpy refuses the second at every point, and the compiled run computes it
669
- as the first; `a - b` and `a + -b` of an `int8` `b` likewise differ
670
- natively at `b = -128` alone, and of an `Int` `a` and an unsigned `b` at
671
- every `b` but zero (#151). A loop variable's arithmetic beside
672
- a numpy integer whose type does not hold its value at some point,
673
- `u[i] + (i - 3)` of a `uint32` `u`, is refused natively there and
674
- computed compiled (#153), and `abs` of a real in a guard fails inside
675
- loopy (#154). See notes 19, 20 and 23 in `docs/loopy-notes.md`.
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 a user is most likely to meet. Each says what the ledger and
823
- the runs make of it, since that is what decides whether it can go unnoticed.
824
-
825
- - **Some index arithmetic is 32 bits wide compiled**
826
- ([#149](https://github.com/xywei/loopty/issues/149), [#157](https://github.com/xywei/loopty/issues/157)). A loop bound, and a subscript loopy
827
- reads as affine with no division, are loopy's index arithmetic, 32 bits
828
- wide. Every other subscript is computed in 64 bits, as natively. A sum of
829
- loop variables and sizes inside a dividing subscript stays in 32 bits, so
830
- `x[(i + n) // 2]` wraps round past `2**30` (#149). A subscript over a
831
- narrow numpy integer type, such as `int8` or `uint16`, is computed in C's
832
- `int` compiled, and wraps round natively (#157).
833
- - **A guard loopy's bounds check cannot read**
834
- ([#148](https://github.com/xywei/loopty/issues/148)). An access that only a floating-point guard keeps in
835
- bounds, `x[2 * i]` under `when(i * 2.0 < n)`, is refused by loopy's bounds
836
- check, with a message that names no fix. Write the guard over integers.
837
- - **Integers past 64 bits** ([#139](https://github.com/xywei/loopty/issues/139), [#159](https://github.com/xywei/loopty/issues/159)). Arithmetic of
838
- loop variables, sizes and literals alone is exact natively and 64 bits wide
839
- compiled: `1.0 * i ** 5` wraps round compiled past `i = 6208`, and the
840
- differential fact is refuted. A `uint64` value past `2**63` stored into an
841
- `Int` array is refused natively and wraps round compiled.
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), [#133](https://github.com/xywei/loopty/issues/133)). `Schedule.substitute` computes an
844
- intermediate again at every read; keeping only the cells live at once, a
845
- window of three cells of `f` after the Burgers fusion, is not done. Nor is
846
- a substitution through two intermediates, one computed from the other.
847
- - **A program's intermediates live on the C stack**
848
- ([#62](https://github.com/xywei/loopty/issues/62)). An array a program makes
849
- with `Arr.zeros_like` is a variable-length array on the stack of the compiled
850
- call, so one larger than the stack crashes the compiled run, and nothing
851
- checks the size first.
852
- - **The OpenCL target is not run in CI.** `--target opencl`, with the `opencl`
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. A comparison of complex values does
858
- not build on it ([#158](https://github.com/xywei/loopty/issues/158)).
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, and the `trace-faithful` fact checks
861
- that trace. A theorem, and the claim after a kernel's `->`, are annotations
862
- lanky evaluates, and lanky's
863
- [Known limits](https://github.com/xywei/lanky#known-limits) apply to them:
864
- an operation that answers from a term object rather than through its
865
- overloads can make the statement say something other than what was
866
- written. Read the ledger's `STATEMENT` column.
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.1.0 or later, the proof host:
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