ce_amtic's
语义实验解析
  1. 1. 整体设计
  2. 2. 分部解析
    1. 2.1. 栈空间管理
    2. 2.2. 符号表
    3. 2.3. 表达式
    4. 2.4. 条件分支和循环
    5. 2.5. 初始化数组展平
  3. 3. 总结

我要成为bison高手.jpg

本文是对C++实现的SysY语言编译器的解析,仓库地址ce-amtic/sysy-compiler。编译器不分前后端,直接产生汇编语言。

整体设计

工程目录如下:

.
├── assembly.hpp
├── ast_node.hpp
├── compiler.hpp
├── symbols.hpp
├── sysy.l
├── sysy.y
├── plot/
│   └── tree.py
├── out/                # outputs
└── tests/              # testcase

其中sysy.l为词法分析器;sysy.y是语义分析的主程序,其中定义了语法规则和动作;ast_node.hpp定义了语法树节点的抽象类型;assembly.hpp定义了Assembly类,用于实现汇编代码的生成;symbols.hpp定义了符号类Symbol和符号表Symbols;compiler.hpp定义了CompileState, RuntimeStack类,用于维护编译时状态。

根据bison的相关原理,sysy.y会在语义分析时,按定义的产生式规则递归语法树,但是并不会将其显示存储。语法树可视化的相关代码被放在了plot/tree.py中,这个脚本会根据detail.txt中的内容绘制语法树,后者按递归顺序记录了归约过程中的每条产生式。

详细信息请阅读GitHub上的指南文档

分部解析

栈空间管理

每个过程栈空间会在进入过程之后立刻确定,以便满足16字节对齐的要求。这一过程通过在CompileState类中维护一个offset变量来实现,它记录了当前用到的栈空间相对于基址的偏移。在编译器完成对过程的解析时,会计算offset峰值并对齐到16字节,回填到过程开始的位置。

本程序中,函数的参数是通过栈来传递的,对于callee来说,它的第一个参数在8(%rbp)的位置,第二个在16(%rbp),以此类推。对于caller而言,编译器会记录其函数调用传参的情况,并提前预留出栈空间,相关逻辑实现在CompileState中。

符号表

对于一个符号,会记录它的类别,数值(或偏移量),以及数组维度之类的附加信息,它们被抽象为Symbol类。

符号表是一个vector<map<string, Symbol> >类型的映射,其中第一层vector用于维护代码中的层级关系,第二层map用于按名称查找。

表达式

表达式被定义为Exp类,它可能是函数调用,算式或它们的组合。本程序根据如下的规则简化代码编写:

针对表达式的规则

如果一个表达式有计算结果,这个结果要么是一个可以编译时计算的常数,要么对应到栈上的一个临时变量。对于前者,类中记录这个常数;对于后者,类中记录这个变量相对于栈基的偏移。

在这一规则下,完成表达式计算的相关逻辑就会变得十分简单,代码在sysy.y里Exp相关的语义动作中。

条件分支和循环

通过预生成跳转指令和回填的方式完成条件语句,代码在Cond和Stmt相关的语义动作中。

循环可以视为加入了回跳的条件语句,代码为While相关的语义动作。对于break, continue等影响控制流的语句,使用RuntimeStack类来维护运行状态,并完成跳转回填相关逻辑。

初始化数组展平

针对int a[4][3][2] = {0, {0}, {{0}}}等等初始化方式,我将后面的每个大括号抽象为一个ExpList类,其中定义了vector容器用于维护嵌套大括号的情况,并在语法动作中记录层级信息。

在解析数组初始化时,我在ExpList类中定义了dfsArray方法,用于递归地解析这种层级信息,将初始值展平为一维数组。

总结

更细节的实现,请直接参考代码中的注释。