Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitmodules
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
[submodule "vendor/melange-compiler-libs"]
path = vendor/melange-compiler-libs
url = https://github.com/melange-re/melange-compiler-libs.git
url = https://github.com/TheCBaH/melange-compiler-libs.git
12 changes: 12 additions & 0 deletions Changes.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
Unreleased
---------------

- BREAKING(code generation): name a module's runtime fields after the OCaml
namespace they come from, so that e.g. an exception `Foo` and a module `Foo`
can coexist in a structure instead of failing with `Foo are exported as
twice`. Extension constructors are emitted as `Foo$extension` and classes as
`foo$class`; values and modules keep their name. Mangled fields are also
exposed under their plain name when nothing else in the module claims it, so
JavaScript callers keep working


7.0.1-55 2026-07-12
---------------

Expand Down
8 changes: 7 additions & 1 deletion jscomp/core/j.ml
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,13 @@ type number =
| Uint of int32

type ident_info = Js_op.ident_info
type exports = Ident.t list
(* The unit's exported fields, with the namespace each comes from: that is what
decides the name they are exported under (see [Runtime_fields]).

NOTE: no doc comments in this file. [gen/gen_traversal.ml] parses it as an
interface and picks the first type group carrying attributes, which a doc
comment would silently make this one. *)
type exports = Runtime_fields.t list
type tag_info = Lam.Tag_info.t
type property_name = string

Expand Down
19 changes: 17 additions & 2 deletions jscomp/core/js_dump.ml
Original file line number Diff line number Diff line change
Expand Up @@ -772,8 +772,23 @@ and expression_desc cxt ~(level : int) x : cxt =
E.runtime_call ~module_name:Js_runtime_modules.option ~fn_name:"some"
[ e ])
| Caml_block { fields = el; tag_info = Blk_module fields; _ } ->
expression_desc cxt ~level
(Object (List.map_combine fields el ~f:(fun x -> Ident.convert x)))
let properties = List.map_combine fields el ~f:Ident.convert in
(* Fields renamed to keep OCaml namespaces apart in the single namespace
of a JS object are also exposed under their plain OCaml name, for the
benefit of JavaScript callers written against it. Only when nothing
else claims that name, and only for values that are free to repeat --
a coercion wrapper is not worth duplicating. *)
let aliases =
List.filter_map properties ~f:(fun (name, (e : J.expression)) ->
match (Runtime_fields.unmangle name, e.expression_desc) with
| Some plain, Var _
when not
(List.exists properties ~f:(fun (other, _) ->
String.equal other plain)) ->
Some (Ident.convert plain, e)
| _ -> None)
in
expression_desc cxt ~level (Object (properties @ aliases))
(*name convention of Record is slight different from modules*)
| Caml_block { fields = el; mutable_flag; tag_info = Blk_record fields; _ } ->
if block_has_all_int_fields fields then
Expand Down
39 changes: 23 additions & 16 deletions jscomp/core/js_dump_import_export.ml
Original file line number Diff line number Diff line change
Expand Up @@ -47,33 +47,40 @@ let print_es6_export f s export =
P.string f s);
P.string f L.comma)

let export_name cxt id =
let id_name = Ident.name id in
let s = Ident.convert id_name in
let export, cxt = Js_pp.Scope.str_of_ident cxt id in
let is_default = id_name = L.default in
let export_name cxt (field : Runtime_fields.t) =
let name = Runtime_fields.name field in
let s = Ident.convert name in
let export, cxt = Js_pp.Scope.str_of_ident cxt field.id in
let is_default = name = L.default in
let s = if is_default then L.default else s in
(s, export, is_default, cxt)

let iter_exports cxt f idents ~add_esmodule ~print_export =
let iter_exports cxt f fields ~add_esmodule ~print_export =
let first = ref true in
let print_one s export =
if !first then first := false else P.newline f;
print_export f s export
in
List.fold_left idents ~init:cxt ~f:(fun cxt id ->
let s, export, is_default, cxt = export_name cxt id in
List.fold_left fields ~init:cxt ~f:(fun cxt (field : Runtime_fields.t) ->
let s, export, is_default, cxt = export_name cxt field in
print_one s export;
(* Fields whose name is mangled to keep OCaml namespaces apart are also
exported under their plain OCaml name, so that JavaScript code written
against it keeps working. Skipped when another export answers to that
name already. *)
(match Runtime_fields.compat_alias ~fields field with
| Some alias -> print_one (Ident.convert alias) export
| None -> ());
if add_esmodule && is_default then (
P.newline f;
print_export f "__esModule" "true");
cxt)

(* Print exports in CommonJS format *)
let module_exports cxt f (idents : Ident.t list) =
match idents with
let module_exports cxt f (fields : Runtime_fields.t list) =
match fields with
| [] -> cxt
| idents ->
| fields ->
P.at_least_two_lines f;
P.string f L.module_;
P.string f L.dot;
Expand All @@ -82,19 +89,19 @@ let module_exports cxt f (idents : Ident.t list) =
P.string f L.eq;
P.space f;
P.brace_vgroup f 1 (fun () ->
iter_exports cxt f idents ~add_esmodule:true
iter_exports cxt f fields ~add_esmodule:true
~print_export:print_commonjs_export)

(** Print module in ES6 format, it is ES6, trailing comma is valid ES6 code *)
let es6_export cxt f (idents : Ident.t list) =
match idents with
let es6_export cxt f (fields : Runtime_fields.t list) =
match fields with
| [] -> cxt
| idents ->
| fields ->
P.at_least_two_lines f;
P.string f L.export;
P.space f;
P.brace_vgroup f 1 (fun () ->
iter_exports cxt f idents ~add_esmodule:false
iter_exports cxt f fields ~add_esmodule:false
~print_export:print_es6_export)

type module_ = { id : Ident.t; path : string; default : bool }
Expand Down
7 changes: 5 additions & 2 deletions jscomp/core/js_dump_import_export.mli
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,11 @@

open Import

val module_exports : Js_pp.Scope.t -> Js_pp.t -> Ident.t list -> Js_pp.Scope.t
val es6_export : Js_pp.Scope.t -> Js_pp.t -> Ident.t list -> Js_pp.Scope.t
val module_exports :
Js_pp.Scope.t -> Js_pp.t -> Runtime_fields.t list -> Js_pp.Scope.t

val es6_export :
Js_pp.Scope.t -> Js_pp.t -> Runtime_fields.t list -> Js_pp.Scope.t

type module_ = { id : Ident.t; path : string; default : bool }

Expand Down
36 changes: 24 additions & 12 deletions jscomp/core/lam_coercion.ml
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ open Import
*)

type t = {
export_list : Ident.t list;
export_list : Runtime_fields.t list;
export_set : Ident.Set.t;
export_map : Lam.t Ident.Map.t;
(* not used in code generation, mostly used
Expand All @@ -83,30 +83,36 @@ type t = {

let export_map t = t.export_map
let groups t = t.groups
let export_idents export_list = List.map export_list ~f:Runtime_fields.id

let handle_exports (meta : Lam_stats.t) (lambda_exports : Lam.t list)
(reverse_input : Lam_group.t list) =
let (original_exports : Ident.t list) = meta.exports in
let (original_exports : Runtime_fields.t list) = meta.exports in
let (original_export_set : Ident.Set.t) = meta.export_idents in
let len = List.length original_exports in
let tbl = String.Hashtbl.create len in
let ({ export_list; export_set; _ } as result) =
List.fold_right2
~f:(fun (original_export_id : Ident.t) (lam : Lam.t) (acc : t) ->
~f:(fun (original_export : Runtime_fields.t) (lam : Lam.t) (acc : t) ->
let original_export_id = Runtime_fields.id original_export in
let original_name = Ident.name original_export_id in
(* Two fields of different namespaces can share [original_name]; what
has to be unique is the name they are given at runtime. *)
let runtime_name = Runtime_fields.name original_export in
let already_present =
let already_present = String.Hashtbl.mem tbl original_name in
String.Hashtbl.replace tbl ~key:original_name ~data:();
let already_present = String.Hashtbl.mem tbl runtime_name in
String.Hashtbl.replace tbl ~key:runtime_name ~data:();
already_present
in
if already_present then
Mel_exception.error (Mel_duplicate_exports original_name);
Mel_exception.error (Mel_duplicate_exports runtime_name);
let export_field id = { original_export with Runtime_fields.id } in
match lam with
| Lvar id | Lmutvar id ->
if Ident.name id = original_name then
{
acc with
export_list = id :: acc.export_list;
export_list = export_field id :: acc.export_list;
export_set =
(if Ident.stamp id = Ident.stamp original_export_id then
acc.export_set
Expand All @@ -120,7 +126,7 @@ let handle_exports (meta : Lam_stats.t) (lambda_exports : Lam.t list)
Lam_util.alias_ident_or_global meta newid id NA;
{
acc with
export_list = newid :: acc.export_list;
export_list = export_field newid :: acc.export_list;
export_map = Ident.Map.add ~key:newid ~data:lam acc.export_map;
groups =
Single
Expand Down Expand Up @@ -150,7 +156,8 @@ let handle_exports (meta : Lam_stats.t) (lambda_exports : Lam.t list)
of size 4 instead of 2
*)
let newid = Ident.rename original_export_id in
(let arity = Lam_arity_analysis.get_arity meta lam in
let () =
let arity = Lam_arity_analysis.get_arity meta lam in
if not (Lam_arity.first_arity_na arity) then
Ident.Hashtbl.add meta.ident_tbl ~key:newid
~data:
Expand All @@ -162,10 +169,11 @@ let handle_exports (meta : Lam_stats.t) (lambda_exports : Lam.t list)
| Lfunction _ -> Some (lam, Lam_non_rec)
| _ -> None);
call_summary = Lam_call_summary.Unknown;
}));
})
in
{
acc with
export_list = newid :: acc.export_list;
export_list = export_field newid :: acc.export_list;
export_map = Ident.Map.add ~key:newid ~data:lam acc.export_map;
groups = Single (Strict, newid, lam) :: acc.groups;
})
Expand Down Expand Up @@ -193,7 +201,11 @@ let handle_exports (meta : Lam_stats.t) (lambda_exports : Lam.t list)
~init:(result.export_map, result.groups)
reverse_input
in
{ result with export_map; groups = Lam_dce.remove export_list coerced_input }
{
result with
export_map;
groups = Lam_dce.remove (export_idents export_list) coerced_input;
}

(* TODO: more flattening,
- also for function compilation, flattening should be done first
Expand Down
11 changes: 7 additions & 4 deletions jscomp/core/lam_compile_main.cppo.ml
Original file line number Diff line number Diff line change
Expand Up @@ -116,14 +116,17 @@ let compile ~package_info (output_prefix: string) (lam: Lambda.lambda) =
in

let export_idents = Translmod.get_export_identifiers() in
let export_ident_sets = Ident.Set.of_list export_idents in
let export_ident_sets =
Ident.Set.of_list (List.map export_idents ~f:Runtime_fields.id)
in
(* To make toplevel happy - reentrant for js-demo *)
let () =
#ifndef MELANGE_RELEASE_BUILD
List.iter export_idents ~f:(fun id ->
List.iter export_idents ~f:(fun (field : Runtime_fields.t) ->
Log.warn
~loc:(Loc.of_pos __POS__)
(Pp.textf "export idents: %s/%d" (Ident.name id) (Ident.stamp id)));
(Pp.textf "export idents: %s/%d"
(Ident.name field.id) (Ident.stamp field.id)));
#endif
Lam_compile_env.reset ();
in
Expand Down Expand Up @@ -244,7 +247,7 @@ let compile ~package_info (output_prefix: string) (lam: Lambda.lambda) =
#endif
let js : J.program =
{ exports = meta.exports
; export_set = Ident.Set.of_list meta.exports
; export_set = Ident.Set.of_list (List.map meta.exports ~f:Runtime_fields.id)
; block = body
}
in
Expand Down
9 changes: 6 additions & 3 deletions jscomp/core/lam_stats.ml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@ open Import

type t = {
export_idents : Ident.Set.t;
exports : Ident.t list; (* It is kept since order matters? *)
exports : Runtime_fields.t list;
(** The compilation unit's runtime fields, in the order of the module
block. Each carries the namespace it comes from, which decides the
name it is exported under (see {!Runtime_fields}). *)
ident_tbl : Lam_id_kind.t Ident.Hashtbl.t;
(** we don't need count arities for all identifiers, for identifiers
for sure it's not a function, there is no need to count them *)
Expand Down Expand Up @@ -79,8 +82,8 @@ let print (v : t) =
Pp.box
(Pp.concat ~sep:(Pp.text "; ")
(List.map
~f:(fun export ->
Pp.text (Format.asprintf "%a" Ident.print export))
~f:(fun (export : Runtime_fields.t) ->
Pp.text (Format.asprintf "%a" Ident.print export.id))
v.exports));
]);
]
Expand Down
5 changes: 3 additions & 2 deletions jscomp/core/lam_stats.mli
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,13 @@ open Import

type t = {
export_idents : Ident.Set.t;
exports : Ident.t list;
exports : Runtime_fields.t list;
ident_tbl : Lam_id_kind.t Ident.Hashtbl.t;
(** we don't need count arities for all identifiers, for identifiers
for sure it's not a function, there is no need to count them
*)
}

val print : t -> _ Pp.t
val make : export_idents:Ident.t list -> export_ident_sets:Ident.Set.t -> t
val make :
export_idents:Runtime_fields.t list -> export_ident_sets:Ident.Set.t -> t
8 changes: 6 additions & 2 deletions jscomp/core/lam_stats_export.ml
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,9 @@ let values_of_export =
->
Ident.Hashtbl.clear arity_cache;
Ident.Hashtbl.clear nested_call_summary_cache;
List.fold_left meta.exports ~init:String.Map.empty ~f:(fun acc x ->
List.fold_left meta.exports ~init:String.Map.empty
~f:(fun acc (field : Runtime_fields.t) ->
let x = field.id in
let arity =
memoize arity_cache x (fun () ->
match Ident.Hashtbl.find meta.ident_tbl x with
Expand Down Expand Up @@ -252,7 +254,9 @@ let values_of_export =
when Lam_call_summary.is_unknown summary ->
acc
| _, _, _ ->
String.Map.add acc ~key:(Ident.name x)
(* Keyed by the name the field is given at runtime: that is what
other units resolve their accesses against. *)
String.Map.add acc ~key:(Runtime_fields.name field)
~data:
{
Js_cmj_format.arity;
Expand Down
4 changes: 2 additions & 2 deletions jscomp/test/dist/app_root_finder.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions jscomp/test/dist/arith_parser.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 6 additions & 6 deletions jscomp/test/dist/arity_infer.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading