EMPL IR
EMPL IR (Intermediate Representation)
EMPL IR (Intermediate Representation)
The EMPL IR (Extended Multi-Platform Language Intermediate Representation) is the language-neutral intermediate representation used by the MPL_Compiler as the canonical pivot between all frontends and all backends. Every source language (JavaScript, Python, C#, Go, Rust, PHP, Ruby, Java, EMPLS) is parsed into an EMPL IR tree, and every backend consumes EMPL IR to emit target code (currently C++20, with other backends planned).
The IR lives entirely under the mpl::ir C++ namespace, declared across the following headers in MPL_Compiler/core/ir/:
| Header | Purpose |
|---|---|
EMPLTypes.h |
Type system (TypeKind, UIRType, Visibility, SourceLocation, Parameter) |
EMPLNode.h |
Node hierarchy: 38+ node classes plus NodeKind enum and EMPLNode base |
UIRNode.h |
Backwards-compatible alias header (#include "EMPLNode.h") |
EMPLVisitor.h |
Visitor pattern base class used by all node accept() methods |
EMPLPrinter.h |
Pretty-printer that renders IR trees as human-readable text |
EMPLValidator.h |
Static validation: enforces structural rules, reports ValidationResult |
EMPLCanonicalizer.h |
Normalization: strips frontend-specific variants, extracts closure bindings |
EMPLModule.h |
Top-level convenience module aggregator |
Design philosophy
The IR is deliberately minimal but expressive. The goal is a small set of node kinds — each capturing exactly one syntactic / semantic concept shared by the majority of source languages — that is still rich enough to round-trip all modern language features (classes, async/await, generators, destructuring, ESM/CJS modules, BigInt, decorators).
The design follows three guiding principles:
- Language neutrality. No node, field, or enum value references a specific source language. The pre-existing JS-only metadata flag
js.arrowwas retired in favour of structural boolean fields (capturesLexicalThis,isConstructible,hasOwnArguments) onLambdaNode. SeeLambdaNodeinnodes/lambda-async.md. - Tree-based, not SSA. The IR is a concrete syntax tree with explicit control-flow nodes (
IfNode,ForNode,SwitchNode,TryCatchNode). This keeps the IR easy to construct from hand-written recursive-descent parsers and easy to lower in a single bottom-up pass. - Smart-pointer ownership. Every node is heap-allocated and owned via
std::shared_ptr<EMPLNode>(aliasEMPLNodePtr). Parent nodes own their children; sibling lists are stored instd::vector<EMPLNodePtr>.
Neutrality guarantees
The IR is contractually neutral on the following axes. Adding a new frontend or backend MUST NOT require modifications to any core/ir/EMPLNode.h definition (although the canonicalizer may be extended to produce cleaner cores).
- C1 — No language name leakage.
ModuleNode::sourceLanguageis a free-form string, but the IR never branches on it. Backends consume only structural fields. - C2 — No source-text preservation. Nodes store semantic fields, not raw source text.
originalTextexists only onUnsupportedNode(line 624) and is consumed solely to render fallback diagnostics. - C3 — No syntax sugar. Syntactic shortcuts from a single language (JS
??, Python**, Ruby..) are normalized toBinaryOpNode::opstrings; backends decide how to lower. - C4 — No implicit type coercion.
BinaryOpNode::opstrings are the IR contract. Backends MAY widen but MUST NOT silently coerce. - C5 — No scope chain. Closures are represented as
LambdaNodewith explicitcapturesLexicalThisand aClosureBindingsanalysis produced byEMPLCanonicalizer::extractClosureBindings(EMPLCanonicalizer.h:21). - C6 — Single error model. All exceptions are
mpl_js_error/mpl_errorinstances; the IR has no per-language error type. - C7 — Visitor pattern, no RTTI abuse. All node traversal goes through
EMPLVisitor; backends do notdynamic_castonEMPLNodeto discriminate kind (they switch onnode->kind). - C8 — No parallel arrays. Every list inside a node lives in a
std::vector<EMPLNodePtr>named after its semantic role (statements,cases,arguments,declarations).
C1–C8 constraints in detail
| ID | Constraint | Enforced by | Reference |
|---|---|---|---|
| C1 | No if (sourceLanguage == "javascript") branching in backends |
Code review; no static check | — |
| C2 | Only UnsupportedNode::originalText carries source text |
EMPLValidator |
EMPLNode.h:624 |
| C3 | BinaryOpNode::op is the canonical operator name |
Canonicalizer rewrites language-specific forms | EMPLNode.h:472 |
| C4 | No implicit numeric promotion in IR | EMPLValidator warns on == between mismatched types |
EMPLValidator.h:23 |
| C5 | Closures are explicit LambdaNode + ClosureBindings |
EMPLCanonicalizer::extractClosureBindings |
EMPLCanonicalizer.h:21 |
| C6 | mpl_js_error / mpl_error everywhere |
Library convention | empl_core.h:82, empl.h:73 |
| C7 | All visitors derive from EMPLVisitor |
Compile-time | EMPLVisitor.h |
| C8 | Each list field has a semantic name | Naming convention | EMPLNode.h (all node classes) |
High-level structure
ProgramNode ← top-level (MPL_Compiler.cpp)
└─ ModuleEntry[]
└─ ModuleNode ← one per source file / module
├─ imports: ImportNode[] ← other modules this depends on
├─ declarations: ← top-level class/function/var decls
│ FunctionDeclNode
│ ClassDeclNode
│ InterfaceDeclNode
│ EnumDeclNode
│ TypeAliasDeclNode
│ VariableDeclNode
└─ statements: ← top-level executable code
BlockNode
└─ statements: EMPLNodePtr[]
IfNode, ForNode, ForEachNode, …
ExpressionStmt (via ExpressionNode)
ReturnNode, ThrowNode, …
BinaryOpNode, UnaryOpNode, FunctionCallNode, …
Node taxonomy
The 38+ NodeKind enum values are grouped into six semantic families. See node-kind.md for the exhaustive table.
| Family | Members | Typical source constructs |
|---|---|---|
| Module / program | Program, Module, Import, Export |
ESM, CJS, Python imports, Go packages |
| Declarations | FunctionDecl, VariableDecl, ClassDecl, InterfaceDecl, EnumDecl, TypeAliasDecl, DestructuringPattern |
function, let, class, interface, enum |
| Blocks & control flow | Block, If, For, ForEach, While, DoWhile, Switch, TryCatch |
{ }, if, for, switch, try |
| Jumps | Return, Throw, Break, Continue |
return, throw, break, continue |
| Expressions | BinaryOp, UnaryOp, Ternary, FunctionCall, MethodCall, MemberAccess, IndexAccess, Cast, Lambda, Await, Yield, Assignment |
a + b, f(x), obj.field, arr[i], x => y |
| Literals / values | Literal, Identifier, ArrayLiteral, ObjectLiteral, RecordLiteral, TupleLiteral, StandardLibraryCall |
42, "x", [1,2], console.log |
| Legacy | PrintCall, FuncDecl, Unsupported |
Pre-v3 aliases (deprecated) |
Type system overview
Types are described by UIRType (EMPLTypes.h:50) — a tagged-struct (kind: TypeKind) carrying generic / array / function / union / nullable payloads. The TypeKind enum (EMPLTypes.h:13) has 32 values ranging from Void to UserDefined and Never. See types.md for the full table.
Tooling pipeline
Source ─┐
├─► Frontend ─► EMPL IR ─► EMPLCanonicalizer ─► EMPL IR (core) ─► Backend ─► C++
│ └► EMPLValidator (optional; can run before canonicalize)
└─► EMPLPrinter (debug dump at any stage)
- Parse: a frontend (
frontends/<lang>/) consumes source text and emits aProgramNodewhose modules each hold aModuleNodepopulated withEMPLNodePtrchildren. - Print:
EMPLPrinter::print(module)orEMPLPrinter::print(program)produces a stable, indented textual dump for debugging. - Validate:
EMPLValidator::validate(module|program)returns aValidationResult { ok, errors[], warnings[] }. The validator can be run in strict mode (fail onUnsupportedNode) or permissive mode (only warn). - Canonicalize:
EMPLCanonicalizer::canonicalize(module|program)rewrites language-specific variants into a backend-friendly core. It also exposesextractClosureBindingsfor backends that need closure-capture metadata. - Lower: the C++ backend (
backends/cpp/CppBackend.h) walks the IR and emits C++20 source.
Build configuration
The IR headers compile under C++20 with -std=c++20. They have no third-party dependencies — only the C++ standard library (<memory>, <string>, <vector>, <variant>, <unordered_map>, <unordered_set>, <functional>, <utility>). The IR is consumed by the backend via direct header inclusion (#include "core/ir/EMPLNode.h").
The MPL_Compiler CMake build aggregates the IR sources via Unity Build (25-file batches). This is transparent to client code but means that editing one IR header can trigger a large recompile — see CMakeLists.txt for the unity-batch boundaries.
Versioning policy
The IR evolves conservatively. Changes follow this policy:
- Adding a node kind. Requires a new
NodeKindenum value, a new class deriving fromEMPLNode, and updates toEMPLVisitor(for the visitor overload),EMPLPrinter(for the dump format), andEMPLValidator(for any structural rules). Old IRs (without the new kind) MUST still validate cleanly — the validator emits a warning, not an error, for unknown kinds from older sources. - Adding a field to an existing node. Allowed without a major version bump. Frontends that don't populate the field get the field's default value. Backends that read the field MUST handle the default case.
- Renaming a field, node, or enum value. Forbidden without a deprecation cycle. The deprecated alias is retained for at least one minor version with a compiler warning (
[[deprecated]]on the type alias or static method). - Changing a field's semantic meaning. Treated as a breaking change. Requires major version bump, written migration notes, and dual-version support in the C++ backend.
The current IR is v3.x. The retirement of js.arrow metadata in favour of LambdaNode::capturesLexicalThis / isConstructible / hasOwnArguments (see EMPLNode.h:572-574) is the most recent example of the deprecation policy in action.
See also
node-kind.md— exhaustive table of allNodeKindenum valuestypes.md—UIRType,TypeKind,Visibility,Parameter,SourceLocationprinter.md—EMPLPrinterAPI and output formatvalidator.md—EMPLValidatorrule setcanonicalizer.md—EMPLCanonicalizertransformationsnodes/— per-node-class documentation (function decls, classes, control flow, …)../standard-library/index.md— the language-neutral stdlib surface
Quick-reference appendix
Smart-pointer alias
namespace mpl { namespace ir {
class EMPLNode;
using EMPLNodePtr = std::shared_ptr<EMPLNode>; // canonical alias
using UIRNode = EMPLNode; // legacy alias
using UIRNodePtr = EMPLNodePtr; // legacy alias
}}
Node construction idiom
auto fn = std::make_shared<mpl::ir::FunctionDeclNode>();
fn->name = "add";
fn->parameters = { {"a", "a"}, {"b", "b"} };
fn->body = std::make_shared<mpl::ir::BlockNode>();
auto lit = mpl::ir::LiteralNode::makeInt(42);
fn->body->statements.push_back(
std::make_shared<mpl::ir::ReturnNode>(...)); // returns ReturnNode holding lit
Visitor pattern
class MyVisitor : public mpl::ir::EMPLVisitor {
public:
void visit(mpl::ir::FunctionDeclNode& node) override { /* … */ }
void visit(mpl::ir::ClassDeclNode& node) override { /* … */ }
// … one overload per concrete node class …
};
program->accept(visitor); // dispatches to the right visit overload
Walking a tree without the visitor
For simple traversals (e.g. collecting all IdentifierNodes), a manual recursive walk is often clearer than a full visitor:
void collectIdentifiers(const mpl::ir::EMPLNodePtr& node,
std::vector<std::string>& out) {
if (!node) return;
if (auto* id = dynamic_cast<mpl::ir::IdentifierNode*>(node.get())) {
out.push_back(id->name);
}
// Recurse into known child slots …
}
The downside of this approach is that it does NOT scale — every new node kind requires updating every manual walker. The visitor pattern scales linearly because the visitor's dispatch table updates automatically when a new node kind is added (compiler enforces the visit overload).
Glossary
| Term | Definition |
|---|---|
| Frontend | A parser for one source language (JS, Python, C#, …) that produces EMPL IR. |
| Backend | A code generator that consumes EMPL IR and produces one target language (currently C++). |
| IR | Intermediate Representation. The EMPL IR is the canonical form passed between frontend and backend. |
| Node | A single EMPLNode-derived class instance. The IR is a tree of nodes. |
| Kind | The NodeKind enum value that discriminates the concrete node class. |
| Module | A single compilation unit (one source file). Multiple modules form a ProgramNode. |
| Statement | A node that produces an effect (decl, assignment, call, control-flow). |
| Expression | A node that produces a value. May appear inside another expression or as a statement. |
| Declaration | A node that introduces a binding (function, class, variable, type). |
| Canonicalization | The pass that rewrites language-specific variants into a backend-friendly core. |