手写Pascal解释器(一)
一、编写解释器的动机
学习了Vue之后,我发现对字符串的处理对于编写一个程序框架来说是非常重要的,就拿Vue来说,我们使用该框架时可以通过如v-on:, v-model等html的属性时,我们能够在里面嵌入js代码,其实这块就已经使用了编译原理的知识来对输入的字符串进行解析,然后将它们嵌入到js代码中去,这也是我们在Vue中可以如此轻松地进行双向绑定,使用v-for进行列表渲染等等的技术基础。此外在做ccf csp的题目时,我也往往被一些字符串处理的题目给卡住,有时候虽然可以做出来,但有种只见树木不见森林之感。所以我希望可以快点先学习到一些编译原理的知识(而且自学往往比上课学习的效率和积极性高得多),为后面的学习打个基础。
至于为什么学习编译原理先学习编写解释器呢?之前我是直接啃“龙书”(《compiler》)来着,可是里面真的许多东西比较晦涩难懂,之后我先去逛下知乎看看大神们是怎么学习编译原理的,看到有位大佬说可以多抄几遍这个解释器项目,很多东西自然就理解了:学习编译原理有什么好的书籍? - 时雨的回答 - 知乎。然后我就点进去那个GitHub项目的链接,发现居然star数高达1.3k:

于是我决定先通过它来进行学习了。
二、part1
(补上gitee项目地址,欢迎clone项目:https://gitee.com/warrior__night/learn-writing-interpreter)
资料链接:https://ruslanspivak.com/lsbasi-part1/
part1的任务比较简单,它需要输入一个带有两个操作数的字符串,且只支持“+”号,如输入“1+2”,它会输出“3”。
项目的源码是使用python编写的,而我为了印象深刻些(也防止自己不加思考嗯抄),就使用java进行重新编写了。

编写了这样几个类,由于java是半静态语言,故各个Token都使用类来进行封装了(如TK_Interger,即为整形数字,继承自Token类,其他的类似),使用起来比较方便一些。
主要的类为Interpreter类,重点解读一些Interpreter类干了什么事:
类的成员变量:
private final String text;
private int pos;
private Token currentToken;
text是输入的表达式字符串,pos是当前指向哪个位置的字符,currentToken是当前的Token是什么。
构造函数:
public Interpreter(String text){
this.text = text;
this.pos = 0;
this.currentToken = null;
}
接收一个表达式字符串,其他的变量都清零
抛出异常函数:
public void error() throws Exception {
throw new Exception("Error parsing input");
}
当输入不符合当前规则时抛出“Error parsing input”异常
获取下一个Token的函数getNextToken:
public Token getNextToken() throws Exception {
// 如果下标到了字符串的尽头,则返回TK_EOF
if (pos > text.length()-1){
return new TK_EOF();
}
char currentChar = text.charAt(pos);
// 如果是数字,则返回TK_Interger(即数字Token),由于part1只考虑一个数字的情况,故只需解析一个数字
if (Character.isDigit(currentChar)){
Token token = new TK_Integer(Integer.parseInt(currentChar+""));
// 指针移动到读取完Token的位置
pos++;
return token;
}
// 解析符号也是相同的过程
if (currentChar == '+'){
Token token = new TK_Plus();
pos++;
return token;
}
// 如果不是数字或者“+”,则抛出异常
this.error();
return null;
}
eat函数:
public void eat(Token.TokenType tokenType) throws Exception {
if (currentToken.type == tokenType){
currentToken = getNextToken();
}
else {
this.error();
}
}
判断当前读取到的Token和预想的是不是一样的类型,然后读取下一个Token。
完成整个解析过程的函数:
public int expr() throws Exception {
currentToken = getNextToken();
// 第一个数
Token left = currentToken;
// 查看第一个Token是否是数字,并读取下一个Token
eat(Token.TokenType.INTEGER);
// 查看这个Token是否是“+”
eat(Token.TokenType.PLUS);
// 第二个数
Token right = currentToken;
// 查看第二个Token是否是数字,并读取下一个Token
eat(Token.TokenType.INTEGER);
// 最后返回运算结果
return (Integer)left.value + (Integer)right.value;
}
客户端类Main:
public class Main {
public static void main(String[] args) throws Exception {
Scanner scanner = new Scanner(System.in);
while (true){
System.out.print("calc> ");
String text = scanner.nextLine();
if (text.equals("exit"))
break;
Interpreter interpreter = new Interpreter(text); <--使用解释器进行解释
int res = interpreter.expr();
System.out.println("res: "+res);
}
}
}
运行结果:

运行结果符合预期,part1搞定!
三、part2
资料链接:https://ruslanspivak.com/lsbasi-part2/
part2相较于part1增加了“-”(减法)的支持,然后可以跳过表达式中间的空格。
增加的函数或修改:
成员变量添加currentChar,代表当前指针指向的字符:
...
private Character currentChar;
构造函数添加currentChar的初始化:
public Interpreter(String text){
...
this.currentChar = text.charAt(pos);
}
添加指针向前的函数advance():
private void advance(){
pos++;
if (pos>text.length()-1){
this.currentChar = null;
}
else {
this.currentChar = text.charAt(pos);
}
}
添加跳过空格函数:
private void skipWhitespace(){
while (this.currentChar!=null && this.currentChar==' '){
this.advance();
}
}
读取整形数字函数:
private int integer(){
StringBuilder sb = new StringBuilder();
while (currentChar!=null && Character.isDigit(currentChar)){
sb.append(currentChar);
this.advance();
}
return Integer.parseInt(sb.toString());
}
有了这个函数,我们就可以读出多位的数字了。
修改getNextToken()函数:
private Token getNextToken() throws Exception {
while (currentChar != null){
// 如果是空格则直接跳过
if (this.currentChar == ' '){
this.skipWhitespace();
continue;
}
// 如果是数字则交给integer函数读取处理
if (Character.isDigit(currentChar)){
return new TK_Integer(this.integer());
}
// 如果是“+”或者“-”则返回TK_Plus和TK_Minus
if (currentChar=='+'){
this.advance();
return new TK_Plus();
}
if (currentChar=='-'){
this.advance();
return new TK_Minus();
}
// 如果是其他情况则抛出异常
this.error();
}
// 如果为currentChar空则返回TK_EOF
return new TK_EOF();
}
修改expr函数:
public int expr() throws Exception {
currentToken = getNextToken();
Token left = currentToken;
eat(Token.TokenType.INTEGER);
Token op = currentToken;
if (op.type == Token.TokenType.PLUS)
eat(Token.TokenType.PLUS);
else
eat(Token.TokenType.MINUS);
Token right = currentToken;
eat(Token.TokenType.INTEGER);
if (op.type == Token.TokenType.PLUS)
return (Integer)left.value + (Integer)right.value;
else
return (Integer)left.value - (Integer)right.value;
}
和part1的基本相同,这里不再赘述。
运行结果:

四、part3
资料链接:https://ruslanspivak.com/lsbasi-part3/
以下图片为大佬博客翻译的内容:


part3的任务是可以解析多个操作数的加减运算,如“1+2 -3 +4”等。
添加或修改的部分:
添加term()函数:
public int term() throws Exception {
Token token = currentToken;
this.eat(Token.TokenType.INTEGER);
return (Integer) token.value;
}
其实就是eat+返回int的值
修改expr函数:
public int expr() throws Exception {
currentToken = getNextToken();
// 获取当前Token的值并移动指针
int result = this.term();
while (currentToken.type == Token.TokenType.PLUS || currentToken.type== Token.TokenType.MINUS){
Token token = currentToken;
if (token.type== Token.TokenType.PLUS){
// 判断并移动
eat(Token.TokenType.PLUS);
// 加上下一个数并移动指针
result+=term();
}
else{
// 解析同上
eat(Token.TokenType.MINUS);
result-=term();
}
}
return result;
}
运行结果:

下一篇:手写Pascal解释器(二)
手写Pascal解释器(一)的更多相关文章
- 手写Pascal解释器(三)
目录 一.part7 抽象语法树和具体语法树(解析树) 代码实现 二.part8 一.part7 资料来源:https://ruslanspivak.com/lsbasi-part7/ 看作者博客的标 ...
- 手写Pascal解释器(二)
目录 一.part4 补充理论知识 二.part5 设计生成式 三.part6 一.part4 承接上次的内容,我们继续编写part4,这个部分我们的任务是完成输入一个仅带乘除运算符的表达式,然后返回 ...
- MINST手写数字识别(一)—— 全连接网络
这是一个简单快速入门教程——用Keras搭建神经网络实现手写数字识别,它大部分基于Keras的源代码示例 minst_mlp.py. 1.安装依赖库 首先,你需要安装最近版本的Python,再加上一些 ...
- 用 F# 手写 TypeScript 转 C# 类型绑定生成器
前言 我们经常会遇到这样的事情:有时候我们找到了一个库,但是这个库是用 TypeScript 写的,但是我们想在 C# 调用,于是我们需要设法将原来的 TypeScript 类型声明翻译成 C# 的代 ...
- 剖析手写Vue,你也可以手写一个MVVM框架
剖析手写Vue,你也可以手写一个MVVM框架# 邮箱:563995050@qq.com github: https://github.com/xiaoqiuxiong 作者:肖秋雄(eddy) 温馨提 ...
- 以鶸ice为例,手撸一个解释器(一)明确目标
代码地址 # HelloWorld.ice print("hello, world") 前言(废话) 其实从开始学习编译原理到现在已经有快半年的时间了,但是其间常常不能坚持看下去龙 ...
- 【Win 10 应用开发】手写识别
记得前面(忘了是哪天写的,反正是前些天,请用力点击这里观看)老周讲了一个14393新增的控件,可以很轻松地结合InkCanvas来完成涂鸦.其实,InkCanvas除了涂鸦外,另一个大用途是墨迹识别, ...
- JS / Egret 单笔手写识别、手势识别
UnistrokeRecognizer 单笔手写识别.手势识别 UnistrokeRecognizer : https://github.com/RichLiu1023/UnistrokeRecogn ...
- 如何用卷积神经网络CNN识别手写数字集?
前几天用CNN识别手写数字集,后来看到kaggle上有一个比赛是识别手写数字集的,已经进行了一年多了,目前有1179个有效提交,最高的是100%,我做了一下,用keras做的,一开始用最简单的MLP, ...
随机推荐
- java变量及常量
变量 本质:就是代表一个"可操作的存储空间",空间位置是确定的,但是里面放置什么值不确定.我们可通过变量名来访问"对应的存储空间",从而操纵这个"存储 ...
- VLAN的基础介绍与使用方法
一.VLAN概述与优势 二.VLAN的种类 三.VLAN的范围 四.VLAN的三种接口模式 五.VLAN的实例操作 一.VLAN概述与优势 VLAN(虚拟局域网)通过为子网提供数据链路连接来抽象出局域 ...
- 基于Istio构建微服务安全加固平台的探索
简介 An open platform to connect, secure, control and observe services. Istio 是一个由谷歌.IBM 与Lyft共同开发的开源项 ...
- 富文本编辑器之游戏角色升级ing
一.前言 想必大家看到这个标题,心中不禁会浮现几个问题: 什么是富文本编辑器? 富文本编辑器和游戏角色有什么关系? 为什么是升级ing? 什么是富文本编辑器--富文本编辑器集成了格式设置.媒体嵌入.社 ...
- 测试开发:推荐一款阿里最新 Python 自动化开源工具!
大家好,我是麦小米,是狂师老师全栈测开训练营中的一名学员. 如果之前做过iOS自动化的同学相信都知道,一直以来,iOS自动化的实现&执行都必须依赖 Mac 系统,其主要原因是因为需要通过 xc ...
- Binding(四):数据校验
除了上一节讲的类型转换器,Binding还自带数据校验功能,这节主要来讲一下. 跟类型转换器一样,数据校验需要我们继承ValidationRule类,实现其中的Validate方法,并写入我 ...
- oracle 日常运维
1.查询表或存储过程.函数异常 select * from user_errors where name ='TEST_TABLE' 2.查询表是否存在 select * from user_tabl ...
- SpringBoot Redis 2.0.x
redis的安装 在笔者之前的文章中有介绍redis的安装,不会的可以去看 笔者之前写的文章redis安装 完成安装后如果不熟悉redis的操作,redis官方文档也有基本操作指南,redis基本操作 ...
- shell 重定向以及文件描述符
1.对重定向的理解 Linux Shell 重定向分为两种,一种输入重定向,一种是输出重定向:从字面上理解,输入输出重定向就是「改变输入与输出的方向」的意思. 输入方向就是数据从哪里流向程序.标准输入 ...
- python 两种排序方法 sort() sorted()
python中有两种排序方法,list内置sort()方法或者python内置的全局sorted()方法 区别为: sort()方法对list排序会修改list本身,不会返回新list.sort()只 ...