字節碼解釋執行

字節碼的解釋執行和AST的解釋執行有類似之處,而且更簡單,因爲樹形結構已經展開成順序了,以棧虛擬機爲例,爲方便起見,假設所有的指令都在一個指令數組裏,每個元素是一個指令對象,有code和arg兩個屬性,解釋器入口: 
Object execute(Inst[] inst_list, Object[] func_arg); 
由於continue和break已經被jmp指令代替了,這裏我們認爲execute的執行對應於源語言的一個函數調用,因此返回值就是源語言函數調用的返回,我們可以用null(或一個特殊的對象)來表示異常,func_arg表示對應函數的輸入參數 

既然是棧虛擬機,就需要一個棧,假設用Stack對象,pop和push兩個操作,代碼依然用動態類型形式,類型統一爲Object: 
int idx = 0; 
Stack<Object> stk; 
for (;;) 
{ 
    Inst inst = inst_list[idx]; //如果這裏越界,肯定是編譯器bug 
    ++ idx; 
    switch(inst.code) 
    { 
        case CODE_LOAD: 
        { 
            stk.push(env.get(inst.arg)); //env是運行時環境的抽象 
            continue; 
        } 
        case CODE_STORE: 
        { 
            env.set(inst.arg, stk.pop()); 
            continue; 
        } 
        ... 
        case CODE_ADD: 
        { 
            Object b = stk.pop(); 
            Object a = stk.pop(); 
            stk.push(a.add(b)); //其實這是不好的 
            continue; 
        } 
        ... 
        case CODE_DIV: 
        { 
            Object b = stk.pop(); 
            Object a = stk.pop(); 
            Object c = a.div(b); 
            if (c == null) 
            { 
                return null; 
            } 
            stk.push(c); 
        } 
        ... 
        case CODE_JMP: //假設採用絕對地址跳轉 
        { 
            idx = inst.arg; //假設arg是個int類型,否則根據實際情況轉換 
            continue; 
        } 
        ... 
        case CODE_CALL: 
        { 
            Object[] func_arg = new Object[inst.arg]; //call ARG_COUNT這種形式 
            for (int i = inst.arg - 1; i >= 0; -- i) //彈出參數 
            { 
                func_arg[i] = stk.pop(); 
            } 
            Object func = stk.pop(); 
            Object ret = execute(func.inst_list, func_arg); //遞歸調用解釋器解釋要call的函數 
            if (ret == null) 
            { 
                //異常了 
                return null; 
            } 
            stk.push(ret); //ret是函數調用的運算結果,壓棧繼續計算 
            continue; 
        } 
        ... 
        case CODE_RETURN: 
        { 
            //棧頂是需要返回的值 
            return stk.pop(); 
        } 
        ... 
        default: 
        { 
            //字節碼非法,嚴重錯誤,直接退出整個解釋器 
            show_fatal_error("Invalid instruction"); 
            exit(1); 
        } 
    } 
//這裏不需要返回值,因爲上面是死循環 
} 

上面這個大結構已經能表示幾乎所有機制了,像pop_jmp_if_false之類的代碼就沒有列出來,但很容易也能想到怎麼實現的,單個字節碼的操作都是非常簡單的 

需要注意上面的ADD和DIV兩個過程,ADD那個實際是錯的實現,DIV的是對的,因爲動態類型語言的運算可能是會有異常的,當然就僞代碼來說,ADD也表述清楚意思了,DIV判斷了div過程是否有異常,比如整數對象的div過程的實現可能是: 
Object div(Object x) 
{ 
    if (!(x instanceof IntObj)) 
    { 
        env.set_exception("Int div by non-Int"); 
        return null; 
    } 
    IntObj i = (IntObj)x; 
    if (i.is_zero()) 
    { 
        //除數爲0異常 
        env.set_exception("Zero division"); 
        return null; 
    } 
    return new IntObj(this.value / i.value); 
} 

函數調用方便起見直接使用瞭解釋器本身實現語言的遞歸,也可以實現爲先壓棧再jmp代碼,這樣只需要一個execute就可以執行一個很複雜的程序,當然需要實現數據棧的壓棧操作,就像彙編一樣,兩種做法差別不算很大,後面只討論上面的遞歸做法 

最後是異常機制的實現,這個上面只體現了拋出異常,而沒有捕獲的代碼,相關字節碼可能是這樣: 
setup_try ON_EXC 
... 
@ON_EXC 
... 

實現的時候,需要一個try_stk保持當前函數的try棧(因爲try可以嵌套),然後在出現異常的時候: 
if (c == null) 
{ 
    if (try_stk.size() > 0) 
    { 
        //有try,跳到捕獲錯誤的代碼位置 
        idx = try_stk.pop(); 
        continue; 
    } 
    return null; //當前函數沒有try,拋給上一層 
} 

當然,如果宿主語言本身支持異常機制,如java,則可以利用throw來拋異常,不過,這個異常體系最好不要和宿主的混起來,自己設計一套比較好
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章