首页 文章

如何用ANTLR4创建AST?

提问于
浏览
46

我一直在寻找很多关于这一点,我找不到任何有用的,真正帮助我 Build 一个AST . 我已经知道ANTLR4不像以前的ANTLR3那样构建AST . 每个人都说:“嘿,使用访客!”,但我找不到任何示例或更详细的解释如何我这样做...

我的语法必须像C一样,但每个命令都用葡萄牙语(portuga编程语言)编写 . 我可以使用ANTLR4轻松生成解析树 . 我的问题是:现在我需要做些什么才能创建AST?

顺便说一下,我正在使用Java和IntelliJ ......

EDIT1: 我能得到的最接近的是使用这个主题的答案:Is there a simple example of using antlr4 to create an AST from java source code and extract methods, variables and comments?但它只打印被访问方法的名称..

由于第一次尝试对我不起作用,我试图使用来自ANTLR3的this tutorial,但我无法弄清楚如何使用StringTamplate而不是ST ...

读这本书The Definitive ANTLR 4 Reference我也找不到与AST有关的任何东西 .

EDIT2: 现在我有一个类来创建DOT文件,我只需要弄清楚如何正确使用访问者

2 回答

  • 115

    好的,让我们构建一个简单的数学示例 . 构建AST对于这样的任务来说完全是过度的,但它是展示原理的好方法 .

    我将在C#中完成它,但Java版本将非常相似 .

    语法

    首先,让我们编写一个非常基本的数学语法来处理:

    grammar Math;
    
    compileUnit
        :   expr EOF
        ;
    
    expr
        :   '(' expr ')'                         # parensExpr
        |   op=('+'|'-') expr                    # unaryExpr
        |   left=expr op=('*'|'/') right=expr    # infixExpr
        |   left=expr op=('+'|'-') right=expr    # infixExpr
        |   func=ID '(' expr ')'                 # funcExpr
        |   value=NUM                            # numberExpr
        ;
    
    OP_ADD: '+';
    OP_SUB: '-';
    OP_MUL: '*';
    OP_DIV: '/';
    
    NUM :   [0-9]+ ('.' [0-9]+)? ([eE] [+-]? [0-9]+)?;
    ID  :   [a-zA-Z]+;
    WS  :   [ \t\r\n] -> channel(HIDDEN);
    

    非常基本的东西,我们有一个 expr 规则来处理所有事情(优先规则等) .

    AST节点

    然后,让我们定义一些我们将使用的AST节点 . 这些都是完全自定义的,您可以按照自己的方式定义它们 .

    以下是我们将在此示例中使用的节点:

    internal abstract class ExpressionNode
    {
    }
    
    internal abstract class InfixExpressionNode : ExpressionNode
    {
        public ExpressionNode Left { get; set; }
        public ExpressionNode Right { get; set; }
    }
    
    internal class AdditionNode : InfixExpressionNode
    {
    }
    
    internal class SubtractionNode : InfixExpressionNode
    {
    }
    
    internal class MultiplicationNode : InfixExpressionNode
    {
    }
    
    internal class DivisionNode : InfixExpressionNode
    {
    }
    
    internal class NegateNode : ExpressionNode
    {
        public ExpressionNode InnerNode { get; set; }
    }
    
    internal class FunctionNode : ExpressionNode
    {
        public Func<double, double> Function { get; set; }
        public ExpressionNode Argument { get; set; }
    }
    
    internal class NumberNode : ExpressionNode
    {
        public double Value { get; set; }
    }
    

    将CST转换为AST

    ANTLR为我们生成了CST节点( MathParser.*Context 类) . 我们现在必须将它们转换为AST节点 .

    这很容易通过访问者来完成,而ANTLR为我们提供了一个 MathBaseVisitor<T> 类,所以让我们使用它 .

    internal class BuildAstVisitor : MathBaseVisitor<ExpressionNode>
    {
        public override ExpressionNode VisitCompileUnit(MathParser.CompileUnitContext context)
        {
            return Visit(context.expr());
        }
    
        public override ExpressionNode VisitNumberExpr(MathParser.NumberExprContext context)
        {
            return new NumberNode
            {
                Value = double.Parse(context.value.Text, NumberStyles.AllowDecimalPoint | NumberStyles.AllowExponent)
            };
        }
    
        public override ExpressionNode VisitParensExpr(MathParser.ParensExprContext context)
        {
            return Visit(context.expr());
        }
    
        public override ExpressionNode VisitInfixExpr(MathParser.InfixExprContext context)
        {
            InfixExpressionNode node;
    
            switch (context.op.Type)
            {
                case MathLexer.OP_ADD:
                    node = new AdditionNode();
                    break;
    
                case MathLexer.OP_SUB:
                    node = new SubtractionNode();
                    break;
    
                case MathLexer.OP_MUL:
                    node = new MultiplicationNode();
                    break;
    
                case MathLexer.OP_DIV:
                    node = new DivisionNode();
                    break;
    
                default:
                    throw new NotSupportedException();
            }
    
            node.Left = Visit(context.left);
            node.Right = Visit(context.right);
    
            return node;
        }
    
        public override ExpressionNode VisitUnaryExpr(MathParser.UnaryExprContext context)
        {
            switch (context.op.Type)
            {
                case MathLexer.OP_ADD:
                    return Visit(context.expr());
    
                case MathLexer.OP_SUB:
                    return new NegateNode
                    {
                        InnerNode = Visit(context.expr())
                    };
    
                default:
                    throw new NotSupportedException();
            }
        }
    
        public override ExpressionNode VisitFuncExpr(MathParser.FuncExprContext context)
        {
            var functionName = context.func.Text;
    
            var func = typeof(Math)
                .GetMethods(BindingFlags.Public | BindingFlags.Static)
                .Where(m => m.ReturnType == typeof(double))
                .Where(m => m.GetParameters().Select(p => p.ParameterType).SequenceEqual(new[] { typeof(double) }))
                .FirstOrDefault(m => m.Name.Equals(functionName, StringComparison.OrdinalIgnoreCase));
    
            if (func == null)
                throw new NotSupportedException(string.Format("Function {0} is not supported", functionName));
    
            return new FunctionNode
            {
                Function = (Func<double, double>)func.CreateDelegate(typeof(Func<double, double>)),
                Argument = Visit(context.expr())
            };
        }
    }
    

    如您所见,只需使用访问者从CST节点创建AST节点即可 . 代码应该是不言自明的(好吧,可能除了 VisitFuncExpr 之外的东西,但它只是一种快速的方法将委托连接到System.Math类的合适方法) .

    在这里你有AST建设的东西 . 这就是所需要的 . 只需从CST中提取相关信息并将其保存在AST中 .

    AST访客

    现在,让's play a bit with the AST. We' ll必须构建一个AST访问者基类来遍历它 . 让我们做一些类似于ANTLR提供的 AbstractParseTreeVisitor<T> 的事情 .

    internal abstract class AstVisitor<T>
    {
        public abstract T Visit(AdditionNode node);
        public abstract T Visit(SubtractionNode node);
        public abstract T Visit(MultiplicationNode node);
        public abstract T Visit(DivisionNode node);
        public abstract T Visit(NegateNode node);
        public abstract T Visit(FunctionNode node);
        public abstract T Visit(NumberNode node);
    
        public T Visit(ExpressionNode node)
        {
            return Visit((dynamic)node);
        }
    }
    

    在这里,我利用C#的 dynamic 关键字在一行代码中执行双重调度 . 在Java中,您必须使用如下所示的一系列 if 语句自行连接:

    if (node is AdditionNode) {
        return Visit((AdditionNode)node);
    } else if (node is SubtractionNode) {
        return Visit((SubtractionNode)node);
    } else if ...
    

    但我只是去寻找这个例子的捷径 .

    使用AST

    那么,我们可以用数学表达式树做什么呢?当然要评估它!让我们实现一个表达式求值器:

    internal class EvaluateExpressionVisitor : AstVisitor<double>
    {
        public override double Visit(AdditionNode node)
        {
            return Visit(node.Left) + Visit(node.Right);
        }
    
        public override double Visit(SubtractionNode node)
        {
            return Visit(node.Left) - Visit(node.Right);
        }
    
        public override double Visit(MultiplicationNode node)
        {
            return Visit(node.Left) * Visit(node.Right);
        }
    
        public override double Visit(DivisionNode node)
        {
            return Visit(node.Left) / Visit(node.Right);
        }
    
        public override double Visit(NegateNode node)
        {
            return -Visit(node.InnerNode);
        }
    
        public override double Visit(FunctionNode node)
        {
            return node.Function(Visit(node.Argument));
        }
    
        public override double Visit(NumberNode node)
        {
            return node.Value;
        }
    }
    

    一旦我们有了AST就很简单,不是吗?

    把它们放在一起

    最后但并非最不重要的是,我们必须实际编写主程序:

    internal class Program
    {
        private static void Main()
        {
            while (true)
            {
                Console.Write("> ");
                var exprText = Console.ReadLine();
    
                if (string.IsNullOrWhiteSpace(exprText))
                    break;
    
                var inputStream = new AntlrInputStream(new StringReader(exprText));
                var lexer = new MathLexer(inputStream);
                var tokenStream = new CommonTokenStream(lexer);
                var parser = new MathParser(tokenStream);
    
                try
                {
                    var cst = parser.compileUnit();
                    var ast = new BuildAstVisitor().VisitCompileUnit(cst);
                    var value = new EvaluateExpressionVisitor().Visit(ast);
    
                    Console.WriteLine("= {0}", value);
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex.Message);
                }
    
                Console.WriteLine();
            }
        }
    }
    

    现在我们终于可以玩了:

    enter image description here

  • 3

    我创建了一个小型Java项目,允许您通过编译内存中ANTLR生成的词法分析器和解析器来立即测试您的ANTLR语法 . 您可以通过将字符串传递给解析器来解析字符串,它将自动生成一个AST,然后可以在您的应用程序中使用它 .

    为了减小AST的大小,您可以使用NodeFilter,您可以在构建AST时添加您希望考虑的非终端的 生产环境 规则名称 .

    代码和一些代码示例可以在https://github.com/julianthome/inmemantlr找到

    希望这个工具很有用;-)

相关问题