解析SICP-Proj4 EC

本文章请搭配NJU-SICP Project04 EC (Adopted from CS61A from Berkeley) 食用
Written By SweetGargamel
Debugging Guide - proj04: Scheme
原问题
Why current interpreter cannot properly process tail call?
Consider such scheme code:
|
|
The current evaluation process is:
|
|
The problem is python interpreter does not properly perform tail call.
There’re mainly three way to solve this problem:
- transform the scheme interpreter to an non-recursive interpreter, i.e., eliminate all recursive call to
scheme_eval. That can be accomplished by- rewrite this interpreter to an CEK interpreter. (‘C’ means control, ‘E’ means environment, ‘K’ means continuation). See here and Chap. 5 of ESSENTIALS OF PROGRAMMING LANGUAGES.
- rewrite this interpreter to an trampolined interpreter. We use this approach.
- transform the source scheme code, e.g. Continuation-Passing Style transformation.
The trampoline technique is the simplest one. So we use this approach.
Trampoline
The trampoline technique is a simple technique to perform proper tail call in a “bad” language (e.g. Python, C, C++, Java, …).
For example, we can define a sum recursive function in python:
|
|
And sum(10000, 0) will produce a StackOverflow error. Now we “trampoline” such function:
|
|
Now sum(10000, 0) will properly works. This can be generalized to transform all recursive functions to loops, see here. But the above form is enough for our problem.
The Tasks
Complete the function optimize_tail_calls in scheme_eval_apply.py. It returns an alternative to scheme_eval that is properly tail recursive. That is, the interpreter will allow an unbounded number of active tail calls in constant space. It has a third argument tail that indicates whether the expression to be evaluated is in a tail context.
The Unevaluated class represents an expression that needs to be evaluated in an environment. When optimized_eval receives a non-atomic expression in a tail context, it returns an Unevaluated instance. Otherwise, it should repeatedly call original_scheme_eval until the result is a value, rather than an Unevaluated.
A successful implementation will require changes to several other functions, including some functions that we provided for you. All expressions throughout your interpreter that are in a tail context should be evaluated by calling scheme_eval with True as the third argument (now called tail). Your goal is to determine which expressions are in a tail context throughout your code and change calls to scheme_eval as needed.
Once you finish, uncomment the following line in scheme_eval_apply.py to use your implementation:
scheme_eval = optimize_tail_calls(scheme_eval)
疑惑引出
问题
|
|
- 是的,很显然你会不会觉得,开始在运行第一行代码的时候,如果是尾递归的话就会直接返回一个
Unevaluated的玩意,这程序不就该崩了吗?
答案
|
|
- 是的你看了答案会不会更觉得下面的代码根本就运行不到?
- (当然除了该这些代码还应该按照助教的建议,把凡是尾递归的部分都给加一个
tail=True的参数)
使用Vscode的Debug功能解析
怎么使用Debug
-
直接按照助教的提示修改
.vscode/launch.json文件即可 -

|
|
- 你可以修改
tests.scm里面的测试用例来测试,下面是我的例子 
|
|
-
当然别忘打断点了
-

Debug,启动!
-
点击
Start Debugging -

-
然后你会发现左边出来这些东西,分别是你的
VARIABLES、WATCH、CALL_STACKVARIABLES是当前的变量WATCH可以添加自己想看的表达式或者变量的值CALL_STACK看调用的堆栈

我们发现他停到了if这个语句停下来了
现在解释器在干啥?
- 我们发现左上角的
expr的第一个sym是define,说明他正在进行do_define_form
怎么让解释器往下走?
在最上面有几个按钮,分别是下面几个

Step Into如果当前语句是一个call expression,他就会进入函数体让你看函数体内部是怎么执行的;否则和Step Over一样。Step Over:不管是否是call expression直接当一条普通语句运行过去。Step Out:跳出当前Frame(比如A里面调用了B函数,我现在通过Step Into进入了B所在的Frame,Step Out就直接回到A里面)
所以我们按Step Into,你会发现他跳过了上面的if语句;这是因为我们现在是在do_define_form,并不是尾递归操作,所以就调用原始的eval_call.

- 我们再次选择
Step Into,发现:
这个部分就比较好理解了,他就是在做define的内容,我就不解释了。
做完define form之后
- 按照解释器解释的规则,我们把
factor绑定到对应的lambda函数之后,就会执行(factor 10 1)
那么他的的执行顺序就是(请你注意看左下角的CALL_STACK)
eval factor
eval 10
eval 1图略,类似上面apply lambda 10 1
我们发现这些都不是尾递归;
在这一步之后选择Step Into
进入apply过程!
-
你会发现apply后,他就在
do_if_form。注意,在调用do_if_form的时候参数tail=True
-
这里继续选择
Step into -
然后就到了最核心的部分,这里终于会有
Unevaluated类产生了(下面都简写为un类)
-
这里你要思考,我的
un类返回到哪里了?你点一下Step Into,发现这个东西是返回到了scheme_eval里面。 -
那么他是在
eval谁?你看左边的Variables,这会正在做do_define_form。
-
然后你就会发现他回到了最外层的
apply的过程

-
诶你是不是发现,这里上面虽然最左边的
tail是false,但是这里返回了un类。
思路理顺
通过上面的分析,我们发现,实际上un类并不会”漏出去“,实际上他是通过好层的递归调用,在最外层的eval_call(eval的是函数的call-expression)拿到了最里层eval_call(这里是do_if_form)返回的un类,然后再循环往复的eval。
反思总结
- 我和xsh和fhy同学都交流过,他们似乎都感觉最后这个
un类会被返回到最外层,从而你最后的eval结果都是一个un类。说明这应该是许多同学的共同的问题 - 然而通过
Debug的逐句调用,我们发现靠空想是不行的,需要深入的一行行把自己当编译器理解。Debug应该是我们要掌握的一门技术。
最后再次感谢助教、各位同学的帮助!