229 lines
8.8 KiB
OCaml
229 lines
8.8 KiB
OCaml
(** What a printer needs from the text a program was read from and a
|
|
[Form.t] does not carry: the comments, and the spelling of each number.
|
|
[flan convert] reads both here and puts them back (author's decision 83),
|
|
so a converted file keeps its [;] notes and its [0xFFF00FFF]s.
|
|
|
|
Both syntaxes share the lexical rules this depends on: a comment runs
|
|
from [;] to the end of its line, a string is ["..."] with backslash
|
|
escapes, and [\c] is a character — so [\;] is not a comment. *)
|
|
|
|
type comment = {
|
|
line : int; (* 1-based *)
|
|
text : string; (* from the [;] to the end of the line *)
|
|
own_line : bool; (* nothing but spaces before it on its line *)
|
|
gap_after : bool; (* the line after it is blank *)
|
|
}
|
|
|
|
let comments (src : string) : comment list =
|
|
let n = String.length src in
|
|
let out = ref [] in
|
|
let line = ref 1 and line_start = ref 0 in
|
|
let i = ref 0 in
|
|
while !i < n do
|
|
(match src.[!i] with
|
|
| '\n' -> incr line; line_start := !i + 1; incr i
|
|
| '"' ->
|
|
incr i;
|
|
while !i < n && src.[!i] <> '"' do
|
|
if src.[!i] = '\\' then incr i;
|
|
if !i < n && src.[!i] = '\n' then (incr line; line_start := !i + 1);
|
|
incr i
|
|
done;
|
|
incr i
|
|
| '\\' -> i := !i + 2
|
|
| ';' ->
|
|
let j = ref !i in
|
|
while !j < n && src.[!j] <> '\n' do incr j done;
|
|
let text = String.sub src !i (!j - !i) in
|
|
let text =
|
|
if text <> "" && text.[String.length text - 1] = '\r'
|
|
then String.sub text 0 (String.length text - 1) else text
|
|
in
|
|
let before = String.sub src !line_start (!i - !line_start) in
|
|
let k = ref (!j + 1) in
|
|
while !k < n && (src.[!k] = ' ' || src.[!k] = '\r') do incr k done;
|
|
let gap_after = !j < n && (!k >= n || src.[!k] = '\n') in
|
|
out := { line = !line; text; own_line = String.trim before = ""; gap_after } :: !out;
|
|
i := !j
|
|
| _ -> incr i)
|
|
done;
|
|
List.rev !out
|
|
|
|
(** A number's text as written, when it reads back to the same value: the
|
|
text under its span. [Form.Int] keeps only the value. *)
|
|
let spelling (src : string) : Form.t -> string option =
|
|
let lines = Array.of_list (String.split_on_char '\n' src) in
|
|
fun (f : Form.t) ->
|
|
let l = f.loc in
|
|
if l.Loc.line < 1 || l.Loc.line > Array.length lines || l.Loc.eline <> l.Loc.line
|
|
then None
|
|
else
|
|
let text = lines.(l.Loc.line - 1) in
|
|
let a = l.Loc.col - 1 and b = l.Loc.ecol - 1 in
|
|
if a < 0 || b > String.length text || b <= a then None
|
|
else
|
|
let t = String.sub text a (b - a) in
|
|
match f.v with
|
|
| Form.Int i when Int64.of_string_opt t = Some i -> Some t
|
|
| Form.Float x
|
|
when String.exists (fun c -> c = '.' || c = 'e' || c = 'E') t
|
|
&& (match float_of_string_opt t with
|
|
| Some y -> Int64.equal (Int64.bits_of_float x) (Int64.bits_of_float y)
|
|
| None -> false) ->
|
|
Some t
|
|
| _ -> None
|
|
|
|
(* ── Lines tagged with where they came from ───────────────────────────
|
|
|
|
A printer marks the first line of each form it lays out with the source
|
|
line that form started on. [weave] reads the marks back out and uses them
|
|
to put each comment where it was: an own-line comment above the first
|
|
line that came from after it, a trailing comment at the end of the line
|
|
the code it followed was printed on. *)
|
|
|
|
(* A line can come from several forms — a statement and the test at its
|
|
start — so it carries every line they started on. *)
|
|
let untag (text : string) : int list * string =
|
|
if String.length text > 0 && text.[0] = '\001' then
|
|
match String.index_opt text '\002' with
|
|
| Some k ->
|
|
(List.filter_map int_of_string_opt
|
|
(String.split_on_char ',' (String.sub text 1 (k - 1))),
|
|
String.sub text (k + 1) (String.length text - k - 1))
|
|
| None -> ([], text)
|
|
else ([], text)
|
|
|
|
let tag (line : int) (text : string) =
|
|
let tags, body = untag text in
|
|
let tags = if line > 0 && not (List.mem line tags) then line :: tags else tags in
|
|
if tags = [] then body
|
|
else "\001" ^ String.concat "," (List.map string_of_int tags) ^ "\002" ^ body
|
|
|
|
let indent_of s =
|
|
let n = String.length s in
|
|
let rec go i = if i < n && s.[i] = ' ' then go (i + 1) else i in
|
|
go 0
|
|
|
|
(** Where every form in [fs] starts, as (line, col), in source order. *)
|
|
let form_starts (fs : Form.t list) : (int * int) list =
|
|
let out = ref [] in
|
|
let rec walk (f : Form.t) =
|
|
out := (f.loc.Loc.line, f.loc.Loc.col) :: !out;
|
|
match f.v with
|
|
| Form.List l | Form.Vec l | Form.Map l -> List.iter walk l
|
|
| _ -> ()
|
|
in
|
|
List.iter walk fs;
|
|
List.sort_uniq compare !out
|
|
|
|
let weave ?(starts = []) (cs : comment list) (text : string) : string =
|
|
let lines = Array.of_list (List.map untag (String.split_on_char '\n' text)) in
|
|
let n = Array.length lines in
|
|
let all = Array.map fst lines and body = Array.map snd lines in
|
|
(* For ordering, a line is as early as the earliest form on it. *)
|
|
let tags =
|
|
Array.map (function [] -> None | l -> Some (List.fold_left min max_int l)) all
|
|
in
|
|
let before = Array.make (n + 1) [] and trailing = Array.make n [] in
|
|
List.iter
|
|
(fun c ->
|
|
(* The line printed from exactly [line], when one was: a printer
|
|
may reorder forms — handler-bind's clauses go after its body — so a
|
|
comment goes with the form, not with whatever line follows. *)
|
|
let exact line =
|
|
let r = ref (-1) in
|
|
Array.iteri (fun i t -> if List.mem line t && !r < 0 then r := i) all;
|
|
!r
|
|
in
|
|
if c.own_line then begin
|
|
(* The form it precedes: the first to start after it. *)
|
|
let owner =
|
|
List.find_opt (fun (l, _) -> l > c.line) starts |> Option.map fst
|
|
in
|
|
let rec find i =
|
|
if i >= n then n
|
|
else match tags.(i) with Some t when t > c.line -> i | _ -> find (i + 1)
|
|
in
|
|
let i =
|
|
match owner with
|
|
| Some l when exact l >= 0 -> exact l
|
|
| _ -> find 0
|
|
in
|
|
before.(i) <- c :: before.(i)
|
|
end
|
|
else begin
|
|
(* The line the code before it went to: the one printed from its own
|
|
line, or else the latest line tagged at or before it. *)
|
|
let last_exact =
|
|
let r = ref (-1) in
|
|
Array.iteri (fun i t -> if List.mem c.line t then r := i) all;
|
|
!r
|
|
in
|
|
if last_exact >= 0 then trailing.(last_exact) <- c :: trailing.(last_exact)
|
|
else begin
|
|
let best = ref (-1) and best_tag = ref 0 in
|
|
Array.iteri
|
|
(fun i t ->
|
|
match t with
|
|
| Some t when t <= c.line && t >= !best_tag -> best := i; best_tag := t
|
|
| _ -> ())
|
|
tags;
|
|
if !best < 0 then before.(0) <- c :: before.(0)
|
|
else trailing.(!best) <- c :: trailing.(!best)
|
|
end
|
|
end)
|
|
cs;
|
|
let b = Buffer.create (String.length text + 256) in
|
|
let emit s = Buffer.add_string b s; Buffer.add_char b '\n' in
|
|
for i = 0 to n do
|
|
let ind =
|
|
if i < n then indent_of body.(i)
|
|
else 0
|
|
in
|
|
List.iter
|
|
(fun c ->
|
|
emit (String.make ind ' ' ^ c.text);
|
|
(* A comment set apart from what follows it, at the top level, stays
|
|
set apart: a file's header, a section rule. *)
|
|
if c.gap_after && ind = 0 && i < n then emit "")
|
|
(List.rev before.(i));
|
|
if i < n then begin
|
|
match List.rev trailing.(i) with
|
|
| [] -> emit body.(i)
|
|
| first :: more ->
|
|
emit (body.(i) ^ " " ^ first.text);
|
|
(* A second trailing comment for the same printed line goes on its
|
|
own line under it, which reads the same and keeps both. *)
|
|
let ind = if i + 1 < n then indent_of body.(i + 1) else indent_of body.(i) in
|
|
List.iter (fun c -> emit (String.make ind ' ' ^ c.text)) more
|
|
end
|
|
done;
|
|
(* [text] ended in a newline, which split into a last empty line. *)
|
|
let s = Buffer.contents b in
|
|
let rec trim s =
|
|
let k = String.length s in
|
|
if k >= 2 && s.[k - 1] = '\n' && s.[k - 2] = '\n' then trim (String.sub s 0 (k - 1))
|
|
else s
|
|
in
|
|
trim s
|
|
|
|
(** Top-level forms' printed texts joined with a blank line between them,
|
|
except that one-line globals written on adjacent lines stay adjacent. *)
|
|
let join_top (items : (Form.t * string) list) : string =
|
|
let global (f : Form.t) =
|
|
match f.v with
|
|
| Form.List ({ v = Form.Sym ("def" | "defonce" | "defconst"); _ } :: _) -> true
|
|
| _ -> false
|
|
in
|
|
let rec go = function
|
|
| [] -> []
|
|
| [ (_, t) ] -> [ t ]
|
|
| ((a : Form.t), ta) :: (((b : Form.t), tb) :: _ as rest) ->
|
|
let tight =
|
|
global a && global b && (not (String.contains ta '\n'))
|
|
&& (not (String.contains tb '\n')) && b.loc.Loc.line = a.loc.Loc.eline + 1
|
|
in
|
|
ta :: (if tight then "\n" else "\n\n") :: go rest
|
|
in
|
|
String.concat "" (go items)
|