Skip to content

Commit 273c77a

Browse files
Sourcemap support for wasm
Implement mapping between source and wasm locations. To work, this requires a version of Binaryen compiled with Jérôme's patch WebAssembly/binaryen#6372. Single-stepping can jump around in slightly surprising ways in the OCaml code, due to the different order of operations in wasm. This could be improved by modifying Binaryen to support “no location” annotations. Another future improvement can be to support mapping Wasm identifiers to OCaml ones. Co-authored-by: Jérôme Vouillon <jerome.vouillon@gmail.com>
1 parent 5ca926f commit 273c77a

19 files changed

+491
-330
lines changed

compiler/bin-wasm_of_ocaml/cmd_arg.ml

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ type t =
2828
; runtime_files : string list
2929
; output_file : string * bool
3030
; input_file : string
31+
; enable_source_maps : bool
3132
; params : (string * string) list
3233
}
3334

@@ -50,11 +51,11 @@ let options =
5051
Arg.(value & opt (some (enum profile)) None & info [ "opt" ] ~docv:"NUM" ~doc)
5152
in
5253
let no_sourcemap =
53-
let doc = "Currently ignored (for compatibility with Js_of_ocaml)." in
54+
let doc = "Disable sourcemap output." in
5455
Arg.(value & flag & info [ "no-sourcemap"; "no-source-map" ] ~doc)
5556
in
5657
let sourcemap =
57-
let doc = "Currently ignored (for compatibility with Js_of_ocaml)." in
58+
let doc = "Output source locations in a separate sourcemap file." in
5859
Arg.(value & flag & info [ "sourcemap"; "source-map" ] ~doc)
5960
in
6061
let sourcemap_inline_in_js =
@@ -69,24 +70,41 @@ let options =
6970
& opt_all (list (pair ~sep:'=' (enum all) string)) []
7071
& info [ "set" ] ~docv:"PARAM=VALUE" ~doc)
7172
in
72-
let build_t common set_param profile _ _ _ output_file input_file runtime_files =
73+
let build_t
74+
common
75+
set_param
76+
profile
77+
sourcemap
78+
no_sourcemap
79+
_
80+
output_file
81+
input_file
82+
runtime_files =
7383
let chop_extension s = try Filename.chop_extension s with Invalid_argument _ -> s in
7484
let output_file =
7585
match output_file with
7686
| Some s -> s, true
7787
| None -> chop_extension input_file ^ ".js", false
7888
in
7989
let params : (string * string) list = List.flatten set_param in
80-
`Ok { common; params; profile; output_file; input_file; runtime_files }
90+
let enable_source_maps = not no_sourcemap && sourcemap in
91+
`Ok {
92+
common;
93+
params;
94+
profile;
95+
output_file;
96+
input_file;
97+
runtime_files;
98+
enable_source_maps }
8199
in
82100
let t =
83101
Term.(
84102
const build_t
85103
$ Jsoo_cmdline.Arg.t
86104
$ set_param
87105
$ profile
88-
$ no_sourcemap
89106
$ sourcemap
107+
$ no_sourcemap
90108
$ sourcemap_inline_in_js
91109
$ output_file
92110
$ input_file

compiler/bin-wasm_of_ocaml/cmd_arg.mli

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ type t =
2626
; runtime_files : string list
2727
; output_file : string * bool
2828
; input_file : string
29+
; enable_source_maps : bool
2930
; params : (string * string) list
3031
}
3132

compiler/bin-wasm_of_ocaml/compile.ml

Lines changed: 69 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -83,15 +83,19 @@ let common_binaryen_options () =
8383
in
8484
if Config.Flag.pretty () then "-g" :: l else l
8585

86-
let link runtime_files input_file output_file =
86+
let link ~enable_source_maps runtime_files input_file output_file =
8787
command
8888
("wasm-merge"
8989
:: (common_binaryen_options ()
9090
@ List.flatten
9191
(List.map
92-
~f:(fun runtime_file -> [ Filename.quote runtime_file; "env" ])
92+
~f:(fun runtime_file ->
93+
[ Filename.quote runtime_file; "env" ])
9394
runtime_files)
94-
@ [ Filename.quote input_file; "exec"; "-o"; Filename.quote output_file ]))
95+
@ [ Filename.quote input_file; "exec"; "-o"; Filename.quote output_file ]
96+
@ (if enable_source_maps then
97+
[ "--output-source-map"; Filename.quote (output_file ^ ".map") ]
98+
else [])))
9599

96100
let generate_dependencies primitives =
97101
Yojson.Basic.to_string
@@ -119,7 +123,7 @@ let filter_unused_primitives primitives usage_file =
119123
with End_of_file -> ());
120124
!s
121125

122-
let dead_code_elimination in_file out_file =
126+
let dead_code_elimination ~enable_source_maps in_file out_file =
123127
with_intermediate_file (Filename.temp_file "deps" ".json")
124128
@@ fun deps_file ->
125129
with_intermediate_file (Filename.temp_file "usage" ".txt")
@@ -131,21 +135,27 @@ let dead_code_elimination in_file out_file =
131135
:: (common_binaryen_options ()
132136
@ [ "--graph-file"
133137
; Filename.quote deps_file
134-
; Filename.quote in_file
135-
; "-o"
136-
; Filename.quote out_file
137-
; ">"
138+
; Filename.quote in_file ]
139+
@ (if enable_source_maps then
140+
[ "--input-source-map"; Filename.quote (in_file ^ ".map") ]
141+
else [])
142+
@ [ "-o"
143+
; Filename.quote out_file ]
144+
@ (if enable_source_maps then
145+
[ "--output-source-map"; Filename.quote (out_file ^ ".map") ]
146+
else [])
147+
@ [ ">"
138148
; Filename.quote usage_file
139149
]));
140150
filter_unused_primitives primitives usage_file
141151

142152
let optimization_options =
143-
[| [ "-O2"; "--skip-pass=inlining-optimizing" ]
153+
[| [ "--simplify-locals-notee-nostructure"; "--vacuum"; "--reorder-locals"]
144154
; [ "-O2"; "--skip-pass=inlining-optimizing"; "--traps-never-happen" ]
145155
; [ "-O3"; "--traps-never-happen" ]
146156
|]
147157

148-
let optimize ~profile in_file out_file =
158+
let optimize ~profile ?sourcemap_file in_file out_file =
149159
let level =
150160
match profile with
151161
| None -> 1
@@ -155,19 +165,48 @@ let optimize ~profile in_file out_file =
155165
("wasm-opt"
156166
:: (common_binaryen_options ()
157167
@ optimization_options.(level - 1)
158-
@ [ Filename.quote in_file; "-o"; Filename.quote out_file ]))
168+
@ [ Filename.quote in_file; "-o"; Filename.quote out_file ])
169+
@ (match sourcemap_file with
170+
| Some sourcemap_file ->
171+
[ "--input-source-map"
172+
; Filename.quote (in_file ^ ".map")
173+
; "--output-source-map"
174+
; Filename.quote sourcemap_file
175+
; "--output-source-map-url"
176+
; Filename.quote sourcemap_file ]
177+
| None -> []))
159178

160-
let link_and_optimize ~profile runtime_wasm_files wat_file output_file =
179+
let link_and_optimize
180+
~profile
181+
?sourcemap_file
182+
runtime_wasm_files
183+
wat_file
184+
output_file =
185+
let enable_source_maps = Option.is_some sourcemap_file in
161186
with_intermediate_file (Filename.temp_file "runtime" ".wasm")
162187
@@ fun runtime_file ->
163188
write_file runtime_file Wa_runtime.wasm_runtime;
164189
with_intermediate_file (Filename.temp_file "wasm-merged" ".wasm")
165190
@@ fun temp_file ->
166-
link (runtime_file :: runtime_wasm_files) wat_file temp_file;
191+
link ~enable_source_maps (runtime_file :: runtime_wasm_files) wat_file temp_file;
167192
with_intermediate_file (Filename.temp_file "wasm-dce" ".wasm")
168193
@@ fun temp_file' ->
169-
let primitives = dead_code_elimination temp_file temp_file' in
170-
optimize ~profile temp_file' output_file;
194+
let primitives = dead_code_elimination ~enable_source_maps temp_file temp_file' in
195+
optimize ~profile ?sourcemap_file temp_file' output_file;
196+
(* Add source file contents to source map *)
197+
Option.iter sourcemap_file ~f:(fun sourcemap_file ->
198+
let open Source_map in
199+
let source_map, mappings = Source_map_io.of_file_no_mappings sourcemap_file in
200+
assert (List.is_empty (Option.value source_map.sources_content ~default:[]));
201+
let sources_content =
202+
Some (
203+
List.map source_map.sources ~f:(fun file ->
204+
if Sys.file_exists file && not (Sys.is_directory file) then
205+
Some (Fs.read_file file)
206+
else None))
207+
in
208+
let source_map = { source_map with sources_content } in
209+
Source_map_io.to_file ?mappings source_map ~file:sourcemap_file);
171210
primitives
172211

173212
let escape_string s =
@@ -274,7 +313,14 @@ let build_js_runtime primitives (strings, fragments) wasm_file output_file =
274313
^ trim_semi (Buffer.contents fragment_buffer)
275314
^ String.sub s ~pos:(k + 7) ~len:(String.length s - k - 7))
276315

277-
let run { Cmd_arg.common; profile; runtime_files; input_file; output_file; params } =
316+
let run {
317+
Cmd_arg.common;
318+
profile;
319+
runtime_files;
320+
input_file;
321+
output_file;
322+
enable_source_maps;
323+
params } =
278324
Jsoo_cmdline.Arg.eval common;
279325
Wa_generate.init ();
280326
let output_file = fst output_file in
@@ -364,7 +410,13 @@ let run { Cmd_arg.common; profile; runtime_files; input_file; output_file; param
364410
@@ fun tmp_wasm_file ->
365411
let strings = output_gen wat_file (output code ~standalone:true) in
366412
let primitives =
367-
link_and_optimize ~profile runtime_wasm_files wat_file tmp_wasm_file
413+
link_and_optimize
414+
~profile
415+
?sourcemap_file:
416+
(if enable_source_maps then Some (wasm_file ^ ".map") else None)
417+
runtime_wasm_files
418+
wat_file
419+
tmp_wasm_file
368420
in
369421
build_js_runtime primitives strings wasm_file output_file
370422
| `Cmo _ | `Cma _ -> assert false);

compiler/lib/driver.ml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -618,7 +618,7 @@ let full ~target ~standalone ~wrap_with_fun ~profile ~linkall ~source_map d p =
618618
source_map, ([], [])
619619
| `Wasm ch ->
620620
let (p, live_vars), _, in_cps = r in
621-
None, Wa_generate.f ch ~live_vars ~in_cps p
621+
None, Wa_generate.f ~debug:d ch ~live_vars ~in_cps p
622622

623623
let full_no_source_map ~target ~standalone ~wrap_with_fun ~profile ~linkall d p =
624624
let (_ : Source_map.t option * _) =

compiler/lib/generate.ml

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -341,11 +341,14 @@ let bool e = J.ECond (e, one, zero)
341341

342342
(****)
343343

344-
let source_location ctx ?force (pc : Code.loc) =
345-
match Parse_bytecode.Debug.find_loc ctx.Ctx.debug ?force pc with
344+
let source_location debug ?force (pc : Code.loc) =
345+
match Parse_bytecode.Debug.find_loc debug ?force pc with
346346
| Some pi -> J.Pi pi
347347
| None -> J.N
348348

349+
let source_location_ctx ctx ?force (pc : Code.loc) =
350+
source_location ctx.Ctx.debug ?force pc
351+
349352
(****)
350353

351354
let float_const f = J.ENum (J.Num.of_float f)
@@ -1240,13 +1243,13 @@ let rec translate_expr ctx queue loc x e level : _ * J.statement_list =
12401243
let (px, cx), queue = access_queue queue x in
12411244
(Mlvalue.Block.field cx n, or_p px mutable_p, queue), []
12421245
| Closure (args, ((pc, _) as cont)) ->
1243-
let loc = source_location ctx ~force:After (After pc) in
1246+
let loc = source_location_ctx ctx ~force:After (After pc) in
12441247
let clo = compile_closure ctx cont in
12451248
let clo =
12461249
match clo with
12471250
| (st, x) :: rem ->
12481251
let loc =
1249-
match x, source_location ctx (Before pc) with
1252+
match x, source_location_ctx ctx (Before pc) with
12501253
| (J.U | J.N), (J.U | J.N) -> J.U
12511254
| x, (J.U | J.N) -> x
12521255
| (J.U | J.N), x -> x
@@ -1495,14 +1498,14 @@ and translate_instr ctx expr_queue instr =
14951498
let instr, pc = instr in
14961499
match instr with
14971500
| Assign (x, y) ->
1498-
let loc = source_location ctx pc in
1501+
let loc = source_location_ctx ctx pc in
14991502
let (_py, cy), expr_queue = access_queue expr_queue y in
15001503
flush_queue
15011504
expr_queue
15021505
mutator_p
15031506
[ J.Expression_statement (J.EBin (J.Eq, J.EVar (J.V x), cy)), loc ]
15041507
| Let (x, e) -> (
1505-
let loc = source_location ctx pc in
1508+
let loc = source_location_ctx ctx pc in
15061509
let (ce, prop, expr_queue), instrs = translate_expr ctx expr_queue loc x e 0 in
15071510
let keep_name x =
15081511
match Code.Var.get_name x with
@@ -1533,23 +1536,23 @@ and translate_instr ctx expr_queue instr =
15331536
prop
15341537
(instrs @ [ J.variable_declaration [ J.V x, (ce, loc) ], loc ]))
15351538
| Set_field (x, n, y) ->
1536-
let loc = source_location ctx pc in
1539+
let loc = source_location_ctx ctx pc in
15371540
let (_px, cx), expr_queue = access_queue expr_queue x in
15381541
let (_py, cy), expr_queue = access_queue expr_queue y in
15391542
flush_queue
15401543
expr_queue
15411544
mutator_p
15421545
[ J.Expression_statement (J.EBin (J.Eq, Mlvalue.Block.field cx n, cy)), loc ]
15431546
| Offset_ref (x, 1) ->
1544-
let loc = source_location ctx pc in
1547+
let loc = source_location_ctx ctx pc in
15451548
(* FIX: may overflow.. *)
15461549
let (_px, cx), expr_queue = access_queue expr_queue x in
15471550
flush_queue
15481551
expr_queue
15491552
mutator_p
15501553
[ J.Expression_statement (J.EUn (J.IncrA, Mlvalue.Block.field cx 0)), loc ]
15511554
| Offset_ref (x, n) ->
1552-
let loc = source_location ctx pc in
1555+
let loc = source_location_ctx ctx pc in
15531556
(* FIX: may overflow.. *)
15541557
let (_px, cx), expr_queue = access_queue expr_queue x in
15551558
flush_queue
@@ -1558,7 +1561,7 @@ and translate_instr ctx expr_queue instr =
15581561
[ J.Expression_statement (J.EBin (J.PlusEq, Mlvalue.Block.field cx 0, int n)), loc
15591562
]
15601563
| Array_set (x, y, z) ->
1561-
let loc = source_location ctx pc in
1564+
let loc = source_location_ctx ctx pc in
15621565
let (_px, cx), expr_queue = access_queue expr_queue x in
15631566
let (_py, cy), expr_queue = access_queue expr_queue y in
15641567
let (_pz, cz), expr_queue = access_queue expr_queue z in
@@ -1619,7 +1622,7 @@ and compile_block st queue (pc : Addr.t) loop_stack frontier interm =
16191622
else (
16201623
if debug () then Format.eprintf "break;@;}@]@,";
16211624
body @ [ J.Break_statement None, J.N ])) )
1622-
, source_location st.ctx (Code.location_of_pc pc) )
1625+
, source_location_ctx st.ctx (Code.location_of_pc pc) )
16231626
in
16241627
let label = if !lab_used then Some lab else None in
16251628
let for_loop =
@@ -1854,7 +1857,7 @@ and compile_conditional st queue last loop_stack backs frontier interm =
18541857
| Stop -> Format.eprintf "stop;@;"
18551858
| Cond (x, _, _) -> Format.eprintf "@[<hv 2>cond(%a){@;" Code.Var.print x
18561859
| Switch (x, _, _) -> Format.eprintf "@[<hv 2>switch(%a){@;" Code.Var.print x);
1857-
let loc = source_location st.ctx pc in
1860+
let loc = source_location_ctx st.ctx pc in
18581861
let res =
18591862
match last with
18601863
| Return x ->

compiler/lib/generate.mli

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,9 @@ val f :
2929
-> Javascript.program
3030

3131
val init : unit -> unit
32+
33+
val source_location :
34+
Parse_bytecode.Debug.t
35+
-> ?force:Parse_bytecode.Debug.force
36+
-> Code.loc
37+
-> Javascript.location

compiler/lib/link_js.ml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -469,7 +469,7 @@ let link ~output ~linkall ~mklib ~toplevel ~files ~resolve_sourcemap_url ~source
469469
let s = sourceMappingURL_base64 ^ Base64.encode_exn data in
470470
Line_writer.write oc s
471471
| Some file ->
472-
Source_map_io.to_file sm file;
472+
Source_map_io.to_file sm ~file;
473473
let s = sourceMappingURL ^ Filename.basename file in
474474
Line_writer.write oc s));
475475
if times () then Format.eprintf " sourcemap: %a@." Timer.print t

compiler/lib/source_map_io.mli

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@ val enabled : bool
2323

2424
val to_string : t -> string
2525

26-
val to_file : t -> string -> unit
27-
2826
val of_string : string -> t
27+
28+
(** Read source map from a file without parsing the mappings (which can be costly). The
29+
[mappings] field is returned empty and the raw string is returned alongside the map.
30+
*)
31+
val of_file_no_mappings : string -> t * string option
32+
33+
(** Write to a file. If a string is supplied as [mappings], use it instead of the
34+
sourcemap's [mappings]. *)
35+
val to_file : ?mappings:string -> t -> file:string -> unit

0 commit comments

Comments
 (0)