ANTLR 获取并拆分词法分析器内容

1 投票
2 回答
3605 浏览
提问于 2025-04-16 17:14

首先,抱歉我的英语,我还在学习中。

我正在为我的框架编写一个Python模块,用来解析CSS文件。我尝试过正则表达式和ply(Python的词法分析器和解析器),但最后发现自己在用ANTLR。

第一次尝试,我需要从CSS文件中解析注释。以下是我需要解析的CSS字符串:

/*test*/

/*
test1
/*

/*test2/*nested*/comment/*

我知道CSS不允许嵌套注释,但我在我的框架中需要这个功能。我写了一个简单的ANTLR语法:

grammar CSS;

options {
    language = Python;
}

styleSheet
    : comments EOF ;

comments
    : NESTED_ML_COMMENT*
    ;

NESTED_ML_COMMENT
    :   '/*' 
        (options {greedy=false;} : (NESTED_ML_COMMENT | . ) )* 
        '*/' 
    ;

LINEBREAK 
    :  ('\n\r' | '\n')+{$channel=HIDDEN; };

我得到的结果是:

这里输入图片描述

我期望的结果(画得不错 :D):

这里输入图片描述

请注意,我想在结果中看到 /* 和 */。

有没有办法在纯ANTLR中做到这一点?我对在ANTLR中使用Python没有问题,但如果有办法不使用Python实现这一点,我将非常感激。

2 个回答

0

你应该使用 !^ 这两个提示来处理抽象语法树(AST)。如果你想让 /* 在你的 AST 中不出现,就在它后面加上 !。而如果你想控制哪些元素成为 AST 子树的根节点,就在它后面加上 ^。这可能看起来像这样:

NESTED_ML_COMMENT
:   COMMENT_START!
    (options {greedy=false;} : (NESTED_ML_COMMENT^ | . ) )* 
    COMMENT_END!
;

这里有一个专门关于这些操作符的问题,现在你知道它们的存在了,希望对你有帮助:在 ANTLR 语法中,^ 和 ! 代表什么

2

不,这没有简单的方法。因为 NESTED_ML_COMMENT 是一种词法分析规则(简单的标记),你不能让解析规则在源代码中创建更多的结构,比如 /*test2/*nested*/comment*/:词法规则总是保持为“平面”的字符序列。当然,有一些简单的方法可以重写这个字符序列(比如去掉 /**/),但是要创建父子层级结构就不行了。

为了创建你在第二张图片中展示的层级结构,你需要把你的注释规则“提升”到解析器中(也就是把它变成解析规则)。在这种情况下,你的词法分析器会有 COMMENT_START : '/*';COMMENT_END : '*/'; 规则。但这样会引发很多问题:在你的词法分析器中,你还需要考虑所有可能出现在 /**/ 之间的字符。

你可以创建另一个解析器来解析(嵌套的)注释,并在你的 CSS 语法中使用它。在你的 CSS 语法中,你可以保持原样,而你的第二个解析器是一个专门处理注释的解析器,它会从注释标记中创建层级结构。

这里有个快速演示。语法:

grammar T;

parse
  :  comment EOF 
  ;

comment
  :  COMMENT_START (ANY | comment)* COMMENT_END
  ;

COMMENT_START : '/*';
COMMENT_END   : '*/';
ANY           :  . ;

会把源代码 /*test2/*nested*/comment*/ 解析成以下的解析树:

enter image description here

当然,你可以把 /**/ 去掉。

在你的 CSS 语法中,你接着这样做:

comment
  :  NESTED_ML_COMMENT 
     {
       text = $NESTED_ML_COMMENT.text
       # invoke the TParser (my demo grammar) on `text`
     }
  ;

编辑

注意,ANTLRWorks 创建了它自己的内部解析树,你无法访问。如果你不告诉 ANTLR 生成一个合适的抽象语法树(AST),你最终只会得到一个平面的标记列表(尽管 ANTLRWorks 表示这是一种树)。

这里有个之前的问答,解释了如何创建一个合适的 AST: 如何输出使用 ANTLR 构建的 AST?

现在让我们回到我上面提到的“注释”语法。我会把 ANY 规则重命名为 TEXT。目前,这个规则一次只匹配一个字符。但让它匹配到下一个 /**/ 更方便。可以通过在词法分析器类中引入一个简单的 Python 方法来实现这个检查。在 TEXT 规则中,我们会在一个谓词中使用这个方法,这样 * 会在不直接跟 / 的情况下被匹配,而 / 会在不直接跟 * 的情况下被匹配:

grammar Comment;

options {
  output=AST;
  language=Python;
}

tokens {
  COMMENT;
}

@lexer::members {
  def not_part_of_comment(self):
    current = self.input.LA(1)
    next = self.input.LA(2)
    if current == ord('*'): return next != ord('/')
    if current == ord('/'): return next != ord('*')  
    return True
}

parse
  :  comment EOF -> comment
  ;

comment
  :  COMMENT_START atom* COMMENT_END -> ^(COMMENT atom*)
  ;

atom
  :  TEXT
  |  comment
  ;

COMMENT_START : '/*';
COMMENT_END   : '*/';
TEXT          : ({self.not_part_of_comment()}?=> . )+ ;

想了解更多关于谓词语法的信息,{ boolean_expression }?=>,可以查看这个问答: 什么是 ANTLR 中的“语义谓词”?

要测试这一切,确保你安装了合适的 Python 运行时库(见 ANTLR Wiki)。并确保使用 ANTLR 版本 3.1.3 和这个运行时。

生成词法分析器和解析器的方法如下:

java -cp antlr-3.1.3.jar org.antlr.Tool Comment.g 

并用以下 Python 脚本测试词法分析器和解析器:

#!/usr/bin/env python

import antlr3
from antlr3 import *
from antlr3.tree import *
from CommentLexer import *
from CommentParser import *

# http://www.antlr.org/wiki/display/ANTLR3/Python+runtime
# http://www.antlr.org/download/antlr-3.1.3.jar

def print_level_order(tree, indent):
  print '{0}{1}'.format('   '*indent, tree.text)
  for child in tree.getChildren():
    print_level_order(child, indent+1)

input = '/*aaa1/*bbb/*ccc*/*/aaa2*/'
char_stream = antlr3.ANTLRStringStream(input)
lexer = CommentLexer(char_stream)
tokens = antlr3.CommonTokenStream(lexer)
parser = CommentParser(tokens)
tree = parser.parse().tree 
print_level_order(tree, 0)

如你所见,从源代码 "/*aaa1/*bbb/*ccc*/*/aaa2*/" 创建了以下的 AST:

COMMENT
   aaa1
   COMMENT
      bbb
      COMMENT
         ccc
   aaa2

编辑 II

我还可以展示如何从你的 CSS 语法中调用注释解析器。这里有个快速演示:

grammar CSS;

options {
  output=AST;
  language=Python;
}

tokens {
  CSS_FILE;
  RULE;
  BLOCK;
  DECLARATION;
}

@parser::header {
import antlr3
from antlr3 import *
from antlr3.tree import *
from CommentLexer import *
from CommentParser import *
}

@parser::members {
  def parse_comment(self, text):
    lexer = CommentLexer(antlr3.ANTLRStringStream(text))
    parser = CommentParser(antlr3.CommonTokenStream(lexer))
    return parser.parse().tree 
}

parse
  :  atom+ EOF -> ^(CSS_FILE atom+)
  ;

atom
  :  rule
  |  Comment -> {self.parse_comment($Comment.text)}
  ;

rule
  :  Identifier declarationBlock -> ^(RULE Identifier declarationBlock)
  ;

declarationBlock
  :  '{' declaration+ '}' -> ^(BLOCK declaration+)
  ;

declaration
  :  a=Identifier ':' b=Identifier ';' -> ^(DECLARATION $a $b)
  ;

Identifier
  :  ('a'..'z' | 'A'..'Z') ('a'..'z' | 'A'..'Z' | '0'..'9')*
  ;

Comment
  :  '/*' (options {greedy=false;} : Comment | . )* '*/'
  ;

Space
  :  (' ' | '\t' | '\r' | '\n') {$channel=HIDDEN;}
  ;

如果你用 CSSParser 解析源代码:

h1 {  a: b;  c: d;}

/*aaa1/*bbb/*ccc*/*/aaa2*/

p {x  :  y;}

你将得到以下树:

CSS_FILE
   RULE
      h1
      BLOCK
         DECLARATION
            a
            b
         DECLARATION
            c
            d
   COMMENT
      aaa1
      COMMENT
         bbb
         COMMENT
            ccc
      aaa2
   RULE
      p
      BLOCK
         DECLARATION
            x
            y

你可以通过运行这个测试脚本来查看:

#!/usr/bin/env python

import antlr3
from antlr3 import *
from antlr3.tree import *
from CSSLexer import *
from CSSParser import *

def print_level_order(tree, indent):
  print '{0}{1}'.format('   '*indent, tree.text)
  for child in tree.getChildren():
    print_level_order(child, indent+1)

input = 'h1 {  a: b;  c: d;}\n\n/*aaa1/*bbb/*ccc*/*/aaa2*/\n\np {x  :  y;}'
char_stream = antlr3.ANTLRStringStream(input)
lexer = CSSLexer(char_stream)
tokens = antlr3.CommonTokenStream(lexer)
parser = CSSParser(tokens)
tree = parser.parse().tree 
print_level_order(tree, 0)

撰写回答