Elvelt

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:

  1. 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 the mpl_ prefix and never the js_ prefix when they could equally serve Python or C# semantics.
  2. The backend is the only place that knows about C++ syntax. Frontends produce IR; canonicalizers strip syntactic sugar; only CppBackend ever emits auto, std::variant, std::shared_ptr<mpl_object>, lambda capture lists, or std::function<void()>.
  3. One mpl_value flows through every expression. Type-erased values travel as std::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.
  4. The backend is large but flat. All state lives in private: member variables on CppBackend (line 2387-2433) and is reset at the top of every generate() 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 CppBackend instance can serve many generate() calls without leaking state.
  • Multi-module programs (where generateProgram() calls generate() per module) work correctly because extractModuleBody_ invokes generate() for each module and the state is fresh each time.
  • Tests can use a single global CppBackend instance and call generate() 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:

  1. 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 separate MPL_Standard/Backends/Cpp/Runtime.h header — currently it's inline because it must be in the same TU as the user's main.

  2. Per-IR-node dispatch tables. Every IR node kind becomes a switch branch in either generateStatement() or generateExpr(). With 38+ node kinds and an average of 30 lines per branch (including the std::static_pointer_cast<ir::XNode> and the C++ emission), the dispatcher alone is ~1 200 lines.

  3. JS-quirk bookkeeping. jsFnGlobals, jsFnGlobals2, jsGlobals tables (~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).


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