Rust 实现一个表达式 Parser(11) Format 实现

4,903 阅读3分钟

本文将实现最后一个需求: 格式化表达式

源码: src/traversal/format.rs

实现目标

将这个表达式 "1*2+(3/(4+(-5)))" 处理成 "1 * 2 + 3 / (4 + (-5))"

也就是说

  • 去除冗余括号
  • 只保留必要括号
  • 在操作符左右插入空格

写一下

Formatter 定义

像前面的 eval 一样, 这里直接定义一个结构体来承载逻辑

pub struct Formatter {
    output: String,
}

impl Formatter {
    pub fn new() -> Self {
        Formatter { output: String::new() }
    }
    pub fn format(&mut self, node: &Node) -> &str {
        // 随后就会实现 Visitor Trait
        self.visit(node);
        self.output.as_str()
    }
}

工具函数实现

考虑到格式化会需要插入一些固定的符号, 如空格, 括号之类的, 因此先来简单实现一些工具函数

impl Formatter {
    fn push(&mut self, str: &str) {
        self.output.push_str(str)
    }
    // 插入一个空格
    fn ws(&mut self) {
        self.push(" ");
    }
}

Visitor 实现

下面来正式实现格式化逻辑, 比较简单, 直接贴代码

// 不需要返回值, 因此传入 ()
impl Visitor<()> for Formatter {
    // Literal 直接插入
    fn visit_num(&mut self, _: i32, raw: &str) {
        self.push(raw)
    }
    fn visit_expr(&mut self, left: &Node, op: &str, right: &Node) {
        // 优先访问左子树
        self.visit(left);
        // 插入空格
        self.ws();
        // 插入操作符
        self.push(op);
        // 插入空格
        self.ws();
        // 访问右子树
        self.visit(right);
    }
}

处理括号

这里简单的分析一下, 何时需要添加括号

第一反应想到的就是操作数是一个负数 "1 + (-2)", 这种情况下括号不可省略, 但是如果第一个操作数是负数呢 "-1 + 2", 这种情况下括号可省略, 综合一下, 我们的第一种情况得到了

  • 右子树是一个负数

第二种情况就需要思考一下括号的语义了, 在数学四则运算中, 括号表示提高表达式的计算优先级, 意为优先计算, 而我们在 Parser 阶段本就会根据计算优先级来构建 AST, 也就是说在 AST 中本来就已经隐含了计算优先级的问题, 也就是 节点所处深度越大, 优先级越高

但是如果碰到如下图中的 AST 呢

format_ast.jpg

很显然, 这不符合常理, 因为加法的优先级就应该比乘法要低, 但是这里为什么加法节点的深度要比乘法节点的深度大呢

因为这里 AST(Abstract Syntax Tree) abstract 掉了一对括号, 这棵 AST 的表达式应该是 "1 * (2 + 3)"

发现了这点就很容易得出第二个情况了, 在这种情况下我们必须添加括号

  • 右子树的符号的优先级小于当前符号的优先级

以下是代码的具体实现

impl Formatter {
    // 将给定表达式包裹在括号中
    fn push_paren_expr(&mut self, node: &Node) {
      // 先添加括号
      self.push("(");
      // 在访问节点
      self.visit(node);
      self.push(")");
    }

}
impl Visitor<()> for Formatter {
    // 数字依然直接插入
    fn visit_num(&mut self, _: i32, raw: &str) {
        self.push(raw)
    }
    fn visit_expr(&mut self, left: &Node, op: &str, right: &Node) {
        // 左子树依然直接访问
        self.visit(left);
        self.ws();
        // 符号依然直接插入
        self.push(op);
        self.ws();

        // 访问右子树前先做个判断
        match right {
            // 若当前符号的优先级大于右子树符号的优先级
            Node::Expr { op: next_op, .. }
                if SyntaxKind::get_op_priority(op)
                    > SyntaxKind::get_op_priority(next_op.into_str()) =>
            {
                // 把右子树裹在括号里
                self.push_paren_expr(right);
            }
            // 若右子树是个负数
            Node::Literal { raw, .. } if raw.chars().nth(0) == Some('-') => {
                // 把右子树裹在括号里
                self.push_paren_expr(right);
            }
            // 其他情况不需要特殊处理
            _ => {
                self.visit(right);
            }
        }
    }
}

跑一下

确实是没有问题

#[test]
fn smoke_test() {
    let mut f = Formatter::new();
    assert_eq!(
        "1 * 2 + 3 / (4 + (-5))",
        f.format(&get_node("1*2+(3/(4+(-5)))"))
    )
}

模块导出

源码: src/traversal/mod.rs

如下

pub fn eval(root: &Node) -> i32 {
    Executor::new().eval(root)
}

pub fn format(root: &Node) -> String {
    Formatter::new().format(root).to_string()
}