Skip to content

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

@steveluc

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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions