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

  1. Balance parentheses — every '(' needs a matching ')' before the next operator/newline.
  2. Check the echoed remaining_src: it shows precisely where the ')' should go.
  3. 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

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.