孜孜不倦的程序员

Roslyn 的兴起

Joe Hummel
Ted Neward

Ted Neward过去数年来,各类计算机专业人员、思想领导者和专家倡导将域特定语言 (DSL) 的概念作为研究软件问题解决方案的一种方法。如果“临时用户”可以使用 DSL 语法在系统中调整和修改业务规则,这种方法似乎尤其适用。这对许多开发人员来说是软件的“圣杯” — 构建在业务需要更改时人们可自行维护的系统。

但是,对 DSL 的一个主要批评是编写编译器已是一项失传的技术这一事实。来自各行各业的程序员通常将创建编译器或解释器看成一种黑暗技术。

在今年的 Build 2014 大会上,Microsoft 正式公布了 Microsoft .NET Framework 开发生态系统的一项人尽皆知的秘密 — Roslyn 开源。这是基于 C# 和 Visual Basic 语言的改进/重建编译器系统。对于某些人而言,这是 Microsoft 将他们的语言放入开源社区并从中获益的机会,好处包括 Bug 错误、增强功能、公开评论新语言功能等。对于开发人员而言,这是深入探讨编译器(以及解释器,尽管鉴于所涉及的语言,Roslyn 专注于编译)如何在底层工作的机会。

有关详细背景(以及安装提示),请查看位于 roslyn.codeplex.com 上的 Roslyn CodePlex 页面。和一直尚未发布的位数一样,强烈建议您在虚拟机或不太重要的计算机上执行此操作。

Roslyn 基础

从较高层次来看,编译器的目标是将程序员的输入(源代码)转换成可执行的输出,例如 .NET 程序集或本机 .exe 文件。尽管编译器中模块的准确名称各不相同,但我们通常认为编译器分为两个基本部分:前端和后端(请参阅图 1)。

高级编译器设计
图 1 高级编译器设计

前端的主要职责之一是验证入站源代码的格式是否正确。与所有编程语言一样,程序员必须遵守特定的格式,以便计算机清楚、明确地了解要执行的操作。以下列 C# 语句为例:

if x < 0   <-- syntax error!
  x = 0;

这在语法上不正确,因为 if 条件必须用 ( ) 括起来,如下所示:

if (x < 0)
  x = 0;

解析代码后,后端负责对源进行更深入的验证,如类型安全违规:

string x = "123";
if (x < 0)                   <-- semantic error!
  x = 0;                     <-- semantic error!

顺便说一下,这些示例是语言实施者的精心设计决策。针对哪些相对较好的问题一直饱受争议。要了解详细信息,请访问任何在线编程论坛,并键入“D00d ur language sux”。您很快就会发现自己陷入了难以忘怀的“教育”会话。

假设没有语法或语义错误,编译继续进行,后端将输入转换成所需目标语言的等效程序。

深入研究

尽管使用最简单的语言您可能要采取两部分方法,但语言编译器/解释器通常远不止分解为两部分。在下一级别的复杂性中,绝大多数编译器自动在六个主要阶段进行操作,前端有两个阶段,后端有四个阶段(请参阅图 2)。

编译器的主要阶段
图 2 编译器的主要阶段

前端执行前两个阶段:词法分析和解析。词法分析的目标是读取输入程序并输出令牌 — 关键字、标点符号和标识符等。还会保留每个令牌的位置,确保程序的格式不会丢失。假设源文件开头从以下程序段开始:

// Comment
if (score>100)
  grade = "A++";

词法分析的输出应为如下令牌序列:

IfKeyword                       @ Span=[12..14)
OpenParenToken              @ Span=[15..16)
IdentifierToken                  @ Span=[16..21), Value=score
GreaterThanToken             @ Span=[21..22)
NumericLiteralToken           @ Span=[22..25), Value=100
CloseParenToken              @ Span=[25..26)
IdentifierToken                  @ Span=[30..35), Value=grade
EqualsToken                    @ Span=[36..37)
StringLiteralToken             @ Span=[38..43), Value=A++
SemicolonToken               @ Span=[43..44)

每个令牌都携带其他信息,如从源文件开头算起的起始位置和结束位置(跨度)。请注意,IfKeyword 开始于位置 12。这是由于跨越 [0..10) 的注释以及跨越 [10..12) 的行尾字符造成的。虽然从技术角度来讲这并不属于令牌,但词法分析器中的输出通常包含有关空格的信息(包括注释)。在 .NET 编译器中,空格被传递为语法琐碎内容。

编译器的第二个阶段是解析。解析器与词法分析器协同工作来执行语法分析。解析器执行绝大多数的工作,从词法分析器请求令牌,因为词法分析器针对源语言的各种语法规则来检查输入程序。例如,C# 程序员都了解 if 语句的语法:

if  (  condition  )  then-part  [ else-part ]

[ … ] 表示 else-part 可选。解析器通过匹配令牌以及对更加复杂的语法元素(如 condition 和 then-part)应用其他规则,来强制执行这个规则:

void if( )
{
  match(IfKeyword);
  match(OpenParenToken);
  condition();
  match(CloseParenToken);
  then_part();
  if (lookahead(ElseKeyword))
  else_part();
}

函数匹配 (T) 调用词法分析器获取下一令牌,并查看此令牌是否与 T 匹配。如果匹配,通常会继续编译。否则,会报告一个语法错误。解析器最简单的处理方式是使用匹配函数针对语法错误引发异常。这可有效地停止编译。此类编译如下所示:

void match(SyntaxToken T)
{
  var next = lexer.NextToken();
  if (next == T)
  ;  // Keep going, all is well:
  else
  throw new SyntaxError(...);
}

我们很幸运,.NET 编译器包含更复杂的解析器。它能在面对严重的语法错误时继续运行。

假设没有语法错误,前端的工作实质上已经完成。它只剩下一个任务,就是将它的工作传递给后端。它内部存储的形式称为中间表示形式或 IR。(尽管术语相似,但 IR 与 .NET 公共中间语言毫无关联。).NET 编译器中的解析器将抽象语法树 (AST) 构建为 IR,并将该树传递到后端。

考虑到 C# 和 Visual Basic 程序的层次结构特性,树自然就是 IR。程序将包含一个或多个类。类包含属性和方法,属性和方法包含语句,语句通常包含块,块包含其他语句。AST 的目的是基于其语法结构来表示程序。AST 中的“抽象”表示缺少语法修饰,如 ; 和 ( )。例如,考虑 C# 语句的以下序列(假设这些编译没有错误):

sum = 0;
foreach (var x in A)   // A is an array:
  sum += x;
avg = sum / A.Length;

从较高层次来看,此代码片段的 AST 如图 3 所示。

C# 代码片段的高级抽象语法树
图 3 C# 代码片段的高级抽象语法树(为简单起见,详细信息欠奉)

AST 捕获程序的必需信息:语句、语句的顺序、每个语句的片段等。丢弃不必要的语法,如所有分号。图 3 中要了解的 AST 主要功能是 AST 可捕获程序的语法结构。

即如何编写程序,而不是如何执行程序。请考虑 foreach 语句,在它遍历集合时会循环零次或多次。AST 捕获 foreach 语句的组件,即循环变量、集合和主体。AST 没有表达的是 foreach 可能会不断重复。实际上,如果查看该树,树中并没有箭头表示如何执行 foreach。想知道的唯一方法就是通过了解 foreach 关键字 == 循环。

AST 是非常好的 IR,具有一个主要优点:它们易于构建和理解。缺点是,在 AST 上更难执行更为复杂的分析,如编译器后端使用的分析。因此,编译器通常会保留多个 IR,包括 AST 的常见替代方法。此替代方法是控制流图 (CFG),表示基于其控制流的程序:循环、if-then-else 语句、异常等。(我们将在下一专栏对其进行详细介绍。)

要了解如何在 .NET 编译器中使用 AST,最好的方法是通过 Roslyn 语法可视化工具。此工具安装为 Roslyn SDK 的一部分。安装后,在 Visual Studio 2013 中打开任意 C# 或 Visual Basic 程序,将光标放到感兴趣的源代码行并打开可视化工具。您将看到“视图”菜单,其他 Windows 和 Roslyn 语法可视化工具(请参阅图 4)。

Visual Studio 2013 中的 Roslyn 语法可视化工具
图 4 Visual Studio 2013 中的 Roslyn 语法可视化工具

作为一个具体示例,请考虑我们前面解析过的 if 语句:

 

// Comment
  if (score>100)
    grade = "A++";

图 5 显示了用 .NET 编译器构建的相应 AST 片段。

.NET 编译器为 IfStatement 构建的抽象语句树
图 5 .NET 编译器为 IfStatement 构建的抽象语句树

与很多东西一样,此树最初看起来很庞大。但请记住两件事情。第一,树只是较早源代码语句的扩展,因此实际上很容易走查树,并查看它如何映射回原始源代码。第二,AST 适合计算机使用,而不适合人类。通常,人查看 AST 的唯一情况是调试解析器。还请注意,有关文法和解析的更完整课程远远超出了我们这里讨论的范围。想要深入研究这项工作的人们有许多可用的资源。本文旨在做简要介绍,而不是深入研究。

总结

不管怎样,我们对 Roslyn 的探究尚未结束,因此请继续关注。既然您对更深入研究 Roslyn 感兴趣,我们建议您安装 Roslyn。请来阅读这些文档,从 Roslyn CodePlex 页面开始。

如果您想要更加深入地研究解析和文法,这里提供了许多书籍。有久负盛名的“龙书”,也称为《Compilers:Principles, Techniques & Tools》(Addison Wesley, 2006)。如果您对以 .NET 为中心的方法更为感兴趣,请参考《Compiling for the .NET Common Language Runtime (CLR)》,作者:John Gough (Prentice Hall, 2001);或 Ronald Mak 的《Writing Compilers and Interpeters:A Software Engineering Approach》(Wiley, 2009)。祝您工作愉快!


Joe Hummel 是芝加哥伊利诺伊大学的副研究员,Pluralsight.com 的内容创建者,Visual C++ MVP 以及私人顾问。他在加利福尼亚大学欧文分校获得了高性能计算领域的博士学位,并且对并行处理方面很有兴趣。他住在芝加哥地区,如果他不去航海,您应该可以从 joe@joehummel.net 与他联络。

Ted Neward 是 iTrellis(一家咨询服务公司)的 CTO。他曾写过 100 多篇文章,独自撰写过十几本书,包括《Professional F# 2.0》(Wrox, 2010)。他是一位 F# MVP,经常在全球会议上发表演讲。他定期担任顾问和导师,如果您感兴趣,请通过 ted@tedneward.comted@itrellis.com 与他联系。

衷心感谢以下 Microsoft 技术专家对本文的审阅:Derick Campbell