这是我参与「第五届青训营」伴学笔记创作活动的第 8 天
go语言将源代码通过词法分析得到的token序列进行语法分析生产AST(Abstract Syntax Tree;抽象语法树)。go语言和其他语言的编译器一样没有用parser generator,而是用手写的递归下降进行解析,并且不进行回溯。
go语言关于语法解析的部分存在GOROOT/src/go/parser和GOROOT/src/go/ast中,其中包ast是抽象语法树的数据结构,而parser包负责构建相应的数据结构,给后续的types包做类型推断和类型检查。
在parser包中,interface.go才是调用的入口,其中以ParseFile为例,就是读取源文件为[]byte然后对Parse对象调用init方法初始化对象,调用parseFile返回ast.File的指针里面包含这个文件的所有语法内容。而ParseDir则是递归地将给出目录中的所有文件编译成ast.File并放入相应名字的ast.Package,最后会返回一个与包名相对应的map。
接下来看Parser结构体的parseFile方法,可以看出一个源码文件去除头部的注释之外第一个语法单元是源文件所属的包,这个声明包所属的语法单元不是一个声明Declaration所以是单独处理的。
// package clause
doc := p.leadComment
pos := p.expect(token.PACKAGE)
// Go spec: The package clause is not a declaration;
// the package name does not appear in any scope.
ident := p.parseIdent()
if ident.Name == "_" && p.mode&DeclarationErrors != 0 {
p.error(p.pos, "invalid package name _")
}
// 虽然我们一般不显式写这个分号,但是语法上是存在的,go语言是通过
// 在词法分析阶段根据token等条件自动插入分号实现的
p.expectSemi()
接下来parseFile的内容就是一系列声明构成的了,其会首先检查是否有包引入声明,并将这些包引入声明编译。像import,const,type和var声明都可以用括号一次性引入或声明多个包或变量也可以只引入或声明一个而不用括号,这个编译方法是通过func (p *parser) parseGenDecl(keyword token.Token, f parseSpecFunction) *ast.GenDecl方法实现的其中第二个参数f是一个结构体的方法,这是使用了go语言函数为一等公民的特性实现了类似策略模式的效果,并且由于f是一个结构体的方法意味着对象的结构体方法作为参数传入也会保留对象状态。import声明是在其他声明之前进行的,所以在代码中会首先处理这部分。
// import decls
for p.tok == token.IMPORT {
// 第一个参数代表token检查
decls = append(decls, p.parseGenDecl(token.IMPORT, p.parseImportSpec))
}
之后就是通过剩余声明的首个token类型来判断使用什么方法进行声明的处理。
// rest of package body
for p.tok != token.EOF {
decls = append(decls, p.parseDecl(declStart))
}
func (p *parser) parseDecl(sync map[token.Token]bool) ast.Decl {
if p.trace {
defer un(trace(p, "Declaration"))
}
var f parseSpecFunction
// 声明的首个token类型来判断使用什么方法进行声明的处理
switch p.tok {
case token.CONST, token.VAR:
f = p.parseValueSpec
case token.TYPE:
f = p.parseTypeSpec
case token.FUNC:
// 对于函数的声明很显然与其他几种的列表式声明很不同
// 所以是单独处理的
return p.parseFuncDecl()
default:
pos := p.pos
p.errorExpected(pos, "declaration")
p.advance(sync)
return &ast.BadDecl{From: pos, To: p.pos}
}
// 所以是统一处理其他可以列表式声明的声明
return p.parseGenDecl(p.tok, f)
}
接下来先看ast包中的抽象语法树数据结构,他们在ast.go中定义,ast在数据结构上有三个比较基本的接口类型,分别为类型和表达式节点、语句节点和声明节点,他们都继承了接口Node。
// All node types implement the Node interface.
type Node interface {
Pos() token.Pos // position of first character belonging to the node
End() token.Pos // position of first character immediately after the node
}
// 类型和表达式节点 All expression nodes implement the Expr interface.
type Expr interface {
Node
exprNode()
}
// 语句节点 All statement nodes implement the Stmt interface.
type Stmt interface {
Node
stmtNode()
}
// 声明节点 All declaration nodes implement the Decl interface.
type Decl interface {
Node
declNode()
}
接下来ast.go中定义了相应的Expr、Stmt和Decl的结构体,他们有着一定的组合关系来最终组成抽象语法树。下面以函数声明的结构为例。
// 函数声明结构体
FuncDecl struct {
Doc *CommentGroup // 头部注释,由于doc
Recv *FieldList // 如果有内容就是相应结构体的方法,否则为普通函数
Name *Ident // 函数名
Type *FuncType // 函数签名,即函数的类型参数,返回值类型和参数类型
Body *BlockStmt // 代表函数体的一个块语句
}
// 函数类型表达式
FuncType struct {
Func token.Pos // func关键字在源码中的位置
TypeParams *FieldList // 用于泛型的类型参数列表
Params *FieldList // 函数参数列表,空代表无参
Results *FieldList // 函数返回值,空代表无返回
}
// 由括号、方括号或花括号括起来的内容
type FieldList struct {
Opening token.Pos // 左括位置
List []*Field // 内容列表
Closing token.Pos // 右括位置
}
// 结构体类型中的属性部分,接口类型中的方法列表,
// 或是函数等签名中的参数/返回值
// 当是一个不需要名字的参数(只有类型)或为结构体属性嵌入时Field.Names为空
type Field struct {
Doc *CommentGroup // 头部注释
Names []*Ident // field/method/(type) parameter names; or nil
Type Expr // field/method/parameter type; or nil
Tag *BasicLit // field tag; or nil
Comment *CommentGroup // line comments; or nil
}
// 花括号括起的语句列表
// 语句可以是声明语句可以是赋值语句,循环、条件语句等
BlockStmt struct {
Lbrace token.Pos // 起始位置
List []Stmt
Rbrace token.Pos // 结束位置
}
下一次进行正式递归下降处理的简述。