Skip to content

Latest commit

 

History

History
57 lines (39 loc) · 10.3 KB

remind.md

File metadata and controls

57 lines (39 loc) · 10.3 KB

设计思路

  1. 词法分析 词法分析可以直接分词,存在Token中,记录若干信息,包括类型,val(如果需要的话),Token本身即字符串。

  2. 语法分析 语法分析可以直接用递归下降,最重要的是构建出语法分析树,存储形式为Node,这个表示语法分析树上面的一个节点,这个节点需要包括语法元素类型,例如if,while等等,指向其儿子节点的指针,这里有很多不同情况,比如if语句就要包括if(expr)state这样的三个部分。所以不同语句共用Node节点,空间换时间。

  3. 符号表 符号应该在语法分析阶段就已经完成了,并且已经获得了他的类型和名称。但需要检查该符号是否出现在符号表。类C语言的符号表要考虑嵌套作用域问题,一个作用域(以花括号为区域)可能被嵌套在外层作用域中。所以要递归查找符号,不断地向外层查找,直到找到。可以使用栈来动态管理嵌套作用域,在完成分析后释放。为每一个过程建立一张符号表。并记录其最近外层的符号表指针。 符号表采用链表的形式,并且也无需提取出templist{temp,next}这样的结构形式,因为在C中可以用更紧凑的结构,直接将next纳入到temp类型中。 temp的offset可以使用一种简单的方式来计算,假设temp只定义和使用一次,那么,遍历每个四元式,如果遇到一次使用则offset-4,定义则offset+4,至于定义和使用的先后问题,可以考虑首先发生的是使用,其次发生的是定义。定义总是父亲节点,而使用是儿子节点。 考虑如下情况,由于后代节点必然先计算,所以,后代节点的作用域在代码序列上必然先于父亲节点。 所以可以看到,后代节点的作用域结束的地方就是父亲节点作用域开始的地方。所以,左边节点的作用域嵌套右边节点的作用域,父亲节点的作用域和左边节点的作用域并列。因此作用域不相交,可以使用这种简单的方法。

  4. 语义分析和中间代码生成 这个时候需要考虑到生成四元式的问题,那么需要相应地定义四元式的结构体,Quad,相应地定义语法制导翻译的结构体保存一些语义信息,用以生成结构体。例如临时变量。

  5. 目标代码生成 目标代码生成基于中间代码,即四元式,因此四元式中需要保存完整的目标代码生成的信息。特别是跳转的信息。

return生成思路:因为需要保存在eax中,所以需要将表达式的最终结果移动到eax中,所以首先判断表达式是否已经在eax中,如果没有,则需要将eax清空,然后将表达式移动或者加载到eax中 二元运算生成思路:为每个源操作数分配寄存器,如果有寄存器则不需要分配,没有则需要找一个空闲的寄存器。注意寄存器之间不要冲突 除法生成思路:首先清空eax和edx,然后将被除数放到eax中,加载除数,然后执行除法。 比较运算的生成思路:首先进行比较,然后清空eax方便放置结果。

应该要考虑将变量的生成全部移动到quadgen中,而prase中纯粹的是抽象语法树的分析。类型检查也应该在quadgen中完成。

赋值语句还需要进一步考虑,因为左侧可以不是变量,而是指针的解引用

重构心得

把四元式的参数转换为变量,而非抽象语法树节点,写的过程中会发现,使用抽象语法树节点其实是偷懒的结果,在中间代码生成这一步就应该要解决大部分代码逻辑生成,类型检查等等一系列的任务而最后一步目标代码生成则应该尽可能简单,仅仅起到与目标机器匹配的作用。 把临时变量,局部变量和常量整合成为一个类型,用不同type区分。也就是说,Var,广义地指向所有变量,常量在此需要理解为不可被用户修改的变量。所以准确说来,在程序设计过程中,所有的数据均称为变量。把他们统一在一起是至关重要的,因为他们虽然变量类型不同,但数据类型却可以相同。因此把他们拆开是极为麻烦的。这一点应该要洞见到。这就是数据结构的设计问题了。 还有就是,目标代码的生成又简化了很多,因为如果仅仅在寄存器中修改了变量而没有写回到内存中,这是极为麻烦的,必须要追踪其写入。而且,目前看起来,唯一能够修改内存的语句是赋值语句,其他语句均为临时变量的计算和控制逻辑。而在赋值语句的检查过程中,左边的值必须为一左值,左值在此应该被理解成为地址,往日我们对左值的理解不够清晰,将其理解成为非常量、非临时变量的东西。但事实证明,即使是临时变量(例如解引用)也是可以进行赋值的。所以左侧毋宁取出其地址,才是恰当的做法。

对数组和指针的讨论

数组在以下三种情况被作为数组看待,分别是取地址符&,数组赋初值和sizeof,其他情况下被认定为是退化了的指针。即如果数组为 int a[5][5],那么a退化之后的结果就是 int (*a) [5],即指向 int [5]的指针。目前来看,被当做数组处理的情况只有&,其余两者我们暂时不打算实现。所以就这两种情况考虑编译器行为。 假如有如下语句,int a[5]; &(a+2),那么这个时候会报错,说&应该作用于左值上,也就说a+2不是一个左值,所谓左值,即可以解析出其地址的对象。所以我们应该要对任意表达式是否是左值进行严格检查。a+2之所以不是一个左值,就是其本质上是表达式的中间结果。那么a+2是什么类型?在这里a显然发生了退化,变成了一个int * 类型,所以结果也不难得知了。

但是我们还需要考虑到,左值的直接含义是能够出现在表达式左侧的对象。那么数组显然不是一个可以出现在左侧的东西,这里我们可以对数组进行赋值,可以发现报错的信息是:assignment to expression with array type,所以,数组仍然是左值,但出现在左侧的类型还需要检查是否是数组。

所以我们还要想到,我们该如何理解我们所创建的变量?Var有三种类型,分别是Var,Const和Temp,这里的Var我们此前理解成为是在代码中声明的一个对象,但我们也已经注意到了,Var和Temp最大的区别就是Var在内存中占据位置,而Temp理论上不占据(实际上为了寄存器保存中间结果,还是为其分配了空间)。但左值并不只有Var,Var准确说来是具有名字的左值。例如在赋值表达式中,int *a; *a=3; 或者int a[5]; a[1]=3;后者会被翻译为*(a+1)。但a+1则并不是一个左值。为何?这两者的区别在于,a+1是一个地址,或者说地址本身。一旦进行*,那么就表示这个地址所对应的内容。这个内容的解读方式是根据*(a+1)的类型来诉说的。因此,左值意指的就是内存的一块连续区域,而非地址。所以,普通的变量有其名称,并意味着内存的一块区域。而使用解引用符,将其作用在地址上正是意味着那地址所对应的一块区域,因此是左值,而地址本身不是左值,因为其是区域的索引,而非区域本身。

在具体实现上,被当做左值来使用的地方则更加明确地体现为地址,例如赋值语句的左侧,因为我们计算机总不是直接地访问内存本身,而是通过地址来查内存的。所以: 赋值语句:当赋值语句AST解析的时候,左边的表达式应该是要求被解析为一个地址,通过get_lvalue来表达。用(a=b)++,可以验证,a=b表达式不是左值,所以,返回的并不是a所引用的内存区域,而是a的值。因此,在解析完赋值语句之后,应该还需要返回一个对左值的解引用。

取地址符作用的表达式,也应该用get_lvalue来表达,表示获得其地址。

解引用符作用的表达式,

闲聊

在remind.md中有一些思考的心路历程,其中最重要的是对指针、右值等等的考虑和实现,以及中间一次大的重构的心路历程。没有整理过,只是为了厘清思路。 大概只打算实现到这里为止,因为这学期太忙,编译器固然有意思,可是已经无力继续了。其实本来我只需要按照上学期的编译原理作业继续写就够了,但偶然看到这个chibicc,便一发不可收拾,很想试试。中间后悔了好多次,因为其实为了完成课程作业无需如此的。但还是坚持了下来,写下来感觉收获很大。尤其是对一个大的工程的编写,积攒了一些经验。以后也许有用吧。 顺便说一说这个课程作业,TJ计算机系作为A-学科,课程作业却惨不忍睹。无论是CPU、编译器还是什么人工智能课程等等,还有**的数字逻辑,都很少有指导,完全是学生自己乱写。可是最后验收的时候又只看报告,或者看UI设计,又或者根本不关心写的怎么样。等等这些都与课程所教学的关系很少。所以如果真的自己瞎写的话,最后恐怕什么收获都没有。上学期写的语法词法分析,还有中间代码生成,感觉没有想很多,大多数都是两三天写完的。这也许是我重写的原因之一,想为这件事情找点意义。 但为了那些无意义的事情,去寻找他们有意义的地方,或者赋予他们意义,真的值得吗?倘若为了成绩,或者仅仅只是通过这门课的考核,其实无需如此,只需要去写报告,搞UI。我固然想要好的成绩,可是只有好的成绩又是否真的有意义?于是,我想在成绩之外,尽可能去做点有意义的事情。比如参考着这个chibicc,实现编译器。可是代价是巨大的,实现这个编译器,谁也不会看到,而且花费了大量的时间,这些时间原本可以用于去卷UI,或者做点别的作业,这样会让这学期过的轻松一些。固然做了有意义的事情,可是这意义只有自己知道罢了,那世俗之中的生活却被耽搁了。功利的生活,总是催促着我,让我无法安安静静地去做些有意义的事情。常常感到生活很紧迫,节奏很快,如何寻找一种慢下来的生活方式呢。