main.bend

bend-ml-tensor-array@0.1.15.0 · 1,818 lines · 0 definitions

1# bend-ml-tensor-array: tensors over a flat Array<F32>, with the shape in the type.
2#
3#   import bend-ml-tensor-array@0.1.15.0/main.bend as TA
4#
5# Mat<r, c> stores r*c numbers in an Array<F32>, row by row (index i*c + j). The
6# dimensions are erased type parameters: the same guarantee as bend-ml-tensor
7# (a product with wrong dimensions does not compile), but ~50x faster than lists:
8# Array.get/set by index, and products in parallel blocks of rows.
9#
10# An Array is affine (a single owner): every operation that reads an operand returns it
11# together with the result (as Array.get does), in records MM, AR, ... Use Mat.clone
12# when you need two copies.
13#
14# What is NOT proved: the invariant "the Array has capacity >= r*c" holds because
15# the constructors (Mat.zeros, Mat.of_list) allocate the right size; and F32 is
16# validated by tests against PyTorch (reference/test_tensor_array.py), not by proof.
17
18import Base
19
20# ---------------------------------------------------------------------
21# helper types
22# ---------------------------------------------------------------------
23
24type R is Type:
25  R{a: Array<F32>, b: Array<F32>, x: F32}
26
27type G is Type:
28  G{a: Array<F32>, b: Array<F32>, c: Array<F32>}
29
30# A dot product with eight partial sums (0.1.15.0; four before). Each step adds its product to a0 and rotates the sums
31# (a1, ..., a7, a0), so each sum gets every eighth product and the processor can work on the next additions before the
32# last one has finished; at the end the sums are added as a balanced tree of pairs. The order of the additions is fixed,
33# so the result is the same on every run and every thread count, but it is not the order of a single running sum
34# (NOTES.md, exp. 17 and 2026-10-25). The cells of 8 below keep exactly this order: sum q is lane q of every cell.
35def dotr(k: Nat, +ia: U32, +ib: U32, +sa: U32, +sb: U32, a0: F32, a1: F32, a2: F32, a3: F32, a4: F32, a5: F32, a6: F32, a7: F32, ra: Array<F32> & F32, rb: Array<F32> & F32) -> R:
36  match k ra rb:
37    case 0n Tuple{a, x} Tuple{b, y}:
38      R{a, b, (((a0 + a1 : F32) + (a2 + a3 : F32) : F32) + ((a4 + a5 : F32) + (a6 + a7 : F32) : F32) : F32)}
39    case 1n+p Tuple{a, +x} Tuple{b, +y}:
40      dotr(p, (ia + sa : U32), (ib + sb : U32), sa, sb, a1, a2, a3, a4, a5, a6, a7, (a0 + (x * y : F32) : F32), Array.get(F32, a, (ia + sa : U32)), Array.get(F32, b, (ib + sb : U32)))
41
42def dotg(k: Nat, +ia: U32, +ib: U32, +sa: U32, +sb: U32, acc: F32, ra: Array<F32> & F32, rb: Array<F32> & F32) -> R:
43  dotr(k, ia, ib, sa, sb, acc, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, ra, rb)
44
45# writes one cell of C: c[idx] = x, or c[idx] = c[idx] + x when ac (an accumulating product, 0.1.10.0). x is computed
46# exactly as without ac and added once, so C + A·B has the bits of adding a separately computed A·B to C.
47
48def cwr_add(+idx: U32, +x: F32, r: Array<F32> & F32) -> Array<F32>:
49  match r:
50    case Tuple{c, +v}:
51      Array.set(F32, c, idx, (v + x : F32))
52
53def cwr(ac: Bool, +idx: U32, +x: F32, c: Array<F32>) -> Array<F32>:
54  match ac:
55    case True{}:
56      cwr_add(idx, x, Array.get(F32, c, idx))
57    case False{}:
58      Array.set(F32, c, idx, x)
59
60# row i of C, columns j.. ; r is the product of the current column (already computed)
61
62def gcols(+ac: Bool, mleft: Nat, +kn: Nat, +ia0: U32, +ib0: U32, +sa: U32, +sb: U32, +bcol: U32, +cidx: U32, c: Array<F32>, r: R) -> G:
63  match mleft r:
64    case 0n R{a, b, +x}:
65      G{a, b, cwr(ac, cidx, x, c)}
66    case 1n+p R{a, b, +x}:
67      gcols(ac, p, kn, ia0, (ib0 + bcol : U32), sa, sb, bcol, (cidx + 1 : U32), cwr(ac, cidx, x, c), dotg(kn, ia0, (ib0 + bcol : U32), sa, sb, 0.0, Array.get(F32, a, ia0), Array.get(F32, b, (ib0 + bcol : U32))))
68
69def grows(+ac: Bool, nleft: Nat, +mm1: Nat, +kn: Nat, +i: U32, +arow: U32, +sa: U32, +sb: U32, +bcol: U32, +mu: U32, g: G) -> G:
70  match nleft g:
71    case 0n G{a, b, c}:
72      G{a, b, c}
73    case 1n+p G{a, b, c}:
74      grows(ac, p, mm1, kn, (i + 1 : U32), arow, sa, sb, bcol, mu, gcols(ac, mm1, kn, (i * arow : U32), 0, sa, sb, bcol, (i * mu : U32), c, dotg(kn, (i * arow : U32), 0, sa, sb, 0.0, Array.get(F32, a, (i * arow : U32)), Array.get(F32, b, 0))))
75
76# C (n x m) = A · B with k steps, or C + A · B when ac; c is the output array (with capacity n*m)
77
78def gemm_ac(+ac: Bool, +n: Nat, +m: Nat, +k: Nat, +arow: U32, +sa: U32, +sb: U32, +bcol: U32, a: Array<F32>, b: Array<F32>, c: Array<F32>) -> G:
79  grows(ac, n, Nat.sub(m, 1n), k, 0, arow, sa, sb, bcol, U32.from_nat(m), G{a, b, c})
80
81def gemm(+n: Nat, +m: Nat, +k: Nat, +arow: U32, +sa: U32, +sb: U32, +bcol: U32, a: Array<F32>, b: Array<F32>, c: Array<F32>) -> G:
82  gemm_ac(False{}, n, m, k, arow, sa, sb, bcol, a, b, c)
83
84
85# ---------------------------------------------------------------------
86# parallel gemm: 2^d blocks of rows of C, each with its own copy of A and B,
87# returning lists that are written into C at the end
88# ---------------------------------------------------------------------
89
90type S3 is Type:
91  S3{a: Array<F32>, b: Array<F32>, out: List<&2, F32>}
92
93def lcols(mleft: Nat, +kn: Nat, +ia0: U32, +ib0: U32, +sa: U32, +sb: U32, +bcol: U32, out: List<&2, F32>, r: R) -> S3:
94  match mleft r:
95    case 0n R{a, b, +x}:
96      S3{a, b, x <> out}
97    case 1n+p R{a, b, +x}:
98      lcols(p, kn, ia0, (ib0 + bcol : U32), sa, sb, bcol, x <> out, dotg(kn, ia0, (ib0 + bcol : U32), sa, sb, 0.0, Array.get(F32, a, ia0), Array.get(F32, b, (ib0 + bcol : U32))))
99
100def lrows(nleft: Nat, +mm1: Nat, +kn: Nat, +i: U32, +arow: U32, +sa: U32, +sb: U32, +bcol: U32, st: S3) -> S3:
101  match nleft st:
102    case 0n S3{a, b, out}:
103      S3{a, b, out}
104    case 1n+p S3{a, b, out}:
105      lrows(p, mm1, kn, (i + 1 : U32), arow, sa, sb, bcol, lcols(mm1, kn, (i * arow : U32), 0, sa, sb, bcol, out, dotg(kn, (i * arow : U32), 0, sa, sb, 0.0, Array.get(F32, a, (i * arow : U32)), Array.get(F32, b, 0))))
106
107def lblock_fin(st: S3) -> List<&2, F32>:
108  match st:
109    case S3{a, b, out}:
110      List.reverse(&2, F32, out)
111
112def lblock(cnt: Nat, +mm1: Nat, +kn: Nat, +i: U32, +arow: U32, +sa: U32, +sb: U32, +bcol: U32, a: Array<F32>, b: Array<F32>) -> List<&2, F32>:
113  lblock_fin(lrows(cnt, mm1, kn, i, arow, sa, sb, bcol, S3{a, b, Nil{}}))
114
115def lpar(d: Nat, +cnt: Nat, +mm1: Nat, +kn: Nat, +i: U32, +arow: U32, +sa: U32, +sb: U32, +bcol: U32, ra: Array<F32> & Array<F32>, rb: Array<F32> & Array<F32>) -> List<&2, F32>:
116  match d ra rb:
117    case 0n Tuple{a, a2} Tuple{b, b2}:
118      lblock(cnt, mm1, kn, i, arow, sa, sb, bcol, a, b)
119    case 1n++q Tuple{a, a2} Tuple{b, b2}:
120      x y = lpar(q, Nat.div(cnt, 2n), mm1, kn, i, arow, sa, sb, bcol, Array.clone(F32, a), Array.clone(F32, b)) lpar(q, Nat.sub(cnt, Nat.div(cnt, 2n)), mm1, kn, (i + U32.from_nat(Nat.div(cnt, 2n)) : U32), arow, sa, sb, bcol, Array.clone(F32, a2), Array.clone(F32, b2))
121      List.append(&2, F32, x, y)
122
123def write_l(xs: List<&2, F32>, +idx: U32, c: Array<F32>) -> Array<F32>:
124  match xs:
125    case Nil{}:
126      c
127    case Con{h, t}:
128      write_l(t, (idx + 1 : U32), Array.set(F32, c, idx, h))
129
130# c[idx..] = list, or c[idx..] += list when ac
131
132def write_la(+ac: Bool, xs: List<&2, F32>, +idx: U32, c: Array<F32>) -> Array<F32>:
133  match xs:
134    case Nil{}:
135      c
136    case Con{+h, t}:
137      write_la(ac, t, (idx + 1 : U32), cwr(ac, idx, h, c))
138
139def gp3(+ac: Bool, a: Array<F32>, b: Array<F32>, c: Array<F32>, xs: List<&2, F32>) -> G:
140  G{a, b, write_la(ac, xs, 0, c)}
141
142def gp2(d: Nat, +n: Nat, +mm1: Nat, +kn: Nat, +arow: U32, +sa: U32, +sb: U32, +bcol: U32, c: Array<F32>, pa: Array<F32> & Array<F32>, pb: Array<F32> & Array<F32>) -> G:
143  match pa pb:
144    case Tuple{a1, a2} Tuple{b1, b2}:
145      gp3(False{}, a1, b1, c, lpar(d, n, mm1, kn, 0, arow, sa, sb, bcol, Array.clone(F32, a2), Array.clone(F32, b2)))
146
147# C (n x m) = A · B, with 2^d blocks of rows in parallel; returns A, B (intact) and C
148
149def gemm_par(d: Nat, +n: Nat, +m: Nat, +k: Nat, +arow: U32, +sa: U32, +sb: U32, +bcol: U32, a: Array<F32>, b: Array<F32>, c: Array<F32>) -> G:
150  gp2(d, n, Nat.sub(m, 1n), k, arow, sa, sb, bcol, c, Array.clone(F32, a), Array.clone(F32, b))
151
152
153# ---------------------------------------------------------------------
154# parallel gemm by COLUMNS, for n = 1 (matrix · vector): each block computes a
155# range of columns of C, with its own copy of the operands
156# ---------------------------------------------------------------------
157
158def cblock_fin(st: S3) -> List<&2, F32>:
159  match st:
160    case S3{a, b, out}:
161      List.reverse(&2, F32, out)
162
163# cnt columns starting at j0 (cnt >= 1); row 0 of A is at ia0
164
165def cblock_go(cnt1: Nat, +kn: Nat, +ia0: U32, +j0: U32, +sa: U32, +sb: U32, +bcol: U32, a: Array<F32>, b: Array<F32>) -> List<&2, F32>:
166  cblock_fin(lcols(cnt1, kn, ia0, (j0 * bcol : U32), sa, sb, bcol, Nil{}, dotg(kn, ia0, (j0 * bcol : U32), sa, sb, 0.0, Array.get(F32, a, ia0), Array.get(F32, b, (j0 * bcol : U32)))))
167
168def cblock(cnt: Nat, +kn: Nat, +ia0: U32, +j0: U32, +sa: U32, +sb: U32, +bcol: U32, a: Array<F32>, b: Array<F32>) -> List<&2, F32>:
169  match cnt:
170    case 0n:
171      Nil{}
172    case 1n+p:
173      cblock_go(p, kn, ia0, j0, sa, sb, bcol, a, b)
174
175def cpar(d: Nat, +cnt: Nat, +kn: Nat, +ia0: U32, +j0: U32, +sa: U32, +sb: U32, +bcol: U32, ra: Array<F32> & Array<F32>, rb: Array<F32> & Array<F32>) -> List<&2, F32>:
176  match d ra rb:
177    case 0n Tuple{a, a2} Tuple{b, b2}:
178      cblock(cnt, kn, ia0, j0, sa, sb, bcol, a, b)
179    case 1n++q Tuple{a, a2} Tuple{b, b2}:
180      x y = cpar(q, Nat.div(cnt, 2n), kn, ia0, j0, sa, sb, bcol, Array.clone(F32, a), Array.clone(F32, b)) cpar(q, Nat.sub(cnt, Nat.div(cnt, 2n)), kn, ia0, (j0 + U32.from_nat(Nat.div(cnt, 2n)) : U32), sa, sb, bcol, Array.clone(F32, a2), Array.clone(F32, b2))
181      List.append(&2, F32, x, y)
182
183def cp2(+ac: Bool, d: Nat, +m: Nat, +kn: Nat, +sa: U32, +sb: U32, +bcol: U32, c: Array<F32>, pa: Array<F32> & Array<F32>, pb: Array<F32> & Array<F32>) -> G:
184  match pa pb:
185    case Tuple{a1, a2} Tuple{b1, b2}:
186      gp3(ac, a1, b1, c, cpar(d, m, kn, 0, 0, sa, sb, bcol, Array.clone(F32, a2), Array.clone(F32, b2)))
187
188# C (1 x m) = A (1 x k) · B, with 2^d blocks of columns in parallel. Since 0.1.12.0 no product calls it (one row runs
189# the scalar loop, see gemm_any3): it clones both operands at every node. Kept for callers of earlier versions.
190
191def gemm_par_cols(+ac: Bool, d: Nat, +m: Nat, +k: Nat, +sa: U32, +sb: U32, +bcol: U32, a: Array<F32>, b: Array<F32>, c: Array<F32>) -> G:
192  cp2(ac, d, m, k, sa, sb, bcol, c, Array.clone(F32, a), Array.clone(F32, b))
193
194# ---------------------------------------------------------------------
195# element-wise operations (in place, by index)
196# ---------------------------------------------------------------------
197
198# ---------------------------------------------------------------------
199# gemm: C[i][j] = sum_p A[i*arow + p*sa] * B[p*sb + j*bcol]
200# (nn: arow=K, sa=1, sb=M, bcol=1 | nt: B stored m x k: sb=1, bcol=K | tn: A stored k x n: arow=1, sa=N)
201# ---------------------------------------------------------------------
202
203# acc += A[ia + t*sa] * B[ib + t*sb] for k steps; ra, rb already carry the first pair
204
205type P2 is Type:
206  P2{a: Array<F32>, b: Array<F32>}
207
208type AL is Type:
209  AL{c: Array<F32>, l: List<&2, F32>}
210
211# list of m elements starting at idx (reads with Array.get, returning the array)
212
213def read_l(mleft: Nat, +idx: U32, acc: List<&2, F32>, rc: Array<F32> & F32) -> AL:
214  match mleft rc:
215    case 0n Tuple{c, +v}:
216      AL{c, List.reverse(&2, F32, v <> acc)}
217    case 1n+p Tuple{c, +v}:
218      read_l(p, (idx + 1 : U32), v <> acc, Array.get(F32, c, (idx + 1 : U32)))
219
220# writes the list starting at idx
221
222def addl(xs: List<&2, F32>, +idx: U32, rc: Array<F32> & F32) -> Array<F32>:
223  match xs rc:
224    case Nil{} Tuple{c, v}:
225      c
226    case Con{+h, t} Tuple{c, +v}:
227      addl(t, (idx + 1 : U32), Array.get(F32, Array.set(F32, c, idx, (v + h : F32)), (idx + 1 : U32)))
228
229# adds a bias (a list of m) to each of the n rows of c (n x m)
230
231def bias_rows(nleft: Nat, +i: U32, +mu: U32, +bl: List<&2, F32>, c: Array<F32>) -> Array<F32>:
232  match nleft:
233    case 0n:
234      c
235    case 1n+p:
236      bias_rows(p, (i + 1 : U32), mu, bl, addl(bl, (i * mu : U32), Array.get(F32, c, (i * mu : U32))))
237
238# in-place relu on the first n elements
239
240def relu_l(nleft: Nat, +t: U32, rc: Array<F32> & F32) -> Array<F32>:
241  match nleft rc:
242    case 0n Tuple{c, v}:
243      c
244    case 1n+p Tuple{c, +v}:
245      relu_l(p, (t + 1 : U32), Array.get(F32, Array.set(F32, c, t, F32.max(v, 0.0)), (t + 1 : U32)))
246
247def pos2(b: Bool) -> F32:
248  match b:
249    case True{}:
250      1.0
251    case False{}:
252      0.0
253
254def pos(+y: F32) -> F32:
255  pos2(F32.is_gt(y, 0.0))
256
257# d[t] *= (h[t] > 0): the relu gradient; returns both arrays
258
259def mask_l(nleft: Nat, +t: U32, rd: Array<F32> & F32, rh: Array<F32> & F32) -> P2:
260  match nleft rd rh:
261    case 0n Tuple{d, x} Tuple{h, y}:
262      P2{d, h}
263    case 1n+p Tuple{d, +x} Tuple{h, +y}:
264      mask_l(p, (t + 1 : U32), Array.get(F32, Array.set(F32, d, t, (x * pos(y) : F32)), (t + 1 : U32)), Array.get(F32, h, (t + 1 : U32)))
265
266# w[t] -= lr * dw[t]
267
268def maxl2(xs: List<&2, F32>, cur: F32) -> F32:
269  match xs:
270    case Nil{}:
271      cur
272    case Con{+h, t}:
273      maxl2(t, F32.max(cur, h))
274
275def maxl(xs: List<&2, F32>) -> F32:
276  match xs:
277    case Nil{}:
278      0.0
279    case Con{+h, t}:
280      maxl2(t, h)
281
282def expl(xs: List<&2, F32>, +m: F32) -> List<&2, F32>:
283  match xs:
284    case Nil{}:
285      Nil{}
286    case Con{+h, t}:
287      F32.exp((h - m : F32)) <> expl(t, m)
288
289def suml(xs: List<&2, F32>, acc: F32) -> F32:
290  match xs:
291    case Nil{}:
292      acc
293    case Con{+h, t}:
294      suml(t, (acc + h : F32))
295
296def divl(xs: List<&2, F32>, +s: F32) -> List<&2, F32>:
297  match xs:
298    case Nil{}:
299      Nil{}
300    case Con{+h, t}:
301      (h / s : F32) <> divl(t, s)
302
303def softmax2(+es: List<&2, F32>) -> List<&2, F32>:
304  divl(es, suml(es, 0.0))
305
306def softmax(+xs: List<&2, F32>) -> List<&2, F32>:
307  softmax2(expl(xs, maxl(xs)))
308
309# p[label]
310
311def sgd_l(nleft: Nat, +t: U32, +lr: F32, rw: Array<F32> & F32, rd: Array<F32> & F32) -> P2:
312  match nleft rw rd:
313    case 0n Tuple{w, x} Tuple{d, y}:
314      P2{w, d}
315    case 1n+p Tuple{w, +x} Tuple{d, +y}:
316      sgd_l(p, (t + 1 : U32), lr, Array.get(F32, Array.set(F32, w, t, (x - (lr * y : F32) : F32)), (t + 1 : U32)), Array.get(F32, d, (t + 1 : U32)))
317
318# ---------------------------------------------------------------------
319# softmax + cross-entropy per row (lists of 10 elements: negligible cost)
320# ---------------------------------------------------------------------
321
322def pick(ps: List<&2, F32>, n: Nat) -> F32:
323  match ps n:
324    case Con{+h, t} 0n:
325      h
326    case Con{h, t} 1n+q:
327      pick(t, q)
328    case Nil{} _:
329      0.0
330
331# (p - onehot(label)) / n
332
333def hit(b: Bool) -> F32:
334  match b:
335    case True{}:
336      1.0
337    case False{}:
338      0.0
339
340def grad_l(ps: List<&2, F32>, +label: Nat, +j: Nat, +nf: F32) -> List<&2, F32>:
341  match ps:
342    case Nil{}:
343      Nil{}
344    case Con{+h, t}:
345      (((h - hit(Nat.is_eq(label, j)) : F32) / nf) : F32) <> grad_l(t, label, 1n+j, nf)
346
347type L2 is Type:
348  L2{loss: F32, c: Array<F32>}
349
350def ce_row2(+ps: List<&2, F32>, +label: Nat, +nf: F32, +idx: U32, loss: F32, c: Array<F32>) -> L2:
351  L2{(loss - F32.log(F32.max(pick(ps, label), 0.000000000001)) : F32), write_l(grad_l(ps, label, 0n, nf), idx, c)}
352
353def ce_row(al: AL, +label: Nat, +nf: F32, +idx: U32, loss: F32) -> L2:
354  match al:
355    case AL{c, l}:
356      ce_row2(softmax(l), label, nf, idx, loss, c)
357
358# walks the rows of z2 (n x m): r carries the accumulated loss and the array; at the end the array has become the gradient
359
360type Mat<-r: Nat, -c: Nat> is Type:
361  Mat{d: Array<F32>}
362
363def ce_rows(labels: List<&2, Nat>, +mm1: Nat, +mu: U32, +nf: F32, +i: U32, r: L2) -> L2:
364  match labels r:
365    case Nil{} L2{loss, c}:
366      L2{loss, c}
367    case Con{+y, t} L2{loss, c}:
368      ce_rows(t, mm1, mu, nf, (i + 1 : U32), ce_row(read_l(mm1, (i * mu : U32), Nil{}, Array.get(F32, c, (i * mu : U32))), y, nf, (i * mu : U32), loss))
369
370# ---- helper: hits per row ----
371
372type CR is Type:
373  CR{c: Array<F32>, n: Nat}
374
375def gt_head(xs: List<&2, F32>, +v: F32) -> Bool:
376  match xs:
377    case Nil{}:
378      False{}
379    case Con{+h, t}:
380      F32.is_gt(h, v)
381
382def argmax2(xs: List<&2, F32>, g: Bool, +i: Nat, +bi: Nat, +bv: F32) -> Nat:
383  match xs g:
384    case Nil{} _:
385      bi
386    case Con{+h, +t} True{}:
387      argmax2(t, gt_head(t, h), 1n+i, i, h)
388    case Con{h, +t} False{}:
389      argmax2(t, gt_head(t, bv), 1n+i, bi, bv)
390
391def argmax(xs: List<&2, F32>) -> Nat:
392  match xs:
393    case Nil{}:
394      0n
395    case Con{+h, +t}:
396      argmax2(t, gt_head(t, h), 1n, 0n, h)
397
398def one(b: Bool) -> Nat:
399  match b:
400    case True{}:
401      1n
402    case False{}:
403      0n
404
405def count_rows3(+y: Nat, n: Nat, al: AL) -> CR:
406  match al:
407    case AL{c, l}:
408      CR{c, Nat.add(n, one(Nat.is_eq(argmax(l), y)))}
409
410def count_rows(labels: List<&2, Nat>, +mm1: Nat, +mu: U32, +i: U32, r: CR) -> CR:
411  match labels r:
412    case Nil{} CR{c, n}:
413      CR{c, n}
414    case Con{+y, t} CR{c, n}:
415      count_rows(t, mm1, mu, (i + 1 : U32), count_rows3(y, n, read_l(mm1, (i * mu : U32), Nil{}, Array.get(F32, c, (i * mu : U32)))))
416
417
418# =====================================================================
419# Typed API: Mat<r, c> (the dimensions enter the type)
420# =====================================================================
421
422# =====================================================================
423# Array capacity: a PROVED depth for the number of slots
424# =====================================================================
425#
426# cap_depth(n) is the d such that an Array with 2^d slots holds n elements. It is computed by
427# structural recursion on n (not with U32.log2) so that the law cap_ok below can prove
428# `n <= 2^cap_depth(n)`. What stays trusted is only that Array.new(T, d, v) allocates 2^d slots.
429
430# a type chosen by a Bool: rewriting through it refutes True == False
431def BD(b: Bool, t: Type, f: Type) -> Type:
432  match b:
433    case True{}:
434      t
435    case False{}:
436      f
437
438# a <= b, by direct recursion
439def leb(a: Nat, b: Nat) -> Bool:
440  match a b:
441    case 0n _:
442      True{}
443    case 1n+p 0n:
444      False{}
445    case 1n+p 1n+q:
446      leb(p, q)
447
448def pow2(d: Nat) -> Nat:
449  match d:
450    case 0n:
451      1n
452    case 1n++q:
453      Nat.add(pow2(q), pow2(q))
454
455# smallest power of two >= n, by structural recursion on n: returns (depth, 2^depth)
456def cap_pick(ok: Bool, d: Nat, +p: Nat) -> Nat & Nat:
457  match ok:
458    case True{}:
459      (d, p)
460    case False{}:
461      (1n+d, Nat.add(p, p))
462
463def cap_step(r: Nat & Nat, +n: Nat) -> Nat & Nat:
464  match r:
465    case Tuple{+d, +p}:
466      cap_pick(leb(n, p), d, p)
467
468def cap_pair(n: Nat) -> Nat & Nat:
469  match n:
470    case 0n:
471      (0n, 1n)
472    case 1n++m:
473      cap_step(cap_pair(m), 1n+m)
474
475def fst_nat(r: Nat & Nat) -> Nat:
476  match r:
477    case Tuple{d, p}:
478      d
479
480def snd_nat(r: Nat & Nat) -> Nat:
481  match r:
482    case Tuple{d, p}:
483      p
484
485# leb is Base's Nat.is_le, case for case. leb walks its arguments one step at a time in the compiled program; Base's
486# comparison is one native comparison there (0.1.13.0).
487def le_leb(a: Nat, b: Nat) -> {Nat.is_le(a, b) == leb(a, b) : Bool}:
488  match a b:
489    case 0n 0n:
490      {==}
491    case 0n 1n+q:
492      {==}
493    case 1n+p 0n:
494      {==}
495    case 1n+p 1n+q:
496      le_leb(p, q)
497
498def leb_of(+a: Nat, +b: Nat, e: {True{} == Nat.is_le(a, b) : Bool}) -> {True{} == leb(a, b) : Bool}:
499  %le_leb(a, b) : {True{} == _ : Bool}
500  e
501
502# the same pair by doubling (0.1.10.0): p doubles until n <= p, log2(n) steps in a tail loop, where cap_pair takes n
503# steps of a recursion that is not a tail call, each with a comparison of up to n steps. Its fuel is n, which always
504# suffices (2^n >= n); if it ran out, the answer would be cap_pair's, so the law below needs no arithmetic about how
505# many doublings there are. ok is Nat.is_le(n, p), computed by the caller (match only inspects a parameter); with leb
506# it was n steps in all, 152 ms for n = 786,432 on one GPU lane (0.1.13.0).
507def cap_dbl(f: Nat, ok: Bool, +n: Nat, +d: Nat, +p: Nat) -> Nat & Nat:
508  match f ok:
509    case _ True{}:
510      (d, p)
511    case 0n False{}:
512      cap_pair(n)
513    case 1n+g False{}:
514      cap_dbl(g, Nat.is_le(n, Nat.add(p, p)), n, 1n+d, Nat.add(p, p))
515
516# the depth d such that an Array with 2^d slots holds n elements (the smallest such d)
517def cap_depth(+n: Nat) -> Nat:
518  fst_nat(cap_dbl(n, Nat.is_le(n, 1n), n, 0n, 1n))
519
520# ---- lemmas ----
521
522# a <= b  ->  a <= 1 + b
523def leb_succ(a: Nat, b: Nat, e: {True{} == leb(a, b) : Bool}) -> {True{} == leb(a, 1n+b) : Bool}:
524  match a b:
525    case 0n _:
526      {==}
527    case 1n+p 0n:
528      %e : BD(_, Unit, {True{} == leb(1n+p, 1n) : Bool})
529      Unit{}
530    case 1n+p 1n+q:
531      leb_succ(p, q, e)
532
533# a <= b  ->  a <= c + b
534def leb_up_l(+a: Nat, +b: Nat, c: Nat, +e: {True{} == leb(a, b) : Bool}) -> {True{} == leb(a, Nat.add(c, b)) : Bool}:
535  match c:
536    case 0n:
537      e
538    case 1n++c2:
539      leb_succ(a, Nat.add(c2, b), leb_up_l(a, b, c2, e))
540
541# a <= b  ->  a <= b + c
542def leb_up(a: Nat, b: Nat, c: Nat, e: {True{} == leb(a, b) : Bool}) -> {True{} == leb(a, Nat.add(b, c)) : Bool}:
543  match a b:
544    case 0n _:
545      {==}
546    case 1n+p 0n:
547      %e : BD(_, Unit, {True{} == leb(1n+p, Nat.add(0n, c)) : Bool})
548      Unit{}
549    case 1n+p 1n+q:
550      leb_up(p, q, c, e)
551
552# a <= b  and  c <= d  ->  a + c <= b + d
553def leb_add(a: Nat, b: Nat, c: Nat, d: Nat, e1: {True{} == leb(a, b) : Bool}, e2: {True{} == leb(c, d) : Bool}) -> {True{} == leb(Nat.add(a, c), Nat.add(b, d)) : Bool}:
554  match a b:
555    case 0n _:
556      leb_up_l(c, d, b, e2)
557    case 1n+p 0n:
558      %e1 : BD(_, Unit, {True{} == leb(Nat.add(1n+p, c), Nat.add(0n, d)) : Bool})
559      Unit{}
560    case 1n+p 1n+q:
561      leb_add(p, q, c, d, e1, e2)
562
563# 1 <= 2^d
564def pow2_pos(d: Nat) -> {True{} == leb(1n, pow2(d)) : Bool}:
565  match d:
566    case 0n:
567      {==}
568    case 1n++q:
569      leb_up(1n, pow2(q), pow2(q), pow2_pos(q))
570
571# 1 <= p when p = 2^d
572def pos_p(d: Nat, p: Nat, i1: {p == pow2(d) : Nat}) -> {True{} == leb(1n, p) : Bool}:
573  %Equal.sym(Nat, p, pow2(d), i1) : {True{} == leb(1n, _) : Bool}
574  pow2_pos(d)
575
576# p + p == 2^(1+d) when p == 2^d
577def step_eq(d: Nat, p: Nat, i1: {p == pow2(d) : Nat}) -> {Nat.add(p, p) == pow2(1n+d) : Nat}:
578  %i1 : {Nat.add(p, p) == Nat.add(_, _) : Nat}
579  {==}
580
581# one step: given the invariant for m, the invariant for 1+m holds whichever way the comparison goes
582def step_ok(ok: Bool, +d: Nat, +p: Nat, +m: Nat, +e: {ok == leb(1n+m, p) : Bool}, +i1: {p == pow2(d) : Nat}, +i2: {True{} == leb(m, p) : Bool}) -> {snd_nat(cap_pick(ok, d, p)) == pow2(fst_nat(cap_pick(ok, d, p))) : Nat} & {True{} == leb(1n+m, snd_nat(cap_pick(ok, d, p))) : Bool}:
583  match ok:
584    case True{}:
585      (i1, e)
586    case False{}:
587      (step_eq(d, p, i1), leb_add(1n, p, m, p, pos_p(d, p, i1), i2))
588
589
590# the invariant for cap_step on a pair, opened
591def step_pair_ok(+m: Nat, r: Nat & Nat, i1: {snd_nat(r) == pow2(fst_nat(r)) : Nat}, i2: {True{} == leb(m, snd_nat(r)) : Bool}) -> {snd_nat(cap_step(r, 1n+m)) == pow2(fst_nat(cap_step(r, 1n+m))) : Nat} & {True{} == leb(1n+m, snd_nat(cap_step(r, 1n+m))) : Bool}:
592  match r:
593    case Tuple{+d, +p}:
594      step_ok(leb(1n+m, p), d, p, m, {==}, i1, i2)
595
596def cap_succ(+m: Nat, ih: {snd_nat(cap_pair(m)) == pow2(fst_nat(cap_pair(m))) : Nat} & {True{} == leb(m, snd_nat(cap_pair(m))) : Bool}) -> {snd_nat(cap_pair(1n+m)) == pow2(fst_nat(cap_pair(1n+m))) : Nat} & {True{} == leb(1n+m, snd_nat(cap_pair(1n+m))) : Bool}:
597  match ih:
598    case Tuple{i1, i2}:
599      step_pair_ok(m, cap_pair(m), i1, i2)
600
601# the invariant of cap_pair: the second component is 2^(first) and is >= n
602def cap_inv(n: Nat) -> {snd_nat(cap_pair(n)) == pow2(fst_nat(cap_pair(n))) : Nat} & {True{} == leb(n, snd_nat(cap_pair(n))) : Bool}:
603  match n:
604    case 0n:
605      ({==}, {==})
606    case 1n++m:
607      cap_succ(m, cap_inv(m))
608
609# the invariant of cap_dbl: whichever way it ends (n <= p found, or the fuel out and cap_pair's answer), the second
610# component is 2^(first) and is >= n
611def dbl_inv(f: Nat, ok: Bool, +n: Nat, +d: Nat, +p: Nat, e: {ok == Nat.is_le(n, p) : Bool}, i1: {p == pow2(d) : Nat}) -> {snd_nat(cap_dbl(f, ok, n, d, p)) == pow2(fst_nat(cap_dbl(f, ok, n, d, p))) : Nat} & {True{} == leb(n, snd_nat(cap_dbl(f, ok, n, d, p))) : Bool}:
612  match f ok:
613    case _ True{}:
614      (i1, leb_of(n, p, e))
615    case 0n False{}:
616      cap_inv(n)
617    case 1n+g False{}:
618      dbl_inv(g, Nat.is_le(n, Nat.add(p, p)), n, 1n+d, Nat.add(p, p), {==}, step_eq(d, p, i1))
619
620def dbl_final(n: Nat, pr: {snd_nat(cap_dbl(n, Nat.is_le(n, 1n), n, 0n, 1n)) == pow2(fst_nat(cap_dbl(n, Nat.is_le(n, 1n), n, 0n, 1n))) : Nat} & {True{} == leb(n, snd_nat(cap_dbl(n, Nat.is_le(n, 1n), n, 0n, 1n))) : Bool}) -> {True{} == leb(n, pow2(cap_depth(n))) : Bool}:
621  match pr:
622    case Tuple{i1, i2}:
623      %i1 : {True{} == leb(n, _) : Bool}
624      i2
625
626# LAW (cap_ok): an Array with 2^cap_depth(n) slots always has room for n elements.
627law cap_ok:
628  for +n: Nat
629  {True{} == leb(n, pow2(cap_depth(n))) : Bool}
630
631def cap_ok(n):
632  dbl_final(n, dbl_inv(n, Nat.is_le(n, 1n), n, 0n, 1n, {==}, {==}))
633
634# r x c of zeros, with the exact capacity (smallest power of 2)
635
636def Mat.zeros(+r: Nat, +c: Nat) -> Mat<r, c>:
637  Mat{Array.new(F32, cap_depth(Nat.mul(r, c)), 0.0)}
638
639def Mat.fill(+r: Nat, +c: Nat, +v: F32) -> Mat<r, c>:
640  Mat{Array.new(F32, cap_depth(Nat.mul(r, c)), v)}
641
642def fill_list(xs: List<&2, F32>, +i: U32, a: Array<F32>) -> Array<F32>:
643  match xs:
644    case Nil{}:
645      a
646    case Con{h, t}:
647      fill_list(t, (i + 1 : U32), Array.set(F32, a, i, h))
648
649def len_is(xs: List<&2, F32>, n: Nat) -> Bool:
650  match xs n:
651    case Nil{} 0n:
652      True{}
653    case Con{h, t} 1n+p:
654      len_is(t, p)
655    case _ _:
656      False{}
657
658def Mat.of_list2(-r: Nat, -c: Nat, +rr: Nat, +cc: Nat, ok: Bool, xs: List<&2, F32>) -> Maybe<&1, Mat<r, c>>:
659  match ok:
660    case True{}:
661      Some{Mat{fill_list(xs, 0, Array.new(F32, cap_depth(Nat.mul(rr, cc)), 0.0))}}
662    case False{}:
663      None{}
664
665# Mat<r, c> from r*c numbers in row order; None if the size does not match
666
667def Mat.of_list(+r: Nat, +c: Nat, +xs: List<&2, F32>) -> Maybe<&1, Mat<r, c>>:
668  Mat.of_list2(r, c, r, c, len_is(xs, Nat.mul(r, c)), xs)
669
670# reads the first n numbers, returning the matrix
671
672# Mat<r, c> without checking the list size (a short list leaves the rest at zero, a long one is cut
673# by the capacity): only for when the size is known by construction. Prefer Mat.of_list.
674
675def Mat.from_list(+r: Nat, +c: Nat, xs: List<&2, F32>) -> Mat<r, c>:
676  Mat{fill_list(xs, 0, Array.new(F32, cap_depth(Nat.mul(r, c)), 0.0))}
677
678# writes the numbers of xs into m starting at flat index i (row-major); numbers past the
679# capacity are dropped. For loading a large matrix block by block without building one big list.
680def Mat.fill_at(-r: Nat, -c: Nat, +i: U32, xs: List<&2, F32>, m: Mat<r, c>) -> Mat<r, c>:
681  match m:
682    case Mat{d}:
683      Mat{fill_list(xs, i, d)}
684
685type ML<-r: Nat, -c: Nat> is Type:
686  ML{m: Mat<r, c>, l: List<&2, F32>}
687
688def Mat.to_list2(-r: Nat, -c: Nat, al: AL) -> ML<r, c>:
689  match al:
690    case AL{d, l}:
691      ML{Mat{d}, l}
692
693# two independent copies
694
695def Mat.to_list(+r: Nat, +c: Nat, m: Mat<r, c>) -> ML<r, c>:
696  match m:
697    case Mat{d}:
698      Mat.to_list2(r, c, read_l(Nat.sub(Nat.mul(r, c), 1n), 0, Nil{}, Array.get(F32, d, 0)))
699
700type MM2<-r: Nat, -c: Nat> is Type:
701  MM2{a: Mat<r, c>, b: Mat<r, c>}
702
703def Mat.clone2(-r: Nat, -c: Nat, p: Array<F32> & Array<F32>) -> MM2<r, c>:
704  match p:
705    case Tuple{x, y}:
706      MM2{Mat{x}, Mat{y}}
707
708# ---- products: C (n x m) = A · B, with 2^par blocks of rows in parallel ----
709
710# C = A(n x k) · B(k x m)
711
712def Mat.clone(-r: Nat, -c: Nat, m: Mat<r, c>) -> MM2<r, c>:
713  match m:
714    case Mat{d}:
715      Mat.clone2(r, c, Array.clone(F32, d))
716
717type MMul<-n: Nat, -k: Nat, -m: Nat> is Type:
718  MMul{a: Mat<n, k>, b: Mat<k, m>, c: Mat<n, m>}
719
720# C = A(n x k) · Bᵀ, with B stored m x k
721
722type MMulNT<-n: Nat, -k: Nat, -m: Nat> is Type:
723  MMulNT{a: Mat<n, k>, b: Mat<m, k>, c: Mat<n, m>}
724
725# C = Aᵀ · B, with A stored k x n
726
727type MMulTN<-n: Nat, -k: Nat, -m: Nat> is Type:
728  MMulTN{a: Mat<k, n>, b: Mat<k, m>, c: Mat<n, m>}
729
730# ---------------------------------------------------------------------
731# gemm in tiles of 8 (0.1.8.0): for products with 2 or more rows, A's rows and B's columns are packed once into cells of
732# 8 F32 (a row of k numbers is ceil(k/8) cells, zeros after the last), so the inner loop reads 8 numbers per array
733# access instead of one. The order of the additions is that of `dotr`: product p of a dot goes to partial sum p mod 8
734# (lane p mod 8 of its cell), in order, and the eight sums are combined as dotr combines them after k steps (its
735# rotation depends on k mod 8). The padding adds +0 to a sum that starts at +0, which changes nothing, so the results are
736# the same bits as the scalar loop's. Since 0.1.15.0 a pass over k computes two cells of C (columns j and j + 1), so each
737# A cell is read once for both: with one sum a lane, the two dots' sums are independent chains (23% fewer cycles over
738# SmolLM2's three products on one thread; NOTES.md, 2026-10-25). Products with one row (matrix · vector) keep the scalar loop: packing B would cost as much as the product.
739# ---------------------------------------------------------------------
740
741type T8 is Data:
742  T8{v0: F32, v1: F32, v2: F32, v3: F32, v4: F32, v5: F32, v6: F32, v7: F32}
743
744def t8_zero() -> T8:
745  T8{0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}
746
747def pick0(b: Bool, +v: F32) -> F32:
748  match b:
749    case True{}:
750      v
751    case False{}:
752      0.0
753
754type PK is Type:
755  PK{a: Array<F32>, t: Array<T8>}
756
757# one cell: the 8 numbers of a at idx, idx + st, ..., those at positions q0 + q >= lim (past the end of the line) as 0
758def pk_g7(+v0: F32, +v1: F32, +v2: F32, +v3: F32, +v4: F32, +v5: F32, +v6: F32, +q0: U32, +lim: U32, +idx: U32, +st: U32, +o: U32, t: Array<T8>, r: Array<F32> & F32) -> PK:
759  match r:
760    case Tuple{a, +v}:
761      PK{a, Array.set(T8, t, o, T8{v0, v1, v2, v3, v4, v5, v6, pick0(U32.is_lt((q0 + 7 : U32), lim), v)})}
762
763def pk_g6(+v0: F32, +v1: F32, +v2: F32, +v3: F32, +v4: F32, +v5: F32, +q0: U32, +lim: U32, +idx: U32, +st: U32, +o: U32, t: Array<T8>, r: Array<F32> & F32) -> PK:
764  match r:
765    case Tuple{a, +v}:
766      pk_g7(v0, v1, v2, v3, v4, v5, pick0(U32.is_lt((q0 + 6 : U32), lim), v), q0, lim, idx, st, o, t, Array.get(F32, a, (idx + (st * 7 : U32) : U32)))
767
768def pk_g5(+v0: F32, +v1: F32, +v2: F32, +v3: F32, +v4: F32, +q0: U32, +lim: U32, +idx: U32, +st: U32, +o: U32, t: Array<T8>, r: Array<F32> & F32) -> PK:
769  match r:
770    case Tuple{a, +v}:
771      pk_g6(v0, v1, v2, v3, v4, pick0(U32.is_lt((q0 + 5 : U32), lim), v), q0, lim, idx, st, o, t, Array.get(F32, a, (idx + (st * 6 : U32) : U32)))
772
773def pk_g4(+v0: F32, +v1: F32, +v2: F32, +v3: F32, +q0: U32, +lim: U32, +idx: U32, +st: U32, +o: U32, t: Array<T8>, r: Array<F32> & F32) -> PK:
774  match r:
775    case Tuple{a, +v}:
776      pk_g5(v0, v1, v2, v3, pick0(U32.is_lt((q0 + 4 : U32), lim), v), q0, lim, idx, st, o, t, Array.get(F32, a, (idx + (st * 5 : U32) : U32)))
777
778def pk_g3(+v0: F32, +v1: F32, +v2: F32, +q0: U32, +lim: U32, +idx: U32, +st: U32, +o: U32, t: Array<T8>, r: Array<F32> & F32) -> PK:
779  match r:
780    case Tuple{a, +v}:
781      pk_g4(v0, v1, v2, pick0(U32.is_lt((q0 + 3 : U32), lim), v), q0, lim, idx, st, o, t, Array.get(F32, a, (idx + (st * 4 : U32) : U32)))
782
783def pk_g2(+v0: F32, +v1: F32, +q0: U32, +lim: U32, +idx: U32, +st: U32, +o: U32, t: Array<T8>, r: Array<F32> & F32) -> PK:
784  match r:
785    case Tuple{a, +v}:
786      pk_g3(v0, v1, pick0(U32.is_lt((q0 + 2 : U32), lim), v), q0, lim, idx, st, o, t, Array.get(F32, a, (idx + (st * 3 : U32) : U32)))
787
788def pk_g1(+v0: F32, +q0: U32, +lim: U32, +idx: U32, +st: U32, +o: U32, t: Array<T8>, r: Array<F32> & F32) -> PK:
789  match r:
790    case Tuple{a, +v}:
791      pk_g2(v0, pick0(U32.is_lt((q0 + 1 : U32), lim), v), q0, lim, idx, st, o, t, Array.get(F32, a, (idx + (st * 2 : U32) : U32)))
792
793def pk_g0(+q0: U32, +lim: U32, +idx: U32, +st: U32, +o: U32, t: Array<T8>, r: Array<F32> & F32) -> PK:
794  match r:
795    case Tuple{a, +v}:
796      pk_g1(pick0(U32.is_lt((q0 + 0 : U32), lim), v), q0, lim, idx, st, o, t, Array.get(F32, a, (idx + (st * 1 : U32) : U32)))
797
798# the cells of one line, cell o.. of t; q0 counts the line's numbers before the cell
799def pk_line(tl: Nat, +q0: U32, +lim: U32, +base: U32, +st: U32, +o: U32, r: PK) -> PK:
800  match tl r:
801    case 0n r:
802      r
803    case 1n+q PK{a, t}:
804      pk_line(q, (q0 + 8 : U32), lim, (base + (st * 8 : U32) : U32), st, (o + 1 : U32), pk_g0(q0, lim, base, st, o, t, Array.get(F32, a, base)))
805
806# lines count lines; source line i starts at i * lstep (in a), and goes to cell row o (cells o * tku .. in t)
807def pk_lines(cnt: Nat, +tk: Nat, +tku: U32, +ku: U32, +i: U32, +o: U32, +lstep: U32, +st: U32, r: PK) -> PK:
808  match cnt:
809    case 0n:
810      r
811    case 1n+p:
812      pk_lines(p, tk, tku, ku, (i + 1 : U32), (o + 1 : U32), lstep, st, pk_line(tk, 0, ku, (i * lstep : U32), st, (o * tku : U32), r))
813
814def pk_pack(+cnt: Nat, +tk: Nat, +k: Nat, +lstep: U32, +st: U32, a: Array<F32>) -> PK:
815  pk_lines(cnt, tk, U32.from_nat(tk), U32.from_nat(k), 0, 0, lstep, st, PK{a, Array.new(T8, cap_depth(Nat.mul(cnt, tk)), t8_zero())})
816
817# the dot of two lines of cells: tk cells from ia (in a) and ib (in b); ra, rb hold the arrays and the cells at ia, ib,
818# already read. Product q of a cell goes to sum q, as dotr adds them.
819def t8_dot(tk: Nat, +ia: U32, +ib: U32, s0: F32, s1: F32, s2: F32, s3: F32, s4: F32, s5: F32, s6: F32, s7: F32, ra: Array<T8> & T8, rb: Array<T8> & T8) -> TD:
820  match tk ra rb:
821    case 0n Tuple{a, x} Tuple{b, y}:
822      TD{a, b, s0, s1, s2, s3, s4, s5, s6, s7}
823    case 1n+p Tuple{a, T8{x0, x1, x2, x3, x4, x5, x6, x7}} Tuple{b, T8{y0, y1, y2, y3, y4, y5, y6, y7}}:
824      t8_dot(p, (ia + 1 : U32), (ib + 1 : U32), (s0 + (x0 * y0 : F32) : F32), (s1 + (x1 * y1 : F32) : F32), (s2 + (x2 * y2 : F32) : F32), (s3 + (x3 * y3 : F32) : F32), (s4 + (x4 * y4 : F32) : F32), (s5 + (x5 * y5 : F32) : F32), (s6 + (x6 * y6 : F32) : F32), (s7 + (x7 * y7 : F32) : F32), Array.get(T8, a, (ia + 1 : U32)), Array.get(T8, b, (ib + 1 : U32)))
825
826type TD is Type:
827  TD{a: Array<T8>, b: Array<T8>, s0: F32, s1: F32, s2: F32, s3: F32, s4: F32, s5: F32, s6: F32, s7: F32}
828
829# dotr's final sum after k products: the sums sit rotated by k mod 8 when its loop ends
830def t8_comb(+rot: Nat, +s0: F32, +s1: F32, +s2: F32, +s3: F32, +s4: F32, +s5: F32, +s6: F32, +s7: F32) -> F32:
831  match rot:
832    case 0n:
833      (((s0 + s1 : F32) + (s2 + s3 : F32) : F32) + ((s4 + s5 : F32) + (s6 + s7 : F32) : F32) : F32)
834    case 1n:
835      (((s1 + s2 : F32) + (s3 + s4 : F32) : F32) + ((s5 + s6 : F32) + (s7 + s0 : F32) : F32) : F32)
836    case 2n:
837      (((s2 + s3 : F32) + (s4 + s5 : F32) : F32) + ((s6 + s7 : F32) + (s0 + s1 : F32) : F32) : F32)
838    case 3n:
839      (((s3 + s4 : F32) + (s5 + s6 : F32) : F32) + ((s7 + s0 : F32) + (s1 + s2 : F32) : F32) : F32)
840    case 4n:
841      (((s4 + s5 : F32) + (s6 + s7 : F32) : F32) + ((s0 + s1 : F32) + (s2 + s3 : F32) : F32) : F32)
842    case 5n:
843      (((s5 + s6 : F32) + (s7 + s0 : F32) : F32) + ((s1 + s2 : F32) + (s3 + s4 : F32) : F32) : F32)
844    case 6n:
845      (((s6 + s7 : F32) + (s0 + s1 : F32) : F32) + ((s2 + s3 : F32) + (s4 + s5 : F32) : F32) : F32)
846    case _:
847      (((s7 + s0 : F32) + (s1 + s2 : F32) : F32) + ((s3 + s4 : F32) + (s5 + s6 : F32) : F32) : F32)
848
849type TR is Type:
850  TR{a: Array<T8>, b: Array<T8>, x: F32}
851
852def t8_fin(+rot: Nat, d: TD) -> TR:
853  match d:
854    case TD{a, b, +s0, +s1, +s2, +s3, +s4, +s5, +s6, +s7}:
855      TR{a, b, t8_comb(rot, s0, s1, s2, s3, s4, s5, s6, s7)}
856
857def t8_cell(+tk: Nat, +tku: U32, +rot: Nat, +i: U32, +j: U32, a: Array<T8>, b: Array<T8>) -> TR:
858  t8_fin(rot, t8_dot(tk, (i * tku : U32), (j * tku : U32), 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, Array.get(T8, a, (i * tku : U32)), Array.get(T8, b, (j * tku : U32))))
859
860# two cells of C at once: row i against columns j and j + 1 (cells ib and ic of b), each A cell read once for both; each
861# dot keeps t8_dot's order, so the two cells are t8_cell's bits
862type B2 is Type:
863  B2{b: Array<T8>, y: T8, z: T8}
864
865def b2_mk(y: T8, r: Array<T8> & T8) -> B2:
866  match r:
867    case Tuple{b, z}:
868      B2{b, y, z}
869
870def b2_get(+i2: U32, r: Array<T8> & T8) -> B2:
871  match r:
872    case Tuple{b, y}:
873      b2_mk(y, Array.get(T8, b, i2))
874
875def t8_get2(b: Array<T8>, +i1: U32, +i2: U32) -> B2:
876  b2_get(i2, Array.get(T8, b, i1))
877
878type TD2 is Type:
879  TD2{a: Array<T8>, b: Array<T8>, s0: F32, s1: F32, s2: F32, s3: F32, s4: F32, s5: F32, s6: F32, s7: F32, u0: F32, u1: F32, u2: F32, u3: F32, u4: F32, u5: F32, u6: F32, u7: F32}
880
881def t8_dot2(tk: Nat, +ia: U32, +ib: U32, +ic: U32, s0: F32, s1: F32, s2: F32, s3: F32, s4: F32, s5: F32, s6: F32, s7: F32, u0: F32, u1: F32, u2: F32, u3: F32, u4: F32, u5: F32, u6: F32, u7: F32, ra: Array<T8> & T8, rb: B2) -> TD2:
882  match tk ra rb:
883    case 0n Tuple{a, x} B2{b, y, z}:
884      TD2{a, b, s0, s1, s2, s3, s4, s5, s6, s7, u0, u1, u2, u3, u4, u5, u6, u7}
885    case 1n+p Tuple{a, T8{+x0, +x1, +x2, +x3, +x4, +x5, +x6, +x7}} B2{b, T8{y0, y1, y2, y3, y4, y5, y6, y7}, T8{z0, z1, z2, z3, z4, z5, z6, z7}}:
886      t8_dot2(p, (ia + 1 : U32), (ib + 1 : U32), (ic + 1 : U32), (s0 + (x0 * y0 : F32) : F32), (s1 + (x1 * y1 : F32) : F32), (s2 + (x2 * y2 : F32) : F32), (s3 + (x3 * y3 : F32) : F32), (s4 + (x4 * y4 : F32) : F32), (s5 + (x5 * y5 : F32) : F32), (s6 + (x6 * y6 : F32) : F32), (s7 + (x7 * y7 : F32) : F32), (u0 + (x0 * z0 : F32) : F32), (u1 + (x1 * z1 : F32) : F32), (u2 + (x2 * z2 : F32) : F32), (u3 + (x3 * z3 : F32) : F32), (u4 + (x4 * z4 : F32) : F32), (u5 + (x5 * z5 : F32) : F32), (u6 + (x6 * z6 : F32) : F32), (u7 + (x7 * z7 : F32) : F32), Array.get(T8, a, (ia + 1 : U32)), t8_get2(b, (ib + 1 : U32), (ic + 1 : U32)))
887
888type TR2 is Type:
889  TR2{a: Array<T8>, b: Array<T8>, x: F32, y: F32}
890
891def t8_fin2(+rot: Nat, d: TD2) -> TR2:
892  match d:
893    case TD2{a, b, +s0, +s1, +s2, +s3, +s4, +s5, +s6, +s7, +u0, +u1, +u2, +u3, +u4, +u5, +u6, +u7}:
894      TR2{a, b, t8_comb(rot, s0, s1, s2, s3, s4, s5, s6, s7), t8_comb(rot, u0, u1, u2, u3, u4, u5, u6, u7)}
895
896def t8_cell2(+tk: Nat, +tku: U32, +rot: Nat, +i: U32, +j: U32, a: Array<T8>, b: Array<T8>) -> TR2:
897  t8_fin2(rot, t8_dot2(tk, (i * tku : U32), (j * tku : U32), ((j + 1 : U32) * tku : U32), 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, Array.get(T8, a, (i * tku : U32)), t8_get2(b, (j * tku : U32), ((j + 1 : U32) * tku : U32))))
898
899type TG is Type:
900  TG{a: Array<T8>, b: Array<T8>, c: Array<F32>}
901
902def t8_set(+ac: Bool, +idx: U32, c: Array<F32>, r: TR) -> TG:
903  match r:
904    case TR{a, b, +x}:
905      TG{a, b, cwr(ac, idx, x, c)}
906
907def t8_set2(+ac: Bool, +idx: U32, c: Array<F32>, r: TR2) -> TG:
908  match r:
909    case TR2{a, b, +x, +y}:
910      TG{a, b, cwr(ac, (idx + 1 : U32), y, cwr(ac, idx, x, c))}
911
912# cells j.. of row i, two at a time, the last one alone when mleft is odd
913def t8_cols(+ac: Bool, mleft: Nat, +tk: Nat, +tku: U32, +rot: Nat, +i: U32, +j: U32, +cidx: U32, g: TG) -> TG:
914  match mleft g:
915    case 0n g:
916      g
917    case 1n TG{a, b, c}:
918      t8_set(ac, cidx, c, t8_cell(tk, tku, rot, i, j, a, b))
919    case 2n+p TG{a, b, c}:
920      t8_cols(ac, p, tk, tku, rot, i, (j + 2 : U32), (cidx + 2 : U32), t8_set2(ac, cidx, c, t8_cell2(tk, tku, rot, i, j, a, b)))
921
922def t8_rows(+ac: Bool, nleft: Nat, +m: Nat, +tk: Nat, +tku: U32, +rot: Nat, +i: U32, +mu: U32, g: TG) -> TG:
923  match nleft:
924    case 0n:
925      g
926    case 1n+p:
927      t8_rows(ac, p, m, tk, tku, rot, (i + 1 : U32), mu, t8_cols(ac, m, tk, tku, rot, i, 0, (i * mu : U32), g))
928
929# rows i0 .. i0+cnt-1 of C as a list, in order (the parallel version's leaves)
930type TL is Type:
931  TL{a: Array<T8>, b: Array<T8>, out: List<&2, F32>}
932
933def tl_one(out: List<&2, F32>, r: TR) -> TL:
934  match r:
935    case TR{a, b, +x}:
936      TL{a, b, x <> out}
937
938def tl_two(out: List<&2, F32>, r: TR2) -> TL:
939  match r:
940    case TR2{a, b, +x, +y}:
941      TL{a, b, y <> (x <> out)}
942
943# cells j.. of row i, mleft of them, onto out (newest first), two at a time as t8_cols
944def t8_lcols(mleft: Nat, +tk: Nat, +tku: U32, +rot: Nat, +i: U32, +j: U32, l: TL) -> TL:
945  match mleft l:
946    case 0n l:
947      l
948    case 1n TL{a, b, out}:
949      tl_one(out, t8_cell(tk, tku, rot, i, j, a, b))
950    case 2n+p TL{a, b, out}:
951      t8_lcols(p, tk, tku, rot, i, (j + 2 : U32), tl_two(out, t8_cell2(tk, tku, rot, i, j, a, b)))
952
953def t8_lrows(nleft: Nat, +m: Nat, +tk: Nat, +tku: U32, +rot: Nat, +i: U32, l: TL) -> TL:
954  match nleft:
955    case 0n:
956      l
957    case 1n+p:
958      t8_lrows(p, m, tk, tku, rot, (i + 1 : U32), t8_lcols(m, tk, tku, rot, i, 0, l))
959
960def t8_lfin(l: TL) -> List<&2, F32>:
961  match l:
962    case TL{a, b, out}:
963      List.reverse(&2, F32, out)
964
965type PKK is Type:
966  PKK{a: Array<F32>, b: Array<F32>, ta: Array<T8>, tb: Array<T8>}
967
968def t8_pk2(pa: PK, pb: PK) -> PKK:
969  match pa pb:
970    case PK{a, ta} PK{b, tb}:
971      PKK{a, b, ta, tb}
972
973def t8_g(a: Array<F32>, b: Array<F32>, g: TG) -> G:
974  match g:
975    case TG{ta, tb, c}:
976      G{a, b, c}
977
978def t8_out_seq(+ac: Bool, +n: Nat, +m: Nat, +tk: Nat, +tku: U32, +rot: Nat, c: Array<F32>, p: PKK) -> G:
979  match p:
980    case PKK{a, b, ta, tb}:
981      t8_g(a, b, t8_rows(ac, n, m, tk, tku, rot, 0, U32.from_nat(m), TG{ta, tb, c}))
982
983# ---- the parallel path (0.1.9.0): the larger operand is packed into one piece per block, so a block owns its piece;
984# the other operand is copied once per node of the fork tree (2^d - 1 copies), instead of both at every node ----
985
986type PT is Type:
987  PLeaf{t: Array<T8>}
988  PNode{l: PT, r: PT}
989
990type PKT is Type:
991  PKT{a: Array<F32>, t: PT}
992
993def pkt_leaf(r: PK) -> PKT:
994  match r:
995    case PK{a, t}:
996      PKT{a, PLeaf{t}}
997
998def pkt_then(r: PKT, k: Array<F32> -> PT -> PKT) -> PKT:
999  match r:
1000    case PKT{a, t}:
1001      k(a, t)
1002
1003# lines i .. i+cnt-1 of a, packed into 2^d pieces (halves of the line count, as the fork tree splits them)
1004def pk_tree(+d: Nat, +cnt: Nat, +i: U32, +tk: Nat, +tku: U32, +ku: U32, +lstep: U32, +st: U32, a: Array<F32>) -> PKT:
1005  match d:
1006    case 0n:
1007      pkt_leaf(pk_lines(cnt, tk, tku, ku, i, 0, lstep, st, PK{a, Array.new(T8, cap_depth(Nat.mul(cnt, tk)), t8_zero())}))
1008    case 1n+q:
1009      pkt_then(pk_tree(q, Nat.div(cnt, 2n), i, tk, tku, ku, lstep, st, a), a2 => l => pkt_then(pk_tree(q, Nat.sub(cnt, Nat.div(cnt, 2n)), (i + U32.from_nat(Nat.div(cnt, 2n)) : U32), tk, tku, ku, lstep, st, a2), a3 => r => PKT{a3, PNode{l, r}}))
1010
1011def t8_with2(-R: Type, p: Array<T8> & Array<T8>, k: Array<T8> -> Array<T8> -> R) -> R:
1012  match p:
1013    case Tuple{x, y}:
1014      k(x, y)
1015
1016# split by rows of C: the pieces are A's rows, B is shared; the result is C's rows in order
1017def t8_rpar(t: PT, +cnt: Nat, +m: Nat, +tk: Nat, +tku: U32, +rot: Nat, b: Array<T8>) -> List<&2, F32>:
1018  match t:
1019    case PLeaf{a}:
1020      t8_lfin(t8_lrows(cnt, m, tk, tku, rot, 0, TL{a, b, Nil{}}))
1021    case PNode{l, r}:
1022      t8_with2(List<&2, F32>, Array.clone(T8, b), b1 => b2 => (
1023        x y = t8_rpar(l, Nat.div(cnt, 2n), m, tk, tku, rot, b1) t8_rpar(r, Nat.sub(cnt, Nat.div(cnt, 2n)), m, tk, tku, rot, b2)
1024        List.append(&2, F32, x, y)))
1025
1026# a block of C's columns j0 .. j0+w-1 for every row, row by row
1027type CB is Data:
1028  CB{j0: U32, w: Nat, xs: List<&2, F32>}
1029
1030# split by columns of C: the pieces are B's columns, A is shared
1031def t8_cpar(t: PT, +cnt: Nat, +j0: U32, +n: Nat, +tk: Nat, +tku: U32, +rot: Nat, a: Array<T8>) -> List<&2, CB>:
1032  match t:
1033    case PLeaf{b}:
1034      [CB{j0, cnt, t8_lfin(t8_lrows(n, cnt, tk, tku, rot, 0, TL{a, b, Nil{}}))}]
1035    case PNode{l, r}:
1036      t8_with2(List<&2, CB>, Array.clone(T8, a), a1 => a2 => (
1037        x y = t8_cpar(l, Nat.div(cnt, 2n), j0, n, tk, tku, rot, a1) t8_cpar(r, Nat.sub(cnt, Nat.div(cnt, 2n)), (j0 + U32.from_nat(Nat.div(cnt, 2n)) : U32), n, tk, tku, rot, a2)
1038        List.append(&2, CB, x, y)))
1039
1040type RW is Type:
1041  RW{c: Array<F32>, xs: List<&2, F32>}
1042
1043# w numbers of xs into c from idx; what is left of xs comes back
1044def cb_row(+ac: Bool, w: Nat, +idx: U32, c: Array<F32>, xs: List<&2, F32>) -> RW:
1045  match w xs:
1046    case 1n+p Con{+h, t}:
1047      cb_row(ac, p, (idx + 1 : U32), cwr(ac, idx, h, c), t)
1048    case _ _:
1049      RW{c, xs}
1050
1051def cb_rows(+ac: Bool, n: Nat, +i: U32, +j0: U32, +w: Nat, +mu: U32, r: RW) -> Array<F32>:
1052  match n r:
1053    case 0n RW{c, xs}:
1054      c
1055    case 1n+p RW{c, xs}:
1056      cb_rows(ac, p, (i + 1 : U32), j0, w, mu, cb_row(ac, w, ((i * mu : U32) + j0 : U32), c, xs))
1057
1058def cb_write(+ac: Bool, bs: List<&2, CB>, +n: Nat, +mu: U32, c: Array<F32>) -> Array<F32>:
1059  match bs:
1060    case Nil{}:
1061      c
1062    case Con{CB{+j0, +w, xs}, t}:
1063      cb_write(ac, t, n, mu, cb_rows(ac, n, 0, j0, w, mu, RW{c, xs}))
1064
1065def t8_pr2(+ac: Bool, +n: Nat, +m: Nat, +tk: Nat, +tku: U32, +rot: Nat, c: Array<F32>, ra: PKT, rb: PK) -> G:
1066  match ra rb:
1067    case PKT{a, t} PK{b, tb}:
1068      G{a, b, write_la(ac, t8_rpar(t, n, m, tk, tku, rot, tb), 0, c)}
1069
1070def t8_pc2(+ac: Bool, +n: Nat, +m: Nat, +tk: Nat, +tku: U32, +rot: Nat, c: Array<F32>, ra: PK, rb: PKT) -> G:
1071  match ra rb:
1072    case PK{a, ta} PKT{b, t}:
1073      G{a, b, cb_write(ac, t8_cpar(t, m, 0, n, tk, tku, rot, ta), n, U32.from_nat(m), c)}
1074
1075# 2^d blocks along the larger of n (rows of C, pieces of A) and m (columns of C, pieces of B)
1076def t8_par2(+ac: Bool, byrows: Bool, d: Nat, +n: Nat, +m: Nat, +k: Nat, +tk: Nat, +tku: U32, +rot: Nat, +arow: U32, +sa: U32, +sb: U32, +bcol: U32, a: Array<F32>, b: Array<F32>, c: Array<F32>) -> G:
1077  match byrows:
1078    case True{}:
1079      t8_pr2(ac, n, m, tk, tku, rot, c, pk_tree(d, n, 0, tk, tku, U32.from_nat(k), arow, sa, a), pk_pack(m, tk, k, bcol, sb, b))
1080    case False{}:
1081      t8_pc2(ac, n, m, tk, tku, rot, c, pk_pack(n, tk, k, arow, sa, a), pk_tree(d, m, 0, tk, tku, U32.from_nat(k), bcol, sb, b))
1082
1083def gemm_t8b(+ac: Bool, d: Nat, +n: Nat, +m: Nat, +k: Nat, +tk: Nat, +tku: U32, +rot: Nat, +arow: U32, +sa: U32, +sb: U32, +bcol: U32, a: Array<F32>, b: Array<F32>, c: Array<F32>) -> G:
1084  match d:
1085    case 0n:
1086      t8_out_seq(ac, n, m, tk, tku, rot, c, t8_pk2(pk_pack(n, tk, k, arow, sa, a), pk_pack(m, tk, k, bcol, sb, b)))
1087    case 1n+q:
1088      t8_par2(ac, Nat.is_le(m, n), 1n+q, n, m, k, tk, tku, rot, arow, sa, sb, bcol, a, b, c)
1089
1090# C (n x m) = A · B (or C + A · B when ac) in cells of 8, sequential (d = 0) or with 2^d blocks of rows in parallel; A
1091# and B come back intact
1092def gemm_t8_ac(+ac: Bool, d: Nat, +n: Nat, +m: Nat, +k: Nat, +arow: U32, +sa: U32, +sb: U32, +bcol: U32, a: Array<F32>, b: Array<F32>, c: Array<F32>) -> G:
1093  gemm_t8b(ac, d, n, m, k, Nat.div(Nat.add(k, 7n), 8n), U32.from_nat(Nat.div(Nat.add(k, 7n), 8n)), Nat.mod(k, 8n), arow, sa, sb, bcol, a, b, c)
1094
1095def gemm_t8(d: Nat, +n: Nat, +m: Nat, +k: Nat, +arow: U32, +sa: U32, +sb: U32, +bcol: U32, a: Array<F32>, b: Array<F32>, c: Array<F32>) -> G:
1096  gemm_t8_ac(False{}, d, n, m, k, arow, sa, sb, bcol, a, b, c)
1097
1098# one row (matrix · vector): the scalar loop at every depth; two or more rows: cells of 8. Up to 0.1.11.0 one row in
1099# parallel ran gemm_par_cols, which clones both operands at every node of the fork tree: a 50,257 x 768 weight matrix at
1100# depth 4 ran out of the default heap (5.1 GB) and 8192 x 2048 took 124 ms against 10 ms sequentially. Packing the
1101# matrix into cells of 8 instead costs more than the product, which reads each weight once (64 ms against 24 at
1102# 50,257 x 768). Matrix · vector in parallel is Bands.matvec. Measured 2026-10-16 (NOTES.md).
1103def gemm_any3(+ac: Bool, one: Bool, d: Nat, +n: Nat, +m: Nat, +k: Nat, +arow: U32, +sa: U32, +sb: U32, +bcol: U32, a: Array<F32>, b: Array<F32>, c: Array<F32>) -> G:
1104  match one:
1105    case True{}:
1106      gemm_ac(ac, n, m, k, arow, sa, sb, bcol, a, b, c)
1107    case False{}:
1108      gemm_t8_ac(ac, d, n, m, k, arow, sa, sb, bcol, a, b, c)
1109
1110def gemm_any2(+ac: Bool, d: Nat, +n: Nat, +m: Nat, +k: Nat, +arow: U32, +sa: U32, +sb: U32, +bcol: U32, a: Array<F32>, b: Array<F32>, c: Array<F32>) -> G:
1111  gemm_any3(ac, Nat.is_le(n, 1n), d, n, m, k, arow, sa, sb, bcol, a, b, c)
1112
1113def gemm_any(+ac: Bool, d: Nat, +n: Nat, +m: Nat, +k: Nat, +arow: U32, +sa: U32, +sb: U32, +bcol: U32, a: Array<F32>, b: Array<F32>, c: Array<F32>) -> G:
1114  gemm_any2(ac, d, n, m, k, arow, sa, sb, bcol, a, b, c)
1115
1116def mmul_fin(-n: Nat, -k: Nat, -m: Nat, g: G) -> MMul<n, k, m>:
1117  match g:
1118    case G{a, b, c}:
1119      MMul{Mat{a}, Mat{b}, Mat{c}}
1120
1121def mmul_nt_fin(-n: Nat, -k: Nat, -m: Nat, g: G) -> MMulNT<n, k, m>:
1122  match g:
1123    case G{a, b, c}:
1124      MMulNT{Mat{a}, Mat{b}, Mat{c}}
1125
1126def mmul_tn_fin(-n: Nat, -k: Nat, -m: Nat, g: G) -> MMulTN<n, k, m>:
1127  match g:
1128    case G{a, b, c}:
1129      MMulTN{Mat{a}, Mat{b}, Mat{c}}
1130
1131# ---- the split depth of a product (0.1.11.0): par is an upper bound, and a product too small to gain splits 0 times ----
1132#
1133# A product of n x k by k x m splits into 2^par blocks only when it does at least 2^23 multiply-adds and each cell of C
1134# sums at least 64 of them; otherwise it runs sequentially. Measured on 16 threads (2026-10-16, 18 shapes): below 2^23 the
1135# split costs more than it saves, and with a short inner dimension (a weight gradient over a few tokens) every split
1136# was 2-3.6x slower, because each block hands its rows of C back as a list that is then written into C, a cost per cell
1137# of C that k multiply-adds per cell do not pay for. The result does not depend on the depth: the same bits at every
1138# depth (tests/t8.bend, tests/acc.bend, tests/split.bend).
1139def split_pick(big: Bool, d: Nat) -> Nat:
1140  match big:
1141    case True{}:
1142      d
1143    case False{}:
1144      0n
1145
1146def split_depth(d: Nat, +n: Nat, +m: Nat, +k: Nat) -> Nat:
1147  split_pick(Bool.and(Nat.is_le(64n, k), Nat.is_le(8388608n, Nat.mul(Nat.mul(n, m), k))), d)
1148
1149def mm_a(+n: Nat, +k: Nat, +m: Nat, d: Nat, a: Array<F32>, b: Array<F32>) -> MMul<n, k, m>:
1150  mmul_fin(n, k, m, gemm_any(False{}, d, n, m, k, U32.from_nat(k), 1, U32.from_nat(m), 1, a, b, Array.new(F32, cap_depth(Nat.mul(n, m)), 0.0)))
1151
1152# C = A · B   (n x k) · (k x m); par = log2 of the most parallel blocks (0 = sequential); split_depth picks 0 or par
1153
1154def Mat.matmul(+n: Nat, +k: Nat, +m: Nat, par: Nat, a: Mat<n, k>, b: Mat<k, m>) -> MMul<n, k, m>:
1155  match a b:
1156    case Mat{x} Mat{y}:
1157      mm_a(n, k, m, split_depth(par, n, m, k), x, y)
1158
1159# C + A · B, into C (n x m); A and B come back with the result, as Mat.matmul returns them. Each cell of A · B is
1160# computed as Mat.matmul computes it and added once to C's cell: the same bits as Mat.add(c, Mat.matmul(a, b)'s c),
1161# without allocating the product or walking it a second time.
1162
1163def Mat.matmul_acc(+n: Nat, +k: Nat, +m: Nat, par: Nat, a: Mat<n, k>, b: Mat<k, m>, c: Mat<n, m>) -> MMul<n, k, m>:
1164  match a b c:
1165    case Mat{x} Mat{y} Mat{z}:
1166      mmul_fin(n, k, m, gemm_any(True{}, split_depth(par, n, m, k), n, m, k, U32.from_nat(k), 1, U32.from_nat(m), 1, x, y, z))
1167
1168def mm_nt(+n: Nat, +k: Nat, +m: Nat, d: Nat, a: Array<F32>, b: Array<F32>) -> MMulNT<n, k, m>:
1169  mmul_nt_fin(n, k, m, gemm_any(False{}, d, n, m, k, U32.from_nat(k), 1, 1, U32.from_nat(k), a, b, Array.new(F32, cap_depth(Nat.mul(n, m)), 0.0)))
1170
1171# C = A · Bᵀ   (n x k) · (m x k)ᵀ: for weights stored with one row per output
1172
1173def Mat.matmul_nt(+n: Nat, +k: Nat, +m: Nat, par: Nat, a: Mat<n, k>, b: Mat<m, k>) -> MMulNT<n, k, m>:
1174  match a b:
1175    case Mat{x} Mat{y}:
1176      mm_nt(n, k, m, split_depth(par, n, m, k), x, y)
1177
1178# C + A · Bᵀ, into C (n x m)
1179
1180def Mat.matmul_nt_acc(+n: Nat, +k: Nat, +m: Nat, par: Nat, a: Mat<n, k>, b: Mat<m, k>, c: Mat<n, m>) -> MMulNT<n, k, m>:
1181  match a b c:
1182    case Mat{x} Mat{y} Mat{z}:
1183      mmul_nt_fin(n, k, m, gemm_any(True{}, split_depth(par, n, m, k), n, m, k, U32.from_nat(k), 1, 1, U32.from_nat(k), x, y, z))
1184
1185def mm_tn(+n: Nat, +k: Nat, +m: Nat, d: Nat, a: Array<F32>, b: Array<F32>) -> MMulTN<n, k, m>:
1186  mmul_tn_fin(n, k, m, gemm_any(False{}, d, n, m, k, 1, U32.from_nat(n), U32.from_nat(m), 1, a, b, Array.new(F32, cap_depth(Nat.mul(n, m)), 0.0)))
1187
1188# C = Aᵀ · B   (k x n)ᵀ · (k x m): weight gradient, X^T · dY
1189
1190def Mat.matmul_tn(+n: Nat, +k: Nat, +m: Nat, par: Nat, a: Mat<k, n>, b: Mat<k, m>) -> MMulTN<n, k, m>:
1191  match a b:
1192    case Mat{x} Mat{y}:
1193      mm_tn(n, k, m, split_depth(par, n, m, k), x, y)
1194
1195# C + Aᵀ · B, into C (n x m): a weight gradient added into the gradient it accumulates, dW += Xᵀ · dY
1196
1197def Mat.matmul_tn_acc(+n: Nat, +k: Nat, +m: Nat, par: Nat, a: Mat<k, n>, b: Mat<k, m>, c: Mat<n, m>) -> MMulTN<n, k, m>:
1198  match a b c:
1199    case Mat{x} Mat{y} Mat{z}:
1200      mmul_tn_fin(n, k, m, gemm_any(True{}, split_depth(par, n, m, k), n, m, k, 1, U32.from_nat(n), U32.from_nat(m), 1, x, y, z))
1201
1202# ---- a precise product for long dots (0.1.14.0) ----
1203#
1204# Mat.matmul_precise is Mat.matmul with k summed in blocks of 1024 (the last one 1..1024): block b's columns of A and
1205# rows of B are copied out, the first block's product written into C, each next one's added to it as matmul_acc adds,
1206# so every cell is ((x0 + x1) + x2) .., x_b the block's dot with the usual four partial sums. A dot of 49,152 products
1207# as four running sums of 12,288 lost the small terms (SmolLM2's head: its gradient 1.8x further from float64 than
1208# PyTorch's float32, 0.03x in blocks; BendTorch NOTES, 2026-10-18). Opt-in: it copies the slices and runs one product
1209# a block, and the default products keep their speed and bits. Up to 1024 steps it is Mat.matmul itself.
1210
1211type SL is Type:
1212  SL{a: Array<F32>, s: Array<F32>}
1213
1214# d[di..] = src[si..], k numbers; r holds src[si], already read
1215def sl_row(k: Nat, +si: U32, +di: U32, d: Array<F32>, r: Array<F32> & F32) -> SL:
1216  match k r:
1217    case 0n Tuple{a, x}: SL{a, d}
1218    case 1n+p Tuple{a, +x}: sl_row(p, (si + 1 : U32), (di + 1 : U32), Array.set(F32, d, di, x), Array.get(F32, a, (si + 1 : U32)))
1219
1220# rows i.. of a slice: w numbers a row from src[i*st + o], into d[i*w]
1221def sl_rows(r: Nat, +i: U32, +w: Nat, +w32: U32, +st: U32, +o: U32, sl: SL) -> SL:
1222  match r sl:
1223    case 0n sl: sl
1224    case 1n+p SL{a, d}: sl_rows(p, (i + 1 : U32), w, w32, st, o, sl_row(w, ((i * st : U32) + o : U32), (i * w32 : U32), d, Array.get(F32, a, ((i * st : U32) + o : U32))))
1225
1226def blk_len(+bi: U32, +k: U32) -> Nat:
1227  U32.to_nat(Bool.pick(U32, U32.is_lt((k - (bi * 1024 : U32) : U32), 1024), (k - (bi * 1024 : U32) : U32), 1024))
1228
1229def mp_s4(a: Array<F32>, b: Array<F32>, g: G) -> G:
1230  match g:
1231    case G{x, y, c}: G{a, b, c}
1232
1233def mp_s3(+ac: Bool, +par: Nat, +n: Nat, +m: Nat, +w: Nat, +w32: U32, +m32: U32, a: Array<F32>, as: Array<F32>, c: Array<F32>, sb: SL) -> G:
1234  match sb:
1235    case SL{b, bs}: mp_s4(a, b, gemm_any(ac, split_depth(par, n, m, w), n, m, w, w32, 1, m32, 1, as, bs, c))
1236
1237def mp_s2(+ac: Bool, +par: Nat, +n: Nat, +m: Nat, +w: Nat, +w32: U32, +m32: U32, +o: U32, c: Array<F32>, b: Array<F32>, sa: SL) -> G:
1238  match sa:
1239    case SL{a, as}: mp_s3(ac, par, n, m, w, w32, m32, a, as, c, sl_row(Nat.mul(w, m), (o * m32 : U32), 0, Array.new(F32, cap_depth(Nat.mul(w, m)), 0.0), Array.get(F32, b, (o * m32 : U32))))
1240
1241# block bi: A's columns o..o+w (A is n x k) and B's rows o..o+w (B is k x m), multiplied into C (added when ac)
1242def mp_step(+ac: Bool, +par: Nat, +n: Nat, +m: Nat, +k32: U32, +bi: U32, a: Array<F32>, b: Array<F32>, c: Array<F32>) -> G:
1243  mp_s2(ac, par, n, m, blk_len(bi, k32), U32.from_nat(blk_len(bi, k32)), U32.from_nat(m), (bi * 1024 : U32), c, b, sl_rows(n, 0, blk_len(bi, k32), U32.from_nat(blk_len(bi, k32)), k32, (bi * 1024 : U32), SL{a, Array.new(F32, cap_depth(Nat.mul(n, blk_len(bi, k32))), 0.0)}))
1244
1245def mp_go(nb: Nat, +bi: U32, +par: Nat, +n: Nat, +m: Nat, +k32: U32, g: G) -> G:
1246  match nb g:
1247    case 0n g: g
1248    case 1n++q G{a, b, c}: mp_go(q, (bi + 1 : U32), par, n, m, k32, mp_step(U32.is_lt(0, bi), par, n, m, k32, bi, a, b, c))
1249
1250def mm_precise2(long: Bool, +n: Nat, +k: Nat, +m: Nat, par: Nat, a: Mat<n, k>, b: Mat<k, m>) -> MMul<n, k, m>:
1251  match long a b:
1252    case True{} Mat{x} Mat{y}: mmul_fin(n, k, m, mp_go(U32.to_nat(U32.div((U32.from_nat(k) + 1023 : U32), 1024)), 0, par, n, m, U32.from_nat(k), G{x, y, Array.new(F32, cap_depth(Nat.mul(n, m)), 0.0)}))
1253    case False{} a b: Mat.matmul(n, k, m, par, a, b)
1254
1255# C = A · B (n x k) · (k x m), k summed in blocks of 1024 (above); opt-in
1256def Mat.matmul_precise(+n: Nat, +k: Nat, +m: Nat, +par: Nat, a: Mat<n, k>, b: Mat<k, m>) -> MMul<n, k, m>:
1257  mm_precise2(Nat.is_lt(1024n, k), n, k, m, par, a, b)
1258
1259# ---- element-wise and training operations ----
1260
1261# adds the bias (one row, m numbers) to each row of C (n x m)
1262
1263type MBias<-n: Nat, -m: Nat> is Type:
1264  MBias{c: Mat<n, m>, b: Mat<1n, m>}
1265
1266def add_row2(-n: Nat, -m: Nat, +nn: Nat, +mm: Nat, cd: Array<F32>, al: AL) -> MBias<n, m>:
1267  match al:
1268    case AL{bd, bl}:
1269      MBias{Mat{bias_rows(nn, 0, U32.from_nat(mm), bl, cd)}, Mat{bd}}
1270
1271def Mat.add_row(+n: Nat, +m: Nat, c: Mat<n, m>, b: Mat<1n, m>) -> MBias<n, m>:
1272  match c b:
1273    case Mat{cd} Mat{bd}:
1274      add_row2(n, m, n, m, cd, read_l(Nat.sub(m, 1n), 0, Nil{}, Array.get(F32, bd, 0)))
1275
1276# pair of matrices of the same shape (the result of an operation that reads two operands)
1277
1278type MPair<-r: Nat, -c: Nat> is Type:
1279  MPair{a: Mat<r, c>, b: Mat<r, c>}
1280
1281def mpair_fin(-r: Nat, -c: Nat, p: P2) -> MPair<r, c>:
1282  match p:
1283    case P2{a, b}:
1284      MPair{Mat{a}, Mat{b}}
1285
1286def relu_fin(-r: Nat, -c: Nat, d: Array<F32>) -> Mat<r, c>:
1287  Mat{d}
1288
1289def Mat.relu(+r: Nat, +c: Nat, a: Mat<r, c>) -> Mat<r, c>:
1290  match a:
1291    case Mat{d}:
1292      relu_fin(r, c, relu_l(Nat.mul(r, c), 0, Array.get(F32, d, 0)))
1293
1294# (dy * (h > 0), h): the gradient that passes through the relu
1295
1296def Mat.relu_bwd(+r: Nat, +c: Nat, dy: Mat<r, c>, h: Mat<r, c>) -> MPair<r, c>:
1297  match dy h:
1298    case Mat{d} Mat{x}:
1299      mpair_fin(r, c, mask_l(Nat.mul(r, c), 0, Array.get(F32, d, 0), Array.get(F32, x, 0)))
1300
1301# (w - lr * dw, dw): one SGD step
1302
1303def Mat.sgd(+r: Nat, +c: Nat, +lr: F32, w: Mat<r, c>, dw: Mat<r, c>) -> MPair<r, c>:
1304  match w dw:
1305    case Mat{x} Mat{y}:
1306      mpair_fin(r, c, sgd_l(Nat.mul(r, c), 0, lr, Array.get(F32, x, 0), Array.get(F32, y, 0)))
1307
1308# (a + b, b): element-wise sum (a - (-1)*b is exactly a + b)
1309
1310def Mat.add(+r: Nat, +c: Nat, a: Mat<r, c>, b: Mat<r, c>) -> MPair<r, c>:
1311  match a b:
1312    case Mat{x} Mat{y}:
1313      mpair_fin(r, c, sgd_l(Nat.mul(r, c), 0, (0.0 - 1.0 : F32), Array.get(F32, x, 0), Array.get(F32, y, 0)))
1314
1315# sum of each column (n x m -> 1 x m), returning the matrix
1316
1317type MColSum<-n: Nat, -m: Nat> is Type:
1318  MColSum{a: Mat<n, m>, s: Mat<1n, m>}
1319
1320def colsum_fin(-n: Nat, -m: Nat, g: G) -> MColSum<n, m>:
1321  match g:
1322    case G{ones, a, s}:
1323      MColSum{Mat{a}, Mat{s}}
1324
1325def Mat.col_sums(+n: Nat, +m: Nat, a: Mat<n, m>) -> MColSum<n, m>:
1326  match a:
1327    case Mat{d}:
1328      colsum_fin(n, m, gemm(1n, m, n, 0, 1, U32.from_nat(m), 1, Array.new(F32, cap_depth(n), 1.0), d, Array.new(F32, cap_depth(m), 0.0)))
1329
1330# s + the sum of each column, into s (1 x m): a bias gradient added into its accumulator, with the bits of
1331# Mat.add(s, Mat.col_sums(a)'s s)
1332
1333def Mat.col_sums_acc(+n: Nat, +m: Nat, a: Mat<n, m>, s: Mat<1n, m>) -> MColSum<n, m>:
1334  match a s:
1335    case Mat{d} Mat{z}:
1336      colsum_fin(n, m, gemm_ac(True{}, 1n, m, n, 0, 1, U32.from_nat(m), 1, Array.new(F32, cap_depth(n), 1.0), d, z))
1337
1338# mean cross-entropy and its gradient on the logits: (softmax - one-hot) / n.
1339# labels has one entry per row (n); the gradient replaces the logits.
1340
1341type MCE<-n: Nat, -c: Nat> is Type:
1342  MCE{loss: F32, grad: Mat<n, c>}
1343
1344def ce_fin(-n: Nat, -c: Nat, +nf: F32, r: L2) -> MCE<n, c>:
1345  match r:
1346    case L2{loss, d}:
1347      MCE{(loss / nf : F32), Mat{d}}
1348
1349def Mat.softmax_ce(+n: Nat, +c: Nat, z: Mat<n, c>, labels: List<&2, Nat>) -> MCE<n, c>:
1350  match z:
1351    case Mat{d}:
1352      ce_fin(n, c, F32.from_nat(n), ce_rows(labels, Nat.sub(c, 1n), U32.from_nat(c), F32.from_nat(n), 0, L2{0.0, d}))
1353
1354# argmax hits per row against labels
1355
1356type MHits<-n: Nat, -c: Nat> is Type:
1357  MHits{z: Mat<n, c>, hits: Nat}
1358
1359def hits_fin(-n: Nat, -c: Nat, r: CR) -> MHits<n, c>:
1360  match r:
1361    case CR{d, k}:
1362      MHits{Mat{d}, k}
1363
1364def Mat.count_correct(+n: Nat, +c: Nat, z: Mat<n, c>, labels: List<&2, Nat>) -> MHits<n, c>:
1365  match z:
1366    case Mat{d}:
1367      hits_fin(n, c, count_rows(labels, Nat.sub(c, 1n), U32.from_nat(c), 0, CR{d, 0n}))
1368
1369# a row as a list (returning the matrix) and the other way around
1370
1371type MRow<-r: Nat, -c: Nat> is Type:
1372  MRow{m: Mat<r, c>, row: List<&2, F32>}
1373
1374def row_fin(-r: Nat, -c: Nat, al: AL) -> MRow<r, c>:
1375  match al:
1376    case AL{d, l}:
1377      MRow{Mat{d}, l}
1378
1379def Mat.read_row(+r: Nat, +c: Nat, m: Mat<r, c>, +i: U32) -> MRow<r, c>:
1380  match m:
1381    case Mat{d}:
1382      row_fin(r, c, read_l(Nat.sub(c, 1n), (i * U32.from_nat(c) : U32), Nil{}, Array.get(F32, d, (i * U32.from_nat(c) : U32))))
1383
1384def Mat.write_row(-r: Nat, +c: Nat, m: Mat<r, c>, +i: U32, xs: List<&2, F32>) -> Mat<r, c>:
1385  match m:
1386    case Mat{d}:
1387      Mat{write_l(xs, (i * U32.from_nat(c) : U32), d)}
1388
1389# ---- variants that check the number of labels ----
1390
1391# does the list of labels have exactly n entries (one per row)?
1392def labels_len_is(xs: List<&2, Nat>, n: Nat) -> Bool:
1393  match xs n:
1394    case Nil{} 0n:
1395      True{}
1396    case Con{h, t} 1n+p:
1397      labels_len_is(t, p)
1398    case _ _:
1399      False{}
1400
1401def ce_checked(ok: Bool, +n: Nat, +c: Nat, z: Mat<n, c>, labels: List<&2, Nat>) -> Maybe<&1, MCE<n, c>>:
1402  match ok:
1403    case True{}:
1404      Some{Mat.softmax_ce(n, c, z, labels)}
1405    case False{}:
1406      None{}
1407
1408# like Mat.softmax_ce, but returns None unless labels has exactly n entries
1409def Mat.softmax_ce_checked(+n: Nat, +c: Nat, z: Mat<n, c>, +labels: List<&2, Nat>) -> Maybe<&1, MCE<n, c>>:
1410  ce_checked(labels_len_is(labels, n), n, c, z, labels)
1411
1412def hits_checked(ok: Bool, +n: Nat, +c: Nat, z: Mat<n, c>, labels: List<&2, Nat>) -> Maybe<&1, MHits<n, c>>:
1413  match ok:
1414    case True{}:
1415      Some{Mat.count_correct(n, c, z, labels)}
1416    case False{}:
1417      None{}
1418
1419# like Mat.count_correct, but returns None unless labels has exactly n entries
1420def Mat.count_correct_checked(+n: Nat, +c: Nat, z: Mat<n, c>, +labels: List<&2, Nat>) -> Maybe<&1, MHits<n, c>>:
1421  hits_checked(labels_len_is(labels, n), n, c, z, labels)
1422
1423# ---------------------------------------------------------------------
1424# Bands<r, c>: a matrix cut into bands of rows, for matrix · vector in parallel
1425# without copying the weights.
1426#
1427# Mat.matmul_nt with n = 1 can only run in parallel by cloning the whole weight
1428# matrix for every task, and in matrix · vector each weight is read once, so the
1429# copies cost more than the products (NOTES.md, exp. 9). Bands keeps each band in
1430# its own Array: a task takes its band and nothing is copied except the input vector.
1431#
1432# A node with r rows has children with half(r) and r - half(r) rows. The split is
1433# part of the type, so the row count of every band follows from r, and the law
1434# half_cover says that the two halves always cover the r rows exactly.
1435# ---------------------------------------------------------------------
1436
1437# n / 2 rounded down. A tail loop with an accumulator: a non-tail recursion here would put a
1438# continuation on every node of the band tree and keep the bands from running in parallel.
1439def half.go(n: Nat, acc: Nat) -> Nat:
1440  match n:
1441    case 0n:
1442      acc
1443    case 1n+p:
1444      match p:
1445        case 0n:
1446          acc
1447        case 1n+q:
1448          half.go(q, 1n+acc)
1449
1450def half(n: Nat) -> Nat:
1451  half.go(n, 0n)
1452
1453# a <= a
1454def leb_refl(+a: Nat) -> {True{} == leb(a, a) : Bool}:
1455  match a:
1456    case 0n:
1457      {==}
1458    case 1n+p:
1459      leb_refl(p)
1460
1461# 1 + (a + b) == a + (1 + b)
1462def add_succ_r(+a: Nat, -b: Nat) -> {1n+Nat.add(a, b) == Nat.add(a, 1n+b) : Nat}:
1463  match a:
1464    case 0n:
1465      {==}
1466    case 1n+p:
1467      %add_succ_r(p, b) : {2n+Nat.add(p, b) == 1n+_ : Nat}
1468      {==}
1469
1470# a + 0 == a
1471def add_zero_r(+a: Nat) -> {Nat.add(a, 0n) == a : Nat}:
1472  match a:
1473    case 0n:
1474      {==}
1475    case 1n+p:
1476      %add_zero_r(p) : {1n+Nat.add(p, 0n) == 1n+_ : Nat}
1477      {==}
1478
1479# half.go(n, acc) <= n + acc
1480def half_go_le(+n: Nat, +acc: Nat) -> {True{} == leb(half.go(n, acc), Nat.add(n, acc)) : Bool}:
1481  match n:
1482    case 0n:
1483      leb_refl(acc)
1484    case 1n+p:
1485      match p:
1486        case 0n:
1487          leb_succ(acc, acc, leb_refl(acc))
1488        case 1n+q:
1489          %Equal.sym(Nat, 1n+Nat.add(q, acc), Nat.add(q, 1n+acc), add_succ_r(q, acc)) : {True{} == leb(half.go(q, 1n+acc), 1n+_) : Bool}
1490          leb_succ(half.go(q, 1n+acc), Nat.add(q, 1n+acc), half_go_le(q, 1n+acc))
1491
1492# half(n) <= n
1493def half_le(+n: Nat) -> {True{} == leb(half(n), n) : Bool}:
1494  %add_zero_r(n) : {True{} == leb(half(n), _) : Bool}
1495  half_go_le(n, 0n)
1496
1497# a <= r  ->  r == a + (r - a)
1498def add_sub(+a: Nat, +r: Nat, e: {True{} == leb(a, r) : Bool}) -> {r == Nat.add(a, Nat.sub(r, a)) : Nat}:
1499  match a r:
1500    case 0n 0n:
1501      {==}
1502    case 0n 1n+q:
1503      {==}
1504    case 1n+p 0n:
1505      %e : BD(_, Unit, {0n == Nat.add(1n+p, Nat.sub(0n, 1n+p)) : Nat})
1506      Unit{}
1507    case 1n+p 1n+q:
1508      %add_sub(p, q, e) : {1n+q == 1n+_ : Nat}
1509      {==}
1510
1511# LAW (half_cover): the two bands of a node, half(r) rows and r - half(r) rows,
1512# add up to exactly r rows: no row is lost and none is counted twice.
1513law half_cover:
1514  for +n: Nat
1515  {n == Nat.add(half(n), Nat.sub(n, half(n))) : Nat}
1516
1517def half_cover(n):
1518  add_sub(half(n), n, half_le(n))
1519
1520type Bands<-r: Nat, -c: Nat> is Type:
1521  BLeaf{m: Mat<r, c>}
1522  BNode{x: Bands<half(r), c>, y: Bands<Nat.sub(r, half(r)), c>}
1523
1524# r x c of zeros in 2^d bands (d = 0: a single band, the same as a Mat)
1525def Bands.zeros(+d: Nat, +r: Nat, +c: Nat) -> Bands<r, c>:
1526  match d:
1527    case 0n:
1528      BLeaf{Mat.zeros(r, c)}
1529    case 1n+q:
1530      BNode{Bands.zeros(q, half(r), c), Bands.zeros(q, Nat.sub(r, half(r)), c)}
1531
1532# the row counts of the two bands follow the row count of the node: if rr == r at run
1533# time, the same holds for both halves (the bands carry r only in their type)
1534def eq_half(-a: Nat, -b: Nat, e: {a == b : Nat}) -> {half(a) == half(b) : Nat}:
1535  %e : {half(a) == half(_) : Nat}
1536  {==}
1537
1538def eq_rest(-a: Nat, -b: Nat, e: {a == b : Nat}) -> {Nat.sub(a, half(a)) == Nat.sub(b, half(b)) : Nat}:
1539  %e : {Nat.sub(a, half(a)) == Nat.sub(_, half(_)) : Nat}
1540  {==}
1541
1542# does a block of n numbers starting at flat index i end before the second band of a
1543# node with r rows? does it start in the second band?
1544def bfill_l(+r: Nat, +c: Nat, +i: Nat, +n: Nat) -> Bool:
1545  Nat.is_le(Nat.add(i, n), Nat.mul(half(r), c))
1546
1547def bfill_r(+r: Nat, +c: Nat, +i: Nat) -> Bool:
1548  Nat.is_le(Nat.mul(half(r), c), i)
1549
1550# writes the first n numbers of xs at index i (and nothing past them)
1551def fill_n(n: Nat, xs: List<&2, F32>, +i: U32, a: Array<F32>) -> Array<F32>:
1552  match n xs:
1553    case 1n+p Con{h, t}:
1554      fill_n(p, t, (i + 1 : U32), Array.set(F32, a, i, h))
1555    case _ _:
1556      a
1557
1558def fill_band(-r: Nat, -c: Nat, n: Nat, xs: List<&2, F32>, +i: U32, m: Mat<r, c>) -> Mat<r, c>:
1559  match m:
1560    case Mat{d}:
1561      Mat{fill_n(n, xs, i, d)}
1562
1563# writes the first n numbers of xs at flat index i; l and rt say which bands they touch
1564# (bfill_l, bfill_r); rr is the row count r at run time. A block that crosses into the second
1565# band is not copied: the first band writes only its part, the second gets the rest by List.drop.
1566def bfill(+c: Nat, -r: Nat, b: Bands<r, c>, l: Bool, rt: Bool, +rr: Nat, -e: {rr == r : Nat}, +i: Nat, +n: Nat, +xs: List<&2, F32>) -> Bands<r, c>:
1567  match b l rt:
1568    case BLeaf{m} _ _:
1569      BLeaf{fill_band(r, c, n, xs, U32.from_nat(i), m)}
1570    case BNode{x, y} True{} _:
1571      BNode{bfill(c, half(r), x, bfill_l(half(rr), c, i, n), bfill_r(half(rr), c, i), half(rr), eq_half(rr, r, e), i, n, xs), y}
1572    case BNode{x, y} False{} True{}:
1573      BNode{x, bfill(c, Nat.sub(r, half(r)), y, bfill_l(Nat.sub(rr, half(rr)), c, Nat.sub(i, Nat.mul(half(rr), c)), n), bfill_r(Nat.sub(rr, half(rr)), c, Nat.sub(i, Nat.mul(half(rr), c))), Nat.sub(rr, half(rr)), eq_rest(rr, r, e), Nat.sub(i, Nat.mul(half(rr), c)), n, xs)}
1574    case BNode{x, y} False{} False{}:
1575      BNode{bfill(c, half(r), x, bfill_l(half(rr), c, i, Nat.sub(Nat.mul(half(rr), c), i)), bfill_r(half(rr), c, i), half(rr), eq_half(rr, r, e), i, Nat.sub(Nat.mul(half(rr), c), i), xs), bfill(c, Nat.sub(r, half(r)), y, bfill_l(Nat.sub(rr, half(rr)), c, 0n, Nat.sub(n, Nat.sub(Nat.mul(half(rr), c), i))), bfill_r(Nat.sub(rr, half(rr)), c, 0n), Nat.sub(rr, half(rr)), eq_rest(rr, r, e), 0n, Nat.sub(n, Nat.sub(Nat.mul(half(rr), c), i)), List.drop(&2, F32, xs, Nat.sub(Nat.mul(half(rr), c), i)))}
1576
1577def bfill_top(ok: Bool, +r: Nat, +c: Nat, b: Bands<r, c>, +i: Nat, +n: Nat, +xs: List<&2, F32>) -> Bands<r, c>:
1578  match ok:
1579    case True{}:
1580      bfill(c, r, b, bfill_l(r, c, i, n), bfill_r(r, c, i), r, {==}, i, n, xs)
1581    case False{}:
1582      bfill(c, r, b, bfill_l(r, c, i, Nat.sub(Nat.mul(r, c), i)), bfill_r(r, c, i), r, {==}, i, Nat.sub(Nat.mul(r, c), i), xs)
1583
1584# writes the n numbers of xs into b starting at flat index i (row-major, as Mat.fill_at);
1585# numbers past r*c are dropped. For loading a large matrix block by block.
1586def Bands.fill_at(+r: Nat, +c: Nat, +i: Nat, +n: Nat, +xs: List<&2, F32>, b: Bands<r, c>) -> Bands<r, c>:
1587  bfill_top(Nat.is_le(Nat.add(i, n), Nat.mul(r, c)), r, c, b, i, n, xs)
1588
1589# ---- y = B · x for B with r rows of c numbers (a weight matrix stored out x in) ----
1590
1591# B (intact) and y = B · x, typed like Mat.matmul_nt: x is 1 x c, y is 1 x r
1592type BV<-r: Nat, -c: Nat> is Type:
1593  BV{b: Bands<r, c>, y: Mat<1n, r>}
1594
1595# the bands and y as a list in row order (the result of Bands.matvec_l)
1596type BL<-r: Nat, -c: Nat> is Type:
1597  BL{b: Bands<r, c>, y: List<&2, F32>}
1598
1599def bv_g2(-r: Nat, -c: Nat, w: Array<F32>, al: AL) -> BL<r, c>:
1600  match al:
1601    case AL{d, l}:
1602      BL{BLeaf{Mat{w}}, l}
1603
1604# the band's 1 + m rows came out in an Array (one number per row); read them back as a list
1605def bv_g(-r: Nat, -c: Nat, +m: Nat, g: G) -> BL<r, c>:
1606  match g:
1607    case G{a, b, y}:
1608      bv_g2(r, c, b, read_l(m, 0, Nil{}, Array.get(F32, y, 0)))
1609
1610# the rows of one band, one dot product each, with the gemm of Mat.matmul_nt (n = 1), so the
1611# results are identical. Writing the results into an Array and reading them back is faster than
1612# building the list row by row (NOTES.md, exp. 14).
1613def bv_gemm(-r: Nat, +c: Nat, rows: Nat, +rr: Nat, x: Array<F32>, w: Array<F32>) -> BL<r, c>:
1614  match rows:
1615    case 0n:
1616      BL{BLeaf{Mat{w}}, Nil{}}
1617    case 1n+p:
1618      bv_g(r, c, p, gemm(1n, rr, c, U32.from_nat(c), 1, 1, U32.from_nat(c), x, w, Array.new(F32, cap_depth(rr), 0.0)))
1619
1620def bv_mat(-r: Nat, +c: Nat, +rr: Nat, x: Array<F32>, m: Mat<r, c>) -> BL<r, c>:
1621  match m:
1622    case Mat{w}:
1623      bv_gemm(r, c, rr, rr, x, w)
1624
1625def bv_join(-r: Nat, -c: Nat, p: BL<half(r), c>, q: BL<Nat.sub(r, half(r)), c>) -> BL<r, c>:
1626  match p q:
1627    case BL{bx, yx} BL{by, yy}:
1628      BL{BNode{bx, by}, List.append(&2, F32, yx, yy)}
1629
1630# the two bands of a node run in parallel, each with its own copy of x (c numbers);
1631# xp is a pair of copies of x (a leaf uses one). xp comes after the erased e: up to Bend 2.0.36 an erased
1632# parameter at the end of the list turned the parallel let into two sequential calls (NOTES.md, exp. 11; fixed in
1633# 2.0.37 by bendlang/bend#1377). The order is kept, and harmless.
1634def bmv(+c: Nat, -r: Nat, b: Bands<r, c>, +rr: Nat, -e: {rr == r : Nat}, xp: Array<F32> & Array<F32>) -> BL<r, c>:
1635  match b xp:
1636    case BLeaf{m} Tuple{x, x2}:
1637      bv_mat(r, c, rr, x, m)
1638    case BNode{bx, by} Tuple{x1, x2}:
1639      p q = bmv(c, half(r), bx, half(rr), eq_half(rr, r, e), Array.clone(F32, x1)) bmv(c, Nat.sub(r, half(r)), by, Nat.sub(rr, half(rr)), eq_rest(rr, r, e), Array.clone(F32, x2))
1640      bv_join(r, c, p, q)
1641
1642def bv_fin(+r: Nat, -c: Nat, bl: BL<r, c>) -> BV<r, c>:
1643  match bl:
1644    case BL{b, y}:
1645      BV{b, Mat.from_list(1n, r, y)}
1646
1647# y = B · x for B (r x c, one row per output) and x (1 x c); returns B intact and y (1 x r).
1648# The same product as Mat.matmul_nt(1n, c, r, ..), and the same numbers, bit for bit.
1649def Bands.matvec(+r: Nat, +c: Nat, b: Bands<r, c>, x: Mat<1n, c>) -> BV<r, c>:
1650  match x:
1651    case Mat{xa}:
1652      bv_fin(r, c, bmv(c, r, b, r, {==}, Array.clone(F32, xa)))
1653
1654# the same product with x and y as lists (x: c numbers, y: r numbers in row order), without the
1655# Mat conversions of Bands.matvec: for callers that already hold the vector as a list
1656def Bands.matvec_l(+r: Nat, +c: Nat, b: Bands<r, c>, +x: List<&2, F32>) -> BL<r, c>:
1657  bmv(c, r, b, r, {==}, Array.clone(F32, fill_list(x, 0, Array.new(F32, cap_depth(c), 0.0))))
1658
1659# ---- reading rows and the whole matrix ----
1660
1661type BRow<-r: Nat, -c: Nat> is Type:
1662  BRow{b: Bands<r, c>, row: List<&2, F32>}
1663
1664def brow_fin(-r: Nat, -c: Nat, al: AL) -> BRow<r, c>:
1665  match al:
1666    case AL{d, l}:
1667      BRow{BLeaf{Mat{d}}, l}
1668
1669# n numbers starting at flat index i of one band (a band may have no rows)
1670def brow_leaf2(-r: Nat, -c: Nat, n: Nat, +i: U32, d: Array<F32>) -> BRow<r, c>:
1671  match n:
1672    case 0n:
1673      BRow{BLeaf{Mat{d}}, Nil{}}
1674    case 1n+p:
1675      brow_fin(r, c, read_l(p, i, Nil{}, Array.get(F32, d, i)))
1676
1677def brow_leaf(-r: Nat, -c: Nat, m: Mat<r, c>, +i: U32, n: Nat) -> BRow<r, c>:
1678  match m:
1679    case Mat{d}:
1680      brow_leaf2(r, c, n, i, d)
1681
1682def brow_l(-r: Nat, -c: Nat, p: BRow<half(r), c>, y: Bands<Nat.sub(r, half(r)), c>) -> BRow<r, c>:
1683  match p:
1684    case BRow{bx, row}:
1685      BRow{BNode{bx, y}, row}
1686
1687def brow_r(-r: Nat, -c: Nat, x: Bands<half(r), c>, q: BRow<Nat.sub(r, half(r)), c>) -> BRow<r, c>:
1688  match q:
1689    case BRow{by, row}:
1690      BRow{BNode{x, by}, row}
1691
1692# row i; left says whether i is in the first band of the node (i < half(rr))
1693def brow(+c: Nat, -r: Nat, b: Bands<r, c>, left: Bool, +rr: Nat, -e: {rr == r : Nat}, +i: Nat) -> BRow<r, c>:
1694  match b left:
1695    case BLeaf{m} _:
1696      brow_leaf(r, c, m, U32.from_nat(Nat.mul(i, c)), c)
1697    case BNode{x, y} True{}:
1698      brow_l(r, c, brow(c, half(r), x, Nat.is_lt(i, half(half(rr))), half(rr), eq_half(rr, r, e), i), y)
1699    case BNode{x, y} False{}:
1700      brow_r(r, c, x, brow(c, Nat.sub(r, half(r)), y, Nat.is_lt(Nat.sub(i, half(rr)), half(Nat.sub(rr, half(rr)))), Nat.sub(rr, half(rr)), eq_rest(rr, r, e), Nat.sub(i, half(rr))))
1701
1702# row i (c numbers), for example an embedding lookup
1703def Bands.read_row(+r: Nat, +c: Nat, b: Bands<r, c>, +i: Nat) -> BRow<r, c>:
1704  brow(c, r, b, Nat.is_lt(i, half(r)), r, {==}, i)
1705
1706def bl_join(-r: Nat, -c: Nat, p: BRow<half(r), c>, q: BRow<Nat.sub(r, half(r)), c>) -> BRow<r, c>:
1707  match p q:
1708    case BRow{bx, xs} BRow{by, ys}:
1709      BRow{BNode{bx, by}, List.append(&2, F32, xs, ys)}
1710
1711def blist(+c: Nat, -r: Nat, b: Bands<r, c>, +rr: Nat, -e: {rr == r : Nat}) -> BRow<r, c>:
1712  match b:
1713    case BLeaf{m}:
1714      brow_leaf(r, c, m, 0, Nat.mul(rr, c))
1715    case BNode{x, y}:
1716      bl_join(r, c, blist(c, half(r), x, half(rr), eq_half(rr, r, e)), blist(c, Nat.sub(r, half(r)), y, Nat.sub(rr, half(rr)), eq_rest(rr, r, e)))
1717
1718# all r*c numbers in row order (in BRow.row)
1719def Bands.to_list(+r: Nat, +c: Nat, b: Bands<r, c>) -> BRow<r, c>:
1720  blist(c, r, b, r, {==})
1721
1722# ---- the list path returns exactly r numbers ----
1723#
1724# The numbers do not matter here, only how many there are: each lemma below counts the list a
1725# function builds, following the function's own recursion.
1726
1727def bl_len(-r: Nat, -c: Nat, bl: BL<r, c>) -> Nat:
1728  match bl:
1729    case BL{b, y}:
1730      List.length(&2, F32, y)
1731
1732# s == a + (1 + b)  ->  s == 1 + (a + b)
1733def succ_out(-s: Nat, +a: Nat, +b: Nat, e: {s == Nat.add(a, 1n+b) : Nat}) -> {s == 1n+Nat.add(a, b) : Nat}:
1734  %Equal.sym(Nat, 1n+Nat.add(a, b), Nat.add(a, 1n+b), add_succ_r(a, b)) : {s == _ : Nat}
1735  e
1736
1737# s == 1 + (p + (1 + l))  ->  s == 2 + (p + l)
1738def len_step(-s: Nat, +p: Nat, +l: Nat, e: {s == 1n+Nat.add(p, 1n+l) : Nat}) -> {s == 2n+Nat.add(p, l) : Nat}:
1739  %Equal.sym(Nat, 1n+Nat.add(p, l), Nat.add(p, 1n+l), add_succ_r(p, l)) : {s == 1n+_ : Nat}
1740  e
1741
1742# s == n + 0  ->  s == n
1743def zero_out(-s: Nat, +n: Nat, e: {s == Nat.add(n, 0n) : Nat}) -> {s == n : Nat}:
1744  %add_zero_r(n) : {s == _ : Nat}
1745  e
1746
1747# reversing onto acc adds the lengths
1748def rev_len(+xs: List<&2, F32>, +acc: List<&2, F32>) -> {List.length(&2, F32, List.reverse.go(&2, F32, xs, acc)) == Nat.add(List.length(&2, F32, xs), List.length(&2, F32, acc)) : Nat}:
1749  match xs:
1750    case Nil{}:
1751      {==}
1752    case Con{h, t}:
1753      succ_out(List.length(&2, F32, List.reverse.go(&2, F32, t, h <> acc)), List.length(&2, F32, t), List.length(&2, F32, acc), rev_len(t, h <> acc))
1754
1755# appending adds the lengths
1756def app_len(+xs: List<&2, F32>, +ys: List<&2, F32>) -> {List.length(&2, F32, List.append(&2, F32, xs, ys)) == Nat.add(List.length(&2, F32, xs), List.length(&2, F32, ys)) : Nat}:
1757  match xs:
1758    case Nil{}:
1759      {==}
1760    case Con{h, t}:
1761      %app_len(t, ys) : {1n+List.length(&2, F32, List.append(&2, F32, t, ys)) == 1n+_ : Nat}
1762      {==}
1763
1764# a leaf's list: read_l reads mleft + 1 numbers onto acc and reverses them. By induction on
1765# mleft: with 0 left it reads one number (rev_len counts the reversed list); with 1 + p left it
1766# reads one and recurses, and len_step moves the 1 out of the sum.
1767law leaf_len:
1768  for -r: Nat
1769  for -c: Nat
1770  for w: Array<F32>
1771  for +mleft: Nat
1772  for +idx: U32
1773  for +acc: List<&2, F32>
1774  for rc: Array<F32> & F32
1775  {bl_len(r, c, bv_g2(r, c, w, read_l(mleft, idx, acc, rc))) == Nat.add(1n+mleft, List.length(&2, F32, acc)) : Nat}
1776
1777def leaf_len(r, c, w, mleft, idx, acc, rc):
1778  match mleft rc:
1779    case 0n Tuple{d, +v}:
1780      zero_out(List.length(&2, F32, List.reverse.go(&2, F32, v <> acc, Nil{})), 1n+List.length(&2, F32, acc), rev_len(v <> acc, Nil{}))
1781    case 1n+p Tuple{d, +v}:
1782      len_step(bl_len(r, c, bv_g2(r, c, w, read_l(p, (idx + 1 : U32), v <> acc, Array.get(F32, d, (idx + 1 : U32))))), p, List.length(&2, F32, acc), leaf_len(r, c, w, p, (idx + 1 : U32), v <> acc, Array.get(F32, d, (idx + 1 : U32))))
1783
1784# whatever the gemm wrote, reading 1 + p rows back gives 1 + p numbers (G has one constructor, so
1785# matching g opens it, and leaf_len counts the list)
1786def g_len(-r: Nat, -c: Nat, +p: Nat, g: G) -> {bl_len(r, c, bv_g(r, c, p, g)) == 1n+p : Nat}:
1787  match g:
1788    case G{a, b, y}:
1789      zero_out(bl_len(r, c, bv_g2(r, c, b, read_l(p, 0, Nil{}, Array.get(F32, y, 0)))), 1n+p, leaf_len(r, c, b, p, 0, Nil{}, Array.get(F32, y, 0)))
1790
1791# LAW (band_len): the product of one band with `rows` rows gives exactly `rows` numbers.
1792# 0 rows give the empty list; 1 + p rows go through the gemm, and g_len counts what is read back.
1793law band_len:
1794  for -r: Nat
1795  for +c: Nat
1796  for +rows: Nat
1797  for x: Array<F32>
1798  for w: Array<F32>
1799  {bl_len(r, c, bv_gemm(r, c, rows, rows, x, w)) == rows : Nat}
1800
1801def band_len(r, c, rows, x, w):
1802  match rows:
1803    case 0n:
1804      {==}
1805    case 1n+p:
1806      g_len(r, c, p, gemm(1n, 1n+p, c, U32.from_nat(c), 1, 1, U32.from_nat(c), x, w, Array.new(F32, cap_depth(1n+p), 0.0)))
1807
1808# joining two halves of h1 and h2 numbers gives h1 + h2 numbers. (The statement for the whole tree,
1809# "Bands.matvec_l gives r numbers", would apply this to the results of the two recursive calls and
1810# to the induction hypotheses about the same calls; that uses each band's Array twice, which Bend's
1811# affine rules refuse. It stays open, covered by tests: NOTES.md, v3.1.)
1812def join_len(-r: Nat, -c: Nat, -h1: Nat, -h2: Nat, p: BL<half(r), c>, q: BL<Nat.sub(r, half(r)), c>, hp: {bl_len(half(r), c, p) == h1 : Nat}, hq: {bl_len(Nat.sub(r, half(r)), c, q) == h2 : Nat}) -> {bl_len(r, c, bv_join(r, c, p, q)) == Nat.add(h1, h2) : Nat}:
1813  match p q:
1814    case BL{bx, +yx} BL{by, +yy}:
1815      %hp : {List.length(&2, F32, List.append(&2, F32, yx, yy)) == Nat.add(_, h2) : Nat}
1816      %hq : {List.length(&2, F32, List.append(&2, F32, yx, yy)) == Nat.add(List.length(&2, F32, yx), _) : Nat}
1817      app_len(yx, yy)
1818