File Coverage

blib/lib/Acme/Chef/Recipe.pm
Criterion Covered Total %
statement 158 163 96.9
branch 48 66 72.7
condition 3 6 50.0
subroutine 15 15 100.0
pod 9 9 100.0
total 233 259 89.9


line stmt bran cond sub pod time code
1              
2             package Acme::Chef::Recipe;
3              
4 10     10   50 use strict;
  10         14  
  10         306  
5 10     10   44 use warnings;
  10         14  
  10         255  
6              
7 10     10   39 use Carp;
  10         17  
  10         580  
8              
9 10     10   3612 use Acme::Chef::Ingredient;
  10         23  
  10         325  
10 10     10   3986 use Acme::Chef::Container;
  10         24  
  10         471  
11              
12             =head1 NAME
13              
14             Acme::Chef::Recipe - Internal module used by Acme::Chef
15              
16             =head1 SYNOPSIS
17              
18             use Acme::Chef;
19              
20             =head1 DESCRIPTION
21              
22             Please see L;
23              
24             =head2 METHODS
25              
26             This is list of methods in this package.
27              
28             =over 2
29              
30             =cut
31              
32 10     10   63 use vars qw/$VERSION %Grammars @GrammarOrder %Commands/;
  10         14  
  10         58883  
33             $VERSION = '1.01';
34              
35             @GrammarOrder = qw(
36             take_from add_dry put fold add remove combine divide
37             liquify_contents liquify stir_time stir_ingredient
38             mix clean pour refrigerate set_aside serve_with
39             until_verbed verb
40             );
41              
42             { # scope of grammar definition
43              
44             my $ord = qr/([1-9]\d*)(?:st|nd|rd|th)/;
45             my $ord_noncap = qr/[1-9]\d*(?:st|nd|rd|th)/;
46             my $ingr_noncap = qr/[\-\w][\- \w]*/;
47             my $ingr = qr/($ingr_noncap)/;
48             my $verb = qr/([\-\w]+)/;
49              
50             %Grammars = (
51              
52             put => sub {
53             my $recipe = shift;
54             local $_ = shift;
55             my $regex;
56             if (/ into (?:the )?(?:$ord )?mixing bowl$/) {
57             $regex = qr/^Put (?:the )?$ingr into (?:the )?(?:$ord )?mixing bowl$/;
58             } else {
59             $regex = qr/^Put (?:the )?$ingr$/;
60             }
61             /$regex/ or return();
62             $recipe->require_bowl($2||1);
63             $recipe->require_ingredient($1, 'put');
64             return 'put', $1, ($2||1);
65             },
66              
67             take_from => sub {
68             my $recipe = shift;
69             local $_ = shift;
70             /^Take $ingr from refrigerator$/ or return();
71             $recipe->require_ingredient($1);
72             return 'take_from', $1;
73             },
74              
75             fold => sub {
76             my $recipe = shift;
77             local $_ = shift;
78             /^Fold (?:the )?$ingr into (?:the )?(?:$ord )?mixing bowl$/ or return();
79             $recipe->require_bowl($2||1);
80             $recipe->require_ingredient($1, 'fold');
81             return 'fold', $1, ($2||1);
82             },
83              
84             add => sub {
85             my $recipe = shift;
86             local $_ = shift;
87             my $regex;
88             if (/ to (?:the )?(?:$ord )?mixing bowl$/) {
89             $regex = qr/^Add (?:the )?$ingr to (?:the )?(?:$ord )?mixing bowl$/;
90             } else {
91             $regex = qr/^Add (?:the )?$ingr()$/;
92             }
93             /$regex/ or return();
94             $recipe->require_bowl($2||1);
95             $recipe->require_ingredient($1, 'add');
96             return 'add', $1, ($2||1);
97             },
98              
99             remove => sub {
100             my $recipe = shift;
101             local $_ = shift;
102             my $regex;
103             if (/ from (?:the )?(?:$ord )?mixing bowl$/) {
104             $regex = qr/^Remove (?:the )?$ingr from (?:the )?(?:$ord )?mixing bowl$/;
105             } else {
106             $regex = qr/^Remove (?:the )?$ingr()$/;
107             }
108             /$regex/ or return();
109             $recipe->require_bowl($2||1);
110             $recipe->require_ingredient($1, 'remove');
111             return 'remove', $1, ($2||1);
112             },
113              
114             combine => sub {
115             my $recipe = shift;
116             local $_ = shift;
117             my $regex;
118             if (/ into (?:the )?(?:$ord )?mixing bowl$/) {
119             $regex = qr/^Combine (?:the )?$ingr into (?:the )?(?:$ord )?mixing bowl$/;
120             } else {
121             $regex = qr/^Combine (?:the )?$ingr()$/;
122             }
123             /$regex/ or return();
124             $recipe->require_bowl($2||1);
125             $recipe->require_ingredient($1, 'combine');
126             return 'combine', $1, ($2||1);
127             },
128              
129             divide => sub {
130             my $recipe = shift;
131             local $_ = shift;
132             my $regex;
133             if (/ into (?:the )?(?:$ord )?mixing bowl$/) {
134             $regex = qr/^Divide (?:the )?$ingr into (?:the )?(?:$ord )?mixing bowl$/;
135             } else {
136             $regex = qr/^Divide(?: the)?$ingr()$/;
137             }
138             /$regex/ or return();
139             $recipe->require_bowl($2||1);
140             $recipe->require_ingredient($1, 'divide');
141             return 'divide', $1, ($2||1);
142             },
143              
144             add_dry => sub {
145             my $recipe = shift;
146             local $_ = shift;
147             /^Add (?:the )?dry ingredients(?: to (?:the )?(?:$ord )?mixing bowl)?$/ or return();
148             $recipe->require_bowl($1||1);
149             return 'add_dry', ($1||1);
150             },
151              
152             liquify_contents => sub {
153             my $recipe = shift;
154             local $_ = shift;
155             /^Liqu(?:i|e)fy (?:the )?contents of (?:the )?(?:$ord )?mixing bowl$/ or return();
156             $recipe->require_bowl($1||1);
157             return 'liquify_contents', ($1||1);
158             },
159              
160             liquify => sub {
161             my $recipe = shift;
162             local $_ = shift;
163             /^Liqu(?:i|e)fy (?:the )?$ingr$/ or return();
164             $recipe->require_ingredient($1, 'liquify');
165             return 'liquify', $1;
166             },
167              
168             stir_time => sub {
169             my $recipe = shift;
170             local $_ = shift;
171             /^Stir (?:(?:the )?(?:$ord )?mixing bowl )?for (\d+) minutes?$/ or return();
172             $recipe->require_bowl($1||1);
173             return 'stir_time', $2, ($1||1);
174             },
175              
176             stir_ingredient => sub {
177             my $recipe = shift;
178             local $_ = shift;
179             /^Stir $ingr into (?:the )?(?:$ord )?mixing bowl$/ or return();
180             $recipe->require_bowl($2||1);
181             $recipe->require_ingredient($1, 'stir_ingredient');
182             return 'stir_ingredient', $1, ($2||1);
183             },
184              
185             mix => sub {
186             my $recipe = shift;
187             local $_ = shift;
188             /^Mix (?:the (?:$ord )?mixing bowl )well$/ or return();
189             $recipe->require_bowl($1||1);
190             return 'mix', ($1||1);
191             },
192              
193             clean => sub {
194             my $recipe = shift;
195             local $_ = shift;
196             /^Clean (?:the )?(?:$ord )?mixing bowl$/ or return();
197             $recipe->require_bowl($1||1);
198             return 'clean', ($1||1);
199             },
200              
201             pour => sub {
202             my $recipe = shift;
203             local $_ = shift;
204             /^Pour contents of (?:the )?((?:[1-9]\d*(?:st|nd|rd|th) )?)mixing bowl into (?:the )?((?:[1-9]\d*(?:st|nd|rd|th) )?)baking dish$/ or return();
205             my $m = $1 || 1;
206             my $b = $2 || 1;
207             $m =~ s/\D//g;
208             $b =~ s/\D//g;
209             $recipe->require_bowl($m);
210             $recipe->require_dish($b);
211             return 'pour', $m, $b;
212             },
213              
214             refrigerate => sub {
215             my $recipe = shift;
216             local $_ = shift;
217             /^Refrigerate(?: for (\d+) hours?)?$/ or return();
218             return 'refrigerate', (defined $1 ? $1 : 0);
219             },
220              
221             set_aside => sub {
222             my $recipe = shift;
223             local $_ = shift;
224             /^Set aside$/ or return();
225             return 'set_aside';
226             },
227              
228             serve_with => sub {
229             my $recipe = shift;
230             local $_ = shift;
231             /^Serve with $ingr$/ or return();
232             # $ingr is a recipe name here
233             return 'serve_with', lc($1);
234             },
235              
236             verb => sub {
237             my $recipe = shift;
238             local $_ = shift;
239             /^$verb (?:the )?$ingr$/ or return();
240             $recipe->require_ingredient($2, 'verb');
241             return 'verb', lc($1), $2;
242             },
243              
244             until_verbed => sub {
245             my $recipe = shift;
246             local $_ = shift;
247             /^$verb ((?:(?:the )?$ingr_noncap )?)until ${verb}ed$/ or return();
248             my $ing = (defined $2 ? $2 : '');
249             my $verbed = $3;
250             $verbed .= 'e' if not exists $recipe->{loops}{$verbed};
251             $ing =~ s/^the //;
252             $ing =~ s/ $//;
253             $recipe->require_ingredient($ing, 'until_verbed') if $ing ne '';
254             return 'until_verbed', $verbed, $ing;
255             },
256              
257             );
258              
259             }
260              
261             %Commands = (
262             put => sub {
263             my $recipe = shift;
264             my $data = shift;
265             $recipe -> {bowls} -> [$data -> [2] - 1]
266             ->
267             put( $recipe -> {ingredients} -> {$data -> [1]} );
268             return 1;
269             },
270            
271             take_from => sub {
272             my $recipe = shift;
273             my $data = shift;
274             local $/ = "\n";
275             my $value;
276             while (1) {
277             $value = ;
278             last if $value =~ /^\s*\.?\d+/;
279             }
280             $recipe -> {ingredients} -> {$data -> [1]}
281             -> value($value+0);
282             },
283              
284             fold => sub {
285             my $recipe = shift;
286             my $data = shift;
287             $recipe -> {bowls} -> [$data -> [2] - 1]
288             ->
289             fold( $recipe -> {ingredients} -> {$data -> [1]} );
290             return 1;
291             },
292              
293             add => sub {
294             my $recipe = shift;
295             my $data = shift;
296             $recipe -> {bowls} -> [$data -> [2] - 1]
297             ->
298             add( $recipe -> {ingredients} -> {$data -> [1]} );
299             return 1;
300             },
301              
302             remove => sub {
303             my $recipe = shift;
304             my $data = shift;
305             $recipe -> {bowls} -> [$data -> [2] - 1]
306             ->
307             remove( $recipe -> {ingredients} -> {$data -> [1]} );
308             return 1;
309             },
310              
311             combine => sub {
312             my $recipe = shift;
313             my $data = shift;
314             $recipe -> {bowls} -> [$data -> [2] - 1]
315             ->
316             combine( $recipe -> {ingredients} -> {$data -> [1]} );
317             return 1;
318             },
319              
320             divide => sub {
321             my $recipe = shift;
322             my $data = shift;
323             $recipe -> {bowls} -> [$data -> [2] - 1]
324             ->
325             divide( $recipe -> {ingredients} -> {$data -> [1]} );
326             return 1;
327             },
328              
329             add_dry => sub {
330             my $recipe = shift;
331             my $data = shift;
332             $recipe -> {bowls} -> [$data -> [1] - 1]
333             ->
334             put_sum(
335             grep { $_->type() eq 'dry' }
336             values %{ $recipe -> {ingredients} }
337             );
338             return 1;
339             },
340              
341             liquify => sub {
342             my $recipe = shift;
343             my $data = shift;
344             $recipe -> {ingredients} -> {$data -> [1]} -> liquify();
345             return 1;
346             },
347              
348             liquify_contents => sub {
349             my $recipe = shift;
350             my $data = shift;
351             $recipe -> {bowls} -> [$data -> [1] - 1] -> liquify_contents();
352             return 1;
353             },
354              
355             stir_time => sub {
356             my $recipe = shift;
357             my $data = shift;
358             $recipe -> {bowls} -> [$data -> [2] - 1]
359             ->
360             stir_time( $data -> [1] );
361             return 1;
362             },
363              
364             stir_ingredient => sub {
365             my $recipe = shift;
366             my $data = shift;
367             $recipe -> {bowls} -> [$data -> [2] - 1]
368             ->
369             stir_ingredient( $recipe -> {ingredients} -> {$data -> [1]} );
370             return 1;
371             },
372              
373             mix => sub {
374             my $recipe = shift;
375             my $data = shift;
376             $recipe -> {bowls} -> [$data -> [1] - 1] -> mix();
377             return 1;
378             },
379              
380             clean => sub {
381             my $recipe = shift;
382             my $data = shift;
383             $recipe -> {bowls} -> [$data -> [1] - 1] -> clean();
384             return 1;
385             },
386              
387             pour => sub {
388             my $recipe = shift;
389             my $data = shift;
390             my @stuff = $recipe -> {bowls} -> [$data -> [1] - 1] -> pour();
391              
392             $recipe -> {dishes} -> [$data -> [2] - 1] -> put( $_ ) foreach @stuff;
393              
394             return 1;
395             },
396              
397             refrigerate => sub {
398             my $recipe = shift;
399             my $data = shift;
400             my $serves = $recipe->{serves};
401             my $hours = $data->[1];
402             $serves ||= 0;
403             $hours ||= 0;
404             $recipe->{serves} = $hours if $serves < $hours;
405             return 'halt';
406             },
407              
408             set_aside => sub {
409             my $recipe = shift;
410             my $data = shift;
411              
412             return 'break';
413             },
414              
415             serve_with => sub {
416             my $recipe = shift;
417             my $data = shift;
418              
419             my $rec_recipe = $data->[1];
420              
421             return "recurse.$rec_recipe" ;
422             },
423              
424             verb => sub {
425             my $recipe = shift;
426             my $data = shift;
427              
428             my $verb = $data->[1];
429             my $ingr = $data->[2];
430             return "loop.$verb.$ingr";
431             },
432              
433             until_verbed => sub {
434             my $recipe = shift;
435             my $data = shift;
436              
437             my $verb = $data->[1];
438              
439             if ( exists $recipe->{ingredients}->{$data->[2]} ) {
440             my $ingr = $recipe->{ingredients}->{$data->[2]};
441             $ingr->value( $ingr->value() - 1 );
442             }
443              
444             return "endloop.$verb";
445             },
446              
447             );
448              
449             =item new
450              
451             Acme::Chef::Recipe constructor. Arguments are interpreted as key/value pairs
452             and used as object attributes.
453              
454             =cut
455              
456              
457             sub new {
458 108     108 1 132 my $proto = shift;
459 108   66     308 my $class = ref $proto || $proto;
460              
461 108         144 my $self = {};
462              
463 108 100       262 if (ref $proto) {
464 98         685 %$self = %$proto;
465              
466 98         159 $self->{bowls} = [ map { $_->new() } @{$self -> {bowls }} ];
  137         289  
  98         192  
467 98         108 $self->{dishes} = [ map { $_->new() } @{$self -> {dishes}} ];
  71         137  
  98         173  
468 84         226 $self->{loops} = { map { ( $_, $self->{loops}{$_} ) }
469 98         116 keys %{$self->{loops}} };
  98         192  
470              
471 98 50       226 if ( $self->{compiled} ) {
472             $self->{ingredients} = { map {
473             (
474             $_,
475 341         670 $self -> {ingredients} -> {$_} -> new()
476             )
477 98         88 } keys %{ $self->{ingredients} }
  98         239  
478             };
479             }
480             }
481              
482 108         268 my %args = @_;
483              
484 108         1376 %$self = (
485             compiled => 0,
486             name => '',
487             comments => '',
488             ingredients => '',
489             cooking_time => '',
490             temperature => '',
491             method => '',
492             serves => '',
493             output => '',
494             loops => {},
495             bowls => [],
496             dishes => [],
497             %$self,
498             %args,
499             );
500              
501 108         448 bless $self => $class;
502 108         260 return $self;
503             }
504              
505              
506             =item execute
507              
508             Executes the recipe (program). First argument should be a reference to a
509             hash of sous-recipes.
510              
511             =cut
512              
513              
514             sub execute {
515 62     62 1 76 my $self = shift;
516              
517 62         72 my $recipes = shift;
518              
519 62 50       150 $self->compile() unless $self->{compiled};
520              
521 62         66 my @loop_stack;
522              
523 62         66 my $max_pos = $#{$self->{method}};
  62         110  
524 62         72 my $exec_pos = 0;
525 62         92 while (1) {
526              
527 2615         2670 my $next_method = $self->{method}->[$exec_pos];
528             # print ' ' x scalar(@loop_stack), join(',', @$next_method),"\n";
529              
530 2615         4303 my $return = $Commands{$next_method->[0]}->($self, $next_method);
531              
532 2615 100       4448 last if $return eq 'halt';
533              
534 2607 100       8439 if ( $return =~ /^recurse\.([\-\w][\-\w ]*)/ ) {
    100          
    100          
    50          
535 36 50       90 exists $recipes->{$1}
536             or croak "Invalid recipe '$1' specified for recursion.";
537              
538 36         58 my $clone = $self->new();
539             my $sous_recipe = $recipes->{$1}->new(
540             bowls => $clone->{bowls},
541             dishes => $clone->{dishes},
542 36         106 );
543              
544 36         62 my $sous_done = $sous_recipe->execute( $recipes );
545              
546 36         58 $self->output( $sous_done->output() );
547              
548 36         68 $self -> {bowls} -> [0]
549             -> put( $sous_done -> first_bowl() -> new() -> pour() );
550              
551             } elsif ( $return =~ /^loop\.([^\.]+)\.([^\.]+)/ ) {
552 624         882 my $verb = $1;
553 624         647 my $ingr = $2;
554              
555 624         751 push @loop_stack, $verb;
556              
557 624 100       1354 if ( not $self -> {ingredients} -> {$ingr} -> value() ) {
558 45         51 pop @loop_stack;
559 45         82 $exec_pos = $self -> {loops} -> {$verb} -> {end};
560             }
561              
562             } elsif ( $return =~ /^endloop\.([^\.]+)/ ) {
563 576         937 my $verb = $1;
564              
565 576         779 $exec_pos = $self -> {loops} -> {$verb} -> {start} - 1;
566              
567             } elsif ( $return =~ /^break/ ) {
568 0         0 my $verb = pop @loop_stack;
569 0         0 $exec_pos = $self -> {loops} -> {$verb} -> {end};
570             }
571              
572 2607         2324 $exec_pos++;
573 2607 100       4040 last if $exec_pos > $max_pos;
574             }
575              
576 62 100       156 if ( $self->{serves} ) {
577 26         124 foreach my $serve ( 0..($self->{serves}-1) ) {
578 35 50       42 last if $serve > $#{$self->{dishes}};
  35         110  
579 35         143 my $string = $self->{dishes}->[$serve]->print();
580 35         95 $self->{output} .= $string;
581             }
582             }
583              
584 62         155 return $self;
585             }
586              
587             =item first_bowl
588              
589             Returns the first bowl of the recipe.
590              
591             =cut
592              
593             sub first_bowl {
594 36     36 1 34 my $self = shift;
595 36         76 return $self->{bowls}->[0];
596             }
597              
598             =item require_ingredient
599              
600             First argument must be an ingredient object. Second may be a string indicating
601             the location of the requirement. Throws a fatal error if the ingredient is not
602             present.
603              
604             =cut
605              
606             sub require_ingredient {
607 162     162 1 161 my $self = shift;
608 162         225 my $ingredient = shift;
609 162         150 my $sub = shift;
610              
611 162 0 33     541 (defined $ingredient and exists $self->{ingredients}{$ingredient})
    0          
    50          
612             or croak "Unknown ingredient '".(defined$ingredient?$ingredient:'').
613             "' required for recipe '$self->{name}'".
614             (defined $sub?" in '$sub'":'').".";
615              
616 162         178 return $self;
617             }
618              
619             =item output
620              
621             Mutator for the Recipe output.
622              
623             =cut
624              
625             sub output {
626 98     98 1 102 my $self = shift;
627              
628 98 100       217 $self->{output} .= shift if @_;
629              
630 98         1402 return $self->{output};
631             }
632              
633             =item require_bowl
634              
635             First argument must be a number of bowls. Additional bowls are added to the
636             recipe if it currently has less than this number of bowls.
637              
638             =cut
639              
640             sub require_bowl {
641 195     195 1 206 my $self = shift;
642 195         211 my $no = shift;
643              
644 195 100       166 return if @{$self->{bowls}} >= $no;
  195         613  
645              
646 23         32 while (@{$self->{bowls}} < $no) {
  46         117  
647 23         27 push @{$self->{bowls}}, Acme::Chef::Container->new();
  23         122  
648             }
649              
650 23         47 return $self;
651             }
652              
653              
654             =item require_dish
655              
656             First argument must be a number of dishes. Additional dishes are added to the
657             recipe if it currently has less than this number of dishes.
658              
659             =cut
660              
661             sub require_dish {
662 22     22 1 35 my $self = shift;
663 22         31 my $no = shift;
664              
665 22 100       29 return if @{$self->{dishes}} >= $no;
  22         77  
666              
667 12         22 while (@{$self->{dishes}} < $no) {
  24         74  
668 12         15 push @{$self->{dishes}}, Acme::Chef::Container->new();
  12         59  
669             }
670              
671 12         20 return $self;
672             }
673              
674             =item recipe_name
675              
676             Mutator for the recipe name.
677              
678             =cut
679              
680             sub recipe_name {
681 19     19 1 29 my $self = shift;
682              
683 19 50       57 $self->{name} = shift if @_;
684              
685 19         139 return $self->{name};
686             }
687              
688              
689             =item compile
690              
691             Tries to compile the recipe. Returns 0 on error or if the recipe was
692             already compiled. Returns the compiled recipe if the compilation succeeded.
693              
694             =cut
695              
696             sub compile {
697 10     10 1 19 my $self = shift;
698              
699 10 50       81 return 0 if $self->{compiled};
700              
701 10         66 my @ingredients = split /\n/, $self->{ingredients};
702              
703 10         21 shift @ingredients; # remove header line
704              
705 10 50       34 @ingredients or croak "Failed compiling recipe. No ingredients specified.";
706              
707 10         18 my %ingredients;
708 10         20 my $ingredient_no = 0;
709              
710 10         27 foreach (@ingredients) {
711 56         78 $ingredient_no++;
712              
713 56         44 my $value;
714 56 100       288 if (s/^[ ]*(\d+)[ ]//) {
715 55         104 $value = $1;
716             } else {
717 1         1 $value = undef;
718             }
719              
720 56         69 my $measure_type = '';
721 56         128 foreach my $type ( keys %Acme::Chef::Ingredient::MeasureTypes ) {
722 112 50       1309 if ( s/^\Q$type\E[ ]// ) {
723 0         0 $measure_type = $type;
724 0         0 last;
725             }
726             }
727              
728 56         87 my $measure = '';
729 56         187 foreach my $meas ( keys %Acme::Chef::Ingredient::Measures ) {
730 652 100       1019 next if $meas eq '';
731              
732 615 100       5392 if ( s/^\Q$meas\E[ ]// ) {
733 23         36 $measure = $meas;
734 23         41 last;
735             }
736             }
737              
738             /[ ]*([\-\w][\- \w]*)[ ]*$/
739 56 50       333 or croak "Invalid ingredient specification (ingredient no. $ingredient_no, name).";
740              
741 56         112 my $ingredient_name = $1;
742              
743 56         226 my $ingredient = Acme::Chef::Ingredient->new(
744             name => $ingredient_name,
745             value => $value,
746             measure => $measure,
747             measure_type => $measure_type,
748             );
749              
750 56         228 $ingredients{$ingredient_name} = $ingredient;
751             }
752              
753 10         117 $self->{ingredients} = \%ingredients;
754              
755 10         590 $self->{method} =~ s/\s+/ /g;
756              
757 10         518 my @steps = split /\s*\.\s*/, $self->{method};
758              
759 10         99 shift @steps; # remove "Method."
760              
761 10         23 my $step_no = 0;
762 10         54 foreach my $step (@steps) {
763 212         186 $step_no++;
764              
765 212         288 foreach my $grammar (@GrammarOrder) {
766 1498         2564 my @res = $Grammars{$grammar}->($self, $step);
767 1498 100       3303 @res or next;
768              
769 212 100       536 if ( $res[0] eq 'verb' ) {
    100          
770 5         13 my $verb = $res[1];
771 5         11 my $ingr = $res[2];
772              
773 5         42 $self->{loops}->{$verb} = {start => ($step_no-1), test => $ingr};
774             } elsif ( $res[0] eq 'until_verbed' ) {
775 5         8 my $verb = $res[1];
776 5 50       18 exists $self->{loops}->{$verb}
777             or croak "Loop end without loop start '$verb'.";
778              
779 5         16 $self->{loops}->{$verb}->{end} = $step_no - 1;
780             }
781              
782 212         462 $step = [@res];
783 212         328 last;
784             }
785              
786 212 50       515 croak "Invalid method step (step no. $step_no): '$step'."
787             if not ref $step eq 'ARRAY';
788             }
789              
790 10 50       24 if ( grep { not exists $self->{loops}{$_}{end} } keys %{$self->{loops}} ) {
  5         26  
  10         47  
791 0         0 croak "Not all loop starting points have matching ends.";
792             }
793              
794 10         30 $self->{method} = \@steps;
795              
796 10         18 $self->{compiled} = 1;
797              
798 10         51 return $self;
799             }
800              
801              
802             __END__