Skip to main content

Module loader

Module loader 

Source
Expand description

Multi-file loader: resolves import "./...", import "../...", and import "/abs/..." statements relative to the importer, recursively parses, and produces a single Program with all stages merged.

Names that are local to an imported file are mangled with a per-file-path prefix, so the same module imported via multiple aliases (or from multiple parents in a diamond shape) collapses to one set of mangled names — same SigId, same nominal identity. Stdlib imports (import "std.foo" as bar) pass through unchanged.

§Mangling

Each loaded file gets a prefix derived from its filesystem path. The entry file’s prefix is empty (so lex run main.lex process works unchanged). Imported files use <stem>_<hash> where hash is the first 8 hex chars of SHA-256 of the file’s mangling key. The hash disambiguates same-stem files in different directories without forcing a project manifest.

The mangling key is the canonical absolute path by default, and the path relative to a caller-supplied root when loading through load_program_with_root. Absolute paths are only stable as long as the tree stays put, which makes them unusable for anything that loads the same logical package from a fresh directory each time: a server unpacking an uploaded package into a per-request temp dir got a different prefix — and therefore a brand-new set of function names — for every file reached through a local import on every single request, so byte-identical republishes diffed as all-new functions and grew the branch’s function set without bound (#826). Pass the package root and the key becomes src/error.lex, identical across requests. Files outside the root keep the absolute-path key (a dependency in the shared package cache lives at a stable absolute path of its own, and “relative to this package” says nothing useful about it).

Within a file at prefix P:

  • fn foo declared in this file becomes <P>.foo (just foo at root).
  • type T declared in this file becomes <P>.T.
  • References to a locally-declared name get mangled, unless the name is shadowed by a binder (let, fn param, lambda param, or pattern binder) in scope.
  • m.foo where m is a path-import alias is rewritten to the imported file’s prefix-qualified name. Two parents importing the same file see the same prefix → calls and types unify.
  • m.foo where m is a stdlib alias is unchanged.

Variant constructors are not mangled — they live in a global namespace, and a collision between two imported types’ constructors surfaces later as a type-check error. Same for record field names.

§Diamond imports

main.lex imports ./left and ./right, both of which import ./shared. shared.lex is parsed once per resolution, but its mangled items are merged into the output exactly once (subsequent loads from the same canonical path return an empty Program). This is what makes s.build_report(...) and v.read_score(...) agree on Report’s nominal identity.

§Limitations (tracked separately)

The mangling key is a filesystem path (see above). Moving a file changes its SigId; renaming changes the file-stem half of the prefix. A root-relative key narrows this to moves within the package, but does not remove it. The eventual fix — content-addressed identity decoupled from filesystem layout — lives with store-native imports (import "stage:..."); see the corresponding follow-up tracker.

Enums§

LoadError

Functions§

load_program
Load a multi-file Lex program, expanding local imports relative to the entry path. Stdlib imports (std.*) pass through unchanged.
load_program_from_str
Load a Lex program from a string source. Local-path imports are rejected up-front since there’s no base path to resolve from.
load_program_with_root
Load a multi-file Lex program like load_program, but derive mangling prefixes from each file’s path relative to root instead of its absolute path.