EMPL IR
BlockNode
BlockNode
BlockNode represents a { … } block of statements. It is the universal body type for functions, if/else branches, loops, switch cases, try blocks, etc.
// MPL_Compiler/core/ir/EMPLNode.h:153
class BlockNode : public EMPLNode {
public:
std::vector<EMPLNodePtr> statements;
BlockNode() : EMPLNode(NodeKind::Block) {}
void accept(EMPLVisitor& v) override { v.visit(*this); }
};
Field reference
| Field | Type | Source | Default | Description |
|---|---|---|---|---|
kind |
NodeKind::Block |
EMPLNode.h:157 |
— | Inherited discriminator. |
location |
SourceLocation |
EMPLNode.h:75 |
empty | Source position of the opening brace (typically). |
metadata |
unordered_map<string,string> |
EMPLNode.h:76 |
empty | Free-form metadata. |
statements |
vector<EMPLNodePtr> |
EMPLNode.h:155 |
empty | Ordered list of statements. Each entry is one of: VariableDeclNode, IfNode, ForNode, ForEachNode, WhileNode, DoWhileNode, SwitchNode, TryCatchNode, ReturnNode, ThrowNode, BreakNode, ContinueNode, BlockNode, ExpressionNode (via any expression node used as a statement), ImportNode, ExportNode, ClassDeclNode, FunctionDeclNode, InterfaceDeclNode, EnumDeclNode, TypeAliasDeclNode. |
Constructors
| Constructor | Source | Notes |
|---|---|---|
BlockNode() (default) |
EMPLNode.h:157 |
All fields default-initialized. statements is empty. |
There is no parameterized constructor.
Methods
| Method | Source | Description |
|---|---|---|
accept(EMPLVisitor& v) override |
EMPLNode.h:159 |
Standard visitor dispatch. |
Block-as-statement vs. block-as-body
The IR overloads BlockNode for both purposes:
| Usage | Typical parent | Notes |
|---|---|---|
| Block-as-body | FunctionDeclNode::body, IfNode::thenBranch, WhileNode::body, ForEachNode::body, DoWhileNode::body, SwitchCase::body, TryCatchNode::tryBlock |
Identifies an execution scope. The C++ backend emits { … } and may introduce RAII guards. |
| Block-as-statement | Appears inside another BlockNode::statements |
{ stmt1; stmt2; } used as a single statement (e.g. the body of a lambda or to scope variable declarations). |
The IR does not distinguish the two with separate node kinds; the validator does not enforce a difference either.
Source-language mapping
| Source construct | Field configuration |
|---|---|
{ a; b; } (compound) |
statements = [a, b] |
{ } (empty) |
statements = [] |
{ let x = 1; use(x); } (Rust scope) |
statements = [VariableDeclNode("x", initializer=LiteralNode(1)), FunctionCallNode("use", IdentifierNode("x"))] |
do { … } (JS expression block — not a JS do-while) |
statements = […], parent is the expression position |
IR construction example
auto body = std::make_shared<mpl::ir::BlockNode>();
auto decl = std::make_shared<mpl::ir::VariableDeclNode>();
decl->name = "x";
decl->initializer = mpl::ir::LiteralNode::makeInt(42);
body->statements.push_back(decl);
auto print = std::make_shared<mpl::ir::StandardLibraryCallNode>();
print->moduleName = "Console";
print->functionName = "WriteLine";
print->arguments.push_back(mpl::ir::LiteralNode::makeString("done"));
body->statements.push_back(print);
body->location = { .filename = "input.js", .line = 1, .column = 0 };
Empty-block semantics
An empty BlockNode (no statements) is valid and represents {}. The validator does not flag this. The C++ backend emits an empty { } and may elide it if the empty block is the body of a void-returning function with no side effects.
Scope semantics
BlockNode does NOT carry an explicit scope descriptor. Frontends that need to model lexical scoping (Rust let lifetimes, C++ RAII) attach scope information via metadata keys ("scope_id", "scope_kind"). The canonicalizer may rewrite scope semantics during normalization.
Validation rules (subset)
- V1: an empty
statementslist is allowed. - (No specific structural rule for
BlockNodebeyond V1.)
Backend lower-cases
C++ backend
| Context | Lowered form |
|---|---|
| Block-as-body of a function | void f() { stmt1; stmt2; } |
Block-as-body of if/while/etc. |
if (cond) { stmt1; stmt2; } |
| Block-as-statement inside another block | { stmt1; stmt2; } (with optional scope-lifetime annotations) |
If metadata["scope_kind"] = "unsafe", the C++ backend emits { … } inside an mpl_unsafe_block wrapper. Other scope kinds ("transaction", "critical_section") emit RAII guards.
See also
nodes/function-decl.md— function bodies useBlockNodenodes/control-flow.md—IfNode,ForNode, etc. all carryBlockNodebodiesnodes/operators.md— expression statements inside blocks
Detailed examples
Function body
function add(a, b) {
const result = a + b;
return result;
}
Lowered IR:
auto fn = std::make_shared<mpl::ir::FunctionDeclNode>();
fn->name = "add";
fn->body = std::make_shared<mpl::ir::BlockNode>();
auto decl = std::make_shared<mpl::ir::VariableDeclNode>();
decl->name = "result";
decl->isConst = true;
auto binOp = std::make_shared<mpl::ir::BinaryOpNode>();
binOp->op = "+";
binOp->left = std::make_shared<mpl::ir::IdentifierNode>("a");
binOp->right = std::make_shared<mpl::ir::IdentifierNode>("b");
decl->initializer = binOp;
fn->body->statements.push_back(decl);
auto ret = std::make_shared<mpl::ir::ReturnNode>();
ret->value = std::make_shared<mpl::ir::IdentifierNode>("result");
fn->body->statements.push_back(ret);
If / else with empty branch
if (condition) {
doSomething();
}
// no else
Lowered IR:
auto ifNode = std::make_shared<mpl::ir::IfNode>();
ifNode->condition = std::make_shared<mpl::ir::IdentifierNode>("condition");
ifNode->thenBranch = std::make_shared<mpl::ir::BlockNode>();
auto callExpr = std::make_shared<mpl::ir::FunctionCallNode>();
callExpr->name = "doSomething";
ifNode->thenBranch->statements.push_back(callExpr);
// elseBranch is nullptr
Nested blocks (scope introduction)
{
let x = 1;
{
let y = 2;
console.log(x + y);
}
}
Lowered IR:
auto outer = std::make_shared<mpl::ir::BlockNode>();
auto xDecl = std::make_shared<mpl::ir::VariableDeclNode>();
xDecl->name = "x";
xDecl->initializer = mpl::ir::LiteralNode::makeInt(1);
outer->statements.push_back(xDecl);
auto inner = std::make_shared<mpl::ir::BlockNode>();
auto yDecl = std::make_shared<mpl::ir::VariableDeclNode>();
yDecl->name = "y";
yDecl->initializer = mpl::ir::LiteralNode::makeInt(2);
inner->statements.push_back(yDecl);
auto printCall = std::make_shared<mpl::ir::StandardLibraryCallNode>();
printCall->moduleName = "Console";
printCall->functionName = "WriteLine";
auto binOp = std::make_shared<mpl::ir::BinaryOpNode>();
binOp->op = "+";
binOp->left = std::make_shared<mpl::ir::IdentifierNode>("x");
binOp->right = std::make_shared<mpl::ir::IdentifierNode>("y");
printCall->arguments.push_back(binOp);
inner->statements.push_back(printCall);
outer->statements.push_back(inner);
The C++ backend emits the nested { } blocks directly — RAII guards are introduced for any local variables that need destruction (std::unique_ptr, std::lock_guard, etc.).
Try-catch with multiple catches
try {
riskyOperation();
} catch (e of NetworkError) {
console.log("network:", e);
} catch (e) {
console.log("other:", e);
} finally {
cleanup();
}
Lowered IR:
auto tryNode = std::make_shared<mpl::ir::TryCatchNode>();
// try block
tryNode->tryBlock = std::make_shared<mpl::ir::BlockNode>();
auto riskyCall = std::make_shared<mpl::ir::FunctionCallNode>();
riskyCall->name = "riskyOperation";
tryNode->tryBlock->statements.push_back(riskyCall);
// catch (e of NetworkError)
auto networkCatch = std::make_shared<mpl::ir::BlockNode>();
networkCatch->metadata["catch_type"] = "NetworkError";
auto log1 = std::make_shared<mpl::ir::StandardLibraryCallNode>();
log1->moduleName = "Console";
log1->functionName = "WriteLine";
log1->arguments.push_back(mpl::ir::LiteralNode::makeString("network:"));
log1->arguments.push_back(std::make_shared<mpl::ir::IdentifierNode>("e"));
networkCatch->statements.push_back(log1);
tryNode->catchBlocks.push_back({"e", networkCatch});
// catch (e) — type-less
auto genericCatch = std::make_shared<mpl::ir::BlockNode>();
auto log2 = std::make_shared<mpl::ir::StandardLibraryCallNode>();
log2->moduleName = "Console";
log2->functionName = "WriteLine";
log2->arguments.push_back(mpl::ir::LiteralNode::makeString("other:"));
log2->arguments.push_back(std::make_shared<mpl::ir::IdentifierNode>("e"));
genericCatch->statements.push_back(log2);
tryNode->catchBlocks.push_back({"e", genericCatch});
// finally
tryNode->finallyBlock = std::make_shared<mpl::ir::BlockNode>();
auto cleanupCall = std::make_shared<mpl::ir::FunctionCallNode>();
cleanupCall->name = "cleanup";
tryNode->finallyBlock->statements.push_back(cleanupCall);
Scope semantics — extended
The IR's BlockNode does not carry explicit scope metadata by default. For languages with explicit scope constructs (Rust unsafe, C++ transactional), the frontend populates metadata["scope_kind"]:
metadata["scope_kind"] |
C++ backend emission |
|---|---|
(absent) / "plain" |
{ … } |
"unsafe" |
mpl_unsafe_block([&]() { … }()); |
"transaction" |
mpl_transaction([&]() { … }()); |
"critical_section" |
mpl_critical_section _cs(mutex); { … } |
"synchronized" |
std::scoped_lock _lk(mutex); { … } |
"async" |
co_await mpl_async_scope([&]() -> mpl::mpl_future<void> { … }()); |
The canonicalizer rule C5 strips empty sourceLanguage flags but does NOT touch scope_kind metadata — that is preserved.
Empty block semantics
function noop() {}
Lowered IR:
auto fn = std::make_shared<mpl::ir::FunctionDeclNode>();
fn->name = "noop";
fn->body = std::make_shared<mpl::ir::BlockNode>(); // empty statements
fn->body->location = { .filename = "input.js", .line = 1, .column = 14 };
The C++ backend emits void noop() {} — an empty function body. With optimization passes enabled (-O2), the compiler may eliminate the function entirely if it has no side effects.
Block-as-expression-statement
Some languages allow blocks in expression position:
const result = {
const temp = compute();
temp * 2
};
In this JS comma-expression style, the block evaluates to its last expression. The IR represents this as a BlockNode inside a VariableDeclNode::initializer. The C++ backend emits:
const auto result = [&]() {
const auto temp = compute();
return temp * 2;
}();
RAII integration
For C++ backend emission, BlockNodes that introduce local variables with non-trivial destructors (std::unique_ptr, file handles, mutex locks) trigger automatic RAII guards. For example:
{
using file = openFile("data.txt");
process(file);
}
The C++ backend emits:
{
auto file = openFile("data.txt");
try {
process(file);
} catch (...) {
// implicit close via destructor
throw;
}
// implicit close via destructor (normal path)
}
See also (post-summary)
nodes/function-decl.md— function bodiesnodes/control-flow.md— control-flow node bodiesnodes/operators.md— expression statementsvalidator.md— rule V1