diff --git a/CHANGELOG.md b/CHANGELOG.md index ff63df08f1..9a4d0eea4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ #### :rocket: New Feature +- Add `@res.hoistedFunction` for emitting nested module functions as flat JavaScript exports. https://github.com/rescript-lang/rescript/pull/8402 - Add source map support with linked, inline, and hidden modes. https://github.com/rescript-lang/rescript/pull/8393 - Add `List.includes`, deprecate `List.has` in favor of `List.some`, and clarify the equality semantics of `List.includes` and `Array.includes`. https://github.com/rescript-lang/rescript/pull/8530 diff --git a/compiler/core/js_cmj_format.ml b/compiler/core/js_cmj_format.ml index 0956812bb0..005b34d0de 100644 --- a/compiler/core/js_cmj_format.ml +++ b/compiler/core/js_cmj_format.ml @@ -45,14 +45,22 @@ type keyed_cmj_value = { type keyed_cmj_values = keyed_cmj_value array +type hoisted_export = { + path: string list; (** Exact source-level module path segments. *) + export_name: string; + (** Flat compiler identifier used for the public JS export. *) +} + type t = { values: keyed_cmj_values; + hoisted_exports: hoisted_export array; pure: bool; package_spec: Js_packages_info.t; case: Ext_js_file_kind.case; } -let make ~(values : cmj_value Map_string.t) ~effect_ ~package_spec ~case : t = +let make ~(values : cmj_value Map_string.t) ~hoisted_exports ~effect_ + ~package_spec ~case : t = { values = Map_string.to_sorted_array_with_f values (fun k v -> @@ -61,6 +69,7 @@ let make ~(values : cmj_value Map_string.t) ~effect_ ~package_spec ~case : t = arity = v.arity; persistent_closed_lambda = v.persistent_closed_lambda; }); + hoisted_exports = Array.of_list hoisted_exports; pure = effect_ = None; package_spec; case; @@ -97,7 +106,7 @@ let to_file name ~check_exists (v : t) = output_string oc s; close_out oc) -let key_comp (a : string) b = Map_string.compare_key a b.name +let key_comp a b = Map_string.compare_key a b.name let not_found key = {name = key; arity = single_na; persistent_closed_lambda = None} @@ -151,6 +160,13 @@ let query_by_name (cmj_table : t) name : keyed_cmj_value = let values = cmj_table.values in binary_search values name +let find_hoisted_export (cmj_table : t) path = + Array.find_map + (fun value -> + if List.equal Ext_string.equal value.path path then Some value.export_name + else None) + cmj_table.hoisted_exports + type path = string type cmj_load_info = { diff --git a/compiler/core/js_cmj_format.mli b/compiler/core/js_cmj_format.mli index 32c8c423e8..913ac3a889 100644 --- a/compiler/core/js_cmj_format.mli +++ b/compiler/core/js_cmj_format.mli @@ -60,8 +60,15 @@ type keyed_cmj_value = { persistent_closed_lambda: Lam.t option; } +type hoisted_export = { + path: string list; (** Exact source-level module path segments. *) + export_name: string; + (** Flat compiler identifier used for the public JS export. *) +} + type t = { values: keyed_cmj_value array; + hoisted_exports: hoisted_export array; pure: bool; package_spec: Js_packages_info.t; case: Ext_js_file_kind.case; @@ -69,6 +76,7 @@ type t = { val make : values:cmj_value Map_string.t -> + hoisted_exports:hoisted_export list -> effect_:effect_ -> package_spec:Js_packages_info.t -> case:Ext_js_file_kind.case -> @@ -76,6 +84,8 @@ val make : val query_by_name : t -> string -> keyed_cmj_value +val find_hoisted_export : t -> string list -> string option + val single_na : arity val from_file : string -> t diff --git a/compiler/core/js_implementation.ml b/compiler/core/js_implementation.ml index df6ab959d1..49fcdc3c53 100644 --- a/compiler/core/js_implementation.ml +++ b/compiler/core/js_implementation.ml @@ -143,12 +143,12 @@ let after_parsing_impl ppf outputprefix (ast : Parsetree.structure) = Printtyped.implementation_with_coercion typedtree_coercion; (if !Js_config.cmi_only then Warnings.check_fatal () else - let lambda, exports = + let {Translmod.lambda; exports; hoisted_functions} = Translmod.transl_implementation modulename typedtree_coercion in let js_program = print_if_pipe ppf Clflags.dump_rawlambda Printlambda.lambda lambda - |> Lam_compile_main.compile outputprefix exports + |> Lam_compile_main.compile outputprefix exports hoisted_functions in if not !Js_config.cmj_only then Lam_compile_main.lambda_as_module js_program outputprefix); diff --git a/compiler/core/lam_compile.ml b/compiler/core/lam_compile.ml index dfb703f37d..05c78d08d2 100644 --- a/compiler/core/lam_compile.ml +++ b/compiler/core/lam_compile.ml @@ -290,6 +290,39 @@ type initialization = J.block *) let compile output_prefix = + (* When compiling a read from another module, a nested source path like + Other.A.B.make reaches this point as nested module-field reads: + + Pfield "make" (Pfield "B" (Pfield "A" (Lglobal_module Other))) + + Normal compilation does not look up the full path. It only queries the + first field, "A", and then emits the remaining fields as JS property + access: Other.A.B.make. The "A" lookup may include Submodule arity data, + but it does not say whether A.B.make has a separate root-level export. + + Hoisted functions need that extra question. For them, query the separate + hoisted-values table with an unambiguous key for source path A.B.make. If + present, the table returns the root-level JS export name, for example + A$B$make. Normal export metadata still lives in the regular .cmj values + table. *) + let rec extract_field_path segments primitive args = + match (primitive, args) with + | ( Lam_primitive.Pfield (_, Fld_module {name}), + [Lam.Lprim {primitive; args; _}] ) -> + extract_field_path (name :: segments) primitive args + | ( Lam_primitive.Pfield (_, Fld_module {name}), + [Lam.Lglobal_module (id, dynamic_import)] ) -> + Some (id, dynamic_import, name :: segments) + | _ -> None + in + let hoisted_external_field_name primitive args = + match extract_field_path [] primitive args with + | Some (id, dynamic_import, (_ :: _ :: _ as segments)) -> + Ext_option.map + (Lam_compile_env.find_hoisted_external_export ~dynamic_import id + segments) (fun name -> (id, dynamic_import, name)) + | Some (_, _, ([] | [_])) | None -> None + in let rec compile_external_field (* Like [List.empty]*) ?(dynamic_import = false) (lamba_cxt : Lam_compile_context.t) (id : Ident.t) name : Js_output.t = @@ -1718,17 +1751,47 @@ let compile output_prefix = fn_code args))) and compile_prim (prim_info : Lam.prim_info) (lambda_cxt : Lam_compile_context.t) = + let compile_primitive_default primitive args loc = + let args_block, args_expr = + if args = [] then ([], []) + else + let new_cxt = {lambda_cxt with continuation = NeedValue Not_tail} in + Ext_list.split_map args (fun x -> + match compile_lambda new_cxt x with + | {block; value = Some b} -> (block, b) + | {value = None} -> assert false) + in + let args_code : J.block = List.concat args_block in + let exp = + (* TODO: all can be done in [compile_primitive] *) + Lam_compile_primitive.translate output_prefix loc lambda_cxt primitive + args_expr + in + Js_output.output_of_block_and_expression lambda_cxt.continuation args_code + (with_source_loc loc exp) + in match prim_info with - | { - primitive = Pfield (_, fld_info); - args = [Lglobal_module (id, dynamic_import)]; - _; - } -> ( - (* should be before Lglobal_global *) - match fld_info with - | Fld_module {name = field} -> - compile_external_field ~dynamic_import lambda_cxt id field - | _ -> assert false) + | {primitive = Pfield (_, Fld_module _); _} -> ( + match hoisted_external_field_name prim_info.primitive prim_info.args with + | Some (id, dynamic_import, hoisted_name) -> + Js_output.output_of_expression lambda_cxt.continuation + ~no_effects:no_effects_const + (E.ml_var_dot ~dynamic_import id hoisted_name) + | None -> ( + match prim_info with + | { + primitive = Pfield (_, fld_info); + args = [Lglobal_module (id, dynamic_import)]; + _; + } -> ( + (* should be before Lglobal_global *) + match fld_info with + | Fld_module {name = field} -> + compile_external_field ~dynamic_import lambda_cxt id field + | _ -> assert false) + | _ -> + compile_primitive_default prim_info.primitive prim_info.args + prim_info.loc)) | {primitive = Praise; args = [e]; loc} -> ( match compile_lambda {lambda_cxt with continuation = NeedValue Not_tail} e @@ -1898,24 +1961,7 @@ let compile output_prefix = Location.raise_errorf ~loc "Invalid argument: unsupported argument to dynamic import. If you \ believe this should be supported, please open an issue.") - | {primitive; args; loc} -> - let args_block, args_expr = - if args = [] then ([], []) - else - let new_cxt = {lambda_cxt with continuation = NeedValue Not_tail} in - Ext_list.split_map args (fun x -> - match compile_lambda new_cxt x with - | {block; value = Some b} -> (block, b) - | {value = None} -> assert false) - in - let args_code : J.block = List.concat args_block in - let exp = - (* TODO: all can be done in [compile_primitive] *) - Lam_compile_primitive.translate output_prefix loc lambda_cxt primitive - args_expr - in - Js_output.output_of_block_and_expression lambda_cxt.continuation args_code - (with_source_loc loc exp) + | {primitive; args; loc} -> compile_primitive_default primitive args loc and collect_dup_overrides (copy_id : Ident.t) (lam : Lam.t) (acc : (Lam_compat.set_field_dbg_info * Lam.t) list) : (Lam_compat.set_field_dbg_info * Lam.t) list option = diff --git a/compiler/core/lam_compile_env.ml b/compiler/core/lam_compile_env.ml index a23b611680..98cc70ddc3 100644 --- a/compiler/core/lam_compile_env.ml +++ b/compiler/core/lam_compile_env.ml @@ -84,20 +84,26 @@ let add_js_module ?import_attributes id | Some old_key -> old_key.id +let cmj_table_of_module_id ~dynamic_import (module_id : Ident.t) = + let oid = Lam_module_ident.of_ml ~dynamic_import module_id in + match Lam_module_ident.Hash.find_opt cached_tbl oid with + | None -> + let cmj_load_info = !Js_cmj_load.load_unit module_id.name in + oid +> Ml cmj_load_info; + cmj_load_info.cmj_table + | Some (Ml {cmj_table}) -> cmj_table + | Some External -> assert false + let query_external_id_info ?(dynamic_import = false) (module_id : Ident.t) (name : string) : ident_info = - let oid = Lam_module_ident.of_ml ~dynamic_import module_id in - let cmj_table = - match Lam_module_ident.Hash.find_opt cached_tbl oid with - | None -> - let cmj_load_info = !Js_cmj_load.load_unit module_id.name in - oid +> Ml cmj_load_info; - cmj_load_info.cmj_table - | Some (Ml {cmj_table}) -> cmj_table - | Some External -> assert false - in + let cmj_table = cmj_table_of_module_id ~dynamic_import module_id in Js_cmj_format.query_by_name cmj_table name +let find_hoisted_external_export ?(dynamic_import = false) (module_id : Ident.t) + (path : string list) : string option = + let cmj_table = cmj_table_of_module_id ~dynamic_import module_id in + Js_cmj_format.find_hoisted_export cmj_table path + let get_package_path_from_cmj (id : Lam_module_ident.t) : string * Js_packages_info.t * Ext_js_file_kind.case = let cmj_load_info = diff --git a/compiler/core/lam_compile_env.mli b/compiler/core/lam_compile_env.mli index 75527d0eee..3472b82d88 100644 --- a/compiler/core/lam_compile_env.mli +++ b/compiler/core/lam_compile_env.mli @@ -71,6 +71,9 @@ val query_external_id_info : will raise if not found *) +val find_hoisted_external_export : + ?dynamic_import:bool -> Ident.t -> string list -> string option + val is_pure_module : Lam_module_ident.t -> bool val get_package_path_from_cmj : diff --git a/compiler/core/lam_compile_main.ml b/compiler/core/lam_compile_main.ml index 4db6c65917..07f53dbc34 100644 --- a/compiler/core/lam_compile_main.ml +++ b/compiler/core/lam_compile_main.ml @@ -107,10 +107,149 @@ let no_side_effects (rest : Lam_group.t list) : string option = Some "" else None (* TODO :*)) +(* Materialize JS-hoisted values as root-level aliases and exports. The source + value still lives at its normal module path, but downstream tools can import + the flat name directly when the .cmj metadata marks it as hoisted. *) +let js_hoisted_aliases (export_ids : Ident.t list) + (hoisted : Lambda.hoisted_function list) (groups : Lam_group.t list) = + if hoisted = [] then [] + else + let group_map = + Ext_list.fold_left groups Map_ident.empty (fun group_map group -> + match group with + | Single (_, id, lam) -> Map_ident.add group_map id lam + | Recursive bindings -> + Ext_list.fold_left bindings group_map (fun group_map (id, lam) -> + Map_ident.add group_map id lam) + | Nop _ -> group_map) + in + let rec access loc base fields = + match fields with + | [] -> base + | (pos, name) :: fields -> + access loc + (Lam.prim + ~primitive: + (Lam_primitive.Pfield (pos, Lam_compat.Fld_module {name})) + ~args:[base] loc) + fields + in + let rec resolve_binding seen = function + | Lam.Lvar id as lam -> ( + if Set_ident.mem seen id then (lam, Some id) + else + match Map_ident.find_opt group_map id with + | Some + ((Lam.Lvar _ | Lam.Lprim {primitive = Lam_primitive.Pfield _; _}) + as alias) -> + resolve_binding (Set_ident.add seen id) alias + | Some resolved -> (resolved, Some id) + | None -> (lam, Some id)) + | Lam.Lprim {primitive = Lam_primitive.Pfield (pos, _); args = [base]} as + lam -> ( + match fst (resolve_binding seen base) with + | Lam.Lprim + {primitive = Lam_primitive.Pmakeblock (_, Blk_module _, _); args} + -> ( + match List.nth_opt args pos with + | Some field -> resolve_binding seen field + | None -> (lam, None)) + | _ -> (lam, None)) + | lam -> (lam, None) + in + let resolve seen lam = fst (resolve_binding seen lam) in + let rec find_field name pos fields args = + match (fields, args) with + | field :: _, arg :: _ when field = name -> Some (pos, arg) + | _ :: fields, _ :: args -> find_field name (pos + 1) fields args + | [], [] -> None + | _ -> invalid_arg "find_field" + in + let rec find_path lam fields positions = + match fields with + | [] -> + let target, binding_id = resolve_binding Set_ident.empty lam in + Some (List.rev positions, binding_id, target) + | field :: fields -> ( + match resolve Set_ident.empty lam with + | Lam.Lprim + { + primitive = Lam_primitive.Pmakeblock (_, Blk_module names, _); + args; + } -> ( + match find_field field 0 names args with + | Some (pos, arg) -> find_path arg fields ((pos, field) :: positions) + | None -> None) + | _ -> None) + in + let exported_modules = + Ext_list.fold_left export_ids Map_string.empty (fun modules id -> + Map_string.add modules id.Ident.name id) + in + let occupied_names = + Ext_list.fold_left groups Set_string.empty (fun occupied group -> + match group with + | Single (_, id, _) -> + Set_string.add occupied (Ext_ident.convert id.Ident.name) + | Recursive bindings -> + Ext_list.fold_left bindings occupied (fun occupied (id, _) -> + Set_string.add occupied (Ext_ident.convert id.Ident.name)) + | Nop _ -> occupied) + in + fst + (Ext_list.fold_left hoisted ([], occupied_names) + (fun ((aliases, occupied_names) as state) hoisted -> + let {Lambda.binding; path; loc} = hoisted in + let missing_path () = + Location.prerr_warning loc + (Warnings.Misplaced_attribute "res.hoistedFunction"); + state + in + match path with + | top :: fields -> ( + match Map_string.find_opt exported_modules top with + | Some top_id -> ( + match Map_ident.find_opt group_map top_id with + | Some lam -> ( + match find_path lam fields [] with + | Some (access_path, Some target_id, target) + when Ident.same binding target_id -> + let name = + path + |> List.map Ext_ident.unwrap_uppercase_exotic + |> String.concat "$" + in + let js_name = Ext_ident.convert name in + if Set_string.mem occupied_names js_name then + let error_loc = + match target with + | Lam.Lfunction {loc} -> loc + | _ -> loc + in + Location.raise_errorf ~loc:error_loc + "Cannot hoist this function as `%s` because that name \ + is already used by a top-level binding." + name + else + let alias_id = Ident.create name in + let alias = access loc (Lam.var top_id) access_path in + ( ( Lam_group.Single (Alias, alias_id, alias), + alias_id, + alias, + path, + name ) + :: aliases, + Set_string.add occupied_names js_name ) + | Some _ | None -> missing_path ()) + | None -> missing_path ()) + | None -> missing_path ()) + | [] -> missing_path ())) + (** Actually simplify_lets is kind of global optimization since it requires you to know whether it's used or not *) -let compile (output_prefix : string) export_idents (lam : Lambda.lambda) = +let compile (output_prefix : string) export_idents hoisted (lam : Lambda.lambda) + = let debug_ir = !Js_config.debug_ir in let diagnostics = if debug_ir then Some (Ir_diagnostics.create ~output_prefix) else None @@ -203,6 +342,34 @@ let compile (output_prefix : string) export_idents (lam : Lambda.lambda) = Ir_diagnostics.dump_groups diagnostics coerced_input.groups)) in let maybe_pure = no_side_effects groups in + (* Add the generated alias groups before JS lowering so regular export + printing, tree shaking, and .cmj metadata all see the flat runtime value. *) + let hoisted_aliases = js_hoisted_aliases meta.exports hoisted groups in + let hoisted_groups, hoisted_exports, hoisted_export_map, hoisted_metadata = + Ext_list.fold_left hoisted_aliases ([], [], Map_ident.empty, []) + (fun + (groups, exports, export_map, hoisted_metadata) + (group, id, lam, path, name) + -> + ( group :: groups, + id :: exports, + Map_ident.add export_map id lam, + {Js_cmj_format.path; export_name = name} :: hoisted_metadata )) + in + let groups = groups @ List.rev hoisted_groups in + let meta = + { + meta with + exports = meta.exports @ List.rev hoisted_exports; + export_idents = + Ext_list.fold_left hoisted_exports meta.export_idents (fun acc id -> + Set_ident.add acc id); + } + in + let export_map = + Map_ident.fold hoisted_export_map coerced_input.export_map + (fun id lam acc -> Map_ident.add acc id lam) + in let () = if debug_ir then Ext_log.dwarn ~__POS__ "\n@[[TIME:]Pre-compile: %f@]@." @@ -250,7 +417,7 @@ let compile (output_prefix : string) export_idents (lam : Lambda.lambda) = Lam_stats_export.get_dependent_module_effect maybe_pure external_module_ids in let v : Js_cmj_format.t = - Lam_stats_export.export_to_cmj meta effect_ coerced_input.export_map + Lam_stats_export.export_to_cmj meta effect_ export_map hoisted_metadata (if Ext_char.is_lower_case (Filename.basename output_prefix).[0] then Little else Upper) diff --git a/compiler/core/lam_compile_main.mli b/compiler/core/lam_compile_main.mli index fcd298ce3a..2cfeef8001 100644 --- a/compiler/core/lam_compile_main.mli +++ b/compiler/core/lam_compile_main.mli @@ -27,7 +27,12 @@ (** Compile and register the hook of function to compile a lambda to JS IR *) -val compile : string -> Ident.t list -> Lambda.lambda -> J.deps_program +val compile : + string -> + Ident.t list -> + Lambda.hoisted_function list -> + Lambda.lambda -> + J.deps_program (** For toplevel, [filename] is [""] which is the same as {!Env.get_unit_name ()} *) diff --git a/compiler/core/lam_stats_export.ml b/compiler/core/lam_stats_export.ml index 711ab5be42..c84f3874e3 100644 --- a/compiler/core/lam_stats_export.ml +++ b/compiler/core/lam_stats_export.ml @@ -128,11 +128,11 @@ let get_dependent_module_effect (maybe_pure : string option) ]} TODO: check that we don't do this in browser environment *) -let export_to_cmj (meta : Lam_stats.t) effect_ export_map case : Js_cmj_format.t - = +let export_to_cmj (meta : Lam_stats.t) effect_ export_map hoisted_exports case : + Js_cmj_format.t = let values = values_of_export meta export_map in - Js_cmj_format.make ~values ~effect_ + Js_cmj_format.make ~values ~hoisted_exports ~effect_ ~package_spec:(Js_packages_state.get_packages_info ()) ~case (* FIXME: make sure [-o] would not change its case diff --git a/compiler/core/lam_stats_export.mli b/compiler/core/lam_stats_export.mli index 593ff0a1b9..9d8e814581 100644 --- a/compiler/core/lam_stats_export.mli +++ b/compiler/core/lam_stats_export.mli @@ -29,5 +29,6 @@ val export_to_cmj : Lam_stats.t -> Js_cmj_format.effect_ -> Lam.t Map_ident.t -> + Js_cmj_format.hoisted_export list -> Ext_js_file_kind.case -> Js_cmj_format.t diff --git a/compiler/ext/config.ml b/compiler/ext/config.ml index c44aa8392d..e848a4ccbc 100644 --- a/compiler/ext/config.ml +++ b/compiler/ext/config.ml @@ -1,4 +1,4 @@ -let cmi_magic_number = "Caml1999I023" +let cmi_magic_number = "Caml1999I024" (* Magic numbers for marshaled values of the *current* parsetree, whose layout changes across compiler versions. *) diff --git a/compiler/frontend/bs_ast_invariant.ml b/compiler/frontend/bs_ast_invariant.ml index 023043f9f4..a5b38f9669 100644 --- a/compiler/frontend/bs_ast_invariant.ml +++ b/compiler/frontend/bs_ast_invariant.ml @@ -22,19 +22,20 @@ * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *) -(** Warning unused bs attributes +(** Warn about unused compiler attributes. Note if we warn `deriving` too, it may fail third party ppxes *) -let is_bs_attribute txt = +let is_checked_attribute txt = match txt with - | "as" | "config" | "ignore" | "int" | "optional" | "string" | "unwrap" -> + | "as" | "config" | "ignore" | "int" | "optional" | "res.hoistedFunction" + | "string" | "unwrap" -> true | _ -> false let warn_unused_attribute ((({txt; loc} as sloc), _) : Parsetree.attribute) = if - is_bs_attribute txt && (not loc.loc_ghost) + is_checked_attribute txt && (not loc.loc_ghost) && not (Used_attributes.is_used_attribute sloc) then (* diff --git a/compiler/frontend/bs_builtin_ppx.ml b/compiler/frontend/bs_builtin_ppx.ml index b71c3d2524..3a24dde987 100644 --- a/compiler/frontend/bs_builtin_ppx.ml +++ b/compiler/frontend/bs_builtin_ppx.ml @@ -406,6 +406,22 @@ let expr_mapper ~async_context ~in_function_def (self : mapper) let typ_mapper (self : mapper) (typ : Parsetree.core_type) = Ast_core_type_class_type.typ_mapper self typ +let mark_hoisted_function_attributes (bindings : Parsetree.value_binding list) = + Ext_list.iter bindings (fun {pvb_attributes} -> + Ext_list.iter pvb_attributes (fun (({txt}, _) as attr) -> + if txt = "res.hoistedFunction" then + Used_attributes.mark_used_attribute attr)) + +let value_bindings_mapper (self : mapper) + (bindings : Parsetree.value_binding list) = + mark_hoisted_function_attributes bindings; + Ast_tuple_pattern_flatten.value_bindings_mapper self bindings + +let value_bindings_rec_mapper (self : mapper) + (bindings : Parsetree.value_binding list) = + mark_hoisted_function_attributes bindings; + default_mapper.value_bindings_rec self bindings + let signature_item_mapper (self : mapper) (sigi : Parsetree.signature_item) : Parsetree.signature_item = match sigi.psig_desc with @@ -486,6 +502,9 @@ let signature_item_mapper (self : mapper) (sigi : Parsetree.signature_item) : let structure_item_mapper (self : mapper) (str : Parsetree.structure_item) : Parsetree.structure_item = + (match str.pstr_desc with + | Pstr_value (_, bindings) -> mark_hoisted_function_attributes bindings + | _ -> ()); match str.pstr_desc with | Pstr_value (_, vbs) when List.exists @@ -784,7 +803,8 @@ let mapper : mapper = pat = pat_mapper; typ = typ_mapper; signature_item = signature_item_mapper; - value_bindings = Ast_tuple_pattern_flatten.value_bindings_mapper; + value_bindings = value_bindings_mapper; + value_bindings_rec = value_bindings_rec_mapper; structure_item = structure_item_mapper; structure = structure_mapper ~await_context:(ref (Hashtbl.create 10)); (* Ad-hoc way to internalize stuff *) diff --git a/compiler/jsoo/jsoo_playground_main.ml b/compiler/jsoo/jsoo_playground_main.ml index a78cf8c3eb..2ca3db5725 100644 --- a/compiler/jsoo/jsoo_playground_main.ml +++ b/compiler/jsoo/jsoo_playground_main.ml @@ -518,13 +518,14 @@ module Compile = struct types_signature := signature; (a, b) in - typed_tree |> Translmod.transl_implementation modulename - |> (* Printlambda.lambda ppf *) fun (lam, exports) -> + let {Translmod.lambda; exports; hoisted_functions} = + Translmod.transl_implementation modulename typed_tree + in let buffer = Buffer.create 1000 in let () = Js_dump_program.pp_deps_program ~output_prefix:"" (* does not matter here *) module_system - (Lam_compile_main.compile "" exports lam) + (Lam_compile_main.compile "" exports hoisted_functions lambda) (Ext_pp.from_buffer buffer) in let v = Buffer.contents buffer in @@ -548,15 +549,15 @@ module Compile = struct let typedtree = Printer.to_string Printtyped.implementation_with_coercion typed_tree in - let lambda = Printer.to_string Printlambda.lambda lam in - let lam, _ = Lam_convert.convert export_ident_sets lam in + let lambda_output = Printer.to_string Printlambda.lambda lambda in + let lam, _ = Lam_convert.convert export_ident_sets lambda in let lam = Lam_print.lambda_to_string lam in let debug_attrs = Js.Unsafe. [| ("parsetree", inject @@ Js.string parsetree); ("typedtree", inject @@ Js.string typedtree); - ("lambda", inject @@ Js.string lambda); + ("lambda", inject @@ Js.string lambda_output); ("lam", inject @@ Js.string lam); |] in diff --git a/compiler/ml/lambda.ml b/compiler/ml/lambda.ml index 987d844ddd..e649a0876e 100644 --- a/compiler/ml/lambda.ml +++ b/compiler/ml/lambda.ml @@ -15,6 +15,8 @@ type loc_kind = Loc_FILE | Loc_LINE | Loc_MODULE | Loc_LOC | Loc_POS +type hoisted_function = {binding: Ident.t; path: string list; loc: Location.t} + type tag_info = | Blk_constructor of { name: string; diff --git a/compiler/ml/lambda.mli b/compiler/ml/lambda.mli index efe4d9802b..d781c525dc 100644 --- a/compiler/ml/lambda.mli +++ b/compiler/ml/lambda.mli @@ -19,6 +19,8 @@ open Asttypes type loc_kind = Loc_FILE | Loc_LINE | Loc_MODULE | Loc_LOC | Loc_POS +type hoisted_function = {binding: Ident.t; path: string list; loc: Location.t} + type tag_info = | Blk_constructor of { name: string; diff --git a/compiler/ml/translattribute.ml b/compiler/ml/translattribute.ml index 7c9c9a7a0a..91314cfdcf 100644 --- a/compiler/ml/translattribute.ml +++ b/compiler/ml/translattribute.ml @@ -37,6 +37,21 @@ let find_attribute p (attributes : t list) = in (attr, other_attributes) +let get_empty_attribute name attributes = + let attr, _ = + find_attribute + (fun (({txt}, _) : Parsetree.attribute) -> txt = name) + attributes + in + match attr with + | None -> None + | Some ({loc}, Parsetree.PStr []) -> Some loc + | Some ({loc}, _) -> + Location.prerr_warning loc + (Warnings.Attribute_payload + (name, "This attribute does not accept a payload")); + None + let parse_inline_attribute (attr : t option) : Lambda.inline_attribute = match attr with | None -> Default_inline diff --git a/compiler/ml/translattribute.mli b/compiler/ml/translattribute.mli index 03115eb0ee..bac456ba8d 100644 --- a/compiler/ml/translattribute.mli +++ b/compiler/ml/translattribute.mli @@ -23,6 +23,8 @@ val add_inline_attribute : val get_inline_attribute : Parsetree.attributes -> Lambda.inline_attribute +val get_empty_attribute : string -> Parsetree.attributes -> Location.t option + val get_and_remove_inlined_attribute : Typedtree.expression -> Lambda.inline_attribute * Typedtree.expression diff --git a/compiler/ml/translcore.ml b/compiler/ml/translcore.ml index 6a55f5d026..d9fa195465 100644 --- a/compiler/ml/translcore.ml +++ b/compiler/ml/translcore.ml @@ -598,6 +598,34 @@ let extract_directive_for_fn exp = if txt = "directive" then Ast_payload.is_single_string payload else None) +let hoisted_function_attr_name = "res.hoistedFunction" + +let find_js_hoisted_attr attrs = + Translattribute.get_empty_attribute hoisted_function_attr_name attrs + +(* A value binding's source attributes are not carried all the way to JS + emission. Record the binding and its source path here so later compiler + stages can add the flat JS export and matching .cmj metadata. *) +let mark_js_hoisted_pattern ~js_hoist attrs pat lam = + match find_js_hoisted_attr attrs with + | None -> () + | Some loc -> ( + match lam with + | Lfunction _ -> ( + match pat.pat_desc with + | Tpat_var (id, _) | Tpat_alias ({pat_desc = Tpat_any}, id, _) -> ( + match js_hoist with + | Some register -> register id loc + | None -> + Location.prerr_warning loc + (Warnings.Misplaced_attribute hoisted_function_attr_name)) + | _ -> + Location.prerr_warning loc + (Warnings.Misplaced_attribute hoisted_function_attr_name)) + | _ -> + Location.prerr_warning loc + (Warnings.Misplaced_attribute hoisted_function_attr_name)) + let rec transl_exp e = Builtin_attributes.warning_scope ~ppwarning:false e.exp_attributes (fun () -> List.iter (Translattribute.check_attribute e) e.exp_attributes; @@ -611,7 +639,7 @@ and transl_exp0 (e : Typedtree.expression) : Lambda.lambda = transl_value_path ~loc:e.exp_loc e.exp_env path | Texp_constant cst -> Lconst (Const_base cst) | Texp_let (rec_flag, pat_expr_list, body) -> - transl_let rec_flag pat_expr_list (transl_exp body) + transl_let ~js_hoist:None rec_flag pat_expr_list (transl_exp body) | Texp_function {params = fparams; body; async} -> let directive = match extract_directive_for_fn e with @@ -1019,7 +1047,7 @@ and transl_function loc (params : function_param list) body = fp_partial, return_unit ) -and transl_let rec_flag pat_expr_list body = +and transl_let ~js_hoist rec_flag pat_expr_list body = match rec_flag with | Nonrecursive -> let rec transl = function @@ -1030,6 +1058,7 @@ and transl_let rec_flag pat_expr_list body = transl_exp expr) in let lam = Translattribute.add_inline_attribute lam vb_loc attr in + mark_js_hoisted_pattern ~js_hoist attr pat lam; Matching.for_let pat.pat_loc lam pat (transl rem) in transl pat_expr_list @@ -1049,6 +1078,7 @@ and transl_let rec_flag pat_expr_list body = (fun () -> transl_exp expr) in let lam = Translattribute.add_inline_attribute lam vb_loc vb_attributes in + mark_js_hoisted_pattern ~js_hoist vb_attributes pat lam; (id, lam) in Lletrec (Ext_list.map pat_expr_list transl_case, body) diff --git a/compiler/ml/translcore.mli b/compiler/ml/translcore.mli index 1847a4883c..f3d90ee02a 100644 --- a/compiler/ml/translcore.mli +++ b/compiler/ml/translcore.mli @@ -19,6 +19,7 @@ val transl_exp : Typedtree.expression -> Lambda.lambda val transl_let : + js_hoist:(Ident.t -> Location.t -> unit) option -> Asttypes.rec_flag -> Typedtree.value_binding list -> Lambda.lambda -> diff --git a/compiler/ml/translmod.ml b/compiler/ml/translmod.ml index aaf8312967..45e6a705a5 100644 --- a/compiler/ml/translmod.ml +++ b/compiler/ml/translmod.ml @@ -32,6 +32,18 @@ let is_top (rootpath : Path.t option) = | Some (Pident _) -> true | _ -> false +let module_path = function + | Some path -> ( + match Path.flatten path with + | `Ok (_, segments) -> Some segments + | `Contains_apply -> None) + | None -> None + +let exportable_module_path rootpath = + match module_path rootpath with + | Some (_ :: _ as path) -> Some path + | _ -> None + let functor_path path param : Path.t option = match path with | None -> None @@ -222,6 +234,17 @@ let get_functor_params mexp coercion root_path = | _ -> assert false let export_identifiers : Ident.t list ref = ref [] +let js_hoisted : Lambda.hoisted_function list ref = ref [] + +let js_hoist_handler rootpath = + match exportable_module_path rootpath with + | None -> None + | Some path -> + Some + (fun id loc -> + js_hoisted := + {Lambda.binding = id; path = path @ [id.Ident.name]; loc} + :: !js_hoisted) let rec compile_functor mexp coercion root_path loc = let functor_param, body, body_path, res_coercion, inline_attribute = @@ -367,7 +390,10 @@ and transl_structure loc fields cc rootpath final_env = function | _ -> if not (Parmatch.irrefutable vb_pat) then raise (Error (vb_pat.pat_loc, Fragile_pattern_in_toplevel))); - (Translcore.transl_let rec_flag pat_expr_list body, size) + ( Translcore.transl_let + ~js_hoist:(js_hoist_handler rootpath) + rec_flag pat_expr_list body, + size ) | Tstr_typext tyext -> let ids = List.map (fun ext -> ext.ext_id) tyext.tyext_constructors in let body, size = @@ -391,9 +417,10 @@ and transl_structure loc fields cc rootpath final_env = function size ) | Tstr_module mb as s -> let id = mb.mb_id in + let hidden = Typemod.rescript_hide s in let body, size = transl_structure loc - (if Typemod.rescript_hide s then fields else id :: fields) + (if hidden then fields else id :: fields) cc rootpath final_env rem in let module_body = @@ -456,11 +483,22 @@ let _ = Translcore.transl_module := transl_module (* Compile an implementation *) +type implementation = { + lambda: Lambda.lambda; + exports: Ident.t list; + hoisted_functions: Lambda.hoisted_function list; +} + let transl_implementation module_name (str, cc) = export_identifiers := []; + js_hoisted := []; let module_id = Ident.create_persistent module_name in let body, _ = transl_struct Location.none [] cc (global_path module_id) str in - (body, !export_identifiers) + { + lambda = body; + exports = !export_identifiers; + hoisted_functions = !js_hoisted; + } (* Build the list of value identifiers defined by a toplevel structure (excluding primitive declarations). *) diff --git a/compiler/ml/translmod.mli b/compiler/ml/translmod.mli index 74ef747e10..5da4808c1f 100644 --- a/compiler/ml/translmod.mli +++ b/compiler/ml/translmod.mli @@ -16,10 +16,14 @@ (* Translation from typed abstract syntax to lambda terms, for the module language *) +type implementation = { + lambda: Lambda.lambda; + exports: Ident.t list; + hoisted_functions: Lambda.hoisted_function list; +} + val transl_implementation : - string -> - Typedtree.structure * Typedtree.module_coercion -> - Lambda.lambda * Ident.t list + string -> Typedtree.structure * Typedtree.module_coercion -> implementation type error (* exception Error of Location.t * error *) diff --git a/tests/build_tests/super_errors/expected/hoisted_function_export_collision.res.expected b/tests/build_tests/super_errors/expected/hoisted_function_export_collision.res.expected new file mode 100644 index 0000000000..a8c130e227 --- /dev/null +++ b/tests/build_tests/super_errors/expected/hoisted_function_export_collision.res.expected @@ -0,0 +1,11 @@ + + We've found a bug for you! + /.../fixtures/hoisted_function_export_collision.res:3:14-21 + + 1 │ module One = { + 2 │ @res.hoistedFunction + 3 │ let make = () => () + 4 │ } + 5 │ let \"One$make" = () => () + + Cannot hoist this function as `One$make` because that name is already used by a top-level binding. diff --git a/tests/build_tests/super_errors/expected/hoisted_function_hidden_by_signature.res.expected b/tests/build_tests/super_errors/expected/hoisted_function_hidden_by_signature.res.expected new file mode 100644 index 0000000000..bbba188005 --- /dev/null +++ b/tests/build_tests/super_errors/expected/hoisted_function_hidden_by_signature.res.expected @@ -0,0 +1,11 @@ + + Warning number 53 + /.../fixtures/hoisted_function_hidden_by_signature.res:4:3-22 + + 2 │ let visible: unit => string + 3 │ } = { + 4 │ @res.hoistedFunction + 5 │ let hidden = () => "hidden" + 6 │ let visible = hidden + + the @res.hoistedFunction attribute cannot appear in this context \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/hoisted_function_invalid_payload.res.expected b/tests/build_tests/super_errors/expected/hoisted_function_invalid_payload.res.expected new file mode 100644 index 0000000000..ae6bd4b594 --- /dev/null +++ b/tests/build_tests/super_errors/expected/hoisted_function_invalid_payload.res.expected @@ -0,0 +1,11 @@ + + Warning number 47 + /.../fixtures/hoisted_function_invalid_payload.res:2:3-22 + + 1 │ module Nested = { + 2 │ @res.hoistedFunction("name") + 3 │ let make = () => () + 4 │ } + + illegal payload for attribute @res.hoistedFunction. +This attribute does not accept a payload \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/hoisted_function_not_exportable.res.expected b/tests/build_tests/super_errors/expected/hoisted_function_not_exportable.res.expected new file mode 100644 index 0000000000..9d299b480a --- /dev/null +++ b/tests/build_tests/super_errors/expected/hoisted_function_not_exportable.res.expected @@ -0,0 +1,70 @@ + + Warning number 32 + /.../fixtures/hoisted_function_not_exportable.res:26:7-10 + + 24 │ module Shadowed = { + 25 │ @res.hoistedFunction + 26 │ let make = () => "first" + 27 │ let make = () => "second" + 28 │ } + + unused value make. + + + Warning number 53 + /.../fixtures/hoisted_function_not_exportable.res:15:3-22 + + 13 │ } + 14 │ module Make = () => { + 15 │ @res.hoistedFunction + 16 │ let make = () => () + 17 │ } + + the @res.hoistedFunction attribute cannot appear in this context + + + Warning number 53 + /.../fixtures/hoisted_function_not_exportable.res:9:5-24 + + 7 │ let localModule = () => { + 8 │ module Local = { + 9 │ @res.hoistedFunction + 10 │ let make = () => () + 11 │ } + + the @res.hoistedFunction attribute cannot appear in this context + + + Warning number 53 + /.../fixtures/hoisted_function_not_exportable.res:2:3-22 + + 1 │ let run = () => { + 2 │ @res.hoistedFunction + 3 │ let local = () => () + 4 │ local() + + the @res.hoistedFunction attribute cannot appear in this context + + + Warning number 53 + /.../fixtures/hoisted_function_not_exportable.res:19:3-22 + + 17 │ } + 18 │ %%private( + 19 │ @res.hoistedFunction + 20 │ let privateMake = () => () + 21 │ ) + + the @res.hoistedFunction attribute cannot appear in this context + + + Warning number 53 + /.../fixtures/hoisted_function_not_exportable.res:25:3-22 + + 23 │ let usePrivate = privateMake() + 24 │ module Shadowed = { + 25 │ @res.hoistedFunction + 26 │ let make = () => "first" + 27 │ let make = () => "second" + + the @res.hoistedFunction attribute cannot appear in this context \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/hoisted_function_path_collision.res.expected b/tests/build_tests/super_errors/expected/hoisted_function_path_collision.res.expected new file mode 100644 index 0000000000..3f049f9697 --- /dev/null +++ b/tests/build_tests/super_errors/expected/hoisted_function_path_collision.res.expected @@ -0,0 +1,11 @@ + + We've found a bug for you! + /.../fixtures/hoisted_function_path_collision.res:7:19-26 + + 5 │ } + 6 │ @res.hoistedFunction + 7 │ let \"B$make" = () => () + 8 │ } + 9 │ let after = () + + Cannot hoist this function as `A$B$make` because that name is already used by a top-level binding. diff --git a/tests/build_tests/super_errors/expected/hoisted_function_unsupported_pattern.res.expected b/tests/build_tests/super_errors/expected/hoisted_function_unsupported_pattern.res.expected new file mode 100644 index 0000000000..a2e9b9970e --- /dev/null +++ b/tests/build_tests/super_errors/expected/hoisted_function_unsupported_pattern.res.expected @@ -0,0 +1,10 @@ + + Warning number 53 + /.../fixtures/hoisted_function_unsupported_pattern.res:2:3-22 + + 1 │ module Nested = { + 2 │ @res.hoistedFunction + 3 │ let f as g = () => "ok" + 4 │ } + + the @res.hoistedFunction attribute cannot appear in this context \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/warning_101_bs_unused_attribute.res.expected b/tests/build_tests/super_errors/expected/warning_101_bs_unused_attribute.res.expected index 395cc146bc..7fcb36fc44 100644 --- a/tests/build_tests/super_errors/expected/warning_101_bs_unused_attribute.res.expected +++ b/tests/build_tests/super_errors/expected/warning_101_bs_unused_attribute.res.expected @@ -8,4 +8,34 @@ Unused attribute: @as This attribute has no effect here. +For example, some attributes are only meaningful in externals. + + + + Warning number 101 (configured as error) + /.../fixtures/warning_101_bs_unused_attribute.res:4:1-20 + + 2 │ let x = 1 + 3 │ + 4 │ @res.hoistedFunction + 5 │ type t = int + 6 │ + + Unused attribute: @res.hoistedFunction +This attribute has no effect here. +For example, some attributes are only meaningful in externals. + + + + Warning number 101 (configured as error) + /.../fixtures/warning_101_bs_unused_attribute.res:8:14-33 + + 6 │ + 7 │ module ExpressionAttribute = { + 8 │ let make = @res.hoistedFunction () => () + 9 │ } + 10 │ + + Unused attribute: @res.hoistedFunction +This attribute has no effect here. For example, some attributes are only meaningful in externals. \ No newline at end of file diff --git a/tests/build_tests/super_errors/fixtures/hoisted_function_export_collision.res b/tests/build_tests/super_errors/fixtures/hoisted_function_export_collision.res new file mode 100644 index 0000000000..ac642bfe4e --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/hoisted_function_export_collision.res @@ -0,0 +1,5 @@ +module One = { + @res.hoistedFunction + let make = () => () +} +let \"One$make" = () => () diff --git a/tests/build_tests/super_errors/fixtures/hoisted_function_hidden_by_signature.res b/tests/build_tests/super_errors/fixtures/hoisted_function_hidden_by_signature.res new file mode 100644 index 0000000000..7b50cba0d1 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/hoisted_function_hidden_by_signature.res @@ -0,0 +1,7 @@ +module A: { + let visible: unit => string +} = { + @res.hoistedFunction + let hidden = () => "hidden" + let visible = hidden +} diff --git a/tests/build_tests/super_errors/fixtures/hoisted_function_invalid_payload.res b/tests/build_tests/super_errors/fixtures/hoisted_function_invalid_payload.res new file mode 100644 index 0000000000..3f4a9a6b25 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/hoisted_function_invalid_payload.res @@ -0,0 +1,4 @@ +module Nested = { + @res.hoistedFunction("name") + let make = () => () +} diff --git a/tests/build_tests/super_errors/fixtures/hoisted_function_not_exportable.res b/tests/build_tests/super_errors/fixtures/hoisted_function_not_exportable.res new file mode 100644 index 0000000000..a4e0037fd3 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/hoisted_function_not_exportable.res @@ -0,0 +1,29 @@ +let run = () => { + @res.hoistedFunction + let local = () => () + local() +} + +let localModule = () => { + module Local = { + @res.hoistedFunction + let make = () => () + } + Local.make() +} +module Make = () => { + @res.hoistedFunction + let make = () => () +} +%%private( + @res.hoistedFunction + let privateMake = () => () +) + +let usePrivate = privateMake() +module Shadowed = { + @res.hoistedFunction + let make = () => "first" + let make = () => "second" +} +let after = () diff --git a/tests/build_tests/super_errors/fixtures/hoisted_function_path_collision.res b/tests/build_tests/super_errors/fixtures/hoisted_function_path_collision.res new file mode 100644 index 0000000000..c74d5feb43 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/hoisted_function_path_collision.res @@ -0,0 +1,9 @@ +module A = { + module B = { + @res.hoistedFunction + let make = () => () + } + @res.hoistedFunction + let \"B$make" = () => () +} +let after = () diff --git a/tests/build_tests/super_errors/fixtures/hoisted_function_unsupported_pattern.res b/tests/build_tests/super_errors/fixtures/hoisted_function_unsupported_pattern.res new file mode 100644 index 0000000000..258b059e26 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/hoisted_function_unsupported_pattern.res @@ -0,0 +1,4 @@ +module Nested = { + @res.hoistedFunction + let f as g = () => "ok" +} diff --git a/tests/build_tests/super_errors/fixtures/warning_101_bs_unused_attribute.res b/tests/build_tests/super_errors/fixtures/warning_101_bs_unused_attribute.res index 73dc385515..2206888afd 100644 --- a/tests/build_tests/super_errors/fixtures/warning_101_bs_unused_attribute.res +++ b/tests/build_tests/super_errors/fixtures/warning_101_bs_unused_attribute.res @@ -1,2 +1,9 @@ @as("foo") let x = 1 + +@res.hoistedFunction +type t = int + +module ExpressionAttribute = { + let make = @res.hoistedFunction () => () +} diff --git a/tests/tests/src/hoisted_function_attr.mjs b/tests/tests/src/hoisted_function_attr.mjs new file mode 100644 index 0000000000..3d46623238 --- /dev/null +++ b/tests/tests/src/hoisted_function_attr.mjs @@ -0,0 +1,186 @@ +// Generated by ReScript, PLEASE EDIT WITH CARE + + +function make() { + return "one"; +} + +function keep() { + return "one-keep"; +} + +let One = { + make: make, + keep: keep +}; + +function keep$1() { + return "two-keep"; +} + +function make$1() { + return "two"; +} + +function keep$2() { + return "two-inner-keep"; +} + +let Inner = { + make: make$1, + keep: keep$2 +}; + +let Two = { + keep: keep$1, + Inner: Inner +}; + +function keep$3() { + return "three-inner-keep"; +} + +function make$2() { + return "three"; +} + +function keep$4() { + return "three-deep-keep"; +} + +let Deep = { + make: make$2, + keep: keep$4 +}; + +let Inner$1 = { + keep: keep$3, + Deep: Deep +}; + +let Three = { + Inner: Inner$1 +}; + +function $$switch() { + return "keyword"; +} + +function $plus() { + return "dollar"; +} + +let Escaped = { + $$switch: $$switch, + $plus: $plus +}; + +function $plus$1() { + return "operator"; +} + +let Operator = { + $plus: $plus$1 +}; + +function make$3() { + return "nested"; +} + +let B = { + make: make$3 +}; + +function B$make() { + return "exotic"; +} + +let Ambiguous = { + B: B, + B$make: B$make +}; + +function value() { + return "recursive"; +} + +let RecursiveB = { + value: value +}; + +function make$4() { + return RecursiveB.value(); +} + +let RecursiveA = { + make: make$4 +}; + +function make$5() { + return "typed"; +} + +let Typed = { + make: make$5 +}; + +function make$6() { + return "coerced"; +} + +let Coerced = { + make: make$6 +}; + +let Included = { + make: make, + keep: keep +}; + +let Aliased; + +let Coerced$make = Coerced.make; + +let Typed$make = Typed.make; + +let RecursiveA$make = RecursiveA.make; + +let Ambiguous$B$make = B.make; + +let Operator$$plus = Operator.$plus; + +let Escaped$$plus = $plus; + +let Escaped$switch = $$switch; + +let Three$Inner$Deep$make = Three.Inner.Deep.make; + +let Two$Inner$make = Inner.make; + +let One$make = make; + +export { + One, + Two, + Three, + Escaped, + Operator, + Ambiguous, + RecursiveA, + RecursiveB, + Typed, + Coerced, + Included, + Aliased, + Coerced$make, + Typed$make, + RecursiveA$make, + Ambiguous$B$make, + Operator$$plus, + Escaped$$plus, + Escaped$switch, + Three$Inner$Deep$make, + Two$Inner$make, + One$make, +} +/* No side effect */ diff --git a/tests/tests/src/hoisted_function_attr.res b/tests/tests/src/hoisted_function_attr.res new file mode 100644 index 0000000000..149eabd560 --- /dev/null +++ b/tests/tests/src/hoisted_function_attr.res @@ -0,0 +1,84 @@ +module One = { + @res.hoistedFunction + let make = () => "one" + + let keep = () => "one-keep" +} + +module Two = { + let keep = () => "two-keep" + + module Inner = { + @res.hoistedFunction + let make = () => "two" + + let keep = () => "two-inner-keep" + } +} + +module Three = { + module Inner = { + let keep = () => "three-inner-keep" + + module Deep = { + @res.hoistedFunction + let make = () => "three" + + let keep = () => "three-deep-keep" + } + } +} + +module Escaped = { + @res.hoistedFunction + let \"switch" = () => "keyword" + + @res.hoistedFunction + let \"$plus" = () => "dollar" +} + +module Operator = { + @res.hoistedFunction + let \"+" = () => "operator" +} + +module Ambiguous = { + module B = { + @res.hoistedFunction + let make = () => "nested" + } + + let \"B$make" = () => "exotic" +} + +module rec RecursiveA: { + let make: unit => string +} = { + @res.hoistedFunction + let make = () => RecursiveB.value() +} +and RecursiveB: { + let value: unit => string +} = { + let value = () => "recursive" +} + +module Typed = { + @res.hoistedFunction + let make: unit => string = () => "typed" +} + +module Coerced: { + let make: unit => string +} = { + let hidden = () => "coerced" + + @res.hoistedFunction + let make = () => hidden() +} + +module Included = { + include One +} + +module Aliased = One diff --git a/tests/tests/src/hoisted_function_attr_test.mjs b/tests/tests/src/hoisted_function_attr_test.mjs new file mode 100644 index 0000000000..94f44dc7be --- /dev/null +++ b/tests/tests/src/hoisted_function_attr_test.mjs @@ -0,0 +1,31 @@ +// Generated by ReScript, PLEASE EDIT WITH CARE + +import * as Mocha from "mocha"; +import * as Test_utils from "./test_utils.mjs"; +import * as Hoisted_function_attr_use from "./hoisted_function_attr_use.mjs"; + +Mocha.describe("Hoisted_function_attr_test", () => { + Mocha.test("flat cross-module exports", () => { + Test_utils.eq("File \"hoisted_function_attr_test.res\", line 6, characters 7-14", Hoisted_function_attr_use.one, "one"); + Test_utils.eq("File \"hoisted_function_attr_test.res\", line 7, characters 7-14", Hoisted_function_attr_use.two, "two"); + Test_utils.eq("File \"hoisted_function_attr_test.res\", line 8, characters 7-14", Hoisted_function_attr_use.three, "three"); + }); + Mocha.test("exotic identifiers", () => { + Test_utils.eq("File \"hoisted_function_attr_test.res\", line 12, characters 7-14", Hoisted_function_attr_use.keyword, "keyword"); + Test_utils.eq("File \"hoisted_function_attr_test.res\", line 13, characters 7-14", Hoisted_function_attr_use.dollar, "dollar"); + Test_utils.eq("File \"hoisted_function_attr_test.res\", line 14, characters 7-14", Hoisted_function_attr_use.operator, "operator"); + }); + Mocha.test("structurally distinct paths", () => { + Test_utils.eq("File \"hoisted_function_attr_test.res\", line 18, characters 7-14", Hoisted_function_attr_use.nested, "nested"); + Test_utils.eq("File \"hoisted_function_attr_test.res\", line 19, characters 7-14", Hoisted_function_attr_use.exoticPath, "exotic"); + }); + Mocha.test("recursive modules", () => Test_utils.eq("File \"hoisted_function_attr_test.res\", line 23, characters 7-14", Hoisted_function_attr_use.recursive, "recursive")); + Mocha.test("explicit function type annotations", () => Test_utils.eq("File \"hoisted_function_attr_test.res\", line 27, characters 7-14", Hoisted_function_attr_use.typed, "typed")); + Mocha.test("signature coercion preserves hoists", () => Test_utils.eq("File \"hoisted_function_attr_test.res\", line 31, characters 7-14", Hoisted_function_attr_use.coerced, "coerced")); + Mocha.test("includes and aliases do not create additional hoists", () => { + Test_utils.eq("File \"hoisted_function_attr_test.res\", line 35, characters 7-14", Hoisted_function_attr_use.included, "one"); + Test_utils.eq("File \"hoisted_function_attr_test.res\", line 36, characters 7-14", Hoisted_function_attr_use.aliased, "one"); + }); +}); + +/* Not a pure module */ diff --git a/tests/tests/src/hoisted_function_attr_test.res b/tests/tests/src/hoisted_function_attr_test.res new file mode 100644 index 0000000000..e5e3212bfd --- /dev/null +++ b/tests/tests/src/hoisted_function_attr_test.res @@ -0,0 +1,38 @@ +open Mocha +open Test_utils + +describe(__MODULE__, () => { + test("flat cross-module exports", () => { + eq(__LOC__, Hoisted_function_attr_use.one, "one") + eq(__LOC__, Hoisted_function_attr_use.two, "two") + eq(__LOC__, Hoisted_function_attr_use.three, "three") + }) + + test("exotic identifiers", () => { + eq(__LOC__, Hoisted_function_attr_use.keyword, "keyword") + eq(__LOC__, Hoisted_function_attr_use.dollar, "dollar") + eq(__LOC__, Hoisted_function_attr_use.operator, "operator") + }) + + test("structurally distinct paths", () => { + eq(__LOC__, Hoisted_function_attr_use.nested, "nested") + eq(__LOC__, Hoisted_function_attr_use.exoticPath, "exotic") + }) + + test("recursive modules", () => { + eq(__LOC__, Hoisted_function_attr_use.recursive, "recursive") + }) + + test("explicit function type annotations", () => { + eq(__LOC__, Hoisted_function_attr_use.typed, "typed") + }) + + test("signature coercion preserves hoists", () => { + eq(__LOC__, Hoisted_function_attr_use.coerced, "coerced") + }) + + test("includes and aliases do not create additional hoists", () => { + eq(__LOC__, Hoisted_function_attr_use.included, "one") + eq(__LOC__, Hoisted_function_attr_use.aliased, "one") + }) +}) diff --git a/tests/tests/src/hoisted_function_attr_use.mjs b/tests/tests/src/hoisted_function_attr_use.mjs new file mode 100644 index 0000000000..979b206242 --- /dev/null +++ b/tests/tests/src/hoisted_function_attr_use.mjs @@ -0,0 +1,55 @@ +// Generated by ReScript, PLEASE EDIT WITH CARE + +import * as Hoisted_function_attr from "./hoisted_function_attr.mjs"; + +let one = Hoisted_function_attr.One$make(); + +let oneKeep = Hoisted_function_attr.One.keep(); + +let two = Hoisted_function_attr.Two$Inner$make(); + +let twoKeep = Hoisted_function_attr.Two.Inner.keep(); + +let three = Hoisted_function_attr.Three$Inner$Deep$make(); + +let threeKeep = Hoisted_function_attr.Three.Inner.Deep.keep(); + +let keyword = Hoisted_function_attr.Escaped$switch(); + +let dollar = Hoisted_function_attr.Escaped$$plus(); + +let operator = Hoisted_function_attr.Operator$$plus(); + +let nested = Hoisted_function_attr.Ambiguous$B$make(); + +let exoticPath = Hoisted_function_attr.Ambiguous.B$make(); + +let recursive = Hoisted_function_attr.RecursiveA$make(); + +let typed = Hoisted_function_attr.Typed$make(); + +let coerced = Hoisted_function_attr.Coerced$make(); + +let included = Hoisted_function_attr.Included.make(); + +let aliased = Hoisted_function_attr.One$make(); + +export { + one, + oneKeep, + two, + twoKeep, + three, + threeKeep, + keyword, + dollar, + operator, + nested, + exoticPath, + recursive, + typed, + coerced, + included, + aliased, +} +/* one Not a pure module */ diff --git a/tests/tests/src/hoisted_function_attr_use.res b/tests/tests/src/hoisted_function_attr_use.res new file mode 100644 index 0000000000..28470c4c06 --- /dev/null +++ b/tests/tests/src/hoisted_function_attr_use.res @@ -0,0 +1,20 @@ +let one = Hoisted_function_attr.One.make() +let oneKeep = Hoisted_function_attr.One.keep() + +let two = Hoisted_function_attr.Two.Inner.make() +let twoKeep = Hoisted_function_attr.Two.Inner.keep() + +let three = Hoisted_function_attr.Three.Inner.Deep.make() +let threeKeep = Hoisted_function_attr.Three.Inner.Deep.keep() + +let keyword = Hoisted_function_attr.Escaped.\"switch"() +let dollar = Hoisted_function_attr.Escaped.\"$plus"() +let operator = Hoisted_function_attr.Operator.\"+"() + +let nested = Hoisted_function_attr.Ambiguous.B.make() +let exoticPath = Hoisted_function_attr.Ambiguous.\"B$make"() +let recursive = Hoisted_function_attr.RecursiveA.make() +let typed = Hoisted_function_attr.Typed.make() +let coerced = Hoisted_function_attr.Coerced.make() +let included = Hoisted_function_attr.Included.make() +let aliased = Hoisted_function_attr.Aliased.make()