匹配类似字符串的Antlr lexer令牌,如果贪婪的lexer犯了错误怎么办



似乎有时Antlr lexer在标记字符流时对使用哪条规则做出了错误的选择。。。我正试图找出如何帮助Antlr做出显而易见的人权选择。我想解析这样的文本:

d/dt(x)=a
a=d/dt
d=3
dt=4

这是一种现有语言使用的不幸语法,我正试图为其编写解析器。"d/dt(x)"表示微分方程的左手边。如果必须的话,忽略这个行话,只要知道它不是"d"除以"dt"。然而,"d/dt"的第二次出现实际上是"d"除以"dt"。

这是我的语法:

grammar diffeq_grammar;
program :   (statement? NEWLINE)*;
statement
    :   diffeq
    |   assignment;
diffeq  :   DDT ID ')' '=' ID;
assignment
    :   ID '=' NUMBER
    |   ID '=' ID '/' ID
    ;
DDT :   'd/dt(';
ID  :   'a'..'z'+;
NUMBER  :   '0'..'9'+;
NEWLINE :   'rn'|'r'|'n';

当使用这个语法时,lexer会抓住第一个"d/dt()",并将其转换为标记DDT。太好了!现在,lexer看到第二个"d"后面跟着一个"/",并说"嗯,我可以将其匹配为ID和一个'/',或者我可以贪婪并匹配DDT"。lexer选择贪婪…但它几乎不知道,没有"("输入流中稍后的几个字符。当lexer查找丢失的"("时,它抛出一个MismatchedTokenException!

到目前为止,我找到的唯一解决方案是将所有规则移动到语法分析器中,语法如下:

grammar diffeq_grammar;
program :   (statement? NEWLINE)*;
statement
    :   diffeq
    |   assignment;
diffeq  :   ddt id ')' '=' id;
assignment
    :   id '=' number
    |   id '=' id '/' id
    ;
ddt :   'd' '/' 'd' 't' '(';
id  :   CHAR+;
number  :   DIGIT+;
CHAR    :   'a'..'z';
DIGIT   :   '0'..'9';
NEWLINE :   'rn'|'r'|'n';

如果我还没有成千上万行依赖于第一个语法工作的工作代码,这是一个很好的解决方案。在花了两天时间研究这个问题后,我得出结论,lexer。。。真的应该能够区分这两种情况。在某个时刻,Antlr lexer在两个规则之间做出决定:DDT和ID。它选择DDT是因为lexer贪婪。但是当匹配DDT失败时,我希望lexer重新使用ID.

只要语法基本保持不变,我就可以使用谓词或其他技巧(即,lexer中的规则,留在lexer中。大多数规则都不受影响。)。

理想情况下,我可以使用任何有效的Antlr代码修改DDT的lexer规则。。。并完成。

我的目标语言是Java。

谢谢!

更新

谢谢你们的回答!!我接受了最符合我问题的答案。我使用的实际解决方案是我自己的答案(而不是公认的答案),还有更多的答案本可以奏效。读者们,看看所有的答案;其中一些可能比我的更适合你的情况。

只要语法基本保持不变,我就可以使用谓词或其他技巧(即,lexer中的规则,留在lexer中。大多数规则都不受影响。)。

在这种情况下,强制lexer在char流中向前看,以确保确实存在使用门控语法谓词的"d/dt("

演示:

grammar diffeq_grammar;
@parser::members {
  public static void main(String[] args) throws Exception {
    String src = 
        "d/dt(x)=an" +
        "a=d/dtn" +
        "d=3n" +
        "dt=4n";
    diffeq_grammarLexer lexer = new diffeq_grammarLexer(new ANTLRStringStream(src));
    diffeq_grammarParser parser = new diffeq_grammarParser(new CommonTokenStream(lexer));
    parser.program();
  }
}
@lexer::members {
  private boolean ahead(String text) {
    for(int i = 0; i < text.length(); i++) {
      if(input.LA(i + 1) != text.charAt(i)) {
        return false;
      }
    }
    return true;
  }
}
program
 : (statement? NEWLINE)* EOF
 ;
statement
 : diffeq     {System.out.println("diffeq     : " + $text);}
 | assignment {System.out.println("assignment : " + $text);}
 ;
diffeq
 : DDT ID ')' '=' ID
 ;
assignment
 : ID '=' NUMBER
 | ID '=' ID '/' ID
 ;
DDT     : {ahead("d/dt(")}?=> 'd/dt(';
ID      : 'a'..'z'+;
NUMBER  : '0'..'9'+;
NEWLINE : 'rn' | 'r' | 'n';

如果你现在运行演示:

java-cp antlr-3.3.jar org.antlr.Tool diffiq_grammar.gjavac-cpantlr-3.3.jar*.javajava-cp.:antlr-3.3.jar diffeq_grammarParser

(使用Windows时,在最后一个命令中将:替换为;

您将看到以下输出:

diffeq:d/dt(x)=a赋值:a=d/dt赋值:d=3赋值:dt=4

尽管考虑到项目中有大量的工作代码,这不是您想要做的,但您仍然应该考虑更彻底地分离解析器和lexer。我最好让解析器和lexer做他们最擅长的事情,而不是把它们"融合"在一起。出现问题的最明显迹象是()令牌之间缺乏对称性:一个是复合令牌的一部分,而另一个是独立令牌。

如果重构是一种选择,那么您可以像这样更改解析器和lexer:

grammar diffeq_grammar;
program :   (statement? NEWLINE)* EOF; // <-- You forgot EOF
statement
    :   diffeq
    |   assignment;
diffeq  :   D OVER DT OPEN id CLOSE EQ id; // <-- here, id is a parser rule
assignment
    :   id EQ NUMBER
    |   id EQ id OVER id
    ;
id  : ID | D | DT; // <-- Nice trick, isn't it?
D       : 'D';
DT      : 'DT';
OVER    : '/';
EQ      : '=';
OPEN    : '(';
CLOSE   : ')';
ID      : 'a'..'z'+;
NUMBER  : '0'..'9'+;
NEWLINE : 'rn'|'r'|'n';

您可能需要启用回溯和记忆功能才能正常工作(但请先尝试在不回溯的情况下编译它)。

这是我最后使用的解决方案。我知道这违反了我的一个要求:将lexer规则保留在lexer中,将解析器规则保留在解析器中,但事实证明,将DDT移动到DDT不需要更改我的代码。此外,dasblinkenlight在他的回答和评论中对不匹配的括号提出了一些很好的观点。

grammar ddt_problem;
program :   (statement? NEWLINE)*;
statement
    :   diffeq
    |   assignment;
diffeq  :   ddt ID ')' '=' ID;
assignment
    :   ID '=' NUMBER
    |   ID '=' ID '/' ID
    ;
ddt :   ( d=ID ) { $d.getText().equals("d") }? '/' ( dt=ID ) { $dt.getText().equals("dt") }? '(';
ID  :   'a'..'z'+;
NUMBER  :   '0'..'9'+;
NEWLINE :   'rn'|'r'|'n';

最新更新