使用pyparsing 实现一个let语言的解释器

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,已更新。

作者:小王

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容