ANTLR Что такое самый простой способ реализовать python, например, грамматику, зависящую от отступа? - программирование
Подтвердить что ты не робот

ANTLR Что такое самый простой способ реализовать python, например, грамматику, зависящую от отступа?

Я пытаюсь реализовать python как грамматику, зависящую от отступа.

Пример источника:

ABC QWE
  CDE EFG
  EFG CDE
    ABC 
  QWE ZXC

Как я вижу, мне нужно реализовать два токена INDENT и DEDENT, поэтому я мог бы написать что-то вроде:

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

Есть ли простой способ реализовать это с помощью ANTLR?

(я бы предпочел, если это возможно, использовать стандартный ANTLR lexer.)

4b9b3361

Ответ 1

Я не знаю, какой самый простой способ справиться с этим, но следующее - это относительно простой способ. Всякий раз, когда вы соответствуете разрыв строки в вашем лексере, при желании сопоставить один или несколько пробелов. Если после разрыва строки есть пробелы, сравните длину этих пробелов с текущим размером отступа. Если это больше, чем текущий размер отступа, испускают Indent маркер, если он меньше, чем текущий отступа размера, испускать Dedent маркер и если это то же самое, ничего не делать.

Вы также хотите, чтобы испускать ряд Dedent лексем в конце файла, чтобы позволить каждому Indent иметь соответствующий Dedent маркер.

Чтобы это работало правильно, вы должны добавить начальный и конечный разрыв строки в ваш исходный файл!

ANTRL3

Быстрая демонстрация:

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

Вы можете проверить парсер с классом:

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

Если вы сейчас поместите в файл с именем in.txt:

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

(Обратите внимание на разрывы в начале и в конце строки!)

тогда вы увидите вывод, который соответствует следующему AST:

enter image description here

Обратите внимание, что моя демонстрация не создаст достаточно последовательных отступов, например, отступ от ccc до aaa (необходимо 2 токена dedent):

aaa
  bbb
    ccc
aaa

Вам необходимо настроить код внутри else if(n < previousIndents) {... } чтобы он мог испускать более 1 токена dedent, основываясь на разнице между n и previousIndents Indent. С макушки головы это может выглядеть так:

 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

Для ANTLR4 сделайте что-то вроде этого:

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

Взято из: https://github.com/antlr/grammars-v4/blob/master/python3/Python3.g4

Ответ 2

Существует библиотека с открытым исходным кодом antlr-denter для ANTLR v4, которая помогает анализировать отступы и разделители для вас, Посмотрите README, чтобы использовать его.

Поскольку это библиотека, а не фрагменты кода для копирования и вставки в вашу грамматику, ее обработка отступов может быть обновлена ​​отдельно от остальной части вашей грамматики.

Ответ 3

Вы просмотрели грамматика ANTLR Python?

Изменить: Добавлен код Python для псевдонимов INDENT/DEDENT

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

Ответ 4

Есть относительно простой способ сделать это ANTLR, который я написал в качестве эксперимента: DentLexer.g4. Это решение отличается от других, упомянутых на этой странице, которые были написаны Киерсом и Шавитом. Он интегрируется со средой выполнения исключительно через переопределение метода Lexer nextToken(). Он выполняет свою работу, проверяя токены: (1) токен NEWLINE запускает фазу "отслеживания отступов"; (2) пробел и комментарии, оба для которых установлен канал HIDDEN, подсчитываются и игнорируются, соответственно, на этом этапе; и (3) любой токен non- HIDDEN завершает фазу. Таким образом, управление логикой отступа является простым вопросом установки канала токена.

Оба решения, упомянутые на этой странице, требуют, чтобы токен NEWLINE также захватывал все последующие пробелы, но при этом не может обрабатывать многострочные комментарии, прерывающие этот пробел. Вместо этого Dent хранит разделители NEWLINE и пробелы отдельно и может обрабатывать многострочные комментарии.

Ваша грамматика будет настроена примерно так, как показано ниже. Обратите внимание, что правила pendingDent NEWLINE и WS имеют действия, которые управляют состоянием pendingDent и отслеживают уровень отступа с indentCount переменной 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...