上次在公司内部讲《词法分析——使用正则文法》是一次失败的尝试——上午有十几个人在场,下午就只来了四个听众。

本来我还在构思如何来讲“语法分析”的知识呢,但现在看来已不太可能。

这个课程没有预想中的受欢迎,其原因可能是:

1.课程内容相对复杂,听众知识背景与基础差异比较大。

2.授课技巧不够,不能把复杂的知识简单化的呈现给基础稍差一点的人。

针对这两个可能的原因,我要尝试做出以下调整:

1.使用antlr来实现词法和语法的部分。

2.暂时把“编译”过程改为“解释”来实现。

使用antlr的原因是:

1.采用文法生成器可直接略过词法和语法的部分直接进入语义分析,这样利于速成,同时避免学员被词法分析和语法分析的复杂性吓到,而失去了继续学习的勇气。

2.antlr的文法是LL(k)型,非常易于编写——虽然k型方法的性能肯定不如1型文法,但与初学者谈性能问题并不是一个好主意,不如直接避开性能不谈,能运行即可。

3.antlr默认生成的是java代码,这与公司内大多数员工的现有知识是相吻合的。

下面进入正文。

一、什么是antlr?如何安装?

这不是一篇凑字数的文章,所以请直接参考官方网站(http://www.antlr.org/)。

我使用的是目前的最新版本(V4.2.2).

我上传了参考资料(包括jar包、电子书和官方示例)到百度云上,可从这个地址下载(http://pan.baidu.com/s/1hq65XWC)。

二、本计算器的文法示例及文法的解释。

整个计算器的词法的语法就由以下几行的antlr4代码来实现,先贴在下面:

grammar Calc;                            // 文法的名字为Calc

// 以下以小写字母开头的文法表示为语法元素
// 由大写字母开头的文法表示为词法元素
// 词法元素的表示类似于正则表示式
// 语法元素的表示类似于BNF exprs : setExpr // set表达式
| calcExpr // 或calc表达式
; setExpr : 'set' agmts ; // 以set命令开头,后面是多个赋值语句
agmts : agmt (';' agmts)? ';'? ; // 多个赋值语句是由一个赋值语句后根着多个赋值语句,中间由分号分隔,结尾有一个可选的分号
agmt : id=ID '=' num=NUMBER ; // 一个赋值语句是由一个ID,后跟着一个等号,再后面跟送一个数字组成
calcExpr: 'calc' expr ; // 以calc命令开头,后面是一个计算表达式 // expr可能由多个产生式
// 在前面的产生式优先于在后面的产生式
// 这样来解决优先级的问题 expr: expr op=(MUL | DIV) expr // 乘法或除法
| expr op=(ADD | SUB) expr // 加法或减法
| factor // 一个计算因子——可做为+-*/的操作数据的东西
; factor: (sign=(ADD | SUB))? num=NUMBER // 计算因子可以是一个正数或负数
| '(' expr ')' // 计算因子可以是括号括起来的表示式
| id=ID // 计算因子可以是一个变量
| funCall // 计算因子可以是一个函数调用
; funCall: name=ID '(' params ')' ; // 函数名后面加参数列表
params : expr (',' params)? ; // 参数列表是由一个表达式后面跟关一个可选的参数列表组成 WS : [ \t\n\r]+ -> skip ; // 空白, 后面的->skip表示antlr4在分析语言的文本时,符合这个规则的词法将被无视
ID : [a-z]+ ; // 标识符,由0到多个小写字母组成
NUMBER : [0-9]+('.'([0-9]+)?)? ; // 数字
ADD : '+' ;
SUB : '-' ;
MUL : '*' ;
DIV : '/' ;

我们把这段文法保存到一个文件Calc.g4中,并运行命令“antlr4 -visitor Calc.g4”即生成6个java文件和两个tokens文件。

这几个文件包括了这个计算器的“词法分析程序”、“语法分析程序”和一个visitor(CalcBaseVisitor.java),不过此时这个visitor内部实现都是空的,我们需要自己实现它。

在实现这个visitor之前,我们先实现一个上下文,上下文的做用有两个:

1.保存变量——用于在计算表达式中引用变量。

2.保存堆栈——用于函数的参数传递。

这个上下文的内容很少,代码也很短,直接贴在下面:

 public class Context {
private static Context ourInstance = new Context(); public static Context getInstance() {
return ourInstance;
} private Context() {
} private Map<String, Double> map = new HashMap<>();
private Deque<Double> stack = new ArrayDeque<>(); public Double getValue(String key) {
Double d = map.get(key);
return d == null ? Double.NaN : d;
} public void setContext(String key, Double value) {
map.put(key, value);
} public void setContext(String key, String value) {
setContext(key, Double.valueOf(value));
} public void pushStack(Double d) {
stack.push(d);
} public Double popStack() {
return stack.pop();
}
}

下面我们开始实现这个计算器的visitor,

 public class MyCalcVisitor extends CalcBaseVisitor<Double> {

     @Override
public Double visitExprs(CalcParser.ExprsContext ctx) {
return visit(ctx.getChild(0));
} @Override
public Double visitAgmt(CalcParser.AgmtContext ctx) {
Context.getInstance().setContext(ctx.id.getText(), ctx.num.getText());
return null;
} @Override
public Double visitAgmts(CalcParser.AgmtsContext ctx) {
visit(ctx.agmt());
if (ctx.agmts() != null)
visit(ctx.agmts());
return null;
} @Override
public Double visitCalcExpr(CalcParser.CalcExprContext ctx) {
return visit(ctx.expr());
} @Override
public Double visitExpr(CalcParser.ExprContext ctx) {
int cc = ctx.getChildCount();
if (cc == 3) {
switch (ctx.op.getType()) {
case CalcParser.ADD:
return visit(ctx.expr(0)) + visit(ctx.expr(1));
case CalcParser.SUB:
return visit(ctx.expr(0)) - visit(ctx.expr(1));
case CalcParser.MUL:
return visit(ctx.expr(0)) * visit(ctx.expr(1));
case CalcParser.DIV:
return visit(ctx.expr(0)) / visit(ctx.expr(1));
}
} else if (cc == 1) {
return visit(ctx.getChild(0));
}
throw new RuntimeException();
} @Override
public Double visitFactor(CalcParser.FactorContext ctx) {
int cc = ctx.getChildCount();
if (cc == 3) {
return visit(ctx.getChild(1));
} else if (cc == 2) {
if (ctx.sign.getType() == CalcParser.ADD)
return Double.valueOf(ctx.getChild(1).getText());
if (ctx.sign.getType() == CalcParser.SUB)
return -1 * Double.valueOf(ctx.getChild(1).getText());
} else if (cc == 1) {
if (ctx.num != null)
return Double.valueOf(ctx.getChild(0).getText());
if (ctx.id != null)
return Context.getInstance().getValue(ctx.id.getText());
return visit(ctx.funCall());
}
throw new RuntimeException();
} @Override
public Double visitParams(CalcParser.ParamsContext ctx) {
if (ctx.params() != null)
visit(ctx.params());
Context.getInstance().pushStack(visit(ctx.expr()));
return null;
} @Override
public Double visitFunCall(CalcParser.FunCallContext ctx) {
visit(ctx.params());
String funName = ctx.name.getText();
switch (funName) {
case "pow":
return Math.pow(Context.getInstance().popStack(), Context.getInstance().popStack());
case "sqrt":
return Math.sqrt(Context.getInstance().popStack());
}
throw new RuntimeException();
} @Override
public Double visitSetExpr(CalcParser.SetExprContext ctx) {
return visit(ctx.agmts());
} }

最后再实现一个入口,调用这个Visitor即完成了我们的计算器。

入口代码如下:

 import java.util.Scanner;

 import org.antlr.v4.runtime.ANTLRInputStream;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.tree.ParseTree; public class Portal { private static final String lineStart = "CALC> "; public static void main(String[] args) {
try (Scanner scanner = new Scanner(System.in)) {
System.out.print(lineStart);
while (scanner.hasNext()) {
String line = scanner.nextLine();
if (line != null) {
line = line.trim();
if (line.length() != 0) {
if ("exit".equals(line) || "bye".equals(line))
break;
ANTLRInputStream input = new ANTLRInputStream(line);
CalcLexer lexer = new CalcLexer(input);
CommonTokenStream tokens = new CommonTokenStream(lexer);
CalcParser parser = new CalcParser(tokens);
ParseTree tree = parser.exprs();
MyCalcVisitor mv = new MyCalcVisitor();
Double res = mv.visit(tree);
if (res != null)
System.out.println(res);
}
} System.out.print(lineStart);
}
}
} }

整个计算器只写了一个文法和三个类,所有代码都贴在上面了,相对于完全自己手写的计算器来说,的确是简单很多了。

用antlr4来实现《按编译原理的思路设计的一个计算器》中的计算器的更多相关文章

  1. 学了编译原理能否用 Java 写一个编译器或解释器?

    16 个回答 默认排序​ RednaxelaFX JavaScript.编译原理.编程 等 7 个话题的优秀回答者 282 人赞同了该回答 能.我一开始学编译原理的时候就是用Java写了好多小编译器和 ...

  2. 编译原理-词法分析05-正则表达式到DFA-01

    编译原理-词法分析05-正则表达式到DFA 要经历 正则表达式 --> NFA --> DFA 的过程. 0. 术语 Thompson构造Thompson Construction 利用ε ...

  3. 跟vczh看实例学编译原理——三:Tinymoe与无歧义语法分析

    文章中引用的代码均来自https://github.com/vczh/tinymoe.   看了前面的三篇文章,大家应该基本对Tinymoe的代码有一个初步的感觉了.在正确分析"print ...

  4. 跟vczh看实例学编译原理——二:实现Tinymoe的词法分析

    文章中引用的代码均来自https://github.com/vczh/tinymoe.   实现Tinymoe的第一步自然是一个词法分析器.词法分析其所作的事情很简单,就是把一份代码分割成若干个tok ...

  5. 跟vczh看实例学编译原理——一:Tinymoe的设计哲学

    自从<序>胡扯了快一个月之后,终于迎来了正片.之所以系列文章叫<看实例学编译原理>,是因为整个系列会通过带大家一步一步实现Tinymoe的过程,来介绍编译原理的一些知识点. 但 ...

  6. 跟vczh看实例学编译原理——零:序言

    在<如何设计一门语言>里面,我讲了一些语言方面的东西,还有痛快的喷了一些XX粉什么的.不过单纯讲这个也是很无聊的,所以我开了这个<跟vczh看实例学编译原理>系列,意在科普一些 ...

  7. 编译原理-词法分析04-NFA & 代码实现

    编译原理-词法分析04-NFA & 代码实现 0.术语 NFA 非确定性有穷自动机nondeterministic finite automation. ε-转换ε-transition 是无 ...

  8. .NET程序的简单编译原理

    1.不管是什么程序,最终的执行官是CPU,而CPU只认识1和0的机器码. 2.我们现在写的一般是高级语言写的程序.CPU是不认识我们用高级语言写的源代码的,那应该怎么办才能让CPU执行我们写好的程序尼 ...

  9. Atitit.编译原理与概论

    Atitit.编译原理与概论 编译原理 词法分析 Ast构建,语法分析 语意分析 6 数据结构  1. ▪ 记号 2. ▪ 语法树 3. ▪ 符号表 4. ▪ 常数表 5. ▪ 中间代码 1. ▪ 临 ...

随机推荐

  1. 网络安全-安全散列函数,信息摘要SHA-1,MD5原理

    -----------------------------------------------欢迎查看网络安全连载博客-----------------------------------[网络安全] ...

  2. Java之旅hibernate(2)——文件夹结构

    Hibernate的jar最好是到官网上进行下载.下载最新的稳定的版本号.之后进行解压,以下我们介绍一下hibernate的包结构. 1.      包结构 我们能够看到包文件夹结构发生了变化.我以5 ...

  3. 【Android】应用安全——反编译

    用java开发最操心的就是得到反编译,所以作为开发人员我们须要知道怎么反编译,那样才干知道怎样防止反编译.保证代码安全. 以下来看下比較经常使用的方法! 第一种方式:利用apktool反编译 1,首先 ...

  4. 使用heartbeat+monit实现主备双热备份系统

    一.使用背景 项目须要实现主备双热自己主动切换的功能,保证系统7*24小时不间断执行.现已有两台双网卡的IBM的server,为了不再添加成本採购独立外部存储设备和双机热备软件.採用了linux下开源 ...

  5. Echarts 如何使用 bmap 的 API

    使用 Echarts 在绘制 Binning on map 的图形时(其实也就是 在地图上绘制热力色块图) 解决因为数据量过大,希望在拖拽加载或者缩放加载的时候,根据可视区域的经纬度范围,来请求相应的 ...

  6. Wordpress3.9开启多网站配置配置nginx进行局域网測试.

    由于须要帮staff迁移一些数据, 所以想到了使用wordpress的多网站. 这个功能在wordpress3.0后就有了. 软件系统等信息:  OS: linux debian wheezy php ...

  7. Java的Graphics中drawImage与drawLine的坐标区别

    drawImage复制的区域是 dx1 <= x < dx2,dy1 <= y < dy2 drawLine绘制区域是 dx1 <= x <= dx2,dy1 &l ...

  8. 混淆时报:Proguard returned with error code 1. See console

    发生这个错误是因为打包混淆时找不到我们的引用包,有的人可能说我没有引用什么Library啊,事实上,我们现在的项目创建时就默认有v4包,这是google提供的兼容包,主要为了应对Android3.0以 ...

  9. Java:EL表达式

    ylbtech-Java:EL表达式 EL(Expression Language) 是为了使JSP写起来更加简单.表达式语言的灵感来自于 ECMAScript 和 XPath 表达式语言,它提供了在 ...

  10. PCB 漏加阻抗条的臆想(转)

    阻抗条,我对你是有感情的,这你一定要相信我! 否则,不会在之前的每一次拼板,都不忘拥你入Panel之怀. 自做CAM开始,已记不清我们曾有多少次不期而遇, 我们一同迎接朝阳,送走晚霞,凝望窗外如洗的月 ...