Files
rdbms-playground/src/dsl/grammar/app.rs
T
claude@clouddev1 911a537a83 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.
2026-05-15 21:58:22 +00:00

279 lines
8.5 KiB
Rust

//! App-lifecycle command nodes (ADR-0024 §migration Phase A).
//!
//! Eleven commands: quit, help, rebuild, save (+ save as), new,
//! load, export, import, mode, messages.
//!
//! Each block is one `CommandNode`: entry keyword, shape, AST
//! builder, help / usage references. The ast_builders match
//! against the `MatchedPath` items in declaration order.
use crate::dsl::command::{AppCommand, Command, MessagesValue, ModeValue};
use crate::dsl::grammar::{
CommandNode, HintMode, IdentSource, IdentValidator, Node, ValidationError,
Word,
};
use crate::dsl::walker::outcome::{MatchedKind, MatchedPath};
// --- Validators ----------------------------------------------------
//
// The catch-all `Ident` branches in `mode <value>` /
// `messages <value>` exist solely to convert any out-of-set
// identifier into a friendly `mode.unknown` / `messages.unknown`
// catalog wording. The known values are `Word` siblings in the
// same `Choice`, so they're never reached on the happy path —
// these validators always fail.
fn validate_unknown_mode(value: &str) -> Result<(), ValidationError> {
Err(ValidationError {
message_key: "mode.unknown",
args: vec![("value", value.to_string())],
})
}
fn validate_unknown_messages(value: &str) -> Result<(), ValidationError> {
Err(ValidationError {
message_key: "messages.unknown",
args: vec![("value", value.to_string())],
})
}
const UNKNOWN_MODE_VALIDATOR: IdentValidator = validate_unknown_mode;
const UNKNOWN_MESSAGES_VALIDATOR: IdentValidator = validate_unknown_messages;
// --- Shapes (constants are referenced by Optional/Choice slices) --
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")),
IMPORT_TARGET,
]);
const IMPORT_AS_TARGET_OPT: Node = Node::Optional(&IMPORT_AS_TARGET);
const IMPORT_PATH_AND_TARGET: Node = Node::Seq(&[Node::BarePath, IMPORT_AS_TARGET_OPT]);
const EXPORT_PATH_OPT: Node = Node::Optional(&Node::BarePath);
const IMPORT_BODY_OPT: Node = Node::Optional(&IMPORT_PATH_AND_TARGET);
// `mode <value>`: known keywords are surfaced as `Word` children
// so they appear in the walker's expected set (and feed the
// completion engine's keyword candidates). The trailing `Ident`
// child catches any other identifier shape and funnels it into
// the friendly `mode.unknown` validator.
const MODE_CHOICES: &[Node] = &[
Node::Word(Word::keyword("simple")),
Node::Word(Word::keyword("advanced")),
Node::Ident {
source: IdentSource::Free,
role: "mode_value",
validator: Some(UNKNOWN_MODE_VALIDATOR),
highlight_override: None,
writes_table: false,
writes_column: false,
writes_user_listed_column: false,
},
];
const MODE_VALUE: Node = Node::Choice(MODE_CHOICES);
const MESSAGES_CHOICES: &[Node] = &[
Node::Word(Word::keyword("short")),
Node::Word(Word::keyword("verbose")),
Node::Ident {
source: IdentSource::Free,
role: "messages_value",
validator: Some(UNKNOWN_MESSAGES_VALIDATOR),
highlight_override: None,
writes_table: false,
writes_column: false,
writes_user_listed_column: false,
},
];
const MESSAGES_VALUE: Node = Node::Choice(MESSAGES_CHOICES);
const MESSAGES_VALUE_OPT: Node = Node::Optional(&MESSAGES_VALUE);
const EMPTY_SEQ: Node = Node::Seq(&[]);
const SAVE_AS_OPT: Node = Node::Optional(&SAVE_AS_WORD);
// --- AST builders --------------------------------------------------
const fn build_quit(_path: &MatchedPath) -> Result<Command, ValidationError> {
Ok(Command::App(AppCommand::Quit))
}
const fn build_help(_path: &MatchedPath) -> Result<Command, ValidationError> {
Ok(Command::App(AppCommand::Help))
}
const fn build_rebuild(_path: &MatchedPath) -> Result<Command, ValidationError> {
Ok(Command::App(AppCommand::Rebuild))
}
fn build_save(path: &MatchedPath) -> Result<Command, ValidationError> {
if path.contains_word("as") {
Ok(Command::App(AppCommand::SaveAs))
} else {
Ok(Command::App(AppCommand::Save))
}
}
const fn build_new(_path: &MatchedPath) -> Result<Command, ValidationError> {
Ok(Command::App(AppCommand::New))
}
const fn build_load(_path: &MatchedPath) -> Result<Command, ValidationError> {
Ok(Command::App(AppCommand::Load))
}
fn build_export(path: &MatchedPath) -> Result<Command, ValidationError> {
let bare = path
.find(|i| matches!(i.kind, MatchedKind::BarePath))
.map(|i| i.text.clone());
Ok(Command::App(AppCommand::Export { path: bare }))
}
fn build_import(path: &MatchedPath) -> Result<Command, ValidationError> {
let bare_path = path
.find(|i| matches!(i.kind, MatchedKind::BarePath))
.map(|i| i.text.clone())
.unwrap_or_default();
let target = path
.find(|i| matches!(&i.kind, MatchedKind::Ident { role } if *role == "target"))
.map(|i| i.text.clone());
Ok(Command::App(AppCommand::Import {
path: bare_path,
target,
}))
}
fn build_mode(path: &MatchedPath) -> Result<Command, ValidationError> {
// The Choice surfaces the matched value as either a `Word`
// (known) or an `Ident` (unknown). The unknown branch's
// validator always errors, so reaching the AST builder
// implies one of the Word branches matched.
let value = if path.contains_word("simple") {
ModeValue::Simple
} else if path.contains_word("advanced") {
ModeValue::Advanced
} else {
ModeValue::Simple
};
Ok(Command::App(AppCommand::Mode { value }))
}
fn build_messages(path: &MatchedPath) -> Result<Command, ValidationError> {
let value = if path.contains_word("short") {
Some(MessagesValue::Short)
} else if path.contains_word("verbose") {
Some(MessagesValue::Verbose)
} else {
None
};
Ok(Command::App(AppCommand::Messages { value }))
}
// --- Command nodes -------------------------------------------------
pub static QUIT: CommandNode = CommandNode {
entry: Word::keyword("quit"),
shape: EMPTY_SEQ,
ast_builder: build_quit,
help_id: Some("app.quit"),
usage_ids: &["parse.usage.quit"],
hint_mode: None,
};
pub static HELP: CommandNode = CommandNode {
entry: Word::keyword("help"),
shape: EMPTY_SEQ,
ast_builder: build_help,
help_id: Some("app.help"),
usage_ids: &["parse.usage.help"],
hint_mode: None,
};
pub static REBUILD: CommandNode = CommandNode {
entry: Word::keyword("rebuild"),
shape: EMPTY_SEQ,
ast_builder: build_rebuild,
help_id: Some("app.rebuild"),
usage_ids: &["parse.usage.rebuild"],
hint_mode: None,
};
pub static SAVE: CommandNode = CommandNode {
entry: Word::keyword("save"),
shape: SAVE_AS_OPT,
ast_builder: build_save,
help_id: Some("app.save"),
usage_ids: &["parse.usage.save"],
hint_mode: None,
};
pub static NEW: CommandNode = CommandNode {
entry: Word::keyword("new"),
shape: EMPTY_SEQ,
ast_builder: build_new,
help_id: Some("app.new"),
usage_ids: &["parse.usage.new"],
hint_mode: None,
};
pub static LOAD: CommandNode = CommandNode {
entry: Word::keyword("load"),
shape: EMPTY_SEQ,
ast_builder: build_load,
help_id: Some("app.load"),
usage_ids: &["parse.usage.load"],
hint_mode: None,
};
pub static EXPORT: CommandNode = CommandNode {
entry: Word::keyword("export"),
shape: EXPORT_PATH_OPT,
ast_builder: build_export,
help_id: Some("app.export"),
usage_ids: &["parse.usage.export"],
hint_mode: None,
};
pub static IMPORT: CommandNode = CommandNode {
entry: Word::keyword("import"),
shape: IMPORT_BODY_OPT,
ast_builder: build_import,
help_id: Some("app.import"),
usage_ids: &["parse.usage.import"],
hint_mode: None,
};
pub static MODE: CommandNode = CommandNode {
entry: Word::keyword("mode"),
shape: MODE_VALUE,
ast_builder: build_mode,
help_id: Some("app.mode"),
usage_ids: &["parse.usage.mode"],
hint_mode: None,
};
pub static MESSAGES: CommandNode = CommandNode {
entry: Word::keyword("messages"),
shape: MESSAGES_VALUE_OPT,
ast_builder: build_messages,
help_id: Some("app.messages"),
usage_ids: &["parse.usage.messages"],
hint_mode: None,
};