三元运算符(ternary)解析

...?...:... 处理成if_else(...,...,...) 的形式.

初衷是用正则替换把对应DSL的operator转换成python语言, 然后用python的ast 直接解析. 但是三元运算符的处理比较复杂, Python 中并没有直接对应的语法结构, 导致没法简单的替换.

可以把字符串token解析成AST 的构建过程理解成“对运算符按优先级排序”的过程.

方法 优缺点 类比排序思路 平均复杂度
用正则表达式替换三元运算符 思路naive, 但是可读性差,难以维护   O(n logn)+kO(n)
全字符串扫描+递归
设计语法树解析器(Parser) 灵活性高, 但实现较复杂 选择排序 O(n logn)
近似二分递归
结合 Pratt Parser 优化解析 代码简洁, 可读性高, 扩展性好 插入排序/堆排序 O(n)
线性扫描

直接用字符串替换

  • 用字符串正则查找tenary并替换
  • 递归处理?: 和括号, =, 逗号的关系
    • 处理 括号, 逗号 都依赖栈来正确处理嵌套结构

写parser处理

可以通过编写自定义的解析器来处理三元运算符。下面是一些处理流程:

  1. 定义:
    1. 定义 AST 节点:设计不同类型的节点来表示表达式,如常量、变量、运算符等。
    2. Tokenize:将源代码字符串分割成 tokens(例如:数字、运算符、字符串等)。
      1. 例如: ``a<0.2? → [(“NAME”,”a”) (“OP”,”<”) (“NUMBER”,”0.2”) (“QMARK”,”?”)]`
    3. 定义优先级: 找表达式中最低的优先级处理.
  2. 具体流程:
    1. 预处理(字符串替换/注释/关键字替换)
      • 把wq expr 中的'...' / "..." 用占位符替换
        • ts_sum(group_zscore(a<0.2? (a+0.7):(a>0.9? a:0.9),bucket(rank(cap),range='0.1, 1, 0.1')),5) -> ts_sum(group_zscore(a<0.2? (a+0.7):(a>0.9? a:0.9),bucket(rank(cap),range=__STR_CONST_0__)),5)
      • 做若干文本替换(or/and/not → logical_or/logical_and/…、–→+、NaN→nan 等)。
      • 删除注释(/* … */ 与行注释 // 或 #)。
      • 按分号 ; 切分成多条语句,逐条 tokenize() → parse_expr() → to_python()。
      • token 化
      • 自顶向下递归解析(赋值 → 三元→ 二元按最低 precedence 为根 → 原子/函数/一元)
    2. 解析每句代码:
      • 顶层赋值检测
        • 去掉最外层的分号
        • 检查最外层(用括号记录层数)的赋值 =, 把赋值语句的左右两侧分开.
      • 三元检测
        • 检查最外层 ? 的第一个位置作为三元入口, 再从 ? 后扫描并找到对应的最外层 :(depth=0)的位置
        • 每当遇到括号要剥去括号重新检测三元
      • 二元检测(binop)
        • 检查最外层优先级最低的op. 优先级相同, 取最右边的op拆分(左结合)
      • 一元检测(atom):
        • 检查 一元运算符(!, -) -> 单token原子(number, string, name) -> function call -> 检查每个参数
    3. 构建 AST
      • 递归生成 Python 代码: 对不同节点分别处理
      • 还原字符串并输出
      • 每条语句生成的 Python 片段再还原字符串常量占位符.
    4. 收集所有处理过的语句并最终用 \n 拼接返回
  • ⚠️ 注意点: ast解析一定要注意代码的覆盖率. 保证不会出现不认识的token, operator, 分支情况.

parse优化

token优化

把预处理步骤内化到token中统一处理.

pratt优化

Pratt parser 其实是 自顶向下递归下降解析器的一种优化方式: 不是从最低优先级的字符开始, 而是随着解析从左往右动态的构建ast.

  • 每个token除了优先级外, 定义两个方法: (nud和led)
    • nud(null denotation, 前序) 当token出现在表达式开头时
    • led(left denotation, 中序) 当 token 出现在 表达式中间(左边有东西)时调用

下面是一个DSL对应的token行为表格.

Token/操作符 进入 nud / led 触发条件 动作描述
NUMBER nud token 类型是 "NUMBER" - 若全是数字 → Const(int)
- 否则 → Const(float)
STRING nud token 类型是 "STRING" 构造 Const(str),去掉引号
NAME(变量) nud token 类型是 "NAME"
且 peek不是(
返回 Name
NAME (调用) nud token 类型是 "NAME" 且 peek=(

 函数调用
- 消费 ( → 解析参数列表
- 参数支持关键字:key = value
- 遇到 , 继续,否则结束
- 消费 ) 
→ 返回 Call(Name, args)
LPAREN nud 当前 token = (

3
- 进入子表达式解析(rbp=-1000
- 必须遇到 ) 结束
- 返回子树
+ / - (unary) nud token=OP 且值为 +/-,在前缀位置 以 get_prec("U+/-") 为 rbp 递归解析右操作数
→ UnaryOp
! (unary not) nud token=OP 且值为 ! 以 get_prec("!") 为 rbp 递归解析右操作数
→ UnaryOp("!", expr)
QMARK (?) led peek = QMARK 
且 TERNARY_PREC > rbp
- 消费 ?
- 解析 true_part (rbp=TERNARY_PREC)
- 期望 :
- 解析 false_part (rbp=TERNARY_PREC)
- 构造 Ternary(left, true, false)
二元运算符
(如 + - * / ^ < > == != &&)
led peek 发现下一个 token 是操作符 (OP),
并且该操作符的优先级
lbp > rbp 1 2
- 消费当前操作符(op)
- 结合性处理:
如果操作符是左结合(默认情况),则 next_rbp = lbp;
如果操作符是右结合(如 =、^),则 next_rbp = lbp - 0.0001(轻微调整优先级, 为了遇到相同的运算符时继续解析)
- 递归调用 parse_expression(next_rbp) 来解析右操作数(right)
- 生成 BinOp(left, op, right) 节点
LPAREN (函数调用)

led peek = (
3
支持更一般化的函数调用.
比如,
(a+b)(x)、foo()(x) 这种高阶调用
- 消费 ( → 解析参数列表
- 参数使用 parse_expression(rbp=-1000)
, 分隔,直到遇到 )
- 构造 Call(left, args)

表格解释 $\downarrow$ :

  • lbp(left binding precedence, 左操作数绑定优先级, 当前op的优先级), rbp (右操作符绑定优先级, 上一个op的优先级)
    • 当下一个op优先级更高时, ast会不断加深. 当下一个op优先级不足时, ast会从底向上直到找到合适的位置添加
    • 比如 a = b = c, 在第二个等号处, 把第一个等号的优先级稍稍降低, 保证右侧的优先级更高. 解析为 a = (b = c)
    • 括号总是最优先的, 直接把rdp降到最低

a + b x c -d: Pasted image 20250912052333.png

a = b = c:

Pasted image 20250912052438.png

代码