2.4 扫描Token

识别 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
}

重要的字段包括:

  • 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: 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

最后更新于