如何用ANTLR4创建AST?

Lea*_*dro 58 java compiler-construction antlr abstract-syntax-tree antlr4

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

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

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

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

由于第一次尝试对我不起作用,我试图使用ANTLR3中的这个教程,但我无法弄清楚如何使用StringTamplate而不是ST ...

阅读本书The Definitive ANTLR 4 Reference我也找不到任何与AST有关的内容.

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

Luc*_*ski 141

好的,让我们构建一个简单的数学示例.构建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);
Run Code Online (Sandbox Code Playgroud)

非常基本的东西,我们有一个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; }
}
Run Code Online (Sandbox Code Playgroud)

将CST转换为AST

ANTLR为我们(MathParser.*Context类)生成了CST节点.我们现在必须将它们转换为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())
        };
    }
}
Run Code Online (Sandbox Code Playgroud)

如您所见,只需使用访问者从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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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

if (node is AdditionNode) {
    return Visit((AdditionNode)node);
} else if (node is SubtractionNode) {
    return Visit((SubtractionNode)node);
} else if ...
Run Code Online (Sandbox Code Playgroud)

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

使用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;
    }
}
Run Code Online (Sandbox Code Playgroud)

一旦我们有了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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

现在我们终于可以玩了:

在此输入图像描述

  • @Waschbaer IMO手动微调节点可以提高可维护性,这是值得的,但是您可能会不同意。ANTLR 3具有AST输出模式,因此,如果您对使用过时的工具感到满意,则可以使用它。也许您可以使用元编程/模板化来生成一些样板代码,但是最终您可能需要做更多的工作。另一种选择是直接使用CST。 (2认同)
  • 在多年来阅读SO的技巧,建议和答案的过程中,这是我见过的最好的答案之一。概念的出色表现。 (2认同)
  • @Trejkaz 这个类是由 ANTLR 生成的,就像监听器一样 - 你应该有一个“QueryBaseVisitor”(除非你的语法名称*包含*“Parser”部分,但在这种情况下,你还将有一个“QueryParserParser”)。IIRC NuGet 包会自动生成它,但如果您手动运行 ANTLR,则必须添加“-visitor”选项。 (2认同)
  • @Johannes我找到了解决方案,我将其上传到[here](https://www.dropbox.com/s/tsi9op7ljsk97jw/MathExample.zip?dl=0),其中包含所有生成的输出和依赖项。这并不是要看起来漂亮,几乎所有内容都在同一文件中。两种“访问”方法无关,它们的名称相同,但是由于两个类都是[visitors](https://en.wikipedia.org/wiki/Visitor_pattern),所以它们使用相同的名称:) (2认同)

Jul*_*ian 5

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

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

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

希望该工具有用;-)

  • 嗨,John inmemantlr 的代码库由 48 个 JavaClasses(`find inmemantlr-api/src/main -name "*.java" | nl`)组成,其中大部分都得到了很好的评论(http://www.javadoc.io/doc /com.github.julianthome/inmemantlr-api/1.3.9)。为了说明您上面提到的要点(API 使用、ParseTree 创建),我在 `README.md` 中提供了解释,并在 https://github.com/julianthome/inmemantlr/tree/master/inmemantlr 中提供了测试用例-api/src/test/java. 但是,如果您在使用该工具时遇到问题,我们很乐意为您提供帮助。请给我发一封电子邮件或在 github 上创建一个问题。 (3认同)
  • 会不会是你拉取了grammars-v4 子模块(请看`inmemantlr-api/src/test/resources/grammars-v4`)?实际上,这个模块不是 inmemantlr 代码库的一部分;它用于确保 inmemantlr 适用于所有文法-v4 文法。但是,在执行 `git clone https://github.com/julianthome/inmemantlr` 时,默认情况下不会拉取子模块。 (2认同)