Elvelt

EMPL IR

Module / Program / Import / Export

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

// MPL_Compiler/core/ir/EMPLNode.h:103
class ProgramNode : public EMPLNode {
public:
    struct ModuleEntry {
        std::string id;          // Unique module identifier (normalized path)
        std::shared_ptr<ModuleNode> module;
        std::vector<std::string> dependencies;  // IDs of modules this depends on
        bool isEntry = false;    // True for the entry-point module
    };

    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";

// Add a module entry
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);

// Iterate in dependency order
for (const auto& id : program->topologicalOrder()) {
    auto module = program->findModule(id);
    // ...emit module definition...
}

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

// MPL_Compiler/core/ir/EMPLNode.h:88
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

// MPL_Compiler/core/ir/EMPLNode.h:634
class ImportNode : public EMPLNode {
public:
    struct ImportBinding {
        std::string importedName;  // What name is imported from the module
        std::string localName;     // Local binding name in current scope
        bool isDefault = false;    // true for default imports
        bool isNamespace = false;  // true for namespace imports (import * as ns)
    };

    std::string module;                      // Module specifier (e.g. "./foo", "lodash")
    std::vector<ImportBinding> bindings;     // Import bindings
    bool isSideEffectOnly = false;           // true for import "module" with no bindings
    std::string attributes;                  // Import attributes (e.g. type: "json")

    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:

// import foo from "./bar"
#include "bar.gen.h"  // generated header for the bar module
namespace _mpl_import_bar { /* forward declarations */ }
auto foo = _mpl_import_bar::default_export;

// import * as ns from "./bar"
#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

// MPL_Compiler/core/ir/EMPLNode.h:656
class ExportNode : public EMPLNode {
public:
    struct ExportBinding {
        std::string localName;     // Local name to export
        std::string exportedName;  // Name as exported (can differ with "as")
    };

    std::vector<ExportBinding> bindings;
    EMPLNodePtr value;              // For default export or module.exports = value
    bool isDefault = false;         // true for export default
    bool isReexport = false;        // true for export { x } from "module"
    std::string reexportModule;     // Module to re-export from

    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 {
    // Re-export from other.gen.h
    namespace _from_other = _mpl_module_other_js;
    using _from_other::x;  // for `export { x } from "./other"`
}

See also