Files
rdbms-playground/src/dsl/walker/context.rs
T
claude@clouddev1 4f89106a63 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.
2026-05-20 11:34:53 +00:00

222 lines
9.1 KiB
Rust

//! `WalkContext` — per-walk mutable state that flows through the
//! walker (ADR-0024 §WalkContext, §Phase D).
//!
//! Phase D plumbed a schema reference through the context so
//! schema-aware nodes (`Ident { source: Tables }` writing
//! `current_table`, `DynamicSubgrammar` reading
//! `current_table_columns`) can resolve real entities at walk
//! time. Pre-Phase-D `default()` callers (tests, the chumsky-
//! era `parse_command(input)` signature) still work — the
//! schema slot is `None` and dynamic dispatch falls back to a
//! generic value-literal slot.
use crate::completion::{SchemaCache, TableColumn};
use crate::dsl::types::Type;
use crate::mode::Mode;
/// A single `FROM`-source binding in the active lexical scope
/// (ADR-0032 §10.1). One binding per `FROM` table or `JOIN`
/// target, populated as the walker descends through
/// `from_clause` / `join_clause`.
#[derive(Debug, Clone)]
pub struct TableBinding {
/// The table name as the user typed it (case-preserving
/// per ADR-0009).
pub table: String,
/// The optional `AS` alias or bare alias (ADR-0032 §1).
pub alias: Option<String>,
/// The schema-resolved columns for the table. Empty if the
/// schema did not know the table (the unknown-table
/// diagnostic will fire in 2d).
pub columns: Vec<TableColumn>,
}
/// A CTE definition visible from inside its own body
/// (`WITH RECURSIVE` self-reference) and from the outer scope
/// after the body completes (ADR-0032 §10.3).
#[derive(Debug, Clone)]
pub struct CteBinding {
pub name: String,
pub columns: Vec<CteColumn>,
}
/// One output column derived from a CTE body's projection
/// list per ADR-0032 §10.3's derivation rules.
#[derive(Debug, Clone)]
pub struct CteColumn {
/// `None` for computed projections without an alias —
/// the engine assigns an implementation-defined name and
/// the slot is silently skipped from the qualified-prefix
/// candidate list (ADR-0032 §10.3).
pub name: Option<String>,
/// The resolved playground type if the body's projection
/// yields one (a single column reference). `None` for
/// computed columns and recursive CTE bodies (per ADR-0032
/// Amendment 1's empirical findings).
pub type_: Option<Type>,
}
/// One lexical scope on the walker's `from_scope_stack`.
///
/// Pushed on entry to a `Node::ScopedSubgrammar` and popped on
/// exit (ADR-0032 §10.2). The bottom of the stack is the
/// implicit top-level scope DSL paths and top-level SQL
/// statements operate in.
#[derive(Debug, Default, Clone)]
pub struct ScopeFrame {
/// In-scope FROM-source bindings for this frame. Populated
/// by `from_clause` / `join_clause` walks.
pub from_scope: Vec<TableBinding>,
/// CTE definitions visible in this frame. Populated by
/// `with_clause` walks before each CTE's body; the body's
/// output columns are harvested into the placeholder
/// binding at the body's frame exit (§10.3).
pub cte_bindings: Vec<CteBinding>,
/// Projection-list aliases observed in this frame.
/// `ORDER BY` slots offer these as additional candidates
/// per ADR-0032 §10.4.
pub projection_aliases: Vec<String>,
}
/// Per-walk state.
///
/// Carries an optional schema reference (so callers without a
/// schema continue to work) plus mutable accumulators that
/// nodes can write to during the walk:
///
/// - `current_table` / `current_table_columns` — populated when
/// an `Ident { source: Tables }` node with `writes_table:
/// true` matches a known table.
/// - `current_column` — populated by `Ident { source: Columns
/// writes_column: true }` for `set col = …` / `where col =
/// …` slots so the next value-slot picks the column's typed
/// sub-grammar.
#[derive(Debug)]
pub struct WalkContext<'a> {
pub schema: Option<&'a SchemaCache>,
/// The input mode this walk runs under (ADR-0030 §2). In
/// `Mode::Simple` the walker gates out SQL-only commands —
/// an advanced-only entry word yields the "this is SQL"
/// hint rather than a normal parse. Defaults to
/// `Mode::Simple`; real call sites set it from the active
/// `App` mode.
pub mode: Mode,
pub current_table: Option<String>,
pub current_table_columns: Option<Vec<TableColumn>>,
pub current_column: Option<TableColumn>,
/// The column type the walker is *about* to consume a value
/// for (ADR-0024 §Phase D §typed-value-slots). Set by the
/// walker on entry to a `Node::TypedValueSlot`, cleared on
/// successful inner match. The hint resolver reads this to
/// emit per-type prose ("Type an integer", "Type a date as
/// 'YYYY-MM-DD'", …) at empty prefix at typed value slots.
pub pending_value_type: Option<crate::dsl::types::Type>,
/// The column name (if known) the walker is about to
/// consume a value for.
///
/// Populated by:
/// - `Ident { source: Columns, writes_column: true }` for
/// `update set <col>=` and `where <col>=` positions, where
/// the column ident matches in the path immediately
/// before the value slot.
/// - `Node::TypedValueSlot { column_name: Some(name), … }`
/// for the per-column typed slots in `column_value_list`
/// (insert-into-T-values positions, where the column name
/// is keyed by position in the table's column list).
///
/// Cleared on successful inner match alongside
/// `pending_value_type`.
pub pending_value_column: Option<String>,
/// The hint-panel `HintMode` declared by the grammar node
/// the walker is currently inside (ADR-0024
/// §HintMode-per-node). Set on entry to a `Node::Hinted`
/// wrapper, cleared on successful inner match. The hint
/// resolver reads this directly instead of inferring the
/// slot kind from the shape of the expected set.
pub pending_hint_mode: Option<crate::dsl::grammar::HintMode>,
/// The columns the user explicitly listed in
/// `insert into <T> (col1, col2, …) values (…)` (Form A),
/// in declaration order.
///
/// Populated as each ident-shape token in the leading paren
/// matches an `Ident` node with `writes_user_listed_column:
/// true`. `None` (default) means no explicit list was
/// observed — the inner `values (…)` slot list then
/// defaults to "every non-auto-generated column of the
/// current table" (Form B `insert into T values (…)`
/// behavior; ADR-0018 §3 — auto-generated columns are
/// skipped from the value list because the dispatch path
/// auto-fills them).
pub user_listed_columns: Option<Vec<String>>,
/// Count of active `Node::Subgrammar` frames on the walk
/// stack (ADR-0026 §2). The walker increments on entry to a
/// `Subgrammar`, restores the saved value on exit, and
/// refuses past `driver::MAX_SUBGRAMMAR_DEPTH` so a
/// pathologically nested expression fails with a friendly
/// error instead of overflowing the process stack.
/// `Node::ScopedSubgrammar` shares the same counter
/// uniformly (ADR-0032 §9).
pub subgrammar_depth: usize,
/// The stack of lexical scope frames (ADR-0032 §10.2).
/// The bottom frame is the implicit top-level scope DSL
/// paths and top-level SQL statements operate in;
/// `Node::ScopedSubgrammar` entries push and pop new frames
/// on top. Always non-empty: the bottom frame is created at
/// `WalkContext::new` / `with_schema` time and never popped.
pub from_scope_stack: Vec<ScopeFrame>,
}
impl<'a> WalkContext<'a> {
/// Schemaless walk context — the legacy default used by
/// pre-Phase-D callers and tests that don't care about
/// schema-aware narrowing. Carries a single empty
/// `ScopeFrame` on `from_scope_stack` (ADR-0032 §10.2).
#[must_use]
pub fn new() -> Self {
Self {
schema: None,
mode: Mode::Simple,
current_table: None,
current_table_columns: None,
current_column: None,
pending_value_type: None,
pending_value_column: None,
pending_hint_mode: None,
user_listed_columns: None,
subgrammar_depth: 0,
from_scope_stack: vec![ScopeFrame::default()],
}
}
/// Schema-aware walk context. Dynamic sub-grammars read
/// `schema` (via `current_table_columns`) to unfold typed
/// per-column value slots.
#[must_use]
pub fn with_schema(schema: &'a SchemaCache) -> Self {
Self {
schema: Some(schema),
mode: Mode::Simple,
current_table: None,
current_table_columns: None,
current_column: None,
pending_value_type: None,
pending_value_column: None,
pending_hint_mode: None,
user_listed_columns: None,
subgrammar_depth: 0,
from_scope_stack: vec![ScopeFrame::default()],
}
}
}
impl Default for WalkContext<'_> {
fn default() -> Self {
Self::new()
}
}
/// Convenience re-export so non-walker modules don't reach
/// across `completion::TableColumn` directly.
#[allow(dead_code)]
pub type ColumnInfo = TableColumn;