詞法分析*

詞法分析和語法分析是同一個階段。

詞法分析

源代碼在計(jì)算機(jī)『眼中』其實(shí)是一團(tuán)亂麻熏纯,一個由字符組成的桃熄、無法被理解的字符串,所有的字符在計(jì)算器看來并沒有什么區(qū)別恨锚,為了理解這些字符我們需要做的第一件事情就是將字符串分組宇驾,這能夠降低理解字符串的成本,簡化源代碼的分析過程猴伶。

make(chan int)

哪怕是不懂編程的人看到上述文本的第一反應(yīng)也應(yīng)該會將上述字符串分成幾個部分 - make课舍、chanint 和括號他挎,這個憑直覺分解文本的過程就是詞法分析筝尾,詞法分析是將字符序列轉(zhuǎn)換為標(biāo)記(token)序列的過程。

lex(詞法生成器)

lex 是用于生成詞法分析器的工具办桨,lex 生成的代碼能夠?qū)⒁粋€文件中的字符分解成 Token 序列筹淫,很多語言在設(shè)計(jì)早期都會使用它快速設(shè)計(jì)出原型。詞法分析作為具有固定模式的任務(wù)呢撞,出現(xiàn)這種更抽象的工具必然的损姜,lex 作為一個代碼生成器,使用了類似 C 語言的語法殊霞,我們將 lex 理解為正則匹配的生成器摧阅,它會使用正則匹配掃描輸入的字符流,下面是一個 lex 文件的示例:

%{
#include <stdio.h>
%}

%%
package      printf("PACKAGE ");
import       printf("IMPORT ");
\.           printf("DOT ");
\{           printf("LBRACE ");
\}           printf("RBRACE ");
\(           printf("LPAREN ");
\)           printf("RPAREN ");
\"           printf("QUOTE ");
\n           printf("\n");
[0-9]+       printf("NUMBER ");
[a-zA-Z_]+   printf("IDENT ");
%%

這個定義好的文件能夠解析 package 和 import 關(guān)鍵字绷蹲、常見的特殊字符棒卷、數(shù)字以及標(biāo)識符,雖然這里的規(guī)則可能有一些簡陋和不完善瘸右,但是用來解析下面的這一段代碼還是比較輕松的:

package main

import (
    "fmt"
)

func main() {
    fmt.Println("Hello")
}

.l 結(jié)尾的 lex 代碼并不能直接運(yùn)行娇跟,我們首先需要通過 lex 命令將上面的 simplego.l 展開成 C 語言代碼,這里可以直接執(zhí)行如下所示的命令編譯并打印文件中的內(nèi)容:

$ lex simplego.l
$ cat lex.yy.c
...
int yylex (void) {
    ...
    while ( 1 ) {
        ...
yy_match:
        do {
            register YY_CHAR yy_c = yy_ec[YY_SC_TO_UI(*yy_cp)];
            if ( yy_accept[yy_current_state] ) {
                (yy_last_accepting_state) = yy_current_state;
                (yy_last_accepting_cpos) = yy_cp;
            }
            while ( yy_chk[yy_base[yy_current_state] + yy_c] != yy_current_state ) {
                yy_current_state = (int) yy_def[yy_current_state];
                if ( yy_current_state >= 30 )
                    yy_c = yy_meta[(unsigned int) yy_c];
                }
            yy_current_state = yy_nxt[yy_base[yy_current_state] + (unsigned int) yy_c];
            ++yy_cp;
        } while ( yy_base[yy_current_state] != 37 );
        ...

do_action:
        switch ( yy_act )
            case 0:
                ...

            case 1:
                YY_RULE_SETUP
                printf("PACKAGE ");
                YY_BREAK
            ...
}

lex.yy.c的前 600 行基本都是宏和函數(shù)的聲明和定義太颤,后面生成的代碼大都為 yylex 這個函數(shù)服務(wù)的苞俘,這個函數(shù)使用 有限自動機(jī)(Deterministic Finite Automaton、DFA)的程序結(jié)構(gòu)來分析輸入的字符流龄章,上述代碼中 while 循環(huán)就是這個有限自動機(jī)的主體吃谣,你如果仔細(xì)看這個文件生成的代碼會發(fā)現(xiàn)當(dāng)前的文件中并不存在 main 函數(shù)乞封,main 函數(shù)是在 liblex 庫中定義的,所以在編譯時其實(shí)需要添加額外的 -ll 選項(xiàng):

$ cc lex.yy.c -o simplego -ll
$ cat main.go | ./simplego

當(dāng)我們將 C 語言代碼通過 gcc 編譯成二進(jìn)制代碼之后岗憋,就可以使用管道將上面提到的 Go 語言代碼作為輸入傳遞到生成的詞法分析器中肃晚,這個詞法分析器會打印出如下的內(nèi)容:

PACKAGE  IDENT

IMPORT  LPAREN
    QUOTE IDENT QUOTE
RPAREN

IDENT  IDENT LPAREN RPAREN  LBRACE
    IDENT DOT IDENT LPAREN QUOTE IDENT QUOTE RPAREN
RBRACE

從上面的輸出我們能夠看到 Go 源代碼的影子,lex 生成的詞法分析器 lexer 通過正則匹配的方式將機(jī)器原本很難理解的字符串進(jìn)行分解成很多的 Token仔戈,有利于后面的處理关串。
從 .l 文件到二進(jìn)制:


image.png

到這里我們已經(jīng)為各位讀者展示了從定義 .l 文件、使用 lex 將 .l 文件編譯成 C 語言代碼以及二進(jìn)制的全過程监徘,而最后生成的詞法分析器也能夠?qū)⒑唵蔚?Go 語言代碼進(jìn)行轉(zhuǎn)換成 Token 序列晋修。

Go

Go 語言的詞法解析是通過 src/cmd/compile/internal/syntax/scanner.go文件中的 cmd/compile/internal/syntax.scanner結(jié)構(gòu)體實(shí)現(xiàn)的,這個結(jié)構(gòu)體會持有當(dāng)前掃描的數(shù)據(jù)源文件凰盔、啟用的模式和當(dāng)前被掃描到的 Token:

type scanner struct {
    source
    mode   uint
    nlsemi bool

    // current token, valid after calling next()
    line, col uint
    blank     bool // line is blank up to col
    tok       token
    lit       string   // valid if tok is _Name, _Literal, or _Semi ("semicolon", "newline", or "EOF"); may be malformed if bad is true
    bad       bool     // valid if tok is _Literal, true if a syntax error occurred, lit may be malformed
    kind      LitKind  // valid if tok is _Literal
    op        Operator // valid if tok is _Operator, _AssignOp, or _IncOp
    prec      int      // valid if tok is _Operator, _AssignOp, or _IncOp
}

src/cmd/compile/internal/syntax/tokens.go 文件中定義了 Go 語言中支持的全部 Token 類型墓卦,所有的 token 類型都是正整數(shù),你可以在這個文件中找到一些常見 Token 的定義户敬,例如:操作符落剪、括號和關(guān)鍵字等:

const (
    _    token = iota
    _EOF       // EOF

    // operators and operations
    _Operator // op
    ...

    // delimiters
    _Lparen    // (
    _Lbrack    // [
    ...

    // keywords
    _Break       // break
    ...
    _Type        // type
    _Var         // var

    tokenCount //
)

從 Go 語言中定義的 Token 類型,我們可以將語言中的元素分成幾個不同的類別尿庐,分別是名稱和字面量忠怖、操作符、分隔符和關(guān)鍵字抄瑟。詞法分析主要是由 cmd/compile/internal/syntax.scanner 這個結(jié)構(gòu)體中的 cmd/compile/internal/syntax.scanner.next 方法驅(qū)動脑又,這個 250 行函數(shù)的主體是一個 switch/case 結(jié)構(gòu):

func (s *scanner) next() {
    ...
    s.stop()
    startLine, startCol := s.pos()
    for s.ch == ' ' || s.ch == '\t' || s.ch == '\n' && !nlsemi || s.ch == '\r' {
        s.nextch()
    }

    s.line, s.col = s.pos()
    s.blank = s.line > startLine || startCol == colbase
    s.start()
    if isLetter(s.ch) || s.ch >= utf8.RuneSelf && s.atIdentChar(true) {
        s.nextch()
        s.ident()
        return
    }

    switch s.ch {
    case -1:
        s.tok = _EOF

    case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
        s.number(false)
    ...
    }
}

cmd/compile/internal/syntax.scanner每次都會通過 cmd/compile/internal/syntax.source.nextch函數(shù)獲取文件中最近的未被解析的字符,然后根據(jù)當(dāng)前字符的不同執(zhí)行不同的 case锐借,如果遇到了空格和換行符這些空白字符會直接跳過,如果當(dāng)前字符是 0 就會執(zhí)行 cmd/compile/internal/syntax.scanner.number方法嘗試匹配一個數(shù)字往衷。

func (s *scanner) number(seenPoint bool) {
    kind := IntLit
    base := 10        // number base
    digsep := 0
    invalid := -1     // index of invalid digit in literal, or < 0

    s.kind = IntLit
    if !seenPoint {
        digsep |= s.digits(base, &invalid)
    }

    s.setLit(kind, ok)
}

func (s *scanner) digits(base int, invalid *int) (digsep int) {
    max := rune('0' + base)
    for isDecimal(s.ch) || s.ch == '_' {
        ds := 1
        if s.ch == '_' {
            ds = 2
        } else if s.ch >= max && *invalid < 0 {
            _, col := s.pos()
            *invalid = int(col - s.col) // record invalid rune index
        }
        digsep |= ds
        s.nextch()
    }
    return
}

上述的 cmd/compile/internal/syntax.scanner.number 方法省略了很多的代碼钞翔,包括如何匹配浮點(diǎn)數(shù)、指數(shù)和復(fù)數(shù)席舍,我們只是簡單看一下詞法分析匹配整數(shù)的邏輯:在 for 循環(huán)中不斷獲取最新的字符布轿,將字符通過 cmd/compile/internal/syntax.source.nextch 方法追加到 cmd/compile/internal/syntax.scanner持有的緩沖區(qū)中;
當(dāng)前包中的詞法分析器 cmd/compile/internal/syntax.scanner也只是為上層提供了 cmd/compile/internal/syntax.scanner.next方法来颤,詞法解析的過程都是惰性的汰扭,只有在上層的解析器需要時才會調(diào)用 cmd/compile/internal/syntax.scanner.next 獲取最新的 Token。
Go 語言的詞法元素相對來說還是比較簡單福铅,使用這種巨大的 switch/case 進(jìn)行詞法解析也比較方便和順手萝毛,早期的 Go 語言雖然使用 lex 這種工具來生成詞法解析器,但是最后還是使用 Go 來實(shí)現(xiàn)詞法分析器滑黔,用自己寫的詞法分析器來解析自己笆包。

本文要點(diǎn):lex环揽、有限自動機(jī)、Go詞法

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
禁止轉(zhuǎn)載庵佣,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者歉胶。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市巴粪,隨后出現(xiàn)的幾起案子通今,更是在濱河造成了極大的恐慌,老刑警劉巖肛根,帶你破解...
    沈念sama閱讀 219,490評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件辫塌,死亡現(xiàn)場離奇詭異,居然都是意外死亡晶通,警方通過查閱死者的電腦和手機(jī)璃氢,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來狮辽,“玉大人一也,你說我怎么就攤上這事『聿保” “怎么了椰苟?”我有些...
    開封第一講書人閱讀 165,830評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長树叽。 經(jīng)常有香客問我舆蝴,道長,這世上最難降的妖魔是什么题诵? 我笑而不...
    開封第一講書人閱讀 58,957評論 1 295
  • 正文 為了忘掉前任洁仗,我火速辦了婚禮,結(jié)果婚禮上性锭,老公的妹妹穿的比我還像新娘赠潦。我一直安慰自己,他們只是感情好草冈,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,974評論 6 393
  • 文/花漫 我一把揭開白布她奥。 她就那樣靜靜地躺著,像睡著了一般怎棱。 火紅的嫁衣襯著肌膚如雪哩俭。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,754評論 1 307
  • 那天拳恋,我揣著相機(jī)與錄音凡资,去河邊找鬼。 笑死诅岩,一個胖子當(dāng)著我的面吹牛讳苦,可吹牛的內(nèi)容都是我干的带膜。 我是一名探鬼主播,決...
    沈念sama閱讀 40,464評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼鸳谜,長吁一口氣:“原來是場噩夢啊……” “哼膝藕!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起咐扭,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤芭挽,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后蝗肪,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體袜爪,經(jīng)...
    沈念sama閱讀 45,847評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,995評論 3 338
  • 正文 我和宋清朗相戀三年薛闪,在試婚紗的時候發(fā)現(xiàn)自己被綠了辛馆。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,137評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡豁延,死狀恐怖昙篙,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情诱咏,我是刑警寧澤苔可,帶...
    沈念sama閱讀 35,819評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站袋狞,受9級特大地震影響焚辅,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜苟鸯,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,482評論 3 331
  • 文/蒙蒙 一同蜻、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧早处,春花似錦埃仪、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,023評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽颁股。三九已至么库,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間甘有,已是汗流浹背诉儒。 一陣腳步聲響...
    開封第一講書人閱讀 33,149評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留亏掀,地道東北人忱反。 一個月前我還...
    沈念sama閱讀 48,409評論 3 373
  • 正文 我出身青樓泛释,卻偏偏與公主長得像,于是被迫代替她去往敵國和親温算。 傳聞我的和親對象是個殘疾皇子怜校,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,086評論 2 355

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