v5 Linker: merging module bytecode

Lesson, slides, and applied problem sets.

View Slides

Lesson

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
  • main exists 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

    Link module bytecode blobs into a single program.

    medium Sign in to access medium and hard problems
Join Discord