if handles imperative branches. when selects the first matching condition. for, foreach, and loop handle iteration. while provides bounded pipeline loops.
If Blocks
if runs imperative branches. Use block form with optional else.
if @isProd [
show "Production mode"
] else [
show "Local mode"
]
if works at top level and inside exe blocks:
exe @validate(input) = [
if !@input [
=> { error: "missing" }
]
=> { ok: @input }
]
Use => inside exe blocks and at script top level.
when
When block (first match wins):
when [
@score > 90 => show "Excellent!"
@hasBonus => show "Bonus earned!"
none => show "No matches" >> runs only if nothing matched
]
Use multiple if blocks when you need several actions to run.
Inline form uses when @cond => action.
When Match Form
Two forms: value matching (when @expr [patterns]) and condition matching (when [conditions]).
Value matching — match a value against literal patterns:
var @status = "active"
when @status [
"active" => show "Running"
"pending" => show "Waiting"
* => show "Unknown"
]
The colon form when @expr: [patterns] also works but the colon is optional.
Condition matching — evaluate boolean expressions (no value after when):
when [
@score > 90 => show "A"
@score > 80 => show "B"
* => show "F"
]
Patterns evaluate in order. First match wins.
Simple inline (single condition, single result):
when @cond => show "Match"
Returns the result when the condition is truthy. Use block form when [@cond => action; * => default] when you need multiple branches or a fallback.
Value-Returning When
Value-returning when (in exe):
exe @classify(score) = when [
@score >= 90 => "A"
@score >= 80 => "B"
* => "F"
]
var @grade = @classify(85) >> "B"
when returns the first matching branch. Use if for imperative flow.
Inside exe block statements, when values also return from the enclosing exe on match:
exe @guard(x) = [
when !@x => [
=> "missing"
]
=> "ok"
]
Block form keeps return intent explicit. Bare value actions such as when !@x => "missing" are valid and return early.
Block Actions in When
Block actions (side effects + return):
var @result = when [
@needsProcessing => [
show "Processing..."
let @processed = @transform(@data)
=> @processed
]
* => @data
]
Conditions evaluate in order and the first match runs.
Inside exe blocks, a matched when action that evaluates to a value returns from the enclosing exe.
Use block-form return for explicit intent:
exe @guard(x) = [
when !@x => [
=> "missing"
]
=> "ok"
]
when !@x => "missing" also returns from the exe.
Local Variables in When
Local variables in when:
when @mode [
let @prefix = "Status:"
"active" => show "@prefix Active"
"pending" => show "@prefix Pending"
* => show "@prefix Unknown"
]
Augmented assignment:
exe @collect() = when [
let @items = []
@items += "a"
@items += "b"
* => @items >> ["a", "b"]
]
+= works with arrays (concat), strings (append), objects (merge).
Operators in When Conditions
Operators in conditions:
- Comparison:
<,>,<=,>=,==,!= - Logical:
&&,||,! - Parentheses:
(@a || @b) && @c
when [
@role == "admin" || @role == "mod" => show "Privileged"
@active && @verified => show "Active user"
!@banned => show "Allowed"
* => show "Blocked"
]
for
Arrow form:
for @item in @items => show `Processing @item`
for @n in [1,2,3] => log @n
For Collection Form
Collection form (returns results):
var @doubled = for @x in [1,2,3] => @x * 2 >> [2, 4, 6]
var @names = for @user in @users => @user.name
For Block Form
Block form:
for @item in @items [
let @processed = @transform(@item)
show `Done: @processed`
]
>> Collection with block
var @results = for @item in @items [
let @step1 = @validate(@item)
let @step2 = @transform(@step1)
=> @step2
]
For with Inline Filter
For with inline filter:
var @valid = for @x in @items when @x != null => @x
var @admins = for @u in @users when @u.role == "admin" => @u.name
Skip Keyword
Skip keyword (drop items from results):
var @filtered = for @x in @items => when [
@x.valid => @x
none => skip >> omit this item from results
]
>> Equivalent to inline filter, but allows complex logic
var @processed = for @item in @data => when [
@item.type == "a" => @transformA(@item)
@item.type == "b" => @transformB(@item)
* => skip >> unknown types dropped
]
Object Iteration
Object iteration:
var @cfg = {"host": "localhost", "port": 3000}
for @k, @v in @cfg => show `@k: @v`
>> Output: host: localhost, port: 3000
Value-only form:
for @v in @cfg => show `@v.mx.key: @v`
When you bind only the value, the key is available at @v.mx.key and @v_key. The key/value form does not bind @v_key.
Nested For Loops
Nested for:
for @x in ["A","B"] => for @y in [1,2] => show `@x-@y`
>> Output: A-1, A-2, B-1, B-2
Batch Pipelines
Batch pipelines (process collected results):
var @total = for @n in [1,2,3,4] => @n => | @sum
var @sorted = for @item in @items => @process(@item) => | @sortBy("priority")
Parallel For
Run iterations concurrently with for parallel.
>> Default concurrency (MLLD_PARALLEL_LIMIT, default 4)
for parallel @x in @items => show @x
>> Custom concurrency cap
for parallel(3) @task in @tasks => @runTask(@task)
>> With pacing (delay between starts)
for parallel(2, 1s) @x in @items => @process(@x)
>> Variable cap and pacing
var @cap = 2
var @pace = "1s"
for parallel(@cap, @pace) @x in @items => @process(@x)
Parallel blocks:
for parallel(3) @task in @tasks [
let @result = @runTask(@task)
show `Done: @task.id`
]
Error handling:
- Errors accumulate in
@mx.errors - Failed iterations add error markers to results
- Outer-scope writes blocked (use block-scoped
letonly)
exe @process(tasks) = [
let @results = for parallel @t in @tasks => @run(@t)
=> when [
@mx.errors.length == 0 => @results
* => @repair(@results, @mx.errors)
]
]
For Loop Context
Loop context via @mx.for:
var @items = [10, 20, 30]
for @n in @items => show `Index: @mx.for.index, Value: @n`
>> Output:
>> Index: 0, Value: 10
>> Index: 1, Value: 20
>> Index: 2, Value: 30
Available @mx.for fields:
@mx.for.index- Current 0-based iteration index@mx.for.total- Total number of items in collection@mx.for.key- Key for objects, stringified index for arrays@mx.for.parallel- Boolean indicating parallel execution
Array binding metadata @item.mx.index:
var @items = [10, 20, 30]
for @item in @items => show `Item @item.mx.index: @item`
>> Output:
>> Item 0: 10
>> Item 1: 20
>> Item 2: 30
For arrays, the loop-bound variable exposes @item.mx.index with the zero-based array position. This works in expressions, when filters, templates, directive bodies, and nested loops.
Object iteration:
var @cfg = {"host": "localhost", "port": 3000}
for @v in @cfg => show `Key: @v.mx.key, Value: @v`
>> Output:
>> Key: host, Value: localhost
>> Key: port, Value: 3000
Object iteration binds @v.mx.key but does not set @v.mx.index.
In parallel loops:
var @tasks = ["a", "b", "c"]
for parallel(2) @item in @tasks => show `Slot @mx.for.index, item @item.mx.index: @item`
@mx.for.parallel returns true in parallel loops, false in sequential loops.
Parallel iterations complete out of order, so output lines are emitted in completion order. In that mode, treat @mx.for.index as execution-context metadata, not a stable ordering key for reconstructing source position. Use @item.mx.index when you need the original dispatch position in both sequential and parallel loops.
Loop Blocks
Block-based iteration with explicit done and continue.
var @result = loop(10) [
let @count = (@input ?? 0) + 1
when @count >= 3 => done @count
continue @count
]
Control keywords:
done @value- Exit loop and return valuedone- Exit loop and return nullcontinue @value- Next iteration with new@inputcontinue- Next iteration with unchanged@input
@input starts as null and updates only via continue @value.
Loop context (@mx.loop):
iteration- Current iteration (1-based)limit- Configured cap or null for endlessactive- true when inside loop
Until clause:
loop until @input >= 3 [
let @next = (@input ?? 0) + 1
show `@next`
continue @next
]
With pacing:
loop(endless, 10ms) until @input >= 3 [
let @next = (@input ?? 0) + 1
show "poll"
continue @next
]
Bail Directive
Terminates the entire script immediately with exit code 1. Works from any context including nested blocks and imported modules.
>> Explicit message
bail "config file missing"
>> With variable interpolation
var @missing = "database"
bail `Missing: @missing`
>> Bare bail (uses default message)
bail
Exit from nested contexts:
>> From if blocks
if @checkFailed [
bail "validation failed"
]
>> From when expressions
when [
!@ready => bail "not ready"
* => @process()
]
>> From for loops
for @item in @items [
if !@item.valid [
bail `Invalid item: @item.id`
]
]
Terminates imported modules:
When an imported module calls bail, the entire script terminates, not just the module:
>> module.mld
if !@configured [
bail "module not configured"
]
export { data: "ok" }
>> main.mld
import { data } from "./module.mld" >> terminates here
show @data >> never reached
Markdown mode:
/bail "markdown mode termination"
Exit behavior:
- Throws
MlldBailErrorwith codeBAIL_EXIT - Exit code: 1
- Default message: "Script terminated by bail directive."
- Message accepts strings, variables, and expressions
While Loops
Bounded iteration with while.
exe @countdown(n) = when [
@n <= 0 => done "finished"
* => continue (@n - 1)
]
var @result = 5 | while(10) @countdown
Control keywords:
done @value- Terminate, return valuedone- Terminate, return current statecontinue @value- Next iteration with new statecontinue- Next iteration with current state
While context (@mx.while):
iteration- Current iteration (1-based)limit- Configured capactive- true when inside while
With pacing:
var @result = @initial | while(100, 1s) @processor >> 1s between iterations
Foreach
foreach applies a function to each element, returning transformed array.
var @names = ["alice", "bob", "charlie"]
exe @greet(name) = `Hi @name!`
var @greetings = foreach @greet(@names)
>> ["Hi alice!", "Hi bob!", "Hi charlie!"]
In exe:
exe @wrapAll(items) = foreach @wrap(@items)
show @wrapAll(["a", "b"]) >> ["[a]", "[b]"]
With options:
show foreach @greet(@names) with { separator: " | " }
>> "Hi alice! | Hi bob! | Hi charlie!"