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






4.99/5 (54投票s)
Mono 是一个开源的免费编程语言项目。它基于 ECMA 的 C# 语言和通用语言运行时 (CLR) 标准,实现了微软的 .NET 框架。在本文中,我将探讨 Mono C# 编译器的工作原理。
目录
引言
Mono 是一个开源的免费编程语言项目。它是微软 .NET 框架的一个实现,基于欧洲信息与通信系统标准化协会(ECMA)的 C# 语言和通用语言运行时(CLR)标准。Mono C# 编译器由 Miguel de Icaza 发起。在表 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 编译器及其支持的框架。
编译器版本 | 目标框架 |
mcs | 1.1 |
gmcs | 2.0 |
smcs | 2.1 |
dmcs | 4.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 的不同目录及其简短描述。
docs | 关于 Mono 运行时的技术文档。 | ||
data | 作为 Mono 运行时一部分安装的配置文件。 | ||
mono | Mono 运行时的核心。 | ||
metadata | 对象系统和元数据读取器。 | ||
mini | 即时编译器。 | ||
dis | CIL 可执行文件反汇编器。 | ||
cli | JIT 和解释器的通用代码。 | ||
io-layer | 用于模拟 .NET IO 模型的 I/O 层和系统抽象。 | ||
cil | 通用中间表示,CIL 字节码的 XML 定义。 | ||
interp | CLI 可执行文件的解释器(已废弃)。 | ||
arch | 特定于体系结构的部分。 | ||
mcs | Mono 编译器代码的核心 | ||
mcs | |||
mcs | 编译器源代码 | ||
jay | 解析器生成器 | ||
man | 各种 Mono 命令和程序的手册页。 | ||
samples | 一些关于将 Mono 运行时用作嵌入式库的简单示例程序。 | ||
scripts | 用于调用 Mono 和相应程序的脚本。 | ||
runtime | 包含链接 mono/ 和 mcs/ 构建系统的 Makefiles 的目录。 | ||
../olive | 如果 ../olive 目录存在(作为独立检出)于 Mono 模块之外,该目录会自动配置为与此模块共享相同的前缀。 |
注意:以上 Mono 源代码目录信息来自 https://github.com/mono/mono。
Mono 编译器的源代码位于 /mono/mcs 的 mcs 文件夹内。
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 所示。
请将 Mono 源代码复制到 Cygwin 安装目录的 /usr/src 目录中。然后打开 Cygwin 终端并输入代码清单 1 中列出的命令。
$cd /usr/src/Mono/mcs
$cd jay
$make
$cd ..
$cd mcs
$../jay/jay.exe -ctv < ../jay/skeleton.cs cs-parser.jay > cs-parser.cs
请看下图
因此,在用适当的参数执行 Jay.exe 后,它会将 cs-parser.jay 文件转换为 cs-parser.cs,后者是 Mono 的解析器。
Mono 源代码关系
根据 Mono 源代码附带的文档(mcs\mcs\compiler.txt 或 https://github.com/mono/mono/blob/master/mcs/docs/compiler.txt),Mono C# 编译器的整个源代码文件被分为五类:基础结构、解析、表达式、语句和声明、类、结构体、枚举。如果我们查看下面的 Mono C# 编译器源代码分类表 4,将更容易理解 Mono 编译器构造中使用的所有类型。
Mono 编译器源代码分类 | ||||
基础结构 | 解析 | 表达式 | 语句 | 声明、类、结构体、枚举 |
driver.cs | cs-tokenizer.cs | ecore.cs | statement.cs | decl.cs |
codegen.cs | cs-parser.jay, cs-parser.cs | expression.cs | iterators.cs | class.cs |
attribute.cs | location.cs | assign.cs | delegate.cs | |
rootcontext.cs | constant.cs | enum.cs | ||
typemanager.cs | literal.cs | interface.cs | ||
report.cs | cfold.cs | parameter.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
方法的部分代码,我们可以看到编译过程的主要流程。
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
方法以继续解析。
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 解析器实例,正如我们之前讨论过的。
public CSharpParser (SeekableStreamReader reader, CompilationUnit file, CompilerContext ctx)
代码清单 3 中列出的构造函数将接收文件读取器流和在 driver 类中定义的编译器上下文的只读值。
%{
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
方法的签名,它接收词法分析器作为参数。
internal Object yyparse (yyParser.yyInput yyLex)
解析将在这个 yyparse
方法中进行。这个 yyparse
方法将解析由词法分析器生成的每个标记。
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 所示。
if (is_identifier_start_character (c)) {
tokens_seen = true;
return consume_identifier (c);
}
当词法分析器尝试消费标识符时,它会通过调用 GetKeyword
方法来查找是否有任何关键字匹配,如代码清单 8 所示。
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 所示。
public Object value ()
{
return val;
}
完成检查过程后,词法分析器将把标记返回给解析器以继续解析过程。因此,当解析器发现词法分析器返回的标记值(例如,对于代码清单 5.18 中列出的例子,IDENTIFIER
的值为 418;有关 Mono 标记的详细信息,请参见附录),解析器会将其视为标识符,并尝试访问与该标识符关联的 val
。为了访问 Tokenizer
类的 val
,解析器将调用词法分析器的 value
属性,并将该值赋给解析器的 yyVal
(yyVal
是在 yyparse
方法中定义的 object 类型局部变量)。每个这样的 yyVal
将被存储在 CSharpParser
类的 yyparse
方法内的 yyVals
数组中。存储在 yyVals
数组中的值稍后将用作语法的替换变量。在下面的代码清单 5.13 中,我们可以看到 yyVal
是如何存储在 yyVals
数组中的。注意:替换是语法-解析器通信,即将一个值传递到语法文件中。例如,如果我们想从解析中替换语法中定义的变量 $1、$2 或 $3,我们必须从解析器传递替换值。在这里,yyVals
将根据标记为语法存储所有这些替换变量。
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。
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 类的实例添加到类型容器中。
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
方法。
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 所示。
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()
方法,我们可以看到主要功能如下
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
。
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()
方法。
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.cs 的 Parse()
,然后是 CSharpParser
类的 yyparse
。yyparse
将通过调用 Tokenizer
类的 xtoken()
方法从输入流中消费标记。
在我们继续之前,我们先看一下在 xtoken()
方法内部发生的过程。在文件读取的第一阶段,Tokenizer
将从流中读取第一个字符,即 117。在这里,117 是 u 在 ASCII 中的表示(请参阅附录中完整的 ASCII 和十进制值表)。如果我们看图 4,它显示 c 的当前值(字符指的是标记)是 117,即 u,它是 ClassToParse
类中使用的 using
语句的第一个字符。
由于 117 不是一个标准标记,它将被验证为标识符,并且分词器将开始消费该标识符,如图 5 所示。
在消费完标识符后,它会与分词器类内部存储的关键字进行匹配,试图找出它是否是一个关键字,如图 6 所示。
它将被识别为一个关键字,因为 Mono 中有一个值为 335 的关键字(请参阅附录获取完整的标记列表)。最终,词法分析器将返回标记值 335,即 using
语句。图 7 显示了词法分析器的 token 方法的 return
语句,它返回 335 作为当前标记值。
解析器现在会尝试找出是否有任何条件等于这个标记值,如果有,它将执行作为语法一部分定义的相应动作块。
在解析器可以执行动作块之前,它必须计算 yyN 值,正如我们之前看到的,yyN 是标记值和语法之间的映射。解析器用来计算 yyN 的代码片段在代码清单 18 中列出。
if ((yyN = yyRindex[yyState]) != 0 && (yyN += yyToken) >= 0
&& yyN < yyTable.Length && yyCheck[yyN] == yyToken)
yyN = yyTable[yyN];
如果我们看图 8,我们可以看到在调试编译器时,当标记值为 374 时的监视窗口。在这个计算过程中,解析器将从 yyTable 数组(yyTable 是在使用 Jay 生成解析器时创建的)中检索 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 将被用作索引,以检索存储在 yyTable
(yyTable
是由编译器-编译器 Jay 在将 cs-parser.jay 转换为 cs-parser.cs 时创建的)中该位置的值。而这个值将作为 yyN 的新值,用作 switch case 选择器来执行动作块。我想在这里介绍一下代码清单 5.23 中列出的以下数组。所有这些数组都是由编译器-编译器 Jay 在将语法文件转换为解析器时创建的。
注意:由 Jay 为 Mono 解析器生成的数组。
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 文件。
从图 10 我们可以看到 Mono 在解析程序源代码时如何构建抽象语法树。每当解析器找到一个有效的标记和一个 yyN
值,它就会与条件匹配,以运行相关的动作块,该动作块会将相关的类型(基于语法规范,请参阅附录中 Mono C# 编译器的完整语法列表)添加到 TypeContainer
中,这个容器稍后用于解析类型。
Compile()
方法将调用 rootContext.cs 文件中 RootContext
类型的 ResolveTree
方法。
RootContext.ResolveTree ();
ResolveTree
方法将生成层次树或解析树。然后,Compile()
方法调用 RootContext
类的 PopulateTypes
。到目前为止,我们已经看到了 Mono 如何对源代码进行分词、解析源代码,并在此基础上构建抽象语法树。在下一节中,我们将看到 Mono 如何生成中间语言(IL)代码来生成程序集。
参考文献
- CodeProject - Mono 入门 - 你的第一个 Mono 应用
- CodeProject - MONO:.NET 框架的替代方案
- CodeProject - 破解 Mono C# 编译器
- CodeProject - 使用 Reflection.Emit 动态创建类型
- CodeProject - IL 汇编语言入门
- Gough J. (2002). 为 .NET 通用语言运行时 (CLR) 编译。Prentice Hall PTR.
- man jay (Commandes) - 一个用于 Java 和 C# 的 LALR(1) 解析器生成器,
- Yacc:另一个编译器-编译器,
- Rahman M. (2012). 理解 Mono C# 编译器。