2011-12-27 21 views
17

Estoy tratando de darme cuenta de que python es una gramática que depende de los guiones. ejemploANTLR ¿Cuál es la forma más sencilla de realizar una gramática dependiente de sangría de pitón?

Fuente:

ABC QWE 
    CDE EFG 
    EFG CDE 
    ABC 
    QWE ZXC 

como yo lo veo, lo que necesito es darse cuenta de dos fichas guión y DEDENT, por lo que podría escribir algo como:

grammar mygrammar; 
text: (ID | block)+; 
block: INDENT (ID|block)+ DEDENT; 
INDENT: ????; 
DEDENT: ????; 

¿Hay alguna forma sencilla de darse cuenta de esto usando ANTLR?

(yo preferiría, si es posible, utilizar léxico antlr estándar.)

Respuesta

17

No sé cuál es la forma más fácil de manejar que es, pero la siguiente es una manera relativamente fácil. Siempre que coincida con un salto de línea en su Lexer, coincida opcionalmente con uno o más espacios. Si hay espacios después del salto de línea, compare la longitud de estos espacios con el tamaño de sangría actual. Si es más que el tamaño de sangría actual, emita un token Indent, si es menor que el tamaño de sangría actual, emita un token Dedent y, si es el mismo, no haga nada.

También querrá emitir un número de Dedent tokens al final del archivo para permitir que cada Indent tenga un token que coincida con Dedent.

Para que esto funcione correctamente, es imprescindible añadir un salto de ataque y de salida de línea a su archivo fuente de entrada!

ANTRL3

Una demostración rápida:

grammar PyEsque; 

options { 
    output=AST; 
} 

tokens { 
    BLOCK; 
} 

@lexer::members { 

    private int previousIndents = -1; 
    private int indentLevel = 0; 
    java.util.Queue<Token> tokens = new java.util.LinkedList<Token>(); 

    @Override 
    public void emit(Token t) { 
    state.token = t; 
    tokens.offer(t); 
    } 

    @Override 
    public Token nextToken() { 
    super.nextToken(); 
    return tokens.isEmpty() ? Token.EOF_TOKEN : tokens.poll(); 
    } 

    private void jump(int ttype) { 
    indentLevel += (ttype == Dedent ? -1 : 1); 
    emit(new CommonToken(ttype, "level=" + indentLevel)); 
    } 
} 

parse 
: block EOF -> block 
; 

block 
: Indent block_atoms Dedent -> ^(BLOCK block_atoms) 
; 

block_atoms 
: (Id | block)+ 
; 

NewLine 
: NL SP? 
    { 
    int n = $SP.text == null ? 0 : $SP.text.length(); 
    if(n > previousIndents) { 
     jump(Indent); 
     previousIndents = n; 
    } 
    else if(n < previousIndents) { 
     jump(Dedent); 
     previousIndents = n; 
    } 
    else if(input.LA(1) == EOF) { 
     while(indentLevel > 0) { 
     jump(Dedent); 
     } 
    } 
    else { 
     skip(); 
    } 
    } 
; 

Id 
: ('a'..'z' | 'A'..'Z')+ 
; 

SpaceChars 
: SP {skip();} 
; 

fragment NL  : '\r'? '\n' | '\r'; 
fragment SP  : (' ' | '\t')+; 
fragment Indent : ; 
fragment Dedent : ; 

Puede probar el programa de análisis con la clase:

import org.antlr.runtime.*; 
import org.antlr.runtime.tree.*; 
import org.antlr.stringtemplate.*; 

public class Main { 
    public static void main(String[] args) throws Exception { 
    PyEsqueLexer lexer = new PyEsqueLexer(new ANTLRFileStream("in.txt")); 
    PyEsqueParser parser = new PyEsqueParser(new CommonTokenStream(lexer)); 
    CommonTree tree = (CommonTree)parser.parse().getTree(); 
    DOTTreeGenerator gen = new DOTTreeGenerator(); 
    StringTemplate st = gen.toDOT(tree); 
    System.out.println(st); 
    } 
}  

Si ahora pones lo siguiente en un archivo llamado in.txt:

 

AAA AAAAA 
    BBB BB B 
    BB BBBBB BB 
    CCCCCC C CC 
    BB BBBBBB 
    C CCC 
     DDD DD D 
     DDD D DDD 

(¡Observe los saltos de línea principales y finales!)

continuación, podrás ver el resultado que corresponde a la siguiente AST:

enter image description here

Tenga en cuenta que mi demo no produciría suficientes dedents en la serie, al igual que dedenting ccc-aaa (2 dedent se necesitan fichas):

aaa 
    bbb 
    ccc 
aaa 

Usted tendría que ajustar el código dentro de else if(n < previousIndents) { ... } para emitir posiblemente más de 1 ficha de dedent bas ed en la diferencia entre n y previousIndents. De la parte superior de mi cabeza, que podría tener este aspecto:

else if(n < previousIndents) { 
    // Note: assuming indent-size is 2. Jumping from previousIndents=6 
    // to n=2 will result in emitting 2 `Dedent` tokens 
    int numDedents = (previousIndents - n)/2; 
    while(numDedents-- > 0) { 
    jump(Dedent); 
    } 
    previousIndents = n; 
} 

ANTLR4

Para ANTLR4, hacer algo como esto:

grammar Python3; 

tokens { INDENT, DEDENT } 

@lexer::members { 
    // A queue where extra tokens are pushed on (see the NEWLINE lexer rule). 
    private java.util.LinkedList<Token> tokens = new java.util.LinkedList<>(); 
    // The stack that keeps track of the indentation level. 
    private java.util.Stack<Integer> indents = new java.util.Stack<>(); 
    // The amount of opened braces, brackets and parenthesis. 
    private int opened = 0; 
    // The most recently produced token. 
    private Token lastToken = null; 
    @Override 
    public void emit(Token t) { 
    super.setToken(t); 
    tokens.offer(t); 
    } 

    @Override 
    public Token nextToken() { 
    // Check if the end-of-file is ahead and there are still some DEDENTS expected. 
    if (_input.LA(1) == EOF && !this.indents.isEmpty()) { 
     // Remove any trailing EOF tokens from our buffer. 
     for (int i = tokens.size() - 1; i >= 0; i--) { 
     if (tokens.get(i).getType() == EOF) { 
      tokens.remove(i); 
     } 
     } 

     // First emit an extra line break that serves as the end of the statement. 
     this.emit(commonToken(Python3Parser.NEWLINE, "\n")); 

     // Now emit as much DEDENT tokens as needed. 
     while (!indents.isEmpty()) { 
     this.emit(createDedent()); 
     indents.pop(); 
     } 

     // Put the EOF back on the token stream. 
     this.emit(commonToken(Python3Parser.EOF, "<EOF>")); 
    } 

    Token next = super.nextToken(); 

    if (next.getChannel() == Token.DEFAULT_CHANNEL) { 
     // Keep track of the last token on the default channel. 
     this.lastToken = next; 
    } 

    return tokens.isEmpty() ? next : tokens.poll(); 
    } 

    private Token createDedent() { 
    CommonToken dedent = commonToken(Python3Parser.DEDENT, ""); 
    dedent.setLine(this.lastToken.getLine()); 
    return dedent; 
    } 

    private CommonToken commonToken(int type, String text) { 
    int stop = this.getCharIndex() - 1; 
    int start = text.isEmpty() ? stop : stop - text.length() + 1; 
    return new CommonToken(this._tokenFactorySourcePair, type, DEFAULT_TOKEN_CHANNEL, start, stop); 
    } 

    // Calculates the indentation of the provided spaces, taking the 
    // following rules into account: 
    // 
    // "Tabs are replaced (from left to right) by one to eight spaces 
    // such that the total number of characters up to and including 
    // the replacement is a multiple of eight [...]" 
    // 
    // -- https://docs.python.org/3.1/reference/lexical_analysis.html#indentation 
    static int getIndentationCount(String spaces) { 
    int count = 0; 
    for (char ch : spaces.toCharArray()) { 
     switch (ch) { 
     case '\t': 
      count += 8 - (count % 8); 
      break; 
     default: 
      // A normal space char. 
      count++; 
     } 
    } 

    return count; 
    } 

    boolean atStartOfInput() { 
    return super.getCharPositionInLine() == 0 && super.getLine() == 1; 
    } 
} 

single_input 
: NEWLINE 
| simple_stmt 
| compound_stmt NEWLINE 
; 

// more parser rules 

NEWLINE 
: ({atStartOfInput()}? SPACES 
    | ('\r'? '\n' | '\r') SPACES? 
    ) 
    { 
    String newLine = getText().replaceAll("[^\r\n]+", ""); 
    String spaces = getText().replaceAll("[\r\n]+", ""); 
    int next = _input.LA(1); 
    if (opened > 0 || next == '\r' || next == '\n' || next == '#') { 
     // If we're inside a list or on a blank line, ignore all indents, 
     // dedents and line breaks. 
     skip(); 
    } 
    else { 
     emit(commonToken(NEWLINE, newLine)); 
     int indent = getIndentationCount(spaces); 
     int previous = indents.isEmpty() ? 0 : indents.peek(); 
     if (indent == previous) { 
     // skip indents of the same size as the present indent-size 
     skip(); 
     } 
     else if (indent > previous) { 
     indents.push(indent); 
     emit(commonToken(Python3Parser.INDENT, spaces)); 
     } 
     else { 
     // Possibly emit more than 1 DEDENT token. 
     while(!indents.isEmpty() && indents.peek() > indent) { 
      this.emit(createDedent()); 
      indents.pop(); 
     } 
     } 
    } 
    } 
; 

// more lexer rules 

Tomado de: https://github.com/antlr/grammars-v4/blob/master/python3/Python3.g4

+0

Gracias, funciona :) – Astronavigator

+0

De nada @Astronavigator. –

+0

Hola, @Bart Kiers, ¿cómo puedo superar la limitación inicial y final de los límites de línea? Traté de hacerlo emitir un token de sangrado programáticamente antes de comenzar el análisis sintáctico, pero no tuve suerte. – ains

3

¿Ha mirado el Python ANTLR grammar?

Editar: Añadido código Python para crear pseudo GUIÓN/DEDENT fichas

UNKNOWN_TOKEN = 0 
INDENT_TOKEN = 1 
DEDENT_TOKEN = 2 

# filestream has already been processed so that each character is a newline and 
# every tab outside of quotations is converted to 8 spaces. 
def GetIndentationTokens(filestream): 
    # Stores (indentation_token, line, character_index) 
    indentation_record = list() 
    line = 0 
    character_index = 0 
    column = 0 
    counting_whitespace = true 
    indentations = list() 
    for c in filestream: 
     if IsNewLine(c): 
      character_index = 0 
      column = 0 
      line += 1 
      counting_whitespace = true 
     elif c != ' ' and counting_whitespace: 
      counting_whitespace = false 
      if(len(indentations) == 0): 
       indentation_record.append((token, line, character_index)) 
      else: 
       while(len(indentations) > 0 and indentations[-1] != column: 
        if(column < indentations[-1]): 
         indentations.pop() 
         indentation_record.append((
          DEDENT, line, character_index)) 
        elif(column > indentations[-1]): 
         indentations.append(column) 
         indentation_record.append((
          INDENT, line, character_index)) 

     if not IsNewLine(c): 
      column += 1 

     character_index += 1 
    while(len(indentations) > 0): 
     indentations.pop() 
     indentation_record.append((DEDENT_TOKEN, line, character_index)) 
    return indentation_record 
+0

Yep. Esta gramática no tiene implementación de las reglas INDENT y DEDENT. Parece que esta gramática no usa lexer estándar ... – Astronavigator

+0

@Astronavigator Bueno, después de haber analizado [el enfoque del Análisis Léxico de Python para la sangría] (http://docs.python.org/reference/lexical_analysis.html#indentation), su INDENT y los tokens DEDENT se producen en un proceso separado (que se puede realizar antes de pasar a ANTLR). Cuando lo ves a su manera, es mucho más simple. –

+0

Gracias por responder, JSPerfUnknown. Bueno, realizar tokens INDENT y DEDENT antes de pasar a ANTLR es un buen punto. Lo pensare. Por ahora, prefiero usar solo lexer estándar, así que acepta la respuesta de Bart. – Astronavigator

4

Hay una biblioteca de código abierto antlr-denter para ANTLR v4 que ayuda a analizar sangrías y dedeciones por usted. Consulte su README para saber cómo usarlo.

Dado que es una biblioteca, en lugar de fragmentos de código para copiar y pegar en su gramática, su manejo de sangría puede actualizarse por separado del resto de su gramática.

1

Hay una forma relativamente simple de hacer esta ANTLR, que escribí como experimento: Dent.g4. Esta solución es diferente de las otras mencionadas en esta página que fueron escritas por Kiers y Shavit. Se integra con el tiempo de ejecución únicamente a través de una anulación del método nextToken() de Lexer. Hace su trabajo examinando tokens: (1) un token NEWLINE activa el inicio de una fase de "mantener un seguimiento de la sangría"; (2) el espacio en blanco y los comentarios, ambos configurados en el canal HIDDEN, se cuentan e ignoran, respectivamente, durante esa fase; y, (3) cualquier token que no sea HIDDEN finaliza la fase. Por lo tanto, controlar la lógica de indentación es una simple cuestión de establecer un canal de token.

Ambas soluciones mencionadas en esta página requieren un token NEWLINE para capturar también todos los espacios en blanco posteriores, pero al hacerlo no pueden manejar comentarios de varias líneas que interrumpen ese espacio en blanco. Dent, en cambio, mantiene separadas NEWLINE y tokens de espacio en blanco y puede manejar comentarios de varias líneas.

Su gramática se configuraría a continuación. Tenga en cuenta que las reglas NEWLINE y WS lexer tienen acciones que controlan el estado pendingDent y realizan un seguimiento del nivel de sangría con la variable indentCount.

grammar MyGrammar; 

tokens { INDENT, DEDENT } 

@lexer::members { 
    // override of nextToken(), see Dent.g4 grammar on github 
    // https://github.com/wevrem/wry/blob/master/grammars/Dent.g4 
} 

script : (NEWLINE | statement)* EOF ; 

statement 
    : simpleStatement 
    | blockStatements 
    ; 

simpleStatement : LEGIT+ NEWLINE ; 

blockStatements : LEGIT+ NEWLINE INDENT statement+ DEDENT ; 

NEWLINE : ('\r'? '\n' | '\r') { 
    if (pendingDent) { setChannel(HIDDEN); } 
    pendingDent = true; 
    indentCount = 0; 
    initialIndentToken = null; 
} ; 

WS : [ \t]+ { 
    setChannel(HIDDEN); 
    if (pendingDent) { indentCount += getText().length(); } 
} ; 

BlockComment : '/*' (BlockComment | .)*? '*/' -> channel(HIDDEN) ; // allow nesting comments 
LineComment : '//' ~[\r\n]* -> channel(HIDDEN) ; 

LEGIT : ~[ \t\r\n]+ ~[\r\n]*; // Replace with your language-specific rules... 
Cuestiones relacionadas