Elvelt

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