自定义编程语言的 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; } 时,正常过程应当是:
a->5归约为assignment,再归约为statement。;与statement一起组成statements。}结束循环体。all_set结束外层block。- 如果顶层也使用这套语句列表规则,最后的
;就满足外层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 在这里等待分号。
备注:内容仅供参考。
