识别 Token 的逻辑位于如下源代码中:
tokens.go
该文件内定义了与 Token 相关的三种类型:
复制 type token uint // Token 类型
type LitKind uint8 // 如果当前 Token 是字面量,则标识该字面量类型
type Operator uint // 如果当前 Token 是操作符,则标识操作符类型
然后初始化了4种枚举常量,除了 token, LitKind, Operator 三种类型对应的常量外,还初始化了操作符优先级的常量,这些常量都在扫描 Token 时使用。详情请查看 tokens.go 文件,为了节约篇幅,这里不再贴出所有代码。
scanner.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
}
重要的字段包括:
扫描 Token 的逻辑在next
中,该方法首先忽略掉空白字符,然后通过当前字符并根据其内容解析出下一个 Token. 我们来详细看一下其如何解析出程序中的标识符的。
标识符(Identifier)就是程序中用于命名的那些词素,可以抽象的理解为用来绑定一个值的“变量”(先不要将此处的变量与程序中通过 var
申明的变量混淆,此处的变量是更加抽象的概念),先通过一个程序片段来理解一下:
复制 package gostudy
import fmtalias "fmt"
func main () {
content := "hello, gopher!"
fmtalias. Println (content)
}
其中用于命名的词素包括:
这些标识符有时也被直接称为符号(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: package
Token Type: name, lit: gostudy
Token Type: import
Token Type: name, lit: fmtalias
Token Type: func
Token Type: name, lit: main
Token Type: name, lit: content
Token Type: name, lit: fmtalias
Token Type: name, lit: Println
Token Type: name, lit: content