4. 设计与实现
4.1. 词法分析和语法分析
samoyed使用lark的lair语法分析器解析代码。
lark 是一个现代的python解析库,lark可以解析任何上下文无关文法。
使用lark一方面是避免重复造轮子,减少工作量;直接使用EBNF描述文法能更灵活地拓展DSL的文法。
关于详细的EBNF文法,可以参考文档
4.2. core.Interpreter
解释器是项目的核心。
4.2.1. 初始化
解释器初始化时,如果外部传入的是代码,会使用解析器解析得到语法树。
然后,解释器会建立一个上下文。上下文中包括一些内置的函数、外部传入的变量和解释器状态的信息。
建立完上下文后,扫描一遍生成好的语法树,并确认所有的state和入口state(main)。对于外部的语句,解释器会直接将其执行。
4.2.2. 执行语句
可执行的语句有以下几种:
simple_stmt简单的语句,包括:赋值
跳转表达式
pass表达式
普通表达式
if_stmt
if语句
match_stmt包括普通的match和可以控制输入时间的match
4.2.2.1. simple_stmt
对于simple_stmt,其语法分析结果类似下面:
simple_stmt
|
expr
|
...
其中的赋值语句也类似,只是有两边:
assign_expr
|
┌───┬─────┐
var = expr
因此只需要判断一下类型,然后调用计算表达式即可。
4.2.2.2. if_stmt
if_stmt
|
┌───────┬─────────┐
expr true_st false_st
首先计算expr的值,然后根据结果去执行true_st或者false_st。这里迭代块中的语句,递归调用执行即可。
4.2.2.3. match_stmt
match有两种形式
第一种形式是带有时间控制的匹配 其中,@的第一个参数是超时时间,必选;第二个参数是最少持续时间,可选; @后应该接入一个函数调用。延时表达式会不断调用这个函数
match @(10,2)funcall :
compare_expr =>
stat
...
silence =>
stat
这种形式下,语法树大概是这样的:
match_stmt
|
┌─────────────────────────────────┬────────────┬─────────────┐
expr0 match_case1 match_case.. silence
| | |
┌────────┬─────────┐ ┌────┬──────┐ ┌────┐
at_para func func_para expr1 stmt1 ... stmt1 ...
这种形式下,假定func是会一直产生值的。(当然,即使没产生也有响应机制防止其无限阻塞)
这里会开启一个循环,每当func有新的值产生,就会进行一轮判断。如果有匹配,就执行值并结束匹配。
这里支持子串匹配和正则表达式匹配。可以见语法参考。其具体实现只是多加了一层类型判断,并对对应类型比较和匹配。
第二种形式是普通的多值匹配
match expr :
compare_expr =>
stat
compare_expr =>
stat
default =>
stat
这个顺序比较即可,也不再赘述。
4.2.3. 计算表达式
表达式是一个树状的结构,这里和执行语句类似,采用的是递归式的方法。
如果传入的是一个终结符(叶子节点),那么提取出值返回即可。这里比较特殊的是变量。如果是一个变量则需要查询上下文。
如果传入的是一些常量,直接返回即可(为什么会有常量呢,因为语法分析的时候还做了简单的语法制导,一些叶子节点被直接压缩成值)
如果传入的是一棵树。
判断它的类型,如果是简单的二元运算或者一元运算,例如:
三目表达式
逻辑非
比较运算
正负号
那么直接操作即可。
如果涉及到多个符号的,例如:
逻辑与
逻辑或
四则运算
那么使用reduce函数进行运算。
这里四则运算使用的reduce和一般的reduce不太一样,它是形如下面的结构的:
[1,+,2,-,3]或者[1,*,2,//,3]
一种比较函数式的想法是用交换x,y的方式:
def reduce(x, y):
if callable(x):
return x(y)
else:
return partial(y, x)
这里没有采用这种形式,而是用另一种比较易懂的形式:
for i in range(1, n, 2):
"""
从1开始,步长为2,每次应该都要取到函数
"""
op = l[i]
operand2 = l[i + 1]
sum = op(sum, operand2)
4.3. libs.TimeControl
这个类用于控制超时和最早允许退出的时间。
这个类可以产生生成器实例,它的基本用法如下:
t = TimeControl(input,5)
for i in t():
# i 是每次获取的结果
# 判断i....
if 判断成功:
t.cancel() # 关掉两个计时器
break
其中t是一个生成器函数。
它主要有三个工具成员
max_wait_timermin_wait_timerwatch_dog
前两者主要用于监控最大和最小时间,而第三者用于中断传入的函数。因为传入的函数可能因为阻塞永远无法唤醒。
4.3.1. watch_dog
watch_dog是一个装饰器。采用的内部信号唤醒的机制:
def _handle_timeout(signum, frame):
raise SamoyedTimeout(f"Timeout for function '{func.__name__}'")
signal.signal(signal.SIGALRM, _handle_timeout)
signal.setitimer(signal.ITIMER_REAL, seconds)
do_something...
# 如果超时了,会调用_handle_timeout,继而抛出一个异常
利用这个机制,我们将超时的程序中断。
4.3.2. timer
timer是一个普通的计时线程。当超过一个时间时,它会调用构造时传入的函数。利用这个机制,我们可以知道什么时候超时了。
为什么需要两个超时?
主要是一开始使用的是线程计时,后面发现信号计时的效果似乎也不错。信号计时可能会造成多级异常,因此这里没有采用两个信号计时
4.3.3. 函数体
外层的try只用于处理
KeyboardInterrupt,无其他用途每次取完数据,判断是否已经超时;超时则退出
time.sleep防止空转。
# 外层的try只用于处理KeyboardInterrupt,无其他用途
try:
while True:
# 尝试执行函数
result = None
try:
result = timeout_on_interval(*args, **kwargs)
except SamoyedTimeout:
# 如果超时,返回None
yield None
except EOFError:
yield result
except Exception as e:
raise SamoyedRuntimeError(str(e))
else:
# 否则,返回结果
yield result
# 如果函数执行时间已经超时,那么退出
if self.timeout.is_set():
self.cancel()
return
# 睡眠,防止频繁运行
time.sleep(self.sleep_interval)
except KeyboardInterrupt:
self.cancel()
raise KeyboardInterrupt