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
without removing them.
** NEXT Lambdas are written with =>
Decided 2026-09-26: a lambda's body follows ~=>~, and ~=>~ is its only spelling
(~fn(a) = x~ is refused for a lambda; named functions keep ~=~). A header ending in ~=>~
takes an indented block even inside brackets, closing where the brackets close:
~sort-by(xs, fn(a, b) =>~ plus a block.
** DONE Lambdas are written with =>
CLOSED: [2026-09-26]
A block lambda inside brackets is the last thing in them, its block ending where they close: a comma
after it (so a second block lambda, or one not last) is refused, and the fix names it with ~let~.
** 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

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) |
| `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 |
| `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 |
| `C-c <` / `C-c >` | `<` / `>` | shift the region's lines a level |
| `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`.
- **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.
- **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.

View File

@ -25,7 +25,8 @@
;; leaves open, the lines an operator continues, and the
;; `else'/`elif'/`on'/`restart' clauses at its own column. Blank
;; 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,
;; up to its first clause.
;; 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)))
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)
"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
starting with one: the reader's three ways a line break is not a new line."
(or (flan-fln--in-open-p pos)
starting with one: the reader's three ways a line break is not a new line.
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)
(let ((p (flan-fln--prev-code pos)))
(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)
"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."
@ -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)
"The first line of the line POS is on, after continuation lines are joined."
(let ((bol (flan-fln--bol pos)) p)
(while (and (flan-fln--continuation-p bol)
(setq p (flan-fln--prev-code bol)))
(setq bol p))
(while (cond
;; A closer's line belongs with the line its bracket opens on,
;; 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))
(defun flan-fln--logical-end (pos)
"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))
(flan-fln--continuation-p n))
(flan-fln--continues-p n start))
(setq bol n))
bol))
@ -309,18 +361,23 @@ depth, outside strings and comments, or nil."
(flan-fln--joined-end l))))
(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)
"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
(goto-char pos)
(and (looking-at "fn(")
(let ((close (ignore-errors (scan-lists (+ pos 2) 1 0))))
(and close (<= close end)
(progn (goto-char close) (skip-chars-forward " \t")
(or (>= (point) end)
(and (looking-at "->[ \t]")
(not (flan-fln--find-top "[ \t]=[ \t]" (point) end))))))))))
(progn (goto-char end) (looking-back "[ \t]=>" close)))))))
(defun flan-fln--value-opens-p (l)
"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))
(next (flan-fln--next-code last)))
(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)
(flan-fln--continuation-p next)
(and (not no-clauses)
@ -355,9 +414,26 @@ line's own block only."
next (flan-fln--next-code 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)
"(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)
"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)))
(cond
((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)))
(arm (plist-get arm :value))
((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)))
(or (looking-back ":" 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,
;; which the author's list leaves out and the reader reads.
(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."
(let* ((prev (flan-fln--prev-code pos))
(stack (mapcar #'car (flan-fln--stack pos))))
(if (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)
stack)))
(cond
;; A lambda's block goes under the line its `=>' ends, which may be a
;; line of a call wrapped inside its brackets.
((and prev (flan-fln--ends-in-arrow-p prev))
(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)
"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))))
(cond
((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
(let ((prev (flan-fln--prev-code (point))))
(cond
((null prev) (list 0))
((save-excursion (back-to-indentation) (looking-at flan-fln--clause-re))
(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))
(flan-fln--ends-in-op-p prev))
(list (+ (flan-fln--indent-at (flan-fln--logical-start prev))
flan-fln-indent-offset)))
(t (flan-fln--levels (point))))))))))
(t (flan-fln--block-levels (point))))))))))
(defun flan-fln-indent-line ()
"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))
(> (current-column) 0)
(= (current-column) (current-indentation))
(not (flan-fln--in-open-p (point))))
(not (flan-fln--bracketed-p (point))))
(let ((cur (current-indentation)))
(indent-line-to (or (seq-find (lambda (c) (< c cur))
(flan-fln--levels (point)))
0)))
(flan-fln--block-levels (point)))
;; 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)
#'delete-backward-char)))
(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)))))
(when (and (string-match "\\`[ \t]*\\(else\\|elif\\|on\\|restart\\)[ \t]*\\'"
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))))
(when (and cols (not (memq (current-indentation) 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)
;; A lambda's `fn', glued to its parameters.
("\\(?:^\\|[ \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.
("\\_<[A-Z][^][ \t\n(){},;\":.]*\\.[^][ \t\n(){},;\":.]+\\_>"
. font-lock-constant-face)

View File

@ -63,11 +63,18 @@ fn size(k: i64) -> i64
else 2
fn lam(k: i64) -> i64
let add = fn(a: i64, b: i64) -> i64 = a + b
let dbl = fn(a: i64) -> i64
let add = fn(a: i64, b: i64) -> i64 => a + b
let dbl = fn(a: i64) -> i64 =>
a * 2
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
restart-case
3
@ -92,6 +99,9 @@ comment():
elif 1 > 2 or
3 > 4
0
app(5, fn(a) =>
a * 3
)
if 2 < 1 then 5
elif 2 == 1 then 6
else 7
@ -208,6 +218,7 @@ comment():
'(("fn sign2" "(sign2 0)" "0")
("fn size" "(size 20)" "2")
("fn lam" "(lam 3)" "7")
("fn lam2" "(lam2 3)" "8")
("fn rs" "(rs)" "3")
("fn dir" "(dir :north)" "7")))
(funcall goto needle)
@ -262,6 +273,16 @@ comment():
(unless (and (car r) (string-suffix-p (cadr r) (car r)))
(message " want ...%s\n got %S" (cadr r) (car r))))))
(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)
(flan-fln-eval-last)
(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")
("a: i64, b" "a typed lambda, from its fn")
("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")
("Dir.north" "an enum member's arm, 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
fn k(n: i64) -> i64
let add = fn(a: i64, b) -> i64 = a + b
let dbl = fn(a: Fn(i64) -> i64, b: i64) -> i64
let add = fn(a: i64, b) -> i64 => a + b
let dbl = fn(a: Fn(i64) -> i64, b: i64) -> i64 =>
a(b) * 2
let r = match n
0 -> 1
@ -689,7 +689,7 @@ of its line with AT-END."
(null (funcall face "x:")))))
(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)
restart-case
3
@ -779,14 +779,14 @@ of its line with AT-END."
("let r = if c" "a let's if")
("x = if c" "an assignment's if")
("let r = handler-case" "a let's handler-case")
("let f = fn(a, b)" "a 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")
("fn(a: i64) -> i64" "a typed lambda as a statement")))
("let f = fn(a, b) =>" "a 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")
("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--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--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--tabs "fn f(x) = match x\n|" 1) 2)
(test-flan-fln--is "no deeper after a one-line else"
@ -849,6 +849,82 @@ of its line with AT-END."
(buffer-string)
"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
(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
`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.
*Built as a depth counter instead: inside brackets a line break is always
whitespace, so no block opens inside a call's parentheses (§3.1's blocks all
open after the `)`; a lambda with a block body is a statement or a value,
`let f = fn(x)` plus a block).*
*Built as a depth counter instead: inside brackets a line break is
whitespace, with one exception. A `=>` that ends a line inside brackets opens
a lambda's block there, laid out as at the top level against the column its
line starts at, and the block ends where the enclosing bracket closes.*
- **Continuation outside brackets:** a line that starts with a spaced infix
operator (`+`, `and`, `==`, …) continues the previous line; so does a line
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**;
inside an expression `()` stays `()`, and the printer writes a lone `()`
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**;
`_ -> ()`, `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. 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
@ -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
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.
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
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
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
`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
sit inside a call's brackets; the refusal shows the typed `let` form to
bind it with.
printer writes that form back as the typed lambda. A typed block lambda
sits inside brackets as an untyped one does.
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
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`).
**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.
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.