靜態(tài)分析(static analysis)

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 = 0sum = 0.0 。在Julia中力图,從字面上來說步绸, 0Int64類型, 而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)于stableunstable更有力的對比數(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在幕后是如何工作的。

unstablestable之間的差異是因為unstablesum必須進行裝箱轉(zhuǎn)換祭往,而stable中的sum可以不必如此伦意。裝箱值由類型標簽和表示該值的實際比特位組成;而拆箱值只含有實際比特位链沼。但是類型標簽很小默赂,所以這不是裝箱值分配更多內(nèi)存的原因。

真正的差異來自編譯器可以進行的優(yōu)化括勺。當變量具有具體的缆八、不可變類型時,編譯器可以在函數(shù)內(nèi)將它拆箱疾捍。如果不是這種情況奈辰,則必須在堆上分配變量,并參與垃圾回收乱豆。不可變類型是Julia特有的概念奖恰。不可變類型的值無法更改。

不可變類型通常是表示值的類型宛裕,而不是值的集合瑟啃。例如,大多數(shù)數(shù)字類型(包括Int64Float64 )都是不可變的揩尸。(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_loweredcode_typed 饲宿, code_llvmcode_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_typedArray將包含多個元素烦衣,每個匹配方法都會有一個元素。

為了方便討論掩浙,讓我們將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有三個屬性: headtypargs

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作為其名稱。)
  • argsExpr最復(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è)置zz = 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)已被重寫為labelgoto表達式。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 的辦法:

  1. 對整個函數(shù)調(diào)用:檢查給定函數(shù)的每個方法。

  2. 對一個表達式調(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 + 2x += 2這兩個表達式在左側(cè)和右側(cè)都出現(xiàn)了尤勋,因為x出現(xiàn)在=符號的兩側(cè)。

尋找一次性變量

我們需要查找兩種情況:

  1. 使用一次的變量茵宪。
  2. 只在左側(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具有值為:ifhead 科盛。我們希望從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ā)你編寫自己的工具忠寻。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市存和,隨后出現(xiàn)的幾起案子奕剃,更是在濱河造成了極大的恐慌,老刑警劉巖捐腿,帶你破解...
    沈念sama閱讀 210,978評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件纵朋,死亡現(xiàn)場離奇詭異,居然都是意外死亡茄袖,警方通過查閱死者的電腦和手機操软,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評論 2 384
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來宪祥,“玉大人聂薪,你說我怎么就攤上這事』妊颍” “怎么了藏澳?”我有些...
    開封第一講書人閱讀 156,623評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長耀找。 經(jīng)常有香客問我翔悠,道長,這世上最難降的妖魔是什么野芒? 我笑而不...
    開封第一講書人閱讀 56,324評論 1 282
  • 正文 為了忘掉前任蓄愁,我火速辦了婚禮,結(jié)果婚禮上狞悲,老公的妹妹穿的比我還像新娘撮抓。我一直安慰自己,他們只是感情好摇锋,可當我...
    茶點故事閱讀 65,390評論 5 384
  • 文/花漫 我一把揭開白布丹拯。 她就那樣靜靜地躺著,像睡著了一般乱投。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上顷编,一...
    開封第一講書人閱讀 49,741評論 1 289
  • 那天戚炫,我揣著相機與錄音,去河邊找鬼媳纬。 笑死双肤,一個胖子當著我的面吹牛施掏,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播茅糜,決...
    沈念sama閱讀 38,892評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼七芭,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了蔑赘?” 一聲冷哼從身側(cè)響起狸驳,我...
    開封第一講書人閱讀 37,655評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎缩赛,沒想到半個月后耙箍,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,104評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡酥馍,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年辩昆,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片旨袒。...
    茶點故事閱讀 38,569評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡汁针,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出砚尽,到底是詐尸還是另有隱情施无,我是刑警寧澤,帶...
    沈念sama閱讀 34,254評論 4 328
  • 正文 年R本政府宣布尉辑,位于F島的核電站帆精,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏隧魄。R本人自食惡果不足惜卓练,卻給世界環(huán)境...
    茶點故事閱讀 39,834評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望购啄。 院中可真熱鬧襟企,春花似錦、人聲如沸狮含。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,725評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽几迄。三九已至蔚龙,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間映胁,已是汗流浹背木羹。 一陣腳步聲響...
    開封第一講書人閱讀 31,950評論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人坑填。 一個月前我還...
    沈念sama閱讀 46,260評論 2 360
  • 正文 我出身青樓抛人,卻偏偏與公主長得像,于是被迫代替她去往敵國和親脐瑰。 傳聞我的和親對象是個殘疾皇子妖枚,可洞房花燭夜當晚...
    茶點故事閱讀 43,446評論 2 348

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