A class and each case of a data type are names on the definitions list

This commit is contained in:
Joseph Ferano 2026-09-25 07:09:42 +07:00
parent 2e58880160
commit cf028e23ea
5 changed files with 162 additions and 24 deletions

View File

@ -1718,11 +1718,13 @@ with the selected frame's locals.
** TODO Hex, binary and an address on a primitive in the inspector ** TODO Hex, binary and an address on a primitive in the inspector
The last item of the Emacs batch besides the break buffer, and independent of it. The last item of the Emacs batch besides the break buffer, and independent of it.
** TODO A defclass is not on the definitions list as a type, and a sum's cases are not drawn ** DONE A defclass is not on the definitions list as a type, and a sum's cases are not drawn
A =defclass= is expanded away before the checker — a dyn map and a shape tag by CLOSED: [2026-09-25]
then — so there is no class table to read. A sum's cases are one symbol each and =defs= reads classes off the session's declarations, which still hold every
the daemon answers with the type's name only. =CFn= also wants adding to the type =defclass=, and lists each as kind =class= with its slots and location; its
rule. constructor is not listed again as a =fn=. Each data case is a =case= row named
=Type.Case=, drawn whenever =data= is, and the data row's signature lists its
cases. =CFn= is in =flan-mode='s type rule.
** CANCELLED A flycheck checker, and a structured JSON report ** CANCELLED A flycheck checker, and a structured JSON report
CLOSED: [2026-09-20] CLOSED: [2026-09-20]

View File

@ -1850,8 +1850,9 @@ other, and KIND is what tells it apart where that matters.")
"Which kinds of name to colour by what the running program says they are. "Which kinds of name to colour by what the running program says they are.
A list of kinds drawn from `flan--dynamic-faces': `macro', `fn', `var', A list of kinds drawn from `flan--dynamic-faces': `macro', `fn', `var',
`const', `struct', `data', `union', `enum', `alias', `extern', `builtin'. `const', `struct', `data', `union', `enum', `alias', `class', `extern',
t draws all of them and nil draws none. `builtin'. t draws all of them and nil draws none. A data type's cases are
drawn when `data' is on the list.
The default is macros alone, which is the one kind a reader cannot work out The default is macros alone, which is the one kind a reader cannot work out
from the call itself — a macro does not evaluate its arguments, so the shape from the call itself — a macro does not evaluate its arguments, so the shape
@ -1916,7 +1917,11 @@ compiler has grown since, which is the point of asking rather than listing."
("data" . flan-type-face) ("data" . flan-type-face)
("union" . flan-type-face) ("union" . flan-type-face)
("enum" . flan-type-face) ("enum" . flan-type-face)
("alias" . flan-type-face)) ("alias" . flan-type-face)
;; A data type's case, `Shape.Rect', and a `defclass', which is a name
;; that constructs a value of that class.
("case" . flan-type-face)
("class" . flan-type-face))
"The face for each kind the `defs' op answers with. "The face for each kind the `defs' op answers with.
A kind not listed here is left undrawn rather than guessed at: the daemon is A kind not listed here is left undrawn rather than guessed at: the daemon is
allowed to grow the set, and a name drawn in the wrong colour says something allowed to grow the set, and a name drawn in the wrong colour says something
@ -1932,7 +1937,9 @@ The setting is written with symbols because that is what a user types; the
wire answers with strings." wire answers with strings."
(cond ((eq flan-font-lock-dynamically t) t) (cond ((eq flan-font-lock-dynamically t) t)
((null flan-font-lock-dynamically) nil) ((null flan-font-lock-dynamically) nil)
(t (and (memq (intern kind) flan-font-lock-dynamically) t)))) (t (and (memq (intern (if (equal kind "case") "data" kind))
flan-font-lock-dynamically)
t))))
(defvar flan--dynamic-face nil (defvar flan--dynamic-face nil
"The face `flan--dynamic-match' found, read by the font-lock rule after it.") "The face `flan--dynamic-match' found, read by the font-lock rule after it.")
@ -1973,10 +1980,10 @@ Leaves its face in `flan--dynamic-face' for the rule that calls this."
(let ((sym (match-string-no-properties 0))) (let ((sym (match-string-no-properties 0)))
(setq face (gethash sym flan--dynamic-table)) (setq face (gethash sym flan--dynamic-table))
;; A constructor is written `Type.Case', and a dot is a name character, ;; A constructor is written `Type.Case', and a dot is a name character,
;; so the whole thing is one symbol and no table could hold it — the ;; so the whole thing is one symbol. The daemon lists each case under
;; daemon answers with the type's name and knows nothing of the cases. ;; that full name, so the lookup above finds it. A dotted symbol it
;; The type half is drawn and the case half left alone, which is the ;; does not list, such as a case that does not exist, has only its
;; true statement: one of them is a name the program defines. ;; type half drawn.
(unless face (unless face
(let ((dot (string-search "." sym))) (let ((dot (string-search "." sym)))
(when (and dot (> dot 0)) (when (and dot (> dot 0))
@ -2050,10 +2057,11 @@ Called for its effect on one buffer; `flan--dynamic-sync' does every buffer."
(message "flan: %d names" (length flan--defs))) (message "flan: %d names" (length flan--defs)))
flan--defs) flan--defs)
(defconst flan--compiled-kinds '("fn" "macro") (defconst flan--compiled-kinds '("fn" "macro" "class")
"The kinds that have a body the compiler emitted code for. "The kinds that have a body the compiler emitted code for.
A macro is one: `Parse' desugars `(defmacro m [a] …)' into a `defn', so it is A macro is one: `Parse' desugars `(defmacro m [a] …)' into a `defn', so it is
compiled, installed and disassemblable exactly as a function is. It reaches compiled, installed and disassemblable exactly as a function is. So is a
class, whose name is its constructor `defn'. It reaches
this end as kind `macro' rather than as `fn' — that is the point of the kind this end as kind `macro' rather than as `fn' — that is the point of the kind
— and every list that offers \"a thing with a body\" has to say both words or — and every list that offers \"a thing with a body\" has to say both words or
it silently stops offering macros.") it silently stops offering macros.")

View File

@ -485,7 +485,10 @@
("gravity" "const" "gravity f32" "" "") ("gravity" "const" "gravity f32" "" "")
("with-retry" "macro" "with-retry [args] Form" "" "") ("with-retry" "macro" "with-retry [args] Form" "" "")
("Pixel" "struct" "Pixel" "" "") ("Pixel" "struct" "Pixel" "" "")
("Shape" "data" "Shape" "" "") ("Shape" "data" "Shape [Empty (Dot [x f64 y f64])]" "" "")
("Shape.Empty" "case" "Shape.Empty" "" "")
("Shape.Dot" "case" "(Shape.Dot [x f64 y f64])" "" "")
("point" "class" "point [x y]" "sand.flan:20:1" "")
("Key" "enum" "Key" "" "") ("Key" "enum" "Key" "" "")
("sim/step" "fn" "sim/step [] ()" "sand.flan:9" "") ("sim/step" "fn" "sim/step [] ()" "sand.flan:9" "")
("a/draw" "fn" "a/draw [] ()" "" "") ("a/draw" "fn" "a/draw [] ()" "" "")
@ -572,17 +575,30 @@
(let ((flan--defs test-flan-mode--defs)) (let ((flan--defs test-flan-mode--defs))
(not (member "ticks" (flan--compiled-names))))) (not (member "ticks" (flan--compiled-names)))))
;; A constructor is `Type.Case' and is one symbol, so the type half is what ;; A constructor is `Type.Case' and is one symbol. The daemon lists each
;; the program can speak for and the case half is left alone. ;; case under that name, so the whole symbol is drawn, and it is drawn when
;; the data type is: a case is part of its type, not a kind to ask for apart.
(test-flan--check (test-flan--check
"a constructor's type half is drawn" "a constructor is drawn whole, case half included"
(let ((flan-font-lock-dynamically t)) (let ((flan-font-lock-dynamically '(data)))
(eq (test-flan-mode--dyn-face "(match s (Shape.Dot) 1)" "Shape") (eq (test-flan-mode--dyn-face "(match s (Shape.Dot) 1)" ".Dot")
'flan-type-face))) 'flan-type-face)))
(test-flan--check (test-flan--check
"and its case half is not" "and not when data types are not asked for"
(null (test-flan-mode--dyn-face "(match s (Shape.Dot) 1)" ".Dot"))) (null (test-flan-mode--dyn-face "(match s (Shape.Dot) 1)" "Shape.Dot")))
(test-flan--check
"a case the program does not have draws only its type half"
(let ((flan-font-lock-dynamically t))
(and (eq (test-flan-mode--dyn-face "(match s (Shape.Nope) 1)" "Shape")
'flan-type-face)
(null (test-flan-mode--dyn-face "(match s (Shape.Nope) 1)" ".Nope")))))
(test-flan--check
"a class the program defines is drawn when classes are asked for"
(let ((flan-font-lock-dynamically '(class)))
(eq (test-flan-mode--dyn-face "(point 1 2)" "point") 'flan-type-face)))
(test-flan--check (test-flan--check
"a name the program has never heard of is left alone" "a name the program has never heard of is left alone"

View File

@ -1446,6 +1446,21 @@ let defs t =
[M-.] on a prelude macro from "the prelude is not a file on disk" into a [M-.] on a prelude macro from "the prelude is not a file on disk" into a
shrug about the daemon having no location. *) shrug about the daemon having no location. *)
let macro_locs = Hashtbl.create 16 in let macro_locs = Hashtbl.create 16 in
(* The classes, off the session's declarations: [Classes.expand] turns a
[defclass] into its constructor [defn] before the checker runs, so the
class is not in [Tast.program] or the checker's environment, and the
declarations are the one place that still has it. Its constructor is
dropped from the [fn] rows for the macro rows' reason: one name, one row,
and [class] is what was written. *)
let classes =
List.filter_map
(fun (d : Ast.decl) ->
match d.Ast.d with
| Ast.Defclass (n, slots) -> Some (n, List.map fst slots, d.Ast.dloc)
| _ -> None)
t.session.Session.decls
in
let class_names = List.map (fun (n, _, _) -> n) classes in
let fns = let fns =
List.filter_map List.filter_map
(fun (f : Tast.fn) -> (fun (f : Tast.fn) ->
@ -1456,6 +1471,7 @@ let defs t =
| None when List.mem f.Tast.name macro_names -> | None when List.mem f.Tast.name macro_names ->
Hashtbl.replace macro_locs f.Tast.name (Loc.to_string f.Tast.floc); Hashtbl.replace macro_locs f.Tast.name (Loc.to_string f.Tast.floc);
None None
| None when List.mem f.Tast.name class_names -> None
| None -> | None ->
Some Some
(entry ~name:f.Tast.name ~kind:"fn" ~sign:(signature_of_fn f) (entry ~name:f.Tast.name ~kind:"fn" ~sign:(signature_of_fn f)
@ -1550,9 +1566,53 @@ let defs t =
(fun name _ acc -> entry ~name ~kind ~sign:name ~loc:"" () :: acc) (fun name _ acc -> entry ~name ~kind ~sign:name ~loc:"" () :: acc)
tbl [] tbl []
in in
(* A data type's cases are names too: [Shape.Rect] is written at every
construction, and a [case] row is what lets an editor draw it and eldoc
show its fields. The data row's signature lists its cases in the order
and spelling of the [defdata]. *)
let case_sign label (v : Tast.variant) =
match v.Tast.vfields with
| [] -> label
| fs ->
Printf.sprintf "(%s [%s])" label
(String.concat " "
(List.map
(fun (f : Tast.field) ->
f.Tast.fname ^ " " ^ Types.to_string f.Tast.fty)
fs))
in
let datas =
Hashtbl.fold
(fun name (d : Tast.data) acc ->
let cases =
List.map
(fun (v : Tast.variant) ->
let full = name ^ "." ^ v.Tast.vname in
entry ~name:full ~kind:"case" ~sign:(case_sign full v)
~loc:"" ())
d.Tast.cases
in
entry ~name ~kind:"data"
~sign:
(Printf.sprintf "%s [%s]" name
(String.concat " "
(List.map (fun v -> case_sign v.Tast.vname v) d.Tast.cases)))
~loc:"" ()
:: cases
@ acc)
env.Check.datas []
in
let classes =
List.map
(fun (n, slots, loc) ->
entry ~name:n ~kind:"class"
~sign:(Printf.sprintf "%s [%s]" n (String.concat " " slots))
~loc:(Loc.to_string loc) ())
classes
in
List.sort compare List.sort compare
(of_table "struct" env.Check.structs (of_table "struct" env.Check.structs
@ of_table "data" env.Check.datas @ datas @ classes
@ of_table "union" env.Check.unions @ of_table "union" env.Check.unions
@ of_table "enum" env.Check.enums @ of_table "enum" env.Check.enums
@ of_table "alias" env.Check.aliases) @ of_table "alias" env.Check.aliases)

View File

@ -2271,6 +2271,33 @@ let () =
if not (await (fun () -> stopped (ask "(:op \"describe\")"))) then if not (await (fun () -> stopped (ask "(:op \"describe\")"))) then
fail "the inspect program never stopped" fail "the inspect program never stopped"
else begin else begin
(* A data type's cases are on [defs] under their full names, and the
type's own row lists them in the order the [defdata] does. *)
(let r = ask "(:op \"defs\")" in
let row name =
match Wire.field r "defs" with
| Some { Form.v = Form.List entries; _ } ->
List.find_map
(fun (e : Form.t) ->
match e.Form.v with
| Form.List
({ Form.v = Form.Str n; _ } :: { Form.v = Form.Str k; _ }
:: { Form.v = Form.Str s; _ } :: _)
when String.equal n name -> Some (k, s)
| _ -> None)
entries
| _ -> None
in
let want name kind sign =
match row name with
| Some (k, s) when k = kind && s = sign -> ()
| Some (k, s) -> fail "%s is on defs as (%s %S)" name k s
| None -> fail "defs did not mention %s" name
in
want "Shape" "data"
"Shape [Empty (Dot [x f64 y f64]) (Rect [w i32 h i32])]";
want "Shape.Empty" "case" "Shape.Empty";
want "Shape.Rect" "case" "(Shape.Rect [w i32 h i32])");
(* The slot travels by index and the index comes off the listing, (* The slot travels by index and the index comes off the listing,
which is the fourth element of each entry. Reading it here rather which is the fourth element of each entry. Reading it here rather
than writing 0 exercises the field the editor depends on, and keeps than writing 0 exercises the field the editor depends on, and keeps
@ -6519,6 +6546,31 @@ let () =
else begin else begin
if !answered <> "1" then if !answered <> "1" then
fail "the method the program was built with answered %S" !answered; fail "the method the program was built with answered %S" !answered;
(* A class is on [defs] as a class, with its slots and where it is
written, and its constructor is not listed a second time as a fn. *)
(let r = request c "(:op \"defs\")" in
match Wire.field r "defs" with
| Some { Form.v = Form.List entries; _ } ->
let rows name =
List.filter_map
(fun (e : Form.t) ->
match e.Form.v with
| Form.List
({ Form.v = Form.Str n; _ } :: { Form.v = Form.Str k; _ }
:: { Form.v = Form.Str s; _ }
:: { Form.v = Form.Str l; _ } :: _)
when String.equal n name -> Some (k, s, l)
| _ -> None)
entries
in
(match rows "point" with
| [ ("class", "point [x y]", loc) ]
when String.length loc > 0 && loc.[0] = '/' -> ()
| rs ->
fail "point is on defs as %s"
(String.concat "; "
(List.map (fun (k, s, l) -> k ^ " " ^ s ^ " " ^ l) rs)))
| _ -> fail "defs did not answer with a list");
(* A circle has no method yet, so the dispatch misses and the generic (* A circle has no method yet, so the dispatch misses and the generic
signals NoMethod — the answer a program handles, spelled here as signals NoMethod — the answer a program handles, spelled here as
the thing that makes the next step's success mean something. *) the thing that makes the next step's success mean something. *)