Grammar
Formal EBNF grammar for the zyn template language. zyn!(...) accepts a Template as input.
Template = Node*
Node = TokensNode
| InterpNode
| GroupNode
| AtNode
TokensNode = token_tree+
(* any token tree(s) that are NOT: '@', '{', '(', '[' *)
InterpNode = '{' '{' Expr ('|' Pipe)* '}' '}'
(* outer brace content must be exactly one inner brace group *)
Expr = token_tree+
(* all token trees before the first '|' or the closing inner '}' *)
Pipe = ident (':' PipeArg)*
PipeArg = token_tree+
(* tokens until the next ':' or '|' *)
GroupNode = '(' Template ')'
| '[' Template ']'
| '{' Template '}' (* only when brace disambiguation fails *)
AtNode = '@' 'if' IfBody
| '@' 'for' ForBody
| '@' 'match' MatchBody
| '@' ElementName ElementBody
IfBody = '(' Expr ')' '{' Template '}' ElseClause*
ElseClause = '@' 'else' 'if' '(' Expr ')' '{' Template '}'
| '@' 'else' '{' Template '}'
ForBody = '(' ident 'in' Expr ')' '{' Template '}'
| '(' Expr ')' '{' Template '}'
(* classic form: @for (count) { body } expands to for _ in 0..count *)
MatchBody = '(' Expr ')' '{' MatchArm* '}'
MatchArm = Pattern '=>' '{' Template '}' ','?
Pattern = token_tree+ (* all tokens before '=>' *)
ElementName = ident ('::' ident)*
ElementBody = ('(' Props ')')? ('{' Template '}')?
Props = PropField (',' PropField)* ','?
PropField = ident '=' PropValue
PropValue = token_tree+ (* all tokens until ',' or ')' *)
Parsing Model
Template::parse is the root parser. It reads token trees in a loop, dispatching on the leading token:
| Leading token | Action |
|---|---|
@ | Flush pending tokens; parse AtNode |
{ | Flush pending tokens; run brace disambiguation |
( | Flush pending tokens; parse GroupNode (Parenthesis) |
[ | Flush pending tokens; parse GroupNode (Bracket) |
| anything else | Accumulate into pending TokenStream |
When the input is exhausted, any pending tokens are flushed as a TokensNode.
Brace Disambiguation
When the parser sees {, it must decide between an InterpNode and a brace-delimited GroupNode:
- Consume the outer
{, capturing its content stream. - Fork the content stream and attempt to consume a single inner
{group. - If the fork consumed exactly one inner brace group AND the outer content is now empty → interpolation (
InterpNode). - Otherwise → group (
GroupNodewithDelimiter::Brace).
In concrete terms:
{{ expr }}— outer brace contains only{ expr }→InterpNode{ anything }where content is not a single inner brace group →GroupNode{{ }}(empty inner) → parse error:"empty interpolation"
The {{ and }} are two separate { / } tokens, not a special lexeme.