Elvelt

Frontends

Plugin Registration

Plugin Registration

This page documents the plugin registration mechanism for MPL_Compiler frontends and backends, and how to add a 10th language.

The PluginRegistry

// core/plugin/PluginRegistry.h:12-97
class PluginRegistry {
public:
    static PluginRegistry& instance();   // singleton

    void registerFrontend(std::unique_ptr<ILanguageFrontend> fe);
    void registerBackend(std::unique_ptr<ILanguageBackend> be);

    ILanguageFrontend* getFrontendByLanguage(const std::string& lang) const;
    ILanguageFrontend* getFrontendByExtension(const std::string& ext) const;
    ILanguageFrontend* getFrontend(const std::string& langOrExt) const;
    ILanguageBackend* getBackend(const std::string& lang) const;

    std::vector<std::string> availableFrontends() const;
    std::vector<std::string> availableBackends() const;

    // Legacy compatibility helpers
    ILanguageFrontend* getFrontendByExt(const std::string& ext) const;
    ILanguageFrontend* getFrontendLegacy(const std::string& lang) const;
    std::vector<std::string> listFrontends() const;
    std::vector<std::string> listBackends() const;
private:
    PluginRegistry() = default;
    mutable std::mutex mu_;
    std::unordered_map<std::string, std::unique_ptr<ILanguageFrontend>> frontends_;
    std::unordered_map<std::string, std::unique_ptr<ILanguageBackend>> backends_;
    std::unordered_map<std::string, std::string> extToLang_;
};

registerFrontend behaviour

PluginRegistry::registerFrontend (core/plugin/PluginRegistry.h:19):

void registerFrontend(std::unique_ptr<ILanguageFrontend> fe) {
    std::lock_guard<std::mutex> lock(mu_);
    auto name = fe->languageName();
    for (const auto& ext : fe->fileExtensions()) {
        extToLang_[ext] = name;
    }
    frontends_[name] = std::move(fe);
}

The frontend is keyed by both language name and each file extension. Thread-safe (mutex-protected) because the singleton can be touched from worker threads during parallel builds.

Lookup logic

getFrontend(const std::string& langOrExt) (line 45):

  1. If langOrExt starts with ., treat it as an extension.
  2. Otherwise, look up by language name first.
  3. If no language-name match, fall back to extension lookup.

This means getFrontend(".js") and getFrontend("javascript") both return the same JSFrontend*.


registerPlugins() — the single entry point

MPL_Compiler.cpp:32-47:

void registerPlugins() {
    auto& reg = plugin::PluginRegistry::instance();
    reg.registerFrontend(std::make_unique<frontends::JSFrontend>());
    reg.registerFrontend(std::make_unique<frontends::PythonFrontend>());
    reg.registerFrontend(std::make_unique<frontends::CSharpFrontend>());
    reg.registerFrontend(std::make_unique<frontends::GoFrontend>());
    reg.registerFrontend(std::make_unique<frontends::RustFrontend>());
    reg.registerFrontend(std::make_unique<frontends::PHPFrontend>());
    reg.registerFrontend(std::make_unique<frontends::RubyFrontend>());
    reg.registerFrontend(std::make_unique<frontends::JavaFrontend>());
    reg.registerFrontend(std::make_unique<frontends::EMPLSFrontend>());
    reg.registerFrontend(std::make_unique<frontends::SwiftFrontend>());
    reg.registerFrontend(std::make_unique<frontends::KotlinFrontend>());

    reg.registerBackend(std::make_unique<backends::CppBackend>());
}

registerPlugins() is called once during startup (from main, line 3883):

int main(int argc, char** argv) {
    registerPlugins();
    // ... rest of CLI ...
}

File-by-file registration

Frontend Header included at Registration line
JavaScript / TypeScript MPL_Compiler.cpp:17 line 34
Python line 18 line 35
C# line 19 line 36
Go line 20 line 37
Rust line 21 line 38
PHP line 22 line 39
Ruby line 23 line 40
Java line 24 line 41
EMPLS line 25 line 42
Swift line 26 line 43
Kotlin line 27 line 44
// MPL_Compiler.cpp:17-27
#include "frontends/javascript/JSFrontend.h"
#include "frontends/python/PythonFrontend.h"
#include "frontends/csharp/CSharpFrontend.h"
#include "frontends/go/GoFrontend.h"
#include "frontends/rust/RustFrontend.h"
#include "frontends/php/PHPFrontend.h"
#include "frontends/ruby/RubyFrontend.h"
#include "frontends/java/JavaFrontend.h"
#include "frontends/empls/EMPLSFrontend.h"
#include "frontends/swift/SwiftFrontend.h"
#include "frontends/kotlin/KotlinFrontend.h"
#include "backends/cpp/CppBackend.h"

CMake target wiring

The CMakeLists.txt defines per-frontend translation units so the compiler sees the JSFrontend.h, PythonFrontend.h, etc., declarations. The pattern (illustrative, actual layout may vary) is:

add_executable(MPL_Compiler
    MPL_Compiler.cpp

    frontends/javascript/JSFrontend.h
    frontends/javascript/Lexer.cpp
    frontends/javascript/Parser.cpp
    frontends/javascript/JsStdLib.cpp

    frontends/python/PythonFrontend.h
    frontends/python/PythonStdLib.h

    frontends/csharp/CSharpFrontend.h
    frontends/csharp/CSharpFrontend.cpp
    frontends/csharp/CSharpStdLib.cpp
    # ... etc.
)

The 9 production frontends + 2 experimental (Swift, Kotlin) compile into the single MPL_Compiler executable. Unity Build batches them in groups of 25 to reduce compile time.


CLI dispatch

The CLI reads argv[1], looks up the frontend via extension or language name, parses the source, and dispatches to the backend.

// MPL_Compiler.cpp (illustrative)
int main(int argc, char** argv) {
    registerPlugins();

    const std::string inputPath = argv[1];
    const std::string target = (argc > 2 && std::string(argv[2]) == "--target")
        ? argv[3] : "cpp";

    auto& reg = plugin::PluginRegistry::instance();

    // Look up frontend by file extension
    const std::string ext = fs::path(inputPath).extension().string();
    auto* frontend = reg.getFrontendByExtension(ext);
    if (!frontend) {
        std::cerr << "No frontend registered for extension: " << ext << "\n";
        return 1;
    }

    // Look up backend
    auto* backend = reg.getBackend(target);
    if (!backend) {
        std::cerr << "No backend registered for target: " << target << "\n";
        return 1;
    }

    // Parse and generate
    const std::string source = readFile(inputPath);
    auto module = frontend->parse(source, inputPath);
    const std::string output = backend->generate(*module);

    std::cout << output;
    return 0;
}

How to add a 10th language

Step 1 — create the directory

mkdir frontends/mylang/

Step 2 — create the header(s)

// frontends/mylang/MyLangFrontend.h
#pragma once
#include "../../core/plugin/BraceFrontendBase.h"

namespace mpl {
namespace frontends {

class MyLangFrontend : public plugin::BraceFrontendBase {
public:
    std::string languageName() const override { return "mylang"; }
    std::vector<std::string> fileExtensions() const override { return {".ml"}; }

protected:
    std::vector<plugin::BracePrintPattern> printPatterns() const override {
        return { {"println", true, true} };
    }
    bool isComment(const std::string& line) const override {
        return startsWith(line, "//");
    }
    std::shared_ptr<ir::VariableDeclNode> tryParseVarDecl(const std::string& line) override {
        std::string l = trim(line);
        if (startsWith(l, "let ")) {
            size_t eq = l.find('=');
            if (eq == std::string::npos) return nullptr;
            return makeVar(trim(l.substr(4, eq - 4)), trim(l.substr(eq + 1)), true);
        }
        return nullptr;
    }
};

} // namespace frontends
} // namespace mpl

Step 3 — optionally add a StdLib mapping

// frontends/mylang/MyLangStdLib.h
#pragma once
#include <string>
#include <vector>
#include <optional>

namespace mpl {
namespace frontends {
namespace mylang {

struct StdLibMapping {
    std::string functionName;
    std::string mplModule;
    std::string mplFunction;
};

inline std::optional<StdLibMapping> findStdLibMapping(const std::string& name) {
    static const std::vector<StdLibMapping> kMappings = {
        {"println", "Console", "WriteLine"},
        {"abs",     "Math",    "Abs"},
        {"sqrt",    "Math",    "Sqrt"},
    };
    for (const auto& m : kMappings) {
        if (m.functionName == name) return m;
    }
    return std::nullopt;
}

} // namespace mylang
} // namespace frontends
} // namespace mpl

Step 4 — register in MPL_Compiler.cpp

// MPL_Compiler.cpp:17-27 (add the include)
#include "frontends/mylang/MyLangFrontend.h"

// MPL_Compiler.cpp:32-47 (add the registration)
void registerPlugins() {
    auto& reg = plugin::PluginRegistry::instance();
    reg.registerFrontend(std::make_unique<frontends::JSFrontend>());
    // ... existing frontends ...
    reg.registerFrontend(std::make_unique<frontends::MyLangFrontend>());

    reg.registerBackend(std::make_unique<backends::CppBackend>());
}

Step 5 — add to CMakeLists.txt

add_executable(MPL_Compiler
    MPL_Compiler.cpp
    # ... existing sources ...
    frontends/mylang/MyLangFrontend.h
)

Step 6 — add tests

Create tests/mylang_<feature>_test.cpp and add to CMakeLists.txt's test definitions.

Step 7 — build & test

cd MPL_Compiler/build
cmake --build . --parallel
ctest -R mylang --output-on-failure

Threading model

PluginRegistry::registerFrontend and getFrontend* are thread-safe. The CLI uses single-threaded compilation by default but enables parallel source-file compilation via MPL_Compiler.cpp's compileFilesParallel worker pool. Each worker calls getFrontendByExtension once per file.

The mutex (std::mutex mu_) is held only during the map operations themselves, not during the actual parse() call, so multiple workers can parse different files in parallel.


Order of operations

The CLI's flow (at a high level):

argv parsing
   │
   ▼
registerPlugins()              ← populate PluginRegistry singleton
   │
   ▼
readFile(argv[1])              ← load source text
   │
   ▼
frontend->parse(source, fn)    ← PluginRegistry::getFrontendByExtension
   │                              (or by explicit --lang argument)
   ▼
[module graph expansion]       ← resolve imports, multi-file programs
   │                              (JS only — see MPL_Compiler.cpp module
   │                              resolution)
   ▼
backend->generate(module)      ← PluginRegistry::getBackend("cpp")
   │
   ▼
[emit to stdout / --out path]

For multi-file programs (JS modules), registerPlugins() is called once and the same registry serves every file.


Backend registration

There is currently one registered backend: CppBackend.

// MPL_Compiler.cpp:46
reg.registerBackend(std::make_unique<backends::CppBackend>());

Backends follow the same pattern as frontends — subclass ILanguageBackend and implement:

// core/plugin/ILanguageBackend.h
class ILanguageBackend {
public:
    virtual ~ILanguageBackend() = default;
    virtual std::string languageName() const = 0;
    virtual std::string generate(const ir::ModuleNode& module) = 0;
};

A second backend (e.g. JavaScriptBackend or WebAssemblyBackend) would follow the same four-step pattern.


Summary

Step File Lines
Include the frontend header MPL_Compiler.cpp 17-27
Register the frontend MPL_Compiler.cpp 32-47
Call registerPlugins() from main MPL_Compiler.cpp 3883
CMake target CMakeLists.txt various
PluginRegistry core/plugin/PluginRegistry.h 12-97
ILanguageFrontend core/plugin/ILanguageFrontend.h 15-39
BraceFrontendBase (most frontends inherit) core/plugin/BraceFrontendBase.h 29-10539

That's the entire registration mechanism: include, register, ship. The registry handles dispatch transparently.