互动式Antlr

时间:2011-02-24 21:09:22

标签: java parsing antlr

我正在尝试使用antlr编写一个简单的交互式(使用System.in作为源代码)语言,我遇到了一些问题。我在网上找到的例子都是使用每行周期,例如:

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. 有说明我不想再跑两次
  3. 这可以用ANTLR完成,或者如果没有,可以用其他东西吗?

4 个答案:

答案 0 :(得分:4)

  

Dutow写道:

     

或者我可以每次重新排列所有行,但是:

     

它会很慢   有说明我不想跑两次   这可以用ANTLR完成,或者如果没有,还可以用别的东西来完成吗?

是的,ANTLR可以做到这一点。也许没有开箱即用,但有一些自定义代码,它肯定是可能的。您也不需要为它重新解析整个令牌流。

假设您想逐行解析一个非常简单的语言,其中每一行都是program声明,uses声明或statement

始终应以program声明开头,然后是零个或多个uses声明,后跟零个或多个statement s。在uses之后,statement声明无法发出,且program声明不会超过statement

为简单起见,a = 4只是一个简单的作业:b = agrammar 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();};

这种语言的ANTLR语法可能如下所示:

@parser::members { ... }

但是,我们当然需要添加几个检查。此外,默认情况下,解析器在其构造函数中采用令牌流,但由于我们计划逐行地在解析器中处理令牌,因此我们需要在解析器中创建新的构造函数。您可以将lexer或解析器类中的自定义成员分别放在@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 { ... }

现在在我们的语法中,如果我们按照正确的顺序解析声明,我们将检查几个 gated semantic predicates 。在解析某个声明或语句之后,我们将要翻转某些布尔标志以允许或禁止从那时开始声明。这些布尔标志的翻转是通过每个规则的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无法在> 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}? 声明开头:

{{1}}

答案 1 :(得分:2)

这是一个如何解析来自System.in的输入的示例,而不是先一次手动解析一行而不在语法中做出重大妥协。我正在使用ANTLR 3.4。 ANTLR 4可能已经解决了这个问题。不过,我仍在使用ANTLR 3,也许其他人也遇到过这个问题。

在进入解决方案之前,我遇到的障碍是让这个看似微不足道的问题变得容易解决:

  • 派生自CharStream的内置ANTLR类会预先消耗整个数据流。显然,交互模式(或任何其他不确定长度的流源)无法提供所有数据。
  • 内置的BufferedTokenStream和派生类不会以跳过或非通道令牌结束。在交互式设置中,这意味着当使用其中一个类时,当前语句无法结束(因此无法执行),直到下一个语句的第一个标记或EOF被消耗为止。
  • 在下一个语句开始之前,语句本身的结尾可能是不确定的。

考虑一个简单的例子:

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

无法以交互方式解析单个statement(以及单个statement)。要么必须启动下一个statement(即,在输入中点击“动词”),要么必须修改语法以标记语句的结尾,例如:使用';'

  • 我没有找到一种方法来管理我的解决方案的多通道词法分析器。它不会伤害我,因为我可以用$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.
    }

还在我身边吗?好的,就是这样。 :)

答案 2 :(得分:0)

如果您使用System.in作为输入流源,为什么不在输入流时将ANTLR标记为输入流,然后解析标记?

答案 3 :(得分:0)

你必须把它放在doStuff ....

例如,如果你要声明一个函数,那么解析会返回一个函数吗?没有身体,所以,那很好,因为身体会来得更晚。你会做大多数REPL所做的事。