package merlin-lib

  1. Overview
  2. Docs
Merlin's libraries

Install

dune-project
 Dependency

Authors

Maintainers

Sources

merlin-5.8.1-505.tbz
sha256=b8fb32bc0fc092af2fd6bdc831cb966057f2e3fd7b99a172b705e96ba8082583
sha512=01ca96f8167d062ba24036e43f650ff958fb157d44867bd52eb7999b7d19bf9fc97cdcd46c04b6979f0e1149d5041047723eed5913b03c4404d7acb116183bee

doc/src/merlin-lib.index_format/granular_marshal.ml.html

Source file granular_marshal.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
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
module Cache = Hashtbl.Make (Int)

type store = { filename : string; id : int; cache : cache }

and cache = any_link Cache.t

and any_link = Link : 'a link * 'a link Type.Id.t option -> any_link

and parent_link = PLink : 'a link -> parent_link
and any_value =
  | Value : 'a * 'a link Type.Id.t -> any_value
  | Unknown : 'a -> any_value
      (** Marks a small that has not been cleaned yet. Usually because its
          schema was unknown when it was read from the disk. *)
and any_val = V : 'a -> any_val
and any_val_link = Vlink : 'a * 'a link -> any_val_link

and cached = Cached : 'a link -> cached

and 'a link = 'a repr ref

(** Links descriptions.
  There are two different realms: on disk and in memory.
  Things such as On_disk cannot live on disk since the contain a function, schema.
  _ Type.Id.t cannot survive to marshalling either.

  We call "cleaning a value" the process of translates links from the Disk Realm
  to the Memory Realm.

  A lot of the complexity stems from the "small values" optimization. It can be
  seen as an inlining of small-enough values with their parent value. This is
  important both for speed and file size. It removes the overhead of having many
  links which is not worth for small values.

  When reading a small value, its parent value might have an unknown schema,
  resulting in a dirty cache entry. This is marked by [Dirty_unknown_schema].
  Silimarly, small values are dirty until they are explicitely needed, and thus
  their schema known.
*)
and 'a repr =
  (*
   * On-disk realm
   *)
  | Serialized of { loc : int }
      (** {i on-disk} A pointer to a serialized value in the file. *)
  | Small of int
      (** {i on-disk} A "small value" placeholder. Contains the index of this
          small's actual value in the array stored by its parent value. *)
  | Serialized_small of { loc : int; pos : int }
      (** {i on-disk} A pointer to an already serialized small value. *)
  | On_disk_ptr of { filename : string; loc : int; id : int }
      (** {i on-disk} A pointer to a serialized value in another file. The
          optional `pos` field is used to target small values. *)
  | On_disk_small_ptr of { filename : string; loc : int; id : int; pos : int }
  (*
   * In-memory realm
   *)
  | On_disk of { store : store; loc : int; schema : 'a schema option }
      (** {i in-memory} A value that can be read from the disk. *)
  | On_disk_small of
      { parent : parent_link; (* Either the parent or an On_disk_ptr *)
        small_type_id : 'a link Type.Id.t;
        small_pos : int;
        small_schema : 'a schema
      }
      (** {i in-memory} A small value whose parent can be read from the disk. *)
  | In_memory of 'a
      (** {i in-memory} A value that has been created in memory. *)
  | In_memory_reused of 'a
      (** {i in-memory} A value that has been created in memory and is used
          multiple times. *)
  | In_cache of
      { value : 'a;
        store : store;
        loc : int;
        mutable schema : 'a schema option;
        cell : cached Dbllist.cell;
        small_values : any_value array
      }
      (** {i in-memory} A value and its small that has been already read from
          the disk. Both the values and the smalls might be "unclean". They will
          be promoted to clean if read with their expected schema. *)
  | Duplicate of 'a link

and 'a schema = iter -> 'a -> unit

and iter = { yield : 'a. 'a link -> 'a link Type.Id.t -> 'a schema -> unit }

let string_of_link : type a. a link -> string =
 fun link ->
  match !link with
  | Small _ -> Printf.sprintf "Small"
  | Serialized { loc } -> Printf.sprintf "Serialized(loc=%d)" loc
  | On_disk { loc; _ } -> Printf.sprintf "On_disk(loc=%d)" loc
  | On_disk_small { small_pos; _ } ->
    Printf.sprintf "On_disk_small(small_pos=%d)" small_pos
  | Serialized_small { loc; pos } ->
    Printf.sprintf "Serialized_small(loc=%d;small_pos=%d)" loc pos
  | On_disk_ptr { loc; _ } -> Printf.sprintf "On_disk_ptr(loc=%d)" loc
  | On_disk_small_ptr { loc; pos; _ } ->
    Printf.sprintf "On_disk_ptr(loc=%d, pos=%d)" loc pos
  | In_memory _ -> "In_memory"
  | In_cache { schema; loc; _ } ->
    let clean_dirty =
      match schema with
      | Some _ -> "Clean"
      | None -> "Dirty"
    in
    Printf.sprintf "In_cache(%s; loc=%i)" clean_dirty loc
  | In_memory_reused _ -> "In_memory_reused"
  | Duplicate _ -> "Duplicate"

exception
  Outdated_store of
    { filename : string; reason : [ `Missing_file | `Index_ids_do_not_match ] }

let lru_dbllist : cached Dbllist.t option ref = ref None
let lru_size = ref 1_000_000
let set_lru_size i = lru_size := i

let get_lru () =
  match !lru_dbllist with
  | Some lru -> lru
  | None ->
    let lru = Dbllist.create !lru_size in
    lru_dbllist := Some lru;
    lru

let schema_no_sublinks : _ schema = fun _ _ -> ()

let link v = ref (In_memory v)

let rec normalize lnk =
  match !lnk with
  | Duplicate lnk -> normalize lnk
  | _ -> lnk

let is_on_disk lnk =
  match !(normalize lnk) with
  | On_disk _ | On_disk_ptr _ | On_disk_small _ | In_cache _ -> true
  | _ -> false

module Cache_cache = File_cache.Make (struct
  type t = cache
  let read _filename = Cache.create 0

  let cache_name = "Cache_cache"
end)

let ptr_size = 8

let binstring_of_int v =
  String.init ptr_size (fun i -> Char.chr ((v lsr i lsl 3) land 255))

let int_of_binstring s =
  Array.fold_right
    (fun v acc -> (acc lsl 8) + v)
    (Array.init ptr_size (fun i -> Char.code s.[i]))
    0

let last_open_store = ref None

let force_open_store store =
  try
    let fd = open_in_bin store.filename in
    seek_in fd (String.length Config.index_magic_number);
    let required_id = int_of_binstring (really_input_string fd ptr_size) in
    if required_id = store.id then (
      last_open_store := Some (store, fd);
      fd)
    else
      raise
        (Outdated_store
           { filename = store.filename; reason = `Index_ids_do_not_match })
  with Sys_error _ ->
    raise (Outdated_store { filename = store.filename; reason = `Missing_file })

let open_store store =
  match !last_open_store with
  | Some (store', fd)
    when Int.equal store.id store'.id
         && String.equal store.filename store'.filename -> fd
  | Some (_, fd) ->
    close_in fd;
    force_open_store store
  | None -> force_open_store store

let resolve_filename store ~filename =
  if Filename.is_relative filename then
    Filename.concat (Filename.dirname store.filename) filename
  else filename

(** This iterator translates links from the Disk Realm to the Memory Realm.
    This is the process we refer too as "cleaning a value". *)
let rec disk_to_memory_iter store parent_link =
  { yield =
      (fun (type a)
        (lnk : a link)
        (type_id : a link Type.Id.t)
        (schema : a schema)
      ->
        match !lnk with
        | Small pos ->
          lnk :=
            On_disk_small
              { parent = parent_link;
                small_pos = pos;
                small_type_id = type_id;
                small_schema = schema
              }
        | Serialized_small { loc; pos } ->
          let parent =
            match Cache.find_opt store.cache loc with
            | Some (Link (lnk, _)) -> PLink (normalize lnk)
            | None ->
              let lnk = ref (On_disk { store; loc; schema = None }) in
              Cache.add store.cache loc (Link (lnk, None));
              PLink lnk
          in
          lnk :=
            On_disk_small
              { parent;
                small_type_id = type_id;
                small_schema = schema;
                small_pos = pos
              }
        | Serialized { loc } ->
          maybe_reuse_or_upgrade lnk store loc type_id schema
        | On_disk_ptr { filename; loc; id } ->
          let filename = resolve_filename store ~filename in
          let store = { filename; id; cache = Cache_cache.read filename } in
          maybe_reuse_or_upgrade lnk store loc type_id schema
        | On_disk_small_ptr { filename; loc; id; pos = small_pos } ->
          let filename = resolve_filename store ~filename in
          let store = { filename; id; cache = Cache_cache.read filename } in
          let parent =
            match Cache.find_opt store.cache loc with
            | Some (Link (lnk, _)) -> PLink (normalize lnk)
            | None ->
              let lnk = ref (On_disk { store; loc; schema = None }) in
              Cache.add store.cache loc (Link (lnk, None));
              PLink lnk
          in
          lnk :=
            On_disk_small
              { parent;
                small_type_id = type_id;
                small_schema = schema;
                small_pos
              }
        | In_memory _
        | In_cache _
        | In_memory_reused _
        | On_disk_small _
        | On_disk _
        | Duplicate _ -> (* These are already "clean" *) ())
  }

(* Checks if a loc has already been read, clean it if it was read without
   schema, or store it in the cache if it was never read.*)
and maybe_reuse_or_upgrade : type a.
    a link -> store -> int -> a link Type.Id.t -> a schema -> unit =
 fun lnk store loc type_id schema ->
  match Cache.find_opt store.cache loc with
  | Some (Link (type b) ((lnk', Some type_id') : b link * _)) -> (
    match Type.Id.provably_equal type_id type_id' with
    | Some (Equal : (a link, b link) Type.eq) ->
      lnk := Duplicate (normalize lnk')
    | None -> invalid_arg "Granular_marshal.read_loc: reuse of a different type"
    )
  | Some (Link (lnk', None)) ->
    let lnk' = Obj.magic lnk' in
    upgrade_reused_link_schema lnk' store schema;
    Cache.replace store.cache loc (Link (lnk', Some type_id));
    lnk := Duplicate (normalize lnk')
  | None ->
    lnk := On_disk { store; loc; schema = Some schema };
    Cache.add store.cache loc (Link (lnk, Some type_id))

(* Sometimes we reuse a parent whose schema was unknown so far.
   This function updates this parent's schema. *)
and upgrade_reused_link_schema : type a. a link -> store -> a schema -> unit =
 fun lnk store schema ->
  match !lnk with
  | On_disk { store; loc; schema = None; _ } ->
    (* This case only happens if the previous read was an
                    [On_disc_ptr { pos = Some_; _}] with a parent of unknown
                    schema. *)
    lnk := On_disk { store; loc; schema = Some schema }
  | In_cache ({ value; schema = None; _ } as c) ->
    (* If we already have the value in cache we must clean it. *)
    schema (disk_to_memory_iter store (PLink lnk)) value;
    c.schema <- Some schema
  | Small _
  | Serialized _
  | Serialized_small _
  | On_disk _
  | On_disk_small _
  | On_disk_ptr _
  | On_disk_small_ptr _
  | In_memory _
  | In_cache _
  | In_memory_reused _
  | Duplicate _ -> assert false

let add_to_cache value lnk ~loc store ~size small_values schema =
  let discarded = Dbllist.discard_size (get_lru ()) size in
  let cell = Dbllist.add_front (get_lru ()) (Cached lnk, size) in
  List.iter
    (fun (Cached link) ->
      (* This also free the smalls that are stored in the link *)
      match !link with
      | In_cache { loc; store; schema; _ } ->
        link := On_disk { store; loc; schema }
      | _ -> assert false)
    discarded;
  lnk := In_cache { value; loc; store; schema; cell; small_values }

(** Read one value and its smalls from the disk.  *)
let read_loc_dirty fd loc =
  seek_in fd loc;
  let v, small_children = Marshal.from_channel fd in
  let size_read = pos_in fd - loc in
  let small_children = Array.map (fun (V v) -> Unknown v) small_children in
  (v, size_read, small_children)

(** Read one value and its smalls from the disk. Clean it. The smalls are not
    cleaned yet because their schema is unknown at that point.  *)
let read_loc store fd loc schema parent_link =
  let v, size_read, small_children = read_loc_dirty fd loc in
  let iter = disk_to_memory_iter store parent_link in
  schema iter v;
  (v, size_read, small_children)

(** Reads a value with its smalls, clean it and add it to the cache *)
let fetch_on_disk lnk store loc schema =
  let fd = open_store store in
  let parent_link = PLink lnk in
  let v, size, small_values = read_loc store fd loc schema parent_link in
  add_to_cache v lnk ~loc store ~size small_values (Some schema);
  (v, small_values)

let fetch_on_disk_dirty lnk store loc =
  let fd = open_store store in
  let v, size, small_children = read_loc_dirty fd loc in
  add_to_cache v lnk ~loc store ~size small_children None;
  small_children

(* Smalls are stored along their parents so they share the same loc *)
let store_and_loc_of_parent (PLink lnk : parent_link) =
  match !lnk with
  | In_cache { store; loc; _ } | On_disk { store; loc; _ } -> (store, loc)
  | _ ->
    invalid_arg
      ("Granular_marshal.fetch_parent: Unexpected parent link "
     ^ string_of_link lnk)

(** Fetch the parent of a small value in order to read its smalls. If the parent
  has not yet been loaded in memory it will be read from the disk and kept dirty
  because its schema is unknown.*)
let fetch_parent_smalls : parent_link -> any_value array * store =
 fun (PLink parent_link) ->
  match !parent_link with
  | In_cache { store; cell; small_values; _ } ->
    Dbllist.promote (get_lru ()) cell;
    (small_values, store)
  | On_disk { store; loc; schema = None } ->
    (fetch_on_disk_dirty parent_link store loc, store)
  | On_disk { store; loc; schema = Some schema } ->
    (snd (fetch_on_disk parent_link store loc schema), store)
  | _ ->
    invalid_arg
      ("Granular_marshal.fetch_parent: Unexpected parent link "
     ^ string_of_link parent_link)

let rec fetch : type a. a link -> a =
 fun lnk ->
  match !lnk with
  | In_cache { value; schema = Some _; cell; _ } ->
    Dbllist.promote (get_lru ()) cell;
    value
  | In_cache { schema = None; _ } ->
    invalid_arg "Granular_marshal.fetch: accessing dirty cached value"
  | Duplicate original_lnk -> fetch original_lnk
  | On_disk { store; loc; schema = Some schema } ->
    fst (fetch_on_disk lnk store loc schema)
  | On_disk_small { parent; small_pos; small_type_id; small_schema } -> (
    let smalls, store = fetch_parent_smalls parent in
    match smalls.(small_pos) with
    | Value (type b) ((v, type_id') : b * _) -> (
      match Type.Id.provably_equal small_type_id type_id' with
      | None -> invalid_arg "Granular_marshal.read_loc: small has wrong type"
      | Some (Equal : (a link, b link) Type.eq) -> v)
    | Unknown v ->
      let v = Obj.magic v in
      small_schema (disk_to_memory_iter store parent) v;
      smalls.(small_pos) <- Value (v, small_type_id);
      v)
  | In_memory v | In_memory_reused v -> v
  | Serialized _
  | Serialized_small _
  | Small _
  | On_disk_ptr _
  | On_disk_small_ptr _
  | On_disk { schema = None; _ } ->
    invalid_arg
      ("Granular_marshal.fetch: accesssing dirty link " ^ string_of_link lnk)

let rec reuse original_lnk =
  match !original_lnk with
  | In_memory v -> original_lnk := In_memory_reused v
  | In_memory_reused _ -> ()
  | On_disk _ -> ()
  | Duplicate link -> reuse link
  | _ ->
    invalid_arg
    @@ Printf.sprintf "Granular_marshal.reuse: not in memory, got %s"
         (string_of_link original_lnk)

let cache (type a) (module Key : Hashtbl.HashedType with type t = a) =
  let module H = Hashtbl.Make (Key) in
  let cache = H.create 16 in
  fun (lnk : a link) ->
    if not (is_on_disk lnk) then
      let key = fetch lnk in
      match H.find cache key with
      | original_lnk ->
        assert (original_lnk != lnk);
        reuse original_lnk;
        lnk := Duplicate original_lnk
      | exception Not_found -> H.add cache key lnk

let relativize ~wrt:path =
  let path_segments = Misc.split_path path in
  let rec aux path target =
    match (path, target) with
    | p :: tl, t :: tl_target when p = t -> aux tl tl_target
    | [], target -> target
    | _ :: _, [] -> List.map (Fun.const "..") path
    | _ :: _, _ :: _ -> List.map (Fun.const "..") path @ target
  in
  fun target ->
    let target_segments = Misc.split_path target in
    List.fold_left Filename.concat "" (aux path_segments target_segments)

let write ?(flags = []) fd ~filename ~id root_schema root_value =
  let relativize =
    relativize ~wrt:Filename.(dirname (concat (Unix.getcwd ()) filename))
  in
  let id' = binstring_of_int id in
  output_string fd id';
  let pt_root = pos_out fd in
  output_string fd (String.make ptr_size '\000');
  let rec iter size ~small_children =
    { yield =
        (fun (type a) (lnk : a link) _type_id (schema : a schema) : unit ->
          match !lnk with
          | Serialized _
          | Serialized_small _
          | Small _
          | On_disk_ptr _
          | On_disk_small_ptr _ -> ()
          | In_memory_reused v -> write_child_reused lnk schema v
          | Duplicate original_lnk -> (
            match !original_lnk with
            | Serialized _ | Serialized_small _ | On_disk_ptr _ ->
              lnk := !original_lnk
            | In_memory_reused v ->
              write_child_reused original_lnk schema v;
              lnk := !original_lnk
            | On_disk { store = { filename; id; _ }; loc; _ }
            | In_cache { loc; store = { filename; id; _ }; _ } ->
              lnk := On_disk_ptr { filename; id; loc }
            | _ ->
              failwith
                (Format.sprintf
                   "Granular_marshal.write: duplicate not reused got %s"
                   (string_of_link original_lnk)))
          | In_memory v -> write_child lnk schema v size ~small_children
          | In_cache { loc; store = { filename; id; _ }; _ } ->
            let filename = relativize filename in
            lnk := On_disk_ptr { filename; id; loc }
          | On_disk { store = { filename; id; _ }; loc; _ } ->
            (* TODO we could have all the possible filenames wrote once
               somewhere in the file. *)
            let filename = relativize filename in
            lnk := On_disk_ptr { filename; id; loc }
          | On_disk_small { parent; small_pos; _ } ->
            let { filename; id; _ }, loc = store_and_loc_of_parent parent in
            let filename = relativize filename in
            lnk := On_disk_small_ptr { filename; id; loc; pos = small_pos })
    }
  and output_and_mark (V v) (small_children : any_val_link list) =
    let new_smalls =
      (* Some smalls might have been already serialized by another value *)
      List.filter
        (fun (Vlink (_v, lnk)) ->
          match !lnk with
          | On_disk_small_ptr _ | Serialized_small _ ->
            (* This small has already been serialized by another owner *) false
          | _ -> true)
        small_children
    in
    let smalls =
      (* We iter on the smalls to set their links with the position in the array and *)
      List.mapi
        (fun i (Vlink (v, lnk)) ->
          lnk := Small i;
          V v)
        new_smalls
      |> Array.of_list
    in
    let loc = pos_out fd in
    Marshal.to_channel fd (v, smalls) flags;
    (* Now we replace the links by an indirection in case they are reused *)
    List.iteri
      (fun i (Vlink (_v, lnk)) -> lnk := Serialized_small { loc; pos = i })
      new_smalls
  and write_child : type a. a link -> a schema -> a -> _ =
   fun lnk schema v size ~small_children ->
    let v_size, v_smalls = write_children schema v in
    if v_size > 4096 then (
      lnk := Serialized { loc = pos_out fd };
      output_and_mark (V v) v_smalls)
    else (
      size := !size + v_size;
      (* We don't care about the order since smalls are numbered right before
         writing to the disk. *)
      let smalls = List.rev_append v_smalls !small_children in
      small_children := Vlink (v, lnk) :: smalls)
  and write_children : type a. a schema -> a -> _ =
   fun schema v ->
    let children_size = ref 0 in
    let small_children = ref [] in
    schema (iter children_size ~small_children) v;
    let v_size = Obj.(reachable_words (repr v)) in
    (!children_size + v_size, !small_children)
  and write_child_reused : type a. a link -> a schema -> a -> unit =
   fun lnk schema v ->
    let _v_size, v_smalls = write_children schema v in
    lnk := Serialized { loc = pos_out fd };
    output_and_mark (V v) v_smalls
  in
  let _, root_value_smalls = write_children root_schema root_value in
  let root_loc = pos_out fd in
  output_and_mark (V root_value) root_value_smalls;
  seek_out fd pt_root;
  output_string fd (binstring_of_int root_loc)

let read filename fd root_schema =
  let id = int_of_binstring (really_input_string fd 8) in
  let filename =
    if Filename.is_relative filename then
      Filename.concat (Unix.getcwd ()) filename
    else filename
  in
  let store = { filename; id; cache = Cache_cache.read filename } in
  let root_loc = int_of_binstring (really_input_string fd 8) in
  let parent_link =
    ref (On_disk { loc = root_loc; store; schema = Some root_schema })
  in
  let root_value, _, _ =
    read_loc store fd root_loc root_schema (PLink parent_link)
  in
  root_value

let () =
  at_exit (fun () ->
      match !last_open_store with
      | None -> ()
      | Some (_, fd) -> close_in fd)