File Coverage

lib/B/DeparseTree/SyntaxTree.pm
Criterion Covered Total %
statement 161 219 73.5
branch 86 138 62.3
condition 24 53 45.2
subroutine 15 16 93.7
pod 0 14 0.0
total 286 440 65.0


line stmt bran cond sub pod time code
1             # B::DeparseTree tree-building routines.
2             # Copyright (c) 2018 Rocky Bernstein
3             # All rights reserved.
4             # This module is free software; you can redistribute and/or modify
5             # it under the same terms as Perl itself.
6              
7             # This is based on the module B::Deparse by Stephen McCamant.
8             # It has been extended save tree structure, and is addressible
9             # by opcode address.
10              
11             # Note the package name. It is *not* B::DeparseTree::Tree.
12             # In the future there may be a version of this that doesn't
13             # save as much information, but just stores enough to extract
14             # a string, which would be a slightly more heavyweight version of
15             # B::Deparse.
16             package B::DeparseTree::SyntaxTree;
17              
18 8     8   2487 use B::DeparseTree::TreeNode;
  8         71  
  8         11714  
19              
20             our($VERSION, @EXPORT, @ISA);
21             $VERSION = '3.2.0';
22             @ISA = qw(Exporter B::DeparseTree);
23             @EXPORT = qw(
24             combine
25             combine2str
26             get_info_and_str
27             expand_simple_spec
28             indent_less
29             indent_more
30             indent_value
31             info2str
32             info_from_list
33             info_from_template
34             info_from_string
35             info_from_text
36             template_engine
37             template2str
38             );
39              
40             sub combine($$$)
41             {
42 0     0 0 0 my ($self, $sep, $items) = @_;
43             # FIXME: loop over $item, testing type.
44 0 0       0 Carp::confess("should be a reference to a array: is $items") unless
45             ref $items eq 'ARRAY';
46 0         0 my @result = ();
47 0         0 foreach my $item (@$items) {
48 0         0 my $add;
49 0 0       0 if (ref $item) {
50 0 0 0     0 if (ref $item eq 'ARRAY' and scalar(@$item) == 2) {
    0          
51 0         0 $add = [$item->[0], $item->[1]];
52 0         0 } elsif (eval{$item->isa("B::DeparseTree::TreeNode")}) {
53 0         0 $add = [$item->{text}, $item->{addr}];
54             # First item is text and second item is op address.
55             } else {
56 0         0 Carp::confess("don't know what to do with $item");
57             }
58             } else {
59 0         0 $add = $item;
60             }
61 0 0 0     0 push @result, $sep if @result && $sep;
62 0         0 push @result, $add;
63             }
64 0         0 return @result;
65             }
66              
67             sub combine2str($$$)
68             {
69 168     168 0 218 my ($self, $sep, $items) = @_;
70 168         200 my $result = '';
71 168         247 foreach my $item (@$items) {
72 728 100       918 $result .= $sep if $result;
73 728 100       780 if (ref $item) {
74 2 50 33     7 if (ref $item eq 'ARRAY' and scalar(@$item) == 2) {
    50          
75             # First item is text and second item is op address.
76 0         0 $result .= $self->info2str($item->[0]);
77 2         7 } elsif (eval{$item->isa("B::DeparseTree::TreeNode")}) {
78 2 50       5 if (exists $item->{fmt}) {
79 2         5 $result .= $self->template2str($item);
80             } else {
81 0         0 $result .= $self->info2str($item);
82             }
83             } else {
84 0         0 Carp::confess("Invalid ref item ref($item)");
85             }
86             } else {
87             # FIXME: add this and remove errors
88 726 50       924 if (index($item, '@B::DeparseTree::TreeNode') > 0) {
89 0         0 Carp::confess("\@B::DeparseTree::TreeNode as an item is probably wrong");
90             }
91 726         821 $result .= $item;
92             }
93             }
94 168         283 return $result;
95             }
96              
97             sub expand_simple_spec($$)
98             {
99 38331     38331 0 52461 my ($self, $fmt) = @_;
100 38331         42434 my $result = '';
101 38331         61722 while ((my $k=index($fmt, '%')) >= 0) {
102 26583         35177 $result .= substr($fmt, 0, $k);
103 26583         32503 my $spec = substr($fmt, $k, 2);
104 26583         30775 $fmt = substr($fmt, $k+2);
105              
106 26583 50       53245 if ($spec eq '%%') {
    100          
    100          
    50          
107 0         0 $result .= '%';
108             } elsif ($spec eq '%+') {
109 2785         5043 $result .= $self->indent_more();
110             } elsif ($spec eq '%-') {
111 2785         4990 $result .= $self->indent_less();
112             } elsif ($spec eq '%|') {
113 21013         30242 $result .= $self->indent_value();
114             } else {
115 0         0 Carp::confess("Unknown spec $spec")
116             }
117             }
118 38331 100       57138 $result .= $fmt if $fmt;
119 38331         63025 return $result;
120             }
121              
122             sub indent_less($$) {
123 2785     2785 0 4622 my ($self, $check_level) = @_;
124 2785 50       5404 $check_level = 0 if !defined $check_level;
125              
126 2785         4238 $self->{level} -= $self->{'indent_size'};
127 2785         3397 my $level = $self->{level};
128 2785 50       4668 if ($check_level < 0) {
129 0 0       0 Carp::confess("mismatched indent/dedent") if $check_level;
130 0         0 $level = 0;
131 0         0 $self->{level} = 0;
132             }
133 2785         4399 return $self->indent_value();
134             }
135              
136             sub indent_more($) {
137 2785     2785 0 4068 my ($self) = @_;
138 2785         4266 $self->{level} += $self->{'indent_size'};
139 2785         4187 return $self->indent_value();
140             }
141              
142             sub indent_value($) {
143 26583     26583 0 33174 my ($self) = @_;
144 26583         31166 my $level = $self->{level};
145 26583 50       36065 if ($self->{'use_tabs'}) {
146 0         0 return "\t" x ($level / 8) . " " x ($level % 8);
147             } else {
148 26583         80883 return " " x $level;
149             }
150             }
151              
152             sub info2str($$)
153             {
154 122031     122031 0 145974 my ($self, $item) = @_;
155 122031         130568 my $result = '';
156 122031 100       167024 if (ref $item) {
157 114088 50 33     198363 if (ref $item eq 'ARRAY' and scalar(@$item) == 2) {
    50          
158             # This code is going away...
159 0         0 Carp::confess("fixme");
160 0         0 $result = $item->[0];
161 114088         241953 } elsif (eval{$item->isa("B::DeparseTree::TreeNode")}) {
162 114088 100 66     216194 if (exists $item->{fmt}) {
    100          
163 52066         75762 $result .= $self->template2str($item);
164 52066 100       91652 if ($item->{maybe_parens}) {
165 7243         9042 my $mp = $item->{maybe_parens};
166 7243 100 66     20439 if ($mp->{force} || $mp->{parens}) {
167 306         507 $result = "($result)";
168             }
169             }
170             } elsif (!exists $item->{texts} && exists $item->{text}) {
171             # Is a constant string
172 61856         84418 $result .= $item->{text};
173             } else {
174             $result = $self->combine2str($item->{sep},
175 166         278 $item->{texts});
176             }
177              
178             } else {
179 0         0 Carp::confess("Invalid ref item ref($item)");
180             }
181             } else {
182             # FIXME: add this and remove errors
183 7943 50       13255 if (index($item, '@B::DeparseTree::TreeNode') > 0) {
184 0         0 Carp::confess("\@B::DeparseTree::TreeNode as an item is probably wrong");
185             }
186 7943         9590 $result = $item;
187             }
188 122031         190576 return $result;
189             }
190              
191             # Create an info structure from a list of strings
192             # FIXME: $deparse (or rather $self) should be first
193             sub info_from_list($$$$$$)
194             {
195 29     29 0 65 my ($op, $self, $texts, $sep, $type, $opts) = @_;
196              
197             # Set undef in "texts" argument position because we are going to create
198             # our own text from the $texts.
199 29         76 my $info = B::DeparseTree::TreeNode->new($op, $self, $texts, undef,
200             $type, $opts);
201 29         52 $info->{sep} = $sep;
202 29         39 my $text = '';
203 29         45 foreach my $item (@$texts) {
204 116 100 100     243 $text .= $sep if $text and $sep;
205 116 50       162 if(ref($item) eq 'ARRAY'){
    50          
206 0         0 $text .= $item->[0];
207 116         461 } elsif (eval{$item->isa("B::DeparseTree::TreeNode")}) {
208 0         0 $text .= $item->{text};
209             } else {
210 116         196 $text .= $item;
211             }
212             }
213              
214 29         54 $info->{text} = $text;
215 29 100       57 if ($opts->{maybe_parens}) {
216 4         5 my ($obj, $context, $precedence) = @{$opts->{maybe_parens}};
  4         8  
217 4         8 my $parens = B::DeparseTree::TreeNode::parens_test($obj, $context, $precedence);
218             $self->{maybe_parens} = {
219             context => $context,
220             precedence => $precedence,
221 4 50       19 force => $obj->{'parens'},
222             parens => $parens ? 'true' : ''
223             };
224 4 50 33     15 $info->{text} = "($info->{text})" if exists $info->{text} and $parens;
225             }
226              
227 29         116 return $info
228             }
229              
230             # Create an info structure a template pattern
231             sub info_from_template($$$$$) {
232 12206     12206 0 24590 my ($self, $type, $op, $fmt, $indexes, $args, $opts) = @_;
233 12206 100       22880 $opts = {} unless defined($opts);
234             # if (ref($args) ne "ARRAY") {
235             # use Enbugger "trepan"; Enbugger->stop;
236             # }
237 12206         20339 my @args = @$args;
238 12206         27682 my $info = B::DeparseTree::TreeNode->new($op, $self, $args, undef, $type, $opts);
239              
240 12206 100       25514 $indexes = [0..$#args] unless defined $indexes;
241 12206         18339 $info->{'indexes'} = $indexes;
242 12206         23244 my $text = $self->template_engine($fmt, $indexes, $args);
243              
244 12206         23549 $info->{'fmt'} = $fmt;
245 12206         19877 $info->{'text'} = $self->template_engine($fmt, $indexes, $args);
246              
247 12206 100       22060 if (! defined $op) {
248 1341         2669 $info->{addr} = ++$self->{'last_fake_addr'};
249 1341         5990 $self->{optree}{$info->{addr}} = $info;
250             }
251              
252 12206 50       19442 if ($opts->{'relink_children'}) {
253             # FIXME we should specify which children to relink
254 0         0 for (my $i=0; $i < scalar @$args; $i++) {
255 0 0       0 if ($args[$i]->isa("B::DeparseTree::TreeNode")) {
256 0         0 $args[$i]{parent} = $info->{addr};
257             }
258             }
259             }
260              
261             # Link the parent of Deparse::Tree::TreeNodes to this node.
262 12206 50       18013 if ($opts->{'synthesized_nodes'}) {
263 0         0 foreach my $node (@{$opts->{'synthesized_nodes'}}) {
  0         0  
264 0         0 $node->{parent} = $info->{addr};
265             }
266             }
267              
268             # Need to handle maybe_parens since B::DeparseNode couldn't do that
269             # as it was passed a ref ARRAY rather than a string.
270 12206 100       17533 if ($opts->{maybe_parens}) {
271 1560         2313 my ($obj, $context, $precedence) = @{$opts->{maybe_parens}};
  1560         3507  
272 1560         4122 my $parens = B::DeparseTree::TreeNode::parens_test($obj,
273             $context, $precedence);
274             $info->{maybe_parens} = {
275             context => $context,
276             precedence => $precedence,
277 1560 100       8383 force => $obj->{'parens'},
278             parens => $parens ? 'true' : ''
279             };
280 1560 100 66     5818 $info->{text} = "($info->{text})" if exists $info->{text} and $parens;
281             }
282              
283 12206         39818 return $info;
284             }
285              
286             # Create an info structure from a single string
287             sub info_from_string($$$$$)
288             {
289 13779     13779 0 26852 my ($self, $type, $op, $str, $opts) = @_;
290 13779   100     39369 $opts ||= {};
291 13779         33387 return B::DeparseTree::TreeNode->new($op, $self, $str, undef,
292             $type, $opts);
293             }
294              
295             # OBSOLETE: Create an info structure from a single string
296             # FIXME: remove this
297             sub info_from_text($$$$$)
298             {
299 17     17 0 41 my ($op, $self, $text, $type, $opts) = @_;
300             # Use this to smoke outt calls
301             # use Enbugger 'trepan'; Enbugger->stop;
302 17         38 return $self->info_from_string($type, $op, $text, $opts)
303             }
304              
305             # List of suffix characters that are handled by "expand_simple_spec()".
306 8     8   67 use constant SIMPLE_SPEC => '%+-|';
  8         15  
  8         6695  
307              
308             # Extract the string at $args[$index] and if
309             # we are looking for that position include where we are in
310             # that position
311             sub get_info_and_str($$$)
312             {
313 121023     121023 0 156165 my ($self, $index, $args) = @_;
314 121023         140132 my $info = $args->[$index];
315 121023         154061 my $str = $self->info2str($info);
316 121023         217268 return ($info, $str);
317             }
318              
319             sub template_engine($$$$)
320             {
321 76480     76480 0 114698 my ($self, $fmt, $indexes, $args, $find_addr) = @_;
322              
323             # use Data::Dumper;
324             # print "-----\n";
325             # p $args;
326             # print "'======\n";
327             # print $fmt, "\n"
328             # print $args, "\n";
329              
330 76480         78974 my $i = 0;
331 76480 50       103498 $find_addr = -2 unless $find_addr;
332              
333 76480         81769 my $start_fmt = $fmt; # used in error messages
334 76480         102939 my @args = @$args;
335              
336 76480         80055 my $result = '';
337 76480         78473 my $find_pos = undef;
338 76480         134835 while ((my $k=index($fmt, '%')) >= 0) {
339 88927         132144 $result .= substr($fmt, 0, $k);
340 88927         107634 my $spec = substr($fmt, $k, 2);
341 88927         108637 $fmt = substr($fmt, $k+2);
342              
343 88927 100       182762 if (index(SIMPLE_SPEC, substr($spec, 1, 1)) >= 0) {
    100          
    100          
    50          
    100          
    50          
    0          
344 8320         13104 $result .= $self->expand_simple_spec($spec);
345             } elsif ($spec eq "%c") {
346             # Insert child entry
347              
348             # FIXME: turn this into a subroutine.
349 49716 50       51709 if ($i >= scalar @{$indexes}) {
  49716         75965  
350 0         0 Carp::confess("Need another entry in args_spec for %c in fmt: $start_fmt");
351             }
352 49716         64389 my $index = $indexes->[$i++];
353 49716 50       73634 if ($index >= scalar @args) {
354 0         0 Carp::confess("$index in $start_fmt for %c is too large; should be less " .
355             "than scalar(@args)");
356             }
357 49716         52161 my $str;
358 49716         75269 my ($info, $str) = $self->get_info_and_str($index, $args);
359 49716 50 66     133142 if (ref($info) && $info->{'addr'} == $find_addr) {
360 0         0 my $len = length($result);
361             $len++ if (exists $info->{maybe_parens}
362 0 0 0     0 && $info->{maybe_parens}{parens});
363 0         0 $find_pos = [$len, length($str)];
364             }
365 49716         105677 $result .= $str;
366             } elsif ($spec eq "%C") {
367             # Insert separator between child entry lists
368 11748         13216 my ($low, $high, $sub_spec) = @{$indexes->[$i++]};
  11748         21290  
369 11748         21522 my $sep = $self->expand_simple_spec($sub_spec);
370 11748         15084 my $list = '';
371 11748         20766 for (my $j=$low; $j<=$high; $j++) {
372 31243 100       47285 $result .= $sep if $j > $low;
373              
374             # FIXME: Remove duplicate code
375 31243         45800 my ($info, $str) = $self->get_info_and_str($j, $args);
376 31243 50 33     81285 if (ref($info) && $info->{'addr'} == $find_addr) {
377 0         0 my $len = length($result);
378             $len++ if (exists $info->{maybe_parens}
379 0 0 0     0 && $info->{maybe_parens}{parens});
380 0         0 $find_pos = [$len, length($str)];
381             }
382 31243         62866 $result .= $str;
383             }
384             } elsif ($spec eq "%f") {
385             # Run maybe_parens_func
386 0         0 my $fn_name = shift @args;
387 0         0 my ($cx, $prec) = @{$indexes->[$i++]};
  0         0  
388 0         0 my $params = $self->template_engine("%C", [[0, $#args], ', ']);
389 0         0 $result .= B::Deparse::maybe_parens_func($self, $fn_name, $params, $cx, $prec);
390             } elsif ($spec eq "%F") {
391             # Run a transformation function
392 880 50       1351 if ($i >= scalar@$indexes) {
393 0         0 Carp::confess("Need another entry in args_spec for %%F fmt: $start_fmt");
394             }
395 880         863 my ($arg_index, $transform_fn) = @{$indexes->[$i++]};
  880         1376  
396 880 50       1416 if ($arg_index >= scalar @args) {
397 0         0 Carp::confess("argument index $arg_index in $start_fmt for %%F is too large; should be less than @$args");
398             }
399 880 50       1531 if (ref($transform_fn ne 'CODE')) {
400 0         0 Carp::confess("transformation function $transform_fn is not CODE");
401             }
402 880         1100 my ($arg) = $args[$arg_index];
403 880         1443 $result .= $transform_fn->($arg);
404              
405             } elsif ($spec eq "%;") {
406             # Insert semicolons and indented newlines between statements.
407             # Don't insert them around empty strings - some OPs
408             # don't have an text associated with them.
409             # Finally, replace semicolon a the end of statement that
410             # end in "}" with a \n and proper indent.
411 18263         29373 my $sep = $self->expand_simple_spec(";\n%|");
412 18263         23300 my $start_size = length($result);
413 18263         35966 for (my $j=0; $j< @args; $j++) {
414 40064         47554 my $old_result = $result;
415 40064 100 100     85762 if ($j > 0 && length($result) > $start_size) {
416             # Remove any prior ;\n
417 26538 100       45037 $result = substr($result, 0, -1) if substr($result, -1) eq "\n";
418 26538 100       40523 $result = substr($result, 0, -1) if substr($result, -1) eq ";";
419             ## The below needs to be done based on whether the previous construct is a compound statement or not.
420             ## That could be added in a trailing format specifier for it.
421             ## "sub {...}" and "$h = {..}" need a semicolon while "if () {...}" doesn't.
422             # if (substr($result, -1) eq "}" & $j < $#args) {
423             # # Omit ; from sep. FIXME: do this based on an option?
424             # $result .= substr($sep, 1);
425             # } else {
426             # $result .= $sep;
427             # }
428 26538         34794 $result .= $sep;
429             }
430              
431             # FIXME: Remove duplicate code
432 40064         61260 my ($info, $str) = $self->get_info_and_str($j, $args);
433 40064 50 66     101815 if (ref($info) && $info->{'addr'} == $find_addr) {
434 0         0 my $len = length($result);
435 0 0 0     0 $len++ if exists $info->{maybe_parens} and $info->{maybe_parens}{parens};
436 0         0 $find_pos = [length($result), length($str)];
437             }
438             # Note: $str eq 0 add to $result
439 40064 100 66     90082 if (defined $str && $str ne '') {
440 36557         91578 $result .= $str
441             } else {
442 3507         8000 $result = $old_result;
443             }
444             }
445             # # FIXME: Add the final ';' based on an option?
446             # if ($result and not
447             # (substr($result, -1) eq ';' or
448             # (substr($result, -1) eq ';\n'))) {
449             # $result .= ';' if $result and substr($result, -1) ne ';';
450             # }
451              
452             } elsif ($spec eq "\cS") {
453             # FIXME: not handled yet
454             ;
455             } else {
456             # We have % with a non-special symbol. Just preserve those.
457 0         0 $result .= $spec;
458             }
459             }
460 76480 100       114807 $result .= $fmt if $fmt;
461 76480 50       101754 if ($find_addr != -2) {
462             # want result and position
463 0         0 return $result, $find_pos;
464             }
465             # want just result
466 76480         154710 return $result;
467              
468             }
469              
470             sub template2str($$) {
471 52068     52068 0 64924 my ($self, $info) = @_;
472             return $self->template_engine($info->{fmt},
473             $info->{indexes},
474 52068         82399 $info->{texts});
475             }
476              
477             1;