package streamable

  1. Overview
  2. Docs
Legend:
Page
Library
Module
Module type
Parameter
Class
Class type
Source

Source file ppx_streamable.ml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
open! Base
open! Import

module For_testing = struct
  module Nested_variant = Nested_variant
  module Nested_tuple = Nested_tuple
end

let streamable_name = "streamable"
let atomic_arg_name = "atomic"
let rpc_arg_name = "rpc"
let version_arg_name = "version"

module Signature = struct
  let args = Deriving.Args.(empty +> flag rpc_arg_name)

  let generate_functor type_dec ~loc ~rpc =
    let streamable_module_type = Helpers.streamable_module_type ~loc ~rpc in
    let type_parameter_module_names =
      List.mapi
        type_dec.ptype_params
        ~f:(fun index (type_parameter, (variance, injectivity)) ->
        let module_name =
          Helpers.module_name_for_type_parameter
            (match type_parameter.ptyp_desc with
             | Ptyp_var name -> `Ptyp_var name
             | Ptyp_any -> `Ptyp_any index
             | _ ->
               raise_s
                 [%message
                   "Unexpected type for type parameter"
                     [%here]
                     (string_of_core_type type_parameter)])
        in
        (type_parameter, (variance, injectivity)), module_name)
    in
    let type_dec =
      { type_dec with
        ptype_params =
          (* Replace all type parameters with the appropriate concrete types. *)
          List.map
            type_parameter_module_names
            ~f:(fun ((type_parameter, (variance, injectivity)), module_name) ->
            let core_type =
              { type_parameter with
                ptyp_desc =
                  Ptyp_constr
                    (Loc.make ~loc (Longident.Ldot (Lident module_name, "t")), [])
              }
            in
            core_type, (variance, injectivity))
      }
    in
    let functor_ =
      List.fold_right
        type_parameter_module_names
        ~init:
          (pmty_with
             ~loc
             streamable_module_type
             [ Pwith_type
                 ( Loc.make ~loc (Longident.Lident "t")
                 , { ptype_name = Loc.make ~loc "t"
                   ; ptype_params = []
                   ; ptype_cstrs = []
                   ; ptype_kind = Ptype_abstract
                   ; ptype_private = Public
                   ; ptype_manifest = Some (core_type_of_type_declaration type_dec)
                   ; ptype_attributes = []
                   ; ptype_loc = loc
                   } )
             ])
        ~f:(fun (_, module_name) functor_ ->
          pmty_functor
            ~loc
            (Named (Loc.make ~loc (Some module_name), streamable_module_type))
            functor_)
    in
    psig_module
      ~loc
      (module_declaration
         ~loc
         ~name:(Loc.make ~loc (Some Helpers.make_streamable))
         ~type_:functor_)
  ;;

  let generate ~loc ~path:(_ : label) ((_ : rec_flag), type_decs) rpc =
    (* Verify that the type declaration is valid. *)
    let type_dec = Helpers.get_the_one_and_only_type_t type_decs ~loc in
    match type_dec.ptype_params with
    | _ :: _ -> [ generate_functor type_dec ~loc ~rpc ]
    | [] ->
      let module_type = Helpers.streamable_module_type ~loc ~rpc in
      [ [%sigi: include [%m module_type] with type t := t] ]
  ;;
end

module Structure = struct
  let args =
    Deriving.Args.(
      empty
      +> flag atomic_arg_name
      +> flag rpc_arg_name
      +> arg version_arg_name (map1' ~f:Version.of_int_exn (eint __)))
  ;;

  let all_ordinary_clauses : Clause.t list =
    [ Core_primitive_clause.maybe_match
    ; Fqueue_clause.maybe_match
    ; Hashtbl_clause.maybe_match
    ; List_clause.maybe_match
    ; Map_clause.maybe_match
    ; Nonempty_list_clause.maybe_match
    ; Option_clause.maybe_match
    ; Or_error_clause.maybe_match
    ; Record_clause.maybe_match
    ; Result_clause.maybe_match
    ; Sequence_clause.maybe_match
    ; Set_clause.maybe_match
    ; Sexp_clause.maybe_match
    ; Total_map_clause.maybe_match
    ; Tuple_clause.maybe_match
    ; Type_parameter_clause.maybe_match
    ; Variant_clause.maybe_match
    ]
  ;;

  (* We attempt to match clauses in distinct groups. In each group, we expect at most one
     clause to match. If we fail to match any clauses from a given group, we move on to
     the next one. The groups are defined in the following order:

     (1) First, we just try applying [Atomic_clause]. If it matches, we should not recurse
     any further.

     (2) If that fails, we try applying all "ordinary" clauses.

     (3) If that fails, we try applying [Parameterized_type_clause]. We consider this
     separately since we'd otherwise have multiple clauses matching known parameterized
     types like [_ Option.t] within the same group.

     (4) Finally, we try applying [Module_dot_t_clause]. We consider this separately since
     we'd otherwise have multiple clauses matching known types like [Sexp.t] within the
     same group. *)
  let clause_groups_in_descending_order_of_precedence =
    [ [ Atomic_clause.maybe_match ]
    ; all_ordinary_clauses
    ; [ Parameterized_type_clause.maybe_match ]
    ; [ Module_dot_t_clause.maybe_match ]
    ]
  ;;

  let rec maybe_apply_clause maybe_match ~type_ ~loc ~rpc ~version =
    match maybe_match type_ { Ctx.loc; rpc; version } with
    | None -> None
    | Some { Clause.Match.apply_functor; children } ->
      (* We recognize the top-level structure of a type expression, now
         recurse on any arguments it may have. *)
      Some
        (apply_functor
           { loc; rpc; version }
           (List.map children ~f:(generate_streamable_module ~rpc ~version)))

  and find_at_most_one_matching_clause clause_group ~type_ ~loc ~rpc ~version =
    match
      List.filter_map clause_group ~f:(maybe_apply_clause ~type_ ~loc ~rpc ~version)
    with
    | [ module_expr ] -> Some module_expr
    | [] -> None
    | (_ : module_expr list) ->
      Location.raise_errorf
        ~loc
        "Multiple matchers satisfied type `%s' for a given clause group. This is likely \
         a bug with [ppx_streamable]."
        (Type_.human_readable_name type_)

  and generate_streamable_module type_ ~rpc ~version =
    let loc = Type_.loc type_ in
    match
      List.find_map
        clause_groups_in_descending_order_of_precedence
        ~f:(find_at_most_one_matching_clause ~type_ ~loc ~rpc ~version)
    with
    | Some module_expr -> module_expr
    | None ->
      Location.raise_errorf
        ~loc
        "Handling of type `%s' is unknown."
        (Type_.human_readable_name type_)
  ;;

  let extract_derivers type_dec =
    let extract_deriver_name_from_expr expr =
      match expr.pexp_desc with
      (* e.g. bin_io *)
      | Pexp_ident { txt = Lident name; _ } -> Some name
      (* e.g. streamable ~rpc *)
      | Pexp_apply ({ pexp_desc = Pexp_ident { txt = Lident name; _ }; _ }, _) ->
        Some name
      | _ -> None
    in
    List.concat_map type_dec.ptype_attributes ~f:(fun attribute ->
      let attribute_name = attribute.attr_name.txt in
      match
        String.(attribute_name = "deriving" || attribute_name = "deriving_inline")
      with
      | false -> []
      | true ->
        (match attribute.attr_payload with
         (* e.g. [@@deriving foo, bar] *)
         | PStr [ { pstr_desc = Pstr_eval ({ pexp_desc = Pexp_tuple exprs; _ }, _); _ } ]
           -> List.filter_map exprs ~f:extract_deriver_name_from_expr
         (* e.g. [@@deriving foo] *)
         | PStr [ { pstr_desc = Pstr_eval (expr, _); _ } ] ->
           Option.to_list (extract_deriver_name_from_expr expr)
         | _ -> []))
  ;;

  let verify_required_derivers_appear_before
    ~when_passed_args:streamable_args
    ~loc
    ~required_derivers
    ~actual_derivers
    =
    List.fold_until
      actual_derivers
      ~init:required_derivers
      ~finish:(fun (_ : Set.M(String).t) -> ())
      ~f:(fun required_derivers deriver ->
        match String.(deriver = streamable_name) with
        | false -> Continue (Set.remove required_derivers deriver)
        | true ->
          if Set.is_empty required_derivers
          then Stop ()
          else (
            let required_derivers_names =
              String.concat (Set.to_list required_derivers) ~sep:", "
            in
            let streamable_args =
              List.map streamable_args ~f:(fun arg -> "~" ^ arg) |> String.concat ~sep:" "
            in
            Helpers.unsupported_use
              ~loc
              ~why:
                [%string
                  "The following derivers must appear before %{streamable_name} \
                   %{streamable_args}: %{required_derivers_names}"]))
  ;;

  let generate_atomic type_dec ~loc ~rpc ~version =
    let required_derivers =
      match rpc with
      | true -> Set.of_list (module String) [ "bin_io" ]
      | false -> Set.of_list (module String) [ "bin_io"; "sexp" ]
    in
    let actual_derivers = extract_derivers type_dec in
    verify_required_derivers_appear_before
      ~loc
      ~required_derivers
      ~actual_derivers
      ~when_passed_args:(atomic_arg_name :: (if rpc then [ rpc_arg_name ] else []));
    let deriving_expression =
      pexp_tuple
        ~loc
        (List.map (Set.to_list required_derivers) ~f:(fun name ->
           pexp_ident ~loc (Loc.make ~loc (lident name))))
    in
    let type_t_with_deriving =
      [%stri type nonrec t = t [@@deriving [%e deriving_expression]]]
    in
    Helpers.apply_streamable_dot
      { loc; rpc; version }
      ~functor_name:"Of_atomic"
      ~arguments:[ pmod_structure ~loc [ type_t_with_deriving ] ]
  ;;

  let rec generate_for_one_type type_dec ~loc ~atomic ~rpc ~version =
    match type_dec.ptype_params with
    | _ :: _ -> [ generate_functor type_dec ~atomic ~loc ~rpc ~version ]
    | [] ->
      let module_expr =
        match atomic with
        | true -> generate_atomic type_dec ~loc ~rpc ~version
        | false -> generate_streamable_module (Type_declaration type_dec) ~rpc ~version
      in
      [ [%stri
          include
            [%m
            Helpers.apply_streamable_dot
              { loc; rpc; version }
              ~functor_name:"Remove_t"
              ~arguments:[ module_expr ]]]
      ]

  and generate_functor type_dec ~loc ~atomic ~rpc ~version =
    let streamable_module_type = Helpers.streamable_module_type ~loc ~rpc in
    let type_parameter_module_names =
      List.mapi type_dec.ptype_params ~f:(fun index (type_parameter, _) ->
        Helpers.module_name_for_type_parameter
          (match type_parameter.ptyp_desc with
           | Ptyp_var name -> `Ptyp_var name
           | Ptyp_any -> `Ptyp_any index
           | _ ->
             raise_s
               [%message
                 "Unexpected type for type parameter"
                   [%here]
                   (string_of_core_type type_parameter)]))
    in
    let functor_body =
      let type_nonrec_t =
        let concrete_t =
          ptyp_constr
            ~loc
            (Loc.make ~loc (lident "t"))
            (List.map type_parameter_module_names ~f:(fun module_name ->
               ptyp_constr
                 ~loc
                 (Loc.make ~loc (Longident.Ldot (Lident module_name, "t")))
                 []))
        in
        [%stri type nonrec t = [%t concrete_t]]
      in
      let include_streamable =
        (* Generate the body of the functor by calling [generate_for_one_type] on this
           type with all type parameters erased. *)
        generate_for_one_type
          { type_dec with ptype_params = [] }
          ~loc
          ~atomic
          ~rpc
          ~version
      in
      pmod_structure ~loc (type_nonrec_t :: include_streamable)
    in
    let functor_ =
      List.fold_right
        type_parameter_module_names
        ~init:functor_body
        ~f:(fun module_name functor_ ->
        pmod_functor
          ~loc
          (Named (Loc.make ~loc (Some module_name), streamable_module_type))
          functor_)
    in
    pstr_module
      ~loc
      (module_binding
         ~loc
         ~name:(Loc.make ~loc (Some Helpers.make_streamable))
         ~expr:functor_)
  ;;

  let generate ~loc ~path:(_ : label) ((_ : rec_flag), type_decs) atomic rpc version =
    let version =
      match version with
      | Some version -> version
      | None ->
        Helpers.unsupported_use
          ~loc
          ~why:
            [%string
              "the [~%{version_arg_name}] argument must be specified (as an integer \
               value)"]
    in
    let type_dec = Helpers.get_the_one_and_only_type_t type_decs ~loc in
    generate_for_one_type type_dec ~loc ~atomic ~rpc ~version
  ;;
end

let streamable =
  Deriving.add
    streamable_name
    ~sig_type_decl:(Deriving.Generator.make Signature.args Signature.generate)
    ~str_type_decl:
      (Deriving.Generator.make
         Structure.args
         Structure.generate
         ~attributes:[ T Attributes.atomic ])
;;
OCaml

Innovation. Community. Security.