flan-fln-mode reads a lambda's block inside brackets as statements, and the spec and manual write lambdas with =>

This commit is contained in:
Joseph Ferano 2026-09-26 05:27:28 +07:00
parent 3c962197f4
commit 6e56fce7fa
6 changed files with 275 additions and 56 deletions

View File

@ -644,11 +644,10 @@ awaiting confirmation, and the build order. Rules out Parinfer, wisp and
sweet-expressions, and a simplified in-paren syntax — all thin the parens sweet-expressions, and a simplified in-paren syntax — all thin the parens
without removing them. without removing them.
** NEXT Lambdas are written with => ** DONE Lambdas are written with =>
Decided 2026-09-26: a lambda's body follows ~=>~, and ~=>~ is its only spelling CLOSED: [2026-09-26]
(~fn(a) = x~ is refused for a lambda; named functions keep ~=~). A header ending in ~=>~ A block lambda inside brackets is the last thing in them, its block ending where they close: a comma
takes an indented block even inside brackets, closing where the brackets close: after it (so a second block lambda, or one not last) is refused, and the fix names it with ~let~.
~sort-by(xs, fn(a, b) =>~ plus a block.
** NEXT A condition struct with a parent has no sugar ** NEXT A condition struct with a parent has no sugar
On the author's decision. =defstruct(DiskFull, :parent, IoError, [free i64])= is the fallback, with a On the author's decision. =defstruct(DiskFull, :parent, IoError, [free i64])= is the fallback, with a

View File

@ -1205,7 +1205,7 @@ Use `C-c C-g` if you need frames.
| `M-a` / `M-e` | `(` / `)` | statement: start / end (`)`: start of the next) | | `M-a` / `M-e` | `(` / `)` | statement: start / end (`)`: start of the next) |
| `C-M-u` | same | up to the enclosing bracket, or the line that owns the block | | `C-M-u` | same | up to the enclosing bracket, or the line that owns the block |
| `C-M-f` / `C-M-b` | same | brackets and terms, as everywhere | | `C-M-f` / `C-M-b` | same | brackets and terms, as everywhere |
| `TAB` | same | a line at a valid column stays; an empty or misplaced line goes deepest; each repeat steps out a level. One level deeper only after a line that opens a block: never after a `let`, unless its value goes on under it (`= match x`, `= if c`, a lambda header) | | `TAB` | same | a line at a valid column stays; an empty or misplaced line goes deepest; each repeat steps out a level. One level deeper only after a line that opens a block: never after a `let`, unless its value goes on under it (`= match x`, `= if c`, a lambda header). After a line ending in `=>`, one level in from that line, inside brackets too; a line of that block keeps to the block's columns |
| `DEL` in indentation | same | drop one level | | `DEL` in indentation | same | drop one level |
| `C-c <` / `C-c >` | `<` / `>` | shift the region's lines a level | | `C-c <` / `C-c >` | `<` / `>` | shift the region's lines a level |
| `M-<up>` / `M-<down>` | same | move the statement past its neighbour | | `M-<up>` / `M-<down>` | same | move the statement past its neighbour |
@ -1225,7 +1225,7 @@ enclosing statement, top-level form.
- **term**: a run with no space outside brackets — `f(a, b)`, `grid[r, c]`, `p.x`. - **term**: a run with no space outside brackets — `f(a, b)`, `grid[r, c]`, `p.x`.
- **group**: a bracket pair and what is inside it. - **group**: a bracket pair and what is inside it.
- **statement**: a line, the deeper lines under it, lines inside brackets it leaves open, lines an operator continues, and `else`/`elif`/`on`/`restart` at its column. Blank and comment lines inside never end it. - **statement**: a line, the deeper lines under it, lines inside brackets it leaves open, lines an operator continues, and `else`/`elif`/`on`/`restart` at its column. Blank and comment lines inside never end it. A lambda's block inside brackets, `sort-by(xs, fn(a, b) =>` and the lines under it, is part of the call's statement, and each of its lines is a statement too: `C-c C-e` or `C-u C-c C-c` there takes that line, not the call, and never the call's closing `)` at the end of it.
- **body**: a statement's own block, up to its first clause. - **body**: a statement's own block, up to its first clause.
- **clause**: one `else`/`elif`/`on`/`restart` line and its block, or the value on its line (`else x`). - **clause**: one `else`/`elif`/`on`/`restart` line and its block, or the value on its line (`else x`).
- **top-level form**: a column-0 line that is code, not a clause and not a continuation, through the last code line before the next one. - **top-level form**: a column-0 line that is code, not a clause and not a continuation, through the last code line before the next one.

View File

@ -25,7 +25,8 @@
;; leaves open, the lines an operator continues, and the ;; leaves open, the lines an operator continues, and the
;; `else'/`elif'/`on'/`restart' clauses at its own column. Blank ;; `else'/`elif'/`on'/`restart' clauses at its own column. Blank
;; and comment lines inside never end it; trailing ones are not ;; and comment lines inside never end it; trailing ones are not
;; part of it. ;; part of it. A `=>' ending a line inside brackets opens a
;; lambda's block there, whose lines are statements again.
;; body a statement's own block: the deeper lines under its first line, ;; body a statement's own block: the deeper lines under its first line,
;; up to its first clause. ;; up to its first clause.
;; clause one `else'/`elif'/`on'/`restart' line and its block. ;; clause one `else'/`elif'/`on'/`restart' line and its block.
@ -224,15 +225,61 @@ fine here. Brackets and strings are still paired."
(setq hit (line-beginning-position))) (setq hit (line-beginning-position)))
hit))) hit)))
(defun flan-fln--closer-line-p (pos)
"Non-nil if POS's line starts inside a bracket with the bracket's closer."
(save-excursion
(let ((s (syntax-ppss (flan-fln--bol pos))))
(and (nth 1 s) (not (nth 3 s))
(progn (goto-char (flan-fln--bol pos))
(skip-chars-forward " \t")
(looking-at "\\s)"))))))
(defun flan-fln--lambda-arrow (pos)
"The `=>' whose block POS's line is in, when that block is inside brackets.
A `=>' ending a line inside a bracket opens a block there, which lasts until
the bracket closes (lib/indent_reader.ml's `layout'): a line inside the same
bracket after it is a statement of that block, not a continuation. A line
that starts with the bracket's closer is not in the block."
(save-excursion
(let* ((bol (flan-fln--bol pos))
(s (syntax-ppss bol))
(open (nth 1 s)))
(when (and open (not (nth 3 s)) (not (flan-fln--closer-line-p bol)))
(goto-char open)
(let (hit)
(while (and (not hit) (< (line-end-position) bol))
(let ((end (flan-fln--code-end (point))))
(goto-char end)
(when (and (looking-back "[ \t]=>" (line-beginning-position))
(= (nth 1 (save-excursion (syntax-ppss (- end 2)))) open))
(setq hit (- end 2))))
(forward-line 1))
hit)))))
(defun flan-fln--bracketed-p (pos)
"Non-nil if POS's line is inside a bracket or a string, where a line break
is only a space: not in a lambda's block."
(and (flan-fln--in-open-p pos) (not (flan-fln--lambda-arrow pos))))
(defun flan-fln--continuation-p (pos) (defun flan-fln--continuation-p (pos)
"Non-nil if POS's line continues the line above it. "Non-nil if POS's line continues the line above it.
Inside a bracket or a string, after a line that ends in a spaced operator, or Inside a bracket or a string, after a line that ends in a spaced operator, or
starting with one: the reader's three ways a line break is not a new line." starting with one: the reader's three ways a line break is not a new line.
(or (flan-fln--in-open-p pos) Not a line of a lambda's block inside brackets, which is a statement."
(or (flan-fln--bracketed-p pos)
(flan-fln--starts-with-op-p pos) (flan-fln--starts-with-op-p pos)
(let ((p (flan-fln--prev-code pos))) (let ((p (flan-fln--prev-code pos)))
(and p (flan-fln--ends-in-op-p p))))) (and p (flan-fln--ends-in-op-p p)))))
(defun flan-fln--continues-p (pos start)
"Non-nil if POS's line continues the statement that starts at START.
A line that starts with the closer of a bracket opened before START closes
something START is inside, a lambda's block's call, and is not START's."
(and (flan-fln--continuation-p pos)
(not (and (flan-fln--closer-line-p pos)
(< (nth 1 (save-excursion (syntax-ppss (flan-fln--bol pos))))
(flan-fln--bol start))))))
(defun flan-fln--clause-line-p (pos) (defun flan-fln--clause-line-p (pos)
"Non-nil if POS's line starts a clause: else, elif, on or restart. "Non-nil if POS's line starts a clause: else, elif, on or restart.
A word, and only when a space or the end of the line follows it." A word, and only when a space or the end of the line follows it."
@ -245,16 +292,21 @@ A word, and only when a space or the end of the line follows it."
(defun flan-fln--logical-start (pos) (defun flan-fln--logical-start (pos)
"The first line of the line POS is on, after continuation lines are joined." "The first line of the line POS is on, after continuation lines are joined."
(let ((bol (flan-fln--bol pos)) p) (let ((bol (flan-fln--bol pos)) p)
(while (and (flan-fln--continuation-p bol) (while (cond
(setq p (flan-fln--prev-code bol))) ;; A closer's line belongs with the line its bracket opens on,
(setq bol p)) ;; not with a lambda's block just above it.
((flan-fln--closer-line-p bol)
(setq bol (flan-fln--bol (nth 1 (save-excursion (syntax-ppss bol))))))
((and (flan-fln--continuation-p bol)
(setq p (flan-fln--prev-code bol)))
(setq bol p))))
bol)) bol))
(defun flan-fln--logical-end (pos) (defun flan-fln--logical-end (pos)
"The last line of the joined line whose first line is POS's." "The last line of the joined line whose first line is POS's."
(let ((bol (flan-fln--bol pos)) n) (let* ((bol (flan-fln--bol pos)) (start bol) n)
(while (and (setq n (flan-fln--next-code bol)) (while (and (setq n (flan-fln--next-code bol))
(flan-fln--continuation-p n)) (flan-fln--continues-p n start))
(setq bol n)) (setq bol n))
bol)) bol))
@ -309,18 +361,23 @@ depth, outside strings and comments, or nil."
(flan-fln--joined-end l)))) (flan-fln--joined-end l))))
(and m (cdr m)))) (and m (cdr m))))
(defun flan-fln--ends-in-arrow-p (pos)
"Non-nil if POS's line ends in `=>': a lambda's header, its block under it."
(save-excursion
(let ((end (flan-fln--code-end pos)))
(goto-char end)
(and (not (nth 8 (syntax-ppss end)))
(looking-back "[ \t]=>" (line-beginning-position))))))
(defun flan-fln--lambda-header-p (pos end) (defun flan-fln--lambda-header-p (pos end)
"Non-nil if a lambda with its body under it starts at POS and runs to END: "Non-nil if a lambda with its body under it starts at POS and runs to END:
`fn(a, b)', or `fn(a: C, b) -> R', with no `= body' after it." `fn(a, b) =>', or `fn(a: C, b) -> R =>', with nothing after the `=>'."
(save-excursion (save-excursion
(goto-char pos) (goto-char pos)
(and (looking-at "fn(") (and (looking-at "fn(")
(let ((close (ignore-errors (scan-lists (+ pos 2) 1 0)))) (let ((close (ignore-errors (scan-lists (+ pos 2) 1 0))))
(and close (<= close end) (and close (<= close end)
(progn (goto-char close) (skip-chars-forward " \t") (progn (goto-char end) (looking-back "[ \t]=>" close)))))))
(or (>= (point) end)
(and (looking-at "->[ \t]")
(not (flan-fln--find-top "[ \t]=[ \t]" (point) end))))))))))
(defun flan-fln--value-opens-p (l) (defun flan-fln--value-opens-p (l)
"Non-nil if the value the joined line L binds or assigns goes on under it: "Non-nil if the value the joined line L binds or assigns goes on under it:
@ -346,6 +403,8 @@ line's own block only."
(last (flan-fln--logical-end start)) (last (flan-fln--logical-end start))
(next (flan-fln--next-code last))) (next (flan-fln--next-code last)))
(while (and next (while (and next
(not (and (flan-fln--closer-line-p next)
(not (flan-fln--continues-p next start))))
(or (> (flan-fln--indent-at next) indent) (or (> (flan-fln--indent-at next) indent)
(flan-fln--continuation-p next) (flan-fln--continuation-p next)
(and (not no-clauses) (and (not no-clauses)
@ -355,9 +414,26 @@ line's own block only."
next (flan-fln--next-code last))) next (flan-fln--next-code last)))
last)) last))
(defun flan-fln--trim-closers (beg end)
"END, less the closers before it whose brackets open before BEG.
The last statement of a lambda's block inside a call ends with the call's `)'
on its line, which is not the statement's."
(save-excursion
(goto-char end)
(let (done)
(while (not done)
(skip-chars-backward " \t" beg)
(let ((open (and (> (point) beg) (memq (char-before) '(?\) ?\] ?\}))
(nth 1 (save-excursion (syntax-ppss (1- (point))))))))
(if (and open (< open beg))
(backward-char 1)
(setq done t))))
(point))))
(defun flan-fln--span (start last) (defun flan-fln--span (start last)
"(BEG . END) from the text of START's line to the code end of LAST's." "(BEG . END) from the text of START's line to the code end of LAST's."
(cons (flan-fln--first-char start) (flan-fln--code-end last))) (let ((beg (flan-fln--first-char start)))
(cons beg (flan-fln--trim-closers beg (flan-fln--code-end last)))))
(defun flan-fln--statement-bounds (start) (defun flan-fln--statement-bounds (start)
"Bounds of the statement whose first line is START." "Bounds of the statement whose first line is START."
@ -687,7 +763,11 @@ forms, where the clause line itself is not."
(g (flan-fln--group-bounds pos))) (g (flan-fln--group-bounds pos)))
(cond (cond
((and arm (< pos (plist-get arm :arrow))) (plist-get arm :value)) ((and arm (< pos (plist-get arm :arrow))) (plist-get arm :value))
((and g (cdr g) (> (car g) (car b))) ;; Not the brackets a lambda's block is in: a line of the block is a
;; statement of its own.
((and g (cdr g) (> (car g) (car b))
(not (let ((a (flan-fln--lambda-arrow pos)))
(and a (< (car g) a) (< a pos)))))
(cons (flan-fln--group-form-start (car g)) (cdr g))) (cons (flan-fln--group-form-start (car g)) (cdr g)))
(arm (plist-get arm :value)) (arm (plist-get arm :value))
((and l (flan-fln--clause-line-p l)) (flan-fln--clause-target l)) ((and l (flan-fln--clause-line-p l)) (flan-fln--clause-target l))
@ -1158,6 +1238,8 @@ Before it at the same level, else out to the line that owns this block."
(let ((bol (line-beginning-position))) (let ((bol (line-beginning-position)))
(or (looking-back ":" bol) (or (looking-back ":" bol)
(looking-back "[ \t]->" bol) (looking-back "[ \t]->" bol)
;; A lambda's header, at the top of a line or inside brackets.
(looking-back "[ \t]=>" bol)
;; `let x =' and `def colors =' with the value as a block, ;; `let x =' and `def colors =' with the value as a block,
;; which the author's list leaves out and the reader reads. ;; which the author's list leaves out and the reader reads.
(looking-back "[ \t]=" bol)))))) (looking-back "[ \t]=" bol))))))
@ -1177,11 +1259,28 @@ Before it at the same level, else out to the line that owns this block."
"Columns TAB offers POS's line outside brackets, deepest first." "Columns TAB offers POS's line outside brackets, deepest first."
(let* ((prev (flan-fln--prev-code pos)) (let* ((prev (flan-fln--prev-code pos))
(stack (mapcar #'car (flan-fln--stack pos)))) (stack (mapcar #'car (flan-fln--stack pos))))
(if (and prev (flan-fln--opener-p (flan-fln--logical-start prev) prev)) (cond
(cons (+ (flan-fln--indent-at (flan-fln--logical-start prev)) ;; A lambda's block goes under the line its `=>' ends, which may be a
flan-fln-indent-offset) ;; line of a call wrapped inside its brackets.
stack) ((and prev (flan-fln--ends-in-arrow-p prev))
stack))) (cons (+ (flan-fln--indent-at prev) flan-fln-indent-offset) stack))
((and prev (flan-fln--opener-p (flan-fln--logical-start prev) prev))
(cons (+ (flan-fln--indent-at (flan-fln--logical-start prev))
flan-fln-indent-offset)
stack))
(t stack))))
(defun flan-fln--block-levels (pos)
"Columns TAB offers POS's line at a block's level, deepest first.
In a lambda's block inside brackets, only those right of the line its `=>'
ends: a line at or left of it would be outside the block, still inside the
brackets, which the reader refuses."
(let ((arrow (flan-fln--lambda-arrow pos)))
(if (not arrow)
(flan-fln--levels pos)
(let ((base (flan-fln--indent-at arrow)))
(or (seq-filter (lambda (c) (> c base)) (flan-fln--levels pos))
(list (+ base flan-fln-indent-offset)))))))
(defun flan-fln--clause-columns (word pos) (defun flan-fln--clause-columns (word pos)
"Columns of the lines above POS a clause WORD may sit under, deepest first." "Columns of the lines above POS a clause WORD may sit under, deepest first."
@ -1226,19 +1325,20 @@ opening line's column."
(let ((s (syntax-ppss (point)))) (let ((s (syntax-ppss (point))))
(cond (cond
((nth 3 s) nil) ((nth 3 s) nil)
((> (car s) 0) (list (flan-fln--bracket-column (nth 1 s)))) ((and (> (car s) 0) (not (flan-fln--lambda-arrow (point))))
(list (flan-fln--bracket-column (nth 1 s))))
(t (t
(let ((prev (flan-fln--prev-code (point)))) (let ((prev (flan-fln--prev-code (point))))
(cond (cond
((null prev) (list 0)) ((null prev) (list 0))
((save-excursion (back-to-indentation) (looking-at flan-fln--clause-re)) ((save-excursion (back-to-indentation) (looking-at flan-fln--clause-re))
(or (flan-fln--clause-columns (match-string-no-properties 1) (point)) (or (flan-fln--clause-columns (match-string-no-properties 1) (point))
(flan-fln--levels (point)))) (flan-fln--block-levels (point))))
((or (flan-fln--starts-with-op-p (point)) ((or (flan-fln--starts-with-op-p (point))
(flan-fln--ends-in-op-p prev)) (flan-fln--ends-in-op-p prev))
(list (+ (flan-fln--indent-at (flan-fln--logical-start prev)) (list (+ (flan-fln--indent-at (flan-fln--logical-start prev))
flan-fln-indent-offset))) flan-fln-indent-offset)))
(t (flan-fln--levels (point)))))))))) (t (flan-fln--block-levels (point))))))))))
(defun flan-fln-indent-line () (defun flan-fln-indent-line ()
"Indent the line to a block column. "Indent the line to a block column.
@ -1284,11 +1384,13 @@ Never re-indents a line against the others: the columns are the program."
(if (and (= arg 1) (not (use-region-p)) (if (and (= arg 1) (not (use-region-p))
(> (current-column) 0) (> (current-column) 0)
(= (current-column) (current-indentation)) (= (current-column) (current-indentation))
(not (flan-fln--in-open-p (point)))) (not (flan-fln--bracketed-p (point))))
(let ((cur (current-indentation))) (let ((cur (current-indentation)))
(indent-line-to (or (seq-find (lambda (c) (< c cur)) (indent-line-to (or (seq-find (lambda (c) (< c cur))
(flan-fln--levels (point))) (flan-fln--block-levels (point)))
0))) ;; A lambda's block inside brackets has no
;; level left of its own.
(if (flan-fln--lambda-arrow (point)) cur 0))))
(let ((cmd (or (command-remapping 'delete-backward-char) (let ((cmd (or (command-remapping 'delete-backward-char)
#'delete-backward-char))) #'delete-backward-char)))
(setq this-command cmd) (setq this-command cmd)
@ -1306,7 +1408,7 @@ Run when the word is finished by a space or a newline."
(if nl (line-end-position) (point))))) (if nl (line-end-position) (point)))))
(when (and (string-match "\\`[ \t]*\\(else\\|elif\\|on\\|restart\\)[ \t]*\\'" (when (and (string-match "\\`[ \t]*\\(else\\|elif\\|on\\|restart\\)[ \t]*\\'"
text) text)
(not (flan-fln--in-open-p (point)))) (not (flan-fln--bracketed-p (point))))
(let ((cols (flan-fln--clause-columns (match-string 1 text) (point)))) (let ((cols (flan-fln--clause-columns (match-string 1 text) (point))))
(when (and cols (not (memq (current-indentation) cols))) (when (and cols (not (memq (current-indentation) cols)))
(indent-line-to (car cols)))))))) (indent-line-to (car cols))))))))
@ -1567,6 +1669,8 @@ lambda or a `Fn(...)' type, and not after a match arm's."
1 font-lock-function-name-face) 1 font-lock-function-name-face)
;; A lambda's `fn', glued to its parameters. ;; A lambda's `fn', glued to its parameters.
("\\(?:^\\|[ \t=(,]\\)\\(fn\\)(" 1 font-lock-keyword-face) ("\\(?:^\\|[ \t=(,]\\)\\(fn\\)(" 1 font-lock-keyword-face)
;; And the `=>' its body follows.
("[ \t]\\(=>\\)\\(?:[ \t]\\|$\\)" 1 font-lock-keyword-face)
;; An enum member written `Dir.north', a constant as `:north' is. ;; An enum member written `Dir.north', a constant as `:north' is.
("\\_<[A-Z][^][ \t\n(){},;\":.]*\\.[^][ \t\n(){},;\":.]+\\_>" ("\\_<[A-Z][^][ \t\n(){},;\":.]*\\.[^][ \t\n(){},;\":.]+\\_>"
. font-lock-constant-face) . font-lock-constant-face)

View File

@ -63,11 +63,18 @@ fn size(k: i64) -> i64
else 2 else 2
fn lam(k: i64) -> i64 fn lam(k: i64) -> i64
let add = fn(a: i64, b: i64) -> i64 = a + b let add = fn(a: i64, b: i64) -> i64 => a + b
let dbl = fn(a: i64) -> i64 let dbl = fn(a: i64) -> i64 =>
a * 2 a * 2
add(dbl(k), 1) add(dbl(k), 1)
fn app(x: i64, f: Fn(i64) -> i64) -> i64 = f(x)
fn lam2(k: i64) -> i64
app(k, fn(a) =>
let b = a + 1
b * 2)
fn rs() -> i64 fn rs() -> i64
restart-case restart-case
3 3
@ -92,6 +99,9 @@ comment():
elif 1 > 2 or elif 1 > 2 or
3 > 4 3 > 4
0 0
app(5, fn(a) =>
a * 3
)
if 2 < 1 then 5 if 2 < 1 then 5
elif 2 == 1 then 6 elif 2 == 1 then 6
else 7 else 7
@ -208,6 +218,7 @@ comment():
'(("fn sign2" "(sign2 0)" "0") '(("fn sign2" "(sign2 0)" "0")
("fn size" "(size 20)" "2") ("fn size" "(size 20)" "2")
("fn lam" "(lam 3)" "7") ("fn lam" "(lam 3)" "7")
("fn lam2" "(lam2 3)" "8")
("fn rs" "(rs)" "3") ("fn rs" "(rs)" "3")
("fn dir" "(dir :north)" "7"))) ("fn dir" "(dir :north)" "7")))
(funcall goto needle) (funcall goto needle)
@ -262,6 +273,16 @@ comment():
(unless (and (car r) (string-suffix-p (cadr r) (car r))) (unless (and (car r) (string-suffix-p (cadr r) (car r)))
(message " want ...%s\n got %S" (cadr r) (car r)))))) (message " want ...%s\n got %S" (cadr r) (car r))))))
(flan-clear-errors) (flan-clear-errors)
;; A lambda's block inside a call's brackets: the call goes whole
;; from its header line, and reads.
(funcall goto "app(5")
(flan-fln-eval-statement)
(test-flan--check (funcall name "C-c C-e on a call with a block lambda sends it whole, and it reads")
(funcall shows "15"))
(funcall goto "app(5, fn(a) =>" t)
(flan-fln-eval-last)
(test-flan--check (funcall name "C-x C-e at the end of its header line too")
(funcall shows "15"))
(funcall goto "if 2 > 1" t) (funcall goto "if 2 > 1" t)
(flan-fln-eval-last) (flan-fln-eval-last)
(test-flan--check (funcall name "C-x C-e at the end of an if line evaluates the condition") (test-flan--check (funcall name "C-x C-e at the end of an if line evaluates the condition")
@ -281,6 +302,9 @@ comment():
("else 2" "a one-line else after a block, at its value") ("else 2" "a one-line else after a block, at its value")
("a: i64, b" "a typed lambda, from its fn") ("a: i64, b" "a typed lambda, from its fn")
("a * 2" "a typed lambda's block") ("a * 2" "a typed lambda's block")
("a) =>\n let b" "a block lambda in a call, from its fn")
("let b = a + 1" "a statement in a block lambda's block")
("b * 2" "the block's last statement, less the call's closer")
("restart retry" "a restart with a report, at its block") ("restart retry" "a restart with a report, at its block")
("Dir.north" "an enum member's arm, at its value") ("Dir.north" "an enum member's arm, at its value")
("let b = 2" "a let the let above takes in, at its value") ("let b = 2" "a let the let above takes in, at its value")

View File

@ -598,8 +598,8 @@ fn size(n: i64) -> i64
else 2 else 2
fn k(n: i64) -> i64 fn k(n: i64) -> i64
let add = fn(a: i64, b) -> i64 = a + b let add = fn(a: i64, b) -> i64 => a + b
let dbl = fn(a: Fn(i64) -> i64, b: i64) -> i64 let dbl = fn(a: Fn(i64) -> i64, b: i64) -> i64 =>
a(b) * 2 a(b) * 2
let r = match n let r = match n
0 -> 1 0 -> 1
@ -689,7 +689,7 @@ of its line with AT-END."
(null (funcall face "x:"))))) (null (funcall face "x:")))))
(test-flan-fln--in "fn f(d: Dir) -> i64 (test-flan-fln--in "fn f(d: Dir) -> i64
let g = fn(a: Fn(i64) -> i64, b) -> Vec(i64) let g = fn(a: Fn(i64) -> i64, b) -> Vec(i64) =>
a(b) a(b)
restart-case restart-case
3 3
@ -779,14 +779,14 @@ of its line with AT-END."
("let r = if c" "a let's if") ("let r = if c" "a let's if")
("x = if c" "an assignment's if") ("x = if c" "an assignment's if")
("let r = handler-case" "a let's handler-case") ("let r = handler-case" "a let's handler-case")
("let f = fn(a, b)" "a lambda header") ("let f = fn(a, b) =>" "a lambda header")
("let f = fn(a: i64, b) -> i64" "a typed lambda header") ("let f = fn(a: i64, b) -> i64 =>" "a typed lambda header")
("let f = fn(g: Fn(i64) -> i64) -> Option(i64)" "one with a function type in it") ("let f = fn(g: Fn(i64) -> i64) -> Option(i64) =>" "one with a function type in it")
("fn(a: i64) -> i64" "a typed lambda as a statement"))) ("fn(a: i64) -> i64 =>" "a typed lambda as a statement")))
(test-flan-fln--is (format "unless its value goes on under it: %s" (cadr c)) (test-flan-fln--is (format "unless its value goes on under it: %s" (cadr c))
(test-flan-fln--tabs (concat "fn f()\n " (car c) "\n|") 1) 4)) (test-flan-fln--tabs (concat "fn f()\n " (car c) "\n|") 1) 4))
(test-flan-fln--is "but not a typed lambda with its body on the line" (test-flan-fln--is "but not a typed lambda with its body on the line"
(test-flan-fln--tabs "fn f()\n let f = fn(a: i64) -> i64 = a\n|" 1) 2) (test-flan-fln--tabs "fn f()\n let f = fn(a: i64) -> i64 => a\n|" 1) 2)
(test-flan-fln--is "a one-line fn whose value is a match opens it" (test-flan-fln--is "a one-line fn whose value is a match opens it"
(test-flan-fln--tabs "fn f(x) = match x\n|" 1) 2) (test-flan-fln--tabs "fn f(x) = match x\n|" 1) 2)
(test-flan-fln--is "no deeper after a one-line else" (test-flan-fln--is "no deeper after a one-line else"
@ -849,6 +849,82 @@ of its line with AT-END."
(buffer-string) (buffer-string)
"fn f() -> ()\n if a\n while x\n y\n\n")) "fn f() -> ()\n if a\n while x\n y\n\n"))
;;; A lambda's block inside brackets
(defconst test-flan-fln--lam
"fn f(xs) -> i64
let n = 1
sort-by(xs, fn(a, b) =>
let d = a - b
d < n)
map(xs, fn(x) =>
g(x)
x
)
h(n)
")
(defun test-flan-fln--lam-at (needle fn &optional at-end)
"What FN sends with point at NEEDLE in the lambda program, or at the end of
its line with AT-END."
(test-flan-fln--in (test-flan-fln--at test-flan-fln--lam needle)
(when at-end (end-of-line))
(let ((r (test-flan-fln--sending (funcall fn))))
(and r (list (test-flan-fln--sent-code r) (plist-get r :pause))))))
(test-flan-fln--in (test-flan-fln--at test-flan-fln--lam "sort-by")
(test-flan-fln--is "a call with a block lambda is one statement, block and closer too"
(test-flan-fln--thing 'flan-fln-statement)
"sort-by(xs, fn(a, b) =>\n let d = a - b\n d < n)")
(test-flan-fln--is "its body is the lambda's block, less the call's closer"
(test-flan-fln--thing 'flan-fln-body)
"let d = a - b\n d < n"))
(test-flan-fln--in (test-flan-fln--at test-flan-fln--lam "d < n")
(test-flan-fln--is "a line of the block is a statement of its own, less the closer"
(test-flan-fln--thing 'flan-fln-statement) "d < n"))
(test-flan-fln--in (test-flan-fln--at test-flan-fln--lam " )")
(test-flan-fln--is "a closer on a line of its own belongs to the call"
(test-flan-fln--thing 'flan-fln-statement)
"map(xs, fn(x) =>\n g(x)\n x\n )"))
(test-flan-fln--in (test-flan-fln--at test-flan-fln--lam " x\n")
(test-flan-fln--is "and not to the statement above it"
(test-flan-fln--thing 'flan-fln-statement) "x"))
(test-flan-fln--is "C-c C-e on the header sends the call, block and all"
(car (test-flan-fln--lam-at "sort-by" #'flan-fln-eval-statement))
"sort-by(xs, fn(a, b) =>\n let d = a - b\n d < n)")
(test-flan-fln--is "and C-x C-e at the header's end"
(car (test-flan-fln--lam-at "sort-by" #'flan-fln-eval-last t))
"sort-by(xs, fn(a, b) =>\n let d = a - b\n d < n)")
(test-flan-fln--is "C-x C-e at the end of the block's last line sends its statement"
(car (test-flan-fln--lam-at "d < n" #'flan-fln-eval-last t))
"d < n")
(test-flan-fln--is "C-c C-e on a line of the block sends that statement"
(car (test-flan-fln--lam-at "g(x)" #'flan-fln-eval-statement))
"g(x)")
(dolist (c '(("let d" (4 5) "a statement in the block, where the reader starts it")
("g(x)" (7 5) "one in a block with its closer on a line of its own")
("a, b)" (3 15) "the lambda, from its fn")
("xs, fn(a" (3 3) "the call, from its name")))
(test-flan-fln--is (format "C-u C-c C-c marks %s" (nth 2 c))
(cadr (test-flan-fln--lam-at (car c) (lambda () (flan-fln-eval-defun '(4)))))
(cadr c)))
(test-flan-fln--is "TAB after a => inside brackets goes one level in"
(test-flan-fln--tabs "fn f()\n sort-by(xs, fn(a, b) =>\n|" 1) 4)
(test-flan-fln--is "and a line of the block stays in it"
(test-flan-fln--tabs "fn f()\n sort-by(xs, fn(a, b) =>\n g(a)\n| a < b)" 1) 4)
(test-flan-fln--is "under a header on a wrapped argument line, in from that line"
(test-flan-fln--tabs "f(a,\n fn(b) =>\n|" 1) 4)
(test-flan-fln--is "a closer on its own line goes to the call's column"
(test-flan-fln--tabs "fn f()\n m(xs, fn(x) =>\n x\n|)" 1) 2)
(test-flan-fln--is "inside a bracket in the block, under its first argument"
(test-flan-fln--tabs "fn f()\n m(xs, fn(x) =>\n g(x,\n|y))" 1) 6)
(test-flan-fln--in "fn f()\n m(xs, fn(x) => x)\n"
(font-lock-ensure)
(test-flan-fln--is "=> is a keyword"
(save-excursion (goto-char (point-min)) (search-forward "=>")
(get-text-property (match-beginning 0) 'face))
'font-lock-keyword-face))
;;; Block editing ;;; Block editing
(test-flan-fln--in "fn f() -> ()\n if a\n |b()\n c()\n d()\n" (test-flan-fln--in "fn f() -> ()\n if a\n |b()\n c()\n d()\n"

View File

@ -117,10 +117,10 @@ Each item: the proposal, then the reason in one line.
trailing block is allowed. Make it parser-driven, the way GDScript's trailing block is allowed. Make it parser-driven, the way GDScript's
`push_multiline` is (`gdscript_parser.cpp` 658-672, 3695-3770), not a paren `push_multiline` is (`gdscript_parser.cpp` 658-672, 3695-3770), not a paren
counter in the lexer, or a block inside a call can't work. counter in the lexer, or a block inside a call can't work.
*Built as a depth counter instead: inside brackets a line break is always *Built as a depth counter instead: inside brackets a line break is
whitespace, so no block opens inside a call's parentheses (§3.1's blocks all whitespace, with one exception. A `=>` that ends a line inside brackets opens
open after the `)`; a lambda with a block body is a statement or a value, a lambda's block there, laid out as at the top level against the column its
`let f = fn(x)` plus a block).* line starts at, and the block ends where the enclosing bracket closes.*
- **Continuation outside brackets:** a line that starts with a spaced infix - **Continuation outside brackets:** a line that starts with a spaced infix
operator (`+`, `and`, `==`, …) continues the previous line; so does a line operator (`+`, `and`, `==`, …) continues the previous line; so does a line
after one that ends in a spaced infix operator. (F# `LexFilter.fs` 360-380, after one that ends in a spaced infix operator. (F# `LexFilter.fs` 360-380,
@ -267,11 +267,28 @@ Each item: the proposal, then the reason in one line.
- **Unit:** `()` as a statement reads `(do)`; in a type it is `()`. **Built**; - **Unit:** `()` as a statement reads `(do)`; in a type it is `()`. **Built**;
inside an expression `()` stays `()`, and the printer writes a lone `()` inside an expression `()` stays `()`, and the printer writes a lone `()`
statement as `(())`. A bare `()` in a one-line body slot (`fn f() -> () = ()`, statement as `(())`. A bare `()` in a one-line body slot (`fn f() -> () = ()`,
`_ -> ()`, `fn() = ()`, `then ()`) is a statement too, and reads `(do)`. `_ -> ()`, `fn() => ()`, `then ()`) is a statement too, and reads `(do)`.
- **Lambda:** `fn(i, j) = i * 10 + j`, or `fn(i, j)` plus a block. **Built**; - **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`. its parameters are bare names, as `(fn [i j] …)` wants, with no `dyn`.
`fn(…)` followed by anything else is the fallback call. A lambda may state `fn(…)` followed by anything else is the fallback call. A lambda may state
its types, `fn(a: C, b) -> bool = …` or plus a block (section 3, item 7). its types, `fn(a: C, b) -> bool => …` or plus a block (section 3, item 7).
`=>` is a lambda's only spelling: `fn(a) = x` and a lambda header with a
block under it and no `=>` are refused, with the `=>` form as the fix.
Named functions keep `=`. A block lambda may sit inside brackets:
```
sort-by(slice(xs), fn(a, b) =>
let d = a.n - b.n
d < 0)
```
The block ends where the brackets close, with the `)` at the end of its
last line or on a line of its own at the call's column. It is the last
thing in them: a comma after the block is refused (so a call takes one
block lambda, as its last argument; name any other with `let`), as is a
line inside the brackets at or left of the column the `=>` line starts at.
Block lambdas nest, each block ending at its own brackets. `flan convert`
writes a call whose last argument is a lambda with a block this way.
### Definitions ### Definitions
@ -376,16 +393,15 @@ Settled 2026-09-26, after writing programs by hand (`test/syntax/handwritten/`):
is refused at its last line: the first `else` took `if b then y` as its is refused at its last line: the first `else` took `if b then y` as its
value, and the chain is `if a then x else if b then y else z` on one line, value, and the chain is `if a then x else if b then y else z` on one line,
or `elif b then y` on the second. or `elif b then y` on the second.
7. **Typed lambdas.** `fn(a: C, b) -> R = body`, or plus a block, reads 7. **Typed lambdas.** `fn(a: C, b) -> R => body`, or plus a block, reads
`(the (Fn [C dyn] R) (fn [a b] body))`: the paren `fn` has no typed `(the (Fn [C dyn] R) (fn [a b] body))`: the paren `fn` has no typed
parameters, and `the` is how a value states its type, as in parameters, and `the` is how a value states its type, as in
`let x: T = v`. An untyped parameter is `dyn`; the return type is `let x: T = v`. An untyped parameter is `dyn`; the return type is
required. Where a `CFn` of the same signature is wanted, the literal is required. Where a `CFn` of the same signature is wanted, the literal is
that `CFn`; at a generic's `CFn($t) -> $t` parameter the literal is a that `CFn`; at a generic's `CFn($t) -> $t` parameter the literal is a
`CFn` at its own types, which bind `$t` as any argument's would. The `CFn` at its own types, which bind `$t` as any argument's would. The
printer writes that form back as the typed lambda. A block lambda cannot printer writes that form back as the typed lambda. A typed block lambda
sit inside a call's brackets; the refusal shows the typed `let` form to sits inside brackets as an untyped one does.
bind it with.
8. **`Dir.north` is the enum member `:north`**, in a value and in a match 8. **`Dir.north` is the enum member `:north`**, in a value and in a match
pattern, in both syntaxes. `:north` stays. A local named `Dir` shadows the pattern, in both syntaxes. `:north` stays. A local named `Dir` shadows the
enum as a local shadows any global: `Dir.north` is then its field. enum as a local shadows any global: `Dir.north` is then its field.
@ -453,7 +469,7 @@ Each step lands on its own, with `dune test --root .` green.
(`ast.ml:491-492`). (`ast.ml:491-492`).
**Built** (`emacs/flan-fln-mode.el`; keys and objects in `emacs/MANUAL.md`, **Built** (`emacs/flan-fln-mode.el`; keys and objects in `emacs/MANUAL.md`,
"Indented files"). A line ending in `=` or `fn(…)` also opens a block for "Indented files"). A line ending in `=` or `=>` also opens a block for
TAB, and a body is its statement's own block, up to its first clause. TAB, and a body is its statement's own block, up to its first clause.
6. **Return-type inference** in `Check`, with the recursion refusal and the 6. **Return-type inference** in `Check`, with the recursion refusal and the
stale-caller cause. This is independent of steps 1-5 once the marker exists. stale-caller cause. This is independent of steps 1-5 once the marker exists.