Elvelt

EMPL IR

Control-flow nodes

Control-flow nodes

The IR has seven control-flow node kinds plus four jump nodes. This document covers all of them in detail.

IfNode          — `if (cond) { … } else { … }`
ForNode         — `for (init; cond; incr) { … }`
ForEachNode     — `for (target : iterable) { … }`
WhileNode       — `while (cond) { … }`
DoWhileNode     — `do { … } while (cond);`
SwitchNode      — `switch (expr) { case …: … }`
TryCatchNode    — `try { … } catch (e) { … } finally { … }`

ReturnNode      — `return [value];`
ThrowNode       — `throw value;`
BreakNode       — `break;`
ContinueNode    — `continue;`

IfNode

// MPL_Compiler/core/ir/EMPLNode.h:239
class IfNode : public EMPLNode {
public:
    EMPLNodePtr condition;
    std::shared_ptr<BlockNode> thenBranch;
    std::shared_ptr<BlockNode> elseBranch;

    IfNode() : EMPLNode(NodeKind::If) {}
    void accept(EMPLVisitor& v) override { v.visit(*this); }
};
Field Type Source Description
kind NodeKind::If EMPLNode.h:245 Discriminator.
condition EMPLNodePtr EMPLNode.h:241 The test expression. Required (validator V1).
thenBranch shared_ptr<BlockNode> EMPLNode.h:242 The body executed when condition is truthy. Required.
elseBranch shared_ptr<BlockNode> EMPLNode.h:243 The optional else body. nullptr for if (x) { … } with no else.

Source mapping

Source Field config
if (c) { a; } condition=c, thenBranch={a}
if (c) { a; } else { b; } condition=c, thenBranch={a}, elseBranch={b}
if (c) { a; } else if (d) { b; } else { c; } outer IfNode: condition=c, thenBranch={a}, elseBranch=BlockNode{statements=[inner IfNode]}

Backend lowering

Backend Lowered form
C++ if (cond) { … } else if (else is IfNode) … else { … } (the backend flattens chained else if into native else if chains).

ForNode

// MPL_Compiler/core/ir/EMPLNode.h:250
class ForNode : public EMPLNode {
public:
    EMPLNodePtr init;
    EMPLNodePtr condition;
    EMPLNodePtr increment;
    std::shared_ptr<BlockNode> body;

    ForNode() : EMPLNode(NodeKind::For) {}
    void accept(EMPLVisitor& v) override { v.visit(*this); }
};
Field Type Source Description
kind NodeKind::For EMPLNode.h:257 Discriminator.
init EMPLNodePtr EMPLNode.h:252 Initialization (typically VariableDeclNode or AssignmentNode). MAY be nullptr.
condition EMPLNodePtr EMPLNode.h:253 Loop test. MAY be nullptr (treated as true for for (;;)).
increment EMPLNodePtr EMPLNode.h:254 Per-iteration step (typically AssignmentNode or UnaryOpNode). MAY be nullptr.
body shared_ptr<BlockNode> EMPLNode.h:255 Loop body. Required.

Source mapping

Source Field config
for (let i = 0; i < 10; i++) { … } (JS) init=VariableDecl(i,0), condition=BinaryOp(<, i, 10), increment=UnaryOp(++, i), body=…
for (;;) { … } (infinite) init=null, condition=null, increment=null, body=…
for i := 0; i < 10; i++ { … } (Go) same as JS but init/incr may be post-statement (Go has no comma operator, so multiple steps become a BlockNode).

Backend lowering

C++ emits native for (init; cond; incr) { … }. The backend flattens any BlockNode inside init/incr to a comma-expression sequence. If init is a VariableDecl, the backend splits it into a declaration before the loop and an assignment inside init.


ForEachNode

// MPL_Compiler/core/ir/EMPLNode.h:262
class ForEachNode : public EMPLNode {
public:
    EMPLNodePtr target;
    EMPLNodePtr iterable;
    std::shared_ptr<BlockNode> body;

    ForEachNode() : EMPLNode(NodeKind::ForEach) {}
    void accept(EMPLVisitor& v) override { v.visit(*this); }
};
Field Type Source Description
kind NodeKind::ForEach EMPLNode.h:268 Discriminator.
target EMPLNodePtr EMPLNode.h:264 The loop variable. Typically IdentifierNode (single binding) or VariableDeclNode (with optional type annotation).
iterable EMPLNodePtr EMPLNode.h:265 The expression iterated over (array, string, iterable object). Required.
body shared_ptr<BlockNode> EMPLNode.h:266 Loop body. Required.

Source mapping

Source Field config
for (const x of arr) { … } (JS) target=IdentifierNode("x"), iterable=IdentifierNode("arr"), body=…
for (const [k, v] of entries(arr)) { … } (JS destructure) target=DestructuringPatternNode{kind=Array, elements=[{k}, {v}]}, iterable=FunctionCallNode("entries", arr), body=…
for _, v := range m { … } (Go) target=IdentifierNode("_"), iterable=IdentifierNode("m"), body=…
for x in lst: … (Python) target=IdentifierNode("x"), iterable=IdentifierNode("lst"), body=BlockNode{statements=[…]}

Backend lowering

C++ emits a range-based for loop:

for (auto& x : mpl_range(impl::to_range(iterable))) {
    // body
}

The mpl_range adapter handles arrays, std::vector, JS-style iterables (via __mpl_for_of__), and strings.


WhileNode

// MPL_Compiler/core/ir/EMPLNode.h:273
class WhileNode : public EMPLNode {
public:
    EMPLNodePtr condition;
    std::shared_ptr<BlockNode> body;

    WhileNode() : EMPLNode(NodeKind::While) {}
    void accept(EMPLVisitor& v) override { v.visit(*this); }
};
Field Type Source Description
kind NodeKind::While EMPLNode.h:278 Discriminator.
condition EMPLNodePtr EMPLNode.h:275 Loop test. Required.
body shared_ptr<BlockNode> EMPLNode.h:276 Loop body. Required.

Source mapping

Source Field config
while (c) { … } (JS / C / Java / etc.) condition=c, body=…

Backend lowering

Direct while (cond) { … } in C++.


DoWhileNode

// MPL_Compiler/core/ir/EMPLNode.h:283
class DoWhileNode : public EMPLNode {
public:
    std::shared_ptr<BlockNode> body;
    EMPLNodePtr condition;

    DoWhileNode() : EMPLNode(NodeKind::DoWhile) {}
    void accept(EMPLVisitor& v) override { v.visit(*this); }
};
Field Type Source Description
kind NodeKind::DoWhile EMPLNode.h:288 Discriminator.
body shared_ptr<BlockNode> EMPLNode.h:285 Loop body (executes first). Required.
condition EMPLNodePtr EMPLNode.h:286 Loop test (evaluated after each iteration). Required.

Source mapping

Source Field config
do { … } while (c); (JS / C / Java) body=…, condition=c

Backend lowering

do {
    // body
} while (cond);

SwitchNode

// MPL_Compiler/core/ir/EMPLNode.h:293
class SwitchNode : public EMPLNode {
public:
    struct CaseEntry {
        EMPLNodePtr label;
        std::shared_ptr<BlockNode> body;
        bool fallthrough = false;
    };
    EMPLNodePtr expr;
    std::vector<CaseEntry> cases;
    std::shared_ptr<BlockNode> defaultCase;

    SwitchNode() : EMPLNode(NodeKind::Switch) {}
    void accept(EMPLVisitor& v) override { v.visit(*this); }
};
Field Type Source Description
kind NodeKind::Switch EMPLNode.h:304 Discriminator.
expr EMPLNodePtr EMPLNode.h:300 The discriminant expression. Required.
cases vector<CaseEntry> EMPLNode.h:301 Ordered list of case clauses. MAY be empty (switch with only default).
defaultCase shared_ptr<BlockNode> EMPLNode.h:302 Optional default branch. nullptr if absent.

CaseEntry:

Field Type Source Description
label EMPLNodePtr EMPLNode.h:296 The case label (typically LiteralNode or IdentifierNode).
body shared_ptr<BlockNode> EMPLNode.h:297 Statements executed when this case matches. MAY be empty (empty case).
fallthrough bool EMPLNode.h:298 true if this case falls through to the next (no implicit break). Most languages have implicit fallthrough (C, JS pre-strict); others do not.

Source mapping

Source Field config
switch (x) { case 1: a; break; case 2: b; break; default: c; } (JS) expr=x, cases=[{label=1, body={a}, fallthrough=false}, {label=2, body={b}, fallthrough=false}], defaultCase={c}
switch x { case 1, 2: a } (Go — multi-label) Multiple CaseEntry entries share the same body reference (a), one per label.
match x { 1 => a, _ => b } (Rust) expr=x, cases=[{label=Pattern(1), body={a}}], defaultCase={b}

Backend lowering

C++ emits a native switch for integral labels and an if-else chain for string / pattern labels. fallthrough = true omits the trailing break;. The backend canonicalizes empty case bodies by sharing the next case's body (where the source language permits it).


TryCatchNode

// MPL_Compiler/core/ir/EMPLNode.h:309
class TryCatchNode : public EMPLNode {
public:
    std::shared_ptr<BlockNode> tryBlock;
    std::vector<std::pair<std::string, std::shared_ptr<BlockNode>>> catchBlocks;
    std::shared_ptr<BlockNode> finallyBlock;

    TryCatchNode() : EMPLNode(NodeKind::TryCatch) {}
    void accept(EMPLVisitor& v) override { v.visit(*this); }
};
Field Type Source Description
kind NodeKind::TryCatch EMPLNode.h:315 Discriminator.
tryBlock shared_ptr<BlockNode> EMPLNode.h:311 The protected block. Required.
catchBlocks vector<pair<string, shared_ptr<BlockNode>>> EMPLNode.h:312 Catch clauses. Each entry is (exceptionVariableName, body). The name may be empty (catch-all). MAY be empty.
finallyBlock shared_ptr<BlockNode> EMPLNode.h:313 Optional finally block. nullptr if absent.

Source mapping

Source Field config
try { a; } catch (e) { b; } (JS) tryBlock={a}, catchBlocks=[("e", {b})], finallyBlock=null
try { a; } catch (e of Type1) { b; } catch { c; } (TS) tryBlock={a}, catchBlocks=[("e", {b, type=Type1}), ("", {c})] (the type filter is stored in the body's metadata)
try { a; } finally { d; } (no catch) tryBlock={a}, catchBlocks=[], finallyBlock={d}
try { a; } except ValueError as e: b; except: c; (Python) tryBlock={a}, catchBlocks=[("e", {b, type=ValueError}), ("", {c})], finallyBlock=optional

Backend lowering

C++ emits:

try {
    // tryBlock
} catch (const std::exception& e__) {
    // first catch block; e__ bound from e__
} catch (...) {
    // catch-all
}
// finallyBlock

Each catch-block's variable is renamed to a hidden local (e__) and re-bound to the user's name inside the body. The C++ backend uses mpl_try_block_exit RAII guard to ensure the finallyBlock runs even if no catch matches.


Jump nodes

ReturnNode

// MPL_Compiler/core/ir/EMPLNode.h:320
class ReturnNode : public EMPLNode {
public:
    EMPLNodePtr value;
    ReturnNode() : EMPLNode(NodeKind::Return) {}
    void accept(EMPLVisitor& v) override { v.visit(*this); }
};
Field Type Description
value EMPLNodePtr The returned expression. nullptr for return; (void return).

ThrowNode

// MPL_Compiler/core/ir/EMPLNode.h:329
class ThrowNode : public EMPLNode {
public:
    EMPLNodePtr value;
    ThrowNode() : EMPLNode(NodeKind::Throw) {}
    void accept(EMPLVisitor& v) override { v.visit(*this); }
};
Field Type Description
value EMPLNodePtr The thrown value. Required (unlike ReturnNode).

BreakNode

// MPL_Compiler/core/ir/EMPLNode.h:338
class BreakNode : public EMPLNode {
public:
    BreakNode() : EMPLNode(NodeKind::Break) {}
    void accept(EMPLVisitor& v) override { v.visit(*this); }
};

No fields. Emits break;.

ContinueNode

// MPL_Compiler/core/ir/EMPLNode.h:345
class ContinueNode : public EMPLNode {
public:
    ContinueNode() : EMPLNode(NodeKind::Continue) {}
    void accept(EMPLVisitor& v) override { v.visit(*this); }
};

No fields. Emits continue;.

Note. BreakNode and ContinueNode do NOT carry a label. Labelled breaks (break outer;) and labelled continues (continue outer;) are not yet modeled in the IR — the frontend should emit a goto-equivalent (or use a synthetic nested-scope helper) until v4 adds label fields.

Validation rules (subset)

  • V1: each control-flow node's required slots (see validator.md).
  • V1: ReturnNode::value MAY be nullptr.
  • V1: ThrowNode::value is REQUIRED (you cannot throw; in any source language).

See also