flan/lib/form.ml

198 lines
7.7 KiB
OCaml

(** The reader's output: syntax, before any typing or macro expansion.
Deliberately dumb. [true], [false] and [nil] are ordinary symbols here and
are resolved later; the reader knows nothing about special forms. *)
type t = {
v : value;
loc : Loc.t;
}
and value =
| Sym of string (* foo rl/draw-fps .pos + *)
| Kw of string (* :space :else (leading : dropped) *)
| Int of int64 (* 42 -1 0xE6B800FF *)
(* An integer written at or above 2^63 — 18446744073709551615, or a hex
literal with its top bit set. Its 64-bit pattern and its spelling: only a
u64 holds it, and a refusal anywhere else prints the number as written. *)
| UInt of int64 * string
| Float of float (* 0.05 *)
| Str of string (* "SAND" *)
| Byte of int (* \space \0 \( \é a code point, a char *)
| List of t list (* (f x) *)
| Vec of t list (* [1 2 3] and every binding/type bracket *)
| Map of t list (* {.field v} a struct value, and a defn's
{:where ...} clause. Braces are not a type: the
{K V} spelling was withdrawn for (Map K V). The
colon spelling is left for map literals. *)
let make v loc = { v; loc }
(* A code point's UTF-8 bytes. *)
let utf8 b =
let buf = Buffer.create 4 in
Buffer.add_utf_8_uchar buf (Uchar.of_int b);
Buffer.contents buf
(* A char's spelling, the one [Reader.read_byte] reads back as the same code
point: a name where the reader has one, Clojure's \uXXXX for every other
control character (C0, DEL and C1), and the character itself otherwise.
runtime/flan_dyn.c's [char_spell] writes the same table. *)
let byte_repr b =
match b with
| 32 -> "\\space"
| 9 -> "\\tab"
| 10 -> "\\newline"
| 13 -> "\\return"
| 0 -> "\\nul"
| 8 -> "\\backspace"
| 12 -> "\\formfeed"
| b when b < 32 || (b >= 127 && b <= 0x9F) -> Printf.sprintf "\\u%04X" b
| b when b < 127 -> Printf.sprintf "\\%c" (Char.chr b)
| b -> "\\" ^ utf8 b
let rec to_string f =
let seq l = String.concat " " (List.map to_string l) in
match f.v with
| Sym s -> s
| Kw s -> ":" ^ s
| Int i -> Int64.to_string i
| UInt (_, s) -> s
| Float x -> Printf.sprintf "%g" x
| Str s -> Printf.sprintf "%S" s
| Byte b -> byte_repr b
| List l -> "(" ^ seq l ^ ")"
| Vec l -> "[" ^ seq l ^ "]"
| Map l -> "{" ^ seq l ^ "}"
(* ── Printing a Form back as source ─────────────────────────────────
[to_string] above is an error-message renderer: one line, no width, and
[%S] and [%g] where a reader's own spelling was never needed to say
"expected a name, found this". It is also what [Macro.key] digests, so it
is left exactly as it is — every cached macro module on disk is keyed by
what it prints today.
What follows is the other job, and it arrived with [C-c C-m]: text a person
reads and a reader reads back. An expansion is *only* text. A macro answers
a [Form] and nothing in the language ever wrote it down, so unlike every
other thing the editor shows there is no file to point at and no source to
fall back on — whatever this prints is the whole of what anybody sees.
Three places where [to_string] is not a round trip, all of them reachable
from an expansion because a macro may build any literal at all:
- [%g] prints 1.0 as "1", which reads back as an [Int], and it truncates at
six significant digits. Shortest-round-trip here, then a ".0" when
nothing in the text says "float".
- [%S] is OCaml's escaping. The reader takes exactly six escapes — newline,
tab, return, backslash, quote and nul — and every other byte literally,
so the three-digit decimal escape [%S] writes would not read back.
- A control character written as itself is a raw byte in the middle of
the source. [byte_repr] names it, or writes \uXXXX. *)
let escape s =
let b = Buffer.create (String.length s + 2) in
String.iter
(fun c ->
match c with
| '\n' -> Buffer.add_string b "\\n"
| '\t' -> Buffer.add_string b "\\t"
| '\r' -> Buffer.add_string b "\\r"
| '\\' -> Buffer.add_string b "\\\\"
| '"' -> Buffer.add_string b "\\\""
| '\000' -> Buffer.add_string b "\\0"
| c -> Buffer.add_char b c)
s;
Buffer.contents b
let float_repr x =
(* The shortest of the three precisions that survives [float_of_string] is
the one the reader parses back to the same bits. 15 covers almost every
literal anyone writes; 17 covers every double there is. *)
let rec shortest = function
| [] -> Printf.sprintf "%.17g" x
| p :: ps ->
let s = Printf.sprintf "%.*g" p x in
if float_of_string s = x then s else shortest ps
in
let s = shortest [ 15; 16; 17 ] in
(* "1" is an integer to the reader, so a float whose text has no point and
no exponent needs one. Guarded on the text and not on the value: nan and
the infinities print as words, and "nan.0" is no improvement on a literal
no reader accepts either way. *)
let plain =
s <> ""
&& String.for_all (fun c -> (c >= '0' && c <= '9') || c = '-' || c = '+') s
in
if plain then s ^ ".0" else s
(** One line, and a reader reads it back. *)
let rec to_source f =
let seq l = String.concat " " (List.map to_source l) in
match f.v with
| Sym s -> s
| Kw s -> ":" ^ s
| Int i -> Int64.to_string i
| UInt (_, s) -> s
| Float x -> float_repr x
| Str s -> "\"" ^ escape s ^ "\""
| Byte b -> byte_repr b
| List l -> "(" ^ seq l ^ ")"
| Vec l -> "[" ^ seq l ^ "]"
| Map l -> "{" ^ seq l ^ "}"
(** The same text with line breaks in it, for a form too wide to read on one.
Where the breaks go, and deliberately not where the columns do. A list
that fits is written flat; one that does not keeps its head on the opening
line and puts each remaining element on its own, two columns in. That is
the structural half, which a printer has to decide. The indentation is the
editor's: [flan-mode] re-indents what it is shown, and that is where this
project's indentation rules already live, so nothing here tries to know
that a [let] aligns its bindings under the bracket. A client with no Emacs
still gets something readable rather than one very long line. *)
let pretty ?(width = 72) (f : t) : string =
let b = Buffer.create 256 in
let rec go col f =
let flat = to_source f in
if col + String.length flat <= width then Buffer.add_string b flat
else
let elements ind rest =
List.iter
(fun e ->
Buffer.add_char b '\n';
Buffer.add_string b (String.make ind ' ');
go ind e)
rest
in
match f.v with
(* A call or a special form: the head names what this is, so it stays on
the opening line whatever the rest of it costs. *)
| List (({ v = Sym _; _ } as h) :: rest) ->
Buffer.add_char b '(';
Buffer.add_string b (to_source h);
elements (col + 2) rest;
Buffer.add_char b ')'
| List (x :: rest) ->
Buffer.add_char b '(';
go (col + 1) x;
elements (col + 1) rest;
Buffer.add_char b ')'
| Vec (x :: rest) ->
Buffer.add_char b '[';
go (col + 1) x;
elements (col + 1) rest;
Buffer.add_char b ']'
| Map (x :: rest) ->
Buffer.add_char b '{';
go (col + 1) x;
elements (col + 1) rest;
Buffer.add_char b '}'
(* An empty bracket, or a single atom longer than the width. Neither has
a break in it to take. *)
| _ -> Buffer.add_string b flat
in
go 0 f;
Buffer.contents b