目录
代码仓库:whoiscc/shattuck。在我写「欢迎围观」之前都不会有什么观赏价值,纯粹是为了证明我并不是在纸上谈兵。
这些年来看过、跟着做过的「3分钟发明一门编程语言!」的教程数不胜数。它们中的绝大多数都有一个共同点:从前往后。最开始几乎一定是一段展示最终效果的代码块;接下来是词法分析、语法分析,然后对语法树进行eval,全文终。你自然可以嘲笑我看的教程太不入流,但我还是想为这些「不入流」的教程再添上这么一篇。这篇教程(更准确的说,日记)的目的不只是向读者展示编写一个脚本语言的解释器需要哪些部件,而且还希望将更多的精力投入到设计语言特性的方面。甚至可以说,代码并不是这个项目的主体,而仅仅是验证思路的工具。
综上,我决定以如下的方式开展这个项目:
- 以从后往前的顺序实现解释器。如果没有足够的时间,传统意义上的「前端」甚至可以完全省略,就当做是一个只能写native拓展的Python吧
- 设计思路大体上参考(我所了解的)Python,除了两个尽可能完成的小目标
- 少用一些哈希表
- 不需要GIL
如果最后没有完成,那么也一定要弄清楚困难在哪里,算是可以加深对Python语言设计的理解的一种方式。
整个项目计划用Rust完成。这也是这个项目隐藏在表面之下的真实目的:学习和巩固Rust。如果这个目的提前达到了那么本项目即刻宣告烂尾(。如果Rust真的可以作为C/C++的无缝替换者的话,作为脚本语言的运行时应该属于它可以优雅完成的任务,这个项目算是一个粗浅的验证。
作为废话的最后一部分,shattuck的名字来自于我目前所住的公寓面向的大街:

还有最后的两周了。再见,Berkeley。
在Python关于其内部实现的文档中,写在最开头的话包括了「一切存在都是对象」这一句。这里的「对象」和面向对象编程的对象没有什么关系,仅仅是脚本语言用来管理内存和功能的最小单位。在Shattuck中,用户可以通过代码以及native拓展创建的一切也都是对象。这个对象不可以是struct,因为它可能包含任何东西,代表整型的对象需要包含i64
,代表文件的对象需要包含文件描述符,等等。因此在Rust中它是一个trait。
这个trait具有哪些方法呢?思来想去,我觉得最重要的,还是「属性」:
pub trait Object: Debug {
fn get_property(&self, key: &String) -> Option<Addr>;
fn set_property(&mut self, key: &String, new_prop: Addr);
}
比如说一个实现了Object
trait的整型对象struct ,Shattuck无法直接看到和接触到它包含有i64
的字段,但是可以通过访问它的属性,比如string
,来得到这个整数的格式化字符串。当然对于这个字符串对象我们还是不能接触到它内部包含的String
类型的Rust字段,但是也许可以通过某个属性把它打印在屏幕上。目前我们暂时要求实现Object
的类型一定要实现Debug
,就是因为在目前欠缺基础设施的情况下想要获取一个对象的内部信息实在太困难了,所以通过显示出来的方式对付一下。
那么方法呢?我要怎么调用一个对象上绑定的方法呢?
方法本身可以作为对象的一个属性存在,这样做的好处就是我们可以随意的将一个对象的方法绑定在任意的名字上。那么接下来,如何允许方法被调用呢?我在下一篇文章之前会解决好这个问题。
获取和设置属性并没有直接返回和传入类似于Box<dyn Object>
类型的值,而是通过一个叫做Addr
的类型。这在接下来讲Memory
struct的时候会进行说明。尝试获取一个不存在的属性会返回None
。在我看来这是这个方法唯一失败的原因(并且设置属性的方法永远都不应该失败),因此并不需要引入Result
类型的返回值以及对应的Error
类型,这太复杂了。
作为一门脚本语言,垃圾收集自然是必不可少的。Rust标准库中提供了自动引用计数Arc
类型,着实让我眼馋了一会然后不得不放弃。如果通过Arc
,使得每个对象直接保存其他对象的引用的话,这些引用就只能都是只读引用了——总不能要求一个对象最多只能被另外一个对象引用(仔细想了想好像也不是不能,但那实在太难理解了)。因此,我们还是需要一个内存池结构集中化地存储所有的对象。这样做的好处还有,我们可以更加自由地选择垃圾收集的算法,而不受限于引用计数。事实上,我最终所实现的第一版垃圾收集,是一个介于copy和mark-sweep之间的奇怪算法。
目前项目中包含的演示用main.rs
内容如下:
extern crate shattuck;
use shattuck::core::memory::Memory;
use shattuck::objects::{DerivedObject, IntObject};
fn main() {
let mut mem = Memory::with_max_object_count(3);
// yukari = new DerivedObject()
let yukari = mem.append_object(Box::new(DerivedObject::new())).unwrap();
// age = new IntObject(18)
let age = mem.append_object(Box::new(IntObject(18))).unwrap();
// yukari.age = age
let key = "age".to_string();
mem.set_object_property(yukari, &key, age);
// print(yukari.age)
let age_prop = mem.get_object_property(yukari, &key);
println!("{:?}", age_prop);
println!("{:?}", mem.get_object(age_prop.unwrap()));
// marisa = new DerivedObject()
let marisa = mem.append_object(Box::new(DerivedObject::new())).unwrap();
mem.set_root(marisa);
// correct_age = new IntObject(4294967296)
let correct_age = mem.append_object(Box::new(IntObject(4294967296))).unwrap();
println!("{:?}", mem.set_object_property(yukari, &key, correct_age));
}
运行结果是
Some(Addr(1))
Some(IntObject(18))
<shattuck> garbage collected, 1 alive, 2 dead
None
由于mem
中最多只允许3个对象存在,因此在我们尝试添加第4个对象correct_age
时触发了垃圾收集,所有没有被根对象marisa
引用的对象都被释放。所以说,这种事是做不得的……
最值得注意的是
mem.set_object_property(yukari, &key, age);
它是不能被写成
mem.get_object(yukari).set_property(&key, age);
的,因为get_object
返回的是只读引用。事实上后期会把get_object
方法藏起来,所有对属性的操作都要通过mem
完成。
这篇文章原本计划对Memory
类型 进行简单地描述以后结束,但是看起来已经有点太长了。祝愿各位阅读愉快。
目录