5.4 Pratt 表达式分析
递归下降非常适合声明、语句和成对分隔符;表达式却常常变得棘手:前缀运算符、后缀调用、成员访问、下标、优先级、结合性、三元运算符和自定义运算符,会让 parseTerm、parseFactor、parsePrimary 组成的层级越来越脆弱。普拉特语法分析器(Pratt parser)让词法单元(token)携带少量解析行为,并以 binding power 驱动表达式构造。
普拉特解析仍属于自顶向下分析,不是语法分析器生成器,也不能省掉词法分析器(lexer)或 AST 设计。它的核心问题是:“已经解析出的左侧表达式,是否应该被下一个词法单元以足够高的强度继续扩展?”
Prefix 与 Infix Parselet
一个词法单元可以承担两种角色:
- prefix parselet(常称
nud,null denotation)处理表达式开头:number、identifier、括号表达式、前缀-、!。
- infix/postfix parselet(常称
led,left denotation)处理已经有左表达式后的扩展:+、*、调用(、下标[、成员访问.。
核心过程很紧凑:
parseExpression(minBP = 0): Expr {
const token = advance();
let left = prefixParselet(token).parse(this, token);
while (minBP < bindingPower(peek().kind)) {
const operator = advance();
left = infixParselet(operator).parse(this, left, operator);
}
return left;
}比较方式可以是 minBP < nextBP,也可以是用一对 left/right binding power。选择一种约定并写清楚;混用是结合性 bug 的常见根源。没有 prefix parselet 的词法单元应报出定位的“这里需要表达式”;没有 infix parselet 的词法单元若 binding power 为零,会自然结束当前表达式,因此 )、]、,、;、EOF 很自然地成为分隔符。
优先级与结合性是数据,不是散落的条件分支
把运算符绑定强度集中在表里,而不是分散到无关函数:
lowest: assignment =
10: conditional ?:
20: equality == !=
30: comparison < <= > >=
40: sum + -
50: product * /
60: prefix - !
70: call () , index [] , member .左结合运算符解析右操作数时,应给一个阈值,阻止同级运算符进入右子树。a - b - c 因而得到 (a - b) - c。右结合运算符如 ** 或赋值则允许同级进入右侧,得到 a ** (b ** c) 或 a = (b = c)。
一个常见写法是用一对数值:
left-associative +: leftBP = 40, rightBP = 41
right-associative **: leftBP = 60, rightBP = 60解析右侧时调用 parseExpression(rightBP)。+ 的一格差值让同级 + 不能进入右操作数;** 的相等值允许它进入。具体数字可以任意,真正编码语言含义的是相对大小与相等时的规则。
与其他语法分析器结构组合
实际前端(frontend)常用递归下降处理程序整体结构,只把表达式委托给普拉特:
parseIfStatement() {
expect(IF, "expected 'if'");
const condition = parseExpression();
const thenBranch = parseBlock();
const elseBranch = match(ELSE) ? parseBlock() : null;
return IfStmt(condition, thenBranch, elseBranch);
}调用和下标最能显示 Pratt 的可扩展性。先把 f 解析为 identifier 后,紧随其后的 ( 以较高 binding power 解析实参,构成 Call(f, args);第二个 ( 又能继续扩展,得到 f()(x)。随后的 [ 也可把同一个左表达式扩展为下标访问。这正符合程序员阅读 postfix chain 的直觉:它们比二元算术结合得更紧,而且可以连续出现。
错误处理仍需要主动设计。infix 运算符后缺少右操作数时,应在正确范围报告“* 后需要表达式”,不要只说下一个分隔符奇怪。同时要明确每个上下文的表达式终止词法单元;逗号会结束调用实参表达式,但在其他位置不一定合法。
跟踪一个 binding-power 例子
分析 a + b * c 时先得到 a;+ 可以扩展它,于是右侧按 + 的阈值解析。右侧看到更高 power 的 *,先抓住 b/c,结果是 Add(a, Multiply(b,c))。没有给乘法写 special case,规则只是一致比较 binding power。
新增运算符前,先决定 prefix/infix/postfix 角色、left/right power、AST node、右操作数的终止词法单元;一张小表加两个 witness test 比散落的优先级条件可靠。