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:
+109
-50
@@ -410,6 +410,32 @@ pub enum Node {
|
||||
},
|
||||
}
|
||||
|
||||
/// Which mode group a registered command belongs to (ADR-0030
|
||||
/// §2, ADR-0033 Amendment 1).
|
||||
///
|
||||
/// Category is a *dispatcher* concern, not intrinsic to a
|
||||
/// command's grammar, so it is attached at the `REGISTRY`
|
||||
/// registration site rather than as a field on every
|
||||
/// `CommandNode`. The dispatcher (`walker::walk`) uses it to
|
||||
/// route a given input by the active input mode:
|
||||
///
|
||||
/// - `Simple` commands are the DSL surface; available in both
|
||||
/// simple and advanced mode.
|
||||
/// - `Advanced` commands are the SQL surface; available only in
|
||||
/// advanced mode. In simple mode an advanced-only entry word
|
||||
/// yields the "this is SQL" hint (`advanced_mode.sql_in_simple`).
|
||||
///
|
||||
/// A *shared* entry word (e.g. `insert`, from Phase 3 sub-phase
|
||||
/// 3b on) carries a node in *both* groups — a `Simple` DSL node
|
||||
/// and an `Advanced` SQL node. The dispatcher tries the SQL node
|
||||
/// first in advanced mode and falls back to the DSL node when the
|
||||
/// SQL shape does not match.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum CommandCategory {
|
||||
Simple,
|
||||
Advanced,
|
||||
}
|
||||
|
||||
/// Top-level entry record. One per command. The `entry` keyword
|
||||
/// alone identifies which command the walker dispatches to;
|
||||
/// `shape` is what follows the entry word.
|
||||
@@ -503,72 +529,105 @@ pub fn usage_key_for_input(source: &str) -> Option<&'static str> {
|
||||
/// which read the same data through the legacy `usage::REGISTRY`.
|
||||
#[must_use]
|
||||
pub fn entry_words_alphabetised() -> Vec<&'static str> {
|
||||
let mut words: Vec<&'static str> = REGISTRY.iter().map(|c| c.entry.primary).collect();
|
||||
let mut words: Vec<&'static str> =
|
||||
REGISTRY.iter().map(|(c, _)| c.entry.primary).collect();
|
||||
words.sort_unstable();
|
||||
words.dedup();
|
||||
words
|
||||
}
|
||||
|
||||
/// The active grammar registry. Phase A: the eleven app-lifecycle
|
||||
/// commands. Migrated commands route through this; everything
|
||||
/// else falls through to the chumsky path in `dsl::parser`.
|
||||
pub static REGISTRY: &[&CommandNode] = &[
|
||||
&app::QUIT,
|
||||
&app::HELP,
|
||||
&app::REBUILD,
|
||||
&app::SAVE,
|
||||
&app::NEW,
|
||||
&app::LOAD,
|
||||
&app::EXPORT,
|
||||
&app::IMPORT,
|
||||
&app::MODE,
|
||||
&app::MESSAGES,
|
||||
&ddl::DROP,
|
||||
&ddl::ADD,
|
||||
&ddl::RENAME,
|
||||
&ddl::CHANGE,
|
||||
&ddl::CREATE,
|
||||
&data::SHOW,
|
||||
&data::INSERT,
|
||||
&data::UPDATE,
|
||||
&data::DELETE,
|
||||
&data::REPLAY,
|
||||
&data::EXPLAIN,
|
||||
&data::SELECT,
|
||||
&data::WITH,
|
||||
/// The active grammar registry, each command paired with its
|
||||
/// dispatch [`CommandCategory`] (ADR-0033 Amendment 1).
|
||||
///
|
||||
/// Migrated commands route through this; everything else falls
|
||||
/// through to the chumsky path in `dsl::parser`. `Advanced`
|
||||
/// commands (`select`, `with`, and — from sub-phase 3b — the SQL
|
||||
/// `insert` / `update` / `delete` nodes) are the SQL surface;
|
||||
/// the rest are the DSL surface (`Simple`). A shared entry word
|
||||
/// will appear twice (one `Simple`, one `Advanced` node); the
|
||||
/// dispatcher selects by mode.
|
||||
pub static REGISTRY: &[(&CommandNode, CommandCategory)] = &[
|
||||
(&app::QUIT, CommandCategory::Simple),
|
||||
(&app::HELP, CommandCategory::Simple),
|
||||
(&app::REBUILD, CommandCategory::Simple),
|
||||
(&app::SAVE, CommandCategory::Simple),
|
||||
(&app::NEW, CommandCategory::Simple),
|
||||
(&app::LOAD, CommandCategory::Simple),
|
||||
(&app::EXPORT, CommandCategory::Simple),
|
||||
(&app::IMPORT, CommandCategory::Simple),
|
||||
(&app::MODE, CommandCategory::Simple),
|
||||
(&app::MESSAGES, CommandCategory::Simple),
|
||||
(&ddl::DROP, CommandCategory::Simple),
|
||||
(&ddl::ADD, CommandCategory::Simple),
|
||||
(&ddl::RENAME, CommandCategory::Simple),
|
||||
(&ddl::CHANGE, CommandCategory::Simple),
|
||||
(&ddl::CREATE, CommandCategory::Simple),
|
||||
(&data::SHOW, CommandCategory::Simple),
|
||||
(&data::INSERT, CommandCategory::Simple),
|
||||
(&data::UPDATE, CommandCategory::Simple),
|
||||
(&data::DELETE, CommandCategory::Simple),
|
||||
(&data::REPLAY, CommandCategory::Simple),
|
||||
(&data::EXPLAIN, CommandCategory::Simple),
|
||||
(&data::SELECT, CommandCategory::Advanced),
|
||||
(&data::WITH, CommandCategory::Advanced),
|
||||
];
|
||||
|
||||
/// Entry words for commands available only in advanced mode
|
||||
/// (ADR-0030 §2). Phase 1: `select`. In simple mode the walker
|
||||
/// gates these out — typing one yields the precise "this is SQL"
|
||||
/// hint rather than a normal parse or an "unknown command" error.
|
||||
///
|
||||
/// This is whole-command gating keyed on the entry word, which
|
||||
/// suffices while every SQL form is its own command. ADR-0030 §2's
|
||||
/// finer-grained per-`Choice`-branch tagging arrives with the
|
||||
/// shared DSL/SQL entry words (`create`, `insert`, …) in a later
|
||||
/// phase.
|
||||
const ADVANCED_ONLY_ENTRIES: &[&str] = &["select", "with"];
|
||||
|
||||
/// Whether `entry` names an advanced-mode-only command (ADR-0030
|
||||
/// §2). Case-insensitive, matching keyword-matching elsewhere.
|
||||
/// §2, ADR-0033 Amendment 1). Case-insensitive, matching
|
||||
/// keyword-matching elsewhere.
|
||||
///
|
||||
/// True when the entry word is registered and *every* candidate
|
||||
/// for it is `Advanced` — i.e. there is no DSL (`Simple`) command
|
||||
/// to fall back to. A shared entry word (a Simple DSL node plus
|
||||
/// an Advanced SQL node) is therefore *not* advanced-only: it is
|
||||
/// available in simple mode as DSL.
|
||||
#[must_use]
|
||||
pub fn is_advanced_only(entry: &str) -> bool {
|
||||
ADVANCED_ONLY_ENTRIES
|
||||
.iter()
|
||||
.any(|e| e.eq_ignore_ascii_case(entry))
|
||||
let mut found = false;
|
||||
for (c, category) in REGISTRY {
|
||||
if c.entry.matches(entry) {
|
||||
found = true;
|
||||
if *category == CommandCategory::Simple {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
found
|
||||
}
|
||||
|
||||
/// Look up a `CommandNode` by entry word, case-insensitively.
|
||||
/// Look up the first `CommandNode` registered for an entry word,
|
||||
/// case-insensitively. Returns the index into `REGISTRY` so
|
||||
/// callers can use it as a `WalkOutcome::Match { command_idx }`.
|
||||
///
|
||||
/// Used by the router to decide whether the walker owns this
|
||||
/// input. Returns the index into `REGISTRY` so callers can
|
||||
/// later use it as a `WalkOutcome::Match { command_idx }`.
|
||||
/// For shared entry words this returns whichever node is listed
|
||||
/// first in `REGISTRY`; callers that must distinguish the Simple
|
||||
/// from the Advanced candidate use [`commands_for_entry_word`].
|
||||
pub fn command_for_entry_word(word: &str) -> Option<(usize, &'static CommandNode)> {
|
||||
REGISTRY
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_, c)| c.entry.matches(word))
|
||||
.map(|(i, c)| (i, *c))
|
||||
.find(|(_, (c, _))| c.entry.matches(word))
|
||||
.map(|(i, (c, _))| (i, *c))
|
||||
}
|
||||
|
||||
/// Every `CommandNode` registered for an entry word, with its
|
||||
/// `REGISTRY` index and [`CommandCategory`], case-insensitively
|
||||
/// (ADR-0033 Amendment 1).
|
||||
///
|
||||
/// A non-shared entry word returns a single candidate; a shared
|
||||
/// entry word (`insert` / `update` / `delete` from sub-phase 3b)
|
||||
/// returns its `Simple` DSL node and `Advanced` SQL node. The
|
||||
/// dispatcher picks among them by the active input mode.
|
||||
#[must_use]
|
||||
pub fn commands_for_entry_word(
|
||||
word: &str,
|
||||
) -> Vec<(usize, &'static CommandNode, CommandCategory)> {
|
||||
REGISTRY
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, (c, _))| c.entry.matches(word))
|
||||
.map(|(i, (c, category))| (i, *c, *category))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
Reference in New Issue
Block a user