Elvelt

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 [const
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 [prefix
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

Implementation details

The printer's implementation lives in EMPLPrinter.cpp. The structure is:

  1. Pre-amble. Each top-level module is prefixed with Module "<name>" lang=<sourceLanguage> deps=[…]. Programs are prefixed with Program entry="<entryModuleId>".

  2. 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.

  3. Two-space indent. Every nested level adds two spaces of indentation. This is intentional — larger indents waste horizontal space on long argument lists.

  4. No wrapping. The printer does NOT wrap long lines. Argument lists can produce very long lines; this is acceptable for a debug dump.

  5. Stable node ordering. Children are printed in the order they appear in the source vector. There is no sorting.

  6. 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::canonicalize first, 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. metadata entries) may be omitted from the dump.
  • The printer does not handle infinite recursion (deeply nested LambdaNode or BlockNode). A pathological input could produce a stack overflow. The C++ backend's --max-recursion-depth flag (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)