词法分析Simple Lexer
以前尝试阅读《编译原理》,但都没有读下来,现在看《编程语言实现模式》,感觉轻松多了。其实,目前我只对解析感兴趣,只要看这本书的第一部分解析起步就可以了,确实没有必要去啃《编译原理》。下面就是学习的内容了。
解析,分为两步,先是进行词发分析,将输入转换成一个一个的Token,然后是进行语法分析。一个一个的Token组成语句,对应一定的语法。根据这些Toke,匹配一定的语法。词法分析器,lexer,是语法分析器,parser,的基础。
先来看看词发分析器。例如语句,1024+ 78*pi,一个简单的表达式,
词发分析就是要得出上面的语句由这些Token组成,1024, + , 78, * ,pi。Token可以是运算符,可以是数字,也可以是字符串。在处理原来的字符串时,遇到第一个字符'1',把它放入一个buf里面,然后遇到0,仍然是合法的数字,放入buf里面,直到遇到'+',这时就可以把buf返回,得到1024这一个token。然后从刚才的位置继续,'+‘,直接返回这个Token,可以把其作为运算符一类,也可以是运算符细分之后的‘+’一类。后面遇到空格,跳过。后面的类似处理。
在做这些操作的时候,由lexer的数据结构支持,所以lexer需要保存原始的字符串,需要知道当前的位置,而Token应该有两个元素,一个保存它的内容,另一个保存它的类型。在解析的时候,就可以知道所得到的Token类型,直接保存下来,便于以后处理。而对应的操作,就需要有,判断当前字符是什么类型,字母,数字,或是运算符等。然后就是收集Token的操作。当遇到一个数字的时候,需要循环处理,知道遇到的不再是数字,这是对简单的整数,如果需要处理浮点数,科学记数法等,就会复杂一些了。当遇到一个字母时,如果不允许字符串里面包含数字,那么就只收集字母,如果可以包含数字,那么就是遇到运算符的时候结束了。
下面就是一个简单的lexer的实现,支持字符串,运算符,数字,的词法分析。
首先是Token,这里用到了TokenType,type用整数表示,本来是想用enum class TokenType的,但是在继承的时候遇到了问题。虽然这个enum class定义很像class,但是不支持继承。那么我想添加type的时候,就不是很方便。然后就有了下面的一个类来定义TokenType。
class TokenType { public: TokenType():_EOF(0),TEXT(1),NUMBER(2),name({"EOF","TEXT","NUMBER"}){ } string Name(int t) { return name.at(t); } public: const int _EOF; const int TEXT; const int NUMBER; vector<string> name;// = {"EOF","TEXT"}; }TokenType; class Token { public: Token(){} Token(int tp, string tx) { type = tp; text = tx; } int Type() const { return type; } string Text() const { return text; } friend ostream& operator << (ostream& out,const Token& token){ out << "<" << TokenType.Name(token.Type()) << "," << token.Text() << ">" << endl; } protected: int type; string text; };
在Lexer里面,比较重要的是如何收集Token,对应的是NextToken这个方法,而在这里面用到的判断当前字符类型的isLETTER,isWS,isDIGIT,都很容易实现,而TEXT,NUMBER则是在触发收集对应Token的时候开始执行。这几个方法如下。
/*! \brief consume the current char, and get the next char * if it is the end of input, set c as EOF */ void consume(){ p++; if (p >= text.length()) c = _eof; else c = text[p]; } /*! \brief match the current char to target char * * @param x */ void match(char x){ if(c==x) consume(); else LexerError err(x,c,p); } /*! \brief get token from the text, * here only support text word and digital number, * integer, float or scientific number * @return the next token */ Token NextToken(){ while(c!=_eof){ if(isWS()) WS(); else if(isLETTER()) return TEXT(); else if(isDIGIT()) return NUMBER(); else LexerError err(c,p); } return _EOF(); } bool isWS(){ return (c==' ' || c=='\t' || c=='\r'); } bool isLETTER(){ return ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')); } bool isDIGIT(){ return (c >= '0' && c <= '9'); } /*! \brief Get all the following digits as an integer * * @return return the integer in the string format */ string Digit() { string buf; do { buf.push_back(c); consume(); } while (isDIGIT()); return buf; } /*! \brief Skip all whitespace * */ void WS(){ while(isWS()) consume(); } Token _EOF(){ return Token(TokenType._EOF,toString(_eof)); } /*! \brief Get all the following letter into a text word * * @return return the word as a TEXT token */ Token TEXT() { string buf; do { buf.push_back(c); consume(); } while (isLETTER()); return Token(TokenType.TEXT, buf); } /*! \brief Get the number, whether it is a integer, or a float, * or even in scientific format * * @return */ Token NUMBER() { string buf = Digit(); if (c == '.') buf += Digit(); if (c == 'e' || c == 'E') { consume(); buf.push_back(c); if (c == '+' || c == '-') { consume(); buf.push_back(c); buf += Digit(); } } return Token(TokenType.NUMBER, buf); }
可以看看收集NUMBER的过程,如果使用正则表达式,对应的模式为 (\d)+([.](\d)+)?([e|E][-|+]?(\d)+)?。或许正则表达式的过程就是上面的过程。
下面是测试结果,
开始使用github了,这个项目的完整代码可以参考,https://github.com/Frandy/anapig.git
希望能与大家多交流,谢谢。