MYC编译器源码之代码生成
前面讲过语法的解析之后,代码生成方面就简单很多了。虽然myc是一个简单的示例编译器,但是它还是在解析的过程中生成了一个小的语法树,这个语法树将会用在生成exe可执行文件和il源码的过程中。
编译器在解析时,使用emit类来产生中间的语法树,语法树的数据结构和操作方法在iasm这个类型里完成,源程序的语法解析完毕后,Exe和Asm两个类分别遍历生成的语法树产生最终的代码。
我们来看几个代码的例子,下表的函数 Parser.program 里,在函数开始和结束的地方分别调用了 prolog 和 epilog 两个函数,这两个函数的目的就是在语法解析的前后执行一些准备和扫尾工作。如在编译过程的开始阶段,根据.net assembly的要求创建好模块(module)和类(class),虽然c语言是一个面向过程的语言,但是在.net是一个面向对象的环境,所有的代码都应该保存在一个类里。
public void program()
{
prolog();
while (tok.NotEOF())
{
outerDecl();
}
if (Io.genexe && !mainseen)
io.Abort("Generating executable with no main entrypoint");
epilog();
} void prolog()
{
emit = new Emit(io);
emit.BeginModule(); // need assembly module
emit.BeginClass();
}
而在emit里,BeginModule和BeginClass这两个函数的代码如下:
public void BeginModule()
{
// 委托给Exe类来创建这个module,虽然在.net里,一个assembly可以由
// 多个module组成,但是在C程序里,只要一个module就足够了,因此
// 下面的代码并没有在生成IL源码的时候产生module。
exe.BeginModule(io.GetInputFilename());
} public void BeginClass()
{
// 委托给Exe类来创建class,再进一步跟踪代码的时候,会发现它其实
// 是根据反射技术来创建类型的。
exe.BeginClass(Io.GetClassname(), TypeAttributes.Public);
// 如果在执行程序的命令行里,启用了生成源码的开关,那么
// 将会输出IL class的源码定义。
if (Io.genlist)
io.Out(".class " + Io.GetClassname() + "{\r\n");
}
.NET里,可以使用反射技术来生成assembly、类型和函数,下表就是Exe类的BeginModule函数的源码:
public void BeginModule(string ifile)
{
// .net的动态assembly创建功能,要求跟appdomain绑定
appdomain = System.Threading.Thread.GetDomain();
appname = getAssemblyName(filename);
// 调用AppDomain.DefineDynamiceAssembly创建一个Assembly,以这个为
// 起点,可以创建类型,创建函数并执行。实际上,.net上的IronPython等
// 动态语言的实现就非常依赖这个技术。
appbuild = appdomain.DefineDynamicAssembly(appname,
AssemblyBuilderAccess.Save,
Io.genpath);
// 在.net里,所有的代码实际上都应该保存在一个module里。
emodule = appbuild.DefineDynamicModule(
filename+"_module",
Io.GetOutputFilename(),
Io.gendebug);
Guid g = System.Guid.Empty;
if (Io.gendebug)
srcdoc = emodule.DefineDocument(ifile, g, g, g);
}
准备工作做好了以后,就可以生成语法树了,编译器在解析语法的过程当中,不停的往语法树里添加元素,如在编译函数的过程中,以处理while循环为例(其中一个调用路径是:program -> outerDecl -> declFunc -> blockOuter -> fcWhile)
void fcWhile()
{
// 在一般的il或者汇编语言里,循环和判断语句一般都是在不同路径的入口
// 出定义好标签(label),再通过判断条件的方式跳转到指定的label实现的
String label1 = newLabel();
String label2 = newLabel(); // 记录当前源码的位置,以便生成IL源码的时候可以把源代码和IL代码对照生成
CommentHolder(); /* mark the position in insn stream */
// 一般来说,循环语句至少有两个分支代码块,一个是继续循环的代码块,
// 一个是跳出循环的代码块,看后面的代码,这个label是循环中执行的代码块
// 开始的地方,以便满足条件的时候跳到开头继续执行
emit.Label(label1);
tok.scan();
// 做一些错误判断
if (tok.getFirstChar() != '(')
io.Abort("Expected '('"); // 处理循环条件的判断语句相关代码
boolExpr();
CommentFillPreTok();
// 跳出循环的label
emit.Branch("brfalse", label2); // 循环内部的代码块,进入blockInner进行循环里面的编译
blockInner(label2, label1); /* outer label, top of loop */
// 如果满足循环条件,跳转到代码块开头继续执行
emit.Branch("br", label1); // 循环结束,跳出循环的地方
emit.Label(label2);
}
而在emit类型里,各个方法只是将解析出来的语法元素添加到语法树里,语法树的节点、数据结构和操作方法都在IAsm这个类里定义,如下表是 Branch 的源码:
public void Branch(String s, String lname)
{ // this is the branch source
NextInsn();
// 往语法树里添加一个类别为 Branch 的元素
icur.setIType(IAsm.I_BRANCH);
// 指令名称
icur.setInsn(s);
// 指令参数
icur.setLabel(lname);
}
当程序编译完成后,Exe类和Asm类则分别遍历语法树生成最终的结果,在myc编译器的源码里,Parser.declFunc函数通过调用Emit.IL函数来完成程序的生成:
// 因为C程序大部分都是由函数组成的,而且函数使用到的变量或者其他函数,
// 都必须在函数之前定义,所以只需要在解析函数的时候实时生成代码即可
void declFunc(Var e)
{
#if DEBUG
Console.WriteLine("declFunc token=["+tok+"]\n");
#endif
CommentHolder(); // start new comment
// 记录解析出来的函数名
e.setName(tok.getValue()); /* value is the function name */
// 如果函数名是main,则设置一个标识位 - mainseen为true
// 在外层的函数里,会通过判断这个标志来确定程序是否有语义错误
if (e.getName().Equals("main"))
{
if (Io.gendll)
io.Abort("Using main entrypoint when generating a DLL");
mainseen = true;
}
// 函数名也是一个全局变量,放到全局变量表里,以便做语义分析
// 例如要调用的函数之前没有定义,则应该报错,在后文我们将
// 看到语义方面的处理
staticvar.add(e); /* add function name to static VarList */
paramvar = paramList(); // track current param list
e.setParams(paramvar); // and set it in func var
// 记录函数里面定义的局部变量
localvar = new VarList(); // track new local parameters
CommentFillPreTok(); // 开始生成函数的prolog,例如参数传递,this对象等
emit.FuncBegin(e);
if (tok.getFirstChar() != '{')
io.Abort("Expected ‘{'");
// 递归分析函数里面的源码
blockOuter(null, null);
emit.FuncEnd(); // 解析完整个函数后,执行代码生成操作
emit.IL();
// 如果需要生成IL源码,则调用LIST函数生成IL源码
if (Io.genlist)
emit.LIST();
emit.Finish();
}
而emit.IL函数就是用Exe类型遍历整个语法树,生成结果程序:
public void IL()
{
IAsm a = iroot;
IAsm p; // 循环遍历整个语法树
while (a != null)
{
// 根据语法树里各个节点的类型来执行对应的操作
switch (a.getIType())
{
case IAsm.I_INSN:
exe.Insn(a);
break;
case IAsm.I_LABEL:
exe.Label(a);
break;
case IAsm.I_BRANCH:
exe.Branch(a);
break;
// 省略一些代码
default:
io.Abort("Unhandled instruction type " + a.getIType());
break;
}
p = a;
a = a.getNext();
}
}
而Exe类型执行真正的代码生成,如前面IL函数,在碰到I_BRANCH类型的节点时,调用Exe.Branch函数在动态Assembly (DynamicAssemby) 里生成代码:
public void Branch(IAsm a)
{
Object o = opcodehash[a.getInsn()];
if (o == null)
Io.ICE("Instruction branch opcode (" + a.getInsn() + ") not found in hash”);
// 使用 ILGenerator 类生成跳转IL指令。
il.Emit((OpCode) o, (Label) getILLabel(a));
}
而Asm类也采用类似的方法生成IL源码。
最后,myc编译器里也有一些语义方面的处理,如前面讲到的函数调用时,如果被调用的函数没有定义的话,应该抛出异常的情况,在Parser.statement(即编译实际的C语句的函数)中就有所体现
void statement()
{
Var e;
String vname = tok.getValue(); CommentHolder(); /* mark the position in insn stream */
switch (io.getNextChar())
{
case '(': /* this is a function call */
// 省略一些语法处理方面的代码
tok.scan(); /* move to next token */
// 下面这一行即在生成函数调用代码之前,在全局变量列表里
// 查找要调用的函数是否已经定义了,如果没有定义,则应该报告此错误
e = staticvar.FindByName(vname); /* find the symbol (e cannot be null) */
emit.Call(e); // 省略后面的代码
}
if (tok.getFirstChar() != ';')
io.Abort("Expected ';'");
tok.scan();
}
MYC编译器源码之代码生成的更多相关文章
- MYC编译器源码之词法分析
		
前文 .NET框架源码解读之MYC编译器 和 MYC编译器源码分析之程序入口 分别讲解了 SSCLI 里示例编译器的架构和程序入口,本文接着分析它的词法分析部分的代码. 词法解析的工作都由Tok类处 ...
 - MYC编译器源码之语法分析
		
MyC编译器采用自顶向下的方法进行语法解析,这种语法解析方式,一般是从最左边的Token开始,然后自顶向下看哪一条语法规则可能包含这个Token,如果包含这个Token,则自左向右根据这条语法规则逐一 ...
 - MYC编译器源码分析之程序入口
		
前文.NET框架源码解读之MYC编译器讲了MyC编译器的架构,整个编译器是用C#语言写的,上图列出了MyC编译器编译一个C源文件的过程,编译主路径如下: 首先是入口Main函数用来解析命令行参数,读取 ...
 - TypeScript 编译器源码研究(一)
		
TypeScript (以下简称 TS)是一个非常强大的语言,其编译器源码超过 10000 行. 源码在 Github 可以找到:https://github.com/Microsoft/TypeSc ...
 - .NET框架源码解读之MYC编译器
		
在SSCLI里附带了两个示例编译器源码,用来演示CLR整个架构的弹性,一个是简化版的lisp编译器,一个是简化版的C编译器.lisp在国内用的少,因此这里我们主要看看C编译器的源码,源码位置是:\ss ...
 - TypeScript 源码详细解读(1)总览
		
TypeScript 由微软在 2012 年 10 月首发,经过几年的发展,已经成为国内外很多前端团队的首选编程语言.前端三大框架中的 Angular 和 Vue 3 也都改用了 TypeScript ...
 - laravel源码解析
		
本专栏系列文章已经收录到 GitBooklaravel源码解析 Laravel Passport——OAuth2 API 认证系统源码解析(下)laravel源码解析 Laravel Passport ...
 - .NET框架源码解读之SSCLI编译过程简介
		
前文演示了编译SSCLI最简便的方法(在Windows下): 在“Visual Studio 2005 Command Prompt”下,进入SSCLI的根目录: 运行 env.bat 脚本准备环境: ...
 - Vue 源码解读(10)—— 编译器 之 生成渲染函数
		
前言 这篇文章是 Vue 编译器的最后一部分,前两部分分别是:Vue 源码解读(8)-- 编译器 之 解析.Vue 源码解读(9)-- 编译器 之 优化. 从 HTML 模版字符串开始,解析所有标签以 ...
 
随机推荐
- SharePoint 2010 外部数据类型
			
需求描述: 在sharepoint 2010有时需要访问外部的数据(如sql数据库). 参考文章: 外部内容类型 Business Connectivity Services ECT外部内容类型 Se ...
 - test5
			
## 前言 因为vs2010没有集成mvvmlight 所以想要使用mvvmlight的relaycomman需要引用dll 需要测试某个功能的时候,不能进行快带的集成 ## 引用mvvmlight ...
 - .net  发送邮件失败
			
1,是否为企业邮箱,如果是则用最高admin的帐号,降低其安全级别,下面的子帐号自动适用.(Google 阻止了从某个不够安全的应用进行的登录尝试) 2,做一个测试页面,对错误结果进行分析,一步一步查 ...
 - cubic与spline插值点处的区别
			
cubic与spline都是Matlab的三次样条插值法,但是它们在插值点处仍然有着很微妙的区别,这个区别说明不了两种方法的好坏,只能根据实际情况进行合理筛选.以一维插值为例: clc clear % ...
 - 第八章 高级搜索树 (b3)B-树:查找
 - java小知识点简单回顾
			
1.java的数据类型分为两种:简单类型和引用类型(数组.类以及接口).注意,java没有指针的说法,只有引用.简单类型的变量被声明时,存储空间也同时被分配:而引用类型声明变量(对象)时,仅仅为其分配 ...
 - phpStudy6——php导出可以设置样式的excel表格
			
前言: 一般的后台管理页面肯定少不了excel表格导出的功劳,尤其是那些电商平台的订单导入导出,用户列表的导入导出等,那么本文就介绍php是如何导出excel表格的. php导出excel方法有很多, ...
 - Sql求和异常——对象不能从 DBNull 转换为其他类型
			
做项目遇到一个以前没遇到的问题,就是要计算一个用户消费总额, 关键代码如下: string sql = "select sum(Tmoney) from [order] where uid= ...
 - xcode10设置自定义代码快 - Xcode10新功能新内容
			
1. 2. 详情: Xcode10新功能新内容https://blog.csdn.net/u010960265/article/details/80630118
 - OC 三方框架 - SDWebImage
			
内部实现原理: 1. 下载图片 缓存, 并且需要下载进度 2. 下载图片 : 3.不需要缓存处理的下载 4. 使用GIF 图片使用:图片名字不要加上 .gif 因为内部已经做过处理了 GIF 内部实现 ...