词法分析器(Lexer)的实现

2022-12-01 15:53:05 浏览数 (1)

  • 写在前面

写下Compiler系列的主要目的,是为了记录一下本人在学习编译原理以及做出一个简单的Compiler的历程,为后续向二进制安全的更深领域的学习打下基础。

  • Lexer是什么

Lexer是Lexical analyzer的缩写,中文意思为词法分析器,是进行词法分析的程序或者函数,这也是编译器所做的第一项工作。注意:如果没有编译原理的相关知识,请先学了编译原理之后再来看本文章。

  • 词法分析的任务

词法分析的任务就是让编译器搞懂我们究竟写了什么,编译器会先将我们的程序切片成一个一个的单词,将其作为一个token,每个token都会带有一个编号。

  • Lexer的实现

从这里开始,将会开始进行第一步,也就是实现一个简单的词法分析器,文章中只会讲述思想的思路以及部分代码,完整的代码请看我的github:h1J4cker

我们先思考一下,在我们的代码中,哪些东西虽然表达的方式不同,但其实可以被划分为一类呢?

首先是我们定义变量的时候,用到的int,char等,我们可以认为他们都是标识类型的关键字,所以即便int与char在单词的组成以及含义方面不同,但我们仍然可以把他抽象为同一个类型:关键字

注意:我们可以把宏定义的关键词define,以及外部链接extern单独分出来,因为对于这两个关键词我们需要单独处理。

其次,我们可以想到,在一个程序中,被大量使用的不仅是int,char等关键字,还有由他们所定义的数据,为了简单起见,我们把所有的数据都认为是double类型,那么再次,我们又可以抽象出另一个类型:数值

最后我们需要把文件的结束符,也就是EOF单独作为一个类型。

那么从上面的分析中我们可以定义一个枚举类型Tokenizer:

代码语言:javascript复制
enum Tokenizer
{
    tok_eof = -1,
    tok_def = -2,
    tok_extern = -3,
    tok_identifier = -4,
    tok_number = -5
};

此外我们还需要定义两个变量来存储标识符的值,以及数据的数值。

代码语言:javascript复制
static std::string IdentifierStr; //标识符一般是一串字符串,所以将存储它的变量定义为string类
static double NumVal; //数据统一认为是double类型

当我们抽象出一个个token之后,我们需要一段程序来识别这些token,而我们需要做的第一步,就是要去除为了程序的工整性而加入的一个个空格。

代码语言:javascript复制
static int __Get_Tok()
{
    static int LastChar = " ";

    while (isspace(LastChar))
    {
        LastChar = getchar();
    }

这段程序的作用是判断LastChar是否是空格,如果是就不处理,并接收下一个字符。

然后我们需要识别对应的字符串是否属于我们前面定义中的某一类,如果属于,则返回相应的值,如果不属于,那么他可能是一些运算符如: -。那么我们就需要返回他的ASCII码值。

首先我们来识别关键字:

代码语言:javascript复制
if (isalpha(LastChar))
  {
      IdentifierStr = LastChar;
      while (isalnum(LastChar = getchar()))
      {
          IdentifierStr  = LastChar;
      }

      if (IdentifierStr == "def")
      {
          return tok_def;
      }
      else if (IdentifierStr == "extern")
      {
          return tok_extern;
      }
      else
      {
          return tok_identifier;
      }
  }

这段程序首先判断了是否LastChar是字符,如果是那么就进入下面的阶段,接收下一个字符,判断它是否是数字,如果不是,那么就将LastChar赋值给IdentifierStr进行存储,同时判断是否是三种关键字中的某一个,如果是就返回相应的值。

接下来我们来判断数值类型,只要遇到代表数值的字符串,我们就将其转换为数值,并存入Numval中,并返回相应的值:

代码语言:javascript复制
if (isdigit(LastChar) || LastChar == ".")
{

    std::string NumStr;

    do
    {
        NumStr  = LastChar;
        LastChar = getchar();
    } while (isdigit(LastChar) || LastChar == ".");

    NumVal = strtod(NumStr.c_str(), 0);
    return tok_number;
}

当我们遇到注释时,我们只需要跳过当前行即可,也就是说我们遇到注释符时,我们就一直读到n为止:

代码语言:javascript复制
if (LastChar == "#")
{
    do
    {
        LastChar = getchar();
    } while (LastChar != EOF && LastChar != 'n' && LastChar != 'r');

    if (LastChar != EOF)
    {
        return __Get_Tok();
    }
}

当上面这些步骤做完以后,如果能存在未处理的字符串,那么只可能是两种情况:

1.读到了运算符

2.读到了EOF

那么,此时的处理过程也相当简单,我们只需要判断一下到底是哪种情况,并返回相应的值即可:

代码语言:javascript复制
if (LastChar == EOF)
{
    return tok_eof;
}

int ThisChar = LastChar;
LastChar = getchar();
return ThisChar;
  • 结尾

到这里,一个简单的词法分析器就基本上完成了,我们已经可以识别数据,关键词,标识符等等识别出来为下一步语法分析做准备了。

0 人点赞