(** [Form.t] to indented text: the inverse of [Indent_reader], and what [flan convert] writes. The one rule that keeps the round trip exact: a piece of sugar is printed only when the form has exactly the shape that sugar reads back to, and everything else goes through the fallback, [head(arg, ...)], or [head(arg, ...):] with the trailing arguments as an indented block. The fallback reads any form, so a form this printer cannot sweeten still prints; what it cannot print at all is a name with no spelling in the indented syntax, and that raises [Unprintable]. Comments are not in a [Form.t], so a converted file has none. *) module R = Indent_reader exception Unprintable of Form.t * string let width = 80 let unprintable (f : Form.t) why = raise (Unprintable (f, why)) (* Words a statement may start with that the reader takes as a header. A statement whose text would lead with one is wrapped in parentheses, which the reader takes as grouping and so as the plain name. *) let reserved = [ "fn"; "fn-"; "def"; "once"; "const"; "struct"; "union"; "data"; "enum"; "import"; "if"; "elif"; "else"; "while"; "until"; "match"; "let"; "for"; "return"; "break"; "continue"; "defer"; "handler-case"; "handler-bind"; "restart-case"; "quote"; "on"; "restart" ] (* A symbol the reader gives back as itself when it is written bare. *) let name_ok s = let n = String.length s in n > 0 && (not (String.exists Reader.is_delimiter s)) && (not (String.contains s ':')) && s.[0] <> '\'' && s.[0] <> '\\' && (not (Reader.is_digit s.[0])) && (not ((s.[0] = '-' || s.[0] = '+') && n > 1 && Reader.is_digit s.[1])) && (not (n > 1 && s.[0] = '-' && R.is_neg_char s.[1])) && R.split_fields s = [ s ] && (not (R.is_op_word s)) && not (n >= 2 && s.[0] = '#' && s.[1] = '_') let kw_ok k = k <> "" && not (String.exists Reader.is_delimiter k) (* A name a definition's header can take: the reader reads a leading dot there as a field access, so [.init-once.counter] keeps the fallback. *) let def_name s = name_ok s && s.[0] <> '.' let paren s = "(" ^ s ^ ")" let is_sym s (f : Form.t) = match f.v with Form.Sym x -> x = s | _ -> false (* ── Expressions ───────────────────────────────────────────────────── *) (* Text and syntactic level, the same scale [Indent_reader] reads: 10 an atom or bracket, 9 a postfix chain, 8 a unary minus, 1-7 binary, 3 [not], 0 a one-line [if] or a lambda. *) let rec expr (f : Form.t) : string * int = match f.v with | Form.Sym s -> sym f s | Form.Kw k -> if kw_ok k then (":" ^ k, 10) else unprintable f "a keyword with no spelling" | Form.Int i -> (Int64.to_string i, if Int64.compare i 0L < 0 then 8 else 10) | Form.UInt (_, s) -> (s, 10) | Form.Float x -> let s = Form.float_repr x in if not (Reader.is_digit s.[0] || (s.[0] = '-' && String.length s > 1 && Reader.is_digit s.[1])) then unprintable f "a float with no literal"; (s, if s.[0] = '-' then 8 else 10) | Form.Str s -> ("\"" ^ Form.escape s ^ "\"", 10) | Form.Byte b -> (Form.byte_repr b, 10) | Form.Vec xs -> ("[" ^ vec_text xs ^ "]", 10) | Form.Map xs -> ("{" ^ map_text xs ^ "}", 10) | Form.List [] -> ("()", 10) | Form.List (h :: args) -> list f h args and sym f s = if s = "==" then unprintable f "the name == (it reads as =)" else if R.is_op_word s || s = "if" then (paren s, 10) else if name_ok s then (s, 10) else unprintable f (Printf.sprintf "the name %s" s) and at lvl f = let t, l = expr f in if l < lvl then paren t else t and comma_items xs = let rec go = function | [] -> [] | ({ Form.v = Form.Sym "const"; _ }) :: y :: rest -> ("const " ^ at 0 y) :: go rest | x :: rest -> at 0 x :: go rest in go xs and commas xs = String.concat ", " (comma_items xs) (* Whitespace between single terms, as [[1 2 3]] and [[4 f32]] read; commas as soon as one element has an operator in it. *) and vec_text xs = let ts = List.map expr xs in if List.for_all (fun (_, l) -> l >= 8) ts then String.concat " " (List.map fst ts) else String.concat ", " (List.map (fun (t, _) -> t) ts) and map_text xs = let ts = List.map expr xs in if List.for_all (fun (_, l) -> l >= 8) ts then String.concat " " (List.map fst ts) else let rec pairs = function | (k, kl) :: (v, _) :: rest -> ((if kl < 8 then paren k else k) ^ " " ^ v) :: pairs rest | [ (k, _) ] -> [ k ] | [] -> [] in String.concat ", " (pairs ts) and head_text (h : Form.t) = match h.v with | Form.Sym "==" -> unprintable h "the name ==" | Form.Sym s when R.is_op_word s -> s | Form.Sym s -> fst (sym h s) | _ -> at 9 h and list _f h args = let call () = (head_text h ^ "(" ^ commas args ^ ")", 9) in match h.v, args with | Form.Sym "quote", [ x ] -> ("'" ^ Form.to_source x, 10) | Form.Sym "unquote", [ x ] -> ("~" ^ at 10 x, 10) | Form.Sym "unquote-splicing", [ x ] -> ("~@" ^ at 10 x, 10) | Form.Sym s, _ :: _ :: _ when (R.is_binop s || s = "=") && s <> "==" && not (s = "!=" && List.length args > 2) -> let op = if s = "=" then "==" else s in let lvl = Option.get (R.binop_level op) in let first = List.hd args and rest = List.tl args in let ft, fl = expr first in let same = match first.v with | Form.List (h' :: _ :: _ :: _) -> is_sym s h' || lvl = 4 | _ -> false in let ft = if fl < lvl || (fl = lvl && same) then paren ft else ft in (String.concat (" " ^ op ^ " ") (ft :: List.map (at (lvl + 1)) rest), lvl) | Form.Sym "-", [ x ] -> let t, l = expr x in if l >= 9 && t <> "" && R.is_neg_char t.[0] then ("-" ^ t, 8) else ("-(" ^ at 0 x ^ ")", 9) | Form.Sym "not", [ x ] -> ("not " ^ at 3 x, 3) | Form.Sym "at", t :: (_ :: _ as idx) -> (at 9 t ^ "[" ^ commas idx ^ "]", 9) | Form.Sym s, [ t ] when String.length s > 1 && s.[0] = '.' && name_ok s && not (String.contains (String.sub s 1 (String.length s - 1)) '.') -> let tt, tl = expr t in let glued = tl >= 9 && (match t.v with | Form.Byte _ -> false | Form.Sym x -> name_ok x && not (String.contains x '.') && not (R.capitalised x) | _ -> let c = tt.[String.length tt - 1] in c = ')' || c = ']' || c = '}' || c = '"') in if glued then (tt ^ s, 9) else call () | Form.Sym s, [ ({ v = Form.Map _; _ } as m) ] when name_ok s && R.capitalised s -> (s ^ fst (expr m), 9) | Form.Sym "fn", [ { v = Form.Vec ps; _ }; body ] when List.for_all sym_param ps -> ("fn(" ^ commas ps ^ ") = " ^ at 0 body, 0) | Form.Sym "if", [ c; a; b ] -> ("if " ^ at 1 c ^ " then " ^ at 1 a ^ " else " ^ at 0 b, 0) | _ -> call () and sym_param (p : Form.t) = match p.v with Form.Sym s -> name_ok s | _ -> false (* A type after [:] or [->]: the function-type arrow at the top, a postfix term below it. *) let rec ty (f : Form.t) = match f.v with | Form.List [ { v = Form.Sym (("Fn" | "CFn") as h); _ }; { v = Form.Vec ps; _ }; r ] -> h ^ "(" ^ commas ps ^ ") -> " ^ ty r | _ -> at 9 f (* A [defn]'s parameter type the reader could not mistake for a name: a primitive, a capitalised or [$] name, or a bracket. [[x y]] with a lowercase [y] keeps the fallback, because what it means depends on whether [y] names a type. *) let type_shaped (f : Form.t) = match f.v with | Form.Sym t -> List.mem t Types.primitive_names || (t <> "" && t.[0] = '$') || R.capitalised t | Form.List [] | Form.List ({ v = Form.Sym _; _ } :: _) | Form.Vec _ -> true | _ -> false let rec pairs = function | a :: b :: rest -> Option.map (fun r -> (a, b) :: r) (pairs rest) | [] -> Some [] | [ _ ] -> None (* [(a: i32, b)] from [[a i32 b dyn]], when every name is a plain name. *) let params_text ?(shaped = false) (ps : Form.t list) = match pairs ps with | None -> None | Some prs -> if List.for_all (fun ((n : Form.t), t) -> (match n.v with Form.Sym x -> def_name x | _ -> false) && ((not shaped) || is_sym "dyn" t || type_shaped t)) prs then Some (String.concat ", " (List.map (fun ((n : Form.t), t) -> let n = fst (expr n) in if is_sym "dyn" t then n else n ^ ": " ^ ty t) prs)) else None (* ── Statements ────────────────────────────────────────────────────── *) let ind n = String.make n ' ' let lead_word text = let n = String.length text in let rec go i = if i < n && not (Reader.is_delimiter text.[i]) then go (i + 1) else i in let i = go 0 in (String.sub text 0 i, i = n || text.[i] = ' ') (* A statement whose text leads with a reserved word, parenthesised. *) let guard text = let w, spaced = lead_word text in if spaced && List.mem w reserved then paren text else text let stmts_of (f : Form.t) = match f.v with | Form.List ({ v = Form.Sym "do"; _ } :: (_ :: _ :: _ as ss)) -> ss | _ -> [ f ] (* Heads whose trailing arguments are a body, and how many come before it. *) let body_split (h : Form.t) args = match h.v with | Form.Sym s -> let base = match String.rindex_opt s '/' with | Some i -> String.sub s (i + 1) (String.length s - i - 1) | None -> s in let lead = List.length (List.filter (fun (a : Form.t) -> match a.v with Form.List _ -> false | _ -> true) args) in (match base with | "comment" | "do" -> Some 0 | "unless" | "loop" -> Some 1 | "defmacro" -> Some 2 | "defmethod" -> Some 3 | _ when String.length base > 5 && String.sub base 0 5 = "with-" -> let rec leading n = function | ({ Form.v = Form.List _; _ }) :: _ -> n | _ :: rest -> leading (n + 1) rest | [] -> n in ignore lead; Some (leading 0 args) | _ -> None) | _ -> None let sugar_heads = [ "let"; "set"; "if"; "when"; "cond"; "while"; "until"; "dotimes"; "match"; "handler-case"; "handler-bind"; "restart-case"; "return"; "defer"; "do"; "quasiquote" ] let rec block n (fs : Form.t list) : string list = let rec go = function | [] -> [] | [ x ] -> stmt n ~last:true x | x :: rest -> stmt n ~last:false x @ go rest in go fs and stmt n ~last (f : Form.t) : string list = match sugar n ~last f with | Some ls -> ls | None -> plain n f and plain n (f : Form.t) : string list = let text = match f.v with | Form.List [] -> "(())" | Form.List [ { v = Form.Sym "do"; _ } ] -> "()" | Form.Sym s when List.mem s reserved -> paren s | _ -> guard (fst (expr f)) in let one = [ ind n ^ text ] in match f.v with | Form.List (h :: args) when args <> [] -> (match body_split h args with | Some k when k < List.length args -> let fixed = List.filteri (fun i _ -> i < k) args in let rest = List.filteri (fun i _ -> i >= k) args in [ ind n ^ guard (head_text h ^ "(" ^ commas fixed ^ "):") ] @ block (n + 2) rest | _ when n + String.length text > width && fst (expr f) = text -> wrapped n "" f | _ -> one) | _ -> one (* A call too long for its line, broken after commas inside its parentheses, where a line break is only whitespace. [prefix] is what comes before the call on the first line. *) and wrapped n prefix (f : Form.t) = match f.v with | Form.List (h :: (_ :: _ as args)) when (match h.v with | Form.Sym ("at" | "quote" | "unquote" | "unquote-splicing") -> false | Form.Sym s -> not (R.is_op_word s) && not (String.length s > 1 && s.[0] = '.') | _ -> false) -> let open_ = prefix ^ head_text h ^ "(" in let col = n + String.length open_ in let items = comma_items args in let rec go line acc = function | [] -> List.rev ((line ^ ")") :: acc) | [ t ] -> if String.length line = col || String.length line + String.length t + 1 <= width then go (line ^ t) acc [] else let line = String.sub line 0 (String.length line - 1) in go (ind col ^ t) (line :: acc) [] | t :: rest -> let piece = t ^ "," in if String.length line = col || String.length line + String.length piece <= width then go (line ^ piece ^ " ") acc rest else let line = String.sub line 0 (String.length line - 1) in go (ind col ^ piece ^ " ") (line :: acc) rest in (* The last item on a line carries a trailing space; the break drops it. *) let lines = go (ind n ^ open_) [] items in List.map (fun l -> let k = String.length l in if k > 0 && l.[k - 1] = ' ' then String.sub l 0 (k - 1) else l) lines | _ -> [ ind n ^ prefix ^ at 0 f ] (* [prefix = v], or [prefix =] and the value as an indented block when it is too long for the line. *) and value_lines n prefix (v : Form.t) = let inline = prefix ^ " = " ^ at 0 v in if n + String.length inline <= width then [ ind n ^ inline ] else match v.v with | Form.List ({ v = Form.Sym "fn"; _ } :: { v = Form.Vec ps; _ } :: (_ :: _ as body)) when List.for_all sym_param ps -> [ ind n ^ prefix ^ " = fn(" ^ commas ps ^ ")" ] @ block (n + 2) body | Form.List ({ v = Form.Sym h; _ } :: _) when not (List.mem h sugar_heads || h = "fn" || h = "if") -> wrapped n (prefix ^ " = ") v | Form.List (_ :: _) -> [ ind n ^ prefix ^ " =" ] @ block (n + 2) (stmts_of v) | _ -> [ ind n ^ inline ] and slot n (f : Form.t) = block n (stmts_of f) and label_of = function | ({ Form.v = Form.Kw k; _ }) :: rest when kw_ok k -> (":" ^ k ^ " ", rest) | rest -> ("", rest) and sugar n ~last (f : Form.t) : string list option = let i = ind n in match f.v with | Form.List ({ v = Form.Sym "let"; _ } :: { v = Form.Vec bs; _ } :: (_ :: _ as body)) -> (match pairs bs with | None | Some [] -> None | Some prs -> Some (let_lines n ~last prs body)) | Form.List [ { v = Form.Sym "set"; _ }; t; v ] -> Some (value_lines n (guard (at 9 t)) v) | Form.List [ { v = Form.Sym "if"; _ }; c; a; b ] -> let simple (x : Form.t) = match x.v with | Form.List ({ v = Form.Sym h; _ } :: _) -> not (List.mem h sugar_heads) | _ -> true in let line = i ^ fst (expr f) in if simple a && simple b && String.length line <= width then Some [ line ] else Some ([ i ^ "if " ^ at 1 c ] @ slot (n + 2) a @ [ i ^ "else" ] @ slot (n + 2) b) | Form.List ({ v = Form.Sym "when"; _ } :: c :: (_ :: _ as body)) -> Some ((i ^ "if " ^ at 1 c) :: block (n + 2) body) | Form.List ({ v = Form.Sym "cond"; _ } :: args) -> (match pairs args with | None -> None | Some prs -> let tests, else_ = match List.rev prs with | (k, e) :: rest when is_else k -> (List.rev rest, Some e) | _ -> (prs, None) in if List.length tests < 2 then None else Some (List.concat (List.mapi (fun j (c, b) -> (i ^ (if j = 0 then "if " else "elif ") ^ at 1 c) :: slot (n + 2) b) tests) @ (match else_ with | Some e -> (i ^ "else") :: slot (n + 2) e | None -> []))) | Form.List ({ v = Form.Sym (("while" | "until") as w); _ } :: rest) -> let lbl, rest = label_of rest in (match rest with | c :: (_ :: _ as body) -> Some ((i ^ w ^ " " ^ lbl ^ at 0 c) :: block (n + 2) body) | _ -> None) | Form.List ({ v = Form.Sym "dotimes"; _ } :: rest) -> let lbl, rest = label_of rest in (match rest with | { v = Form.Vec ({ v = Form.Sym v; _ } :: bs); _ } :: (_ :: _ as body) when def_name v && bs <> [] && List.length bs <= 3 && v <> "in" -> Some ((i ^ "for " ^ lbl ^ v ^ " in range(" ^ commas bs ^ ")") :: block (n + 2) body) | _ -> None) | Form.List [ { v = Form.Sym "return"; _ } ] -> Some [ i ^ "return" ] | Form.List [ { v = Form.Sym "return"; _ }; v ] -> Some [ i ^ "return " ^ at 0 v ] | Form.List [ { v = Form.Sym (("break" | "continue") as w); _ } ] -> Some [ i ^ w ] | Form.List [ { v = Form.Sym (("break" | "continue") as w); _ }; { v = Form.Kw k; _ } ] when kw_ok k -> Some [ i ^ w ^ " :" ^ k ] | Form.List [ { v = Form.Sym "defer"; _ }; x ] -> let line = i ^ "defer " ^ at 0 x in if String.length line <= width then Some [ line ] else Some ((i ^ "defer") :: block (n + 2) [ x ]) | Form.List ({ v = Form.Sym "defer"; _ } :: (_ :: _ :: _ as body)) -> Some ((i ^ "defer") :: block (n + 2) body) | Form.List ({ v = Form.Sym "match"; _ } :: s :: (_ :: _ as arms)) -> (match pairs arms with | None -> None | Some prs -> Some ((i ^ "match " ^ at 0 s) :: List.concat_map (fun (pat, body) -> let pt = at 8 pat in let line = ind (n + 2) ^ pt ^ " -> " ^ at 0 body in match body.v with | Form.List (_ :: _) when String.length line > width -> (ind (n + 2) ^ pt ^ " ->") :: slot (n + 4) body | _ -> [ line ]) prs)) | Form.List [ { v = Form.Sym "handler-case"; _ }; body; { v = Form.Vec cls; _ } ] when cls <> [] -> Option.map (fun cl -> ((i ^ "handler-case") :: slot (n + 2) body) @ cl) (handler_clauses n cls) | Form.List ({ v = Form.Sym "handler-bind"; _ } :: { v = Form.Vec cls; _ } :: (_ :: _ as body)) when cls <> [] -> Option.map (fun cl -> ((i ^ "handler-bind") :: block (n + 2) body) @ cl) (handler_clauses n cls) | Form.List ({ v = Form.Sym "restart-case"; _ } :: body :: (_ :: _ as cls)) -> let clause (c : Form.t) = match c.v with | Form.List ({ v = Form.Sym r; _ } :: { v = Form.Vec ps; _ } :: (_ :: _ as b)) when def_name r -> Option.map (fun pt -> (i ^ "restart " ^ r ^ "(" ^ pt ^ ")") :: block (n + 2) b) (params_text ps) | _ -> None in let cs = List.map clause cls in if List.mem None cs then None else Some (((i ^ "restart-case") :: slot (n + 2) body) @ List.concat_map Option.get cs) | Form.List [ { v = Form.Sym "quasiquote"; _ }; x ] -> Some ((i ^ "quote") :: slot (n + 2) x) | Form.List ({ v = Form.Sym "fn"; _ } :: { v = Form.Vec ps; _ } :: (_ :: _ :: _ as body)) when List.for_all sym_param ps -> Some ((guard (i ^ "fn(" ^ commas ps ^ ")")) :: block (n + 2) body) | Form.List ({ v = Form.Sym (("defn" | "defn-") as d); _ } :: { v = Form.Sym name; _ } :: { v = Form.Vec ps; _ } :: ret :: body) when def_name name -> (match params_text ~shaped:true ps with | None -> None | Some pt -> let where_, body = match body with | { v = Form.Map [ { v = Form.Kw "where"; _ }; x ]; _ } :: rest -> let preds = match x.v with | Form.Vec (_ :: _ :: _ as xs) -> commas xs | _ -> at 0 x in (" where " ^ preds, rest) | _ -> ("", body) in let head = i ^ (if d = "defn" then "fn " else "fn- ") ^ name ^ "(" ^ pt ^ ") -> " ^ ty ret ^ where_ in (match body with | [] -> Some [ head ] | [ x ] when (match x.v with | Form.List ({ v = Form.Sym h; _ } :: _) -> not (List.mem h sugar_heads) | _ -> true) && String.length head + 3 + String.length (at 0 x) <= width -> Some [ head ^ " = " ^ at 0 x ] | _ -> Some (head :: block (n + 2) body))) | Form.List ({ v = Form.Sym (("def" | "defonce" | "defconst") as d); _ } :: { v = Form.Sym name; _ } :: rest) when def_name name -> let w = match d with "def" -> "def" | "defonce" -> "once" | _ -> "const" in let pre = i ^ w ^ " " ^ name in (match d, rest with | "defconst", [ v ] -> Some (value_lines n (w ^ " " ^ name) v) | "defconst", [ t; v ] -> Some (value_lines n (w ^ " " ^ name ^ ": " ^ ty t) v) | "defconst", _ -> None | _, [ t; v ] when is_sym "dyn" t -> Some (value_lines n (w ^ " " ^ name) v) | _, [ t ] when type_shaped t -> Some [ pre ^ ": " ^ ty t ] | _, [ t; v ] -> Some (value_lines n (w ^ " " ^ name ^ ": " ^ ty t) v) | _ -> None) | Form.List [ { v = Form.Sym (("defstruct" | "defunion") as d); _ }; { v = Form.Sym name; _ }; { v = Form.Vec fs; _ } ] when def_name name -> (match pairs fs with | Some prs when List.for_all (fun ((f : Form.t), _) -> match f.v with Form.Sym x -> def_name x | _ -> false) prs -> Some ((i ^ (if d = "defstruct" then "struct " else "union ") ^ name) :: List.map (fun ((f : Form.t), t) -> let fname = fst (expr f) in ind (n + 2) ^ if is_sym "dyn" t then fname else fname ^ ": " ^ ty t) prs) | _ -> None) | Form.List [ { v = Form.Sym "defdata"; _ }; { v = Form.Sym name; _ }; { v = Form.Vec cs; _ } ] when def_name name -> let case (c : Form.t) = match c.v with | Form.Sym s when def_name s -> Some s | Form.List [ { v = Form.Sym s; _ }; { v = Form.Vec ps; _ } ] when def_name s -> Option.map (fun pt -> s ^ "(" ^ pt ^ ")") (params_text ps) | _ -> None in let cs = List.map case cs in if List.mem None cs then None else Some ((i ^ "data " ^ name) :: List.map (fun c -> ind (n + 2) ^ Option.get c) cs) | Form.List [ { v = Form.Sym "defenum"; _ }; { v = Form.Sym name; _ }; { v = Form.Vec ms; _ } ] when def_name name -> let rec members = function | { Form.v = Form.Sym m; _ } :: ({ Form.v = Form.Int _ | Form.UInt _; _ } as v) :: rest when def_name m -> Option.map (fun r -> (m ^ " = " ^ fst (expr v)) :: r) (members rest) | { Form.v = Form.Sym m; _ } :: rest when def_name m -> Option.map (fun r -> m :: r) (members rest) | [] -> Some [] | _ -> None in Option.map (fun ms -> (i ^ "enum " ^ name) :: List.map (fun m -> ind (n + 2) ^ m) ms) (members ms) | Form.List [ { v = Form.Sym "import"; _ }; { v = Form.Sym a; _ }; ({ v = Form.Str _; _ } as p) ] when def_name a -> Some [ i ^ "import " ^ a ^ " " ^ fst (expr p) ] | _ -> None and is_else (f : Form.t) = match f.v with Form.Kw "else" -> true | _ -> false and handler_clauses n cls = let clause (c : Form.t) = match c.v with | Form.List (t :: { v = Form.Vec [ { v = Form.Sym v; _ } ]; _ } :: (_ :: _ as b)) when def_name v -> Some ((ind n ^ "on " ^ at 9 t ^ "(" ^ v ^ ")") :: block (n + 2) b) | _ -> None in let cs = List.map clause cls in if List.mem None cs then None else Some (List.concat_map Option.get cs) (* A [let] last in its block reads to the block's end, so it is written flat. One with siblings after it takes its body as an indented block under the first binding, and the rest of the bindings go inside that block. *) and let_lines n ~last prs body = let target (t : Form.t) = "let " ^ guard (at 8 t) in if last then List.concat_map (fun (t, v) -> value_lines n (target t) v) prs @ block n body else match prs with | (t, v) :: rest -> (ind n ^ target t ^ " = " ^ at 0 v) :: (List.concat_map (fun (t, v) -> value_lines (n + 2) (target t) v) rest @ block (n + 2) body) | [] -> block n body (** A whole file: top-level forms with a blank line between them. *) let program (fs : Form.t list) : string = let rec go = function | [] -> [] | [ x ] -> [ String.concat "\n" (stmt 0 ~last:true x) ] | x :: rest -> String.concat "\n" (stmt 0 ~last:false x) :: go rest in String.concat "\n\n" (go fs) ^ "\n"