# `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