From 905a4c53be373f8665b680de600cb4cf1c68d46d Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sat, 26 Sep 2026 05:28:08 +0700 Subject: [PATCH] A struct or union fits on one line, struct Pt(x: i32, y: i32), and flan convert writes that form when it fits. --- emacs/flan-fln-mode.el | 8 +++++++- emacs/test-flan-fln-live.el | 6 ++++++ emacs/test-flan-fln.el | 17 +++++++++++++++++ lib/indent_printer.ml | 12 ++++++++++++ lib/indent_reader.ml | 19 +++++++++++++++++-- spec-syntax.md | 5 ++++- test/syntax/handwritten/ledger.fln | 4 +--- test/test_syntax.ml | 15 ++++++++++++++- 8 files changed, 78 insertions(+), 8 deletions(-) diff --git a/emacs/flan-fln-mode.el b/emacs/flan-fln-mode.el index 8562928e..1846d81f 100644 --- a/emacs/flan-fln-mode.el +++ b/emacs/flan-fln-mode.el @@ -1153,6 +1153,11 @@ Before it at the same level, else out to the line that owns this block." ((member w '("defer" "quote" "else")) alone) ((member w '("fn" "fn-")) (not (re-search-forward "[ \t]=[ \t]" end t))) + ;; `struct Pt(x: i32)' has its fields on the line. + ((member w '("struct" "union")) + (not (save-excursion + (goto-char w-end) + (looking-at "[ \t]+[^][ \t\n(){},;\":]+(")))) ((member w '("if" "elif")) (not (flan-fln--then start))) (t t))))) ;; `let r = match n', `x = if c', `fn f(x) = match x', a lambda @@ -1606,7 +1611,8 @@ lambda or a `Fn(...)' type, and not after a match arm's." flan--definers)) (1 font-lock-keyword-face) (2 font-lock-function-name-face)) ;; A condition's parent, `struct DiskFull :parent IoError'. - (,(concat "^struct[ \t]+[^][ \t\n(){},;\":]+[ \t]+:parent[ \t]+" flan-fln--name-re) + (,(concat "^struct[ \t]+[^][ \t\n(){},;\":]+\\(?:([^)\n]*)\\)?[ \t]+:parent[ \t]+" + flan-fln--name-re) 1 font-lock-type-face) (,(concat "^\\(?:def\\|once\\|const\\)[ \t]+" flan-fln--name-re) 1 font-lock-variable-name-face) diff --git a/emacs/test-flan-fln-live.el b/emacs/test-flan-fln-live.el index cccee446..116eff6c 100644 --- a/emacs/test-flan-fln-live.el +++ b/emacs/test-flan-fln-live.el @@ -90,6 +90,10 @@ struct Oops :parent Error type Count = i64 +struct Pair(a: i64, b: i64) + +fn pair-sum(p: Pair) -> i64 = p.a + p.b + fn counted(n: Count) -> Count = n + 1 fn oops-code() -> i64 @@ -240,6 +244,8 @@ comment(): ("fn dir" "(dir :north)" "7") ("struct Oops" "(oops-code)" "7") ("type Count" "(counted 1)" "2") + ("struct Pair" "(pair-sum (Pair {.a 1 .b 2}))" "3") + ("fn pair-sum" "(pair-sum (Pair {.a 1 .b 2}))" "3") ("fn counted" "(counted 1)" "2") ("fn oops-code" "(oops-code)" "7") ("macro dbl-of" "(use-mac 5)" "10") diff --git a/emacs/test-flan-fln.el b/emacs/test-flan-fln.el index e2e098c1..48fb56f1 100644 --- a/emacs/test-flan-fln.el +++ b/emacs/test-flan-fln.el @@ -903,6 +903,23 @@ defconst(k, 3) (test-flan-fln--tabs "macro repeat(i, n, & body)\n|" 1) 2) (test-flan-fln--is "and a struct with a parent" (test-flan-fln--tabs "struct DiskFull :parent IoError\n|" 1) 2) +(test-flan-fln--is "but not a struct on one line" + (test-flan-fln--tabs "struct Pt(x: i32, y: i32)\n|" 1) 0) +(test-flan-fln--is "nor one with a parent" + (test-flan-fln--tabs "struct D(free: i64) :parent IoError\n|" 1) 0) +(test-flan-fln--in "struct D(free: i64) :parent IoError\n\nunion U(a: i32)\n" + (font-lock-ensure) + (let ((face (lambda (needle) + (save-excursion (goto-char (point-min)) (search-forward needle) + (get-text-property (match-beginning 0) 'face))))) + (test-flan-fln--is "a one-line struct's name is a type" (funcall face "D(") 'font-lock-type-face) + (test-flan-fln--is "its field's type too" (funcall face "i64") 'font-lock-type-face) + (test-flan-fln--is "and its parent" (funcall face "IoError") 'font-lock-type-face) + (test-flan-fln--is "a one-line union's name" (funcall face "U(") 'font-lock-type-face)) + (goto-char (point-min)) + (test-flan-fln--is "a one-line struct is a top-level form of one line" + (test-flan-fln--thing 'flan-fln-toplevel) + "struct D(free: i64) :parent IoError")) (test-flan-fln--is "a one-line fn whose value is a match opens it" (test-flan-fln--tabs "fn f(x) = match x\n|" 1) 2) (test-flan-fln--is "no deeper after a one-line else" diff --git a/lib/indent_printer.ml b/lib/indent_printer.ml index 3acf6418..3db13dcb 100644 --- a/lib/indent_printer.ml +++ b/lib/indent_printer.ml @@ -1060,7 +1060,19 @@ and sugar n (f : Form.t) : string list option = | [ _; pf; { v = Form.Vec fs; _ } ] -> (" :parent " ^ ty pf, fs) | _ -> assert false in + (* One line when it fits, [struct Pt(x: i32, y: i32)], and a line per + field otherwise. *) + let one = + match fs, params_text fs with + | _ :: _, Some pt -> + let line = + i ^ (if d = "defstruct" then "struct " else "union ") ^ name ^ "(" ^ pt ^ ")" ^ parent + in + if String.length line <= width && not (!inside f) then Some [ line ] else None + | _ -> None + in (match pairs fs with + | _ when one <> None -> one | Some prs when List.for_all (fun ((f : Form.t), _) -> match f.v with Form.Sym x -> def_name x | _ -> false) prs -> Some diff --git a/lib/indent_reader.ml b/lib/indent_reader.ml index bfba39e1..034daa99 100644 --- a/lib/indent_reader.ml +++ b/lib/indent_reader.ml @@ -1531,6 +1531,13 @@ and header (s : st) w : Form.t = named head items | "struct" | "union" -> let name = name_tok p ~what:"the type's name" in + (* [struct Pt(x: i32, y: i32)]: the fields on the header's line, as a + data case writes them, with no block under it. *) + let inline = + match (peek p).tok with + | LP when not (peek p).sp -> let lp = advance p in Some (params p lp) + | _ -> None + in (* [struct DiskFull :parent IoError]: a condition's parent, before the fields as in the paren form. *) let parent = @@ -1541,9 +1548,17 @@ and header (s : st) w : Form.t = Some (Form.make (Form.Kw "parent") kt.loc, pt) | _ -> None in - expect_eol_block p - ~after:(match parent with Some (_, pt) -> text_of pt | None -> w ^ " " ^ text_of name); + let after = + match parent, inline with + | Some (_, pt), _ -> text_of pt + | None, Some _ -> ")" + | None, None -> w ^ " " ^ text_of name + in let fields = + match inline with + | Some fs -> expect_eol p ~after; fs + | None -> + expect_eol_block p ~after; lines s (fun () -> let f = name_tok p ~what:"a field's name" in let tf = diff --git a/spec-syntax.md b/spec-syntax.md index 8e190381..7204999c 100644 --- a/spec-syntax.md +++ b/spec-syntax.md @@ -290,7 +290,10 @@ Each item: the proposal, then the reason in one line. syntaxes (section 3, item 8). A condition names its parent after the name, `struct DiskFull :parent IoError` with its field lines, reading `(defstruct DiskFull :parent IoError [free i64])`; with no field lines it - reads `(defstruct IoError :parent Error)`. **Built.** + reads `(defstruct IoError :parent Error)`. A struct or union fits on one + line with its fields in parentheses, `struct Pt(x: i32, y: i32)` or + `struct DiskFull(free: i64) :parent IoError`; `flan convert` writes that + when it fits the line and no comment sits among the fields. **Built.** - `type Row = Vec(i32)` reads `(defalias Row (Vec i32))`. **Built.** - `macro repeat(i, n, & body)` plus a block reads `(defmacro repeat [i n & body] …)`. A parameter is a bare name, a diff --git a/test/syntax/handwritten/ledger.fln b/test/syntax/handwritten/ledger.fln index e75b1e18..02ba52ef 100644 --- a/test/syntax/handwritten/ledger.fln +++ b/test/syntax/handwritten/ledger.fln @@ -6,9 +6,7 @@ struct Overdraft :parent Error account: i32 short: i64 -struct Audit - account: i32 - amount: i64 +struct Audit(account: i32, amount: i64) once balances: [4 i64] once audits: i32 diff --git a/test/test_syntax.ml b/test/test_syntax.ml index 3dcca927..95f6c4ff 100644 --- a/test/test_syntax.ml +++ b/test/test_syntax.ml @@ -563,6 +563,12 @@ let () = "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 one-line struct" "struct Pt(x: i32, y)" "(defstruct Pt [x i32 y dyn])"; + reads "a one-line union" "union U(a: i32)" "(defunion U [a i32])"; + reads "a one-line struct with a parent" "struct D(free: i64) :parent Io" + "(defstruct D :parent Io [free i64])"; + refuses "a one-line struct takes no block" "struct Pt(x: i32)\n y: i32" "indent/stray-indent" + "takes no block"; 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))))"; @@ -836,7 +842,14 @@ let () = "(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"; + "struct D(free: i64) :parent Io"; + prints "a struct on one line" "(defstruct Pt [x i32 y dyn])" "struct Pt(x: i32, y)\n"; + prints "a union too" "(defunion U [a i32 b f32])" "union U(a: i32, b: f32)"; + prints "a struct too long for a line takes a line per field" + ("(defstruct W [" ^ String.concat " " (List.init 8 (Printf.sprintf "field-number-%d i32")) ^ "])") + "struct W\n field-number-0: i32\n"; + prints "and so does one with a comment among its fields" + "(defstruct C [a i32 ; first\n b i32])" "struct C\n a: i32 ; first\n b: i32"; prints "a parent with no fields" "(defstruct D :parent Io)" "struct D :parent Io"; prints "an empty field vector under a parent keeps the fallback" "(defstruct D :parent Io [])" "defstruct(D, :parent, Io, [])";