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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,265评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,274评论 1 288
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,087评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,479评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,782评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,218评论 1 207
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,594评论 2 309
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,316评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,955评论 1 237
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,274评论 2 240
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,803评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,177评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,732评论 3 229
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,953评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,687评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,263评论 2 267
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,189评论 2 258

推荐阅读更多精彩内容

  • 警告⚠️:这将是一个又臭又长的系列教程,教程结束的时候,你将拥有一个除了性能差劲、扩展性差、标准库不完善之外,其他...
    每天一道编程题阅读 4,431评论 0 8
  • ¥开启¥ 【iAPP实现进入界面执行逐一显】 〖2017-08-25 15:22:14〗 《//首先开一个线程,因...
    小菜c阅读 6,187评论 0 17
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,083评论 18 139
  • "不要挤,排好队一个个上!" 这是一个胖胖的,脾气暴躁的列车员,声音沙哑却洪亮的企图维持上车秩序,鼻梁上的眼...
    王靠谱阅读 302评论 0 1
  • 没有谁的生活不曾一地鸡毛,没有谁的样子不曾狼狈不堪。 每个人的潜意识里都住了个理想主义者,理想主义对每个人都是公平...
    手剥松子阅读 450评论 0 2