File Coverage

blib/lib/CodeGen/Cpppp.pm
Criterion Covered Total %
statement 231 301 76.7
branch 55 114 48.2
condition 29 52 55.7
subroutine 30 34 88.2
pod 12 14 85.7
total 357 515 69.3


line stmt bran cond sub pod time code
1             package CodeGen::Cpppp;
2 8     8   1612964 use v5.20;
  8         68  
3 8     8   45 use warnings;
  8         15  
  8         181  
4 8     8   47 use Carp;
  8         19  
  8         374  
5 8     8   3640 use experimental 'signatures';
  8         25102  
  8         51  
6 8     8   1415 use version;
  8         23  
  8         35  
7 8     8   555 use Cwd 'abs_path';
  8         26  
  8         377  
8 8     8   47 use Scalar::Util 'blessed', 'looks_like_number';
  8         19  
  8         462  
9 8     8   3737 use CodeGen::Cpppp::Template;
  8         23  
  8         37  
10              
11             our $VERSION= '0.001_04'; # TRIAL VERSION
12             # ABSTRACT: The C Perl-Powered Pre-Processor
13              
14              
15 19     19 1 28 sub autocomma($self, $newval=undef) {
  19         38  
  18         31  
  18         24  
16 18 50       47 $self->{autocomma}= $newval if defined $newval;
17 18   50     127 $self->{autocomma} // 1;
18             }
19 18     19 1 28 sub autostatementline($self, $newval=undef) {
  18         28  
  18         28  
  18         27  
20 18 50       47 $self->{autostatementline}= $newval if defined $newval;
21 18   50     102 $self->{autostatementline} // 1;
22             }
23 18     18 1 30 sub autoindent($self, $newval=undef) {
  18         33  
  18         25  
  18         36  
24 18 50       47 $self->{autoindent}= $newval if defined $newval;
25 18   50     90 $self->{autoindent} // 1;
26             }
27 18     18 1 29 sub autocolumn($self, $newval=undef) {
  18         27  
  18         32  
  18         26  
28 18 50       44 $self->{autocolumn}= $newval if defined $newval;
29 18   50     141 $self->{autocolumn} // 1;
30             }
31              
32              
33 1   50 1 1 6 sub include_path { $_[0]{include_path} //= [] }
34 4   66 4 1 29 sub output { $_[0]{output} //= CodeGen::Cpppp::Output->new }
35              
36              
37 7     7 1 634 sub new($class, @attrs) {
  7         20  
  7         16  
  7         14  
38             my $self= bless {
39 7 50 33     72 @attrs == 1 && ref $attrs[0]? %{$attrs[0]}
  0 50       0  
40             : !(@attrs&1)? @attrs
41             : croak "Expected even-length list or hashref"
42             }, $class;
43             $self->{include_path}= [ $self->{include_path} ]
44 7 100 66     66 if defined $self->{include_path} && ref $self->{include_path} ne 'ARRAY';
45 7         25 $self;
46             }
47              
48              
49 3     3 1 4 sub require_template($self, $filename) {
  3         5  
  3         5  
  3         4  
50 3   66     12 $self->{templates}{$filename} ||= do {
51 1 50       4 my $path= $self->find_template($filename)
52             or croak("No template '$filename' found");
53 1   33     15 $self->{templates}{$path} ||= $self->compile_cpppp($path);
54             }
55             }
56              
57              
58 1     1 1 2 sub find_template($self, $filename) {
  1         8  
  1         4  
  1         2  
59 1 50 33     5 return abs_path($filename) if $filename =~ m,/, and -e $filename;
60             # /foo ./foo and ../foo do not trigger a path search
61 1 50       4 return undef if $filename =~ m,^\.?\.?/,;
62 1         4 for ($self->include_path->@*) {
63 1         4 my $p= "$_/$filename";
64 1         4 $p =~ s,//,/,g; # in case include-path ends with '/'
65 1 50       80 return abs_path($p) if -e $p;
66             }
67 0         0 return undef;
68             }
69              
70              
71 4     4 1 13 sub new_template($self, $class_or_filename, @params) {
  4         8  
  4         6  
  4         7  
  4         6  
72 4 100 66     32 my $class= $class_or_filename =~ /^CodeGen::Cpppp::/ && $class_or_filename->can('new')
73             ? $class_or_filename
74             : $self->require_template($class_or_filename);
75             my %params= (
76             context => $self,
77             output => $self->output,
78             !(@params&1)? @params
79 4 0 0     12 : 1 == @params && ref $params[0] eq 'HASH'? %{$params[0]}
  0 50       0  
80             : croak("Expected even-length key/val list, or hashref"),
81             );
82 4         24 $class->new(\%params);
83             }
84              
85              
86             our $next_pkg= 1;
87 15     15 1 32230 sub compile_cpppp($self, @input_args) {
  15         28  
  15         31  
  15         22  
88 15         110 my $parse= $self->parse_cpppp(@input_args);
89 15         53 my $perl= $self->_gen_perl_template_package($parse);
90 15 50   5   1606 unless (eval $perl) {
  5     5   89  
  5     3   19  
  5     3   45  
  5     2   9  
  5     2   43  
  3         55  
  3         11  
  3         23  
  3         5  
  3         17  
  2         25  
  2         7  
  2         11  
  2         5  
  2         10  
91 0         0 die "$perl\n\nException: $@\n";
92             }
93 15         305 return $parse->{package};
94             }
95              
96 15     15   25 sub _gen_perl_template_package($self, $parse, %opts) {
  15         31  
  15         47  
  15         29  
  15         18  
97 15   50     58 my $perl= $parse->{code} // '';
98 15         43 my ($src_lineno, $src_filename, @global, $perl_ver, $cpppp_ver, $tpl_use_line)= (1);
99             # Extract all initial 'use' and 'no' statements from the script.
100             # If they refer to perl or CodeGen:::Cpppp, make a note of it.
101 15         159 while ($perl =~ s/^ ( [ \t]+ | [#] .* | use [^;]+ ; | no [^;]+ ; \s* ) \n//gx) {
102 15         60 my $line= $1;
103 15         33 push @global, $line;
104 15 50       68 $perl_ver= version->parse($1)
105             if $line =~ /use \s+ ( v.* | ["']? [0-9.]+ ["']? ) \s* ; /x;
106 15 50       62 $cpppp_ver= version->parse($1)
107             if $line =~ /use \s+ CodeGen::Cpppp \s* ( v.* | ["']? [0-9.]+ ["']? ) \s* ; /x;
108 15 50       55 $tpl_use_line= 1
109             if $line =~ /use \s+ CodeGen::Cpppp::Template \s+/;
110 15 50       94 if ($line =~ /^# line (\d+) "([^"]+)"/) {
111 15         44 $src_lineno= $1;
112 15         105 $src_filename= $2;
113             } else {
114 0         0 $src_lineno+= 1 + (()= $line =~ /\n/g);
115             }
116             }
117 15 50       44 if ($opts{with_data}) {
118 0         0 require Data::Dumper;
119 0         0 my $dumper= Data::Dumper->new([ { %$parse, code => '...' } ], [ '$_parse_data' ])
120             ->Indent(1)->Sortkeys(1);
121 0         0 push @global,
122             'our $_parse_data; '.$dumper->Dump;
123             }
124              
125             # Build the boilerplate for the template eval
126 15         94 my $pkg= CodeGen::Cpppp::Template->_create_derived_package($cpppp_ver, $parse);
127 15         45 $parse->{package}= $pkg;
128 15   33     82 $cpppp_ver //= $VERSION;
129 15   33     36 $src_filename //= $parse->{filename};
130 15         174 join '', map "$_\n",
131             "package $pkg;",
132             # Inject a minimum perl version unless user-provided
133             ("use v5.20;")x!(defined $perl_ver),
134             # Inject a Template -setup unless user-provided
135             ("use CodeGen::Cpppp::Template -setup => $cpppp_ver;")x!($tpl_use_line),
136             # All the rest of the user's use/no statements
137             @global,
138             # Everything after that goes into a sub
139             "sub BUILD(\$self, \$constructor_parameters=undef) {",
140             " Scalar::Util::weaken(\$self);",
141             # Inject all the lexical functions that need to be in scope
142             $pkg->_gen_perl_scope_functions($cpppp_ver),
143             qq{# line $src_lineno "$src_filename"},
144             $perl,
145             "}",
146             "1";
147             }
148              
149             sub parse_cpppp($self, $in, $filename=undef, $line=undef) {
150             my @lines;
151             if (ref $in eq 'SCALAR') {
152             @lines= split /^/m, $$in;
153             }
154             else {
155             my $fh;
156             if (ref $in eq 'GLOB' || (blessed($in) && $in->can('getline'))) {
157             $fh= $in;
158             } else {
159             open($fh, '<', $in) or croak "open($in): $!";
160             }
161             local $/= undef;
162             my $text= <$fh>;
163             $filename //= "$in";
164             utf8::decode($text) or warn "$filename is not encoded as utf-8\n";
165             @lines= split /^/m, $text;
166             }
167             $line //= 1;
168             $self->{cpppp_parse}= {
169             autocomma => $self->autocomma,
170             autostatementline => $self->autostatementline,
171             autoindent => $self->autoindent,
172             autocolumn => $self->autocolumn,
173             filename => $filename,
174             colmarker => {},
175             coltrack => { },
176             };
177             my ($perl, $block_group, $tpl_start_line, $cur_tpl)= ('', 1);
178             my sub end_tpl {
179             if (defined $cur_tpl && $cur_tpl =~ /\S/) {
180             my $parsed= $self->_parse_code_block($cur_tpl, $filename, $tpl_start_line);
181             my $current_indent= $perl =~ /\n([ \t]*).*\n\Z/? $1 : '';
182             $current_indent .= ' ' if $perl =~ /\{ *\n\Z/;
183             $perl .= $self->_gen_perl_call_code_block($parsed, $current_indent);
184             }
185             $cur_tpl= undef;
186             };
187             for (@lines) {
188             if (/^#!/) { # ignore #!
189             }
190             elsif (/^##/) { # full-line of perl code
191             if (defined $cur_tpl || !length $perl) {
192             end_tpl();
193             $perl .= qq{# line $line "$filename"\n};
194             }
195             (my $pl= $_) =~ s/^##\s?//;
196             $perl .= $self->_transform_template_perl($pl, $line);
197             }
198             elsif (/^(.*?) ## ?((?:if|unless|for|while|unless) .*)/) { # perl conditional suffix, half tpl/half perl
199             my ($tpl, $pl)= ($1, $2);
200             end_tpl() if defined $cur_tpl;
201             $tpl_start_line= $line;
202             $cur_tpl= $tpl;
203             end_tpl();
204             $perl =~ s/;\s*$//; # remove semicolon
205             $pl .= ';' unless $pl =~ /;\s*$/; # re-add it if user didn't
206             $perl .= qq{\n# line $line "$filename"\n $pl\n};
207             }
208             else { # default is to assume a line of template
209             if (!defined $cur_tpl) {
210             $tpl_start_line= $line;
211             $cur_tpl= '';
212             }
213             $cur_tpl .= $_;
214             }
215             } continue { ++$line }
216             end_tpl() if defined $cur_tpl;
217              
218             # Resolve final bits of column tracking
219             my $ct= delete $self->{cpppp_parse}{coltrack};
220             _finish_coltrack($ct, $_) for grep looks_like_number($_), keys %$ct;
221              
222             $self->{cpppp_parse}{code}= $perl;
223             delete $self->{cpppp_parse};
224             }
225              
226 59     59   99 sub _transform_template_perl($self, $pl, $line) {
  59         78  
  59         91  
  59         84  
  59         75  
227             # If user declares "sub NAME(", convert that to "my sub NAME" so that it can
228             # capture refs to the variables of new template instances.
229 59 100       174 if ($pl =~ /(my)? \s* \b sub \s* ([\w_]+) \b \s* /x) {
230 3         12 my $name= $2;
231 3         21 $self->{cpppp_parse}{template_method}{$name}= { line => $line };
232 3         10 my $ofs= $-[0];
233 3 50       12 my $ofs2= defined $1? $+[1] : $ofs;
234 3         29 substr($pl, $ofs, $ofs2-$ofs, "my sub $name; \$self->define_template_method($name => \\&$name);");
235             }
236             # If user declares 'param $foo = $x' adjust that to 'param my $foo = $x'
237 59 100       225 if ($pl =~ /^ \s* (param) \b /xgc) {
    50          
238 13         39 my $ofs= $-[1];
239             # It's an error if the thing following isn't a variable name
240 13 50       54 $pl =~ /\G \s* ( [\$\@\%] [\w_]+ ) /xgc
241             or croak("Expected variable name (including sigil) after 'param'");
242 13         32 my $var_name= $1;
243 13 50       43 $pl =~ /\G \s* ([;=]) /xgc
244             or croak("Parameter declaration $var_name must be followed by '=' or ';'");
245 13         51 my $term= $1;
246 13         35 my $name= substr($var_name, 1);
247 13 100       88 substr($pl, $ofs, $+[0]-$ofs, qq{param '$name', \\my $var_name }.($term eq ';'? ';' : ','));
248 13         64 $self->{cpppp_parse}{template_parameter}{$name}= substr($var_name,0,1);
249             }
250             # If user declares "define name(", convert that to both a method and a define
251             elsif ($pl =~ /^ \s* (define) \s* ([\w_]+) (\s*) \(/x) {
252 0         0 my $name= $2;
253 0         0 $self->{cpppp_parse}{template_macro}{$name}= 'CODE';
254 0         0 substr($pl, $-[1], $-[2]-$-[1], qq{my sub $name; \$self->define_template_macro($name => \\&$name); sub });
255             }
256 59         152 $pl;
257             }
258              
259 26     26   45 sub _gen_perl_call_code_block($self, $parsed, $indent='') {
  26         42  
  26         36  
  26         44  
  26         35  
260 26   100     115 my $codeblocks= $self->{cpppp_parse}{code_block_templates} ||= [];
261 26         52 push @$codeblocks, $parsed;
262 26         88 my $code= $indent.'$self->_render_code_block('.$#$codeblocks;
263 26         49 my %cache;
264 26         35 my $i= 0;
265 26         41 my $cur_line= 0;
266 26         44 for my $s (@{$parsed->{subst}}) {
  26         64  
267 68 100       131 if (defined $s->{eval}) {
268             # No need to create more than one anonsub for the same expression
269 55 100       124 if (defined $cache{$s->{eval}}) {
270 16         28 $s->{eval_idx}= $cache{$s->{eval}};
271 16         32 next;
272             }
273 39         98 $cache{$s->{eval}}= $s->{eval_idx}= $i++;
274 39 50       136 my $sig= $s->{eval} =~ /self|output/? '($self, $output)' : '';
275 39 100       100 if ($s->{line} == $cur_line) {
    100          
276 10         29 $code .= qq{, sub${sig}{ $s->{eval} }};
277             } elsif ($s->{line} == $cur_line+1) {
278 6         11 $cur_line++;
279 6         25 $code .= qq{,\n$indent sub${sig}{ $s->{eval} }};
280             } else {
281 23         102 $code .= qq{,\n# line $s->{line} "$parsed->{file}"\n$indent sub${sig}{ $s->{eval} }};
282 23         59 $cur_line= $s->{line};
283 23         85 $cur_line++ for $s->{eval} =~ /\n/g;
284             }
285             }
286             }
287 26 100       120 $code .= "\n$indent" if index($code, "\n") >= 0;
288 26         158 $code . ");\n";
289             }
290              
291 4     4   7 sub _finish_coltrack($coltrack, $col) {
  4         7  
  4         6  
  4         4  
292             # did it eventually have an eval to the left?
293 4 50       19 if (grep $_->{follows_eval}, $coltrack->{$col}{members}->@*) {
294 4         8 $coltrack->{$col}{members}[-1]{last}= 1;
295             } else {
296             # invalidate them all, they won't become unaligned anyway.
297 0         0 $_->{colgroup}= undef for $coltrack->{$col}{members}->@*;
298             }
299 4         13 delete $coltrack->{$col};
300             }
301              
302 28     28   8114 sub _parse_code_block($self, $text, $file=undef, $orig_line=undef) {
  28         42  
  28         47  
  28         35  
  28         49  
  28         37  
303 28 100       111 $text .= "\n" unless substr($text,-1) eq "\n";
304 28 50       108 if ($text =~ /^# line (\d+) "([^"]+)"/) {
305 0         0 $orig_line= $1-1;
306 0         0 $file= $2;
307             }
308 28   100     78 local our $line= $orig_line || 1;
309 28         56 local our $parse= $self->{cpppp_parse};
310 28         40 local our $start;
311 28         53 local our @subst;
312             # Everything in coltrack that survived the last _parse_code_block call
313             # ended on the final line of the template. Set the line numbers to
314             # continue into this template.
315 28         134 for my $c (grep looks_like_number($_), keys $parse->{coltrack}->%*) {
316 6         12 $parse->{coltrack}{$c}{line}= $line;
317             }
318 28         53 local $_= $text;
319             # Parse and record the locations of the embedded perl statements
320 28         135 ()= m{
321             # Rough approximation of continuation of perl expressions in quoted strings
322             (?(DEFINE)
323             (? (?>
324             \{ (?&BALANCED_EXPR) \}
325             | \[ (?&BALANCED_EXPR) \]
326             | \( (?&BALANCED_EXPR) \)
327             | [^[\](){}\n]+
328 0         0 | \n (?{ $line++ })
329             )* )
330             )
331            
332             # Start of a perl expression in a quoted string
333 58         205 [\$\@] (?{ $start= -1+pos })
334             (?:
335             \{ (?&BALANCED_EXPR) \} #
336             | [\w_]+ # plain variable
337             (?: # maybe followed by ->[] or similar
338             (?: -> )?
339             (?: \{ (?&BALANCED_EXPR) \} | \[ (?&BALANCED_EXPR) \] )
340             ) *
341 58         339 ) (?{ push @subst, { pos => $start, len => -$start+pos, line => $line }; })
342            
343             # Track what line we're on
344 91         228 | \n (?{ $line++ })
345            
346             # Column alignment detection for the autocolumn feature
347 1345         2425 | (?{ $start= pos; }) [ \t]{2,}+ (?{
348 55         305 push @subst, { pos => pos, len => 0, line => $line, colgroup => undef };
349             })
350             }xg;
351            
352 28         54 my $prev_eval;
353 28         89 for (0..$#subst) {
354 113         218 my $s= $subst[$_];
355 113 100       213 if (exists $s->{colgroup}) {
356 55         152 my $linestart= (rindex($text, "\n", $s->{pos})+1);
357 55         86 my $col= $s->{pos} - $linestart;
358 55   100     187 $s->{follows_eval}= $prev_eval && $prev_eval->{line} == $s->{line};
359             # If same column as previous line, continue the coltracking.
360 55 100       127 if ($parse->{coltrack}{$col}) {
361 10 50       39 if ($parse->{coltrack}{$col}{members}[-1]{line} == $s->{line} - 1) {
362 10         23 push @{ $parse->{coltrack}{$col}{members} }, $s;
  10         28  
363 10         46 $s->{colgroup}= $parse->{coltrack}{$col}{id};
364 10         16 $parse->{coltrack}{$col}{line}= $s->{line};
365 10         38 next;
366             }
367             # column ended prior to this
368 0         0 _finish_coltrack($parse->{coltrack}, $col);
369             }
370             # There's no need to create a column unless nonspace to the left
371             # Otherwise it would just be normal indent.
372 45 100       176 if (substr($text, $linestart, $s->{pos} - $linestart) =~ /\S/) {
373             # new column begins
374 5         22 $s->{colgroup}= $col*10000 + ++$parse->{coltrack}{next_id}{$col};
375 5         9 $s->{first}= 1;
376             $parse->{coltrack}{$col}= {
377             id => $s->{colgroup},
378             line => $s->{line},
379 5         24 members => [ $s ],
380             };
381             }
382             }
383             else { # Perl expression
384 58         143 my $expr= substr($text, $s->{pos}, $s->{len});
385             # Special case: ${{ }} notation is a shortcut for @{[do{ ... }]}
386 58         123 $expr =~ s/^ \$\{\{ (.*) \}\} $/$1/x;
387             # When not inside a string, ${foo} becomes ambiguous with ${foo()}
388 58         148 $expr =~ s/^ ([\$\@]) \{ ([\w_]+) \} /$1$2/x;
389 58         111 $s->{eval}= $expr;
390 58         107 $prev_eval= $s;
391             }
392             }
393             # cleanup
394 28         118 for my $c (grep looks_like_number($_), keys $parse->{coltrack}->%*) {
395 11 100       34 if ($parse->{coltrack}{$c}{line} < $line-1) {
396 3         8 _finish_coltrack($parse->{coltrack}, $c);
397             }
398             }
399 28   100     248 @subst= grep defined $_->{eval} || defined $_->{colgroup}, @subst;
400            
401 28         166 { text => $text, subst => \@subst, file => $file }
402             }
403              
404              
405 0     0 1 0 sub patch_file($self, $fname, $patch_markers, $new_content) {
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
406 0 0       0 $new_content .= "\n" unless $new_content =~ /\n\Z/;
407 0         0 utf8::encode($new_content);
408 0 0       0 open my $fh, '+<', $fname or die "open($fname): $!";
409 0         0 my $content= do { local $/= undef; <$fh> };
  0         0  
  0         0  
410 0 0       0 $content =~ s{(BEGIN \Q$patch_markers\E[^\n]*\n).*?(^[^\n]+?END \Q$patch_markers\E)}
411             {$1$new_content$2}sm
412 0 0       0 or croak "Can't find $patch_markers in $fname";
413 0 0       0 $fh->seek(0,0) or die "seek: $!";
414 0 0       0 $fh->print($content) or die "write: $!";
415 0 0       0 $fh->truncate($fh->tell) or die "truncate: $!";
416 0         0 $fh->close or die "close: $!";
417             $self;
418             }
419 0     0 0 0  
  0         0  
  0         0  
  0         0  
  0         0  
420 0 0       0 sub overwrite_file_with_backup($self, $fname, $new_content) {
421 0         0 $new_content .= "\n" unless $new_content =~ /\n\Z/;
422 0 0       0 utf8::encode($new_content);
423 0         0 if (-e $fname) {
424 0         0 my $n= 0;
425 0         0 ++$n while -e "$fname.$n";
426 0 0       0 require File::Copy;
427             File::Copy::copy($fname, "$fname.$n") or die "copy($fname, $fname.$n): $!";
428 0 0       0 }
429 0 0       0 open my $fh, '>', $fname or die "open($fname): $!";
430 0 0       0 $fh->print($new_content) or die "write: $!";
431 0         0 $fh->close or die "close: $!";
432             $self;
433             }
434 0     0 0 0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
435 0         0 sub write_sections_to_file($self, $sections, $fname, $patch_markers=undef) {
436 0 0       0 my $content= $self->output->get($sections);
437 0         0 if (defined $patch_markers) {
438             $self->patch_file($fname, $patch_markers, $content);
439 0         0 } else {
440             $self->overwrite_file_with_backup($fname, $content);
441             }
442             }
443 0     0   0  
  0         0  
  0         0  
  0         0  
444 0 0       0 sub _slurp_file($self, $fname) {
445 0         0 open my $fh, '<', $fname or die "open($fname): $!";
  0         0  
  0         0  
446 0 0       0 my $content= do { local $/= undef; <$fh> };
447 0         0 $fh->close or die "close: $!";
448             $content;
449             }
450              
451             1;
452              
453             __END__