Skip to content

Commit

Permalink
Merge pull request #709 from ejgallego/support_meta_commands
Browse files Browse the repository at this point in the history
[fleche] Support Coq Meta Commands Reset and Back
  • Loading branch information
ejgallego authored May 15, 2024
2 parents 6fef238 + 2773b45 commit 6d87e3f
Show file tree
Hide file tree
Showing 10 changed files with 205 additions and 6 deletions.
3 changes: 3 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,9 @@
engine. (@ejgallego, @gbdrt, #703, thanks to Alex Sanchez-Stern)
- Always dispose UI elements. This should improve some strange
behaviors on extension restart (@ejgallego, #708)
- Support Coq meta-commands (Reset, Reset Initial, Back) They are
actually pretty useful to hint the incremental engine to ignore
changes in some part of the document (@ejgallego, #709)

# coq-lsp 0.1.8.1: Spring fix
-----------------------------
Expand Down
45 changes: 45 additions & 0 deletions coq/ast.ml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,51 @@ module Require = struct
| _ -> None
end

module Meta = struct
type ast = t

open Ppx_hash_lib.Std.Hash.Builtin
open Ppx_compare_lib.Builtin
module Loc = Serlib.Ser_loc
module Names = Serlib.Ser_names
module Attributes = Serlib.Ser_attributes
module Vernacexpr = Serlib.Ser_vernacexpr

module Command = struct
type t =
| Back of int
| ResetName of Names.lident
| ResetInitial
[@@deriving hash, compare]
end

type t =
{ command : Command.t
; loc : Loc.t option
; attrs : Attributes.vernac_flag list
; control : Vernacexpr.control_flag list
}
[@@deriving hash, compare]

(* EJGA: Coq classification puts these commands as pure? Seems like yet
another bug... *)
let extract : ast -> t option =
CAst.with_loc_val (fun ?loc -> function
| { Vernacexpr.expr = Vernacexpr.(VernacSynPure (VernacResetName id))
; control
; attrs
} ->
let command = Command.ResetName id in
Some { command; loc; attrs; control }
| { expr = VernacSynPure VernacResetInitial; control; attrs } ->
let command = Command.ResetInitial in
Some { command; loc; attrs; control }
| { expr = VernacSynPure (VernacBack num); control; attrs } ->
let command = Command.Back num in
Some { command; loc; attrs; control }
| _ -> None)
end

module Kinds = struct
(* LSP kinds *)
let _file = 1
Expand Down
22 changes: 22 additions & 0 deletions coq/ast.mli
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,28 @@ module Require : sig
val extract : ast -> t option
end

module Meta : sig
type ast = t

module Command : sig
type t =
| Back of int
| ResetName of Names.lident
| ResetInitial
end

type t =
{ command : Command.t
; loc : Loc.t option
; attrs : Attributes.vernac_flag list
; control : Vernacexpr.control_flag list
}
[@@deriving hash, compare]

(** Determine if we are under a meta-command *)
val extract : ast -> t option
end

(** [make_info ~st ast] Compute info about a possible definition in [ast], we
need [~st] to compute the type. *)
val make_info :
Expand Down
1 change: 1 addition & 0 deletions coq/protect.ml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ module E = struct
{ r; feedback = feedback @ fb2 }

let ok v = { r = Completed (Ok v); feedback = [] }
let error err = { r = R.error err; feedback = [] }

module O = struct
let ( let+ ) x f = map ~f x
Expand Down
1 change: 1 addition & 0 deletions coq/protect.mli
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ module E : sig
val map_loc : f:('l -> 'm) -> ('a, 'l) t -> ('a, 'm) t
val bind : f:('a -> ('b, 'l) t) -> ('a, 'l) t -> ('b, 'l) t
val ok : 'a -> ('a, 'l) t
val error : Pp.t -> ('a, 'l) t

module O : sig
val ( let+ ) : ('a, 'l) t -> ('a -> 'b) -> ('b, 'l) t
Expand Down
30 changes: 30 additions & 0 deletions etc/doc/USER_MANUAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,33 @@ from `memprof-limits`. The situation is better in Coq 8.20, but users
on Coq <= 8.19 do need to install a version of Coq with the backported
fixes. See the information about Coq upstream bugs in the README for
more information about available branches.

## Advanced incremental tricks

You can use the `Reset $id` and `Back $steps` commands to isolate
parts of the document from each others in terms of rechecking.

For example, the command `Reset $id` will make the parts of the
document after it use the state before the node `id` was found. Thus,
any change between `$id` and the `Reset $id` command will not trigger
a recheck of the rest of the document.

```coq
(* Coq code 1 *)
Lemma foo : T.
Proof. ... Qed.
(* Coq code 2 *)
Reset foo.
(* Coq code 3 *)
```

In the above code, any change in the "`Coq code 2`" section will not
trigger a recheck of the "`Coq code 3`" Section, by virtue of the
incremental engine.

Using `Reset Initial`, you can effectively partition the document on
`N` parts! This is pretty cool for some use cases!
38 changes: 38 additions & 0 deletions examples/MetaCommands.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
Lemma foo : 3 = 3.
Proof. now reflexivity. Qed.

About foo.

Reset Initial.

About foo.

Lemma foo : 2 = 2.
Proof. now reflexivity. Qed.

Lemma bar : 4 = 4.
Proof. now reflexivity. Qed.

About bar.
About foo.

Reset foo.

About foo.
About bar.

Lemma muu : 4 = 4.
Proof.
now reflexivity.
Back 2.
now reflexivity.
Qed.

Reset Initial.

About muu.
About foo.
About bar.



63 changes: 57 additions & 6 deletions fleche/doc.ml
Original file line number Diff line number Diff line change
Expand Up @@ -672,13 +672,64 @@ end = struct
| Completed (Error _) -> st
end

let interp_and_info ~st ~files ast =
let interp_and_info ~token ~st ~files ast =
match Coq.Ast.Require.extract ast with
| None -> Memo.Interp.evalS (st, ast)
| Some ast -> Memo.Require.evalS (st, files, ast)
| None -> Memo.Interp.evalS ~token (st, ast)
| Some ast -> Memo.Require.evalS ~token (st, files, ast)

let interp_and_info ~token ~parsing_time ~st ~files ast =
let res, stats = interp_and_info ~token ~st ~files ast in
(* Support for meta-commands, a bit messy, but cool in itself *)
let search_node ~command ~doc =
let nstats (node : Node.t option) =
Option.cata
(fun (node : Node.t) -> Option.default Memo.Stats.zero node.info.stats)
Memo.Stats.zero node
in
match command with
| Coq.Ast.Meta.Command.Back num -> (
match Base.List.nth doc.nodes num with
| None ->
let ll = List.length doc.nodes in
let message =
Pp.(
str "not enough nodes: [" ++ int num ++ str " > " ++ int ll
++ str "] available document nodes")
in
(Coq.Protect.E.error message, nstats None)
| Some node -> (Coq.Protect.E.ok node.state, nstats (Some node)))
| ResetName id -> (
let toc = doc.toc in
let id = Names.Id.to_string id.v in
match CString.Map.find_opt id toc with
| None ->
( Coq.Protect.E.error Pp.(str "identifier " ++ str id ++ str " not found")
, Memo.Stats.zero )
| Some range ->
(* this is painful *)
let rec aux st node (nodes : Node.t list) =
match nodes with
| [] -> (st, nstats node)
| node :: nodes ->
if node.range.end_.offset < range.start.offset then
(node.state, nstats (Some node))
else aux node.state (Some node) nodes
in
(* We could error here too *)
let res, stats = aux doc.root None doc.nodes in
(Coq.Protect.E.ok res, stats))
| ResetInitial -> (Coq.Protect.E.ok doc.root, nstats None)

let interp_and_info ~token ~st ~files ~doc ast =
match Coq.Ast.Meta.extract ast with
| None -> interp_and_info ~token ~st ~files ast
| Some { command; loc = _; attrs = _; control = _ } ->
(* That's an interesting point, for now we don't measure time Flèche is
spending on error recovery and meta stuff, we should record that time
actually at some point too. In this case, maybe we could recover the
cache hit from the original node? *)
search_node ~command ~doc

let interp_and_info ~token ~parsing_time ~st ~files ~doc ast =
let res, stats = interp_and_info ~token ~st ~files ~doc ast in
let global_stats = Stats.Global.dump () in
let info = Node.Info.make ~parsing_time ~stats ~global_stats () in
(res, info)
Expand Down Expand Up @@ -816,7 +867,7 @@ let document_action ~token ~st ~parsing_diags ~parsing_feedback ~parsing_time
| Process ast -> (
let lines, files = (doc.contents.lines, doc.env.files) in
let process_res, info =
interp_and_info ~token ~parsing_time ~st ~files ast
interp_and_info ~token ~parsing_time ~st ~files ~doc ast
in
let f = Coq.Utils.to_range ~lines in
let { Coq.Protect.E.r; feedback } = Coq.Protect.E.map_loc ~f process_res in
Expand Down
6 changes: 6 additions & 0 deletions fleche/memo.ml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ module Stats = struct
precise option *)
(* let memory = Obj.magic res |> Obj.reachable_words in *)
{ stats; time_hash; cache_hit }

let zero =
{ stats = { Stats.time = 0.0; memory = 0.0 }
; time_hash = 0.0
; cache_hit = false
}
end

module GlobalCacheStats = struct
Expand Down
2 changes: 2 additions & 0 deletions fleche/memo.mli
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ module Stats : sig
(** Time in hashing consumed in the original execution *)
; cache_hit : bool (** Whether we had a cache hit *)
}

val zero : t
end

(** Flèche memo / cache tables, with some advanced features *)
Expand Down

0 comments on commit 6d87e3f

Please sign in to comment.