package JE::Code; use strict; use warnings; no warnings qw 'utf8 parenthesis'; no constant 1.03 (); use constant::lexical do { my $x = 0; +{ map +($_ => $x++), 'cx_stm', # statement 'cx_void', # statement; retval ignored # expression contexts 'cx_void_expr', 'cx_any', # includes lvalue 'cx_rv', # rvalue 'cx_lv', # lvalue 'cx_bool', # plain boolean, not necessarily a JE::Boolean 'cx_str', # Perl scalar in UTF-16 (with surrogates) 'cx_num', # Perl scalar 'ret_void', # no retval 'ret_maybe', # might have a retval (which could be an lval) 'ret_break', # break or continue 'ret_any', # includes lvalue 'ret_lv', 'ret_rv', 'ret_str', 'ret_num', 'ret_bool', } }; =begin notes The cx_stm constant for statement-level context applies to a context in which any return type (including an lvalue) is expected, but an lvalue has to have ->get called on it before it is returned. The to_perl methods take 1 argument, a cx_ constant, and return 2 or 3: 0. ret_ constant indicating possible return types 1. string of Perl code ( 2. alternate string of Perl code that can be used only in some contexts, such as "..." for JE::String->new(...) ) The reason we have all those return values is most easily demonstrated by example: For the + operator, we have two completely different behaviours, depending on the types of the operands. If an operand can only be a string, we get: ret_str, 'JE::String->new($global, "...")', '"..."' If we see one of those, we can stringify the other operand, returning code like this: "..." . [other operand]->to_string->value16 We need to_perl’s second retval, for cases like - : [str operand]->to_number->value - [num operand] =end notes =cut sub _esc_str($) { my $str = shift; $str =~ s/(['\\])/\\$1/g; "'$str'"; } our %labels; our %loop_labels; our $cache_indx; our $code; no warnings 'redefine'; sub optimise { my $self = shift; local %labels; local %loop_labels; local $cache_indx; local $code = $self; my $global = $self->{global}; $self->set_global(undef); $self->{psrc} = 'no warnings"utf8","exiting";' . $self->{tree}->to_perl(cx_stm); $self->{tree}=[$self->{tree}[0]]; $self->set_global($global); } use Scalar::Util 'refaddr'; sub JE::Code::Statement::to_perl { my $stm = shift; my $cx = shift; my $type = $$stm[1]; $type eq 'empty' and return ret_void, ''; if($type eq 'function' ) { (my $new_code_obj = bless { map+($_=>$code->{$_}),qw/global source file line/ }, 'JE::Code') ->{tree} = $$_[4]; $new_code_obj->{vars} = $$_[5]; $new_code_obj->optimise; $$stm[4] = $new_code_obj; return ret_void, ''; } my $pos = $$stm[0][0]; if ($type eq 'labelled') { my @labels = @$stm[2..$#$stm-1]; my $is_loop = $$stm[-1][1] =~ /^(?:do|while|for|switch)\z/; # We have to alias the labels to a single label, because # Perl does not support multiple labels on a sin- # gle statement. Nor can we stack them up with # blocks, ( foo:{bar:{...}} ) because that won’t work # for loop-control constructs (next foo). We have # to rename them, since JS idents can have $ in them. my $alias_to = refaddr $stm->[-1]; local @labels{@labels} = ($alias_to)x@labels; # ‘continue’ statements only look in this hash: local @loop_labels{@labels} = ($alias_to)x@labels if $is_loop; my($rettype,$code) = $$stm[-1]->to_perl($cx); return $rettype, # loops add their own label, since some have to put # something before it $is_loop ? $code : "JE_Code_$alias_to:" . ( $$stm[-1][1] =~ /^{/ ? $code : length $code ? "{$code}" : '{;}' ); # Without the ; it’s a hash. } if ($type eq 'statements') { # At run time, the statements are executed one by one, and # the return value of the last statement that actually # returned one is returned. To avoid passing cx_stm context # when cx_void will do, we work backwards. # ~~~ This logic is currently flawed. It needs to account # for a ‘break’ or similar in between two ret_any stms. # And also ‘return’ satetments. my @code; my $last_rettype; for (@$stm[reverse 2..$#$stm]) { my($rettype,$code) = $_->to_perl($cx); $last_rettype = $rettype; push @code, $code; if($rettype != ret_void && $rettype != ret_maybe) { $cx = cx_void } } my $code = join '', reverse @code; # $rettype will be undefined if this is an empty block return defined $last_rettype?$last_rettype:ret_void, $code; } if ($type eq 'var') { # The Perl code looks like this: # $scope->find_var("foo")->set( ...expr... ); my $stm_code = ''; for (@$stm[2..$#$stm]) { if (@$_ == 2) { my($rettype,$code) = _term_to_perl($$_[1],cx_rv); $stm_code .= '$scope->find_var(' . _esc_str($$_[0]) .')->set(' . $code . ");"; }} return ret_void, $stm_code; } if ($type eq 'if') { # 2 3 4 # we have: expr statement statement? my($rettype,$code) = $$stm[2]->to_perl(cx_bool); my $stm_code = "if($code){"; ($rettype,$code) = $$stm[3]->to_perl($cx); $stm_code .= "$code}"; my $rettype2; if(exists $$stm[4]) { ($rettype2,$code) = $$stm[4]->to_perl($cx); $stm_code .= "else{$code}" } return $cx == cx_void ? ret_void : (defined $rettype2 ? $rettype2 == ret_void : 1) && $rettype == ret_void ? ret_void : ret_maybe, $stm_code } if ($type =~ /^(?:do|while|for|switch)\z/) { # We have one of the following: # # 1 2 3 4 5 # 'do' statement expression # 'while' expression statement # 'for' expression 'in' expression statement # 'for' var_decl 'in' expression statement # 'for' expression expression expression statement # 'for' var_decl expression expression statement # # In those last two cases, expression may be 'empty'. # (See further down for 'switch'). my $label = refaddr $stm; my $stm_code = "JE_Code_$label:"; # Alias for simple break/continue statements with- # out a label: local $labels{''} = local $loop_labels{''} = $label; if ($type eq 'do') { # For a do statement we need two loop labels: # outer: { # do { inner: { # # }} while foo # } # continue statements use the inner label. # In case there are aliases to the existing label: my @js_labels = grep $labels{$_} == $label =>=> keys %loop_labels; local @loop_labels{'',@js_labels} = ($label.'c')x(@js_labels+1); my($rettype,$code) = $$stm[2]->to_perl($cx); my(undef,$boolcode) = $$stm[3]->to_perl(cx_bool); return $rettype, "$stm_code\{do{JE_Code_${label}c:{" . (length $code ? $code : ';') # foo:{} is a hash ."}}while " . "$boolcode}"; } elsif ($type eq 'while') { my(undef,$boolcode) = $$stm[2]->to_perl(cx_bool); my($rettype,$code) = $$stm[3]->to_perl($cx); return $rettype, "${stm_code}while($boolcode){$code}"; } elsif ($type eq 'for' and $$stm[3] eq 'in') { # for(var i = 3 in ...) { # break; # } # translates into # $scope->find_var('i')->set(3); # JE_Code_loop: for((my $o = ...)->keys) { # next if not defined $o->prop($_); # # in which case it's been deleted # $scope->find_var('i')->set($_); # last JE_Code_loop; # } # (except that the arguments to ->set() have to be # expressions returning JE::(Number|String)). For # statements without the ‘var’, we don’t evaluate # the lhs before the loop, but only in the loop. It # takes the place of $scope->find_var. my $left_side = $$stm[2]; if ($left_side->[1] eq 'var') { substr $stm_code,0,0 =>= ($left_side->to_perl(cx_void))[1]; $left_side = _esc_str($left_side->[2][0]); # now contains the identifier within a # Perl string } (undef,my $obj_code) = $$stm[4]->to_perl(cx_rv); $stm_code .= "for((my\$o=$obj_code)->keys){" . 'next if!defined prop$o $_;' . ( ref $left_side ? ( $left_side->to_perl(cx_lv) )[1] : "\$scope->find_var($left_side)" ) . '->set(_new JE\'String $global,$_);'; my($rettype, $code) = $$stm[5]->to_perl($cx); return $rettype, "$stm_code$code}"; } elsif ($type eq 'for') { # for(;;) $stm_code .= 'for('; if(ref $$stm[2]) { my $code = ($$stm[2]->to_perl(cx_void))[1]; length $code and $stm_code .= "do{$code}" # We need the do } # because to_perl $stm_code .= ';'; # returns whole if(ref $$stm[3]) { # statements my(undef,$code) = $$stm[3]->to_perl(cx_bool); $stm_code .= $code; } $stm_code .= ';'; ref $$stm[4] and $stm_code .= ( $$stm[4]->to_perl(cx_void_expr) )[1]; my($rettype, $code) = $$stm[5]->to_perl($cx); return $rettype, "$stm_code){$code}"; } else { # switch # $stm->[2] is the parenthesized # expression. # Each pair of elements thereafter # represents one case clause, an expr # followed by statements, except for # the default clause, which has the # string 'default' for its first elem # We need to turn something like # switch(3) { # case 5: a(); # default: f(); # case 3636: # case 838: break; # case 3: h(); # } # into # JE_Code_loop: for(3) { # $_->id eq 5->id and goto JE_Code_case1; # $_->id eq 3636->id and goto JE_Code_case2; # $_->id eq 838->id and goto JE_Code_case3; # $_->id eq 3->id and goto JE_Code_case4; # goto JE_Code_default; # JE_Code_case1: $scope->find_var('a')->call; # JE_Code_default: $scope->find_var('f')->call; # JE_Code_case2: ; # JE_Code_case3: last JE_Code_loop; # JE_Code_case4: $scope->find_var('f')->call; # } # (except that the expressions are more complex). # header $stm_code .= 'for(' . ( $$stm[2]->to_perl(cx_rv) )[1] . '){'; # Go through the case clauses and add goto state- # ments to $stm_code while also creating the state- # ments that go further down. my $more_statements = ''; my($n, $there_is_a_default) = 1; while (($n+=2) < @$stm) { if($$stm[$n] eq 'default') { $more_statements .= 'JE_Code_default:'; ++ $there_is_a_default } else { $stm_code .= 'id$_ eq ' . ($$stm[$n]->to_perl(cx_rv))[1] . "->id&&goto JE_Code_case$n;"; $more_statements.="JE_Code_case$n:" } my $stm = ( $$stm[$n+1]->to_perl($cx) )[1]; $more_statements .= length $stm?$stm:';' ; } ; return $cx==ret_void ? ret_void : ret_maybe, # ~~~ need to do some detecting # for the rettype $stm_code . ( $there_is_a_default ? 'goto JE_Code_default' : 'last' ) . ";$more_statements}" } # switch } if ($type eq 'continue') { my $label = exists $$stm[2] ? $$stm[2] : ''; return ret_break, exists $loop_labels{$label} ? "next JE_Code_$loop_labels{$label};" : "die q[continue $label: label '$label' not found];" } if ($type eq 'break') { my $label = exists $$stm[2] ? $$stm[2] : ''; return ret_break, exists $labels{$label} ? "last JE_Code_$labels{$label};" : "die q[break $label: label '$label' not found];" } if ($type eq 'return') { return ret_any, '$return=' . ( exists $$stm[2] ? ($$stm[2]->to_perl(cx_rv))[1] : 'undef' ) . ';last RETURN;' ; } if ($type eq 'with') { my($rettype, $code) = $$stm[3]->to_perl($cx); return $rettype, '{local$scope=bless[@$scope,' . ($$stm[2]->to_perl(cx_rv))[1] . "->to_object],'JE::Scope';$code}"; } if ($type eq 'throw') { return ret_void,'die '.($$stm[2]->to_perl(cx_rv))[1]. ';' ; } if ($type eq 'try') { # We have one of the following: # 1 2 3 4 5 # 'try' block ident block (catch) # 'try' block block (finally) # 'try' block ident block block (catch & finally) # For a try-catch, we can simply use eval{...;1}||do{...}. # With finally, it’s a lot more complicated. We can’t use # Scope::Guard, because a destructor can’t affect execution # flow. So we have to set up a complicated net to catch # every return/break/continue in addition to exceptions. # For every unique value in %labels, we need a separate # block. What we end up with is something like this: # ~~~(The inner do{} is actually redundant. Maybe I should # refactor it a bit.) # { # lexical scope for all these variables: # my $l; #label # my $e; #exception # my $c; #continue # eval { # JE_Code_labels: { # RETURN: { # JE_Code_1: for(0,1){ $_ and ++$c, last; # JE_Code_2: for(0,1){ $_ and ++$c, last; # JE_Code_3: for(0,1){ $_ and ++$c, last; # my $r = $return; # eval { ... try ...; last JE_Code_labels }; # ... catch ...; # last JE_Code_labels; # } $l = 3, last JE_Code_labels # } $l = 2, last JE_Code_labels # } $l = 1, last JE_Code_labels # } $l = 'r' # } # 1 # } or $e=$@; # my $r = $return; # ... finally ... # defined $e && die $e; # $return = $r; # $l and $l == 'r' ? last RETURN : # $c ? next "JE_Code_$l" : last "JE_Code_$l" # } # A catch’s do-block (this is not in a do when there is a # finally) is as follows: # do { # $return = $r; # ref $@ or $@ = _objectify_error($@); # (my $o = new JE::Object $global) # ->prop({ # name => 'e', # value => $@, # dontdel => 1, # }); # local $scope = bless [ # @$scope, $o # ], 'JE::Scope'; # ...code... # }; # Got that? my $finally = $#$stm == 3 || $#$stm == 5; # try and catch my $inner_code = ''; # We don’t want try{3; throw...} to return 3, so we save # the previous value of $return before entering the eval. my $we_can_catch = !ref $$stm[3]; $inner_code .= 'my$r=$return;eval{' if $we_can_catch; my($rettype,$code) = $$stm[2]->to_perl($cx); # try $inner_code .= $code; $inner_code .= 'last JE_Code_labels;' if $finally; if($we_can_catch) { $inner_code .= ($finally ? '};' : '1}||do{') . '$return=$r;' # prevent { 3; throw ... } # from returning 3 # Turn miscellaneous errors into # Error objects .q"ref$@or$@=_objectify_error($@);" .q"(my$o=new JE'Object $global)->prop({" . 'name=>' . _esc_str($$stm[3]) . ',' . 'value=>$@,' . 'dontdel=>1' .'});' .'local$scope=' . 'bless[@$scope,$o],"JE::Scope";'; my($rettype, $code) = $$stm[4]->to_perl($cx); $inner_code .= $code . ($finally ? ';last JE_Code_labels' : '}'); } if ($finally) { # get a list of labels my @labels; my %seen; for(values %labels, values %loop_labels) { push @labels, $_ unless $seen{$_}++ } my $stm_code = '{' . 'my($l,$e,$c);' . 'eval{' . 'JE_Code_labels:{' . 'RETURN:{'; $stm_code .= "JE_Code_$_:for(0,1){\$_ and++\$c,last;", for @labels; $stm_code .= $inner_code; $stm_code .= '}$l='._esc_str($_).",last JE_Code_labels" for reverse @labels; $stm_code .= '}$l="r"' . '}' . '1' . '}or$e=$@;' . 'my$r=$return;' . ( $$stm[-1]->to_perl(cx_void) )[1] . 'defined$e&&die$e;' . '$return=$r;' . '$l and$l eq"r"?last RETURN:' . '$c?eval"next JE_Code_$l"' . ':eval"last JE_Code_$l"' .'}'; return $cx==ret_void ? ret_void : ret_maybe, $stm_code; } else { return $cx==ret_void ? ret_void : ret_maybe, $we_can_catch ? "{$inner_code}" : $inner_code; } # ~~~ need to do some detecting for the rettype } } =begin for-me Types of expressions: 'new' term args? 'member/call' term ( subscript | args) * 'postfix' term op 'hash' term* 'array' term? (comma term?)* 'prefix' op+ term 'lassoc' term (op term)* 'assign' term (op term)* (term term)? (the last two terms are the 2nd and 3rd terms of ? : 'expr' term* (commas are omitted from the array) 'function' ident? params statements =end for-me =cut # Note: each expression object is an array ref. The elems are: # [0] - an array ref containing # [0] - the starting position in the source code and # [1] - the ending position # [1] - the type of expression # [2..$#] - the various terms/tokens that make up the expr sub JE::Code::Expression::to_perl { #ret_rv, "hooha()" . ';' x ($_[1] == cx_stm || $_[1] == cx_void) # ~~~ ++ $ops>$counting and last JE_Code_OP if $counting; my $expr = shift; my $cx = shift; my $type = $$expr[1]; # ~~~ $pos = $$expr[0][0]; if ($type eq 'expr') { my $sc = ''; $cx == cx_stm ? ($sc = ';', $cx = cx_any) : $cx == cx_void && ($sc = ';', $cx = cx_void_expr); if(@$expr == 3) { # no comma my($rettype,$code) = _term_to_perl($$expr[-1], $cx); if($rettype == ret_lv && $sc) { $code = "{my\$v=$code;\$v->get;\$return=\$v}"; }elsif($rettype==ret_any && $sc){ $code = "{my\$v=$code;" . 'ref$v eq"JE::LValue"&&$v->get' . ';$return=$v' .'}'; } elsif($sc) { $code = "\$return=$code;" } return $rettype, $code; } else { # comma op my $result = join ',', map+ (_term_to_perl($_, cx_void_expr))[1], @$expr[2..$#$expr-1]; $result .= ','; my($rettype,$code) = _term_to_perl($$expr[-1], ret_rv); $result .= $code; if($sc) { $result = "\$return=($result);"; } else { $result = "scalar($result)"; } return $cx == cx_void_expr ? ret_void : $rettype, $result; } } if ($type eq 'assign') { my @copy = @$expr[2..$#$expr]; # Evaluation is done left-first in JS, unlike in # Perl, so a = b = c is evaluated in this order: # - evaluate a # - evaluate b # - evaluate c # - assign c to b # - assign b to a # Check first to see whether we have the terms # of a ? : at the end: my @qc_terms = @copy >= 3 && ( ref $copy[-2] # avoid stringification || $copy[-2] =~ /^(?:[tfu]\z|[si0-9])/ ) ? (pop @copy, pop @copy) : (); # @qc_terms is now in reverse order # Rough sketch of what we want to accomplish: # a += b += c += d # # do { # my $l = a; # $l->set($l->get + do{ # my $l = b; # $l->set($l->get + do { # my $l = c; # $l->set($l->get + d) # }) # }) # } # a = b # # a->set(b->get) # a += b = c # # do { # my $l = a; # $l->set($l->get + b->set(my $v = c)) # } # Get the first rhs ready: my $perl_expr = pop @copy; # Now apply ? : if it's there if(@qc_terms) { $perl_expr = (_term_to_perl($perl_expr,cx_bool))[1] . '?' . (_term_to_perl($qc_terms[1],cx_rv))[1] . ':' . (_term_to_perl($qc_terms[0],cx_rv))[1] } else { $perl_expr = (_term_to_perl($perl_expr,cx_rv))[1]; } # short-circuit if we only have ? : and no assignment # ~~~ check return types from both legs return ret_rv, $perl_expr unless @copy; # Iterate through the ops, wrapping each previous expr with # the current one. while(@copy) { my ($op, $term) = (pop @copy, pop @copy); if(length $op > 1) { $perl_expr = 'do{my$l=' . (_term_to_perl($term,cx_lv))[1] . ';' . '$l->set(' . "'JE::Code::Expression::in".substr $op,0,-1, . "'->(\$l->get,$perl_expr))}" } else { $perl_expr = (_term_to_perl($term,cx_lv))[1] . "->set($perl_expr)" } } # ~~~ T and tainted $taint and $val->can('taint') # and $val = taint $val $taint; return ret_rv, $perl_expr; } if($type eq 'lassoc') { # left-associative my @copy = @$expr[2..$#$expr]; my $result = (_term_to_perl(shift @copy, cx_rv))[1]; while(@copy) { # We have to deal with || && specially for the sake # of short-circuiting my $op = $copy[0]; if ($op =~ m&^(?:\&\&|\|\|)\z&) { $result = "$result$op " .(_term_to_perl($copy[1],cx_rv))[1] } else { $result = "'JE::Code::Expression::in$op'->" . "($result," .(_term_to_perl($copy[1],cx_rv))[1] .')' } splice @copy, 0, 2; # double shift } return ret_rv,$result; } if ($type eq 'prefix') { # ~~~ taintedness # $$expr[1] -- 'prefix' # @$expr[2..-2] -- prefix ops # $$expr[-1] -- operand my $term = ( _term_to_perl( $$expr[-1], $$expr[-2] =~ m-^(?:\+\+|\-\-)\z- ? cx_lv : $$expr[-2] =~ m e^(?:typ\eof|d\el\et\e)\ze ? cx_any : cx_rv ) )[1]; $term = "'JE::Code::Expression::pre$_'->($term)" for reverse @$expr[2..@$expr-2]; return ret_rv, $term; } if ($type eq 'postfix') { # ~~~ taintedness # ~~~ These are supposed to use the same rules # as the + and - infix ops for the actual # addition part. Verify that they do this. # This will need to be made more efficient: # do{ # my $l = ...; $l->set( # "JE::Code::Expression::in+"->( # $l->get, # JE::Number->new($global, 1) # ) # ); # $v # } return ret_rv, 'do{' . 'my$l=' . (_term_to_perl($$expr[2], cx_lv))[1] . ';' . '$l->set(' . '"JE::Code::Expression::in+"->(' . 'my$v=$l->get->to_number,' . _cached('JE\'Number->new($global,' . (-1,1)[$$expr[3] eq '++']. ')') . ')' . ')' . ';$v' x ($cx != cx_void_expr) .'}' } if ($type eq 'new') { # ~~~ ? T && tainted $taint #~~~ ? map $_->can('taint') ?taint $_ $taint:$_, return ret_rv, (_term_to_perl($$expr[2],cx_rv))[1] .'->construct' . ( @$expr == 4 ? '(' . $$expr[-1]->to_perl . ')' : '' ) } if($type eq 'member/call') { my($type, $obj) = _term_to_perl( $$expr[2], exists $$expr[3] ? ref $$expr[3]eq 'JE::Code::Subscript' ? cx_rv : cx_any : $cx ); # ~~~ We can probably optimise this not to create LValue objects for calls, # but use the ‘apply’ method directly. for (3..$#$expr) { my $cx = exists $$expr[$_+1] ? ref $$expr[$_+1]eq 'JE::Code::Subscript' ? cx_rv : cx_any : $cx; if(ref $$expr[$_] eq 'JE::Code::Subscript') { if($cx != cx_lv && $cx != cx_any) { $obj .= '->prop(' . $$expr[$_]->to_perl .')'; if($cx == cx_void_expr) { $type = ret_void } else { $obj = "do{my\$v=$obj;" . 'defined$v?$v:$global->undefined}'; $type = ret_rv; } } else { $obj = "JE'LValue->new($obj," . $$expr[$_]->to_perl . ')'; $type = ret_lv } } else { # ~~~ taintedness for calls if($cx != cx_lv && $cx != cx_any) { $obj = 'do{' . "my\$v=$obj->call(" . $$expr[$_]->to_perl . ');' . 'ref$v eq"JE::LValue"' . ( $cx==cx_void_expr ?'&&$v->get' :'?$v->get:$v' ) .'}'; $type = ret_rv; } else { $obj = "$obj->call(" . $$expr[$_]->to_perl .')'; $type = ret_any } # If $obj is an lvalue, # JE::LValue::call will make # the lvalue's base object the 'this' # value. Otherwise, # JE::Object::Function::call # will make the # global object the 'this' value. } # ~~~ need some error-checking } return $type, $obj; # which may be an lvalue } if($type eq 'array') { if($#$expr < 2) { return ret_rv, "JE'Object'Array->new(\$global)" } my @ary; for (2..$#$expr) { if(ref $$expr[$_] eq 'comma') { ref $$expr[$_-1] eq 'comma' || $_ == 2 and push @ary, 'undef'; } else { push @ary, (_term_to_perl( $$expr[$_], cx_rv ))[1]; } } return ret_rv, 'do{my$a=new JE\'Object\'Array $global;' . '$$$a{array}=[' . join(',',@ary) . '];$a}' # sticking it in like that # makes 'undef' elements non- # existent, rather # than undefined } if($type eq 'hash') { local @_ = @$expr[2..$#$expr]; if(!@_) { return ret_rv, "JE'Object->new(\$global)"; } my $obj = 'do{my$o=new JE\'Object $global;'; my ($key, $value); while(@_) { # I have to loop through them to keep # the order. $key = _esc_str(shift); $value = (_term_to_perl( shift, cx_rv ))[1]; $obj .= "\$o->prop($key,$value);"; } return ret_rv, $obj . '$o}'; } if ($type eq 'func') { # format: [[...], function=> 'name', # [ params ], $statements_obj, \@vars] # or: [[...], function => # [ params ], $statements_obj, \@vars] # The code we need to produce will be like this: # e.g.: function(foo){ bar } # JE'Object'Function->new({ # scope=>$scope, # argnames=>['foo'], # function=>$$cache[0], # }) # If there is a name (function x (){}), it’s a little # more complex: # do{ # my $f = JE'Object'Function->new({ # name => 'x', # scope =>[@$scope, my $o = new JE::Object $global], # ... argnames and function ... # }); # $o->prop({ name => 'x', value => $f, # readonly => 1, dontdel => 1 }); # $f # } # The code object has to be created beforehand and placed # in the cache. # ~~~ When we merge this with JE::Parser, the entire code # for the code object can be placed in a # $$cache[0]||=.... my($name,$params,$statements) = ref $$expr[2] ? (undef, @$expr[2,3]) : @$expr[2..4]; my $ret; my $scope; if($name) { $name = _esc_str($name); $ret = 'do{my$f='; # ~~~ I should be able to remove this ‘bless’. See # the comment in jE::Object::Function::New $scope = 'bless(' . '[@$scope,my$o=new JE\'Object $global]' . ',"JE::Scope")' } my $c = $code->{cache}->[my $indx = $cache_indx++] = bless{ map+($_,$code->{$_}),qw/global source file line/ }, "JE::Code"; $c->{tree} = $statements; $c->{vars} = $$expr[-1]; $c->optimise; $ret .= "JE'Object'Function->new({" . 'scope=>' . ($scope||'$scope') . ',' . (defined $name ? ("name=>$name,") : '') . 'argnames=>[' . join(',',map _esc_str($_),@$params).'],' . "function=>\$\$cache[$indx]" .'})'; if($name) { $ret .= ';$o->prop({' . "name=>$name," . 'value=>$f,' . 'readonly=>1,' . 'dontdel=>1' .'})}' } return ret_rv,$ret; } } sub _cached($) { my $code_str = shift; my $indx = $cache_indx++; return "(\$\$cache[$indx]||=$code_str)"; } use constant nan => sin 9**9**9; use constant inf => 9**9**9; sub _term_to_perl { my $term = $_[0]; my $cx = $_[1]; # For booleans, we just use the expression itself, since the over- # loaded booleanness is much faster than ->to_boolean, which has to # create an object. # For strings and numbers, ->to_string->value16 is only slightly # faster than "" for objects, but the latter is 4x the speed for # strings and about 5% faster for strings (fewer method calls). # The converse holds true (exact speed differences aside) for # ->to_number->value vs 0+ (actually, in 5.8.8 the 0+ overload- # ing for JE::Number is slower than two method calls, but it’s # faster in 5.10). If we might have an object, we must # string-/numbify it immediately so that toString and # valueOf are called at the right time. # ~~~ Having said all that, we can’t use "..." because it will Uni- # codify the string (un-UTF-16-ify it). We probably need some # new method added to JE::Types and all the classes. if(ref $term eq 'JE::Code::Expression') { my($rettype,$code,$alt) = $term->to_perl($cx); return $rettype, $cx == cx_str ? defined $alt ? $alt : "$code->to_string->value16" : $cx == cx_num ? defined $alt ? $alt : "(0+$code)" : $cx == cx_bool ? defined $alt ? $alt : $code : $code } elsif(ref $term) { # ’better be’n array my $code = _cached("scalar(require JE'Object'RegExp," . "JE'Object'RegExp->new(\$global," . _esc_str($$term[0]) . (defined $$term[1]?',' . _esc_str($$term[1]):'') . '))'); return ret_rv, $cx == cx_str ? "$code->to_string->value16" : $cx == cx_num ? "(0+$code)" : $code } if($term =~ /^i/) { my $find = "\$scope->find_var(" . _esc_str(substr $term,1) . ")"; return $cx == cx_lv || $cx == cx_any ? ( ret_lv, $find ) : $cx==cx_str ?( ret_str, "$find->get->to_string->value16") : $cx==cx_bool?( ret_bool,"$find->get" ) : $cx==cx_num ?( ret_num, "(0+$find->get)" ) : ( ret_rv, "$find->get" ) } return (ret_void,'0') if $cx == cx_void; return ret_rv,'die(new JE\'Object\'Error\'ReferenceError $global,' .'add_line_number"Cannot assign to a non-lvalue")' if $cx == cx_lv; $term eq'this'? $cx == cx_str ? (ret_str, "\$this->to_string->value16") : $cx == cx_bool ? (ret_bool, '$this') : $cx == cx_num ? (ret_num, '(0+$this)') : ( ret_rv, '$this', 0) : $term =~ /^s/ ? do { my $esc = _esc_str(my $str = substr $term,1); $cx == cx_str ? (ret_str, $esc) : $cx == cx_bool ? (ret_bool, 0+(length $term > 1)) : $cx == cx_num ? (ret_num, # ~~~ JE::Number probably needs a function for # this, as it is a repeat of code found else- # where (in JE::String) $str =~ /^[\p{Zs}\s\ck]* ( [+-]? (?: (?=[0-9]|\.[0-9]) [0-9]* (?:\.[0-9]*)? (?:[Ee][+-]?[0-9]+)? | Infinity ) [\p{Zs}\s\ck]* )? \z /ox ? defined $1 ? $1 eq 'Infinity' ? 9**9**9 : $1 : 0 : $str =~ /^ [\p{Zs}\s\ck]* 0[Xx] ([A-Fa-f0-9]+) [\p{Zs}\s\ck]*\z/ox ? hex $1 : nan ) : ( ret_str, _cached "_new JE'String \$global,$esc", $esc ) } : $term eq 't' ? $cx == cx_str ? (ret_str, '"true"') : $cx == cx_bool ? (ret_bool, 1) : $cx == cx_num ? (ret_num, 1) : ( ret_bool, '$global->true', 1) : $term eq 'f' ? $cx == cx_str ? (ret_str, '"false"') : $cx == cx_bool ? (ret_bool, 0) : $cx == cx_num ? (ret_num, 0) : ( ret_bool, '$global->false', 0) : $term eq 'n' ? $cx == cx_str ? (ret_str, '"null"') : $cx == cx_bool ? (ret_bool, 0) : $cx == cx_num ? (ret_num, 0) : ( ret_rv, '$global->null') : $cx == cx_str ? (ret_str, $term == inf ? '"Infinity"' : $term ==-+inf ? '"-Infinity"': $term == $term? $term : '"NaN"' ) : $cx == cx_bool ? (ret_bool, 0+($term && $term == $term)) : $cx == cx_num ? (ret_num, $term ) : ( ret_rv, _cached "JE'Number->new(\$global," . ( $term == inf ? '"inf"' : $term == -+inf ? '"-inf"': $term == $term ? $term : '"nan"' ) . ')', $term ) } sub JE::Code::Subscript::to_perl { my $val = (my $self = shift)->[1]; ref $val ? $val->to_perl(cx_str) : _esc_str($val); } sub JE::Code::Arguments::to_perl { my $self = shift; join ',', map +(_term_to_perl($_,cx_rv))[1], @$self[1..$#$self]; } 1