Skip to content

Commit 6dfddc0

Browse files
committed
Add @res.hoistedFunction support for flat JS export
1 parent afe06dd commit 6dfddc0

15 files changed

Lines changed: 415 additions & 50 deletions

compiler/core/js_cmj_format.ml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ type effect_ = string option
3737

3838
let single_na = Single Lam_arity.na
3939

40+
let module_field_path_key segments = String.concat "$" segments
41+
4042
type keyed_cmj_value = {
4143
name: string;
4244
arity: arity;
@@ -47,12 +49,17 @@ type keyed_cmj_values = keyed_cmj_value array
4749

4850
type t = {
4951
values: keyed_cmj_values;
52+
(* Full source paths that can be read through flat JS exports. The key uses
53+
[module_field_path_key], so source path [A.B.make] is stored as
54+
[A$B$make]. The same string is also the emitted export name. *)
55+
hoisted_values: string 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 (Set_string.elements hoisted_values);
6472
pure = effect_ = None;
6573
package_spec;
6674
case;
@@ -158,6 +166,10 @@ let query_by_name (cmj_table : t) name : keyed_cmj_value =
158166
let values = cmj_table.values in
159167
binary_search values name
160168

169+
let has_hoisted_value (cmj_table : t) name =
170+
Array.exists (fun hoisted_name -> Ext_string.equal hoisted_name name)
171+
cmj_table.hoisted_values
172+
161173
type path = string
162174

163175
type cmj_load_info = {

compiler/core/js_cmj_format.mli

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,22 +62,35 @@ type keyed_cmj_value = {
6262

6363
type t = {
6464
values: keyed_cmj_value array;
65+
(* Full source paths that can be read through flat JS exports. The key uses
66+
[module_field_path_key], so source path [A.B.make] is stored as
67+
[A$B$make]. The same string is also the emitted export name. *)
68+
hoisted_values: string array;
6569
pure: bool;
6670
package_spec: Js_packages_info.t;
6771
case: Ext_js_file_kind.case;
6872
}
6973

7074
val make :
7175
values:cmj_value Map_string.t ->
76+
hoisted_values:Set_string.t ->
7277
effect_:effect_ ->
7378
package_spec:Js_packages_info.t ->
7479
case:Ext_js_file_kind.case ->
7580
t
7681

7782
val query_by_name : t -> string -> keyed_cmj_value
7883

84+
val has_hoisted_value : t -> string -> bool
85+
7986
val single_na : arity
8087

88+
(* Encode a full module field path as the string key used in .cmj tables.
89+
This is only needed when the whole path is itself a recorded value, such as
90+
a hoisted [A.B.make] export. Ordinary nested module metadata is still read
91+
from the parent field, such as [A], and then interpreted by field index. *)
92+
val module_field_path_key : string list -> string
93+
8194
val from_file : string -> t
8295

8396
val from_file_with_digest : string -> t * Digest.t

compiler/core/lam_compile.ml

Lines changed: 75 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,40 @@ type initialization = J.block
232232
*)
233233

234234
let compile output_prefix =
235+
(* When compiling a read from another module, a nested source path like
236+
Other.A.B.make reaches this point as nested module-field reads:
237+
238+
Pfield "make" (Pfield "B" (Pfield "A" (Lglobal_module Other)))
239+
240+
Normal compilation does not look up the full path. It only queries the
241+
first field, "A", and then emits the remaining fields as JS property
242+
access: Other.A.B.make. The "A" lookup may include Submodule arity data,
243+
but it does not say whether A.B.make has a separate root-level export.
244+
245+
Hoisted functions need that extra question. For them, query the separate
246+
hoisted-values table with the full source path A.B.make. If present, the
247+
same key is the root-level JS export name, for example Other.A$B$make.
248+
The table is only a marker set; normal export metadata still lives in the
249+
regular .cmj values table. *)
250+
let rec extract_field_path segments primitive args =
251+
match (primitive, args) with
252+
| ( Lam_primitive.Pfield (_, Fld_module {name}),
253+
[Lam.Lprim {primitive; args; _}] ) ->
254+
extract_field_path (name :: segments) primitive args
255+
| ( Lam_primitive.Pfield (_, Fld_module {name}),
256+
[Lam.Lglobal_module (id, dynamic_import)] ) ->
257+
Some (id, dynamic_import, name :: segments)
258+
| _ -> None
259+
in
260+
let hoisted_external_field_name primitive args =
261+
match extract_field_path [] primitive args with
262+
| Some (id, dynamic_import, (_ :: _ :: _ as segments)) ->
263+
let name = Js_cmj_format.module_field_path_key segments in
264+
if Lam_compile_env.has_hoisted_external_id ~dynamic_import id name then
265+
Some (id, dynamic_import, name)
266+
else None
267+
| Some (_, _, ([] | [_])) | None -> None
268+
in
235269
let rec compile_external_field (* Like [List.empty]*)
236270
?(dynamic_import = false) (lamba_cxt : Lam_compile_context.t)
237271
(id : Ident.t) name : Js_output.t =
@@ -1637,17 +1671,47 @@ let compile output_prefix =
16371671
fn_code args))
16381672
and compile_prim (prim_info : Lam.prim_info)
16391673
(lambda_cxt : Lam_compile_context.t) =
1674+
let compile_primitive_default primitive args loc =
1675+
let args_block, args_expr =
1676+
if args = [] then ([], [])
1677+
else
1678+
let new_cxt = {lambda_cxt with continuation = NeedValue Not_tail} in
1679+
Ext_list.split_map args (fun x ->
1680+
match compile_lambda new_cxt x with
1681+
| {block; value = Some b} -> (block, b)
1682+
| {value = None} -> assert false)
1683+
in
1684+
let args_code : J.block = List.concat args_block in
1685+
let exp =
1686+
(* TODO: all can be done in [compile_primitive] *)
1687+
Lam_compile_primitive.translate output_prefix loc lambda_cxt primitive
1688+
args_expr
1689+
in
1690+
Js_output.output_of_block_and_expression lambda_cxt.continuation args_code
1691+
exp
1692+
in
16401693
match prim_info with
1641-
| {
1642-
primitive = Pfield (_, fld_info);
1643-
args = [Lglobal_module (id, dynamic_import)];
1644-
_;
1645-
} -> (
1646-
(* should be before Lglobal_global *)
1647-
match fld_info with
1648-
| Fld_module {name = field} ->
1649-
compile_external_field ~dynamic_import lambda_cxt id field
1650-
| _ -> assert false)
1694+
| {primitive = Pfield (_, Fld_module _); _} -> (
1695+
match hoisted_external_field_name prim_info.primitive prim_info.args with
1696+
| Some (id, dynamic_import, hoisted_name) ->
1697+
Js_output.output_of_expression lambda_cxt.continuation
1698+
~no_effects:no_effects_const
1699+
(E.ml_var_dot ~dynamic_import id hoisted_name)
1700+
| None -> (
1701+
match prim_info with
1702+
| {
1703+
primitive = Pfield (_, fld_info);
1704+
args = [Lglobal_module (id, dynamic_import)];
1705+
_;
1706+
} -> (
1707+
(* should be before Lglobal_global *)
1708+
match fld_info with
1709+
| Fld_module {name = field} ->
1710+
compile_external_field ~dynamic_import lambda_cxt id field
1711+
| _ -> assert false)
1712+
| _ ->
1713+
compile_primitive_default prim_info.primitive prim_info.args
1714+
prim_info.loc))
16511715
| {primitive = Praise; args = [e]; _} -> (
16521716
match
16531717
compile_lambda {lambda_cxt with continuation = NeedValue Not_tail} e
@@ -1816,24 +1880,7 @@ let compile output_prefix =
18161880
Location.raise_errorf ~loc
18171881
"Invalid argument: unsupported argument to dynamic import. If you \
18181882
believe this should be supported, please open an issue.")
1819-
| {primitive; args; loc} ->
1820-
let args_block, args_expr =
1821-
if args = [] then ([], [])
1822-
else
1823-
let new_cxt = {lambda_cxt with continuation = NeedValue Not_tail} in
1824-
Ext_list.split_map args (fun x ->
1825-
match compile_lambda new_cxt x with
1826-
| {block; value = Some b} -> (block, b)
1827-
| {value = None} -> assert false)
1828-
in
1829-
let args_code : J.block = List.concat args_block in
1830-
let exp =
1831-
(* TODO: all can be done in [compile_primitive] *)
1832-
Lam_compile_primitive.translate output_prefix loc lambda_cxt primitive
1833-
args_expr
1834-
in
1835-
Js_output.output_of_block_and_expression lambda_cxt.continuation args_code
1836-
exp
1883+
| {primitive; args; loc} -> compile_primitive_default primitive args loc
18371884
and compile_lambda (lambda_cxt : Lam_compile_context.t) (cur_lam : Lam.t) :
18381885
Js_output.t =
18391886
match cur_lam with

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 has_hoisted_external_id ?(dynamic_import = false) (module_id : Ident.t)
103+
(name : string) : bool =
104+
let cmj_table = cmj_table_of_module_id ~dynamic_import module_id in
105+
Js_cmj_format.has_hoisted_value cmj_table name
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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ val query_external_id_info :
7171
will raise if not found
7272
*)
7373

74+
val has_hoisted_external_id : ?dynamic_import:bool -> Ident.t -> string -> bool
75+
7476
val is_pure_module : Lam_module_ident.t -> bool
7577

7678
val get_package_path_from_cmj :

compiler/core/lam_compile_main.ml

Lines changed: 102 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,78 @@ let no_side_effects (rest : Lam_group.t list) : string option =
115115
Some ""
116116
else None (* TODO :*))
117117

118+
(* Materialize JS-hoisted values as root-level aliases and exports. The source
119+
value still lives at its normal module path, but downstream tools can import
120+
the flat name directly when the .cmj metadata marks it as hoisted. *)
121+
let js_hoisted_aliases (export_idents : Set_ident.t) (groups : Lam_group.t list)
122+
=
123+
let group_map =
124+
Ext_list.fold_left groups Map_ident.empty (fun acc group ->
125+
match group with
126+
| Single (_, id, lam) -> Map_ident.add acc id lam
127+
| Recursive bindings ->
128+
Ext_list.fold_left bindings acc (fun acc (id, lam) ->
129+
Map_ident.add acc id lam)
130+
| Nop _ -> acc)
131+
in
132+
let rec access loc base fields =
133+
match fields with
134+
| [] -> base
135+
| (pos, name) :: fields ->
136+
access loc
137+
(Lam.prim
138+
~primitive:(Lam_primitive.Pfield (pos, Lam_compat.Fld_module {name}))
139+
~args:[base] loc)
140+
fields
141+
in
142+
let resolve = function
143+
| Lam.Lvar id as lam -> (
144+
match Map_ident.find_opt group_map id with
145+
| Some resolved -> resolved
146+
| None -> lam)
147+
| lam -> lam
148+
in
149+
let rec fold_module_fields fields args index acc f =
150+
match (fields, args) with
151+
| [], [] -> acc
152+
| field :: fields, arg :: args ->
153+
fold_module_fields fields args (index + 1) (f index field arg acc) f
154+
| _, _ -> invalid_arg "fold_module_fields"
155+
in
156+
let rec scan top_id path acc lam =
157+
match resolve lam with
158+
| Lam.Lprim
159+
{
160+
primitive =
161+
Lam_primitive.Pmakeblock (_, Blk_module fields, _);
162+
args;
163+
loc;
164+
} ->
165+
fold_module_fields fields args 0 acc (fun pos field arg acc ->
166+
let path = path @ [(pos, field)] in
167+
let acc = scan top_id path acc arg in
168+
match arg with
169+
| Lam.Lvar id when Ident.js_hoisted id ->
170+
let name =
171+
Js_cmj_format.module_field_path_key
172+
(top_id.Ident.name
173+
:: Ext_list.map path (fun (_pos, name) -> name))
174+
in
175+
let alias_id = Ident.create name in
176+
Ident.make_js_hoisted alias_id;
177+
let alias =
178+
access loc (Lam.var top_id) path
179+
in
180+
(Lam_group.Single (Alias, alias_id, alias), alias_id, alias) :: acc
181+
| _ -> acc)
182+
| _ -> acc
183+
in
184+
Ext_list.fold_left groups [] (fun acc group ->
185+
match group with
186+
| Single (_, id, lam) when Set_ident.mem export_idents id ->
187+
scan id [] acc lam
188+
| Single _ | Recursive _ | Nop _ -> acc)
189+
118190

119191
let _d = fun s lam ->
120192
#ifndef RELEASE
@@ -227,6 +299,28 @@ let () =
227299
in
228300
#endif
229301
let maybe_pure = no_side_effects groups in
302+
(* Add the generated alias groups before JS lowering so regular export
303+
printing, tree shaking, and .cmj metadata all see the flat runtime value. *)
304+
let hoisted_aliases = js_hoisted_aliases meta.export_idents groups in
305+
let hoisted_groups, hoisted_exports, hoisted_export_map =
306+
Ext_list.fold_left hoisted_aliases ([], [], Map_ident.empty)
307+
(fun (groups, exports, export_map) (group, id, lam) ->
308+
(group :: groups, id :: exports, Map_ident.add export_map id lam))
309+
in
310+
let groups = groups @ List.rev hoisted_groups in
311+
let meta =
312+
{
313+
meta with
314+
exports = meta.exports @ List.rev hoisted_exports;
315+
export_idents =
316+
Ext_list.fold_left hoisted_exports meta.export_idents (fun acc id ->
317+
Set_ident.add acc id);
318+
}
319+
in
320+
let export_map =
321+
Map_ident.fold hoisted_export_map coerced_input.export_map (fun id lam acc ->
322+
Map_ident.add acc id lam)
323+
in
230324
#ifndef RELEASE
231325
let () = Ext_log.dwarn ~__POS__ "\n@[[TIME:]Pre-compile: %f@]@." (Sys.time () *. 1000.) in
232326
#endif
@@ -277,18 +371,18 @@ js
277371
)
278372
in
279373
Warnings.check_fatal();
280-
let effect_ =
374+
let effect_ =
281375
Lam_stats_export.get_dependent_module_effect
282-
maybe_pure external_module_ids in
283-
let v : Js_cmj_format.t =
284-
Lam_stats_export.export_to_cmj
285-
meta
286-
effect_
287-
coerced_input.export_map
376+
maybe_pure external_module_ids in
377+
let v : Js_cmj_format.t =
378+
Lam_stats_export.export_to_cmj
379+
meta
380+
effect_
381+
export_map
288382
(if Ext_char.is_lower_case (Filename.basename output_prefix).[0] then Little else Upper)
289383
in
290384
(if not !Clflags.dont_write_files then
291-
Js_cmj_format.to_file
385+
Js_cmj_format.to_file
292386
~check_exists:(not !Js_config.force_cmj)
293387
(output_prefix ^ Literals.suffix_cmj) v);
294388
{J.program = program ; side_effect = effect_ ; modules = external_module_ids }

0 commit comments

Comments
 (0)