A .fln file writes a type alias as type Row = Vec(i32), and flan check prints nothing for a file that checks unless given --defs.

This commit is contained in:
Joseph Ferano 2026-09-26 05:13:03 +07:00
parent 1a97dfff77
commit 72c73117d6
10 changed files with 80 additions and 47 deletions

View File

@ -650,27 +650,10 @@ Decided 2026-09-26: a lambda's body follows ~=>~, and ~=>~ is its only spelling
takes an indented block even inside brackets, closing where the brackets close: takes an indented block even inside brackets, closing where the brackets close:
~sort-by(xs, fn(a, b) =>~ plus a block. ~sort-by(xs, fn(a, b) =>~ plus a block.
** NEXT A type alias is written type Row = Vec(i32)
Decided 2026-09-26: .fln reads ~type Name = T~ as ~(defalias Name T)~, and the printer
writes it back.
** TODO flan check prints every definition of a file that checks
A clean =flan check= lists the whole prelude (about 170 lines). Clean should print
nothing, or only the file's own definitions behind a flag.
** NEXT Classes and methods have .fln syntax
Decided 2026-09-26: ~class Lambda(param, body, env)~, ~generic describe(v) -> dyn~,
~method describe(f: Lambda)~ plus a block (the class as the parameter's type),
~multi kind(v) -> dyn = type-of(v)~, ~method kind(v) when :int~ plus a block.
** NEXT A top-level let is a global ** NEXT A top-level let is a global
Decided 2026-09-26: in .fln ~let x = v~ at column 0 reads ~(def x v)~ and replaces Decided 2026-09-26: in .fln ~let x = v~ at column 0 reads ~(def x v)~ and replaces
~def~, which is refused with that fix; ~once~ and ~const~ stay. ~def~, which is refused with that fix; ~once~ and ~const~ stay.
** NEXT A struct fits on one line
Decided 2026-09-26: ~struct Pt(x: i32, y: i32)~ beside the block form, like a data
case; union and a struct with a parent too.
** TODO Hard-coded code in messages is still paren syntax in a .fln file ** TODO Hard-coded code in messages is still paren syntax in a .fln file
Types follow the code's syntax now (=Types.spell=). Hints written into a message's Types follow the code's syntax now (=Types.spell=). Hints written into a message's
text — =(Ptr %s)=, =(clone v)=, =(the T x)= in most of =check.ml= and =parse.ml=, the text — =(Ptr %s)=, =(clone v)=, =(the T x)= in most of =check.ml= and =parse.ml=, the

View File

@ -226,9 +226,14 @@ let no_gc_flag = "--no-gc"
downstream that it ran. *) downstream that it ran. *)
let warn_memory_flag = "--warn-memory" let warn_memory_flag = "--warn-memory"
(* [flan check --defs]: every definition the checked program holds, prelude
included. Off by default, so a file that checks prints nothing. *)
let defs_flag = "--defs"
let flags = let flags =
[ no_checks_flag; dev_flag; debug_flag; sanitize_flag; two_process_flag; [ no_checks_flag; dev_flag; debug_flag; sanitize_flag; two_process_flag;
x86_flag; llvm_flag; no_annotate_flag; no_gc_flag; warn_memory_flag ] x86_flag; llvm_flag; no_annotate_flag; no_gc_flag; warn_memory_flag;
defs_flag ]
(* The warnings, where errors go. Printed one to a location in the repo's (* The warnings, where errors go. Printed one to a location in the repo's
standard [file:line:col:] shape, with the squiggle [Loc.entry] draws, so standard [file:line:col:] shape, with the squiggle [Loc.entry] draws, so
@ -355,6 +360,7 @@ let () =
files files
| _ :: "check" :: args when List.exists (fun a -> not (is_flag a)) args -> | _ :: "check" :: args when List.exists (fun a -> not (is_flag a)) args ->
let warn_memory = List.mem warn_memory_flag args in let warn_memory = List.mem warn_memory_flag args in
let defs = List.mem defs_flag args in
let files = List.filter (fun a -> not (is_flag a)) args in let files = List.filter (fun a -> not (is_flag a)) args in
List.iter List.iter
(fun path -> (fun path ->
@ -366,26 +372,28 @@ let () =
program that checked, which is what leaves the exit status program that checked, which is what leaves the exit status
alone. *) alone. *)
if warn_memory then print_memory_warnings ~file:path p; if warn_memory then print_memory_warnings ~file:path p;
List.iter if defs then begin
(fun (g : Flan.Tast.global) -> List.iter
Printf.printf "%s %s %s\n" (fun (g : Flan.Tast.global) ->
(* The three defining forms, told apart the way the compiler Printf.printf "%s %s %s\n"
tells them apart: [gconst] is the image, and [grerun] is (* The three defining forms, told apart the way the compiler
what a re-run does to the storage. A listing that called tells them apart: [gconst] is the image, and [grerun] is
both mutable forms one name could not answer the question what a re-run does to the storage. A listing that called
someone runs [flan check] on a dev file to ask. *) both mutable forms one name could not answer the question
(if g.gconst then "defconst" someone runs [flan check] on a dev file to ask. *)
else if g.grerun then "def" else "defonce") (if g.gconst then "defconst"
g.gname (Flan.Types.to_string g.gty)) else if g.grerun then "def" else "defonce")
p.globals; g.gname (Flan.Types.to_string g.gty))
List.iter p.globals;
(fun (f : Flan.Tast.fn) -> List.iter
if not (Flan.Check.internal_name f.name) then (fun (f : Flan.Tast.fn) ->
Printf.printf "defn %s : (Fn [%s] %s) %d slots\n" f.name if not (Flan.Check.internal_name f.name) then
(String.concat " " Printf.printf "defn %s : (Fn [%s] %s) %d slots\n" f.name
(List.map Flan.Types.to_string f.params)) (String.concat " "
(Flan.Types.to_string f.ret) (Array.length f.slots)) (List.map Flan.Types.to_string f.params))
p.fns)) (Flan.Types.to_string f.ret) (Array.length f.slots))
p.fns
end))
files files
(* The generated C, for looking at. A wrong FFI binding is wrong in the (* The generated C, for looking at. A wrong FFI binding is wrong in the
wrapper, and the wrapper is not on disk anywhere — [Build] hands the text wrapper, and the wrapper is not on disk anywhere — [Build] hands the text
@ -982,7 +990,7 @@ let () =
| None -> code)) | None -> code))
| _ -> | _ ->
prerr_endline prerr_endline
"usage: flan (read|parse|check|emit|shim) <file.flan>...\n flan check <file.flan>... [--warn-memory]\n flan emit <file.flan> [--x86] [--dev] [--debug] [--no-bounds-checks]\n\ "usage: flan (read|parse|check|emit|shim) <file.flan>...\n flan check <file.flan>... [--warn-memory] [--defs]\n flan emit <file.flan> [--x86] [--dev] [--debug] [--no-bounds-checks]\n\
\ flan import-c <header.h> [package.flan...] [clang flags...]\n\ \ flan import-c <header.h> [package.flan...] [clang flags...]\n\
\ flan generate-c <package-dir>\n\ \ flan generate-c <package-dir>\n\
\ flan build <file.flan> [-o out] [-O0|-O1|-O2|-O3] \ \ flan build <file.flan> [-o out] [-O0|-O1|-O2|-O3] \

View File

@ -97,7 +97,7 @@ fine here. Brackets and strings are still paired."
'("fn" "fn-" "def" "once" "const" "struct" "union" "data" "enum" "import" '("fn" "fn-" "def" "once" "const" "struct" "union" "data" "enum" "import"
"if" "elif" "else" "while" "until" "for" "match" "let" "return" "break" "if" "elif" "else" "while" "until" "for" "match" "let" "return" "break"
"continue" "defer" "handler-case" "handler-bind" "restart-case" "on" "continue" "defer" "handler-case" "handler-bind" "restart-case" "on"
"restart" "quote" "macro" "loop")) "restart" "quote" "macro" "loop" "type"))
;; The headers whose block follows on the lines under them. `defer' and ;; The headers whose block follows on the lines under them. `defer' and
;; `quote' open one only when nothing follows them on the line; `fn' does not ;; `quote' open one only when nothing follows them on the line; `fn' does not
@ -112,7 +112,7 @@ fine here. Brackets and strings are still paired."
'(("fn" . "defn") ("fn-" . "defn-") ("def" . "def") ("once" . "defonce") '(("fn" . "defn") ("fn-" . "defn-") ("def" . "def") ("once" . "defonce")
("const" . "defconst") ("struct" . "defstruct") ("data" . "defdata") ("const" . "defconst") ("struct" . "defstruct") ("data" . "defdata")
("enum" . "defenum") ("union" . "defunion") ("import" . "import") ("enum" . "defenum") ("union" . "defunion") ("import" . "import")
("macro" . "defmacro")) ("macro" . "defmacro") ("type" . "defalias"))
"Each declaration header word, and the paren head it reads as.") "Each declaration header word, and the paren head it reads as.")
;;; Syntax ;;; Syntax
@ -1560,7 +1560,10 @@ lambda or a `Fn(...)' type, and not after a match arm's."
1 font-lock-keyword-face) 1 font-lock-keyword-face)
(,(concat "^\\(fn-?\\|macro\\)[ \t]+" flan-fln--name-re) (,(concat "^\\(fn-?\\|macro\\)[ \t]+" flan-fln--name-re)
2 font-lock-function-name-face) 2 font-lock-function-name-face)
(,(concat "^\\(?:struct\\|data\\|union\\|enum\\)[ \t]+" flan-fln--name-re) (,(concat "^\\(?:struct\\|data\\|union\\|enum\\|type\\)[ \t]+" flan-fln--name-re)
1 font-lock-type-face)
;; An alias's type, `type Row = Vec(i32)'.
(,(concat "^type[ \t]+[^][ \t\n(){},;\":]+[ \t]+=[ \t]+" flan-fln--name-re)
1 font-lock-type-face) 1 font-lock-type-face)
;; A condition's parent, `struct DiskFull :parent IoError'. ;; 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(){},;\":]+[ \t]+:parent[ \t]+" flan-fln--name-re)
@ -1600,7 +1603,7 @@ lambda or a `Fn(...)' type, and not after a match arm's."
(defvar flan-fln-imenu-generic-expression (defvar flan-fln-imenu-generic-expression
`(("Functions" ,(concat "^fn-?[ \t]+" flan-fln--name-re) 1) `(("Functions" ,(concat "^fn-?[ \t]+" flan-fln--name-re) 1)
("Macros" ,(concat "^\\(?:macro[ \t]+\\|defmacro(\\)" flan-fln--name-re) 1) ("Macros" ,(concat "^\\(?:macro[ \t]+\\|defmacro(\\)" flan-fln--name-re) 1)
("Types" ,(concat "^\\(?:struct\\|data\\|union\\|enum\\)[ \t]+" flan-fln--name-re) 1) ("Types" ,(concat "^\\(?:struct\\|data\\|union\\|enum\\|type\\)[ \t]+" flan-fln--name-re) 1)
("Variables" ,(concat "^\\(?:def\\|once\\|const\\)[ \t]+" flan-fln--name-re) 1)) ("Variables" ,(concat "^\\(?:def\\|once\\|const\\)[ \t]+" flan-fln--name-re) 1))
"Imenu index for `flan-fln-mode'.") "Imenu index for `flan-fln-mode'.")
@ -1610,7 +1613,7 @@ lambda or a `Fn(...)' type, and not after a match arm's."
(when s (when s
(save-excursion (save-excursion
(goto-char s) (goto-char s)
(and (looking-at (concat "\\(?:fn-?\\|macro\\|def\\|once\\|const\\|struct\\|data\\|union\\|enum\\)[ \t]+" (and (looking-at (concat "\\(?:fn-?\\|macro\\|def\\|once\\|const\\|struct\\|data\\|union\\|enum\\|type\\)[ \t]+"
flan-fln--name-re)) flan-fln--name-re))
(match-string-no-properties 1)))))) (match-string-no-properties 1))))))

View File

@ -88,6 +88,10 @@ fn dir(d: Dir) -> i64
struct Oops :parent Error struct Oops :parent Error
code: i64 code: i64
type Count = i64
fn counted(n: Count) -> Count = n + 1
fn oops-code() -> i64 fn oops-code() -> i64
handler-case handler-case
error(Oops{.code 7}) error(Oops{.code 7})
@ -235,6 +239,8 @@ comment():
("fn rs" "(rs)" "3") ("fn rs" "(rs)" "3")
("fn dir" "(dir :north)" "7") ("fn dir" "(dir :north)" "7")
("struct Oops" "(oops-code)" "7") ("struct Oops" "(oops-code)" "7")
("type Count" "(counted 1)" "2")
("fn counted" "(counted 1)" "2")
("fn oops-code" "(oops-code)" "7") ("fn oops-code" "(oops-code)" "7")
("macro dbl-of" "(use-mac 5)" "10") ("macro dbl-of" "(use-mac 5)" "10")
("fn use-mac" "(use-mac 5)" "10") ("fn use-mac" "(use-mac 5)" "10")

View File

@ -714,6 +714,8 @@ of its line with AT-END."
(test-flan-fln--in "struct DiskFull :parent IoError (test-flan-fln--in "struct DiskFull :parent IoError
free: i64 free: i64
type Row = Vec(i64)
macro repeat(i, n, & body) macro repeat(i, n, & body)
quote quote
for ~i in range(~n) for ~i in range(~n)
@ -731,7 +733,14 @@ fn gcd(a: i32, b: i32) -> i32
(test-flan-fln--is "and :parent a keyword" (funcall face ":parent") 'font-lock-constant-face) (test-flan-fln--is "and :parent a keyword" (funcall face ":parent") 'font-lock-constant-face)
(test-flan-fln--is "macro is a keyword" (funcall face "macro") 'font-lock-keyword-face) (test-flan-fln--is "macro is a keyword" (funcall face "macro") 'font-lock-keyword-face)
(test-flan-fln--is "and its name a function's" (funcall face "repeat") 'font-lock-function-name-face) (test-flan-fln--is "and its name a function's" (funcall face "repeat") 'font-lock-function-name-face)
(test-flan-fln--is "loop is a keyword" (funcall face "loop") 'font-lock-keyword-face)) (test-flan-fln--is "loop is a keyword" (funcall face "loop") 'font-lock-keyword-face)
(test-flan-fln--is "type is a keyword" (funcall face "type") 'font-lock-keyword-face)
(test-flan-fln--is "an alias's name is a type" (funcall face "Row") 'font-lock-type-face)
(test-flan-fln--is "and so is what it names" (funcall face "Vec(i64)") 'font-lock-type-face))
(goto-char (point-min))
(search-forward "Row")
(test-flan-fln--is "an alias installs as a defalias"
(flan-fln--declaration-head-at (line-beginning-position)) "defalias")
(goto-char (point-min)) (goto-char (point-min))
(search-forward "~@body") (search-forward "~@body")
(test-flan-fln--is "a macro is one top-level form" (test-flan-fln--is "a macro is one top-level form"

View File

@ -26,7 +26,7 @@ let reserved =
[ "fn"; "fn-"; "def"; "once"; "const"; "struct"; "union"; "data"; "enum"; [ "fn"; "fn-"; "def"; "once"; "const"; "struct"; "union"; "data"; "enum";
"import"; "if"; "elif"; "else"; "while"; "until"; "match"; "let"; "for"; "import"; "if"; "elif"; "else"; "while"; "until"; "match"; "let"; "for";
"return"; "break"; "continue"; "defer"; "handler-case"; "handler-bind"; "return"; "break"; "continue"; "defer"; "handler-case"; "handler-bind";
"restart-case"; "quote"; "on"; "restart"; "macro"; "loop" ] "restart-case"; "quote"; "on"; "restart"; "macro"; "loop"; "type" ]
(* A symbol the reader gives back as itself when it is written bare. *) (* A symbol the reader gives back as itself when it is written bare. *)
let name_ok s = let name_ok s =
@ -1062,6 +1062,9 @@ and sugar n (f : Form.t) : string list option =
(ind (n + 2) ^ if is_sym "dyn" t then fname else fname ^ ": " ^ ty t)) (ind (n + 2) ^ if is_sym "dyn" t then fname else fname ^ ": " ^ ty t))
prs) prs)
| _ -> None) | _ -> None)
| Form.List [ { v = Form.Sym "defalias"; _ }; { v = Form.Sym name; _ }; t ]
when def_name name && type_shaped t ->
Some [ i ^ "type " ^ name ^ " = " ^ ty t ]
| Form.List [ { v = Form.Sym "defdata"; _ }; { v = Form.Sym name; _ }; { v = Form.Vec cs; _ } ] | Form.List [ { v = Form.Sym "defdata"; _ }; { v = Form.Sym name; _ }; { v = Form.Vec cs; _ } ]
when def_name name -> when def_name name ->
let case (c : Form.t) = let case (c : Form.t) =

View File

@ -1070,6 +1070,8 @@ let header_follow p s =
| "macro" -> | "macro" ->
n.sp && plain_name n.tok n.sp && plain_name n.tok
&& (let a = peek_at p 2 in a.tok = LP && not a.sp) && (let a = peek_at p 2 in a.tok = LP && not a.sp)
(* [type Row = Vec(i32)]: a name and its [=]. *)
| "type" -> n.sp && plain_name n.tok && (peek_at p 2).tok = NAME "="
(* [loop x = a, ...]: a name and its [=]. A name and a comma or the end (* [loop x = a, ...]: a name and its [=]. A name and a comma or the end
of the line, or [loop] alone over a block, is a loop missing its first of the line, or [loop] alone over a block, is a loop missing its first
values, which [header] answers. *) values, which [header] answers. *)
@ -1551,6 +1553,12 @@ and header (s : st) w : Form.t =
(match parent with (match parent with
| None -> [ name; fv ] | None -> [ name; fv ]
| Some (k, pt) -> name :: k :: pt :: (if fields = [] then [] else [ fv ])) | Some (k, pt) -> name :: k :: pt :: (if fields = [] then [] else [ fv ]))
| "type" ->
let name = name_tok p ~what:"the alias's name" in
expect_name p "=" ~what:"= and the type it names";
let t = ty p in
expect_eol p ~after:(text_of t);
named "defalias" [ name; t ]
| "macro" -> | "macro" ->
let name = name_tok p ~what:"the macro's name" in let name = name_tok p ~what:"the macro's name" in
let lp = glued_lp p ~what:"the parameters, in parentheses glued to the name" in let lp = glued_lp p ~what:"the parameters, in parentheses glued to the name" in

View File

@ -291,6 +291,7 @@ Each item: the proposal, then the reason in one line.
`struct DiskFull :parent IoError` with its field lines, reading `struct DiskFull :parent IoError` with its field lines, reading
`(defstruct DiskFull :parent IoError [free i64])`; with no field lines it `(defstruct DiskFull :parent IoError [free i64])`; with no field lines it
reads `(defstruct IoError :parent Error)`. **Built.** reads `(defstruct IoError :parent Error)`. **Built.**
- `type Row = Vec(i32)` reads `(defalias Row (Vec i32))`. **Built.**
- `macro repeat(i, n, & body)` plus a block reads - `macro repeat(i, n, & body)` plus a block reads
`(defmacro repeat [i n & body] …)`. A parameter is a bare name, a `(defmacro repeat [i n & body] …)`. A parameter is a bare name, a
destructuring vector `[a b]`, or `& rest`, last. **Built.** destructuring vector `[a b]`, or `& rest`, last. **Built.**
@ -300,7 +301,7 @@ Each item: the proposal, then the reason in one line.
- `import rl "vendor:raylib"`. **Built.** - `import rl "vendor:raylib"`. **Built.**
- **Every other form uses the fallback** (next item) until someone asks for - **Every other form uses the fallback** (next item) until someone asks for
sugar: `defclass`, `defgeneric`, `defmulti`, `defmethod`, `declare`, sugar: `defclass`, `defgeneric`, `defmulti`, `defmethod`, `declare`,
`declare-c`, `defalias`, `array-fill`. **Built.** `declare-c`, `array-fill`. **Built.**
The class forms keep the fallback for good (2026-09-26): The class forms keep the fallback for good (2026-09-26):
`defmethod(area, point, [p]):` reads well enough. `defmethod(area, point, [p]):` reads well enough.

View File

@ -7314,6 +7314,14 @@ level "1"
cli_case "check on a file that is not there" cli_case "check on a file that is not there"
"check no-such-file.flan" ~code:1 "check no-such-file.flan" ~code:1
~says:[ "no-such-file.flan"; "No such file or directory" ]; ~says:[ "no-such-file.flan"; "No such file or directory" ];
(* A file that checks prints nothing; --defs lists what it defined. *)
(match cli "check programs/recur.flan" with
| 0, "" -> ()
| code, text ->
incr failures;
Printf.printf "FAIL a clean check prints nothing\n got: %S (exit %d)\n" text code);
cli_case "check --defs lists the definitions" "check programs/recur.flan --defs" ~code:0
~says:[ "defn gcd : (Fn [i32 i32] i32)" ];
(* And every other front end takes the same route, since the arm is on the (* And every other front end takes the same route, since the arm is on the
one wrapper they all go through. *) one wrapper they all go through. *)
cli_case "build on a file that is not there" cli_case "build on a file that is not there"

View File

@ -555,6 +555,9 @@ let () =
reads "struct" "struct Cell\n row: i32\n tag" "(defstruct Cell [row i32 tag dyn])"; reads "struct" "struct Cell\n row: i32\n tag" "(defstruct Cell [row i32 tag dyn])";
reads "struct with a parent" "struct DiskFull :parent IoError\n free: i64" reads "struct with a parent" "struct DiskFull :parent IoError\n free: i64"
"(defstruct DiskFull :parent IoError [free i64])"; "(defstruct DiskFull :parent IoError [free i64])";
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 parent and no fields" "struct Io :parent Error" "(defstruct Io :parent Error)"; 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" 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))))"; "(defmacro repeat [i n & body] (quasiquote (do (f (unquote i)) (unquote-splicing body))))";
@ -824,6 +827,7 @@ let () =
prints "a template's for keeps its unquotes" prints "a template's for keeps its unquotes"
"(defmacro m [i n & body] `(dotimes [~i ~n] ~@body))" "for ~i in range(~n)"; "(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 macro" "(defmacro m [[a b] n & body] `(do ~@body))" "macro m([a b], n, & body)\n quote";
prints "a type alias" "(defalias Row (Vec i32))" "type Row = Vec(i32)";
prints "a struct with a parent" "(defstruct D :parent Io [free i64])" prints "a struct with a parent" "(defstruct D :parent Io [free i64])"
"struct D :parent Io\n free: i64"; "struct D :parent Io\n free: i64";
prints "a parent with no fields" "(defstruct D :parent Io)" "struct D :parent Io"; prints "a parent with no fields" "(defstruct D :parent Io)" "struct D :parent Io";