LZR language reference
This page completes the LZR language surface documented across LZR templating and expressions and scope: how the parser really treats the two syntax layers, the numeric <for> loop, the <frame> element, the data-hm-* runtime binding vocabulary, and the diagnostics the compiler and renderer can emit. Every example and every diagnostic message on this page was produced by the real engine (lzr render-page / lzr compile-template), not transcribed from intent.
Two syntax layers: tags author, markers lower
LZR control flow has two textual forms. The structural tag form (<if>, <elseif>, <else>, <foreach>, <for>, <set>, <frame>) is the authoring surface. Before compilation the parser rewrites each structural tag into an internal marker region in mustache syntax — {{#if expr}}, {{else}}, {{/if}}, {{#foreach item,index in collection}}, {{#set name = expr}}, {{#for key=n;until=3;…}}, {{#frame name64=…}} — and those markers are what the compiler turns into region kinds (if_open, else, if_close, foreach_open, for_open, set, frame, expr, static). An <elseif> has no marker of its own: it lowers to {{else}} plus a nested {{#if}}, closed by as many {{/if}} markers as the chain nested.
The numeric range loop: <for>
Where <foreach> iterates a collection you already have, <for> generates its collection from numeric bounds. All bounds are expressions and must evaluate to numbers.
for attributes
Verified against parseFor and evaluateForRange behavior through lzr render-page.
| Attribute | Required | Default | Meaning |
|---|---|---|---|
key | yes | — | Scope name bound to the current range value. |
until | one of until/through | — | Exclusive end bound: iterate while the value is before it. |
through | one of until/through | — | Inclusive end bound: iterate while the value has not passed it. Declaring both until and through is malformed. |
from | no | 0 | Start value. |
step | no | 1 | Increment per iteration. Negative counts down; zero is an error; fractions are allowed. |
index | no | index | Scope name bound to the zero-based iteration counter. |
Verified example — this source:
<ol>
<for until="{{ 3 }}" key="n">
<li>n={{ n }} index={{ index }}</li>
</for>
</ol>
<p><for from="{{ 2 }}" through="{{ 10 }}" step="{{ 4 }}" key="v" index="slot">{{ slot }}:{{ v }} </for></p>
renders through lzr render-page to:
<ol> <li>n=0 index=0</li> <li>n=1 index=1</li> <li>n=2 index=2</li> </ol> <p>0:2 1:6 2:10 </p>
A negative step counts down (from=3 until=0 step=-1 renders 3 2 1), and fractional steps keep their fractions while whole numbers render as integers (from=0 through=1 step=0.5 renders 0 0.5 1). Item identity inside the loop is the zero-based index over a synthetic range collection named after the loop key, so editing a bound refreshes the loop frame in place. The live scenario fixture is controls/for-range.
for failure modes
Messages are verbatim engine output.
| Diagnostic | When | Message |
|---|---|---|
malformed_for | Missing key, neither or both of until/through, or whitespace in names. | for syntax must be <for from="{{ start }}" until="{{ end }}" step="{{ step }}" key=value index=index> |
for.zero_step | step evaluates to 0. | for step must not be zero |
for.non_numeric_bound | A bound expression produces a non-number. | for expression "'x'" produced string, not a number |
for.range_too_large | More than 100000 iterations. | for range exceeds 100000 iterations |
Bounded route-render jumps: <frame>
A <frame> declares a named region whose content is rendered from another route. The server render emits a host element; the browser runtime fills, refreshes, and navigates it. name and src are required; src may interpolate expressions (src="/items/{{ 40 + 2 }}" resolves to /items/42 — engine-verified).
frame attributes
Defaults are the values the renderer stamps when the attribute is omitted - taken from real host output.
| Attribute | Values | Default | Meaning |
|---|---|---|---|
name | identifier (no whitespace) | required | Frame identity for the runtime and inspector. |
src | route path, expressions allowed | required | Route whose render fills the frame. |
target | string | derived from src (slashes become dashes) | Render-target key for the frame content. |
loading | eager | visible | on-activate | lazy (legacy) | eager | When the runtime loads the frame content. |
route-pattern | absolute path, literal or {param} segments | none | Route family this frame serves; parameters must be unique. |
refresh | string | manual | Refresh policy stamped for the runtime. |
history | push | replace | none | push | How frame navigation writes browser history. |
error | inline | retain | inline | Show errors in place, or retain the last good content. |
preserve | string | none | State preservation hint stamped for the runtime. |
A frame written with a body is a container: the server-authored body stays in the host as placeholder content and the host is stamped data-hm-frame-state="placeholder". That body is mandatory for loading="visible", loading="on-activate", and error="retain" — a self-closing frame with those values fails compile with frame.placeholder_missing. Host attributes on the tag pass through a whitelist (class, id, role, aria-hidden, data-*); everything else is dropped. Verified render:
<frame name="docs-stage" src="/runtime/frame/root-1/stage.html"
loading="visible" history="replace" error="retain"
class="stage-shell" data-stage="docs">
<p>Placeholder body shown until the frame activates.</p>
</frame>
<div class="stage-shell" data-stage="docs" data-hm-frame="docs-stage" data-hm-frame-src="/runtime/frame/root-1/stage.html" data-hm-frame-target="runtime-frame-root-1-stage.html" data-hm-frame-route="/runtime/frame/root-1/stage.html" data-hm-frame-source-template="..." data-hm-frame-source-span="..." data-hm-frame-loading="visible" data-hm-frame-refresh="manual" data-hm-frame-history="replace" data-hm-frame-error="retain" data-hm-frame-state="placeholder"> <p>Placeholder body shown until the frame activates.</p> </div>
frame failure modes
Messages are verbatim engine output.
| Diagnostic | Message |
|---|---|
malformed_frame | frame syntax must be <frame name=main src=/routes/path /> |
frame.loading_invalid | frame loading "fast" is not eager, visible, on-activate, or the legacy lazy value |
frame.history_invalid | frame history "rewrite" is not push, replace, or none |
frame.error_mode_invalid | frame error mode "explode" is not inline or retain |
frame.route_pattern_invalid | frame route pattern "a//b" must be an absolute pathname with literal or {param} segments |
frame.placeholder_missing | visible, on-activate, and retained frames require a server-authored container body |
frame.src_missing | Render-side guard: frame src is required when the evaluated src is empty. |
Live frame fixtures with runtime canaries live under src://runtime/frame/eager.html and src://runtime/frame/visible.html.
Runtime binding attributes: data-hm-*
Templates opt elements into browser-runtime behavior with data-hm-* attributes. The binding compiler (lzr compile-template-metadata) classifies every such attribute into a binding kind; the table below is the verified classification of a real report.
data-hm-* binding vocabulary
Kinds and keys verified via lzr compile-template-metadata (schema lzr.template-metadata.v1).
| Attribute | Binding kind | Key | Role |
|---|---|---|---|
data-hm-route | route | route | Client-graph navigation to a route on activation. |
data-hm-route-param-* | route-param | the * suffix | Named parameter fed into the route pattern. |
data-hm-partial | partial | partial | Marks a region the runtime can swap partially. |
data-hm-selection | selection | selection | Selection state binding. |
data-hm-resource | resource | resource | Declares the data resource an element depends on. |
data-hm-command | command | command | Dispatches a named command (for example an action-pack action). |
data-hm-command-param-* | command-param | the * suffix | Named argument for the command. |
data-hm-cutout | imperative-cutout | cutout | Region owned imperatively by runtime code, excluded from declarative patching. |
any other data-hm-* | local-effect | name minus data-hm- | Local presentation effect; no graph authority. |
Frames join this vocabulary from the other side: a <frame> tag is projected into the same metadata report as data-hm-frame, data-hm-frame-src, and its option attributes, and the rendered host carries them (see above). Expressions inside attribute values additionally produce expression-binding rows that record the expression, its root, and its resolved data target. The expansion-provenance attribute data-hm-from is documented with components.
Diagnostics vocabulary
Compile and render diagnostics carry a severity, a stable code, a message, and a source span. Three families cover template structure; each code below was reproduced against the real engine. The malformed-repair scenario shows how the Inspector surfaces them with repair affordances.
Structural balance diagnostics
Emitted by the marker-region scanner when control regions do not pair up.
| Code | Meaning |
|---|---|
unclosed_if / unclosed_foreach / unclosed_for | A control region was opened and never closed. The scope mapper mirrors this as template.scope_unclosed_if (etc.) with its own span. |
unmatched_if / unmatched_foreach / unmatched_for | A closing tag or marker appeared without its matching open. |
unmatched_else | else without a matching if. |
malformed_foreach / malformed_for / malformed_set / malformed_frame | The construct's attributes do not satisfy its grammar; the message states the expected shape. |
Markup diagnostics (template.markup.*)
Tag-syntax validation over the authored source; balance of ordinary HTML elements is deliberately not enforced here.
| Code | Meaning |
|---|---|
template.markup.unterminated_tag | A < opened a tag that never reaches >. |
template.markup.unterminated_comment | <!-- without -->. |
template.markup.malformed_tag | Tag body is empty or not a tag at all. |
template.markup.malformed_tag_name | Tag name starts with an illegal character. |
template.markup.closing_tag_has_attributes | Closing tags must not contain attributes or trailing bytes. |
template.markup.invalid_self_closing_tail | The self-closing / must be the final token in a start tag. |
template.markup.invalid_attribute | Attribute does not start with a legal name character. |
template.markup.invalid_attribute_tail | Unexpected bytes after an attribute name (expected whitespace, =, /, or >). |
template.markup.missing_attribute_value | name= with no value following. |
template.markup.unterminated_attribute_value | A quoted attribute value never closes its quote. |
template.markup.invalid_unquoted_attribute_value | An unquoted value contains ", ', <, or =. |
Value and strict-mode diagnostics
Emitted at expression evaluation, frame validation, and the strict-runtime gate.
| Family | Codes | Meaning |
|---|---|---|
| for ranges | for.zero_step, for.non_numeric_bound, for.range_too_large | Numeric-range rules above. |
| frames | frame.loading_invalid, frame.history_invalid, frame.error_mode_invalid, frame.route_pattern_invalid, frame.placeholder_missing, frame.src_missing | Frame runtime-contract rules above. |
| strict-runtime gate | legacy.runtime.handlebars_control, legacy.runtime.repeat_tag, legacy.runtime.make_tag, legacy.runtime.set_shorthand, legacy.runtime.root_wrapper, legacy.runtime.original_src_protocol, legacy.runtime.json_decode_pipe | Legacy-only constructs rejected before strict rendering; most messages name a migration path (lzr legacy-normalize or --legacy-compat). |
lzr render-page -in src/controls/for-range.html -out /tmp/for-range.html lzr compile-template-metadata -in src/docs/lzr-language-reference.html -out /tmp/meta.json hypermake explain rendered/public/controls/for-range.html hypermake affected-by src/docs/lzr-language-reference.html