Language reference

Expressions and pipes

This is the reference for the expression dialect inside {{ }}: how a source is classified, the operator grammar as the evaluator actually splits it, bracket indexing, scope frames, protocol reads, and every built-in pipe. Every example on this page was run through the real engine (lzr render-page, the same entry the site recipes use) and shows the output it actually produced. The single authority is the compatibility evaluator in internal/lzr/render/compat_expr.go — there is no separate spec.

How an expression is classified

Before evaluation, expr.ParseProgram (internal/lzr/expr/expr.go) sorts each {{ }} source into one of four kinds:

Expression kinds

Classification decides dependency extraction; evaluation of compat expressions happens in compat_expr.go.

KindShapeBehavior
Route capture{{ {slug} }} — the whole source wrapped in single braces, identifier onlyResolves the route capture; dependency target route://capture/slug. Outside a routed render it fails with missing_value: missing value for route://capture/slug (verified).
Literal'text', "text", a number, true, false, nullSelf-contained value, no dependency.
PathDotted identifiers: campaign.title. Identifier chars are A–Z a–z 0–9 _ -Scope-chain lookup; dependency data://campaign#/title. Note hyphens are legal in a plain path but split as subtraction inside any compound expression.
Compat expressionEverything else: operators, pipes, indexing, protocol readsEvaluated by compatEval.eval; dependencies are extracted by scanning roots and protocol addresses.

Operator grammar and precedence

The evaluator has no token tree; it repeatedly splits the source at the top level (quote- and bracket-aware) in a fixed order. Whatever splits first binds loosest. The order, loosest to tightest:

Split order (loosest first)

From compatEval.eval in internal/lzr/render/compat_expr.go. All example outputs verified against the engine.

#FormSemanticsVerified example → output
1( …)Outer parens stripped; [] is the empty-list literal(1 + 2) * 39
2helper:restRegistered prefix helper (see extensions)pack-dependent
3c? a: bTernary; splits at the first top-level ?:1 > 2? 'big': 'small'small
4a || bLogical or; returns a boolean, never the operandfalse || 0false
5v | pipe:…Pipe chain (see pipes)'abc' | sha256 | substr:0:8ba7816bf
6scheme://…Protocol read (see protocol reads)data://campaign.json → payload value
7a && bLogical and; returns a booleantrue && 'yes'true
8a == bEquality of printed forms'1' == 1true
9>= <=!= > <Numeric when both sides are numbers; string when both are strings; != compares printed forms'b' > 'a'true
10a. bPHP-style concat; the dot must have whitespace on both sides'x'. 'y'xy
11literals's'/"s", numbers, true false null. No escape sequences inside strings — use the other quote kind4.54.5
12a + b + …N-ary. Sum when every part is a number, else string-concat of all parts1 + 2 + 36; 'a' + 1a1
13a - b, a * b, a / bNumeric only, in that order; division by zero errors1 + 2 * 37; 10 / 42.5
14!aTruthiness negation!''true
15path / indexScope lookup with suffix chain (below)items[0].titleAlpha

Truthiness

One rule, from truthy in internal/lzr/render/shape.go, used by <if>, ternary, ||/&&, ! and the empty pipe: null, "" and numeric 0 are false; everything else is true — including empty lists and maps. Test emptiness of a collection with | empty or | jq:'length', not with bare truthiness.

Paths, bracket indexing, suffix chains

Paths resolve against the scope chain (next section). After any resolved base you can chain .field and [expr] segments freely. The bracket expression is a full expression itself. All verified against this corpus' data:

Verified indexing forms lzr
{{ campaign.title }}             -> Corpus Campaign
{{ items[0].title }}             -> Alpha        (list index)
{{ items[-1].title }}            -> Gamma        (negative index counts from the end)
{{ items[1 + 1].title }}         -> Gamma        (computed index)
{{ labels.statuses['live'] }}    -> Live now     (quoted map key)
{{ labels['site']['meta']['lang'] }} -> en       (chained brackets)
{{ labels.statuses[item.status] }}            (loop-variable key)
		

Out-of-range list indices are hard errors (index 3 out of range); missing map keys are missing-value errors. Inside a foreach body, missing item subpaths render as an empty string instead (verified: {{ item.nope }}"") — loop rows tolerate ragged data, page-level names do not.

Scope frames and name resolution

A name resolves through nested frames, innermost first. This is the same chain hypermake lzr scope-map reports (see Expressions and scope for the worked tour):

Resolution order

Innermost frame wins. Component-attribute precedence details live on the components page.

FrameProvidesVerified behavior
Loop frameforeach/for item binding (key=, default item) and index binding (index=, default index, zero-based number){{ index + 1 }}1, 2, 3…; renamed via key="row" index=i. A foreach over a map iterates its values in key-sorted order.
<set> bindings<set key=name value=… />, in statement orderA later <set> with the same key shadows the earlier one from that point on (verified). A <set> inside an <if> branch or loop body is not visible after the branch closes — using it outside is the compile error template.binding_ambiguous (verified).
Component inputsAttribute bindings of the enclosing component definitionSee Components.
Page contextRoute captures, initial scope (--scope-json), graph-provided contextNames do not auto-bind to data/*.json files — {{ items }} without a <set> fails with missing_value (verified). Bind protocol reads explicitly.

Unresolvable names are build-failing diagnostics, not silent blanks: missing_value for plain paths, compat_expr_failed inside compound expressions. The sanctioned probes are the missing-tolerant pipes | isset and | empty — the only pipes that accept a missing input instead of propagating the error.

Protocol reads in expressions

An expression whose source starts with a known scheme is a protocol read. The compat evaluator recognizes src://, data://, cache://, public://, rendered://, projection://, client://, inspector://, project://, request-projection:// (hasProtocolPrefix, internal/lzr/render/core_helpers.go). Resolution asks the graph authority first; in page renders the file-backed schemes then resolve from the project root:

Read behavior, as verified

resolveProtocol / protocolPhysicalPath in compat_expr.go.

RuleVerified example
.json reads decode to values; a $hyper entity envelope is unwrapped to its data payload automatically{{ data://campaign.json }} yields the payload map — campaign.title on this page renders "Corpus Campaign"
A bare name (no slash, no extension) appends .json; a bare data:// reads data/data.jsondata://campaigndata://campaign.json (verified)
.html and .svg reads are trusted HTML (inserted unescaped in text position){{ src://sub/inc.html }}<b>included</b> rendered as markup
Everything else is a plain string; a missing cache:// file reads as "" instead of erroring{{ cache://missing.txt }} → empty (verified)
{expr} segments inside a path are interpolated before resolution{{ data://sub/{which}.html }} with which = 'note' reads data/sub/note.html (verified)
A scheme the current authority cannot resolve falls back to the literal source string — silently{{ inspector://timeline/recent.json }} in a plain page render outputs the address text itself (verified)

Pipes

A pipe chain applies left to right: value | name:arg1:arg2. Arguments follow : and are full expressions themselves (quoted strings, numbers, paths — {{ labels | get:key }} works); :// inside an argument is protected from the split, so protocol addresses pass through intact. Pipe evaluation binds looser than &&/==/arithmetic and tighter than || and the ternary — {{ item.href | empty? 'unreleased': item.href }} is a verified working idiom from this corpus.

The complete built-in registry — every applyPipe case in compat_expr.go. Each output below is what the engine actually produced:

Built-in pipe reference (26 names)

Aliases: escape_html ≡ html, json_encode ≡ json, mb_substr ≡ substr. Args in [brackets] are optional.

PipeSignature / behaviorVerified example → output
html, escape_htmlHTML-escape the printed value'<b>x</b>' | html&lt;b&gt;x&lt;/b&gt;
rawMark the printed value as trusted HTML. Text position is already unescaped (see output contract); attribute positions still escape even trusted values (verified)'<i>x</i>' | raw in an attribute → &lt;i&gt;x&lt;/i&gt;
json, json_encodeJSON-encode the valuelabels.site | json{"meta":{"lang":"en"},"name":"Exprlab"}
json_scriptJSON-encode and mark trusted, for inline <script> payloadsvar x = {{ labels.site | json_script }}; emits the JSON verbatim
json_decodeIdentity — protocol .json reads are already decoded; kept for compatibilityvalue unchanged (verified)
jqjq:'query' — full jq (gojq) over the value; compiled queries are LRU-cached (internal/lzr/datatransform)items | jq:'map(.title) | join(", ")'Alpha, Beta, Gamma
getget:'a.b' — dotted sub-path; missing paths yield the missing sentinel instead of an error (pair with empty/isset)labels | get:'site.name'Exprlab; … | get:'site.nope' | emptytrue
emptyMissing-tolerant: true for missing input, else !truthymissingName | emptytrue; 0 | emptytrue; 'x' | emptyfalse
issetMissing-tolerant: false for missing input, else value!= nullmissingName | issetfalse
array_mergearray_merge:m1[:m2…] — shallow map merge, later keys win; non-map inputs are ignoredlabels.site | array_merge:labels.statuses | json → merged object (verified)
sha256Canonical lowercase SHA-256 hex of the printed value'abc' | sha256ba7816bf8f01cfea…f20015ad (matches sha256sum)
sha1Not SHA-1. Legacy-named 128-bit content fingerprint (hyperhash.Hex128String, xxhash-based, 32 hex chars)'abc' | sha106b05ab6733a618578af5f94892f3950 (real SHA-1 would be a9993e36…)
shortHashFirst 8 characters of the printed value — truncation, not hashing; use after a hash pipe'abc' | sha1 | shortHash06b05ab6
padlpadl:width[:pad] — left-pad the printed value (default pad " ")'7' | padl:3:'0'007
substsubst:from:to — replace all occurrences'a-b-c' | subst:'-':'_'a_b_c
substr, mb_substrsubstr:start[:length] — rune-safe; negative start counts from the end, negative length trims from the end'hello world' | substr:-5world; … | substr:0:-6hello
dirnameDirectory part of a path (trailing slashes trimmed)'/a/b/c.txt' | dirname/a/b
basenamebasename[:suffix] — last path element, optional suffix strip'/a/b/c.txt' | basename:'.txt'c
file_existsAlways returns false — a defused legacy compat shim; do not gate on it'/a/b/c.txt' | file_existsfalse (verified)
prefixCssClassesprefixCssClasses:'scope' — prefix each class-bearing selector block with .scope ; trusted output. Absent from the protocol-chain allowlist (callout above)'.card { color: red}' | prefixCssClasses:'scope'.scope.card{ color: red}
svgToTextsvgToText[:from[:length]] — extract <text>/<tspan> (or flowPara) content as trusted HTML; bold spans become <strong>SVG with a bold title tspan → <strong>Title line</strong> Body copy here. (verified)
charChunkWrap each character in <span>, spaces become <br>; trusted output (animation-friendly)'Hi'<span>H</span><span>i</span> (verified)
rich_text_htmlblock | rich_text_html:authority — validate a rich-text projection block (hypermake.rich-text.block.v1) against an independent media authority (…media-authority.v1) and emit its trusted HTML. The renderer additionally requires block and authority to come from two distinct graph data targets (rich_text_proof_incomplete otherwise)data://rt/block.json | rich_text_html:data://rt/authority.json<p>This is <strong>markdown</strong> …</p> (block generated by lzr project-rich-text, verified end to end)

Unknown pipe names are hard errors: unsupported pipe "name". There is no machine-readable pipe registry today — this table is transcribed from applyPipe and verified case by case.

Extension registry: prefix helpers and pack pipes

Component packs can extend the dialect through rendercompat.Registry (internal/lzr/rendercompat/helpers.go): named pipes, prefix helpers (helperName:rest-of-expression, matched before everything else), and per-pipe missing-tolerance. The stock packs register: seoDetails (prefix + missing-tolerant pipe) and mkJsonLD from the SEO pack, mkImageDetails (prefix + pipe) and imageSources from the image pack (internal/lzr/componentpack/{seo,img}/helpers.go). They attach only when the pack's component directory is on the render's component path (attachBundledComponentPackHelpers, internal/lzr/client/component_registry.go) — this site does not load them, so they are cited here, not demonstrated.

Output and escaping contract

What a value becomes in the page

FormatRenderedValue + contentResultFromValue in internal/lzr/render/content.go; all rows verified, including under --minify.

RuleVerified example
Whole-number floats print as integers; other floats print full precision6 / 23; 0.1 + 0.20.30000000000000004
Booleans print true/false; null prints &lt;nil&gt; — map it away with a ternary before emitting{{ null }}<nil> (verified)
Maps and lists print Go-style (map[key:value]) — emit structured data with | json{{ labels }}map[site:map[…]]
Text position does not auto-escape. Attribute position always escapes, even | raw/trusted values<p>{{ evil }}</p> with evil = '<b>x</b>' emits live markup; title="{{ evil }}" emits &lt;b&gt;… (verified)

Verify this page

Every example above was run through the standalone renderer against a scratch project (probe templates p1…p17), and this page itself performs live reads of this corpus' data — the campaign title and jq count shown above are rendered, not typed. Reproduce any row with:

Reproduce a probe shell
lzr render-page --root . --in src/probe.html --template probe.html --out out.html --sidecars skip
hypermake lzr scope-map src/docs/expressions-pipes.html
hypermake explain rendered/public/docs/expressions-pipes.html
		

Source authorities, in reading order: internal/lzr/render/compat_expr.go (eval, applyPipe, resolveProtocol), internal/lzr/render/content.go (output contract), internal/lzr/render/shape.go (truthy), internal/lzr/render/foreach.go (loop frames), internal/lzr/expr/expr.go (classification, dependencies), internal/lzr/richtext/validate.go (rich-text trust boundary).