从零开始实现 Lua 解释器之 Token 实现

作者: 每天一道编程题 | 来源:发表于2016-05-06 14:49 被阅读1821次

警告⚠️:这将是一个又臭又长的系列教程,教程结束的时候,你将拥有一个除了性能差劲、扩展性差、标准库不完善之外,其他方面都和官方相差无几的 Lua 语言解释器。说白了,这个系列的教程实现的是一个玩具语言,仅供学习,无实用性。请谨慎 Follow,请谨慎 Follow,请谨慎 Follow。

这是本系列教程的第二篇,如果你没有看过之前的文章,请从头观看。

前言


本节我们正式开始这个教程,在本文中,我们将开始着手实现 SLua 的词法解析模块一个重要的类型:Token。

Token 定义


所谓的 Token 就是一个词法单元,用来表示程序中最基本的一个元素。举例来说,编程语言中的每个关键字、操作符、Identifier、数字、字符串等,都是 Token。所以第一步,我们需要定义一个结构体来表示一个 Token。

首先想到的是 Token 需要有字段来表示类型。通常我们都会使用枚举来完成这个任务。但 Go 语言并没有提供枚举的支持,所以我们使用 const 表达式定义一系列的常量来达到这个目的:

const (
    TokenAnd          string = "and"
    TokenDo                  = "do"
    TokenElse                = "else"
    TokenElseif              = "elseif"
    TokenEnd                 = "end"
    TokenFalse               = "false"
    TokenIf                  = "if"
    TokenLocal               = "local"
    TokenNil                 = "nil"
    TokenNot                 = "not"
    TokenOr                  = "or"
    TokenThen                = "then"
    TokenTrue                = "true"
    TokenWhile               = "while"
    TokenID                  = "<id>"
    TokenString              = "<string>"
    TokenNumber              = "<number>"
    TokenAdd                 = "+"
    TokenSub                 = "-"
    TokenMul                 = "*"
    TokenDiv                 = "/"
    TokenLen                 = "#"
    TokenLeftParen           = "("
    TokenRightParen          = ")"
    TokenAssign              = "="
    TokenSemicolon           = ";"
    TokenComma               = ","
    TokenEqual               = "=="
    TokenNotEqual            = "~="
    TokenLess                = "<"
    TokenLessEqual           = "<="
    TokenGreater             = ">"
    TokenGreaterEqual        = ">="
    TokenConcat              = ".."
    TokenEOF                 = "<eof>"
)

后面会有解释为什么类型枚举要使用 string 类型,而不是通常用的 int。

在上一篇文章中介绍过,为了简化任务,我们第一次实现的是一个被严重阉割过的 Lua,所以相应地,Token 的类型也少了很多。

有了类型的定义,我们就可以定义 Token 结构体了:

type Token struct {
    Category string
}

其中,Category 中存储的就是 Token 的类型。对于关键字和操作符,这样的定义就足够了,因为它们并不需要其他额外的信息。但 TokenID、TokenString 需要有个额外的字符串存储具体的值,同理对于 TokenNumber,则需要有个 float64 的字段来存储具体的数值。

为了不浪费存储空间,在 C 语言中,我们很容易想到使用 union 结构来达到这一目的:

union {
    double num;
    char* str;
};

但不幸的是,Go 语言并不支持 union,所以只能另辟蹊径。好在 Go 语言提供了一个强大的类型:interface{}。这是一个通用类型,可以存储任意类型的值。我们可以使用它来存储 string 和 float64 类型的值。所以,现在 Token 类型的定义如下:

type Token struct {
    Value    interface{}
    Category string
}

为了简化实现,在 Lua 中,不管整数值还是浮点数,在底层都是存储在一个 float64 中的。

另外,为了用户的体验,在程序出错的时候需要告诉用户是哪一行哪一列出了错,所以我们需要记录下 Token 所在的行和列,现在的 Token 定义如下:

type Token struct {
    Value    interface{}
    Line     int
    Column   int
    Category string
}

有了这个结构体,我们还需要为它定义一些方法。

func NewToken() *Token {
    token := new(Token)
    token.Category = TokenEOF
    return token
}

上面的 NewToken 方法返回一个类型为 TokenEOF 的 Token,EOF 的意思是 End Of File,用来表示文件的结尾,也就是源代码读取结束。

我们使用 TokenEOF 作为默认值。

为了调试打印方便,我们给 Token 定义 String 方法,以满足 fmt.Stringer 接口:

func (t *Token) String() string {
    var s string
    if t.Category == TokenNumber || t.Category == TokenID ||
        t.Category == TokenString {
        s = fmt.Sprintf("%v", t.Value)
    } else {
        s = t.Category
    }
    return s
}

注意到,在 String 方法中,如果 Token 的类型为 TokenNumber、TokenID 或 TokenString 时,返回值为 Value 字段中存储的具体值。否则直接返回 Token 的类型。这也是我们的类型枚举使用的是 string 类型,而不是常见的 int 类型的原因。

1.Go 语言不同于 Java 等语言,它的接口实现机制是隐式的。只要你实现了接口要求的方法,就相当于实现了接口, 没有显式声明的必要。所以自然也就没有关键字 implements。

2.fmt.Stringer 的定义为:

type Stringer interface {
    String() string
}

除此之外,我们还定义了 Clone 方法,用来深度拷贝 Token 值:

func (t *Token) Clone() *Token {
    return &Token{
        Value:    t.Value,
        Line:     t.Line,
        Column:   t.Column,
        Category: t.Category,
    }
}

另外,我们还提供了一个帮助函数,用来确定某个字符串是不是 Lua 的关键字之一。这个函数将在之后的词法解析中用到:

func isKeyword(id string) bool {
    switch id {
    case TokenAnd, TokenDo, TokenElse, TokenElseif, TokenEnd,
        TokenFalse, TokenIf, TokenLocal, TokenNil, TokenNot,
        TokenOr, TokenThen, TokenTrue, TokenWhile:
        return true
    default:
        return false
    }
 }

注意到,Go 语言中的 switch 语句不需要使用 break。

至此,Token 类型的完整定义就完成了。你可以在此查看完整的源代码:地址

获取源代码


代码已托管到 Github 上:SLua,每一个阶段的代码我都会创建一个 release,你可以直接下载作为参照。虽然提供了源代码,但并不建议直接复制粘贴,因为这样学到的知识会很容易忘记。

刚开始玩 Github 和简书,所以没有任何粉丝和关注量(哭),如果你觉得这篇教程有帮助,请不要吝啬给文章点个喜欢,给 Github 上的项目点个 Star。如果能 Follow 一下简书和 Github 的账号就更好啦,我也会更加有动力将这个系列写下去。:)

相关文章

网友评论

    本文标题:从零开始实现 Lua 解释器之 Token 实现

    本文链接:https://www.haomeiwen.com/subject/bnydrttx.html