ljr/wcmtools/s2/S2/NodeTerm.pm

667 lines
20 KiB
Perl
Raw Normal View History

2019-02-05 21:49:12 +00:00
#!/usr/bin/perl
#
package S2::NodeTerm;
use strict;
use S2::Node;
use S2::NodeExpr;
use S2::NodeArrayLiteral;
use S2::NodeArguments;
use vars qw($VERSION @ISA
$INTEGER $STRING $BOOL $VARREF $SUBEXPR
$DEFINEDTEST $SIZEFUNC $REVERSEFUNC $ISNULLFUNC
$NEW $NEWNULL $FUNCCALL $METHCALL $ARRAY $OBJ_INTERPOLATE);
$VERSION = '1.0';
@ISA = qw(S2::NodeExpr);
$INTEGER = 1;
$STRING = 2;
$BOOL = 3;
$VARREF = 4;
$SUBEXPR = 5;
$DEFINEDTEST = 6;
$SIZEFUNC = 7;
$REVERSEFUNC = 8;
$ISNULLFUNC = 12;
$NEW = 9;
$NEWNULL = 13;
$FUNCCALL = 10;
$METHCALL = 11;
$ARRAY = 14;
$OBJ_INTERPOLATE = 15;
sub new {
my ($class, $n) = @_;
my $node = new S2::NodeExpr;
bless $node, $class;
}
sub canStart {
my ($class, $toker) = @_;
my $t = $toker->peek();
return $t->isa('S2::TokenIntegerLiteral') ||
$t->isa('S2::TokenStringLiteral') ||
$t->isa('S2::TokenIdent') ||
$t == $S2::TokenPunct::DOLLAR ||
$t == $S2::TokenPunct::LPAREN ||
$t == $S2::TokenPunct::LBRACK ||
$t == $S2::TokenPunct::LBRACE ||
$t == $S2::TokenKeyword::DEFINED ||
$t == $S2::TokenKeyword::TRUE ||
$t == $S2::TokenKeyword::FALSE ||
$t == $S2::TokenKeyword::NEW ||
$t == $S2::TokenKeyword::SIZE ||
$t == $S2::TokenKeyword::REVERSE ||
$t == $S2::TokenKeyword::ISNULL ||
$t == $S2::TokenKeyword::NULL;
}
sub getType {
my ($this, $ck, $wanted) = @_;
return $this->{'_cache_type'} if exists $this->{'_cache_type'};
$this->{'_cache_type'} = _getType($this, $ck, $wanted);
}
sub _getType {
my ($this, $ck, $wanted) = @_;
my $type = $this->{'type'};
if ($type == $INTEGER) { return $S2::Type::INT; }
if ($type == $STRING) {
return $this->{'nodeString'}->getType($ck, $S2::Type::STRING)
if $this->{'nodeString'};
if ($ck->isStringCtor($wanted)) {
$this->{'ctorclass'} = $wanted->baseType();
return $wanted;
}
return $S2::Type::STRING;
}
if ($type == $SUBEXPR) { return $this->{'subExpr'}->getType($ck, $wanted); }
if ($type == $BOOL) { return $S2::Type::BOOL; }
if ($type == $SIZEFUNC) {
$this->{'subType'} = $this->{'subExpr'}->getType($ck);
return $S2::Type::INT if
$this->{'subType'}->isArrayOf() ||
$this->{'subType'}->isHashOf() ||
$this->{'subType'}->equals($S2::Type::STRING);
S2::error($this, "Can't use size on expression that's not a string, hash or array.");
}
if ($type == $REVERSEFUNC) {
$this->{'subType'} = $this->{'subExpr'}->getType($ck);
# reverse a string
return $S2::Type::STRING if
$this->{'subType'}->equals($S2::Type::STRING);
# reverse an array
return $this->{'subType'} if
$this->{'subType'}->isArrayOf();
S2::error($this, "Can't reverse on expression that's not a string or array.");
}
if ($type == $ISNULLFUNC || $type == $DEFINEDTEST) {
my $op = ($type == $ISNULLFUNC) ? "isnull" : "defined";
$this->{'subType'} = $this->{'subExpr'}->getType($ck);
if ($this->{'subExpr'}->isa('S2::NodeTerm')) {
my $nt = $this->{'subExpr'};
if ($nt->{'type'} != $VARREF && $nt->{'type'} != $FUNCCALL &&
$nt->{'type'} != $METHCALL) {
S2::error($this, "$op must only be used on an object variable, ".
"function call or method call.");
}
} else {
S2::error($this, "$op must only be used on an object variable, ".
"function call or method call.");
}
# can't be used on arrays and hashes
unless ($this->{'subType'}->isSimple()) {
S2::error($this, "Can't use $op on an array or hash.");
}
# not primitive types either
if ($this->{'subType'}->isPrimitive()) {
S2::error($this, "Can't use $op on primitive types.");
}
# nor void
if ($this->{'subType'}->equals($S2::Type::VOID)) {
S2::error($this, "Can't use $op on a void value.");
}
return $S2::Type::BOOL;
}
if ($type == $NEW || $type == $NEWNULL) {
my $clas = $this->{'newClass'}->getIdent();
if ($clas eq "int" || $clas eq "string") {
S2::error($this, "Can't use 'new' with primitive type '$clas'");
}
my $nc = $ck->getClass($clas);
unless ($nc) {
S2::error($this, "Can't instantiate unknown class.");
}
return new S2::Type $clas;
}
if ($type == $VARREF) {
unless ($ck->getInFunction()) {
S2::error($this, "Can't reference a variable outside of a function.");
}
return $this->{'var'}->getType($ck, $wanted);
}
if ($type == $METHCALL || $type == $FUNCCALL) {
S2::error($this, "Can't call a function or method outside of a function")
unless $ck->getInFunction();
if ($type == $METHCALL) {
my $vartype = $this->{'var'}->getType($ck, $wanted);
S2::error($this, "Cannot call a method on an array or hash")
unless $vartype->isSimple();
$this->{'funcClass'} = $vartype->toString;
my $methClass = $ck->getClass($this->{'funcClass'});
S2::error($this, "Can't call a method on an instance of an undefined class")
unless $methClass;
}
$this->{'funcID'} =
S2::Checker::functionID($this->{'funcClass'},
$this->{'funcIdent'}->getIdent(),
$this->{'funcArgs'}->typeList($ck));
$this->{'funcBuiltin'} = $ck->isFuncBuiltin($this->{'funcID'});
$this->{'funcID_noclass'} =
S2::Checker::functionID(undef,
$this->{'funcIdent'}->getIdent(),
$this->{'funcArgs'}->typeList($ck));
my $t = $ck->functionType($this->{'funcID'});
$this->{'funcNum'} = $ck->functionNum($this->{'funcID'})
unless $this->{'funcBuiltin'};
S2::error($this, "Unknown function $this->{'funcID'}")
unless $t;
return $t;
}
if ($type == $ARRAY) {
return $this->{'subExpr'}->getType($ck, $wanted);
}
S2::error($this, "Unknown NodeTerm type");
}
sub isLValue {
my $this = shift;
return 1 if $this->{'type'} == $VARREF;
return $this->{'subExpr'}->isLValue()
if $this->{'type'} == $SUBEXPR;
return 0;
}
# make the object interpolate in a string
sub makeAsString {
my ($this, $ck) = @_;
if ($this->{'type'} == $STRING) {
return $this->{'nodeString'}->makeAsString($ck);
}
return 0 unless $this->{'type'} == $VARREF;
my $t = $this->{'var'}->getType($ck);
return 0 unless $t->isSimple();
my $bt = $t->baseType;
# class has .toString() or .as_string() method?
if (my $methname = $ck->classHasToString($bt)) {
# let's change this VARREF into a METHCALL!
# warning: ugly hacks ahead...
my $funcID = "${bt}::$methname()";
if ($ck->isFuncBuiltin($funcID)) {
# builtins map to a normal function call.
# the builtin function is responsible for checking if the
# object is S2::check_defined() and then returning nothing.
$this->{'type'} = $METHCALL;
$this->{'funcIdent'} = new S2::TokenIdent $methname;
$this->{'funcClass'} = $bt;
$this->{'funcArgs'} = new S2::NodeArguments; # empty
$this->{'funcID_noclass'} = "$methname()";
$this->{'funcID'} = $funcID;
$this->{'funcBuiltin'} = 1;
} else {
# if it's S2-level as_string(), then we call
# S2::interpolate_object($ctx, "ClassName", $obj, $methname)
$this->{'type'} = $OBJ_INTERPOLATE;
$this->{'funcClass'} = $bt;
$this->{'objint_method'} = $methname;
}
return 1;
}
# class has $.as_string string member?
if ($ck->classHasAsString($bt)) {
$this->{'var'}->useAsString();
return 1;
}
return 0;
}
sub parse {
my ($class, $toker) = @_;
my $nt = new S2::NodeTerm;
my $t = $toker->peek();
# integer literal
if ($t->isa('S2::TokenIntegerLiteral')) {
$nt->{'type'} = $INTEGER;
$nt->{'tokInt'} = $nt->eatToken($toker);
return $nt;
}
# boolean literal
if ($t == $S2::TokenKeyword::TRUE ||
$t == $S2::TokenKeyword::FALSE) {
$nt->{'type'} = $BOOL;
$nt->{'boolValue'} = $t == $S2::TokenKeyword::TRUE;
$nt->eatToken($toker);
return $nt;
}
# string literal
if ($t->isa('S2::TokenStringLiteral')) {
my $ts = $t;
my $ql = $ts->getQuotesLeft();
my $qr = $ts->getQuotesRight();
if ($qr) {
# whole string literal
$nt->{'type'} = $STRING;
$nt->{'tokStr'} = $nt->eatToken($toker);
$nt->setStart($nt->{'tokStr'});
return $nt;
}
# interpolated string literal (turn into a subexpr)
my $toklist = [];
$toker->pushInString($ql);
$nt->{'type'} = $STRING;
$nt->{'tokStr'} = $nt->eatToken($toker);
push @$toklist, $nt->{'tokStr'}->clone();
$nt->{'tokStr'}->setQuotesRight($ql);
my $lhs = $nt;
my $filepos = $nt->{'tokStr'}->getFilePos();
my $loop = 1;
while ($loop) {
my $rhs = undef;
my $tok = $toker->peek();
unless ($tok) {
S2::error($tok, "Unexpected end of file. Unclosed string literal?");
}
if ($tok->isa('S2::TokenStringLiteral')) {
$rhs = new S2::NodeTerm;
$ts = $tok;
$rhs->{'type'} = $STRING;
$rhs->{'tokStr'} = $rhs->eatToken($toker);
push @$toklist, $rhs->{'tokStr'}->clone();
$loop = 0 if $ts->getQuotesRight() == $ql;
$ts->setQuotesRight($ql);
$ts->setQuotesLeft($ql);
} elsif ($tok == $S2::TokenPunct::DOLLAR) {
$rhs = parse S2::NodeTerm $toker;
push @$toklist, @{$rhs->getTokenList()};
} else {
S2::error($tok, "Error parsing interpolated string: " . $tok->toString);
}
# don't make a sum out of a blank string on either side
my $join = 1;
if ($lhs->isa('S2::NodeTerm') &&
$lhs->{'type'} == $STRING &&
length($lhs->{'tokStr'}->getString()) == 0)
{
$lhs = $rhs;
$join = 0;
}
if ($rhs->isa('S2::NodeTerm') &&
$rhs->{'type'} == $STRING &&
length($rhs->{'tokStr'}->getString()) == 0)
{
$join = 0;
}
if ($join) {
$lhs = S2::NodeSum->new($lhs, $S2::TokenPunct::PLUS, $rhs);
}
}
$toker->popInString();
$lhs->setTokenList($toklist);
$lhs->setStart($filepos);
my $rnt = new S2::NodeTerm;
$rnt->{'type'} = $STRING;
$rnt->{'nodeString'} = $lhs;
$rnt->addNode($lhs);
return $rnt;
}
# Sub-expression (in parenthesis)
if ($t == $S2::TokenPunct::LPAREN) {
$nt->{'type'} = $SUBEXPR;
$nt->setStart($nt->eatToken($toker));
$nt->{'subExpr'} = parse S2::NodeExpr $toker;
$nt->addNode($nt->{'subExpr'});
$nt->requireToken($toker, $S2::TokenPunct::RPAREN);
return $nt;
}
# defined test
if ($t == $S2::TokenKeyword::DEFINED) {
$nt->{'type'} = $DEFINEDTEST;
$nt->setStart($nt->eatToken($toker));
$nt->{'subExpr'} = parse S2::NodeTerm $toker;
$nt->addNode($nt->{'subExpr'});
return $nt;
}
# reverse function
if ($t == $S2::TokenKeyword::REVERSE) {
$nt->{'type'} = $REVERSEFUNC;
$nt->eatToken($toker);
$nt->{'subExpr'} = parse S2::NodeTerm $toker;
$nt->addNode($nt->{'subExpr'});
return $nt;
}
# size function
if ($t == $S2::TokenKeyword::SIZE) {
$nt->{'type'} = $SIZEFUNC;
$nt->eatToken($toker);
$nt->{'subExpr'} = parse S2::NodeTerm $toker;
$nt->addNode($nt->{'subExpr'});
return $nt;
}
# isnull function
if ($t == $S2::TokenKeyword::ISNULL) {
$nt->{'type'} = $ISNULLFUNC;
$nt->eatToken($toker);
$nt->{'subExpr'} = parse S2::NodeTerm $toker;
$nt->addNode($nt->{'subExpr'});
return $nt;
}
# new andnull
if ($t == $S2::TokenKeyword::NEW ||
$t == $S2::TokenKeyword::NULL) {
$nt->{'type'} = $t == $S2::TokenKeyword::NEW ? $NEW : $NEWNULL;
$nt->eatToken($toker);
$nt->{'newClass'} = $nt->getIdent($toker);
return $nt;
}
# VarRef
if ($t == $S2::TokenPunct::DOLLAR) {
$nt->{'type'} = $VARREF;
$nt->{'var'} = parse S2::NodeVarRef $toker;
$nt->addNode($nt->{'var'});
# check for -> after, like: $object->method(arg1, arg2, ...)
if ($toker->peek() == $S2::TokenPunct::DEREF) {
$nt->{'derefLine'} = $toker->peek()->getFilePos()->line;
$nt->eatToken($toker);
$nt->{'type'} = $METHCALL;
# don't return... parsing continues below.
} else {
return $nt;
}
}
# function/method call
if ($nt->{'type'} == $METHCALL || $t->isa('S2::TokenIdent')) {
$nt->{'type'} = $FUNCCALL unless $nt->{'type'} == $METHCALL;
$nt->{'funcIdent'} = $nt->getIdent($toker);
$nt->{'funcArgs'} = parse S2::NodeArguments $toker;
$nt->addNode($nt->{'funcArgs'});
return $nt;
}
# array/hash literal
if (S2::NodeArrayLiteral->canStart($toker)) {
$nt->{'type'} = $ARRAY;
$nt->{'subExpr'} = parse S2::NodeArrayLiteral $toker;
$nt->addNode($nt->{'subExpr'});
return $nt;
}
S2::error($toker->peek(), "Can't finish parsing NodeTerm");
}
sub asS2 {
my ($this, $o) = @_;
die "NodeTerm::asS2(): not implemented";
}
sub asPerl {
my ($this, $bp, $o) = @_;
my $type = $this->{'type'};
if ($type == $INTEGER) {
$this->{'tokInt'}->asPerl($bp, $o);
return;
}
if ($type == $STRING) {
if (defined $this->{'nodeString'}) {
$o->write("(");
$this->{'nodeString'}->asPerl($bp, $o);
$o->write(")");
return;
}
if ($this->{'ctorclass'}) {
my $pkg = $bp->getBuiltinPackage() || "S2::Builtin";
$o->write("${pkg}::$this->{'ctorclass'}__$this->{'ctorclass'}(");
}
$this->{'tokStr'}->asPerl($bp, $o);
$o->write(")") if $this->{'ctorclass'};
return;
}
if ($type == $BOOL) {
$o->write($this->{'boolValue'} ? "1" : "0");
return;
}
if ($type == $SUBEXPR) {
$o->write("(");
$this->{'subExpr'}->asPerl($bp, $o);
$o->write(")");
return;
}
if ($type == $ARRAY) {
$this->{'subExpr'}->asPerl($bp, $o);
return;
}
if ($type == $NEW) {
$o->write("{'_type'=>" .
$bp->quoteString($this->{'newClass'}->getIdent()) .
"}");
return;
}
if ($type == $NEWNULL) {
$o->write("{'_type'=>" .
$bp->quoteString($this->{'newClass'}->getIdent()) .
", '_isnull'=>1}");
return;
}
if ($type == $REVERSEFUNC) {
if ($this->{'subType'}->isArrayOf()) {
$o->write("[reverse(\@{");
$this->{'subExpr'}->asPerl($bp, $o);
$o->write("})]");
} elsif ($this->{'subType'}->equals($S2::Type::STRING)) {
$o->write("reverse(");
$this->{'subExpr'}->asPerl($bp, $o);
$o->write(")");
}
return;
}
if ($type == $SIZEFUNC) {
if ($this->{'subType'}->isArrayOf()) {
$o->write("scalar(\@{");
$this->{'subExpr'}->asPerl($bp, $o);
$o->write("})");
} elsif ($this->{'subType'}->isHashOf()) {
$o->write("scalar(keys \%{");
$this->{'subExpr'}->asPerl($bp, $o);
$o->write("})");
} elsif ($this->{'subType'}->equals($S2::Type::STRING)) {
$o->write("length(");
$this->{'subExpr'}->asPerl($bp, $o);
$o->write(")");
}
return;
}
if ($type == $DEFINEDTEST) {
$o->write("S2::check_defined(");
$this->{'subExpr'}->asPerl($bp, $o);
$o->write(")");
return;
}
if ($type == $ISNULLFUNC) {
$o->write("(ref ");
$this->{'subExpr'}->asPerl($bp, $o);
$o->write(" ne \"HASH\" || ");
$this->{'subExpr'}->asPerl($bp, $o);
$o->write("->{'_isnull'})");
return;
}
if ($type == $VARREF) {
$this->{'var'}->asPerl($bp, $o);
return;
}
if ($type == $OBJ_INTERPOLATE) {
$o->write("S2::interpolate_object(\$_ctx, '$this->{'funcClass'}', ");
$this->{'var'}->asPerl($bp, $o);
$o->write(", '$this->{'objint_method'}()')");
return;
}
if ($type == $FUNCCALL || $type == $METHCALL) {
# builtin functions can be optimized.
if ($this->{'funcBuiltin'}) {
# these built-in functions can be inlined.
if ($this->{'funcID'} eq "string(int)") {
$this->{'funcArgs'}->asPerl($bp, $o, 0);
return;
}
if ($this->{'funcID'} eq "int(string)") {
# cast from string to int by adding zero to it
$o->write("int(");
$this->{'funcArgs'}->asPerl($bp, $o, 0);
$o->write(")");
return;
}
# otherwise, call the builtin function (avoid a layer
# of indirection), unless it's for a class that has
# children (won't know until run-time which class to call)
my $pkg = $bp->getBuiltinPackage() || "S2::Builtin";
$o->write("${pkg}::");
if ($this->{'funcClass'}) {
$o->write("$this->{'funcClass'}__");
}
$o->write($this->{'funcIdent'}->getIdent());
} else {
if ($type == $METHCALL && $this->{'funcClass'} ne "string") {
$o->write("\$_ctx->[VTABLE]->{get_object_func_num(");
$o->write($bp->quoteString($this->{'funcClass'}));
$o->write(",");
$this->{'var'}->asPerl($bp, $o);
$o->write(",");
$o->write($bp->quoteString($this->{'funcID_noclass'}));
$o->write(",");
$o->write($bp->getLayerID());
$o->write(",");
$o->write($this->{'derefLine'}+0);
if ($this->{'var'}->isSuper()) {
$o->write(",1");
}
$o->write(")}->");
} elsif ($type == $METHCALL) {
$o->write("\$_ctx->[VTABLE]->{get_func_num(");
$o->write($bp->quoteString($this->{'funcID'}));
$o->write(")}->");
} else {
$o->write("\$_ctx->[VTABLE]->{\$_l2g_func[$this->{'funcNum'}]}->");
}
}
$o->write("(\$_ctx, ");
# this pointer
if ($type == $METHCALL) {
$this->{'var'}->asPerl($bp, $o);
$o->write(", ");
}
$this->{'funcArgs'}->asPerl($bp, $o, 0);
$o->write(")");
return;
}
die "Unknown term type";
}
sub isProperty {
my $this = shift;
return 0 unless $this->{'type'} == $VARREF;
return $this->{'var'}->isProperty();
}
sub isBuiltinProperty {
my ($this, $ck) = @_;
return 0 unless $this->{'type'} == $VARREF;
return 0 unless $this->{'var'}->isProperty();
my $name = $this->{'var'}->propName();
return $ck->propertyBuiltin($name);
}