v5 Linker: merging module bytecode
Lesson, slides, and applied problem sets.
View SlidesLesson
v5 Linking: merging module bytecode
Linking is where independent object modules become one executable program.
Why a linker is mandatory in a multi-module compiler
Each module compiles functions and closures with local function IDs. When executed separately, MAKE_CLOSURE 0 inside each module refers to different local functions. Linker solves this by creating a single global namespace.
1) Invariants the linker must enforce
- module names are unique
mainexists and is last- every import resolves to a known earlier module (no forward import)
- function IDs become unique in merged space
- closure immediates are rewritten to merged IDs
- concatenated init code keeps deterministic runtime order
These are not optional safety checks—they are the correctness surface of the linker.
2) ID rebasing mathematically
Assume module functions currently use [0..n-1]. Let offset be number of functions already emitted in prior modules.
For each function f in module m:
f.id' = f.id + offset
For each MAKE_CLOSURE k instruction encountered in:
- every function body
- module main/init body
- rewrite
k' = k + offset
Then increment offset += len(m.functions).
This is a deterministic, one-pass affine transform.
3) Why only MAKE_CLOSURE (today)
In this pack, function invocation goes through closure objects, so closure construction is the only place IDs are embedded today.
If a future version emits direct function-call IDs, CALL/jump targets would need rebasing too. That should be tracked in a separate linker version.
4) Dependency ordering as topological constraint
Rule in this pack: imports must refer only to earlier modules. That yields straightforward one-pass linking.
Implementation pattern:
- map module name -> index
- process modules in source order
- on import
name, require existence in map and index lower than current
This gives an implicit DAG guarantee without general cycle solving.
5) Main concatenation semantics
MAIN blocks are concatenated in module order, with main last.
That means:
- imported libraries initialize before dependents
- side effects become deterministic across runs
- execution starts from merged main program
If module order is deterministic and deterministic within each compiled module, final initialization output is deterministic.
6) Failure discipline and diagnostics
Linker should reject early and clearly:
- duplicate module names
- unknown module imports
- future import targets
- missing or misordered
main - function ID collisions before/after rewrite
- closure IDs that remain out of range
Good diagnostic practice:
- include module name/context
- keep first error deterministic for grader stability
7) Practice checkpoints (almost solved)
1) Offset arithmetic + Module A has 2 funcs, Module B has 1 func.
- A local ids:
0,1 - B local ids:
0 - with offset rewrite, B id becomes
2
2) Why main-only execution order?
main must be last so it can depend on all imported modules in this v5 contract.
3) Future import rejection
If module B imports later module C, linker should reject before compile/link merge.
4) Closure rewrite scope
Rebase MAKE_CLOSURE inside both function bodies and module main code.
5) Deterministic IDs
len(totalFunctions) in final bytecode must equal sum of per-module function counts.
6) Invalid linkage case
Unknown import target should fail in linker validation (semantic stage only can’t resolve implementation shape differences).
Linker is the last big compiler step that can produce a fully deterministic executable artifact before runtime.
Module Items
Bytecode Linker v5