.NET 編譯器(”Roslyn“)介紹

edithfang發表於2014-11-12
介紹

一般來說,編譯器是一個黑箱,原始碼從一端進入,然後箱子中發生一些奇妙的變化,最後從另一端出來目標檔案或程式集。編譯器施展它們的魔法,它們必須對所處理的程式碼進行深入的理解,不過相關知識不是每個人都需要知道,除了實現編譯器的大法師。因此在轉換輸出完成後相關的資訊就會被遺忘。

對編譯器來說,幾十年來一直很好地為我們所用,但只是會用編譯器已經不夠。我們越來越依賴於整合開發環境(IDE)的特性,比如智慧感知、重構、智慧重新命名、“查詢所有引用”和“轉到定義”來提高我們的生產率。我們依賴於程式碼分析工具來提高我們的程式碼質量,使用程式碼生成器來幫助構造程式。這些工具變得越聰明,他們需要了解越來越多的深入程式碼知識,但是這些知識只有編譯器知道。這是.NET編譯器平臺得核心任務(“Roslyn”):開啟黑箱,讓工具和終端使用者共享編譯器掌握的關於我們程式碼的豐富資訊。取代不透明的原始碼入和物件出的轉換器,通過.NET編譯器平臺(“Roslyn”),編譯器變成你可以使用的平臺API,以用於你的工具和應用的編碼相關的工作。

讓編譯器作為平臺的過渡,為集中建立程式碼工具和應用程式大大降低了進入門檻。它創造了許多革新,如:meta-Programming、程式碼生成和轉換,互動使用C#和VB語言,和某些特殊領域的嵌入式C#和VB語言。

.NET編譯器平臺(“Roslyn”)SDK預覽版包含了最新的新語言物件模型草案,以用於程式碼生成、分析以及重構。在將來的預覽版中,我們希望包含用於指令碼以及互動式使用C#和Visual Basic的API支援草案。本文件提供了.NET編譯器平臺(“Roslyn”)概念上的概覽。更多的細節可以在SDK預覽版的演練及例子中找到。

揭示編譯器API

編譯器管道功能區

.NET編譯器平臺(“Roslyn”)通過提供一個API層,是一個傳統編譯器管道映象,向你這樣的消費者揭示了C#和Visual Basic編譯器的程式碼分析。



這條管道的每一部分,現在都是單獨的元件。首先,在解析階段,其中原始碼被記號化和解析成不同語言的句法。第二,宣告階段,即從原始碼和輸入的metadata進行分析,以形成命名符號。下一個階段,原始碼中的標示符(identifier)被匹配成符號(symbol)。最後釋出(emit)階段,所有編譯器構建的資訊作為一個程式集被髮布。



對應每一個階段都會有一個物件模型,它允許在該階段訪問相關資訊。解析階段表現為句法樹(syntax tree),宣告階段則是分層語法表(hierarchical symbol table),繫結階段作為一個模型,用以展現編譯器進行語義分析後的結果,釋出階段則作為API以產生IL位元組碼。



每個編譯器將這些元件組合在一起,作為一個單一的端到端的(end-to-end)整體。

為了保證公開的編譯器 API 足以建立世界一流的 IDE 功能,下一代 Visual Studio 將會使用這些增強 C#/VB 體驗的語言服務來重建。舉個例子,通過句法樹來實現程式碼大綱和格式化功能、通過符號表實現物件瀏覽器和導航功能、通過語義模型實現重構和“轉到定義”,以及使用上述所有模型(包括 emit API) 實現的“編輯”和“Continue” 功能。通過  “Rosyln” 終端使用者體驗版,這些體驗可以在 Visual Studio 2013中感受到。該體驗版是為了構建並測試基於.NET編譯器平臺( “Roslyn”) SDK 開發的應用,並將應用整合到 Visual Studio 中。你也可以用.NET編譯器平臺( “Roslyn”) API 建立獨立於 Visual Studio 的應用,此類應用無需安裝終端使用者體驗版。

API 層

.NET 編譯器平臺(“Roslyn”)由兩個主要的API層組成,分別是編譯器API和工作區API。


編譯器API(Compiler APIs)

編譯器層包含的物件模型與編譯器管道每一部分的公開資訊相對應,包括語法和語義兩部分。編譯器層還包含了對編譯器單獨呼叫的固定快照,其中包括程式集引用、編譯器選項以及原始碼檔案。針對C#和Visual Basic語言有兩種不同的API。兩種API大小差不多,但是對每種語言又進行了高度的定製。該層不依賴於Visual Studio元件。

診斷 API

作為分析結果的一部分,編譯器會產生一組診斷資訊,涵蓋了從句法、語義、定義賦值的錯誤到各種警告和診斷資訊。編譯器 API 層提供一些可擴充套件的 API 來公開診斷資訊,並允許在編譯過程中插入自定義的分析器,也可以象 StyleCop 或 FxCop 那樣在編譯器預定義資訊之外生成自定義的診斷資訊。以這種方式來生成診斷有個好處,即可以很方便的整合 MSBuild 或 Visual Studio 這些工具,這些工具依賴於診斷資訊,以用於體驗如基於策略停止生成、在編輯器中顯示波浪線並提示程式碼修復。

作為編譯器層的一部分,該團隊還提供了 宿主(Hosting)/指令碼 API 原型以執行程式碼片段和累積執行時下上文。  REPL 使用這些 API,不過到目前為止無論是 REPL 還是指令碼 API 都不是 .NET 編譯器平臺專案的一部分。在重新引入這些元件前團隊還需要審查這些設計。 

工作區 API 

工作區層包含工作區 API,是做程式碼分析和重構整個解決方案的起點。它協助你將解決方案中的專案資訊組織成單一的物件模型, 你可以直接訪問編譯器層的物件模型,而無需解析檔案、配置項或管理專案間的依賴關係。 

此外,工作區層還提供了一組 API 可用於在如 Visual Studio IDE 宿主環境中實現程式碼分析與重構工具,包括:查詢所有引用、程式碼格式化、程式碼生成API等等。 

該層不依賴於 Visual Studio 元件。

句法方面(Working with Syntax)

編譯器API所展示的最基本得資料結構是句法樹。這些樹展示了原始碼的詞彙和語法結構。它們有兩個重要得目的:

    1、允許工具—比如IDE、外掛、程式碼分析工具以及重構—去看和處理使用者專案原始碼中的語法結構。
    2、確保工具—比如重構和IDE—可以以一種自然得方式建立、更改和重排原始碼,而不需要直接使用文字編輯器。通過建立和操作樹,工具可以簡單的建立和重排原始碼。

句法樹(Syntax Trees)

句法樹是用於合輯、程式碼分析、繫結、重構、IDE特性以及程式碼生成得主要結構。如果沒有被識別和歸類為許多知名結構語言元素的其中一個,那麼沒有任何原始碼可以被理解。

句法樹有三個關鍵屬性。第一個屬性是,句法樹儲存了完整的源資訊。意味著句法樹含有源文件中得每一條資訊、每一個語法結構、每一個詞彙記號,以及工作區、註釋和預處理指令中的所有。例如,源中準確展示的每一條文字資訊就像是輸入進行去的一樣。當程式未完成或有異常時,通過在句法樹中展示跳過和丟失令牌,句法樹可以展示原始碼中的錯誤。

這一特點讓語法樹的第二個屬性成為可能。從解析器得到的語法樹與被解析的文字之間是完全可相互轉換的。從任何一個語法樹節點,都可以得到該節點子樹的文字表示。這意味著,語法樹可以用以構造和編輯源文字。建立樹等於隱式建立等效文字,而編輯語法樹,根據已存在樹的變化做出一個新樹,你才算是有效的編輯了文字。

語法樹的第三個屬性是:語法樹是不變的且執行緒安全的。這意味著所獲得的語法樹是當前 程式碼狀態的一個快照,且永遠不會被改變。這允許多個使用者在需要加鎖或複製的情況下,以不同的執行緒在同一時間與同一棵語法樹進行互動。因為樹是固定不變的並且無法直接修改,通過建立額外的快照,工廠方法可以建立和更改語法樹。通過重用底層的節點,這些樹將十分高效,因此可以快速重建新版本且只需很少的額外記憶體。 

語法樹是名副其實的樹形結構,其中非終止元素是其他元素的父元素。每一個語法樹都是由節點、令牌和雜項構成。

句法節點(Syntax Nodes)

句法節點是句法樹的主要元素。這些節點呈現瞭如宣告、語句、子句和表示式。每一類句法節點都是通過繼承自SyntaxNode的類來表示的。節點類集是不可擴充套件的。

句法樹中所有的語法節點都是非終止節點,意思是它們可以一直有其他節點作為子節點。作為其他節點的子節點,每一個子節點都可以通過Parent屬性獲取父節點。因為節點和樹是固定不變的,因此節點的父節點從來不會變。樹的根節點的父節點是null。

每一個節點都有一個CHildNodes的方法,改方法返回一個源文件中基於自身位置的子節點序列。這個列表不包括任何令牌。每一個節點都有一個Descendant*的方法集合,比如DescendantNodes、DescendantTokens或DescendantTrivia,用於呈現所有該節點所在子樹根的節點、令牌或雜項(trivia)的列表。

另外,通過強型別屬性每一個語法節點子類可顯示所有相同得子節點。例如,一個 BinaryExpressionSyntax節點類有三個標示二進位制操作的額外屬性:Left、OperatorToken和Right。Left和Right是ExpressionSyntax,OperatorToken型別是SyntaxToken。

一些語法節點有可選子節點。例如,IfStatementSyntax有一個可選的ElseClauseSyntax。如果沒有子節點,該屬性返回null。

句法令牌(Syntax Tokens)

句法令牌是語言語法的終端,是程式碼的最小語法單位。它們從來都不是其他節點或令牌的父輩。句法令牌由關鍵詞、標示符、文字和標點符號組成。

出於效率的目的,SyntaxToken型別是CLR值型別。但是,不像句法節點,對於混合了屬性(依賴於所要表示令牌的種類)的所有令牌只有一種結構。

例如,一個整型文字令牌表示一個數字值。此外,對於令牌所指的原始源文字,文字令牌有一個Value屬性用來告訴你怎麼準確解碼整型值。該屬性被記為物件型別,因為它可能是許多原始型別中得一種。

ValueText屬性和Value屬性一樣,是告訴你同樣的資訊。但是這個屬性被定義為String型別。在C#源文字中的一個標示符可能包含Unicode轉義字元,但是轉義序列句法本身不是標示符名稱的構成部分。所以雖然令牌指向的原始文字包含有轉義序列,但是ValueText屬性卻不是。相反,它包含被轉義的Unicode字元標示符。

句法雜項(Syntax Trivia)

句法雜項是用來表示源文字中那些大量的對於理解程式碼來說是微不足道部分,比如空白字元、註釋和預處理指令。

因為雜項並不是普通語言語法的一部分,而且可能出現在任何兩個令牌之間,它們也不作為節點的孩子以包含在語法樹中。然而,當實現像重構這種特性以及為了完全忠於原文時它們又很重要,它們又作為語法樹的一部分存在。

你可以通過訪問一個令牌的前導雜項(LeadingTrivia)或緊隨雜項(TrailingTrivia)集合來訪問雜項。當源文字被解析後,雜項序列將與令牌關聯起來。通常,一個令牌擁有同一行上自身之後下一令牌之前的任何雜項。該行之後的任何雜項都與下一令牌關聯。原始檔的第一個令牌取得所有初始雜項,並且檔案中最後的雜項序列被附加到檔案結束令牌,否則寬度為零。

與句法節點和令牌不同,句法雜項沒有父節點。不過,因為它們是句法樹的一部分且每一個都與令牌關聯,你可以通過 Token 屬性來訪問所關聯的令牌。

與句法令牌一樣,雜項是值型別。單個SyntaxTrivia被用來描述各種各樣的雜項。

區塊

每個節點、令牌或者是雜項都能找到其在源文字中的位置和所包含的字元數。文字位置用 32 位整數來表示,它是以零為下標的 Unicode 字元索引。一個TextSpan物件是由開始位置和包含的字元陣列成,兩者都是整數形式。如果TextSpan長度為0,它則指向兩個字元中間的位置。

每個節點有兩個 TextSpan 型別的屬性: Span 和 FullSpan。

Span 屬性指的是從該節點的子樹中第一個令牌開始到最後一個令牌結束的文字區塊。這個區塊不包含任何前導或緊隨的雜項。

FullSpan 屬性則包含了該節點的正常區塊,再加上任何前導或緊隨的雜項。

例如:
if (x > 3)
      {
||        // this is bad
          |throw new Exception("Not right.");|  // better exception?||
      }


上面程式碼塊中用單個垂直豎線(|)括起來的是宣告節點的span。它是“throw new Exception("Not right.");”。完整的區塊是被雙垂直線(||)括起來的部分。它包括與span同樣的字元以及與其相關的前導和緊隨雜項。

種類 (Kinds )

節點、令牌或雜項都有個型別為 System.Int32的RawKind 屬性,用來標識它們所表示的確切句法元素。這個值可轉換為特定語言的列舉型別; C# 或 VB 語言都有個 SyntaxKind 列舉型別,列出了語法中所有可能的節點、令牌和雜項元素。通過呼叫CSharpSyntaxKind 或 VisualBasicSyntaxKind 擴充套件方法可以自動完成轉換。

RawKind屬性讓共享相同節點類的句法節點更容易被區分開。對於令牌和雜項來說,該屬性是區分一種元素與另一種元素的唯一途徑。

例如, BinaryExpressionSynta 類有 Left、 OperatorToken 和 Right 三個子類。而 Kind 屬性可以區分出它是 AddExpression、SubtractExpression 或者 MultiplyExpression 中的哪種句法節點。

錯誤(Errors)

甚至當源文字含有句法錯誤時,都能表明完整的句法樹是可往返於源的。當解析器遇到無法確定該語言定義的句法程式碼時,它將使用兩種技術中的一種來建立句法樹。

第一種,假如解析器需要一種特殊標記,但是卻找不到時,它將在句法樹該特殊標記應該存在的地方插入一個丟失標記。丟失標記描述需要的實際標記,但是它是一個空區,並且它的IsMissing屬性將返回真。

第二種,解析器可能跳過標記直到它找到了能夠讓它繼續解析一個標記。這種情況下,被跳過的標記將附加上一個有SkippedTokens的雜項節點。

語義方面(Working with Semantics)

句法樹表達的是原始碼的詞法和句法結構。雖然僅靠資訊就足以描述原始碼中的所有宣告和邏輯,但不足以表示哪些東西正在被引用。

例如,許多同名的型別、欄位、方法和本地變數分佈在原始碼的各處。雖然它們中的每個都是獨一無二的,但要知道某個標示符真正指向的是哪一個就需要對語言規則的深入理解。

這些是原始碼中的程式元素,而且程式也可以引用打包成程式集的編譯好的類庫。雖然程式集中不存在原始碼,因此也就不存在句法節點或者樹,但程式仍可以引用其中的元素。

在原始碼的句法模型之外,語義模型封裝了語言規則,讓你有個簡單的方法來對上面的情況作出區別。

合輯( compilation )

合輯 就是編譯 C#或VB 程式所需的所有東西,包括所有引用的程式集、編譯器選項及原始檔。

由於這些資訊儲存在同一個地方,因此原始碼中所包含的元素可以得到更加詳細的說明。合輯用符號表示每一個宣告的型別、成員或變數。它還提供多種方法,以幫助你找到相關符號,無論該符號是在原始碼中宣告的,還是作為後設資料從程式集中匯入的。

與句法樹一樣,合輯是不可變的。當你建立了合輯,你或者你想共享的其他人都不能改變它。不過,你可以從一個已存在的合輯中做出修改以此來建立一個新的合輯。比如,你可以建立一個 除了包含額外的原始檔或程式集引用以外,其他所有的地方都和一個已存在合輯一樣的新合輯,

符號(Symbols)

符號就是在原始碼中宣告的或者從程式集中匯入的後設資料的獨特元素。每個名稱空間、型別、方法、屬性、欄位、事件、引數或區域性變數都可用符號表示。

在 Compilation 這個型別中有各種各樣的方法和屬性來幫你查詢符號。比如你可以通過公用後設資料名稱來查詢宣告的型別的符號。你也可以以符號樹的形式來訪問整個符號表,這些符號以全域性名稱空間為根節點。

符號也包含編譯器從原始碼或者後設資料中得到的附加資訊,例如其他被引用的符號。每種符號都是用從ISymbol派生的介面來表示,每個介面都有自己的方法和屬性來詳細說明編譯器收集到的資訊。其中的許多屬性直接引用其他符號。比如,IMethodSymbol 類的 ReturnType 屬性,告訴你方法宣告所引用的實際型別符號。

符號是名稱空間、型別和成員在原始碼和後設資料之間的通用表示。例如,在原始碼中宣告的方法和從後設資料匯入的方法,均表示為有相同屬性的 IMethodSymbol。 

用System.Reflection API表示的符號在概念上與CLT型別系統相似,但它們是模型而不僅僅是型別,因此要更豐富些。名稱空間、本地變數和標籤都是符號。此外,符號表現為語言概念而非 CLR 概念。它們有很多重疊的地方,但也有很多有意義的區別。比如,在 C# 或 Visual Basic 中的迭代器方法(iterator)是單一符號。但是當迭代器方法轉換為 CLR 後設資料,它是一個型別和多個方法。

語義模型

語義模型 展示了單個原始檔的所有語義資訊。使用它你能發現如下內容:
  • 符號被引用在源中特定位置。
  • 任何型別的表示式組合。
  • 包含錯誤和警告的所有診斷。
  • 變數如何流入和流出源。
  • 更多猜測性問題的答案。
工作區方面(Working with a Workspace)

工作區層是對整個解決方案做程式碼分析和重構的起點。在該層內,工作區API將協助你組織一個解決方案中有關專案的所有資訊到一個單獨物件模型中,提供你直接訪問如源文字、句法樹、語義模型和合輯的編譯器層物件模型,而不需要解析檔案、配置選項或管理內部專案依賴。

宿主環境,比如IDE,提供了一個工作區讓你開啟解決方案。也可以簡單的通過載入一個解決方案檔案在IDE外部來使用該模型。

工作區(Workspace)

工作區作為專案的一個集合,是解決方案的活躍展現,每一個都是文件的集合。典型的,工作區會被繫結一個宿主環境,作為一種使用者型別或操作效能是經常變化的。

工作區提供了訪問解決方案的當前模型。當宿主環境發生變化時,工作區就好觸發相應的事件,並且更新CurrentSolution屬性。例如,當一個文字編輯器中的使用者型別與源文件中的其中一個相關聯時,工作區將用事件像整個解決方案的模型傳送已變更訊號,且告知是哪一個文件被修改。你可以從正確性、高亮內容的意義或對程式碼的更改提出建議來分析新模型,從而對這些變化做出反應。
你也可以建立單獨的工作區,前提是斷開宿主環境,或者用在一個沒有宿主環境的應用中。

解決方案(Solutions),專案(Projects),文件(Documents)

雖然每按一次鍵都可能改變工作區,你也可以在隔離的解決方案模型中工作。

解決方案是工程和文件的固定模型。這意味著模型無需加鎖或複製就可以被共享。在你從工作區的CurrentSolution屬性中獲取一個解決方案例項後,該例項就不會再變了。不過,像語法樹和合輯,你可以在已存在的解決方案和特定修改上通過構造新例項來更改解決方案。要在工作區中看到你做的更改,你必須明確的將更改的解決方案應用到工作區。

專案是整體不變的解決方案模型的一部分。它呈現了所有的原始碼文件、解析和合輯選項以及程式集和專案到專案的引用。從一個專案中,你可以訪問相應的合輯而無需判斷專案依賴項或解析任何原始檔。

文件也是整體不變的解決方案模型的一部分。文件呈現了一個單個原始檔,從中你可以訪問檔案、語法樹和語義模型的文字。下面的圖顯示了與宿主環境、工具和怎樣做的更改有關的工作區。



總結

.NET編譯器平臺(“Roslyn”)公開了一組編譯器API和工作區API,它們提供了關於你的原始碼的豐富資訊,並且完全忠於C#和Visual Basic語言。讓編譯器作為平臺的過渡,為集中建立程式碼工具和應用程式大大降低了進入門檻。它創造了許多革新,如:meta-Programming、程式碼生成和轉換,互動使用C#和VB語言,和某些特殊領域的嵌入式C#和VB語言。
相關閱讀
評論(1)

相關文章