.NET 编译器(”Roslyn“)介绍

jopen 10年前

介绍

一般来说,编译器是一个黑箱,源代码从一端进入,然后箱子中发生一些奇妙的变化,最后从另一端出来目标文件或程序集。编译器施展它们的魔法,它们必须对所 处理的代码进行深入的理解,不过相关知识不是每个人都需要知道,除了实现编译器的大法师。因此在转换输出完成后相关的信息就会被遗忘。

对编译器来说,几十年来一直很好地为我们所用,但只是会用编译器已经不够。我们越来越依赖于集成开发环境(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       }
</div> </div>

上面代码块中用单个垂直竖线(|)括起来的是声明节点的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语言。