ADR-0024 Phase A: walker framework + app-lifecycle commands
Stand up the unified-grammar tree walker alongside the existing
chumsky parser and migrate the eleven app-lifecycle commands
(quit, help, rebuild, save / save as, new, load, export, import,
mode, messages) end-to-end. The router in parse_tokens consults
the walker first; non-migrated commands still fall through to
chumsky.
Scope:
- src/dsl/grammar/{mod,app}.rs: Node enum (13 kinds), Word /
IdentSource / HintMode / HighlightClass / ValidationError /
CommandNode types, REGISTRY of the eleven app commands.
- src/dsl/walker/{mod,driver,context,outcome,lex_helpers}.rs:
scannerless byte-level walker, per-node-kind dispatch with
Choice/Seq/Optional backtracking, WalkContext (Phase B-D
schema fields stubbed), WalkOutcome with Match/Incomplete/
Mismatch/ValidationFailed.
- src/dsl/parser.rs: try_walker_route() runs first in
parse_tokens; bridge converts WalkOutcome to ParseError
preserving catalog wording (mode.unknown / messages.unknown
surface verbatim via friendly::translate). Legacy
try_parse_app_path_command deleted; chumsky's bare-keyword
app branches remain unreachable until Phase F sweep.
Walker design choices worth noting:
- mode <value> / messages <value> use Choice(Word, Word, Ident)
so known keywords appear in the expected-set; the trailing
Ident catch-all funnels unknown values into the friendly
validator that always errors with the catalog wording.
- save / save as is one CommandNode (Optional(Word("as"))) -
closes the round-5 "save Tab can't offer as" limitation
structurally.
- Path-bearing UX shipped per ADR-0024: BarePath terminates at
whitespace; paths with spaces use the (not-yet-wired) quoted
form. Existing tests pass on the new shape.
Tests:
- 28 new walker-specific tests in dsl::walker::tests covering
every app-lifecycle command, friendly-error wording for
mode/messages unknown values, trailing-garbage detection,
whitespace tolerance, and routing fall-through.
- Total: 805 passed, 0 failed, 1 ignored (was 777 / 1).
- cargo clippy --all-targets -- -D warnings clean.
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
//! Walker output types (ADR-0024 §architecture).
|
||||
//!
|
||||
//! `WalkResult` carries everything a consumer (parse / completion /
|
||||
//! highlight / hint) needs from a single walk: the outcome
|
||||
//! (matched, incomplete, mismatched, validation-failed), the
|
||||
//! matched-node path the AST builder reads, and the per-byte
|
||||
//! highlight class assignments collected as terminals matched.
|
||||
//!
|
||||
//! Phase A note: only the parse consumer is wired today. The
|
||||
//! `per_byte_class` field is populated but unused outside
|
||||
//! tests; completion + highlighting still flow through the
|
||||
//! chumsky path until Phase D / F.
|
||||
|
||||
use crate::dsl::grammar::{HighlightClass, ValidationError};
|
||||
|
||||
/// How far into the input the walker should consume.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum WalkBound {
|
||||
/// Consume all input. Trailing whitespace OK; trailing tokens
|
||||
/// fail the walk. Used by the parse consumer.
|
||||
EndOfInput,
|
||||
/// Consume up to (but not including) the given byte position.
|
||||
/// Used by completion / hint to ask "what was expected at the
|
||||
/// cursor?".
|
||||
#[allow(dead_code)]
|
||||
Position(usize),
|
||||
}
|
||||
|
||||
/// Closed shape describing what could legally have continued the
|
||||
/// walk at its stopping position. Phase A keeps this minimal —
|
||||
/// only what the router needs to render a parse error.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Expectation {
|
||||
/// The walker expected one of these literal keywords.
|
||||
Word(&'static str),
|
||||
/// The walker expected an identifier of the given role.
|
||||
Ident { role: &'static str },
|
||||
/// The walker expected this exact punctuation character.
|
||||
Punct(char),
|
||||
/// The walker expected a number literal.
|
||||
NumberLit,
|
||||
/// The walker expected a string literal.
|
||||
StringLit,
|
||||
/// The walker expected a blob literal.
|
||||
#[allow(dead_code)]
|
||||
BlobLit,
|
||||
/// The walker expected a flag with this name (without `--`).
|
||||
#[allow(dead_code)]
|
||||
Flag(&'static str),
|
||||
/// The walker expected a bare-path argument (non-whitespace
|
||||
/// run).
|
||||
BarePath,
|
||||
/// The walker expected end of input.
|
||||
EndOfInput,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum WalkOutcome {
|
||||
/// Input fully matched a command. `command_idx` is the
|
||||
/// position of the matched command in the active registry.
|
||||
Match { command_idx: usize },
|
||||
/// Input matched a prefix; more input would have continued
|
||||
/// the parse. `position` is the byte offset where input ran
|
||||
/// out (post-whitespace).
|
||||
Incomplete {
|
||||
position: usize,
|
||||
expected: Vec<Expectation>,
|
||||
},
|
||||
/// Input had a token at `position` that no expected node
|
||||
/// accepts.
|
||||
Mismatch {
|
||||
position: usize,
|
||||
expected: Vec<Expectation>,
|
||||
},
|
||||
/// The walker matched a terminal but a content validator
|
||||
/// rejected the value (e.g. `mode foo` matched the value
|
||||
/// slot's identifier shape, then the validator fired
|
||||
/// `mode.unknown`).
|
||||
ValidationFailed {
|
||||
position: usize,
|
||||
error: ValidationError,
|
||||
},
|
||||
}
|
||||
|
||||
/// One terminal-node match. Combinators (Seq / Choice / Optional /
|
||||
/// Repeated) shape the order; the AST builder reads the items in
|
||||
/// declaration order.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MatchedItem {
|
||||
pub kind: MatchedKind,
|
||||
pub text: String,
|
||||
pub span: (usize, usize),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum MatchedKind {
|
||||
/// A `Word` node matched. Carries the primary literal (not
|
||||
/// the alias actually typed) so the AST builder can match
|
||||
/// on it canonically.
|
||||
Word(&'static str),
|
||||
Punct(char),
|
||||
/// An `Ident` matched. The role identifies which slot.
|
||||
Ident { role: &'static str },
|
||||
NumberLit,
|
||||
StringLit,
|
||||
BlobLit,
|
||||
Flag(&'static str),
|
||||
BarePath,
|
||||
}
|
||||
|
||||
/// The path of matched terminals, in order. Optional / Repeated
|
||||
/// nodes that produced no match contribute nothing.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct MatchedPath {
|
||||
pub items: Vec<MatchedItem>,
|
||||
}
|
||||
|
||||
impl MatchedPath {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn push(&mut self, item: MatchedItem) {
|
||||
self.items.push(item);
|
||||
}
|
||||
|
||||
/// Convenience: find the first item matching the predicate.
|
||||
pub fn find<F: Fn(&MatchedItem) -> bool>(&self, pred: F) -> Option<&MatchedItem> {
|
||||
self.items.iter().find(|i| pred(i))
|
||||
}
|
||||
|
||||
/// Convenience: did any item match this exact word literal
|
||||
/// (by primary)? Used by Optional-keyword discrimination
|
||||
/// (e.g., `save` vs `save as`).
|
||||
pub fn contains_word(&self, primary: &'static str) -> bool {
|
||||
self.items
|
||||
.iter()
|
||||
.any(|i| matches!(&i.kind, MatchedKind::Word(p) if *p == primary))
|
||||
}
|
||||
}
|
||||
|
||||
/// Per-byte highlight class assignment, collected as terminals
|
||||
/// match. Phase A keeps this for future consumers; not yet used
|
||||
/// outside walker-internal tests.
|
||||
#[derive(Debug, Clone)]
|
||||
#[allow(dead_code)]
|
||||
pub struct ByteClass {
|
||||
pub start: usize,
|
||||
pub end: usize,
|
||||
pub class: HighlightClass,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct WalkResult {
|
||||
pub outcome: WalkOutcome,
|
||||
pub matched_path: MatchedPath,
|
||||
#[allow(dead_code)]
|
||||
pub per_byte_class: Vec<ByteClass>,
|
||||
}
|
||||
Reference in New Issue
Block a user