Summary
In packages/actionGrammar, an optional rule-reference <Rule>? causes the matcher to silently fail: the NFA consumes all input tokens but never reaches an accepting state, so matchNFA returns matched: false. The equivalent inline optional group (... )? works correctly, as does a required (non-optional) rule-reference. So the defect is specific to the combination optional + rule-reference.
Minimal repro
import { loadGrammarRulesNoThrow, compileGrammarToNFA, matchNFA, registerBuiltInEntities } from "action-grammar";
registerBuiltInEntities();
function run(label, text) {
const errs = [];
const g = loadGrammarRulesNoThrow("r.agr", text, errs);
const nfa = compileGrammarToNFA(g, "r");
const toks = "add a to the b playlist".split(" ");
const r = matchNFA(nfa, toks);
console.log(label.padEnd(22), "matched:", r.matched, "consumed:", r.tokensConsumed, "/", toks.length);
}
// FAILS — optional rule-reference
run("optional rule-ref", `<Start> = add $(x:wildcard) to <Owner>? $(y:wildcard) playlist -> { actionName:"a", parameters:{ x, y } };
<Owner> = the | my ;`);
// WORKS — inline optional group
run("inline optional group", `<Start> = add $(x:wildcard) to (the | my)? $(y:wildcard) playlist -> { actionName:"a", parameters:{ x, y } };`);
// WORKS — required rule-reference
run("required rule-ref", `<Start> = add $(x:wildcard) to <Owner> $(y:wildcard) playlist -> { actionName:"a", parameters:{ x, y } };
<Owner> = the | my ;`);
Output
optional rule-ref matched: false consumed: 6 / 6
inline optional group matched: true consumed: 6 / 6
required rule-ref matched: true consumed: 6 / 6
Expected
All three grammars should match add a to the b playlist (with Owner matching the). The optional rule-reference form should behave identically to the inline optional group.
Observed
The optional rule-reference form fails: the input is fully consumed (consumed: 6 / 6) but matched is false and actionValue is undefined — i.e. the path reaches the end of input but no accepting state, suggesting the optionality (epsilon skip) of a <Rule>? reference is not wired into the NFA accept path the way an inline (...)? group is.
Workaround
Replace <Rule>? with an inline optional group ( a | b )?, or make the reference required.
Notes
- Affects the NFA interpreter path (
matchNFA / nfaInterpreter.ts); not yet checked against the DFA path.
- Found while building
examples/snipsBench (a SNIPS slot-filling benchmark over action-grammar); the hand-authored grammars hit this and switched to inline optional groups.
Environment
packages/actionGrammar @ current main
- Node 22+, the engine's default
matchNFA path
Summary
In
packages/actionGrammar, an optional rule-reference<Rule>?causes the matcher to silently fail: the NFA consumes all input tokens but never reaches an accepting state, somatchNFAreturnsmatched: false. The equivalent inline optional group(... )?works correctly, as does a required (non-optional) rule-reference. So the defect is specific to the combination optional + rule-reference.Minimal repro
Output
Expected
All three grammars should match
add a to the b playlist(withOwnermatchingthe). The optional rule-reference form should behave identically to the inline optional group.Observed
The optional rule-reference form fails: the input is fully consumed (
consumed: 6 / 6) butmatchedisfalseandactionValueisundefined— i.e. the path reaches the end of input but no accepting state, suggesting the optionality (epsilon skip) of a<Rule>?reference is not wired into the NFA accept path the way an inline(...)?group is.Workaround
Replace
<Rule>?with an inline optional group( a | b )?, or make the reference required.Notes
matchNFA/nfaInterpreter.ts); not yet checked against the DFA path.examples/snipsBench(a SNIPS slot-filling benchmark over action-grammar); the hand-authored grammars hit this and switched to inline optional groups.Environment
packages/actionGrammar@ currentmainmatchNFApath