//! 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, IdentSource, 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 this literal keyword. Word(&'static str), /// The walker expected this verbatim literal byte sequence /// (used today for the `1` in `add 1:n …`). Literal(&'static str), /// The walker expected an identifier slot. `source` drives /// the user-facing expected-label rendering ("table name", /// "column name", …) so the existing completion engine's /// `IdentSlot::from_expected_label` round-trip still works. /// `role` is the walker-internal slot tag. Ident { role: &'static str, source: IdentSource, }, /// 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)] pub struct ByteClass { pub start: usize, pub end: usize, pub class: HighlightClass, } /// Severity of a pre-submit [`Diagnostic`] (ADR-0027 §1). /// /// Ordered so `Error > Warning` — taking the `max` across a /// diagnostic set gives the validity indicator's verdict. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub enum Severity { /// Valid and runnable, but very likely not what the user /// intends — a type-mismatched comparison, `= NULL` /// (ADR-0026 §7). Warning, /// Known to fail if submitted now — a parse error, or a /// reference to a table / column that does not exist. Error, } /// A problem the walker found in the input *before* submission /// (ADR-0027 §2). The severity drives the validity indicator; /// the span drives highlighting; the message is the hint text. /// /// Note: the parse outcome (`Incomplete` / `Mismatch` / /// `ValidationFailed`) is *not* recorded as a `Diagnostic` — /// the indicator reads it from `WalkOutcome` directly /// (ADR-0027 §2, "the highest severity across the outcome and /// the diagnostics"). `diagnostics` carries the schema-aware /// findings layered on top of a structurally-valid parse. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Diagnostic { pub severity: Severity, /// Byte range in the source the diagnostic refers to. pub span: (usize, usize), /// Ready-to-show message — already catalog-translated by /// whichever pass produced the diagnostic. pub message: String, } #[derive(Debug, Clone)] pub struct WalkResult { pub outcome: WalkOutcome, pub matched_path: MatchedPath, pub per_byte_class: Vec, /// Schema-aware pre-submit findings layered on a /// structurally-valid parse (ADR-0027): unknown table / /// column references (ERROR), type-mismatched WHERE /// comparisons and `= NULL` (WARNING). Empty when the /// input does not parse — those are the outcome's job. pub diagnostics: Vec, /// Optional-Optional expectations the walker could have /// accepted but didn't because the outer shape ran out at a /// node boundary (ADR-0024 §architecture, round-5 follow-up). /// /// Populated on `WalkOutcome::Match` so completion can offer /// optional-suffix candidates at the end of a valid command /// — e.g., after typing `save` the walker matches the /// Optional `as` as skipped, the suffix carries it here, and /// the completion engine surfaces `as` as a Tab candidate. /// Empty on the non-Match outcomes — those carry expected /// information inside the outcome variant itself. pub tail_expected: Vec, }