Walker: node-attached HintMode via Node::Hinted (ADR-0024 §HintMode-per-node)

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.
This commit is contained in:
claude@clouddev1
2026-05-15 21:58:22 +00:00
parent f1ff5970bf
commit 911a537a83
8 changed files with 193 additions and 165 deletions
+44 -122
View File
@@ -95,15 +95,19 @@ pub fn hint_resolution_at_input(
source: &str,
schema: Option<&crate::completion::SchemaCache>,
) -> Option<HintResolution> {
use crate::dsl::grammar::{HintMode, IdentSource};
use crate::dsl::walker::outcome::Expectation;
use crate::dsl::grammar::HintMode;
let snap = expected_for_hint_snapshot(source, schema);
// Empty expected set means the command is already complete
// (`WalkOutcome::Match`) — no slot to hint at.
if snap.expected.is_empty() {
return None;
}
let expected = snap.expected;
// Typed value slot: the walker tagged `pending_value_type`
// on entry to a `Node::TypedValueSlot`. Per-column-type
// prose, narrowed by the column's user-facing type, plus
// the Form B auto-gen pedagogical note.
if let Some(ty) = snap.pending_value_type {
return Some(HintResolution {
mode: HintMode::ProseOnly(catalog_key_for_value_type(ty)),
@@ -117,42 +121,23 @@ pub fn hint_resolution_at_input(
});
}
let has_word = |w: &str| {
expected
.iter()
.any(|e| matches!(e, Expectation::Word(x) if *x == w))
};
let value_literal_slot = has_word("null")
&& has_word("true")
&& has_word("false")
&& expected.iter().any(|e| matches!(e, Expectation::NumberLit))
&& expected.iter().any(|e| matches!(e, Expectation::StringLit));
if value_literal_slot {
return Some(HintResolution {
mode: HintMode::ProseOnly("hint.value_literal_slot"),
column: None,
form_b_autogen_skipped: Vec::new(),
});
// Node-attached HintMode (ADR-0024 §HintMode-per-node): the
// grammar declares the mode at the slot via `Node::Hinted`;
// the walker recorded it in `pending_hint_mode`. The hint
// resolver reads it directly — no signature-matching on the
// shape of the expected set. `ProseOnly` covers the
// value-literal fallback slot; `ForceProse` covers `NewName`
// ident slots ("Type a name").
match snap.pending_hint_mode {
Some(mode @ (HintMode::ProseOnly(_) | HintMode::ForceProse(_))) => {
Some(HintResolution {
mode,
column: None,
form_b_autogen_skipped: Vec::new(),
})
}
Some(HintMode::SuppressProse | HintMode::Default) | None => None,
}
let new_name_slot = expected.iter().any(|e| {
matches!(
e,
Expectation::Ident {
source: IdentSource::NewName,
..
}
)
});
if new_name_slot {
return Some(HintResolution {
mode: HintMode::ForceProse("hint.ambient_typing_name"),
column: None,
form_b_autogen_skipped: Vec::new(),
});
}
None
}
/// Auto-generated columns a Form B insert skips from its value
@@ -206,68 +191,12 @@ fn hint_mode_at_input_inner(
source: &str,
schema: Option<&crate::completion::SchemaCache>,
) -> Option<crate::dsl::grammar::HintMode> {
use crate::dsl::grammar::{HintMode, IdentSource};
use crate::dsl::walker::outcome::Expectation;
// Hint mode is only meaningful at *required* slot positions
// (Incomplete / Mismatch outcomes). For complete commands
// (Match), `tail_expected` may carry optional-suffix
// expectations — completion surfaces those as Tab
// candidates, but the hint resolver should stay silent so
// we don't push prose like "Type a name" at the end of a
// valid command.
let (expected, pending_value_type) = expected_for_hint_with_ctx(source, schema);
if expected.is_empty() {
return None;
}
// Typed value slot at the cursor: the walker tagged
// ctx.pending_value_type on entry to the slot but did not
// clear it (no inner literal matched). Emit per-type prose.
if let Some(ty) = pending_value_type {
return Some(HintMode::ProseOnly(catalog_key_for_value_type(ty)));
}
// Value-literal slot signature: all five forms present.
let has_word = |w: &str| {
expected
.iter()
.any(|e| matches!(e, Expectation::Word(x) if *x == w))
};
let value_literal_slot = has_word("null")
&& has_word("true")
&& has_word("false")
&& expected.iter().any(|e| matches!(e, Expectation::NumberLit))
&& expected.iter().any(|e| matches!(e, Expectation::StringLit));
if value_literal_slot {
// Fallback prose: lists every literal form with format
// examples. Fires when the walker can't resolve a column
// type at the cursor (schemaless caller, missing table,
// unknown column).
return Some(HintMode::ProseOnly("hint.value_literal_slot"));
}
// NewName ident slot: user invents a name.
let new_name_slot = expected.iter().any(|e| {
matches!(
e,
Expectation::Ident {
source: IdentSource::NewName,
..
}
)
});
if new_name_slot {
// The "Type a name" prose key is selected by the
// ambient_hint dispatch — the `ForceProse` key here is
// a stable identifier the resolver maps to one of the
// two variants (`hint.ambient_typing_name` /
// `hint.ambient_typing_name_then`) depending on whether
// a next-token probe yields content.
return Some(HintMode::ForceProse("hint.ambient_typing_name"));
}
None
// Single source of truth: `hint_resolution_at_input` already
// resolves the slot's HintMode (typed-value-slot per-type
// prose, or the node-attached `Node::Hinted` annotation).
// This thin wrapper just drops the resolution's column /
// skip detail for callers that only need the mode.
hint_resolution_at_input(source, schema).map(|r| r.mode)
}
const fn catalog_key_for_value_type(ty: crate::dsl::types::Type) -> &'static str {
@@ -421,6 +350,9 @@ struct HintWalkSnapshot {
expected: Vec<outcome::Expectation>,
pending_value_type: Option<crate::dsl::types::Type>,
pending_value_column: Option<String>,
/// The grammar-declared `HintMode` at the cursor's slot
/// (`Node::Hinted` annotation, ADR-0024 §HintMode-per-node).
pending_hint_mode: Option<crate::dsl::grammar::HintMode>,
current_table_columns: Option<Vec<crate::completion::TableColumn>>,
/// `Some` when the input used Form A's explicit column list.
/// `None` for Form B (`insert into T values …`) and for
@@ -428,14 +360,6 @@ struct HintWalkSnapshot {
user_listed_columns: Option<Vec<String>>,
}
fn expected_for_hint_with_ctx(
source: &str,
schema: Option<&crate::completion::SchemaCache>,
) -> (Vec<outcome::Expectation>, Option<crate::dsl::types::Type>) {
let snap = expected_for_hint_snapshot(source, schema);
(snap.expected, snap.pending_value_type)
}
fn expected_for_hint_snapshot(
source: &str,
schema: Option<&crate::completion::SchemaCache>,
@@ -449,27 +373,24 @@ fn expected_for_hint_snapshot(
.collect()
};
let empty_snapshot = || HintWalkSnapshot {
expected: entry_words(),
pending_value_type: None,
pending_value_column: None,
pending_hint_mode: None,
current_table_columns: None,
user_listed_columns: None,
};
if source.trim().is_empty() {
return HintWalkSnapshot {
expected: entry_words(),
pending_value_type: None,
pending_value_column: None,
current_table_columns: None,
user_listed_columns: None,
};
return empty_snapshot();
}
let mut ctx = schema.map_or_else(context::WalkContext::new, |s| {
context::WalkContext::with_schema(s)
});
let (result, _cmd) = walk(source, outcome::WalkBound::EndOfInput, &mut ctx);
let Some(result) = result else {
return HintWalkSnapshot {
expected: entry_words(),
pending_value_type: None,
pending_value_column: None,
current_table_columns: None,
user_listed_columns: None,
};
return empty_snapshot();
};
let expected = match result.outcome {
outcome::WalkOutcome::Match { .. } | outcome::WalkOutcome::ValidationFailed { .. } => {
@@ -482,6 +403,7 @@ fn expected_for_hint_snapshot(
expected,
pending_value_type: ctx.pending_value_type,
pending_value_column: ctx.pending_value_column,
pending_hint_mode: ctx.pending_hint_mode,
current_table_columns: ctx.current_table_columns,
user_listed_columns: ctx.user_listed_columns,
}