A block lambda in code cut mid-line reads against its line's start, an arm or else value ending in => takes its block, and a missing comma between indices shows the indexed spelling

This commit is contained in:
Joseph Ferano 2026-09-26 05:37:22 +07:00
parent 6e56fce7fa
commit 34ef99d1f2
5 changed files with 88 additions and 16 deletions

View File

@ -239,7 +239,9 @@ fine here. Brackets and strings are still paired."
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."
that starts with the bracket's closer is not in the block.
Each call scans the lines from the open bracket to POS, so walking a long
bracketed literal line by line costs the square of its length."
(save-excursion
(let* ((bol (flan-fln--bol pos))
(s (syntax-ppss bol))
@ -337,10 +339,18 @@ depth, outside strings and comments, or nil."
(flan-fln--joined-end l))))
(and m (car m))))
(defun flan-fln--value-end (l)
"Where a value written on the joined line L ends: at the line's code end,
or, when the line ends in `=>', at the end of the lambda's block under it."
(let ((last (flan-fln--logical-end l)))
(if (flan-fln--ends-in-arrow-p last)
(cdr (flan-fln--span l (flan-fln--statement-last l t)))
(flan-fln--code-end last))))
(defun flan-fln--clause-value (l)
"Bounds of the value on the clause line L itself: `x' of `else x' or of
`elif c then x'; nil when the clause's value is its block."
(let ((end (flan-fln--joined-end l))
(let ((end (flan-fln--value-end l))
(then (flan-fln--then l)))
(save-excursion
(goto-char (flan-fln--first-char l))
@ -839,7 +849,7 @@ pattern names something, so the value cannot be evaluated alone."
(let* ((pat (string-trim (buffer-substring-no-properties start arrow)))
(vbeg (save-excursion (goto-char (+ arrow 2))
(skip-chars-forward " \t") (point)))
(value (if (< vbeg last) (cons vbeg last)
(value (if (< vbeg last) (cons vbeg (flan-fln--value-end l))
(flan-fln--body-bounds l))))
(and value
(list :arrow arrow :value value

View File

@ -71,9 +71,13 @@ fn lam(k: i64) -> i64
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)
let a = 1
let b = app(k, fn(x) =>
let y = x + a
y)
let c = fn(z: i64) -> i64 =>
z * 2
c(b)
fn rs() -> i64
restart-case
@ -302,9 +306,11 @@ 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")
("x) =>" "a block lambda in a call, from its fn")
("let y = x" "a statement in a block lambda's block")
("y)" "the block's last statement, less the call's closer")
("let b = app" "a merged let's call with a block lambda, at its value")
("let c = fn" "a merged let's block lambda, at its value")
("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

@ -925,6 +925,27 @@ its line with AT-END."
(get-text-property (match-beginning 0) 'face))
'font-lock-keyword-face))
(defconst test-flan-fln--lam-arm
"fn f(n: i64) -> i64
match n
1 -> app(1, fn(a) =>
a * 2)
_ -> 0
")
(test-flan-fln--is "C-x C-e at the end of an arm whose value ends in => sends the value, block and all"
(test-flan-fln--in (test-flan-fln--at test-flan-fln--lam-arm "1 ->")
(end-of-line)
(test-flan-fln--sent-code (test-flan-fln--sending (flan-fln-eval-last))))
"app(1, fn(a) =>\n a * 2)")
(test-flan-fln--is "and C-u C-c C-c on its pattern stops at the value"
(test-flan-fln--in (test-flan-fln--at test-flan-fln--lam-arm "1 ->")
(plist-get (test-flan-fln--sending (flan-fln-eval-defun '(4))) :pause))
'(3 10))
(test-flan-fln--is "an else whose value ends in => takes the block too"
(test-flan-fln--in "fn f(c)\n if c\n g()\n |else app(1, fn(a) =>\n a)\n"
(test-flan-fln--text (flan-fln--clause-value (line-beginning-position))))
"app(1, fn(a) =>\n a)")
;;; Block editing
(test-flan-fln--in "fn f() -> ()\n if a\n |b()\n c()\n d()\n"

View File

@ -289,8 +289,17 @@ let layout ?(snippet = false) ?(base = 1) ?indent (toks : token list) : token ar
let arr = Array.of_list toks in
let n = Array.length arr in
(* A snippet from the editor starts wherever it was written, and its first
line is its base: a later line may not go left of it. *)
let base = ref (if snippet && n > 0 then arr.(0).loc.Loc.col else base) in
line is its base: a later line may not go left of it. One cut from the
middle of a line (see [indent]) has that line's start as its base, so a
block under the line, a [let]'s [match] arms or a lambda's, reads as it
does in the file. *)
let base =
ref (if snippet && n > 0 then
match indent with
| Some c -> min c arr.(0).loc.Loc.col
| None -> arr.(0).loc.Loc.col
else base)
in
let out = ref [] in
let add tok loc = out := { tok; loc; sp = true } :: !out in
let stack = ref [ !base ] in
@ -315,7 +324,7 @@ let layout ?(snippet = false) ?(base = 1) ?indent (toks : token list) : token ar
for i = 0 to n - 1 do
let t = arr.(i) in
(if i = 0 then begin
if t.loc.Loc.col <> !base then
if t.loc.Loc.col <> !base && indent = None then
failk "unexpected-indent" t.loc
"the first line starts at column %d, and a file's top-level lines \
start at column %d. Remove the indentation"
@ -439,7 +448,12 @@ let layout ?(snippet = false) ?(base = 1) ?indent (toks : token list) : token ar
&& arr.(i + 1).loc.Loc.line > t.loc.Loc.eline ->
frames := { f_base = !base; f_stack = !stack; f_opens = !opens; f_arrow = t }
:: !frames;
base := line_col i;
(* A snippet cut from the middle of a line starts where its line
does in the file, [indent], not where the cut does. *)
base :=
(match indent with
| Some c when t.loc.Loc.line = arr.(0).loc.Loc.line -> min c (line_col i)
| _ -> line_col i);
stack := [ !base ];
opens := []
| _ -> ())
@ -723,7 +737,7 @@ and postfix p =
loop (mk p l0 (Form.List (f :: args)), 9)
| LB ->
ignore (advance p);
let idx = items p RB t.loc ~what:"indices" in
let idx = items p RB t.loc ~what:"indices" ~head:(text_of f) in
loop (mk p l0 (Form.List (sym t.loc "at" :: f :: idx)), 9)
| NAME s when String.length s > 1 && s.[0] = '.' ->
ignore (advance p);
@ -892,6 +906,7 @@ and fn_expr p =
(sym t.loc "fn" :: Form.make (Form.Vec ps) (span_of_list lp.loc args) :: body)),
0)
| NAME "=" when names -> lambda_equals p (header ())
| NAME w when names && glued_arrow w -> lambda_glued n (header ()) w
| NEWLINE when names && (peek_at p 1).tok = INDENT ->
lambda_arrow (peek_at p 2).loc (header ())
(* Inside brackets a line break is no token: the next line's first token
@ -933,6 +948,15 @@ and lambda_equals : 'a. p -> string -> 'a = fun p header ->
function is written. Write:\n\n %s => %s"
header body
(* [fn(a) =>x]: the body glued to the arrow reads as one name. *)
and glued_arrow w = String.length w > 2 && String.sub w 0 2 = "=>"
and lambda_glued : 'a. token -> string -> string -> 'a = fun t header w ->
failk "lambda-arrow-space" t.loc
"%s is one name, with nothing between => and the body. Put a space \
after the arrow: %s => %s"
w header (String.sub w 2 (String.length w - 2))
(* A lambda header with lines under it and no [=>]. *)
and lambda_arrow : 'a. Loc.t -> string -> 'a = fun at header ->
failk "lambda-arrow" at
@ -975,7 +999,8 @@ and lambda_params args =
(* Comma-separated values up to [closer]. [const T] is two elements without a
comma, for [Ptr(const u8)]: const is a reserved word in a type and never a
value. *)
and items p closer open_loc ~what =
(* [head] is the text of what is indexed, for [[ ]]'s message. *)
and items ?head p closer open_loc ~what =
let opener = if closer = RB then '[' else '(' in
let rec go acc =
let t = peek p in
@ -1006,8 +1031,11 @@ and items p closer open_loc ~what =
else if starts_value n.tok && n.sp && not (negative_literal n.tok) then
failk "missing-comma" n.loc
"%s follows %s with no comma between them. Separate %s with \
commas: f(a, b)"
commas: %s"
(show n.tok) (text_of e) what
(match head with
| Some h -> Printf.sprintf "%s[%s, %s]" h (text_of e) (show n.tok)
| None -> "f(a, b)")
else stray p ~after:(text_of e))
in
go []
@ -1265,6 +1293,7 @@ let () = typed_fn_expr := fun p ->
let body = lambda_body p ~header:(header ()) in
(wrap body, 0)
| NAME "=" -> lambda_equals p (header ())
| NAME w when glued_arrow w -> lambda_glued (peek p) (header ()) w
| NEWLINE when (peek_at p 1).tok = INDENT -> lambda_arrow (peek_at p 2).loc (header ())
(* Inside brackets a line break is no token: the next line's first token
is what follows. *)

View File

@ -615,6 +615,12 @@ let () =
"indent/lambda-block-left" "still inside the brackets";
refuses "a block lambda's brackets left open" "f(fn(a) =>\n a\n"
"indent/unclosed" "ends where this bracket closes";
refuses "indices with no comma" "x = grid[row col].color-idx"
"indent/missing-comma" "Separate indices with commas: grid[row, col]";
refuses "arguments with no comma" "x = f(a b)"
"indent/missing-comma" "Separate arguments with commas: f(a, b)";
refuses "a body glued to =>" "x = fn(a) =>a"
"indent/lambda-arrow-space" "fn(a) => a";
refuses "a lambda written with =" "x = fn(a, b) = a + b"
"indent/lambda-equals" "fn(a, b) => a + b";
refuses "a typed lambda written with =" "x = fn(a: C) -> bool = a.n > 1"