自制簡易解釋器


title: 自制簡易解釋器
date: 2019-02-18 22:00:01
origin: 自制簡易解釋器


自制簡易解釋器

用C語言寫了一個簡單的動態(tài)語言解釋器, 代碼放在了github上面:hedegehog. 編譯, 運行可以看看github上的readme.

先簡單介紹下這門語言. hedgehog 的多數(shù)設計和 python 比較相似, 無需聲明變量類型, if,for等語句沒有塊級作用域. 語法上又有點像 go 語言: if, for不需要(), 但是后面的代碼塊都必須加{}; 沒有while, 不過有for condition {}來替代. 不過行尾必須加;這點和 go 不同. 大多數(shù)設計都是為了簡化實現(xiàn)方式, 比如必須加{}, ;是為了簡化語法的解析.

已實現(xiàn)的功能

  • 數(shù)據(jù)類型

    a = 10;//int
    b = 3.14;//float
    c = true;//boolean
    d = null;//null
    s = "Hello, World!";//string
    
  • 控制語句

      a = 10;
      if a > 10 { // `()` is not necessary.
          b = a+20;
      } elsif a==10 {
          b = a+10;
      } else {
          b = a-10;
      }
      print(b);
      // block has no local environment, 
      // so 'b' is a global variable.
    
  • 循環(huán)

      for i=0; i<10; i=i+1 {
          print(i);
          if i>=4 {break;}
      }
      i = 0;
      for i<10 {
          if i<5 {continue;}
          print(i);
      }
    
  • 函數(shù)
    function也被看作一種值(基本數(shù)據(jù)類型), 不過目前還沒有對它實現(xiàn)垃圾回收, 所以直接以函數(shù)賦值或者其他操作會出現(xiàn)內(nèi)存錯誤.

      // 模仿python首頁的函數(shù):)
      func fbi(n) {
          a, b = 0, 1;
          for a<n {
              print(a);
              a, b = b, a+b;//支持這種賦值方式
          }
      }
      fbi(100);
    
      func factorial(n) {
          if n==0 {return 1;}
          return n*factorial(n-1);
      }
      print(factorial(5));
    

    目前只實現(xiàn)了一個原生函數(shù)print. print接收一個基本數(shù)據(jù)類型作為參數(shù), 輸出并換行, 或者無參數(shù), 直接換行.

  • 運算符
    大多數(shù)與c保持一致, 除了&, |. 因為沒有提供位運算的功能, 所以直接用這兩個符號表示邏輯與和邏輯或.

    b = 2;
    a = 10;
    if a>20 & b<10 {
        print("`b` is less than 10 and `a` is greater than 20");
    }
    if a>20 | b<10 {
        print("`b` is less than 10 or `a` is greater than 20");
    }
    

概述

image

首先詞法分析和語法分析, 構(gòu)建分析樹. 這里以a=1+2*3+fn();為例, 介紹一下表達式分析樹的構(gòu)建.

首先, 它是一個賦值表達式, 把右邊的值1+2*3+fn()賦給左邊的變量a. 而1+2*3+fn() 又由加法表達式, 乘法表達式, 函數(shù)調(diào)用表達式構(gòu)成. yacc中編寫的規(guī)則會約束表達式構(gòu)建的順序, 構(gòu)建過程大概是這樣的:

a = 1 + 2 * 3 + fn()
identifier = value + value * value + function_call // 詞法分析
identifier = value + value * value + value // function_call 約歸為value
identifier = value + multiply_expression + value // 根據(jù)規(guī)則, 先生成乘法表達式
identifier = add_expression + value // 把前兩項歸約為加法表達式
identifier = add_expression // 繼續(xù)歸約(加法運算遵循從左到右)
identifier = expression // 歸約為更一般的普通表達式
assgin_expression // 匹配到賦值表達式的規(guī)則, 歸約為賦值表達式
expression // 賦值表達式歸約為一般表達式

然后就可以構(gòu)建了下面的分析樹了:

image

求值的時候從最底層依次往上求值, 就能得到表達式的值.

語法與詞法分析

這部分直接使用lex做詞法分析, yacc做語法分析. 這兩個工具在大多數(shù)UNIX上都有預裝, GUN提供的版本分別叫bison, flex. 直接用bison生成的文件可能和yacc有些區(qū)別(需要修改生成文件的的文件名, 或者改c語言文件中包含的頭文件名), 不過在Linux下安裝了bison可以直接使用yacc命令. flex 與 lex 生成文件沒有區(qū)別.

yacc與lex網(wǎng)上有大量的資料, 而且使用比較簡單, 這里就僅作簡單的介紹.

lex可以使用正則表達式做匹配:

"func" return FUNCTION;//func 函數(shù)定義關(guān)鍵字
[A-Za-z_][A-Za-z0-9_]* {
    //辨識符匹配
    yylval.identifier = initString(yytext);
    return IDENTIFIER;
}

這里的FUNCTIONIDENTIFIER被稱為token, 一般在yacc的文件中定義. 在生成的C語言文件中token用枚舉變量表示.

lex詞法分析的結(jié)果會交給yacc處理. yacc使用類似BNF的規(guī)范來編寫規(guī)則.

// 加法表達式由乘法表達式歸約, 這樣可以限制乘法(除法, 取模)優(yōu)先于
//加法(減法)運算.
ADD_EXPRESSION:
    MUL_EXPRESSION
    |
    ADD_EXPRESSION ADD MUL_EXPRESSION {
        $$ = initBinaryExpression(ADD_OPERATOR, $1, $3);
    }
    |
    ADD_EXPRESSION SUB MUL_EXPRESSION {
        $$ = initBinaryExpression(SUB_OPERATOR, $1, $3);
    }
    ;

MUL_EXPRESSION:
    UNARY_EXPRESSION//單個單目運算表達式直接歸約到乘法表達式
    |
    MUL_EXPRESSION MUL UNARY_EXPRESSION {
        $$ = initBinaryExpression(MUL_OPERATOR, $1, $3);
    }
    |
    MUL_EXPRESSION DIV UNARY_EXPRESSION {
        $$ = initBinaryExpression(DIV_OPERATOR, $1, $3);
    }
    |
    MUL_EXPRESSION MOD UNARY_EXPRESSION {
        $$ = initBinaryExpression(MOD_OPERATOR, $1, $3);
    }
    ;

詞法分析和語法分析屬于解釋器的前端, 這部分沒有自己編寫, 主要把精力放在了后端.

表達式與語句

表達式與語句是整個后端最重要的兩個模塊, 大部分的邏輯都在兩者中實現(xiàn). 這里主要介紹一下表達式, 語句與之類似.
大部分的代碼都用C語言實現(xiàn)了面向?qū)ο? 這里必須推薦一下劉大的C語言:春節(jié)回家過年,我發(fā)現(xiàn)只有我沒有對象算凿!.

// expression.h
struct ExpressionTag {
    void (*free)(Expression *self);

    Value (*evaluate)(Expression *self, Environment *env);

    Expression *pre;
};

這是表達式接口的結(jié)構(gòu)體, freeevaluate是C語言中的函數(shù)指針, 定義了所有表達式都應該具備的方法. 這個pre看起來有些突兀, 它其實是為了函數(shù)傳參和多變量同時賦值時鏈接表達式使用的. 比如a, b = 1, 2;, 1, 2會分別解析為兩個表達式, 通過pre鏈接. 這樣的設計可能不符合面向?qū)ο? 不過為了實現(xiàn)鏈表更加簡單, 就暫時這樣寫了.

所有需要釋放內(nèi)存的結(jié)構(gòu)體都有free函數(shù)指針, 所以可以定義一個簡單的宏#define del(x) x->free(x), 使用del(obj)就可以釋放內(nèi)存了.

下面以賦值表達式介紹一下賦值表達式的實現(xiàn)過程.

// expression.h
void *initAssignExpression(String *id, Expression *expression) 

向外提供的唯一接口是initAssignExpression, 也就是說所有一般的表達式在模塊外引用時都會被向上轉(zhuǎn)型為Expression, 只有freeevaluate兩個方法.

// expression.c
typedef struct {
    Expression base;//base必須放在第一個, 保證類型轉(zhuǎn)換時的正確性.
    String *id;
    Expression *expression;
} AssignExpression;

static Value evaluateAssignExpression(Expression *_self, Environment *env) {
    AssignExpression *self = (AssignExpression *) _self;//向下轉(zhuǎn)型為 `AssignExpression`
    Value value = self->expression->evaluate(self->expression, env);
    // 字符串采用引用計數(shù)
    on_self(self->id, refer);
    //把變量加到environment, 后文會介紹
    env->addVariable(env, initVariable(self->id, value));
    return value;
}

static void freeAssignExpression(Expression *_self) {
    AssignExpression *self = (AssignExpression *) _self;
    del(self->expression);
    on_self(self->id, release);
    free(self);
}

//綁定函數(shù)
const static Expression AssignExpressionBase = {freeAssignExpression,evaluateAssignExpression};

void *initAssignExpression(String *id, Expression *expression) {
    AssignExpression *exp = malloc(sizeof(AssignExpression));
    exp->expression = expression;
    //給base賦值, 函數(shù)的綁定在new的時候完成.
    exp->base = AssignExpressionBase;
    exp->id = id;
    return exp;
}

AssignExpression的結(jié)構(gòu)體和除init外方法都在.c文件中實現(xiàn), 并且標記為static, 從而就實現(xiàn)了封裝. 在init中實現(xiàn)函數(shù)的綁定, 以Expression引用的時候就能調(diào)用相應的方法, 這就實現(xiàn)了多態(tài).

其他的表達式根據(jù)具體的功能如法炮制.

語句的實現(xiàn)也是類似:

struct StatementTag {
    StatementResult (*execute)(Statement *self, Environment *env);

    void (*free)(Statement *self);

    Statement *next;
};

解釋器與運行環(huán)境

Environment

struct EnvironmentTag {
    void (*addVariable)(Environment *self, Variable *var);

    Variable *(*findVariable)(Environment *self, String *id);

    void (*free)(Environment *self);

    VariableTrie *trie;
};

void *initEnvironment();

運行環(huán)境主要負責變量和函數(shù)的保存, 查找. Global environment保存所有的全局變量和函數(shù). 函數(shù)有獨立于 global environment的local environment. for, if等語句塊沒有environment, 它們使用所在函數(shù)或者全局的environment.

Environment中的trie是字典樹, 負責記錄變量名和函數(shù)名.

Interpreter

typedef struct InterpreterTag {
    Environment *globalEnv;
    StatementList *list;

    void (*free)(struct InterpreterTag *);

    void (*compile)(struct InterpreterTag *, FILE *);

    void (*interpret)(struct InterpreterTag *);
} Interpreter;

Interpreter *initInterpreter();

Interpreter *getCurrentInterpreter();

Interpreter 中compile實現(xiàn)分析樹的構(gòu)建, interpret實現(xiàn)語句的執(zhí)行. 因為全局只需要一個 interpreter, 所以別的地方可以通過getCurrentInterpreter獲取當前interpreter.

總結(jié)

整個解釋器的大概構(gòu)成就是這樣. 目前只實現(xiàn)了一些簡單的功能, 數(shù)組, 字典, 垃圾回收(目前只對字符串做了引用計數(shù)回收), 文件IO等特性都還沒有寫. 而且完全沒有優(yōu)化, 運行效率極低. 不過寫這個解釋器的時候還是收獲不少: 深入學習了C語言, 對編譯原理有了大概的了解, 更加深刻地理解了面向?qū)ο?..

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末戒努,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子侍筛,更是在濱河造成了極大的恐慌撒穷,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異蛤奥,居然都是意外死亡,警方通過查閱死者的電腦和手機蟀伸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門啊掏,熙熙樓的掌柜王于貴愁眉苦臉地迎上來衰猛,“玉大人,你說我怎么就攤上這事小泉∶岣埽” “怎么了?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵兢交,是天一觀的道長配喳。 經(jīng)常有香客問我凳干,道長,這世上最難降的妖魔是什么涧团? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮钮追,結(jié)果婚禮上阿迈,老公的妹妹穿的比我還像新娘。我一直安慰自己刊棕,他們只是感情好待逞,可當我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布飒焦。 她就那樣靜靜地躺著屿笼,像睡著了一般。 火紅的嫁衣襯著肌膚如雪休雌。 梳的紋絲不亂的頭發(fā)上肝断,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天胸懈,我揣著相機與錄音,去河邊找鬼涌献。 笑死首有,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的卜壕。 我是一名探鬼主播烙常,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼轮蜕!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起率触,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤葱蝗,失蹤者是張志新(化名)和其女友劉穎细燎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體悼凑,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡璧瞬,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年嗤锉,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片奥额。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡垫挨,死狀恐怖触菜,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情帚屉,我是刑警寧澤漾峡,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布生逸,位于F島的核電站且预,受9級特大地震影響烙无,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜截酷,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一迂苛、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧就漾,春花似錦念搬、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽骑丸。三九已至通危,卻和暖如春铸豁,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背菊碟。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工节芥, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人逆害。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓头镊,卻偏偏與公主長得像,于是被迫代替她去往敵國和親魄幕。 傳聞我的和親對象是個殘疾皇子相艇,可洞房花燭夜當晚...
    茶點故事閱讀 44,979評論 2 355