手写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, ...
随机推荐
- 重新整理 .net core 实践篇—————领域事件[二十九]
前文 前面整理了仓储层,工作单元模式,同时简单介绍了一下mediator. 那么就mediator在看下领域事件启到了什么作用吧. 正文 这里先注册一下MediatR服务: // 注册中间者:Medi ...
- hackthebox TheNotebook
前言 只拿到了user,提权没成功--有wp说是CVE-2019-5736,我没打成. 打点 nmap-sV -v -A 10.10.10.230 端口扫描结果: PORT STATE SERVICE ...
- Java-学习日记(Atomic,Volatile)
很早之前在公司就看到了atomicInteger,atomicLong这些变量了,一直不明白是什么意思,今天花了点时间了解下. volatile: 先从volatile开始讲起,volatile是多线 ...
- Golang的一致性哈希实现
Golang的一致性哈希实现 一致性哈希的具体介绍,可以参考:http://www.cnblogs.com/haippy/archive/2011/12/10/2282943.html 1 imp ...
- 《面试补习》- Java集合知识梳理
一.ArrayList ArrayList 底层数据结构为 动态数组 ,所以我们可以将之称为数组队列. ArrayList 的依赖关系: public class ArrayList<E> ...
- hdu 1556 Color the ball 线段树 区间更新
水一下 #include <bits/stdc++.h> #define lson l, m, rt<<1 #define rson m+1, r, rt<<1|1 ...
- Spring:Spring-AOP简介
什么是SpringAOP? 将一些相关的编程方法,独立提取出来,独立实现,然后动态地将代码切入到类的指定方法.指定位置上的编程方式就是AOP(面向切面编程). 讲解一下AOP中的相关概念 Aspect ...
- Linux使用shell脚本监控
(1)性能监控脚本 performance.sh #!/bin/bash #-------------------------------------------------------------- ...
- python中的内置函数lambda map filter reduce
p.p1 { margin: 0; font: 12px "Helvetica Neue" } p.p2 { margin: 0; font: 12px "Helveti ...
- ctf实验吧逻辑问题
ctf5.shiyanbar.com/web/5/index.php 绕开. php题,习惯先看源码,F12,结果发现了 url输入了一看 告诉了我们后台逻辑.分析一下,发现只要使得$row[&quo ...