前言:
当我们使用编程语言编写代码时,我们都知道代码都是写给人看的,只是除了人能看外编译器可以编译和执行。而这些复杂的代码其实是一堆堆字符串,只是我们训练编译器拆分解读。
而编译器将一堆堆代码的字符串分组的过程,我们把这个过程称之为词法解析。
本文中工具:
lex:3.1
golang : 1.13.4 darwin/amd64
dlv: 1.3.2
编译过程一般分为 6 步:扫描、语法分析、语义分析、源代码优化、代码生成、目标代码优化。
下图来自《程序员的自我修养》
词法解析相关知识:
说到词法解析我们可以关注两个工具:
Lex(LEXical compiler)和yacc(Yet Another Compiler Compiler),Lex 是一种生成扫描器的工具。扫描器是一种识别文本中的词汇模式的程序。yacc是语法分析器。
一个简单的例子demo.lex :
%%
[\t ]+ /* 忽略空白 */;
学而思轻课 |
轻课 |
好未来 |
zhaoyu |
学而思 {printf("%s : 棒棒哒\n",yytext);}
[a-zA-Z]+ {printf("%s: 这个字母\n",yytext);}
.|\n {ECHO; /* 通常的默认状态 */}
%%
int main()
{
yylex();
return 0;
}
//必须要包含的函数
int yywrap()
{
return 1;
}
生成lex文件,会默认生成一个lex.yy.c的源码文件。用lex的语法去编写,需要用lex生成对应的源码文件,才可以编译执行。
lex demo.lex
编译文件
gcc lex.yy.c -o demo
图中例子比较简单,输入(“学而思”、“轻课”、“zhaoyu”、“好未来”或者是“学而思轻课”)关键词则显示 %s:棒棒哒 ,如果输入字母则则显示 %s:这是一个字母。输入其他的就原样输出。其实都是一堆正则去做匹配,匹配到对应的关键词。PHP源码中也是这样的规则。
IBM关于LEX比较不错的文章。
https://www.ibm.com/developerworks/cn/linux/sdk/lex/
从上面的示例其实我们可以简单的理解为,词法解析。就是对我们关键词做拆分处理,然而在编译过程中。词法解析会将我们的代码拆分成一个个token。
Golang相关词法解析:
我们一起来看看go的词法解析过程;
default.go
package main
func demo1() {
println("demo1")
}
func main() {
demo1()
println("abcd")
}
我们来一下一个断点,compile文件在你自己的$GOROOT/pkg/tool中
运行 complie
dlv exec /usr/local/Cellar/go/1.13.4/libexec/pkg/tool/darwin_amd64/compile default.go
我们下第一个断点
(dlv)b cmd/compile/internal/gc.parseFiles
(dlv)c
跟踪到图中,我们可以看到这就是读取源码文件过程;
再下一个断点,跟踪到断点
(dlv)b cmd/compile/internal/syntax.Parse
(dlv)c
我们可以看到parser结构
cmd/compile/internal/syntax/parser.go:17
type parser struct {
file *PosBase // 文件的信息(行、列)源位置
errh ErrorHandler
mode Mode
scanner
base *PosBase // 当前位置及地址
first error // 遇到的第一个错误
errcnt int // 遇到的错误数
pragma Pragma // pragma标志
fnest int // 函数嵌套级别(用于错误处理)
xnest int // 表达式嵌套级别(用于complit歧义解决)
indent []byte // 跟踪支持
}
cmd/compile/internal/syntax/scanner.go:30
type scanner struct {
source
mode uint
nlsemi bool // 如果设置为“\n”,则EOF转换为“;”
// 当前token , 调用后next()函数后变为下一个token
line, col uint
tok token
lit string // valid if tok is _Name, _Literal, or _Semi ("semicolon", "newline", or "EOF")
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
}
cmd/compile/internal/syntax/source.go:33
type source struct {
src io.Reader //os.Open的文件缓冲
errh func(line, pos uint, msg string) //错误提示
// 缓冲区数据
buf [4 << 10]byte //2进制100左移10位1000000000000,对应的就是4096
r0, r, w int // 以前/当前读写buf位置,不包括sentinel
line0, line uint // 之前/当前 行号
col0, col uint // 之前/当前 列 (从行开始的字节偏移量)
ioerr error // 挂起io错误
// 文字缓冲区
lit []byte // 文字前缀
suf int // 文字后缀; suf >= 0 意思是我们正在扫描一个文本
}
我们继续下跟,跟到cmd/compile/internal/syntax.(*scanner).next 中,next函数其实是不断的读取下一个字符。
看图中的打印,*cmd/compile/internal/syntax.scanner中的source中的buf其实就存储来我们的源代码;
ident字符串处理流程:
我们代码解析对应的token在src/cmd/compile/internal/syntax/tokens.go中。
RUN TESTING测试:
cd $GOROOT/src/cmd/compile/internal/syntax
添加一个代码:
然后修改scanner_test.go中的TestScanner的打开文件
执行命令:
#cd $GOROOT/src/cmd/compile/internal/syntax
#go test -run=TestScanner
TOKEN解析过程;
总结词法解析扫码过程:
有疑问加站长微信联系(非本文作者)