package HTML::Template::JIT::Compiler; use 5.006; use strict; use warnings; our $VERSION = '0.01'; use HTML::Template; use Carp qw(croak confess); use File::Path qw(mkpath rmtree); sub compile { my %args = @_; my $self = bless({}); # parse the template as usual $self->{template} = HTML::Template->new(%args); # setup state $self->{jit_path} = $args{jit_path}; $self->{package} = $args{package}; $self->{package_dir} = $args{package_dir}; $self->{package_path} = $args{package_path}; $self->{jit_pool} = []; $self->{jit_sym} = 0; $self->{jit_debug} = $args{jit_debug}; $self->{text_size} = 0; $self->{loop_context_vars} = $args{loop_context_vars}; $self->{max_depth} = 0; $self->{global_vars} = $args{global_vars}; $self->{print_to_stdout} = $args{print_to_stdout}; $self->{case_sensitive} = $args{case_sensitive}; # compile internal representation into a chunk of C code # get code for param function my @code = $self->_output(); if ($self->{jit_debug}) { print STDERR "###################### CODE START ######################\n\n"; open(INDENT, "| indent -kr > code.tmp"); print INDENT join("\n", @code); close INDENT; open(CODE, 'code.tmp'); print STDERR join('', ); close(CODE); unlink('code.tmp'); print STDERR "\n\n###################### CODE END ######################\n\n"; } $self->_write_module(\@code); # try to load the module and return package handle if successful my $result; eval { $result = require $self->{package_path}; }; return 1 if $result; # don't leave failed compiles lying around unless we're debuging rmtree($self->{package_dir}, 0, 0) unless $self->{jit_debug}; die $@ if $@; return 0; } # writes out the module file sub _write_module { my ($self, $code) = @_; # make directory mkpath($self->{package_dir}, 0, 0700); # open module file open(MODULE, ">$self->{package_path}") or die "Unable to open $self->{package_path} for output : $!"; my $inline_debug = ""; my $optimize = "-O3"; if ($self->{jit_debug}) { $inline_debug = ", CLEAN_AFTER_BUILD => 0"; $optimize = "-g"; } # print out preamble print MODULE <{package}; use base 'HTML::Template::JIT::Base'; use Inline C => Config => OPTIMIZE => "$optimize", DIRECTORY => "$self->{package_dir}" $inline_debug; use Inline C => <<'CODE_END'; END # print out code print MODULE join("\n", @$code), "\nCODE_END\n"; # output the param hash print MODULE "our \%param_hash = (\n", join(',', $self->_param_hash([])), ");\n"; # empty param map print MODULE "our \%param_map;\n"; # note case sensitivity print MODULE "our \$case_sensitive = $self->{case_sensitive};\n"; print MODULE "\n1;\n"; # all done close MODULE; } # construct the output function sub _output { my $self = shift; my $template = $self->{template}; # construct body of output my @code = $self->_output_template($template, 0); # write global pool unshift @code, '', $self->_write_pool(); # setup result size based on gathered stats with a little extra for variables my $size = int ($self->{text_size} + ($self->{text_size} * .10)); # head code for output function, deferred to allow for $size and # max_depth setup unshift @code, <{max_depth} + 1)]; SV ** temp_svp; SV * temp_sv; int i; STRLEN len; char c; char buf[4]; SvPOK_on(result); param_map[0] = get_hv("$self->{package}::param_map", 0); END # finish output function push @code, "return result;", "}"; return @code; } # output the body of a single scope (top-level or loop) sub _output_template { my ($self, $template, $offset) = @_; $self->{max_depth} = $offset if $offset > $self->{max_depth}; my (@code, @top, %vars, @pool, %blocks, $type, $name, $var, $do_escape, $has_default); # setup some convenience aliases ala HTML::Template::output() use vars qw($line @parse_stack %param_map); local (*line, *parse_stack, *param_map); *parse_stack = $template->{parse_stack}; *param_map = $template->{param_map}; my %reverse_param_map = map { $param_map{$_} => $_ } keys %param_map; my $parse_stack_length = $#parse_stack; for (my $x = 0; $x <= $parse_stack_length; $x++) { *line = \$parse_stack[$x]; $type = ref($line); # need any block closings on this line? push(@code, "}" x $blocks{$x}) if $blocks{$x}; if ($type eq 'SCALAR') { # append string and add size to text_size counter if ($self->{print_to_stdout}) { push @code, _print_string($$line); } else { push @code, _concat_string($$line); $self->{text_size} += length $$line; } } elsif ($type eq 'HTML::Template::VAR') { # get name for this variable from reverse map $name = $reverse_param_map{$line}; # check var cache - can't use it for escaped variables if (exists $vars{$name}) { $var = $vars{$name}; } # load a new one if needed else { $var = $self->_get_var("SV *", "&PL_sv_undef", \@pool); push @top, _load_var($name, $var, $offset, $self->{global_vars}); $vars{$name} = $var; } # escape var if needed if ($do_escape) { push @code, _escape_var($var, $do_escape); } # append the var push @code, ($self->{print_to_stdout} ? _print_var($var, $do_escape, $has_default) : _concat_var($var, $do_escape, $has_default)); # reset flags undef $do_escape; undef $has_default; } elsif ($type eq 'HTML::Template::DEFAULT') { $has_default = $$line; } elsif ($type eq 'HTML::Template::LOOP') { # get loop template my $loop_template = $line->[HTML::Template::LOOP::TEMPLATE_HASH]{$x}; # allocate an hv for the loop param_map my $loop_offset = $offset + 1; # remember text_size before loop my $old_text_size = $self->{text_size}; # output the loop start push @code, $self->_start_loop($reverse_param_map{$line}, $offset, $loop_offset); # output the loop code push @code, $self->_output_template($loop_template, $loop_offset); # send the loop push @code, $self->_end_loop(); # guesstimate average loop run of 10 and pre-allocate space for # text accordingly. This is a bit silly but something has to be # done to account for loops increasing result size... $self->{text_size} += (($self->{text_size} - $old_text_size) * 9); } elsif ($type eq 'HTML::Template::COND') { # if, unless and else # store block end loc $blocks{$line->[HTML::Template::COND::JUMP_ADDRESS]}++; # get name for this var $name = $reverse_param_map{$line->[HTML::Template::COND::VARIABLE]}; # load a new var unless we have this one if (exists $vars{$name}) { $var = $vars{$name}; } else { $var = $self->_get_var("SV *", "&PL_sv_undef", \@pool); push @top, _load_var($name, $var, $offset, $self->{global_vars}); $vars{$name} = $var; } # output conditional push(@code, $self->_cond($line->[HTML::Template::COND::JUMP_IF_TRUE], $line->[HTML::Template::COND::VARIABLE_TYPE] == HTML::Template::COND::VARIABLE_TYPE_VAR, $var )); } elsif ($type eq 'HTML::Template::ESCAPE') { $do_escape = 'HTML'; } elsif ($type eq 'HTML::Template::URLESCAPE') { $do_escape = 'URL'; } elsif ($type eq 'HTML::Template::NOOP') { # noop } else { confess("Unsupported object type in parse stack : $type"); } } # output pool of variables used in body unshift @code, '{', $self->_write_pool(\@pool), @top; push @code, '}'; return @code; } # output a conditional expression sub _cond { my ($self, $is_unless, $is_var, $var) = @_; my @code; if ($is_var) { if ($is_unless) { # unless var push(@code, "if (!SvTRUE($var)) {"); } else { # if var push(@code, "if (SvTRUE($var)) {"); } } else { if ($is_unless) { # unless loop push(@code, "if ($var == &PL_sv_undef || av_len((AV *) SvRV($var)) == -1) {"); } else { # if loop push(@code, "if ($var != &PL_sv_undef && av_len((AV *) SvRV($var)) != -1) {"); } } return @code; } # start a loop sub _start_loop { my ($self, $name, $offset, $loop_offset) = @_; my $name_string = _quote_string($name); my $name_len = length($name_string); my @pool; my $av = $self->_get_var("AV *", 0, \@pool); my $av_len = $self->_get_var("I32", 0, \@pool); my $counter = $self->_get_var("I32", 0, \@pool); my @code; my $odd; if ($self->{loop_context_vars}) { $odd = $self->_get_var("I32", 0, \@pool); push(@code, "$odd = 0;"); } push @code, <{loop_context_vars}) { push @code, <_write_pool(\@pool); return @code; } # end a loop sub _end_loop { return '}}}'; } # construct %param_hash sub _param_hash { my ($self, $path) = @_; my $template = $self->{template}; my @params; if (@$path) { @params = $template->query(LOOP => $path); } else { @params = $template->param(); } my @out; foreach my $name (@params) { my $type = $template->query(name => [ @$path, $name ]); if ($type eq 'VAR') { push @out, "'$name'", 1; } else { push @out, "'$name'", "\n{" . join(', ', $self->_param_hash([ @$path, $name ])) . "\n}\n"; } } return @out; } # get a fresh var of the requested C type from the pool sub _get_var { my ($self, $type, $default, $pool) = @_; $pool = $self->{jit_pool} unless defined $pool; my $sym = "sym_" . $self->{jit_sym}++; push @$pool, $type, ($default ? "$sym = $default" : $sym); return $sym; } # write out the code to initialize the pool sub _write_pool { my ($self, $pool) = @_; $pool = $self->{jit_pool} unless defined $pool; my @code; for (my $index = 0; $index < @$pool; $index += 2) { push(@code, $pool->[$index] . ' ' . $pool->[$index + 1] . ";"); } @$pool = (); return @code; } # concatenate a string onto result sub _concat_string { return "" unless $_[0]; my $len = length($_[0]); my $string = _quote_string($_[0]); return "sv_catpvn(result, \"$string\", $len);" } # concatenate a string onto result sub _print_string { return "" unless $_[0]; my $string = _quote_string($_[0]); return "PerlIO_stdoutf(\"$string\");"; } # loads a variable into a lexical pool variable sub _load_var { my ($name, $var, $offset, $global) = @_; my $string = _quote_string($name); my $len = length($name); return <= 0; i--) { if (hv_exists(param_map[i], "$string", $len)) { $var = *(hv_fetch(param_map[i], "$string", $len, 0)); if ($var != &PL_sv_undef) break; } } END return <': sv_insert(temp_sv, len, 1, ">", 4); len += 3; break; case '<': sv_insert(temp_sv, len, 1, "<", 4); len += 3; break; case '\\'': sv_insert(temp_sv, len, 1, "'", 5); len += 4; break; } END } elsif ($escape eq 'URL') { push @code, <compile(...); =head1 DESCRIPTION This module is used internally by HTML::Template::JIT to compile template files. Don't use it directly - use HTML::Template::JIT instead. =head1 AUTHOR Sam Tregar =head1 LICENSE HTML::Template::JIT : Just-in-time compiler for HTML::Template Copyright (C) 2001 Sam Tregar (sam@tregar.com) This module is free software; you can redistribute it and/or modify it under the terms of either: a) the GNU General Public License as published by the Free Software Foundation; either version 1, or (at your option) any later version, or b) the "Artistic License" which comes with this module. This program is distributed in the hope that it will be useful,but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See either the GNU General Public License or the Artistic License for more details. You should have received a copy of the Artistic License with this module, in the file ARTISTIC. If not, I'll be glad to provide one. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA