2.4 扫描Token
识别 Token 的逻辑位于如下源代码中:
该文件内定义了与 Token 相关的三种类型:
type token uint // Token 类型
type LitKind uint8 // 如果当前 Token 是字面量,则标识该字面量类型
type Operator uint // 如果当前 Token 是操作符,则标识操作符类型
然后初始化了4种枚举常量,除了 token, LitKind, Operator 三种类型对应的常量外,还初始化了操作符优先级的常量,这些常量都在扫描 Token 时使用。详情请查看 tokens.go 文件,为了节约篇幅,这里不再贴出所有代码。
所有扫描 Token 的逻辑都通过结构
scanner
实现,其定义如下:type scanner struct {
source
mode uint
nlsemi bool // if set '\n' and EOF translate to ';'
// current token, valid after calling next()
line, col uint
blank bool // line is blank up to col
tok token
lit string // valid if tok is _Name, _Literal, or _Semi ("semicolon", "newline", or "EOF"); may be malformed if bad is true
bad bool // valid if tok is _Literal, true if a syntax error occurred, lit may be malformed
kind LitKind // valid if tok is _Literal
op Operator // valid if tok is _Operator, _AssignOp, or _IncOp
prec int // valid if tok is _Operator, _AssignOp, or _IncOp
}
重要的字段包括:
- source: 用来扫描字符
- tok: 当前 Token 的类型
- lit: 保存当前 Token 的字面量
- kind: 标识当前 lit 的种类
- op: 记录当前操作符
- prec: 标识当前操作符的优先级
扫描 Token 的逻辑在
next
中,该方法首先忽略掉空白字符,然后通过当前字符并根据其内容解析出下一个 Token. 我们来详细看一下其如何解析出程序中的标识符的。标识符(Identifier)就是程序中用于命名的那些词素,可以抽象的理解为用来绑定一个值的“变量”(先不要将此处的变量与程序中通过
var
申明的变量混淆,此处的变量是更加抽象的概念),先通过一个程序片段来理解一下:package gostudy
import fmtalias "fmt"
func main() {
content := "hello, gopher!"
fmtalias.Println(content)
}
其中用于命名的词素包括:
- gostudy: 为包命名
- fmtalias: 为所引入的包 fmt 命名
- main: 为函数命名
- content: 为局部变量命名
- Println: 绑定包中的函数值
这些标识符有时也被直接称为符号(Symbol),其 Token 类型被定义为
_Name
. Golang 将关键字也归为此类,在 init 方法中对关键字对应的 Token 类型做了初始化:func hash(s []byte) uint {
return (uint(s[0])<<4 ^ uint(s[1]) + uint(len(s))) & uint(len(keywordMap)-1)
}
var keywordMap [1 << 6]token // size must be power of two
func init() {
// populate keywordMap
for tok := _Break; tok <= _Var; tok++ {
h := hash([]byte(tok.String()))
if keywordMap[h] != 0 {
panic("imperfect hash")
}
keywordMap[h] = tok
}
}
init 将所有关键字的 Token 类型存放在数组
keywordMap
中,后续逻辑依此来判定扫描的标识符是否是关键字。Golang 规范的标识符命名规则是:
- 名字由数字、英文字母、下划线、或者编码超过两个字节的 Unicode 字符(编码数字大于 utf8.RuneSelf)组成
- 不能以数字开头
- 大小写敏感
- 不能使用关键字
- 长度不限制
函数
next
在过滤了空白字符后,首先就会判断当前字符是否是一个标识符的开端,其逻辑如下:func (s *scanner) next() {
// 忽略开头代码
if isLetter(s.ch) || s.ch >= utf8.RuneSelf && s.atIdentChar(true) {
s.nextch()
s.ident()
return
}
// 忽略后续代码
}
如果当前字符是合法的标识符开端,则通过
scanner.ident()
方法扫描对应标识符并返回。该方法也比较紧凑,我们可以贴出来看看:func (s *scanner) ident() {
// accelerate common case (7bit ASCII)
for isLetter(s.ch) || isDecimal(s.ch) {
s.nextch()
}
// general case
if s.ch >= utf8.RuneSelf {
for s.atIdentChar(false) {
s.nextch()
}
}
// possibly a keyword
lit := s.segment()
if len(lit) >= 2 {
if tok := keywordMap[hash(lit)]; tok != 0 && tokStrFast(tok) == string(lit) {
s.nlsemi = contains(1<<_Break|1<<_Continue|1<<_Fallthrough|1<<_Return, tok)
s.tok = tok
return
}
}
s.nlsemi = true
s.lit = string(lit)
s.tok = _Name
}
该方法的逻辑与 Golang 中对标识符的规范完全一致,其一直读取合法的标识符字符,再通过
s.segment()
取出标识符词素;然后根据之前初始化的 keywordMap
判断其是否是语言内置的关键字,如果是关键字则设置 tok 为对应的关键字类型,否则便将 tok 设置为 _Name
类型,并将词素存放在 lit 属性中。nlsemi: Golang 中使用分号来作为语句结束的标识符,但是大多数情况下都可以忽略掉,忽略的规则见:https://golang.org/ref/spec#Semicolons. 该字段用来判断如果当前 token 是当前行的最后一个 token 的话,是否可以自动插入一个分号。根据规则可见,如果该 token 是关键字 break, continue, fallthrough 与 return 之 一时,可以插入分号;而当该 token 是一个标识符时,总是可以插入分号。
我们来跑一下测试代码看看 scanner 对标识符的解析情况。在
scanner_test.go
中新增如下测试代码:func TestScanIdentTokens(t *testing.T) {
code := `package gostudy
import fmtalias "fmt"
func main() {
content := "hello, gopher!"
fmtalias.Println(content)
}
`
var scn scanner
scn.init(strings.NewReader(code), func(line, col uint, msg string) {
fmt.Printf("%d:%d: %s\n", line, col, msg)
}, 0)
scn.next()
for scn.tok != _EOF {
if scn.tok == _Name {
fmt.Printf("Token Type: %s, lit: %s\n", tokStrFast(scn.tok), scn.lit)
}
// 关键字标识符不会设置 lit 字段
if scn.tok >= _Break && scn.tok <= _Var {
fmt.Printf("Token Type: %s\n", tokStrFast(scn.tok))
}
scn.next()
}
}
运行后可以得到结果:
Token Type: packageToken Type: name, lit: gostudyToken Type: importToken Type: name, lit: fmtaliasToken Type: funcToken Type: name, lit: mainToken Type: name, lit: contentToken Type: name, lit: fmtaliasToken Type: name, lit: PrintlnToken Type: name, lit: content