原文地址:http://zserge.com/blog/cucu-part2.html

到目前为止,我们已经定义了我们语言的语法并编写了一个词法分析器。在本篇文章中,我们将为我们的语言写解析器。但在开始之前,我们先需要一些辅助函数:

int peek(char *s) {return (strcmp(tok, s) == 0);
}int accept(char *s) {if (peek(s)) {readtok();return 1;}return 0;
}int expect(char *s) {if (accept(s) == 0) {error("Error: expected '%s'\n", s);}
}

peek() 函数若下一个符号与传入的字符串相等,则返回非零值。 accept()函数读取下一个符号,如果其与传入参数相同,否则返回0。expect() h帮助我们检查语言的语法。

较为困难的部分

从语言的语法中我们可以得知,语句和表达式是相互掺杂在一起的。因此这就意味着一旦我们开始写解析器,我们必须时刻记住这些递归生成规则。让我们从顶至底来进行分析。下面是最高层的函数compiler():

static int typename();
static void statement();static void compile() {while (tok[0] != 0) { /* until EOF */if (typename() == 0) {error("Error: type name expected\n");}DEBUG("identifier: %s\n", tok);readtok();if (accept(";")) {DEBUG("variable definition\n");continue;} expect("(");int argc = 0;for (;;) {argc++;typename();DEBUG("function argument: %s\n", tok);readtok();if (peek(")")) {break;}expect(",");}expect(")");if (accept(";") == 0) {DEBUG("function body\n");statement();}}
}

这个函数首先尝试读取类型名,其次是标识符。如果在此之后紧跟一个分号,则说明是一个变量声明。如果跟着括号,说明是一个函数。如果是函数,就接着去逐一搜索参数,再次之后如果没有分号,则说明是一个函数定义(有函数体),否则就只是一个函数声明(只有函数名和类型)。

这里, typename() 是一个用来让我们跳过类型名的函数。 我们指接受int类型、char类型以及其指针(char*):

static int typename() {if (peek("int") || peek("char")) {readtok();while (accept("*"));return 1;}return 0;
}

最有趣的大概就是 statement() 函数了。它可以分析一个单独的语句,而这个语句可以是一个块、一个局部变量的定义/声明、一个return语句等。

现在让我们来看看它的样子:

static void statement() {if (accept("{")) {while (accept("}") == 0) {statement();}} else if (typename()) {DEBUG("local variable: %s\n", tok);readtok();if (accept("=")) {expr();DEBUG(" :=\n");}expect(";");} else if (accept("if")) {/* TODO */} else if (accept("while")) {/* TODO */} else if (accept("return")) {if (peek(";") == 0) {expr();}expect(";");DEBUG("RET\n");} else {expr();expect(";");}
}

如果遇到的是一个“块”,即{...}的部分,就继续尝试在块中解析语句直到块结束。如果以变量名开头,则说明是一个局部变量的定义。条件语句(if/then/else)和循环语句在这里没有列出,留给读者去思考根据我们的语法,这些部分应当如何去实现。

当然,大部分语句里都包含着表达式,因此我们需要写一个函数取分析表达式。表达式解析器是一个向下递归的解析器,因此很多的表达式解析函数会互相调用直到找到主表达式为止。所谓的主表达式,根据我们的语法,是指一个数字(常量)或者一个标识符(变量或者函数)。

static void prim_expr() {if (isdigit(tok[0])) {DEBUG(" const-%s ", tok);} else if (isalpha(tok[0])) {DEBUG(" var-%s ", tok);} else if (accept("(")) {expr();expect(")");} else {error("Unexpected primary expression: %s\n", tok);}readtok();
}static void postfix_expr() {prim_expr();if (accept("[")) {expr();expect("]");DEBUG(" [] ");} else if (accept("(")) {if (accept(")") == 0) {expr();DEBUG(" FUNC-ARG\n");while (accept(",")) {expr();DEBUG(" FUNC-ARG\n");}expect(")");}DEBUG(" FUNC-CALL\n");}
}static void add_expr() {postfix_expr();while (peek("+") || peek("-")) {if (accept("+")) {postfix_expr();DEBUG(" + ");} else if (accept("-")) {postfix_expr();DEBUG(" - ");}}
}static void shift_expr() {add_expr();while (peek("<<") || peek(">>")) {if (accept("<<")) {add_expr();DEBUG(" << ");} else if (accept(">>")) {add_expr();DEBUG(" >> ");}}
}static void rel_expr() {shift_expr();while (peek("<")) {if (accept("<")) {shift_expr();DEBUG(" < ");}}
}static void eq_expr() {rel_expr();while (peek("==") || peek("!=")) {if (accept("==")) {rel_expr();DEBUG(" == ");} else if (accept("!=")) {rel_expr();DEBUG("!=");}}
}static void bitwise_expr() {eq_expr();while (peek("|") || peek("&")) {if (accept("|")) {eq_expr();DEBUG(" OR ");} else if (accept("&")) {eq_expr();DEBUG(" AND ");}}
}static void expr() {bitwise_expr();if (accept("=")) {expr();DEBUG(" := ");}
}

上面是一大段的代码,但是不需要感到头疼,因为它们都简单的很。每一个分析表达式的函数首先都尝试调用一个更高优先级的表达式分析函数。接着,如果找到了这个函数期望的符号,则它继续调用高优先级的函数。然后当它分析完了一个二元表达式(如x+y、x&y、x==y)的两部分之后,就将值返回。有些表达式可以链式连接(如a+b+c+d),因此需要循环的分析它们。

我们在分析每一个表达式的时候都会输出一些调试信息,这些信息会给我们带来一些有趣的结果。例如,若我们分析以下代码片段:

int main(int argc, char **argv) {int i = 2 + 3;char *s;func(i+2, i == 2 + 2, s[i+2]);return i & 34 + 2;
}

我们将会得到如下的输出:

identifier: main
function argument: argc
function argument: argv
function body
local variable: iconst-2  const-3  +  :=
local variable: svar-func  var-i  const-2  +  FUNC-ARGvar-i  const-2  const-2  +  ==  FUNC-ARGvar-s  var-i  const-2  +  []  FUNC-ARGFUNC-CALLvar-i  const-34  const-2  +  AND RET

所有的表达式都会被写成逆波兰式(比如2+3变成23+)。而这对于有堆栈的计算机来说,是更为方便合理的形式,当操作数在栈顶的时候,函数能够执行出栈操作并取得操作数,之后将结果压栈。

虽然对于现在的以寄存器为基础的CPU,这或许不是一个最优的方法,但这个方法很简单并且能够满足我们编译器的需要。

symbols

现在,我们已经完成了很多工作了,我们使用不到300行的代码写了一个词法分析器和解析器。接下来我们要做的事情是添加以下函数,以便让这些符号(比如变量名、函数名)能够正确的工作。一个编译器应该有一个符号表以便能够很快的找到这些符号的地址,所以当你在代码中写“i=0"的时候,实际上你是将0这个值放入了内存的0x1234的位置(假设变量i在内存的位置就是0x1234)。相似的,当我们调用函数”func()"时,实际上做的是跳转到0x5678继续执行而已(假设func这个符号的的值是0x5678)。

我们需要一个数据结构来存放符号:

struct sym {char type;int addr;char name[];
};

这里type 有不同的含义。 我们用一个单独的字母来标示不同的类型:

  • L - 局部变量。 addr 存储变量在堆栈里的地址
  • A - 函数参数。 addr 存储参数在堆栈里的地址
  • U - 未定义的全局变量。 addr 存储其在内存中的绝对地址。
  • D - 定义过的全局变量。 其余同上。

So far, I've added two functions: sym_find(char *s) to find symbol by its name, andsym_declare() to add a new symbol.

到此为止,我们还需要增加两个函数:: sym_find(char *s) 来根据符号名查找符号, sym_declare() 来加入一个新的符号。

现在我们已经可以去设计后端的架构了,详情见下篇文章。

如果你忘了前面的信息你可以到part1部分去查阅。

cucu: a compiler u can understand (part 2)相关推荐

  1. Effective C# 原则48:了解更多的工具和资源(译)

    Effective C# 原则48:了解更多的工具和资源   Item 48: Learn About Tools and Resources 对于C#以及.Net来说这是激动人心的时候.这些工具目前 ...

  2. java 字符串中转义字符_Java中的转义字符

    java 字符串中转义字符 Learn how we can use escape sequence in Java 了解如何在Java中使用转义序列 These characters can be ...

  3. Build Instructions (Windows) – The Chromium Projects

    转自:http://121.199.54.6/wordpress/?p=1156 原始地址:http://www.chromium.org/developers/how-tos/build-instr ...

  4. C++11 FAQ中文版

    C++11 FAQ中文版 http://www.chenlq.net/cpp11-faq-chs http://www.stroustrup.com/C++11FAQ.html Morgan Stan ...

  5. “找不到符号”或“无法解析符号”错误是什么意思?

    本文翻译自:What does a "Cannot find symbol" or "Cannot resolve symbol" error mean? Pl ...

  6. 观看2014年DotNetConf的所有视频

    Did you miss out on DotNetConf when it streamed LIVE just a few weeks ago? Don't you worry, it's all ...

  7. 用c语言编写一个简易的编译器,面向教学的简易c语言编译器的设计与实现(54页)-原创力文档...

    目录 TOC \o "1-5" \h \z \o "Current Document" 摘要I ABSTRACTII \o "Current Docu ...

  8. understand软件使用教程

    源代码阅读工具(Scientific Toolworks Understand)的特色 1.支持多语言:Ada, C, C++, C#, Java, FORTRAN, Delphi, Jovial, ...

  9. linux安装 icc编译器,安装 Intel Compiler (ifort icc icpc)

    在下载目录下解压 heqin@heqin-dell:~/Downloads$ tar zxvf parallel_studio_xe_2017_update7.tgz 进入解压后的文件夹 heqin@ ...

  10. 关于Maven项目build时出现No compiler is provided in this environment的处理

    近日有同事遇到在编译Maven项目时出现 [ERROR] No compiler is provided in this environment. Perhaps you are running on ...

最新文章

  1. C语言常用排序方法大全
  2. OpenGL Fur Rendering毛发渲染的实例
  3. RAID5EE 含有上次残余信息的分析
  4. 睡觉时,禁带6种东西,最后一点最严重,可能致命
  5. IPC通信:Posix消息队列的属性设置
  6. 显示脸上的关键点的程序
  7. Affinity Designer 查询面板渐变颜色
  8. 突然发现Windows 7 Ultimate中有个BUG,分享一下哈!
  9. C语言图书出入库管理系统
  10. Python 每日一题(计算数值和)
  11. 电脑桌面天气计算机备忘录,有什么桌面软件可以显示:时间,天气,还有备忘录的?...
  12. C++银行管理系统设计分析及程序设计介绍
  13. hexdec() 函数
  14. Web(ics-07)
  15. Avril Lavigne: Complicated
  16. echarts的x轴自动动态刷新
  17. linux cat命令的作用,Linux命令cat使用详解
  18. 微信支付的架构到底有多牛?
  19. 搭建 Cobbler 无人值守安装服务器
  20. OGRE渲染引擎之地形、天空和雾

热门文章

  1. echarts分割柱形图实现渐变电量效果柱状图
  2. Adobe XD的UI设计学习以及示例
  3. 2013年中国手游用户行为习惯调查报告
  4. 手机重启问题快速分析定位指南
  5. vivo市场投放APP - 特殊行业类资质要求
  6. 计算机专业开题报告案例20: 基于SpringBoot的高校排课系统的设计与实现
  7. 谨慎选择网络变压器固化胶,谨防回流焊后溢胶
  8. 拥塞控制在mininet中的仿真
  9. 推荐5款提高生活和工作效率的好帮手
  10. 目标检测如何计算召回率_目标检测 — 评价指标(示例代码)