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 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 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 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 (save-excursion
(let* ((bol (flan-fln--bol pos)) (let* ((bol (flan-fln--bol pos))
(s (syntax-ppss bol)) (s (syntax-ppss bol))
@ -337,10 +339,18 @@ depth, outside strings and comments, or nil."
(flan-fln--joined-end l)))) (flan-fln--joined-end l))))
(and m (car m)))) (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) (defun flan-fln--clause-value (l)
"Bounds of the value on the clause line L itself: `x' of `else x' or of "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." `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))) (then (flan-fln--then l)))
(save-excursion (save-excursion
(goto-char (flan-fln--first-char l)) (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))) (let* ((pat (string-trim (buffer-substring-no-properties start arrow)))
(vbeg (save-excursion (goto-char (+ arrow 2)) (vbeg (save-excursion (goto-char (+ arrow 2))
(skip-chars-forward " \t") (point))) (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)))) (flan-fln--body-bounds l))))
(and value (and value
(list :arrow arrow :value 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 app(x: i64, f: Fn(i64) -> i64) -> i64 = f(x)
fn lam2(k: i64) -> i64 fn lam2(k: i64) -> i64
app(k, fn(a) => let a = 1
let b = a + 1 let b = app(k, fn(x) =>
b * 2) let y = x + a
y)
let c = fn(z: i64) -> i64 =>
z * 2
c(b)
fn rs() -> i64 fn rs() -> i64
restart-case restart-case
@ -302,9 +306,11 @@ 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") ("x) =>" "a block lambda in a call, from its fn")
("let b = a + 1" "a statement in a block lambda's block") ("let y = x" "a statement in a block lambda's block")
("b * 2" "the block's last statement, less the call's closer") ("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") ("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

@ -925,6 +925,27 @@ its line with AT-END."
(get-text-property (match-beginning 0) 'face)) (get-text-property (match-beginning 0) 'face))
'font-lock-keyword-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 ;;; 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

@ -289,8 +289,17 @@ let layout ?(snippet = false) ?(base = 1) ?indent (toks : token list) : token ar
let arr = Array.of_list toks in let arr = Array.of_list toks in
let n = Array.length arr in let n = Array.length arr in
(* A snippet from the editor starts wherever it was written, and its first (* 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. *) line is its base: a later line may not go left of it. One cut from the
let base = ref (if snippet && n > 0 then arr.(0).loc.Loc.col else base) in 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 out = ref [] in
let add tok loc = out := { tok; loc; sp = true } :: !out in let add tok loc = out := { tok; loc; sp = true } :: !out in
let stack = ref [ !base ] 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 for i = 0 to n - 1 do
let t = arr.(i) in let t = arr.(i) in
(if i = 0 then begin (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 failk "unexpected-indent" t.loc
"the first line starts at column %d, and a file's top-level lines \ "the first line starts at column %d, and a file's top-level lines \
start at column %d. Remove the indentation" 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 -> && arr.(i + 1).loc.Loc.line > t.loc.Loc.eline ->
frames := { f_base = !base; f_stack = !stack; f_opens = !opens; f_arrow = t } frames := { f_base = !base; f_stack = !stack; f_opens = !opens; f_arrow = t }
:: !frames; :: !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 ]; stack := [ !base ];
opens := [] opens := []
| _ -> ()) | _ -> ())
@ -723,7 +737,7 @@ and postfix p =
loop (mk p l0 (Form.List (f :: args)), 9) loop (mk p l0 (Form.List (f :: args)), 9)
| LB -> | LB ->
ignore (advance p); 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) loop (mk p l0 (Form.List (sym t.loc "at" :: f :: idx)), 9)
| NAME s when String.length s > 1 && s.[0] = '.' -> | NAME s when String.length s > 1 && s.[0] = '.' ->
ignore (advance p); 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)), (sym t.loc "fn" :: Form.make (Form.Vec ps) (span_of_list lp.loc args) :: body)),
0) 0)
| NAME "=" when names -> lambda_equals p (header ()) | 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 -> | NEWLINE when names && (peek_at p 1).tok = INDENT ->
lambda_arrow (peek_at p 2).loc (header ()) lambda_arrow (peek_at p 2).loc (header ())
(* Inside brackets a line break is no token: the next line's first token (* 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" function is written. Write:\n\n %s => %s"
header body 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 [=>]. *) (* A lambda header with lines under it and no [=>]. *)
and lambda_arrow : 'a. Loc.t -> string -> 'a = fun at header -> and lambda_arrow : 'a. Loc.t -> string -> 'a = fun at header ->
failk "lambda-arrow" at 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-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 comma, for [Ptr(const u8)]: const is a reserved word in a type and never a
value. *) 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 opener = if closer = RB then '[' else '(' in
let rec go acc = let rec go acc =
let t = peek p in 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 else if starts_value n.tok && n.sp && not (negative_literal n.tok) then
failk "missing-comma" n.loc failk "missing-comma" n.loc
"%s follows %s with no comma between them. Separate %s with \ "%s follows %s with no comma between them. Separate %s with \
commas: f(a, b)" commas: %s"
(show n.tok) (text_of e) what (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)) else stray p ~after:(text_of e))
in in
go [] go []
@ -1265,6 +1293,7 @@ let () = typed_fn_expr := fun p ->
let body = lambda_body p ~header:(header ()) in let body = lambda_body p ~header:(header ()) in
(wrap body, 0) (wrap body, 0)
| NAME "=" -> lambda_equals p (header ()) | 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 ()) | 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 (* Inside brackets a line break is no token: the next line's first token
is what follows. *) is what follows. *)

View File

@ -615,6 +615,12 @@ let () =
"indent/lambda-block-left" "still inside the brackets"; "indent/lambda-block-left" "still inside the brackets";
refuses "a block lambda's brackets left open" "f(fn(a) =>\n a\n" refuses "a block lambda's brackets left open" "f(fn(a) =>\n a\n"
"indent/unclosed" "ends where this bracket closes"; "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" refuses "a lambda written with =" "x = fn(a, b) = a + b"
"indent/lambda-equals" "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" refuses "a typed lambda written with =" "x = fn(a: C) -> bool = a.n > 1"