File Coverage

blib/lib/Promise/ES6/Backend/PP.pm
Criterion Covered Total %
statement 158 163 96.9
branch 55 66 83.3
condition 15 27 55.5
subroutine 28 30 93.3
pod 0 11 0.0
total 256 297 86.2


line stmt bran cond sub pod time code
1             package Promise::ES6;
2              
3             #----------------------------------------------------------------------
4             # This module iS NOT a defined interface. Nothing to see here …
5             #----------------------------------------------------------------------
6              
7 42     42   315 use strict;
  42         81  
  42         1243  
8 42     42   232 use warnings;
  42         75  
  42         945  
9              
10 42     42   205 use Carp ();
  42         91  
  42         1656  
11              
12             use constant {
13              
14             # These aren’t actually defined.
15 42         8187 _RESOLUTION_CLASS => 'Promise::ES6::_RESOLUTION',
16             _REJECTION_CLASS => 'Promise::ES6::_REJECTION',
17             _PENDING_CLASS => 'Promise::ES6::_PENDING',
18              
19             _DEBUG => 0,
20 42     42   278 };
  42         139  
21              
22             use constant {
23 42         87898 _PROMISE_ID_IDX => 0,
24             _PID_IDX => _DEBUG + 0,
25             _CHILDREN_IDX => _DEBUG + 1,
26             _VALUE_SR_IDX => _DEBUG + 2,
27             _DETECT_LEAK_IDX => _DEBUG + 3,
28             _ON_RESOLVE_IDX => _DEBUG + 4,
29             _ON_REJECT_IDX => _DEBUG + 5,
30             _IS_FINALLY_IDX => _DEBUG + 6,
31              
32             # For async/await:
33             _ON_READY_IMMEDIATE_IDX => _DEBUG + 7,
34             _SELF_REF_IDX => _DEBUG + 8,
35 42     42   325 };
  42         86  
36              
37             # "$value_sr" => $value_sr
38             our %_UNHANDLED_REJECTIONS;
39              
40             my $_debug_promise_id = 0;
41 0     0   0 sub _create_promise_id { return $_debug_promise_id++ . "-$_[0]" }
42              
43             sub new {
44 139     139 0 91529 my ( $class, $cr ) = @_;
45              
46 139 50       406 die 'Need callback!' if !$cr;
47              
48 139         228 my $value;
49 139         505 my $value_sr = bless \$value, _PENDING_CLASS();
50              
51 139         255 my @children;
52              
53 139         612 my $self = bless [
54             ( _DEBUG ? undef : () ),
55             $$,
56             \@children,
57             $value_sr,
58             $Promise::ES6::DETECT_MEMORY_LEAKS,
59             ], $class;
60              
61 139         250 $self->[_PROMISE_ID_IDX] = _create_promise_id($self) if _DEBUG;
62              
63             # NB: These MUST NOT refer to $self, or else we can get memory leaks
64             # depending on how $resolver and $rejector are used.
65             my $resolver = sub {
66 59     59   1086546 $$value_sr = $_[0];
67              
68             # NB: UNIVERSAL::can() is used in order to avoid an eval {}.
69             # It is acknowledged that many Perl experts strongly discourage
70             # use of this technique.
71 59 100       787 if ( UNIVERSAL::can( $$value_sr, 'then' ) ) {
72 2         8 return _repromise( $value_sr, \@children, $value_sr );
73             }
74              
75 57         603 bless $value_sr, _RESOLUTION_CLASS();
76              
77 57 50       288 $self->[_ON_READY_IMMEDIATE_IDX]->() if $self->[_ON_READY_IMMEDIATE_IDX];
78              
79 57         227 undef $self->[_SELF_REF_IDX];
80              
81 57 100       245 if (@children) {
82 17         378 $_->_settle($value_sr) for splice @children;
83             }
84 139         688 };
85              
86             my $rejecter = sub {
87 44 100   44   113773 if (!defined $_[0]) {
88 4         6 my $msg;
89              
90 4 100       12 if (@_) {
91 3         7 $msg = "$class: Uninitialized rejection value given";
92             }
93             else {
94 1         3 $msg = "$class: No rejection value given";
95             }
96              
97 4         20 require Carp;
98 4         521 Carp::carp($msg);
99             }
100              
101 44         148 $$value_sr = $_[0];
102 44         279 bless $value_sr, _REJECTION_CLASS();
103              
104 44         166 $_UNHANDLED_REJECTIONS{$value_sr} = $value_sr;
105              
106 44 50       175 $self->[_ON_READY_IMMEDIATE_IDX]->() if $self->[_ON_READY_IMMEDIATE_IDX];
107              
108 44         122 undef $self->[_SELF_REF_IDX];
109              
110             # We do not repromise rejections. Whatever is in $$value_sr
111             # is literally what rejection callbacks receive.
112 44 100       195 if (@children) {
113 8         109 $_->_settle($value_sr) for splice @children;
114             }
115 139         752 };
116              
117 139         286 local $@;
118 139 100       283 if ( !eval { $cr->( $resolver, $rejecter ); 1 } ) {
  139         443  
  130         697  
119 9         99 $$value_sr = $@;
120 9         25 bless $value_sr, _REJECTION_CLASS();
121              
122 9         31 $_UNHANDLED_REJECTIONS{$value_sr} = $value_sr;
123             }
124              
125 139         1867 return $self;
126             }
127              
128             sub then {
129 130     130 0 19326 return $_[0]->_then_or_finally(@_[1, 2]);
130             }
131              
132             sub finally {
133              
134             # There’s no reason to call finally() without a callback
135             # since it would just be a no-op.
136 13 50   13 0 78 die 'finally() requires a callback!' if !$_[1];
137              
138 13         36 return $_[0]->_then_or_finally($_[1], undef, 1);
139             }
140              
141             sub _then_or_finally {
142 143     143   364 my ($self, $on_resolve_or_finish, $on_reject, $is_finally) = @_;
143              
144 143         259 my $value_sr = bless( \do { my $v }, _PENDING_CLASS() );
  143         443  
145              
146 143         711 my $new = bless [
147             ( _DEBUG ? undef : () ),
148             $$,
149             [],
150             $value_sr,
151             $Promise::ES6::DETECT_MEMORY_LEAKS,
152             $on_resolve_or_finish,
153             $on_reject,
154             $is_finally,
155             ],
156             ref($self);
157              
158 143         275 $new->[_PROMISE_ID_IDX] = _create_promise_id($new) if _DEBUG;
159              
160 143 100       577 if ( _PENDING_CLASS eq ref $self->[_VALUE_SR_IDX] ) {
161 46         114 push @{ $self->[_CHILDREN_IDX] }, $new;
  46         181  
162             }
163             else {
164              
165             # $self might already be settled, in which case we immediately
166             # settle the $new promise as well.
167              
168 97         228 $new->_settle( $self->[_VALUE_SR_IDX] );
169             }
170              
171 143         567 return $new;
172             }
173              
174             sub _repromise {
175 10     10   33 my ( $value_sr, $children_ar, $repromise_value_sr, $orig_finally_sr ) = @_;
176             $$repromise_value_sr->then(
177             sub {
178 6 100   6   217 if (ref $orig_finally_sr) {
179 1         2 $$value_sr = $$orig_finally_sr;
180             }
181             else {
182 5         24 $$value_sr = $_[0];
183             }
184              
185 6         20 bless $value_sr, _RESOLUTION_CLASS;
186 6         81 $_->_settle($value_sr) for splice @$children_ar;
187             },
188             sub {
189 4     4   17 $$value_sr = $_[0];
190 4         9 bless $value_sr, _REJECTION_CLASS;
191 4         11 $_UNHANDLED_REJECTIONS{$value_sr} = $value_sr;
192 4         18 $_->_settle($value_sr) for splice @$children_ar;
193             },
194 10         180 );
195 10         94 return;
196              
197             }
198              
199             # It’s gainfully faster to inline this:
200             #sub _is_completed {
201             # return (_PENDING_CLASS ne ref $_[0][ _VALUE_SR_IDX ]);
202             #}
203              
204             # This method *only* runs to “settle” a promise.
205             sub _settle {
206 133     133   378 my ( $self, $final_value_sr ) = @_;
207              
208 133 50       782 die "$self already settled!" if _PENDING_CLASS ne ref $self->[_VALUE_SR_IDX];
209              
210 133         390 my $settle_is_rejection = _REJECTION_CLASS eq ref $final_value_sr;
211              
212             # This has to happen up-front or else we can get spurious
213             # unhandled-rejection warnings in asynchronous mode.
214 133 100       420 delete $_UNHANDLED_REJECTIONS{$final_value_sr} if $settle_is_rejection;
215              
216 133 50       334 if ($Promise::ES6::_EVENT) {
217             _postpone( sub {
218 0     0   0 $self->_settle_now($final_value_sr, $settle_is_rejection);
219 0         0 } );
220             }
221             else {
222 133         726 $self->_settle_now($final_value_sr, $settle_is_rejection);
223             }
224             }
225              
226             sub _settle_now {
227 139     139   389 my ( $self, $final_value_sr, $settle_is_rejection ) = @_;
228              
229 139         337 my $self_is_finally = $self->[_IS_FINALLY_IDX];
230              
231             # A promise that new() created won’t have on-settle callbacks,
232             # but a promise that came from then/catch/finally will.
233             # It’s a good idea to delete the callbacks in order to trigger garbage
234             # collection as soon and as reliably as possible. It’s safe to do so
235             # because _settle() is only called once.
236 139 100 100     630 my $callback = $self->[ ($settle_is_rejection && !$self_is_finally) ? _ON_REJECT_IDX : _ON_RESOLVE_IDX ];
237              
238 139         289 @{$self}[ _ON_RESOLVE_IDX, _ON_REJECT_IDX ] = ();
  139         461  
239              
240             # In some contexts this function runs quite a lot,
241             # so caching the is-promise lookup is useful.
242 139         259 my $value_sr_contents_is_promise = 1;
243              
244 139 100       339 if ($callback) {
245              
246             # This is the block that runs for promises that were created by a
247             # call to then() that assigned a handler for the state that
248             # $final_value_sr indicates (i.e., resolved or rejected).
249              
250 130         245 my ($new_value, $callback_failed);
251              
252 130         232 local $@;
253              
254 130 100       294 if ( eval { $new_value = $callback->($self_is_finally ? () : $$final_value_sr); 1 } ) {
  130 100       507  
  127         9490  
255              
256             # The callback succeeded. If $new_value is not itself a promise,
257             # then $self is now resolved. (Yay!) Note that this is true
258             # even if $final_value_sr indicates a rejection: in this case, we’ve
259             # just run a successful “catch” block, so resolution is correct.
260              
261             # If $new_value IS a promise, though, then we have to wait.
262 127 100       603 if ( !UNIVERSAL::can( $new_value, 'then' ) ) {
263 119         217 $value_sr_contents_is_promise = 0;
264              
265 119 100       284 if ($self_is_finally) {
266              
267             # finally() is a bit weird. Assuming its callback succeeds,
268             # it takes its parent’s resolution state. It’s important
269             # that we make a *new* reference to the resolution value,
270             # though, rather than merely using $final_value_sr itself,
271             # because we need $self to have its own entry in
272             # %_UNHANDLED_REJECTIONS.
273 8         12 ${ $self->[_VALUE_SR_IDX] } = $$final_value_sr;
  8         15  
274 8         16 bless $self->[_VALUE_SR_IDX], ref $final_value_sr;
275              
276 8 100       27 $_UNHANDLED_REJECTIONS{ $self->[_VALUE_SR_IDX] } = $self->[_VALUE_SR_IDX] if $settle_is_rejection;
277             }
278             else {
279 111         344 bless $self->[_VALUE_SR_IDX], _RESOLUTION_CLASS;
280             }
281             }
282             }
283             else {
284 3         36 $callback_failed = 1;
285              
286             # The callback errored, which means $self is now rejected.
287              
288 3         7 $new_value = $@;
289 3         4 $value_sr_contents_is_promise = 0;
290              
291 3         7 bless $self->[_VALUE_SR_IDX], _REJECTION_CLASS();
292 3         10 $_UNHANDLED_REJECTIONS{ $self->[_VALUE_SR_IDX] } = $self->[_VALUE_SR_IDX];
293             }
294              
295 130 100 100     463 if (!$self_is_finally || $value_sr_contents_is_promise || ($self_is_finally && $callback_failed)) {
      66        
      100        
296 122         200 ${ $self->[_VALUE_SR_IDX] } = $new_value;
  122         353  
297             }
298             }
299             else {
300              
301             # There was no handler from then(), so whatever state $final_value_sr
302             # indicates # (i.e., resolution or rejection) is now $self’s state
303             # as well.
304              
305             # NB: We should NEVER be here if the promise is from finally().
306              
307 9         24 bless $self->[_VALUE_SR_IDX], ref($final_value_sr);
308 9         18 ${ $self->[_VALUE_SR_IDX] } = $$final_value_sr;
  9         20  
309 9         71 $value_sr_contents_is_promise = UNIVERSAL::can( $$final_value_sr, 'then' );
310              
311 9 100       28 if ($settle_is_rejection) {
312 4         20 $_UNHANDLED_REJECTIONS{ $self->[_VALUE_SR_IDX] } = $self->[_VALUE_SR_IDX];
313             }
314             }
315              
316 139 100       367 if ($value_sr_contents_is_promise) {
    100          
317              
318             # Stash the given concrete value. If the $value_sr promise
319             # rejects, then we’ll accept that, but if it resolves, then
320             # we’ll look at this to know to discard that resolution.
321 8 100       59 if ($self_is_finally) {
322 3         8 $self->[_IS_FINALLY_IDX] = $final_value_sr;
323             }
324              
325 8         24 return _repromise( @{$self}[ _VALUE_SR_IDX, _CHILDREN_IDX, _VALUE_SR_IDX, _IS_FINALLY_IDX ] );
  8         55  
326             }
327 131         385 elsif ( @{ $self->[_CHILDREN_IDX] } ) {
328 2         21 $_->_settle( $self->[_VALUE_SR_IDX] ) for splice @{ $self->[_CHILDREN_IDX] };
  2         72  
329             }
330              
331 131 100       347 $self->[_ON_READY_IMMEDIATE_IDX]->() if $self->[_ON_READY_IMMEDIATE_IDX];
332              
333 131         290 undef $self->[_SELF_REF_IDX];
334              
335 131         547 return;
336             }
337              
338             sub DESTROY {
339              
340             # The PID should always be there, but this accommodates mocks.
341 10 50 33 10   5087 return unless $_[0][_PID_IDX] && $$ == $_[0][_PID_IDX];
342              
343 10 0 33     32 if ( $_[0][_DETECT_LEAK_IDX] && ${^GLOBAL_PHASE} && ${^GLOBAL_PHASE} eq 'DESTRUCT' ) {
      0        
344 0         0 warn( ( '=' x 70 ) . "\n" . 'XXXXXX - ' . ref( $_[0] ) . " survived until global destruction; memory leak likely!\n" . ( "=" x 70 ) . "\n" );
345             }
346              
347 10 50       32 if ( defined $_[0][_VALUE_SR_IDX] ) {
348 10         18 my $promise_value_sr = $_[0][_VALUE_SR_IDX];
349 10 50       75 if ( my $value_sr = delete $_UNHANDLED_REJECTIONS{$promise_value_sr} ) {
350 0           warn "$_[0]: Unhandled rejection: $$value_sr";
351             }
352             }
353             }
354              
355             #----------------------------------------------------------------------
356              
357             # Future::AsyncAwait::Awaitable interface:
358              
359             # Future::AsyncAwait doesn’t retain a strong reference to its created
360             # promises, as a result of which we need to create a self-reference
361             # inside the promise. We’ll clear that self-reference once the promise
362             # is finished, which avoids memory leaks.
363             #
364             sub _immortalize {
365 8     8   17 my $method = $_[0];
366              
367 8         54 my $new = $_[1]->$method(@_[2 .. $#_]);
368              
369 8         67 $new->[_SELF_REF_IDX] = $new;
370             }
371              
372             sub AWAIT_NEW_DONE {
373 2   33 2 0 2856 _immortalize('resolve', (ref($_[0]) || $_[0]), $_[1]);
374             }
375              
376             sub AWAIT_NEW_FAIL {
377 2   33 2 0 5438 _immortalize('reject', (ref($_[0]) || $_[0]), $_[1]);
378             }
379              
380             sub AWAIT_CLONE {
381 4     4 0 4033 _immortalize('new', ref($_[0]), \&_noop);
382             }
383              
384             sub AWAIT_DONE {
385 4     4 0 547 my $copy = $_[1];
386              
387 4         17 $_[0]->_settle_now(bless \$copy, _RESOLUTION_CLASS);
388             }
389              
390             sub AWAIT_FAIL {
391 2     2 0 6 my $copy = $_[1];
392              
393 2         10 $_[0]->_settle_now(bless(\$copy, _REJECTION_CLASS), 1);
394             }
395              
396             sub AWAIT_IS_READY {
397 12     12 0 89 !UNIVERSAL::isa( $_[0]->[_VALUE_SR_IDX], _PENDING_CLASS );
398             }
399              
400 42     42   385 use constant AWAIT_IS_CANCELLED => 0;
  42         111  
  42         6562  
401              
402             sub AWAIT_GET {
403 12     12 0 40 delete $_UNHANDLED_REJECTIONS{$_[0]->[_VALUE_SR_IDX]};
404              
405 12 100       52 return ${ $_[0]->[_VALUE_SR_IDX] } if UNIVERSAL::isa( $_[0]->[_VALUE_SR_IDX], _RESOLUTION_CLASS );
  8         43  
406              
407 4         8 Carp::croak ${ $_[0]->[_VALUE_SR_IDX] };
  4         504  
408             }
409              
410 42     42   331 use constant _noop => ();
  42         113  
  42         4958  
411              
412             sub AWAIT_ON_READY {
413 2     2 0 30 $_[0][_ON_READY_IMMEDIATE_IDX] = $_[1];
414             }
415              
416             *AWAIT_CHAIN_CANCEL = *_noop;
417             *AWAIT_ON_CANCEL = *_noop;
418              
419             1;