From 47b6e40e2ef7c211e52871980ff651f05f076453 Mon Sep 17 00:00:00 2001 From: Robert Sayre Date: Sun, 1 Aug 2010 09:57:01 -0700 Subject: [PATCH 1/4] Reland bug 577648 tests. --- .../tests/js1_8_5/regress/regress-577648-1.js | 87 +++++++++++++++++++ .../tests/js1_8_5/regress/regress-577648-2.js | 12 +++ js/src/trace-test/tests/basic/bug582479.js | 5 ++ 3 files changed, 104 insertions(+) create mode 100644 js/src/tests/js1_8_5/regress/regress-577648-1.js create mode 100644 js/src/tests/js1_8_5/regress/regress-577648-2.js create mode 100644 js/src/trace-test/tests/basic/bug582479.js diff --git a/js/src/tests/js1_8_5/regress/regress-577648-1.js b/js/src/tests/js1_8_5/regress/regress-577648-1.js new file mode 100644 index 00000000000..d40c7b0e423 --- /dev/null +++ b/js/src/tests/js1_8_5/regress/regress-577648-1.js @@ -0,0 +1,87 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/licenses/publicdomain/ + */ + +var count = 0; + +function testCaller(obj) { + switch (++count) { + case 1: + case 2: + /* + * The first two times, obj is objA. The first time, we reference + * arguments.callee.caller before obj.go, so the caller getter must + * force the joined function object in the stack frame to cross the + * method read barrier. The second time, obj.go has been cloned and + * it should match the new frame's callee from the get-go. + */ + assertEq(obj, objA); + break; + + case 3: { + assertEq(obj, objB); + + /* + * Store another clone of the joined function object before obj.go has + * been read, but after it has been invoked via objB.go(objB). + * + * In this case, arguments.callee.caller must not lie and return what + * is currently stored in objB.go, since that function object (objA.go) + * was cloned earlier, when count was 1, and it is not the function + * object that was truly invoked. + * + * But since the invocation of objB.go(objB) did not clone go, and the + * following assignment overwrote the invoked value, leaving the only + * reference to the joined function object for go in the stack frame's + * callee (argv[-2]) member, the arguments.callee.caller reference must + * clone a function object for the callee, store it as the callee, and + * return it here. + * + * It won't equal obj.go, but (implementation detail) it should have + * the same proto as obj.go + */ + obj.go = objA.go; + + let caller = arguments.callee.caller; + let obj_go = obj.go; + return caller != obj_go && caller.__proto__ == obj_go.__proto__; + } + + case 4: { + assertEq(obj, objC); + + let save = obj.go; + delete obj.go; + return arguments.callee.caller == save; + } + + case 5: { + assertEq(obj, objD); + + let read = obj.go; + break; + } + } + + return arguments.callee.caller == obj.go; +} + +function make() { + return { + go: function(obj) { + return testCaller(obj); + } + }; +} + +var objA = make(), + objB = make(), + objC = make(), + objD = make(); + +reportCompare(true, objA.go(objA), "1"); +reportCompare(true, objA.go(objA), "2"); +reportCompare(true, objB.go(objB), "3"); +reportCompare(true, objC.go(objC), "4"); +reportCompare(true, objD.go(objD), "5"); diff --git a/js/src/tests/js1_8_5/regress/regress-577648-2.js b/js/src/tests/js1_8_5/regress/regress-577648-2.js new file mode 100644 index 00000000000..d9ba9131630 --- /dev/null +++ b/js/src/tests/js1_8_5/regress/regress-577648-2.js @@ -0,0 +1,12 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/licenses/publicdomain/ + * Contributor: Jason Orendorff + */ + +var o = { f: function() { return o.g(); }, g: function() { return arguments.callee.caller; } }; +var c = o.f(); +var i = 'f'; +var d = o[i](); + +reportCompare(true, c === o.f && d === o.f(), ""); diff --git a/js/src/trace-test/tests/basic/bug582479.js b/js/src/trace-test/tests/basic/bug582479.js new file mode 100644 index 00000000000..4029dcf53ae --- /dev/null +++ b/js/src/trace-test/tests/basic/bug582479.js @@ -0,0 +1,5 @@ +for (b = 0; b < 2; b++) { + / / + (function(){}) +} + From 4cd8354fb65acb09020888c8122095f8d93bdf52 Mon Sep 17 00:00:00 2001 From: Robert Sayre Date: Sun, 1 Aug 2010 09:58:03 -0700 Subject: [PATCH 2/4] Reland: Bug 577648 - arguments.callee.caller does not work in FF 4 under certain circumstances. --- js/src/jsapi.cpp | 12 +- js/src/jsarray.cpp | 4 +- js/src/jsarray.h | 9 ++ js/src/jscntxt.cpp | 99 ++++++++------- js/src/jscntxt.h | 41 +++++-- js/src/jsdbgapi.cpp | 11 ++ js/src/jsdbgapi.h | 33 ++++- js/src/jsemit.cpp | 3 +- js/src/jsfun.cpp | 140 +++++++++++++++++----- js/src/jsfun.h | 44 +++++++ js/src/jsgc.cpp | 13 ++ js/src/jsinterp.cpp | 111 ++++++++++++++--- js/src/jsinterp.h | 25 +++- js/src/jsobj.cpp | 43 ++++++- js/src/jsobjinlines.h | 19 +++ js/src/jsopcode.cpp | 4 - js/src/jsparse.cpp | 9 +- js/src/jsscope.cpp | 2 +- js/src/jsscope.h | 4 +- js/src/jsscopeinlines.h | 34 +++++- js/src/jsscript.cpp | 14 ++- js/src/jsstr.cpp | 4 +- js/src/jsstr.h | 9 ++ js/src/jstracer.cpp | 49 +++++++- js/src/jsvalue.h | 2 +- js/src/tests/js1_5/Regress/jstests.list | 2 +- js/src/tests/js1_5/Scope/jstests.list | 2 +- js/src/tests/js1_8_5/regress/jstests.list | 2 + 28 files changed, 590 insertions(+), 154 deletions(-) diff --git a/js/src/jsapi.cpp b/js/src/jsapi.cpp index ff1d62e1a52..1084e6f0c6a 100644 --- a/js/src/jsapi.cpp +++ b/js/src/jsapi.cpp @@ -546,7 +546,7 @@ static JSBool js_NewRuntimeWasCalled = JS_FALSE; #endif JSRuntime::JSRuntime() - : gcChunkAllocator(&defaultGCChunkAllocator) + : gcChunkAllocator(&defaultGCChunkAllocator) { /* Initialize infallibly first, so we can goto bad and JS_DestroyRuntime. */ JS_INIT_CLIST(&contextList); @@ -557,6 +557,16 @@ JSRuntime::JSRuntime() bool JSRuntime::init(uint32 maxbytes) { +#ifdef DEBUG + functionMeterFilename = getenv("JS_FUNCTION_STATFILE"); + if (functionMeterFilename) { + if (!methodReadBarrierCountMap.init()) + return false; + if (!unjoinedFunctionCountMap.init()) + return false; + } +#endif + if (!(defaultCompartment = new JSCompartment(this)) || !defaultCompartment->init()) { return false; diff --git a/js/src/jsarray.cpp b/js/src/jsarray.cpp index e7869c98285..0b397eabc94 100644 --- a/js/src/jsarray.cpp +++ b/js/src/jsarray.cpp @@ -1778,8 +1778,8 @@ sort_compare_strings(void *arg, const void *a, const void *b, int *result) return JS_TRUE; } -static JSBool -array_sort(JSContext *cx, uintN argc, Value *vp) +JSBool +js::array_sort(JSContext *cx, uintN argc, Value *vp) { jsuint len, newlen, i, undefs; size_t elemsize; diff --git a/js/src/jsarray.h b/js/src/jsarray.h index 695f34f4f14..358b1fd4516 100644 --- a/js/src/jsarray.h +++ b/js/src/jsarray.h @@ -190,6 +190,15 @@ extern bool js_MergeSort(void *vec, size_t nel, size_t elsize, JSComparator cmp, void *arg, void *tmp, JSMergeSortElemType elemType); +/* + * The Array.prototype.sort fast-native entry point is exported for joined + * function optimization in js{interp,tracer}.cpp. + */ +namespace js { +extern JSBool +array_sort(JSContext *cx, uintN argc, js::Value *vp); +} + #ifdef DEBUG_ARRAYS extern JSBool js_ArrayInfo(JSContext *cx, JSObject *obj, uintN argc, js::Value *argv, js::Value *rval); diff --git a/js/src/jscntxt.cpp b/js/src/jscntxt.cpp index c2bd4c6b628..2e587fec9d8 100644 --- a/js/src/jscntxt.cpp +++ b/js/src/jscntxt.cpp @@ -959,76 +959,87 @@ private: FILE *mFile; }; -#ifdef JS_EVAL_CACHE_METERING static void DumpEvalCacheMeter(JSContext *cx) { - struct { - const char *name; - ptrdiff_t offset; - } table[] = { + if (const char *filename = getenv("JS_EVALCACHE_STATFILE")) { + struct { + const char *name; + ptrdiff_t offset; + } table[] = { #define frob(x) { #x, offsetof(JSEvalCacheMeter, x) } - EVAL_CACHE_METER_LIST(frob) + EVAL_CACHE_METER_LIST(frob) #undef frob - }; - JSEvalCacheMeter *ecm = &JS_THREAD_DATA(cx)->evalCacheMeter; + }; + JSEvalCacheMeter *ecm = &JS_THREAD_DATA(cx)->evalCacheMeter; - static JSAutoFile fp; - if (!fp) { - fp.open("/tmp/evalcache.stats", "w"); - if (!fp) + static JSAutoFile fp; + if (!fp && !fp.open(filename, "w")) return; - } - fprintf(fp, "eval cache meter (%p):\n", + fprintf(fp, "eval cache meter (%p):\n", #ifdef JS_THREADSAFE - (void *) cx->thread + (void *) cx->thread #else - (void *) cx->runtime + (void *) cx->runtime #endif - ); - for (uintN i = 0; i < JS_ARRAY_LENGTH(table); ++i) { - fprintf(fp, "%-8.8s %llu\n", - table[i].name, - (unsigned long long int) *(uint64 *)((uint8 *)ecm + table[i].offset)); + ); + for (uintN i = 0; i < JS_ARRAY_LENGTH(table); ++i) { + fprintf(fp, "%-8.8s %llu\n", + table[i].name, + (unsigned long long int) *(uint64 *)((uint8 *)ecm + table[i].offset)); + } + fprintf(fp, "hit ratio %g%%\n", ecm->hit * 100. / ecm->probe); + fprintf(fp, "avg steps %g\n", double(ecm->step) / ecm->probe); + fflush(fp); } - fprintf(fp, "hit ratio %g%%\n", ecm->hit * 100. / ecm->probe); - fprintf(fp, "avg steps %g\n", double(ecm->step) / ecm->probe); - fflush(fp); } # define DUMP_EVAL_CACHE_METER(cx) DumpEvalCacheMeter(cx) -#endif -#ifdef JS_FUNCTION_METERING +static void +DumpFunctionCountMap(const char *title, JSRuntime::FunctionCountMap &map, FILE *fp) +{ + fprintf(fp, "\n%s count map:\n", title); + + for (JSRuntime::FunctionCountMap::Range r = map.all(); !r.empty(); r.popFront()) { + JSFunction *fun = r.front().key; + int32 count = r.front().value; + + fprintf(fp, "%10d %s:%u\n", count, fun->u.i.script->filename, fun->u.i.script->lineno); + } +} + static void DumpFunctionMeter(JSContext *cx) { - struct { - const char *name; - ptrdiff_t offset; - } table[] = { + if (const char *filename = cx->runtime->functionMeterFilename) { + struct { + const char *name; + ptrdiff_t offset; + } table[] = { #define frob(x) { #x, offsetof(JSFunctionMeter, x) } - FUNCTION_KIND_METER_LIST(frob) + FUNCTION_KIND_METER_LIST(frob) #undef frob - }; - JSFunctionMeter *fm = &cx->runtime->functionMeter; + }; + JSFunctionMeter *fm = &cx->runtime->functionMeter; - static JSAutoFile fp; - if (!fp) { - fp.open("/tmp/function.stats", "a"); - if (!fp) + static JSAutoFile fp; + if (!fp && !fp.open(filename, "w")) return; - } - fprintf(fp, "function meter (%s):\n", cx->runtime->lastScriptFilename); - for (uintN i = 0; i < JS_ARRAY_LENGTH(table); ++i) { - fprintf(fp, "%-11.11s %d\n", - table[i].name, *(int32 *)((uint8 *)fm + table[i].offset)); + fprintf(fp, "function meter (%s):\n", cx->runtime->lastScriptFilename); + for (uintN i = 0; i < JS_ARRAY_LENGTH(table); ++i) + fprintf(fp, "%-19.19s %d\n", table[i].name, *(int32 *)((uint8 *)fm + table[i].offset)); + + DumpFunctionCountMap("method read barrier", cx->runtime->methodReadBarrierCountMap, fp); + DumpFunctionCountMap("unjoined function", cx->runtime->unjoinedFunctionCountMap, fp); + + putc('\n', fp); + fflush(fp); } - fflush(fp); } + # define DUMP_FUNCTION_METER(cx) DumpFunctionMeter(cx) -#endif #endif /* DEBUG && XP_UNIX */ diff --git a/js/src/jscntxt.h b/js/src/jscntxt.h index 1b721808af6..bf619086221 100644 --- a/js/src/jscntxt.h +++ b/js/src/jscntxt.h @@ -919,18 +919,13 @@ struct TraceMonitor { # define JS_ON_TRACE(cx) JS_FALSE #endif -#ifdef DEBUG_brendan -# define JS_EVAL_CACHE_METERING 1 -# define JS_FUNCTION_METERING 1 -#endif - /* Number of potentially reusable scriptsToGC to search for the eval cache. */ #ifndef JS_EVAL_CACHE_SHIFT # define JS_EVAL_CACHE_SHIFT 6 #endif #define JS_EVAL_CACHE_SIZE JS_BIT(JS_EVAL_CACHE_SHIFT) -#ifdef JS_EVAL_CACHE_METERING +#ifdef DEBUG # define EVAL_CACHE_METER_LIST(_) _(probe), _(hit), _(step), _(noscope) # define identity(x) x @@ -941,10 +936,14 @@ struct JSEvalCacheMeter { # undef identity #endif -#ifdef JS_FUNCTION_METERING +#ifdef DEBUG # define FUNCTION_KIND_METER_LIST(_) \ _(allfun), _(heavy), _(nofreeupvar), _(onlyfreevar), \ - _(display), _(flat), _(setupvar), _(badfunarg) + _(display), _(flat), _(setupvar), _(badfunarg), \ + _(joinedsetmethod), _(joinedinitmethod), \ + _(joinedreplace), _(joinedsort), _(joinedmodulepat), \ + _(mreadbarrier), _(mwritebarrier), _(mwslotbarrier), \ + _(unjoined) # define identity(x) x struct JSFunctionMeter { @@ -952,8 +951,13 @@ struct JSFunctionMeter { }; # undef identity + +# define JS_FUNCTION_METER(cx,x) JS_RUNTIME_METER((cx)->runtime, functionMeter.x) +#else +# define JS_FUNCTION_METER(cx,x) ((void)0) #endif + #define NATIVE_ITER_CACHE_LOG2 8 #define NATIVE_ITER_CACHE_MASK JS_BITMASK(NATIVE_ITER_CACHE_LOG2) #define NATIVE_ITER_CACHE_SIZE JS_BIT(NATIVE_ITER_CACHE_LOG2) @@ -999,7 +1003,7 @@ struct JSThreadData { /* Lock-free hashed lists of scripts created by eval to garbage-collect. */ JSScript *scriptsToGC[JS_EVAL_CACHE_SIZE]; -#ifdef JS_EVAL_CACHE_METERING +#ifdef DEBUG JSEvalCacheMeter evalCacheMeter; #endif @@ -1603,9 +1607,22 @@ struct JSRuntime { JSGCStats gcStats; #endif -#ifdef JS_FUNCTION_METERING +#ifdef DEBUG + /* + * If functionMeterFilename, set from an envariable in JSRuntime's ctor, is + * null, the remaining members in this ifdef'ed group are not initialized. + */ + const char *functionMeterFilename; JSFunctionMeter functionMeter; char lastScriptFilename[1024]; + + typedef js::HashMap, + js::SystemAllocPolicy> FunctionCountMap; + + FunctionCountMap methodReadBarrierCountMap; + FunctionCountMap unjoinedFunctionCountMap; #endif JSWrapObjectCallback wrapObjectCallback; @@ -1646,7 +1663,7 @@ struct JSRuntime { #define JS_TRACE_MONITOR(cx) (JS_THREAD_DATA(cx)->traceMonitor) #define JS_SCRIPTS_TO_GC(cx) (JS_THREAD_DATA(cx)->scriptsToGC) -#ifdef JS_EVAL_CACHE_METERING +#ifdef DEBUG # define EVAL_CACHE_METER(x) (JS_THREAD_DATA(cx)->evalCacheMeter.x++) #else # define EVAL_CACHE_METER(x) ((void) 0) @@ -2174,7 +2191,7 @@ JS_ALWAYS_INLINE jsbytecode * JSStackFrame::pc(JSContext *cx) const { JS_ASSERT(cx->containingSegment(this) != NULL); - return cx->fp == this ? cx->regs->pc : savedPC; + return (cx->fp == this) ? cx->regs->pc : savedPC; } /* diff --git a/js/src/jsdbgapi.cpp b/js/src/jsdbgapi.cpp index 7cf8c3bdade..9cbd9c8bdea 100644 --- a/js/src/jsdbgapi.cpp +++ b/js/src/jsdbgapi.cpp @@ -1270,6 +1270,17 @@ JS_GetFrameCalleeObject(JSContext *cx, JSStackFrame *fp) return fp->callee(); } +JS_PUBLIC_API(JSBool) +JS_GetValidFrameCalleeObject(JSContext *cx, JSStackFrame *fp, jsval *vp) +{ + Value v; + + if (!fp->getValidCalleeObject(cx, &v)) + return false; + *vp = Jsvalify(v); + return true; +} + JS_PUBLIC_API(JSBool) JS_IsDebuggerFrame(JSContext *cx, JSStackFrame *fp) { diff --git a/js/src/jsdbgapi.h b/js/src/jsdbgapi.h index 948fda46623..56416bba234 100644 --- a/js/src/jsdbgapi.h +++ b/js/src/jsdbgapi.h @@ -272,11 +272,42 @@ extern JS_PUBLIC_API(void) JS_SetFrameReturnValue(JSContext *cx, JSStackFrame *fp, jsval rval); /** - * Return fp's callee function object (fp->callee) if it has one. + * Return fp's callee function object (fp->callee) if it has one. Note that + * this API cannot fail. A null return means "no callee": fp is a global or + * eval-from-global frame, not a call frame. + * + * This API began life as an infallible getter, but now it can return either: + * + * 1. An optimized closure that was compiled assuming the function could not + * escape and be called from sites the compiler could not see. + * + * 2. A "joined function object", an optimization whereby SpiderMonkey avoids + * creating fresh function objects for every evaluation of a function + * expression that is used only once by a consumer that either promises to + * clone later when asked for the value or that cannot leak the value. + * + * Because Mozilla's Gecko embedding of SpiderMonkey (and no doubt other + * embeddings) calls this API in potentially performance-sensitive ways (e.g. + * in nsContentUtils::GetDocumentFromCaller), we are leaving this API alone. It + * may now return an unwrapped non-escaping optimized closure, or a joined + * function object. Such optimized objects may work well if called from the + * correct context, never mutated or compared for identity, etc. + * + * However, if you really need to get the same callee object that JS code would + * see, which means undoing the optimizations, where an undo attempt can fail, + * then use JS_GetValidFrameCalleeObject. */ extern JS_PUBLIC_API(JSObject *) JS_GetFrameCalleeObject(JSContext *cx, JSStackFrame *fp); +/** + * Return fp's callee function object after running the deferred closure + * cloning "method read barrier". This API can fail! If the frame has no + * callee, this API returns true with JSVAL_IS_VOID(*vp). + */ +extern JS_PUBLIC_API(JSBool) +JS_GetValidFrameCalleeObject(JSContext *cx, JSStackFrame *fp, jsval *vp); + /************************************************************************/ extern JS_PUBLIC_API(const char *) diff --git a/js/src/jsemit.cpp b/js/src/jsemit.cpp index 0010f8004a6..66dd640b547 100644 --- a/js/src/jsemit.cpp +++ b/js/src/jsemit.cpp @@ -6633,8 +6633,7 @@ js_EmitTree(JSContext *cx, JSCodeGenerator *cg, JSParseNode *pn) bool lambda = PN_OP(init) == JSOP_LAMBDA; if (lambda) ++methodInits; - if (op == JSOP_INITPROP && lambda && init->pn_funbox->joinable()) - { + if (op == JSOP_INITPROP && lambda && init->pn_funbox->joinable()) { op = JSOP_INITMETHOD; pn2->pn_op = uint8(op); } else { diff --git a/js/src/jsfun.cpp b/js/src/jsfun.cpp index b7ed5d092a4..9495db6008e 100644 --- a/js/src/jsfun.cpp +++ b/js/src/jsfun.cpp @@ -1290,6 +1290,102 @@ JS_PUBLIC_DATA(Class) js_CallClass = { JS_CLASS_TRACE(args_or_call_trace) }; +bool +JSStackFrame::getValidCalleeObject(JSContext *cx, Value *vp) +{ + if (!fun) { + *vp = argv ? argv[-2] : UndefinedValue(); + return true; + } + + /* + * See the equivalent condition in args_getProperty for ARGS_CALLEE, but + * note that here we do not want to throw, since this escape can happen via + * a foo.caller reference alone, without any debugger or indirect eval. And + * alas, it seems foo.caller is still used on the Web. + */ + if (fun->needsWrapper()) { + JSObject *wrapper = WrapEscapingClosure(cx, this, fun, fun); + if (!wrapper) + return false; + vp->setObject(*wrapper); + return true; + } + + JSObject *funobj = &calleeObject(); + vp->setObject(*funobj); + + /* + * Check for an escape attempt by a joined function object, which must go + * through the frame's |this| object's method read barrier for the method + * atom by which it was uniquely associated with a property. + */ + if (thisv.isObject()) { + JS_ASSERT(GET_FUNCTION_PRIVATE(cx, funobj) == fun); + + if (fun == funobj && fun->methodAtom()) { + JSObject *thisp = &thisv.toObject(); + JS_ASSERT(thisp->canHaveMethodBarrier()); + + JSScope *scope = thisp->scope(); + if (scope->hasMethodBarrier()) { + JSScopeProperty *sprop = scope->lookup(ATOM_TO_JSID(fun->methodAtom())); + + /* + * The method property might have been deleted while the method + * barrier scope flag stuck, so we must lookup and test here. + * + * Two cases follow: the method barrier was not crossed yet, so + * we cross it here; the method barrier *was* crossed, in which + * case we must fetch and validate the cloned (unjoined) funobj + * in the method property's slot. + * + * In either case we must allow for the method property to have + * been replaced, or its value to have been overwritten. + */ + if (sprop) { + if (sprop->isMethod() && &sprop->methodObject() == funobj) { + if (!scope->methodReadBarrier(cx, sprop, vp)) + return false; + setCalleeObject(vp->toObject()); + return true; + } + if (sprop->hasSlot()) { + Value v = thisp->getSlot(sprop->slot); + JSObject *clone; + + if (IsFunctionObject(v, &clone) && + GET_FUNCTION_PRIVATE(cx, clone) == fun && + clone->hasMethodObj(*thisp)) { + JS_ASSERT(clone != funobj); + *vp = v; + setCalleeObject(*clone); + return true; + } + } + } + + /* + * If control flows here, we can't find an already-existing + * clone (or force to exist a fresh clone) created via thisp's + * method read barrier, so we must clone fun and store it in + * fp's callee to avoid re-cloning upon repeated foo.caller + * access. It seems that there are no longer any properties + * referring to fun. + */ + funobj = CloneFunctionObject(cx, fun, fun->getParent()); + if (!funobj) + return false; + funobj->setMethodObj(*thisp); + setCalleeObject(*funobj); + return true; + } + } + } + + return true; +} + /* Generic function tinyids. */ enum { FUN_ARGUMENTS = -1, /* predefined arguments local variable */ @@ -1303,7 +1399,7 @@ static JSBool fun_getProperty(JSContext *cx, JSObject *obj, jsid id, Value *vp) { if (!JSID_IS_INT(id)) - return JS_TRUE; + return true; jsint slot = JSID_TO_INT(id); @@ -1331,10 +1427,10 @@ fun_getProperty(JSContext *cx, JSObject *obj, jsid id, Value *vp) while (!(fun = (JSFunction *) GetInstancePrivate(cx, obj, &js_FunctionClass, NULL))) { if (slot != FUN_LENGTH) - return JS_TRUE; + return true; obj = obj->getProto(); if (!obj) - return JS_TRUE; + return true; } /* Find fun's top-most activation record. */ @@ -1353,11 +1449,11 @@ fun_getProperty(JSContext *cx, JSObject *obj, jsid id, Value *vp) js_GetErrorMessage, NULL, JSMSG_DEPRECATED_USAGE, js_arguments_str)) { - return JS_FALSE; + return false; } if (fp) { if (!js_GetArgsValue(cx, fp, vp)) - return JS_FALSE; + return false; } else { vp->setNull(); } @@ -1374,30 +1470,12 @@ fun_getProperty(JSContext *cx, JSObject *obj, jsid id, Value *vp) break; case FUN_CALLER: - if (fp && fp->down && fp->down->fun) { - JSFunction *caller = fp->down->fun; - /* - * See equivalent condition in args_getProperty for ARGS_CALLEE, - * but here we do not want to throw, since this escape can happen - * via foo.caller alone, without any debugger or indirect eval. And - * it seems foo.caller is still used on the Web. - */ - if (caller->needsWrapper()) { - JSObject *wrapper = WrapEscapingClosure(cx, fp->down, FUN_OBJECT(caller), caller); - if (!wrapper) - return JS_FALSE; - vp->setObject(*wrapper); - return JS_TRUE; - } + vp->setNull(); + if (fp && fp->down && !fp->down->getValidCalleeObject(cx, vp)) + return false; - JS_ASSERT(fp->down->argv); - *vp = fp->down->calleeValue(); - } else { - vp->setNull(); - } - - /* Censor the caller if it is from another compartment. */ if (vp->isObject()) { + /* Censor the caller if it is from another compartment. */ if (vp->toObject().getCompartment(cx) != cx->compartment) vp->setNull(); } @@ -1410,7 +1488,7 @@ fun_getProperty(JSContext *cx, JSObject *obj, jsid id, Value *vp) break; } - return JS_TRUE; + return true; } struct LazyFunctionProp { @@ -1842,7 +1920,8 @@ JSFunction::countInterpretedReservedSlots() const */ JS_PUBLIC_DATA(Class) js_FunctionClass = { js_Function_str, - JSCLASS_HAS_PRIVATE | JSCLASS_NEW_RESOLVE | JSCLASS_HAS_RESERVED_SLOTS(2) | + JSCLASS_HAS_PRIVATE | JSCLASS_NEW_RESOLVE | + JSCLASS_HAS_RESERVED_SLOTS(JSObject::FUN_FIXED_RESERVED_SLOTS) | JSCLASS_MARK_IS_TRACE | JSCLASS_HAS_CACHED_PROTO(JSProto_Function), PropertyStub, /* addProperty */ PropertyStub, /* delProperty */ @@ -2182,8 +2261,7 @@ Function(JSContext *cx, JSObject *obj, uintN argc, Value *argv, Value *rval) * js_CheckContentSecurityPolicy is defined in jsobj.cpp */ if (!js_CheckContentSecurityPolicy(cx)) { - JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL, - JSMSG_CSP_BLOCKED_FUNCTION); + JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL, JSMSG_CSP_BLOCKED_FUNCTION); return JS_FALSE; } diff --git a/js/src/jsfun.h b/js/src/jsfun.h index 5ca4f91f801..e24b25651c2 100644 --- a/js/src/jsfun.h +++ b/js/src/jsfun.h @@ -45,6 +45,8 @@ #include "jsprvtd.h" #include "jspubtd.h" #include "jsobj.h" +#include "jsatom.h" +#include "jsstr.h" typedef struct JSLocalNameMap JSLocalNameMap; @@ -95,6 +97,10 @@ typedef union JSLocalNames { * can move to u.i.script->flags. For now we use function flag bits to minimize * pointer-chasing. */ +#define JSFUN_JOINABLE 0x0001 /* function is null closure that does not + appear to call itself via its own name + or arguments.callee */ + #define JSFUN_EXPR_CLOSURE 0x1000 /* expression closure: function(x) x*x */ #define JSFUN_TRCINFO 0x2000 /* when set, u.n.trcinfo is non-null, JSFunctionSpec::call points to a @@ -202,6 +208,44 @@ struct JSFunction : public JSObject bool mightEscape() const { return FUN_INTERPRETED(this) && (FUN_FLAT_CLOSURE(this) || u.i.nupvars == 0); } + + bool joinable() const { + return flags & JSFUN_JOINABLE; + } + + private: + /* + * js_FunctionClass reserves two slots, which are free in JSObject::fslots + * without requiring dslots allocation. Null closures that can be joined to + * a compiler-created function object use the first one to hold a mutable + * methodAtom() state variable, needed for correct foo.caller handling. + */ + enum { + METHOD_ATOM_SLOT = JSSLOT_FUN_METHOD_ATOM + }; + + public: + void setJoinable() { + JS_ASSERT(FUN_INTERPRETED(this)); + fslots[METHOD_ATOM_SLOT].setNull(); + flags |= JSFUN_JOINABLE; + } + + /* + * Method name imputed from property uniquely assigned to or initialized, + * where the function does not need to be cloned to carry a scope chain or + * flattened upvars. + */ + JSAtom *methodAtom() const { + return (joinable() && fslots[METHOD_ATOM_SLOT].isString()) + ? STRING_TO_ATOM(fslots[METHOD_ATOM_SLOT].toString()) + : NULL; + } + + void setMethodAtom(JSAtom *atom) { + JS_ASSERT(joinable()); + fslots[METHOD_ATOM_SLOT].setString(ATOM_TO_STRING(atom)); + } }; JS_STATIC_ASSERT(sizeof(JSFunction) % JS_GCTHING_ALIGN == 0); diff --git a/js/src/jsgc.cpp b/js/src/jsgc.cpp index 1f322a03529..03dfc2e1942 100644 --- a/js/src/jsgc.cpp +++ b/js/src/jsgc.cpp @@ -2564,6 +2564,19 @@ js_TraceRuntime(JSTracer *trc) */ if (rt->gcExtraRootsTraceOp) rt->gcExtraRootsTraceOp(trc, rt->gcExtraRootsData); + +#ifdef DEBUG + if (rt->functionMeterFilename) { + for (int k = 0; k < 2; k++) { + typedef JSRuntime::FunctionCountMap HM; + HM &h = (k == 0) ? rt->methodReadBarrierCountMap : rt->unjoinedFunctionCountMap; + for (HM::Range r = h.all(); !r.empty(); r.popFront()) { + JSFunction *fun = r.front().key; + JS_CALL_OBJECT_TRACER(trc, fun, "FunctionCountMap key"); + } + } + } +#endif } void diff --git a/js/src/jsinterp.cpp b/js/src/jsinterp.cpp index 73ff39e3f5e..edaedaa36aa 100644 --- a/js/src/jsinterp.cpp +++ b/js/src/jsinterp.cpp @@ -2684,15 +2684,15 @@ BEGIN_CASE(JSOP_STOP) == JSOP_CALL_LENGTH); TRACE_0(LeaveFrame); if (!TRACE_RECORDER(cx) && recursive) { - if (*(regs.pc + JSOP_CALL_LENGTH) == JSOP_TRACE) { + if (regs.pc[JSOP_CALL_LENGTH] == JSOP_TRACE) { regs.pc += JSOP_CALL_LENGTH; MONITOR_BRANCH(Record_LeaveFrame); op = (JSOp)*regs.pc; DO_OP(); } } - if (*(regs.pc + JSOP_CALL_LENGTH) == JSOP_TRACE || - *(regs.pc + JSOP_CALL_LENGTH) == JSOP_NOP) { + if (regs.pc[JSOP_CALL_LENGTH] == JSOP_TRACE || + regs.pc[JSOP_CALL_LENGTH] == JSOP_NOP) { JS_STATIC_ASSERT(JSOP_TRACE_LENGTH == JSOP_NOP_LENGTH); regs.pc += JSOP_CALL_LENGTH; len = JSOP_TRACE_LENGTH; @@ -4050,7 +4050,8 @@ BEGIN_CASE(JSOP_GETXPROP) jsid id = ATOM_TO_JSID(atom); if (JS_LIKELY(!aobj->getOps()->getProperty) ? !js_GetPropertyHelper(cx, obj, id, - fp->imacpc + (fp->imacpc || + regs.pc[JSOP_GETPROP_LENGTH + i] == JSOP_IFEQ) ? JSGET_CACHE_RESULT | JSGET_NO_METHOD_BARRIER : JSGET_CACHE_RESULT | JSGET_METHOD_BARRIER, &rval) @@ -5672,24 +5673,19 @@ BEGIN_CASE(JSOP_LAMBDA) parent = fp->scopeChain; if (obj->getParent() == parent) { - op = JSOp(regs.pc[JSOP_LAMBDA_LENGTH]); + jsbytecode *pc2 = regs.pc + JSOP_LAMBDA_LENGTH; + JSOp op2 = JSOp(*pc2); /* - * Optimize ({method: function () { ... }, ...}) and - * this.method = function () { ... }; bytecode sequences. + * Optimize var obj = {method: function () { ... }, ...}, + * this.method = function () { ... }; and other significant + * single-use-of-null-closure bytecode sequences. + * + * WARNING: code in TraceRecorder::record_JSOP_LAMBDA must + * match the optimization cases in the following code that + * break from the outer do-while(0). */ - if (op == JSOP_SETMETHOD) { -#ifdef DEBUG - JSOp op2 = JSOp(regs.pc[JSOP_LAMBDA_LENGTH + JSOP_SETMETHOD_LENGTH]); - JS_ASSERT(op2 == JSOP_POP || op2 == JSOP_POPV); -#endif - - const Value &lref = regs.sp[-1]; - if (lref.isObject() && - lref.toObject().getClass() == &js_ObjectClass) { - break; - } - } else if (op == JSOP_INITMETHOD) { + if (op2 == JSOP_INITMETHOD) { #ifdef DEBUG const Value &lref = regs.sp[-1]; JS_ASSERT(lref.isObject()); @@ -5697,9 +5693,86 @@ BEGIN_CASE(JSOP_LAMBDA) JS_ASSERT(obj2->getClass() == &js_ObjectClass); JS_ASSERT(obj2->scope()->object == obj2); #endif + + fun->setMethodAtom(script->getAtom(GET_FULL_INDEX(JSOP_LAMBDA_LENGTH))); + JS_FUNCTION_METER(cx, joinedinitmethod); break; } + + if (op2 == JSOP_SETMETHOD) { +#ifdef DEBUG + op2 = JSOp(pc2[JSOP_SETMETHOD_LENGTH]); + JS_ASSERT(op2 == JSOP_POP || op2 == JSOP_POPV); +#endif + + const Value &lref = regs.sp[-1]; + if (lref.isObject() && lref.toObject().canHaveMethodBarrier()) { + fun->setMethodAtom(script->getAtom(GET_FULL_INDEX(JSOP_LAMBDA_LENGTH))); + JS_FUNCTION_METER(cx, joinedsetmethod); + break; + } + } else if (fun->joinable()) { + if (op2 == JSOP_CALL) { + /* + * Array.prototype.sort and String.prototype.replace are + * optimized as if they are special form. We know that they + * won't leak the joined function object in obj, therefore + * we don't need to clone that compiler- created function + * object for identity/mutation reasons. + */ + int iargc = GET_ARGC(pc2); + + /* + * Note that we have not yet pushed obj as the final argument, + * so regs.sp[1 - (iargc + 2)], and not regs.sp[-(iargc + 2)], + * is the callee for this JSOP_CALL. + */ + const Value &cref = regs.sp[1 - (iargc + 2)]; + JSObject *callee; + + if (IsFunctionObject(cref, &callee)) { + JSFunction *calleeFun = GET_FUNCTION_PRIVATE(cx, callee); + FastNative fastNative = FUN_FAST_NATIVE(calleeFun); + + if (fastNative) { + if (iargc == 1 && fastNative == array_sort) { + JS_FUNCTION_METER(cx, joinedsort); + break; + } + if (iargc == 2 && fastNative == str_replace) { + JS_FUNCTION_METER(cx, joinedreplace); + break; + } + } + } + } else if (op2 == JSOP_NULL) { + pc2 += JSOP_NULL_LENGTH; + op2 = JSOp(*pc2); + + if (op2 == JSOP_CALL && GET_ARGC(pc2) == 0) { + JS_FUNCTION_METER(cx, joinedmodulepat); + break; + } + } + } } + +#ifdef DEBUG + if (rt->functionMeterFilename) { + // No locking, this is mainly for js shell testing. + ++rt->functionMeter.unjoined; + + typedef JSRuntime::FunctionCountMap HM; + HM &h = rt->unjoinedFunctionCountMap; + HM::AddPtr p = h.lookupForAdd(fun); + if (!p) { + h.add(p, fun, 1); + } else { + JS_ASSERT(p->key == fun); + ++p->value; + } + } +#endif } else { parent = js_GetScopeChain(cx, fp); if (!parent) diff --git a/js/src/jsinterp.h b/js/src/jsinterp.h index e954d9b6863..47ba9b6c206 100644 --- a/js/src/jsinterp.h +++ b/js/src/jsinterp.h @@ -181,14 +181,33 @@ struct JSStackFrame return argv[-2]; } + /* Infallible getter to return the callee object from this frame. */ + JSObject &calleeObject() const { + JS_ASSERT(argv); + return argv[-2].toObject(); + } + + /* + * Fallible getter to compute the correct callee function object, which may + * require deferred cloning due to JSScope::methodReadBarrier. For a frame + * with null fun member, return true with *vp set from this->calleeValue(), + * which may not be an object (it could be undefined). + */ + bool getValidCalleeObject(JSContext *cx, js::Value *vp); + + void setCalleeObject(JSObject &callable) { + JS_ASSERT(argv); + argv[-2].setObject(callable); + } + JSObject *callee() { return argv ? &argv[-2].toObject() : NULL; } /* - * Get the object associated with the Execution Context's - * VariableEnvironment (ES5 10.3). The given CallStackSegment must contain - * this stack frame. + * Get the "variable object" (ES3 term) associated with the Execution + * Context's VariableEnvironment (ES5 10.3). The given CallStackSegment + * must contain this stack frame. */ JSObject *varobj(js::CallStackSegment *css) const; diff --git a/js/src/jsobj.cpp b/js/src/jsobj.cpp index b2800405eec..c3e3d52ae7c 100644 --- a/js/src/jsobj.cpp +++ b/js/src/jsobj.cpp @@ -5079,13 +5079,13 @@ js_SetPropertyHelper(JSContext *cx, JSObject *obj, jsid id, uintN defineHow, * Check for Object class here to avoid defining a method on a class * with magic resolve, addProperty, getProperty, etc. hooks. */ - if ((defineHow & JSDNP_SET_METHOD) && - obj->getClass() == &js_ObjectClass) { + if ((defineHow & JSDNP_SET_METHOD) && obj->canHaveMethodBarrier()) { JS_ASSERT(IsFunctionObject(*vp)); JS_ASSERT(!(attrs & (JSPROP_GETTER | JSPROP_SETTER))); JSObject *funobj = &vp->toObject(); - if (FUN_OBJECT(GET_FUNCTION_PRIVATE(cx, funobj)) == funobj) { + JSFunction *fun = GET_FUNCTION_PRIVATE(cx, funobj); + if (fun == funobj) { flags |= JSScopeProperty::METHOD; getter = CastAsPropertyOp(funobj); } @@ -5236,8 +5236,39 @@ js_DeleteProperty(JSContext *cx, JSObject *obj, jsid id, Value *rval) } scope = obj->scope(); - if (SPROP_HAS_VALID_SLOT(sprop, scope)) - GC_POKE(cx, obj->lockedGetSlot(sprop->slot)); + if (SPROP_HAS_VALID_SLOT(sprop, scope)) { + const Value &v = obj->lockedGetSlot(sprop->slot); + GC_POKE(cx, v); + + /* + * Delete is rare enough that we can take the hit of checking for an + * active cloned method function object that must be homed to a callee + * slot on the active stack frame before this delete completes, in case + * someone saved the clone and checks it against foo.caller for a foo + * called from the active method. + * + * We do not check suspended frames. They can't be reached via caller, + * so the only way they could have the method's joined function object + * as callee is through an API abusage. We break any such edge case. + */ + if (scope->hasMethodBarrier()) { + JSObject *funobj; + + if (IsFunctionObject(v, &funobj)) { + JSFunction *fun = GET_FUNCTION_PRIVATE(cx, funobj); + + if (fun != funobj) { + for (JSStackFrame *fp = cx->fp; fp; fp = fp->down) { + if (fp->callee() == fun && + fp->thisv.isObject() && + &fp->thisv.toObject() == obj) { + fp->setCalleeObject(*funobj); + } + } + } + } + } + } ok = scope->removeProperty(cx, id); JS_UNLOCK_OBJ(cx, obj); @@ -6153,7 +6184,7 @@ dumpValue(const Value &v) Class *clasp = obj->getClass(); fprintf(stderr, "<%s%s at %p>", clasp->name, - clasp == &js_ObjectClass ? "" : " object", + (clasp == &js_ObjectClass) ? "" : " object", (void *) obj); } else if (v.isBoolean()) { if (v.toBoolean()) diff --git a/js/src/jsobjinlines.h b/js/src/jsobjinlines.h index 19f3dbd0672..1544797b888 100644 --- a/js/src/jsobjinlines.h +++ b/js/src/jsobjinlines.h @@ -106,6 +106,12 @@ JSObject::getReservedSlot(uintN index) const return (slot < numSlots()) ? getSlot(slot) : js::UndefinedValue(); } +inline bool +JSObject::canHaveMethodBarrier() const +{ + return isObject() || isFunction() || isPrimitive() || isDate(); +} + inline bool JSObject::isPrimitive() const { @@ -308,6 +314,19 @@ JSObject::setDateUTCTime(const js::Value &time) fslots[JSSLOT_DATE_UTC_TIME] = time; } +inline bool +JSObject::hasMethodObj(const JSObject& obj) const +{ + return fslots[JSSLOT_FUN_METHOD_OBJ].isObject() && + &fslots[JSSLOT_FUN_METHOD_OBJ].toObject() == &obj; +} + +inline void +JSObject::setMethodObj(JSObject& obj) +{ + fslots[JSSLOT_FUN_METHOD_OBJ].setObject(obj); +} + inline NativeIterator * JSObject::getNativeIterator() const { diff --git a/js/src/jsopcode.cpp b/js/src/jsopcode.cpp index d27e03522df..153be1dcb95 100644 --- a/js/src/jsopcode.cpp +++ b/js/src/jsopcode.cpp @@ -5495,10 +5495,6 @@ SimulateImacroCFG(JSContext *cx, JSScript *script, #undef LOCAL_ASSERT #define LOCAL_ASSERT(expr) LOCAL_ASSERT_RV(expr, -1); -static intN -ReconstructPCStack(JSContext *cx, JSScript *script, jsbytecode *target, - jsbytecode **pcstack); - static intN ReconstructImacroPCStack(JSContext *cx, JSScript *script, jsbytecode *imacstart, jsbytecode *target, diff --git a/js/src/jsparse.cpp b/js/src/jsparse.cpp index 2bbeaaf7878..69b8e352586 100644 --- a/js/src/jsparse.cpp +++ b/js/src/jsparse.cpp @@ -2130,11 +2130,7 @@ DeoptimizeUsesWithin(JSDefinition *dn, JSFunctionBox *funbox, uint32& tcflags) void Parser::setFunctionKinds(JSFunctionBox *funbox, uint32& tcflags) { -#ifdef JS_FUNCTION_METERING -# define FUN_METER(x) JS_RUNTIME_METER(context->runtime, functionMeter.x) -#else -# define FUN_METER(x) ((void)0) -#endif +#define FUN_METER(x) JS_FUNCTION_METER(context, x) for (;;) { JSParseNode *fn = funbox->node; @@ -2344,6 +2340,9 @@ Parser::setFunctionKinds(JSFunctionBox *funbox, uint32& tcflags) } } + if (funbox->joinable()) + fun->setJoinable(); + funbox = funbox->siblings; if (!funbox) break; diff --git a/js/src/jsscope.cpp b/js/src/jsscope.cpp index 31537abae85..76b442e1600 100644 --- a/js/src/jsscope.cpp +++ b/js/src/jsscope.cpp @@ -1211,7 +1211,7 @@ JSScope::methodShapeChange(JSContext *cx, JSScopeProperty *sprop) const Value &prev = object->lockedGetSlot(sprop->slot); JS_ASSERT(&sprop->methodObject() == &prev.toObject()); JS_ASSERT(hasMethodBarrier()); - JS_ASSERT(object->getClass() == &js_ObjectClass); + JS_ASSERT(object->canHaveMethodBarrier()); JS_ASSERT(!sprop->rawSetter || sprop->rawSetter == js_watch_set); #endif diff --git a/js/src/jsscope.h b/js/src/jsscope.h index f8a2ff4c61e..6988d007ade 100644 --- a/js/src/jsscope.h +++ b/js/src/jsscope.h @@ -611,7 +611,9 @@ struct JSScopeProperty { union { js::PropertyOp rawGetter; /* getter and setter hooks or objects */ JSObject *getterObj; /* user-defined callable "get" object or - null if sprop->hasGetterValue() */ + null if sprop->hasGetterValue(); or + joined function object if METHOD flag + is set. */ JSScopeProperty *next; /* next node in freelist */ }; diff --git a/js/src/jsscopeinlines.h b/js/src/jsscopeinlines.h index 703ab71a5a3..3a914020f1c 100644 --- a/js/src/jsscopeinlines.h +++ b/js/src/jsscopeinlines.h @@ -122,17 +122,37 @@ JSScope::methodReadBarrier(JSContext *cx, JSScopeProperty *sprop, js::Value *vp) JS_ASSERT(hasProperty(sprop)); JS_ASSERT(sprop->isMethod()); JS_ASSERT(&vp->toObject() == &sprop->methodObject()); - JS_ASSERT(object->getClass() == &js_ObjectClass); + JS_ASSERT(object->canHaveMethodBarrier()); JSObject *funobj = &vp->toObject(); JSFunction *fun = GET_FUNCTION_PRIVATE(cx, funobj); - JS_ASSERT(FUN_OBJECT(fun) == funobj && FUN_NULL_CLOSURE(fun)); + JS_ASSERT(fun == funobj && FUN_NULL_CLOSURE(fun)); funobj = CloneFunctionObject(cx, fun, funobj->getParent()); if (!funobj) return false; + funobj->setMethodObj(*object); + vp->setObject(*funobj); - return !!js_SetPropertyHelper(cx, object, sprop->id, 0, vp); + if (!js_SetPropertyHelper(cx, object, sprop->id, 0, vp)) + return false; + +#ifdef DEBUG + if (cx->runtime->functionMeterFilename) { + JS_FUNCTION_METER(cx, mreadbarrier); + + typedef JSRuntime::FunctionCountMap HM; + HM &h = cx->runtime->methodReadBarrierCountMap; + HM::AddPtr p = h.lookupForAdd(fun); + if (!p) { + h.add(p, fun, 1); + } else { + JS_ASSERT(p->key == fun); + ++p->value; + } + } +#endif + return true; } static JS_ALWAYS_INLINE bool @@ -149,8 +169,10 @@ JSScope::methodWriteBarrier(JSContext *cx, JSScopeProperty *sprop, { if (flags & (BRANDED | METHOD_BARRIER)) { const js::Value &prev = object->lockedGetSlot(sprop->slot); - if (ChangesMethodValue(prev, v)) + if (ChangesMethodValue(prev, v)) { + JS_FUNCTION_METER(cx, mwritebarrier); return methodShapeChange(cx, sprop); + } } return true; } @@ -160,8 +182,10 @@ JSScope::methodWriteBarrier(JSContext *cx, uint32 slot, const js::Value &v) { if (flags & (BRANDED | METHOD_BARRIER)) { const js::Value &prev = object->lockedGetSlot(slot); - if (ChangesMethodValue(prev, v)) + if (ChangesMethodValue(prev, v)) { + JS_FUNCTION_METER(cx, mwslotbarrier); return methodShapeChange(cx, slot); + } } return true; } diff --git a/js/src/jsscript.cpp b/js/src/jsscript.cpp index ecb6f07d86d..fac571ba0a1 100644 --- a/js/src/jsscript.cpp +++ b/js/src/jsscript.cpp @@ -617,12 +617,14 @@ SaveScriptFilename(JSRuntime *rt, const char *filename, uint32 flags) sfp->flags |= flags; } -#ifdef JS_FUNCTION_METERING - size_t len = strlen(sfe->filename); - if (len >= sizeof rt->lastScriptFilename) - len = sizeof rt->lastScriptFilename - 1; - memcpy(rt->lastScriptFilename, sfe->filename, len); - rt->lastScriptFilename[len] = '\0'; +#ifdef DEBUG + if (rt->functionMeterFilename) { + size_t len = strlen(sfe->filename); + if (len >= sizeof rt->lastScriptFilename) + len = sizeof rt->lastScriptFilename - 1; + memcpy(rt->lastScriptFilename, sfe->filename, len); + rt->lastScriptFilename[len] = '\0'; + } #endif return sfe; diff --git a/js/src/jsstr.cpp b/js/src/jsstr.cpp index 2e6e168bd9b..4a14e8c0ac3 100644 --- a/js/src/jsstr.cpp +++ b/js/src/jsstr.cpp @@ -2277,8 +2277,8 @@ BuildFlatReplacement(JSContext *cx, JSString *textstr, JSString *repstr, return true; } -static JSBool -str_replace(JSContext *cx, uintN argc, Value *vp) +JSBool +js::str_replace(JSContext *cx, uintN argc, Value *vp) { ReplaceData rdata(cx); NORMALIZE_THIS(cx, vp, rdata.str); diff --git a/js/src/jsstr.h b/js/src/jsstr.h index f2022afa08b..a54a3b7d34d 100644 --- a/js/src/jsstr.h +++ b/js/src/jsstr.h @@ -1113,6 +1113,15 @@ extern JSBool js_str_escape(JSContext *cx, JSObject *obj, uintN argc, js::Value *argv, js::Value *rval); +/* + * The String.prototype.replace fast-native entry point is exported for joined + * function optimization in js{interp,tracer}.cpp. + */ +namespace js { +extern JSBool +str_replace(JSContext *cx, uintN argc, js::Value *vp); +} + extern JSBool js_str_toString(JSContext *cx, uintN argc, js::Value *vp); diff --git a/js/src/jstracer.cpp b/js/src/jstracer.cpp index b1b5ba555f1..a74ff8592d7 100644 --- a/js/src/jstracer.cpp +++ b/js/src/jstracer.cpp @@ -14873,19 +14873,56 @@ TraceRecorder::record_JSOP_LAMBDA() if (FUN_NULL_CLOSURE(fun)) { if (FUN_OBJECT(fun)->getParent() != globalObj) RETURN_STOP_A("Null closure function object parent must be global object"); - JSOp op2 = JSOp(cx->regs->pc[JSOP_LAMBDA_LENGTH]); + + jsbytecode *pc2 = cx->regs->pc + JSOP_LAMBDA_LENGTH; + JSOp op2 = JSOp(*pc2); + + if (op2 == JSOP_INITMETHOD) { + stack(0, INS_CONSTOBJ(FUN_OBJECT(fun))); + return ARECORD_CONTINUE; + } if (op2 == JSOP_SETMETHOD) { Value lval = stackval(-1); - if (!lval.isPrimitive() && - lval.toObject().getClass() == &js_ObjectClass) { + if (!lval.isPrimitive() && lval.toObject().canHaveMethodBarrier()) { stack(0, INS_CONSTOBJ(FUN_OBJECT(fun))); return ARECORD_CONTINUE; } - } else if (op2 == JSOP_INITMETHOD) { - stack(0, INS_CONSTOBJ(FUN_OBJECT(fun))); - return ARECORD_CONTINUE; + } else if (fun->joinable()) { + if (op2 == JSOP_CALL) { + /* + * Array.prototype.sort and String.prototype.replace are + * optimized as if they are special form. We know that they + * won't leak the joined function object in obj, therefore + * we don't need to clone that compiler- created function + * object for identity/mutation reasons. + */ + int iargc = GET_ARGC(pc2); + + /* + * Note that we have not yet pushed obj as the final argument, + * so regs.sp[1 - (iargc + 2)], and not regs.sp[-(iargc + 2)], + * is the callee for this JSOP_CALL. + */ + JSFunction *calleeFun = + GET_FUNCTION_PRIVATE(cx, &cx->regs->sp[1 - (iargc + 2)].toObject()); + FastNative fastNative = FUN_FAST_NATIVE(calleeFun); + + if ((iargc == 1 && fastNative == array_sort) || + (iargc == 2 && fastNative == str_replace)) { + stack(0, INS_CONSTOBJ(FUN_OBJECT(fun))); + return ARECORD_CONTINUE; + } + } else if (op2 == JSOP_NULL) { + pc2 += JSOP_NULL_LENGTH; + op2 = JSOp(*pc2); + + if (op2 == JSOP_CALL && GET_ARGC(pc2) == 0) { + stack(0, INS_CONSTOBJ(FUN_OBJECT(fun))); + return ARECORD_CONTINUE; + } + } } LIns *proto_ins; diff --git a/js/src/jsvalue.h b/js/src/jsvalue.h index 38309290032..3d975f57c80 100644 --- a/js/src/jsvalue.h +++ b/js/src/jsvalue.h @@ -598,7 +598,7 @@ class Value * * Private setters/getters allow the caller to read/write arbitrary types * that fit in the 64-bit payload. It is the caller's responsibility, after - * storing to a value with setPrivateX to only read with getPrivateX. + * storing to a value with setPrivateX to read only using getPrivateX. * Privates values are given a type type which ensures they are not marked. */ diff --git a/js/src/tests/js1_5/Regress/jstests.list b/js/src/tests/js1_5/Regress/jstests.list index bef21fe9488..bada3aecaa4 100644 --- a/js/src/tests/js1_5/Regress/jstests.list +++ b/js/src/tests/js1_5/Regress/jstests.list @@ -170,7 +170,7 @@ skip-if(xulRuntime.OS=="WINNT"&&isDebugBuild) script regress-341360.js # slow script regress-343713.js script regress-343966.js script regress-344711-n.js -random-if(!xulRuntime.shell&&xulRuntime.OS=="WINNT") asserts-if(!xulRuntime.shell&&xulRuntime.OS=="WINNT",1) script regress-344804.js # bug 524732 +skip script regress-344804.js # bug 524732 script regress-344959.js script regress-346237.js script regress-346801.js diff --git a/js/src/tests/js1_5/Scope/jstests.list b/js/src/tests/js1_5/Scope/jstests.list index 4a6044cf58e..af4f8784ce4 100644 --- a/js/src/tests/js1_5/Scope/jstests.list +++ b/js/src/tests/js1_5/Scope/jstests.list @@ -1,6 +1,6 @@ url-prefix ../../jsreftest.html?test=js1_5/Scope/ script regress-154693.js -random-if(!xulRuntime.shell&&xulRuntime.OS=="WINNT") asserts-if(!xulRuntime.shell&&xulRuntime.OS=="WINNT",1) script regress-181834.js # bug 524732 +skip script regress-181834.js # bug 524732 script regress-184107.js script regress-185485.js script regress-191276.js diff --git a/js/src/tests/js1_8_5/regress/jstests.list b/js/src/tests/js1_8_5/regress/jstests.list index 979a9dd2e04..181230e3045 100644 --- a/js/src/tests/js1_8_5/regress/jstests.list +++ b/js/src/tests/js1_8_5/regress/jstests.list @@ -24,4 +24,6 @@ script regress-566914.js script regress-567152.js script regress-569306.js script regress-571014.js +script regress-577648-1.js +script regress-577648-2.js script regress-583429.js From 4a6d82bcd2eb3778b64984a042e40c2577ff19d4 Mon Sep 17 00:00:00 2001 From: Robert Sayre Date: Sun, 1 Aug 2010 09:59:02 -0700 Subject: [PATCH 3/4] Reland Bug 582479 - TM: Assertion failure: (&cx->regs->sp[1 - (iargc + 2)].toObject())->isFunction(). --- js/src/jstracer.cpp | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/js/src/jstracer.cpp b/js/src/jstracer.cpp index a74ff8592d7..a2b0b639245 100644 --- a/js/src/jstracer.cpp +++ b/js/src/jstracer.cpp @@ -14905,14 +14905,18 @@ TraceRecorder::record_JSOP_LAMBDA() * so regs.sp[1 - (iargc + 2)], and not regs.sp[-(iargc + 2)], * is the callee for this JSOP_CALL. */ - JSFunction *calleeFun = - GET_FUNCTION_PRIVATE(cx, &cx->regs->sp[1 - (iargc + 2)].toObject()); - FastNative fastNative = FUN_FAST_NATIVE(calleeFun); + const Value &cref = cx->regs->sp[1 - (iargc + 2)]; + JSObject *callee; - if ((iargc == 1 && fastNative == array_sort) || - (iargc == 2 && fastNative == str_replace)) { - stack(0, INS_CONSTOBJ(FUN_OBJECT(fun))); - return ARECORD_CONTINUE; + if (IsFunctionObject(cref, &callee)) { + JSFunction *calleeFun = GET_FUNCTION_PRIVATE(cx, callee); + FastNative fastNative = FUN_FAST_NATIVE(calleeFun); + + if ((iargc == 1 && fastNative == array_sort) || + (iargc == 2 && fastNative == str_replace)) { + stack(0, INS_CONSTOBJ(FUN_OBJECT(fun))); + return ARECORD_CONTINUE; + } } } else if (op2 == JSOP_NULL) { pc2 += JSOP_NULL_LENGTH; From 2b561f9f3e1c2cd0cb7beb4a26ffbaa16d638b97 Mon Sep 17 00:00:00 2001 From: Shu-yu Guo Date: Sun, 1 Aug 2010 12:20:18 -0700 Subject: [PATCH 4/4] Bug 579230 - Rewrite narcissus expr parser, refactor into builder pattern. r=gal --- js/narcissus/jsdefs.js | 4 +- js/narcissus/jsexec.js | 28 +- js/narcissus/jslex.js | 71 +- js/narcissus/jsparse.js | 2474 +++++++++++++++++++++++++++++---------- 4 files changed, 1910 insertions(+), 667 deletions(-) diff --git a/js/narcissus/jsdefs.js b/js/narcissus/jsdefs.js index eeb7a3ccdf2..d2b44692ed5 100644 --- a/js/narcissus/jsdefs.js +++ b/js/narcissus/jsdefs.js @@ -72,7 +72,7 @@ var tokens = [ // Nonterminal tree node type codes. "SCRIPT", "BLOCK", "LABEL", "FOR_IN", "CALL", "NEW_WITH_ARGS", "INDEX", "ARRAY_INIT", "OBJECT_INIT", "PROPERTY_INIT", "GETTER", "SETTER", - "GROUP", "LIST", "LET_STM", "LET_EXP", "LET_DEF", + "GROUP", "LIST", "LET_BLOCK", "ARRAY_COMP", "GENERATOR", "COMP_TAIL", // Terminals. "IDENTIFIER", "NUMBER", "STRING", "REGEXP", @@ -81,7 +81,7 @@ var tokens = [ "break", "case", "catch", "const", "continue", "debugger", "default", "delete", "do", - "else", "enum", + "else", "false", "finally", "for", "function", "if", "in", "instanceof", "let", diff --git a/js/narcissus/jsexec.js b/js/narcissus/jsexec.js index a6ac99c4294..538f3a9021c 100644 --- a/js/narcissus/jsexec.js +++ b/js/narcissus/jsexec.js @@ -71,10 +71,27 @@ var global = { x2.scope = x.scope; ExecutionContext.current = x2; try { - execute(parse(s), x2); + execute(parse(new VanillaBuilder, s), x2); } catch (e if e == THROW) { x.result = x2.result; throw e; + } catch (e if e instanceof SyntaxError) { + x.result = e; + throw THROW; + } catch (e if e instanceof InternalError) { + /* + * If we get too much recursion during parsing we need to re-throw + * it as a narcissus THROW. + * + * See bug 152646. + */ + var re = /InternalError: (script stack space quota is exhausted|too much recursion)/; + if (re.test(e.toString())) { + x.result = e; + throw THROW; + } else { + throw e; + } } finally { ExecutionContext.current = x; } @@ -106,8 +123,9 @@ var global = { var t = new Tokenizer("anonymous(" + p + ") {" + b + "}"); // NB: Use the STATEMENT_FORM constant since we don't want to push this - // function onto the null compilation context. - var f = FunctionDefinition(t, null, false, STATEMENT_FORM); + // function onto the fake compilation context. + var x = { builder: new VanillaBuilder }; + var f = FunctionDefinition(t, x, false, STATEMENT_FORM); var s = {object: global, parent: null}; return newFunction(f,{scope:s}); }, @@ -482,7 +500,7 @@ function execute(n, x) { case ASSIGN: r = execute(n[0], x); - t = n[0].assignOp; + t = n.assignOp; if (t) u = getValue(r); v = getValue(execute(n[1], x)); @@ -1007,7 +1025,7 @@ function evaluate(s, f, l) { var x2 = new ExecutionContext(GLOBAL_CODE); ExecutionContext.current = x2; try { - execute(parse(s, f, l), x2); + execute(parse(new VanillaBuilder, s, f, l), x2); } catch (e if e == THROW) { if (x) { x.result = x2.result; diff --git a/js/narcissus/jslex.js b/js/narcissus/jslex.js index 89944ba8789..a6783c69181 100644 --- a/js/narcissus/jslex.js +++ b/js/narcissus/jslex.js @@ -1,4 +1,4 @@ -/* vim: set sw=4 ts=8 et tw=78: */ +/* vim: set sw=4 ts=4 et tw=78: */ /* ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * @@ -57,7 +57,9 @@ for (var op in opTypeNames) { } } -// file ptr, path to file, line number -> Tokenizer +/* + * Tokenizer :: (file ptr, path, line number) -> Tokenizer + */ function Tokenizer(s, f, l) { this.cursor = 0; this.source = String(s); @@ -65,22 +67,23 @@ function Tokenizer(s, f, l) { this.tokenIndex = 0; this.lookahead = 0; this.scanNewlines = false; - this.scanOperand = true; this.filename = f || ""; this.lineno = l || 1; } Tokenizer.prototype = { get done() { - return this.peek() == END; + // We need to set scanOperand to true here because the first thing + // might be a regexp. + return this.peek(true) == END; }, get token() { return this.tokens[this.tokenIndex]; }, - match: function (tt) { - return this.get() == tt || this.unget(); + match: function (tt, scanOperand) { + return this.get(scanOperand) == tt || this.unget(); }, mustMatch: function (tt) { @@ -89,7 +92,7 @@ Tokenizer.prototype = { return this.token; }, - peek: function () { + peek: function (scanOperand) { var tt, next; if (this.lookahead) { next = this.tokens[(this.tokenIndex + this.lookahead) & 3]; @@ -97,15 +100,15 @@ Tokenizer.prototype = { ? NEWLINE : next.type; } else { - tt = this.get(); + tt = this.get(scanOperand); this.unget(); } return tt; }, - peekOnSameLine: function () { + peekOnSameLine: function (scanOperand) { this.scanNewlines = true; - var tt = this.peek(); + var tt = this.peek(scanOperand); this.scanNewlines = false; return tt; }, @@ -334,13 +337,6 @@ Tokenizer.prototype = { op += '='; } else { token.type = tokenIds[opTypeNames[op]]; - if (this.scanOperand) { - switch (token.type) { - case PLUS: token.type = UNARY_PLUS; break; - case MINUS: token.type = UNARY_MINUS; break; - } - } - token.assignOp = null; } @@ -364,10 +360,13 @@ Tokenizer.prototype = { token.value = id; }, - // void -> token type - // It consumes input *only* if there is no lookahead. - // Dispatch to the appropriate lexing function depending on the input. - get: function () { + /* + * Tokenizer.get :: void -> token type + * + * Consumes input *only* if there is no lookahead. + * Dispatch to the appropriate lexing function depending on the input. + */ + get: function (scanOperand) { var token; while (this.lookahead) { --this.lookahead; @@ -395,7 +394,7 @@ Tokenizer.prototype = { if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch === '$' || ch === '_') { this.lexIdent(ch); - } else if (this.scanOperand && ch === '/') { + } else if (scanOperand && ch === '/') { this.lexRegExp(ch); } else if (ch in opTokens) { this.lexOp(ch); @@ -419,8 +418,11 @@ Tokenizer.prototype = { return token.type; }, - // void -> undefined - // match depends on unget returning undefined. + /* + * Tokenizer.unget :: void -> undefined + * + * Match depends on unget returning undefined. + */ unget: function () { if (++this.lookahead == 4) throw "PANIC: too much lookahead!"; this.tokenIndex = (this.tokenIndex - 1) & 3; @@ -431,6 +433,25 @@ Tokenizer.prototype = { e.source = this.source; e.cursor = this.cursor; return e; + }, + + save: function () { + return { + cursor: this.cursor, + tokenIndex: this.tokenIndex, + tokens: this.tokens.slice(), + lookahead: this.lookahead, + scanNewlines: this.scanNewlines, + lineno: this.lineno + }; + }, + + rewind: function(point) { + this.cursor = point.cursor; + this.tokenIndex = point.tokenIndex; + this.tokens = point.tokens.slice(); + this.lookahead = point.lookahead; + this.scanNewline = point.scanNewline; + this.lineno = point.lineno; } }; - diff --git a/js/narcissus/jsparse.js b/js/narcissus/jsparse.js index cdbabd4c38a..1112a4927cd 100644 --- a/js/narcissus/jsparse.js +++ b/js/narcissus/jsparse.js @@ -1,5 +1,5 @@ /* -*- Mode: JS; tab-width: 4; indent-tabs-mode: nil; -*- - * vim: set sw=4 ts=8 et tw=78: + * vim: set sw=4 ts=4 et tw=78: * ***** BEGIN LICENSE BLOCK ***** * * Version: MPL 1.1/GPL 2.0/LGPL 2.1 @@ -43,15 +43,758 @@ * Parser. */ -// boolean -> undefined -// inFunction is used to check if a return stm appears in a valid context. -function CompilerContext(inFunction) { +/* + * The vanilla AST builder. + */ + +function VanillaBuilder() { +} + +VanillaBuilder.prototype = { + IF$build: function(t) { + return new Node(t, IF); + }, + + IF$setCondition: function(n, e) { + n.condition = e; + }, + + IF$setThenPart: function(n, s) { + n.thenPart = s; + }, + + IF$setElsePart: function(n, s) { + n.elsePart = s; + }, + + IF$finish: function(n) { + }, + + SWITCH$build: function(t) { + var n = new Node(t, SWITCH); + n.cases = []; + n.defaultIndex = -1; + return n; + }, + + SWITCH$setDiscriminant: function(n, e) { + n.discriminant = e; + }, + + SWITCH$setDefaultIndex: function(n, i) { + n.defaultIndex = i; + }, + + SWITCH$addCase: function(n, n2) { + n.cases.push(n2); + }, + + SWITCH$finish: function(n) { + }, + + CASE$build: function(t) { + return new Node(t, CASE); + }, + + CASE$setLabel: function(n, e) { + n.caseLabel = e; + }, + + CASE$initializeStatements: function(n, t) { + n.statements = new Node(t, BLOCK); + }, + + CASE$addStatement: function(n, s) { + n.statements.push(s); + }, + + CASE$finish: function(n) { + }, + + DEFAULT$build: function(t, p) { + return new Node(t, DEFAULT); + }, + + DEFAULT$initializeStatements: function(n, t) { + n.statements = new Node(t, BLOCK); + }, + + DEFAULT$addStatement: function(n, s) { + n.statements.push(s); + }, + + DEFAULT$finish: function(n) { + }, + + FOR$build: function(t) { + var n = new Node(t, FOR); + n.isLoop = true; + n.isEach = false; + return n; + }, + + FOR$rebuildForEach: function(n) { + n.isEach = true; + }, + + // NB. This function is called after rebuildForEach, if that's called + // at all. + FOR$rebuildForIn: function(n) { + n.type = FOR_IN; + }, + + FOR$setCondition: function(n, e) { + n.condition = e; + }, + + FOR$setSetup: function(n, e) { + n.setup = e || null; + }, + + FOR$setUpdate: function(n, e) { + n.update = e; + }, + + FOR$setObject: function(n, e) { + n.object = e; + }, + + FOR$setIterator: function(n, e, e2) { + n.iterator = e; + n.varDecl = e2; + }, + + FOR$setBody: function(n, s) { + n.body = s; + }, + + FOR$finish: function(n) { + }, + + WHILE$build: function(t) { + var n = new Node(t, WHILE); + n.isLoop = true; + return n; + }, + + WHILE$setCondition: function(n, e) { + n.condition = e; + }, + + WHILE$setBody: function(n, s) { + n.body = s; + }, + + WHILE$finish: function(n) { + }, + + DO$build: function(t) { + var n = new Node(t, DO); + n.isLoop = true; + return n; + }, + + DO$setCondition: function(n, e) { + n.condition = e; + }, + + DO$setBody: function(n, s) { + n.body = s; + }, + + DO$finish: function(n) { + }, + + BREAK$build: function(t) { + return new Node(t, BREAK); + }, + + BREAK$setLabel: function(n, v) { + n.label = v; + }, + + BREAK$setTarget: function(n, n2) { + n.target = n2; + }, + + BREAK$finish: function(n) { + }, + + CONTINUE$build: function(t) { + return new Node(t, CONTINUE); + }, + + CONTINUE$setLabel: function(n, v) { + n.label = v; + }, + + CONTINUE$setTarget: function(n, n2) { + n.target = n2; + }, + + CONTINUE$finish: function(n) { + }, + + TRY$build: function(t) { + var n = new Node(t, TRY); + n.catchClauses = []; + return n; + }, + + TRY$setTryBlock: function(n, s) { + n.tryBlock = s; + }, + + TRY$addCatch: function(n, n2) { + n.catchClauses.push(n2); + }, + + TRY$finishCatches: function(n) { + }, + + TRY$setFinallyBlock: function(n, s) { + n.finallyBlock = s; + }, + + TRY$finish: function(n) { + }, + + CATCH$build: function(t) { + var n = new Node(t, CATCH); + n.guard = null; + return n; + }, + + CATCH$setVarName: function(n, v) { + n.varName = v; + }, + + CATCH$setGuard: function(n, e) { + n.guard = e; + }, + + CATCH$setBlock: function(n, s) { + n.block = s; + }, + + CATCH$finish: function(n) { + }, + + THROW$build: function(t) { + return new Node(t, THROW); + }, + + THROW$setException: function(n, e) { + n.exception = e; + }, + + THROW$finish: function(n) { + }, + + RETURN$build: function(t) { + return new Node(t, RETURN); + }, + + RETURN$setValue: function(n, e) { + n.value = e; + }, + + RETURN$finish: function(n) { + }, + + YIELD$build: function(t) { + return new Node(t, YIELD); + }, + + YIELD$setValue: function(n, e) { + n.value = e; + }, + + YIELD$finish: function(n) { + }, + + GENERATOR$build: function(t) { + return new Node(t, GENERATOR); + }, + + GENERATOR$setExpression: function(n, e) { + n.expression = e; + }, + + GENERATOR$setTail: function(n, n2) { + n.tail = n2; + }, + + GENERATOR$finish: function(n) { + }, + + WITH$build: function(t) { + return new Node(t, WITH); + }, + + WITH$setObject: function(n, e) { + n.object = e; + }, + + WITH$setBody: function(n, s) { + n.body = s; + }, + + WITH$finish: function(n) { + }, + + DEBUGGER$build: function(t) { + return new Node(t, DEBUGGER); + }, + + SEMICOLON$build: function(t) { + return new Node(t, SEMICOLON); + }, + + SEMICOLON$setExpression: function(n, e) { + n.expression = e; + }, + + SEMICOLON$finish: function(n) { + }, + + LABEL$build: function(t) { + return new Node(t, LABEL); + }, + + LABEL$setLabel: function(n, e) { + n.label = e; + }, + + LABEL$setStatement: function(n, s) { + n.statement = s; + }, + + LABEL$finish: function(n) { + }, + + FUNCTION$build: function(t) { + var n = new Node(t); + if (n.type != FUNCTION) + n.type = (n.value == "get") ? GETTER : SETTER; + n.params = []; + return n; + }, + + FUNCTION$setName: function(n, v) { + n.name = v; + }, + + FUNCTION$addParam: function(n, v) { + n.params.push(v); + }, + + FUNCTION$setBody: function(n, s) { + n.body = s; + }, + + FUNCTION$hoistVars: function(x) { + }, + + FUNCTION$finish: function(n, x) { + }, + + VAR$build: function(t) { + return new Node(t, VAR); + }, + + VAR$addDecl: function(n, n2, x) { + n.push(n2); + }, + + VAR$finish: function(n) { + }, + + CONST$build: function(t) { + return new Node(t, VAR); + }, + + CONST$addDecl: function(n, n2, x) { + n.push(n2); + }, + + CONST$finish: function(n) { + }, + + LET$build: function(t) { + return new Node(t, LET); + }, + + LET$addDecl: function(n, n2, x) { + n.push(n2); + }, + + LET$finish: function(n) { + }, + + DECL$build: function(t) { + return new Node(t, IDENTIFIER); + }, + + DECL$setName: function(n, v) { + n.name = v; + }, + + DECL$setInitializer: function(n, e) { + n.initializer = e; + }, + + DECL$setReadOnly: function(n, b) { + n.readOnly = b; + }, + + DECL$finish: function(n) { + }, + + LET_BLOCK$build: function(t) { + var n = Node(t, LET_BLOCK); + n.varDecls = []; + return n; + }, + + LET_BLOCK$setVariables: function(n, n2) { + n.variables = n2; + }, + + LET_BLOCK$setExpression: function(n, e) { + n.expression = e; + }, + + LET_BLOCK$setBlock: function(n, s) { + n.block = s; + }, + + LET_BLOCK$finish: function(n) { + }, + + BLOCK$build: function(t, id) { + var n = new Node(t, BLOCK); + n.varDecls = []; + n.id = id; + return n; + }, + + BLOCK$hoistLets: function(n) { + }, + + BLOCK$addStatement: function(n, n2) { + n.push(n2); + }, + + BLOCK$finish: function(n) { + }, + + EXPRESSION$build: function(t, tt) { + return new Node(t, tt); + }, + + EXPRESSION$addOperand: function(n, n2) { + n.push(n2); + }, + + EXPRESSION$finish: function(n) { + }, + + ASSIGN$build: function(t) { + return new Node(t, ASSIGN); + }, + + ASSIGN$addOperand: function(n, n2) { + n.push(n2); + }, + + ASSIGN$setAssignOp: function(n, o) { + n.assignOp = o; + }, + + ASSIGN$finish: function(n) { + }, + + HOOK$build: function(t) { + return new Node(t, HOOK); + }, + + HOOK$setCondition: function(n, e) { + n[0] = e; + }, + + HOOK$setThenPart: function(n, n2) { + n[1] = n2; + }, + + HOOK$setElsePart: function(n, n2) { + n[2] = n2; + }, + + HOOK$finish: function(n) { + }, + + OR$build: function(t) { + return new Node(t, OR); + }, + + OR$addOperand: function(n, n2) { + n.push(n2); + }, + + OR$finish: function(n) { + }, + + AND$build: function(t) { + return new Node(t, AND); + }, + + AND$addOperand: function(n, n2) { + n.push(n2); + }, + + AND$finish: function(n) { + }, + + BITWISE_OR$build: function(t) { + return new Node(t, BITWISE_OR); + }, + + BITWISE_OR$addOperand: function(n, n2) { + n.push(n2); + }, + + BITWISE_OR$finish: function(n) { + }, + + BITWISE_XOR$build: function(t) { + return new Node(t, BITWISE_XOR); + }, + + BITWISE_XOR$addOperand: function(n, n2) { + n.push(n2); + }, + + BITWISE_XOR$finish: function(n) { + }, + + BITWISE_AND$build: function(t) { + return new Node(t, BITWISE_AND); + }, + + BITWISE_AND$addOperand: function(n, n2) { + n.push(n2); + }, + + BITWISE_AND$finish: function(n) { + }, + + EQUALITY$build: function(t) { + // NB t.token.type must be EQ, NE, STRICT_EQ, or STRICT_NE. + return new Node(t); + }, + + EQUALITY$addOperand: function(n, n2) { + n.push(n2); + }, + + EQUALITY$finish: function(n) { + }, + + RELATIONAL$build: function(t) { + // NB t.token.type must be LT, LE, GE, or GT. + return new Node(t); + }, + + RELATIONAL$addOperand: function(n, n2) { + n.push(n2); + }, + + RELATIONAL$finish: function(n) { + }, + + SHIFT$build: function(t) { + // NB t.token.type must be LSH, RSH, or URSH. + return new Node(t); + }, + + SHIFT$addOperand: function(n, n2) { + n.push(n2); + }, + + SHIFT$finish: function(n) { + }, + + ADD$build: function(t) { + // NB t.token.type must be PLUS or MINUS. + return new Node(t); + }, + + ADD$addOperand: function(n, n2) { + n.push(n2); + }, + + ADD$finish: function(n) { + }, + + MULTIPLY$build: function(t) { + // NB t.token.type must be MUL, DIV, or MOD. + return new Node(t); + }, + + MULTIPLY$addOperand: function(n, n2) { + n.push(n2); + }, + + MULTIPLY$finish: function(n) { + }, + + UNARY$build: function(t) { + // NB t.token.type must be DELETE, VOID, TYPEOF, NOT, BITWISE_NOT, + // UNARY_PLUS, UNARY_MINUS, INCREMENT, or DECREMENT. + if (t.token.type == PLUS) + t.token.type = UNARY_PLUS; + else if (t.token.type == MINUS) + t.token.type = UNARY_MINUS; + return new Node(t); + }, + + UNARY$addOperand: function(n, n2) { + n.push(n2); + }, + + UNARY$setPostfix: function(n) { + n.postfix = true; + }, + + UNARY$finish: function(n) { + }, + + MEMBER$build: function(t, tt) { + // NB t.token.type must be NEW, DOT, or INDEX. + return new Node(t, tt); + }, + + MEMBER$rebuildNewWithArgs: function(n) { + n.type = NEW_WITH_ARGS; + }, + + MEMBER$addOperand: function(n, n2) { + n.push(n2); + }, + + MEMBER$finish: function(n) { + }, + + PRIMARY$build: function(t, tt) { + // NB t.token.type must be NULL, THIS, TRUIE, FALSE, IDENTIFIER, + // NUMBER, STRING, or REGEXP. + return new Node(t, tt); + }, + + PRIMARY$finish: function(n) { + }, + + ARRAY_INIT$build: function(t) { + return new Node(t, ARRAY_INIT); + }, + + ARRAY_INIT$addElement: function(n, n2) { + n.push(n2); + }, + + ARRAY_INIT$finish: function(n) { + }, + + ARRAY_COMP: { + build: function(t) { + return new Node(t, ARRAY_COMP); + }, + + setExpression: function(n, e) { + n.expression = e + }, + + setTail: function(n, n2) { + n.tail = n2; + }, + + finish: function(n) { + } + }, + + COMP_TAIL$build: function(t) { + return new Node(t, COMP_TAIL); + }, + + COMP_TAIL$setGuard: function(n, e) { + n.guard = e; + }, + + COMP_TAIL$addFor: function(n, n2) { + n.push(n2); + }, + + COMP_TAIL$finish: function(n) { + }, + + OBJECT_INIT$build: function(t) { + return new Node(t, OBJECT_INIT); + }, + + OBJECT_INIT$addProperty: function(n, n2) { + n.push(n2); + }, + + OBJECT_INIT$finish: function(n) { + }, + + PROPERTY_INIT$build: function(t) { + return new Node(t, PROPERTY_INIT); + }, + + PROPERTY_INIT$addOperand: function(n, n2) { + n.push(n2); + }, + + PROPERTY_INIT$finish: function(n) { + }, + + COMMA$build: function(t) { + return new Node(t, COMMA); + }, + + COMMA$addOperand: function(n, n2) { + n.push(n2); + }, + + COMMA$finish: function(n) { + }, + + LIST$build: function(t) { + return new Node(t, LIST); + }, + + LIST$addOperand: function(n, n2) { + n.push(n2); + }, + + LIST$finish: function(n) { + }, + + setHoists: function(id, vds) { + } +}; + +function CompilerContext(inFunction, builder) { this.inFunction = inFunction; - //The elms of stmtStack are used to find the target label of CONTINUEs and - // BREAKs. Its length is used in function definitions. + this.hasEmptyReturn = false; + this.hasReturnWithValue = false; + this.isGenerator = false; + this.blockId = 0; + this.builder = builder; this.stmtStack = []; this.funDecls = []; - //varDecls accumulate when we process decls w/ the var keyword. this.varDecls = []; } @@ -64,15 +807,16 @@ CompilerContext.prototype = { inForLoopInit: false, }; -// tokenizer, compiler context -> node -// parses the toplevel and function bodies +/* + * Script :: (tokenizer, compiler context) -> node + * + * Parses the toplevel and function bodies. + */ function Script(t, x) { var n = Statements(t, x); n.type = SCRIPT; n.funDecls = x.funDecls; - // LETs may add varDecls to blocks. - n.varDecls = n.varDecls || []; - Array.prototype.push.apply(n.varDecls, x.varDecls); + n.varDecls = x.varDecls; return n; } @@ -82,21 +826,23 @@ defineProperty(Array.prototype, "top", return this.length && this[this.length-1]; }, false, false, true); -// tokenizer, optional type -> node +/* + * Node :: (tokenizer, optional type) -> node + */ function Node(t, type) { var token = t.token; if (token) { this.type = type || token.type; this.value = token.value; this.lineno = token.lineno; - // start & end are file positions for error handling + // Start & end are file positions for error handling. this.start = token.start; this.end = token.end; } else { this.type = type; this.lineno = t.lineno; } - // nodes use a tokenizer for debugging (getSource, filename getter) + // Nodes use a tokenizer for debugging (getSource, filename getter). this.tokenizer = t; for (var i = 2; i < arguments.length; i++) @@ -109,7 +855,8 @@ Np.toSource = Object.prototype.toSource; // Always use push to add operands to an expression, to update start and end. Np.push = function (kid) { - if (kid !== null) { // kids can be null e.g. [1, , 2] + // kid can be null e.g. [1, , 2]. + if (kid !== null) { if (kid.start < this.start) this.start = kid.start; if (this.end < kid.end) @@ -168,14 +915,25 @@ function nest(t, x, node, func, end) { return n; } -// tokenizer, compiler context -> node -// parses a list of Statements +/* + * Statements :: (tokenizer, compiler context) -> node + * + * Parses a list of Statements. + */ function Statements(t, x) { - var n = new Node(t, BLOCK); + var b = x.builder; + var n = b.BLOCK$build(t, x.blockId++); + b.BLOCK$hoistLets(n); x.stmtStack.push(n); - while (!t.done && t.peek() != RIGHT_CURLY) - n.push(Statement(t, x)); + while (!t.done && t.peek(true) != RIGHT_CURLY) + b.BLOCK$addStatement(n, Statement(t, x)); x.stmtStack.pop(); + b.BLOCK$finish(n); + if (n.needsHoisting) { + b.setHoists(n.id, n.varDecls); + // Propagate up to the function. + x.needsHoisting = true; + } return n; } @@ -188,28 +946,20 @@ function Block(t, x) { const DECLARED_FORM = 0, EXPRESSED_FORM = 1, STATEMENT_FORM = 2; -// tokenizer, compiler context -> node -// parses a Statement +/* + * Statement :: (tokenizer, compiler context) -> node + * + * Parses a Statement. + */ function Statement(t, x) { - var i, label, n, n2, ss, tt = t.get(); + var i, label, n, n2, ss, tt = t.get(true); + var b = x.builder; // Cases for statements ending in a right curly return early, avoiding the // common semicolon insertion magic after this switch. switch (tt) { - case LET: - n = LetForm(t, x, STATEMENT_FORM); - if (n.type === LET_STM) - return n; - if (n.type === LET_EXP) {// exps in stm context are semi nodes - n2 = new Node(t, SEMICOLON); - n2.expression = n; - n = n2; - n.end = n.expression.end; - } - break; - case FUNCTION: - // DECLD_FORM extends fundefs of x, STM_FORM doesn't. + // DECLARED_FORM extends funDecls of x, STATEMENT_FORM doesn't. return FunctionDefinition(t, x, true, (x.stmtStack.length > 1) ? STATEMENT_FORM @@ -221,21 +971,20 @@ function Statement(t, x) { return n; case IF: - n = new Node(t); - n.condition = ParenExpression(t, x); + n = b.IF$build(t); + b.IF$setCondition(n, ParenExpression(t, x)); x.stmtStack.push(n); - n.thenPart = Statement(t, x); - n.elsePart = t.match(ELSE) ? Statement(t, x) : null; + b.IF$setThenPart(n, Statement(t, x)); + if (t.match(ELSE)) + b.IF$setElsePart(n, Statement(t, x)); x.stmtStack.pop(); + b.IF$finish(n); return n; case SWITCH: // This allows CASEs after a DEFAULT, which is in the standard. - n = new Node(t); - - n.discriminant = ParenExpression(t, x); - n.cases = []; - n.defaultIndex = -1; + n = b.SWITCH$build(t); + b.SWITCH$setDiscriminant(n, ParenExpression(t, x)); x.stmtStack.push(n); t.mustMatch(LEFT_CURLY); while ((tt = t.get()) != RIGHT_CURLY) { @@ -243,100 +992,110 @@ function Statement(t, x) { case DEFAULT: if (n.defaultIndex >= 0) throw t.newSyntaxError("More than one switch default"); - // FALL THROUGH - case CASE: - n2 = new Node(t); - if (tt == DEFAULT) - n.defaultIndex = n.cases.length; - else - n2.caseLabel = Expression(t, x, COLON); + n2 = b.DEFAULT$build(t); + b.SWITCH$setDefaultIndex(n, n.cases.length); + t.mustMatch(COLON); + b.DEFAULT$initializeStatements(n2, t); + while ((tt=t.peek(true)) != CASE && tt != DEFAULT && + tt != RIGHT_CURLY) + b.DEFAULT$addStatement(n2, Statement(t, x)); + b.DEFAULT$finish(n2); break; + + case CASE: + n2 = b.CASE$build(t); + b.CASE$setLabel(n2, Expression(t, x, COLON)); + t.mustMatch(COLON); + b.CASE$initializeStatements(n2, t); + while ((tt=t.peek(true)) != CASE && tt != DEFAULT && + tt != RIGHT_CURLY) + b.CASE$addStatement(n2, Statement(t, x)); + b.CASE$finish(n2); + break; + default: throw t.newSyntaxError("Invalid switch case"); } - t.mustMatch(COLON); - n2.statements = new Node(t, BLOCK); - while ((tt=t.peek()) != CASE && tt != DEFAULT && tt != RIGHT_CURLY) - n2.statements.push(Statement(t, x)); - n.cases.push(n2); + b.SWITCH$addCase(n, n2); } x.stmtStack.pop(); + b.SWITCH$finish(n); return n; case FOR: - n = new Node(t); - n.isLoop = true; - if (t.match(IDENTIFIER)) { - if (t.token.value !== "each") - throw t.newSyntaxError("Illegal identifier after for"); - else - n.foreach = true; - } + n = b.FOR$build(t); + if (t.match(IDENTIFIER) && t.token.value == "each") + b.FOR$rebuildForEach(n); t.mustMatch(LEFT_PAREN); if ((tt = t.peek()) != SEMICOLON) { x.inForLoopInit = true; - switch (tt) { - case VAR: case CONST: + if (tt == VAR || tt == CONST) { t.get(); n2 = Variables(t, x); - break; - case LET: + } else if (tt == LET) { t.get(); - n2 = Variables(t, x, "local decls"); - // don't confuse w/ n.varDecl used by for/in. - n.varDecls = []; - for (var i = 0, len = n2.length, vdecls = n.varDecls; i < len; i++) - vdecls.push(n2[i]); - break; - default: + if (t.peek() == LEFT_PAREN) { + n2 = LetBlock(t, x, false); + } else { + /* + * Let in for head, we need to add an implicit block + * around the rest of the for. + */ + var forBlock = b.BLOCK$build(t, x.blockId++); + x.stmtStack.push(forBlock); + n2 = Variables(t, x, forBlock); + } + } else { n2 = Expression(t, x); - break; } x.inForLoopInit = false; } - if (n2 && t.match(IN)) { // for...in - var n2t = n2.type, - se = t.newSyntaxError("Invalid for..in left-hand side"); - n.type = FOR_IN; - if (n2t === VAR || n2t === LET) { - if (n2.length != 1) throw se; - n.iterator = n2[0]; - n.varDecl = n2; + if (n2 && t.match(IN)) { + b.FOR$rebuildForIn(n); + b.FOR$setObject(n, Expression(t, x), forBlock); + if (n2.type == VAR || n2.type == LET) { + if (n2.length != 1) { + throw new SyntaxError("Invalid for..in left-hand side", + t.filename, n2.lineno); + } + b.FOR$setIterator(n, n2[0], n2, forBlock); } else { - var oldn2 = n2; - while (n2.type === GROUP) n2 = n2[0]; // strip parens - n2t = n2.type; - if (n2t !== IDENTIFIER && n2t !== CALL && - n2t !== DOT && n2t !== INDEX) - throw se; - n.iterator = oldn2; - n.varDecl = null; + b.FOR$setIterator(n, n2, null, forBlock); } - n.object = Expression(t, x); - } else { // classic for - if (n.foreach) throw t.newSyntaxError("Illegal for-each syntax"); - n.setup = n2 || null; + } else { + b.FOR$setSetup(n, n2); t.mustMatch(SEMICOLON); - n.condition = (t.peek() == SEMICOLON) ? null : Expression(t, x); + if (n.isEach) + throw t.newSyntaxError("Invalid for each..in loop"); + b.FOR$setCondition(n, (t.peek() == SEMICOLON) + ? null + : Expression(t, x)); t.mustMatch(SEMICOLON); - n.update = (t.peek() == RIGHT_PAREN) ? null : Expression(t, x); + b.FOR$setUpdate(n, (t.peek() == RIGHT_PAREN) + ? null + : Expression(t, x)); } t.mustMatch(RIGHT_PAREN); - n.body = nest(t, x, n, Statement); + b.FOR$setBody(n, nest(t, x, n, Statement)); + if (forBlock) { + b.BLOCK$finish(forBlock); + x.stmtStack.pop(); + } + b.FOR$finish(n); return n; case WHILE: - n = new Node(t); - n.isLoop = true; - n.condition = ParenExpression(t, x); - n.body = nest(t, x, n, Statement); + n = b.WHILE$build(t); + b.WHILE$setCondition(n, ParenExpression(t, x)); + b.WHILE$setBody(n, nest(t, x, n, Statement)); + b.WHILE$finish(n); return n; case DO: - n = new Node(t); - n.isLoop = true; - n.body = nest(t, x, n, Statement, WHILE); - n.condition = ParenExpression(t, x); + n = b.DO$build(t); + b.DO$setBody(n, nest(t, x, n, Statement, WHILE)); + b.DO$setCondition(n, ParenExpression(t, x)); + b.DO$finish(n); if (!x.ecmaStrictMode) { //