You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于Jacc实现计算器AST与递归访问器的疑问:构建与类型转换

关于Jacc计算器AST与访问者模式的优化建议

嘿,我来帮你梳理下这个问题~首先得说:你既没有错误构建AST,也没有完全误解访问者模式,但当前的写法确实没发挥出访问者模式的核心优势,才导致了强制转换的繁琐感。下面具体拆解:

1. 访问者模式的正确打开方式:避免强制转换的关键

你现在遇到的强制转换问题,本质是没把访问者模式的“双分派”特性用起来。正确的做法应该是让AST节点主动接纳访问者,而不是让访问者去猜节点类型。

调整Expr接口与Visitor接口

首先给你的Expr接口添加一个accept方法,参数是访问者接口:

public interface Expr {
    void accept(Visitor visitor);
}

然后定义Visitor接口,为每个具体的AST节点类对应一个visit方法:

public interface Visitor {
    void visitDoubleLiteral(DoubleLiteral literal);
    void visitAddExpr(AddExpr addExpr);
    void visitParenExpr(ParenExpr parenExpr); // 对应带括号的表达式
}

让具体节点类实现accept方法

每个AST节点类(比如DoubleLiteral、AddExpr、ParenExpr)实现accept方法,主动把自己传给访问者的对应方法:

public class DoubleLiteral implements Expr {
    private final double value;

    public DoubleLiteral(double value) {
        this.value = value;
    }

    public double getValue() {
        return value;
    }

    @Override
    public void accept(Visitor visitor) {
        visitor.visitDoubleLiteral(this);
    }
}

实现DotVisitor

这样你的DotVisitor实现Visitor接口时,每个visit方法直接拿到的就是具体类型的节点,完全不需要强制转换:

public class DotVisitor implements Visitor {
    private final StringBuilder dotContent = new StringBuilder();
    private int nodeId = 0;

    @Override
    public void visitDoubleLiteral(DoubleLiteral literal) {
        int id = nodeId++;
        dotContent.append(String.format("node%d [label=\"%f\"];\n", id, literal.getValue()));
        // 直接使用literal的属性,无需强制转换
    }

    @Override
    public void visitAddExpr(AddExpr addExpr) {
        int id = nodeId++;
        dotContent.append(String.format("node%d [label=\"+\"];\n", id));
        // 递归访问左右子节点
        addExpr.getLeft().accept(this);
        addExpr.getRight().accept(this);
        // 添加节点间的边
        dotContent.append(String.format("node%d -> node%d;\n", id, nodeId-2));
        dotContent.append(String.format("node%d -> node%d;\n", id, nodeId-1));
    }

    @Override
    public void visitParenExpr(ParenExpr parenExpr) {
        int id = nodeId++;
        dotContent.append(String.format("node%d [label=\"()\"];\n", id));
        parenExpr.getInnerExpr().accept(this);
        dotContent.append(String.format("node%d -> node%d;\n", id, nodeId-1));
    }

    // 其他辅助方法...
}

2. AST结构的小优化:拆分带括号的表达式

你提到Value产生式对应DoubleLiteral或带括号的AddExpr,建议把带括号的表达式单独抽成ParenExpr类,而不是让Value直接关联AddExpr。这样做的好处是:

  • AST节点职责更单一:ParenExpr专门处理括号包裹的逻辑,AddExpr专注于加法运算,DoubleLiteral就是纯数值节点
  • 后续求值逻辑更清晰:遇到ParenExpr时,直接递归求值其内部的Expr即可,不用额外判断
  • 访问者模式的实现更规整,每个节点对应一个visit方法,避免逻辑混杂

3. 关于左递归语法的补充

因为Jacc无法使用优先级指令,你采用左递归语法是完全正确的——左递归语法天然能生成左结合的AST,这符合加减运算的左结合规则(比如1+2+3会生成AddExpr(AddExpr(1,2),3)),后续求值时从左到右计算也符合预期,这部分没有问题。


内容的提问来源于stack exchange,提问作者Anonimista

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.12 04:28:36