Elvelt

EMPL IR

FunctionDeclNode

FunctionDeclNode

FunctionDeclNode represents a function or method declaration. It is the most heavily populated node in the IR — used for top-level functions, methods, constructors, async functions, generators, and lambdas (which are structurally similar but live in LambdaNode).

// MPL_Compiler/core/ir/EMPLNode.h:162
class FunctionDeclNode : public EMPLNode {
public:
    std::string name;
    std::string returnType;
    UIRTypePtr returnTypeInfo;
    std::vector<std::pair<std::string, std::string>> parameters;
    std::vector<Parameter> typedParameters;
    std::shared_ptr<BlockNode> body;
    bool isAsync = false;
    bool isGenerator = false;
    bool isConstructor = false;
    bool isStatic = false;

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

The legacy type alias using MethodNode = FunctionDeclNode; at line 180 is retained for source compatibility but new code should use FunctionDeclNode directly.

Field reference

Field Type Source Default Description
kind NodeKind::FunctionDecl EMPLNode.h:175 Inherited discriminator.
location SourceLocation EMPLNode.h:75 empty Source position (filename / line / column).
metadata unordered_map<string,string> EMPLNode.h:76 empty Free-form string→string metadata. Frontends may stash language-specific hints here; the canonicalizer migrates structural ones to first-class fields.
name std::string EMPLNode.h:164 empty The function identifier. Required (validator rule V1).
returnType std::string EMPLNode.h:165 empty Source-text return type (e.g. "number", "void", "Promise<string>"). Use this when returnTypeInfo is empty.
returnTypeInfo UIRTypePtr EMPLNode.h:166 nullptr Structured return type. If non-null, preferred over returnType.
parameters vector<pair<string,string>> EMPLNode.h:167 empty Un-typed parameter list — each entry is (parameterName, sourceText). Preserved for backends that need to recover original source spelling.
typedParameters vector<Parameter> EMPLNode.h:168 empty Typed parameter list — each Parameter carries name, type: UIRTypePtr, defaultValue: EMPLNodePtr, isVariadic: bool. The C++ backend prefers this list.
body shared_ptr<BlockNode> EMPLNode.h:169 nullptr The function body. nullptr denotes a forward declaration (abstract method or extern function).
isAsync bool EMPLNode.h:170 false true if the function returns a Promise/Future/Task and may use await.
isGenerator bool EMPLNode.h:171 false true if the function may use yield and returns an iterator.
isConstructor bool EMPLNode.h:172 false true if the function is a class constructor.
isStatic bool EMPLNode.h:173 false true if the function is a static method.

Constructors

Constructor Source Notes
FunctionDeclNode() (default) EMPLNode.h:175 All fields default-initialized. The standard pattern is auto fn = std::make_shared<FunctionDeclNode>(); followed by manual field assignment.

There is no parameterized constructor — frontends build the node field-by-field.

Methods

Method Source Description
accept(EMPLVisitor& v) override EMPLNode.h:177 Standard visitor dispatch. Visitor calls v.visit(*this) (where the visitor has an overload for FunctionDeclNode&).

Source-language mapping

Source construct Field configuration
function f(x, y) { … } (JS) name="f", parameters={{"x",""},{"y",""}}, typedParameters=[{name:"x"},{name:"y"}], body=BlockNode{…}
async function f() { … } name="f", parameters={}, body=BlockNode{…}, isAsync=true
function* gen() { yield 1; } name="gen", parameters={}, body=BlockNode{…}, isGenerator=true
constructor(x) { this.x = x; } (JS class) name="constructor", parameters={{"x",""}}, body=BlockNode{…}, isConstructor=true
static helper() { … } name="helper", parameters={}, body=BlockNode{…}, isStatic=true
def f(x: int) -> int: return x (Python) name="f", parameters={{"x","x: int"}}, typedParameters=[{name:"x", type:Int32}], returnTypeInfo=Int32, body=BlockNode{…}
func Add(a, b int) int { return a+b } (Go) name="Add", parameters={{"a","a int"},{"b","b int"}}, typedParameters=[{name:"a",type:Int32},{name:"b",type:Int32}], returnTypeInfo=Int32, body=BlockNode{…}
pub fn add(a: i32, b: i32) -> i32 { a+b } (Rust) name="add", parameters={{"a","a: i32"},{"b","b: i32"}}, typedParameters=[…], returnTypeInfo=Int32, body=BlockNode{…}

IR construction example (JS frontend)

auto fn = std::make_shared<mpl::ir::FunctionDeclNode>();
fn->name = "add";
fn->parameters = { {"a", "a"}, {"b", "b"} };
fn->typedParameters = {
    { .name = "a", .type = std::make_shared<mpl::ir::UIRType>(mpl::ir::UIRType{TypeKind::Int32}), .defaultValue = nullptr, .isVariadic = false },
    { .name = "b", .type = std::make_shared<mpl::ir::UIRType>(mpl::ir::UIRType{TypeKind::Int32}), .defaultValue = nullptr, .isVariadic = false }
};
fn->returnTypeInfo = std::make_shared<mpl::ir::UIRType>(mpl::ir::UIRType{TypeKind::Int32});
fn->body = std::make_shared<mpl::ir::BlockNode>();
fn->body->statements.push_back(/* ReturnNode */);
fn->location = { .filename = "input.js", .line = 3, .column = 0 };

Validation rules (subset)

  • V1: name must be non-empty.
  • V1: body may be nullptr (forward declaration / abstract / extern).
  • V8 (canonicalizer, not validator): if parameters and typedParameters disagree on arity or names, canonicalize aligns parameters to match typedParameters and emits a warning.

Backend lower-cases

Backend Lowered form
C++ int32_t add(int32_t a, int32_t b) { … } (or a member of the enclosing class if isStatic/isConstructor)
Python (planned) def add(a: int, b: int) -> int: …
JavaScript (planned) Direct emission — the JS backend is essentially a 1:1 copy

For isAsync = true, the C++ backend wraps the return in mpl::future<T> and loweres the body as a coroutine. For isGenerator = true, it wraps the return in mpl::generator<T> (defined in empl.h:340).

See also

Detailed examples by source language

JavaScript / TypeScript

async function* gen(): AsyncGenerator<number> {
    let i = 0;
    while (i < 10) {
        yield i++;
    }
}

Lowered IR:

auto fn = std::make_shared<mpl::ir::FunctionDeclNode>();
fn->name = "gen";
fn->returnTypeInfo = std::make_shared<mpl::ir::UIRType>(mpl::ir::UIRType{TypeKind::UserDefined});
fn->returnTypeInfo->name = "AsyncGenerator";
fn->isAsync = true;
fn->isGenerator = true;

auto whileLoop = std::make_shared<mpl::ir::WhileNode>();
auto cond = std::make_shared<mpl::ir::BinaryOpNode>();
cond->op = "<";
cond->left = std::make_shared<mpl::ir::IdentifierNode>("i");
cond->right = mpl::ir::LiteralNode::makeInt(10);
whileLoop->condition = cond;

auto yieldStmt = std::make_shared<mpl::ir::YieldNode>();
auto postIncr = std::make_shared<mpl::ir::UnaryOpNode>();
postIncr->op = "++";
postIncr->isPrefix = false;
postIncr->operand = std::make_shared<mpl::ir::IdentifierNode>("i");
yieldStmt->expression = postIncr;

whileLoop->body = std::make_shared<mpl::ir::BlockNode>();
whileLoop->body->statements.push_back(yieldStmt);
fn->body = std::make_shared<mpl::ir::BlockNode>();
fn->body->statements.push_back(whileLoop);

C#

public static int Add(int a, int b) => a + b;

Lowered IR:

auto fn = std::make_shared<mpl::ir::FunctionDeclNode>();
fn->name = "Add";
fn->isStatic = true;
fn->parameters = { {"a", "int a"}, {"b", "int b"} };
fn->typedParameters = {
    {.name = "a", .type = std::make_shared<mpl::ir::UIRType>(mpl::ir::UIRType{TypeKind::Int32}), .defaultValue = nullptr, .isVariadic = false},
    {.name = "b", .type = std::make_shared<mpl::ir::UIRType>(mpl::ir::UIRType{TypeKind::Int32}), .defaultValue = nullptr, .isVariadic = false}
};
fn->returnTypeInfo = std::make_shared<mpl::ir::UIRType>(mpl::ir::UIRType{TypeKind::Int32});
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");
fn->expressionBody = binOp;  // C# expression-bodied member

Go

func Add(a, b int) int {
    return a + b
}

Lowered IR:

auto fn = std::make_shared<mpl::ir::FunctionDeclNode>();
fn->name = "Add";
fn->parameters = { {"a", "a int"}, {"b", "b int"} };
fn->typedParameters = {
    {.name = "a", .type = std::make_shared<mpl::ir::UIRType>(mpl::ir::UIRType{TypeKind::Int32}), .defaultValue = nullptr, .isVariadic = false},
    {.name = "b", .type = std::make_shared<mpl::ir::UIRType>(mpl::ir::UIRType{TypeKind::Int32}), .defaultValue = nullptr, .isVariadic = false}
};
fn->returnTypeInfo = std::make_shared<mpl::ir::UIRType>(mpl::ir::UIRType{TypeKind::Int32});
auto returnStmt = std::make_shared<mpl::ir::ReturnNode>();
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");
returnStmt->value = binOp;
fn->body = std::make_shared<mpl::ir::BlockNode>();
fn->body->statements.push_back(returnStmt);

Rust

pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

Lowered IR is essentially identical to the Go version. The C++ backend emits static int32_t add(int32_t a, int32_t b) { return a + b; }. The pub visibility is encoded via metadata["visibility"]="pub" on the declaration.

Python

def add(a: int, b: int) -> int:
    return a + b

Lowered IR matches Go / Rust. Python's def does not carry static / async / generator flags — those default to false (Python async uses async def which sets isAsync = true).

Recursion and mutual recursion

For:

def is_even(n):
    return n == 0 or is_odd(n - 1)

def is_odd(n):
    return n != 0 and is_even(n - 1)

The IR has two FunctionDeclNodes in the module's declarations[]. The bodies reference the OTHER function via IdentifierNode("is_odd") / IdentifierNode("is_even"). The C++ backend emits forward declarations before the bodies:

bool is_odd(int64_t n);  // forward decl

bool is_even(int64_t n) {
    return n == 0 || is_odd(n - 1);
}

bool is_odd(int64_t n) {
    return n != 0 && is_even(n - 1);
}

The canonicalizer rule C6 ("Resolve forward declarations") handles the case where one declaration explicitly declares body = nullptr and a later one provides the body — the canonicalizer merges them so the C++ backend does not have to.

Forward declaration pattern

A common pattern in source languages is the "abstract method" or "extern function":

declare function externalApi(input: string): Promise<number>;

abstract class Base {
    abstract handleEvent(e: Event): void;
}

For the function: FunctionDeclNode { name = "externalApi", body = nullptr }.

For the class method: FunctionDeclNode { name = "handleEvent", body = nullptr, isStatic = false } inside ClassDeclNode::methods[]. Validator rule V9 enforces that body = nullptr AND isStatic = false — abstract static methods are not modeled.

Default parameter values

For:

function greet(name = "World") {
    return `Hello, ${name}`;
}

Lowered IR:

fn->typedParameters = {
    {.name = "name",
     .type = std::make_shared<mpl::ir::UIRType>(mpl::ir::UIRType{TypeKind::String}),
     .defaultValue = mpl::ir::LiteralNode::makeString("World"),
     .isVariadic = false}
};

The C++ backend emits void greet(std::string name = std::string{"World"}).

Variadic parameters

For:

function sum(...nums) {
    return nums.reduce((a, b) => a + b, 0);
}

Lowered IR:

fn->typedParameters = {
    {.name = "nums",
     .type = std::make_shared<mpl::ir::UIRType>(mpl::ir::UIRType{TypeKind::List}),
     .defaultValue = nullptr,
     .isVariadic = true}
};
fn->typedParameters[0].type->elementType = std::make_shared<mpl::ir::UIRType>(mpl::ir::UIRType{TypeKind::Float64});

The C++ backend emits double sum(std::vector<double> nums) and the body calls nums.begin() / nums.end() etc.

See also (post-summary)