v5 Bytecode: compiling modules
Lesson, slides, and applied problem sets.
View SlidesLesson
v5 Bytecode: compiling modules
Modules now lower into a per-module executable artifact with explicit import/export boundaries. This is the bridge between semantic module contracts and linker/runtime execution.
1) Module object shape and bytecode shape
A compiled module contains:
- module name
- import metadata
- exported symbol list
- function table fragment
- module initialization bytecode (
Main)
Why this shape:
- import side effects and bindings are separated from value emission
- linker can combine modules by explicit export objects
- runtime does not need special-case module opcodes
2) Deterministic module init lowering
A canonical compile pattern:
ENTER_SCOPE
emit import bindings
emit module body declarations/expressions/statements
collect exports in deterministic order
for each exported name:
PUSH_STR name
LOAD name
MAKE_MAP N
DEFINE_CONST <moduleName>
EXIT_SCOPE
Important:
ENTER_SCOPE/EXIT_SCOPEisolate module locals.- exports are captured as a map value so
moduleis a value object, not global namespace magic. - even empty export sets still emit
MAKE_MAP 0so module shape is consistent.
3) Import lowering and alias semantics
import math and import math as m become alias bindings in module scope.
- if alias omitted, alias defaults to module identifier (
math) - alias binding is generally immutable in this version (
const-style) - analyzer already validates import existence/order; compiler translates that into direct alias definitions
Do not flatten imports. Keep import references as aliases that participate in existing identifier and indexing rules.
4) Export lowering and ordering
Exports must remain declaration-order deterministic. That means
- collect exported names during semantic phase
- emit map entries in stable order
- emit
PUSH_STR/LOADpairs beforeMAKE_MAP
If the order changes, bytecode diffs become unstable and grader snapshots become noisy.
5) Member access as map read
No new instruction needed for m.name:
LOAD m
PUSH_STR "name"
GET_INDEX
This keeps module and map systems unified:
- same opcode path for object-like access
- same runtime contract for unknown keys
- less VM surface area to debug and test
Function-call on member: m.name() lowers to member read + CALL.
6) Module values and link composition
DEFINE_CONST <moduleName> stores module object in local scope after body emission.
That module object contains at least:
- exports map
- function references
- any captured runtime constants for module-local initialization behavior
Linker can then compose by module-import dependency without re-parsing source.
7) Invariants worth writing tests for
- each module compiles exactly once, no re-parse in compiler path
- import aliases are bound before use
- module scope does not leak locals beyond scope end
- empty export module still produces valid deterministic object
- map construction count matches declared export count
8) Practice checkpoints (almost solved)
1) Empty export map + Module with no exports should still emit MAKE_MAP 0 and be loadable.
2) Import default alias + import math; and import math as m; both bind aliases and are available in member expressions.
3) Export ordering + export let a, export let b should emit map entries in a, then b.
4) Member lowering + m.x must become LOAD m, PUSH_STR "x", GET_INDEX in that order.
5) Module object binding + A compiled module should bind as immutable module constant in module init scope.
This module keeps module behavior explicit and stable before linker-level complexity increases.
Module Items
Compile to Bytecode v5