File Coverage

blib/lib/Build/Hopen/G/DAG.pm
Criterion Covered Total %
statement 119 150 79.3
branch 23 58 39.6
condition 1 2 50.0
subroutine 17 25 68.0
pod 6 6 100.0
total 166 241 68.8


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   1030 use Build::Hopen::Base;
  2         5  
  2         18  
4 2     2   913 use Build::Hopen qw(hlog $QUIET);
  2         6  
  2         290  
5              
6             our $VERSION = '0.000008'; # TRIAL
7              
8 2     2   15 use parent 'Build::Hopen::G::Op';
  2         4  
  2         15  
9             use Class::Tiny {
10 0         0 goals => sub { [] },
11 2         19 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 sink - 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   178 };
  2         4  
28              
29 2     2   2745 use Build::Hopen::G::Goal;
  2         6  
  2         90  
30 2     2   461 use Build::Hopen::G::Link;
  2         5  
  2         55  
31 2     2   10 use Build::Hopen::G::Node;
  2         4  
  2         41  
32 2     2   950 use Build::Hopen::G::CollectOp;
  2         4  
  2         79  
33 2     2   13 use Build::Hopen::Util::Data qw(forward_opts);
  2         4  
  2         83  
34 2     2   12 use Build::Hopen::Arrrgs;
  2         4  
  2         68  
35 2     2   1609 use Graph;
  2         204944  
  2         77  
36 2     2   21 use Storable ();
  2         5  
  2         68  
37              
38             # Class data {{{1
39              
40             use constant {
41 2         3464 LINKS => 'link_list', # Graph edge attr: array of BHG::Link instances
42 2     2   12 };
  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([-context=>$scope][, other options])
104              
105             C<$scope> must be a L or subclass if provided.
106             Other options are as L.
107              
108             =cut
109              
110             # The implementation of run(). $self->scope has already been linked to the context.
111             sub _run {
112 1     1   4 my ($self, %args) = parameters('self', [qw(; phase generator)], @_);
113 1         3 my $retval = {};
114              
115             # --- Get the initialization ops ---
116              
117 1         2 my @init_order = eval { $self->_init_graph->toposort };
  1         22  
118 1 50       1538 die "Initializations contain a cycle!" if $@;
119 1 50       29 @init_order = () if $self->_init_graph->vertices == 1; # no init nodes => skip
120              
121             # --- Get the runtime ops ---
122              
123 1         48 my @order = eval { $self->_graph->toposort };
  1         18  
124             # TODO someday support multi-core-friendly topo-sort, so nodes can run
125             # in parallel until they block each other.
126 1 50       3767 die "Graph contains a cycle!" if $@;
127              
128             # Remove _final from the order for now - I don't yet know what it means
129             # to traverse _final.
130 1 50       34 die "Last item in order isn't _final! This might indicate a bug in hopen."
131             unless $order[$#order] == $self->_final;
132 1         8 pop @order;
133              
134             # --- Traverse ---
135              
136             # Note: while hacking, please make sure Goal nodes can appear
137             # anywhere in the graph.
138              
139 1     0   7 hlog { my $x = 'Traversing DAG ' . $self->name; $x, '*' x (78-length($x)) };
  0         0  
  0         0  
140 1         20 my $graph = $self->_init_graph;
141 1         6 foreach my $node (@init_order, undef, @order) {
142              
143 3 100       22 if(!defined($node)) { # undef is the marker between init and run
144 1         16 $graph = $self->_graph;
145 1         5 next;
146             }
147              
148             # Inputs to this node. These are different from the DAG's inputs.
149             # The scope stack is (outer to inner) DAG's inputs, DAG's overrides,
150             # then $node_inputs, then the individual node's overrides.
151 2         7 my $node_inputs = Build::Hopen::Scope::Hash->new;
152             # TODO make this a BH::Scope::Inputs once it's implemented
153 2         69 $node_inputs->outer($self->scope);
154             # Data specifically being provided to the current node, e.g.,
155             # on input edges, beats the scope of the DAG as a whole.
156 2         78 $node_inputs->local(true);
157             # A CollectOp won't reach above the node's inputs by default.
158              
159             # Iterate over each node's edges and process any Links
160 2         15 foreach my $pred ($graph->predecessors($node)) {
161 1     0   186 hlog { ('From', $pred->name, 'to', $node->name) };
  0         0  
162              
163             # Goals do not feed outputs to other Goals. This is so you can
164             # add edges between Goals to set their order while keeping the
165             # data for each Goal separate.
166             # TODO add tests for this. Also TODO decide whether this is
167             # actually the Right Thing!
168 1 50       4 next if eval { $pred->DOES('Build::Hopen::G::Goal') };
  1         8  
169              
170 1         4 my $links = $graph->get_edge_attribute($pred, $node, LINKS);
171              
172 1 50       828 unless($links) { # Simple case: predecessor's outputs become our inputs
173 0         0 $node_inputs->add(%{$pred->outputs});
  0         0  
174 0         0 next;
175             }
176              
177             # More complex case: Process all the links
178 1         5 my $hrPredOutputs = $pred->outputs;
179             # In one test, outputs was undef if not on its own line.
180 1         4 my $link_inputs = Build::Hopen::Scope::Hash->new->add(%{$hrPredOutputs});
  1         22  
181             # All links get the same outer scope --- they are parallel,
182             # not in series.
183 1         18 $link_inputs->outer($self->scope);
184             # The links run at the same scope level as the node.
185 1         36 $link_inputs->local(true);
186              
187             # Run the links in series - not parallel!
188 1         10 my $link_outputs = $link_inputs->as_hashref(-levels=>'local');
189 1         2 foreach my $link (@$links) {
190 1     0   18 hlog { ('From', $pred->name, 'via', $link->name, 'to', $node->name) };
  0         0  
191              
192 1         6 $link_outputs = $link->run(
193             -context=>$link_inputs,
194             forward_opts(\%args, {'-'=>1}, 'phase')
195             # Generator not passed to links.
196             );
197             } #foreach incoming link
198              
199 1         12 $node_inputs->add(%{$link_outputs});
  1         5  
200             # TODO specify which set these are.
201             } #foreach predecessor node
202              
203 2         235 my $step_output = $node->run(-context=>$node_inputs,
204             forward_opts(\%args, {'-'=>1}, 'phase', 'generator')
205             );
206 2         42 $node->outputs($step_output);
207              
208             # Give the Generator a chance, and stash the results if necessary.
209 2 100       4 if(eval { $node->DOES('Build::Hopen::G::Goal') }) {
  2         23  
210 1 50       4 $args{generator}->visit_goal($node) if $args{generator};
211              
212             # Save the result if there is one. Don't save {}.
213             # use $node->outputs, not $step_output, since the generator may
214             # alter $node->outputs.
215 1 50       2 $retval->{$node->name} = $node->outputs if keys %{$node->outputs};
  1         3  
216             } else {
217 1 50       9 $args{generator}->visit_node($node) if $args{generator};
218             }
219              
220             } #foreach node in topo-sort order
221              
222 1         15 return $retval;
223             } #run()
224              
225             =head1 ADDING DATA
226              
227             =head2 goal
228              
229             Creates a goal of the DAG. Goals are names for sequences of operations,
230             akin to top-level Makefile targets. Usage:
231              
232             my $goalOp = $dag->goal('name')
233              
234             Returns a passthrough operation representing the goal. Any inputs passed into
235             that operation are provided as outputs of the DAG under the corresponding name.
236              
237             The first call to C also sets L.
238              
239             =cut
240              
241             sub goal {
242 3 50   3 1 1761 my $self = shift or croak 'Need an instance';
243 3 50       11 my $name = shift or croak 'Need a goal name';
244 3         25 my $goal = Build::Hopen::G::Goal->new(name => $name);
245 3         129 $self->_graph->add_vertex($goal);
246             #$self->_node_by_name->{$name} = $goal;
247 3         513 $self->_graph->add_edge($goal, $self->_final);
248 3 100       1516 $self->default_goal($goal) unless $self->default_goal;
249 3         93 return $goal;
250             } #goal()
251              
252             =head2 connect
253              
254             - C, , , )>:
255             connects output C<< out-edge >> of operation C<< op1 >> as input C<< in-edge >> of
256             operation C<< op2 >>. No processing is done between output and input.
257             - C<< out-edge >> and C<< in-edge >> can be anything usable as a table index,
258             provided that table index appears in the corresponding operation's
259             descriptor.
260             - C, )>: creates a dependency edge from C<< op1 >> to
261             C<< op2 >>, indicating that C<< op1 >> must be run before C<< op2 >>.
262             Does not transfer any data from C<< op1 >> to C<< op2 >>.
263             - C, , )>: Connects C<< op1 >> to
264             C<< op2 >> via L C<< Link >>.
265              
266             TODO return the name of the edge? The edge instance itself? Maybe a
267             fluent interface to the DAG for chaining C calls?
268              
269             =cut
270              
271             sub connect {
272 1 50   1 1 398 my $self = shift or croak 'Need an instance';
273 1         3 my ($op1, $out_edge, $in_edge, $op2) = @_;
274              
275 1         2 my $link;
276 1 50       49 if(!defined($in_edge)) { # dependency edge
    50          
277 0         0 $op2 = $out_edge;
278 0         0 $out_edge = false; # No outputs
279 0         0 $in_edge = false; # No inputs
280             } elsif(!defined($op2)) {
281 1         3 $op2 = $in_edge;
282 1         1 $link = $out_edge;
283 1         34 $out_edge = false; # No outputs TODO
284 1         13 $in_edge = false; # No inputs TODO
285             }
286              
287             # # Create the link
288             # unless($link) {
289             # $link = Build::Hopen::G::Link->new(
290             # name => 'link_' . $op1->name . '_' . $op2->name,
291             # in => [$out_edge], # Output of op1
292             # out => [$in_edge], # Input to op2
293             # );
294             # }
295              
296 0 0   0   0 hlog { 'DAG::connect(): Edge from', $op1->name,
297             'via', $link ? $link->name : '(no link)',
298 1         9 'to', $op2->name };
299              
300             # Add it to the graph (idempotent)
301 1         27 $self->_graph->add_edge($op1, $op2);
302             #$self->_node_by_name->{$_->name} = $_ foreach ($op1, $op2);
303              
304             # Save the BHG::Link as an edge attribute (not idempotent!)
305 1   50     575 my $attrs = $self->_graph->get_edge_attribute($op1, $op2, LINKS) || [];
306 1 50       2031 push @$attrs, $link if $link;
307 1         27 $self->_graph->set_edge_attribute($op1, $op2, LINKS, $attrs);
308              
309 1         818 return undef; # TODO decide what to return
310             } #connect()
311              
312             =head2 add
313              
314             Add a regular node to the graph. An attempt to add the same node twice will be
315             ignored. Usage:
316              
317             my $node = Build::Hopen::G::Op->new(name=>"whatever");
318             $dag->add($node);
319              
320             Returns the node, for the sake of chaining.
321              
322             =cut
323              
324             sub add {
325 0 0   0 1 0 my $self = shift or croak 'Need an instance';
326 0 0       0 my $node = shift or croak 'Need a node';
327 0 0       0 return if $self->_graph->has_vertex($node);
328 0     0   0 hlog { __PACKAGE__, 'adding', Dumper($node) } 2;
  0         0  
329              
330 0         0 $self->_graph->add_vertex($node);
331             #$self->_node_by_name->{$node->name} = $node if $node->name;
332              
333 0         0 return $node;
334             } #add()
335              
336             =head2 init
337              
338             Add an initialization operation to the graph. Initialization operations run
339             before all other operations. An attempt to add the same initialization
340             operation twice will be ignored. Usage:
341              
342             my $op = Build::Hopen::G::Op->new(name=>"whatever");
343             $dag->init($op[, $first]);
344              
345             If C<$first> is truthy, the op will be run before anything already in the
346             graph. However, later calls to C with C<$first> set will push
347             operations even before C<$op>.
348              
349             Returns the node, for the sake of chaining.
350              
351             =cut
352              
353             sub init {
354 0 0   0 1 0 my $self = shift or croak 'Need an instance';
355 0 0       0 my $op = shift or croak 'Need an op';
356 0         0 my $first = shift;
357 0 0       0 return if $self->_init_graph->has_vertex($op);
358              
359 0         0 $self->_init_graph->add_vertex($op);
360             #$self->_node_by_name->{$op->name} = $op;
361              
362 0 0       0 if($first) { # $op becomes the new _init_first node
363 0         0 $self->_init_graph->add_edge($op, $self->_init_first);
364 0         0 $self->_init_first($op);
365             } else { # Not first, so can happen anytime. Add it after the
366             # current first node.
367 0         0 $self->_init_graph->add_edge($self->_init_first, $op);
368             }
369              
370 0         0 return $op;
371             } #init()
372              
373             =head1 ACCESSORS
374              
375             =head2 empty
376              
377             Returns truthy if the only nodes in the graph are internal nodes.
378             Intended for use by hopen files.
379              
380             =cut
381              
382             sub empty {
383 0 0   0 1 0 my $self = shift or croak 'Need an instance';
384 0         0 return ($self->_graph->vertices == 1);
385             # _final is the node in an empty() graph.
386             # We don't check the _init_graph since empty() is intended
387             # for use by hopen files, not toolsets.
388             } #empty()
389              
390             =head1 OTHER
391              
392             =head2 BUILD
393              
394             Initialize the instance.
395              
396             =cut
397              
398             sub BUILD {
399             #use Data::Dumper;
400             #say Dumper(\@_);
401 2 50   2 1 675 my $self = shift or croak 'Need an instance';
402 2         7 my $hrArgs = shift;
403              
404             # DAGs always have names
405 2 50       16 $self->name('__R_DAG_' . $_id_counter++) unless $self->has_custom_name;
406              
407             # Graph of normal operations
408 2         24 my $graph = Graph->new( directed => true,
409             refvertexed => true);
410 2         602 my $final = Build::Hopen::G::Node->new(
411             name => '__R_DAG_ROOT' . $_id_counter++);
412 2         257 $graph->add_vertex($final);
413 2         537 $self->_graph($graph);
414 2         48 $self->_final($final);
415              
416             # Graph of initialization operations
417 2         19 my $init_graph = Graph->new( directed => true,
418             refvertexed => true);
419 2         389 my $init = Build::Hopen::G::CollectOp->new(
420             name => '__R_DAG_INIT' . $_id_counter++);
421 2         37 $init_graph->add_vertex($init);
422              
423 2         336 $self->_init_graph($init_graph);
424 2         46 $self->_init_first($init);
425             } #BUILD()
426              
427             1;
428             # Rest of the docs {{{1
429             __END__