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
| Type | Kind | Description |
|---|---|---|
Candidate | type | One slot a prepared patch will fill, and the implementation to fill it with. |
Captures | type | Where a function's captured names sit in its own upvalue list. |
Definition | type | What a live slot is known by, beside the implementation filling it. |
Implementation | type | One function body a slot can hold. |
Slots | type | The implementation array a generated watch module dispatches through. |
Staging | type | What stage accumulates while the patch chunk runs. |
Transaction | type | A validated patch that has changed nothing yet. |
Functions
| Function | Kind | Description |
|---|---|---|
abort | function | Discards an incomplete registration after its initial chunk fails. |
commit | function | Atomically publishes a prepared candidate vector. |
define | function | Registers an initial implementation, or one candidate in the active stage. |
generation | function | Answers the currently published implementation generation. |
loaded | function | Answers whether a module has completed its initial watch execution. |
module | function | Answers the direct implementation array for one loaded watch module. |
policy | function | Records one managed-cell cleanup policy a generated module uses. |
resetForTesting | function | Clears process state. |
seal | function | Marks a module's initial registration complete after its chunk returns. |
stage | function | Loads 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): anyOne 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#
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
| Name | Type | Description |
|---|---|---|
name | string | the watch module's name |
Returns
| Type | Description |
|---|---|
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
| Name | Type | Description |
|---|---|---|
prepared | hotreload.Transaction | what |
Returns
| Type | Description |
|---|---|
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): nilRegisters 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
| Name | Type | Description |
|---|---|---|
slots | hotreload.Slots | the module's slot array |
index | integer | which slot this fills |
id | string | the declaration identity the slot was registered under |
signature | string | the declaration's signature |
selfName | string? | the recursive binding's name, when the function has one |
implementation | hotreload.Implementation | the function body to install |
Returns
| Type | Description |
|---|---|
nil |
Raises
when identity, signature, or captured lexical cells are incompatible
hotreload.generationfunction#
function hotreload.generation(): integerAnswers the currently published implementation generation.
Returns
| Type | Description |
|---|---|
integer |
hotreload.loadedfunction#
Answers whether a module has completed its initial watch execution.
Arguments
| Name | Type | Description |
|---|---|---|
name | string |
Returns
| Type | Description |
|---|---|
boolean |
hotreload.modulefunction#
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
| Name | Type | Description |
|---|---|---|
name | string | the watch module's name |
Returns
| Type | Description |
|---|---|
hotreload.Slots | the slot array that module dispatches through |
Raises
when a patch names a module that has not executed yet
hotreload.policyfunction#
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
| Name | Type | Description |
|---|---|---|
slots | hotreload.Slots | the module's slot array |
key | string | the stable policy key |
Returns
| Type | Description |
|---|---|
nil |
hotreload.resetForTestingfunction#
function hotreload.resetForTesting(): nilClears process state. Intended for compiler/runtime tests only.
Returns
| Type | Description |
|---|---|
nil |
hotreload.sealfunction#
Marks a module's initial registration complete after its chunk returns.
Arguments
| Name | Type | Description |
|---|---|---|
name | string | the watch module's name |
Returns
| Type | Description |
|---|---|
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
| Name | Type | Description |
|---|---|---|
source | string | the patch chunk |
baseGeneration | integer | the generation the patch was built against |
Returns
| Type | Description |
|---|---|
hotreload.Transaction? | the prepared transaction, or nil when it was rejected |
string? | why it was rejected, when it was |