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

目錄

前文列表

用 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 {
  char *tag;
  char *contents;
  mpc_state_t state;
  int children_num;
  struct mpc_ast_t **children;
} mpc_ast_t;
  • tag:就是在節點內容之前的信息,它表示瞭解析這個節點時所用到的所有規則。例如:expr|number|regex。tag 字段非常重要,因爲它可以讓我們知道創建節點時所匹配到的規則。
  • contents:包含了節點中具體的操作數和操作符內容,例如 *(5。你會發現,對於表示分支的非葉子節點,這個字段爲空。而對於葉子節點,則包含了操作數或操作符的字符串形式。
  • state:這裏麪包含了解析器發現這個節點時所處的狀態,例如行數和列數等信息。本書不會用到這個字段。
  • children_numchildren:幫助我們來遍歷 AST。前一個字段告訴我們有多少個子節點,後一個字段是包含這些節點的數組。其中,children 的數據類型爲 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 == 0) { return 1; }
  if (t->children_num >= 1) {
    int total = 1;
    for (int i = 0; i < t->children_num; i++) {
      total = total + number_of_nodes(t->children[i]);
    }
    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 輸出的特徵:

  • 有 number 標籤的節點一定是一個數字,並且沒有子節點。我們可以直接將其轉換爲一個數字。
  • 如果一個節點有 expr 標籤,但沒有 number 標籤,那麼第一個子節點永遠是 ( 字符,最後一個子節點是 ) 字符。我們需要看他的第二個子節點是什麼操作符,然後我們需要使用這個操作符來對後面的子節點進行求值。

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

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

在這裏插入圖片描述
我們可以使用 strcmp 來檢查應該使用什麼操作符,並使用 strstr 來檢測 tag 中是否含有某個字段:

#include <stdio.h>
#include <stdlib.h>
#include "mpc.h"

#ifdef _WIN32
#include <string.h>

static char buffer[2048];

char *readline(char *prompt) {
    fputs(prompt, stdout);
    fgets(buffer, 2048, stdin);

    char *cpy = malloc(strlen(buffer) + 1);

    strcpy(cpy, buffer);
    cpy[strlen(cpy) - 1] = '\0';

    return cpy;
}

void add_history(char *unused) {}

#else

#ifdef __linux__
#include <readline/readline.h>
#include <readline/history.h>
#endif

#ifdef __MACH__
#include <readline/readline.h>
#endif

#endif

/* Use operator string to see which operation to perform */
long eval_op(long x, char *op, long y) {
    if (strcmp(op, "+") == 0) { return x + y; }
    if (strcmp(op, "-") == 0) { return x - y; }
    if (strcmp(op, "*") == 0) { return x * y; }
    if (strcmp(op, "/") == 0) { return x / y; }
    return 0;
}

long eval(mpc_ast_t *t) {

    /* If tagged as number return it directly.
     * 有 number 標籤的節點一定是一個數字,並且沒有子節點
     * 直接將其轉換爲一個數字。
     */
    if (strstr(t->tag, "number")) {
        return atoi(t->contents);
    }

    /* 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")) {
        x = eval_op(x, op, eval(t->children[i]));
        i++;
    }
    return x;
}

int main(int argc, char *argv[]) {

    /* Create Some Parsers */
    mpc_parser_t *Number   = mpc_new("number");
    mpc_parser_t *Operator = mpc_new("operator");
    mpc_parser_t *Expr     = mpc_new("expr");
    mpc_parser_t *Lispy    = mpc_new("lispy");

    /* Define them with the following Language */
    mpca_lang(MPCA_LANG_DEFAULT,
      "                                                     \
        number   : /-?[0-9]+/ ;                             \
        operator : '+' | '-' | '*' | '/' ;                  \
        expr     : <number> | '(' <operator> <expr>+ ')' ;  \
        lispy    : /^/ <operator> <expr>+ /$/ ;             \
      ",
      Number, Operator, Expr, Lispy);

    puts("Lispy Version 0.1");
    puts("Press Ctrl+c to Exit\n");

    while(1) {
        char *input = NULL;

        input = readline("lispy> ");
        add_history(input);

        /* Attempt to parse the user input */
        mpc_result_t r;

        if (mpc_parse("<stdin>", input, Lispy, &r)) {
            /* On success print and delete the AST */
            long result = eval(r.output);
            printf("%li\n", result);
            mpc_ast_delete(r.output);
        } else {
            /* Otherwise print and delete the Error */
            mpc_err_print(r.error);
            mpc_err_delete(r.error);
        }

        free(input);

    }

    /* Undefine and delete our parsers */
    mpc_cleanup(4, Number, Operator, Expr, Lispy);

    return 0;
}

編譯:

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)
97
lispy> + 5 6
11

抽象語法樹與行爲樹

在這裏插入圖片描述
行爲樹和抽象語法樹之間有一個細微但非常重要的區別,我們應該區別對待(這促成了解析器的改寫)。

簡單來說,行爲樹是帶有上下文的 AST。上下文是一個函數返回的類型的信息,或者兩個地方使用的變量實際上是相同的變量。 因爲它需要弄清楚並記住所有這些上下文,生成行爲樹的代碼需要大量的命名空間查找表和其他的東西。

一旦我們有了行爲樹,運行代碼就很容易了。 每個行爲節點都有一個函數 “execute”,它接受一些輸入,不管行爲應該如何(包括可能調用子行爲),返回行爲的輸出。 這是行爲中的解釋器。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章