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:
- Strips UTF-8 BOM (first line only).
- Skips shebangs (
#!...). - Removes
//and/* */comments while respecting string literals. - Returns
truewhen 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:
- CLI calls
JSFrontend::parse(source, "example.js")(JSFrontend.h:42-94). - TypeScript namespace rewrite — not relevant here.
undicistub — filename doesn't containundici/, skip.normalizeIdentifierEscapesInSource(line 88) — normalise\u{…}escape sequences.BraceFrontendBase::parseruns:detectUseStrictDirective→ false.preprocess(source)→ vector of lines.parseBlock(false, false)walks the lines, callingJSFrontend::tryParseLanguageSpecificStatementfor each.
import { readFile } from 'node:fs/promises';matchesparser.parseImportDeclaration()→ emits aVariableDeclNode(name="readFile", isConst=true, decl_origin="import")with aFunctionCallNode(name="__import__", args=["node:fs/promises", "readFile"])(seeJSFrontend.h:651-706).export async function readFirst(path) { … }matchesparser.parseFunctionDeclarationHeader()→ emitsFunctionDeclNode(name="readFirst", parameters=[("auto","path")])with metadataasync="true"andparam_srcset. The body is parsed recursively. Inside the body,const buf = await readFile(path);emits aVariableDeclNodewithawaitkeyword wrapped in anAwaitNode.- Export wrapping: at the end of
BraceFrontendBase::parse, theexport_namemetadata onFunctionDeclNodecauses it to be replaced with anExportNode(value=FunctionDeclNode, isDefault=false, bindings=[("readFirst","readFirst")]). - Backend (
backends/cpp/CppBackend.h) walks the module, emits#include <empl.h>, a__import__function-pointer table for ESM, and anmpl_promise<…>-returning coroutine forasync 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.