0136 - A module's function declarations are compiled while it is linked, and read its imports by name
Date: 2026-10-13
Area: modules, bytecode compiler, bytecode VM
Related: ADR 0014, ADR 0134
Context
ES2026 §16.2.1.7.3.1 InitializeEnvironment creates a module's function declarations while the module is linked, before any module body runs, with the module environment as their environment. A module in an import cycle can therefore call another module's functions before that module's body has run.
The module loader did this with the tree-walk evaluator
(HoistFunctionDeclarations), in bytecode mode too. Bytecode compilation of
the module body happened later, when the module was evaluated, and read the
exported declarations from the environment instead of compiling them. Those
functions, and everything nested in them, tree-walked on every call, and the
VM had to carry code that hands objects between the two executors
(#1342). This coupling
has to go before the interpreter can be removed
(#825).
The problem is ordering. The functions must exist at link time, but the body whose compilation produces them used to be compiled only at evaluation time. A closure created at link time also has no module-body frame to capture. The body keeps a module's imports in registers of that frame, and the closures the body creates capture them as upvalues.
Decision
- The executor instantiates a module's function declarations. The loader
calls
TGocciaModuleLoader.InstantiateModuleFunctionswhere it used to call the evaluator.TGocciaEngine.Executemakes the same call for its entry module. The interpreter keepsHoistFunctionDeclarationsbehind this hook. Module linking no longer callsGoccia.Evaluator. - The bytecode executor compiles the module while it is linked. Each
top-level function declaration,
export default functionincluded, compiles to a template of the top-level template, marked with the binding it initializes (TGocciaFunctionTemplate.ModuleEnvironmentBinding). The executor creates each function from its template in the module environment (TGocciaVM.InstantiateModuleFunction). It keeps the compiled module in the evaluation context, and the body runs that module later. The body reads each declaration's function from the environment instead of creating it again, so a function has one identity from linking on. - A function created while linking reads its module's imports by name.
Every other top-level binding of a linked module is already read by name
from the module environment. A link-time function's scope now does the
same for an import (
TGocciaCompilerScope.ImportUpvaluesByName). If such a function would still capture anything from the body's frame, the compiler raises an internal error. Its upvalues are therefore never read, and the VM gives them cells of their own. - The module environment holds every import a link-time function can
read. Linking already binds evaluation-phase imports.
Executebinds the entry's as each requested module loads. A source-phase or deferred import is bound when its declaration runs, as the interpreter binds it. - An import read by name is cached at its read site.
OP_GET_GLOBALcaches the environment's import binding object. The cache is validated by the environment's lexical-map stamp, which changes whenever an import binding of the map is created or replaced.
The template field and the evaluation-context field are runtime-only. A
loader-linked module is compiled in process and never serialized, so the
.gbc format does not change.
Consequences
- No function of a module tree-walks in bytecode mode. An exported function's calls run on the VM, and so do the classes and closures it creates.
- A compile-time error of an imported module is raised while the module is linked, before any module of the graph evaluates, instead of when the module is evaluated.
- In
TGocciaEngine.Executewith a bytecode executor, an exported function of the entry module now reads the entry's imports. It used to throwReferenceErrorfor the import's temporal dead zone even after the body had run. - A link-time function's import read costs a cached binding lookup rather than an upvalue load followed by a namespace read. Measured, it costs less.
TGocciaClassValue.UsesOwnInstantiation,TryConstructOnReceiverand the VM's construct redirect stay for classes that other evaluator-run code builds, the bootstrap shims, until the interpreter is removed.