2010-10-11 21 views
11

Tengo una DSL relativamente simple que me gustaría manejar de forma más robusta que un montón de instrucciones codificadas manualmente java.util.regex.Pattern + lógica de análisis.ANTLR (o alternativa): análisis de desacoplamiento de la evaluación

La herramienta más citada parece ser ANTLR. No estoy familiarizado con esto y estoy dispuesto a intentarlo. Sin embargo, me da un poco de recelo cuando veo los ejemplos (por ejemplo, ANTLR expression evaluator example, o Martin Fowler's HelloAntlr, o this other Q on stackoverflow). La razón de esto es que los archivos de la gramática parecen ser un batiburrillo de definiciones gramaticales intercaladas con fragmentos del lenguaje de implementación (por ejemplo, Java) que son de naturaleza imperativa.

Lo que realmente preferiría es separar la parte de imperativo/evaluación del analizador. ¿Hay alguna manera de usar ANTLR (o alguna otra herramienta) para definir una gramática & producir un conjunto de archivos fuente Java para que se compile en clases que pueda usar para analizar la entrada en una estructura sin actuar sobre esa estructura?

por ejemplo, si quería utilizar evaluación de la expresión con sólo las + y * y () operadores, y que tenía la entrada

3 * (4 + 7 * 6) * (3 + 7 * (4 + 2))

entonces lo que me gustaría hacer es escribir una gramática para convertir eso a una estructura jerárquica como

Product 
    Term(3) 
    Sum 
    Term(4) 
    Product 
     Term(7) 
     Term(6) 
    Sum 
    Term(3) 
    Product 
     Term(7) 
     Sum 
      Term(4) 
      Term(2) 

donde puedo utilizar clases como

interface Expression<T> { 
    public T evaluate(); 
} 

class Term implements Expression<Double> { 
    final private double value; 
    @Override public Double evaluate() { return value; } 
} 

class Product implements Expression<Double> { 
    final private List<Expression<Double>> terms; 
    @Override public Double evaluate() { 
     double result = 1; 
     for (Expression<Double> ex : terms) 
      result *= ex.evaluate(); 
     return result; 
    } 
} 

class Sum implements Expression<Double> { 
    final private List<Expression<Double>> terms; 
    @Override public Double evaluate() { 
     double result = 0; 
     for (Expression<Double> ex : terms) 
      result += ex.evaluate(); 
     return result; 
    } 
} 

y utilizar antlr para construir la estructura. ¿Hay alguna forma de hacer esto? Realmente preferiría seguir este enfoque, ya que me permite (y otros ingenieros de software) editar y visualizar clases completas de Java sin tener que tener esas clases fragmentadas en piezas extrañas en los archivos de gramática ANTLR.

¿Hay alguna manera de hacerlo?


aclaración: Quiero pasar gran parte de mi esfuerzo posible de dos maneras: la definición de la gramática en sí, y en Java (por ejemplo, mis clases de productos/Suma/Plazo) antlr-independientes. Quiero minimizar la cantidad de tiempo/experiencia que tengo que pasar aprendiendo sintaxis ANTLR, peculiaridades y API. No sé cómo crear y manipular un AST de la gramática ANTLR. Debido a que esta es solo una pequeña parte de un gran proyecto de Java, no soy solo yo, es alguien de mi equipo el que tiene que revisar o mantener mi código.

(No pretendo parecer impertinente: estoy dispuesto a invertir tiempo y energía para utilizar una herramienta, pero solo si la herramienta se convierte en una herramienta útil y no sigue siendo un obstáculo.)

Respuesta

10

Jason S escribió:

¿Hay una manera de hacer esto?

Sí.

En primer lugar definir su gramática (I tomó el ejemplo de un analizador de expresiones con sólo el + y * y () operadores):

grammar Exp; 

// parser rules 
parse 
    : additionExp 
    ; 

additionExp 
    : multiplyExp (Add multiplyExp)* 
    ; 

multiplyExp 
    : atomExp (Mult atomExp)* 
    ; 

atomExp 
    : Number 
    | LParen additionExp RParen 
    ; 

// lexer rules 
Add : '+' ; 
Mult : '*' ; 
LParen : '(' ; 
RParen : ')' ; 
Number : ('0'..'9')+ ('.' ('0'..'9')+)? ; 
Spaces : (' ' | '\t' | '\r'| '\n') {$channel=HIDDEN;} ; 

Si desea permitir que antlr generar un AST adecuado de la gramática anterior, hay que poner lo siguiente en la parte superior de la gramática (en virtud de la declaración de la gramática):

options { 
    output=AST; 
} 

y se debe indicar cuál debe ser la raíz de cada una de sus reglas de análisis sintáctico. Esto se puede hacer de dos maneras:

  1. usando rewrite rules;
  2. o colocando uno de los "línea de árboles-operadores" ^ y ! después de las fichas:
    • ^ significa: hacer esta muestra la raíz;
    • ! significa: excluye este token del AST.

Ahora su gramática se vería así:

grammar Exp; 

options { 
    output=AST; 
} 

// parser rules 
parse 
    : additionExp 
    ; 

additionExp 
    : multiplyExp (Add^ multiplyExp)* 
    ; 

multiplyExp 
    : atomExp (Mult^ atomExp)* 
    ; 

atomExp 
    : Number 
    | LParen! additionExp RParen! 
    ; 

// lexer rules 
Add : '+' ; 
Mult : '*' ; 
LParen : '(' ; 
RParen : ')' ; 
Number : ('0'..'9')+ ('.' ('0'..'9')+)? ; 
Spaces : (' ' | '\t' | '\r'| '\n') {$channel=HIDDEN;} ; 

Como se puede ver, hice las raíces Add y Mult, y excluidos los paréntesis.

Ahora generar un analizador léxico & de la gramática:

java -cp antlr-3.2.jar org.antlr.Tool Exp.g 

crear un pequeño instrumento de prueba:

import org.antlr.runtime.*; 
import org.antlr.runtime.tree.*; 
import java.util.*; 

public class Main { 

    private static void preOrder(CommonTree tree, int depth) { 
     for(int i = 0; i < depth; i++) { 
      System.out.print("- "); 
     } 
     System.out.println("> "+tree + " :: " + ExpParser.tokenNames[tree.getType()]); 
     List children = tree.getChildren(); 
     if(children == null) return; 
     for(Object o : children) { 
      preOrder((CommonTree)o, depth+1); 
     } 
    } 

    public static void main(String[] args) throws Exception { 
     ANTLRStringStream in = new ANTLRStringStream("3 * (4 + 7 * 6) * (3 + 7 * (4 + 2))"); 
     ExpLexer lexer = new ExpLexer(in); 
     CommonTokenStream tokens = new CommonTokenStream(lexer); 
     ExpParser parser = new ExpParser(tokens); 
     CommonTree tree = (CommonTree)parser.parse().getTree(); 
     preOrder(tree, 0); 
    } 
} 

compilar todo:

javac -cp antlr-3.2.jar *.java 

y ejecutar la clase Main:

// *nix/Mac OS 
java -cp .:antlr-3.2.jar Main 

// Windows 
java -cp .;antlr-3.2.jar Main 

que produce lo siguiente:

> * :: Mult 
- > * :: Mult 
- - > 3 :: Number 
- - > + :: Add 
- - - > 4 :: Number 
- - - > * :: Mult 
- - - - > 7 :: Number 
- - - - > 6 :: Number 
- > + :: Add 
- - > 3 :: Number 
- - > * :: Mult 
- - - > 7 :: Number 
- - - > + :: Add 
- - - - > 4 :: Number 
- - - - > 2 :: Number 

Como se puede ver, el parse regla (método) devuelve un objeto CommonTree puede utilizar para crear su propio Walker/visitante dejando la gramática como es .

HTH

+1

+1. Gracias por publicar el ejemplo paso a paso, eso es más o menos lo que necesitaba. Todos los otros ejemplos que miré tenían acciones imperativas en el archivo .g y no pude entender qué sintaxis era un ANTLR-ismo frente a fragmentos de Java que podían eliminarse. –

+0

p.s. ¿Qué parte (s) de org.antlr.stringtemplate estás usando? ¿son recomendados/necesarios? –

+0

Hmm. Cada uno de los nodos de su árbol * imprime * como una cadena ... pero, ¿qué es el nodo? ¿Cómo sabría si un nodo es un multiplyExp o un additionExp o algo más? Le daré un giro a través del depurador cuando tenga la oportunidad, pero si hay una respuesta corta + obvia, lo agradecería enormemente. –

3

¿Qué hay de usar ANTLR AST (Árbol de sintaxis abstracta) y construir un árbol reflejado con sus clases visitando cada nodo de árbol.


@Giuseppe Cardone añade algunos grandes enlaces que he puesto aquí:

http://www.antlr.org/article/1100569809276/use.tree.grammars.tml

http://www.antlr.org/article/1170602723163/treewalkers.html

Un ejemplo se puede encontrar en:

http://sagarsunkle.spaces.live.com/blog/cns!E07F3B561597E4EE!664.entry?sa=97619042

+0

Hmm. Soy nuevo en ANTLR, por lo que no estoy familiarizado con cómo hacer lo que sugiere, o qué ventajas/desventajas podría tener. –

+1

Quise elaborar más la pregunta, pero básicamente estoy de acuerdo con @smink: construir un AST (tal vez usando la opción ANTLR 'output = AST') y luego verificarlo/evaluarlo/compilarlo usando un tree walker es la manera más fácil de desacoplar gramática del código. –

+0

+1 @Giuseppe Cardone para los enlaces. Utilicé esta técnica en un proyecto anterior y funciona bien. –

2

Los ejemplos que mencionas integran las acciones del analizador dentro de la gramática en aras de la concisión. Esto funciona bien para proyectos pequeños. Para los más grandes, preferiría hacer un AST primero y luego hacer lo que quiera con él. Usted puede hacer esto, jeje, mediante la incorporación de acciones que crean el árbol, pero proporciona una antlr, de manera declarativa más agradable:

http://www.antlr.org/wiki/display/ANTLR3/Tree+construction

continuación, se puede utilizar una gramática de árbol para generar el código, por ejemplo, con StringTemplate. He utilizado esta cadena de herramientas para mi tesis y funcionó como un amuleto. Pero apuesto a que me he sufrido mucho sin tener el libro de referencia Anlr3 (http://pragprog.com/titles/tpantlr/the-definitive-antlr-reference)

También encontré las notas de la conferencia vinculados en la página antlr realmente útil: http://www.antlr.org/wiki/display/CS652/CS652+Home

Además, hacen uso de AntlrWorks a prueba tu gramática También hay un conjunto de pruebas de unidad de gramática disponible. Además, la lista de correo antlr está realmente activa, y Terence Parr responde activamente a la mayoría de las publicaciones. Además, es muy divertido.

Cuestiones relacionadas