this repo has no description
at universe-builder 1218 lines 43 kB view raw
1(** {1 OCaml Toplevel Implementation} 2 3 This module provides the core toplevel functionality for js_top_worker. 4 It implements phrase execution, type checking, and Merlin integration 5 (completion, errors, type info). 6 7 The module is parameterized by a backend signature [S] which provides 8 platform-specific operations for different environments (WebWorker, 9 Node.js, Unix). *) 10 11open Js_top_worker_rpc 12module M = Rpc_lwt.ErrM (* Server is not synchronous *) 13module IdlM = Rpc_lwt 14 15let ( let* ) = Lwt.bind 16 17(** {2 Cell Dependency System} 18 19 Cells are identified by string IDs and can depend on previous cells. 20 Each cell is wrapped in a module [Cell__<id>] so that later cells can 21 access earlier bindings via [open Cell__<id>]. *) 22 23type captured = { stdout : string; stderr : string } 24 25let modname_of_id id = "Cell__" ^ id 26 27let is_mangled_broken orig src = 28 String.length orig <> String.length src 29 || Seq.exists2 30 (fun c c' -> c <> c' && c' <> ' ') 31 (String.to_seq orig) (String.to_seq src) 32 33let mangle_toplevel is_toplevel orig_source deps = 34 let src = 35 if not is_toplevel then orig_source 36 else if 37 String.length orig_source < 2 38 || orig_source.[0] <> '#' 39 || orig_source.[1] <> ' ' 40 then ( 41 Logs.err (fun m -> 42 m "xx Warning, ignoring toplevel block without a leading '# '.\n%!"); 43 orig_source) 44 else 45 try 46 let s = String.sub orig_source 2 (String.length orig_source - 2) in 47 let list = 48 try Ocamltop.parse_toplevel s 49 with _ -> Ocamltop.fallback_parse_toplevel s 50 in 51 let lines = 52 List.map 53 (fun (phr, junk, output) -> 54 let l1 = 55 Printf.sprintf " %s%s" phr 56 (String.make (String.length junk) ' ') 57 in 58 match output with 59 | [] -> l1 60 | _ -> 61 let s = 62 List.map (fun x -> String.make (String.length x) ' ') output 63 in 64 String.concat "\n" (l1 :: s)) 65 list 66 in 67 String.concat "\n" lines 68 with e -> 69 Logs.err (fun m -> 70 m "Error in mangle_toplevel: %s" (Printexc.to_string e)); 71 let ppf = Format.err_formatter in 72 let _ = Location.report_exception ppf e in 73 orig_source 74 in 75 let line1 = 76 List.map (fun id -> Printf.sprintf "open %s" (modname_of_id id)) deps 77 |> String.concat " " 78 in 79 let line1 = if line1 = "" then "" else line1 ^ ";;\n" in 80 Logs.debug (fun m -> m "Line 1: '%s'\n%!" line1); 81 Logs.debug (fun m -> m "Source: %s\n%!" src); 82 if is_mangled_broken orig_source src then ( 83 Printf.printf "Warning: mangled source is broken\n%!"; 84 Printf.printf "orig length: %d\n%!" (String.length orig_source); 85 Printf.printf "src length: %d\n%!" (String.length src)); 86 (line1, src) 87 88(** {2 PPX Preprocessing} 89 90 Handles PPX rewriter registration and application. Supports: 91 - Old-style [Ast_mapper] PPXs (e.g., [Ppx_js.mapper] for js_of_ocaml) 92 - [ppx_deriving]-based PPXs (registered via [Ppx_deriving.register]) 93 - Modern [ppxlib]-based PPXs (registered via [Ppxlib.Driver]) 94 95 The [Ppx_js.mapper] is registered by default to support js_of_ocaml 96 syntax extensions. Other PPXs can be dynamically loaded via [#require]. *) 97 98module JsooTopPpx = struct 99 open Js_of_ocaml_compiler.Stdlib 100 101 (** Old-style Ast_mapper rewriters *) 102 let ppx_rewriters = ref [ (fun _ -> Ppx_js.mapper) ] 103 104 let () = 105 Ast_mapper.register_function := 106 fun _ f -> ppx_rewriters := f :: !ppx_rewriters 107 108 (** Apply old-style Ast_mapper rewriters *) 109 let apply_ast_mapper_rewriters_structure str = 110 let open Ast_mapper in 111 List.fold_right !ppx_rewriters ~init:str ~f:(fun ppx_rewriter str -> 112 let mapper = ppx_rewriter [] in 113 mapper.structure mapper str) 114 115 let apply_ast_mapper_rewriters_signature sg = 116 let open Ast_mapper in 117 List.fold_right !ppx_rewriters ~init:sg ~f:(fun ppx_rewriter sg -> 118 let mapper = ppx_rewriter [] in 119 mapper.signature mapper sg) 120 121 (** Apply ppx_deriving transformations using its mapper class. 122 This handles [@@deriving] attributes for dynamically loaded derivers. *) 123 let apply_ppx_deriving_structure str = 124 let mapper = new Ppx_deriving.mapper in 125 mapper#structure str 126 127 let apply_ppx_deriving_signature sg = 128 let mapper = new Ppx_deriving.mapper in 129 mapper#signature sg 130 131 (** Apply all PPX transformations in order: 132 1. Old-style Ast_mapper (e.g., Ppx_js) 133 2. ppx_deriving derivers 134 3. ppxlib-based PPXs 135 Handles AST version conversion between compiler's Parsetree and ppxlib's internal AST. *) 136 let preprocess_structure str = 137 str 138 |> apply_ast_mapper_rewriters_structure 139 |> Ppxlib_ast.Selected_ast.of_ocaml Structure 140 |> apply_ppx_deriving_structure 141 |> Ppxlib.Driver.map_structure 142 |> Ppxlib_ast.Selected_ast.to_ocaml Structure 143 144 let preprocess_signature sg = 145 sg 146 |> apply_ast_mapper_rewriters_signature 147 |> Ppxlib_ast.Selected_ast.of_ocaml Signature 148 |> apply_ppx_deriving_signature 149 |> Ppxlib.Driver.map_signature 150 |> Ppxlib_ast.Selected_ast.to_ocaml Signature 151 152 let preprocess_phrase phrase = 153 let open Parsetree in 154 match phrase with 155 | Ptop_def str -> Ptop_def (preprocess_structure str) 156 | Ptop_dir _ as x -> x 157end 158 159(** {2 Backend Signature} 160 161 Platform-specific operations that must be provided by each backend 162 (WebWorker, Node.js, Unix). *) 163 164module type S = sig 165 type findlib_t 166 167 val capture : (unit -> 'a) -> unit -> captured * 'a 168 val create_file : name:string -> content:string -> unit 169 val sync_get : string -> string option 170 val async_get : string -> (string, [> `Msg of string ]) result Lwt.t 171 val import_scripts : string list -> unit 172 val init_function : string -> unit -> unit 173 val get_stdlib_dcs : string -> Toplevel_api_gen.dynamic_cmis list 174 val findlib_init : string -> findlib_t Lwt.t 175 val path : string 176 177 val require : 178 bool -> findlib_t -> string list -> Toplevel_api_gen.dynamic_cmis list 179end 180 181(** {2 Main Functor} 182 183 The toplevel implementation, parameterized by backend operations. *) 184 185module Make (S : S) = struct 186 (** {3 Global State} 187 188 These are shared across all environments. *) 189 190 let functions : (unit -> unit) list option ref = ref None 191 let requires : string list ref = ref [] 192 let path : string option ref = ref None 193 let findlib_v : S.findlib_t Lwt.t option ref = ref None 194 let findlib_resolved : S.findlib_t option ref = ref None 195 let execution_allowed = ref true 196 197 (** {3 Environment Management} 198 199 Helper to resolve env_id string to an Environment.t. 200 Empty string means the default environment. *) 201 202 let resolve_env env_id = 203 let id = if env_id = "" then Environment.default_id else env_id in 204 Environment.get_or_create id 205 206 (** {3 Lexer Helpers} *) 207 208 let refill_lexbuf s p ppf buffer len = 209 if !p = String.length s then 0 210 else 211 let len', nl = 212 try (String.index_from s !p '\n' - !p + 1, false) 213 with _ -> (String.length s - !p, true) 214 in 215 let len'' = min len len' in 216 String.blit s !p buffer 0 len''; 217 (match ppf with 218 | Some ppf -> 219 Format.fprintf ppf "%s" (Bytes.sub_string buffer 0 len''); 220 if nl then Format.pp_print_newline ppf (); 221 Format.pp_print_flush ppf () 222 | None -> ()); 223 p := !p + len''; 224 len'' 225 226 (** {3 Setup and Initialization} *) 227 228 let exec' s = 229 S.capture 230 (fun () -> 231 let res : bool = Toploop.use_silently Format.std_formatter (String s) in 232 if not res then Format.eprintf "error while evaluating %s@." s) 233 () 234 235 (** {3 Custom Require Directive} 236 237 Replaces the standard findlib #require with one that loads JavaScript 238 archives via importScripts. This is necessary because in js_of_ocaml, 239 we can't use Topdirs.dir_load to load .cma files - we need to load 240 .cma.js files via importScripts instead. *) 241 242 let add_dynamic_cmis_sync dcs = 243 (* Synchronous version for #require directive. 244 Fetches and installs toplevel CMIs synchronously. *) 245 let furl = "file://" in 246 let l = String.length furl in 247 if String.length dcs.Toplevel_api_gen.dcs_url > l 248 && String.sub dcs.dcs_url 0 l = furl 249 then begin 250 let path = String.sub dcs.dcs_url l (String.length dcs.dcs_url - l) in 251 Topdirs.dir_directory path 252 end 253 else begin 254 (* Web URL - fetch CMIs synchronously *) 255 let fetch_sync filename = 256 let url = Filename.concat dcs.Toplevel_api_gen.dcs_url filename in 257 S.sync_get url 258 in 259 let path = 260 match !path with Some p -> p | None -> failwith "Path not set" 261 in 262 let to_cmi_filename name = 263 Printf.sprintf "%s.cmi" (String.uncapitalize_ascii name) 264 in 265 Logs.info (fun m -> m "Adding toplevel modules for dynamic cmis from %s" dcs.dcs_url); 266 Logs.info (fun m -> m " toplevel modules: %s" 267 (String.concat ", " dcs.dcs_toplevel_modules)); 268 (* Fetch and create toplevel module CMIs *) 269 List.iter 270 (fun name -> 271 let filename = to_cmi_filename name in 272 match fetch_sync filename with 273 | Some content -> 274 let fs_name = Filename.(concat path filename) in 275 (try S.create_file ~name:fs_name ~content with _ -> ()) 276 | None -> ()) 277 dcs.dcs_toplevel_modules; 278 (* Install on-demand loader for prefixed modules *) 279 if dcs.dcs_file_prefixes <> [] then begin 280 let open Persistent_env.Persistent_signature in 281 let old_loader = !load in 282#if defined OXCAML 283 load := fun ~allow_hidden ~unit_name -> 284 let filename = to_cmi_filename (Compilation_unit.Name.to_string unit_name) in 285#else 286 load := fun ~allow_hidden ~unit_name -> 287 let filename = to_cmi_filename unit_name in 288#endif 289 let fs_name = Filename.(concat path filename) in 290 if (not (Sys.file_exists fs_name)) 291 && List.exists 292 (fun prefix -> String.starts_with ~prefix filename) 293 dcs.dcs_file_prefixes 294 then begin 295 Logs.info (fun m -> m "Fetching %s\n%!" filename); 296 match fetch_sync filename with 297 | Some content -> 298 (try S.create_file ~name:fs_name ~content with _ -> ()) 299 | None -> () 300 end; 301 old_loader ~allow_hidden ~unit_name 302 end 303 end 304 305 let register_require_directive () = 306 let require_handler pkg = 307 Logs.info (fun m -> m "Custom #require: loading %s" pkg); 308 match !findlib_resolved with 309 | None -> 310 Format.eprintf "Error: findlib not initialized@." 311 | Some v -> 312 let cmi_only = not !execution_allowed in 313 let dcs_list = S.require cmi_only v [pkg] in 314 List.iter add_dynamic_cmis_sync dcs_list; 315 Logs.info (fun m -> m "Custom #require: %s loaded" pkg) 316 in 317 (* Replace the standard findlib #require directive with our custom one. 318 We use add_directive which will override the existing one. *) 319 let info = { Toploop.section = "Findlib"; doc = "Load a package (js_top_worker)" } in 320 Toploop.add_directive "require" (Toploop.Directive_string require_handler) info 321 322 let setup functions () = 323 let stdout_buff = Buffer.create 100 in 324 let stderr_buff = Buffer.create 100 in 325 326 let combine o = 327 Buffer.add_string stdout_buff o.stdout; 328 Buffer.add_string stderr_buff o.stderr 329 in 330 331 let exec' s = 332 let o, () = exec' s in 333 combine o 334 in 335 Sys.interactive := false; 336 337 Toploop.input_name := "//toplevel//"; 338 let path = 339 match !path with Some p -> p | None -> failwith "Path not set" 340 in 341 342 Topdirs.dir_directory path; 343 344 Toploop.initialize_toplevel_env (); 345 346 List.iter (fun f -> f ()) functions; 347 exec' "open Stdlib"; 348 let header1 = Printf.sprintf " %s version %%s" "OCaml" in 349 exec' (Printf.sprintf "Format.printf \"%s@.\" Sys.ocaml_version;;" header1); 350 exec' "#enable \"pretty\";;"; 351 exec' "#disable \"shortvar\";;"; 352 Sys.interactive := true; 353 Logs.info (fun m -> m "Setup complete"); 354 { 355 stdout = Buffer.contents stdout_buff; 356 stderr = Buffer.contents stderr_buff; 357 } 358 359 (** {3 Output Helpers} *) 360 361 let stdout_buff = Buffer.create 100 362 let stderr_buff = Buffer.create 100 363 364 let buff_opt b = 365 match String.trim (Buffer.contents b) with "" -> None | s -> Some s 366 367 let string_opt s = match String.trim s with "" -> None | s -> Some s 368 369 let loc = function 370 | Syntaxerr.Error x -> Some (Syntaxerr.location_of_error x) 371 | Lexer.Error (_, loc) 372 | Typecore.Error (loc, _, _) 373 | Typetexp.Error (loc, _, _) 374 | Typeclass.Error (loc, _, _) 375 | Typemod.Error (loc, _, _) 376 | Typedecl.Error (loc, _) 377 | Translcore.Error (loc, _) 378 | Translclass.Error (loc, _) 379 | Translmod.Error (loc, _) -> 380 Some loc 381 | _ -> None 382 383 (** {3 Phrase Execution} 384 385 Executes OCaml phrases in an environment, capturing all output. 386 Handles parsing, PPX preprocessing, and execution with error reporting. *) 387 388 let execute_in_env env phrase = 389 let code_buff = Buffer.create 100 in 390 let res_buff = Buffer.create 100 in 391 let pp_code = Format.formatter_of_buffer code_buff in 392 let pp_result = Format.formatter_of_buffer res_buff in 393 let highlighted = ref None in 394 let set_highlight loc = 395 let _file1, line1, col1 = Location.get_pos_info loc.Location.loc_start in 396 let _file2, line2, col2 = Location.get_pos_info loc.Location.loc_end in 397 highlighted := Some Toplevel_api_gen.{ line1; col1; line2; col2 } 398 in 399 Buffer.clear code_buff; 400 Buffer.clear res_buff; 401 Buffer.clear stderr_buff; 402 Buffer.clear stdout_buff; 403 let phrase = 404 let l = String.length phrase in 405 if l >= 2 && String.sub phrase (l - 2) 2 = ";;" then phrase 406 else phrase ^ ";;" 407 in 408 let o, () = 409 Environment.with_env env (fun () -> 410 S.capture 411 (fun () -> 412 let lb = Lexing.from_function (refill_lexbuf phrase (ref 0) (Some pp_code)) in 413 (try 414 while true do 415 try 416 let phr = !Toploop.parse_toplevel_phrase lb in 417 let phr = JsooTopPpx.preprocess_phrase phr in 418 ignore (Toploop.execute_phrase true pp_result phr : bool) 419 with 420 | End_of_file -> raise End_of_file 421 | x -> 422 (match loc x with Some l -> set_highlight l | None -> ()); 423 Errors.report_error Format.err_formatter x 424 done 425 with End_of_file -> ()); 426 flush_all ()) 427 ()) 428 in 429 let mime_vals = Mime_printer.get () in 430 Format.pp_print_flush pp_code (); 431 Format.pp_print_flush pp_result (); 432 Toplevel_api_gen. 433 { 434 stdout = string_opt o.stdout; 435 stderr = string_opt o.stderr; 436 sharp_ppf = buff_opt code_buff; 437 caml_ppf = buff_opt res_buff; 438 highlight = !highlighted; 439 mime_vals; 440 } 441 442 (** {3 Incremental Phrase Execution} 443 444 Executes OCaml phrases incrementally, calling a callback after each 445 phrase with its output and location. *) 446 447 type phrase_output = { 448 loc : int; 449 caml_ppf : string option; 450 mime_vals : Toplevel_api_gen.mime_val list; 451 } 452 453 let execute_in_env_incremental env phrase ~on_phrase_output = 454 let code_buff = Buffer.create 100 in 455 let res_buff = Buffer.create 100 in 456 let pp_code = Format.formatter_of_buffer code_buff in 457 let pp_result = Format.formatter_of_buffer res_buff in 458 let highlighted = ref None in 459 let set_highlight loc = 460 let _file1, line1, col1 = Location.get_pos_info loc.Location.loc_start in 461 let _file2, line2, col2 = Location.get_pos_info loc.Location.loc_end in 462 highlighted := Some Toplevel_api_gen.{ line1; col1; line2; col2 } 463 in 464 Buffer.clear code_buff; 465 Buffer.clear res_buff; 466 Buffer.clear stderr_buff; 467 Buffer.clear stdout_buff; 468 let phrase = 469 let l = String.length phrase in 470 if l >= 2 && String.sub phrase (l - 2) 2 = ";;" then phrase 471 else phrase ^ ";;" 472 in 473 let o, () = 474 Environment.with_env env (fun () -> 475 S.capture 476 (fun () -> 477 let lb = Lexing.from_function (refill_lexbuf phrase (ref 0) (Some pp_code)) in 478 (try 479 while true do 480 try 481 let phr = !Toploop.parse_toplevel_phrase lb in 482 let phr = JsooTopPpx.preprocess_phrase phr in 483 ignore (Toploop.execute_phrase true pp_result phr : bool); 484 (* Get location from phrase AST *) 485 let loc = match phr with 486 | Parsetree.Ptop_def ({ pstr_loc; _ } :: _) -> 487 pstr_loc.loc_end.pos_cnum 488 | Parsetree.Ptop_dir { pdir_loc; _ } -> 489 pdir_loc.loc_end.pos_cnum 490 | _ -> lb.lex_curr_p.pos_cnum 491 in 492 (* Flush and get current output *) 493 Format.pp_print_flush pp_result (); 494 let caml_ppf = buff_opt res_buff in 495 let mime_vals = Mime_printer.get () in 496 (* Call callback with phrase output *) 497 on_phrase_output { loc; caml_ppf; mime_vals }; 498 (* Clear for next phrase *) 499 Buffer.clear res_buff 500 with 501 | End_of_file -> raise End_of_file 502 | x -> 503 (match loc x with Some l -> set_highlight l | None -> ()); 504 Errors.report_error Format.err_formatter x 505 done 506 with End_of_file -> ()); 507 flush_all ()) 508 ()) 509 in 510 (* Get any remaining mime_vals (shouldn't be any after last callback) *) 511 let mime_vals = Mime_printer.get () in 512 Format.pp_print_flush pp_code (); 513 Format.pp_print_flush pp_result (); 514 Toplevel_api_gen. 515 { 516 stdout = string_opt o.stdout; 517 stderr = string_opt o.stderr; 518 sharp_ppf = buff_opt code_buff; 519 caml_ppf = buff_opt res_buff; 520 highlight = !highlighted; 521 mime_vals; 522 } 523 524 (** {3 Dynamic CMI Loading} 525 526 Handles loading .cmi files on demand for packages that weren't 527 compiled into the worker. *) 528 529 let filename_of_module unit_name = 530 Printf.sprintf "%s.cmi" (String.uncapitalize_ascii unit_name) 531 532 let get_dirs () = 533 let { Load_path.visible; hidden } = Load_path.get_paths () in 534 visible @ hidden 535 536 let reset_dirs () = 537 Ocaml_utils.Directory_content_cache.clear (); 538 let open Ocaml_utils.Load_path in 539 let dirs = get_dirs () in 540 reset (); 541 List.iter (fun p -> prepend_dir (Dir.create ~hidden:false p)) dirs 542 543 let reset_dirs_comp () = 544 let open Load_path in 545 let dirs = get_dirs () in 546 reset (); 547 List.iter (fun p -> prepend_dir (Dir.create ~hidden:false p)) dirs 548 549 let add_dynamic_cmis dcs = 550 let fetch filename = 551 let url = Filename.concat dcs.Toplevel_api_gen.dcs_url filename in 552 S.async_get url 553 in 554 let fetch_sync filename = 555 let url = Filename.concat dcs.Toplevel_api_gen.dcs_url filename in 556 S.sync_get url 557 in 558 let path = 559 match !path with Some p -> p | None -> failwith "Path not set" 560 in 561 let ( let* ) = Lwt.bind in 562 let* () = 563 Logs.info (fun m -> m "Adding toplevel modules for dynamic cmis from %s" dcs.dcs_url); 564 Logs.info (fun m -> m " toplevel modules: %s" 565 (String.concat ", " dcs.dcs_toplevel_modules)); 566 Lwt_list.iter_p 567 (fun name -> 568 let filename = filename_of_module name in 569 let* r = fetch (filename_of_module name) in 570 let () = 571 match r with 572 | Ok content -> ( 573 let name = Filename.(concat path filename) in 574 try S.create_file ~name ~content with _ -> ()) 575 | Error _ -> () 576 in 577 Lwt.return ()) 578 dcs.dcs_toplevel_modules 579 in 580 581#if defined OXCAML 582 let new_load : 583 'a 'b. 584 s:string -> 585 to_string:('a -> string) -> 586 old_loader:(allow_hidden:bool -> unit_name:'a -> 'b option) -> 587 allow_hidden:bool -> 588 unit_name:'a -> 589 'b option = 590 fun ~s ~to_string ~old_loader ~allow_hidden ~unit_name -> 591 let filename = filename_of_module (to_string unit_name) in 592#else 593 let new_load ~s ~old_loader ~allow_hidden ~unit_name = 594 let filename = filename_of_module unit_name in 595#endif 596 597 let fs_name = Filename.(concat path filename) in 598 (* Check if it's already been downloaded. This will be the 599 case for all toplevel cmis. Also check whether we're supposed 600 to handle this cmi *) 601 (* if Sys.file_exists fs_name 602 then Logs.info (fun m -> m "Found: %s" fs_name) 603 else Logs.info (fun m -> m "No sign of %s locally" fs_name); *) 604 if 605 (not (Sys.file_exists fs_name)) 606 && List.exists 607 (fun prefix -> String.starts_with ~prefix filename) 608 dcs.dcs_file_prefixes 609 then ( 610 Logs.info (fun m -> m "Fetching %s\n%!" filename); 611 match fetch_sync filename with 612 | Some x -> 613 (try S.create_file ~name:fs_name ~content:x with _ -> ()); 614 (* At this point we need to tell merlin that the dir contents 615 have changed *) 616 if s = "merl" then reset_dirs () else reset_dirs_comp () 617 | None -> 618 Printf.eprintf "Warning: Expected to find cmi at: %s\n%!" 619 (Filename.concat dcs.Toplevel_api_gen.dcs_url filename)); 620 if s = "merl" then reset_dirs () else reset_dirs_comp (); 621 old_loader ~allow_hidden ~unit_name 622 in 623 let furl = "file://" in 624 let l = String.length furl in 625 let () = 626 if String.length dcs.dcs_url > l && String.sub dcs.dcs_url 0 l = furl then 627 let path = String.sub dcs.dcs_url l (String.length dcs.dcs_url - l) in 628 Topdirs.dir_directory path 629 else 630 let open Persistent_env.Persistent_signature in 631 let old_loader = !load in 632#if defined OXCAML 633 load := new_load ~s:"comp" ~to_string:Compilation_unit.Name.to_string ~old_loader; 634#else 635 load := new_load ~s:"comp" ~old_loader; 636#endif 637 638#if defined OXCAML 639 let open Persistent_env.Persistent_signature in 640 let old_loader = !load in 641 load := new_load ~s:"merl" ~to_string:Compilation_unit.Name.to_string ~old_loader 642#else 643 let open Ocaml_typing.Persistent_env.Persistent_signature in 644 let old_loader = !load in 645 load := new_load ~s:"merl" ~old_loader 646#endif 647 in 648 Lwt.return () 649 650 (** {3 RPC Handlers} 651 652 Functions that implement the toplevel RPC API. Each function returns 653 results in the [IdlM.ErrM] monad. *) 654 655 let init (init_libs : Toplevel_api_gen.init_config) = 656 Lwt.catch 657 (fun () -> 658 Logs.info (fun m -> m "init()"); 659 path := Some S.path; 660 661 let findlib_path = Option.value ~default:"findlib_index.json" init_libs.findlib_index in 662 findlib_v := Some (S.findlib_init findlib_path); 663 664 let stdlib_dcs = 665 match init_libs.stdlib_dcs with 666 | Some dcs -> dcs 667 | None -> "lib/ocaml/dynamic_cmis.json" 668 in 669 let* () = 670 match S.get_stdlib_dcs stdlib_dcs with 671 | [ dcs ] -> add_dynamic_cmis dcs 672 | _ -> Lwt.return () 673 in 674#if defined OXCAML 675 Language_extension.(set_universe_and_enable_all Universe.Beta); 676#endif 677 Clflags.no_check_prims := true; 678 679 requires := init_libs.findlib_requires; 680 functions := Some []; 681 execution_allowed := init_libs.execute; 682 683 (* Set up the toplevel environment *) 684 Logs.info (fun m -> m "init() finished"); 685 686 Lwt.return (Ok ())) 687 (fun e -> 688 Lwt.return 689 (Error (Toplevel_api_gen.InternalError (Printexc.to_string e)))) 690 691 let setup env_id = 692 Lwt.catch 693 (fun () -> 694 let env = resolve_env env_id in 695 Logs.info (fun m -> m "setup() for env %s..." (Environment.id env)); 696 697 if Environment.is_setup env then ( 698 Logs.info (fun m -> m "setup() already done for env %s" (Environment.id env)); 699 Lwt.return 700 (Ok 701 Toplevel_api_gen. 702 { 703 stdout = None; 704 stderr = Some "Environment already set up"; 705 sharp_ppf = None; 706 caml_ppf = None; 707 highlight = None; 708 mime_vals = []; 709 })) 710 else 711 let o = 712 Environment.with_env env (fun () -> 713 try 714 match !functions with 715 | Some l -> setup l () 716 | None -> failwith "Error: toplevel has not been initialised" 717 with 718 | Persistent_env.Error e -> 719 Persistent_env.report_error Format.err_formatter e; 720 let err = Format.asprintf "%a" Persistent_env.report_error e in 721 failwith ("Error: " ^ err) 722#if defined OXCAML 723 | Env.Error e -> 724 Env.report_error ~level:0 Format.err_formatter e; 725 let err = Format.asprintf "%a" (Env.report_error ~level:0) e in 726 failwith ("Error: " ^ err)) 727#else 728 | Env.Error _ as exn -> 729 Location.report_exception Format.err_formatter exn; 730 let err = Format.asprintf "%a" Location.report_exception exn in 731 failwith ("Error: " ^ err)) 732#endif 733 in 734 735 let* dcs = 736 match !findlib_v with 737 | Some v -> 738 let* v = v in 739 (* Store the resolved findlib value for use by #require directive *) 740 findlib_resolved := Some v; 741 (* Register our custom #require directive that uses findlibish *) 742 register_require_directive (); 743 Lwt.return (S.require (not !execution_allowed) v !requires) 744 | None -> Lwt.return [] 745 in 746 747 let* () = Lwt_list.iter_p add_dynamic_cmis dcs in 748 749 Environment.mark_setup env; 750 Logs.info (fun m -> m "setup() finished for env %s" (Environment.id env)); 751 752 Lwt.return 753 (Ok 754 Toplevel_api_gen. 755 { 756 stdout = string_opt o.stdout; 757 stderr = string_opt o.stderr; 758 sharp_ppf = None; 759 caml_ppf = None; 760 highlight = None; 761 mime_vals = []; 762 })) 763 (fun e -> 764 Lwt.return 765 (Error (Toplevel_api_gen.InternalError (Printexc.to_string e)))) 766 767 let handle_toplevel env stripped = 768 if String.length stripped < 2 || stripped.[0] <> '#' || stripped.[1] <> ' ' 769 then ( 770 Printf.eprintf 771 "Warning, ignoring toplevel block without a leading '# '.\n"; 772 IdlM.ErrM.return 773 { Toplevel_api_gen.script = stripped; mime_vals = []; parts = [] }) 774 else 775 let s = String.sub stripped 2 (String.length stripped - 2) in 776 let list = Ocamltop.parse_toplevel s in 777 let buf = Buffer.create 1024 in 778 let mime_vals = 779 List.fold_left 780 (fun acc (phr, _junk, _output) -> 781 let new_output = execute_in_env env phr in 782 Printf.bprintf buf "# %s\n" phr; 783 let r = 784 Option.to_list new_output.stdout 785 @ Option.to_list new_output.stderr 786 @ Option.to_list new_output.caml_ppf 787 in 788 let r = 789 List.concat_map (fun l -> Astring.String.cuts ~sep:"\n" l) r 790 in 791 List.iter (fun x -> Printf.bprintf buf " %s\n" x) r; 792 let mime_vals = new_output.mime_vals in 793 acc @ mime_vals) 794 [] list 795 in 796 let content_txt = Buffer.contents buf in 797 let content_txt = 798 String.sub content_txt 0 (String.length content_txt - 1) 799 in 800 let result = 801 { Toplevel_api_gen.script = content_txt; mime_vals; parts = [] } 802 in 803 IdlM.ErrM.return result 804 805 let exec_toplevel env_id (phrase : string) = 806 let env = resolve_env env_id in 807 try handle_toplevel env phrase 808 with e -> 809 Logs.info (fun m -> m "Error: %s" (Printexc.to_string e)); 810 IdlM.ErrM.return_err 811 (Toplevel_api_gen.InternalError (Printexc.to_string e)) 812 813 let execute env_id (phrase : string) = 814 Logs.info (fun m -> m "execute() for env_id=%s" env_id); 815 let env = resolve_env env_id in 816 let result = execute_in_env env phrase in 817 Logs.info (fun m -> m "execute() done for env_id=%s" env_id); 818 IdlM.ErrM.return result 819 820 let execute_incremental env_id (phrase : string) ~on_phrase_output = 821 Logs.info (fun m -> m "execute_incremental() for env_id=%s" env_id); 822 let env = resolve_env env_id in 823 let result = execute_in_env_incremental env phrase ~on_phrase_output in 824 Logs.info (fun m -> m "execute_incremental() done for env_id=%s" env_id); 825 IdlM.ErrM.return result 826 827 (** {3 Merlin Integration} 828 829 Code intelligence features powered by Merlin: completion, type info, 830 error diagnostics. *) 831 832 let config () = 833 let path = 834 match !path with Some p -> p | None -> failwith "Path not set" 835 in 836 let initial = Merlin_kernel.Mconfig.initial in 837 { initial with merlin = { initial.merlin with stdlib = Some path } } 838 839 let make_pipeline source = Merlin_kernel.Mpipeline.make (config ()) source 840 841 let wdispatch source query = 842 let pipeline = make_pipeline source in 843 Merlin_kernel.Mpipeline.with_pipeline pipeline @@ fun () -> 844 Query_commands.dispatch pipeline query 845 846 (** Completion prefix extraction, adapted from ocaml-lsp-server. *) 847 module Completion = struct 848 open Merlin_utils 849 open Std 850 open Merlin_kernel 851 852 (* Prefixing code from ocaml-lsp-server *) 853 let rfindi = 854 let rec loop s ~f i = 855 if i < 0 then None 856 else if f (String.unsafe_get s i) then Some i 857 else loop s ~f (i - 1) 858 in 859 fun ?from s ~f -> 860 let from = 861 let len = String.length s in 862 match from with 863 | None -> len - 1 864 | Some i -> 865 if i > len - 1 then 866 raise @@ Invalid_argument "rfindi: invalid from" 867 else i 868 in 869 loop s ~f from 870 871 let lsplit2 s ~on = 872 match String.index_opt s on with 873 | None -> None 874 | Some i -> 875 let open StdLabels.String in 876 Some (sub s ~pos:0 ~len:i, sub s ~pos:(i + 1) ~len:(length s - i - 1)) 877 878 (** @see <https://ocaml.org/manual/lex.html> reference *) 879 let prefix_of_position ?(short_path = false) source position = 880 match Msource.text source with 881 | "" -> "" 882 | text -> 883 let from = 884 let (`Offset index) = Msource.get_offset source position in 885 min (String.length text - 1) (index - 1) 886 in 887 let pos = 888 let should_terminate = ref false in 889 let has_seen_dot = ref false in 890 let is_prefix_char c = 891 if !should_terminate then false 892 else 893 match c with 894 | 'a' .. 'z' 895 | 'A' .. 'Z' 896 | '0' .. '9' 897 | '\'' | '_' 898 (* Infix function characters *) 899 | '$' | '&' | '*' | '+' | '-' | '/' | '=' | '>' | '@' | '^' 900 | '!' | '?' | '%' | '<' | ':' | '~' | '#' -> 901 true 902 | '`' -> 903 if !has_seen_dot then false 904 else ( 905 should_terminate := true; 906 true) 907 | '.' -> 908 has_seen_dot := true; 909 not short_path 910 | _ -> false 911 in 912 rfindi text ~from ~f:(fun c -> not (is_prefix_char c)) 913 in 914 let pos = match pos with None -> 0 | Some pos -> pos + 1 in 915 let len = from - pos + 1 in 916 let reconstructed_prefix = StdLabels.String.sub text ~pos ~len in 917 (* if we reconstructed [~f:ignore] or [?f:ignore], we should take only 918 [ignore], so: *) 919 if 920 String.is_prefixed ~by:"~" reconstructed_prefix 921 || String.is_prefixed ~by:"?" reconstructed_prefix 922 then 923 match lsplit2 reconstructed_prefix ~on:':' with 924 | Some (_, s) -> s 925 | None -> reconstructed_prefix 926 else reconstructed_prefix 927 928 let at_pos source position = 929 let prefix = prefix_of_position source position in 930 let (`Offset to_) = Msource.get_offset source position in 931 let from = 932 to_ 933 - String.length (prefix_of_position ~short_path:true source position) 934 in 935 if prefix = "" then None 936 else 937 let query = 938 Query_protocol.Complete_prefix (prefix, position, [], true, true) 939 in 940 Some (from, to_, wdispatch source query) 941 end 942 943 let complete_prefix env_id id deps is_toplevel source position = 944 let _env = resolve_env env_id in (* Reserved for future use *) 945 try 946 Logs.info (fun m -> m "completing for id: %s" (match id with Some x -> x | None -> "(none)")); 947 948 let line1, src = mangle_toplevel is_toplevel source deps in 949 Logs.info (fun m -> m "line1: '%s' (length: %d)" line1 (String.length line1)); 950 Logs.info (fun m -> m "src: '%s' (length: %d)" src (String.length src)); 951 let src = line1 ^ src in 952 let source = Merlin_kernel.Msource.make src in 953 let map_kind : 954 [ `Value 955 | `Constructor 956 | `Variant 957 | `Label 958 | `Module 959 | `Modtype 960 | `Type 961 | `MethodCall 962 | `Keyword ] -> 963 Toplevel_api_gen.kind_ty = function 964 | `Value -> Value 965 | `Constructor -> Constructor 966 | `Variant -> Variant 967 | `Label -> Label 968 | `Module -> Module 969 | `Modtype -> Modtype 970 | `Type -> Type 971 | `MethodCall -> MethodCall 972 | `Keyword -> Keyword 973 in 974 let position = 975 match position with 976 | Toplevel_api_gen.Start -> `Offset (String.length line1) 977 | Offset x -> `Offset (x + String.length line1) 978 | Logical (x, y) -> `Logical (x + 1, y) 979 | End -> `End 980 in 981 982 (match position with 983 | `Offset x -> 984 let first_char = String.sub src (x-1) 1 in 985 Logs.info (fun m -> m "complete after offset: %s" first_char) 986 | _ -> ()); 987 988 match Completion.at_pos source position with 989 | Some (from, to_, compl) -> 990 let entries = 991 List.map 992 (fun (entry : Query_protocol.Compl.entry) -> 993 { 994 Toplevel_api_gen.name = entry.name; 995 kind = map_kind entry.kind; 996 desc = entry.desc; 997 info = entry.info; 998 deprecated = entry.deprecated; 999 }) 1000 compl.entries 1001 in 1002 let l1l = String.length line1 in 1003 IdlM.ErrM.return { Toplevel_api_gen.from = from - l1l; to_ = to_ - l1l; entries } 1004 | None -> 1005 IdlM.ErrM.return { Toplevel_api_gen.from = 0; to_ = 0; entries = [] } 1006 with e -> 1007 Logs.info (fun m -> m "Error: %s" (Printexc.to_string e)); 1008 IdlM.ErrM.return_err 1009 (Toplevel_api_gen.InternalError (Printexc.to_string e)) 1010 1011 let add_cmi execution_env id deps source = 1012 Logs.info (fun m -> m "add_cmi"); 1013 let dep_modules = List.map modname_of_id deps in 1014 let loc = Location.none in 1015 let path = 1016 match !path with Some p -> p | None -> failwith "Path not set" 1017 in 1018 let filename = modname_of_id id |> String.uncapitalize_ascii in 1019 let prefix = Printf.sprintf "%s/%s" path filename in 1020 let filename = Printf.sprintf "%s.ml" prefix in 1021 Logs.info (fun m -> m "prefix: %s" prefix); 1022 let oc = open_out filename in 1023 Printf.fprintf oc "%s" source; 1024 close_out oc; 1025 (try Sys.remove (prefix ^ ".cmi") with Sys_error _ -> ()); 1026#if defined OXCAML 1027 let unit_info = Unit_info.make ~source_file:filename Impl prefix 1028 ~for_pack_prefix:Compilation_unit.Prefix.empty in 1029#else 1030 let unit_info = Unit_info.make ~source_file:filename Impl prefix in 1031#endif 1032 try 1033 let store = Local_store.fresh () in 1034 Local_store.with_store store (fun () -> 1035 Local_store.reset (); 1036 let env = 1037 Typemod.initial_env ~loc ~initially_opened_module:(Some "Stdlib") 1038 ~open_implicit_modules:dep_modules 1039 in 1040 let lexbuf = Lexing.from_string source in 1041 let ast = Parse.implementation lexbuf in 1042 Logs.info (fun m -> m "About to type_implementation"); 1043#if defined OXCAML 1044 let _ = Typemod.type_implementation unit_info 1045 (Compilation_unit.of_string (modname_of_id id)) env ast in 1046#else 1047 let _ = Typemod.type_implementation unit_info env ast in 1048#endif 1049 let b = Sys.file_exists (prefix ^ ".cmi") in 1050 Environment.remove_failed_cell execution_env id; 1051 Logs.info (fun m -> m "file_exists: %s = %b" (prefix ^ ".cmi") b)); 1052 Ocaml_typing.Cmi_cache.clear () 1053 with 1054#if defined OXCAML 1055 | Env.Error e -> 1056 Logs.err (fun m -> m "Env.Error: %a" (Env.report_error ~level:0) e); 1057 Environment.add_failed_cell execution_env id; 1058 () 1059#else 1060 | Env.Error _ as exn -> 1061 Logs.err (fun m -> m "Env.Error: %a" Location.report_exception exn); 1062 Environment.add_failed_cell execution_env id; 1063 () 1064#endif 1065 | exn -> 1066 let s = Printexc.to_string exn in 1067 Logs.err (fun m -> m "Error in add_cmi: %s" s); 1068 Logs.err (fun m -> m "Backtrace: %s" (Printexc.get_backtrace ())); 1069 let ppf = Format.err_formatter in 1070 let _ = Location.report_exception ppf exn in 1071 Environment.add_failed_cell execution_env id; 1072 () 1073 1074 let map_pos line1 pos = 1075 (* Only subtract line number when there's actually a prepended line *) 1076 let line_offset = if line1 = "" then 0 else 1 in 1077 Lexing. 1078 { 1079 pos with 1080 pos_bol = pos.pos_bol - String.length line1; 1081 pos_lnum = pos.pos_lnum - line_offset; 1082 pos_cnum = pos.pos_cnum - String.length line1; 1083 } 1084 1085 let map_loc line1 (loc : Ocaml_parsing.Location.t) = 1086 { 1087 loc with 1088 Ocaml_utils.Warnings.loc_start = map_pos line1 loc.loc_start; 1089 Ocaml_utils.Warnings.loc_end = map_pos line1 loc.loc_end; 1090 } 1091 1092 let query_errors env_id id deps is_toplevel orig_source = 1093 let execution_env = resolve_env env_id in 1094 try 1095 let deps = 1096 List.filter (fun dep -> not (Environment.is_cell_failed execution_env dep)) deps 1097 in 1098 let line1, src = mangle_toplevel is_toplevel orig_source deps in 1099 let full_source = line1 ^ src in 1100 let source = Merlin_kernel.Msource.make full_source in 1101 let query = 1102 Query_protocol.Errors { lexing = true; parsing = true; typing = true } 1103 in 1104 let errors = 1105 wdispatch source query 1106 |> StdLabels.List.filter_map 1107 ~f:(fun 1108 (Ocaml_parsing.Location.{ kind; main = _; sub; source; _ } as 1109 error) 1110 -> 1111 let of_sub sub = 1112 Ocaml_parsing.Location.print_sub_msg Format.str_formatter sub; 1113 String.trim (Format.flush_str_formatter ()) 1114 in 1115 let loc = 1116 map_loc line1 (Ocaml_parsing.Location.loc_of_report error) 1117 in 1118 let main = 1119 Format.asprintf "@[%a@]" Ocaml_parsing.Location.print_main 1120 error 1121 |> String.trim 1122 in 1123 if loc.loc_start.pos_lnum = 0 then None 1124 else 1125 Some 1126 { 1127 Toplevel_api_gen.kind; 1128 loc; 1129 main; 1130 sub = StdLabels.List.map ~f:of_sub sub; 1131 source; 1132 }) 1133 in 1134 (* Only track cell CMIs when id is provided (notebook mode) *) 1135 (match id with 1136 | Some cell_id -> 1137 if List.length errors = 0 then add_cmi execution_env cell_id deps src 1138 else Environment.add_failed_cell execution_env cell_id 1139 | None -> ()); 1140 1141 (* Logs.info (fun m -> m "Got to end"); *) 1142 IdlM.ErrM.return errors 1143 with e -> 1144 Logs.info (fun m -> m "Error: %s" (Printexc.to_string e)); 1145 IdlM.ErrM.return_err 1146 (Toplevel_api_gen.InternalError (Printexc.to_string e)) 1147 1148 let type_enclosing env_id _id deps is_toplevel orig_source position = 1149 let execution_env = resolve_env env_id in 1150 try 1151 let deps = 1152 List.filter (fun dep -> not (Environment.is_cell_failed execution_env dep)) deps 1153 in 1154 let line1, src = mangle_toplevel is_toplevel orig_source deps in 1155 let src = line1 ^ src in 1156 let position = 1157 match position with 1158 | Toplevel_api_gen.Start -> `Start 1159 | Offset x -> `Offset (x + String.length line1) 1160 | Logical (x, y) -> `Logical (x + 1, y) 1161 | End -> `End 1162 in 1163 let source = Merlin_kernel.Msource.make src in 1164 let query = Query_protocol.Type_enclosing (None, position, None) in 1165 let enclosing = wdispatch source query in 1166 let map_index_or_string = function 1167 | `Index i -> Toplevel_api_gen.Index i 1168 | `String s -> String s 1169 in 1170 let map_tail_position = function 1171 | `No -> Toplevel_api_gen.No 1172 | `Tail_position -> Tail_position 1173 | `Tail_call -> Tail_call 1174 in 1175 let enclosing = 1176 List.map 1177 (fun (x, y, z) -> 1178 (map_loc line1 x, map_index_or_string y, map_tail_position z)) 1179 enclosing 1180 in 1181 IdlM.ErrM.return enclosing 1182 with e -> 1183 Logs.info (fun m -> m "Error: %s" (Printexc.to_string e)); 1184 IdlM.ErrM.return_err 1185 (Toplevel_api_gen.InternalError (Printexc.to_string e)) 1186 1187 (** {3 Environment Management RPCs} *) 1188 1189 let create_env env_id = 1190 Lwt.catch 1191 (fun () -> 1192 Logs.info (fun m -> m "create_env(%s)" env_id); 1193 let _env = Environment.create env_id in 1194 Lwt.return (Ok ())) 1195 (fun e -> 1196 Lwt.return 1197 (Error (Toplevel_api_gen.InternalError (Printexc.to_string e)))) 1198 1199 let destroy_env env_id = 1200 Lwt.catch 1201 (fun () -> 1202 Logs.info (fun m -> m "destroy_env(%s)" env_id); 1203 Environment.destroy env_id; 1204 Lwt.return (Ok ())) 1205 (fun e -> 1206 Lwt.return 1207 (Error (Toplevel_api_gen.InternalError (Printexc.to_string e)))) 1208 1209 let list_envs () = 1210 Lwt.catch 1211 (fun () -> 1212 let envs = Environment.list () in 1213 Logs.info (fun m -> m "list_envs() -> [%s]" (String.concat ", " envs)); 1214 Lwt.return (Ok envs)) 1215 (fun e -> 1216 Lwt.return 1217 (Error (Toplevel_api_gen.InternalError (Printexc.to_string e)))) 1218end