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 let only)
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 value
  • done - Exit loop and return null
  • continue @value - Next iteration with new @input
  • continue - 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 endless
  • active - 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 MlldBailError with code BAIL_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 value
  • done - Terminate, return current state
  • continue @value - Next iteration with new state
  • continue - Next iteration with current state

While context (@mx.while):

  • iteration - Current iteration (1-based)
  • limit - Configured cap
  • active - 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!"