A binding whose value is a block ends its let's bindings, a tab between let and its name is refused, and only a binding-shaped line under a let gets the alignment advice

This commit is contained in:
Joseph Ferano 2026-09-26 10:39:05 +07:00
parent b5380b4750
commit 63dc853121
5 changed files with 99 additions and 28 deletions

View File

@ -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))))

View File

@ -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")

View File

@ -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;

View File

@ -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.

View File

@ -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";