介绍一般来说,编译器是一个黑箱,源代码从一端进入,然后箱子中发生一些奇妙的变化,最后从另一端出来目标文件或程序集。编译器施展它们的魔法,它们必须对所处理的代码进行深入的理解,不过相关知识不是每个人都需要知道,除了实现编译器的大法师。因此在转换输出完成后相关的信息就会被遗忘。
对编译器来说,几十年来一直很好地为我们所用,但只是会用编译器已经不够。我们越来越依赖于集成开发环境(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 这些工具,这些工具依赖于诊断信息,以用于体验如基于策略停止生成、在编辑器中显示波浪线并提示代码修复。脚本 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 属性则包含了该节点的正常区块,再加上任何前导或紧随的杂项。 例如:
上面代码块中用单个垂直竖线(|)括起来的是声明节点的span。它是“throw new Exception("Not right.");”。完整的区块是被双垂直线(||)括起来的部分。它包括与span同样的字符以及与其相关的前导和紧随杂项。 |