this repo has no description
at main 366 lines 13 kB view raw
1open Js_top_worker 2 3(* OCamlorg toplevel in a web worker 4 5 This communicates with the toplevel code via a simple message-based 6 protocol defined in {!Js_top_worker_message.Message}. This allows 7 the OCaml execution to not block the "main thread" keeping the page 8 responsive. *) 9 10module Msg = Js_top_worker_message.Message 11 12let loc = function 13 | Syntaxerr.Error x -> Some (Syntaxerr.location_of_error x) 14 | Lexer.Error (_, loc) 15 | Typecore.Error (loc, _, _) 16 | Typetexp.Error (loc, _, _) 17 | Typeclass.Error (loc, _, _) 18 | Typemod.Error (loc, _, _) 19 | Typedecl.Error (loc, _) 20 | Translcore.Error (loc, _) 21 | Translclass.Error (loc, _) 22 | Translmod.Error (loc, _) -> 23 Some loc 24 | _ -> None 25 26module S : Impl.S = struct 27 type findlib_t = Findlibish.t 28 29 let capture : (unit -> 'a) -> unit -> Impl.captured * 'a = 30 fun f () -> 31 let stdout_buff = Buffer.create 1024 in 32 let stderr_buff = Buffer.create 1024 in 33 Js_of_ocaml.Sys_js.set_channel_flusher stdout 34 (Buffer.add_string stdout_buff); 35 Js_of_ocaml.Sys_js.set_channel_flusher stderr 36 (Buffer.add_string stderr_buff); 37 let x = f () in 38 let captured = 39 { 40 Impl.stdout = Buffer.contents stdout_buff; 41 stderr = Buffer.contents stderr_buff; 42 } 43 in 44 (captured, x) 45 46 let sync_get = Jslib.sync_get 47 let async_get = Jslib.async_get 48 49 (* Idempotent create_file that ignores "file already exists" errors. 50 This is needed because multiple .cma.js files compiled with --toplevel 51 may embed the same CMI files, and when loaded via import_scripts they 52 all try to register those CMIs. *) 53 let create_file ~name ~content = 54 try Js_of_ocaml.Sys_js.create_file ~name ~content 55 with Sys_error _ -> () 56 57 let get_stdlib_dcs uri = 58 Findlibish.fetch_dynamic_cmis sync_get uri |> Result.to_list 59 60 let find_stdlib_dcs v = 61 (* Try "stdlib" first (standard META name), fall back to "ocaml" *) 62 let pkg = match Findlibish.find_dcs_url v "stdlib" with 63 | Some _ as r -> r 64 | None -> Findlibish.find_dcs_url v "ocaml" 65 in 66 match pkg with 67 | Some url -> 68 Jslib.log "Found stdlib dcs via findlib: %s" url; 69 (match Findlibish.fetch_dynamic_cmis sync_get url with 70 | Ok dcs -> 71 (* The dcs_url in the JSON is relative to the package dir. 72 Rewrite it to be absolute using the dynamic_cmis.json URL 73 parent directory. *) 74 let abs_dcs_url = 75 match String.rindex_opt url '/' with 76 | Some i -> String.sub url 0 (i + 1) 77 | None -> url 78 in 79 Jslib.log "Rewriting dcs_url from %s to %s" dcs.Impl.dcs_url abs_dcs_url; 80 [{ dcs with Impl.dcs_url = abs_dcs_url }] 81 | Error _ -> []) 82 | None -> 83 Jslib.log "stdlib not found in findlib (tried 'stdlib' and 'ocaml')"; 84 [] 85 86 let import_scripts urls = 87 (* Map relative URLs to absolute using the global base URL *) 88 let absolute_urls = List.map Jslib.map_url urls in 89 (* Chrome blocks cross-origin importScripts for .cma.js files (CORB). 90 Work around this by fetching with sync XHR and eval'ing the code. *) 91 let worker_origin = 92 Js_of_ocaml.Js.Optdef.case 93 (Js_of_ocaml.Js.Unsafe.js_expr "self.location.origin") 94 (fun () -> "") 95 Js_of_ocaml.Js.to_string 96 in 97 List.iter (fun url -> 98 let is_cross_origin = 99 worker_origin <> "" 100 && String.length url > 8 101 && (let prefix = 102 try String.sub url 0 (String.index_from url 8 '/' + 1) 103 with Not_found -> url 104 in 105 not (String.length prefix <= String.length worker_origin + 1 106 && String.sub url 0 (String.length worker_origin) 107 = worker_origin)) 108 in 109 if is_cross_origin then begin 110 Jslib.log "Cross-origin import via fetch+eval: %s" url; 111 match Jslib.sync_get url with 112 | Some code -> 113 let (_ : 'a) = 114 Js_of_ocaml.Js.Unsafe.fun_call 115 (Js_of_ocaml.Js.Unsafe.js_expr "eval") 116 [| Js_of_ocaml.Js.Unsafe.inject 117 (Js_of_ocaml.Js.string code) |] 118 in 119 () 120 | None -> 121 failwith (Printf.sprintf "Failed to fetch cross-origin script: %s" url) 122 end else 123 Js_of_ocaml.Worker.import_scripts [url] 124 ) absolute_urls 125 let findlib_init = Findlibish.init async_get 126 127 let require b v = function 128 | [] -> [] 129 | packages -> Findlibish.require ~import_scripts sync_get b v packages 130 131 let init_function func_name = 132 let open Js_of_ocaml in 133 let func = Js.Unsafe.js_expr func_name in 134 fun () -> Js.Unsafe.fun_call func [| Js.Unsafe.inject Dom_html.window |] 135 136 let path = "/static/cmis" 137end 138 139module M = Impl.Make (S) 140 141(** Send a message back to the client *) 142let send_message msg = 143 let json = Msg.string_of_worker_msg msg in 144 Jslib.log "Worker sending: %s" json; 145 Js_of_ocaml.Worker.post_message (Js_of_ocaml.Js.string json) 146 147(** Convert exec_result to Message.Output *) 148let output_of_exec_result cell_id (r : Impl.exec_result) = 149 let mime_vals = List.map (fun (mv : Impl.mime_val) -> 150 { Msg.mime_type = mv.mime_type; data = mv.data } 151 ) r.mime_vals in 152 Msg.Output { 153 cell_id; 154 stdout = Option.value ~default:"" r.stdout; 155 stderr = Option.value ~default:"" r.stderr; 156 caml_ppf = Option.value ~default:"" r.caml_ppf; 157 mime_vals; 158 } 159 160(** Convert phrase_output to Message.OutputAt *) 161let output_at_of_phrase cell_id (p : Impl.Make(S).phrase_output) = 162 let mime_vals = List.map (fun (mv : Impl.mime_val) -> 163 { Msg.mime_type = mv.mime_type; data = mv.data } 164 ) p.mime_vals in 165 Msg.OutputAt { 166 cell_id; 167 loc = p.loc; 168 caml_ppf = Option.value ~default:"" p.caml_ppf; 169 mime_vals; 170 } 171 172(** Convert completions to Message.Completions *) 173let completions_of_result cell_id (c : Impl.completions) = 174 let entries = List.map (fun (e : Impl.query_protocol_compl_entry) -> 175 let kind = match e.kind with 176 | Constructor -> "Constructor" 177 | Keyword -> "Keyword" 178 | Label -> "Label" 179 | MethodCall -> "MethodCall" 180 | Modtype -> "Modtype" 181 | Module -> "Module" 182 | Type -> "Type" 183 | Value -> "Value" 184 | Variant -> "Variant" 185 in 186 { Msg.name = e.name; kind; desc = e.desc; info = e.info; deprecated = e.deprecated } 187 ) c.entries in 188 Msg.Completions { 189 cell_id; 190 completions = { from = c.from; to_ = c.to_; entries }; 191 } 192 193(** Convert location to Message.location *) 194let location_of_loc (loc : Impl.location) : Msg.location = 195 { 196 loc_start = { 197 pos_cnum = loc.loc_start.pos_cnum; 198 pos_lnum = loc.loc_start.pos_lnum; 199 pos_bol = loc.loc_start.pos_bol; 200 }; 201 loc_end = { 202 pos_cnum = loc.loc_end.pos_cnum; 203 pos_lnum = loc.loc_end.pos_lnum; 204 pos_bol = loc.loc_end.pos_bol; 205 }; 206 } 207 208(** Convert error_kind to string *) 209let string_of_error_kind = function 210 | Impl.Report_error -> "error" 211 | Report_warning s -> "warning:" ^ s 212 | Report_warning_as_error s -> "warning_as_error:" ^ s 213 | Report_alert s -> "alert:" ^ s 214 | Report_alert_as_error s -> "alert_as_error:" ^ s 215 216(** Convert error_source to string *) 217let string_of_error_source = function 218 | Impl.Lexer -> "lexer" 219 | Parser -> "parser" 220 | Typer -> "typer" 221 | Warning -> "warning" 222 | Unknown -> "unknown" 223 | Env -> "env" 224 | Config -> "config" 225 226(** Convert errors to Message.ErrorList *) 227let errors_of_result cell_id (errors : Impl.error list) = 228 let errors = List.map (fun (e : Impl.error) -> 229 { 230 Msg.kind = string_of_error_kind e.kind; 231 loc = location_of_loc e.loc; 232 main = e.main; 233 sub = e.sub; 234 source = string_of_error_source e.source; 235 } 236 ) errors in 237 Msg.ErrorList { cell_id; errors } 238 239(** Convert typed_enclosings to Message.Types *) 240let types_of_result cell_id (enclosings : Impl.typed_enclosings list) = 241 let types = List.map (fun ((loc, idx_or_str, tail) : Impl.typed_enclosings) -> 242 let type_str = match idx_or_str with 243 | Impl.String s -> s 244 | Index _ -> "" 245 in 246 let tail = match tail with 247 | Impl.No -> "no" 248 | Tail_position -> "tail_position" 249 | Tail_call -> "tail_call" 250 in 251 { 252 Msg.loc = location_of_loc loc; 253 type_str; 254 tail; 255 } 256 ) enclosings in 257 Msg.Types { cell_id; types } 258 259(** Convert position from int to Impl.msource_position *) 260let position_of_int pos = 261 Impl.Offset pos 262 263(** Handle a client message *) 264let handle_message msg = 265 let open Lwt.Infix in 266 match msg with 267 | Msg.Init config -> 268 let init_config : Impl.init_config = { 269 findlib_requires = config.findlib_requires; 270 stdlib_dcs = config.stdlib_dcs; 271 findlib_index = config.findlib_index; 272 execute = true; 273 } in 274 M.init init_config >>= fun result -> 275 (match result with 276 | Ok () -> 277 (* After init, automatically setup the default environment *) 278 M.setup "" >|= fun setup_result -> 279 (match setup_result with 280 | Ok _ -> send_message Msg.Ready 281 | Error (Impl.InternalError msg) -> 282 send_message (Msg.InitError { message = msg })) 283 | Error (Impl.InternalError msg) -> 284 send_message (Msg.InitError { message = msg }); 285 Lwt.return_unit) 286 287 | Msg.Eval { cell_id; env_id; code } -> 288 Jslib.log "Eval cell_id=%d env_id=%s" cell_id env_id; 289 let on_phrase_output p = send_message (output_at_of_phrase cell_id p) in 290 M.execute_incremental env_id code ~on_phrase_output >|= fun result -> 291 (match result with 292 | Ok exec_result -> 293 send_message (output_of_exec_result cell_id exec_result) 294 | Error (Impl.InternalError msg) -> 295 send_message (Msg.EvalError { cell_id; message = msg })) 296 297 | Msg.Complete { cell_id; env_id; source; position; filename } -> 298 let pos = position_of_int position in 299 M.complete_prefix ?filename env_id None [] false source pos >|= fun result -> 300 (match result with 301 | Ok completions -> 302 send_message (completions_of_result cell_id completions) 303 | Error (Impl.InternalError msg) -> 304 send_message (Msg.EvalError { cell_id; message = msg })) 305 306 | Msg.TypeAt { cell_id; env_id; source; position; filename } -> 307 let pos = position_of_int position in 308 M.type_enclosing ?filename env_id None [] false source pos >|= fun result -> 309 (match result with 310 | Ok types -> 311 send_message (types_of_result cell_id types) 312 | Error (Impl.InternalError msg) -> 313 send_message (Msg.EvalError { cell_id; message = msg })) 314 315 | Msg.Errors { cell_id; env_id; source; filename } -> 316 M.query_errors ?filename env_id None [] false source >|= fun result -> 317 (match result with 318 | Ok errors -> 319 send_message (errors_of_result cell_id errors) 320 | Error (Impl.InternalError msg) -> 321 send_message (Msg.EvalError { cell_id; message = msg })) 322 323 | Msg.CreateEnv { env_id } -> 324 M.create_env env_id >|= fun result -> 325 (match result with 326 | Ok () -> send_message (Msg.EnvCreated { env_id }) 327 | Error (Impl.InternalError msg) -> 328 send_message (Msg.InitError { message = msg })) 329 330 | Msg.DestroyEnv { env_id } -> 331 M.destroy_env env_id >|= fun result -> 332 (match result with 333 | Ok () -> send_message (Msg.EnvDestroyed { env_id }) 334 | Error (Impl.InternalError msg) -> 335 send_message (Msg.InitError { message = msg })) 336 337 | Msg.WidgetEvent { widget_id; handler_id; value; _ } -> 338 Widget.handle_event ~widget_id ~handler_id ~value; 339 Lwt.return_unit 340 341let run () = 342 let open Js_of_ocaml in 343 try 344 Console.console##log (Js.string "Starting worker (message protocol)..."); 345 346 Logs.set_reporter (Logs_browser.console_reporter ()); 347 Logs.set_level (Some Logs.Debug); 348 349 (* Initialize Widget module sender *) 350 Widget.set_sender (fun json -> 351 Jslib.log "Widget sending: %s" json; 352 Js_of_ocaml.Worker.post_message (Js_of_ocaml.Js.string json)); 353 354 Js_of_ocaml.Worker.set_onmessage (fun x -> 355 let s = Js_of_ocaml.Js.to_string x in 356 Jslib.log "Worker received: %s" s; 357 try 358 let msg = Msg.client_msg_of_string s in 359 Lwt.async (fun () -> handle_message msg) 360 with e -> 361 Jslib.log "Error parsing message: %s" (Printexc.to_string e); 362 send_message (Msg.InitError { message = Printexc.to_string e })); 363 364 Console.console##log (Js.string "Worker ready") 365 with e -> 366 Console.console##log (Js.string ("Exception: " ^ Printexc.to_string e))