{"record":{"id":"09135ca0064180bd","repo":"oobabooga/textgen","slug":"expecting-at-remaining-src","errorCode":null,"errorMessage":"expecting ')' at {remaining_src}","messagePattern":"expecting '\\)' at (.+?)","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"warning","filePath":"modules/grammar/grammar_utils.py","lineNumber":197,"sourceCode":"            remaining_src = remove_leading_white_space(remaining_src[1:], is_nested)\n        elif is_word_char(remaining_src[0]):  # rule reference\n            name, remaining_src = parse_name(remaining_src)\n            ref_rule_id = get_symbol_id(state, name)\n            remaining_src = remove_leading_white_space(remaining_src, is_nested)\n            last_sym_start = len(outbuf)\n            outbuf.append(REF_RULE_MARKER)\n            outbuf.append(ref_rule_id)\n        elif remaining_src[0] == \"(\":  # grouping\n            # parse nested alternates into synthesized rule\n            remaining_src = remove_leading_white_space(remaining_src[1:], True)\n            sub_rule_id = generate_symbol_id(state, rule_name)\n            remaining_src = parse_alternates(state, remaining_src, rule_name, sub_rule_id, True)\n            last_sym_start = len(outbuf)\n            # output reference to synthesized rule\n            outbuf.append(REF_RULE_MARKER)\n            outbuf.append(sub_rule_id)\n            if remaining_src[0] != \")\":\n                raise RuntimeError(\"expecting ')' at \" + remaining_src)\n            remaining_src = remove_leading_white_space(remaining_src[1:], is_nested)\n        elif remaining_src[0] in (\"*\", \"+\", \"?\"):  # repetition operator\n            if len(outbuf) - out_start_pos - 1 == 0:\n                raise RuntimeError(\"expecting preceeding item to */+/? at \" + remaining_src)\n            out_grammar = state.grammar_encoding\n\n            # apply transformation to previous symbol (last_sym_start -\n            # end) according to rewrite rules:\n            # S* --> S' ::= S S' |\n            # S+ --> S' ::= S S' | S\n            # S? --> S' ::= S |\n            sub_rule_id = generate_symbol_id(state, rule_name)\n            out_grammar.append(sub_rule_id)\n            sub_rule_start = len(out_grammar)\n            # placeholder for size of 1st alternate\n            out_grammar.append(TO_BE_FILLED_MARKER)\n            # add preceding symbol to generated rule\n            out_grammar.extend(outbuf[last_sym_start:])","sourceCodeStart":179,"sourceCodeEnd":215,"githubUrl":"https://github.com/oobabooga/textgen/blob/ed888c71f221df552750e1834b3654abab8ae345/modules/grammar/grammar_utils.py#L179-L215","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nroot ::= (\"a\" | \"b\"\n\n// after\nroot ::= (\"a\" | \"b\")\n","handlingStrategy":"validation","validationCode":"def parens_balanced(grammar: str) -> bool:\n    depth = 0\n    for line in grammar.splitlines():\n        code = line.split('#', 1)[0]  # strip comments\n        depth += code.count('(') - code.count(')')\n        if depth < 0:\n            return False\n        if depth != 0 and not code.rstrip().endswith(('|', '(', '#')):\n            return False\n    return depth == 0\n","typeGuard":null,"tryCatchPattern":"try:\n    parse_grammar(grammar_text)\nexcept RuntimeError as e:\n    if 'expecting' in str(e):\n        raise ValueError(f'GBNF syntax error near: {str(e)[:120]}') from None\n    raise\n","preventionTips":["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."],"tags":["grammar","gbnf","syntax-error","grouping"],"backgroundTag":null,"analyzedSha":"ed888c71f221df552750e1834b3654abab8ae345","analyzedAt":"2026-08-15T05:24:21.000Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}