e22f933e02
Adds `Severity` (Error / Warning, ordered so Error > Warning)
and `Diagnostic { severity, span, message }` in
`walker::outcome`, plus a `diagnostics` field on `WalkResult`
— the schema-aware findings layered on a structurally-valid
parse (ADR-0027 §2).
`input_verdict(source, schema)` is the validity-indicator
entry point: `None` when the input would run clean (and for
empty input), `Some(Error)` for a parse failure or unknown
command, `Some(Warning)` for the ADR-0026 expression flags.
The verdict is the highest severity across the parse outcome
and the diagnostics set.
`diagnostics` is empty at this step — the schema-existence
(ERROR) and expression (WARNING) passes that fill it land
next. Covered by `input_verdict` unit tests.
221 lines
7.8 KiB
Rust
221 lines
7.8 KiB
Rust
//! 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<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)]
|
|
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<ByteClass>,
|
|
/// 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<Diagnostic>,
|
|
/// 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<Expectation>,
|
|
}
|