| 
 | 1 | +open Parsetree  | 
 | 2 | +open Longident  | 
 | 3 | + | 
 | 4 | +(*  | 
 | 5 | +   Optimise calls to Option.forEach/map/flatMap so they produce the same switch  | 
 | 6 | +   structure as handwritten code. We only rewrite calls whose callback is a  | 
 | 7 | +   simple literal lambda or identifier; more complex callbacks are left intact  | 
 | 8 | +   to preserve ReScript's call-by-value semantics.  | 
 | 9 | +*)  | 
 | 10 | + | 
 | 11 | +let value_name = "__res_option_value"  | 
 | 12 | + | 
 | 13 | +type option_call = ForEach | Map | FlatMap  | 
 | 14 | + | 
 | 15 | +(* Inlineable callbacks are bare identifiers (possibly wrapped in coercions or  | 
 | 16 | +   type annotations). Those can be applied directly inside the emitted switch  | 
 | 17 | +   without introducing a let-binding that might change evaluation behaviour. *)  | 
 | 18 | +let rec callback_is_inlineable expr =  | 
 | 19 | +  match expr.pexp_desc with  | 
 | 20 | +  | Pexp_ident _ -> true  | 
 | 21 | +  | Pexp_constraint (inner, _) | Pexp_coerce (inner, _, _) ->  | 
 | 22 | +    callback_is_inlineable inner  | 
 | 23 | +  | _ -> false  | 
 | 24 | + | 
 | 25 | +(* Detect literal lambdas (ignoring type annotations) so we can reuse their  | 
 | 26 | +   argument binder in the rewritten switch. *)  | 
 | 27 | +let rec inline_lambda expr =  | 
 | 28 | +  match expr.pexp_desc with  | 
 | 29 | +  | Pexp_constraint (inner, _) | Pexp_coerce (inner, _, _) ->  | 
 | 30 | +    inline_lambda inner  | 
 | 31 | +  | Pexp_fun {arg_label = Asttypes.Nolabel; lhs; rhs; async = false} ->  | 
 | 32 | +    Some (lhs, rhs)  | 
 | 33 | +  | _ -> None  | 
 | 34 | + | 
 | 35 | +let transform (expr : Parsetree.expression) : Parsetree.expression =  | 
 | 36 | +  match expr.pexp_desc with  | 
 | 37 | +  | Pexp_apply  | 
 | 38 | +      {  | 
 | 39 | +        funct =  | 
 | 40 | +          {  | 
 | 41 | +            pexp_desc =  | 
 | 42 | +              Pexp_ident  | 
 | 43 | +                {txt = Ldot (Lident ("Option" | "Stdlib_Option"), fname)};  | 
 | 44 | +          };  | 
 | 45 | +        args = [(_, opt_expr); (_, func_expr)];  | 
 | 46 | +      } -> (  | 
 | 47 | +    let call_kind =  | 
 | 48 | +      match fname with  | 
 | 49 | +      | "forEach" -> Some ForEach  | 
 | 50 | +      | "map" -> Some Map  | 
 | 51 | +      | "flatMap" -> Some FlatMap  | 
 | 52 | +      | _ -> None  | 
 | 53 | +    in  | 
 | 54 | +    match call_kind with  | 
 | 55 | +    | None -> expr  | 
 | 56 | +    | Some call_kind -> (  | 
 | 57 | +      let loc_ghost = {expr.pexp_loc with loc_ghost = true} in  | 
 | 58 | +      let emit_option_match value_pat result_expr =  | 
 | 59 | +        let some_rhs =  | 
 | 60 | +          match call_kind with  | 
 | 61 | +          | ForEach | FlatMap -> result_expr  | 
 | 62 | +          | Map ->  | 
 | 63 | +            Ast_helper.Exp.construct ~loc:loc_ghost  | 
 | 64 | +              {txt = Lident "Some"; loc = loc_ghost}  | 
 | 65 | +              (Some result_expr)  | 
 | 66 | +        in  | 
 | 67 | +        let none_rhs =  | 
 | 68 | +          match call_kind with  | 
 | 69 | +          | ForEach ->  | 
 | 70 | +            Ast_helper.Exp.construct ~loc:loc_ghost  | 
 | 71 | +              {txt = Lident "()"; loc = loc_ghost}  | 
 | 72 | +              None  | 
 | 73 | +          | Map | FlatMap ->  | 
 | 74 | +            Ast_helper.Exp.construct ~loc:loc_ghost  | 
 | 75 | +              {txt = Lident "None"; loc = loc_ghost}  | 
 | 76 | +              None  | 
 | 77 | +        in  | 
 | 78 | +        let mk_case ctor payload rhs =  | 
 | 79 | +          {  | 
 | 80 | +            Parsetree.pc_bar = None;  | 
 | 81 | +            pc_lhs =  | 
 | 82 | +              Ast_helper.Pat.construct ~loc:loc_ghost  | 
 | 83 | +                {txt = Lident ctor; loc = loc_ghost}  | 
 | 84 | +                payload;  | 
 | 85 | +            pc_guard = None;  | 
 | 86 | +            pc_rhs = rhs;  | 
 | 87 | +          }  | 
 | 88 | +        in  | 
 | 89 | +        let some_case = mk_case "Some" (Some value_pat) some_rhs in  | 
 | 90 | +        let none_case = mk_case "None" None none_rhs in  | 
 | 91 | +        let transformed =  | 
 | 92 | +          Ast_helper.Exp.match_ ~loc:loc_ghost opt_expr [some_case; none_case]  | 
 | 93 | +        in  | 
 | 94 | +        {  | 
 | 95 | +          transformed with  | 
 | 96 | +          pexp_loc = expr.pexp_loc;  | 
 | 97 | +          pexp_attributes = expr.pexp_attributes;  | 
 | 98 | +        }  | 
 | 99 | +      in  | 
 | 100 | +      match inline_lambda func_expr with  | 
 | 101 | +      (* Literal lambda with a simple binder: reuse the binder directly inside  | 
 | 102 | +         the generated switch, so the body runs exactly once with the option's  | 
 | 103 | +         payload. *)  | 
 | 104 | +      | Some ({ppat_desc = Parsetree.Ppat_var {txt}}, body) ->  | 
 | 105 | +        let value_pat =  | 
 | 106 | +          Ast_helper.Pat.var ~loc:loc_ghost {txt; loc = loc_ghost}  | 
 | 107 | +        in  | 
 | 108 | +        emit_option_match value_pat body  | 
 | 109 | +      (* Callback is a simple identifier (possibly annotated). Apply it inside  | 
 | 110 | +         the switch so evaluation order matches handwritten code. *)  | 
 | 111 | +      | _ when callback_is_inlineable func_expr ->  | 
 | 112 | +        let value_pat =  | 
 | 113 | +          Ast_helper.Pat.var ~loc:loc_ghost {txt = value_name; loc = loc_ghost}  | 
 | 114 | +        in  | 
 | 115 | +        let value_ident =  | 
 | 116 | +          Ast_helper.Exp.ident ~loc:loc_ghost  | 
 | 117 | +            {txt = Lident value_name; loc = loc_ghost}  | 
 | 118 | +        in  | 
 | 119 | +        let apply_callback =  | 
 | 120 | +          Ast_helper.Exp.apply ~loc:loc_ghost func_expr  | 
 | 121 | +            [(Asttypes.Nolabel, value_ident)]  | 
 | 122 | +        in  | 
 | 123 | +        emit_option_match value_pat apply_callback  | 
 | 124 | +      (* Complex callbacks are left as-is so we don't change when they run. *)  | 
 | 125 | +      | _ -> expr))  | 
 | 126 | +  | _ -> expr  | 
0 commit comments