我正在尝试使用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
但有了这个,我也隐藏了真正的错误。
或者我可以每次重新排列所有行,但是:
这可以用ANTLR完成,或者如果没有,可以用其他东西吗?
答案 0 :(得分:4)
Dutow写道:
或者我可以每次重新排列所有行,但是:
它会很慢 有说明我不想跑两次 这可以用ANTLR完成,或者如果没有,还可以用别的东西来完成吗?
是的,ANTLR可以做到这一点。也许没有开箱即用,但有一些自定义代码,它肯定是可能的。您也不需要为它重新解析整个令牌流。
假设您想逐行解析一个非常简单的语言,其中每一行都是program
声明,uses
声明或statement
。
始终应以program
声明开头,然后是零个或多个uses
声明,后跟零个或多个statement
s。在uses
之后,statement
声明无法发出,且program
声明不会超过statement
。
为简单起见,a = 4
只是一个简单的作业:b = a
或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();};
。
这种语言的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所做的事。