Backends
C++ Code Emission
C++ Code Emission
This document explains how CppBackend lowers each IR node kind to C++ source. The lowering is split across three top-level entry points: generateStatement() for statements, generateExpr() for expressions, and the per-statement preamble that sets up the runtime.
| Entry point | Location | Inputs | Output |
|---|---|---|---|
generate(ModuleNode) |
CppBackend.h:24 |
One IR module | One .cpp file containing int main() |
generateProgram(ProgramNode) |
CppBackend.h:2264 |
Linked program | ProgramGenerationResult (per-module .cpp + __mpl_main.cpp + __mpl_module_registry.h) |
generateStatement(EMPLNodePtr, indent) |
CppBackend.h:5409 |
One IR statement | C++ statements at the given indent level |
generateExpr(EMPLNodePtr) |
CppBackend.h:7810 |
One IR expression | A C++ expression returning mpl_value (or auto) |
Dispatch flow
generate(ModuleNode)
│
├─ emit runtime preamble (line 50-1750)
├─ collectFunctionScopedVars + collectNested… + … (line 1865-2231)
├─ emitModuleStatement(i) for each i in [0, N) (line 2205)
│ └─ calls generateStatement(stmt, indent=1)
│ └─ for expression-children, calls generateExpr(node)
└─ wrap body in `int main(int argc, char** argv)`
Every generateExpr() and generateStatement() begins with a runtime-need check (nodeNeedsRuntime, lines 4394-4406) that returns __mpl_undefined__ (or an empty statement + __host_log__) for IR constructs the backend cannot lower. This makes the backend fail loudly at runtime instead of silently miscompiling.
Runtime needs guard
File: CppBackend.h:4394-4406
static bool nodeNeedsRuntime(const ir::EMPLNodePtr& node, std::string* reasonOut = nullptr) {
// Returns true for: AwaitNode, YieldNode, DecoratorNode, MacroNode,
// PatternMatchNode, certain UnsupportedNode variants
}
When nodeNeedsRuntime(stmt, &reason) is true:
| Caller | Effect |
|---|---|
generateStatement (line 5418) |
Emits __host_log__("MPL runtime-required statement encountered: <reason>"); |
generateExpr (line 7816) |
Returns __mpl_undefined__ |
__host_log__ is a host-side debug helper that prints to stderr so unresolved IR nodes are visible during dev runs.
Statement dispatch (generateStatement)
File: CppBackend.h:5409-7809
This 2 400-line routine handles every statement kind in the IR. The shape of each branch is:
if (stmt->kind == ir::NodeKind::X) {
auto xNode = std::static_pointer_cast<ir::XNode>(stmt);
// …emit C++…
return ss.str();
}
Statement → C++ mapping
IR NodeKind |
C++ output | Line | Notes |
|---|---|---|---|
Import |
(empty — handled at module level) | 5424 | Imports are resolved in ProgramNode linking |
Export |
recurse to value | 5427 | Named exports become __export_value_binding__ calls (see modular-mode.md) |
StandardLibraryCall |
std::cout << …, std::sqrt(…), etc. |
5435-5535 | See Standard Library Lowering below |
PrintCall |
rewritten as StandardLibraryCall ("Console.WriteLine" / "Write") |
5538-5545 | Legacy lowering path |
VariableDecl |
auto x = init; (or mpl_value x = …; if boxed) |
5547-5795 | See ./hoisting.md for hoisting-aware re-emission |
Assignment |
target = value; or mpl_set(target, key, value) |
~5795 | Member / index → mpl_set |
Block |
recursive generateStatement for each child |
~5820 | |
If |
if (mpl_truthy(cond)) { … } else { … } |
~5830 | |
For |
for (init; mpl_truthy(cond); incr) { … } |
~5900 | |
ForEach |
for (auto& __it : __mpl_for_of__(iterable)) { … } |
~5950 | |
While / DoWhile |
standard C++ while / do-while with mpl_truthy |
~6000 | |
Switch |
switch (mpl_unwind_switch(expr)) { case __mpl_case_0: … } |
~6100 | Uses switchCounter_ to mint unique labels |
TryCatch |
try { … } catch (const mpl_value& e) { … } catch (const mpl_js_error& e) { … } |
~6300 | Catches both mpl_value throws and JS-style error objects |
Return |
return value; (boxed if needed) |
~6500 | |
Throw |
throw mpl_box_value(value); |
~6510 | |
Break / Continue |
standard C++ | ~6520 | |
FunctionDecl / FuncDecl |
auto name = [cap](params…) -> mpl_value { … }; (hoisted) |
~6600 | Hoisting controls whether this is static or auto |
ClassDecl |
class CXX_name { public: … }; then a constructor lambda |
~6800 | Lowered to a C++ class with mpl_value member layout |
EnumDecl |
enum class name : int { … }; + lookup map |
~7200 | |
LambdaDecl (inline) |
[cap](params…) -> mpl_value { … } |
inline | |
Break / Continue |
C++ break; / continue; |
~6520 | |
TryCatch |
try { … } catch (...) { … } |
~6300 |
Standard-library lowering (StandardLibraryCall)
File: CppBackend.h:5435-5535
StandardLibraryCallNode is the IR's language-neutral standard-library call. The C++ backend lowers each (moduleName, functionName) to a direct C++ call:
| IR call | C++ output | Example |
|---|---|---|
Console.WriteLine(a, b, c) |
std::cout << mpl_to_string(a) << mpl_to_string(b) << mpl_to_string(c) << std::endl; |
line 5438-5446 |
Console.Write(a) |
std::cout << mpl_to_string(a); |
line 5442 |
Console.ErrorWriteLine(a) |
std::cerr << mpl_to_string(a) << std::endl; |
line 5449-5454 |
Console.ReadLine() |
std::string __mpl_read_line__; std::getline(std::cin, __mpl_read_line__); |
line 5457 |
Math.Abs(x) |
std::abs(x) |
line 5464-5466 |
Math.Floor(x) |
std::floor(x) |
line 5468 |
Math.Ceil(x) |
std::ceil(x) |
line 5472 |
Math.Round(x) |
std::round(x) |
line 5476 |
Math.Sqrt(x) |
std::sqrt(x) |
line 5480 |
Math.Pow(x, y) |
std::pow(x, y) |
line 5484 |
Math.Max(a, b) / Math.Min(a, b) |
std::max(a, b) / std::min(a, b) |
line 5488-5494 |
String.Trim(s) |
inline lambda (whitespace strip) | line 5499-5501 |
String.ToLower(s) / ToUpper(s) |
std::transform + ::tolower / ::toupper |
line 5503-5509 |
String.StartsWith(s, p) / EndsWith(s, p) |
inline lambda (string compare) | line 5511-5518 |
DateTime.NowUnixMs |
std::chrono::duration_cast<…>(…).count() |
line 5521-5528 |
Unmapped calls emit __host_log__("Unhandled standard library call: <mod>.<fn>"); (line 5531-5533) so a future frontend can find its unimplemented calls by grepping generated code.
Expression dispatch (generateExpr)
File: CppBackend.h:7810-9792
The expression dispatcher mirrors generateStatement but emits values, not side effects. It always returns a std::string containing a C++ expression of type mpl_value (or auto when the caller can infer the type).
Expression → C++ mapping
IR NodeKind |
C++ output | Line | Notes |
|---|---|---|---|
Literal (int / double / string / bool / nullptr) |
numeric / std::string("…") / true/false / nullptr |
7820-7847 | std::to_chars for full-precision doubles (line 7827) |
Identifier |
mpl_box_value(name) or direct identifier |
7850-7988 | See identifier resolution below |
ArrayLiteral |
std::vector<…>{…} or std::vector<mpl_value>{…} lambda |
8009-8093 | Spread, hole, primitive-typed vs boxed |
BinaryOp |
mpl_add, mpl_sub, mpl_mul, mpl_strict_eq, … |
8095-8213 | All operators use runtime helpers (see table) |
UnaryOp |
mpl_unary_plus, mpl_truthy(!x), mpl_delete, ++/-- |
8215-8288 | Prefix vs postfix inc/dec handled |
Ternary |
mpl_truthy(c) ? box(t) : box(f) lambda |
8290-8294 | Always wrapped in a box-value lambda |
VariableDecl (as expression) |
name = init |
8296-8312 | Used inside comma expressions |
FunctionCall |
mpl_call(name, args…) or direct call |
8314-8710 | See ./special-patterns.md for the long table |
MemberAccess |
mpl_get_value(obj, "memberName") |
8715-8750 | Hoisted member access for classes |
IndexAccess |
mpl_get_value(obj, key) (or mpl_is_nullish-guarded) |
7990-8007 | Optional chaining → lambda |
Lambda |
[capture](params…) -> mpl_value { … } |
8750-8800 | Capture list from buildLambdaCaptureList |
FunctionDecl (as expr) |
auto name = [cap](params…) -> mpl_value { … }; |
similar | |
ClassDecl (as expr) |
auto name = (function-pointer-from-ctor); |
similar | Used for class-expression const X = class {} |
Await |
mpl_await(expr) |
~8900 | From runtime preamble |
Yield |
mpl_yield(expr) |
~8920 | |
Ternary / New (handled in FunctionCall as new keyword) |
8598-8630 | new lowered as lambda creating prototype-bearing mpl_object |
Identifier resolution
File: CppBackend.h:7850-7988
generateExpr(IdentifierNode) is the most complex branch — it is the only place the backend has to decide what an unqualified name refers to. The resolution order is:
1. exports / __mpl_module_exports__ (line 7859)
2. void / nullptr / __magic__ (line 7862-7870)
3. this / arguments inside class context (line 7872-7882)
4. classCaptureAccessFor(name) (hoisted class member access) (line 7883)
5. isVisibleJsClassName(name) (hoisted JS class) (line 7886)
6. isCurrentLocalBindingName(name) (function-scope locals) (line 7889)
7. moduleScopeVarNames_ (module-scope locals) (line 7892)
8. jsGlobals (Map<String, runtime-helper-expr>) (line 7896-7982)
9. fallback: sanitizeVarName(name) (line 7987)
Step 8 is the largest — it covers ~70 JS global names (Object, Array, Map, Set, Promise, Symbol, BigInt, Headers, fetch, setTimeout, …). For languages that don't emit those identifiers, the table is simply never consulted.
Operator dispatch (BinaryOpNode)
File: CppBackend.h:8095-8213
The IR uses the canonical operator strings defined in core/ir/EMPLNode.h. The backend maps each to a runtime helper:
IR op |
C++ helper | Line |
|---|---|---|
=== |
mpl_strict_eq(a, b) |
8122 |
!== |
(!mpl_strict_eq(a, b)) |
8125 |
== |
mpl_loose_eq(a, b) |
8128 |
!= |
(!mpl_loose_eq(a, b)) |
8131 |
?? |
lambda mpl_is_nullish(lhs) ? rhs : lhs |
8134 |
&& |
lambda mpl_truthy(lhs) ? rhs : lhs |
8139 |
|| |
lambda mpl_truthy(lhs) ? lhs : rhs |
8144 |
** |
mpl_pow(a, b) |
8149 |
>>> |
mpl_urshift(a, b) |
8152 |
instanceof |
mpl_instanceof or mpl_instanceof_type<T> |
8155 |
in |
mpl_in(a, b) |
8164 |
, |
((a), (b)) |
8167 |
+ |
mpl_add(a, b) |
8170 |
-, *, /, % |
mpl_sub, mpl_mul, mpl_div, mpl_mod |
8173-8183 |
<, <=, >, >= |
mpl_lt, mpl_le, mpl_gt, mpl_ge |
8185-8195 |
<<, >>, &, |, ^ |
mpl_shl, mpl_shr, mpl_bitand, mpl_bitor, mpl_bitxor |
8197-8211 |
=== is not operator== on the variant — it uses mpl_strict_eq which short-circuits type mismatches (5 === "5" is false). Likewise == uses mpl_loose_eq which performs numeric coercion.
The three core value helpers (mpl_call, mpl_get_value, mpl_set)
The runtime preamble (defined in MPL_Standard/System/empl.h and inlined at the top of every translation unit) provides three helpers that all IR expressions eventually funnel through:
| Helper | Signature (informal) | Purpose | Where emitted from |
|---|---|---|---|
mpl_call(callee, args…) |
mpl_value(mpl_value, std::vector<mpl_value>) |
Invoke any callable value (function, class ctor, lambda, built-in) with boxed args | line 8584, 8619, mpl_call(...) |
mpl_get_value(obj, key) |
mpl_value(mpl_value, mpl_value) |
Read a property by string or numeric key | line 7999, 8161 |
mpl_set(obj, key, value) |
void(mpl_value, mpl_value, mpl_value) |
Write a property | line 8253, 8267 |
Every member access (obj.prop) becomes mpl_get_value(obj, "prop"). Every assignment to a property becomes mpl_set(obj, "prop", value). Every function call becomes mpl_call(callee, args…). The backend only deviates from this rule for built-ins (std::abs, std::sqrt, std::pow) and for __mpl_* helpers that are internal.
Boxing discipline
Every expression in the IR lowers to either:
- a value already in
mpl_valueform (from a nested call), or - a primitive (
int,double,std::string,bool) that the caller will box if needed
Boxing is done by mpl_box_value<T> (preamble line 334-350), which uses if constexpr to dispatch on the type. The backend inserts mpl_box_value(…) calls at three places:
| Where | Why |
|---|---|
Before passing args to mpl_call (line 8585-8587, 8620-8622) |
mpl_call expects boxed args |
When assigning to a mpl_value typed slot (line 5570, etc.) |
To preserve heterogeneous storage |
| When returning from a lambda (line 8307, etc.) | To normalize the return type |
Unboxed primitives are emitted in expression position when the operator explicitly accepts them (mpl_add, mpl_sub, etc. all take mpl_value and internally unbox). See ./variant-handling.md for the unwrap rules.
Statement emission: the hoisting-aware emit ordering
File: CppBackend.h:2205-2231
When a module-level statement references a hoisted callable that appears later in the statement list, the backend needs to emit the referenced callable first (since C++ requires a forward declaration for same-scope references). The dispatch loop is:
std::function<void(size_t)> emitModuleStatement = [&](size_t index) {
if (index >= N || moduleStatementEmitted[index]) return;
const auto& stmt = module->statements[index];
std::unordered_set<std::string> refs;
collectIdentifierRefs(stmt, refs);
for (const auto& refName : refs) {
auto it = moduleHoistedCallableIndices.find(refName);
if (it != moduleHoistedCallableIndices.end() && it->second > index) {
emitModuleStatement(it->second); // recurse to emit the callee first
}
}
ss << generateStatement(stmt, indent);
moduleStatementEmitted[index] = true;
};
for (size_t i = 0; i < N; ++i) emitModuleStatement(i);
This produces a depth-first pre-order traversal where dependencies come first. See ./hoisting.md for the exact forward-declaration rules.
End-to-end example: a JS function with closure
Source JS:
function makeAdder(x) {
return function(y) { return x + y; };
}
const add5 = makeAdder(5);
console.log(add5(3));
Lowered IR (simplified):
ModuleNode
├── FunctionDeclNode name="makeAdder"
│ parameters=[Parameter("x")]
│ body=Block
│ └── ReturnNode value=LambdaNode
│ parameters=[Parameter("y")]
│ body=BinaryOpNode op="+" left=Identifier("x") right=Identifier("y")
│ capturesLexicalThis=false
├── VariableDeclNode name="add5" initializer=FunctionCallNode("makeAdder", [Literal(5)])
├── ExpressionStatement
│ └── FunctionCallNode name="console.log" arguments=[FunctionCallNode(add5, [Literal(3)])]
Generated C++ (single-file mode):
// Auto-generated by MPL Compiler
// Source language: javascript
// Target backend: C++
// EMPL Runtime: Language-agnostic core + javascript adapter
#include <iostream>
#include <string>
// …preamble…
// Hoisted forward declarations:
static mpl_value makeAdder = __mpl_undefined__; // line 2118
static mpl_value add5 = __mpl_undefined__; // line 2122
int main(int argc, char** argv) {
__mpl_js_set_process_argv(argc, argv);
// Module statements in emit order:
// [1] makeAdder = [=](auto x) -> mpl_value {
// return [=, &x](auto y) -> mpl_value {
// return mpl_box_value(mpl_add(x, y));
// };
// };
makeAdder = [=](mpl_value x) -> mpl_value {
return [=, &x](mpl_value y) -> mpl_value {
return mpl_box_value(mpl_add(x, y));
};
};
// [2] add5 = mpl_call(makeAdder, mpl_box_value(5));
add5 = mpl_call(makeAdder, mpl_box_value(5));
// [3] std::cout << mpl_to_string(mpl_call(add5, mpl_box_value(3))) << std::endl;
std::cout << mpl_to_string(mpl_call(add5, mpl_box_value(3))) << std::endl;
mpl_run_event_loop();
_exit(0);
return 0;
}
Every primitive goes through mpl_box_value and mpl_to_string. Every call goes through mpl_call. Every closure uses C++ lambdas with explicit capture lists computed by buildLambdaCaptureList (see ./closures.md).
Cross-references
- Hoisting logic that produces the
static mpl_value … = __mpl_undefined__;lines:./hoisting.md - How
mpl_call,mpl_get_value,mpl_setare defined:MPL_Standard/System/empl.h - How identifiers are resolved when they aren't in
jsGlobals:./closures.md(class capture access) - Multi-module equivalent of
generate():./modular-mode.md - IR node types being lowered:
../empl/node-kind.md