walker: 3a — category-grouped mode-aware dispatch (ADR-0033 Amendment 1)

Replaces ADR-0033 §2's original Node::Guard + Choice(SQL,DSL) mechanism,
which was found during 3a to be unworkable: any guard-in-Choice approach
forces a walk_choice change (walk_choice falls through only on NoMatch, so
simple-mode valid-DSL would wrongly surface "this is SQL"), and walk_seq
treats a NoMatch past idx 0 as a hard Failed, breaking advanced-mode DSL
fall-through.

Mechanism (Amendment 1): each REGISTRY entry is tagged
CommandCategory::{Simple, Advanced}, generalising the whole-command
is_advanced_only gate. walk() becomes a thin dispatcher over decide()
(mode-aware candidate selection: simple commits the DSL node or emits the
"this is SQL" hint; advanced tries SQL first, DSL as a full-line fallback)
and an extracted walk_one_command(); speculative match-testing runs on a
scratch WalkContext so the caller's context is only touched by the
committed walk. No Node::Guard, no walk_choice/walk_seq change.

6 dispatch smoke tests on a shared-entry-word smoke registry; 1446 baseline
green; clippy clean.
This commit is contained in:
claude@clouddev1
2026-05-21 18:18:50 +00:00
parent a37a0b7d40
commit 4e16d97fe0
5 changed files with 720 additions and 92 deletions
+146
View File
@@ -1030,6 +1030,152 @@ grammar on top.
- **No `EXPLAIN` of SQL DML** (ADR-0030 §13 OOS-2 — the DSL
`explain` still wraps what it already wraps).
## Amendment 1 — Dispatch mechanism: category-grouped dispatch supersedes `Node::Guard` (2026-05-21)
This amendment **supersedes §2's "Mode-gating mechanism"
subsection** and **revises sub-phase 3a's scope**. It was
written during 3a implementation, after tracing the proposed
mechanism against the actual walker code, and is recorded with
explicit user approval before any code landed.
### The finding — option (a) does not work as §2 frames it
§2 selected option (a): a standalone zero-byte `Node::Guard(fn)`
placed as the first element of the SQL branch's `Seq`, inside a
`Choice(SQL_shape, DSL_shape)`. §2 framed this as the "smallest
mechanism change" that "reuses the existing validator path" with
the failure surfacing as the same `WalkOutcome::ValidationFailed`
as the entry-word gate — implying **no `walk_choice` change**.
Tracing the four required behaviours against
`src/dsl/walker/driver.rs` shows that framing is inaccurate, and
that **any** guard-in-`Choice` mechanism is forced to modify
`walk_choice`:
1. **`walk_choice` returns immediately on a branch `Failed`**
(`driver.rs` `walk_choice`) — it only falls through to the
next branch on `NoMatch`. So in **Simple mode**,
`delete from t where id = 1` (which is *valid DSL*) would hit
the SQL branch's guard, the guard would fail, and the user
would see the "this is SQL" hint instead of the statement
running as DSL. That contradicts §2's own "Simple mode → DSL
only" promise. Fixing it requires a `walk_choice` change
regardless of how the guard node is shaped.
2. **`walk_seq` treats a `NoMatch` at `idx > 0` as a hard
`Failed`** (`driver.rs` `walk_seq`). A zero-byte Guard at
`idx 0` still advances `idx` to 1, pushing the SQL branch's
first real token to `idx 1`. So in **Advanced mode**,
`delete from t --all-rows` (DSL-only) cannot fall through to
DSL — the SQL branch returns `Failed`, and `walk_choice`
won't try the DSL branch. The literal form needs an
*additional* `walk_seq` semantics change (`idx == 0` → "no
bytes consumed yet").
This is exactly the R1 risk the ADR budgeted for ("if `walk_seq`
commits the Seq before the validator runs … the Choice can't
fall through").
### The replacement — category-grouped, mode-aware dispatch
Rather than gate a `Choice` branch from inside the recursive
grammar, the dispatch decision moves up to the command
dispatcher (`walker::walk`), which is **where mode gating
already lives**: the existing whole-command gate
(`is_advanced_only` + the Simple-mode short-circuit in `walk`)
is precisely this pattern, hardcoded for `select` / `with`. The
amendment generalises that existing code instead of inventing a
parallel in-grammar mechanism.
Concretely:
- **Each registry entry carries a category** —
`CommandCategory::{Simple, Advanced}`. The category lives at
the registration site (the `REGISTRY` table), not as a field
on every `CommandNode` literal, because category is a
dispatcher concern, not intrinsic to a command's grammar.
`select` / `with` (and, from 3b, the SQL `insert` / `update` /
`delete` nodes) are `Advanced`; everything else is `Simple`.
This subsumes `ADVANCED_ONLY_ENTRIES`: `is_advanced_only(entry)`
becomes "every registry candidate for this entry word is
`Advanced`".
- **A shared entry word has a node in both groups** — e.g. a
`Simple` DSL `insert` node and an `Advanced` SQL `insert`
node, both with entry word `insert`. The entry-word lookup
returns *all* candidates; the dispatcher selects by mode.
- **Mode-aware selection in `walk` (no combinator changes):**
- **Simple mode** considers `Simple` candidates. If a Simple
candidate exists, it is committed. If none exists but an
`Advanced` candidate does (a pure-SQL entry word like
`select`), the dispatcher emits the
`advanced_mode.sql_in_simple` `ValidationFailed` — identical
to today's whole-command gate.
- **Advanced mode** tries `Advanced` candidate(s) first, then
the `Simple` candidate as a fallback. The first candidate
that fully matches wins (`delete from t where id = 1` →
SQL; `delete from t --all-rows` → falls through to DSL
because the SQL shape doesn't match `--all-rows`).
- **No `Node::Guard`, no `walk_choice` / `walk_seq` change.** The
recursive combinators are untouched; each candidate is walked
cleanly and independently. Speculative evaluation runs on a
scratch `WalkContext`; only the committed candidate is walked
into the caller's context, so post-walk context state
(completion / hint accumulators) always reflects the chosen
candidate.
### The two details (user-approved)
1. **"This is SQL" hint in Simple mode for SQL-shaped input.**
For SQL-only input in Simple mode (e.g.
`delete … returning *`), the Simple candidate won't match.
To keep parity with `select` / `with`, the dispatcher
speculatively checks whether the `Advanced` candidate *would*
match; if so, it emits the `advanced_mode.sql_in_simple`
hint rather than a generic DSL parse error. Lives entirely
in `walk`.
2. **Advanced-mode completion is SQL-first, DSL as full-line
fallback.** The `Choice` approach would have unioned SQL and
DSL continuations mid-typing for free; the grouping approach
surfaces one candidate's walk for completion / hints. The
decision is to show SQL completions primarily in Advanced
mode and fall to DSL only on a full DSL-shaped line — simpler
and matching the "SQL-first, DSL as familiar fallback"
intent. No unioned completion.
### Consequences of the amendment
- **`Node::Guard` is NOT added.** §2's mechanism caveat,
Consequences bullet 1 ("the walker gains a new node variant
`Node::Guard`"), and the original 3a scope are withdrawn.
- **`REGISTRY`'s element type changes** to carry the category
alongside each `&CommandNode`. The handful of registry
iteration sites (entry-word listing, `command_for_entry_word`,
the completion entry-word filters, `note_help`) destructure the
tuple; no behavioural change for the current single-candidate
registry.
- **The `Command` variants (§10), worker handlers, RETURNING
flag, cascade-summary plan, shortid auto-fill, and diagnostics
are unaffected** — this amendment is purely about how the
dispatcher routes a shared entry word to the right grammar.
- **Sub-phase 3a now validates the dispatch mechanism**, not the
guard: a smoke registry with a shared entry word (a `Simple`
and an `Advanced` node using distinguishable tokens) exercises
the Simple/Advanced selection, the fallback, and the
"this is SQL" path. The 3a exit gate's four cases map onto the
new mechanism unchanged in spirit.
- **3b adds the first real shared entry words** to `REGISTRY`
(the SQL `insert` / `update` / `delete` nodes). At that point
the completion entry-word *lists* will need to de-duplicate
the shared primary (two candidates, one entry word) — tracked
for 3b, not 3a.
- The mechanism remains reusable for Phase 4 DDL's analogous
shared-entry problem (`create`, …): tag the SQL DDL nodes
`Advanced`; the dispatcher handles the rest.
## See also
- ADR-0005 — the ten-type vocabulary INSERT works with.