# 从RegExp开始搓一个简单的脚本语言
## Ez Regular Expression
来点简单的正则语法(
可能也不会很简单,因为已经开始预查了(
一些基本的语法就直接跳过(
### 预查
如果要匹配一个Matrix
,但是不想匹配一个Matrix2
,这个时候,就可以用预查来完成(
可以用向前预查Lookahead assertion,写成Matrix(?!2)
,来匹配所有Matrix
且下一个字符不是2
的字串,肯定预查为Matrix(?=2)
有的时候,想匹配一个Number
,但是不想匹配一个INumber
,这个时候,也可以用预查来完成(
以上面的例子写regex,用向后预查Lookbehind assertion,可以写成(?<!I)Number
,这会匹配所有Number
且前一个字符不为I
的字串
当然这是否定预查,也可以使用肯定预查(?<=I)Number
,这会匹配所有Number
且前一个字符为I
的字串,注意,匹配到的字串不包括I
## 开搓!
### 创建venv
对的,用python搓一个,为什么用python呢,因为我懒。
regex
是解释器的核心之一,正则写的好,解释器就完成了一部分(
matplotlib
是课程要求(因为要求是做一个能用来绘图的语言,所以直接用matplotlib
### 字面量的匹配
先定义一些字面量的类型,直接写成正则:
要注意这里的正则限定了开头,但是没有限定末尾,所以一定要写好匹配的边界,不然可能会漏掉内容或者匹配到了额外的内容。然后就是写一个函数,来处理这些字面量:
这里会涉及到一个Var
类型,定义如下:
这个Var
其实就是一个wrapper,用来在运行时进行一些简单的类型检查(虽然写到后面直接开摆了),和引用在scope里面的Var
对象,并修改所绑定的值
### 标识符和关键词和运算符
这里写了好几个关键词,但是其实只用到了for
和in
以及写一下各个运算符所对于的函数:
### 开始解析表达式
### 表达式求值
写两个方便debug的函数
### 测试功能
it works
## 具体代码分析
摸了
## 改进方向
- 加入流程控制
- 优化运算符的解析,支持更复杂的表达式
# 实验报告版(大部分内容相同,只是为了模板改了改)
## 1. 总体任务
题目:为函数绘图语言编写一个解释器,解释器接受用绘图语言编写的源程序,经语法和语义分析之后,将源程序所规定的图形显示在显示屏(或窗口)中。
目的:通过自己动手编写解释器,掌握语言翻译特别是语言识别的基本方法
- 会使用正则表达式设计简单的语法
- 会用递归下降子程序编写编译器或解释器
- 会写报告
## 2. 函数绘图语言简介
### 2.1 运算符
+
, -
, *
, /
, %
: 基本算数运算
=
: 赋值运算
==
: 判断相等
is
: 当lval
未定义类型时,将rval
的赋给lval
; 当lval
已定义类型时,判断lval
与rval
是否相等
### 2.2 基本类型
#### 2.2.1 字面量类型
string
: 字符串类型,运行时内部类型为str
e.g. "some string"
integer
: 整数类型,运行时内部类型为int
e.g. 114
float
: 浮点数类型,运行时内部类型为float
e.g. 114.514
bool
: boolean类型,运行时内部类型为bool
e.g. true
or false
range
: range类型,运行时内部类型为tuple[int, int]
e.g. 0..1919
从0
到1919
(不包含)的整数序列
- 作为
for
表达式的范围参数时会进行自动推导成generator,a..b
会被推导为range(a,b)
#### 2.2.2 内部类型
tuple
: 元组,用于函数调用参数和定义复杂结构,由,
表达式生成(在作为函数参数的时候,嵌套的元组会被自动压平成一维列表)
function
: 函数,目前只支持使用@func_import
导入python函数,暂不支持自定义函数
### 2.3 基本语法
- 注释:
;
- 标识符:
/[A-z_][A-z0-9_]*/
- 定义变量:
<varname> = <value>
or <varname> is <value>
- 基本运算符:
<lval> <op> <rval>
- 元组定义:
<tuple> = (<value1>, <value2>, ...)
- 函数调用:
<funcname>(<arg1>, <arg2>, ...)
- for循环:
for <loop_var> in <iterable> <expression>
### 2.4 变量
- 变量具有作用域,在搜索时从当前上下文向上搜索
- 变量可以由字面量或运行时导入产生,这个时候的变量是immutable,不可以修改值
- 变量可以由赋值运算符产生,这个时候的变量可以修改值
### 2.5 内置函数
解释器后端是python,可以使用@func_import
decorator在特定上下文中导入python函数
print
: 打印 print("scale is", scale)
draw
: 添加点 draw(1, 1)
draw
函数会使用当前上下文中的origin
, scale
, rot
三个变量
plot
: 绘图 plot()
sin
: 正弦函数 sin(1)
cos
: 余弦函数 cos(1)
tan
: 正切函数 tan(1)
### 2.6 绘图函数
绘图函数涉及三个全局变量:
origin
: 原点偏移量 origin is (1, 1)
rot
: 旋转角度 rot is 0.5
scale
: 横纵坐标比例 scale is (0.1, 12)
draw
函数接受x
,y
两个参数,在调用时,会对坐标进行变换操作,然后将变换后的坐标添加到散点列表中,坐标变换公式如下:
plot
函数调用时,会将散点列表使用matplotlib绘制出来,并显示窗口,阻塞当前线程
## 3. 函数绘图程序举例
origin is (1, 1); 设置原点的偏移量
rot is 0.5; -- 设置旋转角度
scale is (0.1, 12); 设置横坐标和纵坐标的比例
print("origin is", origin, ", rot is", rot, ", scale is", scale); 输出当前设置的参数
for T in 0..1000 draw(T,((sin((T*0.01))+1)+(cos((T*0.01))*cos((T*0.01))))); 从0到1000,每次增加1
plot(); 绘图
## 4. 开发环境及配置
- Operating System: Fedora Workstation 39
- Python version:3.12.0
- IDE: Visual Studio Code (Language server: Pylance)
使用的Python包:
- regex: python内置的正则表达式包
- matplotlib: 绘图包
## 5. 基本原理与解决思路
解释器运行逻辑:
- 初始化解释器上下文,加载内置函数
- 读入所有源代码文件,按
\n
拆分成多个statements,逐行分析
- 使用正则表达式循环匹配当前行,生成指令序列
- 记未分析的字符串为
expr
,初始化指令序列ops
为空
- 使用正则表达式匹配
expr
,如果匹配成功,将匹配到的内容加入ops
,并将expr
被匹配到的部分截断
- 循环直至
expr
为空或者无法匹配,抛出异常,终止解释器
- 根据指令序列生成指令图
- 初始化栈
stacks
为空,当前帧为current
,指令图optree
指向current
,即图的根节点
- 遍历指令序列
ops
- 如果当前指令为
brance_open
,将current
压入栈stacks
,创建新的帧new
,将new
加入current
,并将current
置为new
- 如果当前指令为
brance_close
,将current
中的join
操作进行解析,生成group
指令,弹出stacks
栈顶,并将current
置为弹出的栈顶元素,初步处理函数调用
- 如果当前指令不是
brance_open
或brance_close
或join
,将当前指令加入current
- 从指令图递归生成表达式
- 如果当前节点为
group
,递归生成子表达式,将子节点加入group
的子表达式参数列表
- 如果当前节点为
call
,递归生成子表达式,将子节点加入call
的子表达式参数列表
- 如果当前节点为原子表达式,直接返回
- 如果当前表达式为运算符表达式,递归生成左右子表达式,将左右子表达式加入运算符表达式的参数列表
- 如果当前表达式为for循环表达式,递归生成循环变量、迭代器、循环体,将循环变量、迭代器、循环体加入for循环表达式的参数列表
- 如果当前表达式无法匹配,抛出异常,终止解释器
- 在当前上下文中求值表达式
- 如果当前表达式为字面量,返回字面量的运行时引用
- 如果当前表达式为标识符,从当前上下文向上搜索,返回第一个匹配的变量
- 如果当前表达式为元组,递归求值子表达式,返回元组
- 如果当前表达式为函数调用,递归求值子表达式,返回函数调用结果
- 如果当前表达式为for循环,递归求值循环变量、迭代器、循环体,对每次循环修改当前
ctx
中绑定的迭代变量,并对循环体表达式求值,返回空
- 如果当前表达式无法匹配,抛出异常,终止解释器
## 6. 关键类及主要方法
### 字面量的匹配
先定义一些字面量的类型,直接写成正则:
要注意这里的正则限定了开头,但是没有限定末尾,所以一定要写好匹配的边界,不然可能会漏掉内容或者匹配到了额外的内容。然后就是写一个函数,来处理这些字面量:
这里会涉及到一个Var
类型,定义如下:
这个Var
其实就是一个wrapper,用来在运行时进行一些简单的类型检查(虽然写到后面直接开摆了),和引用在scope里面的Var
对象,并修改所绑定的值
### 标识符和关键词和运算符
这里写了好几个关键词,但是其实只用到了for
和in
,其他关键字保留备用
以及写一下各个运算符所对于的函数:
### 开始解析表达式
此处对应章节5中的3. 4. 5. 三部分
### 表达式求值
### 初始化运行时上下文和测试代码
## 7. 测试截图
主要代码已包含在上一节中,这里只展示运行结果
(自己截图)
## 8. 总体体会(遇到的问题与体会)
- 解释器运行过程中,尤其是进行语法分析和表达式求值的时候,会进行非常多递归操作,调试难度较大
- 把问题复杂化了,本次实验的语法点较少,且语法形式很单一,完全可以直接匹配特定字符串,但是为了拓展性做了很多workaround