ProofEngine.ml 19.12 KiB
(**************************************************************************)
(* *)
(* This file is part of WP plug-in of Frama-C. *)
(* *)
(* Copyright (C) 2007-2024 *)
(* CEA (Commissariat a l'energie atomique et aux energies *)
(* alternatives) *)
(* *)
(* you can redistribute it and/or modify it under the terms of the GNU *)
(* Lesser General Public License as published by the Free Software *)
(* Foundation, version 2.1. *)
(* *)
(* It is distributed in the hope that it will be useful, *)
(* but WITHOUT ANY WARRANTY; without even the implied warranty of *)
(* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *)
(* GNU Lesser General Public License for more details. *)
(* *)
(* See the GNU Lesser General Public License version 2.1 *)
(* for more details (enclosed in the file licenses/LGPLv2.1). *)
(* *)
(**************************************************************************)
(* -------------------------------------------------------------------------- *)
(* --- Interactive Proof Engine --- *)
(* -------------------------------------------------------------------------- *)
type node = {
tree : Wpo.t ; (* root, to check consistency *)
goal : Wpo.t ; (* only GoalAnnot of a sequent *)
child : string option ; (* child name from parent node *)
parent : node option ;
mutable script : script ;
mutable stats : Stats.stats option ; (* memoized *)
mutable strategy : string option ; (* hint *)
mutable search_index : int ;
mutable search_space : Strategy.t array ; (* sorted by priority *)
}
and script =
| Opened
| Script of ProofScript.jscript (* to replay *)
| Tactic of ProofScript.jtactic * (string * node) list (* played *)
module Node =
struct
type t = node
let hash t = Wpo.S.hash t.goal
let equal a b = Wpo.S.equal a.goal b.goal
let compare a b = Wpo.S.compare a.goal b.goal
let pretty fmt a = Wpo.S.pretty fmt a.goal
end
type tree = {
main : Wpo.t ; (* Main goal to be proved. *)
mutable pool : Lang.F.pool option ; (* Global pool variable *)
mutable saved : bool ; (* Saved on Disk. *)
mutable dirty : bool ; (* Tactical result is outdated *)
mutable gid : int ; (* WPO goal numbering *)
mutable head : node option ; (* the current node *)
mutable root : node option ; (* the root node *)
}
module PROOFS = WpContext.StaticGenerator(Wpo.S)
(struct
type key = Wpo.S.t
type data = tree
let name = "Wp.ProofEngine.Proofs"
let compile main =
ignore (Wpo.resolve main) ;
{
main ; gid = 0 ;
pool = None ;
head = None ;
root = None ;
dirty = true ;
saved = false ;
}
end)
module NODES = WpContext.Static
(struct
type key = Wpo.t
type data = node
let name = "Wp.ProofEngine.Nodes"
let compare = Wpo.S.compare
let pretty = Wpo.S.pretty
end)
(* -------------------------------------------------------------------------- *)
(* --- Signaling --- *)
(* -------------------------------------------------------------------------- *)
let goal_hooks = ref []
let clear_hooks = ref []
let remove_hooks = ref []
let update_hooks = ref []
let add_goal_hook fn = goal_hooks := !goal_hooks @ [fn]
let add_clear_hook fn = clear_hooks := !clear_hooks @ [fn]
let add_remove_hook fn = remove_hooks := !remove_hooks @ [fn]
let add_update_hook fn = update_hooks := !update_hooks @ [fn]
let signal_goal g = List.iter (fun fn -> fn g) !goal_hooks
let signal_node n = List.iter (fun fn -> fn n) !update_hooks
let dirty_root g =
try
let tree = PROOFS.find g in
if not tree.dirty then
begin
tree.dirty <- true ;
signal_goal g ;
end
with Not_found -> ()
let rec dirty_node n =
match n.stats with
| None -> ()
| Some _ ->
n.stats <- None ;
signal_node n ;
match n.parent with
| Some p -> dirty_node p
| None -> dirty_root n.tree
let self_updating = ref false
let dirty_goal g =
if not !self_updating then
match NODES.get g with
| Some n -> dirty_node n
| None -> dirty_root g
let get wpo =
try
let proof = PROOFS.find wpo in
match proof.root with
| None | Some { script = Opened | Script _ } -> raise Not_found
| Some { script = Tactic _ } -> if proof.saved then `Saved else `Proof
with Not_found ->
if ProofSession.exists wpo then `Script else `None
let iter_children f ns = List.iter (fun m -> f (snd m)) ns
let map_children f ns = List.map (fun m -> fst m,f m) ns
let pool tree =
match tree.pool with
| Some pool -> pool
| None ->
let _,sequent = Wpo.compute tree.main in
let pool = Lang.new_pool ~vars:(Conditions.vars_seq sequent) () in
tree.pool <- Some pool ; pool
(* -------------------------------------------------------------------------- *)
(* --- Proofs --- *)
(* -------------------------------------------------------------------------- *)
let proof ~main =
assert (not (Wpo.is_tactic main)) ;
PROOFS.get main
let saved t = t.saved
let set_saved t s = t.saved <- s ; signal_goal t.main
(* -------------------------------------------------------------------------- *)
(* --- Removal --- *)
(* -------------------------------------------------------------------------- *)
let rec revert_tactic t n =
n.strategy <- None ;
match n.script with
| Opened | Script _ -> ()
| Tactic(_,children) ->
t.saved <- false ;
n.script <- Opened ;
iter_children (remove_node t) children
and remove_node t n =
NODES.remove n.goal ;
if Wpo.is_tactic n.goal then
Wpo.remove n.goal
else
Wpo.clear_results n.goal ;
List.iter (fun f -> f n) !remove_hooks ;
Option.iter (fun n0 -> if n0 == n then t.head <- None) t.head ;
revert_tactic t n
let clear_tree t =
begin
Option.iter (remove_node t) t.root ;
t.gid <- 0 ;
t.head <- None ;
t.root <- None ;
t.saved <- false ;
List.iter (fun fn -> fn t.main) !clear_hooks ;
signal_goal t.main ;
end
let clear_node_tactic t n =
revert_tactic t n ; dirty_node n ;
if t.head = None then t.head <- Some n
let clear_parent_tactic t n =
match n.parent with
| None -> clear_tree t
| Some p as h -> revert_tactic t p ; dirty_node p ; t.head <- h
let clear_node t n =
Wpo.clear_results n.goal ;
clear_node_tactic t n
let clear_goal w =
try clear_tree (PROOFS.find w)
with Not_found -> Wpo.clear_results w
(* -------------------------------------------------------------------------- *)
(* --- Removal From Wpo --- *)
(* -------------------------------------------------------------------------- *)
let removed_from_wpo g =
if not @@ Wpo.is_tactic g then
begin
try
clear_tree (PROOFS.find g) ;
PROOFS.remove g ;
with Not_found ->
signal_goal g
end
let cleared_from_wpo () =
begin
NODES.clear () ;
PROOFS.clear () ;
end
let () = Wpo.add_removed_hook removed_from_wpo
let () = Wpo.add_cleared_hook cleared_from_wpo
let () = Wpo.add_modified_hook dirty_goal
(* -------------------------------------------------------------------------- *)
(* --- Walking --- *)
(* -------------------------------------------------------------------------- *)
let rec walk f node =
if not (Wpo.is_locally_valid node.goal) then
match node.script with
| Tactic (_,children) -> iter_children (walk f) children
| Opened | Script _ -> f node
let iteri f tree =
match tree.root with
| None -> ()
| Some r ->
let k = ref 0 in
walk (fun node -> f !k node ; incr k) r
let rec depth node =
match node.parent with
| None -> 1
| Some p -> succ @@ depth p
(* -------------------------------------------------------------------------- *)
(* --- Consolidating --- *)
(* -------------------------------------------------------------------------- *)
let pending n =
let k = ref 0 in
walk (fun _ -> incr k) n ; !k
let locally_proved n = Wpo.is_locally_valid n.goal
let fully_proved n =
let exception HasPending in
try walk (fun _ -> raise HasPending) n ; true
with HasPending -> false
let is_prover (p,_) = VCS.is_prover p
let prover_stats ~smoke goal =
Stats.results ~smoke @@
List.filter is_prover @@
Wpo.get_results goal
let rec consolidate ~smoke n =
match n.stats with
| Some s -> s
| None -> let s = compute ~smoke n in n.stats <- Some s ; s
and compute ~smoke n =
if Wpo.is_locally_valid n.goal then
prover_stats ~smoke n.goal
else
match n.script with
| Opened | Script _ -> prover_stats ~smoke n.goal
| Tactic(_,children) ->
Stats.tactical ~qed:(Wpo.qed_time n.goal) @@
List.map (fun (_,n) -> consolidate ~smoke n) children
let tactical tree =
match tree.root with
| None -> VCS.no_result
| Some root ->
let smoke = Wpo.is_smoke_test tree.main in
Stats.script @@ consolidate ~smoke root
let validate tree =
if tree.dirty then
let result =
if Wpo.is_locally_valid tree.main then VCS.no_result else tactical tree
in
try
self_updating := true ;
Wpo.set_result tree.main VCS.Tactical result ;
tree.dirty <- false ;
self_updating := false ;
with exn ->
self_updating := false ;
raise exn
let consolidated wpo =
let smoke = Wpo.is_smoke_test wpo in
try
match (PROOFS.find wpo).root with
| Some node -> consolidate ~smoke node
| _ -> raise Not_found
with Not_found -> prover_stats ~smoke wpo
let consolidate wpo =
try validate (PROOFS.find wpo) with Not_found -> ()
let results wpo = consolidate wpo ; Wpo.get_results wpo
let stats node =
match node.stats with Some s -> s | None ->
let smoke = Wpo.is_smoke_test node.tree in
let s = compute ~smoke node in
node.stats <- Some s ; s
(* -------------------------------------------------------------------------- *)
(* --- Accessors --- *)
(* -------------------------------------------------------------------------- *)
let main t = t.main
let head t = t.head
let head_goal t = match t.head with
| None -> t.main
| Some n -> n.goal
let tree n = proof ~main:n.tree
let goal n = n.goal
let tree_context t = Wpo.get_context t.main
let node_context n = Wpo.get_context n.goal
let parent n = n.parent
let title n = n.goal.Wpo.po_name
let tactical n =
match n.script with
| Tactic(tactic,_) -> Some tactic
| Opened | Script _ -> None
let get_strategies n = n.search_index , n.search_space
let set_strategies n ?(index=0) hs =
n.search_index <- index ; n.search_space <- hs ; signal_node n
let children n =
match n.script with
| Tactic(_,children) -> children
| Opened | Script _ -> []
let subgoals n = List.map snd @@ children n
let rec path n = match n.parent with
| None -> []
| Some p -> p::path p
let child_label n = n.child
let tactic_label n =
match n.script with Tactic({ header }, _) -> Some header | _ -> None
let tactic n =
match n.script with
| Tactic({ tactic }, _) ->
begin try Some (Tactical.lookup ~id:tactic) with Not_found -> None end
| _ -> None
(* -------------------------------------------------------------------------- *)
(* --- State & Status --- *)
(* -------------------------------------------------------------------------- *)
type status = [
| `Unproved (* proof obligation not proved *)
| `Proved (* proof obligation is proved *)
| `Pending of int (* proof is pending *)
| `Passed (* smoke test is passed (PO is not proved) *)
| `Invalid (* smoke test has failed (PO is proved) *)
| `StillResist of int (* proof is pending *)
]
let status t : status =
match t.root with
| None ->
if Wpo.is_locally_valid t.main
then if Wpo.is_smoke_test t.main then `Invalid else `Proved
else if Wpo.is_smoke_test t.main then `Passed else `Unproved
| Some root ->
match root.script with
| Opened | Script _ ->
if Wpo.is_smoke_test t.main then `Passed else `Unproved
| Tactic _ ->
let n = pending root in
if n = 0 then
if Wpo.is_smoke_test t.main then `Invalid else `Proved
else
if Wpo.is_smoke_test t.main then `StillResist n else `Pending n
(* -------------------------------------------------------------------------- *)
(* --- Navigation --- *)
(* -------------------------------------------------------------------------- *)
type current = [ `Main | `Internal of node | `Leaf of int * node ]
let current t : current =
match t.head with
| Some h ->
let p = ref (`Internal h) in
iteri (fun i n -> if n == h then p := `Leaf(i,n)) t ; !p
| None -> `Main
type position = [ `Main | `Node of node | `Leaf of int ]
let goto t = function
| `Main ->
t.head <- t.root ; signal_goal t.main
| `Node n ->
if n.tree == t.main then t.head <- Some n ; signal_goal t.main
| `Leaf k ->
t.head <- t.root ;
iteri (fun i n -> if i = k then t.head <- Some n) t ;
signal_goal t.main
let fetch t node =
try
t.head <- t.root ;
walk (fun n -> t.head <- Some n ; raise Exit) node ;
false
with Exit -> true
let rec forward t =
match t.head with
| None -> t.head <- t.root ; signal_goal t.main
| Some hd ->
if not (fetch t hd)
then ( t.head <- hd.parent ; forward t )
else signal_goal t.main
let cancel_parent_tactic t =
match t.head with
| None -> ()
| Some n -> clear_parent_tactic t n
let cancel_current_node t =
match t.head with
| None -> ()
| Some node -> clear_node t node
(* -------------------------------------------------------------------------- *)
(* --- Sub-Goal --- *)
(* -------------------------------------------------------------------------- *)
let mk_formula ~main axioms sequent =
Wpo.VC_Annot.{ main with goal = Wpo.GOAL.make sequent ; axioms }
let mk_goal t ~title ~part ~axioms sequent =
let id = t.gid in t.gid <- succ id ;
let gid = Printf.sprintf "%s-%d" t.main.Wpo.po_gid id in
let sid = Printf.sprintf "%s-%d" t.main.Wpo.po_sid id in
Wpo.({
po_gid = gid ;
po_sid = sid ;
po_name = Printf.sprintf "%s (%s)" title part ;
po_idx = t.main.po_idx ;
po_pid = WpPropId.tactical ~gid ;
po_model = t.main.po_model ;
po_formula = mk_formula ~main:t.main.po_formula axioms sequent ;
})
let mk_node ~main ?parent ?child goal =
let node = {
tree=main ; parent ; child ; goal ;
script = Opened ;
stats = None ;
search_index = 0 ;
search_space = [| |] ;
strategy = None ;
} in NODES.define goal node ; node
let mk_root ~tree =
let main = tree.main in
let node = mk_node ~main main in
let root = Some node in
tree.root <- root ;
tree.head <- root ;
node
let root tree =
match tree.root with
| Some node -> node
| None -> mk_root ~tree
(* -------------------------------------------------------------------------- *)
(* --- Forking --- *)
(* -------------------------------------------------------------------------- *)
module Fork =
struct
type t = {
tree : tree ;
anchor : node ;
tactic : ProofScript.jtactic ;
goals : (string * Wpo.t) list ;
}
let create tree ~anchor tactic process =
let axioms , sequent = Wpo.compute anchor.goal in
let vars = Conditions.vars_seq sequent in
let dseqs = Lang.local ~vars process sequent in
let title = tactic.ProofScript.header in
let goals = List.map
(fun (part,s) -> part , mk_goal tree ~title ~part ~axioms s) dseqs
in { tree ; tactic ; anchor ; goals }
let iter f w = iter_children f w.goals
let header frk = frk.tactic.ProofScript.header
end
let pretty fmt frk = Format.pp_print_string fmt (Fork.header frk)
type fork = Fork.t
let fork = Fork.create
let iter = Fork.iter
let anchor tree ?node () =
match node with
| Some n -> n
| None ->
match tree.head with
| Some n -> n
| None ->
match tree.root with
| Some n -> n
| None -> mk_root ~tree
let commit fork =
List.iter (fun (_,wp) -> ignore (Wpo.resolve wp)) fork.Fork.goals ;
let tree = fork.Fork.tree in
let parent = fork.Fork.anchor in
let subnode (child,goal) = mk_node ~main:tree.main ~parent ~child goal in
let children = map_children subnode fork.Fork.goals in
tree.saved <- false ;
parent.script <- Tactic( fork.Fork.tactic , children ) ;
dirty_node parent ;
parent , children
(* -------------------------------------------------------------------------- *)
(* --- Scripting --- *)
(* -------------------------------------------------------------------------- *)
let script_provers wpo =
List.map (fun (p,r) -> ProofScript.a_prover p r) @@
Wpo.get_prover_results wpo
let rec script_node (node : node) =
script_provers node.goal @
begin
match node.script with
| Script s -> List.filter ProofScript.is_tactic s
| Tactic( tactic , children ) ->
[ ProofScript.a_tactic tactic (List.map subscript_node children) ]
| Opened -> []
end
and subscript_node (key,node) = key , script_node node
let script tree =
match tree.root with
| None -> script_provers tree.main
| Some node -> script_node node
let bind node script =
match node.script with
| Tactic _ ->
(*TODO: saveback the thrown script *)
()
| Opened | Script _ ->
(*TODO: saveback the previous script *)
node.script <- Script script ;
signal_node node ;
signal_goal node.tree
let bound node =
match node.script with
| Tactic _ | Opened -> []
| Script s -> s
let is_script_result ~margin (prv,res) =
VCS.is_extern prv && VCS.is_valid res &&
res.prover_time > margin
let has_result tree =
let margin = float_of_string @@ Wp_parameters.TimeMargin.get () in
let results = Wpo.get_results tree.main in
List.exists (is_script_result ~margin) results
let has_tactics tree =
match tree.root with
| None -> false
| Some node ->
match node.script with
| Opened -> false
| Tactic _ -> true
| Script s -> List.exists ProofScript.is_tactic s
let has_script tree = has_tactics tree || has_result tree
let has_proof wpo =
try has_script @@ PROOFS.find wpo
with Not_found -> false
let get_hint node = node.strategy
let set_hint node strategy = node.strategy <- Some strategy ; signal_node node
(* -------------------------------------------------------------------------- *)