Frontends
EMPLS Frontend
EMPLS Frontend
EMPLS (Extended Multi-Platform Language Source) is the built-in programming language that ships with MPL_Compiler. It's a small, brace-delimited language designed to demonstrate the language-neutral IR and to be a clean substrate for cross-language tooling.
| File | Lines | Purpose |
|---|---|---|
frontends/empls/EMPLSFrontend.h |
750 | Frontend class with all method bodies inline (single header). No StdLib table — uses shared brace-level print patterns. |
Language registration
// EMPLSFrontend.h:35-36
std::string languageName() const { return "empls"; }
std::vector<std::string> fileExtensions() const { return {".empls"}; }
Only .empls is recognised.
Distinctive EMPLS syntax
EMPLS adds three language extensions on top of the brace-language defaults, plus a few naming conventions that make it the "ground truth" of the language-neutral IR:
Typed bindings var::T / let::T / const::T
The headline EMPLS feature is explicit storage + type in a single binding token:
var::int x = 42;
let::string name = "Alice";
const::double PI = 3.14159;
var::List<int> primes = [2, 3, 5, 7];
parseTypedBinding (EMPLSFrontend.h:533-558) parses these:
left ::= ('var' | 'let' | 'const') '::' <Type> ' ' <Name>
EMPLSFrontend::tryParseVarDecl (line 560-613) recognises:
| Form | IR |
|---|---|
var::int x = 42; |
VariableDeclNode(name="x", isConst=false, type=UIRType(Int32), initializer=LiteralNode(42)) |
let::string name = "Alice"; |
VariableDeclNode(name="name", isConst=false, type=UIRType(String), initializer=LiteralNode("Alice")) |
const::double PI = 3.14159; |
VariableDeclNode(name="PI", isConst=true, type=UIRType(Float64), initializer=LiteralNode(3.14159)) |
var x = 42; |
VariableDeclNode(name="x", isConst=false, initializer=LiteralNode(42)) (untyped fallback) |
let x = 42; |
same as var (untyped fallback) |
const x = 42; |
VariableDeclNode(name="x", isConst=true, …) |
x = 42; (assignment-shaped) |
(only if isValidIdent(name); becomes VariableDeclNode with implicit var) |
Typed function declarations func::T name(args) { … }
func::int add(var::int a, var::int b) {
return a + b;
}
tryParseFunctionDeclHeader (line 615-676) recognises:
| Form | IR |
|---|---|
func::int name(args) { … } |
FunctionDeclNode(name="name", returnType="int", returnTypeInfo=UIRType(Int32), typedParameters=[…]) |
func name(args) { … } |
FunctionDeclNode(name="name", returnType="auto", …) |
def name(args) { … } |
(Rust/legacy) FunctionDeclNode(name="name", returnType="auto", …) |
Parameter types can be either typed (var::int a) or untyped (a). Both
forms populate fn->typedParameters[].
Type system
parseTypeName (line 507-531) maps type spellings to ir::UIRType:
| Spelling | TypeKind |
|---|---|
void |
Void |
bool, boolean |
Bool |
int, int32 |
Int32 |
int64, long |
Int64 |
uint, uint32 |
UInt32 |
uint64, ulong |
UInt64 |
float, float32 |
Float32 |
double, float64 |
Float64 |
string, str |
String |
char |
Char |
| anything else | UserDefined |
lambda keyword
EMPLS supports a lambda keyword in addition to JavaScript-style
arrow functions:
var add = lambda (a, b) { return a + b; };
var square = lambda (x) => x * x;
transformLambdaKeyword (line 97-173) rewrites lambda (params) { body }
to (params) => { body } so the brace parser sees a normal arrow
function. Comments and string literals are skipped during the rewrite.
Range expressions A..B / A..=B
EMPLS supports Rust-style inclusive/exclusive integer ranges:
for i in 0..5 {
println(i); // 0, 1, 2, 3, 4
}
for i in 1..=3 {
println(i); // 1, 2, 3
}
transformRangeExpressions (line 220-317) expands literal integer ranges
into array literals:
0..5 → [0,1,2,3,4]
1..=3 → [1,2,3]
buildRangeArrayLiteral (line 205-216) caps expansion at 1,024
elements. Non-integer operands and ranges that exceed the cap are left
untouched (the brace parser then treats them as BinaryOpNode-less
expressions, which currently is an UnsupportedNode).
Pipeline operator |>
EMPLS supports F#-style pipeline operators:
" hello " |> trim |> toUpperCase |> print;
[1, 2, 3, 4] |> filter(>0) |> map(double) |> sum |> println;
transformPipelineOperator (line 323-470) rewrites:
value |> name → name(value)
value |> name(args) → name(value, args)
value |> name(a, b) → name(value, a, b)
The rewrite is depth-aware (paren, bracket, brace) so unrelated | and
> characters are left alone. The RHS must start with an identifier —
otherwise the |> is preserved literally.
Classes and structs
class Point {
var::int x;
var::int y;
func::Point(var::int x, var::int y) {
this.x = x;
this.y = y;
}
}
struct Vec2 {
var::float x;
var::float y;
}
tryParseLanguageSpecificStatement (line 678-746) handles:
| Form | IR |
|---|---|
class Name { … } |
ClassDeclNode(name="Name") |
struct Name { … } |
ClassDeclNode(name="Name", isStruct=true) |
Imports and exports
import "std/math";
import "std/io" as io;
export { PI, sqrt };
export func::int add(...) { ... }
| Form | IR |
|---|---|
import "module"; |
ImportNode(module="module") |
export { a, b, c }; |
ExportNode(bindings=[("a","a"), ("b","b"), ("c","c")]) |
export func::… |
(handled by BraceFrontendBase's export-wrapping) |
Module-level wrapper
EMPLSFrontend::parse (line 472-490) supports a module Name { … }
wrapper:
module math {
func::int square(var::int x) => x * x;
print(square(5)); // 25
}
The wrapper is stripped (its { … } body becomes the top-level
source) and the inner body is passed through the same transform pipeline
(transformLambdaKeyword, transformRangeExpressions,
transformPipelineOperator) before being handed to
BraceFrontendBase::parse.
Print patterns
// EMPLSFrontend.h:493-495
return {{"print", true, false}, {"println", true, true}};
println(x) becomes Console.WriteLine(x). print(x) becomes
Console.Write(x).
Comments
EMPLSFrontend::isComment (line 497-499) accepts both # and //:
# This is an EMPLS comment.
// This is also a comment.
using directive lines (e.g. using std;) are isIgnoredLine (line 501)
— they're a frontend-only sugar that doesn't reach the IR.
Source-level rewrites (preprocessing pipeline)
EMPLSFrontend::parse (line 472) applies these rewrites in order:
transformLambdaKeyword(line 97) —lambda (a, b) { body }→(a, b) => { body }transformRangeExpressions(line 220) —A..B/A..=B→[a, …, b]transformPipelineOperator(line 323) —value |> name(args)→name(value, args)
All three are comment-aware and string-aware.
Standard library
EMPLS does not own a StdLib mapping table. The brace-level print
patterns in BraceFrontendBase route print/println to
Console.Write/Console.WriteLine. Other stdlib calls must be emitted
by the call site as StandardLibraryCallNode(moduleName, functionName)
directly. The canonical mapping is MPL_Standard/Specification.json.
import "std/math";
print(math::sqrt(16.0)); // → StdLibCall("Math", "Sqrt", 16.0) via runtime
IR examples
Example 1 — typed function with ranges
module math_demo {
func::int sum_to(var::int n) {
var::int total = 0;
for i in 1..=n {
total = total + i;
}
return total;
}
println(sum_to(10)); // 55
}
After source rewrites:
func::int sum_to(var::int n) {
var::int total = 0;
for i in [1,2,3,4,5,6,7,8,9,10] {
total = total + i;
}
return total;
}
println(sum_to(10));
Lowers to:
FunctionDeclNode(name="sum_to",
returnTypeInfo=UIRType(Int32),
typedParameters=[Parameter("n", Int32)])
body:
VariableDeclNode(name="total", type=Int32, initializer=0)
ForNode(
init=VariableDeclNode(name="i", initializer=0),
condition=BinaryOpNode("<", i, 10),
increment=AssignmentNode(i, i + 1),
body: AssignmentNode(total, total + i))
ReturnNode(value=IdentifierNode("total"))
Example 2 — pipeline with method chain
" hello " |> trim |> toUpperCase |> print;
After transformPipelineOperator:
trim(" hello ") |> toUpperCase |> print
toUpperCase(trim(" hello ")) |> print
print(toUpperCase(trim(" hello ")))
Lowers to three nested function calls ending in
StandardLibraryCallNode("Console", "Write", …).
Example 3 — class with typed fields
class Counter {
var::int n = 0;
func::Counter() {}
func::int next() {
n = n + 1;
return n;
}
}
var c = Counter();
println(c.next());
println(c.next());
println(c.next());
Lowers to:
ClassDeclNode(name="Counter")
members:
VariableDeclNode(name="n", type=Int32, initializer=0)
constructor:
FunctionDeclNode(name="Counter")
methods:
FunctionDeclNode(name="next",
returnTypeInfo=Int32)
body:
AssignmentNode(MemberAccessNode(this, "n"),
BinaryOpNode("+", MemberAccessNode(this, "n"), 1))
ReturnNode(value=MemberAccessNode(this, "n"))
What is not supported (EMPLS-specific gaps)
- Generic type parameters beyond simple
List<T>: parsed as plain identifiers, not lowered to typed generics. - Async / await: not yet modelled.
- Traits / interfaces:
interface Name { … }is recognised but the body isn't specialised. - Module file split (
module math { … }in one file, used byimport "math"in another): CLI module resolution is the same as JavaScript CJS — flat file paths under the source root. - Async lambdas:
lambda async (x) { … }not yet recognised.
Why a built-in language?
EMPLS exists to:
- Demonstrate language neutrality: a language that uses only the
shared
MPL_Standardruntime proves that any frontend output can target any backend. - Test the IR: when adding a new IR node, you can write a small EMPLS file and compile it directly.
- Provide a clean reference implementation: each language frontend
ships with
MPL_Compileritself as the executable; EMPLS ships with the source. Users can read EMPLS to understand the IR without needing to install Node, Python, etc.
EMPLS is intentionally not a complete language — it's a teaching and testing substrate.