原文地址:mukulrathi.co.uk/create-your…
原文作者:mukulrathi.co.uk/
发布时间:2020年6月1日-9分钟阅读
系列。创建BOLT编译器
- 第1部分:为什么要写自己的编程语言?为什么要写自己的编程语言?
- 第2部分:那么如何架构一个编译器项目?
- 第3部分:使用OCamllex和Menhir编写一个Lexer和解析器。
- 第4部分:类型理论和实现类型检查器的易懂介绍。
- 第5部分:关于活泼度和别名数据流分析的教程。
- 第6部分:Desugaring--将我们的高级语言简单化!
- 第7部分:OCaml和C++的Protobuf教程。
- 第8部分: 编程语言创建者的LLVM完全指南。
- 第9部分:实现并发和我们的运行库
- 第10部分:Bolt中的继承和方法重写。
- 第11部分:通用性--为Bolt添加多态性。
即将推出
什么是类型?
我们在编译器系列的第一部分已经简单地讨论过这个问题,然而我们再来看看这个问题。
类型是根据我们希望它们具有的行为 对程序值进行分组的一种方式。我们在日常生活中也有自己的 "类型"。如:把人分组为大人和小孩。
将值分组意味着编译器没有那么多的情况要处理。这意味着我们不需要看实际的值,我们只需要根据它所在的组(它的类型)来推理它。
例如,0,1,2,3......等所有其他整数的值都被赋予了int的类型。int类型的值可以参与算术运算,比如对它们进行乘法,但我们不能对它们进行连接(像字符串一样)。看到类型(int与字符串)是如何定义值的行为(我们可以用它们做什么)的吗?
在我们的自定义类型中就更清楚了。比如当你定义了一个自定义的Animal类,有eat()和sleep()方法时,你就会说Animal类型的对象有吃和睡的行为。但是我们不能把两个Animal对象互相分割--这不是我们允许的行为。狗/猫是什么意思呢?
类型检查器的作用是防止这种无意义的行为发生。这就是我们今天要建立的东西。
所有实用的语言都有某种形式的类型检查。像Rust、Java或Haskell这样的静态类型语言在编译时检查类型。像JS和Python这样的动态类型语言仍然有类型--值在运行时被标记为类型,并且它们在执行时检查类型。如果你试图运行5 /"Hello",它实际上不会运行代码,JS/Python会看到 "Hello有类型字符串,并将抛出一个运行时错误,而不是执行它。
现在,这一切都让人感觉有点手忙脚乱。让我们把 "防止无厘头行为 "正式化。我们想要一个规则列表来检查程序,然后如果它通过了这些规则,我们就知道我们的程序是安全的。
这个规则的集合叫做类型系统。
类型系统
事情是这样的:类型定义了安全行为。因此,如果我们能给一个表达式一个类型,我们就可以使用它的类型来确保它的使用方式是正确的。因此,类型系统的规则(称为类型判断)试图将类型tt分配给一个表达式ee。
我们从定义看似明显的事实开始,比如true是bool类型。在我们的类型系统中,规则写成如下。
⊢ true : bool
不要被数学符号吓到。⊢的意思是 "it follows that"。您可以将其理解为 "it follows that",即表达式true的类型为bool。一般来说,⊢ e : t可以理解为 "it follows that expression e has type t"。
下面是几个比较明显的事实。
⊢ false : bool
⊢ n : int for any integer n.
类型环境
问题是,看一个表达式本身并不总是足以让我们对它进行类型检查。一个变量x的类型是什么?我们应该用什么来代替下面的 ?
⊢ x : ?
好吧,这取决于它被定义时被赋予的类型。所以在类型检查时,我们需要跟踪变量的类型,因为它们是被定义的。我们称之为类型环境,在我们的规则中表示为Γ。把 Γ看作是一个将变量映射到其类型的查找函数。
我们更新我们的类型化规则以包含 Γ。
Γ ⊢ x : t.
这里回到我们的类型规则。
Γ ⊢ x : ?
如果 Γ(x) = t,那么我们可以说。
Γ ⊢ x : t.
在我们的类型规则中,我们将这两个语句堆叠起来,给出一个变量的复合类型规则。这是一个推理规则的例子。
\cfrac{\Gamma(x) = t} {\Gamma \vdash x :\ t}
推断规则
这种奇特的叠加 "分数 "是代表演绎推理的一种方式(就像福尔摩斯!)。如果上面的所有东西都成立,那么我们推断下面也成立。
这里还有一条规则。这就是说,如果我们知道e1和e2是int,那么如果我们把它们加在一起,结果也是int。
\cfrac{\Gamma \vdash e_1 : int\ \ \ \ \ \ \Gamma \vdash e_2 : int } {\Gamma \vdash e_1 + e_2 :\ int}
使用这些推理规则,我们可以将我们关于程序不同部分的证据堆叠在一起,对整个程序进行推理。这就是我们如何建立我们的类型系统--我们堆叠我们的规则。
让我们结合到目前为止所学到的知识来对一个小表达式进行类型检查:x + 1,其中我们的环境\GammaΓ告诉我们x是一个int。
\cfrac{ \cfrac{\cfrac{}{\Gamma(x) = int}} {\Gamma \vdash x :\ int} \ \ \ \ \ \ \cfrac{} {\Gamma \vdash 1 :\ int} } {\Gamma \vdash x + 1 :\ int}
现在如果发现x是一个字符串,那么\Gamma(x) = intΓ(x)=int不成立,所以它下面堆叠的所有规则都不成立。我们的类型检查器会提出一个错误,因为类型不匹配。
公理
这里的惯例是在事实(公理)上面用一条线表示。你可以把它们看作是 "基本情况"。
\cfrac{} {\Gamma \vdash 1 :\ int}
因为他们上面什么都没有,技术上上面的一切都真实。所以,底部始终保持不变。
如果你回过头来看我们的例子,把它倒过来,它看起来有点像一棵树。它也列出了我们的推理,所以可以作为一个证明--你可以按照步骤来做。为什么x+1是一个int?那么x和1是ints吗?为什么x是一个int?因为......你懂的。所以我们在这里构建的叫做证明树。
好吧,让我们看看另一条规则。一个if -else块怎么样?
if (something) {
do_one_thing
} else {
do_something_else
}
好吧,something必须有类型bool,因为它是一个条件。那么分支呢?我们需要给这个表达式一个整体类型,但是因为我们是静态类型检查,我们不知道哪个分支会在运行时执行。因此,我们需要两个分支都返回相同的类型(称之为tt),所以无论我们执行哪个分支,表达式都具有相同的类型。
\cfrac{\Gamma \vdash e_1 : bool\ \ \ \ \ \ \Gamma \vdash e_2 : t \ \ \ \ \ \ \Gamma \vdash e_3 : t } {\Gamma \vdash \mathbf{if}\ e_1\ \{ e_2 \}\ \mathbf{else}\ \{ e_3 \} :\ t}
JavaScript和Python会允许每个分支有不同的类型,因为它们在运行时进行类型检查(动态类型化),所以它们知道选择了哪个分支。
类型整体方案
如果我把所有的规则都列举出来,这篇文章最终会变得太长,但你可以通过每一种情况来解决。我们看一下我们在上一篇文章中定义的语法,然后写出相应的规则。
我们要证明程序的类型良好(你可以给它分配一个类型),以证明它是安全的。所以我们本质上想要一个证明树,证明它有一些类型tt(我们不关心是哪一个)。
{} ⊢program: t
这里只少了一件事。最初 Γ = {} - 它是空的,因为我们没有定义任何变量。我们如何向 Γ 添加变量呢?
请记住,我们在程序中声明变量时,就会将它们添加到 \GammaΓ 中。语法是一个let声明。所以说我们有以下程序。
// we have some gamma
let x = e1
// update gamma with x's type
// continue type-checking
e2
我们按照程序执行的顺序进行类型检查。
- 我们得到表达式e1的类型,称为t1
- 然后,我们用新的映射
x : t1扩展环境Γ。 - 我们使用这个扩展环境(我们将其写成
Γ,x :t1)进行类型检查e2并赋予其类型t2
因此,整个类型规则看起来像这样。
\cfrac{\Gamma \vdash e_1 : t_1 \ \ \ \ \ \Gamma, x: t_1 \vdash e_2 : t_2} {\Gamma \vdash \mathbf{let}\ x = e_1 ;\ e_2 : t_2 }
类型检查与类型推断
我们同样可以把它写成let x : t = e--程序员用类型t注释x,例如let x : int = 1 + 2。
这导致了不同的类型算法--在第一种情况下,编译器推断1+2具有int类型,而在第二种情况下,编译器必须检查1+2是否具有int类型(因为我们指定了x的int类型)。
正如你所想象的那样,类型推断意味着程序员需要写更少的类型注释,但对于编译器来说,它要复杂得多--这就像从头开始填充数独与检查数独解是否有效一样。OCaml和Haskell是内置类型推理的语言的例子。
在实践中,大多数静态类型的语言确实需要一些类型注释,但可以推断一些类型(例如C++中的auto关键字)。这就像完成一个部分解开的数独谜题一样,要简单得多。
对于Bolt,我们要在函数或方法定义中推断类型,但要求程序员对参数和返回类型进行注释。这是一个很好的中间地带。
function int something(int x, bool y){
let z = ... // z's type is inferred
...
}
好了,理论说的够多了,让我们来实现这个类型检查器吧!
实现类型检查器
只要把代码给我就可以了
你需要克隆Bolt仓库。
git clone https://github.com/mukul-rathi/bolt
Bolt仓库主分支包含了一个更复杂的类型检查器,支持继承、函数/方法重载和泛型。这些主题中的每一个都将在后面的系列文章中得到自己的特别文章。
因此,你会想看看简单编译器教程版本。你可以按照以下步骤进行。
git checkout simple-compiler-tutorial
这包含了Bolt在早期开发过程中的脱胎换骨版本。(在线查看)
我们关心的文件夹是 src/frontend/typing。
关于OCaml语法的说明
在本教程中,我们将使用OCaml语法。如果你不熟悉这个,主要的要点是我们将。
a) 模式匹配每一个案例(就像其他语言中的switch语句)。这里我们有一个变量x,我们根据它的每一种情况A、B做不同的事情
match x with
| A -> something
| B -> something_else
b) 使用一个结果单项:本质上,它有两个值。Ok something和Error。我们使用>>=操作符对每一个操作进行排序--在本教程中,你不需要了解任何关于单项式的知识,只需认为这与使用;s的普通表达式排序是一样的,只是我们使用另一个操作符来表示前面的表达式可能会引起错误。
Bolt中的类型
Bolt有四种主要类型:int、bool、void和用户自定义类。我们用OCaml中的变体类型type_expr来表示这四个选项。
type type_expr = TEInt | TEClass of Class_name.t | TEVoid | TEBool
用类型注释我们的AST
让我们回顾一下本系列上一部分的抽象语法树。
type identifier =
| Variable of Var_name.t (* x *)
| ObjField of Var_name.t * Field_name.t (* x.f *)
type expr =
| Integer of loc * int (* 1, 2 *)
| Boolean of loc * bool (* true*)
| Identifier of loc * identifier
| Constructor of loc * Class_name.t * constructor_arg list (* new Class(args)*)
| Let of loc * type_expr option * Var_name.t * expr
(** let x = e (optional type annotation) *)
| Assign of loc * identifier * expr
| If of loc * expr * block_expr * block_expr
(** If ___ then ___ else ___ *)
...
and block_expr = Block of loc * expr list
这个AST用loc注释每个表达式--表达式的行和位置。在我们的类型检查阶段,我们将检查每个可能的表达式的类型。我们将希望通过直接注释AST来存储我们的结果,这样下一个编译器阶段就可以通过查看AST来查看类型。
这个AST得到的名字很有想象力,叫typed_ast。
type identifier =
| Variable of type_expr * Var_name.t
| ObjField of Class_name.t * Var_name.t * type_expr * Field_name.t
(** class of the object, type of field *)
type expr =
| Integer of loc * int (** no need to annotate as obviously TEInt *)
| Boolean of loc * bool (** no need to annotate as obviously TEBool *)
| Identifier of loc * identifier (** Type info associated with identifier *)
| Constructor of loc * type_expr * Class_name.t * constructor_arg list
| Let of loc * type_expr * Var_name.t * expr
| Assign of loc * type_expr * identifier * expr
| If of loc * type_expr * expr * block_expr * block_expr
(** the If-else type is that of the branch exprs *)
...
and block_expr =
| Block of loc * type_expr * expr list (** type is of the final expr in block *)
我们不会注释明显的类型,比如对于Integer和Boolean,但是对于其他表达式,我们会注释整体表达式的类型,比如if-else语句返回的类型。
在注释AST时,一个很好的经验法则是,下一阶段需要被告知程序的什么,它无法从程序的良好类型中猜到?对于一个if-else语句,如果它是类型良好的,那么if条件表达式显然是类型为bool的,但是我们需要被告知分支的类型。
类型环境
回顾一下,我们使用我们的类型环境\GammaΓ来查找变量的类型。我们可以将其存储为绑定(变量,类型)对的列表。
type_env.ml 还包含了一个我们在类型检查阶段会用到的辅助函数的 "环境"。这些大多是无趣的getter方法,你可以在repo中查看。
type type_binding = Var_name.t * type_expr
type type_env = type_binding list
(** A bunch of getter methods used in type-checking the core language *)
val get_var_type : Var_name.t -> type_env -> loc -> type_expr Or_error.t
...
它还包含了几个函数,用于检查我们可以分配给一个标识符(它不是const或特殊标识符this),以及检查我们在同一作用域中没有重复的变量声明(影子)。这些同样在概念上是直接的,但只是为了覆盖边缘情况而必须的。
例如
let check_identifier_assignable class_defns id env loc =
let open Result in
match id with
| Parsed_ast.Variable x ->
if x = Var_name.of_string "this" then
Error
(Error.of_string
(Fmt.str "%s Type error - Assigning expr to 'this'.@." (string_of_loc loc)))
else Ok ()
| Parsed_ast.ObjField (obj_name, field_name) ->
get_obj_class_defn obj_name env class_defns loc
>>= fun class_defn ->
get_class_field field_name class_defn loc
>>= fun (TField (modifier, _, _, _)) ->
if modifier = MConst then
Error
(Error.of_string
(Fmt.str "%s Type error - Assigning expr to a const field.@."
(string_of_loc loc)))
else Ok ()
输入表达式
这。这才是实施的关键。所以如果你一直在浏览帖子,这里你应该注意的地方。
我们需要什么来对一个表达式进行类型检查?
我们需要类定义和函数定义,以备我们需要查询字段的类型和函数/方法类型签名。我们还需要表达式本身,以及我们用来进行类型检查的类型环境。
我们要返回什么?类型化的表达式,以及它的类型(我们单独返回类型以使递归调用更直接)。如果表达式不是类型良好,我们也会返回一个错误。
我们的函数类型签名正好抓住了这一点。
val type_expr :
Parsed_ast.class_defn list
-> Parsed_ast.function_defn list
-> Parsed_ast.expr
-> type_env
-> (Typed_ast.expr * type_expr) Or_error.t
在本系列的第二篇文章中,我讨论了为什么我们要使用OCaml。编译器的这一阶段是一个真正有价值的阶段。
例如要输入一个标识符,我们根据它是一个变量x还是一个对象字段x.f进行模式匹配。如果它是一个变量,那么我们就从环境中获取它的类型(我们将loc作为行+位置信息传递给错误信息)。如果它没有错误地返回一个var_type,那么我们就返回类型注释的变量。
如果它是一个对象字段x.f,那么我们需要在env中查找对象x的类型,并得到它对应的类定义。然后我们就可以在类定义中查找字段f的类型。然后我们用刚刚学到的两个类型信息来注释标识符:对象的类,和字段类型。
我们的代码正是这样做的,没有任何模板。
let type_identifier class_defns identifier env loc =
let open Result in
match identifier with
| Parsed_ast.Variable var_name ->
get_var_type var_name env loc
>>| fun var_type -> (Typed_ast.Variable (var_type, var_name), var_type)
| Parsed_ast.ObjField (var_name, field_name) ->
get_obj_class_defn var_name env class_defns loc
>>= fun (Parsed_ast.TClass (class_name, _, _, _) as class_defn) ->
get_class_field field_name class_defn loc
>>| fun (TField (_, field_type, _, _)) ->
(Typed_ast.ObjField (class_name, var_name, field_type, field_name), field_type)
对了,那我们来看看表达式。这又是一段干净的代码,读起来就像我们的输入判断。我们有expr1 binop expr2,其中expr1和expr2正在使用一些二进制操作符如+进行组合。
我们来提醒一下自己的规则。
\cfrac{\Gamma \vdash e_1 : int\ \ \ \ \ \ \Gamma \vdash e_2 : int } {\Gamma \vdash e_1 + e_2 :\ int}
这说明了什么?我们打字检查一下子表达式e1和e2首先。如果它们都是int,那么整体表达式就是一个int。
这里有一个主要的概念跳跃:对判断上面的每一个表达式进行类型检查,相当于对子表达式递归调用我们的类型检查函数。然后,我们使用我们的规则将这些类型检查判断的结果结合起来。
所以在这里,我们对子表达式expr1和expr2分别进行类型检查(type_with_defns只是让递归调用的时间更短)。
let rec type_expr class_defns function_defns (expr : Parsed_ast.expr) env =
let open Result in
let type_with_defns = type_expr class_defns function_defns in
let type_block_with_defns = type_block_expr class_defns function_defns in match expr with
| Parsed_ast.BinOp (loc, bin_op, expr1, expr2) -> (
type_with_defns expr1 env
>>= fun (typed_expr1, expr1_type) ->
type_with_defns expr2 env
>>= fun (typed_expr2, expr2_type) ->
递归调用,检查。
接下来,因为我们的代码处理的二进制运算符不止+,所以我们稍微概括一下我们的类型判断。不管是&& +还是>,操作数expr1和expr2必须有相同的类型。然后我们检查这个类型对于+ * / %算术运算符的情况是否是int。
如果是这样,那么所有的操作数都是OK的,我们返回TEInt作为操作数的类型。
if not (expr1_type = expr2_type) then
Error ... (* can't have different types *)
else
let type_mismatch_error expected_type actual_type = ...
match bin_op with
| BinOpPlus | BinOpMinus | BinOpMult | BinOpIntDiv | BinOpRem ->
if expr1_type = TEInt then
Ok (Typed_ast.BinOp (loc, TEInt, bin_op, typed_expr1, typed_expr2), TEInt)
else type_mismatch_error TEInt expr1_type
...
作为二进制运算符的另一个例子,让我们看看 < <= >> >=. 这些运算符接收整数并返回一个 bool,所以我们检查操作数 expr1 是否具有 TEInt 类型,我们返回 TEBool
| BinOpLessThan | BinOpLessThanEq | BinOpGreaterThan | BinOpGreaterThanEq ->
if expr1_type = TEInt then
Ok (Typed_ast.BinOp (loc, TEBool, bin_op, typed_expr1, typed_expr2), TEBool)
else type_mismatch_error TEInt expr1_type
好了,还在听我说吗?这篇文章越来越长了,我们就先看一下if-else语句和let表达式,然后你就可以看回帖中其他的代码了。
再来提醒一下我们自己对if-else的类型判断。
\cfrac{\Gamma \vdash e_1 : bool\ \ \ \ \ \ \Gamma \vdash e_2 : t \ \ \ \ \ \ \Gamma \vdash e_3 : t } {\Gamma \vdash \mathbf{if}\ e_1\ \{ e_2 \}\ \mathbf{else}\ \{ e_3 \} :\ t}
这就是推理规则上面的三个表达式。
这些表达式对应的是什么?跟我说说,递归调用!
| Parsed_ast.If (loc, cond_expr, then_expr, else_expr) -> (
type_with_defns cond_expr env
>>= fun (typed_cond_expr, cond_expr_type) ->
type_block_with_defns then_expr env
>>= fun (typed_then_expr, then_expr_type) ->
type_block_with_defns else_expr env
>>= fun (typed_else_expr, else_expr_type) ->
现在我们需要检查返回的类型是否符合我们的预期,即分支的类型相同,条件表达式的类型为TEBool。如果是这样,那就一切OK了,我们可以返回分支的类型。
if not (then_expr_type = else_expr_type) then
Error ...
else
match cond_expr_type with
| TEBool ->
Ok
( Typed_ast.If
(loc, then_expr_type, typed_cond_expr, typed_then_expr, typed_else_expr)
, then_expr_type )
| _ ->
Error ...
对了,这篇文章的最后一个表达式,let表达式,它是这样的。
\cfrac{\Gamma \vdash e_1 : bool\ \ \ \ \ \ \Gamma \vdash e_2 : t \ \ \ \ \ \ \Gamma \vdash e_3 : t } {\Gamma \vdash \mathbf{if}\ e_1\ \{ e_2 \}\ \mathbf{else}\ \{ e_3 \} :\ t}
同样,这需要两次递归调用,但请注意,第二次类型判断需要类型t1的第一种类型--我们需要传递一个扩展的类型环境(在我们对e1进行类型检查后)来解决这个问题。
我们还有一个额外的要求:我们希望我们的let表达式是块范围的。
if {
let x = ...
// can access x here
} else {
// shouldn't be able to access x here
}
// or here
所以,从本质上讲,我们只想更新环境
- 如果我们有一个let表达式。
- 只适用于该块中后续的表达式
我们可以通过模式匹配对我们的块类型检查规则进行编码。如果没有后续的表达式(即我们在块中没有表达式(模式匹配在[]上)或只有一个表达式(模式匹配在[expr]上)),那么我们就不需要更新环境。
type_block_expr class_defns function_defns (Parsed_ast.Block (loc, exprs)) env =
...
>>= fun () ->
match exprs with
| [] -> Ok (Typed_ast.Block (loc, TEVoid, []), TEVoid) (* empty block has type void *)
| [expr] ->
type_with_defns expr env
>>| fun (typed_expr, expr_type) ->
(Typed_ast.Block (loc, expr_type, [typed_expr]), expr_type)
只有当我们的代码块中至少还剩下两个表达式(pattern-match on expr1 :: expr2 :: exprs),并且这两个表达式中的第一个是让表达式时,我们才会更新代码块其余部分的环境。我们在对expr2::exprs(其余表达式)的递归调用中使用这个更新的环境。然后我们将类型块的结果组合起来。
| expr1 :: expr2 :: exprs ->
type_with_defns expr1 env
>>= fun (typed_expr1, expr1_type) ->
(let updated_env =
match typed_expr1 with
| Typed_ast.Let (_, _, var_name, _) -> (var_name, expr1_type) :: env
| _ -> env in
type_block_with_defns (Parsed_ast.Block (loc, expr2 :: exprs)) updated_env)
>>| fun (Typed_ast.Block (_, _, typed_exprs), block_expr_type) ->
(Typed_ast.Block (loc, block_expr_type, typed_expr1 :: typed_exprs), block_expr_type)
类型类和函数定义
我们现在已经打破了类型检查的背面。我们就以检查类和函数定义来收尾吧。
我们将跳过检查类中没有重复的类定义或重复的字段定义等繁琐的工作。这在repo中)。同样,检查在字段和方法/函数类型签名中注释的类型是否为valids,也只是检查是否有相应的类定义。
与我们的主函数不同,对于其他函数,我们在对函数主体进行类型检查时,实际上并不是从一个空的环境开始,因为我们已经知道了一些变量的类型--函数的参数!
let init_env_from_params params =
List.map
~f:(function TParam (type_expr, param_name, _, _) -> (param_name, type_expr))
params
...
type_block_expr class_defns function_defns body_expr(init_env_from_params params)
我们还需要检查体的结果类型是否是返回类型(如果是void,那么我们就不关心返回的类型)。
>>= fun (typed_body_expr, body_return_type) ->
(* We throw away returned expr if return type is void *)
if return_type = TEVoid || body_return_type = return_type then
Ok
(Typed_ast.TFunction
(func_name, maybe_borrowed_ret_ref, return_type, params, typed_body_expr))
else Error ...
对于类方法,我们可以用同样的方式初始化环境和检查返回类型。但是我们知道另一个特殊变量的类型--类本身。
let init_env_from_method_params params class_name =
let param_env =
List.map
~f:(function TParam (type_expr, param_name, _, _) -> (param_name, type_expr))
params in
(Var_name.of_string "this", TEClass class_name) :: param_env
...
这在Bolt的流水线中处于什么位置?
我们已经进入了Bolt编译器流水线的两个阶段--从编译_程序_ir函数中可以看出。
let compile_program_ir ?(should_pprint_past = false) ?(should_pprint_tast = false)
?(should_pprint_dast = false) ?(should_pprint_drast = false)
?(should_pprint_fir = false) ?(ignore_data_races = false) ?compile_out_file lexbuf =
let open Result in
parse_program lexbuf
>>= maybe_pprint_ast should_pprint_past pprint_parsed_ast
>>= type_program
>>= maybe_pprint_ast should_pprint_tast pprint_typed_ast
>>= ...
收获。3个可行的步骤
如果你已经走到了这一步,那你就做得很好了 Bolt repo包含了完整的代码列表,其中包含了编译器每一种情况下的所有排版判断。
让我们回顾一下到目前为止我们所做的工作。
- 在你的表达式中定义你想要检查的属性。例如,一个条件表达式的类型为bool。
- 用类型判断将其形式化。使用推理规则对子表达式进行推理。
- 将推理规则中的子表达式映射到递归调用中,然后使用推理规则来组合它们的结果。
接下来,我们将谈论用于类型检查我们在Bolt中的线性能力的数据流分析。这类似于Rust借贷检查器如何使用 "非线性寿命 "来检查借贷。
在Twitter上分享这个
如果你喜欢这篇文章,请考虑与你的网络分享。如果你有任何问题,请发微博,我会回答:) 当有新文章发布时,我也会在推特上发布
PS:我也会在学习的过程中分享有用的技巧和链接--这样你就可以在它们进入文章之前就得到它们了。
通过www.DeepL.com/Translator(免费版)翻译