今天来讲第一部分Scanner,俗称扫描器,也叫词法分析器。想要了解Scanner究竟做了什么,我们要从整个流程讲起。 首先,计算器得到的输入的是一串字符,如 ”1 + 2“。 如果不学编译原理,应该如何计算出结果呢?可能会利用栈,一个数字栈一个符号栈云云,但这样处理简单运算还好,如果有大于10的数,小数或者含有括号的情况,情况会很糟糕,何况编程语言本就比四则运算复杂许多。一般编译器的做法,则是将字符串以Token为单位分割开来。Token,中译为”标记、令牌“,不直观,你可以理解为表达式中的最小类型单位(《编译语言实现模式》中举例自然语言的句子成分也很贴切)。例如上文提到的”1 + 2“可以分解为“数字+符号+数字”的组合。得出Token之后,我们就可以在接下来的语法分析部分(Parser)中根据文法对其进行处理,具体方法我们下节再说。
词法分析可以采用lex等工具自动实现,但为了学习,我们还是人肉实现。这里引入一个概念,状态机(state machine),也叫自动机(automation),顾名思义,就是拥有状态集合的机器,并可以通过边进行状态的迁移(类似图,我突然想是否能直接用图实现状态机,等活干完试一下)。
先举一个简单的例子,下图表示一个个位整数,state 1为开始状态,此时向状态机输入一个0-9的整数,状态机迁移到state 2结束(接受)状态,当字符输入完毕并达到结束状态时,表示匹配成功。
多位整数应该如何表示?可以在结束状态上添加一个循环,表示接受多次输入,相当于正则中的”+“。下方的边表示单独的0。
PS:其实状态机也可以表示状态。如下图,表示二元运算式子,当然,这里不讲,只是让大家了解下Token到底是用来干什么的。
好了,状态机的概念理解后,我们来讲如何实现它。先理清思路,我们刚才讲到,Scanner的目的就是构造出Token,所以,首先要创建class Token
class Token { // 略 private: TokenType type_; int intValue_; };
type_代表Token的类型,这里把Token分成四大类,即数字(暂只支持整数)、符号(+-*/)、左右括号以及无效状态(invalid)
intValue_表示整数的值
enum class TokenType { INT, FLOAT, ADD, // + SUB, // - MUL, // * DIV, // / LEFT_PAR, // ( RIGHT_PAR, // ) INVALID, // 无效类型 };
其次,由于operator多而杂,我们可以写一个dictionary以键值来存储它们。它有HasToken、FindToken等方便以供调用(太长不写了,详见代码)。
有了Token之后,就可以构建提取Token的函数了。这里主要讲一下GetNextToken,他是整个状态机的核心,处于while循环中,共分为两部分。第一部分为状态处理,根据当前状态选择处理方式,第二部分state迁移。此阶段对char进行识别,判定迁移state。初始状态为start,会直接进入state迁移,所以本质上就是根据前一个 or 几个char确定状态后,交由handle函数处理。
Token Scanner::GetNextToken(stringstream &expression) { // first char auto currectChar = GetNextChar(expression); // state judge while (!expression.eof()) { // 第一部分 switch (state_) { case State::START: break; case State::NUMBER: return HandleNumberState(expression, currectChar); break; case State::OPERATOR: return HandleOperatorState(expression, currectChar); break; case State::ERROR: ErrorToken("error input"); return Token(TokenType::INVALID); } string currectStr; currectStr.push_back(currectChar); //第二部分 if (iswdigit(currectChar)) { state_ = State::NUMBER; } else if (dict_.HasToken(currectStr)) { state_ = State::OPERATOR; } else { state_ = State::ERROR; } } return Token(TokenType::INVALID); }
两个handle函数的会把符合条件的char存入buffer,返回构建成Token。
HandleNumberState:接收两个参数字符stream和currectChar(刚才iswdigit()的数字)。构建后重置state为start表示匹配结束。代码如下:
Token Scanner::HandleNumberState(stringstream &expression, char currectChar) { // first char string buffer; buffer.push_back(currectChar); while (!expression.eof() && isdigit(expression.peek())) { buffer += GetNextChar(expression); } // reset state state_ = State::START; // string to int int value; std::stringstream stream(buffer); stream >> value; return Token(TokenType::INT, value); }
HandleOperatorState:与number唯一不同的是Token通过自建的Dictionary类构建。
Token Scanner::HandleOperatorState(stringstream &expression, char currectChar) { // first char string buffer; buffer.push_back(currectChar); auto token = dict_.FindToken(buffer); //reset state state_ = State::START; return token; }
GetTokenList、GetNextTokenList就是以此为基础获得一行内的所有Token。
Scanner的内容至此就讲完了,接下来是Parser部分。另外,写东西果然好难,磕磕巴巴,很多思路根本无法表达( ╯□╰ )