nupp.runtime.hotreload

The development-only half of Nupp hot reload.

Generated watch modules keep their public function values stable and dispatch through one array of implementation functions. A patch is loaded while stage owns the registry: define validates every candidate and joins its authored upvalues to the cells held by the live implementation. Nothing in a live slot changes until commit has a complete candidate vector.

The state is process-wide so a host and generated modules may require this module through different loader paths without splitting the registry. Ordinary generated programs never require it.

See Hot reload for what a watch build does with this, and NEP 6: Hot reload for why a patch is refused rather than applied partially.

Module contents

Types

TypeKindDescription
CandidatetypeOne slot a prepared patch will fill, and the implementation to fill it with.
CapturestypeWhere a function's captured names sit in its own upvalue list.
DefinitiontypeWhat a live slot is known by, beside the implementation filling it.
ImplementationtypeOne function body a slot can hold.
SlotstypeThe implementation array a generated watch module dispatches through.
StagingtypeWhat stage accumulates while the patch chunk runs.
TransactiontypeA validated patch that has changed nothing yet.

Functions

FunctionKindDescription
abortfunctionDiscards an incomplete registration after its initial chunk fails.
commitfunctionAtomically publishes a prepared candidate vector.
definefunctionRegisters an initial implementation, or one candidate in the active stage.
generationfunctionAnswers the currently published implementation generation.
loadedfunctionAnswers whether a module has completed its initial watch execution.
modulefunctionAnswers the direct implementation array for one loaded watch module.
policyfunctionRecords one managed-cell cleanup policy a generated module uses.
resetForTestingfunctionClears process state.
sealfunctionMarks a module's initial registration complete after its chunk returns.
stagefunctionLoads and validates a patch without changing any live implementation.

Types#

Candidatetype#

type hotreload.Candidate = {
    --- The registration record the slot belongs to, which is private to this module.
    module: any,

    index: integer,
    implementation: hotreload.Implementation,

    --- The implementation's own upvalue numbering. Committing publishes it into the
    --- definition, because the next patch joins against this implementation and Lua
    --- numbers upvalues by first reference, which a changed body reorders.
    captures: hotreload.Captures
}

One slot a prepared patch will fill, and the implementation to fill it with.

Capturestype#

type hotreload.Captures = {
    byName: {[string]: integer},
    order: {string}
}

Where a function's captured names sit in its own upvalue list.

order is sorted, so two implementations of the same function can be compared name by name without either side's upvalue numbering mattering.

Definitiontype#

type hotreload.Definition = {
    id: string,
    signature: string,
    selfName: string?,
    captures: hotreload.Captures
}

What a live slot is known by, beside the implementation filling it.

A candidate has to match all four before it may be joined: a changed identity, signature, recursive binding or capture set is a change the running process cannot take, and each is refused by name.

Implementationtype#

type hotreload.Implementation = function(...: any): any

One function body a slot can hold.

Generated code writes these and generated code calls them, so no two slots need agree about what a call takes or answers. What this module does with one is join its upvalues and store it, neither of which reads a parameter.

Slotstype#

type hotreload.Slots = {hotreload.Implementation}

The implementation array a generated watch module dispatches through.

Naming it is what keeps the three functions that pass one around agreeing about what they are passing, and what lets bySlots be keyed by it.

Stagingtype#

type hotreload.Staging = {
    baseGeneration: integer,
    candidates: {hotreload.Candidate},

    --- Module name and slot index, so one patch cannot fill a slot twice.
    byKey: {[string]: hotreload.Candidate},

    policies: {[any]: {[string]: boolean}}
}

What stage accumulates while the patch chunk runs.

It is the registry define writes into: private to the staging call, discarded whether the chunk succeeds or raises, and turned into a transaction only once the whole chunk has run without one.

Transactiontype#

type hotreload.Transaction = {
    baseGeneration: integer,
    generation: integer,
    serial: integer,
    candidates: {hotreload.Candidate},

    --- Dynamic-custody policies per registration record, keyed the way `candidates`
    --- names them.
    policies: {[any]: {[string]: boolean}},

    committed: boolean
}

A validated patch that has changed nothing yet.

Everything fallible has already happened by the time one of these exists, which is what lets commit publish the whole vector with no failure point in the middle. serial distinguishes two transactions prepared from the same generation.

Functions#

hotreload.abortfunction#

function hotreload.abort(name: string): nil

Discards an incomplete registration after its initial chunk fails.

A module that already sealed is left alone, so this is safe to call from an error path that cannot tell how far the chunk got.

Arguments

NameTypeDescription
namestring

the watch module's name

Returns

TypeDescription
nil

hotreload.commitfunction#

function hotreload.commit(prepared: hotreload.Transaction): integer?, string?

Atomically publishes a prepared candidate vector.

Lua cannot run authored code between the direct array writes below, so no dispatch can observe half a patch.

Arguments

NameTypeDescription
preparedhotreload.Transaction

what stage answered

Returns

TypeDescription
integer?

the committed generation, or nil when this transaction is stale

string?

why it is stale, when it is

hotreload.definefunction#

function hotreload.define(slots: hotreload.Slots, index: integer, id: string, signature: string, selfName: string?, implementation: hotreload.Implementation): nil

Registers an initial implementation, or one candidate in the active stage.

A candidate must match the live definition's identity, signature, recursive binding and capture set. Each of those is a change the running process cannot take, and each is refused by name.

Arguments

NameTypeDescription
slotshotreload.Slots

the module's slot array

indexinteger

which slot this fills

idstring

the declaration identity the slot was registered under

signaturestring

the declaration's signature

selfNamestring?

the recursive binding's name, when the function has one

implementationhotreload.Implementation

the function body to install

Returns

TypeDescription
nil

Raises

  • when identity, signature, or captured lexical cells are incompatible

hotreload.generationfunction#

function hotreload.generation(): integer

Answers the currently published implementation generation.

Returns

TypeDescription
integer

hotreload.loadedfunction#

function hotreload.loaded(name: string): boolean

Answers whether a module has completed its initial watch execution.

Arguments

NameTypeDescription
namestring

Returns

TypeDescription
boolean

hotreload.modulefunction#

function hotreload.module(name: string): hotreload.Slots

Answers the direct implementation array for one loaded watch module.

Outside staging, naming a module that has not executed registers it, because that is a module about to run its own initial chunk. Inside staging it is a patch naming something that was never loaded, which is refused.

Arguments

NameTypeDescription
namestring

the watch module's name

Returns

TypeDescription
hotreload.Slots

the slot array that module dispatches through

Raises

  • when a patch names a module that has not executed yet

hotreload.policyfunction#

function hotreload.policy(slots: hotreload.Slots, key: string): nil

Records one managed-cell cleanup policy a generated module uses.

During staging the candidate set stays private until every live managed cell admits the change, so a policy that still owns a payload cannot be changed out from under it.

Arguments

NameTypeDescription
slotshotreload.Slots

the module's slot array

keystring

the stable policy key

Returns

TypeDescription
nil

hotreload.resetForTestingfunction#

function hotreload.resetForTesting(): nil

Clears process state. Intended for compiler/runtime tests only.

Returns

TypeDescription
nil

hotreload.sealfunction#

function hotreload.seal(name: string): nil

Marks a module's initial registration complete after its chunk returns.

Arguments

NameTypeDescription
namestring

the watch module's name

Returns

TypeDescription
nil

Raises

  • when the module is unknown or has already been sealed

hotreload.stagefunction#

function hotreload.stage(source: string, baseGeneration: integer): hotreload.Transaction?, string?

Loads and validates a patch without changing any live implementation.

Everything fallible happens here, so a transaction that comes back can be committed without a failure point in the middle of the vector.

Arguments

NameTypeDescription
sourcestring

the patch chunk

baseGenerationinteger

the generation the patch was built against

Returns

TypeDescription
hotreload.Transaction?

the prepared transaction, or nil when it was rejected

string?

why it was rejected, when it was