Skip to content

Commit b0e30dd

Browse files
committed
Add @res.hoistedFunction support for flat JS export
Signed-off-by: Christoph Knittel <ck@cca.io>
1 parent cf8dd8c commit b0e30dd

21 files changed

Lines changed: 572 additions & 46 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
#### :rocket: New Feature
2525

26+
- Add `@res.hoistedFunction` for emitting nested module functions as flat JavaScript exports. https://github.com/rescript-lang/rescript/pull/8402
2627
- Add source map support with linked, inline, and hidden modes. https://github.com/rescript-lang/rescript/pull/8393
2728
- 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
2829

compiler/core/js_cmj_format.ml

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,21 @@ type keyed_cmj_value = {
4545

4646
type keyed_cmj_values = keyed_cmj_value array
4747

48+
type hoisted_value = {
49+
path: string list; (** Exact source-level module path segments. *)
50+
name: string; (** Flat compiler identifier used for the public JS export. *)
51+
}
52+
4853
type t = {
4954
values: keyed_cmj_values;
55+
hoisted_values: hoisted_value array;
5056
pure: bool;
5157
package_spec: Js_packages_info.t;
5258
case: Ext_js_file_kind.case;
5359
}
5460

55-
let make ~(values : cmj_value Map_string.t) ~effect_ ~package_spec ~case : t =
61+
let make ~(values : cmj_value Map_string.t) ~hoisted_values ~effect_
62+
~package_spec ~case : t =
5663
{
5764
values =
5865
Map_string.to_sorted_array_with_f values (fun k v ->
@@ -61,6 +68,7 @@ let make ~(values : cmj_value Map_string.t) ~effect_ ~package_spec ~case : t =
6168
arity = v.arity;
6269
persistent_closed_lambda = v.persistent_closed_lambda;
6370
});
71+
hoisted_values = Array.of_list hoisted_values;
6472
pure = effect_ = None;
6573
package_spec;
6674
case;
@@ -97,7 +105,8 @@ let to_file name ~check_exists (v : t) =
97105
output_string oc s;
98106
close_out oc)
99107

100-
let key_comp (a : string) b = Map_string.compare_key a b.name
108+
let key_comp (a : string) (b : keyed_cmj_value) =
109+
Map_string.compare_key a b.name
101110

102111
let not_found key =
103112
{name = key; arity = single_na; persistent_closed_lambda = None}
@@ -151,6 +160,13 @@ let query_by_name (cmj_table : t) name : keyed_cmj_value =
151160
let values = cmj_table.values in
152161
binary_search values name
153162

163+
let find_hoisted_value (cmj_table : t) path =
164+
Array.find_map
165+
(fun value ->
166+
if List.equal Ext_string.equal value.path path then Some value.name
167+
else None)
168+
cmj_table.hoisted_values
169+
154170
type path = string
155171

156172
type cmj_load_info = {

compiler/core/js_cmj_format.mli

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,22 +60,31 @@ type keyed_cmj_value = {
6060
persistent_closed_lambda: Lam.t option;
6161
}
6262

63+
type hoisted_value = {
64+
path: string list; (** Exact source-level module path segments. *)
65+
name: string; (** Flat compiler identifier used for the public JS export. *)
66+
}
67+
6368
type t = {
6469
values: keyed_cmj_value array;
70+
hoisted_values: hoisted_value array;
6571
pure: bool;
6672
package_spec: Js_packages_info.t;
6773
case: Ext_js_file_kind.case;
6874
}
6975

7076
val make :
7177
values:cmj_value Map_string.t ->
78+
hoisted_values:hoisted_value list ->
7279
effect_:effect_ ->
7380
package_spec:Js_packages_info.t ->
7481
case:Ext_js_file_kind.case ->
7582
t
7683

7784
val query_by_name : t -> string -> keyed_cmj_value
7885

86+
val find_hoisted_value : t -> string list -> string option
87+
7988
val single_na : arity
8089

8190
val from_file : string -> t

compiler/core/lam_compile.ml

Lines changed: 74 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,39 @@ type initialization = J.block
290290
*)
291291

292292
let compile output_prefix =
293+
(* When compiling a read from another module, a nested source path like
294+
Other.A.B.make reaches this point as nested module-field reads:
295+
296+
Pfield "make" (Pfield "B" (Pfield "A" (Lglobal_module Other)))
297+
298+
Normal compilation does not look up the full path. It only queries the
299+
first field, "A", and then emits the remaining fields as JS property
300+
access: Other.A.B.make. The "A" lookup may include Submodule arity data,
301+
but it does not say whether A.B.make has a separate root-level export.
302+
303+
Hoisted functions need that extra question. For them, query the separate
304+
hoisted-values table with an unambiguous key for source path A.B.make. If
305+
present, the table returns the root-level JS export name, for example
306+
A$B$make. Normal export metadata still lives in the regular .cmj values
307+
table. *)
308+
let rec extract_field_path segments primitive args =
309+
match (primitive, args) with
310+
| ( Lam_primitive.Pfield (_, Fld_module {name}),
311+
[Lam.Lprim {primitive; args; _}] ) ->
312+
extract_field_path (name :: segments) primitive args
313+
| ( Lam_primitive.Pfield (_, Fld_module {name}),
314+
[Lam.Lglobal_module (id, dynamic_import)] ) ->
315+
Some (id, dynamic_import, name :: segments)
316+
| _ -> None
317+
in
318+
let hoisted_external_field_name primitive args =
319+
match extract_field_path [] primitive args with
320+
| Some (id, dynamic_import, (_ :: _ :: _ as segments)) ->
321+
Ext_option.map
322+
(Lam_compile_env.find_hoisted_external_id ~dynamic_import id segments)
323+
(fun name -> (id, dynamic_import, name))
324+
| Some (_, _, ([] | [_])) | None -> None
325+
in
293326
let rec compile_external_field (* Like [List.empty]*)
294327
?(dynamic_import = false) (lamba_cxt : Lam_compile_context.t)
295328
(id : Ident.t) name : Js_output.t =
@@ -1718,17 +1751,47 @@ let compile output_prefix =
17181751
fn_code args)))
17191752
and compile_prim (prim_info : Lam.prim_info)
17201753
(lambda_cxt : Lam_compile_context.t) =
1754+
let compile_primitive_default primitive args loc =
1755+
let args_block, args_expr =
1756+
if args = [] then ([], [])
1757+
else
1758+
let new_cxt = {lambda_cxt with continuation = NeedValue Not_tail} in
1759+
Ext_list.split_map args (fun x ->
1760+
match compile_lambda new_cxt x with
1761+
| {block; value = Some b} -> (block, b)
1762+
| {value = None} -> assert false)
1763+
in
1764+
let args_code : J.block = List.concat args_block in
1765+
let exp =
1766+
(* TODO: all can be done in [compile_primitive] *)
1767+
Lam_compile_primitive.translate output_prefix loc lambda_cxt primitive
1768+
args_expr
1769+
in
1770+
Js_output.output_of_block_and_expression lambda_cxt.continuation args_code
1771+
(with_source_loc loc exp)
1772+
in
17211773
match prim_info with
1722-
| {
1723-
primitive = Pfield (_, fld_info);
1724-
args = [Lglobal_module (id, dynamic_import)];
1725-
_;
1726-
} -> (
1727-
(* should be before Lglobal_global *)
1728-
match fld_info with
1729-
| Fld_module {name = field} ->
1730-
compile_external_field ~dynamic_import lambda_cxt id field
1731-
| _ -> assert false)
1774+
| {primitive = Pfield (_, Fld_module _); _} -> (
1775+
match hoisted_external_field_name prim_info.primitive prim_info.args with
1776+
| Some (id, dynamic_import, hoisted_name) ->
1777+
Js_output.output_of_expression lambda_cxt.continuation
1778+
~no_effects:no_effects_const
1779+
(E.ml_var_dot ~dynamic_import id hoisted_name)
1780+
| None -> (
1781+
match prim_info with
1782+
| {
1783+
primitive = Pfield (_, fld_info);
1784+
args = [Lglobal_module (id, dynamic_import)];
1785+
_;
1786+
} -> (
1787+
(* should be before Lglobal_global *)
1788+
match fld_info with
1789+
| Fld_module {name = field} ->
1790+
compile_external_field ~dynamic_import lambda_cxt id field
1791+
| _ -> assert false)
1792+
| _ ->
1793+
compile_primitive_default prim_info.primitive prim_info.args
1794+
prim_info.loc))
17321795
| {primitive = Praise; args = [e]; loc} -> (
17331796
match
17341797
compile_lambda {lambda_cxt with continuation = NeedValue Not_tail} e
@@ -1898,24 +1961,7 @@ let compile output_prefix =
18981961
Location.raise_errorf ~loc
18991962
"Invalid argument: unsupported argument to dynamic import. If you \
19001963
believe this should be supported, please open an issue.")
1901-
| {primitive; args; loc} ->
1902-
let args_block, args_expr =
1903-
if args = [] then ([], [])
1904-
else
1905-
let new_cxt = {lambda_cxt with continuation = NeedValue Not_tail} in
1906-
Ext_list.split_map args (fun x ->
1907-
match compile_lambda new_cxt x with
1908-
| {block; value = Some b} -> (block, b)
1909-
| {value = None} -> assert false)
1910-
in
1911-
let args_code : J.block = List.concat args_block in
1912-
let exp =
1913-
(* TODO: all can be done in [compile_primitive] *)
1914-
Lam_compile_primitive.translate output_prefix loc lambda_cxt primitive
1915-
args_expr
1916-
in
1917-
Js_output.output_of_block_and_expression lambda_cxt.continuation args_code
1918-
(with_source_loc loc exp)
1964+
| {primitive; args; loc} -> compile_primitive_default primitive args loc
19191965
and collect_dup_overrides (copy_id : Ident.t) (lam : Lam.t)
19201966
(acc : (Lam_compat.set_field_dbg_info * Lam.t) list) :
19211967
(Lam_compat.set_field_dbg_info * Lam.t) list option =

compiler/core/lam_compile_env.ml

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -84,20 +84,26 @@ let add_js_module ?import_attributes
8484
id
8585
| Some old_key -> old_key.id
8686

87+
let cmj_table_of_module_id ~dynamic_import (module_id : Ident.t) =
88+
let oid = Lam_module_ident.of_ml ~dynamic_import module_id in
89+
match Lam_module_ident.Hash.find_opt cached_tbl oid with
90+
| None ->
91+
let cmj_load_info = !Js_cmj_load.load_unit module_id.name in
92+
oid +> Ml cmj_load_info;
93+
cmj_load_info.cmj_table
94+
| Some (Ml {cmj_table}) -> cmj_table
95+
| Some External -> assert false
96+
8797
let query_external_id_info ?(dynamic_import = false) (module_id : Ident.t)
8898
(name : string) : ident_info =
89-
let oid = Lam_module_ident.of_ml ~dynamic_import module_id in
90-
let cmj_table =
91-
match Lam_module_ident.Hash.find_opt cached_tbl oid with
92-
| None ->
93-
let cmj_load_info = !Js_cmj_load.load_unit module_id.name in
94-
oid +> Ml cmj_load_info;
95-
cmj_load_info.cmj_table
96-
| Some (Ml {cmj_table}) -> cmj_table
97-
| Some External -> assert false
98-
in
99+
let cmj_table = cmj_table_of_module_id ~dynamic_import module_id in
99100
Js_cmj_format.query_by_name cmj_table name
100101

102+
let find_hoisted_external_id ?(dynamic_import = false) (module_id : Ident.t)
103+
(path : string list) : string option =
104+
let cmj_table = cmj_table_of_module_id ~dynamic_import module_id in
105+
Js_cmj_format.find_hoisted_value cmj_table path
106+
101107
let get_package_path_from_cmj (id : Lam_module_ident.t) :
102108
string * Js_packages_info.t * Ext_js_file_kind.case =
103109
let cmj_load_info =

compiler/core/lam_compile_env.mli

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ val query_external_id_info :
7171
will raise if not found
7272
*)
7373

74+
val find_hoisted_external_id :
75+
?dynamic_import:bool -> Ident.t -> string list -> string option
76+
7477
val is_pure_module : Lam_module_ident.t -> bool
7578

7679
val get_package_path_from_cmj :

0 commit comments

Comments
 (0)