1923 lines
107 KiB
OCaml
1923 lines
107 KiB
OCaml
(* The indented syntax (spec-syntax.md): its reader, its printer, and the
|
|
switch between the two readers by file extension.
|
|
|
|
Four parts. Two programs hand-converted from paren to indented must read to
|
|
the same forms. Every corpus file must survive paren -> printed indented ->
|
|
read indented unchanged, up to the normalisation the spec allows. A table pins
|
|
the lexical edge cases and the refusals, with their kinds. And a program in
|
|
each syntax importing a package in the other builds and runs the same on
|
|
both backends. *)
|
|
|
|
open Flan
|
|
|
|
let () = Watchdog.arm ~seconds:300 "test_syntax"
|
|
|
|
let fail fmt = Test_support.fail fmt
|
|
let scratch = Test_support.scratch
|
|
|
|
(* ── Forms, compared without locations ─────────────────────────────── *)
|
|
|
|
let rec eq (a : Form.t) (b : Form.t) =
|
|
match a.v, b.v with
|
|
| Form.List x, Form.List y | Form.Vec x, Form.Vec y | Form.Map x, Form.Map y ->
|
|
List.length x = List.length y && List.for_all2 eq x y
|
|
| Form.Float x, Form.Float y ->
|
|
Int64.equal (Int64.bits_of_float x) (Int64.bits_of_float y)
|
|
| x, y -> x = y
|
|
|
|
(* The innermost pair that differs, for the failure line. *)
|
|
let rec first_diff (a : Form.t) (b : Form.t) =
|
|
match a.v, b.v with
|
|
| (Form.List x, Form.List y | Form.Vec x, Form.Vec y | Form.Map x, Form.Map y)
|
|
when List.length x = List.length y ->
|
|
(match List.find_opt (fun (p, q) -> not (eq p q)) (List.combine x y) with
|
|
| Some (p, q) -> first_diff p q
|
|
| None -> (a, b))
|
|
| _ -> (a, b)
|
|
|
|
let same_forms a b =
|
|
List.length a = List.length b && List.for_all2 eq a b
|
|
|
|
let describe_diff a b =
|
|
if List.length a <> List.length b then
|
|
Printf.sprintf "%d forms against %d" (List.length a) (List.length b)
|
|
else
|
|
match List.find_opt (fun (x, y) -> not (eq x y)) (List.combine a b) with
|
|
| Some (x, y) ->
|
|
let u, w = first_diff x y in
|
|
Printf.sprintf "wanted %s, read %s (at %d:%d)" (Form.to_string u)
|
|
(Form.to_string w) w.loc.Loc.line w.loc.Loc.col
|
|
| None -> "equal"
|
|
|
|
(* Spec §4 step 3's normalisation. Each rule keeps the meaning.
|
|
|
|
First every name a [let] binds is renamed, through its scope, to one
|
|
numbered in the order the binders come: so two forms that differ only in
|
|
what their [let]s call things compare equal, and one where a name was
|
|
captured does not. Then, where statements are a body run in order, a
|
|
[let] takes in the statements after it (the printer's flat [let]; with
|
|
every [let] name unique by now, nothing after it can mean one of them). A
|
|
[(do x)] whose [x] is a [let] is [x], a [let] whose whole body is another
|
|
[let] is the merged [let], [(fn [a] (do x y))] is [(fn [a] x y)], and
|
|
[(and x)] and [(or x)] are [x]. The flat
|
|
[let] and the one-argument [and] stop at a quote or quasiquote: data, or
|
|
a template whose unquotes could name anything. *)
|
|
|
|
(* A binding target with every struct pattern written as [name .field]
|
|
pairs: [{.x}] and [{:keys [x]}] are [{x .x}] (Parse.dmap). *)
|
|
let rec pairs_pat (t : Form.t) : Form.t =
|
|
let dotted s = String.length s > 1 && s.[0] = '.' in
|
|
let rec items = function
|
|
| ({ Form.v = Form.Sym s; _ } as f) :: rest when dotted s ->
|
|
{ f with v = Form.Sym (String.sub s 1 (String.length s - 1)) } :: f :: items rest
|
|
| { Form.v = Form.Kw "keys"; _ } :: { Form.v = Form.Vec ns; _ } :: rest ->
|
|
List.concat_map
|
|
(fun (n : Form.t) -> match n.v with
|
|
| Form.Sym x -> [ n; { n with v = Form.Sym ("." ^ x) } ]
|
|
| _ -> [ n ])
|
|
ns
|
|
@ items rest
|
|
| pat :: f :: rest -> pairs_pat pat :: f :: items rest
|
|
| rest -> rest
|
|
in
|
|
match t.v with
|
|
| Form.Vec l -> { t with v = Form.Vec (List.map pairs_pat l) }
|
|
| Form.Map l -> { t with v = Form.Map (items l) }
|
|
| _ -> t
|
|
|
|
(* The names a target so written binds, in order. *)
|
|
let rec binders (t : Form.t) =
|
|
match t.v with
|
|
| Form.Sym "&" -> []
|
|
| Form.Sym s -> [ s ]
|
|
| Form.Vec l -> List.concat_map binders l
|
|
| Form.Map l -> List.concat (List.filteri (fun i _ -> i mod 2 = 0) (List.map binders l))
|
|
| _ -> []
|
|
|
|
let canon (f : Form.t) : Form.t =
|
|
let k = ref 0 in
|
|
let look env s =
|
|
match List.assoc_opt s env with
|
|
| Some c -> c
|
|
| None ->
|
|
(* [x.y], a field path on a bound [x]. *)
|
|
match String.index_opt s '.' with
|
|
| Some i when i > 0 ->
|
|
(match List.assoc_opt (String.sub s 0 i) env with
|
|
| Some c -> c ^ String.sub s i (String.length s - i)
|
|
| None -> s)
|
|
| _ -> s
|
|
in
|
|
let rec go env (f : Form.t) =
|
|
let v =
|
|
match f.v with
|
|
(* The Lisp side may write the bit operators' .fln spellings, which read
|
|
back as their words: the same builtin by two names. *)
|
|
| Form.Sym ("&&" | "||" | "^^" as s) ->
|
|
Form.Sym (match s with "&&" -> "bit-and" | "||" -> "bit-or" | _ -> "bit-xor")
|
|
| Form.Sym s -> Form.Sym (look env s)
|
|
(* Quoted data keeps its names: renaming them would hide a printer
|
|
that renamed them too. *)
|
|
| Form.List ({ v = Form.Sym ("quote" | "quasiquote"); _ } :: _) -> f.v
|
|
| Form.List (({ v = Form.Sym "let"; _ } as h) :: ({ v = Form.Vec bs; _ } as bv) :: body) ->
|
|
let rec binds env acc = function
|
|
| t :: v :: rest ->
|
|
let v' = go env v in
|
|
let t = pairs_pat t in
|
|
let env' =
|
|
List.fold_left (fun e n -> incr k; (n, "%" ^ string_of_int !k) :: e)
|
|
env (binders t)
|
|
in
|
|
binds env' (v' :: go env' t :: acc) rest
|
|
| rest -> (env, List.rev_append acc (List.map (go env) rest))
|
|
in
|
|
let env', bs' = binds env [] bs in
|
|
Form.List (h :: { bv with v = Form.Vec bs' } :: List.map (go env') body)
|
|
| Form.List l -> Form.List (List.map (go env) l)
|
|
| Form.Vec l -> Form.Vec (List.map (go env) l)
|
|
| Form.Map l -> Form.Map (List.map (go env) l)
|
|
| v -> v
|
|
in
|
|
{ f with v }
|
|
in
|
|
go [] f
|
|
|
|
(* The macros of the file being compared ([Body_macros.table]). *)
|
|
let macros : Body_macros.t ref = ref (Body_macros.create ())
|
|
|
|
(* Where the statements of a body start, for a head whose trailing arguments
|
|
are a body run in order. *)
|
|
let body_start (l : Form.t list) =
|
|
let label k = match List.nth_opt l 1 with
|
|
| Some { Form.v = Form.Kw _; _ } -> k + 1 | _ -> k in
|
|
match l with
|
|
| { Form.v = Form.Sym h; _ } :: _ ->
|
|
(match h with
|
|
| "do" | "defer" -> Some 1
|
|
| "let" | "when" | "fn" | "loop" -> Some 2
|
|
| "while" | "until" | "dotimes" -> Some (label 2)
|
|
| "defmacro" -> Some 3
|
|
| "defmethod" -> Some 4
|
|
| "defn" | "defn-" ->
|
|
Some (match List.nth_opt l 4 with
|
|
| Some { Form.v = Form.Map _; _ } -> 5 | _ -> 4)
|
|
| h ->
|
|
(match List.assoc_opt h Body_macros.core with
|
|
| Some k -> Some (k + 1)
|
|
| None -> Option.map (fun k -> k + 1) (Hashtbl.find_opt !macros.bodies h)))
|
|
| _ -> None
|
|
|
|
let is_let (f : Form.t) =
|
|
match f.v with Form.List ({ v = Form.Sym "let"; _ } :: _) -> true | _ -> false
|
|
|
|
let rec shape ?(q = false) (f : Form.t) : Form.t =
|
|
let q = q || (match f.v with
|
|
| Form.List ({ v = Form.Sym ("quote" | "quasiquote"); _ } :: _) -> true | _ -> false) in
|
|
let sh = shape ~q in
|
|
(* A body's statements, each [let] taking in the ones after it. *)
|
|
let rec stmts = function
|
|
| [] -> []
|
|
| x :: (_ :: _ as rest) when not q ->
|
|
(match (sh x).v with
|
|
| Form.List (({ v = Form.Sym "let"; _ } as h) :: ({ v = Form.Vec (_ :: _); _ } as bv)
|
|
:: (_ :: _ as body)) ->
|
|
[ sh { x with v = Form.List (h :: bv :: (body @ rest)) } ]
|
|
| _ -> sh x :: stmts rest)
|
|
| x :: rest -> sh x :: stmts rest
|
|
in
|
|
let seq_list l =
|
|
match body_start l with
|
|
| Some k when List.length l > k ->
|
|
List.map sh (List.filteri (fun i _ -> i < k) l)
|
|
@ stmts (List.filteri (fun i _ -> i >= k) l)
|
|
| _ -> List.map sh l
|
|
in
|
|
(* Handler and restart clauses: [(name [v] body ...)]. *)
|
|
let clause (c : Form.t) =
|
|
match c.v with
|
|
| Form.List (n :: p :: body) -> { c with v = Form.List (sh n :: sh p :: stmts body) }
|
|
| _ -> sh c
|
|
in
|
|
match f.v with
|
|
| Form.List [ { v = Form.Sym ("and" | "or"); _ }; x ] when not q -> sh x
|
|
| _ ->
|
|
let v =
|
|
match f.v with
|
|
| Form.List (({ v = Form.Sym "let"; _ } as h) :: { v = Form.Vec bs; loc } :: body) ->
|
|
(match stmts body with
|
|
| [ { v = Form.List ({ v = Form.Sym "let"; _ } :: { v = Form.Vec bs2; _ } :: body2); _ } ] ->
|
|
Form.List (h :: Form.make (Form.Vec (List.map sh bs @ bs2)) loc :: body2)
|
|
| body -> Form.List (h :: Form.make (Form.Vec (List.map sh bs)) loc :: body))
|
|
(* A lambda whose body is one [do] is the lambda of its statements:
|
|
the printer writes them straight under [=>]. *)
|
|
| Form.List [ ({ v = Form.Sym "fn"; _ } as h); ({ v = Form.Vec _; _ } as ps);
|
|
{ v = Form.List ({ v = Form.Sym "do"; _ } :: (_ :: _ :: _ as ss)); _ } ]
|
|
when not q ->
|
|
(sh { f with v = Form.List (h :: ps :: ss) }).v
|
|
| Form.List (({ v = Form.Sym "handler-case"; _ } as h) :: body :: ({ v = Form.Vec cls; _ } as cv) :: more) ->
|
|
Form.List (h :: sh body :: { cv with v = Form.Vec (List.map clause cls) }
|
|
:: List.map sh more)
|
|
| Form.List (({ v = Form.Sym "handler-bind"; _ } as h) :: ({ v = Form.Vec cls; _ } as cv) :: body) ->
|
|
Form.List (h :: { cv with v = Form.Vec (List.map clause cls) } :: stmts body)
|
|
| Form.List (({ v = Form.Sym "restart-case"; _ } as h) :: body :: cls) ->
|
|
Form.List (h :: sh body :: List.map clause cls)
|
|
| Form.List l ->
|
|
(match seq_list l with
|
|
| [ { v = Form.Sym "do"; _ }; x ] when is_let x -> x.v
|
|
| l -> Form.List l)
|
|
| Form.Vec l -> Form.Vec (List.map sh l)
|
|
| Form.Map l -> Form.Map (List.map sh l)
|
|
| v -> v
|
|
in
|
|
{ f with v }
|
|
|
|
let norm f = shape (canon f)
|
|
|
|
let diag_text = function
|
|
| Loc.Error d -> Printf.sprintf "%s %d:%d %s" d.Loc.kind d.dloc.Loc.line d.dloc.Loc.col d.dmsg
|
|
| e -> Printexc.to_string e
|
|
|
|
(* ── The hand-converted pairs ──────────────────────────────────────── *)
|
|
|
|
let pair flan fln =
|
|
match Reader.read_file flan, Source.read_file fln with
|
|
| a, b ->
|
|
macros := Body_macros.table ~file:flan a;
|
|
(* Normalised: a hand conversion writes a let flat where its scope does
|
|
not matter, as the printer does. *)
|
|
let a = List.map norm a and b = List.map norm b in
|
|
if not (same_forms a b) then
|
|
fail "%s and %s read differently: %s" flan fln (describe_diff a b)
|
|
| exception e -> fail "%s / %s: %s" flan fln (diag_text e)
|
|
|
|
let () =
|
|
pair "syntax/algorithms.flan" "syntax/algorithms.fln";
|
|
(* sand itself is .fln now (decision 130), so there is no more paren
|
|
original to pair it against here; syntax/sand.fln is still checked
|
|
below. *)
|
|
pair "syntax/infer/main.flan" "syntax/infer/main.fln";
|
|
(* Checked, never run: sand opens a window. *)
|
|
List.iter
|
|
(fun f ->
|
|
match Front.checked f with
|
|
| _ -> ()
|
|
| exception e -> fail "%s does not check: %s" f (diag_text e))
|
|
[ "syntax/sand.fln"; "syntax/algorithms.fln" ]
|
|
|
|
(* ── The round trip over the corpus ────────────────────────────────── *)
|
|
|
|
(* The comments of a text, as a sorted list: where each lands may move — a
|
|
comment inside an expression printed on one line goes above it — but none
|
|
may be lost or made. *)
|
|
let comment_texts src =
|
|
List.sort compare
|
|
(List.map (fun (c : Source_text.comment) -> String.trim c.text) (Source_text.comments src))
|
|
|
|
(* What each comment is attached to. An own-line comment belongs to the form
|
|
after it; a trailing one to the last form that starts on its line. After a
|
|
conversion, the form after an own-line comment must be that form or one
|
|
holding it (a comment inside an expression printed on one line goes above
|
|
the line), and a trailing comment's line — or, when it had to move onto a
|
|
line of its own, the line above — must hold its form. So a comment that
|
|
drifted to another statement is caught, not only a lost one. *)
|
|
let starts_of (fs : Form.t list) =
|
|
let out = ref [] in
|
|
let rec walk (f : Form.t) =
|
|
(* Outermost first among forms starting at one place: [x = v] and its
|
|
[x] start together, and the statement is what a comment is about. *)
|
|
let t = Form.to_string f in
|
|
out := ((f.loc.Loc.line, f.loc.Loc.col, - String.length t), t) :: !out;
|
|
match f.v with
|
|
| Form.List l | Form.Vec l | Form.Map l -> List.iter walk l
|
|
| _ -> ()
|
|
in
|
|
(* The normalised forms: a [let] a flat line extended is, on both sides,
|
|
the one that holds what now follows it. *)
|
|
List.iter walk (List.map norm fs);
|
|
List.map (fun ((l, c, _), t) -> (l, c, t)) (List.sort compare !out)
|
|
|
|
let attachments src forms =
|
|
let st = starts_of forms in
|
|
List.map
|
|
(fun (c : Source_text.comment) ->
|
|
let owner =
|
|
if c.own_line then
|
|
List.find_opt (fun (l, _, _) -> l > c.line) st
|
|
else
|
|
(* The last place a form starts on the line, and the outermost
|
|
form starting there. *)
|
|
List.fold_left
|
|
(fun acc ((l, col, _) as x) ->
|
|
match acc with
|
|
| Some (_, col', _) when l = c.line && col = col' -> acc
|
|
| _ -> if l = c.line then Some x else acc)
|
|
None st
|
|
in
|
|
(c, Option.map (fun (_, _, t) -> t) owner))
|
|
(Source_text.comments src)
|
|
|
|
let attached_ok ~what path (src, forms) (out, back) =
|
|
let want = attachments src forms and got = attachments out back in
|
|
let st = starts_of back in
|
|
let on_line l = List.filter_map (fun (l', _, t) -> if l' = l then Some t else None) st in
|
|
(* Paired by text, in order: the n-th copy of a text with the n-th. *)
|
|
let rec pair = function
|
|
| [] -> ()
|
|
| ((c : Source_text.comment), o) :: rest ->
|
|
let t = String.trim c.text in
|
|
let same (d : Source_text.comment) = String.trim d.text = t in
|
|
let rec take = function
|
|
| [] -> None
|
|
| ((d, _) as x) :: xs ->
|
|
if same d && not (List.memq x !used) then (used := x :: !used; Some x)
|
|
else take xs
|
|
in
|
|
(match take got, o with
|
|
| None, _ -> fail "%s %s: the comment %s went missing" what path t
|
|
| Some _, None -> ()
|
|
| Some ((d : Source_text.comment), g), Some o ->
|
|
let holds x = Test_support.contains x o in
|
|
(* The code line a moved trailing comment sits under: up past the
|
|
comment lines between. *)
|
|
let rec code_above l =
|
|
if l < 1 then []
|
|
else match on_line l with [] -> code_above (l - 1) | fs -> fs
|
|
in
|
|
let fine =
|
|
if c.own_line then
|
|
(match g with
|
|
(* Above the form, above the statement holding it, or above the
|
|
first statement of the block it was: all of those read as
|
|
being about it. *)
|
|
| Some g -> holds g || (String.length g > 4 && Test_support.contains o g)
|
|
| None -> false)
|
|
else
|
|
List.exists holds (on_line d.line)
|
|
|| (d.own_line && List.exists holds (code_above (d.line - 1)))
|
|
in
|
|
if not fine then
|
|
fail "%s %s: the comment %s (line %d) was about %s and is now beside %s"
|
|
what path t c.line o (Option.value g ~default:"nothing"));
|
|
pair rest
|
|
and used = ref [] in
|
|
pair want
|
|
|
|
(* Every .flan the build tree holds. [..] is the workspace root from here;
|
|
the deps in test/dune decide what is in it. *)
|
|
let corpus () =
|
|
(* recur.flan is about the Lisp loop form, which the indented syntax does
|
|
not have. *)
|
|
let lisp_only = [ "recur.flan" ] in
|
|
let rec walk dir acc =
|
|
Array.fold_left
|
|
(fun acc name ->
|
|
let path = Filename.concat dir name in
|
|
if name <> "" && (name.[0] = '.' || name.[0] = '_') then acc
|
|
else if Sys.is_directory path then walk path acc
|
|
else if Filename.check_suffix name ".flan" && not (List.mem name lisp_only) then
|
|
path :: acc
|
|
else acc)
|
|
acc (Sys.readdir dir)
|
|
in
|
|
List.sort String.compare (walk ".." [])
|
|
|
|
let () =
|
|
let ok = ref 0 in
|
|
List.iter
|
|
(fun path ->
|
|
match Reader.read_file path with
|
|
| exception Loc.Error _ -> () (* not a program the paren reader takes *)
|
|
| forms ->
|
|
let source = In_channel.with_open_bin path In_channel.input_all in
|
|
macros := Body_macros.table ~file:path forms;
|
|
match Indent_printer.program ~source ~macros:!macros forms with
|
|
| exception Indent_printer.Unprintable (f, why) ->
|
|
fail "round trip %s: %s at %d:%d" path why f.loc.Loc.line f.loc.Loc.col
|
|
| text ->
|
|
match Indent_reader.read_all ~file:(path ^ ".fln") text with
|
|
| exception e -> fail "round trip %s: %s" path (diag_text e)
|
|
| back ->
|
|
let a = List.map norm forms and b = List.map norm back in
|
|
if not (same_forms a b) then
|
|
fail "round trip %s: %s" path (describe_diff a b)
|
|
else if comment_texts text <> comment_texts source then
|
|
fail "round trip %s: the comments did not all come through" path
|
|
else begin
|
|
attached_ok ~what:"round trip" path (source, forms) (text, back);
|
|
(* And back to parens, from the indented text: the forms and
|
|
the comments survive the second printer too. *)
|
|
let paren = Paren_printer.program ~source:text back in
|
|
match Reader.read_all ~file:path paren with
|
|
| exception e -> fail "back to parens %s: %s" path (diag_text e)
|
|
| again ->
|
|
if not (same_forms (List.map norm again) b) then
|
|
fail "back to parens %s: %s" path
|
|
(describe_diff b (List.map norm again))
|
|
else if comment_texts paren <> comment_texts source then
|
|
fail "back to parens %s: the comments did not all come through" path
|
|
else begin
|
|
attached_ok ~what:"back to parens" path (text, back) (paren, again);
|
|
incr ok
|
|
end
|
|
end)
|
|
(corpus ());
|
|
Printf.printf "round trip: %d files\n" !ok;
|
|
(* The deps decide what is walked, and a stanza that lost them would pass
|
|
over nothing. *)
|
|
if !ok < 390 then fail "round trip covered only %d files" !ok
|
|
|
|
(* Names that are operator words are written in parentheses, and a group
|
|
reads them back. *)
|
|
let () =
|
|
let src =
|
|
"(defn f [] () (let [a 1 not 2 and 3 or 4 + 5 - 6 < 7 = 8 mod 9 % 10 b 11] \
|
|
(g a not and or + - < = mod % b)))"
|
|
in
|
|
let forms = Reader.read_all ~file:"<ops>" src in
|
|
let text = Indent_printer.program ~source:src forms in
|
|
if not (Test_support.contains text " (not) = 2\n (and) = 3") then
|
|
fail "operator-word bindings printed %S" text;
|
|
match Indent_reader.read_all ~file:"<ops>.fln" text with
|
|
| back ->
|
|
if not (same_forms (List.map norm forms) (List.map norm back)) then
|
|
fail "operator-word bindings: %s" (describe_diff (List.map norm forms) (List.map norm back))
|
|
| exception e -> fail "operator-word bindings read back: %s\n%s" (diag_text e) text
|
|
|
|
(* ── Lexical edge cases ────────────────────────────────────────────── *)
|
|
|
|
(* [~global:false] reads as an expression the editor sends, where a [let] is
|
|
local; a file's top-level [let] is a global. *)
|
|
let read ?(global = true) src = Indent_reader.read_all ~global_let:global ~file:"<syntax>" src
|
|
|
|
let reads ?global name src want =
|
|
match read ?global src with
|
|
| forms ->
|
|
let got = String.concat "\n" (List.map Form.to_string forms) in
|
|
if got <> want then fail "%s: read %s, wanted %s" name got want
|
|
| exception e -> fail "%s: refused: %s" name (diag_text e)
|
|
|
|
let refuses ?global name src kind needle =
|
|
match read ?global src with
|
|
| forms ->
|
|
fail "%s: read %s, wanted the refusal %s" name
|
|
(String.concat " " (List.map Form.to_string forms)) kind
|
|
| exception Loc.Error d ->
|
|
if d.Loc.kind <> kind then fail "%s: refused as %s, wanted %s (%s)" name d.Loc.kind kind d.dmsg
|
|
else if not (Test_support.contains d.dmsg needle) then
|
|
fail "%s: %s does not say %S: %s" name kind needle d.dmsg
|
|
| exception e -> fail "%s: %s" name (Printexc.to_string e)
|
|
|
|
let () =
|
|
(* Minus. *)
|
|
reads "subtraction" "x = a - 1" "(set x (- a 1))";
|
|
reads "negative literal" "x = -1" "(set x -1)";
|
|
reads "negation" "x = -y" "(set x (- y))";
|
|
reads "negation binds after postfix" "x = -p.x" "(set x (- (.x p)))";
|
|
reads "lisp name" "x = a-b" "(set x a-b)";
|
|
reads "decrement is a name" "--(j)" "(-- j)";
|
|
reads "minus as a call" "x = -(a + b)" "(set x (- (+ a b)))";
|
|
refuses "glued minus" "x = a -1" "indent/glued-minus" "a - 1";
|
|
refuses "glued minus in a call" "f(a -1)" "indent/glued-minus" "space the minus";
|
|
(* The arrow. *)
|
|
reads "return arrow" "fn f(x: i32) -> i32 = x" "(defn f [x i32] i32 x)";
|
|
reads "arrow inside a name" "fn dyn->f64(v: f64) -> f64 = v" "(defn dyn->f64 [v f64] f64 v)";
|
|
reads "function type"
|
|
"fn g(h: Fn(i32, i32) -> bool) -> () = h(1, 2)"
|
|
"(defn g [h (Fn [i32 i32] bool)] () (h 1 2))";
|
|
reads "untyped parameter is dyn" "fn id(x) -> dyn = x" "(defn id [x dyn] dyn x)";
|
|
reads "no arrow infers the return" "fn f(x)\n x" "(defn f [x dyn] _ x)";
|
|
reads "no arrow, one expression" "fn f(x: i32) = x + 1" "(defn f [x i32] _ (+ x 1))";
|
|
reads "no arrow, with where" "fn f(x: $t) where is-ordered($t) = x"
|
|
"(defn f [x $t] _ {:where (is-ordered $t)} x)";
|
|
(* The fix is .fln's commas whatever the file is called: [read] names it
|
|
<syntax>. *)
|
|
refuses "where predicates joined with and"
|
|
"fn f(x: $t, y: $u) -> i32 where is-ordered($t) and is-equal($u)\n 0"
|
|
"indent/where-and" "commas, not and — write where is-ordered($t), is-equal($u)";
|
|
(* Characters, lexed before brackets and separators. *)
|
|
reads "character literals" "x = [\\( \\, \\space \\)]" "(set x [\\( \\, \\space \\)])";
|
|
reads "character arguments" "f(\\,, \\))" "(f \\, \\))";
|
|
reads "character spellings"
|
|
"x = [\\u0041 \\u0007 \\backspace \\formfeed \\日]"
|
|
"(set x [\\A \\u0007 \\backspace \\formfeed \\日])";
|
|
(* Keywords and annotations. *)
|
|
reads "keyword" "let k = :else" "(def k dyn :else)";
|
|
reads "annotation" "once grid: [4 [8 u32]]" "(defonce grid [4 [8 u32]])";
|
|
reads "keyword argument" "rl/is-key-pressed(:key-r)" "(rl/is-key-pressed :key-r)";
|
|
refuses "colon inside a name" "fn f(x:i32) -> () = x" "indent/colon-in-name" "x: i32";
|
|
(* Adjacency. *)
|
|
reads "call" "f(a, b)" "(f a b)";
|
|
reads "index" "x[i, j]" "(at x i j)";
|
|
reads "call of a call" "f(a)(b)" "((f a) b)";
|
|
reads "field chain" "camera.target.x" "(.x (.target camera))";
|
|
reads "qualified case" "Shape.Rect" "Shape.Rect";
|
|
reads "field of a call" "f(x).y" "(.y (f x))";
|
|
reads "struct literal" "Vector2{.x 1, .y 2}" "(Vector2 {.x 1 .y 2})";
|
|
reads "operator call" "+(a, b, c)" "(+ a b c)";
|
|
reads "operator value" "reduce(+, 0, xs)" "(reduce + 0 xs)";
|
|
refuses "spaced call" "f (a)" "indent/spaced-call" "f(...)";
|
|
refuses "spaced index" "x [i]" "indent/spaced-index" "x[i]";
|
|
refuses "missing comma" "f(a b)" "indent/missing-comma" "commas";
|
|
refuses "unspaced operator" "x = f(a)+ b" "indent/unspaced-operator" "a + b";
|
|
(* Collections. *)
|
|
reads "whitespace vector" "x = [i n]" "(set x [i n])";
|
|
reads "comma vector" "x = [a - 1, b]" "(set x [(- a 1) b])";
|
|
refuses "operator between spaces" "x = [a - 1 b]" "indent/separate-elements" "commas";
|
|
reads "quoted list" "x = '(a b c)" "(set x (quote (a b c)))";
|
|
(* Trailing colon blocks. *)
|
|
reads "trailing block" "rl/with-drawing():\n clear()\n draw()"
|
|
"(rl/with-drawing (clear) (draw))";
|
|
reads "fallback with a block" "defmethod(describe, :square, [s]):\n s"
|
|
"(defmethod describe :square [s] s)";
|
|
refuses "block without the colon" "f(x)\n y" "indent/stray-indent" "trailing colon";
|
|
refuses "colon on a non-call" "a + b:\n y" "indent/colon-block" "comment:";
|
|
reads "bare name takes a block" "comment:\n f()\n g()" "(comment (f) (g))";
|
|
reads "qualified name takes a block" "rl/with-drawing:\n f()" "(rl/with-drawing (f))";
|
|
(* Indentation. *)
|
|
refuses "tab" "fn f() -> ()\n\tg()" "indent/tab" "spaces";
|
|
refuses "dedent to no block" "if a\n b\n c" "indent/dedent"
|
|
"between the block at column 1 and the one at column 5";
|
|
(* A continuation sits deeper than the line it continues. *)
|
|
refuses "leading operator left of its block" "if a\n b\n+ 1" "indent/continuation" "column 3";
|
|
refuses "leading operator at the statement's column" "let x = 1\n+ 2\nx"
|
|
"indent/continuation" "Indent it further";
|
|
refuses "trailing operator, shallower next line" "if a\n x = b +\nc"
|
|
"indent/continuation" "finish the line above";
|
|
reads "blank and comment lines" "if a\n\n ; note\n b\n\n; more\nc"
|
|
"(when a b)\nc";
|
|
(* Continuation lines. *)
|
|
reads "trailing operator" "x = a +\n b" "(set x (+ a b))";
|
|
reads "leading operator" "x = a\n + b" "(set x (+ a b))";
|
|
reads "continued condition" "if a\n and b\n c" "(when (and a b) c)";
|
|
(* Runs of one operator. *)
|
|
reads "flattened" "x = a + b + c" "(set x (+ a b c))";
|
|
reads "chain" "x = a < b < c" "(set x (< a b c))";
|
|
reads "left to right" "x = a - b + c" "(set x (+ (- a b) c))";
|
|
reads "precedence" "x = a or b and not c == d" "(set x (or a (and b (not (= c d)))))";
|
|
(* not is a prefix word, between and and the comparisons. *)
|
|
reads "not over a comparison" "x = not a == b" "(set x (not (= a b)))";
|
|
reads "not under and" "x = not a and b" "(set x (and (not a) b))";
|
|
reads "not twice" "x = not not a" "(set x (not (not a)))";
|
|
reads "not of a group" "x = not (a or b)" "(set x (not (or a b)))";
|
|
reads "not glued is a call" "x = not(a) or b" "(set x (or (not a) b))";
|
|
reads "not as a statement" "if not done\n go()" "(when (not done) (go))";
|
|
refuses "not after a comparison" "x = a == not b" "indent/not-operand" "parentheses with what it negates:\n\n x = a == (not b)";
|
|
refuses "not after arithmetic" "if 1 + not b and c\n g()" "indent/not-operand" "if 1 + (not b) and c";
|
|
refuses "not in a spaced vector" "x = [not a b]" "indent/separate-elements" "commas";
|
|
(* The bit operators: tighter than a comparison, looser than a shift, and
|
|
among themselves && then ^^ then ||. *)
|
|
reads "bit and under a comparison" "x = a && mask == 0"
|
|
"(set x (= (bit-and a mask) 0))";
|
|
reads "bit operator order" "x = a || b ^^ c && d << 2 + 1"
|
|
"(set x (bit-or a (bit-xor b (bit-and c (<< d (+ 2 1))))))";
|
|
reads "bit operators left to right" "x = a && b && c || d"
|
|
"(set x (bit-or (bit-and a b c) d))";
|
|
reads "bit-not" "x = ~~a && ~~f(b)" "(set x (bit-and (bit-not a) (bit-not (f b))))";
|
|
reads "bit-not of a negation" "x = ~~-a" "(set x (bit-not (- a)))";
|
|
reads "bit operator values" "x = reduce(^^, 0, xs)" "(set x (reduce bit-xor 0 xs))";
|
|
reads "bit-not in a spaced vector" "x = [~~a b]" "(set x [(bit-not a) b])";
|
|
reads "a nested unquote" "quote\n f(~(~x))" "(quasiquote (f (unquote (unquote x))))";
|
|
reads "bit-not in a template" "quote\n f(~~x, ~(~~y))"
|
|
"(quasiquote (f (bit-not x) (unquote (bit-not y))))";
|
|
refuses "not-equal chain" "x = a != b != c" "indent/chained-not-equal" "!=(a, b, c)";
|
|
reads "not-equal call" "x = !=(a, b, c)" "(set x (!= a b c))";
|
|
(* One direction mixes; each operand that is a call is bound once, in
|
|
order, before any test. *)
|
|
reads "mixed chain" "x = 0 <= r < rows" "(set x (and (<= 0 r) (< r rows)))";
|
|
reads "mixed chain of four" "x = a < b <= c < d"
|
|
"(set x (and (< a b) (<= b c) (< c d)))";
|
|
reads "mixed chain downward" "x = x >= y > 0" "(set x (and (>= x y) (> y 0)))";
|
|
(* A name is bound too once any operand is, so it is read in its turn. *)
|
|
reads "mixed chain over a call" "x = a < f(b) <= c"
|
|
"(set x (let [~cmp1 a ~cmp2 (f b) ~cmp3 c] (and (< ~cmp1 ~cmp2) (<= ~cmp2 ~cmp3))))";
|
|
reads "mixed chain over two calls" "x = 0 < f() <= g() < h()"
|
|
"(set x (let [~cmp1 (f) ~cmp2 (g) ~cmp3 (h)] (and (< 0 ~cmp1) (<= ~cmp1 ~cmp2) (< ~cmp2 ~cmp3))))";
|
|
refuses "chain that turns around" "x = a < b > c" "indent/mixed-comparison"
|
|
"a < b and b > c";
|
|
refuses "== in a chain" "x = a == b < c" "indent/mixed-comparison" "a == b and b < c";
|
|
refuses "== after a chain" "x = x < 1 <= 2 == true" "indent/mixed-comparison"
|
|
"x < 1 and 1 <= 2 and 2 == true";
|
|
refuses "a refused chain's middle call is named once" "x = a < f(b) <= g(c) > d"
|
|
"indent/mixed-comparison"
|
|
"let mid = f(b)\n let mid2 = g(c)\n a < mid and mid <= mid2 and mid2 > d";
|
|
(* Statements. *)
|
|
reads "lets merge" "fn f() -> i32\n let a = 1\n let b = 2\n a + b"
|
|
"(defn f [] i32 (let [a 1 b 2] (+ a b)))";
|
|
refuses ~global:false "let with a block" "let a = 1\n a\nb" "indent/let-block" "goes at the let's column";
|
|
reads ~global:false "flat let" "let a = 1\na\nb" "(let [a 1] a b)";
|
|
reads "elif" "if a\n 1\nelif b\n 2\nelse\n 3" "(cond a 1 b 2 :else 3)";
|
|
reads "one-line if" "x = if a then 1 else 2" "(set x (if a 1 2))";
|
|
reads "assignment ops" "a[i] += 1" "(set (at a i) (+ (at a i) 1))";
|
|
(* A place with a call in it is evaluated once: it reads as update. *)
|
|
reads "assignment op over a call's place" "a[next()] += 1"
|
|
"(update (at a (next)) + 1)";
|
|
reads "for" "for :outer i in range(1, n)\n f(i)" "(dotimes :outer [i 1 n] (f i))";
|
|
reads "unit statement" "restart-case\n f()\nrestart continue()\n ()"
|
|
"(restart-case (f) (continue [] (do)))";
|
|
reads "match" "match s\n Circle(r) -> r\n _ ->\n a()\n b()"
|
|
"(match s (Circle r) r _ (do (a) (b)))";
|
|
reads "match over literals" "match n\n 5 -> a\n -2.5 -> b\n \"go\" -> c\n \\a -> d\n _ -> e"
|
|
"(match n 5 a -2.5 b \"go\" c \\a d _ e)";
|
|
reads "match over a bool" "match b\n true -> a\n false -> b"
|
|
"(match b true a false b)";
|
|
reads "keyword arms over a dyn" "match d\n :north -> a\n 1 -> b\n _ -> c"
|
|
"(match d :north a 1 b _ c)";
|
|
reads "handler-bind moves the clauses" "handler-bind\n f()\non E(c)\n g(c)"
|
|
"(handler-bind [(E [c] (g c))] (f))";
|
|
reads "quote block"
|
|
"defmacro(m, [x & ys]):\n quote\n f(~x)\n ~@ys"
|
|
"(defmacro m [x & ys] (quasiquote (do (f (unquote x)) (unquote-splicing ys))))";
|
|
reads "lambda" "g = fn(i, j) => i * 10 + j" "(set g (fn [i j] (+ (* i 10) j)))";
|
|
reads "lambda with a block" "g = fn(i) =>\n a(i)\n b(i)" "(set g (fn [i] (a i) (b i)))";
|
|
reads "where" "fn s(xs: [$t]) -> () where is-ordered($t) = f(xs)"
|
|
"(defn s [xs [$t]] () {:where (is-ordered $t)} (f xs))";
|
|
reads "data" "data Shape\n Circle(r: f32)\n Empty"
|
|
"(defdata Shape [(Circle [r f32]) Empty])";
|
|
reads "enum" "enum K\n lo = -1\n mid" "(defenum K [lo -1 mid])";
|
|
reads "struct" "struct Cell\n row: i32\n tag" "(defstruct Cell [row i32 tag dyn])";
|
|
reads "struct with a parent" "struct DiskFull :parent IoError\n free: i64"
|
|
"(defstruct DiskFull :parent IoError [free i64])";
|
|
reads "type alias" "type Row = Vec(i32)" "(defalias Row (Vec i32))";
|
|
reads "type alias of an array" "type V2 = [2 f32]" "(defalias V2 [2 f32])";
|
|
reads "a local named type" "type = 3" "(set type 3)";
|
|
reads "a header word assigned" "data += 3" "(set data (+ data 3))";
|
|
reads "a clause word assigned after its header"
|
|
"handler-case\n g()\non E(c)\n h(c)\non = 2"
|
|
"(handler-case (g) [(E [c] (h c))])\n(set on 2)";
|
|
reads "an else assigned after an if" "if a\n b\nelse = 2" "(when a b)\n(set else 2)";
|
|
reads "class" "class lambda(param, body, env)" "(defclass lambda [param body env])";
|
|
reads "class with typed slots" "class state\n pause: bool\n tag"
|
|
"(defclass state [pause bool tag])";
|
|
reads "generic" "generic describe(v) -> dyn" "(defgeneric describe [v] dyn)";
|
|
reads "multi" "multi kind(v) -> dyn = type-of(v)" "(defmulti kind [v] dyn (type-of v))";
|
|
reads "method on a class" "method describe(f: lambda, x)\n g(f)"
|
|
"(defmethod describe lambda [f x] (g f))";
|
|
reads "method on a value" "method kind(v) when :else = 1" "(defmethod kind :else [v] 1)";
|
|
refuses "a generic's typed parameter" "generic g(p: point) -> dyn" "indent/dyn-parameter"
|
|
"write generic g(p) -> dyn";
|
|
refuses "a method with no dispatch" "method g(p)\n 1" "indent/method-key"
|
|
"method g(p: point)";
|
|
refuses "a method with both" "method g(p: point) when :x\n 1" "indent/method-key"
|
|
"not both";
|
|
reads "a top-level let is a global" "let g = 1\nlet h: i32 = 2\nlet s: [4 u8]\nf(g)"
|
|
"(def g dyn 1)\n(def h i32 2)\n(def s [4 u8])\n(f g)";
|
|
refuses "def" "def g: i32 = 1" "indent/def-is-let" "let g: i32 = 1";
|
|
refuses "a bare def" "def g" "indent/def-is-let" "let g: i32 = 0";
|
|
(match read "generic f(a)\n\nfn g() = 1" with
|
|
| exception Loc.Error d when d.Loc.dloc.Loc.line = 1 && d.Loc.dloc.Loc.col = 13 -> ()
|
|
| exception Loc.Error d ->
|
|
fail "a generic with no arrow is refused at %d:%d, not at its line's end"
|
|
d.Loc.dloc.Loc.line d.Loc.dloc.Loc.col
|
|
| _ -> fail "a generic with no arrow was read");
|
|
reads "a let in a comment block stays local" "comment:\n let x = 1\n f(x)"
|
|
"(comment (let [x 1] (f x)))";
|
|
reads "and in a fn" "fn f() -> i32\n let x = 1\n x" "(defn f [] i32 (let [x 1] x))";
|
|
reads "a one-line struct" "struct Pt(x: i32, y)" "(defstruct Pt [x i32 y dyn])";
|
|
reads "a one-line union" "union U(a: i32)" "(defunion U [a i32])";
|
|
reads "a one-line struct with a parent" "struct D(free: i64) :parent Io"
|
|
"(defstruct D :parent Io [free i64])";
|
|
refuses "a one-line struct takes no block" "struct Pt(x: i32)\n y: i32" "indent/stray-indent"
|
|
"takes no block";
|
|
reads "a parent and no fields" "struct Io :parent Error" "(defstruct Io :parent Error)";
|
|
reads "macro" "macro repeat(i, n, & body)\n quote\n f(~i)\n ~@body"
|
|
"(defmacro repeat [i n & body] (quasiquote (do (f (unquote i)) (unquote-splicing body))))";
|
|
reads "macro with a pattern" "macro m([a b], c)\n a" "(defmacro m [[a b] c] a)";
|
|
reads "macro with no parameters" "macro m()\n a" "(defmacro m [] a)";
|
|
refuses "a rest parameter not last" "macro m(& a, b)\n a" "indent/macro-rest-last"
|
|
"comes last: macro m(b, & a)";
|
|
(* No loop and no recur: a .fln loop is a while, until, dotimes or for. *)
|
|
refuses "a loop header" "loop x = a, y = b + 1\n recur(y, x)" "indent/no-loop"
|
|
"while i < 10";
|
|
refuses ~global:false "a let-bound loop" "let r = loop i = 0\n i\nr" "indent/no-loop"
|
|
"loop is not part of the indented syntax";
|
|
refuses "a loop call" "loop([x 1]):\n x" "indent/no-loop" "while, until, dotimes or for";
|
|
refuses "a loop over a block" "loop\n g()" "indent/no-loop" "loop is not";
|
|
refuses "a recur call" "f(recur(1))" "indent/no-loop" "recur is not part of the indented syntax";
|
|
refuses "a loop in a macro's quote" "macro m(a)\n quote\n loop([i ~a]):\n i"
|
|
"indent/no-loop" "loop is not";
|
|
refuses "a quoted loop" "f('(loop [i 0] (recur i)))" "indent/no-loop" "loop is not";
|
|
reads "loop as a name" "loop = 4" "(set loop 4)";
|
|
reads "if over loop" "if loop\n 1" "(when loop 1)";
|
|
reads "while over loop" "while loop\n g()" "(while loop (g))";
|
|
reads "until over loop" "until loop\n g()" "(until loop (g))";
|
|
reads "elif over loop" "if recur\n 1\nelif loop\n 2" "(cond recur 1 loop 2)";
|
|
reads "a one-line if over loop" "if loop then 1 else 2" "(if loop 1 2)";
|
|
reads "a one-line if over recur" "if recur > 0 then recur else 0" "(if (> recur 0) recur 0)";
|
|
reads ~global:false "a typed let of loop" "let loop: i32 = 1\nloop" "(let [loop (the i32 1)] loop)";
|
|
reads "a match over loop, and an arm of it" "match loop\n loop -> loop" "(match loop loop loop)";
|
|
reads "read-only pointer" "let p: Ptr(const u8) = uninit" "(def p (Ptr const u8) uninit)";
|
|
(* Statements that fit on a line, in one-line slots. *)
|
|
reads "arm statements" "match s\n 1 -> break\n 2 -> continue :outer\n _ -> x += 1"
|
|
"(match s 1 (break) 2 (continue :outer) _ (set x (+ x 1)))";
|
|
reads "then break" "if c then break" "(when c (break))";
|
|
reads "then return else assign" "if c then return 5 else x = 2" "(if c (return 5) (set x 2))";
|
|
reads "return in an expression if" "y = if c then return else 1" "(set y (if c (return) 1))";
|
|
reads "defer an assignment" "defer x = 0" "(defer (set x 0))";
|
|
(* Messages with a shape of their own. *)
|
|
refuses "parenthesised pair" "x = (a, b)" "indent/tuple" "[a, b]";
|
|
refuses "rest parameter" "fn f(& rest) -> () = 0" "indent/rest-parameter" "xs: [T]";
|
|
refuses "assignment as a test" "if x = 1\n y" "indent/assign-in-test" "write == instead";
|
|
(* A message quotes the text as written, never the paren form. *)
|
|
refuses "two assignments" "if a then b = c = d" "indent/assign-in-test" "if a then b = c,";
|
|
refuses "a let-bound if with no block" "let r = if a > 1\nr" "indent/expected-block" "if a > 1 takes";
|
|
refuses "two bindings on a line" "let v: i32 = a, w = b" "indent/one-binding" "a is followed by a comma";
|
|
refuses "two bindings on a let's line" "fn f()\n let v = a, w = b\n v" "indent/one-binding"
|
|
"under its name:\n\n let v = a\n w = b";
|
|
reads ~global:false "a let-bound match" "let r = match a\n 1 -> 2\n _ -> 3\nr" "(let [r (match a 1 2 _ 3)] r)";
|
|
reads ~global:false "a let-bound if" "let q = if a\n 1\nelse\n 2\nq" "(let [q (if a 1 2)] q)";
|
|
reads ~global:false "a let-bound call with a block" "let v = foo(a):\n x\nv" "(let [v (foo a x)] v)";
|
|
refuses "colon after if" "if c:\n y" "indent/header-colon" "no colon";
|
|
refuses "colon after a return type" "fn f() -> i32:\n 0" "indent/header-colon" "no colon";
|
|
refuses "colon after a number" "while x < 3:\n y" "indent/header-colon" "no colon";
|
|
refuses "one-line handler-case" "handler-case g()" "indent/clause-header" "on Type(c)";
|
|
refuses "one-line on clause" "handler-case\n g()\non A(c) -> 1" "indent/clause-body" "on A(c)";
|
|
refuses "one-line elif" "x = if a then 1 elif b then 2 else 3" "indent/one-line-elif" "else if b";
|
|
refuses "elif with then" "if a\n 1\nelif b then 2" "indent/elif-then" "no then";
|
|
refuses "brace hint" "x = {.x a + 1 .y 2}" "indent/separate-elements" "{.x a + 1, .y 2}";
|
|
refuses "mixed separators" "x = [1 2, 3]" "indent/mixed-separators" "[1, 2, 3]";
|
|
reads "one-line quote" "defmacro(m, [x]):\n quote ~x + 1"
|
|
"(defmacro m [x] (quasiquote (+ (unquote x) 1)))";
|
|
reads ~global:false "typed let" "let x: i32 = 5\nx" "(let [x (the i32 5)] x)";
|
|
refuses "a let takes no block" "fn f() -> ()\n let x = 1\n g(x)\n h(x)"
|
|
"indent/let-block" "goes at the let's column";
|
|
(* Mistakes carried over from other languages, answered in this one. *)
|
|
refuses "a block without the colon names the call" "with-allocator(a, b)\n g()"
|
|
"indent/stray-indent" "as in with-allocator(a, b):";
|
|
refuses "field with =" "p = P{x = 1}" "indent/brace-field" "{.x value}";
|
|
refuses "field with a colon" "p = P{x: 1}" "indent/brace-field" "no colon";
|
|
refuses "a dotted range" "for i in 0..10\n g(i)" "indent/dot-range" "range(0, 10)";
|
|
(* A lambda's block inside brackets ends where they close. *)
|
|
reads "a block lambda inside a call" "sort-by(xs, fn(a, b) =>\n let c = a + 1\n c < b)"
|
|
"(sort-by xs (fn [a b] (let [c (+ a 1)] (< c b))))";
|
|
reads "its closer on a line of its own" "sort-by(xs, fn(a, b) =>\n a < b\n)\ng()"
|
|
"(sort-by xs (fn [a b] (< a b)))\n(g)";
|
|
reads "a typed block lambda inside a call" "sort-by(xs, fn(a: C, b: C) -> bool =>\n a < b)"
|
|
"(sort-by xs (the (Fn [C C] bool) (fn [a b] (< a b))))";
|
|
reads "nested block lambdas"
|
|
"map(xs, fn(x) =>\n let ys = map(x, fn(y) =>\n if y > 0\n y\n else\n 0)\n sum(ys))"
|
|
"(map xs (fn [x] (let [ys (map x (fn [y] (if (> y 0) y 0)))] (sum ys))))";
|
|
reads "a block lambda on a wrapped argument line" "f(a,\n fn(b) =>\n g(b)\n h(b))"
|
|
"(f a (fn [b] (g b) (h b)))";
|
|
reads "a block lambda in a vector" "x = [1, fn(b) =>\n b]" "(set x [1 (fn [b] b)])";
|
|
reads ~global:false "a call after a block lambda's call" "let v = f(fn(a) =>\n a)\ng(v)"
|
|
"(let [v (f (fn [a] a))] (g v))";
|
|
refuses "a block lambda is the last argument" "sort-by(fn(a, b) =>\n a < b, xs)"
|
|
"indent/lambda-block-last" "let f = fn(a) =>";
|
|
refuses "one block lambda to a call" "f(fn(a) =>\n a\n, fn(b) =>\n b)"
|
|
"indent/lambda-block-last" "on line 1";
|
|
refuses "a line back at the header's column" "f(fn(a) =>\n a\nb)"
|
|
"indent/lambda-block-last" "b follows the block of the lambda on line 1";
|
|
refuses "an element after a block lambda, left of its block" "m = {:a fn(x) =>\n x\n :b 2}"
|
|
"indent/lambda-block-last" ":b follows the block";
|
|
refuses "a block's first line not indented" "f(fn(a) =>\na)"
|
|
"indent/lambda-block-left" "Indent it into the block";
|
|
refuses "a block lambda's brackets left open" "f(fn(a) =>\n a\n"
|
|
"indent/unclosed" "ends where this bracket closes";
|
|
(* A let's bindings on the lines under it. *)
|
|
reads ~global:false "a let's bindings on indented lines" "let a = 1\n b = a + 1\n c: i32 = b\ng(c)"
|
|
"(let [a 1 b (+ a 1) c (the i32 b)] (g c))";
|
|
reads ~global:false "patterns among them" "let p = q()\n {x .x} = p\n [h & t] = xs\ng(x, h)"
|
|
"(let [p (q) {x .x} p [h & t] xs] (g x h))";
|
|
reads ~global:false "a block value last in a group" "let a = 1\n c = match a\n 1 -> 2\n _ -> 3\ng(c)"
|
|
"(let [a 1 c (match a 1 2 _ 3)] (g c))";
|
|
reads ~global:false "a let after a block value" "let a = 1\n b =\n f()\n a\nlet c = 2\ng(c)"
|
|
"(let [a 1 b (do (f) a) c 2] (g c))";
|
|
reads ~global:false "a let after a group merges into it" "let a = 1\n b = 2\nlet c = 3\ng(c)"
|
|
"(let [a 1 b 2 c 3] (g c))";
|
|
reads ~global:false "a group in a body" "fn f()\n let a = 1\n b = 2\n a + b"
|
|
"(defn f [] _ (let [a 1 b 2] (+ a b)))";
|
|
refuses ~global:false "a statement under a let" "let a = 1\n g(a)\nh()"
|
|
"indent/let-block" "b = a + 1";
|
|
refuses ~global:false "an assignment to a field under a let" "let a = p()\n a.x = 1\nh()"
|
|
"indent/let-block" "only lines that go there are more bindings";
|
|
refuses ~global:false "a compound assignment under a let" "let a = 1\n a += 1\nh()"
|
|
"indent/let-block" "let's column";
|
|
reads "a top-level group is several globals" "let a = 1\n b: i32 = 2\nfn f() = a"
|
|
"(def a dyn 1)\n(def b i32 2)\n(defn f [] _ a)";
|
|
refuses "a global group binds names" "let a = 1\n {x .x} = p"
|
|
"indent/global-pattern" "a global binds one name";
|
|
refuses "a statement under a global" "let a = 1\n f(a)"
|
|
"indent/let-block" "more bindings of the let";
|
|
reads "typed globals with no value in a group" "let a: i32\n b: i32\n c = 2"
|
|
"(def a i32)\n(def b i32)\n(def c dyn 2)";
|
|
reads "a group under a typed global with no value" "let a: i32 = 1\n b: i64"
|
|
"(def a i32 1)\n(def b i64)";
|
|
refuses ~global:false "a local binding with no value" "let a = 1\n b: i32\ng()"
|
|
"indent/let-block" "more bindings of the let";
|
|
(* A binding lines up with the let's first name. *)
|
|
refuses ~global:false "a binding right of the first name" "let x = 1\n y = 2\ng()"
|
|
"indent/let-align" "starts at column 7, under the let on line 1, whose bindings line up with its first name, x, at column 5. Move it to column 5:\n\n let x = 1\n y = 2";
|
|
refuses ~global:false "a binding left of the first name" "fn f()\n let x = 1\n y = 2\n z = 3\n g()"
|
|
"indent/let-align" "Move it to column 7";
|
|
refuses ~global:false "a binding deeper than the one above" "fn f()\n let x = 1\n y = 2\n z = 3\n g()"
|
|
"indent/let-align" "Move it to column 7";
|
|
refuses "a global binding out of line" "let x = 1\n y = 2"
|
|
"indent/let-align" "Move it to column 5";
|
|
(* A let whose value is a block takes no more bindings under it. *)
|
|
refuses "a binding after a lambda block" "fn f()\n let g = fn(x) =>\n x + 1\n y = 2\n g(y)"
|
|
"indent/let-after-block" "one more binding of the let on line 2, after g, whose value is the block above it";
|
|
refuses "a binding after a match's arms" "fn f(a)\n let g = match a\n 1 -> 2\n _ -> 3\n y = 2\n g"
|
|
"indent/let-after-block" "let of its own, at that let's column:\n\n let y = 2";
|
|
refuses "a binding after a deeper block" "fn f()\n let g =\n h()\n y = 2\n g"
|
|
"indent/let-after-block" "after g";
|
|
refuses "a binding after a later binding's block" "fn f()\n let a = 0\n g = fn(x) =>\n x\n h = 2\n h"
|
|
"indent/let-after-block" "one more binding of the let on line 2, after g, whose value is the block above it. A binding whose value is a block ends its let's bindings, so this one needs a let of its own, at that let's column:\n\n let h = 2";
|
|
(* A block lambda whose brackets close ends no group: its block is shut
|
|
when its value ends. *)
|
|
reads ~global:false "a binding after a later binding's bracketed lambda"
|
|
"let a = 1\n g = map(xs, fn(x) =>\n x + 1)\n h = 2\nh"
|
|
"(let [a 1 g (map xs (fn [x] (+ x 1))) h 2] h)";
|
|
reads ~global:false "a binding after a first binding's bracketed lambda"
|
|
"let g = map(xs, fn(x) =>\n x + 1)\n h = 2\nh"
|
|
"(let [g (map xs (fn [x] (+ x 1))) h 2] h)";
|
|
refuses "a binding after a later binding's match" "fn f()\n let a = 1\n b = match a\n 1 -> 2\n _ -> 3\n c = 4\n c"
|
|
"indent/let-after-block" "the let on line 2, after b";
|
|
refuses "a global after a global's block" "let a = 0\n g =\n h()\n b = 2"
|
|
"indent/let-after-block" "after g";
|
|
(* Only a line shaped as a binding gets the alignment advice. *)
|
|
refuses "a statement between a let and its lambda's block" "fn f()\n let g = fn(x) =>\n x + 1\n print(g)"
|
|
"indent/dedent" "belongs to neither";
|
|
refuses "an else between a let and its if's block" "fn f(c)\n let g = if c\n 1\n else\n 2\n g"
|
|
"indent/dedent" "belongs to neither";
|
|
refuses ~global:false "a statement deeper than a binding" "let x = 1\n y = 2\n g()\nh()"
|
|
"indent/let-block" "more bindings of the let";
|
|
(* A tab between let and its first name. *)
|
|
refuses "a tab after a local let" "fn f()\n let\ta = 1\n a" "indent/tab" "put a space there: let a";
|
|
refuses "a tab after a global let" "let \ta = 1" "indent/tab" "a tab between let and a";
|
|
(* Indices separate as a vector's elements do. *)
|
|
reads "indices separated by spaces" "x = grid[row col].color-idx"
|
|
"(set x (.color-idx (at grid row col)))";
|
|
reads "grouped indices separated by spaces" "x = grid[(r + 1) (c - 1)]"
|
|
"(set x (at grid (+ r 1) (- c 1)))";
|
|
reads "indices separated by commas" "x = grid[r + 1, c]" "(set x (at grid (+ r 1) c))";
|
|
reads "calls and fields as spaced indices" "x = grid[f(r) p.y]" "(set x (at grid (f r) (.y p)))";
|
|
reads "a negative literal first" "x = grid[-1 c]" "(set x (at grid -1 c))";
|
|
reads "a spaced index assigned" "grid[r c] = 1" "(set (at grid r c) 1)";
|
|
refuses "an operator among spaced indices" "x = grid[r + 1 c]"
|
|
"indent/separate-elements" "Separate the indices with commas: grid[r + 1, c]";
|
|
refuses "an operator last among spaced indices" "x = grid[r c - 1]"
|
|
"indent/separate-elements" "grid[r, c - 1]";
|
|
refuses "mixed index separators" "x = grid[r c, d]"
|
|
"indent/mixed-separators" "Use one: grid[r, c, d] or grid[r c d]";
|
|
refuses "a glued minus among indices" "x = grid[i -1]"
|
|
"indent/glued-minus" "grid[i - 1]";
|
|
refuses "a glued negation among indices" "x = grid[i -j]"
|
|
"indent/glued-minus" "grid[i, -j]";
|
|
refuses "arguments with no comma" "x = f(a b)"
|
|
"indent/missing-comma" "Separate arguments with commas: f(a, b)";
|
|
refuses "a body glued to =>" "x = fn(a) =>a"
|
|
"indent/lambda-arrow-space" "fn(a) => a";
|
|
refuses "a lambda written with =" "x = fn(a, b) = a + b"
|
|
"indent/lambda-equals" "fn(a, b) => a + b";
|
|
refuses "a typed lambda written with =" "x = fn(a: C) -> bool = a.n > 1"
|
|
"indent/lambda-equals" "fn(a: C) -> bool => a.n > 1";
|
|
refuses "a block lambda with no =>" "let f = fn(a)\n a\nf"
|
|
"indent/lambda-arrow" "fn(a) =>";
|
|
refuses "a block lambda with no => inside a call" "sort-by(xs, fn(a, b)\n a < b)"
|
|
"indent/lambda-arrow" "fn(a, b) =>";
|
|
refuses "a typed block lambda with no => inside a call" "sort-by(xs, fn(a: C, b: C) -> bool\n a < b)"
|
|
"indent/lambda-arrow" "fn(a: C, b: C) -> bool =>";
|
|
refuses "a => with nothing under it" "let f = fn(a) =>\nf"
|
|
"indent/lambda-body" "fn(a) =>";
|
|
refuses "an else after else-if on one line" "if a then x\nelse if b then y\nelse z"
|
|
"indent/orphan-else" "Write that line as elif";
|
|
refuses "else deeper than a one-line if" "if a then b\n else c"
|
|
"indent/else-column" "Put it at the if's column";
|
|
reads "a one-line else after an if with a block" "if a\n b()\n c()\nelse d()"
|
|
"(if a (do (b) (c)) (d))";
|
|
refuses "an unclosed call swallows the next line" "fn f() -> ()\n push(v, 1\n g()"
|
|
"indent/missing-comma" "If the ( on line 2 was meant to close";
|
|
reads "else on the line after a one-line if" "if a then b\nelse c" "(if a b c)";
|
|
reads "elif and else continuing a one-line if"
|
|
"if a then b\nelif c then d\nelif e\n f()\n g()\nelse\n h()"
|
|
"(cond a b c d e (do (f) (g)) :else (h))";
|
|
refuses "else left of a one-line if" "while x\n if a then b\nelse c"
|
|
"indent/orphan-else" "goes at the if's column";
|
|
reads "a typed lambda" "f = fn(a: C, b) -> bool => a.n < b"
|
|
"(set f (the (Fn [C dyn] bool) (fn [a b] (< (.n a) b))))";
|
|
reads ~global:false "a typed lambda with a block" "let f = fn(x: i32) -> i32 =>\n let y = x + 1\n y\ng(f)"
|
|
"(let [f (the (Fn [i32] i32) (fn [x] (let [y (+ x 1)] y)))] (g f))";
|
|
refuses "a typed lambda states its return type" "f = fn(a: C) = a"
|
|
"indent/lambda-return" "fn(a: C) -> R => value";
|
|
reads "a restart's report on its header"
|
|
"restart-case\n go()\nrestart retry(n: i32) \"Try again\"\n n"
|
|
"(restart-case (go) (retry [n i32] :report \"Try again\" n))";
|
|
reads "a bare () in a body slot does nothing"
|
|
"fn f() -> () = ()\nfn g(x) -> ()\n match x\n 1 -> h()\n _ -> ()\n k = fn() => ()"
|
|
"(defn f [] () (do))\n(defn g [x dyn] () (match x 1 (h) _ (do)) (set k (fn [] (do))))";
|
|
reads "a parenthesised () stays a value" "x = (())" "(set x ())";
|
|
reads "a template's for takes an unquoted variable"
|
|
"quote\n for ~i in range(~n)\n g(~i)"
|
|
"(quasiquote (dotimes [(unquote i) (unquote n)] (g (unquote i))))";
|
|
(* And back: the printer writes the idioms. *)
|
|
let prints name src want =
|
|
match Reader.read_all ~file:"<p>" src with
|
|
| forms ->
|
|
let got = Indent_printer.program ~source:src forms in
|
|
if not (Test_support.contains got want) then
|
|
fail "%s: printed %S, wanted it to contain %S" name got want
|
|
| exception e -> fail "%s: %s" name (diag_text e)
|
|
in
|
|
prints "compound assignment" "(defn f [] () (set x (+ x 1)))" " x += 1";
|
|
prints "not as a word" "(defn f [] () (g (not (= a b)) (and (not x) y)))"
|
|
"g(not a == b, not x and y)";
|
|
prints "not of a lower operator in parentheses" "(defn f [] () (g (not (or a b))))"
|
|
"g(not (a or b))";
|
|
prints "not under a comparison in parentheses" "(defn f [] () (g (= (not a) b)))"
|
|
"g((not a) == b)";
|
|
prints "compound update" "(defn f [] () (update (at a (next)) + 1))"
|
|
" a[next()] += 1";
|
|
prints "arm statements" "(defn f [] () (match s 1 (break) _ (return 2)))"
|
|
"1 -> break\n _ -> return 2";
|
|
prints "then and else statements" "(defn f [] () (if c (return 1) (set x 2)))"
|
|
"if c then return 1 else x = 2";
|
|
prints "a statement argument makes a block" "(foo 1 (set x 2))" "foo(1):\n x = 2";
|
|
prints "no arguments before the block" "(comment (f))" "comment:\n f()";
|
|
prints "typed let" "(defn f [] i32 (let [x (the i32 5)] x))" "let x: i32 = 5";
|
|
(* A let is always flat: it takes in the rest of its block. *)
|
|
prints "flat let" "(defn f [] () (let [j 1] (g j)) (h))" " let j = 1\n g(j)\n h()";
|
|
prints "a chain of lets, all flat" "(defn f [] () (let [a 1] (let [b 2] (g b)) (k a)) (h))"
|
|
" let a = 1\n b = 2\n g(b)\n k(a)\n h()";
|
|
(* A later statement that means an outer name of the same spelling: the
|
|
let's own is renamed. *)
|
|
prints "a later outer name of the same spelling renames the let's"
|
|
"(defn f [x i32] () (let [x 1] (g x)) (h x))" " let x-2 = 1\n g(x-2)\n h(x)";
|
|
prints "the inner let of a chain renamed"
|
|
"(defn f [] () (let [a 1] (let [b 2] (g b)) (h b)))" " let a = 1\n b-2 = 2\n g(b-2)\n h(b)";
|
|
prints "the binding's own value keeps the outer name"
|
|
"(defn f [x i32] () (let [x (+ x 1)] (g x)) (h x))" " let x-2 = x + 1\n g(x-2)\n h(x)";
|
|
prints "a later binding's value takes the new name"
|
|
"(defn f [x i32] () (let [x 1 y (+ x 1)] (g y)) (h x))"
|
|
" let x-2 = 1\n y = x-2 + 1\n g(y)\n h(x)";
|
|
prints "the new name is one the function does not use"
|
|
"(defn f [x i32] () (let [x 1] (g x x-2)) (h x))" " let x-3 = 1\n g(x-3, x-2)\n h(x)";
|
|
prints "a later let of the same name is no mention"
|
|
"(defn f [] () (let [a 1] (g a)) (let [a 2] (k a)))" " let a = 1\n g(a)\n let a = 2\n k(a)";
|
|
prints "unless its value uses the name"
|
|
"(defn f [a i32] () (let [a 1] (g a)) (let [a (+ a 1)] (k a)))"
|
|
" let a-2 = 1\n g(a-2)\n let a = a + 1\n k(a)";
|
|
prints "a destructured name renamed alone" "(defn f [] () (let [[p q] v] (g p q)) (h q))"
|
|
" let [p q-2] = v\n g(p, q-2)\n h(q)";
|
|
prints "a qualified name counts" "(defn f [] () (let [p (pt)] (g p)) (h p/x))"
|
|
" let p-2 = pt()\n g(p-2)\n h(p/x)";
|
|
prints "a quoted name later counts" "(defn f [] () (let [a 1] (g a)) (h 'a))"
|
|
" let a-2 = 1\n g(a-2)\n h('a)";
|
|
(* Where a rename cannot be trusted, a do: block holds the let. *)
|
|
prints "a quoted name inside is not renamed" "(defn f [] () (let [a 1] (g 'a)) (h a))"
|
|
" do:\n let a = 1\n g('a)\n h(a)";
|
|
prints "a call of the name inside is not renamed" "(defn f [] () (let [len 1] (len v)) (h len))"
|
|
" do:\n let len = 1\n len(v)\n h(len)";
|
|
(* A struct pattern renames as pairs, so the field keeps its name. *)
|
|
prints "a struct pattern renamed"
|
|
"(defn f [x i32] () (let [{.x .y} p] (g x y)) (h x))" " let {x-2 .x y .y} = p\n g(x-2, y)\n h(x)";
|
|
prints "a :keys pattern renamed"
|
|
"(defn f [x i32] () (let [{:keys [x y]} p] (g x y)) (h x))" " let {x-2 .x y .y} = p\n g(x-2, y)\n h(x)";
|
|
prints "a struct pattern that binds none of them stays"
|
|
"(defn f [x i32] () (let [{.y .z} p] (g y)) (h x))" " let {.y .z} = p\n g(y)\n h(x)";
|
|
prints "a later struct pattern rebinding the name is no mention"
|
|
"(defn f [] () (let [x 1] (g x)) (let [{.x} p] (k x)))" " let x = 1\n g(x)\n let {.x} = p\n k(x)";
|
|
prints "a struct literal inside is renamed"
|
|
"(defn f [x i32] () (let [x 1] (g (P {.x x}))) (h x))" " let x-2 = 1\n g(P{.x x-2})\n h(x)";
|
|
(* A macro whose body its definition splices into a do is a body run in
|
|
order; one that splices it anywhere else is not. *)
|
|
prints "a macro's in-order body"
|
|
"(defmacro twice [n & body] `(do ~@body ~@body))\n(defn f [] () (twice 2 (let [a 1] (g a)) (h)))"
|
|
" twice(2):\n let a = 1\n g(a)\n h()";
|
|
prints "a macro's list of arguments"
|
|
"(defmacro listed [& xs] `(list ~@xs))\n(defn f [] () (listed (let [a 1] (g a)) (set x 2)))"
|
|
" listed:\n do:\n let a = 1\n g(a)\n x = 2";
|
|
prints "comment is a body in order" "(comment (let [a 1] (g a)) (h))" "comment:\n let a = 1\n g(a)\n h()";
|
|
prints "a later lambda keeps the outer name"
|
|
"(defn f [] i32 (let [x 1] (let [x 5] (g x)) (app (fn [y] (+ x y)) 2)))"
|
|
" let x = 1\n x-2 = 5\n g(x-2)\n app(fn(y) => x + y, 2)";
|
|
prints "a renamed name renamed again counts on"
|
|
"(defn f [] () (let [x 1] (let [x 2] (let [x 3] (g x)) (g x)) (g x)))"
|
|
" let x = 1\n x-2 = 2\n x-3 = 3\n g(x-3)\n g(x-2)\n g(x)";
|
|
prints "a macro that names the let's name keeps its scope"
|
|
"(defmacro show-it [] `(println it))\n(defn f [] () (let [it 1] (let [it 2] (show-it)) (show-it)))"
|
|
" let it = 1\n do:\n let it = 2\n show-it()\n show-it()";
|
|
(* Which macros take a body run in order, read off their definitions. *)
|
|
let body name src want =
|
|
let t = Body_macros.table (Reader.read_all ~file:"<m>" src) in
|
|
let got = Hashtbl.find_opt t.Body_macros.bodies name in
|
|
if got <> want then
|
|
fail "%s: body at %s, wanted %s" name
|
|
(match got with Some k -> string_of_int k | None -> "none")
|
|
(match want with Some k -> string_of_int k | None -> "none")
|
|
in
|
|
body "twice" "(defmacro twice [n & b] `(do ~@b ~@b))" (Some 1);
|
|
body "tail" "(defmacro tail [& a] `(let [x ~(at a 0)] ~@(form-rest a 1)))" (Some 1);
|
|
body "nested" "(defmacro inner [& b] `(do ~@b))\n(defmacro nested [& b] `(inner ~@b))" (Some 0);
|
|
body "listed" "(defmacro listed [& b] `(list ~@b))" None;
|
|
body "vtwice" "(defmacro vtwice [& b] `(do ~@b (println (length [~@b]))))" None;
|
|
body "counted" "(defmacro counted [& b] (let [n (length b)] `(do ~n ~@b)))" None;
|
|
body "counts" "(defmacro counts [& b] (if (= (length b) 2) `(do) `(do ~@b)))" None;
|
|
body "guarded" "(defmacro guarded [& b] (if (< (length b) 1) `(do) `(do ~@b)))" (Some 0);
|
|
body "labelled" "(defmacro labelled [& b] `(while :l ~@b))" None;
|
|
body "labelled-test" "(defmacro labelled-test [& b] `(while :l true ~@b))" (Some 0);
|
|
body "reads-body" "(defmacro reads-body [& b] `(do ~(at b 0) ~@b))" None;
|
|
body "unless" "" (Some 1);
|
|
body "comment" "" (Some 0);
|
|
body "with-drawing"
|
|
"(defmacro with-drawing [& args]\n (if (or (< (length args) 1) (and (= (length args) 1) (is-form-empty-list (at args 0))))\n `(takes-a-body)\n `(do (begin) ~@args (end))))"
|
|
(Some 0);
|
|
prints "in a quasiquote" "(defmacro m [x] (quasiquote (do (let [a 1] (g a)) (h ~x))))"
|
|
" do:\n let a = 1\n g(a)\n h(~x)";
|
|
prints "among a call's arguments" "(foo 1 (let [a 1] (g a)) (set x 2))"
|
|
"foo(1):\n do:\n let a = 1\n g(a)\n x = 2";
|
|
prints "at the top level" "(let [a 1] (g a))\n(h)" "do:\n let a = 1\n g(a)\n\nh()";
|
|
prints "one-argument and" "(defn f [] () (while (and (< i n)) (g)))" " while i < n\n";
|
|
prints "one-argument or" "(defn f [] () (when (or c) (g)))" " if c\n";
|
|
prints "one-argument and in a quasiquote" "(defmacro m [x] (quasiquote (and ~x)))" "and(~x)";
|
|
prints "do in an arm is a block" "(defn f [] () (match s _ (do (a) (b))))" "_ ->\n a()";
|
|
prints "hex spelling" "(def c dyn 0xFFF00FFF)" "0xFFF00FFF";
|
|
prints "own-line comment above its form" "(defn f [] ()\n ;; why\n (g))" " ;; why\n g()";
|
|
prints "trailing comment at its line's end" "(defn f [] ()\n (g) ; note\n (h))" " g() ; note\n";
|
|
(* The other direction keeps them too. *)
|
|
let back name src want =
|
|
match Indent_reader.read_all ~file:"<b>" src with
|
|
| forms ->
|
|
let got = Paren_printer.program ~source:src forms in
|
|
if not (Test_support.contains got want) then
|
|
fail "%s: printed %S, wanted it to contain %S" name got want
|
|
| exception e -> fail "%s: %s" name (diag_text e)
|
|
in
|
|
back "spellings to parens" "fn main() -> i32\n println(0x1F, 1e3, 1_000, 3.0, 2.50, 0b101)\n 0"
|
|
"(println 0x1F 1e3 1_000 3.0 2.50 0b101)";
|
|
back "each binding keeps its comment"
|
|
"fn main() -> i32\n let a = 1 ; first\n let b = 2 ; second\n a + b"
|
|
"(let [a 1 ; first\n b 2] ; second";
|
|
prints "each binding keeps its comment, indented"
|
|
"(defn f [] i32\n (let [a 1 ; first\n b 2] ; second\n (+ a b)))"
|
|
" let a = 1 ; first\n b = 2 ; second";
|
|
prints "a comment line between grouped bindings"
|
|
"(defn f [] i32\n (let [a 1\n ;; why b\n b 2]\n (+ a b)))"
|
|
" let a = 1\n ;; why b\n b = 2\n";
|
|
prints "a long value starts a let of its own"
|
|
"(defn f [] i32 (let [a 1 b (some-function-with-a-long-name alpha beta gamma) c 2] (+ a b c)))"
|
|
" let a = 1\n let b = some-function-with-a-long-name(alpha, beta, gamma)\n let c = 2\n";
|
|
prints "a block value ends the group"
|
|
"(defn f [] i32 (let [a 1 b 2 c (do (g) a) d 4 e 5] (+ a b c d e)))"
|
|
" let a = 1\n b = 2\n let c =\n g()\n a\n let d = 4\n e = 5\n";
|
|
prints "a block value starts and ends a let of its own"
|
|
"(defn f [] i32 (let [a 0 b 1 g (fn [x] (p x) x) h 2 k 3] (+ a h k)))"
|
|
" let a = 0\n b = 1\n let g = fn(x) =>\n p(x)\n x\n let h = 2\n k = 3\n";
|
|
prints "typed and pattern bindings in a group"
|
|
"(defn f [p dyn] i32 (let [a (the i32 1) {x .x} p [h & t] xs] (+ a x h)))"
|
|
" let a: i32 = 1\n {x .x} = p\n [h & t] = xs\n";
|
|
back "comments to parens" "; head\n\nfn main() -> i32\n ; why\n g() ; note\n 0"
|
|
"; head\n\n(defn main [] i32\n ; why\n (g) ; note\n 0)";
|
|
(* Written the way the corpus writes them. *)
|
|
back "quasiquote as its reader sugar"
|
|
"defmacro(m, [x & body]):\n quote\n g(~x)\n ~@body"
|
|
"`(do (g ~x) ~@body)";
|
|
back "arms a pair to a line"
|
|
("fn f(s) -> dyn\n match s\n 1 -> \"one, a long string to break the line\"\n"
|
|
^ " _ -> \"another long string to push it over\"")
|
|
" (match s\n 1 \"one, a long string to break the line\"\n _ \"another";
|
|
back "a let's bindings hang after their names"
|
|
("fn f() -> dyn\n let a = compute-something-long(1, 2, 3, 4, 5)\n"
|
|
^ " let b = compute-something-long(5, 6, 7, 8, 9)\n a")
|
|
"(let [a (compute-something-long 1 2 3 4 5)\n b (compute-something-long 5 6 7 8 9)]";
|
|
back "a label stays with its test"
|
|
"fn f() -> ()\n while :outer is-some-long-condition(1, 2, 3) and is-another-long-one(4, 5, 6)\n g()"
|
|
"(while :outer";
|
|
back "a call's arguments fill the line"
|
|
"fn f() -> ()\n println(\"alpha\", \"beta\", \"gamma\", \"delta\", \"epsilon\", \"zeta\", \"eta\", \"theta\", \"iota\", g(1))"
|
|
"\"eta\" \"theta\" \"iota\"\n (g 1))";
|
|
back "a comment between a cond's test and its branch stays there"
|
|
"fn f(x) -> dyn\n if x\n ; why\n 1\n elif y\n 2\n else\n 3"
|
|
"; why";
|
|
prints "an if with no else keeps its test in the parentheses"
|
|
"(defn f [] () (if (> a 1) (let [k 2] (g k))))" " if(a > 1):\n let k = 2";
|
|
prints "and inside or keeps its parentheses"
|
|
"(defn f [a bool b bool c bool] bool (or (and a b) c))" "= (a and b) or c";
|
|
prints "a typed lambda prints as one"
|
|
"(defn f [] () (let [g (the (Fn [C] bool) (fn [c] (> (.n c) 3)))] (h g)))"
|
|
"let g = fn(c: C) -> bool => c.n > 3";
|
|
(* A lambda with a block as a call's last argument prints inside the call,
|
|
and reads back as it was. *)
|
|
let round name src want =
|
|
prints name src want;
|
|
let forms = Reader.read_all ~file:"<p>" src in
|
|
let text = Indent_printer.program ~source:src forms in
|
|
match Indent_reader.read_all ~file:"<p>" text with
|
|
| back ->
|
|
if not (same_forms forms back) then
|
|
fail "%s: read back %s from %S" name (describe_diff forms back) text
|
|
| exception e -> fail "%s: its text is refused: %s\n%s" name (diag_text e) text
|
|
in
|
|
round "a mixed chain" "(defn f [r i32 n i32] bool (and (<= 0 r) (< r n)))" "= 0 <= r < n";
|
|
round "an and of one operator stays an and"
|
|
"(defn f [r i32 n i32] bool (and (< 0 r) (< r n)))" "= 0 < r and r < n";
|
|
round "an and whose middles differ stays an and"
|
|
"(defn f [r i32 n i32] bool (and (<= 0 r) (< n 9)))" "= 0 <= r and n < 9";
|
|
round "a let the reader would not make stays a let"
|
|
"(defn f [a i32] bool (let [m (g)] (and (<= a m) (< m (h)))))" " let m = g()";
|
|
back "a chain's middle call gets a name paren text can spell"
|
|
"fn f(a, b) -> bool = a < g() <= b"
|
|
"(let [mid a mid2 (g) mid3 b] (and (< mid mid2) (<= mid2 mid3)))";
|
|
(* And the paren text prints as the chain again, up to the names. *)
|
|
let src = "(defn f [a i32 b i32] bool (let [mid a mid2 (g) mid3 b] (and (< mid mid2) (<= mid2 mid3))))" in
|
|
prints "a mixed chain over a call comes back a chain" src " a < g() <= b";
|
|
(let forms = Reader.read_all ~file:"<p>" src in
|
|
let back = Indent_reader.read_all ~file:"<p>" (Indent_printer.program ~source:src forms) in
|
|
if not (same_forms (List.map norm forms) (List.map norm back)) then
|
|
fail "a mixed chain over a call: read back %s" (describe_diff forms back));
|
|
round "bit operators print infix"
|
|
"(defn f [a i32 m i32] bool (= (bit-and a (bit-not m)) (bit-or (bit-xor a 1) (<< m 2))))"
|
|
"a && ~~m == a ^^ 1 || m << 2";
|
|
round "bit operators parenthesise against precedence"
|
|
"(defn f [a i32 b i32 c i32] i32 (bit-and (bit-or a b) (+ c 1) (bit-not (bit-xor a b))))"
|
|
"= (a || b) && c + 1 && ~~(a ^^ b)";
|
|
round "a nested unquote prints with parentheses"
|
|
"(defmacro m [x] `(defmacro n [] `(g ~~x ~(bit-not x))))" "~(~x)";
|
|
prints "the Lisp spellings print as the operators"
|
|
"(defn f [a i32 b i32] i32 (^^ (&& a b) (|| a b)))" "a && b ^^ (a || b)";
|
|
round "a one-line lambda""(defn f [] () (h (fn [a] (+ a 1)) 2))" "= h(fn(a) => a + 1, 2)";
|
|
round "a block lambda as a call's last argument"
|
|
"(defn f [] () (sort-by xs (fn [a b] (g a) (< a b))))"
|
|
" sort-by(xs, fn(a, b) =>\n g(a)\n a < b)";
|
|
round "one closer for a call in a call"
|
|
"(defn f [] () (println (run (fn [] (g) 1))))" " println(run(fn() =>\n g()\n 1))";
|
|
(* if let and when, read both ways and printed back. *)
|
|
round "if let with a block and an else"
|
|
"(defn f [o (Option i32)] i32 (if-let [(Some g) o] (do (println g) g) 0))"
|
|
" if let Some(g) = o\n println(g)\n g\n else\n 0";
|
|
round "if let on one line"
|
|
"(defn f [o (Option i32)] i32 (if-let [(Some g) o] g 0))"
|
|
" if let Some(g) = o then g else 0";
|
|
round "if let with elif"
|
|
"(defn f [o (Option i32) k i32] i32 (if-let [(Some g) o] (+ g 1) (cond (> k 5) 100 :else 0)))"
|
|
" elif k > 5\n 100\n else\n 0";
|
|
round "if let with no else" "(defn f [o (Option i32)] () (if-let [(Some g) o] (do (println g) (println g))))"
|
|
" if let Some(g) = o\n println(g)";
|
|
round "elif let"
|
|
"(defn f [a (Option i32) b (Option i32) k i32] i32 \
|
|
(if-let [(Some x) a] x (if-let [(Some y) b] (* y 10) (if (> k 5) 100 0))))"
|
|
" elif let Some(y) = b\n y * 10\n elif k > 5\n 100\n else\n 0";
|
|
round "elif let after a plain if"
|
|
"(defn f [a bool b (Option i32)] () \
|
|
(if a (println 1) (if-let [(Some y) b] (println y) (when (> 1 0) (println 2)))))"
|
|
" elif let Some(y) = b\n println(y)\n elif 1 > 0\n println(2)";
|
|
reads "elif let reads as a nested if-let"
|
|
"if let Some(x) = a\n f(x)\nelif let None = b\n g()\nelse\n h()"
|
|
"(if-let [(Some x) a] (f x) (if-let [None b] (g) (h)))";
|
|
round "a kept if let chain with no else"
|
|
"(defn f [a (Option i32) k i32] (Option i32) (if-let [(Some x) a] (do (g) x) (cond (> k 0) k)))"
|
|
" if let Some(x) = a\n g()\n x\n elif k > 0\n k";
|
|
round "a kept when" "(defn f [] () (let [w (when (> a 1) 2)] (g w)))"
|
|
" let w = when a > 1 then 2";
|
|
reads "when with a block" "when a\n b()\n c()" "(when a (b) (c))";
|
|
reads "when on one line" "when a then b()" "(when a (b))";
|
|
reads "if let with elif and no else" "if let None = o\n a()\nelif c\n b()"
|
|
"(if-let [None o] (a) (cond c (b)))";
|
|
refuses "when has no else" "when a then b() else c()" "indent/when-else" "a when has no else";
|
|
refuses "nor an else block" "when a\n b()\nelse\n c()" "indent/when-else" "a when has no else";
|
|
round "a typed block lambda in a call"
|
|
"(defn f [] () (h (the (Fn [C] bool) (fn [c] (g c) (> (.n c) 3)))))"
|
|
" h(fn(c: C) -> bool =>\n g(c)\n c.n > 3)";
|
|
round "a let's call with a block lambda"
|
|
"(defn f [] () (let [v (m xs (fn [x] (g x) x))] (h v)))"
|
|
" let v = m(xs, fn(x) =>\n g(x)\n x)\n h(v)";
|
|
round "nested block lambdas"
|
|
"(defn f [] () (m xs (fn [x] (let [y (m x (fn [z] (g z) z))] (h y)))))"
|
|
" m(xs, fn(x) =>\n let y = m(x, fn(z) =>\n g(z)\n z)\n h(y))";
|
|
round "a block lambda inside an expression, the line going on after it"
|
|
"(defn f [] i64 (+ (ap 1 (fn [x] (g x) x)) 1))" " ap(1, fn(x) =>\n g(x)\n x) + 1";
|
|
round "one in a lambda that fits on its line"
|
|
"(defn f [] i64 (ap 1 (fn [x] (ap x (fn [y] (g y) y)))))"
|
|
" ap(1, fn(x) =>\n ap(x, fn(y) =>\n g(y)\n y))";
|
|
round "a struct literal's last field"
|
|
"(defn f [] i64 (let [o (Ops {.k 1 .run (fn [x] (g x) x)})] (.k o)))"
|
|
" let o = Ops{.k 1, .run fn(x) =>\n g(x)\n x}";
|
|
round "a returned call"
|
|
"(defn f [] i64 (return (ap 2 (fn [x] (g x) x))))" " return ap(2, fn(x) =>\n g(x)\n x)";
|
|
round "a let in a lambda inside a call's argument"
|
|
"(defn f [] i64 (println (call0 (fn [] (let [n 0] (set n 1) n)))) 0)"
|
|
" println(call0(fn() =>\n let n = 0\n n = 1\n n))";
|
|
prints "a body that is one do goes straight under =>"
|
|
"(defn f [] i64 (call0 (fn [] (do (g 1) 2))))" " call0(fn() =>\n g(1)\n 2)";
|
|
round "a block lambda not last keeps the fallback"
|
|
"(defn f [] () (r (fn [a b] (g a) b) 0))" "= r(fn([a b], g(a), b), 0)";
|
|
round "a lambda bound by let"
|
|
"(defn f [] () (let [k (fn [a] (g a) a)] (k 1)))" " let k = fn(a) =>\n g(a)\n a\n k(1)";
|
|
prints "a restart's report goes on its header"
|
|
"(defn f [] i32 (restart-case (go) (retry [] :report \"Try again\" 7)))"
|
|
"restart retry() \"Try again\"\n 7";
|
|
prints "adjacent one-line globals stay adjacent"
|
|
"(defonce a i32)\n(def b i32 2)\n\n(defconst c 3)\n"
|
|
"once a: i32\nlet b: i32 = 2\n\nconst c = 3";
|
|
back "adjacent one-line globals stay adjacent in parens"
|
|
"once a: i32\nlet b: i32 = 2\n\nconst c = 3\n"
|
|
"(defonce a i32)\n(def b i32 2)\n\n(defconst c 3)";
|
|
prints "a field of a field chains" "(defn f [] () (g (.count (.x w))))" "g(w.x.count)";
|
|
prints "an else-if chain on one line"
|
|
"(defn f [r] dyn (if (> r 7) :rich (if (> r 4) :fair :poor)))"
|
|
"if r > 7 then :rich else if r > 4 then :fair else :poor";
|
|
prints "a long vector wraps" ("(defn f [] () (let [v [" ^ String.concat " " (List.init 30 string_of_int) ^ "]] (g v)))")
|
|
" let v = [0 1 2 3";
|
|
prints "a template's for keeps its unquotes"
|
|
"(defmacro m [i n & body] `(dotimes [~i ~n] ~@body))" "for ~i in range(~n)";
|
|
prints "a macro" "(defmacro m [[a b] n & body] `(do ~@body))" "macro m([a b], n, & body)\n quote";
|
|
prints "a header word assigned keeps no parentheses"
|
|
"(defn f [] () (set data 3) (set loop 4) (set on 5))" " data = 3\n loop = 4\n on = 5";
|
|
prints "a class" "(defclass point [x y])" "class point(x, y)";
|
|
prints "a class's typed slots, and one typed as a class of the file"
|
|
"(defclass state [pause bool tag])\n(defclass node [owner state n])"
|
|
"class state(pause: bool, tag)\n\nclass node(owner: state, n)";
|
|
prints "a generic" "(defgeneric area [self] dyn)" "generic area(self) -> dyn";
|
|
prints "a multi" "(defmulti kind [v] dyn (type-of v))" "multi kind(v) -> dyn = type-of(v)";
|
|
prints "a method on a class" "(defmethod area point [p] (g p) (h p))"
|
|
"method area(p: point)\n g(p)\n h(p)";
|
|
prints "a method on a value" "(defmethod kind :int [v] \"n\")" "method kind(v) when :int = \"n\"";
|
|
prints "a global is a top-level let" "(def g dyn 1)\n(def h i32 2)" "let g = 1\nlet h: i32 = 2";
|
|
prints "a def inside a form keeps the fallback" "(comment (def g i32 1))" "comment:\n def(g, i32, 1)";
|
|
prints "a local let at the top level goes in a do block" "(let [x 1] (f x))"
|
|
"do:\n let x = 1\n f(x)";
|
|
prints "a type alias" "(defalias Row (Vec i32))" "type Row = Vec(i32)";
|
|
prints "a struct with a parent" "(defstruct D :parent Io [free i64])"
|
|
"struct D(free: i64) :parent Io";
|
|
prints "a struct on one line" "(defstruct Pt [x i32 y dyn])" "struct Pt(x: i32, y)\n";
|
|
prints "a union too" "(defunion U [a i32 b f32])" "union U(a: i32, b: f32)";
|
|
prints "a struct too long for a line takes a line per field"
|
|
("(defstruct W [" ^ String.concat " " (List.init 8 (Printf.sprintf "field-number-%d i32")) ^ "])")
|
|
"struct W\n field-number-0: i32\n";
|
|
prints "and so does one with a comment among its fields"
|
|
"(defstruct C [a i32 ; first\n b i32])" "struct C\n a: i32 ; first\n b: i32";
|
|
prints "a parent with no fields" "(defstruct D :parent Io)" "struct D :parent Io";
|
|
prints "an empty field vector under a parent keeps the fallback"
|
|
"(defstruct D :parent Io [])" "defstruct(D, :parent, Io, [])";
|
|
(* A .flan file with loop or recur is refused, every line named. *)
|
|
(match
|
|
Reader.read_all ~file:"<p>"
|
|
"(defn f [a i32] i32\n (loop [x a y 0]\n (if (= x 0) y (recur (- x 1) (+ y 1)))))\n\n(defn g [] i32 (loop [i 0] i))"
|
|
with
|
|
| forms ->
|
|
(match Indent_printer.program forms with
|
|
| text -> fail "a loop printed: %s" text
|
|
| exception Loc.Error d ->
|
|
if d.Loc.kind <> "convert/no-loop" then fail "a loop refused as %s" d.Loc.kind;
|
|
List.iter
|
|
(fun n ->
|
|
if not (Test_support.contains d.Loc.dmsg n) then
|
|
fail "the loop refusal does not say %S: %s" n d.Loc.dmsg)
|
|
[ "on lines 2, 3 and 5. The indented syntax"; "(while (< i 10)" ];
|
|
if List.length d.Loc.notes <> 2 then
|
|
fail "the loop refusal points at %d more places, wanted 2" (List.length d.Loc.notes))
|
|
| exception e -> fail "a loop: %s" (diag_text e))
|
|
|
|
(* ── Spans, for pause marks and error overlays ──────────────────────── *)
|
|
|
|
let span_is name (f : Form.t) (l, c, el, ec) =
|
|
let g = f.loc in
|
|
if (g.Loc.line, g.Loc.col, g.Loc.eline, g.Loc.ecol) <> (l, c, el, ec) then
|
|
fail "%s spans %d:%d-%d:%d, wanted %d:%d-%d:%d" name g.Loc.line g.Loc.col
|
|
g.Loc.eline g.Loc.ecol l c el ec
|
|
|
|
let () =
|
|
(* A rewritten statement spans its text from the first token to the last,
|
|
so a mark or an overlay drawn from it covers what was written. *)
|
|
match read "x = a.b.c + b + c" with
|
|
| [ ({ v = Form.List [ _; _; ({ v = Form.List [ _; abc; _; _ ]; _ } as sum) ]; _ } as set) ] ->
|
|
span_is "x = ..." set (1, 1, 1, 18);
|
|
span_is "a.b.c + b + c" sum (1, 5, 1, 18);
|
|
span_is "a.b.c" abc (1, 5, 1, 10)
|
|
| _ -> fail "x = a.b.c + b + c read as another shape"
|
|
|
|
let () =
|
|
(* Editor code, placed where it was written: line 40, column 5, and the
|
|
indent stack seeded with that column, so the next line at column 5 is a
|
|
sibling rather than a dedent. *)
|
|
Source.with_code ~syntax:Source.Indented ~at:(Some (40, 5)) (fun () ->
|
|
match Source.read_code ~expr:true ~file:"<buf>" "f(1)\n g(2)" with
|
|
| [ ({ v = Form.List [ { v = Form.Sym "do"; _ }; a; b ]; _ } as d) ] ->
|
|
span_is "the snippet" d (40, 5, 41, 9);
|
|
span_is "its first line" a (40, 5, 40, 9);
|
|
span_is "its second line" b (41, 5, 41, 9)
|
|
| fs ->
|
|
fail "a two-line snippet read as %s"
|
|
(String.concat " " (List.map Form.to_string fs)));
|
|
(* A snippet's first line is its left edge: a later line left of it is
|
|
refused as that, and a snippet sent with leading spaces starts where its
|
|
first token does. *)
|
|
Source.with_code ~syntax:Source.Indented ~at:(Some (40, 5)) (fun () ->
|
|
(match Source.read_code ~file:"<buf>" "f(1)\n g(2)" with
|
|
| _ -> fail "a line left of the snippet's first was read"
|
|
| exception Loc.Error d ->
|
|
if not (Test_support.contains d.Loc.dmsg "left of column 5 where the code sent starts")
|
|
then fail "a line left of a snippet: %s" d.Loc.dmsg);
|
|
match Source.read_code ~file:"<buf>" " f(1)\n g(2)" with
|
|
| [ _; _ ] -> ()
|
|
| _ -> fail "a snippet with leading spaces"
|
|
| exception e -> fail "a snippet with leading spaces: %s" (diag_text e));
|
|
(* A condition cut out from after [elif ] at column 3: its wrapped line at
|
|
column 8 is deeper than the elif, which is what the file says, though not
|
|
deeper than the cut. [:indent] says where the statement starts; every
|
|
location stays the buffer's own. *)
|
|
Source.with_code ~indent:3 ~syntax:Source.Indented ~at:(Some (10, 8)) (fun () ->
|
|
(match Source.read_code ~expr:true ~file:"<buf>" "x == 0 or\n x == 1" with
|
|
| [ f ] -> span_is "a wrapped condition, cut mid-line" f (10, 8, 11, 14)
|
|
| _ -> fail "a wrapped condition read as more than one form"
|
|
| exception e -> fail "a wrapped condition: %s" (diag_text e));
|
|
match Source.read_code ~expr:true ~file:"<buf>" "x == 0 or\n x == 1" with
|
|
| _ -> fail "a continuation left of its statement was read"
|
|
| exception Loc.Error _ -> ());
|
|
Source.with_code ~syntax:Source.Indented ~at:(Some (10, 8)) (fun () ->
|
|
match Source.read_code ~expr:true ~file:"<buf>" "x == 0 or\n x == 1" with
|
|
| _ -> fail "without :indent, a wrapped line is measured from the cut"
|
|
| exception Loc.Error _ -> ());
|
|
Source.with_code ~syntax:Source.Paren ~at:(Some (7, 3)) (fun () ->
|
|
match Source.read_code ~file:"<buf>" "(f 1)" with
|
|
| [ f ] -> span_is "a paren snippet" f (7, 3, 7, 8)
|
|
| _ -> fail "a paren snippet")
|
|
|
|
(* ── Loading ───────────────────────────────────────────────────────── *)
|
|
|
|
let write path text = Out_channel.with_open_bin path (fun oc -> output_string oc text)
|
|
|
|
let () =
|
|
(* A spaced-out operator is one name; the checker says which arithmetic. *)
|
|
let f = Filename.concat scratch "syntax-hint.fln" in
|
|
write f "fn main() -> i32\n let x = 3\n x-1\n";
|
|
(match Front.checked f with
|
|
| _ -> fail "x-1 checked"
|
|
| exception Loc.Error d ->
|
|
if not (Test_support.contains d.Loc.dmsg "Did you mean x - 1?") then
|
|
fail "x-1: %s" d.Loc.dmsg
|
|
| exception e -> fail "x-1: %s" (Printexc.to_string e));
|
|
(* One package, one file in two syntaxes: refused naming both. *)
|
|
let dir = Filename.concat scratch "syntax-twin" in
|
|
let pkg = Filename.concat dir "geo" in
|
|
(try Unix.mkdir dir 0o755 with Unix.Unix_error _ -> ());
|
|
(try Unix.mkdir pkg 0o755 with Unix.Unix_error _ -> ());
|
|
write (Filename.concat pkg "geo.flan") "(defn one [] i32 1)\n";
|
|
write (Filename.concat pkg "geo.fln") "fn one() -> i32 = 1\n";
|
|
let main = Filename.concat dir "main.flan" in
|
|
write main "(import geo \"geo\")\n(defn main [] i32 (geo/one))\n";
|
|
match Front.checked main with
|
|
| _ -> fail "a package with geo.flan and geo.fln loaded"
|
|
| exception Loc.Error d ->
|
|
if not (Test_support.contains d.Loc.dmsg "geo.flan"
|
|
&& Test_support.contains d.Loc.dmsg "geo.fln") then
|
|
fail "twin files: %s" d.Loc.dmsg
|
|
| exception e -> fail "twin files: %s" (Printexc.to_string e)
|
|
|
|
(* ── A refusal's fix is spelled in the file's own syntax, and compiles ── *)
|
|
|
|
let refused name text needles =
|
|
let f = Filename.concat scratch name in
|
|
write f text;
|
|
match Front.checked f with
|
|
| _ -> fail "%s checked" name
|
|
| exception (Loc.Error d | Loc.Errors [ d ]) ->
|
|
List.iter
|
|
(fun n ->
|
|
if not (Test_support.contains d.Loc.dmsg n) then
|
|
fail "%s: wanted %S in: %s" name n d.Loc.dmsg)
|
|
needles
|
|
| exception e -> fail "%s: %s" name (diag_text e)
|
|
|
|
let checks name text =
|
|
let f = Filename.concat scratch name in
|
|
write f text;
|
|
match Front.checked f with
|
|
| _ -> ()
|
|
| exception e -> fail "%s does not check: %s" name (diag_text e)
|
|
|
|
(* Names carry no ? or !; the forms that read them instead. *)
|
|
let () =
|
|
refuses "a ? ends no name" "fn empty-cell?(c: i32) -> bool = c == 0"
|
|
"indent/question-name" "starts with is- or has- instead: is-empty-cell";
|
|
refuses "a ? in a call's name" "rl/key-pressed?(k)"
|
|
"indent/question-name" "rl/is-key-pressed";
|
|
refuses "a renamed prelude question" "starts-with?(a, b)"
|
|
"indent/question-name" "has-prefix";
|
|
refuses "a ? in a binding" "let ok? = 1" "indent/question-name" "is-ok";
|
|
refuses "a ! in a name" "set!(x)" "indent/mark-in-name" "Leave it out: set";
|
|
refuses "a ? inside a name" "a?b" "indent/mark-in-name" "cannot contain ?";
|
|
refuses "a glued ??" "x??y" "indent/unspaced-operator" "x ?? y";
|
|
refuses "a glued !=" "x!=y" "indent/unspaced-operator" "x != y";
|
|
reads "T? is Option(T)" "fn f(a: i32?, b: [i32?], c: Vec(Shape?), d: rl/Vector2?) -> $t? = a"
|
|
"(defn f [a (Option i32) b [(Option i32)] c (Vec (Option Shape)) d (Option rl/Vector2)] (Option $t) a)";
|
|
reads "?? is variadic and above the comparisons" "x = a ?? b ?? c == d + 1"
|
|
"(set x (= (?? a b c) (+ d 1)))";
|
|
reads "! unwraps, and a chain goes on after it" "x = a!.b + c!"
|
|
"(set x (+ (.b (!! a)) (!! c)))";
|
|
reads "?. reads the rest over a fresh name" "x = a?.b.c(1)?.d"
|
|
"(set x (?. [~o1 a] (?. [~o2 ((.c (.b ~o1)) 1)] (.d ~o2))))";
|
|
reads "?[ indexes" "x = f(a)?[2]" "(set x (?. [~o1 (f a)] (at ~o1 2)))";
|
|
(* Decision 133: x? tests, narrows a local, and e? as g names what it
|
|
found; if let over a plain name is refused toward those. *)
|
|
refuses "if let over a plain name" "if let g = x\n g" "indent/if-let-name"
|
|
"write if x?, and in the block it is what it holds; to name what it holds, \
|
|
write if x as g";
|
|
reads "x? is a test" "y = f(x)? and not z.w?" "(set y (and (? (f x)) (not (? (.w z)))))";
|
|
reads "e? as g" "if f(x)? as g\n g\nelif y? as h\n h\nelse\n 0"
|
|
"(cond (as g (f x)) g (as h y) h :else 0)";
|
|
reads "one-line e? as g" "v = if y? as h then h else 0" "(set v (if (as h y) h 0))";
|
|
reads "while e? as g" "while pop(s)? as x\n f(x)"
|
|
"(while true (if (as x (pop s)) (do (f x)) (break)))";
|
|
(* Decision 136: e as g without the ?, and inside an and chain. *)
|
|
reads "e as g" "if f(x) as g\n g" "(when (as g (f x)) g)";
|
|
reads "as in an and chain" "if a and f(x) as g and g > 1 and b\n g"
|
|
"(when (and a (as g (f x)) (> g 1) b) g)";
|
|
reads "two as in one chain" "v = if a as x and b? as y and x < y then x else y"
|
|
"(set v (if (and (as x a) (as y b) (< x y)) x y))";
|
|
reads "a kept when with as" "left = when get(grid, r + 1, c - 1) as g and is-empty-cell(g) then g"
|
|
"(set left (when (and (as g (get grid (+ r 1) (- c 1))) (is-empty-cell g)) g))";
|
|
reads "while as and" "while pop(s) as x and x > 0\n f(x)"
|
|
"(while true (if (and (as x (pop s)) (> x 0)) (do (f x)) (break)))";
|
|
reads "elif as and" "if a\n 1\nelif f(x) as g and g > 1\n g"
|
|
"(cond a 1 (and (as g (f x)) (> g 1)) g)";
|
|
refuses "as under or" "if a or f(x) as g\n g" "indent/as-or" "Bind with as in an if of its own";
|
|
refuses "or after as" "if f(x) as g or b\n g" "indent/as-or" "nothing to name";
|
|
refuses "or later in the chain" "if f(x) as g and a or b\n g" "indent/as-or" "nothing to name";
|
|
refuses "as under not" "if not f(x) as g\n g" "indent/as-not" "not turns the test around";
|
|
refuses "as in parentheses under not" "if not (a as g)\n g" "indent/as-paren"
|
|
"not inside parentheses";
|
|
refuses "as in a bracketed chain" "if (a as g and g > 1) and b\n g" "indent/as-paren"
|
|
"if a as g and";
|
|
refuses "as after until" "until f(x) as g\n g" "indent/as-until" "Write while";
|
|
refused "as-not-optional.fln" "fn main()\n let n = 5\n if n as g and g > 1\n println(g)\n"
|
|
[ "n is i32, which always holds a value, so as has nothing to test";
|
|
"It is not a conversion" ];
|
|
refused "as-not-in-else.fln"
|
|
"fn f(o: i32?) -> i32\n if o as g and g > 1\n g\n else\n g\n\nfn main()\n println(f(None))\n"
|
|
[ "unknown name g" ];
|
|
refused "as-not-in-elif.fln"
|
|
"fn f(o: i32?) -> i32\n if o as g and g > 1\n g\n elif g > 0\n 1\n else\n 0\n\nfn main()\n println(f(None))\n"
|
|
[ "unknown name g" ];
|
|
refused "as-not-after.fln"
|
|
"fn f(o: i32?) -> i32\n if o as g and g > 1\n println(g)\n g\n\nfn main()\n println(f(None))\n"
|
|
[ "unknown name g" ];
|
|
(* Decision 137: x |> f(a) is f(x, a), below or. *)
|
|
reads "|> into a call" "y = x |> f(1, 2)" "(set y (f x 1 2))";
|
|
reads "|> into an empty call" "y = x |> f()" "(set y (f x))";
|
|
reads "|> into a name" "y = x |> f" "(set y (f x))";
|
|
reads "|> into a case" "y = x |> Some" "(set y (Some x))";
|
|
reads "|> chains left to right"
|
|
"y = get(grid, r, c) |> or-else(empty) |> is-empty-cell()"
|
|
"(set y (is-empty-cell (or-else (get grid r c) empty)))";
|
|
reads "|> into a qualified name" "y = x |> m/f(a) |> m/g" "(set y (m/g (m/f x a)))";
|
|
reads "|> is below arithmetic" "y = a + 1 |> f()" "(set y (f (+ a 1)))";
|
|
reads "|> is below ??" "y = a ?? b |> f()" "(set y (f (?? a b)))";
|
|
reads "|> is below comparisons" "y = a == b |> f()" "(set y (f (= a b)))";
|
|
reads "|> is below not" "y = not x |> f()" "(set y (f (not x)))";
|
|
reads "|> is below or" "y = a or b and c |> f()" "(set y (f (or a (and b c))))";
|
|
reads "|> in an argument" "g(x |> f, 2)" "(g (f x) 2)";
|
|
reads "|> in a condition" "if x |> is-empty() then 1 else 2" "(if (is-empty x) 1 2)";
|
|
reads "|> starting lines" "let y = get(g, r)\n |> or-else(0)\n |> f()\ny"
|
|
"(def y dyn (f (or-else (get g r) 0)))\ny";
|
|
reads "|> ending a line" "let y = get(g, r) |>\n or-else(0)\ny"
|
|
"(def y dyn (or-else (get g r) 0))\ny";
|
|
reads "|> under a statement" "x\n |> f(1)\n |> g" "(g (f x 1))";
|
|
reads "|> into a call with a block" "xs |> each(1):\n print(2)" "(each xs 1 (print 2))";
|
|
reads "|>( is a call" "y = |>(a, b)" "(set y (|> a b))";
|
|
refuses "|> into a number" "y = x |> 3" "indent/pipe-target" "3 is neither";
|
|
refuses "|> into arithmetic" "y = x |> a + b" "indent/pipe-target" "\n (x |> a) + b";
|
|
refuses "|> into a field" "y = x |> a.b" "indent/pipe-target" "(x |> a).b";
|
|
refuses "|> into a field of a call" "y = x |> f(a).x" "indent/pipe-target" "f(a).x";
|
|
refuses "|> into a call's result" "y = x |> f(a)(b)" "indent/pipe-target" "f(a)(b)";
|
|
refuses "|> into an index" "y = x |> a[1]" "indent/pipe-target" "a[1]";
|
|
refuses "|> into a lambda" "y = x |> fn(a) => g(a)" "indent/pipe-keyword" "fn is a word";
|
|
refuses "|> into if" "y = 1 |> if" "indent/pipe-keyword" "if is a word";
|
|
refuses "|> into let" "y = 1 |> let" "indent/pipe-keyword" "let is a word";
|
|
refuses "|> into fn" "y = 1 |> fn" "indent/pipe-keyword" "fn is a word";
|
|
reads "|> into not" "y = x |> not" "(set y (not x))";
|
|
refuses "|> before ??" "y = a |> f ?? d" "indent/pipe-target" "\n (a |> f) ?? d";
|
|
refuses "|> before or" "y = a |> f or c" "indent/pipe-target" "\n (a |> f) or c";
|
|
refuses "|> before a test" "y = a |> f?" "indent/pipe-target" "\n (a |> f)?";
|
|
refuses "|> chained before ==" "y = a |> b |> f(1) == 2" "indent/pipe-target"
|
|
"\n (a |> b |> f(1)) == 2";
|
|
reads "as names what a pipe answers" "if a |> f(1) as v and v > 0\n v"
|
|
"(when (and (as v (f a 1)) (> v 0)) v)";
|
|
refuses "as before a pattern" "if a |> f as Some(v)\n v" "indent/as-name"
|
|
"if let Some(v) = a |> f";
|
|
reads "|> into a macro passes the place" "y = x |> set(5)" "(set y (set x 5))";
|
|
refuses "|> into parentheses" "y = x |> (f)" "indent/pipe-target" "(f) is neither";
|
|
refuses "|> unspaced on the right" "y = x |>[f]" "indent/unspaced-operator" "x |> f(a)";
|
|
refuses "|> at a statement's column" "f(x)\n|> g()" "indent/continuation" "starts with the operator |>";
|
|
refuses "|> in a list separated by spaces" "y = [x |> f() 2]" "indent/separate-elements" "commas";
|
|
(match read "y = |>(a, b)" with
|
|
| forms ->
|
|
let got = Indent_printer.program ~source:"y = |>(a, b)" forms in
|
|
if not (Test_support.contains got "|>(a, b)") then
|
|
fail "a call to |> prints as a call: %S" got
|
|
| exception e -> fail "a call to |> prints as a call: %s" (diag_text e));
|
|
refused "present-i32.fln" "fn main()\n let x = 5\n println(x?)\n"
|
|
[ "x is i32, which always holds a value, so x? has nothing to test";
|
|
"a yes-or-no name starts with is- or has-, as in is-x" ];
|
|
refused "narrowed-set.fln"
|
|
"fn main()\n let x: i32? = Some(1)\n if x?\n x = None\n println(x ?? 0)\n"
|
|
[ "x is tested with x? above, so in this block it is i32";
|
|
"while x as item" ];
|
|
refused "not-narrowed-in-else.fln"
|
|
"fn main()\n let x: i32? = None\n if x?\n println(x + 1)\n else\n println(x + 1)\n"
|
|
[ "Option(i32)" ];
|
|
refused "not-narrowed-through-or.fln"
|
|
"fn main()\n let x: i32? = None\n if x? or true\n println(x + 1)\n"
|
|
[ "Option(i32)" ];
|
|
refused "coalesce-i32.fln"
|
|
"fn main()\n let x = 5\n println(x ?? 1)\n"
|
|
[ "the left side of ?? is i32, which always holds a value" ];
|
|
(* After review: ?? continues a line, a chain is no place, nested marks
|
|
and a ? on a value say what to write, and a lowercase type takes ?. *)
|
|
reads "a line ending in ?? continues" "x = a ??\n 5" "(set x (?? a 5))";
|
|
reads "a line starting with ?? continues" "x = a\n ?? 5" "(set x (?? a 5))";
|
|
refuses "a chain is not a place" "q?.x = 5" "indent/chain-assign" "cannot be assigned to";
|
|
refuses "nor under +=" "q?.x += 5" "indent/chain-assign" "cannot be assigned to";
|
|
refuses "an unwrap is not a place" "x! = 5" "indent/chain-assign" "a value cannot be assigned to";
|
|
refuses "T?? is not read" "fn f(a: i32??) = a" "indent/nested-option" "Option(i32?)";
|
|
refuses "x!! is not read" "y = x!!" "indent/double-unwrap" "(x!)!";
|
|
refuses "!= with its space missing names what follows" "y = x!= z"
|
|
"indent/unspaced-operator" "x != z";
|
|
refuses "? on a parameter's name" "fn f(ok?: bool) = ok" "indent/question-name" "is-ok";
|
|
reads "a lowercase type takes ?" "fn f(a: grain?, b: [grain?]) -> grain? = a"
|
|
"(defn f [a (Option grain) b [(Option grain)]] (Option grain) a)";
|
|
refused "option-value.fln" "fn main()\n let y = i32?\n"
|
|
[ "i32? is an Option type, and a value is wanted here" ];
|
|
refused "where-fln.fln"
|
|
"fn big(a: $t, b: $t) -> bool = a < b\n\nfn main()\n println(big(1, 2))\n"
|
|
[ "nothing declares $t ordered (is-ordered)"; "Write where is-ordered($t)" ];
|
|
(* The second review: assignability holds in a narrowed block, a trailing ?
|
|
tests a whole chain, and the messages name what to write. *)
|
|
reads "a trailing ? tests the whole chain" "y = o?.i?"
|
|
"(set y (? (?. [~o1 o] (.i ~o1))))";
|
|
reads "and ? then as binds the chain's result" "if d?.k? as k\n k"
|
|
"(when (as k (?. [~o1 d] (.k ~o1))) k)";
|
|
refused "narrowed-param.fln"
|
|
"fn f(x: i32?)\n if x?\n x += 100\n\nfn main()\n f(Some(1))\n"
|
|
[ "x is a parameter, and a parameter is not assignable" ];
|
|
refused "narrowed-field.fln"
|
|
"fn main()\n let x: i32? = Some(1)\n if x?\n x.n = 1\n"
|
|
[ "so here it is what the Option holds, i32, and i32 has no fields" ];
|
|
refuses "a chain is no place, and the fix is a test" "q?.x = 5" "indent/chain-assign"
|
|
"Test it first: if q?, and in the block q is what it holds, or if q as g";
|
|
refused "lowercase-type-arg.fln"
|
|
"struct grain\n w: i32\n\nfn main()\n let v = vec-new(grain?)\n"
|
|
[ "grain? here is the test that a value is present, and grain is a type";
|
|
"vec-new(Option(grain))" ];
|
|
refused "addr-taken.fln"
|
|
"fn clear(p: Ptr(i32?))\n deref(p) = None\n\nfn main()\n let x: i32? = Some(1)\n\
|
|
\ let p = addr(x)\n if x?\n clear(p)\n println(x + 1)\n"
|
|
[ "+ takes numbers, found Option(i32)" ];
|
|
refused "capital-local.fln" "fn main()\n let X: i32? = Some(1)\n println(X?)\n"
|
|
[ "To test the local X, give it a lowercase name, as in x?" ];
|
|
checks "addr-taken-test.fln"
|
|
"fn main()\n let x: i32? = Some(1)\n let p = addr(x)\n if x?\n println(x ?? 0)\n\
|
|
\ while x?\n x = None\n";
|
|
refused "chain-i32.fln"
|
|
"struct P\n x: i32\n\nfn main()\n let p = P{.x 1}\n println(p?.x)\n"
|
|
[ "?. has nothing to test. Write . instead" ]
|
|
|
|
(* A kept if-let chain whose arm gives no value is a statement, refused as a
|
|
plain if's is; one whose arm returns stays Never, in both syntaxes. *)
|
|
let () =
|
|
refused "if-let-unit.flan"
|
|
"(defn main [] () (let [a (Some 1) r (if-let [(Some x) a] (println x) \
|
|
(when true (println 2)))] (println r)))\n"
|
|
[ "r would be bound to (), which is not a value" ];
|
|
refused "if-let-unit.fln"
|
|
"fn main()\n let a = Some(1)\n let r = if let Some(x) = a then println(x) \
|
|
else when true then println(2)\n println(r)\n"
|
|
[ "r would be bound to (), which is not a value" ];
|
|
checks "if-let-never.flan"
|
|
"(defn f [a (Option i32)] (Option i32) (if-let [(Some x) a] (return None) \
|
|
(when true 3)))\n(defn main [] () (println (f None)))\n";
|
|
checks "if-let-never.fln"
|
|
"fn f(a: Option(i32)) -> Option(i32)\n if let Some(x) = a\n return None\n \
|
|
elif true\n 3\n\nfn main()\n println(f(None))\n"
|
|
|
|
let () =
|
|
let poke_fln = "fn poke(coll) -> dyn\n coll[0] = 99\n coll\n\n" in
|
|
let poke_flan = "(defn poke [coll] dyn (set (at coll 0) 99) coll)\n" in
|
|
(* A container of pointers has no dyn view, and the refusal's subject and
|
|
fix follow what the container is: a local, a temporary, a parameter. *)
|
|
refused "view-local.fln"
|
|
(poke_fln ^ "fn main() -> ()\n let a = vec-new(Ptr(i64))\n poke(a)\n")
|
|
[ "a is a Vec(Ptr(i64)), and a dyn value is wanted here";
|
|
"a Ptr(i64) is none of these";
|
|
"as in let a: dyn = [...]" ];
|
|
refused "view-local.flan"
|
|
(poke_flan ^ "(defn main [] () (let [a (vec-new (Ptr i64))] (poke a)))\n")
|
|
[ "a is a (Vec (Ptr i64))"; "as in (let [a (the dyn [...])] ...)" ];
|
|
refused "view-temp.flan"
|
|
(poke_flan ^ "(defn main [] () (poke (vec-new (Ptr i64))))\n")
|
|
[ "This is a (Vec (Ptr i64))"; "as in (the dyn [...])" ];
|
|
(* Any storage and any number crosses: a local [4 i32] is a view. *)
|
|
checks "view-elem.fln"
|
|
(poke_fln ^ "fn main() -> ()\n let d = [6 2 4 9]\n poke(d)\n");
|
|
(* A parameter is made by the caller, so its fix is its declaration. *)
|
|
refused "view-param.fln"
|
|
"fn take(d) -> i32 = 1\n\nfn give(n: i32, v: [Ptr(i64)]) -> i32\n take(v)\n\n\
|
|
fn main() -> i32 = 0\n"
|
|
[ "v is a [Ptr(i64)] parameter"; "Declare v as dyn in give's parameters: v: dyn" ];
|
|
refused "view-param.flan"
|
|
"(defn take [d dyn] i32 1)\n(defn give [n i32 v (Vec (Ptr i64))] i32 (take v))\n\
|
|
(defn main [] i32 0)\n"
|
|
[ "v is a (Vec (Ptr i64)) parameter"; "Declare v as dyn in give's parameters: v dyn" ];
|
|
checks "view-param-fix.fln"
|
|
"fn take(d) -> i32 = 1\n\nfn give(n: i32, v: dyn) -> i32\n take(v)\n\n\
|
|
fn main() -> i32 = 0\n";
|
|
(* A global's fix redefines it, in the form it was defined with. *)
|
|
let show_flan = "(defn show [d dyn] i32 1)\n" in
|
|
refused "view-global.flan"
|
|
("(defonce gs (Vec (Ptr i32)) (vec-new (Ptr i32)))\n" ^ show_flan
|
|
^ "(defn main [] i32 (show gs))\n")
|
|
[ "gs is a (Vec (Ptr i32))"; "as in (defonce gs dyn [...])" ];
|
|
refused "view-global-def.flan"
|
|
("(def gs (Vec (Ptr i32)) (vec-new (Ptr i32)))\n" ^ show_flan
|
|
^ "(defn main [] i32 (show gs))\n")
|
|
[ "as in (def gs dyn [...])" ];
|
|
refused "view-global.fln"
|
|
"once gs: Vec(Ptr(i32)) = vec-new(Ptr(i32))\n\nfn show(d) -> i32 = 1\n\n\
|
|
fn main() -> i32 = show(gs)\n"
|
|
[ "gs is a Vec(Ptr(i32))"; "as in once gs: dyn = [...]" ];
|
|
checks "view-global-typed.flan"
|
|
("(defonce gs [2 i32] [1 2])\n" ^ show_flan ^ "(defn main [] i32 (show gs))\n");
|
|
(* A dyn global's initialiser cannot view what it builds itself. *)
|
|
refused "view-global-init.fln"
|
|
"once xs = array-fill([3], i64(1))\n\nfn main() -> i32 = 0\n"
|
|
[ "xs is a dyn global"; "as in once xs: [3 i64] = ..." ];
|
|
checks "view-global-fix.flan"
|
|
("(defonce gs dyn [1 2])\n(def hs dyn [1 2])\n" ^ show_flan
|
|
^ "(defn main [] i32 (show gs) (show hs))\n");
|
|
checks "view-global-fix.fln"
|
|
"once gs: dyn = [1 2]\n\nfn show(d) -> i32 = 1\n\nfn main() -> i32 = show(gs)\n";
|
|
(* The fix is spelled in the syntax the code was sent in, not the one the
|
|
file's name implies: an editor request from an indented buffer. *)
|
|
Source.with_code ~syntax:Source.Indented ~at:None (fun () ->
|
|
refused "unit-tail-request.flan"
|
|
"(defn f [coll] dyn (let [i 1] (while (< i 3) (++ i))))\n(defn main [] () (f 1))\n"
|
|
[ "fn f(...) -> ()" ]);
|
|
(* The fix both of them name. *)
|
|
checks "view-fix.fln"
|
|
(poke_fln ^ "fn main() -> ()\n let d: dyn = [6 2 4 9]\n poke(d)\n poke(the(dyn, [1 2]))\n");
|
|
checks "view-fix.flan"
|
|
(poke_flan
|
|
^ "(defn main [] () (let [a (the dyn [6 2 4 9])] (poke a)) (poke (the dyn [1 2])))\n");
|
|
(* A dyn function whose body ends in a while gives no value. *)
|
|
let loop_fln ret tail =
|
|
"fn f(coll) -> " ^ ret ^ "\n let i = 1\n while i < 3\n ++(i)\n" ^ tail
|
|
^ "\nfn main() -> ()\n f(1)\n"
|
|
in
|
|
refused "unit-tail.fln" (loop_fln "dyn" "")
|
|
[ "f is declared to return dyn, but the last form of its body gives no value";
|
|
"fn f(...) -> ()" ];
|
|
refused "unit-tail.flan"
|
|
"(defn f [coll] dyn (let [i 1] (while (< i 3) (++ i))))\n(defn main [] () (f 1))\n"
|
|
[ "f is declared to return dyn"; "(defn f [...] () ...)" ];
|
|
checks "unit-tail-nil.fln" (loop_fln "dyn" " nil\n");
|
|
checks "unit-tail-unit.fln" (loop_fln "()" "");
|
|
(* A unit argument deeper in the last form is about that argument. *)
|
|
refused "unit-arg.flan"
|
|
"(defn g [x dyn] dyn x)\n(defn f [coll] dyn (g (println 1)))\n(defn main [] () (f 1))\n"
|
|
[ "() does not box into dyn" ];
|
|
(* Checker messages about a .fln file are in its spelling. *)
|
|
refused "enum-none.fln" "fn main() -> i32\n let n = None\n 0\n" [ "the(Option(i32), None)" ];
|
|
checks "enum-annotated.fln"
|
|
"enum Dir\n north\n south\n\nfn main() -> i32\n let d: Dir = :north\n i32(d)\n";
|
|
refused "annotation-dyn.fln" "fn main() -> i32\n let d = the(dyn, 3)\n let x: i32 = d\n x\n"
|
|
[ "a type annotation checks a value as i32"; "write i32(d)" ];
|
|
refused "narrowing.fln" "fn f() -> i64 = 1\n\nfn main() -> i32\n let x: i32 = 0\n x = f()\n x\n"
|
|
[ "it has to be written: i32(x)" ];
|
|
refused "unknown-type.fln" "fn f(p: Keyword) -> i32 = 0\n\nfn main() -> i32 = 0\n"
|
|
[ "unknown type Keyword" ];
|
|
refused "untyped-lambda.fln" "fn main() -> i32\n let f = fn(a) =>\n a\n 0\n"
|
|
[ "let f: Fn(T, ...) -> R = fn(...) =>" ];
|
|
refused "plusplus.fln" "fn main() -> i32\n let x = 1\n x++\n x\n"
|
|
[ "write ++(x) or x += 1" ];
|
|
refused "plusplus-global.fln" "once g = 0\n\nfn main() -> i32\n g--\n 0\n"
|
|
[ "write --(g) or g -= 1" ];
|
|
(* Types in a message are in the syntax of the code it is about. *)
|
|
refused "types.fln" "fn g(x: Option(i32)) -> i32 = 0\n\nfn main() -> i32\n let v = vec-new(i32)\n g(v)\n"
|
|
[ "expected Option(i32), found Vec(i32)" ];
|
|
refused "types.flan" "(defn g [x (Option i32)] i32 0)\n(defn main [] i32 (let [v (vec-new i32)] (g v)))\n"
|
|
[ "expected (Option i32), found (Vec i32)" ];
|
|
refused "fn-field.fln" "struct R\n f: Fn(i32) -> bool\n\nfn main() -> i32 = 0\n"
|
|
[ "cannot be Fn(i32) -> bool"; "store a CFn(i32) -> bool" ];
|
|
refused "generic-struct.fln"
|
|
"struct Small\n items: [$n $t]\n\nfn main() -> i32\n let s: Small(4, i32) = zeroed()\n let q: i32 = s\n 0\n"
|
|
[ "found Small(4, i32)" ];
|
|
(* Dir.north is the member :north. *)
|
|
checks "enum-qualified.fln"
|
|
("enum Dir\n north\n south\n\nfn name(d: Dir) -> i32\n match d\n Dir.north -> 1\n :south -> 2\n\n"
|
|
^ "fn main() -> i32\n let d = Dir.north\n let e: Dir = Dir.south\n name(d) + name(e)\n");
|
|
refused "enum-qualified-miss.fln"
|
|
"enum Dir\n north\n\nfn f(d: Dir) -> i32\n match d\n Dir.west -> 1\n _ -> 0\n\nfn main() -> i32 = 0\n"
|
|
[ "Dir has no member west — it has Dir.north" ];
|
|
(* A bad key type is one error, not three. *)
|
|
refused "map-key.fln" "fn main() -> i32\n let m = map-new([const u8], i32)\n 0\n"
|
|
[ "[const u8] is not a map key" ];
|
|
(match Front.checked (Filename.concat scratch "map-key.fln") with
|
|
| exception Loc.Errors (_ :: _ :: _ as ds) ->
|
|
fail "map-key.fln: %d errors, wanted one" (List.length ds)
|
|
| exception _ -> ()
|
|
| _ -> ());
|
|
(* The fix a block lambda with no => is shown compiles, inside the call. *)
|
|
(match read "sort-by(xs, fn(a, b)\n a.n < b.n)" with
|
|
| _ -> fail "lambda in brackets: read"
|
|
| exception Loc.Error d ->
|
|
let header =
|
|
let m = d.Loc.dmsg in
|
|
let i = String.index m '\n' + 6 in
|
|
String.sub m i (String.index_from m i '\n' - i)
|
|
in
|
|
checks "lambda-fix.fln"
|
|
("struct C\n n: i32\n\nfn main() -> i32\n let xs = [C{.n 2} C{.n 1}]\n"
|
|
^ " sort-by(slice(xs), " ^ header ^ "\n a.n < b.n)\n xs[0].n\n"));
|
|
(* A typed block lambda inside a call, a closer on its own line, and one
|
|
nested in another's block. *)
|
|
checks "block-lambdas.fln"
|
|
("struct C\n n: i32\n\nfn app(x: i64, f: Fn(i64) -> i64) -> i64 = f(x)\n\n"
|
|
^ "fn main() -> i32\n let xs = [C{.n 2} C{.n 1}]\n"
|
|
^ " sort-by(slice(xs), fn(a: C, b: C) -> bool =>\n let d = a.n - b.n\n d < 0)\n"
|
|
^ " let k = app(2, fn(a) =>\n let b = app(a, fn(c) =>\n c * 10\n )\n b + 1)\n"
|
|
^ " i32(k) + xs[0].n\n");
|
|
refused "cfn-captures.fln"
|
|
"fn app(f: CFn(Option(i32)) -> i32) -> i32 = f(None)\n\nfn main() -> i32\n let k = 1\n app(fn(o) => k)\n"
|
|
[ "so it is a Fn(Option(i32)) -> i32 and not a CFn(Option(i32)) -> i32" ];
|
|
refused "defvar.fln" "defvar(x, 1)\n\nfn main() -> i32 = 0\n"
|
|
[ "once x = 1 initialises once"; "def x = 1 re-initialises" ]
|
|
|
|
(* ── Both directions of an import, on both backends ────────────────── *)
|
|
|
|
let run_both ?(backends = [ false; true ]) path want =
|
|
List.iter
|
|
(fun x86 ->
|
|
let exe =
|
|
Filename.concat scratch
|
|
(Printf.sprintf "flan-syntax-%s-%d%s"
|
|
(Filename.basename path) (Unix.getpid ()) (if x86 then "-x86" else ""))
|
|
in
|
|
match
|
|
let p, csrcs, lflags = Test_support.linked path in
|
|
ignore (Build.executable ~opts:{ Build.default with x86 } ~csrcs ~lflags p ~out:exe)
|
|
with
|
|
| exception e -> fail "%s%s does not build: %s" path (if x86 then " --x86" else "") (diag_text e)
|
|
| () ->
|
|
let out = exe ^ ".out" in
|
|
let code = Sys.command (Filename.quote exe ^ " > " ^ Filename.quote out ^ " 2>&1") in
|
|
let text = In_channel.with_open_bin out In_channel.input_all in
|
|
(try Sys.remove out; Sys.remove exe with Sys_error _ -> ());
|
|
if code <> 0 || text <> want then
|
|
fail "%s%s printed %S and exited %d, wanted %S" path
|
|
(if x86 then " --x86" else "") text code want)
|
|
backends
|
|
|
|
(* A program and its conversion print the same: the flat lets, the renames
|
|
and the macro bodies they rest on keep what each name means. *)
|
|
let run_converted path =
|
|
let run p =
|
|
let exe = Filename.concat scratch
|
|
(Printf.sprintf "flan-flat-%s-%d" (Filename.basename p) (Unix.getpid ())) in
|
|
let prog, csrcs, lflags = Test_support.linked p in
|
|
ignore (Build.executable ~opts:Build.default ~csrcs ~lflags prog ~out:exe);
|
|
let out = exe ^ ".out" in
|
|
let code = Sys.command (Filename.quote exe ^ " > " ^ Filename.quote out ^ " 2>&1") in
|
|
let text = In_channel.with_open_bin out In_channel.input_all in
|
|
(try Sys.remove out; Sys.remove exe with Sys_error _ -> ());
|
|
(code, text)
|
|
in
|
|
match
|
|
let forms = Reader.read_file path in
|
|
let source = In_channel.with_open_bin path In_channel.input_all in
|
|
let macros = Body_macros.table ~file:path forms in
|
|
let fln = Filename.concat scratch
|
|
(Printf.sprintf "%d-%s.fln" (Unix.getpid ()) (Filename.remove_extension (Filename.basename path))) in
|
|
Out_channel.with_open_bin fln (fun oc ->
|
|
output_string oc (Indent_printer.program ~source ~macros forms));
|
|
let a = run path and b = run fln in
|
|
(try Sys.remove fln with Sys_error _ -> ());
|
|
(a, b)
|
|
with
|
|
| exception e -> fail "%s converted: %s" path (diag_text e)
|
|
| ((0, a), (0, b)) when a = b -> ()
|
|
| ((c, a), (d, b)) ->
|
|
fail "%s printed %S (exit %d), and converted %S (exit %d)" path a c b d
|
|
|
|
(* ── Programs written by hand in the indented syntax ─────────────────── *)
|
|
|
|
(* Each [syntax/handwritten/x.fln] prints [x.out] on both backends; its
|
|
conversion to parens reads back to the same forms, with every comment,
|
|
and converts back to indented text that reads to them again; and the
|
|
converted .flan builds and prints the same. *)
|
|
let handwritten () =
|
|
let dir = "syntax/handwritten" in
|
|
Sys.readdir dir |> Array.to_list
|
|
|> List.filter (fun f -> Filename.check_suffix f ".fln")
|
|
|> List.sort compare
|
|
|> List.map (Filename.concat dir)
|
|
|
|
let converts_back path =
|
|
let src = In_channel.with_open_bin path In_channel.input_all in
|
|
let forms = Source.read_file path in
|
|
let norm_all fs =
|
|
macros := Body_macros.table ~file:path fs;
|
|
List.map norm fs
|
|
in
|
|
let want = norm_all forms in
|
|
let paren = Paren_printer.program ~source:src forms in
|
|
match Reader.read_all ~file:path paren with
|
|
| exception e -> fail "%s to parens: %s" path (diag_text e)
|
|
| again ->
|
|
if not (same_forms want (norm_all again)) then
|
|
fail "%s to parens: %s" path (describe_diff want (norm_all again))
|
|
else if comment_texts paren <> comment_texts src then
|
|
fail "%s to parens: the comments did not all come through" path
|
|
else begin
|
|
let fln = Indent_printer.program ~source:paren ~macros:!macros again in
|
|
(match Indent_reader.read_all ~file:path fln with
|
|
| exception e -> fail "%s back to indented: %s" path (diag_text e)
|
|
| back ->
|
|
if not (same_forms want (norm_all back)) then
|
|
fail "%s back to indented: %s" path (describe_diff want (norm_all back)));
|
|
(* Beside the original, so its imports resolve the same way. *)
|
|
let flan = Filename.concat (Filename.dirname path)
|
|
(Printf.sprintf ".conv-%d-%s.flan" (Unix.getpid ())
|
|
(Filename.remove_extension (Filename.basename path))) in
|
|
Out_channel.with_open_bin flan (fun oc -> output_string oc paren);
|
|
Fun.protect ~finally:(fun () -> try Sys.remove flan with Sys_error _ -> ())
|
|
(fun () -> run_both ~backends:[ false ] flan
|
|
(In_channel.with_open_bin (Filename.remove_extension path ^ ".out")
|
|
In_channel.input_all))
|
|
end
|
|
|
|
let () =
|
|
List.iter
|
|
(fun p -> try converts_back p with e -> fail "%s: %s" p (diag_text e))
|
|
(List.filter (fun _ -> Test_support.have "clang") (handwritten ()));
|
|
if List.length (handwritten ()) < 8 then
|
|
fail "only %d hand-written programs" (List.length (handwritten ()));
|
|
if Test_support.have "clang" then
|
|
List.iter
|
|
(fun p ->
|
|
run_both p (In_channel.with_open_bin (Filename.remove_extension p ^ ".out")
|
|
In_channel.input_all))
|
|
(handwritten ())
|
|
|
|
let () =
|
|
if Test_support.have "clang" then begin
|
|
List.iter run_converted
|
|
[ "syntax/flat/shadows.flan"; "syntax/flat/macros.flan"; "syntax/flat/capture.flan" ];
|
|
run_both "syntax/mixed/main.flan" "12\n12\n0\n55\n";
|
|
run_both "syntax/mixed/main.fln" "25\n7\nfar\n3\n";
|
|
(* A chain in a template: its names made where the macro expands in the
|
|
paren text, and the chain printed back as one. *)
|
|
let src = In_channel.with_open_bin "syntax/chain/macro.fln" In_channel.input_all in
|
|
let want = "false\ntrue\nfalse\n" in
|
|
run_both "syntax/chain/macro.fln" want;
|
|
let forms = Indent_reader.read_all ~file:"syntax/chain/macro.fln" src in
|
|
let paren = Paren_printer.program ~source:src forms in
|
|
if not (Test_support.contains paren "~(Form.Sym {.s \"~cmp1\"}) ~lo") then
|
|
fail "a template's chain in parens: %s" paren;
|
|
let flan = Filename.concat scratch (Printf.sprintf "chain-macro-%d.flan" (Unix.getpid ())) in
|
|
Out_channel.with_open_bin flan (fun oc -> output_string oc paren);
|
|
run_both flan want;
|
|
let back = Indent_printer.program ~source:paren (Reader.read_all ~file:flan paren) in
|
|
(try Sys.remove flan with Sys_error _ -> ());
|
|
if not (Test_support.contains back "~lo <= ~x < ~hi") then
|
|
fail "a template's chain back from parens: %s" back;
|
|
(* Return types read off the body, in both spellings of [_]. *)
|
|
List.iter
|
|
(fun p -> run_both p "3\n2.5\n1.5\n2.5\nyes 0\n4\n0 5\n2\n1\n")
|
|
[ "syntax/infer/main.flan"; "syntax/infer/main.fln" ]
|
|
end
|
|
else print_endline "syntax: no clang, the import programs are not built"
|
|
|
|
let () =
|
|
(* A local named like an enum shadows it. *)
|
|
List.iter
|
|
(fun (name, text) ->
|
|
let f = Filename.concat scratch name in
|
|
write f text;
|
|
match Test_support.linked f with
|
|
| exception e -> fail "%s: %s" name (diag_text e)
|
|
| _ -> run_both f "5 true\n")
|
|
(if Test_support.have "clang" then
|
|
[ ("shadow-enum.fln",
|
|
"enum Dir\n north\n south\n\nstruct P\n north: i32\n\nfn main() -> i32\n"
|
|
^ " let a = Dir.north\n let Dir = P{.north 5}\n println(Dir.north, a == :north)\n 0\n");
|
|
("shadow-enum.flan",
|
|
"(defenum Dir [north south])\n(defstruct P [north i32])\n(defn main [] i32 "
|
|
^ "(let [a Dir.north Dir (P {.north 5})] (println Dir.north (= a :north)) 0))\n") ]
|
|
else [])
|
|
|
|
(* A local whose address is taken is not narrowed by if x?: a pointer could
|
|
clear it inside the block. The refusal of a payload use says so. *)
|
|
let () =
|
|
let f = Filename.concat scratch "addr-taken-note.fln" in
|
|
write f
|
|
"fn main()\n let x: i32? = Some(1)\n let p = addr(x)\n if x?\n println(x + 1)\n";
|
|
match Front.checked f with
|
|
| _ -> fail "addr-taken-note.fln checked"
|
|
| exception (Loc.Error d | Loc.Errors (d :: _)) ->
|
|
if not (List.exists
|
|
(fun (n : Loc.note) -> Test_support.contains n.Loc.nmsg "Write if x as g")
|
|
d.Loc.notes)
|
|
then fail "addr-taken-note.fln: no note naming if x as g on: %s" d.Loc.dmsg
|
|
| exception e -> fail "addr-taken-note.fln: %s" (diag_text e)
|
|
|
|
let () = Test_support.report ~label:"syntax" ()
|