diff --git a/src/bootstrapper.cc b/src/bootstrapper.cc index 27ba49e97c..ffd432a685 100644 --- a/src/bootstrapper.cc +++ b/src/bootstrapper.cc @@ -1213,6 +1213,17 @@ bool Genesis::InstallSpecialObjects() { Handle(js_global->builtins()), DONT_ENUM); } + if (FLAG_capture_stack_traces) { + Handle Error = GetProperty(js_global, "Error"); + if (Error->IsJSObject()) { + Handle name = Factory::LookupAsciiSymbol("captureStackTraces"); + SetProperty(Handle::cast(Error), + name, + Factory::true_value(), + NONE); + } + } + #ifdef ENABLE_DEBUGGER_SUPPORT // Expose the debug global object in global if a name for it is specified. if (FLAG_expose_debug_as != NULL && strlen(FLAG_expose_debug_as) != 0) { diff --git a/src/flag-definitions.h b/src/flag-definitions.h index 8110e12848..983fe2225f 100644 --- a/src/flag-definitions.h +++ b/src/flag-definitions.h @@ -110,6 +110,7 @@ DEFINE_string(expose_natives_as, NULL, "expose natives in global object") DEFINE_string(expose_debug_as, NULL, "expose debug in global object") DEFINE_string(natives_file, NULL, "alternative natives file") DEFINE_bool(expose_gc, false, "expose gc extension") +DEFINE_bool(capture_stack_traces, false, "capture stack traces") // builtins-ia32.cc DEFINE_bool(inline_new, true, "use fast inline allocation") diff --git a/src/macros.py b/src/macros.py index 49be54f7d0..c75f0ea487 100644 --- a/src/macros.py +++ b/src/macros.py @@ -91,6 +91,7 @@ macro IS_BOOLEAN_WRAPPER(arg) = (%_ClassOf(arg) === 'Boolean'); macro IS_ERROR(arg) = (%_ClassOf(arg) === 'Error'); macro IS_SCRIPT(arg) = (%_ClassOf(arg) === 'Script'); macro IS_ARGUMENTS(arg) = (%_ClassOf(arg) === 'Arguments'); +macro IS_GLOBAL(arg) = (%_ClassOf(arg) === 'global'); macro FLOOR(arg) = %Math_floor(arg); # Inline macros. Use %IS_VAR to make sure arg is evaluated only once. diff --git a/src/messages.js b/src/messages.js index bef5540f2b..882fed5e2e 100644 --- a/src/messages.js +++ b/src/messages.js @@ -557,55 +557,9 @@ function MakeMessage(type, args, startPos, endPos, script, stackTrace) { function GetStackTraceLine(recv, fun, pos, isGlobal) { - try { - return UnsafeGetStackTraceLine(recv, fun, pos, isGlobal); - } catch (e) { - return ""; - } + return FormatSourcePosition(new CallSite(recv, fun, pos)); } - -function GetFunctionName(fun, recv) { - var name = %FunctionGetName(fun); - if (name) return name; - for (var prop in recv) { - if (recv[prop] === fun) - return prop; - } - return "[anonymous]"; -} - - -function UnsafeGetStackTraceLine(recv, fun, pos, isTopLevel) { - var result = ""; - // The global frame has no meaningful function or receiver - if (!isTopLevel) { - // If the receiver is not the global object then prefix the - // message send - if (recv !== global) - result += ToDetailString(recv) + "."; - result += GetFunctionName(fun, recv); - } - if (pos != -1) { - var script = %FunctionGetScript(fun); - var file; - if (script) { - file = %FunctionGetScript(fun).data; - } - if (file) { - var location = %FunctionGetScript(fun).locationFromPosition(pos, true); - if (!isTopLevel) result += "("; - result += file; - if (location != null) { - result += ":" + (location.line + 1) + ":" + (location.column + 1); - } - if (!isTopLevel) result += ")"; - } - } - return (result) ? " at " + result : result; -} - - // ---------------------------------------------------------------------------- // Error implementation @@ -632,6 +586,197 @@ function DefineOneShotAccessor(obj, name, fun) { }); } +function CallSite(receiver, fun, pos) { + this.receiver = receiver; + this.fun = fun; + this.pos = pos; +} + +CallSite.prototype.getThis = function () { + return this.receiver; +}; + +CallSite.prototype.getTypeName = function () { + var constructor = this.receiver.constructor; + if (!constructor) + return $Object.prototype.toString.call(this.receiver); + var constructorName = constructor.name; + if (!constructorName) + return $Object.prototype.toString.call(this.receiver); + return constructorName; +}; + +CallSite.prototype.isToplevel = function () { + if (this.receiver == null) + return true; + var className = $Object.prototype.toString.call(this.receiver); + return IS_GLOBAL(this.receiver); +}; + +CallSite.prototype.isEval = function () { + var script = %FunctionGetScript(this.fun); + return script && script.compilation_type == 1; +}; + +CallSite.prototype.getEvalOrigin = function () { + var script = %FunctionGetScript(this.fun); + if (!script || script.compilation_type != 1) + return null; + return new CallSite(null, script.eval_from_function, + script.eval_from_position); +}; + +CallSite.prototype.getFunctionName = function () { + // See if the function knows its own name + var name = this.fun.name; + if (name) + return name; + // See if we can find a unique property on the receiver that holds + // this function. + for (var prop in this.receiver) { + if (this.receiver[prop] === this.fun) { + // If we find more than one match bail out to avoid confusion + if (name) + return null; + name = prop; + } + } + if (name) + return name; + // Maybe this is an evaluation? + var script = %FunctionGetScript(this.fun); + if (script && script.compilation_type == 1) + return "eval"; + return null; +}; + +CallSite.prototype.getFileName = function () { + var script = %FunctionGetScript(this.fun); + return script ? script.name : null; +}; + +CallSite.prototype.getLineNumber = function () { + if (this.pos == -1) + return null; + var script = %FunctionGetScript(this.fun); + var location = null; + if (script) { + location = script.locationFromPosition(this.pos, true); + } + return location ? location.line + 1 : null; +}; + +CallSite.prototype.getColumnNumber = function () { + if (this.pos == -1) + return null; + var script = %FunctionGetScript(this.fun); + var location = null; + if (script) { + location = script.locationFromPosition(this.pos, true); + } + return location ? location.column : null; +}; + +CallSite.prototype.isNative = function () { + var script = %FunctionGetScript(this.fun); + return script ? (script.type == 0) : false; +}; + +CallSite.prototype.getPosition = function () { + return this.pos; +}; + +CallSite.prototype.isConstructor = function () { + var constructor = this.receiver ? this.receiver.constructor : null; + if (!constructor) + return false; + return this.fun === constructor; +}; + +function FormatSourcePosition(frame) { + var fileLocation = ""; + if (frame.isNative()) { + fileLocation = "native"; + } else if (frame.isEval()) { + fileLocation = "eval at " + FormatSourcePosition(frame.getEvalOrigin()); + } else { + var fileName = frame.getFileName(); + if (fileName) { + fileLocation += fileName; + var lineNumber = frame.getLineNumber(); + if (lineNumber != null) { + fileLocation += ":" + lineNumber; + var columnNumber = frame.getColumnNumber(); + if (columnNumber) { + fileLocation += ":" + columnNumber; + } + } + } + } + if (!fileLocation) { + fileLocation = "unknown source"; + } + var line = ""; + var functionName = frame.getFunctionName(); + if (functionName) { + if (frame.isToplevel()) { + line += functionName; + } else if (frame.isConstructor()) { + line += "new " + functionName; + } else { + line += frame.getTypeName() + "." + functionName; + } + line += " (" + fileLocation + ")"; + } else { + line += fileLocation; + } + return line; +} + +function FormatStackTrace(error, frames) { + var lines = []; + try { + lines.push(error.toString()); + } catch (e) { + try { + lines.push(""); + } catch (ee) { + lines.push(""); + } + } + for (var i = 0; i < frames.length; i++) { + var frame = frames[i]; + try { + var line = FormatSourcePosition(frame); + } catch (e) { + try { + var line = ""; + } catch (ee) { + // Any code that reaches this point is seriously nasty! + var line = ""; + } + } + lines.push(" at " + line); + } + return lines.join("\n"); +} + +function FormatRawStackTrace(error, raw_stack) { + var frames = [ ]; + for (var i = 0; i < raw_stack.length; i += 3) { + var recv = raw_stack[i]; + var fun = raw_stack[i+1]; + var pc = raw_stack[i+2]; + var pos = %FunctionGetPositionForOffset(fun, pc); + frames.push(new CallSite(recv, fun, pos)); + } + if (IS_FUNCTION($Error.prepareStackTrace)) { + return $Error.prepareStackTrace(error, frames); + } else { + return FormatStackTrace(error, frames); + } +} + function DefineError(f) { // Store the error function in both the global object // and the runtime object. The function is fetched @@ -667,6 +812,12 @@ function DefineError(f) { } else if (!IS_UNDEFINED(m)) { this.message = ToString(m); } + if ($Error.captureStackTraces) { + var raw_stack = %CollectStackTrace(f); + DefineOneShotAccessor(this, 'stack', function (obj) { + return FormatRawStackTrace(obj, raw_stack); + }); + } } else { return new f(m); } diff --git a/src/objects.h b/src/objects.h index 6c83b24f1a..775b6c7e88 100644 --- a/src/objects.h +++ b/src/objects.h @@ -2685,16 +2685,16 @@ class Script: public Struct { public: // Script types. enum Type { - TYPE_NATIVE, - TYPE_EXTENSION, - TYPE_NORMAL + TYPE_NATIVE = 0, + TYPE_EXTENSION = 1, + TYPE_NORMAL = 2 }; // Script compilation types. enum CompilationType { - COMPILATION_TYPE_HOST, - COMPILATION_TYPE_EVAL, - COMPILATION_TYPE_JSON + COMPILATION_TYPE_HOST = 0, + COMPILATION_TYPE_EVAL = 1, + COMPILATION_TYPE_JSON = 2 }; // [source]: the script source. diff --git a/src/runtime.cc b/src/runtime.cc index f10899f2e0..4adc2b91bb 100644 --- a/src/runtime.cc +++ b/src/runtime.cc @@ -1107,6 +1107,21 @@ static Object* Runtime_FunctionGetScriptSourcePosition(Arguments args) { } +static Object* Runtime_FunctionGetPositionForOffset(Arguments args) { + ASSERT(args.length() == 2); + + CONVERT_CHECKED(JSFunction, fun, args[0]); + CONVERT_NUMBER_CHECKED(int, offset, Int32, args[1]); + + Code* code = fun->code(); + RUNTIME_ASSERT(0 <= offset && offset < code->Size()); + + Address pc = code->address() + offset; + return Smi::FromInt(fun->code()->SourcePosition(pc)); +} + + + static Object* Runtime_FunctionSetInstanceClassName(Arguments args) { NoHandleAllocation ha; ASSERT(args.length() == 2); @@ -7360,6 +7375,67 @@ static Object* Runtime_GetScript(Arguments args) { } +// Determines whether the given stack frame should be displayed in +// a stack trace. The caller is the error constructor that asked +// for the stack trace to be collected. The first time a construct +// call to this function is encountered it is skipped. The seen_caller +// in/out parameter is used to remember if the caller has been seen +// yet. +static bool ShowFrameInStackTrace(StackFrame* raw_frame, Object* caller, + bool* seen_caller) { + // Only display JS frames. + if (!raw_frame->is_java_script()) + return false; + JavaScriptFrame* frame = JavaScriptFrame::cast(raw_frame); + Object* raw_fun = frame->function(); + // Not sure when this can happen but skip it just in case. + if (!raw_fun->IsJSFunction()) + return false; + if ((raw_fun == caller) && !(*seen_caller) && frame->IsConstructor()) { + *seen_caller = true; + return false; + } + // Skip the most obvious builtin calls. Some builtin calls (such as + // Number.ADD which is invoked using 'call') are very difficult to + // recognize so we're leaving them in for now. + return !frame->receiver()->IsJSBuiltinsObject(); +} + + +// Collect the raw data for a stack trace. Returns an array of three +// element segments each containing a receiver, function and native +// code offset. +static Object* Runtime_CollectStackTrace(Arguments args) { + ASSERT_EQ(args.length(), 1); + Object* caller = args[0]; + + StackFrameIterator iter; + int frame_count = 0; + bool seen_caller = false; + while (!iter.done()) { + if (ShowFrameInStackTrace(iter.frame(), caller, &seen_caller)) + frame_count++; + iter.Advance(); + } + HandleScope scope; + Handle result = Factory::NewJSArray(frame_count * 3); + int i = 0; + seen_caller = false; + for (iter.Reset(); !iter.done(); iter.Advance()) { + StackFrame* raw_frame = iter.frame(); + if (ShowFrameInStackTrace(raw_frame, caller, &seen_caller)) { + JavaScriptFrame* frame = JavaScriptFrame::cast(raw_frame); + result->SetElement(i++, frame->receiver()); + result->SetElement(i++, frame->function()); + Address pc = frame->pc(); + Address start = frame->code()->address(); + result->SetElement(i++, Smi::FromInt(pc - start)); + } + } + return *result; +} + + static Object* Runtime_Abort(Arguments args) { ASSERT(args.length() == 2); OS::PrintError("abort: %s\n", reinterpret_cast(args[0]) + diff --git a/src/runtime.h b/src/runtime.h index 3c094952ed..36e274ad3d 100644 --- a/src/runtime.h +++ b/src/runtime.h @@ -169,8 +169,10 @@ namespace internal { F(FunctionGetSourceCode, 1) \ F(FunctionGetScript, 1) \ F(FunctionGetScriptSourcePosition, 1) \ + F(FunctionGetPositionForOffset, 2) \ F(FunctionIsAPIFunction, 1) \ F(GetScript, 1) \ + F(CollectStackTrace, 1) \ \ F(ClassOf, 1) \ F(SetCode, 2) \ diff --git a/test/mjsunit/stack-traces.js b/test/mjsunit/stack-traces.js new file mode 100644 index 0000000000..6ac8b0acfe --- /dev/null +++ b/test/mjsunit/stack-traces.js @@ -0,0 +1,160 @@ +// Copyright 2009 the V8 project authors. All rights reserved. +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following +// disclaimer in the documentation and/or other materials provided +// with the distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Error.captureStackTraces = true; + +function testMethodNameInference() { + function Foo() { } + Foo.prototype.bar = function () { FAIL; }; + (new Foo).bar(); +} + +function testNested() { + function one() { + function two() { + function three() { + FAIL; + } + three(); + } + two(); + } + one(); +} + +function testArrayNative() { + [1, 2, 3].map(function () { FAIL; }); +} + +function testImplicitConversion() { + function Nirk() { } + Nirk.prototype.valueOf = function () { FAIL; }; + return 1 + (new Nirk); +} + +function testEval() { + eval("function Doo() { FAIL; }; Doo();"); +} + +function testNestedEval() { + var x = "FAIL"; + eval("function Outer() { eval('function Inner() { eval(x); }'); Inner(); }; Outer();"); +} + +function testValue() { + Number.prototype.causeError = function () { FAIL; }; + (1).causeError(); +} + +function testConstructor() { + function Plonk() { FAIL; } + new Plonk(); +} + +// Utility function for testing that the expected strings occur +// in the stack trace produced when running the given function. +function testTrace(fun, expected) { + var threw = false; + try { + fun(); + } catch (e) { + for (var i = 0; i < expected.length; i++) { + assertTrue(e.stack.indexOf(expected[i]) != -1); + } + threw = true; + } + assertTrue(threw); +} + +// Test that the error constructor is not shown in the trace +function testCallerCensorship() { + var threw = false; + try { + FAIL; + } catch (e) { + assertEquals(-1, e.stack.indexOf('at new ReferenceError')); + threw = true; + } + assertTrue(threw); +} + +// Test that the explicit constructor call is shown in the trace +function testUnintendedCallerCensorship() { + var threw = false; + try { + new ReferenceError({ + toString: function () { + FAIL; + } + }); + } catch (e) { + assertTrue(e.stack.indexOf('at new ReferenceError') != -1); + threw = true; + } + assertTrue(threw); +} + +// If an error occurs while the stack trace is being formatted it should +// be handled gracefully. +function testErrorsDuringFormatting() { + function Nasty() { } + Nasty.prototype.foo = function () { throw new RangeError(); }; + var n = new Nasty(); + n.__defineGetter__('constructor', function () { CONS_FAIL; }); + var threw = false; + try { + n.foo(); + } catch (e) { + threw = true; + assertTrue(e.stack.indexOf('') != -1); + } + assertTrue(threw); +} + +testTrace(testArrayNative, ["Array.map (native)"]); +testTrace(testNested, ["at one", "at two", "at three"]); +testTrace(testMethodNameInference, ["at Foo.bar"]); +testTrace(testImplicitConversion, ["at Nirk.valueOf"]); +testTrace(testEval, ["at Doo (eval at testEval"]); +testTrace(testNestedEval, ["at eval (eval at Inner (eval at Outer"]); +testTrace(testValue, ["at Number.causeError"]); +testTrace(testConstructor, ["new Plonk"]); + +testCallerCensorship(); +testUnintendedCallerCensorship(); +testErrorsDuringFormatting();