File Coverage

blib/lib/HTML/Template/JIT/Compiler.pm
Criterion Covered Total %
statement 193 215 89.7
branch 65 86 75.5
condition 3 3 100.0
subroutine 24 24 100.0
pod 0 1 0.0
total 285 329 86.6


line stmt bran cond sub pod time code
1             package HTML::Template::JIT::Compiler;
2              
3 9     9   235 use 5.006;
  9         29  
  9         552  
4 9     9   47 use strict;
  9         15  
  9         302  
5 9     9   42 use warnings;
  9         15  
  9         478  
6              
7             our $VERSION = '0.01';
8              
9 9     9   16710 use HTML::Template;
  9         158850  
  9         384  
10 9     9   106 use Carp qw(croak confess);
  9         19  
  9         772  
11 9     9   57 use File::Path qw(mkpath rmtree);
  9         19  
  9         8043  
12              
13             sub compile {
14 9     9 0 101 my %args = @_;
15 9         29 my $self = bless({});
16              
17             # parse the template as usual
18 9         99 $self->{template} = HTML::Template->new(%args);
19            
20             # setup state
21 9         8737 $self->{jit_path} = $args{jit_path};
22 9         26 $self->{package} = $args{package};
23 9         18 $self->{package_dir} = $args{package_dir};
24 9         25 $self->{package_path} = $args{package_path};
25 9         17 $self->{jit_pool} = [];
26 9         25 $self->{jit_sym} = 0;
27 9         24 $self->{jit_debug} = $args{jit_debug};
28 9         20 $self->{text_size} = 0;
29 9         25 $self->{loop_context_vars} = $args{loop_context_vars};
30 9         24 $self->{max_depth} = 0;
31 9         22 $self->{global_vars} = $args{global_vars};
32 9         21 $self->{print_to_stdout} = $args{print_to_stdout};
33 9         21 $self->{case_sensitive} = $args{case_sensitive};
34              
35             # compile internal representation into a chunk of C code
36              
37             # get code for param function
38 9         33 my @code = $self->_output();
39              
40 9 50       53 if ($self->{jit_debug}) {
41 0         0 print STDERR "###################### CODE START ######################\n\n";
42 0         0 open(INDENT, "| indent -kr > code.tmp");
43 0         0 print INDENT join("\n", @code);
44 0         0 close INDENT;
45 0         0 open(CODE, 'code.tmp');
46 0         0 print STDERR join('', );
47 0         0 close(CODE);
48 0         0 unlink('code.tmp');
49 0         0 print STDERR "\n\n###################### CODE END ######################\n\n";
50             }
51              
52 9         33 $self->_write_module(\@code);
53              
54             # try to load the module and return package handle if successful
55 9         16 my $result;
56 9         17 eval { $result = require $self->{package_path}; };
  9         8412  
57 9 50       2165665 return 1 if $result;
58              
59             # don't leave failed compiles lying around unless we're debuging
60 9 50       11490 rmtree($self->{package_dir}, 0, 0) unless $self->{jit_debug};
61 9 50       5915 die $@ if $@;
62 0         0 return 0;
63             }
64              
65             # writes out the module file
66             sub _write_module {
67 9     9   53 my ($self, $code) = @_;
68              
69             # make directory
70 9         97620 mkpath($self->{package_dir}, 0, 0700);
71            
72             # open module file
73 9 50       930 open(MODULE, ">$self->{package_path}") or die "Unable to open $self->{package_path} for output : $!";
74            
75 9         33 my $inline_debug = "";
76 9         18 my $optimize = "-O3";
77 9 50       43 if ($self->{jit_debug}) {
78 0         0 $inline_debug = ", CLEAN_AFTER_BUILD => 0";
79 0         0 $optimize = "-g";
80             }
81              
82             # print out preamble
83 9         209 print MODULE <
84             package $self->{package};
85             use base 'HTML::Template::JIT::Base';
86              
87             use Inline C => Config => OPTIMIZE => "$optimize", DIRECTORY => "$self->{package_dir}" $inline_debug;
88             use Inline C => <<'CODE_END';
89              
90             END
91              
92             # print out code
93 9         96 print MODULE join("\n", @$code), "\nCODE_END\n";
94              
95             # output the param hash
96 9         52 print MODULE "our \%param_hash = (\n", join(',', $self->_param_hash([])), ");\n";
97            
98             # empty param map
99 9         24 print MODULE "our \%param_map;\n";
100              
101             # note case sensitivity
102 9         153 print MODULE "our \$case_sensitive = $self->{case_sensitive};\n";
103              
104 9         21 print MODULE "\n1;\n";
105              
106             # all done
107 9         744 close MODULE;
108             }
109              
110             # construct the output function
111             sub _output {
112 9     9   21 my $self = shift;
113 9         20 my $template = $self->{template};
114              
115             # construct body of output
116 9         35 my @code = $self->_output_template($template, 0);
117            
118             # write global pool
119 9         38 unshift @code, '', $self->_write_pool();
120              
121             # setup result size based on gathered stats with a little extra for variables
122 9         52 my $size = int ($self->{text_size} + ($self->{text_size} * .10));
123              
124             # head code for output function, deferred to allow for $size and
125             # max_depth setup
126 9         58 unshift @code, <
127             SV * output(SV *self) {
128             SV *result = NEWSV(0, $size);
129             HV *param_map[($self->{max_depth} + 1)];
130             SV ** temp_svp;
131             SV * temp_sv;
132             int i;
133             STRLEN len;
134             unsigned char c;
135             char buf[4];
136              
137             SvPOK_on(result);
138             param_map[0] = get_hv("$self->{package}::param_map", 0);
139              
140             END
141              
142             # finish output function
143 9         21 push @code, "return result;", "}";
144              
145 9         101 return @code;
146             }
147              
148             # output the body of a single scope (top-level or loop)
149             sub _output_template {
150 16     16   32 my ($self, $template, $offset) = @_;
151 16 100       71 $self->{max_depth} = $offset
152             if $offset > $self->{max_depth};
153            
154 16         24 my (@code, @top, %vars, @pool, %blocks, $type, $name, $var,
155             $do_escape, $has_default);
156            
157             # setup some convenience aliases ala HTML::Template::output()
158 9     9   57 use vars qw($line @parse_stack %param_map);
  9         20  
  9         26710  
159 16         62 local (*line, *parse_stack, *param_map);
160 16         38 *parse_stack = $template->{parse_stack};
161 16         31 *param_map = $template->{param_map};
162            
163 16         60 my %reverse_param_map = map { $param_map{$_} => $_ } keys %param_map;
  28         160  
164 16         41 my $parse_stack_length = $#parse_stack;
165            
166 16         64 for (my $x = 0; $x <= $parse_stack_length; $x++) {
167 116         196 *line = \$parse_stack[$x];
168 116         208 $type = ref($line);
169            
170             # need any block closings on this line?
171 116 100       277 push(@code, "}" x $blocks{$x}) if $blocks{$x};
172              
173 116 100       437 if ($type eq 'SCALAR') {
    100          
    50          
    100          
    100          
    100          
    50          
    50          
    50          
174             # append string and add size to text_size counter
175 64 100       162 if ($self->{print_to_stdout}) {
176 5         8 push @code, _print_string($$line);
177             } else {
178 59         127 push @code, _concat_string($$line);
179 59         206 $self->{text_size} += length $$line;
180             }
181              
182             } elsif ($type eq 'HTML::Template::VAR') {
183             # get name for this variable from reverse map
184 18         42 $name = $reverse_param_map{$line};
185              
186             # check var cache - can't use it for escaped variables
187 18 100       49 if (exists $vars{$name}) {
188 4         7 $var = $vars{$name};
189             }
190            
191             # load a new one if needed
192             else {
193 14         49 $var = $self->_get_var("SV *", "&PL_sv_undef", \@pool);
194 14         79 push @top, _load_var($name, $var, $offset, $self->{global_vars});
195 14         38 $vars{$name} = $var;
196             }
197            
198             # escape var if needed
199 18 100       43 if ($do_escape) {
200 4         7 push @code, _escape_var($var, $do_escape);
201             }
202              
203             # append the var
204 18 100       62 push @code, ($self->{print_to_stdout} ? _print_var($var, $do_escape, $has_default) :
205             _concat_var($var, $do_escape, $has_default));
206              
207             # reset flags
208 18         29 undef $do_escape;
209 18         49 undef $has_default;
210              
211             } elsif ($type eq 'HTML::Template::DEFAULT') {
212 0         0 $has_default = $$line;
213              
214             } elsif ($type eq 'HTML::Template::LOOP') {
215             # get loop template
216 7         30 my $loop_template = $line->[HTML::Template::LOOP::TEMPLATE_HASH]{$x};
217              
218             # allocate an hv for the loop param_map
219 7         10 my $loop_offset = $offset + 1;
220              
221             # remember text_size before loop
222 7         16 my $old_text_size = $self->{text_size};
223              
224             # output the loop start
225 7         26 push @code, $self->_start_loop($reverse_param_map{$line}, $offset,
226             $loop_offset);
227              
228             # output the loop code
229 7         52 push @code, $self->_output_template($loop_template, $loop_offset);
230            
231             # send the loop
232 7         22 push @code, $self->_end_loop();
233              
234             # guesstimate average loop run of 10 and pre-allocate space for
235             # text accordingly. This is a bit silly but something has to be
236             # done to account for loops increasing result size...
237 7         31 $self->{text_size} += (($self->{text_size} - $old_text_size) * 9);
238            
239             } elsif ($type eq 'HTML::Template::COND') {
240             # if, unless and else
241            
242             # store block end loc
243 13         44 $blocks{$line->[HTML::Template::COND::JUMP_ADDRESS]}++;
244              
245             # get name for this var
246 13         29 $name = $reverse_param_map{$line->[HTML::Template::COND::VARIABLE]};
247              
248             # load a new var unless we have this one
249 13 100       29 if (exists $vars{$name}) {
250 6         8 $var = $vars{$name};
251             } else {
252 7         19 $var = $self->_get_var("SV *", "&PL_sv_undef", \@pool);
253 7         18 push @top, _load_var($name, $var, $offset, $self->{global_vars});
254 7         17 $vars{$name} = $var;
255             }
256              
257             # output conditional
258 13         51 push(@code, $self->_cond($line->[HTML::Template::COND::JUMP_IF_TRUE],
259             $line->[HTML::Template::COND::VARIABLE_TYPE] == HTML::Template::COND::VARIABLE_TYPE_VAR,
260             $var,
261             $line->[HTML::Template::COND::UNCONDITIONAL_JUMP],
262             ));
263             } elsif ($type eq 'HTML::Template::ESCAPE') {
264 4         8 $do_escape = 'HTML';
265             } elsif ($type eq 'HTML::Template::URLESCAPE') {
266 0         0 $do_escape = 'URL';
267             } elsif ($type eq 'HTML::Template::JSESCAPE') {
268 0         0 $do_escape = 'JS';
269             } elsif ($type eq 'HTML::Template::NOOP') {
270             # noop
271             } else {
272 0         0 confess("Unsupported object type in parse stack : $type");
273             }
274             }
275              
276             # output pool of variables used in body
277 16         50 unshift @code, '{', $self->_write_pool(\@pool), @top;
278 16         32 push @code, '}';
279              
280 16         196 return @code;
281             }
282              
283             # output a conditional expression
284             sub _cond {
285 13     13   34 my ($self, $is_unless, $is_var, $var, $is_uncond) = @_;
286 13         13 my @code;
287              
288 13 100       29 if ($is_uncond) {
289 3         7 push(@code, "else {");
290             } else {
291 10 100       20 if ($is_var) {
292 7 100       14 if ($is_unless) {
293             # unless var
294 1         3 push(@code, "if (!SvTRUE($var)) {");
295             } else {
296             # if var
297 6         16 push(@code, "if (SvTRUE($var)) {");
298             }
299             } else {
300 3 100       9 if ($is_unless) {
301             # unless loop
302 1         4 push(@code, "if ($var == &PL_sv_undef || av_len((AV *) SvRV($var)) == -1) {");
303             } else {
304             # if loop
305 2         7 push(@code, "if ($var != &PL_sv_undef && av_len((AV *) SvRV($var)) != -1) {");
306             }
307             }
308             }
309              
310 13         88 return @code;
311             }
312              
313             # start a loop
314             sub _start_loop {
315 7     7   16 my ($self, $name, $offset, $loop_offset) = @_;
316 7         13 my $name_string = _quote_string($name);
317 7         14 my $name_len = length($name_string);
318 7         11 my @pool;
319 7         19 my $av = $self->_get_var("AV *", 0, \@pool);
320 7         20 my $av_len = $self->_get_var("I32", 0, \@pool);
321 7         27 my $counter = $self->_get_var("I32", 0, \@pool);
322 7         10 my @code;
323              
324             my $odd;
325 7 50       26 if ($self->{loop_context_vars}) {
326 0         0 $odd = $self->_get_var("I32", 0, \@pool);
327 0         0 push(@code, "$odd = 0;");
328             }
329              
330 7         43 push @code, <
331             temp_svp = hv_fetch(param_map[$offset], "$name_string", $name_len, 0);
332             if (temp_svp && (*temp_svp != &PL_sv_undef)) {
333             $av = (AV *) SvRV(*temp_svp);
334             $av_len = av_len($av);
335              
336             for($counter = 0; $counter <= $av_len; $counter++) {
337             param_map[$loop_offset] = (HV *) SvRV(*(av_fetch($av, $counter, 0)));
338             END
339              
340 7 50       24 if ($self->{loop_context_vars}) {
341 0         0 push @code, <
342             if ($counter == 0) {
343             hv_store(param_map[$loop_offset], "__first__", 9, &PL_sv_yes, 0);
344             hv_store(param_map[$loop_offset], "__inner__", 9, &PL_sv_no, 0);
345             if ($av_len == 0)
346             hv_store(param_map[$loop_offset], "__last__", 8, &PL_sv_yes, 0);
347             } else if ($counter == $av_len) {
348             hv_store(param_map[$loop_offset], "__first__", 9, &PL_sv_no, 0);
349             hv_store(param_map[$loop_offset], "__inner__", 9, &PL_sv_no, 0);
350             hv_store(param_map[$loop_offset], "__last__", 8, &PL_sv_yes, 0);
351             } else {
352             hv_store(param_map[$loop_offset], "__first__", 9, &PL_sv_no, 0);
353             hv_store(param_map[$loop_offset], "__inner__", 9, &PL_sv_yes, 0);
354             hv_store(param_map[$loop_offset], "__last__", 8, &PL_sv_no, 0);
355             }
356              
357             hv_store(param_map[$loop_offset], "__odd__", 7, (($odd = !$odd) ? &PL_sv_yes : &PL_sv_no), 0);
358             hv_store(param_map[$loop_offset], "__counter__", 11, newSViv($counter + 1), 0);
359             END
360              
361             }
362              
363 7         24 unshift @code, "{", $self->_write_pool(\@pool);
364              
365 7         42 return @code;
366             }
367              
368             # end a loop
369             sub _end_loop {
370 7     7   13 return '}}}';
371             }
372              
373             # construct %param_hash
374             sub _param_hash {
375 16     16   34 my ($self, $path) = @_;
376 16         29 my $template = $self->{template};
377              
378 16         23 my @params;
379 16 100       83 if (@$path) {
380 7         21 @params = $template->query(LOOP => $path);
381             } else {
382 9         53 @params = $template->param();
383             }
384              
385 16         379 my @out;
386 16         29 foreach my $name (@params) {
387 28         113 my $type = $template->query(name => [ @$path, $name ]);
388 28 100       1035 if ($type eq 'VAR') {
389 21         68 push @out, "'$name'", 1;
390             } else {
391 7         42 push @out, "'$name'", "\n{" . join(', ', $self->_param_hash([ @$path, $name ])) . "\n}\n";
392             }
393             }
394            
395 16         416 return @out;
396             }
397              
398              
399             # get a fresh var of the requested C type from the pool
400             sub _get_var {
401 42     42   70 my ($self, $type, $default, $pool) = @_;
402 42 50       98 $pool = $self->{jit_pool} unless defined $pool;
403 42         91 my $sym = "sym_" . $self->{jit_sym}++;
404 42 100       131 push @$pool, $type, ($default ? "$sym = $default" : $sym);
405 42         97 return $sym;
406             }
407              
408             # write out the code to initialize the pool
409             sub _write_pool {
410 32     32   51 my ($self, $pool) = @_;
411 32 100       98 $pool = $self->{jit_pool} unless defined $pool;
412 32         37 my @code;
413            
414 32         94 for (my $index = 0; $index < @$pool; $index += 2) {
415 42         151 push(@code, $pool->[$index] . ' ' . $pool->[$index + 1] . ";");
416             }
417 32         64 @$pool = ();
418 32         116 return @code;
419             }
420              
421             # concatenate a string onto result
422             sub _concat_string {
423 59 100   59   162 return "" unless $_[0];
424 53         68 my $len = length($_[0]);
425 53         100 my $string = _quote_string($_[0]);
426              
427 53         163 return "sv_catpvn(result, \"$string\", $len);"
428             }
429              
430             # concatenate a string onto result
431             sub _print_string {
432 5 100   5   12 return "" unless $_[0];
433 4         13 my $string = _quote_string($_[0]);
434 4         14 return "PerlIO_stdoutf(\"$string\");";
435             }
436              
437             # loads a variable into a lexical pool variable
438             sub _load_var {
439 21     21   38 my ($name, $var, $offset, $global) = @_;
440 21         46 my $string = _quote_string($name);
441 21         36 my $len = length($name);
442            
443 21 100 100     97 return <
444             for (i = $offset; i >= 0; i--) {
445             if (hv_exists(param_map[i], "$string", $len)) {
446             $var = *(hv_fetch(param_map[i], "$string", $len, 0));
447             if ($var != &PL_sv_undef) break;
448             }
449             }
450             END
451              
452 18         117 return <
453             if (hv_exists(param_map[$offset], "$string", $len))
454             $var = *(hv_fetch(param_map[$offset], "$string", $len, 0));
455             END
456             }
457              
458             # loads a variable and escapes it
459             sub _escape_var {
460 4     4   4 my ($var, $escape) = @_;
461            
462             # apply escaping to a mortal copy in temp_sv
463 4         18 my @code = (<
464             if ($var != &PL_sv_undef) {
465             SvPV_force($var, len);
466             temp_sv = sv_mortalcopy($var);
467             len = 0;
468             while (len < SvCUR(temp_sv)) {
469             c = *(SvPVX(temp_sv) + len);
470             END
471              
472             # perform the appropriate escapes
473 4 50       7 if ($escape eq 'HTML') {
    0          
    0          
474 4         4 push @code, <
475             switch (c) {
476             case '&':
477             sv_insert(temp_sv, len, 1, "&", 5);
478             len += 4;
479             break;
480             case '"':
481             sv_insert(temp_sv, len, 1, """, 6);
482             len += 5;
483             break;
484             case '>':
485             sv_insert(temp_sv, len, 1, ">", 4);
486             len += 3;
487             break;
488             case '<':
489             sv_insert(temp_sv, len, 1, "<", 4);
490             len += 3;
491             break;
492             case '\\'':
493             sv_insert(temp_sv, len, 1, "'", 5);
494             len += 4;
495             break;
496             }
497             END
498             } elsif ($escape eq 'URL') {
499 0         0 push @code, <
500             if (!(isALNUM(c) || (c == '-') || (c == '.'))) {
501             sprintf(buf, "%%%02X", c);
502             sv_insert(temp_sv, len, 1, buf, 3);
503             len += 2;
504             }
505             END
506             } elsif ($escape eq 'JS') {
507 0         0 push @code, <<'END';
508             switch (c) {
509             case '\\':
510             case '\'':
511             case '"':
512             sprintf(buf, "\\%c", c);
513             sv_insert(temp_sv, len, 1, buf, 2);
514             len += 1;
515             break;
516             case '\n':
517             sprintf(buf, "\\n");
518             sv_insert(temp_sv, len, 1, buf, 2);
519             len += 1;
520             break;
521             case '\r':
522             sprintf(buf, "\\r");
523             sv_insert(temp_sv, len, 1, buf, 2);
524             len += 1;
525             }
526             END
527            
528             } else {
529 0         0 die "Unknown escape type '$escape'.";
530             }
531              
532 4         5 push @code, <
533             len++;
534             }
535             }
536             END
537              
538 4         10 return @code;
539             }
540              
541             # concatenate a var onto result
542             sub _concat_var {
543 16 100   16   108 return "if ($_[0] != &PL_sv_undef) sv_catsv(result, " .
    50          
544             ($_[1] ? "temp_sv" : $_[0]) . ");" .
545             (defined $_[2] ? " else " . _concat_string($_[2]) : "");
546             }
547              
548             # print a var to stdout
549             sub _print_var {
550 2 50   2   9 return "if ($_[0] != &PL_sv_undef) PerlIO_stdoutf(SvPV_nolen(" .
    50          
551             ($_[1] ? "temp_sv" : $_[0]) . "));" .
552             (defined $_[2] ? " else " . _print_string($_[2]) : "");
553             }
554              
555             # turn a string into something that C will accept inside
556             # double-quotes. or should I go the array of bytes route? I think
557             # that might be the only way to get UTF-8 working. It's such hell to
558             # debug though...
559             sub _quote_string {
560 85     85   112 my $string = shift;
561 85         125 $string =~ s/\\/\\\\/g;
562 85         91 $string =~ s/"/\\"/g;
563 85         94 $string =~ s/\r/\\r/g;
564 85         154 $string =~ s/\n/\\n/g;
565 85         111 $string =~ s/\t/\\t/g;
566 85         154 return $string;
567             }
568              
569             1;
570              
571             __END__