什么是語(yǔ)法樹(shù)?
你是否曾想過(guò),這個(gè)世界存在這么多語(yǔ)言的意義。
假如現(xiàn)在你面前有一個(gè)物體,它是一個(gè)不規(guī)則的圓體,整個(gè)身體通紅,頭部還有一根細(xì)長(zhǎng)稍微彎曲偏右呈棕色的圓柱體。
在中文我們稱之為「蘋(píng)果」,
在英文我們稱之為「Apple」,
在日文中我們稱之為「アップル」,
在法語(yǔ)中我們稱之為「pomme」,
在德語(yǔ)中我們稱之為「Apfel」,
無(wú)論用不同的語(yǔ)言,針對(duì)這個(gè)物體在文字上、發(fā)音上都完全不一樣,但這個(gè)物體確確實(shí)實(shí)的存在這個(gè)時(shí)空上,顏色、氣味、形狀都不曾因?yàn)檎Z(yǔ)言而改變過(guò)。
無(wú)論這個(gè)世界存在多少語(yǔ)言,它們所描述的真理都不曾改變過(guò)。
或者說(shuō),真理就存在那里,可以用不同的語(yǔ)言的不同表達(dá)方式描述出來(lái)。那么計(jì)算機(jī)的世界,這么多編程的語(yǔ)言,C、C++、Java、C#、JavaScript、Python、Go、Ruby等等等,它們共同所描述的真理是什么?
我們知道人類語(yǔ)言上,無(wú)論什么語(yǔ)種,都會(huì)有「主語(yǔ)」「動(dòng)詞」「賓語(yǔ)」「標(biāo)點(diǎn)符號(hào)」來(lái)描述一個(gè)現(xiàn)實(shí)世界所發(fā)生的事件。
而在計(jì)算機(jī)編程語(yǔ)言上,無(wú)論什么語(yǔ)種,都會(huì)有「類型」「運(yùn)算符」「流程語(yǔ)句」「函數(shù)」「對(duì)象」等概念來(lái)表達(dá)計(jì)算機(jī)中存在內(nèi)存中的0和1,以及背后運(yùn)算與邏輯。
語(yǔ)法樹(shù),計(jì)算機(jī)描述世界真理的樹(shù)狀結(jié)構(gòu)。
不同的語(yǔ)言,都會(huì)配之不同的語(yǔ)法分析器,而語(yǔ)法分析器是把源代碼作為字符串讀入、解析,并建立語(yǔ)法樹(shù)的程序。語(yǔ)法的設(shè)計(jì)和語(yǔ)法分析器的實(shí)現(xiàn)是決定語(yǔ)言外在表現(xiàn)的重要因素。
什么是語(yǔ)法樹(shù)?摘自Wiki一段:
在計(jì)算機(jī)科學(xué)中,抽象語(yǔ)法樹(shù)(abstract syntax tree 或者縮寫(xiě)為 AST),或者語(yǔ)法樹(shù)(syntax tree),是源代碼的抽象語(yǔ)法結(jié)構(gòu)的樹(shù)狀表現(xiàn)形式,這里特指編程語(yǔ)言的源代碼。樹(shù)上的每個(gè)節(jié)點(diǎn)都表示源代碼中的一種結(jié)構(gòu)。之所以說(shuō)語(yǔ)法是「抽象」的,是因?yàn)檫@里的語(yǔ)法并不會(huì)表示出真實(shí)語(yǔ)法中出現(xiàn)的每個(gè)細(xì)節(jié)。
一則簡(jiǎn)單的例子
如果我們需要讓計(jì)算機(jī)幫忙算一下 「1加2再乘以3」 的結(jié)果,該怎么表達(dá)呢?
現(xiàn)在我們大多數(shù)的現(xiàn)代編程語(yǔ)言,都是使用「中綴表達(dá)式」的方式來(lái)編寫(xiě)運(yùn)算,比如JavaScript:
(1 + 2) * 3
而FORTH語(yǔ)言則使用「后綴表達(dá)式」,這基本上與日語(yǔ)中的語(yǔ)序是一致的:
1 2 + 3 *
LISP語(yǔ)言使用的「前綴表達(dá)式」:
( * (+ 1 2) 3)
我們?cè)倏匆幌逻@三種表達(dá)式的語(yǔ)法樹(shù):

可以看出,對(duì)于這三種簡(jiǎn)單的語(yǔ)言,它們只是在這個(gè)語(yǔ)法樹(shù)上按不同的規(guī)則遍歷而已。三者的代碼看起來(lái)差別很大,但實(shí)際上所用的樹(shù)結(jié)構(gòu)是相同的。
先來(lái)看看Python的語(yǔ)法樹(shù)
通過(guò)Python語(yǔ)言自帶的庫(kù)文件ast,我們可以查看特定的代碼被轉(zhuǎn)換成怎樣的語(yǔ)法樹(shù)。
>>> import ast
>>> ast.dump(ast.parse("(1 + 2) * 3"))
'Module(
body=[
Expr(
value=BinOp(
left=BinOp(
left=Num(n=1),
op=Add(),
right=Num(n=2)
),
op=Mult(),
right=Num(n=3)
)
)
]
)'
BinOp op = Mult()表示乘法運(yùn)算,與*相對(duì)應(yīng);
BinOp op = Add()表示加法運(yùn)算,與+相對(duì)應(yīng);
Num n = 1既為數(shù)值1。

再窺視一下JavaScript的語(yǔ)法樹(shù)
在語(yǔ)法復(fù)雜的語(yǔ)言中,語(yǔ)法樹(shù)是包含很多細(xì)節(jié)的語(yǔ)法結(jié)果表達(dá)式,我們需要靠語(yǔ)法樹(shù)把這種形式以更簡(jiǎn)潔的形式表達(dá)出來(lái)。
Javascript 有不少工具可以把代碼構(gòu)造出清晰的語(yǔ)法樹(shù),比如 esprima、v8、SpiderMonkey、UglifyJS、AST explorer等。
這里,我使用「esprima」來(lái)探討一下JavaScript運(yùn)算(1 + 2) * 3的語(yǔ)法樹(shù)。
javascript code:
(1 + 2)* 3;
ast for json:
{
"type": "Program",
"body": [
{
"type": "ExpressionStatement",
"expression": {
"type": "BinaryExpression",
"operator": "*",
"left": {
"type": "BinaryExpression",
"operator": "+",
"left": {
"type": "Literal",
"value": 1,
"raw": "1"
},
"right": {
"type": "Literal",
"value": 2,
"raw": "2"
}
},
"right": {
"type": "Literal",
"value": 3,
"raw": "3"
}
}
}
],
"sourceType": "script"
}
body表示程序體,而程序體中包含了一則表達(dá)式ExpressionStatement, 表達(dá)式體里包含了操作符 *,以及左右兩邊表達(dá)式,其中右邊是數(shù)字3,而左邊表達(dá)式還包含一層表達(dá)式,里面是一個(gè)+ 操作符,以及左右兩邊分別為1和2的數(shù)字。

如果還沒(méi)有看懂,你可以到這里看一下這段代碼所生成的語(yǔ)法樹(shù):AST for (1 + 2)* 3;
我們可以利用語(yǔ)法樹(shù)做些什么?
看到這里你可能會(huì)問(wèn),知道語(yǔ)法是又有什么用呢?跟我日常編寫(xiě)代碼貌似半毛錢(qián)關(guān)系都沒(méi)有。其實(shí)語(yǔ)法樹(shù)還是很有用的,想一下如果想做「語(yǔ)法高亮」、「關(guān)鍵字匹配」、「作用域判斷」、以及「代碼壓縮」等等,都是最好把代碼解構(gòu)成語(yǔ)法樹(shù)之后再去各種操作,當(dāng)然僅僅解構(gòu)還不夠,還需要提供各種函數(shù)去遍歷與修改語(yǔ)法樹(shù)。
另一方面,去研究、去探討計(jì)算機(jī)真實(shí)的世界不是一個(gè)很精彩很刺激的過(guò)程么?