Elvelt

EMPL IR

Lambda, Await, Yield

Lambda, Await, Yield

Three node kinds covering closures, asynchronous suspension, and generator yields. They are documented together because they share semantic overlap (lambdas may be async; generators yield values; await suspends async functions).

LambdaNode

// MPL_Compiler/core/ir/EMPLNode.h:565
class LambdaNode : public EMPLNode {
public:
    std::vector<std::pair<std::string, std::string>> parameters;
    std::shared_ptr<BlockNode> body;
    EMPLNodePtr expressionBody;
    bool isAsync = false;
    bool capturesLexicalThis = false;  // arrow functions / Python lambdas
    bool isConstructible = true;       // false for arrow functions
    bool hasOwnArguments = true;       // false for arrow functions

    LambdaNode() : EMPLNode(NodeKind::Lambda) {}
    void accept(EMPLVisitor& v) override { v.visit(*this); }
};

Field reference

Field Type Source Description
kind NodeKind::Lambda EMPLNode.h:576 Discriminator.
parameters vector<pair<string,string>> EMPLNode.h:567 Un-typed parameter list. Each entry is (parameterName, sourceText). MAY be empty.
body shared_ptr<BlockNode> EMPLNode.h:568 Block body. Exactly one of body or expressionBody MUST be set.
expressionBody EMPLNodePtr EMPLNode.h:569 Expression body (for (x) => x + 1). Exactly one of body or expressionBody MUST be set.
isAsync bool EMPLNode.h:570 true for async lambdas (async (x) => x).
capturesLexicalThis bool EMPLNode.h:572 true if the lambda captures this lexically (arrow functions in JS, lambdas in Python). Defaults to false.
isConstructible bool EMPLNode.h:573 false for arrow functions (which cannot be new-ed). Defaults to true.
hasOwnArguments bool EMPLNode.h:574 false for arrow functions (which inherit arguments from the enclosing function). Defaults to true.

Constructors

Constructor Source Notes
LambdaNode() (default) EMPLNode.h:576 All fields default-initialized.

There is no parameterized constructor. Use default construction + field assignment.

Methods

Method Source Description
accept(EMPLVisitor& v) override EMPLNode.h:578 Standard visitor dispatch.

Source mapping

Source parameters body / expressionBody isAsync capturesLexicalThis isConstructible hasOwnArguments
(x) => x + 1 (JS arrow) [("x","x")] expressionBody = BinaryOp("+", x, 1) false true false false
() => 42 [] expressionBody = Literal(42) false true false false
async (x) => x (TS) [("x","x")] expressionBody = Identifier("x") true true false false
function(x) { return x; } (JS traditional) [("x","x")] body = BlockNode{ReturnNode(Identifier("x"))} false false true true
lambda x: x + 1 (Python) [("x","x")] expressionBody = BinaryOp("+", x, 1) false true false false
def f(x): return x + 1 (Python, but treated as expression) [("x","x")] body = BlockNode{…} false true false false
|x| x + 1 (Ruby block) [("x","x")] expressionBody = BinaryOp("+", x, 1) false false true true
func(x) { x + 1 } (Go func literal) [("x","x")] body = BlockNode{…} false false true true
|x| x + 1 (Rust closure) [("x","x")] expressionBody = BinaryOp("+", x, 1) false true (depends on usage) true true
(x) => { return x; } (TS expression block) [("x","x")] body = BlockNode{…} false true false false

Validation rules

Rule Constraint
V1 body or expressionBody MUST be set (exactly one).
V1 parameters MAY be empty.

Structural-attribute rationale

The three booleans (capturesLexicalThis, isConstructible, hasOwnArguments) were introduced in v3 to replace the legacy metadata["js.arrow"] = "true" flag. They are structural — they describe properties of the lambda itself, not the source language — so backends do not need to know which language produced the lambda.

Source-language feature capturesLexicalThis isConstructible hasOwnArguments
JS arrow function true false false
JS traditional function false true true
Python lambda true false false
Python def (used as closure) true (lexical) true false (no arguments)
Ruby block false true true
Go func literal false true true
Rust closure (Fn / FnMut / FnOnce) true (Fn / FnMut) or false (FnOnce) true true

Backend lowering (C++)

For capturesLexicalThis = false and isConstructible = true (traditional function):

auto lambda = [params...](args...) -> RetType {
    // body
};

For arrow-style (capturesLexicalThis = true, isConstructible = false, hasOwnArguments = false):

auto lambda = [&env, params...](args...) -> RetType {
    // body
};
// where env is the closure-binding struct produced by EMPLCanonicalizer::extractClosureBindings

For async (isAsync = true):

auto lambda = [params...](args...) -> mpl::mpl_future<RetType> {
    co_return /* body */;
};

The closure-binding struct is a mpl::mpl_object containing one slot per captured variable.

Closure binding extraction

EMPLCanonicalizer::extractClosureBindings(body, parameters, typedParameters) (declared at EMPLCanonicalizer.h:21) returns:

struct ClosureBindings {
    std::vector<std::string> capturedVars;  // sorted
    bool capturesThis = false;
};

The backend uses this to allocate the env struct referenced in the lambda capture list. See ../canonicalizer.md for the analysis rules.


AwaitNode

// MPL_Compiler/core/ir/EMPLNode.h:581
class AwaitNode : public EMPLNode {
public:
    EMPLNodePtr expression;

    AwaitNode() : EMPLNode(NodeKind::Await) {}
    void accept(EMPLVisitor& v) override { v.visit(*this); }
};
Field Type Description
expression EMPLNodePtr The expression whose value to await. Required.

Source mapping

Source Field config
await promise (JS) expression=Identifier("promise")
await fetch(url) expression=FunctionCallNode("fetch", url)
await asyncio.sleep(1) (Python) expression=FunctionCallNode("asyncio.sleep", Literal(1))
co_await fut (C++) expression=Identifier("fut") (canonicalized from co_await)

Validation rules

Rule Constraint
V1 expression MUST be non-null.
V1 AwaitNode MUST appear inside a FunctionDeclNode or LambdaNode with isAsync = true. The validator warns (rule V1 strict mode) if used outside an async context.

Backend lowering (C++)

For mpl_promise<T>: mpl_await(expr). For stdlib futures: expr.wait() (if void return) or expr.get() (if value). For raw values: expr (the await is a no-op).


YieldNode

// MPL_Compiler/core/ir/EMPLNode.h:590
class YieldNode : public EMPLNode {
public:
    EMPLNodePtr expression;
    bool delegate = false;

    YieldNode() : EMPLNode(NodeKind::Yield) {}
    void accept(EMPLVisitor& v) override { v.visit(*this); }
};
Field Type Description
expression EMPLNodePtr The yielded value. MAY be nullptr (bare yield).
delegate bool true for yield* (JS) — delegates iteration to an inner iterable. Defaults to false.

Source mapping

Source Field config
yield 1 (JS generator) expression=Literal(1), delegate=false
yield (no value) expression=null, delegate=false
yield* other (JS) expression=Identifier("other"), delegate=true
yield x (Python) expression=Identifier("x"), delegate=false

Validation rules

Rule Constraint
V1 YieldNode MUST appear inside a FunctionDeclNode or LambdaNode with isGenerator = true. Validator warns otherwise.

Backend lowering (C++)

mpl_yield(generator, value)mpl_yield is a template defined in empl.h:365 (non-Promise case) and empl.h:376 (Promise case).

For delegate = true: mpl_yield_delegate(generator, inner) (defined in empl.h adapter region).


IR construction example

// JS: const f = (x) => x * 2;
auto lambda = std::make_shared<mpl::ir::LambdaNode>();
lambda->parameters = { {"x", "x"} };
lambda->expressionBody = std::make_shared<mpl::ir::BinaryOpNode>();
auto binOp = std::static_pointer_cast<mpl::ir::BinaryOpNode>(lambda->expressionBody);
binOp->op = "*";
binOp->left = std::make_shared<mpl::ir::IdentifierNode>("x");
binOp->right = mpl::ir::LiteralNode::makeInt(2);
lambda->isAsync = false;
lambda->capturesLexicalThis = true;
lambda->isConstructible = false;
lambda->hasOwnArguments = false;

See also