ADR-0024 Phase F (full) step 2: usage via CommandNode.usage_ids
Migrates parse-error usage-block rendering from the legacy `dsl::usage::matched_entry` (which scanned a `Vec<Token>` for the first matched Keyword) to walker-side lookup driven by each `CommandNode`'s `usage_ids` slice. `CommandNode.usage_id: Option<&'static str>` becomes `usage_ids: &'static [&'static str]`. Multi-form families (`drop`, `add`, `show`) carry every variant — `drop` lists table/column/relationship templates; `add` lists column / relationship; `show` lists data / table. The single-shape commands carry their single catalog key. App-lifecycle CommandNodes had pointed at non-existent `parse.usage.app.*` keys (never noticed because the field was unused); they now point at the real catalog entries (`parse.usage.quit`, `parse.usage.help`, …). New helpers in `dsl::grammar`: - `usage_keys_for_input(source) -> Option<(entry_word, usage_ids)>` resolves the first identifier-shape token to a CommandNode and returns its usage_ids list. Used by `app::render_usage_block` and `input_render::ambient_hint`. - `entry_words_alphabetised() -> Vec<&'static str>` replaces `dsl::usage::entry_keywords_alphabetised`. `dsl::usage` is deleted. The "available commands:" fallback in `render_usage_block` now formats entry words as `` `<word>` `` directly (matching the `parse.token.keyword.*` catalog renders); the per-keyword catalog wrappers will collapse in the next step (ADR-0024 §cleanup-pass §F). `parse_command` and `parse_tokens` slim down: - `parse_command(input)` no longer pre-lexes — the walker scans source bytes directly. - `parse_tokens` (internal-only `pub` for "future I3/I4 work") is removed; its body folded into `parse_command`. - `unknown_command_error` reads the walker registry directly. Touched modules also drop their `crate::dsl::lexer::lex` and `crate::dsl::usage` imports: `app.rs`, `input_render.rs`, `completion.rs`. Tests: 852 passing, 0 failing, 1 ignored (down from 860 because the 8 `dsl::usage::tests::*` tests are gone with the module).
This commit is contained in:
+23
-40
@@ -1,17 +1,18 @@
|
||||
//! DSL parser (ADR-0020 + ADR-0021).
|
||||
//! DSL parser (ADR-0024).
|
||||
//!
|
||||
//! Two-phase: a lexer (`crate::dsl::lexer`) produces a span-tagged
|
||||
//! token stream, and chumsky combinators over `&[Token]` build the
|
||||
//! `Command` AST. Keyword identity is exact via the `Keyword` enum
|
||||
//! from `crate::dsl::keyword`; alternative-aggregation across
|
||||
//! `choice` is chumsky-native (the load-bearing fix that motivated
|
||||
//! ADR-0020).
|
||||
//! The chumsky+lexer pipeline has been retired (ADR-0024 §migration
|
||||
//! Phase F minimal). `parse_command` now routes every input through
|
||||
//! the unified-grammar walker in `crate::dsl::walker`. The walker
|
||||
//! reads source bytes directly — there is no separate token pre-pass.
|
||||
//!
|
||||
//! Errors from chumsky are mapped to the local [`ParseError`] type
|
||||
//! so callers do not depend on chumsky's API surface.
|
||||
//! This module remains the public entry point for parsing because
|
||||
//! consumers depend on `ParseError`'s shape (the `expected`,
|
||||
//! `position`, `at_eof` fields drive completion, hint rendering,
|
||||
//! and the input-renderer's error overlay). It also produces the
|
||||
//! synthetic "unknown command" error when the input's first
|
||||
//! identifier-shape token isn't a registered entry word.
|
||||
|
||||
use crate::dsl::command::Command;
|
||||
use crate::dsl::lexer::{Token, lex};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ParseError {
|
||||
@@ -81,48 +82,30 @@ impl ParseError {
|
||||
}
|
||||
|
||||
/// Parse a single DSL command end-to-end.
|
||||
///
|
||||
/// Routes through the unified-grammar walker (ADR-0024
|
||||
/// §architecture). If the walker doesn't engage (the input's
|
||||
/// first identifier-shape token isn't a registered entry word),
|
||||
/// produces a synthetic "unknown command" error naming every
|
||||
/// valid entry keyword.
|
||||
pub fn parse_command(input: &str) -> Result<Command, ParseError> {
|
||||
if input.trim().is_empty() {
|
||||
return Err(ParseError::Empty);
|
||||
}
|
||||
let tokens = lex(input);
|
||||
parse_tokens(&tokens, input)
|
||||
}
|
||||
|
||||
/// Parse a token slice into a `Command`. The `source` argument is
|
||||
/// kept in scope so the `replay` bare-path special case
|
||||
/// (ADR-0020 §6) can source-slice its argument.
|
||||
///
|
||||
/// Public so future I3 (tab completion) and I4 (syntax
|
||||
/// highlighting) work can re-enter the parser at this layer
|
||||
/// without having to re-lex.
|
||||
pub fn parse_tokens(tokens: &[Token], source: &str) -> Result<Command, ParseError> {
|
||||
if tokens.is_empty() {
|
||||
return Err(ParseError::Empty);
|
||||
}
|
||||
// ADR-0024 Phase F: the unified-grammar walker now owns
|
||||
// every command. If the walker doesn't engage (the input's
|
||||
// first identifier-shape token isn't a registered entry
|
||||
// word), produce an "unknown command" error naming the
|
||||
// valid entry keywords.
|
||||
if let Some(result) = try_walker_route(source) {
|
||||
if let Some(result) = try_walker_route(input) {
|
||||
return result;
|
||||
}
|
||||
Err(unknown_command_error(source))
|
||||
Err(unknown_command_error(input))
|
||||
}
|
||||
|
||||
/// Synthetic ParseError for inputs whose first identifier-shape
|
||||
/// token isn't a registered command entry word. Replaces the
|
||||
/// chumsky-side "expected `create`, `drop`, …" structural error
|
||||
/// the legacy parser produced for the same case.
|
||||
/// token isn't a registered command entry word.
|
||||
fn unknown_command_error(source: &str) -> ParseError {
|
||||
use crate::dsl::grammar::REGISTRY;
|
||||
use crate::dsl::walker::lex_helpers::{consume_ident, skip_whitespace};
|
||||
let mut entries: Vec<String> = REGISTRY
|
||||
.iter()
|
||||
.map(|c| format!("`{}`", c.entry.primary))
|
||||
let entries: Vec<String> = crate::dsl::grammar::entry_words_alphabetised()
|
||||
.into_iter()
|
||||
.map(|w| format!("`{w}`"))
|
||||
.collect();
|
||||
entries.sort();
|
||||
let joined = oxford_join(&entries);
|
||||
let start = skip_whitespace(source, 0);
|
||||
let (position, found_word) = consume_ident(source, start).map_or_else(
|
||||
|
||||
Reference in New Issue
Block a user