# # 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; * => 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"; } isarray(o: ref Obj): int { return (o.host == nil || o.host == me) && o.class == "Array"; } isactobj(o: ref Obj): int { return o.host == nil && o.class == "Activation"; } isnull(v: ref Val): int { return v == null; } 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; } # # 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, s: string) { ex.error = s; if(debug['r']){ print("ecmascript runtime error: %s\n", s); if(""[5] == -1); # abort } sys->raise("ecmascript runtime error"); 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); } 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, "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; } 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, "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); v = nil; if(!r.isref) v = r.val; if(v != nil && isprimval(v)) return v; } } runtime(ex, "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, "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))); # 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, func.val.str+" failed to generate an object"); return o; } escall(ex: ref Exec, func, this: ref Obj, args: array of ref Val): ref Ref { if(func.call == nil) runtime(ex, "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); if(r.isref && r.name == nil) runtime(ex, "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 exe := ref *ex; exe.this = this; exe.scopechain = actobj :: ex.scopechain; (k, v) := exec(exe, func.call.code); # # i can find nothing in the docs which defines # the value of a function call # this seems like a reasonable definition # if(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); } strval(s: string): ref Val { return ref Val(TStr, 0., s, nil); } objval(o: ref Obj): ref Val { return ref Val(TObj, 0., nil, o); } # # 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, "not a reference"); return r.base; } getPropertyName(ex: ref Exec, r: ref Ref): string { if(!r.isref) runtime(ex, "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, "reference is null"); return esget(ex, b, r.name, 0); } putValue(ex: ref Exec, r: ref Ref, v: ref Val) { if(!r.isref) runtime(ex, "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, "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(v.num == 0.) return false; TStr => if(v.str == "") return false; TObj => break; * => runtime(ex, "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); if(si != len v.str) r = Math->NaN; return r; TObj => return toNumber(ex, toPrimitive(ex, v, TNum)); * => runtime(ex, "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)); * => runtime(ex, "unknown type in ToString"); return ""; } } toObject(ex: ref Exec, v: ref Val): ref Obj { case v.ty{ TUndef => runtime(ex, "can't convert undefined to an object"); TNull => runtime(ex, "can't convert null to an object"); TBool or TStr or TNum => return coerceToObj(ex, v).obj; TObj => return v.obj; * => runtime(ex, "unknown type in toObject"); return nil; } return nil; }