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 or load_package. 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).

load_package adds a namespace ahead of the relative path (lex-schema/src/error.lex), because a relative key is only unique within one package: two packages published into one branch can both have a src/error.lex, and without the namespace both get the same error_<hash>.format.

§Whole-package loading

load_program and load_program_with_root each flatten one entry’s entire local-import closure into that entry’s program, which is what lex run/lex check want for a single file. A caller holding every file of a package — a publish server, say — gets each shared dependency back once per importer instead: 2,239 declarations for 693 distinct names on a real 21-file package whose error.lex 17 files import (#828). load_package is the whole-package entry point: one shared pass, every file exactly once, and every file mangled (no unmangled entry), since bare names from different files would collide in one program.

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, and under load_package that applies to every declaration, not only imported ones — a function moved between two files of a package is a new function there. 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.

Structs§

LoadedPackage
A package loaded as one unit by load_package.

Enums§

LoadError

Functions§

load_package
Load a whole package as one program: every file gets its path-derived mangling prefix (no file is the unmangled “entry”), and each file’s declarations appear exactly once however many other files import it.
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.