目錄

項目地址:whoiscc/shattuck。目前的master分支已經更新到把對象看作方法進行調用了(一把辛酸淚),這篇文章對應的歷史版本是0e1400。

這篇文章會比前兩篇短一些,因為這部分代碼寫的比較順利。

在實現了掌握所有對象生命週期的Memory以後,我決定接下來實現一個「中度」封裝的解釋器Interp。說它是「中度」因為它的封裝可以完成隱藏Memory的存在,但是依然提供足夠精細度的各種底層操作。Interp的重要意義有兩個:

  • 作為代表運行時「狀態」的結構。舉個例子,理想條件下,在程序運行到一半的時候停下來,只要把Interp類型的實例序列化下來,之後就可以用來無痛恢復現場。
  • 作為Native拓展與運行時交互的代理。

在此之前,我們有了基本的對象抽象,有了自動化的內存管理,我們還缺什麼呢?沒錯,就是標題裏那倆貨。

當然,我們不缺它們倆就是了。

看看這一版的main.rs吧,已經開始讓人有一點在寫彙編的感覺了。

//

extern crate shattuck;
use shattuck::core::interp::Interp;
use shattuck::objects::{DerivedObject, IntObject};

fn main() {
let context = Box::new(DerivedObject::new());
let mut interp = Interp::new(context, 128);
interp.push_frame();
interp.push_env();
// let laptop = new DerivedObject()
// 1. <t1> = new DerivedObject()
let t1 = interp.append_object(Box::new(DerivedObject::new())).unwrap();
// 2. let laptop = <t1>
interp.insert_name(t1, "laptop");

// laptop.size = new IntObject(13)
// 1. <t2> = new IntObject(13)
let t2 = interp.append_object(Box::new(IntObject(13))).unwrap();
// 2. <t1>.size = <t2>
interp.set_property(t1, "size", t2);

// this.laptop = laptop
// 1. <t3> = this
let t3 = interp.context();
// 2. <t3>.laptop = <t1>
interp.set_property(t3, "laptop", t1);

// laptop.size = new IntObject(15)
// 1. <t4> = new IntObject(15)
let t4 = interp.append_object(Box::new(IntObject(15))).unwrap();
// 2. <t1>.size = <t4>
interp.set_property(t1, "size", t4);

// print(this.laptop.size)
let t5 = interp.context(); // unnecessary
let t6 = interp.get_property(t5, "laptop").unwrap(); // unnecessary
let t7 = interp.get_property(t6, "size").unwrap(); // unnecessary
println!("{:?}", interp.get_object::<IntObject>(t7));

// IntObject(13) should be collected
interp.garbage_collect();
}

這裡除了棧幀frame和作用域env以外,還有一個重要概念上下文context。這是我從Ruby借鑒來的思想:

無論什麼時候,this永遠指向某個對象。

那麼調用方法的過程估計就是,先調用set_context方法把上下文對象切換成方法所綁定的對象,然後進入方法中開始執行(無論是native方法還是解釋位元組碼),執行完成後恢復原來的上下文對象。在方法中,無時無刻都可以通過調用context方法獲取當前上下文。上面這段代碼中,最初傳入Interp 的第2個參數顯然是最大允許對象個數,而第1個參數就是最初的上下文對象,這樣就可以保證無論什麼時候永遠都存在一個上下文。目前它還被用作內存管理的根對象,待我以後謹慎思考後妥善處理之。

那麼接下來,就是關於frameenv的說明瞭。


棧幀的作用是記錄程序中「有來有回」的跳轉歷史。比如說程序的入口是main函數,在main中調用a,在a中調用b,那麼我們在執行b中代碼的時候就要始終記得,我們是從a中的哪一行代碼跳過來的,同時也要記得我們是從main中的哪一行代碼跳進a的。(確切的說,是「哪一個調用」。)顯然,棧幀的作用決定了它是一個堆棧結構。

那麼,有沒有辦法不用棧幀呢?

方法很簡單,所有的跳轉都是「有來無回」的就行了。巧的是我研究了很多年怎麼用CPS做到這一點,不過那實在是和我們的藍圖Python相去甚遠。而且不管怎麼樣,我們還是希望在程序出錯時,能夠給用戶提供一個調用棧來方便用戶調試bug,而遍歷一遍出錯時的棧幀恰好就可以做到這一點。

因此,在代碼一行一行順序執行時,我們不需要操作棧幀;當代碼遇到條件分支時,我們也不需要操作棧幀,因為我們不會回來了;只要當代碼遇到方法調用之類的語義時,我們需要在進入方法前先調用push_frame創建一層新的棧幀,在方法結束後再調用pop_frame將其清除掉。

棧幀的另外一個作用是存放局部變數,或者說「局部對象」,因為Shattuck和Rust一樣沒有變數的概念(應該是和Python也一樣)。這也是非常人性化的一項功能,當你調用完一個方法回到原處時,你應該會希望自己在調用前進行到一半的工作不要丟失。因此,在每一層棧幀中,還會包含一個表格,用來存放每一個「名字」所對應的對象。Python(從原理上)就是這樣做的。

但是Shattuck的做法略有不同。Shattuck加入了Python中沒有的「塊作用域」概念。在Python當中一個函數內的所有代碼共享所有的名字,我也正是因為遇到太多次像下面這樣的失誤才對這個特性深惡痛絕:

for i, girlfriend in enumerate(my_girlfriends):
for minute in range(180):
# 此處省略100行不可描述
# ……然後就把外層循環變數也叫`girlfriend`這事給忘了
if i >= 5:
girlfriend = my_girlfriends[random_below(i)]
girlfriend.break_up()

所以說,我從來就不能擁有超過5個女朋友,否則從第1分鐘起就會崩潰。(

因此,我決定重新引入C系語言中大家熟知的塊作用域語義。在棧幀中存放一個由作用域組成的堆棧,將對象的名字查找表放入作用域中,在查找名字時,由內向外逐個查找作用域,返回最裡層的名字。

這裡出現了一個比較重要的設計決策:我為作用域結構體Env 實現了Objecttrait,並且將Env都存入Memory中,與普通的對象一起參與生命週期管理。並且,對Env讀寫屬性的操作被映射到查找和更新名字查找表的操作,始終通過Memoryset_object_property 來更新Env的內容。這樣一來,我們就可以「借用」已有的內存管理系統來管理局部變數的生命週期了,這一點比較典型的例子是Lua,它直接把作用域對象暴露給了Lua代碼。舉個例子,假如有這樣一段代碼:

let a = 42
let b = "cowsay"
let a = 114514

那麼如果在創建第3行的整型對象是觸發了垃圾回收,第1行的整型對象就會被回收而第2行的字元串對象則不會,這正是由於作用域對象對它們的引用導致的。

此外,當今後加入閉包語義時,只需要閉包在創建時引用它所在的作用域(同時確保每一層作用域引用它的上層作用域),就可以使閉包可能用到的變數在閉包還活著的時候都不會死。這樣做不如顯式的捕獲列表那樣「精準」,不過實現起來要簡單得多。

最後我遇到了一個不起眼的小問題:誰來引用每一層棧幀的最內層作用域?因為我並沒有把棧幀實現成對象,所以不能通過棧幀鏈條把它們都盤活。一個也許可行的想法是由當前上下文對象來引用,不過還是需要仔細考慮一下。

所以說,我說的什麼「這篇文章會短一些」,永遠都不要信……

目錄

推薦閱讀:

相關文章