File Coverage

blib/lib/Build/Hopen/G/DAG.pm
Criterion Covered Total %
statement 124 153 81.0
branch 22 54 40.7
condition 1 2 50.0
subroutine 21 27 77.7
pod 7 7 100.0
total 175 243 72.0


line stmt bran cond sub pod time code
1             # Build::Hopen::G::DAG - hopen build graph
2             package Build::Hopen::G::DAG;
3 2     2   763 use Build::Hopen::Base;
  2         4  
  2         14  
4 2     2   696 use Build::Hopen qw(hlog $QUIET);
  2         5  
  2         194  
5              
6             our $VERSION = '0.000006'; # TRIAL
7              
8 2     2   12 use parent 'Build::Hopen::G::Op';
  2         4  
  2         10  
9             use Class::Tiny {
10 0         0 goals => sub { [] },
11 2         15 default_goal => undef,
12              
13             # Private attributes with simple defaults
14             #_node_by_name => sub { +{} }, # map from node names to nodes in either
15             # # _init_graph or _graph
16              
17             # Private attributes - initialized by BUILD()
18             _graph => undef, # L instance
19             _final => undef, # The graph root - all goals have edges to this
20              
21             #Initialization operations
22             _init_graph => undef, # L for initializations
23             _init_first => undef, # Graph node for initialization - the first
24             # init operation to be performed.
25              
26             # TODO? also support fini to run operations after _graph runs?
27 2     2   121 };
  2         4  
28              
29 2     2   2146 use Build::Hopen::G::Goal;
  2         6  
  2         62  
30 2     2   352 use Build::Hopen::G::Link;
  2         3  
  2         44  
31 2     2   9 use Build::Hopen::G::Node;
  2         3  
  2         35  
32 2     2   718 use Build::Hopen::G::PassthroughOp;
  2         4  
  2         60  
33 2     2   10 use Build::Hopen::Util::Data qw(forward_opts);
  2         3  
  2         67  
34 2     2   9 use Build::Hopen::Arrrgs;
  2         3  
  2         54  
35 2     2   1257 use Graph;
  2         165191  
  2         61  
36 2     2   18 use Storable ();
  2         5  
  2         42  
37              
38             # Class data {{{1
39              
40             use constant {
41 2         2896 LINKS => 'link_list', # Graph edge attr: array of BHG::Link instances
42 2     2   8 };
  2         4  
43              
44             # A counter used for making unique names
45             my $_id_counter = 0; # threads: make shared
46              
47             # }}}1
48             # Docs {{{1
49              
50             =head1 NAME
51              
52             Build::Hopen::G::DAG - A hopen build graph
53              
54             =head1 SYNOPSIS
55              
56             This class encapsulates the DAG for a particular set of one or more goals.
57             It is itself a L so that it can be composed into
58             other DAGs.
59              
60             =head1 ATTRIBUTES
61              
62             =head2 goals
63              
64             Arrayref of the goals for this DAG.
65              
66             =head2 default_goal
67              
68             The default goal for this DAG.
69              
70             =head2 _graph
71              
72             The actual L. If you find that you have to use it, please open an
73             issue so we can see about providing a documented API for your use case!
74              
75             =head2 _final
76              
77             The node to which all goals are connected.
78              
79             =head2 _init_graph
80              
81             A separate L of operations that will run before all the operations
82             in L. This is because I don't want to add an edge to every
83             single node just to force the topological sort to work out.
84              
85             =head2 _init_first
86              
87             The first node to be run in _init_graph.
88              
89             =head1 FUNCTIONS
90              
91             =cut
92              
93             # }}}1
94              
95             =head2 run
96              
97             Traverses the graph. The DAG is similar to a subroutine in this respect.
98             The outputs from all the goals
99             of the DAG are aggregated and provided as the outputs of the DAG.
100             The output is a hash keyed by the name of each goal, with each goal's outputs
101             as the values under that name. Usage:
102              
103             my $hrOutputs = $dag->run(-scope=>$scope[, other options])
104              
105             C<$scope> is required, and must be a L or subclass.
106             Other options are as L.
107              
108             =cut
109              
110             # my $merger = Hash::Merge->new('RETAINMENT_PRECEDENT'); # TODO
111              
112             sub run {
113 1     1 1 837 my ($self, %args) = parameters('self', [qw(scope; phase generator)], @_);
114 1         11 my $outer_scope = $args{scope}; # From the caller
115 1         3 my $retval = {};
116              
117             # The scope attached to the DAG takes precedence over the provided Scope.
118             # This is realized by making $outer_scope the outer of our scope for
119             # the duration of this call.
120 1         30 my $dag_scope_saver = $self->scope->outerize($outer_scope);
121              
122             # --- Get the initialization ops ---
123              
124 1         2 my @init_order = eval { $self->_init_graph->toposort };
  1         29  
125 1 50       1450 die "Initializations contain a cycle!" if $@;
126              
127             # --- Get the runtime ops ---
128              
129 1         2 my @order = eval { $self->_graph->toposort };
  1         24  
130             # TODO someday support multi-core-friendly topo-sort, so nodes can run
131             # in parallel until they block each other.
132 1 50       2883 die "Graph contains a cycle!" if $@;
133              
134             # Remove _final from the order for now - I don't yet know what it means
135             # to traverse _final.
136 1 50       26 die "Last item in order isn't _final!"
137             unless $order[$#order] == $self->_final;
138 1         9 pop @order;
139              
140             # --- Traverse ---
141              
142             # Note: while hacking, please make sure Goal nodes can appear
143             # anywhere in the graph.
144              
145 1     1   21 hlog { 'Traversing DAG ' . $self->name };
  1         10  
146 1         28 my $graph = $self->_init_graph;
147 1         9 foreach my $node (@init_order, undef, @order) {
148              
149 4 100       33 if(!defined($node)) { # undef is the marker between init and run
150 1         22 $graph = $self->_graph;
151 1         7 next;
152             }
153              
154             # Inputs to this node. TODO should the provided inputs be given
155             # to each node? Any node with no predecessors? Currently each
156             # node has the option.
157 3         11 my $node_scope = Build::Hopen::Scope::Hash->new;
158             # TODO make this a BH::Scope::Inputs once it's implemented
159 3         99 $node_scope->outer($self->scope);
160             # Data specifically being provided to the current node, e.g.,
161             # on input edges, beats the scope of the DAG as a whole.
162              
163             # Iterate over each node's edges and process any Links
164 3         67 foreach my $pred ($graph->predecessors($node)) {
165 1     1   178 hlog { ('From', $pred->name, 'to', $node->name) };
  1         4  
166              
167             # Goals do not feed outputs to other Goals. This is so you can
168             # add edges between Goals to set their order while keeping the
169             # data for each Goal separate.
170             # TODO add tests for this
171 1 50       7 next if eval { $pred->DOES('Build::Hopen::G::Goal') };
  1         11  
172              
173 1         7 my $links = $graph->get_edge_attribute($pred, $node, LINKS);
174              
175 1 50       752 unless($links) { # Simple case: predecessor's outputs become our inputs
176 0         0 push @{$node_scope->inputs}, $pred->outputs;
  0         0  
177 0         0 next;
178             }
179              
180             # More complex case: Process all the links
181 1         4 my $hrPredOutputs = $pred->outputs; # In one test, outputs was undef if not on its own line.
182 1         18 my $link_scope = Build::Hopen::Scope::Hash->new->add(%{$hrPredOutputs});
  1         23  
183             # All links get the same outer scope --- they are parallel,
184             # not in series.
185 1         15 $link_scope->outer($self->scope);
186             # The links run at the same scope level as the node.
187              
188 1         19 foreach my $link (@$links) {
189 1     1   9 hlog { ('From', $pred->name, 'via', $link->name, 'to', $node->name) };
  1         4  
190 1         12 my $link_outputs = $link->run(
191             -scope=>$link_scope,
192             forward_opts(\%args, {'-'=>1}, 'phase')
193             # Generator not passed to links.
194             );
195 1         12 $node_scope->add($_, $link_outputs->{$_}) foreach keys %{$link_outputs};
  1         7  
196             #say 'Link ', $link->name, ' outputs: ', Dumper($link_outputs); # DEBUG
197             } #foreach incoming link
198             } #foreach predecessor node
199              
200 3     0   372 hlog { 'Node', $node->name, 'input', Dumper($node_scope->as_hashref) } 3;
  0         0  
201 3         21 my $step_output = $node->run(-scope=>$node_scope,
202             forward_opts(\%args, {'-'=>1}, 'phase', 'generator')
203             );
204 3         49 $node->outputs($step_output);
205 3     0   18 hlog { 'Node', $node->name, 'output', Dumper($step_output) } 3;
  0         0  
206              
207             # Give the Generator a chance, and stash the results if necessary.
208 3 100       8 if(eval { $node->DOES('Build::Hopen::G::Goal') }) {
  3         45  
209 1 50       4 $args{generator}->visit_goal($node) if $args{generator};
210              
211             # Save the result if there is one. Don't save {}.
212             # use $node->outputs, not $step_output, since the generator may
213             # alter $node->outputs.
214 1 50       4 $retval->{$node->name} = $node->outputs if keys %{$node->outputs};
  1         4  
215             } else {
216 2 50       13 $args{generator}->visit_node($node) if $args{generator};
217             }
218              
219             } #foreach node
220              
221 1         15 return $retval;
222             } #run()
223              
224             =head1 ADDING DATA
225              
226             =head2 goal
227              
228             Creates a goal of the DAG. Goals are names for sequences of operations,
229             akin to top-level Makefile targets. Usage:
230              
231             my $goalOp = $dag->goal('name')
232              
233             Returns a passthrough operation representing the goal. Any inputs passed into
234             that operation are provided as outputs of the DAG under the corresponding name.
235              
236             TODO integrate
237             A C file with no C calls will result in nothing
238             happening when C is run.
239              
240             The first call to C also sets L.
241              
242             =cut
243              
244             sub goal {
245 3 50   3 1 1015 my $self = shift or croak 'Need an instance';
246 3 50       11 my $name = shift or croak 'Need a goal name';
247 3         22 my $goal = Build::Hopen::G::Goal->new(name => $name);
248 3         88 $self->_graph->add_vertex($goal);
249             #$self->_node_by_name->{$name} = $goal;
250 3         410 $self->_graph->add_edge($goal, $self->_final);
251 3 100       1203 $self->default_goal($goal) unless $self->default_goal;
252 3         52 return $goal;
253             } #goal()
254              
255             =head2 connect
256              
257             - C, , , )>:
258             connects output C<< out-edge >> of operation C<< op1 >> as input C<< in-edge >> of
259             operation C<< op2 >>. No processing is done between output and input.
260             - C<< out-edge >> and C<< in-edge >> can be anything usable as a table index,
261             provided that table index appears in the corresponding operation's
262             descriptor.
263             - C, )>: creates a dependency edge from C<< op1 >> to
264             C<< op2 >>, indicating that C<< op1 >> must be run before C<< op2 >>.
265             Does not transfer any data from C<< op1 >> to C<< op2 >>.
266             - C, , )>: Connects C<< op1 >> to
267             C<< op2 >> via L C<< Link >>.
268              
269             Returns the name of the edge? The edge instance itself? Maybe a
270             fluent interface to the DAG for chaining C calls?
271              
272             =cut
273              
274             sub connect {
275 1 50   1 1 426 my $self = shift or croak 'Need an instance';
276 1         3 my ($op1, $out_edge, $in_edge, $op2) = @_;
277              
278 1         2 my $link;
279 1 50       5 if(!defined($in_edge)) { # dependency edge
    50          
280 0         0 $op2 = $out_edge;
281 0         0 $out_edge = false; # No outputs
282 0         0 $in_edge = false; # No inputs
283             } elsif(!defined($op2)) {
284 1         2 $op2 = $in_edge;
285 1         2 $link = $out_edge;
286 1         2 $out_edge = false; # No outputs TODO
287 1         2 $in_edge = false; # No inputs TODO
288             }
289              
290             # Create the link
291 1 50       52 unless($link) {
292 0         0 $link = Build::Hopen::G::Link->new(
293             name => 'link_' . $op1->name . '_' . $op2->name,
294             in => [$out_edge], # Output of op1
295             out => [$in_edge], # Input to op2
296             );
297             }
298              
299 1     1   7 hlog { 'DAG::connect(): Edge from', $op1->name, 'via', $link->name,
300 1         10 'to', $op2->name };
301              
302             # Add it to the graph (idempotent)
303 1         28 $self->_graph->add_edge($op1, $op2);
304             #$self->_node_by_name->{$_->name} = $_ foreach ($op1, $op2);
305              
306             # Save the BHG::Link as an edge attribute (not idempotent!)
307 1   50     519 my $attrs = $self->_graph->get_edge_attribute($op1, $op2, LINKS) || [];
308 1         1639 push @$attrs, $link;
309 1         22 $self->_graph->set_edge_attribute($op1, $op2, LINKS, $attrs);
310              
311 1         685 return $link;
312             } #connect()
313              
314             =head2 add
315              
316             Add a regular node to the graph. An attempt to add the same node twice will be
317             ignored. Usage:
318              
319             my $node = Build::Hopen::G::Op->new(name=>"whatever");
320             $dag->add($node);
321              
322             Returns the node, for the sake of chaining.
323              
324             =cut
325              
326             sub add {
327 0 0   0 1 0 my $self = shift or croak 'Need an instance';
328 0 0       0 my $node = shift or croak 'Need a node';
329 0 0       0 return if $self->_graph->has_vertex($node);
330 0     0   0 hlog { __PACKAGE__, 'adding', Dumper($node) } 2;
  0         0  
331              
332 0         0 $self->_graph->add_vertex($node);
333             #$self->_node_by_name->{$node->name} = $node if $node->name;
334              
335 0         0 return $node;
336             } #add()
337              
338             =head2 init
339              
340             Add an initialization operation to the graph. Initialization operations run
341             before all other operations. An attempt to add the same initialization
342             operation twice will be ignored. Usage:
343              
344             my $op = Build::Hopen::G::Op->new(name=>"whatever");
345             $dag->init($op[, $first]);
346              
347             If C<$first> is truthy, the op will be run before anything already in the
348             graph. However, later calls to C with C<$first> set will push
349             operations even before C<$op>.
350              
351             Returns the node, for the sake of chaining.
352              
353             =cut
354              
355             sub init {
356 0 0   0 1 0 my $self = shift or croak 'Need an instance';
357 0 0       0 my $op = shift or croak 'Need an op';
358 0         0 my $first = shift;
359 0 0       0 return if $self->_init_graph->has_vertex($op);
360              
361 0         0 $self->_init_graph->add_vertex($op);
362             #$self->_node_by_name->{$op->name} = $op;
363              
364 0 0       0 if($first) { # $op becomes the new _init_first node
365 0         0 $self->_init_graph->add_edge($op, $self->_init_first);
366 0         0 $self->_init_first($op);
367             } else { # Not first, so can happen anytime. Add it after the
368             # current first node.
369 0         0 $self->_init_graph->add_edge($self->_init_first, $op);
370             }
371              
372 0         0 return $op;
373             } #init()
374              
375             =head1 ACCESSORS
376              
377             =head2 empty
378              
379             Returns truthy if the only nodes in the graph are internal nodes.
380             Intended for use by hopen files.
381              
382             =cut
383              
384             sub empty {
385 0 0   0 1 0 my $self = shift or croak 'Need an instance';
386 0         0 return ($self->_graph->vertices == 1);
387             # _final is the node in an empty() graph.
388             # We don't check the _init_graph since empty() is intended
389             # for use by hopen files, not toolsets.
390             } #empty()
391              
392             =head1 OTHER
393              
394             =head2 BUILD
395              
396             Initialize the instance.
397              
398             =cut
399              
400             sub BUILD {
401             #use Data::Dumper;
402             #say Dumper(\@_);
403 2 50   2 1 483 my $self = shift or croak 'Need an instance';
404 2         4 my $hrArgs = shift;
405              
406             # DAGs always have names
407 2 50       12 $self->name('__R_DAG_' . $_id_counter++) unless $self->has_custom_name;
408              
409             # Graph of normal operations
410 2         12 my $graph = Graph->new( directed => true,
411             refvertexed => true);
412 2         495 my $final = Build::Hopen::G::Node->new(
413             name => '__R_DAG_ROOT' . $_id_counter++);
414 2         206 $graph->add_vertex($final);
415 2         389 $self->_graph($graph);
416 2         47 $self->_final($final);
417              
418             # Graph of initialization operations
419 2         14 my $init_graph = Graph->new( directed => true,
420             refvertexed => true);
421 2         316 my $init = Build::Hopen::G::PassthroughOp->new(
422             name => '__R_DAG_INIT' . $_id_counter++);
423 2         28 $init_graph->add_vertex($init);
424              
425 2         271 $self->_init_graph($init_graph);
426 2         37 $self->_init_first($init);
427             } #BUILD()
428              
429             1;
430             # Rest of the docs {{{1
431             __END__