From 68c516ba9db9abf212b42c4b6da5089789db8347 Mon Sep 17 00:00:00 2001 From: mununki Date: Mon, 27 Apr 2026 17:06:35 +0900 Subject: [PATCH 01/13] introduce linked external source map --- compiler/bsc/rescript_compiler_main.ml | 23 ++ compiler/common/js_config.ml | 4 + compiler/common/js_config.mli | 7 + compiler/core/dune | 2 +- compiler/core/js_dump.ml | 4 +- compiler/core/js_source_map.ml | 275 +++++++++++++++++++++ compiler/core/js_source_map.mli | 14 ++ compiler/core/lam.ml | 10 +- compiler/core/lam.mli | 2 + compiler/core/lam_bounded_vars.ml | 4 +- compiler/core/lam_compile.ml | 143 +++++++++-- compiler/core/lam_compile_main.ml | 33 ++- compiler/core/lam_convert.ml | 34 ++- compiler/core/lam_eta_conversion.ml | 28 +-- compiler/core/lam_pass_alpha_conversion.ml | 8 +- compiler/core/lam_pass_deep_flatten.ml | 4 +- compiler/core/lam_pass_exits.ml | 4 +- compiler/core/lam_pass_lets_dce.ml | 4 +- compiler/core/lam_pass_remove_alias.ml | 4 +- compiler/core/lam_subst.ml | 4 +- compiler/ext/ext_pp.ml | 62 ++++- compiler/ext/ext_pp.mli | 2 + rescript.opam | 2 +- rescript.opam.template | 2 +- rewatch/src/build/clean.rs | 9 +- rewatch/src/build/compile.rs | 14 ++ rewatch/src/config.rs | 78 ++++++ tests/build_tests/source_map/input.js | 31 +++ tests/build_tests/source_map/rescript.json | 6 + tests/build_tests/source_map/src/Demo.res | 5 + 30 files changed, 734 insertions(+), 88 deletions(-) create mode 100644 compiler/core/js_source_map.ml create mode 100644 compiler/core/js_source_map.mli create mode 100644 tests/build_tests/source_map/input.js create mode 100644 tests/build_tests/source_map/rescript.json create mode 100644 tests/build_tests/source_map/src/Demo.res diff --git a/compiler/bsc/rescript_compiler_main.ml b/compiler/bsc/rescript_compiler_main.ml index 67fbc0043de..300cc391abc 100644 --- a/compiler/bsc/rescript_compiler_main.ml +++ b/compiler/bsc/rescript_compiler_main.ml @@ -209,6 +209,20 @@ let[@inline] string_optional_set s : Bsc_args.spec = let[@inline] unit_call s : Bsc_args.spec = Unit (Unit_call s) let[@inline] string_list_add s : Bsc_args.spec = String (String_list_add s) +let parse_source_map value = + Js_config.source_map := + match String.lowercase_ascii value with + | "true" | "linked" -> Linked + | "false" | "none" -> No_source_map + | value -> Bsc_args.bad_arg ("Unsupported sourceMap value: " ^ value) + +let parse_bool_ref target value = + target := + match String.lowercase_ascii value with + | "true" -> true + | "false" -> false + | value -> Bsc_args.bad_arg ("Expected true or false, got: " ^ value) + (* mostly common used to list in the beginning to make search fast *) let command_line_flags : (string * Bsc_args.spec * string) array = @@ -259,6 +273,15 @@ let command_line_flags : (string * Bsc_args.spec * string) array = string_call ignore, "*internal* Set jsx mode, this is no longer used and is a no-op." ); ("-bs-jsx-preserve", set Js_config.jsx_preserve, "*internal* Preserve jsx"); + ( "-bs-source-map", + string_call parse_source_map, + "*internal* Configure source map output" ); + ( "-bs-source-map-sources-content", + string_call (parse_bool_ref Js_config.source_map_sources_content), + "*internal* Include original source text in source maps" ); + ( "-bs-source-map-root", + string_call (fun value -> Js_config.source_map_root := value), + "*internal* Set sourceRoot in source maps" ); ( "-bs-package-output", string_call Js_packages_state.update_npm_package_path, "*internal* Set npm-output-path: [opt_module]:path, for example: \ diff --git a/compiler/common/js_config.ml b/compiler/common/js_config.ml index 9e2b6f598ca..ffbc36dc717 100644 --- a/compiler/common/js_config.ml +++ b/compiler/common/js_config.ml @@ -26,6 +26,7 @@ type jsx_version = Jsx_v4 type jsx_module = React | Generic of {module_name: string} +type source_map = No_source_map | Linked let no_version_header = ref false @@ -53,6 +54,9 @@ let jsx_version = ref None let jsx_module = ref React let jsx_preserve = ref false let js_stdout = ref true +let source_map = ref No_source_map +let source_map_sources_content = ref false +let source_map_root = ref "" let all_module_aliases = ref false let no_stdlib = ref false let no_export = ref false diff --git a/compiler/common/js_config.mli b/compiler/common/js_config.mli index f19b3c6126c..fa48d9cc4dc 100644 --- a/compiler/common/js_config.mli +++ b/compiler/common/js_config.mli @@ -24,6 +24,7 @@ type jsx_version = Jsx_v4 type jsx_module = React | Generic of {module_name: string} +type source_map = No_source_map | Linked (* val get_packages_info : unit -> Js_packages_info.t *) @@ -86,6 +87,12 @@ val jsx_preserve : bool ref val js_stdout : bool ref +val source_map : source_map ref + +val source_map_sources_content : bool ref + +val source_map_root : string ref + val all_module_aliases : bool ref val no_stdlib : bool ref diff --git a/compiler/core/dune b/compiler/core/dune index d7d75f34437..6f1d77a652d 100644 --- a/compiler/core/dune +++ b/compiler/core/dune @@ -6,4 +6,4 @@ (run %{bin:cppo} %{env:CPPO_FLAGS=} %{input-file}))) (flags (:standard -w +a-4-9-27-30-40-41-42-48-70)) - (libraries depends ext flow_parser frontend gentype)) + (libraries depends ext flow_parser frontend gentype yojson)) diff --git a/compiler/core/js_dump.ml b/compiler/core/js_dump.ml index 4310b1f80d1..324ef4e5421 100644 --- a/compiler/core/js_dump.ml +++ b/compiler/core/js_dump.ml @@ -1291,6 +1291,7 @@ and variable_declaration top cxt f (variable : J.variable_declaration) : cxt = | _ -> ( match e.expression_desc with | Fun {is_method; params; body; env; return_unit; async; directive} -> + pp_comment_option f e.comment; pp_function ?directive ~is_method ~return_unit ~async ~fn_state:(if top then Name_top name else Name_non_top name) cxt f params body env @@ -1311,7 +1312,8 @@ and ipp_comment : 'a. P.t -> 'a -> unit = fun _f _comment -> () *) and pp_comment f comment = - if String.length comment > 0 then ( + if Js_source_map.mark_comment f comment then () + else if String.length comment > 0 then ( P.string f "/* "; P.string f comment; P.string f " */") diff --git a/compiler/core/js_source_map.ml b/compiler/core/js_source_map.ml new file mode 100644 index 00000000000..f473d862288 --- /dev/null +++ b/compiler/core/js_source_map.ml @@ -0,0 +1,275 @@ +type source = {relative_path: string; content: string option} + +type mapping = { + generated_line: int; + generated_column: int; + source_index: int; + original_line: int; + original_column: int; +} + +type t = { + generated_file: string; + generated_dir: string; + source_root: string; + sources_content: bool; + sources: (string, int) Hashtbl.t; + mutable source_list: source list; + mutable mappings: mapping list; + mutable last_generated: (int * int) option; +} + +let current : t option ref = ref None + +let marker_prefix = "\000RESCRIPT_SOURCE_MAP:" +let next_marker = ref 0 +let marker_locs : (int, Location.t) Hashtbl.t = Hashtbl.create 128 + +let is_prefix ~prefix s = + let prefix_len = String.length prefix in + String.length s >= prefix_len + && + let rec loop i = + i = prefix_len + || (String.unsafe_get s i = String.unsafe_get prefix i && loop (i + 1)) + in + loop 0 + +let comment_of_loc (loc : Location.t) = + match !Js_config.source_map with + | No_source_map -> None + | Linked -> + if loc.loc_ghost || loc.loc_start.pos_cnum < 0 then None + else + let id = !next_marker in + incr next_marker; + Hashtbl.replace marker_locs id loc; + Some (marker_prefix ^ string_of_int id) + +let with_builder builder f = + let old = !current in + current := builder; + Ext_pervasives.finally () ~clean:(fun () -> current := old) f + +let normalize_slashes s = + String.map + (function + | '\\' -> '/' + | c -> c) + s + +let absolute_path path = + if path = "" then path + else if Filename.is_relative path then Filename.concat (Sys.getcwd ()) path + else path + +let split_path path = + path |> normalize_slashes |> String.split_on_char '/' + |> List.filter (fun part -> part <> "") + +let rec drop_common xs ys = + match (xs, ys) with + | x :: xs, y :: ys when x = y -> drop_common xs ys + | _ -> (xs, ys) + +let repeat x n = + let rec loop acc n = if n <= 0 then acc else loop (x :: acc) (n - 1) in + loop [] n + +let relative_path ~from_dir ~to_file = + let from_dir = absolute_path from_dir in + let to_file = absolute_path to_file in + let from_parts = split_path from_dir in + let to_parts = split_path to_file in + match (from_parts, to_parts) with + | from_root :: _, to_root :: _ when from_root = to_root -> + let from_rest, to_rest = drop_common from_parts to_parts in + let parts = repeat ".." (List.length from_rest) @ to_rest in + if parts = [] then Filename.basename to_file else String.concat "/" parts + | _ -> Filename.basename to_file + +let make ~generated_file ~source_root ~sources_content = + { + generated_file = Filename.basename generated_file; + generated_dir = Filename.dirname generated_file; + source_root; + sources_content; + sources = Hashtbl.create 4; + source_list = []; + mappings = []; + last_generated = None; + } + +let load_content filename = + try Some (Ext_io.load_file filename) with _ -> None + +let add_source builder filename = + let filename = + match filename with + | "" | "_none_" -> !Location.input_name + | filename -> filename + in + let filename = absolute_path filename in + match Hashtbl.find_opt builder.sources filename with + | Some index -> (index, List.nth builder.source_list index) + | None -> + let source = + { + relative_path = + relative_path ~from_dir:builder.generated_dir ~to_file:filename; + content = load_content filename; + } + in + let index = List.length builder.source_list in + builder.source_list <- builder.source_list @ [source]; + Hashtbl.add builder.sources filename index; + (index, source) + +let utf16_units_in_utf8_slice s start stop = + let len = String.length s in + let stop = min stop len in + let rec loop i count = + if i >= stop then count + else + match String.unsafe_get s i with + | '\n' -> loop (i + 1) 0 + | c -> + let byte = Char.code c in + if byte < 0x80 then loop (i + 1) (count + 1) + else if byte land 0xE0 = 0xC0 && i + 1 < stop then + loop (i + 2) (count + 1) + else if byte land 0xF0 = 0xE0 && i + 2 < stop then + loop (i + 3) (count + 1) + else if byte land 0xF8 = 0xF0 && i + 3 < stop then + loop (i + 4) (count + 2) + else loop (i + 1) (count + 1) + in + loop (max 0 start) 0 + +let original_column source (pos : Lexing.position) = + match source.content with + | None -> max 0 (pos.pos_cnum - pos.pos_bol) + | Some content -> utf16_units_in_utf8_slice content pos.pos_bol pos.pos_cnum + +let add_mapping builder ~generated_line ~generated_column (loc : Location.t) = + if loc.loc_ghost || loc.loc_start.pos_cnum < 0 then () + else + match builder.last_generated with + | Some (line, column) + when line = generated_line && column = generated_column -> + () + | _ -> + let source_index, source = add_source builder loc.loc_start.pos_fname in + let original_line = max 0 (loc.loc_start.pos_lnum - 1) in + let original_column = original_column source loc.loc_start in + builder.mappings <- + { + generated_line; + generated_column; + source_index; + original_line; + original_column; + } + :: builder.mappings; + builder.last_generated <- Some (generated_line, generated_column) + +let mark_comment fmt comment = + if is_prefix ~prefix:marker_prefix comment then ( + let prefix_len = String.length marker_prefix in + let id = + int_of_string + (String.sub comment prefix_len (String.length comment - prefix_len)) + in + (match (!current, Hashtbl.find_opt marker_locs id) with + | Some builder, Some loc -> + let generated_line, generated_column = Ext_pp.position fmt in + add_mapping builder ~generated_line ~generated_column loc + | _ -> ()); + true) + else false + +let base64_vlq_chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" + +let add_vlq buf value = + let value = if value < 0 then (-value lsl 1) + 1 else value lsl 1 in + let rec loop value = + let digit = value land 31 in + let value = value lsr 5 in + let digit = if value > 0 then digit lor 32 else digit in + Buffer.add_char buf base64_vlq_chars.[digit]; + if value > 0 then loop value + in + loop value + +let compare_mapping a b = + match compare a.generated_line b.generated_line with + | 0 -> compare a.generated_column b.generated_column + | n -> n + +let encode_mappings mappings = + let buf = Buffer.create 256 in + let current_line = ref 0 in + let previous_generated_column = ref 0 in + let previous_source = ref 0 in + let previous_original_line = ref 0 in + let previous_original_column = ref 0 in + let first_segment = ref true in + mappings |> List.sort compare_mapping + |> List.iter (fun mapping -> + while !current_line < mapping.generated_line do + Buffer.add_char buf ';'; + incr current_line; + previous_generated_column := 0; + first_segment := true + done; + if not !first_segment then Buffer.add_char buf ','; + first_segment := false; + add_vlq buf (mapping.generated_column - !previous_generated_column); + add_vlq buf (mapping.source_index - !previous_source); + add_vlq buf (mapping.original_line - !previous_original_line); + add_vlq buf (mapping.original_column - !previous_original_column); + previous_generated_column := mapping.generated_column; + previous_source := mapping.source_index; + previous_original_line := mapping.original_line; + previous_original_column := mapping.original_column); + Buffer.contents buf + +let json builder = + let mappings = encode_mappings builder.mappings in + let fields = + [ + ("version", `Int 3); + ("file", `String builder.generated_file); + ( "sources", + `List + (List.map + (fun source -> `String source.relative_path) + builder.source_list) ); + ("names", `List []); + ("mappings", `String mappings); + ] + in + let fields = + if builder.source_root = "" then fields + else fields @ [("sourceRoot", `String builder.source_root)] + in + let fields = + if builder.sources_content then + fields + @ [ + ( "sourcesContent", + `List + (List.map + (fun source -> + match source.content with + | None -> `Null + | Some content -> `String content) + builder.source_list) ); + ] + else fields + in + Yojson.Safe.to_string (`Assoc fields) + +let linked_comment ~map_file = + "//# sourceMappingURL=" ^ Filename.basename map_file ^ "\n" diff --git a/compiler/core/js_source_map.mli b/compiler/core/js_source_map.mli new file mode 100644 index 00000000000..adad8e3303a --- /dev/null +++ b/compiler/core/js_source_map.mli @@ -0,0 +1,14 @@ +type t + +val make : + generated_file:string -> source_root:string -> sources_content:bool -> t + +val with_builder : t option -> (unit -> 'a) -> 'a + +val comment_of_loc : Location.t -> string option + +val mark_comment : Ext_pp.t -> string -> bool + +val json : t -> string + +val linked_comment : map_file:string -> string diff --git a/compiler/core/lam.ml b/compiler/core/lam.ml index 15875608b97..d987065b664 100644 --- a/compiler/core/lam.ml +++ b/compiler/core/lam.ml @@ -47,6 +47,7 @@ module Types = struct params: ident list; body: t; attr: Lambda.function_attribute; + loc: Location.t; } (* @@ -142,6 +143,7 @@ module X = struct params: ident list; body: t; attr: Lambda.function_attribute; + loc: Location.t; } and t = Types.t = @@ -181,9 +183,9 @@ let inner_map (l : t) (f : t -> X.t) : X.t = let ap_func = f ap_func in let ap_args = Ext_list.map ap_args f in Lapply {ap_func; ap_args; ap_info; ap_transformed_jsx} - | Lfunction {body; arity; params; attr} -> + | Lfunction {body; arity; params; attr; loc} -> let body = f body in - Lfunction {body; arity; params; attr} + Lfunction {body; arity; params; attr; loc} | Llet (str, id, arg, body) -> let arg = f arg in let body = f body in @@ -475,8 +477,8 @@ let global_module ?(dynamic_import = false) id = Lglobal_module (id, dynamic_import) let const ct : t = Lconst ct -let function_ ~attr ~arity ~params ~body : t = - Lfunction {arity; params; body; attr} +let function_ ~loc ~attr ~arity ~params ~body : t = + Lfunction {arity; params; body; attr; loc} let let_ kind id e body : t = Llet (kind, id, e, body) let letrec bindings body : t = Lletrec (bindings, body) diff --git a/compiler/core/lam.mli b/compiler/core/lam.mli index f6a398d677b..0bd2897fec1 100644 --- a/compiler/core/lam.mli +++ b/compiler/core/lam.mli @@ -53,6 +53,7 @@ and lfunction = { params: ident list; body: t; attr: Lambda.function_attribute; + loc: Location.t; } and prim_info = private { @@ -116,6 +117,7 @@ val const : Lam_constant.t -> t val apply : ?ap_transformed_jsx:bool -> t -> t list -> ap_info -> t val function_ : + loc:Location.t -> attr:Lambda.function_attribute -> arity:int -> params:ident list -> diff --git a/compiler/core/lam_bounded_vars.ml b/compiler/core/lam_bounded_vars.ml index 5499bb77ab4..597a638b770 100644 --- a/compiler/core/lam_bounded_vars.ml +++ b/compiler/core/lam_bounded_vars.ml @@ -88,10 +88,10 @@ let rewrite (map : _ Hash_ident.t) (lam : Lam.t) : Lam.t = in let body = aux body in Lam.letrec bindings body - | Lfunction {arity; params; body; attr} -> + | Lfunction {arity; params; body; attr; loc} -> let params = Ext_list.map params rebind in let body = aux body in - Lam.function_ ~arity ~params ~body ~attr + Lam.function_ ~loc ~arity ~params ~body ~attr | Lstaticcatch (l1, (i, xs), l2) -> let l1 = aux l1 in let xs = Ext_list.map xs rebind in diff --git a/compiler/core/lam_compile.ml b/compiler/core/lam_compile.ml index 9897f0c01e4..0f251a20116 100644 --- a/compiler/core/lam_compile.ml +++ b/compiler/core/lam_compile.ml @@ -25,6 +25,64 @@ module E = Js_exp_make module S = Js_stmt_make +let source_map_comment = Js_source_map.comment_of_loc + +let with_source_loc loc (exp : J.expression) = + match (source_map_comment loc, exp.comment) with + | Some comment, None -> {exp with comment = Some comment} + | _ -> exp + +let rec source_loc_of_lam (lam : Lam.t) = + match lam with + | Lapply {ap_info = {ap_loc}} -> Some ap_loc + | Lprim {loc} | Lfunction {loc} -> Some loc + | Llet (_, _, arg, body) -> ( + match source_loc_of_lam arg with + | Some _ as loc -> loc + | None -> source_loc_of_lam body) + | Lletrec (_, body) | Lsequence (_, body) -> source_loc_of_lam body + | Lifthenelse (_, then_, _) -> source_loc_of_lam then_ + | Lstaticcatch (body, _, _) | Ltrywith (body, _, _) -> source_loc_of_lam body + | Lstringswitch (_, cases, default) -> ( + match cases with + | (_, body) :: _ -> source_loc_of_lam body + | [] -> ( + match default with + | Some body -> source_loc_of_lam body + | None -> None)) + | Lswitch (_, sw) -> ( + match (sw.sw_consts, sw.sw_blocks, sw.sw_failaction) with + | (_, body) :: _, _, _ | _, (_, body) :: _, _ -> source_loc_of_lam body + | [], [], Some body -> source_loc_of_lam body + | [], [], None -> None) + | Lstaticraise (_, args) -> ( + match args with + | arg :: _ -> source_loc_of_lam arg + | [] -> None) + | Lwhile (_, body) + | Lfor (_, _, _, _, body) + | Lfor_of (_, _, body) + | Lfor_await_of (_, _, body) -> + source_loc_of_lam body + | Lassign (_, body) -> source_loc_of_lam body + | Lvar _ | Lglobal_module _ | Lconst _ | Lbreak | Lcontinue -> None + +let source_map_comment_of_lam lam = + match source_loc_of_lam lam with + | Some loc -> source_map_comment loc + | None -> None + +let with_statement_comment comment (stmt : J.statement) = + match (comment, stmt.comment) with + | Some comment, None -> {stmt with comment = Some comment} + | _ -> stmt + +let with_block_source_loc lam block = + match block with + | [] -> [] + | stmt :: rest -> + with_statement_comment (source_map_comment_of_lam lam) stmt :: rest + let args_either_function_or_const (args : Lam.t list) = Ext_list.for_all args (fun x -> match x with @@ -315,6 +373,7 @@ let compile output_prefix = | Single x -> apply_with_arity fn ~arity:(Lam_arity.extract_arity x) args) in + let expression = with_source_loc appinfo.ap_info.ap_loc expression in Js_output.output_of_block_and_expression lambda_cxt.continuation args_code expression (* @@ -329,7 +388,12 @@ let compile output_prefix = (id : Ident.t) (arg : Lam.t) : Js_output.t * initialization = match arg with | Lfunction - {params; body; attr = {return_unit; async; one_unit_arg; directive}} -> + { + params; + body; + attr = {return_unit; async; one_unit_arg; directive}; + loc; + } -> (* TODO: Think about recursive value {[ let rec v = ref (fun _ ... @@ -361,11 +425,13 @@ let compile output_prefix = } body in + let comment = source_map_comment loc in let result = if ret.triggered then let body_block = Js_output.output_as_block output in E.ocaml_fun - (* TODO: save computation of length several times + ?comment + (* TODO: save computation of length several times Here we always create [ocaml_fun], it will be renamed into [method] when it is detected by a primitive @@ -382,7 +448,7 @@ let compile output_prefix = ] else (* TODO: save computation of length several times *) - E.ocaml_fun params + E.ocaml_fun ?comment params (Js_output.output_as_block output) ~return_unit ~async ~one_unit_arg ?directive in @@ -539,8 +605,12 @@ let compile output_prefix = (a * J.case_clause) list -> J.statement) ~(switch_exp : J.expression) ~(default : default_case) ?(merge_cases = fun _ _ -> true) (cases : (a * Lam.t) list) -> + let output_block_with_source_loc cxt lam = + compile_lambda cxt lam |> Js_output.output_as_block + |> with_block_source_loc lam + in match (cases, default) with - | [], Default lam -> Js_output.output_as_block (compile_lambda cxt lam) + | [], Default lam -> output_block_with_source_loc cxt lam | [], (Complete | NonComplete) -> [] | [(_, lam)], Complete -> (* To take advantage of such optimizations, @@ -549,18 +619,18 @@ let compile output_prefix = otherwise the compiler engine would think that it's also complete *) - Js_output.output_as_block (compile_lambda cxt lam) + output_block_with_source_loc cxt lam | [(id, lam)], NonComplete -> morph_declare_to_assign cxt (fun cxt define -> [ S.if_ ?declaration:define (eq_exp None switch_exp (Some id) (make_exp id)) - (Js_output.output_as_block (compile_lambda cxt lam)); + (output_block_with_source_loc cxt lam); ]) | [(id, lam)], Default x | [(id, lam); (_, x)], Complete -> morph_declare_to_assign cxt (fun cxt define -> - let else_block = Js_output.output_as_block (compile_lambda cxt x) in - let then_block = Js_output.output_as_block (compile_lambda cxt lam) in + let else_block = output_block_with_source_loc cxt x in + let then_block = output_block_with_source_loc cxt lam in [ S.if_ ?declaration:define (eq_exp None switch_exp (Some id) (make_exp id)) @@ -599,9 +669,7 @@ let compile output_prefix = | Complete -> None | NonComplete -> None | Default lam -> ( - let statements = - Js_output.output_as_block (compile_lambda switch_cxt lam) - in + let statements = output_block_with_source_loc switch_cxt lam in match statements with | [] -> None | _ -> Some statements) @@ -613,6 +681,7 @@ let compile output_prefix = let switch_body, should_break = Js_output.to_break_block (compile_lambda switch_cxt lam) in + let switch_body = with_block_source_loc lam switch_body in let should_break = if not @@ -621,10 +690,20 @@ let compile output_prefix = then should_break else should_break && Lam_exit_code.has_exit lam in - (switch_case, J.{switch_body; should_break; comment = None}) + ( switch_case, + J. + { + switch_body; + should_break; + comment = source_map_comment_of_lam lam; + } ) else ( switch_case, - {switch_body = []; should_break = false; comment = None} )) + { + switch_body = []; + should_break = false; + comment = source_map_comment_of_lam lam; + } )) (* TODO: we should also group default *) (* The last clause does not need [break] common break through, *) @@ -1630,11 +1709,12 @@ let compile output_prefix = | _ -> Js_output.output_of_block_and_expression lambda_cxt.continuation args_code - (E.call - ~info: - (call_info_of_ap_status appinfo.ap_transformed_jsx - appinfo.ap_info.ap_status) - fn_code args)) + (with_source_loc appinfo.ap_info.ap_loc + (E.call + ~info: + (call_info_of_ap_status appinfo.ap_transformed_jsx + appinfo.ap_info.ap_status) + fn_code args))) and compile_prim (prim_info : Lam.prim_info) (lambda_cxt : Lam_compile_context.t) = match prim_info with @@ -1648,13 +1728,14 @@ let compile output_prefix = | Fld_module {name = field} -> compile_external_field ~dynamic_import lambda_cxt id field | _ -> assert false) - | {primitive = Praise; args = [e]; _} -> ( + | {primitive = Praise; args = [e]; loc} -> ( match compile_lambda {lambda_cxt with continuation = NeedValue Not_tail} e with | {block; value = Some v} -> + let comment = source_map_comment loc in Js_output.make - (Ext_list.append_one block (S.throw_stmt v)) + (Ext_list.append_one block (S.throw_stmt ?comment v)) ~value:E.undefined ~output_finished:True (* FIXME -- breaks invariant when NeedValue, reason is that js [throw] is statement while ocaml it's an expression, we should remove such things in lambda optimizations @@ -1721,9 +1802,10 @@ let compile output_prefix = | {primitive = Pjs_unsafe_downgrade _; args} -> assert false | {primitive = Pjs_fn_method; args = args_lambda} -> ( match args_lambda with - | [Lfunction {params; body; attr = {return_unit; async}}] -> + | [Lfunction {params; body; attr = {return_unit; async}; loc}] -> + let comment = source_map_comment loc in Js_output.output_of_block_and_expression lambda_cxt.continuation [] - (E.method_ ~async ~return_unit params + (E.method_ ?comment ~async ~return_unit params (* Invariant: jmp_table can not across function boundary, here we share env *) @@ -1782,7 +1864,7 @@ let compile output_prefix = [args_expr] in Js_output.output_of_block_and_expression lambda_cxt.continuation - args_code exp + args_code (with_source_loc loc exp) | Lfunction { body = @@ -1811,7 +1893,7 @@ let compile output_prefix = [args_expr] in Js_output.output_of_block_and_expression lambda_cxt.continuation - args_code exp + args_code (with_source_loc loc exp) | _ -> Location.raise_errorf ~loc "Invalid argument: unsupported argument to dynamic import. If you \ @@ -1833,15 +1915,22 @@ let compile output_prefix = args_expr in Js_output.output_of_block_and_expression lambda_cxt.continuation args_code - exp + (with_source_loc loc exp) and compile_lambda (lambda_cxt : Lam_compile_context.t) (cur_lam : Lam.t) : Js_output.t = match cur_lam with | Lfunction - {params; body; attr = {return_unit; async; one_unit_arg; directive}} -> + { + params; + body; + attr = {return_unit; async; one_unit_arg; directive}; + loc; + } -> + let comment = source_map_comment loc in Js_output.output_of_expression lambda_cxt.continuation ~no_effects:no_effects_const - (E.ocaml_fun params ~return_unit ~async ~one_unit_arg ?directive + (E.ocaml_fun ?comment params ~return_unit ~async ~one_unit_arg + ?directive (* Invariant: jmp_table can not across function boundary, here we share env *) diff --git a/compiler/core/lam_compile_main.ml b/compiler/core/lam_compile_main.ml index cdecf32ef8e..54e8a119825 100644 --- a/compiler/core/lam_compile_main.ml +++ b/compiler/core/lam_compile_main.ml @@ -297,6 +297,31 @@ js let (//) = Filename.concat +let source_map_enabled () = + match !Js_config.source_map with + | No_source_map -> false + | Linked -> true + +let dump_deps_program_with_source_map ~target_file ~output_prefix module_system + lambda_output chan = + let builder = + if source_map_enabled () then + Some + (Js_source_map.make ~generated_file:target_file + ~source_root:!Js_config.source_map_root + ~sources_content:!Js_config.source_map_sources_content) + else None + in + Js_source_map.with_builder builder (fun () -> + Js_dump_program.pp_deps_program ~output_prefix module_system lambda_output + (Ext_pp.from_channel chan)); + match (builder, !Js_config.source_map) with + | Some builder, Linked -> + let map_file = target_file ^ ".map" in + output_string chan (Js_source_map.linked_comment ~map_file); + Ext_io.write_file map_file (Js_source_map.json builder) + | _ -> () + let lambda_as_module (lambda_output : J.deps_program) (output_prefix : string) @@ -306,11 +331,6 @@ let lambda_as_module Js_dump_program.dump_deps_program ~output_prefix Commonjs (lambda_output) stdout end else Js_packages_info.iter package_info (fun {module_system; path; suffix} -> - let output_chan chan = - Js_dump_program.dump_deps_program ~output_prefix - module_system - (lambda_output) - chan in let basename = Ext_namespace.change_ext_ns_suffix (Filename.basename output_prefix) suffix in @@ -320,6 +340,9 @@ let lambda_as_module basename (* #913 only generate little-case js file *) ) in + let output_chan chan = + dump_deps_program_with_source_map ~target_file ~output_prefix + module_system (lambda_output) chan in (if not !Clflags.dont_write_files then Ext_pervasives.with_file_as_chan target_file output_chan ); diff --git a/compiler/core/lam_convert.ml b/compiler/core/lam_convert.ml index 3c0c7c058d0..789770d45e5 100644 --- a/compiler/core/lam_convert.ml +++ b/compiler/core/lam_convert.ml @@ -407,18 +407,18 @@ let convert (exports : Set_ident.t) (lam : Lambda.lambda) : (Ext_list.map args convert_aux) {ap_loc = loc; ap_inlined; ap_status = App_uncurry} ~ap_transformed_jsx - | Lfunction {params; body; attr} -> + | Lfunction {params; body; attr; loc} -> let new_map, body = rename_optional_parameters Map_ident.empty params body in if Map_ident.is_empty new_map then - Lam.function_ ~attr ~arity:(List.length params) ~params + Lam.function_ ~loc ~attr ~arity:(List.length params) ~params ~body:(convert_aux body) else let params = Ext_list.map params (fun x -> Map_ident.find_default new_map x x) in - Lam.function_ ~attr ~arity:(List.length params) ~params + Lam.function_ ~loc ~attr ~arity:(List.length params) ~params ~body:(convert_aux body) | Llet (_, _, _, Lprim (Pgetglobal id, args, _), _body) when dynamic_import -> @@ -565,13 +565,29 @@ let convert (exports : Set_ident.t) (lam : Lambda.lambda) : } | _ -> Lam.let_ kind id new_e new_body) and convert_pipe (f : Lambda.lambda) (x : Lambda.lambda) outer_loc = + let pipe_loc = + let candidate = + match f with + | Lapply {ap_loc} -> Some ap_loc + | Lfunction {loc} -> Some loc + | Lprim (_, _, loc) + | Lswitch (_, _, loc) + | Lstringswitch (_, _, _, loc) + | Lsend (_, _, loc) -> + Some loc + | _ -> None + in + match candidate with + | Some loc when (not loc.loc_ghost) && loc.loc_start.pos_cnum >= 0 -> loc + | _ -> outer_loc + in let x = convert_aux x in let f = convert_aux f in match f with | Lfunction {params = [param]; body = Lprim {primitive; args = [Lvar inner_arg]}} when Ident.same param inner_arg -> - Lam.prim ~primitive ~args:[x] outer_loc + Lam.prim ~primitive ~args:[x] pipe_loc | Lapply { ap_func = @@ -580,18 +596,14 @@ let convert (exports : Set_ident.t) (lam : Lambda.lambda) : } when Ext_list.for_all2_no_exn inner_args params lam_is_var && Ext_list.length_larger_than_n inner_args args 1 -> - Lam.prim ~primitive ~args:(Ext_list.append_one args x) outer_loc + Lam.prim ~primitive ~args:(Ext_list.append_one args x) pipe_loc | Lapply {ap_func; ap_args; ap_info; ap_transformed_jsx} -> Lam.apply ~ap_transformed_jsx ap_func (Ext_list.append_one ap_args x) - { - ap_loc = outer_loc; - ap_inlined = ap_info.ap_inlined; - ap_status = App_na; - } + {ap_loc = pipe_loc; ap_inlined = ap_info.ap_inlined; ap_status = App_na} | _ -> Lam.apply f [x] - {ap_loc = outer_loc; ap_inlined = Default_inline; ap_status = App_na} + {ap_loc = pipe_loc; ap_inlined = Default_inline; ap_status = App_na} and convert_switch (e : Lambda.lambda) (s : Lambda.lambda_switch) = let e = convert_aux e in match s with diff --git a/compiler/core/lam_eta_conversion.ml b/compiler/core/lam_eta_conversion.ml index 220fa760221..320ecd41647 100644 --- a/compiler/core/lam_eta_conversion.ml +++ b/compiler/core/lam_eta_conversion.ml @@ -61,12 +61,12 @@ let transform_under_supply n ap_info fn args = But it is dangerous to change the arity of an existing function which may cause inconsistency *) - Lam.function_ ~arity:n ~params:extra_args + Lam.function_ ~loc:Location.none ~arity:n ~params:extra_args ~attr:Lambda.default_function_attribute ~body:(Lam.apply fn (Ext_list.append args extra_lambdas) ap_info) | fn :: args, bindings -> let rest : Lam.t = - Lam.function_ ~arity:n ~params:extra_args + Lam.function_ ~loc:Location.none ~arity:n ~params:extra_args ~attr:Lambda.default_function_attribute ~body:(Lam.apply fn (Ext_list.append args extra_lambdas) ap_info) in @@ -123,8 +123,8 @@ let unsafe_adjust_to_arity loc ~(to_ : int) ?(from : int option) (fn : Lam.t) : if from = to_ || is_async_fn then fn else if to_ = 0 then match fn with - | Lfunction {params = [param]; body} -> - Lam.function_ ~arity:0 ~attr:Lambda.default_function_attribute + | Lfunction {params = [param]; body; loc} -> + Lam.function_ ~loc ~arity:0 ~attr:Lambda.default_function_attribute ~params:[] ~body:(Lam.let_ Alias param Lam.unit body) (* could be only introduced by @@ -148,7 +148,7 @@ let unsafe_adjust_to_arity loc ~(to_ : int) ?(from : int option) (fn : Lam.t) : in let cont = - Lam.function_ ~attr:Lambda.default_function_attribute ~arity:0 + Lam.function_ ~loc ~attr:Lambda.default_function_attribute ~arity:0 ~params:[] ~body:(Lam.apply new_fn [Lam.unit] ap_info) in @@ -158,7 +158,7 @@ let unsafe_adjust_to_arity loc ~(to_ : int) ?(from : int option) (fn : Lam.t) : | Some partial_arg -> Lam.let_ Strict partial_arg fn cont) else if to_ > from then match fn with - | Lfunction {params; body} -> + | Lfunction {params; body; loc} -> (* {[fun x -> f]} -> {[ fun x y -> f y ]} *) @@ -170,7 +170,7 @@ let unsafe_adjust_to_arity loc ~(to_ : int) ?(from : int option) (fn : Lam.t) : | [] -> body | var :: vars -> mk_apply (Lam.apply body [var] ap_info) vars in - Lam.function_ ~attr:Lambda.default_function_attribute ~arity:to_ + Lam.function_ ~loc ~attr:Lambda.default_function_attribute ~arity:to_ ~params:(Ext_list.append params extra_args) ~body:(mk_apply body (Ext_list.map extra_args Lam.var)) | _ -> ( @@ -193,7 +193,7 @@ let unsafe_adjust_to_arity loc ~(to_ : int) ?(from : int option) (fn : Lam.t) : (Some partial_arg, Lam.var partial_arg) in let cont = - Lam.function_ ~arity ~attr:Lambda.default_function_attribute + Lam.function_ ~loc ~arity ~attr:Lambda.default_function_attribute ~params:extra_args ~body: (let first_args, rest_args = Ext_list.split_at extra_args from in @@ -216,16 +216,16 @@ let unsafe_adjust_to_arity loc ~(to_ : int) ?(from : int option) (fn : Lam.t) : This is okay if the function is not held by other.. *) match fn with - | Lfunction {params; body} + | Lfunction {params; body; loc} (* TODO check arity = List.length params in debug mode *) -> let arity = to_ in let extra_outer_args, extra_inner_args = Ext_list.split_at params arity in - Lam.function_ ~arity ~attr:Lambda.default_function_attribute + Lam.function_ ~loc ~arity ~attr:Lambda.default_function_attribute ~params:extra_outer_args ~body: - (Lam.function_ ~arity:(from - to_) + (Lam.function_ ~loc ~arity:(from - to_) ~attr:Lambda.default_function_attribute ~params:extra_inner_args ~body) | _ -> ( @@ -247,14 +247,14 @@ let unsafe_adjust_to_arity loc ~(to_ : int) ?(from : int option) (fn : Lam.t) : (Some partial_arg, Lam.var partial_arg) in let cont = - Lam.function_ ~arity:to_ ~params:extra_outer_args + Lam.function_ ~loc ~arity:to_ ~params:extra_outer_args ~attr:Lambda.default_function_attribute ~body: (let arity = from - to_ in let extra_inner_args = Ext_list.init arity (fun _ -> Ident.create Literals.param) in - Lam.function_ ~arity ~params:extra_inner_args + Lam.function_ ~loc ~arity ~params:extra_inner_args ~attr:Lambda.default_function_attribute ~body: (Lam.apply new_fn @@ -285,7 +285,7 @@ let unsafe_adjust_to_arity loc ~(to_ : int) ?(from : int option) (fn : Lam.t) : in let cont = - Lam.function_ ~attr:Lambda.default_function_attribute ~arity:0 + Lam.function_ ~loc ~attr:Lambda.default_function_attribute ~arity:0 ~params:[] ~body:(Lam.apply new_fn [Lam.unit] ap_info) in diff --git a/compiler/core/lam_pass_alpha_conversion.ml b/compiler/core/lam_pass_alpha_conversion.ml index 7965cfc6011..8ab29e4ca75 100644 --- a/compiler/core/lam_pass_alpha_conversion.ml +++ b/compiler/core/lam_pass_alpha_conversion.ml @@ -75,9 +75,9 @@ let alpha_conversion (meta : Lam_stats.t) (lam : Lam.t) : Lam.t = | Lprim {primitive = Pjs_fn_make_unit; args = [arg]; loc} -> let arg = match arg with - | Lfunction {arity = 1; params = [x]; attr; body} + | Lfunction {arity = 1; params = [x]; attr; body; loc} when Ident.name x = "param" (* "()" *) -> - Lam.function_ ~params:[x] + Lam.function_ ~loc ~params:[x] ~attr:{attr with one_unit_arg = true} ~body ~arity:1 | _ -> arg @@ -85,9 +85,9 @@ let alpha_conversion (meta : Lam_stats.t) (lam : Lam.t) : Lam.t = simpl arg | Lprim {primitive; args; loc} -> Lam.prim ~primitive ~args:(Ext_list.map args simpl) loc - | Lfunction {arity; params; body; attr} -> + | Lfunction {arity; params; body; attr; loc} -> (* Lam_mk.lfunction kind params (simpl l) *) - Lam.function_ ~arity ~params ~body:(simpl body) ~attr + Lam.function_ ~loc ~arity ~params ~body:(simpl body) ~attr | Lswitch ( l, { diff --git a/compiler/core/lam_pass_deep_flatten.ml b/compiler/core/lam_pass_deep_flatten.ml index 6e94a78a587..33ceff7cd07 100644 --- a/compiler/core/lam_pass_deep_flatten.ml +++ b/compiler/core/lam_pass_deep_flatten.ml @@ -229,8 +229,8 @@ let deep_flatten (lam : Lam.t) : Lam.t = | Lprim {primitive; args; loc} -> let args = Ext_list.map args aux in Lam.prim ~primitive ~args loc - | Lfunction {arity; params; body; attr} -> - Lam.function_ ~arity ~params ~body:(aux body) ~attr + | Lfunction {arity; params; body; attr; loc} -> + Lam.function_ ~loc ~arity ~params ~body:(aux body) ~attr | Lswitch ( l, { diff --git a/compiler/core/lam_pass_exits.ml b/compiler/core/lam_pass_exits.ml index e47be329551..355ac011500 100644 --- a/compiler/core/lam_pass_exits.ml +++ b/compiler/core/lam_pass_exits.ml @@ -205,8 +205,8 @@ let subst_helper (subst : subst_tbl) (query : int -> int) (lam : Lam.t) : Lam.t Lam.apply (simplif ap_func) (Ext_list.map ap_args simplif) ap_info ~ap_transformed_jsx - | Lfunction {arity; params; body; attr} -> - Lam.function_ ~arity ~params ~body:(simplif body) ~attr + | Lfunction {arity; params; body; attr; loc} -> + Lam.function_ ~loc ~arity ~params ~body:(simplif body) ~attr | Llet (kind, v, l1, l2) -> Lam.let_ kind v (simplif l1) (simplif l2) | Lletrec (bindings, body) -> Lam.letrec (Ext_list.map_snd bindings simplif) (simplif body) diff --git a/compiler/core/lam_pass_lets_dce.ml b/compiler/core/lam_pass_lets_dce.ml index 503e90c1f81..9f14f0bede3 100644 --- a/compiler/core/lam_pass_lets_dce.ml +++ b/compiler/core/lam_pass_lets_dce.ml @@ -147,8 +147,8 @@ let lets_helper (count_var : Ident.t -> Lam_pass_count.used_info) lam : Lam.t = | Lapply {ap_func = l1; ap_args = ll; ap_info; ap_transformed_jsx} -> Lam.apply (simplif l1) (Ext_list.map ll simplif) ap_info ~ap_transformed_jsx - | Lfunction {arity; params; body; attr} -> - Lam.function_ ~arity ~params ~body:(simplif body) ~attr + | Lfunction {arity; params; body; attr; loc} -> + Lam.function_ ~loc ~arity ~params ~body:(simplif body) ~attr | Lconst _ -> lam | Lletrec (bindings, body) -> Lam.letrec (Ext_list.map_snd bindings simplif) (simplif body) diff --git a/compiler/core/lam_pass_remove_alias.ml b/compiler/core/lam_pass_remove_alias.ml index 52a88ad02eb..c6b10824ad3 100644 --- a/compiler/core/lam_pass_remove_alias.ml +++ b/compiler/core/lam_pass_remove_alias.ml @@ -244,8 +244,8 @@ let simplify_alias (meta : Lam_stats.t) (lam : Lam.t) : Lam.t = (* simpl (Lam_beta_reduce.propogate_beta_reduce meta params body args) *) | Lapply {ap_func = l1; ap_args = ll; ap_info; ap_transformed_jsx} -> Lam.apply (simpl l1) (Ext_list.map ll simpl) ap_info ~ap_transformed_jsx - | Lfunction {arity; params; body; attr} -> - Lam.function_ ~arity ~params ~body:(simpl body) ~attr + | Lfunction {arity; params; body; attr; loc} -> + Lam.function_ ~loc ~arity ~params ~body:(simpl body) ~attr | Lswitch ( l, { diff --git a/compiler/core/lam_subst.ml b/compiler/core/lam_subst.ml index e449102dc5e..3be69db85fb 100644 --- a/compiler/core/lam_subst.ml +++ b/compiler/core/lam_subst.ml @@ -35,8 +35,8 @@ let subst (s : Lam.t Map_ident.t) lam = | Lconst _ -> x | Lapply {ap_func; ap_args; ap_info} -> Lam.apply (subst_aux ap_func) (Ext_list.map ap_args subst_aux) ap_info - | Lfunction {arity; params; body; attr} -> - Lam.function_ ~arity ~params ~body:(subst_aux body) ~attr + | Lfunction {arity; params; body; attr; loc} -> + Lam.function_ ~loc ~arity ~params ~body:(subst_aux body) ~attr | Llet (str, id, arg, body) -> Lam.let_ str id (subst_aux arg) (subst_aux body) | Lletrec (decl, body) -> diff --git a/compiler/ext/ext_pp.ml b/compiler/ext/ext_pp.ml index f9237c271a7..384782f526e 100644 --- a/compiler/ext/ext_pp.ml +++ b/compiler/ext/ext_pp.ml @@ -36,9 +36,46 @@ type t = { flush: unit -> unit; mutable indent_level: int; mutable last_new_line: bool; - (* only when we print newline, we print the indent *) + mutable line: int; + mutable column: int; (* only when we print newline, we print the indent *) } +let update_position t s = + let len = String.length s in + let rec loop i = + if i < len then + match String.unsafe_get s i with + | '\n' -> + t.line <- t.line + 1; + t.column <- 0; + loop (i + 1) + | c -> + let byte = Char.code c in + if byte < 0x80 then ( + t.column <- t.column + 1; + loop (i + 1)) + else if byte land 0xE0 = 0xC0 && i + 1 < len then ( + t.column <- t.column + 1; + loop (i + 2)) + else if byte land 0xF0 = 0xE0 && i + 2 < len then ( + t.column <- t.column + 1; + loop (i + 3)) + else if byte land 0xF8 = 0xF0 && i + 3 < len then ( + t.column <- t.column + 2; + loop (i + 4)) + else ( + t.column <- t.column + 1; + loop (i + 1)) + in + loop 0 + +let update_position_char t c = + match c with + | '\n' -> + t.line <- t.line + 1; + t.column <- 0 + | _ -> t.column <- t.column + 1 + let from_channel chan = { output_string = (fun s -> output_string chan s); @@ -46,6 +83,8 @@ let from_channel chan = flush = (fun _ -> flush chan); indent_level = 0; last_new_line = false; + line = 0; + column = 0; } let from_buffer buf = @@ -55,6 +94,8 @@ let from_buffer buf = flush = (fun _ -> ()); indent_level = 0; last_new_line = false; + line = 0; + column = 0; } (* If we have [newline] in [s], @@ -63,28 +104,37 @@ let from_buffer buf = *) let string t s = t.output_string s; + update_position t s; t.last_new_line <- false let newline t = if not t.last_new_line then ( t.output_char '\n'; + update_position_char t '\n'; for _ = 0 to t.indent_level - 1 do - t.output_string L.indent_str + t.output_string L.indent_str; + update_position t L.indent_str done; t.last_new_line <- true) let at_least_two_lines t = - if not t.last_new_line then t.output_char '\n'; + if not t.last_new_line then ( + t.output_char '\n'; + update_position_char t '\n'); t.output_char '\n'; + update_position_char t '\n'; for _ = 0 to t.indent_level - 1 do - t.output_string L.indent_str + t.output_string L.indent_str; + update_position t L.indent_str done; t.last_new_line <- true let force_newline t = t.output_char '\n'; + update_position_char t '\n'; for _ = 0 to t.indent_level - 1 do - t.output_string L.indent_str + t.output_string L.indent_str; + update_position t L.indent_str done; t.last_new_line <- true @@ -169,3 +219,5 @@ let brace_group st n action = group st n (fun _ -> brace st action) t.indent_level <- t.indent_level + n *) let flush t () = t.flush () + +let position t = (t.line, t.column) diff --git a/compiler/ext/ext_pp.mli b/compiler/ext/ext_pp.mli index aaf2176214a..4990abe7515 100644 --- a/compiler/ext/ext_pp.mli +++ b/compiler/ext/ext_pp.mli @@ -77,3 +77,5 @@ val from_channel : out_channel -> t val from_buffer : Buffer.t -> t val flush : t -> unit -> unit + +val position : t -> int * int diff --git a/rescript.opam b/rescript.opam index 40a9251350f..c4b447cae17 100644 --- a/rescript.opam +++ b/rescript.opam @@ -26,7 +26,7 @@ depends: [ "dune" {>= "3.17"} "flow_parser" {= "0.267.0"} "ocamlformat" {with-test & = "0.27.0"} - "yojson" {with-test & = "2.2.2"} + "yojson" {= "2.2.2"} "ounit2" {with-test & = "2.2.7"} "odoc" {with-doc} "ocaml-lsp-server" {with-dev-setup & = "1.22.0"} diff --git a/rescript.opam.template b/rescript.opam.template index e5629e01d6a..71c93ea688d 100644 --- a/rescript.opam.template +++ b/rescript.opam.template @@ -4,7 +4,7 @@ depends: [ "dune" {>= "3.17"} "flow_parser" {= "0.267.0"} "ocamlformat" {with-test & = "0.27.0"} - "yojson" {with-test & = "2.2.2"} + "yojson" {= "2.2.2"} "ounit2" {with-test & = "2.2.7"} "odoc" {with-doc} "ocaml-lsp-server" {with-dev-setup & = "1.22.0"} diff --git a/rewatch/src/build/clean.rs b/rewatch/src/build/clean.rs index 2d5b3d1342f..8d88f1f4bc3 100644 --- a/rewatch/src/build/clean.rs +++ b/rewatch/src/build/clean.rs @@ -33,10 +33,15 @@ fn remove_iast(package: &packages::Package, source_file: &Path) { } fn remove_mjs_file(source_file: &Path, suffix: &str) { - let _ = std::fs::remove_file(source_file.with_extension( + let js_file = source_file.with_extension( // suffix.to_string includes the ., so we need to remove it &suffix[1..], - )); + ); + let _ = std::fs::remove_file(&js_file); + + let mut map_file = js_file.into_os_string(); + map_file.push(".map"); + let _ = std::fs::remove_file(PathBuf::from(map_file)); } fn remove_compile_asset(package: &packages::Package, source_file: &Path, extension: &str) { diff --git a/rewatch/src/build/compile.rs b/rewatch/src/build/compile.rs index 8042c8cde82..bce155ac4e6 100644 --- a/rewatch/src/build/compile.rs +++ b/rewatch/src/build/compile.rs @@ -686,6 +686,7 @@ pub fn compiler_args( let jsx_module_args = root_config.get_jsx_module_args(); let jsx_mode_args = root_config.get_jsx_mode_args(); let jsx_preserve_args = root_config.get_jsx_preserve_args(); + let source_map_args = root_config.get_source_map_args(); let bsb_project_root = project_context.get_root_path(); let dep_paths: Vec<(String, PathBuf)> = if config.gentype_config.is_some() { let resolved = packages.as_ref().map(|pkgs| { @@ -770,6 +771,7 @@ pub fn compiler_args( jsx_module_args, jsx_mode_args, jsx_preserve_args, + source_map_args, bsc_flags.to_owned(), warning_args, gentype_arg, @@ -1052,6 +1054,18 @@ fn compile_file( if source.exists() { let _ = std::fs::copy(&source, &destination).expect("copying source file failed"); } + + let mut source_map = source.clone().into_os_string(); + source_map.push(".map"); + let source_map = PathBuf::from(source_map); + let mut destination_map = destination.clone().into_os_string(); + destination_map.push(".map"); + let destination_map = PathBuf::from(destination_map); + + if source_map.exists() { + let _ = std::fs::copy(&source_map, &destination_map) + .expect("copying source map file failed"); + } } }); diff --git a/rewatch/src/config.rs b/rewatch/src/config.rs index 96cce330ae3..4674c1a1c53 100644 --- a/rewatch/src/config.rs +++ b/rewatch/src/config.rs @@ -282,6 +282,13 @@ pub struct JsxSpecs { pub preserve: Option, } +#[derive(Deserialize, Debug, Clone, Eq, PartialEq)] +#[serde(untagged)] +pub enum SourceMapConfig { + Bool(bool), + String(String), +} + #[derive(Deserialize, Debug, Clone, PartialEq, Eq)] pub enum GenTypeModule { #[serde(rename = "commonjs")] @@ -488,6 +495,12 @@ pub struct Config { pub namespace: Option, pub jsx: Option, + #[serde(rename = "sourceMap")] + pub source_map: Option, + #[serde(rename = "sourceMapSourcesContent")] + pub source_map_sources_content: Option, + #[serde(rename = "sourceMapRoot")] + pub source_map_root: Option, #[serde(rename = "experimental-features")] pub experimental_features: Option>, #[serde(rename = "gentypeconfig")] @@ -790,6 +803,35 @@ impl Config { } } + pub fn get_source_map_args(&self) -> Vec { + let mut args = Vec::new(); + + if let Some(source_map) = &self.source_map { + let value = match source_map { + SourceMapConfig::Bool(true) => "true".to_string(), + SourceMapConfig::Bool(false) => "false".to_string(), + SourceMapConfig::String(value) => match value.as_str() { + "true" | "linked" | "false" | "none" => value.to_string(), + _ => panic!("sourceMap value {value} is unsupported"), + }, + }; + args.extend(["-bs-source-map".to_string(), value]); + } + + if let Some(sources_content) = self.source_map_sources_content { + args.extend([ + "-bs-source-map-sources-content".to_string(), + sources_content.to_string(), + ]); + } + + if let Some(source_root) = &self.source_map_root { + args.extend(["-bs-source-map-root".to_string(), source_root.to_string()]); + } + + args + } + pub fn get_experimental_features_args(&self) -> Vec { match &self.experimental_features { None => vec![], @@ -1284,6 +1326,9 @@ pub mod tests { compiler_flags: None, namespace: None, jsx: None, + source_map: None, + source_map_sources_content: None, + source_map_root: None, gentype_config: None, js_post_build: None, editor: None, @@ -1586,6 +1631,39 @@ pub mod tests { ); } + #[test] + fn test_source_map_args() { + let json = r#" + { + "name": "testrepo", + "sources": [ { "dir": "src/", "subdirs": true } ], + "sourceMap": true, + "sourceMapSourcesContent": true + } + "#; + + let config = serde_json::from_str::(json).unwrap(); + assert_eq!( + config.get_source_map_args(), + vec!["-bs-source-map", "true", "-bs-source-map-sources-content", "true",] + ); + } + + #[test] + #[should_panic(expected = "sourceMap value inline is unsupported")] + fn test_source_map_rejects_inline_for_mvp() { + let json = r#" + { + "name": "testrepo", + "sources": [ { "dir": "src/", "subdirs": true } ], + "sourceMap": "inline" + } + "#; + + let config = serde_json::from_str::(json).unwrap(); + let _ = config.get_source_map_args(); + } + #[test] fn test_get_suffix() { let json = r#" diff --git a/tests/build_tests/source_map/input.js b/tests/build_tests/source_map/input.js new file mode 100644 index 00000000000..e0f53fa8e07 --- /dev/null +++ b/tests/build_tests/source_map/input.js @@ -0,0 +1,31 @@ +// @ts-check + +import * as assert from "node:assert"; +import * as fs from "node:fs/promises"; +import * as path from "node:path"; +import { setup } from "#dev/process"; + +const { execBuildOrThrow, execClean } = setup(import.meta.dirname); + +await execBuildOrThrow(); + +const jsPath = path.join(import.meta.dirname, "lib", "bs", "src", "Demo.js"); +const mapPath = `${jsPath}.map`; + +const js = await fs.readFile(jsPath, "utf8"); +assert.match(js, /\/\/# sourceMappingURL=Demo\.js\.map/); + +const map = JSON.parse(await fs.readFile(mapPath, "utf8")); +assert.equal(map.version, 3); +assert.equal(map.file, "Demo.js"); +assert.ok(map.mappings.length > 0, "source map should include mappings"); +assert.ok( + map.sources.some(source => source.endsWith("Demo.res")), + `source map should include Demo.res, got ${map.sources.join(", ")}`, +); +assert.ok( + map.sourcesContent.some(content => content.includes("let add = (a, b)")), + "source map should include source contents", +); + +await execClean(); diff --git a/tests/build_tests/source_map/rescript.json b/tests/build_tests/source_map/rescript.json new file mode 100644 index 00000000000..84483523ad0 --- /dev/null +++ b/tests/build_tests/source_map/rescript.json @@ -0,0 +1,6 @@ +{ + "name": "source_map", + "sources": ["src"], + "sourceMap": true, + "sourceMapSourcesContent": true +} diff --git a/tests/build_tests/source_map/src/Demo.res b/tests/build_tests/source_map/src/Demo.res new file mode 100644 index 00000000000..c10790a27b0 --- /dev/null +++ b/tests/build_tests/source_map/src/Demo.res @@ -0,0 +1,5 @@ +let add = (a, b) => a + b + +let crash = () => Js.Exn.raiseError("source map test") + +let value = add(20, 22) From d0ed388aadde080a76a922a9c05910542ece4fd8 Mon Sep 17 00:00:00 2001 From: mununki Date: Mon, 27 Apr 2026 17:57:32 +0900 Subject: [PATCH 02/13] Remove marker entries after lookup --- compiler/core/js_source_map.ml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/compiler/core/js_source_map.ml b/compiler/core/js_source_map.ml index f473d862288..95763b02af4 100644 --- a/compiler/core/js_source_map.ml +++ b/compiler/core/js_source_map.ml @@ -173,6 +173,13 @@ let add_mapping builder ~generated_line ~generated_column (loc : Location.t) = :: builder.mappings; builder.last_generated <- Some (generated_line, generated_column) +let take_marker_loc id = + match Hashtbl.find_opt marker_locs id with + | None -> None + | Some loc -> + Hashtbl.remove marker_locs id; + Some loc + let mark_comment fmt comment = if is_prefix ~prefix:marker_prefix comment then ( let prefix_len = String.length marker_prefix in @@ -180,7 +187,7 @@ let mark_comment fmt comment = int_of_string (String.sub comment prefix_len (String.length comment - prefix_len)) in - (match (!current, Hashtbl.find_opt marker_locs id) with + (match (!current, take_marker_loc id) with | Some builder, Some loc -> let generated_line, generated_column = Ext_pp.position fmt in add_mapping builder ~generated_line ~generated_column loc From b021c2f0df7f9219cc9e973d940e04ee49d2ced5 Mon Sep 17 00:00:00 2001 From: mununki Date: Mon, 27 Apr 2026 18:03:33 +0900 Subject: [PATCH 03/13] Preserve relative source paths in maps --- compiler/core/js_source_map.ml | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/compiler/core/js_source_map.ml b/compiler/core/js_source_map.ml index 95763b02af4..4df5c623304 100644 --- a/compiler/core/js_source_map.ml +++ b/compiler/core/js_source_map.ml @@ -76,17 +76,26 @@ let repeat x n = let rec loop acc n = if n <= 0 then acc else loop (x :: acc) (n - 1) in loop [] n +let drive_root parts = + match parts with + | drive :: _ when String.length drive = 2 && drive.[1] = ':' -> + Some (String.uppercase_ascii drive) + | _ -> None + let relative_path ~from_dir ~to_file = let from_dir = absolute_path from_dir in let to_file = absolute_path to_file in let from_parts = split_path from_dir in let to_parts = split_path to_file in - match (from_parts, to_parts) with - | from_root :: _, to_root :: _ when from_root = to_root -> + match (drive_root from_parts, drive_root to_parts) with + (* Cross-drive Windows paths cannot be represented as a filesystem-relative path. *) + | Some from_drive, Some to_drive when from_drive <> to_drive -> + normalize_slashes to_file + | Some _, None | None, Some _ -> normalize_slashes to_file + | _ -> let from_rest, to_rest = drop_common from_parts to_parts in let parts = repeat ".." (List.length from_rest) @ to_rest in if parts = [] then Filename.basename to_file else String.concat "/" parts - | _ -> Filename.basename to_file let make ~generated_file ~source_root ~sources_content = { From cdea1ce8019db297490741928db21c24fa14bf86 Mon Sep 17 00:00:00 2001 From: mununki Date: Tue, 28 Apr 2026 11:08:20 +0900 Subject: [PATCH 04/13] Change sourceMap field schema to false or object --- rewatch/src/config.rs | 108 +++++++++++++++------ tests/build_tests/source_map/rescript.json | 6 +- 2 files changed, 82 insertions(+), 32 deletions(-) diff --git a/rewatch/src/config.rs b/rewatch/src/config.rs index 4674c1a1c53..b96ccefcca7 100644 --- a/rewatch/src/config.rs +++ b/rewatch/src/config.rs @@ -286,7 +286,15 @@ pub struct JsxSpecs { #[serde(untagged)] pub enum SourceMapConfig { Bool(bool), - String(String), + Options(SourceMapOptions), +} + +#[derive(Deserialize, Debug, Clone, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct SourceMapOptions { + pub mode: String, + pub sources_content: Option, + pub source_root: Option, } #[derive(Deserialize, Debug, Clone, PartialEq, Eq)] @@ -497,10 +505,6 @@ pub struct Config { pub jsx: Option, #[serde(rename = "sourceMap")] pub source_map: Option, - #[serde(rename = "sourceMapSourcesContent")] - pub source_map_sources_content: Option, - #[serde(rename = "sourceMapRoot")] - pub source_map_root: Option, #[serde(rename = "experimental-features")] pub experimental_features: Option>, #[serde(rename = "gentypeconfig")] @@ -807,26 +811,31 @@ impl Config { let mut args = Vec::new(); if let Some(source_map) = &self.source_map { - let value = match source_map { - SourceMapConfig::Bool(true) => "true".to_string(), - SourceMapConfig::Bool(false) => "false".to_string(), - SourceMapConfig::String(value) => match value.as_str() { - "true" | "linked" | "false" | "none" => value.to_string(), - _ => panic!("sourceMap value {value} is unsupported"), - }, - }; - args.extend(["-bs-source-map".to_string(), value]); - } + match source_map { + SourceMapConfig::Bool(false) => { + args.extend(["-bs-source-map".to_string(), "false".to_string()]); + } + SourceMapConfig::Bool(true) => { + panic!("sourceMap true is unsupported; use {{ \"mode\": \"linked\" }}") + } + SourceMapConfig::Options(options) => { + match options.mode.as_str() { + "linked" => args.extend(["-bs-source-map".to_string(), "linked".to_string()]), + value => panic!("sourceMap.mode value {value} is unsupported"), + } - if let Some(sources_content) = self.source_map_sources_content { - args.extend([ - "-bs-source-map-sources-content".to_string(), - sources_content.to_string(), - ]); - } + if let Some(sources_content) = options.sources_content { + args.extend([ + "-bs-source-map-sources-content".to_string(), + sources_content.to_string(), + ]); + } - if let Some(source_root) = &self.source_map_root { - args.extend(["-bs-source-map-root".to_string(), source_root.to_string()]); + if let Some(source_root) = &options.source_root { + args.extend(["-bs-source-map-root".to_string(), source_root.to_string()]); + } + } + } } args @@ -1327,8 +1336,6 @@ pub mod tests { namespace: None, jsx: None, source_map: None, - source_map_sources_content: None, - source_map_root: None, gentype_config: None, js_post_build: None, editor: None, @@ -1637,26 +1644,67 @@ pub mod tests { { "name": "testrepo", "sources": [ { "dir": "src/", "subdirs": true } ], - "sourceMap": true, - "sourceMapSourcesContent": true + "sourceMap": { + "mode": "linked", + "sourcesContent": true, + "sourceRoot": "webpack://testrepo/" + } } "#; let config = serde_json::from_str::(json).unwrap(); assert_eq!( config.get_source_map_args(), - vec!["-bs-source-map", "true", "-bs-source-map-sources-content", "true",] + vec![ + "-bs-source-map", + "linked", + "-bs-source-map-sources-content", + "true", + "-bs-source-map-root", + "webpack://testrepo/", + ] ); } #[test] - #[should_panic(expected = "sourceMap value inline is unsupported")] + fn test_source_map_false_args() { + let json = r#" + { + "name": "testrepo", + "sources": [ { "dir": "src/", "subdirs": true } ], + "sourceMap": false + } + "#; + + let config = serde_json::from_str::(json).unwrap(); + assert_eq!(config.get_source_map_args(), vec!["-bs-source-map", "false",]); + } + + #[test] + #[should_panic(expected = "sourceMap true is unsupported")] + fn test_source_map_rejects_true_for_nested_config() { + let json = r#" + { + "name": "testrepo", + "sources": [ { "dir": "src/", "subdirs": true } ], + "sourceMap": true + } + "#; + + let config = serde_json::from_str::(json).unwrap(); + let _ = config.get_source_map_args(); + } + + #[test] + #[should_panic(expected = "sourceMap.mode value inline is unsupported")] fn test_source_map_rejects_inline_for_mvp() { let json = r#" { "name": "testrepo", "sources": [ { "dir": "src/", "subdirs": true } ], - "sourceMap": "inline" + "sourceMap": { + "mode": "inline" + } } "#; diff --git a/tests/build_tests/source_map/rescript.json b/tests/build_tests/source_map/rescript.json index 84483523ad0..2485256bb0f 100644 --- a/tests/build_tests/source_map/rescript.json +++ b/tests/build_tests/source_map/rescript.json @@ -1,6 +1,8 @@ { "name": "source_map", "sources": ["src"], - "sourceMap": true, - "sourceMapSourcesContent": true + "sourceMap": { + "mode": "linked", + "sourcesContent": true + } } From 879cd697020f2cba50f61534a98448b346c3172d Mon Sep 17 00:00:00 2001 From: mununki Date: Tue, 28 Apr 2026 11:08:41 +0900 Subject: [PATCH 05/13] sourcemap schema in rescript.json --- docs/docson/build-schema.json | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/docs/docson/build-schema.json b/docs/docson/build-schema.json index af78908ea8c..9c0462822e0 100644 --- a/docs/docson/build-schema.json +++ b/docs/docson/build-schema.json @@ -47,6 +47,33 @@ } ] }, + "source-map-spec": { + "oneOf": [ + { + "enum": [false], + "description": "Disable source map generation." + }, + { + "type": "object", + "properties": { + "mode": { + "enum": ["linked"], + "description": "Generate a separate .js.map file next to each generated JavaScript file and append a sourceMappingURL comment. Only linked source maps are supported for now." + }, + "sourcesContent": { + "type": "boolean", + "description": "Include original .res source text in the source map. Default: false." + }, + "sourceRoot": { + "type": "string", + "description": "Optional sourceRoot value written to the generated source map." + } + }, + "required": ["mode"], + "additionalProperties": false + } + ] + }, "package-specs": { "oneOf": [ { @@ -448,6 +475,10 @@ "$ref": "#/definitions/package-specs", "description": "ReScript can currently output to [Commonjs](https://en.wikipedia.org/wiki/CommonJS), and [ES6 modules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import)" }, + "sourceMap": { + "$ref": "#/definitions/source-map-spec", + "description": "Configure Source Map v3 output for generated JavaScript." + }, "bs-external-includes": { "type": "array", "items": { From f7ca25e7a655b23e0d087d86783adf2eea442850 Mon Sep 17 00:00:00 2001 From: mununki Date: Wed, 29 Apr 2026 02:56:34 +0900 Subject: [PATCH 06/13] Add source map enabled policy --- docs/docson/build-schema.json | 6 +- rewatch/src/build.rs | 7 +- rewatch/src/build/build_types.rs | 8 +- rewatch/src/build/clean.rs | 4 +- rewatch/src/build/compile.rs | 4 +- rewatch/src/config.rs | 93 ++++++++++++++++++++-- rewatch/src/watcher.rs | 5 +- tests/build_tests/source_map/rescript.json | 1 + 8 files changed, 113 insertions(+), 15 deletions(-) diff --git a/docs/docson/build-schema.json b/docs/docson/build-schema.json index 9c0462822e0..266a8f07186 100644 --- a/docs/docson/build-schema.json +++ b/docs/docson/build-schema.json @@ -56,6 +56,10 @@ { "type": "object", "properties": { + "enabled": { + "enum": ["dev", "always"], + "description": "`dev` generates source maps only during watch mode. `always` generates source maps during both build and watch." + }, "mode": { "enum": ["linked"], "description": "Generate a separate .js.map file next to each generated JavaScript file and append a sourceMappingURL comment. Only linked source maps are supported for now." @@ -69,7 +73,7 @@ "description": "Optional sourceRoot value written to the generated source map." } }, - "required": ["mode"], + "required": ["enabled", "mode"], "additionalProperties": false } ] diff --git a/rewatch/src/build.rs b/rewatch/src/build.rs index 53d61de9808..4852670d2fc 100644 --- a/rewatch/src/build.rs +++ b/rewatch/src/build.rs @@ -12,6 +12,7 @@ pub mod read_compile_state; use self::parse::parser_args; use crate::build::compile::{mark_modules_with_deleted_deps_dirty, mark_modules_with_expired_deps_dirty}; use crate::build::compiler_info::{CompilerCheckResult, verify_compiler_info, write_compiler_info}; +use crate::config::SourceMapCommand; use crate::helpers::emojis::*; use crate::helpers::{self}; use crate::lock::{LockKind, drop_lock, get_lock_or_exit}; @@ -140,7 +141,8 @@ pub fn get_compiler_args(rescript_file_path: &Path) -> Result { is_type_dev, true, None, // No warn_error_override for compiler-args command - &[], // Source dirs not available outside full build; gentype falls back to defaults. + SourceMapCommand::Build, + &[], // Source dirs not available outside full build; gentype falls back to defaults. )?; let result = serde_json::to_string_pretty(&CompilerArgs { @@ -175,6 +177,7 @@ pub fn initialize_build( warn_error: Option, prod: bool, features: Option>, + source_map_command: SourceMapCommand, ) -> Result { let project_context = ProjectContext::new(path)?; let compiler = get_compiler_info(&project_context)?; @@ -195,6 +198,7 @@ pub fn initialize_build( compiler, warn_error, features, + source_map_command, ); packages::parse_packages(&mut build_state)?; @@ -609,6 +613,7 @@ pub fn build( warn_error, prod, features, + SourceMapCommand::Build, ) .with_context(|| "Could not initialize build")?; diff --git a/rewatch/src/build/build_types.rs b/rewatch/src/build/build_types.rs index 4d1f3f8bf0f..3916c6d8ef7 100644 --- a/rewatch/src/build/build_types.rs +++ b/rewatch/src/build/build_types.rs @@ -1,5 +1,5 @@ use crate::build::packages::{Namespace, Package}; -use crate::config::Config; +use crate::config::{Config, SourceMapCommand}; use crate::project_context::ProjectContext; use ahash::{AHashMap, AHashSet}; use blake3::Hash; @@ -110,6 +110,7 @@ pub struct BuildState { pub deleted_modules: AHashSet, pub compiler_info: CompilerInfo, pub deps_initialized: bool, + pub source_map_command: SourceMapCommand, } /// Extended build state that includes command-line specific overrides. @@ -151,6 +152,7 @@ impl BuildState { project_context: ProjectContext, packages: AHashMap, compiler: CompilerInfo, + source_map_command: SourceMapCommand, ) -> Self { Self { project_context, @@ -160,6 +162,7 @@ impl BuildState { deleted_modules: AHashSet::new(), compiler_info: compiler, deps_initialized: false, + source_map_command, } } @@ -181,10 +184,11 @@ impl BuildCommandState { compiler: CompilerInfo, warn_error_override: Option, features: Option>, + source_map_command: SourceMapCommand, ) -> Self { Self { root_folder, - build_state: BuildState::new(project_context, packages, compiler), + build_state: BuildState::new(project_context, packages, compiler, source_map_command), warn_error_override, features, } diff --git a/rewatch/src/build/clean.rs b/rewatch/src/build/clean.rs index 8d88f1f4bc3..a5084a63468 100644 --- a/rewatch/src/build/clean.rs +++ b/rewatch/src/build/clean.rs @@ -2,7 +2,7 @@ use super::build_types::*; use super::packages; use crate::build; use crate::build::packages::Package; -use crate::config::Config; +use crate::config::{Config, SourceMapCommand}; use crate::helpers; use crate::helpers::emojis::*; use crate::project_context::ProjectContext; @@ -373,7 +373,7 @@ pub fn clean(path: &Path, show_progress: bool, plain_output: bool, prod: bool) - } let timing_clean_mjs = Instant::now(); - let mut build_state = BuildState::new(project_context, packages, compiler_info); + let mut build_state = BuildState::new(project_context, packages, compiler_info, SourceMapCommand::Build); packages::parse_packages(&mut build_state)?; let root_config = build_state.get_root_config(); let suffix_for_print = match root_config.package_specs { diff --git a/rewatch/src/build/compile.rs b/rewatch/src/build/compile.rs index bce155ac4e6..9158820b851 100644 --- a/rewatch/src/build/compile.rs +++ b/rewatch/src/build/compile.rs @@ -652,6 +652,7 @@ pub fn compiler_args( is_local_dep: bool, // Command-line --warn-error flag override (takes precedence over rescript.json config) warn_error_override: Option, + source_map_command: config::SourceMapCommand, // Pre-expanded source directories for the current package (used by gentype). // Pass an empty slice when unavailable (e.g. the compiler-args CLI command). current_package_dirs: &[PathBuf], @@ -686,7 +687,7 @@ pub fn compiler_args( let jsx_module_args = root_config.get_jsx_module_args(); let jsx_mode_args = root_config.get_jsx_mode_args(); let jsx_preserve_args = root_config.get_jsx_preserve_args(); - let source_map_args = root_config.get_source_map_args(); + let source_map_args = root_config.get_source_map_args(source_map_command); let bsb_project_root = project_context.get_root_path(); let dep_paths: Vec<(String, PathBuf)> = if config.gentype_config.is_some() { let resolved = packages.as_ref().map(|pkgs| { @@ -919,6 +920,7 @@ fn compile_file( is_type_dev, package.is_local_dep, warn_error_override, + build_state.source_map_command, current_package_dirs, )?; diff --git a/rewatch/src/config.rs b/rewatch/src/config.rs index b96ccefcca7..c9b50541b3c 100644 --- a/rewatch/src/config.rs +++ b/rewatch/src/config.rs @@ -289,14 +289,28 @@ pub enum SourceMapConfig { Options(SourceMapOptions), } +#[derive(Deserialize, Debug, Clone, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub enum SourceMapEnabled { + Dev, + Always, +} + #[derive(Deserialize, Debug, Clone, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub struct SourceMapOptions { + pub enabled: SourceMapEnabled, pub mode: String, pub sources_content: Option, pub source_root: Option, } +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum SourceMapCommand { + Build, + Watch, +} + #[derive(Deserialize, Debug, Clone, PartialEq, Eq)] pub enum GenTypeModule { #[serde(rename = "commonjs")] @@ -807,7 +821,7 @@ impl Config { } } - pub fn get_source_map_args(&self) -> Vec { + pub fn get_source_map_args(&self, command: SourceMapCommand) -> Vec { let mut args = Vec::new(); if let Some(source_map) = &self.source_map { @@ -819,11 +833,22 @@ impl Config { panic!("sourceMap true is unsupported; use {{ \"mode\": \"linked\" }}") } SourceMapConfig::Options(options) => { - match options.mode.as_str() { - "linked" => args.extend(["-bs-source-map".to_string(), "linked".to_string()]), + let source_map_mode = match options.mode.as_str() { + "linked" => "linked", value => panic!("sourceMap.mode value {value} is unsupported"), + }; + + let source_map_enabled = match options.enabled { + SourceMapEnabled::Dev => command == SourceMapCommand::Watch, + SourceMapEnabled::Always => true, + }; + + if !source_map_enabled { + return vec!["-bs-source-map".to_string(), "false".to_string()]; } + args.extend(["-bs-source-map".to_string(), source_map_mode.to_string()]); + if let Some(sources_content) = options.sources_content { args.extend([ "-bs-source-map-sources-content".to_string(), @@ -1645,6 +1670,7 @@ pub mod tests { "name": "testrepo", "sources": [ { "dir": "src/", "subdirs": true } ], "sourceMap": { + "enabled": "always", "mode": "linked", "sourcesContent": true, "sourceRoot": "webpack://testrepo/" @@ -1654,7 +1680,7 @@ pub mod tests { let config = serde_json::from_str::(json).unwrap(); assert_eq!( - config.get_source_map_args(), + config.get_source_map_args(SourceMapCommand::Build), vec![ "-bs-source-map", "linked", @@ -1666,6 +1692,36 @@ pub mod tests { ); } + #[test] + fn test_source_map_dev_args_only_in_watch() { + let json = r#" + { + "name": "testrepo", + "sources": [ { "dir": "src/", "subdirs": true } ], + "sourceMap": { + "enabled": "dev", + "mode": "linked", + "sourcesContent": true + } + } + "#; + + let config = serde_json::from_str::(json).unwrap(); + assert_eq!( + config.get_source_map_args(SourceMapCommand::Build), + vec!["-bs-source-map", "false",] + ); + assert_eq!( + config.get_source_map_args(SourceMapCommand::Watch), + vec![ + "-bs-source-map", + "linked", + "-bs-source-map-sources-content", + "true", + ] + ); + } + #[test] fn test_source_map_false_args() { let json = r#" @@ -1677,7 +1733,10 @@ pub mod tests { "#; let config = serde_json::from_str::(json).unwrap(); - assert_eq!(config.get_source_map_args(), vec!["-bs-source-map", "false",]); + assert_eq!( + config.get_source_map_args(SourceMapCommand::Build), + vec!["-bs-source-map", "false",] + ); } #[test] @@ -1692,7 +1751,26 @@ pub mod tests { "#; let config = serde_json::from_str::(json).unwrap(); - let _ = config.get_source_map_args(); + let _ = config.get_source_map_args(SourceMapCommand::Build); + } + + #[test] + fn test_source_map_requires_enabled() { + let json = r#" + { + "name": "testrepo", + "sources": [ { "dir": "src/", "subdirs": true } ], + "sourceMap": { + "mode": "linked" + } + } + "#; + + let error = serde_json::from_str::(json).unwrap_err(); + assert!( + error.to_string().contains("SourceMapConfig"), + "unexpected error: {error}" + ); } #[test] @@ -1703,13 +1781,14 @@ pub mod tests { "name": "testrepo", "sources": [ { "dir": "src/", "subdirs": true } ], "sourceMap": { + "enabled": "always", "mode": "inline" } } "#; let config = serde_json::from_str::(json).unwrap(); - let _ = config.get_source_map_args(); + let _ = config.get_source_map_args(SourceMapCommand::Build); } #[test] diff --git a/rewatch/src/watcher.rs b/rewatch/src/watcher.rs index 89c405cd0ad..fe0ca13931f 100644 --- a/rewatch/src/watcher.rs +++ b/rewatch/src/watcher.rs @@ -2,7 +2,7 @@ use crate::build; use crate::build::build_types::{BuildCommandState, SourceType}; use crate::build::clean; use crate::cmd; -use crate::config; +use crate::config::{self, SourceMapCommand}; use crate::helpers; use crate::helpers::StrippedVerbatimPath; use crate::lock::LockKind; @@ -471,6 +471,7 @@ async fn async_watch( build_state.get_warn_error_override(), prod, features.clone(), + SourceMapCommand::Watch, ) .expect("Could not initialize build"); @@ -569,6 +570,7 @@ pub fn start( warn_error.clone(), prod, features.clone(), + SourceMapCommand::Watch, ) .with_context(|| "Could not initialize build")?; @@ -669,6 +671,7 @@ mod tests { compiler, None, None, + SourceMapCommand::Watch, ); build_state.insert_module(module_name, module); build_state diff --git a/tests/build_tests/source_map/rescript.json b/tests/build_tests/source_map/rescript.json index 2485256bb0f..ac944f2b4fa 100644 --- a/tests/build_tests/source_map/rescript.json +++ b/tests/build_tests/source_map/rescript.json @@ -2,6 +2,7 @@ "name": "source_map", "sources": ["src"], "sourceMap": { + "enabled": "always", "mode": "linked", "sourcesContent": true } From e258af09601d58e1a2e7f069891500e6a5b1a1a4 Mon Sep 17 00:00:00 2001 From: mununki Date: Wed, 29 Apr 2026 10:26:08 +0900 Subject: [PATCH 07/13] Preserve source map markers across package targets --- compiler/core/js_implementation.ml | 23 +++++++------- compiler/core/js_source_map.ml | 18 ++++++----- compiler/core/js_source_map.mli | 2 ++ tests/build_tests/source_map/input.js | 37 ++++++++++++---------- tests/build_tests/source_map/rescript.json | 12 +++++++ 5 files changed, 57 insertions(+), 35 deletions(-) diff --git a/compiler/core/js_implementation.ml b/compiler/core/js_implementation.ml index 5f4e4e6c765..a6ea25a5b1c 100644 --- a/compiler/core/js_implementation.ml +++ b/compiler/core/js_implementation.ml @@ -141,17 +141,18 @@ let after_parsing_impl ppf outputprefix (ast : Parsetree.structure) = let typedtree_coercion = (typedtree, coercion) in print_if ppf Clflags.dump_typedtree Printtyped.implementation_with_coercion typedtree_coercion; - (if !Js_config.cmi_only then Warnings.check_fatal () - else - let lambda, exports = - 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 - in - if not !Js_config.cmj_only then - Lam_compile_main.lambda_as_module js_program outputprefix); + Js_source_map.with_marker_scope (fun () -> + if !Js_config.cmi_only then Warnings.check_fatal () + else + let lambda, exports = + 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 + in + if not !Js_config.cmj_only then + Lam_compile_main.lambda_as_module js_program outputprefix); process_with_gentype (outputprefix ^ ".cmt")) let implementation ~parser ppf ?outputprefix fname = diff --git a/compiler/core/js_source_map.ml b/compiler/core/js_source_map.ml index 4df5c623304..7b5d90d31a6 100644 --- a/compiler/core/js_source_map.ml +++ b/compiler/core/js_source_map.ml @@ -46,6 +46,15 @@ let comment_of_loc (loc : Location.t) = Hashtbl.replace marker_locs id loc; Some (marker_prefix ^ string_of_int id) +let with_marker_scope f = + let first_marker = !next_marker in + Ext_pervasives.finally () + ~clean:(fun () -> + for id = first_marker to !next_marker - 1 do + Hashtbl.remove marker_locs id + done) + f + let with_builder builder f = let old = !current in current := builder; @@ -182,13 +191,6 @@ let add_mapping builder ~generated_line ~generated_column (loc : Location.t) = :: builder.mappings; builder.last_generated <- Some (generated_line, generated_column) -let take_marker_loc id = - match Hashtbl.find_opt marker_locs id with - | None -> None - | Some loc -> - Hashtbl.remove marker_locs id; - Some loc - let mark_comment fmt comment = if is_prefix ~prefix:marker_prefix comment then ( let prefix_len = String.length marker_prefix in @@ -196,7 +198,7 @@ let mark_comment fmt comment = int_of_string (String.sub comment prefix_len (String.length comment - prefix_len)) in - (match (!current, take_marker_loc id) with + (match (!current, Hashtbl.find_opt marker_locs id) with | Some builder, Some loc -> let generated_line, generated_column = Ext_pp.position fmt in add_mapping builder ~generated_line ~generated_column loc diff --git a/compiler/core/js_source_map.mli b/compiler/core/js_source_map.mli index adad8e3303a..ad267a5b881 100644 --- a/compiler/core/js_source_map.mli +++ b/compiler/core/js_source_map.mli @@ -3,6 +3,8 @@ type t val make : generated_file:string -> source_root:string -> sources_content:bool -> t +val with_marker_scope : (unit -> 'a) -> 'a + val with_builder : t option -> (unit -> 'a) -> 'a val comment_of_loc : Location.t -> string option diff --git a/tests/build_tests/source_map/input.js b/tests/build_tests/source_map/input.js index e0f53fa8e07..1a63ffb80f5 100644 --- a/tests/build_tests/source_map/input.js +++ b/tests/build_tests/source_map/input.js @@ -9,23 +9,28 @@ const { execBuildOrThrow, execClean } = setup(import.meta.dirname); await execBuildOrThrow(); -const jsPath = path.join(import.meta.dirname, "lib", "bs", "src", "Demo.js"); -const mapPath = `${jsPath}.map`; +for (const filename of ["Demo.cjs", "Demo.mjs"]) { + const jsPath = path.join(import.meta.dirname, "lib", "bs", "src", filename); + const mapPath = `${jsPath}.map`; -const js = await fs.readFile(jsPath, "utf8"); -assert.match(js, /\/\/# sourceMappingURL=Demo\.js\.map/); + const js = await fs.readFile(jsPath, "utf8"); + assert.match( + js, + new RegExp(`//# sourceMappingURL=${filename.replace(".", "\\.")}\\.map`), + ); -const map = JSON.parse(await fs.readFile(mapPath, "utf8")); -assert.equal(map.version, 3); -assert.equal(map.file, "Demo.js"); -assert.ok(map.mappings.length > 0, "source map should include mappings"); -assert.ok( - map.sources.some(source => source.endsWith("Demo.res")), - `source map should include Demo.res, got ${map.sources.join(", ")}`, -); -assert.ok( - map.sourcesContent.some(content => content.includes("let add = (a, b)")), - "source map should include source contents", -); + const map = JSON.parse(await fs.readFile(mapPath, "utf8")); + assert.equal(map.version, 3); + assert.equal(map.file, filename); + assert.ok(map.mappings.length > 0, `${filename}.map should include mappings`); + assert.ok( + map.sources.some(source => source.endsWith("Demo.res")), + `${filename}.map should include Demo.res, got ${map.sources.join(", ")}`, + ); + assert.ok( + map.sourcesContent.some(content => content.includes("let add = (a, b)")), + `${filename}.map should include source contents`, + ); +} await execClean(); diff --git a/tests/build_tests/source_map/rescript.json b/tests/build_tests/source_map/rescript.json index ac944f2b4fa..f4556b02c12 100644 --- a/tests/build_tests/source_map/rescript.json +++ b/tests/build_tests/source_map/rescript.json @@ -1,6 +1,18 @@ { "name": "source_map", "sources": ["src"], + "package-specs": [ + { + "module": "commonjs", + "in-source": true, + "suffix": ".cjs" + }, + { + "module": "esmodule", + "in-source": true, + "suffix": ".mjs" + } + ], "sourceMap": { "enabled": "always", "mode": "linked", From f941252c24b7e578647afbc39e63c12bdd493092 Mon Sep 17 00:00:00 2001 From: mununki Date: Thu, 30 Apr 2026 16:27:07 +0900 Subject: [PATCH 08/13] Add comments --- compiler/core/js_source_map.ml | 29 +++++++++++++++++++++++++++++ compiler/ext/ext_pp.ml | 4 ++++ 2 files changed, 33 insertions(+) diff --git a/compiler/core/js_source_map.ml b/compiler/core/js_source_map.ml index 7b5d90d31a6..6a7cf7f77fd 100644 --- a/compiler/core/js_source_map.ml +++ b/compiler/core/js_source_map.ml @@ -1,3 +1,32 @@ +(* Source Map v3 maps generated JavaScript positions back to original ReScript + positions. Both generated and original columns are UTF-16 code unit offsets, + which is the convention used by JavaScript engines and browsers. + + Source map generation is tied to the JS printer: + + 1. Lambda-to-JS conversion attaches internal marker comments to JS nodes, + because it does not know the final generated line/column yet. + 2. A source map builder is installed while one generated JS file is printed. + It tracks sources, optional source contents, and mapping entries for that + output file. + 3. The JS printer updates its generated line/column as it writes text. When + it sees one of the internal marker comments, it suppresses the comment from + output and records the current generated position against the original + ReScript location. + 4. After printing, the collected mappings are sorted and encoded into the + compact Source Map v3 "mappings" field using base64 VLQ. The JSON map is + emitted next to the generated JavaScript and the JS file receives a + sourceMappingURL comment. + + Original locations come from OCaml Location.t values, whose columns are byte + offsets. When source contents are available, original columns are converted + from UTF-8 byte offsets to UTF-16 code unit offsets before being stored in the + map. + + A compiled JS program can be printed more than once for multiple package + targets, such as CommonJS and ESM. Marker locations therefore remain available + for every print pass and are cleaned up when the compilation unit finishes. *) + type source = {relative_path: string; content: string option} type mapping = { diff --git a/compiler/ext/ext_pp.ml b/compiler/ext/ext_pp.ml index 384782f526e..d45f558eed4 100644 --- a/compiler/ext/ext_pp.ml +++ b/compiler/ext/ext_pp.ml @@ -51,6 +51,10 @@ let update_position t s = loop (i + 1) | c -> let byte = Char.code c in + (* Source map columns are counted in UTF-16 code units, while OCaml + strings are UTF-8 bytes. Decode only enough UTF-8 structure to + advance the generated column correctly: 1-3 byte sequences are one + UTF-16 code unit, and 4-byte sequences are surrogate pairs. *) if byte < 0x80 then ( t.column <- t.column + 1; loop (i + 1)) From fdebf10593b0ea9eca90f68ab1d6f5e348293db4 Mon Sep 17 00:00:00 2001 From: mununki Date: Thu, 30 Apr 2026 22:52:53 +0900 Subject: [PATCH 09/13] Add mapping %debugger statement --- compiler/core/js_stmt_make.ml | 4 +- compiler/core/js_stmt_make.mli | 2 + compiler/core/lam_compile.ml | 6 +- tests/build_tests/source_map/input.js | 113 ++++++++++++++++++++++ tests/build_tests/source_map/src/Demo.res | 11 +++ 5 files changed, 133 insertions(+), 3 deletions(-) diff --git a/compiler/core/js_stmt_make.ml b/compiler/core/js_stmt_make.ml index c0edf8bc321..16b43e4782d 100644 --- a/compiler/core/js_stmt_make.ml +++ b/compiler/core/js_stmt_make.ml @@ -339,4 +339,6 @@ let break_ ?label () : t = {statement_desc = Break label; comment = None} let continue_ ?label () : t = {statement_desc = Continue label; comment = None} -let debugger_block : t list = [{statement_desc = Debugger; comment = None}] +let debugger_stmt ?comment () : t = {statement_desc = Debugger; comment} + +let debugger_block : t list = [debugger_stmt ()] diff --git a/compiler/core/js_stmt_make.mli b/compiler/core/js_stmt_make.mli index d58ced95248..e83c8f8a9ff 100644 --- a/compiler/core/js_stmt_make.mli +++ b/compiler/core/js_stmt_make.mli @@ -174,4 +174,6 @@ val break_ : ?label:J.label -> unit -> t val continue_ : ?label:J.label -> unit -> t +val debugger_stmt : ?comment:string -> unit -> t + val debugger_block : t list diff --git a/compiler/core/lam_compile.ml b/compiler/core/lam_compile.ml index 0f251a20116..9ddf1714a42 100644 --- a/compiler/core/lam_compile.ml +++ b/compiler/core/lam_compile.ml @@ -1743,11 +1743,13 @@ let compile output_prefix = | {value = None} -> assert false) | {primitive = Psequand; args = [l; r]; _} -> compile_sequand l r lambda_cxt | {primitive = Psequor; args = [l; r]} -> compile_sequor l r lambda_cxt - | {primitive = Pdebugger; _} -> + | {primitive = Pdebugger; loc; _} -> (* [%debugger] guarantees that the expression does not matter TODO: make it even safer *) + let comment = source_map_comment loc in Js_output.output_of_block_and_expression lambda_cxt.continuation - S.debugger_block E.unit + [S.debugger_stmt ?comment ()] + E.unit (* TODO: check the arity of fn before wrapping it we need mark something that such eta-conversion can not be simplified in some cases diff --git a/tests/build_tests/source_map/input.js b/tests/build_tests/source_map/input.js index 1a63ffb80f5..e3a995c6fce 100644 --- a/tests/build_tests/source_map/input.js +++ b/tests/build_tests/source_map/input.js @@ -9,6 +9,88 @@ const { execBuildOrThrow, execClean } = setup(import.meta.dirname); await execBuildOrThrow(); +const base64VlqChars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +function decodeVlq(segment) { + const values = []; + let value = 0; + let shift = 0; + + for (const char of segment) { + let digit = base64VlqChars.indexOf(char); + assert.notEqual(digit, -1, `invalid base64 VLQ character: ${char}`); + + const continuation = (digit & 32) !== 0; + digit &= 31; + value += digit << shift; + + if (continuation) { + shift += 5; + } else { + values.push(value & 1 ? -(value >> 1) : value >> 1); + value = 0; + shift = 0; + } + } + + return values; +} + +function decodeMappings(mappings) { + const decoded = []; + let previousSource = 0; + let previousOriginalLine = 0; + let previousOriginalColumn = 0; + + mappings.split(";").forEach((line, generatedLine) => { + let previousGeneratedColumn = 0; + + for (const segment of line.split(",")) { + if (segment === "") { + continue; + } + + const fields = decodeVlq(segment); + previousGeneratedColumn += fields[0]; + + if (fields.length >= 4) { + previousSource += fields[1]; + previousOriginalLine += fields[2]; + previousOriginalColumn += fields[3]; + decoded.push({ + generatedLine, + generatedColumn: previousGeneratedColumn, + sourceIndex: previousSource, + originalLine: previousOriginalLine, + originalColumn: previousOriginalColumn, + }); + } + } + }); + + return decoded; +} + +function findTokenPositions(content, token) { + return content.split(/\r?\n/).flatMap((line, lineIndex) => { + const positions = []; + let column = line.indexOf(token); + + while (column !== -1) { + positions.push({ line: lineIndex, column }); + column = line.indexOf(token, column + token.length); + } + + return positions; + }); +} + +const sourcePath = path.join(import.meta.dirname, "src", "Demo.res"); +const source = await fs.readFile(sourcePath, "utf8"); +const originalDebuggerPositions = findTokenPositions(source, "%debugger"); +assert.equal(originalDebuggerPositions.length, 2); + for (const filename of ["Demo.cjs", "Demo.mjs"]) { const jsPath = path.join(import.meta.dirname, "lib", "bs", "src", filename); const mapPath = `${jsPath}.map`; @@ -31,6 +113,37 @@ for (const filename of ["Demo.cjs", "Demo.mjs"]) { map.sourcesContent.some(content => content.includes("let add = (a, b)")), `${filename}.map should include source contents`, ); + + const generatedDebuggerPositions = findTokenPositions(js, "debugger"); + assert.equal(generatedDebuggerPositions.length, 2); + + const decodedMappings = decodeMappings(map.mappings); + const debuggerMappings = generatedDebuggerPositions.map(position => { + const mapping = decodedMappings.find( + decoded => + decoded.generatedLine === position.line && + decoded.generatedColumn === position.column, + ); + assert.ok( + mapping, + `${filename}.map should include an exact mapping for debugger at ${position.line}:${position.column}`, + ); + return mapping; + }); + + assert.deepEqual( + debuggerMappings.map(mapping => ({ + line: mapping.originalLine, + column: mapping.originalColumn, + })), + originalDebuggerPositions, + ); + assert.ok( + debuggerMappings.every(mapping => + map.sources[mapping.sourceIndex].endsWith("Demo.res"), + ), + `${filename}.map debugger mappings should point to Demo.res`, + ); } await execClean(); diff --git a/tests/build_tests/source_map/src/Demo.res b/tests/build_tests/source_map/src/Demo.res index c10790a27b0..9637d4217e4 100644 --- a/tests/build_tests/source_map/src/Demo.res +++ b/tests/build_tests/source_map/src/Demo.res @@ -2,4 +2,15 @@ let add = (a, b) => a + b let crash = () => Js.Exn.raiseError("source map test") +let debugStatement = () => { + %debugger + add(1, 2) +} + +let debugValue = () => { + let v = %debugger + ignore(v) + add(3, 4) +} + let value = add(20, 22) From 0778383f435ae9817b896aab431853fcbd7815e7 Mon Sep 17 00:00:00 2001 From: mununki Date: Fri, 1 May 2026 20:13:47 +0900 Subject: [PATCH 10/13] Refactor source maps to use JS IR source locations --- compiler/core/j.ml | 14 +- compiler/core/js_analyzer.ml | 2 +- compiler/core/js_dump.ml | 13 +- compiler/core/js_exp_make.ml | 383 ++++++++++++++----- compiler/core/js_fold.ml | 11 +- compiler/core/js_implementation.ml | 23 +- compiler/core/js_of_lam_variant.ml | 3 + compiler/core/js_pass_flatten.ml | 6 +- compiler/core/js_record_fold.ml | 7 +- compiler/core/js_record_iter.ml | 9 +- compiler/core/js_record_map.ml | 13 +- compiler/core/js_source_map.ml | 67 +--- compiler/core/js_source_map.mli | 6 +- compiler/core/js_stmt_make.ml | 85 +++- compiler/core/lam_compile.ml | 116 +++--- compiler/core/lam_compile_primitive.ml | 8 +- tests/build_tests/source_map/input.js | 32 ++ tests/build_tests/source_map/src/Demo.res | 2 + tests/ounit_tests/ounit_js_analyzer_tests.ml | 2 + 19 files changed, 529 insertions(+), 273 deletions(-) diff --git a/compiler/core/j.ml b/compiler/core/j.ml index f20b22ec727..d3e517126d7 100644 --- a/compiler/core/j.ml +++ b/compiler/core/j.ml @@ -245,6 +245,7 @@ and case_clause = { should_break: bool; (* true means break *) comment: string option; + source_loc: Location.t option; } and string_clause = Ast_untagged_variants.tag_type * case_clause @@ -289,8 +290,17 @@ and statement_desc = | Try of block * (exception_ident * block) option * block option | Debugger -and expression = {expression_desc: expression_desc; comment: string option} -and statement = {statement_desc: statement_desc; comment: string option} +and expression = { + expression_desc: expression_desc; + comment: string option; + source_loc: Location.t option; +} + +and statement = { + statement_desc: statement_desc; + comment: string option; + source_loc: Location.t option; +} and variable_declaration = { ident: ident; diff --git a/compiler/core/js_analyzer.ml b/compiler/core/js_analyzer.ml index 25852412667..08551ba0079 100644 --- a/compiler/core/js_analyzer.ml +++ b/compiler/core/js_analyzer.ml @@ -264,7 +264,7 @@ let rev_flatten_seq (x : J.expression) = let rec aux acc (x : J.expression) : J.block = match x.expression_desc with | Seq (a, b) -> aux (aux acc a) b - | _ -> {statement_desc = Exp x; comment = None} :: acc + | _ -> {statement_desc = Exp x; comment = None; source_loc = None} :: acc in aux [] x diff --git a/compiler/core/js_dump.ml b/compiler/core/js_dump.ml index 324ef4e5421..d3de5f7bbcb 100644 --- a/compiler/core/js_dump.ml +++ b/compiler/core/js_dump.ml @@ -429,7 +429,8 @@ and pp_function ~return_unit ~async ~is_method ?directive cxt (f : P.t) and pp_one_case_clause : 'a. _ -> P.t -> (P.t -> 'a -> unit) -> 'a * J.case_clause -> _ = fun cxt f pp_cond - (switch_case, ({switch_body; should_break; comment} : J.case_clause)) -> + ( switch_case, + ({switch_body; should_break; comment; source_loc} : J.case_clause) ) -> P.newline f; let cxt = P.group f 1 (fun _ -> @@ -437,6 +438,7 @@ and pp_one_case_clause : P.string f L.case; P.space f; pp_comment_option f comment; + Js_source_map.mark_source_loc f source_loc; pp_cond f switch_case; (* could be integer or string *) P.space f; @@ -483,6 +485,7 @@ and vident cxt f (v : J.vident) = (* The higher the level, the more likely that inner has to add parens *) and expression ~level:l cxt f (exp : J.expression) : cxt = pp_comment_option f exp.comment; + Js_source_map.mark_source_loc f exp.source_loc; expression_desc cxt ~level:l f exp.expression_desc and expression_desc cxt ~(level : int) f x : cxt = @@ -1292,6 +1295,7 @@ and variable_declaration top cxt f (variable : J.variable_declaration) : cxt = match e.expression_desc with | Fun {is_method; params; body; env; return_unit; async; directive} -> pp_comment_option f e.comment; + Js_source_map.mark_source_loc f e.source_loc; pp_function ?directive ~is_method ~return_unit ~async ~fn_state:(if top then Name_top name else Name_non_top name) cxt f params body env @@ -1312,8 +1316,7 @@ and ipp_comment : 'a. P.t -> 'a -> unit = fun _f _comment -> () *) and pp_comment f comment = - if Js_source_map.mark_comment f comment then () - else if String.length comment > 0 then ( + if String.length comment > 0 then ( P.string f "/* "; P.string f comment; P.string f " */") @@ -1323,8 +1326,10 @@ and pp_comment_option f comment = | None -> () | Some x -> pp_comment f x -and statement top cxt f ({statement_desc = s; comment; _} : J.statement) : cxt = +and statement top cxt f + ({statement_desc = s; comment; source_loc} : J.statement) : cxt = pp_comment_option f comment; + Js_source_map.mark_source_loc f source_loc; statement_desc top cxt f s and statement_desc top cxt f (s : J.statement_desc) : cxt = diff --git a/compiler/core/js_exp_make.ml b/compiler/core/js_exp_make.ml index 210c0a58dd9..12dcc623ba4 100644 --- a/compiler/core/js_exp_make.ml +++ b/compiler/core/js_exp_make.ml @@ -54,29 +54,35 @@ and is_pure_sub_exp (x : t) = remove_pure_sub_exp x = None (* let mk ?comment exp : t = {expression_desc = exp ; comment } *) -let var ?comment id : t = {expression_desc = Var (Id id); comment} +let var ?comment id : t = + {expression_desc = Var (Id id); comment; source_loc = None} (* only used in property access, Invariant: it should not call an external module .. *) let js_global ?comment (v : string) = var ?comment (Ext_ident.create_js v) let undefined : t = - {expression_desc = Undefined {is_unit = false}; comment = None} -let nil : t = {expression_desc = Null; comment = None} + { + expression_desc = Undefined {is_unit = false}; + comment = None; + source_loc = None; + } +let nil : t = {expression_desc = Null; comment = None; source_loc = None} let call ?comment ~info e0 args : t = - {expression_desc = Call (e0, args, info); comment} + {expression_desc = Call (e0, args, info); comment; source_loc = None} (* TODO: optimization when es is known at compile time to be an array *) let flat_call ?comment e0 es : t = - {expression_desc = FlatCall (e0, es); comment} + {expression_desc = FlatCall (e0, es); comment; source_loc = None} let tagged_template ?comment call_expr string_args value_args : t = { expression_desc = Tagged_template (call_expr, string_args, value_args); comment; + source_loc = None; } let runtime_var_dot ?comment (x : string) (e1 : string) : J.expression = @@ -91,6 +97,7 @@ let runtime_var_dot ?comment (x : string) (e1 : string) : J.expression = }, Some e1 )); comment; + source_loc = None; } let ml_var_dot ?comment ?(dynamic_import = false) (id : Ident.t) e : @@ -98,6 +105,7 @@ let ml_var_dot ?comment ?(dynamic_import = false) (id : Ident.t) e : { expression_desc = Var (Qualified ({id; kind = Ml; dynamic_import}, Some e)); comment; + source_loc = None; } (** @@ -119,6 +127,7 @@ let external_var_field ?import_attributes ?comment ~external_name:name }, Some field )); comment; + source_loc = None; } let external_var ?import_attributes ?comment ~external_name (id : Ident.t) : t = @@ -135,12 +144,14 @@ let external_var ?import_attributes ?comment ~external_name (id : Ident.t) : t = }, None )); comment; + source_loc = None; } let ml_module_as_var ?comment ?(dynamic_import = false) (id : Ident.t) : t = { expression_desc = Var (Qualified ({id; kind = Ml; dynamic_import}, None)); comment; + source_loc = None; } (* Static_index .....................**) @@ -157,28 +168,38 @@ let pure_runtime_call module_name fn_name args = let runtime_ref module_name fn_name = runtime_var_dot module_name fn_name let str ?(delim = J.DNone) ?comment txt : t = - {expression_desc = Str {txt; delim}; comment} + {expression_desc = Str {txt; delim}; comment; source_loc = None} let raw_js_code ?comment info s : t = { expression_desc = Raw_js_code {code = String.trim s; code_info = info}; comment; + source_loc = None; } -let array ?comment mt es : t = {expression_desc = Array (es, mt); comment} +let array ?comment mt es : t = + {expression_desc = Array (es, mt); comment; source_loc = None} let some_comment = None let optional_block e : J.expression = - {expression_desc = Optional_block (e, false); comment = some_comment} + { + expression_desc = Optional_block (e, false); + comment = some_comment; + source_loc = None; + } let optional_not_nest_block e : J.expression = - {expression_desc = Optional_block (e, true); comment = None} + { + expression_desc = Optional_block (e, true); + comment = None; + source_loc = None; + } (** used in normal property like [e.length], no dependency introduced *) let dot ?comment (e0 : t) (e1 : string) : t = - {expression_desc = Static_index (e0, e1, None); comment} + {expression_desc = Static_index (e0, e1, None); comment; source_loc = None} let module_access (e : t) (name : string) (pos : int32) = let name = Ext_ident.convert name in @@ -187,12 +208,25 @@ let module_access (e : t) (name : string) (pos : int32) = match Ext_list.nth_opt l (Int32.to_int pos) with | Some x -> x | None -> - {expression_desc = Static_index (e, name, Some pos); comment = None}) - | _ -> {expression_desc = Static_index (e, name, Some pos); comment = None} + { + expression_desc = Static_index (e, name, Some pos); + comment = None; + source_loc = None; + }) + | _ -> + { + expression_desc = Static_index (e, name, Some pos); + comment = None; + source_loc = None; + } let make_block ?comment (tag : t) (tag_info : J.tag_info) (es : t list) (mutable_flag : J.mutable_flag) : t = - {expression_desc = Caml_block (es, mutable_flag, tag, tag_info); comment} + { + expression_desc = Caml_block (es, mutable_flag, tag, tag_info); + comment; + source_loc = None; + } module L = Literals @@ -203,18 +237,28 @@ let typeof ?comment (e : t) : t = | Str _ -> str ?comment L.js_type_string | Array _ -> str ?comment L.js_type_object | Bool _ -> str ?comment L.js_type_boolean - | _ -> {expression_desc = Typeof e; comment} + | _ -> {expression_desc = Typeof e; comment; source_loc = None} let instanceof ?comment (e0 : t) (e1 : t) : t = - {expression_desc = Bin (InstanceOf, e0, e1); comment} + {expression_desc = Bin (InstanceOf, e0, e1); comment; source_loc = None} let is_array (e0 : t) : t = let f = str "Array.isArray" ~delim:DNoQuotes in - {expression_desc = Call (f, [e0], Js_call_info.ml_full_call); comment = None} + { + expression_desc = Call (f, [e0], Js_call_info.ml_full_call); + comment = None; + source_loc = None; + } -let new_ ?comment e0 args : t = {expression_desc = New (e0, Some args); comment} +let new_ ?comment e0 args : t = + {expression_desc = New (e0, Some args); comment; source_loc = None} -let unit : t = {expression_desc = Undefined {is_unit = true}; comment = None} +let unit : t = + { + expression_desc = Undefined {is_unit = true}; + comment = None; + source_loc = None; + } (* let math ?comment v args : t = {comment ; expression_desc = Math(v,args)} *) @@ -252,6 +296,7 @@ let ocaml_fun ?comment ?immutable_mask ?directive ~return_unit ~async directive; }; comment; + source_loc = None; } let method_ ?comment ?immutable_mask ~async ~return_unit params body : t = @@ -269,6 +314,7 @@ let method_ ?comment ?immutable_mask ~async ~return_unit params body : t = directive = None; }; comment; + source_loc = None; } (** ATTENTION: This is coupuled with {!Caml_obj.caml_update_dummy} *) @@ -280,9 +326,9 @@ let dummy_obj ?comment (info : Lam_tag_info.t) : t = match info with | Blk_record _ | Blk_module _ | Blk_constructor _ | Blk_record_inlined _ | Blk_poly_var _ | Blk_extension | Blk_record_ext _ -> - {comment; expression_desc = Object (None, [])} + {comment; source_loc = None; expression_desc = Object (None, [])} | Blk_tuple | Blk_module_export _ -> - {comment; expression_desc = Array ([], Mutable)} + {comment; source_loc = None; expression_desc = Array ([], Mutable)} | Blk_some | Blk_some_not_nested -> assert false (* TODO: complete @@ -301,52 +347,98 @@ let rec seq ?comment (e0 : t) (e1 : t) : t = (* Return value could not be changed*) seq ?comment (seq e0 a) v | (Number _ | Var _ | Undefined _), _ -> e1 - | _ -> {expression_desc = Seq (e0, e1); comment} + | _ -> {expression_desc = Seq (e0, e1); comment; source_loc = None} let fuse_to_seq x xs = if xs = [] then x else Ext_list.fold_left xs x seq (* let empty_string_literal : t = - {expression_desc = Str (true,""); comment = None} *) + {expression_desc = Str (true,""); comment = None; source_loc = None} *) let zero_int_literal : t = - {expression_desc = Number (Int {i = 0l; c = None}); comment = None} + { + expression_desc = Number (Int {i = 0l; c = None}); + comment = None; + source_loc = None; + } let one_int_literal : t = - {expression_desc = Number (Int {i = 1l; c = None}); comment = None} + { + expression_desc = Number (Int {i = 1l; c = None}); + comment = None; + source_loc = None; + } let two_int_literal : t = - {expression_desc = Number (Int {i = 2l; c = None}); comment = None} + { + expression_desc = Number (Int {i = 2l; c = None}); + comment = None; + source_loc = None; + } let three_int_literal : t = - {expression_desc = Number (Int {i = 3l; c = None}); comment = None} + { + expression_desc = Number (Int {i = 3l; c = None}); + comment = None; + source_loc = None; + } let four_int_literal : t = - {expression_desc = Number (Int {i = 4l; c = None}); comment = None} + { + expression_desc = Number (Int {i = 4l; c = None}); + comment = None; + source_loc = None; + } let five_int_literal : t = - {expression_desc = Number (Int {i = 5l; c = None}); comment = None} + { + expression_desc = Number (Int {i = 5l; c = None}); + comment = None; + source_loc = None; + } let six_int_literal : t = - {expression_desc = Number (Int {i = 6l; c = None}); comment = None} + { + expression_desc = Number (Int {i = 6l; c = None}); + comment = None; + source_loc = None; + } let seven_int_literal : t = - {expression_desc = Number (Int {i = 7l; c = None}); comment = None} + { + expression_desc = Number (Int {i = 7l; c = None}); + comment = None; + source_loc = None; + } let eight_int_literal : t = - {expression_desc = Number (Int {i = 8l; c = None}); comment = None} + { + expression_desc = Number (Int {i = 8l; c = None}); + comment = None; + source_loc = None; + } let nine_int_literal : t = - {expression_desc = Number (Int {i = 9l; c = None}); comment = None} + { + expression_desc = Number (Int {i = 9l; c = None}); + comment = None; + source_loc = None; + } -let int ?comment ?c i : t = {expression_desc = Number (Int {i; c}); comment} +let int ?comment ?c i : t = + {expression_desc = Number (Int {i; c}); comment; source_loc = None} let bigint ?comment sign i : t = - {expression_desc = Number (BigInt {positive = sign; value = i}); comment} + { + expression_desc = Number (BigInt {positive = sign; value = i}); + comment; + source_loc = None; + } let zero_bigint_literal : t = { expression_desc = Number (BigInt {positive = true; value = "0"}); comment = None; + source_loc = None; } let small_int i : t = @@ -363,17 +455,23 @@ let small_int i : t = | 9 -> nine_int_literal | i -> int (Int32.of_int i) -let true_ : t = {comment = None; expression_desc = Bool true} -let false_ : t = {comment = None; expression_desc = Bool false} +let true_ : t = {comment = None; source_loc = None; expression_desc = Bool true} +let false_ : t = + {comment = None; source_loc = None; expression_desc = Bool false} let bool v = if v then true_ else false_ -let float ?comment f : t = {expression_desc = Number (Float {f}); comment} +let float ?comment f : t = + {expression_desc = Number (Float {f}); comment; source_loc = None} let zero_float_lit : t = - {expression_desc = Number (Float {f = "0."}); comment = None} + { + expression_desc = Number (Float {f = "0."}); + comment = None; + source_loc = None; + } let float_mod ?comment e1 e2 : J.expression = - {comment; expression_desc = Bin (Mod, e1, e2)} + {comment; source_loc = None; expression_desc = Bin (Mod, e1, e2)} let array_index ?comment (e0 : t) (e1 : t) : t = match (e0.expression_desc, e1.expression_desc) with @@ -381,9 +479,10 @@ let array_index ?comment (e0 : t) (e1 : t) : t = (* Float i -- should not appear here *) when no_side_effect e0 -> ( match Ext_list.nth_opt l (Int32.to_int i) with - | None -> {expression_desc = Array_index (e0, e1); comment} + | None -> + {expression_desc = Array_index (e0, e1); comment; source_loc = None} | Some x -> x (* FIX #3084*)) - | _ -> {expression_desc = Array_index (e0, e1); comment} + | _ -> {expression_desc = Array_index (e0, e1); comment; source_loc = None} let array_index_by_int ?comment (e : t) (pos : int32) : t = match e.expression_desc with @@ -393,8 +492,17 @@ let array_index_by_int ?comment (e : t) (pos : int32) : t = match Ext_list.nth_opt l (Int32.to_int pos) with | Some x -> x | None -> - {expression_desc = Array_index (e, int ?comment pos); comment = None}) - | _ -> {expression_desc = Array_index (e, int ?comment pos); comment = None} + { + expression_desc = Array_index (e, int ?comment pos); + comment = None; + source_loc = None; + }) + | _ -> + { + expression_desc = Array_index (e, int ?comment pos); + comment = None; + source_loc = None; + } let record_access (e : t) (name : string) (pos : int32) = (* let name = Ext_ident.convert name in *) @@ -405,8 +513,17 @@ let record_access (e : t) (name : string) (pos : int32) = match Ext_list.nth_opt l (Int32.to_int pos) with | Some x -> x | None -> - {expression_desc = Static_index (e, name, Some pos); comment = None}) - | _ -> {expression_desc = Static_index (e, name, Some pos); comment = None} + { + expression_desc = Static_index (e, name, Some pos); + comment = None; + source_loc = None; + }) + | _ -> + { + expression_desc = Static_index (e, name, Some pos); + comment = None; + source_loc = None; + } (* The same as {!record_access} except tag*) let inline_record_access = record_access @@ -432,6 +549,7 @@ let poly_var_tag_access (e : t) = { expression_desc = Static_index (e, Literals.polyvar_hash, Some 0l); comment = None; + source_loc = None; } let poly_var_value_access (e : t) = @@ -444,6 +562,7 @@ let poly_var_value_access (e : t) = { expression_desc = Static_index (e, Literals.polyvar_value, Some 1l); comment = None; + source_loc = None; } let extension_access (e : t) name (pos : int32) : t = @@ -459,14 +578,22 @@ let extension_access (e : t) name (pos : int32) : t = | Some n -> n | None -> "_" ^ Int32.to_string pos in - {expression_desc = Static_index (e, name, Some pos); comment = None}) + { + expression_desc = Static_index (e, name, Some pos); + comment = None; + source_loc = None; + }) | _ -> let name = match name with | Some n -> n | None -> "_" ^ Int32.to_string pos in - {expression_desc = Static_index (e, name, Some pos); comment = None} + { + expression_desc = Static_index (e, name, Some pos); + comment = None; + source_loc = None; + } let string_index ?comment (e0 : t) (e1 : t) : t = match (e0.expression_desc, e1.expression_desc) with @@ -478,10 +605,11 @@ let string_index ?comment (e0 : t) (e1 : t) : t = RangeError? *) str (String.make 1 txt.[i]) - else {expression_desc = String_index (e0, e1); comment} - | _ -> {expression_desc = String_index (e0, e1); comment} + else {expression_desc = String_index (e0, e1); comment; source_loc = None} + | _ -> {expression_desc = String_index (e0, e1); comment; source_loc = None} -let assign ?comment e0 e1 : t = {expression_desc = Bin (Eq, e0, e1); comment} +let assign ?comment e0 e1 : t = + {expression_desc = Bin (Eq, e0, e1); comment; source_loc = None} let assign_by_exp (e : t) index value : t = match e.expression_desc with @@ -496,7 +624,14 @@ let assign_by_exp (e : t) index value : t = | Caml_block _ when no_side_effect e && no_side_effect index -> value - | _ -> assign {expression_desc = Array_index (e, index); comment = None} value + | _ -> + assign + { + expression_desc = Array_index (e, index); + comment = None; + source_loc = None; + } + value let assign_by_int ?comment e0 (index : int32) value = assign_by_exp e0 (int ?comment index) value @@ -516,7 +651,11 @@ let record_assign (e : t) (pos : int32) (name : string) (value : t) = value | _ -> assign - {expression_desc = Static_index (e, name, Some pos); comment = None} + { + expression_desc = Static_index (e, name, Some pos); + comment = None; + source_loc = None; + } value let extension_assign (e : t) (pos : int32) name (value : t) = @@ -534,7 +673,11 @@ let extension_assign (e : t) (pos : int32) name (value : t) = value | _ -> assign - {expression_desc = Static_index (e, name, Some pos); comment = None} + { + expression_desc = Static_index (e, name, Some pos); + comment = None; + source_loc = None; + } value (* This is a property access not external module *) @@ -544,13 +687,13 @@ let array_length ?comment (e : t) : t = (* TODO: use array instead? *) | (Array (l, _) | Caml_block (l, _, _, _)) when no_side_effect e -> int ?comment (Int32.of_int (List.length l)) - | _ -> {expression_desc = Length (e, Array); comment} + | _ -> {expression_desc = Length (e, Array); comment; source_loc = None} let string_length ?comment (e : t) : t = match e.expression_desc with | Str {txt; delim = DNone} -> int ?comment (Int32.of_int (String.length txt)) (* No optimization for {j||j}*) - | _ -> {expression_desc = Length (e, String); comment} + | _ -> {expression_desc = Length (e, String); comment; source_loc = None} let function_length ?comment (e : t) : t = match e.expression_desc with @@ -558,11 +701,11 @@ let function_length ?comment (e : t) : t = let params_length = List.length params in int ?comment (Int32.of_int (if is_method then params_length - 1 else params_length)) - | _ -> {expression_desc = Length (e, Function); comment} + | _ -> {expression_desc = Length (e, Function); comment; source_loc = None} (** no dependency introduced *) (* let js_global_dot ?comment (x : string) (e1 : string) : t = - { expression_desc = Static_index (js_global x, e1,None); comment} + { expression_desc = Static_index (js_global x, e1,None); comment; source_loc = None} *) let rec string_append ?comment (e : t) (el : t) : t = @@ -583,11 +726,12 @@ let rec string_append ?comment (e : t) (el : t) : t = when delim = delim_ -> string_append ?comment (string_append a (concat b c ~delim)) d | Str {txt = a; delim}, Str {txt = b; delim = delim_} when delim = delim_ -> - {(concat a b ~delim) with comment} - | _, _ -> {comment; expression_desc = String_append (e, el)} + {(concat a b ~delim) with comment; source_loc = None} + | _, _ -> + {comment; source_loc = None; expression_desc = String_append (e, el)} let obj ?comment ?dup properties : t = - {expression_desc = Object (dup, properties); comment} + {expression_desc = Object (dup, properties); comment; source_loc = None} let str_equal (txt0 : string) (delim0 : External_arg_spec.delim) txt1 delim1 = if delim0 = delim1 then @@ -618,7 +762,7 @@ let rec triple_equal ?comment (e0 : t) (e1 : t) : t = when no_side_effect e0 && no_side_effect e1 -> false_ | Null, Null | Undefined _, Undefined _ -> true_ - | _ -> {expression_desc = Bin (EqEqEq, e0, e1); comment} + | _ -> {expression_desc = Bin (EqEqEq, e0, e1); comment; source_loc = None} let bin ?comment (op : J.binop) (e0 : t) (e1 : t) : t = match (op, e0.expression_desc, e1.expression_desc) with @@ -627,8 +771,8 @@ let bin ?comment (op : J.binop) (e0 : t) (e1 : t) : t = true_ (* x.length >=0 | [x] is pure -> true*) | Gt, Length (_, _), Number (Int {i = 0l}) -> (* [e] is kept so no side effect check needed *) - {expression_desc = Bin (NotEqEq, e0, e1); comment} - | _ -> {expression_desc = Bin (op, e0, e1); comment} + {expression_desc = Bin (NotEqEq, e0, e1); comment; source_loc = None} + | _ -> {expression_desc = Bin (op, e0, e1); comment; source_loc = None} (* TODO: Constant folding, Google Closure will do that?, Even if Google Clsoure can do that, we will see how it interact with other @@ -643,7 +787,7 @@ let bin ?comment (op : J.binop) (e0 : t) (e1 : t) : t = and_ ?comment { e1 with expression_desc = - J.Int_of_boolean { expression_desc = Bin (And, e10,e20); comment = None} + J.Int_of_boolean { expression_desc = Bin (And, e10,e20); comment = None; source_loc = None} } e3 Note that @@ -756,16 +900,30 @@ let rec simplify_and_ ~n (e1 : t) (e2 : t) : t option = | Some a_, Some b_ -> simplify_and_force ~n:(n + 1) a_ b_) | _, Bin (And, a, b) -> simplify_and_ ~n:(n + 1) - {expression_desc = Bin (And, e1, a); comment = None} + { + expression_desc = Bin (And, e1, a); + comment = None; + source_loc = None; + } b | Bin (Or, a, b), _ -> ( let ao = simplify_and_ ~n:(n + 1) a e2 in let bo = simplify_and_ ~n:(n + 1) b e2 in match (ao, bo) with | Some {expression_desc = Bool false}, None -> - Some {expression_desc = Bin (And, b, e2); comment = None} + Some + { + expression_desc = Bin (And, b, e2); + comment = None; + source_loc = None; + } | None, Some {expression_desc = Bool false} -> - Some {expression_desc = Bin (And, a, e2); comment = None} + Some + { + expression_desc = Bin (And, a, e2); + comment = None; + source_loc = None; + } | None, _ | _, None -> ( match simplify_or_ ~n:(n + 1) a b with | None -> None @@ -796,7 +954,7 @@ let rec simplify_and_ ~n (e1 : t) (e2 : t) : t option = {expression_desc = Typeof {expression_desc = Var ia}}, {expression_desc = Str {txt = "boolean"}} ) ) when Js_op_util.same_vident ia ib -> - Some {expression_desc = b; comment = None} + Some {expression_desc = b; comment = None; source_loc = None} | ( Bin ( EqEqEq, {expression_desc = Typeof {expression_desc = Var ia}}, @@ -810,7 +968,7 @@ let rec simplify_and_ ~n (e1 : t) (e2 : t) : t option = {expression_desc = Typeof {expression_desc = Var ia}}, {expression_desc = Str {txt = "string"}} ) ) when Js_op_util.same_vident ia ib -> - Some {expression_desc = s; comment = None} + Some {expression_desc = s; comment = None; source_loc = None} | ( Bin ( EqEqEq, {expression_desc = Typeof {expression_desc = Var ia}}, @@ -824,7 +982,7 @@ let rec simplify_and_ ~n (e1 : t) (e2 : t) : t option = {expression_desc = Typeof {expression_desc = Var ia}}, {expression_desc = Str {txt = "number"}} ) ) when Js_op_util.same_vident ia ib -> - Some {expression_desc = i; comment = None} + Some {expression_desc = i; comment = None; source_loc = None} | ( Bin ( EqEqEq, {expression_desc = Typeof {expression_desc = Var ia}}, @@ -881,8 +1039,12 @@ let rec simplify_and_ ~n (e1 : t) (e2 : t) : t option = Some { expression_desc = - Bin (EqEqEq, {expression_desc = Var ib; comment = None}, true_); + Bin + ( EqEqEq, + {expression_desc = Var ib; comment = None; source_loc = None}, + true_ ); comment = None; + source_loc = None; } | ( Bin ( EqEqEq, @@ -898,8 +1060,12 @@ let rec simplify_and_ ~n (e1 : t) (e2 : t) : t option = Some { expression_desc = - Bin (EqEqEq, {expression_desc = Var ib; comment = None}, false_); + Bin + ( EqEqEq, + {expression_desc = Var ib; comment = None; source_loc = None}, + false_ ); comment = None; + source_loc = None; } | ( Bin ( EqEqEq, @@ -913,7 +1079,7 @@ let rec simplify_and_ ~n (e1 : t) (e2 : t) : t option = {expression_desc = Typeof {expression_desc = Var ia}}, {expression_desc = Str {txt = "boolean"}} ) ) when Js_op_util.same_vident ia ib -> - Some {expression_desc = Bool (not b); comment = None} + Some {expression_desc = Bool (not b); comment = None; source_loc = None} | ( Bin ( EqEqEq, {expression_desc = Typeof {expression_desc = Var ia}}, @@ -976,7 +1142,7 @@ let rec simplify_and_ ~n (e1 : t) (e2 : t) : t option = } ) as typeof) ) when Js_op_util.same_vident ia ib -> (* Note: cases boolean / Bool _, number / Number _, string / Str _, object / Null are handled above *) - Some {expression_desc = typeof; comment = None} + Some {expression_desc = typeof; comment = None; source_loc = None} | ( (Call ( {expression_desc = Str {txt = "Array.isArray"}}, [{expression_desc = Var ia}], @@ -996,7 +1162,7 @@ let rec simplify_and_ ~n (e1 : t) (e2 : t) : t option = [{expression_desc = Var ia}], _ ) as is_array) ) when Js_op_util.same_vident ia ib -> - Some {expression_desc = is_array; comment = None} + Some {expression_desc = is_array; comment = None; source_loc = None} | _ when Js_analyzer.eq_expression e1 e2 -> Some e1 | ( Bin ( EqEqEq, @@ -1040,7 +1206,9 @@ let rec simplify_and_ ~n (e1 : t) (e2 : t) : t option = and simplify_and_force ~n (e1 : t) (e2 : t) : t option = match simplify_and_ ~n e1 e2 with - | None -> Some {expression_desc = Bin (And, e1, e2); comment = None} + | None -> + Some + {expression_desc = Bin (And, e1, e2); comment = None; source_loc = None} | x -> x (** @@ -1094,7 +1262,8 @@ and simplify_or_ ~n (e1 : t) (e2 : t) : t option = and simplify_or_force ~n (e1 : t) (e2 : t) : t option = match simplify_or_ ~n e1 e2 with - | None -> Some {expression_desc = Bin (Or, e1, e2); comment = None} + | None -> + Some {expression_desc = Bin (Or, e1, e2); comment = None; source_loc = None} | x -> x let simplify_and (e1 : t) (e2 : t) : t option = @@ -1123,7 +1292,7 @@ let and_ ?comment (e1 : t) (e2 : t) : t = | _, _ -> ( match simplify_and e1 e2 with | Some e -> e - | None -> {expression_desc = Bin (And, e1, e2); comment}) + | None -> {expression_desc = Bin (And, e1, e2); comment; source_loc = None}) let or_ ?comment (e1 : t) (e2 : t) = match (e1.expression_desc, e2.expression_desc) with @@ -1137,10 +1306,10 @@ let or_ ?comment (e1 : t) (e2 : t) = | _, _ -> ( match simplify_or e1 e2 with | Some e -> e - | None -> {expression_desc = Bin (Or, e1, e2); comment}) + | None -> {expression_desc = Bin (Or, e1, e2); comment; source_loc = None}) let in_ (prop : t) (obj : t) : t = - {expression_desc = In (prop, obj); comment = None} + {expression_desc = In (prop, obj); comment = None; source_loc = None} let not (e : t) : t = match e.expression_desc with @@ -1156,7 +1325,7 @@ let not (e : t) : t = | _ -> ( match push_negation e with | Some e_ -> e_ - | None -> {expression_desc = Js_not e; comment = None}) + | None -> {expression_desc = Js_not e; comment = None; source_loc = None}) let not_empty_branch (x : t) = match x.expression_desc with @@ -1167,7 +1336,8 @@ let rec econd ?comment (pred : t) (ifso : t) (ifnot : t) : t = let default () = if Js_analyzer.eq_expression ifso ifnot then if no_side_effect pred then ifso else seq ?comment pred ifso - else {expression_desc = Cond (pred, ifso, ifnot); comment} + else + {expression_desc = Cond (pred, ifso, ifnot); comment; source_loc = None} in match (pred.expression_desc, ifso.expression_desc, ifnot.expression_desc) with | Bool false, _, _ -> ifnot @@ -1248,7 +1418,7 @@ let rec float_equal ?comment (e0 : t) (e1 : t) : t = *) float_equal ?comment a e1 | Number (Float {f = f0; _}), Number (Float {f = f1}) when f0 = f1 -> true_ - | _ -> {expression_desc = Bin (EqEqEq, e0, e1); comment} + | _ -> {expression_desc = Bin (EqEqEq, e0, e1); comment; source_loc = None} let int_equal = float_equal @@ -1315,7 +1485,7 @@ let is_int_tag ?has_null_undefined_other e = *) let tag ?comment ?(name = Js_dump_lit.tag) e : t = - {expression_desc = Caml_block_tag (e, name); comment} + {expression_desc = Caml_block_tag (e, name); comment; source_loc = None} (* according to the compiler, [Btype.hash_variant], it's reduced to 31 bits for hash @@ -1346,7 +1516,7 @@ let rec int32_bor ?comment (e1 : J.expression) (e2 : J.expression) : | ( Bin (Bor, e1, {expression_desc = Number (Int {i = 0l}); _}), Number (Int {i = 0l}) ) -> int32_bor e1 e2 - | _ -> {comment; expression_desc = Bin (Bor, e1, e2)} + | _ -> {comment; source_loc = None; expression_desc = Bin (Bor, e1, e2)} let to_int32 ?comment (e : J.expression) : J.expression = int32_bor ?comment e zero_int_literal @@ -1372,7 +1542,8 @@ let is_type_string ?comment (e : t) : t = let is_type_object (e : t) : t = string_equal (typeof e) (str "object") let obj_length ?comment e : t = - to_int32 {expression_desc = Length (e, Caml_block); comment} + to_int32 + {expression_desc = Length (e, Caml_block); comment; source_loc = None} let compare_int_aux (cmp : Lam_compat.comparison) (l : int) r = match cmp with @@ -1454,7 +1625,7 @@ let rec int32_lsr ?comment (e1 : J.expression) (e2 : J.expression) : | ( Bin (Bor, e1, {expression_desc = Number (Int {i = 0l; _}); _}), Number (Int {i = 0l}) ) -> int32_lsr ?comment e1 e2 - | _, _ -> {comment; expression_desc = Bin (Lsr, e1, e2)} + | _, _ -> {comment; source_loc = None; expression_desc = Bin (Lsr, e1, e2)} (* TODO: we can apply a more general optimization here, @@ -1522,7 +1693,11 @@ let rec float_add ?comment (e1 : t) (e2 : t) = {e2 with expression_desc = Number (Int {i = Int32.neg j; c})} | ( Bin (Plus, a1, {expression_desc = Number (Int {i = k; _})}), Number (Int {i = j; _}) ) -> - {comment; expression_desc = Bin (Plus, a1, int (Int32.add k j))} + { + comment; + source_loc = None; + expression_desc = Bin (Plus, a1, int (Int32.add k j)); + } (* bin ?comment Plus a1 (int (k + j)) *) (* TODO remove commented code ?? *) (* | Bin(Plus, a0 , ({expression_desc = Number (Int a1)} )), *) @@ -1540,14 +1715,14 @@ let rec float_add ?comment (e1 : t) (e2 : t) = (* | Number _, _ *) (* -> *) (* bin ?comment Plus e2 e1 *) - | _ -> {comment; expression_desc = Bin (Plus, e1, e2)} + | _ -> {comment; source_loc = None; expression_desc = Bin (Plus, e1, e2)} (* bin ?comment Plus e1 e2 *) (* associative is error prone due to overflow *) and float_minus ?comment (e1 : t) (e2 : t) : t = match (e1.expression_desc, e2.expression_desc) with | Number (Int {i; _}), Number (Int {i = j; _}) -> int ?comment (Int32.sub i j) - | _ -> {comment; expression_desc = Bin (Minus, e1, e2)} + | _ -> {comment; source_loc = None; expression_desc = Bin (Minus, e1, e2)} (* bin ?comment Minus e1 e2 *) let unchecked_int32_add ?comment e1 e2 = float_add ?comment e1 e2 @@ -1567,7 +1742,7 @@ let float_pow ?comment e1 e2 = bin ?comment Pow e1 e2 let float_notequal ?comment e1 e2 = bin ?comment NotEqEq e1 e2 let int32_asr ?comment e1 e2 : J.expression = - {comment; expression_desc = Bin (Asr, e1, e2)} + {comment; source_loc = None; expression_desc = Bin (Asr, e1, e2)} (** Division by zero is undefined behavior*) let int32_div ~checked ?comment (e1 : t) (e2 : t) : t = @@ -1584,10 +1759,10 @@ let int32_div ~checked ?comment (e1 : t) (e2 : t) : t = let int32_mod ~checked ?comment e1 (e2 : t) : J.expression = match e2.expression_desc with | Number (Int {i}) when i <> 0l -> - {comment; expression_desc = Bin (Mod, e1, e2)} + {comment; source_loc = None; expression_desc = Bin (Mod, e1, e2)} | _ -> if checked then runtime_call Primitive_modules.int "mod_" [e1; e2] - else {comment; expression_desc = Bin (Mod, e1, e2)} + else {comment; source_loc = None; expression_desc = Bin (Mod, e1, e2)} let float_mul ?comment e1 e2 = bin ?comment Mul e1 e2 @@ -1596,7 +1771,7 @@ let int32_lsl ?comment (e1 : J.expression) (e2 : J.expression) : J.expression = | ( {expression_desc = Number (Int {i = i0})}, {expression_desc = Number (Int {i = i1})} ) -> int ?comment (Int32.shift_left i0 (Int32.to_int i1)) - | _ -> {comment; expression_desc = Bin (Lsl, e1, e2)} + | _ -> {comment; source_loc = None; expression_desc = Bin (Lsl, e1, e2)} let is_pos_pow n = let exception E in @@ -1625,12 +1800,12 @@ let int32_mul ?comment (e1 : J.expression) (e2 : J.expression) : J.expression = | _ -> to_int32 (float_mul ?comment e1 e2) let unchecked_int32_mul ?comment e1 e2 : J.expression = - {comment; expression_desc = Bin (Mul, e1, e2)} + {comment; source_loc = None; expression_desc = Bin (Mul, e1, e2)} let int_bnot ?comment (e : t) : J.expression = match e.expression_desc with | Number (Int {i}) -> int ?comment (Int32.lognot i) - | _ -> {comment; expression_desc = Js_bnot e} + | _ -> {comment; source_loc = None; expression_desc = Js_bnot e} let int32_pow ?comment (e1 : t) (e2 : t) : J.expression = match (e1.expression_desc, e2.expression_desc) with @@ -1646,7 +1821,7 @@ let rec int32_bxor ?comment (e1 : t) (e2 : t) : J.expression = int32_bxor e1 e2 | Bin (Lsr, e1, {expression_desc = Number (Int {i = 0l}); _}), _ -> int32_bxor e1 e2 - | _ -> {comment; expression_desc = Bin (Bxor, e1, e2)} + | _ -> {comment; source_loc = None; expression_desc = Bin (Bxor, e1, e2)} let rec int32_band ?comment (e1 : J.expression) (e2 : J.expression) : J.expression = @@ -1657,10 +1832,10 @@ let rec int32_band ?comment (e1 : J.expression) (e2 : J.expression) : {[ (-1 >>> 0 | 0 ) & 0xffffff ]} *) int32_band a e2 - | _ -> {comment; expression_desc = Bin (Band, e1, e2)} + | _ -> {comment; source_loc = None; expression_desc = Bin (Band, e1, e2)} (* let int32_bin ?comment op e1 e2 : J.expression = *) -(* {expression_desc = Int32_bin(op,e1, e2); comment} *) +(* {expression_desc = Int32_bin(op,e1, e2); comment; source_loc = None} *) let bigint_op ?comment op (e1 : t) (e2 : t) = bin ?comment op e1 e2 @@ -1708,6 +1883,7 @@ let of_block ?comment ?e block : t = call ~info:Js_call_info.ml_full_call { comment; + source_loc = None; expression_desc = Fun { @@ -1717,7 +1893,8 @@ let of_block ?comment ?e block : t = (match e with | None -> block | Some e -> - Ext_list.append block [{J.statement_desc = Return e; comment}]); + Ext_list.append block + [{J.statement_desc = Return e; comment; source_loc = None}]); env = Js_fun_env.make 0; return_unit; async = false; @@ -1738,7 +1915,7 @@ let is_null_undefined ?comment (x : t) : t = match x.expression_desc with | Null | Undefined _ -> true_ | Number _ | Array _ | Caml_block _ -> false_ - | _ -> {comment; expression_desc = Is_null_or_undefined x} + | _ -> {comment; source_loc = None; expression_desc = Is_null_or_undefined x} let eq_null_undefined_boolean ?comment (a : t) (b : t) = (* [a == b] when either a or b is null or undefined *) @@ -1753,7 +1930,7 @@ let eq_null_undefined_boolean ?comment (a : t) (b : t) = false_ | Null, Undefined _ | Undefined _, Null -> false_ | Null, Null | Undefined _, Undefined _ -> true_ - | _ -> {expression_desc = Bin (EqEqEq, a, b); comment} + | _ -> {expression_desc = Bin (EqEqEq, a, b); comment; source_loc = None} let neq_null_undefined_boolean ?comment (a : t) (b : t) = (* [a != b] when either a or b is null or undefined *) @@ -1768,7 +1945,7 @@ let neq_null_undefined_boolean ?comment (a : t) (b : t) = true_ | Null, Null | Undefined _, Undefined _ -> false_ | Null, Undefined _ | Undefined _, Null -> true_ - | _ -> {expression_desc = Bin (NotEqEq, a, b); comment} + | _ -> {expression_desc = Bin (NotEqEq, a, b); comment; source_loc = None} let make_exception (s : string) = pure_runtime_call Primitive_modules.exceptions Literals.create [str s] diff --git a/compiler/core/js_fold.ml b/compiler/core/js_fold.ml index e080f501196..25e2525f24f 100644 --- a/compiler/core/js_fold.ml +++ b/compiler/core/js_fold.ml @@ -198,7 +198,12 @@ class fold = _self#expression method case_clause : case_clause -> 'self_type = - fun {switch_body = _x0; should_break = _x1; comment = _x2} -> + fun { + switch_body = _x0; + should_break = _x1; + comment = _x2; + source_loc = _x3; + } -> let _self = _self#block _x0 in _self @@ -284,12 +289,12 @@ class fold = | Debugger -> _self method expression : expression -> 'self_type = - fun {expression_desc = _x0; comment = _x1} -> + fun {expression_desc = _x0; comment = _x1; source_loc = _x2} -> let _self = _self#expression_desc _x0 in _self method statement : statement -> 'self_type = - fun {statement_desc = _x0; comment = _x1} -> + fun {statement_desc = _x0; comment = _x1; source_loc = _x2} -> let _self = _self#statement_desc _x0 in _self diff --git a/compiler/core/js_implementation.ml b/compiler/core/js_implementation.ml index a6ea25a5b1c..5f4e4e6c765 100644 --- a/compiler/core/js_implementation.ml +++ b/compiler/core/js_implementation.ml @@ -141,18 +141,17 @@ let after_parsing_impl ppf outputprefix (ast : Parsetree.structure) = let typedtree_coercion = (typedtree, coercion) in print_if ppf Clflags.dump_typedtree Printtyped.implementation_with_coercion typedtree_coercion; - Js_source_map.with_marker_scope (fun () -> - if !Js_config.cmi_only then Warnings.check_fatal () - else - let lambda, exports = - 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 - in - if not !Js_config.cmj_only then - Lam_compile_main.lambda_as_module js_program outputprefix); + (if !Js_config.cmi_only then Warnings.check_fatal () + else + let lambda, exports = + 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 + in + if not !Js_config.cmj_only then + Lam_compile_main.lambda_as_module js_program outputprefix); process_with_gentype (outputprefix ^ ".cmt")) let implementation ~parser ppf ?outputprefix fname = diff --git a/compiler/core/js_of_lam_variant.ml b/compiler/core/js_of_lam_variant.ml index e8135e86336..8ae3c4b9ac4 100644 --- a/compiler/core/js_of_lam_variant.ml +++ b/compiler/core/js_of_lam_variant.ml @@ -47,6 +47,7 @@ let eval (arg : J.expression) (dispatches : (string * string) list) : E.t = should_break = false; (* FIXME: if true, still print break*) comment = None; + source_loc = None; } ))); ] @@ -87,6 +88,7 @@ let eval_as_event (arg : J.expression) should_break = false; (* FIXME: if true, still print break*) comment = None; + source_loc = None; } ))); ] | None -> E.poly_var_tag_access arg), @@ -115,6 +117,7 @@ let eval_as_int (arg : J.expression) (dispatches : (string * int) list) : E.t = should_break = false; (* FIXME: if true, still print break*) comment = None; + source_loc = None; } ))); ] diff --git a/compiler/core/js_pass_flatten.ml b/compiler/core/js_pass_flatten.ml index 1e40c4827f8..c668d2be1b8 100644 --- a/compiler/core/js_pass_flatten.ml +++ b/compiler/core/js_pass_flatten.ml @@ -51,7 +51,7 @@ let flatten_map = } -> S.block (Ext_list.map args (fun arg -> self.statement self (S.exp arg))) - | Exp {expression_desc = Cond (a, b, c); comment} -> + | Exp {expression_desc = Cond (a, b, c); comment; source_loc} -> { statement_desc = If @@ -59,6 +59,7 @@ let flatten_map = [self.statement self (S.exp b)], [self.statement self (S.exp c)] ); comment; + source_loc; } | Exp { @@ -76,7 +77,7 @@ let flatten_map = (* super#statement *) (* (S.block (List.rev_append rest_rev [S.exp (E.assign a last_one)])) *) | _ -> assert false) - | Return {expression_desc = Cond (a, b, c); comment} -> + | Return {expression_desc = Cond (a, b, c); comment; source_loc} -> { statement_desc = If @@ -84,6 +85,7 @@ let flatten_map = [self.statement self (S.return_stmt b)], [self.statement self (S.return_stmt c)] ); comment; + source_loc; } | Return ({expression_desc = Seq _; _} as v) -> ( let block = Js_analyzer.rev_flatten_seq v in diff --git a/compiler/core/js_record_fold.ml b/compiler/core/js_record_fold.ml index d3e0de74358..491525443d5 100644 --- a/compiler/core/js_record_fold.ml +++ b/compiler/core/js_record_fold.ml @@ -204,7 +204,8 @@ let finish_ident_expression : 'a. ('a, finish_ident_expression) fn = fun _self arg -> _self.expression _self arg let case_clause : 'a. ('a, case_clause) fn = - fun _self st {switch_body = _x0; should_break = _x1; comment = _x2} -> + fun _self st + {switch_body = _x0; should_break = _x1; comment = _x2; source_loc = _x3} -> let st = _self.block _self st _x0 in st @@ -288,12 +289,12 @@ let statement_desc : 'a. ('a, statement_desc) fn = | Debugger -> st let expression : 'a. ('a, expression) fn = - fun _self st {expression_desc = _x0; comment = _x1} -> + fun _self st {expression_desc = _x0; comment = _x1; source_loc = _x2} -> let st = expression_desc _self st _x0 in st let statement : 'a. ('a, statement) fn = - fun _self st {statement_desc = _x0; comment = _x1} -> + fun _self st {statement_desc = _x0; comment = _x1; source_loc = _x2} -> let st = statement_desc _self st _x0 in st diff --git a/compiler/core/js_record_iter.ml b/compiler/core/js_record_iter.ml index da86618ae3c..f30664e546c 100644 --- a/compiler/core/js_record_iter.ml +++ b/compiler/core/js_record_iter.ml @@ -153,7 +153,8 @@ let finish_ident_expression : finish_ident_expression fn = fun _self arg -> _self.expression _self arg let case_clause : case_clause fn = - fun _self {switch_body = _x0; should_break = _x1; comment = _x2} -> + fun _self + {switch_body = _x0; should_break = _x1; comment = _x2; source_loc = _x3} -> _self.block _self _x0 let string_clause : string_clause fn = @@ -210,10 +211,12 @@ let statement_desc : statement_desc fn = | Debugger -> () let expression : expression fn = - fun _self {expression_desc = _x0; comment = _x1} -> expression_desc _self _x0 + fun _self {expression_desc = _x0; comment = _x1; source_loc = _x2} -> + expression_desc _self _x0 let statement : statement fn = - fun _self {statement_desc = _x0; comment = _x1} -> statement_desc _self _x0 + fun _self {statement_desc = _x0; comment = _x1; source_loc = _x2} -> + statement_desc _self _x0 let variable_declaration : variable_declaration fn = fun _self {ident = _x0; value = _x1; property = _x2; ident_info = _x3} -> diff --git a/compiler/core/js_record_map.ml b/compiler/core/js_record_map.ml index 26551861718..4af2014514a 100644 --- a/compiler/core/js_record_map.ml +++ b/compiler/core/js_record_map.ml @@ -202,9 +202,10 @@ let finish_ident_expression : finish_ident_expression fn = fun _self arg -> _self.expression _self arg let case_clause : case_clause fn = - fun _self {switch_body = _x0; should_break = _x1; comment = _x2} -> + fun _self + {switch_body = _x0; should_break = _x1; comment = _x2; source_loc = _x3} -> let _x0 = _self.block _self _x0 in - {switch_body = _x0; should_break = _x1; comment = _x2} + {switch_body = _x0; should_break = _x1; comment = _x2; source_loc = _x3} let string_clause : string_clause fn = fun _self (_x0, _x1) -> @@ -286,14 +287,14 @@ let statement_desc : statement_desc fn = | Debugger as v -> v let expression : expression fn = - fun _self {expression_desc = _x0; comment = _x1} -> + fun _self {expression_desc = _x0; comment = _x1; source_loc = _x2} -> let _x0 = expression_desc _self _x0 in - {expression_desc = _x0; comment = _x1} + {expression_desc = _x0; comment = _x1; source_loc = _x2} let statement : statement fn = - fun _self {statement_desc = _x0; comment = _x1} -> + fun _self {statement_desc = _x0; comment = _x1; source_loc = _x2} -> let _x0 = statement_desc _self _x0 in - {statement_desc = _x0; comment = _x1} + {statement_desc = _x0; comment = _x1; source_loc = _x2} let variable_declaration : variable_declaration fn = fun _self {ident = _x0; value = _x1; property = _x2; ident_info = _x3} -> diff --git a/compiler/core/js_source_map.ml b/compiler/core/js_source_map.ml index 6a7cf7f77fd..70072c2cc94 100644 --- a/compiler/core/js_source_map.ml +++ b/compiler/core/js_source_map.ml @@ -4,15 +4,14 @@ Source map generation is tied to the JS printer: - 1. Lambda-to-JS conversion attaches internal marker comments to JS nodes, - because it does not know the final generated line/column yet. + 1. Lambda-to-JS conversion attaches source locations to JS IR nodes, because + it does not know the final generated line/column yet. 2. A source map builder is installed while one generated JS file is printed. It tracks sources, optional source contents, and mapping entries for that output file. - 3. The JS printer updates its generated line/column as it writes text. When - it sees one of the internal marker comments, it suppresses the comment from - output and records the current generated position against the original - ReScript location. + 3. The JS printer updates its generated line/column as it writes text. Right + before it prints a node with a source location, it records the current + generated position against the original ReScript location. 4. After printing, the collected mappings are sorted and encoded into the compact Source Map v3 "mappings" field using base64 VLQ. The JSON map is emitted next to the generated JavaScript and the JS file receives a @@ -24,8 +23,8 @@ map. A compiled JS program can be printed more than once for multiple package - targets, such as CommonJS and ESM. Marker locations therefore remain available - for every print pass and are cleaned up when the compilation unit finishes. *) + targets, such as CommonJS and ESM. Source locations live on the JS IR itself, + so every print pass can emit an independent map without a side table. *) type source = {relative_path: string; content: string option} @@ -50,39 +49,11 @@ type t = { let current : t option ref = ref None -let marker_prefix = "\000RESCRIPT_SOURCE_MAP:" -let next_marker = ref 0 -let marker_locs : (int, Location.t) Hashtbl.t = Hashtbl.create 128 - -let is_prefix ~prefix s = - let prefix_len = String.length prefix in - String.length s >= prefix_len - && - let rec loop i = - i = prefix_len - || (String.unsafe_get s i = String.unsafe_get prefix i && loop (i + 1)) - in - loop 0 - -let comment_of_loc (loc : Location.t) = +let source_loc_of_loc (loc : Location.t) = match !Js_config.source_map with | No_source_map -> None | Linked -> - if loc.loc_ghost || loc.loc_start.pos_cnum < 0 then None - else - let id = !next_marker in - incr next_marker; - Hashtbl.replace marker_locs id loc; - Some (marker_prefix ^ string_of_int id) - -let with_marker_scope f = - let first_marker = !next_marker in - Ext_pervasives.finally () - ~clean:(fun () -> - for id = first_marker to !next_marker - 1 do - Hashtbl.remove marker_locs id - done) - f + if loc.loc_ghost || loc.loc_start.pos_cnum < 0 then None else Some loc let with_builder builder f = let old = !current in @@ -220,20 +191,12 @@ let add_mapping builder ~generated_line ~generated_column (loc : Location.t) = :: builder.mappings; builder.last_generated <- Some (generated_line, generated_column) -let mark_comment fmt comment = - if is_prefix ~prefix:marker_prefix comment then ( - let prefix_len = String.length marker_prefix in - let id = - int_of_string - (String.sub comment prefix_len (String.length comment - prefix_len)) - in - (match (!current, Hashtbl.find_opt marker_locs id) with - | Some builder, Some loc -> - let generated_line, generated_column = Ext_pp.position fmt in - add_mapping builder ~generated_line ~generated_column loc - | _ -> ()); - true) - else false +let mark_source_loc fmt source_loc = + match (!current, source_loc) with + | Some builder, Some loc -> + let generated_line, generated_column = Ext_pp.position fmt in + add_mapping builder ~generated_line ~generated_column loc + | _ -> () let base64_vlq_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" diff --git a/compiler/core/js_source_map.mli b/compiler/core/js_source_map.mli index ad267a5b881..cf774b914ca 100644 --- a/compiler/core/js_source_map.mli +++ b/compiler/core/js_source_map.mli @@ -3,13 +3,11 @@ type t val make : generated_file:string -> source_root:string -> sources_content:bool -> t -val with_marker_scope : (unit -> 'a) -> 'a - val with_builder : t option -> (unit -> 'a) -> 'a -val comment_of_loc : Location.t -> string option +val source_loc_of_loc : Location.t -> Location.t option -val mark_comment : Ext_pp.t -> string -> bool +val mark_source_loc : Ext_pp.t -> Location.t option -> unit val json : t -> string diff --git a/compiler/core/js_stmt_make.ml b/compiler/core/js_stmt_make.ml index 16b43e4782d..1c2ec55023e 100644 --- a/compiler/core/js_stmt_make.ml +++ b/compiler/core/js_stmt_make.ml @@ -26,12 +26,15 @@ module E = Js_exp_make type t = J.statement -let return_stmt ?comment e : t = {statement_desc = Return e; comment} +let return_stmt ?comment e : t = + {statement_desc = Return e; comment; source_loc = None} -let empty_stmt : t = {statement_desc = Block []; comment = None} +let empty_stmt : t = + {statement_desc = Block []; comment = None; source_loc = None} (* let empty_block : J.block = [] *) -let throw_stmt ?comment v : t = {statement_desc = Throw v; comment} +let throw_stmt ?comment v : t = + {statement_desc = Throw v; comment; source_loc = None} (* avoid nested block *) let rec block ?comment (b : J.block) : t = @@ -39,7 +42,7 @@ let rec block ?comment (b : J.block) : t = | [{statement_desc = Block bs}] -> block bs | [b] -> b | [] -> empty_stmt - | _ -> {statement_desc = Block b; comment} + | _ -> {statement_desc = Block b; comment; source_loc = None} (* It's a statement, we can discard some values *) let rec exp ?comment (e : E.t) : t = @@ -50,7 +53,7 @@ let rec exp ?comment (e : E.t) : t = | Number _ | Undefined _ -> block [] (* TODO: we can do more *) (* | _ when is_pure e -> block [] *) - | _ -> {statement_desc = Exp e; comment} + | _ -> {statement_desc = Exp e; comment; source_loc = None} let declare_variable ?comment ?ident_info ~kind (ident : Ident.t) : t = let property : J.property = kind in @@ -62,6 +65,7 @@ let declare_variable ?comment ?ident_info ~kind (ident : Ident.t) : t = { statement_desc = Variable {ident; value = None; property; ident_info}; comment; + source_loc = None; } let define_variable ?comment ?ident_info ~kind (v : Ident.t) @@ -79,6 +83,7 @@ let define_variable ?comment ?ident_info ~kind (v : Ident.t) statement_desc = Variable {ident = v; value = Some exp; property; ident_info}; comment; + source_loc = None; } (* let alias_variable ?comment ~exp (v:Ident.t) : t= @@ -86,7 +91,7 @@ let define_variable ?comment ?ident_info ~kind (v : Ident.t) Variable { ident = v; value = Some exp; property = Alias; ident_info = {used_stats = NA } }; - comment} *) + comment; source_loc = None} *) let int_switch ?(comment : string option) ?(declaration : (J.property * Ident.t) option) ?(default : J.block option) @@ -129,9 +134,18 @@ let int_switch ?(comment : string option) block [ declare_variable ?comment ~kind did; - {statement_desc = J.Int_switch (e, clauses, default); comment}; + { + statement_desc = J.Int_switch (e, clauses, default); + comment; + source_loc = None; + }; ] - | None -> {statement_desc = J.Int_switch (e, clauses, default); comment}) + | None -> + { + statement_desc = J.Int_switch (e, clauses, default); + comment; + source_loc = None; + }) let string_switch ?(comment : string option) ?(declaration : (J.property * Ident.t) option) ?(default : J.block option) @@ -179,9 +193,18 @@ let string_switch ?(comment : string option) block [ declare_variable ?comment ~kind did; - {statement_desc = String_switch (e, clauses, default); comment}; + { + statement_desc = String_switch (e, clauses, default); + comment; + source_loc = None; + }; ] - | None -> {statement_desc = String_switch (e, clauses, default); comment}) + | None -> + { + statement_desc = String_switch (e, clauses, default); + comment; + source_loc = None; + }) let rec block_last_is_return_throw_or_continue (x : J.block) = match x with @@ -245,9 +268,17 @@ let if_ ?comment ?declaration ?else_ (e : J.expression) (then_ : J.block) : t = [{statement_desc = Return ret_ifnot; _}] ) -> return_stmt (E.econd e ret_ifso ret_ifnot) | _, [{statement_desc = Return _}] -> - block ({statement_desc = If (E.not e, ifnot, []); comment} :: ifso) + block + ({ + statement_desc = If (E.not e, ifnot, []); + comment; + source_loc = None; + } + :: ifso) | _, _ when block_last_is_return_throw_or_continue ifso -> - block ({statement_desc = If (e, ifso, []); comment} :: ifnot) + block + ({statement_desc = If (e, ifso, []); comment; source_loc = None} + :: ifnot) | ( [ { statement_desc = @@ -299,7 +330,7 @@ let if_ ?comment ?declaration ?else_ (e : J.expression) (then_ : J.block) : t = | _, [{statement_desc = If (pred1, ifso1, ifnot1)}] when Js_analyzer.eq_block ifso ifnot1 -> aux ?comment (E.or_ e (E.not pred1)) ifso ifso1 - | _ -> {statement_desc = If (e, ifso, ifnot); comment}) + | _ -> {statement_desc = If (e, ifso, ifnot); comment; source_loc = None}) in let if_block = aux ?comment e then_ @@ -312,10 +343,10 @@ let if_ ?comment ?declaration ?else_ (e : J.expression) (then_ : J.block) : t = | false, Some (kind, id) -> block (declare_variable ~kind id :: [if_block]) let assign ?comment id e : t = - {statement_desc = J.Exp (E.assign (E.var id) e); comment} + {statement_desc = J.Exp (E.assign (E.var id) e); comment; source_loc = None} let while_ ?comment ?label (e : E.t) (st : J.block) : t = - {statement_desc = While (label, e, st); comment} + {statement_desc = While (label, e, st); comment; source_loc = None} let for_ ?comment ?label for_ident_expression finish_ident_expression id direction (b : J.block) : t = @@ -324,21 +355,33 @@ let for_ ?comment ?label for_ident_expression finish_ident_expression id ForRange (label, for_ident_expression, finish_ident_expression, id, direction, b); comment; + source_loc = None; } let for_of ?comment ?label iterable_expression id (b : J.block) : t = - {statement_desc = ForOf (label, id, iterable_expression, b); comment} + { + statement_desc = ForOf (label, id, iterable_expression, b); + comment; + source_loc = None; + } let for_await_of ?comment ?label iterable_expression id (b : J.block) : t = - {statement_desc = ForAwaitOf (label, id, iterable_expression, b); comment} + { + statement_desc = ForAwaitOf (label, id, iterable_expression, b); + comment; + source_loc = None; + } let try_ ?comment ?with_ ?finally body : t = - {statement_desc = Try (body, with_, finally); comment} + {statement_desc = Try (body, with_, finally); comment; source_loc = None} -let break_ ?label () : t = {statement_desc = Break label; comment = None} +let break_ ?label () : t = + {statement_desc = Break label; comment = None; source_loc = None} -let continue_ ?label () : t = {statement_desc = Continue label; comment = None} +let continue_ ?label () : t = + {statement_desc = Continue label; comment = None; source_loc = None} -let debugger_stmt ?comment () : t = {statement_desc = Debugger; comment} +let debugger_stmt ?comment () : t = + {statement_desc = Debugger; comment; source_loc = None} let debugger_block : t list = [debugger_stmt ()] diff --git a/compiler/core/lam_compile.ml b/compiler/core/lam_compile.ml index 9ddf1714a42..994ead1fd87 100644 --- a/compiler/core/lam_compile.ml +++ b/compiler/core/lam_compile.ml @@ -25,11 +25,11 @@ module E = Js_exp_make module S = Js_stmt_make -let source_map_comment = Js_source_map.comment_of_loc +let source_map_loc = Js_source_map.source_loc_of_loc let with_source_loc loc (exp : J.expression) = - match (source_map_comment loc, exp.comment) with - | Some comment, None -> {exp with comment = Some comment} + match (source_map_loc loc, exp.source_loc) with + | Some source_loc, None -> {exp with source_loc = Some source_loc} | _ -> exp let rec source_loc_of_lam (lam : Lam.t) = @@ -67,21 +67,21 @@ let rec source_loc_of_lam (lam : Lam.t) = | Lassign (_, body) -> source_loc_of_lam body | Lvar _ | Lglobal_module _ | Lconst _ | Lbreak | Lcontinue -> None -let source_map_comment_of_lam lam = +let source_map_loc_of_lam lam = match source_loc_of_lam lam with - | Some loc -> source_map_comment loc + | Some loc -> source_map_loc loc | None -> None -let with_statement_comment comment (stmt : J.statement) = - match (comment, stmt.comment) with - | Some comment, None -> {stmt with comment = Some comment} +let with_statement_source_loc source_loc (stmt : J.statement) = + match (source_loc, stmt.source_loc) with + | Some source_loc, None -> {stmt with source_loc = Some source_loc} | _ -> stmt let with_block_source_loc lam block = match block with | [] -> [] | stmt :: rest -> - with_statement_comment (source_map_comment_of_lam lam) stmt :: rest + with_statement_source_loc (source_map_loc_of_lam lam) stmt :: rest let args_either_function_or_const (args : Lam.t list) = Ext_list.for_all args (fun x -> @@ -425,13 +425,11 @@ let compile output_prefix = } body in - let comment = source_map_comment loc in let result = if ret.triggered then let body_block = Js_output.output_as_block output in E.ocaml_fun - ?comment - (* TODO: save computation of length several times + (* TODO: save computation of length several times Here we always create [ocaml_fun], it will be renamed into [method] when it is detected by a primitive @@ -448,10 +446,11 @@ let compile output_prefix = ] else (* TODO: save computation of length several times *) - E.ocaml_fun ?comment params + E.ocaml_fun params (Js_output.output_as_block output) ~return_unit ~async ~one_unit_arg ?directive in + let result = with_source_loc loc result in ( Js_output.output_of_expression (Declare (Alias, id)) result @@ -695,14 +694,16 @@ let compile output_prefix = { switch_body; should_break; - comment = source_map_comment_of_lam lam; + comment = None; + source_loc = source_map_loc_of_lam lam; } ) else ( switch_case, { switch_body = []; should_break = false; - comment = source_map_comment_of_lam lam; + comment = None; + source_loc = source_map_loc_of_lam lam; } )) (* TODO: we should also group default *) (* The last clause does not need [break] @@ -1733,9 +1734,11 @@ let compile output_prefix = compile_lambda {lambda_cxt with continuation = NeedValue Not_tail} e with | {block; value = Some v} -> - let comment = source_map_comment loc in + let stmt = + with_statement_source_loc (source_map_loc loc) (S.throw_stmt v) + in Js_output.make - (Ext_list.append_one block (S.throw_stmt ?comment v)) + (Ext_list.append_one block stmt) ~value:E.undefined ~output_finished:True (* FIXME -- breaks invariant when NeedValue, reason is that js [throw] is statement while ocaml it's an expression, we should remove such things in lambda optimizations @@ -1746,9 +1749,10 @@ let compile output_prefix = | {primitive = Pdebugger; loc; _} -> (* [%debugger] guarantees that the expression does not matter TODO: make it even safer *) - let comment = source_map_comment loc in - Js_output.output_of_block_and_expression lambda_cxt.continuation - [S.debugger_stmt ?comment ()] + let stmt = + with_statement_source_loc (source_map_loc loc) (S.debugger_stmt ()) + in + Js_output.output_of_block_and_expression lambda_cxt.continuation [stmt] E.unit (* TODO: check the arity of fn before wrapping it @@ -1805,24 +1809,24 @@ let compile output_prefix = | {primitive = Pjs_fn_method; args = args_lambda} -> ( match args_lambda with | [Lfunction {params; body; attr = {return_unit; async}; loc}] -> - let comment = source_map_comment loc in Js_output.output_of_block_and_expression lambda_cxt.continuation [] - (E.method_ ?comment ~async ~return_unit params - (* Invariant: jmp_table can not across function boundary, - here we share env - *) - (Js_output.output_as_block - (compile_lambda - { - lambda_cxt with - continuation = - EffectCall - (Maybe_tail_is_return - (Tail_with_name - {label = None; in_staticcatch = false})); - jmp_table = Lam_compile_context.empty_handler_map; - } - body))) + (with_source_loc loc + (E.method_ ~async ~return_unit params + (* Invariant: jmp_table can not across function boundary, + here we share env + *) + (Js_output.output_as_block + (compile_lambda + { + lambda_cxt with + continuation = + EffectCall + (Maybe_tail_is_return + (Tail_with_name + {label = None; in_staticcatch = false})); + jmp_table = Lam_compile_context.empty_handler_map; + } + body)))) | _ -> assert false) | {primitive = Pjs_fn_make arity; args = [fn]; loc} -> compile_lambda lambda_cxt @@ -1928,27 +1932,27 @@ let compile output_prefix = attr = {return_unit; async; one_unit_arg; directive}; loc; } -> - let comment = source_map_comment loc in Js_output.output_of_expression lambda_cxt.continuation ~no_effects:no_effects_const - (E.ocaml_fun ?comment params ~return_unit ~async ~one_unit_arg - ?directive - (* Invariant: jmp_table can not across function boundary, - here we share env - *) - (Js_output.output_as_block - (compile_lambda - { - lambda_cxt with - continuation = - EffectCall - (Maybe_tail_is_return - (Tail_with_name {label = None; in_staticcatch = false})); - jmp_table = Lam_compile_context.empty_handler_map; - switch_depth = 0; - loop_stack = []; - } - body))) + (with_source_loc loc + (E.ocaml_fun params ~return_unit ~async ~one_unit_arg ?directive + (* Invariant: jmp_table can not across function boundary, + here we share env + *) + (Js_output.output_as_block + (compile_lambda + { + lambda_cxt with + continuation = + EffectCall + (Maybe_tail_is_return + (Tail_with_name + {label = None; in_staticcatch = false})); + jmp_table = Lam_compile_context.empty_handler_map; + switch_depth = 0; + loop_stack = []; + } + body)))) | Lapply appinfo -> compile_apply appinfo lambda_cxt | Llet (let_kind, id, arg, body) -> (* Order matters.. see comment below in [Lletrec] *) diff --git a/compiler/core/lam_compile_primitive.ml b/compiler/core/lam_compile_primitive.ml index 47aabff81a3..88c3a0c1fdf 100644 --- a/compiler/core/lam_compile_primitive.ml +++ b/compiler/core/lam_compile_primitive.ml @@ -65,7 +65,13 @@ let wrap_then import value = E.call ~info:call_info (E.dot import "then") [ E.ocaml_fun ~return_unit:false ~async:false ~one_unit_arg:false [arg] - [{statement_desc = J.Return (E.dot (E.var arg) value); comment = None}]; + [ + { + statement_desc = J.Return (E.dot (E.var arg) value); + comment = None; + source_loc = None; + }; + ]; ] let translate output_prefix loc (cxt : Lam_compile_context.t) diff --git a/tests/build_tests/source_map/input.js b/tests/build_tests/source_map/input.js index e3a995c6fce..efcc189b517 100644 --- a/tests/build_tests/source_map/input.js +++ b/tests/build_tests/source_map/input.js @@ -90,12 +90,22 @@ const sourcePath = path.join(import.meta.dirname, "src", "Demo.res"); const source = await fs.readFile(sourcePath, "utf8"); const originalDebuggerPositions = findTokenPositions(source, "%debugger"); assert.equal(originalDebuggerPositions.length, 2); +const originalRaiseErrorPositions = findTokenPositions( + source, + "Js.Exn.raiseError", +); +assert.equal(originalRaiseErrorPositions.length, 1); for (const filename of ["Demo.cjs", "Demo.mjs"]) { const jsPath = path.join(import.meta.dirname, "lib", "bs", "src", filename); const mapPath = `${jsPath}.map`; const js = await fs.readFile(jsPath, "utf8"); + assert.match( + js, + /\/\* @__PURE__ \*\/Primitive_exceptions\.create/, + `${filename} should preserve real JS comments while source maps are enabled`, + ); assert.match( js, new RegExp(`//# sourceMappingURL=${filename.replace(".", "\\.")}\\.map`), @@ -118,6 +128,28 @@ for (const filename of ["Demo.cjs", "Demo.mjs"]) { assert.equal(generatedDebuggerPositions.length, 2); const decodedMappings = decodeMappings(map.mappings); + const generatedRaiseErrorPositions = findTokenPositions( + js, + "Stdlib_Exn.raiseError", + ); + assert.equal(generatedRaiseErrorPositions.length, 1); + const raiseErrorMapping = decodedMappings.find( + decoded => + decoded.generatedLine === generatedRaiseErrorPositions[0].line && + decoded.generatedColumn === generatedRaiseErrorPositions[0].column, + ); + assert.ok( + raiseErrorMapping, + `${filename}.map should include an exact mapping for raiseError`, + ); + assert.deepEqual( + { + line: raiseErrorMapping.originalLine, + column: raiseErrorMapping.originalColumn, + }, + originalRaiseErrorPositions[0], + ); + const debuggerMappings = generatedDebuggerPositions.map(position => { const mapping = decodedMappings.find( decoded => diff --git a/tests/build_tests/source_map/src/Demo.res b/tests/build_tests/source_map/src/Demo.res index 9637d4217e4..935aaad7b01 100644 --- a/tests/build_tests/source_map/src/Demo.res +++ b/tests/build_tests/source_map/src/Demo.res @@ -1,5 +1,7 @@ let add = (a, b) => a + b +exception PureMarker + let crash = () => Js.Exn.raiseError("source map test") let debugStatement = () => { diff --git a/tests/ounit_tests/ounit_js_analyzer_tests.ml b/tests/ounit_tests/ounit_js_analyzer_tests.ml index 6a595cfd1a0..af9806115fc 100644 --- a/tests/ounit_tests/ounit_js_analyzer_tests.ml +++ b/tests/ounit_tests/ounit_js_analyzer_tests.ml @@ -8,6 +8,7 @@ let for_of_statement = J.statement_desc = ForOf (None, Ident.create "_for_of", pure_iterable, empty_body); comment = None; + source_loc = None; } let for_await_of_statement = @@ -15,6 +16,7 @@ let for_await_of_statement = J.statement_desc = ForAwaitOf (None, Ident.create "_for_await_of", pure_iterable, empty_body); comment = None; + source_loc = None; } let suites = From 1892e2f1e95653226f6719eed76b2b504bb0df20 Mon Sep 17 00:00:00 2001 From: mununki Date: Sun, 3 May 2026 14:08:33 +0900 Subject: [PATCH 11/13] Add inline and hidden source map modes --- compiler/bsc/rescript_compiler_main.ml | 9 +- compiler/common/js_config.ml | 2 +- compiler/common/js_config.mli | 2 +- compiler/core/js_source_map.ml | 38 ++++- compiler/core/js_source_map.mli | 2 + compiler/core/lam_compile_main.ml | 33 +++- docs/docson/build-schema.json | 4 +- rewatch/src/build/compile.rs | 2 + rewatch/src/config.rs | 167 +++++++++++++++--- tests/build_tests/source_map/input.js | 227 ++++++++++++++++++++++--- 10 files changed, 430 insertions(+), 56 deletions(-) diff --git a/compiler/bsc/rescript_compiler_main.ml b/compiler/bsc/rescript_compiler_main.ml index 300cc391abc..a638fc4593b 100644 --- a/compiler/bsc/rescript_compiler_main.ml +++ b/compiler/bsc/rescript_compiler_main.ml @@ -212,9 +212,14 @@ let[@inline] string_list_add s : Bsc_args.spec = String (String_list_add s) let parse_source_map value = Js_config.source_map := match String.lowercase_ascii value with - | "true" | "linked" -> Linked + | "linked" -> Linked + | "inline" -> Inline + | "hidden" -> Hidden | "false" | "none" -> No_source_map - | value -> Bsc_args.bad_arg ("Unsupported sourceMap value: " ^ value) + | value -> + Bsc_args.bad_arg + ("Unsupported sourceMap value: " ^ value + ^ ". Expected linked, inline, hidden, false, or none") let parse_bool_ref target value = target := diff --git a/compiler/common/js_config.ml b/compiler/common/js_config.ml index ffbc36dc717..f7a87907387 100644 --- a/compiler/common/js_config.ml +++ b/compiler/common/js_config.ml @@ -26,7 +26,7 @@ type jsx_version = Jsx_v4 type jsx_module = React | Generic of {module_name: string} -type source_map = No_source_map | Linked +type source_map = No_source_map | Linked | Inline | Hidden let no_version_header = ref false diff --git a/compiler/common/js_config.mli b/compiler/common/js_config.mli index fa48d9cc4dc..a57d13af401 100644 --- a/compiler/common/js_config.mli +++ b/compiler/common/js_config.mli @@ -24,7 +24,7 @@ type jsx_version = Jsx_v4 type jsx_module = React | Generic of {module_name: string} -type source_map = No_source_map | Linked +type source_map = No_source_map | Linked | Inline | Hidden (* val get_packages_info : unit -> Js_packages_info.t *) diff --git a/compiler/core/js_source_map.ml b/compiler/core/js_source_map.ml index 70072c2cc94..a8256ca9e4f 100644 --- a/compiler/core/js_source_map.ml +++ b/compiler/core/js_source_map.ml @@ -14,8 +14,8 @@ generated position against the original ReScript location. 4. After printing, the collected mappings are sorted and encoded into the compact Source Map v3 "mappings" field using base64 VLQ. The JSON map is - emitted next to the generated JavaScript and the JS file receives a - sourceMappingURL comment. + either emitted next to the generated JavaScript or embedded into it, + depending on the configured source map mode. Original locations come from OCaml Location.t values, whose columns are byte offsets. When source contents are available, original columns are converted @@ -52,7 +52,7 @@ let current : t option ref = ref None let source_loc_of_loc (loc : Location.t) = match !Js_config.source_map with | No_source_map -> None - | Linked -> + | Linked | Inline | Hidden -> if loc.loc_ghost || loc.loc_start.pos_cnum < 0 then None else Some loc let with_builder builder f = @@ -283,3 +283,35 @@ let json builder = let linked_comment ~map_file = "//# sourceMappingURL=" ^ Filename.basename map_file ^ "\n" + +let base64_chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" + +(* OCaml Stdlib does not provide Base64, and inline source maps only need a + small RFC 4648 encoder with padding for the data URI payload. Keep this + local instead of adding a package dependency just for this narrow use. *) +let base64_encode input = + let len = String.length input in + let output = Buffer.create ((len + 2) / 3 * 4) in + let rec loop index = + if index < len then ( + let b0 = Char.code input.[index] in + let has_b1 = index + 1 < len in + let has_b2 = index + 2 < len in + let b1 = if has_b1 then Char.code input.[index + 1] else 0 in + let b2 = if has_b2 then Char.code input.[index + 2] else 0 in + let chunk = (b0 lsl 16) lor (b1 lsl 8) lor b2 in + Buffer.add_char output base64_chars.[(chunk lsr 18) land 0x3f]; + Buffer.add_char output base64_chars.[(chunk lsr 12) land 0x3f]; + Buffer.add_char output + (if has_b1 then base64_chars.[(chunk lsr 6) land 0x3f] else '='); + Buffer.add_char output + (if has_b2 then base64_chars.[chunk land 0x3f] else '='); + loop (index + 3)) + in + loop 0; + Buffer.contents output + +let inline_comment ~json = + "//# sourceMappingURL=data:application/json;base64," ^ base64_encode json + ^ "\n" diff --git a/compiler/core/js_source_map.mli b/compiler/core/js_source_map.mli index cf774b914ca..684f47c2665 100644 --- a/compiler/core/js_source_map.mli +++ b/compiler/core/js_source_map.mli @@ -12,3 +12,5 @@ val mark_source_loc : Ext_pp.t -> Location.t option -> unit val json : t -> string val linked_comment : map_file:string -> string + +val inline_comment : json:string -> string diff --git a/compiler/core/lam_compile_main.ml b/compiler/core/lam_compile_main.ml index 54e8a119825..8f905226c3c 100644 --- a/compiler/core/lam_compile_main.ml +++ b/compiler/core/lam_compile_main.ml @@ -300,10 +300,10 @@ let (//) = Filename.concat let source_map_enabled () = match !Js_config.source_map with | No_source_map -> false - | Linked -> true + | Linked | Inline | Hidden -> true -let dump_deps_program_with_source_map ~target_file ~output_prefix module_system - lambda_output chan = +let dump_deps_program_with_source_map ?(remove_stale_map = true) ~target_file + ~output_prefix module_system lambda_output chan = let builder = if source_map_enabled () then Some @@ -315,11 +315,23 @@ let dump_deps_program_with_source_map ~target_file ~output_prefix module_system Js_source_map.with_builder builder (fun () -> Js_dump_program.pp_deps_program ~output_prefix module_system lambda_output (Ext_pp.from_channel chan)); + let map_file = target_file ^ ".map" in + let remove_map_file () = + if remove_stale_map && not !Clflags.dont_write_files then + Misc.remove_file map_file + in match (builder, !Js_config.source_map) with | Some builder, Linked -> - let map_file = target_file ^ ".map" in + let json = Js_source_map.json builder in output_string chan (Js_source_map.linked_comment ~map_file); + Ext_io.write_file map_file json + | Some builder, Hidden -> Ext_io.write_file map_file (Js_source_map.json builder) + | Some builder, Inline -> + output_string chan + (Js_source_map.inline_comment ~json:(Js_source_map.json builder)); + remove_map_file () + | _, No_source_map -> remove_map_file () | _ -> () let lambda_as_module @@ -328,7 +340,18 @@ let lambda_as_module : unit = let package_info = Js_packages_state.get_packages_info () in if Js_packages_info.is_empty package_info && !Js_config.js_stdout then begin - Js_dump_program.dump_deps_program ~output_prefix Commonjs (lambda_output) stdout + match !Js_config.source_map with + | Inline -> + let target_file = + Ext_namespace.change_ext_ns_suffix + (Filename.basename output_prefix) + Literals.suffix_js + in + dump_deps_program_with_source_map ~remove_stale_map:false ~target_file + ~output_prefix Commonjs lambda_output stdout + | _ -> + Js_dump_program.dump_deps_program ~output_prefix Commonjs lambda_output + stdout end else Js_packages_info.iter package_info (fun {module_system; path; suffix} -> let basename = diff --git a/docs/docson/build-schema.json b/docs/docson/build-schema.json index 266a8f07186..2c3e1b30fec 100644 --- a/docs/docson/build-schema.json +++ b/docs/docson/build-schema.json @@ -61,8 +61,8 @@ "description": "`dev` generates source maps only during watch mode. `always` generates source maps during both build and watch." }, "mode": { - "enum": ["linked"], - "description": "Generate a separate .js.map file next to each generated JavaScript file and append a sourceMappingURL comment. Only linked source maps are supported for now." + "enum": ["linked", "inline", "hidden"], + "description": "`linked` generates a separate .js.map file and appends a sourceMappingURL comment. `inline` appends the source map as a data URI and does not generate a .js.map file. `hidden` generates a separate .js.map file without appending a sourceMappingURL comment. When `inline` is combined with `sourcesContent: true`, original .res source text is embedded directly into the generated JS." }, "sourcesContent": { "type": "boolean", diff --git a/rewatch/src/build/compile.rs b/rewatch/src/build/compile.rs index 9158820b851..ff480602de0 100644 --- a/rewatch/src/build/compile.rs +++ b/rewatch/src/build/compile.rs @@ -1067,6 +1067,8 @@ fn compile_file( if source_map.exists() { let _ = std::fs::copy(&source_map, &destination_map) .expect("copying source map file failed"); + } else { + let _ = std::fs::remove_file(&destination_map); } } }); diff --git a/rewatch/src/config.rs b/rewatch/src/config.rs index c9b50541b3c..ae8c4a890a3 100644 --- a/rewatch/src/config.rs +++ b/rewatch/src/config.rs @@ -282,13 +282,30 @@ pub struct JsxSpecs { pub preserve: Option, } -#[derive(Deserialize, Debug, Clone, Eq, PartialEq)] -#[serde(untagged)] +#[derive(Debug, Clone, Eq, PartialEq)] pub enum SourceMapConfig { Bool(bool), Options(SourceMapOptions), } +impl<'de> Deserialize<'de> for SourceMapConfig { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let value = serde_json::Value::deserialize(deserializer)?; + match value { + serde_json::Value::Bool(value) => Ok(SourceMapConfig::Bool(value)), + serde_json::Value::Object(_) => SourceMapOptions::deserialize(value) + .map(SourceMapConfig::Options) + .map_err(DeError::custom), + _ => Err(DeError::custom( + "sourceMap must be false or an object with enabled and mode fields", + )), + } + } +} + #[derive(Deserialize, Debug, Clone, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub enum SourceMapEnabled { @@ -296,11 +313,45 @@ pub enum SourceMapEnabled { Always, } +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum SourceMapMode { + Linked, + Inline, + Hidden, +} + +impl<'de> Deserialize<'de> for SourceMapMode { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let value = String::deserialize(deserializer)?; + match value.as_str() { + "linked" => Ok(SourceMapMode::Linked), + "inline" => Ok(SourceMapMode::Inline), + "hidden" => Ok(SourceMapMode::Hidden), + _ => Err(DeError::custom(format!( + "sourceMap.mode must be one of linked, inline, hidden, got {value:?}" + ))), + } + } +} + +impl SourceMapMode { + fn as_str(self) -> &'static str { + match self { + SourceMapMode::Linked => "linked", + SourceMapMode::Inline => "inline", + SourceMapMode::Hidden => "hidden", + } + } +} + #[derive(Deserialize, Debug, Clone, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub struct SourceMapOptions { pub enabled: SourceMapEnabled, - pub mode: String, + pub mode: SourceMapMode, pub sources_content: Option, pub source_root: Option, } @@ -833,21 +884,11 @@ impl Config { panic!("sourceMap true is unsupported; use {{ \"mode\": \"linked\" }}") } SourceMapConfig::Options(options) => { - let source_map_mode = match options.mode.as_str() { - "linked" => "linked", - value => panic!("sourceMap.mode value {value} is unsupported"), - }; - - let source_map_enabled = match options.enabled { - SourceMapEnabled::Dev => command == SourceMapCommand::Watch, - SourceMapEnabled::Always => true, - }; - - if !source_map_enabled { + let Some(mode) = self.effective_source_map_mode(command) else { return vec!["-bs-source-map".to_string(), "false".to_string()]; - } + }; - args.extend(["-bs-source-map".to_string(), source_map_mode.to_string()]); + args.extend(["-bs-source-map".to_string(), mode.as_str().to_string()]); if let Some(sources_content) = options.sources_content { args.extend([ @@ -866,6 +907,22 @@ impl Config { args } + pub fn effective_source_map_mode(&self, command: SourceMapCommand) -> Option { + match &self.source_map { + Some(SourceMapConfig::Options(options)) => { + let source_map_enabled = match options.enabled { + SourceMapEnabled::Dev => command == SourceMapCommand::Watch, + SourceMapEnabled::Always => true, + }; + source_map_enabled.then_some(options.mode) + } + Some(SourceMapConfig::Bool(true)) => { + panic!("sourceMap true is unsupported; use {{ \"mode\": \"linked\" }}") + } + _ => None, + } + } + pub fn get_experimental_features_args(&self) -> Vec { match &self.experimental_features { None => vec![], @@ -1690,6 +1747,10 @@ pub mod tests { "webpack://testrepo/", ] ); + assert_eq!( + config.effective_source_map_mode(SourceMapCommand::Build), + Some(SourceMapMode::Linked) + ); } #[test] @@ -1711,6 +1772,7 @@ pub mod tests { config.get_source_map_args(SourceMapCommand::Build), vec!["-bs-source-map", "false",] ); + assert_eq!(config.effective_source_map_mode(SourceMapCommand::Build), None); assert_eq!( config.get_source_map_args(SourceMapCommand::Watch), vec![ @@ -1720,6 +1782,10 @@ pub mod tests { "true", ] ); + assert_eq!( + config.effective_source_map_mode(SourceMapCommand::Watch), + Some(SourceMapMode::Linked) + ); } #[test] @@ -1737,6 +1803,7 @@ pub mod tests { config.get_source_map_args(SourceMapCommand::Build), vec!["-bs-source-map", "false",] ); + assert_eq!(config.effective_source_map_mode(SourceMapCommand::Build), None); } #[test] @@ -1768,27 +1835,85 @@ pub mod tests { let error = serde_json::from_str::(json).unwrap_err(); assert!( - error.to_string().contains("SourceMapConfig"), + error.to_string().contains("missing field `enabled`"), "unexpected error: {error}" ); } #[test] - #[should_panic(expected = "sourceMap.mode value inline is unsupported")] - fn test_source_map_rejects_inline_for_mvp() { + fn test_source_map_inline_args() { let json = r#" { "name": "testrepo", "sources": [ { "dir": "src/", "subdirs": true } ], "sourceMap": { "enabled": "always", - "mode": "inline" + "mode": "inline", + "sourcesContent": false } } "#; let config = serde_json::from_str::(json).unwrap(); - let _ = config.get_source_map_args(SourceMapCommand::Build); + assert_eq!( + config.get_source_map_args(SourceMapCommand::Build), + vec![ + "-bs-source-map", + "inline", + "-bs-source-map-sources-content", + "false", + ] + ); + assert_eq!( + config.effective_source_map_mode(SourceMapCommand::Build), + Some(SourceMapMode::Inline) + ); + } + + #[test] + fn test_source_map_hidden_args() { + let json = r#" + { + "name": "testrepo", + "sources": [ { "dir": "src/", "subdirs": true } ], + "sourceMap": { + "enabled": "always", + "mode": "hidden" + } + } + "#; + + let config = serde_json::from_str::(json).unwrap(); + assert_eq!( + config.get_source_map_args(SourceMapCommand::Build), + vec!["-bs-source-map", "hidden",] + ); + assert_eq!( + config.effective_source_map_mode(SourceMapCommand::Build), + Some(SourceMapMode::Hidden) + ); + } + + #[test] + fn test_source_map_rejects_external_mode() { + let json = r#" + { + "name": "testrepo", + "sources": [ { "dir": "src/", "subdirs": true } ], + "sourceMap": { + "enabled": "always", + "mode": "external" + } + } + "#; + + let error = serde_json::from_str::(json).unwrap_err(); + assert!( + error + .to_string() + .contains("sourceMap.mode must be one of linked, inline, hidden"), + "unexpected error: {error}" + ); } #[test] diff --git a/tests/build_tests/source_map/input.js b/tests/build_tests/source_map/input.js index efcc189b517..5b65ce0dd93 100644 --- a/tests/build_tests/source_map/input.js +++ b/tests/build_tests/source_map/input.js @@ -1,13 +1,12 @@ // @ts-check import * as assert from "node:assert"; +import { Buffer } from "node:buffer"; import * as fs from "node:fs/promises"; import * as path from "node:path"; import { setup } from "#dev/process"; -const { execBuildOrThrow, execClean } = setup(import.meta.dirname); - -await execBuildOrThrow(); +const { bsc, execBuildOrThrow, execClean } = setup(import.meta.dirname); const base64VlqChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; @@ -86,8 +85,27 @@ function findTokenPositions(content, token) { }); } +async function fileExists(filename) { + try { + await fs.access(filename); + return true; + } catch { + return false; + } +} + +function mapFromInlineComment(js, filename) { + const match = js.match( + /\/\/# sourceMappingURL=data:application\/json;base64,([A-Za-z0-9+/=]+)\s*$/, + ); + assert.ok(match, `${filename} should include an inline source map`); + return JSON.parse(Buffer.from(match[1], "base64").toString("utf8")); +} + const sourcePath = path.join(import.meta.dirname, "src", "Demo.res"); const source = await fs.readFile(sourcePath, "utf8"); +const configPath = path.join(import.meta.dirname, "rescript.json"); +const originalConfig = await fs.readFile(configPath, "utf8"); const originalDebuggerPositions = findTokenPositions(source, "%debugger"); assert.equal(originalDebuggerPositions.length, 2); const originalRaiseErrorPositions = findTokenPositions( @@ -96,33 +114,64 @@ const originalRaiseErrorPositions = findTokenPositions( ); assert.equal(originalRaiseErrorPositions.length, 1); -for (const filename of ["Demo.cjs", "Demo.mjs"]) { - const jsPath = path.join(import.meta.dirname, "lib", "bs", "src", filename); - const mapPath = `${jsPath}.map`; +function configWithSourceMap(sourceMap) { + const config = JSON.parse(originalConfig); + config.sourceMap = + typeof sourceMap === "object" && sourceMap !== null + ? { + ...config.sourceMap, + ...sourceMap, + } + : sourceMap; + return `${JSON.stringify(config, null, 2)}\n`; +} - const js = await fs.readFile(jsPath, "utf8"); - assert.match( - js, - /\/\* @__PURE__ \*\/Primitive_exceptions\.create/, - `${filename} should preserve real JS comments while source maps are enabled`, - ); - assert.match( - js, - new RegExp(`//# sourceMappingURL=${filename.replace(".", "\\.")}\\.map`), - ); +function configWithMode(mode) { + return configWithSourceMap({ mode }); +} - const map = JSON.parse(await fs.readFile(mapPath, "utf8")); +async function removeGeneratedMapFiles() { + for (const filename of ["Demo.cjs", "Demo.mjs"]) { + await fs.rm(path.join(import.meta.dirname, "src", `${filename}.map`), { + force: true, + }); + await fs.rm( + path.join(import.meta.dirname, "lib", "bs", "src", `${filename}.map`), + { force: true }, + ); + } +} + +function assertSourceMap(filename, js, map, options = {}) { + const { expectSourcesContent = true, sourceRoot = undefined } = options; assert.equal(map.version, 3); assert.equal(map.file, filename); assert.ok(map.mappings.length > 0, `${filename}.map should include mappings`); + if (sourceRoot === undefined) { + assert.equal( + map.sourceRoot, + undefined, + `${filename}.map should not include sourceRoot`, + ); + } else { + assert.equal(map.sourceRoot, sourceRoot); + } assert.ok( map.sources.some(source => source.endsWith("Demo.res")), `${filename}.map should include Demo.res, got ${map.sources.join(", ")}`, ); - assert.ok( - map.sourcesContent.some(content => content.includes("let add = (a, b)")), - `${filename}.map should include source contents`, - ); + if (expectSourcesContent) { + assert.ok( + map.sourcesContent.some(content => content.includes("let add = (a, b)")), + `${filename}.map should include source contents`, + ); + } else { + assert.equal( + map.sourcesContent, + undefined, + `${filename}.map should not include source contents`, + ); + } const generatedDebuggerPositions = findTokenPositions(js, "debugger"); assert.equal(generatedDebuggerPositions.length, 2); @@ -178,4 +227,140 @@ for (const filename of ["Demo.cjs", "Demo.mjs"]) { ); } +async function assertLinkedOutput() { + await fs.writeFile(configPath, configWithMode("linked")); + await execBuildOrThrow(); + + for (const filename of ["Demo.cjs", "Demo.mjs"]) { + const jsPath = path.join(import.meta.dirname, "lib", "bs", "src", filename); + const mapPath = `${jsPath}.map`; + + const js = await fs.readFile(jsPath, "utf8"); + assert.match( + js, + /\/\* @__PURE__ \*\/Primitive_exceptions\.create/, + `${filename} should preserve real JS comments while source maps are enabled`, + ); + assert.match( + js, + new RegExp(`//# sourceMappingURL=${filename.replace(".", "\\.")}\\.map`), + ); + + assertSourceMap( + filename, + js, + JSON.parse(await fs.readFile(mapPath, "utf8")), + ); + } +} + +async function assertInlineStdoutOutput() { + const { stdout } = await bsc( + [ + "-bs-source-map", + "inline", + "-bs-source-map-sources-content", + "true", + sourcePath, + ], + { throwOnFail: true }, + ); + + assert.match( + stdout, + /\/\/# sourceMappingURL=data:application\/json;base64,/, + "stdout output should include an inline source map", + ); + assertSourceMap("Demo.js", stdout, mapFromInlineComment(stdout, "stdout")); +} + +async function assertHiddenOutput() { + const sourceRoot = "rescript://source-map-test/"; + await removeGeneratedMapFiles(); + await fs.writeFile( + configPath, + configWithSourceMap({ mode: "hidden", sourceRoot }), + ); + await execBuildOrThrow(); + + for (const filename of ["Demo.cjs", "Demo.mjs"]) { + const jsPath = path.join(import.meta.dirname, "lib", "bs", "src", filename); + const mapPath = `${jsPath}.map`; + + const js = await fs.readFile(jsPath, "utf8"); + assert.doesNotMatch( + js, + /\/\/# sourceMappingURL=/, + `${filename} should not include a sourceMappingURL comment in hidden mode`, + ); + assert.ok(await fileExists(mapPath), `${filename}.map should exist`); + assertSourceMap( + filename, + js, + JSON.parse(await fs.readFile(mapPath, "utf8")), + { sourceRoot }, + ); + } +} + +async function assertDisabledOutput() { + await fs.writeFile(configPath, configWithSourceMap(false)); + await execBuildOrThrow(); + + for (const filename of ["Demo.cjs", "Demo.mjs"]) { + const jsPath = path.join(import.meta.dirname, "lib", "bs", "src", filename); + const mapPath = `${jsPath}.map`; + + const js = await fs.readFile(jsPath, "utf8"); + assert.doesNotMatch( + js, + /\/\/# sourceMappingURL=/, + `${filename} should not include a sourceMappingURL comment when source maps are disabled`, + ); + assert.equal( + await fileExists(mapPath), + false, + `${filename}.map should be removed when source maps are disabled`, + ); + } +} + +async function assertInlineOutput() { + await fs.writeFile( + configPath, + configWithSourceMap({ mode: "inline", sourcesContent: false }), + ); + await execBuildOrThrow(); + + for (const filename of ["Demo.cjs", "Demo.mjs"]) { + const jsPath = path.join(import.meta.dirname, "lib", "bs", "src", filename); + const mapPath = `${jsPath}.map`; + + const js = await fs.readFile(jsPath, "utf8"); + assert.match( + js, + /\/\/# sourceMappingURL=data:application\/json;base64,/, + `${filename} should include an inline source map`, + ); + assert.equal( + await fileExists(mapPath), + false, + `${filename}.map should not exist in inline mode`, + ); + assertSourceMap(filename, js, mapFromInlineComment(js, filename), { + expectSourcesContent: false, + }); + } +} + await execClean(); +try { + await assertInlineStdoutOutput(); + await assertLinkedOutput(); + await assertDisabledOutput(); + await assertHiddenOutput(); + await assertInlineOutput(); +} finally { + await fs.writeFile(configPath, originalConfig); + await execClean(); +} From 32ef222eccfdee776a3bcf186df4aa21f5edb463 Mon Sep 17 00:00:00 2001 From: mununki Date: Sun, 3 May 2026 21:10:25 +0900 Subject: [PATCH 12/13] Reject sourceMap true during config parsing --- rewatch/src/config.rs | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/rewatch/src/config.rs b/rewatch/src/config.rs index ae8c4a890a3..dfdf67dcb05 100644 --- a/rewatch/src/config.rs +++ b/rewatch/src/config.rs @@ -284,7 +284,7 @@ pub struct JsxSpecs { #[derive(Debug, Clone, Eq, PartialEq)] pub enum SourceMapConfig { - Bool(bool), + Disabled, Options(SourceMapOptions), } @@ -295,7 +295,10 @@ impl<'de> Deserialize<'de> for SourceMapConfig { { let value = serde_json::Value::deserialize(deserializer)?; match value { - serde_json::Value::Bool(value) => Ok(SourceMapConfig::Bool(value)), + serde_json::Value::Bool(false) => Ok(SourceMapConfig::Disabled), + serde_json::Value::Bool(true) => Err(DeError::custom( + "sourceMap true is unsupported; use an object such as { \"enabled\": \"dev\", \"mode\": \"linked\" } or false", + )), serde_json::Value::Object(_) => SourceMapOptions::deserialize(value) .map(SourceMapConfig::Options) .map_err(DeError::custom), @@ -877,12 +880,9 @@ impl Config { if let Some(source_map) = &self.source_map { match source_map { - SourceMapConfig::Bool(false) => { + SourceMapConfig::Disabled => { args.extend(["-bs-source-map".to_string(), "false".to_string()]); } - SourceMapConfig::Bool(true) => { - panic!("sourceMap true is unsupported; use {{ \"mode\": \"linked\" }}") - } SourceMapConfig::Options(options) => { let Some(mode) = self.effective_source_map_mode(command) else { return vec!["-bs-source-map".to_string(), "false".to_string()]; @@ -916,9 +916,6 @@ impl Config { }; source_map_enabled.then_some(options.mode) } - Some(SourceMapConfig::Bool(true)) => { - panic!("sourceMap true is unsupported; use {{ \"mode\": \"linked\" }}") - } _ => None, } } @@ -1807,7 +1804,6 @@ pub mod tests { } #[test] - #[should_panic(expected = "sourceMap true is unsupported")] fn test_source_map_rejects_true_for_nested_config() { let json = r#" { @@ -1817,8 +1813,11 @@ pub mod tests { } "#; - let config = serde_json::from_str::(json).unwrap(); - let _ = config.get_source_map_args(SourceMapCommand::Build); + let error = serde_json::from_str::(json).unwrap_err(); + assert!( + error.to_string().contains("sourceMap true is unsupported"), + "unexpected error: {error}" + ); } #[test] From 5238b01b0a143ea04a18dd4f3194988eb49713d1 Mon Sep 17 00:00:00 2001 From: mununki Date: Tue, 5 May 2026 01:43:40 +0900 Subject: [PATCH 13/13] Expand source map coverage across modes --- tests/build_tests/source_map/input.js | 66 +++++++++++++++++------ tests/build_tests/source_map/src/Demo.res | 22 +++++++- 2 files changed, 71 insertions(+), 17 deletions(-) diff --git a/tests/build_tests/source_map/input.js b/tests/build_tests/source_map/input.js index 5b65ce0dd93..a7111087b96 100644 --- a/tests/build_tests/source_map/input.js +++ b/tests/build_tests/source_map/input.js @@ -85,6 +85,12 @@ function findTokenPositions(content, token) { }); } +function findSingleTokenPosition(content, token) { + const positions = findTokenPositions(content, token); + assert.equal(positions.length, 1, `${token} should appear exactly once`); + return positions[0]; +} + async function fileExists(filename) { try { await fs.access(filename); @@ -112,7 +118,13 @@ const originalRaiseErrorPositions = findTokenPositions( source, "Js.Exn.raiseError", ); -assert.equal(originalRaiseErrorPositions.length, 1); +assert.equal(originalRaiseErrorPositions.length, 2); +const originalPipeCallPositions = findTokenPositions(source, "input->fn"); +assert.equal(originalPipeCallPositions.length, 1); +const originalPatternBranchPositions = [ + findSingleTokenPosition(source, "Int.toString(value)"), + findSingleTokenPosition(source, "Int.toString(left + right)"), +]; function configWithSourceMap(sourceMap) { const config = JSON.parse(originalConfig); @@ -181,24 +193,46 @@ function assertSourceMap(filename, js, map, options = {}) { js, "Stdlib_Exn.raiseError", ); - assert.equal(generatedRaiseErrorPositions.length, 1); - const raiseErrorMapping = decodedMappings.find( - decoded => - decoded.generatedLine === generatedRaiseErrorPositions[0].line && - decoded.generatedColumn === generatedRaiseErrorPositions[0].column, - ); - assert.ok( - raiseErrorMapping, - `${filename}.map should include an exact mapping for raiseError`, - ); assert.deepEqual( - { - line: raiseErrorMapping.originalLine, - column: raiseErrorMapping.originalColumn, - }, - originalRaiseErrorPositions[0], + generatedRaiseErrorPositions.map(position => { + const mapping = decodedMappings.find( + decoded => + decoded.generatedLine === position.line && + decoded.generatedColumn === position.column, + ); + assert.ok( + mapping, + `${filename}.map should include an exact mapping for raiseError at ${position.line}:${position.column}`, + ); + assert.ok( + map.sources[mapping.sourceIndex].endsWith("Demo.res"), + `${filename}.map raiseError mapping should point to Demo.res`, + ); + return { + line: mapping.originalLine, + column: mapping.originalColumn, + }; + }), + originalRaiseErrorPositions, ); + for (const [label, positions] of [ + ["pipe call", originalPipeCallPositions], + ["pattern branch", originalPatternBranchPositions], + ]) { + for (const position of positions) { + const mapping = decodedMappings.find( + decoded => + decoded.originalLine === position.line && + map.sources[decoded.sourceIndex].endsWith("Demo.res"), + ); + assert.ok( + mapping, + `${filename}.map should include a ${label} mapping for Demo.res:${position.line}`, + ); + } + } + const debuggerMappings = generatedDebuggerPositions.map(position => { const mapping = decodedMappings.find( decoded => diff --git a/tests/build_tests/source_map/src/Demo.res b/tests/build_tests/source_map/src/Demo.res index 935aaad7b01..95958650a5e 100644 --- a/tests/build_tests/source_map/src/Demo.res +++ b/tests/build_tests/source_map/src/Demo.res @@ -2,6 +2,11 @@ let add = (a, b) => a + b exception PureMarker +type item = + | Empty + | Single(int) + | Pair(int, int) + let crash = () => Js.Exn.raiseError("source map test") let debugStatement = () => { @@ -15,4 +20,19 @@ let debugValue = () => { add(3, 4) } -let value = add(20, 22) +let applyPipe = (input, fn) => input->fn(10) + +let pipedValue = applyPipe(5, add) + +let describeItem = item => + switch item { + | Empty => "empty" + | Single(value) => Int.toString(value) + | Pair(left, right) => Int.toString(left + right) + } + +let unicodeMessage = "ν•œκΈ€ 🌏" + +let unicodeCrash = () => Js.Exn.raiseError(unicodeMessage) + +let value = add(pipedValue, 22)