词法分析
实验内容
完成 SQL 语言的词法分析器,要求采用课程教授方法,实现有限状态机确定化,最小化算法。词法分析器的输入为 SQL 语言源代码,输出识别出单词的二元属性,填写符号表。单词符号的类型包括关键字、标识符、界符、运算符、整数、浮点数、字符串。
实验过程
在词法分析的实现中我们分别实现了两套机制:手工构造词法分析以及基于 NFA 转 DFA 的确定化以及 DFA 最小化算法,下面我将详细这两种实现方式。
手工构造词法分析
+-------+ +--------+
-- source code --> | lexer | --> token stream --> | parser | --> assembly
+-------+ +--------+
在词法分析中,我们需要输入源代码,进而转化为 token stream。由于采用手工构造词法分析器,因此我们实现了 next()
方法来对一串源代码进行识别,并输出当前识别的 token,并将指针移到下一个 token 上,关于 next()
的框架如下所示:
// 获取下一个 token
bool Lexer::next(bool show) {
char* last_pos;
while((token = *src) && *src != 0) {
...
}
}
在主函数时,我们就可以不断循环调用 next()
函数从而不断获取 token。
在进行识别 token 之前,我们首先需要规定 token 的类型,token 类型的定义如下所示:
enum TokenType {
Int, Float, // 整数, 浮点数
Idn, // 标识符
Str, // 字符串
Equal, NonEqual, Less, LessEqual, Great, GreatEqual, SafeEqual, // 比较运算符
And, And2, Or, Or2, Xor, Not, Not2,// 逻辑运算符(AND, &&, OR, || , XOR)
Sub, // 算术运算符
Dot, // 属性运算符
Lp, Rp, Comma, // 界符((, ), ,)
// 关键字
Select, From, Where, As, WildCard,// 查询表达式
Insert, Into, Values, Value, Default, // 插入表达式
Update, Set, // 更新表达式
Delete, // 删除表达式
Join, Left, Right, On, // 连接操作
Min, Max, Avg, Sum, // 聚合操作
Union, All, // 集合操作
GroupBy, Having, Distinct, OrderBy, // 组操作
True, False, Unknown, Is, Null, // 条件语句
Invalid // 无效的 token
};
关于 token 的值的表示如下所示:
struct TokenValue {
double value; // token value, for Num
// used when return a string or a symbol address for assignment
Symbol* sym_ptr;
char* str_ptr;
};
其中关键字的定义如下,关于关键字如何识别我们将在下文提到:
const TokenType keywords[] = {
TokenType::Select,
TokenType::From,
TokenType::Where,
TokenType::As,
TokenType::Insert,
TokenType::Into,
TokenType::Values,
TokenType::Value,
TokenType::Default,
TokenType::Update,
TokenType::Set,
TokenType::Delete,
TokenType::Join,
TokenType::Left,
TokenType::Right,
TokenType::On,
TokenType::Min,
TokenType::Max,
TokenType::Avg,
TokenType::Sum,
TokenType::Union,
TokenType::All,
TokenType::GroupBy,
TokenType::Having,
TokenType::Distinct,
TokenType::OrderBy,
TokenType::True,
TokenType::False,
TokenType::Unknown,
TokenType::Is,
TokenType::Null,
TokenType::And,
TokenType::Or,
TokenType::Xor,
TokenType::Not
};
接下来我们就可以去进行识别 token 了,首先是关于整数与浮点数的识别,由于在这里我们仅仅涉及到 10 进制的整数,因此我们可以简单地将字符串在 ‘0’ - ‘9’ 之间的识别为整数,而中间出现 ‘.’ 的为浮点数,因此我们关于整数与浮点数的识别就很简单了:
else if(token >= '0' && token <= '9') {
this->token_val.value = (double)token - '0';
while(*src >= '0' && *src <= '9') {
this->token_val.value = this->token_val.value * 10.0 + ((double)(*src++) - '0');
}
if(*src == '.') {
this->token_type = TokenType::Float;
// 浮点数
src++;
int countDig = 1;
while(*src >= '0' && *src <= '9') {
this->token_val.value = this->token_val.value + ((double)(*src++) - '0')/(10.0 * countDig);
countDig++;
}
this->parser_token.type = "FLOAT";
} else {
this->token_type = TokenType::Int;
this->parser_token.type = "INT";
}
this->parser_token.value.emplace(this->token_val.value);
goto OUT;
}
随后是字符串的识别,由于在所给的标准中不区分字符和字符串,因此我们可以简单认为以 " 开头并以 " 结尾的源代码可以被识别为字符串,否则将被识别为错误,其中关于字符串识别的实现如下所示:
else if(token == '"') {
// 字符串
int size = 0;
while(*src != token) {
if(*src == 0) {
this->token_type = TokenType::Invalid;
goto OUT;
}
src++;
size++;
}
// 将对应的字符串放入地址中并将其存入符号表中
char* str = new char[size + 5];
memcpy(str, src - size, size);
str[size] = '\0';
this->token_type = TokenType::Str;
this->token_val.str_ptr = str;
src++;
// 为 parser 添加 token
this->parser_token.type = "STRING";
this->parser_token.str.emplace(str);
goto OUT;
}
接下来就是我们的重头戏:关于标识符的识别,由于在标识符的识别中,我们需要用到符号表,因此我们先介绍一下符号表及我们的实现。首先符号是为了记录某个标识符的名字以及它所对应的值,而符号表则是记录标识符符号的集合:
struct Symbol {
// Symbol Type: Int, Float, Str,...
TokenType type;
char name[MAX_NAME_SIZE];
double value;
};
std::vector<Symbol> symtab; // 符号表
在定义了符号与符号表之后我们就可以来实现关于标识符的识别,根据定义,标识符的第一个字符只能由 ‘a’ - ‘z’,‘A’ - ‘Z’ 和 ‘_’ 来组成,而在第一个字符后除了这些也可以使用 ‘1’ - ‘9’。 因而我们需要去记录标识符的名字并去符号表进行查找,如果找到了则直接将从符号表取出并传给语法分析器,否则则构造新的标识符并将其加入到符号表中并返回,实现如下所示:
值得注意的是,我们将关键字的识别也加入到了标识符中,因此对于 “ORDER BY” 和 “GROUP BY” 这种类型需要进行特殊处理,其中关于关键字我们在读入源代码前将其全部加入到符号表中,这样在识别到关键字的时候就可以在符号表中直接查找:
void Lexer::add_keywords() {
const char* src = "SELECT FROM WHERE AS INSERT INTO VALUES VALUE DEFAULT\
UPDATE SET DELETE JOIN LEFT RIGHT ON MIN MAX AVG SUM \
UNION ALL GROUP BY HAVING DISTINCT ORDER BY TRUE FALSE UNKNOWN \
IS NULL AND OR XOR NOT";
this->src = (char*)src;
size_t keyword_size = sizeof(keywords)/sizeof(keywords[0]);
for(size_t i = 0; i < keyword_size; i++) {
this->next(0);
this->symtab[this->symtab.size() - 1].type = keywords[i];
}
}
其他 token 的识别较为简单,这里不再赘述。
NFA 确定化以及 DFA 最小化
在实现 NFA 确定化以及最小化的时候,我们基于一种自己设定的文件格式来进行识别,首先从文件读入 NFA 然后将其转化成软件中的 NFA 的结构,随后实现 NFA 的确定化以及 DFA 的最小化,其中关于格式定义如下例子所示:
Initial State: {1}
Final States: {6}
Total States: 6
State a b c E
1 {2} {} {} {3}
2 {} {4} {} {4}
3 {4} {} {} {}
4 {} {3} {5} {2}
5 {6} {} {} {}
6 {} {} {} {5}
其中 E 表示 eplision,也就是空符号。
我们定义的 DFA 状态表和 NFA 状态表如下所示:
struct DFAState {
// 是否被遍历过
bool marked;
// 当前的状态,使用字符集来表示
std::vector<int> states;
// 经过某字符的 move 后的下一状态的标号
std::map<char, int> moves;
};
// DFA 状态表
typedef std::map<int, DFAState> DFATable;
// NFA 状态表
typedef std::map<int, std::map<char, std::vector<int>>> NFATable;
关于从文件读取与解析的代码较长而且对于算法实现意义不大,这里就不再进行贴出了。
在 NFA 的确定化中,关键的两个函数是 eplision 转换以及对应的字符转换。由于 eplision 转换允许若干次 eplision 转换,因此我们需要使用 stack 来存储所有结果中不包含的元素:
而经过字符的状态转换只经过一次转换,实现较为简单:
// 根据一次元素 move 获取对应所有元素组成的状态
std::vector<int> NFAToDFA::move(std::vector<int> T, char ele, NFATable nfa_table){
std::vector<int> res;
// 遍历给定的状态,找到可以通过 ele 发现的所有元素
for(auto &i: T) {
auto reachable_states = nfa_table[i][ele];
for(auto &j: reachable_states) {
if(!vector_contain(res, j)) {
res.push_back(j);
}
}
}
return res;
}
随后的 NFA 确定化即为不断使用这两个函数到 NFA 上面直到不产生新的状态:
而关于 DFA 的最小化则需要首先将非终结态和终结态里的状态组成两个集合 push 到队列中,随后分别对队列中每个集合中的每个元素根据基于字符移动,倘若该集合中的每个元素的目标全为同一个集合,则将其从队列中 pop 出去,并加入结果集;否则将其分为两个集合,再次 push 到队列中,实现如下所示:
// DFA 最小化
std::vector<std::vector<int>> NFAToDFA::dfa_minialize() {
// 首先将 DFA Table 分为终结状态和非终结状态
std::queue<std::vector<int>> dfa_partiation;
std::vector<std::vector<int>> dfa_min_ans;
// 非终结状态
std::vector<int> non_finial_state;
for(size_t i = 0; i < _dfa_state_table.size(); i++) {
if(std::find(_dfa_final_states.begin(), _dfa_final_states.end(), i) == _dfa_final_states.end()) {
non_finial_state.push_back(i);
}
}
dfa_partiation.push(non_finial_state);
dfa_partiation.push(_dfa_final_states);
dfa_min_ans.push_back(non_finial_state);
dfa_min_ans.push_back(_dfa_final_states);
// 分别读入不同的符号来进行下一步划分
// 主要查看该状态在 move 后是否会变为其他状态
for(auto &ele: _alphabet) {
// 遍历所有字母
// 查看某个子集所有元素 move 后仍属一个集合
while(!dfa_partiation.empty()) {
// 从队列中取出一个集合
auto set = dfa_partiation.front();
dfa_partiation.pop();
std::vector<int> set_a;
std::vector<int> set_b;
std::optional<int> a;
std::optional<int> b;
for(auto &state: set) {
// 遍历每个子集的状态
// 获取到 move 后的状态
int move_state = _dfa_state_table[state].moves[ele];
int move_set = find_key_in_vec(dfa_min_ans, move_state);
if(!a.has_value()) {
a.emplace(move_set);
set_a.push_back(state);
}else{
if(move_set == a.value()) {
set_a.push_back(state);
}else {
if(!b.has_value()) {
b.emplace(move_set);
set_b.push_back(state);
}else{
set_b.push_back(state);
}
}
}
}
if(!b.has_value()) {
// 如果只有一个集合的话不动
}else{
// dfa_min_ans.erase(dfa_min_ans.begin());
erase_vec_vec(dfa_min_ans, set);
dfa_partiation.push(set_a);
dfa_partiation.push(set_b);
dfa_min_ans.push_back(set_a);
dfa_min_ans.push_back(set_b);
}
}
// 将所有得到的序列 push 进队列进行下一次 move
for(auto &i: dfa_min_ans) {
dfa_partiation.push(i);
}
}
return dfa_min_ans;
}
实验结果
测试第二个测试用例结果如下所示: