Skip to content

Commit 2ef64a2

Browse files
authored
Compiler: add js_of_ocaml-check-runtime tool (#1121)
* Compiler: add js_of_ocaml-check-primitives tool * Compiler: compat for List.concat_map * Changes
1 parent 23b25ca commit 2ef64a2

File tree

12 files changed

+493
-38
lines changed

12 files changed

+493
-38
lines changed

CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# ??? (??) - ??
22
## Features/Changes
33
* Compiler: add support for OCaml 4.13
4+
* Compiler: new tool to check for missing primitives
45
* Lib: add offsetX and offsetY to Dom_html.mouseEvent
56
* Lib: add innerText property for Dom_html
67
* Runtime: add dummy implementation for many dummy primitives
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
(* Js_of_ocaml compiler
2+
* http://www.ocsigen.org/js_of_ocaml/
3+
* Copyright (C) 2021 Hugo Heuzard
4+
*
5+
* This program is free software; you can redistribute it and/or modify
6+
* it under the terms of the GNU Lesser General Public License as published by
7+
* the Free Software Foundation, with linking exception;
8+
* either version 2.1 of the License, or (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU Lesser General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU Lesser General Public License
16+
* along with this program; if not, write to the Free Software
17+
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
18+
*)
19+
20+
open! Js_of_ocaml_compiler.Stdlib
21+
open Js_of_ocaml_compiler
22+
23+
let group_by_snd l =
24+
l
25+
|> List.sort_uniq ~compare:(fun (n1, l1) (n2, l2) ->
26+
match Poly.compare l1 l2 with
27+
| 0 -> String.compare n1 n2
28+
| c -> c)
29+
|> List.group ~f:(fun (_, g1) (_, g2) -> Poly.equal g1 g2)
30+
31+
let print_groups output l =
32+
List.iter l ~f:(fun group ->
33+
match group with
34+
| [] -> assert false
35+
| (_, loc) :: _ ->
36+
(match loc with
37+
| [] -> ()
38+
| loc ->
39+
output_string
40+
output
41+
(Printf.sprintf "\nFrom %s:\n" (String.concat ~sep:"," loc)));
42+
List.iter group ~f:(fun (name, _) ->
43+
output_string output (Printf.sprintf "%s\n" name)))
44+
45+
let f (runtime_files, bytecode) =
46+
let runtime_files, builtin =
47+
List.partition_map runtime_files ~f:(fun name ->
48+
match Builtins.find name with
49+
| Some t -> `Snd t
50+
| None -> `Fst name)
51+
in
52+
let builtin = if false then builtin else Jsoo_runtime.runtime @ builtin in
53+
List.iter builtin ~f:(fun t ->
54+
let filename = Builtins.File.name t in
55+
let runtimes = Linker.parse_builtin t in
56+
List.iter runtimes ~f:(Linker.load_fragment ~filename));
57+
Linker.load_files runtime_files;
58+
let all_prims =
59+
List.concat_map bytecode ~f:(fun f ->
60+
let ic = open_in_bin f in
61+
let prims =
62+
match Parse_bytecode.from_channel ic with
63+
| `Cmo x -> x.Cmo_format.cu_primitives
64+
| `Cma x ->
65+
List.concat_map
66+
~f:(fun x -> x.Cmo_format.cu_primitives)
67+
x.Cmo_format.lib_units
68+
| `Exe ->
69+
let toc = Parse_bytecode.Toc.read ic in
70+
Parse_bytecode.read_primitives toc ic
71+
in
72+
close_in ic;
73+
List.map ~f:(fun p -> p, f) prims)
74+
in
75+
let _percent_prim, needed =
76+
List.partition all_prims ~f:(fun (x, _) -> Char.equal (String.get x 0) '%')
77+
in
78+
let origin =
79+
List.fold_left
80+
~f:(fun acc (x, y) ->
81+
let l = try StringMap.find x acc with Not_found -> [] in
82+
StringMap.add x (y :: l) acc)
83+
~init:StringMap.empty
84+
needed
85+
in
86+
let needed = StringSet.of_list (List.map ~f:fst needed) in
87+
let from_runtime1 = Linker.get_provided () in
88+
let from_runtime2 = Primitive.get_external () in
89+
(* [from_runtime2] is a superset of [from_runtime1].
90+
Extra primitives are registered on the ocaml side (e.g. generate.ml) *)
91+
assert (StringSet.is_empty (StringSet.diff from_runtime1 from_runtime2));
92+
let missing' = StringSet.diff needed from_runtime1 in
93+
let all_used, missing =
94+
let state = Linker.init () in
95+
let state, missing = Linker.resolve_deps state needed in
96+
StringSet.of_list (Linker.all state), missing
97+
in
98+
assert (StringSet.equal missing missing');
99+
let extra =
100+
StringSet.diff from_runtime1 all_used
101+
|> StringSet.elements
102+
|> List.map ~f:(fun name ->
103+
( name
104+
, match Linker.origin ~name with
105+
| None -> []
106+
| Some x -> [ x ] ))
107+
|> group_by_snd
108+
in
109+
110+
let missing_for_real =
111+
StringSet.diff missing from_runtime2
112+
|> StringSet.elements
113+
|> List.map ~f:(fun x -> x, StringMap.find x origin)
114+
|> group_by_snd
115+
in
116+
117+
let output = stdout in
118+
output_string output "Missing\n";
119+
output_string output "-------\n";
120+
print_groups output missing_for_real;
121+
output_string output "\n";
122+
output_string output "Unused\n";
123+
output_string output "-------\n";
124+
print_groups output extra;
125+
output_string output "\n";
126+
()
127+
128+
let options =
129+
let open Cmdliner in
130+
(* TODO: add flags to only display missing or extra primitives *)
131+
let files =
132+
let doc = "Bytecode and JavaScript files [$(docv)]. " in
133+
Arg.(value & pos_all string [] & info [] ~docv:"FILES" ~doc)
134+
in
135+
let build_t files =
136+
let files = List.partition files ~f:(fun file -> Filename.check_suffix file ".js") in
137+
`Ok files
138+
in
139+
let t = Term.(pure build_t $ files) in
140+
Term.ret t
141+
142+
let info =
143+
Info.make
144+
~name:"check-runtime"
145+
~doc:"Check runtime"
146+
~description:"js_of_ocaml-check-runtime checks the runtime."
147+
148+
let command = Cmdliner.Term.(pure f $ options), info

compiler/bin-js_of_ocaml/js_of_ocaml.ml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ let _ =
5353
; Build_fs.command
5454
; Build_runtime.command
5555
; Print_runtime.command
56+
; Check_runtime.command
5657
; Compile.command
5758
]
5859
with

compiler/lib/linker.ml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,3 +490,9 @@ let all state =
490490
with Not_found -> acc)
491491
state.ids
492492
[]
493+
494+
let origin ~name =
495+
try
496+
let _, ploc, _ = Hashtbl.find provided name in
497+
Option.bind ploc ~f:(fun ploc -> ploc.Parse_info.src)
498+
with Not_found -> None

compiler/lib/linker.mli

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,5 @@ val link : Javascript.program -> state -> output
6262
val get_provided : unit -> StringSet.t
6363

6464
val all : state -> string list
65+
66+
val origin : name:string -> string option

compiler/lib/parse_bytecode.ml

Lines changed: 84 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -2142,30 +2142,78 @@ let override_global =
21422142

21432143
(* HACK END *)
21442144

2145-
let seek_section toc ic name =
2146-
let rec seek_sec curr_ofs = function
2147-
| [] -> raise Not_found
2148-
| (n, len) :: rem ->
2149-
if String.equal n name
2150-
then (
2151-
seek_in ic (curr_ofs - len);
2152-
len)
2153-
else seek_sec (curr_ofs - len) rem
2154-
in
2155-
seek_sec (in_channel_length ic - 16 - (8 * List.length toc)) toc
2156-
2157-
let read_toc ic =
2158-
let pos_trailer = in_channel_length ic - 16 in
2159-
seek_in ic pos_trailer;
2160-
let num_sections = input_binary_int ic in
2161-
seek_in ic (pos_trailer - (8 * num_sections));
2162-
let section_table = ref [] in
2163-
for _i = 1 to num_sections do
2164-
let name = really_input_string ic 4 in
2165-
let len = input_binary_int ic in
2166-
section_table := (name, len) :: !section_table
2167-
done;
2168-
!section_table
2145+
module Toc : sig
2146+
type t
2147+
2148+
val read : in_channel -> t
2149+
2150+
val seek_section : t -> in_channel -> string -> int
2151+
2152+
val read_code : t -> in_channel -> string
2153+
2154+
val read_data : t -> in_channel -> Obj.t array
2155+
2156+
val read_crcs : t -> in_channel -> (string * Digest.t option) list
2157+
2158+
val read_prim : t -> in_channel -> string
2159+
2160+
val read_symb : t -> in_channel -> Ocaml_compiler.Symtable.GlobalMap.t
2161+
end = struct
2162+
type t = (string * int) list
2163+
2164+
let seek_section toc ic name =
2165+
let rec seek_sec curr_ofs = function
2166+
| [] -> raise Not_found
2167+
| (n, len) :: rem ->
2168+
if String.equal n name
2169+
then (
2170+
seek_in ic (curr_ofs - len);
2171+
len)
2172+
else seek_sec (curr_ofs - len) rem
2173+
in
2174+
seek_sec (in_channel_length ic - 16 - (8 * List.length toc)) toc
2175+
2176+
let read ic =
2177+
let pos_trailer = in_channel_length ic - 16 in
2178+
seek_in ic pos_trailer;
2179+
let num_sections = input_binary_int ic in
2180+
seek_in ic (pos_trailer - (8 * num_sections));
2181+
let section_table = ref [] in
2182+
for _i = 1 to num_sections do
2183+
let name = really_input_string ic 4 in
2184+
let len = input_binary_int ic in
2185+
section_table := (name, len) :: !section_table
2186+
done;
2187+
!section_table
2188+
2189+
let read_code toc ic =
2190+
let code_size = seek_section toc ic "CODE" in
2191+
really_input_string ic code_size
2192+
2193+
let read_data toc ic =
2194+
ignore (seek_section toc ic "DATA");
2195+
let init_data : Obj.t array = input_value ic in
2196+
init_data
2197+
2198+
let read_symb toc ic =
2199+
ignore (seek_section toc ic "SYMB");
2200+
let orig_symbols : Ocaml_compiler.Symtable.GlobalMap.t = input_value ic in
2201+
orig_symbols
2202+
2203+
let read_crcs toc ic =
2204+
ignore (seek_section toc ic "CRCS");
2205+
let orig_crcs : (string * Digest.t option) list = input_value ic in
2206+
orig_crcs
2207+
2208+
let read_prim toc ic =
2209+
let prim_size = seek_section toc ic "PRIM" in
2210+
let prim = really_input_string ic prim_size in
2211+
prim
2212+
end
2213+
2214+
let read_primitives toc ic =
2215+
let prim = Toc.read_prim toc ic in
2216+
String.split_char ~sep:'\000' prim
21692217

21702218
let from_exe
21712219
?(includes = [])
@@ -2175,19 +2223,14 @@ let from_exe
21752223
?(debug = false)
21762224
ic =
21772225
let debug_data = Debug.create ~toplevel debug in
2178-
let toc = read_toc ic in
2179-
let prim_size = seek_section toc ic "PRIM" in
2180-
let prim = really_input_string ic prim_size in
2181-
let primitive_table = Array.of_list (String.split_char ~sep:'\000' prim) in
2182-
let code_size = seek_section toc ic "CODE" in
2183-
let code = really_input_string ic code_size in
2184-
ignore (seek_section toc ic "DATA");
2185-
let init_data : Obj.t array = input_value ic in
2226+
let toc = Toc.read ic in
2227+
let primitives = read_primitives toc ic in
2228+
let primitive_table = Array.of_list primitives in
2229+
let code = Toc.read_code toc ic in
2230+
let init_data = Toc.read_data toc ic in
21862231
let init_data = Array.map ~f:Constants.parse init_data in
2187-
ignore (seek_section toc ic "SYMB");
2188-
let orig_symbols : Ocaml_compiler.Symtable.GlobalMap.t = input_value ic in
2189-
ignore (seek_section toc ic "CRCS");
2190-
let orig_crcs : (string * Digest.t option) list = input_value ic in
2232+
let orig_symbols = Toc.read_symb toc ic in
2233+
let orig_crcs = Toc.read_crcs toc ic in
21912234
let keeps =
21922235
let t = Hashtbl.create 17 in
21932236
List.iter ~f:(fun (_, s) -> Hashtbl.add t s ()) predefined_exceptions;
@@ -2213,7 +2256,7 @@ let from_exe
22132256
then ()
22142257
else
22152258
try
2216-
ignore (seek_section toc ic "DBUG");
2259+
ignore (Toc.seek_section toc ic "DBUG");
22172260
Debug.read debug_data ~crcs ~includes ic
22182261
with Not_found ->
22192262
if Debug.enabled debug_data || Debug.toplevel debug_data
@@ -2268,7 +2311,10 @@ let from_exe
22682311
then
22692312
(* Include linking information *)
22702313
let toc =
2271-
[ "SYMB", Obj.repr symbols; "CRCS", Obj.repr crcs; "PRIM", Obj.repr prim ]
2314+
[ "SYMB", Obj.repr symbols
2315+
; "CRCS", Obj.repr crcs
2316+
; "PRIM", Obj.repr (String.concat ~sep:"\000" primitives)
2317+
]
22722318
in
22732319
let gdata = Var.fresh () in
22742320
let infos =

compiler/lib/parse_bytecode.mli

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,14 @@ type one =
3838
; debug : Debug.t
3939
}
4040

41+
module Toc : sig
42+
type t
43+
44+
val read : in_channel -> t
45+
end
46+
47+
val read_primitives : Toc.t -> in_channel -> string list
48+
4149
val from_exe :
4250
?includes:string list
4351
-> ?toplevel:bool

compiler/lib/stdlib.ml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,30 @@ module List = struct
205205
else count_append tl l2 (count + 1)))
206206

207207
let append l1 l2 = count_append l1 l2 0
208+
209+
let group l ~f =
210+
let rec loop (l : 'a list) (this_group : 'a list) (acc : 'a list list) : 'a list list
211+
=
212+
match l with
213+
| [] -> List.rev (List.rev this_group :: acc)
214+
| x :: xs ->
215+
let pred = List.hd this_group in
216+
if f x pred
217+
then loop xs (x :: this_group) acc
218+
else loop xs [ x ] (List.rev this_group :: acc)
219+
in
220+
match l with
221+
| [] -> []
222+
| x :: xs -> loop xs [ x ] []
223+
224+
let concat_map ~f l =
225+
let rec aux f acc = function
226+
| [] -> rev acc
227+
| x :: l ->
228+
let xs = f x in
229+
aux f (rev_append xs acc) l
230+
in
231+
aux f [] l
208232
end
209233

210234
let ( @ ) = List.append
@@ -274,6 +298,11 @@ module Option = struct
274298
| None -> None
275299
| Some v -> Some (f v)
276300

301+
let bind ~f x =
302+
match x with
303+
| None -> None
304+
| Some v -> f v
305+
277306
let iter ~f x =
278307
match x with
279308
| None -> ()

0 commit comments

Comments
 (0)