詞法分析和語法分析是同一個階段。
詞法分析
源代碼在計(jì)算機(jī)『眼中』其實(shí)是一團(tuán)亂麻熏纯,一個由字符組成的桃熄、無法被理解的字符串,所有的字符在計(jì)算器看來并沒有什么區(qū)別恨锚,為了理解這些字符我們需要做的第一件事情就是將字符串分組宇驾,這能夠降低理解字符串的成本,簡化源代碼的分析過程猴伶。
make(chan int)
哪怕是不懂編程的人看到上述文本的第一反應(yīng)也應(yīng)該會將上述字符串分成幾個部分 - make
课舍、chan
、int
和括號他挎,這個憑直覺分解文本的過程就是詞法分析筝尾,詞法分析是將字符序列轉(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)制:
到這里我們已經(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詞法