Source file trivia.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
type position = Line_start | Inline
type kind = Line_comment | Block_comment | Annotation
type trivia = Item of { content : string; kind : kind } | Blank_line
type entry = { anchor : int; trivia : trivia; position : position }

type associated = {
  before : entry list;
  within : entry list;
  after : entry list;
}

type t = (Ast.location, associated) Hashtbl.t

type context = {
  mutable comments : entry list;
  mutable at_start_of_line : bool;
  mutable prev_token_end : int;
  mutable locations : Ast.location list;
}

let make () =
  { comments = []; at_start_of_line = true; prev_token_end = 0; locations = [] }

let add_entry ctx entry = ctx.comments <- entry :: ctx.comments

let report_item ctx kind content =
  add_entry ctx
    {
      anchor = ctx.prev_token_end;
      trivia = Item { content; kind };
      position = (if ctx.at_start_of_line then Line_start else Inline);
    };
  ctx.at_start_of_line <- kind = Line_comment

let report_newline ctx =
  if ctx.at_start_of_line then
    add_entry ctx
      {
        anchor = ctx.prev_token_end;
        trivia = Blank_line;
        position = Line_start;
      };
  ctx.at_start_of_line <- true

let report_token ctx pos =
  ctx.at_start_of_line <- false;
  ctx.prev_token_end <- pos

let with_pos ctx info desc =
  ctx.locations <- info :: ctx.locations;
  { Ast.desc; info }

let drop_in_ranges ctx ranges =
  match ranges with
  | [] -> ()
  | _ ->
      let ranges =
        List.sort (fun (a, _) (b, _) -> compare (a : int) b) ranges
      in
      (* [ctx.comments] is built in reverse (most recent first); [List.rev] puts
         it back in lexing order. A stable sort by anchor then keeps the lexing
         order of comments sharing an anchor (consecutive line comments anchor
         at the same preceding token). Sweep the ascending comments alongside
         the ascending ranges in one pass, dropping any comment whose anchor
         lies in a deleted range [\[start, end)], and restore the reverse order
         {!associate} expects. *)
      let comments =
        List.stable_sort
          (fun a b -> compare a.anchor b.anchor)
          (List.rev ctx.comments)
      in
      let rec sweep ranges = function
        | [] -> []
        | (c : entry) :: rest -> (
            let rec skip = function
              | (_, e) :: rs when e <= c.anchor -> skip rs
              | rs -> rs
            in
            let ranges = skip ranges in
            match ranges with
            | (s, _) :: _ when s <= c.anchor -> sweep ranges rest
            | _ -> c :: sweep ranges rest)
      in
      ctx.comments <- List.rev (sweep ranges comments)

let associate ?only ctx =
  (* Only consider locations the caller will actually look up while printing
     (when [only] is given). A comment otherwise risks being attached to a node
     that the printer never emits trivia for — e.g. a struct-field label printed
     via its [.desc] only — and would then be silently dropped. Restricting to
     emitted locations makes every comment bubble up to a location that prints. *)
  let locations =
    match only with
    | None -> ctx.locations
    | Some set -> List.filter (fun l -> Hashtbl.mem set l) ctx.locations
  in
  let tbl = Hashtbl.create (List.length locations) in
  let comments = List.rev ctx.comments in
  let locs =
    List.sort
      (fun a b ->
        let c =
          compare a.Ast.loc_start.Lexing.pos_cnum
            b.Ast.loc_start.Lexing.pos_cnum
        in
        if c <> 0 then c
        else compare b.Ast.loc_end.Lexing.pos_cnum a.Ast.loc_end.Lexing.pos_cnum)
      locations
  in
  (* Collapse identical spans: a single source range is often recorded by more
     than one node (e.g. a [Get] instruction and the identifier it wraps both
     span the same name), and the printer looks each up but, via [seen], only
     the first carries the trivia. Two same-range entries would otherwise make
     [process] treat one as the other's child, and the "steal the last child's
     trailing comments" path then hands the parent the child's (empty) [after]
     instead of computing the gap up to the next sibling — silently dropping a
     trailing comment anchored just past the span. *)
  let locs =
    let same a b =
      a.Ast.loc_start.Lexing.pos_cnum = b.Ast.loc_start.Lexing.pos_cnum
      && a.Ast.loc_end.Lexing.pos_cnum = b.Ast.loc_end.Lexing.pos_cnum
    in
    (* Tail-recursive (accumulate then reverse): [a :: dedup rest] is not
       tail-recursive (OCaml's tail-modulo-cons is opt-in, via [@tail_mod_cons]),
       so it recurses to the list length. The native stack absorbs that, but the
       much smaller wasm call stack overflows on a large module with many
       recorded locations (e.g. formatting a big .wat in the editor). *)
    let rec dedup acc = function
      | a :: (b :: _ as rest) when same a b -> dedup acc rest
      | a :: rest -> dedup (a :: acc) rest
      | [] -> List.rev acc
    in
    dedup [] locs
  in
  let pos_of_entry e = e.anchor in
  let split_before threshold comments =
    let rec aux acc = function
      | c :: rest when pos_of_entry c < threshold -> aux (c :: acc) rest
      | rest -> (List.rev acc, rest)
    in
    aux [] comments
  in
  (* Trailing comments of a node: those anchored in [\[parent_end, upto)], where
     [upto] is the next sibling's start (so a comment separated from the node by
     a punctuation token — e.g. a list comma — still trails it rather than
     leading the next sibling). An inline line comment ends the node's line and
     is its trailing comment; a line-start comment or blank line begins the next
     sibling and is left in place. *)
  let get_after parent_end ~upto comments =
    let in_gap anchor = anchor >= parent_end && anchor < upto in
    let rec aux acc = function
      | ({ anchor; trivia = Item { kind = Line_comment; _ }; position = Inline }
         as c)
        :: rest
        when in_gap anchor ->
          (List.rev (c :: acc), rest)
      | ({
           anchor;
           trivia = Item { kind = Line_comment; _ };
           position = Line_start;
         } as c)
        :: rest
        when in_gap anchor ->
          (List.rev acc, c :: rest)
      | ({ anchor; trivia = Item _; _ } as c) :: rest when in_gap anchor ->
          aux (c :: acc) rest
      | ({ anchor; trivia = Blank_line; _ } as c) :: rest when in_gap anchor ->
          (List.rev acc, c :: rest)
      | l -> (List.rev acc, l)
    in
    aux [] comments
  in
  (* Rebuild the nesting from the flat, sorted (start asc, end desc — i.e.
     preorder) location list in one linear pass. [subtree_end.(i)] is the last
     index whose node is contained in [arr.(i)]: the maximal run of nodes right
     after [i] whose end does not exceed [arr.(i)]'s. That run is exactly the
     prefix the old recursive [span] recomputed at every nesting level, which
     made the whole pass O(n^2) in depth. A monotonic stack of still-open
     ancestors yields it in O(n): a node's subtree ends at [i-1] the moment we
     meet an [i] whose end exceeds it (a strictly greater end means [i] is not
     contained, so [i] is a sibling/uncle); equal ends keep it open, since an
     equal-end node with a later start nests inside — matching [is_child]'s
     [<=]. Any node never popped spans to the end. *)
  let arr = Array.of_list locs in
  let n = Array.length arr in
  let ecnum i = arr.(i).Ast.loc_end.Lexing.pos_cnum in
  let scnum i = arr.(i).Ast.loc_start.Lexing.pos_cnum in
  let subtree_end = Array.make (max n 1) 0 in
  let stack = ref [] in
  for i = 0 to n - 1 do
    let rec pop = function
      | t :: tl when ecnum t < ecnum i ->
          subtree_end.(t) <- i - 1;
          pop tl
      | s -> s
    in
    stack := i :: pop !stack
  done;
  List.iter (fun t -> subtree_end.(t) <- n - 1) !stack;
  (* Partition the comments over that tree. Semantics unchanged from the old
     [process]: [before] = comments anchored before the node; [within] = those
     between its children and its own end; [after] = trailing comments up to the
     next sibling's start ([upto]) — or, when the node's last flat descendant
     ([arr.(child_hi)]) ends exactly where the node does, that descendant's
     [after] (the [steal] path, preserving shared-span comment attachment).
     Comments thread left to right through [rem]; each index is visited once as
     a node, so the pass is O(n + #comments). The sibling tail call recurses in
     width, the child call in depth — the same recursion depth (tree height) as
     the old code. *)
  (* [last_upto] is the [upto] a range's *last* node uses — the point up to which
     it may still claim trailing comments. For a child range it is the enclosing
     parent's end ([ecnum] of the node that spawned the recursion), so the last
     child reaches across its separator (a block's [;]) to grab the comment
     trailing the last statement, and a comment after a stack of co-terminating
     closers bubbles out to the *outermost* of them (each inner one's window ends,
     exclusive, exactly where the comment is anchored). At the top level there is
     no enclosing node, so it falls back to [ecnum lo + 1] and a module-tail
     comment stays in [leftover]. *)
  let rec process_range ~last_upto lo hi comments =
    if lo > hi then comments
    else
      let child_lo = lo + 1 and child_hi = subtree_end.(lo) in
      let next_sib = child_hi + 1 in
      let upto =
        if next_sib <= hi then scnum next_sib
        else match last_upto with Some u -> u | None -> ecnum lo + 1
      in
      let before, rem1 = split_before (scnum lo) comments in
      let rem2 =
        process_range ~last_upto:(Some (ecnum lo)) child_lo child_hi rem1
      in
      (* Own-line comments still inside the node ([within]) render inside it; an
         inline trailing comment has already been claimed above by the last child
         (as its [after]). Comments past the node's end are its [after] region,
         rendered on the far side of the closing delimiter. *)
      let within, rem3 = split_before (ecnum lo) rem2 in
      let steal_candidate =
        if child_hi >= child_lo && ecnum child_hi = ecnum lo then
          Some arr.(child_hi)
        else None
      in
      let final_after, rem4 =
        match steal_candidate with
        | Some last_child -> (
            match Hashtbl.find_opt tbl last_child with
            | Some assoc ->
                let stolen = assoc.after in
                Hashtbl.replace tbl last_child { assoc with after = [] };
                (* A co-terminating last child's own [after] window collapsed to
                   empty (its [upto] reached only to [ecnum lo]); scan on up to
                   this node's [upto] so its real trailing comments aren't
                   dropped. Whatever the child did drain is already out of the
                   stream, so [extra] can't double-count. *)
                let extra, rem4 = get_after (ecnum lo) ~upto rem3 in
                (stolen @ extra, rem4)
            | None -> get_after (ecnum lo) ~upto rem3)
        | None -> get_after (ecnum lo) ~upto rem3
      in
      Hashtbl.add tbl arr.(lo) { before; within; after = final_after };
      process_range ~last_upto next_sib hi rem4
  in
  let leftover = process_range ~last_upto:None 0 (n - 1) comments in
  (* [leftover] holds comments that no location owns: trailing comments after
     the last node, or — when the module has no locations at all (e.g. an empty
     [(module)]) — the whole file. The caller prints them as tail trivia. *)
  (tbl, leftover)

let drop_trailing_blank_lines entries =
  let rec drop = function
    | { trivia = Blank_line; _ } :: rest -> drop rest
    | rest -> rest
  in
  List.rev (drop (List.rev entries))

let dummy_assoc = { before = []; within = []; after = [] }

let get ?collect trivia ~seen loc =
  (* A dry pass records every looked-up location into [collect]; the real pass
     then restricts {!associate} to that set. *)
  (match (collect, loc) with
  | Some set, Some loc -> Hashtbl.replace set loc ()
  | _ -> ());
  match loc with
  | None -> dummy_assoc
  | Some loc -> (
      match Hashtbl.find_opt trivia loc with
      | None -> dummy_assoc
      | Some assoc ->
          if Hashtbl.mem seen loc then dummy_assoc
          else (
            Hashtbl.add seen loc ();
            assoc))

(* Cross-format delimiter translation. *)

type comment_syntax = {
  line : string;
  block_open : string;
  block_close : string;
}

let wax_syntax = { line = "//"; block_open = "/*"; block_close = "*/" }
let wat_syntax = { line = ";;"; block_open = "(;"; block_close = ";)" }

let replace_all ~sub ~by s =
  let sl = String.length sub in
  if sl = 0 then s
  else
    let n = String.length s in
    let buf = Buffer.create n in
    let rec aux i =
      if i >= n then Buffer.contents buf
      else if i + sl <= n && String.sub s i sl = sub then (
        Buffer.add_string buf by;
        aux (i + sl))
      else (
        Buffer.add_char buf s.[i];
        aux (i + 1))
    in
    aux 0

let retarget_content ~src ~dst kind content =
  match kind with
  | Line_comment ->
      if String.starts_with ~prefix:src.line content then
        dst.line
        ^ String.sub content (String.length src.line)
            (String.length content - String.length src.line)
      else content
  | Block_comment ->
      content
      |> replace_all ~sub:src.block_open ~by:dst.block_open
      |> replace_all ~sub:src.block_close ~by:dst.block_close
  | Annotation -> content

let retarget_entry ~src ~dst e =
  match e.trivia with
  | Item { content; kind } ->
      {
        e with
        trivia =
          Item { content = retarget_content ~src ~dst kind content; kind };
      }
  | Blank_line -> e

let retarget ~src ~dst tbl tail =
  let conv = retarget_entry ~src ~dst in
  let conv_assoc a =
    {
      before = List.map conv a.before;
      within = List.map conv a.within;
      after = List.map conv a.after;
    }
  in
  let tbl' = Hashtbl.create (Hashtbl.length tbl) in
  Hashtbl.iter (fun k v -> Hashtbl.add tbl' k (conv_assoc v)) tbl;
  (tbl', List.map conv tail)