v5 Bytecode: compiling modules
Work through examples, try the reasoning yourself, then put it into practice.
View SlidesBefore you start
- Validate v5 module order/visibility and understand BC2 closures.
What you will practice
- Derive module initialization, imports, and export-map stack effects.
- Distinguish exported values from live bindings.
- Produce reusable local-ID artifacts for relocation.
In this lesson
Lesson
3 min readv5 Bytecode: compiling modules
CompileModules(prog) returns one ModuleBytecode per source module. Each
contains Name, Imports, Exports, and Bytecode. Compile each module with a fresh
local function-ID allocator; do not globally number functions yet. Preserve
module order and export declaration order.
Worked reconstruction
For module util { export let answer=7; }, initializer code is:
ENTER_SCOPE
PUSH_NUM 7
DEFINE_VAR answer
PUSH_STR "answer"
LOAD answer
MAKE_MAP 1
EXIT_SCOPE
DEFINE_CONST util
The map remains on the operand stack while EXIT_SCOPE discards the temporary module environment from the current frame. DEFINE_CONST installs that map in the surrounding global scope. A function exported from this environment can keep it alive through its closure capture even after EXIT_SCOPE.
For import util as u, the importing module emits LOAD util then DEFINE_CONST
u inside its own initializer scope. u.answer becomes LOAD u, PUSH_STR answer,
GET_INDEX. No new VM member opcode is required; the analyzer already checked
visibility and the compiler uses ordinary map indexing.
Exported value versus binding
Suppose util exports var n=0 and a function inc that updates n. The export map
receives the current primitive value 0 during initialization. A later inc call
changes the captured environment's n but does not rewrite the map entry for n.
A function export and a shared array export retain object references. These
are this implementation's value exports, not a live-binding module system.
Relocatable artifact invariant
Each function's ID is local and dense in 0..N-1, although nested-function append order can differ from ID order. Each Main jump points into that module's own Main slice; each function jump points into its own function code. CompileModules must not guess the offsets of earlier modules' emitted code. The linker owns those offsets and must patch the appropriate operands without changing the reusable original artifacts.
Debugging lab
A compiler exits the module scope before loading exported values, then tries to build its map. Another puts imported util directly into the importer’s outer globals under alias u. What lifetime/scope information was lost?
Compare your diagnosis after attempting the lab
Build the export map while its bindings remain accessible, keep the map on the stack, then exit the module scope. Bind the import alias in the importer’s module scope so unrelated modules do not share or collide on that local alias.
Transfer task
Derive initializer instructions for a module exporting one function and one array. Trace the map stack shape, identify which values retain objects/environments, and annotate all operands that the linker will relocate.
Carry forward
Carry independent module artifacts with local coordinates into Link. Keep a saved copy so repeated linking can prove the originals were not modified.
Build compile-bytecode-v5. Keep a before/after trace of one failing test. Use the checkpoint to diagnose specific skills; passing it does not replace implementing and testing the artifact.
Practice sequence
Use the order below for a guided path, or choose an item to revisit. A pass records a successful attempt; try a fresh example later to check what you can reconstruct.
- Step 1
Compile to Bytecode v5
- Step 2
v5 Bytecode: compiling modules Checkpoint
Predict fresh artifacts, diagnose a broken invariant, and test a version boundary. Select one answer per multiple-choice question; numeric responses use the stated integer format. Use review links to revisit the relevant representation, then attempt the independent transfer task.