From 25e7db944fc643deeace3591010a8faea8833cab Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sat, 26 Sep 2026 06:32:09 +0700 Subject: [PATCH] In a .fln buffer a let's binding lines belong to its statement, TAB offers the column under its first name, and C-c C-e sends the group --- TODO.org | 5 --- emacs/MANUAL.md | 6 +-- emacs/flan-fln-mode.el | 78 ++++++++++++++++++++++++++++++++----- emacs/test-flan-fln-live.el | 17 ++++++++ emacs/test-flan-fln.el | 38 ++++++++++++++++++ 5 files changed, 127 insertions(+), 17 deletions(-) diff --git a/TODO.org b/TODO.org index ea4ee6f5..459d0ed0 100644 --- a/TODO.org +++ b/TODO.org @@ -652,11 +652,6 @@ after it (so a second block lambda, or one not last) is refused, and the fix nam Held 2026-09-26 by the author: F#-style ~f x y~ or Nim-style one-argument calls without parentheses. Collides with space-separated vector and index elements. -** NEXT A let takes several bindings on indented lines -Decided 2026-09-26: lines indented under a ~let~ that are ~name = v~ or ~name: T = v~ are -more bindings of the same let; anything else there stays refused. flan convert writes -consecutive lets this way. - ** TODO Hard-coded code in messages is still paren syntax in a .fln file Types follow the code's syntax now (=Types.spell=). Hints written into a message's text — =(Ptr %s)=, =(clone v)=, =(the T x)= in most of =check.ml= and =parse.ml=, the diff --git a/emacs/MANUAL.md b/emacs/MANUAL.md index 3db7b4f1..d5901c44 100644 --- a/emacs/MANUAL.md +++ b/emacs/MANUAL.md @@ -1197,7 +1197,7 @@ Use `C-c C-g` if you need frames. | `C-c C-c`, `C-M-x` | same | the top-level form: a declaration installed, anything else evaluated | | `C-u C-c C-c` | same | ...and stop at the innermost bracket group, else the statement on point's line: an elif's condition, an else's block or its value on the line, a match arm's value (`C-u C-u`: on entry) | | `C-x C-e` | same, cursor on the line's last character | at a line's end, the innermost statement ending there: a match arm's value, an if/elif/while condition, or the whole statement a header or clause line opens (a one-line `if c then a` with `else` under it included); elsewhere, the term before point | -| `C-c C-e` | same | the statement at point with its body and clauses, or the region's whole lines; on a `let`, the `let` and the rest of its block | +| `C-c C-e` | same | the statement at point with its body and clauses, or the region's whole lines; on a `let` or one of its binding lines, the `let`, its bindings and the rest of its block | | `C-c C-n` | same | `C-c C-e`, then move to the next statement | | `C-c C-s` | same | step through the top-level `fn` at point | | `C-c C-k` | same | the whole buffer | @@ -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`, `= loop i = 0`, 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. Inside open brackets, under the first element after the opener, or one level in from the opener's line when it ends that line; a line that starts with the closer goes there too, so `RET` in `and(a|)` leaves room for the next argument. The closer of a bracket that holds a lambda's block goes to the call's column | +| `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`, `= loop i = 0`, a lambda header). After a `let` line, the column under its first name is offered second, for its next binding (`let a = 1` and `b = 2` under `a`); after such a binding line it comes first, and `DEL` steps out to the `let`'s column. 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. Inside open brackets, under the first element after the opener, or one level in from the opener's line when it ends that line; a line that starts with the closer goes there too, so `RET` in `and(a|)` leaves room for the next argument. The closer of a bracket that holds a lambda's block goes to the call's column | | `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. 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. +- **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. A `let`'s binding lines, lined up under its first name, belong to the `let`. 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 260ac514..d25d5b47 100644 --- a/emacs/flan-fln-mode.el +++ b/emacs/flan-fln-mode.el @@ -480,11 +480,52 @@ on its line, which is not the statement's." (or (flan-fln--prev-code bol) (flan-fln--next-code bol)) bol))) +(defun flan-fln--let-line-p (l) + "Non-nil if the line L starts with `let', at any column." + (save-excursion (goto-char (flan-fln--first-char l)) (looking-at "let[ \t]"))) + +(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." + (save-excursion + (goto-char (flan-fln--first-char l)) + (skip-chars-forward "let") + (skip-chars-forward " \t") + (current-column))) + +(defun flan-fln--binding-shape-p (l) + "Non-nil if the joined line L reads as a binding: a name, `~x', or a +`[...]' or `{...}' pattern, then ` = ' or `: T = '." + (save-excursion + (goto-char (flan-fln--first-char l)) + (let ((end (flan-fln--joined-end l))) + (cond + ((looking-at "[[{]") + (let ((c (ignore-errors (scan-lists (point) 1 0)))) + (and c (<= c end) + (progn (goto-char c) (looking-at "[ \t]+=\\(?:[ \t]\\|$\\)"))))) + ((looking-at "~?[^][ \t\n(){},;\":.~][^][ \t\n(){},;\":.]*\\(?:\\([ \t]+=\\(?:[ \t]\\|$\\)\\)\\|:[ \t]\\)") + (or (match-beginning 1) + (flan-fln--find-top "[ \t]=\\(?:[ \t]\\|$\\)" (match-end 0) end))))))) + +(defun flan-fln--binding-let (l) + "The `let' line whose binding the joined line L is, or nil. +Lines indented under a let whose value ends on its line are more bindings +of it (lib/indent_reader.ml's `binding_lines'), lined up with its first +name." + (and (not (flan-fln--continuation-p l)) + (flan-fln--binding-shape-p l) + (let ((p (flan-fln--parent l))) + (and p (flan-fln--let-line-p p) + (not (flan-fln--opener-p p (flan-fln--logical-end p))) + p)))) + (defun flan-fln--line-statement (pos) "The first line of the statement POS's line starts or continues. -A clause line answers for itself." +A clause line answers for itself; a let's binding line gives the let." (let ((l (flan-fln--code-line-at pos))) - (and l (flan-fln--logical-start l)))) + (and l (let ((s (flan-fln--logical-start l))) + (or (flan-fln--binding-let s) s))))) (defun flan-fln--statement-start-at (pos) "The first line of the statement at POS; a clause gives its header's." @@ -792,6 +833,9 @@ forms, where the clause line itself is not." (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)) + ;; A let's binding line starts no form; its value is what runs there. + ((let ((raw (flan-fln--logical-start (flan-fln--code-line-at pos)))) + (and (flan-fln--binding-let raw) (flan-fln--binding-value raw)))) (t (let ((s (flan-fln--statement-start-at pos))) (and s (or (flan-fln--merged-let-value s) (flan-fln--statement-bounds s)))))))) @@ -803,11 +847,19 @@ The reader merges consecutive lets into one binding vector, so a second the form that runs where the line stands." (let ((p (and (flan-fln--let-p s) (flan-fln--sibling s -1)))) (when (and p (flan-fln--let-p p)) - (let ((v (flan-fln--value-start s)) - (end (flan-fln--joined-end s))) - (if (and v (< v end)) - (cons v (cdr (flan-fln--statement-bounds s))) - (flan-fln--body-bounds s)))))) + (flan-fln--binding-value s)))) + +(defun flan-fln--binding-value (s) + "Bounds of the value the binding line S binds: after its `=' on the line, +or the block under it." + (let ((v (flan-fln--value-start s)) + (end (flan-fln--joined-end s))) + (cond ((not (and v (< v end))) (flan-fln--body-bounds s)) + ;; `= match x', a lambda header: the value takes the lines under it. + ((flan-fln--opener-p s (flan-fln--logical-end s)) + (cons v (cdr (flan-fln--statement-bounds s)))) + ;; Otherwise the lines under a let are its other bindings. + (t (cons v end))))) (defun flan-fln--clause-target (l) "What a clause line L stops at: an elif's condition, else its block. @@ -1022,9 +1074,10 @@ before point. With ARG, stop there instead, as \\[flan-eval-last-sexp] does." (skip-chars-backward " \t\n" first) (point)))))) (defun flan-fln--let-p (start) - "Non-nil if START's statement is a `let'. + "Non-nil if START's statement is a local `let'. A let takes no block: lines under it are its value's (`= match x', a lambda -header), and its name lasts to the end of the block it is in." +header) or more bindings of it, and its names last to the end of the block +it is in." (and (> (flan-fln--indent-at start) 0) (save-excursion (goto-char (flan-fln--first-char start)) (looking-at "let[ \t]")))) @@ -1305,6 +1358,13 @@ whose brackets have closed is no block POS can join." (cons (+ (flan-fln--indent-at (flan-fln--logical-start prev)) flan-fln-indent-offset) stack)) + ;; After a let's line, its next binding's column too, lined up with + ;; its first name -- second, so RET after a let stays at its column. + ;; 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 + (cons (car stack) (cons b (cdr stack)))))) (t stack)))) (defun flan-fln--block-levels (pos) diff --git a/emacs/test-flan-fln-live.el b/emacs/test-flan-fln-live.el index 62f41a79..71aeaf81 100644 --- a/emacs/test-flan-fln-live.el +++ b/emacs/test-flan-fln-live.el @@ -165,6 +165,16 @@ comment(): 0 let x = 3 twice(x) + 1 + +fn grp(k: i64) -> i64 + let a = k + 1 + b = a * 2 + a + b + +comment(): + let u = 2 + v = u * 5 + u + v ") (defun test-flan-fln-live--run (backend args) @@ -240,6 +250,12 @@ comment(): (test-flan--check (funcall name "C-c C-e on a bare let sends its scope with it") (funcall shows "13")) + ;; A let's bindings on the lines under it go with it, and read. + (funcall goto "v = u * 5") + (flan-fln-eval-statement) + (test-flan--check (funcall name "C-c C-e on a binding line sends its let's group and scope") + (funcall shows "12")) + ;; A region of several statements reads as one (do ...). (funcall goto "twice(4)") (transient-mark-mode 1) @@ -403,6 +419,7 @@ comment(): ("loop x = a" "a loop, at its word") ("if y == 0" "a loop's block") ("let r = loop" "a let-bound loop, at its let") + ("b = a * 2" "a binding under a let, at its value") ("if i > n" "a let-bound loop's block"))) (funcall goto (car c)) (let ((reply (flan-fln-eval-defun '(4)))) diff --git a/emacs/test-flan-fln.el b/emacs/test-flan-fln.el index 05d5b995..2d077612 100644 --- a/emacs/test-flan-fln.el +++ b/emacs/test-flan-fln.el @@ -422,6 +422,34 @@ fn twice(n: i64) -> i64 = n * 2 (test-flan-fln--is "a free parenthesis marks the value inside it" (plist-get r :pause) '(2 6)))) +;;; A let's bindings on the lines under it + +(defconst test-flan-fln--group + "fn f(p) -> i64 + let a = 1 + b = a + 1 + {x .x} = p + g(a, b) + a + x +") + +(test-flan-fln--in (test-flan-fln--at test-flan-fln--group "b = a") + (test-flan-fln--is "a binding line is part of its let's statement" + (test-flan-fln--thing 'flan-fln-statement) + "let a = 1\n b = a + 1\n {x .x} = p")) +(test-flan-fln--in (test-flan-fln--at test-flan-fln--group "{x .x}") + (let ((r (test-flan-fln--sending (flan-fln-eval-statement)))) + (test-flan-fln--is "C-c C-e on a binding line sends the group and its scope" + (test-flan-fln--sent-code r) + "let a = 1\n b = a + 1\n {x .x} = p\n g(a, b)\n a + x"))) +(test-flan-fln--in (test-flan-fln--at test-flan-fln--group "a + 1") + (let ((r (test-flan-fln--sending (flan-fln-eval-defun '(4))))) + (test-flan-fln--is "C-u C-c C-c on a binding line marks its value" + (plist-get r :pause) '(3 11)))) +(test-flan-fln--in (test-flan-fln--at "fn f()\n let f = fn(x) =>\n y = x\n y\n f\n" "y = x") + (test-flan-fln--is "a line of a lambda value's block is no binding" + (test-flan-fln--thing 'flan-fln-statement) "y = x")) + ;;; Match arms, header lines, clauses, comments (defconst test-flan-fln--arms @@ -878,6 +906,16 @@ defconst(k, 3) (test-flan-fln--tabs " if a and\n|b" 1) 4) (test-flan-fln--is "never deeper after a let" (test-flan-fln--tabs "fn f()\n let r = 3\n|" 1) 2) +(test-flan-fln--is "TAB after a let stays at its column first" + (test-flan-fln--tabs "fn f()\n let a = 1\n|" 1) 2) +(test-flan-fln--is "and offers its next binding's column, under the first name" + (test-flan-fln--tabs "fn f()\n let a = 1\n|" 2) 6) +(test-flan-fln--is "after a binding line, that column comes first" + (test-flan-fln--tabs "fn f()\n let a = 1\n b = 2\n|" 1) 6) +(test-flan-fln--is "and a binding written there stays" + (test-flan-fln--tabs "fn f()\n let a = 1\n| b = 2" 1) 6) +(test-flan-fln--is "at the top level too" + (test-flan-fln--tabs "let a = 1\n|" 2) 4) (dolist (c '(("let r = match n" "a let's match") ("let r = if c" "a let's if") ("x = if c" "an assignment's if")