Module / Program / Import / Export
The four node kinds that model cross-module structure:
ProgramNode — top-level container (one per compilation); graph of modules
ModuleNode — a single compilation unit
ImportNode — cross-module dependency
ExportNode — cross-module export
These four are documented together because they form an integrated system: a ProgramNode contains ModuleNodes; each ModuleNode has ImportNodes and ExportNodes that reference other ModuleNodes.
ProgramNode
class ProgramNode : public EMPLNode {
public:
struct ModuleEntry {
std::string id;
std::shared_ptr<ModuleNode> module;
std::vector<std::string> dependencies;
bool isEntry = false;
};
std::vector<ModuleEntry> modules;
std::string entryModuleId;
ProgramNode() : EMPLNode(NodeKind::Program) {}
void accept(EMPLVisitor& v) override { v.visit(*this); }
std::shared_ptr<ModuleNode> findModule(const std::string& id) const;
std::vector<std::string> topologicalOrder() const;
};
Field reference
| Field |
Type |
Source |
Description |
kind |
NodeKind::Program |
EMPLNode.h:115 |
Discriminator. |
modules |
vector<ModuleEntry> |
EMPLNode.h:112 |
All modules in the program. MAY be empty (a program with no modules is valid but useless). |
entryModuleId |
std::string |
EMPLNode.h:113 |
The id of the entry-point module (typically src/index.js or the main file path). Empty string if no entry (library mode). |
ModuleEntry struct
| Field |
Type |
Source |
Description |
id |
std::string |
EMPLNode.h:106 |
Unique module identifier — typically a normalized absolute path (/abs/path/to/src/index.js) or a synthetic key for dynamically-loaded modules. |
module |
shared_ptr<ModuleNode> |
EMPLNode.h:107 |
The actual module node. |
dependencies |
vector<string> |
EMPLNode.h:108 |
IDs of modules this module directly depends on. Populated from ImportNodes after resolution. |
isEntry |
bool |
EMPLNode.h:109 |
true if this module is the entry point. Exactly one ModuleEntry in modules should have isEntry = true. |
Methods
| Method |
Source |
Description |
accept(EMPLVisitor& v) override |
EMPLNode.h:117 |
Standard visitor dispatch. |
findModule(id) |
EMPLNode.h:120 |
Linear search for a module by id. Returns nullptr if not found. |
topologicalOrder() |
EMPLNode.h:128 |
Returns module IDs in dependency order (dependencies before dependents). Used by the C++ backend to emit module definitions in the correct order. Uses DFS with cycle detection (no-op on cycles). |
Usage
auto program = std::make_shared<mpl::ir::ProgramNode>();
program->entryModuleId = "src/index.js";
mpl::ir::ProgramNode::ModuleEntry entry;
entry.id = "src/index.js";
entry.module = std::make_shared<mpl::ir::ModuleNode>();
entry.module->name = "index.js";
entry.dependencies = { "src/util.js" };
entry.isEntry = true;
program->modules.push_back(entry);
for (const auto& id : program->topologicalOrder()) {
auto module = program->findModule(id);
}
Validation rules
| Rule |
Constraint |
| V1 |
modules MAY be empty (rare but allowed). |
| (Implicit) |
Exactly one ModuleEntry.isEntry = true is expected (validator emits a warning if 0 or >1). |
ModuleNode
class ModuleNode : public EMPLNode {
public:
std::string name;
std::string sourceLanguage;
std::vector<EMPLNodePtr> imports;
std::vector<EMPLNodePtr> declarations;
std::vector<EMPLNodePtr> statements;
ModuleNode() : EMPLNode(NodeKind::Module) {}
void accept(EMPLVisitor& v) override { v.visit(*this); }
};
Field reference
| Field |
Type |
Source |
Description |
kind |
NodeKind::Module |
EMPLNode.h:96 |
Discriminator. |
name |
std::string |
EMPLNode.h:90 |
The module identifier (typically the filename without path, e.g. "index.js"). Required. |
sourceLanguage |
std::string |
EMPLNode.h:91 |
The source language name (e.g. "javascript", "python"). Canonicalized by canonicalizer rule C5 — empty/unknown is removed. |
imports |
vector<EMPLNodePtr> |
EMPLNode.h:92 |
List of ImportNodes. Each entry declares a dependency on another module. MAY be empty. |
declarations |
vector<EMPLNodePtr> |
EMPLNode.h:93 |
Top-level declarations: FunctionDeclNode, ClassDeclNode, InterfaceDeclNode, EnumDeclNode, TypeAliasDeclNode, VariableDeclNode. MAY be empty. |
statements |
vector<EMPLNodePtr> |
EMPLNode.h:94 |
Top-level executable statements: BlockNode, IfNode, ExpressionStmt, StandardLibraryCallNode, etc. MAY be empty. |
Source mapping
| Source file |
Field config |
index.js |
name="index.js", sourceLanguage="javascript", imports=[…], declarations=[…], statements=[…] |
utils.py |
name="utils.py", sourceLanguage="python", imports=[…], declarations=[…] |
Validation rules
| Rule |
Constraint |
| V1 |
name MUST be non-empty. |
| C5 (canonicalizer) |
sourceLanguage MUST NOT be "unknown" or empty in canonical form. |
Backend lowering (C++)
Each module becomes a single translation unit (.cpp file) emitted by the C++ backend. The backend names the file using the module id (from the enclosing ProgramNode::ModuleEntry::id).
The module's imports are emitted as #include directives (with the include path computed from the imported module's id). The declarations and statements are emitted in declaration order.
ImportNode
class ImportNode : public EMPLNode {
public:
struct ImportBinding {
std::string importedName;
std::string localName;
bool isDefault = false;
bool isNamespace = false;
};
std::string module;
std::vector<ImportBinding> bindings;
bool isSideEffectOnly = false;
std::string attributes;
ImportNode() : EMPLNode(NodeKind::Import) {}
void accept(EMPLVisitor& v) override { v.visit(*this); }
};
Field reference
| Field |
Type |
Source |
Description |
kind |
NodeKind::Import |
EMPLNode.h:648 |
Discriminator. |
module |
std::string |
EMPLNode.h:643 |
The module specifier (e.g. "./foo", "lodash", "@babel/core"). Required. |
bindings |
vector<ImportBinding> |
EMPLNode.h:644 |
The imported names + local aliases. MAY be empty (side-effect-only import). |
isSideEffectOnly |
bool |
EMPLNode.h:645 |
true for import "module" (no bindings, just side effects). |
attributes |
std::string |
EMPLNode.h:646 |
Free-form attributes string (e.g. "type: \"json\"" for JSON imports). Empty if no attributes. |
ImportBinding struct
| Field |
Type |
Source |
Description |
importedName |
std::string |
EMPLNode.h:637 |
The name as exported from the source module (e.g. "default", "foo", "*"). |
localName |
std::string |
EMPLNode.h:638 |
The local binding name in the importing scope. |
isDefault |
bool |
EMPLNode.h:639 |
true for default imports (import foo from "..."). |
isNamespace |
bool |
EMPLNode.h:640 |
true for namespace imports (import * as ns from "..."). |
Source mapping
| Source |
Field config |
import foo from "./bar" (ESM) |
module="./bar", bindings=[{importedName:"default", localName:"foo", isDefault:true}] |
import { a, b as c } from "./bar" (ESM named) |
module="./bar", bindings=[{importedName:"a", localName:"a"}, {importedName:"b", localName:"c"}] |
import * as ns from "./bar" (ESM namespace) |
module="./bar", bindings=[{importedName:"*", localName:"ns", isNamespace:true}] |
import "./bar" (side-effect only) |
module="./bar", bindings=[], isSideEffectOnly=true |
import "./bar" with { type: "json" } |
module="./bar", isSideEffectOnly=true, attributes="type: \"json\"" |
const foo = require("./bar") (CJS) |
module="./bar", bindings=[{importedName:"*", localName:"foo"}] |
from bar import a, b as c (Python) |
module="bar", bindings=[{importedName:"a", localName:"a"}, {importedName:"b", localName:"c"}] |
require "foo" (Ruby) |
module="foo", bindings=[{importedName:"*", localName:"foo"}] |
use crate::bar; (Rust) |
module="crate::bar", bindings=[{importedName:"*", localName:"bar"}] |
import "fmt" (Go) |
module="fmt", bindings=[{importedName:"*", localName:"fmt"}] |
Validation rules
| Rule |
Constraint |
| V1 |
module MUST be non-empty. |
| V10 |
If isSideEffectOnly = true, bindings MUST be empty. |
| V10 |
If bindings is non-empty, each ImportBinding::localName MUST be non-empty. |
Backend lowering (C++)
The backend uses the dependency graph from ProgramNode::topologicalOrder() plus the module specifier to compute the include path:
#include "bar.gen.h"
namespace _mpl_import_bar { }
auto foo = _mpl_import_bar::default_export;
#include "bar.gen.h"
auto ns = _mpl_import_bar::make_namespace();
The actual lookup of the imported module is performed by the MPL_Compiler module resolver before canonicalization.
ExportNode
class ExportNode : public EMPLNode {
public:
struct ExportBinding {
std::string localName;
std::string exportedName;
};
std::vector<ExportBinding> bindings;
EMPLNodePtr value;
bool isDefault = false;
bool isReexport = false;
std::string reexportModule;
ExportNode() : EMPLNode(NodeKind::Export) {}
void accept(EMPLVisitor& v) override { v.visit(*this); }
};
Field reference
| Field |
Type |
Source |
Description |
kind |
NodeKind::Export |
EMPLNode.h:669 |
Discriminator. |
bindings |
vector<ExportBinding> |
EMPLNode.h:663 |
Named exports. MAY be empty (default / re-export). |
value |
EMPLNodePtr |
EMPLNode.h:664 |
The exported value (for default export or module.exports = …). MAY be nullptr. |
isDefault |
bool |
EMPLNode.h:665 |
true for export default …. |
isReexport |
bool |
EMPLNode.h:666 |
true for export { x } from "module". |
reexportModule |
std::string |
EMPLNode.h:667 |
Module specifier for re-export. Required if isReexport = true. |
ExportBinding struct
| Field |
Type |
Source |
Description |
localName |
std::string |
EMPLNode.h:659 |
Local binding name (as declared in the module). |
exportedName |
std::string |
EMPLNode.h:660 |
The name as exported (may differ via as). |
Source mapping
| Source |
Field config |
export const x = 5 (ESM) |
bindings=[{localName:"x", exportedName:"x"}] (no separate ExportNode — the VariableDeclNode for x is marked exported via metadata, OR an ExportNode is emitted alongside it for clarity) |
export { foo, bar as baz } (ESM named) |
bindings=[{localName:"foo", exportedName:"foo"}, {localName:"bar", exportedName:"baz"}] |
export default 42 (ESM) |
isDefault=true, value=Literal(42) |
export { x } from "./other" (ESM re-export) |
isReexport=true, reexportModule="./other", bindings=[{localName:"x", exportedName:"x"}] |
export * from "./other" (ESM namespace re-export) |
isReexport=true, reexportModule="./other", bindings=[{localName:"*", exportedName:"*"}] |
module.exports = { foo, bar } (CJS) |
value=ObjectLiteral([("foo",…), ("bar",…)]) |
module.exports.foo = 42 (CJS) |
value=AssignmentNode(MemberAccessNode(module, "exports.foo"), Literal(42)) — typically the assignment is in the statements list and the ExportNode is omitted |
__all__ = ["foo", "bar"] (Python) |
NOT an ExportNode — Python module-level bindings are exported implicitly; canonicalizer may emit ExportNodes for __all__-listed names |
pub fn foo() {} (Rust) |
NOT an ExportNode — Rust pub modifiers are encoded as metadata["visibility"]="pub" on the declaration |
func (Foo) Bar() {} (Go) |
NOT an ExportNode — Go capitalized method names are exported implicitly; canonicalizer may emit ExportNodes |
Validation rules
| Rule |
Constraint |
| V10 |
Exactly one of: isDefault = true, isReexport = true with reexportModule non-empty, bindings non-empty, OR value != nullptr. |
| V10 |
If isReexport = true, reexportModule MUST be non-empty. |
Backend lowering (C++)
For named exports: an entry in the module's generated *.gen.h:
namespace _mpl_module_index_js {
inline int x = 5;
}
For default exports: a default_export symbol in the namespace:
namespace _mpl_module_index_js {
inline mpl_value default_export = 42;
}
For re-exports: a thin alias:
namespace _mpl_module_index_js {
namespace _from_other = _mpl_module_other_js;
using _from_other::x;
}
See also