File Coverage

blib/lib/CodeGen/Cpppp.pm
Criterion Covered Total %
statement 157 179 87.7
branch 38 66 57.5
condition 7 10 70.0
subroutine 13 14 92.8
pod 2 3 66.6
total 217 272 79.7


line stmt bran cond sub pod time code
1             package CodeGen::Cpppp;
2 3     3   693467 use strict;
  3         19  
  3         89  
3 3     3   20 use warnings;
  3         4  
  3         68  
4 3     3   15 use Carp;
  3         4  
  3         8107  
5              
6             our $VERSION = '0.001_02'; # TRIAL VERSION
7             # ABSTRACT: The C Perl-Powered Pre-Processor
8              
9              
10              
11             sub new {
12 3     3 0 275 my ($class, %attrs)= @_;
13 3         9 $attrs{code_block_templates}= [];
14 3         14 bless \%attrs, $class;
15             }
16              
17              
18             sub compile_template {
19 1     1 1 20 my ($self, $in, $filename, $line)= @_;
20 1         5 my $perl= $self->_translate_cpppp($in, $filename, $line);
21 1         6 $perl= "package CodeGen::Cpppp::Tmp::0;\n"
22             ."use strict; use warnings;\n"
23             ."sub { my \$self= shift;\n"
24             ."$perl;\n"
25             ."}\n";
26 1 50   1   89 unless ($self->{fn}= eval $perl) {
  1         6  
  1         2  
  1         197  
27 0         0 print $perl;
28 0         0 die $@;
29             }
30 1         22 return $self;
31             }
32              
33              
34             sub render {
35 1     1 1 2 my $self= shift;
36 1         3 $self->{out}= '';
37 1         31 $self->{fn}->($self);
38 1         5 return $self->{out};
39             }
40              
41             sub _translate_cpppp {
42 4     4   7568 my ($self, $in, $filename, $line)= @_;
43 4 50       17 my $line_ofs= $line? $line - 1 : 0;
44 4 50       15 if (ref $in eq 'SCALAR') {
45 4         8 my $tmp= $in;
46 4 50       14 utf8::encode($tmp) if utf8::is_utf8($tmp);
47 4         9 undef $in;
48 4 50   3   82 open($in, '<', $tmp) or die;
  3         20  
  3         5  
  3         49  
49 4 50       1528 defined $in or die;
50             }
51 4         10 my ($perl, $tpl_start_line, $cur_tpl);
52             my $end_tpl= sub {
53 3     3   15 my $parsed= $self->_parse_code_block($cur_tpl, $filename, $tpl_start_line);
54 3         13 $perl .= '$self->_render_code_block('.$self->_generate_code_block_perl($parsed).");\n";
55 3         9 $cur_tpl= undef;
56 4         23 };
57 4         24 while (<$in>) {
58 16 50       72 if (/^#!/) { # ignore #!
    100          
    50          
59             }
60             elsif (/^##(?!#)/) { # full-line of perl code
61 7 100       17 if (defined $cur_tpl) {
62 2         7 &$end_tpl;
63 2         21 $perl .= '# line '.($.+$line_ofs).qq{ "$filename"\n};
64             }
65 7 100       24 if (!defined $perl) {
66 3         21 $perl= '# line '.($.+$line_ofs).qq{ "$filename"\n};
67             }
68 7         30 s/^##\s*//;
69 7         22 my $pl= $_;
70 7 50       22 if ($pl =~ /sub \w+ \s* \( ( [^,)\n]* )/x) {
71 0 0       0 if ($1 ne '$self') {
72 0 0       0 substr($pl, $-[1], $+[1]-$-[1], '$self'.(length $1? ', '.$1 : ''));
73             }
74             }
75 7         44 $perl .= $pl;
76             }
77             elsif (/^(.*?) ## ?((?:if|unless) .*)/) { # perl conditional suffix, half tpl/half perl
78 0         0 my ($tpl, $pl)= ($1, $2);
79 0 0       0 &$end_tpl if defined $cur_tpl;
80 0         0 $tpl_start_line= $. + $line_ofs;
81 0         0 $cur_tpl= $tpl;
82 0         0 &$end_tpl;
83 0         0 $perl =~ s/;\s*$//; # remove semicolon
84 0 0       0 $pl .= ';' unless $pl =~ /;\s*$/; # re-add it if user didn't
85 0         0 $perl .= qq{\n# line }.($.+$line_ofs).qq{ "$filename"\n $pl\n};
86             }
87             else { # default is to assume a line of template
88 9 100       18 if (!defined $cur_tpl) {
89 3         8 $tpl_start_line= $. + $line_ofs;
90 3         14 $cur_tpl= '';
91             }
92 9         30 $cur_tpl .= $_;
93             }
94             }
95 4 100       12 &$end_tpl if defined $cur_tpl;
96 4         41 $perl;
97             }
98              
99             sub _generate_code_block_perl {
100 3     3   8 my ($self, $parsed)= @_;
101 3         5 my $n= @{$self->{code_block_templates}};
  3         16  
102 3         5 push @{$self->{code_block_templates}}, $parsed;
  3         9  
103 3         7 my $code= 'do { my @expr_subs;'."\n";
104 3         14 for (0 .. $#{$parsed->{subst}}) {
  3         13  
105 9         16 my $s= $parsed->{subst}[$_];
106 9 100       27 if ($s->{len}) {
107 6         14 my $expr= substr($parsed->{text}, $s->{pos}, $s->{len});
108 6 50       16 if ($expr eq '$anticomma') {
109             # Special case: trim out the previous comma, skipping over whitespace
110 0     0   0 $s->{fn}= sub { ${$_[1]} =~ s/,(\s*)/$1/; '' };
  0         0  
  0         0  
  0         0  
111             }
112             else {
113             # Notation ${{ ... }} is an extension that means "run this literal perl"
114 6 100       21 if ($expr =~ /\$\{\{(.*)\}\}$/) {
115 2         6 $expr= $1;
116             }
117             $code .= join "\n",
118             ' $expr_subs['.$_.']= sub { my $self= shift;',
119 6         39 '# line '.$s->{line}.' "'.$parsed->{file}.'"',
120             ' '.$expr,
121             " };\n";
122             }
123             }
124             }
125 3         30 return $code.'($self->{code_block_templates}['.$n."], \\\@expr_subs)\n}";
126             }
127              
128             sub _parse_code_block {
129 5     5   8086 my ($self, $text, $file, $orig_line)= @_;
130 5 50       28 $text .= "\n" unless substr($text,-1) eq "\n";
131 5 50       22 if ($text =~ /^# line (\d+) "([^"]+)"/) {
132 0         0 $orig_line= $1-1;
133 0         0 $file= $2;
134             }
135 5   100     23 local our $line= $orig_line || 1;
136 5         10 local our $start;
137 5         11 local our @subst;
138 5         9 local $_= $text;
139             # Parse and record the locations of the embedded perl statements
140 5         43 ()= m{
141             (?(DEFINE)
142             (? (?>
143             \{ (?&BALANCED_EXPR) \}
144             | \[ (?&BALANCED_EXPR) \]
145             | \( (?&BALANCED_EXPR) \)
146             | [^[\](){}\n]+
147 0         0 | \n (?{ $line++ })
148             )* )
149             )
150 9         48 [\$\@] (?{ $start= -1+pos })
151             (?:
152             \{ (?&BALANCED_EXPR) \} #
153             | [\w_]+ # plain variable
154             (?: # maybe followed by ->[] or similar
155             (?: -> )?
156             (?: \{ (?&BALANCED_EXPR) \} | \[ (?&BALANCED_EXPR) \] )
157             ) *
158 9         62 ) (?{ push @subst, { pos => $start, len => -$start+pos, line => $line };
159            
160             })
161 14         54 | \n (?{ $line++ })
162             }xg;
163             # Detect columns. Look for any location where two spaces occur.
164 5         13 local our %cols;
165 5         13 local our $linestart= 0;
166 5   100     18 $line= $orig_line || 1;
167 5         15 pos= 0;
168 5         114 while (m{\G(?>
169 14         25 \n (?{ ++$line; $linestart= pos })
  14         60  
170 8         14 | [ ][ ]+ (?{ push @{$cols{-$linestart + pos}}, { pos => pos, len => 0, line => $line } })
  8         131  
171             | .
172             )}xcg) {}
173 5 50       17 warn "BUG: failed to parse columns" unless pos == length($text);
174             # Delete all column markers that occur inside of code substitutions
175 5         14 for my $s (@subst) {
176 9   66     51 for my $col (grep $_ > $s->{pos} && $_ < $s->{pos} + $s->{len}, keys %cols) {
177 1         3 my $markers= $cols{$col};
178             @$markers= grep $_->{pos} > $s->{pos}+$s->{len} || $_->{pos} < $s->{pos},
179 1   33     10 @$markers;
180             }
181             }
182             # Detect the actual columns from the remaining markers
183 5         10 my $colgroup= 0;
184 5         20 for my $col (sort { $a <=> $b } keys %cols) {
  1         4  
185             # Find out which column markers are from adjacent lines
186 5         11 my $lines= $cols{$col};
187 5         16 my @adjacent= [ $lines->[0] ];
188 5         22 for (1..$#$lines) {
189 3 50       32 if ($adjacent[-1][-1]{line} + 1 == $lines->[$_]{line}) {
190 3         9 push @{ $adjacent[-1] }, $lines->[$_];
  3         21  
191             } else {
192 0         0 push @adjacent, [ $lines->[$_] ];
193             }
194             }
195             # Need at least 2 adjacent lines to count as a colum.
196 5         27 for (grep @$_ > 1, @adjacent) {
197             # At least one of the lines must have text to the left of it
198 2         6 my $has_left= 0;
199 2         6 for (@$_) {
200 2         11 my $linestart= rindex($text, "\n", $_->{pos})+1;
201 2 50       17 if (substr($text, $linestart, $_->{pos}-$linestart) =~ /\S/) {
202 2         4 $has_left= 1;
203 2         5 last;
204             }
205             }
206 2 50       9 next unless $has_left;
207             # this is a new linked column group
208 2         4 ++$colgroup;
209             # add one column marker per line in this group
210 2         19 push @subst, map +{ colgroup => $colgroup, pos => $_->{pos}, len => 0, line => $_->{line} }, @$_;
211             }
212             }
213             # Now merge the column markers into the substitutions in string order
214 5         20 @subst= sort { $a->{pos} <=> $b->{pos} } @subst;
  18         42  
215            
216 5         42 { text => $text, subst => \@subst, file => $file }
217             }
218              
219             sub _render_code_block {
220 3     3   49 my ($self, $block, $expr_subs)= @_;
221 3         7 my $text= $block->{text};
222 3         5 my $newtext= '';
223 3         5 my $at= 0;
224 3         4 my %colpos;
225             # First pass, perform substitutions and record new column markers
226 3         16 for my $i (0..$#{$block->{subst}}) {
  3         11  
227 21         32 my $s= $block->{subst}[$i];
228 21 100       37 if ($s->{colgroup}) {
229 9         12 push @{$colpos{$s->{colgroup}}}, length($newtext) + $s->{pos} - $at;
  9         23  
230             }
231             else {
232 12         26 $newtext .= substr($text, $at, $s->{pos} - $at);
233 12         14 my $fn= $expr_subs->[$i];
234 12 50       62 if ($fn) {
235 12         28 $newtext .= $fn->($self, \$newtext);
236             }
237 12         54 $at= $s->{pos} + $s->{len};
238             }
239             }
240 3         8 $text= $newtext . substr($text, $at);
241             # Second pass, adjust whitespace of all column markers so they line up.
242             # Iterate from leftmost column rightward.
243 3         10 for my $group_i (sort { $a <=> $b } keys %colpos) {
  0         0  
244 3         6 my $group= $colpos{$group_i};
245             # Find the longest prefix (excluding trailing whitespace)
246 3         4 my $newcol= 0;
247 3         6 for (@$group) {
248 9         16 my $linestart= rindex($text, "\n", $_)+1;
249 9         35 substr($text, $linestart, $_-$linestart) =~ /(.*? ) *$/;
250 9         19 my $l= length($1);
251 9 100       20 $newcol= $l if $l > $newcol;
252             }
253             # Now update them all to that common length, but after each update
254             # need to update all other positions by the amount changed because the
255             # source string is changing.
256 3         10 @$group= sort { $a <=> $b } @$group;
  9         17  
257 3         11 for (my $i= 0; $i < @$group; $i++) {
258 9         15 my $linestart= rindex($text, "\n", $group->[$i])+1;
259 9         15 my $oldcol= $group->[$i] - $linestart;
260 9         12 my $diff= $newcol - $oldcol;
261 9 50       16 if ($diff < 0) {
    0          
262 9         16 substr($text, $linestart + $newcol, -$diff, '');
263             } elsif($diff > 0) {
264 0         0 substr($text, $linestart + $oldcol, 0, ' 'x$diff);
265             }
266             # update all positions beyond this one
267 9         17 for (values %colpos) {
268 9         13 for (@$_) {
269 27 100       69 $_ += $diff if $_ > $group->[$i];
270             }
271             }
272             }
273             }
274 3         25 $self->{out} .= $text;
275             }
276              
277             1;
278              
279             =pod
280              
281             =encoding UTF-8
282              
283             =head1 NAME
284              
285             CodeGen::Cpppp - The C Perl-Powered Pre-Processor
286              
287             =head1 VERSION
288              
289             version 0.001_02
290              
291             =head1 SYNOPSIS
292              
293             I
294              
295             I. You see, most
296             blokes gonna be templating with C or C, you're on C here all the way up,
297             all the way up, Where can you go from there? Where?>
298              
299             I
300              
301             I
302              
303             I, exactly.>
304              
305             I.>
306              
307             B
308              
309             #! /usr/bin/env cpppp
310             ## for (my $bits= 8; $bits <= 32; $bits <<= 1) {
311             struct tree_node_$bits {
312             uint${bits}_t left: ${{$bits-1}},
313             color: 1,
314             right: ${{$bits-1}};
315             };
316             ## }
317              
318             B
319              
320             struct tree_node_8 {
321             uint8_t left: 7,
322             right: 7,
323             color: 1;
324             };
325             struct tree_node_16 {
326             uint16_t left: 15,
327             right: 15,
328             color: 1;
329             };
330             struct tree_node_32 {
331             uint32_t left: 31,
332             right: 31,
333             color: 1;
334             };
335              
336             =head1 DESCRIPTION
337              
338             B.
339              
340             This module is a preprocessor for C,
341              
342             If you have an interest in this, contact me, because I could use help brainstorming ideas
343             about how to accommodate the most possibilities, here.
344              
345             Possibilities:
346              
347             =over
348              
349             =item *
350              
351             Scan existing headers to discover available macros, structs, and functions on the host.
352              
353             =item *
354              
355             Pass a list of headers through the real cpp and analyze the macro output.
356              
357             =item *
358              
359             Shell out to a compiler to find 'sizeof' information for structs.
360              
361             =head1 CONSTRUCTOR
362              
363             Bare-bones for now, it accepts whatever hash values you hand to it.
364              
365             =head1 METHODS
366              
367             =head2 compile_template
368              
369             $cpppp->compile_template($input_fh, $filename);
370             $cpppp->compile_template(\$scalar_tpl, $filename, $line_offset);
371              
372             This reads the input file handle (or scalar-ref) and builds a perl subroutine out of it, then
373             evals that subroutine so it is ready to run (and spits out any compile errors in the template).
374              
375             =head2 render
376              
377             $cpppp->render();
378              
379             Execute the template previously compiled. Passing arguments to this template is a TODO item.
380              
381             =head1 AUTHOR
382              
383             Michael Conrad
384              
385             =head1 COPYRIGHT AND LICENSE
386              
387             This software is copyright (c) 2023 by Michael Conrad.
388              
389             This is free software; you can redistribute it and/or modify it under
390             the same terms as the Perl 5 programming language system itself.
391              
392             =cut
393              
394             __END__