| /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- |
| * vim: set ts=8 sts=4 et sw=4 tw=99: |
| * This Source Code Form is subject to the terms of the Mozilla Public |
| * License, v. 2.0. If a copy of the MPL was not distributed with this |
| * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
| |
| /* |
| * JS bytecode descriptors, disassemblers, and (expression) decompilers. |
| */ |
| |
| #include "jsopcode.h" |
| |
| #include "mozilla/Util.h" |
| |
| #include <stdarg.h> |
| #include <stdio.h> |
| #include <string.h> |
| |
| #include "jstypes.h" |
| #include "jsutil.h" |
| #include "jsprf.h" |
| #include "jsanalyze.h" |
| #include "jsapi.h" |
| #include "jsatom.h" |
| #include "jscntxt.h" |
| #include "jscompartment.h" |
| #include "jsfun.h" |
| #include "jsnum.h" |
| #include "jsobj.h" |
| #include "jsscript.h" |
| #include "jsstr.h" |
| |
| #include "frontend/BytecodeCompiler.h" |
| #include "frontend/SourceNotes.h" |
| #include "js/CharacterEncoding.h" |
| #include "vm/Shape.h" |
| #include "vm/StringBuffer.h" |
| |
| #include "jscntxtinlines.h" |
| #include "jsobjinlines.h" |
| #include "jscompartmentinlines.h" |
| #include "jsopcodeinlines.h" |
| |
| #include "jsautooplen.h" |
| |
| #include "vm/RegExpObject-inl.h" |
| #include "vm/ScopeObject-inl.h" |
| |
| using namespace js; |
| using namespace js::gc; |
| |
| using js::frontend::IsIdentifier; |
| using mozilla::ArrayLength; |
| |
| /* |
| * Index limit must stay within 32 bits. |
| */ |
| JS_STATIC_ASSERT(sizeof(uint32_t) * JS_BITS_PER_BYTE >= INDEX_LIMIT_LOG2 + 1); |
| |
| /* Verify JSOP_XXX_LENGTH constant definitions. */ |
| #define OPDEF(op,val,name,token,length,nuses,ndefs,format) \ |
| JS_STATIC_ASSERT(op##_LENGTH == length); |
| #include "jsopcode.tbl" |
| #undef OPDEF |
| |
| static const char js_incop_strs[][3] = {"++", "--"}; |
| static const char js_for_each_str[] = "for each"; |
| |
| const JSCodeSpec js_CodeSpec[] = { |
| #define OPDEF(op,val,name,token,length,nuses,ndefs,format) \ |
| {length,nuses,ndefs,format}, |
| #include "jsopcode.tbl" |
| #undef OPDEF |
| }; |
| |
| const unsigned js_NumCodeSpecs = JS_ARRAY_LENGTH(js_CodeSpec); |
| |
| /* |
| * Each element of the array is either a source literal associated with JS |
| * bytecode or null. |
| */ |
| static const char * const CodeToken[] = { |
| #define OPDEF(op,val,name,token,length,nuses,ndefs,format) \ |
| token, |
| #include "jsopcode.tbl" |
| #undef OPDEF |
| }; |
| |
| /* |
| * Array of JS bytecode names used by PC count JSON, DEBUG-only js_Disassemble |
| * and JIT debug spew. |
| */ |
| const char * const js_CodeName[] = { |
| #define OPDEF(op,val,name,token,length,nuses,ndefs,format) \ |
| name, |
| #include "jsopcode.tbl" |
| #undef OPDEF |
| }; |
| |
| /************************************************************************/ |
| |
| #define COUNTS_LEN 16 |
| |
| size_t |
| js_GetVariableBytecodeLength(jsbytecode *pc) |
| { |
| JSOp op = JSOp(*pc); |
| JS_ASSERT(js_CodeSpec[op].length == -1); |
| switch (op) { |
| case JSOP_TABLESWITCH: { |
| /* Structure: default-jump case-low case-high case1-jump ... */ |
| pc += JUMP_OFFSET_LEN; |
| int32_t low = GET_JUMP_OFFSET(pc); |
| pc += JUMP_OFFSET_LEN; |
| int32_t high = GET_JUMP_OFFSET(pc); |
| unsigned ncases = unsigned(high - low + 1); |
| return 1 + 3 * JUMP_OFFSET_LEN + ncases * JUMP_OFFSET_LEN; |
| } |
| default: |
| JS_NOT_REACHED("Unexpected op"); |
| return 0; |
| } |
| } |
| |
| static uint32_t |
| NumBlockSlots(JSScript *script, jsbytecode *pc) |
| { |
| JS_ASSERT(*pc == JSOP_ENTERBLOCK || *pc == JSOP_ENTERLET0 || *pc == JSOP_ENTERLET1); |
| JS_STATIC_ASSERT(JSOP_ENTERBLOCK_LENGTH == JSOP_ENTERLET0_LENGTH); |
| JS_STATIC_ASSERT(JSOP_ENTERBLOCK_LENGTH == JSOP_ENTERLET1_LENGTH); |
| |
| return script->getObject(GET_UINT32_INDEX(pc))->as<StaticBlockObject>().slotCount(); |
| } |
| |
| unsigned |
| js::StackUses(JSScript *script, jsbytecode *pc) |
| { |
| JSOp op = (JSOp) *pc; |
| const JSCodeSpec &cs = js_CodeSpec[op]; |
| if (cs.nuses >= 0) |
| return cs.nuses; |
| |
| JS_ASSERT(js_CodeSpec[op].nuses == -1); |
| switch (op) { |
| case JSOP_POPN: |
| return GET_UINT16(pc); |
| case JSOP_LEAVEBLOCK: |
| return GET_UINT16(pc); |
| case JSOP_LEAVEBLOCKEXPR: |
| return GET_UINT16(pc) + 1; |
| case JSOP_ENTERLET0: |
| return NumBlockSlots(script, pc); |
| case JSOP_ENTERLET1: |
| return NumBlockSlots(script, pc) + 1; |
| default: |
| /* stack: fun, this, [argc arguments] */ |
| JS_ASSERT(op == JSOP_NEW || op == JSOP_CALL || op == JSOP_EVAL || |
| op == JSOP_FUNCALL || op == JSOP_FUNAPPLY); |
| return 2 + GET_ARGC(pc); |
| } |
| } |
| |
| unsigned |
| js::StackDefs(JSScript *script, jsbytecode *pc) |
| { |
| JSOp op = (JSOp) *pc; |
| const JSCodeSpec &cs = js_CodeSpec[op]; |
| if (cs.ndefs >= 0) |
| return cs.ndefs; |
| |
| uint32_t n = NumBlockSlots(script, pc); |
| return op == JSOP_ENTERLET1 ? n + 1 : n; |
| } |
| |
| static const char * const countBaseNames[] = { |
| "interp", |
| "mjit", |
| "mjit_calls", |
| "mjit_code", |
| "mjit_pics" |
| }; |
| |
| JS_STATIC_ASSERT(JS_ARRAY_LENGTH(countBaseNames) == PCCounts::BASE_LIMIT); |
| |
| static const char * const countAccessNames[] = { |
| "infer_mono", |
| "infer_di", |
| "infer_poly", |
| "infer_barrier", |
| "infer_nobarrier", |
| "observe_undefined", |
| "observe_null", |
| "observe_boolean", |
| "observe_int32", |
| "observe_double", |
| "observe_string", |
| "observe_object" |
| }; |
| |
| JS_STATIC_ASSERT(JS_ARRAY_LENGTH(countBaseNames) + |
| JS_ARRAY_LENGTH(countAccessNames) == PCCounts::ACCESS_LIMIT); |
| |
| static const char * const countElementNames[] = { |
| "id_int", |
| "id_double", |
| "id_other", |
| "id_unknown", |
| "elem_typed", |
| "elem_packed", |
| "elem_dense", |
| "elem_other" |
| }; |
| |
| JS_STATIC_ASSERT(JS_ARRAY_LENGTH(countBaseNames) + |
| JS_ARRAY_LENGTH(countAccessNames) + |
| JS_ARRAY_LENGTH(countElementNames) == PCCounts::ELEM_LIMIT); |
| |
| static const char * const countPropertyNames[] = { |
| "prop_static", |
| "prop_definite", |
| "prop_other" |
| }; |
| |
| JS_STATIC_ASSERT(JS_ARRAY_LENGTH(countBaseNames) + |
| JS_ARRAY_LENGTH(countAccessNames) + |
| JS_ARRAY_LENGTH(countPropertyNames) == PCCounts::PROP_LIMIT); |
| |
| static const char * const countArithNames[] = { |
| "arith_int", |
| "arith_double", |
| "arith_other", |
| "arith_unknown", |
| }; |
| |
| JS_STATIC_ASSERT(JS_ARRAY_LENGTH(countBaseNames) + |
| JS_ARRAY_LENGTH(countArithNames) == PCCounts::ARITH_LIMIT); |
| |
| /* static */ const char * |
| PCCounts::countName(JSOp op, size_t which) |
| { |
| JS_ASSERT(which < numCounts(op)); |
| |
| if (which < BASE_LIMIT) |
| return countBaseNames[which]; |
| |
| if (accessOp(op)) { |
| if (which < ACCESS_LIMIT) |
| return countAccessNames[which - BASE_LIMIT]; |
| if (elementOp(op)) |
| return countElementNames[which - ACCESS_LIMIT]; |
| if (propertyOp(op)) |
| return countPropertyNames[which - ACCESS_LIMIT]; |
| JS_NOT_REACHED("bad op"); |
| return NULL; |
| } |
| |
| if (arithOp(op)) |
| return countArithNames[which - BASE_LIMIT]; |
| |
| JS_NOT_REACHED("bad op"); |
| return NULL; |
| } |
| |
| #ifdef DEBUG |
| |
| #ifdef JS_ION |
| void |
| js::DumpIonScriptCounts(Sprinter *sp, jit::IonScriptCounts *ionCounts) |
| { |
| Sprint(sp, "IonScript [%lu blocks]:\n", ionCounts->numBlocks()); |
| for (size_t i = 0; i < ionCounts->numBlocks(); i++) { |
| const jit::IonBlockCounts &block = ionCounts->block(i); |
| if (block.hitCount() < 10) |
| continue; |
| Sprint(sp, "BB #%lu [%05u]", block.id(), block.offset()); |
| for (size_t j = 0; j < block.numSuccessors(); j++) |
| Sprint(sp, " -> #%lu", block.successor(j)); |
| Sprint(sp, " :: %llu hits %u instruction bytes %u spill bytes\n", |
| block.hitCount(), block.instructionBytes(), block.spillBytes()); |
| Sprint(sp, "%s\n", block.code()); |
| } |
| } |
| #endif |
| |
| void |
| js_DumpPCCounts(JSContext *cx, HandleScript script, js::Sprinter *sp) |
| { |
| JS_ASSERT(script->hasScriptCounts); |
| |
| jsbytecode *pc = script->code; |
| while (pc < script->code + script->length) { |
| JSOp op = JSOp(*pc); |
| |
| int len = js_CodeSpec[op].length; |
| jsbytecode *next = (len != -1) ? pc + len : pc + js_GetVariableBytecodeLength(pc); |
| |
| if (!js_Disassemble1(cx, script, pc, pc - script->code, true, sp)) |
| return; |
| |
| size_t total = PCCounts::numCounts(op); |
| double *raw = script->getPCCounts(pc).rawCounts(); |
| |
| Sprint(sp, " {"); |
| bool printed = false; |
| for (size_t i = 0; i < total; i++) { |
| double val = raw[i]; |
| if (val) { |
| if (printed) |
| Sprint(sp, ", "); |
| Sprint(sp, "\"%s\": %.0f", PCCounts::countName(op, i), val); |
| printed = true; |
| } |
| } |
| Sprint(sp, "}\n"); |
| |
| pc = next; |
| } |
| |
| #ifdef JS_ION |
| jit::IonScriptCounts *ionCounts = script->getIonCounts(); |
| |
| while (ionCounts) { |
| DumpIonScriptCounts(sp, ionCounts); |
| ionCounts = ionCounts->previous(); |
| } |
| #endif |
| } |
| |
| /* |
| * If pc != NULL, include a prefix indicating whether the PC is at the current line. |
| * If showAll is true, include the source note type and the entry stack depth. |
| */ |
| JS_FRIEND_API(JSBool) |
| js_DisassembleAtPC(JSContext *cx, JSScript *scriptArg, JSBool lines, |
| jsbytecode *pc, bool showAll, Sprinter *sp) |
| { |
| RootedScript script(cx, scriptArg); |
| |
| jsbytecode *next, *end; |
| unsigned len; |
| |
| if (showAll) |
| Sprint(sp, "%s:%u\n", script->filename(), script->lineno); |
| |
| if (pc != NULL) |
| sp->put(" "); |
| if (showAll) |
| sp->put("sn stack "); |
| sp->put("loc "); |
| if (lines) |
| sp->put("line"); |
| sp->put(" op\n"); |
| |
| if (pc != NULL) |
| sp->put(" "); |
| if (showAll) |
| sp->put("-- ----- "); |
| sp->put("----- "); |
| if (lines) |
| sp->put("----"); |
| sp->put(" --\n"); |
| |
| next = script->code; |
| end = next + script->length; |
| while (next < end) { |
| if (next == script->main()) |
| sp->put("main:\n"); |
| if (pc != NULL) { |
| if (pc == next) |
| sp->put("--> "); |
| else |
| sp->put(" "); |
| } |
| if (showAll) { |
| jssrcnote *sn = js_GetSrcNote(cx, script, next); |
| if (sn) { |
| JS_ASSERT(!SN_IS_TERMINATOR(sn)); |
| jssrcnote *next = SN_NEXT(sn); |
| while (!SN_IS_TERMINATOR(next) && SN_DELTA(next) == 0) { |
| Sprint(sp, "%02u\n ", SN_TYPE(sn)); |
| sn = next; |
| next = SN_NEXT(sn); |
| } |
| Sprint(sp, "%02u ", SN_TYPE(sn)); |
| } |
| else |
| sp->put(" "); |
| if (script->hasAnalysis() && script->analysis()->maybeCode(next)) |
| Sprint(sp, "%05u ", script->analysis()->getCode(next).stackDepth); |
| else |
| sp->put(" "); |
| } |
| len = js_Disassemble1(cx, script, next, next - script->code, lines, sp); |
| if (!len) |
| return JS_FALSE; |
| next += len; |
| } |
| return JS_TRUE; |
| } |
| |
| JSBool |
| js_Disassemble(JSContext *cx, HandleScript script, JSBool lines, Sprinter *sp) |
| { |
| return js_DisassembleAtPC(cx, script, lines, NULL, false, sp); |
| } |
| |
| JS_FRIEND_API(JSBool) |
| js_DumpPC(JSContext *cx) |
| { |
| js::gc::AutoSuppressGC suppressGC(cx); |
| Sprinter sprinter(cx); |
| if (!sprinter.init()) |
| return JS_FALSE; |
| ScriptFrameIter iter(cx); |
| RootedScript script(cx, iter.script()); |
| JSBool ok = js_DisassembleAtPC(cx, script, true, iter.pc(), false, &sprinter); |
| fprintf(stdout, "%s", sprinter.string()); |
| return ok; |
| } |
| |
| JS_FRIEND_API(JSBool) |
| js_DumpScript(JSContext *cx, JSScript *scriptArg) |
| { |
| js::gc::AutoSuppressGC suppressGC(cx); |
| Sprinter sprinter(cx); |
| if (!sprinter.init()) |
| return JS_FALSE; |
| RootedScript script(cx, scriptArg); |
| JSBool ok = js_Disassemble(cx, script, true, &sprinter); |
| fprintf(stdout, "%s", sprinter.string()); |
| return ok; |
| } |
| |
| /* |
| * Useful to debug ReconstructPCStack. |
| */ |
| JS_FRIEND_API(JSBool) |
| js_DumpScriptDepth(JSContext *cx, JSScript *scriptArg, jsbytecode *pc) |
| { |
| js::gc::AutoSuppressGC suppressGC(cx); |
| Sprinter sprinter(cx); |
| if (!sprinter.init()) |
| return JS_FALSE; |
| RootedScript script(cx, scriptArg); |
| JSBool ok = js_DisassembleAtPC(cx, script, true, pc, true, &sprinter); |
| fprintf(stdout, "%s", sprinter.string()); |
| return ok; |
| } |
| |
| static char * |
| QuoteString(Sprinter *sp, JSString *str, uint32_t quote); |
| |
| static bool |
| ToDisassemblySource(JSContext *cx, jsval v, JSAutoByteString *bytes) |
| { |
| if (JSVAL_IS_STRING(v)) { |
| Sprinter sprinter(cx); |
| if (!sprinter.init()) |
| return false; |
| char *nbytes = QuoteString(&sprinter, JSVAL_TO_STRING(v), '"'); |
| if (!nbytes) |
| return false; |
| nbytes = JS_sprintf_append(NULL, "%s", nbytes); |
| if (!nbytes) |
| return false; |
| bytes->initBytes(nbytes); |
| return true; |
| } |
| |
| if (cx->runtime()->isHeapBusy() || cx->runtime()->noGCOrAllocationCheck) { |
| char *source = JS_sprintf_append(NULL, "<value>"); |
| if (!source) |
| return false; |
| bytes->initBytes(source); |
| return true; |
| } |
| |
| if (!JSVAL_IS_PRIMITIVE(v)) { |
| JSObject *obj = JSVAL_TO_OBJECT(v); |
| if (obj->is<BlockObject>()) { |
| char *source = JS_sprintf_append(NULL, "depth %d {", |
| obj->as<BlockObject>().stackDepth()); |
| if (!source) |
| return false; |
| |
| Shape::Range<CanGC> r(cx, obj->lastProperty()); |
| |
| while (!r.empty()) { |
| Rooted<Shape*> shape(cx, &r.front()); |
| JSAtom *atom = JSID_IS_INT(shape->propid()) |
| ? cx->names().empty |
| : JSID_TO_ATOM(shape->propid()); |
| |
| JSAutoByteString bytes; |
| if (!js_AtomToPrintableString(cx, atom, &bytes)) |
| return false; |
| |
| r.popFront(); |
| source = JS_sprintf_append(source, "%s: %d%s", |
| bytes.ptr(), shape->shortid(), |
| !r.empty() ? ", " : ""); |
| if (!source) |
| return false; |
| } |
| |
| source = JS_sprintf_append(source, "}"); |
| if (!source) |
| return false; |
| bytes->initBytes(source); |
| return true; |
| } |
| |
| if (obj->is<JSFunction>()) { |
| JSString *str = JS_DecompileFunction(cx, &obj->as<JSFunction>(), JS_DONT_PRETTY_PRINT); |
| if (!str) |
| return false; |
| return bytes->encodeLatin1(cx, str); |
| } |
| |
| if (obj->is<RegExpObject>()) { |
| JSString *source = obj->as<RegExpObject>().toString(cx); |
| if (!source) |
| return false; |
| JS::Anchor<JSString *> anchor(source); |
| return bytes->encodeLatin1(cx, source); |
| } |
| } |
| |
| return !!js_ValueToPrintable(cx, v, bytes, true); |
| } |
| |
| unsigned |
| js_Disassemble1(JSContext *cx, HandleScript script, jsbytecode *pc, |
| unsigned loc, JSBool lines, Sprinter *sp) |
| { |
| JSOp op = (JSOp)*pc; |
| if (op >= JSOP_LIMIT) { |
| char numBuf1[12], numBuf2[12]; |
| JS_snprintf(numBuf1, sizeof numBuf1, "%d", op); |
| JS_snprintf(numBuf2, sizeof numBuf2, "%d", JSOP_LIMIT); |
| JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL, |
| JSMSG_BYTECODE_TOO_BIG, numBuf1, numBuf2); |
| return 0; |
| } |
| const JSCodeSpec *cs = &js_CodeSpec[op]; |
| ptrdiff_t len = (ptrdiff_t) cs->length; |
| Sprint(sp, "%05u:", loc); |
| if (lines) |
| Sprint(sp, "%4u", JS_PCToLineNumber(cx, script, pc)); |
| Sprint(sp, " %s", js_CodeName[op]); |
| |
| switch (JOF_TYPE(cs->format)) { |
| case JOF_BYTE: |
| // Scan the trynotes to find the associated catch block |
| // and make the try opcode look like a jump instruction |
| // with an offset. This simplifies code coverage analysis |
| // based on this disassembled output. |
| if (op == JSOP_TRY) { |
| TryNoteArray *trynotes = script->trynotes(); |
| uint32_t i; |
| for(i = 0; i < trynotes->length; i++) { |
| JSTryNote note = trynotes->vector[i]; |
| if (note.kind == JSTRY_CATCH && note.start == loc + 1) { |
| Sprint(sp, " %u (%+d)", |
| (unsigned int) (loc+note.length+1), |
| (int) (note.length+1)); |
| break; |
| } |
| } |
| } |
| break; |
| |
| case JOF_JUMP: { |
| ptrdiff_t off = GET_JUMP_OFFSET(pc); |
| Sprint(sp, " %u (%+d)", loc + (int) off, (int) off); |
| break; |
| } |
| |
| case JOF_SCOPECOORD: { |
| Value v = StringValue(ScopeCoordinateName(cx, script, pc)); |
| JSAutoByteString bytes; |
| if (!ToDisassemblySource(cx, v, &bytes)) |
| return 0; |
| ScopeCoordinate sc(pc); |
| Sprint(sp, " %s (hops = %u, slot = %u)", bytes.ptr(), sc.hops, sc.slot); |
| break; |
| } |
| |
| case JOF_ATOM: { |
| Value v = StringValue(script->getAtom(GET_UINT32_INDEX(pc))); |
| JSAutoByteString bytes; |
| if (!ToDisassemblySource(cx, v, &bytes)) |
| return 0; |
| Sprint(sp, " %s", bytes.ptr()); |
| break; |
| } |
| |
| case JOF_DOUBLE: { |
| Value v = script->getConst(GET_UINT32_INDEX(pc)); |
| JSAutoByteString bytes; |
| if (!ToDisassemblySource(cx, v, &bytes)) |
| return 0; |
| Sprint(sp, " %s", bytes.ptr()); |
| break; |
| } |
| |
| case JOF_OBJECT: { |
| /* Don't call obj.toSource if analysis/inference is active. */ |
| if (cx->compartment()->activeAnalysis) { |
| Sprint(sp, " object"); |
| break; |
| } |
| |
| JSObject *obj = script->getObject(GET_UINT32_INDEX(pc)); |
| { |
| JSAutoByteString bytes; |
| if (!ToDisassemblySource(cx, ObjectValue(*obj), &bytes)) |
| return 0; |
| Sprint(sp, " %s", bytes.ptr()); |
| } |
| break; |
| } |
| |
| case JOF_REGEXP: { |
| JSObject *obj = script->getRegExp(GET_UINT32_INDEX(pc)); |
| JSAutoByteString bytes; |
| if (!ToDisassemblySource(cx, ObjectValue(*obj), &bytes)) |
| return 0; |
| Sprint(sp, " %s", bytes.ptr()); |
| break; |
| } |
| |
| case JOF_TABLESWITCH: |
| { |
| int32_t i, low, high; |
| |
| ptrdiff_t off = GET_JUMP_OFFSET(pc); |
| jsbytecode *pc2 = pc + JUMP_OFFSET_LEN; |
| low = GET_JUMP_OFFSET(pc2); |
| pc2 += JUMP_OFFSET_LEN; |
| high = GET_JUMP_OFFSET(pc2); |
| pc2 += JUMP_OFFSET_LEN; |
| Sprint(sp, " defaultOffset %d low %d high %d", int(off), low, high); |
| for (i = low; i <= high; i++) { |
| off = GET_JUMP_OFFSET(pc2); |
| Sprint(sp, "\n\t%d: %d", i, int(off)); |
| pc2 += JUMP_OFFSET_LEN; |
| } |
| len = 1 + pc2 - pc; |
| break; |
| } |
| |
| case JOF_QARG: |
| Sprint(sp, " %u", GET_ARGNO(pc)); |
| break; |
| |
| case JOF_LOCAL: |
| Sprint(sp, " %u", GET_SLOTNO(pc)); |
| break; |
| |
| case JOF_SLOTOBJECT: { |
| Sprint(sp, " %u", GET_SLOTNO(pc)); |
| JSObject *obj = script->getObject(GET_UINT32_INDEX(pc + SLOTNO_LEN)); |
| JSAutoByteString bytes; |
| if (!ToDisassemblySource(cx, ObjectValue(*obj), &bytes)) |
| return 0; |
| Sprint(sp, " %s", bytes.ptr()); |
| break; |
| } |
| |
| { |
| int i; |
| |
| case JOF_UINT16PAIR: |
| i = (int)GET_UINT16(pc); |
| Sprint(sp, " %d", i); |
| pc += UINT16_LEN; |
| /* FALL THROUGH */ |
| |
| case JOF_UINT16: |
| i = (int)GET_UINT16(pc); |
| goto print_int; |
| |
| case JOF_UINT24: |
| JS_ASSERT(op == JSOP_UINT24 || op == JSOP_NEWARRAY || op == JSOP_INITELEM_ARRAY); |
| i = (int)GET_UINT24(pc); |
| goto print_int; |
| |
| case JOF_UINT8: |
| i = GET_UINT8(pc); |
| goto print_int; |
| |
| case JOF_INT8: |
| i = GET_INT8(pc); |
| goto print_int; |
| |
| case JOF_INT32: |
| JS_ASSERT(op == JSOP_INT32); |
| i = GET_INT32(pc); |
| print_int: |
| Sprint(sp, " %d", i); |
| break; |
| } |
| |
| default: { |
| char numBuf[12]; |
| JS_snprintf(numBuf, sizeof numBuf, "%lx", (unsigned long) cs->format); |
| JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL, |
| JSMSG_UNKNOWN_FORMAT, numBuf); |
| return 0; |
| } |
| } |
| sp->put("\n"); |
| return len; |
| } |
| |
| #endif /* DEBUG */ |
| |
| /************************************************************************/ |
| |
| const size_t Sprinter::DefaultSize = 64; |
| |
| bool |
| Sprinter::realloc_(size_t newSize) |
| { |
| JS_ASSERT(newSize > (size_t) offset); |
| char *newBuf = (char *) context->realloc_(base, newSize); |
| if (!newBuf) |
| return false; |
| base = newBuf; |
| size = newSize; |
| base[size - 1] = 0; |
| return true; |
| } |
| |
| Sprinter::Sprinter(JSContext *cx) |
| : context(cx), |
| #ifdef DEBUG |
| initialized(false), |
| #endif |
| base(NULL), size(0), offset(0), reportedOOM(false) |
| { } |
| |
| Sprinter::~Sprinter() |
| { |
| #ifdef DEBUG |
| if (initialized) |
| checkInvariants(); |
| #endif |
| js_free(base); |
| } |
| |
| bool |
| Sprinter::init() |
| { |
| JS_ASSERT(!initialized); |
| base = (char *) context->malloc_(DefaultSize); |
| if (!base) |
| return false; |
| #ifdef DEBUG |
| initialized = true; |
| #endif |
| *base = 0; |
| size = DefaultSize; |
| base[size - 1] = 0; |
| return true; |
| } |
| |
| void |
| Sprinter::checkInvariants() const |
| { |
| JS_ASSERT(initialized); |
| JS_ASSERT((size_t) offset < size); |
| JS_ASSERT(base[size - 1] == 0); |
| } |
| |
| const char * |
| Sprinter::string() const |
| { |
| return base; |
| } |
| |
| const char * |
| Sprinter::stringEnd() const |
| { |
| return base + offset; |
| } |
| |
| char * |
| Sprinter::stringAt(ptrdiff_t off) const |
| { |
| JS_ASSERT(off >= 0 && (size_t) off < size); |
| return base + off; |
| } |
| |
| char & |
| Sprinter::operator[](size_t off) |
| { |
| JS_ASSERT(off < size); |
| return *(base + off); |
| } |
| |
| bool |
| Sprinter::empty() const |
| { |
| return *base == 0; |
| } |
| |
| char * |
| Sprinter::reserve(size_t len) |
| { |
| InvariantChecker ic(this); |
| |
| while (len + 1 > size - offset) { /* Include trailing \0 */ |
| if (!realloc_(size * 2)) |
| return NULL; |
| } |
| |
| char *sb = base + offset; |
| offset += len; |
| return sb; |
| } |
| |
| char * |
| Sprinter::reserveAndClear(size_t len) |
| { |
| char *sb = reserve(len); |
| if (sb) |
| memset(sb, 0, len); |
| return sb; |
| } |
| |
| ptrdiff_t |
| Sprinter::put(const char *s, size_t len) |
| { |
| InvariantChecker ic(this); |
| |
| const char *oldBase = base; |
| const char *oldEnd = base + size; |
| |
| ptrdiff_t oldOffset = offset; |
| char *bp = reserve(len); |
| if (!bp) |
| return -1; |
| |
| /* s is within the buffer already */ |
| if (s >= oldBase && s < oldEnd) { |
| /* buffer was realloc'ed */ |
| if (base != oldBase) |
| s = stringAt(s - oldBase); /* this is where it lives now */ |
| memmove(bp, s, len); |
| } else { |
| js_memcpy(bp, s, len); |
| } |
| |
| bp[len] = 0; |
| return oldOffset; |
| } |
| |
| ptrdiff_t |
| Sprinter::put(const char *s) |
| { |
| return put(s, strlen(s)); |
| } |
| |
| ptrdiff_t |
| Sprinter::putString(JSString *s) |
| { |
| InvariantChecker ic(this); |
| |
| size_t length = s->length(); |
| const jschar *chars = s->getChars(context); |
| if (!chars) |
| return -1; |
| |
| size_t size = length; |
| if (size == (size_t) -1) |
| return -1; |
| |
| ptrdiff_t oldOffset = offset; |
| char *buffer = reserve(size); |
| if (!buffer) |
| return -1; |
| DeflateStringToBuffer(context, chars, length, buffer, &size); |
| buffer[size] = 0; |
| |
| return oldOffset; |
| } |
| |
| int |
| Sprinter::printf(const char *fmt, ...) |
| { |
| InvariantChecker ic(this); |
| |
| do { |
| va_list va; |
| va_start(va, fmt); |
| int i = vsnprintf(base + offset, size - offset, fmt, va); |
| va_end(va); |
| |
| if (i > -1 && (size_t) i < size - offset) { |
| offset += i; |
| return i; |
| } |
| } while (realloc_(size * 2)); |
| |
| return -1; |
| } |
| |
| void |
| Sprinter::setOffset(const char *end) |
| { |
| JS_ASSERT(end >= base && end < base + size); |
| offset = end - base; |
| } |
| |
| void |
| Sprinter::setOffset(ptrdiff_t off) |
| { |
| JS_ASSERT(off >= 0 && (size_t) off < size); |
| offset = off; |
| } |
| |
| ptrdiff_t |
| Sprinter::getOffset() const |
| { |
| return offset; |
| } |
| |
| ptrdiff_t |
| Sprinter::getOffsetOf(const char *string) const |
| { |
| JS_ASSERT(string >= base && string < base + size); |
| return string - base; |
| } |
| |
| void |
| Sprinter::reportOutOfMemory() { |
| if (reportedOOM) |
| return; |
| js_ReportOutOfMemory(context); |
| reportedOOM = true; |
| } |
| |
| bool |
| Sprinter::hadOutOfMemory() const { |
| return reportedOOM; |
| } |
| |
| ptrdiff_t |
| js::Sprint(Sprinter *sp, const char *format, ...) |
| { |
| va_list ap; |
| char *bp; |
| ptrdiff_t offset; |
| |
| va_start(ap, format); |
| bp = JS_vsmprintf(format, ap); /* XXX vsaprintf */ |
| va_end(ap); |
| if (!bp) { |
| sp->reportOutOfMemory(); |
| return -1; |
| } |
| offset = sp->put(bp); |
| js_free(bp); |
| return offset; |
| } |
| |
| const char js_EscapeMap[] = { |
| '\b', 'b', |
| '\f', 'f', |
| '\n', 'n', |
| '\r', 'r', |
| '\t', 't', |
| '\v', 'v', |
| '"', '"', |
| '\'', '\'', |
| '\\', '\\', |
| '\0' |
| }; |
| |
| #define DONT_ESCAPE 0x10000 |
| |
| static char * |
| QuoteString(Sprinter *sp, JSString *str, uint32_t quote) |
| { |
| /* Sample off first for later return value pointer computation. */ |
| JSBool dontEscape = (quote & DONT_ESCAPE) != 0; |
| jschar qc = (jschar) quote; |
| ptrdiff_t offset = sp->getOffset(); |
| if (qc && Sprint(sp, "%c", (char)qc) < 0) |
| return NULL; |
| |
| const jschar *s = str->getChars(sp->context); |
| if (!s) |
| return NULL; |
| const jschar *z = s + str->length(); |
| |
| /* Loop control variables: z points at end of string sentinel. */ |
| for (const jschar *t = s; t < z; s = ++t) { |
| /* Move t forward from s past un-quote-worthy characters. */ |
| jschar c = *t; |
| while (c < 127 && isprint(c) && c != qc && c != '\\' && c != '\t') { |
| c = *++t; |
| if (t == z) |
| break; |
| } |
| |
| { |
| ptrdiff_t len = t - s; |
| ptrdiff_t base = sp->getOffset(); |
| char *bp = sp->reserve(len); |
| if (!bp) |
| return NULL; |
| |
| for (ptrdiff_t i = 0; i < len; ++i) |
| (*sp)[base + i] = (char) *s++; |
| (*sp)[base + len] = 0; |
| } |
| |
| if (t == z) |
| break; |
| |
| /* Use js_EscapeMap, \u, or \x only if necessary. */ |
| bool ok; |
| const char *e; |
| if (!(c >> 8) && c != 0 && (e = strchr(js_EscapeMap, (int)c)) != NULL) { |
| ok = dontEscape |
| ? Sprint(sp, "%c", (char)c) >= 0 |
| : Sprint(sp, "\\%c", e[1]) >= 0; |
| } else { |
| /* |
| * Use \x only if the high byte is 0 and we're in a quoted string, |
| * because ECMA-262 allows only \u, not \x, in Unicode identifiers |
| * (see bug 621814). |
| */ |
| ok = Sprint(sp, (qc && !(c >> 8)) ? "\\x%02X" : "\\u%04X", c) >= 0; |
| } |
| if (!ok) |
| return NULL; |
| } |
| |
| /* Sprint the closing quote and return the quoted string. */ |
| if (qc && Sprint(sp, "%c", (char)qc) < 0) |
| return NULL; |
| |
| /* |
| * If we haven't Sprint'd anything yet, Sprint an empty string so that |
| * the return below gives a valid result. |
| */ |
| if (offset == sp->getOffset() && Sprint(sp, "") < 0) |
| return NULL; |
| |
| return sp->stringAt(offset); |
| } |
| |
| JSString * |
| js_QuoteString(JSContext *cx, JSString *str, jschar quote) |
| { |
| Sprinter sprinter(cx); |
| if (!sprinter.init()) |
| return NULL; |
| char *bytes = QuoteString(&sprinter, str, quote); |
| JSString *escstr = bytes ? JS_NewStringCopyZ(cx, bytes) : NULL; |
| return escstr; |
| } |
| |
| /************************************************************************/ |
| |
| static int ReconstructPCStack(JSContext*, JSScript*, jsbytecode*, jsbytecode**); |
| |
| static unsigned |
| StackDepth(JSScript *script) |
| { |
| return script->nslots - script->nfixed; |
| } |
| |
| static JSObject * |
| GetBlockChainAtPC(JSContext *cx, JSScript *script, jsbytecode *pc) |
| { |
| jsbytecode *start = script->main(); |
| |
| JS_ASSERT(pc >= start && pc < script->code + script->length); |
| |
| JSObject *blockChain = NULL; |
| for (jsbytecode *p = start; p < pc; p += GetBytecodeLength(p)) { |
| JSOp op = JSOp(*p); |
| |
| switch (op) { |
| case JSOP_ENTERBLOCK: |
| case JSOP_ENTERLET0: |
| case JSOP_ENTERLET1: { |
| JSObject *child = script->getObject(p); |
| JS_ASSERT_IF(blockChain, child->as<BlockObject>().stackDepth() >= |
| blockChain->as<BlockObject>().stackDepth()); |
| blockChain = child; |
| break; |
| } |
| case JSOP_LEAVEBLOCK: |
| case JSOP_LEAVEBLOCKEXPR: |
| case JSOP_LEAVEFORLETIN: { |
| // Some LEAVEBLOCK instructions are due to early exits via |
| // return/break/etc. from block-scoped loops and functions. We |
| // should ignore these instructions, since they don't really signal |
| // the end of the block. |
| jssrcnote *sn = js_GetSrcNote(cx, script, p); |
| if (!(sn && SN_TYPE(sn) == SRC_HIDDEN)) { |
| JS_ASSERT(blockChain); |
| blockChain = blockChain->as<StaticBlockObject>().enclosingBlock(); |
| JS_ASSERT_IF(blockChain, blockChain->is<BlockObject>()); |
| } |
| break; |
| } |
| default: |
| break; |
| } |
| } |
| |
| return blockChain; |
| } |
| |
| class PCStack |
| { |
| jsbytecode **stack; |
| int depth_; |
| |
| public: |
| PCStack() : stack(NULL), depth_(0) {} |
| ~PCStack(); |
| bool init(JSContext *cx, JSScript *script, jsbytecode *pc); |
| int depth() const { return depth_; } |
| jsbytecode *operator[](int i) const; |
| }; |
| |
| PCStack::~PCStack() |
| { |
| js_free(stack); |
| } |
| |
| bool |
| PCStack::init(JSContext *cx, JSScript *script, jsbytecode *pc) |
| { |
| stack = cx->pod_malloc<jsbytecode*>(StackDepth(script)); |
| if (!stack) |
| return false; |
| depth_ = ReconstructPCStack(cx, script, pc, stack); |
| JS_ASSERT(depth_ >= 0); |
| return true; |
| } |
| |
| /* Indexes the pcstack. */ |
| jsbytecode * |
| PCStack::operator[](int i) const |
| { |
| if (i < 0) { |
| i += depth_; |
| JS_ASSERT(i >= 0); |
| } |
| JS_ASSERT(i < depth_); |
| return stack[i]; |
| } |
| |
| /* |
| * The expression decompiler is invoked by error handling code to produce a |
| * string representation of the erroring expression. As it's only a debugging |
| * tool, it only supports basic expressions. For anything complicated, it simply |
| * puts "(intermediate value)" into the error result. |
| * |
| * Here's the basic algorithm: |
| * |
| * 1. Find the stack location of the value whose expression we wish to |
| * decompile. The error handler can explicitly pass this as an |
| * argument. Otherwise, we search backwards down the stack for the offending |
| * value. |
| * |
| * 2. Call ReconstructPCStack with the current frame's pc. This creates a stack |
| * of pcs parallel to the interpreter stack; given an interpreter stack |
| * location, the corresponding pc stack location contains the opcode that pushed |
| * the value in the interpreter. Now, with the result of step 1, we have the |
| * opcode responsible for pushing the value we want to decompile. |
| * |
| * 3. Pass the opcode to decompilePC. decompilePC is the main decompiler |
| * routine, responsible for a string representation of the expression that |
| * generated a certain stack location. decompilePC looks at one opcode and |
| * returns the JS source equivalent of that opcode. |
| * |
| * 4. Expressions can, of course, contain subexpressions. For example, the |
| * literals "4" and "5" are subexpressions of the addition operator in "4 + |
| * 5". If we need to decompile a subexpression, we call decompilePC (step 2) |
| * recursively on the operands' pcs. The result is a depth-first traversal of |
| * the expression tree. |
| * |
| */ |
| struct ExpressionDecompiler |
| { |
| JSContext *cx; |
| StackFrame *fp; |
| RootedScript script; |
| RootedFunction fun; |
| BindingVector *localNames; |
| Sprinter sprinter; |
| |
| ExpressionDecompiler(JSContext *cx, JSScript *script, JSFunction *fun) |
| : cx(cx), |
| script(cx, script), |
| fun(cx, fun), |
| localNames(NULL), |
| sprinter(cx) |
| {} |
| ~ExpressionDecompiler(); |
| bool init(); |
| bool decompilePC(jsbytecode *pc); |
| JSAtom *getVar(unsigned slot); |
| JSAtom *getArg(unsigned slot); |
| JSAtom *findLetVar(jsbytecode *pc, unsigned depth); |
| JSAtom *loadAtom(jsbytecode *pc); |
| bool quote(JSString *s, uint32_t quote); |
| bool write(const char *s); |
| bool write(JSString *str); |
| bool getOutput(char **out); |
| }; |
| |
| bool |
| ExpressionDecompiler::decompilePC(jsbytecode *pc) |
| { |
| JS_ASSERT(script->code <= pc && pc < script->code + script->length); |
| |
| PCStack pcstack; |
| if (!pcstack.init(cx, script, pc)) |
| return false; |
| |
| JSOp op = (JSOp)*pc; |
| |
| if (const char *token = CodeToken[op]) { |
| // Handle simple cases of binary and unary operators. |
| switch (js_CodeSpec[op].nuses) { |
| case 2: { |
| jssrcnote *sn = js_GetSrcNote(cx, script, pc); |
| if (!sn || SN_TYPE(sn) != SRC_ASSIGNOP) |
| return write("(") && |
| decompilePC(pcstack[-2]) && |
| write(" ") && |
| write(token) && |
| write(" ") && |
| decompilePC(pcstack[-1]) && |
| write(")"); |
| break; |
| } |
| case 1: |
| return write(token) && |
| write("(") && |
| decompilePC(pcstack[-1]) && |
| write(")"); |
| default: |
| break; |
| } |
| } |
| |
| switch (op) { |
| case JSOP_GETGNAME: |
| case JSOP_CALLGNAME: |
| case JSOP_NAME: |
| case JSOP_CALLNAME: |
| return write(loadAtom(pc)); |
| case JSOP_GETARG: |
| case JSOP_CALLARG: { |
| unsigned slot = GET_ARGNO(pc); |
| JSAtom *atom = getArg(slot); |
| return write(atom); |
| } |
| case JSOP_GETLOCAL: |
| case JSOP_CALLLOCAL: { |
| unsigned i = GET_SLOTNO(pc); |
| JSAtom *atom; |
| if (i >= script->nfixed) { |
| i -= script->nfixed; |
| JS_ASSERT(i < unsigned(pcstack.depth())); |
| atom = findLetVar(pc, i); |
| if (!atom) |
| return decompilePC(pcstack[i]); // Destructing temporary |
| } else { |
| atom = getVar(i); |
| } |
| JS_ASSERT(atom); |
| return write(atom); |
| } |
| case JSOP_CALLALIASEDVAR: |
| case JSOP_GETALIASEDVAR: { |
| JSAtom *atom = ScopeCoordinateName(cx, script, pc); |
| JS_ASSERT(atom); |
| return write(atom); |
| } |
| case JSOP_LENGTH: |
| case JSOP_GETPROP: |
| case JSOP_CALLPROP: { |
| RootedAtom prop(cx, (op == JSOP_LENGTH) ? cx->names().length : loadAtom(pc)); |
| if (!decompilePC(pcstack[-1])) |
| return false; |
| if (IsIdentifier(prop)) { |
| return write(".") && |
| quote(prop, '\0'); |
| } |
| return write("[") && |
| quote(prop, '\'') && |
| write("]"); |
| } |
| case JSOP_GETELEM: |
| case JSOP_CALLELEM: |
| return decompilePC(pcstack[-2]) && |
| write("[") && |
| decompilePC(pcstack[-1]) && |
| write("]"); |
| case JSOP_NULL: |
| return write(js_null_str); |
| case JSOP_TRUE: |
| return write(js_true_str); |
| case JSOP_FALSE: |
| return write(js_false_str); |
| case JSOP_ZERO: |
| case JSOP_ONE: |
| case JSOP_INT8: |
| case JSOP_UINT16: |
| case JSOP_UINT24: |
| case JSOP_INT32: |
| return sprinter.printf("%d", GetBytecodeInteger(pc)) >= 0; |
| case JSOP_STRING: |
| return quote(loadAtom(pc), '"'); |
| case JSOP_UNDEFINED: |
| return write(js_undefined_str); |
| case JSOP_THIS: |
| // |this| could convert to a very long object initialiser, so cite it by |
| // its keyword name. |
| return write(js_this_str); |
| case JSOP_CALL: |
| case JSOP_FUNCALL: |
| return decompilePC(pcstack[-int32_t(GET_ARGC(pc) + 2)]) && |
| write("(...)"); |
| case JSOP_NEWARRAY: |
| return write("[]"); |
| case JSOP_REGEXP: |
| case JSOP_OBJECT: { |
| JSObject *obj = (op == JSOP_REGEXP) |
| ? script->getRegExp(GET_UINT32_INDEX(pc)) |
| : script->getObject(GET_UINT32_INDEX(pc)); |
| JSString *str = ValueToSource(cx, ObjectValue(*obj)); |
| if (!str) |
| return false; |
| return write(str); |
| } |
| default: |
| break; |
| } |
| return write("(intermediate value)"); |
| } |
| |
| ExpressionDecompiler::~ExpressionDecompiler() |
| { |
| js_delete<BindingVector>(localNames); |
| } |
| |
| bool |
| ExpressionDecompiler::init() |
| { |
| assertSameCompartment(cx, script); |
| |
| if (!sprinter.init()) |
| return false; |
| |
| localNames = cx->new_<BindingVector>(cx); |
| if (!localNames) |
| return false; |
| RootedScript script_(cx, script); |
| if (!FillBindingVector(script_, localNames)) |
| return false; |
| |
| return true; |
| } |
| |
| bool |
| ExpressionDecompiler::write(const char *s) |
| { |
| return sprinter.put(s) >= 0; |
| } |
| |
| bool |
| ExpressionDecompiler::write(JSString *str) |
| { |
| return sprinter.putString(str) >= 0; |
| } |
| |
| bool |
| ExpressionDecompiler::quote(JSString *s, uint32_t quote) |
| { |
| return QuoteString(&sprinter, s, quote) != NULL; |
| } |
| |
| JSAtom * |
| ExpressionDecompiler::loadAtom(jsbytecode *pc) |
| { |
| return script->getAtom(GET_UINT32_INDEX(pc)); |
| } |
| |
| JSAtom * |
| ExpressionDecompiler::findLetVar(jsbytecode *pc, unsigned depth) |
| { |
| if (script->hasObjects()) { |
| JSObject *chain = GetBlockChainAtPC(cx, script, pc); |
| if (!chain) |
| return NULL; |
| JS_ASSERT(chain->is<BlockObject>()); |
| do { |
| BlockObject &block = chain->as<BlockObject>(); |
| uint32_t blockDepth = block.stackDepth(); |
| uint32_t blockCount = block.slotCount(); |
| if (uint32_t(depth - blockDepth) < uint32_t(blockCount)) { |
| for (Shape::Range<NoGC> r(block.lastProperty()); !r.empty(); r.popFront()) { |
| const Shape &shape = r.front(); |
| if (shape.shortid() == int(depth - blockDepth)) |
| return JSID_TO_ATOM(shape.propid()); |
| } |
| } |
| chain = chain->getParent(); |
| } while (chain && chain->is<BlockObject>()); |
| } |
| return NULL; |
| } |
| |
| JSAtom * |
| ExpressionDecompiler::getArg(unsigned slot) |
| { |
| JS_ASSERT(fun); |
| JS_ASSERT(slot < script->bindings.count()); |
| return (*localNames)[slot].name(); |
| } |
| |
| JSAtom * |
| ExpressionDecompiler::getVar(unsigned slot) |
| { |
| JS_ASSERT(fun); |
| slot += fun->nargs; |
| JS_ASSERT(slot < script->bindings.count()); |
| return (*localNames)[slot].name(); |
| } |
| |
| bool |
| ExpressionDecompiler::getOutput(char **res) |
| { |
| ptrdiff_t len = sprinter.stringEnd() - sprinter.stringAt(0); |
| *res = cx->pod_malloc<char>(len + 1); |
| if (!*res) |
| return false; |
| js_memcpy(*res, sprinter.stringAt(0), len); |
| (*res)[len] = 0; |
| return true; |
| } |
| |
| static bool |
| FindStartPC(JSContext *cx, ScriptFrameIter &iter, int spindex, int skipStackHits, Value v, |
| jsbytecode **valuepc) |
| { |
| jsbytecode *current = *valuepc; |
| |
| if (spindex == JSDVG_IGNORE_STACK) |
| return true; |
| |
| /* |
| * Fall back on *valuepc as start pc if this frame is calling .apply and |
| * the methodjit has "splatted" the arguments array, bumping the caller's |
| * stack pointer and skewing it from what static analysis in pcstack.init |
| * would compute. |
| * |
| * FIXME: also fall back if iter.isIon(), since the stack snapshot may be |
| * for the previous pc (see bug 831120). |
| */ |
| if (iter.isIon()) |
| return true; |
| |
| *valuepc = NULL; |
| |
| PCStack pcstack; |
| if (!pcstack.init(cx, iter.script(), current)) |
| return false; |
| |
| if (spindex < 0 && spindex + pcstack.depth() < 0) |
| spindex = JSDVG_SEARCH_STACK; |
| |
| if (spindex == JSDVG_SEARCH_STACK) { |
| size_t index = iter.numFrameSlots(); |
| JS_ASSERT(index >= size_t(pcstack.depth())); |
| |
| // We search from fp->sp to base to find the most recently calculated |
| // value matching v under assumption that it is the value that caused |
| // the exception. |
| int stackHits = 0; |
| Value s; |
| do { |
| if (!index) |
| return true; |
| s = iter.frameSlotValue(--index); |
| } while (s != v || stackHits++ != skipStackHits); |
| |
| // If index is out of bounds in pcstack, the blamed value must be one |
| // pushed by the current bytecode, so restore *valuepc. |
| if (index < size_t(pcstack.depth())) |
| *valuepc = pcstack[index]; |
| else |
| *valuepc = current; |
| } else { |
| *valuepc = pcstack[spindex]; |
| } |
| return true; |
| } |
| |
| static bool |
| DecompileExpressionFromStack(JSContext *cx, int spindex, int skipStackHits, HandleValue v, char **res) |
| { |
| JS_ASSERT(spindex < 0 || |
| spindex == JSDVG_IGNORE_STACK || |
| spindex == JSDVG_SEARCH_STACK); |
| |
| *res = NULL; |
| |
| #ifdef JS_MORE_DETERMINISTIC |
| /* |
| * Give up if we need deterministic behavior for differential testing. |
| * IonMonkey doesn't use StackFrames and this ensures we get the same |
| * error messages. |
| */ |
| return true; |
| #endif |
| |
| ScriptFrameIter frameIter(cx); |
| |
| if (frameIter.done()) |
| return true; |
| |
| RootedScript script(cx, frameIter.script()); |
| AutoCompartment ac(cx, &script->global()); |
| jsbytecode *valuepc = frameIter.pc(); |
| RootedFunction fun(cx, frameIter.isFunctionFrame() |
| ? frameIter.callee() |
| : NULL); |
| |
| JS_ASSERT(script->code <= valuepc && valuepc < script->code + script->length); |
| |
| // Give up if in prologue. |
| if (valuepc < script->main()) |
| return true; |
| |
| if (!FindStartPC(cx, frameIter, spindex, skipStackHits, v, &valuepc)) |
| return false; |
| if (!valuepc) |
| return true; |
| |
| ExpressionDecompiler ed(cx, script, fun); |
| if (!ed.init()) |
| return false; |
| if (!ed.decompilePC(valuepc)) |
| return false; |
| |
| return ed.getOutput(res); |
| } |
| |
| char * |
| js::DecompileValueGenerator(JSContext *cx, int spindex, HandleValue v, |
| HandleString fallbackArg, int skipStackHits) |
| { |
| RootedString fallback(cx, fallbackArg); |
| { |
| char *result; |
| if (!DecompileExpressionFromStack(cx, spindex, skipStackHits, v, &result)) |
| return NULL; |
| if (result) { |
| if (strcmp(result, "(intermediate value)")) |
| return result; |
| js_free(result); |
| } |
| } |
| if (!fallback) { |
| if (v.isUndefined()) |
| return JS_strdup(cx, js_undefined_str); // Prevent users from seeing "(void 0)" |
| fallback = ValueToSource(cx, v); |
| if (!fallback) |
| return NULL; |
| } |
| |
| Rooted<JSLinearString *> linear(cx, fallback->ensureLinear(cx)); |
| if (!linear) |
| return NULL; |
| TwoByteChars tbchars(linear->chars(), linear->length()); |
| return LossyTwoByteCharsToNewLatin1CharsZ(cx, tbchars).c_str(); |
| } |
| |
| static bool |
| DecompileArgumentFromStack(JSContext *cx, int formalIndex, char **res) |
| { |
| JS_ASSERT(formalIndex >= 0); |
| |
| *res = NULL; |
| |
| #ifdef JS_MORE_DETERMINISTIC |
| /* See note in DecompileExpressionFromStack. */ |
| return true; |
| #endif |
| |
| /* |
| * Settle on the nearest script frame, which should be the builtin that |
| * called the intrinsic. |
| */ |
| ScriptFrameIter frameIter(cx); |
| JS_ASSERT(!frameIter.done()); |
| |
| /* |
| * Get the second-to-top frame, the caller of the builtin that called the |
| * intrinsic. |
| */ |
| ++frameIter; |
| if (frameIter.done()) |
| return true; |
| |
| RootedScript script(cx, frameIter.script()); |
| AutoCompartment ac(cx, &script->global()); |
| jsbytecode *current = frameIter.pc(); |
| RootedFunction fun(cx, frameIter.isFunctionFrame() |
| ? frameIter.callee() |
| : NULL); |
| |
| JS_ASSERT(script->code <= current && current < script->code + script->length); |
| |
| if (current < script->main()) |
| return true; |
| |
| /* Don't handle getters, setters or calls from fun.call/fun.apply. */ |
| if (JSOp(*current) != JSOP_CALL || static_cast<unsigned>(formalIndex) >= GET_ARGC(current)) |
| return true; |
| |
| PCStack pcStack; |
| if (!pcStack.init(cx, script, current)) |
| return false; |
| |
| int formalStackIndex = pcStack.depth() - GET_ARGC(current) + formalIndex; |
| JS_ASSERT(formalStackIndex >= 0); |
| if (formalStackIndex >= pcStack.depth()) |
| return true; |
| |
| ExpressionDecompiler ed(cx, script, fun); |
| if (!ed.init()) |
| return false; |
| if (!ed.decompilePC(pcStack[formalStackIndex])) |
| return false; |
| |
| return ed.getOutput(res); |
| } |
| |
| char * |
| js::DecompileArgument(JSContext *cx, int formalIndex, HandleValue v) |
| { |
| { |
| char *result; |
| if (!DecompileArgumentFromStack(cx, formalIndex, &result)) |
| return NULL; |
| if (result) { |
| if (strcmp(result, "(intermediate value)")) |
| return result; |
| js_free(result); |
| } |
| } |
| if (v.isUndefined()) |
| return JS_strdup(cx, js_undefined_str); // Prevent users from seeing "(void 0)" |
| RootedString fallback(cx, ValueToSource(cx, v)); |
| if (!fallback) |
| return NULL; |
| |
| Rooted<JSLinearString *> linear(cx, fallback->ensureLinear(cx)); |
| if (!linear) |
| return NULL; |
| return LossyTwoByteCharsToNewLatin1CharsZ(cx, linear->range()).c_str(); |
| } |
| |
| unsigned |
| js_ReconstructStackDepth(JSContext *cx, JSScript *script, jsbytecode *pc) |
| { |
| return ReconstructPCStack(cx, script, pc, NULL); |
| } |
| |
| #define LOCAL_ASSERT_CUSTOM(expr, BAD_EXIT) \ |
| JS_BEGIN_MACRO \ |
| JS_ASSERT(expr); \ |
| if (!(expr)) { BAD_EXIT; } \ |
| JS_END_MACRO |
| #define LOCAL_ASSERT_RV(expr, rv) \ |
| LOCAL_ASSERT_CUSTOM(expr, return (rv)) |
| #define LOCAL_ASSERT(expr) LOCAL_ASSERT_RV(expr, -1); |
| |
| static int |
| SimulateOp(JSScript *script, JSOp op, const JSCodeSpec *cs, |
| jsbytecode *pc, jsbytecode **pcstack, unsigned &pcdepth) |
| { |
| unsigned nuses = StackUses(script, pc); |
| unsigned ndefs = StackDefs(script, pc); |
| LOCAL_ASSERT(pcdepth >= nuses); |
| pcdepth -= nuses; |
| LOCAL_ASSERT(pcdepth + ndefs <= StackDepth(script)); |
| |
| /* |
| * Fill the slots that the opcode defines withs its pc unless it just |
| * reshuffles the stack. In the latter case we want to preserve the |
| * opcode that generated the original value. |
| */ |
| switch (op) { |
| default: |
| if (pcstack) { |
| for (unsigned i = 0; i != ndefs; ++i) |
| pcstack[pcdepth + i] = pc; |
| } |
| break; |
| |
| case JSOP_CASE: |
| /* Keep the switch value. */ |
| JS_ASSERT(ndefs == 1); |
| break; |
| |
| case JSOP_DUP: |
| JS_ASSERT(ndefs == 2); |
| if (pcstack) |
| pcstack[pcdepth + 1] = pcstack[pcdepth]; |
| break; |
| |
| case JSOP_DUP2: |
| JS_ASSERT(ndefs == 4); |
| if (pcstack) { |
| pcstack[pcdepth + 2] = pcstack[pcdepth]; |
| pcstack[pcdepth + 3] = pcstack[pcdepth + 1]; |
| } |
| break; |
| |
| case JSOP_SWAP: |
| JS_ASSERT(ndefs == 2); |
| if (pcstack) { |
| jsbytecode *tmp = pcstack[pcdepth + 1]; |
| pcstack[pcdepth + 1] = pcstack[pcdepth]; |
| pcstack[pcdepth] = tmp; |
| } |
| break; |
| } |
| pcdepth += ndefs; |
| return pcdepth; |
| } |
| |
| /* Ensure that script analysis reports the same stack depth. */ |
| static void |
| AssertPCDepth(JSScript *script, jsbytecode *pc, unsigned pcdepth) { |
| /* |
| * If this assertion fails, run the failing test case under gdb and use the |
| * following gdb command to understand the execution path of this function. |
| * |
| * call js_DumpScriptDepth(cx, script, pc) |
| */ |
| JS_ASSERT_IF(script->hasAnalysis() && script->analysis()->maybeCode(pc), |
| script->analysis()->getCode(pc).stackDepth == pcdepth); |
| } |
| |
| static int |
| ReconstructPCStack(JSContext *cx, JSScript *script, jsbytecode *target, jsbytecode **pcstack) |
| { |
| /* |
| * Walk forward from script->code and compute the stack depth and stack of |
| * operand-generating opcode PCs in pcstack. |
| * |
| * Every instruction has a statically computable stack depth before and |
| * after. This function returns the stack depth before the target |
| * instruction. |
| * |
| * The stack depth can be computed from these three common-sense rules... |
| * |
| * - The stack depth before the first instruction of the script is 0. |
| * |
| * - The stack depth after an instruction is always simply the stack |
| * depth before, plus whatever effect of the instruction has. |
| * SimulateOp computes this. |
| * |
| * - Except for three specific cases listed below, the stack depth before |
| * an instruction is simply the stack depth after the preceding one. |
| * |
| * ... and these three less obvious rules: |
| * |
| * - Special case 1: break, continue, or return statements that exit |
| * certain kinds of blocks. |
| * |
| * Rule: The stack depth before the next instruction (following the |
| * GOTO/RETRVAL) equals the stack depth before the first SRC_HIDDEN |
| * instruction. |
| * |
| * - Special case 2: The rethrow at the end of conditional catch blocks. |
| * |
| * Rule: The stack depth before the next rethrow instruction of a |
| * conditional catch block equals the stack depth at the end of the IFEQ |
| * of the catch condition (which is jumping to this re-throw). |
| * |
| * - Special case 3: Conditional expressions (A ? B : C). |
| * |
| * Rule: The code for B and C are pushing a result on the stack, so the |
| * stack depth before C equals the stack depth before B, which is one |
| * less than the stack depth after the GOTO ending B. |
| */ |
| |
| LOCAL_ASSERT(script->code <= target && target < script->code + script->length); |
| |
| /* |
| * Save depth of hidden instructions to recover the depth if a branch is not |
| * taken. |
| */ |
| unsigned hpcdepth = unsigned(-1); |
| |
| /* |
| * Save depth of catch instructions to recover it in the rethrow path, at |
| * the end of a conditional catch. |
| */ |
| unsigned cpcdepth = unsigned(-1); |
| |
| jsbytecode *pc; |
| unsigned pcdepth; |
| ptrdiff_t oplen; |
| for (pc = script->code, pcdepth = 0; ; pc += oplen) { |
| JSOp op = JSOp(*pc); |
| oplen = GetBytecodeLength(pc); |
| const JSCodeSpec *cs = &js_CodeSpec[op]; |
| jssrcnote *sn = js_GetSrcNote(cx, script, pc); |
| |
| /* |
| * *** Special case 1.a *** |
| * |
| * Hidden instructions are used to pop scoped variables (leaveblock) in |
| * early-exit paths and are not supposed to affect the stack depth when |
| * they are not taken. However, when the target is in an early-exit |
| * path, hidden instructions need to be taken into account. |
| * |
| * As we are not following any branch, we need to restore the stack |
| * depth after any instruction which is breaking the flow of the |
| * program. Sequences of hidden instructions are *included* in the |
| * following grammar: |
| * |
| * ExitSequence: |
| * EarlyExit // return / break / continue |
| * | ConditionalCatchExit // conditional catch-block exit |
| * |
| * EarlyExit: |
| * setrval; ExitSegment* retrval; // return stmt |
| * | ExitSegment* goto; // break/continue stmt |
| * |
| * ConditionalCatchExit: |
| * ExitSegment* hidden goto; CatchRethrow |
| * |
| * CatchRethrow: |
| * hidden throw; end-brace nop; // last catch-block |
| * | hidden throw; finally; // followed by a finally |
| * | hidden throwing; hidden leaveblock; catch enterblock; |
| * // followed by a catch-block |
| * |
| * ExitSegment: |
| * hidden leaveblock; // leave let-block or |
| * // leave catch-block |
| * | hidden leavewith; // leave with-block |
| * | hidden gosub; // leave try or |
| * // leave catch-block with finally |
| * | hidden enditer; // leave for-in/of block |
| * | hidden leaveforletin; hidden enditer; hidden popn; |
| * // leave for-let-in/of block |
| * |
| * The following code save the depth of the first hidden |
| * instruction. When we hit either the last instruction of the EarlyExit |
| * (retrval/goto) or the goto of a ConditionalCacthExit, we restore (see |
| * Special case 1.b) the depth after the execution of the instruction. |
| * |
| * In case of a ConditionalCacthExit, we keep the hidden depth after the |
| * hidden goto to again restore (see Special case 1.b) the depth after |
| * the throw/throwing instruction of the CatchRethrow. |
| * |
| * Note: The last instructions of the EarlyExit, such as break, continue |
| * and return are not hidden as they are part of the original sources, |
| * so we need to consider them as-if they were hidden instruction (do |
| * not reset the depth) and restore the depth after their execution. It |
| * is important to restore them after their execution, and not at the |
| * next instruction, because they might be followed by another sequence |
| * of hidden instruction. |
| */ |
| bool exitPath = |
| op == JSOP_GOTO || |
| op == JSOP_RETRVAL || |
| op == JSOP_THROW; |
| |
| bool isConditionalCatchExit = false; |
| |
| if (sn && SN_TYPE(sn) == SRC_HIDDEN) { |
| /* Only ConditionalCatchExit's goto are hidden. */ |
| isConditionalCatchExit = op == JSOP_GOTO; |
| if (hpcdepth == unsigned(-1)) |
| hpcdepth = pcdepth; |
| } else if (!exitPath) { |
| hpcdepth = unsigned(-1); |
| } |
| |
| |
| /* |
| * *** Special case 2 *** |
| * |
| * The catch depth is used to restore the depth expected in case of |
| * rethrow or in case of guard failure. The depth expected by throw and |
| * throwing instructions correspond to the result of enterblock and |
| * exception instructions. |
| * |
| * enterblock depth d <-- depth += d |
| * exception <-- depth += 1 |
| * |
| * This is not a problem, except with conditional catch staements. |
| * Conditional catch statements are looking like: |
| * |
| * ifeq +... <-- jump to the rethrow path. |
| * pop <-- pop the exception value. |
| * |
| * Unfortunately, we iterate linearly over the bytecode, so we cannot |
| * use a stack to save the depth for each catch statement that we |
| * encounter. The trick done here is to save the stack depth before we |
| * unwind the scoped variables with leaveblock, and restore the stack |
| * depth plus one to account for the missing exception value. |
| * |
| * {Catch} leaveblock d <-- save depth |
| * ( gosub ... )? |
| * {Hidden} goto ... |
| * ( {Hidden} throw )? <-- restore depth + 1 |
| * |
| * If there is no rethrow, then we should expect an end-brace nop or a |
| * finally. |
| */ |
| if (op == JSOP_LEAVEBLOCK && sn && SN_TYPE(sn) == SRC_CATCH) { |
| LOCAL_ASSERT(cpcdepth == unsigned(-1)); |
| cpcdepth = pcdepth; |
| } else if (sn && SN_TYPE(sn) == SRC_HIDDEN && |
| (op == JSOP_THROW || op == JSOP_THROWING)) |
| { |
| /* |
| * The current catch block is conditional, we compensate for the pop |
| * of the exception added after the ifeq by adding 1 to the restored |
| * depth. This fake slot might not contain the right value, but |
| * it will be eliminated with the throw or throwing instruction. |
| */ |
| LOCAL_ASSERT(cpcdepth != unsigned(-1)); |
| pcdepth = cpcdepth + 1; |
| cpcdepth = unsigned(-1); |
| } else if (!(op == JSOP_GOTO && sn && SN_TYPE(sn) == SRC_HIDDEN) && |
| !(op == JSOP_GOSUB && cpcdepth != unsigned(-1))) |
| { |
| /* |
| * We need to ignore gosub and goto when we exit a catch block. |
| * Otherwise we might reset the catch depth before hitting the throw |
| * or throwing instruction. |
| * |
| * {Catch} leaveblock d |
| * ( gosub ... )? |
| * {Hidden} goto ... |
| * ( {Hidden} throw )? |
| * finally |
| * |
| * If there is no rethrow, the end-brace nop or the finally |
| * instruction will reset the saved depth. |
| */ |
| if (cpcdepth != unsigned(-1)) |
| LOCAL_ASSERT(op == JSOP_NOP || op == JSOP_FINALLY); |
| cpcdepth = unsigned(-1); |
| } |
| |
| /* At this point, pcdepth is the stack depth *before* the insn at pc. */ |
| AssertPCDepth(script, pc, pcdepth); |
| if (pc >= target) |
| break; |
| |
| /* Simulate the instruction at pc. */ |
| if (SimulateOp(script, op, cs, pc, pcstack, pcdepth) < 0) |
| return -1; |
| |
| /* At this point, pcdepth is the stack depth *after* the insn at pc. */ |
| |
| /* |
| * *** Special case 1.b *** |
| * |
| * Restore the stack depth after the execution of an EarlyExit or the |
| * goto of the ConditionalCatchExit. |
| */ |
| if (exitPath && hpcdepth != unsigned(-1)) { |
| pcdepth = hpcdepth; |
| |
| /* Keep the depth if we are on the ConditionalCatchExit path. */ |
| if (!isConditionalCatchExit) |
| hpcdepth = unsigned(-1); |
| } |
| |
| /* |
| * *** Special case 3 *** |
| * |
| * A (C ? T : E) expression requires skipping T if target is in E or |
| * after the whole expression, because this expression is pushing a |
| * result on the stack and the goto cannot be skipped. |
| * |
| * 09 001 pc : ifeq +11 |
| * 000 pc+ 5: null |
| * 001 pc+ 6: goto +... <-- The stack after the goto |
| * 000 pc+11: ... <-- does not have the same depth. |
| * |
| */ |
| if (sn && SN_TYPE(sn) == SRC_COND) { |
| ptrdiff_t jmplen = GET_JUMP_OFFSET(pc); |
| if (pc + jmplen <= target) { |
| /* Target does not lie in T. */ |
| oplen = jmplen; |
| } |
| } |
| } |
| LOCAL_ASSERT(pc == target); |
| AssertPCDepth(script, pc, pcdepth); |
| return pcdepth; |
| } |
| |
| #undef LOCAL_ASSERT |
| #undef LOCAL_ASSERT_RV |
| |
| bool |
| js::CallResultEscapes(jsbytecode *pc) |
| { |
| /* |
| * If we see any of these sequences, the result is unused: |
| * - call / pop |
| * |
| * If we see any of these sequences, the result is only tested for nullness: |
| * - call / ifeq |
| * - call / not / ifeq |
| */ |
| |
| if (*pc != JSOP_CALL) |
| return true; |
| |
| pc += JSOP_CALL_LENGTH; |
| |
| if (*pc == JSOP_POP) |
| return false; |
| |
| if (*pc == JSOP_NOT) |
| pc += JSOP_NOT_LENGTH; |
| |
| return (*pc != JSOP_IFEQ); |
| } |
| |
| extern bool |
| js::IsValidBytecodeOffset(JSContext *cx, JSScript *script, size_t offset) |
| { |
| // This could be faster (by following jump instructions if the target is <= offset). |
| for (BytecodeRange r(cx, script); !r.empty(); r.popFront()) { |
| size_t here = r.frontOffset(); |
| if (here >= offset) |
| return here == offset; |
| } |
| return false; |
| } |
| |
| JS_FRIEND_API(size_t) |
| js::GetPCCountScriptCount(JSContext *cx) |
| { |
| JSRuntime *rt = cx->runtime(); |
| |
| if (!rt->scriptAndCountsVector) |
| return 0; |
| |
| return rt->scriptAndCountsVector->length(); |
| } |
| |
| enum MaybeComma {NO_COMMA, COMMA}; |
| |
| static void |
| AppendJSONProperty(StringBuffer &buf, const char *name, MaybeComma comma = COMMA) |
| { |
| if (comma) |
| buf.append(','); |
| |
| buf.append('\"'); |
| buf.appendInflated(name, strlen(name)); |
| buf.appendInflated("\":", 2); |
| } |
| |
| static void |
| AppendArrayJSONProperties(JSContext *cx, StringBuffer &buf, |
| double *values, const char * const *names, unsigned count, |
| MaybeComma &comma) |
| { |
| for (unsigned i = 0; i < count; i++) { |
| if (values[i]) { |
| AppendJSONProperty(buf, names[i], comma); |
| comma = COMMA; |
| NumberValueToStringBuffer(cx, DoubleValue(values[i]), buf); |
| } |
| } |
| } |
| |
| JS_FRIEND_API(JSString *) |
| js::GetPCCountScriptSummary(JSContext *cx, size_t index) |
| { |
| JSRuntime *rt = cx->runtime(); |
| |
| if (!rt->scriptAndCountsVector || index >= rt->scriptAndCountsVector->length()) { |
| JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL, JSMSG_BUFFER_TOO_SMALL); |
| return NULL; |
| } |
| |
| const ScriptAndCounts &sac = (*rt->scriptAndCountsVector)[index]; |
| RootedScript script(cx, sac.script); |
| |
| /* |
| * OOM on buffer appends here will not be caught immediately, but since |
| * StringBuffer uses a ContextAllocPolicy will trigger an exception on the |
| * context if they occur, which we'll catch before returning. |
| */ |
| StringBuffer buf(cx); |
| |
| buf.append('{'); |
| |
| AppendJSONProperty(buf, "file", NO_COMMA); |
| JSString *str = JS_NewStringCopyZ(cx, script->filename()); |
| if (!str || !(str = ValueToSource(cx, StringValue(str)))) |
| return NULL; |
| buf.append(str); |
| |
| AppendJSONProperty(buf, "line"); |
| NumberValueToStringBuffer(cx, Int32Value(script->lineno), buf); |
| |
| if (script->function()) { |
| JSAtom *atom = script->function()->displayAtom(); |
| if (atom) { |
| AppendJSONProperty(buf, "name"); |
| if (!(str = ValueToSource(cx, StringValue(atom)))) |
| return NULL; |
| buf.append(str); |
| } |
| } |
| |
| double baseTotals[PCCounts::BASE_LIMIT] = {0.0}; |
| double accessTotals[PCCounts::ACCESS_LIMIT - PCCounts::BASE_LIMIT] = {0.0}; |
| double elementTotals[PCCounts::ELEM_LIMIT - PCCounts::ACCESS_LIMIT] = {0.0}; |
| double propertyTotals[PCCounts::PROP_LIMIT - PCCounts::ACCESS_LIMIT] = {0.0}; |
| double arithTotals[PCCounts::ARITH_LIMIT - PCCounts::BASE_LIMIT] = {0.0}; |
| |
| for (unsigned i = 0; i < script->length; i++) { |
| PCCounts &counts = sac.getPCCounts(script->code + i); |
| if (!counts) |
| continue; |
| |
| JSOp op = (JSOp)script->code[i]; |
| unsigned numCounts = PCCounts::numCounts(op); |
| |
| for (unsigned j = 0; j < numCounts; j++) { |
| double value = counts.get(j); |
| if (j < PCCounts::BASE_LIMIT) { |
| baseTotals[j] += value; |
| } else if (PCCounts::accessOp(op)) { |
| if (j < PCCounts::ACCESS_LIMIT) |
| accessTotals[j - PCCounts::BASE_LIMIT] += value; |
| else if (PCCounts::elementOp(op)) |
| elementTotals[j - PCCounts::ACCESS_LIMIT] += value; |
| else if (PCCounts::propertyOp(op)) |
| propertyTotals[j - PCCounts::ACCESS_LIMIT] += value; |
| else |
| JS_NOT_REACHED("Bad opcode"); |
| } else if (PCCounts::arithOp(op)) { |
| arithTotals[j - PCCounts::BASE_LIMIT] += value; |
| } else { |
| JS_NOT_REACHED("Bad opcode"); |
| } |
| } |
| } |
| |
| AppendJSONProperty(buf, "totals"); |
| buf.append('{'); |
| |
| MaybeComma comma = NO_COMMA; |
| |
| AppendArrayJSONProperties(cx, buf, baseTotals, countBaseNames, |
| JS_ARRAY_LENGTH(baseTotals), comma); |
| AppendArrayJSONProperties(cx, buf, accessTotals, countAccessNames, |
| JS_ARRAY_LENGTH(accessTotals), comma); |
| AppendArrayJSONProperties(cx, buf, elementTotals, countElementNames, |
| JS_ARRAY_LENGTH(elementTotals), comma); |
| AppendArrayJSONProperties(cx, buf, propertyTotals, countPropertyNames, |
| JS_ARRAY_LENGTH(propertyTotals), comma); |
| AppendArrayJSONProperties(cx, buf, arithTotals, countArithNames, |
| JS_ARRAY_LENGTH(arithTotals), comma); |
| |
| uint64_t ionActivity = 0; |
| jit::IonScriptCounts *ionCounts = sac.getIonCounts(); |
| while (ionCounts) { |
| for (size_t i = 0; i < ionCounts->numBlocks(); i++) |
| ionActivity += ionCounts->block(i).hitCount(); |
| ionCounts = ionCounts->previous(); |
| } |
| if (ionActivity) { |
| AppendJSONProperty(buf, "ion", comma); |
| NumberValueToStringBuffer(cx, DoubleValue(ionActivity), buf); |
| } |
| |
| buf.append('}'); |
| buf.append('}'); |
| |
| if (cx->isExceptionPending()) |
| return NULL; |
| |
| return buf.finishString(); |
| } |
| |
| static bool |
| GetPCCountJSON(JSContext *cx, const ScriptAndCounts &sac, StringBuffer &buf) |
| { |
| RootedScript script(cx, sac.script); |
| |
| buf.append('{'); |
| AppendJSONProperty(buf, "text", NO_COMMA); |
| |
| JSString *str = JS_DecompileScript(cx, script, NULL, 0); |
| if (!str || !(str = ValueToSource(cx, StringValue(str)))) |
| return false; |
| |
| buf.append(str); |
| |
| AppendJSONProperty(buf, "line"); |
| NumberValueToStringBuffer(cx, Int32Value(script->lineno), buf); |
| |
| AppendJSONProperty(buf, "opcodes"); |
| buf.append('['); |
| bool comma = false; |
| |
| SrcNoteLineScanner scanner(script->notes(), script->lineno); |
| |
| for (jsbytecode *pc = script->code; |
| pc < script->code + script->length; |
| pc += GetBytecodeLength(pc)) |
| { |
| size_t offset = pc - script->code; |
| |
| JSOp op = (JSOp) *pc; |
| |
| if (comma) |
| buf.append(','); |
| comma = true; |
| |
| buf.append('{'); |
| |
| AppendJSONProperty(buf, "id", NO_COMMA); |
| NumberValueToStringBuffer(cx, Int32Value(pc - script->code), buf); |
| |
| scanner.advanceTo(offset); |
| |
| AppendJSONProperty(buf, "line"); |
| NumberValueToStringBuffer(cx, Int32Value(scanner.getLine()), buf); |
| |
| { |
| const char *name = js_CodeName[op]; |
| AppendJSONProperty(buf, "name"); |
| buf.append('\"'); |
| buf.appendInflated(name, strlen(name)); |
| buf.append('\"'); |
| } |
| |
| { |
| ExpressionDecompiler ed(cx, script, script->function()); |
| if (!ed.init()) |
| return false; |
| if (!ed.decompilePC(pc)) |
| return false; |
| char *text; |
| if (!ed.getOutput(&text)) |
| return false; |
| AppendJSONProperty(buf, "text"); |
| JSString *str = JS_NewStringCopyZ(cx, text); |
| js_free(text); |
| if (!str || !(str = ValueToSource(cx, StringValue(str)))) |
| return false; |
| buf.append(str); |
| } |
| |
| PCCounts &counts = sac.getPCCounts(pc); |
| unsigned numCounts = PCCounts::numCounts(op); |
| |
| AppendJSONProperty(buf, "counts"); |
| buf.append('{'); |
| |
| MaybeComma comma = NO_COMMA; |
| for (unsigned i = 0; i < numCounts; i++) { |
| double value = counts.get(i); |
| if (value > 0) { |
| AppendJSONProperty(buf, PCCounts::countName(op, i), comma); |
| comma = COMMA; |
| NumberValueToStringBuffer(cx, DoubleValue(value), buf); |
| } |
| } |
| |
| buf.append('}'); |
| buf.append('}'); |
| } |
| |
| buf.append(']'); |
| |
| jit::IonScriptCounts *ionCounts = sac.getIonCounts(); |
| if (ionCounts) { |
| AppendJSONProperty(buf, "ion"); |
| buf.append('['); |
| bool comma = false; |
| while (ionCounts) { |
| if (comma) |
| buf.append(','); |
| comma = true; |
| |
| buf.append('['); |
| for (size_t i = 0; i < ionCounts->numBlocks(); i++) { |
| if (i) |
| buf.append(','); |
| const jit::IonBlockCounts &block = ionCounts->block(i); |
| |
| buf.append('{'); |
| AppendJSONProperty(buf, "id", NO_COMMA); |
| NumberValueToStringBuffer(cx, Int32Value(block.id()), buf); |
| AppendJSONProperty(buf, "offset"); |
| NumberValueToStringBuffer(cx, Int32Value(block.offset()), buf); |
| AppendJSONProperty(buf, "successors"); |
| buf.append('['); |
| for (size_t j = 0; j < block.numSuccessors(); j++) { |
| if (j) |
| buf.append(','); |
| NumberValueToStringBuffer(cx, Int32Value(block.successor(j)), buf); |
| } |
| buf.append(']'); |
| AppendJSONProperty(buf, "hits"); |
| NumberValueToStringBuffer(cx, DoubleValue(block.hitCount()), buf); |
| |
| AppendJSONProperty(buf, "code"); |
| JSString *str = JS_NewStringCopyZ(cx, block.code()); |
| if (!str || !(str = ValueToSource(cx, StringValue(str)))) |
| return false; |
| buf.append(str); |
| |
| AppendJSONProperty(buf, "instructionBytes"); |
| NumberValueToStringBuffer(cx, Int32Value(block.instructionBytes()), buf); |
| |
| AppendJSONProperty(buf, "spillBytes"); |
| NumberValueToStringBuffer(cx, Int32Value(block.spillBytes()), buf); |
| |
| buf.append('}'); |
| } |
| buf.append(']'); |
| |
| ionCounts = ionCounts->previous(); |
| } |
| buf.append(']'); |
| } |
| |
| buf.append('}'); |
| |
| return !cx->isExceptionPending(); |
| } |
| |
| JS_FRIEND_API(JSString *) |
| js::GetPCCountScriptContents(JSContext *cx, size_t index) |
| { |
| JSRuntime *rt = cx->runtime(); |
| |
| if (!rt->scriptAndCountsVector || index >= rt->scriptAndCountsVector->length()) { |
| JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL, JSMSG_BUFFER_TOO_SMALL); |
| return NULL; |
| } |
| |
| const ScriptAndCounts &sac = (*rt->scriptAndCountsVector)[index]; |
| JSScript *script = sac.script; |
| |
| StringBuffer buf(cx); |
| |
| if (!script->function() && !script->compileAndGo) |
| return buf.finishString(); |
| |
| { |
| AutoCompartment ac(cx, &script->global()); |
| if (!GetPCCountJSON(cx, sac, buf)) |
| return NULL; |
| } |
| |
| return buf.finishString(); |
| } |