shithub: purgatorio

ref: fb7dd4b3a868cb8987049c95bb32e6425a73c8b9
dir: /appl/lib/ecmascript/obj.b/

View raw version
#
# want to use the value in a context which
# prefers an object, so coerce schizo vals
# to object versions
#
coerceToObj(ex: ref Exec, v: ref Val): ref Val
{
	o: ref Obj;

	case v.ty{
	TBool =>
		o = mkobj(ex.boolproto, "Boolean");
		o.val = v;
	TStr =>
		o = mkobj(ex.strproto, "String");
		o.val = v;
		valinstant(o, DontEnum|DontDelete|ReadOnly, "length", numval(real len v.str));
	TNum =>
		o = mkobj(ex.numproto, "Number");
		o.val = v;
	TRegExp =>
		o = mkobj(ex.regexpproto, "RegExp");
		o.val = v;
		valinstant(o, DontEnum|DontDelete|ReadOnly, "length", numval(real len v.rev.p));
		valinstant(o, DontEnum|DontDelete|ReadOnly, "source", strval(v.rev.p));
		valinstant(o, DontEnum|DontDelete|ReadOnly, "global", strhas(v.rev.f, 'g'));
		valinstant(o, DontEnum|DontDelete|ReadOnly, "ignoreCase", strhas(v.rev.f, 'i'));
		valinstant(o, DontEnum|DontDelete|ReadOnly, "multiline", strhas(v.rev.f, 'm'));
		valinstant(o, DontEnum|DontDelete, "lastIndex", numval(real v.rev.i));
	* =>
		return v;
	}
	return objval(o);
}

coerceToVal(v: ref Val): ref Val
{
	if(v.ty != TObj)
		return v;
	o := v.obj;
	if(o.host != nil && o.host != me
	|| o.class != "String"
	|| o.class != "Number"
	|| o.class != "Boolean")
		return v;
	return o.val;
}

isstrobj(o: ref Obj): int
{
	return (o.host == nil || o.host == me) && o.class == "String";
}

isnumobj(o: ref Obj): int
{
	return (o.host == nil || o.host == me) && o.class == "Number";
}

isboolobj(o: ref Obj): int
{
	return (o.host == nil || o.host == me) && o.class == "Boolean";
}

isdateobj(o: ref Obj): int
{
	return (o.host == nil || o.host == me) && o.class == "Date";
}

isregexpobj(o: ref Obj): int
{
	return (o.host == nil || o.host == me) && o.class == "RegExp";
}

isfuncobj(o: ref Obj): int
{
	return (o.host == nil || o.host == me) && o.class == "Function";
}

isarray(o: ref Obj): int
{
#	return (o.host == nil || o.host == me) && o.class == "Array";
	# relax the host test
	# so that hosts can intercept Array operations and defer
	# unhandled ops to the builtin
	return o.class == "Array";
}

iserr(o: ref Obj): int
{
	return (o.host == nil || o.host == me) && o.class == "Error";
}

isactobj(o: ref Obj): int
{
	return o.host == nil && o.class == "Activation";
}

isnull(v: ref Val): int
{
	return v == null || v == nil;
}

isundefined(v: ref Val): int
{
	return v == undefined;
}

isstr(v: ref Val): int
{
	return v.ty == TStr;
}

isnum(v: ref Val): int
{
	return v.ty == TNum;
}

isbool(v: ref Val): int
{
	return v.ty == TBool;
}

isobj(v: ref Val): int
{
	return v.ty == TObj;
}

isregexp(v: ref Val): int
{
	return v.ty == TRegExp || v.ty == TObj && isregexpobj(v.obj);
}

#
# retrieve the object field if it's valid
#
getobj(v: ref Val): ref Obj
{
	if(v.ty == TObj)
		return v.obj;
	return nil;
}

isprimval(v: ref Val): int
{
	return v.ty != TObj;
}

pushscope(ex: ref Exec, o: ref Obj)
{
	ex.scopechain = o :: ex.scopechain;
}

popscope(ex: ref Exec)
{
	ex.scopechain = tl ex.scopechain;
}

runtime(ex: ref Exec, o: ref Obj, s: string)
{
	ex.error = s;
	if(o == nil)
		ex.errval = undefined;
	else
		ex.errval = objval(o);
	if(debug['r']){
		print("ecmascript runtime error: %s\n", s);
		if(""[5] == -1);	# abort
	}
	raise "throw";
	exit;	# never reached
}

mkobj(proto: ref Obj, class: string): ref Obj
{
	if(class == nil)
		class = "Object";
	return ref Obj(nil, proto, nil, nil, nil, class, nil, nil);
}

valcheck(ex: ref Exec, v: ref Val, hint: int)
{
	if(v == nil
	|| v.ty < 0
	|| v.ty >= NoHint
	|| v.ty == TBool && v != true && v != false
	|| v.ty == TObj && v.obj == nil
	|| hint != NoHint && v.ty != hint)
		runtime(ex, RangeError, "bad value generated by host object");
}

# builtin methods for properties
esget(ex: ref Exec, o: ref Obj, prop: string, force: int): ref Val
{
	for( ; o != nil; o = o.prototype){
		if(!force && o.host != nil && o.host != me){
			v := o.host->get(ex, o, prop);
			valcheck(ex, v, NoHint);
			return v;
		}

		for(i := 0; i < len o.props; i++)
			if(o.props[i] != nil && o.props[i].name == prop)
				return o.props[i].val.val;
		force = 0;
	}
	return undefined;
}

esputind(o: ref Obj, prop: string): int
{
	empty := -1;
	props := o.props;
	for(i := 0; i < len props; i++){
		if(props[i] == nil)
			empty = i;
		else if(props[i].name == prop)
			return i;
	}
	if(empty != -1)
		return empty;
	
	props = array[i+1] of ref Prop;
	props[:] = o.props;
	o.props = props;
	return i;
}

esput(ex: ref Exec, o: ref Obj, prop: string, v: ref Val, force: int)
{
	ai: big;

	if(!force && o.host != nil && o.host != me)
		return o.host->put(ex, o, prop, v);

	if(escanput(ex, o, prop, 0) != true)
		return;

	#
	# should this test for prototype == ex.arrayproto?
	# hard to say, but 15.4.5 "Properties of Array Instances" implies not
	#
	if(isarray(o))
		al := toUint32(ex, esget(ex, o, "length", 1));

	i := esputind(o, prop);
	props := o.props;
	if(props[i] != nil)
		props[i].val.val = v;
	else
		props[i] = ref Prop(0, prop, ref RefVal(v));
	if(!isarray(o))
		return;

	if(prop == "length"){
		nl := toUint32(ex, v);
		for(ai = nl; ai < al; ai++)
			esdelete(ex, o, string ai, 1);
		props[i].val.val = numval(real nl);
	}else{
		ai = big prop;
		if(prop != string ai || ai < big 0 || ai >= 16rffffffff)
			return;
		i = esputind(o, "length");
		if(props[i] == nil)
			fatal(ex, "bogus array esput");
		else if(toUint32(ex, props[i].val.val) <= ai)
			props[i].val.val = numval(real(ai+big 1));
	}
}

escanput(ex: ref Exec, o: ref Obj, prop: string, force: int): ref Val
{
	for( ; o != nil; o = o.prototype){
		if(!force && o.host != nil && o.host != me){
			v := o.host->canput(ex, o, prop);
			valcheck(ex, v, TBool);
			return v;
		}

		for(i := 0; i < len o.props; i++){
			if(o.props[i] != nil && o.props[i].name == prop){
				if(o.props[i].attr & ReadOnly)
					return false;
				else
					return true;
			}
		}

		force = 0;
	}
	return true;
}

eshasproperty(ex: ref Exec, o: ref Obj, prop: string, force: int): ref Val
{
	for(; o != nil; o = o.prototype){
		if(!force && o.host != nil && o.host != me){
			v := o.host->hasproperty(ex, o, prop);
			valcheck(ex, v, TBool);
			return v;
		}
		for(i := 0; i < len o.props; i++)
			if(o.props[i] != nil && o.props[i].name == prop)
				return true;
	}
	return false;
}

eshasenumprop(o: ref Obj, prop: string): ref Val
{
	for(i := 0; i < len o.props; i++)
		if(o.props[i] != nil && o.props[i].name == prop){
			if(o.props[i].attr & DontEnum)
				return false;
			return true;
		}
	return false;
}
	
propshadowed(start, end: ref Obj, prop: string): int
{
	for(o := start; o != end; o = o.prototype){
		if(o.host != nil && o.host != me)
			return 0;
		for(i := 0; i < len o.props; i++)
			if(o.props[i] != nil && o.props[i].name == prop)
				return 1;
	}
	return 0;
}

esdelete(ex: ref Exec, o: ref Obj, prop: string, force: int)
{
	if(!force && o.host != nil && o.host != me)
		return o.host->delete(ex, o, prop);

	for(i := 0; i < len o.props; i++){
		if(o.props[i] != nil && o.props[i].name == prop){
			if(!(o.props[i].attr & DontDelete))
				o.props[i] = nil;
			return;
		}
	}
}

esdeforder := array[] of {"valueOf", "toString"};
esdefaultval(ex: ref Exec, o: ref Obj, ty: int, force: int): ref Val
{
	v: ref Val;

	if(!force && o.host != nil && o.host != me){
		v = o.host->defaultval(ex, o, ty);
		valcheck(ex, v, NoHint);
		if(!isprimval(v))
			runtime(ex, TypeError, "host object returned an object to [[DefaultValue]]");
		return v;
	}

	hintstr := 0;
	if(ty == TStr || ty == NoHint && isdateobj(o))
		hintstr = 1;

	for(i := 0; i < 2; i++){
		v = esget(ex, o, esdeforder[hintstr ^ i], 0);
		if(v != undefined && v.ty == TObj && v.obj.call != nil){
			r := escall(ex, v.obj, o, nil, 0);
			v = nil;
			if(!r.isref)
				v = r.val;
			if(v != nil && isprimval(v))
				return v;
		}
	}
	runtime(ex, TypeError, "no default value");
	return nil;
}

esprimid(ex: ref Exec, s: string): ref Ref
{
	for(sc := ex.scopechain; sc != nil; sc = tl sc){
		o := hd sc;
		if(eshasproperty(ex, o, s, 0) == true)
			return ref Ref(1, nil, o, s);
	}

	#
	# the right place to add literals?
	#
	case s{
	"null" =>
		return ref Ref(0, null, nil, "null");
	"true" =>
		return ref Ref(0, true, nil, "true");
	"false" =>
		return ref Ref(0, false, nil, "false");
	}
	return ref Ref(1, nil, nil, s);
}

bivar(ex: ref Exec, sc: list of ref Obj, s: string): ref Val
{
	for(; sc != nil; sc = tl sc){
		o := hd sc;
		if(eshasproperty(ex, o, s, 0) == true)
			return esget(ex, o, s, 0);
	}
	return nil;
}

esconstruct(ex: ref Exec, func: ref Obj, args: array of ref Val): ref Obj
{
	o: ref Obj;

	if(func.construct == nil)
		runtime(ex, TypeError, "new must be applied to a constructor object");
	if(func.host != nil)
		o = func.host->construct(ex, func, args);
	else{
		o = getobj(esget(ex, func, "prototype", 0));
		if(o == nil)
			o = ex.objproto;
		this := mkobj(o, "Object");
		o = getobj(getValue(ex, escall(ex, func, this, args, 0)));

		# Divergence from ECMA-262
		#
		# observed that not all script-defined constructors return an object,
		# the value of 'this' is assumed to be the value of the constructor
		if (o == nil)
			o = this;
	}
	if(o == nil)
		runtime(ex, TypeError, func.val.str+" failed to generate an object");
	return o;
}

escall(ex: ref Exec, func, this: ref Obj, args: array of ref Val, eval: int): ref Ref
{
	if(func.call == nil)
		runtime(ex, TypeError, "can only call function objects");
	if(this == nil)
		this = ex.global;

	r: ref Ref = nil;
	if(func.host != nil){
		r = func.host->call(ex, func, this, args, 0);
		if(r.isref && r.name == nil)
			runtime(ex, ReferenceError, "host call returned a bad reference");
		else if(!r.isref)
			valcheck(ex, r.val, NoHint);
		return r;
	}

	argobj := mkobj(ex.objproto, "Object");
	actobj := mkobj(nil, "Activation");

	oargs: ref RefVal = nil;
	props := func.props;
	empty := -1;
	i := 0;
	for(i = 0; i < len props; i++){
		if(props[i] == nil)
			empty = i;
		else if(props[i].name == "arguments"){
			oargs = props[i].val;
			empty = i;
			break;
		}
	}
	if(i == len func.props){
		if(empty == -1){
			props = array[i+1] of ref Prop;
			props[:] = func.props;
			func.props = props;
			empty = i;
		}
		props[empty] = ref Prop(DontDelete|DontEnum|ReadOnly, "arguments", nil);
	}
	props[empty].val = ref RefVal(objval(argobj));

	#
	#see section 10.1.3 page 33
	# if multiple params share the same name, the last one takes effect
	# vars don't override params of the same name, or earlier parms defs
	#
	actobj.props = array[] of {ref Prop(DontDelete, "arguments", ref RefVal(objval(argobj)))};

	argobj.props = array[len args + 2] of {
		ref Prop(DontEnum, "callee", ref RefVal(objval(func))),
		ref Prop(DontEnum, "length", ref RefVal(numval(real len args))),
	};

	#
	# instantiate the arguments by name in the activation object
	# and by number in the arguments object, aliased to the same RefVal.
	#
	params := func.call.params;
	for(i = 0; i < len args; i++){
		rjv := ref RefVal(args[i]);
		argobj.props[i+2] = ref Prop(DontEnum, string i, rjv);
		if(i < len params)
			fvarinstant(actobj, 1, DontDelete, params[i], rjv);
	}
	for(; i < len params; i++)
		fvarinstant(actobj, 1, DontDelete, params[i], ref RefVal(undefined));

	#
	# instantiate the local variables defined within the function
	#
	vars := func.call.code.vars;
	for(i = 0; i < len vars; i++)
		valinstant(actobj, DontDelete, vars[i].name, undefined);

	# NOTE: the treatment of scopechain here is wrong if nested functions are
	# permitted.  ECMA-262 currently does not support nested functions (so we
	# are ok for now) - but other flavours of Javascript do.
	# Difficulties are introduced by multiple execution contexts.
	# e.g. in web browsers, one frame can ref a func in
	# another frame (each frame has a distinct execution context), but the func
	# ids must bind as if in original lexical context

	osc := ex.scopechain;
	ex.this = this;
	ex.scopechain = actobj :: osc;
	(k, v, nil) := exec(ex, func.call.code);
	ex.scopechain = osc;

	#
	# i can find nothing in the docs which defines
	# the value of a function call
	# this seems like a reasonable definition
	#
	if (k == CThrow)
		raise "throw";
	if(!eval && k != CReturn || v == nil)
		v = undefined;
	r = valref(v);

	props = func.props;
	for(i = 0; i < len props; i++){
		if(props[i] != nil && props[i].name == "arguments"){
			if(oargs == nil)
				props[i] = nil;
			else
				props[i].val = oargs;
			break;
		}
	}

	return r;
}

#
# routines for instantiating variables
#
fvarinstant(o: ref Obj, force, attr: int, s: string, v: ref RefVal)
{
	props := o.props;
	empty := -1;
	for(i := 0; i < len props; i++){
		if(props[i] == nil)
			empty = i;
		else if(props[i].name == s){
			if(force){
				props[i].attr = attr;
				props[i].val = v;
			}
			return;
		}
	}
	if(empty == -1){
		props = array[i+1] of ref Prop;
		props[:] = o.props;
		o.props = props;
		empty = i;
	}
	props[empty] = ref Prop(attr, s, v);
}

varinstant(o: ref Obj, attr: int, s: string, v: ref RefVal)
{
	fvarinstant(o, 0, attr, s, v);
}

valinstant(o: ref Obj, attr: int, s: string, v: ref Val)
{
	fvarinstant(o, 0, attr, s, ref RefVal(v));
}

#
# instantiate global or val variables
# note that only function variables are forced to be redefined;
# all other variables have a undefined val.val field
#
globalinstant(o: ref Obj, vars: array of ref Prop)
{
	for(i := 0; i < len vars; i++){
		force := vars[i].val.val != undefined;
		fvarinstant(o, force, 0, vars[i].name, vars[i].val);
	}
}

numval(r: real): ref Val
{
	return ref Val(TNum, r, nil, nil, nil);
}

strval(s: string): ref Val
{
	return ref Val(TStr, 0., s, nil, nil);
}

objval(o: ref Obj): ref Val
{
	return ref Val(TObj, 0., nil, o, nil);
}

regexpval(p: string, f: string, i: int): ref Val
{
	return ref Val(TRegExp, 0., nil, nil, ref REval(p, f, i));
}

#
# operations on refereneces
# note the substitution of nil for an object
# version of null, implied in the discussion of
# Reference Types, since there isn't a null object
#
valref(v: ref Val): ref Ref
{
	return ref Ref(0, v, nil, nil);
}

getBase(ex: ref Exec, r: ref Ref): ref Obj
{
	if(!r.isref)
		runtime(ex, ReferenceError, "not a reference");
	return r.base;
}

getPropertyName(ex: ref Exec, r: ref Ref): string
{
	if(!r.isref)
		runtime(ex, ReferenceError, "not a reference");
	return r.name;
}

getValue(ex: ref Exec, r: ref Ref): ref Val
{
	if(!r.isref)
		return r.val;
	b := r.base;
	if(b == nil)
		runtime(ex, ReferenceError, "reference " + r.name + " is null");
	return esget(ex, b, r.name, 0);
}

putValue(ex: ref Exec, r: ref Ref, v: ref Val)
{
	if(!r.isref)
		runtime(ex, ReferenceError, "not a reference: " + r.name);
	b := r.base;
	if(b == nil)
		b = ex.global;
	esput(ex, b, r.name, v, 0);
}

#
# conversion routines defined by the abstract machine
# see section 9.
# note that string, boolean, and number objects are
# not automaically coerced to values, and vice versa.
# 
toPrimitive(ex: ref Exec, v: ref Val, ty: int): ref Val
{
	if(v.ty != TObj)
		return v;
	v = esdefaultval(ex, v.obj, ty, 0);
	if(v.ty == TObj)
		runtime(ex, TypeError, "toPrimitive returned an object");
	return v;
}

toBoolean(ex: ref Exec, v: ref Val): ref Val
{
	case v.ty{
	TUndef or
	TNull =>
		return false;
	TBool =>
		return v;
	TNum =>
		if(isnan(v.num))
			return false;
		if(v.num == 0.)
			return false;
	TStr =>
		if(v.str == "")
			return false;
	TObj =>
		break;
	TRegExp =>
		break;
	* =>
		runtime(ex, TypeError, "unknown type in toBoolean");
	}
	return true;
}

toNumber(ex: ref Exec, v: ref Val): real
{
	case v.ty{
	TUndef =>
		return NaN;
	TNull =>
		return 0.;
	TBool =>
		if(v == false)
			return 0.;
		return 1.;
	TNum =>
		return v.num;
	TStr =>
		(si, r) := parsenum(ex, v.str, 0, ParseReal|ParseHex|ParseTrim|ParseEmpty);
		if(si != len v.str)
			r = Math->NaN;
		return r;
	TObj =>
		return toNumber(ex, toPrimitive(ex, v, TNum));
	TRegExp =>
		return NaN;
	* =>
		runtime(ex, TypeError, "unknown type in toNumber");
		return 0.;
	}
}

toInteger(ex: ref Exec, v: ref Val): real
{
	r := toNumber(ex, v);
	if(isnan(r))
		return 0.;
	if(r == 0. || r == +Infinity || r == -Infinity)
		return r;
	return copysign(floor(fabs(r)), r);
}

#
# toInt32 == toUint32, except for numbers > 2^31
#
toInt32(ex: ref Exec, v: ref Val): int
{
	r := toNumber(ex, v);
	if(isnan(r) || r == 0. || r == +Infinity || r == -Infinity)
		return 0;
	r = copysign(floor(fabs(r)), r);
	# need to convert to big since it might be unsigned
	return int big fmod(r, 4294967296.);
}

toUint32(ex: ref Exec, v: ref Val): big
{
	r := toNumber(ex, v);
	if(isnan(r) || r == 0. || r == +Infinity || r == -Infinity)
		return big 0;
	r = copysign(floor(fabs(r)), r);
	# need to convert to big since it might be unsigned
	b := big fmod(r, 4294967296.);
	if(b < big 0)
		fatal(ex, "uint32 < 0");
	return b;
}

toUint16(ex: ref Exec, v: ref Val): int
{
	return toInt32(ex, v) & 16rffff;
}

toString(ex: ref Exec, v: ref Val): string
{
	case v.ty{
	TUndef =>
		return "undefined";
	TNull =>
		return "null";
	TBool =>
		if(v == false)
			return "false";
		return "true";
	TNum =>
		r := v.num;
		if(isnan(r))
			return "NaN";
		if(r == 0.)
			return "0";
		if(r == Infinity)
			return "Infinity";
		if(r == -Infinity)
			return "-Infinity";
		# this is wrong, but right is too hard
		if(r < 1000000000000000000000. && r >= 1./(1000000.)){
			return string r;
		}
		return string r;
	TStr =>
		return v.str;
	TObj =>
		return toString(ex, toPrimitive(ex, v, TStr));
	TRegExp =>
		return "/" + v.rev.p + "/" + v.rev.f;
	* =>
		runtime(ex, TypeError, "unknown type in ToString");
		return "";
	}
}

toObject(ex: ref Exec, v: ref Val): ref Obj
{
	case v.ty{
	TUndef =>
		runtime(ex, TypeError, "can't convert undefined to an object");
	TNull =>
		runtime(ex, TypeError, "can't convert null to an object");
	TBool or
	TStr or
	TNum or
	TRegExp =>
		return coerceToObj(ex, v).obj;
	TObj =>
		return v.obj;
	* =>
		runtime(ex, TypeError, "unknown type in toObject");
		return nil;
	}
	return nil;
}