Skip to content

actionGrammar: optional rule-reference <Rule>? silently fails to match (inline optional group works) #2461

Description

@steveluc

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions