Module resolution & composition (@gram-lang/modules)
The @gram-lang/modules package forms the second stage in the Gram pipeline. It takes an entry RecipeAST containing @use import directives and transforms the multi-file dependency graph into a single, self-contained composed AST ready for compilation.
flowchart LR
Raw["📄 Root .gram"] --> Parser["⚡ @gram-lang/parser"]
Parser --> AST["AST with @use"]
Deps["📄 Imported .gram files"] --> Modules["📦 @gram-lang/modules<br/><i>DFS, Scaling, Renaming</i>"]
AST --> Modules
Modules --> ComposedAST["📄 Composed AST<br/><i>Single Unified Tree</i>"]
ComposedAST --> Kitchen["⚙️ @gram-lang/kitchen"]
Why compose at the AST level?
Section titled “Why compose at the AST level?”In traditional software compilation, modules can be compiled into separate object files and linked later. In computational cooking, however, compiling modules independently produces incorrect schedules:
- ALAP (As Late As Possible) Scheduling: If a pie crust requires 1 hour of chilling and 20 minutes of blind baking, its timeline must interleave dynamically with fillings and custards in the host recipe. Compiling the crust into fixed times beforehand would destroy this global scheduling optimization.
- Relational Ingredients & Composites: Ingredients in sub-bases (like butter and flour) must merge into the host’s purchasing shopping list, respecting composite rules (
<@lemon) and unit conversions across the entire dish.
By composing at the AST level before @gram-lang/kitchen runs, Gram compiles and schedules the entire meal as a single coherent recipe in one pass.
Host abstraction (ModuleHost)
Section titled “Host abstraction (ModuleHost)”@gram-lang/modules contains zero filesystem I/O or platform-specific logic. It defines the ModuleHost interface, leaving file reading and path resolution to the environment:
- CLI (
@gram-lang/cli): Resolves project-root (@/...) and alias paths (@bases/...) usingnode:fsandnode:path. - Language Server (
@gram-lang/language-server): Reads from active in-memory editor buffers before falling back to disk. - Browser Playground (
packages/docs): EmployscreateMemoryHostwith a virtual in-memory file dictionary, allowing live multi-tab editing without a filesystem.
1. Graph traversal & cycle detection (graph.ts)
Section titled “1. Graph traversal & cycle detection (graph.ts)”When loadModuleGraph(entryUri, host) is called, it performs a single depth-first search (DFS) traversal:
- Cycle Detection: Maintains a traversal stack
pathStack. If a dependency matches an active parent URI, it records a non-fatalMODULE_CYCLEdiagnostic (A → B → A) and terminates that branch without crashing. - Diamond Dependencies: Each URI is loaded and parsed at most once. If recipes
AandBboth import baseC,Cis loaded once and stored in themodulesregistry. - Topological Ordering: Modules are pushed to
orderas DFS post-order visits complete (leaves first). This guarantees that child dependencies are measured and composed before their parents.
2. Public exports & intermediate hygiene (exports.ts, rename.ts)
Section titled “2. Public exports & intermediate hygiene (exports.ts, rename.ts)”Export rules
Section titled “Export rules”Only section-level intermediate declarations (## Pastry Dough ->&crust) are considered public exports:
- Step-level intermediate declarations (
->&chopped) remain strictly private to their module. - If a module has no section-level declaration, its last section is deterministically selected as the
defaultexport.
Variable renaming & hygiene
Section titled “Variable renaming & hygiene”To prevent name collisions when multiple files declare common names (e.g. &dough or &mixture):
- Bound Exports: Renamed directly to the local identifier chosen in the host’s
@useline (@use "./base.gram" as &myDough). - Internal Variables: Prefixed using the binding name (e.g.
&doughbecomes&myDough$dough). - Collision Safety Net: After renaming,
checkRenameCollisions()validates all identifiers post-slugification to ensure no distinct intermediates accidentally merge.
3. Physical yield measurement & proportional scaling (yield.ts)
Section titled “3. Physical yield measurement & proportional scaling (yield.ts)”When an importing recipe requests a specific quantity of an imported base (e.g. &crust{300g}):
flowchart TD
ModuleAST["Base Module AST"] --> PreCompile["Pre-Compile & Analyze<br/><i>(Leaves First)</i>"]
PreCompile --> MeasureYield["resolveYield()<br/><i>Recursive Mass Sum</i>"]
MeasureYield --> Compare["Host Request (300g)<br/>vs Base Yield (400g)"]
Compare --> Factor["Scale Factor = 0.75<br/><i>scaleAst(depAst, 0.75)</i>"]
resolveYield: Recursively measures the total physical mass in grams (metrics.totalMass) produced by the target export section and any upstream sections feeding into it.computeScaleFactor: Evaluates the ratio:- AST Scaling:
scaleAst()multiplies every numeric ingredient and timer in the sub-tree before splicing it into the host.
4. Inline mode vs. stock mode (--stock)
Section titled “4. Inline mode vs. stock mode (--stock)”Gram supports two execution modes for modular dependencies:
| Feature | Inline Mode (Default) | Stock Mode (--stock) |
|---|---|---|
| Timeline / Gantt | All sub-recipe steps are spliced into the host schedule and interleaved via ALAP. | Zero timeline cost: steps are omitted entirely. |
| Shopping List | Raw ingredients (flour, butter) bubble up to the master shopping list. | Appears as a single purchasable unit (e.g., 1 pastry dough). |
| Nutritional Profile | Aggregated from individual raw ingredients. | Calculated via synthetic ingredient records weighted by accountedMass / yieldBasisMass. |
In stock mode, composeRecipe() generates synthetic ingredient definitions (syntheticIngredients) carrying the module’s real nutritional profile and unit weight. When passed to @gram-lang/analyzer, the host recipe maintains complete nutritional accuracy even though preparation steps were skipped.