Elvelt

EMPL IR

VariableDeclNode

VariableDeclNode

VariableDeclNode represents a variable declaration. It supports both single-name declarations (let x = 1) and destructuring declarations (let { a, b } = obj).

// MPL_Compiler/core/ir/EMPLNode.h:446
class VariableDeclNode : public EMPLNode {
public:
    std::string name;
    std::shared_ptr<DestructuringPatternNode> pattern;
    UIRTypePtr type;
    EMPLNodePtr initializer;
    bool isConst = false;
    bool isMutable = true;

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

Field reference

Field Type Source Default Description
kind NodeKind::VariableDecl EMPLNode.h:455 Inherited discriminator.
location SourceLocation EMPLNode.h:75 empty Source position.
metadata unordered_map<string,string> EMPLNode.h:76 empty Free-form metadata.
name std::string EMPLNode.h:448 empty The single binding name. Required unless pattern is set. Validator V8: must not be set together with pattern.
pattern shared_ptr<DestructuringPatternNode> EMPLNode.h:449 nullptr Destructuring pattern. Either name OR pattern must be set (validator V8).
type UIRTypePtr EMPLNode.h:450 nullptr Type annotation. nullptr means "inferred from initializer".
initializer EMPLNodePtr EMPLNode.h:451 nullptr The right-hand-side expression. MAY be nullptr for forward declarations or parameter-style hoisted bindings.
isConst bool EMPLNode.h:452 false true for compile-time constants and const-style bindings that cannot be reassigned.
isMutable bool EMPLNode.h:453 true false for readonly / immutable bindings. Independent of isConst (some languages distinguish "compile-time constant" from "runtime immutable").

Constructors

Constructor Source Notes
VariableDeclNode() (default) EMPLNode.h:455 All fields default-initialized.

There is no parameterized constructor.

Methods

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

Source-language mapping

Source construct Field configuration
let x = 5 (JS) name="x", initializer=LiteralNode(5), isConst=false, isMutable=true
const y = 10 (JS) name="y", initializer=LiteralNode(10), isConst=true, isMutable=false
let { a, b } = obj (JS destructure) name="", pattern=DestructuringPatternNode{kind=Object, elements=[{a,a}, {b,b}]}, initializer=IdentifierNode("obj")
let [first, ...rest] = arr (JS destructure) name="", pattern=DestructuringPatternNode{kind=Array, elements=[{first}, {rest, isRest=true}]}, initializer=IdentifierNode("arr")
var x int = 5 (Go) name="x", type=UIRType{Int32}, initializer=LiteralNode(5)
let x: i32 = 5; (Rust) name="x", type=UIRType{Int32}, initializer=LiteralNode(5), isConst=false, isMutable=true
let mut x = 5 (Rust) name="x", initializer=LiteralNode(5), isMutable=true
x: int = 5 (Python type-annotated) name="x", type=UIRType{Int32}, initializer=LiteralNode(5)
final int X = 42; (Java) name="X", type=UIRType{Int32}, initializer=LiteralNode(42), isConst=true

IR construction example (JS frontend)

auto v = std::make_shared<mpl::ir::VariableDeclNode>();
v->name = "greeting";
v->isConst = true;
v->isMutable = false;
v->type = std::make_shared<mpl::ir::UIRType>(mpl::ir::UIRType{TypeKind::String});
auto lit = mpl::ir::LiteralNode::makeString("Hello, world");
v->initializer = lit;
v->location = { .filename = "input.js", .line = 1, .column = 4 };

Validation rules (subset)

  • V8: if name is non-empty AND pattern is non-null, validator reports an error.
  • V8: if both name and pattern are empty, validator reports an error.
  • V8: at least one of name or pattern MUST be set.

Backend lower-cases

C++ backend

Configuration Lowered form
isConst=true, value known constexpr auto X = …; (or const auto X = …; if non-literal)
isConst=true, isMutable=false const auto X = …;
isMutable=true (default) auto X = …;
type=UIRType{T} T X = …;
initializer=nullptr T X; (zero-initialized; may be T X{}; for safety)
pattern set Emitted as structured-binding or as a sequence of single-name declarations

Python backend (planned)

Configuration Lowered form
isConst=true X: T = value (no Final[] unless set by canonicalizer)
isMutable=true X: T = value
pattern set a, b = obj (or destructuring assignment)

See also

Detailed examples by source language

JavaScript / TypeScript — basic

const x: number = 42;
let y = "hello";
var z;  // declared but not initialized

Lowered IR:

// const x: number = 42
auto v1 = std::make_shared<mpl::ir::VariableDeclNode>();
v1->name = "x";
v1->isConst = true;
v1->isMutable = false;
v1->type = std::make_shared<mpl::ir::UIRType>(mpl::ir::UIRType{TypeKind::Float64});
v1->initializer = mpl::ir::LiteralNode::makeNumber(42);

// let y = "hello"
auto v2 = std::make_shared<mpl::ir::VariableDeclNode>();
v2->name = "y";
v2->isConst = false;
v2->isMutable = true;
v2->initializer = mpl::ir::LiteralNode::makeString("hello");

// var z (no initializer)
auto v3 = std::make_shared<mpl::ir::VariableDeclNode>();
v3->name = "z";
v3->isConst = false;
v3->isMutable = true;
v3->initializer = nullptr;

JavaScript — destructuring

const { a, b: alias, ...rest } = obj;
const [first, , third, ...others] = arr;

Lowered IR:

// const { a, b: alias, ...rest } = obj
auto v1 = std::make_shared<mpl::ir::VariableDeclNode>();
v1->isConst = true;
v1->isMutable = false;
v1->pattern = std::make_shared<mpl::ir::DestructuringPatternNode>();
v1->pattern->patternKind = mpl::ir::DestructuringPatternNode::PatternKind::Object;
v1->pattern->elements = {
    { .targetName = "a", .sourceKey = "a", .defaultValue = nullptr, .isRest = false },
    { .targetName = "alias", .sourceKey = "b", .defaultValue = nullptr, .isRest = false },
    { .targetName = "rest", .sourceKey = "..." /* special marker */, .defaultValue = nullptr, .isRest = true },
};
v1->initializer = std::make_shared<mpl::ir::IdentifierNode>("obj");

// const [first, , third, ...others] = arr
auto v2 = std::make_shared<mpl::ir::VariableDeclNode>();
v2->isConst = true;
v2->isMutable = false;
v2->pattern = std::make_shared<mpl::ir::DestructuringPatternNode>();
v2->pattern->patternKind = mpl::ir::DestructuringPatternNode::PatternKind::Array;
v2->pattern->elements = {
    { .targetName = "first", .sourceKey = "0", .defaultValue = nullptr, .isRest = false },
    { .targetName = "",       .sourceKey = "1", .defaultValue = nullptr, .isRest = false }, // skipped
    { .targetName = "third", .sourceKey = "2", .defaultValue = nullptr, .isRest = false },
    { .targetName = "others", .sourceKey = "..." /* special marker */, .defaultValue = nullptr, .isRest = true },
};
v2->initializer = std::make_shared<mpl::ir::IdentifierNode>("arr");

Python — type-annotated assignment

x: int = 42
y: list[str] = ["a", "b"]

Lowered IR:

auto v1 = std::make_shared<mpl::ir::VariableDeclNode>();
v1->name = "x";
v1->type = std::make_shared<mpl::ir::UIRType>(mpl::ir::UIRType{TypeKind::Int32});
v1->initializer = mpl::ir::LiteralNode::makeInt(42);

auto v2 = std::make_shared<mpl::ir::VariableDeclNode>();
v2->name = "y";
v2->type = std::make_shared<mpl::ir::UIRType>(mpl::ir::UIRType{TypeKind::List});
v2->type->elementType = std::make_shared<mpl::ir::UIRType>(mpl::ir::UIRType{TypeKind::String});
auto arrLit = std::make_shared<mpl::ir::ArrayLiteralNode>();
arrLit->elements = {
    mpl::ir::LiteralNode::makeString("a"),
    mpl::ir::LiteralNode::makeString("b"),
};
v2->initializer = arrLit;

Rust — mutability

let x = 5;        // immutable
let mut y = 10;   // mutable
const Z: i32 = 100; // compile-time constant

Lowered IR:

// let x = 5  → isMutable=false
auto v1 = std::make_shared<mpl::ir::VariableDeclNode>();
v1->name = "x";
v1->isConst = false;
v1->isMutable = false;  // Rust immutable binding
v1->initializer = mpl::ir::LiteralNode::makeInt(5);

// let mut y = 10  → isMutable=true
auto v2 = std::make_shared<mpl::ir::VariableDeclNode>();
v2->name = "y";
v2->isConst = false;
v2->isMutable = true;
v2->initializer = mpl::ir::LiteralNode::makeInt(10);

// const Z: i32 = 100  → isConst=true, isMutable=false
auto v3 = std::make_shared<mpl::ir::VariableDeclNode>();
v3->name = "Z";
v3->isConst = true;
v3->isMutable = false;
v3->type = std::make_shared<mpl::ir::UIRType>(mpl::ir::UIRType{TypeKind::Int32});
v3->initializer = mpl::ir::LiteralNode::makeInt(100);

Go

var x int = 42
y := 50  // short declaration, type inferred

Lowered IR:

// var x int = 42
auto v1 = std::make_shared<mpl::ir::VariableDeclNode>();
v1->name = "x";
v1->type = std::make_shared<mpl::ir::UIRType>(mpl::ir::UIRType{TypeKind::Int32});
v1->initializer = mpl::ir::LiteralNode::makeInt(42);

// y := 50  (type inferred)
auto v2 = std::make_shared<mpl::ir::VariableDeclNode>();
v2->name = "y";
v2->type = nullptr;  // inferred from initializer
v2->initializer = mpl::ir::LiteralNode::makeInt(50);

C++ backend lowering details

Type inference from initializer

When type is nullptr and initializer is a LiteralNode, the C++ backend infers:

Literal type Inferred C++ type
int64_t int64_t (or auto)
double double (or auto)
string std::string (or auto)
bool bool (or auto)
nullptr std::nullptr_t (or auto)

For complex initializers (function calls, expressions), the backend uses auto and relies on C++ template argument deduction.

Const folding

For:

const PI = 3.14159;

If the C++ backend determines that the initializer is a compile-time constant (no function calls, no non-const globals), it emits constexpr double PI = 3.14159;. Otherwise it falls back to const double PI = 3.14159;.

Destructuring lowering

For:

const { a, b: alias, ...rest } = obj;

The C++ backend emits a structured binding (C++17+) when possible, or a sequence of single-name declarations otherwise:

const auto& _tmp = obj;
const auto& a = _tmp.a;
const auto& alias = _tmp.b;
const auto& rest = [&]() {
    auto _r = obj;
    _r.erase("a", "b");  // remove the destructured keys
    return _r;
}();

Default parameter values

For function parameters with defaults, typedParameters[i].defaultValue holds the expression. The C++ backend emits:

void f(int x, std::string s = "default") { /* … */ }

The default-value expression can be any EMPLNodePtr, including LiteralNode, BinaryOpNode, FunctionCallNode, etc.

Validator invariants

Rule Constraint Source
V8 name XOR pattern must be set (exactly one, not both, not neither) validator.md
V1 initializer MAY be nullptr (forward declaration) validator.md
V1 type MAY be nullptr (inferred) validator.md

Common patterns

Module-level singleton

const config = Object.freeze({ host: "localhost", port: 8080 });

Lowered IR:

auto v = std::make_shared<mpl::ir::VariableDeclNode>();
v->name = "config";
v->isConst = true;
v->isMutable = false;

auto objLit = std::make_shared<mpl::ir::ObjectLiteralNode>();
objLit->properties = {
    { std::make_shared<mpl::ir::LiteralNode>(mpl::ir::LiteralNode::makeString("host")),
      std::make_shared<mpl::ir::LiteralNode>(mpl::ir::LiteralNode::makeString("localhost")) },
    { std::make_shared<mpl::ir::LiteralNode>(mpl::ir::LiteralNode::makeString("port")),
      std::make_shared<mpl::ir::LiteralNode>(mpl::ir::LiteralNode::makeInt(8080)) }
};
v->initializer = objLit;
v->metadata["frozen"] = "true";

The C++ backend emits inline const auto config = mpl::mpl_make_object({{"host", "localhost"}, {"port", 8080}});.

Tuple destructuring

first, second = (1, 2)

Lowered IR:

auto v = std::make_shared<mpl::ir::VariableDeclNode>();
v->pattern = std::make_shared<mpl::ir::DestructuringPatternNode>();
v->pattern->patternKind = mpl::ir::DestructuringPatternNode::PatternKind::Array;
v->pattern->elements = {
    { .targetName = "first",  .sourceKey = "0", .defaultValue = nullptr, .isRest = false },
    { .targetName = "second", .sourceKey = "1", .defaultValue = nullptr, .isRest = false }
};

auto tupleLit = std::make_shared<mpl::ir::TupleLiteralNode>();
tupleLit->elements = {
    mpl::ir::LiteralNode::makeInt(1),
    mpl::ir::LiteralNode::makeInt(2)
};
v->initializer = tupleLit;

Multiple-assignment from function call

const [a, b, c] = compute();

Lowered IR:

auto v = std::make_shared<mpl::ir::VariableDeclNode>();
v->pattern = std::make_shared<mpl::ir::DestructuringPatternNode>();
v->pattern->patternKind = mpl::ir::DestructuringPatternNode::PatternKind::Array;
v->pattern->elements = {
    { .targetName = "a", .sourceKey = "0", .defaultValue = nullptr, .isRest = false },
    { .targetName = "b", .sourceKey = "1", .defaultValue = nullptr, .isRest = false },
    { .targetName = "c", .sourceKey = "2", .defaultValue = nullptr, .isRest = false }
};
auto call = std::make_shared<mpl::ir::FunctionCallNode>();
call->name = "compute";
v->initializer = call;

The C++ backend emits:

const auto _tmp = compute();
const auto& a = std::get<0>(_tmp);
const auto& b = std::get<1>(_tmp);
const auto& c = std::get<2>(_tmp);

See also (post-summary)