Lesson 7.1

Why a Module System?

What breaks when every program is one file. What a module system gives you.

concepts 8 min read

Every real program outgrows one file.

Before Phase 7, every Monk program was a single file. The compiler had one job: turn that file into a binary. Simple. But simple only scales to toy programs.

The moment you want to reuse a function across two programs, you copy-paste it. Now you have two copies. They drift. Bugs get fixed in one place, not the other. The codebase becomes a liability.

A module system solves this by making files the unit of reuse. You write a function once, export it, and any file that needs it can import it by name. The compiler resolves the dependency, checks the types across the boundary, and links everything into a single binary.

A module system is not just an organizational tool. It's a compilation boundary — a place where the compiler can verify that two pieces of code agree on the contract between them before combining them.

What Monk's module system actually does.

Monk modules are just .monk files. There is no special module declaration, no package keyword, no manifest file. A file becomes a module when another file imports from it.

The module system has three jobs:

1

Discover all dependencies.

Starting from the entry file, recursively find every file that is imported. Stop when all imports have been resolved. The result is a dependency graph.

2

Detect cycles.

If a.monk imports b.monk which imports a.monk, there is no valid compilation order. Monk rejects circular imports at compile time with a clear error message.

3

Produce a topological ordering.

Order the modules so every dependency comes before the files that depend on it. This ordering lets type-checking and code generation proceed in one left-to-right pass with no backtracking.

Everything compiles to a single C file. There is no separate compilation, no object files, no linker step for modules. The module system resolves dependencies at the Monk level; the C compiler sees one translation unit.

The updated pipeline.

Before Phase 7, the pipeline was linear: one file in, one binary out.

source.monk → parse → type-check → codegen → binary

After Phase 7, the pipeline forks based on whether the entry file has imports:

// No imports — single-file path (unchanged):
source.monk → parse → type-check → codegen → binary

// Has imports — multi-module path:
entry.monk
  → module.Build()       // DFS: resolve all files, detect cycles, topological sort
  → types.CheckModules() // check each module in dependency order
  → codegen.GenerateModules() // emit all modules as one C file
  → binary

The single-file path is completely unchanged. Existing programs compile identically. The module path only activates when the entry file contains a use statement.

The output is always one C file. Module boundaries exist at the Monk level for type-checking and namespace isolation -- at the C level, everything is a flat list of function definitions.

Module-level code runs once.

Any statement at the top level of a module (not inside a function) is initialization code. It runs when the program starts, in dependency order. If two files import the same module, its initialization code still runs only once.

// utils.monk
let BASE_URL = "https://api.example.com"
export BASE_URL

// main.monk
use BASE_URL from "./utils"
show(BASE_URL)   // "https://api.example.com"

This works because the dependency graph guarantees utils.monk's initialization code is emitted before main.monk's, and each module appears exactly once in the topological order.

Key takeaways

1

Monk modules are just files. No special syntax beyond use and export.

2

Circular imports are a compile error. The module resolver detects them at graph-build time, before any type-checking.

3

All modules compile to one C file. Module isolation is a Monk-level abstraction, not a linker-level one.

4

The single-file path is unchanged. Existing programs are unaffected.