Control Flow¶
All block statements use brace bodies { ... } with no end-keywords. Under the
@-default a body closes at @}; under pragma bare it closes at a lone }
(Templates). The examples below use the @-default spelling.
Scoping is lexical and block-structured; the precise undefined-handling rules are
in Types.
Conditionals¶
One if head, zero or more elseif, an optional else. Conditions are
arbitrary expressions taken in the single truthiness rule (Types).
Loops¶
- One or two targets:
for v in seqandfor k, v in mapping(mappings iterate in insertion order with both targets). - The
elsebranch runs exactly when the sequence yielded zero iterations, and only after the iterand resolved to a collection. It is reached for an iterable-but-empty value; it is not reached when the iterand is non-iterable, because the error preempts the loop. - A non-iterable iterand is a runtime error, not a silent empty loop. A
silent skip would omit a whole section of output with no signal. The explicit
"empty is fine" idiom is
for x in (coll ?? []) { ... }. Where a static type proves non-iterability, the error is promoted to check time.
The three cases:
Iterand E |
Behavior |
|---|---|
| iterable, one or more elements | body runs per element; else not run |
iterable, zero elements ([], {}) |
body skipped; else run |
Null or any non-iterable, written bare (for x in E) |
runtime error; else not reached |
Null/absent, coalesced (for x in (E ?? [])) |
the ?? yields [] -> else run |
Loop metadata¶
Inside the body, loop exposes loop.parent, loop.index0, loop.index,
loop.first, loop.last, loop.length, loop.revindex, loop.revindex0,
loop.prev, loop.next, and the method loop.changed(expr). All loop fields
are always defined: the collection knows its length, and a host iterator is
drained before the loop, trading a small memory cost for an always-present
loop.last.
loop.prev/loop.nextare the previous and next element value;loop.previsNullon the first iteration andloop.nextisNullon the last.loop.changed(expr)istrueon the first iteration and wheneverexprdiffers from its value on the prior iteration; each call site is tracked independently. It is the idiom for section headers over grouped rows:@if loop.changed(row.group) { [{{ row.group }}] @}.
Fused loop filtering¶
An optional if <expr> clause between the iterand and the body brace pre-filters
the iterand to the elements for which the condition is truthy, and the body runs
only over those survivors:
Every loop.* field reflects only the survivors, so a trailing-separator idiom
keyed on loop.last is correct over the filtered subset. The else branch runs
when zero elements survive.
Recursive descent¶
A recursive marker after the iterand turns the loop into a tree walk: the body
may call loop(children) to render the same body over a subtree one level
deeper, and the descent's rendered output is returned as a value the body prints.
Two extra fields appear: loop.depth (1-based) and loop.depth0 (0-based). It
is the idiom for nested structures such as a directory tree, an AST, or a menu:
loop(children) iterates its argument as a fresh level. An argument that is not a
traversable collection renders nothing. A recursive loop body reads outer
variables but does not persist a body set of an outer name after the loop;
accumulate across the walk with a slot (@provide) or a cell.
Scoping¶
The plain loop body is a child scope. A variable that existed before the loop and is reassigned inside keeps its last in-loop value after the loop; a variable introduced only in the body does not leak. The rule is lexical scoping.
Assignment and capture¶
@set binds one or more targets; the same count on both sides or a clear error.
Assignment is an expression returning the assigned value: {{ b = 1 + 3 }} both
stores b and prints 4; @do b = 1 + 3 stores without printing.
A target may be a member place rather than a plain name: @set recv.name = expr
and @set recv[key] = expr assign through a receiver. On a mapping this stores
the key in place; on a reference value (a cell, Standard Library)
it calls the write hook, so @set acc.value = acc.value + w mutates the cell in
place. A receiver that does not support assignment is a runtime error.
Block capture renders a body to a string-like value:
Under the default (escaping off) the capture is a plain Str; under an
escape-on template it is a Safe value.
Effect, logging, and flush¶
@do expr: evaluate for side effects, no output.@log expr: evaluate and write the text form to the host logger (WithLogger, default discarding). No rendered output, but it is a coverable unit.@flush: a documented no-op for a string/byte sink, kept for parity.@deprecated "message" [since "2.0"]: routes a deprecation diagnostic to the diagnostics sink, no output.
Scoped regions¶
@with { x: 1, y: 2 } { ... }introduces a scope merging the given vars;@with { x: 1 } only { ... }replaces the context entirely for the body.@apply | trim | upper { ...body... }captures the body and pipes it through the filter chain.@escape html { ... }/@escape off { ... }sets the active escaping strategy for a region (Escaping & Safety).@sandbox { ... }forces sandboxing for templates included within the region.@tab(n) { ... }indents the entire rendered body bynlevels, nesting cumulatively; blank lines stay blank (Standard Library).@guard filter("markdown") { ... } else { ... }selects a branch on whether a named callable is registered; the dead branch is parsed but not validated against unknown callables, so it is portable across host configs.@types { x: string, n: int }declares context types, consumed by the gradual checker (Types).@line 42resets the reported source line for diagnostics in embedded or generated fragments.@cache key="header" ttl=3600 tags=["a"] { ... }caches a rendered body under a key with optional ttl/tags.
Next¶
- Composition: inheritance, blocks, macros, includes, embeds, and slots.
- Whitespace Control: the trim modifiers and the block cleanup that keep control statements from leaking blank lines.