Sponsoring The Perl Toolchain Summit 2025: Help make this important event another success Learn more

use Mouse;
use Scalar::Util ();
use Carp ();
$DEBUG
value_to_literal
is_int any_in
make_error
p
);
#use constant _VERBOSE => scalar($DEBUG =~ /\b verbose \b/xms);
use constant {
_DUMP_ASM => scalar($DEBUG =~ /\b dump=asm \b/xms),
_DUMP_AST => scalar($DEBUG =~ /\b dump=ast \b/xms),
_DUMP_GEN => scalar($DEBUG =~ /\b dump=gen \b/xms),
_DUMP_CAS => scalar($DEBUG =~ /\b dump=cascade \b/xms),
_OP_NAME => 0,
_OP_ARG => 1,
_OP_LINE => 2,
_OP_FILE => 3,
_OP_LABEL => 4,
_OP_COMMENT => 5,
_FOR_LOOP => 1,
_WHILE_LOOP => 2,
};
our $OPTIMIZE = scalar(($DEBUG =~ /\b optimize=(\d+) \b/xms)[0]);
if(not defined $OPTIMIZE) {
$OPTIMIZE = 1; # enable optimization by default
}
our @CARP_NOT = qw(Text::Xslate Text::Xslate::Parser);
{
package Text::Xslate;
our %OPS; # to avoid 'once' warnings;
}
my %binary = (
'==' => 'eq',
'!=' => 'ne',
'<' => 'lt',
'<=' => 'le',
'>' => 'gt',
'>=' => 'ge',
'~~' => 'match',
'<=>' => 'ncmp',
'cmp' => 'scmp',
'+' => 'add',
'-' => 'sub',
'*' => 'mul',
'/' => 'div',
'%' => 'mod',
'~' => 'concat',
'x' => 'repeat',
'+|' => 'bitor',
'+&' => 'bitand',
'+^' => 'bitxor',
'min' => 'lt', # a < b ? a : b
'max' => 'gt', # a > b ? a : b
'[' => 'fetch_field',
);
my %logical_binary = (
'&&' => 'and',
'||' => 'or',
'//' => 'dor',
);
my %unary = (
'!' => 'not',
'+' => 'noop',
'-' => 'minus',
'+^' => 'bitneg',
'max_index' => 'max_index', # for loop context vars
);
my %goto_family = map { $_ => undef } qw(
for_iter
and
dand
or
dor
goto
);
my %builtin = (
'html_escape' => ['builtin_html_escape',
\&Text::Xslate::Util::html_escape],
'uri_escape' => ['builtin_uri_escape',
\&Text::Xslate::Util::uri_escape],
'mark_raw' => ['builtin_mark_raw',
\&Text::Xslate::Util::mark_raw],
'unmark_raw' => ['builtin_unmark_raw',
\&Text::Xslate::Util::unmark_raw],
'raw' => ['builtin_mark_raw',
\&Text::Xslate::Util::mark_raw],
'html' => ['builtin_html_escape',
\&Text::Xslate::Util::html_escape],
'uri' => ['builtin_uri_escape',
\&Text::Xslate::Util::uri_escape],
'is_array_ref' => ['builtin_is_array_ref',
\&Text::Xslate::Util::is_array_ref],
'is_hash_ref' => ['builtin_is_hash_ref',
\&Text::Xslate::Util::is_hash_ref],
);
has lvar_id => ( # local variable id
is => 'rw',
isa => 'Int',
init_arg => undef,
);
has lvar => ( # local variable id table
is => 'rw',
isa => 'HashRef[Int]',
init_arg => undef,
);
has const => (
is => 'rw',
isa => 'ArrayRef',
init_arg => undef,
);
has macro_table => (
is => 'rw',
isa => 'HashRef',
predicate => 'has_macro_table',
init_arg => undef,
);
has engine => ( # Xslate engine
is => 'ro',
isa => 'Object',
required => 0,
weak_ref => 1,
);
has dependencies => (
is => 'ro',
isa => 'ArrayRef',
init_arg => undef,
);
has type => (
is => 'rw',
isa => enum([qw(html xml text)]),
default => 'html',
);
has syntax => (
is => 'rw',
default => 'Kolon',
);
has parser_option => (
is => 'rw',
isa => 'HashRef',
default => sub { {} },
);
has parser => (
is => 'rw',
isa => 'Object', # Text::Xslate::Parser
handles => [qw(define_function)],
lazy => 1,
builder => '_build_parser',
init_arg => undef,
);
has input_layer => (
is => 'ro',
default => ':utf8',
);
sub _build_parser {
my($self) = @_;
my $syntax = $self->syntax;
if(ref($syntax)) {
return $syntax;
}
else {
my $parser_class = Mouse::Util::load_first_existing_class(
"Text::Xslate::Syntax::" . $syntax,
$syntax,
);
return $parser_class->new(
%{$self->parser_option},
engine => $self->engine,
compiler => $self,
);
}
}
has cascade => (
is => 'rw',
init_arg => undef,
);
has [qw(header footer macro)] => (
is => 'rw',
isa => 'ArrayRef',
);
has current_file => (
is => 'rw',
init_arg => undef,
);
has file => (
is => 'rw',
init_arg => undef,
);
has overridden_builtin => (
is => 'ro',
isa => 'HashRef',
default => sub { +{} },
);
sub lvar_use {
my($self, $n) = @_;
return $self->lvar_id + $n;
}
sub filename {
my($self) = @_;
my $file = $self->file;
return ref($file) ? '<string>' : $file;
}
sub compile {
my($self, $input, %args) = @_;
# each compiling process is independent
local $self->{macro_table} = {};
local $self->{lvar_id } = 0;
local $self->{lvar} = {};
local $self->{const} = [];
local $self->{in_loop} = 0;
local $self->{dependencies} = [];
local $self->{cascade};
local $self->{header} = $self->{header};
local $self->{footer} = $self->{footer};
local $self->{macro} = $self->{macro};
local $self->{current_file} = '<string>'; # for opinfo
local $self->{file} = $args{file} || \$input;
if(my $engine = $self->engine) {
my $ob = $self->overridden_builtin;
Internals::SvREADONLY($ob, 0);
foreach my $name(keys %builtin) {
my $f = $engine->{function}{$name};
$ob->{$name} = ( $builtin{$name}[1] != $f ) + 0;
}
Internals::SvREADONLY($ob, 1);
}
my $parser = $self->parser;
my $header = delete $self->{header};
my $footer = delete $self->{footer};
my $macro = delete $self->{macro};
if(!$args{omit_augment}) {
if($header) {
substr $input, 0, 0, $self->_cat_files($header);
}
if($footer) {
$input .= $self->_cat_files($footer);
}
}
if($macro) {
if(!grep { $_ eq $self->current_file } @$macro) {
substr $input, 0, 0, $self->_cat_files($macro);
}
}
my @code; # main code
{
my $ast = $parser->parse($input, %args);
print STDERR p($ast) if _DUMP_AST;
@code = (
$self->opcode(set_opinfo => undef, file => $self->current_file, line => 1),
$self->compile_ast($ast),
$self->opcode('end'),
);
}
my $cascade = $self->cascade;
if(defined $cascade) {
$self->_process_cascade($cascade, \%args, \@code);
}
push @code, $self->_flush_macro_table() if $self->has_macro_table;
if($OPTIMIZE) {
$self->_optimize_vmcode(\@code) for 1 .. 3;
}
print STDERR "// ", $self->filename, "\n",
$self->as_assembly(\@code, scalar($DEBUG =~ /\b ix \b/xms))
if _DUMP_ASM;
{
my %uniq;
push @code,
map { [ depend => $_ ] }
grep { !ref($_) and !$uniq{$_}++ } @{$self->dependencies};
}
return \@code;
}
sub opcode { # build an opcode
my($self, $name, $arg, %args) = @_;
my $symbol = $args{symbol};
my $file = $args{file};
my $label = $args{label};
if(not defined $file) {
$file = $self->filename;
if(defined $file and $file ne $self->current_file) {
$self->current_file($file);
}
else {
$file = undef;
}
}
# name, arg, label, line, file, comment
return [ $name => $arg,
$args{line} || (ref $symbol ? $symbol->line : undef),
$file,
$label,
$args{comment},
];
}
sub push_expr {
my($self, $node) = @_;
my $list_op = $node->arity eq 'range';
my @code = ($self->compile_ast($node));
if(not $list_op) {
push @code, $self->opcode('push');
}
return @code;
}
sub _cat_files {
my($self, $files) = @_;
my $engine = $self->engine || $self->_error("No Xslate engine which header/footer requires");
my $s = '';
foreach my $file(@{$files}) {
my $fullpath = $engine->find_file($file)->{fullpath};
$s .= $engine->slurp_template( $self->input_layer, $fullpath );
$self->requires($fullpath);
}
return $s;
}
our $_lv = -1;
sub compile_ast {
my($self, $ast) = @_;
return if not defined $ast;
local $_lv = $_lv + 1 if _DUMP_GEN;
my @code;
foreach my $node(ref($ast) eq 'ARRAY' ? @{$ast} : $ast) {
Scalar::Util::blessed($node) or Carp::confess("[BUG] Not a node object: " . p($node));
printf STDERR "%s"."generate %s (%s)\n", "." x $_lv, $node->arity, $node->id if _DUMP_GEN;
my $generator = $self->can('_generate_' . $node->arity)
|| Carp::confess("[BUG] Unexpected node: " . p($node));
push @code, $self->$generator($node);
}
return @code;
}
sub _process_cascade {
my($self, $cascade, $args, $main_code) = @_;
printf STDERR "# cascade %s %s", $self->file, $cascade->dump if _DUMP_CAS;
my $engine = $self->engine
|| $self->_error("Cannot cascade templates without Xslate engine", $cascade);
my($base_file, $base_code);
my $base = $cascade->first;
my @components = $cascade->second
? (map{ $self->_bare_to_file($_) } @{$cascade->second})
: ();
my $vars = $cascade->third;
if(defined $base) { # pure cascade
$base_file = $self->_bare_to_file($base);
$base_code = $engine->load_file($base_file);
$self->requires( $engine->find_file($base_file)->{fullpath} );
}
else { # overlay
$base_file = $args->{file}; # only for error messages
$base_code = $main_code;
if(defined $args->{fullpath}) {
$self->requires( $args->{fullpath} );
}
push @{$main_code}, $self->_flush_macro_table();
}
foreach my $cfile(@components) {
my $code = $engine->load_file($cfile);
my $fullpath = $engine->find_file($cfile)->{fullpath};
my $mtable = $self->macro_table;
my $macro;
foreach my $c(@{$code}) {
# $c = [name, arg, line, file, symbol ]
# retrieve macros from assembly code
if($c->[_OP_NAME] eq 'macro_begin' .. $c->[_OP_NAME] eq 'macro_end') {
if($c->[_OP_NAME] eq 'macro_begin') {
$macro = [];
$macro = {
name => $c->[_OP_ARG],
line => $c->[_OP_LINE],
file => $c->[_OP_FILE],
body => [],
};
push @{ $mtable->{$c->[_OP_ARG]} ||= [] }, $macro;
}
elsif($c->[_OP_NAME] eq 'macro_nargs') {
$macro->{nargs} = $c->[_OP_ARG];
}
elsif($c->[_OP_NAME] eq 'macro_outer') {
$macro->{outer} = $c->[_OP_ARG];
}
elsif($c->[_OP_NAME] eq 'macro_end') {
# noop
}
else {
push @{$macro->{body}}, $c;
}
}
elsif($c->[_OP_NAME] eq 'depend') {
$self->requires($c->[_OP_ARG]);
}
}
$self->requires($fullpath);
$self->_process_cascade_file($cfile, $base_code);
}
if(defined $base) { # pure cascade
$self->_process_cascade_file($base_file, $base_code);
if(defined $vars) {
unshift @{$base_code}, $self->_localize_vars($vars);
}
foreach my $c(@{$main_code}) {
if($c->[_OP_NAME] eq 'print_raw_s'
&& $c->[_OP_ARG] =~ m{ [^ \t\r\n] }xms) {
Carp::carp("Xslate: Useless use of text '$c->[1]'");
}
}
@{$main_code} = @{$base_code};
}
else { # overlay
return;
}
}
sub _process_cascade_file {
my($self, $file, $base_code) = @_;
printf STDERR "# cascade file %s\n", p($file) if _DUMP_CAS;
my $mtable = $self->macro_table;
for(my $i = 0; $i < @{$base_code}; $i++) {
my $c = $base_code->[$i];
if($c->[_OP_NAME] ne 'macro_begin') {
next;
}
# macro
my $name = $c->[_OP_ARG];
$name =~ s/\@.+$//;
printf STDERR "# macro %s\n", $name if _DUMP_CAS;
if(exists $mtable->{$name}) {
my $m = $mtable->{$name};
if(ref($m) ne 'HASH') {
$self->_error('[BUG] Unexpected macro structure: '
. p($m) );
}
$self->_error(
"Redefinition of macro/block $name in " . $file
. " (you must use block modifiers to override macros/blocks)",
$m->{line}
);
}
my $before = delete $mtable->{$name . '@before'};
my $around = delete $mtable->{$name . '@around'};
my $after = delete $mtable->{$name . '@after'};
if(defined $before) {
my $n = scalar @{$base_code};
foreach my $m(@{$before}) {
splice @{$base_code}, $i+1, 0, @{$m->{body}};
}
$i += scalar(@{$base_code}) - $n;
}
my $macro_start = $i+1;
$i++ while($base_code->[$i][_OP_NAME] ne 'macro_end'); # move to the end
if(defined $around) {
my @original = splice @{$base_code}, $macro_start, ($i - $macro_start);
$i = $macro_start;
my @body;
foreach my $m(@{$around}) {
push @body, @{$m->{body}};
}
for(my $j = 0; $j < @body; $j++) {
if($body[$j][_OP_NAME] eq 'super') {
splice @body, $j, 1, @original;
}
}
splice @{$base_code}, $macro_start, 0, @body;
$i += scalar(@body);
}
if(defined $after) {
foreach my $m(@{$after}) {
splice @{$base_code}, $i, 0, @{$m->{body}};
}
}
}
return;
}
sub _flush_macro_table {
my($self) = @_;
my $mtable = $self->macro_table;
my @code;
foreach my $macros(values %{$mtable}) {
foreach my $macro(ref($macros) eq 'ARRAY' ? @{$macros} : $macros) {
push @code,
$self->opcode( macro_begin => $macro->{name},
file => $macro->{file},
line => $macro->{line} );
push @code, $self->opcode( macro_nargs => $macro->{nargs} )
if $macro->{nargs};
push @code, $self->opcode( macro_outer => $macro->{outer} )
if $macro->{outer};
push @code, @{ $macro->{body} }, $self->opcode('macro_end');
}
}
%{$mtable} = ();
return @code;
}
sub _generate_name {
my($self, $node) = @_;
my $id = $node->value; # may be aliased
if(defined(my $lvar_id = $self->lvar->{$id})) { # constants
my $code = $self->const->[$lvar_id];
if(defined $code) {
# because the constant value is very simple,
# its definition is optimized away.
# only its value remains.
return @{$code};
}
else {
return $self->opcode( load_lvar => $lvar_id, symbol => $node );
}
}
return $self->opcode( fetch_symbol => $id, line => $node->line );
}
sub _generate_operator {
my($self, $node) = @_;
# This method is called when an operators is used as an expression,
# e.g. <: + :>, so simply throws the error
$self->_error("Invalid expression", $node);
}
sub _can_optimize_print {
my($self, $name, $node) = @_;
return 0 if !$OPTIMIZE;
return 0 if !($name eq 'print' or $name eq 'print_raw');
my $maybe_name = $node->first;
return $node->arity eq 'call'
&& $maybe_name->arity eq 'name'
&& @{$node->second} == 1 # args of the filter
&& any_in($maybe_name->id, qw(raw mark_raw html))
&& !$self->overridden_builtin->{$maybe_name->id};
}
# also deal with smart escaping
sub _generate_print {
my($self, $node) = @_;
my @code;
my $proc = $node->id;
if($proc eq 'print' and $self->type eq 'text') {
$proc = 'print_raw';
}
foreach my $arg(@{ $node->first }){
if( $proc eq 'print' && $self->overridden_builtin->{html_escape} ) {
# default behaviour of print() is overridden
push @code,
$self->opcode('pushmark'),
$self->compile_ast($arg),
$self->opcode('push'),
$self->opcode('fetch_symbol' => 'html_escape'),
$self->opcode('funcall'),
$self->opcode('print_raw');
}
elsif(exists $Text::Xslate::OPS{$proc . '_s'}
&& $arg->arity eq 'literal'){
push @code,
$self->opcode( $proc . '_s' => $arg->value,
line => $arg->line );
}
elsif($self->_can_optimize_print($proc, $arg)){
my $filter = $arg->first;
my $filter_name = $filter->id;
my $command = $builtin{ $filter_name }[0] eq 'builtin_mark_raw'
? 'print_raw' # mark_raw, raw
: 'print'; # html
push @code,
$self->compile_ast($arg->second->[0]),
$self->opcode(
$command => undef,
symbol => $filter );
}
else {
push @code,
$self->compile_ast($arg),
$self->opcode( $proc => undef, line => $node->line );
}
}
if(!@code) {
$self->_error("$node requires at least one argument", $node);
}
return @code;
}
sub _generate_include {
my($self, $node) = @_;
my $file = $node->first;
my @code = (
( ref($file) eq 'ARRAY'
? $self->opcode( literal => $self->_bare_to_file($file) )
: $self->compile_ast($file) ),
$self->opcode( $node->id => undef, line => $node->line ),
);
if(defined(my $vars = $node->second)) {
@code = ($self->opcode('enter'),
$self->_localize_vars($vars),
@code,
$self->opcode('leave'),
);
}
return @code;
}
sub _bare_to_file {
my($self, $file) = @_;
if(ref($file) eq 'ARRAY') { # myapp::foo
return join('/', map { $_->value } @{$file}) . $self->{engine}->{suffix};
}
elsif($file->arity eq 'literal') {
return $file->value;
}
else {
$self->_error("Expected a name or string literal", $file);
}
}
sub _generate_cascade {
my($self, $node) = @_;
if(defined $self->cascade) {
$self->_error("Cannot cascade twice in a template", $node);
}
$self->cascade( $node );
return;
}
# XXX: need more consideration
sub _compile_loop_block {
my($self, $block) = @_;
my @block_code = $self->compile_ast($block);
foreach my $op(@block_code) {
if(any_in( $op->[_OP_NAME], qw(pushmark loop_control))) {
# pushmark ... funcall (or something) may create mortal SVs
# so surround the block with ENTER and LEAVE
unshift @block_code, $self->opcode('enter');
push @block_code, $self->opcode('leave');
last;
}
}
foreach my $i(1 .. (@block_code-1)) {
my $op = $block_code[$i];
if($op->[_OP_NAME] eq 'loop_control') {
my $type = $op->[_OP_ARG];
$op->[_OP_NAME] = 'goto';
$op->[_OP_ARG] = (@block_code - $i);
$op->[_OP_ARG] += 1 if $type eq 'last';
}
}
return @block_code;
}
sub _generate_for {
my($self, $node) = @_;
my $expr = $node->first;
my $vars = $node->second;
my $block = $node->third;
if(@{$vars} != 1) {
$self->_error("A for-loop requires single variable for each item", $node);
}
local $self->{lvar} = { %{$self->lvar} }; # new scope
local $self->{const} = [ @{$self->const} ]; # new scope
local $self->{in_loop} = _FOR_LOOP;
my @code = $self->compile_ast($expr);
my($iter_var) = @{$vars};
my $lvar_id = $self->lvar_id;
my $lvar_name = $iter_var->id;
$self->lvar->{$lvar_name} = $lvar_id;
$self->lvar->{'($_)'} = $lvar_id;
push @code, $self->opcode( for_start => $lvar_id, symbol => $iter_var );
# a for statement uses three local variables (container, iterator, and item)
local $self->{lvar_id} = $self->lvar_use(3);
my @block_code = $self->_compile_loop_block($block);
push @code,
$self->opcode( literal_i => $lvar_id, symbol => $iter_var ),
$self->opcode( for_iter => scalar(@block_code) + 2 ),
@block_code,
$self->opcode( goto => -(scalar(@block_code) + 2), comment => "end for" );
return @code;
}
sub _generate_for_else {
my($self, $node) = @_;
my $for_block = $node->first;
my $else_block = $node->second;
my @code = (
$self->compile_ast($for_block),
);
# 'for' block sets __a with true if the loop count > 0
my @else = $self->compile_ast($else_block);
push @code, (
$self->opcode( or => scalar(@else) + 1, comment => 'for-else' ),
@else,
);
return @code;
}
sub _generate_while {
my($self, $node) = @_;
my $expr = $node->first;
my $vars = $node->second;
my $block = $node->third;
if(@{$vars} > 1) {
$self->_error("A while-loop requires one or zero variable for each items", $node);
}
(my $cond_op, undef, $expr) = $self->_prepare_cond_expr($expr);
# TODO: combine all the loop contexts into single one
local $self->{lvar} = { %{$self->lvar} }; # new scope
local $self->{const} = [ @{$self->const} ]; # new scope
local $self->{in_loop} = _WHILE_LOOP;
my @code = $self->compile_ast($expr);
my($iter_var) = @{$vars};
my($lvar_id, $lvar_name);
if(@{$vars}) {
$lvar_id = $self->lvar_id;
$lvar_name = $iter_var->id;
$self->lvar->{$lvar_name} = $lvar_id;
push @code, $self->opcode( save_to_lvar => $lvar_id, symbol => $iter_var );
}
local $self->{lvar_id} = $self->lvar_use(scalar @{$vars});
my @block_code = $self->_compile_loop_block($block);
return @code,
$self->opcode( $cond_op => scalar(@block_code) + 2, symbol => $node ),
@block_code,
$self->opcode( goto => -(scalar(@block_code) + scalar(@code) + 1), comment => "end while" );
return @code;
}
sub _generate_loop_control {
my($self, $node) = @_;
my $type = $node->id;
any_in($type, qw(last next))
or $self->_error("[BUG] Unknown loop control statement '$type'");
if(not $self->{in_loop}) {
$self->_error("Use of loop control statement ($type) outside of loops");
}
my @cleanup;
if( $self->{in_loop} == _FOR_LOOP && $type eq 'last' ) {
my $lvar_id = $self->lvar->{'($_)'};
defined($lvar_id)
or $self->_error('[BUG] Undefined loop iterator');
@cleanup = (
$self->opcode( 'nil', undef,
comment => 'to clean the loop context' ),
$self->opcode( save_to_lvar => $lvar_id + 0), # item
$self->opcode( save_to_lvar => $lvar_id + 1), # iterator
$self->opcode( save_to_lvar => $lvar_id + 2), # body
$self->opcode( literal_i => 1 ), # for 'for-else'
);
}
return $self->opcode('leave'),
@cleanup,
$self->opcode('loop_control' => $type, comment => $type);
}
sub _generate_proc { # definition of macro, block, before, around, after
my($self, $node) = @_;
my $type = $node->id;
my $name = $node->first->id;
my @args = map{ $_->id } @{$node->second};
my $block = $node->third;
local $self->{lvar} = { %{$self->lvar} }; # new scope
local $self->{const} = [ @{$self->const} ]; # new scope
my $lvar_used = $self->lvar_id;
my $arg_ix = 0;
foreach my $arg(@args) {
# to fetch ST(ix)
# Note that arg_ix must be start from 1
$self->lvar->{$arg} = $lvar_used + $arg_ix++;
}
local $self->{lvar_id} = $self->lvar_use($arg_ix);
my $opinfo = $self->opcode(set_opinfo => undef, file => $self->filename, line => $node->line);
my %macro = (
name => $name,
nargs => $arg_ix,
body => [ $opinfo, $self->compile_ast($block) ],
line => $opinfo->[2],
file => $opinfo->[3],
outer => $lvar_used,
);
if(any_in($type, qw(macro block))) {
if(exists $self->macro_table->{$name}) {
my $m = $self->macro_table->{$name};
if(p(\%macro) ne p($m)) {
$self->_error("Redefinition of $type $name is forbidden", $node);
}
}
$self->macro_table->{$name} = \%macro;
}
else {
my $fq_name = sprintf '%s@%s', $name, $type;
$macro{name} = $fq_name;
push @{ $self->macro_table->{ $fq_name } ||= [] }, \%macro;
}
return;
}
sub _generate_lambda {
my($self, $node) = @_;
my $macro = $node->first;
$self->compile_ast($macro);
return $self->opcode( fetch_symbol => $macro->first->id, line => $node->line );
}
sub _prepare_cond_expr {
my($self, $expr) = @_;
my $t = "and";
my $f = "or";
while($expr->id eq '!') {
$expr = $expr->first;
($t, $f) = ($f, $t);
}
if($expr->is_logical and any_in($expr->id, qw(== !=))) {
my $rhs = $expr->second;
if($rhs->arity eq "nil") {
# add prefix 'd' (i.e. "and" to "dand", "or" to "dor")
substr $t, 0, 0, 'd';
substr $f, 0, 0, 'd';
if($expr->id eq "==") {
($t, $f) = ($f, $t);
}
$expr = $expr->first;
}
}
return($t, $f, $expr);
}
sub _generate_if {
my($self, $node) = @_;
my $first = $node->first;
my $second = $node->second;
my $third = $node->third;
my($cond_true, $cond_false, $expr) = $self->_prepare_cond_expr($first);
local $self->{lvar} = { %{$self->lvar} }; # new scope
local $self->{const} = [ @{$self->const} ]; # new scope
my @cond = $self->compile_ast($expr);
my @then = do {
local $self->{lvar} = { %{$self->lvar} }; # new scope
local $self->{const} = [ @{$self->const} ]; # new scope
$self->compile_ast($second);
};
my @else = do {
local $self->{lvar} = { %{$self->lvar} }; # new scope
local $self->{const} = [ @{$self->const} ]; # new scope
$self->compile_ast($third);
};
if($OPTIMIZE) {
if($self->_code_is_literal(@cond)) {
my $value = $cond[0][_OP_ARG];
if($cond_true eq 'and' ? $value : !$value) {
return @then;
}
else {
return @else;
}
}
}
if( (@then and @else) or !$OPTIMIZE) {
return(
@cond,
$self->opcode( $cond_true => scalar(@then) + 2, comment => $node->id . ' (then)' ),
@then,
$self->opcode( goto => scalar(@else) + 1, comment => $node->id . ' (else)' ),
@else,
);
}
elsif(!@else) { # no @else
return(
@cond,
$self->opcode( $cond_true => scalar(@then) + 1, comment => $node->id . ' (then/no-else)' ),
@then,
);
}
else { # no @then
return(
@cond,
$self->opcode( $cond_false => scalar(@else) + 1, comment => $node->id . ' (else/no-then)'),
@else,
);
}
}
sub _generate_given {
my($self, $node) = @_;
my $expr = $node->first;
my $vars = $node->second;
my $block = $node->third;
if(@{$vars} > 1) {
$self->_error("A given block requires one or zero variables", $node);
}
local $self->{lvar} = { %{$self->lvar} }; # new scope
local $self->{const} = [ @{$self->const} ]; # new scope
my @code = $self->compile_ast($expr);
my($lvar) = @{$vars};
my $lvar_id = $self->lvar_id;
my $lvar_name = $lvar->id;
$self->lvar->{$lvar_name} = $lvar_id;
local $self->{lvar_id} = $self->lvar_use(1); # topic variable
push @code, $self->opcode( save_to_lvar => $lvar_id, symbol => $lvar ),
$self->compile_ast($block);
return @code;
}
sub _generate_variable {
my($self, $node) = @_;
if(defined(my $lvar_id = $self->lvar->{$node->value})) {
return $self->opcode( load_lvar => $lvar_id, symbol => $node );
}
else {
my $name = $self->_variable_to_value($node);
if($name =~ /~/) {
$self->_error("Undefined iterator variable $node", $node);
}
return $self->opcode( fetch_s => $name, line => $node->line );
}
}
sub _generate_super {
my($self, $node) = @_;
return return $self->opcode( super => undef, symbol => $node );
}
sub _generate_literal {
my($self, $node) = @_;
return $self->opcode( literal => $node->value );
}
sub _generate_nil {
my($self) = @_;
return $self->opcode('nil');
}
sub _generate_vars {
my($self) = @_;
return $self->opcode('vars');
}
sub _generate_composer {
my($self, $node) = @_;
my $list = $node->first;
my $type = $node->id eq '{' ? 'make_hash' : 'make_array';
return
$self->opcode( pushmark => undef, comment => $type ),
(map{ $self->push_expr($_) } @{$list}),
$self->opcode($type),
;
}
sub _generate_unary {
my($self, $node) = @_;
my $id = $node->id;
if(exists $unary{$id}) {
my @operand = $self->compile_ast($node->first);
my @code = (
@operand,
$self->opcode( $unary{$id} )
);
if( $OPTIMIZE and $self->_code_is_literal(@operand) ) {
$self->_fold_constants(\@code);
}
return @code;
}
else {
$self->_error("Unary operator $id is not implemented", $node);
}
}
sub _generate_field {
my($self, $node) = @_;
my @lhs = $self->compile_ast($node->first);
my $field = $node->second;
# $foo.field
# $foo["field"]
if($field->arity eq "literal") {
return
@lhs,
$self->opcode( fetch_field_s => $field->value );
}
# $foo[expression]
else {
local $self->{lvar_id} = $self->lvar_use(1);
my @rhs = $self->compile_ast($field);
if($OPTIMIZE and $self->_code_is_literal(@rhs)) {
return
@lhs,
$self->opcode( fetch_field_s => $rhs[0][1] );
}
return
@lhs,
$self->opcode( save_to_lvar => $self->lvar_id ),
@rhs,
$self->opcode( load_lvar_to_sb => $self->lvar_id ),
$self->opcode( 'fetch_field' ),
;
}
}
sub _generate_binary {
my($self, $node) = @_;
my @lhs = $self->compile_ast($node->first);
my $id = $node->id;
if(exists $binary{$id}) {
local $self->{lvar_id} = $self->lvar_use(1);
my @rhs = $self->compile_ast($node->second);
my @code = (
@lhs,
$self->opcode( save_to_lvar => $self->lvar_id ),
@rhs,
$self->opcode( load_lvar_to_sb => $self->lvar_id ),
$self->opcode( $binary{$id} ),
);
if(any_in($id, qw(min max))) {
local $self->{lvar_id} = $self->lvar_use(1);
splice @code, -1, 0,
$self->opcode(save_to_lvar => $self->lvar_id ); # save lhs
push @code,
$self->opcode( or => +2 , symbol => $node ),
$self->opcode( load_lvar_to_sb => $self->lvar_id ), # on true
# fall through
$self->opcode( 'move_from_sb' ), # on false
}
if($OPTIMIZE) {
if( $self->_code_is_literal(@lhs) and $self->_code_is_literal(@rhs) ){
$self->_fold_constants(\@code);
}
}
return @code;
}
elsif(exists $logical_binary{$id}) {
my @rhs = $self->compile_ast($node->second);
return
@lhs,
$self->opcode( $logical_binary{$id} => scalar(@rhs) + 1, symbol => $node ),
@rhs;
}
$self->_error("Binary operator $id is not implemented", $node);
}
sub _generate_range {
my($self, $node) = @_;
$self->can_be_in_list_context
or $self->_error("Range operator must be in list context");
my @lhs = $self->compile_ast($node->first);
local $self->{lvar_id} = $self->lvar_use(1);
my @rhs = $self->compile_ast($node->second);
return(
@lhs,
$self->opcode( save_to_lvar => $self->lvar_id ),
@rhs,
$self->opcode( load_lvar_to_sb => $self->lvar_id ),
$self->opcode( 'range' ),
);
}
sub _generate_methodcall {
my($self, $node) = @_;
my $args = $node->third;
my $method = $node->second->value;
return (
$self->opcode( pushmark => undef, comment => $method ),
$self->push_expr($node->first),
(map { $self->push_expr($_) } @{$args}),
$self->opcode( methodcall_s => $method, line => $node->line ),
);
}
sub _generate_call {
my($self, $node) = @_;
my $callable = $node->first; # function or macro
my $args = $node->second;
if(my $intern = $builtin{$callable->id} and !$self->overridden_builtin->{$callable->id}) {
if(@{$args} != 1) {
$self->_error("Wrong number of arguments for $callable", $node);
}
return $self->compile_ast($args->[0]),
[ $intern->[0] => undef, $node->line ];
}
return(
$self->opcode( pushmark => undef, comment => $callable->id ),
(map { $self->push_expr($_) } @{$args}),
$self->compile_ast($callable),
$self->opcode( 'funcall' )
);
}
# $~iterator
sub _generate_iterator {
my($self, $node) = @_;
my $item_var = $node->first;
my $lvar_id = $self->lvar->{$item_var};
if(!defined($lvar_id)) {
$self->_error("Refer to iterator $node, but $item_var is not defined",
$node);
}
return $self->opcode(
load_lvar => $lvar_id + 1,
symbol => $node,
);
}
# $~iterator.body
sub _generate_iterator_body {
my($self, $node) = @_;
my $item_var = $node->first;
my $lvar_id = $self->lvar->{$item_var};
if(!defined($lvar_id)) {
$self->_error("Refer to iterator $node.body, but $item_var is not defined",
$node);
}
return $self->opcode(
load_lvar => $lvar_id + 2,
symbol => $node,
);
}
sub _generate_assign {
my($self, $node) = @_;
my $lhs = $node->first;
my $rhs = $node->second;
my $is_decl = $node->third;
my $lvar = $self->lvar;
my $lvar_name = $lhs->id;
if($node->id ne "=") {
$self->_error("Assignment ($node) is not supported", $node);
}
my @expr = $self->compile_ast($rhs);
if($is_decl) {
$lvar->{$lvar_name} = $self->lvar_id;
$self->{lvar_id} = $self->lvar_use(1); # don't use local()
}
if(!exists $lvar->{$lvar_name} or $lhs->arity ne "variable") {
$self->_error("Cannot modify $lhs, which is not a lexical variable", $node);
}
return
@expr,
$self->opcode( save_to_lvar => $lvar->{$lvar_name}, symbol => $lhs, comment => $node->id);
}
sub _generate_constant {
my($self, $node) = @_;
my $lhs = $node->first;
my $rhs = $node->second;
my @expr = $self->compile_ast($rhs);
my $lvar = $self->lvar;
my $lvar_id = $self->lvar_id;
my $lvar_name = $lhs->id;
$lvar->{$lvar_name} = $lvar_id;
$self->{lvar_id} = $self->lvar_use(1); # don't use local()
if($OPTIMIZE) {
if(@expr == 1
&& any_in($expr[0][_OP_NAME], qw(literal load_lvar))) {
$expr[0][_OP_COMMENT] = "constant $lvar_name";
$self->const->[$lvar_id] = \@expr;
return @expr; # no real definition
}
}
return
@expr,
$self->opcode( save_to_lvar => $lvar_id, symbol => $lhs, comment => $node->id);
}
sub _localize_vars {
my($self, $vars) = @_;
my @localize;
my @pairs = @{$vars};
if( (@pairs % 2) != 0 ) {
if(@pairs == 1) {
return $self->compile_ast(@pairs),
$self->opcode( 'localize_vars' );
}
else {
$self->_error("You must pass pairs of expressions to include");
}
}
while(my($key, $expr) = splice @pairs, 0, 2) {
if(!any_in($key->arity, qw(literal variable))) {
$self->_error("You must pass a simple name to localize variables", $key);
}
push @localize,
$self->compile_ast($expr),
$self->opcode( localize_s => $key->value, symbol => $key );
}
return @localize;
}
sub _variable_to_value {
my($self, $arg) = @_;
my $name = $arg->value;
$name =~ s/\$//;
return $name;
}
sub requires {
my($self, @files) = @_;
push @{ $self->dependencies }, @files;
return;
}
sub can_be_in_list_context {
my $i = 2;
while(my $funcname = (caller ++$i)[3]) {
if($funcname =~ /::_generate_(\w+) \z/xms) {
return any_in($1, qw(
methodcall
call
composer
));
}
}
return 0;
}
# optimizatin stuff
sub _code_is_literal {
my($self, @code) = @_;
return @code == 1
&& ( $code[0][_OP_NAME] eq 'literal'
|| $code[0][_OP_NAME] eq 'literal_i');
}
sub _fold_constants {
my($self, $code) = @_;
my $engine = $self->engine or return 0;
local $engine->{warn_handler} = \&Carp::croak;
local $engine->{die_handler} = \&Carp::croak;
local $engine->{verbose} = 1;
my $result = eval {
my @tmp_code = (@{$code}, $self->opcode('print_raw'), $self->opcode('end'));
$engine->_assemble(\@tmp_code, '<string>', undef, undef, undef);
$engine->render('<string>');
};
if($@) {
Carp::carp("[BUG] Constant folding failed (ignored): $@");
return 0;
}
@{$code} = ($self->opcode( literal => $result, comment => "optimized by constant folding"));
return 1;
}
sub _noop {
my($self, $op) = @_;
@{$op} = @{ $self->opcode( noop => undef, comment => "ex-$op->[0]") };
return;
}
sub _optimize_vmcode {
my($self, $c) = @_;
# calculate goto addresses
# eg:
#
# goto +3
# foo
# noop
# bar // goto destination
#
# to be:
#
# goto +2
# foo
# bar // goto destination
my @goto_addr;
for(my $i = 0; $i < @{$c}; $i++) {
if(exists $goto_family{ $c->[$i][_OP_NAME] }) {
my $addr = $c->[$i][_OP_ARG]; # relational addr
# mark ragens that goto family have its effects
my @range = $addr > 0
? ($i .. ($i+$addr-1)) # positive
: (($i+$addr) .. $i); # negative
foreach my $j(@range) {
push @{$goto_addr[$j] ||= []}, $c->[$i];
}
}
}
for(my $i = 0; $i < @{$c}; $i++) {
my $name = $c->[$i][_OP_NAME];
if($name eq 'print_raw_s') {
# merge a chain of print_raw_s into single command
my $j = $i + 1; # from the next op
while($j < @{$c}
&& $c->[$j][_OP_NAME] eq 'print_raw_s'
&& "@{$goto_addr[$i] || []}" eq "@{$goto_addr[$j] || []}") {
$c->[$i][_OP_ARG] .= $c->[$j][_OP_ARG];
$self->_noop($c->[$j]);
$j++;
}
}
elsif($name eq 'save_to_lvar') {
# use registers, instead of local variables
#
# given:
# save_to_lvar $n
# <single-op>
# load_lvar_to_sb $n
# convert into:
# move_to_sb
# <single-op>
my $it = $c->[$i];
my $nn = $c->[$i+2]; # next next
if(defined($nn)
&& $nn->[_OP_NAME] eq 'load_lvar_to_sb'
&& $nn->[_OP_ARG] == $it->[_OP_ARG]) {
@{$it} = @{$self->opcode( move_to_sb => undef, comment => "ex-$it->[0]" )};
$self->_noop($nn);
}
}
elsif($name eq 'literal') {
if(is_int($c->[$i][_OP_ARG])) {
$c->[$i][_OP_NAME] = 'literal_i';
$c->[$i][_OP_ARG] = int($c->[$i][_OP_ARG]); # force int
}
}
elsif($name eq 'fetch_field') {
my $prev = $c->[$i-1];
if($prev->[_OP_NAME] =~ /^literal/) { # literal or literal_i
$c->[$i][_OP_NAME] = 'fetch_field_s';
$c->[$i][_OP_ARG] = $prev->[_OP_ARG]; # arg
$self->_noop($prev);
}
}
}
# remove noop
for(my $i = 0; $i < @{$c}; $i++) {
if($c->[$i][_OP_NAME] eq 'noop') {
if(defined $goto_addr[$i]) {
foreach my $goto(@{ $goto_addr[$i] }) {
# reduce its absolute value
$goto->[1] > 0
? $goto->[1]-- # positive
: $goto->[1]++; # negative
}
}
splice @{$c}, $i, 1;
# adjust @goto_addr, but it may be empty
splice @goto_addr, $i, 1 if @goto_addr > $i;
}
}
return;
}
sub as_assembly {
my($self, $code_ref, $addix) = @_;
my $asm = "";
foreach my $ix(0 .. (@{$code_ref}-1)) {
my($name, $arg, $line, $file, $label, $comment) = @{$code_ref->[$ix]};
$asm .= "$ix:" if $addix; # for debugging
# "$opname $arg #$line:$file *$symbol // $comment"
ref($name) and die "Oops: " . p($code_ref->[$ix]);
$asm .= $name;
if(defined $arg) {
$asm .= " " . value_to_literal($arg);
}
if(defined $line) {
$asm .= " #$line";
if(defined $file) {
$asm .= ":" . value_to_literal($file);
}
}
if(defined $label) {
$asm .= " " . value_to_literal($label);
}
if(defined $comment) {
$asm .= " // $comment";
}
$asm .= "\n";
}
return $asm;
}
sub _error {
my($self, $message, $node) = @_;
my $line = ref($node) ? $node->line : $node;
die $self->make_error($message, $self->file, $line);
}
no Mouse;
no Mouse::Util::TypeConstraints;
__PACKAGE__->meta->make_immutable;
__END__
=head1 NAME
Text::Xslate::Compiler - An Xslate compiler to generate intermediate code
=head1 DESCRIPTION
This is the Xslate compiler to generate the intermediate code from the
abstract syntax tree that parsers build from templates.
=head1 SEE ALSO
L<Text::Xslate>
L<Text::Xslate::Parser>
L<Text::Xslate::Symbol>
=cut