From 6e56fce7fa67faa164780e0b4238c1d021e5c884 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sat, 26 Sep 2026 05:27:28 +0700 Subject: [PATCH] flan-fln-mode reads a lambda's block inside brackets as statements, and the spec and manual write lambdas with => --- TODO.org | 9 +- emacs/MANUAL.md | 4 +- emacs/flan-fln-mode.el | 158 ++++++++++++++++++++++++++++++------ emacs/test-flan-fln-live.el | 28 ++++++- emacs/test-flan-fln.el | 92 +++++++++++++++++++-- spec-syntax.md | 40 ++++++--- 6 files changed, 275 insertions(+), 56 deletions(-) diff --git a/TODO.org b/TODO.org index 10285f11..f2e8617e 100644 --- a/TODO.org +++ b/TODO.org @@ -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 diff --git a/emacs/MANUAL.md b/emacs/MANUAL.md index 36e24899..02f52e10 100644 --- a/emacs/MANUAL.md +++ b/emacs/MANUAL.md @@ -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-` / `M-` | 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. diff --git a/emacs/flan-fln-mode.el b/emacs/flan-fln-mode.el index 34b3cdad..b8e540b9 100644 --- a/emacs/flan-fln-mode.el +++ b/emacs/flan-fln-mode.el @@ -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) diff --git a/emacs/test-flan-fln-live.el b/emacs/test-flan-fln-live.el index 5c36ff93..e694ab88 100644 --- a/emacs/test-flan-fln-live.el +++ b/emacs/test-flan-fln-live.el @@ -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") diff --git a/emacs/test-flan-fln.el b/emacs/test-flan-fln.el index 10721aaf..a563728e 100644 --- a/emacs/test-flan-fln.el +++ b/emacs/test-flan-fln.el @@ -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" diff --git a/spec-syntax.md b/spec-syntax.md index 0416c89a..ded0040a 100644 --- a/spec-syntax.md +++ b/spec-syntax.md @@ -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.