File Coverage

blib/lib/Async/Defer.pm
Criterion Covered Total %
statement 331 335 98.8
branch 127 130 97.6
condition 52 58 89.6
subroutine 44 44 100.0
pod 16 16 100.0
total 570 583 97.7


line stmt bran cond sub pod time code
1             package Async::Defer;
2 17     17   935829 use 5.012;
  17         155  
3 17     17   65 use warnings;
  17         24  
  17         314  
4 17     17   63 use strict;
  17         21  
  17         437  
5 17     17   70 use Carp;
  17         24  
  17         935  
6 17     17   8636 no if $] >= 5.017011, warnings => 'experimental::smartmatch';
  17         177  
  17         76  
7              
8             our $VERSION = 'v1.0.1';
9              
10 17     17   1259 use Scalar::Util qw( refaddr );
  17         34  
  17         674  
11 17     17   78 use List::Util qw( any );
  17         25  
  17         1455  
12              
13             ## no critic (ProhibitBuiltinHomonyms)
14              
15 17     17   83 use constant NOT_RUNNING=> -1;
  17         25  
  17         797  
16 17     17   96 use constant OP_CODE => 1;
  17         26  
  17         689  
17 17     17   78 use constant OP_DEFER => 2;
  17         26  
  17         625  
18 17     17   88 use constant OP_IF => 3;
  17         28  
  17         643  
19 17     17   86 use constant OP_ELSE => 4;
  17         19  
  17         637  
20 17     17   75 use constant OP_ENDIF => 5;
  17         26  
  17         584  
21 17     17   70 use constant OP_WHILE => 6;
  17         26  
  17         685  
22 17     17   70 use constant OP_ENDWHILE=> 7;
  17         23  
  17         751  
23 17     17   85 use constant OP_TRY => 8;
  17         23  
  17         705  
24 17     17   102 use constant OP_CATCH => 9;
  17         27  
  17         732  
25 17     17   76 use constant OP_FINALLY => 10;
  17         22  
  17         608  
26 17     17   71 use constant OP_ENDTRY => 11;
  17         23  
  17         45591  
27              
28             my %SELF;
29              
30              
31             sub new {
32 195     195 1 1011968 my ($class) = @_;
33 195         331 my $this = bless {}, $class;
34 195         906 $SELF{refaddr $this} = {
35             parent => undef, # parent Defer object, if any
36             opcode => [], # [[OP_CODE,$sub], [OP_TRY], ...]
37             pc => NOT_RUNNING,# point to _CURRENT_ opcode, if any
38             iter => [], # [[1,$outer_while_pc], [8,$inner_while_pc], ...]
39             findone => undef, # undef or ['continue'] or ['break'] or ['throw',$err]
40             };
41 195         406 return $this;
42             }
43              
44             sub DESTROY {
45 62     62   488652 my ($this) = @_;
46 62         443 delete $SELF{refaddr $this};
47 62         1584 return;
48             }
49              
50             sub clone {
51 7     7 1 25 my ($this) = @_;
52 7         15 my $self = $SELF{refaddr $this};
53              
54 7         15 my $clone = __PACKAGE__->new();
55 7         12 my $clone_self = $SELF{refaddr $clone};
56              
57 7         9 $clone_self->{opcode} = [ @{ $self->{opcode} } ];
  7         30  
58 7         9 %{$clone} = %{$this};
  7         11  
  7         14  
59 7         16 return $clone;
60             }
61              
62             sub iter {
63 203     203 1 511 my ($this) = @_;
64 203         331 my $self = $SELF{refaddr $this};
65              
66 203 100       209 if (!@{ $self->{iter} }) {
  203         298  
67 5         52 croak 'iter() can be used only inside while';
68             }
69              
70 198         501 return $self->{iter}[-1][0];
71             }
72              
73             sub _add {
74 706     706   1025 my ($this, $op, @params) = @_;
75 706         1146 my $self = $SELF{refaddr $this};
76              
77 706 100       1115 if ($self->{pc} != NOT_RUNNING) {
78 8         58 croak 'unable to modify while running';
79             }
80              
81 698         708 push @{ $self->{opcode} }, [ $op, @params ];
  698         1413  
82 698         1258 return $this;
83             }
84              
85             sub do {
86 297     297 1 4333 my ($this, $task, @more_tasks) = @_;
87 297 100       522 if(@more_tasks){
88 3         6 for ($task, @more_tasks) {
89 15         28 $this->do($_);
90             }
91 3         6 return $this;
92             }
93 294         433 given (ref $task) {
94 294         549 when ('CODE') {
95 198         313 return $this->_add(OP_CODE, $task);
96             }
97 96         166 when (__PACKAGE__) {
98 76         153 return $this->_add(OP_DEFER, $task);
99             }
100 20         27 when ('ARRAY') {
101 10         14 my %task = map { $_ => $task->[$_] } 0 .. $#{ $task };
  23         50  
  10         17  
102 10         35 return $this->_add(OP_CODE, _do_batch(1, %task));
103             }
104 10         13 when ('HASH') {
105 9         10 return $this->_add(OP_CODE, _do_batch(0, %{ $task }));
  9         26  
106             }
107 1         3 default {
108 1         17 croak 'require CODE/Defer object or ARRAY/HASH in first param'
109             }
110             }
111             }
112              
113             sub _do_batch {
114 19     19   40 my ($is_array, %task) = @_;
115              
116             # Isolate each task in own Defer object to guarantee they won't be
117             # surprised by shared state.
118 19         41 for my $key (keys %task) {
119 43         41 my $task;
120 43         57 given (ref $task{$key}) {
121 43         54 when ('CODE') {
122 39         56 $task = __PACKAGE__->new();
123 39         66 $task->do( $task{$key} );
124             }
125 4         6 when (__PACKAGE__) {
126 4         11 $task = $task{$key}->clone();
127             }
128 0         0 default {
129 0 0       0 my $pos = $is_array ? $key+1 : "{$key}";
130 0         0 croak 'require CODE/Defer object in param '.$pos;
131             }
132             }
133 43         71 $task{$key} = $task;
134             }
135              
136             return sub{
137 19     19   29 my ($d, @taskparams) = @_;
138             my %taskparams
139             = !$is_array ? (@taskparams)
140 19 100       41 : (map { ($_ => $taskparams[$_]) } 0 .. $#taskparams);
  14         25  
141              
142 19 100       59 if (!keys %task) {
143 2         7 return $d->done();
144             }
145              
146 17         41 my %taskresults = map { $_ => undef } keys %task;
  43         70  
147 17         55 for my $key (sort keys %task) { # sort just to simplify testing
148 43         91 my $t = __PACKAGE__->new();
149 43         80 $t->try();
150 43         116 $t->do( $task{$key} );
151             $t->catch(
152             qr/.*/ms => sub{
153 2         3 my ($t,$err) = @_; ## no critic (ProhibitReusedNames)
154 2         4 $t->{err} = $err;
155 2         4 $t->done();
156             },
157             FINALLY => sub{
158 19         26 my ($t, @result) = @_; ## no critic (ProhibitReusedNames)
159 19   100     56 $taskresults{$key} = $t->{err} // \@result;
160 19 100       74 if (!any {!defined} values %taskresults) {
  40         63  
161             my @taskresults
162             = !$is_array ? %taskresults
163 5 100       19 : map { $taskresults{$_-1} } 1 .. keys %taskresults;
  11         20  
164 5         14 $d->done(@taskresults);
165             }
166 19         51 return $t->done();
167             },
168 43         423 );
169 43 100       52 $t->run( undef, @{ $taskparams{$key} || [] } );
  43         143  
170             }
171 19         114 };
172             }
173              
174             sub if {
175 23     23 1 1413 my ($this, $code) = @_;
176 23 100 66     113 if (!$code || ref $code ne 'CODE') {
177 1         31 croak 'require CODE in first param';
178             }
179 22         53 return $this->_add(OP_IF, $code);
180             }
181              
182             sub else {
183 15     15 1 1219 my ($this) = @_;
184 15         23 return $this->_add(OP_ELSE);
185             }
186              
187             sub end_if {
188 22     22 1 651 my ($this) = @_;
189 22         36 return $this->_add(OP_ENDIF);
190             }
191              
192             sub while {
193 21     21 1 807 my ($this, $code) = @_;
194 21 100 66     106 if (!$code || ref $code ne 'CODE') {
195 1         14 croak 'require CODE in first param';
196             }
197 20         52 return $this->_add(OP_WHILE, $code);
198             }
199              
200             sub end_while {
201 19     19 1 684 my ($this) = @_;
202 19         36 return $this->_add(OP_ENDWHILE);
203             }
204              
205             sub try {
206 87     87 1 1254 my ($this) = @_;
207 87         147 return $this->_add(OP_TRY);
208             }
209              
210             sub catch {
211 91     91 1 3047 my ($this, @param) = @_;
212 91 100       305 if (2 > @param) {
    100          
213 2         19 croak 'require at least 2 params';
214             } elsif (@param % 2) {
215 1         15 croak 'require even number of params';
216             }
217              
218 88         122 my ($finally, @catch);
219 88         218 while (my ($cond, $code) = splice @param, 0, 2) {
220 146 100       226 if ($cond eq 'FINALLY') {
221 62   66     263 $finally ||= $code;
222             } else {
223 84         220 push @catch, $cond, $code;
224             }
225             }
226              
227 88 100       147 if (@catch) {
228 80         134 $this->_add(OP_CATCH, @catch);
229             }
230 87 100       154 if ($finally) {
231 61         119 $this->_add(OP_FINALLY, $finally);
232             }
233 87         161 return $this->_add(OP_ENDTRY);
234             }
235              
236             sub _check_stack {
237 244     244   334 my ($self) = @_;
238 244         264 my @stack;
239 244         616 my %op_open = (
240             OP_IF() => 'end_if()',
241             OP_WHILE() => 'end_while()',
242             OP_TRY() => 'catch()',
243             );
244 244         848 my %op_close = (
245             OP_ENDIF() => [ OP_IF, 'end_if()' ],
246             OP_ENDWHILE() => [ OP_WHILE, 'end_while()' ],
247             OP_ENDTRY() => [ OP_TRY, 'catch()' ],
248             );
249 244         319 my $extra = 0;
250 244         324 for (my $i = 0; $i < @{ $self->{opcode} }; $i++) {
  1259         2041  
251 1023         1024 my ($op) = @{ $self->{opcode}[ $i ] };
  1023         1359  
252              
253 1023 100 100     2456 if ($op == OP_CATCH || $op == OP_FINALLY) {
254 188         196 $extra++;
255             }
256              
257 1023 100       2082 if ($op_open{$op}) {
    100          
    100          
258 198         430 push @stack, [$op,0]; # second number is counter for seen OP_ELSE
259             }
260             elsif ($op_close{$op}) {
261 192         209 my ($close_op, $close_func) = @{ $op_close{$op} };
  192         283  
262 192 100 100     515 if (@stack && $stack[-1][0] == $close_op) {
263 186         331 pop @stack;
264             } else {
265 6         51 croak 'unexpected '.$close_func.' at operation '.($i+1-$extra);
266             }
267             }
268             elsif ($op == OP_ELSE) {
269 32 100 66     96 if (!(@stack && $stack[-1][0] == OP_IF)) {
    100          
270 1         9 croak 'unexpected else() at operation '.($i+1-$extra);
271             }
272             elsif ($stack[-1][1]) {
273 1         9 croak 'unexpected double else() at operation '.($i+1-$extra);
274             }
275 30         38 $stack[-1][1]++;
276             }
277             }
278 236 100       390 if (@stack) {
279 5         39 croak 'expected '.$op_open{ $stack[-1][0] }.' at end';
280             }
281 231         649 return;
282             }
283              
284             sub run {
285 247     247 1 52633 my ($this, $d, @result) = @_;
286 247         513 my $self = $SELF{refaddr $this};
287              
288 247         366 my %op_stmt = map {$_=>1} OP_CODE, OP_DEFER, OP_FINALLY;
  741         1416  
289 247 100   385   756 if (!any {$op_stmt{ $_->[0] }} @{ $self->{opcode} }) {
  385         807  
  247         643  
290 2         15 croak 'no operations to run, use do() first';
291             }
292 245 100       725 if ($self->{pc} != NOT_RUNNING) {
293 1         7 croak 'already running';
294             }
295 244         476 _check_stack($self);
296              
297 231 100       430 if(ref($d) eq 'CODE') {
298 1         9 my $callback = $d;
299 1         16 $d = __PACKAGE__->new();
300             $d->do(
301             sub {
302 1     1   3 my ($defer, @results) = @_;
303 1         2 $callback->(@results);
304 1         16 $defer->done;
305             }
306 1         6 );
307             }
308              
309 231         305 $self->{parent} = $d;
310 231         600 $this->done(@result);
311 228         687 return;
312             }
313              
314             sub _op {
315 2201     2201   2431 my ($self) = @_;
316 2201         2219 my ($op, @params) = @{ $self->{opcode}[ $self->{pc} ] };
  2201         3136  
317 2201 100       4373 return wantarray ? ($op, @params) : $op;
318             }
319              
320             sub done {
321 733     733 1 30908 my ($this, @result) = @_;
322 733         1377 my $self = $SELF{refaddr $this};
323              
324             # If OP_FINALLY was called while processing continue(), break() or throw(),
325             # and it has finished with done() - continue with continue/break/throw by
326             # calling them _again_ instead of done().
327 733 100       1233 if ($self->{findone}) {
328 23         25 my ($method, @param) = @{ $self->{findone} };
  23         29  
329 23         59 return $this->$method(@param);
330             }
331              
332 710         860 while (++$self->{pc} <= $#{ $self->{opcode} }) {
  1082         1942  
333 921         1322 my ($opcode, @param) = _op($self);
334              
335             # @result received from previous opcode will be available to next
336             # opcode only if these opcodes stay one-after-one without any
337             # other opcodes between them (like OP_IF, for example).
338             # Only exception is (no-op) OP_TRY, OP_CATCH and OP_ENDTRY.
339             # This limitation should help user to avoid subtle bugs.
340 921         1088 given ($opcode) {
341 921         1172 when (OP_CODE) {
342 361         726 return $param[0]->($this, @result);
343             }
344 560         567 when (OP_DEFER) {
345 76         209 return $param[0]->run($this, @result);
346             }
347 484         486 when (OP_FINALLY) {
348 40         94 return $param[0]->($this, @result);
349             }
350 444         874 when ([OP_TRY,OP_CATCH,OP_ENDTRY]) {
351 215         364 next;
352             }
353             }
354 229         310 @result = ();
355              
356 229         257 given ($opcode) {
357 229         249 when (OP_IF) {
358             # true - do nothing (i.e. just move to next opcode)
359             # false - skip to nearest OP_ELSE or OP_ENDIF
360 52 100       89 if (!$param[0]->( $this )) {
361 28         56 my $stack = 0;
362 28         35 while (++$self->{pc} <= $#{ $self->{opcode} }) {
  117         191  
363 117         206 my $op = _op($self);
364 117 100 100     363 $op == OP_ELSE && !$stack ? last
    100 100        
    100          
    100          
365             : $op == OP_ENDIF && !$stack ? last
366             : $op == OP_IF ? $stack++
367             : $op == OP_ENDIF ? $stack--
368             : next;
369             }
370             }
371             }
372 177         205 when (OP_ELSE) {
373             # skip this OP_ELSE branch to nearest OP_ENDIF
374 11         15 my $stack = 0;
375 11         13 while (++$self->{pc} <= $#{ $self->{opcode} }) {
  34         54  
376 34         40 my $op = _op($self);
377 34 100 100     94 $op == OP_ENDIF && !$stack ? last
    100          
    100          
378             : $op == OP_IF ? $stack++
379             : $op == OP_ENDIF ? $stack--
380             : next;
381             }
382             }
383 166         177 when (OP_WHILE) {
384             # We can "enter" OP_WHILE in two cases - for the first time,
385             # OR because of continue() called inside this OP_WHILE.
386 98 100 100     95 if (!@{$self->{iter}} || $self->{iter}[-1][1] != $self->{pc}) {
  98         314  
387 33         42 push @{ $self->{iter} }, [ 1, $self->{pc} ];
  33         70  
388             }
389             # We now already "inside" this OP_WHILE, so we can use break()
390             # to exit _this_ OP_WHILE.
391 98 100       195 if (!$param[0]->( $this )) {
392 23         89 return $this->break();
393             }
394             }
395 68         99 when (OP_ENDWHILE) {
396             # We now still "inside" current OP_WHILE, so we can use continue()
397             # to repeat _this_ OP_WHILE.
398 49         163 return $this->continue();
399             }
400             }
401             }
402              
403 161         248 $self->{pc} = NOT_RUNNING;
404 161 100       547 if ($self->{parent}) {
405 53         246 return $self->{parent}->done(@result);
406             }
407              
408             # If we're here, done() was called by last opcode, and this is
409             # top-level Defer object, nothing more to do - STOP.
410             }
411              
412             # Before executing continue/break logic we have to find and execute all
413             # OP_FINALLY for all already open OP_TRY blocks within this OP_WHILE.
414             # So, this helper skip opcodes inside this OP_WHILE until it found
415             # either OP_FINALLY or OP_ENDWHILE or last opcode.
416             sub _skip_while {
417 132     132   161 my ($self) = @_;
418              
419             # 1. continue() can be called exactly on OP_ENDWHILE (by done())
420             # 2. continue/break can be called by last opcode
421             # In both cases we shouldn't do anything (including moving {pc} forward).
422 132 100 66     161 if (_op($self) == OP_ENDWHILE || $self->{pc} == $#{ $self->{opcode} }) {
  83         215  
423 49         95 return;
424             }
425              
426 83         107 my $stack = 0;
427 83         93 my $trystack = 0;
428 83         97 while (++$self->{pc} < $#{ $self->{opcode} }) {
  333         540  
429 294         348 my $op = _op($self);
430 294 100 100     1046 $op == OP_ENDWHILE && !$stack ? last
    100 100        
    100 100        
    100          
    100          
    100          
431             : $op == OP_WHILE ? $stack++
432             : $op == OP_ENDWHILE ? $stack--
433             : $op == OP_TRY ? $trystack++
434             : $op == OP_ENDTRY && $trystack ? $trystack--
435             : $op == OP_FINALLY && !$trystack ? last
436             : next;
437             }
438              
439 83         121 return;
440             }
441              
442             sub continue {
443 88     88 1 206 my ($this) = @_;
444 88         153 my $self = $SELF{refaddr $this};
445              
446             # Any next call to continue/break/throw cancels current continue/break/throw (if any).
447 88         118 $self->{findone} = undef;
448              
449 88         161 _skip_while($self);
450 88         114 my ($op, @param) = _op($self);
451 88 100       163 if ($op == OP_FINALLY) {
452             # If OP_FINALLY ends with done() - call continue() again instead.
453 21         30 $self->{findone} = ['continue'];
454 21         36 return $param[0]->($this);
455             }
456              
457             # We now at OP_ENDWHILE, rewind to corresponding OP_WHILE.
458 67         78 my $stack = 0;
459 67         116 while (--$self->{pc} > 0) {
460 444         498 $op = _op($self);
461 444 100 100     1101 $op == OP_WHILE && !$stack ? last
    100          
    100          
462             : $op == OP_ENDWHILE ? $stack++
463             : $op == OP_WHILE ? $stack--
464             : next;
465             }
466              
467             # If continue was called outside OP_WHILE there is no iteration number.
468 67 100       71 if (@{ $self->{iter} }) {
  67         106  
469 65         84 $self->{iter}[-1][0]++;
470             }
471              
472             # Step one opcode back because done() will move one opcode forward
473             # and so process this OP_WHILE.
474 67         83 --$self->{pc};
475 67         212 return $this->done();
476             }
477              
478             sub break {
479 44     44 1 111 my ($this) = @_;
480 44         82 my $self = $SELF{refaddr $this};
481              
482             # Any next call to continue/break/throw cancels current continue/break/throw (if any).
483 44         63 $self->{findone} = undef;
484              
485 44         75 _skip_while($self);
486 44         68 my ($op, @param) = _op($self);
487 44 100       76 if ($op == OP_FINALLY) {
488             # If OP_FINALLY ends with done() - call break() again instead.
489 13         23 $self->{findone} = ['break'];
490 13         23 return $param[0]->($this);
491             }
492              
493             # We now at OP_ENDWHILE.
494 31         49 pop @{ $self->{iter} };
  31         40  
495 31         104 return $this->done();
496             }
497              
498             sub throw {
499 58     58 1 236 my ($this, $err) = @_;
500 58         118 my $self = $SELF{refaddr $this};
501 58   100     124 $err //= q{};
502              
503             # Any next call to continue/break/throw cancels current continue/break/throw (if any).
504 58         77 $self->{findone} = undef;
505              
506             # If throw() was called by break opcode in this OP_TRY (either OP_FINALLY,
507             # or OP_CATCH if there no OP_FINALLY in this OP_TRY), then we should look
508             # for handler in outer OP_TRY, not in this one.
509             # So we set $stack=1 to skip over current OP_TRY's OP_ENDTRY.
510 58 100       62 my ($nextop) = @{ $self->{opcode}[ $self->{pc} + 1 ] || [] };
  58         151  
511 58 100 100     175 my $stack = $nextop && $nextop == OP_ENDTRY ? 1 : 0;
512             # Skip until OP_CATCH or OP_FINALLY in current OP_TRY block.
513             # If while skipping we exit some OP_WHILE(s) - pop their iterators.
514 58         74 while (++$self->{pc} <= $#{ $self->{opcode} }) {
  81         145  
515 75         107 my $op = _op($self);
516             $op == OP_CATCH && !$stack ? last
517             : $op == OP_FINALLY && !$stack ? last
518             : $op == OP_TRY ? $stack++
519             : $op == OP_ENDTRY ? $stack--
520 0         0 : $op == OP_WHILE ? push @{ $self->{iter} }, [ 1, $self->{pc} ]
521 75 100 100     276 : $op == OP_ENDWHILE ? pop @{ $self->{iter} }
  3 50 66     6  
    100          
    100          
    100          
    100          
522             : next;
523             }
524              
525 58 100       78 if ($self->{pc} > $#{ $self->{opcode} }) {
  58         124  
526 6 100       13 if ($self->{parent}) {
527 4         38 return $self->{parent}->throw($err);
528             } else {
529 2         41 croak 'uncatched exception in Defer: '.$err;
530             }
531             }
532              
533 52         80 my ($op, @param) = _op($self);
534 52 100       96 if ($op == OP_CATCH) {
535 44         130 while (my ($cond, $code) = splice @param, 0, 2) {
536 49 100       243 if ($err =~ /$cond/ms) {
537 39         105 return $code->($this, $err);
538             }
539             }
540             # Re-throw exception if no one regex in this OP_CATCH match it.
541 5         15 return $this->throw($err);
542             }
543             else { # OP_FINALLY
544             # If OP_FINALLY ends with done() - call throw($err) again instead.
545 8         13 $self->{findone} = ['throw', $err];
546 8         16 return $param[0]->($this, $err);
547             }
548             }
549              
550              
551             1; # Magic true value required at end of module
552             __END__