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

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

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

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. 采用jmeter测试dubbo服务接口

    http://www.kissyu.org/2017/02/08/jmeter%E6%B5%8B%E8%AF%95dubbo%E6%8E%A5%E5%8F%A3/

  2. sonar做代码检测时如何忽略一些代码文件

    1.管理员登录sonar 2.如图 一条规则配置一个,不要填写逗号或者分号分割的多个规则

  3. jQuery异步框架探究1:jQuery._Deferred方法

    jQuery异步框架应用于jQuery数据缓存模块.jQuery ajax模块.jQuery事件绑定模块等多个模块,是jQuery的基础功能之中的一个.实际上jQuery实现的异步回调机制能够看做ja ...

  4. jQuery無刷新上傳之uploadify簡單試用

    先簡單的侃兩句:貌似已經有兩個月的時間沒有寫過文章了,不過仍會像以前那样每天至少有一至兩個小時是泡在园子裏看各位大神的文章.前些天在研究“ajax無刷新上傳”方面的一些插件,用SWFUpload實現了 ...

  5. C# 插件热插拔 .NET:何时应该 “包装异常”? log4.net 自定义日志文件名称

    C# 插件热插拔   所谓热插拔就是插件可以 在主程序不重新启动的情况直接更新插件, 网上有很多方案: https://www.cnblogs.com/happyframework/p/3405811 ...

  6. 秒懂C#通过Emit动态生成代码 C#使用Emit构造拦截器动态代理类

    秒懂C#通过Emit动态生成代码   首先需要声明一个程序集名称, 1 // specify a new assembly name 2 var assemblyName = new Assembly ...

  7. Qt 插件综合编程-基于插件的OpenStreetMap瓦片查看器client(5) 小结

    经过不断试用与改动,这个查看器终于还是完毕了设计.实现.查看器,顾名思义,没有编辑功能:说的白一点,仅仅是一个以OpenStreetMap为底图的显示装置罢了.和专业GIS相比,这款基于插件的Open ...

  8. XML(一)语法

    一.xml语法 1.文档声明 2.元素 3.属性 4.凝视 5.CDATA区.转义字符 6.处理指令 1.文档声明: 用来声明xml的基本属性,用来指挥解析引擎怎样去解析当前xml 通常一个xml都要 ...

  9. Java中的Nested Classes和Inner Classes

    Java中的Nested Classes和Inner Classes Java有嵌套类(Nested Classes)和内部类(Inner Classes)的概念. 嵌套类(Nested Classe ...

  10. react 引入 百度地图API

    使用 Echarts 的地图的时候,发现报错,说 Bmap api is not loaded 百度地图API没有加载 乍一想,Echarts 用的也是 百度地图 啊,没有引入百度地图,还用个啥,当然 ...