oobabooga/textgen · warning · RuntimeError
expecting ')' at
Error message
expecting ')' at {remaining_src} What it means
Raised in parse_sequence (modules/grammar/grammar_utils.py:197): after parsing the alternates inside a '( ... )' group, the next character must be ')'. If the group's contents consumed everything (or junk follows), the closing paren is missing and the parser reports exactly where it stalled.
Solutions
- Balance parentheses — every '(' needs a matching ')' before the next operator/newline.
- Check the echoed remaining_src: it shows precisely where the ')' should go.
- Extract complex groups into named subrules instead of deep nesting; this makes missing parens obvious.
Example fix
// before
root ::= ("a" | "b"
// after
root ::= ("a" | "b")
Defensive patterns
Strategy: validation
Validate before calling
def parens_balanced(grammar: str) -> bool:
depth = 0
for line in grammar.splitlines():
code = line.split('#', 1)[0] # strip comments
depth += code.count('(') - code.count(')')
if depth < 0:
return False
if depth != 0 and not code.rstrip().endswith(('|', '(', '#')):
return False
return depth == 0
Try / catch
try:
parse_grammar(grammar_text)
except RuntimeError as e:
if 'expecting' in str(e):
raise ValueError(f'GBNF syntax error near: {str(e)[:120]}') from None
raise
Prevention
- Balance parentheses per rule line; GBNF groups do not span rules.
- Avoid '#' comments between a group's last alternative and its closing paren.
- Flatten nested groups into named subrules to reduce paren errors.
When it happens
Trigger: Grammars like root ::= ("a" | "b" with no ')'; nested groups where one paren is misplaced: (("a"|"b"); or a comment '#...' swallowed the closing paren because remove_leading_white_space treats '#' as comment-to-end-of-line.
Common situations: Hand-balancing complex optional groups; editors auto-deleting the paren; comments placed between the last alternative and the ')' being silently skipped so a typo'd closer never appears.
Related errors
AI-assisted analysis of oobabooga/textgen@ed888c71f2 (2026-08-15).
Data as JSON: /api/errors/09135ca0064180bd.
Report an issue: GitHub.
Appendix: source
Thrown at modules/grammar/grammar_utils.py:197
remaining_src = remove_leading_white_space(remaining_src[1:], is_nested)
elif is_word_char(remaining_src[0]): # rule reference
name, remaining_src = parse_name(remaining_src)
ref_rule_id = get_symbol_id(state, name)
remaining_src = remove_leading_white_space(remaining_src, is_nested)
last_sym_start = len(outbuf)
outbuf.append(REF_RULE_MARKER)
outbuf.append(ref_rule_id)
elif remaining_src[0] == "(": # grouping
# parse nested alternates into synthesized rule
remaining_src = remove_leading_white_space(remaining_src[1:], True)
sub_rule_id = generate_symbol_id(state, rule_name)
remaining_src = parse_alternates(state, remaining_src, rule_name, sub_rule_id, True)
last_sym_start = len(outbuf)
# output reference to synthesized rule
outbuf.append(REF_RULE_MARKER)
outbuf.append(sub_rule_id)
if remaining_src[0] != ")":
raise RuntimeError("expecting ')' at " + remaining_src)
remaining_src = remove_leading_white_space(remaining_src[1:], is_nested)
elif remaining_src[0] in ("*", "+", "?"): # repetition operator
if len(outbuf) - out_start_pos - 1 == 0:
raise RuntimeError("expecting preceeding item to */+/? at " + remaining_src)
out_grammar = state.grammar_encoding
# apply transformation to previous symbol (last_sym_start -
# end) according to rewrite rules:
# S* --> S' ::= S S' |
# S+ --> S' ::= S S' | S
# S? --> S' ::= S |
sub_rule_id = generate_symbol_id(state, rule_name)
out_grammar.append(sub_rule_id)
sub_rule_start = len(out_grammar)
# placeholder for size of 1st alternate
out_grammar.append(TO_BE_FILLED_MARKER)
# add preceding symbol to generated rule
out_grammar.extend(outbuf[last_sym_start:])View on GitHub (pinned to ed888c71f2)