65.9K
CodeProject 正在变化。 阅读更多。
Home

Mono 的 C# 编译器如何工作?

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.99/5 (54投票s)

2012年2月15日

CPOL

19分钟阅读

viewsIcon

158386

downloadIcon

1089

Mono 是一个开源的免费编程语言项目。它基于 ECMA 的 C# 语言和通用语言运行时 (CLR) 标准,实现了微软的 .NET 框架。在本文中,我将探讨 Mono C# 编译器的工作原理。

目录

引言

Mono 是一个开源的免费编程语言项目。它是微软 .NET 框架的一个实现,基于欧洲信息与通信系统标准化协会(ECMA)的 C# 语言和通用语言运行时(CLR)标准。Mono C# 编译器由 Miguel de Icaza 发起。在表 1 中,我尝试展示 Mono 的不同组件以及这些组件功能的简要描述。 

表 1:Mono 源代码组件
组件 (Component)描述
C#编译器Mono 的 C# 编译器是基于 ECMA 规范的 C# 语言实现。它现在已支持 C# 1.0、2.0、3.0、4.0。
Mono 运行时该运行时实现了 ECMA 通用语言基础结构 (CLI)。运行时提供了即时 (JIT) 编译器、预编译 (AOT) 编译器、库加载器、垃圾收集器、线程系统以及互操作性功能。
基类库Mono 平台提供了一套全面的类,为构建应用程序提供了坚实的基础。这些类与微软的 .NET 框架类兼容。
Mono 类库Mono 还提供了许多超出微软提供的基类库范围的类。这些类提供了额外的功能,在构建 Linux 应用程序时尤其有用。例如 Gtk+、Zip 文件、LDAP、OpenGL、Cairo、POSIX 等的类。

注意:上表中显示的信息来自 http://www.mono-project.com/What_is_Mono

Mono 编译器有许多版本。表 2 显示了不同版本的 Mono 编译器及其支持的框架。

表 2:Mono 编译器版本和相关框架
编译器版本目标框架
mcs1.1
gmcs2.0
smcs2.1
dmcs4.0

注意:表中显示的信息来自 http://www.mono-project.com/CSharp_Compiler

从 Mono 2.8 开始,编译器 mcs 现在默认使用 3.x 语言规范。

获取 Mono 源代码

Mono 是一个免费可用的开源 C# 编程语言项目。如果你想下载 Mono C# 编译器项目的源代码,有很多地方可以获取。例如,我们可以使用 GitHub。Mono 源代码在 GitHub 上的 URL 是 https://github.com/mono/mono/branches。或者我们也可以从其他地方下载,比如 http://www.go-mono.com/mono-downloads/download.html。我从 GitHub 网站下载了 Mono 源代码(图 4.1)。Mono 有几个分支,例如,如图 4.1 所示,Mono 有 mono-2-10、mono-2-10-8、mono-2-6、mono-2-8 等。在下面的表 3 中,我展示了 Mono 的不同目录及其简短描述。

表 3:Mono 源代码目录
docs关于 Mono 运行时的技术文档。
data作为 Mono 运行时一部分安装的配置文件。
monoMono 运行时的核心。
metadata对象系统和元数据读取器。
mini即时编译器。
disCIL 可执行文件反汇编器。
cliJIT 和解释器的通用代码。
io-layer用于模拟 .NET IO 模型的 I/O 层和系统抽象。
cil通用中间表示,CIL 字节码的 XML 定义。
interpCLI 可执行文件的解释器(已废弃)。
arch特定于体系结构的部分。
mcsMono 编译器代码的核心
mcs
mcs编译器源代码
jay解析器生成器
man各种 Mono 命令和程序的手册页。
samples一些关于将 Mono 运行时用作嵌入式库的简单示例程序。
scripts用于调用 Mono 和相应程序的脚本。
runtime包含链接 mono/ 和 mcs/ 构建系统的 Makefiles 的目录。
../olive如果 ../olive 目录存在(作为独立检出)于 Mono 模块之外,该目录会自动配置为与此模块共享相同的前缀。

注意:以上 Mono 源代码目录信息来自 https://github.com/mono/mono

Mono 编译器的源代码位于 /mono/mcsmcs 文件夹内。

Jay

Jay 是一个源自 Berkeley Yacc 的开源编译器-编译器工具。它在 Mono 项目中用作编译器-编译器工具,以生成 Mono C# 编译器的解析器。Jay 从语法文件中读取语法规范,并为其生成一个 LR 解析器。Jay 使用这个 cs-parser.jay 文件来生成 cs-parser.cs 文件,后者将作为解析器被 Mono C# 编译器使用。

cs-parser.jay 到 cs-parser.cs 的转换

Cygwin 是一套开源工具,它为 Windows 提供了一个类似 Linux 的环境,使得 Linux 应用程序(例如 Shell)可以在 Windows 中使用。现在我们假设桌面上有一个可用的 Cygwin 环境。我们将通过点击“开始” > “所有程序” > “Cygwin” > “Cygwin Terminal”来打开 Cygwin 终端。当我们打开 Cygwin 终端时,它将如图 1 所示。

330184/1_CygwinOpenmode.jpg

图 1:Cygwin 打开模式

请将 Mono 源代码复制到 Cygwin 安装目录的 /usr/src 目录中。然后打开 Cygwin 终端并输入代码清单 1 中列出的命令。

代码清单 1:将 cs-parser.jay 转换为 cs-parser.cs 的 Bash 命令
$cd /usr/src/Mono/mcs
$cd jay
$make
$cd ..
$cd mcs
$../jay/jay.exe -ctv < ../jay/skeleton.cs cs-parser.jay > cs-parser.cs

请看下图

330184/2_BashCommandOutput.jpg

图 2:Bash 命令输出

因此,在用适当的参数执行 Jay.exe 后,它会将 cs-parser.jay 文件转换为 cs-parser.cs,后者是 Mono 的解析器。

Mono 源代码关系

根据 Mono 源代码附带的文档(mcs\mcs\compiler.txthttps://github.com/mono/mono/blob/master/mcs/docs/compiler.txt),Mono C# 编译器的整个源代码文件被分为五类:基础结构、解析、表达式、语句和声明、类、结构体、枚举。如果我们查看下面的 Mono C# 编译器源代码分类表 4,将更容易理解 Mono 编译器构造中使用的所有类型。

表 4:Mono 源代码分类
Mono 编译器源代码分类
基础结构解析表达式语句声明、类、结构体、枚举
driver.cscs-tokenizer.csecore.cs statement.csdecl.cs
codegen.cs cs-parser.jay, cs-parser.csexpression.cs iterators.csclass.cs
attribute.cs location.csassign.csdelegate.cs
rootcontext.csconstant.cs enum.cs
typemanager.cs literal.csinterface.cs
report.cscfold.csparameter.cs
support.cs pending.cs

注意:以上 Mono 源代码分类信息来自 https://github.com/mono/mono/blob/master/mcs/docs/compiler.txt

深入 Mono 编译

Mono C# 编译器从 driver.cs 文件开始编译。通过调用 public bool Compile () 方法,Mono 启动其编译过程。它然后初始化 RootContext 类的 TopLevelTypes 变量。之后,它调用 driver 类的 Parse() 方法。Parse() 方法接着调用 void Parse (CompilationUnit file) 开始从源代码文件读取。从源代码文件读取后,driver.cs 文件通过调用

void Parse (SeekableStreamReader reader, CompilationUnit file)

方法来启动解析过程。它将通过调用

public CSharpParser(SeekableStreamReader reader, CompilationUnit file, CompilerContext ctx) 

构造函数来创建 Mono 解析器的一个实例,即创建一个 CSharpParser 对象。如果我们看一下代码清单 1 中列出的 driver.cs 文件中 Compile 方法的部分代码,我们可以看到编译过程的主要流程。

代码清单 1:Compile 方法的部分源代码
public bool Compile()
{
   // TODO: Should be passed to parser as an argument
   RootContext.ToplevelTypes = new ModuleContainer(ctx, RootContext.Unsafe);
   Parse();
   ProcessDefaultConfig();
   GlobalRootNamespace.Instance.AddModuleReference(RootContext.ToplevelTypes.Builder);
   //
   // Load assemblies required
   //
   LoadReferences();
   TypeManager.InitOptionalCoreTypes(ctx);
   //
   // The second pass of the compiler
   //
   RootContext.ResolveTree();
   if (!RootContext.StdLib)
     RootContext.BootCorlib_PopulateCoreTypes();
   RootContext.PopulateTypes();
   //
   // Verify using aliases now
   //
   NamespaceEntry.VerifyAllUsing();
   if (Report.Errors > 0)
   {
     return false;
   }
   CodeGen.Assembly.Resolve();
  if (RootContext.VerifyClsCompliance)
  {
    //......
  }
  RootContext.EmitCode();
  RootContext.CloseTypes();
  CodeGen.Save(output_file, want_debugging_support, Report);
}

从代码清单 1 中,我们可以看到 Mono 解析器通过调用 driver 类的 public void parse() 方法开始解析,该方法将调用代码清单 2 中列出的 Parse 方法以继续解析。

代码清单 2:Parse 方法的源代码
void Parse (SeekableStreamReader reader, CompilationUnit file)
{
   CSharpParser parser = new CSharpParser (reader, file, ctx);
   try {
     parser.parse ();
   } catch (Exception ex) {
   Report.Error(589, parser.Lexer.Location,
      "Compilation aborted in file `{0}', {1}", file.Name, ex);
   }
}

这个 Parse 方法将通过调用代码清单 3 中列出的构造函数来创建 CSharpParser 类的一个实例。这将创建一个由编译器-编译器工具 Jay 生成的 Mono 解析器实例,正如我们之前讨论过的。

代码清单 3:CSharpParser 类的构造函数
public CSharpParser (SeekableStreamReader reader, CompilationUnit file, CompilerContext ctx)

代码清单 3 中列出的构造函数将接收文件读取器流和在 driver 类中定义的编译器上下文的只读值。

代码清单 4:cs-parser.jay 文件中的 CSharpParser 声明
%{
using System.Text;
using System.IO;
using System;
namespace Mono.CSharp
{
using System.Collections;
/// <summary>
///    The C# Parser
/// 
    public class CSharpParser
    {

CSharpParser 创建的解析器对象将调用 CSharpParser 类的内部 parse() 方法,该方法将调用由编译器-编译器生成的 yyparse 方法。为了调用 yyparse,它需要将词法分析器(或分词器)作为参数传入。代码清单 5 中列出的代码显示了 yyparse 方法的签名,它接收词法分析器作为参数。

代码清单 5:yyparse 方法的签名
internal Object yyparse (yyParser.yyInput yyLex)

解析将在这个 yyparse 方法中进行。这个 yyparse 方法将解析由词法分析器生成的每个标记。

代码清单 6:CSharpParser 构造函数中的词法分析器初始化
public CSharpParser (SeekableStreamReader reader, CompilationUnit file, CompilerContext ctx)
{
  // Code has been removed
  lexer = new Tokenizer (reader, file, ctx);
}

yyparse 方法将调用词法分析器的 xToken() 方法来为其生成标记。词法分析器将通过对程序的源代码进行词法分析来返回一个标记。在我们继续之前,我们需要理解标记生成过程。在 Mono 中,标记生成是一个有趣的过程,Tokenizer 类会从源代码(例如,本例中代码清单 13 列出的 ClassToParse.cs)中逐个读取字符,并与存储在分词器类中的关键字进行匹配,以确定它是否有关联的关键字,或判断它是否为字面量。词法分析器将通过调用 Is_identifier_start_character(int c) 方法来执行此操作,通过调用 get_char() 方法来判断字符是否为标识符。如果它是标识符,那么词法分析器将调用 consume_identifier(int s) 方法来消费该标识符。该逻辑如代码清单 7 所示。

代码清单 7:tokenizer.cs 中的标识符检查
if (is_identifier_start_character (c)) {
   tokens_seen = true;
   return consume_identifier (c);
}

当词法分析器尝试消费标识符时,它会通过调用 GetKeyword 方法来查找是否有任何关键字匹配,如代码清单 8 所示。

代码清单 8:tokenizer.cs 的 consume_identifier
private int consume_identifier (int c, bool quoted)
{
    while ((c = get_char ()) != -1) {
    // code has been removed from above for simplicity
    if (id_builder [0] >= '_' && !quoted) {
        int keyword = GetKeyword (id_builder, pos);
        if (keyword != -1) {
            // TODO: No need to store location for keyword, required location cleanup
            val = loc;
            return keyword;
        }
    }
    //......
    CharArrayHashtable identifiers_group = identifiers [pos];
    if (identifiers_group != null) {
        val = identifiers_group [id_builder];
        if (val != null) {
            val = new LocatedToken (loc, (string) val);
            if (quoted)
            AddEscapedIdentifier ((LocatedToken) val);
            return Token.IDENTIFIER;
        }
    }
    //.................
    val = new String (id_builder, 0, pos);
    identifiers_group.Add (chars, val);
    //................
    val = new LocatedToken (loc, (string) val);
    if (quoted)
    AddEscapedIdentifier ((LocatedToken) val);
    return Token.IDENTIFIER;
}

如果标记不是关键字,则词法分析器会将其标记为标识符,并返回 IDENTIFIER 作为标记类型,而它从流中消费的单词将存储在词法分析器类(即 Tokenizer 类,cs-tokenizer.cs)的 val 对象中。val 是在词法分析器(即 cs-tokenizer.cs)中定义的 object 类型的私有变量。Tokenizer.cs 文件中的 val 对象可以通过 Tokenizer 类中名为 value 的属性访问,如代码清单 9 所示。

代码清单 9:tokenizer.cs 的 value 属性
public Object value ()
{
  return val;
}

完成检查过程后,词法分析器将把标记返回给解析器以继续解析过程。因此,当解析器发现词法分析器返回的标记值(例如,对于代码清单 5.18 中列出的例子,IDENTIFIER 的值为 418;有关 Mono 标记的详细信息,请参见附录),解析器会将其视为标识符,并尝试访问与该标识符关联的 val。为了访问 Tokenizer 类的 val,解析器将调用词法分析器的 value 属性,并将该值赋给解析器的 yyValyyVal 是在 yyparse 方法中定义的 object 类型局部变量)。每个这样的 yyVal 将被存储在 CSharpParser 类的 yyparse 方法内的 yyVals 数组中。存储在 yyVals 数组中的值稍后将用作语法的替换变量。在下面的代码清单 5.13 中,我们可以看到 yyVal 是如何存储在 yyVals 数组中的。注意:替换是语法-解析器通信,即将一个值传递到语法文件中。例如,如果我们想从解析中替换语法中定义的变量 $1、$2 或 $3,我们必须从解析器传递替换值。在这里,yyVals 将根据标记为语法存储所有这些替换变量。

代码清单 10:cs-parser.cs 的 yyparse 方法的源代码
internal Object yyparse(yyParser.yyInput yyLex)
{
   /*……….*/
   for (int yyTop = 0; ; ++yyTop)
   {
   /*……….*/
   yyVals[yyTop] = yyVal;
   if (debug != null) debug.push(yyState, yyVal);
   /*……….*/
}

当解析器使用 yyVal=yyLex.value() 语句从 Tokenizer 访问值时,它随后将 yyVal 赋回给 yyVals,如代码清单 5.13 所示。词法分析器和解析器之间的这种通信就像即时(Just in Time)一样,即每当解析器需要一个标记时,它会通过调用词法分析器的 xToken() 方法来请求,词法分析器将执行 xtoken() 方法为解析器执行操作。因此,当解析器从词法分析器获得一个标记时,它将计算 yyN 值。在 Mono 中,yyN 值的用途是与适当的语法动作块匹配。yyN 是解析器中一个重要的变量,因为它实际上用于在词法分析器从源代码文件返回的标记值与语法(语言规范,例如 cs-parser.jay)之间建立映射。使用标记值,解析器将与 yyparse 方法中定义的语法动作块(由编译器-编译器 Jay 生成的 switch case 语句)匹配。如果它匹配任何 case 语句,那么解析器将执行在匹配的 case 条件中定义的相应代码块。此代码块将初始化相关的抽象语法树节点类型,例如,Statement 对象或 Expression 对象的类型,并将其添加到 TypeContainer 中。

简而言之,当任何标记值与 yyNN 值匹配时,解析器将执行语法的动作块,例如,在 Mono 的语法文件 cs-parser.jay 中,第 1265 行有如下所示的方法声明语法,见代码清单 11。

代码清单 11:cs-parser.jay 中方法的语法声明
method_declaration
: method_header {
if (RootContext.Documentation != null)
Lexer.doc_state = XmlCommentState.NotAllowed;
}
method_body
{
Method method = (Method) $1;
method.Block = (ToplevelBlock) $3;
current_container.AddMethod (method);
if (current_container.Kind == Kind.Interface && method.Block != null) {
Report.Error (531, method.Location, "`{0}': interface members cannot have a definition", method.GetSignatureForError ());
}
current_generic_method = null;
current_local_parameters = null;
if (RootContext.Documentation != null)
Lexer.doc_state = XmlCommentState.Allowed;
};

在语法规范文件(本例中为 cs-parser.jay 文件)中指定的方法语法,也在 cs-parser.cs 文件的一个 case 语句中定义。在这种情况下,case 条件值为 159(159 是由编译器-编译器工具,本例中为 Jay,在将 cs-parser.jay 转换为 cs-parser.cs 时给出的),如代码清单 12 所示。每当词法分析器返回一个标记值,该值成为 159 作为 yyN 值(yyN 值是基于标记生成的),语法中为方法定义的代码块就会执行。这个代码块实际上是将 Method 类的实例添加到类型容器中。

代码清单 12:yyparser 方法的部分代码块
switch (yyN)
{
case 159:
#line 1265 "cs-parser.jay"
{
Method method = (Method)yyVals[-2 + yyTop];
method.Block = (ToplevelBlock)yyVals[0 + yyTop];
current_container.AddMethod(method);
if (current_container.Kind == Kind.Interface && method.Block != null)
{
Report.Error(531, method.Location, 
    "`{0}': interface members cannot have a definition", 
    method.GetSignatureForError());
}
current_generic_method = null;
current_local_parameters = null;
if (RootContext.Documentation != null)
Lexer.doc_state = XmlCommentState.Allowed;
}
break;
}

从上面代码清单 12 中列出的代码,我们可以看到此语法与 cs-parser.jay 文件之间通过替换变量进行通信。在代码清单 11 中有两个替换值 $1,它将被代码清单 12 中 (Method)yyVals[-2 + yyTop] 返回的值替换,$3 则被 (ToplevelBlock)yyVals[0 + yyTop] 的返回值替换。这就是整个语法如何与词法分析器返回的标记匹配,并且动作块会根据语法执行。同样的过程将继续,直到源代码文件结束,即完成从源代码中搜索标记。

调试 Mono 编译

我们将使用代码清单 13 进行实验,并尝试通过使用 Visual Studio 2010 作为 IDE 调试 Mono 编译器来理解以下两个基本问题:

  • Mono 如何检索标记并解析源代码。
  • 它如何构建抽象语法树 (AST)。

下面代码清单 13 中列出的 ClassToParse 类是使用 C# 编写的,将用作本实验的源代码。ClassToParse 是一个简单的程序,它有一个 using 语句和一个命名空间声明。它还定义了一个类,在类内部,它有一个作为入口点的 Main 方法。

代码清单 13:在控制台上显示“Hello! world”的源代码。
using System;
namespace gmcs
{
public class ClassToParse
{
    public static int Main (string[] args)
    {
         Console.WriteLine("Hello! World.");
         return 1;
    }
}
}

上面的 ClassToParse 程序将用于进行这个实验。要开始这个调试,我们需要做一些准备工作,比如我们需要修改 Mono 源代码中 driver.cs 文件的 Main (string[] args) 方法,如代码清单 14 所示。

代码清单 14:driver.cs 的 Main 方法源代码
public static int Main(string[] args)
{
    Location.InEmacs = Environment.GetEnvironmentVariable("EMACS") == "t";
    args = new string[] { @"C:\Temp\ClassToParse.cs", @"-out:C:\Temp\Otu.exe" };
 
    Driver d = Driver.Create(args, true, new ConsoleReportPrinter());
    if (d == null)
        return 1;
    if (d.Compile() && d.Report.Errors == 0)
    {
        if (d.Report.Warnings > 0)
        {
            Console.WriteLine("Compilation succeeded - {0} warning(s)", d.Report.Warnings);
        }
        Environment.Exit(0);
        return 0;
    }
    Console.WriteLine("Compilation failed: {0} error(s), {1} warnings",
        d.Report.Errors, d.Report.Warnings);
    Environment.Exit(1);
    return 1;
}

在上面清单 14 中列出的代码中,我将 ClassToParse.cs 文件路径添加到了 args[] 数组中(即 C:\Temp\ClassToParse.cs),并设置了解析选项以及输出文件名,本例中为 Out.exe。如果我们在 if (d.Compile() && d.Report.Errors == 0) 这一行设置一个断点

当开始调试时,Mono 编译器将调用 Driver 对象的 Compile() 方法,例如,d.Compile() 开始调用另一个方法来开始编译,如下所示。如果我们查看 driver.cs 类的 Compile() 方法,我们可以看到主要功能如下

代码清单 15:driver.cs 的 Compile 方法
public bool Compile ()
{
    RootContext.ToplevelTypes = new ModuleContainer (ctx, RootContext.Unsafe);
    Parse ();
    //....
    ProcessDefaultConfig ();
    //
    // Load assemblies required
    //
    LoadReferences ();
    // The second pass of the compiler
    RootContext.ResolveTree ();
    //...
    RootContext.PopulateTypes ();
    //
    // Verify using aliases now
    //
    NamespaceEntry.VerifyAllUsing ();
    //....
    CodeGen.Assembly.Resolve ();
    //
    // The code generator
    //
    RootContext.CloseTypes ();
    //....
    CodeGen.Save (output_file, want_debugging_support, Report);
    //....
}

根据解析器方法内的分词状态,它将继续进行,即它将开始 tokenize_file

代码清单 16:cs-parser.cs 的 Parse 方法
public void Parse()
{
    Location.Initialize();
 
    ArrayList cu = Location.SourceFiles;
    for (int i = 0; i < cu.Count; ++i)
    {
        if (tokenize)
        {
            // MoRe: Step 3
            tokenize_file((CompilationUnit)cu[i], ctx);
        }
        else
        {
            Parse((CompilationUnit)cu[i]);
        }
    }
}

最终,解析器将调用由 Jay 生成的 CSharpParser 类的 parse() 方法。

代码清单 17:Mono 的 Parse 方法
void Parse(SeekableStreamReader reader, CompilationUnit file)
{
    CSharpParser parser = new CSharpParser(reader, file, ctx);
    try
    {
        parser.parse();
    }
    catch (Exception ex)
    {
        Report.Error(589, parser.Lexer.Location,
            "Compilation aborted in file `{0}', {1}", file.Name, ex);
    }
}

cs-parser.jay 中指定的所有语法都有一条规则对应的动作块,并且在解析器方法中,这些语法与其关联的动作之间存在映射(请参阅附录中 Mono C# 编译器的完整语法列表),形式为 switch 语句的 case。根据(标记值转换成的)yyN 值,相关的动作将被执行以构建抽象语法树。如果我们看图 3,我们可以看到 Mono 如何消费一个标记,因为它调用了 driver.csParse(),然后是 CSharpParser 类的 yyparseyyparse 将通过调用 Tokenizer 类的 xtoken() 方法从输入流中消费标记。

330184/3_StackTraceOfCompile.jpg

图 3:Compile() 方法的堆栈跟踪

在我们继续之前,我们先看一下在 xtoken() 方法内部发生的过程。在文件读取的第一阶段,Tokenizer 将从流中读取第一个字符,即 117。在这里,117 是 u 在 ASCII 中的表示(请参阅附录中完整的 ASCII 和十进制值表)。如果我们看图 4,它显示 c 的当前值(字符指的是标记)是 117,即 u,它是 ClassToParse 类中使用的 using 语句的第一个字符。

330184/4_TokeinizingU.jpg

图 4:对 ClassToParse 类进行分词

由于 117 不是一个标准标记,它将被验证为标识符,并且分词器将开始消费该标识符,如图 5 所示。

330184/5_TokeinizingUsing.jpg

图 5:对 using 语句进行分词

在消费完标识符后,它会与分词器类内部存储的关键字进行匹配,试图找出它是否是一个关键字,如图 6 所示。

330184/6_TokeinizingUsingKeyword.jpg

图 6:关键字匹配

它将被识别为一个关键字,因为 Mono 中有一个值为 335 的关键字(请参阅附录获取完整的标记列表)。最终,词法分析器将返回标记值 335,即 using 语句。图 7 显示了词法分析器的 token 方法的 return 语句,它返回 335 作为当前标记值。

330184/7_CurrentTokenValue.jpg

图 7:来自 cs-tokenizer.cs 的 token() 方法的当前标记值

解析器现在会尝试找出是否有任何条件等于这个标记值,如果有,它将执行作为语法一部分定义的相应动作块。

在解析器可以执行动作块之前,它必须计算 yyN 值,正如我们之前看到的,yyN 是标记值和语法之间的映射。解析器用来计算 yyN 的代码片段在代码清单 18 中列出。

代码清单 18:基于 yyTable 计算 yyN
if ((yyN = yyRindex[yyState]) != 0 && (yyN += yyToken) >= 0
     && yyN < yyTable.Length && yyCheck[yyN] == yyToken)
          yyN = yyTable[yyN];

如果我们看图 8,我们可以看到在调试编译器时,当标记值为 374 时的监视窗口。在这个计算过程中,解析器将从 yyTable 数组(yyTable 是在使用 Jay 生成解析器时创建的)中检索 yyN 值。

330184/8_WatchValueofyyN_yyToken.jpg

图 8:监视列表中的 yyN 值

yyN 值的计算是 Mono 编译器中另一个有趣的部分。所以,根据给定的值 yyState = 33, yyToken = 374,我们得到 yyRinedx[33] 中第 33 个位置的值,即 450。yyN 的当前值将是 450,而 if((yyN += yyToken) >= 0) 条件的第二部分将把 yyToken 的值加到 450(yyN 的当前值)上,即 yyN += yyToken。

最终,最新的 yyN 值将是 824(当前标记是 374 + 先前的 yyN 值 450)。这个 824 将被用作索引,以检索存储在 yyTableyyTable 是由编译器-编译器 Jay 在将 cs-parser.jay 转换为 cs-parser.cs 时创建的)中该位置的值。而这个值将作为 yyN 的新值,用作 switch case 选择器来执行动作块。我想在这里介绍一下代码清单 5.23 中列出的以下数组。所有这些数组都是由编译器-编译器 Jay 在将语法文件转换为解析器时创建的。

注意:由 Jay 为 Mono 解析器生成的数组。

代码清单 19:由 yacc 生成的数组
static short[] yyLhs
static short[] yyLen
static short[] yyDefRed
protected static short[] yyDgoto
protected static short[] yySindex
protected static short[] yyRindex
protected static short[] yyGindex
protected static short[] yyTable
protected static short[] yyCheck

在图 9 中,我试图展示 yyN 如何映射 yyparse 方法中定义的 case 语句,以执行语法中定义的相应代码块,即 cs-parser.jay 文件。

330184/9_StackTrace_Grammar_Token_Match.jpg

图 9:标记映射

从图 10 我们可以看到 Mono 在解析程序源代码时如何构建抽象语法树。每当解析器找到一个有效的标记和一个 yyN 值,它就会与条件匹配,以运行相关的动作块,该动作块会将相关的类型(基于语法规范,请参阅附录中 Mono C# 编译器的完整语法列表)添加到 TypeContainer 中,这个容器稍后用于解析类型。

330184/10_GrammarMatchingWithToken.jpg

图 10:监视

Compile() 方法将调用 rootContext.cs 文件中 RootContext 类型的 ResolveTree 方法。

代码清单 20:RootContext 的 ResolveTree 方法
RootContext.ResolveTree ();

ResolveTree 方法将生成层次树或解析树。然后,Compile() 方法调用 RootContext 类的 PopulateTypes。到目前为止,我们已经看到了 Mono 如何对源代码进行分词、解析源代码,并在此基础上构建抽象语法树。在下一节中,我们将看到 Mono 如何生成中间语言(IL)代码来生成程序集。

参考文献

© . All rights reserved.