File Coverage

blib/lib/CodeGen/Cpppp.pm
Criterion Covered Total %
statement 274 290 94.4
branch 44 68 64.7
condition 12 15 80.0
subroutine 57 60 95.0
pod 1 4 25.0
total 388 437 88.7


line stmt bran cond sub pod time code
1             package CodeGen::Cpppp;
2 3     3   723300 use v5.20;
  3         24  
3 3     3   37 use warnings;
  3         7  
  3         73  
4 3     3   16 use Carp;
  3         7  
  3         8042  
5              
6             our $VERSION = '0.001_03'; # TRIAL VERSION
7             # ABSTRACT: The C Perl-Powered Pre-Processor
8              
9              
10             sub new {
11 3     3 0 312 my ($class, %attrs)= @_;
12 3         29 bless \%attrs, $class;
13             }
14              
15              
16             our $next_pkg= 1;
17             sub compile_template {
18 7     7 1 9475 my ($self, $in, $filename, $line)= @_;
19 7         20 my $parse= $self->_parse_cpppp($in, $filename, $line);
20 7         20 my $pkg= 'CodeGen::Cpppp::_Template'.$next_pkg++;
21 7         29 my $perl= "package $pkg;\n"
22             ."use v5.20;\n"
23             ."use warnings;\n"
24             ."no warnings 'experimental::lexical_subs', 'experimental::signatures';\n"
25             ."use feature 'lexical_subs', 'signatures';\n"
26             ."use CodeGen::Cpppp::Template -setup;\n"
27             ."sub process(\$self) {\n"
28             ."$parse->{code};\n"
29             ."}\n"
30             ."1\n";
31 7 50   1   668 unless (eval $perl) {
  1     1   2  
  1     1   59  
  1     1   31  
  1     1   3  
  1     1   95  
  1     1   9  
  1     1   2  
  1     1   204  
  1     1   8  
  1     1   2  
  1     1   8  
  1     1   14  
  1     1   10  
  1     1   8  
  1     1   2  
  1     1   33  
  1     1   17  
  1     1   4  
  1     1   49  
  1     1   7  
  1     1   2  
  1     1   109  
  1     1   9  
  1     1   1  
  1     1   7  
  1     1   13  
  1     1   15  
  1     1   6  
  1     1   2  
  1     1   33  
  1     1   35  
  1     1   2  
  1     1   38  
  1         5  
  1         3  
  1         103  
  1         8  
  1         2  
  1         6  
  1         13  
  1         6  
  1         7  
  1         2  
  1         30  
  1         35  
  1         3  
  1         44  
  1         6  
  1         12  
  1         131  
  1         8  
  1         5  
  1         6  
  1         13  
  1         3  
  1         7  
  1         2  
  1         30  
  1         6  
  1         1  
  1         65  
  1         8  
  1         2  
  1         96  
  1         9  
  1         2  
  1         6  
  1         12  
  1         4  
  1         7  
  1         2  
  1         32  
  1         5  
  1         2  
  1         64  
  1         7  
  1         2  
  1         126  
  1         9  
  1         2  
  1         6  
  1         13  
  1         4  
  1         7  
  1         3  
  1         44  
  1         7  
  1         1  
  1         34  
  1         6  
  1         2  
  1         93  
  1         17  
  1         2  
  1         7  
32 0         0 my $err= "$@";
33 0         0 STDERR->print($perl);
34 0         0 die $err;
35             }
36 7         151 $pkg->_set_parse_data($parse);
37 7         35 return $pkg;
38             }
39              
40             sub _parse_cpppp {
41 11     11   7441 my ($self, $in, $filename, $line)= @_;
42 11 50       34 my $line_ofs= $line? $line - 1 : 0;
43 11 50       39 if (ref $in eq 'SCALAR') {
44 10         14 my $tmp= $in;
45 10 50       33 utf8::encode($tmp) if utf8::is_utf8($tmp);
46 10         20 undef $in;
47 10 50   3   191 open($in, '<', $tmp) or die;
  3         26  
  3         11  
  3         28  
48 10 50       1777 defined $in or die;
49             }
50             $self->{cpppp_parse}= {
51 10         60 autocomma => 1,
52             autostatementline => 1,
53             autoindent => 1,
54             };
55 11         25 my ($perl, $tpl_start_line, $cur_tpl);
56             my $end_tpl= sub {
57 11 50   11   41 if ($cur_tpl =~ /\S/) {
58 10         32 my $parsed= $self->_parse_code_block($cur_tpl, $filename, $tpl_start_line);
59 10         28 $perl .= $self->_gen_perl_call_code_block($parsed);
60             }
61 10         30 $cur_tpl= undef;
62 11         75 };
63 10         47 while (<$in>) {
64 44 50       179 if (/^#!/) { # ignore #!
    100          
    50          
65             }
66             elsif (/^##(?!#)/) { # full-line of perl code
67 23 100       64 if (defined $cur_tpl) {
    100          
68 5         23 &$end_tpl;
69 5         22 $perl .= '# line '.($.+$line_ofs).qq{ "$filename"\n};
70             }
71             elsif (!defined $perl) {
72 9         45 $perl= '# line '.($.+$line_ofs).qq{ "$filename"\n};
73             }
74 22         88 s/^##\s?//;
75 22         46 my $pl= $_;
76 22         50 $perl .= $self->_process_template_perl($pl);
77             }
78             elsif (/^(.*?) ## ?((?:if|unless) .*)/) { # perl conditional suffix, half tpl/half perl
79 0         0 my ($tpl, $pl)= ($1, $2);
80 0 0       0 &$end_tpl if defined $cur_tpl;
81 1         2 $tpl_start_line= $. + $line_ofs;
82 1         2 $cur_tpl= $tpl;
83 1         2 &$end_tpl;
84 0         0 $perl =~ s/;\s*$//; # remove semicolon
85 0 0       0 $pl .= ';' unless $pl =~ /;\s*$/; # re-add it if user didn't
86 0         0 $perl .= qq{\n# line }.($.+$line_ofs).qq{ "$filename"\n $pl\n};
87             }
88             else { # default is to assume a line of template
89 23 100       47 if (!defined $cur_tpl) {
90 11         22 $tpl_start_line= $. + $line_ofs;
91 11         20 $cur_tpl= '';
92             }
93 22         61 $cur_tpl .= $_;
94             }
95             }
96 10 100       31 &$end_tpl if defined $cur_tpl;
97 10         25 $self->{cpppp_parse}{code}= $perl;
98 10         81 delete $self->{cpppp_parse};
99             }
100              
101             sub _process_template_perl {
102 22     23   45 my ($self, $pl)= @_;
103             # If user declares "sub NAME(", convert that to "my sub NAME" so that we
104             # can grab a ref to it later.
105 22 100       71 if ($pl =~ /\b sub \s* (\w+) \s* \(/x) {
106 1         2 push @{$self->{cpppp_parse}{named_subs}}, $1;
  2         9  
107             # look backward and see if it already started with 'my'
108 2         7 my $pos= rindex($pl, "my", $-[0]);
109 2 50       7 if ($pos == -1) {
110 1         5 substr($pl, $-[0], 0, 'my ');
111             }
112             }
113             # If user declares "##define name(", convert that to both a method and a define
114 22 50       48 if ($pl =~ /\b define \s* (\w+) (\s*) \(/x) {
115 0         0 push @{$self->{cpppp_parse}{named_subs}}, $1;
  0         0  
116 0         0 substr($pl, $-[2], $+[2]-$-[2], '=> \$self->{define}{'.$1.'}; my sub '.$1);
117             }
118 22         100 $pl;
119             }
120              
121             sub _gen_perl_call_code_block {
122 10     11   19 my ($self, $parsed)= @_;
123 10   100     51 my $codeblocks= $self->{cpppp_parse}{code_block_templates} ||= [];
124 10         21 push @$codeblocks, $parsed;
125 10         55 my $code= '$self->_render_code_block('.$#$codeblocks;
126 10         22 my %cache;
127 10         15 my $i= 0;
128 10         17 my $cur_line= 0;
129 10         15 for my $s (@{$parsed->{subst}}) {
  10         25  
130 28 100       67 if (defined $s->{eval}) {
131             # No need to create more than one anonsub for the same expression
132 20 100       45 if (defined $cache{$s->{eval}}) {
133 4         9 $s->{eval_idx}= $cache{$s->{eval}};
134 4         9 next;
135             }
136 16         55 $cache{$s->{eval}}= $s->{eval_idx}= $i++;
137 16 50       62 my $sig= $s->{eval} =~ /self|output/? '($self, $output)' : '';
138 16 100       61 if ($s->{line} == $cur_line) {
    100          
139 4         13 $code .= qq{, sub${sig}{ $s->{eval} }};
140             } elsif ($s->{line} == $cur_line+1) {
141 3         4 $cur_line++;
142 3         13 $code .= qq{,\n sub${sig}{ $s->{eval} }};
143             } else {
144 9         32 $code .= qq{,\n# line $s->{line} "$parsed->{file}"\n sub${sig}{ $s->{eval} }};
145 9         16 $cur_line= $s->{line};
146 9         30 $cur_line++ for $s->{eval} =~ /\n/g;
147             }
148             }
149             }
150 10         45 $code .");\n";
151             }
152              
153             sub _parse_code_block {
154 12     13   8812 my ($self, $text, $file, $orig_line)= @_;
155 12 50       46 $text .= "\n" unless substr($text,-1) eq "\n";
156 12 50       41 if ($text =~ /^# line (\d+) "([^"]+)"/) {
157 0         0 $orig_line= $1-1;
158 0         0 $file= $2;
159             }
160 12   100     36 local our $line= $orig_line || 1;
161 12         16 local our $start;
162 12         41 local our @subst;
163 12         29 local $_= $text;
164             # Parse and record the locations of the embedded perl statements
165 12         76 ()= m{
166             (?(DEFINE)
167             (? (?>
168             \{ (?&BALANCED_EXPR) \}
169             | \[ (?&BALANCED_EXPR) \]
170             | \( (?&BALANCED_EXPR) \)
171             | [^[\](){}\n]+
172 0         0 | \n (?{ $line++ })
173             )* )
174             )
175 24         114 [\$\@] (?{ $start= -1+pos })
176             (?:
177             \{ (?&BALANCED_EXPR) \} #
178             | [\w_]+ # plain variable
179             (?: # maybe followed by ->[] or similar
180             (?: -> )?
181             (?: \{ (?&BALANCED_EXPR) \} | \[ (?&BALANCED_EXPR) \] )
182             ) *
183 24         152 ) (?{ push @subst, { pos => $start, len => -$start+pos, line => $line };
184            
185             })
186 27         87 | \n (?{ $line++ })
187             }xg;
188            
189 12         58 for (0..$#subst) {
190 24         40 my $s= $subst[$_];
191             # Special cases
192 24         59 my $expr= substr($text, $s->{pos}, $s->{len});
193 24 100       77 if ($expr eq '$trim_comma') {
    100          
194             # Modify the text being created to remove the final comma
195 1     2   15 $s->{fn}= sub { ${$_[1]} =~ s/,(\s*)$/$1/; '' };
  1         9  
  1         12  
  1         4  
196             } elsif ($expr =~ /^ \$\{\{ (.*) \}\} $/x) {
197             # Notation ${{ ... }} is a shortcut for @{[do{ ... }]}
198 6         24 $s->{eval}= $1;
199             } else {
200 17         44 $s->{eval}= $expr; # Will need to be filled in with a coderef
201             }
202             }
203             # Detect columns. Look for any location where two spaces occur.
204 12         27 local our %cols;
205 12         21 local our $linestart= 0;
206 12   100     29 $line= $orig_line || 1;
207 12         31 pos= 0;
208 12         251 while (m{\G(?>
209 27         48 \n (?{ ++$line; $linestart= pos })
  27         138  
210 14         23 | [ ][ ]+ (?{ push @{$cols{-$linestart + pos}}, { pos => pos, len => 0, line => $line } })
  14         227  
211             | .
212             )}xcg) {}
213 12 50       46 warn "BUG: failed to parse columns" unless pos == length($text);
214             # Delete all column markers that occur inside of code substitutions
215 12         31 for my $s (@subst) {
216 24   100     112 for my $col (grep $_ > $s->{pos} && $_ < $s->{pos} + $s->{len}, keys %cols) {
217 1         5 my $markers= $cols{$col};
218             @$markers= grep $_->{pos} > $s->{pos}+$s->{len} || $_->{pos} < $s->{pos},
219 1   33     9 @$markers;
220             }
221             }
222             # Detect the actual columns from the remaining markers
223 12         22 my $colgroup= 0;
224 12         42 for my $col (sort { $a <=> $b } keys %cols) {
  2         9  
225             # Find out which column markers are from adjacent lines
226 9         18 my $lines= $cols{$col};
227 9         37 my @adjacent= [ $lines->[0] ];
228 9         29 for (1..$#$lines) {
229 5 50       26 if ($adjacent[-1][-1]{line} + 1 == $lines->[$_]{line}) {
230 5         9 push @{ $adjacent[-1] }, $lines->[$_];
  5         20  
231             } else {
232 0         0 push @adjacent, [ $lines->[$_] ];
233             }
234             }
235             # Need at least 2 adjacent lines to count as a colum.
236 9         36 for (grep @$_ > 1, @adjacent) {
237             # At least one of the lines must have text to the left of it
238 4         11 my $has_left= 0;
239 4         10 for (@$_) {
240 4         18 my $linestart= rindex($text, "\n", $_->{pos})+1;
241 4 50       24 if (substr($text, $linestart, $_->{pos}-$linestart) =~ /\S/) {
242 4         8 $has_left= 1;
243 4         8 last;
244             }
245             }
246 4 50       11 next unless $has_left;
247             # this is a new linked column group
248 4         8 ++$colgroup;
249             # add one column marker per line in this group
250 4         36 push @subst, map +{ colgroup => $colgroup, pos => $_->{pos}, len => 0, line => $_->{line} }, @$_;
251             }
252             }
253             # Now merge the column markers into the substitutions in string order
254 12 50       45 @subst= sort { $a->{pos} <=> $b->{pos} or $a->{len} <=> $b->{len} } @subst;
  40         92  
255            
256 12         93 { text => $text, subst => \@subst, file => $file }
257             }
258              
259             package CodeGen::Cpppp::Template;
260             $INC{'CodeGen/Cpppp/Template.pm'}= 1;
261 3     3   47 use v5.20;
  3         13  
262 3     3   24 use warnings;
  3         6  
  3         145  
263 3     3   19 use Carp;
  3         8  
  3         288  
264              
265             sub import {
266 7     7   17 my $class= shift;
267 7         16 my $caller= caller;
268 7         21 for (@_) {
269 7 50       25 if ($_ eq '-setup') {
270 3     3   24 no strict 'refs';
  3         6  
  3         427  
271 7         27 push @{$caller.'::ISA'}, $class;
  7         1235  
272 0         0 } else { croak "$class does not export $_" }
273             }
274             }
275              
276             sub _set_parse_data {
277 7     7   22 my ($class, $parse)= @_;
278 3     3   23 no strict 'refs';
  3         5  
  3         238  
279 7         12 ${$class.'::_parse_data'}= $parse;
  7         35  
280             }
281              
282             sub new {
283 7     7 0 14 my $class= shift;
284 3     3   21 no strict 'refs';
  3         22  
  3         3451  
285             bless {
286 7         13 %{${$class.'::_parse_data'}},
  7         7  
  7         92  
287             out => {
288             public => '',
289             protected => '',
290             private => '',
291             decl => '',
292             impl => '',
293             }
294             }, $class;
295             }
296              
297             sub render {
298 7     7 0 13 my $self= shift;
299 7   66     243 $self->{process_result} //= $self->process;
300 7         37 return $self->{out}{impl};
301             }
302              
303             sub _render_code_block {
304             my ($self, $i, @expr_subs)= @_;
305             my $block= $self->{code_block_templates}[$i];
306             my $text= $block->{text};
307             my $newtext= '';
308             my $at= 0;
309             my %colmarker;
310             my $prev_colmark;
311             # First pass, perform substitutions and record new column markers
312             my sub str_esc{ join '', map +(ord($_) > 0x7e || ord($_) < 0x21? sprintf("\\x{%X}",ord) : $_), split //, $_[0] }
313             for my $s (@{$block->{subst}}) {
314             $newtext .= substr($text, $at, $s->{pos} - $at);
315             if ($s->{colgroup}) {
316             my $mark= $colmarker{$s->{colgroup}} //= join '', "\x{200A}", map chr(0x2000+$_), split //, $s->{colgroup};
317             $newtext .= $mark;
318             $prev_colmark= $s;
319             }
320             elsif (defined $s->{fn}) {
321             $newtext .= $s->{fn}->($self, \$newtext);
322             }
323             elsif (defined $s->{eval_idx}) {
324             my $fn= $expr_subs[$s->{eval_idx}]
325             or die;
326             # Avoid using $_ up to this point so that $_ pases through
327             # from the surrounding code into the evals
328             my @out= $fn->($self, \$newtext);
329             # Expand arrayref and coderefs in the returned list
330             @out= @{$out[0]} if @out == 1 && ref $out[0] eq 'ARRAY';
331             ref eq 'CODE' && ($_= $_->($self, \$newtext)) for @out;
332             # Now decide what to join them with.
333             my $join_sep= $";
334             my $indent= '';
335             my ($last_char)= ($newtext =~ /(\S) (\s*) \Z/x);
336             my $cur_line= substr($newtext, rindex($newtext, "\n")+1);
337             my $inline= $cur_line =~ /\S/;
338             if ($self->{autoindent}) {
339             ($indent= $cur_line) =~ s/\S/ /g;
340             }
341             # Special handling if the user requested a list substitution
342             if (ord $s->{eval} == ord '@') {
343             $last_char= '' unless defined $last_char;
344             if ($self->{autocomma} && ($last_char eq ',' || $last_char eq '(')) {
345             if (@out) {
346             $join_sep= $inline? ', ' : ",\n";
347             @out= grep /\S/, @out; # remove items that are only whitespace
348             }
349             # If no items, or the first nonwhitespace character is a comma,
350             # remove the previous comma
351             if (!@out || $out[0] =~ /^\s*,/) {
352             $newtext =~ s/,(\s*)\Z/$1/;
353             }
354             } elsif ($self->{autostatementline} && ($last_char eq '{' || $last_char eq ';')) {
355             @out= grep /\S/, @out; # remove items that are only whitespace
356             $join_sep= $inline? "; " : ";\n";
357             } elsif ($self->{autoindent} && !$inline && $join_sep !~ /\n/) {
358             $join_sep .= "\n";
359             }
360             }
361             my $out= join $join_sep, @out;
362             # Autoindent: if new text contains newline, add current indent to start of each line.
363             if ($self->{autoindent} && $indent) {
364             $out =~ s/\n/\n$indent/g;
365             }
366             $newtext .= $out;
367             }
368             $at= $s->{pos} + $s->{len};
369             }
370             $text= $newtext . substr($text, $at);
371             # Second pass, adjust whitespace of all column markers so they line up.
372             # Iterate from leftmost column rightward.
373             autoindent: for my $group_i (sort { $a <=> $b } keys %colmarker) {
374             my $token= $colmarker{$group_i};
375             # Find the longest prefix (excluding trailing whitespace)
376             my $maxcol= 0;
377             my ($linestart, $col);
378             while ($text =~ /[ ]*$token/mg) {
379             $linestart= rindex($text, "\n", $-[0])+1;
380             $col= $-[0] - $linestart;
381             $maxcol= $col if $col > $maxcol;
382             }
383             $text =~ s/[ ]*$token/
384             $linestart= rindex($text, "\n", $-[0])+1;
385             " "x(1 + $maxcol - ($-[0] - $linestart))
386             /ge;
387             }
388             $self->{out}{impl} .= $text;
389             }
390              
391             package CodeGen::Cpppp::Template::Imports;
392 3     3   52 use Exporter;
  3         18  
  3         560  
393             our @EXPORT_OK= qw( PUBLIC PROTECTED PRIVATE );
394             our %EXPORT_TAGS= ( all => \@EXPORT_OK );
395              
396       0     sub PUBLIC {}
397       0     sub PROTECTED {}
398       0     sub PRIVATE {}
399              
400             1;
401              
402             =pod
403              
404             =encoding UTF-8
405              
406             =head1 NAME
407              
408             CodeGen::Cpppp - The C Perl-Powered Pre-Processor
409              
410             =head1 VERSION
411              
412             version 0.001_03
413              
414             =head1 SYNOPSIS
415              
416             I
417              
418             I. You see, most
419             blokes gonna be templating with C or C, you're on C here all the way up,
420             all the way up, Where can you go from there? Where?>
421              
422             I
423              
424             I
425              
426             I, exactly.>
427              
428             I.>
429              
430             B
431              
432             #! /usr/bin/env cpppp
433             ## for (my $bits= 8; $bits <= 16; $bits <<= 1) {
434             struct tree_node_$bits {
435             uint${bits}_t left: ${{$bits-1}},
436             color: 1,
437             right: ${{$bits-1}};
438             };
439             ## }
440              
441             B
442              
443             struct tree_node_8 {
444             uint8_t left: 7,
445             right: 7,
446             color: 1;
447             };
448             struct tree_node_16 {
449             uint16_t left: 15,
450             right: 15,
451             color: 1;
452             };
453              
454             B
455              
456             ## my @extra_args;
457             extern int fn( char *format, @extra_args );
458             ## for ('int a', 'int b') {
459             ## push @extra_args, $_;
460             extern int fn_$_( char *format, @extra_args );
461             ## }
462              
463             B
464              
465             extern int fn( char *format );
466             extern int fn_a( char *format, int a );
467             extern int fn_b( char *format, int a, int b );
468              
469             =head1 DESCRIPTION
470              
471             B.
472              
473             This module is a preprocessor for C, or maybe more like a perl template engine
474             that specializes in generating C code. Each input file gets translated to Perl
475             in a way that declares a new OO class, and then you can create instances of that
476             class with various parameters to generate your C output, or call methods on it
477             like automatically generating headers or function prototypes.
478              
479             For the end-user, there is a 'cpppp' command line tool that behaves much like
480             the 'cpp' tool.
481              
482             If you have an interest in this, contact me, because I could use help
483             brainstorming ideas about how to accommodate the most possibilities, here.
484              
485             B
486              
487             =over
488              
489             =item *
490              
491             Scan existing headers to discover available macros, structs, and functions on the host.
492              
493             =item *
494              
495             Pass a list of headers through the real cpp and analyze the macro output.
496              
497             =item *
498              
499             Shell out to a compiler to find 'sizeof' information for structs.
500              
501             =item *
502              
503             Directly perform the work of inlining one function into another.
504              
505             =back
506              
507             =head1 CONSTRUCTOR
508              
509             Bare-bones for now, it accepts whatever hash values you hand to it.
510              
511             =head1 METHODS
512              
513             =head2 compile_template
514              
515             $cpppp->compile_template($input_fh, $filename);
516             $cpppp->compile_template(\$scalar_tpl, $filename, $line_offset);
517              
518             This reads the input file handle (or scalar-ref) and builds a new perl template
519             class out of it (and dies if there are syntax errors in the template).
520              
521             Yes, this 'eval's the input, and no, there are not any guards against
522             malicious templates. But you run the same risk any time you run someone's
523             './configure' script.
524              
525             =head1 AUTHOR
526              
527             Michael Conrad
528              
529             =head1 COPYRIGHT AND LICENSE
530              
531             This software is copyright (c) 2023 by Michael Conrad.
532              
533             This is free software; you can redistribute it and/or modify it under
534             the same terms as the Perl 5 programming language system itself.
535              
536             =cut
537              
538             __END__