The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Template::Alloy::Compile;

=head1 NAME

Template::Alloy::Compile - Compile role - allows for compiling the AST to perl code

=cut

use strict;
use warnings;
use Template::Alloy;
use Template::Alloy::Iterator;

our $VERSION = $Template::Alloy::VERSION;
our $INDENT  = ' ' x 4;
our $DIRECTIVES = {
    BLOCK   => \&compile_BLOCK,
    BREAK   => \&compile_LAST,
    CALL    => \&compile_CALL,
    CASE    => undef,
    CATCH   => undef,
    CLEAR   => \&compile_CLEAR,
    '#'     => sub {},
    COMMENT => sub {},
    CONFIG  => \&compile_CONFIG,
    DEBUG   => \&compile_DEBUG,
    DEFAULT => \&compile_DEFAULT,
    DUMP    => \&compile_DUMP,
    ELSE    => undef,
    ELSIF   => undef,
    END     => sub {},
    EVAL    => \&compile_EVAL,
    FILTER  => \&compile_FILTER,
    '|'     => \&compile_FILTER,
    FINAL   => undef,
    FOR     => \&compile_FOR,
    FOREACH => \&compile_FOR,
    GET     => \&compile_GET,
    IF      => \&compile_IF,
    INCLUDE => \&compile_INCLUDE,
    INSERT  => \&compile_INSERT,
    JS      => \&compile_JS,
    LAST    => \&compile_LAST,
    LOOP    => \&compile_LOOP,
    MACRO   => \&compile_MACRO,
    META    => \&compile_META,
    NEXT    => \&compile_NEXT,
    PERL    => \&compile_PERL,
    PROCESS => \&compile_PROCESS,
    RAWPERL => \&compile_RAWPERL,
    RETURN  => \&compile_RETURN,
    SET     => \&compile_SET,
    STOP    => \&compile_STOP,
    SWITCH  => \&compile_SWITCH,
    TAGS    => sub {},
    THROW   => \&compile_THROW,
    TRY     => \&compile_TRY,
    UNLESS  => \&compile_UNLESS,
    USE     => \&compile_USE,
    VIEW    => \&compile_VIEW,
    WHILE   => \&compile_WHILE,
    WRAPPER => \&compile_WRAPPER,
};

sub new { die "This class is a role for use by packages such as Template::Alloy" }

sub load_perl {
    my ($self, $doc) = @_;

    ### first look for a compiled perl document
    my $perl;
    if ($doc->{'_filename'}) {
        $doc->{'modtime'} ||= (stat $doc->{'_filename'})[9];
        if ($self->{'COMPILE_DIR'} || $self->{'COMPILE_EXT'}) {
            my $file = $doc->{'_filename'};
            if ($self->{'COMPILE_DIR'}) {
                $file =~ y|:|/| if $^O eq 'MSWin32';
                $file = $self->{'COMPILE_DIR'} .'/'. $file;
            } elsif ($doc->{'_is_str_ref'}) {
                $file = ($self->include_paths->[0] || '.') .'/'. $file;
            }
            $file .= $self->{'COMPILE_EXT'} if defined($self->{'COMPILE_EXT'});
            $file .= $Template::Alloy::PERL_COMPILE_EXT if defined $Template::Alloy::PERL_COMPILE_EXT;

            if (-e $file && ($doc->{'_is_str_ref'} || (stat $file)[9] == $doc->{'modtime'})) {
                $perl = $self->slurp($file);
            } else {
                $doc->{'_compile_filename'} = $file;
            }
        }
    }

    $perl ||= $self->compile_template($doc);

    ### save a cache on the fileside as asked
    if ($doc->{'_compile_filename'}) {
        my $dir = $doc->{'_compile_filename'};
        $dir =~ s|/[^/]+$||;
        if (! -d $dir) {
            require File::Path;
            File::Path::mkpath($dir);
        }
        open(my $fh, ">", $doc->{'_compile_filename'}) || $self->throw('compile', "Could not open file \"$doc->{'_compile_filename'}\" for writing: $!");
        ### todo - think about locking
        if ($self->{'ENCODING'} && eval { require Encode } && defined &Encode::encode) {
            print {$fh} Encode::encode($self->{'ENCODING'}, $$perl);
        } else {
            print {$fh} $$perl;
        }
        close $fh;
        utime $doc->{'modtime'}, $doc->{'modtime'}, $doc->{'_compile_filename'};
    }

    $perl = eval $$perl;
    $self->throw('compile', "Trouble loading compiled perl: $@") if ! $perl && $@;

    return $perl;
}

###----------------------------------------------------------------###

sub compile_template {
    my ($self, $doc) = @_;

    local $self->{'_component'} = $doc;
    my $tree = $doc->{'_tree'} ||= $self->load_tree($doc);

    local $self->{'_blocks'} = '';
    local $self->{'_meta'}   = '';

    my $code = $self->compile_tree($tree, $INDENT);
    $self->{'_blocks'} .= "\n" if $self->{'_blocks'};
    $self->{'_meta'}   .= "\n" if $self->{'_meta'};

    my $file = $doc->{'_filename'} || '';
    $file =~ s/\'/\\\'/g;

    my $str = "# Generated by ".__PACKAGE__." v$VERSION on ".localtime()."

my \$file   = '$file';
my \$blocks = {$self->{'_blocks'}};
my \$meta   = {$self->{'_meta'}};
my \$code   = sub {
${INDENT}my (\$self, \$out_ref, \$var) = \@_;"
.($self->{'_blocks'} ? "\n${INDENT}\@{ \$self->{'BLOCKS'} }{ keys %\$blocks } = values %\$blocks;" : "")
.($self->{'_meta'}   ? "\n${INDENT}\@{ \$self->{'_component'} }{ keys %\$meta } = values %\$meta;" : "")
."$code

${INDENT}return 1;
};

{
${INDENT}blocks => \$blocks,
${INDENT}meta   => \$meta,
${INDENT}code   => \$code,
};\n";
#    print $str;
    return \$str;
}

###----------------------------------------------------------------###

sub _node_info {
    my ($self, $node, $indent) = @_;
    my $doc = $self->{'_component'} || return '';
    $doc->{'_content'} ||= $self->slurp($doc->{'_filename'});
    my ($line, $char) = $self->get_line_number_by_index($doc, $node->[1], 'include_chars');
    return "\n\n${indent}# \"$node->[0]\" Line $line char $char (chars $node->[1] to $node->[2])";
}

sub compile_tree {
    my ($self, $tree, $indent) = @_;
    my $code = '';
    # node contains (0: DIRECTIVE,
    #                1: start_index,
    #                2: end_index,
    #                3: parsed tag details,
    #                4: sub tree for block types
    #                5: continuation sub trees for sub continuation block types (elsif, else, etc)
    #                6: flag to capture next directive
    my @doc;
    my $func;
    for my $node (@$tree) {

        # text nodes are just the bare text
        if (! ref $node) {
            my $copy = $node; # must make a copy before modification
            $copy =~ s/([\'\\])/\\$1/g;
            $code .= "\n\n${indent}\$\$out_ref .= '$copy';";
            next;
        }

        if ($self->{'_debug_dirs'} && ! $self->{'_debug_off'}) {
            my $info = $self->node_info($node);
            my ($file, $line, $text) = @{ $info }{qw(file line text)};
            s/\'/\\\'/g foreach $file, $line, $text;
            $code .= "\n
${indent}if (\$self->{'_debug_dirs'} && ! \$self->{'_debug_off'}) { # DEBUG
${indent}${INDENT}my \$info = {file => '$file', line => '$line', text => '$text'};
${indent}${INDENT}my \$format = \$self->{'_debug_format'} || \$self->{'DEBUG_FORMAT'} || \"\\n## \\\$file line \\\$line : [% \\\$text %] ##\\n\";
${indent}${INDENT}\$format =~ s{\\\$(file|line|text)}{\$info->{\$1}}g;
${indent}${INDENT}\$\$out_ref .= \$format;
${indent}}";
        }

        $code .= _node_info($self, $node, $indent);

        if ($func = $DIRECTIVES->{$node->[0]}) {
            $func->($self, $node, \$code, $indent);
        } else {
            ### if the method isn't defined - delegate to the play directive (if there is one)
            require Template::Alloy::Play;
            if ($func = $Template::Alloy::Play::DIRECTIVES->{$node->[0]}) {
                _compile_defer_to_play($self, $node, \$code, $indent);
            } else {
                die "Couldn't find compile or play method for directive \"$node->[0]\"";
            }
        }
    }
    return $code;
}

sub compile_expr {
    my ($self, $var, $indent) = @_;
    return "\$self->play_expr(".$self->ast_string($var).")";
}

sub _compile_defer_to_play {
    my ($self, $node, $str_ref, $indent) = @_;
    my $directive = $node->[0];
    die "Invalid node name \"$directive\"" if $directive !~ /^\w+$/;

    $$str_ref .= "
${indent}require Template::Alloy::Play;
${indent}\$var = ".$self->ast_string($node->[3]).";
${indent}\$Template::Alloy::Play::DIRECTIVES->{'$directive'}->(\$self, \$var, ".$self->ast_string($node).", \$out_ref);";

    return;
}

sub _is_empty_named_args {
    my ($hash_ident) = @_;
    # [[undef, '{}', 'key1', 'val1', 'key2, 'val2'], 0]
    return @{ $hash_ident->[0] } <= 2;
}

###----------------------------------------------------------------###

sub compile_BLOCK {
    my ($self, $node, $str_ref, $indent) = @_;

    my $ref  = \ $self->{'_blocks'};
    my $name = $node->[3];
    $name =~ s/\'/\\\'/g;
    my $name2 = $self->{'_component'}->{'name'} .'/'. $node->[3];
    $name2 =~ s/\'/\\\'/g;

    my $code = $self->compile_tree($node->[4], "$INDENT$INDENT$INDENT");

    $$ref .= "
${INDENT}'$name' => {
${INDENT}${INDENT}name  => '$name2',
${INDENT}${INDENT}_filename => \$file,
${INDENT}${INDENT}_perl => {code => sub {
${INDENT}${INDENT}${INDENT}my (\$self, \$out_ref, \$var) = \@_;$code

${INDENT}${INDENT}${INDENT}return 1;
${INDENT}${INDENT}}},
${INDENT}},";

    return;
}

sub compile_CALL {
    my ($self, $node, $str_ref, $indent) = @_;
    $$str_ref .= "\n${indent}scalar ".$self->compile_expr($node->[3], $indent).";";
    return;
}

sub compile_CLEAR {
    my ($self, $node, $str_ref, $indent) = @_;
    $$str_ref .= "
${indent}\$\$out_ref = '';";
}

sub compile_CONFIG {
    my ($self, $node, $str_ref, $indent) = @_;
    _compile_defer_to_play($self, $node, $str_ref, $indent);
}

sub compile_DEBUG {
    my ($self, $node, $str_ref, $indent) = @_;

    my $text = $node->[3]->[0];

    if ($text eq 'on') {
        $$str_ref .= "\n${indent}delete \$self->{'_debug_off'};";
    } elsif ($text eq 'off') {
        $$str_ref .= "\n${indent}\$self->{'_debug_off'} = 1;";
    } elsif ($text eq 'format') {
        my $format = $node->[3]->[1];
        $format =~ s/\'/\\\'/g;
        $$str_ref .= "\n${indent}\$self->{'_debug_format'} = '$format';";
    }
    return;
}

sub compile_DEFAULT {
    my ($self, $node, $str_ref, $indent) = @_;
    local $self->{'_is_default'} = 1;
    $DIRECTIVES->{'SET'}->($self, $node, $str_ref, $indent);
}

sub compile_DUMP {
    my ($self, $node, $str_ref, $indent) = @_;
    _compile_defer_to_play($self, $node, $str_ref, $indent);
}

sub compile_GET {
    my ($self, $node, $str_ref, $indent) = @_;
    $$str_ref .= "
$indent\$var = ".$self->compile_expr($node->[3], $indent).";
$indent\$\$out_ref .= defined(\$var) ? \$var : \$self->undefined_get(".$self->ast_string($node->[3]).");";
    return;
}

sub compile_EVAL {
    my ($self, $node, $str_ref, $indent) = @_;
    my ($named, @strs) = @{ $node->[3] };

    $$str_ref .= "
${indent}foreach (".join(",\n", map {$self->ast_string($_)} @strs).") {
${indent}${INDENT}my \$str = \$self->play_expr(\$_);
${indent}${INDENT}next if ! defined \$str;
${indent}${INDENT}\$\$out_ref .= \$self->play_expr([[undef, '-temp-', \$str], 0, '|', 'eval', [".$self->ast_string($named)."]]);
${indent}}";
}

sub compile_FILTER {
    my ($self, $node, $str_ref, $indent) = @_;
    my ($name, $filter) = @{ $node->[3] };
    return if ! @$filter;

    $$str_ref .= "
${indent}\$var = do {
${indent}${INDENT}my \$filter = ".$self->ast_string($filter).";";

    ### allow for alias
    if (length $name) {
        $name =~ s/\'/\\\'/g;
        $$str_ref .= "\n${indent}${INDENT}\$self->{'FILTERS'}->{'$name'} = \$filter; # alias for future calls\n";
    }

    $$str_ref .= "
${indent}${INDENT}my \$out = '';
${indent}${INDENT}my \$out_ref = \\\$out;"
.$self->compile_tree($node->[4], "$indent$INDENT")."

${indent}\$out = \$self->play_expr([[undef, '-temp-', \$out], 0, '|', \@\$filter]);
${indent}${INDENT}\$out;
${indent}};
${indent}\$\$out_ref .= \$var if defined \$var;";

}

sub compile_FOR {
    my ($self, $node, $str_ref, $indent) = @_;

    my ($name, $items) = @{ $node->[3] };
    local $self->{'_in_loop'} = 'FOREACH';
    my $code = $self->compile_tree($node->[4], "$indent$INDENT");

    $$str_ref .= "\n${indent}do {
${indent}my \$loop = ".$self->compile_expr($items, $indent).";
${indent}\$loop = [] if ! defined \$loop;
${indent}\$loop = \$self->iterator(\$loop) if ref(\$loop) !~ /Iterator\$/;
${indent}local \$self->{'_vars'}->{'loop'} = \$loop;";
    if (! defined $name) {
        $$str_ref .= "
${indent}my \$swap = \$self->{'_vars'};
${indent}local \$self->{'_vars'} = my \$copy = {%\$swap};";
    }

    $$str_ref .= "
${indent}my (\$var, \$error) = \$loop->get_first;
${indent}FOREACH: while (! \$error) {";

    if (defined $name) {
        $$str_ref .= "\n$indent$INDENT\$self->set_variable(".$self->ast_string($name).", \$var);";
    } else {
        $$str_ref .= "\n$indent$INDENT\@\$copy{keys %\$var} = values %\$var if ref(\$var) eq 'HASH';";
    }

    $$str_ref .= "$code
${indent}${INDENT}(\$var, \$error) = \$loop->get_next;
${indent}}
${indent}};";
    return;
}

sub compile_FOREACH { shift->compile_FOR(@_) }

sub compile_IF {
    my ($self, $node, $str_ref, $indent) = @_;

    $$str_ref .= "\n${indent}if (".$self->compile_expr($node->[3], $indent).") {";
    $$str_ref .= $self->compile_tree($node->[4], "$indent$INDENT");

    while ($node = $node->[5]) { # ELSE, ELSIF's
        $$str_ref .= _node_info($self, $node, $indent);
        if ($node->[0] eq 'ELSE') {
            $$str_ref .= "\n${indent}} else {";
            $$str_ref .= $self->compile_tree($node->[4], "$indent$INDENT");
            last;
        } else {
            $$str_ref .= "\n${indent}} elsif (".$self->compile_expr($node->[3], $indent).") {";
            $$str_ref .= $self->compile_tree($node->[4], "$indent$INDENT");
        }
    }
    $$str_ref .= "\n${indent}}";
}

sub compile_INCLUDE {
    my ($self, $node, $str_ref, $indent) = @_;
    _compile_defer_to_play($self, $node, $str_ref, $indent);
}

sub compile_INSERT {
    my ($self, $node, $str_ref, $indent) = @_;
    _compile_defer_to_play($self, $node, $str_ref, $indent);
}

sub compile_JS {
    my ($self, $node, $str_ref, $indent) = @_;
    _compile_defer_to_play($self, $node, $str_ref, $indent);
}

sub compile_LAST {
    my ($self, $node, $str_ref, $indent) = @_;
    my $type = $self->{'_in_loop'} || die "Found LAST while not in FOR, FOREACH or WHILE";
    $$str_ref .= "\n${indent}last $type;";
    return;
}

sub compile_LOOP {
    my ($self, $node, $str_ref, $indent) = @_;
    my $ref = $node->[3];
    $ref = [$ref, 0] if ! ref $ref;

    $$str_ref .= "
${indent}\$var = ".$self->compile_expr($ref, $indent).";
${indent}if (\$var) {
${indent}${INDENT}my \$global = ! \$self->{'SYNTAX'} || \$self->{'SYNTAX'} ne 'ht' || \$self->{'GLOBAL_VARS'};
${indent}${INDENT}my \$items  = ref(\$var) eq 'ARRAY' ? \$var : ref(\$var) eq 'HASH' ? [\$var] : [];
${indent}${INDENT}my \$i = 0;
${indent}${INDENT}for my \$ref (\@\$items) {
${indent}${INDENT}${INDENT}\$self->throw('loop', 'Scalar value used in LOOP') if \$ref && ref(\$ref) ne 'HASH';
${indent}${INDENT}${INDENT}local \$self->{'_vars'} = (! \$global) ? (\$ref || {}) : (ref(\$ref) eq 'HASH') ? {%{ \$self->{'_vars'} }, %\$ref} : \$self->{'_vars'};
${indent}${INDENT}${INDENT}\@{ \$self->{'_vars'} }{qw(__counter__ __first__ __last__ __inner__ __odd__)}
${indent}${INDENT}${INDENT}${INDENT}= (++\$i, (\$i == 1 ? 1 : 0), (\$i == \@\$items ? 1 : 0), (\$i == 1 || \$i == \@\$items ? 0 : 1), (\$i % 2) ? 1 : 0)
${indent}${INDENT}${INDENT}${INDENT}${INDENT}if \$self->{'LOOP_CONTEXT_VARS'} && ! \$Template::Alloy::QR_PRIVATE;"
.$self->compile_tree($node->[4], "$indent$INDENT$INDENT")."

${indent}${INDENT}}
${indent}}";
}

sub compile_MACRO {
    my ($self, $node, $str_ref, $indent) = @_;
    my ($name, $args) = @{ $node->[3] };

    ### get the sub tree
    my $sub_tree = $node->[4];
    if (! $sub_tree || ! $sub_tree->[0]) {
        $$str_ref .= "
${indent}\$self->set_variable(".$self->ast_string($name).", undef);";
        return;
    } elsif (ref($sub_tree->[0]) && $sub_tree->[0]->[0] eq 'BLOCK') {
        $sub_tree = $sub_tree->[0]->[4];
    }

    my $code = $self->compile_tree($sub_tree, "$indent$INDENT");

    $$str_ref .= "
${indent}do {
${indent}my \$self_copy = \$self;
${indent}eval {require Scalar::Util; Scalar::Util::weaken(\$self_copy)};
${indent}\$var = sub {
${indent}${INDENT}my \$copy = \$self_copy->{'_vars'};
${indent}${INDENT}local \$self_copy->{'_vars'}= {%\$copy};

${indent}${INDENT}local \$self_copy->{'_macro_recurse'} = \$self_copy->{'_macro_recurse'} || 0;
${indent}${INDENT}my \$max = \$self_copy->{'MAX_MACRO_RECURSE'} || \$Template::Alloy::MAX_MACRO_RECURSE;
${indent}${INDENT}\$self_copy->throw('macro_recurse', \"MAX_MACRO_RECURSE \$max reached\")
${indent}${INDENT}${INDENT}if ++\$self_copy->{'_macro_recurse'} > \$max;
";

    foreach my $var (@$args) {
        $$str_ref .= "
${indent}${INDENT}\$self_copy->set_variable(";
        $$str_ref .= $self->ast_string($var);
        $$str_ref .= ", shift(\@_));";
    }
    $$str_ref .= "
${indent}${INDENT}if (\@_ && \$_[-1] && UNIVERSAL::isa(\$_[-1],'HASH')) {
${indent}${INDENT}${INDENT}my \$named = pop \@_;
${indent}${INDENT}${INDENT}foreach my \$name (sort keys %\$named) {
${indent}${INDENT}${INDENT}${INDENT}\$self_copy->set_variable([\$name, 0], \$named->{\$name});
${indent}${INDENT}${INDENT}}
${indent}${INDENT}}

${indent}${INDENT}my \$out = '';
${indent}${INDENT}my \$out_ref = \\\$out;$code
${indent}${INDENT}return \$out;
${indent}};
${indent}\$self->set_variable(".$self->ast_string($name).", \$var);
${indent}};";

    return;
}

sub compile_META {
    my ($self, $node, $str_ref, $indent) = @_;
    if (my $kp = $node->[3]) {
        $kp = {@$kp} if ref($kp) eq 'ARRAY';
        while (my($key, $val) = each %$kp) {
            s/\'/\\\'/g foreach $key, $val;
            $self->{'_meta'} .= "\n${indent}'$key' => '$val',";
        }
    }
    return;
}

sub compile_NEXT {
    my ($self, $node, $str_ref, $indent) = @_;
    my $type = $self->{'_in_loop'} || die "Found next while not in FOR, FOREACH or WHILE";
    $$str_ref .= "\n${indent}(\$var, \$error) = \$loop->get_next;" if $type eq 'FOREACH';
    $$str_ref .= "\n${indent}next $type;";
    return;
}

sub compile_PERL{
    my ($self, $node, $str_ref, $indent) = @_;

    ### fill in any variables
    my $perl = $node->[4] || return;
    my $code = $self->compile_tree($perl, "$indent$INDENT");

    $$str_ref .= "
${indent}\$self->throw('perl', 'EVAL_PERL not set') if ! \$self->{'EVAL_PERL'};
${indent}require Template::Alloy::Play;
${indent}\$var = do {
${indent}${INDENT}my \$out = '';
${indent}${INDENT}my \$out_ref = \\\$out;$code
${indent}${INDENT}\$out;
${indent}};
${indent}#\$var = \$1 if \$var =~ /^(.+)\$/s; # blatant untaint

${indent}my \$err;
${indent}eval {
${indent}${INDENT}package Template::Alloy::Perl;
${indent}${INDENT}my \$context = \$self->context;
${indent}${INDENT}my \$stash   = \$context->stash;
${indent}${INDENT}local *PERLOUT;
${indent}${INDENT}tie *PERLOUT, 'Template::Alloy::EvalPerlHandle', \$out_ref;
${indent}${INDENT}my \$old_fh = select PERLOUT;
${indent}${INDENT}eval \$var;
${indent}${INDENT}\$err = \$\@;
${indent}${INDENT}select \$old_fh;
${indent}};
${indent}\$err ||= \$\@;
${indent}if (\$err) {
${indent}${INDENT}\$self->throw('undef', \$err) if ! UNIVERSAL::can(\$err, 'type');
${indent}${INDENT}die \$err;
${indent}}";

    return;
}


sub compile_PROCESS {
    my ($self, $node, $str_ref, $indent) = @_;
    _compile_defer_to_play($self, $node, $str_ref, $indent);
}

sub compile_RAWPERL {
    my ($self, $node, $str_ref, $indent) = @_;
    _compile_defer_to_play($self, $node, $str_ref, $indent);
}

sub compile_RETURN {
    my ($self, $node, $str_ref, $indent) = @_;

    if (defined($node->[3])) {
        $$str_ref .= "
${indent}\$var = {return_val => ".$self->compile_expr($node->[3])."};
${indent}\$self->throw('return', \$var);";
    } else {
        $$str_ref .= "
${indent}\$self->throw('return', undef);";
    }
}

sub compile_SET {
    my ($self, $node, $str_ref, $indent) = @_;
    my $sets = $node->[3];

    my $out = '';
    foreach (@$sets) {
        my ($op, $set, $val) = @$_;

        if ($self->{'_is_default'}) {
            $$str_ref .= "\n${indent}if (! ".$self->compile_expr($set, $indent).") {";
            $indent .= $INDENT;
        }
        $$str_ref .= "\n$indent\$var = ";

        if (! defined $val) { # not defined
            $$str_ref .= 'undef';
        } elsif ($node->[4] && $val == $node->[4]) { # a captured directive
            my $sub_tree = $node->[4];
            $sub_tree = $sub_tree->[0]->[4] if $sub_tree->[0] && $sub_tree->[0]->[0] eq 'BLOCK';
            my $code = $self->compile_tree($sub_tree, "$indent$INDENT");
            $$str_ref .= "${indent}do {
${indent}${INDENT}my \$out = '';
${indent}${INDENT}my \$out_ref = \\\$out;$code
${indent}${INDENT}\$out;
${indent}}"
        } else { # normal var
            $$str_ref .= $self->compile_expr($val, $indent);
        }

        if ($Template::Alloy::OP_DISPATCH->{$op}) {
            $$str_ref .= ' }';
        }

        $$str_ref .= ";
$indent\$self->set_variable(".$self->ast_string($set).", \$var);";

        if ($self->{'_is_default'}) {
            substr($indent, -length($INDENT), length($INDENT), '');
            $$str_ref .= "\n$indent}";
        }

        $$str_ref .= ";";
    }

    return $out;
}

sub compile_STOP {
    my ($self, $node, $str_ref, $indent) = @_;
    $$str_ref .= "
${indent}\$self->throw('stop', 'Control Exception');";
}

sub compile_SWITCH {
    my ($self, $node, $str_ref, $indent) = @_;

    $$str_ref .= "
${indent}\$var = ".$self->compile_expr($node->[3], $indent).";";

    my $default;
    my $i = 0;
    while ($node = $node->[5]) { # CASES
        if (! defined $node->[3]) {
            $default = $node;
            next;
        }

        $$str_ref .= _node_info($self, $node, $indent);
        $$str_ref .= "\n$indent" .($i++ ? "} els" : ""). "if (do {
${indent}${INDENT}no warnings;
${indent}${INDENT}my \$var2 = ".$self->compile_expr($node->[3], "$indent$INDENT").";
${indent}${INDENT}scalar grep {\$_ eq \$var} (UNIVERSAL::isa(\$var2, 'ARRAY') ? \@\$var2 : \$var2);
${indent}${INDENT}}) {
${indent}${INDENT}my \$var;";

        $$str_ref .= $self->compile_tree($node->[4], "$indent$INDENT");
    }

    if ($default) {
        $$str_ref .= _node_info($self, $default, $indent);
        $$str_ref .= "\n$indent" .($i++ ? "} else {" : "if (1) {");
        $$str_ref .= $self->compile_tree($default->[4], "$indent$INDENT");
    }

    $$str_ref .= "\n$indent}" if $i;

    return;
}

sub compile_THROW {
    my ($self, $node, $str_ref, $indent) = @_;

    my ($name, $args) = @{ $node->[3] };

    my ($named, @args) = @$args;
    push @args, $named if ! _is_empty_named_args($named); # add named args back on at end - if there are some

    $$str_ref .= "
${indent}\$self->throw(".$self->compile_expr($name, $indent).", [".join(", ", map{$self->compile_expr($_, $indent)} @args)."]);";
    return;
}


sub compile_TRY {
    my ($self, $node, $str_ref, $indent) = @_;

    $$str_ref .= "
${indent}do {
${indent}my \$out = '';
${indent}eval {
${indent}${INDENT}my \$out_ref = \\\$out;"
    . $self->compile_tree($node->[4], "$indent$INDENT") ."
${indent}};
${indent}my \$err = \$\@;
${indent}\$\$out_ref .= \$out;
${indent}if (\$err) {";

    my $final;
    my $i = 0;
    my $catches_str = '';
    my @names;
    while ($node = $node->[5]) { # CATCHES
        if ($node->[0] eq 'FINAL') {
            $final = $node;
            next;
        }
        $catches_str .= _node_info($self, $node, "$indent$INDENT");
        $catches_str .= "\n${indent}${INDENT}} elsif (\$index == ".(scalar @names).") {";
        $catches_str .= $self->compile_tree($node->[4], "$indent$INDENT$INDENT");
        push @names, $node->[3];
    }
    if (@names) {
        $$str_ref .= "
${indent}${INDENT}\$err = \$self->exception('undef', \$err) if ! UNIVERSAL::can(\$err, 'type');
${indent}${INDENT}my \$type = \$err->type;
${indent}${INDENT}die \$err if \$type =~ /stop|return/;
${indent}${INDENT}local \$self->{'_vars'}->{'error'} = \$err;
${indent}${INDENT}local \$self->{'_vars'}->{'e'}     = \$err;

${indent}${INDENT}my \$index;
${indent}${INDENT}my \@names = (";
        $i = 0;
        foreach $i (0 .. $#names) {
            if (defined $names[$i]) {
                $$str_ref .= "\n${indent}${INDENT}${INDENT}scalar(".$self->compile_expr($names[$i], "$indent$INDENT$INDENT")."), # $i;";
            } else {
                $$str_ref .= "\n${indent}${INDENT}${INDENT}undef, # $i";
            }
        }
        $$str_ref .= "
${indent}${INDENT});
${indent}${INDENT}for my \$i (0 .. \$#names) {
${indent}${INDENT}${INDENT}my \$name = (! defined(\$names[\$i]) || lc(\$names[\$i]) eq 'default') ? '' : \$names[\$i];
${indent}${INDENT}${INDENT}\$index = \$i if \$type =~ m{^ \\Q\$name\\E \\b}x && (! defined(\$index) || length(\$names[\$index]) < length(\$name));
${indent}${INDENT}}
${indent}${INDENT}if (! defined \$index) {
${indent}${INDENT}${INDENT}die \$err;"
.$catches_str."
${indent}${INDENT}}";

    } else {
        $$str_ref .= "
${indent}\$self->throw('throw', 'Missing CATCH block');";
    }
    $$str_ref .= "
${indent}}";
    if ($final) {
        $$str_ref .= _node_info($self, $final, $indent);
        $$str_ref .= $self->compile_tree($final->[4], "$indent");
    }
    $$str_ref .="
${indent}};";

    return;
}

sub compile_UNLESS { $DIRECTIVES->{'IF'}->(@_) }

sub compile_USE {
    my ($self, $node, $str_ref, $indent) = @_;
    _compile_defer_to_play($self, $node, $str_ref, $indent);
}

sub compile_VIEW {
    my ($self, $node, $str_ref, $indent) = @_;
    my ($blocks, $args, $name) = @{ $node->[3] };

    my $_name = $self->ast_string($name);

    # [[undef, '{}', 'key1', 'val1', 'key2', 'val2'], 0]
    $args = $args->[0];
    $$str_ref .= "
${indent}do {
${indent}${INDENT}my \$name = $_name;
${indent}${INDENT}my \$hash = {};";
    foreach (my $i = 2; $i < @$args; $i+=2) {
        $$str_ref .= "
${indent}${INDENT}\$var = ".$self->compile_expr($args->[$i+1], $indent).";
${indent}${INDENT}";
        my $key = $args->[$i];
        if (ref $key) {
            if (@$key == 2 && ! ref($key->[0]) && ! $key->[1]) {
                $key = $key->[0];
            } else {
                $$str_ref .= "
${indent}${INDENT}\$self->set_variable(".$self->compile_expr($key, $indent).", \$var);";
                next;
            }
        }
        $key =~ s/([\'\\])/\\$1/g;
        $$str_ref .= "\$hash->{'$key'} = \$var;";
    }

    $$str_ref .= "
${indent}${INDENT}my \$prefix = \$hash->{'prefix'} || (ref(\$name) && \@\$name == 2 && ! \$name->[1] && ! ref(\$name->[0])) ? \"\$name->[0]/\" : '';
${indent}${INDENT}my \$blocks = \$hash->{'blocks'} = {};";
    foreach my $key (keys %$blocks) {
        my $code = $self->compile_tree($blocks->{$key}, "$indent$INDENT$INDENT$INDENT");
        $key =~ s/([\'\\])/\\$1/g;
        $$str_ref .= "
${indent}${INDENT}\$blocks->{'$key'} = {
${indent}${INDENT}${INDENT}name  => \$prefix . '$key',
${indent}${INDENT}${INDENT}_perl => {code => sub {
${indent}${INDENT}${INDENT}${INDENT}my (\$self, \$out_ref, \$var) = \@_;$code

${indent}${INDENT}${INDENT}${INDENT}return 1;
${indent}${INDENT}${INDENT}} },
${indent}${INDENT}};";
    }

    $$str_ref .= "
${indent}${INDENT}\$self->throw('view', 'Could not load Template::View library')
${indent}${INDENT}${INDENT} if ! eval { require Template::View };
${indent}${INDENT}my \$view = Template::View->new(\$self->context, \$hash)
${indent}${INDENT}${INDENT}|| \$self->throw('view', \$Template::View::ERROR);
${indent}${INDENT}my \$old_view = \$self->play_expr(['view', 0]);
${indent}${INDENT}\$self->set_variable(\$name, \$view);
${indent}${INDENT}\$self->set_variable(['view', 0], \$view);";

    if ($node->[4]) {
        $$str_ref .= "
${indent}${INDENT}my \$out = '';
${indent}${INDENT}my \$out_ref = \\\$out;"
    .$self->compile_tree($node->[4], "$indent$INDENT");
    }

    $$str_ref .= "
${indent}${INDENT}\$self->set_variable(['view', 0], \$old_view);
${indent}${INDENT}\$view->seal;
${indent}};";


    return;
}

sub compile_WHILE {
    my ($self, $node, $str_ref, $indent) = @_;

    local $self->{'_in_loop'} = 'WHILE';
    my $code = $self->compile_tree($node->[4], "$indent$INDENT");

    $$str_ref .= "
${indent}my \$count = \$Template::Alloy::WHILE_MAX;
${indent}WHILE: while (--\$count > 0) {
${indent}my \$var = ".$self->compile_expr($node->[3], $indent).";
${indent}last if ! \$var;$code
${indent}}";
    return;
}

sub compile_WRAPPER {
    my ($self, $node, $str_ref, $indent) = @_;

    my ($named, @files) = @{ $node->[3] };
    $named = $self->ast_string($named);

    $$str_ref .= "
${indent}\$var = do {
${indent}${INDENT}my \$out = '';
${indent}${INDENT}my \$out_ref = \\\$out;"
.$self->compile_tree($node->[4], "$indent$INDENT")."
${indent}${INDENT}\$out;
${indent}};
${indent}for my \$file (reverse("
.join(",${indent}${INDENT}", map {"\$self->play_expr(".$self->ast_string($_).")"} @files).")) {
${indent}${INDENT}local \$self->{'_vars'}->{'content'} = \$var;
${indent}${INDENT}\$var = '';
${indent}${INDENT}require Template::Alloy::Play;
${indent}\$Template::Alloy::Play::DIRECTIVES->{'INCLUDE'}->(\$self, [$named, \$file], ['$node->[0]', $node->[1], $node->[2]], \\\$var);
${indent}}
${indent}\$\$out_ref .= \$var if defined \$var;";

    return;
}


###----------------------------------------------------------------###

1;

__END__

=head1 DESCRIPTION

The Template::Alloy::Compile role allows for taking the AST returned
by the Parse role, and translating it into a perl code document.  This
is in contrast Template::Alloy::Play which executes the AST directly.

=head1 TODO

=over 4

=item

Translate compile_RAWPERL to actually output rather than calling play_RAWPERL.

=back

=head1 ROLE METHODS

=over 4

=item C<compile_tree>

Takes an AST returned by parse_tree and translates it into
perl code using functions stored in the $DIRECTIVES hashref.

A template that looked like the following:

    Foo
    [% GET foo %]
    [% GET bar %]
    Bar

would parse to the following perl code:

    # Generated by Template::Alloy::Compile v1.001 on Thu Jun  7 12:58:33 2007
    # From file /home/paul/bar.tt

    my $blocks = {};
    my $meta   = {};
    my $code   = sub {
        my ($self, $out_ref, $var) = @_;

        $$out_ref .= 'Foo';

        # "GET" Line 2 char 2 (chars 6 to 15)
        $var = $self->play_expr(['foo', 0]);
        $$out_ref .= defined($var) ? $var : $self->undefined_get(['foo', 0]);

        # "GET" Line 3 char 2 (chars 22 to 31)
        $var = $self->play_expr(['bar', 0]);
        $$out_ref .= defined($var) ? $var : $self->undefined_get(['bar', 0]);

        $$out_ref .= 'Bar';

        return 1;
    };

    {
        blocks => $blocks,
        meta   => $meta,
        code   => $code,
    };

As you can see the output is quite a bit more complex than the AST, but under
mod_perl conditions, the perl will run faster than playing the AST each time.

=item C<compile_expr>

Takes an AST variable or expression and returns perl code that can lookup
the variable.

=back

=head1 AUTHOR

Paul Seamons <paul@seamons.com>

=head1 LICENSE

This module may be distributed under the same terms as Perl itself.

=cut