diff --git a/TODO.org b/TODO.org index fed58224..a1d37892 100644 --- a/TODO.org +++ b/TODO.org @@ -628,6 +628,52 @@ awaiting confirmation, and the build order. Rules out Parinfer, wisp and sweet-expressions, and a simplified in-paren syntax — all thin the parens without removing them. +** TODO A one-line if cannot take its else on the next line +=if c then a= then =else b= under it is refused. Proposal: an =else=/=elif= at +the if's column continues a one-line if, as F#'s does. + +** TODO A lambda with a block cannot be a call's argument +=sort-by(xs, fn(a, b)= plus a block is refused; the lambda has to be bound first, +with its type written. Proposal: a call ending in =fn(...)= and a trailing =:= +hands the block to that lambda: =sort-by(xs, fn(a, b)):=. + +** TODO A lambda's parameters cannot be typed +=fn(a, b)= takes bare names, so a lambda bound by =let= needs +=let f: Fn(C, C) -> bool = fn(a, b)=. Proposal: =fn(a: C, b: C)= as in a +=fn= definition. + +** TODO A condition struct with a parent has no sugar +=defstruct(DiskFull, :parent, IoError, [free i64])= is the fallback, with a +paren field vector. Proposal: =struct DiskFull :parent IoError= plus field lines. + +** TODO defmacro has no sugar +=defmacro(repeat, [i n & body]):= with a space-separated parameter vector. +Proposal: =macro repeat(i, n, & body)= plus a block. + +** TODO loop/recur has no sugar +=loop([x a y b]):=. Proposal: =loop x = a, y = b= plus a block; =recur(...)= +stays a call. + +** TODO A restart's report string is a body line +=:report= and its string sit as two statements under =restart name()=. +Proposal: =restart name() "report text"= on the header line. + +** TODO An enum member has only a keyword spelling +=:north= works, =Dir.north= is an unknown name, while a data case is +=Shape.Rect=. Proposal: accept =Dir.north= as the member. + +** TODO Checker and runtime messages print types and fixes in parens +In a .fln file, =(Fn [A] R)=, =(Option i32)=, =(get 0 :body)=, =(/ x 0)= and most +usage hints still print paren syntax; a handful now print .fln spellings. +Proposal: =Types.to_string= and the hints take the syntax of the location's file. + +** TODO flan convert separates adjacent one-line globals with blank lines +=once a: i32= on consecutive lines come back one blank line apart. + +** TODO A bad map key type is followed by unknown-name errors for its parts +=map-new([const u8], i32)= reports the key, then "unknown name const" and +"unknown name i32" — in both syntaxes. + ** TODO The shims in sand.flan can go =sand.flan= defines =dyn->f64= and =dyn->u32=, one-line functions whose only job is that their parameter slot unboxes. Every call site can write =(f64 d)= and diff --git a/lib/check.ml b/lib/check.ml index a47256b9..fee8fb19 100644 --- a/lib/check.ml +++ b/lib/check.ml @@ -9036,6 +9036,19 @@ and unknown_name : 'a. ?setting:bool -> ctx -> Loc.t -> string -> 'a = name head field head end else + (* [x++]: a name may end in +, so the increment of another language + reads as one unknown name. *) + let n = String.length name in + let stem = if n > 2 then String.sub name 0 (n - 2) else "" in + let suffix = if n > 2 then String.sub name (n - 2) 2 else "" in + if (suffix = "++" || suffix = "--") && lookup ctx stem <> None then + Loc.failk "check/unknown-name" loc + "unknown name %s — to %s %s, write %s" + name (if suffix = "++" then "add one to" else "take one from") stem + (if Source.indented_at loc then + Printf.sprintf "%s(%s) or %s %s= 1" suffix stem stem (String.make 1 suffix.[0]) + else Printf.sprintf "(%s %s)" suffix stem) + else match nearest (value_candidates ctx) name with | Some m -> Loc.failk "check/unknown-name" loc "unknown name %s — did you mean %s?" name m diff --git a/lib/indent_printer.ml b/lib/indent_printer.ml index e7d99832..88b14e23 100644 --- a/lib/indent_printer.ml +++ b/lib/indent_printer.ml @@ -380,7 +380,16 @@ and list _f h args = | _ -> false in let ft = if fl < lvl || (fl = lvl && same) then paren ft else ft in - (String.concat (" " ^ op ^ " ") (ft :: List.map (at (lvl + 1)) rest), lvl) + (* [(a and b) or c]: the parentheses precedence makes optional are + written, as most readers expect them. *) + let and_in_or (x : Form.t) t = + match x.v with + | Form.List ({ v = Form.Sym "and"; _ } :: _ :: _ :: _) when s = "or" && t.[0] <> '(' -> paren t + | _ -> t + in + let ft = and_in_or first ft in + (String.concat (" " ^ op ^ " ") + (ft :: List.map (fun x -> and_in_or x (at (lvl + 1) x)) rest), lvl) | Form.Sym "-", [ x ] -> let t, l = expr x in if l >= 9 && t <> "" && R.is_neg_char t.[0] then ("-" ^ t, 8) @@ -410,7 +419,7 @@ and list _f h args = | Form.Sym s, [ ({ v = Form.Map _; _ } as m) ] when name_ok s && R.capitalised s -> (s ^ fst (expr m), 9) | Form.Sym "fn", [ { v = Form.Vec ps; _ }; body ] when List.for_all sym_param ps -> - ("fn(" ^ commas ps ^ ") = " ^ at 0 body, 0) + ("fn(" ^ commas ps ^ ") = " ^ unit_text body, 0) | Form.Sym "if", [ c; a; b ] -> ("if " ^ at 1 c ^ " then " ^ inline_text ~lvl:1 a ^ " else " ^ inline_text b, 0) | _ -> call () @@ -420,6 +429,10 @@ and list _f h args = everything else as a value. [lvl] is what a value in the slot needs. *) and inline_text ?(lvl = 0) (f : Form.t) = match f.v with + (* In a one-line slot a bare [()] reads as [(do)]; the value [()] is + written [(())]. *) + | Form.List [] -> "(())" + | Form.List [ { v = Form.Sym "do"; _ } ] -> "()" | Form.List [ { v = Form.Sym (("break" | "continue" | "return") as w); _ } ] -> w | Form.List [ { v = Form.Sym (("break" | "continue") as w); _ }; { v = Form.Kw k; _ } ] when kw_ok k -> @@ -431,6 +444,13 @@ and inline_text ?(lvl = 0) (f : Form.t) = at 9 t ^ " " ^ op ^ "= " ^ at (max lvl 1) w | _ -> at lvl f +(* A body after [=]: [()] there reads as [(do)]. *) +and unit_text (f : Form.t) = + match f.v with + | Form.List [] -> "(())" + | Form.List [ { v = Form.Sym "do"; _ } ] -> "()" + | _ -> at 0 f + (* [t = v], or [t += w] when [v] is [(+ t w)]. *) and assign_text ?(lvl = 0) t v = let tt = at 9 t in @@ -653,6 +673,9 @@ and plain n (f : Form.t) : string list = (* No arguments before the block: [comment:] rather than [comment():], the author's decision 85. *) | Form.Sym s, [] when name_ok s && not (List.mem s reserved) -> s ^ ":" + (* A header word glued to its parenthesis is the fallback call, + [if(c):], so it needs no parentheses of its own. *) + | Form.Sym s, _ :: _ when List.mem s reserved -> s ^ "(" ^ commas fixed ^ "):" | _ -> head_text h ^ "(" ^ commas fixed ^ "):" in [ ind n ^ guard opener ] @ block ~seq (n + 2) rest @@ -916,7 +939,7 @@ and sugar n (f : Form.t) : string list option = | _ -> true) && String.length head + 3 + String.length (at 0 x) <= width && not (!inside f) -> - Some [ head ^ " = " ^ at 0 x ] + Some [ head ^ " = " ^ unit_text x ] | _ -> Some (head :: block (n + 2) body))) | Form.List ({ v = Form.Sym (("def" | "defonce" | "defconst") as d); _ } :: { v = Form.Sym name; _ } :: rest) diff --git a/lib/indent_reader.ml b/lib/indent_reader.ml index cfd21f3c..0b3be79e 100644 --- a/lib/indent_reader.ml +++ b/lib/indent_reader.ml @@ -208,9 +208,22 @@ let lex ?(line = 1) ?(col = 1) ~file src : token list = Reader.advance st; emit COLON (piece l0.Loc.line (l0.Loc.col + n) 1) end - else + else begin + (* [0..10]: a range from another language. *) + let text = String.sub src st.Reader.pos (stop - st.Reader.pos) in + (match String.index_opt text '.' with + | Some i when i + 1 < String.length text && text.[i + 1] = '.' -> + failk "dot-range" (piece l0.Loc.line l0.Loc.col (String.length text)) + "%s is not a number. A range of numbers is written range(%s, %s), \ + as in for i in range(%s, %s)" + text (String.sub text 0 i) + (String.sub text (i + 2) (String.length text - i - 2)) + (String.sub text 0 i) + (String.sub text (i + 2) (String.length text - i - 2)) + | _ -> ()); let f = Reader.read_number st in emit (ATOM f.v) f.loc + end | _ -> name_run () in let rec go () = @@ -477,9 +490,13 @@ let expect_eol p ~after = if (peek p).tok = INDENT then failk "stray-indent" (peek_at p 1).loc "this line is indented under %s, which takes no block. A call takes \ - an indented block only with a trailing colon, as in \ - rl/with-drawing():" + an indented block only with a trailing colon, as in %s:" after + (* The call itself when [after] is one, [f(a, b)]; else an example. *) + (match String.index_opt after '(', String.index_opt after ' ' with + | Some i, Some j when i < j -> after + | Some _, None when after.[String.length after - 1] = ')' -> after + | _ -> "rl/with-drawing()") | EOF -> () | _ -> stray p ~after @@ -725,6 +742,14 @@ and if_expr p = (* What a one-line slot takes — a match arm's value, a then or an else, the thing after defer: a value, or one of the statements that fit on a line, break, continue, return and an assignment. *) +(* A bare [()] written where a body goes — a one-line slot, a function's + [= ()] — is the empty statement, [(do)], as it is on a line of its own: + "do nothing" is what it says there. [(())] stays a value. *) +and unit_slot p i0 (t0 : token) (e : Form.t) = + if p.i - i0 = 2 && t0.tok = LP && e.v = Form.List [] then + Form.make (Form.List [ sym t0.loc "do" ]) e.loc + else e + and inline_stmt p : Form.t = let t = peek p in let glued = let n = peek_at p 1 in n.tok = LP && not n.sp in @@ -744,6 +769,7 @@ and inline_stmt p : Form.t = mk p t.loc (Form.List [ sym t.loc "return"; v ]) else mk p t.loc (Form.List [ sym t.loc "return" ]) | _ -> + let i0 = p.i in let e, _ = expr p in match (peek p).tok with | NAME "=" -> @@ -754,7 +780,7 @@ and inline_stmt p : Form.t = let eq = advance p in let v, _ = expr p in mk p t.loc (compound eq.loc (List.assoc op assign_ops) e v (span p e.loc)) - | _ -> e + | _ -> unit_slot p i0 t e (* [fn(a, b) = body] is a lambda; [fn(...)] followed by anything else is the fallback call spelling of [(fn ...)]. *) @@ -766,7 +792,9 @@ and fn_expr p = | NAME "=" -> ignore (advance p); let ps = lambda_params args in + let i0 = p.i and t0 = peek p in let body, _ = expr p in + let body = unit_slot p i0 t0 body in (mk p t.loc (Form.List [ sym t.loc "fn"; Form.make (Form.Vec ps) (span_of_list lp.loc args); body ]), @@ -826,6 +854,15 @@ and items p closer open_loc ~what = \ let f = %s\n ...\n\n\ and pass f, or write it on one line: %s = value" (text_of e) (text_of e) + else if starts_value n.tok && n.sp && not (negative_literal n.tok) + && n.loc.Loc.line > e.loc.Loc.eline then + (* Most often the bracket was never closed: the next statement + has been read as one more argument. *) + failk "missing-comma" n.loc + "%s on a new line follows %s with no comma between them. If the \ + %c on line %d was meant to close before this line, close it; \ + otherwise separate %s with commas" + (show n.tok) (text_of e) opener open_loc.Loc.line what else if starts_value n.tok && n.sp && not (negative_literal n.tok) then failk "missing-comma" n.loc "%s follows %s with no comma between them. Separate %s with \ @@ -883,6 +920,14 @@ and map_items p open_loc = | COMMA -> ignore (advance p); go (e :: acc) | RC -> ignore (advance p); List.rev (e :: acc) | EOF -> unclosed p '{' open_loc + (* [P{x = 1}] or [P{x: 1}]: another language's field syntax. *) + | (NAME "=" | COLON) as tk + when (match e.v with Form.Sym n -> n <> "" && n.[0] <> '.' | _ -> false) -> + let n = text_of e in + failk "brace-field" (peek p).loc + "a field in braces is written {.%s value}: a dot before the name, \ + and no %s between it and the value" + n (if tk = COLON then "colon" else "= sign") | tk when starts_value tk && (peek p).sp -> if lvl < 8 then refuse_ws ~brace:true t.loc e; go (e :: acc) @@ -1294,7 +1339,15 @@ and header (s : st) w : Form.t = ignore (advance p); block s ~after:"fn" end - else [ value_line s ~after:"=" ] + else begin + let i0 = p.i and t0 = peek p in + let v = value_line s ~after:"=" in + let bare = + t0.tok = LP && p.toks.(i0 + 1).tok = RP && v.v = Form.List [] + && (match p.toks.(i0 + 2).tok with NEWLINE | EOF | DEDENT -> true | _ -> false) + in + [ (if bare then Form.make (Form.List [ sym t0.loc "do" ]) v.loc else v) ] + end | NEWLINE -> ignore (advance p); if (peek p).tok = INDENT then block s ~after:"fn" else [] diff --git a/spec-syntax.md b/spec-syntax.md index 5f05b1ef..22218f78 100644 --- a/spec-syntax.md +++ b/spec-syntax.md @@ -206,7 +206,8 @@ Each item: the proposal, then the reason in one line. - **`for i in range(n)`**, `range(a, b)`, `range(a, b, step)` read as `dotimes`. `range` here is syntax, not a function. `..` is avoided because `a..b` would lex as one name. **Built** (a label goes first here too: - `for :outer i in range(n)`). + `for :outer i in range(n)`; in a macro template the variable may be an + unquote, `for ~i in range(~n)`). - **`return v`, `break`, `break :outer`, `continue`, `defer expr`** (or `defer` plus a block). **Built**; `defer` plus a block reads `(defer a b …)`. `break`, `continue`, `return v` and `x = v`/`x += v` also fit the one-line @@ -255,7 +256,8 @@ Each item: the proposal, then the reason in one line. the body, where the form wants them. **Built.** - **Unit:** `()` as a statement reads `(do)`; in a type it is `()`. **Built**; inside an expression `()` stays `()`, and the printer writes a lone `()` - statement as `(())`. + statement as `(())`. A bare `()` in a one-line body slot (`fn f() -> () = ()`, + `_ -> ()`, `fn() = ()`, `then ()`) is a statement too, and reads `(do)`. - **Lambda:** `fn(i, j) = i * 10 + j`, or `fn(i, j)` plus a block. **Built**; its parameters are bare names, as `(fn [i j] …)` wants, with no `dyn`. `fn(…)` followed by anything else is the fallback call. diff --git a/test/test_syntax.ml b/test/test_syntax.ml index 16aee4a8..fb8c886d 100644 --- a/test/test_syntax.ml +++ b/test/test_syntax.ml @@ -582,6 +582,25 @@ let () = reads "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" "go 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)"; + refuses "a block lambda inside a call" "sort-by(xs, fn(a, b)\n a < b)" + "indent/lambda-block-in-brackets" "let f = fn(a, b)"; + 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"; + refuses "else under a one-line if" "if a then b\nelse c" + "indent/orphan-else" "one-line if"; + 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:"

" src with @@ -718,7 +737,40 @@ let () = "(defn f [] i32\n (let [a 1 ; first\n b 2] ; second\n (+ a b)))" " let a = 1 ; first\n let b = 2 ; second"; 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)" + "; 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 some-long-condition?(1, 2, 3) and 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 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)" (* ── Spans, for pause marks and error overlays ──────────────────────── *) @@ -914,7 +966,21 @@ let () = (* 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" ] + [ "() 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" ] (* ── Both directions of an import, on both backends ────────────────── *)