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
+17 -10
View File
@@ -9,7 +9,8 @@
use crate::dsl::command::{AppCommand, Command, MessagesValue, ModeValue};
use crate::dsl::grammar::{
CommandNode, IdentSource, IdentValidator, Node, ValidationError, Word,
CommandNode, HintMode, IdentSource, IdentValidator, Node, ValidationError,
Word,
};
use crate::dsl::walker::outcome::{MatchedKind, MatchedPath};
@@ -43,17 +44,23 @@ const UNKNOWN_MESSAGES_VALIDATOR: IdentValidator = validate_unknown_messages;
const SAVE_AS_WORD: Node = Node::Word(Word::keyword("as"));
const IMPORT_TARGET_IDENT: Node = Node::Ident {
source: IdentSource::NewName,
role: "target",
validator: None,
highlight_override: None,
writes_table: false,
writes_column: false,
writes_user_listed_column: false,
};
const IMPORT_TARGET: Node = Node::Hinted {
mode: HintMode::ForceProse("hint.ambient_typing_name"),
inner: &IMPORT_TARGET_IDENT,
};
const IMPORT_AS_TARGET: Node = Node::Seq(&[
Node::Word(Word::keyword("as")),
Node::Ident {
source: IdentSource::NewName,
role: "target",
validator: None,
highlight_override: None,
writes_table: false,
writes_column: false,
writes_user_listed_column: false,
},
IMPORT_TARGET,
]);
const IMPORT_AS_TARGET_OPT: Node = Node::Optional(&IMPORT_AS_TARGET);
+10 -2
View File
@@ -18,7 +18,7 @@
use crate::dsl::command::{Command, RowFilter};
use crate::dsl::grammar::{
CommandNode, IdentSource, Node, ValidationError, Word,
CommandNode, HintMode, IdentSource, Node, ValidationError, Word,
shared::{column_value_list, current_column_value},
};
use crate::dsl::value::Value;
@@ -61,7 +61,15 @@ const VALUE_LITERAL_CHOICES: &[Node] = &[
Node::NumberLit { validator: None },
Node::StringLit,
];
const VALUE_LITERAL: Node = Node::Choice(VALUE_LITERAL_CHOICES);
const VALUE_LITERAL_INNER: Node = Node::Choice(VALUE_LITERAL_CHOICES);
/// Value-literal slot with the `ProseOnly` HintMode
/// (ADR-0024 §HintMode-per-node) — the hint resolver surfaces
/// the generic "Type a value: …" prose rather than the
/// misleading `null`/`true`/`false` candidate trio.
const VALUE_LITERAL: Node = Node::Hinted {
mode: HintMode::ProseOnly("hint.value_literal_slot"),
inner: &VALUE_LITERAL_INNER,
};
// =================================================================
// show — `show (data|table) <T>`
+52 -22
View File
@@ -14,9 +14,15 @@
use crate::dsl::action::ReferentialAction;
use crate::dsl::command::{ChangeColumnMode, ColumnSpec, Command, RelationshipSelector};
use crate::dsl::grammar::{
CommandNode, IdentSource, Node, ValidationError, Word,
CommandNode, HintMode, IdentSource, Node, ValidationError, Word,
shared::{REFERENTIAL_CLAUSES, TYPE_SLOT, TYPE_VALIDATOR},
};
/// `HintMode` annotation shared by every `NewName` ident slot:
/// the user is inventing a name, so the hint panel forces the
/// "Type a name [then …]" prose rather than offering schema
/// candidates (ADR-0024 §HintMode-per-node).
const NEW_NAME_HINT: HintMode = HintMode::ForceProse("hint.ambient_typing_name");
use crate::dsl::types::Type;
use crate::dsl::walker::outcome::{MatchedKind, MatchedPath};
@@ -24,7 +30,7 @@ use crate::dsl::walker::outcome::{MatchedKind, MatchedPath};
// Building blocks
// =================================================================
const TABLE_NAME_NEW: Node = Node::Ident {
const TABLE_NAME_NEW_IDENT: Node = Node::Ident {
source: IdentSource::NewName,
role: "table_name",
validator: None,
@@ -33,6 +39,10 @@ const TABLE_NAME_NEW: Node = Node::Ident {
writes_column: false,
writes_user_listed_column: false,
};
const TABLE_NAME_NEW: Node = Node::Hinted {
mode: NEW_NAME_HINT,
inner: &TABLE_NAME_NEW_IDENT,
};
// `writes_table: true` so that the column-name slots that
// follow the table name in `drop column` / `rename column` /
@@ -61,7 +71,7 @@ const COLUMN_NAME: Node = Node::Ident {
writes_user_listed_column: false,
};
const COLUMN_NAME_NEW: Node = Node::Ident {
const COLUMN_NAME_NEW_IDENT: Node = Node::Ident {
source: IdentSource::NewName,
role: "column_name",
validator: None,
@@ -70,6 +80,10 @@ const COLUMN_NAME_NEW: Node = Node::Ident {
writes_column: false,
writes_user_listed_column: false,
};
const COLUMN_NAME_NEW: Node = Node::Hinted {
mode: NEW_NAME_HINT,
inner: &COLUMN_NAME_NEW_IDENT,
};
const RELATIONSHIP_NAME: Node = Node::Ident {
source: IdentSource::Relationships,
@@ -81,7 +95,7 @@ const RELATIONSHIP_NAME: Node = Node::Ident {
writes_user_listed_column: false,
};
const RELATIONSHIP_NAME_NEW: Node = Node::Ident {
const RELATIONSHIP_NAME_NEW_IDENT: Node = Node::Ident {
source: IdentSource::NewName,
role: "relationship_name",
validator: None,
@@ -90,6 +104,10 @@ const RELATIONSHIP_NAME_NEW: Node = Node::Ident {
writes_column: false,
writes_user_listed_column: false,
};
const RELATIONSHIP_NAME_NEW: Node = Node::Hinted {
mode: NEW_NAME_HINT,
inner: &RELATIONSHIP_NAME_NEW_IDENT,
};
// `[to]` and `[table]` connectives.
const TO_OPT: Node = Node::Optional(&Node::Word(Word::keyword("to")));
@@ -308,6 +326,20 @@ const ADD_SHAPE: Node = Node::Choice(ADD_CHOICES);
// rename_column — `rename column [in] [table] <T> : <col> to <new>`
// =================================================================
const NEW_COLUMN_NAME_IDENT: Node = Node::Ident {
source: IdentSource::NewName,
role: "new_column_name",
validator: None,
highlight_override: None,
writes_table: false,
writes_column: false,
writes_user_listed_column: false,
};
const NEW_COLUMN_NAME: Node = Node::Hinted {
mode: NEW_NAME_HINT,
inner: &NEW_COLUMN_NAME_IDENT,
};
const RENAME_COLUMN_NODES: &[Node] = &[
Node::Word(Word::keyword("column")),
IN_OPT,
@@ -316,15 +348,7 @@ const RENAME_COLUMN_NODES: &[Node] = &[
Node::Punct(':'),
COLUMN_NAME,
Node::Word(Word::keyword("to")),
Node::Ident {
source: IdentSource::NewName,
role: "new_column_name",
validator: None,
highlight_override: None,
writes_table: false,
writes_column: false,
writes_user_listed_column: false,
},
NEW_COLUMN_NAME,
];
const RENAME_COLUMN: Node = Node::Seq(RENAME_COLUMN_NODES);
@@ -650,16 +674,22 @@ pub static CHANGE: CommandNode = CommandNode {
// (Phase C)
// =================================================================
const COL_NAME_IDENT: Node = Node::Ident {
source: IdentSource::NewName,
role: "col_name",
validator: None,
highlight_override: None,
writes_table: false,
writes_column: false,
writes_user_listed_column: false,
};
const COL_NAME: Node = Node::Hinted {
mode: NEW_NAME_HINT,
inner: &COL_NAME_IDENT,
};
const COL_SPEC_NODES: &[Node] = &[
Node::Ident {
source: IdentSource::NewName,
role: "col_name",
validator: None,
highlight_override: None,
writes_table: false,
writes_column: false,
writes_user_listed_column: false,
},
COL_NAME,
Node::Punct(':'),
Node::Ident {
source: IdentSource::Types,
+18
View File
@@ -308,6 +308,24 @@ pub enum Node {
column_name: Option<&'static str>,
inner: &'static Self,
},
/// Annotates `inner` with a hint-panel `HintMode` (ADR-0024
/// §HintMode-per-node). On entry the walker records `mode`
/// in `WalkContext::pending_hint_mode`; on a successful
/// inner match the record clears (so positions past the
/// slot don't carry stale hint state). Transparent to
/// matching, highlighting and the expected-set otherwise —
/// it walks `inner` and returns its result verbatim.
///
/// This is the node-attached replacement for the hint
/// resolver's earlier signature-matching: the grammar tree
/// declares the hint mode at the slot, the walker
/// propagates it, the resolver reads it. Used by the
/// value-literal fallback slot (`ProseOnly`) and `NewName`
/// ident slots (`ForceProse`).
Hinted {
mode: HintMode,
inner: &'static Self,
},
}
/// Top-level entry record. One per command. The `entry` keyword
+12 -9
View File
@@ -7,7 +7,8 @@
use crate::completion::TableColumn;
use crate::dsl::grammar::{
IdentSource, IdentValidator, Node, NumberValidator, ValidationError, Word,
HintMode, IdentSource, IdentValidator, Node, NumberValidator,
ValidationError, Word,
};
use crate::dsl::types::Type;
use crate::dsl::walker::context::WalkContext;
@@ -347,7 +348,16 @@ const FALLBACK_VALUE_LITERAL_CHOICES: &[Node] = &[
Node::NumberLit { validator: None },
Node::StringLit,
];
const FALLBACK_VALUE_LITERAL: Node = Node::Choice(FALLBACK_VALUE_LITERAL_CHOICES);
const FALLBACK_VALUE_LITERAL_INNER: Node = Node::Choice(FALLBACK_VALUE_LITERAL_CHOICES);
/// The schemaless value-literal slot. The `ProseOnly` HintMode
/// (ADR-0024 §HintMode-per-node) tells the hint resolver to
/// surface the generic "Type a value: number, 'text', …" prose
/// here rather than the misleading `null`/`true`/`false`
/// candidate trio.
const FALLBACK_VALUE_LITERAL: Node = Node::Hinted {
mode: HintMode::ProseOnly("hint.value_literal_slot"),
inner: &FALLBACK_VALUE_LITERAL_INNER,
};
const FALLBACK_VALUE_LIST: Node = Node::Repeated {
inner: &FALLBACK_VALUE_LITERAL,
@@ -434,10 +444,3 @@ pub fn column_value_list(ctx: &WalkContext) -> Node {
}
Node::Seq(Box::leak(children.into_boxed_slice()))
}
// The HintMode / NumberValidator imports are part of the Phase D
// typed-slot toolkit even though only NumberValidator is used by
// the explicit validators above; surface HintMode so future
// per-type prose annotations can attach without re-importing.
#[allow(dead_code)]
const _USES_HINT_MODE: Option<crate::dsl::grammar::HintMode> = None;