Status: PROPOSAL — documentation-only draft, revised per Tech Lead review findings. Not authoritative until ADR-023 is approved and Tech Lead authorizes publication.
Created: 2026-10-22
Revised: 2026-10-22 (Tech Lead review findings addressed — second correction pass)
Author: Kilo (primary implementer)
Tech Lead: SAAI
Baseline: origin/main at 8858e165a76db7b14650298ca17b2265da1e4115
Approved ODs: OD-01 (PHP expressions), OD-02 (verbose keywords, single canonical form), OD-03 (XML-style component tags), OD-04 (HTML-superset), OD-05 (full 12+ directive set)
Supersedes: ADR-005 identity (SuperPHP → NeoSPX) — pending ADR-023
This document specifies the formal grammar of NeoSPX v1. It is the first implementation deliverable under the NeoSPX canonical identity (Tech Lead confirmed 2026-10-22). The grammar is a prerequisite for all lexer/parser/compiler work.
In scope (v1): Component tags, directives (@if, @foreach, @for, @while, @switch, @break, @continue, @persist, @global, @include, @php, ~setup), raw HTML passthrough, PHP expressions in attributes and directives, PHP code blocks in ~setup and @php.
Out of scope (v1): Scoped styles (dg:style), dual authoring modes, component document interchange format (JSON/YAML), incremental/fault-tolerant parsing, context-aware escaping (unresolved OD), dual surface (unresolved OD).
Notation: EBNF per ISO 14977, with LBNF labels for AST node naming.
Conventions:
= defines a production rule| separates alternatives[] denotes optional elements{} denotes zero-or-more repetition() groups elements" delimits terminal strings(* ... *) denotes comments[Label] prefix on a rule maps to the AST node class nameVersioning: Every production rule carries @[since: "1.0.0"]. Breaking changes → major bump. Non-breaking additions → minor. Bug fixes → patch.
Terminals: Terminal symbols are token names produced by the NeoSPXLexer (CORE-07). They are listed in §3.2. Terminals are atomic units — the grammar does not decompose them further.
Nonterminals: Nonterminal symbols correspond to AST node types: ComponentNode, SetupNode, DirectiveNode, RawHtmlNode (per ADR-005). LBNF labels map directly to these class names. Every nonterminal used in this specification is defined in §4 or §3.2 (terminals).
The NeoSPXLexer operates in a stateful mode-stacked architecture (Twig/Sharp pattern). Six lexer modes are active:
| Mode | Trigger | Purpose |
|---|---|---|
STATE_DATA | Default / after HTML content | Accumulate raw HTML; detect component tags, directives, output expressions, setup blocks |
STATE_BLOCK | After @ followed by known directive keyword | Tokenize directive arguments and bodies |
STATE_VAR | Inside {{ }} or {!! !!} | Tokenize PHP expressions for output |
STATE_STRING | Inside quoted strings | Tokenize string literals |
STATE_INTERPOLATION | Inside {{ }} within strings | Nested expression tokenization |
STATE_PHP | Inside ~setup or @php blocks | Tokenize PHP code blocks |
Mode transitions:
STATE_DATA → STATE_BLOCK on @ followed by a known directive keywordSTATE_DATA → STATE_VAR on {{ or {!!STATE_DATA → STATE_PHP on ~setup or @phpSTATE_DATA → STATE_DATA on any other character (raw HTML accumulation)STATE_PHP → STATE_DATA on closing ~setup or @endphpSTATE_STRING on " or 'STATE_STRING → parent mode on closing quoteCritical rule: In STATE_DATA mode, ALL characters that are not recognized as NeoSPX triggers are accumulated into T_HTML. This includes standard HTML tags (<div>, <p>, <a>), HTML attributes, HTML comments (<!-- ... -->), and plain text. The lexer does NOT parse HTML structure — it only detects NeoSPX-specific triggers (<s:, </s:, @ + known keyword, ~setup, {{, {!!).
| Token | Pattern | Mode | Description |
|---|---|---|---|
T_HTML | (any char not matching triggers) | STATE_DATA | Raw HTML text (accumulated) |
T_COMPONENT_OPEN | <s: | STATE_DATA | Component tag open |
T_COMPONENT_CLOSE | </s: | STATE_DATA | Component closing tag open |
T_TAG_CLOSE | > | STATE_DATA | Tag close |
T_SELF_CLOSE | /> | STATE_DATA | Self-closing tag |
T_ATTR_NAME | [a-zA-Z_][a-zA-Z0-9_-]* | STATE_DATA | Attribute name |
T_ATTR_EQUALS | = | STATE_DATA | Attribute assignment |
T_DIRECTIVE_START | @ | STATE_DATA | Directive start |
T_SETUP_START | ~setup | STATE_DATA | Setup block start |
T_SETUP_END | ~setup | STATE_PHP | Setup block end (same keyword, different lexer state) |
T_PHP_OPEN | <?php or <?= | STATE_PHP | PHP open tag (within setup) |
T_PHP_CLOSE | ?> | STATE_PHP | PHP close tag |
T_PHP_CODE | (PHP tokenizer) | STATE_PHP | PHP code block content |
T_OUTPUT_OPEN | {{ | STATE_DATA | Escaped output open |
T_OUTPUT_CLOSE | }} | STATE_VAR | Escaped output close |
T_RAW_OPEN | {!! | STATE_DATA | Raw output open |
T_RAW_CLOSE | !!} | STATE_VAR | Raw output close |
T_STRING | "[^"]*" or '[^']*' | Any | String literal |
T_PHP_EXPR | (PHP expression) | STATE_VAR / STATE_BLOCK | PHP expression (reused verbatim per OD-01) |
T_EOF | End of input | Any | End of file |
T_HTML accumulation: The lexer accumulates ALL characters in STATE_DATA mode into T_HTML until it encounters a NeoSPX trigger. This means:
<div class="foo"> → single T_HTML token<!-- comment --> → single T_HTML tokenHello, world! → single T_HTML token<s:ui:button /> → T_COMPONENT_OPEN, T_ATTR_NAME, ... (NOT T_HTML)PHP expression tokenization (OD-01): PHP expressions are tokenized using PHP's native tokenizer (token_get_all() or equivalent). The NeoSPX lexer delegates to PHP's tokenizer when entering expression context (STATE_VAR or STATE_BLOCK). The expression terminates when the lexer encounters a template-level delimiter.
Expression termination rules (per context):
>, />, or whitespace followed by another attribute name{{ }}): expression terminates at }}{!! !!}): expression terminates at !!}PHP native tokenizer integration: The lexer uses token_get_all() to tokenize PHP expression content. The resulting PHP tokens are mapped to T_PHP_EXPR (a single terminal). The lexer does NOT attempt to parse the PHP expression — it delegates entirely to PHP's tokenizer. The expression boundary is determined by the template lexer's delimiter detection, NOT by PHP's tokenizer.
Quoted strings in PHP expressions: PHP string literals ("...", '...') are handled by PHP's tokenizer. The template lexer does NOT need to track string boundaries within PHP expressions — PHP's tokenizer handles this. The template lexer only detects the outer template-level delimiters.
Comments in PHP expressions: PHP comments (//, /* */, #) are handled by PHP's tokenizer. The template lexer does not need to track comment boundaries within PHP expressions.
Nested delimiters in PHP expressions: Parentheses (), brackets [], and braces {} within PHP expressions are balanced by PHP's tokenizer. The template lexer detects template-level delimiters (}}, !!}, >, />) only at the top level (not inside nested PHP delimiters). Implementation: the lexer counts PHP delimiter depth (parentheses, brackets, braces) and only recognizes template delimiters when depth is 0.
Operators in PHP expressions: All PHP operators (+, -, *, /, ===, ??, ?:, etc.) are handled by PHP's tokenizer. The template lexer does not interpret operators.
Regex anchoring: All regex patterns MUST be anchored with ^ (php-dissect pattern). Pre-scan optimization: use preg_match_all to find all token starts in one pass (Twig pattern).
When multiple patterns match at the same position, the longest match wins. If two patterns match the same length, the order in the table above determines precedence (earlier = higher).
The following nonterminals are used throughout this specification. All are defined here for completeness:
| Nonterminal | Definition | Used In |
|---|---|---|
NeoSPX | Synonym for NeoSPXElement (single element). Used in directive productions to represent the directive body. | §4.2, §4.4 |
PhpBlock | A sequence of PHP statements (full PHP code). Tokenized by PHP's native tokenizer in STATE_PHP mode. Terminates at closing ~setup or @endphp. | §4.5, §4.4.8 |
PhpVariable | A PHP variable expression: $var, $obj->prop, $arr['key'], $obj::$static. Tokenized by PHP's native tokenizer. | §4.4.2 |
PhpExpression | A PHP expression (value-producing). Tokenized by PHP's native tokenizer in expression context. Terminates at template-level delimiter (see §5.8). | §4.3, §4.4, §4.7 |
PhpForHeader | A PHP for-loop header: init; condition; increment. Three expressions separated by semicolons. | §4.4.2 |
ForeachTarget | PhpVariable or PhpVariable '=>' PhpVariable. Defines the loop variable(s). | §4.4.2 |
StringLiteral | A quoted string: "[^"]*" or '[^']*'. No interpolation in v1. | §4.3, §4.4.7 |
IDENT | An identifier: [a-zA-Z_][a-zA-Z0-9_]*. Used for attribute names, component name segments, and directive keywords. | §4.3 |
ComponentName | IDENT { ':' IDENT }. Colon-separated component name (e.g., ui:button). | §4.3 |
ComponentBody | Synonym for NeoSPX (the content between opening and closing component tags). | §4.3 |
RawHtml | T_HTML — accumulated raw HTML text. | §4.6 |
OutputExpr | {{ PhpExpression }} — escaped output expression. | §4.7 |
RawOutputExpr | {!! PhpExpression !!} — raw (unescaped) output expression. | §4.7 |
[NeoSPXDocument] NeoSPXDocument ::= { NeoSPXElement } ;
The document is a sequence of zero or more NeoSPX elements. No single root element requirement.
[NeoSPXElement] NeoSPXElement ::= Component
| Directive
| SetupBlock
| RawHtml
| OutputExpr
| RawOutputExpr ;
[ComponentNode] Component ::= ComponentOpen ComponentBody ComponentClose
| SelfClosingComponent ;
[ComponentOpen] ComponentOpen ::= '<s:' ComponentName Attribute* '>' ;
[SelfClosingComponent] SelfClosingComponent ::= '<s:' ComponentName Attribute* '/>' ;
[ComponentClose] ComponentClose ::= '</s:' ComponentName '>' ;
[ComponentName] ComponentName ::= IDENT { ':' IDENT } ;
[Attribute] Attribute ::= IDENT '=' AttributeValue ;
[AttributeValue] AttributeValue ::= StringLiteral | PhpExpression ;
Component name (OD-03): Component names use colon-separated identifiers (e.g., s:ui:button). The s: prefix is mandatory. The colon-separated name resolves to a dot-separated registry key (e.g., ui.button). The canonical form is s:namespace:component. Slashes are NOT used in component names.
Attribute value: Per OD-01, attribute values can be string literals ("value" or 'value') or PHP expressions (unquoted, e.g., label=$user->name). String literals do NOT support interpolation in v1 — use a PHP expression for dynamic values: title=$item->title instead of title="{{ $item->title }}". Interpolation in attribute values is deferred (see §10, F-EASE-03).
Component matching: The closing tag's component name must exactly match the opening tag's component name. Mismatched tags produce a syntax error.
Nesting: Components can be nested to arbitrary depth. The parser maintains a stack of open component names for matching.
[DirectiveNode] Directive ::= IfDirective
| ForeachDirective
| ForDirective
| WhileDirective
| SwitchDirective
| BreakDirective
| ContinueDirective
| PersistDirective
| GlobalDirective
| IncludeDirective
| PhpDirective ;
[IfDirective] IfDirective ::= '@if' PhpExpression NeoSPX
{ '@elseif' PhpExpression NeoSPX }
[ '@else' NeoSPX ]
'@endif' ;
Canonical form: Only @if, @elseif, @else, @endif. No aliases (OD-02). Nesting: parser tracks directive depth.
[ForeachDirective] ForeachDirective ::= '@foreach' '(' PhpExpression 'as' ForeachTarget ')' NeoSPX '@endforeach' ;
[ForeachTarget] ForeachTarget ::= PhpVariable
| PhpVariable '=>' PhpVariable ;
[ForDirective] ForDirective ::= '@for' '(' PhpForHeader ')' NeoSPX '@endfor' ;
[PhpForHeader] PhpForHeader ::= [PhpExpression] ';' [PhpExpression] ';' [PhpExpression] ;
**PhpForHeader semantics:** The three sections correspond to PHP's for-loop header: initialization, condition, and increment. Each section is optional (per PHP syntax):
- `@for (;;)` — infinite loop (all sections empty)
- `@for ($i = 0; $i < 10; $i++)` — standard loop
- `@for (; $i < 10; )` — condition only
- `@for ($i = 0; ; $i++)` — init + increment, no condition
**Tokenization:** The three sections are separated by semicolons. The lexer scans for semicolons at PHP delimiter depth 0 (not inside strings, comments, or nested delimiters). Each section's content is passed to PHP's tokenizer as a `PhpExpression` (or empty). The semicolons are consumed by the lexer and are NOT part of the PHP expression content.
**Note:** `PhpForHeader` is a lexer-level construct, not a pure EBNF nonterminal. The lexer produces a structured token containing three PHP expression strings. The parser receives the three expressions separately.
[WhileDirective] WhileDirective ::= '@while' '(' PhpExpression ')' NeoSPX '@endwhile' ;
@foreach syntax (OD-01): The as clause follows PHP foreach syntax:
@foreach ($items as $item) — single value variable@foreach ($items as $key => $value) — key and value variables@foreach ($items as $value) — value only (no key)PhpVariable is a PHP variable ($var, $obj->prop, $arr['key']). The PhpExpression before as is the iterable expression.
@for syntax: Uses a PHP for-loop header (three expressions separated by semicolons). Per OD-01, standard PHP. The PhpForHeader production captures the full for-loop construct: initialization, condition, and increment.
@while syntax: Uses a PHP expression as the loop condition.
[SwitchDirective] SwitchDirective ::= '@switch' '(' PhpExpression ')' NeoSPX
{ '@case' PhpExpression NeoSPX }
[ '@default' NeoSPX ]
'@endswitch' ;
Fall-through policy: PHP-style fall-through. @case blocks do NOT implicitly break. @break must be explicitly used to exit the switch. @default is optional and must appear last (if present).
@break and @continue: Valid inside @switch, @for, @foreach, and @while directives. Outside these contexts, they produce a semantic error (not a syntax error). See §6.3 for semantic validity.
[BreakDirective] BreakDirective ::= '@break' ;
[ContinueDirective] ContinueDirective ::= '@continue' ;
Semantic validity (§6.3):
@break is valid inside @for, @foreach, @while, and @switch.@continue is valid inside @for, @foreach, and @while.@continue inside @switch (but outside a loop) produces a semantic error.@break or @continue at top level (not inside any loop or switch) produces a semantic error.[PersistDirective] PersistDirective ::= '@persist' PhpExpression ;
Semantics: @persist $var serializes the variable $var into a s-data JSON attribute on the rendered component. Requires AST-level support (not preprocessor string substitution, per ADR-005).
[GlobalDirective] GlobalDirective ::= '@global' PhpExpression ;
Semantics: @global $var declares a variable available in the template scope. The variable must be provided by the rendering context.
[IncludeDirective] IncludeDirective ::= '@include' StringLiteral ;
Semantics: Includes another template file. Path relative to template root.
[PhpDirective] PhpDirective ::= '@php' PhpBlock '@endphp' ;
Closing behavior: @php opens a PHP code block; @endphp closes it. The lexer's STATE_DATA mode detects @php and transitions to STATE_PHP mode. In STATE_PHP mode, the lexer accumulates PHP code until it encounters @endphp at PHP top-level (not inside a string or comment). The lexer delegates to PHP's tokenizer for string and comment boundaries within STATE_PHP mode — @endphp appearing inside a PHP string literal or comment does NOT trigger the mode transition.
Unterminated block: If @php is opened but no closing @endphp is found before end of file, the lexer reports a lexical error ("Unterminated @php block") and treats the remainder of the file as PHP code.
Semantics: Executes PHP code. Limited to allowed functions per sandbox rules. Subject to sandbox enforcement. Output is inserted at the directive's position. Variables declared in @php blocks are NOT available in the template scope (local to the block).
[SetupNode] SetupBlock ::= '~setup' PhpBlock '~setup' ;
Closing behavior: ~setup is used as BOTH the opening and closing delimiter. The lexer's STATE_DATA mode detects the first ~setup and transitions to STATE_PHP mode. In STATE_PHP mode, the lexer accumulates PHP code until it encounters the next ~setup keyword, which transitions back to STATE_DATA mode.
PHP-aware closing detection: The lexer in STATE_PHP mode must NOT treat ~setup appearing inside PHP string literals or comments as a closing marker. The lexer delegates to PHP's tokenizer for string and comment boundaries within STATE_PHP mode. Only a ~setup token at PHP top-level (not inside a string or comment) triggers the mode transition.
Unterminated block: If ~setup is opened but no closing ~setup is found before end of file, the lexer reports a lexical error ("Unterminated ~setup block") and treats the remainder of the file as PHP code.
Execution order: ~setup blocks execute before template rendering. Variables declared in ~setup are available in the template scope (including in @php blocks and output expressions). Multiple ~setup blocks are concatenated in order of appearance and executed sequentially.
Variable scope: Variables declared in ~setup are available in the template scope. Variables declared in @php blocks are NOT available in the template scope (local to the block).
Semantics: ~setup blocks contain PHP code running before template rendering. Subject to sandbox rules (allowed functions list). No echo/print in ~setup (output is inserted via output expressions).
[RawHtmlNode] RawHtml ::= T_HTML ;
Semantics: Raw HTML passes through to output (except for context-aware escaping of interpolated expressions, which is UNRESOLVED — see §8.3). Per OD-04 (HTML-superset), all non-directive, non-component content is raw HTML.
[OutputExpr] OutputExpr ::= '{{' PhpExpression '}}' ;
[RawOutputExpr] RawOutputExpr ::= '{!!' PhpExpression '!!}' ;
Escaping (UNRESOLVED — see §8.3): {{ $x }} is intended to be escaped by default (HTML context). {!! $x !!} is intended to be raw output (no escaping). The escaping behavior is a semantic decision, not a syntax decision. The syntax is unambiguous: {{ }} vs {!! !!}.
Component tags MUST start with <s:. Any tag not starting with <s: is raw HTML (T_HTML). Lexer STATE_DATA detects <s: and transitions to component-tag parsing mode. Closing tag name must match opening tag name. Mismatched tags produce syntax error. Nesting: arbitrary depth; parser maintains stack of open component names.
Each directive has explicit terminators (see §4.4). Directives without bodies (@break, @continue, @persist, @global, @include) are self-terminating. Nesting: parser maintains stack of open directive types; terminator must match most recent open directive of same type.
~setup can appear anywhere in the template. Multiple ~setup blocks concatenated in order of appearance. Lexer detects ~setup in STATE_DATA mode and transitions to STATE_PHP mode. The block continues until the next ~setup keyword at PHP top-level (not inside a string or comment).
~setup runs before template rendering; variables available in template scope. @php runs during rendering; output inserted at directive position. Both subject to sandbox restrictions (UNRESOLVED — see §8.3).
Lexer STATE_DATA accumulates characters into T_HTML until encountering: <s: (component tag), @ + known directive keyword (directive), ~setup (setup block), {{ or {!! (output expression). All other characters (including standard HTML tags like <div>, <p>, HTML comments, and plain text) are accumulated into T_HTML. The lexer does NOT parse HTML structure.
Attributes and directive arguments use PHP expressions (OD-01). Lexer delegates to PHP's native tokenizer for expression content. ~setup and @php blocks use PHP statements (full PHP code). Lexer delegates to PHP's native tokenizer for the entire block. Expression termination: expression ends at template-level delimiter (>, />, newline + non-expression, }} , !!}, or directive terminator). Statement termination: block ends at closing ~setup or @endphp.
Component names use colon-separated identifiers (e.g., s:ui:button). The s: prefix is mandatory. The colon-separated name resolves to a dot-separated registry key (e.g., ui.button). The canonical form is s:namespace:component. Slashes are NOT used in component names.
PhpExpression is a terminal token produced by the lexer. It is NOT a nonterminal in the EBNF grammar — it is an atomic unit.
Lexer integration: When the lexer encounters a context requiring a PHP expression (attribute value, directive argument, output expression), it delegates to PHP's native tokenizer (token_get_all() or equivalent) to tokenize the expression content. The resulting PHP tokens are collected into a single T_PHP_EXPR terminal.
Expression boundary detection: The template lexer detects the end of a PHP expression by tracking PHP delimiter depth:
(): depth increases on (, decreases on )[]: depth increases on [, decreases on ]{}: depth increases on {, decreases on }The expression terminates when a template-level delimiter is encountered at depth 0:
> (tag close), /> (self-close), or whitespace followed by an identifier followed by = (next attribute)}} or !!}Quoted strings in PHP expressions: PHP string literals ("...", '...') are handled by PHP's tokenizer. The template lexer does NOT need to track string boundaries within PHP expressions — PHP's tokenizer handles this. However, the template lexer MUST NOT recognize template delimiters (}}, !!}, >, />) that appear inside PHP string literals. Implementation: when the template lexer delegates to PHP's tokenizer, it passes the raw expression text and lets PHP's tokenizer handle strings. The template lexer only scans for template-level delimiters at the top level (depth 0, outside PHP strings).
Comments in PHP expressions: PHP comments (//, /* */, #) are handled by PHP's tokenizer. The template lexer does not need to track comment boundaries within PHP expressions.
Nested delimiters in PHP expressions: Parentheses, brackets, and braces within PHP expressions are balanced by PHP's tokenizer. The template lexer detects template-level delimiters (}}, !!}, >, />) only at the top level (not inside nested PHP delimiters). Implementation: the lexer counts PHP delimiter depth and only recognizes template delimiters when depth is 0.
Operators in PHP expressions: All PHP operators (+, -, *, /, ===, ??, ?:, ->, =>, >=, <=, etc.) are handled by PHP's tokenizer. The template lexer does not interpret operators.
Critical rule for > vs ->: The lexer checks for -> BEFORE >. If the current position is ->, it is consumed as part of the PHP expression (property access operator). Only a standalone > (not preceded by -) terminates the expression. Similarly, >= is consumed as a comparison operator, not as > followed by =.
Note: token_get_all() alone does not resolve template boundaries. The template lexer must first identify the expression's start and end positions using delimiter detection (depth tracking + template-level delimiter recognition), then delegate the content to PHP's tokenizer. The two systems work in tandem: the template lexer finds boundaries; PHP's tokenizer tokenizes content.
Problem: Given label=$user->name age=$user->age, how does the lexer distinguish the end of $user->name from the start of age=?
Algorithm (deterministic, PHP-token-aware):
= in attribute context, it enters PHP expression scanning mode.(), brackets [], braces {}. Depth increases on open, decreases on close.//, /* */, #). Inside a comment, template-level delimiters are NOT recognized./> (self-close) — checked BEFORE > to avoid partial match> (tag close) — but ONLY when not preceded by - (to avoid -> operator) and not part of >== (next attribute)= and the terminator) is passed to PHP's native tokenizer for tokenization.Examples:
| Input | Expression | Terminator | Notes |
|---|---|---|---|
label=$user->name | $user->name | > or /> or whitespace+IDENT= | -> consumed as PHP operator, NOT terminator |
label=$user->name age=$user->age | $user->name | whitespace + age= | Next attribute detected |
value=$a ? $b : $c | $a ? $b : $c | > or /> | Ternary operator — depth 0, no template delimiter inside |
data=$arr['key'] | $arr['key'] | > or /> | Brackets balanced (depth 0 after ]) |
fn=$obj->method($a, $b) | $obj->method($a, $b) | > or /> | Parentheses balanced (depth 0 after )) |
expr=$a + $b * $c | $a + $b * $c | > or /> | Operators do not affect depth |
str="hello $world" | "hello $world" | > or /> | String handled by PHP tokenizer |
x=$a /* comment */ + $b | $a /* comment */ + $b | > or /> | Comment handled by PHP tokenizer |
cmp=$a >= $b | $a >= $b | > or /> | >= consumed as PHP operator (checked before >) |
arr=$a => $b | $a => $b | > or /> | => consumed as PHP operator |
nested=$obj->{$prop} | $obj->{$prop} | > or /> | Braces balanced (depth 0 after }) |
Risk: This algorithm requires the template lexer to track PHP string and comment boundaries during the scan. If a PHP string contains a template-level delimiter (e.g., $x = "foo > bar"), the lexer must NOT terminate the expression at >. The lexer handles this by tracking PHP string state during the scan.
OD raised (if unreliable): If this algorithm proves unreliable in practice, raise OD-EXPR-BOUNDARY with options:
attr=($a ? $b : $c)attr:php="$a ? $b : $c"Recommended default: Option 1 — Require parentheses for complex expressions.
Status: This algorithm is a proposed specification, not a tested implementation. Conformance tests are required (see §12.6).
Problem: The lexer must find ~setup and @endphp closing markers in STATE_PHP mode, but these markers may appear inside PHP strings, comments, or heredoc/nowdoc blocks. The lexer must not terminate the block prematurely.
Procedure (reproducible):
STATE_PHP mode when it encounters ~setup or @php in STATE_DATA mode.'...'), double-quoted ("..."), heredoc (<<<IDENT ... IDENT), and nowdoc (<<<'IDENT' ... IDENT) boundaries. Inside any string, ~setup and @endphp are NOT recognized as closing markers.// (to end of line), /* */ (block comment), and # (to end of line) boundaries. Inside any comment, ~setup and @endphp are NOT recognized as closing markers.(), [], {} depth. (Not strictly needed for closing-marker detection, but useful for error reporting.)<<<, it reads the identifier that follows. For heredoc (<<<IDENT), the closing marker is IDENT at the start of a line (column 0) followed by ; or newline. For nowdoc (<<<'IDENT'), the same rule applies but no variable interpolation occurs inside. The lexer must scan until the heredoc/nowdoc terminator before continuing to look for ~setup/@endphp.~setup or @endphp at PHP top-level (not inside a string or comment), it checks:
~setup_foo or @endphp_extra).~setup: the keyword must be followed by whitespace, newline, or EOF.@endphp: the keyword must be followed by whitespace, newline, or EOF.STATE_DATA mode and emits the closing marker as a terminal token (T_SETUP_END for ~setup, or consumes @endphp for @php).T_PHP_CODE).How tokenization works before endpoint is known:
The lexer does NOT wait to find the closing marker before tokenizing. It scans forward, maintaining PHP string/comment state, and accumulates PHP code into T_PHP_CODE tokens. When it encounters a valid closing marker, it:
T_PHP_CODE tokenSTATE_DATA modeThe accumulated T_PHP_CODE content is then passed to PHP's native tokenizer for tokenization. Source offsets are preserved by recording the byte offset of each T_PHP_CODE token's start and end positions in the source file.
Source offset preservation:
T_PHP_CODE) carries startOffset and endOffset (byte positions in the source file).T_PHP_CODE is passed to PHP's tokenizer, the offsets are adjusted by subtracting startOffset from PHP token positions, then adding startOffset back to the result.Note: token_get_all() alone does not resolve template boundaries. The template lexer must first identify the PHP block's start and end positions using the procedure above, then delegate the content to PHP's tokenizer. The two systems work in tandem: the template lexer finds boundaries; PHP's tokenizer tokenizes content.
Problem: Given label=$user->name age=$user->age, how does the lexer distinguish the end of $user->name from the start of age=?
Algorithm (deterministic, PHP-token-aware):
= in attribute context, it enters PHP expression scanning mode.(), brackets [], braces {}. Depth increases on open, decreases on close.//, /* */, #). Inside a comment, template-level delimiters are NOT recognized./> (self-close) — checked BEFORE > to avoid partial match> (tag close) — but ONLY when not preceded by - (to avoid -> operator)= (next attribute)> vs ->: The lexer checks for -> BEFORE >. If the current position is ->, it is consumed as part of the PHP expression (property access operator). Only a standalone > (not preceded by -) terminates the expression.Examples:
| Input | Expression | Terminator | Notes |
|---|---|---|---|
label=$user->name | $user->name | > or /> or whitespace+IDENT= | -> consumed as PHP operator, NOT terminator |
label=$user->name age=$user->age | $user->name | whitespace + age= | Next attribute detected |
value=$a ? $b : $c | $a ? $b : $c | > or /> | Ternary — depth 0, no template delimiter inside |
data=$arr['key'] | $arr['key'] | > or /> | Brackets balanced (depth 0 after ]) |
fn=$obj->method($a, $b) | $obj->method($a, $b) | > or /> | Parentheses balanced (depth 0 after )) |
expr=$a + $b * $c | $a + $b * $c | > or /> | Operators do not affect depth |
str="hello $world" | "hello $world" | > or /> | String handled by PHP tokenizer |
x=$a /* comment */ + $b | $a /* comment */ + $b | > or /> | Comment handled by PHP tokenizer |
cmp=$a >= $b | $a >= $b | > or /> | >= consumed as PHP operator (checked before >) |
arr=$a => $b | $a => $b | > or /> | => consumed as PHP operator |
nested=$obj->{$prop} | $obj->{$prop} | > or /> | Braces balanced (depth 0 after }) |
Risk: This algorithm requires the template lexer to track PHP string and comment boundaries during the scan. If a PHP string contains a template-level delimiter (e.g., $x = "foo > bar"), the lexer must NOT terminate the expression at >. The lexer handles this by delegating string boundary detection to PHP's tokenizer state machine.
OD raised (if unreliable): If this algorithm proves unreliable in practice, raise OD-EXPR-BOUNDARY with options:
attr=($a ? $b : $c)attr:php="$a ? $b : $c"Recommended default: Option 1 — Require parentheses for complex expressions.
Status: This algorithm is a proposed specification, not a tested implementation. Conformance tests are required (see §12.6).
PhpExpression is a terminal token produced by the lexer. It is NOT a nonterminal in the EBNF grammar — it is an atomic unit.
Lexer integration: When the lexer encounters a context requiring a PHP expression (attribute value, directive argument, output expression), it delegates to PHP's native tokenizer (token_get_all() or equivalent) to tokenize the expression content. The resulting PHP tokens are collected into a single T_PHP_EXPR terminal.
Expression boundary detection: The template lexer detects the end of a PHP expression by tracking PHP delimiter depth:
(): depth increases on (, decreases on )[]: depth increases on [, decreases on ]{}: depth increases on {, decreases on }The expression terminates when a template-level delimiter is encountered at depth 0:
>, />, or whitespace followed by an attribute name}} or !!}Quoted strings: PHP string literals ("...", '...') are handled by PHP's tokenizer. The template lexer does NOT need to track string boundaries — PHP's tokenizer handles this. However, the template lexer MUST NOT recognize template delimiters (}}, !!}, >, />) that appear inside PHP string literals. Implementation: when the template lexer delegates to PHP's tokenizer, it passes the raw expression text and lets PHP's tokenizer handle strings. The template lexer only scans for template-level delimiters at the top level (depth 0, outside PHP strings).
Comments: PHP comments (//, /* */, #) are handled by PHP's tokenizer. The template lexer does not need to track comment boundaries within PHP expressions.
Operators: All PHP operators are handled by PHP's tokenizer. The template lexer does not interpret operators.
Note: token_get_all() alone does not resolve template boundaries. The template lexer must first identify the expression's start and end positions using delimiter detection, then delegate the content to PHP's tokenizer. The two systems work in tandem: the template lexer finds boundaries; PHP's tokenizer tokenizes content.
Lexical (lexer): unexpected character, unterminated string, invalid directive name, unterminated ~setup block. Syntax (parser): missing closing tag, malformed attribute, unexpected token, unmatched directive terminator, mismatched component tags. Semantic (parser/compiler): undefined variable, invalid function, type mismatch, sandbox violation, invalid @break/@continue placement. Codegen (compiler): unsupported node type, compilation failure.
Strategy: Panic-mode recovery as baseline. Synchronizing tokens: }, />, </, @, newline, EOF. Recovery procedure: (1) Report error with source location (line, column, offset). (2) Skip tokens until synchronizing token. (3) Resume parsing from synchronizing token. (4) Emit AST placeholder nodes for unparseable sections (enables continued analysis, future IDE/LSP support). Diagnostic quality: report first error precisely, avoid cascading errors, include actionable messages.
@break is valid inside @for, @foreach, @while, and @switch.@continue is valid inside @for, @foreach, and @while.@continue inside @switch (but outside a loop) produces a semantic error.@break or @continue at top level (not inside any loop or switch) produces a semantic error.@break inside a nested loop breaks the innermost loop.@continue inside a nested loop continues the innermost loop.<s:ui:button label="Click me" />
@if ($user->isAdmin())
<s:admin:panel />
@elseif ($user->isModerator())
<s:moderator:panel />
@else
<s:user:panel />
@endif
@foreach ($items as $item)
<s:item:card title=$item->title />
@endforeach
@foreach ($items as $key => $value)
<p>{{ $key }}: {{ $value }}</p>
@endforeach
@for ($i = 0; $i < 10; $i++)
<s:item:index value="$i" />
@endfor
@while ($hasMore)
<s:item:next />
@endwhile
@switch ($status)
@case 'active'
<s:status:active />
@break
@case 'pending'
<s:status:pending />
@break
@default
<s:status:unknown />
@endswitch
~setup
$title = 'Hello, World!';
$items = $db->getItems();
~setup
@persist $counter
<s:counter:display value="$counter" />
<div class="greeting">
Hello, {{ $name }}!
</div>
@php
$timestamp = time();
@endphp
<p>Generated at {{ date('Y-m-d H:i:s', $timestamp) }}</p>
@global $user
<p>Welcome, {{ $user->name }}</p>
@include 'partials/header.neospx'
@foreach ($items as $item)
@if ($item->skip)
@continue
@endif
@if ($item->stop)
@break
@endif
<s:item:display />
@endforeach
<s:layout:page>
<s:ui:header title="My Page" />
<s:ui:content>
<s:ui:button label="Click" />
</s:ui:content>
</s:layout:page>
<s:user:profile name=$user->name age=$user->age />
<!-- This is an HTML comment -->
<div class="container" id="main">
<p>Paragraph with <strong>bold</strong> text.</p>
<a href="/link">Link</a>
</div>
<s:ui:button>
Click me
</s:ui:link>
Error: Component tag mismatch: expected </s:ui:button>, got </s:ui:link>.
@if ($condition)
<p>Hello</p>
Error: Missing @endif for @if directive. Expected @endif before end of file.
@unknown $foo
Error: Unknown directive @unknown. Valid directives: @if, @foreach, @for, @while, @switch, @break, @continue, @persist, @global, @include, @php, ~setup.
<s:ui:button label=Click me />
Error: Attribute value must be a string literal or PHP expression. Expected " or ' or PHP expression after =.
<s:item:card title="{{ $item->title }}" />
Error: Attribute value does not support interpolation in v1. Use a PHP expression: title="$item->title".
~setup
$title = 'Hello';
Error: Unterminated ~setup block. Expected closing ~setup before end of file.
@switch ($status)
@case 'active'
@continue
@endswitch
Error: @continue is not valid inside @switch (only valid inside @for, @foreach, @while).
@break
Error: @break is not valid at top level (only valid inside @for, @foreach, @while, @switch).
@if ($condition)
<p>Hello</p>
@endforeach
Error: Directive terminator mismatch: expected @endif for @if, got @endforeach.
| Decision | OD | Resolution |
|---|---|---|
| Expression language | OD-01 | PHP expressions verbatim |
| Directive syntax | OD-02 | Verbose keywords, single canonical form |
| Component tag syntax | OD-03 | Explicit XML-style tags (<s:...>) |
| Base syntax | OD-04 | HTML-superset |
| v1 directive set | OD-05 | Full (12+ directives) |
| Decision | Resolution | Source |
|---|---|---|
| Semantic analysis placement | Embedded in parser/compiler | ADR-005 |
| Variable scope tracking | @global declares template-scope variables | SPX-COMPILER §5 |
| Function validation | Allowed functions list for ~setup and @php | SPX-COMPILER §5 |
| Component registry | Components must be registered before compilation | ADR-005 §3 |
| @switch fall-through | PHP-style (no implicit break; @break required) | This specification §4.4.3 |
| @foreach key/value | PHP foreach syntax (value, key => value) | This specification §4.4.2 |
| @for loop header | PHP for-loop header (init; condition; increment) | This specification §4.4.2 |
| ~setup closing | Dual delimiter (lexer state determines role) | This specification §4.5 |
| @break/@continue validity | Valid only inside loops/switch (see §6.3) | This specification §6.3 |
| Requirement | Status | OD Reference |
|---|---|---|
| Sandboxing — ~setup and @php limited to allowed functions | UNRESOLVED | F-COMPILER-05 |
| Context-aware escaping — {{ }} escapes HTML/JS/CSS/URL/XML | UNRESOLVED | F-EASE-03 tension |
| Raw output marker — {!! !!} requires explicit opt-in | UNRESOLVED | F-EASE-03 |
| No arbitrary PHP execution — no eval(), no include of user-controlled paths | UNRESOLVED | ADR-005 §4 |
| Component tag validation — tags validated against registered registry | UNRESOLVED | ADR-005 §3 |
Important: PHP-expression support (OD-01) does NOT grant permission for unrestricted PHP execution. The grammar permits PHP expressions in attributes and directives, but the compiler MUST enforce sandbox rules. Sandboxing is a semantic/security requirement, not a syntax requirement, and is tracked as an unresolved OD.
| Requirement | Status | OD Reference |
|---|---|---|
| Dual surface — human shorthand vs machine canonical form | UNRESOLVED | F-EASE-06 |
| Context-aware escaping from MVP | UNRESOLVED | F-EASE-03 tension |
| Debug info in compiled output | UNRESOLVED | F-EASE-11 |
| LBNF vs EBNF notation | UNRESOLVED | F-COMPILER-01 |
| Scoped styles (dg:style) | UNRESOLVED | OD-09 (SCSS framework) |
| Fault-tolerant parsing | UNRESOLVED | F-COMPILER-04 |
| Early cutoff (whitespace-only edits) | UNRESOLVED | F-COMPILER-12 |
| Grammar Feature | OD ID | Source Research | Decision |
|---|---|---|---|
| PHP expressions in attributes/directives | OD-01 | SPX-EASE §5.1.1 (Latte strategy) | Approved |
| Verbose keyword directives (@if, @foreach, etc.) | OD-02 | SPX-EASE §5.1.2 (Anka strategy) | Approved |
| Single canonical form (no aliases) | OD-02 | SPX-EASE §5.1.2 (Anka strategy) | Approved |
| Component tags (<s:...>) | OD-03 | VUE-SFC §3 (Vue/Svelte/Astro) | Approved |
| HTML-superset base syntax | OD-04 | VUE-SFC §3 (Vue/Svelte/Astro/Marko) | Approved |
| Full directive set (12+ directives) | OD-05 | SPX-EASE §5.1.5 (Kölling/Wirth) | Approved (overrode minimal recommendation) |
| @persist directive | OD-05 | ADR-005 §4 (semantic directives) | Approved |
| ~setup block | OD-05 | ADR-005 §4 (setup blocks) | Approved |
| @php directive | OD-05 | SPX-COMPILER §5 (sandbox) | Approved |
| @global directive | OD-05 | SPX-COMPILER §5 (scope tracking) | Approved |
| @include directive | OD-05 | VUE-SFC §3 (component composition) | Approved |
| @foreach key/value forms | OD-01 | PHP foreach syntax | Approved |
| @switch fall-through | OD-05 | PHP switch semantics | Approved |
| @break/@continue validity | OD-05 | PHP control flow | Approved |
| @for loop header | OD-01 | PHP for-loop syntax | Approved |
| ~setup dual delimiter | OD-05 | PHP heredoc pattern | Approved |
| Raw output marker ({!! !!}) | OD-05 | F-EASE-03 (unresolved) | Unresolved |
| Recursive descent parser | — | ADR-005 (ratified) | Approved (ADR-005) |
| Regex-and-DFA lexer | — | ADR-005 (ratified) | Approved (ADR-005) |
| Flat PHP array output | — | ADR-005, ADR-010 | Approved (ADR-005, ADR-010) |
| Checksum-based caching | — | ADR-005 §4 | Approved (ADR-005) |
| No Node.js build step | — | ADR-005 §3 | Approved (ADR-005) |
| EBNF notation | F-EASE-07 | SPX-EASE §5.1.7 (SynCode) | Proposed (not yet approved) |
| LBNF labels for AST nodes | F-COMPILER-01 | SPX-COMPILER §13.10 (BNFC) | Proposed (not yet approved) |
| Context-aware escaping | F-EASE-03 | SPX-EASE §5.1.3 (Twig/Smarty) | Unresolved OD |
| Dual surface | F-EASE-06 | SPX-EASE §5.1.6 (LLMON/DualCode) | Unresolved OD |
| Sandbox as compiler pass | F-COMPILER-05 | SPX-COMPILER §13.5 (Latte) | Unresolved OD |
| OD | Decision | Deferred To | Status |
|---|---|---|---|
| F-EASE-03 | Context-aware escaping (automatic vs explicit) | Milestone 4 (Compiler) | Unresolved |
| F-EASE-06 | Dual surface (single vs dual syntax) | Milestone 3 (Parser) | Unresolved |
| F-EASE-11 | Debug info in compiled output | Milestone 4 (Compiler) | Unresolved |
| F-NAM-06 | Component document JSON-first vs JSON+YAML | Milestone 5 (Integration) | Unresolved |
| F-VUE-02 | Dual authoring modes (Marko) from v1 | Milestone 3 (Parser) | Unresolved |
| F-VUE-03 | DSDS audience tagging | Milestone 5 (Integration) | Unresolved |
| F-VUE-04 | Build-free vs compiled | Milestone 4 (Compiler) | Unresolved |
| F-VUE-05 | Scoped styles (Vue) vs SCSS (OD-09) | Milestone 5 (Integration) | Unresolved |
| F-COMPILER-04 | Fault-tolerant parsing (AST placeholders) | Milestone 3 (Parser) | Unresolved |
| F-COMPILER-12 | Early cutoff (whitespace-only edits) | Milestone 4 (Compiler) | Unresolved |
| F-FW-01 | Feature-sliced convention | LMS track | Unresolved |
| F-FW-05 | Onboarding SUS >=68 | LMS handoff | Unresolved |
| F-REC-01 | Connecting-theme variant | Research gate | Unresolved |
| F-REC-02 | SuperPHP placement (separate vs numbered) | Research gate | Unresolved |
| F-REC-03 | Half Exceptions formulations 3 & 4 | Research gate | Unresolved |
| ADR | Action | Status |
|---|---|---|
| ADR-005 | Identity section superseded by NeoSPX | Pending Tech Lead approval |
| ADR-023 | New ADR: NeoSPX canonical identity | Pending creation and approval |
| ADR-022 | Auth transport (LMS) | Pending Founder decision |
No ADR has been created, amended, or published. The Tech Lead must approve ADR-023 (or an amendment to ADR-005) before the grammar specification is published as authoritative.
| Field | Value |
|---|---|
| Document | NeoSPX EBNF Grammar Specification (v1 Draft, Revised 2) |
| Status | PROPOSAL — awaiting Tech Lead approval |
| Baseline | origin/main at 8858e165a76db7b14650298ca17b2265da1e4115 |
| Approved ODs | OD-01, OD-02, OD-03, OD-04, OD-05 |
| Unresolved ODs | 15 (deferred to later milestones) |
| ADR approval required | ADR-023 (NeoSPX identity) or ADR-005 amendment |
| Frozen contracts modified | None |
| Source code modified | None |
| Implementation started | No |
Method: Manual systematic review of all nonterminals and terminals.
Steps:
Result: ✅ All nonterminals defined. All terminals referenced. All productions labeled.
Reproducible command (if grammar tool available):
# Hypothetical (no tool installed):
# ebnf-validator NEOSPX-EBNF-GRAMMAR-SPECIFICATION.md --check-completeness
# Expected: 0 errors, 0 warnings
Method: Manual review of all 26 examples (17 valid, 9 invalid) against §4 productions.
Steps:
Results:
| Example | Production(s) Used | Verified |
|---|---|---|
| 7.1 Simple Component | ComponentOpen, Attribute, SelfClosingComponent | ✅ |
| 7.2 Conditional | IfDirective, @elseif, @else, @endif | ✅ |
| 7.3 Foreach (Single Value) | ForeachDirective, ForeachTarget, Attribute (PhpExpression) | ✅ |
| 7.4 Foreach (Key/Value) | ForeachDirective, ForeachTarget (key => value) | ✅ |
| 7.5 For Loop | ForDirective, PhpForHeader | ✅ |
| 7.6 While Loop | WhileDirective | ✅ |
| 7.7 Switch Directive | SwitchDirective, @case, @default, @endswitch, @break | ✅ |
| 7.8 Setup Block | SetupBlock (~setup ... ~setup) | ✅ |
| 7.9 Persist Directive | PersistDirective | ✅ |
| 7.10 Raw HTML | RawHtml (T_HTML accumulation) | ✅ |
| 7.11 PHP Directive | PhpDirective (@php ... @endphp) | ✅ |
| 7.12 Global Directive | GlobalDirective | ✅ |
| 7.13 Include Directive | IncludeDirective | ✅ |
| 7.14 Break and Continue | BreakDirective, ContinueDirective (inside ForeachDirective, IfDirective) | ✅ |
| 7.15 Nested Components | Component (nested ComponentOpen/ComponentClose) | ✅ |
| 7.16 PHP Expression Attribute | Attribute (PhpExpression) | ✅ |
| 7.17 HTML Passthrough | RawHtml (T_HTML accumulation) | ✅ |
| 7.18 Invalid: Mismatched Tags | Component matching rule (§4.3) | ✅ |
| 7.19 Invalid: Missing Terminator | Directive termination (§5.2) | ✅ |
| 7.20 Invalid: Unknown Directive | Directive production (§4.4) | ✅ |
| 7.21 Invalid: Malformed Attribute | Attribute production (§4.3) | ✅ |
| 7.22 Invalid: Interpolation | AttributeValue production (§4.3) — no interpolation in v1 | ✅ |
| 7.23 Invalid: Unterminated ~setup | ~setup closing behavior (§4.5) | ✅ |
| 7.24 Invalid: @continue Outside Loop | Semantic validity (§6.3) | ✅ |
| 7.25 Invalid: @break at Top Level | Semantic validity (§6.3) | ✅ |
| 7.26 Invalid: Mismatched Terminator | Directive termination (§5.2) | ✅ |
Result: 26/26 examples verified against productions. 0 mismatches.
Method: Manual review of all 7 ambiguity points in §5.
| # | Ambiguity | Resolution | Verified |
|---|---|---|---|
| 1 | Component tag matching | MUST start with <s:; closing must match opening | ✅ |
| 2 | Directive termination | Explicit terminators per directive; stack-based matching | ✅ |
| 3 | ~setup placement | Anywhere; multiple blocks concatenated | ✅ |
| 4 | @php vs ~setup | Different execution timing and scope | ✅ |
| 5 | HTML passthrough vs NeoSPX detection | Lexer accumulates T_HTML until trigger detected | ✅ |
| 6 | Expression vs statement | Lexer delegates to PHP tokenizer; expression terminates at template delimiters | ✅ |
| 7 | Component name resolution | Colon-separated, resolves to dot-separated registry key | ✅ |
Result: 7/7 ambiguities resolved.
Method: Manual review of all 16 directive productions against the approved full directive set (OD-05).
| Directive | Production | Terminator | Self-Terminating | Example | Verified |
|---|---|---|---|---|---|
| @if | ✅ §4.4.1 | @endif | No | §7.2 | ✅ |
| @elseif | ✅ §4.4.1 (part of @if) | @endif | No | §7.2 | ✅ |
| @else | ✅ §4.4.1 (part of @if) | @endif | No | §7.2 | ✅ |
| @foreach | ✅ §4.4.2 | @endforeach | No | §7.3, 7.4 | ✅ |
| @for | ✅ §4.4.2 | @endfor | No | §7.5 | ✅ |
| @while | ✅ §4.4.2 | @endwhile | No | §7.6 | ✅ |
| @switch | ✅ §4.4.3 | @endswitch | No | §7.7 | ✅ |
| @case | ✅ §4.4.3 (part of @switch) | @endswitch or @break | No | §7.7 | ✅ |
| @default | ✅ §4.4.3 (part of @switch) | @endswitch | No | §7.7 | ✅ |
| @break | ✅ §4.4.4 | Self-terminating | Yes | §7.14 | ✅ |
| @continue | ✅ §4.4.4 | Self-terminating | Yes | §7.14 | ✅ |
| @persist | ✅ §4.4.5 | Self-terminating | Yes | §7.9 | ✅ |
| @global | ✅ §4.4.6 | Self-terminating | Yes | §7.12 | ✅ |
| @include | ✅ §4.4.7 | Self-terminating | Yes | §7.13 | ✅ |
| @php | ✅ §4.4.8 | @endphp | No | §7.11 | ✅ |
| ~setup | ✅ §4.5 | Closing ~setup | No | §7.8 | ✅ |
Result: 16/16 directives verified. All have complete productions, termination rules, and examples.
No grammar parsing tool (ANTLR, Bison, Yacc, EBNF validator, or BNFC) is installed in this environment. Validation was performed through systematic manual review:
Reproducible validation steps (manual):
# Step 1: Extract all nonterminals from §4
grep -oE '\[[A-Za-z]+\]' NEOSPX-EBNF-GRAMMAR-SPECIFICATION.md | sort -u
# Step 2: Extract all terminals from §3.2
grep -oE 'T_[A-Z_]+' NEOSPX-EBNF-GRAMMAR-SPECIFICATION.md | sort -u
# Step 3: Verify every nonterminal in productions is defined in §4.0
# (Manual: compare §4.0 table against §4.1-§4.7 usage)
# Step 4: Verify every example against productions
# (Manual: trace each example's tokens to production rules)
# Step 5: Check for undefined references
# (Manual: search for nonterminal names not in §4.0 table)
If a grammar tool becomes available, the following should be validated:
ADR-005 (Accepted, 2026-08-04) ratified the engine choice (SuperPHP over Blade/Twig/Plates). It has no identity/naming section. Creating ADR-023 is recommended to establish NeoSPX as the canonical identity without amending the ratified ADR-005.
No ADR has been created, amended, or published. The Tech Lead must approve ADR-023 (or an amendment to ADR-005) before the grammar specification is published as authoritative.
~setup dual delimiter documented in §4.5 and §5.3. Lexer state determines role. Unterminated block produces lexical error.@foreach (PhpExpression 'as' ForeachTarget) where ForeachTarget ::= PhpVariable | PhpVariable '=>' PhpVariable. Documented in §4.4.2 with examples 7.3 and 7.4.s:namespace:component (colons). Corrected from IDENT { '/' IDENT } to IDENT { ':' IDENT }. Documented in §4.3 and §5.7.title="$item->title" (PHP expression). Invalid example 7.22 added showing interpolation is rejected.End of grammar specification. This is a proposal awaiting Tech Lead approval. No implementation has been started. No code, ADR, frozen contract, or governance file has been modified. All changes remain local and uncommitted.
comments (0)