let语言来自eopl 《Essentials of Programming Languages》,本段只实现第一个简单的let proc的语言。
eopl的书中的代码使用scheme编写,可以在github中取得。https://github.com/mwand/eopl3 就是作者之一写的,本文将第三章的一个例子转为python语言来实现。
本文的2个py代码在: https://github.com/wangdxh/eopl3-in-python let文件夹下面
python 安装pyparsing : pip install pyparsing
let语言的定义如下:
let xx = 0, bb = 24 in
if zero? ( xx ) then -(bb,3) else - (xx , 5 )
使用pyparsing解析得到:
[['let', [['xx', '=', [0]], ['bb', '=', [24]]], 'in', ['if', ['zero?', ['xx']], 'then', ['-', ['bb'], [3]], 'else', ['-', ['xx'], [5] ]]]]
let语言的pyparsing语法定义如下:
identifier = Word(alphas, alphanums+'_-?') 字符开头,包含字符,数字,下划线,减号,问号
number = Combine( Optional('-') + Word(nums)).setParseAction(lambda t:int(t[0])) 数字:整数
var_exp = Group( identifier )变量表达式,
const_exp = Group( number) 整数表达式
使用pyparsing的Group将变量和整数都封装在一个list里面,这样变量,整数,和其他的语法一样,解析出来的结果都是list,便于解释时整体处理。
expression = Forward() 表达式,先定义为Forword类型,用于递归的类型定义,先给出一个类型声明,后面给出具体的定义。
LPAREN, RPAREN, COMMA = map(Suppress, '(),') 左括号,右括号,逗号定义为Superss,这样在引用到LPAREN等变量的时候,这些符号并不出现在解析结果中,但是如果是‘(’去构建表达式的时候,还是会出现在结果中,Supress(‘,’) 则不会。
diff_exp = Group( '-' + LPAREN + expression + COMMA + expression + RPAREN)
减号语法 - (xx, 3),解析为: ['-', ['xx'], [3]] 这里的3是整数,并不是字符串,因为在number的定义中,设置了解析操作 setParseAction(lambda t:int(t[0])) 讲解析结果转换成了int。
这里注意express, 减法的定义里面包含了experssion,expression的定义里面会包含减法,let语法,if语法等多个语法,会造成递归引用,所以要用Forword
zero_exp = Group( Keyword('zero?') + LPAREN + expression + RPAREN)
判断是否为bool类型,zero?(xx) 解析为:['zero?', ['xx']]
if_exp = Group( Keyword('if') + expression + Keyword('then') + expression + Keyword('else') + expression)
if zero? ( xx ) then -(bb,3)else - (xx , 5 ) if语句定义 解析为:
['if', ['zero?', ['xx']], 'then', ['-', ['bb'], [3]], 'else', ['-', ['xx'], [5]]] 每一个expression 都解析为一个列表,除了整数外第一个字段都是表达式的名称。
let_exp = Group(Keyword('let') + Group( delimitedList( Group( identifier + '=' + expression ))) + "in" + expression)
let 语法,后面跟着一个变量定义的列表,‘in’关键字,最后的expression是执行的body,里面可以引用let后面定义的变量。 delimitedList 表示为 a + ','+ a + ',' + a 至少有一个a,后面跟则多个以逗号间隔的a,用于多个变量声明的列表。
proc_exp = Group(Keyword('proc') + LPAREN + identifier + RPAREN + expression)
定义函数的方式,let f = proc (x) -(x,1) in (f 30), 以proc开头,括号里面是定义的函数参数,目前只支持一个参数的定义,参数后面跟着一个expression,是函数体。在定义的let 和proc里面执行体的表达式暂时都只有一个,方便解析
call_exp = Group('(' + expression + expression + ')' )
调用表达式,以()开始和结尾,这里的解析结果中()是存在的,并且list的第一个字段就是 '(', 因为它没有用Supress封装。第一个expression是函数定义或者函数变量,第二个expression是函数执行的参数
let f = proc (x) -(x,1) in (f 30) 解析为:
[ ['let', [['f', '=', ['proc', 'x', ['-', ['x'], [1]]]]], 'in', ['(', ['f'], [30], ')']] ]
接下来定义
expression << (diff_exp | zero_exp | if_exp | call_exp | proc_exp | let_exp | const_exp | var_exp) << 是pyparsing的语法,表示真正地定义expression。 expression定义为前面定义的表达式中的任何一个。所以各个表达式和expression是递归地引用的。
现在一个完整的语法分析的定义就完成了,调用 expression.parseString(program)对字符串进行解析,expression的分析结果是ParseResults,这个类可以当作list来进行操作,而且所以的分析结果都是放在一个大的list里面的,我们的每条expression定义的时候都用Group封装了,所以每个expression又会独立地在有个list内。
解释执行:
let f = proc (x) -(x,1) in (f 30) 解析为:
[ ['let', [['f', '=', ['proc', 'x', ['-', ['x'], [1]]]]], 'in', ['(', ['f'], [30], ')']] ]
def value_of_program(program):
return value_of(expression.parseString(program)[0], init_env()) parseString解析结果的第一个list字段,就是我们的expression的解析结果,这个结果也是一个list,第一个字段,就是各个表达式的第一个字段,整数的第一个字段,就解析为整数,变量的第一个字段就是变量的字符串,整数和变量所在的list也只有一个字段。
我们引入了一个环境变量的概念,用户扩展 let语法中定义的 变量声明,当有let xx = 0, bb=1 的时候,会向环境中扩展一个xx 和 bb的定义,然后去执行let中body的定义,body中如果有引用到xx或者bb,就去环境中查找它的值,或者为整数,或者为一个函数。 环境的扩展时完成闭包的一个重要的环节,这里环境是按照变量的定义动态扩展的。当用let将函数定义为变量的时候,解释执行时会生成一个闭包的python函数,这个函数会和 变量绑定,扩展到环境中。
因为有些let语法中的函数的字符在python中是不支持的,比如?,-,所以我们使用一个dict去对应 let语法中的 某个要执行的expression的名称和 python中的某个实际函数。使用修饰符定义,在文件enviroments.py中定义:函数dict,这个symboletofun修饰符将修饰符后面带着的字符串参数和要修饰的函数体,在funcdict中进行dict对应。
在pylet.py中调用valueof 执行expression表达式的时候:根据expression的表达式的解析结果list的第一个字段,判断如果字段类型是int,说明他是一个number常量,它的valueof值就是 int自身;字段的其他类型就是字符串了,‘let’,‘if’,‘(’,‘zero?’ , 'xx',等等,根据它的字符串去函数列表中去查找对应的处理函数,每一个expression我们都有一个对应的函数处理,如果找不到,说明这个字符串是一个变量定义,那么变量定义的valueof 就是去环境中查找变量名称对应的值,或者为int值,或者为python的函数值。查找环境中的符号对应的值,是apply_env,扩展环境中的符号对应值是extend_env函数。
首先来看 let的valueof
将 let的解析结果list,赋值给4个变量,name就是‘let’,然后遍历变量声明list,将变量对应的表达式的值计算之后,和 变量的符号串如,‘xx’ 增加到环境中,extend_env会在原来环境的基础上创建一个新的环境,并不影响原来的环境值,只有在执行本次let的body时,使用新定义的变量环境,退出本次let的环境之后,原来的环境中并不包含本次let中的变量声明。而且环境也使用list实现,当出现变量名称重复之后,会使用最后插入的变量值。 let语法的执行结果就是它的body的执行结果,body执行时,环境中增加了let语法中声明的变量的信息。
减号,zero?,if的执行都比较类似,如下,
接下来是非常重要的函数定义和调用了
函数定义如下 :当碰到proc时,对这条expression的解释,会生成一个python的函数体的值,这样对于函数调用的(),或者 变量的环境扩展都很方便。proc过程会通过proceduer的函数,将其body和env闭包地保存,返回一个只带函数参数的新的python函数体,这个函数体里面用到时创建proc 时的body 和 环境的值。例如 let f = proc (x) -(x,1) in (f 30) 执行时,proc_func 会为proc生成一个 函数体,就是procedure返回的python函数,然后let的变量声明表达式执行时,会将这个 python函数和 ‘f’,对应 扩展到环境中,在in后面的 (f,30),‘f’的valueof 执行时,会查找到当时保留的python函数体。
函数的调用,(f,30)或者(( proc(x) -(x,1) 30)),第一个expression的valueof 必然是一个python函数体,然后将第二个expression的值计算出来,作为参数,执行。
测试执行:
执行这些字符串查看结果:
''' let xx = 0, bb = 24 in if zero? ( xx ) then -(bb,3) else - (xx , 5 ) '''
let f = proc (x) -(x,1) in (f 30)
(proc(f)(f 30) proc(x)-(x,1))
((proc (x) proc (y) -(x,y) 5) 6)
let f = proc(x) proc (y) -(x,y) in ((f -(10,5)) 6)
最后来一个eopl代码中的例子执行:
zero? 的解释执行竟然计算反了,没有加not,已更新。
作者:小王