1.2 定义词法单元

首先要做的是定义词法分析器输出的词法单元。这里先定义少量的词法单元,之后在扩展词法分析器时再添加更多的定义。

第一次要解析的Monkey语言代码如下所示:

let five = 5;
let ten = 10;

let add = fn(x, y) {
  x + y;
};

let result = add(five, ten);

来详细看看,这个例子中包含哪些类型的词法单元。首先,有510这样的数字,这很明显;之后是xyaddresult这样的变量名。最后Monkey语言中还有一些单词,它们既不是数字也不是变量名,例如letfn。当然,还有很多特殊字符,如(){}=,;

这些数字都是整数,将按字面量处理,并赋予其一个单独的类型。在词法分析器或语法分析器中,数字的值是5还是10并不重要,只要知道它是一个数字就行。变量名也是如此,都统一用作标识符。除此之外还有一些单词,看起来像标识符但实际上不是,这些称为关键字,也是Monkey语言的一部分。后面在语法分析阶段遇到letfn这样的关键字时,都会特殊处理,所以它们不能与标识符归为一类。最后的特殊字符也会单独列出来,其中每个特殊字符都会有相应的处理方式,例如在源代码中,括号会对代码的含义产生很大影响。

基于这些分析,现在来定义Token数据结构。它需要哪些字段呢?正如刚刚看到的,肯定需要一个类型属性,这样就可以区分“整数”和“右括号”这样不同的词法单元。然后还需要一个字段用于保存词法单元的字面量,以便后续步骤复用,比如对于表示数字的词法单元,这个字段能记录510这样的信息。

新建一个token包,以便定义Token结构和TokenType类型:

// token/token.go

package token

type TokenType string

type Token struct {
    Type    TokenType
    Literal string
}

TokenType类型定义成了字符串,这样我们就可以使用各种TokenType值,而根据TokenType值能区分不同类型的词法单元。使用字符串对调试也有帮助,会让调试更容易,而无须再使用许多样板和辅助函数,只需打印一个字符串即可。当然,与使用intbyte类型相比,使用字符串会导致程序在性能上有损失。但对于本书而言,使用字符串完全没有问题。

从刚刚的分析中可以看到,Monkey语言中的词法单元类型并不多。这意味着可以将所有的TokenType都定义为常量,所以在上面的代码中可以再添加以下内容:

// token/token.go

const (
    ILLEGAL = "ILLEGAL"
    EOF     = "EOF"

    // 标识符+字面量
    IDENT = "IDENT" // add, foobar, x, y, ...
    INT   = "INT"   // 1343456

    // 运算符
    ASSIGN = "="
    PLUS   = "+"

    // 分隔符
    COMMA     = ","
    SEMICOLON = ";"

    LPAREN = "("
    RPAREN = ")"
    LBRACE = "{"
    RBRACE = "}"

    // 关键字
    FUNCTION = "FUNCTION"
    LET      = "LET"
)

如你所见,上面的代码中还出现了ILLEGALEOF这两种特殊类型。这两种类型在之前的示例中并没有遇到,却是必不可少的。ILLEGAL表示遇到未知的词法单元或字符,EOF则表示文件结尾(End Of File),用于通知后续章节会介绍的语法分析器停机。

目前一切顺利,下面准备开始编写词法分析器。