ADR-0024 Phase D: per-column-type hint prose at value slots
The Phase D commit landed parse-time validation but not the
user-facing payoff — per-column-type hints. Typing
`insert into Customers values (` rightfully expected a hint
like "Type an integer (e.g. 42, -7) or null" at an int column.
This commit closes that gap.
End-to-end:
**`Node::TypedValueSlot { ty, inner }`** (new variant in
`src/dsl/grammar/mod.rs`):
- Walker walks `inner` to consume the literal but tags
`WalkContext::pending_value_type = Some(ty)` on entry, then
clears it on a successful inner match. Positions BETWEEN
slots (`insert into T values (1` mid-input) thus don't carry
a stale hint type.
**Typed slot factories wrapped in `TypedValueSlot`**
(`src/dsl/grammar/shared.rs`):
- `INT_SLOT`, `REAL_SLOT`, `DECIMAL_SLOT`, `BOOL_SLOT`,
`TEXT_SLOT`, `DATE_SLOT`, `DATETIME_SLOT`, `BLOB_SLOT`,
`SERIAL_SLOT`, `SHORTID_SLOT` — each pairs an inner literal
Choice with its `Type` so the walker can tag context.
- `slot_for_type(ty)` dispatches to the appropriate constant.
- Bug fix: `ShortId` previously dispatched to `INT_SLOT` (a
pre-Phase-D holdover from the chumsky-side generic
fallback). `shortid` columns store base58 text (ADR-0011
fk_target_type shortid → text); the corrected slot accepts
`StringLit` or `null`.
**Schema-aware hint resolver** (`src/dsl/walker/mod.rs`):
- `hint_mode_at_input_with_schema(source, &SchemaCache) ->
Option<HintMode>` is the new public entry point. Reads
`pending_value_type` from the walker's WalkContext and
emits `HintMode::ProseOnly("hint.value_slot_<type>")` —
one per Type.
- The schemaless `hint_mode_at_input(source)` falls back to
the generic `hint.value_literal_slot` at value-literal slots
(no per-type narrowing without a schema).
- `catalog_key_for_value_type(ty)` is the type → key
dispatcher.
**Catalog entries** (`src/friendly/strings/en-US.yaml`,
`src/friendly/keys.rs`):
- 10 new `hint.value_slot_<type>` keys with per-type prose:
- int/serial → "Type an integer (e.g. 42, -7) or null"
- real/decimal → "Type a number (e.g. 3.14, -0.5) or null"
- bool → "Type true, false, or null"
- text → "Type a quoted string (e.g. 'Alice') or null"
- date → "Type a quoted date as 'YYYY-MM-DD' or null"
- datetime → "Type a quoted datetime as 'YYYY-MM-DD
HH:MM:SS' or null"
- blob → "Type a quoted blob literal or null"
- shortid → "Type a quoted shortid (or omit to auto-generate)
or null"
**Ambient-hint dispatch** (`src/input_render.rs::ambient_hint`):
- Passes the SchemaCache through to
`hint_mode_at_input_with_schema`, so the live hint panel
surfaces per-column-type prose as the user types into a
value slot.
Tests:
- 8 walker-side tests cover insert / update / where typed-slot
hint dispatch, mid-value no-stale-hint behaviour, and a
full-coverage routing matrix for every `Type` variant.
- 4 input_render integration tests cover the end-to-end
ambient_hint path: insert first/second value, update set
value, and the schemaless fallback to generic prose.
Tests: 842 passing, 0 failing, 1 ignored. Clippy clean.
For the user: typing `insert into Customers values (` against
a Customers table whose first column is `id:int` now shows
"Type an integer (e.g. 42, -7) or null" in the hint panel,
replacing the previous generic value-literal prose. After
typing `1, `, the panel updates to whatever the second column
requires — "Type a quoted string (e.g. 'Alice') or null"
for text, "Type a quoted date as 'YYYY-MM-DD'" for date, etc.
This commit is contained in:
@@ -281,6 +281,20 @@ pub enum Node {
|
||||
/// Phase D+ uses this for `column_value_list`.
|
||||
#[allow(dead_code)]
|
||||
DynamicSubgrammar(fn(&WalkContext) -> Self),
|
||||
/// Typed value-literal slot (ADR-0024 §Phase D §typed-value-slots).
|
||||
///
|
||||
/// Walks `inner` to consume the literal but records the
|
||||
/// column type in `WalkContext::pending_value_type` so the
|
||||
/// hint resolver can emit per-type catalog prose ("Type an
|
||||
/// integer", "Type a date as 'YYYY-MM-DD'", …) at empty
|
||||
/// prefix at this slot. The recorded type clears on a
|
||||
/// successful inner match — so positions BETWEEN typed
|
||||
/// slots (`insert into T values (1` mid-input) don't carry
|
||||
/// a stale hint type.
|
||||
TypedValueSlot {
|
||||
ty: crate::dsl::types::Type,
|
||||
inner: &'static Self,
|
||||
},
|
||||
}
|
||||
|
||||
/// Top-level entry record. One per command. The `entry` keyword
|
||||
|
||||
+56
-14
@@ -186,10 +186,22 @@ const INT_SLOT_CHOICES: &[Node] = &[
|
||||
},
|
||||
NULL_WORD,
|
||||
];
|
||||
const INT_SLOT: Node = Node::Choice(INT_SLOT_CHOICES);
|
||||
const INT_SLOT_INNER: Node = Node::Choice(INT_SLOT_CHOICES);
|
||||
const INT_SLOT: Node = Node::TypedValueSlot {
|
||||
ty: Type::Int,
|
||||
inner: &INT_SLOT_INNER,
|
||||
};
|
||||
const SERIAL_SLOT: Node = Node::TypedValueSlot {
|
||||
ty: Type::Serial,
|
||||
inner: &INT_SLOT_INNER,
|
||||
};
|
||||
|
||||
const REAL_SLOT_CHOICES: &[Node] = &[Node::NumberLit { validator: None }, NULL_WORD];
|
||||
const REAL_SLOT: Node = Node::Choice(REAL_SLOT_CHOICES);
|
||||
const REAL_SLOT_INNER: Node = Node::Choice(REAL_SLOT_CHOICES);
|
||||
const REAL_SLOT: Node = Node::TypedValueSlot {
|
||||
ty: Type::Real,
|
||||
inner: &REAL_SLOT_INNER,
|
||||
};
|
||||
|
||||
const DECIMAL_SLOT_CHOICES: &[Node] = &[
|
||||
Node::NumberLit {
|
||||
@@ -197,26 +209,54 @@ const DECIMAL_SLOT_CHOICES: &[Node] = &[
|
||||
},
|
||||
NULL_WORD,
|
||||
];
|
||||
const DECIMAL_SLOT: Node = Node::Choice(DECIMAL_SLOT_CHOICES);
|
||||
const DECIMAL_SLOT_INNER: Node = Node::Choice(DECIMAL_SLOT_CHOICES);
|
||||
const DECIMAL_SLOT: Node = Node::TypedValueSlot {
|
||||
ty: Type::Decimal,
|
||||
inner: &DECIMAL_SLOT_INNER,
|
||||
};
|
||||
|
||||
const BOOL_SLOT_CHOICES: &[Node] = &[
|
||||
Node::Word(Word::keyword("true")),
|
||||
Node::Word(Word::keyword("false")),
|
||||
NULL_WORD,
|
||||
];
|
||||
const BOOL_SLOT: Node = Node::Choice(BOOL_SLOT_CHOICES);
|
||||
const BOOL_SLOT_INNER: Node = Node::Choice(BOOL_SLOT_CHOICES);
|
||||
const BOOL_SLOT: Node = Node::TypedValueSlot {
|
||||
ty: Type::Bool,
|
||||
inner: &BOOL_SLOT_INNER,
|
||||
};
|
||||
|
||||
const TEXT_SLOT_CHOICES: &[Node] = &[Node::StringLit, NULL_WORD];
|
||||
const TEXT_SLOT: Node = Node::Choice(TEXT_SLOT_CHOICES);
|
||||
const TEXT_SLOT_INNER: Node = Node::Choice(TEXT_SLOT_CHOICES);
|
||||
const TEXT_SLOT: Node = Node::TypedValueSlot {
|
||||
ty: Type::Text,
|
||||
inner: &TEXT_SLOT_INNER,
|
||||
};
|
||||
|
||||
const DATE_SLOT_CHOICES: &[Node] = &[Node::StringLit, NULL_WORD];
|
||||
const DATE_SLOT: Node = Node::Choice(DATE_SLOT_CHOICES);
|
||||
|
||||
const DATETIME_SLOT_CHOICES: &[Node] = &[Node::StringLit, NULL_WORD];
|
||||
const DATETIME_SLOT: Node = Node::Choice(DATETIME_SLOT_CHOICES);
|
||||
|
||||
const BLOB_SLOT_CHOICES: &[Node] = &[Node::StringLit, NULL_WORD];
|
||||
const BLOB_SLOT: Node = Node::Choice(BLOB_SLOT_CHOICES);
|
||||
// Date / datetime share the StringLit-or-null shape with text
|
||||
// but get distinct catalog-prose entries so the hint surfaces
|
||||
// the YYYY-MM-DD / YYYY-MM-DD HH:MM:SS format examples.
|
||||
const DATE_SLOT: Node = Node::TypedValueSlot {
|
||||
ty: Type::Date,
|
||||
inner: &TEXT_SLOT_INNER,
|
||||
};
|
||||
const DATETIME_SLOT: Node = Node::TypedValueSlot {
|
||||
ty: Type::DateTime,
|
||||
inner: &TEXT_SLOT_INNER,
|
||||
};
|
||||
const BLOB_SLOT: Node = Node::TypedValueSlot {
|
||||
ty: Type::Blob,
|
||||
inner: &TEXT_SLOT_INNER,
|
||||
};
|
||||
// shortid columns store base58 text (ADR-0011 fk_target_type
|
||||
// shortid → text); the slot accepts a quoted-text literal or
|
||||
// null. The pre-Phase-D plain-Choice scaffolding had this
|
||||
// mapped to INT_SLOT (a holdover from the chumsky-side generic
|
||||
// VALUE_LITERAL fallback). Per-type dispatch corrects that.
|
||||
const SHORTID_SLOT: Node = Node::TypedValueSlot {
|
||||
ty: Type::ShortId,
|
||||
inner: &TEXT_SLOT_INNER,
|
||||
};
|
||||
|
||||
/// Dispatch a value slot per user-facing type
|
||||
/// (ADR-0024 §slot_for_type). Returns the same node every time
|
||||
@@ -225,7 +265,9 @@ const BLOB_SLOT: Node = Node::Choice(BLOB_SLOT_CHOICES);
|
||||
#[must_use]
|
||||
pub const fn slot_for_type(ty: Type) -> Node {
|
||||
match ty {
|
||||
Type::Int | Type::Serial | Type::ShortId => INT_SLOT,
|
||||
Type::Int => INT_SLOT,
|
||||
Type::Serial => SERIAL_SLOT,
|
||||
Type::ShortId => SHORTID_SLOT,
|
||||
Type::Real => REAL_SLOT,
|
||||
Type::Decimal => DECIMAL_SLOT,
|
||||
Type::Bool => BOOL_SLOT,
|
||||
|
||||
Reference in New Issue
Block a user