Backends
MPL Compiler Backends — Overview
MPL Compiler Backends — Overview
The MPL Compiler's backend layer consumes the language-neutral EMPL IR and emits executable code for a target language. This directory documents the backend architecture, the single shipped backend (cpp/), and the roadmap for additional targets.
EMPL IR (ModuleNode | ProgramNode)
────────────────────────────────────
│
▼
┌──────────────────────────────────────┐
│ ILanguageBackend (interface) │
│ core/plugin/ILanguageBackend.h:29 │
├──────────────────────────────────────┤
│ languageName() │
│ fileExtension() │
│ generate(ModuleNode) │
│ generateProgram(ProgramNode) [opt.] │
└──────────────────────────────────────┘
▲
│ implements
│
┌──────────────────────────────────────┐
│ CppBackend (10 037 lines, .h) │
│ backends/cpp/CppBackend.h:19 │
└──────────────────────────────────────┘
| Document | Purpose |
|---|---|
| cpp/index.md | CppBackend.h design philosophy, runtime preamble, language-neutrality guarantees |
| cpp/emitter.md | generate() / generateStatement() / generateExpr() dispatch, mpl_value / mpl_object boxing, helper calls (mpl_call, mpl_get_value, mpl_set) |
| cpp/hoisting.md | Function / variable hoisting, module-level vs function-level vs lambda-level forward declarations, the three hoisting fixes (b43b4af, 32f5694, 5a3f87b) |
| cpp/variant-handling.md | How the std::variant-based mpl_value is unwrapped (post-fix b43b4af) for JS-style semantics |
| cpp/closures.md | Cell-wrapped variable capture, ClosureCaptureMode enum, capture-list construction, [&] vs [=] vs [=, &x] rules |
| cpp/modular-mode.md | --modular mode, per-module .cpp files, the __mpl_module_registry.h system, entry_generated CMake target |
| cpp/special-patterns.md | WebSocket SNI plumbing, host_handle_ref / host_handle_unref, JS destructuring lowering, mpl_is_nullish, optional-chaining |
| cpp/known-issues.md | Known variant-error patterns, hoisting edge cases, debug __host_log__ traces |
| hybrid-equivalence.md | The MPL_HybridCheck tool — static + dynamic equivalence harness for JS↔C++ delta detection |
| future-backends.md | Roadmap for additional backends (Rust, LLVM IR, WebAssembly, Go, Python, JavaScript, C#) |
Layered architecture
The compiler is a five-layer pipeline. Each layer has a single responsibility and a single in/out contract:
┌──────────────────────────────────────────────────────────────────────────┐
│ 1. CLI driver (MPL_Compiler.cpp) │
│ - Parses flags (-t, --modular, --inline-empl-runtime, …) │
│ - Resolves the source file extension → ILanguageFrontend │
│ - Routes to backend::generate() or backend::generateProgram() │
├──────────────────────────────────────────────────────────────────────────┤
│ 2. Frontend (frontends/<lang>/) │
│ - Parses source to ModuleNode (or ProgramNode for multi-file) │
│ - Pure syntax → IR; no semantic analysis │
├──────────────────────────────────────────────────────────────────────────┤
│ 3. Canonicalizer (core/ir/EMPLCanonicalizer.h) │
│ - extractClosureBindings (decides cell-wrap vs by-ref vs static) │
│ - normalize operator strings (e.g. `??` → canonical) │
│ - validate before backend dispatch │
├──────────────────────────────────────────────────────────────────────────┤
│ 4. Backend (backends/<lang>/) │
│ - Walks IR; emits target code │
│ - Manages per-call hoisting, capture lists, runtime preamble │
├──────────────────────────────────────────────────────────────────────────┤
│ 5. Runtime (MPL_Standard/System/empl.h, empl_runtime.h, …) │
│ - Defines mpl_value, mpl_object, mpl_call, mpl_get_value, mpl_set │
│ - Hosts the standard-library implementations │
│ - Linked in by the generated code's #include "empl.h" │
└──────────────────────────────────────────────────────────────────────────┘
The backend layer is strictly downstream of the canonicalizer: every input it sees is already normalized, closure-analyzed, and validated. This isolation is what lets CppBackend.h be a single 10 037-line file without becoming an unmaintainable blob.
Why a single backend file for C++?
CppBackend.h is intentionally monolithic. The reasons, as documented in the C++ backend's CMakeLists.txt:
- One-pass emit — every emit path needs to consult the same hoisting state, the same class context stack, the same closure-capture-mode table. Splitting into multiple files would require sharing dozens of fields across compilation units, paying the include-tax without reducing coupling.
- Unity Build compatibility — the project's CMake setup batches translation units into groups of 25 for compile throughput. A monolithic header is the natural fit.
- Stable output — every change is a local diff; reviewers can see exactly which emit branch changed without jumping files.
- No header bloat for downstream users — the backend is only instantiated by
MPL_Compiler.cpp; nobody else needs to include it.
When a new backend is added (Rust, LLVM, etc.), it is not required to be monolithic. The constraint is interface conformance, not file structure.
ILanguageBackend interface
File: core/plugin/ILanguageBackend.h:29
The interface is intentionally minimal — every backend implements just four members:
| Member | Signature | Default? | Purpose |
|---|---|---|---|
languageName() |
std::string() const |
— | Unique registry key, e.g. "cpp" |
fileExtension() |
std::string() const |
— | Default file extension, e.g. ".cpp" |
generate(module) |
std::string(ModuleNodePtr) |
— | Single-module code generation (mandatory) |
generateProgram(program) |
ProgramGenerationResult(ProgramNodePtr) |
flatten-all → generate() | Multi-module generation; backends SHOULD override |
// core/plugin/ILanguageBackend.h:29-94
class ILanguageBackend {
public:
virtual ~ILanguageBackend() = default;
virtual std::string languageName() const = 0; // line 36
virtual std::string fileExtension() const = 0; // line 41
virtual std::string generate(
const std::shared_ptr<ir::ModuleNode>& module) = 0; // line 48
virtual ProgramGenerationResult generateProgram(
const std::shared_ptr<ir::ProgramNode>& program); // line 56
};
ProgramGenerationResult
File: core/plugin/ILanguageBackend.h:16-23
struct ProgramGenerationResult {
std::unordered_map<std::string, std::string> files; // path → source
std::string entryFilePath; // entry compilation unit
std::vector<std::string> compilationOrder; // dep-first topological order
};
The default implementation of generateProgram() flattens every module into a single synthetic ModuleNode (line 60-92) and delegates to generate() — this lets a single-module backend handle multi-module programs by producing one output file. Backends that want per-module emission (like CppBackend) override it.
Language neutrality contract
Backends MUST NOT branch on module->sourceLanguage. The IR guarantees C1-C8 neutrality (see ../empl/index.md); the backend must respect them. Examples of prohibited patterns:
| Pattern | Where | Why prohibited |
|---|---|---|
if (module->sourceLanguage == "javascript") |
anywhere in CppBackend | Treats one source language specially |
Hard-coding JS-only operators in generateExpr |
CppBackend.h:8640 jsFnGlobals table | Allowed only because the names map to runtime helpers, not because the source is JS — these tables are populated identically for any frontend that emits an IdentifierNode named Object / Array / etc. |
dynamic_cast<...> on EMPLNode |
never | IR uses node->kind (EMPLNode.h:534) plus std::static_pointer_cast after a kind check |
Registry plumbing
File: core/plugin/PluginRegistry.h:12-96
class PluginRegistry {
public:
static PluginRegistry& instance(); // line 14
void registerFrontend(unique_ptr<ILanguageFrontend>); // line 19
void registerBackend(unique_ptr<ILanguageBackend>); // line 28
ILanguageFrontend* getFrontend(string langOrExt) const; // line 45
ILanguageBackend* getBackend(string lang) const; // line 56
vector<string> availableFrontends() const; // line 61
vector<string> availableBackends() const; // line 67
};
The registry is a thread-safe singleton (std::mutex mu_ at line 92). Frontends are dual-keyed by language-name (JavaScript) and file extension (.js); backends are keyed only by language-name (cpp). Resolution order in getFrontend() is language → extension (line 49-53).
Driver-level integration
File: MPL_Compiler.cpp:4065-4142
auto* backend = reg.getBackend(options.targetLanguage); // line 4065
if (!backend) { /* error: no such backend */ }
if (program && options.modular) { // line 4079
auto progResult = backend->generateProgram(program); // line 4082
// write progResult.files into modOutDir
// generate CMake build scaffold (CppBackend path)
} else { // line 4119
std::string cppCode = backend->generate(module); // line 4122
// write single file + scaffold
}
| CLI flag | Effect | See |
|---|---|---|
-t cpp / --target cpp |
Select backend by language name | MPL_Compiler.cpp:3823 |
--modular |
Route to generateProgram() instead of generate() |
MPL_Compiler.cpp:3846 |
--modular-out <dir> |
Output directory for modular mode (default tests/modules/<stem>/) |
MPL_Compiler.cpp:3848 |
--inline-empl-runtime / --embed-empl-runtime |
Embed runtime in single-file mode | MPL_Compiler.cpp:3844 |
--emit-empl / --emit-empl-only |
Print EMPL IR before backend dispatch | MPL_Compiler.cpp:3833-3837 |
--zero-diff / --ecma2025-strict |
Reject programs whose JS↔C++ semantics may diverge | MPL_Compiler.cpp:3840 |
The driver delegates all backend selection to PluginRegistry. Adding a new backend requires zero changes outside backends/<lang>/ and a single registerBackend() call in MPL_Compiler.cpp:46.
Per-backend file layout
backends/
└── cpp/
├── CppBackend.h ← the entire C++20 backend lives here (10 037 lines)
├── MPL_Standard.h ← pulls in the runtime helpers (empl.h, empl_core.h, etc.)
├── CppBackend.h.orig ← pre-edit snapshot
├── CppBackend.h.fix_backup ← last good state before a recent fix
└── CppBackend.h.pre_fix ← snapshot just before a fix landed
The .h extension is intentional — CppBackend is a single header-only class template-free implementation compiled via Unity Build (see CMakeLists.txt). The backup files in backends/cpp/ exist to support bisecting the three post-fix corrections documented in cpp/hoisting.md.
Cross-references
- EMPL IR (the consumed format):
../empl/index.md - IR node types referenced throughout backend code:
../empl/node-kind.md - Standard-library call lowering:
../standard-library/index.md - Frontend layer that produces IR for these backends:
../frontends/index.md - Equivalence harness for JS↔C++ parity:
hybrid-equivalence.md