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.

local soa = nupp.mem.soa

local struct Position
    x: float
    velocity: float
end

local positions = soa.allocate(ffi.typeof<Position>(), 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.

See Structure-of-arrays storage for when this layout is the one to reach for.

Module contents

Types

TypeKindDescription
ArrayrecordAn owned native slab whose top-level struct fields occupy separate segments.
ArrayTokeninterface
FieldLayoutrecordA stored top-level field in a SoA descriptor.
InstanceLayoutrecordThe count-dependent layout of a complete SoA slab.
LayoutrecordImmutable reflection for a reified struct's SoA representation.
ReadViewinterfaceA read-only SoA row view independent of its row type.
SegmentLayoutrecordA field segment for a particular element count.
SpaninterfaceA shared checked view over SoA rows.
WritabletypeAn affine exclusive row view, released at its lexical boundary.
WriteSpaninterfaceAn affine exclusive checked view over SoA rows.
WriteTokeninterface

Functions

FunctionKindDescription
allocatefunctionAllocates a SoA slab for a reified struct type.
destroyArrayfunctionFrees a slab, which is what every SoA array's contract names.
destroyWriteSpanfunctionEnds a row writer, which is what every writable row view's contract names.
layoutoffunctionReturns immutable SoA reflection for a reified struct type without allocating.

Types#

Arrayrecord#

record soa.Array<T> is soa.ArrayToken ...

An owned native slab whose top-level struct fields occupy separate segments.

Type parameters

NameDescription
T

Members

NameKindDescription
rawfield
elementfield
layoutfield
columnsfield
countfieldHow many rows the slab holds.
fingerprintfieldThe layout fingerprint the slab was allocated under.
readmethodBorrows the rows as a shared checked view.
writemethodBorrows the rows as an exclusive checked view.
closemethodFrees the slab.

rawfield#

raw: voidptr
@private

elementfield#

element: ctype<T>
@private@readonly

layoutfield#

@private@readonly

columnsfield#

columns: {any}
@private@readonly

countfield#

count: integer
@readonly

How many rows the slab holds.

fingerprintfield#

fingerprint: string
@readonly

The layout fingerprint the slab was allocated under.

readmethod#

read: function(borrows self: Array<T>): soa.Span<T> 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
NameTypeDescription
borrows selfArray<T>
Returns
TypeDescription
soa.Span<T> borrows (self)

writemethod#

write: function(
    exclusive self: Array<T>
): affine(soa.WriteToken & soa.WriteSpan<T>, 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 nupp.drop is what ends it.

Arguments
NameTypeDescription
exclusive selfArray<T>
Returns
TypeDescription
affine(soa.WriteToken & soa.WriteSpan<T>, soa.destroyWriteSpan) borrows (self)

closemethod#

close: @nosuspend function(takes self: Array<T>): nil

Frees the slab. nupp.drop and scope exit both reach this.

Arguments
NameTypeDescription
takes selfArray<T>
Returns
TypeDescription
nil

ArrayTokeninterface#

sealed interface soa.ArrayToken ...

Members

NameKindDescription
closemethod

closemethod#

close: @nosuspend function(takes self: ArrayToken): nil
Arguments
NameTypeDescription
takes selfArrayToken
Returns
TypeDescription
nil

FieldLayoutrecord#

record soa.FieldLayout ...

A stored top-level field in a SoA descriptor.

Members

NameKindDescription
namefieldThe field's name, as the struct declared it.
identityfieldThe struct name and the field name joined, which is what a fingerprint entry is filed under.
ctypefieldThe C type of an element of this column.
ordinalfieldWhere this field sits in the declaration, counted from one.
elementSizefieldBytes per element.
alignmentfieldThe alignment the column starts on.
pointerTypefield

namefield#

name: string
@readonly

The field's name, as the struct declared it.

identityfield#

identity: string
@readonly

The struct name and the field name joined, which is what a fingerprint entry is filed under.

ctypefield#

ctype: string
@readonly

The C type of an element of this column.

ordinalfield#

ordinal: integer
@readonly

Where this field sits in the declaration, counted from one.

elementSizefield#

elementSize: integer
@readonly

Bytes per element.

alignmentfield#

alignment: integer
@readonly

The alignment the column starts on.

pointerTypefield#

pointerType: any
@private@readonly

InstanceLayoutrecord#

record soa.InstanceLayout ...

The count-dependent layout of a complete SoA slab.

Members

NameKindDescription
countfieldThe row count this layout was computed for.
byteSizefieldThe slab's size in bytes, before alignment padding.
segmentsfieldOne segment per field, in declaration order.

countfield#

count: integer
@readonly

The row count this layout was computed for.

byteSizefield#

byteSize: integer
@readonly

The slab's size in bytes, before alignment padding.

segmentsfield#

segments: {soa.SegmentLayout}
@readonly

One segment per field, in declaration order.

Layoutrecord#

record soa.Layout ...

Immutable reflection for a reified struct's SoA representation.

Members

NameKindDescription
namefieldThe struct's name.
fingerprintfieldEvery field's identity, type, size and alignment in one string, so two layouts can be compared without walking them.
alignmentfieldThe alignment the whole slab starts on, which is the widest field's.
fieldsfieldOne entry per stored top-level field, in declaration order.
forCountmethodComputes checked segment offsets for count rows without allocating storage.

namefield#

name: string
@readonly

The struct's name.

fingerprintfield#

fingerprint: string
@readonly

Every field's identity, type, size and alignment in one string, so two layouts can be compared without walking them.

alignmentfield#

alignment: integer
@readonly

The alignment the whole slab starts on, which is the widest field's.

fieldsfield#

@readonly

One entry per stored top-level field, in declaration order.

forCountmethod#

forCount: function forCount(self, count: integer): soa.InstanceLayout

Computes checked segment offsets for count rows without allocating storage.

Arguments
NameTypeDescription
selfany
countinteger
Returns
TypeDescription
soa.InstanceLayout
Raises
TypeCondition
string

when count is not a nonnegative integer or the slab size overflows

ReadViewinterface#

sealed interface soa.ReadView

A read-only SoA row view independent of its row type.

SegmentLayoutrecord#

record soa.SegmentLayout ...

A field segment for a particular element count.

Members

NameKindDescription
fieldfieldWhich field this segment holds.
offsetfieldWhere the segment starts, in bytes from the slab's aligned base.
byteCountfieldHow many bytes it occupies.

fieldfield#

field: soa.FieldLayout
@readonly

Which field this segment holds.

offsetfield#

offset: integer
@readonly

Where the segment starts, in bytes from the slab's aligned base.

byteCountfield#

byteCount: integer
@readonly

How many bytes it occupies.

Spaninterface#

sealed interface soa.Span<T> is soa.ReadView ...

A shared checked view over SoA rows.

Project a column with rows.name. When a field name collides with a view member, use the literal bracket form rows["name"].

Type parameters

NameDescription
T

Members

NameKindDescription
__lenmetamethod
__indexmetamethod
slicemethodReturns a subrange of rows, inclusive at both ends.

__lenmetamethod#

__len: function(self: Span<T>): integer
Arguments
NameTypeDescription
selfSpan<T>
Returns
TypeDescription
integer

__indexmetamethod#

__index: function(borrows self: Span<T>, index: integer): T
Arguments
NameTypeDescription
borrows selfSpan<T>
indexinteger
Returns
TypeDescription
T

slicemethod#

slice: function(borrows self: Span<T>, first: integer, last: integer?): soa.Span<T> borrows (self)

Returns a subrange of rows, inclusive at both ends.

The result borrows this view, and omitting last runs through to the end.

Arguments
NameTypeDescription
borrows selfSpan<T>
firstinteger
lastinteger?
Returns
TypeDescription
soa.Span<T> borrows (self)

Writabletype#

type soa.Writable<T> = affine(soa.WriteToken & soa.WriteSpan<T>, soa.destroyWriteSpan)

An affine exclusive row view, released at its lexical boundary.

Type parameters

NameDescription
T

WriteSpaninterface#

sealed interface soa.WriteSpan<T> ...

An affine exclusive checked view over SoA rows.

Project a writable column with rows.name. When a field name collides with a view member, use the literal bracket form rows["name"].

Type parameters

NameDescription
T

Members

NameKindDescription
__lenmetamethod
__indexmetamethod
__newindexmetamethod
sharedmethodDowngrades this writer to a shared row view for the lifetime of the result.
copyFrommethodCopies a row range from another view of the same struct.
slicemethodReturns an affine writable subrange of rows, inclusive at both ends.

__lenmetamethod#

__len: function(self: WriteSpan<T>): integer
Arguments
NameTypeDescription
selfWriteSpan<T>
Returns
TypeDescription
integer

__indexmetamethod#

__index: function(borrows self: WriteSpan<T>, index: integer): T
Arguments
NameTypeDescription
borrows selfWriteSpan<T>
indexinteger
Returns
TypeDescription
T

__newindexmetamethod#

__newindex: function(exclusive self: WriteSpan<T>, index: integer, value: T): nil
Arguments
NameTypeDescription
exclusive selfWriteSpan<T>
indexinteger
valueT
Returns
TypeDescription
nil

sharedmethod#

shared: function(borrows self: WriteSpan<T>): soa.Span<T> borrows (self)

Downgrades this writer to a shared row view for the lifetime of the result.

Arguments
NameTypeDescription
borrows selfWriteSpan<T>
Returns
TypeDescription
soa.Span<T> borrows (self)

copyFrommethod#

copyFrom: function(
    exclusive self: WriteSpan<T>,
    targetFirst: integer,
    borrows source: soa.Span<T>,
    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.

with rows = target:write() do
    rows:copyFrom(3, source:read(), 1, 2)
end
Arguments
NameTypeDescription
exclusive selfWriteSpan<T>
targetFirstinteger
borrows sourcesoa.Span<T>
sourceFirstinteger
countinteger
Returns
TypeDescription
nil

slicemethod#

slice: function(
    exclusive self: WriteSpan<T>,
    first: integer,
    last: integer?
): affine(soa.WriteToken & soa.WriteSpan<T>, soa.destroyWriteSpan) borrows (self)

Returns 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
NameTypeDescription
exclusive selfWriteSpan<T>
firstinteger
lastinteger?
Returns
TypeDescription
affine(soa.WriteToken & soa.WriteSpan<T>, soa.destroyWriteSpan) borrows (self)

WriteTokeninterface#

sealed interface soa.WriteToken ...

Members

NameKindDescription
dropmethod

dropmethod#

drop: @nosuspend function(takes self: WriteToken): nil
Arguments
NameTypeDescription
takes selfWriteToken
Returns
TypeDescription
nil

Functions#

soa.allocatefunction#

function soa.allocate<T>(element: ctype<T>, count: integer): affine(soa.Array<T>, soa.destroyArray)

Allocates a 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.

local positions = soa.allocate(ffi.typeof<Position>(), 128)
with rows = positions:write() do
    rows[1].x = 3.5
end

Type parameters

NameDescription
T

Arguments

NameTypeDescription
elementctype<T>

the ctype of a row

countinteger

how many rows to allocate

Returns

TypeDescription
affine(soa.Array<T>, soa.destroyArray)

the slab, owned by the caller

Raises

TypeCondition
string

when count is invalid, layout arithmetic overflows, or allocation fails

soa.destroyArrayfunction#

function soa.destroyArray<T is soa.ArrayToken>(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 nupp.drop reaches it.

Type parameters

NameDescription
T

Arguments

NameTypeDescription
takes selfT

the array, spent by this call

Returns

TypeDescription
nil

soa.destroyWriteSpanfunction#

function soa.destroyWriteSpan<T is soa.WriteToken>(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 nupp.drop reaches it.

Type parameters

NameDescription
T

Arguments

NameTypeDescription
takes selfT

the writer, spent by this call

Returns

TypeDescription
nil

soa.layoutoffunction#

function soa.layoutof<T>(element: ctype<T>): soa.Layout

Returns immutable SoA reflection for a reified struct type without allocating.

Only top-level fields are split, so a nested struct or fixed array is a field here whatever it holds.

local layout = soa.layoutof(ffi.typeof<Sample>())
assert(#layout.fields == 2)
assert(layout.fields[1].name == "position")

Type parameters

NameDescription
T

Arguments

NameTypeDescription
elementctype<T>

the ctype of a row

Returns

TypeDescription
soa.Layout

the count-independent descriptor

Raises

TypeCondition
string

when compiler lowering is unavailable