Backends
Special Lowering Patterns
Special Lowering Patterns
This document covers the bespoke lowerings the C++ backend applies to language constructs that don't have a one-to-one C++ analog. Each section explains the source-language construct, the IR form, and the C++ output.
| Pattern | File:line | Source construct |
|---|---|---|
JS destructuring (const {a, b} = obj) |
CppBackend.h:5549-5576 |
VariableDeclNode with pattern |
Optional chaining (obj?.prop) |
CppBackend.h:8000-8006 |
IndexAccessNode with metadata["optional"] == "true" |
Nullish coalescing (a ?? b) |
CppBackend.h:8134-8138 |
BinaryOpNode { op: "??" } |
BigInt literals (123n) |
CppBackend.h:8480-8482 |
FunctionCallNode { name: "__bigint__" } |
| Tagged template literals | CppBackend.h:8376-8393 |
FunctionCallNode { metadata["tagged_template"] == "true" } |
Spread (...args) |
CppBackend.h:8217, 8026-8033 |
UnaryOpNode { op: "..." } |
| Destructuring source synthetic | CppBackend.h:2592-2596 |
VariableDeclNode whose name starts with __mpl_destr_src_ |
new Constructor(...) |
CppBackend.h:8491-8630 |
FunctionCallNode with metadata["from_new"] or name == "new" |
| Proxy traps | CppBackend.h:4050, 8483-8485 |
FunctionCallNode { name: "Proxy" } |
super(...) / super.method() |
CppBackend.h:8316-8343 |
FunctionCallNode { name: "super" }, __mpl_super_* |
await / yield |
CppBackend.h:~8900, ~8920 |
AwaitNode, YieldNode |
| JS class hoisting | CppBackend.h:5009-5019, 4990-5007 |
ClassDeclNode with metadata["js.class"] == "true" |
| WebSocket SNI plumbing | (runtime header; referenced in generated code) | __mpl_ws_* |
host_handle_ref / host_handle_unref |
(runtime header; referenced in generated code) | runtime contract helpers |
JS destructuring
File: CppBackend.h:5549-5576
Array destructuring
const [a, b, c] = [1, 2, 3];
The frontend produces a VariableDeclNode with pattern.kind == Array. The backend emits:
mpl_value __mpl_destr_tmp_1 = mpl_box_value(
std::vector<mpl_value>{
mpl_box_value(static_cast<long double>(1)),
mpl_box_value(static_cast<long double>(2)),
mpl_box_value(static_cast<long double>(3)),
}
);
mpl_value a = mpl_get(__mpl_destr_tmp_1, 0);
mpl_value b = mpl_get(__mpl_destr_tmp_1, 1);
mpl_value c = mpl_get(__mpl_destr_tmp_1, 2);
(Where mpl_get(value, idx) is the variant-aware getter; mpl_get_value is the property-key version — see ./variant-handling.md.)
Object destructuring
const { a, b: renamed, c = 42 } = obj;
The IR has pattern.kind == Object, pattern.elements[i].sourceKey (the original key), pattern.elements[i].targetName (the new name), pattern.elements[i].defaultValue (optional default).
mpl_value __mpl_destr_tmp_2 = mpl_box_value(<expression-for-obj>);
mpl_value a = mpl_get(__mpl_destr_tmp_2, "a");
mpl_value renamed = mpl_get(__mpl_destr_tmp_2, "b");
mpl_value c = (mpl_is_nullish(mpl_get(__mpl_destr_tmp_2, "c"))
? mpl_box_value(static_cast<long double>(42))
: mpl_get(__mpl_destr_tmp_2, "c"));
The default-value path uses mpl_is_nullish (defined CppBackend.h:366-369) — undefined and null both trigger the default. This matches JS semantics: { c = 42 } defaults when c === undefined, not when c === null.
Destructuring in a for head
for (const { x, y } of pairs) { … }
The frontend emits a synthetic __mpl_destr_src_<n> VariableDeclNode whose initializer is the iterable. collectFunctionScopedVars recognizes the __mpl_destr_src_ prefix and skips it from hoisting (line 2592-2596), while still visiting the initializer for nested declarations.
Optional chaining (?.)
File: CppBackend.h:8000-8006
const city = user?.address?.city;
The frontend lowers ?. to an IndexAccessNode (or MemberAccessNode) with metadata["optional"] == "true". The backend emits:
auto __tmp = mpl_box_value(user); // mpl_value outer
auto __expr = mpl_get_value(__tmp, "address"); // mpl_value inner
const auto city = mpl_is_nullish(mpl_get_value(__tmp, "address"))
? mpl_box_value(__mpl_undefined__)
: mpl_box_value(mpl_get_value(mpl_get_value(__tmp, "address"), "city"));
The implementation uses a fresh __mpl_opt_base_<n> variable (where <n> comes from switchCounter_) so nested optionals don't shadow each other.
Mixed optional chains
arr?.[0]
Becomes:
([&]() -> mpl_value {
auto&& __mpl_opt_base_X = (arr);
return mpl_is_nullish(__mpl_opt_base_X)
? mpl_box_value(__mpl_undefined__)
: mpl_box_value(mpl_get_value(__mpl_opt_base_X, mpl_box_value(static_cast<long double>(0))));
})()
Notice the double-wrapping — the index is itself boxed for the mpl_get_value call.
Negated optional index
arr?.[0]
The ?. followed by an index-access produces:
auto __idx = mpl_box_value(<index-expr>);
auto __tmp = mpl_box_value(<base-expr>);
return mpl_is_nullish(__tmp)
? mpl_box_value(__mpl_undefined__)
: mpl_box_value(mpl_get_value(__tmp, __idx));
Nullish coalescing (??)
File: CppBackend.h:8134-8138
const port = config.port ?? 8080;
([&]() -> mpl_value {
auto&& __mpl_lhs_X = (mpl_box_value(config.port));
return mpl_is_nullish(__mpl_lhs_X)
? mpl_box_value(mpl_box_value(8080))
: mpl_box_value(__mpl_lhs_X);
})()
Both sides are boxed. The result preserves the original LHS type (string vs number vs object) — only the fallback is unconditionally evaluated only when needed (lazy).
BigInt literals
File: CppBackend.h:8480-8482
const big = 123456789012345678901234567890n;
The frontend lowers BigInt literals to a call to __bigint__ with the digit string. The backend bypasses mpl_call (which would try to treat __bigint__ as a JS callable object) and invokes the C++ helper directly:
mpl_value __mpl_destr_tmp_X = mpl_box_value(__bigint__(mpl_to_string(mpl_box_value(std::string("123456789012345678901234567890")))));
The double box (mpl_box_value(mpl_box_value(...))) is necessary because mpl_to_string(mpl_box_value(...)) flattens one layer, and the result needs to round-trip through the variant cleanly.
__bigint__ is defined in MPL_Standard/System/empl.h to call boost::multiprecision::cpp_int::cpp_int("…").
Tagged template literals
File: CppBackend.h:8376-8393
const result = html`<p>${name}</p>`;
The frontend splits tagged-template calls into a sequence of positional args. The metadata["tagged_template"] == "true" flag tells the backend to evaluate each argument once (in case it has side effects) before passing them to the tag function:
([&](){
auto&& __mpl_arg_X_0 = (mpl_box_value(<cooked-strings-array>));
auto&& __mpl_arg_X_1 = (mpl_box_value(name));
return html(__mpl_arg_X_0, __mpl_arg_X_1);
})()
The lambda wrapper is what guarantees each argument is evaluated exactly once.
Spread operator (...)
File: CppBackend.h:8217, 8026-8033, 8072-8092
Spread in array literal
const x = [1, ...rest, 4];
The frontend marks the spread element as UnaryOpNode { op: "...", operand: <iterable> }. The backend detects the spread inside ArrayLiteralNode and switches to a boxed vector lambda:
([&]() {
std::vector<mpl_value> __arr;
__arr.push_back(mpl_box_value(1));
for (const auto& __v : __mpl_for_of__(rest)) {
__arr.push_back(mpl_box_value(__v));
}
__arr.push_back(mpl_box_value(4));
return __arr;
})()
__mpl_for_of__ is the runtime helper that adapts any iterable (array, Map, Set, generator) to a C++ range-for.
Spread in function call
foo(...args, 5);
The backend emits:
__mpl_for_of_call__(foo, mpl_call_arg_spread(args), mpl_box_value(5))
(Implementation detail: spread in call position goes through the runtime's __mpl_for_of_call__ which packs the spread elements into the call argument vector.)
Spread as expression
const x = ...obj; // not valid syntax, but: const x = obj; (no spread)
The backend's UnaryOpNode { op: "..." } handler simply returns the operand expression (line 8217-8219) — this is a defensive default for IR nodes where the operand has already been destructured by the frontend.
new Constructor(...)
File: CppBackend.h:8491-8630
JS new is lowered two ways depending on which frontend form is used:
metadata["from_new"] == "true" form
const x = new Foo(1, 2);
([&]() -> mpl_value {
auto&& __new_ctor_X = (Foo); // lookup the constructor
auto __new_inst_X = std::make_shared<mpl_object>(); // allocate the instance
mpl_copy_prototype(__new_inst_X,
mpl_get_value(__new_ctor_X, "prototype")); // set up prototype chain
auto __new_boxed_X = mpl_box_value(__new_inst_X);
auto __prev_this_X = mpl_new_this_ptr(); // save previous `this`
mpl_new_this_ptr() = &__new_boxed_X; // install new `this`
auto __new_ret_X = mpl_box_value(mpl_call(__new_ctor_X,
mpl_box_value(1), mpl_box_value(2)));
mpl_new_this_ptr() = __prev_this_X; // restore previous `this`
// JS semantics: if constructor returns an object, use it instead of `this`
if (auto* __p_X = std::get_if<std::shared_ptr<mpl_object>>(&__new_ret_X)) {
if (*__p_X) return __new_ret_X;
}
return __new_boxed_X;
})()
The __new_boxed_X allocation and prototype-copy are what makes new Foo() produce a real mpl_object with __proto__ pointing at Foo.prototype. The mpl_new_this_ptr() save/restore is what lets nested new calls work correctly (line 8582, 8617).
FunctionCallNode { name: "new" } form
Some frontends lower new X(...) to FunctionCallNode { name: "new", arguments: [X, …args] }. The backend handles this identically (line 8598-8630), but pulls the constructor out of arguments[0] instead of from call->name.
Special case: new Date(...)
const d = new Date(2024, 0, 1);
The backend skips the prototype-copy ceremony and uses mpl_call(Date, …) directly (line 8493-8500). Date is one of the few JS globals whose constructor is implemented in the runtime as a value-producing function rather than a class with a prototype.
Proxy traps
File: CppBackend.h:4050, 8483-8485
const p = new Proxy(target, {
get(target, key) { return target[key] * 2; },
set(target, key, value) { target[key] = value; return true; }
});
The frontend lowers this to a FunctionCallNode { name: "Proxy", arguments: [target, handler] }. The backend calls:
__Proxy_create__(target, handler)
__Proxy_create__ (defined in the runtime) installs the traps. The trap wrapper (line 4050, proxyTrapWrapperExpr) generates the correct arity for each trap type (get/has/set/defineProperty/etc.):
| Trap | Arity | Wrapper |
|---|---|---|
get |
3 | [&](auto target, auto key, auto recv) -> mpl_value { return handler_get(target, key, recv); } |
set |
4 | [&](auto target, auto key, auto value, auto recv) -> mpl_value { return handler_set(target, key, value, recv); } |
has |
2 | [&](auto target, auto key) -> mpl_value { return handler_has(target, key); } |
deleteProperty |
2 | [&](auto target, auto key) -> mpl_value { return handler_delete(target, key); } |
super(...) / super.method(...)
File: CppBackend.h:8316-8343
super(...) in a class constructor
class B extends A {
constructor(x) {
super(x);
this.y = 0;
}
}
The backend emits:
__mpl_super_call__(A, __this, mpl_box_value(x));
this->y = mpl_box_value(0);
__mpl_super_call__ invokes the parent class's constructor with the current __this and the forwarded args. The super() call must happen before any this.x = … access, which the JS frontend enforces via static analysis (the IR emits super() first in the constructor body).
super.method(...)
class B extends A {
greet() { return super.greet() + " B"; }
}
Becomes:
mpl_value B::greet() {
auto __super_ret = __mpl_super_method_call__(A, __this, std::string("greet"));
return mpl_box_value(mpl_add(__super_ret, mpl_box_value(std::string(" B"))));
}
__mpl_super_method_call__ looks up the method on the parent class's prototype and invokes it with __this as the receiver.
super.prop (read)
class B extends A { whoami() { return super.name; } }
Becomes:
mpl_value B::whoami() {
return __mpl_super_get__(A, __this, std::string("name"));
}
await / yield
File: CppBackend.h:~8900, ~8920 (referenced in dispatch tables)
await expr
const result = await fetch(url);
mpl_await(fetch(url));
mpl_await (preamble line 129-176) is overloaded for two cases:
| Argument type | Behavior |
|---|---|
mpl_promise<T> |
Drains microtask queue, returns p.get() |
| Any other type | Returns the value unchanged (synchronous passthrough) |
The microtask queue (mpl_run_microtasks, line 114-121) ensures that other async work scheduled during the await body gets to run before the result is read.
yield expr (in a generator)
function* gen() {
yield 1;
yield 2;
yield 3;
}
Generators are lowered to an mpl_generator<T> (preamble line 137-159). The body becomes:
mpl_generator<long double> __gen;
__gen.yield(static_cast<long double>(1));
__gen.yield(static_cast<long double>(2));
__gen.yield(static_cast<long double>(3));
return __gen;
The generator holds yielded values in a std::vector<T>. mpl_yield(gen, value) (preamble line 161-164) is the yield-statement macro — it both records the value and returns it for the surrounding expression context.
mpl_yield (preamble line 172-176) is also overloaded for non-generator contexts to drain the microtask queue — this is what makes yield valid outside of generator functions (it then behaves like await).
JS class hoisting
File: CppBackend.h:4990-5019, 4526-4577
JS class declarations have a hoisting quirk: class X { … } is hoisted but in the temporal dead zone until evaluation. To simulate this in C++, the backend:
- Detects
ClassDeclNodewithmetadata["js.class"] == "true"(line 4982-4986). - Emits a
static mpl_value __mpl_js_class_value_X = __mpl_undefined__;slot at module scope (line 5004-5005). - Emits the class body as a constructor lambda that assigns to the slot.
- References to
Xelsewhere in the module resolve to__mpl_js_class_value_X(line 7886-7888).
The visible-class stack (visibleJsClassNamesStack_) tracks which class names are accessible at the current scope, mirroring JS lexical scoping.
Class expressions
const X = class { greet() { return "hi"; } };
Class expressions are emitted into the same __mpl_js_class_value_X slot — but with isLoweredClassExprCtorAliasNode (line 3990) returning true, which suppresses the default [=] capture for the constructor lambda. This is because the class may reference itself in its constructor body, and [=] would attempt to copy the uninitialized slot.
WebSocket SNI plumbing
WebSocket support is exposed via runtime helpers defined in MPL_Standard/System/empl_runtime.h (not in CppBackend.h directly, but referenced by generated code). The contract:
| Helper | Purpose |
|---|---|
__mpl_ws_create(url, protocols) |
Open a WebSocket connection |
__mpl_ws_send(handle, payload, opcode) |
Send a frame |
__mpl_ws_close(handle, code, reason) |
Close the connection |
__mpl_ws_set_sni(hostname) |
Set the SNI hostname for the next TLS handshake |
__mpl_ws_on(handle, event, listener) |
Register a listener for open / message / close / error |
The SNI helper is what allows code like:
const ws = new WebSocket("wss://api.example.com/stream");
to negotiate TLS with SNI = api.example.com instead of the URL host (relevant when behind a CDN).
The C++ backend emits calls to these helpers verbatim — WebSocket is in jsGlobals (line 7896-7982) as a global lookup, and the new WebSocket(...) constructor is in jsFnGlobals2 (line 8513-8562). The lowering is identical to other JS class constructors (see ./closures.md for the new lowering).
host_handle_ref / host_handle_unref
These are runtime contract helpers used by the JS frontend when lowering Node-API (N-API) native addons or Worker / SharedArrayBuffer interop. They are declared in MPL_Standard/System/empl_runtime.h and called from generated code:
| Helper | Purpose |
|---|---|
host_handle_ref(h) |
Increment the host-side reference count for handle h |
host_handle_unref(h) |
Decrement; when zero, the host may free the underlying resource |
Generated code emits these around new Worker(…) and around explicit native addon constructors:
const w = new Worker("./worker.js", { type: "module" });
auto __worker_handle = mpl_call(Worker, mpl_box_value("./worker.js"),
mpl_box_value(/* options obj */));
host_handle_ref(__worker_handle);
host_handle_unref is emitted at the C++ scope-exit ({ … } block end) when a variable falls out of scope.
The handlers exist because the JS frontend translates native handles into mpl_value-wrapped pointers; without host_handle_ref/unref, the host garbage collector would prematurely reclaim the underlying native resource.
Cross-references
- Destructuring source pattern in hoisting:
./hoisting.md§__mpl_destr_src_skip mpl_is_nullishdefinition:./variant-handling.mdnewlowering details:./closures.md§ class hoisting- Class-context
thiscapture:./closures.md§ worked example 2 - Runtime preamble where
mpl_await,mpl_generator,mpl_yield, etc. live:./index.md§ Runtime preamble