Language Reference¶
This is the core reference manual for Quill. It covers the lexical structure and the rule that makes a brace-delimited language safe for emitting brace-dense text; interpolation and output; the expression language, operators, and precedence; control flow and scoping; and template composition (inheritance, blocks, macros, includes). The formal grammar is in the Grammar; the standard library is in Standard Library; the runtime value rules, the gradual type system, escaping, and the sandbox are in Types and Escaping & Safety.
This page is the exhaustive specification. For a gentler introduction, start with the Guide.
1. Lexical structure and the brace-safety rule¶
1.1 Two modes, one boundary rule¶
A Quill source is a sequence of TEXT (emitted verbatim, byte for byte) and CODE (parsed). The lexer is a byte-oriented two-mode state machine. A template starts in TEXT. Under the default statement-lead mode there are exactly three doors out of TEXT into CODE, plus one bulk passthrough region:
| Opener | Closer | Meaning | Mode |
|---|---|---|---|
{{ |
}} |
interpolation: render one expression | CODE (expression) |
{# |
#} |
comment: consumed, emits nothing | CODE (comment) |
an @-led statement keyword |
matching @} or end of line |
a statement | CODE (statement) |
@verbatim { ... } / fenced @verbatim |
balancing @} / fence |
literal region, never scanned | TEXT-literal |
Everything else in TEXT (every bare {, }, <, >, &, ;, (, )) is
ordinary output text. The load-bearing invariant:
A single
{or}in template text is NEVER a delimiter, in either statement-lead mode. A{is a delimiter only when immediately followed by{or#, with no intervening character. Under the default, a statement begins ONLY at@, and a block closes ONLY at@}, so a bare}in text (even a lone}at column 0) is always literal output.
A lone { followed by a space, a letter, a newline, a digit, or a quote is emitted
as text and the scanner stays in TEXT. A block of code such as
{ this.head = node; } passes through TEXT untouched because none of {, ,
t, ;, } opens CODE. The byte-level lexer is byte-oriented so it can copy TEXT
faithfully including bytes that are not valid UTF-8; rune decoding happens only
inside CODE.
The default @-sigil statement lead exists because a common workload is
brace-dense text: literal lone-} lines are pervasive, many at column 0 (top-level
closes of emitted classes, methods, functions, or nested config blocks). The
@-default makes every one of those sites correct with zero escaping, at the cost
of one @ per statement. The keyword-led BARE mode (statements recognized by a
line-leading keyword, a lone } line closing the innermost block) is an explicit
opt-in via a front-matter pragma bare (equivalently pragma sigil off), for
markup and other templates where brace collisions are rare. Section 1.3 specifies
both modes; unless noted, the rest of this manual writes statements in the
@-default spelling.
1.2 The literal-{{ collision¶
The only sequence that flips TEXT into CODE by sigil is a literal {{ adjacent
pair. Most text never contains {{: no mainstream programming language uses {{
as a token. The single real adversary is a Java new HashMap<>() {{ ... }}
double-brace block, handled three ways, in order of preference:
- Wrap brace-dense bulk output in a
verbatimregion (Section 1.6). The body is copied byte for byte and never scanned. This is the recommended tool whenever a template emits a large literal block. - Interpolate a string literal for a one-off:
{{ "{{" }}emits two literal open braces. A trailing}}in TEXT is literal, because the close sigil}}is recognized only when it closes an OPEN{{. - Spot-escape with a backslash: in TEXT,
\{emits a literal{and suppresses sigil detection, so\{{emits{{.\}is symmetric. ONLY\{,\}, and\\are escapes; a lone backslash in TEXT is literal, so backslash sequences in emitted text are untouched unless they precede a brace.
The asymmetry is deliberate: emitting a literal { costs nothing in the
dense-single-brace case, and costs one explicit construct only in the rare
{{-adjacency case. A language that disambiguated {{ by context would need
unbounded lookahead and would still surprise the author; Quill refuses that trade.
1.3 Statements are sigil-led brace blocks, and the two modes¶
Quill has two statement-lead modes. The DEFAULT is the @-sigil mode, chosen
because a common workload is brace-dense text; the BARE keyword-led mode is an
explicit opt-in.
Default mode: @-led statements, @} close. A statement (@for, @if,
@block, @macro, @set, @include, ...) is recognized only when, after
optional leading horizontal whitespace, a line's FIRST non-whitespace character is
@ immediately followed by one of the fixed, closed set of statement keywords
(Section 5.1) at a word boundary, and the construct parses as a complete statement
head. A brace block opened by an @-led statement closes ONLY at @}: a line
whose only non-whitespace content (after optional leading whitespace) is @},
optionally carrying a -/~/+ trim modifier. Consequently:
- A bare
{or}ANYWHERE in TEXT (including a lone}at column 0) is unconditionally literal output. No literal lone-}site needs any escaping. - A line that begins with the WORD
for,if,while,block(no@) is ordinary TEXT; there is no grammar-shape rejection to perform and noline-leading-keyworddiagnostic to emit, because a statement is announced only by@.
Here the only escaping consideration is the rare literal {{ adjacency (Section
1.2); everything else in TEXT, every { and }, is literal by construction.
Opt-in bare mode: pragma bare. A front-matter pragma bare (equivalently
pragma sigil off) selects the keyword-led mode: a statement is recognized when a
line's FIRST non-whitespace token is one of the statement keywords at a word
boundary and the construct parses as a complete statement head, and a block closes
at a lone } line. This suits markup and other templates where literal braces are
rare. In bare mode an output line might legitimately begin with the word for
(C's for (...)) or if, and a literal lone } might collide with a block close;
Quill resolves both deterministically (never by heuristic) with grammar-shape
rejection plus the escape tools below:
-
Grammar-shape rejection. Quill's
forrequiresfor <ident> in <expr> {. An output C linefor (int i = 0; i < n; i++) {is NOT a Quillfor: the parser sees(where it requires an identifier, the line does not form a complete statement head, and it is TEXT. This handles the common C/Java/Gofor (...),if (...),while (...)shapes for free. -
The leading-pipe text marker (per-line total escape). A line whose first non-whitespace character is a literal pipe
|followed by a space is emitted as text verbatim, with the|prefix stripped, and is NEVER classified as a statement. Interpolation within the line still works:
- The verbatim region (bulk escape). For any block of output that begins lines
with Quill keywords,
verbatim { ... }(Section 1.6) copies the body literally with no scanning.
In bare mode the lexer emits a suppressible line-leading-keyword diagnostic at
every line that both begins with a Quill keyword AND forms a Quill-shaped head, so
the collision is never silent. The core lexical invariant (a single { or } is
never a delimiter) is unchanged in either mode.
The block-close rule and the line-leading-} collision (bare mode only)¶
This collision exists ONLY in opt-in bare mode; under the @-default a lone } is
always literal TEXT and there is nothing to resolve. A brace-bodied statement has a
TEXT body in which literal { and } are ordinary bytes and are NOT brace-counted
(bracket balancing applies only inside CODE, Section 1.7). In bare mode the closer
of such a body is recognized by exactly this rule:
A line whose ONLY non-whitespace content is a single
}(optionally carrying a-/~trim modifier) closes the innermost open Quill block. This is the ONLY close form; a}with any other non-whitespace on its line is literal TEXT.
So text that closes its own braces ON THE SAME LINE as other content (an indented
close, an interpolation-led close {{ 1 | tab }}}, or } else { in emitted C)
is never a Quill close, because the line is not a lone }. The residual collision
is a TEXT body that emits a brace-balanced block whose own close lands as a bare
} at COLUMN 0. There the literal close and the intended Quill close are
byte-identical lone-} lines, and the rule above would bind the FIRST such line to
the open block, truncating the body. The @-default exists precisely so this case
never arises; under bare mode the author disambiguates explicitly:
- The bare line-leading
}always closes the innermost open Quill block. The parser maintains the open-block stack; a lone}line pops one level. - An author emitting a literal lone
}at column 0 inside a Quill block MUST disambiguate it, by one of: the leading-pipe text marker (| }), wrapping the brace-dense region inverbatim, or emitting the brace through interpolation ({{ "}" }}). Switching to the@-default removes the requirement entirely. - Indentation does not by itself escape a literal
}: a line that is whitespace then}is still a lone-}close. Only the leading-pipe marker,verbatim, or interpolation make a column-aligned or column-0 literal}non-closing. - A hard error, not the suppressible diagnostic, fires on a structural
mismatch. If the open-block stack is non-empty at end of file OR a lone
}appears with an empty stack, the parser raises an unrecoverableunbalanced-blockerror naming the open construct and its source line.
Under the @-default these escape tools are unnecessary for the lone-} case and
remain available only for genuine edge cases.
1.4 Whitespace control¶
Whitespace control is byte-exact. It is documented in full in Whitespace Control; this section states the rule the rest of the manual relies on. Two trim modifiers attach to either side of any sigil or statement brace:
-(hard trim): strips ALL adjacent whitespace including newlines.{{- expr -}}; on a statement,@for ... {-...-@}.~(line trim): strips adjacent spaces, tabs, NUL, and vertical tab but NOT newlines.{{~ expr ~}}.+(keep, closing side only): suppresses the default close-newline-eating of a statement's closing@}(lone}under bare mode) or a comment's#}.@}+closes a block WITHOUT consuming the following newline.
The statement/interpolation newline asymmetry is a fixed rule: a statement's
closing @} and a comment's #} each consume exactly ONE immediately-following
newline; an interpolation's }} consumes none. This is what lets
emit one line per user with no spurious blank lines. Where the eaten newline would
fuse the last body line with the following line, @}+ or a per-template
pragma keep-close-newline restores byte-exact layout. See
Whitespace Control for the full treatment and the mapping table
for people coming from Jinja, Twig, or Go text/template.
1.5 String literals, interpolation, and comments¶
- String literals: single-quoted
'...'(no interpolation; escapes\\\'\n\t\xHH) and double-quoted"..."(full escape set\n \t \r \\ \" \xHH \u{...}plus embedded interpolation). Adjacent string literals do NOT implicitly concatenate; use~. A backtick raw string`...`performs no escape processing (useful for regex patterns and paths). - String interpolation: inside a double-quoted string,
#{ expr }embeds an expression, compiling to a~concatenation chain.\#{is a literal#{. Single-quoted and backtick strings never interpolate. Example:"Hello #{name | upper}!". - Block comment:
{# ... #}, consumed entirely, emits nothing. An unterminated{#is a lex error at the opening position. The#}closer eats one following newline. - Line comment inside CODE:
#to end of line within an expression or statement head emits nothing. A#inside a string literal is literal.
1.6 The verbatim region¶
The primary escape hatch for brace-dense bulk output. Its body is copied byte for
byte and is NOT scanned for any Quill syntax (not {{, not {#, not statement
keywords):
@verbatim {
public static void main(String[] args) {
Map<String,Integer> m = new HashMap<>() {{
put("a", 1);
}};
}
@}
The body ends at the @} that balances the @verbatim { opener (a lone } under
bare mode); inner { } are tracked by a raw-brace DEPTH COUNTER that never
interprets {{. For a body that must contain an unbalanced brace or the literal
close sequence, a FENCED verbatim takes an author-chosen terminator:
The region ends at the first line equal to the fence token.
1.7 Bracket balancing, literals, and word-operator disambiguation¶
- Bracket balancing inside CODE. Inside an interpolation or statement head, the
lexer balances
(),[], and{}so an inner}does not prematurely close the interpolation. The close sigil}}is recognized only at brace-depth zero relative to its opener, so{{ {a: 1, b: 2} | json }}works. - Numbers:
42,1_000_000(digit-group_separators between digits),3.14,1_0.0_5,0xFF,0b1010,0o755,1e9. Int is int64, float is float64. - Bool / null:
true,false,null(canonical);noneis an accepted alias fornull. Case-sensitive;TrueandNULLare identifiers. - Sequence / mapping literals:
[1, 2, 3]with trailing comma and spread[...xs, 4];{a: 1, b: 2}with shorthand{a}for{a: a}, computed keys{(expr): v}, trailing comma, and spread{...base, c: 3}. Because a mapping literal appears only in CODE, its braces never collide with TEXT braces. -
Word-operator / identifier disambiguation. Tokenization is maximal-munch: longer operators win (
==before=,//before/,?.before?,not inbeforein,<=>before<=). A word-operator (and,or,not,in,is,matches,xor,starts,ends,has) is an operator ONLY in operator position; in primary position and immediately after.or|it is a plain identifier, so a host field namedinis reachable asrecord.in. -
Special primary names. Three reserved primaries are always defined, exempt from the strict-undefined rule, and resolved by the engine rather than by context lookup:
| Name | Value | Use |
|---|---|---|
_self |
the current Template |
import _self as me then me.helper(...); from _self import helper |
_context |
the live context as an *Array |
reflective access, host RNG seeding, dump(_context) |
_charset |
the configured charset string (default "UTF-8") |
charset-sensitive author logic |
A bare read of any of the three NEVER raises an undefined-variable error, even
under strict mode; the engine supplies the value. They are reserved: a context
variable named _self, _context, or _charset is shadowed. _self as an
import/from source lets a template call its OWN macros and those it inherits
via extends (Section 5.4).
1.8 Fixed delimiters and source positions¶
Quill FIXES its delimiters. The verbatim fence token and the pragma bare
statement-lead mode are the only per-template knobs, and neither changes the
interpolation, comment, or string-interpolation sigil bytes. Source is CR/CRLF-
normalized to LF before line counting; every token and AST node carries 1-based
{Line, Col}; diagnostics report template:line:col. A @line N statement
(Section 4.7) resets the reported line for embedded or generated fragments.
2. Interpolation and output¶
2.1 The print form¶
{{ expr }} evaluates expr and renders it via the ToText rules
(Types). {{ u.name | upper }} is a pipe expression rendered to
output.
2.2 Pipe filters¶
| pipes the left value into a filter as its first argument: x | upper is
upper(x); x | f(a, b) is f(x, a, b). Filters chain left to right: x | trim
| upper. The pipe is an ordinary expression operator (Section 3), so a filtered
value may appear anywhere an expression may. Because | is the filter operator,
bitwise-or is the word b_or (Section 3.1).
The pipeline composes with arrow functions and the spaceship comparator into a Unix-style pipeline:
{{ users
| filter((u) => u.active)
| sort((a, b) => a.rank <=> b.rank)
| map((u) => u.name | upper)
| join(", ") }}
2.3 The postfix conditional on output¶
{{ expr if cond }} renders expr only when cond is truthy; otherwise it
renders nothing (the empty string). {{ ", admin" if u.isAdmin }} emits , admin
exactly when u.isAdmin is truthy. An optional else tail supplies a fallback:
{{ u.title if u.hasTitle else "(untitled)" }}. A symmetric {{ expr unless cond }}
is accepted. It is pure sugar: {{ x if c }} desugars to the ternary
{{ c ? x : "" }}, so it adds no new evaluation rule.
3. Expressions, operators, and precedence¶
Expressions are Go-flavored: infix arithmetic, pipe filters, arrow functions, null-safe access. The grammar is Pratt-parsed. An expression appears only in CODE positions and always evaluates to a single dynamic value (Types). The full operator catalogue and precedence ladder are in Expressions; this section summarizes them.
3.1 The published ladder¶
Quill publishes its own precedence numbers; only the relative ordering is binding. Higher binds tighter.
| Level | Operators | Assoc |
|---|---|---|
| 17 | postfix: . ?. [ ] ?[ ] ( ) call | filter |
left |
| 16 | prefix: not (!) - + ... spread |
right |
| 15 | ** power |
right |
| 14 | * / // % |
left |
| 13 | + - |
left |
| 12 | ~ concat |
left |
| 11 | .. range |
left |
| 10 | comparison/membership/test | non-assoc |
| 9 | b_and (&) |
left |
| 8 | b_xor (^) |
left |
| 7 | b_or (|||) |
left |
| 6 | and (&&) |
left |
| 5 | xor |
left |
| 4 | or (||) |
left |
| 3 | ?? coalesce, ?: elvis |
right |
| 2 | ? : ternary, postfix if/unless/else |
right |
| 1 | => arrow, = assignment / destructuring |
right |
Power and unary minus. Power (15) binds tighter than unary minus (16 prefix),
but the right operand of the right-associative ** re-enters at the prefix level
and unary minus wraps the power by AST shape, so -1 ** 0 = -1 and (-1) ** 2 = 1
from the table. Bitwise-or spelling. Because | is the pipe, bitwise OR is
b_or (alias |||), bitwise XOR is b_xor (alias ^), bitwise AND is b_and
(alias &).
3.2 The operator catalogue¶
The runtime rules for equality, ordering, coercion, and arithmetic are in
Types; the surface is in Expressions.
Briefly: logical and/or/xor/not over one truthiness rule; integer-only
bitwise ops; typed ==/!= (1 == "1" false); one ordering comparator; membership
in/not in via typed equality; the RE2 matches operator; starts with/ends
with; the has some/has every quantifiers; the .. range; numeric-only
arithmetic ("3" + 4 is a type error); ~ string concat; the three fallthrough
predicates ?/?:/??; destructuring assignment =; the arrow =>; and
grouping ( ).
3.3 Primary expressions and call arguments¶
A primary is a bare identifier (context lookup), a literal, ( expr ), a sequence
or mapping literal, a function call name(args), or the postfix chain (access,
subscript, slice, call, filter, test). Attribute and index access are specified in
Types.
One argument grammar for every callable. Filter calls, function calls, and test
calls all share the single Args production (Grammar): positional
e, named name: e, and spread ...e, in that order, with declared defaults
filling any parameter not supplied. name(args) is a function call; x | f and
x | f(args) are filter calls (the pipe supplies the first argument); x is t,
x is t arg, and x is t(args) are test calls. Because all three resolve to the
same Args grammar and the same callable signature, named arguments are available
uniformly across filters, functions, methods, and tests.
4. Control flow and scoping¶
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 }. The
examples below use the @-default spelling. The narrative treatment is in
Control Flow.
4.1 Conditionals¶
One if head, zero-or-more elseif, optional else. Conditions are arbitrary
expressions taken in the single truthiness rule (Types).
4.2 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. - Non-iterable is a runtime error, NOT a silent empty loop: a silently
skipped loop would omit an entire 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,
loopexposesloop.parent,loop.index0,loop.index,loop.first,loop.last,loop.length,loop.revindex,loop.revindex0,loop.prev,loop.next, and the methodloop.changed(expr). ALL loop fields are ALWAYS defined: the*Arrayalways knows its length, and a host iterator is drained to an*Arraybefore the loop. loop.prev/loop.nextare the previous and next element;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, by the single typed-equality rule. 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 (
@for ... if cond). An optionalif <expr>clause between the iterand and the body brace pre-filters the iterand to the elements for whichcondis truthy:
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 (@for ... recursive). 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):
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 a body set of an outer name does not persist after the loop;
accumulate 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.
4.3 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: @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.
4.4 Effect, flush, deprecation, logging¶
@do expr: evaluate for side effects, no output.@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.@log exprevaluatesexprand writes its text form to the host logger (WithLogger(l), default a discarding logger). It produces NO rendered output but IS a coverable unit.
4.5 Scoped variable region and filter-apply¶
@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.
4.6 Feature guard and types¶
@guard filter("markdown") { ... } else { ... }selects a branch on whether the named callable is registered; the dead branch is parsed but NOT validated against unknown callables. The value-level counterpart is the inline registry testname is filter/name is function/name is test.@types { x: string, n: int }declares context types, consumed by the gradual checker (Types), not as an inert metadata side-channel.
4.7 Region statements¶
@escape html { ... }/@escape off { ... }sets the active escaping strategy for a region. Default isoff; see 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. One level isWithTabWidthspaces (default 4). See Standard Library.@verbatim { ... }/ fenced verbatim: literal body, not scanned (Section 1.6).@line 42resets the reported source line for diagnostics.@cache key="header" ttl=3600 tags=["a"] { ... }caches a rendered body under a key with optional ttl/tags.
5. Composition: inheritance, blocks, macros, includes¶
Composition is built on the internal template contract: render a body, resolve
a block by name, look up a macro, and walk the parent chain. (The public opaque
quill.Template handle exposes only the inspection surface Name/BlockNames/
HasBlock/HasMacro.) The shared data structure is the BLOCK TABLE, an ordered
map from block name to a
BlockRef{Owner, ID}; inheritance, embed, and trait reuse all reduce to building
and merging block tables and walking a parent chain. Macros are a separate,
isolated function namespace. The narrative treatment is in
Composition.
5.1 The closed keyword set¶
The statement keywords, fixed for the lexer's statement recognition (Section 1.3).
Under the @-default each is written with a leading @ and a block closes with
@}; under pragma bare each is written bare and a block closes with a lone }:
extends block for if elseif else macro set include import
from use embed with apply do flush deprecated guard types
escape sandbox verbatim line cache capture log tab
provide yield call
Under the default, a line begins a statement ONLY when its first non-whitespace
character is @ immediately followed by one of these keywords; everything else at
line start is text. Under pragma bare, a line that begins with one of these
keywords (forming a Quill-shaped head) is a statement.
5.2 Inheritance¶
@extends "base.quill" // single parent; content outside blocks in a child is rejected
@block body { // define + render-in-place; long form
...
@}
@block title "Default Title" // shortcut value form
@block outer { // nested, independently overridable
@block inner { ... @}
@}
@extends "<expr>" takes a string-coerced expression, so a candidate list
@extends ["a.quill", "b.quill"] selects the first that exists. Inside an overriding
block, parent() renders the parent's version. block("name") and block("name",
"other.quill") render a named block of this or another template; block("name") is
defined tests existence.
5.3 Macros¶
@macro greet(name, greeting: string = "Hello", ...rest) {
{{ greeting }} {{ name | default("guest") }}
@}
Declared params with constant defaults, optional type annotations, and a variadic
capture ...rest. A macro sees ONLY its params, defaults, variadics, and host
globals; the caller's local context is invisible. A macro returns its captured
output (a Str, or Safe under escaping).
Tail captures: ...rest for positional args, **opts for named args. A macro
takes two optional tail parameters in a fixed order: an optional positional
variadic ...rest that collects excess POSITIONAL arguments into a list, then an
optional keyword variadic **opts that collects excess NAMED arguments into a
map<string, any>. **opts must be the last parameter:
@macro render_field(name, **opts) {
<input name="{{ name }}"{{ opts | keys | join(" ") }}>
@}
{{ render_field("email", id: "e1", class: "big") }} // opts == { id: "e1", class: "big" }
Without a **opts tail an unmatched named argument is a typo error; with one it is
absorbed. Because **opts is an ordinary mapping, it forwards to a nested call by
spread: inner(...opts).
The macro namespace is in scope inside every macro body. A macro body sees the
names of all macros visible to the template (its OWN, sibling macros in the same
template, and macros brought in by import/from), so a macro may call itself or
a sibling directly by name. Recursion and mutual recursion are reachable by bare
name and by the _self import path (import _self as me; me.tree(...)).
Call blocks (@call). A @call name(args) { body } invokes macro name and
binds a caller() callable inside the macro body that renders the block:
@macro section(title) {
## {{ title }}
{{ caller() }}
@}
@call section("Overview") {
This body is rendered where the macro calls caller().
@}
@call(p1, p2) name(args) { body } declares caller parameters that caller(v1, v2)
binds positionally in the block scope. caller() is visible only in the macro the
@call directly invokes and is a runtime error outside any @call.
5.4 Imports and traits¶
@import "forms.quill" as forms // namespace; call forms.input(...)
@from "forms.quill" import input, label as lbl // selective; call input(...), lbl(...)
@use "buttons.quill" // import all blocks of a traitable template
@use "buttons.quill" with { submit: ok } // block aliasing/rename
Top-level import is global; in-block import is block-local. A trait has no parent, no macros, and no free body; trait-then-own precedence means the importing template's own block definitions win over imported ones.
5.5 Embed¶
Inline an anonymous child of the embedded template: include plus block override in
one construct. Supports with, only, and ignore missing.
5.6 Includes¶
Statement form:
@include "header.quill"
@include "row.quill" with { user: u }
@include "row.quill" with { user: u } only
@include "maybe.quill" ignore missing
@include ["a.quill", "b.quill"] // first that exists
with map adds vars to the current context; only renders with just those vars;
ignore missing tolerates absence, rendering nothing; a sequence is a candidate
list, first existing wins.
Function form, returning rendered output as an expression value:
{{ include("snippet.quill", { x: 1 }, with_context: false, ignore_missing: true, sandboxed: true) }}
5.7 Accumulating content slots¶
@provide and @yield collect rendered content from many sites into a named
buffer and emit it once, the complement of @block: where a block OVERRIDES, a
slot ACCUMULATES.
@provide <label> { body }appends the rendered body to the slot named<label>and emits nothing at its own position. Every contribution appends in execution order.@yield <label>emits the slot's accumulated content once. It is DEFERRED: a@yieldplaced before the@providesites that feed it is backfilled with the complete accumulation after the render.slot(label)is the expression form: it returns the slot's content AS OF THE CALL as a value, capturing only what accumulated before the call.
Slots span the whole render, INCLUDING sub-renders: a @provide inside an
@included or @embedded partial appends to the enclosing render's slot buffer,
and a @yield in the shell is backfilled from those contributions.