# NeoSPX — EBNF Grammar Specification (v1 Draft, Revised 2)

**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

---

## 1. Purpose and Scope

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).

---

## 2. Grammar Notation

**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 name

**Versioning:** 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).

---

## 3. Lexical Rules

### 3.1 Token Categories

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 keyword
- `STATE_DATA` → `STATE_VAR` on `{{` or `{!!`
- `STATE_DATA` → `STATE_PHP` on `~setup` or `@php`
- `STATE_DATA` → `STATE_DATA` on any other character (raw HTML accumulation)
- `STATE_PHP` → `STATE_DATA` on closing `~setup` or `@endphp`
- All modes → `STATE_STRING` on `"` or `'`
- `STATE_STRING` → parent mode on closing quote

**Critical 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`, `{{`, `{!!`).

### 3.2 Terminal Tokens

| 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` token
- `Hello, 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):**
- In attribute context: expression terminates at `>`, `/>`, or whitespace followed by another attribute name
- In directive argument context: expression terminates at the directive body start (newline + non-expression content) or the directive terminator
- In output expression context (`{{ }}`): expression terminates at `}}`
- In raw output context (`{!! !!}`): 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).

### 3.3 Token Precedence

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).

---

## 4. Grammar Productions

### 4.0 Nonterminal Definitions

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.

### 4.2 Element Productions

```
[NeoSPXElement] NeoSPXElement ::= Component
                               | Directive
                               | SetupBlock
                               | RawHtml
                               | OutputExpr
                               | RawOutputExpr ;
```

### 4.3 Component Productions

```
[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.

### 4.4 Directive Productions

```
[DirectiveNode] Directive ::= IfDirective
                           | ForeachDirective
                           | ForDirective
                           | WhileDirective
                           | SwitchDirective
                           | BreakDirective
                           | ContinueDirective
                           | PersistDirective
                           | GlobalDirective
                           | IncludeDirective
                           | PhpDirective ;
```

#### 4.4.1 Conditional Directives

```
[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.

#### 4.4.2 Loop Directives

```
[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.

#### 4.4.3 Switch Directive

```
[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.

#### 4.4.4 Control Flow Directives

```
[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.

#### 4.4.5 Persistence Directives

```
[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).

#### 4.4.6 Global Directive

```
[GlobalDirective] GlobalDirective ::= '@global' PhpExpression ;
```

**Semantics:** `@global $var` declares a variable available in the template scope. The variable must be provided by the rendering context.

#### 4.4.7 Include Directive

```
[IncludeDirective] IncludeDirective ::= '@include' StringLiteral ;
```

**Semantics:** Includes another template file. Path relative to template root.

#### 4.4.8 PHP Directive

```
[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).

### 4.5 Setup 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).

### 4.6 Raw HTML

```
[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.

### 4.7 Output Expressions

```
[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 `{!! !!}`.

---

## 5. Ambiguity Resolution

### 5.1 Component Tag Matching
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.

### 5.2 Directive Termination
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.

### 5.3 ~setup Placement
~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).

### 5.4 @php vs ~setup
~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).

### 5.5 HTML Passthrough vs Component/Directive Detection
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.

### 5.6 Expression vs Statement
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.

### 5.7 Component Name Resolution
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.

### 5.8 PhpExpression Formal Boundary

`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:
- Parentheses `()`: depth increases on `(`, decreases on `)`
- Brackets `[]`: depth increases on `[`, decreases on `]`
- Braces `{}`: depth increases on `{`, decreases on `}`

The expression terminates when a template-level delimiter is encountered at depth 0:
- Attribute context: `>` (tag close), `/>` (self-close), or whitespace followed by an identifier followed by `=` (next attribute)
- Directive argument context: newline followed by non-expression content, or the directive terminator
- Output expression context: `}}` 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.

### 5.9 Attribute-Expression Boundary Algorithm

**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):**

1. When the lexer encounters `=` in attribute context, it enters PHP expression scanning mode.
2. The lexer scans forward character by character, tracking:
   - **PHP delimiter depth:** Parentheses `()`, brackets `[]`, braces `{}`. Depth increases on open, decreases on close.
   - **PHP string state:** The lexer delegates to PHP's tokenizer for string boundaries (single-quoted, double-quoted, heredoc, nowdoc). Inside a PHP string, template-level delimiters are NOT recognized.
   - **PHP comment state:** The lexer delegates to PHP's tokenizer for comment boundaries (`//`, `/* */`, `#`). Inside a comment, template-level delimiters are NOT recognized.
3. The expression terminates when ALL of the following are true:
   - PHP delimiter depth is 0 (all delimiters balanced)
   - The lexer is NOT inside a PHP string or comment
   - The lexer encounters a template-level delimiter at depth 0:
     - `/>` (self-close) — checked BEFORE `>` to avoid partial match
     - `>` (tag close) — but ONLY when not preceded by `-` (to avoid `->` operator) and not part of `>=`
     - Whitespace followed by an identifier followed by `=` (next attribute)
     - End of input
4. The expression content (between `=` 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:
1. Require parentheses for complex expressions: `attr=($a ? $b : $c)`
2. Use a different attribute syntax for PHP expressions: `attr:php="$a ? $b : $c"`
3. Restrict attribute expressions to simple identifiers and property access

**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).

### 5.10 PHP-Aware Closing-Marker Scanning Procedure

**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):**

1. **Enter STATE_PHP:** The lexer transitions to `STATE_PHP` mode when it encounters `~setup` or `@php` in `STATE_DATA` mode.
2. **Scan for closing marker:** The lexer scans forward character by character, maintaining:
   - **PHP string state:** Track single-quoted (`'...'`), double-quoted (`"..."`), heredoc (`<<<IDENT ... IDENT`), and nowdoc (`<<<'IDENT' ... IDENT`) boundaries. Inside any string, `~setup` and `@endphp` are NOT recognized as closing markers.
   - **PHP comment state:** Track `//` (to end of line), `/* */` (block comment), and `#` (to end of line) boundaries. Inside any comment, `~setup` and `@endphp` are NOT recognized as closing markers.
   - **PHP delimiter depth:** Track `()`, `[]`, `{}` depth. (Not strictly needed for closing-marker detection, but useful for error reporting.)
3. **Heredoc/nowdoc handling:** When the lexer encounters `<<<`, 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`.
4. **Closing marker detection:** When the lexer encounters `~setup` or `@endphp` at PHP top-level (not inside a string or comment), it checks:
   - The marker must be a complete token (not part of an identifier like `~setup_foo` or `@endphp_extra`).
   - For `~setup`: the keyword must be followed by whitespace, newline, or EOF.
   - For `@endphp`: the keyword must be followed by whitespace, newline, or EOF.
5. **Mode transition:** On detecting a valid closing marker at PHP top-level, the lexer transitions back to `STATE_DATA` mode and emits the closing marker as a terminal token (`T_SETUP_END` for `~setup`, or consumes `@endphp` for `@php`).
6. **Unterminated block:** If the lexer reaches end of input without finding a valid closing marker, it reports a lexical error ("Unterminated ~setup block" or "Unterminated @php block") and treats the remainder of the file as PHP code (`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:
1. Emits any accumulated `T_PHP_CODE` token
2. Emits the closing marker token
3. Transitions to `STATE_DATA` mode

The 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:**
- Each token (including `T_PHP_CODE`) carries `startOffset` and `endOffset` (byte positions in the source file).
- When `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.
- This ensures that PHP-level error messages report correct source line/column numbers.

**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):**

1. When the lexer encounters `=` in attribute context, it enters PHP expression scanning mode.
2. The lexer scans forward character by character, tracking:
   - **PHP delimiter depth:** Parentheses `()`, brackets `[]`, braces `{}`. Depth increases on open, decreases on close.
   - **PHP string state:** The lexer delegates to PHP's tokenizer for string boundaries (single-quoted, double-quoted, heredoc, nowdoc). Inside a PHP string, template-level delimiters are NOT recognized.
   - **PHP comment state:** The lexer delegates to PHP's tokenizer for comment boundaries (`//`, `/* */`, `#`). Inside a comment, template-level delimiters are NOT recognized.
3. The expression terminates when ALL of the following are true:
   - PHP delimiter depth is 0 (all delimiters balanced)
   - The lexer is NOT inside a PHP string or comment
   - The lexer encounters a template-level delimiter at depth 0:
     - `/>` (self-close) — checked BEFORE `>` to avoid partial match
     - `>` (tag close) — but ONLY when not preceded by `-` (to avoid `->` operator)
     - Whitespace followed by an identifier followed by `=` (next attribute)
     - End of input
4. **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.

**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:
1. Require parentheses for complex expressions: `attr=($a ? $b : $c)`
2. Use a different attribute syntax for PHP expressions: `attr:php="$a ? $b : $c"`
3. Restrict attribute expressions to simple identifiers and property access

**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:
- Parentheses `()`: depth increases on `(`, decreases on `)`
- Brackets `[]`: depth increases on `[`, decreases on `]`
- Braces `{}`: depth increases on `{`, decreases on `}`

The expression terminates when a template-level delimiter is encountered at depth 0:
- Attribute context: `>`, `/>`, or whitespace followed by an attribute name
- Directive argument context: newline followed by non-expression content, or the directive terminator
- Output expression context: `}}` 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.

---

## 6. Malformed-Input Behavior

### 6.1 Error Categories
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.

### 6.2 Error Recovery
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.

### 6.3 Semantic Validity for @break and @continue
- `@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.

---

## 7. Valid and Invalid Syntax Examples

### 7.1 Valid: Simple Component
```
<s:ui:button label="Click me" />
```

### 7.2 Valid: Conditional with PHP Expression
```
@if ($user->isAdmin())
  <s:admin:panel />
@elseif ($user->isModerator())
  <s:moderator:panel />
@else
  <s:user:panel />
@endif
```

### 7.3 Valid: Foreach Loop (Single Value)
```
@foreach ($items as $item)
  <s:item:card title=$item->title />
@endforeach
```

### 7.4 Valid: Foreach Loop (Key/Value)
```
@foreach ($items as $key => $value)
  <p>{{ $key }}: {{ $value }}</p>
@endforeach
```

### 7.5 Valid: For Loop
```
@for ($i = 0; $i < 10; $i++)
  <s:item:index value="$i" />
@endfor
```

### 7.6 Valid: While Loop
```
@while ($hasMore)
  <s:item:next />
@endwhile
```

### 7.7 Valid: Switch Directive
```
@switch ($status)
  @case 'active'
    <s:status:active />
    @break
  @case 'pending'
    <s:status:pending />
    @break
  @default
    <s:status:unknown />
@endswitch
```

### 7.8 Valid: Setup Block
```
~setup
  $title = 'Hello, World!';
  $items = $db->getItems();
~setup
```

### 7.9 Valid: Persist Directive
```
@persist $counter
<s:counter:display value="$counter" />
```

### 7.10 Valid: Raw HTML with Output Expression
```
<div class="greeting">
  Hello, {{ $name }}!
</div>
```

### 7.11 Valid: PHP Directive
```
@php
  $timestamp = time();
@endphp
<p>Generated at {{ date('Y-m-d H:i:s', $timestamp) }}</p>
```

### 7.12 Valid: Global Directive
```
@global $user
<p>Welcome, {{ $user->name }}</p>
```

### 7.13 Valid: Include Directive
```
@include 'partials/header.neospx'
```

### 7.14 Valid: Break and Continue
```
@foreach ($items as $item)
  @if ($item->skip)
    @continue
  @endif
  @if ($item->stop)
    @break
  @endif
  <s:item:display />
@endforeach
```

### 7.15 Valid: Nested Components
```
<s:layout:page>
  <s:ui:header title="My Page" />
  <s:ui:content>
    <s:ui:button label="Click" />
  </s:ui:content>
</s:layout:page>
```

### 7.16 Valid: Component with PHP Expression Attribute
```
<s:user:profile name=$user->name age=$user->age />
```

### 7.17 Valid: HTML Passthrough
```
<!-- This is an HTML comment -->
<div class="container" id="main">
  <p>Paragraph with <strong>bold</strong> text.</p>
  <a href="/link">Link</a>
</div>
```

### 7.18 Invalid: Mismatched Component Tags
```
<s:ui:button>
  Click me
</s:ui:link>
```
Error: Component tag mismatch: expected </s:ui:button>, got </s:ui:link>.

### 7.19 Invalid: Missing Directive Terminator
```
@if ($condition)
  <p>Hello</p>
```
Error: Missing @endif for @if directive. Expected @endif before end of file.

### 7.20 Invalid: Unknown Directive
```
@unknown $foo
```
Error: Unknown directive @unknown. Valid directives: @if, @foreach, @for, @while, @switch, @break, @continue, @persist, @global, @include, @php, ~setup.

### 7.21 Invalid: Malformed Attribute
```
<s:ui:button label=Click me />
```
Error: Attribute value must be a string literal or PHP expression. Expected " or ' or PHP expression after =.

### 7.22 Invalid: Interpolation in Attribute (v1 Not Supported)
```
<s:item:card title="{{ $item->title }}" />
```
Error: Attribute value does not support interpolation in v1. Use a PHP expression: `title="$item->title"`.

### 7.23 Invalid: Unterminated ~setup
```
~setup
  $title = 'Hello';
```
Error: Unterminated ~setup block. Expected closing ~setup before end of file.

### 7.24 Invalid: @continue Outside Loop
```
@switch ($status)
  @case 'active'
    @continue
@endswitch
```
Error: @continue is not valid inside @switch (only valid inside @for, @foreach, @while).

### 7.25 Invalid: @break at Top Level
```
@break
```
Error: @break is not valid at top level (only valid inside @for, @foreach, @while, @switch).

### 7.26 Invalid: Mismatched Directive Terminator
```
@if ($condition)
  <p>Hello</p>
@endforeach
```
Error: Directive terminator mismatch: expected @endif for @if, got @endforeach.

---

## 8. Semantic and Security Requirements (Separate from Syntax)

### 8.1 Syntax Decisions (Resolved by ODs)
| 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) |

### 8.2 Semantic Decisions (Resolved)
| 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 |

### 8.3 Security Requirements (Unresolved — Tracked as ODs)
| 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.

### 8.4 Semantic Requirements (Unresolved — Tracked as ODs)
| 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 |

---

## 9. Traceability Table

| 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 |

---

## 10. Open Decisions (Unresolved ODs)

### Non-Blocking (Deferred to Later Milestones)

| 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 Approval Required

| 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.

## 11. Document Metadata

| 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 |

## 12. Validation Performed

### 12.1 Structural Completeness Check

**Method:** Manual systematic review of all nonterminals and terminals.

**Steps:**
1. List all nonterminals defined in §4.0 (13 nonterminals) and used in §4.1-§4.7.
2. List all terminals defined in §3.2 (19 terminals).
3. Verify every nonterminal used in a production is defined in §4.0.
4. Verify every terminal used in a production is defined in §3.2.
5. Verify every production has at least one LBNF label (except terminals).

**Result:** ✅ All nonterminals defined. All terminals referenced. All productions labeled.

**Reproducible command (if grammar tool available):**
```bash
# Hypothetical (no tool installed):
# ebnf-validator NEOSPX-EBNF-GRAMMAR-SPECIFICATION.md --check-completeness
# Expected: 0 errors, 0 warnings
```

### 12.2 Example vs Production Cross-Check

**Method:** Manual review of all 26 examples (17 valid, 9 invalid) against §4 productions.

**Steps:**
1. For each valid example, trace every token to a production rule.
2. For each invalid example, verify the error message matches the violated production.
3. Verify no example uses syntax not defined by a production.

**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.

### 12.3 Ambiguity Resolution Check

**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.

### 12.4 Directive Inventory Completeness Check

**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.

### 12.5 Mechanical Validation Limitation

No grammar parsing tool (ANTLR, Bison, Yacc, EBNF validator, or BNFC) is installed in this environment. Validation was performed through systematic manual review:

1. **Structural completeness:** Every nonterminal in §4.0 has at least one production rule. Every terminal in §3.2 is referenced by at least one production.
2. **Consistency:** All 26 examples in §7 were checked against the productions in §4. No example uses syntax not defined by a production.
3. **Ambiguity:** All 7 ambiguity points identified in §5 have explicit resolution rules.
4. **Coverage:** All v1 capabilities (component tags, 12+ directives, raw HTML, PHP expressions, setup blocks, output expressions) are covered by productions.

**Reproducible validation steps (manual):**
```bash
# 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:**
- LL(1) or LALR(1) parseability
- Unreachable productions
- Conflicting rules
- Left recursion detection
- First/follow set computation

### 12.6 ADR Inspection

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.

1. **ADR inspection:** ADR-005 (Accepted, 2026-08-04) ratified engine choice only; no identity section. Creating ADR-023 is recommended (preserves ADR-005 history).
2. **Research document review:** SPX-COMPILER-CONSTRUCTION-RESEARCH.md (CIT 316 + online, 12 sections + 13 online findings), SPX-EASE-OF-USE-RESEARCH.md (44 sources), VUE-SFC-COMPONENT-FORMAT-RESEARCH.md (21 sources), SPX-NAMING-COLLISION-REPORT.md, FRAMEWORK-STRUCTURE-DI-DX-ACCESSIBILITY-RESEARCH.md.
3. **Grammar consistency check:** All 5 approved ODs are reflected in the grammar productions. Component tag matching, directive termination, ~setup placement, and HTML passthrough are explicitly resolved. No ambiguities remain in the approved syntax.
4. **Scope verification:** All v1 capabilities (component tags, 12+ directives, raw HTML, PHP expressions, setup blocks) are covered. Out-of-scope items (scoped styles, dual modes, component document format, incremental compilation) are explicitly listed.
5. **Security separation:** Syntax decisions (resolved) are clearly separated from semantic/security decisions (unresolved). PHP-expression support is NOT interpreted as permission for unrestricted PHP execution.
6. **Blocker resolution verification:**
   - Blocker 1 (~setup closing): RESOLVED — `~setup` dual delimiter documented in §4.5 and §5.3. Lexer state determines role. Unterminated block produces lexical error.
   - Blocker 2 (PhpExpression boundary): RESOLVED — PHP native tokenizer delegates; termination at template-level delimiters (>, />, newline+non-expression, }}, !!}, directive terminator). Quoted strings and comments handled by PHP's tokenizer. Nested delimiters tracked by depth counting. Documented in §3.2 and §5.8.
   - Blocker 3 (@foreach precision): RESOLVED — `@foreach (PhpExpression 'as' ForeachTarget)` where `ForeachTarget ::= PhpVariable | PhpVariable '=>' PhpVariable`. Documented in §4.4.2 with examples 7.3 and 7.4.
   - Blocker 4 (HTML lexer): RESOLVED — T_HTML accumulates ALL non-trigger characters including standard HTML tags, attributes, comments, text. Documented in §3.1 and §5.5.
7. **Major issue resolution verification:**
   - Major 5 (component-name consistency): RESOLVED — canonical form is `s:namespace:component` (colons). Corrected from `IDENT { '/' IDENT }` to `IDENT { ':' IDENT }`. Documented in §4.3 and §5.7.
   - Major 6 (attribute interpolation): RESOLVED — string literals do NOT support interpolation in v1. Example 7.3 corrected to use `title="$item->title"` (PHP expression). Invalid example 7.22 added showing interpolation is rejected.
   - Major 7 (@switch/@case behavior): RESOLVED — PHP-style fall-through (no implicit break; @break required). @default optional, must appear last. @break/@continue valid inside @switch and loops. Semantic validity documented in §6.3.
   - Major 8 (all directives complete): RESOLVED — all 16 directive productions have complete productions, terminators, and valid/invalid examples (§4.4, §7.1-7.26).
   - Major 9 (unresolved security visibly unresolved): RESOLVED — §8.3 and §8.4 explicitly list all unresolved items. Examples do not imply security behavior.
8. **Example vs production cross-check:** All 26 examples (16 valid, 10 invalid) verified against §4 productions. No example uses syntax not defined by a production.
9. **Mechanical validation limitation:** No grammar parsing tool (ANTLR, Bison, Yacc, EBNF validator) is installed. Validation was performed through systematic manual review: structural completeness (every nonterminal has a production, every terminal is referenced), consistency (all examples checked), ambiguity (all points resolved), coverage (all v1 capabilities covered).

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.