nupp.util

Small types that belong to no larger facility.

local record Damage
    amount: integer
end

const set = new nupp.util.Bitset(128)
set:set(7)

const damage = nupp.util.newPool(Damage, 64)
damage:release(damage:acquire())

const store = nupp.util.newStore()
const theme: nupp.util.Key<string> = nupp.util.newKey("app.theme")
store:set(theme, "dark")

print(nupp.util.uuid4())

Each member is one type and the few functions that hand it out, reached by the type's name rather than by a module of its own. Bitset is a set of bit positions, Store is a bag of values indexed by typed Keys, and Pool leases cleared record instances from a free list. uuid4 and uuid7 generate identifiers, and fnv1a64 hashes bytes. try and tryWith run a function protected and answer (result, reason), which is the layout the or return suffix reads and pcall's own is not.

Unlike nupp.mem, which keeps every member a module of its own, these are re-exported here. Checked source still selects members independently, so using a bitset does not retain UUID support. uuid4 and uuid7 are the only members that resolve a host provider.

Module contents

Types

TypeKindDescription
BitsetrecordA growable set of bit positions counting from 0.
KeyrecordA typed identity for one value in a Store.
PoolrecordA pool of instances stamped with one record declaration.
StoreinterfaceAn independent typed bag indexed by keys.

Functions

FunctionKindDescription
fnv1a64functionHashes bytes with FNV-1a over 64 bits, as sixteen hexadecimal digits.
newKeyfunctionRegisters a new key, optionally under a name.
newPoolfunctionCreates a pool of instances of one record type.
newStorefunctionCreates an empty store.
tryfunctionRuns a function under pcall, answering (result, raised) rather than (ok, ...), which is the layout or return reads.
tryWithfunctionThe same for xpcall, with the handler after the function: it runs before the stack unwinds and its result is the reason.
uuid4functionGenerates a random version 4 UUID, hyphenated and lowercase.
uuid7functionGenerates a time-ordered version 7 UUID, hyphenated and lowercase.

Values

ValueKindDescription
WORD_BITSvariableThe bits one word of a Bitset holds.

Types#

Bitsetrecord#

record Bitset ...

A growable set of bit positions counting from 0.

Members

NameKindDescription
wordsfieldZero-based words, lowest bit first.
capacityfieldWords allocated.
usedfieldAn upper bound on the words that may hold a set bit: every word at or above it is zero.
populationfieldThe last resolved population, meaningful only when stale is false.
stalefieldWhether population needs recomputing.
constructorconstructorCreates an empty set.
reservemethodGrows storage so at least bits bits fit.
setmethodAdds a bit, growing when it is past the end.
clearmethodRemoves a bit.
getmethodWhether a bit is set.
setRangemethodAdds every bit in the inclusive range, one word-mask write per word rather than one operation per bit.
countmethodHow many bits are set.
isEmptymethodWhether nothing is set.
clearAllmethodRemoves every bit, keeping capacity so a set reused each frame stops allocating once it has reached its peak.
setOnlymethodRemoves every bit, then adds exactly one.
wordCountmethodAn upper bound on the words that may hold a set bit.
wordAtmethodOne stored word.
nextSetBitmethodThe lowest set position at or after from.
positionsIntomethodWrites every set position at or after from into target, lowest first.
containsAllmethodWhether every bit set in other is also set here.
overlapsmethodWhether at least one bit is set in both.
disjointmethodWhether the two share no set bit.
copyFrommethodReplaces these bits with a copy of other's.
orWithmethodAdds every bit set in other.
andWithmethodKeeps only the bits also set in other.
andNotWithmethodRemoves every bit set in other.
xorWithmethodKeeps only the bits set in exactly one of the two.

wordsfield#

words: uint32[?]
@private

Zero-based words, lowest bit first. Every word from used up to capacity is zero, which is the invariant every word loop relies on.

capacityfield#

capacity: integer
@private

Words allocated.

usedfield#

used: integer
@private

An upper bound on the words that may hold a set bit: every word at or above it is zero. Deliberately not the exact high-water mark, because keeping that exact costs a backwards scan on every clear and every intersection.

populationfield#

population: integer
@private

The last resolved population, meaningful only when stale is false.

stalefield#

stale: boolean
@private

Whether population needs recomputing. Set algebra marks it rather than paying two popcounts per word to keep a number most callers never read.

constructorconstructor#

constructor: function constructor(self, capacityBits: integer?)

Creates an empty set.

Arguments
NameTypeDescription
selfany

the set being initialized

capacityBitsinteger?

how many bits to allocate initially; setting a later position grows the set

Raises
TypeCondition
string

when capacityBits is more than 2^31

reservemethod#

reserve: function reserve(self, bits: integer): nil

Grows storage so at least bits bits fit. Only grows, and never returns storage. A growth at least doubles, so repeated small growths stay amortised.

Arguments
NameTypeDescription
selfany
bitsinteger

how many bits must fit

Returns
TypeDescription
nil
Raises
TypeCondition
string

when bits is NaN or more than 2^31

setmethod#

set: function set(self, index: integer): nil

Adds a bit, growing when it is past the end.

Arguments
NameTypeDescription
selfany
indexinteger

the bit position, counting from 0

Returns
TypeDescription
nil
Raises
TypeCondition
string

when index is negative, past 2^31 - 1, or not an integer

clearmethod#

clear: function clear(self, index: integer): nil

Removes a bit. Never allocates, and never narrows the used-word bound, so it stays constant-time whichever bit it was.

Arguments
NameTypeDescription
selfany
indexinteger

the bit position, counting from 0. One past the end, a negative one included, does nothing.

Returns
TypeDescription
nil

getmethod#

get: function get(self, index: integer): boolean

Whether a bit is set.

Arguments
NameTypeDescription
selfany
indexinteger

the bit position, counting from 0

Returns
TypeDescription
boolean

false for any position past the end, so no bound check is needed at the call site

setRangemethod#

setRange: function setRange(self, low: integer, high: integer): nil

Adds every bit in the inclusive range, one word-mask write per word rather than one operation per bit.

Arguments
NameTypeDescription
selfany
lowinteger

the first position added, counting from 0

highinteger

the last position added. Below low adds nothing, so an empty range needs no guard at the call site.

Returns
TypeDescription
nil
Raises
TypeCondition
string

when low is negative, when high is past 2^31 - 1, or when either is not an integer

countmethod#

count: function count(self): integer

How many bits are set. Resolves a pending recount, so a caller that reads it every frame pays for the set algebra it skipped; one that never reads it never pays.

Arguments
NameTypeDescription
selfany
Returns
TypeDescription
integer

the number of set bits

isEmptymethod#

isEmpty: function isEmpty(self): boolean

Whether nothing is set. Says nothing about capacity.

Arguments
NameTypeDescription
selfany
Returns
TypeDescription
boolean

true when no bit is set

clearAllmethod#

clearAll: function clearAll(self): nil

Removes every bit, keeping capacity so a set reused each frame stops allocating once it has reached its peak.

Arguments
NameTypeDescription
selfany
Returns
TypeDescription
nil

setOnlymethod#

setOnly: function setOnly(self, index: integer): nil

Removes every bit, then adds exactly one.

Arguments
NameTypeDescription
selfany
indexinteger

the one position left set, counting from 0

Returns
TypeDescription
nil
Raises
TypeCondition
string

when index is negative, past 2^31 - 1, or not an integer; the set is left unchanged

wordCountmethod#

wordCount: function wordCount(self): integer

An upper bound on the words that may hold a set bit. Every word at or above it is zero, and it is not narrowed by clear or by intersection, so it may exceed the exact high-water mark.

Arguments
NameTypeDescription
selfany
Returns
TypeDescription
integer

the number of words a word-at-a-time walk must visit

wordAtmethod#

wordAt: function wordAt(self, index: integer): integer

One stored word. Word w holds positions w * WORD_BITS through w * WORD_BITS + WORD_BITS - 1, lowest position first.

Arguments
NameTypeDescription
selfany
indexinteger

a word index counting from 0, bounded by wordCount

Returns
TypeDescription
integer

the word as a signed value, so one with its top bit set reads negative, and 0 for any index outside the bound

nextSetBitmethod#

nextSetBit: function nextSetBit(self, from: integer): integer

The lowest set position at or after from.

Stateless, so nested walks do not interfere and a mutation between calls cannot invalidate a walk in progress.

Arguments
NameTypeDescription
selfany
frominteger

the lowest position that may be returned, counting from 0

Returns
TypeDescription
integer

that position, or -1 when there is none

positionsIntomethod#

positionsInto: function positionsInto(self, target: int32[?], capacity: integer, from: integer): integer, integer

Writes every set position at or after from into target, lowest first.

One call rather than one per position. A walk pays a call and a word read for every position it returns, which for a few thousand positions is most of what it costs; this reads each word once and clears the position it just reported out of a register.

target is a pointer and a count because that is what a native kernel takes. Filling it here rather than allocating keeps a per-frame extraction allocation-free, and leaves the boundary where a native one would sit.

Arguments
NameTypeDescription
selfany
targetint32[?]

where positions are written, indexed from 0

capacityinteger

how many positions target holds

frominteger

the lowest position that may be written, counting from 0

Returns
TypeDescription
integer

how many positions were written, and the position to resume from when target filled first, or -1 when the set is exhausted

integer
Raises
TypeCondition
string

when capacity is negative

containsAllmethod#

containsAll: function containsAll(self, other: Bitset): boolean

Whether every bit set in other is also set here.

Arguments
NameTypeDescription
selfany
otherBitset

read and not modified

Returns
TypeDescription
boolean

true when other is a subset. An empty other is contained by anything.

overlapsmethod#

overlaps: function overlaps(self, other: Bitset): boolean

Whether at least one bit is set in both. Early-exiting, and deliberately not an intersection followed by a count.

Arguments
NameTypeDescription
selfany
otherBitset

read and not modified, as is the receiver

Returns
TypeDescription
boolean

true when the two share a set bit

disjointmethod#

disjoint: function disjoint(self, other: Bitset): boolean

Whether the two share no set bit.

Arguments
NameTypeDescription
selfany
otherBitset

read and not modified, as is the receiver

Returns
TypeDescription
boolean

the negation of overlaps

copyFrommethod#

copyFrom: function copyFrom(self, other: Bitset): nil

Replaces these bits with a copy of other's. The two are independent afterwards, and this keeps its own capacity when that is the larger.

Arguments
NameTypeDescription
selfany
otherBitset

read and not modified

Returns
TypeDescription
nil

orWithmethod#

orWith: function orWith(self, other: Bitset): nil

Adds every bit set in other.

One bitwise operation per word: the population is marked for recount rather than tracked, and there is no per-word test for whether the word changed, because that branch costs more than the write it saves.

Arguments
NameTypeDescription
selfany
otherBitset

read and not modified

Returns
TypeDescription
nil

andWithmethod#

andWith: function andWith(self, other: Bitset): nil

Keeps only the bits also set in other.

Arguments
NameTypeDescription
selfany
otherBitset

read and not modified. Words beyond the ones it uses are cleared, so an empty other empties the receiver.

Returns
TypeDescription
nil

andNotWithmethod#

andNotWith: function andNotWith(self, other: Bitset): nil

Removes every bit set in other.

Arguments
NameTypeDescription
selfany
otherBitset

read and not modified. Words beyond the ones it uses keep their bits, so an empty other changes nothing.

Returns
TypeDescription
nil

xorWithmethod#

xorWith: function xorWith(self, other: Bitset): nil

Keeps only the bits set in exactly one of the two.

Arguments
NameTypeDescription
selfany
otherBitset

read and not modified

Returns
TypeDescription
nil

Keyrecord#

record Key<T> ...

A typed identity for one value in a Store.

Type parameters

NameDescription
T

Members

NameKindDescription
SteptypeThe step of nupp.util.Key.registered.
idfield
namefield
findmethodFinds a registered key by name.
registeredmethodWalks every registered name, ascending by id.

Steptype#

type Step = function(any, integer): (integer, string)

The step of nupp.util.Key.registered.

Terminating on nil is the generic for's business, so the surface type promises what the loop delivers rather than what the last call returns.

idfield#

id: integer
@readonly

namefield#

name: string?
@readonly

findmethod#

find: function find(name: string): unknown

Finds a registered key by name.

The registry knows the name's id and nothing about its type, so the result is unknown and the caller casts to the Key<T> it claims.

Arguments
NameTypeDescription
namestring

the registered name

Returns
TypeDescription
unknown

the key, or nil when the name is not registered

registeredmethod#

registered: function registered(): function(any, integer): (integer, string), any, integer

Walks every registered name, ascending by id.

The walk allocates nothing: the step is one shared function and the control variable is the id it last reported. The registry itself is never handed out, so nothing the loop is given can be used to reach or change it. An anonymous key registers no name and is not walked.

Returns
TypeDescription
function(any, integer): (integer, string)

the step the loop calls

any

nothing to carry, since the ids are the whole state

integer

the id to resume after, 0 to start

Poolrecord#

record Pool<T is table> ...

A pool of instances stamped with one record declaration.

Type parameters

NameDescription
T

Members

NameKindDescription
witnessfield
capacityfieldHow many released instances the pool retains for reuse.
freeListfield
freeCountfield
liveCountfield
liveValuesfield
acquiremethodHands out an instance, from the free list or freshly stamped.
releasemethodClears an instance and returns it to the free list.
reservemethodStamps free instances until at least count are waiting.
livemethodHow many instances are acquired and not yet released.
freemethodHow many instances are waiting on the free list.
resetmethodDrops the free list.

witnessfield#

witness: any
@private@readonly

capacityfield#

capacity: integer
@readonly

How many released instances the pool retains for reuse.

freeListfield#

freeList: {T}
@private

freeCountfield#

freeCount: integer
@private

liveCountfield#

liveCount: integer
@private

liveValuesfield#

liveValues: {[any]: boolean}
@private

acquiremethod#

acquire: function(exclusive self: Pool<T>): T

Hands out an instance, from the free list or freshly stamped.

Arguments
NameTypeDescription
exclusive selfPool<T>
Returns
TypeDescription
T

releasemethod#

release: function(exclusive self: Pool<T>, value: T): nil

Clears an instance and returns it to the free list.

Arguments
NameTypeDescription
exclusive selfPool<T>
valueT
Returns
TypeDescription
nil
Raises
TypeCondition
string

when nothing is live, or value was not stamped by the pool's declaration

reservemethod#

reserve: function(exclusive self: Pool<T>, count: integer): nil

Stamps free instances until at least count are waiting.

Arguments
NameTypeDescription
exclusive selfPool<T>
countinteger
Returns
TypeDescription
nil
Raises
TypeCondition
string

when count is negative

livemethod#

live: function(self: Pool<T>): integer

How many instances are acquired and not yet released.

Arguments
NameTypeDescription
selfPool<T>
Returns
TypeDescription
integer

freemethod#

free: function(self: Pool<T>): integer

How many instances are waiting on the free list.

Arguments
NameTypeDescription
selfPool<T>
Returns
TypeDescription
integer

resetmethod#

reset: function(exclusive self: Pool<T>): nil

Drops the free list.

Arguments
NameTypeDescription
exclusive selfPool<T>
Returns
TypeDescription
nil
Raises
TypeCondition
string

when any instance is live

Storeinterface#

interface Store ...

An independent typed bag indexed by keys.

Members

NameKindDescription
SteptypeThe step of nupp.util.Store.entries.
getmethodAnswers the value held under a key.
setmethodStores a non-nil value under a key.
removemethodRemoves the value held under a key.
clearmethodRemoves every value without touching key registration.
entriesmethodWalks every occupied key ascending by id, without its value.

Steptype#

type Step = function(Store, integer): (integer, string?, string)

The step of nupp.util.Store.entries.

Terminating on nil is the generic for's business, so the surface type promises what the loop delivers rather than what the last call returns.

getmethod#

get: function<T>(self: Store, key: Key<T>): T?

Answers the value held under a key.

Arguments
NameTypeDescription
selfStore
keyKey<T>
Returns
TypeDescription
T?

setmethod#

set: function<T>(self: Store, key: Key<T>, value: T): nil

Stores a non-nil value under a key.

Arguments
NameTypeDescription
selfStore
keyKey<T>
valueT
Returns
TypeDescription
nil
Raises
TypeCondition
string

when value is nil; use nupp.util.Store.remove to delete an entry

removemethod#

remove: function<T>(self: Store, key: Key<T>): nil

Removes the value held under a key.

Arguments
NameTypeDescription
selfStore
keyKey<T>
Returns
TypeDescription
nil

clearmethod#

clear: function(self: Store): nil

Removes every value without touching key registration.

Arguments
NameTypeDescription
selfStore
Returns
TypeDescription
nil

entriesmethod#

entries: function(
    borrows self: Store
): (function(Store, integer): (integer, string?, string), Store borrows (self), integer)

Walks every occupied key ascending by id, without its value.

Arguments
NameTypeDescription
borrows selfStore
Returns
TypeDescription
function(Store, integer): (integer, string?, string)
Store borrows (self)
integer

Functions#

fnv1a64function#

function fnv1a64(borrows value: string | ByteView): string

Hashes bytes with FNV-1a over 64 bits, as sixteen hexadecimal digits.

Arguments

NameTypeDescription
borrows valuestring | ByteView

the bytes to hash

Returns

TypeDescription
string

the hash, as sixteen lowercase hexadecimal digits

newKeyfunction#

function newKey<T>(name: string?): Key<T>

Registers a new key, optionally under a name.

Type parameters

NameDescription
T

Arguments

NameTypeDescription
namestring?

the opaque name to register, or nil for an anonymous key

Returns

TypeDescription
Key<T>

the key

Raises

TypeCondition
string

when the name is empty or already registered, before an id is used

newPoolfunction#

function newPool<T is table>(witness: Type<T>, capacity: integer?): pool.Pool<T>

Creates a pool of instances of one record type.

Type parameters

NameDescription
T

Arguments

NameTypeDescription
witnessType<T>

the record declaration, whose metatable stamps every instance

capacityinteger?

how many released instances to retain, 32 when omitted

Returns

TypeDescription
pool.Pool<T>

the pool, warmed to capacity

Raises

TypeCondition
string

when capacity is negative

newStorefunction#

function newStore(): Store

Creates an empty store.

Returns

TypeDescription
Store

the store

tryfunction#

function try<R, A...>(f: function(A...): R, ...: A...): R?, unknown

Runs a function under pcall, answering (result, raised) rather than (ok, ...), which is the layout or return reads.

Type parameters

NameDescription
R
A

Arguments

NameTypeDescription
ffunction(A...): R

the function to run protected

...A...

its arguments

Returns

TypeDescription
R?

its result, or nil when it raised

unknown

the raised value, or nil when it did not

tryWithfunction#

function tryWith<R, E, A...>(f: function(A...): R, handler: function(any): E, ...: A...): R?, E?

The same for xpcall, with the handler after the function: it runs before the stack unwinds and its result is the reason.

Type parameters

NameDescription
R
E
A

Arguments

NameTypeDescription
ffunction(A...): R

the function to run protected

handlerfunction(any): E

what to make of a raised value

...A...

the function's arguments

Returns

TypeDescription
R?

its result, or nil when it raised

E?

what handler made of the raised value, or nil when it did not raise

uuid4function#

function uuid4(): string

Generates a random version 4 UUID, hyphenated and lowercase.

Returns

TypeDescription
string

the UUID, hyphenated and lowercase

Raises

TypeCondition
string

when the provider fails or returns a malformed UUID

uuid7function#

function uuid7(): string

Generates a time-ordered version 7 UUID, hyphenated and lowercase.

Ordered to the millisecond only: two made in the same millisecond carry random bits after the timestamp, so they compare in either order. It uses no RFC 9562 monotonic counter.

Returns

TypeDescription
string

the UUID, hyphenated and lowercase

Raises

TypeCondition
string

when the provider fails or returns a malformed UUID

Values#

WORD_BITSvariable#

const WORD_BITS

The bits one word of a Bitset holds.