diff --git a/emacs/flan-fln-mode.el b/emacs/flan-fln-mode.el index b8e540b9..a2cc173d 100644 --- a/emacs/flan-fln-mode.el +++ b/emacs/flan-fln-mode.el @@ -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 diff --git a/emacs/test-flan-fln-live.el b/emacs/test-flan-fln-live.el index e694ab88..3328a80e 100644 --- a/emacs/test-flan-fln-live.el +++ b/emacs/test-flan-fln-live.el @@ -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") diff --git a/emacs/test-flan-fln.el b/emacs/test-flan-fln.el index a563728e..03cb7422 100644 --- a/emacs/test-flan-fln.el +++ b/emacs/test-flan-fln.el @@ -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" diff --git a/lib/indent_reader.ml b/lib/indent_reader.ml index fd914f54..472b88bb 100644 --- a/lib/indent_reader.ml +++ b/lib/indent_reader.ml @@ -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. *) diff --git a/test/test_syntax.ml b/test/test_syntax.ml index 88f2cb2d..43337bc8 100644 --- a/test/test_syntax.ml +++ b/test/test_syntax.ml @@ -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"