Cómo crear AST con ANTLR4?


He estado buscando MUCHO sobre esto y no pude encontrar nada útil que realmente me ayude a construir un AST. Ya sé que ANTLR4 no construye AST como ANTLR3 solía hacer. Todos digan: "¡Oye, usa a los visitantes!", pero no pude encontrar ningún ejemplo o explicación más detallada sobre CÓMO puedo hacer esto...

Tengo una gramática como C, pero con todos los comandos escritos en portugués (lenguaje de programación portuga). Puedo generar fácilmente el árbol de análisis usando ANTLR4. Mi pregunta es: ¿Qué Tengo que hacer ahora para crear un AST?

Por cierto, estoy usando Java e IntelliJ...

EDIT1: Lo más cercano que pude conseguir fue usar la respuesta de este tema: ¿Hay un ejemplo simple de usar antlr4 para crear un AST a partir del código fuente de Java y extraer métodos, variables y comentarios? Pero solo imprime el nombre de los métodos visitados..

Dado que el primer intento no funcionó para mí como esperaba, traté de usar este tutorial de ANTLR3, pero no pude averiguar cómo usar StringTamplate en lugar de ST...

Leyendo el libro La Referencia Definitiva de ANTLR 4 Tampoco pude encontrar nada relacionado con los ASTs.

EDIT2: Ahora tengo una clase para crear el archivo DOT, solo necesito averiguar cómo usar los visitantes correctamente

Author: Community, 2015-04-30

2 answers

Ok, vamos a construir un ejemplo matemático simple. Construir un AST es totalmente excesivo para tal tarea, pero es una buena manera de mostrar el principio.

Lo haré en C# pero la versión de Java sería muy similar.

La gramática

Primero, escribamos una gramática matemática muy básica para trabajar con:{[16]]}

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

Cosas bastante básicas, tenemos una única regla expr que maneja todo (reglas de precedencia, etc.).

Los nodos AST

Entonces, vamos a definir algunos AST nodos que usaremos. Estos son totalmente personalizados y puede definirlos de la manera que desee.

Aquí están los nodos que usaremos para este ejemplo:

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

Convertir una CST en una AST

ANTLR generó los nodos CST para nosotros (las clases MathParser.*Context). Ahora tenemos que convertirlos en nodos AST.

Esto se hace fácilmente con un visitante, y ANTLR nos proporciona una clase MathBaseVisitor<T>, así que trabajemos con eso.

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

Como puedes ver, es solo una cuestión de crear un nodo AST a partir de un nodo CST mediante un visitante. El código debería ser bastante autoexplicativo (bueno, tal vez excepto por las cosas VisitFuncExpr, pero es solo una forma rápida de conectar un delegado a un método adecuado de la System.Math class).

Y aquí tienes el material de construcción de AST. Es todo lo que se necesita. Simplemente extraiga la información relevante del CST y guárdela en el AST.

El visitante AST

Ahora, juguemos un poco con la AST. Tendremos que construye una clase base de visitantes AST para atravesarla. Vamos a hacer algo similar a la AbstractParseTreeVisitor<T> proporcionada por 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);
    }
}

Aquí, aproveché la palabra clave dynamic de C#para realizar un doble envío en una línea de código. En Java, tendrás que hacer el cableado tú mismo con una secuencia de if instrucciones como estas:

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

Pero simplemente fui por el atajo para este ejemplo.

Trabajar con el AST

Entonces, ¿qué podemos hacer con un árbol de expresiones matemáticas? Evaluar ¡eso, por supuesto! Implementemos un evaluador de expresiones:

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

Bastante simple una vez que tenemos un AST, ¿no es así?

Poniendo todo junto

Por último, pero no menos importante, tenemos que escribir el programa 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();
        }
    }
}

Y ahora finalmente podemos jugar con él:

introduzca la descripción de la imagen aquí

 111
Author: Lucas Trzesniewski,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-02-12 19:01:22

He creado un pequeño proyecto Java que le permite probar su gramática ANTLR al instante compilando el lexer y el analizador generado por ANTLR en memoria. Solo puede analizar una cadena pasándola al analizador, y generará automáticamente un AST a partir de ella que luego se puede usar en su aplicación.

Con el propósito de reducir el tamaño del AST, podría usar un filtro de NOD al que podría agregar los nombres de reglas de producción de los no terminales que le gustaría ser considerado al construir el AST.

El código y algunos ejemplos de código se pueden encontrar en https://github.com/julianthome/inmemantlr

Espero que la herramienta sea útil; -)

 3
Author: Julian,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-06-27 11:29:18