walker: Node::ScopedSubgrammar variant + scope-frame stack (ADR-0032 §10.2)

Sub-phase 2b checkpoint 1 — adds the foundation for SQL SELECT
lexical-scope discipline without changing existing walker
semantics.

New types in `dsl::walker::context`:

- `TableBinding` — one FROM-source binding with table name,
  optional alias, and schema-resolved columns (§10.1).
- `CteBinding` + `CteColumn` — a CTE definition visible from
  inside its body (WITH RECURSIVE self-reference) and from the
  outer scope after harvest (§10.3).
- `ScopeFrame` — `from_scope`, `cte_bindings`, and
  `projection_aliases` for one lexical scope. Default-empty;
  the fields will be populated by later 2b checkpoints.

`WalkContext` gains `from_scope_stack: Vec<ScopeFrame>`,
initialised with one bottom frame in both `new()` and
`with_schema()`. The bottom frame is the implicit top-level
scope DSL paths and top-level SQL statements operate in;
`Node::ScopedSubgrammar` entries push and pop additional frames
on top. `current_table` / `current_table_columns` remain as
direct fields for this checkpoint — converting them to derived
helpers is a later 2b step.

New grammar-tree variant:

- `Node::ScopedSubgrammar(&'static Self)` — like `Subgrammar`,
  but pushes a fresh `ScopeFrame` on entry and pops it on exit
  (ADR-0032 §10.2). Shares `subgrammar_depth` with the plain
  Subgrammar variant so the MAX_SUBGRAMMAR_DEPTH = 64 cap fires
  uniformly across both — §9's "no new walker capability for
  grammar recursion" claim holds. DSL Expr (ADR-0026) and
  sql_expr.rs ladder (ADR-0031) recursion continue to use the
  plain Subgrammar variant and never push a scope.

Driver gains a parallel `walk_scoped_subgrammar` arm; the
push/pop is unconditional so a speculatively-walked branch a
later Choice rolls back leaves the stack clean.

Test coverage in `driver.rs`:

- A recursive ScopedSubgrammar test grammar walks correctly
  through depths 0-3.
- The depth cap fires the same `expression_too_deep` friendly
  validation error as for plain Subgrammar.
- The bottom frame invariant: `WalkContext::new` seeds exactly
  one frame, and after a walk the stack is restored.

No grammar tree references the new variant yet — the rewire of
sql_select.rs CTE bodies and the sql_expr.rs additive
extensions for §5/§6 are the next 2b checkpoint. Test totals:
1330 baseline + 3 = 1333 passing, 0 failed, 1 ignored. Clippy
clean.
This commit is contained in:
claude@clouddev1
2026-05-20 11:34:53 +00:00
parent 8d293358a0
commit 4f89106a63
3 changed files with 236 additions and 4 deletions
+18
View File
@@ -316,6 +316,24 @@ pub enum Node {
/// this one references a fixed fragment already in the
/// grammar tree.
Subgrammar(&'static Self),
/// Like `Subgrammar`, but the walker additionally **pushes a
/// new `ScopeFrame`** onto `WalkContext::from_scope_stack` on
/// entry and pops it on exit (ADR-0032 §10.2). The
/// `subgrammar_depth` counter increments uniformly across
/// both variants — the depth cap applies the same way — so
/// this variant introduces no new walker capability for
/// grammar recursion; it only layers lexical-scope discipline
/// on top.
///
/// Used at every SQL `SELECT` recursion point: subqueries
/// in `sql_expr.rs` (scalar `(SELECT …)`, `IN (SELECT …)`,
/// `[NOT] EXISTS (SELECT …)`) and CTE bodies in
/// `sql_select.rs` reference the compound-SELECT through
/// `Node::ScopedSubgrammar(&SQL_SELECT_COMPOUND)`. DSL `Expr`
/// recursion (ADR-0026) and the `sql_expr.rs` precedence-
/// ladder recursion (ADR-0031) keep using the plain
/// `Subgrammar` variant and never push a scope.
ScopedSubgrammar(&'static Self),
/// Resolves at walk time using the active `WalkContext`.
/// Phase D+ uses this for `column_value_list`. The factory
/// is pure in `ctx`, so the walker memoizes the resolution