0139 - A reload links its new version into the cached module record
Date: 2026-10-14
Area: modules, gc
Related: ADR 0133, ADR 0136
Context
The module loader reloads a cached module when its content provider reports a
newer modification time (TGocciaModuleLoader.CheckForModuleReload). It used
to load the new version as a second TGocciaModule, copy that record's export
bindings into the cached record, and free the second record. Importers and
namespace objects hold the cached record, so copying kept them working, but the
freed record was still in use:
- The reload is linked inside the request that found the module in the cache,
so it is evaluated later, by that request. The load state the evaluation
reads, and the evaluation context in it, named the freed record. The bytecode
VM writes each
exportit runs into the context's module, so the first reloaded version with a local export wrote into freed memory (#1647). - A module linked for the first time during the reload, which imports the reloading module back, was bound to the freed record.
- The cached record took the new version's environment scope as its own, then
freeing the second record retracted that scope's collector root.
AddRootObjectkeeps a set, not a count (ADR 0133), so nothing rooted the environment the cached record now read its exports from.
Every reload also evaluates a module whose namespace object is already alive
while its exports are uninitialized. Marking a namespace object used to read
each export's value, and reading a binding in its temporal dead zone raises,
so a collection during the reload's evaluation threw a ReferenceError out of
the collector. #1695 made an export binding mark its environment or value
without reading it, which a reload relies on.
Decision
A JavaScript module keeps one record for as long as its loader caches it. A reload links and evaluates the new version into that record:
CheckForModuleReloadnames the cached record as the reload target for its cache key, andInstantiateModulelinks the new version into it instead of creating a record. Every reference the reload creates, from the load state, the evaluation context and the modules it links, names the cached record.TGocciaModule.BeginReloadsets the record's exports, environment, evaluation promise and modification time aside, and leaves the record empty for the new version. The record's root source marks what was set aside, and the old environment stays registered as a root until the reload ends.- A reload is linked inside the request that found the module in the cache,
and a module that request links after it, or the request's own check of
its imported names, can still fail. So the reload is committed only when
the outermost
LoadModuleorLinkModuleRequesthas linked its whole graph:CommitReloadthen releases what was set aside and retracts the old environment's root. When the new version or anything else in that link fails,RollbackReloadputs the previous version back and the loader caches the record again, still older than its source, so the next request retries.
A data module (JSON, text, bytes, or a format a runtime extension loads) is
still built as a new record, because extensions create their own records.
Nothing but the reload refers to that record, and it holds only values, so
TGocciaModule.TakeExportsFrom moves its bindings and table into the cached
record before the loader frees it. The move transfers the environment's root
rather than sharing it.
Consequences
LoadModulereturns the same record before and after any number of reloads. Importers, namespace objects and code that captured the record read the newest version.- Functions, classes and objects an earlier version created keep reading the environment they were created in. Once nothing references an old environment, the collector frees it with everything created in it.
- A version that fails to link, or links inside a request that then fails, leaves the previous version in place. A version that links and then throws while evaluating fails the way a first load does: the loader records the error and retires the record.
- The engine keeps every version's compiled code for its lifetime, as it does
for eval and
Functioncode (ADR 0134), so each reload still adds that code to the engine's memory. Goccia.Modules.Reload.Testreloads modules repeatedly and collects garbage after every reload. It covers importers, old closures and class instances, changed exports, a module linked during a reload that imports the reloading module, a reload that fails to link, a reload whose enclosing link fails afterwards (a later import, or a static request's missing name), JSON modules, and the live object count across 200 reloads.tests/language/modules/namespace-collection-during-evaluation.jscollects while a module that imports its own namespace is still evaluating.