911a537a83
Replaces the hint resolver's signature-matching (does the expected set
contain all five literal forms? an Ident{NewName}?) with a grammar-
declared annotation. New Node::Hinted { mode, inner } wrapper; the
walker records the mode in WalkContext::pending_hint_mode on entry and
clears it on any successful match (cursor moved past the slot — this
also undoes the leak where a failed Hinted branch of a Choice would
otherwise strand a stale mode). The resolver reads pending_hint_mode
directly.
Value-literal fallback slots carry ProseOnly; NewName ident slots carry
ForceProse. hint_mode_at_input_inner now delegates to
hint_resolution_at_input — one resolution path, no duplicated logic.
No behaviour change; the typing-surface matrix guards it.
822 lines
28 KiB
Rust
822 lines
28 KiB
Rust
//! Per-node-kind walk dispatch (ADR-0024 §architecture).
|
|
//!
|
|
//! `walk_node` is the recursive workhorse that the public
|
|
//! `walk()` entry calls into for a `CommandNode`'s `shape`. It
|
|
//! tries to match `node` starting at `position`, mutating
|
|
//! `path` (matched terminals collected in declaration order) and
|
|
//! `per_byte` (highlight class assignments) as it goes.
|
|
//!
|
|
//! The return value distinguishes four cases:
|
|
//!
|
|
//! - `Matched { end }` — full match, walker consumed up to `end`.
|
|
//! - `NoMatch { … }` — node didn't engage at this position. For
|
|
//! `Optional` and `Choice` callers this is benign (try the
|
|
//! next branch / skip the optional); for `Seq` it's only
|
|
//! benign on the first child.
|
|
//! - `Incomplete { … }` — node committed (consumed at least one
|
|
//! terminal) but ran out of input. Surfaces as
|
|
//! `WalkOutcome::Incomplete` at the top level.
|
|
//! - `Failed { … }` — node committed and a content validator
|
|
//! rejected the value, or a hard structural failure occurred
|
|
//! mid-shape. Surfaces as `WalkOutcome::Mismatch` or
|
|
//! `WalkOutcome::ValidationFailed` at the top level.
|
|
|
|
use crate::dsl::grammar::{HighlightClass, Node, ValidationError};
|
|
use crate::dsl::walker::context::WalkContext;
|
|
use crate::dsl::walker::lex_helpers::{
|
|
consume_bare_path, consume_flag, consume_ident, consume_number_literal,
|
|
consume_string_literal, skip_whitespace,
|
|
};
|
|
use crate::dsl::walker::outcome::{
|
|
ByteClass, Expectation, MatchedItem, MatchedKind, MatchedPath,
|
|
};
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum NodeWalkResult {
|
|
Matched {
|
|
end: usize,
|
|
/// Expectations contributed by Optional children that
|
|
/// skipped (matched zero terminals). Walker callers
|
|
/// merge these into the next failure's expected set so
|
|
/// completion sees the full "what could have appeared
|
|
/// here" union, not just the strictly-required next
|
|
/// terminal.
|
|
skipped: Vec<Expectation>,
|
|
},
|
|
/// Did not engage at this position. Caller decides whether
|
|
/// this is benign (Optional, Choice fallthrough) or a hard
|
|
/// failure (Seq mid-shape).
|
|
NoMatch {
|
|
position: usize,
|
|
expected: Vec<Expectation>,
|
|
},
|
|
/// Committed and ran out of input.
|
|
Incomplete {
|
|
position: usize,
|
|
expected: Vec<Expectation>,
|
|
},
|
|
/// Committed and hit a hard mismatch or validator failure.
|
|
Failed {
|
|
position: usize,
|
|
kind: FailureKind,
|
|
},
|
|
}
|
|
|
|
const fn matched(end: usize) -> NodeWalkResult {
|
|
NodeWalkResult::Matched {
|
|
end,
|
|
skipped: Vec::new(),
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum FailureKind {
|
|
Mismatch { expected: Vec<Expectation> },
|
|
Validation(ValidationError),
|
|
}
|
|
|
|
pub fn walk_node(
|
|
source: &str,
|
|
position: usize,
|
|
node: &Node,
|
|
ctx: &mut WalkContext,
|
|
path: &mut MatchedPath,
|
|
per_byte: &mut Vec<ByteClass>,
|
|
) -> NodeWalkResult {
|
|
let pos = skip_whitespace(source, position);
|
|
let result = walk_node_inner(source, pos, node, ctx, path, per_byte);
|
|
// ADR-0024 §HintMode-per-node: `pending_hint_mode` records
|
|
// the Hinted slot the cursor is currently inside. Any
|
|
// successful match means the cursor advanced past whatever
|
|
// slot was pending — clear it. This also undoes the leak
|
|
// where a failed `Hinted` branch of a `Choice` sets the
|
|
// mode and the `Choice` then matches via a different
|
|
// branch: that branch's match clears the stale mode.
|
|
if matches!(result, NodeWalkResult::Matched { .. }) {
|
|
ctx.pending_hint_mode = None;
|
|
}
|
|
result
|
|
}
|
|
|
|
fn walk_node_inner(
|
|
source: &str,
|
|
pos: usize,
|
|
node: &Node,
|
|
ctx: &mut WalkContext,
|
|
path: &mut MatchedPath,
|
|
per_byte: &mut Vec<ByteClass>,
|
|
) -> NodeWalkResult {
|
|
match node {
|
|
Node::Word(word) => walk_word(source, pos, word, path, per_byte),
|
|
Node::Punct(ch) => walk_punct(source, pos, *ch, path, per_byte),
|
|
Node::Ident {
|
|
source: src,
|
|
role,
|
|
validator,
|
|
highlight_override: _,
|
|
writes_table,
|
|
writes_column,
|
|
writes_user_listed_column,
|
|
} => walk_ident(
|
|
source,
|
|
pos,
|
|
*src,
|
|
role,
|
|
*validator,
|
|
*writes_table,
|
|
*writes_column,
|
|
*writes_user_listed_column,
|
|
ctx,
|
|
path,
|
|
per_byte,
|
|
),
|
|
Node::NumberLit { validator } => walk_number_lit(source, pos, *validator, path, per_byte),
|
|
Node::Literal(literal) => walk_literal(source, pos, literal, path, per_byte),
|
|
Node::StringLit => walk_string_lit(source, pos, path, per_byte),
|
|
Node::BlobLit => {
|
|
// BlobLit terminals are declared but no current grammar
|
|
// node uses them. Reaching this branch means a future
|
|
// grammar declared a BlobLit without walker support
|
|
// landing — surface as a hard failure so tests catch
|
|
// it loudly rather than silently mis-parsing.
|
|
NodeWalkResult::Failed {
|
|
position: pos,
|
|
kind: FailureKind::Mismatch { expected: vec![] },
|
|
}
|
|
}
|
|
Node::DynamicSubgrammar(factory) => {
|
|
// ADR-0024 §sub-grammars: resolve the inner Node at
|
|
// walk time using the active `WalkContext`, then
|
|
// recursively walk it. `Box::leak` per-walk gives the
|
|
// inner static-slice fields (Choice/Seq) the lifetime
|
|
// they require; the leak is bounded by command-shape
|
|
// complexity per walk.
|
|
let resolved: &'static Node = Box::leak(Box::new(factory(ctx)));
|
|
walk_node(source, pos, resolved, ctx, path, per_byte)
|
|
}
|
|
Node::TypedValueSlot {
|
|
ty,
|
|
column_name,
|
|
inner,
|
|
} => {
|
|
// ADR-0024 §Phase D §typed-value-slots. Tag the
|
|
// pending column type so the hint resolver can emit
|
|
// per-type prose at empty prefix. If a column name
|
|
// is embedded (insert column_value_list path), tag
|
|
// that too so the hint can mention the column by
|
|
// name. Clear on successful inner match — positions
|
|
// BETWEEN typed slots (post-comma, between values)
|
|
// don't carry stale hint state.
|
|
ctx.pending_value_type = Some(*ty);
|
|
if let Some(name) = column_name {
|
|
ctx.pending_value_column = Some((*name).to_string());
|
|
}
|
|
let result = walk_node(source, pos, inner, ctx, path, per_byte);
|
|
if matches!(result, NodeWalkResult::Matched { .. }) {
|
|
ctx.pending_value_type = None;
|
|
ctx.pending_value_column = None;
|
|
}
|
|
result
|
|
}
|
|
Node::Hinted { mode, inner } => {
|
|
// ADR-0024 §HintMode-per-node. Record the grammar's
|
|
// declared hint mode so the hint resolver can read
|
|
// it directly. The `walk_node` wrapper clears it on
|
|
// any successful match (the cursor moved past the
|
|
// slot), so a Hinted slot whose inner fails at EOF
|
|
// leaves the mode set for the resolver to read.
|
|
ctx.pending_hint_mode = Some(*mode);
|
|
walk_node(source, pos, inner, ctx, path, per_byte)
|
|
}
|
|
Node::Flag(name) => walk_flag(source, pos, name, path, per_byte),
|
|
Node::Repeated {
|
|
inner,
|
|
separator,
|
|
min,
|
|
} => walk_repeated(source, pos, inner, *separator, *min, ctx, path, per_byte),
|
|
Node::BarePath => walk_bare_path(source, pos, path, per_byte),
|
|
Node::Choice(children) => walk_choice(source, pos, children, ctx, path, per_byte),
|
|
Node::Seq(children) => walk_seq(source, pos, children, ctx, path, per_byte),
|
|
Node::Optional(child) => walk_optional(source, pos, child, ctx, path, per_byte),
|
|
}
|
|
}
|
|
|
|
fn walk_word(
|
|
source: &str,
|
|
position: usize,
|
|
word: &crate::dsl::grammar::Word,
|
|
path: &mut MatchedPath,
|
|
per_byte: &mut Vec<ByteClass>,
|
|
) -> NodeWalkResult {
|
|
// First scan an identifier-shape token at `position`; if
|
|
// none, we definitely don't have this keyword. If one, check
|
|
// it against the word's primary + aliases.
|
|
let Some((start, end)) = consume_ident(source, position) else {
|
|
return NodeWalkResult::NoMatch {
|
|
position,
|
|
expected: vec![Expectation::Word(word.primary)],
|
|
};
|
|
};
|
|
let candidate = &source[start..end];
|
|
if word.matches(candidate) {
|
|
path.push(MatchedItem {
|
|
kind: MatchedKind::Word(word.primary),
|
|
text: candidate.to_string(),
|
|
span: (start, end),
|
|
});
|
|
per_byte.push(ByteClass {
|
|
start,
|
|
end,
|
|
class: HighlightClass::Keyword,
|
|
});
|
|
NodeWalkResult::Matched { end, skipped: Vec::new() }
|
|
} else {
|
|
NodeWalkResult::NoMatch {
|
|
position,
|
|
expected: vec![Expectation::Word(word.primary)],
|
|
}
|
|
}
|
|
}
|
|
|
|
fn walk_punct(
|
|
source: &str,
|
|
position: usize,
|
|
ch: char,
|
|
path: &mut MatchedPath,
|
|
per_byte: &mut Vec<ByteClass>,
|
|
) -> NodeWalkResult {
|
|
let bytes = source.as_bytes();
|
|
if position < bytes.len() && bytes[position] == ch as u8 {
|
|
path.push(MatchedItem {
|
|
kind: MatchedKind::Punct(ch),
|
|
text: ch.to_string(),
|
|
span: (position, position + 1),
|
|
});
|
|
per_byte.push(ByteClass {
|
|
start: position,
|
|
end: position + 1,
|
|
class: HighlightClass::Punct,
|
|
});
|
|
matched(position + 1)
|
|
} else {
|
|
NodeWalkResult::NoMatch {
|
|
position,
|
|
expected: vec![Expectation::Punct(ch)],
|
|
}
|
|
}
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn walk_ident(
|
|
source: &str,
|
|
position: usize,
|
|
src: crate::dsl::grammar::IdentSource,
|
|
role: &'static str,
|
|
validator: Option<crate::dsl::grammar::IdentValidator>,
|
|
writes_table: bool,
|
|
writes_column: bool,
|
|
writes_user_listed_column: bool,
|
|
ctx: &mut WalkContext,
|
|
path: &mut MatchedPath,
|
|
per_byte: &mut Vec<ByteClass>,
|
|
) -> NodeWalkResult {
|
|
let Some((start, end)) = consume_ident(source, position) else {
|
|
return NodeWalkResult::NoMatch {
|
|
position,
|
|
expected: vec![Expectation::Ident { role, source: src }],
|
|
};
|
|
};
|
|
let text = source[start..end].to_string();
|
|
if let Some(v) = validator
|
|
&& let Err(err) = v(&text)
|
|
{
|
|
return NodeWalkResult::Failed {
|
|
position: start,
|
|
kind: FailureKind::Validation(err),
|
|
};
|
|
}
|
|
// ADR-0024 §Phase D: schema-aware writes. When the ident is
|
|
// a Tables source with `writes_table`, resolve the matched
|
|
// name against the schema cache and populate current_table /
|
|
// current_table_columns so subsequent dynamic sub-grammars
|
|
// can read them. `writes_column` resolves against the
|
|
// already-populated `current_table_columns`.
|
|
if writes_table && matches!(src, crate::dsl::grammar::IdentSource::Tables) {
|
|
ctx.current_table = Some(text.clone());
|
|
ctx.current_table_columns = ctx
|
|
.schema
|
|
.and_then(|s| s.columns_for_table(&text).map(<[_]>::to_vec));
|
|
}
|
|
if writes_column && matches!(src, crate::dsl::grammar::IdentSource::Columns) {
|
|
ctx.current_column = ctx.current_table_columns.as_ref().and_then(|cols| {
|
|
cols.iter()
|
|
.find(|c| c.name.eq_ignore_ascii_case(&text))
|
|
.cloned()
|
|
});
|
|
// Surface the column name to the hint resolver too —
|
|
// this is the `update <T> set <col>=` / `where <col>=`
|
|
// path. The matching column's canonical name (from the
|
|
// schema) wins over the user's spelling so the hint
|
|
// mirrors what's in the schema.
|
|
ctx.pending_value_column = ctx
|
|
.current_column
|
|
.as_ref()
|
|
.map(|c| c.name.clone())
|
|
.or_else(|| Some(text.clone()));
|
|
}
|
|
if writes_user_listed_column
|
|
&& matches!(src, crate::dsl::grammar::IdentSource::Columns)
|
|
{
|
|
// Form A: `insert into <T> (col1, col2, …)`. Append the
|
|
// matched column name to user_listed_columns so the
|
|
// inner `values (…)` slot list mirrors the user's
|
|
// explicit selection. Schema-canonical name wins over
|
|
// user's spelling so downstream lookups (typed slot
|
|
// dispatch, hint rendering) are consistent.
|
|
let canonical = ctx
|
|
.current_table_columns
|
|
.as_ref()
|
|
.and_then(|cols| {
|
|
cols.iter()
|
|
.find(|c| c.name.eq_ignore_ascii_case(&text))
|
|
.map(|c| c.name.clone())
|
|
})
|
|
.unwrap_or_else(|| text.clone());
|
|
ctx.user_listed_columns
|
|
.get_or_insert_with(Vec::new)
|
|
.push(canonical);
|
|
}
|
|
path.push(MatchedItem {
|
|
kind: MatchedKind::Ident { role },
|
|
text,
|
|
span: (start, end),
|
|
});
|
|
per_byte.push(ByteClass {
|
|
start,
|
|
end,
|
|
class: HighlightClass::Identifier,
|
|
});
|
|
NodeWalkResult::Matched { end, skipped: Vec::new() }
|
|
}
|
|
|
|
fn walk_string_lit(
|
|
source: &str,
|
|
position: usize,
|
|
path: &mut MatchedPath,
|
|
per_byte: &mut Vec<ByteClass>,
|
|
) -> NodeWalkResult {
|
|
let Some(((start, end), content)) = consume_string_literal(source, position) else {
|
|
return NodeWalkResult::NoMatch {
|
|
position,
|
|
expected: vec![Expectation::StringLit],
|
|
};
|
|
};
|
|
path.push(MatchedItem {
|
|
kind: MatchedKind::StringLit,
|
|
text: content,
|
|
span: (start, end),
|
|
});
|
|
per_byte.push(ByteClass {
|
|
start,
|
|
end,
|
|
class: HighlightClass::String,
|
|
});
|
|
NodeWalkResult::Matched {
|
|
end,
|
|
skipped: Vec::new(),
|
|
}
|
|
}
|
|
|
|
fn walk_literal(
|
|
source: &str,
|
|
position: usize,
|
|
literal: &'static str,
|
|
path: &mut MatchedPath,
|
|
per_byte: &mut Vec<ByteClass>,
|
|
) -> NodeWalkResult {
|
|
let bytes = source.as_bytes();
|
|
let lit_bytes = literal.as_bytes();
|
|
if position + lit_bytes.len() > bytes.len() {
|
|
return NodeWalkResult::NoMatch {
|
|
position,
|
|
expected: vec![Expectation::Literal(literal)],
|
|
};
|
|
}
|
|
if &bytes[position..position + lit_bytes.len()] != lit_bytes {
|
|
return NodeWalkResult::NoMatch {
|
|
position,
|
|
expected: vec![Expectation::Literal(literal)],
|
|
};
|
|
}
|
|
// Lookahead: if the literal is a single digit / alphabetic
|
|
// run, the next byte must not extend it (so `1` doesn't
|
|
// half-match `12`).
|
|
let end = position + lit_bytes.len();
|
|
let last = lit_bytes[lit_bytes.len() - 1];
|
|
let last_is_word = last.is_ascii_alphanumeric() || last == b'_';
|
|
if last_is_word && end < bytes.len() {
|
|
let next = bytes[end];
|
|
if next.is_ascii_alphanumeric() || next == b'_' {
|
|
return NodeWalkResult::NoMatch {
|
|
position,
|
|
expected: vec![Expectation::Literal(literal)],
|
|
};
|
|
}
|
|
}
|
|
// Highlight class follows the literal's shape: digits get
|
|
// Number; letters get Keyword; mixed defaults to Keyword.
|
|
let class = if lit_bytes.iter().all(|b| b.is_ascii_digit()) {
|
|
HighlightClass::Number
|
|
} else {
|
|
HighlightClass::Keyword
|
|
};
|
|
path.push(MatchedItem {
|
|
kind: MatchedKind::Word(literal),
|
|
text: literal.to_string(),
|
|
span: (position, end),
|
|
});
|
|
per_byte.push(ByteClass {
|
|
start: position,
|
|
end,
|
|
class,
|
|
});
|
|
NodeWalkResult::Matched { end, skipped: Vec::new() }
|
|
}
|
|
|
|
fn walk_number_lit(
|
|
source: &str,
|
|
position: usize,
|
|
validator: Option<crate::dsl::grammar::NumberValidator>,
|
|
path: &mut MatchedPath,
|
|
per_byte: &mut Vec<ByteClass>,
|
|
) -> NodeWalkResult {
|
|
let Some((start, end)) = consume_number_literal(source, position) else {
|
|
return NodeWalkResult::NoMatch {
|
|
position,
|
|
expected: vec![Expectation::NumberLit],
|
|
};
|
|
};
|
|
let text = source[start..end].to_string();
|
|
if let Some(v) = validator
|
|
&& let Err(err) = v(&text)
|
|
{
|
|
return NodeWalkResult::Failed {
|
|
position: start,
|
|
kind: FailureKind::Validation(err),
|
|
};
|
|
}
|
|
path.push(MatchedItem {
|
|
kind: MatchedKind::NumberLit,
|
|
text,
|
|
span: (start, end),
|
|
});
|
|
per_byte.push(ByteClass {
|
|
start,
|
|
end,
|
|
class: HighlightClass::Number,
|
|
});
|
|
NodeWalkResult::Matched { end, skipped: Vec::new() }
|
|
}
|
|
|
|
fn walk_flag(
|
|
source: &str,
|
|
position: usize,
|
|
name: &'static str,
|
|
path: &mut MatchedPath,
|
|
per_byte: &mut Vec<ByteClass>,
|
|
) -> NodeWalkResult {
|
|
let Some((start, end)) = consume_flag(source, position) else {
|
|
return NodeWalkResult::NoMatch {
|
|
position,
|
|
expected: vec![Expectation::Flag(name)],
|
|
};
|
|
};
|
|
// `consume_flag` guarantees `start..end` covers `--<body>`.
|
|
let body = &source[start + 2..end];
|
|
if body != name {
|
|
return NodeWalkResult::NoMatch {
|
|
position,
|
|
expected: vec![Expectation::Flag(name)],
|
|
};
|
|
}
|
|
path.push(MatchedItem {
|
|
kind: MatchedKind::Flag(name),
|
|
text: source[start..end].to_string(),
|
|
span: (start, end),
|
|
});
|
|
per_byte.push(ByteClass {
|
|
start,
|
|
end,
|
|
class: HighlightClass::Flag,
|
|
});
|
|
NodeWalkResult::Matched { end, skipped: Vec::new() }
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn walk_repeated(
|
|
source: &str,
|
|
position: usize,
|
|
inner: &Node,
|
|
separator: Option<&Node>,
|
|
min: usize,
|
|
ctx: &mut WalkContext,
|
|
path: &mut MatchedPath,
|
|
per_byte: &mut Vec<ByteClass>,
|
|
) -> NodeWalkResult {
|
|
let mut cur = position;
|
|
let mut count = 0_usize;
|
|
let mut last_expected: Option<Vec<Expectation>> = None;
|
|
loop {
|
|
let saved_path_len = path.items.len();
|
|
let saved_byte_len = per_byte.len();
|
|
// Track whether the separator successfully consumed
|
|
// before the inner attempt. Used below to distinguish
|
|
// "user typed `,` then stopped at EOF — mid-typing the
|
|
// next item" from "list naturally ended at the inner
|
|
// boundary".
|
|
let mut sep_consumed_to: Option<usize> = None;
|
|
let result = if count == 0 {
|
|
walk_node(source, cur, inner, ctx, path, per_byte)
|
|
} else if let Some(sep) = separator {
|
|
let sep_saved_path = path.items.len();
|
|
let sep_saved_byte = per_byte.len();
|
|
match walk_node(source, cur, sep, ctx, path, per_byte) {
|
|
NodeWalkResult::Matched { end, .. } => {
|
|
sep_consumed_to = Some(end);
|
|
walk_node(source, end, inner, ctx, path, per_byte)
|
|
}
|
|
NodeWalkResult::NoMatch { .. } => {
|
|
path.items.truncate(sep_saved_path);
|
|
per_byte.truncate(sep_saved_byte);
|
|
break;
|
|
}
|
|
other => return other,
|
|
}
|
|
} else {
|
|
walk_node(source, cur, inner, ctx, path, per_byte)
|
|
};
|
|
match result {
|
|
NodeWalkResult::Matched { end, .. } => {
|
|
cur = end;
|
|
count += 1;
|
|
}
|
|
NodeWalkResult::NoMatch { expected, position: inner_pos } => {
|
|
// Mid-typing-the-next-item recovery: if the
|
|
// separator just consumed and the inner failed
|
|
// at EOF, the user is partway through typing the
|
|
// next item — propagate as Incomplete so the
|
|
// outer walker classifies the input as
|
|
// mid-typing rather than rolling the separator
|
|
// back and producing a structural Mismatch at
|
|
// the separator position.
|
|
//
|
|
// Without this branch, `insert into T (a, ` at
|
|
// EOF would roll back the `,`, then the outer
|
|
// `(`-list expected `)` at `cur`, see the
|
|
// separator instead, and report a definite
|
|
// error at the separator. Real users hit this
|
|
// every time they type a comma and pause.
|
|
if let Some(post_sep) = sep_consumed_to {
|
|
let post_ws = skip_whitespace(source, post_sep);
|
|
if post_ws >= source.len() {
|
|
return NodeWalkResult::Incomplete {
|
|
position: inner_pos,
|
|
expected,
|
|
};
|
|
}
|
|
}
|
|
path.items.truncate(saved_path_len);
|
|
per_byte.truncate(saved_byte_len);
|
|
last_expected = Some(expected);
|
|
break;
|
|
}
|
|
other => return other,
|
|
}
|
|
}
|
|
if count < min {
|
|
return NodeWalkResult::NoMatch {
|
|
position: cur,
|
|
expected: last_expected.unwrap_or_default(),
|
|
};
|
|
}
|
|
// The "could continue with another inner" expectations
|
|
// become this Repeated's `skipped` set so the caller's
|
|
// expected-set surfaces them at completion time.
|
|
NodeWalkResult::Matched {
|
|
end: cur,
|
|
skipped: last_expected.unwrap_or_default(),
|
|
}
|
|
}
|
|
|
|
fn walk_bare_path(
|
|
source: &str,
|
|
position: usize,
|
|
path: &mut MatchedPath,
|
|
per_byte: &mut Vec<ByteClass>,
|
|
) -> NodeWalkResult {
|
|
let Some((start, end)) = consume_bare_path(source, position) else {
|
|
return NodeWalkResult::NoMatch {
|
|
position,
|
|
expected: vec![Expectation::BarePath],
|
|
};
|
|
};
|
|
let text = source[start..end].to_string();
|
|
path.push(MatchedItem {
|
|
kind: MatchedKind::BarePath,
|
|
text,
|
|
span: (start, end),
|
|
});
|
|
per_byte.push(ByteClass {
|
|
start,
|
|
end,
|
|
class: HighlightClass::String,
|
|
});
|
|
NodeWalkResult::Matched { end, skipped: Vec::new() }
|
|
}
|
|
|
|
fn walk_choice(
|
|
source: &str,
|
|
position: usize,
|
|
children: &[Node],
|
|
ctx: &mut WalkContext,
|
|
path: &mut MatchedPath,
|
|
per_byte: &mut Vec<ByteClass>,
|
|
) -> NodeWalkResult {
|
|
let mut all_expected: Vec<Expectation> = Vec::new();
|
|
for child in children {
|
|
let saved_path_len = path.items.len();
|
|
let saved_byte_len = per_byte.len();
|
|
match walk_node(source, position, child, ctx, path, per_byte) {
|
|
m @ NodeWalkResult::Matched { .. } => return m,
|
|
NodeWalkResult::NoMatch { expected, .. } => {
|
|
path.items.truncate(saved_path_len);
|
|
per_byte.truncate(saved_byte_len);
|
|
merge_expected(&mut all_expected, expected);
|
|
}
|
|
other => return other,
|
|
}
|
|
}
|
|
NodeWalkResult::NoMatch {
|
|
position,
|
|
expected: all_expected,
|
|
}
|
|
}
|
|
|
|
fn walk_seq(
|
|
source: &str,
|
|
position: usize,
|
|
children: &[Node],
|
|
ctx: &mut WalkContext,
|
|
path: &mut MatchedPath,
|
|
per_byte: &mut Vec<ByteClass>,
|
|
) -> NodeWalkResult {
|
|
let mut cur = position;
|
|
let mut idx = 0;
|
|
// Carries expectations from skipped-Optional children so
|
|
// that a NoMatch on a later child reports the union of "you
|
|
// could have typed any of these" — making the completion
|
|
// engine see optional connectives that haven't been typed.
|
|
let mut pending_skipped: Vec<Expectation> = Vec::new();
|
|
for child in children {
|
|
match walk_node(source, cur, child, ctx, path, per_byte) {
|
|
NodeWalkResult::Matched { end, skipped } => {
|
|
if end == cur {
|
|
// Child matched zero terminals (Optional skipped,
|
|
// empty Repeated, empty Seq). Accumulate its
|
|
// would-be expectations into pending.
|
|
for e in skipped {
|
|
if !pending_skipped.contains(&e) {
|
|
pending_skipped.push(e);
|
|
}
|
|
}
|
|
} else {
|
|
// Child consumed terminals — the "missing optional"
|
|
// window closed; reset the pending list.
|
|
pending_skipped.clear();
|
|
pending_skipped.extend(skipped);
|
|
}
|
|
cur = end;
|
|
idx += 1;
|
|
}
|
|
NodeWalkResult::NoMatch {
|
|
position,
|
|
mut expected,
|
|
} => {
|
|
// Merge pending skipped-optional expectations with this
|
|
// child's expected set.
|
|
for e in std::mem::take(&mut pending_skipped) {
|
|
if !expected.contains(&e) {
|
|
expected.push(e);
|
|
}
|
|
}
|
|
if idx == 0 {
|
|
return NodeWalkResult::NoMatch { position, expected };
|
|
}
|
|
let post_ws = skip_whitespace(source, position);
|
|
if post_ws >= source.len() {
|
|
return NodeWalkResult::Incomplete {
|
|
position: post_ws,
|
|
expected,
|
|
};
|
|
}
|
|
return NodeWalkResult::Failed {
|
|
position: post_ws,
|
|
kind: FailureKind::Mismatch { expected },
|
|
};
|
|
}
|
|
NodeWalkResult::Incomplete {
|
|
position,
|
|
mut expected,
|
|
} => {
|
|
for e in std::mem::take(&mut pending_skipped) {
|
|
if !expected.contains(&e) {
|
|
expected.push(e);
|
|
}
|
|
}
|
|
return NodeWalkResult::Incomplete { position, expected };
|
|
}
|
|
NodeWalkResult::Failed { position, kind } => {
|
|
return NodeWalkResult::Failed { position, kind };
|
|
}
|
|
}
|
|
}
|
|
NodeWalkResult::Matched {
|
|
end: cur,
|
|
skipped: pending_skipped,
|
|
}
|
|
}
|
|
|
|
fn walk_optional(
|
|
source: &str,
|
|
position: usize,
|
|
child: &Node,
|
|
ctx: &mut WalkContext,
|
|
path: &mut MatchedPath,
|
|
per_byte: &mut Vec<ByteClass>,
|
|
) -> NodeWalkResult {
|
|
let saved_path_len = path.items.len();
|
|
let saved_byte_len = per_byte.len();
|
|
let result = walk_node(source, position, child, ctx, path, per_byte);
|
|
let inner_committed = path.items.len() > saved_path_len;
|
|
match result {
|
|
m @ NodeWalkResult::Matched { .. } => m,
|
|
NodeWalkResult::NoMatch { expected, .. } => {
|
|
// Inner didn't engage at all — skip the Optional
|
|
// but carry the inner's expectations so the caller's
|
|
// expected-set sees them.
|
|
path.items.truncate(saved_path_len);
|
|
per_byte.truncate(saved_byte_len);
|
|
NodeWalkResult::Matched {
|
|
end: position,
|
|
skipped: expected,
|
|
}
|
|
}
|
|
NodeWalkResult::Incomplete { position: p, expected } if !inner_committed => {
|
|
// Inner reported Incomplete without consuming
|
|
// anything — same as NoMatch from the user's
|
|
// perspective. Roll back and skip.
|
|
path.items.truncate(saved_path_len);
|
|
per_byte.truncate(saved_byte_len);
|
|
let _ = p;
|
|
NodeWalkResult::Matched {
|
|
end: position,
|
|
skipped: expected,
|
|
}
|
|
}
|
|
NodeWalkResult::Failed {
|
|
kind: FailureKind::Mismatch { expected },
|
|
..
|
|
} if !inner_committed => {
|
|
// Inner reported Mismatch without consuming
|
|
// anything — roll back and skip.
|
|
path.items.truncate(saved_path_len);
|
|
per_byte.truncate(saved_byte_len);
|
|
NodeWalkResult::Matched {
|
|
end: position,
|
|
skipped: expected,
|
|
}
|
|
}
|
|
// Inner committed (consumed at least one terminal) but
|
|
// then ran out / hit a mismatch. Propagate the failure
|
|
// up — the user is mid-typing the optional's content and
|
|
// we'd lose their intent by rolling back. (Pre-fix
|
|
// behavior matched chumsky's `or_not` rollback, but
|
|
// that conflates "Form A in progress" with "Form C with
|
|
// trailing junk" — see e.g. `insert into T (a, b, c)
|
|
// values (1, 2, 3` losing the `values (…)` partial.)
|
|
// Validation failures already propagate as a separate
|
|
// branch below.
|
|
propagated @ (NodeWalkResult::Incomplete { .. } | NodeWalkResult::Failed { .. }) => {
|
|
propagated
|
|
}
|
|
}
|
|
}
|
|
|
|
fn merge_expected(dst: &mut Vec<Expectation>, src: Vec<Expectation>) {
|
|
for e in src {
|
|
if !dst.contains(&e) {
|
|
dst.push(e);
|
|
}
|
|
}
|
|
}
|