Elvelt

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.


// 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:

  1. transformLambdaKeyword (line 97) — lambda (a, b) { body }(a, b) => { body }
  2. transformRangeExpressions (line 220) — A..B / A..=B[a, …, b]
  3. 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 by import "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:

  1. Demonstrate language neutrality: a language that uses only the shared MPL_Standard runtime proves that any frontend output can target any backend.
  2. Test the IR: when adding a new IR node, you can write a small EMPLS file and compile it directly.
  3. Provide a clean reference implementation: each language frontend ships with MPL_Compiler itself 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.