2011-02-24 127 views
4

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

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

但如果我写的东西像帕斯卡尔/ SMTP /等,着有“行头”是什么样子X需求量的?我知道它可以在doStuff中检查,但我认为它是语法的一部分。

或者如果一个命令被分成多行?我可以尝试

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

但是,我也隐藏了真正的错误。

或者,我可以每次重新分析所有行,但:

  1. 这将是缓慢的
  2. 有说明,我不想跑两次

可以这样用ANTLR完成或者如果没有,还有别的东西?

+0

你能举一个这样的“看起来像X”输入的具体例子吗? – 2011-02-24 22:03:13

+0

我的意思是说,pascal程序的第一行必须是“程序X”,使用声明(“使用x,y,z”)是可选的,但是如果指定必须在程序声明之后。因此,在类似帕斯卡的外壳中,第一个有效表达式总是“程序x”; – Dutow 2011-02-24 22:26:43

回答

4

Dutow写道:

或者,我可以每次重新分析所有行,但:

这将是缓慢的 有说明,我不想跑两次 可以这样用ANTLR完成,或者如果没有,用别的东西?

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

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

应该总是以一个program声明,其次是零点或多个uses声明后跟零个或多个statement先从。 uses声明不能在statement之后出现,并且不能有多于一个的program声明。

为简单起见,statement只是一个简单的赋值:a = 4b = 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 
    } 
} 

现在我们的语法里面,我们将通过几个gated semantic predicates检查,如果我们按照正确的顺序解析声明。在解析某个声明或声明之后,我们需要翻转某些布尔标志来允许或者不允许从此声明。这些布尔标志的翻转是通过每条规则的@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}? 
0

如果您使用System.in作为源(输入流),为什么不只是在读取输入流时对它进行标记,然后解析标记呢?

0

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

举例来说,如果你声明一个函数,解析会返回一个函数吧?没有身体,所以,这很好,因为身体会晚点来。你会做大多数REPL所做的。

2

下面是如何解析来自System.in的输入的示例,而不是先一次一行地手动解析它,并且不会在语法中造成重大损害。我正在使用ANTLR 3.4。 ANTLR 4可能已经解决了这个问题。不过,我仍然在使用ANTLR 3,也许还有其他人也有这个问题。

之前进入的解决方案,下面是我遇到的障碍不断被轻松解决这个看似微不足道的问题:

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

考虑一个简单的例子:

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

交互解析单个statement(和单个statement)是不可能的。必须开始下一个statement(即,在输入中命中“动词”),或者必须修改语法以标记语句的结尾,例如,与​​3210。

  • 我还没有找到一种方法来管理我的解决方案的多通道词法分析器。它不会伤害我,因为我可以用skip()替换$channel = HIDDEN,但它仍然是一个值得一提的限制。
  • 语法可能需要一个新规则来简化交互式解析。

例如,我的语法的正常切入点是这个规则:

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

做得很好。我一直在研究解析大文件的问题,并且需要一种将伪EOF标记插入到流中的方法,以便重新输入解析器,以便解析输入流的“块”。这将限制解析树的大小,我在ANTLR4中使用它来驱动解析树监听器。所以,'第一行和编码模式我已经有了,而且我很难管理每个解析调用中输入令牌空间ANTLR4会消耗多少。你所说的问题与我的密切配合。 – 2017-03-01 16:18:19