lexer的构造函数
有了上一节Token
做铺垫, 可以开始设计lexer
, 首先应该想到的是, 源代码是以文件流的格式传到编译器中的, 所以作为编译器的前段的第一个阶段, lexer
必须负责处理输入的文件流.
private:
static const size_t BUFFERSIZE = 256;
static const size_t LIMITSIZE = 250;
static const size_t COPYLENGTH = BUFFERSIZE - LIMITSIZE;
std::ifstream& ifs;
char buffer[BUFFERSIZE];
size_t idx;
size_t row;
size_t column;
public:
Lexer(std::ifstream& ifs):ifs(ifs), idx(0), row(1), column(0){
ifs.read(buffer, BUFFERSIZE);
lex();
}
然后这里有一点要注意的是, 我设计了一个buffer
专门用来缓存读取的char
, 这么做的原因有如下几个 :
- 考虑到读取过程可能会使用前瞰来进行准确判断, 使用缓存
buffer
可以让我们方便地获取上一个读取的字符. - 一次读取一个字符效率太低, 一次多个读取有助于提高效率.
这里的row
和column
用来记录当前分析到的位置, 在编译器发现用户的源代码错误的时候可以给用户一个相对准确的定位. 所以昨天的Token
也需要做相应的修改.
// part of the Token class, not complete!
private:
Token_Type type;
union Value{
long l;
double d;
std::string s;
};
Value value;
size_t row;
size_t column;
public:
Token(Token_Type type, const std::string string, size_t row, size_t column): type(type), row(row), column(column) {
switch (type){
case INT:{
value.l = parseInt(string);
break;
}
case FLOAT:{
value.d = parseFloat(string);
break;
}
然后我们再来看下面几个工具函数.
void fill(){
if(idx <= LIMITSIZE){
return;
}
idx -= LIMITSIZE;
strncpy(buffer, buffer + LIMITSIZE, COPYLENGTH);
ifs.read(buffer + COPYLENGTH, LIMITSIZE);
}
char getNextChar(){
fill();
return buffer[idx++];
}
void eatSpace(){
char ch = 0;
while(ch = buffer[idx++]){
fill();
switch (ch){
case '
':{
++row;
column = 0;
break;
}
case ' ':{
}
case ' ':{
++column;
break;
}
default:{
--idx;
return;
}
}
}
}
第一个函数的作用是当缓存中的字符串几乎要分析完毕的时候, 将最后几个字符移动到开头并重新刷新缓存, 这里我这是的刷新界限是250
. 后面两个很简单, 就不解释了.
在进行词法分析之前, 我准备测试一下, 粗略地一下前面的代码, 我个人认为在代码规模相对较小的时候确保代码的正确性和可靠性是一种珍惜生命的方式...
void Lexer::lex() {
eatSpace();
std::cout << getNextChar() << std::endl;
std::cout << row << std::endl;
std::cout << column << std::endl;
}
int main() {
std::ifstream ifs("/Users/zhangzhimin/x.txt");
if(ifs){
Lexer x(ifs);
}
}
很幸运的是, 报错了...
In file included from /Users/zhangzhimin/ClionProjects/Fred/Font/Lexer/Token.cpp:1:
/Users/zhangzhimin/ClionProjects/Fred/Font/Lexer/Token.h:31:5: error: call to implicitly-deleted default constructor of 'Token::Value'
Token(Token_Type type, const std::string string, size_t row, size_t column): type(type), row(row), column(column) {
^
/Users/zhangzhimin/ClionProjects/Fred/Font/Lexer/Token.h:23:21: note: default constructor of 'Value' is implicitly deleted because variant field 's' has a non-trivial default constructor
std::string s;
^
/Users/zhangzhimin/ClionProjects/Fred/Font/Lexer/Token.h:31:5: error: attempt to use a deleted function
Token(Token_Type type, const std::string string, size_t row, size_t column): type(type), row(row), column(column) {
^
/Users/zhangzhimin/ClionProjects/Fred/Font/Lexer/Token.h:23:21: note: destructor of 'Value' is implicitly deleted because variant field 's' has a non-trivial destructor
std::string s;
^
2 errors generated.
log
看着一大堆, 其实都在说一个错误, 就是我在Token
中使用了一种联合体的方式, 也就是说将字符串放在union
里面会导致编译器无法为联合体自动合成构造函数和析构函数, 关于这一块我也是第一次接触到, so上搜索了一下 :
Unrestricted unions[edit]
In C++03, there are restrictions on what types of objects can be members of a
union
. For example, unions cannot contain any objects that define a non-trivial constructor or destructor. C++11 lifts some of these restrictions.[3]If a
union
member has a non trivial special member function, the compiler will not generate the equivalent member function for theunion
and it must be manually defined.
大概就是说, 在C++03的时候, 联合体如果有任何成员的构造函数或者析构函数是显式的, 那么就不行. 而在C++11中放宽了要求, 如果出现上述情况也可以, 但是对于那个显式出现的函数, 编译器将不再帮助你自动生成对应在联合体中的函数, 你必须自己定义它.
所以这个地方的错误根源在于标准库中的string有它自己的构造函数而且析构函数 这里稍微有点过于隐晦了, 所以我选择的改动方式是 : 对于所有的Token
都只留下string
, 将string
解析为long
或double
等到词法分析的时候再执行. 这样可以避开这种比较隐晦的问题, 因为说实话, 这一块我也不是很了解.
改动之后的Token
代码如下.
#ifndef FRED_TOKEN_H
#define FRED_TOKEN_H
#include <string>
class Token{
public:
enum Token_Type{
INT, // 0|([1-9][0-9]*)
FLOAT, // (0|([1-9][0-9]*).?[0-9]+)
STRING,
IDENTIFIER, // [a-zA-Z_][0-9a-zA-Z_]*
KEYWORD,
OPERATOR, // + - * / += -= *= /= = == ! - && ||
BRACKET, // () {} []
};
private:
Token_Type type;
std::string value;
size_t row;
size_t column;
public:
Token(Token_Type type, const std::string string, size_t row, size_t column): type(type), value(string), row(row), column(column) {
if(type == IDENTIFIER && isKeyword(string)){
this->type = KEYWORD;
}
}
Token(const Token&) = delete;
Token(const Token&&) = delete;
Token& operator=(const Token& rhs) = delete;
Token& operator=(const Token&& rhs) = delete;
virtual ~Token() = default;
Token_Type getType() const { return type; }
const std::string& getValue() const { return value; }
private:
long parseHelper(const std::string&, size_t&);
long parseInt(const std::string&);
double parseFloat(const std::string&);
bool isKeyword(const std::string&);
};
#endif //FRED_TOKEN_H
再次运行, 发现已经可以了, 但是我突然想到了另外一个问题. 我们每次读取下一个字符的时候都会尝试着跟新我们的缓存区, 但是如果文件中内容读取完毕之后是不是应该考虑不再跟新缓存区呢? 对此我做了两个改变 :
- 添加了一个flag
EndOfFile
, 并在更新缓存的fill()
函数中进行了相应的改变. - 将
fill()
改名为updateBuffer()
因为之前这个名字是在是太不贴切了...
所以经过一系列的更改, 目前源代码如下 :
#ifndef FRED_LEXER_H
#define FRED_LEXER_H
#include <fstream>
class Lexer{
private:
static const size_t BUFFERSIZE = 256;
static const size_t LIMITSIZE = 250;
static const size_t COPYLENGTH = BUFFERSIZE - LIMITSIZE;
std::ifstream& ifs;
bool EndOfFile;
char buffer[BUFFERSIZE];
size_t idx;
size_t row;
size_t column;
public:
Lexer(std::ifstream& ifs):ifs(ifs), EndOfFile(false), idx(0), row(1), column(0){
ifs.read(buffer, BUFFERSIZE);
lex();
}
void lex();
private:
void updateBuffer(){
if(idx <= LIMITSIZE || EndOfFile){
return;
}
idx -= LIMITSIZE;
strncpy(buffer, buffer + LIMITSIZE, COPYLENGTH);
ifs.read(buffer + COPYLENGTH, LIMITSIZE);
if(ifs.eof()){
EndOfFile = true;
}
}
char getNextChar(){
updateBuffer();
++column;
return buffer[idx++];
}
void eatSpace();
};
#endif //FRED_LEXER_H