前面讲过语法的解析之后,代码生成方面就简单很多了。虽然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编译器源码之代码生成的更多相关文章

  1. MYC编译器源码之词法分析

    前文  .NET框架源码解读之MYC编译器 和 MYC编译器源码分析之程序入口 分别讲解了 SSCLI 里示例编译器的架构和程序入口,本文接着分析它的词法分析部分的代码. 词法解析的工作都由Tok类处 ...

  2. MYC编译器源码之语法分析

    MyC编译器采用自顶向下的方法进行语法解析,这种语法解析方式,一般是从最左边的Token开始,然后自顶向下看哪一条语法规则可能包含这个Token,如果包含这个Token,则自左向右根据这条语法规则逐一 ...

  3. MYC编译器源码分析之程序入口

    前文.NET框架源码解读之MYC编译器讲了MyC编译器的架构,整个编译器是用C#语言写的,上图列出了MyC编译器编译一个C源文件的过程,编译主路径如下: 首先是入口Main函数用来解析命令行参数,读取 ...

  4. TypeScript 编译器源码研究(一)

    TypeScript (以下简称 TS)是一个非常强大的语言,其编译器源码超过 10000 行. 源码在 Github 可以找到:https://github.com/Microsoft/TypeSc ...

  5. .NET框架源码解读之MYC编译器

    在SSCLI里附带了两个示例编译器源码,用来演示CLR整个架构的弹性,一个是简化版的lisp编译器,一个是简化版的C编译器.lisp在国内用的少,因此这里我们主要看看C编译器的源码,源码位置是:\ss ...

  6. TypeScript 源码详细解读(1)总览

    TypeScript 由微软在 2012 年 10 月首发,经过几年的发展,已经成为国内外很多前端团队的首选编程语言.前端三大框架中的 Angular 和 Vue 3 也都改用了 TypeScript ...

  7. laravel源码解析

    本专栏系列文章已经收录到 GitBooklaravel源码解析 Laravel Passport——OAuth2 API 认证系统源码解析(下)laravel源码解析 Laravel Passport ...

  8. .NET框架源码解读之SSCLI编译过程简介

    前文演示了编译SSCLI最简便的方法(在Windows下): 在“Visual Studio 2005 Command Prompt”下,进入SSCLI的根目录: 运行 env.bat 脚本准备环境: ...

  9. Vue 源码解读(10)—— 编译器 之 生成渲染函数

    前言 这篇文章是 Vue 编译器的最后一部分,前两部分分别是:Vue 源码解读(8)-- 编译器 之 解析.Vue 源码解读(9)-- 编译器 之 优化. 从 HTML 模版字符串开始,解析所有标签以 ...

随机推荐

  1. 神经网络出现nan原因?以及解决

    之前在TensorFlow中实现不同的神经网络,作为新手,发现经常会出现计算的loss中,出现Nan值的情况,总的来说,TensorFlow中出现Nan值的情况有两种,一种是在loss中计算后得到了N ...

  2. 【校招面试 之 C/C++】第8题 C++中的静态绑定与动态绑定

    转自:https://blog.csdn.net/chgaowei/article/details/6427731   做了部分修改 为了支持c++的多态性,才用了动态绑定和静态绑定.理解他们的区别有 ...

  3. 3.滑雪-深搜&dp

    //Michael喜欢滑雪百这并不奇怪, 因为滑雪的确很刺激.可是为了获得速度,滑的区域必须向下倾斜,而且当你滑到坡底,你不得不再次走上坡或者等待升降机来载你.Michael想知道载一个区域中最长底滑 ...

  4. jquery datatables api

    原文地址 学习可参考:http://www.guoxk.com/node/jquery-datatables http://yuemeiqing2008-163-com.iteye.com/blog/ ...

  5. 【转】HttpHandler的认识与加深理解

    原文:http://www.cnblogs.com/whtydn/archive/2009/10/19/1585778.html HttpHandler是HTTP请求的处理中心,真正地对客户端请求的服 ...

  6. 阈值分割与XLD轮廓拼接——第4讲

    一.阈值分割 阈值分割算子众多: threshold :这是最基本最简单的阈值算子. binary_threshold :它是自动阈值算子,自动选出暗(dark)的区域,或者自动选出亮(light)的 ...

  7. OpenSSL 结构体

    X509_STORE 头文件:x509_vfy.h 定义 typedef struct x509_store_st X509_STORE; struct x509_store_st { /* The ...

  8. Git 初始状操作指引

    You have an empty repository To get started you will need to run these commands in your terminal. Ne ...

  9. 1. Install Git and GitExtension

    Install Git Step 1:   Run

  10. HTML5/CSS3基础

    1. HTML 1.1 什么是HTML HTML是用来制作网页的标记语言 HTML是Hypertext Markup Language的英文缩写,即超文本标记语言 HTML语言是一种标记语言,不需要编 ...