手写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, ...
随机推荐
- Vue3全局APi解析-源码学习
本文章共5314字,预计阅读时间5-15分钟. 前言 不知不觉Vue-next的版本已经来到了3.1.2,最近对照着源码学习Vue3的全局Api,边学习边整理了下来,希望可以和大家一起进步. 我们以官 ...
- 为什么catch了异常,但事务还是回滚了?
前几天我发了这篇文章<我来出个题:这个事务会不会回滚?>得到了很多不错的反馈,也有不少读者通过微信.群或者邮件的方式,给了我一些关于test4的回复.其中还有直接发给我测试案例,来证明我的 ...
- 浅谈HttpDNS
今天了解了腾讯云的HTTPDNS,这里使用腾讯的资料以及网上查阅的资料做个记录. 对互联网高度依赖的企业,不可避免的需要通过域名来提供互联网服务,而在复杂的互联网环境下,域名被缓存,被劫持导致的业务影 ...
- QObject::startTimer: timers cannot be started from another thread 一则输出调试:关于QT的线程关联性
开始 今天在调试输出里看见QObject::startTimer: timers cannot be started from another thread QTimer类 从Qt的帮助文档里可以得到 ...
- ABP Framework:移除 EF Core Migrations 项目,统一数据上下文
原文:Unifying DbContexts for EF Core / Removing the EF Core Migrations Project 目录 导读:软件开发的一切都需要平衡 动机 警 ...
- 19、lnmp_mysql、nfs组件分离
19.1.LNMP一体机的数据库分离成独立的数据库: 1.根据以上学习过的方法在db01服务器上安装独立的mysql数据库软件: 2.在web01服务器上导出原先的数据库: [root@web01 t ...
- Linux下的SSH,你知道多少?
Openssh介绍 OpenSSH 是 SSH (Secure Shell) 协议的免费开源实现.SSH协议族可以用来进行远程控制, 或在计算机之间传送文件.而实现此功能的传统方式,如telnet(终 ...
- 10 年 bloger 告诉你要不要写博客,又该如何优雅地写博客?
关于作者:程序猿石头(ID: tangleithu),现任阿里巴巴技术专家,清华学渣,前大疆后端 Leader.公众号后台回复关键字 "1024" 获取程序员大厂面试指南. 图:D ...
- Spring:在web.xml正确加载spring配置文件的方式
web.xml加载spring配置文件的方式主要依据该配置文件的名称和存放的位置不同来区别,目前主要有两种方式. 1. 如果spring配置文件的名称为applicationContext.xml,并 ...
- springboot集成swagger添加消息头(header请求头信息)
springboot集成swagger上篇文章介绍: https://blog.csdn.net/qiaorui_/article/details/80435488 添加头信息: package co ...