Created
January 7, 2025 17:35
-
-
Save Guest0x0/f6f7f55e59c26ac84793117c539c3c8b to your computer and use it in GitHub Desktop.
a generic formalization & evaluation of the "refocus" technique, connecting small-step and big-step evaluators
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| module type Reduction_Theory = sig | |
| type term | |
| type value | |
| type redex | |
| val v2t : value -> term | |
| val contract : redex -> term option | |
| (* an elementary context is a single frame of elemetary context, | |
| for example: | |
| - for CBN lambda calculus, `elem_ctx` will be just `[] t` | |
| - for CBV lambda calculus, `elem_ctx` will be `[] t` or `v []` | |
| *) | |
| type elem_ctx | |
| (* plug a term into an elementary context *) | |
| val plug : elem_ctx -> term -> term | |
| (* This is the core construct of our abstraction of reduction theory: | |
| a single-step decomposition operation. | |
| The three kinds of result are: | |
| - `Value v`, indicating the term is already a value | |
| - `Redex r`, indicating the term is already a redex | |
| - `Decomp c t`, indicating that: | |
| 1. original term can be decomposed into elementary context `c` and sub term `t` | |
| 2. `t` is the next sub term to evaluate | |
| *) | |
| type decomp_result = | |
| | Value of value | |
| | Redex of redex | |
| | Decomp of elem_ctx * term | |
| val one_step_decomp : term -> decomp_result | |
| (* The operations above should satisfy several properties *) | |
| (* 1. sub term generated by [one_step_decomp] is never value: | |
| one_step_decomp t = Decomp (c, t') -> `t'` is not value | |
| This is the most crucial property, | |
| also the main difference with more common approaches. | |
| In usual implementation, decomposition will blindly dive into the first sub-term. | |
| If the sub-term is already a value, it backtrack to the caller and seek the next sub-term, | |
| until a non-value sub term is found, or the expression turn out to be already a redex. | |
| In the formalization here, instead of try-and-backtrack, | |
| we require `one_step_decomp` to inspect its sub-term | |
| and select the first non-value sub-term for us. | |
| *) | |
| (* 2. `one_step_decomp` is left inverse of `plug` for non-values: | |
| `t` is not value -> one_step_decomp (plug c t) = Decomp (c, t) | |
| It is crucial that this property does *NOT* hold for the value case. | |
| `one_step_decomp (plug c v)` should return the next sub-term to evaluate or return a redex directly. | |
| *) | |
| (* 3. evaluable forms are disjoint with values: | |
| `plug c t` is not value for all `c` and `t` | |
| *) | |
| (* 4. `v2t` and `one_step_decomp` respect each other: | |
| t = v2t v <-> one_step_decomp t = Value v | |
| *) | |
| end | |
| (* a generic, small step normalizer in direct style *) | |
| module V1 (R : Reduction_Theory) = struct | |
| type result = | |
| | V of R.value | |
| | T of R.term | |
| | Err | |
| let rec reduce t = | |
| match R.one_step_decomp t with | |
| | Value v -> V v | |
| | Redex r -> | |
| (match R.contract r with | |
| | Some t' -> T t' | |
| | None -> Err) | |
| | Decomp (ctx, t') -> | |
| (match reduce t' with | |
| | V v -> | |
| (* since decomposed sub-term is never value, | |
| the value case never occur in recursive call. | |
| So this branch is unreachable | |
| *) | |
| assert false | |
| | T t -> T (R.plug ctx t) | |
| | Err -> Err) | |
| let rec normalize t = | |
| match reduce t with | |
| | V v -> Some v | |
| | T t -> normalize t | |
| | Err -> None | |
| end | |
| (* CPS *) | |
| module V2 (R : Reduction_Theory) = struct | |
| type result = | |
| | V of R.value | |
| | T of R.term | |
| | Err | |
| let rec reduce t (k : result -> result) = | |
| match R.one_step_decomp t with | |
| | Value v -> k (V v) | |
| | Redex r -> | |
| (match R.contract r with | |
| | Some t -> k (T t) | |
| | None -> k Err) | |
| | Decomp (ctx, t') -> | |
| reduce t' (function | |
| | V v -> assert false | |
| | T t -> T (R.plug ctx t) | |
| | Err -> Err) | |
| let rec normalize t = | |
| match reduce t (fun x -> x) with | |
| | V v -> Some v | |
| | T t -> normalize t | |
| | Err -> None | |
| end | |
| (* split the error continuation out *) | |
| module V3 (R : Reduction_Theory) = struct | |
| type result = | |
| | V of R.value | |
| | T of R.term | |
| | Err | |
| type vot = V of R.value | T of R.term | |
| let rec reduce t (k : vot -> result) = | |
| match R.one_step_decomp t with | |
| | Value v -> k (V v) | |
| | Redex r -> | |
| (match R.contract r with | |
| | Some t -> k (T t) | |
| | None -> Err) | |
| | Decomp (ctx, t') -> | |
| reduce t' (function | |
| | V v -> assert false | |
| | T t -> T (R.plug ctx t)) | |
| let rec normalize t = | |
| match reduce t (function V v -> V v | T t -> T t) with | |
| | V v -> Some v | |
| | T t -> normalize t | |
| | Err -> None | |
| end | |
| (* defunctionalize continuation *) | |
| module V4 (R : Reduction_Theory) = struct | |
| type result = | |
| | V of R.value | |
| | T of R.term | |
| | Err | |
| type vot = V of R.value | T of R.term | |
| let rec reduce t (k : R.elem_ctx list) = | |
| match R.one_step_decomp t with | |
| | Value v -> continue_v k v | |
| | Redex r -> | |
| (match R.contract r with | |
| | Some t -> continue_t k t | |
| | None -> Err) | |
| | Decomp (ctx, t') -> reduce t' (ctx :: k) | |
| (* [continue_v k v = k (V v)] *) | |
| and continue_v (k : R.elem_ctx list) (v : R.value) : result = | |
| match k with | |
| | [] -> V v | |
| | ctx :: k' -> assert false | |
| (* [continue_t k t = k (T t)] *) | |
| and continue_t (k : R.elem_ctx list) (t : R.term) : result = | |
| match k with | |
| | [] -> T t | |
| | ctx :: k' -> continue_t k' (R.plug ctx t) | |
| let rec normalize t = | |
| match reduce t [] with | |
| | V v -> Some v | |
| | T t -> normalize t | |
| | Err -> None | |
| end | |
| (* 1. rewrite [normalize], lift [reduce] outside the main loop | |
| 2. [continue_t] always return [T], so we move the [T] constructor to its call site, | |
| and rename [continue_t] to [recompose] | |
| *) | |
| module V5_1 (R : Reduction_Theory) = struct | |
| type result = | |
| | V of R.value | |
| | T of R.term | |
| | Err | |
| type vot = V of R.value | T of R.term | |
| (* originally [continue_t] *) | |
| let rec recompose (k : R.elem_ctx list) (t : R.term) = | |
| match k with | |
| | [] -> t | |
| | ctx :: k' -> recompose k' (R.plug ctx t) | |
| let rec reduce t (k : R.elem_ctx list) = | |
| match R.one_step_decomp t with | |
| | Value v -> continue_v k v | |
| | Redex r -> | |
| (match R.contract r with | |
| | Some t -> T (recompose k t) | |
| | None -> Err) | |
| | Decomp (ctx, t') -> reduce t' (ctx :: k) | |
| and continue_v (k : R.elem_ctx list) (v : R.value) : result = | |
| match k with | |
| | [] -> V v | |
| | ctx :: k' -> assert false | |
| let rec normalize_loop (result : result) = | |
| match result with | |
| | V v -> Some v | |
| | T t -> normalize_loop (reduce t []) | |
| | Err -> None | |
| let normalize t = normalize_loop (reduce t []) | |
| end | |
| (* do some fixedpoint promotion/demotion to expose decompose/contract/recompose structure: | |
| [reduce] and [continue_v] together only have two base cases: | |
| * the toplevel value case [V v] | |
| * the redex case | |
| we perform lightweight fixedpoint demotion to move the redex handling code outside, | |
| and let [reduce] and [continue_v] return a new type [decomp_result]. | |
| Finally, rename [reduce] to [decompose], and now we get the decompose/contract/recompose | |
| structure of reduction semantic | |
| *) | |
| module V5_2 (R : Reduction_Theory) = struct | |
| type decomp_result = | |
| | Decomp_value of R.value | |
| | Decomp_redex of R.redex * R.elem_ctx list | |
| let rec recompose (k : R.elem_ctx list) (t : R.term) : R.term = | |
| match k with | |
| | [] -> t | |
| | ctx :: k' -> recompose k' (R.plug ctx t) | |
| (* originally [reduce] *) | |
| let rec decompose t (k : R.elem_ctx list) : decomp_result = | |
| match R.one_step_decomp t with | |
| | Value v -> continue_v k v | |
| | Redex r -> | |
| (* ===== CHANGES HERE ===== *) | |
| Decomp_redex (r, k) | |
| | Decomp (ctx, t') -> decompose t' (ctx :: k) | |
| and continue_v (k : R.elem_ctx list) (v : R.value) = | |
| match k with | |
| | [] -> | |
| (* ===== CHANGES HERE ===== *) | |
| Decomp_value v | |
| | ctx :: k' -> assert false | |
| and normalize_loop (decomp : decomp_result) = | |
| match decomp with | |
| | Decomp_value v -> Some v | |
| | Decomp_redex (r, k) -> | |
| (* ===== CHANGES HERE ===== | |
| here we perform case-of-case transformation to simplify the code, | |
| originally it should be: | |
| match | |
| (match R.contract r with | |
| | Some t -> T (recompose k t) | |
| | None -> Err) | |
| with | |
| | V v -> Some v | |
| | T t -> normalize_loop (decompose t []) | |
| | Err -> None | |
| *) | |
| (match R.contract r with | |
| | Some t -> normalize_loop (decompose (recompose k t) []) | |
| | None -> None) | |
| let normalize t = normalize_loop (decompose t []) | |
| end | |
| (* now we can see the [decompose (recompose k t)] structure, | |
| which is inefficient. | |
| we want to introduce a new function [refocus t k] | |
| that is equivant to [decompose (recompose k t) []]. | |
| Unlike existing refocus literature, in the setting here, | |
| [refocus] is highly similar to, but not exactly the same as [decompose]. | |
| In particular, [refocus] is the same as [decompose] for all **non-value** input, | |
| but differ from [decompose] in the value case. | |
| In [decompose], the value case is unreachable with non-empty continuation. | |
| However, in [refocus], the value + non-empty continuation case may happen. | |
| Our [refocus] function is the same as usual presentation, and the difference lies in [decompose]. | |
| As mentioned in the comments in [Reduction_Theory], | |
| [decompose] here does not perform back tracking on value case, | |
| and this backtracking turns out to be exactly the extra bit of work [refocus] need to do! | |
| That explains why [refocus] and [decompose] coindicide in usual presentation. | |
| *) | |
| module V6 (R : Reduction_Theory) = struct | |
| type decomp_result = | |
| | Decomp_value of R.value | |
| | Decomp_redex of R.redex * R.elem_ctx list | |
| let rec refocus t (k : R.elem_ctx list) : decomp_result = | |
| match R.one_step_decomp t with | |
| | Value v -> continue_v k v | |
| | Redex r -> Decomp_redex (r, k) | |
| | Decomp (ctx, t') -> refocus t' (ctx :: k) | |
| and continue_v (k : R.elem_ctx list) (v : R.value) = | |
| match k with | |
| | [] -> Decomp_value v | |
| | ctx :: k' -> | |
| (* It is necessary to plug one layer before continuating [refocus] here. | |
| Otherwise we will get a trivial infinite loop. | |
| The intuition here of [refocus (plug ctx v) k'] here | |
| is to seek the next sub-term to evaluate. | |
| Instead of going all the way up with [recompose] and then dive down again, | |
| we only plug one layer of elementary context here. | |
| Since [plug ctx v] is never value, we know that [refocus (plug ctx v) k'] | |
| will always give us a redex or the next sub-term. | |
| *) | |
| refocus (R.plug ctx (R.v2t v)) k' | |
| and normalize_loop (decomp : decomp_result) = | |
| match decomp with | |
| | Decomp_value v -> Some v | |
| | Decomp_redex (r, k) -> | |
| (match R.contract r with | |
| | Some t -> normalize_loop (refocus t k) | |
| | None -> None) | |
| let normalize t = normalize_loop (refocus t []) | |
| end | |
| (* Now we give a paper-and-pen proof of the refocus theorem. | |
| A formalized proof can be found in the Coq file. | |
| Theorem refocus: | |
| refocus t k = decompose (recompose k t) [] | |
| To prove the theorem, we need to first prove several lemmas: | |
| Lemma 1: `refocus` is the same as `decompose` for non-value: | |
| forall t k, `t` is not value -> refocus t k = decompose t k | |
| Proof: simple induction. | |
| Lemma 2: non-value part of refocus theorem: | |
| forall t, `t` is not value -> refocus t k = decompose (recompose k t) [] | |
| Proof: induction on `k`: | |
| (1). [k = []]: | |
| right | |
| = decompose t [] | |
| = refocus t [] (Lemma 1) | |
| = left | |
| (2). [k = ctx :: k']: | |
| since `t` is not value, we have: | |
| right | |
| = decompose (recompose k' (plug ctx t)) | |
| = refocus (plug ctx t) k' (IH, `plug` is never value) | |
| = refocus t (ctx :: k') (`one_step_decomp` is inverse of `plug` for non-value) | |
| = left | |
| Lemma 3: value part of refocus theorem: | |
| forall v, refocus (v2t v) k = decompose (recompose k (v2t v)) [] | |
| Proof: induction on `k`: | |
| (1). [k = []]: immediate | |
| (2). [k = ctx :: k']: | |
| right | |
| = decompose (recompose k' (plug ctx (v2t v))) [] | |
| = refocus (plug ctx (v2t v)) k' (Lemma 2) | |
| = left | |
| Theorem refocus: | |
| refocus t k = decompose (recompose k t) [] | |
| Proof: immediate from Lemma 2 and Lemma 3 | |
| Notice that the value case still only occur at toplevel in [refocus]. | |
| The difference with [decompose] is that the continuation may now be non-empty at toplevel. | |
| This explains two things: | |
| 1. why [refocus] terminates in the value case | |
| 2. why we need to prove Lemma 2 first, and uses Lemma 2 in the proof of Lemma 3 | |
| *) | |
| (* Now, we finish the last steps of turning the refocus'ed implementation | |
| into a bit-step, direct style evaluator | |
| *) | |
| (* do lightweight fixedpoint promotion: fuse [normalize_loop] into [refocus] *) | |
| module V7 (R : Reduction_Theory) = struct | |
| (* originally [refocus] *) | |
| let rec normalize_aux t (k : R.elem_ctx list) = | |
| match R.one_step_decomp t with | |
| | Value v -> continue_v k v | |
| | Redex r -> | |
| (match R.contract r with | |
| | Some t -> normalize_aux t k | |
| | None -> None) | |
| | Decomp (ctx, t') -> normalize_aux t' (ctx :: k) | |
| and continue_v (k : R.elem_ctx list) (v : R.value) = | |
| match k with | |
| | [] -> Some v | |
| | ctx :: k' -> normalize_aux (R.plug ctx (R.v2t v)) k' | |
| let normalize t = normalize_aux t [] | |
| end | |
| (* refunctionalize the continuation *) | |
| module V8 (R : Reduction_Theory) = struct | |
| let rec normalize_aux t k = | |
| match R.one_step_decomp t with | |
| | Value v -> k v | |
| | Redex r -> | |
| (match R.contract r with | |
| | Some t -> normalize_aux t k | |
| | None -> None) | |
| | Decomp (ctx, t') -> normalize_aux t' (fun v -> normalize_aux (R.plug ctx (R.v2t v)) k) | |
| let normalize t = normalize_aux t (fun v -> Some v) | |
| end | |
| (* back to direct style *) | |
| module V9 (R : Reduction_Theory) = struct | |
| exception Stuck | |
| let rec normalize_aux t = | |
| match R.one_step_decomp t with | |
| | Value v -> v | |
| | Redex r -> | |
| (match R.contract r with | |
| | Some t -> normalize_aux t | |
| | None -> raise Stuck) | |
| | Decomp (ctx, t') -> | |
| let v = normalize_aux t' in | |
| normalize_aux (R.plug ctx (R.v2t v)) | |
| let normalize t = try Some (normalize_aux t) with Stuck -> None | |
| end | |
| (* example 1: simple arithmetic expression *) | |
| module Arith = struct | |
| module Theory = struct | |
| type op = Add | Sub | |
| type term = | |
| | Lit of int | |
| | Op of term * op * term | |
| type value = int | |
| let v2t v = Lit v | |
| type redex = value * op * value | |
| let contract (x, op, y) = | |
| match op with | |
| | Add -> Some (Lit (x + y)) | |
| | Sub -> | |
| if x < y | |
| then None | |
| else Some (Lit (x - y)) | |
| type elem_ctx = | |
| | L of op * term | |
| | R of value * op | |
| let plug ctx t = | |
| match ctx with | |
| | L (op, r) -> Op (t , op, r) | |
| | R (x, op) -> Op (Lit x, op, t) | |
| type decomp_result = | |
| | Value of value | |
| | Redex of redex | |
| | Decomp of elem_ctx * term | |
| let one_step_decomp t = | |
| match t with | |
| | Lit i -> Value i | |
| | Op (Lit x, op, Lit y) -> Redex (x, op, y) | |
| | Op (Lit x, op, r ) -> Decomp (R (x, op), r) | |
| | Op (l , op, r ) -> Decomp (L (op, r), l) | |
| end | |
| module Reduce = V1(Theory) | |
| module Eval = V9(Theory) | |
| open Theory | |
| let lit i = Lit i | |
| let (<+>) x y = Op (x, Add, y) | |
| let (<->) x y = Op (x, Sub, y) | |
| let test expr ~expected = | |
| let r1 = Reduce.normalize expr in | |
| assert (r1 = expected); | |
| let r2 = Eval.normalize expr in | |
| assert (r2 = expected) | |
| ;; | |
| let run_tests () = | |
| test ~expected:(Some 42) ((lit 17 <+> lit 4) <+> (lit 25 <-> lit 4)); | |
| test ~expected:None (lit 1 <+> lit 2 <+> (lit 2 <-> lit 3)) | |
| ;; | |
| end | |
| let () = Arith.run_tests () | |
| (* example 2: | |
| call-by-value lambda calculus (de bruijn index + explicit substitution) | |
| + natural number | |
| *) | |
| module Lambda = struct | |
| module Theory = struct | |
| (* term without explicit substitution *) | |
| type term0 = | |
| | Lit0 of int | |
| | Succ0 | |
| | Var0 of int | |
| | Lam0 of term0 | |
| | App0 of term0 * term0 | |
| type term = | |
| | Lit of int | |
| | Succ | |
| | App of term * term | |
| | Sub of term0 * subst | |
| and value = | |
| | V_Lit of int | |
| | V_Succ | |
| | V_Clo of term0 * subst | |
| and subst = value list | |
| let v2t v = | |
| match v with | |
| | V_Lit i -> Lit i | |
| | V_Succ -> Succ | |
| | V_Clo (t, sub) -> Sub (Lam0 t, sub) | |
| type elem_ctx = | |
| | App_L of term | |
| | App_R of value | |
| let plug ctx t = | |
| match ctx with | |
| | App_L arg -> App (t, arg) | |
| | App_R fn -> App (v2t fn, t) | |
| type redex = | |
| | R_Sub_Var of int * subst | |
| | R_Sub_App of term0 * term0 * subst | |
| | R_Beta of value * value | |
| let contract r = | |
| match r with | |
| | R_Sub_Var (i, sub) -> List.nth_opt sub i |> Option.map v2t | |
| | R_Sub_App (f, a, sub) -> Some (App (Sub (f, sub), Sub (a, sub))) | |
| | R_Beta (V_Succ , V_Lit i) -> Some (Lit (i + 1)) | |
| | R_Beta (V_Clo (t, env), v) -> Some (Sub (t, v :: env)) | |
| | R_Beta _ -> None | |
| type decomp_result = | |
| | Value of value | |
| | Redex of redex | |
| | Decomp of elem_ctx * term | |
| let as_value t : value option = | |
| match t with | |
| | Lit i -> Some (V_Lit i) | |
| | Succ -> Some (V_Succ) | |
| | Sub (Lam0 t, sub) -> Some (V_Clo (t, sub)) | |
| | Sub (Lit0 i, sub) -> Some (V_Lit i) | |
| | Sub (Succ0 , sub) -> Some V_Succ | |
| | _ -> None | |
| let one_step_decomp t = | |
| match t with | |
| | Lit i -> Value (V_Lit i) | |
| | Succ -> Value (V_Succ) | |
| | Sub (Lit0 i , sub) -> Value (V_Lit i) | |
| | Sub (Succ0 , sub) -> Value V_Succ | |
| | Sub (Var0 x , sub) -> Redex (R_Sub_Var (x, sub)) | |
| | Sub (App0 (f, a), sub) -> Redex (R_Sub_App (f, a, sub)) | |
| | Sub (Lam0 t , sub) -> Value (V_Clo (t, sub)) | |
| | App (f, a) -> | |
| (match as_value f with | |
| | Some f -> | |
| (match as_value a with | |
| | Some a -> Redex (R_Beta (f, a)) | |
| | None -> Decomp (App_R f, a)) | |
| | None -> Decomp (App_L a, f)) | |
| end | |
| module Reduce = V1(Theory) | |
| module Eval = V9(Theory) | |
| open Theory | |
| let lit n = fun level -> Lit0 n | |
| let succ = fun level -> Succ0 | |
| let lam t = fun level -> Lam0 (t (fun level' -> Var0 (level' - level - 1)) (level + 1)) | |
| let app f a = fun level -> App0 (f level, a level) | |
| let app2 f x y = app (app f x) y | |
| let mk_term pre_term = Sub (pre_term 0, []) | |
| let church n = | |
| let rec church_aux succ zero n = | |
| match n with | |
| | 0 -> zero | |
| | n -> app succ (church_aux succ zero (n - 1)) | |
| in | |
| lam @@ fun succ -> lam @@ fun zero -> church_aux succ zero n | |
| let church_add = | |
| lam @@ fun x -> | |
| lam @@ fun y -> | |
| lam @@ fun succ -> | |
| lam @@ fun zero -> | |
| app2 x succ (app2 y succ zero) | |
| let church_mul = | |
| lam @@ fun x -> | |
| lam @@ fun y -> | |
| lam @@ fun succ -> | |
| lam @@ fun zero -> | |
| app2 x (lam @@ fun acc -> app2 y succ acc) zero | |
| let test pre_term ~expected = | |
| let t = mk_term pre_term in | |
| let r1 = Reduce.normalize t in | |
| assert (r1 = expected); | |
| let r2 = Eval.normalize t in | |
| assert (r2 = expected) | |
| ;; | |
| let run_test () = | |
| test | |
| ~expected:(Some (V_Lit 42)) | |
| (app2 (app2 church_add (church 40) (church 2)) succ (lit 0)); | |
| test | |
| ~expected:(Some (V_Lit 42)) | |
| (app2 | |
| (app2 church_mul (church 3) (app2 church_add (church 9) (church 5))) | |
| succ | |
| (lit 0)); | |
| test | |
| ~expected:None | |
| (app2 (lam @@ fun x -> lam @@ fun y -> app x y) (lit 1) (lit 2)) | |
| ;; | |
| end | |
| let () = Lambda.run_test () |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment