Elvelt

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 statements list is allowed.
  • (No specific structural rule for BlockNode beyond 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

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)