Repository navigation
fix: start-state merging in MergeEquivalentStates and nested unbounded repetitions - #911
Merged
Seven-Streams merged 2 commits intoSep 20, 2026
Merged
Conversation
Signed-off-by: yuchuan <yuchuan.7streams@gmail.com>
Signed-off-by: yuchuan <yuchuan.7streams@gmail.com>
Seven-Streams
requested review from
DarkSharpness and
Ubospica
as code owners
September 20, 2026 19:36
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two correctness bugs in the grammar compilation pipeline, both reachable from ordinary EBNF, regex and JSON-schema input on
main. They were found by diffing the regular-grammar flattener from #901 againstGrammarMatcher; the flattener was right both times and the matcher side was wrong.1.
MergeEquivalentStatesmerges the start state away (over-acceptance)The "equivalent successor" rule merges two states that have the same single predecessor, the same incoming labels and the same end-ness. When the start state's only incoming edge is its own self-loop, it passes that test, and a state reached from the start state by the loop label looks equivalent to it. But the start state is also entered without taking any edge, so the merged automaton accepts input that never consumed the looped class. Three-state automata escape only because of the
NumStates() < 4early return.root ::= [a-z]* [a-z] "c" "d"acceptscd;root ::= [a-z]* [a-z] [0-9] [0-9]accepts12.{"type": "string", "pattern": "^a*acd$"}accepts"cd", through the regex FSM builder.Fix: skip the start state in that rule, as candidate and as sibling. The "equivalent precursor" rule is unaffected, since states with identical outgoing edges have the same future however they are entered.
2. Nested unbounded repetitions reject everything (under-acceptance)
RepetitionRangeExpanderinlines a single-element rule into the repetition that references it, but copied the element verbatim instead of visiting it. Forroot ::= b{1,2},b ::= a{2,}the inlineda{2,-1}was therefore never expanded into recursion, an unboundedkRepeatedge reached the parser, and the parser'srepeat_count >= Upper()check against-1never let it enter the repeated rule.(a{2,}){1,2},(a{2,}){2},(a{1,}){1,2}and the EBNF form above reject every input.Fix: visit the inlined element (
VisitExpr) so nested repetitions go through the expander like any other.Validation
XGrammarFSMTest.MergeEquivalentStatesKeepsStartStateSeparate(hand-built four-state NFA),test_character_class_star_followed_by_same_class(EBNF and schema-pattern paths), andtest_bounded_repetition_of_unbounded_repetition(four regex cases plus the EBNF form). All fail before their fix.xgrammar_test78/78. Fulltests/pythonwithHF_TOKEN: 4851 passed, 3 skipped, 0 failed.((a{2,}){1,2}){1,2},(a{2,}b{1,}){1,3},(a{130,}){1,2}) agree with Python'sre.mainbuild on the same host, alternating rounds. Compile time and mask latency for the builtin JSON grammar, a 1024-char bounded string, an email regex, a schemapattern, 300 bounded strings, 400 properties withmax_whitespace_cntand large repetitions are all within run-to-run noise; the compiled artifacts being identical, the fixes add only a per-state comparison in the merge pass and one visitor dispatch per inlined element.