First stab at hh_check
[hiphop-php.git] / hphp / hack / src / hh_oxidize / convert_type_decl.ml
blobef5056a99168e1d47a829998d1c1627c91dbe4f7
1 (*
2 * Copyright (c) Facebook, Inc. and its affiliates.
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the "hack" directory of this source tree.
7 *)
9 open Core_kernel
10 open Asttypes
11 open Longident
12 open Parsetree
13 open Printf
14 open Utils
15 open Output
16 open State
17 open Convert_longident
18 open Convert_type
20 let default_derives =
22 (None, "Clone");
23 (None, "Debug");
24 (None, "Eq");
25 (None, "Hash");
26 (None, "Ord");
27 (None, "PartialEq");
28 (None, "PartialOrd");
29 (Some "ocamlrep_derive", "OcamlRep");
30 (Some "serde", "Serialize");
31 (Some "serde", "Deserialize");
34 let derive_blacklists =
36 (* A custom implementation of Ord for Error_ matches the sorting behavior of
37 errors in OCaml. *)
38 ("errors::Error_", ["Ord"; "PartialOrd"]);
39 (* GlobalOptions contains a couple floats, which only implement PartialEq
40 and PartialOrd, and do not implement Hash. *)
41 ("global_options::GlobalOptions", ["Eq"; "Hash"; "Ord"]);
42 (* And GlobalOptions is used in Genv which is used in Env. We
43 * don't care about comparison or hashing on environments *)
44 ("typing_env_types::Env", ["Eq"; "Hash"; "Ord"]);
45 ("typing_env_types::Genv", ["Eq"; "Hash"; "Ord"]);
47 |> List.fold ~init:SMap.empty ~f:(fun map (ty, bl) -> SMap.add ty bl map)
49 let derived_traits ty =
50 let ty = sprintf "%s::%s" (curr_module_name ()) ty in
51 match SMap.find_opt ty derive_blacklists with
52 | None -> default_derives
53 | Some blacklist ->
54 List.filter default_derives ~f:(fun (_, derive) ->
55 not (List.mem blacklist derive ~equal:( = )))
57 let blacklisted_types =
59 ("decl_defs", "Linearization");
60 ("typing_defs", "ExpandEnv");
61 ("typing_defs", "PhaseTy");
64 (* HACK: ignore anything beginning with the "decl" or "locl" prefix, since the
65 oxidized version of Ty does not have a phase. *)
66 let blacklisted_type_prefixes =
68 ("typing_defs", "Decl");
69 ("typing_defs_core", "Decl");
70 ("typing_defs", "Locl");
71 ("typing_defs_core", "Locl");
74 (* HACK: Typing_reason is usually aliased to Reason, so we have lots of
75 instances of Reason.t. Since we usually convert an identifier like Reason.t
76 to reason::Reason, the actual type needs to be renamed to the common alias.
77 This looks nicer anyway. *)
78 let renamed_types = [(("typing_reason", "TypingReason"), "Reason")]
80 (* By default, when we see an alias to a tuple type, we will assume the alias
81 adds some meaning, and generate a new tuple struct type named after the
82 alias. In some cases, the alias adds no meaning and we should also use an
83 alias in Rust. *)
84 let tuple_aliases = [("ast_defs", "Pstring"); ("errors", "Message")]
87 A list of (<module>, <ty1>) where ty1 is enum and all non-empty variant fields should
88 be wrapped by Box to keep ty1 size down.
90 let box_variant = [("aast", "Expr_"); ("aast", "Stmt_"); ("aast", "Def")]
92 let should_box_variant ty =
93 List.mem box_variant (curr_module_name (), ty) ~equal:( = )
95 let add_rcoc = [("aast", "Nsenv")]
97 let should_add_rcoc ty =
98 List.mem add_rcoc (curr_module_name (), ty) ~equal:( = )
100 let override_field_type =
101 SMap.of_list
103 ("Fun_", SMap.of_list [("doc_comment", "Option<doc_comment::DocComment>")]);
104 ( "Method_",
105 SMap.of_list [("doc_comment", "Option<doc_comment::DocComment>")] );
106 ( "Class_",
107 SMap.of_list [("doc_comment", "Option<doc_comment::DocComment>")] );
108 ( "ClassConst",
109 SMap.of_list [("doc_comment", "Option<doc_comment::DocComment>")] );
110 ( "ClassTypeconst",
111 SMap.of_list [("doc_comment", "Option<doc_comment::DocComment>")] );
112 ( "ClassVar",
113 SMap.of_list [("doc_comment", "Option<doc_comment::DocComment>")] );
114 ( "RecordDef",
115 SMap.of_list [("doc_comment", "Option<doc_comment::DocComment>")] );
118 let get_overrides name =
119 match SMap.find_opt name override_field_type with
120 | None -> SMap.empty
121 | Some x -> x
123 let blacklisted ty_name =
124 let ty = (curr_module_name (), ty_name) in
125 List.mem blacklisted_types ty ~equal:( = )
126 || List.exists blacklisted_type_prefixes ~f:(fun (mod_name, prefix) ->
127 mod_name = curr_module_name () && String.is_prefix ty_name ~prefix)
129 let rename ty_name =
130 List.find renamed_types ~f:(fun (x, _) -> x = (curr_module_name (), ty_name))
131 |> Option.value_map ~f:snd ~default:ty_name
133 let should_use_alias_instead_of_tuple_struct ty_name =
134 List.mem tuple_aliases (curr_module_name (), ty_name) ~equal:( = )
136 let doc_comment_of_attribute attr =
137 match attr with
138 | ({ txt = "ocaml.doc"; _ }, PStr structure_items) ->
139 List.find_map structure_items (fun structure_item ->
140 match structure_item.pstr_desc with
141 | Pstr_eval
142 ({ pexp_desc = Pexp_constant (Pconst_string (doc, _)); _ }, _) ->
143 Some doc
144 | _ -> None)
145 | _ -> None
147 let convert_doc_comment doc =
149 |> String.strip ~drop:(function
150 | '*'
151 | ' '
152 | '\n'
153 | '\t' ->
154 true
155 | _ -> false)
156 |> String.split ~on:'\n'
157 |> List.fold
158 ~init:(false, [])
159 ~f:(fun (was_in_code_block, lines) original_line ->
160 (* Remove leading whitespace *)
161 let lstripped = String.lstrip original_line in
162 let maybe_chop_prefix prefix s =
163 String.chop_prefix s ~prefix |> Option.value ~default:s
165 (* Remove leading asterisk and one space after, if present *)
166 let no_asterisk =
167 lstripped |> maybe_chop_prefix "*" |> maybe_chop_prefix " "
169 let now_in_code_block =
170 if String.is_prefix ~prefix:"```" (String.lstrip no_asterisk) then
171 not was_in_code_block
172 else
173 was_in_code_block
175 let line =
176 if now_in_code_block && was_in_code_block && lstripped = no_asterisk
177 then
178 sprintf "///%s\n" original_line
179 else
180 sprintf "/// %s\n" no_asterisk
182 (now_in_code_block, line :: lines))
183 |> (fun (_, l) -> List.rev l)
184 |> String.concat
186 let doc_comment_of_attribute_list attrs =
187 attrs
188 |> List.find_map ~f:doc_comment_of_attribute
189 |> Option.map ~f:convert_doc_comment
190 |> Option.value ~default:""
192 let type_param (ct, _) = core_type ct
194 let type_params params =
195 if List.is_empty params then
197 else
198 params |> map_and_concat ~f:type_param ~sep:", " |> sprintf "<%s>"
200 let record_label_declaration ?(pub = false) ?(prefix = "") overrides ld =
201 let doc = doc_comment_of_attribute_list ld.pld_attributes in
202 let pub =
203 if pub then
204 "pub "
205 else
208 let name =
209 ld.pld_name.txt |> String.chop_prefix_exn ~prefix |> convert_field_name
211 let ty =
212 match SMap.find_opt name overrides with
213 | None -> core_type ld.pld_type
214 | Some x -> x
216 sprintf "%s%s%s: %s,\n" doc pub name ty
218 let record_declaration ?(pub = false) overrides labels =
219 let prefix =
220 labels |> List.map ~f:(fun ld -> ld.pld_name.txt) |> common_prefix_of_list
222 (* Only remove a common prefix up to the last underscore (if a record has
223 fields x_bar and x_baz, we want to remove x_, not x_ba). *)
224 let prefix =
225 let idx = ref (String.length prefix) in
226 while !idx > 1 && prefix.[!idx - 1] <> '_' do
227 idx := !idx - 1
228 done;
229 String.sub prefix 0 !idx
231 labels
232 |> map_and_concat ~f:(record_label_declaration ~pub ~prefix overrides)
233 |> sprintf "{\n%s}"
235 let constructor_arguments ?(box_fields = false) = function
236 | Pcstr_tuple types ->
237 if not box_fields then
238 tuple types
239 else (
240 match types with
241 | [] -> ""
242 | [ty] ->
243 let ty = core_type ty in
245 ty = "String"
246 || String.is_prefix ty ~prefix:"Vec<"
247 || String.is_prefix ty ~prefix:"Block<"
248 then
249 sprintf "(%s)" ty
250 else
251 sprintf "(Box<%s>)" ty
252 | _ -> sprintf "(Box<%s>)" (tuple types)
254 | Pcstr_record labels -> record_declaration SMap.empty labels
256 let variant_constructor_declaration ?(box_fields = false) cd =
257 let doc = doc_comment_of_attribute_list cd.pcd_attributes in
258 let name = convert_type_name cd.pcd_name.txt in
259 let args = constructor_arguments ~box_fields cd.pcd_args in
260 let discriminant =
261 (* If we see the [@value 42] attribute, assume it's for ppx_deriving enum,
262 and that all the variants are zero-argument (i.e., assume this is a
263 C-like enum and provide custom discriminant values). *)
264 List.find_map cd.pcd_attributes (fun attr ->
265 match attr with
266 | ( { txt = "value"; _ },
267 PStr
270 pstr_desc =
271 Pstr_eval
273 pexp_desc =
274 Pexp_constant (Pconst_integer (discriminant, None));
277 _ );
280 ] ) ->
281 Some (" = " ^ discriminant)
282 | _ -> None)
283 |> Option.value ~default:""
285 sprintf "%s%s%s%s,\n" doc name args discriminant
287 let ctor_arg_len (ctor_args : constructor_arguments) : int =
288 match ctor_args with
289 | Pcstr_tuple x -> List.length x
290 | Pcstr_record x -> List.length x
292 let type_declaration name td =
293 let doc = doc_comment_of_attribute_list td.ptype_attributes in
294 let attrs_and_vis additional_derives =
295 let derive_attr =
296 derived_traits name @ additional_derives
297 |> List.sort ~compare:(fun (_, t1) (_, t2) -> String.compare t1 t2)
298 |> List.map ~f:(fun (m, trait) ->
299 Option.iter m ~f:(fun m -> add_extern_use (m ^ "::" ^ trait));
300 trait)
301 |> String.concat ~sep:", "
302 |> sprintf "#[derive(%s)]"
304 doc ^ derive_attr ^ "\npub"
306 let tparams =
307 match (td.ptype_params, td.ptype_name.txt) with
308 (* HACK: eliminate tparam from `type _ ty_` and phase-parameterized types *)
309 | ([({ ptyp_desc = Ptyp_any; _ }, _)], "ty_")
310 | ([({ ptyp_desc = Ptyp_var "phase"; _ }, _)], _)
311 | ([({ ptyp_desc = Ptyp_var "ty"; _ }, _)], _)
312 when curr_module_name () = "typing_defs_core"
313 || curr_module_name () = "typing_defs" ->
315 | (tparams, _) -> type_params tparams
317 match (td.ptype_kind, td.ptype_manifest) with
318 | (_, Some ty) ->
319 (* The manifest represents a `= <some_type>` clause. When td.ptype_kind is
320 Ptype_abstract, this is a simple type alias:
322 type foo = Other_module.bar
324 In this case, the manifest contains the type Other_module.bar.
326 The ptype_kind can also be a full type definition. It is Ptype_variant in
327 a declaration like this:
329 type foo = Other_module.foo =
330 | Bar
331 | Baz
333 For these declarations, the OCaml compiler verifies that the variants in
334 Other_module.foo are equivalent to the ones we define in this
335 Ptype_variant.
337 I don't think there's a direct equivalent to this in Rust, or any reason
338 to try to reproduce it. If we see a manifest, we can ignore the
339 ptype_kind and just alias, re-export, or define a newtype for
340 Other_module.foo. *)
341 (match ty.ptyp_desc with
342 (* Polymorphic variants. *)
343 | Ptyp_variant _ ->
344 raise (Skip_type_decl "polymorphic variants not supported")
345 | Ptyp_constr ({ txt = Lident "t"; _ }, []) ->
346 (* In the case of `type t = prefix * string ;; type relative_path = t`, we
347 have already defined a RelativePath type because we renamed t in the
348 first declaration to the name of the module. We can just skip the second
349 declaration introducing the alias. *)
350 let mod_name_as_type = convert_type_name (curr_module_name ()) in
351 if name = mod_name_as_type then
352 raise
353 (Skip_type_decl
354 ( "it is an alias to type t, which was already renamed to "
355 ^ mod_name_as_type ))
356 else
357 sprintf "%spub type %s%s = %s;" doc name tparams mod_name_as_type
358 | Ptyp_constr ({ txt = id; _ }, targs) ->
359 let id = longident_to_string id in
360 let ty_name = id |> String.split ~on:':' |> List.last_exn in
361 if List.length td.ptype_params = List.length targs && self () = ty_name
362 then (
363 add_ty_reexport id;
364 raise (Skip_type_decl ("it is a re-export of " ^ id))
365 ) else
366 let ty = core_type ty in
367 if should_add_rcoc name then
368 sprintf
369 "%spub type %s%s = ocamlrep::rc::RcOc<%s>;"
371 name
372 tparams
374 else
375 sprintf "%spub type %s%s = %s;" doc name tparams ty
376 | _ ->
377 if should_use_alias_instead_of_tuple_struct name then
378 sprintf "%spub type %s%s = %s;" doc name tparams (core_type ty)
379 else
380 let ty =
381 match ty.ptyp_desc with
382 | Ptyp_tuple tys -> tuple tys ~pub:true
383 | _ -> sprintf "(pub %s)" @@ core_type ty
385 sprintf "%s struct %s%s %s;" (attrs_and_vis []) name tparams ty)
386 (* Variant types, including GADTs. *)
387 | (Ptype_variant ctors, None) ->
388 let all_nullary =
389 List.for_all ctors (fun c -> 0 = ctor_arg_len c.pcd_args)
391 let derives =
392 if all_nullary then
393 [(None, "Copy")]
394 else
397 let should_box_variant = should_box_variant name in
398 let ctors =
399 map_and_concat
400 ctors
401 (variant_constructor_declaration ~box_fields:should_box_variant)
403 sprintf "%s enum %s%s {\n%s}" (attrs_and_vis derives) name tparams ctors
404 (* Record types. *)
405 | (Ptype_record labels, None) ->
406 let labels = record_declaration (get_overrides name) labels ~pub:true in
407 sprintf "%s struct %s%s %s" (attrs_and_vis []) name tparams labels
408 (* `type foo`; an abstract type with no specified implementation. This doesn't
409 mean much outside of an .mli, I don't think. *)
410 | (Ptype_abstract, None) ->
411 raise (Skip_type_decl "Abstract types without manifest not supported")
412 (* type foo += A, e.g. the exn type. *)
413 | (Ptype_open, None) -> raise (Skip_type_decl "Open types not supported")
415 let type_declaration td =
416 let name = td.ptype_name.txt in
417 let name =
418 if name = "t" then
419 curr_module_name ()
420 else
421 name
423 let name = convert_type_name name in
424 let name = rename name in
425 let mod_name = curr_module_name () in
426 if blacklisted name then
427 log "Not converting type %s::%s: it was blacklisted" mod_name name
428 else
429 try with_self name (fun () -> add_decl name (type_declaration name td))
430 with Skip_type_decl reason ->
431 log "Not converting type %s::%s: %s" mod_name name reason