Source file printer.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
(* A document-IR pretty-printer. The imperative builder API (string, space,
   box, …) records a tree of text and breaks; [run] then renders it with our
   own layout algorithm, which gives genuine hard breaks, column awareness and
   break coalescing we control — unlike a thin layer over OCaml's [Format]. *)

type break_strength = Cut | Space | Newline | Blank_line

let strength = function Cut -> 0 | Space -> 1 | Newline -> 2 | Blank_line -> 3

(* ===================================================================== *)
(* Document IR + imperative builder + renderer                           *)
(* ===================================================================== *)

module Doc = struct
  type gkind =
    | GBox (* fill: each soft break wraps independently as needed *)
    | GHov (* same fill semantics, kept distinct for parity with Format *)
    | GHv (* all-or-nothing: whole group flat, or every soft break wraps *)
    | GV (* every soft break wraps *)
    | GH (* soft breaks never wrap (hard/blank still break) *)

  (* A break carries one [break_strength]: Cut/Space are soft (flatten in a
     fitting group); Newline/Blank_line always break. *)
  type doc =
    | Empty
    | Text of int * string (* display width, payload *)
    | Brk of break_strength
    | Cat of doc * doc
    | Nest of int * doc
    | Group of gkind * doc
    | If_broken of doc
  (* Emit [doc] only when the enclosing group is laid out broken (not flat): a
     trailing comma after the last element of a list that wraps. It never counts
     toward the fit decision (a flat list has no trailing comma). *)

  type frame = { wrap : doc -> doc; mutable items : doc list (* reversed *) }

  type state = {
    fmt : Format.formatter;
    width : int;
    mutable stack : frame list;
    mutable has_emitted : bool; (* content since last forced end-of-line *)
    mutable pending_eol : (unit -> unit) option;
    mutable holding_eol : bool;
  }

  let create fmt ~width =
    {
      fmt;
      width;
      stack = [ { wrap = (fun d -> d); items = [] } ];
      has_emitted = false;
      pending_eol = None;
      holding_eol = false;
    }

  let top st = List.hd st.stack

  (* Append, coalescing a break that immediately follows another break (within
     this frame) into the stronger of the two — the analogue of the Format
     engine's [register_break]. Cross-frame coalescing is the renderer's job. *)
  let append st d =
    let fr = top st in
    match (d, fr.items) with
    | Brk s2, Brk s1 :: tl ->
        fr.items <- Brk (if strength s2 >= strength s1 then s2 else s1) :: tl
    | _ -> fr.items <- d :: fr.items

  let push st wrap = st.stack <- { wrap; items = [] } :: st.stack

  let pop st =
    match st.stack with
    | fr :: rest ->
        (* items are most-recent-first; fold_left rebuilds source order. *)
        let body = List.fold_left (fun acc d -> Cat (d, acc)) Empty fr.items in
        st.stack <- rest;
        append st (fr.wrap body)
    | [] -> assert false

  (* Drop a trailing break at this frame's head, so a deferred end-of-line
     comment hugs the preceding token instead of being pushed past a break. *)
  let drop_trailing_break st =
    let fr = top st in
    match fr.items with
    | Brk s :: tl ->
        fr.items <- tl;
        Some s
    | _ -> None

  let force_eol st =
    match st.pending_eol with
    | None -> ()
    | Some emit ->
        st.pending_eol <- None;
        let dropped = drop_trailing_break st in
        emit ();
        let next_brk =
          match dropped with Some Blank_line -> Blank_line | _ -> Newline
        in
        append st (Brk next_brk);
        st.has_emitted <- false

  let defer_eol st emit =
    force_eol st;
    st.pending_eol <- Some emit

  let with_held_eol st f =
    let prev = st.holding_eol in
    st.holding_eol <- true;
    f ();
    st.holding_eol <- prev

  let has_pending_eol st = st.pending_eol <> None

  let text st len s =
    if not st.holding_eol then force_eol st;
    st.has_emitted <- true;
    append st (Text (len, s))

  let string st s = text st (String.length s) s
  let string_as st len s = text st len s
  let space st = if st.has_emitted then append st (Brk Space)
  let cut st = append st (Brk Cut)
  let newline st = append st (Brk Newline)
  let blank_line st = append st (Brk Blank_line)

  let indent st n f =
    push st (fun d -> Nest (n, d));
    f ();
    pop st

  let if_broken st f =
    push st (fun d -> If_broken d);
    f ();
    pop st

  let group_wrap kind indent d =
    Group (kind, if indent = 0 then d else Nest (indent, d))

  let scoped st kind ~skip_space ~indent f =
    if not st.holding_eol then force_eol st;
    (if skip_space then
       let fr = top st in
       match fr.items with Brk (Cut | Space) :: tl -> fr.items <- tl | _ -> ());
    push st (group_wrap kind indent);
    f ();
    pop st

  let box st ~skip_space ~indent f = scoped st GBox ~skip_space ~indent f
  let hvbox st ~skip_space ~indent f = scoped st GHv ~skip_space ~indent f
  let hovbox st ~skip_space ~indent f = scoped st GHov ~skip_space ~indent f
  let vbox st ~skip_space ~indent f = scoped st GV ~skip_space ~indent f
  let hbox st ~skip_space f = scoped st GH ~skip_space ~indent:0 f

  (* --- renderer --- *)

  type mode = Flat | Brkm | Fill

  (* Does the upcoming content fit in [avail] columns up to the next line break?
     Trailing context is included (the worklist [rest] beyond the immediate
     item), so a group is laid flat only when what follows it on the line also
     fits — matching Format, not a local "does this group alone fit" check.

     A break decides line-end based on the carried mode: a break in a breaking
     context ([Brkm]/[Fill]) ends the line, so we have fit so far ([true]); a
     break in [Flat] is the flat-spacing of a unit being measured. Groups are
     always entered [Flat] — measured as atomic units — so a group that cannot
     be flat (a hard break inside) forces the preceding break to break. *)
  let rec fits avail items =
    if avail < 0 then false
    else
      match items with
      | [] -> true
      | (i, m, d) :: rest -> (
          match d with
          | Empty -> fits avail rest
          | Text (w, _) -> fits (avail - w) rest
          | Cat (a, b) -> fits avail ((i, m, a) :: (i, m, b) :: rest)
          | Nest (n, d) -> fits avail ((i + n, m, d) :: rest)
          | Group (_, d) -> fits avail ((i, Flat, d) :: rest)
          | If_broken _ -> fits avail rest
          (* A trailing comma never counts toward the fit: a flat list omits it
             outright, and when measured inside an already-broken list a break
             has ended the line before reaching it. *)
          | Brk b -> (
              match m with
              | Brkm | Fill -> true
              | Flat -> (
                  match b with
                  | Newline | Blank_line -> false
                  | Space -> fits (avail - 1) rest
                  | Cut -> fits avail rest)))

  let render ~width doc =
    let b = Buffer.create 4096 in
    let col = ref 0 in
    let emitted = ref false in
    (* Cap how far breaks indent, so deeply nested code does not march off to
       the right margin (the analogue of Format's [max_indent]); deeper levels
       then pile up at the cap rather than each pushing further right. *)
    let max_indent = max 0 (width - 10) in
    (* A pending line break (indent + blank?) and/or a pending flat separator.
       Deferred until the next text and coalesced — across group boundaries —
       by max strength; a line break supersedes a flat separator. *)
    let pend_line = ref None in
    let pend_flat = ref None in
    let flush () =
      (match !pend_line with
      | Some (ind, blank) ->
          (* Suppress a leading break before any output, like [if started]. *)
          if !emitted then (
            Buffer.add_char b '\n';
            if blank then Buffer.add_char b '\n';
            Buffer.add_string b (String.make ind ' ');
            col := ind)
      | None -> (
          match !pend_flat with
          | Some s when strength s >= strength Space ->
              Buffer.add_char b ' ';
              incr col
          | _ -> ()));
      pend_line := None;
      pend_flat := None
    in
    let break_line ind blank =
      let ind = min ind max_indent in
      (match !pend_line with
      | Some (_, b0) -> pend_line := Some (ind, b0 || blank)
      | None -> pend_line := Some (ind, blank));
      pend_flat := None
    in
    let flat_sep s =
      if !pend_line = None then
        pend_flat :=
          Some
            (match !pend_flat with
            | Some s0 -> if strength s >= strength s0 then s else s0
            | None -> s)
    in
    let eff_col () =
      match !pend_line with
      | Some (ind, _) -> ind
      | None -> !col + if !pend_flat <> None then 1 else 0
    in
    let rec go = function
      | [] -> () (* drop any trailing pending break: no trailing whitespace *)
      | (i, m, d) :: rest -> (
          match d with
          | Empty -> go rest
          | Text (w, s) ->
              flush ();
              Buffer.add_string b s;
              emitted := true;
              col := !col + w;
              go rest
          | Cat (a, c) -> go ((i, m, a) :: (i, m, c) :: rest)
          | Nest (n, d) -> go ((i + n, m, d) :: rest)
          | Group (k, d) ->
              (* A box's break-indentation is measured from the column where the
                 box opens (Format semantics), not from the inherited nesting —
                 they differ when a box starts mid-line (e.g. a WAT s-expression
                 after its [(]). Rebase the group's indent to the open column. *)
              let base = eff_col () in
              let m' =
                match k with
                | GV -> Brkm
                | GH -> Flat
                | GHv ->
                    if fits (width - base) ((base, Flat, d) :: rest) then Flat
                    else Brkm
                | GBox | GHov -> Fill
              in
              go ((base, m', d) :: rest)
          | If_broken d ->
              (* Shown only when the surrounding all-or-nothing box broke
                 ([Brkm]); a one-line ([Flat]) or greedily-packed ([Fill]) list
                 has no trailing comma. *)
              go (match m with Brkm -> (i, m, d) :: rest | _ -> rest)
          | Brk str ->
              (match (str, m) with
              | Newline, _ -> break_line i false
              | Blank_line, _ -> break_line i true
              | (Cut | Space), Flat -> flat_sep str
              | (Cut | Space), Brkm -> break_line i false
              | (Cut | Space), Fill ->
                  (* If kept flat this separator itself occupies a column, so
                     the following content starts one column further right;
                     account for it or a unit that just fits would overflow. *)
                  let sep = match str with Space -> 1 | _ -> 0 in
                  if fits (width - eff_col () - sep) rest then flat_sep str
                  else break_line i false);
              go rest)
    in
    go [ (0, Brkm, doc) ];
    Buffer.contents b

  let finish st =
    force_eol st;
    let body =
      List.fold_left (fun acc d -> Cat (d, acc)) Empty (top st).items
    in
    let s = render ~width:st.width body in
    (* Emit line by line through the formatter's own newline, so that when [run]
       is called inside an enclosing Format box (e.g. an [@[<2>…@]] wrapping the
       snippet), every line — not just the first — picks up that box's
       indentation. A bare [pp_print_string] of the multi-line string would
       leave the embedded newlines at column 0. *)
    match String.split_on_char '\n' s with
    | [] -> ()
    | first :: rest ->
        Format.pp_print_string st.fmt first;
        List.iter
          (fun line ->
            Format.pp_force_newline st.fmt ();
            Format.pp_print_string st.fmt line)
          rest
end

(* ===================================================================== *)
(* Public API                                                            *)
(* ===================================================================== *)

type t = Doc.state

let indent = Doc.indent
let string = Doc.string
let string_as = Doc.string_as
let space t () = Doc.space t
let cut t () = Doc.cut t
let newline t () = Doc.newline t
let blank_line t () = Doc.blank_line t
let defer_eol = Doc.defer_eol
let with_held_eol = Doc.with_held_eol
let has_pending_eol = Doc.has_pending_eol
let if_broken = Doc.if_broken

let box t ?(skip_space = false) ?(indent = 0) f =
  Doc.box t ~skip_space ~indent f

let hvbox t ?(skip_space = false) ?(indent = 0) f =
  Doc.hvbox t ~skip_space ~indent f

let hbox t ?(skip_space = false) f = Doc.hbox t ~skip_space f

let hovbox t ?(skip_space = false) ?(indent = 0) f =
  Doc.hovbox t ~skip_space ~indent f

let vbox t ?(skip_space = false) ?(indent = 0) f =
  Doc.vbox t ~skip_space ~indent f

let run ?(width = 78) fmt f =
  let c = Doc.create fmt ~width in
  f c;
  Doc.finish c