oobabooga/textgen · warning · RuntimeError
expecting ')' at {remaining_src}
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.
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)
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
- expecting name at {src}
- unknown escape at {src}
- unexpected end of input
- expecting ::= at {remaining_src}
- unknown hex char {c}
AI-assisted analysis of oobabooga/textgen@ed888c71f2 (2026-08-15).
Data as JSON: /api/errors/09135ca0064180bd.
Report an issue: GitHub.