-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.json
1 lines (1 loc) · 90.3 KB
/
index.json
1
[{"content":"我发起了一个从 0 开始制作编程语言的项目 fun for fun. 该项目使用 OCaml 实现了一个编译器(称为 ff 编译器), 可以把一个 OCaml 的子集编译到可以 C++ 源程序 (生成程序只用到了 C 的语法), 该 C++ 程序可以被进一步编译为可执行文件.\n为什么又一个编译器教程 为什么又又又一个关于编程语言实现的教程?\n并不是已有的教学项目不够好/不足够, 现在高质量的编译器教程非常多(可以直接看最后一节看我的推荐目录). 之所以发起一个新的编译器教学项目, 主要有以下几方面的原因:\n实现一个编译器真的很有趣: 我最开始是只是对 OCaml 的类型系统(主要是模块系统)的检查很感兴趣, 只是想着实现一个类型系统以加深一下自己的理解. 在完成类型系统的实现后, 每次只是想再往后再走一小步, \u0026ldquo;不知不觉\u0026quot;就完成了一个到 C++ 的后端, 正如这个项目名称 Fun for Fun, 这件事情太有趣了使我一有时间就忍不住不做; 向前辈学习: 现在的高质量编译器教学项目确实很多, 这些优秀的教程已经帮助我少走了很多弯路. 作为一个已经从前辈们的教学项目受益良多的人, 我认为不仅有必要学习他们传播的知识, 还有必要学习他们无私共享的精神, 我希望能通过文字说明和代码演示展示我对知识的理解和学习的过程, 让未来的学习者也能少走一些弯路; 与人沟通对理解深刻的知识是很有帮助: 知识的理解, 从最 看懂书本/论文上的介绍 到 能在实践中根据需要运用自如, 是需要一个过程的, 对于一些比较深刻的知识, 这甚至可能是一个长期的过程. 通过给别人讲解, 可以帮助我站在多个角度重新思考问题, 从而加深我对知识的理解, 让我离事物的\u0026quot;本质\u0026quot;更进一步. 学习资料推荐 计算机程序构造与解释(SICP) 首先要推荐的是已经被很多人推荐过的 SICP (Abelson and Sussman 1996). 这本书的前半部分尝试教会读者: 如何在程序中建立抽象, 如何基于已有的抽象建立新的抽象. 后半部分介绍了在编程语言实现中常见的抽象.\n抽象 是这本书的主题(我一连说了 3 个 抽象), 一方面也暗示了这本书描述的内容十分的抽象(即使书里有演示代码), 另一方面也暗示了建立可靠\u0026amp;可组合的抽象对实现编程语言的重要性.\n我很庆幸当时听了知乎人的建议, 在\u0026quot;生涯早期\u0026quot;就学习了这本书, 如果有时间, 我希望我能重读一遍.\n注: 如果你和我一样在第一次看书时无法理解书上的内容, 公开课视频也许会有帮助, 因为公开课中的学生也许和我们有着一样的问题, 他们有可能会在课上帮我们把问题提出来. Types ans Programming Language(TaPL) todo\nThe Implementation of Functional Programming Language (TIoFPL) 语言常见的两种实现模式: 编译和解释(Language and Interpreters). 对于编译实现, 很多人推荐学习龙书来学习, 但是我在费力的理解了书上的定义以后仍然云里雾里, 不知道实现一个编译器到底应该干嘛, 没能通过这本书学会编译原理.\nTIoFPL 这本书给了我答案: 编译其实就是一个函数, 把一个语言的源程序映射至另一个语言.\n一个函数式编程语言的实现贯穿着整本书, 这本书把这个足够复杂的函数式编程语言映射到足够底层的语言(X86 汇编).\n最关键的是, 书中几乎看不到龙书里那样复杂的算法描述, 而是用很多个简单的函数来描述语言之间的映射, 每个函数做的事情都很少, 因此很容易验证它们正确性. 这些函数环环相扣地组合到一起变成一个大的函数, 这个大函数可以把源语言映射到目标语言, 整个过程相当精彩.\n计算理论导引 之前的书都没有解析将解析(parsing)的内容, 有人说如果对 parsing 感兴趣可以看龙书, 我认为但是龙书的严谨程度和清晰程度都不如这本 计算理论导引, 龙书parsing相关章节中的结论很多, 但是结论的证明是很少的, 取代证明的是大量的算法, 例子和演示.\n我更喜欢 计算理论导引 的讲解方式: 只有少量的结论, 但是这些结论都通过严谨的推导得出. 而这些证明有的时候可以成为理解问题的关键.\n","permalink":"http://butter-xz.com/articles/20240721011820-fun_for_fun/","summary":"\u003cp\u003e我发起了一个从 0 开始制作编程语言的项目 \u003ca href=\"https://github.com/butterunderflow/fun-for-fun\"\u003efun for fun\u003c/a\u003e.\n该项目使用 OCaml 实现了一个编译器(称为 ff 编译器), 可以把一个 OCaml 的子集编译到可以 C++ 源程序\n(生成程序只用到了 C 的语法),\n该 C++ 程序可以被进一步编译为可执行文件.\u003c/p\u003e\n\u003ch2 id=\"为什么又一个编译器教程\"\u003e为什么又一个编译器教程\u003c/h2\u003e\n\u003cp\u003e为什么又又又一个关于编程语言实现的教程?\u003c/p\u003e\n\u003cp\u003e并不是已有的教学项目不够好/不足够, 现在高质量的编译器教程非常多(可以直接看最后一节看我的推荐目录).\n之所以发起一个新的编译器教学项目, 主要有以下几方面的原因:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003e\u003cstrong\u003e实现一个编译器真的很有趣\u003c/strong\u003e\u003c/strong\u003e: 我最开始是只是对 OCaml 的类型系统(主要是模块系统)的检查很感兴趣,\n只是想着实现一个类型系统以加深一下自己的理解. 在完成类型系统的实现后, 每次只是想再往后再走一小步,\n\u0026ldquo;不知不觉\u0026quot;就完成了一个到 C++ 的后端, 正如这个项目名称 Fun for Fun, 这件事情太有趣了使我一有时间就忍不住不做;\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e\u003cstrong\u003e向前辈学习\u003c/strong\u003e\u003c/strong\u003e: 现在的高质量编译器教学项目确实很多, 这些优秀的教程已经帮助我少走了很多弯路.\n作为一个已经从前辈们的教学项目受益良多的人, 我认为不仅有必要学习他们传播的知识,\n还有必要学习他们无私共享的精神, 我希望能通过文字说明和代码演示展示我对知识的理解和学习的过程,\n让未来的学习者也能少走一些弯路;\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e\u003cstrong\u003e与人沟通对理解深刻的知识是很有帮助\u003c/strong\u003e\u003c/strong\u003e:\n知识的理解, 从最 \u003cstrong\u003e\u003cstrong\u003e看懂书本/论文上的介绍\u003c/strong\u003e\u003c/strong\u003e 到 \u003cstrong\u003e\u003cstrong\u003e能在实践中根据需要运用自如\u003c/strong\u003e\u003c/strong\u003e, 是需要一个过程的,\n对于一些比较深刻的知识, 这甚至可能是一个长期的过程.\n通过给别人讲解, 可以帮助我站在多个角度重新思考问题, 从而加深我对知识的理解, 让我离事物的\u0026quot;本质\u0026quot;更进一步.\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"学习资料推荐\"\u003e学习资料推荐\u003c/h2\u003e\n\u003ch3 id=\"计算机程序构造与解释--sicp\"\u003e计算机程序构造与解释(SICP)\u003c/h3\u003e\n\u003cp\u003e首先要推荐的是已经被很多人推荐过的 SICP (\u003ca href=\"#citeproc_bib_item_1\"\u003eAbelson and Sussman 1996\u003c/a\u003e).\n这本书的前半部分尝试教会读者: 如何在程序中建立抽象, 如何基于已有的抽象建立新的抽象.\n后半部分介绍了在编程语言实现中常见的抽象.\u003c/p\u003e","title":"Fun for Fun"},{"content":"这是 [1] 第 8 章 Partial Evaluation for Lambda Calculus 的笔记.\n在 Partial Evaluation for Functional Language 和 Partial Evaluation For Flow Chart Langauge 中, partial evaluation 所 eval 的东西很直观, 就是一个具体的像 int, bool 这样具体的值, 没有考虑高阶函数.\n但是对于有高阶函数的语言, 情况变得复杂, 因为一个表达式的求值结果可能是一个函数, 那么考虑一个简单的场景, 返回一个常量的函数, 应该标记为是 Static 还是 Dynamic ? 比如: (lambda (x) 1)\n如果标记为 S, 那这个函数在 residual program 中对应什么? 似乎也只能是 (lambda (x) 1) 如果标记为 D, 为什么一个这么简单的函数会返回一个常量的函数需要标记为 D? 是不是对于 lambda 表达式 partial evaluation 都无能为力? 我会有这样的疑惑主要有两个原因:\n之前提到的 S 和 D 这样简单的 annotation 的标记对于简单的语言是足够的, 但是对于存在高阶函数的语言, 需要更丰富的 binding time annotation 才能描述\u0026quot;编译期函数\u0026quot;和\u0026quot;运行时函数\u0026quot;; 标记为 D 和 S 的表达式都不一定会出现在最终的 residual program 中, 在有高阶函数的语言, residual program 长什么样主要看 partial evaluation 的结果, 之前的 \u0026ldquo;Dynamic 表达式作为程序骨架, Static 表达式求值后嵌入程序骨架\u0026rdquo; 的基本直觉失效了. 本文将介绍 lambda calculus 的 partial evaluation. 首先将定义一个简单的 lambda calculus, 然后介绍它的 binding time annotation 和 annotated version, 最后再分别展示它的 Binding Time Analysis(由 lambda calculus 得到 Annotated Program)和 具体的 staging(由 annotated program 得到 residual program)算法.\n本文所有代码都将用 OCaml 演示, 代码仓库在 https://github.com/butterunderflow/lambda_pe , 可以在这里 https://butter-xz.com/lambda_pe/ 在线体验 Lambda Calculus 的 Partial Evaluation; 有时候 Lambda Calulus 会被简写成 LC, Partial Evaluation 会被简写为 PE, Binding Time Analysis 会被简写为 BTA. Lambda-calculus 的语法 labmda 表达式的语法定义如下:\n(* expr1.ml, level 1 expression*) type expr = | EInt of int | EVar of string | ELam of string * expr | ELet of string * expr * expr | EApp of expr * expr Binding Time Annotation(for Lambda Calculus) 有 partial evaluation 的语言有个 two-level type system, two-level 的意思是, 一个 level 对应编程语言原来的类型系统, 另一个就对应这个 annotation.\n在之前章节里关于 Binding Time Annotation 的一个比较直观的理解是: 标记为 S 会被 eval 成值, D 会在 residual program 中被保留成代码.\n这还是比较好理解的, 但是 D -\u0026gt; S 呢? D -\u0026gt; S -\u0026gt; S \u0026hellip; 呢? 它们代表什么? Binding time annotation 和一般的 type annotation 很类似, 只不过他不是描述 运行时(run time) 类型信息的, 而是描述 partial evaluation time 的类型信息的, 这个类型信息只做了 code 和 value 的区分, 具体的 code 和 value 又有它们自己在 first-level 的类型.\n这个标记的所描述的是 pe-time 过程中的类型信息, 标记为 D(ynamic) 的表达式在 pe 过程中会被求值为 code, 标记为 S(tatic) 的会被求值为 value, 标记为 D -\u0026gt; S 的表达式在 specialize 过程中会转换为由 code 到 value 函数, 但是它们并不代表 residual program 中的最终形态. 也就是说, 标记为 S 和 D 的表达式都不一定会出现在 residual program 中.\nAnnotated Program(for Lambda Calculus) Annotated Program 对应一个语言的 Two-level syntax\nFigure 1: two-level syntax for Scheme0\nFigure 2: Lambda calculus 的 two-level syntax\n带有 S 标记的表示这个表达式在 pe 过程中会 eval 到 value(可能是一般的值或函数值), 带有 D 标记的则会 eval 到 code.\nnote: variable 没有标记, variable 的 pe 结果是从当前的 environment 中 lookup 的结果. (* expr2.ml, level 2 expression *) type expr = (* use variable lookup as specialize result *) | Var of string (* specialize to a value(a pe time int or function value) *) | SConst of constant | SLam of string * expr | SLet of string * expr * expr | SApp of expr * expr (* specialize to a pe-time code expression *) | DLam of string * expr | DLet of string * expr * expr | DApp of expr * expr | DLift of expr [@@deriving sexp] Binding Time Analysis LC 的 Binding Time Analysis 就是把 LC 编译到 2LC 的过程, 我们可以这样定义 BTA 的接口:\nmodule type BTA_Sig = sig val analysis : Expr1.expr -\u0026gt; Expr2.expr end Naive BTA BTA 是一个比较泛的概念, 并没有唯一正确的做法, 比如说什么都不干, 把所有东西都当作 Dynamic 也是一种可行的 BTA:\nmodule NaiveBTA : BTA_Sig = struct (* blindly push every thing to runtime *) let analysis (e : E1.expr) : E2.expr = let rec go e = match e with | E1.EConst c -\u0026gt; E2.SConst c | E1.EVar x -\u0026gt; E2.Var x | E1.ELam (x, e0) -\u0026gt; E2.DLam (x, go e0) | E1.ELet (x, e0, e1) -\u0026gt; E2.DLet (x, go e0, go e1) | E1.EApp (e0, e1) -\u0026gt; E2.DApp (go e0, go e1) in go e end Naive BTA' 另一个极端是把所有东西都当作 static, 这么做当然也是可行的(只是没什么意义):\n(* blindly stage every thing to compile time *) let analysis (e : E1.expr) : E2.expr = let rec go e = match e with | E1.EConst c -\u0026gt; E2.SConst c | E1.EVar x -\u0026gt; E2.Var x | E1.ELam (x, e0) -\u0026gt; E2.SLam (x, go e0) | E1.ELet (x, e0, e1) -\u0026gt; E2.SLet (x, go e0, go e1) | E1.EApp (e0, e1) -\u0026gt; E2.SApp (go e0, go e1) | E1.EAnn (e0, _) -\u0026gt; go e0 | E1.EOp (op, es) -\u0026gt; E2.SOp (op, List.map go es) in go e BTA by type check 前面两个 NaiveBTA 是\u0026quot;对\u0026quot; 的吗? 当然是对的, 但是这其实没有意义, 因为我们没办法对 annotated program 做 staging (staging nothing 和 staging everything).\n前面提到了, BTA 的结果是不唯一的, 既然不唯一, 那要如何选择一个表达式的 annotation 呢?\n比如说下面的代码, f 既 apply 到了 1(a static)上, 又 apply 到了 y(a dynamic)上, 如何确定 f 的 annotation? 很多种可行解.\nlet f = fun x -\u0026gt; x in (* f: D; D-\u0026gt;D; S-\u0026gt;S; (S-\u0026gt;S) -\u0026gt; (S-\u0026gt;S) ... *) ... f 1 ... ... f y ... (* y is dynamic *) 如果我们只看 f 1 , 那么 f 只能是 S -\u0026gt; 'a, D , D -\u0026gt; 'a (1可以被lift为Dynamic); 如果只看 f y, 那么 f 只能是 D -\u0026gt; 'a, D .\n书上介绍了一种约束收集+求解的方法, 定义了一个 annotation 上的偏序关系, 然后通过求解收集到的约束得到最小的解.\n这个方法太复杂了, 一堆偏序关系头都要晕了, 如果有感兴趣的话可以看 [1] 的 8.7 BTA by solving constraints .\n这里选择另一种简单点的方法作为实现. 首先, 为了让 bta 变得简单, 我们给 LC1 加上一个 binding time annotation 注解的语法, 让我们可以手动的为表达式添加 annotation.\nand expr = | EConst of constant | EVar of string | ELam of string * expr | ELet of string * expr * expr | EApp of expr * expr | EAnn of expr * Ann.t (* Binding time annotation hint *) | EOp of op * expr list 然后用 ML 类型推导的方式(不需要偏序关系, 仅仅需要等价关系)推导一个表达式的 Binding Time Annotation, 根据这个 Annotation 来得到 level 2 expression.\n这里我们借用 Local Type Inference 中 bidirectional type checking 的思路, 把 bta 分为两种 mode:\ncheck mode: 已知一个表达式的 annotation a, 验证这个表达式的 annotation 是否等于 a; infer mode: 对表达式的 annotation 一无所知, 需要推导这个表达式的 annotation. bidirectional type checking 可以把已经推导出的类型信息传递到相邻的语法树节点, 使得我们可以仅仅只写少量的 binding time annotation 注解就可以推导所有节点的 annotation.\n具体的代码实现如下:\nlet rec infer (e : E1.expr) (env : ann_env) : E2.expr * ann = match e with | E1.EConst c -\u0026gt; (E2.SConst c, S) | E1.EVar x -\u0026gt; (E2.Var x, get x env) | E1.ELam (x, e0) -\u0026gt; failwith \u0026#34;can\u0026#39;t infer a lambda\u0026#34; | E1.ELet (x, e0, e1) -\u0026gt; ( let e0\u0026#39;, a0\u0026#39; = infer e0 env in match a0\u0026#39; with | D -\u0026gt; (E2.DLet (x, e0\u0026#39;, check e1 Ann.D ((x, a0\u0026#39;) :: env)), D) | _ -\u0026gt; let e1\u0026#39;, a1\u0026#39; = infer e1 ((x, a0\u0026#39;) :: env) in (E2.SLet (x, e0\u0026#39;, e1\u0026#39;), a1\u0026#39;)) | E1.EApp (e0, e1) -\u0026gt; ( let e0\u0026#39;, a0\u0026#39; = infer e0 env in match a0\u0026#39; with | S -\u0026gt; failwith \u0026#34;error\u0026#34; | D -\u0026gt; let e1\u0026#39; = check e1 Ann.D env in (E2.DApp (e0\u0026#39;, e1\u0026#39;), Ann.D) | Func (arg_ann, ret_ann) -\u0026gt; let e1\u0026#39; = check e1 arg_ann env in (E2.SApp (e0\u0026#39;, e1\u0026#39;), ret_ann)) | E1.EAnn (e0, a0) -\u0026gt; (check e0 a0 env, a0) | E1.EOp (op, es) -\u0026gt; ( match (op, es) with | OAdd, [ e0; e1 ] | OMinus, [ e0; e1 ] | OAnd, [ e0; e1 ] -\u0026gt; let e0\u0026#39;, a0\u0026#39; = infer e0 env in let e1\u0026#39; = check e1 a0\u0026#39; env in (E2.SOp (op, [ e0\u0026#39;; e1\u0026#39; ]), a0\u0026#39;) | ONot, [ e0 ] -\u0026gt; let e0\u0026#39;, a0\u0026#39; = infer e0 env in (E2.SOp (op, [ e0\u0026#39; ]), D) | _ -\u0026gt; failwith \u0026#34;neverreach\u0026#34;) and check (e : E1.expr) (a : ann) (env : ann_env) : E2.expr = match e with | E1.ELam (x, e0) -\u0026gt; check_lambda x e0 a env | _ -\u0026gt; let e\u0026#39;, a\u0026#39; = infer e env in if a\u0026#39; = a then e\u0026#39; else if a\u0026#39; = S \u0026amp;\u0026amp; a = D then E2.DLift e\u0026#39; else failwith \u0026#34;error\u0026#34; and check_lambda x e a env = match a with | S -\u0026gt; failwith \u0026#34;error lambda annotation\u0026#34; | D -\u0026gt; let e\u0026#39; = check e D ((x, D) :: env) in E2.DLam (x, e\u0026#39;) | Func (arg_ann, ret_ann) -\u0026gt; let e\u0026#39; = check e ret_ann ((x, arg_ann) :: env) in E2.SLam (x, e\u0026#39;) let analysis (e : E1.expr) : E2.expr = infer e empty_env |\u0026gt; fst Staging! 万事俱备, 只欠 staging.\nLC 的 two-level syntax 类似于之前在 partial evaluation for Scheme0 提到的 Binding Time Annotation , 但是在值域上有所差异:\nScheme0 的值域只能是 Constant 所在的值域(称之为 Const); lambda calculus 的值域可以是高阶函数: 高阶函数的值域(2FuncVal)是很丰富的: 不仅可以是 \\(Const \\rightarrow Cosnt\\) 的函数; 可以是 \\(2FuncVal \\rightarrow 2FuncVal\\); 还可以是 \\(Code\\) \u0026hellip; . 在做完 BTA 后, staging 的实现就很简单了, LC2 的 staging 其实也可以看作 two-level lambda calculus(2LC)的 interpreter! 只是在值域上有所差异, 这个 2LC 的值域在 LC 的基础上加上了 Code, 对于 2LC 表达式的求值结果可能是:\n求值到 Const 或 FuncVal, 代表这个表达式已经求值完了; 还可能求值到一个 Code, 这个 Code 等之后再去算. dlambda 的 body 也必须求值到 code, 然后再通过 build-lambda 创建一个动态求知 lambda 表达式. 但是这里为什么要用 newname? 书上说是为了避免 confusion, 我理解这个完全是为了 residual program 可读性和 specialize 算法的可维护性考虑的, 就算不重命名也不会导致 bug.\nstaging的代码实现如下:\nlet rec eval (e : expr) (env : env) : value = match e with | Var x -\u0026gt; List.assoc x env | DLam (x, e) -\u0026gt; let new_var = gen_var ~hint:x in VCode (E1.ELam (x, eval e ((x, VCode new_var) :: env) |\u0026gt; get_code)) | DLet (x, e0, e1) -\u0026gt; let updated_env = (x, VCode (E1.EVar x)) :: env in VCode (match eval e0 env with | VCode code -\u0026gt; E1.ELet (x, code, eval e1 updated_env |\u0026gt; get_code) | VConst c -\u0026gt; E1.ELet (x, E1.EConst c, eval e1 updated_env |\u0026gt; get_code) | VFun f -\u0026gt; E1.ELet ( x, f (VCode (E1.EVar \u0026#34;_x\u0026#34;)) |\u0026gt; get_code, eval e1 updated_env |\u0026gt; get_code )) | DApp (e0, e1) -\u0026gt; VCode (E1.EApp (eval e0 env |\u0026gt; get_code, eval e1 env |\u0026gt; get_code)) | DLift e -\u0026gt; let v = get_int (eval e env) in VCode (E1.EConst v) | SConst c -\u0026gt; VConst c | SLam (x, e) -\u0026gt; VFun (fun v -\u0026gt; eval e ((x, v) :: env)) | SLet (x, e0, e1) -\u0026gt; let bind_value = eval e0 env in eval e1 ((x, bind_value) :: env) | SApp (e0, e1) -\u0026gt; let func = get_func (eval e0 env) in eval e1 env |\u0026gt; func thinking Offline PE 中 annotated language(比如 2LC) 和 typeset language 的是否会存在某种同构?\nReferences [1]N. D. Jones, C. K. Gomard, and P. Sestoft, Partial Evaluation and Automatic Program Generation. USA: Prentice-Hall, Inc., 1993. ","permalink":"http://butter-xz.com/articles/20231111153704-partial_evaluation_for_lambda_calculus/","summary":"\u003cp\u003e这是 \u003ca href=\"#citeproc_bib_item_1\"\u003e[1]\u003c/a\u003e 第 8 章 \u003cem\u003ePartial Evaluation for Lambda Calculus\u003c/em\u003e 的笔记.\u003c/p\u003e\n\u003cp\u003e在 \u003ca href=\"/articles/partial-evaluation-for-functional/\"\u003ePartial Evaluation for Functional Language\u003c/a\u003e 和 \u003ca href=\"/articles/partial-evaluation-for-flow-chart/\"\u003ePartial Evaluation For Flow Chart Langauge\u003c/a\u003e 中,\npartial evaluation 所 eval 的东西很直观, 就是一个具体的像 int, bool 这样具体的值, 没有考虑高阶函数.\u003c/p\u003e\n\u003cp\u003e但是对于有高阶函数的语言, 情况变得复杂, 因为一个表达式的求值结果可能是一个函数,\n那么考虑一个简单的场景, 返回一个常量的函数, 应该标记为是 Static 还是 Dynamic ?\n比如: \u003ccode\u003e(lambda (x) 1)\u003c/code\u003e\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e如果标记为 S, 那这个函数在 residual program 中对应什么? 似乎也只能是 \u003ccode\u003e(lambda (x) 1)\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e如果标记为 D, 为什么一个这么简单的函数会返回一个常量的函数需要标记为 D?\n是不是对于 lambda 表达式 partial evaluation 都无能为力?\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e我会有这样的疑惑主要有两个原因:\u003c/p\u003e","title":"Partial Evaluation for Lambda Calculus"},{"content":"关于 [1] 的第7章 Offline and Online Partial Evaluation 的笔记\nOffline 和 Online 指的是什么? Offline 和 Online 的区别在于 Binding Time Analysis 的时机: Offline 会提前把 Binding Time Analysis (BTA)做完, 并保证 binding time 的相合性, 再基于 BTA 的结果(可以由 Division 或 Annotated Program 表示)进行 specialize; Online 会直接基于输入的程序做 specialize, 在 specialize 的过程中保证 congruence.\nFigure 1: Online 方法和 Offline 方法的结构\nFigure 2: Online 方法和 Offline 方法的定义\nOnline 方法的分析 OnPE 方法的类型签名: \\[ Expression \\rightarrow On-Env \\rightarrow On-Value \\]\n其中 \\(On-Value\\) 是 specialize 的结果, 是一个 Variant(tagged union): 如果表达式在\\(On-Env\\)中是 static 的, 则 specialize 到一个值(inVal); 如果表达式在\\(On-Env\\)中是 dynamic 的, 则 specialize 到一个表达式(inExp).\n\\(On-Env = Var \\rightarrow On-Value\\)包含了\\(Expression\\)中变量的绑定信息: 如果变量是 static 的, 则 specialize 到一个值(inVal); 如果是 dynamic 的, 则 specialize 到一个表达式(inExp).\n为什么 OffPE 没有这样的区分呢? 因为 OffPE 在 BTA 中就分析了哪些表达式会 specialize 到 Value, 哪些会 specialize 到 Expression, 然后通过 division 或者 binding time annotation 表示这一信息.\nOnPE 的含义是: 输入一个源程序的表达式(Expression), 和这个表达式中的自由变量在 residual program 中对应的 specialized expression(On-Env = Var -\u0026gt; On-Value), 如果是 static 则绑定到值, 如果是 dynamic 则直接绑定到一个变量表达式. 返回一个 specialized expression(On-Value).\nOnline 方法的输入是不区分 subject program 中的 static 和 dynamic exppression 的, 也就是说 Online 方法直接在输入源程序上进行变换. 因此可以在 specialize 的过程中\u0026quot;实时的\u0026quot;根据当前的 specialize 上下文判断输入的表达式是不是 static 的, 因此相比与 Offline 方法, Online 方法可以把更多的表达式视作为 static(发现更多的 specialize 机会).\nOffline 方法的分析 而 OffPE 的类型签名: \\[ 2Expression \\rightarrow Off-Env \\rightarrow Off-Value \\]\n其中 2Expression 代表了经过 Binding Time Analysis 后得到的 annotated program. OffPE 的含义是: 输入一个经过标注了 Binding Time 的 2Expression 表达式, 和一个 Off-Env 包含这个表达式中的自由变量的 Binding Time(Off-Value 是一个 untaged union: 可以是 static 的 Value, 也可以是一个代表需要动态求值的 1Expression). 返回一个 specialized expression(Off-Value).\nBTA 保证了当一个变量出现在程序中需要 static value 的位置的时候, Off-Env 中的 lookup result 一定是一个 value, 所以在 OffPE 中无需再对 OffPE 的返回值的类型进行判断, 直接把返回值放在需要它的位置就好了.\n为什么 online partial evaluation 对 self application 不友好? OnPE 的类型签名: \\[ Expression \\rightarrow On-Env \\rightarrow On-Value \\]\n光从类型上来看, OnPE 的类型和二村映射的类型是完全匹配的, 但是在通过 OnPE 实现第二二村映射生成 compiler 的表现却并不好, 书中给出了一个例子说明这一点:\n(cons(car names)(car values)) 是解释器中的表达式, names 和 value 都是解释器中的变量. 在应用把 OnPE[OnPE, int] 的后, 会得到如下程序片段,\n在 \\(OnPE(int, prog)\\) 的时候, \\(lookup [names] rho\\) 的返回值总是一个包含 prog 中的表达式的 Value, 但是在上面 \\(OnPE(OnPE, int)\\) 生成的程序中, 其对 \\(lookup_{names} rho\\) 的返回值也进行了判断.\n从直觉上, 这样的\u0026quot;反常\u0026quot;现象可以由两方面来解释:\n生成的 compiler 会从 OnPE 继承一些行为: OnPE 需要处理所有可能的 subject program 中的表达式, 并判断表达式在 On-Env 下是 static 还是 dynamic, 因此使用 OnPE 的返回值时需要根据返回值是 Value 还是 Expr 分别处理; 但是 \\(OnPE(int, prog)\\) 的时候, 有一些表达式一定是 Value, 比如说\\(lookup [names] rho\\), 但在 self application 的时候没有利用这一信息; OnPE 具有通用性(generality), 其能力对于实现二村映射来说\u0026quot;过剩\u0026quot;了: OnPE 对输入的类型没有任何限制, 对 任意 程序都可以specialize到其 任意 输入. OnPE 的输入 On-Env 中包含了 subject program 中的变量到 OnVal 的映射, 而 OnVal = Value | Expr 中的 Value 可以是任意类型的 program text 也可以是一般的数据, 但是在几个二村映射中 Env 中所绑定的都是 program text, (比如说 mix(int, prog), mix(mix, int) 的时候, On-Env 中的 Value 绑定到的就分别是源程序和解释器程序的 program text). 而 OnPE 中没有对这种\u0026quot;程序的输入本身是程序\u0026quot;情况做特殊的考虑:\u0026ldquo;不管 subject program 的获得哪种输入, 我都把它们嵌入到 subject program 中\u0026rdquo;. 也就是说, 我们甚至可以把 OnPE 应用到程序的输入, 生成一个没有什么实际意义的 crazy 程序, 这个 crazy 的语义是: 接受源程序然后得到源程序的运行结果. 而这样的一般性也被带到了由 OnPE [OnPE, int] 生成的 compiler 中: 在 OnPE [OnPE, int] 的输入 On-Env 中, 既可以绑定源程序片段, 从而得到编译器; 也可以绑定源程序的输入, 从而得到 crazy. Figure 3: crazy 程序的语义\n通过消除一般性以更好的 self-Applicatoin 刚刚提到, OnPE 是具有通用性(generality)的: 想在 On-Env 中绑定什么都行, 只要它能作为 subject program 的输入. 但是在二村映射中, 这样的一般性是不必要的, 因为静态输入的参数已经确定了\u0026ndash;待编译的源程序.\n\\(OnPE_2[int, source]\\) 的时候, \\(int\\) 是subejct program, \\(int\\) 中被绑定到 \\(source\\) 程序片段的名字, 在环境中映射到 InVal, 绑定到 \\(input\\) 的名字在 On-Env 中映射到 InExp 中. 所以 \\(OnPE_2\\) 的行为通俗来讲是这样的:\n拿到一个待 PE 的 subject program \\(int\\); 拿到一个subject program的Environment, 在这个Enviroment中, 告诉了 \\(OnPE_2\\) subject program \\(int\\) 中哪些变量绑定到 InVal (source 关联的变量), 哪些变量绑定到 InExp (input 关联的变量). 开始递归遍历 subject program \\(int\\), 返回residual program, 在这个过程中: 如果看到 \\(int\\) 中的name, 需要去 Environment 中查看这个 name 是 InVal 还是 InExp. 在 \\(OnPE_1[OnPE_2, int]\\) 的时候, \\(OnPE_1\\) 的\u0026quot;眼中\u0026quot;:\n\\(OnPE_2\\) 是一个一般的程序, \\(int\\) 是这个程序已知的输入; \\(OnPE_2\\) 的另一个输入 Environment 是未知的, 所以对于 \\(OnPE_2\\) 中依赖于 Environment 的计算在specialize的过程中都会保留, 比如说 lookup [name] 这一操作, 即使 \\(OnPE_2[int, source]\\) 的时候 lookup[name] 一定返回一个 InVal, \\(OnPE_1\\) 也不知道这一点, 它只知道 \\(OnPE_2\\) 往一个既有 InVal 又有 InExp 的Environment中lookup了, 所以lookup的返回值有可能是 InVal 或 InExp 两种, 所以对这个返回值的分支判断当然要保留. Offline Partial Evaluator 在 self-application 上的优势正是通过消除 Online Partial Evaluator 的一般性获得的: Offline PE 会首先通过 Binding Time Analysis 得到一个 annotated interpreter: \\(int^{ann}\\).\n首先分析一下 \\(OffPE_2[int^{ann}, source]\\) 的计算过程: \\(OffPE_2\\) 在判断 \\(int^{ann}\\) 中的表达式应该是 static 还是 dynamic 不需要管另一个输入 \\(source\\), 因为一切都在 \\(int^{ann}\\) 中标定好了.\n然后就可以在已知 \\(int^{ann}\\) 的输入下, 对 \\(OffPE_2\\) 进行标定了: \\(OffPE_2\\) 中的表达式, 如果在只依赖于 \\(int^{ann}\\) 那么就可以标定为 static , 标定后的 \\(OffPE_2\\) 可以称之为 \\(OffPE^{ann}\\).\n再来分析 \\(OffPE_1 [OffPE^{ann}, int^{ann}]\\) : \\(OffPE^{ann}\\) 中保留了 \\(OffPE_2[int^{ann}, source]\\) 时候的信息: 如果已知 \\(int^{ann}\\) 输入, \\(OffPE_2\\) 中\u0026quot;哪些表达式是 static\u0026quot;. 从而 \\(OffPE_1\\) 可以利用这些信息决定 \\(OffPE^{ann}\\) 要 specialize 成什么样.\n由于 \\(int^{ann}\\) 已经标注了\u0026quot;等source来了要如何使用\u0026quot;了, \\(OffPE^{ann}\\) 也会利用这些annotation做specialize (标注了static的就代表environment中能找到Value或者能由environemnt中的Value计算出来, 标注了dynamic的就代表environemnt中有Expression或者保持原样), 也就是说 \\(OffPE^{ann}\\) 不需要知道source是什么也能做specialize. 这个行为继承到生成的compiler中表现为生成的compiler不需要再对Enrionment中取得的值做分支判断, 这个判断已经被 \\(int^{ann}\\) 标注好了.\n为什么说失去了通用性呢? 因 为相比于 OnPE, OffPE 还限制了 self-application 所生成的 compiler 的输入: 哪些东西是 static, 哪些东西是 dynamic 需要与 \\(int^{ann}\\) 中的 annotation 保持一致.\n具体来说, 在 OffPE(int, source) 的 Off-Env 中: 待编译的源程序片段是 static 的输入, 只能作为 Value 绑定到静态变量; 而源程序输入是 dynamic 的, 只能作为 Expression 绑定到动态变量.\nRelated Partial Evaluation\nReferences [1]N. D. Jones, C. K. Gomard, and P. Sestoft, Partial Evaluation and Automatic Program Generation. USA: Prentice-Hall, Inc., 1993. ","permalink":"http://butter-xz.com/articles/20231014172106-offline_and_online_partial_evaluation/","summary":"\u003cp\u003e关于 \u003ca href=\"#citeproc_bib_item_1\"\u003e[1]\u003c/a\u003e 的第7章 \u003cem\u003eOffline and Online Partial Evaluation\u003c/em\u003e 的笔记\u003c/p\u003e\n\u003ch2 id=\"offline-和-online-指的是什么\"\u003eOffline 和 Online 指的是什么?\u003c/h2\u003e\n\u003cp\u003eOffline 和 Online 的区别在于 Binding Time Analysis 的时机:\nOffline 会提前把 \u003ca href=\"/articles/partial-evaluation-for-flow-chart/#division\"\u003eBinding Time Analysis\u003c/a\u003e (BTA)做完, 并保证 binding time 的相合性,\n再基于 BTA 的结果(可以由 Division 或 Annotated Program 表示)进行 specialize;\nOnline 会直接基于输入的程序做 specialize, 在 specialize 的过程中保证 congruence.\u003c/p\u003e\n\u003cfigure\u003e\n \u003cimg loading=\"lazy\" src=\"/ox-hugo/2023-11-05_20-22-15_screenshot.png\"\n alt=\"Figure 1: Online 方法和 Offline 方法的结构\"/\u003e \u003cfigcaption\u003e\n \u003cp\u003e\u003cspan class=\"figure-number\"\u003eFigure 1: \u003c/span\u003eOnline 方法和 Offline 方法的结构\u003c/p\u003e","title":"Offline and Online Partial Evaluation"},{"content":"ML语言的背后 众所周知, ML系语言十分强大, 这不仅仅得益于它们丰富的语义(高阶函数, local binding, lambda), 还得益于这套语义背后强大的类型系统. ML语言的类型系统通常支持十分强大类型推导功能, 强大到什么程度呢? 理论上开发者可以忽略所有类型签名, type checker仍然可以推导出所有的类型签名.\n类型推导的两个\u0026quot;端点\u0026quot; 但是, 如此强大的类型推导技术居然也不小心带来了一些负面影响:\n重要的类型签名被忽略. 很多情况下, 类型签名并不是开发者的负担而是起到了 \u0026ldquo;verified document\u0026rdquo; 的作用, 对可读性有着关键的影响. 类型系统的复杂度增加(主要是类型推导部分). 直观来看这点是在语言实现上的负面影响, 但是过于复杂的类型推导除了会提升类型检查的复杂度也会带来编程的\u0026quot;负担\u0026quot;. 比如说开发者如果不能明确知道哪些类型能推导哪些类型不能推导, 那么考虑\u0026quot;要不要加上类型签名\u0026quot;的这个问题就会给程序员带来心智负担. 对于这些负面的影响, 最极端的做法就是我们要求把所有的类型都加上, 直接抛弃类型推导功能.\n你不是说我推导不好吗, 那你自己写上吧.\n但是这又带来了新的问题:\n为所有类型写上类型签名实在是太啰嗦了, 维护一个冗长但是又没有意义的类型签名, 反而会带来编程时的心智负担. 很多类型签名实际上是\u0026quot;噪音\u0026quot;. 如果完全抛弃类型推导, 那么在一个程序中, 有可能类型签名比描述程序执行信息的核心部分还要多, 这样的类型噪音甚至反而会影响程序的可读性. 尝试找到类型推导平衡点 那么有没有一种办法设计一个类型系统, 可以在描述ML丰富的语义的同时引入\u0026quot;适量\u0026quot;的类型推导: 当显式写出类型签名的对开发者有益的时候不推导这个类型, 只选择推导\u0026quot;显式写出时无意义\u0026quot;的类型签名.\n局部类型推导(Local-Type inference)技术就诞生了\n作者总结了3种ML编程中常见的类型推导, 并通过局部类型推导技术完成这三种类型推导:\n函数调用时类型参数的推导是必要的 匿名函数的类型推导是需要的 local binding的类型推导是需要的 局部类型推导(Local-Type inference) 局部类型推导尝试在保持ML编程的前提下尽可能的减弱类型推导的能力以简化类型推导算法, 局部的含义是类型推导只用到了局部的语法树的类型信息.\n通过Local Type Argument Synthesis可以解决第一个问题, 通过双向类型检查(Bidirectional Type Checking)可以解决问题2, 3.\n语言定义 先介绍了一个用于展示的简单的语言, 是沿用的一个叫 (Kernel F≤_) 的语言, 这个语言支持subtype, 参数化多态, 匿名函数, 局部绑定.\n我觉得很有意思的一点是, 文中把同一个语言区分了两层: 分别是内部语言(internal language)和外部语言(external langauge). 内部语言是full typed language, 外部语言是程序员日常接触的语言是可以省略一些类型签名的. 而类型推导的过程就是把外部语言翻译成内部语言的过程. 这个隔离可以让\u0026quot;推导了什么东西\u0026quot;很清楚的展现.\nFigure 1: 内部语言的定义: 类型表达式, 表达式, 类型环境\nBottom Type的引入可以让任意两个类型都有最大下界, 可以让类型推导总有解.\nFigure 2: 子类型规则\n需要注意这里是有forall类型的子类型关系的, 之后理解约束生成/求解规则需要用到.\nnote: 参数类型也可以有forall, 那是不是说local type inference并没有rank-2 type的限制呢? Local Type Argument Synthesis Figure 3: Declarative Rule\n这条规则的有2个要点: 如果调用目标的类型是 \\( All(X)T \\rightarrow R \\), 并且调用的时候没有显式的给出类型参数, 那么需要通过推导 类型实参; 类型实参需要满足: 应用该类型实参后可以得到最小的函数返回类型. 但是这条规则是声明式的, 为了能够得到一个可用的类型推导算法, 我们还要知道如何得到这个\u0026quot;可以让返回值最小的类型实参\u0026quot;.\n于是就有了算法式规则.\nFigure 4: Algorithmic Rule\n该规则的算法含义是: 当需要infer \\(f(\\bar e)\\) 的类型时, 我们需要做以下几件事情:\n先infer \\(f\\) 的类型; infer 函数调用参数 \\(e\\) 的类型 \\(\\bar S\\) ; 如果 \\(f\\) 的类型是一个需要类型参数的函数(即 \\(|X| \u0026gt; 0\\)), 那么就需要类型推导推导出函数的类型参数; 把空集 \\(\\emptyset\\), 类型 \\(\\bar S\\), 形参类型 \\(\\bar S\\), 类型形参 \\(\\bar X\\), 喂给由关系 \\(\\vdash \\Rightarrow\\) 定义的函数, 从而生成类型变量 \\(\\bar X\\) 上的约束集合 \\(\\bar D\\) ; 通过运算符 \\(\\wedge\\) 合并 \\(\\bar D\\) 得到 \\(\\bar C\\) ; 求解约束 \\(\\bar C\\) 得到能够让 \\(R\\) 最小的 unifier \\(\\sigma\\) 将 \\(\\sigma\\) 再apply到 \\(\\bar X\\), 返回在internal term中填补空缺的类型实参. 可以看到, 相比与声明式规则, 算法式规则中多了 \\(C\\) 和 \\(\\sigma\\) . 其中 \\(C\\) 是在检查函数调用表达式时收集到的约束结合, \\(\\sigma\\) 是对这些约束求解的结果.\n疑惑: 这里是不是有遗漏? 应该要像声明式规则一样把 f 翻译成 f' 的过程. 很清晰对吧!\n约束生成 约束生成规则关系 \\(V \\vdash_{\\bar X} S \u0026lt;: T \\Rightarrow D \\), 描述了对于包含类型变量\\(\\bar X\\)的子类型关系 \\(S \u0026lt;: T\\) 和可以替换成任意类型的类型变量集合\\(V\\), 需要生成的关于 \\(\\bar X\\) 类型约束集合 \\(D\\). 对于任意的 \\(V\\) 的替换都能满足 \\(S \u0026lt;: T\\), 当且仅当 \\(\\bar X\\) 满足类型约束 \\(D\\).\n为什么要有一个 \\(V\\) 呢? 因为文章中用于展示的语言的类型并没有rank-2的限制, 当我们尝试为一个子类型关系生成约束时, 类型中有的变量是free的(我们需要生成约束的变量), 有的变量是被 \\(ALL\\) capture 的. 而对于这些captrured变量, 我们不知道关于它们的任何信息, 所以需要按照这个目标来生成free变量约束: 对于所有被capture的变量的替换, 子类型关系都能成立.\nFigure 5: 约束生成的关系\n这个关系中用到了两个辅助的关系 \\(\\Uparrow\\) 和 \\(\\Downarrow\\), 叫做 Variable Elimination. 这两个关系就不复杂了, 而且是完全对称的.\nFigure 6: Variable Elimination关系\n\\(S \\Uparrow^V T\\) 表示对于类型 \\(S\\), \\(T\\) 是满足以下条件的最小上界类型: \\(T\\) 大于\\(S\u0026rsquo;\\), \\(S\u0026rsquo;\\)为任意把 \\(S\\) 中的 \\(V\\) 替换为任意类型后得到的类型.\n原文中的描述更加简单, 就说要eliminate掉这些变量.\nIn the constraint generation algorithm that we present in the next section, it will sometimes be necessary to eliminate all occurrences of a certain set of variables from a given type by promoting (or demoting) the type until we reach a supertype (or subtype) in which these variables do not occur.\n约束求解 约束生成将为\\(\\bar X\\)中的每个变量生成一个上限和下限, 约束求解将给每个\\(X\\)一个具体的类型, 具体来说就是取能让返回类型最小的边界条件.\n类型变量\\(X\\)在类型\\(T\\)中出现的位置可以分为4类covariant, invariant, contravariant和constant. 个人通俗的解释是:\nCovariant: 表示类型X增长, 类型T也会随之增长(如\\(T = (\u0026hellip;) \\rightarrow X\\)); Contravariant: 表示类型X增长, 类型T也法内容会随之下降 (如\\(T = (\u0026hellip; X \u0026hellip;) \\rightarrow \u0026hellip;\\)); Invariant: X发生变化为X\u0026rsquo;后, 只要X与X\u0026rsquo;不同, T\u0026rsquo;与之前的T是不存在子类型关系的; Constant: X随便替换成什么, 替换后的类型均有子类型关系. 严谨的定义如下:\nFigure 7: covariant, invariant, contravariant和constant\nFigure 8: 约束求解\n因为有subtype的存在, 这里的约束求解与let polymorphism中的unification稍微有些不同, unification基于 \\(=\\) 约束得到unifier, 此处基于 \\(\\le\\) 约束和类型变量在返回类型中所处的位置得到unifier.\n双向类型检查(Bidirectional Checking) 双向类型检查更像是一种类型检查的风格, 把类型检查分为了两个过程: check和infer.\ncheck过程是: \u0026ldquo;已经有一个term出现在了一个需要类型T的上下文中, 我们需要check这个term是否真的为类型T\u0026rdquo;; infer过程是: \u0026ldquo;对于一个term, 不知道这个term所在的上下文需要什么类型, 我们需要在上下文中infer这个term的类型\u0026rdquo;.\n这也是Bidirectional名称的由来: 我们既可以通过check把类型信息传递给子节点, 也可以通过infer把类型信息传递给父节点.\nFigure 9: Bidirectional Checking的规则\n从规则中可以看到, 每一个term分别有两条规则, 一类是C开头的check, 另一类是S开头的Synthesis(就是Infer).\n比较复杂的是Application节点的check\u0026amp;infer:\n只有在Application节点, check会调用infer推导调用目标的类型, 然后根据该类型再check调用参数的类型是否兼容, 并生成类型兼容需要满足的约束条件: 类型信息就是这样被带到了函数调用的实际参数; 类型实参也是在这个时候被上一节描述的算法被推导出来. Abstraction的check过程就是利用了1的类型信息以推导函数的参数类型. 从规则中我们也能看到相比与let-polymorphism的不足, 在Abstraction的infer中, 我们必须写出类型参数, local-type-inference是不支持自动推导泛型参数的.\nBidirectional? 相比与原版的let polymorphism, 对于推导let binding type annotation的能力是要弱很多的, 虽然Bidirectional checking这个词是被local type inference提出的, 但是我觉得类型推导算法实际上都有类似 \u0026ldquo;bidirectional\u0026rdquo; 的过程.\n类型推导还是一个收集约束然后求解约束的过程,\n比如说在HM类型系统中, 我们需要不断的基于类型约束更新类型环境(环境如果关联了语法树上的类型信息的话, 那么语法树上的类型信息也会被更新). 这不也可以算是 environment \u0026lt;-\u0026gt; term 之间的bidirectional吗: 在environment中检查term; 在检查的过程中将收集到的约束\u0026quot;反馈\u0026quot;给environment.\nHM type system在类型检查的过程中无时无刻不在基于新发现的约束更新环境; 而local type inference不会在类型检查过程中更新类型环境, 只允许利用局部的类型信息进行推导.\n现实世界的编程语言 目前看到的编程语言, 要么采用像Haskell和OCaml这样的全局类型推导, 要么采用局部类型推导(Java, Scala), 并且都没有超过rank-2的限制(也就是说从理论上来说, local type inference还可以做更多). 然而局部类型推导是没有像let-polymorphism这样 泛化(generalize) 一个函数的能力的,\n最近出的MoonBit采用了一种以前从来没见过的方案, 我觉得挺机智:\n模块级别的函数必须手动标注类型; 局部函数可以具有类似let-polymorphism的能力 这样即可以限制类型推导的实际开销(只要\\(O(n^2)\\)的n足够小, 那就和常量差不多), 又具有let-polymorphism.\n","permalink":"http://butter-xz.com/articles/20230901095515-local_type_inference/","summary":"\u003ch2 id=\"ml语言的背后\"\u003eML语言的背后\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e众所周知\u003c/strong\u003e, ML系语言十分强大, 这不仅仅得益于它们丰富的语义(高阶函数, local binding, lambda), 还得益于这套语义背后强大的类型系统.\nML语言的类型系统通常支持十分强大类型推导功能, 强大到什么程度呢?\n理论上开发者可以忽略所有类型签名, type checker仍然可以推导出所有的类型签名.\u003c/p\u003e\n\u003ch3 id=\"类型推导的两个-端点\"\u003e类型推导的两个\u0026quot;端点\u0026quot;\u003c/h3\u003e\n\u003cp\u003e但是, 如此强大的类型推导技术居然也不小心带来了一些负面影响:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e重要的类型签名被忽略. 很多情况下, 类型签名并不是开发者的负担而是起到了 \u0026ldquo;verified document\u0026rdquo; 的作用, 对可读性有着关键的影响.\u003c/li\u003e\n\u003cli\u003e类型系统的复杂度增加(主要是类型推导部分).\n直观来看这点是在语言实现上的负面影响,\n但是过于复杂的类型推导除了会提升类型检查的复杂度也会带来编程的\u0026quot;负担\u0026quot;.\n比如说开发者如果不能明确知道哪些类型能推导哪些类型不能推导,\n那么考虑\u0026quot;要不要加上类型签名\u0026quot;的这个问题就会给程序员带来心智负担.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e对于这些负面的影响, 最极端的做法就是我们要求把所有的类型都加上, 直接抛弃类型推导功能.\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e你不是说我推导不好吗, 那你自己写上吧.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e但是这又带来了新的问题:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e为所有类型写上类型签名实在是太啰嗦了, 维护一个冗长但是又没有意义的类型签名,\n反而会带来编程时的心智负担.\u003c/li\u003e\n\u003cli\u003e很多类型签名实际上是\u0026quot;噪音\u0026quot;. 如果完全抛弃类型推导,\n那么在一个程序中, 有可能类型签名比描述程序执行信息的核心部分还要多,\n这样的类型噪音甚至反而会影响程序的可读性.\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 id=\"尝试找到类型推导平衡点\"\u003e尝试找到类型推导平衡点\u003c/h3\u003e\n\u003cp\u003e那么有没有一种办法设计一个类型系统,\n可以在描述ML丰富的语义的同时引入\u0026quot;适量\u0026quot;的类型推导:\n当显式写出类型签名的对开发者有益的时候不推导这个类型,\n只选择推导\u0026quot;显式写出时无意义\u0026quot;的类型签名.\u003c/p\u003e\n\u003cp\u003e局部类型推导(Local-Type inference)技术就诞生了\u003c/p\u003e\n\u003cp\u003e作者总结了3种ML编程中常见的类型推导, 并通过局部类型推导技术完成这三种类型推导:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e函数调用时类型参数的推导是必要的\u003c/li\u003e\n\u003cli\u003e匿名函数的类型推导是需要的\u003c/li\u003e\n\u003cli\u003elocal binding的类型推导是需要的\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"局部类型推导--local-type-inference\"\u003e局部类型推导(Local-Type inference)\u003c/h2\u003e\n\u003cp\u003e局部类型推导尝试在保持ML编程的前提下尽可能的减弱类型推导的能力以简化类型推导算法,\n局部的含义是类型推导只用到了局部的语法树的类型信息.\u003c/p\u003e","title":"Local Type Inference"},{"content":"最近经常和各类人高强度的讨论各种事情, 我在读研期间和经常需要线上/线下和我的导师讨论各种问题, 但是我也没有感受到像最近这样的疲惫感.\n我总结了一下, 主要原因还是在于讨论的时候没有确立讨论主题问题, 比如说对于一个问题X的解决方案的讨论, x提出了A解决方法, y指出了A解决方法中存在的问题P. 在听到了这些问题后, 如果x认为应该继续采用A方法, 正常的做法首先应该是论证P的影响, 比如说:\nP到底是否存在? P的影响有多大? 给问题X的解决造成了多大程度的负面影响? P是否是为了解决某个问题而必然引入的新问题? 如果P确实是一个不可忽视的问题, 然后应该讨论解决方法A的的\u0026quot;补救\u0026quot;措施:\n能否修改方案A以弥补问题P? 是否需要重新设计方案B以同时解决问题X和问题P? 如果沟通不是按照这个步骤结构化的进行, 那么必然产生心智负担. 实际沟通中常常会有如下几种情况:\n如果y提出问题P后, x通过与问题本身无关的方式否认这个问题, 比如说解决问题P不会成为我们的优势, 解决问题P也不会让别人更想用我们的产品. 这种情况相当于直接回避问题. 如果y是个负责的人, 必然会想方设法的说服x问题P的存在性. 另一方面这也会让y感觉到疲惫, 因为\u0026quot;问题P是否存在\u0026quot;和\u0026quot;解决问题P是否会成为优势\u0026quot;是两个十分不相同的事情, 所用到的知识也很有可能属于完全不同的领域, 而y在讨论前可能并没有准备好另一部分的知识. 如果x确认问题P的存在后, 但是并不直面这个问题P, 而是说谁谁的a,b,c方案是用类似于A方案这么解决的, 根本没事. 这就给y带来了心智负担: 如果y没有了解过a,b,c方案, 他还需要再去确认这些方案是否真的是如x所说, 如果不如x所说又要和x对这些a,b,c方案的做法进行对齐. 如果想用别人的方案来说明, 正常的做法应当是x首先尽量详细介绍一下a,b,c方案, 说服y这些方案确实是为了x而存在, 且真的与解法A存在同构. 最后一种情况, 也是最折磨人的情况: 那你搞一个方案解决问题X吧, 我只能想到这种解法. 工作中的分工是明确的, 此时如果y是个对产品负责的人, 必然会在自己的工作量基础上增加额外的工作量. ","permalink":"http://butter-xz.com/articles/20230825121425-disscuss-on-mainline/","summary":"\u003cp\u003e最近经常和各类人高强度的讨论各种事情,\n我在读研期间和经常需要线上/线下和我的导师讨论各种问题,\n但是我也没有感受到像最近这样的疲惫感.\u003c/p\u003e\n\u003cp\u003e我总结了一下, 主要原因还是在于讨论的时候没有确立讨论主题问题,\n比如说对于一个问题X的解决方案的讨论, x提出了A解决方法,\ny指出了A解决方法中存在的问题P.\n在听到了这些问题后, 如果x认为应该继续采用A方法,\n正常的做法首先应该是论证P的影响, 比如说:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eP到底是否存在?\u003c/li\u003e\n\u003cli\u003eP的影响有多大? 给问题X的解决造成了多大程度的负面影响?\u003c/li\u003e\n\u003cli\u003eP是否是为了解决某个问题而必然引入的新问题?\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e如果P确实是一个不可忽视的问题, 然后应该讨论解决方法A的的\u0026quot;补救\u0026quot;措施:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e能否修改方案A以弥补问题P?\u003c/li\u003e\n\u003cli\u003e是否需要重新设计方案B以同时解决问题X和问题P?\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e如果沟通不是按照这个步骤结构化的进行, 那么必然产生心智负担.\n实际沟通中常常会有如下几种情况:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e如果y提出问题P后, x通过与问题本身无关的方式否认这个问题,\n比如说解决问题P不会成为我们的优势, 解决问题P也不会让别人更想用我们的产品.\n这种情况相当于直接回避问题. 如果y是个负责的人,\n必然会想方设法的说服x问题P的存在性. 另一方面这也会让y感觉到疲惫,\n因为\u0026quot;问题P是否存在\u0026quot;和\u0026quot;解决问题P是否会成为优势\u0026quot;是两个十分不相同的事情,\n所用到的知识也很有可能属于完全不同的领域, 而y在讨论前可能并没有准备好另一部分的知识.\u003c/li\u003e\n\u003cli\u003e如果x确认问题P的存在后, 但是并不直面这个问题P, 而是说谁谁的a,b,c方案是用类似于A方案这么解决的, 根本没事.\n这就给y带来了心智负担: 如果y没有了解过a,b,c方案, 他还需要再去确认这些方案是否真的是如x所说,\n如果不如x所说又要和x对这些a,b,c方案的做法进行对齐.\n如果想用别人的方案来说明, 正常的做法应当是x首先尽量详细介绍一下a,b,c方案, 说服y这些方案确实是为了x而存在, 且真的与解法A存在同构.\u003c/li\u003e\n\u003cli\u003e最后一种情况, 也是最折磨人的情况: 那你搞一个方案解决问题X吧, 我只能想到这种解法.\n工作中的分工是明确的, 此时如果y是个对产品负责的人, 必然会在自己的工作量基础上增加额外的工作量.\u003c/li\u003e\n\u003c/ol\u003e","title":"讨论主题的重要性"},{"content":"partial evaluation 的第五章 Partial Evaluation for a First-Order Functional Language 的笔记.\n前一章通过对一个简单的 flow chart (基本块) 语言的 partial evaluation 介绍了许多 partial evaluation 的概念技巧. 一个partial evaluation算法基本可以分为以下两步:\n确定源程序每个程序点可以静态确定的状态(Binding Time Analysis); 依据这些静态状态, 把源程序的每个基本块\u0026quot;展开\u0026quot;到目标程序, 这些静态状态在目标程序中不再需要被计算, 而是直接\u0026quot;嵌入\u0026quot;到了目标程序中. 该目标程序被称之为\u0026quot;残差程序\u0026quot;(residual program). 那么对于更加复杂的语言应该如何做partial evaluation呢?\n这一章的介绍了一门比flow chart稍微强大(同时也复杂)一点的语言, 叫做Scheme0, 并展示了如何对这个语言进行partial evaluation. Scheme0仍然是采用lisp的语法, 支持全局的函数定义, 不支持高阶函数(将函数绑定至临时变量/将函数作为参数传递/将函数作为返回值), 没有副作用.\n从flow chart到Scheme0 在对Scheme0进行partial evaluation的过程中, 有哪些概念/技巧/思想是可以从flow chart语言的partial evaluation中复用的呢? 我们需不需要对一个新的语言从头设计partial evaluation算法呢?\n所幸, 绝大部分都是相似且可以复用的, 下表展示了这些可以直接对应起来的概念. 还有一些其他partial evaluation中的概念Scheme0和Flow Chart是完全没有变化的.\nFlow Chart Scheme0 解释 Program point Function\u0026rsquo;s entry Flow Chart的program point直接对应函数的入口, 是specialize过程中始终保留的东西 Global Variable Parameter Global Variable对应函数的Parameter, 也只有这里的静态值会\u0026quot;嵌入\u0026quot;至residual program Transition Compression Function\u0026rsquo;s Unfolding - Binding Time Analysis 通过抽象解释进行BTA 采用抽象解释的方式分析binding time, 此时抽象域为 参数 -\u0026gt; binding time 的partial mapping, 称之为 Binding Time Environment(BTEnv). 而binding time的序也十分简单, 就是 \\(D \\ge S\\).\n对于BTA的抽象解释, 分为了两种transfer function, 两种transfer function具有不同的含义:\n如何使用BTEnv: 在一个BTEnv中, 如何根据子表达式的binding time得到该表达式的binding bime 如何更新BTEnv: 对于在binding time environment t中求值的表达式e, 对于某个函数g的调用, 这个g的实参的binding time至少为多少. 从Bottom出发(也就是把所有的参数都初始化为Static)不断的应用上面第二个transfer function更新每个函数参数的BTEnv, 直到无法更新任何函数的BTEnv, 此时称抽象解释达到了不动点(fixpoint).\n通过Binding Time Annotation提高Specialization算法的效率 先前的Binding Time都是通过一种叫binding time envionment来表示的, 这种表示在概念上很简洁, 但是因为在使用在实际运用binding time做specialization的时候是很低效的, 因为要不断的查表来看某个变量是否是static的.\n一种提高效率的技巧是先通过binding time做一次程序变换, 变换过程中为程序的每个节点加上binding time annotation来描述BTA的分析结果, 这样在specialization 的过程中就只要看这个annotation就可以了.\n如下图所示, 每个表达式都有一个 s 或 d 来表示这个表达式是static还是dynamic的, 函数的参数列表也被拆分成了static和dynamic参数两部分. lift 表示dynamic表达式中的static部分.\n通过类型系统对BTA进行soundness check 可以把Binding Time Annotation看作是一种类型签名, 然后通过类型系统检查程序Binding Time Annotation的soundness, 类型检查规则如下:\n注意, 这里只能 检查(check) 一个BTA是否sound, 而不能 推导(infer) 出一个程序的BTA, 因为上述规则没有 为没有Binding Time Annotation的程序生成Binding Time Annotation 的能力,\nSpecialization算法 这里和之前的flow chart是十分相似的, 由于有函数的存在, 我觉得算法的表达反而更简洁了, 直接看算法还是可以理解的, 由三个函数组成:\n一个主调函数specialize, 包含输入程序 program 和表示了入口函数的static参数的值 vs_0 . 一个 很像 尾递归的函数 complete : complete将返回对 pending 中的specialized function entry进行specialization的结果; marked 包含已经specialized program point program 表示源程序, 这个参数不会改变 一个基于静态值对表达式进行程序变换的函数 reduce , reduce 的内容很多但并其实不是特别复杂, 有一点需要注意的是, 目前 calls 只会unfold dynamic parameter list为空的函数. 而 unfold strategy 其实可以很多样. 注意: BTA只区分参数是static还是dynamic, 而具体的static value的值只有在reduce函数中才会被求出. Static Bound Variation(The Trick) 在对Scheme0进行partial evaluation的过程中也会遇到Static Bound Variation的问题:\n当一个值是依赖于动态值, 但是其取值范围是静态的, 应该如何利用这样的静态信息.\n解法也是相似的(The Trick), 对包含Static Bound Variation的程序进行一次程序变换, 对取值范围中的每一个值进行一次分支判定, 从而该Static Bound Variation就可以看作Static的, 增加了可specialize的内容.\nStatic Bound Variation是高质量的self-application的关键, 因为如果不这么做的话会丢失掉很多specialize的机会, 从而让self-application生成的程序生成十分trivial的residual program(即使从语义上来说是正确的).\n在对scheme0进行PE的过程中, 因为我们选取的specialation单元是一个函数, 因此如果不想大改现有的算法的话, 需要对程序进行一些变换才能使用the trick.\n这是不是就是这个 知乎回答 中提到的 partial evaluation friendly 的意思? Function Unfolding Strategy unfold 可以消除一些函数定义让residual program变得更简洁, 比如说下面的两种函数显然可以被unfold:\n一个函数什么也没做, 只是调用另一个函数; 一个函数只被调用过一次. 之前提到的unfolding strategy只有在 calls 的目标没有动态参数的时候会进行unfold, 然而unfold strategy的其实可以是比较复杂的. unfold strategy的最终目的还是为了提升residual程序的质量(比如说消除trivial function). 而在这个过程中, 有可能因为unfold反而降低了程序质量(产生了program duplication或computation duplication), 又有各种各样的trick. 这里就不详细介绍每个strategy了, 只介绍一些通用的unfolding strategy的基本要求:\n实参的计算存在副作用, 则需要保证这些副作用的顺序\u0026amp;次数在unfold后的程序中不变; 要保证unfold策略能够停机, 不会无限的unfold; 尽量避免unfold过程中的program duplication和computation duplication. Related Notes/Resource ND.Jones\u0026rsquo;s book: Partial Evaluation的教材\nPartial Evaluation: Partial Evaluation的基本概念\nPartial Evaluation For Flow Chart Langauge: Flow chart语言的Partial Evaluation\nAbstract Interpretation: 抽象解释\n","permalink":"http://butter-xz.com/articles/partial-evaluation-for-functional/","summary":"\u003cp\u003e\u003cem\u003epartial evaluation\u003c/em\u003e 的第五章 \u003cem\u003ePartial Evaluation for a First-Order Functional Language\u003c/em\u003e 的笔记.\u003c/p\u003e\n\u003cp\u003e前一章通过对一个简单的 flow chart (基本块) 语言的 partial evaluation 介绍了许多 partial\nevaluation 的概念技巧. 一个partial evaluation算法基本可以分为以下两步:\u003cbr /\u003e\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e确定源程序每个程序点可以静态确定的状态(Binding Time Analysis);\u003c/li\u003e\n\u003cli\u003e依据这些静态状态, 把源程序的每个基本块\u0026quot;展开\u0026quot;到目标程序,\n这些静态状态在目标程序中不再需要被计算, 而是直接\u0026quot;嵌入\u0026quot;到了目标程序中.\n该目标程序被称之为\u0026quot;残差程序\u0026quot;(residual program).\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e那么对于更加复杂的语言应该如何做partial evaluation呢?\u003c/p\u003e\n\u003cp\u003e这一章的介绍了一门比flow chart稍微强大(同时也复杂)一点的语言, 叫做Scheme0,\n并展示了如何对这个语言进行partial evaluation.\nScheme0仍然是采用lisp的语法, 支持全局的函数定义,\n不支持高阶函数(将函数绑定至临时变量/将函数作为参数传递/将函数作为返回值), 没有副作用.\u003c/p\u003e\n\u003cfigure\u003e\n \u003cimg loading=\"lazy\" src=\"/ox-hugo/Pasted-image-20221127220632.png\"/\u003e \n\u003c/figure\u003e\n\n\u003ch2 id=\"从flow-chart到scheme0\"\u003e从flow chart到Scheme0\u003c/h2\u003e\n\u003cp\u003e在对Scheme0进行partial evaluation的过程中,\n有哪些概念/技巧/思想是可以从flow chart语言的partial evaluation中复用的呢? 我们需不需要对一个新的语言从头设计partial evaluation算法呢?\u003c/p\u003e","title":"Partial Evaluation for Functional Language"},{"content":"partial evaluation 的第四章 Partial Evaluation for a Flow Chart Language 的笔记.\n这一章介绍了一个叫做 Flow Chart 的语言, 是一个以基本块组成的语言. 然后通过这个语言实现了两个二村映射来介绍Partial Evaluation的技巧.\n程序和状态 然后就是程序点和程序状态的概念: 程序点就是程序执行的位置, 程序状态指的是程序执行的状态, 具体的状态的值域取决于语义的定义. 这一点和静态分析里的定义是类似的.\n程序的状态可能有很多, 但大部分的语言都可以通过 变量 - 值 的映射来表示. 其中对于每个 program point, 变量的值可以分为 static 和 dynamic 分为两类. static 表示变量的值可以在静态确定, 而 dynamic 的值只能在运行时确定. 将每个 program point 的变量区分为 static 和 dynamic 的过程叫做 division.\n基于 static 程序状态做 partial evaluation 这就引出了一个概念叫做 poly, 指的是基于输入和已知的程序点的 static 变量的值, 可以最大程度地确认多少程序点的 static 变量? 这个问题的求解结果可以通过一个由 程序点-static value 序对组成的集合来表示 poly. 这些 poly 就对应着 residual program (specialized program) 的 program point.\n一个直觉的描述是: 对于 subject program 在每个 program point, 把这个 program point 对应的 basic block 根据不同的 static state \u0026ldquo;展开\u0026quot;的结果就是 residual program; \u0026ldquo;展开\u0026quot;之后, static 的属性就\u0026quot;嵌入\u0026quot;到 residual program 中, 成为 residual program 的一部分了, 而不再通过 subject program 的属性来表达.\ndivision division 指的是在做 partial evaluation 之前, 先把程序变量做一个 Static 和 Dynamic 的区分, 因此这个过程也叫/Binding Time Analysis(BTA)/, 分析一个 value 的\u0026quot;绑定时机\u0026rdquo;.\nBTA 有一个要点是要满足 相合性(congruence),\ndivision 的结果并不一定是唯一的, 只要满足以下 congruence 条件就行:\nAny variable that depends on a dynamic variable must itself be dynamic.\n也就是标记为 Static 的变量不能依赖于 Dynamic 变量.\n这个定义其实有一些抽象. 我们来设想一种场景, 对于一个循环中的变量, 第一次循环的值是确定的, 然后在循环中会通过赋值更新这个变量(这个 Flow Chart 的语言是允许赋值的), 那么这个变量算是什么呢?\n首先, 我们先看一下相合性本身的强大, 然后我们再来介绍一些更加高级的 division 方法\nStatic 变量可能比想象的多 一个变量标记为 Static 并不等于这个变量没有被修改过, 理论上满足相合性的 BTA 可以标记的 Static 变量可能比想象的多.\n比如说一个循环的循环次数(假设为 k 次)和对变量更新的值都是可以 static 的, 那么把这个变量标记为 Static 是满足相合性的.\n一个简单的验证办法是我们可以把这个循环展开 k 次, 展开后的程序的循环变量显然都是 static 的.\n但是如果每次循环对变量更新的值仍然是 static 的, 但是循环的次数可能有无限多次, 这个变量我们应该标记为 static 吗?\n对于这种情况, 我们将变量标记为 static 是满足相合性的: 因为给定任意的 k, 我们都能确定这个变量第 k 次循环的值.\n但是, 如果把它们当作 Static 的, subject program 的 poly 将会是无限的, 从而导致根本无法有效的计算出一个 residual program (程序都是有限的). 所以, 从计算 residual program 的角度来说, 无限多种的 static variable 是没有意义的.\n所以对于这种虽然标记为 Static 满足相合性, 但是会导致在 partial evaluation 的过程中会得到无限多状态的变量, 我们只选择在 BTA 的过程中将他们标记为 Dynamic.\n这样我们就得到了一种十分简单且对任何情况都适用的处理方式: 所有麻烦的东西都当成 dynamic 就好了!\n即使某些地方这个变量的值是已知的, 为了满足相合性, 我们直接直接将这个变量标记为 Dynamic 的, 这样虽然有一些静态信息我们没有利用, 但基于这样的 division 做 partial evalutaion 仍然是安全且可用的.\n但是从 partial evaluation 最基本的直觉出发, 我们标注为 dynamic 的东西越多, 我们能够 specialize 的东西就越少, partial evaluation 所能带来的优化也就更少. 所以, 我们的 division 应该在保证相合性和可计算性的前提下尽可能的让 dynamic 少.\n那有没有什么通用的办法, 让我们给出 division 的同时, 还能保证这个 division 的 dynamic 是 最少 的?\n很可惜, 这个问题是个 不可判定 问题.\n不过书上说他们之后会给个办法给一个可接受的解(前面提到了, 合理的 division 并不唯一).\n更加高级的 division 除了全程序共用一个 division, 也有很多更加高级的 division 方式, 这又根据其目的分了不同的情况, 有的是为了利用更多的静态信息, 有的是为了生成更高质量的 residual program. 不过基本思想都是把 division 进行\u0026quot;参数化\u0026rdquo;, 以表达更复杂的计算上下文中的 static value. (这点和程序分析中的上下文敏感很相似)\nPoint-wise Division 除了全程序共用一个 division 的方法, 还可以点对点(Point-wise)的设置 division. 在这种 division 下, 每个程序点的 division 可以是不同的.\n可以通过一个程序来说明 pointwise 的含义(书中的例子):\nread(X, Y); init: X := X + 1 Y := Y - 1 cont: Y := 3 next: ... 如果程序的输入 (X, Y) 的 init division 是 (S, D), 如果使用 uniform division(全程序共用一个 division)的话, 那么即使在 next 处 Y 的值明显是已知的, 也不能把他标记为 Static 的.\nPolyvariant Division Polyvariant 的 Division 指的是: 一个 division 不仅仅和程序点有关, 还和程序如何执行到这个程序点有关. 对于一个变量, 有的执行路径下把它标注为 Static, 有的路径标注为 Dynamic. 这种 division 就可以很灵活了, 此时, division 可以由一个 flow -\u0026gt; division 的映射表示.\n此时, 一个程序点可能对应多个 division(之前的 point-wise division 是每个点对应 1 个), 所以叫 poly variant division(每个程序点对应 1 个的叫 mono variant division)\n疑问: 那可不可以再更加灵活一点呢? 比如说通过 flow -\u0026gt; state -\u0026gt; division 表示, state 是上一个程序点的状态.\nLive and Dead Division 目前的 division 只包含变量关于 Static 和 Dynamic 的划分. 但是有的时候, 这个变量在某个 program point 的值是 static 的, 但是这个变量在这个点没有被用到过(dead), 那我们可以在 residual program 的 program point 安全的抹去这些变量的值.\n因此 Live and Dead Division 也可以看作是一种 pointwise division, 前述的 pointwise division 是每个 program point 的 static value 可以不同, Live and Dead Division 是每个 program point 包含的 static value 也可以不同.\nTransition Compression 路径压缩的目的是简化生成的 residual program 的质量, 消灭掉一些没有意义的跳转, 比如说生成的程序包含从一个基本块 x := 1 到基本块 y := 1 的跳转, 那么可以把这两个基本块直接合并成一个基本块. 其本身不会对生成程序的正确性和性能造成影响(这里的性能指的是抽象机中的性能, 真实的程序由于 cache locality, 跳转的数量和距离都会对性能有影响).\n需要注意的是, Transition Compression 是有可能带来代码重复的. 比如说, 有两个基本块都跳转到统一个基本块, 但是我们把两个跳转都压缩了, 这就会带来代码重复. 需要谨慎的选择需要压缩的 transition 以避免代码爆炸的问题.\nMix 算法 Partial Evaluation 一般要做以下几件事情: 根据输入, 确定变量的 division; 也就是把变量区分为 static 和 dynamic 两类. 这一步有的是和计算 residual program 的过程是合在一起的(online) 基于 division 计算 poly, 也就是 specialized program 的 program points. 前面提到了, poly 也有很多种不同的方式 根据 poly, 生成 specialized program\n(optional) transition compression\n(optional) relabel the program 书中展示了一个完整的 mixer 程序, 做了上面的 5 件事情:\n虽然上面的所有事情这个 mix 程序都做了, 只是有一些操作是隐式的. 比如说 transition compression, 没有一个过程叫做\u0026quot;transition compression\u0026quot;, 当遇到 goto l 的时候, 直接把要插入的 basic block 更新成 l 对应的基本块就可以了.\n第一二村映射 如 Partial Evaluation 中的描述, 第一二村映射指的是将一个 interpreter 在 interpreter 的输入程序上进行 specialize, 将会得到一个可执行程序.\n一个解释器的完整定义如下:\n一个简单的程序可以被该解释器解释的程序:\n传入 mix 后, 将得到一个 specialized 的解释器\nProgram point of target 和 (Program point, static variable) of interpreter 之间的对应关系:\n第二二村映射 mix 程序可以把 program specialize 到 program 的静态可确定的程序状态 vs_0. 但是 mix 程序本身不也是程序吗? 它的输入是 program, division 和 vs_0.\n那么 mix 可不可以把 program 参数 specialize 到某个程序呢?\n答案是可以的, 也就是 mix 程序具有 应用到自己(self-application) 能力!\n把 mix 本身作为 mix 待 specilize 的程序, 然后把 mix 在解释器程序上 specialize 后就可以得到一个编译器, 这就是 第二二村映射.\n在 Mix 中利用更加高级的 division 为了简单起见, 刚刚提到 mix 算法采用的是 offline partial evaluation, 也就是先 通过 binding time analysis 算出 division(可以看到 mix 有一个 division 参数), 然后通过这个 divison 计算 residual program, 这个 division 在整个程序中是始终不变的.\n想要使用刚刚提到的更加高级的 division, 我们需要对 mix 程序进行一些修改.\n在 mix 中使用 point-wise division 在 mix 中使用 point-wise division 只需要把原来的 division 表示 改成一个记录了每个 point 的 division 的表就可以了. 每次需要知道某个 program point pp 的 division 的时候 lookup(pp, division) 就可以了.\n有了 point-wise division, live and dead 就十分简单了, 还是给每个让每个 program point 的 division 不同, 刚刚提到的 point-wise division 指的是对于每个 program point, \u0026ldquo;哪些变量是 Static, 哪些变量是 Dynamic\u0026quot;这一信息是不同的; live and dead division 指的是对于每个 program point, \u0026ldquo;需要考虑哪些变量是 Static 或 Dynamic\u0026quot;这一信息不同.\n在 mix 中使用 polyvariant division 想要在 mix 算法中应用 polyvariant 有两种思路:\n在 mix 中加入 polyvariant division 的使用逻辑, 书中的做法是给原有的由 (pp, vs) (subject program point 和 static value 组成的列表) 所表示的 specialized program point 再加上一个 division component \u0026ndash; (pp, vs, div), 以表示这个 program point 是根据具体 哪个 来 division 来 specialize 的. 不仅如此, 如何选取 specialized program point 也有讲究, 不能直接像初版的 mix 一样直接把一个 program point 的后继加入到 pending 里了, 要根据当前点的 divison 来决定后继节点和后继节点相应的 divisions. 通过 polyvariant division 先对程序进行一次变换, 得到一个有 monovariant division 的程序. Self-Application 成功的关键 刚刚提到了 mix 程序是具有通过 self-application 得到编译器的能力的, 但是如果我们简单的把写好的 mix 传给 mix 我们是得不到刚刚图示的编译器的, 虽然可以得到一个\u0026quot;正确\u0026quot;的编译器(只要 mix 程序的实现是正确的, 那么二村映射的含义也一定是正确的), 但这个编译器会有很多的问题, 图示的 mix 函数在背后把这些问题\u0026quot;偷偷\u0026quot;解决了, 只把最直观优雅的东西写了出来, 比如说以下内容, 图示算法没有显式的考虑: 假设 division 已知, 而对于 mix 这样一个复杂的程序, division 的分析并不容易. 一些 base function 有些过于强大了, 它们到底是什么? 我们应该把哪些东西作为 base function, 把哪些东西自己把实现写到 subject program 中. 简单的 binding time analysis 所得到的 static variable 太少; 仅仅由相合性得到的 division, 有很多静态信息没有利用到. (pp, static value)序对有很多, 这会在 residual program 中生成超多的跳转标签(远多余图示的生成的编译器). 这里仅仅介绍个人觉得比较有趣的第 3 点, 如何通过 Bound Static Variation 更多的静态信息(相比与仅满足相合性的程序而言).\n对于其他技巧, 请参考 Partial Evaluation教材 的 4.8 The tricks under the carpet.\nBound Static Variation(\u0026ldquo;The Trick\u0026rdquo;) 我们在 specialize 一个程序的时候, 通常会遇到一个问题: 程序中一个变量的值依赖于动态的输入, 但是其范围是已知的.\n比如说如下程序:\nx = input() array = [1,2,3] t = mod(x, 3) y = array[t] 我们希望对其进行 specialization, x 的值显然是 Dynamic, array 的值显然是 Static, 那 t 的值依赖于 x, 所以按照相合性, t 必须被标记为 dynamic.\n但是与一般的 Dynamic 不同, 我们并不是完全不知道 y 的取值的, 这里 y 只能取 1,2,3 三种值.\n对于这种 值无法确定(Static), 但是取值范围可以确定(Static) 的值, 我们可以通过一个程序变换, 在变换中直接将这个值消除掉, 从而利用 取值范围有限 这一静态信息.\nx = input() array = [1,2,3] y = case t of | 1 -\u0026gt; array[1] | 2 -\u0026gt; array[2] | 3 -\u0026gt; array[3] (其实也就是为每个可能的取值生成一个 case 啦)\n","permalink":"http://butter-xz.com/articles/partial-evaluation-for-flow-chart/","summary":"\u003cp\u003e\u003cem\u003epartial evaluation\u003c/em\u003e 的第四章 \u003cem\u003ePartial Evaluation for a Flow Chart\nLanguage\u003c/em\u003e 的笔记.\u003c/p\u003e\n\u003cp\u003e这一章介绍了一个叫做 \u003cem\u003eFlow Chart\u003c/em\u003e 的语言, 是一个以基本块组成的语言.\n然后通过这个语言实现了两个二村映射来介绍\u003ca href=\"/articles/partial-evaluation/\"\u003ePartial Evaluation\u003c/a\u003e的技巧.\u003c/p\u003e\n\u003ch2 id=\"程序和状态\"\u003e程序和状态\u003c/h2\u003e\n\u003cp\u003e然后就是程序点和程序状态的概念: 程序点就是程序执行的位置,\n程序状态指的是程序执行的状态, 具体的状态的值域取决于语义的定义.\n这一点和静态分析里的定义是类似的.\u003c/p\u003e\n\u003cp\u003e程序的状态可能有很多, 但大部分的语言都可以通过 \u003ccode\u003e变量 - 值\u003c/code\u003e 的映射来表示.\n其中对于每个 program point, 变量的值可以分为 static 和 dynamic 分为两类.\nstatic 表示变量的值可以在静态确定, 而 dynamic 的值只能在运行时确定.\n将每个 program point 的变量区分为 static 和 dynamic 的过程叫做\n\u003cem\u003edivision\u003c/em\u003e.\u003c/p\u003e\n\u003ch2 id=\"基于-static-程序状态做-partial-evaluation\"\u003e基于 static 程序状态做 partial evaluation\u003c/h2\u003e\n\u003cp\u003e这就引出了一个概念叫做 \u003cem\u003epoly\u003c/em\u003e, 指的是基于输入和已知的程序点的 static\n变量的值, 可以最大程度地确认多少程序点的 static 变量?\n这个问题的求解结果可以通过一个由 \u003ccode\u003e程序点-static value\u003c/code\u003e 序对组成的集合来表示 \u003cem\u003epoly\u003c/em\u003e.\n这些 \u003cem\u003epoly\u003c/em\u003e 就对应着 residual program (specialized program) 的 program point.\u003c/p\u003e","title":"Partial Evaluation For Flow Chart Langauge"},{"content":"partial evaluation 的第三章 Programming Languages and Interpreters 的笔记.\n这一章主要是形式化的介绍了一下 Partial Evaluation 中的基本概念: 程序, 解释和编译.\n程序只是一种表示, 程序的语义决定了程序的含义, 在这里程序的语义指的是程序执行语义的描述, 比如说 Operational Semantic, 就提供了一套指导程序应该如何进行规约(reduction)的规则.\n定义语言 = 定义抽象机 所有编程语言的语义都隐式定义了一个用于执行程序的抽象机, 可以把抽象机看作是程序的\u0026quot;解释器\u0026quot;(有的资料也称之为\u0026quot;元解释器\u0026quot;, 但我还没想明白\u0026quot;元\u0026quot;在哪里), 这也是大家经常说 所有语言本质上都是解释执行 的原因.\n解释器和解释器开销 与直接在这个编程语言的抽象机来执行不同, 用另外一个程序来实现程序的语义可以称之为 解释执行, 也就是我们常说的解释器 (注意: 解释器本身也是在解释器对应语言的抽象机上执行的).\n对于某个目标程序 P, 解释器 int 在对应抽象机上的解释程序 P 所需要的执行步数, 一般要比 P 在 P 对应的抽象机上的执行步数要多, 而且往往是成倍数的多.\n比如说, 对于一个变量的求值, 在代表程序语义的抽象机上一共只需要一步 \\(x \\rightarrow Env[ [ x ] ]\\) ; 而在解释器中, 这个求值会 对应 到 5 步:\n解释函数 (如 eval) 的调用 模式匹配判断当前表达式是一个变量 读取环境 读取变量名 读取环境中对应变量名的值 所以我们可以通过 case by case 的分析各种语义的对应解释器步数得到一个大致的倍数, 从而估算解释的开销(overhead), 这个开销被称为 解释器开销(Interpretation Overhand).\n刚刚提到了开销, 这个开销实际上是不可以忽略的, 首先, 在真实世界的编程语言中, 这个开销的倍数可能非常大; 另一方面, 当语言的实现涉及到多层中间表示的时候, 解释也会有多层, 那么这个开销的倍数也会相乘, 最终开销也会呈指数增长. 将源程序直接编译为可执行的程序(比如说机器指令)就是减少开销上的一种思路(减少解释的开销). 这个过程也叫 lowering, 把更高层的表示 lowering 到底层的表示.\n自举(bootstrapping) 自举指的是一个 compiler h 可以通过被其 compiled version t 编译, 并且得到 compiled version t.\n现在发现其实很多介绍没有把这一点讲透, 很多人都说: \u0026ldquo;自举就是编译器可以编译自己.\u0026rdquo; 让人感觉不到这其中的微妙之处: \u0026ldquo;自己编译自己有什么神奇的吗?\u0026rdquo; 实际上更完整的描述应该是: 一个编译器(compiled version)可以通过编译自己(source)*得到自己*(compiled version)\n书上通过举了个例子说明如何通过扩展 S 语言实现的 S 语言的编译器并完成自举的过程:\n首先我们已有 S 语言编译器的源码 h 和编译后的代码 t 然后我们在 h 上扩展, 得到了一个 S'=的编译器 =h' 然后用一开始的 t 把 h' 编译, 得到 t1' 3.再用 t1' 编译一遍 h' . 这里第一次用了扩展后的编译器 h' 的语义. t1' 也是 h\u0026rsquo;的 compiled program, 但因为 t1' 不是由 h\u0026rsquo;的语义编译生成的(是由=h=的语义即 t 编译生成的). 所以这里还算不上自举 4.最后用 t2' 编译 h' , 这里就有了自举了. 因为 t3' 是完全等于 t2' 的, 因为 t1' 的语义和 t2' 的 语义 相同. ","permalink":"http://butter-xz.com/articles/language-and-interpreters/","summary":"\u003cp\u003e\u003cem\u003epartial evaluation\u003c/em\u003e 的第三章 \u003cem\u003eProgramming Languages and Interpreters\u003c/em\u003e\n的笔记.\u003c/p\u003e\n\u003cp\u003e这一章主要是形式化的介绍了一下 \u003ca href=\"/articles/partial-evaluation/\"\u003ePartial Evaluation\u003c/a\u003e 中的基本概念: 程序, 解释和编译.\u003c/p\u003e\n\u003cp\u003e程序只是一种表示, 程序的语义决定了程序的含义, 在这里程序的语义指的是程序执行语义的描述,\n比如说 \u003cem\u003eOperational Semantic\u003c/em\u003e, 就提供了一套指导程序应该如何进行规约(reduction)的规则.\u003c/p\u003e\n\u003ch2 id=\"定义语言-定义抽象机\"\u003e定义语言 = 定义抽象机\u003c/h2\u003e\n\u003cp\u003e所有编程语言的语义都隐式定义了一个用于执行程序的抽象机,\n可以把抽象机看作是程序的\u0026quot;解释器\u0026quot;(有的资料也称之为\u0026quot;元解释器\u0026quot;, 但我还没想明白\u0026quot;元\u0026quot;在哪里),\n这也是大家经常说 \u003cstrong\u003e\u003cstrong\u003e所有语言本质上都是解释执行\u003c/strong\u003e\u003c/strong\u003e 的原因.\u003c/p\u003e\n\u003ch2 id=\"解释器和解释器开销\"\u003e解释器和解释器开销\u003c/h2\u003e\n\u003cp\u003e与直接在这个编程语言的抽象机来执行不同,\n用另外一个程序来实现程序的语义可以称之为 \u003cem\u003e解释执行\u003c/em\u003e, 也就是我们常说的解释器\n(注意: 解释器本身也是在解释器对应语言的抽象机上执行的).\u003c/p\u003e\n\u003cp\u003e对于某个目标程序 \u003ccode\u003eP\u003c/code\u003e, 解释器 \u003ccode\u003eint\u003c/code\u003e 在对应抽象机上的解释程序 \u003ccode\u003eP\u003c/code\u003e 所需要的执行步数,\n一般要比 \u003ccode\u003eP\u003c/code\u003e 在 \u003ccode\u003eP\u003c/code\u003e 对应的抽象机上的执行步数要多, 而且往往是成倍数的多.\u003c/p\u003e\n\u003cp\u003e比如说, 对于一个变量的求值, 在代表程序语义的抽象机上一共只需要一步 \\(x \\rightarrow Env[ [ x ] ]\\) ;\n而在解释器中, 这个求值会 \u003cstrong\u003e对应\u003c/strong\u003e 到 5 步:\u003c/p\u003e","title":"Language and Interpreters"},{"content":"关于 Partial Evaluation 的第一章 Introduction 的笔记.\n从抽象上来看, 程序都可以看作是一个输入到输出的函数. 比如说某个程序输入可以拆分为 in1 和 in2, 如果该程序的输入 in1 是可以在运行之前确定的, 那么我们就可以生成一个针对 in1 优化的程序, 这个过程就叫做 specialization(特化). 针对 in1 优化的\u0026quot;优化器\u0026quot;可以叫做 specializer. 所以 Partial Evaluation 可以看作做了两件事情:\n计算可以预先知道的输入 为提前知道的输入进行特化, 生成一个针对预先输入优化的程序 二村映射 我们通过一个解释器程序来举例子, 一个解释器可以通过下图描述:\n+--------+ P --\u0026gt; | interp | --\u0026gt; out input --\u0026gt; | | +--------+ 对于这个 interpreter, 如果有一个可以针对解释器的源程序输入 P 的 specializer, 那么我们可以通过在解释器上 specialize 得到一个可执行程序. 用图画出来就是这样, 下图中的 PE 就是我们的 specializer, interpP 就是一个可执行程序.\n图 1:\n+----+ interp --\u0026gt; | PE | --\u0026gt; interpP P --\u0026gt; | | +----+ 这个时候我们可以看到, 这个 specialzer 本身也是多参数的函数, 如果我们已经有了一个 specialzer, 如果再把这个 specializer 对 interp 参数进行 specialization, 我们就可以得到一个编译器(下图中的 PEinterp).\n图 2:\n+----+ PE --\u0026gt; | PE | --\u0026gt; PEinterp interp --\u0026gt; | | +----+ 这里的 PE 又是一个多参数的函数, 我们又可以对其进行 Specialization. 如果有一个 specialzer, 可以让 PE 对 PE 进行进行 Specialization, 那么就可以得到一个编译器的生成器(图中的 PE_PE).\n+----+ PE --\u0026gt; | PE | --\u0026gt; PE_PE PE --\u0026gt; | | +----+ 这里的每个 PE 的输入都是两个程序, 其中一个程序 in1 是另外一个程序 in2 的输入; PE 的功能都是为 in2 生成一个特化在 in1 的版本, 这个特化的版本往往会比 in1 |\u0026gt; in2 更加高效.\n二村映射就是指的以上的几个程序特化的过程,\n上面的 specialize 的过程有时候也叫 stage: 通过 specialize 把原来的程序分成了好几步.\nPartial Evaluation 和 Partial Application(柯里化) Partial Evaluation 和编程语言中的 Partial Application(柯里化)还是很不一样的 在 Partial Application 中, 得到的是相同语言中的函数; 而 partial evaluation 中, 得到的是一个 新的程序. 这里就体现了一个 program text 和 running program 的区别: program text 是一个 syntactic world 里的东西, 比如说符号, 表达式, 只是程序的 表示; function 是数学意义(语义)上的东西, 需要把 program text 传给语义函数后(通俗的说就是要把程序运行起来)才能把 program text 和 function 关联起来. 所以 partial evaluation 和 柯里化 时的 partial application 并不是一回事.\n感谢 kokic 为本文找到的 typo!\n","permalink":"http://butter-xz.com/articles/partial-evaluation/","summary":"\u003cp\u003e关于 \u003cem\u003ePartial Evaluation\u003c/em\u003e 的第一章 \u003cem\u003eIntroduction\u003c/em\u003e 的笔记.\u003c/p\u003e\n\u003cp\u003e从抽象上来看, 程序都可以看作是一个输入到输出的函数. 比如说某个程序输入可以拆分为 \u003ccode\u003ein1\u003c/code\u003e 和 \u003ccode\u003ein2\u003c/code\u003e,\n如果该程序的输入 \u003ccode\u003ein1\u003c/code\u003e 是可以在运行之前确定的,\n那么我们就可以生成一个针对 \u003ccode\u003ein1\u003c/code\u003e 优化的程序, 这个过程就叫做 specialization(特化).\n针对 \u003ccode\u003ein1\u003c/code\u003e 优化的\u0026quot;优化器\u0026quot;可以叫做 specializer.\n所以 Partial Evaluation 可以看作做了两件事情:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e计算可以预先知道的输入\u003c/li\u003e\n\u003cli\u003e为提前知道的输入进行特化, 生成一个针对预先输入优化的程序\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"二村映射\"\u003e二村映射\u003c/h2\u003e\n\u003cp\u003e我们通过一个解释器程序来举例子, 一个解释器可以通过下图描述:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e +--------+\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eP --\u0026gt; | interp | --\u0026gt; out\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003einput --\u0026gt; | |\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e +--------+\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pr","title":"Partial Evaluation"},{"content":"Lambda 表达式的 Partial Evaluation Demo 链接: https://butter-xz.com/lambda_pe/ 包含 Lambda 表达式的 Evaluation 和 Partial Evaluation(包括 Binding Time Analysis 和 Staging 算法)的实现, 并提供了一个简单的前端可以在线体验这些算法.\n源代码: https://github.com/butterunderflow/lambda_pe 相关文章: Partial Evaluation for Lambda Calculus ","permalink":"http://butter-xz.com/demos/","summary":"\u003ch1 id=\"lambda-表达式的-partial-evaluation\"\u003eLambda 表达式的 Partial Evaluation\u003c/h1\u003e\n\u003cul\u003e\n\u003cli\u003eDemo 链接: \u003ca href=\"https://butter-xz.com/lambda_pe/\"\u003ehttps://butter-xz.com/lambda_pe/\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e包含 Lambda 表达式的 Evaluation 和 Partial Evaluation(包括 Binding Time Analysis 和 Staging 算法)的实现, 并提供了一个简单的前端可以在线体验这些算法.\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e源代码: \u003ca href=\"https://github.com/butterunderflow/lambda_pe\"\u003ehttps://github.com/butterunderflow/lambda_pe\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e相关文章: \u003ca href=\"/articles/20231111153704-partial_evaluation_for_lambda_calculus/\"\u003ePartial Evaluation for Lambda Calculus\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e","title":""},{"content":"email: [email protected]\n如果您对我发的任何东西感兴趣, 欢迎随时通过邮件联系我.\n","permalink":"http://butter-xz.com/about/","summary":"\u003cp\u003eemail: \u003ca href=\"mailto:[email protected]\"\[email protected]\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e如果您对我发的任何东西感兴趣, 欢迎随时通过邮件联系我.\u003c/p\u003e","title":"About"}]