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.
14 module SyntaxError
= Full_fidelity_syntax_error
15 module SourceText
= Full_fidelity_source_text
16 module Logger
= HackcEventLogger
18 (*****************************************************************************)
19 (* Types, constants *)
20 (*****************************************************************************)
32 config_list
: string list
;
35 output_file
: string option;
36 config_file
: string option;
39 input_file_list
: string option;
40 dump_symbol_refs
: bool;
47 type message_handler
= Hh_json.json
-> string -> unit
49 type message_handlers
= {
50 set_config
: message_handler
;
51 compile
: message_handler
;
52 facts
: message_handler
;
53 error
: message_handler
;
56 (*****************************************************************************)
58 (*****************************************************************************)
63 printing_t
: float ref;
66 let new_debug_time () =
73 (*****************************************************************************)
75 (*****************************************************************************)
81 let is_file_path_for_evaled_code s
=
82 let s = Relative_path.to_absolute
s in
83 String_utils.string_ends_with
s ") : eval()'d code"
85 let print_compiler_version () =
87 let compiler_version_msg = json_to_string
@@ JSON_Object
88 [ ("type", JSON_String
"compiler_version")
89 ; ("version", JSON_String
(Compiler_id.get_compiler_id
()))
91 P.printf
"%s\n%!" compiler_version_msg
93 let assert_regular_file filename
=
94 if not
(Sys.file_exists filename
) ||
95 (Unix.stat filename
).Unix.st_kind
<> Unix.S_REG
96 then raise
(Arg.Bad
(filename ^
" not a valid file"))
98 let parse_options () =
99 let fn_ref = ref None
in
100 let want_version = ref false in
101 let fallback = ref false in
102 let debug_time = ref false in
103 let parser = ref FFP
in
104 let config_list = ref [] in
105 let mode = ref CLI
in
106 let output_file = ref None
in
107 let config_file = ref None
in
108 let quiet_mode = ref false in
109 let input_file_list = ref None
in
110 let dump_symbol_refs = ref false in
111 let extract_facts = ref false in
112 let dump_stats = ref false in
113 let dump_config = ref false in
114 let log_stats = ref false in
115 let usage = P.sprintf
"Usage: hh_single_compile (%s) filename\n" Sys.argv
.(0) in
118 , Arg.Set
want_version
119 , " print the version and do nothing"
123 , " Enables fallback compilation"
127 , " Enables debugging logging for elapsed time"
131 , " Runs very quietly, and ignore any result if invoked without -o "
132 ^
"(lower priority than the debug-time option)"
135 , Arg.Set
extract_facts
136 , "Extract facts from the source code."
139 , Arg.String
(fun str
-> config_list := str
:: !config_list)
140 , " Configuration: Eval.EnableHipHopSyntax=<value> "
141 ^
"or Hack.Lang.IntsOverflowToInts=<value>"
143 ^
"\t\tAllows overriding config options passed on a file"
146 , Arg.String
(fun str
->
147 assert_regular_file str
;
148 config_file := Some str
)
149 , " Config file in JSON format"
152 , Arg.String
(fun str
-> output_file := Some str
)
153 , " Output file. Creates it if necessary"
157 (function "ffp" -> parser := FFP
158 | "legacy" -> parser := Legacy
159 | p
-> failwith
@@ p ^
" is an invalid parser")
160 , " Parser: ffp or legacy [def: ffp]"
163 , Arg.Unit
(fun () -> mode := DAEMON
)
164 , " Run a daemon which processes Hack source from standard input"
167 , Arg.String
(fun str
-> input_file_list := Some str
)
168 , " read a list of files (one per line) from the file `input-file-list'"
170 ("--dump-symbol-refs"
171 , Arg.Set
dump_symbol_refs
172 , " Dump symbol ref sections of HHAS"
176 , " Dump timing stats for functions"
179 , Arg.Set
dump_config
180 , " Dump configuration settings"
182 ("--enable-logging-stats"
183 , Arg.Unit
(fun () -> log_stats := true)
184 , " Starts logging stats"
186 ("--stop-logging-stats"
187 , Arg.Unit
(fun () -> log_stats := false)
188 , " Stop logging stats"
191 let options = Arg.align ~limit
:25 options in
192 Arg.parse
options (fun fn
-> fn_ref := Some fn
) usage;
193 if !want_version then (print_compiler_version (); exit
0);
194 if !mode = DAEMON
then print_compiler_version ();
195 let needs_file = Option.is_none
!input_file_list in
199 | Some
fn -> if !mode = CLI
then fn else die usage
200 | None
-> if !mode = CLI
then die usage else read_line
()
205 ; fallback = !fallback
206 ; config_list = !config_list
207 ; debug_time = !debug_time
209 ; output_file = !output_file
210 ; config_file = !config_file
211 ; quiet_mode = !quiet_mode
213 ; input_file_list = !input_file_list
214 ; dump_symbol_refs = !dump_symbol_refs
215 ; dump_stats = !dump_stats
216 ; dump_config = !dump_config
217 ; log_stats = !log_stats
218 ; extract_facts = !extract_facts
221 let fail_daemon file error
=
223 let file = Option.value ~default
:"[unknown]" file in
224 let msg = json_to_string
@@ JSON_Object
225 [ ("type", JSON_String
"error")
226 ; ("file", JSON_String
file)
227 ; ("error", JSON_String error
)
229 P.printf
"%s\n%!" msg;
232 let rec dispatch_loop handlers
=
235 let read_message () =
236 let line = read_line
() in
237 let header = json_of_string
line in
238 let file = get_field_opt
(get_string
"file") header in
239 let bytes = get_field
(get_number_int
"bytes") (fun _af
-> 0) header in
240 let is_systemlib = get_field_opt
(get_bool
"is_systemlib") header in
241 Emit_env.set_is_systemlib
@@ Option.value ~default
:false is_systemlib;
242 let body = Bytes.create
bytes in begin
244 really_input stdin
body 0 bytes;
247 fail_daemon file ("Exception reading message body: " ^
(Printexc.to_string exc
))
249 let header, body = read_message () in
250 let msg_type = get_field
252 (fun af
-> fail_daemon None
("Cannot determine type of message: " ^ af
))
255 | "code" -> handlers
.compile
header body
256 | "error" -> handlers
.error
header body
257 | "config" -> handlers
.set_config
header body
258 | "facts" -> handlers
.facts
header body
259 | _
-> fail_daemon None
("Unhandled message type '" ^
msg_type ^
"'"));
260 dispatch_loop handlers
262 let set_stats_if_enabled ~compiler_options
=
263 if compiler_options
.dump_stats then
264 Stats_container.set_instance
(Some
(Stats_container.new_container
()))
266 let write_stats_if_enabled ~compiler_options
=
267 if compiler_options
.dump_stats then
268 match (Stats_container.get_instance
()) with
269 | Some
s -> Stats_container.write_out ~out
:stderr
s
272 let parse_text compiler_options popt
fn text
=
273 let () = set_stats_if_enabled ~compiler_options
in
274 match compiler_options
.parser with
277 not
(Hhbc_options.source_mapping
!Hhbc_options.compiler_options
) in
278 let enable_hh_syntax =
279 Hhbc_options.enable_hiphop_syntax
!Hhbc_options.compiler_options
in
280 let php5_compat_mode =
281 not
(Hhbc_options.enable_uniform_variable_syntax
!Hhbc_options.compiler_options
) in
282 let hacksperimental =
283 Hhbc_options.hacksperimental !Hhbc_options.compiler_options
in
284 let lower_coroutines =
285 Hhbc_options.enable_coroutines
!Hhbc_options.compiler_options
in
286 let systemlib_compat_mode = Emit_env.is_systemlib () in
287 let env = Full_fidelity_ast.make_env
292 ~
systemlib_compat_mode
300 let source_text = SourceText.make
fn text
in
301 let { Full_fidelity_ast.ast
; Full_fidelity_ast.is_hh_file
; _
} =
302 Full_fidelity_ast.from_text
env source_text in
303 let () = write_stats_if_enabled ~compiler_options
in
306 let {Parser_return.ast
; Parser_return.is_hh_file
; _
} =
307 Parser_hack.program popt
fn text
in
310 let parse_file compiler_options popt filename text
=
312 `ParseResult
(Errors.do_
begin fun () ->
313 parse_text compiler_options popt filename text
316 (* FFP failed to parse *)
317 | Failure
s -> `ParseFailure
((SyntaxError.make
0 0 s), Pos.none
)
318 (* FFP generated an error *)
319 | SyntaxError.ParserFatal
(e
, p
) -> `ParseFailure
(e
, p
)
321 let add_to_time_ref r t0
=
322 let t = Unix.gettimeofday
() in
323 r
:= !r
+. (t -. t0
);
326 let print_debug_time_info filename
debug_time =
327 let stat = Gc.stat () in
328 (P.eprintf
"File %s:\n" (Relative_path.to_absolute filename
);
329 P.eprintf
"Parsing: %0.3f s\n" !(debug_time.parsing_t
);
330 P.eprintf
"Codegen: %0.3f s\n" !(debug_time.codegen_t
);
331 P.eprintf
"Printing: %0.3f s\n" !(debug_time.printing_t
);
332 P.eprintf
"MinorWords: %0.3f\n" stat.Gc.minor_words
;
333 P.eprintf
"PromotedWords: %0.3f\n" stat.Gc.promoted_words
)
335 let mode_to_string = function
339 let log_success compiler_options filename
debug_time =
341 ~filename
:(Relative_path.to_absolute filename
)
342 ~parsing_t
:!(debug_time.parsing_t
)
343 ~codegen_t
:!(debug_time.codegen_t
)
344 ~printing_t
:!(debug_time.printing_t
)
345 ~
mode:(mode_to_string compiler_options
.mode)
347 let log_fail compiler_options filename exc
=
349 ~filename
:(Relative_path.to_absolute filename
)
350 ~
mode:(mode_to_string compiler_options
.mode)
351 ~exc
:(Printexc.to_string exc ^
"\n" ^
Printexc.get_backtrace
())
354 let do_compile filename compiler_options fail_or_ast
debug_time =
355 let t = Unix.gettimeofday
() in
356 let t = add_to_time_ref debug_time.parsing_t
t in
358 match fail_or_ast
with
359 | `ParseFailure
(e
, pos
) ->
360 let error_t = match SyntaxError.error_type e
with
361 | SyntaxError.ParseError
-> Hhbc_ast.FatalOp.Parse
362 | SyntaxError.RuntimeError
-> Hhbc_ast.FatalOp.Runtime
364 let s = SyntaxError.message e
in
365 Emit_program.emit_fatal_program ~ignore_message
:false error_t pos
s
366 | `ParseResult
(errors
, (ast
, is_hh_file
)) ->
367 List.iter
(Errors.get_error_list errors
) (fun e
->
368 P.eprintf
"%s\n%!" (Errors.to_string
(Errors.to_absolute e
)));
369 if Errors.is_empty errors
370 then Emit_program.from_ast
372 (is_file_path_for_evaled_code filename
)
374 else Emit_program.emit_fatal_program ~ignore_message
:true
375 Hhbc_ast.FatalOp.Parse
Pos.none
"Syntax error"
377 let t = add_to_time_ref debug_time.codegen_t
t in
378 let hhas = Hhbc_hhas.to_segments
380 ~
dump_symbol_refs:compiler_options
.dump_symbol_refs
382 ignore
@@ add_to_time_ref debug_time.printing_t
t;
383 if compiler_options
.debug_time
384 then print_debug_time_info filename
debug_time;
385 if compiler_options
.log_stats
386 then log_success compiler_options filename
debug_time;
389 let extract_facts ?pretty text
=
390 Facts_parser.extract_as_json ~
php5_compat_mode:true ~hhvm_compat_mode
:true text
391 (* return empty string if file has syntax errors *)
392 |> Option.value_map ~default
:"" ~f
:(Hh_json.json_to_string ?pretty
)
395 (*****************************************************************************)
396 (* Main entry point *)
397 (*****************************************************************************)
399 let process_single_source_unit compiler_options popt handle_output
400 handle_exception filename
source_text =
402 let debug_time = new_debug_time () in
403 let t = Unix.gettimeofday
() in
405 if compiler_options
.extract_facts
406 then extract_facts ~pretty
:true source_text
408 let fail_or_ast = parse_file compiler_options popt filename
source_text in
409 ignore
@@ add_to_time_ref debug_time.parsing_t
t;
410 do_compile filename compiler_options
fail_or_ast debug_time
412 handle_output filename
output debug_time
414 if compiler_options
.log_stats
415 then log_fail compiler_options filename exc
;
416 handle_exception filename exc
418 let decl_and_run_mode compiler_options popt
=
422 let print_and_flush_strings strings
=
423 Core_list.iter ~f
:(P.printf
"%s") strings
;
426 let set_compiler_options config_json
=
428 Hhbc_options.get_options_from_config
430 ~init
:!Hhbc_options.compiler_options
431 ~
config_list:compiler_options
.config_list
433 Hhbc_options.set_compiler_options options in
434 let ini_config_json =
435 Option.map ~f
:json_of_file compiler_options
.config_file in
437 set_compiler_options ini_config_json;
438 let dumped_options = lazy (Hhbc_options.to_string
!Hhbc_options.compiler_options
) in
439 Ident.track_names
:= true;
441 match compiler_options
.mode with
443 let handle_output filename
output debug_time =
444 let abs_path = Relative_path.to_absolute filename
in
446 List.fold ~f
:(fun len
s -> len
+ String.length
s) ~init
:0 output in
448 [ ("type", JSON_String
"success")
449 ; ("file", JSON_String
abs_path)
450 ; ("bytes", int_
bytes)
453 if Hhbc_options.enable_perf_logging
!Hhbc_options.compiler_options
455 let json_microsec t = int_
@@ int_of_float
@@ t *. 1000000.0 in
456 ("parsing_time", json_microsec !(debug_time.parsing_t
))
457 :: ("codegen_time", json_microsec !(debug_time.codegen_t
))
458 :: ("printing_time", json_microsec !(debug_time.printing_t
))
462 P.printf
"%s\n" (json_to_string
@@ JSON_Object
msg);
463 print_and_flush_strings output in
464 let handle_exception filename exc
=
465 let abs_path = Relative_path.to_absolute filename
in
466 let msg = json_to_string
@@ JSON_Object
467 [ ("type", JSON_String
"error")
468 ; ("file", JSON_String
abs_path)
469 ; ("error", JSON_String
(Printexc.to_string exc
))
471 P.printf
"%s\n%!" msg in
473 { set_config
= (fun _header
body ->
475 if body = "" then None
else Some
(json_of_string
body) in
476 set_compiler_options config_json)
477 ; error
= (fun header _body
->
478 let filename = get_field
480 (fun af
-> fail_daemon None
("Cannot determine file name of source unit: " ^ af
))
482 let error = get_field
484 (fun _af
-> fail_daemon (Some
filename) ("No 'error' field in error message"))
486 fail_daemon (Some
filename) ("Error processing " ^
filename ^
": " ^
error))
487 ; compile
= (fun header body ->
488 let filename = get_field
490 (fun af
-> fail_daemon None
("Cannot determine file name of source unit: " ^ af
))
492 process_single_source_unit
497 (Relative_path.create
Relative_path.Dummy
filename)
499 ; facts
= (fun header body -> (
500 (* if body is empty - read file from disk *)
501 let filename = get_field
503 (fun af
-> fail_daemon None
("Cannot determine file name of source unit: " ^ af
))
506 if String.length
body = 0
507 then Sys_utils.cat
filename
510 (Relative_path.create
Relative_path.Dummy
filename)
511 (extract_facts body))
514 dispatch_loop handlers
517 let handle_exception filename exc
=
518 if not compiler_options
.quiet_mode
520 P.eprintf
"Error in file %s: %s\n"
521 (Relative_path.to_absolute
filename)
522 (Printexc.to_string exc
) in
524 let process_single_file handle_output filename =
525 let filename = Relative_path.create
Relative_path.Dummy
filename in
526 let abs_path = Relative_path.to_absolute
filename in
527 process_single_source_unit
528 compiler_options popt
handle_output handle_exception filename (cat
abs_path) in
530 let filenames, handle_output = match compiler_options
.input_file_list with
531 (* List of source files explicitly given *)
532 | Some
input_file_list ->
533 let get_lines_in_file filename =
534 let inch = open_in
filename in
536 match input_line
inch with
537 | line -> go (String.trim
line :: lines
)
538 | exception End_of_file
-> lines
in
540 let handle_output _filename
output _debug_time
=
541 if compiler_options
.dump_config then
542 Printf.printf
"===CONFIG===\n%s\n\n%!" (Lazy.force
dumped_options);
543 if not compiler_options
.quiet_mode then
544 print_and_flush_strings output
545 in get_lines_in_file input_file_list, handle_output
548 if Sys.is_directory compiler_options
.filename
550 (* Compile every file under directory *)
553 let rec go dirs
= match dirs
with
556 let ds, fs
= Sys.readdir dir
557 |> Array.map
(Filename.concat dir
)
559 |> List.partition_tf ~f
: Sys.is_directory
in
560 fs
@ go (ds @ dirs
) in
561 go [compiler_options
.filename] in
562 let handle_output filename output _debug_time
=
563 let abs_path = Relative_path.to_absolute
filename in
564 if Filename.check_suffix
abs_path ".php" then
565 let output_file = Filename.chop_suffix
abs_path ".php" ^
".hhas" in
566 if Sys.file_exists
output_file
568 if not compiler_options
.quiet_mode
569 then P.fprintf stderr
"Output file %s already exists\n" output_file)
571 Sys_utils.write_strings_to_file ~
file:output_file output
572 in files_in_dir, handle_output
574 (* Compile a single file *)
576 let handle_output _filename
output _debug_time
=
577 match compiler_options
.output_file with
578 | Some
output_file ->
579 Sys_utils.write_strings_to_file ~
file:output_file output
581 if compiler_options
.dump_config then
582 Printf.printf
"===CONFIG===\n%s\n\n%!" (Lazy.force
dumped_options);
583 if not compiler_options
.quiet_mode then
584 print_and_flush_strings output
585 in [compiler_options
.filename], handle_output
587 (* Actually execute the compilation(s) *)
588 in List.iter
filenames (process_single_file handle_output)
591 let popt = ParserOptions.default
in
592 let start_time = Unix.gettimeofday
() in
593 if opts
.log_stats then Logger.init
start_time;
594 decl_and_run_mode opts
popt
596 (* command line driver *)
598 Printexc.record_backtrace
true;
603 (* On windows, setting 'binary mode' avoids to output CRLF on
604 stdout. The 'text mode' would not hurt the user in general, but
605 it breaks the testsuite where the output is compared to the
606 expected one (i.e. in given file without CRLF). *)
607 set_binary_mode_out stdout
true;
608 let _handle = SharedMem.init
GlobalConfig.default_sharedmem_config
in
609 let options = parse_options () in
612 Printexc.get_backtrace
() |> prerr_endline
;
613 die (Printexc.to_string exc
)