An eval or load reply carries the compiler's warnings, which Emacs marks at their sites and counts in the echo line, and a load says each warning once.

This commit is contained in:
Joseph Ferano 2026-09-26 19:20:40 +07:00
parent bbaca242e5
commit 6c2f7c34c7
8 changed files with 184 additions and 19 deletions

View File

@ -1847,6 +1847,62 @@ Returns non-nil when it put an overlay somewhere."
(goto-char beg)) (goto-char beg))
t))))))) t)))))))
;;; Warnings
;; A warning is drawn the way an error is — a mark at the site, the message
;; beside it, gone at the next command — and logged the same way, in the
;; compiler's own `file:line:col: warning: text' shape, which is what gives it
;; `compilation-minor-mode''s warning face in the log. The marks carry
;; `flan-error' as well, so every path that takes an error mark down takes
;; these with it.
(defface flan-warning-face
'((t :inherit warning :underline (:style wave)))
"Face for the text a compiler warning is about."
:group 'flan)
(defface flan-warning-message-face
'((t :inherit warning :height 0.9))
"Face for a warning's message, shown beside the form it is about."
:group 'flan)
(defun flan--warning-overlays (&optional buffer)
"The Flan warning overlays in BUFFER, or in the current buffer."
(with-current-buffer (or buffer (current-buffer))
(seq-filter (lambda (o) (overlay-get o 'flan-warning))
(overlays-in (point-min) (point-max)))))
(defun flan--show-warning (file line col msg)
"Mark MSG at LINE and COL of FILE, if some buffer is visiting it."
(let ((buf (flan--buffer-visiting file)))
(when buf
(with-current-buffer buf
(let* ((beg (flan--position line col))
(end (save-excursion (goto-char beg) (line-end-position)))
(ov (make-overlay beg end buf t nil)))
(overlay-put ov 'flan-error t)
(overlay-put ov 'flan-warning t)
(overlay-put ov 'face 'flan-warning-face)
(overlay-put ov 'help-echo msg)
(overlay-put ov 'priority 90)
(overlay-put ov 'after-string
(propertize (concat " " msg)
'face 'flan-warning-message-face))
(add-hook 'pre-command-hook #'flan--clear-errors-on-command nil t)
t)))))
(defun flan--report-warnings (warnings)
"Log and mark each of WARNINGS, a reply's `:warnings' list."
(dolist (w warnings)
(let ((file (plist-get w :file))
(line (plist-get w :line))
(col (plist-get w :col))
(msg (plist-get w :message)))
(ignore-errors
(flan--record-diagnostic (format "%s:%d:%d" file line col)
(concat "warning: " msg)))
(ignore-errors (flan--show-warning file line col msg)))))
;;; Inline results ;;; Inline results
;; The value of an expression, drawn after the form it came from, the way eros ;; The value of an expression, drawn after the form it came from, the way eros
@ -2599,7 +2655,8 @@ has nothing to sit beside."
(names (plist-get reply :names)) (names (plist-get reply :names))
(note (plist-get reply :note)) (note (plist-get reply :note))
(value (plist-get reply :value)) (value (plist-get reply :value))
(stale (plist-get reply :stale))) (stale (plist-get reply :stale))
(warnings (plist-get reply :warnings)))
;; Accepted, so whatever the last rejection marked is no longer true. ;; Accepted, so whatever the last rejection marked is no longer true.
;; Redundant now that any command clears it — the command that ran this ;; Redundant now that any command clears it — the command that ran this
;; evaluation already did — and kept because it is the claim being ;; evaluation already did — and kept because it is the claim being
@ -2623,6 +2680,7 @@ has nothing to sit beside."
;; written first is wiped by whatever the drawing does to the buffer. ;; written first is wiped by whatever the drawing does to the buffer.
(when value (ignore-errors (flan--show-result value at))) (when value (ignore-errors (flan--show-result value at)))
(when stale (ignore-errors (flan--report-stale stale))) (when stale (ignore-errors (flan--report-stale stale)))
(when warnings (ignore-errors (flan--report-warnings warnings)))
(when flan-echo-result (when flan-echo-result
(cond (cond
;; An expression's value, rendered inside the running program — ;; An expression's value, rendered inside the running program —
@ -2646,9 +2704,13 @@ has nothing to sit beside."
;; is then empty by construction rather than a repeat of it. Now ;; is then empty by construction rather than a repeat of it. Now
;; that `C-x C-e' reaches this path too, that sentence is also how ;; that `C-x C-e' reaches this path too, that sentence is also how
;; you tell an installed declaration from an expression's `=>'. ;; you tell an installed declaration from an expression's `=>'.
(message "flan: %s installed in %.0f ms%s%s" (message "flan: %s installed in %.0f ms%s%s%s"
(flan--names-phrase (or fns names) what) (flan--names-phrase (or fns names) what)
(or (plist-get reply :ms) 0) (or (plist-get reply :ms) 0)
(if warnings
(format ", %d warning%s" (length warnings)
(if (= (length warnings) 1) "" "s"))
"")
(let ((vars (and fns (seq-difference names fns)))) (let ((vars (and fns (seq-difference names fns))))
(if vars (format " (also %s)" (if vars (format " (also %s)"
(flan--names-phrase vars "")) (flan--names-phrase vars ""))

View File

@ -899,6 +899,30 @@ already rely on it — so nothing here is a stand-in for the real thing."
(goto-char (point-max)) (goto-char (point-max))
(delete-region beg (point-max))) (delete-region beg (point-max)))
;; A form the compiler warns about installs, and the warning is shown as an
;; error is — marked at its site — and counted in the echo line.
(goto-char (point-max))
(let ((beg (point)))
(insert "\n(defn spin-forever [n i64] i64\n (println n)\n (spin-forever n))")
(search-backward "(println")
(let ((said (test-flan--said (flan-eval-defun))))
(test-flan--check "a warned-about form says so in the echo line"
(and said
(string-match-p "installed in [0-9]+ ms, 1 warning\\'"
said))))
(test-flan--check "and the warning is marked at the call it is about"
(let ((ovs (flan--warning-overlays)))
(and (= 1 (length ovs))
(string-match-p
"cannot return without first calling itself"
(or (overlay-get (car ovs) 'help-echo) ""))
(save-excursion
(goto-char (overlay-start (car ovs)))
(looking-at-p "(spin-forever n)")))))
(flan-clear-errors)
(goto-char (point-max))
(delete-region beg (point-max)))
;; A declaration can be written where it is prose and not a declaration, and ;; A declaration can be written where it is prose and not a declaration, and
;; the depth at its open delimiter says nothing about that: a form at column ;; the depth at its open delimiter says nothing about that: a form at column
;; 1 inside a comment or a string is at depth 0 like any other, so the head ;; 1 inside a comment or a string is at depth 0 like any other, so the head

View File

@ -20811,6 +20811,10 @@ let print_warnings = ref true
is a build or a check, which says everything. *) is a build or a check, which says everything. *)
let warn_within : Loc.t list option ref = ref None let warn_within : Loc.t list option ref = ref None
(* The warnings [say_warnings] kept since [build_program] began, in order:
what a dev eval hands the editor in its reply as well as printing. *)
let said_warnings : Loc.diag list ref = ref []
let say_warnings (ds : Loc.diag list) = let say_warnings (ds : Loc.diag list) =
let within (at : Loc.t) = let within (at : Loc.t) =
match !warn_within with match !warn_within with
@ -20822,12 +20826,13 @@ let say_warnings (ds : Loc.diag list) =
&& s.Loc.line <= at.Loc.line && at.Loc.line <= max s.Loc.line s.Loc.eline) && s.Loc.line <= at.Loc.line && at.Loc.line <= max s.Loc.line s.Loc.eline)
spans spans
in in
let ds = List.filter (fun (d : Loc.diag) -> within d.Loc.dloc) ds in
said_warnings := !said_warnings @ ds;
if !print_warnings then if !print_warnings then
List.iter List.iter
(fun (d : Loc.diag) -> (fun (d : Loc.diag) ->
if within d.Loc.dloc then prerr_endline
prerr_endline (Loc.entry ~mark:'~' ~label:"warning: " d.Loc.dloc d.Loc.dmsg))
(Loc.entry ~mark:'~' ~label:"warning: " d.Loc.dloc d.Loc.dmsg))
ds ds
(* A name the renaming above made, which nobody wrote: left out of every (* A name the renaming above made, which nobody wrote: left out of every
@ -21007,8 +21012,8 @@ let unconditional_recursion env (decls : Ast.decl list) : Loc.diag list =
let fln = fln_source at in let fln = fln_source at in
Loc.diag ~kind:"check/unconditional-recursion" at Loc.diag ~kind:"check/unconditional-recursion" at
(Printf.sprintf (Printf.sprintf
"%s calls itself on line %d on every path, so it never \ "%s cannot return without first calling itself on line %d, \
returns. If that call was meant to come after %s, it is %s \ so it never returns. If that call was meant to come after %s, it is %s \
by mistake; otherwise %s needs a base case, a path that \ by mistake; otherwise %s needs a base case, a path that \
returns without calling itself" returns without calling itself"
name at.Loc.line name at.Loc.line
@ -21025,6 +21030,7 @@ let unconditional_recursion env (decls : Ast.decl list) : Loc.diag list =
let build_program ~keep_going ?tolerate ?previous (decls : Ast.decl list) : let build_program ~keep_going ?tolerate ?previous (decls : Ast.decl list) :
Tast.program * env * string list = Tast.program * env * string list =
let env = new_env () in let env = new_env () in
said_warnings := [];
Hashtbl.reset arm_failed; Hashtbl.reset arm_failed;
(* ── A declaration left as it was compiled ─────────────────────────── (* ── A declaration left as it was compiled ───────────────────────────
A dev session installs a function whose signature changed, and a A dev session installs a function whose signature changed, and a

View File

@ -1073,6 +1073,21 @@ let errors_field (ds : Loc.diag list) =
(Wire.quote d.Loc.dmsg)) (Wire.quote d.Loc.dmsg))
ds) ] ds) ]
(* The compiler's warnings about the forms an eval sent, which it also
printed: the editor marks each where it is, as it marks an error. *)
let warnings_field (ds : Loc.diag list) =
match ds with
| [] -> []
| ds ->
[ ":warnings "
^ Wire.list
(List.map
(fun (d : Loc.diag) ->
Printf.sprintf "(:file %s :line %d :col %d :kind %s :message %s)"
(Wire.quote d.Loc.dloc.Loc.file) d.Loc.dloc.Loc.line
d.Loc.dloc.Loc.col (Wire.quote d.Loc.kind) (Wire.quote d.Loc.dmsg))
ds) ]
(* A refusal with several diagnostics: the first where every refusal puts its (* A refusal with several diagnostics: the first where every refusal puts its
message, all of them under [:errors]. *) message, all of them under [:errors]. *)
let errors_reply (ds : Loc.diag list) = let errors_reply (ds : Loc.diag list) =
@ -1118,7 +1133,7 @@ let eval ?forms ?base ?(extra = []) ?(step = false) t ~code ~origin ~pause =
^ Wire.quote ^ Wire.quote
"loaded; the program has ended, so this is in it when M-x \ "loaded; the program has ended, so this is in it when M-x \
flan-rerun starts it again" ] flan-rerun starts it again" ]
@ extra) @ warnings_field c.Session.warnings @ extra)
| exception Loc.Error { Loc.dloc = l; dmsg = msg; _ } -> | exception Loc.Error { Loc.dloc = l; dmsg = msg; _ } ->
Session.restore t.session before; Session.restore t.session before;
error ~loc:(Loc.to_string l) msg error ~loc:(Loc.to_string l) msg
@ -1136,7 +1151,7 @@ let eval ?forms ?base ?(extra = []) ?(step = false) t ~code ~origin ~pause =
ok ok
([ ":names " ^ Wire.strings c.Session.names; ":fns ()"; ([ ":names " ^ Wire.strings c.Session.names; ":fns ()";
":note " ^ Wire.quote "nothing to install" ] ":note " ^ Wire.quote "nothing to install" ]
@ extra) @ warnings_field c.Session.warnings @ extra)
| c -> | c ->
(* Everything from here to the delivery is inside the restore, and by (* Everything from here to the delivery is inside the restore, and by
exception type as well as by arm. The three named below are the ones exception type as well as by arm. The three named below are the ones
@ -1184,6 +1199,7 @@ let eval ?forms ?base ?(extra = []) ?(step = false) t ~code ~origin ~pause =
Printf.sprintf ":ms %.1f" Printf.sprintf ":ms %.1f"
(timing.Build.llc_ms +. timing.Build.link_ms) ] (timing.Build.llc_ms +. timing.Build.link_ms) ]
@ stale_field c.Session.stale @ stale_field c.Session.stale
@ warnings_field c.Session.warnings
@ (match pause with @ (match pause with
| Some (l, c) -> | Some (l, c) ->
[ ":pause " ^ Wire.quote (Printf.sprintf "%d:%d" l c) ] [ ":pause " ^ Wire.quote (Printf.sprintf "%d:%d" l c) ]
@ -1227,10 +1243,16 @@ let load_file t ~code ~origin =
is. *) is. *)
let base = if Sys.file_exists origin then Some origin else None in let base = if Sys.file_exists origin then Some origin else None in
let running = liveness t <> Parked in let running = liveness t <> Parked in
(* A trial, which [Session.pruned] may run several times over the same
forms: only the [eval] of what survives says the warnings. *)
let check forms = let check forms =
let before = Session.held t.session in let before = Session.held t.session in
let was = !Check.print_warnings in
Check.print_warnings := false;
Fun.protect Fun.protect
~finally:(fun () -> Session.restore t.session before) ~finally:(fun () ->
Check.print_warnings := was;
Session.restore t.session before)
(fun () -> (fun () ->
ignore (Session.eval ~origin ?base ~forms ~running t.session code)) ignore (Session.eval ~origin ?base ~forms ~running t.session code))
in in

View File

@ -688,6 +688,8 @@ type change = {
change leaves behind, and the ones earlier changes left that this one change leaves behind, and the ones earlier changes left that this one
did not recompile. Empty for anything that builds no module. *) did not recompile. Empty for anything that builds no module. *)
stale : stale list; stale : stale list;
(* The compiler's warnings about the forms sent, as it printed them. *)
warnings : Loc.diag list;
} }
(* The bodies a change installed, as whoever reads the reply wrote them: a (* The bodies a change installed, as whoever reads the reply wrote them: a
@ -1095,6 +1097,7 @@ let eval ?(origin = "<eval>") ?base ?forms ?pause ?(step = false) ?(running = tr
(* Every error in the form sent, not the first: [keep_going] checks past a (* Every error in the form sent, not the first: [keep_going] checks past a
refused subexpression (see [Check.check]). One error is still raised as refused subexpression (see [Check.check]). One error is still raised as
[Loc.Error], which is what every caller of one form expects. *) [Loc.Error], which is what every caller of one form expects. *)
let said = ref [] in
let program, env, tolerated = let program, env, tolerated =
(* A tolerated body whose return type is read off it keeps the (* A tolerated body whose return type is read off it keeps the
signature the process has for it. *) signature the process has for it. *)
@ -1115,7 +1118,7 @@ let eval ?(origin = "<eval>") ?base ?forms ?pause ?(step = false) ?(running = tr
Check.program_tolerant ~keep_going:true ~tolerate:stale_owner ~previous Check.program_tolerant ~keep_going:true ~tolerate:stale_owner ~previous
decls) decls)
with with
| r -> r | r -> said := !Check.said_warnings; r
| exception Loc.Errors [ d ] -> raise (Loc.Error d) | exception Loc.Errors [ d ] -> raise (Loc.Error d)
in in
let program = let program =
@ -1568,7 +1571,8 @@ let eval ?(origin = "<eval>") ?base ?forms ?pause ?(step = false) ?(running = tr
fns <> [] || allocates || consts <> [] || run_thunk <> None; fns <> [] || allocates || consts <> [] || run_thunk <> None;
stale = stale =
stale_sites ~live ~running ~inferred:(Check.inferred_cause env) built stale_sites ~live ~running ~inferred:(Check.inferred_cause env) built
program } program;
warnings = !said }
(* ── Evaluating an expression ──────────────────────────────────────── *) (* ── Evaluating an expression ──────────────────────────────────────── *)
@ -2278,7 +2282,7 @@ let write_slot ?(origin = "<set>") t ~frame ~(fn : Tast.fn) ~slot ~path
Tast.fns = t.program.Tast.fns @ fresh; Tast.fns = t.program.Tast.fns @ fresh;
structs = t.program.Tast.structs @ copies }; structs = t.program.Tast.structs @ copies };
Ok Ok
({ ir; x86 = t.x86; names = []; fns = []; installs = true; stale = [] }, ({ ir; x86 = t.x86; names = []; fns = []; installs = true; stale = []; warnings = [] },
where, Types.to_string shown.Tast.ty)))) where, Types.to_string shown.Tast.ty))))
(* ── A typed restart, taken from the break loop ──────────────────────── *) (* ── A typed restart, taken from the break loop ──────────────────────── *)
@ -2407,7 +2411,7 @@ let arm_restart ?(origin = "<restart>") t ~index ~(params : Types.t list)
Tast.fns = t.program.Tast.fns @ fresh; Tast.fns = t.program.Tast.fns @ fresh;
structs = t.program.Tast.structs @ copies }; structs = t.program.Tast.structs @ copies };
Ok Ok
({ ir; x86 = t.x86; names = []; fns = []; installs = true; stale = [] }, ({ ir; x86 = t.x86; names = []; fns = []; installs = true; stale = []; warnings = [] },
List.map Types.to_string params) List.map Types.to_string params)
(* [pause] is [C-u C-x C-e] — §9's "last expression" target. It is a flag and (* [pause] is [C-u C-x C-e] — §9's "last expression" target. It is a flag and
@ -2626,7 +2630,7 @@ let eval_expr ?(origin = "<eval>") ?(pause = false) ?frame t src : change =
{ t.program with { t.program with
Tast.fns = t.program.Tast.fns @ fresh; Tast.fns = t.program.Tast.fns @ fresh;
structs = t.program.Tast.structs @ copies }; structs = t.program.Tast.structs @ copies };
{ ir; x86 = t.x86; names = []; fns = []; installs = true; stale = [] } { ir; x86 = t.x86; names = []; fns = []; installs = true; stale = []; warnings = [] }
(* ── What a macro call expands to ──────────────────────────────────── *) (* ── What a macro call expands to ──────────────────────────────────── *)

View File

@ -9581,6 +9581,41 @@ let () =
if not (contains_sub (value "(good)") "refused") then if not (contains_sub (value "(good)") "refused") then
fail "a form that called one left out was installed" fail "a form that called one left out was installed"
end; end;
(* A load checks its forms more than once to find the ones that do not
compile, and says each warning once: for a file that compiles whole
and for one with a form left out. The reply carries it too. *)
let times () =
let s = In_channel.with_open_bin mout In_channel.input_all in
let needle = "cannot return without first calling itself" in
let k = String.length needle and n = ref 0 in
for i = 0 to String.length s - k do
if String.sub s i k = needle then incr n
done;
!n
in
let warned what code =
let before = times () in
let r =
request mc
(Printf.sprintf "(:op \"load-file\" :file %S :code %s)"
("programs/dev-load-" ^ what ^ ".flan") (Wire.quote code))
in
let sent =
match Wire.field r "warnings" with
| Some { Form.v = Form.List l; _ } -> List.length l
| _ -> 0
in
if status r <> "ok" then fail "a load of %s: %s" what (said r)
else begin
if sent <> 1 then fail "a load of %s replied with %d warnings" what sent;
let after = times () in
if after - before <> 1 then
fail "a load of %s printed its warning %d times" what (after - before)
end
in
warned "spin" "(defn spin [n i64] i64 (println n) (spin n))";
warned "spin-bad"
"(defn spin2 [n i64] i64 (println n) (spin2 n))\n(defn bad2 [] i64 \"x\")";
(* Nothing compiles: a refusal, with the error where every refusal (* Nothing compiles: a refusal, with the error where every refusal
puts it and the list beside it. *) puts it and the list beside it. *)
let r = let r =

View File

@ -6690,8 +6690,8 @@ let () =
with with
| Some [ (4, m) ] -> | Some [ (4, m) ] ->
check "a self-call after a loop, outside any branch, is warned at" check "a self-call after a loop, outside any branch, is warned at"
(m = "count-handshakes calls itself on line 4 on every path, so it \ (m = "count-handshakes cannot return without first calling itself \
never returns. If that call was meant to come after the \ on line 4, so it never returns. If that call was meant to come after the \
function, it is indented into the body by mistake; otherwise \ function, it is indented into the body by mistake; otherwise \
count-handshakes needs a base case, a path that returns without \ count-handshakes needs a base case, a path that returns without \
calling itself") calling itself")
@ -6728,6 +6728,11 @@ let () =
"fn g(n: i64) -> i64\n when n > 3\n return 1\n g(n + 1)\n"; "fn g(n: i64) -> i64\n when n > 3\n return 1\n g(n + 1)\n";
quiet "a self-call behind ?? is not warned at" ~fln:true quiet "a self-call behind ?? is not warned at" ~fln:true
"fn c(a: i64?, n: i64) -> i64\n a ?? c(a, n)\n"; "fn c(a: i64?, n: i64) -> i64\n a ?? c(a, n)\n";
(* A call that never comes back but is not typed Never leaves the
self-call dead, and the function still never returns, which is all the
warning claims. *)
warns "a self-call after a call that exits is warned at" ~fln:true
"fn boom() -> ()\n exit(1)\n\nfn f(n: i64) -> ()\n boom()\n f(n)\n" 6;
quiet "a self-call after exit is not warned at" ~fln:true quiet "a self-call after exit is not warned at" ~fln:true
"fn q(n: i64)\n exit(1)\n q(n)\n"; "fn q(n: i64)\n exit(1)\n q(n)\n";
quiet "a self-call after an endless loop is not warned at" ~fln:true quiet "a self-call after an endless loop is not warned at" ~fln:true

View File

@ -2272,14 +2272,21 @@ let () =
(match !Check.recursion_warnings with (match !Check.recursion_warnings with
| [ d ] when d.Loc.kind = "check/unconditional-recursion" && d.Loc.dloc.Loc.line = 4 -> () | [ d ] when d.Loc.kind = "check/unconditional-recursion" && d.Loc.dloc.Loc.line = 4 -> ()
| ds -> fail "a dev eval of count-handshakes found %d recursions" (List.length ds)); | ds -> fail "a dev eval of count-handshakes found %d recursions" (List.length ds));
if not (has said "warning: count-handshakes calls itself on line 4 on every path") if not (has said "warning: count-handshakes cannot return without first calling itself on line 4")
then fail "a dev eval printed no recursion warning: %S" said; then fail "a dev eval printed no recursion warning: %S" said;
(match r.Session.warnings with
| [ a; b ] when a.Loc.kind = "check/shadows-builtin"
&& b.Loc.kind = "check/unconditional-recursion" -> ()
| ds -> fail "the eval's change carries %d warnings, not the 2 it printed"
(List.length ds));
if warnings said <> 2 then if warnings said <> 2 then
fail "the eval that sent them printed %d warnings, not 2: %S" (warnings said) said; fail "the eval that sent them printed %d warnings, not 2: %S" (warnings said) said;
let _, again = let r2, again =
stderr_of (fun () -> stderr_of (fun () ->
Session.eval ~origin:"other.fln" t "fn other-thing(x: i64) -> i64\n x + 1\n") Session.eval ~origin:"other.fln" t "fn other-thing(x: i64) -> i64\n x + 1\n")
in in
if r2.Session.warnings <> [] then
fail "an unrelated eval's change carries an earlier form's warnings";
if warnings again <> 0 then if warnings again <> 0 then
fail "an unrelated eval said an earlier form's warnings again: %S" again); fail "an unrelated eval said an earlier form's warnings again: %S" again);