//! 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, }, /// Input had a token at `position` that no expected node /// accepts. Mismatch { position: usize, expected: Vec, }, /// 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, } 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 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, }