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

jopen 10年前發布 | 25K 次閱讀 .NET .NET開發

介紹

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

對編譯器來說,幾十年來一直很好地為我們所用,但只是會用編譯器已經不夠。我們越來越依賴于集成開發環境(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編譯器的代碼分析。

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

這條管道的每一部分,現在都是單獨的組件。首先,在解析階段,其中原始碼被記號化和解析成不同語言的句法。第二,聲明階段,即從源代碼和輸入的metadata進行分析,以形成命名符號。下一個階段,原始碼中的標示符(identifier)被匹配成符號(symbol)。最后發布(emit)階段,所有編譯器構建的信息作為一個程序集被發布。

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

對應每一個階段都會有一個對象模型,它允許在該階段訪問相關信息。解析階段表現為句法樹(syntax tree),聲明階段則是分層語法表(hierarchical symbol table),綁定階段作為一個模型,用以展現編譯器進行語義分析后的結果,發布階段則作為API以產生IL字節碼。

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

每個編譯器將這些組件組合在一起,作為一個單一的端到端的(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。

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

編譯器API(Compiler APIs)

編譯器層包含的對象模型與編譯器管道每一部分的公開信息相對應,包括語法和語義兩部分。編譯器層還包含了對編譯器單獨調用的固定快照,其中包括程序集引 用、編譯器選項以及源代碼文件。針對C#和Visual Basic語言有兩種不同的API。兩種API大小差不多,但是對每種語言又進行了高度的定制。該層不依賴于Visual Studio組件。

診斷 API

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

腳本 API

作為編譯器層的一部分,該團隊還提供了 宿主(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 屬性則包含了該節點的正常區塊,再加上任何前導或緊隨的雜項。

例如:

1  if (x > 3)
2       {
3 ||        // this is bad
4           |throw new Exception("Not right.");|  // better exception?||
5       }

上面代碼塊中用單個垂直豎線(|)括起來的是聲明節點的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“)介紹

總結

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

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!