Skip to content

feat(action-grammar): treat ?/*/+ as special quantifier chars - #2789

Draft
datduyng wants to merge 6 commits into
mainfrom
domnguyen/agr-quantifier-special-chars
Draft

feat(action-grammar): treat ?/*/+ as special quantifier chars#2789
datduyng wants to merge 6 commits into
mainfrom
domnguyen/agr-quantifier-special-chars

Conversation

@datduyng

@datduyng datduyng commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Summary

Make ? / * / + true special characters in AGR pattern expressions (CurtisM proposal).

Rule Behavior
After ) or > postfix quantifier: optional / zero-or-more / one-or-more
Anywhere else bare parse error (not a lint warning)
Literal punctuation must escape: \? \* \+
Prettier always rewrites <Name>?(<Name>)? (same meaning, clearer)
Colon : stays unescaped (only meaningful inside $())
Quotes "…" / '…' ordinary match characters, not pattern string syntax
Value side after -> unchanged (? ternary, ?., ?? still work)

Before → After (realistic)

1) Question utterance — trailing ? is punctuation

Intent: match the spoken request what is the time?

// BEFORE — bare ? was a normal character glued onto the word
<Start> = what is the time? -> { actionName: "getTime" };
// matched:  "what is the time?"
// AFTER — bare ? is illegal; escape the literal question mark
<Start> = what is the time\? -> { actionName: "getTime" };
// matched:  "what is the time?"
// parse error:  what is the time?     (forgot the backslash)
// NOT how you write a "quoted string" in patterns —
// quotes are match characters, so this looks for quote glyphs in the utterance:
//   <Start> = "what is the time?" -> …
// matches:  "what is the time?"   ← includes the " characters
// misses:   what is the time?

2) Required song name + real question mark

Intent: who sings song Hello? (song name required)

// BEFORE — <Song> required; trailing ? was literal punctuation
<Song> = hello | goodbye | bohemian rhapsody;
<Start> = who sings song <Song>? -> {
    actionName: "lookupSong",
    parameters: { song: Song }
};
// ✅ "who sings song hello?"
// ❌ "who sings song?"          (missing song)
// AFTER — same structure; literal ? must be escaped
<Song> = hello | goodbye | bohemian rhapsody;
<Start> = who sings song <Song>\? -> {
    actionName: "lookupSong",
    parameters: { song: Song }
};
// ✅ "who sings song hello?"
// ❌ "who sings song?"
// ❌ "who sings song hello"     (missing ?)

3) Optional song name + real question mark

Intent: both who sings song? and who sings song Hello?

// BEFORE — must group the optional rule; ? after ) is quantifier;
//          another ? (or \?) is the punctuation
<Song> = hello | goodbye;
<Start> = who sings song (<Song>)? \? -> {
    actionName: "lookupSong",
    parameters: { song }
};
// AFTER — optional via <Song>? or prettier (<Song>)?;
//          punctuation always escaped
<Song> = hello | goodbye;

// RIGHT — optional Song + literal ?
<Start> = who sings song (<Song>)?\? -> {
    actionName: "lookupSong",
    parameters: { song }
};
// ✅ "who sings song?"
// ✅ "who sings song hello?"

// WRONG if you wanted the above — this is REQUIRED Song + "?"
//   who sings song <Song>\?

Silent pitfall (document + tests lock this):

// Looks like a question, but under the new rules it means:
//   optional <Song>, NO literal "?" in the pattern
<Start> = who sings song <Song>? -> { actionName: "lookupSong" };

// matches:  "who sings song"
// matches:  "who sings song hello"
// does NOT require a trailing "?" in the pattern
// (matcher may still accept trailing utterance punct via flex-space)

4) Optional polite prefix (how authors already write grammars)

// BEFORE — <Polite>? was BROKEN (became literal "?" after the rule name)
// authors often wrote this by mistake; it did not make Polite optional
<Polite> = please | can you | would you;
<Start> = <Polite>? open outlook -> {
    actionName: "openApp",
    parameters: { app: "outlook" }
};
// was: required <Polite> + literal "?"   ❌ not optional
// AFTER — <Polite>? is optional (same as (<Polite>)?)
<Polite> = please | can you | would you;
<Start> = <Polite>? open outlook -> {
    actionName: "openApp",
    parameters: { app: "outlook" }
};

// ✅ "open outlook"
// ✅ "please open outlook"
// ✅ "can you open outlook"
// Prettier / writer / LSP format always prefers the grouped form (same meaning):
//   <Polite>? open outlook
// becomes:
//   (<Polite>)? open outlook

5) Groups and captures (unchanged shapes)

// These already worked and stay the same
<Start> =
    (the | a)? item -> { actionName: "pick" }
  | measure $(units:word)? -> { actionName: "measure", parameters: { units } }
  | tag (bug | feature)* -> { actionName: "tag" }
  | need (reviewer)+ -> { actionName: "needReview" }
  | mute (notifications)? -> { actionName: "mute" }
  ;
// NEW — bare word/string + ? is a hard error (was a common silent bug)
//   pause the? music        → PARSE ERROR  (use (the)? )
//   "please"?               → PARSE ERROR  (quotes are not string syntax)
//   'really?'               → PARSE ERROR  (bare ? inside)
//   one* two                → PARSE ERROR
// Captures only support )? today; * / + need the group form
//   measure $(u:word)*      → PARSE ERROR
//   measure ($(u:word))+    → OK

6) Full realistic start-rule sketch

<Polite> = please | can you | would you;
<App> = outlook | teams | spotify;
<Owner> = alice | bob | carol;
<Song> = hello | goodbye | bohemian rhapsody;
<Label> = bug | feature | docs;

<Start> =
    <Polite>? open <App>
        -> { actionName: "openApp", parameters: { app: App } }

  | what is the time\?
        -> { actionName: "getTime" }

  | who sings song <Song>\?
        -> { actionName: "lookupSong", parameters: { song: Song } }

  | who sings song (<Song>)?\?
        -> { actionName: "lookupSongOptional", parameters: { song } }

  | show <Owner>? files
        -> { actionName: "showFiles", parameters: { owner } }

  | tag <Label>+
        -> { actionName: "tag", parameters: { labels: Label } }

  | mute (notifications)?
        -> { actionName: "mute" }
  ;

After prettier / writeGrammarRules / LSP format:

<Polite> = please | can you | would you;
<App> = outlook | teams | spotify;
<Owner> = alice | bob | carol;
<Song> = hello | goodbye | bohemian rhapsody;
<Label> = bug | feature | docs;
<Start> =
    (<Polite>)? open <App> -> { actionName: "openApp", parameters: { app: App } }
  | what is the time\? -> { actionName: "getTime" }
  | who sings song <Song>\? -> { actionName: "lookupSong", parameters: { song: Song } }
  | who sings song (<Song>)?\? -> {
        actionName: "lookupSongOptional",
        parameters: { song }
    }
  | show (<Owner>)? files -> { actionName: "showFiles", parameters: { owner } }
  | tag (<Label>)+ -> { actionName: "tag", parameters: { labels: Label } }
  | mute (notifications)? -> { actionName: "mute" };

7) Unchanged on purpose

// Colon is NOT special in patterns (no ambiguity outside $() type position)
<Start> [spacing=optional] = $(h:number) : $(m:number) -> { h, m };

// import * is not a pattern quantifier
import * from "./shared.agr";

// Value-side ? / ?. / ?? after -> are not pattern quantifiers
<Start> = $(h:number) pm -> { hours: h < 12 ? h + 12 : h };
<Start> = lookup $(obj:word) -> obj.value?.name;
<Start> = get $(x:word) -> x ?? "default";

datduyng and others added 6 commits August 3, 2026 13:58
- Postfix ?/*/+ are quantifiers only after ")" or ">"
- Bare ?/*/+ are parse errors; literals require \? / \* / \+
- <Name>?/*/+ parse as optional/repeat (equiv. to grouped form)
- Writer/prettier prefers (<Name>)? over bare <Name>?
- Colon stays unescaped (no ambiguity outside $())
- Quotes remain literal match chars (not string syntax)
- Corpus + sample.agr + fuzz/generator escapes updated
- Tests cover CurtisM proposal cases and match semantics
- Prettier: lock bare <Name>?/*/+ → (<Name>)?/*/+ rewrite (writer + grammar-tools format)
- Generator prompts: fix \\? so runtime teaches real escapes (not bare ?)
- Sync agentSdkWrapper schema→grammar prompt with quantifier rules
- Capture $(x)*/$(x)+: actionable error pointing at ($(x))+ form
- Phrase-set wrap inherits parent spacingMode (bare ≡ grouped lowering)
- Docs/tests: silent <Song>? pitfall, import *, value ?. / ??, CORRECT-line guards
- Match standalone \? on NFA/DFA by peeling trailing sentence punct
- Grammar matcher: expand PhraseSetPart so bare <Polite>? works
- Wire grammarStore DFA path with request context for punct peel
- Extension schema→grammar prompts teach quantifier specials
- scenarioBasedGenerator: escape ?/*/+ in shared verb categories
- Expand quantifierSpecialChars NFA/DFA + prompt runtime guards
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant