Интерактивный Antlr - PullRequest
       3

Интерактивный Antlr

4 голосов
/ 25 февраля 2011

Я пытаюсь написать простой интерактивный (используя System.in в качестве исходного) язык с использованием antlr, и у меня есть несколько проблем с ним.Все примеры, которые я нашел в Интернете, используют цикл на строку, например:

while(readline)
  result = parse(line)
  doStuff(result)

Но что, если я пишу что-то вроде pascal / smtp / etc, с видом "первая строка"как X требование?Я знаю, что это можно проверить в doStuff, но я думаю, что логически это часть синтаксиса.

Или что, если команда разбита на несколько строк?Я могу попробовать

while(readline)
  lines.add(line)
  try
    result = parse(lines)
    lines = []
    doStuff(result)
  catch
    nop

Но при этом я также скрываю реальные ошибки.

Или я мог бы каждый раз повторять все строки, но:

  1. это будетбыть медленным
  2. есть инструкции, которые я не хочу выполнять дважды

Можно ли это сделать с помощью ANTLR, или, если нет, с чем-то еще?

Ответы [ 4 ]

5 голосов
/ 25 февраля 2011

Дутов писал:

Или я мог бы каждый раз повторять все строки, но:

это будет медленно есть инструкции, которые я не хочу запускать дважды Можно ли это сделать с помощью ANTLR, или, если нет, с чем-то другим?

Да, ANTLR может сделать это. Возможно, не из коробки, но с небольшим количеством специального кода, это возможно. Вам также не нужно повторно анализировать весь поток токенов для этого.

Допустим, вы хотите построчно анализировать очень простой язык, где каждая строка является либо декларацией program, либо декларацией uses, либо statement.

Он всегда должен начинаться с объявления program, за которым следуют ноль или более uses объявлений, за которыми следует ноль или более statement с. uses объявления не могут идти после statement с, и не может быть более одного program объявления.

Для простоты statement - это просто простое назначение: a = 4 или b = a.

Грамматика ANTLR для такого языка может выглядеть следующим образом:

grammar REPL;

parse
  :  programDeclaration EOF
  |  usesDeclaration EOF
  |  statement EOF
  ;

programDeclaration
  :  PROGRAM ID
  ;

usesDeclaration
  :  USES idList
  ;

statement
  :  ID '=' (INT | ID)
  ;

idList
  :  ID (',' ID)*
  ;

PROGRAM : 'program';
USES    : 'uses';
ID      : ('a'..'z' | 'A'..'Z' | '_') ('a'..'z' | 'A'..'Z' | '_' | '0'..'9')*;
INT     : '0'..'9'+;
SPACE   : (' ' | '\t' | '\r' | '\n') {skip();};

Но нам, конечно, нужно добавить пару проверок. Кроме того, по умолчанию парсер принимает поток токенов в своем конструкторе, но поскольку мы планируем перетекать токены в парсер построчно, нам необходимо создать новый конструктор в нашем парсере. Вы можете добавлять пользовательские элементы в классы лексеров или анализаторов, помещая их в раздел @parser::members { ... } или @lexer::members { ... } соответственно. Мы также добавим пару логических флагов, чтобы отслеживать, было ли уже объявлено объявление program и разрешены ли объявления uses. Наконец, мы добавим метод process(String source), который для каждой новой строки создает лексер, который передается парсеру.

Все это будет выглядеть так:

@parser::members {

  boolean programDeclDone;
  boolean usesDeclAllowed;

  public REPLParser() {
    super(null);
    programDeclDone = false;
    usesDeclAllowed = true;
  }

  public void process(String source) throws Exception {
    ANTLRStringStream in = new ANTLRStringStream(source);
    REPLLexer lexer = new REPLLexer(in);
    CommonTokenStream tokens = new CommonTokenStream(lexer);
    super.setTokenStream(tokens);
    this.parse(); // the entry point of our parser
  } 
}

Теперь внутри нашей грамматики мы собираемся проверить пару стробированных семантических предикатов , анализируем ли мы объявления в правильном порядке. И после анализа определенного объявления или оператора мы захотим перевернуть некоторые логические флаги, чтобы разрешить или запретить объявление с этого момента. Переключение этих логических флагов выполняется через секцию @after { ... } каждого правила, которая выполняется (не удивительно) после , с которыми совпадают токены из этого правила синтаксического анализатора.

Ваш окончательный файл грамматики теперь выглядит следующим образом (включая некоторые System.out.println для целей отладки):

grammar REPL;

@parser::members {

  boolean programDeclDone;
  boolean usesDeclAllowed;

  public REPLParser() {
    super(null);
    programDeclDone = false;
    usesDeclAllowed = true;
  }

  public void process(String source) throws Exception {
    ANTLRStringStream in = new ANTLRStringStream(source);
    REPLLexer lexer = new REPLLexer(in);
    CommonTokenStream tokens = new CommonTokenStream(lexer);
    super.setTokenStream(tokens);
    this.parse();
  } 
}

parse
  :  programDeclaration EOF
  |  {programDeclDone}? (usesDeclaration | statement) EOF
  ;

programDeclaration
@after{
  programDeclDone = true;
}
  :  {!programDeclDone}? PROGRAM ID {System.out.println("\t\t\t program <- " + $ID.text);}
  ;

usesDeclaration
  :  {usesDeclAllowed}? USES idList {System.out.println("\t\t\t uses <- " + $idList.text);}
  ;

statement
@after{
  usesDeclAllowed = false; 
}
  :  left=ID '=' right=(INT | ID) {System.out.println("\t\t\t " + $left.text + " <- " + $right.text);}
  ;

idList
  :  ID (',' ID)*
  ;

PROGRAM : 'program';
USES    : 'uses';
ID      : ('a'..'z' | 'A'..'Z' | '_') ('a'..'z' | 'A'..'Z' | '_' | '0'..'9')*;
INT     : '0'..'9'+;
SPACE   : (' ' | '\t' | '\r' | '\n') {skip();};

, который можно проверить в следующем классе:

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

public class Main {
    public static void main(String[] args) throws Exception {
        Scanner keyboard = new Scanner(System.in);
        REPLParser parser = new REPLParser();
        while(true) {
            System.out.print("\n> ");
            String input = keyboard.nextLine();
            if(input.equals("quit")) {
                break;
            }
            parser.process(input);
        }
        System.out.println("\nBye!");
    }
}

Чтобы запустить этот тестовый класс, сделайте следующее:

# generate a lexer and parser:
java -cp antlr-3.2.jar org.antlr.Tool REPL.g

# compile all .java source files:
javac -cp antlr-3.2.jar *.java

# run the main class on Windows:
java -cp .;antlr-3.2.jar Main 
# or on Linux/Mac:
java -cp .:antlr-3.2.jar Main

Как видите, вы можете объявить program только один раз:

> program A
                         program <- A

> program B
line 1:0 rule programDeclaration failed predicate: {!programDeclDone}?

uses не может прийти после statement s:

> program X
                         program <- X

> uses a,b,c
                         uses <- a,b,c

> a = 666
                         a <- 666

> uses d,e
line 1:0 rule usesDeclaration failed predicate: {usesDeclAllowed}?

и вы должны начать с program объявления:

> uses foo
line 1:0 rule parse failed predicate: {programDeclDone}?
2 голосов
/ 15 октября 2012

Вот пример того, как проанализировать ввод из System.in без предварительного анализа вручную одной строки за раз и без существенных компромиссов в грамматике.Я использую ANTLR 3.4.ANTLR 4, возможно, уже решил эту проблему.Я все еще использую ANTLR 3, и, возможно, кто-то еще с этой проблемой все еще тоже

Прежде чем перейти к решению, я столкнулся с препятствиями, которые мешают решить эту, казалось бы, тривиальную проблему:

  • Встроенные классы ANTLR, которые происходят от CharStream потребляет весь поток данных заранее.Очевидно, что интерактивный режим (или любой другой источник потока неопределенной длины) не может предоставить все данные.
  • Встроенный BufferedTokenStream и производный класс (ы) не будут заканчиваться пропущенными или выключеннымитокен каналаВ интерактивном режиме это означает, что текущий оператор не может завершиться (и, следовательно, не может выполняться), пока не будет использован первый токен следующего оператора или EOF при использовании одного из этих классов.
  • Конец самого оператора может быть неопределенным, пока не начнется следующий оператор.

Рассмотрим простой пример:

statement: 'verb' 'noun' ('and' 'noun')*
         ;
WS: //etc...

Интерактивный анализ одного statement (и только * одного statement) невозможен.Либо начинается следующий statement (т. Е. Вводится «глагол» во входных данных), либо необходимо изменить грамматику, чтобы отметить конец оператора, например, с помощью ';'.

* 1027.* Я не нашел способа управлять многоканальным лексером с помощью моего решения.Это не повредит мне, так как я могу заменить $channel = HIDDEN на skip(), но все же стоит упомянуть об этом ограничении. Грамматике может потребоваться новое правило для упрощения интерактивного анализа.

Например, нормальная точка входа моей грамматики - это правило:

script    
    : statement* EOF -> ^(STMTS statement*) 
    ;

Мой интерактивный сеанс не может начинаться по правилу script, поскольку он не закончится доEOF.Но он также не может начинаться с statement, потому что мой анализатор дерева может использовать STMTS.

Итак, я ввел следующее правило специально для интерактивного сеанса:

interactive
    : statement -> ^(STMTS statement)
    ;

В моем случае нет правил "первой линии", поэтому я не могу сказать, насколько легко или сложнобыло бы сделать что-то подобное для них.Это может быть вопросом создания правила, подобного такому, и выполнения его в начале интерактивного сеанса:

interactive_start
    : first_line
    ;
  • Код грамматики (например, код, который отслеживает символы), возможно, былзаписано в предположении, что срок службы входных данных и срок службы объекта синтаксического анализатора будут фактически одинаковыми.Для моего решения это предположение не выполняется.Синтаксический анализатор заменяется после каждого оператора, поэтому новый анализатор должен иметь возможность отслеживать отслеживание символов (или что-то еще), где остановился последний.Это типичная проблема разделения проблем, поэтому я не думаю, что о ней можно еще что-то сказать.

Первая упомянутая проблема - ограничения встроенного CharStream классы, был моим единственным серьезным зависанием.У ANTLRStringStream есть все, что мне нужно, поэтому я извлек из него свой собственный класс CharStream.Предполагается, что член data базового класса прочитал все последние символы, поэтому мне нужно было переопределить все методы, которые обращаются к нему.Затем я изменил прямое чтение на вызов (новый метод) dataAt, чтобы управлять чтением из потока.Это в основном все, что нужно для этого.Обратите внимание, что код здесь может иметь незамеченные проблемы и не обрабатывать реальные ошибки.

public class MyInputStream extends ANTLRStringStream {
    private InputStream in;

    public MyInputStream(InputStream in) {
        super(new char[0], 0);
        this.in = in;
    }

    @Override
    // copied almost verbatim from ANTLRStringStream
    public void consume() {
        if (p < n) {
            charPositionInLine++;
            if (dataAt(p) == '\n') {
                line++;
                charPositionInLine = 0;
            }
            p++;
        }
    }

    @Override
    // copied almost verbatim from ANTLRStringStream
    public int LA(int i) {
        if (i == 0) {
            return 0; // undefined
        }
        if (i < 0) {
            i++; // e.g., translate LA(-1) to use offset i=0; then data[p+0-1]
            if ((p + i - 1) < 0) {
                return CharStream.EOF; // invalid; no char before first char
            }
        }

        // Read ahead
        return dataAt(p + i - 1);
    }

    @Override
    public String substring(int start, int stop) {
        if (stop >= n) {
            //Read ahead.
            dataAt(stop);
        }
        return new String(data, start, stop - start + 1);
    }

    private int dataAt(int i) {
        ensureRead(i);

        if (i < n) {
            return data[i];
        } else {
            // Nothing to read at that point.
            return CharStream.EOF;
        }
    }

    private void ensureRead(int i) {
        if (i < n) {
            // The data has been read.
            return;
        }

        int distance = i - n + 1;

        ensureCapacity(n + distance);

        // Crude way to copy from the byte stream into the char array.
        for (int pos = 0; pos < distance; ++pos) {
            int read;
            try {
                read = in.read();
            } catch (IOException e) {
                // TODO handle this better.
                throw new RuntimeException(e);
            }

            if (read < 0) {
                break;
            } else {
                data[n++] = (char) read;
            }
        }
    }

    private void ensureCapacity(int capacity) {
        if (capacity > n) {
            char[] newData = new char[capacity];
            System.arraycopy(data, 0, newData, 0, n);
            data = newData;
        }
    }
}

Запуск интерактивного сеанса аналогичен стандартному коду синтаксического анализа, за исключением того, что используется UnbufferedTokenStream, и анализ выполняетсяв цикле:

    MyLexer lex = new MyLexer(new MyInputStream(System.in));
    TokenStream tokens = new UnbufferedTokenStream(lex);

    //Handle "first line" parser rule(s) here.

    while (true) {
        MyParser parser = new MyParser(tokens);
        //Set up the parser here.

        MyParser.interactive_return r = parser.interactive();

        //Do something with the return value.
        //Break on some meaningful condition.
    }

Все еще со мной?Хорошо, вот и все.:)

0 голосов
/ 25 февраля 2011

Вы должны поместить его в doStuff ....

Например, если вы объявляете функцию, синтаксический анализ возвращает функцию, верно?без тела, так что все в порядке, потому что тело придет позже.Вы будете делать то, что делает большинство REPL.

0 голосов
/ 25 февраля 2011

Если вы используете System.in в качестве источника, который является входным потоком, почему бы просто не сделать ANTLR-токенизацию входного потока во время чтения, а затем проанализировать токены?

...