用 C 語言開發一門程式語言 抽象語法樹

2021-10-04 20:02:18 字數 4391 閱讀 7087

抽象語法樹與行為樹

《用 c 語言開發一門程式語言 — 互動式解析器l》

《用 c 語言開發一門程式語言 — 跨平台的可移植性》

《用 c 語言開發一門程式語言 — 語法解析器》

lispy> + 5 (* 2 2)

>

regex

operator|char:1:1 '+'

expr|number|regex:1:3 '5'

expr|>

char:1:5 '('

operator|char:1:6 '*'

expr|number|regex:1:8 '2'

expr|number|regex:1:10 '2'

char:1:11 ')'

regex

上篇我們通過 mpc 解析器組合庫完成了讀取輸入,對波蘭表示式的語法解析並得到表示式的 ast(抽象語法樹),運算元(number)和操作符(operator)等需要被處理的有效資料都位於葉子節點上。而非葉子節點上則包含了遍歷和求值的資訊。

但是現在我們仍不能對它進行計算求值。在實現計算求值之前,我們先好好看看 ast 的結構:

typedef

struct mpc_ast_t mpc_ast_t;

/* load ast from output。

* 因為 mpc_ast_t* 是指向結構體的指標型別,所以獲取其字段的語法有些許不同。我們需要使用 -> 符號,而不是 . 符號。

*/mpc_ast_t *a = r.output;

printf

("tag: %s\n"

, a->tag)

;printf

("contents: %s\n"

, a->contents)

;printf

("number of children: %i\n"

, a->children_num)

;/* get first child */

mpc_ast_t *c0 = a->children[0]

;printf

("first child tag: %s\n"

, c0->tag)

;printf

("first child contents: %s\n"

, c0->contents)

;printf

("first child number of children: %i\n"

, c0->children_num)

;

樹形結構是自身重複的。樹的每個子節點都是樹,每個子節點的子節點也是樹,以此類推。可見,樹形結構也是遞迴和重複的。如果我們想編寫函式處理所有可能的情況,就必須要保證函式可以處理任意深度,我們可以使用遞迴函式的天生優勢來輕鬆地處理這種重複自身的結構。

遞迴函式就是在執行的過程中呼叫自身的函式。理論上,遞迴函式會無窮盡地執行下去。但實際上,遞迴函式對於不同的輸入會產生不同的輸出,如果我們每次遞迴都改變或使用不同的輸入,並設定遞迴終止的條件,我們就可以使用遞迴實現預期的效果。例如:使用遞迴來計算樹形結構中節點個數。

首先考慮最簡單的情況,如果輸入的樹沒有子節點,我們只需簡單的返回 1 表示根節點就行了。如果輸入的樹有乙個或多個子節點,這時返回的結果就是根節點再加上所有子節點的值。

使用遞迴,遍歷統計子節點的數量:

int

number_of_nodes

(mpc_ast_t* t)

if(t->children_num >=1)

return total;

}}

lispy> + 5 (* 2 2)

>

regex

operator|char:1:1 '+'

expr|number|regex:1:3 '5'

expr|>

char:1:5 '('

operator|char:1:6 '*'

expr|number|regex:1:8 '2'

expr|number|regex:1:10 '2'

char:1:11 ')'

regex

在實現**之前再好好總結一下 ast 輸出的特徵:

在對語法樹進行求值的時候,還需要儲存計算的結果。在這裡,我們使用 c 語言中 long 型別。另外,為了檢測節點的型別,或是為了獲得節點中儲存的數值,我們會用到節點中的 tag 和 contents 字段。這些欄位都是字串型別的。

我們引入一些輔助性的庫函式:

我們可以使用 strcmp 來檢查應該使用什麼操作符,並使用 strstr 來檢測 tag 中是否含有某個字段:

#include

#include

#include

"mpc.h"

#ifdef _win32

#include

static

char buffer[

2048];

char

*readline

(char

*prompt)

void

add_history

(char

*unused)

#else

#ifdef __linux__

#include

#include

#endif

#ifdef __mach__

#include

#endif

#endif

/* use operator string to see which operation to perform */

long

eval_op

(long x,

char

*op,

long y)if(

strcmp

(op,

"-")==0

)if(strcmp

(op,

"*")==0

)if(strcmp

(op,

"/")==0

)return0;

}long

eval

(mpc_ast_t *t)

/* the operator is always second child.

* 如果乙個節點有 expr 標籤,但沒有 number 標籤,那麼它的第二個子節點肯定是操作符。

* 這個操作符後面的子節點肯定是運算元。

*/char

*op = t->children[1]

->contents;

long x =

eval

(t->children[2]

);/* 迭代剩餘的子節點,並求值。 */

int i =3;

while

(strstr

(t->children[i]

->tag,

"expr"))

return x;

}int

main

(int argc,

char

*ar**)

else

free

(input);}

/* undefine and delete our parsers */

mpc_cleanup(4

, number, operator, expr, lispy)

;return0;

}

編譯:

gcc -std=c99 -wall parsing.c mpc.c -lreadline -lm -o parsing
執行:

$ ./parsing

lispy version 0.1

press ctrl+c to exit

lispy> - (* 10 10) (+ 1 1 1)

97lispy> + 5 6

11

行為樹和抽象語法樹之間有乙個細微但非常重要的區別,我們應該區別對待(這促成了解析器的改寫)。

簡單來說,行為樹是帶有上下文的 ast。上下文是乙個函式返回的型別的資訊,或者兩個地方使用的變數實際上是相同的變數。 因為它需要弄清楚並記住所有這些上下文,生成行為樹的**需要大量的命名空間查詢表和其他的東西。

一旦我們有了行為樹,執行**就很容易了。 每個行為節點都有乙個函式 「execute」,它接受一些輸入,不管行為應該如何(包括可能呼叫子行為),返回行為的輸出。 這是行為中的直譯器。

用 C 語言開發一門程式語言 異常處理

用 c 語言開發一門程式語言 互動式解析器l 用 c 語言開發一門程式語言 跨平台的可移植性 用 c 語言開發一門程式語言 語法解析器 用 c 語言開發一門程式語言 抽象語法樹 在開發過程中,程式崩潰是很正常的。但我們希望最後發布的產品能夠告訴使用者錯誤出在 而不是簡單粗暴的退出。目前,我們的程式僅...

如何開發一門程式語言

首先,你要考慮這是動態語言還是靜態語言,然後去想它面向什麼,如web開發,物件導向的程式設計等。還有它的語法,下面列出了物件導向的程式語言所需要的語句 語句用途 if 表示式1 執行語句1 else 執行語句2 判斷如果表示式1,則執行語句1,否則,執行語句2 cout 輸出cin 輸入int 變數...

用 C 語言開發一門程式語言 互動式直譯器

通過開發一門類 lisp 的程式語言來理解程式語言的設計思想,本實踐來自著名的 build your own lisp 語言主要有兩種型別 編譯型和解釋型。技術上,任何語言都可以被編譯或解釋,但是一種或另一種語言通常對於特定語言更有意義。一般來說,解釋往往更加靈活,而編譯往往具有更高的效能。當我們希...