Elvelt

Frontends

Frontend Architecture

Frontend Architecture

Every language frontend in MPL_Compiler is a subclass of one of two base classes:

Base class File Used by
plugin::ILanguageFrontend core/plugin/ILanguageFrontend.h:15 All frontends (interface)
plugin::FrontendBase core/plugin/FrontendBase.h:17 Line-based trivial fallback
plugin::BraceFrontendBase core/plugin/BraceFrontendBase.h:29 All modern frontends (JS, TS, C#, Go, Rust, PHP, Java, EMPLS)
plugin::IModuleAwareFrontend core/plugin/ILanguageFrontend.h:45 JavaScript — module/strict-mode toggle

PythonFrontend and RubyFrontend are direct subclasses of plugin::ILanguageFrontend because they implement their own line-based, indentation-sensitive parsers (rather than relying on FrontendBase's very limited line-by-line fallback).


1. ILanguageFrontend — the contract

// core/plugin/ILanguageFrontend.h:15
class ILanguageFrontend {
public:
    virtual ~ILanguageFrontend() = default;

    // e.g. "javascript", "python", "csharp"
    virtual std::string languageName() const = 0;

    // e.g. {".js", ".mjs", ".cjs"} for JavaScript
    virtual std::vector<std::string> fileExtensions() const = 0;

    // The main entry: parse source text and produce an IR module.
    virtual std::shared_ptr<ir::ModuleNode> parse(
        const std::string& source,
        const std::string& filename) = 0;
};

PluginRegistry::registerFrontend (core/plugin/PluginRegistry.h:19) consumes these three members to register the frontend by both language name and file extension.

Extended interface: IModuleAwareFrontend

// core/plugin/ILanguageFrontend.h:45
class IModuleAwareFrontend : public ILanguageFrontend {
public:
    virtual bool isForceModuleStrictMode() const { return false; }
    virtual void setForceModuleStrictMode(bool) {}
    virtual bool isModuleMode() const { return false; }
    virtual void setModuleMode(bool) {}
};

Used by JavaScript — JSFrontend exposes hooks that let the CLI toggle strict mode and module mode (--js-source-type module|script|auto).


2. FrontendBase — line-oriented fallback

// core/plugin/FrontendBase.h:17
class FrontendBase : public ILanguageFrontend {
public:
    std::shared_ptr<ir::ModuleNode> parse(
        const std::string& source, const std::string& filename) override
    {
        auto mod = std::make_shared<ir::ModuleNode>();
        mod->name = filename;
        mod->sourceLanguage = languageName();

        for (const auto& raw : splitLines(source)) {
            std::string line = trim(raw);
            if (line.empty() || isComment(line) || isIgnoredLine(line))
                continue;

            auto pn = tryParseStandardLibraryPrint(line);
            if (pn) { mod->statements.push_back(pn); continue; }

            auto vn = tryParseVarDecl(line);
            if (vn) { mod->statements.push_back(vn); continue; }

            auto unsupported = std::make_shared<ir::UnsupportedNode>();
            unsupported->reason = "Unsupported syntax in simplified frontend";
            unsupported->originalText = line;
            mod->statements.push_back(unsupported);
        }
        return mod;
    }
protected:
    virtual std::vector<PrintPattern> printPatterns() const = 0;
    virtual bool isComment(const std::string& line) const = 0;
    virtual bool isIgnoredLine(const std::string&) const { return false; }
    virtual std::shared_ptr<ir::VariableDeclNode> tryParseVarDecl(
        const std::string& line) = 0;
    ...
};

FrontendBase is deliberately minimal — it handles only print(...) calls (mapped to Console.WriteLine via makeStandardLibraryPrint) and simple variable declarations. Anything else becomes an UnsupportedNode. None of the production frontends use it; it exists as a starting point for new trivial language experiments.


3. BraceFrontendBase — the workhorse

BraceFrontendBase (10,539 lines, single header) handles every brace-delimited language in the compiler. Its parse entry is at core/plugin/BraceFrontendBase.h:39:

std::shared_ptr<ir::ModuleNode> parse(
    const std::string& source, const std::string& filename) override {
    auto mod = std::make_shared<ir::ModuleNode>();
    mod->name = filename;
    mod->sourceLanguage = languageName();

    strictMode_ = supportsStrictMode() &&
                  (forceModuleStrictMode_ || detectUseStrictDirective(source));
    if (strictMode_) {
        mod->metadata["strict_mode"] = "true";
    }

    lines_ = preprocess(source);
    index_ = 0;
    mod->statements = parseBlock(false, false);

    if (supportsExportNodes()) {
        for (auto& stmt : mod->statements) {
            if (!stmt) continue;
            auto itExport = stmt->metadata.find("export_name");
            if (itExport != stmt->metadata.end()) {
                stmt->metadata["exported"] = "true";
                auto exNode = std::make_shared<ir::ExportNode>();
                exNode->value = stmt;
                exNode->isDefault = (itExport->second == "default");
                ...
                stmt = exNode;
            }
        }
    }
    return mod;
}

The subclass hooks the parser calls into are:

Hook Purpose Default
supportsStrictMode() Whether "use strict" is honored false
supportsExportNodes() Whether export declarations are auto-wrapped false
printPatterns() List of BracePrintPattern{keyword, usesParens, newline} required
isComment(line) Whether the line is a comment required
isIgnoredLine(line) Whether to skip the line entirely false
tryParseVarDecl(line) var/let/const declarations required
tryParseVarDeclList(line, out) Multi-decl list let a=1, b=2 returns false
tryParseFunctionDeclHeader(line) function foo(...) header returns nullptr
tryParseLanguageSpecificStatement(line, out) Catch-all for imports, exports, decorators, etc. returns false
isExportDefaultDeclarationLike(tail) Detect default-export shape language-agnostic default

Strict mode detection

detectUseStrictDirective (BraceFrontendBase.h:105) is a string-aware scanner that:

  1. Strips UTF-8 BOM (first line only).
  2. Skips shebangs (#!...).
  3. Removes // and /* */ comments while respecting string literals.
  4. Returns true when the first non-string-literal, non-comment directive is "use strict"; or 'use strict';.

The detected flag is stored in mod->metadata["strict_mode"] = "true" and also in strictMode_ (the per-instance boolean that subclasses can read).

Identifier handling

BraceFrontendBase.h:206 defines isIdentChar, isValidIdent, and isValidIdentNoDot. The JS variant accepts $, _, alphanumerics, and UTF-8 bytes ≥ 0x80 (so JS sources with non-ASCII identifiers parse). UTF-8 escape sequences (\u{XXXX}) are normalised by normalizeIdent (line 250+).

Expression parsing

BraceFrontendBase contains a Pratt-style expression parser (parseExpression, parseUnary, parseBinary, …) that handles every operator present in the seven brace-delimited languages. JS extends this with await, yield, optional chaining ?., nullish coalescing ??, private fields #x, decorators @dec, JSX <Foo/>, and tagged templates.


4. IModuleAwareFrontend for JS

// JSFrontend.h:372-373
bool supportsStrictMode() const override { return true; }
bool supportsExportNodes() const override { return true; }

JS is the only frontend that enables both supportsStrictMode and supportsExportNodes. The C++ backend reads mod->metadata["strict_mode"] to decide whether to enforce strict-mode semantics (duplicate bindings, reserved names, octal escapes, etc.).


5. Lexing & parsing per language

Language Lexer file Parser file Strategy
JavaScript frontends/javascript/Lexer.h/cpp frontends/javascript/Parser.h/cpp Full hand-written lexer (680 LOC) + Pratt parser (1,586 LOC). 49 KB of TypeScript-specific extras (namespaces, type assertions, decorators).
C# frontends/csharp/Lexer.h/cpp frontends/csharp/Parser.h/cpp + Sema.h/cpp + Converter.h/cpp Lexer + Sema (semantic analysis for expression bodies) + Converter (source normalizer).
Go frontends/go/Lexer.h/cpp frontends/go/Parser.h/cpp Lexer + parser, used for short-var decls, type declarations, channel sends, for-range.
PHP (none — pure transforms) (none — pure transforms) PHP frontend is a pipeline of static transform* functions in PHPFrontend.h: normalizePHPSource (strip <?php, ?>), convertMatchExpressions, stripNamedArguments, transformTryCatchBindings, stripPhpFunctionModifiers, stripInterfaceMethodDeclarations, transformEnumAccess, transformArrowFunctions, wrapPrintArguments.
Java (none — pure transforms) (none — pure transforms) Pipeline: rewriteJavaTextBlocks, stripJavaMethodModifiers, stripJavaInterfaceMethodDeclarations, rewriteJavaRecordsToClasses, stripJavaVarFromLambdaParams, rewriteJavaArrowToJsArrow, rewriteJavaVarDecls.
Rust (none — pure transforms) (none — pure transforms) Pipeline: rewriteMatchExpressions, rewriteIfLetChains, rewriteIfLetExpressions, rewriteLetElseExpressions.
EMPLS (none — pure transforms) (none — pure transforms) Pipeline: transformLambdaKeyword, transformRangeExpressions, transformPipelineOperator.
Python (line-based parser) (line-based parser) Full bespoke line-driven parser in PythonFrontend.h (2,314 LOC) with indent-stack scope tracker, comprehension lowerer, walrus extraction, decorator collector.
Ruby (line-based parser) (line-based parser) Full bespoke line-driven parser in RubyFrontend.h (1,928 LOC) with scope stack, block literal parser, attr_accessor/attr_reader/attr_writer collector, string interpolation parser.

6. IR generation pipeline (concrete example)

Source (JavaScript):

// example.js
import { readFile } from 'node:fs/promises';
export async function readFirst(path) {
    const buf = await readFile(path);
    return buf.length;
}

Step-by-step:

  1. CLI calls JSFrontend::parse(source, "example.js") (JSFrontend.h:42-94).
  2. TypeScript namespace rewrite — not relevant here.
  3. undici stub — filename doesn't contain undici/, skip.
  4. normalizeIdentifierEscapesInSource (line 88) — normalise \u{…} escape sequences.
  5. BraceFrontendBase::parse runs:
    • detectUseStrictDirective → false.
    • preprocess(source) → vector of lines.
    • parseBlock(false, false) walks the lines, calling JSFrontend::tryParseLanguageSpecificStatement for each.
  6. import { readFile } from 'node:fs/promises'; matches parser.parseImportDeclaration() → emits a VariableDeclNode(name="readFile", isConst=true, decl_origin="import") with a FunctionCallNode(name="__import__", args=["node:fs/promises", "readFile"]) (see JSFrontend.h:651-706).
  7. export async function readFirst(path) { … } matches parser.parseFunctionDeclarationHeader() → emits FunctionDeclNode(name="readFirst", parameters=[("auto","path")]) with metadata async="true" and param_src set. The body is parsed recursively. Inside the body, const buf = await readFile(path); emits a VariableDeclNode with await keyword wrapped in an AwaitNode.
  8. Export wrapping: at the end of BraceFrontendBase::parse, the export_name metadata on FunctionDeclNode causes it to be replaced with an ExportNode(value=FunctionDeclNode, isDefault=false, bindings=[("readFirst","readFirst")]).
  9. Backend (backends/cpp/CppBackend.h) walks the module, emits #include <empl.h>, a __import__ function-pointer table for ESM, and an mpl_promise<…>-returning coroutine for async function.

7. Brace languages — minimal frontend skeleton

To add a 10th brace language, the typical class body is:

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}, {"print", true, false} };
    }
    bool isComment(const std::string& line) const override {
        return line.substr(0, 2) == "//";
    }
    std::shared_ptr<ir::VariableDeclNode> tryParseVarDecl(const std::string& line) override {
        // ...
    }
};

Then register it in registerPlugins() (see registration.md).


8. What the base class does NOT do

Concern Handled by
Lexer (token stream) Subclass (or rely on BraceFrontendBase's regex-driven lexer)
Operator precedence BraceFrontendBase::parseExpression (Pratt)
Class methods / inheritance BraceFrontendBase::parseClassDeclaration
Async / await / yield BraceFrontendBase handles await and yield as part of expression parsing
Decorators Subclass (JSFrontend::parseDecorator)
JSX / templates Subclass (JSFrontend line 913-930)
Match expressions (Rust) Subclass (RustFrontend::rewriteMatchExpressions)
Match expressions (PHP) Subclass (PHPFrontend::convertMatchExpressions)
Go channels / select / go / defer Subclass (GoFrontend::tryBuildChannelInitializer, parseSelectBlock, tryParseGoForRange)
Record classes (Java) Subclass (JavaFrontend::rewriteJavaRecordsToClasses)
Text blocks (Java) Subclass (JavaFrontend::rewriteJavaTextBlocks)
Field keyword (C#) Subclass (CSharpFrontend::tryParseClassFieldDecl)
Lambda keyword (EMPLS) Subclass (EMPLSFrontend::transformLambdaKeyword)
Range expressions (EMPLS) Subclass (EMPLSFrontend::transformRangeExpressions)
Pipeline ` >` (EMPLS)

The base class is deliberately extensible — every language-specific quirk lives in the language's *.h, and the C++ backend never queries language metadata: it reads only IR node kinds and metadata keys that are language-neutral.