diff --git a/emacs/flan-fln-mode.el b/emacs/flan-fln-mode.el index 37db0f53..71148cd8 100644 --- a/emacs/flan-fln-mode.el +++ b/emacs/flan-fln-mode.el @@ -486,12 +486,15 @@ on its line, which is not the statement's." (defun flan-fln--binding-column (l) "The column of the first name on the `let' line L: where the let's other -bindings line up." +bindings line up. Nil when a tab sits before the name: a tab has no one +width, and the reader refuses it there." (save-excursion (goto-char (flan-fln--first-char l)) (skip-chars-forward "let") - (skip-chars-forward " \t") - (current-column))) + (let ((from (point))) + (skip-chars-forward " \t") + (unless (save-excursion (search-backward "\t" from t)) + (current-column))))) (defun flan-fln--binding-shape-p (l &optional global) "Non-nil if the joined line L reads as a binding: a name, `~x', a @@ -1364,7 +1367,7 @@ whose brackets have closed is no block POS can join." ;; After a binding line, that column is on the stack, first. ((and prev (flan-fln--let-line-p (flan-fln--logical-start prev))) (let ((b (flan-fln--binding-column (flan-fln--logical-start prev)))) - (if (memq b stack) stack + (if (or (null b) (memq b stack)) stack (cons (car stack) (cons b (cdr stack)))))) (t stack)))) diff --git a/emacs/test-flan-fln.el b/emacs/test-flan-fln.el index 359c9315..94288569 100644 --- a/emacs/test-flan-fln.el +++ b/emacs/test-flan-fln.el @@ -922,6 +922,10 @@ defconst(k, 3) (test-flan-fln--tabs "fn f()\n let a = 1\n| b = 2" 1) 6) (test-flan-fln--is "at the top level too" (test-flan-fln--tabs "let a = 1\n|" 2) 4) +(test-flan-fln--is "but not after a tab between let and its name" + (list (test-flan-fln--tabs "fn f()\n let\ta = 1\n|" 1) + (test-flan-fln--tabs "fn f()\n let\ta = 1\n|" 2)) + '(2 0)) (dolist (c '(("let r = match n" "a let's match") ("let r = if c" "a let's if") ("x = if c" "an assignment's if") diff --git a/lib/indent_reader.ml b/lib/indent_reader.ml index 28dd7c54..c5bedf3f 100644 --- a/lib/indent_reader.ml +++ b/lib/indent_reader.ml @@ -289,11 +289,40 @@ let let_misaligned loc ~let_line ~name ~name_col = let let_after_block loc ~let_line ~name = let bt, _ = source_line loc.Loc.line in failk "let-after-block" loc - "this line lines up with %s, the first name of the let on line %d, as one \ - more binding of it would. That let's value is the block under its line, \ - and a let whose value is a block takes no more bindings. Give this one a \ - let of its own, at that let's column:\n\n let %s" - name let_line bt + "this line lines up as one more binding of the let on line %d, after %s, \ + 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 %s" + let_line name bt + +(* Whether the tokens from [i] start a line shaped as a binding: [name =], + [name:], a [[...]], [{...}] or [(op)] pattern, or [~x]. The alignment + advice is for such lines only; any other line under a let is some other + mistake. *) +let binding_shaped (arr : token array) i = + let n = Array.length arr in + let at k = if i + k < n then arr.(i + k).tok else EOF in + match at 0, at 1 with + | NAME s, (NAME "=" | COLON) -> s <> "" && s.[0] <> '.' + | (LB | LC | LP | UNQ), _ -> true + | _ -> false + +(* A tab between [let] and its first name: the bindings under the let line + up with that name, and a tab has no one width to line up with. *) +let let_gap_tab (t : token) (nm : token) = + if t.loc.Loc.eline = nm.loc.Loc.line then begin + let _, lines = !source in + if nm.loc.Loc.line <= Array.length lines then + let line = lines.(nm.loc.Loc.line - 1) in + let a = t.loc.Loc.ecol - 1 and b = nm.loc.Loc.col - 1 in + if a >= 0 && b <= String.length line && b > a + && String.contains (String.sub line a (b - a)) '\t' then + failk "tab" t.loc + "there is a tab between let and %s. The bindings under a let line up \ + with its first name, and a tab has no one width, so put a space \ + there: let %s" + (show nm.tok) (show nm.tok) + end let point (l : Loc.t) = { l with Loc.line = l.Loc.eline; col = l.Loc.ecol } @@ -472,7 +501,8 @@ let layout ?(snippet = false) ?(base = 1) ?indent (toks : token list) : token ar in match owner (i - 1) with | Some j when arr.(j).tok = NAME "let" && j + 1 < n - && arr.(j).loc.Loc.col = List.hd !stack -> + && arr.(j).loc.Loc.col = List.hd !stack + && binding_shaped arr i -> let nm = arr.(j + 1) in let let_line = arr.(j).loc.Loc.line and name = show nm.tok in if col = nm.loc.Loc.col then let_after_block t.loc ~let_line ~name @@ -1655,22 +1685,34 @@ and binding_lines : 'a. ?global:bool -> st -> first:Form.t -> name_col:int -> on let_misaligned t.loc ~let_line:first.loc.Loc.line ~name:(text_of first) ~name_col in - let rec go acc = + (* [block] is the binding before this line when its value took the + lines under it, which ends the let's bindings. *) + let rec go ?block acc = match (peek p).tok with | DEDENT -> ignore (advance p); List.rev acc | EOF -> List.rev acc - | INDENT -> misaligned (peek_at p 1) + | INDENT when binding_shaped p.toks (p.i + 1) -> misaligned (peek_at p 1) + | INDENT -> ignore (advance p); not_binding () | _ when binding_line ~global p -> if (peek p).loc.Loc.col <> name_col then misaligned (peek p); - go (one () :: acc) - | _ -> - failk "let-block" (where_ p) - "this line is indented under let %s, and the only lines that go \ - there are more bindings of the let, lined up with its first name:\n\n\ - \ let a = 1\n b = a + 1\n\n\ - A let's names last to the end of the block the let is in, so any \ - other line after it goes at the let's column" - (text_of first) + (match block with + | Some (name, line) -> let_after_block (peek p).loc ~let_line:line ~name + | None -> ()); + let i0 = p.i and t0 = peek p in + let x = one () in + let took = ref false in + for k = i0 to p.i - 1 do if p.toks.(k).tok = INDENT then took := true done; + go ?block:(if !took then Some (text_of_tok t0, t0.loc.Loc.line) else None) + (x :: acc) + | _ -> not_binding () + and not_binding () = + failk "let-block" (where_ p) + "this line is indented under let %s, and the only lines that go \ + there are more bindings of the let, lined up with its first name:\n\n\ + \ let a = 1\n b = a + 1\n\n\ + A let's names last to the end of the block the let is in, so any \ + other line after it goes at the let's column" + (text_of first) in go [] end @@ -1712,6 +1754,7 @@ and binding_line ?(global = false) p = and let_stmt (s : st) : Form.t list = let p = s.p in let t = advance p in + let_gap_tab t (peek p); let name_col = (peek p).loc.Loc.col in let (target, v) = let_values := (name_col, text_of_tok (peek p), t.loc.Loc.line) :: !let_values; @@ -2542,6 +2585,7 @@ let read_all ?(line = 1) ?col ?indent ?(global_let = true) ~file src = | DEDENT -> ignore (advance s.p); [] | NAME "let" when header_follow s.p "let" -> let t = peek s.p in + let_gap_tab t (peek_at s.p 1); let name_col = (peek_at s.p 1).loc.Loc.col in let f = let_values := (name_col, text_of_tok (peek_at s.p 1), t.loc.Loc.line) :: !let_values; diff --git a/spec-syntax.md b/spec-syntax.md index a4cd92d7..6b1b906e 100644 --- a/spec-syntax.md +++ b/spec-syntax.md @@ -203,9 +203,10 @@ Each item: the proposal, then the reason in one line. is `(let [row (+ r 1) col (the i32 (- c 1)) {x .x} p] rest…)`. A name that is an operator word is written in parentheses, `(not) = 3`. A binding at another column than the first name, or any other line indented - there, is refused. A let whose first value is a block (`= match x`, a - lambda header, `=` and the lines under it) takes no more bindings under - it. At the top level each such line is one more global, `(def col i32 …)`, + there, is refused, and so is a tab between `let` and its first name. A + binding whose value is a block (`= match x`, a lambda header, `=` and the + lines under it) is its let's last; the printer starts a new `let` after + one. At the top level each such line is one more global, `(def col i32 …)`, and may be `name: T` alone as a global's own line may; a pattern there is refused, since a global binds one name. A `let` is otherwise flat: its scope is the rest of its block. To end it early, put it in a `do:` block. diff --git a/test/test_syntax.ml b/test/test_syntax.ml index aaa4be35..aae63505 100644 --- a/test/test_syntax.ml +++ b/test/test_syntax.ml @@ -718,8 +718,10 @@ let () = "(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 binding with a block value" "let a = 1\n b =\n f()\n a\n c = match b\n 1 -> 2\n _ -> 3\ng(c)" - "(let [a 1 b (do (f) a) c (match b 1 2 _ 3)] (g c))"; + 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" @@ -753,11 +755,25 @@ let () = "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" "lines up with g, the first name of the let on line 2"; + "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" "lines up with 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 3, 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"; + 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)))"; @@ -973,6 +989,9 @@ let () = 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";