Skip to content

Commit 701cf69

Browse files
Report the proof state when an error occurs during a proof (#1453)
Attach the proof state to errors raised while a proof is in progress, using the err_desc field of Fatal: the goals a failing tactic was applied to, the goals before and after the tactic for a subproof-count mismatch, and the remaining goals for an unfinished proof at `end`. The error message itself is unchanged; the state is printed after it, uncolored. The LSP server disables this for tactic failures since editors display the proof state themselves.
1 parent 8a02c23 commit 701cf69

11 files changed

Lines changed: 178 additions & 15 deletions

File tree

CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
3333
- LSP server: position of the error is removed from diagnostics when the error occurs in the file currently open in the editor.
3434
- Syntax of search query is modified as follows : `in` is used instead of `|` (filtering). `with` is used instead of `,` (conjunction). `|` is used instead of `;` (disjunction).
3535
- Type of `#assume` in order to generate a new symbol and use it inside a tactic term.
36+
- Errors occurring while a proof is in progress now report the proof state: the goals a failing tactic was applied to, the goals before and after the tactic for a subproof-count mismatch, and the remaining goals when a proof is unfinished at `end`. The state is printed after the error message, which stays unchanged. The LSP server does not attach the proof state to tactic failures since editors display it themselves.
3637

3738
### Fixed
3839

src/handle/command.ml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -567,7 +567,9 @@ let get_proof_data : compiler -> sig_state -> p_command -> cmd_output =
567567
| P_proof_end ->
568568
(* Check that the proof is indeed finished. *)
569569
if not (finished ps) then
570-
fatal pe.pos "The proof is not finished:@.%a" goals ps;
570+
fatal pe.pos
571+
~err_desc:(Format.asprintf "Proof state:@.%a@." goals ps)
572+
"The proof is not finished.";
571573
(* Keep the definition only if the symbol is not opaque. *)
572574
let d =
573575
if opaq then None else

src/handle/proof.ml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,31 @@ let goals : proof_state pp = fun ppf ps ->
2727
let goal ppf i g = out ppf "\n%d. %a" (i+1) Goal.pp_no_hyp g in
2828
List.iteri (goal ppf) gs
2929

30+
(** [state_on_error] controls whether the failure of a tactic attaches the
31+
current proof state to the error, as the [err_desc] argument of the
32+
[Fatal] exception, printed after the error message. It is enabled by
33+
default, and disabled by the LSP server, which has its own means of
34+
displaying the proof state. Errors that always reported the proof state
35+
(subproof-count mismatches, unfinished proofs) keep doing so regardless
36+
of this setting. *)
37+
let state_on_error : bool Stdlib.ref = Stdlib.ref true
38+
39+
(** [error_state ?after ps] renders, for attachment to an error report, the
40+
proof state [ps] a failing tactic was applied to, followed, when the
41+
failure was only detected after the tactic was applied (a subproof-count
42+
mismatch), by the proof state [after] its application. *)
43+
let error_state : ?after:proof_state -> proof_state -> string =
44+
fun ?after before ->
45+
match after with
46+
| None ->
47+
Format.asprintf "Proof state before tactic application:@.%a@."
48+
goals before
49+
| Some after ->
50+
Format.asprintf
51+
"Proof state before tactic application:@.%a@.\
52+
Proof state after tactic application:@.%a@."
53+
goals before goals after
54+
3055
(** [remove_solved_goals ps] removes from the proof state [ps] the typing
3156
goals that are solved. *)
3257
let remove_solved_goals : proof_state -> proof_state = fun ps ->

src/handle/tactic.ml

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -818,30 +818,53 @@ let handle :
818818

819819
(** [handle sym_pos priv r tac n] applies the tactic [tac] from the previous
820820
tactic output [r] and checks that the number of goals of the new proof
821-
state is compatible with the number [n] of subproofs. *)
821+
state is compatible with the number [n] of subproofs. When [tac] fails,
822+
the proof state it was applied to is attached to the error (see
823+
{!val:Proof.state_on_error}). *)
822824
let handle :
823825
Sig_state.t -> popt -> bool -> tac_output -> p_tactic -> int -> tac_output =
824826
fun ss sym_pos priv (ps, _) t nb_subproofs ->
825-
let (ps', _) as a = handle ss sym_pos priv ps t in
827+
(* Attach the proof state [t] was applied to, to any error escaping its
828+
application. Errors raised inside tacticals like [try] or [orelse] are
829+
caught below this point, so only failures actually reported to the user
830+
are concerned. *)
831+
let (ps', _) as a =
832+
try handle ss sym_pos priv ps t
833+
with Fatal(p, msg, desc)
834+
when Stdlib.(!state_on_error) && ps.proof_goals <> [] ->
835+
let state = error_state ps in
836+
let desc = if desc = "" then state else desc ^ "\n" ^ state in
837+
raise (Fatal(p, msg, desc))
838+
in
826839
let nb_goals_before = List.length ps.proof_goals in
827840
let nb_goals_after = List.length ps'.proof_goals in
828841
let nb_newgoals = nb_goals_after - nb_goals_before in
842+
(* [t] ran, but the number of subproofs given does not match the number of
843+
subgoals it produced: report the proof state before and after its
844+
application. *)
845+
let mismatch : string -> 'a = fun reason ->
846+
fatal t.pos ~err_desc:(error_state ~after:ps' ps) "%s" reason in
829847
if nb_newgoals <= 0 then
830848
if nb_subproofs = 0 then a
831-
else fatal t.pos "A subproof is given but there is no subgoal."
849+
else mismatch "A subproof is given but there is no subgoal."
832850
else if is_destructive t then
833-
match nb_newgoals + 1 - nb_subproofs with
851+
(match nb_newgoals + 1 - nb_subproofs with
834852
| 0 -> a
835853
| n when n > 0 ->
836-
fatal t.pos "Missing subproofs (%d subproofs for %d subgoals):@.%a"
837-
nb_subproofs (nb_newgoals + 1) goals ps'
854+
mismatch (Printf.sprintf
855+
"Missing subproofs (%d subproofs for %d subgoals)."
856+
nb_subproofs (nb_newgoals + 1))
838857
| _ ->
839-
fatal t.pos "Too many subproofs (%d subproofs for %d subgoals):@.%a"
840-
nb_subproofs (nb_newgoals + 1) goals ps'
858+
mismatch (Printf.sprintf
859+
"Too many subproofs (%d subproofs for %d subgoals)."
860+
nb_subproofs (nb_newgoals + 1)))
841861
else match nb_newgoals - nb_subproofs with
842862
| 0 -> a
843863
| n when n > 0 ->
844-
fatal t.pos "Missing subproofs (%d subproofs for %d subgoals):@.%a"
845-
nb_subproofs nb_newgoals goals ps'
846-
| _ -> fatal t.pos "Too many subproofs (%d subproofs for %d subgoals)."
847-
nb_subproofs nb_newgoals
864+
mismatch (Printf.sprintf
865+
"Missing subproofs (%d subproofs for %d subgoals)."
866+
nb_subproofs nb_newgoals)
867+
| _ ->
868+
mismatch (Printf.sprintf
869+
"Too many subproofs (%d subproofs for %d subgoals)."
870+
nb_subproofs nb_newgoals)

src/lsp/dune

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
(name lsp)
33
(public_name lambdapi.lsp)
44
(modules lsp_base lsp_io lp_doc lp_lsp)
5-
(libraries yojson lambdapi.pure))
5+
(libraries yojson lambdapi.handle lambdapi.pure))

src/lsp/lp_lsp.ml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,9 @@ let main std log_file =
460460
let lp_fmt = F.formatter_of_buffer Lp_doc.lp_logger in
461461
Console.out_fmt := lp_fmt;
462462
Error.err_fmt := lp_fmt;
463+
(* Editors display the proof state themselves, so do not attach it to
464+
tactic failures. *)
465+
Handle.Proof.state_on_error := false;
463466
(* Console.verbose := 4; *)
464467

465468
let rec loop () =

tests/KO/proof_state_fail.lp

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// A tactic failing mid-proof: the error must carry the proof state
2+
// (hypotheses, separator, goals) as its description (see proof_state.ml).
3+
constant symbol Prop : TYPE;
4+
injective symbol π : Prop → TYPE;
5+
constant symbol A : Prop;
6+
constant symbol B : Prop;
7+
symbol imp : Prop → Prop → Prop;
8+
rule π (imp $a $b) ↪ π $a → π $b;
9+
opaque symbol demo : π (imp A (imp B A)) ≔
10+
begin
11+
assume a b;
12+
fail
13+
end;

tests/KO/proof_state_mismatch.lp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// A subproof-count mismatch is only detected after the tactic ran: the error
2+
// must carry the proof state before AND after it (see proof_state.ml).
3+
constant symbol Prop : TYPE;
4+
builtin "Prop" ≔ Prop;
5+
injective symbol π : Prop → TYPE;
6+
builtin "P" ≔ π;
7+
inductive N : TYPE ≔ | z : N | succ : N → N;
8+
symbol Q : N → Prop;
9+
opaque symbol demo : Π n, π (Q n) ≔
10+
begin
11+
induction
12+
end;

tests/KO/proof_state_unfinished.lp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Reaching `end` with goals remaining: the error must carry the remaining
2+
// proof state as its description (see proof_state.ml).
3+
constant symbol Prop : TYPE;
4+
injective symbol π : Prop → TYPE;
5+
constant symbol A : Prop;
6+
symbol imp : Prop → Prop → Prop;
7+
rule π (imp $a $b) ↪ π $a → π $b;
8+
opaque symbol demo : π (imp A A) ≔
9+
begin
10+
assume a
11+
end;

tests/dune

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
(tests
2-
(names ok_ko rewriting purity kernel)
2+
(names ok_ko rewriting purity kernel proof_state)
33
(deps
44
(glob_files OK/*.lp)
55
(glob_files OK/*.dk)

0 commit comments

Comments
 (0)