A variable named like a header or clause word, data = 3 or on += 1, is read and printed as an assignment.

This commit is contained in:
Joseph Ferano 2026-09-26 05:21:11 +07:00
parent 3f4e5a930f
commit de0c8791f3
5 changed files with 53 additions and 9 deletions

View File

@ -83,8 +83,14 @@ fine here. Brackets and strings are still paired."
(defconst flan-fln--clause-words '("else" "elif" "on" "restart")
"Words that start a clause of the statement above, at its column.")
;; What follows a word that starts a clause or a header: a space and not an
;; assignment, or the end of the line. `on = 2' assigns a variable named on,
;; as `assigns' in lib/indent_reader.ml reads it.
(defconst flan-fln--word-end-re
"\\(?:[ \t]+\\(?:[^-+*/= \t\n]\\|[-+*/]\\(?:[^=]\\|$\\)\\)\\|[ \t]*$\\)")
(defconst flan-fln--clause-re
"\\(else\\|elif\\|on\\|restart\\)\\(?:[ \t]\\|$\\)")
(concat "\\(else\\|elif\\|on\\|restart\\)" flan-fln--word-end-re))
(defconst flan-fln--clause-headers
'(("else" "if" "elif") ("elif" "if" "elif")
@ -1137,7 +1143,7 @@ Before it at the same level, else out to the line that owns this block."
(goto-char start)
(back-to-indentation)
(and (looking-at (concat (regexp-opt flan-fln--opener-words t)
"\\(?:[ \t]\\|$\\)"))
flan-fln--word-end-re))
(let* ((w (match-string-no-properties 1))
(w-end (match-end 1))
(end (flan-fln--code-end last))
@ -1579,7 +1585,7 @@ lambda or a `Fn(...)' type, and not after a match arm's."
(defvar flan-fln-font-lock-keywords
`(;; The header words, at the start of a line and followed by a space or the
;; end of it: `if(c, a)' is the fallback call and is not a header.
(,(concat "^[ \t]*" (regexp-opt flan-fln--header-words t) "\\(?:[ \t]\\|$\\)")
(,(concat "^[ \t]*" (regexp-opt flan-fln--header-words t) flan-fln--word-end-re)
1 font-lock-keyword-face)
(,(concat "^\\(fn-?\\|macro\\)[ \t]+" flan-fln--name-re)
2 font-lock-function-name-face)

View File

@ -886,6 +886,19 @@ defconst(k, 3)
(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--is "a header word being assigned opens nothing"
(test-flan-fln--tabs "fn f()\n for = 1\n|" 1) 2)
(test-flan-fln--in "fn f()\n handler-case\n g()\n on E(c)\n h(c)\n on = 2\n data += 1\n"
(font-lock-ensure)
(goto-char (point-min))
(search-forward "on = 2")
(test-flan--check "nor is a clause word being assigned a clause"
(not (flan-fln--clause-line-p (line-beginning-position))))
(test-flan-fln--is "or drawn as a keyword"
(get-text-property (match-beginning 0) 'face) nil)
(search-forward "data")
(test-flan-fln--is "and a header word assigned is not either"
(get-text-property (match-beginning 0) 'face) nil))
(test-flan-fln--is "a macro opens a block"
(test-flan-fln--tabs "macro repeat(i, n, & body)\n|" 1) 2)
(test-flan-fln--is "and a struct with a parent"

View File

@ -26,7 +26,7 @@ let reserved =
[ "fn"; "fn-"; "def"; "once"; "const"; "struct"; "union"; "data"; "enum";
"import"; "if"; "elif"; "else"; "while"; "until"; "match"; "let"; "for";
"return"; "break"; "continue"; "defer"; "handler-case"; "handler-bind";
"restart-case"; "quote"; "on"; "restart"; "macro"; "loop"; "type" ]
"restart-case"; "quote"; "on"; "restart" ]
(* A symbol the reader gives back as itself when it is written bare. *)
let name_ok s =
@ -544,7 +544,17 @@ let lead_word text =
(* A statement whose text leads with a reserved word, parenthesised. *)
let guard text =
let w, spaced = lead_word text in
if spaced && List.mem w reserved then paren text else text
(* [data = 3]: a name being assigned is read as one, header word or not. *)
let assigned =
let k = String.length w + 1 in
List.exists
(fun op ->
let o = op ^ " " in
String.length text >= k + String.length o
&& String.sub text k (String.length o) = o)
[ "="; "+="; "-="; "*="; "/=" ]
in
if spaced && List.mem w reserved && not assigned then paren text else text
let stmts_of (f : Form.t) =
match f.v with

View File

@ -1048,8 +1048,15 @@ let is_lambda_candidate (e : Form.t) =
List.for_all (fun (a : Form.t) -> match a.v with Form.Sym _ -> true | _ -> false) args
| _ -> false
(* The word at the head of the line is a variable being assigned, [data = 3]
or [on += 1], whatever else it could start. *)
let assigns p =
let n = peek_at p 1 in
n.sp && (match n.tok with NAME x -> x = "=" || List.mem_assoc x assign_ops | _ -> false)
let header_follow p s =
let n = peek_at p 1 in
(not (assigns p)) &&
let plain_name = function
| NAME x -> not (is_op_word x || x = "=" || List.mem_assoc x assign_ops)
| _ -> false
@ -1344,6 +1351,7 @@ and stmt (s : st) : Form.t =
let t = peek p in
match t.tok with
| NAME w when header_follow p w -> header s w
| NAME _ when assigns p -> expr_stmt s
| NAME (("else" | "elif") as w) when else_if_above p t ->
failk "orphan-else" t.loc
"the else above took the one-line if after it as its value, so this %s \
@ -1678,7 +1686,7 @@ and header (s : st) w : Form.t =
let clauses ~oneline body =
let rec elifs acc =
match (peek p).tok with
| NAME "elif" ->
| NAME "elif" when not (assigns p) ->
ignore (advance p);
let c, _ = binary p 1 in
(match (peek p).tok with
@ -1700,7 +1708,7 @@ and header (s : st) w : Form.t =
let els_ = elifs [] in
let else_ =
match (peek p).tok with
| NAME "else" ->
| NAME "else" when not (assigns p) ->
let et = advance p in
(match (peek p).tok with
| NEWLINE -> ignore (advance p); Some (et.loc, block s ~after:"else")
@ -1848,7 +1856,7 @@ and header (s : st) w : Form.t =
let body = block s ~after:w in
let rec clauses acc =
match (peek p).tok, (peek_at p 1) with
| NAME "on", n when n.sp ->
| NAME "on", n when n.sp && not (assigns p) ->
let ot = advance p in
let head, _ = postfix p in
let ty, var =
@ -1877,7 +1885,7 @@ and header (s : st) w : Form.t =
let body = block s ~after:w in
let rec clauses acc =
match (peek p).tok, (peek_at p 1) with
| NAME "restart", n when n.sp ->
| NAME "restart", n when n.sp && not (assigns p) ->
ignore (advance p);
let name = name_tok p ~what:"the restart's name" in
let lp = glued_lp p ~what:"the restart's parameters in parentheses" in

View File

@ -558,6 +558,11 @@ let () =
reads "type alias" "type Row = Vec(i32)" "(defalias Row (Vec i32))";
reads "type alias of an array" "type V2 = [2 f32]" "(defalias V2 [2 f32])";
reads "a local named type" "type = 3" "(set type 3)";
reads "a header word assigned" "data += 3" "(set data (+ data 3))";
reads "a clause word assigned after its header"
"handler-case\n g()\non E(c)\n h(c)\non = 2"
"(handler-case (g) [(E [c] (h c))])\n(set on 2)";
reads "an else assigned after an if" "if a\n b\nelse = 2" "(when a b)\n(set else 2)";
reads "a parent and no fields" "struct Io :parent Error" "(defstruct Io :parent Error)";
reads "macro" "macro repeat(i, n, & body)\n quote\n f(~i)\n ~@body"
"(defmacro repeat [i n & body] (quasiquote (do (f (unquote i)) (unquote-splicing body))))";
@ -827,6 +832,8 @@ let () =
prints "a template's for keeps its unquotes"
"(defmacro m [i n & body] `(dotimes [~i ~n] ~@body))" "for ~i in range(~n)";
prints "a macro" "(defmacro m [[a b] n & body] `(do ~@body))" "macro m([a b], n, & body)\n quote";
prints "a header word assigned keeps no parentheses"
"(defn f [] () (set data 3) (set loop 4) (set on 5))" " data = 3\n loop = 4\n on = 5";
prints "a type alias" "(defalias Row (Vec i32))" "type Row = Vec(i32)";
prints "a struct with a parent" "(defstruct D :parent Io [free i64])"
"struct D :parent Io\n free: i64";