diff --git a/emacs/flan-fln-mode.el b/emacs/flan-fln-mode.el index a0fcf4ff..0f6e789f 100644 --- a/emacs/flan-fln-mode.el +++ b/emacs/flan-fln-mode.el @@ -1347,10 +1347,26 @@ whose brackets have closed is no block POS can join." (unless (eql min 0) (push (cons 0 nil) out)) (nreverse out))) +(defun flan-fln--open-ended-p (l) + "Non-nil if the joined line L ends in a block still open at its end: a +trailing `fn(x) =>', `= match x', `= if c', `x =' or `f():', and not a +block lambda inside brackets, whose block closes with them." + (let ((last (flan-fln--logical-end l))) + (and (flan-fln--opener-p l last) + (= (car (save-excursion (syntax-ppss (flan-fln--code-end last)))) + (car (save-excursion (syntax-ppss (flan-fln--first-char l)))))))) + (defun flan-fln--levels (pos) - "Columns TAB offers POS's line outside brackets, deepest first." + "Columns TAB offers POS's line outside brackets, deepest first. +Not the column of a let's binding whose value ended in an open block: that +block ends the let's bindings (lib/indent_reader.ml's `binding_lines'), and +the reader refuses any line there." (let* ((prev (flan-fln--prev-code pos)) - (stack (mapcar #'car (flan-fln--stack pos)))) + (stack (mapcar #'car + (seq-remove (lambda (e) + (and (cdr e) (flan-fln--binding-let (cdr e)) + (flan-fln--open-ended-p (cdr e)))) + (flan-fln--stack pos))))) (cond ;; A lambda's block goes under the line its `=>' ends, which may be a ;; line of a call wrapped inside its brackets. diff --git a/emacs/test-flan-fln.el b/emacs/test-flan-fln.el index 67515d1b..3f8305e9 100644 --- a/emacs/test-flan-fln.el +++ b/emacs/test-flan-fln.el @@ -924,6 +924,14 @@ 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) +(dolist (c '(("fn f()\n let a = 0\n g = fn(x) =>\n x + 1\n|" (8 2 0) "a lambda's block") + ("fn f()\n let a = 1\n b = match a\n 1 -> 2\n|" (8 2 0) "a match's arms") + ("let a = 0\n g = fn(x) =>\n x + 1\n|" (6 0) "a global's lambda block") + ("fn f()\n let a = 1\n g = map(xs, fn(x) =>\n x + 1)\n|" (6 2 0) + "but a lambda's brackets closed, where the column stays"))) + (test-flan-fln--is (format "no binding column after a binding's open block: %s" (nth 2 c)) + (test-flan-fln--in (car c) (flan-fln--block-levels (point))) + (cadr c))) (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)) diff --git a/lib/indent_reader.ml b/lib/indent_reader.ml index a31812a5..2870dca8 100644 --- a/lib/indent_reader.ml +++ b/lib/indent_reader.ml @@ -1704,8 +1704,10 @@ 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 - (* [block] is the binding before this line when its value took the - lines under it, which ends the let's bindings. *) + (* [block] is the binding before this line when its value ended in a + block still open at its end — a trailing [fn(x) =>], [match], [if], + [x =] or [f():] — which ends the let's bindings. A block lambda whose + brackets close after its block does not. *) let rec go ?block acc = match (peek p).tok with | DEDENT -> ignore (advance p); List.rev acc @@ -1715,14 +1717,16 @@ and binding_lines : 'a. ?global:bool -> st -> first:Form.t -> name_col:int -> on | _ when binding_line ~global p -> if (peek p).loc.Loc.col <> name_col then misaligned (peek p); (match block with - | Some (name, line) -> let_after_block (peek p).loc ~let_line:line ~name + | Some name -> let_after_block (peek p).loc ~let_line:first.loc.Loc.line ~name | None -> ()); - let i0 = p.i and t0 = peek p in + let 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) + (* The last token the value took, past its line's end. *) + let rec last_real k = + if k > 0 && p.toks.(k).tok = NEWLINE then last_real (k - 1) else k + in + let open_block = p.toks.(last_real (p.i - 1)).tok = DEDENT in + go ?block:(if open_block then Some (text_of_tok t0) else None) (x :: acc) | _ -> not_binding () and not_binding () = failk "let-block" (where_ p) diff --git a/spec-syntax.md b/spec-syntax.md index c1afbeb6..c1070b98 100644 --- a/spec-syntax.md +++ b/spec-syntax.md @@ -206,7 +206,9 @@ Each item: the proposal, then the reason in one line. 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 …)`, + one. A block lambda whose brackets close after its block, + `g = map(xs, fn(x) =>` over `x + 1)`, is not: its block is shut when the + value ends, and more bindings may follow. 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 b48a0ae7..d7ba28db 100644 --- a/test/test_syntax.ml +++ b/test/test_syntax.ml @@ -779,7 +779,17 @@ let () = 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 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"; + "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. *)