EMPL IR
EMPLPrinter — IR pretty-printer
EMPLPrinter — IR pretty-printer
EMPLPrinter is the small but indispensable debugging utility that turns an IR tree into stable, indented text. It is declared in MPL_Compiler/core/ir/EMPLPrinter.h (20 lines) and implemented in EMPLPrinter.cpp.
// MPL_Compiler/core/ir/EMPLPrinter.h:10
class EMPLPrinter {
public:
static std::string print(const std::shared_ptr<ModuleNode>& module);
static std::string print(const std::shared_ptr<ProgramNode>& program);
private:
static std::string printNode(const EMPLNodePtr& node, int indent);
};
API
| Method | Source | Purpose |
|---|---|---|
print(module) |
EMPLPrinter.h:12 |
Render a single ModuleNode. |
print(program) |
EMPLPrinter.h:13 |
Render a ProgramNode (emits each module in topological order). |
printNode(node, indent) (private) |
EMPLPrinter.h:16 |
Recursive per-node renderer. indent is the current nesting depth (each level = two spaces). |
Both public methods are static; the class is non-instantiable.
Usage
#include "core/ir/EMPLNode.h"
#include "core/ir/EMPLPrinter.h"
auto program = frontend->parse(sourceText);
std::string dump = mpl::ir::EMPLPrinter::print(program);
std::cout << dump << std::endl;
// Or dump just one module:
auto singleModule = program->findModule("src/index.js");
if (singleModule) {
std::cout << mpl::ir::EMPLPrinter::print(singleModule) << std::endl;
}
The C++ backend typically calls the printer to expose a --emit-ir debug switch on the compiler CLI:
./MPL_Compiler input.js --target cpp --emit-ir > input.ir.txt
Output format
The output is line-oriented, one node per line, with two-space indentation per nesting level. Each line starts with the node's NodeKind name, followed by a space and a brief human-readable summary of the node's distinguishing fields.
Sample
For the following JavaScript source:
function add(a, b) {
if (a > 0) {
return a + b;
}
return b;
}
EMPLPrinter::print(module) produces:
Module "input.js"
FunctionDecl add(a, b)
Block
If
BinaryOp >
Identifier a
Literal 0
Block
Return
BinaryOp +
Identifier a
Identifier b
Return
Identifier b
Program output
print(program) first prints Program with the entry-module id, then each module in dependency order (using ProgramNode::topologicalOrder()):
Program entry="src/index.js"
Module "src/index.js" deps=[src/util.js]
Import "src/util.js"
FunctionDecl main()
Block
ExpressionStmt
MethodCall console.log
Literal "Hello, world"
Module "src/util.js" deps=[]
FunctionDecl helper(x)
Block
Return
Identifier x
Per-node formatting
NodeKind |
Format |
|---|---|
Program |
Program entry="<entryModuleId>" (one line, followed by indented modules) |
Module |
Module "<name>" lang=<sourceLanguage> deps=[<id1>, <id2>] |
Import |
Import "<module>" [{ bindings, sideEffectOnly flag, attributes }] |
Export |
Export [{ bindings / default / reexport}] |
FunctionDecl |
FunctionDecl <name>(<params>) [async, generator, ctor, static] |
ClassDecl |
ClassDecl <name> [struct] [extends <baseClass>] |
InterfaceDecl |
InterfaceDecl <name> [extends <base>] |
EnumDecl |
EnumDecl <name> [const] |
TypeAliasDecl |
TypeAliasDecl <name> = <typeExpr> |
VariableDecl |
`VariableDecl |
Block |
Block (followed by indented statements) |
If |
If (followed by indented condition, then-branch, optional else-branch) |
For |
For (init / cond / incr / body, each on its own line) |
ForEach |
ForEach target=<t> iterable=<i> |
While / DoWhile |
While / DoWhile (condition + body) |
Switch |
Switch (expr, then Case lines, then optional Default) |
TryCatch |
TryCatch (try / catch clauses / finally) |
Return / Throw / Break / Continue |
Return, Throw, Break, Continue (followed by optional value) |
BinaryOp |
BinaryOp <op> (followed by left, right) |
UnaryOp |
`UnaryOp |
FunctionCall |
FunctionCall <name> (followed by args) |
MethodCall |
MethodCall <methodName> (followed by object + args) |
MemberAccess |
MemberAccess .<memberName> (followed by object) |
IndexAccess |
IndexAccess (followed by object + index) |
Literal |
Literal <value> (string in quotes, true/false/null, numeric) |
Identifier |
Identifier <name> |
ArrayLiteral / ObjectLiteral / RecordLiteral / TupleLiteral |
The literal name, followed by indented elements / properties |
Lambda |
Lambda(<params>) [async] [arrow] |
Await / Yield |
Await / Yield [delegate] (followed by expression) |
Cast |
Cast <targetType> |
Ternary |
Ternary (condition, true-branch, false-branch) |
StandardLibraryCall |
StdLib <moduleName>.<functionName> (followed by args) |
Unsupported |
Unsupported reason="<reason>" |
Stability
The printer is intended for human consumption, not for diff-based regression testing of the IR. Two structurally identical IR trees may format with different whitespace if metadata fields differ. For canonical text, use EMPLCanonicalizer first.
See also
index.md— IR overviewcanonicalizer.md— produce canonical IR before printingvalidator.md— check IR well-formedness before printing
Implementation details
The printer's implementation lives in EMPLPrinter.cpp. The structure is:
-
Pre-amble. Each top-level module is prefixed with
Module "<name>" lang=<sourceLanguage> deps=[…]. Programs are prefixed withProgram entry="<entryModuleId>". -
Recursive walk. Each concrete node has a dedicated formatting function that emits the node's "header line" (the kind name plus summary fields) and then recurses into child slots with
indent + 1. -
Two-space indent. Every nested level adds two spaces of indentation. This is intentional — larger indents waste horizontal space on long argument lists.
-
No wrapping. The printer does NOT wrap long lines. Argument lists can produce very long lines; this is acceptable for a debug dump.
-
Stable node ordering. Children are printed in the order they appear in the source vector. There is no sorting.
-
No diff-friendly markers. The printer does not emit line numbers, hashes, or other markers that would let diff tools detect "real" changes. For diff-friendly IR text, use
EMPLCanonicalizer::canonicalizefirst, then sort children via a custom visitor.
Debug use cases
| Use case | Call |
|---|---|
| Dump IR after parsing | EMPLPrinter::print(module) |
| Dump IR after canonicalization | EMPLPrinter::print(program) (uses topological order) |
Dump IR with --emit-ir CLI flag |
EMPLPrinter::print(program) |
| Diagnostic for failed validation | EMPLPrinter::print(module) then list errors with line numbers |
| Snapshot test (compare against fixture) | EMPLPrinter::print(program) (must canonicalize first for stability) |
Sample full output
For the following program:
// input.js
import { add } from "./util.js";
function main() {
console.log("Sum:", add(2, 3));
}
main();
EMPLPrinter::print(program) produces:
Program entry="src/input.js"
Module "src/input.js" lang=javascript deps=[src/util.js]
Import "./src/util.js"
ImportBinding importedName="add" localName="add"
FunctionDecl main()
Block
ExpressionStmt
StandardLibraryCall Console.WriteLine
Literal "Sum:"
FunctionCall add
Literal 2
Literal 3
ExpressionStmt
FunctionCall main
Integration with the C++ backend
The C++ backend (backends/cpp/CppBackend.h) does NOT call EMPLPrinter directly — the printer is purely for debugging. The backend has its own pretty-printer tuned for emitting valid C++ (different concerns: balanced braces, semicolons, includes, etc.).
However, the backend DOES call EMPLPrinter when the --emit-ir flag is passed to MPL_Compiler, which writes the canonical IR text alongside the generated C++ files:
$ ./MPL_Compiler input.js --target cpp --emit-ir
$ ls build/output/
input.cpp
input.h
input.ir.txt # ← EMPLPrinter output
This is useful for debugging IR-level issues ("why did the backend emit this code?") without having to set up a debugger on the C++ source.
Limitations
- The printer does not enforce that the printed text round-trips back to the same IR. Field values may be reformatted, and some fields (e.g.
metadataentries) may be omitted from the dump. - The printer does not handle infinite recursion (deeply nested
LambdaNodeorBlockNode). A pathological input could produce a stack overflow. The C++ backend's--max-recursion-depthflag (default 1000) protects against this; the printer does not have such a guard. - The printer is not thread-safe. Multiple threads printing concurrently will produce interleaved output. Serialize with an external mutex if multi-threaded printing is needed.
See also (post-summary)
index.md— IR overviewvalidator.md— validationcanonicalizer.md— normalization