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

目錄

前文列表

用 C 語言開發一門編程語言 — 交互式解析器l
用 C 語言開發一門編程語言 — 跨平臺的可移植性
用 C 語言開發一門編程語言 — 語法解析器
用 C 語言開發一門編程語言 — 抽象語法樹

異常捕獲

在開發過程中,程序崩潰是很正常的。但我們希望最後發佈的產品能夠告訴用戶錯誤出在哪裏,而不是簡單粗暴的退出。目前,我們的程序僅能打印出語法上的錯誤,但對於表達式求值過程中產生的錯誤卻無能爲力。

C 語言有很多種錯誤處理方式,但針對當前的項目,我們考慮將錯誤也作爲表達式求值的一種結果。也就是說,在 Lispy 中,表達式求值的結果要麼是數字,要麼便是錯誤。舉例說,表達式 + 1 2 求值會得到數字 3,而表達式 / 10 0 求值則會得到一個錯誤。

定義 Lisp Value 函數

爲了達到這個目的,我們需要能表示這兩種結果(成功 or 失敗)的數據結構。簡單起見,我們使用結構體來表示,並使用 type 字段來說明當前哪個字段是有意義的。結構體名爲 lval,取義 Lisp Value,定義如下:

/* Declare New lval Struct */
typedef struct {
  int type;
  long num;
  int err;
} lval;

lval 的 type 和 err 字段的類型都是 int,這意味着它們皆由整數值來表示。之所以選用 int,是因爲 “成功或失敗” 符合二元對立的情形。但 C 語言中,沒有 True or False 這樣的 Boolean 數據類型,所以我們使用 0/1 代替:

  • 如果 type 爲 0,那麼此結構體表示一個數字。
  • 如果 type 爲 1,那麼此結構體表示一個錯誤。

並且,我們可以給這些數字起一個有意義的名字,以提高代碼的可讀性。通過 整型別名 這兩個特徵,我們很自然的會聯想到枚舉數據類型:

/* Create Enumeration of Possible lval Types */
enum {
	LVAL_NUM,  // 默認整型數值爲 0
	LVAL_ERR   // 默認整型數值爲 0 + 1
};

另外,Error 也必然是可以枚舉的,所以同樣使用枚舉數據類型:

/* Create Enumeration of Possible Error Types */
enum {
	LERR_DIV_ZERO,  // 除數爲零
	LERR_BAD_OP,	// 操作符未知
	LERR_BAD_NUM	// 操作數過大
};

我們再定義兩個函數來完成 “lval 類型實例” 的初始化:

/* Create a new number type lval
 * 因爲使用無名創建方式定義的 lval 結構體是自定義數據類型,
 * 所以我們可以使用 lval 來聲明函數返回值類型。
 */
lval lval_num(long x) {
  lval v;
  v.type = LVAL_NUM;
  v.num = x;
  return v;
}

/* Create a new error type lval */
lval lval_err(int x) {
  lval v;
  v.type = LVAL_ERR;
  v.err = x;
  return v;
}

/* Print an "lval" */
void lval_print(lval v) {
  switch (v.type) {
    /* In the case the type is a number print it */
    /* Then 'break' out of the switch. */
    case LVAL_NUM:
      printf("%li", v.num);
      break;

    /* In the case the type is an error */
    case LVAL_ERR:
      /* Check what type of error it is and print it */
      if (v.err == LERR_DIV_ZERO) {
        printf("Error: Division By Zero!");
      }
      if (v.err == LERR_BAD_OP)   {
        printf("Error: Invalid Operator!");
      }
      if (v.err == LERR_BAD_NUM)  {
        printf("Error: Invalid Number!");
      }
      break;
  }
}

/* Print an "lval" followed by a newline */
void lval_println(lval v) {
	lval_print(v);
	putchar('\n');
}

最後,我們使用 lval 類型來替換掉之前使用的 long 類型,此外,我們還需要修改函數使其能正確處理數字或是錯誤作爲輸入的情況:

#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

/* Create Enumeration of Possible lval Types */
enum {
    LVAL_NUM,
    LVAL_ERR
};

/* Create Enumeration of Possible Error Types */
enum {
    LERR_DIV_ZERO,
    LERR_BAD_OP,
    LERR_BAD_NUM
};

/* Declare New lval Struct
 * 使用 lval 枚舉類型來替換掉之前使用的 long 類型。
 * 單存的 long 類型沒辦法攜帶成功或失敗、若失敗,是什麼失敗等信息。
 * 所以我們定義 lval 枚舉類型來作爲 “算子” 及 “結果”。
 */
typedef struct {
    int type;
    long num;
    int err;
} lval;

/* Create a new number type lval */
lval lval_num(long x) {
    lval v;
    v.type = LVAL_NUM;
    v.num = x;
    return v;
}

/* Create a new error type lval */
lval lval_err(long x) {
    lval v;
    v.type = LVAL_ERR;
    v.err = x;
    return v;
}

/* Print an "lval"
 * 通過對 lval 枚舉類型變量的解析來完成對計算結果的解析。
 */
void lval_print(lval v) {
    switch (v.type) {
        /* In the case the type is a number print it */
        case LVAL_NUM:
            printf("%li", v.num);
            break;

        /* In the case the type is an error */
        case LVAL_ERR:
            /* Check what type of error it is and print it */
            if (v.err == LERR_DIV_ZERO) {
                printf("Error: Division By Zero!");
            }
            else if (v.err == LERR_BAD_OP) {
                printf("Error: Invalid Operator!");
            }
            else if (v.err == LERR_BAD_NUM) {
                printf("Error: Invalid Number!");
            }
            break;
    }
}

/* Print an "lval" followed by a newline */
void lval_println(lval v) {
    lval_print(v);
    putchar('\n');
}

/* Use operator string to see which operation to perform */
lval eval_op(lval x, char *op, lval y) {

    /* If either value is an error return it
     * 如果 “算子” 的類型是錯誤,則直接返回。
     */
    if (x.type == LVAL_ERR) { return x; }
    if (y.type == LVAL_ERR) { return y; }

    /* Otherwise do maths on the number values
     * 如果 “算子” 是 Number,則取出操作數進行運算。
     */
    if (strcmp(op, "+") == 0) { return lval_num(x.num + y.num); }
    if (strcmp(op, "-") == 0) { return lval_num(x.num + y.num); }
    if (strcmp(op, "*") == 0) { return lval_num(x.num + y.num); }
    if (strcmp(op, "/") == 0) {
        /* If second operand is zero return error */
        if (y.type == LVAL_NUM) {
            return y.num == 0
                ? lval_err(LERR_DIV_ZERO)
                : lval_num(x.num / y.num);
        }
    }
    return lval_err(LERR_BAD_OP);
}

lval eval(mpc_ast_t *t) {

    /* If tagged as number return it directly. */
    if (strstr(t->tag, "number")) {
        /* Check if there is some error in conversion */
        errno = 0;

        /* 使用 strtol 函數進行字符串到數字的轉換,
         * 這樣就可以通過檢測 errno 變量確定是否轉換成功,
         * 對數據類型轉換的準確性進行了加強。
         */
        long x = strtol(t->contents, NULL, 10);
        return errno != ERANGE
            ? lval_num(x)
            : lval_err(LERR_BAD_NUM);
    }

    /* The operator is always second child. */
    char *op = t->children[1]->contents;

    /* We store the third child in `x` */
    lval x = eval(t->children[2]);

    /* Iterate the remaining children and combining. */
    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 */
            lval result = eval(r.output);
            lval_println(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 -g -std=c99 -Wall parsing.c mpc.c -lreadline -lm -o parsing

運行:

$ ./parsing
Lispy Version 0.1
Press Ctrl+c to Exit

lispy> / 10 0
Error: Division By Zero!
lispy> / 10 2
5
lispy>
<stdin>:1:1: error: expected '+', '-', '*' or '/' at end of input
lispy> / 10 2
5
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章