缘起
最近在看Coding 迪士尼老师
的《自己动手用java写编译器》视频教程(一直想入手编译原理
这门课,但是大部分书籍太理论化了, 深深的感觉无力,看到这门课,眼前一亮~ 有门了~). 看了前三集,觉得思想不错,就是代码实现风格不是很赞同. 遂停下观看,针对前三集,自己写一个仅包含加号、乘号、小括号、正整数数字的表达式的编译器. 不然前三集仅仅弄懂思想,却没有自己手写实现的代码,我依旧感觉很无力. 我贯彻的思想是——没有代码的算法思想是耍流氓. 本文作为该视频教程前三集的总结.
分析
其实要做的事情,在老师的视频中讲的很清楚了,我们需要三个东西
compiler
本质上就是一个main方法. 调用其他组件(下面的lexer和parser)用的
lexer
预设一组词法规则——规定什么文本叫做有意义的,以及对应有意义的文本称之为什么(即打上标签),然后从左至右顺序读取文本,顺次将有意义的文本打上标签解析出来.
parser
语法解析器. 它其实就是通过预设的一组语法规则. 利用lexer组件读取标签化的文本的时候,利用语法规则将文本不断的拆分. 最后形成此文本的一棵语法解析树. 形成语法解析树的过程中,我们不仅最终可以得到该文本是否符合语法规则这个结论(即编译是否通过),还可以在生成语法解析树的中途干点事情——完成代码生成. 即生成伪汇编代码.
词法规则如下
1 2 3 4 5 6 7 8
| ; 的标签是 SEMI=1 + 的标签是 PLUS=2 * 的标签是 TIMES=3 ( 的标签是 LP=4 ) 的标签是 RP=5 数字或标识符 的标签是 NUM_OR_ID=6 非法字符 的标签是 UNKNOWN_SYMBOL=7 读到文件尾 的标签是 EOI=0
|
语法规则如下(为什么是这样一个语法规则,日后的视频教程老师会讲的,我们本节就是使用该语法规则将程序编写出来即可)
1 2 3 4 5 6 7 8 9 10 11
| 1. statements -> '空' | expression; statements
2. expression-> term expression'
3. expression'-> + term expression' | '空' ## 如果不以+开头的话, 就解释为空
4. term -> factor term'
5. term' -> * factor term' | '空' ## 如果不以 * 开头的话, 就解释为 '空'
6. factor -> number | (expression) ## number是数字
|
上面的 | 的含义是 “或”, 即二择一.
注意一下语法规则,有 expression和expression’, term和term’, 带撇的,都是前面有符号要处理的——expression’ 要处理+, term’ 要处理 *
用图表示为
下面的代码中, 要开始解析一个东西的时候,都要确保 lexer的current已经站在了那个东西的第一个字符处.
注意哈, 为什么所在的深度比+深? 因为\的优先级比+高. 这个以后会细说. 寄存器赋值总是在叶子处. 2个寄存器之间运算在term’解析出*号以及 expression’解析出 + 的时候.
本编译器使用c++实现. 注意,这里的寄存器的存取显然是通过栈结构实现的. 如下图所示
上图表示当前寄存器栈(reg数组来模拟)只有7个寄存器, t0已经被取走了. top指向reg[1]这个数组元素,也就是top = 1
编写如下 compiler.cpp和 lexer.h以及parser.h 三份文件 ,他们的作用已经讲过了. 代码如下
compiler.cpp
1 2 3 4 5 6 7 8 9 10 11
| #include "parser.h"
int main() { string statements; cin >> statements; Lexer lexer(statements); Parser parser(lexer); parser.statements(); return 0; }
|
lexer.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101
| /************************************************************************/ /* 词法解析器 */ /************************************************************************/ #include <string> #include <cctype>
using namespace std;
class Lexer { public: Lexer(string statements) { this->statements = statements; this->current = -1; } ~Lexer(){} /************************************************************************/ /* 判断当前token和传入的token是否一致 */ /************************************************************************/ bool match(int token) { if (!~current) current = advance(); // 如果尚未开始处理字符的话,那就开始处理第一个字符 return current == token; }
/************************************************************************/ /* 读取下一个字符 */ /************************************************************************/ int advance() { if (!statements.length()) return current = EOI; // 如果待解析的已经空了, 则直接返回 EOI for (int i = 0; i< statements.length();) // 如果还有字符的话, 则遍历直至能读取到合法字符 { char c = statements.at(0); // 取第一个字符 switch(c) { case ';': statements = statements.substr(1); // 截掉第一个字符 return current = SEMI; case '+': statements = statements.substr(1); // 截掉第一个字符 return current = PLUS; case '*': statements = statements.substr(1); // 截掉第一个字符 return current = TIMES; case '(': statements = statements.substr(1); // 截掉第一个字符 return current = LP; case ')': statements = statements.substr(1); // 截掉第一个字符 return current = RP; case '\n': case '\t': case ' ': // 只有是空格、Tab、换行符的时候, 循环才要继续, 其余情况都是直接返回的 statements = statements.substr(1); // 截掉第一个字符 i++; continue; default: if (!isdigitsorid(c)) return UNKNOWN_SYMBOL; // 如果既不是数字又不是标识符的话, 返回非法字符 symbol = ""; // string在 c++中是一等公民, 是可以直接赋值的 while(i<statements.length() && isdigitsorid(c)) // 走到这里,c肯定是数字或者标识符, 所以肯定会进入循环 { symbol += c; if (++i<statements.length()) // 防御性编程 { c=statements.at(i); // 读取下一个字符, 最后要么超了(即i=statements.length(),此时调用substr得到的是空字符串""), 要么statements[i]不是字母或者数字 } } statements = statements.substr(i); // 把数字或标识符给截掉 return current = NUM_OR_ID; // 记录当前是数字或者标识符 } } return current = EOI; // 走到这里,只可能是 换行符、空格、Tab将for循环耗尽, 然后这里返回EOI. }
/************************************************************************/ /* 词法解析器规定的token们 */ /************************************************************************/ static int const EOI = 0; // 输入尾 static int const SEMI = 1; // 分号 static int const PLUS = 2; // + static int const TIMES = 3; // * static int const LP = 4; // ( static int const RP = 5; // ) static int const NUM_OR_ID = 6; // 数字或者标识符 static int const UNKNOWN_SYMBOL = 7; // 未知字符 string symbol; // 当前符号(即当前解析出来的有意义的字符串,如数字"49",如标识符"wocao")
private: /************************************************************************/ /* 判断一个字符是否是数字或者标识符 */ /************************************************************************/ bool isdigitsorid(char c) { return isdigit(c) || isalpha(c); }
int current; // 当前token string statements; // 当前还需要分析的文本 };
|
其实lexer.h 暴露对外的接口仅仅有一个 match(当前字符是否匹配) 一个是 advance(读取下一个字符).
最后是parser.h, 它暴露对外的接口仅仅是一个 statements (即解析表达式(statements)的.)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147
| #include "lexer.h"
#include <iostream> using namespace std;
class Parser { public: Parser(Lexer lexer):lexer(lexer),islegal(true) { reg[0] = "t0"; reg[1] = "t1"; reg[2] = "t2"; reg[3] = "t3"; reg[4] = "t4"; reg[5] = "t5"; reg[6] = "t6"; reg[7] = "t7"; top = 0; }
void statements() { while(!lexer.match(Lexer::EOI)) { string regName = fetchReg(); expression(regName); if (lexer.match(Lexer::SEMI)) { lexer.advance(); } else { islegal = false; cout<<"【syntax error】 缺少分号" << endl; } } if (islegal) { cout << "表达式解析正确." << endl; } }
private: Lexer lexer; void expression(string regName) { term(regName); while(lexer.match(Lexer::PLUS)) { lexer.advance(); string _regName = fetchReg(); term(_regName); cout << regName << "+=" << _regName << endl; freeReg(_regName); } }
void term(string regName) { factor(regName); while(lexer.match(Lexer::TIMES)) { lexer.advance(); string _regName = fetchReg(); factor(_regName); cout << regName << "*=" << _regName <<endl; freeReg(_regName); } }
void factor(string regName) { if (lexer.match(Lexer::NUM_OR_ID)) { cout << regName << "=" << lexer.symbol << endl; lexer.advance(); } else if (lexer.match(Lexer::LP)) { lexer.advance(); expression(regName); if (lexer.match(Lexer::RP)) { lexer.advance(); } else { islegal = false; cout << "【syntax error】 需要右括号." << endl; } } else { islegal = false; cout << "【syntax error】 需要数字或者标识符." << endl; } }
string fetchReg() { if (top>=8) { puts("【运行时错误】 寄存器栈underflow"); exit(EXIT_FAILURE); } return reg[top++]; }
void freeReg(string s) { if (!top) { puts("【运行时错误】 寄存器栈overflow."); exit(EXIT_FAILURE); } reg[--top] = s; }
bool islegal;
string reg[8];
int top; };
|
最后将这三份文件放在一个目录下面,确保机器正确安装了mingw64(并且已经写入了Path). 然后在这个目录下cmd执行如下命令
1
| g++ -o simplecompiler compiler.cpp lexer.h parser.h
|
得到的simplecompiler.exe 见DEMO【1】. 大家可以下载下来运行玩玩.
参考
【1】https://github.com/yfsyfs/algorithm/blob/master/simplecompiler.exe