# `nupp.mem.soa`
Structure-of-arrays storage for reified structs.
Every top-level field of a reified struct gets its own contiguous column, so a
loop that walks one field of many rows brings nothing else along.
```nupp
local soa = nupp.mem.soa
local struct Position
x: float
velocity: float
end
local positions = soa.allocate(ffi.typeof(), 128)
with rows = positions:write() do
for index = 1, #rows do
rows[index].x += rows[index].velocity
end
end
```
The compiler supplies the private layout argument to `allocate` and `layoutof`,
and lowers indexed row fields directly to the typed columns kept by these views.
This module owns allocation, checked whole-row operations, slicing, and
field-span construction; it never exposes the slab or its column pointers in the
public types.
::: tip Nonescaping views at -O1
A nonescaping `read` or `write` can remain virtual, so no row-view wrapper is
allocated. An escape or an opaque call materializes the same checked view.
:::
See [Structure-of-arrays storage](../../../../learn/runtime/data/structure-of-arrays/index.html) for when this layout is the one to
reach for, and [NEP 10: Structure-of-arrays storage](../../../../reference/neps/0010-structure-of-arrays/index.html) for why the container
rather than the declaration chooses it.
## Types
### `Array` _record_
```nupp
record soa.Array is soa.ArrayToken
readonly count: integer
readonly fingerprint: string
read: function(borrows self: Array): soa.Span borrows (self)
write: function(
exclusive self: Array
): affine(soa.WriteToken & soa.WriteSpan, soa.destroyWriteSpan) borrows (self)
close: nosuspend function(takes self: Array): nil
drop: nosuspend function(takes self: Array): nil
end
```
One owned native slab whose top-level struct fields occupy separate segments.
#### Type parameters
| Name | Description |
| --- | --- |
| `T` | |
#### Methods
##### `read`
```nupp
read: function(borrows self: Array): soa.Span borrows (self)
```
Borrows the rows as a shared checked view.
The borrow lasts as long as the view does, so a writer cannot be taken out
while one is live.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `borrows self` | `Array\` | |
###### Returns
| Type | Description |
| --- | --- |
| `soa.Span\ borrows (self)` | |
##### `write`
```nupp
write: function(
exclusive self: Array
): affine(soa.WriteToken & soa.WriteSpan, soa.destroyWriteSpan) borrows (self)
```
Borrows the rows as an exclusive checked view.
Exclusive for as long as the writer is live, and affine, so a scope
boundary or an explicit `drop` is what ends it.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `exclusive self` | `Array\` | |
###### Returns
| Type | Description |
| --- | --- |
| `affine(soa.WriteToken & soa.WriteSpan\, soa.destroyWriteSpan) borrows (self)` | |
##### `close`
```nupp
close: nosuspend function(takes self: Array): nil
```
Frees the slab. `drop` and scope exit both reach this.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `takes self` | `Array\` | |
###### Returns
| Type | Description |
| --- | --- |
| `nil` | |
##### `drop`
```nupp
drop: nosuspend function(takes self: Array): nil
```
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `takes self` | `Array\` | |
###### Returns
| Type | Description |
| --- | --- |
| `nil` | |
#### Fields
##### `count`
```nupp
count: integer
```
How many rows the slab holds.
##### `fingerprint`
```nupp
fingerprint: string
```
The layout fingerprint the slab was allocated under.
### `ArrayToken` _interface_
```nupp
sealed interface soa.ArrayToken
close: nosuspend function(takes self: ArrayToken): nil
end
```
#### Methods
##### `close`
```nupp
close: nosuspend function(takes self: ArrayToken): nil
```
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `takes self` | `ArrayToken` | |
###### Returns
| Type | Description |
| --- | --- |
| `nil` | |
### `FieldLayout` _record_
```nupp
record soa.FieldLayout
readonly name: string
readonly identity: string
readonly ctype: string
readonly ordinal: integer
readonly elementSize: integer
readonly alignment: integer
end
```
One stored top-level field in a SoA descriptor.
#### Fields
##### `name`
```nupp
name: string
```
The field's name, as the struct declared it.
##### `identity`
```nupp
identity: string
```
The struct name and the field name joined, which is what a fingerprint
entry is filed under.
##### `ctype`
```nupp
ctype: string
```
The C type of one element of this column.
##### `ordinal`
```nupp
ordinal: integer
```
Where this field sits in the declaration, counted from one.
##### `elementSize`
```nupp
elementSize: integer
```
Bytes per element.
##### `alignment`
```nupp
alignment: integer
```
The alignment the column starts on.
### `InstanceLayout` _record_
```nupp
record soa.InstanceLayout
readonly count: integer
readonly byteSize: integer
readonly segments: {soa.SegmentLayout}
end
```
The count-dependent layout of one complete SoA slab.
#### Fields
##### `count`
```nupp
count: integer
```
The row count this layout was computed for.
##### `byteSize`
```nupp
byteSize: integer
```
The slab's size in bytes, before alignment padding.
##### `segments`
```nupp
segments: {soa.SegmentLayout}
```
One segment per field, in declaration order.
### `Layout` _record_
```nupp
record soa.Layout
readonly name: string
readonly fingerprint: string
readonly alignment: integer
readonly fields: {soa.FieldLayout}
function forCount(self, count: integer): soa.InstanceLayout end
end
```
Immutable reflection for a reified struct's SoA representation.
#### Methods
##### `forCount`
```nupp
forCount: function forCount(self, count: integer): soa.InstanceLayout
```
Computes checked segment offsets for `count` rows without allocating storage.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `self` | `any` | |
| `count` | `integer` | |
###### Returns
| Type | Description |
| --- | --- |
| `soa.InstanceLayout` | |
###### Raises
- when count is negative or the slab size overflows a Lua integer
#### Fields
##### `name`
```nupp
name: string
```
The struct's name.
##### `fingerprint`
```nupp
fingerprint: string
```
Every field's identity, type, size and alignment in one string, so two
layouts can be compared without walking them.
##### `alignment`
```nupp
alignment: integer
```
The alignment the whole slab starts on, which is the widest field's.
##### `fields`
```nupp
fields: {soa.FieldLayout}
```
One entry per stored top-level field, in declaration order.
### `SegmentLayout` _record_
```nupp
record soa.SegmentLayout
readonly field: soa.FieldLayout
readonly offset: integer
readonly byteCount: integer
end
```
One field segment for a particular element count.
#### Fields
##### `field`
```nupp
field: soa.FieldLayout
```
Which field this segment holds.
##### `offset`
```nupp
offset: integer
```
Where the segment starts, in bytes from the slab's aligned base.
##### `byteCount`
```nupp
byteCount: integer
```
How many bytes it occupies.
### `Span` _interface_
```nupp
sealed interface soa.Span
metamethod __len: function(self: Span): integer
metamethod __index: function(borrows self: Span, index: integer): T
slice: function(borrows self: Span, first: integer, last: integer?): soa.Span borrows (self)
field: function(borrows self: Span, name: string): any borrows (self)
end
```
A shared checked view over SoA rows.
#### Type parameters
| Name | Description |
| --- | --- |
| `T` | |
#### Methods
##### `__len`
```nupp
__len: function(self: Span): integer
```
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `self` | `Span\` | |
###### Returns
| Type | Description |
| --- | --- |
| `integer` | |
##### `__index`
```nupp
__index: function(borrows self: Span, index: integer): T
```
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `borrows self` | `Span\` | |
| `index` | `integer` | |
###### Returns
| Type | Description |
| --- | --- |
| `T` | |
##### `slice`
```nupp
slice: function(borrows self: Span, first: integer, last: integer?): soa.Span borrows (self)
```
Answers a subrange of rows, inclusive at both ends.
The result borrows this view, and omitting `last` runs through to the end.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `borrows self` | `Span\` | |
| `first` | `integer` | |
| `last` | `integer?` | |
###### Returns
| Type | Description |
| --- | --- |
| `soa.Span\ borrows (self)` | |
##### `field`
```nupp
field: function(borrows self: Span, name: string): any borrows (self)
```
Answers one field's column as a `nupp.mem.span` view over this range.
The name must be a string literal naming a stored field, which is what lets
the checker give the result that field's element type rather than `any`. A
dynamic string or a missing field reports `NUPP2403`.
```nupp
const rows = positions:read()
const velocity: span.Span = rows:field("velocity")
for index = 1, #velocity do
print(velocity[index])
end
```
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `borrows self` | `Span\` | |
| `name` | `string` | |
###### Returns
| Type | Description |
| --- | --- |
| `any borrows (self)` | |
### `Writable` _type_
```nupp
type soa.Writable = affine(soa.WriteToken & soa.WriteSpan, soa.destroyWriteSpan)
```
An affine exclusive row view, released at its lexical boundary.
#### Type parameters
| Name | Description |
| --- | --- |
| `T` | |
### `WriteSpan` _interface_
```nupp
sealed interface soa.WriteSpan
metamethod __len: function(self: WriteSpan): integer
metamethod __index: function(borrows self: WriteSpan, index: integer): T
metamethod __newindex: function(exclusive self: WriteSpan, index: integer, value: T): nil
field: function(exclusive self: WriteSpan, name: string): any borrows (self)
shared: function(borrows self: WriteSpan): soa.Span borrows (self)
copyFrom: function(
exclusive self: WriteSpan,
targetFirst: integer,
borrows source: soa.Span,
sourceFirst: integer,
count: integer
): nil
slice: function(
exclusive self: WriteSpan,
first: integer,
last: integer?
): affine(soa.WriteToken & soa.WriteSpan, soa.destroyWriteSpan) borrows (self)
end
```
An affine exclusive checked view over SoA rows.
#### Type parameters
| Name | Description |
| --- | --- |
| `T` | |
#### Methods
##### `__len`
```nupp
__len: function(self: WriteSpan): integer
```
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `self` | `WriteSpan\` | |
###### Returns
| Type | Description |
| --- | --- |
| `integer` | |
##### `__index`
```nupp
__index: function(borrows self: WriteSpan, index: integer): T
```
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `borrows self` | `WriteSpan\` | |
| `index` | `integer` | |
###### Returns
| Type | Description |
| --- | --- |
| `T` | |
##### `__newindex`
```nupp
__newindex: function(exclusive self: WriteSpan, index: integer, value: T): nil
```
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `exclusive self` | `WriteSpan\` | |
| `index` | `integer` | |
| `value` | `T` | |
###### Returns
| Type | Description |
| --- | --- |
| `nil` | |
##### `field`
```nupp
field: function(exclusive self: WriteSpan, name: string): any borrows (self)
```
Answers one field's column as a writable `nupp.mem.span` view.
Two different fields may be projected at once, because the columns do not
overlap. The name must be a string literal naming a stored field; a dynamic
string or a missing field reports `NUPP2403`.
```nupp
do
local rows = particles:write()
local xs: span.Writable = rows:field("x")
local ys: span.Writable = rows:field("y")
xs[1] = 3.5
ys[1] = 4.5
drop xs
drop ys
drop rows
end
```
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `exclusive self` | `WriteSpan\` | |
| `name` | `string` | |
###### Returns
| Type | Description |
| --- | --- |
| `any borrows (self)` | |
##### `shared`
```nupp
shared: function(borrows self: WriteSpan): soa.Span borrows (self)
```
Downgrades this writer to a shared row view for the lifetime of the result.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `borrows self` | `WriteSpan\` | |
###### Returns
| Type | Description |
| --- | --- |
| `soa.Span\ borrows (self)` | |
##### `copyFrom`
```nupp
copyFrom: function(
exclusive self: WriteSpan,
targetFirst: integer,
borrows source: soa.Span,
sourceFirst: integer,
count: integer
): nil
```
Copies a row range from another view of the same struct.
Both ranges are checked before the first byte moves, and the copy is one
contiguous run per column rather than a row at a time, so no row struct is
ever materialized.
```nupp
with rows = target:write() do
rows:copyFrom(3, source:read(), 1, 2)
end
```
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `exclusive self` | `WriteSpan\` | |
| `targetFirst` | `integer` | |
| `borrows source` | `soa.Span\` | |
| `sourceFirst` | `integer` | |
| `count` | `integer` | |
###### Returns
| Type | Description |
| --- | --- |
| `nil` | |
##### `slice`
```nupp
slice: function(
exclusive self: WriteSpan,
first: integer,
last: integer?
): affine(soa.WriteToken & soa.WriteSpan, soa.destroyWriteSpan) borrows (self)
```
Answers an affine writable subrange of rows, inclusive at both ends.
The child borrows this view, so the parent is unusable until the child is
dropped. Row indexes start at one in the child, as they do in the parent.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `exclusive self` | `WriteSpan\` | |
| `first` | `integer` | |
| `last` | `integer?` | |
###### Returns
| Type | Description |
| --- | --- |
| `affine(soa.WriteToken & soa.WriteSpan\, soa.destroyWriteSpan) borrows (self)` | |
### `WriteToken` _interface_
```nupp
sealed interface soa.WriteToken
drop: nosuspend function(takes self: WriteToken): nil
end
```
#### Methods
##### `drop`
```nupp
drop: nosuspend function(takes self: WriteToken): nil
```
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `takes self` | `WriteToken` | |
###### Returns
| Type | Description |
| --- | --- |
| `nil` | |
## Functions
### `soa.allocate` _function_
```nupp
function soa.allocate(element: ctype, count: integer): affine(soa.Array, soa.destroyArray)
```
Allocates one SoA slab for a reified struct type.
The compiler injects the descriptor, and aliases of this exact function retain
the intrinsic identity. The result owns the slab and frees it at its lexical
boundary; the bytes are not zeroed.
```nupp
local positions = soa.allocate(ffi.typeof(), 128)
with rows = positions:write() do
rows[1].x = 3.5
end
```
#### Type parameters
| Name | Description |
| --- | --- |
| `T` | |
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `element` | `ctype\` | the ctype of one row |
| `count` | `integer` | how many rows to allocate |
#### Returns
| Type | Description |
| --- | --- |
| `affine(soa.Array\, soa.destroyArray)` | the slab, owned by the caller |
#### Raises
- when count is negative, layout arithmetic overflows, or allocation fails
### `soa.destroyArray` _function_
```nupp
function soa.destroyArray(takes self: T): nil
```
Frees a slab, which is what every SoA array's contract names.
Nothing calls this by hand. It is the terminal consumer `affine` carries, so a
scope boundary or an explicit `drop` reaches it.
#### Type parameters
| Name | Description |
| --- | --- |
| `T` | |
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `takes self` | `T` | the array, spent by this call |
#### Returns
| Type | Description |
| --- | --- |
| `nil` | |
### `soa.destroyWriteSpan` _function_
```nupp
function soa.destroyWriteSpan(takes self: T): nil
```
Ends a row writer, which is what every writable row view's contract names.
Nothing calls this by hand. It is the terminal consumer `affine` carries, so a
scope boundary or an explicit `drop` reaches it.
#### Type parameters
| Name | Description |
| --- | --- |
| `T` | |
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `takes self` | `T` | the writer, spent by this call |
#### Returns
| Type | Description |
| --- | --- |
| `nil` | |
### `soa.layoutof` _function_
```nupp
function soa.layoutof(element: ctype): soa.Layout
```
Answers immutable SoA reflection for a reified struct type without allocating.
Only top-level fields are split, so a nested struct or fixed array is one field
here whatever it holds.
```nupp
local layout = soa.layoutof(ffi.typeof())
assert(#layout.fields == 2)
assert(layout.fields[1].name == "position")
```
#### Type parameters
| Name | Description |
| --- | --- |
| `T` | |
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `element` | `ctype\` | the ctype of one row |
#### Returns
| Type | Description |
| --- | --- |
| `soa.Layout` | the count-independent descriptor |
#### Raises
- when compiler lowering is unavailable