500 lines or less 是對開源程序架構(gòu)中一些典型過程進行講解的系列文章,github英文項目地址:https://github.com/aosabook/500lines。在看的過程中發(fā)現(xiàn)github上已有中文翻譯項目
蓖租,項目中還沒有 static analysis 一文的翻譯,因此嘗試了一下(已PR翻譯項目)楔壤,不足之處還請不吝指教氓扛。
標題:靜態(tài)分析
作者:Leah Hanson
Leah Hanson是令Hacker School感到自豪的校友瀑晒,而且喜歡幫助人們了解Julia語言算墨。她的博客:http://blog.leahhanson.us/宵荒,以及推特:@astrieanna。
介紹
你可能對一些精致的IDE感到熟悉米同,它們會將你無法編譯的部分代碼劃上紅色下劃線骇扇。你可能在你的代碼上運行了一個代碼檢查工具來檢查格式或樣式問題。你可能在打開了所有警告面粮,在超級挑剔的模式下運行著編譯器。所有這些工具都應(yīng)用了靜態(tài)分析继低。
靜態(tài)分析是一種在不運行代碼的情況下檢查其中問題的方法熬苍。 “靜態(tài)”的意思是在編譯時而非運行時,“分析”則意味著我們正在分析代碼袁翁。當你使用我上面提到的工具時柴底,它可能感覺像是魔術(shù)。但這些工具只是程序——它們是由一個人(像你這樣的程序員)編寫的源代碼構(gòu)成的粱胜。在這個章節(jié)中柄驻,我們將討論如何實現(xiàn)一些靜態(tài)分析檢查。為了做到這一點焙压,我們需要知道我們希望通過檢查實現(xiàn)什么鸿脓,以及我們要怎樣完成檢查。
通過將所有流程分三個階段陳述涯曲,我們可以更加具體地了解你需要了解的內(nèi)容:
1. 決定你要檢查的內(nèi)容野哭。
你要能用讓該編程語言的用戶能夠識別的方式,解釋你想要解決的一般問題幻件。例子包括:
- 找出拼寫錯誤的變量名稱
- 找出在并行代碼中存在的競爭
- 找出對未實現(xiàn)的函數(shù)的調(diào)用
2. 決定具體如何去檢查拨黔。
雖然我們可以要求一個朋友完成上面列出的任務(wù)之一,但他們?nèi)詿o法向計算機解釋得足夠清楚绰沥。例如篱蝇,要解決“找出拼寫錯誤的變量名稱”這個問題時贺待,我們需要定義“拼寫錯誤”在此處的含義。一種辦法是提倡變量名應(yīng)該由字典中的英文單詞組成零截;另一個辦法是查找僅使用過一次的變量(就是你輸錯的那一次)狠持。
如果我們知道我們正在尋找僅使用過一次的變量,我們可以討論各種變量用法(將其值分配或讀日叭蟆)以及哪些代碼會喘垂、或不會觸發(fā)警告。
3. 實施細節(jié)。
這包括真正去編寫代碼的行為,閱讀你所使用的庫的文檔所花費的時間圈驼,以及弄清楚如何獲取你所需的信息來編寫分析尤莺。這可能涉及讀取代碼文件,解析代碼以理解結(jié)構(gòu)备埃,然后對該結(jié)構(gòu)進行特定的檢查。
對于本章中實施的每項檢查,我們將逐項完成這些步驟鸭限。第1步需要充分了解我們正在分析的語言,以理解其用戶所面臨的問題两踏。本章將全部使用Julia代碼編寫败京,同時也用來分析Julia代碼。
Julia語言簡介
Julia是一門針對技術(shù)計算的年輕語言梦染。它于2012年春季發(fā)布于0.1版赡麦;截至2015年初,它的版本號已經(jīng)升到了0.3帕识。一般來說泛粹,Julia看起來很像Python,但多了一些可選的類型注釋肮疗,且完全沒有任何面向?qū)ο蟮臇|西晶姊。大多數(shù)程序員會對Julia的多次調(diào)度特性感到新奇,這對API設(shè)計和語言中的其他設(shè)計選擇都有著普遍的影響伪货。
這是Julia代碼的片段:
# 關(guān)于increment的一段注釋
function increment(x::Int64)
return x + 1
end
increment(5)
這段代碼定義了increment
函數(shù)的一個方法们衙,該方法接受一個名為x
、類型為Int64
的參數(shù)超歌。該方法返回x + 1
的值砍艾。接著,使用參數(shù)5
去調(diào)用這個剛剛定義的方法巍举;正如你可能已經(jīng)猜到的那樣脆荷,這次函數(shù)調(diào)用求得了6
。
Int64
是在內(nèi)存中以64位表示的帶符號的整數(shù)類型;如果你的計算機具有64位處理器蜓谋,那么它們是你的硬件能夠理解的整數(shù)梦皮。除了影響方法調(diào)度之外,Julia中的類型定義了數(shù)據(jù)在內(nèi)存中表示形式桃焕。
名稱increment
指的是一個一般函數(shù)剑肯,這個函數(shù)可能有許多方法。我們剛剛為它定義了一種方法观堂。在許多語言中让网,術(shù)語“函數(shù)”和“方法”可互換指代;但在Julia里师痕,他們有不同的含義溃睹。如果你細心地將“函數(shù)”理解為一個眾多方法的命名集合,其中“方法”是特定類型簽名的特定實現(xiàn)胰坟,那么本章將更好理解因篇。
讓我們定義increment
函數(shù)的另一個方法:
# 使x增加y
function increment(x::Int64, y::Number)
return x + y
end
increment(5) # =\> 6
increment(5,4) # =\> 9
現(xiàn)在函數(shù)increment
有了兩種方法。Julia根據(jù)參數(shù)的數(shù)量和類型決定為指定的調(diào)用運行哪個方法笔横;這稱為動態(tài)多次調(diào)度 :
- 動態(tài)是指它基于運行時使用的值的類型竞滓。
- 多次是指它查看所有參數(shù)的類型和順序。
- 調(diào)度是指這是一種將函數(shù)調(diào)用和方法定義匹配起來的辦法吹缔。
用你可能已經(jīng)了解的語言環(huán)境來舉例商佑,面向?qū)ο笳Z言使用單次調(diào)度,因為它們只考慮第一個參數(shù)涛菠。(在x.foo(y)
中 莉御,第一個參數(shù)是x
。)
單次和多次調(diào)度都基于參數(shù)的類型俗冻。上面的x::Int64
是一個純粹用于調(diào)度的類型注釋。在Julia的動態(tài)類型系統(tǒng)中牍颈,你可以在函數(shù)中為x
分配任何類型的值而不會出錯迄薄。
我們還沒有真正看到“多次”的部分,但如果你對Julia很好奇煮岁,你就必須得自己查查看了讥蔽。我們要繼續(xù)我們的第一個檢查了。
檢查循環(huán)中的變量類型
與大多數(shù)編程語言一樣画机,在Julia中編寫非骋鄙。快速的代碼需要了解計算機和Julia的工作原理。幫助編譯器為你創(chuàng)建快速代碼的一個重要部分是編寫類型穩(wěn)定的代碼步氏;這在Julia和JavaScript中很重要响禽,在其他JIT的語言中也很有用。相對于編譯器認為某個變量存在多種可能的類型(無論正確與否)的情況,當編譯器明白代碼段中的某個變量將始終包含相同的特定類型時芋类,編譯器可以完成更多優(yōu)化工作隆嗅。有關(guān)為什么類型穩(wěn)定性(也稱為“單態(tài)”)對于JavaScript重要的原因,你可以 在線閱讀侯繁,了解更多胖喳。
為什么這很重要
讓我們編寫一個函數(shù),它接受Int64
并將其增大一些贮竟。如果數(shù)字比較欣龊浮(小于10),我們將它加上一個大數(shù)字(50)咕别,但如果數(shù)字很大技健,那么我們只增加0.5。
function increment(x::Int64)
if x < 10
x = x + 50
else
x = x + 0.5
end
return x
end
這個函數(shù)看起來非常簡單顷级,但x
的類型是不穩(wěn)定的凫乖。我選擇了兩個數(shù)字:一個Int64
類型:50,和一個Float64
類型:0.5弓颈。取決于x
的值帽芽,它可能會和兩者中的任何一個相加。如果你將例如22的Int64
和例如0.5這樣的Float64
相加 翔冀,你會得到一個Float64
類型數(shù)據(jù)(22.5)导街。因為函數(shù)(x
)的變量類型會根據(jù)傳給函數(shù)(x
)的參數(shù)變化,increment
的這一方法纤子,尤其是變量x
搬瑰,是類型不穩(wěn)定的。
Float64
是一種表示以64位存儲的浮點值的類型;在C語言中控硼,它被稱為雙精度浮點型(double
) 泽论。這是64位處理器理解的浮點類型之一。
與大多數(shù)效率問題一樣卡乾,這個問題在循環(huán)中發(fā)生時將更加明顯翼悴。for和while循環(huán)中的代碼會運行很多、很多次幔妨,所以讓循環(huán)快速運行鹦赎,要比讓僅運行一兩次的代碼加快速度更加重要。因此误堡,我們的第一個檢查是查找循環(huán)中具有不穩(wěn)定類型的變量古话。
首先,讓我們看一下我們想要捕捉的例子锁施。我們將查看兩個函數(shù)陪踩。兩個函數(shù)都從1加到100杖们,但是它們不是對整數(shù)進行求和,而是在求和之前先將每個數(shù)除以2膊毁。兩個函數(shù)都會得到相同的答案(2525.0)胀莹;兩者都將返回相同的類型( Float64
)。然而婚温,第一個函數(shù):unstable
描焰,受到類型不穩(wěn)定的影響,而第二個函數(shù): stable
栅螟,則不會荆秦。
function unstable()
sum = 0
for i=1:100
sum += i/2
end
return sum
end
function stable()
sum = 0.0
for i=1:100
sum += i/2
end
return sum
end
兩個函數(shù)之間唯一字面上的差異在于sum
的初始化: sum = 0
和sum = 0.0
。在Julia中力图,從字面上來說步绸, 0
是Int64
類型, 而0.0
則是Float64
類型吃媒。這個微小的變化能造成多大差別瓤介?
由于Julia是實時(JIT)編譯的,因此第一次運行函數(shù)所需的時間比該函數(shù)后續(xù)運行的時間長赘那。(第一次運行包括為這些參數(shù)類型編譯函數(shù)所花費的時間刑桑。)當我們對函數(shù)進行基準測試時,我們必須確保在對它們進行計時之前先運行它們一次(或者把它們預(yù)編譯好)募舟。
julia> unstable()
2525.0
julia> stable()
2525.0
julia> @time unstable()
elapsed time: 9.517e-6 seconds (3248 bytes allocated)
2525.0
julia> @time stable()
elapsed time: 2.285e-6 seconds (64 bytes allocated)
2525.0
@time
宏打印出函數(shù)運行的時間以及運行時分配的字節(jié)數(shù)祠斧。每次需要新內(nèi)存時,分配的字節(jié)數(shù)就會增加拱礁;即便垃圾回收機制清理不再使用的內(nèi)存時琢锋,它也不會減少。也就是說呢灶,分配的字節(jié)數(shù)與我們分配和管理內(nèi)存所花費的時間有關(guān)吴超,并不表示我們在同一時刻使用了所有這些內(nèi)存。
如果我們想要獲得關(guān)于stable
與unstable
更有力的對比數(shù)據(jù)鸯乃,我們需要更長的循環(huán)或更多次地運行函數(shù)烛芬。然而,看起來unstable
可能更慢飒责。更有趣的是,我們可以發(fā)現(xiàn)分配的字節(jié)數(shù)有很大差距;仆潮。unstable
分配了大約3 KB的內(nèi)存宏蛉,而stable
僅使用64字節(jié)。
既然我們明白unstable
是多么簡單性置,我們會去猜想這種分配是在循環(huán)中發(fā)生的拾并。為了測試這一點,我們可以使循環(huán)更長,并查看分配是否相應(yīng)地增加嗅义。把循環(huán)改成從1到10000屏歹,是原先迭代次數(shù)的100倍。我們所期望看到的是分配的字節(jié)數(shù)也增加約100倍之碗,達到約300 KB蝙眶。
function unstable()
sum = 0
for i=1:10000
sum += i/2
end
return sum
end
由于我們重新定義了函數(shù),因此我們需要運行它褪那,使其在測量之前完成編譯幽纷。我們期望從新的函數(shù)定義中得到一個不同的、更大的答案博敬,因為它現(xiàn)在對更多的數(shù)字進行了求和運算友浸。
julia> unstable()
2.50025e7
julia>@time unstable()
elapsed time: 0.000667613 seconds (320048 bytes allocated)
2.50025e7
新的unstable
分配了大約320 KB內(nèi)存,這符合我們對于“內(nèi)存分配在循環(huán)中發(fā)生”這一期望偏窝。為了解釋這里發(fā)生了什么收恢,我們將看看Julia在幕后是如何工作的。
unstable
和stable
之間的差異是因為unstable
的sum
必須進行裝箱轉(zhuǎn)換祭往,而stable
中的sum
可以不必如此伦意。裝箱值由類型標簽和表示該值的實際比特位組成;而拆箱值只含有實際比特位链沼。但是類型標簽很小默赂,所以這不是裝箱值分配更多內(nèi)存的原因。
真正的差異來自編譯器可以進行的優(yōu)化括勺。當變量具有具體的缆八、不可變類型時,編譯器可以在函數(shù)內(nèi)將它拆箱疾捍。如果不是這種情況奈辰,則必須在堆上分配變量,并參與垃圾回收乱豆。不可變類型是Julia特有的概念奖恰。不可變類型的值無法更改。
不可變類型通常是表示值的類型宛裕,而不是值的集合瑟啃。例如,大多數(shù)數(shù)字類型(包括Int64
和Float64
)都是不可變的揩尸。(Julia中的數(shù)字類型是普通類型蛹屿,而不是特殊的原始類型。你可以定義一個與Julia所提供的類型相同的新的MyInt64
岩榆。)由于無法修改不可變類型错负,因此每次當你想要更改時都必須創(chuàng)建一個新的副本坟瓢。例如, 4 + 6
必須創(chuàng)建一個新的Int64
來保存結(jié)果犹撒。反之折联,可變類型的成員可以就地(in-place)更新。這意味著你不必在修改過程中做一次完整的復(fù)制识颊。
用x = x + 2
來分配內(nèi)存的想法可能聽起來很奇怪诚镰。為什么你要使Int64
值不可變,從而導(dǎo)致這樣的基本操作變慢谊囚?這就是那些編譯器優(yōu)化的用武之地:使用不可變類型(通常)不會使它變慢怕享。如果x
具有穩(wěn)定的具體類型(例如Int64
),則編譯器可以自由地在堆棧上分配x
镰踏,并就地變換x
函筋。問題是當x
具有不穩(wěn)定類型時(因此編譯器對它的大小或類型一無所知),一旦x
被裝箱并且在堆上奠伪,編譯器就不能完全確定有沒有其他代碼使用該值跌帐,因此無法編輯它。
因為stable
中的sum
有具體類型( Float64
)绊率,編譯器知道它可以在函數(shù)本地拆箱存儲它并改變其值谨敛。這里的sum
不會被分配到堆上,并且每次添加i/2
時都不需要創(chuàng)建新副本滤否。
因為unstable
中的sum
沒有具體類型脸狸,所以編譯器會在堆上分配它。每次我們修改sum時藐俺,我們在堆上都分配了一個新值炊甲。所有這些在堆上分配值(以及每次我們想要讀取sum
的值時進行的檢索)所花費的時間都是寶貴的。
使用0
而不是0.0
是一個容易犯的錯誤欲芹,尤其是當你剛接觸Julia時卿啡。自動檢查循環(huán)中使用的變量是否是類型穩(wěn)定的,有助于程序員更深入理解他們的代碼性能關(guān)鍵(performance-critical)部分中的變量類型菱父。
實施細節(jié)
我們需要找出循環(huán)中使用的變量颈娜,并且識別這些變量的類型。然后我們需要決定如何以人類可讀的格式打印它們浙宜。
- 我們?nèi)绾握业窖h(huán)官辽?
- 我們?nèi)绾卧谘h(huán)中找到變量?
- 我們?nèi)绾巫R別變量的類型粟瞬?
- 我們?nèi)绾未蛴〗Y(jié)果野崇?
- 我們?nèi)绾闻袛囝愋褪欠癫环€(wěn)定?
我將首先解決最后一個問題亩钟,因為整個嘗試是否成功都取決于它乓梨。我們已經(jīng)研究了一個不穩(wěn)定的函數(shù),作為程序員清酥,也看到了如何識別不穩(wěn)定的變量扶镀,但是我們需要程序去找到它們。這聽起來像是需要通過模擬函數(shù)來查找值可能會發(fā)生變化的變量——聽起來需要好些工作焰轻。對我們來說幸運的是臭觉,Julia的類型推斷已經(jīng)通過跟蹤函數(shù)執(zhí)行完成了類型檢測。
unstable
中的sum
的類型是Union(Float64,Int64)
辱志。這是一種UnionType
蝠筑,是一種特殊類型。這種類型的變量可以保存一組類型值中的任一類型值揩懒。比如Union(Float64,Int64)
類型的變量既可以保存Int64
什乙,也可以保存Float64
類型的值,但這個值只能是其中一種已球。UnionType
可以連接任意數(shù)量的類型(例如臣镣,UnionType(Float64, Int64, Int32)
連接了三種類型)。我們要在循環(huán)中查找UnionType
類型的變量智亮。
將代碼解析為代表性結(jié)構(gòu)是一項復(fù)雜的業(yè)務(wù)忆某,并且它隨著語言的發(fā)展變得越來越復(fù)雜。在本章中阔蛉,我們將依賴于編譯器使用的內(nèi)部數(shù)據(jù)結(jié)構(gòu)弃舒。這意味著我們不必擔心讀取文件或解析它們,但它確實意味著我們必須和一些不受我們控制状原、有時感覺笨拙或丑陋的數(shù)據(jù)結(jié)構(gòu)打交道聋呢。
除去因無需自己解析代碼所節(jié)省下來的所有工作,使用與編譯器相同的數(shù)據(jù)結(jié)構(gòu)意味著我們的檢查將是基于一種編譯器理解的準確評估——這意味著我們的檢查將與代碼實際運行的方式保持一致遭笋。
從Julia代碼中檢查Julia代碼的過程稱為自拾用帷(introspection)。當你我自省時瓦呼,我們思考的正是我們思考和感受的方式和原因喂窟。當代碼自省時,它會檢查相同語言(可能是自己的代碼)的代碼的表達或執(zhí)行屬性央串。當代碼的自省擴展到修改被檢查的代碼時磨澡,它被稱為元編程(編寫或修改程序的程序)。
Julia的自省
Julia的自省很簡單质和。它有四個內(nèi)置函數(shù)稳摄,能讓我們看到編譯器在想什么: code_lowered
, code_typed
饲宿, code_llvm
和code_native
厦酬。編譯過程中哪個步驟越先有輸出胆描,哪個函數(shù)就越排在前面。第一個函數(shù)最接近我們輸入的代碼仗阅,而最后一個最接近CPU運行的代碼昌讲。在本章中,我們將重點關(guān)注code_typed
减噪,它為我們提供了優(yōu)化的短绸,類型推斷的抽象語法樹(AST)。
code_typed
需要兩個參數(shù):感興趣的函數(shù)和一個參數(shù)類型的元組筹裕。例如醋闭,如果我們想在使用兩個Int64
參數(shù)調(diào)用函數(shù)foo
時觀察它的AST,那么我們將調(diào)用code_typed(foo, (Int64,Int64))
朝卒。
function foo(x,y)
z = x + y
return 2 * z
end
code_typed(foo,(Int64,Int64))
這是code_typed
將會返回的結(jié)構(gòu):
1-element Array{Any,1}:
:($(Expr(:lambda, {:x,:y}, {{:z},{{:x,Int64,0},{:y,Int64,0},{:z,Int64,18}},{}},
:(begin # none, line 2:
z = (top(box))(Int64,(top(add_int))(x::Int64,y::Int64))::Int64 # line 3:
return (top(box))(Int64,(top(mul_int))(2,z::Int64))::Int64
end::Int64))))
這是一個Array
证逻,它允許code_typed
返回多個匹配方法。函數(shù)和參數(shù)類型的某些組合可能無法完全確定應(yīng)調(diào)用哪個方法扎运。例如瑟曲,你可以傳入類似Any
的類型(而不是Int64
)。Any
是類型層次結(jié)構(gòu)的頂部類型豪治。所有類型都是Any
(包括Any
)的子類型洞拨。如果我們在參數(shù)類型的元組中包含Any
,并且有多個匹配方法负拟,那么code_typed
的Array
將包含多個元素烦衣,每個匹配方法都會有一個元素。
為了方便討論掩浙,讓我們將Expr
的例子單獨拉出來花吟。
julia> e = code_typed(foo,(Int64,Int64))[1]
:($(Expr(:lambda, {:x,:y}, {{:z},{{:x,Int64,0},{:y,Int64,0},{:z,Int64,18}},{}},
:(begin # none, line 2:
z = (top(box))(Int64,(top(add_int))(x::Int64,y::Int64))::Int64 # line 3:
return (top(box))(Int64,(top(mul_int))(2,z::Int64))::Int64
end::Int64))))
我們感興趣的結(jié)構(gòu)在Array
之中:它是一個Expr
。Julia使用Expr
(表達式的縮寫)來表示其AST厨姚。 (抽象語法樹是編譯器對代碼含義的理解衅澈。這有點像你在小學時做的語句圖解。)我們得到的Expr
代表了一種方法谬墙。它包含了一些元數(shù)據(jù)(關(guān)于方法中出現(xiàn)的變量)和構(gòu)成方法主體的表達式今布。
現(xiàn)在我們可以問一些關(guān)于e
的問題了。
我們可以通過使用names
函數(shù)來詢問Expr
具有哪些屬性拭抬,該函數(shù)適用于任何Julia的值或類型部默。它返回由該類型(或值的類型)定義的名稱Array
。
julia> names(e)
3-element Array{Symbol,1}:
:head
:args
:typ
我們只是詢問了e
有什么樣的名稱造虎,現(xiàn)在我們可以詢問每個名稱對應(yīng)的值傅蹂。Expr
有三個屬性: head
, typ
和args
。
julia> e.head
:lambda
julia> e.typ
Any
julia> e.args
3-element Array{Any,1}:
{:x,:y}
{{:z},{{:x,Int64,0},{:y,Int64,0},{:z,Int64,18}},{}}
:(begin # none, line 2:
z = (top(box))(Int64,(top(add_int))(x::Int64,y::Int64))::Int64 # line 3:
return (top(box))(Int64,(top(mul_int))(2,z::Int64))::Int64
end::Int64)
馬上我們就看到打印出了一些值份蝴,但關(guān)于它們的含義或使用方式犁功,還無法給我們足夠的信息。
-
head
告訴我們這是什么樣的表達式搞乏。通常波桩,你會在Julia中使用單獨的類型,但Expr
是一種對解析器中使用的結(jié)構(gòu)進行建模的類型请敦。解析器是用某種Scheme語言編寫的,它將所有內(nèi)容都構(gòu)造為嵌套列表储玫。head
告訴我們Expr
的其余部分是如何組織的以及它代表了什么樣的表達式侍筛。 -
typ
是表達式自動推斷的返回類型。當你求解任何表達式時撒穷,它會產(chǎn)生一些值匣椰。typ
是表達式求得的值的類型。對于幾乎所有Expr
端礼,該值將為Any
(這永遠都正確禽笑,因為每種可能的類型都是Any
的子類型)。只有類型推斷方法的body
和它們內(nèi)部的大多數(shù)表達式才會將其typ
設(shè)置為更具體的類型蛤奥。(由于type
是關(guān)鍵字佳镜,因此該字段不能用type作為其名稱。) -
args
是Expr
最復(fù)雜的部分凡桥。它的結(jié)構(gòu)根據(jù)head
的值而變化蟀伸。它始終是一個Array{Any}
(一個無類型數(shù)組),但除此之外缅刽,結(jié)構(gòu)也會發(fā)生變化啊掏。
在表示一個方法的Expr
中, e.args
中將有三個元素:
julia> e.args[1] # 參數(shù)名稱的符號
2-element Array{Any,1}:
:x
:y
符號是一種特殊類型衰猛,用于表示變量迟蜜,常量,函數(shù)和模塊的名稱啡省。它們與字符串的類型不同娜睛,因為它們專門用來表示程序構(gòu)造的名稱。
julia> e.args[2] # 變量元數(shù)據(jù)的3個列表
3-element Array{Any,1}:
{:z}
{{:x,Int64,0},{:y,Int64,0},{:z,Int64,18}}
{}
上面的第一個列表包含所有局部變量的名稱冕杠,這里我們只有一個( z
)微姊。第二個列表包含方法中每個變量和參數(shù)的元組。每個元組都有變量名分预,變量的推斷類型和數(shù)字兢交。該數(shù)字以機器友好(而不是人類友好)的方式傳達了有關(guān)變量如何使用的信息。最后一個列表是捕獲的變量名稱笼痹,在這個例子中它是空的配喳。
julia> e.args[3] # 方法的主體
:(begin # none, line 2:
z = (top(box))(Int64,(top(add_int))(x::Int64,y::Int64))::Int64 # line 3:
return (top(box))(Int64,(top(mul_int))(2,z::Int64))::Int64
end::Int64)
前兩個args
元素是第三個元素的元數(shù)據(jù)酪穿。雖然元數(shù)據(jù)非常有趣,但現(xiàn)在不是必須的晴裹。重要的部分是方法的主體被济,而這就是第三個要素。下面是另一個Expr
涧团。
julia> body = e.args[3]
:(begin # none, line 2:
z = (top(box))(Int64,(top(add_int))(x::Int64,y::Int64))::Int64 # line 3:
return (top(box))(Int64,(top(mul_int))(2,z::Int64))::Int64
end::Int64)
julia> body.head
:body
這個Expr
的頭是:body
只磷,因為它是方法的主體。
julia> body.typ
Int64
typ
是方法的推斷返回類型泌绣。
julia> body.args
4-element Array{Any,1}:
:( # none, line 2:)
:(z = (top(box))(Int64,(top(add_int))(x::Int64,y::Int64))::Int64)
:( # line 3:)
:(return (top(box))(Int64,(top(mul_int))(2,z::Int64))::Int64)
args
包含一個表達式列表:即方法主體中的表達式列表钮追。有一些行號注釋(如 ::( # line 3:)
),但主體的大部分在做的是設(shè)置z
( z = x + y
)的值并返回2 * z
阿迈。注意元媚,這些操作已被特定的Int64
類型的內(nèi)部函數(shù)替換。top(function-name)
表示了一個內(nèi)在函數(shù)苗沧,這是在Julia的代碼生成中實現(xiàn)的東西豫尽,而不是Julia本身實現(xiàn)的東西籍滴。
我們還沒有看過循環(huán)是什么樣子衍慎,所以讓我們嘗試一下垦搬。
julia> function lloop(x)
for x = 1:100
x *= 2
end
end
lloop (generic function with 1 method)
julia> code_typed(lloop, (Int,))[1].args[3]
:(begin # none, line 2:
#s120 = $(Expr(:new, UnitRange{Int64}, 1, :(((top(getfield))(Intrinsics,
:select_value))((top(sle_int))(1,100)::Bool,100,(top(box))(Int64,(top(
sub_int))(1,1))::Int64)::Int64)))::UnitRange{Int64}
#s119 = (top(getfield))(#s120::UnitRange{Int64},:start)::Int64 unless
(top(box))(Bool,(top(not_int))(#s119::Int64 === (top(box))(Int64,(top(
add_int))((top(getfield))
(#s120::UnitRange{Int64},:stop)::Int64,1))::Int64::Bool))::Bool goto 1
2:
_var0 = #s119::Int64
_var1 = (top(box))(Int64,(top(add_int))(#s119::Int64,1))::Int64
x = _var0::Int64
#s119 = _var1::Int64 # line 3:
x = (top(box))(Int64,(top(mul_int))(x::Int64,2))::Int64
3:
unless (top(box))(Bool,(top(not_int))((top(box))(Bool,(top(not_int))
(#s119::Int64 === (top(box))(Int64,(top(add_int))((top(getfield))(
#s120::UnitRange{Int64},:stop)::Int64,1))::Int64::Bool))::Bool))::Bool
goto 2
1: 0:
return
end::Nothing)
你會注意到主題中并沒有for或while循環(huán)。當編譯器將代碼從我們編寫的代碼轉(zhuǎn)換為CPU理解的二進制指令時飒焦,會刪除對人類有用但不被CPU理解的功能(如循環(huán))蜈膨。循環(huán)已被重寫為label
和goto
表達式。goto
后有一個數(shù)字牺荠,每個label
也有一個數(shù)字翁巍。goto
將跳轉(zhuǎn)到具有相同數(shù)字的label
。
檢測和提取循環(huán)
我們將通過查找后向跳轉(zhuǎn)的goto
表達式來識別循環(huán)休雌。
我們需要找到標簽和那些goto灶壶,并弄清匹配的部分。我打算先把全部實現(xiàn)給你杈曲。在代碼墻之后驰凛,我們再分解和檢查這些代碼片段。
# 這是一個嘗試檢測方法體內(nèi)循環(huán)的函數(shù)
# 返回一個或多個循環(huán)函數(shù)中的行
function loopcontents(e::Expr)
b = body(e)
loops = Int[]
nesting = 0
lines = {}
for i in 1:length(b)
if typeof(b[i]) == LabelNode
l = b[i].label
jumpback = findnext(x-> (typeof(x) == GotoNode && x.label == l)
|| (Base.is_expr(x,:gotoifnot) && x.args[end] == l),
b, i)
if jumpback != 0
push!(loops,jumpback)
nesting += 1
end
end
if nesting > 0
push!(lines,(i,b[i]))
end
if typeof(b[i]) == GotoNode && in(i,loops)
splice!(loops,findfirst(loops,i))
nesting -= 1
end
end
lines
end
現(xiàn)在解釋一下:
b = body(e)
我們首先將方法主體中的所有表達式作為Array
担扑。 body
是我已經(jīng)實現(xiàn)的函數(shù):
#返回方法的主體恰响。
#參數(shù)是表示方法的Expr,
#返回向量{Expr}涌献。
function body(e::Expr)
return e.args[3].args
end
然后:
loops = Int[]
nesting = 0
lines = {}
loops
是個用來保存標簽行號的Array
胚宦,這些行號是產(chǎn)生循環(huán)的goto所對應(yīng)的標簽行號。nesting
表示我們當前所處的循環(huán)次數(shù)。lines
是一個保存(索引枢劝, Expr
)元組的Array
井联。
for i in 1:length(b)
if typeof(b[i]) == LabelNode
l = b[i].label
jumpback = findnext(
x-> (typeof(x) == GotoNode && x.label == l)
|| (Base.is_expr(x,:gotoifnot) && x.args[end] == l),
b, i)
if jumpback != 0
push!(loops,jumpback)
nesting += 1
end
end
我們看一下e
主體中的每個表達式。如果它是一個標簽您旁,我們檢查是否有跳轉(zhuǎn)到此標簽的goto(并且發(fā)生在當前索引之后)烙常。如果findnext
的結(jié)果大于零,那么這樣的goto節(jié)點就存在鹤盒,所以我們將它添加到loops
(我們當前所在的循環(huán)的Array
)并增加嵌套級別蚕脏。
if nesting > 0
push!(lines,(i,b[i]))
end
如果我們當前正處在循環(huán)中,我們將當前行推到我們的返回行數(shù)組中侦锯。
if typeof(b[i]) == GotoNode && in(i,loops)
splice!(loops,findfirst(loops,i))
nesting -= 1
end
end
lines
end
如果我們在GotoNode
中 蝗锥,那么我們檢查它是否是循環(huán)的結(jié)束。如果是率触,我們從loops
中刪除該條目并降低嵌套級別。
這個函數(shù)的結(jié)果是lines
數(shù)組汇竭,一個(索引葱蝗,值)元組的數(shù)組。這意味著數(shù)組中的每個值都有一個到方法-主體- Expr
的主體的索引细燎,和該索引處的值两曼。lines
中的每個元素都是循環(huán)中的一個表達式。
查找和鍵入變量
我們剛剛完成了loopcontents
函數(shù) 玻驻,它返回了循環(huán)內(nèi)部的所有Expr
悼凑。我們的下一個函數(shù)是loosetypes
,它獲取Expr
的列表并返回松散類型的變量列表璧瞬。稍后户辫,我們將loopcontents
的輸出傳遞給loosetypes
。
在循環(huán)內(nèi)發(fā)生的每個表達式中嗤锉, loosetypes
搜索符號及其相關(guān)類型的出現(xiàn)次數(shù)渔欢。變量的使用在AST中表示為SymbolNode
; SymbolNode
保存變量的名稱和其推斷類型。
我們不能檢查每個loopcontents
收集到的表達式瘟忱,來判斷它是否是一個SymbolNode
奥额。問題是每個Expr
可能包含一個或多個Expr
;每個Expr
也可能包含一個或多個SymbolNode
。這意味著我們需要提取任何嵌套的Expr
访诱,以便我們可以依次從中查找SymbolNode
垫挨。
# 給定\`lr\`,一個表達式向量(Expr + 文本等)
# 嘗試在\`lr\`中查找所有出現(xiàn)的變量
# 并確定它們的類型函數(shù)
function loosetypes(lr::Vector)
symbols = SymbolNode[]
for (i,e) in lr
if typeof(e) == Expr
es = copy(e.args)
while !isempty(es)
e1 = pop!(es)
if typeof(e1) == Expr
append!(es,e1.args)
elseif typeof(e1) == SymbolNode
push!(symbols,e1)
end
end
end
end
loose_types = SymbolNode[]
for symnode in symbols
if !isleaftype(symnode.typ) && typeof(symnode.typ) == UnionType
push!(loose_types, symnode)
end
end
return loose_types
end
symbols = SymbolNode[]
for (i,e) in lr
if typeof(e) == Expr
es = copy(e.args)
while !isempty(es)
e1 = pop!(es)
if typeof(e1) == Expr
append!(es,e1.args)
elseif typeof(e1) == SymbolNode
push!(symbols,e1)
end
end
end
end
while循環(huán)以遞歸方式遍歷所有Expr
的內(nèi)部触菜。每次循環(huán)找到SymbolNode
九榔,就將它添加到symbols
矢量 。
loose_types = SymbolNode[]
for symnode in symbols
if !isleaftype(symnode.typ) && typeof(symnode.typ) == UnionType
push!(loose_types, symnode)
end
end
return loose_types
end
現(xiàn)在我們有一個變量列表及其類型,因此很容易檢查類型是否松散帚屉。這種檢查由 loosetypes
查找特定類型的非具體類型( UnionType
)完成 谜诫。當我們認為所有非具體類型都屬于“失敗”時,我們會得到許多“失敗”的結(jié)果攻旦。這是因為我們正在使用帶注釋的參數(shù)類型來評估每個方法喻旷,這些參數(shù)類型可能是抽象的。
提高可用性
既然我們已經(jīng)可以對表達式進行檢查牢屋,我們應(yīng)該讓它能更方便地調(diào)用用戶代碼且预。我們將創(chuàng)造兩種調(diào)用checklooptypes
的辦法:
對整個函數(shù)調(diào)用:檢查給定函數(shù)的每個方法。
對一個表達式調(diào)用:用于用戶自己提取
code_typed
結(jié)果的場合烙无。
## 對于一個給定函數(shù)锋谐,對每個方法運行checklooptypes
function checklooptypes(f::Callable;kwargs...)
lrs = LoopResult[]
for e in code_typed(f)
lr = checklooptypes(e)
if length(lr.lines) > 0 push!(lrs,lr) end
end
LoopResults(f.env.name,lrs)
end
# 對于表示一個方法的Expr,
# 檢查循環(huán)中使用的每個變量的類型
# 都有具體類型
checklooptypes(e::Expr;kwargs...) =
LoopResult(MethodSignature(e),loosetypes(loopcontents(e)))
對于只有一種方法的函數(shù)截酷,我們可以看到兩種方式的效果幾乎相同:
julia> using TypeCheck
julia> function foo(x::Int)
s = 0
for i = 1:x
s += i/2
end
return s
end
foo (generic function with 1 method)
julia> checklooptypes(foo)
foo(Int64)::Union(Int64,Float64)
s::Union(Int64,Float64)
s::Union(Int64,Float64)
julia> checklooptypes(code_typed(foo,(Int,))[1])
(Int64)::Union(Int64,Float64)
s::Union(Int64,Float64)
s::Union(Int64,Float64)
漂亮的輸出
我在這里跳過了一個實現(xiàn)細節(jié):我們是如何將結(jié)果打印到REPL(交互式編譯器)的涮拗?
首先,我制造了一些新的類型迂苛。LoopResults
是對整個函數(shù)的檢查結(jié)果三热,它具有函數(shù)名稱和每個方法的結(jié)果。LoopResult
是單個方法的檢查結(jié)果三幻,它具有參數(shù)類型和松散類型的變量就漾。
checklooptypes
函數(shù)返回一個LoopResults
。該類型定義了一個名為show
的函數(shù)念搬。REPL對它想要顯示的值調(diào)用display
抑堡,然后 display
將調(diào)用執(zhí)行我們的show
。
此代碼對于此靜態(tài)分析的可用性非常重要朗徊,但它本身不進行靜態(tài)分析首妖。你應(yīng)該在你的實現(xiàn)語言中,為漂亮的打印類型和輸出采用更喜歡的方法荣倾。這只是Julia中的做法悯搔。
type LoopResult
msig::MethodSignature
lines::Vector{SymbolNode}
LoopResult(ms::MethodSignature,ls::Vector{SymbolNode}) = new(ms,unique(ls))
end
function Base.show(io::IO, x::LoopResult)
display(x.msig)
for snode in x.lines
println(io,"\\t",string(snode.name),"::",string(snode.typ))
end
end
type LoopResults
name::Symbol
methods::Vector{LoopResult}
end
function Base.show(io::IO, x::LoopResults)
for lr in x.methods
print(io,string(x.name))
display(lr)
end
end
查找未使用的變量
有時,當你在編寫程序時舌仍,輸錯了變量名稱妒貌。程序無法辨別你輸錯的變量實際上是指之前拼寫正確的那個變量。它看到的是一個只使用了一次的變量铸豁,而你可能看到的是變量名稱拼寫錯誤灌曙。需要變量聲明的語言自然會捕獲這些拼寫錯誤,但許多動態(tài)語言不需要聲明节芥,因此需要額外的分析層來捕獲這些錯誤在刺。
我們可以通過查找僅使用一次逆害、或僅以一種方式使用過的變量來查找拼寫錯誤的變量名稱(以及其他未使用的變量)。
下面是一個帶有一個拼寫錯誤名稱的代碼示例蚣驼。
function foo(variable_name::Int)
sum = 0
for i=1:variable_name
sum += variable_name
end
variable_nme = sum
return variable_name
end
這種錯誤可能會導(dǎo)致代碼中出現(xiàn)問題魄幕,而只有在運行時才能發(fā)現(xiàn)。假設(shè)你的每個變量名稱都只打錯一次颖杏。我們可以將變量的用法分為讀和寫纯陨。如果拼寫錯誤發(fā)生在寫時(如, worng = 5
)留储,則不會拋出錯誤翼抠。你只是默默地將值放在錯誤的變量中——查找錯誤的過程可能令人懊惱。如果拼寫錯誤發(fā)生在讀時(如获讳, right = worng + 2
)阴颖,那么在運行代碼時會出現(xiàn)運行時錯誤。我們希望對此有一個靜態(tài)警告丐膝,以便你可以更快地找到此錯誤量愧,但你還是需要等到運行代碼才能發(fā)現(xiàn)這個問題。
隨著代碼變得越來越長帅矗、越來越復(fù)雜侠畔,要發(fā)現(xiàn)錯誤也變得更加困難——除非靜態(tài)分析能幫到你。
左側(cè)和右側(cè)
另一個討論“讀”和“寫”這兩種用法的方式是稱它們?yōu)椤坝覀?cè)”(RHS)和“左側(cè)”(LHS)用法损晤。這是指變量相對于 =
符號的位置。
以下是x
一些用法:
- 左側(cè):
x = 2
x = y + 22
x = x + y + 2
-
x += 2
(轉(zhuǎn)換為x = x + 2
)
- 右側(cè):
y = x + 22
x = x + y + 2
-
x += 2
(轉(zhuǎn)換為x = x + 2
) 2 * x
X
注意红竭, x = x + y + 2
和x += 2
這兩個表達式在左側(cè)和右側(cè)都出現(xiàn)了尤勋,因為x
出現(xiàn)在=
符號的兩側(cè)。
尋找一次性變量
我們需要查找兩種情況:
- 使用一次的變量茵宪。
- 只在左側(cè)或右側(cè)使用的變量最冰。
我們將查找所有變量用法,但我們將分別查找左側(cè)和右側(cè)用法稀火,以涵蓋這兩種情況暖哨。
尋找左側(cè)用法
變量在左側(cè),是指變量需要處在=
的左邊凰狞。這意味著我們可以在AST中查找=
符號篇裁,然后查看它們的左側(cè)以找到相關(guān)變量。
在AST中赡若, =
是帶有:(=)
頭部的Expr
达布。(括號是為了清楚地表明這是=
的符號而不是另一個運算符, :=
.)args
的第一個值將是其左側(cè)的變量名稱逾冬。因為我們正在查看編譯器已經(jīng)清理過的AST黍聂,所以我們的=
符號左側(cè)(幾乎)總是只有一個符號躺苦。
讓我們看看代碼中的含義:
julia> :(x = 5)
:(x = 5)
julia> :(x = 5).head
:(=)
julia> :(x = 5).args
2-element Array{Any,1}:
:x
5
julia> :(x = 5).args[1]
:x
下面是完整的實現(xiàn),隨后是解釋产还。
# 返回賦值(=)左側(cè)使用的所有變量列表
#
# 參數(shù):
# e: 一個表示方法的Expr匹厘,正如code_typed中得到的
#
# 返回:
# 一個{符號}集合,其中每個元素都出現(xiàn)在e中賦值的左側(cè)脐区。
#
function find_lhs_variables(e::Expr)
output = Set{Symbol}()
for ex in body(e)
if Base.is_expr(ex,:(=))
push!(output,ex.args[1])
end
end
return output
end
output = Set{Symbol}()
我們有一個符號集合愈诚,這些是我們在左側(cè)找到的變量名稱。
for ex in body(e)
if Base.is_expr(ex,:(=))
push!(output,ex.args[1])
end
end
我們沒有深入研究表達式坡椒,因為code_typed
的AST非常扁平扰路。循環(huán)和條件語句已轉(zhuǎn)換為goto控制流的扁平語句。在函數(shù)調(diào)用的參數(shù)中不會隱藏有任何賦值倔叼。如果等號左側(cè)有任何符號以外的東西汗唱,則此代碼將失敗。這沒有考慮到兩個特定的邊緣情況:數(shù)組訪問(如a[5]
丈攒,將表示為:ref
表達式)和屬性(如a.head
哩罪,將表示為:.
表達式)。這些仍始終將相關(guān)符號作為其args
的第一個值巡验,它可能只是藏得深一些(如在a.property.name.head.other_property
)际插。此代碼無法處理這些情況,但if
語句中的幾行代碼可以解決這個問題显设。
push!(output,ex.args[1])
當我們找到左側(cè)變量時框弛,我們將變量名稱push!
到Set
中 。該Set
能確保我們每個名稱只有一個副本捕捂。
尋找右側(cè)用法
要查找所有其他變量的使用瑟枫,我們還需要查看每個Expr
。這更加復(fù)雜指攒,因為我們基本上關(guān)心所有的Expr
慷妙,而不僅僅是有:(=)
的那些。還因為我們必須深入研究嵌套的Expr
(以處理嵌套函數(shù)調(diào)用)允悦。
這是完整的實現(xiàn)膝擂,隨后是解釋。
# 給定一個表達式隙弛,查找其中使用的(右側(cè))變量
#
# 參數(shù):e: 一個Expr
#
# 返回: 一個{符號}集合, 其中每個e都在e的右側(cè)表達式中
#
function find_rhs_variables(e::Expr)
output = Set{Symbol}()
if e.head == :lambda
for ex in body(e)
union!(output,find_rhs_variables(ex))
end
elseif e.head == :(=)
for ex in e.args[2:end] # skip lhs
union!(output,find_rhs_variables(ex))
end
elseif e.head == :return
output = find_rhs_variables(e.args[1])
elseif e.head == :call
start = 2 # skip function name
e.args[1] == TopNode(:box) && (start = 3) # skip type name
for ex in e.args[start:end]
union!(output,find_rhs_variables(ex))
end
elseif e.head == :if
for ex in e.args # want to check condition, too
union!(output,find_rhs_variables(ex))
end
elseif e.head == :(::)
output = find_rhs_variables(e.args[1])
end
return output
end
該函數(shù)的主要結(jié)構(gòu)是一個龐大的if-else語句架馋,其中每個分支處理一種不同的頭部符號。
output = Set{Symbol}()
output
是變量名稱的集合全闷,我們將在函數(shù)末尾返回绩蜻。由于我們只關(guān)心一件事,那就是每個變量至少被讀取一次室埋。因此使用Set
可以使我們免于擔心變量名稱的唯一性办绝。
if e.head == :lambda
for ex in body(e)
union!(output,find_rhs_variables(ex))
end
這是if-else語句中的第一個條件伊约。:lambda
代表函數(shù)體。我們對定義的主體進行了遞歸孕蝉,這樣應(yīng)該能從定義中獲得所有右側(cè)變量屡律。
elseif e.head == :(=)
for ex in e.args[2:end] # skip lhs
union!(output,find_rhs_variables(ex))
end
如果頭部是:(=)
,則表達式是一個賦值過程降淮。我們跳過args
的第一個元素超埋,因為這是被賦值的變量。對于每個剩余的表達式佳鳖,我們遞歸地找到右側(cè)變量并將它們添加到我們的集合中霍殴。
elseif e.head == :return
output = find_rhs_variables(e.args[1])
如果這是一個return語句,那么args
的第一個元素是返回了值的表達式系吩。我們將把其中的任何變量添加到我們的集合中来庭。
elseif e.head == :call
# 跳過函數(shù)名
for ex in e.args[2:end]
union!(output,find_rhs_variables(ex))
end
對于函數(shù)調(diào)用,我們希望獲得調(diào)用的所有參數(shù)中使用的所有變量穿挨。我們跳過函數(shù)名月弛,它是args
的第一個元素。
elseif e.head == :if
for ex in e.args # want to check condition, too
union!(output,find_rhs_variables(ex))
end
表示if語句的Expr
具有值為:if
的head
科盛。我們希望從if語句主體中的所有表達式中獲取變量用法帽衙,因此我們對args
每個元素進行遞歸。
elseif e.head == :(::)
output = find_rhs_variables(e.args[1])
end
:(::)
運算符用于添加類型注釋贞绵。第一個參數(shù)是被注釋的表達式或變量厉萝。我們檢查被注釋的表達式中的變量用法。
return output
在函數(shù)的最后榨崩,我們返回一組右側(cè)變量冀泻。
還有一些代碼可以簡化上述方法。因為上面的版本只處理Expr
蜡饵,但是遞歸傳遞的某些值可能不是Expr
,我們還需要一些方法來適當?shù)靥幚砥渌赡艿念愋汀?/p>
# 遞歸基本用例胳施,用于簡化Expr版本中的控制流
find_rhs_variables(a) = Set{Symbol}() # 未經(jīng)處理溯祸,應(yīng)當是立即值,如Int
find_rhs_variables(s::Symbol) = Set{Symbol}([s])
find_rhs_variables(s::SymbolNode) = Set{Symbol}([s.name])
組合起來
現(xiàn)在我們已經(jīng)定義了上述的兩個函數(shù)舞肆,我們可以一起使用它們來查找只進行了讀取或?qū)懭氲淖兞拷垢ā⒉檎宜鼈兊暮瘮?shù)命名為unused_locals
。
function unused_locals(e::Expr)
lhs = find_lhs_variables(e)
rhs = find_rhs_variables(e)
setdiff(lhs,rhs)
end
unused_locals
將返回一組變量名稱椿胯。很容易就可以編寫一個函數(shù)筷登,來確定unused_locals
的輸出是否可以計為“通過”。如果該集為空哩盲,則該方法通過前方。如果一個函數(shù)的所有方法都通過狈醉,則此函數(shù)通過。下面的函數(shù)check_locals
實現(xiàn)了這個邏輯惠险。
check_locals(f::Callable) = all([check_locals(e) for e in code_typed(f)])
check_locals(e::Expr) = isempty(unused_locals(e))
結(jié)論
我們對Julia代碼進行了兩次靜態(tài)分析——一種基于類型苗傅,一種基于變量使用。
靜態(tài)類型語言已經(jīng)完成了我們基于類型的分析所做的工作班巩;額外的基于類型的靜態(tài)分析在動態(tài)類型語言中最常用渣慕。已經(jīng)有很多(主要是研究)項目試圖為Python,Ruby和Lisp等語言構(gòu)建靜態(tài)類型推斷系統(tǒng)抱慌。這些系統(tǒng)通常圍繞可選的類型注釋構(gòu)建逊桦。你可以在需要時使用靜態(tài)類型,而在不需要時轉(zhuǎn)而使用動態(tài)類型抑进。這對于將一些靜態(tài)類型集成到現(xiàn)有代碼庫中特別有用强经。
非類型基礎(chǔ)的檢查(如我們的變量使用檢查)皆適用于動態(tài)和靜態(tài)類型語言。但是单匣,許多靜態(tài)類型的語言(如C ++和Java)要求你聲明變量夕凝,并且已經(jīng)提供了類似我們創(chuàng)建的基本警告。仍然可以編寫自定義檢查户秤。例如码秉,特定于項目樣式指南的檢查,或基于安全策略的額外安全預(yù)防檢查鸡号。
雖然Julia確實有很好的工具可以實現(xiàn)靜態(tài)分析转砖,但它并不孤單。顯然鲸伴,Lisp出了名的地方府蔗,就是能使其代碼成為嵌套列表的數(shù)據(jù)結(jié)構(gòu),所以它往往很容易獲得AST汞窗。 Java也暴露了它的AST姓赤,盡管它的AST比Lisp復(fù)雜得多。某些語言或語言工具鏈的設(shè)計不允許純用戶在內(nèi)部表達式中肆意搜索仲吏。對于開源工具鏈(特別是有良好注釋的工具鏈)不铆,一種選擇是在環(huán)境中添加鉤子(hook),以使你能訪問AST裹唆。
如果這不起作用誓斥,最后的退路就是自己寫一個解析器,不過要盡可能避免這種情況许帐。覆蓋大多數(shù)編程語言的完整語法需要做很多工作劳坑,并且當有新功能添加到語言中時,你必須自己去更新它(而無法從上游自動獲取更新)成畦。根據(jù)你要執(zhí)行的檢查距芬,你可能只需解析某些行或某個語言功能的子集涝开,這將大大降低編寫自己的解析器的成本。
希望你對靜態(tài)分析工具如何編寫的新理解能幫助你理解你代碼中使用的工具蔑穴,并且也許還能激發(fā)你編寫自己的工具忠寻。