2011-02-24 128 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