警告⚠️:这将是一个又臭又长的系列教程,教程结束的时候,你将拥有一个除了性能差劲、扩展性差、标准库不完善之外,其他方面都和官方相差无几的 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 的账号就更好啦,我也会更加有动力将这个系列写下去。:)
网友评论