Skip to content

v2026.3.22 — use jan: Multi-File Programs

Released: March 22, 2026

Janus programs can now span multiple files. This is the G-01 deliverable — the import resolution path from spec to working binary in a single session.

math.jan
func add(a: i64, b: i64) -> i64 do
return a + b
end
main.jan
use math
func main() -> i64 do
return math.add(21, 21)
end
Terminal window
$ janus build main.jan -o program
Imported 1 functions from Janus module 'math.jan'
SPEC-041: Module 'math' CID=9415369dac44c07a...
$ ./program; echo $?
42

Three new AST node kinds — use_jan, import_list, import_alias — and full syntax support:

use math // basic import
use math.trig // dotted path
use math { sin, cos } // selective import
use math { sin as trig_sin } // aliased selective
use crypto.hash as h // module alias
use .sibling // relative import (single dot)
use ..parent.child // relative import (double dot)

19 parser tests verify all forms.

Content-Addressed Module Identity (Phase 2)

Section titled “Content-Addressed Module Identity (Phase 2)”

Every module gets a BLAKE3 CID — a cryptographic hash of its canonical AST. Identity is content, not filesystem path. Moving a file doesn’t change its CID; changing a function does.

Module 'math' CID=9415369dac44c07a643863356137d5fe5e768d23291526d5ebe159e7ea46252c

The ModuleIndex tracks path aliases to CIDs. The Cid.computeModule() API is available for tooling.

4 golden tests verify determinism, path-independence, and import-independence.

The collectModuleImports() sema pass resolves use_jan nodes to ASTDB compilation units. Module paths match against unit paths (math matches math.jan or src/math.jan). Unresolved imports are tracked for error reporting.

7 sema tests verify resolution and edge cases.

The pipeline discovers imported files, parses them into the ASTDB, lowers ALL units into QTJIR, and emits a single LLVM module. Cross-module function calls resolve because the LLVM emitter pre-declares all symbols before emitting bodies.

Qualified calls (math.add()) work through an imported_modules set in the lowering context — the field-call path recognizes module names and skips UFCS receiver probing.

Cycle detection prevents infinite recursion on circular imports.

  • Parser single-identifier quirkuse math (bare, no dots) now correctly emits an edge child. Branch 2 condition changed from negated (NOT dot, NOT brace) to positive (IS string_literal) for FFI form detection.
  • Monomorphized generic return typestruncate[u8] now correctly returns i8 in LLVM instead of void. Added isJanusIntegerType() helper covering all i8-u64 types.
  • Speculative stderr suppressed — UFCS receiver probing no longer emits “undefined variable” diagnostics when the receiver is a module name.

1,771 tests passing. Green build.

SuiteTestsCoverage
test-parser-trivia21All use_jan syntax forms
test-module-cid4CID determinism, path-independence
test-module-index6ModuleIndex CRUD operations
test-sema-imports7Import resolution, missing modules
test-import-e2e6End-to-end multi-file compilation
test-conv-e2e9Generic type conversion (fixed)

The work follows SPEC-041: Content-Addressed Module Import Resolution, which defines:

  • Three-layer resolution: path alias -> module index -> content store
  • BLAKE3 CID computation via canon.zig deterministic encoder
  • Error codes E4100-E4106 for import validation
  • Name mangling scheme for ecosystem-scale collision avoidance (future)
  • Snapshot unit-0 hardcoded — each imported module uses its own temporary ASTDB. True shared-ASTDB multi-unit requires a Snapshot refactor.
  • Selective imports not enforced at loweringuse math { add } parses correctly but lowering imports all functions. Enforcement is a Phase 3 follow-up.
  • No visibility enforcement — non-pub functions are currently importable. E4101/E4106 errors are specified but not yet emitted.
  • pub use re-exports — parser support deferred; requires top-level statement dispatcher changes.

“A module is not where it lives. A module is what it is.”