Check argument to 'unset' in reactive mode
[hiphop-php.git] / hphp / hack / src / hh_single_compile.ml
blob003bc96e2ebf8555f8f198f61703f28159f6e55b
1 (**
2 * Copyright (c) 2017, Facebook, Inc.
3 * All rights reserved.
5 * This source code is licensed under the MIT license found in the
6 * LICENSE file in the "hack" directory of this source tree.
8 *)
10 open Hh_core
11 open Sys_utils
13 module P = Printf
14 module SyntaxError = Full_fidelity_syntax_error
15 module SourceText = Full_fidelity_source_text
16 module Logger = HackcEventLogger
18 (*****************************************************************************)
19 (* Types, constants *)
20 (*****************************************************************************)
21 type parser =
22 | Legacy
23 | FFP
25 type mode =
26 | CLI
27 | DAEMON
29 type options = {
30 filename : string;
31 fallback : bool;
32 config_list : string list;
33 debug_time : bool;
34 parser : parser;
35 output_file : string option;
36 config_file : string option;
37 quiet_mode : bool;
38 mode : mode;
39 input_file_list : string option;
40 dump_symbol_refs : bool;
41 dump_stats : bool;
42 dump_config : bool;
43 extract_facts : bool;
44 log_stats : 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 (*****************************************************************************)
57 (* Debug info refs *)
58 (*****************************************************************************)
60 type debug_time = {
61 parsing_t: float ref;
62 codegen_t: float ref;
63 printing_t: float ref;
66 let new_debug_time () =
68 parsing_t = ref 0.0;
69 codegen_t = ref 0.0;
70 printing_t = ref 0.0;
73 (*****************************************************************************)
74 (* Helpers *)
75 (*****************************************************************************)
77 let die str =
78 prerr_endline str;
79 exit 2
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 () =
86 let open Hh_json in
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 ()))
90 ] in
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
116 let options =
117 [ ("--version"
118 , Arg.Set want_version
119 , " print the version and do nothing"
121 ("--fallback"
122 , Arg.Set fallback
123 , " Enables fallback compilation"
125 ("--debug-time"
126 , Arg.Set debug_time
127 , " Enables debugging logging for elapsed time"
129 ("--quiet-mode"
130 , Arg.Set quiet_mode
131 , " Runs very quietly, and ignore any result if invoked without -o "
132 ^ "(lower priority than the debug-time option)"
134 ("--facts"
135 , Arg.Set extract_facts
136 , "Extract facts from the source code."
138 ("-v"
139 , Arg.String (fun str -> config_list := str :: !config_list)
140 , " Configuration: Eval.EnableHipHopSyntax=<value> "
141 ^ "or Hack.Lang.IntsOverflowToInts=<value>"
142 ^ "\n"
143 ^ "\t\tAllows overriding config options passed on a file"
145 ("-c"
146 , Arg.String (fun str ->
147 assert_regular_file str;
148 config_file := Some str)
149 , " Config file in JSON format"
151 ("-o"
152 , Arg.String (fun str -> output_file := Some str)
153 , " Output file. Creates it if necessary"
155 ("--parser"
156 , Arg.String
157 (function "ffp" -> parser := FFP
158 | "legacy" -> parser := Legacy
159 | p -> failwith @@ p ^ " is an invalid parser")
160 , " Parser: ffp or legacy [def: ffp]"
162 ("--daemon"
163 , Arg.Unit (fun () -> mode := DAEMON)
164 , " Run a daemon which processes Hack source from standard input"
166 ("--input-file-list"
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"
174 ("--dump-stats"
175 , Arg.Set dump_stats
176 , " Dump timing stats for functions"
178 ("--dump-config"
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"
190 ] in
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
196 let fn =
197 if needs_file then
198 match !fn_ref with
199 | Some fn -> if !mode = CLI then fn else die usage
200 | None -> if !mode = CLI then die usage else read_line ()
201 else
204 { filename = fn
205 ; fallback = !fallback
206 ; config_list = !config_list
207 ; debug_time = !debug_time
208 ; parser = !parser
209 ; output_file = !output_file
210 ; config_file = !config_file
211 ; quiet_mode = !quiet_mode
212 ; mode = !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 =
222 let open Hh_json in
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)
228 ] in
229 P.printf "%s\n%!" msg;
230 die error
232 let rec dispatch_loop handlers =
233 let open Hh_json in
234 let open Access in
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;
245 header, body
246 with exc ->
247 fail_daemon file ("Exception reading message body: " ^ (Printexc.to_string exc))
248 end in
249 let header, body = read_message () in
250 let msg_type = get_field
251 (get_string "type")
252 (fun af -> fail_daemon None ("Cannot determine type of message: " ^ af))
253 header in
254 (match msg_type with
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
270 | None -> ()
272 let parse_text compiler_options popt fn text =
273 let () = set_stats_if_enabled ~compiler_options in
274 match compiler_options.parser with
275 | FFP ->
276 let ignore_pos =
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
288 ~parser_options:popt
289 ~ignore_pos
290 ~codegen:true
291 ~fail_open:false
292 ~systemlib_compat_mode
293 ~php5_compat_mode
294 ~enable_hh_syntax
295 ~hacksperimental
296 ~keep_errors:false
297 ~lower_coroutines
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
304 (ast, is_hh_file)
305 | Legacy ->
306 let {Parser_return.ast; Parser_return.is_hh_file; _} =
307 Parser_hack.program popt fn text in
308 (ast, is_hh_file)
310 let parse_file compiler_options popt filename text =
312 `ParseResult (Errors.do_ begin fun () ->
313 parse_text compiler_options popt filename text
314 end)
315 with
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
336 | CLI -> "CLI"
337 | DAEMON -> "DAEMON"
339 let log_success compiler_options filename debug_time =
340 Logger.success
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 =
348 Logger.fail
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
357 let hhas_prog =
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
371 is_hh_file
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
379 ~path:filename
380 ~dump_symbol_refs:compiler_options.dump_symbol_refs
381 hhas_prog in
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;
387 hhas
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)
393 |> fun x -> [x]
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
404 let output =
405 if compiler_options.extract_facts
406 then extract_facts ~pretty:true source_text
407 else begin
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
411 end in
412 handle_output filename output debug_time
413 with exc ->
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 =
419 let open Hh_json in
420 let open Access in
422 let print_and_flush_strings strings =
423 Core_list.iter ~f:(P.printf "%s") strings;
424 P.printf "%!" in
426 let set_compiler_options config_json =
427 let options =
428 Hhbc_options.get_options_from_config
429 config_json
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
442 | DAEMON ->
443 let handle_output filename output debug_time =
444 let abs_path = Relative_path.to_absolute filename in
445 let bytes =
446 List.fold ~f:(fun len s -> len + String.length s) ~init:0 output in
447 let msg =
448 [ ("type", JSON_String "success")
449 ; ("file", JSON_String abs_path)
450 ; ("bytes", int_ bytes)
451 ] in
452 let msg =
453 if Hhbc_options.enable_perf_logging !Hhbc_options.compiler_options
454 then
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))
459 :: msg
460 else msg
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))
470 ] in
471 P.printf "%s\n%!" msg in
472 let handlers =
473 { set_config = (fun _header body ->
474 let config_json =
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
479 (get_string "file")
480 (fun af -> fail_daemon None ("Cannot determine file name of source unit: " ^ af))
481 header in
482 let error = get_field
483 (get_string "error")
484 (fun _af -> fail_daemon (Some filename) ("No 'error' field in error message"))
485 header in
486 fail_daemon (Some filename) ("Error processing " ^ filename ^ ": " ^ error))
487 ; compile = (fun header body ->
488 let filename = get_field
489 (get_string "file")
490 (fun af -> fail_daemon None ("Cannot determine file name of source unit: " ^ af))
491 header in
492 process_single_source_unit
493 compiler_options
494 popt
495 handle_output
496 handle_exception
497 (Relative_path.create Relative_path.Dummy filename)
498 body)
499 ; facts = (fun header body -> (
500 (* if body is empty - read file from disk *)
501 let filename = get_field
502 (get_string "file")
503 (fun af -> fail_daemon None ("Cannot determine file name of source unit: " ^ af))
504 header in
505 let body =
506 if String.length body = 0
507 then Sys_utils.cat filename
508 else body in
509 handle_output
510 (Relative_path.create Relative_path.Dummy filename)
511 (extract_facts body))
512 (new_debug_time ())
513 )} in
514 dispatch_loop handlers
516 | CLI ->
517 let handle_exception filename exc =
518 if not compiler_options.quiet_mode
519 then
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
535 let rec go lines =
536 match input_line inch with
537 | line -> go (String.trim line :: lines)
538 | exception End_of_file -> lines in
539 go [] 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
547 | None ->
548 if Sys.is_directory compiler_options.filename
550 (* Compile every file under directory *)
551 then
552 let files_in_dir =
553 let rec go dirs = match dirs with
554 | [] -> []
555 | dir :: dirs ->
556 let ds, fs = Sys.readdir dir
557 |> Array.map (Filename.concat dir)
558 |> Array.to_list
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
567 then (
568 if not compiler_options.quiet_mode
569 then P.fprintf stderr "Output file %s already exists\n" output_file)
570 else
571 Sys_utils.write_strings_to_file ~file:output_file output
572 in files_in_dir, handle_output
574 (* Compile a single file *)
575 else
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
580 | None ->
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)
590 let main_hack opts =
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 *)
597 let _ =
598 Printexc.record_backtrace true;
600 if ! Sys.interactive
601 then ()
602 else
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
610 main_hack options
611 with exc ->
612 Printexc.get_backtrace () |> prerr_endline;
613 die (Printexc.to_string exc)