2 * Copyright (c) 2017, Facebook, Inc.
5 * This source code is licensed under the MIT license found in the
6 * LICENSE file in the "hack" directory of this source tree.
12 module SyntaxError
= Full_fidelity_syntax_error
13 module SourceText
= Full_fidelity_source_text
15 Full_fidelity_syntax_tree.WithSyntax
(Full_fidelity_positioned_syntax
)
16 module Logger
= HackcEventLogger
18 (*****************************************************************************)
19 (* Types, constants *)
20 (*****************************************************************************)
25 let is_mode_cli = function
29 let is_mode_daemon = function
36 config_list
: string list
;
38 output_file
: string option;
39 config_file
: string option;
41 input_file_list
: string option;
42 dump_symbol_refs
: bool;
46 for_debugger_eval
: bool;
47 (* below are used during Rust porting *)
48 disable_toplevel_elaboration
: bool;
51 dump_desugared_expression_trees
: bool;
54 type message_handler
= Hh_json.json
-> string -> unit
56 type message_handlers
= {
57 set_config
: message_handler
;
58 compile
: message_handler
;
59 facts
: message_handler
;
60 parse
: message_handler
;
61 error
: message_handler
;
64 (*****************************************************************************)
66 (*****************************************************************************)
71 printing_t
: float ref;
74 let new_debug_time () =
75 { parsing_t
= ref 0.0; codegen_t
= ref 0.0; printing_t
= ref 0.0 }
77 (*****************************************************************************)
79 (*****************************************************************************)
85 let is_file_path_for_evaled_code s
=
86 let s = Relative_path.to_absolute
s in
87 String_utils.string_ends_with
s ") : eval()'d code"
89 let print_compiler_version () =
91 let compiler_version_msg =
95 ("type", JSON_String
"compiler_version");
96 ("version", JSON_String
(Compiler_id.get_compiler_id
()));
99 P.printf
"%s\n%!" compiler_version_msg)
101 let assert_regular_file filename
=
103 (not
(Sys.file_exists filename
))
104 || Poly.((Unix.stat filename
).Unix.st_kind
<> Unix.S_REG
)
106 raise
(Arg.Bad
(filename ^
" not a valid file"))
108 let parse_options () =
109 let fn_ref = ref None
in
110 let want_version = ref false in
111 let fallback = ref false in
112 let debug_time = ref false in
113 let config_list = ref [] in
114 let mode = ref CLI
in
115 let output_file = ref None
in
116 let config_file = ref None
in
117 let input_file_list = ref None
in
118 let dump_symbol_refs = ref false in
119 let extract_facts = ref false in
120 let dump_config = ref false in
121 let log_stats = ref false in
122 let for_debugger_eval = ref false in
123 let disable_toplevel_elaboration = ref false in
124 let include_header = ref false in
125 let dump_desugared_expression_trees = ref false in
127 P.sprintf
"Usage: hh_single_compile (%s) filename\n" Sys.argv
.(0)
131 ("--version", Arg.Set
want_version, " print the version and do nothing");
132 ("--fallback", Arg.Set
fallback, " Enables fallback compilation");
135 " Enables debugging logging for elapsed time" );
136 ("--facts", Arg.Set
extract_facts, "Extract facts from the source code.");
138 Arg.String
(fun str
-> config_list := str
:: !config_list),
139 " Configuration: Server.Port=<value> "
141 ^
"\t\tAllows overriding config options passed on a file" );
145 assert_regular_file str
;
146 config_file := Some str
),
147 " Config file in JSON format" );
149 Arg.String
(fun str
-> output_file := Some str
),
150 " Output file. Creates it if necessary" );
152 Arg.Unit
(fun () -> mode := DAEMON
),
153 " Run a daemon which processes Hack source from standard input" );
154 ( "--input-file-list",
155 Arg.String
(fun str
-> input_file_list := Some str
),
156 " read a list of files (one per line) from the file `input-file-list'"
158 ( "--dump-symbol-refs",
159 Arg.Set
dump_symbol_refs,
160 " Dump symbol ref sections of HHAS" );
161 ("--dump-config", Arg.Set
dump_config, " Dump configuration settings");
162 ( "--enable-logging-stats",
163 Arg.Unit
(fun () -> log_stats := true),
164 " Starts logging stats" );
165 ( "--stop-logging-stats",
166 Arg.Unit
(fun () -> log_stats := false),
167 " Stop logging stats" );
168 ( "--for-debugger-eval",
169 Arg.Unit
(fun () -> for_debugger_eval := true),
170 " Mutate the program as if we're in the debugger repl" );
171 ( "--disable-toplevel-elaboration",
172 Arg.Unit
(fun () -> disable_toplevel_elaboration := true),
173 "Disable toplevel definition elaboration" );
174 ( "--include-header",
175 Arg.Unit
(fun () -> include_header := true),
176 "Include JSON header" );
177 ( "--dump-desugared-expression-trees",
178 Arg.Unit
(fun () -> dump_desugared_expression_trees := true),
179 "Print the source code with expression tree literals desugared. Best effort debugging tool."
183 let options = Arg.align ~limit
:25 options in
184 Arg.parse
options (fun fn
-> fn_ref := Some fn
) usage;
185 if !want_version then (
186 print_compiler_version ();
189 if is_mode_daemon !mode then print_compiler_version ();
190 let needs_file = Option.is_none
!input_file_list in
195 if is_mode_cli !mode then
200 if is_mode_cli !mode then
209 fallback = !fallback;
210 config_list = !config_list;
211 debug_time = !debug_time;
212 output_file = !output_file;
213 config_file = !config_file;
215 input_file_list = !input_file_list;
216 dump_symbol_refs = !dump_symbol_refs;
217 dump_config = !dump_config;
218 log_stats = !log_stats;
219 extract_facts = !extract_facts;
220 for_debugger_eval = !for_debugger_eval;
221 disable_toplevel_elaboration = !disable_toplevel_elaboration;
222 include_header = !include_header;
223 dump_desugared_expression_trees = !dump_desugared_expression_trees;
226 let fail_daemon file error
=
228 let file = Option.value ~default
:"[unknown]" file in
233 ("type", JSON_String
"error");
234 ("file", JSON_String
file);
235 ("error", JSON_String error
);
238 P.printf
"%s\n%!" msg;
241 let rec dispatch_loop handlers
=
244 let read_message () =
245 let line = Caml.read_line
() in
246 let header = json_of_string
line in
247 let file = get_field_opt
(get_string
"file") header in
248 let bytes = get_field
(get_number_int
"bytes") (fun _af
-> 0) header in
249 let body = Bytes.create
bytes in
251 Caml.really_input
Caml.stdin
body 0 bytes;
252 (header, Bytes.to_string
body)
256 ("Exception reading message body: " ^
Caml.Printexc.to_string exc
)
258 let (header, body) = read_message () in
263 fail_daemon None
("Cannot determine type of message: " ^ af
))
267 | "code" -> handlers
.compile
header body
268 | "error" -> handlers
.error
header body
269 | "config" -> handlers
.set_config
header body
270 | "facts" -> handlers
.facts
header body
271 | "parse" -> handlers
.parse
header body
272 | _
-> fail_daemon None
("Unhandled message type '" ^
msg_type ^
"'"));
273 dispatch_loop handlers
))
275 let print_debug_time_info filename
debug_time =
276 let stat = Caml.Gc.stat () in
277 P.eprintf
"File %s:\n" (Relative_path.to_absolute filename
);
278 P.eprintf
"Parsing: %0.3f s\n" !(debug_time.parsing_t
);
279 P.eprintf
"Codegen: %0.3f s\n" !(debug_time.codegen_t
);
280 P.eprintf
"Printing: %0.3f s\n" !(debug_time.printing_t
);
281 P.eprintf
"MinorWords: %0.3f\n" stat.Caml.Gc.minor_words
;
282 P.eprintf
"PromotedWords: %0.3f\n" stat.Caml.Gc.promoted_words
284 let mode_to_string = function
288 let log_success compiler_options filename
debug_time =
290 ~filename
:(Relative_path.to_absolute filename
)
291 ~parsing_t
:!(debug_time.parsing_t
)
292 ~codegen_t
:!(debug_time.codegen_t
)
293 ~printing_t
:!(debug_time.printing_t
)
294 ~
mode:(mode_to_string compiler_options
.mode)
296 let log_fail compiler_options filename exc ~stack
=
298 ~filename
:(Relative_path.to_absolute filename
)
299 ~
mode:(mode_to_string compiler_options
.mode)
300 ~exc
:(Caml.Printexc.to_string exc ^
"\n" ^ stack
)
303 (bytecode
: string list
)
304 (config
: Compile_ffi.rust_output_config
)
305 (file : Relative_path.t
)
306 (debug_time : debug_time option)
307 (log_config_json
: bool)
308 (config_jsons
: string list
)
309 (config_list : string list
) =
311 match config
.Compile_ffi.output_file with
312 | Some
file -> Sys_utils.write_strings_to_file ~
file
315 List.iter ~f
:(P.printf
"%s") c
;
318 if config
.Compile_ffi.include_header then (
320 List.fold ~f
:(fun len
s -> len
+ String.length
s) ~init
:0 bytecode
322 let abs_path = Relative_path.to_absolute
file in
326 ("bytes", int_
bytes);
327 ("file", JSON_String
abs_path);
328 ("type", JSON_String
"success");
335 (Relative_path.to_absolute
file)
341 ~f
:(fun x
-> Hh_json.JSON_String x
)
342 (config_jsons
@ config_list)) )
348 match debug_time with
350 let json_microsec t
=
351 Hh_json.int_
@@ int_of_float
@@ (t
*. 1000000.0)
353 ("parsing_time", json_microsec !(debug_time.parsing_t
))
354 :: ("codegen_time", json_microsec !(debug_time.codegen_t
))
355 :: ("printing_time", json_microsec !(debug_time.printing_t
))
359 write Hh_json.[json_to_string
@@ JSON_Object
msg];
367 (compiler_options
: options)
371 (* dummy line to load Full_fidelity_ast *)
372 let _ = Full_fidelity_ast.make_env filename
in
376 re_filepath
= filename
;
377 re_config_jsons
= List.rev config_jsons
;
378 re_config_list
= compiler_options
.config_list;
380 ( ( if is_systemlib
then
384 lor ( if is_file_path_for_evaled_code filename
then
388 lor ( if compiler_options
.for_debugger_eval then
392 lor ( if compiler_options
.dump_symbol_refs then
397 if compiler_options
.disable_toplevel_elaboration then
403 match Compile_ffi.rust_from_text_ffi
env rust_output_config source_text
with
405 | Error
msg -> raise
(Failure
msg)
407 let extract_facts ~compiler_options ~config_jsons ~filename text
=
408 let (co
, log_config_json
) =
409 Hhbc_options.apply_config_overrides_statelessly
410 compiler_options
.config_list
415 Facts_parser.extract_as_json_string
416 ~php5_compat_mode
:true
417 ~hhvm_compat_mode
:true
418 ~disable_nontoplevel_declarations
:
419 (phpism_disable_nontoplevel_declarations co
)
420 ~disable_legacy_soft_typehints
:(disable_legacy_soft_typehints co
)
421 ~allow_new_attribute_syntax
:(allow_new_attribute_syntax co
)
422 ~disable_legacy_attribute_syntax
:(disable_legacy_attribute_syntax co
)
423 ~enable_xhp_class_modifier
:(enable_xhp_class_modifier co
)
424 ~disable_xhp_element_mangling
:(disable_xhp_element_mangling co
)
427 |> Option.value ~default
:"");
432 let parse_hh_file ~config_jsons ~compiler_options filename
body =
433 let (co
, log_config_json
) =
434 Hhbc_options.apply_config_overrides_statelessly
435 compiler_options
.config_list
439 let file = Relative_path.create
Relative_path.Dummy filename
in
440 let source_text = SourceText.make
file body in
441 let mode = Full_fidelity_parser.parse_mode
source_text in
443 Full_fidelity_parser_env.make
445 ~php5_compat_mode
:true
446 ~hhvm_compat_mode
:true
447 ~disable_nontoplevel_declarations
:
448 (phpism_disable_nontoplevel_declarations co
)
449 ~disable_legacy_soft_typehints
:(disable_legacy_soft_typehints co
)
450 ~allow_new_attribute_syntax
:(allow_new_attribute_syntax co
)
451 ~disable_legacy_attribute_syntax
:(disable_legacy_attribute_syntax co
)
452 ~enable_xhp_class_modifier
:(enable_xhp_class_modifier co
)
453 ~disable_xhp_element_mangling
:(disable_xhp_element_mangling co
)
454 ~disallow_hash_comments
:(disallow_hash_comments co
)
455 ~disallow_fun_and_cls_meth_pseudo_funcs
:
456 (disallow_fun_and_cls_meth_pseudo_funcs co
)
460 let syntax_tree = SyntaxTree.make ~
env source_text in
461 let json = SyntaxTree.to_json
syntax_tree in
462 ([Hh_json.json_to_string
json], co
, log_config_json
))
464 (*****************************************************************************)
465 (* Main entry point *)
466 (*****************************************************************************)
468 let process_single_source_unit
477 if compiler_options
.extract_facts then
478 let (output
, _, log_config_json
) =
479 extract_facts ~compiler_options ~config_jsons ~filename
source_text
488 compiler_options
.config_list
496 (Full_fidelity_source_text.make filename
source_text)
498 let stack = Caml.Printexc.get_backtrace
() in
499 if compiler_options
.log_stats then
500 log_fail compiler_options filename exc ~
stack;
501 handle_exception filename exc
503 let desugar_and_print_expr_trees
504 ~config_jsons ~compiler_options
(filename
: string) : unit =
505 let rel_path = Relative_path.create
Relative_path.Dummy filename
in
509 re_filepath
= rel_path;
510 re_config_jsons
= List.rev config_jsons
;
511 re_config_list
= compiler_options
.config_list;
513 ( ( if is_file_path_for_evaled_code rel_path then
517 lor ( if compiler_options
.for_debugger_eval then
521 lor ( if compiler_options
.dump_symbol_refs then
526 if compiler_options
.disable_toplevel_elaboration then
532 Compile_ffi.desugar_and_print_expr_trees env
534 let decl_and_run_mode compiler_options
=
537 (* list of pending config JSONs *)
538 let config_jsons = ref [] in
539 let add_config (config_json
: string option) =
540 config_jsons := config_json
:: !config_jsons
544 match !config_jsons with
545 | _ :: old_config_jsons
-> old_config_jsons
548 let get_config_jsons () = List.filter_map ~f
:(fun x
-> x
) !config_jsons in
549 let ini_config_json : string option =
551 ~f
:(fun path
-> (try Sys_utils.cat path
with _ -> ""))
552 compiler_options
.config_file
554 add_config ini_config_json;
555 Ident.track_names
:= true;
557 match compiler_options
.mode with
559 let add_config_overrides header =
560 let config_overrides =
562 (get_obj
"config_overrides")
563 (fun _af
-> JSON_Object
[])
565 |> Hh_json.json_to_string
567 add_config (Some
config_overrides)
569 let get_filename_and_path (header : json) =
576 ("Cannot determine file name of source unit: " ^ af
))
579 (filename, Relative_path.create
Relative_path.Dummy
filename)
581 let body_or_file_contents (body : string) (filename : string) =
582 (* if body is empty - read file from disk *)
583 if String.length
body = 0 then
584 Sys_utils.cat
filename
597 Compile_ffi.{ include_header = true; output_file = None
}
599 ( if Hhbc_options.log_extern_compiler_perf hhbc_options
then
604 (get_config_jsons ())
605 compiler_options
.config_list
607 let handle_exception filename exc
=
608 let abs_path = Relative_path.to_absolute
filename in
613 ("type", JSON_String
"error");
614 ("file", JSON_String
abs_path);
615 ("error", JSON_String
(Caml.Printexc.to_string exc
));
618 P.printf
"%s\n%!" msg
625 if String.is_empty
body then
630 add_config config_json);
633 let (filename, _) = get_filename_and_path header in
640 "No 'error' field in error message")
645 ("Error processing " ^
filename ^
": " ^
error));
648 let (_, path
) = get_filename_and_path header in
650 get_field_opt
(get_bool
"is_systemlib") header
651 |> Option.value ~default
:false
653 let for_debugger_eval =
655 (get_bool
"for_debugger_eval")
657 fail_daemon None
("for_debugger_eval flag missing: " ^ af
))
660 add_config_overrides header;
661 let compiler_options =
662 { compiler_options with for_debugger_eval }
665 process_single_source_unit
667 ~
config_jsons:(get_config_jsons ())
669 Compile_ffi.{ include_header = true; output_file = None
}
678 (let (filename, path
) = get_filename_and_path header in
679 let body = body_or_file_contents body filename in
680 add_config_overrides header;
681 let (output
, hhbc_options
, log_config_json
) =
684 ~
config_jsons:(get_config_jsons ())
698 (new_debug_time ()));
701 (let (filename, path
) = get_filename_and_path header in
702 let body = body_or_file_contents body filename in
703 add_config_overrides header;
704 let (output
, hhbc_options
, log_config_json
) =
706 ~
config_jsons:(get_config_jsons ())
721 (new_debug_time ()));
724 dispatch_loop handlers
726 let handle_exception filename exc
=
727 let stack = Caml.Printexc.get_backtrace
() in
730 "Error in file %s: %s\n"
731 (Relative_path.to_absolute
filename)
732 (Caml.Printexc.to_string exc
)
734 let process_single_file output_file filename =
735 let filename = Relative_path.create
Relative_path.Dummy
filename in
736 (* let abs_path = Relative_path.to_absolute filename in *)
737 let files = Multifile.file_to_file_list
filename in
738 List.iter
files ~f
:(fun (filename, content
) ->
739 process_single_source_unit
741 ~
config_jsons:(get_config_jsons ())
745 include_header = compiler_options.include_header;
752 let (filenames
, output_file) =
753 match compiler_options.input_file_list with
754 (* List of source files explicitly given *)
755 | Some
input_file_list ->
756 let get_lines_in_file filename =
757 let inch = Caml.open_in
filename in
759 match Caml.input_line
inch with
760 | line -> go (Caml.String.trim
line :: lines
)
761 | exception End_of_file
-> lines
765 (get_lines_in_file input_file_list, None
)
766 | None
-> ([compiler_options.filename], compiler_options.output_file)
767 (* Actually execute the compilation(s) *)
769 if compiler_options.dump_config then
771 "===CONFIG===\n%s\n\n%!"
772 ( Hhbc_options.apply_config_overrides_statelessly
773 compiler_options.config_list
774 (get_config_jsons ())
776 |> Hhbc_options.to_string
);
778 (not
(String.is_empty
compiler_options.filename))
779 && Sys.is_directory
compiler_options.filename
782 "%s is a directory, directory is not supported."
783 compiler_options.filename
786 if compiler_options.dump_desugared_expression_trees then
787 desugar_and_print_expr_trees
788 ~
config_jsons:(get_config_jsons ())
791 process_single_file output_file
793 List.iter filenames
process_fn))
796 let start_time = Unix.gettimeofday
() in
797 if opts
.log_stats then Logger.init
start_time;
798 decl_and_run_mode opts
800 (* command line driver *)
802 Printexc.record_backtrace
true;
804 if !Sys.interactive
then
807 (* On windows, setting 'binary mode' avoids to output CRLF on
808 stdout. The 'text mode' would not hurt the user in general, but
809 it breaks the testsuite where the output is compared to the
810 expected one (i.e. in given file without CRLF). *)
811 Caml.set_binary_mode_out
Caml.stdout
true;
812 let handle = SharedMem.init ~num_workers
:0 SharedMem.empty_config
in
813 ignore
(handle : SharedMem.handle);
814 let options = parse_options () in
817 let stack = Caml.Printexc.get_backtrace
() in
819 die (Caml.Printexc.to_string exc
)