Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ts/packages/actionGrammar/src/builtInGrammarCategories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* the stored grammar is fully self-contained.
*
* Naming convention for prompt use: <CategoryName>
* Usage in patterns: (<CategoryName>)? (note: (<Name>)? not <Name>? — bare optional not yet supported)
* Usage in patterns: (<CategoryName>)? or bare <CategoryName>?
*/
export interface BuiltInGrammarCategory {
/** AGR rule name — used as <Name> in patterns */
Expand Down
50 changes: 35 additions & 15 deletions ts/packages/actionGrammar/src/grammarCompiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1318,29 +1318,45 @@ function createGrammarRule(
// match time — no rule definition needed, no NFA state expansion.
// BUT: only use the phrase-set if the rule is NOT defined locally
// or via import (preserves grammars that define their own <Polite> etc.)
const { optional, repeat } = expr;
const isLocallyDefined =
context.ruleDefMap.has(expr.refName.name) ||
context.importedRuleMap.has(expr.refName.name);
if (
!isLocallyDefined &&
globalPhraseSetRegistry.isPhraseSetName(expr.refName.name)
) {
parts.push(
createPhraseSetPart(
expr.refName.name,
undefined,
allocPartId(
context,
expr.pos,
`<${expr.refName.name}>`,
),
const phrasePart = createPhraseSetPart(
expr.refName.name,
undefined,
allocPartId(
context,
expr.pos,
`<${expr.refName.name}>`,
),
);
// PhraseSetPart cannot carry optional/repeat; wrap so bare
// <Polite>? / <Polite>* / <Polite>+ match grouped form.
if (optional || repeat) {
parts.push(
createRulesPart([{ parts: [phrasePart] }], {
optional,
repeat,
partId: allocPartId(
context,
expr.pos,
`<${expr.refName.name}>${repeat ? (optional ? "*" : "+") : "?"}`,
),
}),
);
} else {
parts.push(phrasePart);
}
// Phrase sets don't produce a captured value on their own.
// Use defaultValue=true so single-part rules using a phrase set
// don't trip the "Start rule does not produce a value" check.
defaultValue = true;
consumedInput(); // phrase sets always consume input
if (!optional) consumedInput(); // required / + still consume
break;
}
const record = createNamedGrammarRules(
Expand All @@ -1355,21 +1371,25 @@ function createGrammarRule(
parts.push(
createRulesPart(record.grammarRules, {
name: expr.refName.name,
optional,
repeat,
partId: allocPartId(
context,
expr.pos,
`<${expr.refName.name}>`,
),
}),
);
// RuleRefExpr has no optional modifier; it is always non-optional.
// Optional / * rule refs can be skipped — do not force non-null.
// === false: only clear when *definitely* non-nullable (same
// asymmetry as the variable ruleRef case above).
if (record.nullable === false) {
currentEpr = new Set();
if (!optional) {
if (record.nullable === false) {
currentEpr = new Set();
}
// ?? false: treat undefined (back-ref) as non-nullable.
ruleNullable = ruleNullable && (record.nullable ?? false);
}
// ?? false: treat undefined (back-ref) as non-nullable.
ruleNullable = ruleNullable && (record.nullable ?? false);
break;
}
case "rules": {
Expand Down
18 changes: 17 additions & 1 deletion ts/packages/actionGrammar/src/grammarRuleParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ const debugParse = registerDebug("typeagent:grammar:parse");
* // TODO: Support nested instead of just Rule Ref
* <VariableSpecifier> ::= <VarName> (":" (<TypeName> | <RuleName>))?
*
* <RuleRefExpr> ::= <RuleName>
* <RuleRefExpr> ::= <RuleName> ( "?" | "*" | "+" )?
* <GroupExpr> ::= "(" <Rules> ( ")" | ")?" | ")*" | ")+" )
*
* // ── Value (basic mode: enableValueExpressions=false) ──────────────────────────
Expand Down Expand Up @@ -231,6 +231,8 @@ export type CommentedName = {
export type RuleRefExpr = {
type: "ruleReference";
refName: CommentedName;
optional?: boolean | undefined;
repeat?: boolean | undefined; // Kleene star/plus: zero-or-more / one-or-more
pos?: number | undefined;
leadingComments?: Comment[] | undefined;
};
Expand Down Expand Up @@ -802,6 +804,20 @@ class GrammarRuleParser implements ValueExprParserContext {
refName: this.parseRuleName(),
pos,
};
// Bare quantifiers on rule refs: <Name>?, <Name>*, <Name>+
// (equivalent to (<Name>)?, (<Name>)*, (<Name>)+).
// Without this, "?" is parsed as a literal string part.
if (this.isAt("?")) {
node.optional = true;
this.skipWhitespace(1);
} else if (this.isAt("*")) {
node.optional = true;
node.repeat = true;
this.skipWhitespace(1);
} else if (this.isAt("+")) {
node.repeat = true;
this.skipWhitespace(1);
}
attach(node);
expNodes.push(node);
continue;
Expand Down
5 changes: 5 additions & 0 deletions ts/packages/actionGrammar/src/grammarRuleWriter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -923,6 +923,11 @@ function writeSingleExpr(
}
case "ruleReference":
writeBracketedName(result, expr.refName);
if (expr.repeat) {
result.write(expr.optional ? "*" : "+");
} else if (expr.optional) {
result.write("?");
}
break;
case "rules": {
result.write("(");
Expand Down
Loading
Loading