如何编写适配PEST prec climber的无左递归Solidity表达式PEG文法
方案选型
优先使用PEST内置的prec climber(优先级爬升器)处理表达式左递归问题,不推荐手动按优先级分层编写规则消除左递归,核心原因如下:
- 手动分层需要为每一级优先级单独定义非终结符,Solidity表达式共有17个优先级层级,加上前缀、后缀、调用、索引等逻辑,文法规模会膨胀3倍以上,后续调整运算符、修改优先级需要改动多处规则,维护成本极高,且很容易因规则调用顺序错误重新引入左递归或优先级逻辑错误。
- PEST原生提供的prec climber不需要手动改写文法消除左递归,只需要定义基础原子表达式、前缀/后缀/中缀算子的优先级与结合性,解析阶段会自动按优先级规则构造语法树,规则简洁,调整优先级只需要修改对应算子的优先级数值即可,出错概率极低。
文法调整步骤
调整核心思路是把原规则中所有规则开头直接递归调用expression的左递归结构拆分,将原子表达式、后缀操作单独抽离,所有中缀、前缀、三元、赋值运算交给prec climber处理。
1. 定义基础原子表达式
原子表达式是所有表达式的最小匹配单元,规则开头不能递归调用expression,把原规则中无左递归的基础项归入此类,注意补充原规则缺失的字面量匹配:
// 基础原子表达式 primary = { Payable ~ callArgumentList | Type ~ LParen ~ typeName ~ RParen | New ~ typeName | tupleExpression | inlineArrayExpression | identifier | literal // 需自行补充:包含整数、布尔值、字符串、地址、十六进制字面量等基础值 }
2. 定义后缀操作
后缀操作优先级高于所有前缀、中缀运算,左结合,匹配所有跟在原子表达式后面的索引、成员访问、调用、后缀自增自减逻辑:
// 后缀操作 postfix = { LBrack ~ expression? ~ RBrack // 数组索引 arr[i] | LBrack ~ expression? ~ Colon ~ expression? ~ RBrack // 数组切片 arr[i:j] | Period ~ (identifier | Address) // 成员访问 obj.member | LBrack ~ (namedArgument ~ (Comma ~ namedArgument)*)? ~ RBrack // 命名参数调用/索引 | callArgumentList // 普通函数调用 fn(arg1, arg2) | Inc | Dec // 后缀自增自减 a++、a-- } // 绑定所有后缀操作的基础表达式,无左递归 primary_with_postfix = { primary ~ postfix* }
这部分规则不会触发左递归:所有后缀匹配都以LBrack/Period/LParen等token开头,只有匹配完前面的primary内容后才会递归匹配内部的expression,不会出现规则开头直接递归的情况。
3. 定义前缀、中缀算子规则,接入prec climber
按Solidity官方定义的运算符优先级(数值越大优先级越高)、结合性配置算子,核心expression规则直接调用prec climber即可:
// 前缀算子 prefix_op = _{ (Inc | Dec | Not | BitNot | Delete | Sub) } // 中缀/三元/赋值算子,优先级从低到高对应数值0-12 infix_op = _{ assignOp > { 0, r } // 赋值运算,右结合 | Conditional ~ expression ~ Colon > { 1, r } // 三元运算 a?b:c,右结合 | Or > { 2, l } // 逻辑或 ||,左结合 | And > { 3, l } // 逻辑与 &&,左结合 | (Equal | NotEqual) > { 4, l } // 相等判断 == !=,左结合 | (LessThan | GreaterThan | LessThanOrEqual | GreaterThanOrEqual) > {5, l} // 比较运算,左结合 | BitOr > { 6, l } // 按位或 |,左结合 | BitXor > { 7, l } // 按位异或 ^,左结合 | BitAnd > { 8, l } // 按位与 &,左结合 | (Shl | Sar | Shr) > {9, l} // 移位运算,左结合 | (Add | Sub) > {10, l} // 加减 + -,左结合 | (Mul | Div | Mod) > {11, l} // 乘除模 * / %,左结合 | Exp > {12, r} // 幂运算 **,右结合 } // 核心表达式规则,静默规则(加_前缀)避免生成多余语法树节点 expression = _{ prec_climb(primary_with_postfix, prefix_op, infix_op) }
注意事项
- 词法规则需要和算子严格对齐:比如逻辑与
And对应&&、按位与BitAnd对应&,不要混淆单双字符运算符,否则会出现优先级匹配错误。 - 结合性不要写错:幂运算、赋值、三元运算为右结合,其余中缀运算、后缀运算为左结合,前缀运算为右结合,和Solidity语义保持一致。
- 如果需要支持更细粒度的语法树处理,可以去掉算子规则前的
_静默标记,自定义匹配时的节点生成逻辑。
内容的提问来源于stack exchange,提问作者0xKitsune
相关产品推荐
相关产品推荐

