V编译器源代码
最后更新于
V已实现了自举,所有编译器和标准库都是V开发的,在实现自举之前,使用golang作为早期编译器的开发。
V编译器基于AST(抽象语法树)的方式来编译,源代码先生成AST,再生成目标代码(c/js/native/interpreter),是目前各语言编译器的主流方式,好处很多:
编译器代码更清晰,更好维护
编译器再增加新东西,速度会更快
AST可以更快速地生成其他目标代码
AST可以给开发环境以及编译工具使用,更快开发对应的插件和工具
同时V编译器代码也是V标准库vlib的一部分,位于vlib/v目录中,这样的设计可以很方便地调用V编译器本身的各种功能模块,开发各种编译工具链。
子目录 | 说明 |
---|---|
子目录 | 说明 |
---|---|
V命令行的入口文件是v/cmd/v/v.v
V命令行负责处理命令参数,创建编译器参数对象(pref.Preferences)
V命令行根据参数调用builder.compiler()开始编译,或调用tools目录中的各种工具,比如vfmt代码格式化工具
V编译器对象根据编译参数,分析得到所有需要编译的源文件数组[ ]os.File,源文件列表也包括了vlib/builtin中的内置源文件
通过调用builder.compile()方法,开始编译,首先创建代码生成器对象(builder.Builder)
通过调用代码生成器对象的b.build_c()方法,把源代码文件数组[]File中的所有V源代码文件生成C源文件
一个源代码文件对应一个语法分析器,一个语法分析器对应一个词法扫描器
代码生成器对象负责创建语法分析器对象(parser.Parser),启动语法分析
语法分析器负责创建词法扫描器对象(scanner.Scanner),启动词法扫描
语法分析和词法扫描实际上是同步进行,同步完成的.语法分析器对[ ]os.File进行遍历,以词法单元(token)为基本单位,进行语法分析,词法单元由词法扫描器负责扫描,识别,提供给语法分析
语法分析遍历[ ]os.File的所有源文件后,生成对应的文件语法树对象数组[ ]ast.File
语法分析器分析完[ ]File源文件后,接着对所有导入的依赖,再次进行分析,将分析结果追加到文件语法树对象数组([ ]ast.File)中
语法分析器对象分析完成后,输出[ ]ast.File给代码生成器对象,由代码生成器对象,调用指定平台的代码生成对象(gen.Gen/gen.JsGen/gen.x64Gen)来生成目标代码,目前主要是调用gen.Gen来生成C源文件
最后调用builder.cc方法,调用编译器参数指定的C编译器,将C源文件编译生成可执行文件,完成整个编译过程
简单总结整个编译过程就是: [ ]os.File => [ ]ast.File => 目标代码(c/x64/js) => 可执行文件
V实现了自举,整个V编译器都是由V语言开发的,编译器加上标准库的代码量大约为15W多行代码,持续增加中.
使用scc代码统计工具的统计数据如下:
V专门开发了一个工具,用来监控每一次V代码库提交,对编译速度的影响。
详细记录可以参考:https://fast.vlang.io/
以下类图是V编译器中主要的类和枚举:
主要的调用关系是:cmd => builder.Builder => parser.Parser => scanner.Scanner
类和枚举说明:
pref.Preferences
V编译器选项类,保存着所有的编译器选项和参数
参数太多,不一一展开,列举几个主要的:
builder.Builder
代码生成器类,由V编译器负责创建
parser.Parser
语法分析器,由代码生成器负责创建
ast.table
符号表,保存着语法分析后,得到的所有变量,函数,类型等
scanner.Scanner
词法扫描器,由语法分析器负责创建,对源文件进行逐个字节进行扫描,识别出token
token.Token
词法单元类,表示词法扫描器识别出来的一个token
token.Kind
词法单元种类枚举,一共有139个可以被扫描器识别的词法单元
ast.Fn
函数类型
ast.Var
变量和常量类型
ast.Field
结构体字段类型
ast.Scope
作用域
ast.TypeSymbol
类型,描述类型的类型
ast.Type
类型引用,i32的类型别名,表示每一个类型在符号表中的唯一ID
这个i32的ID有进行位移处理,前16位保存着类型的ID信息,后16保存着类型的指针引用层级信息
原来是设计为一个TypeRef结构体的,因为在符号表等很多地方都被引用,所以把ID和指针引用层级信息合并成一个int32,性能更好,占用内存更小.
ast.AccessMod
结构体字段的访问控制枚举
ast.Kind
类型的种类,包含了V语言中的所有类型种类,包括基本类型,内置类型,结构体类型等其他种类,对类型进行分类以后,就可以针对不同的种类进行不同的语法分析,语法检查
ast.TypeInfo
类型信息类,该类型是一个联合类型,作为几种特殊类型的额外信息
ast.Struct
结构体类型,TypeInfo联合类型的其中一种类型
作为类型信息类的其中一种类型,要描述一个完整的结构体类型所包含的信息,就是TypeSymbol中的基本字段,再加上这个结构体中的字段
ast.Array
数组类型,TypeInfo联合类型的其中一种类型
ast.ArrayFixed
固定大小数组类型,TypeInfo联合类型的其中一种类型
ast.Map
字典类型,TypeInfo联合类型的其中一种类型
ast.MultiReturn
多返回值类型,TypeInfo联合类型的其中一种类型
V语言的多返回值实际上是返回一个结构体,动态生成的结构体,就是用这个来定义
ast.Alias
类型别名,TypeInfo联合类型的其中一种类型
check.Checker
代码检查器,用于检查各种语法的合法性
gen.Gen
C源代码生成器
gen.JsGen
js源代码生成器
gen.x64.Gen
x64机器码生成器
fmt.Fmt
代码格式化器,根据文件语法树,生成格式工整的代码,然后再覆盖回未格式化的代码
源代码文件经过词法扫描,语法分析后的输出就是文件语法树对象数组([ ]ast.File)
以下类图就是文件语法树中每一个节点对应的类型
类型中有2个基本的类型:Stmt语句类型和Expr表达式类型,这两个类型是联合类型,联合类型的含义就是这种类型可以是图中箭头关联的子类型的其中一种,有点像继承关系中基类的作用,但又不是
而且Expr表达式类型通过ExprStmt表达式语句类型的关联,也是Stmt语句类型的子类,这样一来,所有的语法树节点都是Stmt语句类型的子类
文件语法树中以ast.File为根节点,节点包含子节点,形成一棵完整的语法树
联合类型Stmt和Expr,对应的V源代码:
主要类别说明:
ast.File
文件语法树类,保存了一个v源文件的整棵语法树
ast.Scope
作用域节点,整个ast.File里也是保存了一棵作用域树,以ast.File的scope为根节点
作用域示意图:
其他语法树节点,不详细展开,参考类图
可以使用vast这个工具,从V源文件生成AST json文件,这样就可以直观地看到AST:
以下仅是举例,生成的语法树JSON方式表示如下:
以下是扫描器扫描识别token的逻辑:
s.text: 字符串类型,就是源代码的字符串表示
s.pos: 扫描的当前字节位置
s.scan()函数: 每调用一次,就接着s.pos当前位置,继续往下扫描,识别到一个token后返回
以下是扫描器碰到每一个字节时,进行扫描和识别的顺序:
###语法分析 parser
语法分析器从p.parse_file()或者p.parse_files()开始启动
调用p.read_first_token()进行初始化后,p.tok和p.peek_tok就位,从第一个token开始分析
p.tok: 当前token
p.peek_tok: 下一个token
p.next(): 每调用一次,就调用一次扫描器的scan(),返回一个token,并将p.tok向后推进一个
p.check(token):
检查p.tok是否为指定的token
如果是就调用p.next(),将p.tok向后推进一个
如果不是就报语法错误
p.check_name() string:
检查p.tok是否为.name(标识符)的token
如果是就返回tok.lit(具体的标识名),并调用p.next(),将p.tok向后推进一个
如果不是就报语法错误
以下是语法分析的分析顺序:
如果还存在以上都不是的顶级节点, 就报不合法的顶级节点错误
以下为顶级节点中包含的各种下级token的语法分析:
代码格式化的基本处理逻辑是: V代码(未格式化) => AST => V代码(格式化)
可以对文件或目录(包括子目录)进行代码格式化
格式化选项:
-w 最常用的选项,将格式化后的代码写入到源文件中,如果没有-w,默认只是输出到stdout
-l 列出所有需要格式化的文件名
-c 检查文件是否已经格式化过,如果文件未格式化,则输出文件路径,并且命令行退出码为2
-verify 跟-c基本一样,检查文件是否已经格式化过,速度慢一些,更准确一些,ci一般都用这个
-diff 调用系统默认的文件比较工具diff,比较文件格式化和未格式化之间的差异
-debug 输出格式化文件中所有的AST结构体,没啥用处
涉及的环境变量:
VDIFF_TOOL
如果指定了该环境变量,v fmt -diff不再使用系统默认的比较工具diff,而是使用指定的比较工具。
VDIFF_OPTIONS
传递给VDIFF_TOOL比较工具的选项。
子目录 | 说明 |
---|---|
字段 | 说明 |
---|---|
字段/方法 | 说明 |
---|---|
字段/方法 | 说明 |
---|---|
字段/方法 | 说明 |
---|---|
字段/方法 | 说明 |
---|---|
字段/方法 | 说明 |
---|---|
枚举值 | 说明 |
---|---|
字段/方法 | 说明 |
---|---|
字段/方法 | 说明 |
---|---|
字段/方法 | 说明 |
---|---|
字段/方法 | 说明 |
---|---|
字段/方法 | 说明 |
---|---|
字段/方法 | 说明 |
---|---|
字段/方法 | 说明 |
---|---|
字段/方法 | 说明 |
---|---|
字段/方法 | 说明 |
---|---|
字段/方法 | 说明 |
---|---|
字段/方法 | 说明 |
---|---|
字段/方法 | 说明 |
---|---|
字段/方法 | 说明 |
---|---|
字段/方法 | 说明 |
---|---|
字段/方法 | 说明 |
---|---|
字段/方法 | 说明 |
---|---|
字段/方法 | 说明 |
---|---|
字段/方法 | 说明 |
---|---|
字段/方法 | 说明 |
---|---|
字段/方法 | 说明 |
---|---|
当前字符 | 识别内容 | 继续识别 |
---|---|---|
当前token | 下个token | 识别内容(ast中的节点类型) | 说明 |
---|---|---|---|
当前token | 下个token | 识别内容(ast中的节点类型) | 说明 |
---|---|---|---|
cmd/v
V编译器命令行,入口文件v.v,编译成V可执行文件
cmd/v/help
V编译器命令行的帮助文本
cmd/tools
各种工具类,如vup/vfmt/vpm/vrepl/vtest/vcreate等
ast
AST抽象语法树和全局符号表
token
词法单元相关
pref
编译选项/参数相关
scanner
词法分析/扫描器相关
parser
语法分析/解析器相关
checker
语法检查器相关
eval
解释器相关
depgraph
模块依赖相关
builder
代码生成器相关
gen
生成具体平台代码相关,如生成C/js/native
fmt
代码格式化相关
doc
代码文档生成相关
tests
编译器测试代码
errors
编译器报错和警告
vmod
解析v.mod文件
cflag
解析cflag标识
vcache
编译器缓存相关
vet
小工具,用于分析项目代码存在的错误
pkgconfig
小工具,用于配置C代码的编译标记(C flags)
util
编译器各种辅助类
embed_file
将各种文件嵌入到可执行文件中,形成单一可执行文件
callgraph
显示函数调用图,生成Graphviz的dot格式
preludes
preludes_js
live
markused
transformer
cJSON
fontstash
ios
ios依赖库
libgc
mssql
mssql头文件库
sokol
picoev
picohttpparser
stb_image
stdatomic
C的原子操作库
tcc
vschannel
zip
C的zip压缩库
build_mode
编译模式
is_script
是否vscript脚本
is_prof
是否生产模式编译
is_run
是否v run执行
....
pref
对编译选项对象的引用
tables
对符号表对象的引用
checker
对代码检查器对象的引用
parsed_files
语法分析器生成的文件语法树对象数组
module_search_paths
模块搜索路径
out_name_c
保存需要编译生成的C文件名
out_name_js
保存需要编译生成的js文件名
...
compile()
负责启动编译
build_c()
负责编译生成C源代码
build_js()
负责编译生成js源代码
build_x64()
负责编译生成x64机器码
cc()
将C源代码编译生成可执行文件
...
scanner
保存对应的词法扫描器引用
file_name
保存对应的源文件
tables
对符号表对象的引用
pref
对编译选项对象的引用
其他字段都是语法分析中的过程变量:
pos
当前token
peek_tok
下一个token
scope &ast.Scope
当前作用域
...
parse_file(path string,table &table.Table)
对一个源文件进行语法分析
parse_files(paths []string)
对一组源文件进行语法分析
...
types []TypeSymbol
所有类型
local_vars
所有类型索引
fns map[string]Fn
所有函数和方法
consts map[string]Var
所有常量
imports []string
所有导入模块
modules []string
所有模块
register_const()
注册常量到符号表
register_global()
注册全局变量到符号表
register_var()
注册变量到符号表
register_fn()
注册函数到符号表
register_method()
注册方法到符号表
register_type()
注册类型到符号表
...
file_path
保存扫描的当前源文件路径
text
保存文件所有内容的字符串
其他字段全是扫描的过程变量:
pos
扫描到的当前字节位置
line_nr
扫描到的行
...
scan()
启动一次扫描,返回一个token,由语法分析器调用
scan_res()
返回一次扫描结果
...
根据指定的源文件,创建扫描器
...
kind
token的种类
lit
string 字面量值,只有种类为name,number,str,str_inter,chartoken时才会有值
line_nr
行位置
pos
列位置
position()
返回当前token的位置,行位置和列位置
eof
表示文件结束
name
标识符,tokon的lit字段有具体值
number
数字,tokon的lit字段有具体值
str
字符串,tokon的lit字段有具体值
chartoken
单字符,tokon的lit字段有具体值
plus
+加
minus
-减
mul
*乘
div
/除
mod
%余
xor
^异或
bit_not
~位取反
pipe
|管道符
hash
# 用于C宏
amp
&位且,也用于变量取地址
inc
++递增
dec
--递减
and
&&逻辑且
logical_or
||逻辑或
not
!逻辑非
dot
.点运算符
dotdot
..运算符,用于数组区间
ellipsis
...展开符,用于函数不确定参数
comma
,逗号
semicolon
;分号
colon
:冒号,用于字典初始化,结构体初始化,结构体字段访问控制,goto标签
arrow
=>箭头
assign
=变量赋值/分配
decl_assign
:=变量声明
plus_assign
+=加等于
minus_assign
-=减等于
mult_assign
*=乘等于
div_assign
/=除等于
xor_assign
^=异或等于
mod_assign
%=余等于
or_assign
|=位或等于
and_assign
&=位且等于
righ_shift_assign
>>=位右移等于
left_shift_assign
<<=位左移等于
lcbr
{左大括号
rcbr
}右大括号
lpar
(左小括号
rpar
)右小括号
lsbr
[左中括号
rsbr
]右中括号
eq
==相等
ne
≠不等于
gt
>大于
lt
<小于
ge
>=大于等于
le
<=小于等于
question
?问号
left_shift
<<位左移
right_shift
>>位右移
line_comment
//单行注释
mline_comment
/*多行注释开头
nl
NLL空值字符
dollar
$美金符号
str_dollar
$字符串内的美金符号
keyword_beg
表示在keyword_beg和keyword_end之间的都是关键字,本身无意义
key_assert
关键字assert
key_struct
关键字struct
key_if
关键字if
key_else
关键字else
key_asm
关键字asm
key_return
关键字return
key_module
关键字module
key_sizeof
关键字sizeof
key_go
关键字go
key_goto
关键字goto
key_const
关键字const
key_var
关键字var
key_type
关键字type
key_for
关键字for
key_switch
关键字switch
key_fn
关键字fn
key_true
关键字true
key_false
关键字false
key_continue
关键字continue
key_break
关键字break
key_import
关键字import
key_embed
关键字embed,未使用
key_unsafe
关键字unsafe
key_typeof
关键字typeof
key_enum
关键字enum
key_interface
关键字interface
key_pub
关键字pub
key_import_const
关键字import_const,用于导入C常量
key_in
关键字in
key_atomic
关键字atomic,未使用
key_orelse
关键字or
key_global
关键字__global,全局变量
key_union
关键字union
key_static
关键字static,用于C函数的static
key_as
关键字as
key_defer
关键字defer
key_match
关键字match
key_select
关键字select,用于db.select
key_none
关键字none
key_offsetof
关键字__offsetof,未使用
keyword_end
表示在keyword_beg和keyword_end之间的都是关键字,本身无意义
name string
函数名
args []Var
函数的参数
return_type Type
函数的返回类型
is_variadic bool
是不是可变参数函数
name string
变量名
idx int
变量在符号表中的索引
is_mut bool
是否可变
is_const bool
是否常量
is_global bool
是否全局变量
scope_level int
作用域级别
typ Type
变量类型
name string
字段名称
typ Type
字段类型
parent &Scope
父作用域
children []&Scope
子作用域
start_pos int
作用域开始位置
end_pos int
作用域结束位置
parent &TypeSymbol
类型的父类型
kind Kind
类型的种类
info TypeInfo
类型的额外信息类,只对6个类型有用
name string
类型的名字
private
模块私有,且只读
private_mut
模块私有,但可变
public
所有模块可访问,但只读
public_mut
所有模块可访问,且可变
global
全局字段,所有模块可访问,且可修改,不推荐使用
placeholder
占位种类
void
voidptr
通用指针类型
&char
C字符指针类型
&u8
字节指针
i8
i16
int
i64
u16
u32
u64
f32
f64
string
字符串
char
单字符
u8
bool
const_
常量
enum_
枚举
struct_
结构体
array
数组
array_fixed
固定大小数组
map
字典
multi_return
多返回值类型
sum_type
联合类型
alias
类型别名
unresolved
fields []Field
结构体的所有字段
elem_type Type
数组类型
nr_dims int
数组维度
elem_type Type
数组类型
nr_dims int
数组维度
size int
数组大小
key_type Type
键的类型
value_type Type
值的类型
name string
类型名字
types []Type
包含多返回值的类型数组
foo string
类型别名
table &table.Table
符号表的引用
file ast.File
要检查的文件语法树
nr_errors int
错误个数
errors []string
错误内容数组
check_files()
检查多个文件语法树的语法合法性
check()
检查单个文件语法树
stmt()
检查语句,递归检查
expr()
检查表达式,递归检查
error()
没有通过检查项,报错
check_struct_init
检查结构体初始化的合法性
check_assign_expr
检查变量赋值的合法性
call_expr
检查函数调用
check_method_call_expr
检查方法调用
return_stmt
检查返回语句
array_init
检查数组初始化
...
out strings.Builder
生成的C源代码主体,保存在这个字符串中,字符串生成器对象
definitions strings.Builder
生成的C源代码宏部分,保存在这个字符串中,字符串生成器对象
完整的C源代码由这两个部分组成:definitions+out
table &table.Table
符号表的引用
fn_decl &ast.FnDecl
过程变量,生成过程中的当前函数
tmp_count int
过程变量,用于生成临时变量的计数,每一个函数重置
cgen() string
生成所有[ ]ast.File的C源代码
stmts()
遍历stmts生成所有的C代码
stmt()
根据一个stmt语句,生成对应的C代码段
write() string
生成C代码段,不换行
writeln() string
生成C代码段,换行
out strings.Builder
生成的js源代码,保存在这个字符串中,字符串生成器对象
table &table.Table
符号表的引用
out_name string
生成的机器代码,保存在这个字符串中,字符串生成器对象
buf []u8
sect_header_name_pos int
offset i64
str_pos []i64
strings []string
file_size_pos i64
main_fn_addr i64
code_start_pos i64
fn_addr map[string]i64
gen()
生成x64代码
out strings.Builder
格式化后的代码字符串
table &table.Table
符号表的引用
indent int
格式化过程变量,记录当前代码缩进的等级
empty_line bool
格式化过程变量,记录当前行是否是空行,如果是,要先生成缩进
line_len int
格式化过程变量,记录当前行目前的长度,为了处理80换行
single_line_if bool
格式化过程变量,判断是否单行if,为了格式化成1行
cur_mod string
格式化过程变量,记录当前模块
fmt()
格式化代码某个文件
wrtie()
往out字符串写入代码
writeln()
往out字符串写入代码,并换行
stmts()
格式化多个语句
stmt()
格式化单个语句
path string
对应的v源文件路径
mod Module
模块节点
imports []Import
导入模块节点
stmts []Stmt
语句数组
scope &Scope
文件作用域
parent &Scope
父作用域
children []&Scope
子作用域数组
start_pos int
作用域开始位置
end_pos int
作用域结束位置
vars map[string]VarDecl
该作用域内,声明的变量字典
register_var()
注册声明的变量
find_var()
在作用域内查找已声明的变量
override_var()
覆盖变量
字符值为9/10/13/32
优先识别tab制表符/换行/回车/空格,跳过这些空格
字母/下划线
name名字
keyword关键字
数字/点加数字
number数字
0b 二进制数字
0x十六进制数字
0八进制数字
1-9/0.十进制数字
)
)
+
++
+=
+
-
--
-=
-
*
*=
*
^
^=
^
%
%=
%
?
?
'或"
string字符串
`
char单字符
(
(
)
)
[
[
]
]
{
{
字符串内递归识别
$
$
字符串内递归识别
}
}
字符串内递归识别
|
|=
||
,
,
@
FN
FILE
LINE
COLUMN
VHASH
@keyword
name名字
.
...
..
.
#
#!
#宏定义
>
>=
>>
>
0xE2
≠
≤
≥
<
<=
<<
<
=
==
=>
=
:
:=
:
;
;
!
!=
!
~
~
/
/=
//
/*
\0
.eof windows end of file
以上都判断完了
如果还有不能识别的,就是invalid character
s.pos≥s.text.len
.eof 就是文件结束end of file
module
Module
识别模块定义
import
[ ]Import
识别导入语句
以下开始
识别所有的
顶级节点
遍历识别后,形成[ ]stmts
pub
识别以下公共的顶级节点
const
ConstDecl
识别一组常量声明
fn
FnDecl
识别函数声明或方法声明
struct
StructDecl
识别结构体声明
union
识别C联合类型
interface
识别接口声明
enum
EnumDecl
识别枚举声明
type
TypeDecl
识别类型别名/联合类型
[
Attr
识别函数注解
__global
GlobalDecl
识别全局变量
const
ConstDecl
识别一组常量声明
fn
FnDecl
识别函数声明或方法声明
struct
StructDecl
识别结构体声明
$
CompIf
识别条件编译语句
#
HashStmt
识别C宏
type
TypeDecl
识别类型别名/联合类型
enum
EnumDecl
识别枚举声明
//
Comment
识别单行注释
[ ]stmts
StmtBlock代码块语句
通过p.stmt()的递归调用进行分析
mut
VarDecl
识别变量声明语句
for
ForCStmt/ForStmt/ForInStmt
识别3种for循环语句
return
Return
识别函数返回语句
$
CompIf
识别条件编译语句
continue
BranchStmt
识别continue语句
break
BranchStmt
识别break语句
unsafe
识别unsafe代码块语句(未实现)
defer
DeferStmt
识别defer代码块语句
goto
GotoStmt
识别goto代码块语句
.name
:
GotoLabel
识别为goto标签语句
.name
:
AssignStmt
识别为分配语句
=
Expr,识别表达式的值和类型
识别表达式,最复杂的
.name
继续识别出现名字的各种情况
.str
StringLiteral, table.string_type
识别字符串 x='abc'
.dot
EnumVal, table.int_type
识别枚举值 x=.blue
.chartoken
CharLiteral, table.u8_type
识别单字符 x=c
.key_true, .key_false
BoolLiteral,table.bool_type
识别布尔类型 x=true
.minus, .amp, .mul, .not, .bit_not
PrefixExpr
识别前缀表达式
.key_match
MatchExpr
识别match赋值语句
.number
IntegerLiteral/FloatLiteral
识别数值
.lpar
继续递归识别表达式
.key_if
IfExpr
识别if赋值语句
.lsbr
ArrayInit
识别数组初始化语句
.key_none
None, table.none_type
识别none类型
.key_sizeof
SizeOf,table.int_type
识别sizeof()函数
.lcbr
继续识别各种情况
如果以上都不是
报错:无效表达式