MELP Language Reference
Canonical source: ORTAK/dil/ and
MELP_KANONΔ°K/05_DIL_OZET.md |
Status: alpha | Updated: August 2026
This page documents what the compiler does today, not what is planned. Where a construct is experimental it is marked π¬. If you find something here that the compiler does not accept, that is a bug in this page β please report it.
Build Path
A .mlp file is compiled to LLVM IR and then to a
native binary. There is no interpreter and no runtime to install alongside
the result.
# Run
bin/run_melp program.mlp
# Inspect the LLVM IR
bin/run_melp program.mlp --ir
# Produce a native binary
bin/melp_compiler program.mlp -o program
Ground Rules
| Rule | Correct β | Wrong β |
|---|---|---|
| Argument separator | foo(a; b; c) | foo(a, b, c) |
| Decimal mark | 3,14 | 3.14 |
| Comment | -- comment | // comment |
| Block terminator | end function (with a space) | end_function, } |
| Return type comes first | numeric function foo() | function foo() -> numeric |
| One statement per line | x = 42 | x = 42; |
| Logical operators | and, or, not | &&, ||, ! |
| Modulo | x mod y | x % y |
| String concatenation | a & b | a + b |
| Collections | list() | [1; 2; 3] β closed to users |
The decimal mark is a comma, which is why the argument separator is a semicolon. This is a language decision, not a locale setting.
Types
MELP has three variable types. There is no null, no void and no none.
| Type | Representation | Notes |
|---|---|---|
numeric | i64 | Default: raw i64, up to 18 digits. Exceeding that without the import is compile error E054. |
string | i64 handle | Inline (β€7 bytes) or heap. Concatenation with &. |
boolean | i1 / i64 | true = 1, false = 0. |
numeric x = 42
string name = "MELP"
boolean ready = true
-- The second numeric world: opt in per file
import bigdecimal
numeric pi = 3,14159265358979323846
numeric amount = 19,99
Two worlds. By default numeric is
a raw i64 β one CPU instruction, zero overhead. A file that writes
import bigdecimal gains automatic big-number
promotion driven by the hardware overflow flag, plus decimals stored as
mantissa + scale, so 0,1 + 0,2 is exactly
0,3. Writing a decimal without the
import is caught by error E055 rather than silently misread.
π¬ The bigdecimal world is not yet
corpus-proven and is not recommended for production. In the default world,
i64 overflow wraps silently β a deliberate cost.
Variables
The type name is the declaration β there is no
dim, var or
let ceremony, and typing is fully static.
numeric counter = 10
string lang = "MELP"
boolean active = true
-- Reassignment β no type keyword
counter = counter + 1
lang = lang & " language"
const was removed from the keyword list; the
compiler may still recognise it, but it is not canonical MELP.
Operators
Arithmetic
x + y
x - y
x * y
x / y
x mod y -- modulo (not %)
Comparison
x == y
x != y
x < y
x > y
x <= y
x >= y
Logical
x and y
x or y
not x
String Concatenation
string full = "Hello, " & name & "!"
Concatenation is O(nΒ²) today; for hundreds of thousands of joins, build the string incrementally rather than in a tight concatenation loop.
Conditions
if score >= 90 then
println("A")
else if score >= 80 then
println("B")
else
println("C")
end if
then is optional.
else if is written on one line. There is no
ternary operator β it was considered and rejected.
Loops
There is one loop construct. No while,
no for, and no continue β
continue was removed from the language.
Conditional loop
numeric i = 0
loop i < 10
println(i)
i = i + 1
end loop
Infinite loop with an exit
loop
if done then exit end if
process()
end loop
Iterating a collection
loop each n in numbers
println(n)
end loop
Functions
-- Return type comes first
numeric function add(numeric a; numeric b)
return a + b
end function
-- The entry point
numeric function main()
numeric result = add(3; 5)
println(result)
return 0
end function
OK is a value, not a type. There
is no void: a function that "returns nothing"
returns OK, carried as an i64 in the ABI.
The one exception is the entry point:
numeric function main() with an explicit
return 0 is what the compiler accepts, and it is
what the compiler's own source uses.
return returns a value from a function; in
main it sets the process exit code.
exit is different β it leaves a
scope (a loop, an if, a
match, a named scope), not a function.
Scopes
Everything that occupies memory is a scope. A variable's lifetime is the lifetime of its scope; when the scope closes, everything inside it is cleaned up. A child cannot write directly to a parent's variable β the scope boundary is the isolation boundary.
scope search
loop each i in rows
loop each j in columns
if found(i; j) then
exit search -- clean exit from nested loops
end if
end loop
end loop
end scope
A scope is already alive and runs by being called β
counter() creates a new instance on every call.
Sharing across a scope boundary happens through three controlled gates:
tunnel | peek | channel | |
|---|---|---|---|
| What it carries | Ownership (temporary custody) | A snapshot copy | A copy of a message |
| Access | AβB, private | AβB, one-way, read-only | Many-to-many, broadcast |
| The source | Suspended | Active | Independent |
π¬ tunnel and peek are
parsed, validated and code-generated but currently desugar to a plain
assignment; channel is a flat global rather than a
FIFO queue yet. The isolation rules they enforce are real today; the richer
runtime semantics are not.
Struct, Enum, Match
Data is defined with struct, behaviour with
functions. No classes, no inheritance, no virtual methods. A struct is a
dead template; new revives it on the heap,
and it dies when the calling scope closes.
struct Point
numeric x
numeric y
end struct
Point p = Point(10; 20)
println(p.x)
-- On the heap; freed when the calling scope closes
Point q = new struct Point(30; 40)
enum Colour
Red
Green
Blue
end enum
Colour c = Colour.Blue
match c
case Colour.Red then println("red")
case Colour.Green then println("green")
case Colour.Blue then println("blue")
end match
Enum variants are written bare β no case keyword in
the declaration. case appears only inside
match, qualified by the enum name. π¬ Payload-carrying
variants are described in the language spec but are not yet what the compiler
accepts; the form above is.
Lists
list() is the collection type. The
[...] array literal syntax is closed to users.
-- .add() returns the list, so it is reassigned
names = names.add("Ali")
names = names.add("Can")
loop each n in names
println(n)
end loop
π¬ The user-facing collection API is still settling. The form above is the one
the compiler's own source uses; treat the surface as unstable and check
MELP_LANGUAGE_SPEC.md before relying on it.
An algorithmic stack or queue is built by hand on a list today.
Error Handling
An error is resolved inside the scope where it was born and does not spill
outward. The construct in use today is the
expect block.
expect
numeric result = risky_operation()
println(result)
end expect
Nothing is left to runtime guesswork. Where another language might pick a plausible interpretation and continue, MELP stops with an explicit error code. A missing implementation must fail loudly rather than exit 0 β silent wrongness is treated as the more dangerous failure.
π¬ The Result<T; E> type and the
? propagation operator are experimental.
try/catch/finally
and throw are not part of the
language.
debug Blocks
A debug block is a letter to the future: a running,
conditional note that stops the program when the world outgrows the assumption
it records.
debug
if member_count > 10000 then stop
-- Past ten thousand members this design may need
-- a new database architecture. β 2026, the first team
end debug
Compiled with MELP_RELEASE=1, debug blocks are
stripped entirely β zero cost in the production binary. In a debug build you
can break on every block from gdb or
lldb.
Imports & FFI
import "accounts.mlp" -- auto-declare
import bigdecimal -- the second numeric world
-- C ABI foreign function
external function write(numeric fd; string buf; numeric len)
Circular imports are detected at compile time and reported as errors.
Honesty line: the moment you write
external, the memory-safety guarantee ends β
this is MELP's equivalent of Rust's unsafe.
MELP also has no answer to supply-chain security: the
import mechanism is still crawling and there is no chain of trust.
Deliberate Absences
These are not features awaiting implementation. They were considered and rejected, and the reasons matter more than the list.
| Not in MELP | Why |
|---|---|
async, await, spawn, threads, coroutines, futures, callbacks | Imported patterns. MELP's route is scope + channel + event loop. |
null | The billion-dollar mistake. There is no null value and no null type. |
void | OK is a real value; there is nothing to name "no value". |
mutex, atomic | Mutation happens at one point in one scope β there is nothing to guard. |
Stack, Queue types | LIFO is the life order of scopes; FIFO is the delivery order of a channel. |
continue | Removed from the language. |
| Classes, inheritance, virtual methods | struct + enum + match is enough. |
| Ternary operator, tuples | Considered and rejected. |
Garbage collector, borrow checker, &/lifetime annotations | The scope boundary already determines lifetime. |
Tutorial
A step-by-step guide to trying the language from scratch: π MELP in 30 Minutes β installation, first program, variables, file operations, structs and a small web server example.