聽(tīng)說(shuō)你想寫(xiě)個(gè) Lisp 解釋器 - read

大家好悴了,我是微微笑的蝸牛搏恤,??。

這個(gè)系列的文章湃交,我們將來(lái)動(dòng)手實(shí)現(xiàn)一個(gè)小型的 Lisp 解釋器熟空,使用 Swift 編寫(xiě)。至于寫(xiě)解釋器的緣由呢搞莺,是因?yàn)榍靶┨炜吹揭黄?a target="_blank">國(guó)外的文章息罗,里面比較詳細(xì)的介紹了如何編寫(xiě)自己的 Lisp 解釋器。以前也沒(méi)有弄過(guò)這方面的東西才沧,出于學(xué)習(xí)的目的迈喉,讀完文章后,動(dòng)手實(shí)踐了一番温圆。

說(shuō)起來(lái)挨摸,只讀文章和動(dòng)手寫(xiě)的差別還是相當(dāng)大的。在讀文章時(shí)岁歉,你會(huì)發(fā)現(xiàn)大概思路我都懂了得运,但是細(xì)細(xì)一想,對(duì)有些小細(xì)節(jié)如何實(shí)現(xiàn)還是有點(diǎn)疑惑刨裆;而在實(shí)踐過(guò)程中澈圈,必須得去弄清楚每個(gè)細(xì)節(jié)(不然就是照抄代碼了??),從中會(huì)發(fā)現(xiàn)一些意外并令人驚喜的點(diǎn)帆啃,甚至突然有種熱血沸騰的感覺(jué)瞬女。

比如 lambda 的實(shí)現(xiàn),它是一個(gè)匿名函數(shù)努潘,那如何實(shí)現(xiàn)在聲明后就立即調(diào)用的呢诽偷?這個(gè)點(diǎn)其實(shí)有點(diǎn)困擾我坤学,因?yàn)槲以趯?shí)現(xiàn)代碼中并沒(méi)有看到直接調(diào)用。后來(lái)仔細(xì)一調(diào)試报慕,才發(fā)現(xiàn)其精妙之處深浮。

下面,就讓我們開(kāi)始這段旅程吧~

LISP 簡(jiǎn)介

Lisp 是一種古老的語(yǔ)言眠冈,其全稱為 List Processor飞苇,它是一種函數(shù)式編程語(yǔ)言,該語(yǔ)言的主要數(shù)據(jù)結(jié)構(gòu)就是 list∥贤纾現(xiàn)在由它也衍生出了很多變種布卡,比如 Common Lisp、Scheme 等雇盖。

Lisp 中開(kāi)創(chuàng)了一些先驅(qū)概念忿等,比如 REPL,讀取-求值-循環(huán)輸出崔挖。我們的解釋器就構(gòu)建在該模型之上贸街。

Lisp 的代碼組成很簡(jiǎn)單,只包括 (狸相、)薛匪、字符三種類型。它由原子 atom 或者列表 list 組成脓鹃。

  • 原子:包括符號(hào)和數(shù)值蛋辈,比如 "hello"2将谊。
  • 列表:用 () 表示冷溶,內(nèi)部可有 0~n 個(gè)表達(dá)式,也可以嵌套列表尊浓,比如 "(a b c)"逞频,"(a b (c d))",表達(dá)式之間用空格隔開(kāi)栋齿。

另外苗胀,函數(shù)調(diào)用也是列表形式:

// f 是函數(shù)名,a, b, c 分別是三個(gè)參數(shù)
(f a b c)

接下來(lái)瓦堵,我們會(huì)根據(jù) MicroManual-LISP 的定義來(lái)實(shí)現(xiàn)它基协。不過(guò),也不是全部菇用。先來(lái)看看 Lisp 中定義的操作符澜驮。

操作符

quote

  • 定義:
// 返回 x
(quote x)
  • 說(shuō)明:取出數(shù)據(jù) x。如果 x 是表達(dá)式惋鸥,x 的值無(wú)需計(jì)算杂穷,原樣返回悍缠。

atom

  • 定義:
// 當(dāng) x 是 atom 或者是空 list 時(shí)返回 true;否則返回空表 ()
(atom x)
  • 說(shuō)明:判斷表達(dá)式是否是原子耐量。若是 atom 或者是空列表 ()飞蚓,則返回 true;否則返回空列表 ()廊蜒。

equal

  • 定義:
// 判斷 x趴拧,y 是否相等
(equal x y)
  • 說(shuō)明:判斷兩個(gè)表達(dá)式是否相等。如果相等山叮,則返回 true八堡;若不等,則返回空列表 ()聘芜。
  • 舉例:
// true
(equal a a)

// ()
(equal a ())

car

  • 定義:
// x 是一個(gè)列表,返回第一個(gè)元素
(car x)
  • 說(shuō)明:取出列表中的第一個(gè)元素缝龄,x 必須是列表汰现。
  • 舉例:
// 結(jié)果:x
(car (x y))

cdr

  • 定義
// x 是一個(gè)列表,返回除第一個(gè)元素外叔壤,其余的元素組成的列表瞎饲。也就是剔除掉第一個(gè)元素
(car x)
  • 說(shuō)明:返回列表中除第一個(gè)元素外,其余的元素組成的列表炼绘。注意參數(shù)必須是列表嗅战。
  • 舉例:
// 結(jié)果:(y z)
(car (x y z))

cons

  • 定義:
(cons e list)
  • 說(shuō)明:將元素 elist 組合起來(lái)返回新的列表。注意:第二個(gè)參數(shù)必須是列表俺亮。
  • 舉例:
// (x y z)
(cons (x) (y z))

cond

  • 定義
// p驮捍、e 為表達(dá)式
(cond (p1 e1) ...(pn en))
  • 說(shuō)明:對(duì)參數(shù)中的 p 逐個(gè)求值,直至返回 true脚曾,此時(shí)計(jì)算 e 的值后返回东且。

具體操作為:

  1. 首先對(duì) p1 求值,如果為 true本讥,則計(jì)算 e1 的值返回珊泳;否則繼續(xù)求值 p2。
  2. 若 p2 的值不為 true拷沸,繼續(xù)求值 p3色查,以此類推,直至到 pn撞芍。
  3. 若沒(méi)有滿足條件的 p秧了,則返回空列表 ()。
  • 舉例
// second 
(cond ((equal a b) (quote first)) ((atom a) (quote second)))

計(jì)算步驟如下:

  • 首先對(duì) p0 = (equal a b) 求值序无,兩者不相等示惊,返回 ()好港。
  • 然后繼續(xù)對(duì) p1 = (atom a) 求值,此時(shí)返回 true米罚。那么計(jì)算對(duì)應(yīng)表達(dá)式 e1 = (quote second) 的值钧汹,結(jié)果是 second,然后將其返回录择。

lambda

  • 定義
// v1~vn 是參數(shù)拔莱,它的值會(huì)被 e1~en 替換,e 是函數(shù)體表達(dá)式
((lambda (v1 ... vn) e) e1 ... en)
  • 說(shuō)明:lambda 用于定義匿名函數(shù)隘竭,注意匿名函數(shù)是會(huì)立即執(zhí)行的塘秦。

(v1 ... vn) 是參數(shù),e 是函數(shù)體表達(dá)式动看,e1 ... en 的對(duì)應(yīng)參數(shù)的值尊剔。

在計(jì)算 e 表達(dá)式時(shí),參數(shù)會(huì)被替換為具體的值菱皆,也就是 v1 = e1须误,...,vn = en仇轻。

  • 舉例:

比如下面這個(gè)函數(shù):參數(shù)為 x京痢,函數(shù)體為 (car x),傳入?yún)?shù)值為 (c a b)篷店。

// 結(jié)果 c
((lambda (x) (car x)) (c a b))

將參數(shù)值 (c a b) 帶入 x祭椰,那么最終計(jì)算的表達(dá)式為:(car (c a b)),結(jié)果為 c疲陕。

defun

  • 定義:
// v1~vn 是參數(shù)方淤,e 是函數(shù)體表達(dá)式
(defun test(v1 ... vn) e)
  • 說(shuō)明:表示方法定義,跟 lambda 很類似蹄殃。只不過(guò)它有方法名臣淤,需主動(dòng)調(diào)用。
  • 舉例:

定義 test 方法窃爷,帶有一個(gè)參數(shù) x

(defun test (x) (car x))

調(diào)用 test 方法邑蒋,傳入 (a b)

(test (a b))

REPL

全稱是 Read - Evaluate - Print Loop,讀取-計(jì)算-打印循環(huán)按厘。

其模型如下圖所示:

REPL 模型

我們將按照這個(gè)模型進(jìn)行實(shí)現(xiàn)医吊,其中最重要的兩步為:

  • Read:讀取輸入的代碼,提取 Token逮京,然后解析為抽象語(yǔ)法樹(shù) AST卿堂。
  • Evaluate:計(jì)算表達(dá)式,返回結(jié)果。

這篇文章草描,我們主要來(lái)介紹 Read 的實(shí)現(xiàn)览绿。

Read 又分為兩步:

  • 詞法分析:Tokenize,也稱之為「Token 化」穗慕。將代碼解析為一個(gè)個(gè)的 Token饿敲,輸出 「Token 列表」。

    由于 Lisp 中只有三種符號(hào):(逛绵、)怀各、字符串(字母+數(shù)字),那么 Token 的取值也很簡(jiǎn)單术浪,只包含這三種瓢对。

  • 語(yǔ)法分析:Parse,將上一步得到的 「Token 列表」進(jìn)行結(jié)構(gòu)化胰苏,輸出 AST硕蛹。

數(shù)據(jù)結(jié)構(gòu)定義

由于 Lisp 代碼就是各種表達(dá)式,而它僅僅只有 atom 和 list 兩種類型硕并,且 list 內(nèi)部可嵌套法焰。那么它的數(shù)據(jù)結(jié)構(gòu)相對(duì)簡(jiǎn)單:

// 表達(dá)式定義
public enum SExpr {
        // 原子
    case Atom(String)

        // 列表,可嵌套
    case List([SExpr])
}

Tokenize

識(shí)別輸入代碼鲤孵,將其分割為一個(gè)個(gè)的有效 token。上面我們說(shuō)過(guò)辰如,token 只有三種類型普监,可用枚舉來(lái)進(jìn)行定義。

enum Token {
    // 左括號(hào)
    case pOpen
    
    // 右括號(hào)
    case pClose
    
    // 字符串
    case text(String)
}

而解析 token 的過(guò)程琉兜,只需逐個(gè)遍歷每個(gè)字符進(jìn)行處理凯正。

遍歷過(guò)程中,只會(huì)有如下幾種情形:

  • '('豌蟋,此時(shí)添加 pOpen 到 token 列表廊散。

    但要注意 ( 前面還可能存在字符串,比如 cons(B C)梧疲,此時(shí)需將字符串 cons 作為一個(gè)字符串 token 添加到列表允睹。

    // "cons(B C)",當(dāng)遍歷到 ( 時(shí)幌氮,cons 需作為一個(gè) token
    if tmpText != "" {
        res.append(.text(tmpText))
        tmpText = ""
    }
    
    res.append(.pOpen)
    
  • ')'缭受,此時(shí)添加 pClose 到 token 列表。

    同樣注意字符串處理该互,比如 (B)米者,當(dāng)遇到 ) 時(shí),需添加字符串 B 到列表。

    // "(B)"蔓搞,遍歷到 )胰丁,B 作為一個(gè) token
    if tmpText != "" {
        res.append(.text(tmpText))
        tmpText = ""
    }
    
    res.append(.pClose)
    
  • ' ',空格是分隔 token 的標(biāo)記喂分,多個(gè)空格只會(huì)被看成一個(gè)锦庸。

    當(dāng)遇到空格時(shí),說(shuō)明可能有 token 定義結(jié)束妻顶。這里主要用于處理字符串 token酸员。

    // "A B",遍歷到到 A 后面的空格讳嘱,A 作為一個(gè) token
    if tmpText != "" {
        res.append(.text(tmpText))
        tmpText = ""
    }
    
  • 其他就是字符了幔嗦,逐個(gè)累加字符。在上面三種情況中沥潭,將其添加到 token 列表邀泉。

    tmpText.append(c)
    

經(jīng)過(guò)這幾種情況的處理,我們可以得到一個(gè) token 列表钝鸽。

舉個(gè)例子:"(a b c)"汇恤,得到的 token 列表為:

[.pOpen, .text(a), .text(b), .text(c), .pClose]

Parse

接下來(lái)是進(jìn)行 token 列表的解析,將其結(jié)構(gòu)化為 AST拔恰,其實(shí)也就是轉(zhuǎn)換為上面定義的 SExpr 的結(jié)構(gòu)因谎。

最主要的就是列表左右括號(hào)配對(duì)處理,內(nèi)嵌列表的遞歸處理颜懊。

舉一個(gè)簡(jiǎn)單的例子财岔,比如代碼:"(a b c)",轉(zhuǎn)換為 AST 后河爹,應(yīng)是如下結(jié)構(gòu):

.List([.Atom(a), .Atom(b), .Atom(c)])

如下圖所示:

image

再看一個(gè)嵌套的例子匠璧,比如代碼:"(a (b c))",轉(zhuǎn)換為 AST 后咸这,結(jié)構(gòu)如下:

.List([.Atom(a), .List([.Atom(b), .Atom(c)])])

如下圖所示:

image

那么看起來(lái)規(guī)則挺簡(jiǎn)單的夷恍,遇到 list,將其變?yōu)?.List媳维;遇到 atom酿雪,將其變?yōu)?.Atom。正所謂是侄刽,逢山開(kāi)山执虹,遇水淌水。

總結(jié)一下轉(zhuǎn)換規(guī)則:

  • 對(duì)于列表 list唠梨,轉(zhuǎn)換為 .List(xx)袋励;
  • 對(duì)于原子 atom,轉(zhuǎn)換為 .Atom(xx)

那么如何實(shí)現(xiàn)呢茬故?

在遍歷 token 時(shí)盖灸,只有如下三種情形:

  • 當(dāng)遇到 .pOpen,也就是 ( 時(shí)磺芭,說(shuō)明是列表的開(kāi)始赁炎,初始化一個(gè)空列表 .List([]) 作為父節(jié)點(diǎn),繼續(xù)遞歸處理其后的 token 列表钾腺。

  • 當(dāng)遇到 .pClose徙垫,也就是 ) 時(shí),說(shuō)明是列表的結(jié)束放棒,返回配對(duì) () 組成的表達(dá)式姻报。

    此時(shí)會(huì)回到 .pOpen 后續(xù)邏輯的處理,因?yàn)槭?strong>遞歸函數(shù)的返回间螟。將表達(dá)式添加到「當(dāng)前上下文的父節(jié)點(diǎn)」中吴旋,然后繼續(xù)往后遍歷剩余 token

  • 當(dāng)遇到 text 時(shí)厢破,轉(zhuǎn)換成 .Atom(text)荣瑟,添加到當(dāng)前父節(jié)點(diǎn)即可。

解析過(guò)程如下所示摩泪,其中 parser 表示解析函數(shù)笆焰。

// 入?yún)ⅲ簍oken 列表,父節(jié)點(diǎn)
// 返回參數(shù):剩余 token 列表见坑,子節(jié)點(diǎn)
parser(tokens, parentNode) -> (remainTokens, node)
image

舉個(gè)例子嚷掠,比方說(shuō):"(a (b))"tokenize 之后的結(jié)果為:

[.pOpen, .text(a), .pOpen, .text(b), .pClose]

其解析過(guò)程如下圖所示鳄梅。PS:為了表示方便叠国,圖中直接使用了字符串未檩,而非 token戴尸。

image

這樣我們就得到了 SExpr 的結(jié)構(gòu),圖示如下:

image

到此冤狡,Read 的過(guò)程就結(jié)束啦孙蒙,相關(guān)代碼可查看 github

總結(jié)

這篇文章主要介紹了如何通過(guò)詞法分析生成 token 列表悲雳、解析 token 生成 ast挎峦,最終得到 SExpr 的結(jié)構(gòu)。

下一篇文章我們將介紹如何利用 SExpr 結(jié)構(gòu)進(jìn)行計(jì)算合瓢,也就是 evaluation 的過(guò)程坦胶,敬請(qǐng)期待~

參考資料

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子顿苇,更是在濱河造成了極大的恐慌峭咒,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,839評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件纪岁,死亡現(xiàn)場(chǎng)離奇詭異凑队,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)幔翰,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)漩氨,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人遗增,你說(shuō)我怎么就攤上這事叫惊。” “怎么了贡定?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,116評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵赋访,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我缓待,道長(zhǎng)蚓耽,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,371評(píng)論 1 279
  • 正文 為了忘掉前任旋炒,我火速辦了婚禮步悠,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘瘫镇。我一直安慰自己鼎兽,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布铣除。 她就那樣靜靜地躺著谚咬,像睡著了一般。 火紅的嫁衣襯著肌膚如雪尚粘。 梳的紋絲不亂的頭發(fā)上择卦,一...
    開(kāi)封第一講書(shū)人閱讀 49,111評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音郎嫁,去河邊找鬼秉继。 笑死,一個(gè)胖子當(dāng)著我的面吹牛泽铛,可吹牛的內(nèi)容都是我干的尚辑。 我是一名探鬼主播,決...
    沈念sama閱讀 38,416評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼盔腔,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼杠茬!你這毒婦竟也來(lái)了月褥?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,053評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤瓢喉,失蹤者是張志新(化名)和其女友劉穎吓坚,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體灯荧,經(jīng)...
    沈念sama閱讀 43,558評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡礁击,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了逗载。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片哆窿。...
    茶點(diǎn)故事閱讀 38,117評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖厉斟,靈堂內(nèi)的尸體忽然破棺而出挚躯,到底是詐尸還是另有隱情,我是刑警寧澤擦秽,帶...
    沈念sama閱讀 33,756評(píng)論 4 324
  • 正文 年R本政府宣布码荔,位于F島的核電站,受9級(jí)特大地震影響感挥,放射性物質(zhì)發(fā)生泄漏缩搅。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評(píng)論 3 307
  • 文/蒙蒙 一触幼、第九天 我趴在偏房一處隱蔽的房頂上張望硼瓣。 院中可真熱鬧,春花似錦置谦、人聲如沸堂鲤。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,315評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)瘟栖。三九已至,卻和暖如春谅阿,著一層夾襖步出監(jiān)牢的瞬間半哟,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,539評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工奔穿, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留镜沽,地道東北人敏晤。 一個(gè)月前我還...
    沈念sama閱讀 45,578評(píng)論 2 355
  • 正文 我出身青樓贱田,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親嘴脾。 傳聞我的和親對(duì)象是個(gè)殘疾皇子男摧,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容