原始的无类型\(\lambda\)-calculus中唯一的值类型是\(\lambda\)抽象,另外也只有变量和应用两种项形式。经过de-Bruijn
index的变换后,可以引入环境这一元素(若干嵌套的\(\lambda\)抽象中的值的列表),变量指向环境中特定位置项,因此可以用一个索引值替代,由此我们就把Lvar of string换成了Lvar of int,这里之所以能做这样的变换还得感谢静态作用域。
比如在fun x -> fun y -> x中,x和y都在环境中,x可以用de
Bruijn index 1代替。
在ML中,值类型还可以是像int这样的基本类型,因此我们需要一个额外的构造器Lconst of constant。原始的无类型\(\lambda\)-calculus也无法表示加法这样的运算,只能采用Church
encoding。在ML这样的实际语言中需要有办法表示这些运算,以及其他很多操作如数组取元素、比较两数、根据构造器和参数创建代数数据类型等,因此我们添加了构造器Lprim of prim * lambda list,可以看作prim表示的原语函数应用在一些参数上。
抛开let-polymorphism,let可以等价变形为\(\lambda\)抽象和应用的一个简单组合(let x=... in y变换为(fun x -> y) (...))。而在类型检查通过后,如果把所有值看作是boxed类型,并且使用无类型的\(\lambda\)-calculus,那么我们可以不忽略let-polymorphism的影响。但实现为函数应用会有一定的性能开销,因此尽管不是必须有的,我们还是提供了构造器Llet of lambda list * lambda。
利用call-by-value的求值策略顺序,可以用\(\lambda\)抽象和应用实现表示顺序执行的;(x; y变换为(fun _ -> y) x),但为了性能考虑设立了Lsequence of lambda * lambda构造器,它会先执行第一项,再执行第二项。通过使用这种方式,实现了对C中语句和表达式的统一。
模式匹配引入了更多的复杂性。当没有模式匹配时,环境中的项(函数的形式参数)只有一种引用方式,即视为一个整体引用。但涉及模式匹配后,考虑fun ((x,y) as a) ->,作为整体的参数可以用a引用,但模式匹配又引入了构成二元组a的两个成分x、y,它们也可以被函数体引用,所以该如何表示它们呢?另外代数数据类型的构造器可以有多个参数,也可以在模式匹配中出现,需要提供一种办法引用它们的参数。
就像代数数据类型的描述那样(元组可以看成是提供了语法支持的特殊代数数据类型),参数可以看成是它们的子域,我们使用原语Pfield of int来引用某个子域。用于de
Bruijn
index的环境不再是简单变量的列表,列表中的每一项不仅要提供引用参数自身的方法,也要提供方法引用出现在模式匹配中的所有变量。因此我们把环境表示为一个列表的列表,每个内层列表代替的原来的简单参数,其中包含所有变量的引用方式。
| Pexpr_apply(e,es) -> Lapply(go e, List.map go es) | Pexpr_array es -> Lprim(Pmakeblock(0,0), List.map go es) | Pexpr_constant c -> Lconst c | Pexpr_sequence(e1,e2) -> Lsequence(go e1, go e2) | Pexpr_tuple es -> Lprim(Pmakeblock(1,0), List.map go es)
| Pexpr_constr(id,arg) -> let cd = find_constr_desc id in begin match arg with | None -> begin match cd.info.cs_kind with | Constr_constant -> Lprim(Pmakeblock cd.info.cs_tag, []) | _ -> Labstract(Lprim(Pmakeblock cd.info.cs_tag, [Lvar 0])) end | Some arg -> Lprim(Pmakeblock cd.info.cs_tag, [go arg]) end
Lcatch of lambda * lambda的第一个参数是try的代码体,如果抛出异常则把控制权交给模式匹配部分,即第二个参数。
扩充\(\lambda\)-calculus定义
lambda.ml中的定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
type lambda = | Labstract of lambda | Lapply of lambda * lambda list | Lcatch of lambda * lambda | Lcond of lambda * (constant * lambda) list | Lconst of constant | Lif of lambda * lambda * lambda | Llet of lambda list * lambda | Lletrec of lambda list * lambda | Lprim of prim * lambda list | Lsequence of lambda * lambda | Lstaticcatch of lambda * lambda | Lstaticraise | Lswitch of int * lambda * (constr_tag * lambda) list | Lvar of int
type zinc_instruction = | Kaccess of int | Kapply | Kbranch of int | Kbranchif of int | Kbranchifnot of int | Kcur of int | Kdummy of int | Kendlet of int | Kgetglobal of long_ident | Kgrab | Klabel of int | Klet | Kmakeblock of constr_tag * int | Kpoptrap | Kprim of prim | Kpush | Kpushmark | Kpushtrap of int | Kquote of struct_constant | Kreturn | Ksetglobal of long_ident | Kswitch of int array | Ktermapply | Ktest of bool_test * int | Kupdate of int
type zinc_instruction的多数构造器都和字节码有直观的对应关系,比如构造器Kaccess of int翻译成字节码时,会额外输出一个uint8_t类型的操作数。表示的标号构造器Klabel of int不会生成字节码,但能作为一个标签供其他指令引用。
中间表示翻译成linear
code的实现在文件back.ml中,采用了compile with
continuation的编译方法。其中的主要函数是compile_lambda中的compile_expr : int -> lambda -> instruction list -> instruction list,第一个参数表示模式匹配失败后需要跳转到的代码,第二个参数是待编译的扩充\(\lambda\)-calculus,第三个参数代表该指令执行完后需要执行的continuation,返回Zinc指令的列表(需要包含continuation)。
很多结构的翻译是直观的:
1 2 3 4 5 6 7 8
| Lvar i -> Kaccess i :: cont | Lconst c -> Kquote c :: cont | Lprim(Pdummy n, []) -> Kdummy n::cont | Lprim(Pgetglobal id, []) -> Kgetglobal id::cont
原始的无类型\(\lambda\)-calculus中唯一的值类型是\(\lambda\)抽象,另外也只有变量和应用两种项形式。经过de-Bruijn
index的变换后,可以引入环境这一元素(若干嵌套的\(\lambda\)抽象中的值的列表),变量指向环境中特定位置项,因此可以用一个索引值替代,由此我们就把Lvar of string换成了Lvar of int,这里之所以能做这样的变换还得感谢静态作用域。
比如在fun x -> fun y -> x中,x和y都在环境中,x可以用de
Bruijn index 1代替。
在ML中,值类型还可以是像int这样的基本类型,因此我们需要一个额外的构造器Lconst of constant。原始的无类型\(\lambda\)-calculus也无法表示加法这样的运算,只能采用Church
encoding。在ML这样的实际语言中需要有办法表示这些运算,以及其他很多操作如数组取元素、比较两数、根据构造器和参数创建代数数据类型等,因此我们添加了构造器Lprim of prim * lambda list,可以看作prim表示的原语函数应用在一些参数上。
抛开let-polymorphism,let可以等价变形为\(\lambda\)抽象和应用的一个简单组合(let x=... in y变换为(fun x -> y) (...))。而在类型检查通过后,如果把所有值看作是boxed类型,并且使用无类型的\(\lambda\)-calculus,那么我们可以不忽略let-polymorphism的影响。但实现为函数应用会有一定的性能开销,因此尽管不是必须有的,我们还是提供了构造器Llet of lambda list * lambda。
利用call-by-value的求值策略顺序,可以用\(\lambda\)抽象和应用实现表示顺序执行的;(x; y变换为(fun _ -> y) x),但为了性能考虑设立了Lsequence of lambda * lambda构造器,它会先执行第一项,再执行第二项。通过使用这种方式,实现了对C中语句和表达式的统一。
模式匹配引入了更多的复杂性。当没有模式匹配时,环境中的项(函数的形式参数)只有一种引用方式,即视为一个整体引用。但涉及模式匹配后,考虑fun ((x,y) as a) ->,作为整体的参数可以用a引用,但模式匹配又引入了构成二元组a的两个成分x、y,它们也可以被函数体引用,所以该如何表示它们呢?另外代数数据类型的构造器可以有多个参数,也可以在模式匹配中出现,需要提供一种办法引用它们的参数。
就像代数数据类型的描述那样(元组可以看成是提供了语法支持的特殊代数数据类型),参数可以看成是它们的子域,我们使用原语Pfield of int来引用某个子域。用于de
Bruijn
index的环境不再是简单变量的列表,列表中的每一项不仅要提供引用参数自身的方法,也要提供方法引用出现在模式匹配中的所有变量。因此我们把环境表示为一个列表的列表,每个内层列表代替的原来的简单参数,其中包含所有变量的引用方式。
| Pexpr_apply(e,es) -> Lapply(go e, List.map go es) | Pexpr_array es -> Lprim(Pmakeblock(0,0), List.map go es) | Pexpr_constant c -> Lconst c | Pexpr_sequence(e1,e2) -> Lsequence(go e1, go e2) | Pexpr_tuple es -> Lprim(Pmakeblock(1,0), List.map go es)
| Pexpr_constr(id,arg) -> let cd = find_constr_desc id in begin match arg with | None -> begin match cd.info.cs_kind with | Constr_constant -> Lprim(Pmakeblock cd.info.cs_tag, []) | _ -> Labstract(Lprim(Pmakeblock cd.info.cs_tag, [Lvar 0])) end | Some arg -> Lprim(Pmakeblock cd.info.cs_tag, [go arg]) end
Lcatch of lambda * lambda的第一个参数是try的代码体,如果抛出异常则把控制权交给模式匹配部分,即第二个参数。
扩充\(\lambda\)-calculus定义
lambda.ml中的定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
type lambda = | Labstract of lambda | Lapply of lambda * lambda list | Lcatch of lambda * lambda | Lcond of lambda * (constant * lambda) list | Lconst of constant | Lif of lambda * lambda * lambda | Llet of lambda list * lambda | Lletrec of lambda list * lambda | Lprim of prim * lambda list | Lsequence of lambda * lambda | Lstaticcatch of lambda * lambda | Lstaticraise | Lswitch of int * lambda * (constr_tag * lambda) list | Lvar of int
type zinc_instruction = | Kaccess of int | Kapply | Kbranch of int | Kbranchif of int | Kbranchifnot of int | Kcur of int | Kdummy of int | Kendlet of int | Kgetglobal of long_ident | Kgrab | Klabel of int | Klet | Kmakeblock of constr_tag * int | Kpoptrap | Kprim of prim | Kpush | Kpushmark | Kpushtrap of int | Kquote of struct_constant | Kreturn | Ksetglobal of long_ident | Kswitch of int array | Ktermapply | Ktest of bool_test * int | Kupdate of int
type zinc_instruction的多数构造器都和字节码有直观的对应关系,比如构造器Kaccess of int翻译成字节码时,会额外输出一个uint8_t类型的操作数。表示的标号构造器Klabel of int不会生成字节码,但能作为一个标签供其他指令引用。
中间表示翻译成linear
code的实现在文件back.ml中,采用了compile with
continuation的编译方法。其中的主要函数是compile_lambda中的compile_expr : int -> lambda -> instruction list -> instruction list,第一个参数表示模式匹配失败后需要跳转到的代码,第二个参数是待编译的扩充\(\lambda\)-calculus,第三个参数代表该指令执行完后需要执行的continuation,返回Zinc指令的列表(需要包含continuation)。
很多结构的翻译是直观的:
1 2 3 4 5 6 7 8
| Lvar i -> Kaccess i :: cont | Lconst c -> Kquote c :: cont | Lprim(Pdummy n, []) -> Kdummy n::cont | Lprim(Pgetglobal id, []) -> Kgetglobal id::cont