参考文章: http://zh.lucida.me/blog/how-to-implement-an-interpreter-in-csharp/
该代码还有一些bug,这篇文章主要是记录解释器的实现过程。详情一定参考上面的文章,这篇文章主要给你C/C++实现提供一种思路。
bug包括但不限于(好怂呀 (ノ`Д)ノ):
- iScheme的解释器对iScheme所有定义的变量,常量(字面量和具名量)都没有考虑析构(delete) 这对于大程序应该是致命的。
- 没有完善的错误处理机制。
- 语言特性方面有欠缺,具体看我参考的文章。
其他我发现的bug已经解决了,递归也可以了。但是测试Scheme时请一定仔细检查你的小括号(技巧:左括号马上跟你想判断的操作),太闹心了!!!
编程环境是: CLion2019.3
,Win10
,MinGW
。
如果需要使用交互界面(类似于python那个),不推荐在Clion的命令行直接运行,格式打印有问题,不好看。
首先直接用replaceAddWhite
函数给(
)
添加空格。
然后getTokens
分割得到token(自我感觉比C#的库函数快一点,可能吧 /W\ )
首先定义每个结点的类型SExpression
。
然后函数 parseAsIScheme
parse的时候和上面参考文章是一样的。非常巧妙的遇到(
下一层,遇到)
上一层。
作用域可以参考这篇文章:https://www.jianshu.com/p/9536b75181d5
每进入一个函数域,或者是每次声明局部变量,都会新建一个符号表, 作为当前的计算环境,并存储上一个环境的地址。 在需要查找符号表的时候,先在当前环境查找,找不到再到前一个环境中查找。 每个域都只保存自己的环境地址,所以不会影响到其它域的使用。
而新建作用域应该是只有初始化,def
定义,func
定义才可以用。
这里的作用域直接用的SScope
,包含一个用于向前查找的指针和一张变量表,map应该是红黑树,效率杠杠滴。
变量表是用std::map<std::string, void *> variableTable;
定义的。
我的打算是指针无敌,指哪打哪,速度快 ( ̄︶ ̄)↗ ,就是看着头疼,而且危险。
我直接用的C++的int保存iScheme的数值和Bool型,
将std::vector<void *> vec;
封装为一个结构体表示iScheme的列表,方便添加功能。(指针当元素,有种python的感觉)
函数则是用SFunction
结构体实现的,保存函数体指针、参数符号(形参的string
)、作用域指针。
这这两步我放一起了(其实是不知道怎么分开),核心函数evaluate
。
没有将这一函数设置为SExpression
的方法,不想太面向对象,耽误了效率(好吧,其实面向对象我只能叫入门)
还有就是evaluate
里面好多跳转语句呀,怎么提高程序效率呀!!!
叶节点就是字面量和具名量了,直接判断child
是否为空,为空后再判断是否为digits
。
如果是数字则返回指针(原来这就是一切皆对象呀!解释程序太低效率了吧!),每次都得去内存里面取,而不是直接编译后传给寄存器。
如果是变量则通过作用域查找变量的值(这个值是通过scope->define
函数定义的,在iScheme里面写def
就会调用)。
这三类操作不费太多精力,写好一个,复制粘贴一把梭。( •̀ ω •́ )y
特点就是+,-,*,/,and,or,not都是多操作数的,直接遍历就好。这些操作的结果也是直接new的,内存访问,而且没有delete /W\
iScheme 的列表操作包括 first
, rest
, empty?
和 append
,我只实现了前三个,头晕
当定义list
的时候,直接计算参数后插入,注意first和rest都是深拷贝的。
def操作会给当前的作用域添加一层,并在新层添加def定义的变量
if操作会根据condition的结果(实质是化为int存储在内存里面的1或者0),来选择
begin操作会顺序执行,并将最后执行的结果返回。
这个操作和SFunction
密切相关。定义函数位于else if (str == "func")
分支,调用函数位于else
分支。
仔细想想,Scheme的**应该是函数和变量统一的(我刚学了半小时 (lll¬ω¬) ),它们都是Scheme的使用者自己定义的,不能提前写好分支。
但是调用(使用)变量一定是叶节点,它就可以放在叶节点的else里面,调用函数一定不是叶节点(是不是呢?),它就放在非叶节点的else里面。
而它们的定义def
func
则直接在非叶节点里面定义好分支了。(再次问下,怎么做才能高效率访问呀?)
回过头来,func
操作就是分别赋值函数体、参数(形参)、新的作用域(防止调用函数添加变量表,影响到原先的作用域)。
然后是函数调用操作,位于else分支,分为非具名函数调用如:((func (x) (* x x)) 3)
,具名函数调用如:(square 3)
。
计算实参的值后,传入arguments
。然后根据arguments
,直接设置变量表。并递归计算函数体和新的作用域。