Elvelt

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_tmpl_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_value is defined (preamble): ./index.md § Runtime preamble
  • Where mpl_box_value is inserted by the backend: ./emitter.md § Boxing discipline
  • How mpl_truthy and mpl_is_nullish are used in special patterns (?., ??): ./special-patterns.md
  • The closure/cell machinery that depends on these variants: ./closures.md