【问题标题】:Different lexer rules in different state不同状态下的不同词法分析器规则
【发布时间】:2011-10-22 17:53:52
【问题描述】:

我一直在为一些嵌入在 HTML (FreeMarker) 中的模板语言开发解析器,这里有一个示例:

${abc}
<html> 
<head> 
  <title>Welcome!</title> 
</head> 
<body> 
  <h1> 
    Welcome ${user}<#if user == "Big Joe">, our beloved 
leader</#if>! 
  </h1> 
  <p>Our latest product: 
  <a href="${latestProduct}">${latestProduct}</a>! 
</body> 
</html>

模板语言在一些特定的标签之间,例如'${' '}', ''。介于两者之间的其他原始文本可以视为相同的标记 (RAW)。

这里的关键点是相同的文本,例如一个整数,对于解析器来说意味着不同的事情取决于它是否在这些标签之间,因此需要被视为不同的标记。

我尝试了以下丑陋的实现,并使用自定义状态来指示它是否在这些标签中。如你所见,我几乎必须检查每条规则中的状态,这让我抓狂......

我也想过以下两种解决方案:

  1. 使用多个词法分析器。在这些标签内部/外部时,我可以在两个词法分析器之间切换。但是,对于 ANTLR3,这方面的文档很差。我不知道如何让一个解析器共享两个不同的词法分析器并在它们之间切换。

  2. 在 NUMERICAL_ESCAPE 规则之后将 RAW 规则上移。检查那里的状态,如果它在标签中,则放回令牌并继续尝试左侧规则。这将节省大量的状态检查。但是,我没有找到任何“放回”功能,并且 ANTLR 抱怨某些规则永远无法匹配...

有没有一个优雅的解决方案?

grammar freemarker_simple;

@lexer::members {
int freemarker_type = 0;
}

expression
    :   primary_expression ;

primary_expression
    :   number_literal | identifier | parenthesis | builtin_variable
    ;

parenthesis
    :   OPEN_PAREN expression CLOSE_PAREN ;

number_literal
    :   INTEGER | DECIMAL
    ;

identifier
    :   ID
    ;

builtin_variable
    :   DOT ID
    ;

string_output
    :   OUTPUT_ESCAPE expression CLOSE_BRACE
    ;

numerical_output
    :   NUMERICAL_ESCAPE expression  CLOSE_BRACE
    ;

if_expression
    :   START_TAG IF expression DIRECTIVE_END optional_block
        ( START_TAG ELSE_IF expression loose_directive_end optional_block )*
        ( END_TAG ELSE optional_block )?
        END_TAG END_IF
    ;

list    :   START_TAG LIST expression AS ID DIRECTIVE_END optional_block END_TAG END_LIST ;

for_each
    :   START_TAG FOREACH ID IN expression DIRECTIVE_END optional_block END_TAG END_FOREACH ;

loose_directive_end
    :   ( DIRECTIVE_END | EMPTY_DIRECTIVE_END ) ;

freemarker_directive
    :   ( if_expression | list | for_each  ) ;
content :   ( RAW |  string_output | numerical_output | freemarker_directive ) + ;
optional_block
    :   ( content )? ;

root    :   optional_block EOF  ;

START_TAG
    :   '<#'
        { freemarker_type = 1; }
    ;

END_TAG :   '</#'
        { freemarker_type = 1; }
    ;

DIRECTIVE_END
    :   '>'
        {
        if(freemarker_type == 0) $type=RAW;
        freemarker_type = 0;
        }
    ;
EMPTY_DIRECTIVE_END
    :   '/>'
        {
        if(freemarker_type == 0) $type=RAW;
        freemarker_type = 0;
        }
    ;

OUTPUT_ESCAPE
    :   '${'
        { if(freemarker_type == 0) freemarker_type = 2; }
    ;
NUMERICAL_ESCAPE
    :   '#{'
        { if(freemarker_type == 0) freemarker_type = 2; }
    ;

IF  :   'if'
        { if(freemarker_type == 0) $type=RAW; }
    ;
ELSE    :   'else' DIRECTIVE_END
        { if(freemarker_type == 0) $type=RAW; }
    ; 
ELSE_IF :   'elseif'
        { if(freemarker_type == 0) $type=RAW; }
    ; 
LIST    :   'list'
        { if(freemarker_type == 0) $type=RAW; }
    ; 
FOREACH :   'foreach'
        { if(freemarker_type == 0) $type=RAW; }
    ; 
END_IF  :   'if' DIRECTIVE_END
        { if(freemarker_type == 0) $type=RAW; }
    ; 
END_LIST
    :   'list' DIRECTIVE_END
        { if(freemarker_type == 0) $type=RAW; }
    ; 
END_FOREACH
    :   'foreach' DIRECTIVE_END
        { if(freemarker_type == 0) $type=RAW; }
    ;


FALSE: 'false' { if(freemarker_type == 0) $type=RAW; };
TRUE: 'true' { if(freemarker_type == 0) $type=RAW; };
INTEGER: ('0'..'9')+ { if(freemarker_type == 0) $type=RAW; };
DECIMAL: INTEGER '.' INTEGER { if(freemarker_type == 0) $type=RAW; };
DOT: '.' { if(freemarker_type == 0) $type=RAW; };
DOT_DOT: '..' { if(freemarker_type == 0) $type=RAW; };
PLUS: '+' { if(freemarker_type == 0) $type=RAW; };
MINUS: '-' { if(freemarker_type == 0) $type=RAW; };
TIMES: '*' { if(freemarker_type == 0) $type=RAW; };
DIVIDE: '/' { if(freemarker_type == 0) $type=RAW; };
PERCENT: '%' { if(freemarker_type == 0) $type=RAW; };
AND: '&' | '&&' { if(freemarker_type == 0) $type=RAW; };
OR: '|' | '||' { if(freemarker_type == 0) $type=RAW; };
EXCLAM: '!' { if(freemarker_type == 0) $type=RAW; };
OPEN_PAREN: '(' { if(freemarker_type == 0) $type=RAW; };
CLOSE_PAREN: ')' { if(freemarker_type == 0) $type=RAW; };
OPEN_BRACE
    :   '{'
    { if(freemarker_type == 0) $type=RAW; }
    ;
CLOSE_BRACE
    :   '}'
    {
        if(freemarker_type == 0) $type=RAW;
        if(freemarker_type == 2) freemarker_type = 0;
    }
    ;
IN: 'in' { if(freemarker_type == 0) $type=RAW; };
AS: 'as' { if(freemarker_type == 0) $type=RAW; };
ID  :   ('A'..'Z'|'a'..'z')+
    //{ if(freemarker_type == 0) $type=RAW; }
    ;

BLANK   :   ( '\r' | ' ' | '\n' | '\t' )+
    {
        if(freemarker_type == 0) $type=RAW;
        else $channel = HIDDEN;
    }
    ;

RAW
    :   .
    ;

编辑

我发现了类似于How do I lex this input? 的问题,其中需要“启动条件”。但不幸的是,答案也使用了很多谓词,就像我的州一样。

现在,我尝试使用谓词将 RAW 移到更高的位置。希望在 RAW 规则之后消除所有状态检查。但是,我的示例输入失败,第一行结束被识别为 BLANK 而不是它应该是 RAW。

我想规则优先级有问题: CLOSE_BRACE匹配后,下一个token从CLOSE_BRACE规则之后的规则开始匹配,而不是从头开始。

有什么办法解决这个问题?

以下带有一些调试输出的新语法:

grammar freemarker_simple;

@lexer::members {
int freemarker_type = 0;
}

expression
    :   primary_expression ;

primary_expression
    :   number_literal | identifier | parenthesis | builtin_variable
    ;

parenthesis
    :   OPEN_PAREN expression CLOSE_PAREN ;

number_literal
    :   INTEGER | DECIMAL
    ;

identifier
    :   ID
    ;

builtin_variable
    :   DOT ID
    ;

string_output
    :   OUTPUT_ESCAPE expression CLOSE_BRACE
    ;

numerical_output
    :   NUMERICAL_ESCAPE expression  CLOSE_BRACE
    ;

if_expression
    :   START_TAG IF expression DIRECTIVE_END optional_block
        ( START_TAG ELSE_IF expression loose_directive_end optional_block )*
        ( END_TAG ELSE optional_block )?
        END_TAG END_IF
    ;

list    :   START_TAG LIST expression AS ID DIRECTIVE_END optional_block END_TAG END_LIST ;

for_each
    :   START_TAG FOREACH ID IN expression DIRECTIVE_END optional_block END_TAG END_FOREACH ;

loose_directive_end
    :   ( DIRECTIVE_END | EMPTY_DIRECTIVE_END ) ;

freemarker_directive
    :   ( if_expression | list | for_each  ) ;
content :   ( RAW |  string_output | numerical_output | freemarker_directive ) + ;
optional_block
    :   ( content )? ;

root    :   optional_block EOF  ;

START_TAG
    :   '<#'
        { freemarker_type = 1; }
    ;

END_TAG :   '</#'
        { freemarker_type = 1; }
    ;

OUTPUT_ESCAPE
    :   '${'
        { if(freemarker_type == 0) freemarker_type = 2; }
    ;
NUMERICAL_ESCAPE
    :   '#{'
        { if(freemarker_type == 0) freemarker_type = 2; }
    ;
RAW
    :
        { freemarker_type == 0 }?=> .
        {System.out.printf("RAW \%s \%d\n",getText(),freemarker_type);}
    ;

DIRECTIVE_END
    :   '>'
        { if(freemarker_type == 1) freemarker_type = 0; }
    ;
EMPTY_DIRECTIVE_END
    :   '/>'
        { if(freemarker_type == 1) freemarker_type = 0; }
    ;

IF  :   'if'

    ;
ELSE    :   'else' DIRECTIVE_END

    ; 
ELSE_IF :   'elseif'

    ; 
LIST    :   'list'

    ; 
FOREACH :   'foreach'

    ; 
END_IF  :   'if' DIRECTIVE_END
    ; 
END_LIST
    :   'list' DIRECTIVE_END
    ; 
END_FOREACH
    :   'foreach' DIRECTIVE_END
    ;


FALSE: 'false' ;
TRUE: 'true' ;
INTEGER: ('0'..'9')+ ;
DECIMAL: INTEGER '.' INTEGER ;
DOT: '.' ;
DOT_DOT: '..' ;
PLUS: '+' ;
MINUS: '-' ;
TIMES: '*' ;
DIVIDE: '/' ;
PERCENT: '%' ;
AND: '&' | '&&' ;
OR: '|' | '||' ;
EXCLAM: '!' ;
OPEN_PAREN: '(' ;
CLOSE_PAREN: ')' ;
OPEN_BRACE
    :   '{'
    ;
CLOSE_BRACE
    :   '}'
    { if(freemarker_type == 2) {freemarker_type = 0;} }
    ;
IN: 'in' ;
AS: 'as' ;
ID  :   ('A'..'Z'|'a'..'z')+
    { System.out.printf("ID \%s \%d\n",getText(),freemarker_type);}
    ;

BLANK   :   ( '\r' | ' ' | '\n' | '\t' )+
    {
        System.out.printf("BLANK \%d\n",freemarker_type);
        $channel = HIDDEN;
    }
    ;

我的输入结果与输出:

ID abc 2
BLANK 0  <<< incorrect, should be RAW when state==0
RAW < 0  <<< correct
ID html 0 <<< incorrect, should be RAW RAW RAW RAW
RAW > 0

编辑2

还尝试了使用 Bart 语法的第二种方法,仍然无法将“html”识别为 ID,应该是 4 个 RAW。当mmode=false时,不应该先匹配RAW吗?还是词法分析器在这里仍然选择最长的匹配?

grammar freemarker_bart;

options {
  output=AST;
  ASTLabelType=CommonTree;
}

tokens {
  FILE;
  OUTPUT;
  RAW_BLOCK;
}

@parser::members {

  // merge a given list of tokens into a single AST
  private CommonTree merge(List tokenList) {
    StringBuilder b = new StringBuilder();
    for(int i = 0; i < tokenList.size(); i++) {
      Token token = (Token)tokenList.get(i);
      b.append(token.getText());
    }
    return new CommonTree(new CommonToken(RAW, b.toString()));
  }
}

@lexer::members {
  private boolean mmode = false;
}

parse
  :  content* EOF -> ^(FILE content*)
  ;

content
  :  (options {greedy=true;}: t+=RAW)+ -> ^(RAW_BLOCK {merge($t)})
  |  if_stat
  |  output
  ;

if_stat
  :  TAG_START IF expression TAG_END raw_block TAG_END_START IF TAG_END -> ^(IF expression raw_block)
  ;

output
  :  OUTPUT_START expression OUTPUT_END -> ^(OUTPUT expression)
  ;

raw_block
  :  (t+=RAW)* -> ^(RAW_BLOCK {merge($t)})
  ;

expression
  :  eq_expression
  ;

eq_expression
  :  atom (EQUALS^ atom)* 
  ;

atom
  :  STRING
  |  ID
  ;

// these tokens denote the start of markup code (sets mmode to true)
OUTPUT_START  : '${'  {mmode=true;};
TAG_START     : '<#'  {mmode=true;};
TAG_END_START : '</' ('#' {mmode=true;} | ~'#' {$type=RAW;});

RAW           : {!mmode}?=> . ;

// these tokens denote the end of markup code (sets mmode to false)
OUTPUT_END    : '}' {mmode=false;};
TAG_END       : '>' {mmode=false;};

// valid tokens only when in "markup mode"
EQUALS        : '==';
IF            : 'if';
STRING        : '"' ~'"'* '"';
ID            : ('a'..'z' | 'A'..'Z')+;
SPACE         : (' ' | '\t' | '\r' | '\n')+ {skip();};

【问题讨论】:

    标签: antlr state antlr3 lexer


    【解决方案1】:

    您可以使用gated semantic predicates 让词法分析器规则匹配,您可以在其中测试某个布尔表达式。

    一个小演示:

    freemarker_simple.g

    grammar freemarker_simple;
    
    options {
      output=AST;
      ASTLabelType=CommonTree;
    }
    
    tokens {
      FILE;
      OUTPUT;
      RAW_BLOCK;
    }
    
    @parser::members {
    
      // merge a given list of tokens into a single AST
      private CommonTree merge(List tokenList) {
        StringBuilder b = new StringBuilder();
        for(int i = 0; i < tokenList.size(); i++) {
          Token token = (Token)tokenList.get(i);
          b.append(token.getText());
        }
        return new CommonTree(new CommonToken(RAW, b.toString()));
      }
    }
    
    @lexer::members {
      private boolean mmode = false;
    }
    
    parse
      :  content* EOF -> ^(FILE content*)
      ;
    
    content
      :  (options {greedy=true;}: t+=RAW)+ -> ^(RAW_BLOCK {merge($t)})
      |  if_stat
      |  output
      ;
    
    if_stat
      :  TAG_START IF expression TAG_END raw_block TAG_END_START IF TAG_END -> ^(IF expression raw_block)
      ;
    
    output
      :  OUTPUT_START expression OUTPUT_END -> ^(OUTPUT expression)
      ;
    
    raw_block
      :  (t+=RAW)* -> ^(RAW_BLOCK {merge($t)})
      ;
    
    expression
      :  eq_expression
      ;
    
    eq_expression
      :  atom (EQUALS^ atom)* 
      ;
    
    atom
      :  STRING
      |  ID
      ;
    
    // these tokens denote the start of markup code (sets mmode to true)
    OUTPUT_START  : '${'  {mmode=true;};
    TAG_START     : '<#'  {mmode=true;};
    TAG_END_START : '</' ('#' {mmode=true;} | ~'#' {$type=RAW;});
    
    // these tokens denote the end of markup code (sets mmode to false)
    OUTPUT_END    : {mmode}?=> '}' {mmode=false;};
    TAG_END       : {mmode}?=> '>' {mmode=false;};
    
    // valid tokens only when in "markup mode"
    EQUALS        : {mmode}?=> '==';
    IF            : {mmode}?=> 'if';
    STRING        : {mmode}?=> '"' ~'"'* '"';
    ID            : {mmode}?=> ('a'..'z' | 'A'..'Z')+;
    SPACE         : {mmode}?=> (' ' | '\t' | '\r' | '\n')+ {skip();};
    
    RAW           : . ;
    

    解析您的输入:

    test.html

    ${abc}
    <html> 
    <head> 
      <title>Welcome!</title> 
    </head> 
    <body> 
      <h1> 
        Welcome ${user}<#if user == "Big Joe">, our beloved leader</#if>! 
      </h1> 
      <p>Our latest product: <a href="${latestProduct}">${latestProduct}</a>!</p>
    </body> 
    </html>
    

    进入以下 AST:

    因为你可以在课堂上测试自己:

    Main.java

    import org.antlr.runtime.*;
    import org.antlr.runtime.tree.*;
    import org.antlr.stringtemplate.*;
    
    public class Main {
      public static void main(String[] args) throws Exception {
        freemarker_simpleLexer lexer = new freemarker_simpleLexer(new ANTLRFileStream("test.html"));
        freemarker_simpleParser parser = new freemarker_simpleParser(new CommonTokenStream(lexer));
        CommonTree tree = (CommonTree)parser.parse().getTree();
        DOTTreeGenerator gen = new DOTTreeGenerator();
        StringTemplate st = gen.toDOT(tree);
        System.out.println(st);
      }
    }
    

    编辑 1

    当我使用从您发布的第二个语法生成的解析器运行您的示例输入时,以下是前 5 行打印到控制台(不计算生成的许多警告):

    ID abc 2
    RAW 
     0
    RAW < 0
    ID html 0
    ...
    

    编辑 2

    布德写道:

    还尝试了使用 Bart 语法的第二种方法,仍然无法将“html”识别为 ID,应该是 4 个 RAW。当mmode=false时,不应该先匹配RAW吗?还是词法分析器仍然在这里选择最长的匹配?

    是的,这是正确的:在这种情况下,ANTLR 会选择更长的匹配项。

    但是现在我(终于 :))看到了您要做什么,这是最后一个建议:您可以让 RAW 规则匹配字符,只要该规则看不到以下字符之一前面的序列:"&lt;#""&lt;/#""${"。请注意,该规则仍必须保留在语法的末尾。此检查在词法分析器内部执行。此外,在这种情况下,您不需要解析器中的 merge(...) 方法:

    grammar freemarker_simple;
    
    options {
      output=AST;
      ASTLabelType=CommonTree;
    }
    
    tokens {
      FILE;
      OUTPUT;
      RAW_BLOCK;
    }
    
    @lexer::members {
      
      private boolean mmode = false;
      
      private boolean rawAhead() {
        if(mmode) return false;
        int ch1 = input.LA(1), ch2 = input.LA(2), ch3 = input.LA(3);
        return !(
            (ch1 == '<' && ch2 == '#') ||
            (ch1 == '<' && ch2 == '/' && ch3 == '#') ||
            (ch1 == '$' && ch2 == '{')
        );
      }
    }
    
    parse
      :  content* EOF -> ^(FILE content*)
      ;
    
    content
      :  RAW
      |  if_stat
      |  output
      ;
    
    if_stat
      :  TAG_START IF expression TAG_END RAW TAG_END_START IF TAG_END -> ^(IF expression RAW)
      ;
    
    output
      :  OUTPUT_START expression OUTPUT_END -> ^(OUTPUT expression)
      ;
    
    expression
      :  eq_expression
      ;
    
    eq_expression
      :  atom (EQUALS^ atom)*
      ;
    
    atom
      :  STRING
      |  ID
      ;
    
    OUTPUT_START  : '${'  {mmode=true;};
    TAG_START     : '<#'  {mmode=true;};
    TAG_END_START : '</' ('#' {mmode=true;} | ~'#' {$type=RAW;});
    
    OUTPUT_END    : '}' {mmode=false;};
    TAG_END       : '>' {mmode=false;};
    
    EQUALS        : '==';
    IF            : 'if';
    STRING        : '"' ~'"'* '"';
    ID            : ('a'..'z' | 'A'..'Z')+;
    SPACE         : (' ' | '\t' | '\r' | '\n')+ {skip();};
    
    RAW           : ({rawAhead()}?=> . )+;
    

    上面的语法将从这个答案开头发布的输入产生以下 AST:

    【讨论】:

    • 好吧,在这种情况下,我必须对“标记模式”中的所有可能标记使用谓词,对吧?我认为这与我在问题中所说的“freemarker_type”状态基本相同。但是仍然非常感谢,因为您提供了一个生动的版本,带有 AST 生成和 RAW 令牌合并,这是我以后必须找到的所有内容,非常有帮助,谢谢 :)
    • 想知道有没有更优雅的方法来做到这一点?你知道为什么我的第二种方法失败了吗?
    • 是的,它基本上和你提到的一样,但是我发布了一个工作版本,因为你说你的不工作(或者有错误/警告?)。是的,您必须在属于您的“内在语言”的标记前面放置一个谓词。不,我不认为你会得到任何更优雅的东西,因为你基本上是在一个语法中塞满两种语言。当然,不客气。
    • 不,我看了你的第二种方法,但没有立即明白,抱歉。
    • 我在第二种方法中所做的是增加 RAW 规则的优先级,希望它在不处于 mmode 时在大多数其他令牌之前得到匹配。因此,我可以删除“大多数其他标记”的那些谓词,因为只有当 RAW 规则的谓词失败时,即在 mmode 中,ANTLR 才会访问它们。
    猜你喜欢
    • 2021-11-18
    • 1970-01-01
    • 1970-01-01
    • 2020-09-14
    • 2013-04-29
    • 2022-10-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多