type witness = {
case : Syntax.t;
model : Syntax.model;
bound : int;
fences : Syntax.location list;
actors : int array;
}
type result = { witness : witness option; query : Solver.query }
type phase = Searching of int * Syntax.location list
| Counterexample of witness | Excluded of int | Refining of int
| Verified of Syntax.location list * int * int
type repair = {
case : Syntax.t;
fences : Syntax.location list;
clauses : int list list;
rounds : int;
bound : int;
states : int;
}
let decision = function
| Solver.Unknown reason -> failwith ("solver returned unknown: " ^ reason)
| answer -> answer
let verify case model fences bound values =
let rec split count prefix = function
| rest when count = 0 -> List.rev prefix, rest
| value :: rest -> split (count - 1) (value :: prefix) rest
| [] -> failwith "incomplete scheduler model"
in
let actors, observations = split bound [] values in
let actors = Array.of_list (List.map (fun value -> Int64.to_int (Solver.bits value)) actors) in
let states, _ = Machine.replay case model fences actors in
let expected = List.concat (Array.to_list (Array.map Machine.observations states)) in
if List.map Solver.bits observations <> expected then
failwith "solver model disagrees with independent state-by-state replay";
actors, states
let witness_sexp (witness : witness) =
let open Sexp in
List [Atom "witness"; List [Atom "version"; i 1];
List [Atom "case"; Syntax.encode witness.case];
List [Atom "model"; a (Syntax.model_name witness.model)];
List [Atom "bound"; i witness.bound];
List (Atom "fences" :: List.map (fun (t, pc) -> List [i t; i pc]) witness.fences);
List (Atom "actors" :: Array.to_list (Array.map i witness.actors))]
let fields tag = function
| Sexp.List (Sexp.Atom name :: fields) when name = tag -> fields
| _ -> invalid_arg ("expected " ^ tag)
let single key fields = match Sexp.field key fields with
| [value] -> value | _ -> invalid_arg ("invalid field " ^ key)
let replay (witness : witness) =
Syntax.validate witness.case;
Syntax.validate {witness.case with candidates=witness.fences};
if witness.bound < 1 || witness.bound > 64 || Array.length witness.actors <> witness.bound then
invalid_arg "witness length differs from its bound";
let states, events = Machine.replay witness.case witness.model witness.fences witness.actors in
if not (Machine.bad witness.case states.(Array.length states - 1)) then
invalid_arg "witness does not reach the requested outcome";
states, events
let load_witness path =
let fields = fields "witness" (Sexp.one (Store.read path)) in
if Sexp.integer (single "version" fields) <> 1 then invalid_arg "unsupported witness version";
let witness = {
case = Syntax.decode (single "case" fields);
model = Syntax.model (Sexp.atom (single "model" fields));
bound = Sexp.integer (single "bound" fields);
fences = List.map Syntax.location (Sexp.field "fences" fields);
actors = Array.of_list (List.map Sexp.integer (Sexp.field "actors" fields));
} in
ignore (replay witness);
witness
let save folder result =
Store.mkdir folder;
Store.write (Filename.concat folder "query.smt2") (Solver.standalone result.query);
let path = Filename.concat folder "witness.sexp" in
match result.witness with
| Some witness -> Store.write path (Sexp.print (witness_sexp witness) ^ "\n")
| None -> if Sys.file_exists path then Sys.remove path
let check ?(timeout=300000) ?(tick=fun () -> ()) ?bound ?(fences=[]) ?folder ?fixed case model =
let bound = Option.value bound ~default:case.Syntax.bound in
let query = Solver.compile ~timeout ?fixed case model fences bound in
let witness = match decision (Solver.solve ~timeout ~tick query) with
| Solver.Unsat -> None
| Solver.Sat values ->
let actors, states = verify case model fences bound values in
if not (Machine.bad case states.(bound)) then failwith "invalid final model state";
Some {case; model; bound; fences; actors}
| Solver.Unknown _ -> assert false in
let result = {witness; query} in
Option.iter (fun folder -> save folder result) folder;
result
let blockers (witness : witness) =
let states, _ = Machine.replay witness.case Syntax.Tso [] witness.actors in
let result = ref [] in
Array.iteri (fun i actor ->
if actor < 2 && states.(i).buffers.(actor) <> [] then
List.iteri (fun candidate location ->
if location = (actor, states.(i).pc.(actor)) then result := candidate :: !result)
witness.case.candidates) witness.actors;
List.sort_uniq compare !result
let repair_sexp (repair : repair) =
let open Sexp in
List [Atom "repair"; List [Atom "case"; Syntax.encode repair.case];
List [Atom "rounds"; i repair.rounds];
List [Atom "bound"; i repair.bound]; List [Atom "states"; i repair.states];
List (Atom "fences" :: List.map (fun (t, pc) -> List [i t; i pc]) repair.fences);
List (Atom "clauses" :: List.map (fun clause -> List (List.map i clause)) repair.clauses)]
let load_repair path =
let fields = fields "repair" (Sexp.one (Store.read path)) in
let repair = {
case = Syntax.decode (single "case" fields);
rounds = Sexp.integer (single "rounds" fields);
bound = Sexp.integer (single "bound" fields);
states = Sexp.integer (single "states" fields);
fences = List.map Syntax.location (Sexp.field "fences" fields);
clauses = List.map (fun clause -> List.map Sexp.integer (Sexp.list clause)) (Sexp.field "clauses" fields);
} in
if repair.bound < repair.case.bound || repair.bound > 64 || repair.states < 1 ||
repair.rounds <> List.length repair.clauses + 1 ||
List.sort_uniq compare repair.fences <> List.sort compare repair.fences ||
not (List.for_all (fun location -> List.mem location repair.case.candidates) repair.fences)
then invalid_arg "invalid repair certificate";
repair
let repair ?(timeout=300000) ?(tick=fun () -> ()) ?(notify=fun _ -> ()) case folder =
Store.mkdir folder;
List.iter (fun name ->
let path = Filename.concat folder name in
if Sys.file_exists path then Sys.remove path) ["repair.sexp"; "witness.sexp"];
let count = List.length case.Syntax.candidates in
let clauses = ref [] in
let rounds = ref 0 in
let search_bound = ref case.bound in
let rec search budget =
if budget > count then failwith "fence search exhausted";
let candidate = Solver.candidate ~count ~budget !clauses in
Store.write (Filename.concat folder "synthesis.smt2") (Solver.standalone candidate);
match decision (Solver.solve ~timeout ~tick candidate) with
| Solver.Unsat -> notify (Excluded budget); search (budget + 1)
| Solver.Sat values ->
let selected = List.filter_map Fun.id (List.mapi (fun i value ->
if Solver.truth value then Some (List.nth case.candidates i) else None) values) in
notify (Searching (budget, selected));
let target = Filename.concat folder (Printf.sprintf "round%02d" !rounds) in
let bounded = check ~timeout ~tick ~bound:!search_bound ~fences:selected ~folder:target case Syntax.Tso in
let result, closure = match bounded.witness with
| Some _ -> bounded, None
| None ->
let graph = Machine.explore case Syntax.Tso selected in
match Machine.counterexample graph with
| None -> bounded, Some graph
| Some actors ->
search_bound := max !search_bound (Array.length actors);
notify (Refining !search_bound);
let deeper = check ~timeout ~tick ~bound:(Array.length actors)
~fixed:actors ~fences:selected ~folder:target case Syntax.Tso in
if deeper.witness = None then failwith "graph counterexample has no SMT execution";
deeper, None
in
incr rounds;
begin match result.witness with
| None ->
let lower = Solver.candidate ~count ~budget:(List.length selected - 1) !clauses in
begin match decision (Solver.solve ~timeout ~tick lower) with
| Solver.Unsat -> () | _ -> failwith "minimum fence certificate is satisfiable"
end;
Store.write (Filename.concat folder "minimal.smt2") (Solver.standalone lower);
let graph = match closure with Some graph -> graph | None -> assert false in
let report = {case; fences = selected; clauses = !clauses; rounds = !rounds;
bound= !search_bound; states=Array.length graph.states} in
Store.write (Filename.concat folder "repair.sexp") (Sexp.print (repair_sexp report) ^ "\n");
notify (Verified (selected, report.bound, report.states));
Some report
| Some witness ->
if !rounds = 1 then Store.write (Filename.concat folder "witness.sexp")
(Sexp.print (witness_sexp witness) ^ "\n");
notify (Counterexample witness);
let clause = blockers witness in
if clause = [] then None else begin
if List.mem clause !clauses then failwith "repeated excluded counterexample";
clauses := !clauses @ [clause];
search budget
end
end
| Solver.Unknown _ -> assert false
in
search 0
let certify ?(timeout=300000) ?(tick=fun () -> ()) path =
let report = load_repair path in
List.iteri (fun i clause ->
let path = Filename.concat (Filename.dirname path) (Printf.sprintf "round%02d/witness.sexp" i) in
let witness = load_witness path in
if witness.case <> report.case || witness.model <> Syntax.Tso || witness.bound > report.bound || blockers witness <> clause then
invalid_arg "counterexample does not support its fence constraint") report.clauses;
let lower = Solver.candidate ~count:(List.length report.case.candidates)
~budget:(List.length report.fences - 1) report.clauses in
begin match decision (Solver.solve ~timeout ~tick lower) with
| Solver.Unsat -> () | _ -> invalid_arg "minimum fence count is unsupported"
end;
let graph = Machine.explore report.case Syntax.Tso report.fences in
if graph.violations <> [] || Array.length graph.states <> report.states then
invalid_arg "repair state graph does not support the certificate";
report
let audit ?(timeout=300000) ?(tick=fun () -> ()) path folder =
let report = certify ~timeout ~tick path in
let verified = check ~timeout ~tick ~bound:report.bound ~fences:report.fences ~folder report.case Syntax.Tso in
if verified.witness <> None then invalid_arg "repair still admits the forbidden outcome";
report
let validate_schedule ?(timeout=300000) case fences actors =
let bound = Array.length actors in
let query = Solver.compile ~timeout ~fixed:actors ~require_goal:false case Syntax.Tso fences bound in
match decision (Solver.solve ~timeout query) with
| Solver.Sat values -> ignore (verify case Syntax.Tso fences bound values)
| _ -> failwith "repaired schedule has no matching SMT execution"
let outcomes ?(timeout=300000) ?(tick=fun () -> ()) case model =
let case = {case with Syntax.goal = Syntax.Outcome {registers=[]; memory=[]; drained=false}} in
let rec loop found =
if List.length found = 16 then failwith "outcome enumeration exceeded sixteen distinct register pairs";
let query = Solver.compile ~timeout ~blocks:found case model [] case.bound in
match decision (Solver.solve ~timeout ~tick query) with
| Solver.Unsat -> List.sort_uniq compare found
| Solver.Sat values ->
let _, states = verify case model [] case.bound values in
let final = states.(case.bound) in
loop ((final.regs.(0).(0), final.regs.(1).(0)) :: found)
| Solver.Unknown _ -> assert false
in
loop []