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.
BreakNodeandContinueNodedo 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::valueMAY benullptr. - V1:
ThrowNode::valueis REQUIRED (you cannotthrow;in any source language).
See also
nodes/block.md—BlockNode(used as the body of every control-flow node)validator.md— full rule setcanonicalizer.md— IR normalization