Lowent Manual←↑→

soa — an SoA layout trial: one array per field

Source
lib/soa.low
Layer
L0 — pure computation
Capabilities
none

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

A study proposing SoA as a language feature (a type constructor like 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.
opShapeFailure
step_xproc (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_fieldfn (f slice u64) → u64none
get_xfn (xs slice u64, i u64) → u640 out of range
step_allproc (xs, ys mut slice u64, vxs, vys slice u64, n u64) → u64none — the smaller of two counts
step_x_aosproc (rows mut slice u64, stride, xoff, voff, n u64) → u64 — the AoS versionstops 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 .
end

Where 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.