如何使用ANTLR4创建AST?


70

我一直在搜索很多有关此信息,但找不到任何能真正帮助我构建AST的有用信息。我已经知道ANTLR4不会像以前的ANTLR3那样构建AST。每个人都说:“嘿,请使用访客!”,但是我找不到任何示例或有关如何执行此操作的详细说明...

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

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

EDIT1:我能得到的最接近的答案是该主题:是否有一个简单的示例使用antlr4从Java源代码创建AST并提取方法,变量和注释? 但是它只打印访问方法的名称。

由于第一次尝试对我不起作用,所以我尝试使用ANTLR3的本教程,但我不知道如何使用StringTamplate而不是ST ...

阅读《权威ANTLR 4参考》一书我也找不到与AST相关的任何内容。

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


2
您能否分享一些您尝试过的东西?
桑迪·吉福德

@SandyGifford我编辑了我的帖子,试图解释...我现在没有我的代码,因为我只是删除了我所做的。现在,我只有从ATNLR4生成的代码(分析器,词法和基本游客和听众)
林德罗-

不幸的是,我对ANTLR一无所知(您排在队列中),但是您提高了帖子的质量!
桑迪·吉福德

有关“ CST”与“ AST”的讨论,请参见以下答案:stackoverflow.com/a/29456792/120163 最后的评论讨论了另一个人实际上是如何获得AST的(基本上是通过走CST并制造自己想要的AST)。
伊拉·巴克斯特

我遇到了与您类似的情况,在我无法使用ANTLR找到一个超级简单的AST示例后,我自己创建了一个 github.com/adamsiemion/antlr-java-ast
Adam Siemion

Answers:


168

好的,让我们建立一个简单的数学示例。对于这样的任务,构建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访客

现在,让我们玩一下AST。我们必须建立一个AST访问者基类来遍历它。让我们做一些与AbstractParseTreeVisitor<T>ANTLR提供的类似的事情。

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();
        }
    }
}

现在我们终于可以使用它了:

在此处输入图片说明


2
@Waschbaer IMO手动微调节点可以提高可维护性,这是值得的,但是您可能会不同意。ANTLR 3具有AST输出模式,因此,如果您对使用过时的工具感到满意,则可以使用它。也许您可以使用元编程/模板化来生成一些样板代码,但是最终您可能需要做更多的工作。另一种选择是直接使用CST。
卢卡斯Trzesniewski '16

2
在多年来阅读SO的技巧,建议和答案的过程中,这是我见过的最好的答案之一。概念的出色表现。

1
您是如何获得MathBaseVisitor类的?我的语法称为QueryParser,但没有QueryParserBaseVisitor类。我有一个QuertyParserBaseListener,但它更像是一个进出式助行器,实际上是无法使用的。(我尝试过)
Trejkaz

1
@Trejkaz此类是由ANTLR生成的,就像侦听器一样-您应该有一个QueryBaseVisitor(除非您的语法名称包括Parser部分,但在这种情况下,您还将有一个QueryParserParser)。IIRC的NuGet软件包会自动生成它,但是如果您手动运行ANTLR,则必须添加该-visitor选项。
卢卡斯Trzesniewski '17

2
@Johannes我找到了解决方案,我将其与所有生成的输出和依赖项一起原始上传到这里。这并不是要看起来漂亮,几乎所有内容都在同一文件中。这两种Visit方法是无关的,它们恰好具有相同的名称,但是由于这两个类都是访问者,所以它们使用相同的名称:)
Lucas Trzesniewski

9

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

为了减小AST的大小,可以使用NodeFilter,可以在其中添加在构造AST时要考虑的非终端的生产规则名称。

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

希望该工具有用;-)


我尝试使用您的“小型”项目,但是您有数百个文件,没有任何注释,因此无法弄清楚发生了什么。您已将所有内容包装在自己的包装函数版本中。用户将必须下载整个项目并按原样使用它,并学习使用新类(GenericParser ??)。我无法使用您的代码来弄清楚如何在自己的代码中创建自己的AST。
约翰·克特吉克

2
嗨,约翰inmemantlr的代码库由48个JavaClass(find inmemantlr-api/src/main -name "*.java" | nl)组成,其中大多数注释都很好(javadoc.io/doc/com.github.julianthome/inmemantlr-api/1.3.9)。为了说明您在上面提到的要点(API用法,创建ParseTree),我README.mdgithub.com/julianthome/inmemantlr/tree/master/inmemantlr-api / ...中提供了测试案例并提供了解释。但是,如果您在使用该工具时遇到问题,将很乐意为您提供帮助。请给我发送电子邮件或在github上创建问题。
朱利安

1
可能是您提取了grammars-v4子模块(请参阅inmemantlr-api/src/test/resources/grammars-v4)?实际上,该模块不是inmemantlr的代码库的一部分。它用于确保inmemantlr对所有语法v4语法都有效。但是,执行时,默认情况下不会拉子模块git clone https://github.com/julianthome/inmemantlr
朱利安

1
@朱利安惊人的工具。它非常强大且易于使用。非常感谢您与社区分享。希望在Wiki中看到更多示例。
Vaibhav Jain

@VaibhavJain非常感谢。如果您对如何改进文档/工具/ API有任何建议,如果您可以在github项目页面上创建一个问题,我将非常高兴;)。再次感谢。
朱利安(Julian)

1

我发现了两种简单的方法,主要关注antlr4的TestRig.java文件中提供的功能。

  1. 通过终端

这是我用相应的CPP14.g4语法文件解析C ++的示例 java -cp .:antlr-4.9-complete.jar org.antlr.v4.gui.TestRig CPP14 translationunit -tree filename.cpp。如果省略filename.cpp,则绑定将从stdin读取。“ translationunit”是我使用的CPP14.g4语法文件的开始规则名称。

  1. 通过Java

我使用了TestRig.java文件中的部分代码。再次假设我们有一串C ++源代码,我们要从中产生AST(您也可以直接从文件中读取)。

String source_code = "...your cpp source code...";

CodePointCharStream stream_from_string = CharStreams.fromString(source_code);
CPP14Lexer lexer = new CPP14Lexer(new ANTLRInputStream(source_code));
CommonTokenStream tokens = new CommonTokenStream(lexer);
CPP14Parser parser = new CPP14Parser(tokens);

String parserName = "CPP14Parser";
ClassLoader cl = Thread.currentThread().getContextClassLoader();
Class<? extends Parser> parserClass = null;
parserClass = cl.loadClass(parserName).asSubclass(Parser.class);

String startRuleName = "translationunit"; //as specified in my CPP14.g4 file
Method startRule = parserClass.getMethod(startRuleName);
ParserRuleContext tree = (ParserRuleContext)startRule.invoke(parser, (Object[])null);
System.out.println(tree.toStringTree(parser));

我的进口是:

import java.lang.reflect.Method;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CodePointCharStream;
import org.antlr.v4.runtime.ANTLRInputStream;
import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.Parser;

所有这些都要求您使用命令生成了必要的文件(词法分析器,解析器等),java -jar yournaltrfile.jar yourgrammar.g4然后编译了所有* .java文件。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.