mondrian 源码解读(四)-从parser开始

前面几节我们介绍了一下mondrian最基础的东西,只有在了解了这些后,我们才能继续往下看。

首先,mondrian需要将mdx语句解析成java对象,我们先看看他是如何解析的。

Query创建过程

我们从ConnectionBase的parser开始,在parseStatement中,有一句很重要的话语句。

public Query parseQuery(String query) {
    return (Query) parseStatement(query);
}

Query parseStatement(...){
...
return parser.parseInternal(
                statement, query, debug, funTable, strictValidation);
...
}

一,创建解析器

首先,我们需要一个parser。在ConnectionBase类中,我们看到了parser的创建过程:

protected MdxParserValidator createParser() {
    return true
        ? new JavaccParserValidatorImpl()
        : new MdxParserValidatorImpl();
}

实际上,由于这里永远是true,所以返回的是JavaccParserValidatorImpl。这个类也是实现了MdxParserValidator。它与原生的javaccparser生成的MdxParserValidatorImpl的主要不同在于factory的创建方式和异常的处理方式,这里就不做过多解读了,具体请看源代码。

mondrian 源码解读(三)-mondrian架构

mondrian系统的层次

mondrian总共包含四个层次:表示层,计算层,聚集层,存储层.。

表示层(presentation layer)

指最终呈现在用户显示器上的,以及与用户之间的交互,有许多方法来展现多维数据,包括数据透视表,饼,柱,线状图.

计算层(dimensional layer)

分析,验证,执行MDX查询. 一个mdx查询语句会有多个处理阶段。先是计算轴,然后是轴上的单元值。为效率起见,计算层批量将单元请求发送到聚集层。请求转换器允许程序操作存在的请求,而不是为每个请求从头构造mdx请求。元数据描述了计算模型和它怎么匹配到关系模型。

聚集层(star layer)

一个聚集指内存中一组计算值(cell),这些值通过维列来限制.计算层发送单元请求,如果请求不在缓存中,或者不能通过旋转聚集导出的话,聚集层向存储层发送请求. 聚集层是一个数据缓冲层(cache)
从数据库来的单元数据,聚合后提供给计算层。聚集层的主要作用是提高系统的性能。

存储层(storage layer)

提供聚集单元数据(cell)和维表的成员(member),这些层可以不在同一机子上,但是计算和聚集层必须在同一台机子上。有三种需要存储的数据:事实数据(事实表)、维度表和聚集数据(即聚合表)

架构图

更多内容请移步:http://mondrian.pentaho.com/documentation/architecture.php

mondrian 源码解读(二)-MDX

什么是MDX?

MDX为MultiDimensional Expressions的缩写,多维表达式,是标准的OLAP查询语言。在多数OLAPServer都提供MDX支持,如Microsoft SQL Server OLAP Services,SAS,Hyperion Essbase等。

支持多维对象与数据的定义和操作。MDX 在很多方面与结构化查询语言 (SQL) 语法相似,但它不是 SQL 语言的扩展;事实上,MDX 所提供的一些功能也可由 SQL 提供,尽管不是那么有效或直观。
如同 SQL 查询一样,每个 MDX 查询都要求有数据请求(SELECT 子句)、起始点(FROM 子句)和筛选(WHERE 子句)。这些关键字以及其它关键字提供了各种工具,用来从多维数据集析取数据的特定部分。

MDX 还提供了可靠的函数集,用来对所检索的数据进行操作,同时还具有用户定义函数扩展 MDX 的能力。MDX为多维数据库提供了表达式查询语法,用于查询Cube数据,并提供了许多强大的分析函数,用于支持常用的OLAP分析。

mondrian 源码解读(一)-部署mondrian项目

mondrian是什么?

mondrian是一款基于java的开源的多维分析olap服务器。更多资料请移步mondrian官方网站:http://community.pentaho.com/projects/mondrian/。

如何部署mondrian?

在mondrian官方文档中,已经有详细的步骤了,请移步:http://mondrian.pentaho.com/documentation/developers_guide.php

可能部署的时候会有点问题,我将我遇到的问题分享给大家:

antlr4-技巧(十)

在这一小节中我们主要介绍一些antlr4中的小技巧,这些技巧将极大的扩展antlr4的灵活性。

在不同的通道广播tokens

绝大多数编程语言忽略tokens之间的空格和注释,这意味着他们可以出现在任何地方。对解析器来说,最简单的方式就是跳过他们,什么也不做。
但是如果我们想留下注释可空格的话,应该怎么做呢?

antlr为我们提供了通道,它就像收音机的频道,我们可以指定某些字符发送到不同的频道中。解析器指向正确的通道并且忽略其他的通道。词法规则负责把tokens放到不同的通道中,类CommonTokenStream的职责就是对parser呈现仅仅一个通道。CommonTokenStream保留所有的token序列以至于我们能请求注释在一个特定的token之前或之后。下面的图片呈现了CommonTokenStream的运行过程:

不同的注释可可以发送到不同的通道

访问隐藏的通道

为了解释如何访问隐藏的通道,我们举个简答的例子。在这里例子中,我们将构建一个解析器将变量后面的注释移到变量前面。
我们最常用的策略是使用TokenStreamRewriter类来重写输入流。下面是一个简单的例子,在类中插入一个序列化id:

ParseTreeWalker walker = new ParseTreeWalker(); // create standard walker
InsertSerialIDListener extractor = new InsertSerialIDListener(tokens);
walker.walk(extractor, tree); // initiate walk of tree with listener

public class InsertSerialIDListener extends JavaBaseListener {
    TokenStreamRewriter rewriter;
    public InsertSerialIDListener(TokenStream tokens) {
    rewriter = new TokenStreamRewriter(tokens);
}
@Override
public void enterClassBody(JavaParser.ClassBodyContext ctx) {
        String field = "\n\tpublic static final long serialVersionUID = 1L;";
        rewriter.insertAfter(ctx.start, field);
    }
}

回到最开始,我们看看如何使用隐藏的通道。

public static class CommentShifter extends CymbolBaseListener {
    BufferedTokenStream tokens;
    TokenStreamRewriter rewriter;
    /** Create TokenStreamRewriter attached to token stream
     *  sitting between the Cymbol lexer and parser.
     */
    public CommentShifter(BufferedTokenStream tokens) {
        this.tokens = tokens;
        rewriter = new TokenStreamRewriter(tokens);
    }

    @Override
    public void exitVarDecl(CymbolParser.VarDeclContext ctx) {
        Token semi = ctx.getStop(); 
        int i = semi.getTokenIndex();
        List<Token> cmtChannel =
            tokens.getHiddenTokensToRight(i, CymbolLexer.COMMENTS); 
        if ( cmtChannel!=null ) {
            Token cmt = cmtChannel.get(0); 
            if ( cmt!=null ) {
                String txt = cmt.getText().substring(2);
                String newCmt = "/* " + txt.trim() + " */\n";
                rewriter.insertBefore(ctx.start, newCmt); 
                rewriter.replace(cmt, "\n");              
            }
        }
    }
}

ANTLRInputStream input = new ANTLRInputStream(is);
CymbolLexer lexer = new CymbolLexer(input);
CommonTokenStream tokens = new CommonTokenStream(lexer);
CymbolParser parser = new CymbolParser(tokens);
RuleContext tree = parser.file();

ParseTreeWalker walker = new ParseTreeWalker();
CommentShifter shifter = new CommentShifter(tokens);
walker.walk(shifter, tree);


lexmagic/t.cym
int n = 0; // define a counter
int i = 9;
we want to generate the following output:
/* define a counter */
int n = 0;
int i = 9;

所有的工作都发送在exitVarDecl()中。首先,我们得到‘;’所在的位置。然后我们查询它的后面是否有隐藏的通道,如果有的话,我们将获取通道的第一个字符来判断通道中是否有tokens,最后插入到最前面即可。

上下文敏感的词法

在流中孤立

这里需要注意一点,.*?表示非贪婪操作符。他们一直扫描直到遇到符合下一个规则的操作符为止。

COMMENT : '<!--' .*? '-->' -> skip ;
CDATA : '<![CDATA[' .*? ']]>' ;
TAG : '<' .*? '>' ; // must come after other tag-like structures
ENTITY : '&' .*? ';' ;
TEXT : ~[<&]+ ; // any sequence of chars except < and & chars

在某种场景下,一个语言可能有几种不同的模式。词法模式可以允许我们分离单个的词法语言到多个子规则。当进入到当前规则的模式时,词法仅仅返回符合的token。最常用的需求可能是在不同的词法之间进行转换。 例如,对于一个xml文件,我们可以定义2个模式,在标签‘<’之外的模式和在‘>’之内的模式。这2中模式是孤立的,下面是这个例子的语法:

lexer grammar ModeTagsLexer;
// Default mode rules (the SEA)
OPEN : '<' -> mode(ISLAND) ; // switch to ISLAND mode
TEXT : ~'<' + ; // clump all text together
mode ISLAND;
CLOSE : '>' -> mode(DEFAULT_MODE) ; // back to SEA mode
SLASH : '/' ;
ID : [a-zA-Z]+ ; // match/send ID in tag to parser

parser grammar ModeTagsParser;
options { tokenVocab=ModeTagsLexer; } // use tokens from ModeTagsLexer.g4
file: (tag | TEXT)* ;
tag : '<' ID '>'
| '<' '/' ID '>'
;

在这个例子中,仅仅是options tokenVocab不常用的。它的意思是保持lexer和parser是同步的,即有相同的token类型。

解析xml

在这个例子中,我们只需要了解有pushMode和popMode即可,就像入栈和出栈一样,挺简单的。

lexer grammar XMLLexer;

// Default "mode": Everything OUTSIDE of a tag
COMMENT     :   '<!--' .*? '-->' ;
CDATA       :   '<![CDATA[' .*? ']]>' ;
/** Scarf all DTD stuff, Entity Declarations like <!ENTITY ...>,
 *  and Notation Declarations <!NOTATION ...>
 */
DTD         :   '<!' .*? '>'            -> skip ; 
EntityRef   :   '&' Name ';' ;
CharRef     :   '&#' DIGIT+ ';'
            |   '&#x' HEXDIGIT+ ';'
            ;
SEA_WS      :   (' '|'\t'|'\r'? '\n') ;

OPEN        :   '<'                     -> pushMode(INSIDE) ;
XMLDeclOpen :   '<?xml' S               -> pushMode(INSIDE) ;
SPECIAL_OPEN:   '<?' Name               -> more, pushMode(PROC_INSTR) ;

TEXT        :   ~[<&]+ ;        // match any 16 bit char other than < and &

// ----------------- Everything INSIDE of a tag ---------------------
mode INSIDE;

CLOSE       :   '>'                     -> popMode ;
SPECIAL_CLOSE:  '?>'                    -> popMode ; // close <?xml...?>
SLASH_CLOSE :   '/>'                    -> popMode ;
SLASH       :   '/' ;
EQUALS      :   '=' ;
STRING      :   '"' ~[<"]* '"'
            |   '\'' ~[<']* '\''
            ;
Name        :   NameStartChar NameChar* ;
S           :   [ \t\r\n]               -> skip ;

fragment
HEXDIGIT    :   [a-fA-F0-9] ;

fragment
DIGIT       :   [0-9] ;

fragment
NameChar    :   NameStartChar
            |   '-' | '.' | DIGIT 
            |   '\u00B7'
            |   '\u0300'..'\u036F'
            |   '\u203F'..'\u2040'
            ;

fragment
NameStartChar
            :   [:a-zA-Z]
            |   '\u2070'..'\u218F' 
            |   '\u2C00'..'\u2FEF' 
            |   '\u3001'..'\uD7FF' 
            |   '\uF900'..'\uFDCF' 
            |   '\uFDF0'..'\uFFFD'
            ;

// ----------------- Handle <? ... ?> ---------------------
mode PROC_INSTR;
PI          :   '?>'                    -> popMode ; // close <?...?>
IGNORE      :   .                       -> more ;

antlr4-语义预测(九)

在这一节,我们将介绍anltr的特别的行为 {…}?,就是所谓的语义预测。可以让我们在运行的时候使部分语法无效。预测是boolean型的表达式,可以有效的减少解析树的选择。

有2个比较常用的语义预测用例。第一个,我们可能需要一个解析器去处理多个,有点不同版本的相同语言。例如,各个数据库厂商的定制sql语言。
第二个,可能涉及解析语法模糊不清的部分。在有些语言中,相同的词法构造可能意味着不同的含义,预测可以让我们在多个含义中选择一条。

例子太简单了,这里就不介绍了。
有一点需要注意的是,看下面这个例子,ENUM必须在ID的前面。这是因为‘enum’被包含在ID中,antlr对这种模糊不清的词法总是优先用第一个解析,在下面的例子中是ENUM。如果将他们颠倒的话,那么‘enum’将被永远的解析为ID了。
predicates/Enum2.g4
ENUM: ‘enum’ {java5}? ; // must be before ID
ID : [a-zA-Z]+ ;

antlr4-使用小结(续)

本文主要记录在使用antlr4中中遇到的一些问题。

一,标明了fragment的词法是不会被parser识别的,它只会在当前词法中可用。

二,当未标明fragment的词法似乎是有包含关系的。当前面的词法包含了后面的词法时,后面的词法将不被parser解析出来。(可以看第9小节关系词法包含关系)。

三,antlr的ieda插件只支持4.4以上的版本,所以如果需要生成4.4一下的版本的话,可以使用maven插件,或者手工生成。

四,当一个词法中出现了2个同名的tokens名称时,另一个需要命名一个别名,不然会引起冲突。

//tokens1
term4  returns [Exp exp]
: term3 {
    $exp = $term3.exp;
}
|   NOT  term4 {
    $exp =  new UnresolvedFunCall("NOT", Syntax.Prefix, new Exp[] {$term4.exp}); //$term4.exp取的是第一行的term4的内容
}
;


//tokens2
term4  returns [Exp exp]
: term3 {
    $exp = $term3.exp;
}
|   NOT  term4alias = term4 {
    $exp =  new UnresolvedFunCall("NOT", Syntax.Prefix, new Exp[] {$term4alias.exp});//命名别名后,取出的才是真正需要的的内容,即当前term4下的表达式
}
;

antlr4-异常处理(八)

antlr4的异常处理机制很简单,下面我们来具体看看

修改和重定向错误

默认情况下, antlr发送所有的错误到标准错误中,但是我们可以提供ANTERErrorListener接口的实现来改变错误的内容和目的地。
这个接口有一个syntaxError方法适用于词法和解析器。方法接收所有已排序的本地错误和消息错误。它也可以接收一个parser的引用,所以我们能够查询状态的识别。

public static class UnderlineListener extends BaseErrorListener {
public void syntaxError(Recognizer<?, ?> recognizer,
            Object offendingSymbol,
            int line, int charPositionInLine,
            String msg,
            RecognitionException e)
{
    System.err.println("line "+line+":"+charPositionInLine+" "+msg);
    underlineError(recognizer,(Token)offendingSymbol,
                   line, charPositionInLine);
}

protected void underlineError(Recognizer recognizer,
                              Token offendingToken, int line,
                              int charPositionInLine) {
    CommonTokenStream tokens =
        (CommonTokenStream)recognizer.getInputStream();
    String input = tokens.getTokenSource().getInputStream().toString();
    String[] lines = input.split("\n");
    String errorLine = lines[line - 1];
    System.err.println(errorLine);
    for (int i=0; i<charPositionInLine; i++) System.err.print(" ");
    int start = offendingToken.getStartIndex();
    int stop = offendingToken.getStopIndex();
    if ( start>=0 && stop>=0 ) {
        for (int i=start; i<=stop; i++) System.err.print("^");
        }
    System.err.println();
    }
}

public static void main(String[] args) throws Exception {
    ANTLRInputStream input = new ANTLRInputStream(System.in);
    SimpleLexer lexer = new SimpleLexer(input);
    CommonTokenStream tokens = new CommonTokenStream(lexer);
    SimpleParser parser = new SimpleParser(tokens);
    parser.removeErrorListeners(); // remove ConsoleErrorListener
    parser.addErrorListener(new UnderlineListener());
    parser.prog();
}

自动错误恢复

错误恢复就是运行解析器在发现了一个词法错误后可以继续执行。原则上,最佳的错误恢复应该是我们手写的递归下沿的解析器。解析器执行单个token插入和单个token删除直到不符合的token错误被修正。如果没有,解析器吞掉tokens知道他发现一个token能够可理解的跟着当前的规则然后返回,继续直到所有的解析完。

捕获失败的语义预测

so easy! 当预测失败时,你可以使用这样的函数{…}?来执行某些动作。

vec4:   '[' ints[4] ']' ;

ints[int max]
locals [int i=1]
:   INT ( ',' {$i++;} {$i<=$max}?<fail={"exceeded max "+$max}> INT )*
;

INT :   [0-9]+ ;
WS  :   [ \t\r\n]+ -> skip ;

错误的替代选择

同样不解释了.

fcall
:   ID '(' expr ')'
|   ID '(' expr ')' ')' {notifyErrorListeners("Too many parentheses");}
|   ID '(' expr         {notifyErrorListeners("Missing closing ')'");}
;

另一个可选的错误策略

有时候我们不想用默认的错误处理策略。第一,在运行期的时候,我们可能想要禁用一些内部的错误处理。第二,我们可能想要提示最开始的语法错误。
要查看错误处理策略,我们可以看一下ANTLRErrorStrategy接口的默认具体实现DefaultErrorStrategy。这个类处理了antlr4所有的错误处理行为。
从antlr生成的代码中我们也可以看到:

_errHandler.reportError(this, re);
_errHandler.recover(this, re);

另一种错误处理就是BailErrorStrategy处理策略,即遇到异常后自动抛出。我们只需要告诉parser使用这种策略即可。当然,我们也可以用我们自己的错误处理方式,只需要继承DefaultErrorStrategy即可。

parser.setErrorHandler( new BailErrorStrategy());

public class BailErrorStrategy extends DefaultErrorStrategy {
/** Instead of recovering from exception {@code e}, re-throw it wrapped
 *  in a {@link ParseCancellationException} so it is not caught by the
 *  rule function catches.  Use {@link Exception#getCause()} to get the
 *  original {@link RecognitionException}.
 */
@Override
public void recover(Parser recognizer, RecognitionException e) {
    for (ParserRuleContext context = recognizer.getContext(); context != null; context = context.getParent()) {
        context.exception = e;
    }

    throw new ParseCancellationException(e);
}

/** Make sure we don't attempt to recover inline; if the parser
 *  successfully recovers, it won't throw an exception.
 */
@Override
public Token recoverInline(Parser recognizer)
    throws RecognitionException
{
    InputMismatchException e = new InputMismatchException(recognizer);
    for (ParserRuleContext context = recognizer.getContext(); context != null; context = context.getParent()) {
        context.exception = e;
    }

    throw new ParseCancellationException(e);
}

/** Make sure we don't attempt to recover from problems in subrules. */
@Override
public void sync(Parser recognizer) { }

antlr4-添加嵌入式的行为(七)

一般情况下我们最好不要这么做,这会使应用代码与语法代码混淆。但是在某些情况下也有她的好处:

  • 简易的:有时候仅仅是简答的几个动作,避免创建监听器或访问器。
  • 有效的:在特定的资源应用中,我们不想浪费时间和内存去构建一个解析树。
  • 判断解析:在很少的情况下,我么需要依靠之前的输入流来判断解析属性。一些语法需要构建符号表来意识到将来的不同情况的输入,依赖与标识符是否是类型或者方法。

演示

grammar Rows;

@parser::members { // add members to generated RowsParser
    int col;
    public RowsParser(TokenStream input, int col) { // custom constructor
        this(input);
        this.col = col;
    }
}

file: (row NL)+ ;

row
locals [int i=0]   //locals 本地变量表
    : (   STUFF
          { //行为 ,通过$来取出本地变量 ,col是全局变量
          $i++;
          if ( $i == col ) System.out.println($STUFF.text);
          }
      )+
    ;

TAB  :  '\t' -> skip ;   // match but don't pass to the parser
NL   :  '\r'? '\n' ;     // match and pass to the parser
STUFF:  ~[\t\r\n]+ ;     // match any chars except tab, newline

利用语法规则的外部行为

<header>
public class <grammarName> Parser extends Parser {
<members>
...
}

package/import statements is header and
class members like fields and methods is members
如果要指定header行为,我们用@header,如果要注入字段和方法,我们用@memers。
在一个合并的parser/lexer语法中,他们命名的行为应用在parser和lexer中。如果要限制生成的行为在parser或lexer中,我们可以用 @parser::name 或者 @lexer::name.

通常情况下,$x.y表示y属性的x元素,x可以是token的引用或者rule引用。

expr例子

/** Grammar from tour chapter augmented with actions */
grammar Expr;

@header {
/**package tools;*/
import java.util.*;
}

@parser::members {
    /** "memory" for our calculator; variable/value pairs go here */
    Map<String, Integer> memory = new HashMap<String, Integer>();

    int eval(int left, int op, int right) {
        switch ( op ) {
            case MUL : return left * right;
            case DIV : return left / right;
            case ADD : return left + right;
            case SUB : return left - right;
        }
        return 0;
    }
}

stat:   e NEWLINE           {System.out.println($e.v);}
    |   ID '=' e NEWLINE    {memory.put($ID.text, $e.v);}
    |   NEWLINE                   
    ;

e returns [int v]
    : a=e op=('*'|'/') b=e  {$v = eval($a.v, $op.type, $b.v);}
    | a=e op=('+'|'-') b=e  {$v = eval($a.v, $op.type, $b.v);}  
    | INT                   {$v = $INT.int;}    
    | ID
      {
      String id = $ID.text;
      $v = memory.containsKey(id) ? memory.get(id) : 0;
      }
    | '(' e ')'             {$v = $e.v;}       
    ; 

MUL : '*' ;
DIV : '/' ;
ADD : '+' ;
SUB : '-' ;

ID  :   [a-zA-Z]+ ;      // match identifiers
INT :   [0-9]+ ;         // match integers
NEWLINE:'\r'? '\n' ;     // return newlines to parser (is end-statement signal)
WS  :   [ \t]+ -> skip ; // toss out whitespace                

keyword例子

grammar Keywords;
@lexer::header {    // place this header action only in lexer, not the parser
    import java.util.*;
}

// explicitly define keyword token types to avoid implicit def warnings
tokens { BEGIN, END, IF, THEN, WHILE }

@lexer::members {   // place this class member only in lexer
    Map<String,Integer> keywords = new HashMap<String,Integer>() <!--0-->;
}


stat:   BEGIN stat* END 
    |   IF expr THEN stat
    |   WHILE expr stat
    |   ID '=' expr ';'
    ;

expr:   INT | CHAR ;

ID  :   [a-zA-Z]+
        {//嵌入式代码
        if ( keywords.containsKey(getText()) ) {
            setType(keywords.get(getText())); // reset token type
        }
        }
    ;

/** Convert 3-char 'x' input sequence to string x */
CHAR:   '\'' . '\'' {setText( String.valueOf(getText().charAt(1)) );} ;

INT :   [0-9]+ ;

WS  :   [ \t\n\r]+ -> skip ;

csvexample

grammar CSV;

@header {
import java.util.*;
}

/** Derived from rule "file : hdr row+ ;" */
file
locals [int i=0]
     : hdr ( rows+=row[$hdr.text.split(",")] {$i++;} )+
       {
       System.out.println($i+" rows");
       for (RowContext r : $rows) {
           System.out.println("row token interval: "+r.getSourceInterval());
       }
       }
     ;

hdr : row[null] {System.out.println("header: '"+$text.trim()+"'");} ;

/** Derived from rule "row : field (',' field)* '\r'? '\n' ;" */
//参数 columns ,返回值values
row[String[] columns] returns [Map<String,String> values]
locals [int col=0]
@init {
    $values = new HashMap<String,String>();
}
@after {
    if ($values!=null && $values.size()>0) {
        System.out.println("values = "+$values);
    }
}
// rule row cont'd...
    :   field
        {
        if ($columns!=null) {
            $values.put($columns[$col++].trim(), $field.text.trim());
        }
        }
        (   ',' field
            {
            if ($columns!=null) {
                $values.put($columns[$col++].trim(), $field.text.trim());
            }
            }
        )* '\r'? '\n'
    ;

field
    :   TEXT
    |   STRING
    |
    ;

TEXT : ~[,\n\r"]+ ;
STRING : '"' ('""'|~'"')* '"' ; // quote-quote is an escaped quote

antlr4-共享上下文(六)

当我们收集信息或者计算值的时候,最好的方式是传递参数和返回值,而不是使用全局变量或者类变量。
由于antlr自动对监听器生成了不带参数和返回值的方法。antlr对访问器也生成了没有应用指定参数的方法。

这一小节中,我们将探索基于事件的处理方式。我们将构建3个不同的实现计算器的例子来说明问题。

Visitor方式

通常的访问器并没有指定返回值,在这个例子中,我们只需要返回Integer对象即可实现功能。

public static class EvalVisitor extends LExprBaseVisitor<Integer> {
    public Integer visitMult(LExprParser.MultContext ctx) {
        return visit(ctx.e(0)) * visit(ctx.e(1));
    }

    public Integer visitAdd(LExprParser.AddContext ctx) {
        return visit(ctx.e(0)) + visit(ctx.e(1));
    }

    public Integer visitInt(LExprParser.IntContext ctx) {
        return Integer.valueOf(ctx.INT().getText());
    }
}

注意在这个例子中我们并没有对规则s进行定义,默认的实现在LExprBaseVisitor如下:

@Override public T visitS(@NotNull LExprParser.SContext ctx) { return visitChildren(ctx); }

visitChildren()方法返回最后一个子节点返回的值。

Listener方式

在listener方式中,我们使用Stack来保存返回的值。

public static class Evaluator extends LExprBaseListener {
    Stack<Integer> stack = new Stack<Integer>();

    public void exitMult(LExprParser.MultContext ctx) {
        int right = stack.pop();
        int left = stack.pop();
        stack.push( left * right );
    }

    public void exitAdd(LExprParser.AddContext ctx) {
        int right = stack.pop();
        int left = stack.pop();
        stack.push(left + right);
    }

    public void exitInt(LExprParser.IntContext ctx) {
        stack.push( Integer.valueOf(ctx.INT().getText()) );
    }
}

annotating方式

为了代替在事件的方式中需要临时存储空间,我们可以存储这些值在解析树上。我们可以用树注解的方式在visitor或者listener中。下面将使用listener解释如何使用annotating。

每个子表达式都有一个相应的子根(对一个e规则的调用)。从e节点向右的箭头指向的是一个局部的结果,我们可以认为是返回值。

所以,我们可以按照下面这种方式来赋值。但是不幸的是,我们不能在java中通过继承ExprContext来动态的添加字段value的值(Ruby和Python可以)。

public void exitAdd(LExprParser.AddContext ctx) {
// e(0).value is the subexpression value of the first e in the alternative
    ctx.value = ctx.e(0).value + ctx.e(1).value; // e '+' e # Add
}

所以,我们用一个map来保存所有的值。

public static class EvaluatorWithProps extends LExprBaseListener {
    /** maps nodes to integers with Map<ParseTree,Integer> */
    ParseTreeProperty<Integer> values = new ParseTreeProperty<Integer>();

    /** Need to pass e's value out of rule s : e ; */
    public void exitS(LExprParser.SContext ctx) {
        setValue(ctx, getValue(ctx.e())); // like: int s() { return e(); }
    }

    public void exitMult(LExprParser.MultContext ctx) {
        int left = getValue(ctx.e(0));  // e '*' e   # Mult
        int right = getValue(ctx.e(1));
        setValue(ctx, left * right);
    }

    public void exitAdd(LExprParser.AddContext ctx) {
        int left = getValue(ctx.e(0)); // e '+' e   # Add
        int right = getValue(ctx.e(1));
        setValue(ctx, left + right);
    }

    public void exitInt(LExprParser.IntContext ctx) {
        String intText = ctx.INT().getText(); // INT   # Int
        setValue(ctx, Integer.valueOf(intText));
    }

    public void setValue(ParseTree node, int value) { values.put(node, value); }
    public int getValue(ParseTree node) { return values.get(node); }
}

//保存树的节点和值的map
public class ParseTreeProperty <V> {
    protected java.util.Map<org.antlr.v4.runtime.tree.ParseTree,V> annotations;

    public ParseTreeProperty() { /* compiled code */ }

    public V get(org.antlr.v4.runtime.tree.ParseTree node) { /* compiled code */ }

    public void put(org.antlr.v4.runtime.tree.ParseTree node, V value) { /* compiled code */ }

    public V removeFrom(org.antlr.v4.runtime.tree.ParseTree node) { /* compiled code */ }
}

对比

  • 本地java调用栈:访问者方法返回了用户定义的类型的值。如果访问者需要传递参数,也需要用到下面2中技术。
  • 基于栈: 栈字段模拟了java的参数和返回值。
  • 注解:map字段映射了节点和值的关系。