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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user