nupp.mem.span

nupp.mem.span gives a C array a rooted, one-based, bounds-checked view. A shared Span<T> reads contiguous elements; a writable span adds exclusive access and an affine lifetime, so the pointer cannot outlive or overlap its owner.

local span = nupp.mem.span

const text = "hello"
const bytes = span.fromString(text)
assert(#bytes == 5)

Use spans at checked boundaries. Direct indexing through a pointer or variable-length C array remains an unsafe operation.

Span<T> is a sealed contract over a private implementation that keeps an element count beside a rooted pointer, so public code can neither forge one nor reach the raw pointer it holds. Indexing and slicing check that count first.

Creating a shared span#

fromCarray(source, count) borrows a C array and records its logical element count. fromString(source) creates the byte-specialized ByteSpan. Both keep their source rooted for the lifetime of the view.

local span = nupp.mem.span

local struct Point
    x: float
    y: float
end

const storage = carray(Point, 4)
const points = span.fromCarray(storage, 4)
assert(#points == 4)
assert(points[1].x == 0)

Indexes start at one. view[index] raises when the index is outside 1 through #view. slice(first, last) includes both endpoints and borrows the parent; omitting last extends through the end. An empty slice uses first, first - 1.

The operator surface replaces the former element methods and public count field. Each former member is a migration error, not a deprecated alternative:

Removed Replacement
view.count #view
view:get(index) view[index]
view:set(index, value) view[index] = value
view:getMut(index).field view[index].field
span.range(first, last, ...) indexed.range(first, last, ...)
Nonescaping spans allocate nothing at -O1

When the complete use of an exact standard Span constructor is static and nonescaping, Nupp keeps its anchor, pointer, offset, count, and capability as compiler-owned values instead of allocating a wrapper. Constructor validation and bounds checks still run, and the source remains strongly rooted through the last access, so the checked meaning does not change with the optimization level. An escape or an opaque call materializes the same checked span.

fromFixedCarray(source, count) returns FixedSpan<T, N> when the array and literal count carry the same static N. It satisfies Span<T>, while preserving the exact count for checks and generated code.

Writable spans#

writeCarray returns the affine alias Writable<T>. Its underlying WriteSpan<T> contract is what a function accepting exclusive access names:

local span = nupp.mem.span
local indexed = nupp.mem.indexed

local struct Point
    x: float
    y: float
end

local function clear(exclusive points: span.WriteSpan<Point>): nil
    const view = points
    const indexes = indexed.range(1, #view, view)
    for index = indexes.first, indexes.last do
        view[index] = new Point(0, 0)
    end
end

local storage = carray(Point, 4)
const points = span.writeCarray(storage, 4)
clear(points)
drop points

Whole-element and direct field assignments write through the checked indexed place. shared() downgrades the writer to a shared view for the returned view's lifetime.

A writable span is affine because it represents exclusive access rather than owned memory. Dropping it, explicitly or at scope exit, ends that access; it does not free or copy the source array. writeFixedCarray and FixedWritable<T, N> preserve a static count in the same way as their shared counterparts.

Asking one array for a shared span while a writer over it is live reports NUPP2607, and so does the reverse.

Slices and partitions#

WriteSpan.slice(first, last) creates one affine child writer. The parent remains blocked until the child is dropped, preventing a write through the parent from overlapping the slice.

splitAt(mid) partitions a writer into audited, non-overlapping left and right regions. mid is the number of elements in the left region, so zero gives an empty left side and count gives an empty right side. Both children retain the parent as their root.

Use a slice for one subrange and splitAt when two disjoint writable regions must be live together. Unknown indexes and bounds otherwise conservatively overlap.

Shared range for several spans#

indexed.range(first, last, ...) checks one inclusive range against every trusted Span or SoA view and answers a record whose first and last are ordinary integers:

local span = nupp.mem.span
local indexed = nupp.mem.indexed

local struct Value
    n: integer
end

local function dot(
    borrows left: span.Span<Value>,
    borrows right: span.Span<Value>
): integer
    const first = left
    const second = right
    const indexes = indexed.range(1, #first, first, second)
    local total: integer = 0
    for index = indexes.first, indexes.last do
        total = total + first[index].n * second[index].n
    end
    return total
end

const storage = carray(Value, 4)
const values = span.fromCarray(storage, 4)
print(dot(values, values))

At least one span is required. Empty ranges use the same first, first - 1 convention as slices. The typed borrowed vararg passes each original span directly and allocates no container or interface wrapper.

When the bounds and spans are const-bound in the same function, the successful range check proves matching indexed reads and writes non-raising inside the dominated numeric loop. This proof is part of checking at every optimization level: it is what permits those calls inside noraise code.

See OPT-6 for how that proof is spent at -O1, as direct FFI element access and virtual slices that allocate no wrapper.

Passing a span to C#

ref() returns the rooted pointer and logical count for a handwritten native wrapper. Shared spans return a const T[?]; writable spans require exclusive access and return T[?]. The returned pointer borrows the span, so it cannot escape independently.

A declarative C binding can use countedBy(count) instead. Its checked call surface accepts spans, verifies shared counts, and projects the physical pointer and count arguments automatically.

Module contents

Types

TypeKindDescription
ByteSpantypeThe byte-specialized shared view, which is what fromString answers.
ByteWriteSpantypeThe byte-specialized write range.
FixedSpaninterfaceA shared span whose exact element count is part of its static type.
FixedWritabletypeAn affine fixed-width writable span.
FixedWriteSpaninterfaceA writable span whose exact element count is part of its static type.
SpaninterfaceA checked, shared view over contiguous elements.
WritabletypeAn affine dynamic writable span.
WriteSpaninterfaceAn affine checked write range.
WriteSplitrecordTwo sibling, non-overlapping writable regions borrowed from one parent writer.
WriteTokeninterfaceThe consuming operation shared by every writable span representation.

Functions

FunctionKindDescription
destroyWriteSpanfunctionEnds a write range, which is what every writable span's contract names.
fromCarrayfunctionCreates a checked shared span over a C array and an explicit logical count.
fromFixedCarrayfunctionCreates a fixed shared span without a runtime length check.
fromStringfunctionCreates a byte span over a Lua string and keeps that string rooted.
writeCarrayfunctionCreates an affine write span over a C array and an explicit logical count.
writeFixedCarrayfunctionCreates a fixed affine write span without a runtime length check.

Types#

ByteSpantype#

type span.ByteSpan = span.Span<uint8>

The byte-specialized shared view, which is what fromString answers.

ByteWriteSpantype#

type span.ByteWriteSpan = span.WriteSpan<uint8>

The byte-specialized write range.

FixedSpaninterface#

sealed interface span.FixedSpan<T, const N: integer> is span.Span<T>
    metamethod __len: function(self: FixedSpan<T, N>): N
end

A shared span whose exact element count is part of its static type. It refines the dynamic contract, so APIs accepting Span<T> also accept a fixed span.

Type parameters

NameDescription
T
N

Methods

__len#
__len: function(self: FixedSpan<T, N>): N
Arguments
NameTypeDescription
selfFixedSpan<T, N>
Returns
TypeDescription
N

FixedWritabletype#

type span.FixedWritable<T, const N: integer> = affine(span.FixedWriteSpan<T, N>, span.destroyWriteSpan)

An affine fixed-width writable span.

Type parameters

NameDescription
T
N

FixedWriteSpaninterface#

sealed interface span.FixedWriteSpan<T, const N: integer> is span.WriteSpan<T>
    metamethod __len: function(self: FixedWriteSpan<T, N>): N
    shared: function(borrows self: FixedWriteSpan<T, N>): span.FixedSpan<T, N> borrows (self)
end

A writable span whose exact element count is part of its static type.

Type parameters

NameDescription
T
N

Methods

__len#
__len: function(self: FixedWriteSpan<T, N>): N
Arguments
NameTypeDescription
selfFixedWriteSpan<T, N>
Returns
TypeDescription
N
shared#
shared: function(borrows self: FixedWriteSpan<T, N>): span.FixedSpan<T, N> borrows (self)
Arguments
NameTypeDescription
borrows selfFixedWriteSpan<T, N>
Returns
TypeDescription
span.FixedSpan<T, N> borrows (self)

Spaninterface#

sealed interface span.Span<T>
    metamethod __len: function(self: Span<T>): integer
    metamethod __index: function(self: Span<T>, index: integer): T
    slice: function(self: Span<T>, first: integer, last: integer?): Span<T> borrows (self)
    ref: function(self: Span<T>): (const T[?] borrows (self), integer)
end

A checked, shared view over contiguous elements. Only this module can declare an implementation, so the contract is proof that ref() and count agree.

Type parameters

NameDescription
T

Methods

__len#
__len: function(self: Span<T>): integer
Arguments
NameTypeDescription
selfSpan<T>
Returns
TypeDescription
integer
__index#
__index: function(self: Span<T>, index: integer): T
Arguments
NameTypeDescription
selfSpan<T>
indexinteger
Returns
TypeDescription
T
slice#
slice: function(self: Span<T>, first: integer, last: integer?): Span<T> borrows (self)

Answers a subspan, inclusive at both ends and borrowed from this one.

Indexes are one-based. Omitting last runs through to the end, and first, first - 1 is how an empty subspan is written.

const values = span.fromCarray(storage, 4)
const middle = values:slice(2, 3)
assert(#middle == 2)
Arguments
NameTypeDescription
selfSpan<T>
firstinteger
lastinteger?
Returns
TypeDescription
Span<T> borrows (self)
ref#
ref: function(self: Span<T>): (const T[?] borrows (self), integer)

Answers the checked range as a const pointer and count, for a native call.

The pointer borrows this span, so it cannot outlive the view that proved its bounds. This is the one supported way out to C.

Arguments
NameTypeDescription
selfSpan<T>
Returns
TypeDescription
const T[?] borrows (self)
integer

Writabletype#

type span.Writable<T> = affine(span.WriteSpan<T>, span.destroyWriteSpan)

An affine dynamic writable span.

Type parameters

NameDescription
T

WriteSpaninterface#

sealed interface span.WriteSpan<T> is span.WriteToken
    metamethod __len: function(self: WriteSpan<T>): integer
    metamethod __index: function(borrows self: WriteSpan<T>, index: integer): T
    metamethod __newindex: function(exclusive self: WriteSpan<T>, index: integer, value: T): nil
    drop: nosuspend function(takes self: WriteSpan<T>): nil
    ref: function(exclusive self: WriteSpan<T>): (T[?] borrows (self), integer)
    shared: function(borrows self: WriteSpan<T>): span.Span<T> borrows (self)
    slice: function(
        exclusive self: WriteSpan<T>,
        first: integer,
        last: integer?
    ): affine(span.WriteSpan<T>, span.destroyWriteSpan) borrows (self)
    @partition(left, right)
    splitAt: function(exclusive self: WriteSpan<T>, mid: integer): span.WriteSplit<T> borrows (self)
end

An affine checked write range. Its live token keeps the source under an incompatible-borrow barrier until drop or scope exit consumes it. Only this module can declare an implementation.

Type parameters

NameDescription
T

Methods

__len#
__len: function(self: WriteSpan<T>): integer
Arguments
NameTypeDescription
selfWriteSpan<T>
Returns
TypeDescription
integer
__index#
__index: function(borrows self: WriteSpan<T>, index: integer): T
Arguments
NameTypeDescription
borrows selfWriteSpan<T>
indexinteger
Returns
TypeDescription
T
__newindex#
__newindex: function(exclusive self: WriteSpan<T>, index: integer, value: T): nil
Arguments
NameTypeDescription
exclusive selfWriteSpan<T>
indexinteger
valueT
Returns
TypeDescription
nil
drop#
drop: nosuspend function(takes self: WriteSpan<T>): nil

Ends this write range, releasing the barrier on its source.

Arguments
NameTypeDescription
takes selfWriteSpan<T>
Returns
TypeDescription
nil
ref#
ref: function(exclusive self: WriteSpan<T>): (T[?] borrows (self), integer)

Answers the checked range as a mutable pointer and count, for a native call.

The pointer borrows this range, so the barrier on the source outlives the call that was handed it.

Arguments
NameTypeDescription
exclusive selfWriteSpan<T>
Returns
TypeDescription
T[?] borrows (self)
integer
shared#
shared: function(borrows self: WriteSpan<T>): span.Span<T> borrows (self)

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

The writer is borrowed rather than spent, so it comes back when the shared view ends.

Arguments
NameTypeDescription
borrows selfWriteSpan<T>
Returns
TypeDescription
span.Span<T> borrows (self)
slice#
slice: function(
    exclusive self: WriteSpan<T>,
    first: integer,
    last: integer?
): affine(span.WriteSpan<T>, span.destroyWriteSpan) borrows (self)

Answers an affine writable subrange, inclusive at both ends.

The child borrows this range, so the parent is unusable until the child is dropped. Two disjoint children at once is what splitAt is for.

Arguments
NameTypeDescription
exclusive selfWriteSpan<T>
firstinteger
lastinteger?
Returns
TypeDescription
affine(span.WriteSpan<T>, span.destroyWriteSpan) borrows (self)
splitAt#
splitAt: function(exclusive self: WriteSpan<T>, mid: integer): span.WriteSplit<T> borrows (self)
@partition(left, right)

Partitions this range at a zero-based boundary count.

The two children are non-overlapping, so both are writable at once, which a pair of slice calls could not be. mid is how many elements go left.

do
    local writable = values:write()
    local split = writable:splitAt(2)
    split.left[1] = 11 as int32
    split.right[1] = 22 as int32
end
Arguments
NameTypeDescription
exclusive selfWriteSpan<T>
midinteger
Returns
TypeDescription
span.WriteSplit<T> borrows (self)

WriteSplitrecord#

record span.WriteSplit<T>
    readonly left: span.WriteSpan<T> borrows (anchor)
    readonly right: span.WriteSpan<T> borrows (anchor)
end

Two sibling, non-overlapping writable regions borrowed from one parent writer. The representation is private so only this module can assert how the children relate to their anchor.

Type parameters

NameDescription
T

Fields

left#
left: span.WriteSpan<T> borrows (anchor)

The first mid elements, writable independently of right.

right#
right: span.WriteSpan<T> borrows (anchor)

Everything after them, writable independently of left.

WriteTokeninterface#

sealed interface span.WriteToken
    drop: nosuspend function(takes self: WriteToken): nil
end

The consuming operation shared by every writable span representation.

Methods

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

Functions#

span.destroyWriteSpanfunction#

function span.destroyWriteSpan<T is span.WriteToken>(takes writable: T): nil

Ends a write range, which is what every writable span'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

NameDescription
T

Arguments

NameTypeDescription
takes writableT

the write range, spent by this call

Returns

TypeDescription
nil

span.fromCarrayfunction#

function span.fromCarray<T>(borrows source: T[?], count: integer): span.Span<T>

Creates a checked shared span over a C array and an explicit logical count.

A native raw reference uses the caller's explicit extent. Wasm references also check it against their rooted allocation. Later operations check this extent.

local struct Value
    n: int32
end

const storage = carray(Value, 4)
const values = span.fromCarray(storage, 4)
assert(#values == 4)

Type parameters

NameDescription
T

Arguments

NameTypeDescription
borrows sourceT[?]

the array the view reads, rooted for the view's lifetime

countinteger

how many elements the array holds

Returns

TypeDescription
span.Span<T>

the view, borrowed from the array

Raises

  • when count is negative

span.fromFixedCarrayfunction#

function span.fromFixedCarray<T, const N: integer>(borrows source: T[N], count: N): span.FixedSpan<T, N>

Creates a fixed shared span without a runtime length check.

The literal count is both the stored count and the proof that the source has exactly N elements, so there is nothing left to check at run time.

Type parameters

NameDescription
T
N

Arguments

NameTypeDescription
borrows sourceT[N]

the array the view reads, rooted for the view's lifetime

countN

the literal count, which must be the source's N

Returns

TypeDescription
span.FixedSpan<T, N>

the view, borrowed from the array

span.fromStringfunction#

function span.fromString(borrows source: string): span.ByteSpan

Creates a byte span over a Lua string and keeps that string rooted.

const text = "hello"
const bytes = span.fromString(text)
assert(#bytes == 5 and bytes[1] == 104)

Arguments

NameTypeDescription
borrows sourcestring

the string the view reads, rooted for the view's lifetime

Returns

TypeDescription
span.ByteSpan

the byte view, borrowed from the string

span.writeCarrayfunction#

function span.writeCarray<T>(exclusive source: T[?], count: integer): span.Writable<T>

Creates an affine write span over a C array and an explicit logical count.

The source is exclusive during construction, and the returned owner keeps it under an incompatible-borrow barrier until drop or a scope boundary consumes the token. Nothing else may read or write the array in between.

local struct Value
    n: int32
end

const storage = carray(Value, 4)
do
    local writable = span.writeCarray(storage, 4)
    writable[1] = new Value(42)
    drop writable
end
const values = span.fromCarray(storage, 4)
assert(values[1].n == 42)

Type parameters

NameDescription
T

Arguments

NameTypeDescription
exclusive sourceT[?]

the array the view writes, held exclusively for its lifetime

countinteger

how many elements the array holds

Returns

TypeDescription
span.Writable<T>

the affine writer, borrowed from the array

Raises

  • when count is negative

span.writeFixedCarrayfunction#

function span.writeFixedCarray<T, const N: integer>(exclusive source: T[N], count: N): span.FixedWritable<T, N>

Creates a fixed affine write span without a runtime length check.

The source array type and the literal count must name the same N, which is what stands in for the bounds check.

Type parameters

NameDescription
T
N

Arguments

NameTypeDescription
exclusive sourceT[N]

the array the view writes, held exclusively for its lifetime

countN

the literal count, which must be the source's N

Returns

TypeDescription
span.FixedWritable<T, N>

the affine writer, borrowed from the array