兩週自制指令碼語言 - 讀後心得

descent發表於2017-08-30

在編譯原理的討論上都會提到 compile 的過程會建立一棵 ast (Abstract syntax tree), 可是在我看到的實作上卻很少有產生 ast 的資料結構。

而用 yacc 產生的 ast 你又知道要如何操作它嗎? 所以還是自己產生好了, 自己寫的自然知道怎麼操作這棵 ast。

原來 ast 並不是必須的, 請參考: 《符號表和抽象語法樹是什麼關係? 兩者在編譯器設計中是否必需?》

ast 的妙用: AST - 像lisp一樣自定義程式碼行為

而這本《兩週自制指令碼語言》實作的 parser 就會產生一棵 ast, 這是我比較有興趣的實作方式。

本書雖然說是用兩周的時間來學習, 但實際上需要的時間絕對是超過兩周的, 把它想成總共大概要 14 個章節來介紹會適合點。

可惜本書是用 java 來實作這個指令碼語言, 我對 java 非常不熟, 連要編譯書中的範例程式都讓我大傷腦筋, 臨時惡補了一些 java 的知識。

我一直以為我會用《自己動手寫編譯器、連結器》來學習 compiler, 《自己動手寫編譯器、連結器》有建立符號表, 不過目前是《兩週自制指令碼語言》幫了我最大的忙。讓我得以突破 lexer, parser, ast 的困難。

chapter 3 做了一個 lexer, 使用了 regular expression 的 library 來實作。而 chapter 15 會說明如何手工打造 lexer。

chapter 5 實作出產生 ast 的 parser。可惜我還是不知道那棵 ast 長什麼樣。這章並沒有提到實作細節, 而是解說 parser library 的使用方式, 這其實等於沒解釋怎麼寫出 parser, parser library 的原理則是 chapter 17 才說明, 使用的是一種 combinator 的作法, 我就沒特別研究了。我最後沒能看懂程式, 而是參考 chapter 16 的作法。

一般談到 ast 都會有類似 fig 1 的圖, 表示 13 + x * 2,

fig 1

不過若是

if (x>1)
  1+2
else
  5*3

的 ast 應該長的怎麼樣呢? 我相信應該難倒你了。

長的像 fig 2 這樣。

fig 2

如果 if/else 難不倒你, 那 function declare 的 ast 應該長怎麼樣呢? 我不信還難不倒你。list 1 是我的表示方法, 我也不知道是不是對的。

list 1 - function declare AST

descent@debian64:simple_compiler$ cat f1 int i,j;

int func(int x) { char a,b; } descent@debian64:simple_compiler$ ./c_parser < f1| ~/git/progs/tree/tree/tree token: int token: i token: , token: j token: ; token: int token: func token: ( token: int token: x token: ) token: { token: char token: a token: , token: b token: ; token: } root | prog | | | var func |_ __|_ | | | | i j para func_body | | x var |_ | | a b

有了 ast 之後該怎麼辦呢?

chapter 6 則是實作 eval 來對那棵 ast 求值。

讓每個 node 執行 eval(), 求出每個 node 該有的值就可以了, if node 則是在 eval() predicate 後, 選擇 then node, 或是 else node 來 eval(), 層層下去後, 就可以計算出這個 expression 了。while node 也是一樣的道理。

這章用到了 gluonj, 對於一本教學書來說, 這無疑是大大的進入門檻, 我要學的是 compiler 技術, 你卻匯入了很多 java 的額外特殊功能, 我要看懂這些程式碼, 還需要去看這些東西, 大大增加了學習曲線。我覺得這不是很好的教學方式。對於不懂 java 的人來說, 額外負擔太大了。我花了點時間終於編譯出使用 gluonj 的 java 程式。又花了一點時間才知道怎麼執行。

這裡有個困難點是《環境》, 就是 sicp 4.1 講的東西, 這是變數名稱和變數直對應的東西, 由於我已經在 sicp 4.1 痛苦過了, 這部份就沒覺得那麼難了。用 std::map 來搞定《環境》吧!

最後發現 chapter 18 就談到這些東西了, 害我花了大量時間找資料, 應該要寫在同一章的。

javac -cp .:../gluonj.jar chap6/BasicEvaluator.java javac -cp .:../gluonj.jar chap6/BasicInterpreter.java javac chap6/BasicEnv.java javac -cp .:../gluonj.jar chap6/Runner.java

執行:

java -cp .:../gluonj.jar chap6.Runner ref: Introduction to GluonJ GluonJ 教學

chapter 15 講解了自動機程式, 手工寫出 lexer, 不像 chapter 3 那樣使用 regular library 那麼好寫, 但我很容易就可以看著 fig3 就寫出程式, 這是寫出 lexer 的方法之一, 很好用, lexer 雖然沒有 parser 那麼難寫, 但也不是那麼容易, 我覺得是很有趣的程式。

lexer.cpp L38 就是實作 fig 3 的 lexer。

fig 3 lexer.cpp

1 #include 2 #include 3 #include 4 5 #include 6 7 using namespace std; 8 9 #define OK 0 10 #define ERR 1 11 12 // printable ascii, but not (, ) 13 static inline int isgraph_ex(int c) 14 { 15 #if 1 16 if (c == '(') 17 return 0; 18 if (c == ')') 19 return 0; 20 #endif 21 return isgraph(c); 22 } 23 24 int la=-1; 25 26 int getchar_la() 27 { 28 if (la != -1) 29 { 30 int tmp=la; 31 la = -1; 32 return tmp; 33 } 34 else 35 return getchar(); 36 } 37 38 int get_token(string &token) 39 { 40 int c; 41 42 do 43 { 44 c = getchar_la(); 45 }while(isspace(c)); 46 47 if (c == EOF) 48 return EOF; 49 else if (isdigit(c)) 50 { 51 do 52 { 53 token.push_back(c); 54 c = getchar_la(); 55 }while(isdigit(c)); 56 } 57 else if (isalpha(c)) 58 { 59 do 60 { 61 token.push_back(c); 62 c = getchar_la(); 63 } while(isalnum(c)); 64 } 65 else if (c == '=') 66 { 67 c = getchar_la(); 68 if (c == '=') 69 { 70 token = "=="; 71 } 72 else 73 { 74 la = c; 75 token = "="; 76 } 77 return OK; 78 } 79 else 80 { 81 return ERR; 82 } 83 if (c != EOF) 84 la = c; 85 return OK; 86 } 87 88 int get_se_token(string &token) 89 { 90 int c; 91 92 do 93 { 94 c = getchar_la(); 95 }while(isspace(c)); 96 97 if (c == EOF) 98 return EOF; 99 else if (c == '(') 100 { 101 token = '('; 102 return OK; 103 } 104 else if (c == ')') 105 { 106 token = ')'; 107 return OK; 108 } 109 else if (isgraph_ex(c)) // printable ascii, but not (, ) 110 { 111 do 112 { 113 token.push_back(c); 114 c = getchar_la(); 115 }while(isgraph_ex(c)); 116 } 117 else 118 { 119 return ERR; 120 } 121 122 123 if (c != EOF) 124 la = c; 125 return OK; 126 } 127 128 int main(int argc, char *argv[]) 129 { 130 while(1) 131 { 132 string token; 133 134 //int ret = get_token(token); 135 int ret = get_se_token(token); 136 if (ret == EOF) 137 { 138 break; 139 } 140 if (ret == OK) 141 { 142 cout << "token: " << token << endl; 143 } 144 else 145 { 146 cout << "token error" << endl; 147 } 148 token.clear(); 149 } 150 151 return 0; 152 }

後來畫了一張 scheme 的自動機圖, 難得畫的這麼好看 , 發現很快就可以寫出正確分割 token 的程式, 比之前的胡思亂想更有可讀性。

fig4

lexer.cpp L88 就是實作 fig 4 的 lexer。

chapter 16 手工打造 parser, 不是使用原本的 combinator 作法, 使用 recursive descent 來做示範, 這是我最想知道的作法, recursive descent 是由上到下的作法, 以下面的 BNF 來說

expression: term {("+" | "-") term} term: factor { ("*" | "/") factor} factor: NUMBER | "(" expression ")"

就是從 expression 開始往 factor 去 match。由 factor 去對應到 expression 就是由下到上, yacc 產生的 parser 就是這樣的行為。

對照其 java code, 我打造了 c++ 的版本: https://github.com/descent/progs/tree/master/compiler 已經可以建立 ast, 並轉出 post order, in order, prefix order。那個範例程式雖小, 卻幫了我很大的忙, 讓我得以突破 parser 的困難點。

ret

1 inorder to postorder 2 descent@debian64:compiler$ ./parser 3 2*(3+1) 4 token: 2 5 token: * 6 token: ( 7 token: 3 8 token: + 9 token: 1 10 token: ) 11 ast node type: MUL 12 ( 2 ( 3 1 + )* ) 13 14 15 inorder to prefix order 16 descent@debian64:compiler$ ./parser 17 2*(3+1) 18 token: 2 19 token: * 20 token: ( 21 token: 3 22 token: + 23 token: 1 24 token: ) 25 ast node type: MUL 26 ( * 2 ( + 3 1 ))

operator precedence parsing 是運算子號的優先順序處理方法, 一般都是使用 BNF 規則來定義運算子號的優先順序, 不過每增加一個規則, 程式就要重寫, 有點麻煩, 所以才有了這個方法, 我喜歡這個作法, 可以省下不少 bnf rule, 這對於手工撰寫程式的我來說, 可以省下撰寫大量的 bnf function, 所以我努力的把它看懂, 事實上也不算是太難。

expr 可以省略到這樣。

expr : factor {OP factor}

加入新的運算子號只要這麼做就好, 左結合或是右結合都沒問題。

operators.insert({"=", new Precedence{1, false}}); operators.insert({"==", new Precedence{2, true}}); operators.insert({">", new Precedence{2, false}}); operators.insert({"<", new Precedence{2, false}}); operators.insert({"+", new Precedence{3, true}}); operators.insert({"*", new Precedence{4, true}});

我其實沒有看懂 java 建立 ast 的程式碼, 看懂觀念後, 靠著這章的 java 範例實作。

辛苦了很久, 目前到 if/else 階段, 這個有點問題, 更新如下 if_ast

if/else ast

1 if (x>2) 2 { 3 1+2*3; 4 } 5 else 6 { 7 7*8; 8 } 9 10 if (y>2) 11 { 12 (1+2)3; 13 } 14 else 15 { 16 7(8+2); 17 } 18 19 20 root 21 _____|_____ 22 | | 23 if if 24 __|__ __|__ 25 | | | | | | 26 > + * > * * 27 |_ |_ |_ |_ |_ |_ 28 | | | | | | | | | | | | 29 x 2 1 * 7 8 y 2 + 3 7 + 30 |_ |_ |_ 31 | | | | | | 32 2 3 1 2 8 2

以下是修改後的版本:

if_ast

descent@debian64:compiler$ cat if3

if (x>2) { 1+2*3; (11+9)*5 } else { 7*8; 13+9*5 }

if (y>2) { (1+2)3; } else { 7(8+2); }

descent@debian64:compiler$ ./parser < if3 |/home/descent/git/progs/tree/tree/tree


                           root
           _________________|_________________
           |                                 |
           if                                if

__________|__________ ________|________ | | | | | |

    then_block    else_block  >       then_block  else_block

|_ | |_ |_ | | | | | | | | | | * * x 2 + * * + y 2 |_ |_ |_ |_ |_ | | | | | | | | | | | | | + 3 7 + 1 * + 5 7 8 13 * |_ |_ |_ | |_ | | | | | | | | | | 1 2 8 2 2 3 11 9 9 5

chapter 13 則是將原本的 interpreter 轉成 vm 的 machine code, 所以還會介紹如何實作一個 vm, 這裡才真的有「編譯」的動作。

不過我打算使用《實作一個簡單的 virtual machine》這個 vm, 這個簡單不少。

chapter 15/16 我認為才是這本書的重點, 我從這兩章的內容終於知道怎麼把 ast 建立出來。

書中範例: http://www.csg.ci.i.u-tokyo.ac.jp/~chiba/files/

相關文章