SPI#
nupp.spi.load(Interface) iterates the implementations a package advertises. A module chooses one while it initializes, usually with nupp.spi.select, and the choosing is ordinary code.
The interface is an exported declaration in a module that does not initialize its consumer:
module example.codec.spi
export interface Codec
priority: integer?
encode: function(value: string): string
endImplementations#
An implementation exports the interface's members directly:
The fallback is another module of the same shape, and nothing advertises it:
Both return their input unchanged: the example is about the selection, not the codec. An implementation needs typed Nupp source, a .d.nupp declaration, or a typed adapter, and the build holds it to an ordinary assignment covering generic functions, borrowing, ownership, and suspension. A plain .lua module carries nothing to check:
nupp: SPI implementation needs typed Nupp or a declaration: example.luacodecChecking is by signature. A provider still needs behavioral tests for its byte formats, ownership, cleanup, cancellation, and supported hosts.
Advertising#
A package lists its implementation modules in nupp/spi.json, under the interface's qualified name:
{
"example.codec.spi.Codec": ["example.fastcodec"]
}The name is the interface module and the interface it exports, in ordinary dots. A key starting with $, such as $schema, is reserved for metadata and ignored, so a descriptor can carry some without breaking an older compiler; every other key must be an interface name. Imported aliases and re-exports resolve to the defining interface, which must be exported and take no type parameters:
nupp: SPI declaration must name a concrete exported interface: example.codec.spi.TransformChoosing an implementation#
This consumer takes the unique highest priority and keeps the one function it calls:
nupp.spi.select reads each candidate's priority, counting a missing one as 0, and answers the one with the highest. A higher priority supersedes a tie below it; a tie for the highest raises, naming the interface and the two modules, rather than letting discovery order decide. It answers nil when nothing is advertised, so the fallback after ?? is loaded only when it is used. Every standard library facade selects its provider this way.
priority is a convention between an interface and select, and load knows nothing about it. Another consumer can iterate load itself to compare capabilities, read configuration, combine implementations, or reject duplicates instead. Discovery order assigns no preference: the use site decides what wins.
Initialization selects once, so a later call through encode performs no SPI lookup.
Discovery order#
The application's own descriptor comes first, then the target's runtime dependencies in declared order, depth first, visiting each dependency once. A module array keeps its order, a repeated interface and module pair appears once, and tool-only and compile-only dependencies contribute nothing.
Lazy loading#
Creating an iterator executes no provider. Each advance requires one module, so stopping early leaves the rest unloaded, and an empty index gives an empty iterator. A provider that fails to load propagates its ordinary require error, values and identities intact, and ordinary module caching keeps an implementation's identity within a Lua state. If a caller catches that error, the next advance retries the same provider; a failed provider never becomes an implicit skip to the next one.
Build artifacts#
A build writes a data-only index and the advertised modules into its artifact, and reports what it found:
"spi": [
{
"interface": "example.codec.spi.Codec",
"implementation": "example.fastcodec",
"dependency": "application"
}
]dependency is the package the descriptor came from, or application for the project's own. Editing a descriptor invalidates the generated index.
Standard-library providers#
A standard-library consumer chooses with nupp.spi.select: the unique highest priority, an omitted one counting as zero, and initialization fails on equal highest priorities. Without an external implementation it chooses its built-in fallback under ordinary target and host conditions.
| Interface module | Implementation interface |
|---|---|
nupp.text.spi, nupp.codec.json.spi, nupp.random.spi, nupp.time.spi |
Provider |
nupp.io.path.spi, nupp.io.uri.spi, nupp.runtime.bitops.spi, nupp.runtime.uuid.spi |
Provider |
nupp.digest.spi, nupp.checksum.spi, nupp.mac.spi |
Provider |
nupp.compression.spi, nupp.system.spi, nupp.gpu.spi |
Provider |
nupp.io.files.spi, nupp.io.http.spi, nupp.io.net.spi, nupp.io.tls.spi, nupp.io.process.spi |
Provider |
nupp.suspension.spi, nupp.workers.spi |
Provider |
nupp.runtime.representation.spi |
CstorageProvider, Int64Provider |
Every interface is named Provider, except where one module declares several: the representation module keeps one name per storage concern.
An algorithm catalog overlays its entries on the built-in catalog, and each interface module declares the shared resource types and cleanup identities to reuse. Storage and its integer operations must use one coherent representation, because selection cannot change the layout compiled into the program.
Workers#
A worker receives the artifact's immutable index and loads its own module instances in its own Lua state. Provider objects and closures do not cross a worker boundary.