package ppx_interact

  1. Overview
  2. Docs
Legend:
Page
Library
Module
Module type
Parameter
Class
Class type
Source

Source file ppx_interact_runtime.ml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
(* box-drawing characters *)
let box_h = "─"
let box_v = "│"
let box_t = "┬"
let box_bot = "┴"

let view_file ?(context = (4, 2)) line file =
  let before, after = context in
  let show () =
    let ic = open_in file in
    let rec loop skip left =
      if left <= 0 then []
      else
        try
          let line = input_line ic in
          if skip > 0 then loop (skip - 1) left
          else
            let line = if skip = 0 then line else line in
            line :: loop 0 (left - 1)
        with End_of_file -> []
    in
    let lines = loop (max 0 (line - before - 1)) (before + after + 1) in
    let line_number_width =
      2 + (log10 (line + after |> float_of_int) |> int_of_float)
    in
    let title_width = line_number_width + 3 in
    let divider joint =
      List.init 60 (fun i -> if i = line_number_width + 1 then joint else box_h)
      |> String.concat ""
    in
    Format.printf "%s@." (divider box_h);
    Format.printf "%s@." (String.init title_width (fun _ -> ' ') ^ file);
    Format.printf "%s@." (divider box_t);
    List.iteri
      (fun i l ->
        Format.printf "%*d %s %s\n" line_number_width
          (i + max 1 (line - before))
          box_v l)
      lines;
    Format.printf "%s@." (divider box_bot);
    close_in ic
  in
  match Sys.getenv_opt "NO_BAT" with
  | Some _ -> show ()
  | None ->
    let open Unix in
    (match
       create_process "bat"
         [|
           "--paging=never";
           "--line-range";
           Format.asprintf "%d:%d" (line - before) (line + after);
           "--highlight-line";
           string_of_int line;
           file;
           "--style";
           "header,numbers,grid";
         |]
         stdin stdout stderr
       |> waitpid [] |> snd
     with
    | WEXITED 0 -> ()
    | WEXITED _ | WSIGNALED _ | WSTOPPED _
    | (exception Unix_error (ENOENT, "create_process", "bat")) ->
      show ())

let eval ~show text =
  let lexbuf = Lexing.from_string text in
  let phrase = !Toploop.parse_toplevel_phrase lexbuf in
  ignore (Toploop.execute_phrase show Format.std_formatter phrase)

exception Found of Env.t
exception Term of int

type value = V : string * _ -> value

let walk dir ~init ~f =
  let rec loop dir acc =
    let acc = f dir acc in
    ArrayLabels.fold_left (Sys.readdir dir) ~init:acc ~f:(fun acc fn ->
        let fn = Filename.concat dir fn in
        match Unix.lstat fn with
        | { st_kind = S_DIR; _ } -> loop fn acc
        | _ -> acc)
  in
  match Unix.lstat dir with
  | exception Unix.Unix_error (ENOENT, _, _) -> init
  | _ -> loop dir init

(** https://github.com/ocaml/ocaml/blob/trunk/toplevel/toploop.ml *)
module Toploop2 = struct
  exception PPerror

  let phrase_buffer = Buffer.create 1024

  let loop () =
    let ppf = Format.std_formatter in
    Clflags.debug := true;
    Location.formatter_for_warnings := ppf;
    (* don't initialize the toplevel environment, as we don't want to clear bindings passed in *)
    let lb = Lexing.from_function Topcommon.refill_lexbuf in
    Location.init lb "//toplevel//";
    Location.input_name := "//toplevel//";
    Location.input_lexbuf := Some lb;
    Location.input_phrase_buffer := Some phrase_buffer;
    Sys.catch_break true;
    (* loading ocamlinit is done elsewhere *)
    try
      while true do
        let snap = Btype.snapshot () in
        try
          Lexing.flush_input lb;
          Buffer.reset phrase_buffer;
          Location.reset ();
          Warnings.reset_fatal ();
          Topcommon.first_line := true;
          let phr =
            try !Toploop.parse_toplevel_phrase lb with Exit -> raise PPerror
          in
          let phr = Toploop.preprocess_phrase ppf phr in
          Env.reset_cache_toplevel ();
          ignore (Toploop.execute_phrase true ppf phr)
        with
        | Sys.Break ->
          Btype.backtrack snap;
          raise End_of_file
        | PPerror -> ()
        | x ->
          Location.report_exception ppf x;
          Btype.backtrack snap
      done
    with End_of_file -> ()

  (* modified to return all .ocamlinit files, in order *)
  let find_ocamlinit () =
    let exists_in_dir dir file =
      match dir with
      | None -> None
      | Some dir ->
        let file = Filename.concat dir file in
        if Sys.file_exists file then Some file else None
    in
    let home_dir () = Sys.getenv_opt "HOME" in
    let config_dir () =
      if Sys.win32 then None
      else
        match Sys.getenv_opt "XDG_CONFIG_HOME" with
        | Some _ as v -> v
        | None ->
          (match home_dir () with
          | None -> None
          | Some dir -> Some (Filename.concat dir ".config"))
    in
    let init_ml = Filename.concat "ocaml" "init.ml" in
    let ocamlinit = ".ocamlinit" in
    let local = if Sys.file_exists ocamlinit then [ocamlinit] else [] in
    let global =
      match exists_in_dir (config_dir ()) init_ml with
      | Some v -> [v]
      | None ->
        (match exists_in_dir (home_dir ()) ocamlinit with
        | Some v -> [v]
        | None -> [])
    in
    (* load global first, then local *)
    global @ local
end

let linenoise_prompt completion_words =
  let rec user_input prompt f =
    match LNoise.linenoise prompt with
    | None -> ()
    | Some v ->
      f v;
      user_input prompt f
  in
  (* this goes from front-to-back, which is the right order, so more recent bindings are suggested first *)
  LNoise.set_hints_callback (fun inp ->
      match inp with
      | "" -> None
      | _ ->
        Option.bind
          (List.find_opt (String.starts_with ~prefix:inp) completion_words)
          (fun sugg ->
            let sl = String.length sugg in
            let il = String.length inp in
            if il < sl then
              let s = String.sub sugg il (sl - il) in
              Some (s, LNoise.White, false)
            else None));
  LNoise.set_completion_callback (fun so_far ln_completions ->
      List.filter (String.starts_with ~prefix:so_far) completion_words
      |> List.iter (LNoise.add_completion ln_completions));
  user_input "> " (fun s ->
      let s = String.trim s in
      let doesn't_end_with_semicolons s =
        let l = String.length s in
        l < 2 || String.sub s (l - 2) 2 <> ";;"
      in
      let s = if doesn't_end_with_semicolons s then s ^ ";;" else s in
      LNoise.history_add s |> ignore;
      (* LNoise.history_save ~filename:"history.txt" |> ignore; *)
      try eval ~show:true s
      with exn -> Location.report_exception Format.err_formatter exn)

(** see https://github.com/ocaml-community/utop/blob/master/src/lib/uTop_main.ml *)
let interact ?(search_path = []) ?(build_dir = "_build") ~unit
    ~loc:(fname, lnum, cnum, _) ?(init = []) ~values () =
  let verbose = Sys.getenv_opt "VERBOSE" |> Option.is_some in
  Toploop.initialize_toplevel_env ();
  let search_path =
    walk build_dir ~init:search_path ~f:(fun dir acc -> dir :: acc)
  in
  let cmt_fname =
    try Unstable.Misc.find_in_path_uncap search_path (unit ^ ".cmt")
    with Not_found ->
      Printf.ksprintf failwith "%s.cmt not found in search path!" unit
  in
  let cmt_infos = Cmt_format.read_cmt cmt_fname in
  let get_required_label name args =
    match List.find (fun (lab, _) -> lab = Asttypes.Labelled name) args with
    | _, x -> x
    | exception Not_found -> None
  in
  let expr next (e : Typedtree.expression) =
    match e.exp_desc with
    | Texp_apply (_, args) ->
      begin
        try
          match
            (get_required_label "loc" args, get_required_label "values" args)
          with
          | Some l, Some v ->
            let pos = l.exp_loc.loc_start in
            if
              pos.pos_fname = fname && pos.pos_lnum = lnum
              && pos.pos_cnum - pos.pos_bol = cnum
            then raise (Found v.exp_env)
          | _ -> next e
        with Not_found -> next e
      end
    | _ -> next e
  in
  let next iterator e = Tast_iterator.default_iterator.expr iterator e in
  let expr iterator = expr (next iterator) in
  let iter = { Tast_iterator.default_iterator with expr } in
  let search = iter.structure iter in
  try
    begin
      match cmt_infos.cmt_annots with
      | Implementation st -> search st
      | _ -> ()
    end;
    failwith "Couldn't find location in cmt file"
  with Found env ->
    (try
       List.iter Topdirs.dir_directory (search_path @ Unstable.get_load_paths cmt_infos);
       let env = Envaux.env_of_only_summary env in
       List.iter
         (fun (V (name, v)) -> Toploop.setvalue name (Obj.repr v))
         values;
       Toploop.toplevel_env := env;
       (* let idents = Env.diff Env.empty env in *)
       (* List.iter print_endline (List.map Ident.name idents); *)
       let names = List.map (fun (V (name, _)) -> name) values in

       List.iter
         (fun line ->
           try eval ~show:verbose line
           with exn ->
             Format.printf "initialization failed: %s@." line;
             Location.report_exception Format.err_formatter exn)
         init;

       List.iter
         (fun oi ->
           let ic = open_in oi in
           let s = really_input_string ic (in_channel_length ic) in
           begin
             try eval ~show:verbose s with
             | End_of_file -> ()
             | exn -> Location.report_exception Format.err_formatter exn
           end;
           close_in_noerr ic;
           if verbose then Format.printf "Loaded %s@." oi)
         (Toploop2.find_ocamlinit ());

       let use_linenoise =
         Option.is_some (Sys.getenv_opt "NO_DOWN")
         ||
         try
           Load_path.find "down.top" |> ignore;
           Toploop.use_file Format.std_formatter "down.top" |> not
         with Not_found -> true
       in

       (* eval "b;;"; *)
       (* eval "let c = b + 1;;"; *)
       (* let v : int = Obj.obj (Toploop.getvalue "c") in *)
       (* Format.printf "v = %d@." v; *)
       match use_linenoise with
       | false -> Toploop2.loop ()
       | true -> linenoise_prompt names
     with exn ->
       Location.report_exception Format.err_formatter exn;
       exit 2)
OCaml

Innovation. Community. Security.