1
0
mirror of https://github.com/robertkrimen/otto synced 2025-10-05 19:19:10 +08:00

make call stacks aware of native functions

* add stackFramesToPop argument to error factories
* put native functions in their own stack frames
* add tests for native stack frames
* amend Context functionality to account for native frames
This commit is contained in:
deoxxa 2015-11-28 09:27:56 +11:00
parent 3a69c44018
commit 668c95f04e
11 changed files with 352 additions and 60 deletions

View File

@ -5,11 +5,11 @@ import (
)
func builtinError(call FunctionCall) Value {
return toValue_object(call.runtime.newError("Error", call.Argument(0)))
return toValue_object(call.runtime.newError("Error", call.Argument(0), 1))
}
func builtinNewError(self *_object, argumentList []Value) Value {
return toValue_object(self.runtime.newError("Error", valueOfArrayIndex(argumentList, 0)))
return toValue_object(self.runtime.newError("Error", valueOfArrayIndex(argumentList, 0), 0))
}
func builtinError_toString(call FunctionCall) Value {
@ -42,7 +42,7 @@ func builtinError_toString(call FunctionCall) Value {
}
func (runtime *_runtime) newEvalError(message Value) *_object {
self := runtime.newErrorObject("EvalError", message)
self := runtime.newErrorObject("EvalError", message, 0)
self.prototype = runtime.global.EvalErrorPrototype
return self
}
@ -56,7 +56,7 @@ func builtinNewEvalError(self *_object, argumentList []Value) Value {
}
func (runtime *_runtime) newTypeError(message Value) *_object {
self := runtime.newErrorObject("TypeError", message)
self := runtime.newErrorObject("TypeError", message, 0)
self.prototype = runtime.global.TypeErrorPrototype
return self
}
@ -70,7 +70,7 @@ func builtinNewTypeError(self *_object, argumentList []Value) Value {
}
func (runtime *_runtime) newRangeError(message Value) *_object {
self := runtime.newErrorObject("RangeError", message)
self := runtime.newErrorObject("RangeError", message, 0)
self.prototype = runtime.global.RangeErrorPrototype
return self
}
@ -84,13 +84,13 @@ func builtinNewRangeError(self *_object, argumentList []Value) Value {
}
func (runtime *_runtime) newURIError(message Value) *_object {
self := runtime.newErrorObject("URIError", message)
self := runtime.newErrorObject("URIError", message, 0)
self.prototype = runtime.global.URIErrorPrototype
return self
}
func (runtime *_runtime) newReferenceError(message Value) *_object {
self := runtime.newErrorObject("ReferenceError", message)
self := runtime.newErrorObject("ReferenceError", message, 0)
self.prototype = runtime.global.ReferenceErrorPrototype
return self
}
@ -104,7 +104,7 @@ func builtinNewReferenceError(self *_object, argumentList []Value) Value {
}
func (runtime *_runtime) newSyntaxError(message Value) *_object {
self := runtime.newErrorObject("SyntaxError", message)
self := runtime.newErrorObject("SyntaxError", message, 0)
self.prototype = runtime.global.SyntaxErrorPrototype
return self
}

View File

@ -50,9 +50,12 @@ func (err _error) formatWithStack() string {
}
type _frame struct {
file *file.File
offset int
callee string
native bool
nativeFile string
nativeLine int
file *file.File
offset int
callee string
}
var (
@ -62,23 +65,24 @@ var (
type _at int
func (fr _frame) location() string {
if fr.file == nil {
return "<unknown>"
}
str := "<unknown>"
var str string
p := fr.file.Position(file.Idx(fr.offset))
if p == nil {
str = "<unknown>"
} else {
path, line, column := p.Filename, p.Line, p.Column
if path == "" {
path = "<anonymous>"
switch {
case fr.native:
str = "<native code>"
if fr.nativeFile != "" && fr.nativeLine != 0 {
str = fmt.Sprintf("%s:%d", fr.nativeFile, fr.nativeLine)
}
case fr.file != nil:
if p := fr.file.Position(file.Idx(fr.offset)); p != nil {
path, line, column := p.Filename, p.Line, p.Column
str = fmt.Sprintf("%s:%d:%d", path, line, column)
if path == "" {
path = "<anonymous>"
}
str = fmt.Sprintf("%s:%d:%d", path, line, column)
}
}
if fr.callee != "" {
@ -130,7 +134,7 @@ func (rt *_runtime) typeErrorResult(throw bool) bool {
return false
}
func newError(rt *_runtime, name string, in ...interface{}) _error {
func newError(rt *_runtime, name string, stackFramesToPop int, in ...interface{}) _error {
err := _error{
name: name,
offset: -1,
@ -140,7 +144,15 @@ func newError(rt *_runtime, name string, in ...interface{}) _error {
if rt != nil {
scope := rt.scope
for i := 0; i < stackFramesToPop; i++ {
if scope.outer != nil {
scope = scope.outer
}
}
frame := scope.frame
if length > 0 {
if at, ok := in[length-1].(_at); ok {
in = in[0 : length-1]
@ -173,36 +185,37 @@ func newError(rt *_runtime, name string, in ...interface{}) _error {
}
}
err.message = err.describe(description, in...)
return err
}
func (rt *_runtime) panicTypeError(argumentList ...interface{}) *_exception {
return &_exception{
value: newError(rt, "TypeError", argumentList...),
value: newError(rt, "TypeError", 0, argumentList...),
}
}
func (rt *_runtime) panicReferenceError(argumentList ...interface{}) *_exception {
return &_exception{
value: newError(rt, "ReferenceError", argumentList...),
value: newError(rt, "ReferenceError", 0, argumentList...),
}
}
func (rt *_runtime) panicURIError(argumentList ...interface{}) *_exception {
return &_exception{
value: newError(rt, "URIError", argumentList...),
value: newError(rt, "URIError", 0, argumentList...),
}
}
func (rt *_runtime) panicSyntaxError(argumentList ...interface{}) *_exception {
return &_exception{
value: newError(rt, "SyntaxError", argumentList...),
value: newError(rt, "SyntaxError", 0, argumentList...),
}
}
func (rt *_runtime) panicRangeError(argumentList ...interface{}) *_exception {
return &_exception{
value: newError(rt, "RangeError", argumentList...),
value: newError(rt, "RangeError", 0, argumentList...),
}
}
@ -213,6 +226,9 @@ func catchPanic(function func()) (err error) {
caught = exception.eject()
}
switch caught := caught.(type) {
case *Error:
err = caught
return
case _error:
err = &Error{caught}
return

39
error_native_test.go Normal file
View File

@ -0,0 +1,39 @@
package otto
import (
"testing"
)
// this is its own file because the tests in it rely on the line numbers of
// some of the functions defined here. putting it in with the rest of the
// tests would probably be annoying.
func TestErrorContextNative(t *testing.T) {
tt(t, func() {
vm := New()
vm.Set("N", func(c FunctionCall) Value {
v, err := c.Argument(0).Call(NullValue())
if err != nil {
panic(err)
}
return v
})
s, _ := vm.Compile("test.js", `
function F() { throw new Error('wow'); }
function G() { return N(F); }
`)
vm.Run(s)
f1, _ := vm.Get("G")
_, err := f1.Call(NullValue())
err1 := err.(*Error)
is(err1.message, "wow")
is(len(err1.trace), 3)
is(err1.trace[0].location(), "F (test.js:2:29)")
is(err1.trace[1].location(), "github.com/robertkrimen/otto.TestErrorContextNative.func1.1 (error_native_test.go:15)")
is(err1.trace[2].location(), "G (test.js:3:26)")
})
}

48
function_stack_test.go Normal file
View File

@ -0,0 +1,48 @@
package otto
import (
"testing"
)
// this is its own file because the tests in it rely on the line numbers of
// some of the functions defined here. putting it in with the rest of the
// tests would probably be annoying.
func TestFunction_stack(t *testing.T) {
tt(t, func() {
vm := New()
s, _ := vm.Compile("fake.js", `function X(fn1, fn2, fn3) { fn1(fn2, fn3); }`)
vm.Run(s)
expected := []_frame{
{native: true, nativeFile: "function_stack_test.go", nativeLine: 30, offset: 0, callee: "github.com/robertkrimen/otto.TestFunction_stack.func1.2"},
{native: true, nativeFile: "function_stack_test.go", nativeLine: 25, offset: 0, callee: "github.com/robertkrimen/otto.TestFunction_stack.func1.1"},
{native: false, nativeFile: "", nativeLine: 0, offset: 29, callee: "X", file: s.program.file},
{native: false, nativeFile: "", nativeLine: 0, offset: 29, callee: "X", file: s.program.file},
}
vm.Set("A", func(c FunctionCall) Value {
c.Argument(0).Call(UndefinedValue())
return UndefinedValue()
})
vm.Set("B", func(c FunctionCall) Value {
depth := 0
for scope := c.Otto.runtime.scope; scope != nil; scope = scope.outer {
is(scope.frame, expected[depth])
depth++
}
is(depth, 4)
return UndefinedValue()
})
x, _ := vm.Get("X")
a, _ := vm.Get("A")
b, _ := vm.Get("B")
x.Call(UndefinedValue(), x, a, b)
})
}

View File

@ -166,7 +166,7 @@ func (runtime *_runtime) newDate(epoch float64) *_object {
return self
}
func (runtime *_runtime) newError(name string, message Value) *_object {
func (runtime *_runtime) newError(name string, message Value, stackFramesToPop int) *_object {
var self *_object
switch name {
case "EvalError":
@ -183,7 +183,7 @@ func (runtime *_runtime) newError(name string, message Value) *_object {
return runtime.newURIError(message)
}
self = runtime.newErrorObject(name, message)
self = runtime.newErrorObject(name, message, stackFramesToPop)
self.prototype = runtime.global.ErrorPrototype
if name != "" {
self.defineProperty("name", toValue_string(name), 0111, false)
@ -191,8 +191,8 @@ func (runtime *_runtime) newError(name string, message Value) *_object {
return self
}
func (runtime *_runtime) newNativeFunction(name string, _nativeFunction _nativeFunction) *_object {
self := runtime.newNativeFunctionObject(name, _nativeFunction, 0)
func (runtime *_runtime) newNativeFunction(name, file string, line int, _nativeFunction _nativeFunction) *_object {
self := runtime.newNativeFunctionObject(name, file, line, _nativeFunction, 0)
self.prototype = runtime.global.FunctionPrototype
prototype := runtime.newObject()
self.defineProperty("prototype", toValue_object(prototype), 0100, false)

111
native_stack_test.go Normal file
View File

@ -0,0 +1,111 @@
package otto
import (
"testing"
)
func TestNativeStackFrames(t *testing.T) {
tt(t, func() {
vm := New()
s, err := vm.Compile("input.js", `
function A() { ext1(); }
function B() { ext2(); }
A();
`)
if err != nil {
panic(err)
}
vm.Set("ext1", func(c FunctionCall) Value {
if _, err := c.Otto.Eval("B()"); err != nil {
panic(err)
}
return UndefinedValue()
})
vm.Set("ext2", func(c FunctionCall) Value {
{
// no limit, include innermost native frames
ctx := c.Otto.ContextSkip(-1, false)
is(ctx.Stacktrace, []string{
"github.com/robertkrimen/otto.TestNativeStackFrames.func1.2 (native_stack_test.go:28)",
"B (input.js:3:22)",
"github.com/robertkrimen/otto.TestNativeStackFrames.func1.1 (native_stack_test.go:20)",
"A (input.js:2:22)", "input.js:4:7",
})
is(ctx.Callee, "github.com/robertkrimen/otto.TestNativeStackFrames.func1.2")
is(ctx.Filename, "native_stack_test.go")
is(ctx.Line, 28)
is(ctx.Column, 0)
}
{
// no limit, skip innermost native frames
ctx := c.Otto.ContextSkip(-1, true)
is(ctx.Stacktrace, []string{
"B (input.js:3:22)",
"github.com/robertkrimen/otto.TestNativeStackFrames.func1.1 (native_stack_test.go:20)",
"A (input.js:2:22)", "input.js:4:7",
})
is(ctx.Callee, "B")
is(ctx.Filename, "input.js")
is(ctx.Line, 3)
is(ctx.Column, 22)
}
if _, err := c.Otto.Eval("ext3()"); err != nil {
panic(err)
}
return UndefinedValue()
})
vm.Set("ext3", func(c FunctionCall) Value {
{
// no limit, include innermost native frames
ctx := c.Otto.ContextSkip(-1, false)
is(ctx.Stacktrace, []string{
"github.com/robertkrimen/otto.TestNativeStackFrames.func1.3 (native_stack_test.go:69)",
"github.com/robertkrimen/otto.TestNativeStackFrames.func1.2 (native_stack_test.go:28)",
"B (input.js:3:22)",
"github.com/robertkrimen/otto.TestNativeStackFrames.func1.1 (native_stack_test.go:20)",
"A (input.js:2:22)", "input.js:4:7",
})
is(ctx.Callee, "github.com/robertkrimen/otto.TestNativeStackFrames.func1.3")
is(ctx.Filename, "native_stack_test.go")
is(ctx.Line, 69)
is(ctx.Column, 0)
}
{
// no limit, skip innermost native frames
ctx := c.Otto.ContextSkip(-1, true)
is(ctx.Stacktrace, []string{
"B (input.js:3:22)",
"github.com/robertkrimen/otto.TestNativeStackFrames.func1.1 (native_stack_test.go:20)",
"A (input.js:2:22)", "input.js:4:7",
})
is(ctx.Callee, "B")
is(ctx.Filename, "input.js")
is(ctx.Line, 3)
is(ctx.Column, 22)
}
return UndefinedValue()
})
if _, err := vm.Run(s); err != nil {
panic(err)
}
})
}

40
otto.go
View File

@ -383,7 +383,7 @@ func (self Otto) SetStackDepthLimit(limit int) {
// MakeCustomError creates a new Error object with the given name and message,
// returning it as a Value.
func (self Otto) MakeCustomError(name, message string) Value {
return self.runtime.toValue(self.runtime.newError(name, self.runtime.toValue(message)))
return self.runtime.toValue(self.runtime.newError(name, self.runtime.toValue(message), 0))
}
// MakeRangeError creates a new RangeError object with the given message,
@ -416,8 +416,23 @@ type Context struct {
Stacktrace []string
}
// Context returns the current execution context of the vm
func (self Otto) Context() (ctx Context) {
// Context returns the current execution context of the vm, traversing up to
// ten stack frames, and skipping any innermost native function stack frames.
func (self Otto) Context() Context {
return self.ContextSkip(10, true)
}
// ContextLimit returns the current execution context of the vm, with a
// specific limit on the number of stack frames to traverse, skipping any
// innermost native function stack frames.
func (self Otto) ContextLimit(limit int) Context {
return self.ContextSkip(limit, true)
}
// ContextSkip returns the current execution context of the vm, with a
// specific limit on the number of stack frames to traverse, optionally
// skipping any innermost native function stack frames.
func (self Otto) ContextSkip(limit int, skipNative bool) (ctx Context) {
// Ensure we are operating in a scope
if self.runtime.scope == nil {
self.runtime.enterGlobalScope()
@ -427,14 +442,24 @@ func (self Otto) Context() (ctx Context) {
scope := self.runtime.scope
frame := scope.frame
for skipNative && frame.native && scope.outer != nil {
scope = scope.outer
frame = scope.frame
}
// Get location information
ctx.Filename = "<unknown>"
ctx.Callee = frame.callee
if frame.file != nil {
switch {
case frame.native:
ctx.Filename = frame.nativeFile
ctx.Line = frame.nativeLine
ctx.Column = 0
case frame.file != nil:
ctx.Filename = "<anonymous>"
p := frame.file.Position(file.Idx(frame.offset))
if p != nil {
if p := frame.file.Position(file.Idx(frame.offset)); p != nil {
ctx.Line = p.Line
ctx.Column = p.Column
@ -448,10 +473,9 @@ func (self Otto) Context() (ctx Context) {
ctx.This = toValue_object(scope.this)
// Build stacktrace (up to 10 levels deep)
limit := 10
ctx.Symbols = make(map[string]Value)
ctx.Stacktrace = append(ctx.Stacktrace, frame.location())
for limit > 0 {
for limit != 0 {
// Get variables
stash := scope.lexical
for {

View File

@ -4,7 +4,9 @@ import (
"errors"
"fmt"
"math"
"path"
"reflect"
"runtime"
"sync"
"github.com/robertkrimen/otto/ast"
@ -123,7 +125,7 @@ func (self *_runtime) tryCatchEvaluate(inner func() Value) (tryValue Value, exce
switch caught := caught.(type) {
case _error:
exception = true
tryValue = toValue_object(self.newError(caught.name, caught.messageValue()))
tryValue = toValue_object(self.newError(caught.name, caught.messageValue(), 0))
case Value:
exception = true
tryValue = caught
@ -351,9 +353,27 @@ func (self *_runtime) toValue(value interface{}) Value {
case Value:
return value
case func(FunctionCall) Value:
return toValue_object(self.newNativeFunction("", value))
var name, file string
var line int
pc := reflect.ValueOf(value).Pointer()
fn := runtime.FuncForPC(pc)
if fn != nil {
name = fn.Name()
file, line = fn.FileLine(pc)
file = path.Base(file)
}
return toValue_object(self.newNativeFunction(name, file, line, value))
case _nativeFunction:
return toValue_object(self.newNativeFunction("", value))
var name, file string
var line int
pc := reflect.ValueOf(value).Pointer()
fn := runtime.FuncForPC(pc)
if fn != nil {
name = fn.Name()
file, line = fn.FileLine(pc)
file = path.Base(file)
}
return toValue_object(self.newNativeFunction(name, file, line, value))
case Object, *Object, _object, *_object:
// Nothing happens.
// FIXME We should really figure out what can come here.
@ -370,10 +390,23 @@ func (self *_runtime) toValue(value interface{}) Value {
return toValue_object(self.newGoArray(value))
}
case reflect.Func:
var name, file string
var line int
v := reflect.ValueOf(value)
if v.Kind() == reflect.Ptr {
pc := v.Pointer()
fn := runtime.FuncForPC(pc)
if fn != nil {
name = fn.Name()
file, line = fn.FileLine(pc)
file = path.Base(file)
}
}
// TODO Maybe cache this?
return toValue_object(self.newNativeFunction("", func(call FunctionCall) Value {
return toValue_object(self.newNativeFunction(name, file, line, func(call FunctionCall) Value {
argsCount := len(call.ArgumentList)
in := make([]reflect.Value, argsCount)
in := make([]reflect.Value, len(call.ArgumentList))
t := value.Type()
callSlice := false
paramsCount := t.NumIn()

View File

@ -1,18 +1,18 @@
package otto
func (rt *_runtime) newErrorObject(name string, message Value) *_object {
func (rt *_runtime) newErrorObject(name string, message Value, stackFramesToPop int) *_object {
self := rt.newClassObject("Error")
if message.IsDefined() {
msg := message.string()
self.defineProperty("message", toValue_string(msg), 0111, false)
self.value = newError(rt, name, msg)
self.value = newError(rt, name, stackFramesToPop, msg)
} else {
self.value = newError(rt, name)
self.value = newError(rt, name, stackFramesToPop)
}
self.defineOwnProperty("stack", _property{
value: _propertyGetSet{
rt.newNativeFunction("get", func(FunctionCall) Value {
rt.newNativeFunction("get", "internal", 0, func(FunctionCall) Value {
return toValue_string(self.value.(_error).formatWithStack())
}),
&_nilGetSetObject,

View File

@ -31,13 +31,18 @@ type _nativeFunction func(FunctionCall) Value
type _nativeFunctionObject struct {
name string
file string
line int
call _nativeFunction // [[Call]]
construct _constructFunction // [[Construct]]
}
func (runtime *_runtime) newNativeFunctionObject(name string, native _nativeFunction, length int) *_object {
func (runtime *_runtime) newNativeFunctionObject(name, file string, line int, native _nativeFunction, length int) *_object {
self := runtime.newClassObject("Function")
self.value = _nativeFunctionObject{
name: name,
file: file,
line: line,
call: native,
construct: defaultConstruct,
}
@ -125,11 +130,27 @@ func (self *_object) call(this Value, argumentList []Value, eval bool, frame _fr
switch fn := self.value.(type) {
case _nativeFunctionObject:
// TODO Enter a scope, name from the native object...
// Since eval is a native function, we only have to check for it here
if eval {
eval = self == self.runtime.eval // If eval is true, then it IS a direct eval
}
// Enter a scope, name from the native object...
rt := self.runtime
if rt.scope != nil && !eval {
rt.enterFunctionScope(rt.scope.lexical, this)
rt.scope.frame = _frame{
native: true,
nativeFile: fn.file,
nativeLine: fn.line,
callee: fn.name,
file: nil,
}
defer func() {
rt.leaveScope()
}()
}
return fn.call(FunctionCall{
runtime: self.runtime,
eval: eval,
@ -149,7 +170,7 @@ func (self *_object) call(this Value, argumentList []Value, eval bool, frame _fr
stash := rt.enterFunctionScope(fn.stash, this)
rt.scope.frame = _frame{
callee: fn.node.name,
file: fn.node.file,
file: fn.node.file,
}
defer func() {
rt.leaveScope()
@ -267,5 +288,5 @@ func (self FunctionCall) toObject(value Value) *_object {
// CallerLocation will return file location information (file:line:pos) where this function is being called.
func (self FunctionCall) CallerLocation() string {
// see error.go for location()
return self.runtime.scope.frame.location()
return self.runtime.scope.outer.frame.location()
}

View File

@ -271,11 +271,11 @@ func toValue_reflectValuePanic(value interface{}, kind reflect.Kind) {
// FIXME?
switch kind {
case reflect.Struct:
panic(newError(nil, "TypeError", "invalid value (struct): missing runtime: %v (%T)", value, value))
panic(newError(nil, "TypeError", 0, "invalid value (struct): missing runtime: %v (%T)", value, value))
case reflect.Map:
panic(newError(nil, "TypeError", "invalid value (map): missing runtime: %v (%T)", value, value))
panic(newError(nil, "TypeError", 0, "invalid value (map): missing runtime: %v (%T)", value, value))
case reflect.Slice:
panic(newError(nil, "TypeError", "invalid value (slice): missing runtime: %v (%T)", value, value))
panic(newError(nil, "TypeError", 0, "invalid value (slice): missing runtime: %v (%T)", value, value))
}
}
@ -375,7 +375,7 @@ func toValue(value interface{}) Value {
return toValue(reflect.ValueOf(value))
}
// FIXME?
panic(newError(nil, "TypeError", "invalid value: %v (%T)", value, value))
panic(newError(nil, "TypeError", 0, "invalid value: %v (%T)", value, value))
}
// String will return the value as a string.