web-dev-qa-db-fra.com

Comment créer AST avec ANTLR4?

J'ai beaucoup recherché à ce sujet et je n'ai rien trouvé d'utile qui puisse m'aider VRAIMENT à construire un AST. Je sais déjà que ANTLR4 ne construit pas AST comme le faisait ANTLR3. Tout le monde dit: "Hé, utilisez les visiteurs!", Mais je n'ai pas trouvé d'exemple ni d'explication plus détaillée sur COMMENT puis-je faire cela ...

J'ai une grammaire doit aimer C, mais avec toutes les commandes écrites en portugais (langage de programmation portuga). Je peux facilement générer l’arbre d’analyse à l’aide de ANTLR4. Ma question est la suivante: que dois-je faire maintenant pour créer un AST?

BTW, j'utilise Java et IntelliJ ...

EDIT1: Le plus proche que j'ai pu obtenir était d'utiliser la réponse de ce sujet: Existe-t-il un exemple simple d'utilisation d'antlr4 pour créer un AST de Java code source et méthodes d'extraction, variables et commentaires? Mais il affiche uniquement le nom des méthodes visitées.

Comme la première tentative n'a pas fonctionné comme prévu, j'ai essayé d'utiliser ce tutoriel de ANTLR3, mais je ne savais pas comment utiliser StringTamplate au lieu de ST ...

Lire le livre The Definitive ANTLR 4 Reference Je n'ai également rien trouvé en rapport avec les AST.

EDIT2: Maintenant, j'ai une classe pour créer le fichier DOT, j'ai juste besoin de comprendre comment utiliser correctement les visiteurs

53
Leandro

Ok, construisons un exemple mathématique simple. Construire un AST est totalement exagéré pour une telle tâche, mais c’est une bonne façon de montrer le principe.

Je le ferai en C # mais la version Java serait très similaire.

La grammaire

Commençons par écrire une grammaire mathématique très basique:

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

C'est assez simple, nous avons une seule règle expr qui gère tout (règles de priorité, etc.).

Les nœuds AST

Ensuite, définissons quelques nœuds AST que nous allons utiliser. Celles-ci sont totalement personnalisées et vous pouvez les définir comme vous le souhaitez.

Voici les nœuds que nous allons utiliser pour cet exemple:

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

Conversion d'un CST en AST

ANTLR a généré les nœuds CST pour nous (les classes MathParser.*Context). Nous devons maintenant les convertir en nœuds AST.

Cela se fait facilement avec un visiteur, et ANTLR nous fournit une classe MathBaseVisitor<T>, Alors travaillons avec cela.

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

Comme vous pouvez le constater, il suffit de créer un nœud AST à partir d'un nœud CST en utilisant un visiteur. Le code devrait être assez explicite (bon, sauf peut-être pour le VisitFuncExpr, mais c'est un moyen rapide de connecter un délégué à une méthode appropriée du System.Math classe).

Et ici, vous avez le matériel de construction AST. C'est tout ce qui est nécessaire. Il suffit d'extraire les informations pertinentes du CST et de les conserver dans l'AST.

Le AST visiteur

Maintenant, jouons un peu avec l'AST. Nous devrons créer une classe de base de visiteurs AST pour la parcourir. Faisons juste quelque chose de similaire au AbstractParseTreeVisitor<T> Fourni par 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);
    }
}

Ici, j'ai profité du mot clé dynamic de C # pour effectuer une double distribution dans une ligne de code. En Java, vous devrez effectuer vous-même le câblage avec une séquence d'instructions if comme celles-ci:

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

Mais je suis juste allé pour le raccourci pour cet exemple.

Travailler avec l'AST

Alors, que pouvons-nous faire avec un arbre d'expression mathématique? L'évaluer, bien sûr! Implémentons un évaluateur d'expression:

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

Assez simple une fois que nous avons un AST, n'est-ce pas?

Mettre tous ensemble

Enfin, nous devons écrire le programme principal:

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

Et maintenant, nous pouvons enfin jouer avec:

enter image description here

132

J'ai créé un petit projet Java) qui vous permet de tester votre grammaire ANTLR instantanément en compilant le lexer et l'analyseur générés par ANTLR en mémoire. Vous pouvez simplement analyser une chaîne en la passant à l'analyseur , et il générera automatiquement un AST à partir de celui-ci, qui pourra ensuite être utilisé dans votre application.

Afin de réduire la taille de l'AST, vous pouvez utiliser un NodeFilter auquel vous pouvez ajouter les noms de règles de production des non-terminaux que vous souhaitez voir pris en compte lors de la construction de l'AST.

Le code et des exemples de code sont disponibles à l’adresse suivante: https://github.com/julianthome/inmemantlr

J'espère que l'outil est utile ;-)

4
Julian