在基本操作中分解表达式:ANTLR + StringTemplate

时间:2012-12-11 09:16:35

标签: translation antlr grammar stringtemplate

我正在尝试为多种语言编写类似Java语言的翻译器。

现在我面临两个问题:

首先是在基本操作序列中分解复杂表达式,然后将它们翻译成目标语言。

例如我的起始语言可以是:

var a = (ln(b) + avg(c))*2

我想把它翻译成:

var x1 = log_N(b);
var x2 = average(c);
var x3 = sum(x1, x2);
var a = multiply(x3, 2);

我想我必须使用Tree解析器,我不知道如何将它与StringTemplate集成。 此外,我正在添加额外的变量,如x1,x2和x3,我不知道如何处理这种情况。

第二个问题是我的目标语言之一是像语言一样的plsql。 在这种情况下,需要确保所有输出变量都成为游标并将它们传递给函数。

例如表达式:

var a = (ln(b) + avg(c))*2

应该用这种方式翻译:

log_N(x1, b);
average(x2, c);
sum(x3, x1, x2);
multiply(a, x3, 2);

其中x1,x2,x3和a将成为输出游标。

任何人都可以帮我找到合适的解决方案吗?

由于

1 个答案:

答案 0 :(得分:4)

  

我想我必须使用Tree解析器,但我不确定如何将它与StringTemplate集成。

将StringTemplate集成到树解析器中与将其集成到令牌解析器中基本相同:将output选项定义为template,然后相应地编写规则生成。

下面是一个使用模板输出的小树语法。请注意,此语法与the one I described in a previous answer之间唯一有意义的区别在于此语法在树节点上运行而不是在令牌上运行。模板的工作原理相同。

tree grammar AstToTemplateParser;

options { 
    output = template;
    tokenVocab = JavaLikeToAst;
    ASTLabelType = CommonTree;
}


program
    : ^(PROGRAM decls+=decl+) -> write(text={$decls})
    ;

decl
    : ^(DECL ID ^(op args+=arg*)) -> assign(name={$ID.text}, op={$op.st}, args={$args})
    | ^(DECL ID ^(CALL method args+=arg*)) -> assign(name={$ID.text}, op={$method.st}, args={$args})
    ;

arg 
    : ID -> write(text={$ID.text})
    | INT -> write(text={$INT.text})
    ;

method
    : ID -> op(name={$ID.text})
    ;

op  : STAR  -> op(name={$STAR.text})
    | DIV   -> op(name={$DIV.text})
    | PLUS  -> op(name={$PLUS.text})
    | MINUS -> op(name={$MINUS.text})
    ;
  

此外,我正在添加额外的变量,如x1,x2和x3,我不知道如何处理这种情况。

那是踢球者。我所知道的最简单的方法(这并非易事)是首先使用令牌解析器生成基线AST,然后使用树解析器将AST展平为声明列表,每个声明表示来自原始输入的表达式 - 问题中的x1x2x3

中间阶段看起来像这样:

原始输入

var a = (ln(b) + avg(c))*2

基线AST

baseline AST

展平AST

flattened AST

应用标准模板

 var var0 = log_N(b);
 var var1 = average(c); 
 var var2 = add(var0, var1);
 var a = multiply(var2, 2);

应用PLSQL模板

  log_N(var0, b);
  average(var1, c);
  add(var2, var0, var1);
  multiply(a, var2, 2);

这里是一个令牌解析器语法,它只生成一个类似你问题中的基线AST。这里没有什么值得注意的,它只会产生一个典型的AST。

grammar JavaLikeToAst;

options { 
    output = AST;
}

tokens { 
    PROGRAM; DECL; CALL; 
}

compilationUnit : statement* EOF -> ^(PROGRAM statement*);
statement       : decl;
decl            : VAR ID EQ expr -> ^(DECL ID expr);
expr            : add_expr;
add_expr        : mul_expr ((PLUS|MINUS)^ mul_expr)*;
mul_expr        : call_expr ((STAR|DIV)^ call_expr)*;
call_expr       : ID LPAR arglist? RPAR -> ^(CALL ID arglist?)
                | primary_expr;
arglist         : expr (COMMA! expr)*;
primary_expr    : ID | INT | LPAR! expr RPAR!; 

VAR     : 'var';
ID      : ('a'..'z'|'A'..'Z')('a'..'z'|'A'..'Z'|'_'|'0'..'9')*;
INT     : ('0'..'9')+;
COMMA   : ',';
SEMI    : ';';
LCUR    : '{';
RCUR    : '}';
LPAR    : '(';
RPAR    : ')';
EQ      : '=';
PLUS    : '+';
MINUS   : '-';
STAR    : '*';
DIV     : '/';
WS      : (' '|'\t'|'\f'|'\r'|'\n'){skip();};

这是丑陋的地方(至少我选择这样做的方式;))。下面的树语法将从上面生成的基线AST转换为与AST的表达式相对应的声明列表 - 平坦的AST。在此之下,我将逐步介绍语法的有趣内容。

tree grammar AstToFlatAstParser;

options { 
    output = AST;
    tokenVocab = JavaLikeToAst;
    ASTLabelType = CommonTree;
    filter = true;
}

@header { 
    import java.util.HashMap;
}

@members {
    private int currentId = 0;
    private HashMap<Integer, Object> exprs = new HashMap<Integer, Object>();
    private boolean newDecls = false;

    private int nextId() { 
        return currentId++;
    }

    private Object generateId(int id) { 
        return adaptor.create(ID, "var" + id);
    }  

    private void saveExpr(int id, Object expr){
        newDecls = true;
        exprs.put(id, expr);
    }

    private Object buildNewDecls() {
        Object newDecls = adaptor.nil();

        for (int i = 0; i < currentId; ++i){
            if (!exprs.containsKey(i)){
                continue; //This id was generated but not used.
            }

            Object expr = exprs.get(i);
            Object decl = adaptor.create(DECL, tokenNames[DECL]);
            adaptor.addChild(decl, adaptor.create(ID, "var" + i));
            adaptor.addChild(decl, expr);
            adaptor.addChild(newDecls, decl);
        }

        exprs.clear();

        return newDecls;
    }
}

bottomup
    : exit_program
    | exit_op
    ;

exit_op
    @init {
        int myId = nextId();
    }
    : ^(binary_op reduced reduced)
        {$start.parent != null && $start.parent.getType() != DECL}? 
        {saveExpr(myId, $start);} 
        -> {generateId(myId)}
    | ^(CALL ID .*) 
        {$start.parent != null && $start.parent.getType() != DECL}? 
        {saveExpr(myId, $start);} 
        -> {generateId(myId)}
    ;   

binary_op       : STAR | DIV | PLUS | MINUS;

reduced         : ID | INT; 

exit_program
    //Only rebuild PROGRAM if a new declaration is going to be built, that is, when "newDecls" is true.
    //Otherwise PROGRAM is considered changed when it isn't and the processing never ends.
    : {newDecls}? ^(PROGRAM old+=.*) {newDecls = false;} 
        -> ^(PROGRAM {buildNewDecls()} $old*)
    ;

首先,请注意语法主要是Java代码。只有五个解析器规则,其中大多数都很简单。这是filter树语法,因此规则bottomuptopdown是入口点。在这种情况下,只需要bottomup,因此未指定topdown。重复规则bottomup直到输出树不变,这意味着当没有更多的声明要生成并且树完全展平时。

其次,请注意规则exit_program是将新声明写入AST的位置。我使用语义谓词({newDecls}?)来确保仅在添加新声明时修改PROGRAM。还记得在没有进行任何更改之前我是如何调用bottomup的吗?如果没有此语义谓词,exit_program始终修改PROGRAM,树解析将永远不会停止处理bottomup。对于这个特殊情况,这是一个粗略的解决方案,但它有效。新声明将插入PROGRAM的开头,以确保它们在引用之前出现。在它预期之后定义x1十行是不合适的。

第三,注意规则exit_op用声明(如ln(b))替换表达式(如var0)。如果满足下列条件之一,则替换表达式:

  • 表达式是一个二进制运算,其操作数都是&#34;减少&#34; (也就是说,它们都是整数或变量id)并且不是DECL节点的子节点。 var a = 1 + 2未更改,因为1 + 2是声明的子项。 var b = a + (2 + c)已更改,因为(2 + c)有两个&#34;缩减&#34;操作数并不是DECL节点的子节点(它是+a + ...的子节点。

  • 表达式是CALL,它不是DECL节点的子节点。 var a = ln(b)未受影响,但var a = ln(b) + 3已更改,因为ln(b)+的孩子。

表达式在被{id}替换之前存储在exprs中。当规则调用exit_program时,它会在buildNewDecls规则中重新构建。 buildNewDecls只使用解析器的内置TreeAdaptor成员(名为adaptor)来生成出现在展平AST中的DECL个节点。适配器方法的Javadoc可以很好地解释调用的功能,因此我不会详细介绍。

警告:上述语法生成的解析器适用于您所呈现的非常有限的案例。我不知道在应用于任何更广泛的场景时会产生什么错误。

  

第二个问题是我的目标语言之一是像语言一样的plsql。在这种情况下,需要确保所有输出变量成为游标并将它们传递给函数......

如果您的AST只是一个平坦的声明列表,那么这些模板可以为您管理,如上所示。

您将平顶的AST传递给基于模板的树解析器(如顶部的树形解析器),以生成与您列出的不同的文本版本。在这种情况下,模板将接受声明的所有部分 - 变量名称,操作/方法名称和操作数/参数 - 并生成variable = method(arg0, arg1)method(variable, arg0, arg1)之类的文本,具体取决于使用的模板。关键是确保输入是平的,模板接收与声明相关的所有内容。


这是一个将所有内容联系在一起的测试应用程序。

JavaLikeToAstTest.java

public class JavaLikeToAstTest {

    public static void main(String[] args) throws Exception {

        final String code = "var a = (ln(b) + avg(c))*2";

        CharStream input = new ANTLRStringStream(code);
        JavaLikeToAstLexer lexer = new JavaLikeToAstLexer(input);
        CommonTokenStream tokens = new CommonTokenStream(lexer);

        JavaLikeToAstParser parser = new JavaLikeToAstParser(tokens);

        JavaLikeToAstParser.compilationUnit_return result = parser
                .compilationUnit();

        if (lexer.getNumberOfSyntaxErrors() > 0 || parser.getNumberOfSyntaxErrors() > 0) {
            throw new Exception("Syntax Errors encountered!");
        }

        CommonTree tree = (CommonTree) result.tree;

        System.out.printf("Baseline AST: %s%n%n", tree.toStringTree());

        tree = flatten(tree);

        System.out.printf("Flattened AST: %s%n%n", tree.toStringTree());

        translate(tree, "AstToPlsql.stg");
        translate(tree, "AstToGlobal.stg");
    }

    private static CommonTree flatten(CommonTree tree) {
        AstToFlatAstParser parser = new AstToFlatAstParser(
                new CommonTreeNodeStream(tree));
        return (CommonTree) parser.downup(tree, true);
    }

    private static void translate(CommonTree tree, String templateResourceName)
            throws Exception {
        AstToTemplateParser parser = new AstToTemplateParser(
                new CommonTreeNodeStream(tree));
        InputStream stream = JavaLikeToTemplateTest.class
                .getResourceAsStream(templateResourceName);
        Reader reader = new InputStreamReader(stream);
        parser.setTemplateLib(new StringTemplateGroup(reader));
        reader.close();
        stream.close();

        System.out.printf("Result for %s%n%n%s%n%n", templateResourceName,
                parser.program().st.toString());

    }

以下是两个简单的StringTemplate组文件来处理翻译过程。

AstToGlobal.stg

group AstToGlobal;

methods ::= ["*":"multiply", "/":"divide", "+":"add", "-":"subtract", "avg":"average", "ln":"log_N", default:key]

assign(name, op, args) ::= <<var <name> = <op>(<args;separator=", ">) >>

op(name) ::= "<methods.(name)>"

write(text) ::= << <text;separator="\n"> >>

AstToPlsql.stg

group AstToPlsql;

methods ::= ["*":"multiply", "/":"divide", "+":"add", "-":"subtract", "avg":"average", "ln":"log_N", default:key]

assign(name, op, args) ::=<< <op>(<name>, <args;separator=", ">) >>

op(name) ::= "<methods.(name)>"

write(text) ::= << <text;separator="\n"> >>

应用程序产生以下输出:

Baseline AST: (PROGRAM (DECL a (* (+ (CALL ln b) (CALL avg c)) 2)))

(CALL ln b) -> var0
(CALL avg c) -> var1
(+ var0 var1) -> var2
(PROGRAM (DECL a (* var2 2))) -> (PROGRAM (DECL var0 (CALL ln b)) (DECL var1 (CALL avg c)) (DECL var2 (+ var0 var1)) (DECL a (* var2 2)))
Flattened AST: (PROGRAM (DECL var0 (CALL ln b)) (DECL var1 (CALL avg c)) (DECL var2 (+ var0 var1)) (DECL a (* var2 2)))

Result for AstToPlsql.stg

  log_N(var0, b ) 
  average(var1, c ) 
  add(var2, var0 , var1 ) 
  multiply(a, var2 , 2 )  

Result for AstToGlobal.stg

 var var0 = log_N(b ) 
 var var1 = average(c ) 
 var var2 = add(var0 , var1 ) 
 var a = multiply(var2 , 2 )  

AstToTemplate.g中没有代码或处理简单分配的模板,例如var a = 3,但我认为使用op / method赋值来添加代码来处理它是很容易的引导。