簡體   English   中英

如何使用 ANTLR4 創建 AST?

[英]How to create AST with ANTLR4?

我一直在搜索很多關於這個的東西,但我找不到任何真正幫助我構建 AST 的有用信息。 我已經知道 ANTLR4 不像以前的 ANTLR3 那樣構建 AST。 每個人都說:“嘿,使用訪問者!”,但我找不到任何關於如何執行此操作的示例或更詳細的說明......

我有一個語法必須像 C,但每個命令都是用葡萄牙語(葡萄牙語編程語言)編寫的。 我可以使用 ANTLR4 輕松生成解析樹。 我的問題是:我現在需要做什么來創建 AST?

順便說一句,我正在使用 Java 和 IntelliJ ...

EDIT1:我能得到的最接近的是使用本主題的答案: 是否有一個使用 antlr4 從 Java 源代碼創建 AST 並提取方法、變量和注釋的簡單示例? 但它只打印被訪問方法的名稱..

由於第一次嘗試沒有像我預期的那樣對我有用,我嘗試使用 ANTLR3 中的本教程,但我無法弄清楚如何使用 StringTamplate 而不是 ST ...

閱讀The Definitive ANTLR 4 Reference這本書我也找不到與 AST 相關的任何內容。

EDIT2:現在我有一個類來創建 DOT 文件,我只需要弄清楚如何正確使用訪問者

好的,讓我們構建一個簡單的數學示例。 對於這樣的任務,構建 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 訪問者基類來遍歷它。 讓我們做一些類似於 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();
        }
    }
}

現在我們終於可以玩它了:

在此處輸入圖片說明

我創建了一個小型 Java 項目,它允許您通過編譯內存中 ANTLR 生成的詞法分析器和解析器來立即測試您的 ANTLR 語法。 您可以通過將字符串傳遞給解析器來解析字符串,它會自動從中生成一個 AST,然后可以在您的應用程序中使用。

為了減小 AST 的大小,您可以使用一個 NodeFilter,您可以向其中添加在構建 AST 時要考慮的非終端的生產規則名稱。

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

希望該工具有用;-)

我找到了兩種簡單的方法,專注於antlr4 的 TestRig.java文件中可用的功能。

  1. 通過終端

這是我使用相應的 CPP14.g4 語法文件java -cp .:antlr-4.9-complete.jar org.antlr.v4.gui.TestRig CPP14 translationunit -tree filename.cpp解析 C++ 的示例。 如果省略 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 文件。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM