PAYATHON 2026

自定义编程语言的 parser 为何报 T_RCURPAR 错误?

排障赵师傅

结论

unexpected T_RCURPAR, expecting ';' 表示 parser 收到了 T_RCURPAR,但当前解析状态仍在等待分号。按给出的括号规则,这通常意味着读到 } 时,循环体内的语句还没有解析完整。

给出的规则没有要求在 } 和 all_set 之间添加分号。如果 assignment 不包含分号,lexer 也正确返回了各个 token,下面的结构就符合这些规则:

go
as_long_as(a>5){ a->5; }
all_set;

缺少 assignment、expression 和 lexer 的完整定义,目前还无法确定具体原因。优先检查内外两层规则是否都要求分号,以及 lexer 是否把循环体中的 ; 返回给了 parser。

为什么错误指向 },而不是 all_set

循环体的规则是:

while_block:
    T_AS_LONG_AS T_LPAREN expression T_RPAREN
    T_LCURPAR statements T_RCURPAR
;

其中,statements 要求每条语句后都有分号:

statements:
    statement ';'
  | statements statement ';'
;

解析 a->5; } 时,正常过程应当是:

  1. a->5 归约为 assignment,再归约为 statement。
  2. ; 与 statement 一起组成 statements。
  3. } 结束循环体。
  4. all_set 结束外层 block。
  5. 如果顶层也使用这套语句列表规则,最后的 ; 就满足外层 statements 对 statement ';' 的要求。

报错说明,parser 当时无法接受的前瞻 token 是 T_RCURPAR。即使日志看起来已经输出到了 all_set,也不能据此认定错误发生在 all_set 上,需要核对 parser 的实际 trace。

优先检查分号是否被重复消费

假设未展示的 assignment 已经包含分号:

/* 仅作结构示例,符号名称以实际项目为准。 */
assignment:
    identifier T_ASSIGN expression ';'
;

外层又要求:

statement:
    assignment
;

statements:
    statement ';'
;

这样,一条赋值语句就需要两个分号:

identifier T_ASSIGN expression ';' ';'

输入为:

a->5; }

第一个分号由 assignment 消费。parser 随后读到 },却还在等待 statements 要求的第二个分号,因此可能报告:

unexpected T_RCURPAR, expecting ';'

确认是这个问题后,应统一分号的归属。沿用当前设计,让 statements 处理分号,assignment、declaration、function_call 等规则只解析语句主体。给输入补上第二个分号会掩盖规则本身的问题。

用 token 日志和解析状态定位

核对 lexer 实际返回的 token

用下面的最小输入记录 lexer 实际返回的 token。只打印匹配到的文本,还不足以确认 token 是否正确返回:

go
as_long_as(a>5){ a->5; }
all_set;

重点检查末尾的顺序:

赋值语句的 token
';'
T_RCURPAR
T_ALL_SET
';'

逐项核对:

  • lexer 没有把 ; 当作可忽略字符跳过。
  • 文法使用字符 token ';' 时,lexer 返回的也是 ';',而不是另一个独立定义的分号 token。
  • } 返回 T_RCURPAR,all_set 返回 T_ALL_SET。
  • lexer 使用本次生成的 parser 头文件,避免手工定义或旧头文件导致 token 编号不一致。

如果 token 序列正确,再检查 parser 为什么没有按预期归约。

查看 Bison trace 和状态报告

使用 Bison 的常见 C skeleton 时,可以按项目文件名调整并执行:

bison -Wall -d -v -t parser.y

重新编译后,在调用 yyparse() 前启用调试输出:

yydebug = 1;
yyparse();

其他 skeleton 或封装接口的启用方式可能不同。

在 trace 中找到报错前的状态编号,再查看生成的 .output 状态报告。如果状态中出现类似项目:

statements: statement • ';'

就说明 parser 已识别出一条 statement,正在等待后面的分号。结合此前的移进和归约记录,可以判断输入中的分号是否已被下层规则消费,或根本没有到达 parser。

如果 Bison 报告 shift/reduce 或 reduce/reduce 冲突,还需要检查冲突涉及的具体产生式。仅凭这条错误信息无法判断是否存在冲突,也不能直接用优先级声明修复。

修正规则时保留现有代码块结构

让 statements 统一处理分号时,可以保留以下规则。语义动作中当前产生式的返回值也应写为 $$:

block:
    T_GO control_block T_ALL_SET
    {
        $$ = node1(_CONTROLBLOCK, $2);
    }
;

while_block:
    T_AS_LONG_AS T_LPAREN expression T_RPAREN
    T_LCURPAR statements T_RCURPAR
    {
        $$ = node2(_WHILE, $3, $6);
    }
;

statements:
    statement ';'
    {
        $$ = $1;
    }
  | statements statement ';'
    {
        $$ = node2(_STATEMENTS, $1, $2);
    }
;

statement:
    declaration
    {
        $$ = node1(_DECLARATION, $1);
    }
  | assignment
    {
        $$ = node1(_ASSIGNMENT, $1);
    }
  | function_call
    {
        $$ = $1;
    }
  | block
    {
        $$ = $1;
    }
  | T_SENDBACK expression
    {
        $$ = node1(_RETURN, $2);
    }
  | T_THROWUP expression
    {
        $$ = node1(_PRINT, $2);
    }
;

这些规则仍依赖项目原有的其他产生式、语义类型声明和 AST 构造函数。

如果 .y 文件中确实存在原代码里的写法:

$ = node1(...);

需要改为:

$$ = node1(...);

Bison 用 $$ 表示当前产生式左部的语义值,用 $1、$2 等访问右部符号的语义值。这个问题需要单独修正,但通常不能解释 parser 为什么正在等待分号。

验证与注意事项

先用单条赋值验证分号规则:

a->5;

再验证最小循环:

go
as_long_as(a>5){ a->5; }
all_set;

最后恢复完整输入,便于区分问题出在赋值语句本身、代码块组合,还是前面的其他语句影响了解析状态。

另外:

  • 当前 statements 至少包含一条语句,因此不接受空循环体 {}。只有语言设计允许空块时,才需要增加可空的语句列表规则。
  • 如果多行输入始终报告 line 1,应检查 lexer 的换行计数和错误报告使用的位置信息。行号错误会妨碍定位,但不会直接导致 parser 在这里等待分号。

备注:内容仅供参考。