Backends
CppBackend.h — Overview
CppBackend.h — Overview
Source: MPL_Compiler/backends/cpp/CppBackend.h (10 037 lines)
Class: mpl::backends::CppBackend (line 19)
Implements: mpl::plugin::ILanguageBackend (core/plugin/ILanguageBackend.h:29)
CppBackend is the only backend currently shipped with MPL_Compiler. It lowers every EMPL IR construct (literals, expressions, statements, classes, modules, programs) to self-contained C++20 source code that links against the language-neutral MPL runtime (MPL_Standard/System/empl.h).
Design philosophy
The backend follows four principles that are enforced by review and by the lack of sourceLanguage branches anywhere in the implementation:
- The runtime is language-neutral. Every JS-specific runtime primitive (
mpl_undefined_t,mpl_value,mpl_object,mpl_symbol,mpl_bigint,mpl_generator, microtask queue,mpl_call,mpl_get_value,mpl_set,mpl_loose_eq,mpl_strict_eq) is defined in the preamble emitted at the top of every translation unit (lines 50-1750). These names have thempl_prefix and never thejs_prefix when they could equally serve Python or C# semantics. - The backend is the only place that knows about C++ syntax. Frontends produce IR; canonicalizers strip syntactic sugar; only
CppBackendever emitsauto,std::variant,std::shared_ptr<mpl_object>, lambda capture lists, orstd::function<void()>. - One
mpl_valueflows through every expression. Type-erased values travel asstd::variant<mpl_undefined_t, nullptr_t, bool, long double, std::string, mpl_bigint, mpl_symbol, shared_ptr<mpl_object>>(line 333). The C++ type system is intentionally not used to encode source-language types — that would re-introduce C++-specific quirks into the lowering. - The backend is large but flat. All state lives in
private:member variables onCppBackend(line 2387-2433) and is reset at the top of everygenerate()call (line 24-43). There are no free functions and no globals — this keeps the backend reentrant and trivially testable.
File layout at a glance
| Lines | Region | Purpose |
|---|---|---|
| 1-15 | Header preamble | Includes (<sstream>, <regex>, <boost/multiprecision/cpp_int.hpp>) |
| 19 | Class declaration | CppBackend : public ILanguageBackend |
| 21-22 | Identity | languageName() → "cpp", fileExtension() → ".cpp" |
| 24-2245 | generate(ModuleNode) |
Top-level driver: preamble + hoisting + statement dispatch |
| 2264-2385 | generateProgram(ProgramNode) |
Multi-module generation, module registry, entry file |
| 2387-2433 | Private state | All hoisting, closure, class context stacks |
| 2557-2718 | collectFunctionScopedVars |
Hoisting pass 1 — module-level bindings |
| 2720-2886 | collectNestedNonCallableAssignments |
Hoisting pass 2 — nested re-assignments |
| 3231-3239 | collectCommaIdentifiers |
Helper for comma-operator seqs |
| 3290-3547 | buildSequencedCppCall / buildDynamicCall* |
Direct vs dynamic dispatch wrappers |
| 3816-3884 | ClosureCaptureMode enum + closureCaptureModeFor |
Per-var capture mode resolution |
| 3886-3937 | collectReferencedCapturedCellVars |
Cell-wrapped captures for closure bodies |
| 4894-4972 | buildLambdaCaptureList |
Produces [=, &x, __this] capture strings |
| 4990-5019 | jsClassValueStorageName / isVisibleJsClassName |
JS class hoisting helpers |
| 5409-7809 | generateStatement |
Statement-level dispatch (large; all control flow, declarations, print, stdlib calls) |
| 7810-9792 | generateExpr |
Expression-level dispatch (literals, binary ops, calls, member access, lambdas) |
| 9794-9805 | sanitizeModuleId_ |
Replace non-alphanumeric with _ for C++ identifiers |
| 9811-9880 | extractModuleBody_ |
Strip main() wrapper from generated code for modular mode |
| 9883-9980 | generateModuleRegistryHeader_ |
Emit the shared __mpl_module_registry.h |
| 9983+ | generateMainEntryFile_ |
Emit __mpl_main.cpp driver |
The class is not split into multiple translation units — the .h extension and inline definitions are deliberate. The Unity Build (CMakeLists.txt) batches this single file into 25-file groups for compile throughput.
Top-level dispatch (generate)
File: CppBackend.h:24-2245
generate() is a long but mechanical routine. Its outline is:
1. Reset all per-instance state (line 24-43)
2. Emit the runtime preamble (helpers, mpl_value, etc.) (line 50-1750)
3. Hoist module-level variables and functions (line 1865-2130)
4. Build the emit ordering (forward-decl graph) (line 2188-2214)
5. Emit module statements with hoisting-aware ordering (line 2205-2231)
6. Wrap into `int main(int argc, char** argv) { … }` (line ~2235)
The state reset on line 24-43 is critical — the same CppBackend instance is reused across multiple generate() calls during multi-module programs (see cpp/modular-mode.md). All std::unordered_set<std::string> and std::vector<…> members are .clear()-ed, and moduleSourceLanguage_ is captured for downstream use.
Runtime preamble (what generate() emits at the top)
The first ~1700 lines of every translation unit are the same. They define the vocabulary the rest of the code uses. The complete list:
| Helper | Defined at | Purpose |
|---|---|---|
mpl_bigint |
line 88 | boost::multiprecision::cpp_int alias |
mpl_to_string<T> |
line 90-104 | Generic formatter (handles NaN/Inf, dispatches by std::is_floating_point / std::is_integral / requires(oss << value)) |
mpl_microtask / mpl_queue_microtask / mpl_run_microtasks / mpl_run_event_loop |
line 106-124 | Microtask queue (JS event-loop semantics) |
mpl_promise<T> / mpl_await / mpl_yield |
line 127-176 | Async/await + generator scaffolding |
mpl_js_error / mpl_throw_*_error / mpl_set_strict_mode / mpl_is_strict_mode |
line 182-194 | Strict-mode error model (shared with non-JS frontends) |
mpl_undefined_t / __mpl_undefined__ |
line 197-200 | JS undefined sentinel (used by every frontend that lacks a null literal) |
mpl_symbol / mpl_symbols::* / Symbol() / Symbol.for / Symbol.keyFor |
line 204-363 | Unique-symbol type + well-known constants |
mpl_property_descriptor / mpl_object / mpl_next_object_id |
line 329-331 | Object storage with insertion order, prototype chain |
mpl_value (the variant) |
line 333 | std::variant<mpl_undefined_t, nullptr_t, bool, long double, string, bigint, mpl_symbol, shared_ptr<mpl_object>> |
mpl_box_value<T> |
line 334-350 | Forwarding mpl_value constructor with if constexpr type-dispatch |
mpl_to_string(mpl_object) / mpl_to_string(shared_ptr<mpl_object>) |
line 351-352 | Object → JSON-ish debug string |
__Symbol_keyFor__ |
line 355-363 | Runtime helper for Symbol.keyFor |
mpl_is_nullish<T> / overloads |
line 366-369 | True for undefined / null (used by ??, ?., default values) |
mpl_truthy<T> / overloads |
line 372-384 | Falsy per language semantics (handles NaN, 0, "", etc.) |
mpl_is_cstr<T> / mpl_is_string_like_v / mpl_is_object_like_v / mpl_is_bigint_v |
line 386-400 | Compile-time predicates for if constexpr dispatch |
The preamble is not emitted separately for modular mode — extractModuleBody_ (line 9811) strips the int main() wrapper from the already-preamble'd code and re-uses it inside each module's __mpl_module_init().
Language-neutrality guarantees (B1-B6)
The C++ backend, like the IR it consumes, is contractually neutral. Adding a new frontend MUST NOT require any modification of CppBackend.h (it MAY require a new entry in jsGlobals / jsFnGlobals / jsFnGlobals2 tables if the new language emits identifiers not yet listed — see B3 below).
B1 — No sourceLanguage branching
A full grep of CppBackend.h for sourceLanguage returns only the string-literal in the auto-generated header comment (line 51, 53) and one metadata-key read (module->sourceLanguage at line 43, captured only for the comment). The code path that follows is identical for every source language.
B2 — All variant unwrapping goes through std::visit or std::get_if
Expressions never use static_cast<mpl_value> or reinterpret_cast to bypass the variant. See cpp/variant-handling.md for the post-b43b4af rules.
B3 — JS global-name tables are language-neutral by intent
jsGlobals (line 7896-7982), jsFnGlobals (line 8638-8703), and jsFnGlobals2 (line 8513-8562) map specific identifier strings to runtime helpers. They are named js* for historical reasons — every other source language that needs to refer to Object, Array, Promise, Map, etc. emits the same identifier strings into its IR (Python: Promise(...) is unusual but valid; C#: not used). A new frontend that emits a different identifier (e.g. .NET instead of Object) only needs to add an entry — no structural change.
B4 — Strict-mode is a flag, not a hard-coded branch
The mpl_set_strict_mode(true) call is emitted when a JS frontend marks module.metadata["strict_mode"] == "true" (line 47). Other frontends that need the same runtime checks (Python disallow_undeclared_globals, for example) can set the same flag.
B5 — No JS-only types in the helper vocabulary
mpl_value carries every primitive a frontend may emit. There is no JS-specific JSValue class — even BigInt and Symbol (which Python and C# lack in source form) are general-purpose C++ types in the preamble, and the IR uses LiteralNode with the appropriate variant alternative.
B6 — The runtime helpers are not coupled to JS error model
mpl_js_error (line 182) is the name of the exception class, but it is the only exception class emitted. Frontends that prefer different error names can re-emit as mpl_value from their own catch blocks; the backend does not assume JS-style error objects.
Performance and build characteristics
| Property | Value | Source |
|---|---|---|
| Translation-unit size | 10 037 lines | wc -l backends/cpp/CppBackend.h |
| Public ABI | class CppBackend (single class, virtual ~ILanguageBackend) |
line 19 |
| Thread safety | None (state is per-instance, but PluginRegistry is locked) |
PluginRegistry.h:92 |
| Compile time | < 30 s cold, ~5 s incremental | measured via cmake --build . --parallel |
| Output size per module | ~150 KB of preamble + body | line 50-1750 |
| Memory footprint | Bounded by std::stringstream buffer |
generate() returns ss.str() |
State management
Every CppBackend instance owns its mutable state. There are no free functions, no globals, no static caches. The fields are declared on lines 2387-2433 and reset at the top of every generate() call.
Per-instance state categories
| Category | Members | Purpose |
|---|---|---|
| Identity | switchCounter_, thisBindingDepth_, captureSuppressCount_ |
Monotonic counters and nesting depths |
| Class context | classNames_, jsClassNames_, classStaticFieldAccessors_, classInstanceHints_, classBaseNames_, classConstructorParamTypes_, classMethodParamTypes_, classCaptureAccessStack_, classContextStack_, visibleJsClassNamesStack_, deferredClassCaptureHooksStack_ |
All hoisted-class lowering state |
| Hoisting | hoistedVarNames_, moduleHoistedFunctionNames_, moduleScopeVarNames_, localHoistedValueVarsStack_, localHoistedFunctionVarsStack_, localHoistedStaticStorageStack_, localInitializedVarsStack_, localBindingNamesStack_, localBindingScopeIds_, closureCaptureScopeBindingIds_, nextLocalBindingScopeId_, pendingSelfCellBindingNames_ |
Forward-declaration and capture resolution |
| Closures | closureCellVarsStack_, lastCapturedCellVars_ |
Cell-wrap tracking |
| Async | asyncFunctionStack_ |
Used by await / yield lowering |
| Constructor | inConstructorContext_ |
Toggled inside class constructors (affects super() handling) |
| Source | moduleSourceLanguage_ |
Captured from module->sourceLanguage only for the auto-generated header comment (line 51-53) |
Reset discipline
At the top of generate() (lines 24-43), every collection member is .clear()-ed, every counter is reset to zero, and the source-language string is captured. This means:
- The same
CppBackendinstance can serve manygenerate()calls without leaking state. - Multi-module programs (where
generateProgram()callsgenerate()per module) work correctly becauseextractModuleBody_invokesgenerate()for each module and the state is fresh each time. - Tests can use a single global
CppBackendinstance and callgenerate()repeatedly without interference.
The discipline is strict: any field added to the class must also be reset in this prologue, or the next generate() call will inherit stale data.
Why the backend is so large
Three structural factors compound to produce a 10 037-line file:
-
The preamble is ~1 700 lines. Every translation unit must define
mpl_value,mpl_object,mpl_symbol,mpl_bigint,mpl_promise,mpl_generator, the microtask queue, the strict-mode flag, and the full operator/control-flow helper vocabulary. Future backends may extract this to a separateMPL_Standard/Backends/Cpp/Runtime.hheader — currently it's inline because it must be in the same TU as the user's main. -
Per-IR-node dispatch tables. Every IR node kind becomes a
switchbranch in eithergenerateStatement()orgenerateExpr(). With 38+ node kinds and an average of 30 lines per branch (including thestd::static_pointer_cast<ir::XNode>and the C++ emission), the dispatcher alone is ~1 200 lines. -
JS-quirk bookkeeping.
jsFnGlobals,jsFnGlobals2,jsGlobalstables (~250 lines combined),closureCaptureModeFor(~60 lines),buildLambdaCaptureList(~80 lines),sanitizeVarName,sanitizeModuleId_,classCaptureAccessFor,classBaseCalleeExpr,classMemberKeyExpr,outerIdentifierExpr,proxyTrapWrapperExpr,staticPropertyKey,cppTypeFromUIR,escapeForCppString— each is a small helper but they accumulate. They are not duplicative; each addresses a specific shape of JS lowering (anonymous class ctor, hoisted callable ref, dotted lookup, etc.).
The total is approximately:
| Region | Lines | % of file |
|---|---|---|
| Runtime preamble | ~1 700 | 17% |
generate() driver + hoisting |
~2 200 | 22% |
generateStatement() |
~2 400 | 24% |
generateExpr() |
~2 000 | 20% |
| Class / closure helpers | ~1 000 | 10% |
| Modular-mode helpers | ~700 | 7% |
The remaining ~37 lines are the class declaration itself and the namespace closures.
Testing the backend
| Test type | Mechanism | Where |
|---|---|---|
| Unit (per IR node) | tests/<frontend>_all_test in CTest |
tests/ |
| Integration (full pipeline) | tests/<frontend>_impl_test with WILL_FAIL TRUE for known failure modes |
tests/CMakeLists.txt |
| Equivalence (JS↔C++) | MPL_HybridCheck (see ../hybrid-equivalence.md) |
tools/HybridEquivalence.cpp |
| Strict-mode regression | js_strict_mode_violation_impl_test (must FAIL) |
tests/CMakeLists.txt |
| Module-mode regression | js_module_duplicate_lexical_impl_test (must FAIL) |
tests/CMakeLists.txt |
The WILL_FAIL TRUE tests are listed in ../../AGENTS.md — they encode correct failures (e.g. strict-mode violations must be rejected by the frontend, so the backend never sees them).
Where to read next
| If you want to… | Read |
|---|---|
| Understand how IR nodes map to C++ constructs | cpp/emitter.md |
| Understand hoisting and forward declarations | cpp/hoisting.md |
Understand how the mpl_value variant is unwrapped |
cpp/variant-handling.md |
| Understand how closures and captures work | cpp/closures.md |
Understand --modular / generateProgram() |
cpp/modular-mode.md |
Look up a specific emit pattern (destructuring, ?., ??) |
cpp/special-patterns.md |
| See known limitations | cpp/known-issues.md |