Backends
`mpl_value` Variant Handling
mpl_value Variant Handling
mpl_value is the C++ std::variant used for every value in every IR expression. It is the language-neutral substitute for C++'s type system: it carries whatever the source language had — undefined, null, bool, long double, std::string, mpl_bigint, mpl_symbol, or a heap-allocated mpl_object — and the backend lowers all numeric, string, and object operations through it.
The variant is defined in the runtime preamble (emitted at the top of every translation unit):
File: CppBackend.h:333
using mpl_value = std::variant<
mpl_undefined_t, // 0 — JS undefined / Python None-equivalent sentinel
std::nullptr_t, // 1 — explicit null
bool, // 2
long double, // 3 — every numeric (int promoted to double)
std::string, // 4
mpl_bigint, // 5 — boost::multiprecision::cpp_int
mpl_symbol, // 6 — unique symbol
std::shared_ptr<mpl_object> // 7 — heap-allocated object
>;
The order is significant: mpl_undefined_t is index 0 so that an empty mpl_value{} constructed via ProgramGenerationResult fallback (line 9934) is "undefined" not "null".
This document covers the rules for constructing, inspecting, and unwrapping the variant. The rules were formalized in the b43b4af commit and are why every expression in the generated code looks the way it does.
Constructing an mpl_value
There are two construction paths: from inside the runtime (the user-facing mpl_box_value) and from inside the backend (literal emission).
mpl_box_value<T> — the universal constructor
File: CppBackend.h:334-350
template <typename T>
inline mpl_value mpl_box_value(T&& v) {
using D = std::decay_t<T>;
if constexpr (std::is_same_v<D, mpl_value>) return v;
else if constexpr (std::is_same_v<D, mpl_undefined_t>) return __mpl_undefined__;
else if constexpr (std::is_same_v<D, std::nullptr_t>) return nullptr;
else if constexpr (std::is_same_v<D, bool>) return v;
else if constexpr (std::is_integral_v<D>
&& !std::is_same_v<D, bool>) return static_cast<long double>(v);
else if constexpr (std::is_floating_point_v<D>) return static_cast<long double>(v);
else if constexpr (std::is_same_v<D, std::string>) return v;
else if constexpr (std::is_same_v<D, const char*>
|| std::is_same_v<D, char*>) return std::string(v ? v : "");
else if constexpr (std::is_same_v<D, mpl_bigint>) return v;
else if constexpr (std::is_same_v<D, mpl_symbol>) return v;
else if constexpr (std::is_same_v<D, std::shared_ptr<mpl_object>>) return v;
else if constexpr (std::is_same_v<D, mpl_object>) return std::make_shared<mpl_object>(v);
else return mpl_to_string(v);
}
| Input type | Result variant index | Notes |
|---|---|---|
mpl_value |
passthrough | Idempotent |
mpl_undefined_t |
0 | The singleton __mpl_undefined__ |
std::nullptr_t |
1 | C++ nullptr |
bool |
2 | Direct |
int, long, size_t, … |
3 | Promoted to long double (precision loss possible — backend uses mpl_bigint for arbitrary-precision literals) |
double, float |
3 | Promoted to long double |
std::string |
4 | Direct |
const char*, char* |
4 | Null → empty string |
mpl_bigint |
5 | Direct |
mpl_symbol |
6 | Direct |
shared_ptr<mpl_object> |
7 | Direct |
mpl_object (by value) |
7 | Heap-allocated via make_shared |
| Anything else | 4 | mpl_to_string(v) — fallback for unknown types |
The "anything else → string" branch is what allows arbitrary user types (e.g. std::vector<X>) to flow through the variant without compile errors — they become a stringified representation. This is rarely hit in practice but prevents downstream surprises.
Backend-side construction (generateExpr)
File: CppBackend.h:7820-7847
Literals are emitted unboxed — the backend trusts the caller to box them:
| IR literal | Emitted C++ | Reason |
|---|---|---|
Literal(int64_t) |
42 |
Caller knows the destination type |
Literal(double) |
1.5 (via std::to_chars) |
Same |
Literal(string) |
std::string("…") |
Could be a std::string or a mpl_value — caller boxes |
Literal(bool) |
true / false |
Same |
Literal(nullptr) |
nullptr |
Same |
The boxing happens at three boundaries: assignment to a mpl_value slot, argument to mpl_call, or explicit mpl_box_value(…) insertion by the backend.
Inspecting an mpl_value
The runtime helpers expose several predicates and accessors. They are the only safe way to read a variant alternative; the backend never uses std::get<T> directly (which would throw on a wrong alternative).
Truthiness and nullish-ness
| Predicate | Defined | Returns true for |
|---|---|---|
mpl_is_nullish(v) |
CppBackend.h:366-369 |
mpl_undefined_t or nullptr_t |
mpl_truthy(v) |
CppBackend.h:372-384 |
non-zero, non-empty, non-nullish — via std::visit (line 384) |
mpl_truthy is the workhorse of control-flow lowering — every if, while, for, ternary, and &&/||/?? consults it. Its mpl_value overload uses std::visit to dispatch on the actual alternative (line 384).
Numeric coercion
template <typename T> long double mpl_to_number(const T& v); // (defined in runtime)
Used to coerce an arbitrary variant alternative to a number. Throws mpl_js_error("TypeError", …) for un-coercible types (e.g. Symbol).
String coercion
mpl_to_string is overloaded for every alternative (lines 90-104, 178-181, 351-352, plus per-symbol/per-object overloads). The variant overload uses std::ostringstream with if constexpr short-circuits for NaN/±Infinity/0.
Type-tag
mpl_typeof(v) returns a string tag: "undefined", "null", "boolean", "number", "string", "bigint", "symbol", "object". Used by typeof (line 8221).
Property access
mpl_get_value(obj, key) and mpl_set(obj, key, value) (defined in MPL_Standard/System/empl.h, referenced at CppBackend.h:7999, 8253):
| Operation | Generated C++ |
|---|---|
obj.prop |
mpl_get_value(obj, "prop") |
obj[key] |
mpl_get_value(obj, key) |
obj.prop = value |
mpl_set(obj, "prop", value) |
obj[key] = value |
mpl_set(obj, key, value) |
For std::shared_ptr<mpl_object> arguments, these helpers walk the prototype chain (the prototype field on mpl_object); for primitive alternatives, they treat the value as a wrapper that holds properties on a hidden box.
The b43b4af fix: structured variant access
Before commit b43b4af, the backend used ad-hoc std::get<…> calls scattered through generateExpr. These would throw std::bad_variant_access when a programmer's code produced an unexpected type. The fix unified all variant access through a small set of helpers and std::visit-based dispatchers.
The post-fix rules (enforced by code review):
Rule 1 — Never std::get<T>(value) directly
std::get_if<T>(&value) is allowed (returns nullptr on mismatch). std::get<T>(value) is forbidden (throws).
| Pattern | Where it's used now |
|---|---|
std::get_if<std::shared_ptr<mpl_object>>(&v) |
CppBackend.h:2331, 2343, 8591, 8625 |
std::holds_alternative<T>(v) |
CppBackend.h:360, 369 |
std::visit([&](const auto& x) { … }, v) |
CppBackend.h:384 (mpl_truthy) |
Rule 2 — Every operator goes through a mpl_<op> helper
mpl_add, mpl_sub, mpl_mul, mpl_div, mpl_mod, mpl_lt, mpl_le, mpl_gt, mpl_ge, mpl_strict_eq, mpl_loose_eq, mpl_pow, mpl_shl, mpl_shr, mpl_urshift, mpl_bitand, mpl_bitor, mpl_bitxor, mpl_in, mpl_instanceof, mpl_instanceof_type<T>, mpl_call, mpl_get_value, mpl_set, mpl_delete, mpl_prefix_inc, mpl_postfix_inc, mpl_prefix_dec, mpl_postfix_dec.
These helpers all take mpl_value arguments, internally unbox (via std::get_if or std::visit), perform the operation with the correct JS-style semantics (NaN propagation, string concatenation, etc.), and return a mpl_value.
Rule 3 — mpl_truthy is the only falsiness check
Wherever the source code says "is this value falsy?", the backend emits mpl_truthy(x) (line 8280 for !x, line 8139 for &&, etc.). Direct !x on a mpl_value would not compile (! doesn't work on a variant), and direct checks like v == mpl_value{} would have wrong semantics.
Rule 4 — String concatenation goes through mpl_to_string or mpl_add
String + is not a direct C++ operator+ — it's mpl_add(a, b) (line 8170). When one side is a string, mpl_add internally calls mpl_to_string on the other side and concatenates. When both sides are numbers, it does numeric addition.
Worked examples
Example 1: numeric comparison
if (a < b) { … }
if (mpl_truthy(mpl_lt(generateExpr(a), generateExpr(b)))) { … }
mpl_lt internally does:
inline mpl_value mpl_lt(const mpl_value& a, const mpl_value& b) {
return std::visit([](auto&& x, auto&& y) -> mpl_value {
if constexpr (std::is_arithmetic_v<decltype(x)> && std::is_arithmetic_v<decltype(y)>) {
return mpl_box_value(x < y);
}
// … handle string, bigint, mixed …
}, a, b);
}
Example 2: optional chaining
obj?.prop
After the frontend lowers ?. to an explicit IndexAccess with metadata["optional"] == "true" (line 8000-8006):
([&]() -> mpl_value {
auto&& __mpl_opt_base_X = (obj);
return mpl_is_nullish(__mpl_opt_base_X)
? mpl_box_value(__mpl_undefined__)
: mpl_box_value(mpl_get_value(__mpl_opt_base_X, "prop"));
})()
Example 3: strict equality with different types
5 === "5" // false
5 == "5" // true (coerces string to number)
null === undefined // false
The two operators route to different helpers (lines 8122-8133):
| Source | Generated |
|---|---|
5 === "5" |
mpl_strict_eq(mpl_box_value(5), std::string("5")) → false (different types) |
5 == "5" |
mpl_loose_eq(mpl_box_value(5), std::string("5")) → true (numeric coercion) |
null === undefined |
mpl_strict_eq(nullptr, __mpl_undefined__) → false (nullptr_t ≠ mpl_undefined_t) |
Worked example: a switch with mixed-type cases
switch (x) {
case 1: return "one";
case "two": return "dos";
default: return "many";
}
The IR has a SwitchNode with cases [(Literal(1), "one"), (Literal("two"), "dos")]. The backend emits:
auto __mpl_switch_0 = (x);
if (mpl_strict_eq(__mpl_switch_0, mpl_box_value(1))) {
return std::string("one");
}
if (mpl_strict_eq(__mpl_switch_0, std::string("two"))) {
return std::string("dos");
}
// default:
return std::string("many");
The use of mpl_strict_eq (not ==) is critical — 1 == "1" would be true under JS semantics, but switch uses strict equality, so the backend uses the strict helper.
Cross-references
- How
mpl_valueis defined (preamble):./index.md§ Runtime preamble - Where
mpl_box_valueis inserted by the backend:./emitter.md§ Boxing discipline - How
mpl_truthyandmpl_is_nullishare used in special patterns (?.,??):./special-patterns.md - The closure/cell machinery that depends on these variants:
./closures.md