soa — an SoA layout trial: one array per field
There are two ways to keep n elements of {x, y, vx, vy}. AoS (Array of Structs) lays whole elements in one array; SoA (Struct of Arrays) keeps one array per field side by side. For a computation scanning only x, SoA reads just the needed values contiguously and uses the cache economically.
A measurement, not an answer
store[T, soa] with dozens of keywords and ops) was deferred, with the reconsideration condition “first write it in lib/ with zero new builtins; where it gets stuck becomes the list of language work”. This module is that. Finding — the SoA layout itself is fully expressible as a library. Per-field arrays, stride-free sequential access and single-field kernels are all written without new language machinery. No speed claims without measurement — this module guarantees only correctness, that both layouts give the same answer, and code touching whole elements is better off with AoS.| op | Shape | Failure |
|---|---|---|
step_x | proc (xs mut slice u64, vxs slice u64, n u64) → u64 — xs[i] += vxs[i] | none — processes only min(n, len xs, len vxs) and answers that count |
sum_field | fn (f slice u64) → u64 | none |
get_x | fn (xs slice u64, i u64) → u64 | 0 out of range |
step_all | proc (xs, ys mut slice u64, vxs, vys slice u64, n u64) → u64 | none — the smaller of two counts |
step_x_aos | proc (rows mut slice u64, stride, xoff, voff, n u64) → u64 — the AoS version | stops at the element whose velocity slot is out of range and answers that i |
Table 50.1 — Ops of soa — all effects none
proc demo input xs mut slice u64 . . input vxs mut slice u64 . .
input rows mut slice u64 . . output u64 .
do
set (index xs 0) 1 .
set (index xs 1) 2 .
set (index vxs 0) 10 .
set (index vxs 1) 20 .
let n1 u64 be soa.step_x xs vxs 2 .
guard eq n1 2 . else return 90 .
let s1 u64 be soa.sum_field (subslice xs 0 2) .
set (index rows 0) 1 .
set (index rows 1) 10 .
set (index rows 2) 2 .
set (index rows 3) 20 .
let n2 u64 be soa.step_x_aos rows 2 0 1 2 .
guard eq n2 2 . else return 91 .
var s2 u64 be add (index rows 0) (index rows 2) .
guard eq s1 s2 . else return 92 .
return s1 .
endWhere it gets stuck — the list of language work. ① There is no syntax for handling one element “as a lump” — the caller gathers fields by hand (get_x). Inconvenient, not impossible. ② Arguments multiply with the number of fields (step_all) — solved since slices were allowed in struct fields, but this module is a measurement record and keeps the four-argument shape. ③ Types do not know the layout — AoS and SoA versions get different op names (step_x versus step_x_aos), and AoS offset arguments are all u64 where the compiler cannot help.
Counter-example. Ignoring the returned count
soa.step_x xs vxs 1000 silently processes only 3 if xs has 3 slots. Code assuming all n were processed confirms with guard eq m n ..Counter-example. Swapping the AoS offsets
soa.step_x_aos rows 2 1 0 3 has xoff and voff reversed and adds position to velocity without error. That is how “layout is not in the type” feels.Cautions. Keeping parallel arrays the same length is the caller’s responsibility — ops shrink to the shorter one without telling. get_x’s failure value 0 cannot be told from a normal value. A bare index does not shrink the range and stops with E-VM-BOUNDS. Splitting a long let or set across lines lets the newline close the form — end the line with , to continue.