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:
namemust be non-empty. - V1:
bodymay benullptr(forward declaration / abstract / extern). - V8 (canonicalizer, not validator): if
parametersandtypedParametersdisagree on arity or names, canonicalize alignsparametersto matchtypedParametersand 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
types.md—UIRType,Parameternodes/class-decl.md—ClassDeclNodecarries themethods[]list ofFunctionDeclNodenodes/lambda-async.md—LambdaNodefor closures../canonicalizer.md— closure binding extraction
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)
types.md—UIRType,Parameternodes/class-decl.md— class methodsnodes/lambda-async.md— closures vs. function declarations../canonicalizer.md— forward-declaration fill