(* * Copyright (c) 2013-2020 Thomas Gazagnaire * Copyright (c) 2013-2020 Anil Madhavapeddy * Copyright (c) 2015-2020 Gabriel Radanne * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *) let src = Logs.Src.create "functoria" ~doc:"functoria library" module Log = (val Logs.src_log src : Logs.LOG) type 'a t = | If : { cond : 't Key.value; branches : ('t * 'a t) list; default : 'a t; } -> 'a t | Dev : { dev : 'a device; args : ('a, 'v) tl; deps : abstract list } -> 'v t | App : { f : 'a t; args : ('a, 'v) tl } -> 'v t and abstract = Abstract : _ t -> abstract and ('a, 'b) tl = | Nil : ('a, 'a) tl | Cons : 'a t * ('b, 'c) tl -> ('a -> 'b, 'c) tl and 'a device = ('a, abstract) Device.t (** Constructors *) let abstract t = Abstract t let rec app_has_no_arguments : type a. a t -> bool = function | App { f = _; args = Cons _ } -> false | App { f; args = Nil } -> app_has_no_arguments f | Dev { args = Nil; deps = []; dev } -> (* special hack for Job.noop *) if not (String.equal (Device.module_name dev) "Unit") then match Device.runtime_args dev with [] -> true | _ -> false else false | Dev _ -> false | If { cond = _; branches; default } -> app_has_no_arguments default || List.exists (fun (_, branch) -> app_has_no_arguments branch) branches (* Devices *) let mk_dev ~args ~deps dev = Dev { dev; args; deps } let of_device dev = mk_dev ~args:Nil ~deps:(Device.extra_deps dev) dev let local_libs = function Dev { dev; _ } -> Device.local_libs dev | _ -> [] let v ?packages ?packages_v ?runtime_args ?keys ?extra_deps ?connect ?dune ?configure ?files module_name module_type = of_device @@ Device.v ?packages ?packages_v ?runtime_args ?keys ?extra_deps ?connect ?dune ?configure ?files module_name module_type let main ?pos ?packages ?packages_v ?runtime_args ?keys ?extra_deps module_name ty = let connect _ = Device.start ?pos in v ?packages ?packages_v ?runtime_args ?keys ?extra_deps ~connect module_name ty (* If *) let mk_switch ~cond ~branches ~default = If { cond; branches; default } let if_ cond then_ else_ = mk_switch ~cond ~branches:[ (true, then_); (false, else_) ] ~default:then_ let match_ cond ~default branches = mk_switch ~cond ~branches ~default (* App *) let rec concat_tl : type a b c. (a, b) tl -> (b, c) tl -> (a, c) tl = fun t1 t2 -> match t1 with Nil -> t2 | Cons (h, t) -> Cons (h, concat_tl t t2) let rec mk_app : type a v. f:a t -> args:(a, v) tl -> v t = fun ~f ~args:args1 -> match f with | Dev { dev; args = args2; deps } -> mk_dev ~args:(concat_tl args2 args1) ~deps dev | App { f; args = args2 } -> mk_app ~f ~args:(concat_tl args2 args1) | _ -> App { f; args = args1 } let ( $ ) f x = mk_app ~f ~args:(Cons (x, Nil)) (** Utilities *) let rec pp : type a. a t Fmt.t = fun ppf -> function | Dev { dev; args; deps = _ } -> Fmt.pf ppf "@[@[Dev %a@]@,@[args=[%a]@]@]" (Device.pp pp_abstract) dev pp_tl args | App { f; args } -> Fmt.pf ppf "App %a(%a)" pp f pp_tl args | If { cond = _; branches; default } -> Fmt.pf ppf "Switch (_,%a,%a)" (Fmt.list pp) (List.map snd branches) pp default and pp_tl : type a b. (a, b) tl Fmt.t = fun ppf -> function | Nil -> () | Cons (h, t) -> Fmt.pf ppf "%a,@ %a" pp h pp_tl t and pp_abstract ppf (Abstract i) = pp ppf i (** Tables and traversals *) (* **** WARNING ****** The [impl] type forms a DAG, implemented as terms with sharing. It is *essential* to preserve sharing while walking the terms. Otherwise - We risk double initialization of devices - The DOT graph is a mess - We might collect information twice As such, the equality, hashing, and tables must be tuned to share [impl]s appropriately and the various traversals must use appropriate tables. *) let rec hash : type a. a t -> int = function | Dev { dev; args; deps } -> Hashtbl.hash (`Dev, Device.hash dev, hash_tl args, List.map hash_abstract deps) | App { f; args } -> Hashtbl.hash (`App, hash f, hash_tl args) | If { cond; branches; default } -> Hashtbl.hash ( `If, cond, List.map (fun (p, t) -> Hashtbl.hash (p, hash t)) branches, hash default ) and hash_abstract (Abstract x) = hash x and hash_tl : type a v. (a, v) tl -> int = fun x -> match x with | Nil -> Hashtbl.hash `Nil | Cons (h, t) -> Hashtbl.hash (`Cons, hash h, hash_tl t) type ex = Ex : 'a -> ex let equal_list p l1 l2 = List.length l1 = List.length l2 && List.for_all2 p l1 l2 let rec equal : type t1 t2. t1 t -> t2 t -> (t1, t2) Typeid.witness = fun x y -> match (x, y) with | Dev c, Dev c' -> ( match ( equal_list equal_abstract c.deps c'.deps, equal_tl c.args c'.args (Device.witness c.dev c'.dev) ) with | true, Eq -> Eq | _ -> NotEq) | App a, App b -> ( match equal_tl a.args b.args (equal a.f b.f) with | Eq -> Eq | NotEq -> NotEq) | If x1, If x2 -> ( match ( equal x1.default x2.default, Obj.repr x1.cond == Obj.repr x2.cond, equal_list (fun (p1, t1) (p2, t2) -> Ex p1 = Ex p2 && equal_abstract (abstract t1) (abstract t2)) x1.branches x2.branches ) with | Eq, true, true -> Eq | _ -> NotEq) | _ -> NotEq and equal_abstract (Abstract x) (Abstract y) = Typeid.to_bool @@ equal x y and equal_tl : type t1 t2 v1 v2. (t1, v1) tl -> (t2, v2) tl -> (t1, t2) Typeid.witness -> (v1, v2) Typeid.witness = fun x y eq -> match (x, y, eq) with | Nil, Nil, Eq -> Eq | Cons (h1, t1), Cons (h2, t2), Eq -> ( match (equal h1 h2, equal_tl t1 t2 Eq) with Eq, Eq -> Eq | _ -> NotEq) | _ -> NotEq module Tbl = Hashtbl.Make (struct type t = abstract let hash = hash_abstract let equal = equal_abstract end) module Hashcons : sig type tbl val create : unit -> tbl val add : tbl -> 'a t -> 'a t -> unit val get : tbl -> 'a t -> 'a t option end = struct type tbl = abstract Tbl.t let create () = Tbl.create 50 let add tbl a b = Tbl.add tbl (abstract a) (abstract b) let get (type a) tbl (oldv : a t) : a t option = if Tbl.mem tbl @@ abstract oldv then let (Abstract newv) = Tbl.find tbl (abstract oldv) in match equal oldv newv with Eq -> Some newv | NotEq -> None else None end let simplify ~full ~context (Abstract t) = let tbl = Hashcons.create () in let rec aux : type a. a t -> a t = fun impl -> match Hashcons.get tbl impl with | Some impl' -> impl' | None -> let acc = match impl with | If { cond; branches; default } -> (* Either - A key is present in the context - We are in full mode, and we use its default value *) if full || Key.mem context cond then let path = Key.eval context cond in let t = try List.assoc path branches with Not_found -> default in aux t else let branches = List.map (fun (p, t) -> (p, aux t)) branches in mk_switch ~cond ~branches ~default | Dev { dev; args; deps } -> let args = aux_tl args in let deps = List.map aux_abstract deps in mk_dev ~args ~deps dev | App { f; args } -> let f = aux f in let args = aux_tl args in mk_app ~f ~args in Hashcons.add tbl impl acc; acc and aux_abstract (Abstract a) = Abstract (aux a) and aux_tl : type a v. (a, v) tl -> (a, v) tl = function | Nil -> Nil | Cons (h, t) -> Cons (aux h, aux_tl t) in Abstract (aux t) let eval ~context (Abstract t) = let new_id = let r = ref 0 in fun () -> incr r; !r in let tbl = Tbl.create 50 in let rec aux : type a. a t -> Device.Graph.t = fun impl -> if Tbl.mem tbl @@ abstract impl then Tbl.find tbl (abstract impl) else let acc = match impl with | Dev { dev; args; deps } -> let args = aux_tl args in let deps = List.map aux_abstract deps in Device.Graph.D { dev; args; deps; id = new_id () } | App { f; args = extra_args } -> let (D { dev; args; deps; id = _ }) = aux f in let extra_args = aux_tl extra_args in D { dev; args = args @ extra_args; deps; id = new_id () } | If { cond; branches; default } -> let path = Key.eval context cond in let t = try List.assoc path branches with Not_found -> default in aux t in Tbl.add tbl (abstract impl) acc; acc and aux_abstract (Abstract a) = aux a and aux_tl : type a v. (a, v) tl -> _ = function | Nil -> [] | Cons (h, t) -> let a = aux h in a :: aux_tl t in aux t type 'b f_dev = { f : 'a. ('a, abstract) Device.t -> 'b } let with_left_most_device ctx t (f : _ f_dev) = let rec aux : type a. a t -> _ = function | Dev d -> f.f d.dev | App a -> aux a.f | If { cond; branches; default } -> let path = Key.eval ctx cond in let t = try List.assoc path branches with Not_found -> default in aux t in aux t type 'b f_dev_full = { f : 'a 'v. args:'b list -> deps:'b list -> 'a device -> 'b; } type 'a f_switch = { if_ : 'r. cond:'r Key.value -> branches:('r * 'a) list -> default:'a -> 'a; } type 'a f_app = f:'a -> args:'a list -> 'a let map (type r) ~(mk_switch : _ f_switch) ~(mk_app : _ f_app) ~(mk_dev : _ f_dev_full) t = let tbl = Tbl.create 50 in let rec aux : type a. a t -> r = fun impl -> if Tbl.mem tbl @@ abstract impl then Tbl.find tbl (abstract impl) else let acc = match impl with | Dev { dev; args; deps } -> let deps = List.fold_right (fun (Abstract x) l -> aux x :: l) deps [] in let args = aux_tl args in mk_dev.f ~args ~deps dev | App { f; args } -> let f = aux f in let args = aux_tl args in mk_app ~f ~args | If { cond; branches; default } -> let branches = List.map (fun (p, t) -> (p, aux t)) branches in let default = aux default in mk_switch.if_ ~cond ~branches ~default in Tbl.add tbl (abstract impl) acc; acc and aux_tl : type a v. (a, v) tl -> r list = function | Nil -> [] | Cons (h, t) -> aux h :: aux_tl t in aux t type label = If : _ Key.value -> label | Dev : _ Device.t -> label | App let collect : type ty. (module Misc.Monoid with type t = ty) -> (label -> ty) -> abstract -> ty = fun (module M) op (Abstract t) -> let r = ref M.empty in let add x = r := M.union (op x) !r in let mk_switch = { if_ = (fun ~cond ~branches:_ ~default:_ -> add @@ If cond) } and mk_app ~f:_ ~args:_ = add App and mk_dev = { f = (fun ~args:_ ~deps:_ dev -> add @@ Dev dev) } in let () = map ~mk_switch ~mk_app ~mk_dev t in !r (* {2 Dot output} *) module Dot = struct type edge_label = | Functor | Argument | Dependency | Branch of { default : bool } let as_dot_graph (Abstract t) = let r = ref 0 in let new_id () = incr r; !r in let vertices = ref [] in let edges = ref [] in let add r x = r := x :: !r in let mk_switch = { if_ = (fun ~cond ~branches ~default -> let id = new_id () in add vertices (id, If cond); List.iter (fun (_, id') -> add edges (id, id', Branch { default = false })) branches; add edges (id, default, Branch { default = true }); id); } and mk_app ~f ~args = let id = new_id () in add vertices (id, App); add edges (id, f, Functor); List.iter (fun id' -> add edges (id, id', Argument)) args; id and mk_dev = { f = (fun ~args ~deps dev -> let id = new_id () in add vertices (id, Dev dev); List.iter (fun id' -> add edges (id, id', Argument)) args; List.iter (fun id' -> add edges (id, id', Dependency)) deps; id); } in let _ = map ~mk_switch ~mk_app ~mk_dev t in (List.rev !vertices, List.rev !edges) let pp_vertice ppf (id, label) = let attrs = match label with | App -> [ ("label", "$"); ("shape", "diamond") ] | If cond -> [ ("label", Fmt.str "If\n%a" Key.pp_deps cond) ] | Dev dev -> let name = Fmt.str "%s__%i" (Device.nice_name dev) id in let label = Fmt.str "%s\n%s\n%a" name (Device.module_name dev) Fmt.(list ~sep:(any ", ") Key.pp) (Device.keys dev) in [ ("label", label); ("shape", "box") ] in let pp_attr ppf (field, v) = Fmt.pf ppf "%s=%S" field v in Fmt.pf ppf "%d [%a];" id (Fmt.list ~sep:(Fmt.any ", ") pp_attr) attrs let pp_edges ppf (id, id', label) = let attrs = match label with | Functor -> [ ("style", "bold"); ("tailport", "sw") ] | Argument -> [] | Dependency -> [ ("style", "dashed") ] | Branch { default } -> let l = [ ("style", "dotted"); ("headport", "n") ] in if default then ("style", "bold") :: l else l in let pp_attr ppf (field, v) = Fmt.pf ppf "%s=%S" field v in Fmt.pf ppf "%d -> %d [%a];" id id' (Fmt.list ~sep:(Fmt.any ", ") pp_attr) attrs let pp ppf t = let vertices, edges = as_dot_graph t in Fmt.pf ppf {|@[digraph G {@,ordering=out;@,%a@,@,%a@,}@]|} (Fmt.list ~sep:Fmt.cut pp_vertice) vertices (Fmt.list ~sep:Fmt.cut pp_edges) edges end let pp_dot = Dot.pp