Mercurial > hg > callgraph
diff prolog/callgraph.pl @ 0:b12b733d1dd0
Initial check-in.
author | samer |
---|---|
date | Sun, 23 Mar 2014 16:05:36 +0000 |
parents | |
children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/prolog/callgraph.pl Sun Mar 23 16:05:36 2014 +0000 @@ -0,0 +1,404 @@ +:- module(callgraph, + [ module_dotpdf/2 + , module_dot/2 + % , assert_module_graph/1 + % , current_ugraph/1 + % , current_dot/3 + % , module_graphml/1 + ]). + +/** <module> Visualisation of inter-predicate call graphs + + ---++ Usage + This module allows you to produce a call graph of a module, where + nodes (boxes) represent predicates in the module and an edge between + two predicates indicates that one (the source end) calls the other + (the pointy end). + + By default, node styles are used to indicate whether or not the predicate is + exported (bold outline), dynamic (italic label, filled), multifile (box with diagonals + in the corners). + For dynamic predicates, dashed edges are used to represent operations which + mutate (assert to or retract from) the predicate. + + Items in the recorded database are also represented, as they consitute mutable + state much like dynamic predicates. Recorded items are labelled as Key:Functor/Arity + and drawn in a filled octagonal box. Dashed edges represents writing to the + recorded database and ordinary solid edges represent reading. + + Basic method of usage is: + 1. Load the module you want to graph. + 2. Load this module. + 3. Generate and write the module call graph to a PDF document: + == + ?- module_dotpdf(ModuleName,[]). + == + + See module_dot/2 for options that affect graph content and style, and + module_dotpdf/2 for options that affect rendering. + + ---++ Implementation notes + + NB. This is very preliminary. The intereface is not necessarily stable. + The dot DCG implementation is a bit embarassing but it does the job for now. + + Three parts to this: + 1. Using prolog_walk_code/1 to compile a database of inter-predicate calls + into dynamic predicates calls/2, mutates/2, reads/2 and writes/2. + 2. Possible transformations of graph (eg pruning). + 3. Collect information about desired modules/predicates into a Dot graph + structure as defined by dotdct.pl +*/ + + +:- use_module('library/dcgu'). +:- use_module('library/dot'). +% :- use_module(library(graphml_ugraph)). + +% ------------ Building the call graph in the Prolog database ----------------------- + +:- dynamic calls/2. +:- dynamic mutates/2. +:- dynamic reads/2, writes/2. + +%% assert_module_graph(+ModuleName) is det. +% Analyses module ModuleName (using prolog_walk_code/1) asserting information about the +% call graph to a set of private dynamic predicates. +assert_module_graph(Mod) :- + retract_call_graph, + prolog_walk_code([ trace_reference(_), module(Mod), on_trace(assert_edge(Mod)), source(false) ]), + predicate_property(calls(_,_), number_of_clauses(N)), + format('Got ~D edges~n', [N]). + +retract_call_graph :- + retractall(calls(_,_)), + retractall(mutates(_,_)), + retractall(reads(_,_)), + retractall(writes(_,_)). + +% Irreflexive version of calls. +calls_ir(Caller,Callee) :- + calls(Caller,Callee), + Caller\=Callee. + +%% mutator(+Goal,-Pred) is semidet. +% True when Goal changes predicate Pred. +mutator(assert(H:-_),H) :- !. +mutator(assertz(H:-_),H) :- !. +mutator(asserta(H:-_),H) :- !. +mutator(assert(H),H) :- !. +mutator(assertz(H),H). +mutator(asserta(H),H). +mutator(retract(H),H). +mutator(retractall(H),H). +caller(retract(H), H). + +reader(recorded(Key,Term,_), Node) :- goal_pred(Key:Term,Node). +reader(recorded(Key,Term), Node) :- goal_pred(Key:Term,Node). +writer(recorda(Key,Term,_), Node) :- goal_pred(Key:Term,Node). +writer(recorda(Key,Term), Node) :- goal_pred(Key:Term,Node). +writer(recordz(Key,Term,_), Node) :- goal_pred(Key:Term,Node). +writer(recordz(Key,Term), Node) :- goal_pred(Key:Term,Node). +writer(record(Key,Term,_), Node) :- goal_pred(Key:Term,Node). +writer(record(Key,Term), Node) :- goal_pred(Key:Term,Node). + +assert_edge(M, M:Head, M:Caller, _Where) :- + writer(Head,Node), !, + goal_pred(M:Caller,CallerFA), + assert_fact(writes(CallerFA,Node)). + +assert_edge(M, M:Head, M:Caller, _Where) :- + reader(Head,Node), !, + goal_pred(M:Caller,CallerFA), + assert_fact(reads(CallerFA,Node)). + +assert_edge(M, M:Head, M:Modifier, _Where) :- + mutator(Head,Modified), !, + goal_pred(M:Modified,ModifiedFA), + goal_pred(M:Modifier,ModifierFA), + assert_fact(mutates(ModifierFA,ModifiedFA)), + ( caller(Head,Modified) + -> assert_fact(calls(ModifierFA,ModifiedFA)) + ). + +% matches calls within module M. +assert_edge(M, M:Callee, M:Caller, _Where) :- + \+predicate_property(M:Callee, built_in), + \+predicate_property(M:Callee, imported_from(_)), !, + goal_pred(M:Callee,CalleeFA), + goal_pred(M:Caller,CallerFA), + assert_fact(calls(CallerFA,CalleeFA)). + +% do nothing silently for other calls +assert_edge(_,_,_,_). + +% asserts fact if not already asserted. +assert_fact(Head) :- call(Head) -> true; assertz(Head). + +goal_pred(M:H,M:F/A) :- nonvar(M), (nonvar(H);ground(F/A)), functor(H,F,A). + + +% ----------------------- GraphML output ---------------------- +% Leaving this out for the time being. + +% module_graphml(Mod) :- +% assert_module_graph(Mod), +% current_ugraph(Graph), +% retract_call_graph, +% format(atom(File),'~w.graphml',[Mod]), +% graphml_write_ugraph(File, nomap, [], Graph). + +% nomap(id,node(N),A) :- term_to_atom(N,A), writeln(nomap(id,node(N),A)). +% % nomap(id,edge(_,_),''). +% % [key(node, color, string), key(edge,color,string)], +% % cmap(color, node(_), green). +% % cmap(color, edge(_), red). + + +% %% current_ugraph(-Graph:graphml) is det. +% % Returns the current call graph as a GraphML document structure. +% current_ugraph(Graph) :- +% findall(Pred, (calls_ir(Mod:Pred,_);calls_ir(_,Mod:Pred)), Preds), +% sort(Preds,Preds1), +% setof(Caller-Callees, (member(Caller,Preds1), callees(Mod,Caller,Callees)), Graph). + +% callees(Mod,Caller,Callees) :- setof(Callee, calls_ir(Mod:Caller,Mod:Callee),Callees), !. +% callees(_,[]). + + + +% ----------------------- Dot output ---------------------- + + +%% module_dot(+ModuleName,Opts) is det. +% Writes a call graph for named module as a dot file named "[ModuleName].dot". +% This predicate also accepts various options (some of the these types +% refer to the GraphViz attribute types): +% * prune(Prune:bool) / false +% If true, then graph subtrees are removed using prune_subtrees/0. +% * recursive(bool) / false +% If true, then looping edges are used to decorate predicates that call themselves +% directly. Otherwise, such direct recursion is hidden. +% * hide_list(list(pred_ind)) / [] +% A list of predicate (name/arity) to hide from the graph. +% * arrowhead(arrow_type) / vee +% Dot arrowhead name as an atom, for all call edges. +% * export_style(node_style) / bold +% Dot node style for exported predicates. +% * dynamic_shape(node_shape) / box +% Dot node shape for dynamic predicates. +% * dynamic_style(node_style) / filled +% Dot node style for dynamic predicates. +% * multifile_shape(node_shape) / box +% Dot node shape for multifile predicates. +% * multifile_style(node_style) / diagonals +% Dot node style for multifile predicates. +% * recorded_shape(node_shape) / octagon +% Dot node shape for recorded facts. +% * recorded_style(node_style) / filled +% Dot node style for recorded facts. +% * mutate_style(line_style) / dashed +% Dot line style for edges representing mutation of a dynamic predicate. +% * read_style(line_style) / solid +% Dot line style for edges representing readed of a recorded fact. +% * write_style(line_style) / dashed +% Dot line style for edges representing writing of a recored fact. +% * font(S:string) +% Font family (as a list of codes) for all labels. How these names are +% interpreted depends on your host operating system. On Mac OS X, I find +% I am able to use any font available in the "Font Book" application with +% the name written exactly (including spaces) as in the "Font" column. +% Default is "Times". +% +% Types for Dot attributes: +% see http://graphviz.org/Documentation.php for more details on +% * arrow_type: http://graphviz.org/content/attrs#karrowType +% * node_shape: http://graphviz.org/content/node-shapes +% * node_style: http://graphviz.org/content/attrs#kstyle +% +% == +% line_style ---> solid ; dashed ; dotted ; bold. +% arrow_type ---> normal ; vee ; empty ; box ; none ; dot ; ... . +% node_shape ---> box ; ellipse ; circle ; diamond ; trapezium ; parallelogram +% ; house ; square ; pentagon ; hexagon ; septagon ; octagon ; ... . +% node_style ---> solid ; dashed ; dotted ; bold ; rounded +% ; diagonals ; filled ; striped ; wedged. +% == +module_dot(Mod,Opts) :- + assert_module_graph(Mod), + (option(prune(true),Opts) -> prune_subtrees; true), + current_dot(Mod,Opts,Graph), + retract_call_graph, + format(atom(File),'~w.dot',[Mod]), + graph_dot(Graph,File). + +%% module_dotpdf(+Mod,Opts) is det. +% Writes a call graph for module Mod as a PDF file named "[Mod].pdf". +% As well as the options accepted by module_dot/2, this predicate also accepts: +% * method(Method:graphviz_method) / unflatten +% Determines which GraphViz programs are used to render the graph. The type +% graphviz_method is defined as: +% == +% graphviz_method ---> dot ; neato; fdp ; sfdp ; circo ; twopi +% ; unflatten(list(unflatten_opt)) +% ; unflatten. +% unflatten_opt ---> l(N:natural) % -l<N> +% ; fl(N:natural) % -f -l<N> +% ; c(natural). % -c<N> +% == +% The unflatten methods filter the graph through unflatten before passing +% on to dot. +module_dotpdf(Mod,Opts) :- + assert_module_graph(Mod), + option(method(Method),Opts,unflatten), + (option(prune(true),Opts) -> prune_subtrees; true), + current_dot(Mod,Opts,Graph), + retract_call_graph, + dotrun(Method,pdf,Graph,Mod). + + +%% current_dot(+Mod,+Opts,-DotGraph) is det. +% Returns the currently asserted graph as a dot graph structure, +% using the given options and restricting the graph to module Mod. +% The options are documented under module_dot/2. +current_dot(Mod,Opts,digraph(Mod,Graph)) :- + predopt(Opts,recorded,DBNodeAttr,[]), + setof(with_opts(node(Pred),Attrs), node_decl(Opts,Mod,Pred,Attrs), Decls), + esetof(with_opts(node(N),DBNodeAttr), db_node(N), DBNodes), + writeln(DBNodes), + module_graph(Mod,Opts,Decls,DBNodes,Graph,[]). + +node_decl(Opts,Mod,Pred,Attrs) :- + declarable_node(Opts,Mod,Pred), + pred_attr(Opts,Mod:Pred,Attrs). + +read_edge(Mod,_Opts,Pred,DBTerm) :- reads(Mod:Pred, DBTerm). +write_edge(Mod,_Opts,Pred,DBTerm) :- writes(Mod:Pred, DBTerm). + +declarable_node(Opts,M,Pred) :- + option(hide_list(HideList),Opts,[]), + ( predicate_property(M:Head, dynamic) + ; predicate_property(M:Head, exported) + ; predicate_property(M:Head, multifile) + ), + \+predicate_property(M:Head, built_in), + \+predicate_property(M:Head, imported_from(_)), + goal_pred(M:Head,M:Pred), + \+member(Pred, ['$mode'/2,'$pldoc'/4, '$pldoc_link'/2]), + \+member(Pred,HideList). + +visible_call(Mod,Opts,Caller,Callee) :- + option(hide_list(L),Opts,[]), + option(recursive(T),Opts,false), + calls(Mod:Caller,Mod:Callee), + (T=false -> Caller\=Callee; true), + \+member(Caller,L), + \+member(Callee,L). + +visible_mutation(Mod,Opts,P1,P2) :- + option(hide_list(L),Opts,[]), + mutates(Mod:P1,Mod:P2), + \+member(P1,L), + \+member(P2,L). + +module_graph(Mod,Opts,Decls,DBNodes) --> + { edgeopt(Opts,mutates,MAttr,[]), + edgeopt(Opts,reads,RAttr,[]), + edgeopt(Opts,writes,WAttr,[]) + }, + seqmap(global_opts(Opts),[graph,node,edge]), % global attributes + list(Decls), list(DBNodes), + findall(arrow(Caller,Callee), visible_call(Mod,Opts,Caller,Callee)), + findall(with_opts(arrow(Mutator,Mutatee),MAttr), visible_mutation(Mod,Opts,Mutator,Mutatee)), + findall(with_opts(arrow(Pred,DBTerm),RAttr), read_edge(Mod,Opts,Pred,DBTerm)), + findall(with_opts(arrow(Pred,DBTerm),WAttr), write_edge(Mod,Opts,Pred,DBTerm)). + +esetof(A,B,C) :- setof(A,B,C) *-> true; C=[]. + +list([]) --> []. +list([X|XS]) --> [X], list(XS). + +db_node(N) :- reads(_,N); writes(_,N). + +global_opts(_,graph) --> []. +global_opts(O,node) --> {font(normal,O,F)}, [node_opts([ shape=at(box), fontname=qq(F) ])]. +global_opts(O,edge) --> {option(arrowhead(AH),O,vee)}, [edge_opts([ arrowhead=at(AH) ])]. + +predopt(O,exported) --> + {option(export_style(S),O,bold)}, + {font(bold,O,F)}, + [ style = qq(at(S)), fontname=qq(F) ]. +predopt(O,dynamic) --> + {option(dynamic_shape(S),O,box)}, + {option(dynamic_style(St),O,filled)}, + {font(italic,O,F)}, + [ shape = at(S), fontname=qq(F), style = qq(at(St)) ]. +predopt(O,multifile) --> + {option(multifile_shape(S),O,box)}, + {option(multifile_style(St),O,diagonals)}, + [ shape = at(S), style = qq(at(St)) ]. +predopt(O,recorded) --> + {option(recorded_shape(S),O,octagon)}, + {option(recorded_style(St),O,filled)}, + [ shape = at(S), style = qq(at(St)) ]. + +edgeopt(O,mutates) --> {option(mutate_style(S),O,dashed)}, [ style = qq(at(S)) ]. +edgeopt(O,writes) --> {option(write_style(S),O,dashed)}, [ style = qq(at(S)) ]. +edgeopt(O,reads) --> {option(read_style(S),O,solid)}, [ style = qq(at(S)) ]. + +pred_attr(O,Pred,Attrs1) :- + goal_pred(Goal,Pred), + phrase( ( if( predicate_property(Goal,dynamic), predopt(O,dynamic)), + if( predicate_property(Goal,multifile), predopt(O,multifile)), + if( predicate_property(Goal,exported), predopt(O,exported))), + Attrs, []), + Attrs = [_|_], + compile_attrs(Attrs,[],Attrs1). + +compile_attrs([],A,A). +compile_attrs([style=S|AX],AttrsSoFar,FinalAttrs) :- !, + ( select(style=OS,AttrsSoFar,A1) + -> combine_styles(S,OS,NS), A2=[style=NS|A1] + ; A2=[style=S|AttrsSoFar] + ), + compile_attrs(AX,A2,FinalAttrs). +compile_attrs([A|AX],A0,A2) :- compile_attrs(AX,[A|A0],A2). + +combine_styles(qq(S1),qq(S2),qq((S1,",",S2))). + +% compile_attrs1([],A,[]). +% compile_attrs1([A|AX],A0,[A|A1]) :- compile_attrs1(AX,[A|A0],A1). + +font_family(O) --> {option(font(FF),O,"Times")}, seqmap(out,FF). +font(normal,O,F) :- phrase(font_family(O),F,[]). +font(italic,O,F) :- phrase((font_family(O)," Italic"),F,[]). +font(bold,O,F) :- phrase((font_family(O)," Bold"),F,[]). + +do_until(P) :- + call(P,Flag), + ( Flag=true -> true + ; do_until(P) + ). + +prunable(Node) :- + setof( Parent, calls(Parent,Node), [_]), % node has exactly one caller + \+calls(Node,_), % no children + \+mutates(Node,_), % doesn't affect dynamic preds + goal_pred(G,Node), + \+predicate_property(G,dynamic), + \+predicate_property(G,multifile), + \+predicate_property(G,exported). + +%% prune_subtrees is det. +% Operates on the currently asserted graph (see assert_module_graph/1). It searches +% for any part of the call graph which is a pure tree, and removes all the nodes below +% the root. Thus, any 'leaf' predicate which is only ever called by one 'parent' is +% removed. This is step is repeated until there are no more leaf predicates. The idea +% is that the child tree can be considered 'private' to its parent. +prune_subtrees :- do_until(prune_subtrees). + +prune_subtrees(false) :- + bagof(Node, prunable(Node), Nodes), !, + forall(member(N,Nodes), (writeln(pruning:N), retractall(calls(_,N)))). + +prune_subtrees(true).