ANTLR4(十) 内嵌代码之计算器

tech2025-12-12  0

写在之前

采用访问器的计算器

语法文件:

grammar Expr; prog: stat+ ; stat: expr NEWLINE | ID '=' expr NEWLINE | CLEAR NEWLINE | NEWLINE ; expr: expr op=('*'|'/') expr | expr op=('+'|'-') expr | INT | ID | '(' expr ')' ; 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

自定义Visitor:

//EvalVistor.java import java.util.HashMap; import java.util.Map; public class EvalVisitor extends LabeledExprBaseVisitor<Integer> { /** "memory" for our calculator; variable/value pairs go here */ Map<String, Integer> memory = new HashMap<String, Integer>(); /** ID '=' expr NEWLINE */ @Override public Integer visitAssign(LabeledExprParser.AssignContext ctx) { String id = ctx.ID().getText(); // id is left-hand side of '=' int value = visit(ctx.expr()); // compute value of expression on right memory.put(id, value); // store it in our memory return value; } /** expr NEWLINE */ @Override public Integer visitPrintExpr(LabeledExprParser.PrintExprContext ctx) { Integer value = visit(ctx.expr()); // evaluate the expr child System.out.println(value); // print the result return 0; // return dummy value } /** CLEAR MEMORY */ @Override public Integer visitClearMemory(LabeledExprParser.ClearMemoryContext ctx) { memory.clear(); System.out.println("clear"); return 0; } /** INT */ @Override public Integer visitInt(LabeledExprParser.IntContext ctx) { return Integer.valueOf(ctx.INT().getText()); } /** ID */ @Override public Integer visitId(LabeledExprParser.IdContext ctx) { String id = ctx.ID().getText(); if ( memory.containsKey(id) ) return memory.get(id); return 0; } /** expr op=('*'|'/') expr */ @Override public Integer visitMulDiv(LabeledExprParser.MulDivContext ctx) { int left = visit(ctx.expr(0)); // get value of left subexpression int right = visit(ctx.expr(1)); // get value of right subexpression if ( ctx.op.getType() == LabeledExprParser.MUL ) return left * right; return left / right; // must be DIV } /** expr op=('+'|'-') expr */ @Override public Integer visitAddSub(LabeledExprParser.AddSubContext ctx) { int left = visit(ctx.expr(0)); // get value of left subexpression int right = visit(ctx.expr(1)); // get value of right subexpression if ( ctx.op.getType() == LabeledExprParser.ADD ) return left + right; return left - right; // must be SUB } /** '(' expr ')' */ @Override public Integer visitParens(LabeledExprParser.ParensContext ctx) { return visit(ctx.expr()); // return child expr's value } }

内嵌在语法文件中

这次我们想要达成这样的效果:一行一行地输入表达式,每次回车就输出运算结果。

而不是和原有语法一样,输入整个文件进行语法树分析再输出。

内嵌语法文件

我们分开各个部分讲解。

@header:我们设定打包文件在tools文件夹,并且输入jar文件。@parser::members:指内嵌到语法分析中的成员。memory和eval本来是在自定义访问器中的。需要注意op的分支是词法符号MUL、DIV、ADD、SUB。e returns [int v]:a、op、b等作为标记,它们可以标记到词法符号以及语法分析过程中的context上,e会被重新打包成这样一个EContext: 需要注意的是op,它通过.type返回的就是MUL等词法符号。而INT.int实际上就是把text的类型(String)通过int返回。 grammar Expr; @header{ package tools; import java.util.*; } @parser::members{ 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; } } //prog: stat+ ; 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 <label id="code.tour.expr.3"/> INT : [0-9]+ ; // match integers NEWLINE:'\r'? '\n' ; // return newlines to parser (is end-statement signal) WS : [ \t]+ -> skip ; // toss out whitespace

重写主程序

输入部分还是不变。

我们设置一个足够大的缓冲区BufferedReader,在实例化的时候创建InputStream的Reader并赋给br。

先创建语法分析器,不生成树。

只要缓冲区还能读得到内容,就进行这样一个循环:

ANTLRInputStream->ExprLexer->CommonTokenStream->Parser。

需要注意的是,由于流里的数据一行一行在增加,每次lexer需要通过行号去读取一行。

package tools; import org.antlr.v4.runtime.ANTLRInputStream; import org.antlr.v4.runtime.CommonTokenStream; import java.io.BufferedReader; import java.io.FileInputStream; import java.io.InputStream; import java.io.InputStreamReader; public class Calc{ public static void main(String[] args)throws Exception{ String inputFile = null; if ( args.length>0 ) inputFile = args[0]; InputStream is = System.in; if ( inputFile!=null ) { is = new FileInputStream(inputFile); } BufferedReader br=new BufferedReader(new InputStreamReader(is)); String expr=br.readLine(); int line=1; ExprParser parser=new ExprParser(null); parser.setBuildParseTree(false); while(expr!=null) { ANTLRInputStream input=new ANTLRInputStream(expr+'\n'); ExprLexer lexer=new ExprLexer(input); lexer.setLine(line); lexer.setCharPositionInLine(0); CommonTokenStream tokens=new CommonTokenStream(lexer); parser.setInputStream(tokens); parser.stat(); expr=br.readLine(); line++; } } }

运行结果

输入:

antlr4 tools/Expr.g4

javac -d . tools/*.java

java tools.Calc

注意第二句,-d指明当前类层次根目录,.也就是当前目录,tools/*java为编译文件。

输出:

最新回复(0)