From 37522a24dbb66a23ef5e3d66ae91bec0492e767b Mon Sep 17 00:00:00 2001 From: Dmitry Panov Date: Sat, 29 Feb 2020 12:31:35 +0000 Subject: [PATCH 01/46] Implemented Symbol (#128) --- array.go | 63 ++++++- array_sparse.go | 11 +- builtin_array.go | 115 +++++++++++- builtin_function.go | 1 + builtin_object.go | 85 +++++++-- builtin_regexp.go | 373 +++++++++++++++++++++++++++++++++++++- builtin_string.go | 212 +++++++++------------- builtin_string_test.go | 57 ++++++ builtin_symbol.go | 139 ++++++++++++++ compiler_expr.go | 3 +- date_test.go | 31 ++++ func.go | 20 +- object.go | 211 ++++++++++++++++++--- object_args.go | 22 ++- object_gomap.go | 17 +- object_gomap_reflect.go | 65 +++++-- object_goreflect.go | 60 +++--- object_goslice.go | 6 +- object_goslice_reflect.go | 6 +- object_lazy.go | 14 +- regexp.go | 17 +- runtime.go | 155 ++++++++++------ runtime_test.go | 21 +++ string.go | 5 +- tc39_test.go | 74 +++++--- value.go | 79 +++++++- vm.go | 70 ++----- 27 files changed, 1528 insertions(+), 404 deletions(-) create mode 100644 builtin_symbol.go diff --git a/array.go b/array.go index abd24d8b..3afa282f 100644 --- a/array.go +++ b/array.go @@ -6,6 +6,55 @@ import ( "strconv" ) +type arrayIterObject struct { + baseObject + obj *Object + nextIdx int64 + kind iterationKind +} + +func (ai *arrayIterObject) next() Value { + if ai.obj == nil { + return ai.val.runtime.createIterResultObject(_undefined, true) + } + l := toLength(ai.obj.self.getStr("length")) + index := ai.nextIdx + if index >= l { + ai.obj = nil + return ai.val.runtime.createIterResultObject(_undefined, true) + } + ai.nextIdx++ + if ai.kind == iterationKindKey { + return ai.val.runtime.createIterResultObject(intToValue(index), false) + } + elementKey := asciiString(strconv.FormatInt(index, 10)) + elementValue := ai.obj.self.get(intToValue(index)) + var result Value + if ai.kind == iterationKindValue { + result = elementValue + } else { + result = ai.val.runtime.newArrayValues([]Value{elementKey, elementValue}) + } + return ai.val.runtime.createIterResultObject(result, false) +} + +func (r *Runtime) createArrayIterator(iterObj *Object, kind iterationKind) Value { + o := &Object{runtime: r} + + ai := &arrayIterObject{ + obj: iterObj, + kind: kind, + } + ai.class = classArrayIterator + ai.val = o + ai.extensible = true + o.self = ai + ai.prototype = r.global.ArrayIteratorPrototype + ai.init() + + return o +} + type arrayObject struct { baseObject values []Value @@ -129,6 +178,9 @@ func toIdx(v Value) (idx int64) { if idxVal, ok1 := v.(valueInt); ok1 { idx = int64(idxVal) } else { + if _, ok := v.(*valueSymbol); ok { + return -1 + } if i, err := strconv.ParseInt(v.String(), 10, 64); err == nil { idx = i } @@ -155,9 +207,10 @@ func (a *arrayObject) getProp(n Value) Value { if idx := toIdx(n); idx >= 0 { return a.getIdx(idx, "", n) } - - if n.String() == "length" { - return a.getLengthProp() + if _, ok := n.(*valueSymbol); !ok { + if n.String() == "length" { + return a.getLengthProp() + } } return a.baseObject.getProp(n) } @@ -177,7 +230,7 @@ func (a *arrayObject) getPropStr(name string) Value { return a.baseObject.getPropStr(name) } -func (a *arrayObject) getOwnProp(name string) Value { +func (a *arrayObject) getOwnPropStr(name string) Value { if i := strToIdx(name); i >= 0 { if i >= 0 && i < int64(len(a.values)) { return a.values[i] @@ -186,7 +239,7 @@ func (a *arrayObject) getOwnProp(name string) Value { if name == "length" { return a.getLengthProp() } - return a.baseObject.getOwnProp(name) + return a.baseObject.getOwnPropStr(name) } func (a *arrayObject) putIdx(idx int64, val Value, throw bool, origNameStr string, origName Value) { diff --git a/array_sparse.go b/array_sparse.go index 50340caf..ec67f29a 100644 --- a/array_sparse.go +++ b/array_sparse.go @@ -120,9 +120,12 @@ func (a *sparseArrayObject) getProp(n Value) Value { return a.getIdx(idx, "", n) } - if n.String() == "length" { - return a.getLengthProp() + if _, ok := n.(*valueSymbol); !ok { + if n.String() == "length" { + return a.getLengthProp() + } } + return a.baseObject.getProp(n) } @@ -131,7 +134,7 @@ func (a *sparseArrayObject) getLengthProp() Value { return &a.lengthProp } -func (a *sparseArrayObject) getOwnProp(name string) Value { +func (a *sparseArrayObject) getOwnPropStr(name string) Value { if idx := strToIdx(name); idx >= 0 { i := a.findIdx(idx) if i < len(a.items) && a.items[i].idx == idx { @@ -142,7 +145,7 @@ func (a *sparseArrayObject) getOwnProp(name string) Value { if name == "length" { return a.getLengthProp() } - return a.baseObject.getOwnProp(name) + return a.baseObject.getOwnPropStr(name) } func (a *sparseArrayObject) getPropStr(name string) Value { diff --git a/builtin_array.go b/builtin_array.go index 6abad846..12fd0c3f 100644 --- a/builtin_array.go +++ b/builtin_array.go @@ -6,11 +6,48 @@ import ( "strings" ) +func (r *Runtime) newArray(prototype *Object) (a *arrayObject) { + v := &Object{runtime: r} + + a = &arrayObject{} + a.class = classArray + a.val = v + a.extensible = true + v.self = a + a.prototype = prototype + a.init() + return +} + +func (r *Runtime) newArrayObject() *arrayObject { + return r.newArray(r.global.ArrayPrototype) +} + +func setArrayValues(a *arrayObject, values []Value) *arrayObject { + a.values = values + a.length = int64(len(values)) + a.objCount = a.length + return a +} + +func setArrayLength(a *arrayObject, l int64) *arrayObject { + a.putStr("length", intToValue(l), true) + return a +} + +func (r *Runtime) newArrayValues(values []Value) *Object { + return setArrayValues(r.newArrayObject(), values).val +} + +func (r *Runtime) newArrayLength(l int64) *Object { + return setArrayLength(r.newArrayObject(), l).val +} + func (r *Runtime) builtin_newArray(args []Value, proto *Object) *Object { l := len(args) if l == 1 { if al, ok := args[0].assertInt(); ok { - return r.newArrayLength(al) + return setArrayLength(r.newArray(proto), al).val } else if f, ok := args[0].assertFloat(); ok { al := int64(f) if float64(al) == f { @@ -19,11 +56,11 @@ func (r *Runtime) builtin_newArray(args []Value, proto *Object) *Object { panic(r.newError(r.global.RangeError, "Invalid array length")) } } - return r.newArrayValues([]Value{args[0]}) + return setArrayValues(r.newArray(proto), []Value{args[0]}).val } else { argsCopy := make([]Value, l) copy(argsCopy, args) - return r.newArrayValues(argsCopy) + return setArrayValues(r.newArray(proto), argsCopy).val } } @@ -47,7 +84,7 @@ func (r *Runtime) arrayproto_push(call FunctionCall) Value { return r.generic_push(obj, call) } -func (r *Runtime) arrayproto_pop_generic(obj *Object, call FunctionCall) Value { +func (r *Runtime) arrayproto_pop_generic(obj *Object) Value { l := toLength(obj.self.getStr("length")) if l == 0 { obj.self.putStr("length", intToValue(0), true) @@ -72,11 +109,11 @@ func (r *Runtime) arrayproto_pop(call FunctionCall) Value { } if val == nil { // optimisation bail-out - return r.arrayproto_pop_generic(obj, call) + return r.arrayproto_pop_generic(obj) } if _, ok := val.(*valueProperty); ok { // optimisation bail-out - return r.arrayproto_pop_generic(obj, call) + return r.arrayproto_pop_generic(obj) } //a._setLengthInt(l, false) a.values[l] = nil @@ -86,7 +123,7 @@ func (r *Runtime) arrayproto_pop(call FunctionCall) Value { } return _undefined } else { - return r.arrayproto_pop_generic(obj, call) + return r.arrayproto_pop_generic(obj) } } @@ -191,6 +228,14 @@ func (r *Runtime) arrayproto_toLocaleString(call FunctionCall) Value { } +func isConcatSpreadable(obj *Object) bool { + spreadable := obj.self.get(symIsConcatSpreadable) + if spreadable != nil && spreadable != _undefined { + return spreadable.ToBoolean() + } + return isArray(obj) +} + func (r *Runtime) arrayproto_concat_append(a *Object, item Value) { descr := propertyDescr{ Writable: FLAG_TRUE, @@ -200,7 +245,7 @@ func (r *Runtime) arrayproto_concat_append(a *Object, item Value) { aLength := toLength(a.self.getStr("length")) if obj, ok := item.(*Object); ok { - if isArray(obj) { + if isConcatSpreadable(obj) { length := toLength(obj.self.getStr("length")) for i := int64(0); i < length; i++ { v := obj.self.get(intToValue(i)) @@ -220,8 +265,29 @@ func (r *Runtime) arrayproto_concat_append(a *Object, item Value) { a.self.defineOwnProperty(intToValue(aLength), descr, false) } +func arraySpeciesCreate(obj *Object, size int) *Object { + if isArray(obj) { + v := obj.self.getStr("constructor") + if constructObj, ok := v.(*Object); ok { + species := constructObj.self.get(symSpecies) + if species != nil && !IsUndefined(species) && !IsNull(species) { + constructObj, _ = species.(*Object) + if constructObj != nil { + constructor := getConstructor(constructObj) + if constructor != nil { + return constructor([]Value{intToValue(int64(size))}) + } + } + panic(obj.runtime.NewTypeError()) + } + } + } + return obj.runtime.newArrayValues(nil) +} + func (r *Runtime) arrayproto_concat(call FunctionCall) Value { - a := r.newArrayValues(nil) + obj := call.This.ToObject(r) + a := arraySpeciesCreate(obj, 0) r.arrayproto_concat_append(a, call.This.ToObject(r)) for _, item := range call.Arguments { r.arrayproto_concat_append(a, item) @@ -758,6 +824,10 @@ func (r *Runtime) arrayproto_shift(call FunctionCall) Value { return first } +func (r *Runtime) arrayproto_values(call FunctionCall) Value { + return r.createArrayIterator(call.This.ToObject(r), iterationKindValue) +} + func (r *Runtime) array_isArray(call FunctionCall) Value { if o, ok := call.Argument(0).(*Object); ok { if isArray(o) { @@ -767,6 +837,14 @@ func (r *Runtime) array_isArray(call FunctionCall) Value { return valueFalse } +func (r *Runtime) arrayIterProto_next(call FunctionCall) Value { + thisObj := r.toObject(call.This) + if iter, ok := thisObj.self.(*arrayIterObject); ok { + return iter.next() + } + panic(r.NewTypeError("Method Array Iterator.prototype.next called on incompatible receiver %s", thisObj.String())) +} + func (r *Runtime) createArrayProto(val *Object) objectImpl { o := &arrayObject{ baseObject: baseObject{ @@ -800,6 +878,9 @@ func (r *Runtime) createArrayProto(val *Object) objectImpl { o._putProp("filter", r.newNativeFunc(r.arrayproto_filter, nil, "filter", nil, 1), true, false, true) o._putProp("reduce", r.newNativeFunc(r.arrayproto_reduce, nil, "reduce", nil, 1), true, false, true) o._putProp("reduceRight", r.newNativeFunc(r.arrayproto_reduceRight, nil, "reduceRight", nil, 1), true, false, true) + valuesFunc := r.newNativeFunc(r.arrayproto_values, nil, "values", nil, 0) + o._putProp("values", valuesFunc, true, false, true) + o.put(symIterator, valueProp(valuesFunc, false, false, true), true) return o } @@ -807,10 +888,26 @@ func (r *Runtime) createArrayProto(val *Object) objectImpl { func (r *Runtime) createArray(val *Object) objectImpl { o := r.newNativeFuncConstructObj(val, r.builtin_newArray, "Array", r.global.ArrayPrototype, 1) o._putProp("isArray", r.newNativeFunc(r.array_isArray, nil, "isArray", nil, 1), true, false, true) + o.putSym(symSpecies, &valueProperty{ + getterFunc: r.newNativeFunc(r.returnThis, nil, "get [Symbol.species]", nil, 0), + accessor: true, + configurable: true, + }, true) + + return o +} + +func (r *Runtime) createArrayIterProto(val *Object) objectImpl { + o := newBaseObjectObj(val, r.global.IteratorPrototype, classObject) + + o._putProp("next", r.newNativeFunc(r.arrayIterProto_next, nil, "next", nil, 0), true, false, true) + o.put(symToStringTag, valueProp(asciiString(classArrayIterator), false, false, true), true) + return o } func (r *Runtime) initArray() { + r.global.ArrayIteratorPrototype = r.newLazyObject(r.createArrayIterProto) //r.global.ArrayPrototype = r.newArray(r.global.ObjectPrototype).val //o := r.global.ArrayPrototype.self r.global.ArrayPrototype = r.newLazyObject(r.createArrayProto) diff --git a/builtin_function.go b/builtin_function.go index 075cd312..aaeb0352 100644 --- a/builtin_function.go +++ b/builtin_function.go @@ -143,6 +143,7 @@ repeat: ff := r.newNativeFuncObj(v, r.boundCallable(fcall, call.Arguments), r.boundConstruct(construct, call.Arguments), "", nil, l) v.self = &boundFuncObject{ nativeFuncObject: *ff, + wrapped: obj, } //ret := r.newNativeFunc(r.boundCallable(f, call.Arguments), nil, "", nil, l) diff --git a/builtin_object.go b/builtin_object.go index 050512d7..dda319d3 100644 --- a/builtin_object.go +++ b/builtin_object.go @@ -11,7 +11,7 @@ func (r *Runtime) builtin_Object(args []Value, proto *Object) *Object { return arg.ToObject(r) } } - return r.NewObject() + return r.newBaseObject(proto, classObject).val } func (r *Runtime) object_getPrototypeOf(call FunctionCall) Value { @@ -25,7 +25,7 @@ func (r *Runtime) object_getPrototypeOf(call FunctionCall) Value { func (r *Runtime) object_getOwnPropertyDescriptor(call FunctionCall) Value { obj := call.Argument(0).ToObject(r) - propName := call.Argument(1).String() + propName := call.Argument(1) desc := obj.self.getOwnProp(propName) if desc == nil { return _undefined @@ -83,6 +83,11 @@ func (r *Runtime) object_getOwnPropertyNames(call FunctionCall) Value { return r.newArrayValues(values) } +func (r *Runtime) object_getOwnPropertySymbols(call FunctionCall) Value { + obj := call.Argument(0).ToObject(r) + return r.newArrayValues(obj.self.getOwnSymbols()) +} + func (r *Runtime) toPropertyDescr(v Value) (ret propertyDescr) { if o, ok := v.(*Object); ok { descr := o.self @@ -188,7 +193,7 @@ func (r *Runtime) object_seal(call FunctionCall) Value { Configurable: FLAG_FALSE, } for item, f := obj.self.enumerate(true, false)(); f != nil; item, f = f() { - v := obj.self.getOwnProp(item.name) + v := obj.self.getOwnPropStr(item.name) if prop, ok := v.(*valueProperty); ok { if !prop.configurable { continue @@ -200,6 +205,18 @@ func (r *Runtime) object_seal(call FunctionCall) Value { //obj.self._putProp(item.name, v, true, true, false) } } + for _, sym := range obj.self.getOwnSymbols() { + v := obj.self.getOwnProp(sym) + if prop, ok := v.(*valueProperty); ok { + if !prop.configurable { + continue + } + prop.configurable = false + } else { + descr.Value = v + obj.self.defineOwnProperty(sym, descr, true) + } + } obj.self.preventExtensions() return obj } @@ -215,7 +232,7 @@ func (r *Runtime) object_freeze(call FunctionCall) Value { Configurable: FLAG_FALSE, } for item, f := obj.self.enumerate(true, false)(); f != nil; item, f = f() { - v := obj.self.getOwnProp(item.name) + v := obj.self.getOwnPropStr(item.name) if prop, ok := v.(*valueProperty); ok { prop.configurable = false if prop.value != nil { @@ -226,6 +243,18 @@ func (r *Runtime) object_freeze(call FunctionCall) Value { obj.self.defineOwnProperty(newStringValue(item.name), descr, true) } } + for _, sym := range obj.self.getOwnSymbols() { + v := obj.self.getOwnProp(sym) + if prop, ok := v.(*valueProperty); ok { + prop.configurable = false + if prop.value != nil { + prop.writable = false + } + } else { + descr.Value = v + obj.self.defineOwnProperty(sym, descr, true) + } + } obj.self.preventExtensions() return obj } else { @@ -252,7 +281,17 @@ func (r *Runtime) object_isSealed(call FunctionCall) Value { return valueFalse } for item, f := obj.self.enumerate(true, false)(); f != nil; item, f = f() { - prop := obj.self.getOwnProp(item.name) + prop := obj.self.getOwnPropStr(item.name) + if prop, ok := prop.(*valueProperty); ok { + if prop.configurable { + return valueFalse + } + } else { + return valueFalse + } + } + for _, sym := range obj.self.getOwnSymbols() { + prop := obj.self.getOwnProp(sym) if prop, ok := prop.(*valueProperty); ok { if prop.configurable { return valueFalse @@ -275,7 +314,17 @@ func (r *Runtime) object_isFrozen(call FunctionCall) Value { return valueFalse } for item, f := obj.self.enumerate(true, false)(); f != nil; item, f = f() { - prop := obj.self.getOwnProp(item.name) + prop := obj.self.getOwnPropStr(item.name) + if prop, ok := prop.(*valueProperty); ok { + if prop.configurable || prop.value != nil && prop.writable { + return valueFalse + } + } else { + return valueFalse + } + } + for _, sym := range obj.self.getOwnSymbols() { + prop := obj.self.getOwnProp(sym) if prop, ok := prop.(*valueProperty); ok { if prop.configurable || prop.value != nil && prop.writable { return valueFalse @@ -321,9 +370,9 @@ func (r *Runtime) object_keys(call FunctionCall) Value { } func (r *Runtime) objectproto_hasOwnProperty(call FunctionCall) Value { - p := call.Argument(0).String() + p := call.Argument(0) o := call.This.ToObject(r) - if o.self.hasOwnPropertyStr(p) { + if o.self.hasOwnProperty(p) { return valueTrue } else { return valueFalse @@ -347,9 +396,9 @@ func (r *Runtime) objectproto_isPrototypeOf(call FunctionCall) Value { } func (r *Runtime) objectproto_propertyIsEnumerable(call FunctionCall) Value { - p := call.Argument(0).ToString() + p := call.Argument(0) o := call.This.ToObject(r) - pv := o.self.getOwnProp(p.String()) + pv := o.self.getOwnProp(p) if pv == nil { return valueFalse } @@ -367,11 +416,18 @@ func (r *Runtime) objectproto_toString(call FunctionCall) Value { return stringObjectNull case valueUndefined: return stringObjectUndefined - case *Object: - return newStringValue(fmt.Sprintf("[object %s]", o.self.className())) default: - obj := call.This.ToObject(r) - return newStringValue(fmt.Sprintf("[object %s]", obj.self.className())) + obj := o.ToObject(r) + var clsName string + if tag := obj.self.get(symToStringTag); tag != nil { + if str, ok := tag.assertString(); ok { + clsName = str.String() + } + } + if clsName == "" { + clsName = obj.self.className() + } + return newStringValue(fmt.Sprintf("[object %s]", clsName)) } } @@ -399,6 +455,7 @@ func (r *Runtime) initObject() { o._putProp("getOwnPropertyDescriptor", r.newNativeFunc(r.object_getOwnPropertyDescriptor, nil, "getOwnPropertyDescriptor", nil, 2), true, false, true) o._putProp("getPrototypeOf", r.newNativeFunc(r.object_getPrototypeOf, nil, "getPrototypeOf", nil, 1), true, false, true) o._putProp("getOwnPropertyNames", r.newNativeFunc(r.object_getOwnPropertyNames, nil, "getOwnPropertyNames", nil, 1), true, false, true) + o._putProp("getOwnPropertySymbols", r.newNativeFunc(r.object_getOwnPropertySymbols, nil, "getOwnPropertySymbols", nil, 1), true, false, true) o._putProp("create", r.newNativeFunc(r.object_create, nil, "create", nil, 2), true, false, true) o._putProp("seal", r.newNativeFunc(r.object_seal, nil, "seal", nil, 1), true, false, true) o._putProp("freeze", r.newNativeFunc(r.object_freeze, nil, "freeze", nil, 1), true, false, true) diff --git a/builtin_regexp.go b/builtin_regexp.go index 3a4d524b..ca3b721b 100644 --- a/builtin_regexp.go +++ b/builtin_regexp.go @@ -5,6 +5,7 @@ import ( "github.com/dlclark/regexp2" "github.com/dop251/goja/parser" "regexp" + "strings" ) func (r *Runtime) newRegexpObject(proto *Object) *regexpObject { @@ -20,7 +21,7 @@ func (r *Runtime) newRegexpObject(proto *Object) *regexpObject { return o } -func (r *Runtime) newRegExpp(pattern regexpPattern, patternStr valueString, global, ignoreCase, multiline bool, proto *Object) *Object { +func (r *Runtime) newRegExpp(pattern regexpPattern, patternStr valueString, global, ignoreCase, multiline, sticky bool, proto *Object) *Object { o := r.newRegexpObject(proto) o.pattern = pattern @@ -28,11 +29,12 @@ func (r *Runtime) newRegExpp(pattern regexpPattern, patternStr valueString, glob o.global = global o.ignoreCase = ignoreCase o.multiline = multiline + o.sticky = sticky return o.val } -func compileRegexp(patternStr, flags string) (p regexpPattern, global, ignoreCase, multiline bool, err error) { +func compileRegexp(patternStr, flags string) (p regexpPattern, global, ignoreCase, multiline, sticky bool, err error) { if flags != "" { invalidFlags := func() { @@ -58,6 +60,12 @@ func compileRegexp(patternStr, flags string) (p regexpPattern, global, ignoreCas return } ignoreCase = true + case 'y': + if sticky { + invalidFlags() + return + } + sticky = true default: invalidFlags() return @@ -104,11 +112,11 @@ func compileRegexp(patternStr, flags string) (p regexpPattern, global, ignoreCas } func (r *Runtime) newRegExp(patternStr valueString, flags string, proto *Object) *Object { - pattern, global, ignoreCase, multiline, err := compileRegexp(patternStr.String(), flags) + pattern, global, ignoreCase, multiline, sticky, err := compileRegexp(patternStr.String(), flags) if err != nil { panic(r.newSyntaxError(err.Error(), -1)) } - return r.newRegExpp(pattern, patternStr, global, ignoreCase, multiline, proto) + return r.newRegExpp(pattern, patternStr, global, ignoreCase, multiline, sticky, proto) } func (r *Runtime) builtin_newRegExp(args []Value) *Object { @@ -175,7 +183,7 @@ func (r *Runtime) regexpproto_test(call FunctionCall) Value { func (r *Runtime) regexpproto_toString(call FunctionCall) Value { if this, ok := r.toObject(call.This).self.(*regexpObject); ok { - var g, i, m string + var g, i, m, y string if this.global { g = "g" } @@ -185,7 +193,10 @@ func (r *Runtime) regexpproto_toString(call FunctionCall) Value { if this.multiline { m = "m" } - return newStringValue(fmt.Sprintf("/%s/%s%s%s", this.source.String(), g, i, m)) + if this.sticky { + y = "y" + } + return newStringValue(fmt.Sprintf("/%s/%s%s%s%s", this.source.String(), g, i, m, y)) } else { r.typeErrorResult(true, "Method RegExp.prototype.toString called on incompatible receiver %s", call.This) return nil @@ -240,10 +251,338 @@ func (r *Runtime) regexpproto_getIgnoreCase(call FunctionCall) Value { } } +func (r *Runtime) regexpproto_getSticky(call FunctionCall) Value { + if this, ok := r.toObject(call.This).self.(*regexpObject); ok { + if this.sticky { + return valueTrue + } else { + return valueFalse + } + } else { + r.typeErrorResult(true, "Method RegExp.prototype.sticky getter called on incompatible receiver %s", call.This.ToString()) + return nil + } +} + +func (r *Runtime) regexpproto_getFlags(call FunctionCall) Value { + var global, ignoreCase, multiline, sticky bool + + thisObj := r.toObject(call.This) + if this, ok := thisObj.self.(*regexpObject); ok { + global, ignoreCase, multiline, sticky = this.global, this.ignoreCase, this.multiline, this.sticky + } else { + if v := thisObj.self.getStr("global"); v != nil { + global = v.ToBoolean() + } + if v := thisObj.self.getStr("ignoreCase"); v != nil { + ignoreCase = v.ToBoolean() + } + if v := thisObj.self.getStr("multiline"); v != nil { + multiline = v.ToBoolean() + } + if v := thisObj.self.getStr("sticky"); v != nil { + sticky = v.ToBoolean() + } + } + + var sb strings.Builder + if global { + sb.WriteByte('g') + } + if ignoreCase { + sb.WriteByte('i') + } + if multiline { + sb.WriteByte('m') + } + if sticky { + sb.WriteByte('y') + } + + return asciiString(sb.String()) +} + +func (r *Runtime) regExpExec(execFn func(FunctionCall) Value, rxObj *Object, arg Value) Value { + res := execFn(FunctionCall{ + This: rxObj, + Arguments: []Value{arg}, + }) + + if res != _null { + if _, ok := res.(*Object); !ok { + panic(r.NewTypeError("RegExp exec method returned something other than an Object or null")) + } + } + + return res +} + +func (r *Runtime) regexpproto_stdMatcherGeneric(rxObj *Object, arg Value) Value { + rx := rxObj.self + global := rx.getStr("global") + if global != nil && global.ToBoolean() { + rx.putStr("lastIndex", intToValue(0), true) + execFn, ok := r.toObject(rx.getStr("exec")).self.assertCallable() + if !ok { + panic(r.NewTypeError("exec is not a function")) + } + var a []Value + for { + res := r.regExpExec(execFn, rxObj, arg) + if res == _null { + break + } + matchStr := nilSafe(r.toObject(res).self.get(intToValue(0))).ToString() + a = append(a, matchStr) + if matchStr.length() == 0 { + thisIndex := rx.getStr("lastIndex").ToInteger() + rx.putStr("lastIndex", intToValue(thisIndex+1), true) // TODO fullUnicode + } + } + if len(a) == 0 { + return _null + } + return r.newArrayValues(a) + } + + execFn, ok := r.toObject(rx.getStr("exec")).self.assertCallable() + if !ok { + panic(r.NewTypeError("exec is not a function")) + } + + return r.regExpExec(execFn, rxObj, arg) +} + +func (r *Runtime) checkStdRegexp(rxObj *Object) *regexpObject { + rx, ok := rxObj.self.(*regexpObject) + if !ok { + return nil + } + execFn := rx.getPropStr("exec") + if execFn != nil && execFn != r.global.regexpProtoExec { + return nil + } + + return rx +} + +func (r *Runtime) regexpproto_stdMatcher(call FunctionCall) Value { + thisObj := r.toObject(call.This) + s := call.Argument(0).ToString() + rx := r.checkStdRegexp(thisObj) + if rx == nil { + return r.regexpproto_stdMatcherGeneric(thisObj, s) + } + if rx.global { + rx.putStr("lastIndex", intToValue(0), true) + var a []Value + var previousLastIndex int64 + for { + match, result := rx.execRegexp(s) + if !match { + break + } + thisIndex := rx.getStr("lastIndex").ToInteger() + if thisIndex == previousLastIndex { + previousLastIndex++ + rx.putStr("lastIndex", intToValue(previousLastIndex), true) + } else { + previousLastIndex = thisIndex + } + a = append(a, s.substring(int64(result[0]), int64(result[1]))) + } + if len(a) == 0 { + return _null + } + return r.newArrayValues(a) + } else { + return rx.exec(s) + } +} + +func (r *Runtime) regexpproto_stdSearchGeneric(rxObj *Object, arg valueString) Value { + rx := rxObj.self + previousLastIndex := rx.getStr("lastIndex") + rx.putStr("lastIndex", intToValue(0), true) + execFn, ok := r.toObject(rx.getStr("exec")).self.assertCallable() + if !ok { + panic(r.NewTypeError("exec is not a function")) + } + + result := r.regExpExec(execFn, rxObj, arg) + rx.putStr("lastIndex", previousLastIndex, true) + + if result == _null { + return intToValue(-1) + } + + return r.toObject(result).self.getStr("index") +} + +func (r *Runtime) regexpproto_stdSearch(call FunctionCall) Value { + thisObj := r.toObject(call.This) + s := call.Argument(0).ToString() + rx := r.checkStdRegexp(thisObj) + if rx == nil { + return r.regexpproto_stdSearchGeneric(thisObj, s) + } + + previousLastIndex := rx.getStr("lastIndex") + rx.putStr("lastIndex", intToValue(0), true) + + match, result := rx.execRegexp(s) + rx.putStr("lastIndex", previousLastIndex, true) + + if !match { + return intToValue(-1) + } + return intToValue(int64(result[0])) +} + +func (r *Runtime) regexpproto_stdSplitterGeneric(splitter *Object, s valueString, limit Value) Value { + var a []Value + var lim int64 + if limit == nil || limit == _undefined { + lim = maxInt - 1 + } else { + lim = toLength(limit) + } + size := s.length() + p := int64(0) + if lim == 0 { + return r.newArrayValues(a) + } + execFn := toMethod(splitter.ToObject(r).self.getStr("exec")) // must be non-nil + + if size == 0 { + if r.regExpExec(execFn, splitter, s) == _null { + a = append(a, s) + } + return r.newArrayValues(a) + } + + q := p + for q < size { + splitter.self.putStr("lastIndex", intToValue(q), true) + z := r.regExpExec(execFn, splitter, s) + if z == _null { + q++ + } else { + z := r.toObject(z) + e := toLength(splitter.self.getStr("lastIndex")) + if e == p { + q++ + } else { + a = append(a, s.substring(p, q)) + if int64(len(a)) == lim { + return r.newArrayValues(a) + } + p = e + numberOfCaptures := max(toLength(z.self.getStr("length"))-1, 0) + for i := int64(1); i <= numberOfCaptures; i++ { + a = append(a, z.self.get(intToValue(i))) + if int64(len(a)) == lim { + return r.newArrayValues(a) + } + } + q = p + } + } + } + a = append(a, s.substring(p, size)) + return r.newArrayValues(a) +} + +func (r *Runtime) regexpproto_stdSplitter(call FunctionCall) Value { + rxObj := r.toObject(call.This) + c := r.speciesConstructor(rxObj, r.global.RegExp) + flags := nilSafe(rxObj.self.getStr("flags")).ToString() + + // Add 'y' flag if missing + if flagsStr := flags.String(); !strings.Contains(flagsStr, "y") { + flags = newStringValue(flagsStr + "y") + } + splitter := c([]Value{rxObj, flags}) + + s := call.Argument(0).ToString() + limitValue := call.Argument(1) + search := r.checkStdRegexp(splitter) + if search == nil { + return r.regexpproto_stdSplitterGeneric(splitter, s, limitValue) + } + + limit := -1 + if limitValue != _undefined { + limit = int(toUInt32(limitValue)) + } + + if limit == 0 { + return r.newArrayValues(nil) + } + + targetLength := s.length() + var valueArray []Value + result := search.pattern.FindAllSubmatchIndex(s, -1) + lastIndex := 0 + found := 0 + + for _, match := range result { + if match[0] == match[1] { + // FIXME Ugh, this is a hack + if match[0] == 0 || int64(match[0]) == targetLength { + continue + } + } + + if lastIndex != match[0] { + valueArray = append(valueArray, s.substring(int64(lastIndex), int64(match[0]))) + found++ + } else if lastIndex == match[0] { + if lastIndex != -1 { + valueArray = append(valueArray, stringEmpty) + found++ + } + } + + lastIndex = match[1] + if found == limit { + goto RETURN + } + + captureCount := len(match) / 2 + for index := 1; index < captureCount; index++ { + offset := index * 2 + var value Value + if match[offset] != -1 { + value = s.substring(int64(match[offset]), int64(match[offset+1])) + } else { + value = _undefined + } + valueArray = append(valueArray, value) + found++ + if found == limit { + goto RETURN + } + } + } + + if found != limit { + if int64(lastIndex) != targetLength { + valueArray = append(valueArray, s.substring(int64(lastIndex), targetLength)) + } else { + valueArray = append(valueArray, stringEmpty) + } + } + +RETURN: + return r.newArrayValues(valueArray) +} + func (r *Runtime) initRegExp() { r.global.RegExpPrototype = r.NewObject() o := r.global.RegExpPrototype.self - o._putProp("exec", r.newNativeFunc(r.regexpproto_exec, nil, "exec", nil, 1), true, false, true) + r.global.regexpProtoExec = valueProp(r.newNativeFunc(r.regexpproto_exec, nil, "exec", nil, 1), true, false, true) + o.putStr("exec", r.global.regexpProtoExec, true) o._putProp("test", r.newNativeFunc(r.regexpproto_test, nil, "test", nil, 1), true, false, true) o._putProp("toString", r.newNativeFunc(r.regexpproto_toString, nil, "toString", nil, 0), true, false, true) o.putStr("source", &valueProperty{ @@ -266,7 +605,27 @@ func (r *Runtime) initRegExp() { getterFunc: r.newNativeFunc(r.regexpproto_getIgnoreCase, nil, "get ignoreCase", nil, 0), accessor: true, }, false) + o.putStr("sticky", &valueProperty{ + configurable: true, + getterFunc: r.newNativeFunc(r.regexpproto_getSticky, nil, "get sticky", nil, 0), + accessor: true, + }, false) + o.putStr("flags", &valueProperty{ + configurable: true, + getterFunc: r.newNativeFunc(r.regexpproto_getFlags, nil, "get flags", nil, 0), + accessor: true, + }, false) + + o.put(symMatch, valueProp(r.newNativeFunc(r.regexpproto_stdMatcher, nil, "[Symbol.match]", nil, 1), true, false, true), true) + o.put(symSearch, valueProp(r.newNativeFunc(r.regexpproto_stdSearch, nil, "[Symbol.search]", nil, 1), true, false, true), true) + o.put(symSplit, valueProp(r.newNativeFunc(r.regexpproto_stdSplitter, nil, "[Symbol.split]", nil, 2), true, false, true), true) r.global.RegExp = r.newNativeFunc(r.builtin_RegExp, r.builtin_newRegExp, "RegExp", r.global.RegExpPrototype, 2) + o = r.global.RegExp.self + o.put(symSpecies, &valueProperty{ + getterFunc: r.newNativeFunc(r.returnThis, nil, "get [Symbol.species]", nil, 0), + accessor: true, + configurable: true, + }, true) r.addToGlobal("RegExp", r.global.RegExp) } diff --git a/builtin_string.go b/builtin_string.go index d659466a..91e6ada4 100644 --- a/builtin_string.go +++ b/builtin_string.go @@ -20,15 +20,21 @@ func (r *Runtime) collator() *collate.Collator { return collator } +func toString(arg Value) valueString { + if s, ok := arg.assertString(); ok { + return s + } + if s, ok := arg.(*valueSymbol); ok { + return newStringValue(s.descString()) + } + return arg.ToString() +} + func (r *Runtime) builtin_String(call FunctionCall) Value { if len(call.Arguments) > 0 { - arg := call.Arguments[0] - if _, ok := arg.assertString(); ok { - return arg - } - return arg.ToString() + return toString(call.Arguments[0]) } else { - return newStringValue("") + return stringEmpty } } @@ -51,7 +57,7 @@ func (r *Runtime) _newString(s valueString) *Object { func (r *Runtime) builtin_newString(args []Value) *Object { var s valueString if len(args) > 0 { - s = args[0].ToString() + s = toString(args[0]) } else { s = stringEmpty } @@ -226,8 +232,16 @@ func (r *Runtime) stringproto_localeCompare(call FunctionCall) Value { func (r *Runtime) stringproto_match(call FunctionCall) Value { r.checkObjectCoercible(call.This) - s := call.This.ToString() regexp := call.Argument(0) + if regexp != _undefined && regexp != _null { + if matcher := toMethod(regexp.ToObject(r).self.get(symMatch)); matcher != nil { + return matcher(FunctionCall{ + This: regexp, + Arguments: []Value{call.This}, + }) + } + } + var rx *regexpObject if regexp, ok := regexp.(*Object); ok { rx, _ = regexp.self.(*regexpObject) @@ -237,34 +251,29 @@ func (r *Runtime) stringproto_match(call FunctionCall) Value { rx = r.builtin_newRegExp([]Value{regexp}).self.(*regexpObject) } - if rx.global { - rx.putStr("lastIndex", intToValue(0), false) - var a []Value - var previousLastIndex int64 - for { - match, result := rx.execRegexp(s) - if !match { - break - } - thisIndex := rx.getStr("lastIndex").ToInteger() - if thisIndex == previousLastIndex { - previousLastIndex++ - rx.putStr("lastIndex", intToValue(previousLastIndex), false) - } else { - previousLastIndex = thisIndex - } - a = append(a, s.substring(int64(result[0]), int64(result[1]))) - } - if len(a) == 0 { - return _null - } - return r.newArrayValues(a) - } else { - return rx.exec(s) + if matcher, ok := r.toObject(rx.getSym(symMatch)).self.assertCallable(); ok { + return matcher(FunctionCall{ + This: rx.val, + Arguments: []Value{call.This.ToString()}, + }) } + + panic(r.NewTypeError("RegExp matcher is not a function")) } func (r *Runtime) stringproto_replace(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + searchValue := call.Argument(0) + replaceValue := call.Argument(1) + if searchValue != _undefined && searchValue != _null { + if replacer := toMethod(searchValue.ToObject(r).self.get(symReplace)); replacer != nil { + return replacer(FunctionCall{ + This: searchValue, + Arguments: []Value{call.This, replaceValue}, + }) + } + } + s := call.This.ToString() var str string var isASCII bool @@ -274,8 +283,6 @@ func (r *Runtime) stringproto_replace(call FunctionCall) Value { } else { str = s.String() } - searchValue := call.Argument(0) - replaceValue := call.Argument(1) var found [][]int @@ -408,8 +415,16 @@ func (r *Runtime) stringproto_replace(call FunctionCall) Value { func (r *Runtime) stringproto_search(call FunctionCall) Value { r.checkObjectCoercible(call.This) - s := call.This.ToString() regexp := call.Argument(0) + if regexp != _undefined && regexp != _null { + if searcher := toMethod(regexp.ToObject(r).self.get(symSearch)); searcher != nil { + return searcher(FunctionCall{ + This: regexp, + Arguments: []Value{call.This}, + }) + } + } + var rx *regexpObject if regexp, ok := regexp.(*Object); ok { rx, _ = regexp.self.(*regexpObject) @@ -419,11 +434,14 @@ func (r *Runtime) stringproto_search(call FunctionCall) Value { rx = r.builtin_newRegExp([]Value{regexp}).self.(*regexpObject) } - match, result := rx.execRegexp(s) - if !match { - return intToValue(-1) + if searcher, ok := r.toObject(rx.getSym(symSearch)).self.assertCallable(); ok { + return searcher(FunctionCall{ + This: rx.val, + Arguments: []Value{call.This.ToString()}, + }) } - return intToValue(int64(result[0])) + + panic(r.NewTypeError("RegExp searcher is not a function")) } func (r *Runtime) stringproto_slice(call FunctionCall) Value { @@ -469,10 +487,18 @@ func (r *Runtime) stringproto_slice(call FunctionCall) Value { func (r *Runtime) stringproto_split(call FunctionCall) Value { r.checkObjectCoercible(call.This) - s := call.This.ToString() - separatorValue := call.Argument(0) limitValue := call.Argument(1) + if separatorValue != _undefined && separatorValue != _null { + if splitter := toMethod(separatorValue.ToObject(r).self.get(symSplit)); splitter != nil { + return splitter(FunctionCall{ + This: separatorValue, + Arguments: []Value{call.This, limitValue}, + }) + } + } + s := call.This.ToString() + limit := -1 if limitValue != _undefined { limit = int(toUInt32(limitValue)) @@ -486,97 +512,31 @@ func (r *Runtime) stringproto_split(call FunctionCall) Value { return r.newArrayValues([]Value{s}) } - var search *regexpObject - if o, ok := separatorValue.(*Object); ok { - search, _ = o.self.(*regexpObject) - } - - if search != nil { - targetLength := s.length() - valueArray := []Value{} - result := search.pattern.FindAllSubmatchIndex(s, -1) - lastIndex := 0 - found := 0 - - for _, match := range result { - if match[0] == match[1] { - // FIXME Ugh, this is a hack - if match[0] == 0 || int64(match[0]) == targetLength { - continue - } - } - - if lastIndex != match[0] { - valueArray = append(valueArray, s.substring(int64(lastIndex), int64(match[0]))) - found++ - } else if lastIndex == match[0] { - if lastIndex != -1 { - valueArray = append(valueArray, stringEmpty) - found++ - } - } - - lastIndex = match[1] - if found == limit { - goto RETURN - } - - captureCount := len(match) / 2 - for index := 1; index < captureCount; index++ { - offset := index * 2 - var value Value - if match[offset] != -1 { - value = s.substring(int64(match[offset]), int64(match[offset+1])) - } else { - value = _undefined - } - valueArray = append(valueArray, value) - found++ - if found == limit { - goto RETURN - } - } - } - - if found != limit { - if int64(lastIndex) != targetLength { - valueArray = append(valueArray, s.substring(int64(lastIndex), targetLength)) - } else { - valueArray = append(valueArray, stringEmpty) - } - } - - RETURN: - return r.newArrayValues(valueArray) - - } else { - separator := separatorValue.String() + separator := separatorValue.String() - excess := false - str := s.String() - if limit > len(str) { - limit = len(str) - } - splitLimit := limit - if limit > 0 { - splitLimit = limit + 1 - excess = true - } - - split := strings.SplitN(str, separator, splitLimit) + excess := false + str := s.String() + if limit > len(str) { + limit = len(str) + } + splitLimit := limit + if limit > 0 { + splitLimit = limit + 1 + excess = true + } - if excess && len(split) > limit { - split = split[:limit] - } + split := strings.SplitN(str, separator, splitLimit) - valueArray := make([]Value, len(split)) - for index, value := range split { - valueArray[index] = newStringValue(value) - } + if excess && len(split) > limit { + split = split[:limit] + } - return r.newArrayValues(valueArray) + valueArray := make([]Value, len(split)) + for index, value := range split { + valueArray[index] = newStringValue(value) } + return r.newArrayValues(valueArray) } func (r *Runtime) stringproto_substring(call FunctionCall) Value { diff --git a/builtin_string_test.go b/builtin_string_test.go index ebf261eb..8f538ee0 100644 --- a/builtin_string_test.go +++ b/builtin_string_test.go @@ -88,3 +88,60 @@ assert.sameValue('Aβ€”', String.fromCharCode(65, 0x2014)); testScript1(TESTLIB+SCRIPT, _undefined, t) } + +func TestStringMatchSym(t *testing.T) { + const SCRIPT = ` +function Prefix(p) { + this.p = p; +} + +Prefix.prototype[Symbol.match] = function(s) { + return s.substring(0, this.p.length) === this.p; +} + +var prefix1 = new Prefix("abc"); +var prefix2 = new Prefix("def"); + +"abc123".match(prefix1) === true && "abc123".match(prefix2) === false && +"def123".match(prefix1) === false && "def123".match(prefix2) === true; +` + testScript1(SCRIPT, valueTrue, t) +} + +func TestGenericSplitter(t *testing.T) { + const SCRIPT = ` +function MyRegexp(pattern, flags) { + if (pattern instanceof MyRegexp) { + pattern = pattern.wrapped; + } + this.wrapped = new RegExp(pattern, flags); +} + +MyRegexp.prototype.exec = function() { + return this.wrapped.exec.apply(this.wrapped, arguments); +} + +Object.defineProperty(MyRegexp.prototype, "lastIndex", { + get: function() { + return this.wrapped.lastIndex; + }, + set: function(v) { + this.wrapped.lastIndex = v; + } +}); + +Object.defineProperty(MyRegexp.prototype, "flags", { + get: function() { + return this.wrapped.flags; + } +}); + +MyRegexp[Symbol.species] = MyRegexp; +MyRegexp.prototype[Symbol.split] = RegExp.prototype[Symbol.split]; + +var r = new MyRegexp(/ /); +var res = "a b c".split(r); +res.length === 3 && res[0] === "a" && res[1] === "b" && res[2] === "c"; +` + testScript1(SCRIPT, valueTrue, t) +} diff --git a/builtin_symbol.go b/builtin_symbol.go new file mode 100644 index 00000000..25ac0d31 --- /dev/null +++ b/builtin_symbol.go @@ -0,0 +1,139 @@ +package goja + +var ( + symHasInstance = &valueSymbol{desc: "Symbol.hasInstance"} + symIsConcatSpreadable = &valueSymbol{desc: "Symbol.isConcatSpreadable"} + symIterator = &valueSymbol{desc: "Symbol.iterator"} + symMatch = &valueSymbol{desc: "Symbol.match"} + symReplace = &valueSymbol{desc: "Symbol.replace"} + symSearch = &valueSymbol{desc: "Symbol.search"} + symSpecies = &valueSymbol{desc: "Symbol.species"} + symSplit = &valueSymbol{desc: "Symbol.split"} + symToPrimitive = &valueSymbol{desc: "Symbol.toPrimitive"} + symToStringTag = &valueSymbol{desc: "Symbol.toStringTag"} + symUnscopables = &valueSymbol{desc: "Symbol.unscopables"} +) + +func (r *Runtime) builtin_symbol(call FunctionCall) Value { + desc := "" + if arg := call.Argument(0); !IsUndefined(arg) { + desc = arg.ToString().String() + } + return &valueSymbol{ + desc: desc, + } +} + +func (r *Runtime) symbolproto_tostring(call FunctionCall) Value { + sym, ok := call.This.(*valueSymbol) + if !ok { + if obj, ok := call.This.(*Object); ok { + if v, ok := obj.self.(*primitiveValueObject); ok { + if sym1, ok := v.pValue.(*valueSymbol); ok { + sym = sym1 + } + } + } + } + if sym == nil { + panic(r.NewTypeError("Method Symbol.prototype.toString is called on incompatible receiver")) + } + return newStringValue(sym.descString()) +} + +func (r *Runtime) symbolproto_valueOf(call FunctionCall) Value { + _, ok := call.This.(*valueSymbol) + if ok { + return call.This + } + + if obj, ok := call.This.(*Object); ok { + if v, ok := obj.self.(*primitiveValueObject); ok { + if sym, ok := v.pValue.(*valueSymbol); ok { + return sym + } + } + } + + panic(r.NewTypeError("Symbol.prototype.valueOf requires that 'this' be a Symbol")) +} + +func (r *Runtime) symbol_for(call FunctionCall) Value { + key := call.Argument(0).ToString().String() + if v := r.symbolRegistry[key]; v != nil { + return v + } + if r.symbolRegistry == nil { + r.symbolRegistry = make(map[string]*valueSymbol) + } + v := &valueSymbol{ + desc: key, + } + r.symbolRegistry[key] = v + return v +} + +func (r *Runtime) symbol_keyfor(call FunctionCall) Value { + arg := call.Argument(0) + sym, ok := arg.(*valueSymbol) + if !ok { + panic(r.NewTypeError("%s is not a symbol", arg.String())) + } + for key, s := range r.symbolRegistry { + if s == sym { + return r.ToValue(key) + } + } + return _undefined +} + +func (r *Runtime) createSymbolProto(val *Object) objectImpl { + o := &baseObject{ + class: classObject, + val: val, + extensible: true, + prototype: r.global.ObjectPrototype, + } + o.init() + + o._putProp("constructor", r.global.Symbol, true, false, true) + o._putProp("toString", r.newNativeFunc(r.symbolproto_tostring, nil, "toString", nil, 0), true, false, true) + o._putProp("valueOf", r.newNativeFunc(r.symbolproto_valueOf, nil, "valueOf", nil, 0), true, false, true) + o.putSym(symToPrimitive, valueProp(r.newNativeFunc(r.symbolproto_valueOf, nil, "[Symbol.toPrimitive]", nil, 1), false, false, true), true) + o.putSym(symToStringTag, valueProp(newStringValue("Symbol"), false, false, true), true) + + return o +} + +func (r *Runtime) createSymbol(val *Object) objectImpl { + o := r.newNativeFuncObj(val, r.builtin_symbol, nil, "Symbol", r.global.SymbolPrototype, 0) + + o._putProp("for", r.newNativeFunc(r.symbol_for, nil, "for", nil, 1), true, false, true) + o._putProp("keyFor", r.newNativeFunc(r.symbol_keyfor, nil, "keyFor", nil, 1), true, false, true) + + for _, s := range []*valueSymbol{ + symHasInstance, + symIsConcatSpreadable, + symIterator, + symMatch, + symReplace, + symSearch, + symSpecies, + symSplit, + symToPrimitive, + symToStringTag, + symUnscopables, + } { + o._putProp(s.desc[len("Symbol."):], s, false, false, false) + } + + return o +} + +func (r *Runtime) initSymbol() { + r.global.SymbolPrototype = r.newLazyObject(r.createSymbolProto) + + r.global.Symbol = r.newLazyObject(r.createSymbol) + r.addToGlobal("Symbol", r.global.Symbol) + +} diff --git a/compiler_expr.go b/compiler_expr.go index c8e9fce7..0d966ea6 100644 --- a/compiler_expr.go +++ b/compiler_expr.go @@ -1388,7 +1388,7 @@ func (c *compiler) compileArrayLiteral(v *ast.ArrayLiteral) compiledExpr { func (e *compiledRegexpLiteral) emitGetter(putOnStack bool) { if putOnStack { - pattern, global, ignoreCase, multiline, err := compileRegexp(e.expr.Pattern, e.expr.Flags) + pattern, global, ignoreCase, multiline, sticky, err := compileRegexp(e.expr.Pattern, e.expr.Flags) if err != nil { e.c.throwSyntaxError(e.offset, err.Error()) } @@ -1398,6 +1398,7 @@ func (e *compiledRegexpLiteral) emitGetter(putOnStack bool) { global: global, ignoreCase: ignoreCase, multiline: multiline, + sticky: sticky, }) } } diff --git a/date_test.go b/date_test.go index def2df69..e27b2227 100644 --- a/date_test.go +++ b/date_test.go @@ -10,6 +10,9 @@ function $ERROR(message) { throw new Error(message); } +function Test262Error() { +} + function assert(mustBeTrue, message) { if (mustBeTrue === true) { return; @@ -47,6 +50,34 @@ assert.sameValue = function (actual, expected, message) { $ERROR(message); }; +assert.throws = function (expectedErrorConstructor, func, message) { + if (typeof func !== "function") { + $ERROR('assert.throws requires two arguments: the error constructor ' + + 'and a function to run'); + return; + } + if (message === undefined) { + message = ''; + } else { + message += ' '; + } + + try { + func(); + } catch (thrown) { + if (typeof thrown !== 'object' || thrown === null) { + message += 'Thrown value was not an object!'; + $ERROR(message); + } else if (thrown.constructor !== expectedErrorConstructor) { + message += 'Expected a ' + expectedErrorConstructor.name + ' but got a ' + thrown.constructor.name; + $ERROR(message); + } + return; + } + + message += 'Expected a ' + expectedErrorConstructor.name + ' to be thrown but no exception was thrown at all'; + $ERROR(message); +}; ` diff --git a/func.go b/func.go index a4927b00..ba816cee 100644 --- a/func.go +++ b/func.go @@ -25,6 +25,7 @@ type nativeFuncObject struct { type boundFuncObject struct { nativeFuncObject + wrapped *Object } func (f *nativeFuncObject) export() interface{} { @@ -52,10 +53,6 @@ func (f *funcObject) addPrototype() Value { return f._putProp("prototype", proto, true, false, false) } -func (f *funcObject) getProp(n Value) Value { - return f.getPropStr(n.String()) -} - func (f *funcObject) hasOwnProperty(n Value) bool { if r := f.baseObject.hasOwnProperty(n); r { return true @@ -205,10 +202,6 @@ func (f *nativeFuncObject) assertCallable() (func(FunctionCall) Value, bool) { return nil, false } -func (f *boundFuncObject) getProp(n Value) Value { - return f.getPropStr(n.String()) -} - func (f *boundFuncObject) getPropStr(name string) Value { if name == "caller" || name == "arguments" { //f.runtime.typeErrorResult(true, "'caller' and 'arguments' are restricted function properties and cannot be accessed in this context.") @@ -218,6 +211,9 @@ func (f *boundFuncObject) getPropStr(name string) Value { } func (f *boundFuncObject) delete(n Value, throw bool) bool { + if s, ok := n.(*valueSymbol); ok { + return f.deleteSym(s, throw) + } return f.deleteStr(n.String(), throw) } @@ -236,5 +232,13 @@ func (f *boundFuncObject) putStr(name string, val Value, throw bool) { } func (f *boundFuncObject) put(n Value, val Value, throw bool) { + if s, ok := n.(*valueSymbol); ok { + f.putSym(s, val, throw) + return + } f.putStr(n.String(), val, throw) } + +func (f *boundFuncObject) hasInstance(v Value) bool { + return instanceOfOperator(v, f.wrapped) +} diff --git a/object.go b/object.go index 85b3f600..23f3adba 100644 --- a/object.go +++ b/object.go @@ -1,6 +1,9 @@ package goja -import "reflect" +import ( + "fmt" + "reflect" +) const ( classObject = "Object" @@ -12,6 +15,8 @@ const ( classError = "Error" classRegExp = "RegExp" classDate = "Date" + + classArrayIterator = "Array Iterator" ) type Object struct { @@ -36,7 +41,8 @@ type objectImpl interface { getProp(Value) Value getPropStr(string) Value getStr(string) Value - getOwnProp(string) Value + getOwnProp(Value) Value + getOwnPropStr(string) Value put(Value, Value, bool) putStr(string, Value, bool) hasProperty(Value) bool @@ -60,6 +66,7 @@ type objectImpl interface { export() interface{} exportType() reflect.Type equal(objectImpl) bool + getOwnSymbols() []Value } type baseObject struct { @@ -70,6 +77,8 @@ type baseObject struct { values map[string]Value propNames []string + + symValues map[*valueSymbol]Value } type primitiveValueObject struct { @@ -118,7 +127,7 @@ func (o *baseObject) className() string { } func (o *baseObject) getPropStr(name string) Value { - if val := o.getOwnProp(name); val != nil { + if val := o.getOwnPropStr(name); val != nil { return val } if o.prototype != nil { @@ -127,7 +136,20 @@ func (o *baseObject) getPropStr(name string) Value { return nil } +func (o *baseObject) getPropSym(s *valueSymbol) Value { + if val := o.symValues[s]; val != nil { + return val + } + if o.prototype != nil { + return o.prototype.self.getProp(s) + } + return nil +} + func (o *baseObject) getProp(n Value) Value { + if s, ok := n.(*valueSymbol); ok { + return o.getPropSym(s) + } return o.val.self.getPropStr(n.String()) } @@ -140,7 +162,7 @@ func (o *baseObject) hasPropertyStr(name string) bool { } func (o *baseObject) _getStr(name string) Value { - p := o.getOwnProp(name) + p := o.getOwnPropStr(name) if p == nil && o.prototype != nil { p = o.prototype.self.getPropStr(name) @@ -162,7 +184,19 @@ func (o *baseObject) getStr(name string) Value { return p } +func (o *baseObject) getSym(s *valueSymbol) Value { + p := o.getPropSym(s) + if p, ok := p.(*valueProperty); ok { + return p.get(o.val) + } + + return p +} + func (o *baseObject) get(n Value) Value { + if s, ok := n.(*valueSymbol); ok { + return o.getSym(s) + } return o.getStr(n.String()) } @@ -198,20 +232,36 @@ func (o *baseObject) deleteStr(name string, throw bool) bool { return false } o._delete(name) - return true + } + return true +} + +func (o *baseObject) deleteSym(s *valueSymbol, throw bool) bool { + if val, exists := o.symValues[s]; exists { + if !o.checkDelete(s.String(), val, throw) { + return false + } + delete(o.symValues, s) } return true } func (o *baseObject) delete(n Value, throw bool) bool { + if s, ok := n.(*valueSymbol); ok { + return o.deleteSym(s, throw) + } return o.deleteStr(n.String(), throw) } func (o *baseObject) put(n Value, val Value, throw bool) { - o.putStr(n.String(), val, throw) + if s, ok := n.(*valueSymbol); ok { + o.putSym(s, val, throw) + } else { + o.putStr(n.String(), val, throw) + } } -func (o *baseObject) getOwnProp(name string) Value { +func (o *baseObject) getOwnPropStr(name string) Value { v := o.values[name] if v == nil && name == __proto__ { return o.prototype @@ -219,6 +269,14 @@ func (o *baseObject) getOwnProp(name string) Value { return v } +func (o *baseObject) getOwnProp(name Value) Value { + if s, ok := name.(*valueSymbol); ok { + return o.symValues[s] + } + + return o.val.self.getOwnPropStr(name.String()) +} + func (o *baseObject) putStr(name string, val Value, throw bool) { if v, exists := o.values[name]; exists { if prop, ok := v.(*valueProperty); ok { @@ -276,7 +334,54 @@ func (o *baseObject) putStr(name string, val Value, throw bool) { o.propNames = append(o.propNames, name) } +func (o *baseObject) putSym(s *valueSymbol, val Value, throw bool) { + if v, exists := o.symValues[s]; exists { + if prop, ok := v.(*valueProperty); ok { + if !prop.isWritable() { + o.val.runtime.typeErrorResult(throw, "Cannot assign to read only property '%s'", s.String()) + return + } + prop.set(o.val, val) + return + } + o.symValues[s] = val + return + } + + var pprop Value + if proto := o.prototype; proto != nil { + pprop = proto.self.getProp(s) + } + + if pprop != nil { + if prop, ok := pprop.(*valueProperty); ok { + if !prop.isWritable() { + o.val.runtime.typeErrorResult(throw) + return + } + if prop.accessor { + prop.set(o.val, val) + return + } + } + } else { + if !o.extensible { + o.val.runtime.typeErrorResult(throw) + return + } + } + + if o.symValues == nil { + o.symValues = make(map[*valueSymbol]Value, 1) + } + o.symValues[s] = val +} + func (o *baseObject) hasOwnProperty(n Value) bool { + if s, ok := n.(*valueSymbol); ok { + _, exists := o.symValues[s] + return exists + } v := o.values[n.String()] return v != nil } @@ -390,6 +495,17 @@ Reject: } func (o *baseObject) defineOwnProperty(n Value, descr propertyDescr, throw bool) bool { + if s, ok := n.(*valueSymbol); ok { + existingVal := o.symValues[s] + if v, ok := o._defineOwnProperty(n, existingVal, descr, throw); ok { + if o.symValues == nil { + o.symValues = make(map[*valueSymbol]Value, 1) + } + o.symValues[s] = v + return true + } + return false + } name := n.String() existingVal := o.values[name] if v, ok := o._defineOwnProperty(n, existingVal, descr, throw); ok { @@ -410,22 +526,35 @@ func (o *baseObject) _put(name string, v Value) { o.values[name] = v } -func (o *baseObject) _putProp(name string, value Value, writable, enumerable, configurable bool) Value { +func valueProp(value Value, writable, enumerable, configurable bool) Value { if writable && enumerable && configurable { - o._put(name, value) return value - } else { - p := &valueProperty{ - value: value, - writable: writable, - enumerable: enumerable, - configurable: configurable, - } - o._put(name, p) - return p + } + return &valueProperty{ + value: value, + writable: writable, + enumerable: enumerable, + configurable: configurable, } } +func (o *baseObject) _putProp(name string, value Value, writable, enumerable, configurable bool) Value { + prop := valueProp(value, writable, enumerable, configurable) + o._put(name, prop) + return prop +} + +func (o *baseObject) tryExoticToPrimitive(hint string) Value { + exoticToPrimitive := toMethod(o.getSym(symToPrimitive)) + if exoticToPrimitive != nil { + return exoticToPrimitive(FunctionCall{ + This: o.val, + Arguments: []Value{newStringValue(hint)}, + }) + } + return nil +} + func (o *baseObject) tryPrimitive(methodName string) Value { if method, ok := o.getStr(methodName).(*Object); ok { if call, ok := method.self.assertCallable(); ok { @@ -441,6 +570,10 @@ func (o *baseObject) tryPrimitive(methodName string) Value { } func (o *baseObject) toPrimitiveNumber() Value { + if v := o.tryExoticToPrimitive("number"); v != nil { + return v + } + if v := o.tryPrimitive("valueOf"); v != nil { return v } @@ -454,6 +587,10 @@ func (o *baseObject) toPrimitiveNumber() Value { } func (o *baseObject) toPrimitiveString() Value { + if v := o.tryExoticToPrimitive("string"); v != nil { + return v + } + if v := o.tryPrimitive("toString"); v != nil { return v } @@ -614,12 +751,42 @@ func (o *baseObject) enumerate(all, recursive bool) iterNextFunc { }).next } -func (o *baseObject) equal(other objectImpl) bool { +func (o *baseObject) equal(objectImpl) bool { // Rely on parent reference comparison return false } -func (o *baseObject) hasInstance(v Value) bool { - o.val.runtime.typeErrorResult(true, "Expecting a function in instanceof check, but got %s", o.val.ToString()) - panic("Unreachable") +func (o *baseObject) getOwnSymbols() (res []Value) { + for s := range o.symValues { + res = append(res, s) + } + + return +} + +func (o *baseObject) hasInstance(Value) bool { + panic(o.val.runtime.NewTypeError("Expecting a function in instanceof check, but got %s", o.val.ToString())) +} + +func toMethod(v Value) func(FunctionCall) Value { + if v == nil || IsUndefined(v) || IsNull(v) { + return nil + } + if obj, ok := v.(*Object); ok { + if call, ok := obj.self.assertCallable(); ok { + return call + } + } + panic(typeError(fmt.Sprintf("%s is not a method", v.String()))) +} + +func instanceOfOperator(o Value, c *Object) bool { + if instOfHandler := toMethod(c.self.get(symHasInstance)); instOfHandler != nil { + return instOfHandler(FunctionCall{ + This: c, + Arguments: []Value{o}, + }).ToBoolean() + } + + return c.self.hasInstance(o) } diff --git a/object_args.go b/object_args.go index 16113dec..d0ee818e 100644 --- a/object_args.go +++ b/object_args.go @@ -27,6 +27,10 @@ func (a *argumentsObject) init() { } func (a *argumentsObject) put(n Value, val Value, throw bool) { + if s, ok := n.(*valueSymbol); ok { + a.putSym(s, val, throw) + return + } a.putStr(n.String(), val, throw) } @@ -55,15 +59,12 @@ func (a *argumentsObject) deleteStr(name string, throw bool) bool { } func (a *argumentsObject) delete(n Value, throw bool) bool { + if s, ok := n.(*valueSymbol); ok { + return a.deleteSym(s, throw) + } return a.deleteStr(n.String(), throw) } -type argumentsPropIter1 struct { - a *argumentsObject - idx int - recursive bool -} - type argumentsPropIter struct { wrapped iterNextFunc } @@ -94,6 +95,9 @@ func (a *argumentsObject) enumerate(all, recursive bool) iterNextFunc { } func (a *argumentsObject) defineOwnProperty(n Value, descr propertyDescr, throw bool) bool { + if _, ok := n.(*valueSymbol); ok { + return a.baseObject.defineOwnProperty(n, descr, throw) + } name := n.String() if mapped, ok := a.values[name].(*mappedProperty); ok { existing := &valueProperty{ @@ -130,17 +134,17 @@ func (a *argumentsObject) defineOwnProperty(n Value, descr propertyDescr, throw return a.baseObject.defineOwnProperty(n, descr, throw) } -func (a *argumentsObject) getOwnProp(name string) Value { +func (a *argumentsObject) getOwnPropStr(name string) Value { if mapped, ok := a.values[name].(*mappedProperty); ok { return *mapped.v } - return a.baseObject.getOwnProp(name) + return a.baseObject.getOwnPropStr(name) } func (a *argumentsObject) export() interface{} { arr := make([]interface{}, a.length) - for i, _ := range arr { + for i := range arr { v := a.get(intToValue(int64(i))) if v != nil { arr[i] = v.Export() diff --git a/object_gomap.go b/object_gomap.go index 678c5e30..54c826a0 100644 --- a/object_gomap.go +++ b/object_gomap.go @@ -51,14 +51,18 @@ func (o *objectGoMapSimple) getStr(name string) Value { return o.baseObject._getStr(name) } -func (o *objectGoMapSimple) getOwnProp(name string) Value { +func (o *objectGoMapSimple) getOwnPropStr(name string) Value { if v := o._getStr(name); v != nil { return v } - return o.baseObject.getOwnProp(name) + return o.baseObject.getOwnPropStr(name) } func (o *objectGoMapSimple) put(n Value, val Value, throw bool) { + if _, ok := n.(*valueSymbol); ok { + o.val.runtime.typeErrorResult(throw, "Cannot set Symbol properties on Go maps") + return + } o.putStr(n.String(), val, throw) } @@ -107,8 +111,7 @@ func (o *objectGoMapSimple) _putProp(name string, value Value, writable, enumera } func (o *objectGoMapSimple) defineOwnProperty(name Value, descr propertyDescr, throw bool) bool { - if descr.Getter != nil || descr.Setter != nil { - o.val.runtime.typeErrorResult(throw, "Host objects do not support accessor properties") + if !o.val.runtime.checkHostObjectPropertyDescr(name, descr, throw) { return false } o.put(name, descr.Value, throw) @@ -139,6 +142,10 @@ func (o *objectGoMapSimple) deleteStr(name string, throw bool) bool { } func (o *objectGoMapSimple) delete(name Value, throw bool) bool { + if _, ok := name.(*valueSymbol); ok { + return true + } + return o.deleteStr(name.String(), throw) } @@ -176,7 +183,7 @@ func (o *objectGoMapSimple) enumerate(all, recursive bool) iterNextFunc { func (o *objectGoMapSimple) _enumerate(recursive bool) iterNextFunc { propNames := make([]string, len(o.data)) i := 0 - for key, _ := range o.data { + for key := range o.data { propNames[i] = key i++ } diff --git a/object_gomap_reflect.go b/object_gomap_reflect.go index a040d363..fee6db3c 100644 --- a/object_gomap_reflect.go +++ b/object_gomap_reflect.go @@ -14,24 +14,32 @@ func (o *objectGoMapReflect) init() { o.valueType = o.value.Type().Elem() } -func (o *objectGoMapReflect) toKey(n Value) reflect.Value { +func (o *objectGoMapReflect) toKey(n Value, throw bool) reflect.Value { + if _, ok := n.(*valueSymbol); ok { + o.val.runtime.typeErrorResult(throw, "Cannot set Symbol properties on Go maps") + return reflect.Value{} + } key, err := o.val.runtime.toReflectValue(n, o.keyType) if err != nil { - o.val.runtime.typeErrorResult(true, "map key conversion error: %v", err) - panic("unreachable") + o.val.runtime.typeErrorResult(throw, "map key conversion error: %v", err) + return reflect.Value{} } return key } -func (o *objectGoMapReflect) strToKey(name string) reflect.Value { +func (o *objectGoMapReflect) strToKey(name string, throw bool) reflect.Value { if o.keyType.Kind() == reflect.String { return reflect.ValueOf(name).Convert(o.keyType) } - return o.toKey(newStringValue(name)) + return o.toKey(newStringValue(name), throw) } func (o *objectGoMapReflect) _get(n Value) Value { - if v := o.value.MapIndex(o.toKey(n)); v.IsValid() { + key := o.toKey(n, false) + if !key.IsValid() { + return nil + } + if v := o.value.MapIndex(key); v.IsValid() { return o.val.runtime.ToValue(v.Interface()) } @@ -39,7 +47,11 @@ func (o *objectGoMapReflect) _get(n Value) Value { } func (o *objectGoMapReflect) _getStr(name string) Value { - if v := o.value.MapIndex(o.strToKey(name)); v.IsValid() { + key := o.strToKey(name, false) + if !key.IsValid() { + return nil + } + if v := o.value.MapIndex(key); v.IsValid() { return o.val.runtime.ToValue(v.Interface()) } @@ -68,7 +80,7 @@ func (o *objectGoMapReflect) getPropStr(name string) Value { return o.getStr(name) } -func (o *objectGoMapReflect) getOwnProp(name string) Value { +func (o *objectGoMapReflect) getOwnPropStr(name string) Value { if v := o._getStr(name); v != nil { return &valueProperty{ value: v, @@ -76,7 +88,7 @@ func (o *objectGoMapReflect) getOwnProp(name string) Value { enumerable: true, } } - return o.objectGoReflect.getOwnProp(name) + return o.objectGoReflect.getOwnPropStr(name) } func (o *objectGoMapReflect) toValue(val Value, throw bool) (reflect.Value, bool) { @@ -90,7 +102,7 @@ func (o *objectGoMapReflect) toValue(val Value, throw bool) (reflect.Value, bool } func (o *objectGoMapReflect) put(key, val Value, throw bool) { - k := o.toKey(key) + k := o.toKey(key, throw) v, ok := o.toValue(val, throw) if !ok { return @@ -99,7 +111,10 @@ func (o *objectGoMapReflect) put(key, val Value, throw bool) { } func (o *objectGoMapReflect) putStr(name string, val Value, throw bool) { - k := o.strToKey(name) + k := o.strToKey(name, throw) + if !k.IsValid() { + return + } v, ok := o.toValue(val, throw) if !ok { return @@ -113,8 +128,7 @@ func (o *objectGoMapReflect) _putProp(name string, value Value, writable, enumer } func (o *objectGoMapReflect) defineOwnProperty(n Value, descr propertyDescr, throw bool) bool { - name := n.String() - if !o.val.runtime.checkHostObjectPropertyDescr(name, descr, throw) { + if !o.val.runtime.checkHostObjectPropertyDescr(n, descr, throw) { return false } @@ -123,11 +137,20 @@ func (o *objectGoMapReflect) defineOwnProperty(n Value, descr propertyDescr, thr } func (o *objectGoMapReflect) hasOwnPropertyStr(name string) bool { - return o.value.MapIndex(o.strToKey(name)).IsValid() + key := o.strToKey(name, false) + if !key.IsValid() { + return false + } + return o.value.MapIndex(key).IsValid() } func (o *objectGoMapReflect) hasOwnProperty(n Value) bool { - return o.value.MapIndex(o.toKey(n)).IsValid() + key := o.toKey(n, false) + if !key.IsValid() { + return false + } + + return o.value.MapIndex(key).IsValid() } func (o *objectGoMapReflect) hasProperty(n Value) bool { @@ -145,12 +168,20 @@ func (o *objectGoMapReflect) hasPropertyStr(name string) bool { } func (o *objectGoMapReflect) delete(n Value, throw bool) bool { - o.value.SetMapIndex(o.toKey(n), reflect.Value{}) + key := o.toKey(n, throw) + if !key.IsValid() { + return false + } + o.value.SetMapIndex(key, reflect.Value{}) return true } func (o *objectGoMapReflect) deleteStr(name string, throw bool) bool { - o.value.SetMapIndex(o.strToKey(name), reflect.Value{}) + key := o.strToKey(name, throw) + if !key.IsValid() { + return false + } + o.value.SetMapIndex(key, reflect.Value{}) return true } diff --git a/object_goreflect.go b/object_goreflect.go index 4d94d61b..73a1edea 100644 --- a/object_goreflect.go +++ b/object_goreflect.go @@ -127,22 +127,14 @@ func (o *objectGoReflect) getStr(name string) Value { return o.baseObject._getStr(name) } -func (o *objectGoReflect) getProp(n Value) Value { - name := n.String() - if p := o.getOwnProp(name); p != nil { - return p - } - return o.baseObject.getOwnProp(name) -} - func (o *objectGoReflect) getPropStr(name string) Value { - if v := o.getOwnProp(name); v != nil { + if v := o.getOwnPropStr(name); v != nil { return v } return o.baseObject.getPropStr(name) } -func (o *objectGoReflect) getOwnProp(name string) Value { +func (o *objectGoReflect) getOwnPropStr(name string) Value { if o.value.Kind() == reflect.Struct { if v := o._getField(name); v.IsValid() { return &valueProperty{ @@ -164,6 +156,10 @@ func (o *objectGoReflect) getOwnProp(name string) Value { } func (o *objectGoReflect) put(n Value, val Value, throw bool) { + if _, ok := n.(*valueSymbol); ok { + o.val.runtime.typeErrorResult(throw, "Cannot assign to Symbol property %s of a host object", n.String()) + return + } o.putStr(n.String(), val, throw) } @@ -199,40 +195,46 @@ func (o *objectGoReflect) _putProp(name string, value Value, writable, enumerabl return o.baseObject._putProp(name, value, writable, enumerable, configurable) } -func (r *Runtime) checkHostObjectPropertyDescr(name string, descr propertyDescr, throw bool) bool { +func (r *Runtime) checkHostObjectPropertyDescr(n Value, descr propertyDescr, throw bool) bool { + if _, ok := n.(*valueSymbol); ok { + r.typeErrorResult(throw, "Host objects do not support symbol properties") + return false + } if descr.Getter != nil || descr.Setter != nil { r.typeErrorResult(throw, "Host objects do not support accessor properties") return false } if descr.Writable == FLAG_FALSE { - r.typeErrorResult(throw, "Host object field %s cannot be made read-only", name) + r.typeErrorResult(throw, "Host object field %s cannot be made read-only", n.String()) return false } if descr.Configurable == FLAG_TRUE { - r.typeErrorResult(throw, "Host object field %s cannot be made configurable", name) + r.typeErrorResult(throw, "Host object field %s cannot be made configurable", n.String()) return false } return true } func (o *objectGoReflect) defineOwnProperty(n Value, descr propertyDescr, throw bool) bool { - if o.value.Kind() == reflect.Struct { - name := n.String() - if v := o._getField(name); v.IsValid() { - if !o.val.runtime.checkHostObjectPropertyDescr(name, descr, throw) { - return false - } - val := descr.Value - if val == nil { - val = _undefined - } - vv, err := o.val.runtime.toReflectValue(val, v.Type()) - if err != nil { - o.val.runtime.typeErrorResult(throw, "Go struct conversion error: %v", err) - return false + if _, ok := n.(*valueSymbol); !ok { + if o.value.Kind() == reflect.Struct { + name := n.String() + if v := o._getField(name); v.IsValid() { + if !o.val.runtime.checkHostObjectPropertyDescr(n, descr, throw) { + return false + } + val := descr.Value + if val == nil { + val = _undefined + } + vv, err := o.val.runtime.toReflectValue(val, v.Type()) + if err != nil { + o.val.runtime.typeErrorResult(throw, "Go struct conversion error: %v", err) + return false + } + v.Set(vv) + return true } - v.Set(vv) - return true } } diff --git a/object_goslice.go b/object_goslice.go index cb7df72e..c13ea040 100644 --- a/object_goslice.go +++ b/object_goslice.go @@ -74,7 +74,7 @@ func (o *objectGoSlice) getPropStr(name string) Value { return o.baseObject.getPropStr(name) } -func (o *objectGoSlice) getOwnProp(name string) Value { +func (o *objectGoSlice) getOwnPropStr(name string) Value { if v := o._getStr(name); v != nil { return &valueProperty{ value: v, @@ -82,7 +82,7 @@ func (o *objectGoSlice) getOwnProp(name string) Value { enumerable: true, } } - return o.baseObject.getOwnProp(name) + return o.baseObject.getOwnPropStr(name) } func (o *objectGoSlice) grow(size int64) { @@ -189,7 +189,7 @@ func (o *objectGoSlice) _putProp(name string, value Value, writable, enumerable, func (o *objectGoSlice) defineOwnProperty(n Value, descr propertyDescr, throw bool) bool { if idx := toIdx(n); idx >= 0 { - if !o.val.runtime.checkHostObjectPropertyDescr(n.String(), descr, throw) { + if !o.val.runtime.checkHostObjectPropertyDescr(n, descr, throw) { return false } val := descr.Value diff --git a/object_goslice_reflect.go b/object_goslice_reflect.go index d0ac9d24..ca4bd2a4 100644 --- a/object_goslice_reflect.go +++ b/object_goslice_reflect.go @@ -79,11 +79,11 @@ func (o *objectGoSliceReflect) getPropStr(name string) Value { return o.objectGoReflect.getPropStr(name) } -func (o *objectGoSliceReflect) getOwnProp(name string) Value { +func (o *objectGoSliceReflect) getOwnPropStr(name string) Value { if v := o._getStr(name); v != nil { return v } - return o.objectGoReflect.getOwnProp(name) + return o.objectGoReflect.getOwnPropStr(name) } func (o *objectGoSliceReflect) putIdx(idx int64, v Value, throw bool) { @@ -151,7 +151,7 @@ func (o *objectGoSliceReflect) _putProp(name string, value Value, writable, enum } func (o *objectGoSliceReflect) defineOwnProperty(name Value, descr propertyDescr, throw bool) bool { - if !o.val.runtime.checkHostObjectPropertyDescr(name.String(), descr, throw) { + if !o.val.runtime.checkHostObjectPropertyDescr(name, descr, throw) { return false } o.put(name, descr.Value, throw) diff --git a/object_lazy.go b/object_lazy.go index 6fb405d0..4d4fa0ab 100644 --- a/object_lazy.go +++ b/object_lazy.go @@ -37,7 +37,13 @@ func (o *lazyObject) getStr(name string) Value { return obj.getStr(name) } -func (o *lazyObject) getOwnProp(name string) Value { +func (o *lazyObject) getOwnPropStr(name string) Value { + obj := o.create(o.val) + o.val.self = obj + return obj.getOwnPropStr(name) +} + +func (o *lazyObject) getOwnProp(name Value) Value { obj := o.create(o.val) o.val.self = obj return obj.getOwnProp(name) @@ -181,6 +187,12 @@ func (o *lazyObject) equal(other objectImpl) bool { return obj.equal(other) } +func (o *lazyObject) getOwnSymbols() []Value { + obj := o.create(o.val) + o.val.self = obj + return obj.getOwnSymbols() +} + func (o *lazyObject) sortLen() int64 { obj := o.create(o.val) o.val.self = obj diff --git a/regexp.go b/regexp.go index 9c20d80f..b50b56dd 100644 --- a/regexp.go +++ b/regexp.go @@ -24,7 +24,7 @@ type regexpObject struct { pattern regexpPattern source valueString - global, multiline, ignoreCase bool + global, multiline, ignoreCase, sticky bool } func (r *regexp2Wrapper) FindSubmatchIndex(s valueString, start int) (result []int) { @@ -308,26 +308,24 @@ func (r *regexpObject) execRegexp(target valueString) (match bool, result []int) } } index := lastIndex - if !r.global { + if !r.global && !r.sticky { index = 0 } if index >= 0 && index <= target.length() { result = r.pattern.FindSubmatchIndex(target, int(index)) } - if result == nil { + if result == nil || r.sticky && result[0] != 0 { r.putStr("lastIndex", intToValue(0), true) return } match = true - startIndex := index - endIndex := int(lastIndex) + result[1] // We do this shift here because the .FindStringSubmatchIndex above // was done on a local subordinate slice of the string, not the whole string - for index, _ := range result { - result[index] += int(startIndex) + for i := range result { + result[i] += int(index) } - if r.global { - r.putStr("lastIndex", intToValue(int64(endIndex)), true) + if r.global || r.sticky { + r.putStr("lastIndex", intToValue(int64(result[1])), true) } return } @@ -352,6 +350,7 @@ func (r *regexpObject) clone() *Object { r1.global = r.global r1.ignoreCase = r.ignoreCase r1.multiline = r.multiline + r1.sticky = r.sticky return r1.val } diff --git a/runtime.go b/runtime.go index 82e56d5d..77b6eca1 100644 --- a/runtime.go +++ b/runtime.go @@ -27,6 +27,14 @@ var ( typeTime = reflect.TypeOf(time.Time{}) ) +type iterationKind int + +const ( + iterationKindKey iterationKind = iota + iterationKindValue + iterationKindKeyValue +) + type global struct { Object *Object Array *Object @@ -36,6 +44,7 @@ type global struct { Boolean *Object RegExp *Object Date *Object + Symbol *Object ArrayBuffer *Object @@ -57,9 +66,14 @@ type global struct { FunctionPrototype *Object RegExpPrototype *Object DatePrototype *Object + SymbolPrototype *Object + ArrayIterator *Object ArrayBufferPrototype *Object + IteratorPrototype *Object + ArrayIteratorPrototype *Object + ErrorPrototype *Object TypeErrorPrototype *Object SyntaxErrorPrototype *Object @@ -74,6 +88,8 @@ type global struct { thrower *Object throwerProperty Value + + regexpProtoExec Value } type Flag int @@ -107,6 +123,8 @@ type Runtime struct { now Now _collator *collate.Collator + symbolRegistry map[string]*valueSymbol + typeInfoCache map[reflect.Type]*reflectTypeInfo fieldNameMapper FieldNameMapper @@ -237,6 +255,13 @@ func (r *Runtime) addToGlobal(name string, value Value) { r.globalObject.self._putProp(name, value, true, false, true) } +func (r *Runtime) createIterProto(val *Object) objectImpl { + o := newBaseObjectObj(val, r.global.ObjectPrototype, classObject) + + o.put(symIterator, valueProp(r.newNativeFunc(r.returnThis, nil, "[Symbol.iterator]", nil, 0), true, false, true), true) + return o +} + func (r *Runtime) init() { r.rand = rand.Float64 r.now = time.Now @@ -249,6 +274,8 @@ func (r *Runtime) init() { r.vm.init() r.global.FunctionPrototype = r.newNativeFunc(nil, nil, "Empty", nil, 0) + r.global.IteratorPrototype = r.newLazyObject(r.createIterProto) + r.initObject() r.initFunction() r.initArray() @@ -269,6 +296,7 @@ func (r *Runtime) init() { r.initJSON() //r.initTypedArrays() + r.initSymbol() r.global.thrower = r.newNativeFunc(r.builtin_thrower, nil, "thrower", nil, 0) r.global.throwerProperty = &valueProperty{ @@ -297,56 +325,20 @@ func (r *Runtime) newSyntaxError(msg string, offset int) Value { return r.builtin_new((r.global.SyntaxError), []Value{newStringValue(msg)}) } -func (r *Runtime) newArray(prototype *Object) (a *arrayObject) { - v := &Object{runtime: r} - - a = &arrayObject{} - a.class = classArray - a.val = v - a.extensible = true - v.self = a - a.prototype = prototype - a.init() - return -} - -func (r *Runtime) newArrayObject() *arrayObject { - return r.newArray(r.global.ArrayPrototype) -} - -func (r *Runtime) newArrayValues(values []Value) *Object { - v := &Object{runtime: r} - - a := &arrayObject{} - a.class = classArray - a.val = v - a.extensible = true - v.self = a - a.prototype = r.global.ArrayPrototype - a.init() - a.values = values - a.length = int64(len(values)) - a.objCount = a.length - return v -} - -func (r *Runtime) newArrayLength(l int64) *Object { - a := r.newArrayValues(nil) - a.self.putStr("length", intToValue(l), true) - return a -} - -func (r *Runtime) newBaseObject(proto *Object, class string) (o *baseObject) { - v := &Object{runtime: r} - - o = &baseObject{} +func newBaseObjectObj(obj, proto *Object, class string) *baseObject { + o := &baseObject{} o.class = class - o.val = v + o.val = obj o.extensible = true - v.self = o + obj.self = o o.prototype = proto o.init() - return + return o +} + +func (r *Runtime) newBaseObject(proto *Object, class string) (o *baseObject) { + v := &Object{runtime: r} + return newBaseObjectObj(v, proto, class) } func (r *Runtime) NewObject() (v *Object) { @@ -603,37 +595,44 @@ func (r *Runtime) builtin_Error(args []Value, proto *Object) *Object { return obj.val } -func (r *Runtime) builtin_new(construct *Object, args []Value) *Object { +func getConstructor(construct *Object) func(args []Value) *Object { repeat: switch f := construct.self.(type) { case *nativeFuncObject: if f.construct != nil { - return f.construct(args) + return f.construct } else { - panic("Not a constructor") + panic(construct.runtime.NewTypeError("Not a constructor")) } case *boundFuncObject: if f.construct != nil { - return f.construct(args) + return f.construct } else { - panic("Not a constructor") + panic(construct.runtime.NewTypeError("Not a constructor")) } case *funcObject: - // TODO: implement - panic("Not implemented") + return f.construct case *lazyObject: construct.self = f.create(construct) goto repeat - default: + } + + return nil +} + +func (r *Runtime) builtin_new(construct *Object, args []Value) *Object { + f := getConstructor(construct) + if f == nil { panic("Not a constructor") } + return f(args) } func (r *Runtime) throw(e Value) { panic(e) } -func (r *Runtime) builtin_thrower(call FunctionCall) Value { +func (r *Runtime) builtin_thrower(FunctionCall) Value { r.typeErrorResult(true, "'caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for calls to them") return nil } @@ -1572,3 +1571,49 @@ func tryFunc(f func()) (err error) { return nil } + +func (r *Runtime) toObject(v Value, args ...interface{}) *Object { + if obj, ok := v.(*Object); ok { + return obj + } + if len(args) > 0 { + panic(r.NewTypeError(args...)) + } else { + var s string + if v == nil { + s = "undefined" + } else { + s = v.String() + } + panic(r.NewTypeError("Value is not an object: %s", s)) + } +} + +func (r *Runtime) speciesConstructor(o, defaultConstructor *Object) func(args []Value) *Object { + c := o.self.getStr("constructor") + if c != nil && c != _undefined { + c = r.toObject(c).self.get(symSpecies) + } + if c == nil || c == _undefined { + c = defaultConstructor + } + return getConstructor(r.toObject(c)) +} + +func (r *Runtime) returnThis(call FunctionCall) Value { + return call.This +} + +func (r *Runtime) createIterResultObject(value Value, done bool) Value { + o := r.NewObject() + o.self.putStr("value", value, false) + o.self.putStr("done", r.toBoolean(done), false) + return o +} + +func nilSafe(v Value) Value { + if v != nil { + return v + } + return _undefined +} diff --git a/runtime_test.go b/runtime_test.go index ff9403ef..df07d50c 100644 --- a/runtime_test.go +++ b/runtime_test.go @@ -1327,6 +1327,27 @@ func TestProtoGetter(t *testing.T) { testScript1(SCRIPT, valueTrue, t) } +func TestSymbol1(t *testing.T) { + const SCRIPT = ` + Symbol.toPrimitive[Symbol.toPrimitive]() === Symbol.toPrimitive; + ` + + testScript1(SCRIPT, valueTrue, t) +} + +func TestFreezeSymbol(t *testing.T) { + const SCRIPT = ` + var s = Symbol(1); + var o = {}; + o[s] = 42; + Object.freeze(o); + o[s] = 43; + o[s] === 42 && Object.isFrozen(o); + ` + + testScript1(SCRIPT, valueTrue, t) +} + /* func TestArrayConcatSparse(t *testing.T) { function foo(a,b,c) diff --git a/string.go b/string.go index b9fa1e2d..35175099 100644 --- a/string.go +++ b/string.go @@ -20,6 +20,7 @@ var ( stringFunction valueString = asciiString("function") stringBoolean valueString = asciiString("boolean") stringString valueString = asciiString("string") + stringSymbol valueString = asciiString("symbol") stringNumber valueString = asciiString("number") stringNaN valueString = asciiString("NaN") stringInfinity = asciiString("Infinity") @@ -120,7 +121,7 @@ func (s *stringObject) getProp(n Value) Value { return s.baseObject.getProp(n) } -func (s *stringObject) getOwnProp(name string) Value { +func (s *stringObject) getOwnPropStr(name string) Value { if i := strToIdx(name); i >= 0 && i < s.length { val := s.getIdx(i) return &valueProperty{ @@ -129,7 +130,7 @@ func (s *stringObject) getOwnProp(name string) Value { } } - return s.baseObject.getOwnProp(name) + return s.baseObject.getOwnPropStr(name) } func (s *stringObject) getIdx(idx int64) Value { diff --git a/tc39_test.go b/tc39_test.go index c92beaba..1dcacd25 100644 --- a/tc39_test.go +++ b/tc39_test.go @@ -2,6 +2,7 @@ package goja import ( "errors" + "fmt" "gopkg.in/yaml.v2" "io/ioutil" "os" @@ -29,33 +30,53 @@ var ( "test/built-ins/Date/prototype/toISOString/15.9.5.43-0-8.js": true, // timezone "test/built-ins/Date/prototype/toISOString/15.9.5.43-0-9.js": true, // timezone "test/built-ins/Date/prototype/toISOString/15.9.5.43-0-10.js": true, // timezone + "test/annexB/built-ins/escape/escape-above-astral.js": true, // \u{xxxxx} + + "test/built-ins/Symbol/unscopables/cross-realm.js": true, + "test/built-ins/Symbol/toStringTag/cross-realm.js": true, + "test/built-ins/Symbol/toPrimitive/cross-realm.js": true, + "test/built-ins/Symbol/split/cross-realm.js": true, + "test/built-ins/Symbol/species/cross-realm.js": true, + "test/built-ins/Symbol/search/cross-realm.js": true, + "test/built-ins/Symbol/replace/cross-realm.js": true, + "test/built-ins/Symbol/match/cross-realm.js": true, + "test/built-ins/Symbol/keyFor/cross-realm.js": true, + "test/built-ins/Symbol/iterator/cross-realm.js": true, + "test/built-ins/Symbol/isConcatSpreadable/cross-realm.js": true, + "test/built-ins/Symbol/hasInstance/cross-realm.js": true, + "test/built-ins/Symbol/for/cross-realm.js": true, + "test/built-ins/Set/symbol-as-entry.js": true, + "test/built-ins/Map/symbol-as-entry-key.js": true, + "test/language/statements/class/subclass/builtin-objects/Symbol/symbol-valid-as-extends-value.js": true, + "test/language/statements/class/subclass/builtin-objects/Symbol/new-symbol-with-super-throws.js": true, + + // Proxy + "test/built-ins/Object/prototype/toString/proxy-revoked.js": true, + "test/built-ins/Object/prototype/toString/proxy-function.js": true, + "test/built-ins/Object/prototype/toString/proxy-array.js": true, + "test/built-ins/JSON/stringify/value-proxy.js": true, } - es6WhiteList = map[string]bool{ - "test/annexB/built-ins/escape/empty-string.js": true, - "test/annexB/built-ins/escape/escape-above.js": true, - "test/annexB/built-ins/escape/escape-below.js": true, - "test/annexB/built-ins/escape/length.js": true, - "test/annexB/built-ins/escape/name.js": true, - "test/annexB/built-ins/escape/to-string-err.js": true, - "test/annexB/built-ins/escape/to-string-observe.js": true, - "test/annexB/built-ins/escape/unmodified.js": true, - - "test/annexB/built-ins/unescape/empty-string.js": true, - "test/annexB/built-ins/unescape/four.js": true, - "test/annexB/built-ins/unescape/four-ignore-bad-u.js": true, - "test/annexB/built-ins/unescape/four-ignore-end-str.js": true, - "test/annexB/built-ins/unescape/four-ignore-non-hex.js": true, - "test/annexB/built-ins/unescape/length.js": true, - "test/annexB/built-ins/unescape/name.js": true, - "test/annexB/built-ins/unescape/to-string-err.js": true, - "test/annexB/built-ins/unescape/to-string-observe.js": true, - "test/annexB/built-ins/unescape/two.js": true, - "test/annexB/built-ins/unescape/two-ignore-end-str.js": true, - "test/annexB/built-ins/unescape/two-ignore-non-hex.js": true, + es6WhiteList = map[string]bool{} + + es6IdWhiteList = []string{ + "12.9.3", + "12.9.4", + "19.1.2.8", + "19.1.2.5", + "19.1.3.6", + "19.4", + "21.1.3.14", + "21.1.3.15", + "21.1.3.17", + //"21.2.5.6", + "22.1.2.5", + //"22.1.3.1", + "22.1.3.29", + "25.1.2", + "B.2.1", + "B.2.2", } - - es6IdWhiteList = []string{} ) type tc39Test struct { @@ -131,6 +152,11 @@ func parseTC39File(name string) (*tc39Meta, string, error) { } func (ctx *tc39TestCtx) runTC39Test(name, src string, meta *tc39Meta, t testing.TB) { + defer func() { + if x := recover(); x != nil { + panic(fmt.Sprintf("panic while running %s: %v", name, x)) + } + }() vm := New() err, early := ctx.runTC39Script(name, src, meta.Includes, vm) diff --git a/value.go b/value.go index 39225dbd..f5e36350 100644 --- a/value.go +++ b/value.go @@ -1,6 +1,7 @@ package goja import ( + "fmt" "math" "reflect" "regexp" @@ -53,6 +54,8 @@ type Value interface { baseObject(r *Runtime) *Object } +type typeError string + type valueInt int64 type valueFloat float64 type valueBool bool @@ -60,6 +63,9 @@ type valueNull struct{} type valueUndefined struct { valueNull } +type valueSymbol struct { + desc string +} type valueUnresolved struct { r *Runtime @@ -498,7 +504,7 @@ func (f valueFloat) ToString() valueString { return asciiString(f.String()) } -var matchLeading0Exponent = regexp.MustCompile(`([eE][\+\-])0+([1-9])`) // 1e-07 => 1e-7 +var matchLeading0Exponent = regexp.MustCompile(`([eE][+\-])0+([1-9])`) // 1e-07 => 1e-7 func (f valueFloat) String() string { value := float64(f) @@ -854,6 +860,77 @@ func (o valueUnresolved) ExportType() reflect.Type { return nil } +func (s *valueSymbol) ToInteger() int64 { + panic(typeError("Cannot convert a Symbol value to a number")) +} + +func (s *valueSymbol) ToString() valueString { + panic(typeError("Cannot convert a Symbol value to a string")) +} + +func (s *valueSymbol) String() string { + return s.descString() +} + +func (s *valueSymbol) ToFloat() float64 { + panic(typeError("Cannot convert a Symbol value to a number")) +} + +func (s *valueSymbol) ToNumber() Value { + panic(typeError("Cannot convert a Symbol value to a number")) +} + +func (s *valueSymbol) ToBoolean() bool { + return true +} + +func (s *valueSymbol) ToObject(r *Runtime) *Object { + return s.baseObject(r) +} + +func (s *valueSymbol) SameAs(other Value) bool { + if s1, ok := other.(*valueSymbol); ok { + return s == s1 + } + return false +} + +func (s *valueSymbol) Equals(o Value) bool { + return s.SameAs(o) +} + +func (s *valueSymbol) StrictEquals(o Value) bool { + return s.SameAs(o) +} + +func (s *valueSymbol) Export() interface{} { + return s.String() +} + +func (s *valueSymbol) ExportType() reflect.Type { + return reflectTypeString +} + +func (s *valueSymbol) assertInt() (int64, bool) { + return 0, false +} + +func (s *valueSymbol) assertString() (valueString, bool) { + return nil, false +} + +func (s *valueSymbol) assertFloat() (float64, bool) { + return 0, false +} + +func (s *valueSymbol) baseObject(r *Runtime) *Object { + return r.newPrimitiveObject(s, r.global.SymbolPrototype, "Symbol") +} + +func (s *valueSymbol) descString() string { + return fmt.Sprintf("Symbol(%s)", s.desc) +} + func init() { for i := 0; i < 256; i++ { intCache[i] = valueInt(i - 128) diff --git a/vm.go b/vm.go index 95ed61f6..e7693727 100644 --- a/vm.go +++ b/vm.go @@ -366,6 +366,10 @@ func (vm *vm) try(f func()) (ex *Exception) { panic(x1) case *Exception: ex = x1 + case typeError: + ex = &Exception{ + val: vm.r.NewTypeError(string(x1)), + } default: /* if vm.prg != nil { @@ -448,19 +452,7 @@ func (vm *vm) popCtx() { vm.callStack = vm.callStack[:l] } -func (r *Runtime) toObject(v Value, args ...interface{}) *Object { - //r.checkResolveable(v) - if obj, ok := v.(*Object); ok { - return obj - } - if len(args) > 0 { - panic(r.NewTypeError(args...)) - } else { - panic(r.NewTypeError("Value is not an object: %s", v.String())) - } -} - -func (r *Runtime) toCallee(v Value) *Object { +func (vm *vm) toCallee(v Value) *Object { if obj, ok := v.(*Object); ok { return obj } @@ -469,11 +461,9 @@ func (r *Runtime) toCallee(v Value) *Object { unresolved.throw() panic("Unreachable") case memberUnresolved: - r.typeErrorResult(true, "Object has no member '%s'", unresolved.ref) - panic("Unreachable") + panic(vm.r.NewTypeError("Object has no member '%s'", unresolved.ref)) } - r.typeErrorResult(true, "Value is not an object: %s", v.ToString()) - panic("Unreachable") + panic(vm.r.NewTypeError("Value is not an object: %s", v.ToString())) } type _newStash struct{} @@ -1263,11 +1253,11 @@ type newRegexp struct { pattern regexpPattern src valueString - global, ignoreCase, multiline bool + global, ignoreCase, multiline, sticky bool } func (n *newRegexp) exec(vm *vm) { - vm.push(vm.r.newRegExpp(n.pattern, n.src, n.global, n.ignoreCase, n.multiline, vm.r.global.RegExpPrototype)) + vm.push(vm.r.newRegExpp(n.pattern, n.src, n.global, n.ignoreCase, n.multiline, n.sticky, vm.r.global.RegExpPrototype)) vm.pc++ } @@ -1725,7 +1715,7 @@ func (numargs call) exec(vm *vm) { // arg n := int(numargs) v := vm.stack[vm.sp-n-1] // callee - obj := vm.r.toCallee(v) + obj := vm.toCallee(v) repeat: switch f := obj.self.(type) { case *funcObject: @@ -2136,7 +2126,7 @@ func (_op_instanceof) exec(vm *vm) { left := vm.stack[vm.sp-2] right := vm.r.toObject(vm.stack[vm.sp-1]) - if right.self.hasInstance(left) { + if instanceOfOperator(left, right) { vm.stack[vm.sp-2] = valueTrue } else { vm.stack[vm.sp-2] = valueFalse @@ -2238,37 +2228,15 @@ func (_throw) exec(vm *vm) { type _new uint32 func (n _new) exec(vm *vm) { - obj := vm.r.toObject(vm.stack[vm.sp-1-int(n)]) -repeat: - switch f := obj.self.(type) { - case *funcObject: - args := make([]Value, n) - copy(args, vm.stack[vm.sp-int(n):]) - vm.sp -= int(n) - vm.stack[vm.sp-1] = f.construct(args) - case *nativeFuncObject: - vm._nativeNew(f, int(n)) - case *boundFuncObject: - vm._nativeNew(&f.nativeFuncObject, int(n)) - case *lazyObject: - obj.self = f.create(obj) - goto repeat - default: - vm.r.typeErrorResult(true, "Not a constructor") - } - - vm.pc++ -} - -func (vm *vm) _nativeNew(f *nativeFuncObject, n int) { - if f.construct != nil { - args := make([]Value, n) - copy(args, vm.stack[vm.sp-n:]) - vm.sp -= n - vm.stack[vm.sp-1] = f.construct(args) + sp := vm.sp - int(n) + obj := vm.r.toObject(vm.stack[sp-1]) + if ctor := getConstructor(obj); ctor != nil { + vm.stack[sp-1] = ctor(vm.stack[sp:vm.sp]) + vm.sp = sp } else { - vm.r.typeErrorResult(true, "Not a constructor") + panic(vm.r.NewTypeError("Not a constructor")) } + vm.pc++ } type _typeof struct{} @@ -2299,6 +2267,8 @@ func (_typeof) exec(vm *vm) { r = stringString case valueInt, valueFloat: r = stringNumber + case *valueSymbol: + r = stringSymbol default: panic(fmt.Errorf("Unknown type: %T", v)) } From 0a7d410a91351cb26db0256f2bbb22aa16a8aac9 Mon Sep 17 00:00:00 2001 From: Dmitry Panov Date: Tue, 3 Mar 2020 14:38:03 +0000 Subject: [PATCH 02/46] Implemented Map, Set, WeakMap, WeakSet (#130) --- builtin_array.go | 1 + builtin_date.go | 14 +-- builtin_map.go | 267 ++++++++++++++++++++++++++++++++++++++++ builtin_set.go | 242 ++++++++++++++++++++++++++++++++++++ builtin_weakmap.go | 202 ++++++++++++++++++++++++++++++ builtin_weakmap_test.go | 33 +++++ builtin_weakset.go | 177 ++++++++++++++++++++++++++ builtin_weakset_test.go | 88 +++++++++++++ map.go | 163 ++++++++++++++++++++++++ map_test.go | 196 +++++++++++++++++++++++++++++ object.go | 76 ++++++++++++ runtime.go | 80 +++++++++++- string_ascii.go | 7 ++ string_unicode.go | 8 ++ tc39_test.go | 58 ++++++--- value.go | 51 ++++++++ 16 files changed, 1630 insertions(+), 33 deletions(-) create mode 100644 builtin_map.go create mode 100644 builtin_set.go create mode 100644 builtin_weakmap.go create mode 100644 builtin_weakmap_test.go create mode 100644 builtin_weakset.go create mode 100644 builtin_weakset_test.go create mode 100644 map.go create mode 100644 map_test.go diff --git a/builtin_array.go b/builtin_array.go index 12fd0c3f..e46e90c1 100644 --- a/builtin_array.go +++ b/builtin_array.go @@ -881,6 +881,7 @@ func (r *Runtime) createArrayProto(val *Object) objectImpl { valuesFunc := r.newNativeFunc(r.arrayproto_values, nil, "values", nil, 0) o._putProp("values", valuesFunc, true, false, true) o.put(symIterator, valueProp(valuesFunc, false, false, true), true) + r.global.arrayValues = valuesFunc return o } diff --git a/builtin_date.go b/builtin_date.go index f4bbdbc6..a0ae93e8 100644 --- a/builtin_date.go +++ b/builtin_date.go @@ -111,7 +111,7 @@ func (r *Runtime) builtin_newDate(args []Value) *Object { return r.newDateTime(args, time.Local) } -func (r *Runtime) builtin_date(call FunctionCall) Value { +func (r *Runtime) builtin_date(FunctionCall) Value { return asciiString(dateFormat(r.now())) } @@ -131,7 +131,7 @@ func (r *Runtime) date_UTC(call FunctionCall) Value { return intToValue(timeToMsec(t)) } -func (r *Runtime) date_now(call FunctionCall) Value { +func (r *Runtime) date_now(FunctionCall) Value { return intToValue(timeToMsec(r.now())) } @@ -910,16 +910,6 @@ func (r *Runtime) createDate(val *Object) objectImpl { return o } -func (r *Runtime) newLazyObject(create func(*Object) objectImpl) *Object { - val := &Object{runtime: r} - o := &lazyObject{ - val: val, - create: create, - } - val.self = o - return val -} - func (r *Runtime) initDate() { //r.global.DatePrototype = r.newObject() //o := r.global.DatePrototype.self diff --git a/builtin_map.go b/builtin_map.go new file mode 100644 index 00000000..5196595e --- /dev/null +++ b/builtin_map.go @@ -0,0 +1,267 @@ +package goja + +type mapObject struct { + baseObject + m *orderedMap +} + +type mapIterObject struct { + baseObject + iter *orderedMapIter + kind iterationKind +} + +func (o *mapIterObject) next() Value { + if o.iter == nil { + return o.val.runtime.createIterResultObject(_undefined, true) + } + + entry := o.iter.next() + if entry == nil { + o.iter = nil + return o.val.runtime.createIterResultObject(_undefined, true) + } + + var result Value + switch o.kind { + case iterationKindKey: + result = entry.key + case iterationKindValue: + result = entry.value + default: + result = o.val.runtime.newArrayValues([]Value{entry.key, entry.value}) + } + + return o.val.runtime.createIterResultObject(result, false) +} + +func (mo *mapObject) init() { + mo.baseObject.init() + mo.m = newOrderedMap() +} + +func (r *Runtime) mapProto_clear(call FunctionCall) Value { + thisObj := r.toObject(call.This) + mo, ok := thisObj.self.(*mapObject) + if !ok { + panic(r.NewTypeError("Method Map.prototype.clear called on incompatible receiver %s", thisObj.String())) + } + + mo.m.clear() + + return _undefined +} + +func (r *Runtime) mapProto_delete(call FunctionCall) Value { + thisObj := r.toObject(call.This) + mo, ok := thisObj.self.(*mapObject) + if !ok { + panic(r.NewTypeError("Method Map.prototype.delete called on incompatible receiver %s", thisObj.String())) + } + + return r.toBoolean(mo.m.remove(call.Argument(0))) +} + +func (r *Runtime) mapProto_get(call FunctionCall) Value { + thisObj := r.toObject(call.This) + mo, ok := thisObj.self.(*mapObject) + if !ok { + panic(r.NewTypeError("Method Map.prototype.get called on incompatible receiver %s", thisObj.String())) + } + + return nilSafe(mo.m.get(call.Argument(0))) +} + +func (r *Runtime) mapProto_has(call FunctionCall) Value { + thisObj := r.toObject(call.This) + mo, ok := thisObj.self.(*mapObject) + if !ok { + panic(r.NewTypeError("Method Map.prototype.has called on incompatible receiver %s", thisObj.String())) + } + if mo.m.has(call.Argument(0)) { + return valueTrue + } + return valueFalse +} + +func (r *Runtime) mapProto_set(call FunctionCall) Value { + thisObj := r.toObject(call.This) + mo, ok := thisObj.self.(*mapObject) + if !ok { + panic(r.NewTypeError("Method Map.prototype.set called on incompatible receiver %s", thisObj.String())) + } + mo.m.set(call.Argument(0), call.Argument(1)) + return call.This +} + +func (r *Runtime) mapProto_entries(call FunctionCall) Value { + return r.createMapIterator(call.This, iterationKindKeyValue) +} + +func (r *Runtime) mapProto_forEach(call FunctionCall) Value { + thisObj := r.toObject(call.This) + mo, ok := thisObj.self.(*mapObject) + if !ok { + panic(r.NewTypeError("Method Map.prototype.forEach called on incompatible receiver %s", thisObj.String())) + } + callbackFn, ok := r.toObject(call.Argument(0)).self.assertCallable() + if !ok { + panic(r.NewTypeError("object is not a function %s")) + } + t := call.Argument(1) + iter := mo.m.newIter() + for { + entry := iter.next() + if entry == nil { + break + } + callbackFn(FunctionCall{This: t, Arguments: []Value{entry.value, entry.key, thisObj}}) + } + + return _undefined +} + +func (r *Runtime) mapProto_keys(call FunctionCall) Value { + return r.createMapIterator(call.This, iterationKindKey) +} + +func (r *Runtime) mapProto_values(call FunctionCall) Value { + return r.createMapIterator(call.This, iterationKindValue) +} + +func (r *Runtime) mapProto_getSize(call FunctionCall) Value { + thisObj := r.toObject(call.This) + mo, ok := thisObj.self.(*mapObject) + if !ok { + panic(r.NewTypeError("Method get Map.prototype.size called on incompatible receiver %s", thisObj.String())) + } + return intToValue(int64(mo.m.size)) +} + +func (r *Runtime) builtin_newMap(args []Value) *Object { + o := &Object{runtime: r} + + mo := &mapObject{} + mo.class = classMap + mo.val = o + mo.extensible = true + o.self = mo + mo.prototype = r.global.MapPrototype + mo.init() + if len(args) > 0 { + if arg := args[0]; arg != nil && arg != _undefined && arg != _null { + adder := mo.getStr("set") + iter := r.getIterator(arg.ToObject(r), nil) + i0 := intToValue(0) + i1 := intToValue(1) + if adder == r.global.mapAdder { + r.iterate(iter, func(item Value) { + itemObj := r.toObject(item) + k := itemObj.self.get(i0) + v := itemObj.self.get(i1) + mo.m.set(k, v) + }) + } else { + adderFn := toMethod(adder) + if adderFn == nil { + panic(r.NewTypeError("Map.set in missing")) + } + r.iterate(iter, func(item Value) { + itemObj := r.toObject(item) + k := itemObj.self.get(i0) + v := itemObj.self.get(i1) + adderFn(FunctionCall{This: o, Arguments: []Value{k, v}}) + }) + } + } + } + return o +} + +func (r *Runtime) createMapIterator(mapValue Value, kind iterationKind) Value { + obj := r.toObject(mapValue) + mapObj, ok := obj.self.(*mapObject) + if !ok { + panic(r.NewTypeError("Object is not a Map")) + } + + o := &Object{runtime: r} + + mi := &mapIterObject{ + iter: mapObj.m.newIter(), + kind: kind, + } + mi.class = classMapIterator + mi.val = o + mi.extensible = true + o.self = mi + mi.prototype = r.global.MapIteratorPrototype + mi.init() + + return o +} + +func (r *Runtime) mapIterProto_next(call FunctionCall) Value { + thisObj := r.toObject(call.This) + if iter, ok := thisObj.self.(*mapIterObject); ok { + return iter.next() + } + panic(r.NewTypeError("Method Map Iterator.prototype.next called on incompatible receiver %s", thisObj.String())) +} + +func (r *Runtime) createMapProto(val *Object) objectImpl { + o := newBaseObjectObj(val, r.global.ObjectPrototype, classObject) + + o._putProp("constructor", r.global.Map, true, false, true) + o._putProp("clear", r.newNativeFunc(r.mapProto_clear, nil, "clear", nil, 0), true, false, true) + r.global.mapAdder = r.newNativeFunc(r.mapProto_set, nil, "set", nil, 2) + o._putProp("set", r.global.mapAdder, true, false, true) + o._putProp("delete", r.newNativeFunc(r.mapProto_delete, nil, "delete", nil, 1), true, false, true) + o._putProp("forEach", r.newNativeFunc(r.mapProto_forEach, nil, "forEach", nil, 1), true, false, true) + o._putProp("has", r.newNativeFunc(r.mapProto_has, nil, "has", nil, 1), true, false, true) + o._putProp("get", r.newNativeFunc(r.mapProto_get, nil, "get", nil, 1), true, false, true) + o.putStr("size", &valueProperty{ + getterFunc: r.newNativeFunc(r.mapProto_getSize, nil, "get size", nil, 0), + accessor: true, + writable: true, + configurable: true, + }, true) + o._putProp("keys", r.newNativeFunc(r.mapProto_keys, nil, "keys", nil, 0), true, false, true) + o._putProp("values", r.newNativeFunc(r.mapProto_values, nil, "values", nil, 0), true, false, true) + + entriesFunc := r.newNativeFunc(r.mapProto_entries, nil, "entries", nil, 0) + o._putProp("entries", entriesFunc, true, false, true) + o.put(symIterator, valueProp(entriesFunc, true, false, true), true) + o.put(symToStringTag, valueProp(asciiString(classMap), false, false, true), true) + + return o +} + +func (r *Runtime) createMap(val *Object) objectImpl { + o := r.newNativeFuncObj(val, r.constructorThrower("Map"), r.builtin_newMap, "Map", r.global.MapPrototype, 0) + o.putSym(symSpecies, &valueProperty{ + getterFunc: r.newNativeFunc(r.returnThis, nil, "get [Symbol.species]", nil, 0), + accessor: true, + configurable: true, + }, true) + + return o +} + +func (r *Runtime) createMapIterProto(val *Object) objectImpl { + o := newBaseObjectObj(val, r.global.IteratorPrototype, classObject) + + o._putProp("next", r.newNativeFunc(r.mapIterProto_next, nil, "next", nil, 0), true, false, true) + o.put(symToStringTag, valueProp(asciiString(classMapIterator), false, false, true), true) + + return o +} + +func (r *Runtime) initMap() { + r.global.MapIteratorPrototype = r.newLazyObject(r.createMapIterProto) + + r.global.MapPrototype = r.newLazyObject(r.createMapProto) + r.global.Map = r.newLazyObject(r.createMap) + + r.addToGlobal("Map", r.global.Map) +} diff --git a/builtin_set.go b/builtin_set.go new file mode 100644 index 00000000..460a1916 --- /dev/null +++ b/builtin_set.go @@ -0,0 +1,242 @@ +package goja + +type setObject struct { + baseObject + m *orderedMap +} + +type setIterObject struct { + baseObject + iter *orderedMapIter + kind iterationKind +} + +func (o *setIterObject) next() Value { + if o.iter == nil { + return o.val.runtime.createIterResultObject(_undefined, true) + } + + entry := o.iter.next() + if entry == nil { + o.iter = nil + return o.val.runtime.createIterResultObject(_undefined, true) + } + + var result Value + switch o.kind { + case iterationKindValue: + result = entry.key + default: + result = o.val.runtime.newArrayValues([]Value{entry.key, entry.key}) + } + + return o.val.runtime.createIterResultObject(result, false) +} + +func (so *setObject) init() { + so.baseObject.init() + so.m = newOrderedMap() +} + +func (r *Runtime) setProto_add(call FunctionCall) Value { + thisObj := r.toObject(call.This) + so, ok := thisObj.self.(*setObject) + if !ok { + panic(r.NewTypeError("Method Set.prototype.add called on incompatible receiver %s", thisObj.String())) + } + + so.m.set(call.Argument(0), nil) + return call.This +} + +func (r *Runtime) setProto_clear(call FunctionCall) Value { + thisObj := r.toObject(call.This) + so, ok := thisObj.self.(*setObject) + if !ok { + panic(r.NewTypeError("Method Set.prototype.clear called on incompatible receiver %s", thisObj.String())) + } + + so.m.clear() + return _undefined +} + +func (r *Runtime) setProto_delete(call FunctionCall) Value { + thisObj := r.toObject(call.This) + so, ok := thisObj.self.(*setObject) + if !ok { + panic(r.NewTypeError("Method Set.prototype.delete called on incompatible receiver %s", thisObj.String())) + } + + return r.toBoolean(so.m.remove(call.Argument(0))) +} + +func (r *Runtime) setProto_entries(call FunctionCall) Value { + return r.createSetIterator(call.This, iterationKindKeyValue) +} + +func (r *Runtime) setProto_forEach(call FunctionCall) Value { + thisObj := r.toObject(call.This) + so, ok := thisObj.self.(*setObject) + if !ok { + panic(r.NewTypeError("Method Set.prototype.forEach called on incompatible receiver %s", thisObj.String())) + } + callbackFn, ok := r.toObject(call.Argument(0)).self.assertCallable() + if !ok { + panic(r.NewTypeError("object is not a function %s")) + } + t := call.Argument(1) + iter := so.m.newIter() + for { + entry := iter.next() + if entry == nil { + break + } + callbackFn(FunctionCall{This: t, Arguments: []Value{entry.key, entry.key, thisObj}}) + } + + return _undefined +} + +func (r *Runtime) setProto_has(call FunctionCall) Value { + thisObj := r.toObject(call.This) + so, ok := thisObj.self.(*setObject) + if !ok { + panic(r.NewTypeError("Method Set.prototype.has called on incompatible receiver %s", thisObj.String())) + } + + return r.toBoolean(so.m.has(call.Argument(0))) +} + +func (r *Runtime) setProto_getSize(call FunctionCall) Value { + thisObj := r.toObject(call.This) + so, ok := thisObj.self.(*setObject) + if !ok { + panic(r.NewTypeError("Method get Set.prototype.size called on incompatible receiver %s", thisObj.String())) + } + + return intToValue(int64(so.m.size)) +} + +func (r *Runtime) setProto_values(call FunctionCall) Value { + return r.createSetIterator(call.This, iterationKindValue) +} + +func (r *Runtime) builtin_newSet(args []Value) *Object { + o := &Object{runtime: r} + + so := &setObject{} + so.class = classSet + so.val = o + so.extensible = true + o.self = so + so.prototype = r.global.SetPrototype + so.init() + if len(args) > 0 { + if arg := args[0]; arg != nil && arg != _undefined && arg != _null { + adder := so.getStr("add") + iter := r.getIterator(arg.ToObject(r), nil) + if adder == r.global.setAdder { + r.iterate(iter, func(item Value) { + so.m.set(item, nil) + }) + } else { + adderFn := toMethod(adder) + if adderFn == nil { + panic(r.NewTypeError("Set.add in missing")) + } + r.iterate(iter, func(item Value) { + adderFn(FunctionCall{This: o, Arguments: []Value{item}}) + }) + } + } + } + return o +} + +func (r *Runtime) createSetIterator(setValue Value, kind iterationKind) Value { + obj := r.toObject(setValue) + setObj, ok := obj.self.(*setObject) + if !ok { + panic(r.NewTypeError("Object is not a Set")) + } + + o := &Object{runtime: r} + + si := &setIterObject{ + iter: setObj.m.newIter(), + kind: kind, + } + si.class = classSetIterator + si.val = o + si.extensible = true + o.self = si + si.prototype = r.global.SetIteratorPrototype + si.init() + + return o +} + +func (r *Runtime) setIterProto_next(call FunctionCall) Value { + thisObj := r.toObject(call.This) + if iter, ok := thisObj.self.(*setIterObject); ok { + return iter.next() + } + panic(r.NewTypeError("Method Set Iterator.prototype.next called on incompatible receiver %s", thisObj.String())) +} + +func (r *Runtime) createSetProto(val *Object) objectImpl { + o := newBaseObjectObj(val, r.global.ObjectPrototype, classObject) + + o._putProp("constructor", r.global.Set, true, false, true) + r.global.setAdder = r.newNativeFunc(r.setProto_add, nil, "add", nil, 1) + o._putProp("add", r.global.setAdder, true, false, true) + + o._putProp("clear", r.newNativeFunc(r.setProto_clear, nil, "clear", nil, 0), true, false, true) + o._putProp("delete", r.newNativeFunc(r.setProto_delete, nil, "delete", nil, 1), true, false, true) + o._putProp("forEach", r.newNativeFunc(r.setProto_forEach, nil, "forEach", nil, 1), true, false, true) + o._putProp("has", r.newNativeFunc(r.setProto_has, nil, "has", nil, 1), true, false, true) + o.putStr("size", &valueProperty{ + getterFunc: r.newNativeFunc(r.setProto_getSize, nil, "get size", nil, 0), + accessor: true, + writable: true, + configurable: true, + }, true) + + valuesFunc := r.newNativeFunc(r.setProto_values, nil, "values", nil, 0) + o._putProp("values", valuesFunc, true, false, true) + o._putProp("keys", valuesFunc, true, false, true) + o._putProp("entries", r.newNativeFunc(r.setProto_entries, nil, "entries", nil, 0), true, false, true) + o.put(symIterator, valueProp(valuesFunc, true, false, true), true) + o.put(symToStringTag, valueProp(asciiString(classSet), false, false, true), true) + + return o +} + +func (r *Runtime) createSet(val *Object) objectImpl { + o := r.newNativeFuncObj(val, r.constructorThrower("Set"), r.builtin_newSet, "Set", r.global.SetPrototype, 0) + o.putSym(symSpecies, &valueProperty{ + getterFunc: r.newNativeFunc(r.returnThis, nil, "get [Symbol.species]", nil, 0), + accessor: true, + configurable: true, + }, true) + + return o +} + +func (r *Runtime) createSetIterProto(val *Object) objectImpl { + o := newBaseObjectObj(val, r.global.IteratorPrototype, classObject) + + o._putProp("next", r.newNativeFunc(r.setIterProto_next, nil, "next", nil, 0), true, false, true) + o.put(symToStringTag, valueProp(asciiString(classSetIterator), false, false, true), true) + + return o +} + +func (r *Runtime) initSet() { + r.global.SetIteratorPrototype = r.newLazyObject(r.createSetIterProto) + + r.global.SetPrototype = r.newLazyObject(r.createSetProto) + r.global.Set = r.newLazyObject(r.createSet) + + r.addToGlobal("Set", r.global.Set) +} diff --git a/builtin_weakmap.go b/builtin_weakmap.go new file mode 100644 index 00000000..d2d941b3 --- /dev/null +++ b/builtin_weakmap.go @@ -0,0 +1,202 @@ +package goja + +import "sync" + +type weakMap struct { + // need to synchronise access to the data map because it may be accessed + // from the finalizer goroutine + sync.Mutex + data map[uintptr]Value +} + +type weakMapObject struct { + baseObject + m *weakMap +} + +func newWeakMap() *weakMap { + return &weakMap{ + data: make(map[uintptr]Value), + } +} + +func (wmo *weakMapObject) init() { + wmo.baseObject.init() + wmo.m = newWeakMap() +} + +func (wm *weakMap) removePtr(ptr uintptr) { + wm.Lock() + delete(wm.data, ptr) + wm.Unlock() +} + +func (wm *weakMap) set(key *Object, value Value) { + refs := key.getWeakCollRefs() + wm.Lock() + wm.data[refs.id()] = value + wm.Unlock() + refs.add(wm) +} + +func (wm *weakMap) get(key *Object) Value { + refs := key.weakColls + if refs == nil { + return nil + } + wm.Lock() + ret := wm.data[refs.id()] + wm.Unlock() + return ret +} + +func (wm *weakMap) remove(key *Object) bool { + refs := key.weakColls + if refs == nil { + return false + } + id := refs.id() + wm.Lock() + _, exists := wm.data[id] + if exists { + delete(wm.data, id) + } + wm.Unlock() + if exists { + refs.remove(wm) + } + return exists +} + +func (wm *weakMap) has(key *Object) bool { + refs := key.weakColls + if refs == nil { + return false + } + id := refs.id() + wm.Lock() + _, exists := wm.data[id] + wm.Unlock() + return exists +} + +func (r *Runtime) weakMapProto_delete(call FunctionCall) Value { + thisObj := r.toObject(call.This) + wmo, ok := thisObj.self.(*weakMapObject) + if !ok { + panic(r.NewTypeError("Method WeakMap.prototype.delete called on incompatible receiver %s", thisObj.String())) + } + key, ok := call.Argument(0).(*Object) + if ok && wmo.m.remove(key) { + return valueTrue + } + return valueFalse +} + +func (r *Runtime) weakMapProto_get(call FunctionCall) Value { + thisObj := r.toObject(call.This) + wmo, ok := thisObj.self.(*weakMapObject) + if !ok { + panic(r.NewTypeError("Method WeakMap.prototype.get called on incompatible receiver %s", thisObj.String())) + } + var res Value + if key, ok := call.Argument(0).(*Object); ok { + res = wmo.m.get(key) + } + if res == nil { + return _undefined + } + return res +} + +func (r *Runtime) weakMapProto_has(call FunctionCall) Value { + thisObj := r.toObject(call.This) + wmo, ok := thisObj.self.(*weakMapObject) + if !ok { + panic(r.NewTypeError("Method WeakMap.prototype.has called on incompatible receiver %s", thisObj.String())) + } + key, ok := call.Argument(0).(*Object) + if ok && wmo.m.has(key) { + return valueTrue + } + return valueFalse +} + +func (r *Runtime) weakMapProto_set(call FunctionCall) Value { + thisObj := r.toObject(call.This) + wmo, ok := thisObj.self.(*weakMapObject) + if !ok { + panic(r.NewTypeError("Method WeakMap.prototype.set called on incompatible receiver %s", thisObj.String())) + } + key := r.toObject(call.Argument(0)) + wmo.m.set(key, call.Argument(1)) + return call.This +} + +func (r *Runtime) builtin_newWeakMap(args []Value) *Object { + o := &Object{runtime: r} + + wmo := &weakMapObject{} + wmo.class = classWeakMap + wmo.val = o + wmo.extensible = true + o.self = wmo + wmo.prototype = r.global.WeakMapPrototype + wmo.init() + if len(args) > 0 { + if arg := args[0]; arg != nil && arg != _undefined && arg != _null { + adder := wmo.getStr("set") + iter := r.getIterator(arg.ToObject(r), nil) + i0 := intToValue(0) + i1 := intToValue(1) + if adder == r.global.weakMapAdder { + r.iterate(iter, func(item Value) { + itemObj := r.toObject(item) + k := itemObj.self.get(i0) + v := itemObj.self.get(i1) + wmo.m.set(r.toObject(k), v) + }) + } else { + adderFn := toMethod(adder) + if adderFn == nil { + panic(r.NewTypeError("WeakMap.set in missing")) + } + r.iterate(iter, func(item Value) { + itemObj := r.toObject(item) + k := itemObj.self.get(i0) + v := itemObj.self.get(i1) + adderFn(FunctionCall{This: o, Arguments: []Value{k, v}}) + }) + } + } + } + return o +} + +func (r *Runtime) createWeakMapProto(val *Object) objectImpl { + o := newBaseObjectObj(val, r.global.ObjectPrototype, classObject) + + o._putProp("constructor", r.global.WeakMap, true, false, true) + r.global.weakMapAdder = r.newNativeFunc(r.weakMapProto_set, nil, "set", nil, 2) + o._putProp("set", r.global.weakMapAdder, true, false, true) + o._putProp("delete", r.newNativeFunc(r.weakMapProto_delete, nil, "delete", nil, 1), true, false, true) + o._putProp("has", r.newNativeFunc(r.weakMapProto_has, nil, "has", nil, 1), true, false, true) + o._putProp("get", r.newNativeFunc(r.weakMapProto_get, nil, "get", nil, 1), true, false, true) + + o.put(symToStringTag, valueProp(asciiString(classWeakMap), false, false, true), true) + + return o +} + +func (r *Runtime) createWeakMap(val *Object) objectImpl { + o := r.newNativeFuncObj(val, r.constructorThrower("WeakMap"), r.builtin_newWeakMap, "WeakMap", r.global.WeakMapPrototype, 0) + + return o +} + +func (r *Runtime) initWeakMap() { + r.global.WeakMapPrototype = r.newLazyObject(r.createWeakMapProto) + r.global.WeakMap = r.newLazyObject(r.createWeakMap) + + r.addToGlobal("WeakMap", r.global.WeakMap) +} diff --git a/builtin_weakmap_test.go b/builtin_weakmap_test.go new file mode 100644 index 00000000..39eeae5e --- /dev/null +++ b/builtin_weakmap_test.go @@ -0,0 +1,33 @@ +package goja + +import ( + "runtime" + "testing" +) + +func TestWeakMapExpiry(t *testing.T) { + vm := New() + _, err := vm.RunString(` + var m = new WeakMap(); + var key = {}; + m.set(key, true); + if (!m.has(key)) { + throw new Error("has"); + } + if (m.get(key) !== true) { + throw new Error("value does not match"); + } + key = undefined; + `) + if err != nil { + t.Fatal(err) + } + runtime.GC() + wmo := vm.Get("m").ToObject(vm).self.(*weakMapObject) + wmo.m.Lock() + l := len(wmo.m.data) + wmo.m.Unlock() + if l > 0 { + t.Fatal("Object has not been removed") + } +} diff --git a/builtin_weakset.go b/builtin_weakset.go new file mode 100644 index 00000000..53ceabc8 --- /dev/null +++ b/builtin_weakset.go @@ -0,0 +1,177 @@ +package goja + +import "sync" + +type weakSet struct { + // need to synchronise access to the data map because it may be accessed + // from the finalizer goroutine + sync.Mutex + data map[uintptr]struct{} +} + +type weakSetObject struct { + baseObject + set *weakSet +} + +func newWeakSet() *weakSet { + return &weakSet{ + data: make(map[uintptr]struct{}), + } +} + +func (ws *weakSetObject) init() { + ws.baseObject.init() + ws.set = newWeakSet() +} + +func (ws *weakSet) removePtr(ptr uintptr) { + ws.Lock() + delete(ws.data, ptr) + ws.Unlock() +} + +func (ws *weakSet) add(o *Object) { + refs := o.getWeakCollRefs() + ws.Lock() + ws.data[refs.id()] = struct{}{} + ws.Unlock() + refs.add(ws) +} + +func (ws *weakSet) remove(o *Object) bool { + if o.weakColls == nil { + return false + } + id := o.weakColls.id() + ws.Lock() + _, exists := ws.data[id] + if exists { + delete(ws.data, id) + } + ws.Unlock() + if exists { + o.weakColls.remove(ws) + } + return exists +} + +func (ws *weakSet) has(o *Object) bool { + if o.weakColls == nil { + return false + } + ws.Lock() + _, exists := ws.data[o.weakColls.id()] + ws.Unlock() + return exists +} + +func (r *Runtime) weakSetProto_add(call FunctionCall) Value { + thisObj := r.toObject(call.This) + wso, ok := thisObj.self.(*weakSetObject) + if !ok { + panic(r.NewTypeError("Method WeakSet.prototype.add called on incompatible receiver %s", thisObj.String())) + } + wso.set.add(r.toObject(call.Argument(0))) + return call.This +} + +func (r *Runtime) weakSetProto_delete(call FunctionCall) Value { + thisObj := r.toObject(call.This) + wso, ok := thisObj.self.(*weakSetObject) + if !ok { + panic(r.NewTypeError("Method WeakSet.prototype.delete called on incompatible receiver %s", thisObj.String())) + } + obj, ok := call.Argument(0).(*Object) + if ok && wso.set.remove(obj) { + return valueTrue + } + return valueFalse +} + +func (r *Runtime) weakSetProto_has(call FunctionCall) Value { + thisObj := r.toObject(call.This) + wso, ok := thisObj.self.(*weakSetObject) + if !ok { + panic(r.NewTypeError("Method WeakSet.prototype.has called on incompatible receiver %s", thisObj.String())) + } + obj, ok := call.Argument(0).(*Object) + if ok && wso.set.has(obj) { + return valueTrue + } + return valueFalse +} + +func (r *Runtime) populateWeakSetGeneric(s *Object, adderValue Value, iterable Value) { + adder := toMethod(adderValue) + if adder == nil { + panic(r.NewTypeError("WeakSet.add is not set")) + } + iter := r.getIterator(iterable.ToObject(r), nil) + r.iterate(iter, func(val Value) { + adder(FunctionCall{This: s, Arguments: []Value{val}}) + }) +} + +func (r *Runtime) builtin_newWeakSet(args []Value) *Object { + o := &Object{runtime: r} + + wso := &weakSetObject{} + wso.class = classWeakSet + wso.val = o + wso.extensible = true + o.self = wso + wso.prototype = r.global.WeakSetPrototype + wso.init() + if len(args) > 0 { + if arg := args[0]; arg != nil && arg != _undefined && arg != _null { + adder := wso.getStr("add") + if adder == r.global.weakSetAdder { + if obj, ok := arg.(*Object); ok { + if arr, ok := obj.self.(*arrayObject); ok && + arr.propValueCount == 0 && + arr.length == int64(len(arr.values)) && + arr.getSym(symIterator) == r.global.arrayValues { + + for i, v := range arr.values { + if v == nil { + v = arr.get(intToValue(int64(i))) + } + wso.set.add(r.toObject(v)) + } + return o + } + } + } + r.populateWeakSetGeneric(o, adder, arg) + } + } + return o +} + +func (r *Runtime) createWeakSetProto(val *Object) objectImpl { + o := newBaseObjectObj(val, r.global.ObjectPrototype, classObject) + + o._putProp("constructor", r.global.WeakSet, true, false, true) + r.global.weakSetAdder = r.newNativeFunc(r.weakSetProto_add, nil, "add", nil, 1) + o._putProp("add", r.global.weakSetAdder, true, false, true) + o._putProp("delete", r.newNativeFunc(r.weakSetProto_delete, nil, "delete", nil, 1), true, false, true) + o._putProp("has", r.newNativeFunc(r.weakSetProto_has, nil, "has", nil, 1), true, false, true) + + o.put(symToStringTag, valueProp(asciiString(classWeakSet), false, false, true), true) + + return o +} + +func (r *Runtime) createWeakSet(val *Object) objectImpl { + o := r.newNativeFuncObj(val, r.constructorThrower("WeakSet"), r.builtin_newWeakSet, "WeakSet", r.global.WeakSetPrototype, 0) + + return o +} + +func (r *Runtime) initWeakSet() { + r.global.WeakSetPrototype = r.newLazyObject(r.createWeakSetProto) + r.global.WeakSet = r.newLazyObject(r.createWeakSet) + + r.addToGlobal("WeakSet", r.global.WeakSet) +} diff --git a/builtin_weakset_test.go b/builtin_weakset_test.go new file mode 100644 index 00000000..0194a981 --- /dev/null +++ b/builtin_weakset_test.go @@ -0,0 +1,88 @@ +package goja + +import ( + "runtime" + "testing" +) + +func TestWeakSetBasic(t *testing.T) { + const SCRIPT = ` + var s = new WeakSet(); + var o = {}; + s.add(o); + if (!s.has(o)) { + throw new Error("has"); + } + s.delete(o); + if (s.has(o)) { + throw new Error("still has"); + } + ` + testScript1(SCRIPT, _undefined, t) +} + +func TestWeakSetExpiry(t *testing.T) { + vm := New() + _, err := vm.RunString(` + var s = new WeakSet(); + var o = {}; + s.add(o); + if (!s.has(o)) { + throw new Error("has"); + } + o = undefined; + `) + if err != nil { + t.Fatal(err) + } + runtime.GC() + wso := vm.Get("s").ToObject(vm).self.(*weakSetObject) + wso.set.Lock() + l := len(wso.set.data) + wso.set.Unlock() + if l > 0 { + t.Fatal("Object has not been removed") + } +} + +func TestWeakSetArraySimple(t *testing.T) { + const SCRIPT = ` + var o1 = {}, o2 = {}, o3 = {}; + + var s = new WeakSet([o1, o2, o3]); + s.has(o1) && s.has(o2) && s.has(o3); + ` + testScript1(SCRIPT, valueTrue, t) +} + +func TestWeakSetArrayGeneric(t *testing.T) { + const SCRIPT = ` + var o1 = {}, o2 = {}, o3 = {}; + var a = new Array(); + var s; + var thrown = false; + a[1] = o2; + + try { + s = new WeakSet(a); + } catch (e) { + if (e instanceof TypeError) { + thrown = true; + } + } + if (!thrown) { + throw new Error("Case 1 does not throw"); + } + + Object.defineProperty(a.__proto__, "0", {value: o1, writable: true, enumerable: true, configurable: true}); + s = new WeakSet(a); + if (!(s.has(o1) && s.has(o2) && !s.has(o3))) { + throw new Error("Case 2 failed"); + } + + Object.defineProperty(a, "2", {value: o3, configurable: true}); + s = new WeakSet(a); + s.has(o1) && s.has(o2) && s.has(o3); + ` + testScript1(SCRIPT, valueTrue, t) +} diff --git a/map.go b/map.go new file mode 100644 index 00000000..50fae8ae --- /dev/null +++ b/map.go @@ -0,0 +1,163 @@ +package goja + +type mapEntry struct { + key, value Value + + iterPrev, iterNext *mapEntry + hNext *mapEntry +} + +type orderedMap struct { + hash map[uint64]*mapEntry + iterFirst, iterLast *mapEntry + size int +} + +type orderedMapIter struct { + m *orderedMap + cur *mapEntry +} + +func (m *orderedMap) lookup(key Value) (h uint64, entry, hPrev *mapEntry) { + if key == _negativeZero { + key = intToValue(0) + } + h = key.hash() + for entry = m.hash[h]; entry != nil && !entry.key.SameAs(key); hPrev, entry = entry, entry.hNext { + } + return +} + +func (m *orderedMap) set(key, value Value) { + h, entry, hPrev := m.lookup(key) + if entry != nil { + entry.value = value + } else { + if key == _negativeZero { + key = intToValue(0) + } + entry = &mapEntry{key: key, value: value} + if hPrev == nil { + m.hash[h] = entry + } else { + hPrev.hNext = entry + } + if m.iterLast != nil { + entry.iterPrev = m.iterLast + m.iterLast.iterNext = entry + } else { + m.iterFirst = entry + } + m.iterLast = entry + m.size++ + } +} + +func (m *orderedMap) get(key Value) Value { + _, entry, _ := m.lookup(key) + if entry != nil { + return entry.value + } + + return nil +} + +func (m *orderedMap) remove(key Value) bool { + h, entry, hPrev := m.lookup(key) + if entry != nil { + entry.key = nil + entry.value = nil + + // remove from the doubly-linked list + if entry.iterPrev != nil { + entry.iterPrev.iterNext = entry.iterNext + } else { + m.iterFirst = entry.iterNext + } + if entry.iterNext != nil { + entry.iterNext.iterPrev = entry.iterPrev + } else { + m.iterLast = entry.iterPrev + } + + // remove from the hash + if hPrev == nil { + if entry.hNext == nil { + delete(m.hash, h) + } else { + m.hash[h] = entry.hNext + } + } else { + hPrev.hNext = entry.hNext + } + + m.size-- + return true + } + + return false +} + +func (m *orderedMap) has(key Value) bool { + _, entry, _ := m.lookup(key) + return entry != nil +} + +func (iter *orderedMapIter) next() *mapEntry { + if iter.m == nil { + // closed iterator + return nil + } + + cur := iter.cur + // if the current item was deleted, track back to find the latest that wasn't + for cur != nil && cur.key == nil { + cur = cur.iterPrev + } + + if cur != nil { + cur = cur.iterNext + } else { + cur = iter.m.iterFirst + } + + if cur == nil { + iter.close() + } else { + iter.cur = cur + } + + return cur +} + +func (iter *orderedMapIter) close() { + iter.m = nil + iter.cur = nil +} + +func newOrderedMap() *orderedMap { + return &orderedMap{ + hash: make(map[uint64]*mapEntry), + } +} + +func (m *orderedMap) newIter() *orderedMapIter { + iter := &orderedMapIter{ + m: m, + } + return iter +} + +func (m *orderedMap) clear() { + for item := m.iterFirst; item != nil; item = item.iterNext { + item.key = nil + item.value = nil + if item.iterPrev != nil { + item.iterPrev.iterNext = nil + } + } + m.iterFirst = nil + m.iterLast = nil + m.hash = make(map[uint64]*mapEntry) + m.size = 0 +} diff --git a/map_test.go b/map_test.go new file mode 100644 index 00000000..cdf7addb --- /dev/null +++ b/map_test.go @@ -0,0 +1,196 @@ +package goja + +import ( + "math" + "strconv" + "testing" +) + +func testMapHashVal(v1, v2 Value, expected bool, t *testing.T) { + actual := v1.hash() == v2.hash() + if actual != expected { + t.Fatalf("testMapHashVal failed for %v, %v", v1, v2) + } +} + +func TestMapHash(t *testing.T) { + testMapHashVal(_NaN, _NaN, true, t) + testMapHashVal(valueTrue, valueFalse, false, t) + testMapHashVal(valueTrue, valueTrue, true, t) + testMapHashVal(intToValue(0), _negativeZero, true, t) + testMapHashVal(asciiString("Test"), asciiString("Test"), true, t) + testMapHashVal(newStringValue("ВСст"), newStringValue("ВСст"), true, t) + testMapHashVal(floatToValue(1.2345), floatToValue(1.2345), true, t) + testMapHashVal(symIterator, symToStringTag, false, t) + testMapHashVal(symIterator, symIterator, true, t) + + // The following tests introduce indeterministic behaviour + //testMapHashVal(asciiString("Test"), asciiString("Test1"), false, t) + //testMapHashVal(newStringValue("ВСст"), asciiString("Test"), false, t) + //testMapHashVal(newStringValue("ВСст"), newStringValue("ВСст1"), false, t) +} + +func TestOrderedMap(t *testing.T) { + m := newOrderedMap() + for i := int64(0); i < 50; i++ { + m.set(intToValue(i), asciiString(strconv.FormatInt(i, 10))) + } + if m.size != 50 { + t.Fatalf("Unexpected size: %d", m.size) + } + + for i := int64(0); i < 50; i++ { + expected := asciiString(strconv.FormatInt(i, 10)) + actual := m.get(intToValue(i)) + if !expected.SameAs(actual) { + t.Fatalf("Wrong value for %d", i) + } + } + + for i := int64(0); i < 50; i += 2 { + if !m.remove(intToValue(i)) { + t.Fatalf("remove(%d) return false", i) + } + } + if m.size != 25 { + t.Fatalf("Unexpected size: %d", m.size) + } + + iter := m.newIter() + count := 0 + for { + entry := iter.next() + if entry == nil { + break + } + m.remove(entry.key) + count++ + } + + if count != 25 { + t.Fatalf("Unexpected iter count: %d", count) + } + + if m.size != 0 { + t.Fatalf("Unexpected size: %d", m.size) + } +} + +func TestOrderedMapCollision(t *testing.T) { + m := newOrderedMap() + n1 := uint64(123456789) + n2 := math.Float64frombits(n1) + n1Key := intToValue(int64(n1)) + n2Key := floatToValue(n2) + m.set(n1Key, asciiString("n1")) + m.set(n2Key, asciiString("n2")) + if m.size == len(m.hash) { + t.Fatal("Expected a collision but there wasn't one") + } + if n2Val := m.get(n2Key); !asciiString("n2").SameAs(n2Val) { + t.Fatalf("unexpected n2Val: %v", n2Val) + } + if n1Val := m.get(n1Key); !asciiString("n1").SameAs(n1Val) { + t.Fatalf("unexpected nVal: %v", n1Val) + } + + if !m.remove(n1Key) { + t.Fatal("removing n1Key returned false") + } + if n2Val := m.get(n2Key); !asciiString("n2").SameAs(n2Val) { + t.Fatalf("2: unexpected n2Val: %v", n2Val) + } +} + +func TestOrderedMapIter(t *testing.T) { + m := newOrderedMap() + iter := m.newIter() + ent := iter.next() + if ent != nil { + t.Fatal("entry should be nil") + } + iter1 := m.newIter() + m.set(intToValue(1), valueTrue) + ent = iter.next() + if ent != nil { + t.Fatal("2: entry should be nil") + } + ent = iter1.next() + if ent == nil { + t.Fatal("entry is nil") + } + if !intToValue(1).SameAs(ent.key) { + t.Fatal("unexpected key") + } + if !valueTrue.SameAs(ent.value) { + t.Fatal("unexpected value") + } +} + +func TestOrderedMapIterVisitAfterReAdd(t *testing.T) { + m := newOrderedMap() + one := intToValue(1) + two := intToValue(2) + + m.set(one, valueTrue) + m.set(two, valueTrue) + iter := m.newIter() + entry := iter.next() + if !one.SameAs(entry.key) { + t.Fatalf("1: unexpected key: %v", entry.key) + } + if !m.remove(one) { + t.Fatal("remove returned false") + } + entry = iter.next() + if !two.SameAs(entry.key) { + t.Fatalf("2: unexpected key: %v", entry.key) + } + m.set(one, valueTrue) + entry = iter.next() + if entry == nil { + t.Fatal("entry is nil") + } + if !one.SameAs(entry.key) { + t.Fatalf("3: unexpected key: %v", entry.key) + } +} + +func TestOrderedMapIterAddAfterClear(t *testing.T) { + m := newOrderedMap() + one := intToValue(1) + m.set(one, valueTrue) + iter := m.newIter() + iter.next() + m.clear() + m.set(one, valueTrue) + entry := iter.next() + if entry == nil { + t.Fatal("entry is nil") + } + if entry.key != one { + t.Fatalf("unexpected key: %v", entry.key) + } + entry = iter.next() + if entry != nil { + t.Fatalf("entry is not nil: %v", entry) + } +} + +func TestOrderedMapIterDeleteCurrent(t *testing.T) { + m := newOrderedMap() + one := intToValue(1) + two := intToValue(2) + iter := m.newIter() + m.set(one, valueTrue) + m.set(two, valueTrue) + entry := iter.next() + if entry.key != one { + t.Fatalf("unexpected key: %v", entry.key) + } + m.remove(one) + entry = iter.next() + if entry.key != two { + t.Fatalf("2: unexpected key: %v", entry.key) + } +} diff --git a/object.go b/object.go index 23f3adba..752e94d8 100644 --- a/object.go +++ b/object.go @@ -3,11 +3,17 @@ package goja import ( "fmt" "reflect" + "runtime" + "unsafe" ) const ( classObject = "Object" classArray = "Array" + classWeakSet = "WeakSet" + classWeakMap = "WeakMap" + classMap = "Map" + classSet = "Set" classFunction = "Function" classNumber = "Number" classString = "String" @@ -17,11 +23,72 @@ const ( classDate = "Date" classArrayIterator = "Array Iterator" + classMapIterator = "Map Iterator" + classSetIterator = "Set Iterator" ) +type weakCollection interface { + removePtr(uintptr) +} + +type weakCollections struct { + colls []weakCollection +} + +func (r *weakCollections) add(c weakCollection) { + for _, ec := range r.colls { + if ec == c { + return + } + } + r.colls = append(r.colls, c) +} + +func (r *weakCollections) id() uintptr { + return uintptr(unsafe.Pointer(r)) +} + +func (r *weakCollections) remove(c weakCollection) { + if cap(r.colls) > 16 && cap(r.colls)>>2 > len(r.colls) { + // shrink + colls := make([]weakCollection, 0, len(r.colls)) + for _, coll := range r.colls { + if coll != c { + colls = append(colls, coll) + } + } + r.colls = colls + } else { + for i, coll := range r.colls { + if coll == c { + l := len(r.colls) - 1 + r.colls[i] = r.colls[l] + r.colls[l] = nil + r.colls = r.colls[:l] + break + } + } + } +} + +func finalizeObjectWeakRefs(r *weakCollections) { + id := r.id() + for _, c := range r.colls { + c.removePtr(id) + } + r.colls = nil +} + type Object struct { runtime *Runtime self objectImpl + + // Contains references to all weak collections that contain this Object. + // weakColls has a finalizer that removes the Object's id from all weak collections. + // The id is the weakColls pointer value converted to uintptr. + // Note, cannot set the finalizer on the *Object itself because it's a part of a + // reference cycle. + weakColls *weakCollections } type iterNextFunc func() (propIterItem, iterNextFunc) @@ -790,3 +857,12 @@ func instanceOfOperator(o Value, c *Object) bool { return c.self.hasInstance(o) } + +func (o *Object) getWeakCollRefs() *weakCollections { + if o.weakColls == nil { + o.weakColls = &weakCollections{} + runtime.SetFinalizer(o.weakColls, finalizeObjectWeakRefs) + } + + return o.weakColls +} diff --git a/runtime.go b/runtime.go index 77b6eca1..d46c7c68 100644 --- a/runtime.go +++ b/runtime.go @@ -47,6 +47,10 @@ type global struct { Symbol *Object ArrayBuffer *Object + WeakSet *Object + WeakMap *Object + Map *Object + Set *Object Error *Object TypeError *Object @@ -70,9 +74,15 @@ type global struct { ArrayIterator *Object ArrayBufferPrototype *Object + WeakSetPrototype *Object + WeakMapPrototype *Object + MapPrototype *Object + SetPrototype *Object IteratorPrototype *Object ArrayIteratorPrototype *Object + MapIteratorPrototype *Object + SetIteratorPrototype *Object ErrorPrototype *Object TypeErrorPrototype *Object @@ -90,6 +100,11 @@ type global struct { throwerProperty Value regexpProtoExec Value + weakSetAdder *Object + weakMapAdder *Object + mapAdder *Object + setAdder *Object + arrayValues *Object } type Flag int @@ -297,6 +312,10 @@ func (r *Runtime) init() { //r.initTypedArrays() r.initSymbol() + r.initWeakSet() + r.initWeakMap() + r.initMap() + r.initSet() r.global.thrower = r.newNativeFunc(r.builtin_thrower, nil, "thrower", nil, 0) r.global.throwerProperty = &valueProperty{ @@ -326,12 +345,13 @@ func (r *Runtime) newSyntaxError(msg string, offset int) Value { } func newBaseObjectObj(obj, proto *Object, class string) *baseObject { - o := &baseObject{} - o.class = class - o.val = obj - o.extensible = true + o := &baseObject{ + class: class, + val: obj, + extensible: true, + prototype: proto, + } obj.self = o - o.prototype = proto o.init() return o } @@ -1604,6 +1624,40 @@ func (r *Runtime) returnThis(call FunctionCall) Value { return call.This } +func (r *Runtime) getIterator(obj *Object, method func(FunctionCall) Value) *Object { + if method == nil { + method = toMethod(obj.self.get(symIterator)) + if method == nil { + panic(r.NewTypeError("object is not iterable")) + } + } + + return r.toObject(method(FunctionCall{ + This: obj, + })) +} + +func (r *Runtime) iterate(iter *Object, step func(Value)) { + for { + res := r.toObject(toMethod(iter.self.getStr("next"))(FunctionCall{This: iter})) + if res.self.getStr("done").ToBoolean() { + break + } + err := tryFunc(func() { + step(res.self.getStr("value")) + }) + if err != nil { + retMethod := toMethod(iter.self.getStr("return")) + if retMethod != nil { + _ = tryFunc(func() { + retMethod(FunctionCall{This: iter}) + }) + } + panic(err) + } + } +} + func (r *Runtime) createIterResultObject(value Value, done bool) Value { o := r.NewObject() o.self.putStr("value", value, false) @@ -1611,6 +1665,22 @@ func (r *Runtime) createIterResultObject(value Value, done bool) Value { return o } +func (r *Runtime) newLazyObject(create func(*Object) objectImpl) *Object { + val := &Object{runtime: r} + o := &lazyObject{ + val: val, + create: create, + } + val.self = o + return val +} + +func (r *Runtime) constructorThrower(name string) func(call FunctionCall) Value { + return func(FunctionCall) Value { + panic(r.NewTypeError("Constructor %s requires 'new'", name)) + } +} + func nilSafe(v Value) Value { if v != nil { return v diff --git a/string_ascii.go b/string_ascii.go index 8fecf341..dede99c0 100644 --- a/string_ascii.go +++ b/string_ascii.go @@ -225,6 +225,13 @@ func (s asciiString) baseObject(r *Runtime) *Object { return ss.val } +func (s asciiString) hash() uint64 { + _, _ = mapHasher.WriteString(string(s)) + h := mapHasher.Sum64() + mapHasher.Reset() + return h +} + func (s asciiString) charAt(idx int64) rune { return rune(s[idx]) } diff --git a/string_unicode.go b/string_unicode.go index 48903bab..58434126 100644 --- a/string_unicode.go +++ b/string_unicode.go @@ -13,6 +13,7 @@ import ( "strings" "unicode/utf16" "unicode/utf8" + "unsafe" ) type unicodeString []uint16 @@ -317,3 +318,10 @@ func (s unicodeString) Export() interface{} { func (s unicodeString) ExportType() reflect.Type { return reflectTypeString } + +func (s unicodeString) hash() uint64 { + _, _ = mapHasher.Write(*(*[]byte)(unsafe.Pointer(&s))) + h := mapHasher.Sum64() + mapHasher.Reset() + return h +} diff --git a/tc39_test.go b/tc39_test.go index 1dcacd25..c1642363 100644 --- a/tc39_test.go +++ b/tc39_test.go @@ -32,29 +32,51 @@ var ( "test/built-ins/Date/prototype/toISOString/15.9.5.43-0-10.js": true, // timezone "test/annexB/built-ins/escape/escape-above-astral.js": true, // \u{xxxxx} - "test/built-ins/Symbol/unscopables/cross-realm.js": true, - "test/built-ins/Symbol/toStringTag/cross-realm.js": true, - "test/built-ins/Symbol/toPrimitive/cross-realm.js": true, - "test/built-ins/Symbol/split/cross-realm.js": true, - "test/built-ins/Symbol/species/cross-realm.js": true, - "test/built-ins/Symbol/search/cross-realm.js": true, - "test/built-ins/Symbol/replace/cross-realm.js": true, - "test/built-ins/Symbol/match/cross-realm.js": true, - "test/built-ins/Symbol/keyFor/cross-realm.js": true, - "test/built-ins/Symbol/iterator/cross-realm.js": true, - "test/built-ins/Symbol/isConcatSpreadable/cross-realm.js": true, - "test/built-ins/Symbol/hasInstance/cross-realm.js": true, - "test/built-ins/Symbol/for/cross-realm.js": true, - "test/built-ins/Set/symbol-as-entry.js": true, - "test/built-ins/Map/symbol-as-entry-key.js": true, + // cross-realm + "test/built-ins/Symbol/unscopables/cross-realm.js": true, + "test/built-ins/Symbol/toStringTag/cross-realm.js": true, + "test/built-ins/Symbol/toPrimitive/cross-realm.js": true, + "test/built-ins/Symbol/split/cross-realm.js": true, + "test/built-ins/Symbol/species/cross-realm.js": true, + "test/built-ins/Symbol/search/cross-realm.js": true, + "test/built-ins/Symbol/replace/cross-realm.js": true, + "test/built-ins/Symbol/match/cross-realm.js": true, + "test/built-ins/Symbol/keyFor/cross-realm.js": true, + "test/built-ins/Symbol/iterator/cross-realm.js": true, + "test/built-ins/Symbol/isConcatSpreadable/cross-realm.js": true, + "test/built-ins/Symbol/hasInstance/cross-realm.js": true, + "test/built-ins/Symbol/for/cross-realm.js": true, + "test/built-ins/WeakSet/proto-from-ctor-realm.js": true, + "test/built-ins/WeakMap/proto-from-ctor-realm.js": true, + "test/built-ins/Map/proto-from-ctor-realm.js": true, + "test/built-ins/Set/proto-from-ctor-realm.js": true, + + // class "test/language/statements/class/subclass/builtin-objects/Symbol/symbol-valid-as-extends-value.js": true, "test/language/statements/class/subclass/builtin-objects/Symbol/new-symbol-with-super-throws.js": true, + "test/language/statements/class/subclass/builtin-objects/WeakSet/super-must-be-called.js": true, + "test/language/statements/class/subclass/builtin-objects/WeakSet/regular-subclassing.js": true, + "test/language/statements/class/subclass/builtin-objects/WeakMap/super-must-be-called.js": true, + "test/language/statements/class/subclass/builtin-objects/WeakMap/regular-subclassing.js": true, + "test/language/statements/class/subclass/builtin-objects/Map/super-must-be-called.js": true, + "test/language/statements/class/subclass/builtin-objects/Map/regular-subclassing.js": true, + "test/language/statements/class/subclass/builtin-objects/Set/super-must-be-called.js": true, + "test/language/statements/class/subclass/builtin-objects/Set/regular-subclassing.js": true, // Proxy "test/built-ins/Object/prototype/toString/proxy-revoked.js": true, "test/built-ins/Object/prototype/toString/proxy-function.js": true, "test/built-ins/Object/prototype/toString/proxy-array.js": true, "test/built-ins/JSON/stringify/value-proxy.js": true, + + // Arrow functions + "test/built-ins/Set/prototype/forEach/this-arg-explicit-cannot-override-lexical-this-arrow.js": true, + + // full unicode regexp flag + "test/built-ins/RegExp/prototype/Symbol.match/u-advance-after-empty.js": true, + "test/built-ins/RegExp/prototype/Symbol.match/get-unicode-error.js": true, + "test/built-ins/RegExp/prototype/Symbol.match/builtin-success-u-return-val-groups.js": true, + "test/built-ins/RegExp/prototype/Symbol.match/builtin-infer-unicode.js": true, } es6WhiteList = map[string]bool{} @@ -69,10 +91,14 @@ var ( "21.1.3.14", "21.1.3.15", "21.1.3.17", - //"21.2.5.6", + "21.2.5.6", "22.1.2.5", //"22.1.3.1", "22.1.3.29", + "23.1", + "23.2", + "23.3", + "23.4", "25.1.2", "B.2.1", "B.2.2", diff --git a/value.go b/value.go index f5e36350..ba3bf0e6 100644 --- a/value.go +++ b/value.go @@ -2,10 +2,12 @@ package goja import ( "fmt" + "hash/maphash" "math" "reflect" "regexp" "strconv" + "unsafe" ) var ( @@ -33,6 +35,10 @@ var ( var intCache [256]Value +var ( + mapHasher maphash.Hash +) + type Value interface { ToInteger() int64 ToString() valueString @@ -52,6 +58,8 @@ type Value interface { assertFloat() (float64, bool) baseObject(r *Runtime) *Object + + hash() uint64 } type typeError string @@ -199,6 +207,10 @@ func (i valueInt) ExportType() reflect.Type { return reflectTypeInt } +func (i valueInt) hash() uint64 { + return uint64(i) +} + func (o valueBool) ToInteger() int64 { if o { return 1 @@ -293,6 +305,13 @@ func (o valueBool) ExportType() reflect.Type { return reflectTypeBool } +func (b valueBool) hash() uint64 { + if b { + return uint64(uintptr(unsafe.Pointer(&valueTrue))) + } + return uint64(uintptr(unsafe.Pointer(&valueFalse))) +} + func (n valueNull) ToInteger() int64 { return 0 } @@ -331,6 +350,10 @@ func (u valueUndefined) ToFloat() float64 { return math.NaN() } +func (u valueUndefined) hash() uint64 { + return uint64(uintptr(unsafe.Pointer(&_undefined))) +} + func (n valueNull) ToFloat() float64 { return 0 } @@ -391,6 +414,10 @@ func (n valueNull) ExportType() reflect.Type { return reflectTypeNil } +func (n valueNull) hash() uint64 { + return uint64(uintptr(unsafe.Pointer(&_null))) +} + func (p *valueProperty) ToInteger() int64 { return 0 } @@ -488,6 +515,10 @@ func (n *valueProperty) ExportType() reflect.Type { panic("Cannot export valueProperty") } +func (n *valueProperty) hash() uint64 { + panic("valueProperty should never be used in maps or sets") +} + func (f valueFloat) ToInteger() int64 { switch { case math.IsNaN(float64(f)): @@ -621,6 +652,13 @@ func (f valueFloat) ExportType() reflect.Type { return reflectTypeFloat } +func (f valueFloat) hash() uint64 { + if f == _negativeZero { + return 0 + } + return math.Float64bits(float64(f)) +} + func (o *Object) ToInteger() int64 { return o.self.toPrimitiveNumber().ToNumber().ToInteger() } @@ -710,6 +748,10 @@ func (o *Object) ExportType() reflect.Type { return o.self.exportType() } +func (o *Object) hash() uint64 { + return uint64(uintptr(unsafe.Pointer(o))) +} + func (o *Object) Get(name string) Value { return o.self.getStr(name) } @@ -860,6 +902,11 @@ func (o valueUnresolved) ExportType() reflect.Type { return nil } +func (o valueUnresolved) hash() uint64 { + o.throw() + return 0 +} + func (s *valueSymbol) ToInteger() int64 { panic(typeError("Cannot convert a Symbol value to a number")) } @@ -927,6 +974,10 @@ func (s *valueSymbol) baseObject(r *Runtime) *Object { return r.newPrimitiveObject(s, r.global.SymbolPrototype, "Symbol") } +func (s *valueSymbol) hash() uint64 { + return uint64(uintptr(unsafe.Pointer(s))) +} + func (s *valueSymbol) descString() string { return fmt.Sprintf("Symbol(%s)", s.desc) } From d63d7d53d2dd49cc0b14b7a9bed8d610acf879b9 Mon Sep 17 00:00:00 2001 From: Dmitry Panov Date: Tue, 3 Mar 2020 19:14:14 +0000 Subject: [PATCH 03/46] Fixed panics when iterator returns values with missing properties. --- builtin_map.go | 4 +-- builtin_map_test.go | 59 +++++++++++++++++++++++++++++++++++++++++++++ builtin_set_test.go | 23 ++++++++++++++++++ builtin_weakmap.go | 2 +- runtime.go | 4 +-- 5 files changed, 87 insertions(+), 5 deletions(-) create mode 100644 builtin_map_test.go create mode 100644 builtin_set_test.go diff --git a/builtin_map.go b/builtin_map.go index 5196595e..2d2e3fff 100644 --- a/builtin_map.go +++ b/builtin_map.go @@ -157,8 +157,8 @@ func (r *Runtime) builtin_newMap(args []Value) *Object { if adder == r.global.mapAdder { r.iterate(iter, func(item Value) { itemObj := r.toObject(item) - k := itemObj.self.get(i0) - v := itemObj.self.get(i1) + k := nilSafe(itemObj.self.get(i0)) + v := nilSafe(itemObj.self.get(i1)) mo.m.set(k, v) }) } else { diff --git a/builtin_map_test.go b/builtin_map_test.go new file mode 100644 index 00000000..691eb320 --- /dev/null +++ b/builtin_map_test.go @@ -0,0 +1,59 @@ +package goja + +import "testing" + +func TestMapEvilIterator(t *testing.T) { + const SCRIPT = ` + 'use strict'; + var o = {}; + + function Iter(value) { + this.value = value; + this.idx = 0; + } + + Iter.prototype.next = function() { + var idx = this.idx; + if (idx === 0) { + this.idx++; + return this.value; + } + return {done: true}; + } + + o[Symbol.iterator] = function() { + return new Iter({}); + } + + assert.throws(TypeError, function() { + new Map(o); + }); + + o[Symbol.iterator] = function() { + return new Iter({value: []}); + } + + function t(prefix) { + var m = new Map(o); + assert.sameValue(1, m.size, prefix+": m.size"); + assert.sameValue(true, m.has(undefined), prefix+": m.has(undefined)"); + assert.sameValue(undefined, m.get(undefined), prefix+": m.get(undefined)"); + } + + t("standard adder"); + + var count = 0; + var origSet = Map.prototype.set; + + Map.prototype.set = function() { + count++; + origSet.apply(this, arguments); + } + + t("custom adder"); + assert.sameValue(1, count, "count"); + + undefined; + ` + testScript1(TESTLIB+SCRIPT, _undefined, t) +} diff --git a/builtin_set_test.go b/builtin_set_test.go new file mode 100644 index 00000000..492032ac --- /dev/null +++ b/builtin_set_test.go @@ -0,0 +1,23 @@ +package goja + +import "testing" + +func TestSetEvilIterator(t *testing.T) { + const SCRIPT = ` + var o = {}; + o[Symbol.iterator] = function() { + return { + next: function() { + if (!this.flag) { + this.flag = true; + return {}; + } + return {done: true}; + } + } + } + new Set(o); + undefined; + ` + testScript1(SCRIPT, _undefined, t) +} diff --git a/builtin_weakmap.go b/builtin_weakmap.go index d2d941b3..a8736a5a 100644 --- a/builtin_weakmap.go +++ b/builtin_weakmap.go @@ -153,7 +153,7 @@ func (r *Runtime) builtin_newWeakMap(args []Value) *Object { r.iterate(iter, func(item Value) { itemObj := r.toObject(item) k := itemObj.self.get(i0) - v := itemObj.self.get(i1) + v := nilSafe(itemObj.self.get(i1)) wmo.m.set(r.toObject(k), v) }) } else { diff --git a/runtime.go b/runtime.go index d46c7c68..a6a65232 100644 --- a/runtime.go +++ b/runtime.go @@ -1640,11 +1640,11 @@ func (r *Runtime) getIterator(obj *Object, method func(FunctionCall) Value) *Obj func (r *Runtime) iterate(iter *Object, step func(Value)) { for { res := r.toObject(toMethod(iter.self.getStr("next"))(FunctionCall{This: iter})) - if res.self.getStr("done").ToBoolean() { + if nilSafe(res.self.getStr("done")).ToBoolean() { break } err := tryFunc(func() { - step(res.self.getStr("value")) + step(nilSafe(res.self.getStr("value"))) }) if err != nil { retMethod := toMethod(iter.self.getStr("return")) From a0559b5ea4b1bf7965feef7c9b77b445f6c6ef8c Mon Sep 17 00:00:00 2001 From: Dmitry Panov Date: Wed, 4 Mar 2020 00:07:55 +0000 Subject: [PATCH 04/46] Missing Object methods, ToPropertyKey, GetV --- array.go | 2 +- array_sparse.go | 2 +- builtin_function.go | 6 ++-- builtin_global.go | 16 ++++----- builtin_json.go | 2 +- builtin_map.go | 2 +- builtin_object.go | 68 ++++++++++++++++++++++++++++++++--- builtin_regexp.go | 30 ++++++++-------- builtin_set.go | 2 +- builtin_string.go | 46 ++++++++++++------------ builtin_symbol.go | 4 +-- builtin_weakmap.go | 2 +- builtin_weakset.go | 2 +- object.go | 47 +++++++++++++++++-------- object_goreflect.go | 2 +- object_lazy.go | 6 ++++ object_test.go | 2 +- runtime.go | 36 +++++++++++++++---- runtime_test.go | 70 ++++++++++++++++++++++++++++++++++++ string_ascii.go | 6 +++- string_unicode.go | 6 +++- tc39_test.go | 19 ++++++---- value.go | 86 ++++++++++++++++++++++++++++++++------------- vm.go | 23 ++++++------ vm_test.go | 4 +-- 25 files changed, 359 insertions(+), 132 deletions(-) diff --git a/array.go b/array.go index 3afa282f..eca5b548 100644 --- a/array.go +++ b/array.go @@ -485,7 +485,7 @@ func (a *arrayObject) _deleteProp(idx int64, throw bool) bool { if v := a.values[idx]; v != nil { if p, ok := v.(*valueProperty); ok { if !p.configurable { - a.val.runtime.typeErrorResult(throw, "Cannot delete property '%d' of %s", idx, a.val.ToString()) + a.val.runtime.typeErrorResult(throw, "Cannot delete property '%d' of %s", idx, a.val.toString()) return false } a.propValueCount-- diff --git a/array_sparse.go b/array_sparse.go index ec67f29a..a454ba58 100644 --- a/array_sparse.go +++ b/array_sparse.go @@ -384,7 +384,7 @@ func (a *sparseArrayObject) _deleteProp(idx int64, throw bool) bool { if i < len(a.items) && a.items[i].idx == idx { if p, ok := a.items[i].value.(*valueProperty); ok { if !p.configurable { - a.val.runtime.typeErrorResult(throw, "Cannot delete property '%d' of %s", idx, a.val.ToString()) + a.val.runtime.typeErrorResult(throw, "Cannot delete property '%d' of %s", idx, a.val.toString()) return false } a.propValueCount-- diff --git a/builtin_function.go b/builtin_function.go index aaeb0352..78fd7252 100644 --- a/builtin_function.go +++ b/builtin_function.go @@ -28,9 +28,9 @@ repeat: case *funcObject: return newStringValue(f.src) case *nativeFuncObject: - return newStringValue(fmt.Sprintf("function %s() { [native code] }", f.nameProp.get(call.This).ToString())) + return newStringValue(fmt.Sprintf("function %s() { [native code] }", f.nameProp.get(call.This).toString())) case *boundFuncObject: - return newStringValue(fmt.Sprintf("function %s() { [native code] }", f.nameProp.get(call.This).ToString())) + return newStringValue(fmt.Sprintf("function %s() { [native code] }", f.nameProp.get(call.This).toString())) case *lazyObject: obj.self = f.create(obj) goto repeat @@ -129,7 +129,7 @@ repeat: f = ff.create(obj) goto repeat default: - r.typeErrorResult(true, "Value is not callable: %s", obj.ToString()) + r.typeErrorResult(true, "Value is not callable: %s", obj.toString()) } l := int(toUInt32(obj.self.getStr("length"))) diff --git a/builtin_global.go b/builtin_global.go index da41f7e2..2f22d62d 100644 --- a/builtin_global.go +++ b/builtin_global.go @@ -26,14 +26,14 @@ func (r *Runtime) builtin_isNaN(call FunctionCall) Value { } func (r *Runtime) builtin_parseInt(call FunctionCall) Value { - str := call.Argument(0).ToString().toTrimmedUTF8() + str := call.Argument(0).toString().toTrimmedUTF8() radix := int(toInt32(call.Argument(1))) v, _ := parseInt(str, radix) return v } func (r *Runtime) builtin_parseFloat(call FunctionCall) Value { - m := parseFloatRegexp.FindStringSubmatch(call.Argument(0).ToString().toTrimmedUTF8()) + m := parseFloatRegexp.FindStringSubmatch(call.Argument(0).toString().toTrimmedUTF8()) if len(m) == 2 { if s := m[1]; s != "" && s != "+" && s != "-" { switch s { @@ -217,27 +217,27 @@ func unhex(c byte) byte { } func (r *Runtime) builtin_decodeURI(call FunctionCall) Value { - uriString := call.Argument(0).ToString() + uriString := call.Argument(0).toString() return r._decode(uriString, &uriReservedHash) } func (r *Runtime) builtin_decodeURIComponent(call FunctionCall) Value { - uriString := call.Argument(0).ToString() + uriString := call.Argument(0).toString() return r._decode(uriString, &emptyEscapeSet) } func (r *Runtime) builtin_encodeURI(call FunctionCall) Value { - uriString := call.Argument(0).ToString() + uriString := call.Argument(0).toString() return r._encode(uriString, &uriReservedUnescapedHash) } func (r *Runtime) builtin_encodeURIComponent(call FunctionCall) Value { - uriString := call.Argument(0).ToString() + uriString := call.Argument(0).toString() return r._encode(uriString, &uriUnescaped) } func (r *Runtime) builtin_escape(call FunctionCall) Value { - s := call.Argument(0).ToString() + s := call.Argument(0).toString() var sb strings.Builder l := s.length() for i := int64(0); i < l; i++ { @@ -261,7 +261,7 @@ func (r *Runtime) builtin_escape(call FunctionCall) Value { } func (r *Runtime) builtin_unescape(call FunctionCall) Value { - s := call.Argument(0).ToString() + s := call.Argument(0).toString() l := s.length() _, unicode := s.(unicodeString) var asciiBuf []byte diff --git a/builtin_json.go b/builtin_json.go index bfa93811..bbcb18e1 100644 --- a/builtin_json.go +++ b/builtin_json.go @@ -449,7 +449,7 @@ func (ctx *_builtinJSON_stringifyContext) jo(object *Object) { if !empty { ctx.buf.WriteString(separator) } - ctx.quote(name.ToString()) + ctx.quote(name.toString()) if ctx.gap != "" { ctx.buf.WriteString(": ") } else { diff --git a/builtin_map.go b/builtin_map.go index 2d2e3fff..e0c437e9 100644 --- a/builtin_map.go +++ b/builtin_map.go @@ -151,7 +151,7 @@ func (r *Runtime) builtin_newMap(args []Value) *Object { if len(args) > 0 { if arg := args[0]; arg != nil && arg != _undefined && arg != _null { adder := mo.getStr("set") - iter := r.getIterator(arg.ToObject(r), nil) + iter := r.getIterator(arg, nil) i0 := intToValue(0) i1 := intToValue(1) if adder == r.global.mapAdder { diff --git a/builtin_object.go b/builtin_object.go index dda319d3..1ae0abdc 100644 --- a/builtin_object.go +++ b/builtin_object.go @@ -25,7 +25,7 @@ func (r *Runtime) object_getPrototypeOf(call FunctionCall) Value { func (r *Runtime) object_getOwnPropertyDescriptor(call FunctionCall) Value { obj := call.Argument(0).ToObject(r) - propName := call.Argument(1) + propName := toPropertyKey(call.Argument(1)) desc := obj.self.getOwnProp(propName) if desc == nil { return _undefined @@ -370,7 +370,7 @@ func (r *Runtime) object_keys(call FunctionCall) Value { } func (r *Runtime) objectproto_hasOwnProperty(call FunctionCall) Value { - p := call.Argument(0) + p := toPropertyKey(call.Argument(0)) o := call.This.ToObject(r) if o.self.hasOwnProperty(p) { return valueTrue @@ -396,7 +396,7 @@ func (r *Runtime) objectproto_isPrototypeOf(call FunctionCall) Value { } func (r *Runtime) objectproto_propertyIsEnumerable(call FunctionCall) Value { - p := call.Argument(0) + p := toPropertyKey(call.Argument(0)) o := call.This.ToObject(r) pv := o.self.getOwnProp(p) if pv == nil { @@ -432,13 +432,70 @@ func (r *Runtime) objectproto_toString(call FunctionCall) Value { } func (r *Runtime) objectproto_toLocaleString(call FunctionCall) Value { - return call.This.ToObject(r).ToString() + toString := toMethod(r.getVStr(call.This, "toString")) + return toString(FunctionCall{This: call.This}) } func (r *Runtime) objectproto_valueOf(call FunctionCall) Value { return call.This.ToObject(r) } +func (r *Runtime) object_assign(call FunctionCall) Value { + to := call.Argument(0).ToObject(r) + if len(call.Arguments) > 1 { + for _, arg := range call.Arguments[1:] { + if arg != _undefined && arg != _null { + source := arg.ToObject(r) + for item, f := source.self.enumerate(false, false)(); f != nil; item, f = f() { + p := source.self.getOwnPropStr(item.name) + if v, ok := p.(*valueProperty); ok { + p = v.get(source) + } + to.self.putStr(item.name, p, true) + } + + for _, sym := range source.self.getOwnSymbols() { + p := source.self.getOwnProp(sym) + if v, ok := p.(*valueProperty); ok { + if !v.enumerable { + continue + } + p = v.get(source) + } + to.self.put(sym, p, true) + } + } + } + } + + return to +} + +func (r *Runtime) object_is(call FunctionCall) Value { + return r.toBoolean(call.Argument(0).SameAs(call.Argument(1))) +} + +func (r *Runtime) object_setPrototypeOf(call FunctionCall) Value { + o := call.Argument(0) + r.checkObjectCoercible(o) + proto := call.Argument(1) + var protoObj *Object + if proto != _null { + if obj, ok := proto.(*Object); ok { + protoObj = obj + } else { + panic(r.NewTypeError("Object prototype may only be an Object or null: %s", proto)) + } + } + if o, ok := o.(*Object); ok { + if res := o.self.setProto(protoObj); res != nil { + panic(res) + } + } + + return o +} + func (r *Runtime) initObject() { o := r.global.ObjectPrototype.self o._putProp("toString", r.newNativeFunc(r.objectproto_toString, nil, "toString", nil, 0), true, false, true) @@ -450,10 +507,12 @@ func (r *Runtime) initObject() { r.global.Object = r.newNativeFuncConstruct(r.builtin_Object, classObject, r.global.ObjectPrototype, 1) o = r.global.Object.self + o._putProp("assign", r.newNativeFunc(r.object_assign, nil, "assign", nil, 2), true, false, true) o._putProp("defineProperty", r.newNativeFunc(r.object_defineProperty, nil, "defineProperty", nil, 3), true, false, true) o._putProp("defineProperties", r.newNativeFunc(r.object_defineProperties, nil, "defineProperties", nil, 2), true, false, true) o._putProp("getOwnPropertyDescriptor", r.newNativeFunc(r.object_getOwnPropertyDescriptor, nil, "getOwnPropertyDescriptor", nil, 2), true, false, true) o._putProp("getPrototypeOf", r.newNativeFunc(r.object_getPrototypeOf, nil, "getPrototypeOf", nil, 1), true, false, true) + o._putProp("is", r.newNativeFunc(r.object_is, nil, "is", nil, 2), true, false, true) o._putProp("getOwnPropertyNames", r.newNativeFunc(r.object_getOwnPropertyNames, nil, "getOwnPropertyNames", nil, 1), true, false, true) o._putProp("getOwnPropertySymbols", r.newNativeFunc(r.object_getOwnPropertySymbols, nil, "getOwnPropertySymbols", nil, 1), true, false, true) o._putProp("create", r.newNativeFunc(r.object_create, nil, "create", nil, 2), true, false, true) @@ -464,6 +523,7 @@ func (r *Runtime) initObject() { o._putProp("isFrozen", r.newNativeFunc(r.object_isFrozen, nil, "isFrozen", nil, 1), true, false, true) o._putProp("isExtensible", r.newNativeFunc(r.object_isExtensible, nil, "isExtensible", nil, 1), true, false, true) o._putProp("keys", r.newNativeFunc(r.object_keys, nil, "keys", nil, 1), true, false, true) + o._putProp("setPrototypeOf", r.newNativeFunc(r.object_setPrototypeOf, nil, "setPrototypeOf", nil, 2), true, false, true) r.addToGlobal("Object", r.global.Object) } diff --git a/builtin_regexp.go b/builtin_regexp.go index ca3b721b..42749635 100644 --- a/builtin_regexp.go +++ b/builtin_regexp.go @@ -133,7 +133,7 @@ func (r *Runtime) builtin_newRegExp(args []Value) *Object { } } if args[0] != _undefined { - pattern = args[0].ToString() + pattern = args[0].toString() } } if len(args) > 1 { @@ -161,22 +161,22 @@ func (r *Runtime) builtin_RegExp(call FunctionCall) Value { func (r *Runtime) regexpproto_exec(call FunctionCall) Value { if this, ok := r.toObject(call.This).self.(*regexpObject); ok { - return this.exec(call.Argument(0).ToString()) + return this.exec(call.Argument(0).toString()) } else { - r.typeErrorResult(true, "Method RegExp.prototype.exec called on incompatible receiver %s", call.This.ToString()) + r.typeErrorResult(true, "Method RegExp.prototype.exec called on incompatible receiver %s", call.This.toString()) return nil } } func (r *Runtime) regexpproto_test(call FunctionCall) Value { if this, ok := r.toObject(call.This).self.(*regexpObject); ok { - if this.test(call.Argument(0).ToString()) { + if this.test(call.Argument(0).toString()) { return valueTrue } else { return valueFalse } } else { - r.typeErrorResult(true, "Method RegExp.prototype.test called on incompatible receiver %s", call.This.ToString()) + r.typeErrorResult(true, "Method RegExp.prototype.test called on incompatible receiver %s", call.This.toString()) return nil } } @@ -207,7 +207,7 @@ func (r *Runtime) regexpproto_getSource(call FunctionCall) Value { if this, ok := r.toObject(call.This).self.(*regexpObject); ok { return this.source } else { - r.typeErrorResult(true, "Method RegExp.prototype.source getter called on incompatible receiver %s", call.This.ToString()) + r.typeErrorResult(true, "Method RegExp.prototype.source getter called on incompatible receiver %s", call.This.toString()) return nil } } @@ -220,7 +220,7 @@ func (r *Runtime) regexpproto_getGlobal(call FunctionCall) Value { return valueFalse } } else { - r.typeErrorResult(true, "Method RegExp.prototype.global getter called on incompatible receiver %s", call.This.ToString()) + r.typeErrorResult(true, "Method RegExp.prototype.global getter called on incompatible receiver %s", call.This.toString()) return nil } } @@ -233,7 +233,7 @@ func (r *Runtime) regexpproto_getMultiline(call FunctionCall) Value { return valueFalse } } else { - r.typeErrorResult(true, "Method RegExp.prototype.multiline getter called on incompatible receiver %s", call.This.ToString()) + r.typeErrorResult(true, "Method RegExp.prototype.multiline getter called on incompatible receiver %s", call.This.toString()) return nil } } @@ -246,7 +246,7 @@ func (r *Runtime) regexpproto_getIgnoreCase(call FunctionCall) Value { return valueFalse } } else { - r.typeErrorResult(true, "Method RegExp.prototype.ignoreCase getter called on incompatible receiver %s", call.This.ToString()) + r.typeErrorResult(true, "Method RegExp.prototype.ignoreCase getter called on incompatible receiver %s", call.This.toString()) return nil } } @@ -259,7 +259,7 @@ func (r *Runtime) regexpproto_getSticky(call FunctionCall) Value { return valueFalse } } else { - r.typeErrorResult(true, "Method RegExp.prototype.sticky getter called on incompatible receiver %s", call.This.ToString()) + r.typeErrorResult(true, "Method RegExp.prototype.sticky getter called on incompatible receiver %s", call.This.toString()) return nil } } @@ -332,7 +332,7 @@ func (r *Runtime) regexpproto_stdMatcherGeneric(rxObj *Object, arg Value) Value if res == _null { break } - matchStr := nilSafe(r.toObject(res).self.get(intToValue(0))).ToString() + matchStr := nilSafe(r.toObject(res).self.get(intToValue(0))).toString() a = append(a, matchStr) if matchStr.length() == 0 { thisIndex := rx.getStr("lastIndex").ToInteger() @@ -368,7 +368,7 @@ func (r *Runtime) checkStdRegexp(rxObj *Object) *regexpObject { func (r *Runtime) regexpproto_stdMatcher(call FunctionCall) Value { thisObj := r.toObject(call.This) - s := call.Argument(0).ToString() + s := call.Argument(0).toString() rx := r.checkStdRegexp(thisObj) if rx == nil { return r.regexpproto_stdMatcherGeneric(thisObj, s) @@ -421,7 +421,7 @@ func (r *Runtime) regexpproto_stdSearchGeneric(rxObj *Object, arg valueString) V func (r *Runtime) regexpproto_stdSearch(call FunctionCall) Value { thisObj := r.toObject(call.This) - s := call.Argument(0).ToString() + s := call.Argument(0).toString() rx := r.checkStdRegexp(thisObj) if rx == nil { return r.regexpproto_stdSearchGeneric(thisObj, s) @@ -496,7 +496,7 @@ func (r *Runtime) regexpproto_stdSplitterGeneric(splitter *Object, s valueString func (r *Runtime) regexpproto_stdSplitter(call FunctionCall) Value { rxObj := r.toObject(call.This) c := r.speciesConstructor(rxObj, r.global.RegExp) - flags := nilSafe(rxObj.self.getStr("flags")).ToString() + flags := nilSafe(rxObj.self.getStr("flags")).toString() // Add 'y' flag if missing if flagsStr := flags.String(); !strings.Contains(flagsStr, "y") { @@ -504,7 +504,7 @@ func (r *Runtime) regexpproto_stdSplitter(call FunctionCall) Value { } splitter := c([]Value{rxObj, flags}) - s := call.Argument(0).ToString() + s := call.Argument(0).toString() limitValue := call.Argument(1) search := r.checkStdRegexp(splitter) if search == nil { diff --git a/builtin_set.go b/builtin_set.go index 460a1916..e4ffbbf3 100644 --- a/builtin_set.go +++ b/builtin_set.go @@ -134,7 +134,7 @@ func (r *Runtime) builtin_newSet(args []Value) *Object { if len(args) > 0 { if arg := args[0]; arg != nil && arg != _undefined && arg != _null { adder := so.getStr("add") - iter := r.getIterator(arg.ToObject(r), nil) + iter := r.getIterator(arg, nil) if adder == r.global.setAdder { r.iterate(iter, func(item Value) { so.m.set(item, nil) diff --git a/builtin_string.go b/builtin_string.go index 91e6ada4..305470f8 100644 --- a/builtin_string.go +++ b/builtin_string.go @@ -27,7 +27,7 @@ func toString(arg Value) valueString { if s, ok := arg.(*valueSymbol); ok { return newStringValue(s.descString()) } - return arg.ToString() + return arg.toString() } func (r *Runtime) builtin_String(call FunctionCall) Value { @@ -123,7 +123,7 @@ func (r *Runtime) string_fromcharcode(call FunctionCall) Value { func (r *Runtime) stringproto_charAt(call FunctionCall) Value { r.checkObjectCoercible(call.This) - s := call.This.ToString() + s := call.This.toString() pos := call.Argument(0).ToInteger() if pos < 0 || pos >= s.length() { return stringEmpty @@ -133,7 +133,7 @@ func (r *Runtime) stringproto_charAt(call FunctionCall) Value { func (r *Runtime) stringproto_charCodeAt(call FunctionCall) Value { r.checkObjectCoercible(call.This) - s := call.This.ToString() + s := call.This.toString() pos := call.Argument(0).ToInteger() if pos < 0 || pos >= s.length() { return _NaN @@ -144,11 +144,11 @@ func (r *Runtime) stringproto_charCodeAt(call FunctionCall) Value { func (r *Runtime) stringproto_concat(call FunctionCall) Value { r.checkObjectCoercible(call.This) strs := make([]valueString, len(call.Arguments)+1) - strs[0] = call.This.ToString() + strs[0] = call.This.toString() _, allAscii := strs[0].(asciiString) totalLen := strs[0].length() for i, arg := range call.Arguments { - s := arg.ToString() + s := arg.toString() if allAscii { _, allAscii = s.(asciiString) } @@ -183,8 +183,8 @@ func (r *Runtime) stringproto_concat(call FunctionCall) Value { func (r *Runtime) stringproto_indexOf(call FunctionCall) Value { r.checkObjectCoercible(call.This) - value := call.This.ToString() - target := call.Argument(0).ToString() + value := call.This.toString() + target := call.Argument(0).toString() pos := call.Argument(1).ToInteger() if pos < 0 { @@ -201,8 +201,8 @@ func (r *Runtime) stringproto_indexOf(call FunctionCall) Value { func (r *Runtime) stringproto_lastIndexOf(call FunctionCall) Value { r.checkObjectCoercible(call.This) - value := call.This.ToString() - target := call.Argument(0).ToString() + value := call.This.toString() + target := call.Argument(0).toString() numPos := call.Argument(1).ToNumber() var pos int64 @@ -234,7 +234,7 @@ func (r *Runtime) stringproto_match(call FunctionCall) Value { r.checkObjectCoercible(call.This) regexp := call.Argument(0) if regexp != _undefined && regexp != _null { - if matcher := toMethod(regexp.ToObject(r).self.get(symMatch)); matcher != nil { + if matcher := toMethod(r.getV(regexp, symMatch)); matcher != nil { return matcher(FunctionCall{ This: regexp, Arguments: []Value{call.This}, @@ -254,7 +254,7 @@ func (r *Runtime) stringproto_match(call FunctionCall) Value { if matcher, ok := r.toObject(rx.getSym(symMatch)).self.assertCallable(); ok { return matcher(FunctionCall{ This: rx.val, - Arguments: []Value{call.This.ToString()}, + Arguments: []Value{call.This.toString()}, }) } @@ -266,7 +266,7 @@ func (r *Runtime) stringproto_replace(call FunctionCall) Value { searchValue := call.Argument(0) replaceValue := call.Argument(1) if searchValue != _undefined && searchValue != _null { - if replacer := toMethod(searchValue.ToObject(r).self.get(symReplace)); replacer != nil { + if replacer := toMethod(r.getV(searchValue, symReplace)); replacer != nil { return replacer(FunctionCall{ This: searchValue, Arguments: []Value{call.This, replaceValue}, @@ -274,7 +274,7 @@ func (r *Runtime) stringproto_replace(call FunctionCall) Value { } } - s := call.This.ToString() + s := call.This.toString() var str string var isASCII bool if astr, ok := s.(asciiString); ok { @@ -417,7 +417,7 @@ func (r *Runtime) stringproto_search(call FunctionCall) Value { r.checkObjectCoercible(call.This) regexp := call.Argument(0) if regexp != _undefined && regexp != _null { - if searcher := toMethod(regexp.ToObject(r).self.get(symSearch)); searcher != nil { + if searcher := toMethod(r.getV(regexp, symSearch)); searcher != nil { return searcher(FunctionCall{ This: regexp, Arguments: []Value{call.This}, @@ -437,7 +437,7 @@ func (r *Runtime) stringproto_search(call FunctionCall) Value { if searcher, ok := r.toObject(rx.getSym(symSearch)).self.assertCallable(); ok { return searcher(FunctionCall{ This: rx.val, - Arguments: []Value{call.This.ToString()}, + Arguments: []Value{call.This.toString()}, }) } @@ -446,7 +446,7 @@ func (r *Runtime) stringproto_search(call FunctionCall) Value { func (r *Runtime) stringproto_slice(call FunctionCall) Value { r.checkObjectCoercible(call.This) - s := call.This.ToString() + s := call.This.toString() l := s.length() start := call.Argument(0).ToInteger() @@ -490,14 +490,14 @@ func (r *Runtime) stringproto_split(call FunctionCall) Value { separatorValue := call.Argument(0) limitValue := call.Argument(1) if separatorValue != _undefined && separatorValue != _null { - if splitter := toMethod(separatorValue.ToObject(r).self.get(symSplit)); splitter != nil { + if splitter := toMethod(r.getV(separatorValue, symSplit)); splitter != nil { return splitter(FunctionCall{ This: separatorValue, Arguments: []Value{call.This, limitValue}, }) } } - s := call.This.ToString() + s := call.This.toString() limit := -1 if limitValue != _undefined { @@ -541,7 +541,7 @@ func (r *Runtime) stringproto_split(call FunctionCall) Value { func (r *Runtime) stringproto_substring(call FunctionCall) Value { r.checkObjectCoercible(call.This) - s := call.This.ToString() + s := call.This.toString() l := s.length() intStart := call.Argument(0).ToInteger() @@ -572,27 +572,27 @@ func (r *Runtime) stringproto_substring(call FunctionCall) Value { func (r *Runtime) stringproto_toLowerCase(call FunctionCall) Value { r.checkObjectCoercible(call.This) - s := call.This.ToString() + s := call.This.toString() return s.toLower() } func (r *Runtime) stringproto_toUpperCase(call FunctionCall) Value { r.checkObjectCoercible(call.This) - s := call.This.ToString() + s := call.This.toString() return s.toUpper() } func (r *Runtime) stringproto_trim(call FunctionCall) Value { r.checkObjectCoercible(call.This) - s := call.This.ToString() + s := call.This.toString() return newStringValue(strings.Trim(s.String(), parser.WhitespaceChars)) } func (r *Runtime) stringproto_substr(call FunctionCall) Value { - s := call.This.ToString() + s := call.This.toString() start := call.Argument(0).ToInteger() var length int64 sl := int64(s.length()) diff --git a/builtin_symbol.go b/builtin_symbol.go index 25ac0d31..599078d1 100644 --- a/builtin_symbol.go +++ b/builtin_symbol.go @@ -17,7 +17,7 @@ var ( func (r *Runtime) builtin_symbol(call FunctionCall) Value { desc := "" if arg := call.Argument(0); !IsUndefined(arg) { - desc = arg.ToString().String() + desc = arg.toString().String() } return &valueSymbol{ desc: desc, @@ -59,7 +59,7 @@ func (r *Runtime) symbolproto_valueOf(call FunctionCall) Value { } func (r *Runtime) symbol_for(call FunctionCall) Value { - key := call.Argument(0).ToString().String() + key := call.Argument(0).toString().String() if v := r.symbolRegistry[key]; v != nil { return v } diff --git a/builtin_weakmap.go b/builtin_weakmap.go index a8736a5a..be0f36e9 100644 --- a/builtin_weakmap.go +++ b/builtin_weakmap.go @@ -146,7 +146,7 @@ func (r *Runtime) builtin_newWeakMap(args []Value) *Object { if len(args) > 0 { if arg := args[0]; arg != nil && arg != _undefined && arg != _null { adder := wmo.getStr("set") - iter := r.getIterator(arg.ToObject(r), nil) + iter := r.getIterator(arg, nil) i0 := intToValue(0) i1 := intToValue(1) if adder == r.global.weakMapAdder { diff --git a/builtin_weakset.go b/builtin_weakset.go index 53ceabc8..b905632a 100644 --- a/builtin_weakset.go +++ b/builtin_weakset.go @@ -107,7 +107,7 @@ func (r *Runtime) populateWeakSetGeneric(s *Object, adderValue Value, iterable V if adder == nil { panic(r.NewTypeError("WeakSet.add is not set")) } - iter := r.getIterator(iterable.ToObject(r), nil) + iter := r.getIterator(iterable, nil) r.iterate(iter, func(val Value) { adder(FunctionCall{This: s, Arguments: []Value{val}}) }) diff --git a/object.go b/object.go index 752e94d8..73de00f0 100644 --- a/object.go +++ b/object.go @@ -125,10 +125,11 @@ type objectImpl interface { deleteStr(name string, throw bool) bool delete(name Value, throw bool) bool proto() *Object + setProto(proto *Object) *Object hasInstance(v Value) bool isExtensible() bool preventExtensions() - enumerate(all, recusrive bool) iterNextFunc + enumerate(all, recursive bool) iterNextFunc _enumerate(recursive bool) iterNextFunc export() interface{} exportType() reflect.Type @@ -269,7 +270,7 @@ func (o *baseObject) get(n Value) Value { func (o *baseObject) checkDeleteProp(name string, prop *valueProperty, throw bool) bool { if !prop.configurable { - o.val.runtime.typeErrorResult(throw, "Cannot delete property '%s' of %s", name, o.val.ToString()) + o.val.runtime.typeErrorResult(throw, "Cannot delete property '%s' of %s", name, o.val.toString()) return false } return true @@ -344,6 +345,24 @@ func (o *baseObject) getOwnProp(name Value) Value { return o.val.self.getOwnPropStr(name.String()) } +func (o *baseObject) setProto(proto *Object) *Object { + current := o.prototype + if current.SameAs(proto) { + return nil + } + if !o.extensible { + return o.val.runtime.NewTypeError("%s is not extensible", o.val) + } + for p := proto; p != nil; { + if p.SameAs(o.val) { + return o.val.runtime.NewTypeError("Cyclic __proto__ value") + } + p = p.self.proto() + } + o.prototype = proto + return nil +} + func (o *baseObject) putStr(name string, val Value, throw bool) { if v, exists := o.values[name]; exists { if prop, ok := v.(*valueProperty); ok { @@ -359,18 +378,17 @@ func (o *baseObject) putStr(name string, val Value, throw bool) { } if name == __proto__ { - if !o.extensible { - o.val.runtime.typeErrorResult(throw, "%s is not extensible", o.val) - return - } - if val == _undefined || val == _null { - o.prototype = nil - return - } else { - if val, ok := val.(*Object); ok { - o.prototype = val + var proto *Object + if val != _null { + if obj, ok := val.(*Object); ok { + proto = obj + } else { + return } } + if ex := o.setProto(proto); ex != nil { + panic(ex) + } return } @@ -556,12 +574,13 @@ func (o *baseObject) _defineOwnProperty(name, existingValue Value, descr propert return existing, true Reject: - o.val.runtime.typeErrorResult(throw, "Cannot redefine property: %s", name.ToString()) + o.val.runtime.typeErrorResult(throw, "Cannot redefine property: %s", name.toString()) return nil, false } func (o *baseObject) defineOwnProperty(n Value, descr propertyDescr, throw bool) bool { + n = toPropertyKey(n) if s, ok := n.(*valueSymbol); ok { existingVal := o.symValues[s] if v, ok := o._defineOwnProperty(n, existingVal, descr, throw); ok { @@ -832,7 +851,7 @@ func (o *baseObject) getOwnSymbols() (res []Value) { } func (o *baseObject) hasInstance(Value) bool { - panic(o.val.runtime.NewTypeError("Expecting a function in instanceof check, but got %s", o.val.ToString())) + panic(o.val.runtime.NewTypeError("Expecting a function in instanceof check, but got %s", o.val.toString())) } func toMethod(v Value) func(FunctionCall) Value { diff --git a/object_goreflect.go b/object_goreflect.go index ff2ad169..0276425a 100644 --- a/object_goreflect.go +++ b/object_goreflect.go @@ -322,7 +322,7 @@ func (o *objectGoReflect) toPrimitiveNumber() Value { func (o *objectGoReflect) toPrimitiveString() Value { if v := o._toNumber(); v != nil { - return v.ToString() + return v.toString() } return o._toString() } diff --git a/object_lazy.go b/object_lazy.go index 4d4fa0ab..e86b7a66 100644 --- a/object_lazy.go +++ b/object_lazy.go @@ -193,6 +193,12 @@ func (o *lazyObject) getOwnSymbols() []Value { return obj.getOwnSymbols() } +func (o *lazyObject) setProto(proto *Object) *Object { + obj := o.create(o.val) + o.val.self = obj + return obj.setProto(proto) +} + func (o *lazyObject) sortLen() int64 { obj := o.create(o.val) o.val.self = obj diff --git a/object_test.go b/object_test.go index d5e6d69b..6231fc57 100644 --- a/object_test.go +++ b/object_test.go @@ -153,7 +153,7 @@ func BenchmarkToString1(b *testing.B) { v := asciiString("test") for i := 0; i < b.N; i++ { - v.ToString() + v.toString() } } diff --git a/runtime.go b/runtime.go index a6a65232..b70c970e 100644 --- a/runtime.go +++ b/runtime.go @@ -600,9 +600,9 @@ func (r *Runtime) error_toString(call FunctionCall) Value { return newStringValue(fmt.Sprintf("%s: %s", name.String(), msgStr)) } else { if nameStr != "" { - return name.ToString() + return name.toString() } else { - return msg.ToString() + return msg.toString() } } } @@ -707,7 +707,7 @@ func (r *Runtime) toCallable(v Value) func(FunctionCall) Value { if call, ok := r.toObject(v).self.assertCallable(); ok { return call } - r.typeErrorResult(true, "Value is not callable: %s", v.ToString()) + r.typeErrorResult(true, "Value is not callable: %s", v.toString()) return nil } @@ -1292,11 +1292,11 @@ func (r *Runtime) toReflectValue(v Value, typ reflect.Type) (reflect.Value, erro } if typ == typeTime && et.Kind() == reflect.String { - time, ok := dateParse(v.String()) + tme, ok := dateParse(v.String()) if !ok { return reflect.Value{}, fmt.Errorf("Could not convert string %v to %v", v, typ) } - return reflect.ValueOf(time), nil + return reflect.ValueOf(tme), nil } switch typ.Kind() { @@ -1624,9 +1624,31 @@ func (r *Runtime) returnThis(call FunctionCall) Value { return call.This } -func (r *Runtime) getIterator(obj *Object, method func(FunctionCall) Value) *Object { +func toPropertyKey(key Value) Value { + return key.ToPrimitiveString() +} + +func (r *Runtime) getVStr(v Value, p string) Value { + o := v.ToObject(r) + prop := o.self.getPropStr(p) + if prop, ok := prop.(*valueProperty); ok { + return prop.get(v) + } + return prop +} + +func (r *Runtime) getV(v Value, p Value) Value { + o := v.ToObject(r) + prop := o.self.getProp(p) + if prop, ok := prop.(*valueProperty); ok { + return prop.get(v) + } + return prop +} + +func (r *Runtime) getIterator(obj Value, method func(FunctionCall) Value) *Object { if method == nil { - method = toMethod(obj.self.get(symIterator)) + method = toMethod(r.getV(obj, symIterator)) if method == nil { panic(r.NewTypeError("object is not iterable")) } diff --git a/runtime_test.go b/runtime_test.go index df07d50c..ad9be42e 100644 --- a/runtime_test.go +++ b/runtime_test.go @@ -1348,6 +1348,76 @@ func TestFreezeSymbol(t *testing.T) { testScript1(SCRIPT, valueTrue, t) } +func TestToPropertyKey(t *testing.T) { + const SCRIPT = ` + var sym = Symbol(42); + var callCount = 0; + + var wrapper = { + toString: function() { + callCount += 1; + return sym; + }, + valueOf: function() { + $ERROR("valueOf() called"); + } + }; + + var o = {}; + o[wrapper] = function() { return "test" }; + assert.sameValue(o[wrapper], o[sym], "o[wrapper] === o[sym]"); + assert.sameValue(o[wrapper](), "test", "o[wrapper]()"); + assert.sameValue(o[sym](), "test", "o[sym]()"); + ` + + testScript1(TESTLIB+SCRIPT, _undefined, t) +} + +func TestPrimThisValue(t *testing.T) { + const SCRIPT = ` + function t() { + 'use strict'; + + Boolean.prototype.toString = function() { + return typeof this; + }; + + assert.sameValue(true.toLocaleString(), "boolean"); + + Boolean.prototype[Symbol.iterator] = function() { + return [typeof this][Symbol.iterator](); + } + var s = new Set(true); + assert.sameValue(s.size, 1, "size"); + assert.sameValue(s.has("boolean"), true, "s.has('boolean')"); + } + t(); + ` + + testScript1(TESTLIB+SCRIPT, _undefined, t) +} + +func TestPrimThisValueGetter(t *testing.T) { + const SCRIPT = ` + function t() { + 'use strict'; + Object.defineProperty(Boolean.prototype, "toString", { + get: function() { + var v = typeof this; + return function() { + return v; + }; + } + }); + + assert.sameValue(true.toLocaleString(), "boolean"); + } + t(); + ` + + testScript1(TESTLIB+SCRIPT, _undefined, t) +} + /* func TestArrayConcatSparse(t *testing.T) { function foo(a,b,c) diff --git a/string_ascii.go b/string_ascii.go index dede99c0..16270a8a 100644 --- a/string_ascii.go +++ b/string_ascii.go @@ -104,7 +104,11 @@ func (s asciiString) ToInteger() int64 { return i } -func (s asciiString) ToString() valueString { +func (s asciiString) toString() valueString { + return s +} + +func (s asciiString) ToPrimitiveString() Value { return s } diff --git a/string_unicode.go b/string_unicode.go index 58434126..06b1912a 100644 --- a/string_unicode.go +++ b/string_unicode.go @@ -81,7 +81,11 @@ func (s unicodeString) ToInteger() int64 { return 0 } -func (s unicodeString) ToString() valueString { +func (s unicodeString) toString() valueString { + return s +} + +func (s unicodeString) ToPrimitiveString() Value { return s } diff --git a/tc39_test.go b/tc39_test.go index c1642363..21fceafc 100644 --- a/tc39_test.go +++ b/tc39_test.go @@ -50,6 +50,7 @@ var ( "test/built-ins/WeakMap/proto-from-ctor-realm.js": true, "test/built-ins/Map/proto-from-ctor-realm.js": true, "test/built-ins/Set/proto-from-ctor-realm.js": true, + "test/built-ins/Object/proto-from-ctor.js": true, // class "test/language/statements/class/subclass/builtin-objects/Symbol/symbol-valid-as-extends-value.js": true, @@ -62,12 +63,18 @@ var ( "test/language/statements/class/subclass/builtin-objects/Map/regular-subclassing.js": true, "test/language/statements/class/subclass/builtin-objects/Set/super-must-be-called.js": true, "test/language/statements/class/subclass/builtin-objects/Set/regular-subclassing.js": true, + "test/language/statements/class/subclass/builtin-objects/Object/replacing-prototype.js": true, + "test/language/statements/class/subclass/builtin-objects/Object/regular-subclassing.js": true, // Proxy - "test/built-ins/Object/prototype/toString/proxy-revoked.js": true, - "test/built-ins/Object/prototype/toString/proxy-function.js": true, - "test/built-ins/Object/prototype/toString/proxy-array.js": true, - "test/built-ins/JSON/stringify/value-proxy.js": true, + "test/built-ins/Object/prototype/toString/proxy-revoked.js": true, + "test/built-ins/Object/prototype/toString/proxy-function.js": true, + "test/built-ins/Object/prototype/toString/proxy-array.js": true, + "test/built-ins/JSON/stringify/value-proxy.js": true, + "test/built-ins/Object/setPrototypeOf/set-error.js": true, + "test/built-ins/Object/assign/source-own-prop-keys-error.js": true, + "test/built-ins/Object/assign/source-own-prop-error.js": true, + "test/built-ins/Object/assign/source-own-prop-desc-missing.js": true, // Arrow functions "test/built-ins/Set/prototype/forEach/this-arg-explicit-cannot-override-lexical-this-arrow.js": true, @@ -84,9 +91,7 @@ var ( es6IdWhiteList = []string{ "12.9.3", "12.9.4", - "19.1.2.8", - "19.1.2.5", - "19.1.3.6", + "19.1", "19.4", "21.1.3.14", "21.1.3.15", diff --git a/value.go b/value.go index ba3bf0e6..44ba6ae8 100644 --- a/value.go +++ b/value.go @@ -41,7 +41,8 @@ var ( type Value interface { ToInteger() int64 - ToString() valueString + toString() valueString + ToPrimitiveString() Value String() string ToFloat() float64 ToNumber() Value @@ -103,7 +104,7 @@ func propGetter(o Value, v Value, r *Runtime) *Object { return obj } } - r.typeErrorResult(true, "Getter must be a function: %s", v.ToString()) + r.typeErrorResult(true, "Getter must be a function: %s", v.toString()) return nil } @@ -116,7 +117,7 @@ func propSetter(o Value, v Value, r *Runtime) *Object { return obj } } - r.typeErrorResult(true, "Setter must be a function: %s", v.ToString()) + r.typeErrorResult(true, "Setter must be a function: %s", v.toString()) return nil } @@ -124,10 +125,14 @@ func (i valueInt) ToInteger() int64 { return int64(i) } -func (i valueInt) ToString() valueString { +func (i valueInt) toString() valueString { return asciiString(i.String()) } +func (i valueInt) ToPrimitiveString() Value { + return i +} + func (i valueInt) String() string { return strconv.FormatInt(int64(i), 10) } @@ -218,13 +223,17 @@ func (o valueBool) ToInteger() int64 { return 0 } -func (o valueBool) ToString() valueString { +func (o valueBool) toString() valueString { if o { return stringTrue } return stringFalse } +func (o valueBool) ToPrimitiveString() Value { + return o +} + func (o valueBool) String() string { if o { return "true" @@ -316,18 +325,26 @@ func (n valueNull) ToInteger() int64 { return 0 } -func (n valueNull) ToString() valueString { +func (n valueNull) toString() valueString { return stringNull } +func (n valueNull) ToPrimitiveString() Value { + return n +} + func (n valueNull) String() string { return "null" } -func (u valueUndefined) ToString() valueString { +func (u valueUndefined) toString() valueString { return stringUndefined } +func (u valueUndefined) ToPrimitiveString() Value { + return u +} + func (u valueUndefined) String() string { return "undefined" } @@ -402,7 +419,7 @@ func (n valueNull) assertString() (valueString, bool) { return nil, false } -func (n valueNull) baseObject(r *Runtime) *Object { +func (n valueNull) baseObject(*Runtime) *Object { return nil } @@ -422,10 +439,14 @@ func (p *valueProperty) ToInteger() int64 { return 0 } -func (p *valueProperty) ToString() valueString { +func (p *valueProperty) toString() valueString { return stringEmpty } +func (p *valueProperty) ToPrimitiveString() Value { + return _undefined +} + func (p *valueProperty) String() string { return "" } @@ -438,7 +459,7 @@ func (p *valueProperty) ToBoolean() bool { return false } -func (p *valueProperty) ToObject(r *Runtime) *Object { +func (p *valueProperty) ToObject(*Runtime) *Object { return nil } @@ -494,11 +515,11 @@ func (p *valueProperty) SameAs(other Value) bool { return false } -func (p *valueProperty) Equals(other Value) bool { +func (p *valueProperty) Equals(Value) bool { return false } -func (p *valueProperty) StrictEquals(other Value) bool { +func (p *valueProperty) StrictEquals(Value) bool { return false } @@ -531,10 +552,14 @@ func (f valueFloat) ToInteger() int64 { return int64(f) } -func (f valueFloat) ToString() valueString { +func (f valueFloat) toString() valueString { return asciiString(f.String()) } +func (f valueFloat) ToPrimitiveString() Value { + return f +} + var matchLeading0Exponent = regexp.MustCompile(`([eE][+\-])0+([1-9])`) // 1e-07 => 1e-7 func (f valueFloat) String() string { @@ -663,8 +688,12 @@ func (o *Object) ToInteger() int64 { return o.self.toPrimitiveNumber().ToNumber().ToInteger() } -func (o *Object) ToString() valueString { - return o.self.toPrimitiveString().ToString() +func (o *Object) toString() valueString { + return o.self.toPrimitiveString().toString() +} + +func (o *Object) ToPrimitiveString() Value { + return o.self.toPrimitiveString().ToPrimitiveString() } func (o *Object) String() string { @@ -679,7 +708,7 @@ func (o *Object) ToBoolean() bool { return true } -func (o *Object) ToObject(r *Runtime) *Object { +func (o *Object) ToObject(*Runtime) *Object { return o } @@ -736,7 +765,7 @@ func (o *Object) assertString() (valueString, bool) { return nil, false } -func (o *Object) baseObject(r *Runtime) *Object { +func (o *Object) baseObject(*Runtime) *Object { return o } @@ -827,7 +856,12 @@ func (o valueUnresolved) ToInteger() int64 { return 0 } -func (o valueUnresolved) ToString() valueString { +func (o valueUnresolved) toString() valueString { + o.throw() + return nil +} + +func (o valueUnresolved) ToPrimitiveString() Value { o.throw() return nil } @@ -847,7 +881,7 @@ func (o valueUnresolved) ToBoolean() bool { return false } -func (o valueUnresolved) ToObject(r *Runtime) *Object { +func (o valueUnresolved) ToObject(*Runtime) *Object { o.throw() return nil } @@ -857,17 +891,17 @@ func (o valueUnresolved) ToNumber() Value { return nil } -func (o valueUnresolved) SameAs(other Value) bool { +func (o valueUnresolved) SameAs(Value) bool { o.throw() return false } -func (o valueUnresolved) Equals(other Value) bool { +func (o valueUnresolved) Equals(Value) bool { o.throw() return false } -func (o valueUnresolved) StrictEquals(other Value) bool { +func (o valueUnresolved) StrictEquals(Value) bool { o.throw() return false } @@ -887,7 +921,7 @@ func (o valueUnresolved) assertString() (valueString, bool) { return nil, false } -func (o valueUnresolved) baseObject(r *Runtime) *Object { +func (o valueUnresolved) baseObject(*Runtime) *Object { o.throw() return nil } @@ -911,10 +945,14 @@ func (s *valueSymbol) ToInteger() int64 { panic(typeError("Cannot convert a Symbol value to a number")) } -func (s *valueSymbol) ToString() valueString { +func (s *valueSymbol) toString() valueString { panic(typeError("Cannot convert a Symbol value to a string")) } +func (s *valueSymbol) ToPrimitiveString() Value { + return s +} + func (s *valueSymbol) String() string { return s.descString() } diff --git a/vm.go b/vm.go index e7693727..3eea5d93 100644 --- a/vm.go +++ b/vm.go @@ -463,7 +463,7 @@ func (vm *vm) toCallee(v Value) *Object { case memberUnresolved: panic(vm.r.NewTypeError("Object has no member '%s'", unresolved.ref)) } - panic(vm.r.NewTypeError("Value is not an object: %s", v.ToString())) + panic(vm.r.NewTypeError("Value is not an object: %s", v.toString())) } type _newStash struct{} @@ -600,10 +600,10 @@ func (_add) exec(vm *vm) { if isLeftString || isRightString { if !isLeftString { - leftString = left.ToString() + leftString = left.toString() } if !isRightString { - rightString = right.ToString() + rightString = right.toString() } ret = leftString.concat(rightString) } else { @@ -950,7 +950,7 @@ var setElem _setElem func (_setElem) exec(vm *vm) { obj := vm.stack[vm.sp-3].ToObject(vm.r) - propName := vm.stack[vm.sp-2] + propName := toPropertyKey(vm.stack[vm.sp-2]) val := vm.stack[vm.sp-1] obj.self.put(propName, val, false) @@ -966,7 +966,7 @@ var setElemStrict _setElemStrict func (_setElemStrict) exec(vm *vm) { obj := vm.r.toObject(vm.stack[vm.sp-3]) - propName := vm.stack[vm.sp-2] + propName := toPropertyKey(vm.stack[vm.sp-2]) val := vm.stack[vm.sp-1] obj.self.put(propName, val, true) @@ -982,7 +982,7 @@ var deleteElem _deleteElem func (_deleteElem) exec(vm *vm) { obj := vm.r.toObject(vm.stack[vm.sp-2]) - propName := vm.stack[vm.sp-1] + propName := toPropertyKey(vm.stack[vm.sp-1]) if !obj.self.hasProperty(propName) || obj.self.delete(propName, false) { vm.stack[vm.sp-2] = valueTrue } else { @@ -998,7 +998,7 @@ var deleteElemStrict _deleteElemStrict func (_deleteElemStrict) exec(vm *vm) { obj := vm.r.toObject(vm.stack[vm.sp-2]) - propName := vm.stack[vm.sp-1] + propName := toPropertyKey(vm.stack[vm.sp-1]) obj.self.delete(propName, true) vm.stack[vm.sp-2] = valueTrue vm.sp-- @@ -1154,7 +1154,7 @@ var getElem _getElem func (_getElem) exec(vm *vm) { v := vm.stack[vm.sp-2] obj := v.baseObject(vm.r) - propName := vm.stack[vm.sp-1] + propName := toPropertyKey(vm.stack[vm.sp-1]) if obj == nil { panic(vm.r.NewTypeError("Cannot read property '%s' of undefined", propName.String())) } @@ -1180,10 +1180,9 @@ var getElemCallee _getElemCallee func (_getElemCallee) exec(vm *vm) { v := vm.stack[vm.sp-2] obj := v.baseObject(vm.r) - propName := vm.stack[vm.sp-1] + propName := toPropertyKey(vm.stack[vm.sp-1]) if obj == nil { - vm.r.typeErrorResult(true, "Cannot read property '%s' of undefined", propName.String()) - panic("Unreachable") + panic(vm.r.NewTypeError("Cannot read property '%s' of undefined", propName.String())) } prop := obj.self.getProp(propName) @@ -1735,7 +1734,7 @@ repeat: obj.self = f.create(obj) goto repeat default: - vm.r.typeErrorResult(true, "Not a function: %s", obj.ToString()) + vm.r.typeErrorResult(true, "Not a function: %s", obj.toString()) } } diff --git a/vm_test.go b/vm_test.go index 24a8ba7f..91bdfad3 100644 --- a/vm_test.go +++ b/vm_test.go @@ -91,10 +91,10 @@ func f_add(vm *vm) { if isLeftString || isRightString { if !isLeftString { - leftString = left.ToString() + leftString = left.toString() } if !isRightString { - rightString = right.ToString() + rightString = right.toString() } ret = leftString.concat(rightString) } else { From 15eea636ca1d567fafc27f0b352f6b01f2f78c9b Mon Sep 17 00:00:00 2001 From: Dmitry Panov Date: Wed, 4 Mar 2020 13:05:42 +0000 Subject: [PATCH 05/46] Array.from() --- builtin_array.go | 87 ++++++++++++++++++++++++++++++++ builtin_arrray_test.go | 100 ++++++++++++++++++++++++++++++++++++ builtin_weakset.go | 18 +++---- runtime_test.go | 112 ++++++----------------------------------- tc39_test.go | 9 ++++ 5 files changed, 217 insertions(+), 109 deletions(-) create mode 100644 builtin_arrray_test.go diff --git a/builtin_array.go b/builtin_array.go index e46e90c1..dbea68ba 100644 --- a/builtin_array.go +++ b/builtin_array.go @@ -828,6 +828,92 @@ func (r *Runtime) arrayproto_values(call FunctionCall) Value { return r.createArrayIterator(call.This.ToObject(r), iterationKindValue) } +func (r *Runtime) checkStdArrayIter(v Value) *arrayObject { + if obj, ok := v.(*Object); ok { + if arr, ok := obj.self.(*arrayObject); ok && + arr.propValueCount == 0 && + arr.length == int64(len(arr.values)) && + arr.getSym(symIterator) == r.global.arrayValues { + + return arr + } + } + + return nil +} + +func (r *Runtime) array_from(call FunctionCall) Value { + var mapFn func(FunctionCall) Value + if mapFnArg := call.Argument(1); mapFnArg != _undefined { + if mapFnObj, ok := mapFnArg.(*Object); ok { + if fn, ok := mapFnObj.self.assertCallable(); ok { + mapFn = fn + } + } + if mapFn == nil { + panic(r.NewTypeError("%s is not a function", mapFnArg)) + } + } + t := call.Argument(2) + items := call.Argument(0) + if mapFn == nil && call.This == r.global.Array { // mapFn may mutate the array + if arr := r.checkStdArrayIter(items); arr != nil { + items := make([]Value, len(arr.values)) + for i, item := range arr.values { + if item == nil { + item = nilSafe(arr.get(intToValue(int64(i)))) + } + items[i] = item + } + return r.newArrayValues(items) + } + } + + var ctor func(args []Value) *Object + if o, ok := call.This.(*Object); ok { + if c := getConstructor(o); c != nil { + ctor = c + } + } + var arr *Object + if usingIterator := toMethod(r.getV(items, symIterator)); usingIterator != nil { + if ctor != nil { + arr = ctor([]Value{}) + } else { + arr = r.newArrayValues(nil) + } + iter := r.getIterator(items, usingIterator) + k := int64(0) + r.iterate(iter, func(val Value) { + if mapFn != nil { + val = mapFn(FunctionCall{This: t, Arguments: []Value{val, intToValue(k)}}) + } + arr.self.put(intToValue(k), val, true) + k++ + }) + arr.self.putStr("length", intToValue(k), true) + } else { + arrayLike := items.ToObject(r) + l := toLength(arrayLike.self.getStr("length")) + if ctor != nil { + arr = ctor([]Value{intToValue(l)}) + } else { + arr = r.newArrayLength(l) + } + for k := int64(0); k < l; k++ { + idx := intToValue(k) + item := arrayLike.self.get(idx) + if mapFn != nil { + item = mapFn(FunctionCall{This: t, Arguments: []Value{item, idx}}) + } + arr.self.put(idx, item, true) + } + arr.self.putStr("length", intToValue(l), true) + } + + return arr +} + func (r *Runtime) array_isArray(call FunctionCall) Value { if o, ok := call.Argument(0).(*Object); ok { if isArray(o) { @@ -888,6 +974,7 @@ func (r *Runtime) createArrayProto(val *Object) objectImpl { func (r *Runtime) createArray(val *Object) objectImpl { o := r.newNativeFuncConstructObj(val, r.builtin_newArray, "Array", r.global.ArrayPrototype, 1) + o._putProp("from", r.newNativeFunc(r.array_from, nil, "from", nil, 1), true, false, true) o._putProp("isArray", r.newNativeFunc(r.array_isArray, nil, "isArray", nil, 1), true, false, true) o.putSym(symSpecies, &valueProperty{ getterFunc: r.newNativeFunc(r.returnThis, nil, "get [Symbol.species]", nil, 0), diff --git a/builtin_arrray_test.go b/builtin_arrray_test.go new file mode 100644 index 00000000..a1f78639 --- /dev/null +++ b/builtin_arrray_test.go @@ -0,0 +1,100 @@ +package goja + +import "testing" + +func TestArrayProtoProp(t *testing.T) { + const SCRIPT = ` + Object.defineProperty(Array.prototype, '0', {value: 42, configurable: true, writable: false}) + var a = [] + a[0] = 1 + a[0] + ` + + testScript1(SCRIPT, valueInt(42), t) +} + +func TestArrayDelete(t *testing.T) { + const SCRIPT = ` + var a = [1, 2]; + var deleted = delete a[0]; + var undef = a[0] === undefined; + var len = a.length; + + deleted && undef && len === 2; + ` + + testScript1(SCRIPT, valueTrue, t) +} + +func TestArrayDeleteNonexisting(t *testing.T) { + const SCRIPT = ` + Array.prototype[0] = 42; + var a = []; + delete a[0] && a[0] === 42; + ` + + testScript1(SCRIPT, valueTrue, t) +} + +func TestArraySetLength(t *testing.T) { + const SCRIPT = ` + var a = [1, 2]; + var assert0 = a.length == 2; + a.length = "1"; + a.length = 1.0; + a.length = 1; + var assert1 = a.length == 1; + a.length = 2; + var assert2 = a.length == 2; + assert0 && assert1 && assert2 && a[1] === undefined; + + ` + + testScript1(SCRIPT, valueTrue, t) +} + +func TestArrayReverseNonOptimisable(t *testing.T) { + const SCRIPT = ` + var a = []; + Object.defineProperty(a, "0", {get: function() {return 42}, set: function(v) {Object.defineProperty(a, "0", {value: v + 1, writable: true, configurable: true})}, configurable: true}) + a[1] = 43; + a.reverse(); + + a.length === 2 && a[0] === 44 && a[1] === 42; + ` + + testScript1(SCRIPT, valueTrue, t) +} + +func TestArrayPushNonOptimisable(t *testing.T) { + const SCRIPT = ` + Object.defineProperty(Object.prototype, "0", {value: 42}); + var a = []; + var thrown = false; + try { + a.push(1); + } catch (e) { + thrown = e instanceof TypeError; + } + thrown; + ` + + testScript1(SCRIPT, valueTrue, t) +} + +func TestArraySetLengthWithPropItems(t *testing.T) { + const SCRIPT = ` + var a = [1,2,3,4]; + var thrown = false; + + Object.defineProperty(a, "2", {value: 42, configurable: false, writable: false}); + try { + Object.defineProperty(a, "length", {value: 0, writable: false}); + } catch (e) { + thrown = e instanceof TypeError; + } + thrown && a.length === 3; + ` + + testScript1(SCRIPT, valueTrue, t) +} diff --git a/builtin_weakset.go b/builtin_weakset.go index b905632a..ae2f29c6 100644 --- a/builtin_weakset.go +++ b/builtin_weakset.go @@ -127,20 +127,14 @@ func (r *Runtime) builtin_newWeakSet(args []Value) *Object { if arg := args[0]; arg != nil && arg != _undefined && arg != _null { adder := wso.getStr("add") if adder == r.global.weakSetAdder { - if obj, ok := arg.(*Object); ok { - if arr, ok := obj.self.(*arrayObject); ok && - arr.propValueCount == 0 && - arr.length == int64(len(arr.values)) && - arr.getSym(symIterator) == r.global.arrayValues { - - for i, v := range arr.values { - if v == nil { - v = arr.get(intToValue(int64(i))) - } - wso.set.add(r.toObject(v)) + if arr := r.checkStdArrayIter(arg); arr != nil { + for i, v := range arr.values { + if v == nil { + v = arr.get(intToValue(int64(i))) } - return o + wso.set.add(r.toObject(v)) } + return o } } r.populateWeakSetGeneric(o, adder, arg) diff --git a/runtime_test.go b/runtime_test.go index ad9be42e..e7aaa35a 100644 --- a/runtime_test.go +++ b/runtime_test.go @@ -17,57 +17,6 @@ func TestGlobalObjectProto(t *testing.T) { testScript1(SCRIPT, valueTrue, t) } -func TestArrayProtoProp(t *testing.T) { - const SCRIPT = ` - Object.defineProperty(Array.prototype, '0', {value: 42, configurable: true, writable: false}) - var a = [] - a[0] = 1 - a[0] - ` - - testScript1(SCRIPT, valueInt(42), t) -} - -func TestArrayDelete(t *testing.T) { - const SCRIPT = ` - var a = [1, 2]; - var deleted = delete a[0]; - var undef = a[0] === undefined; - var len = a.length; - - deleted && undef && len === 2; - ` - - testScript1(SCRIPT, valueTrue, t) -} - -func TestArrayDeleteNonexisting(t *testing.T) { - const SCRIPT = ` - Array.prototype[0] = 42; - var a = []; - delete a[0] && a[0] === 42; - ` - - testScript1(SCRIPT, valueTrue, t) -} - -func TestArraySetLength(t *testing.T) { - const SCRIPT = ` - var a = [1, 2]; - var assert0 = a.length == 2; - a.length = "1"; - a.length = 1.0; - a.length = 1; - var assert1 = a.length == 1; - a.length = 2; - var assert2 = a.length == 2; - assert0 && assert1 && assert2 && a[1] === undefined; - - ` - - testScript1(SCRIPT, valueTrue, t) -} - func TestUnicodeString(t *testing.T) { const SCRIPT = ` var s = "ВСст"; @@ -78,52 +27,6 @@ func TestUnicodeString(t *testing.T) { testScript1(SCRIPT, valueTrue, t) } -func TestArrayReverseNonOptimisable(t *testing.T) { - const SCRIPT = ` - var a = []; - Object.defineProperty(a, "0", {get: function() {return 42}, set: function(v) {Object.defineProperty(a, "0", {value: v + 1, writable: true, configurable: true})}, configurable: true}) - a[1] = 43; - a.reverse(); - - a.length === 2 && a[0] === 44 && a[1] === 42; - ` - - testScript1(SCRIPT, valueTrue, t) -} - -func TestArrayPushNonOptimisable(t *testing.T) { - const SCRIPT = ` - Object.defineProperty(Object.prototype, "0", {value: 42}); - var a = []; - var thrown = false; - try { - a.push(1); - } catch (e) { - thrown = e instanceof TypeError; - } - thrown; - ` - - testScript1(SCRIPT, valueTrue, t) -} - -func TestArraySetLengthWithPropItems(t *testing.T) { - const SCRIPT = ` - var a = [1,2,3,4]; - var thrown = false; - - Object.defineProperty(a, "2", {value: 42, configurable: false, writable: false}); - try { - Object.defineProperty(a, "length", {value: 0, writable: false}); - } catch (e) { - thrown = e instanceof TypeError; - } - thrown && a.length === 3; - ` - - testScript1(SCRIPT, valueTrue, t) -} - func Test2TierHierarchyProp(t *testing.T) { const SCRIPT = ` var a = {}; @@ -1368,6 +1271,21 @@ func TestToPropertyKey(t *testing.T) { assert.sameValue(o[wrapper], o[sym], "o[wrapper] === o[sym]"); assert.sameValue(o[wrapper](), "test", "o[wrapper]()"); assert.sameValue(o[sym](), "test", "o[sym]()"); + + var wrapper1 = {}; + wrapper1[Symbol.toPrimitive] = function(hint) { + if (hint === "string" || hint === "default") { + return "1"; + } + if (hint === "number") { + return 2; + } + $ERROR("Unknown hint value "+hint); + }; + var a = []; + a[wrapper1] = 42; + assert.sameValue(a[1], 42, "a[1]"); + assert.sameValue(a[1], a[wrapper1], "a[1] === a[wrapper1]"); ` testScript1(TESTLIB+SCRIPT, _undefined, t) diff --git a/tc39_test.go b/tc39_test.go index 21fceafc..8be90e89 100644 --- a/tc39_test.go +++ b/tc39_test.go @@ -51,6 +51,7 @@ var ( "test/built-ins/Map/proto-from-ctor-realm.js": true, "test/built-ins/Set/proto-from-ctor-realm.js": true, "test/built-ins/Object/proto-from-ctor.js": true, + "test/built-ins/Array/from/proto-from-ctor-realm.js": true, // class "test/language/statements/class/subclass/builtin-objects/Symbol/symbol-valid-as-extends-value.js": true, @@ -84,6 +85,13 @@ var ( "test/built-ins/RegExp/prototype/Symbol.match/get-unicode-error.js": true, "test/built-ins/RegExp/prototype/Symbol.match/builtin-success-u-return-val-groups.js": true, "test/built-ins/RegExp/prototype/Symbol.match/builtin-infer-unicode.js": true, + + // object literals + "test/built-ins/Array/from/source-object-iterator-1.js": true, + "test/built-ins/Array/from/source-object-iterator-2.js": true, + + // Typed arrays + "test/built-ins/Array/from/items-is-arraybuffer.js": true, } es6WhiteList = map[string]bool{} @@ -97,6 +105,7 @@ var ( "21.1.3.15", "21.1.3.17", "21.2.5.6", + "22.1.2.1", "22.1.2.5", //"22.1.3.1", "22.1.3.29", From 7a45ca5b9384e051bcbce5a9c316410ba66a67cd Mon Sep 17 00:00:00 2001 From: Dmitry Panov Date: Wed, 4 Mar 2020 20:07:07 +0000 Subject: [PATCH 06/46] Array.from(), Array.of() --- array.go | 4 +- builtin_array.go | 69 ++++++++++++++++++++++++++--- builtin_arrray_test.go | 98 ++++++++++++++++++++++++++++++++++++++++++ runtime.go | 4 -- tc39_test.go | 29 +++++++------ 5 files changed, 179 insertions(+), 25 deletions(-) diff --git a/array.go b/array.go index eca5b548..0d624a0f 100644 --- a/array.go +++ b/array.go @@ -358,7 +358,7 @@ func (a *arrayObject) enumerate(all, recursive bool) iterNextFunc { func (a *arrayObject) hasOwnProperty(n Value) bool { if idx := toIdx(n); idx >= 0 { - return idx < int64(len(a.values)) && a.values[idx] != nil && a.values[idx] != _undefined + return idx < int64(len(a.values)) && a.values[idx] != nil } else { return a.baseObject.hasOwnProperty(n) } @@ -366,7 +366,7 @@ func (a *arrayObject) hasOwnProperty(n Value) bool { func (a *arrayObject) hasOwnPropertyStr(name string) bool { if idx := strToIdx(name); idx >= 0 { - return idx < int64(len(a.values)) && a.values[idx] != nil && a.values[idx] != _undefined + return idx < int64(len(a.values)) && a.values[idx] != nil } else { return a.baseObject.hasOwnPropertyStr(name) } diff --git a/builtin_array.go b/builtin_array.go index dbea68ba..f6de92de 100644 --- a/builtin_array.go +++ b/builtin_array.go @@ -870,9 +870,11 @@ func (r *Runtime) array_from(call FunctionCall) Value { } var ctor func(args []Value) *Object - if o, ok := call.This.(*Object); ok { - if c := getConstructor(o); c != nil { - ctor = c + if call.This != r.global.Array { + if o, ok := call.This.(*Object); ok { + if c := getConstructor(o); c != nil { + ctor = c + } } } var arr *Object @@ -883,12 +885,22 @@ func (r *Runtime) array_from(call FunctionCall) Value { arr = r.newArrayValues(nil) } iter := r.getIterator(items, usingIterator) + if mapFn == nil { + if a := r.checkStdArrayIter(arr); a != nil { + var values []Value + r.iterate(iter, func(val Value) { + values = append(values, val) + }) + setArrayValues(a, values) + return arr + } + } k := int64(0) r.iterate(iter, func(val Value) { if mapFn != nil { val = mapFn(FunctionCall{This: t, Arguments: []Value{val, intToValue(k)}}) } - arr.self.put(intToValue(k), val, true) + defineDataPropertyOrThrow(arr, intToValue(k), val) k++ }) arr.self.putStr("length", intToValue(k), true) @@ -898,15 +910,27 @@ func (r *Runtime) array_from(call FunctionCall) Value { if ctor != nil { arr = ctor([]Value{intToValue(l)}) } else { - arr = r.newArrayLength(l) + arr = r.newArrayValues(nil) + } + if mapFn == nil { + if a := r.checkStdArrayIter(arr); a != nil { + values := make([]Value, l) + for k := int64(0); k < l; k++ { + values[k] = nilSafe(arrayLike.self.get(intToValue(k))) + } + setArrayValues(a, values) + return arr + } } for k := int64(0); k < l; k++ { idx := intToValue(k) item := arrayLike.self.get(idx) if mapFn != nil { item = mapFn(FunctionCall{This: t, Arguments: []Value{item, idx}}) + } else { + item = nilSafe(item) } - arr.self.put(idx, item, true) + defineDataPropertyOrThrow(arr, idx, item) } arr.self.putStr("length", intToValue(l), true) } @@ -923,6 +947,38 @@ func (r *Runtime) array_isArray(call FunctionCall) Value { return valueFalse } +func defineDataPropertyOrThrow(o *Object, p Value, v Value) { + o.self.defineOwnProperty(p, propertyDescr{ + Writable: FLAG_TRUE, + Enumerable: FLAG_TRUE, + Configurable: FLAG_TRUE, + Value: v, + }, true) +} + +func (r *Runtime) array_of(call FunctionCall) Value { + var ctor func(args []Value) *Object + if call.This != r.global.Array { + if o, ok := call.This.(*Object); ok { + if c := getConstructor(o); c != nil { + ctor = c + } + } + } + if ctor == nil { + values := make([]Value, len(call.Arguments)) + copy(values, call.Arguments) + return r.newArrayValues(values) + } + l := intToValue(int64(len(call.Arguments))) + arr := ctor([]Value{l}) + for i, val := range call.Arguments { + defineDataPropertyOrThrow(arr, intToValue(int64(i)), val) + } + arr.self.putStr("length", l, true) + return arr +} + func (r *Runtime) arrayIterProto_next(call FunctionCall) Value { thisObj := r.toObject(call.This) if iter, ok := thisObj.self.(*arrayIterObject); ok { @@ -976,6 +1032,7 @@ func (r *Runtime) createArray(val *Object) objectImpl { o := r.newNativeFuncConstructObj(val, r.builtin_newArray, "Array", r.global.ArrayPrototype, 1) o._putProp("from", r.newNativeFunc(r.array_from, nil, "from", nil, 1), true, false, true) o._putProp("isArray", r.newNativeFunc(r.array_isArray, nil, "isArray", nil, 1), true, false, true) + o._putProp("of", r.newNativeFunc(r.array_of, nil, "of", nil, 0), true, false, true) o.putSym(symSpecies, &valueProperty{ getterFunc: r.newNativeFunc(r.returnThis, nil, "get [Symbol.species]", nil, 0), accessor: true, diff --git a/builtin_arrray_test.go b/builtin_arrray_test.go index a1f78639..d20aa94d 100644 --- a/builtin_arrray_test.go +++ b/builtin_arrray_test.go @@ -98,3 +98,101 @@ func TestArraySetLengthWithPropItems(t *testing.T) { testScript1(SCRIPT, valueTrue, t) } + +func TestArrayFrom(t *testing.T) { + const SCRIPT = ` + function checkDest(dest, prefix) { + assert(dest !== source, prefix + ": dest !== source"); + assert.sameValue(dest.length, 3, prefix + ": dest.length"); + assert.sameValue(dest[0], 1, prefix + ": [0]"); + assert.sameValue(dest[1], undefined, prefix + ": [1]"); + assert(dest.hasOwnProperty("1"), prefix + ': hasOwnProperty("1")'); + assert.sameValue(dest[2], 3, prefix + ": [2]"); + } + + var source = []; + source[0] = 1; + source[2] = 3; + checkDest(Array.from(source), "std source/std dest"); + + function Iter() { + this.idx = 0; + } + Iter.prototype.next = function() { + if (this.idx < source.length) { + return {value: source[this.idx++]}; + } else { + return {done: true}; + } + } + + var src = {}; + src[Symbol.iterator] = function() { + return new Iter(); + } + checkDest(Array.from(src), "iter src/std dest"); + + src = {0: 1, 2: 3, length: 3}; + checkDest(Array.from(src), "arrayLike src/std dest"); + + function A() {} + A.from = Array.from; + + checkDest(A.from(source), "std src/cust dest"); + checkDest(A.from(src), "arrayLike src/cust dest"); + + function T2() { + Object.defineProperty(this, 0, { + configurable: false, + writable: true, + enumerable: true + }); + } + + assert.throws(TypeError, function() { + Array.from.call(T2, source); + }); + + ` + + testScript1(TESTLIB+SCRIPT, _undefined, t) +} + +func TestArrayOf(t *testing.T) { + const SCRIPT = ` + function T1() { + Object.preventExtensions(this); + } + + assert.throws(TypeError, function() { + Array.of.call(T1, 'Bob'); + }); + + function T2() { + Object.defineProperty(this, 0, { + configurable: false, + writable: true, + enumerable: true + }); + } + + assert.throws(TypeError, function() { + Array.of.call(T2, 'Bob'); + }) + + result = Array.of.call(undefined); + assert( + result instanceof Array, + 'this is not a constructor' + ); + + result = Array.of.call(Math.cos); + assert( + result instanceof Array, + 'this is a builtin function with no [[Construct]] slot' + ); + + ` + + testScript1(TESTLIB+SCRIPT, _undefined, t) +} diff --git a/runtime.go b/runtime.go index b70c970e..e56beb5a 100644 --- a/runtime.go +++ b/runtime.go @@ -621,14 +621,10 @@ repeat: case *nativeFuncObject: if f.construct != nil { return f.construct - } else { - panic(construct.runtime.NewTypeError("Not a constructor")) } case *boundFuncObject: if f.construct != nil { return f.construct - } else { - panic(construct.runtime.NewTypeError("Not a constructor")) } case *funcObject: return f.construct diff --git a/tc39_test.go b/tc39_test.go index 8be90e89..c510ea37 100644 --- a/tc39_test.go +++ b/tc39_test.go @@ -52,6 +52,7 @@ var ( "test/built-ins/Set/proto-from-ctor-realm.js": true, "test/built-ins/Object/proto-from-ctor.js": true, "test/built-ins/Array/from/proto-from-ctor-realm.js": true, + "test/built-ins/Array/of/proto-from-ctor-realm.js": true, // class "test/language/statements/class/subclass/builtin-objects/Symbol/symbol-valid-as-extends-value.js": true, @@ -67,19 +68,6 @@ var ( "test/language/statements/class/subclass/builtin-objects/Object/replacing-prototype.js": true, "test/language/statements/class/subclass/builtin-objects/Object/regular-subclassing.js": true, - // Proxy - "test/built-ins/Object/prototype/toString/proxy-revoked.js": true, - "test/built-ins/Object/prototype/toString/proxy-function.js": true, - "test/built-ins/Object/prototype/toString/proxy-array.js": true, - "test/built-ins/JSON/stringify/value-proxy.js": true, - "test/built-ins/Object/setPrototypeOf/set-error.js": true, - "test/built-ins/Object/assign/source-own-prop-keys-error.js": true, - "test/built-ins/Object/assign/source-own-prop-error.js": true, - "test/built-ins/Object/assign/source-own-prop-desc-missing.js": true, - - // Arrow functions - "test/built-ins/Set/prototype/forEach/this-arg-explicit-cannot-override-lexical-this-arrow.js": true, - // full unicode regexp flag "test/built-ins/RegExp/prototype/Symbol.match/u-advance-after-empty.js": true, "test/built-ins/RegExp/prototype/Symbol.match/get-unicode-error.js": true, @@ -94,6 +82,11 @@ var ( "test/built-ins/Array/from/items-is-arraybuffer.js": true, } + featuresBlackList = []string{ + "Proxy", + "arrow-function", + } + es6WhiteList = map[string]bool{} es6IdWhiteList = []string{ @@ -106,6 +99,7 @@ var ( "21.1.3.17", "21.2.5.6", "22.1.2.1", + "22.1.2.3", "22.1.2.5", //"22.1.3.1", "22.1.3.29", @@ -140,6 +134,7 @@ type tc39Meta struct { Negative TC39MetaNegative Includes []string Flags []string + Features []string Es5id string Es6id string Esid string @@ -274,6 +269,14 @@ func (ctx *tc39TestCtx) runTC39File(name string, t testing.TB) { if skip { t.Skip("Not ES5") } + + for _, feature := range meta.Features { + for _, bl := range featuresBlackList { + if feature == bl { + t.Skip("Blacklisted feature") + } + } + } } hasRaw := meta.hasFlag("raw") From 36b747a12492249f440e1951860d61b655a819c6 Mon Sep 17 00:00:00 2001 From: Dmitry Panov Date: Fri, 6 Mar 2020 12:04:27 +0000 Subject: [PATCH 07/46] Merged changes from master. --- func.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/func.go b/func.go index 1ea1983f..6b8e4cd9 100644 --- a/func.go +++ b/func.go @@ -59,7 +59,11 @@ func (f *funcObject) putStr(name string, val Value, throw bool) { } func (f *funcObject) put(n Value, val Value, throw bool) { - f.putStr(n.String(), val, throw) + if s, ok := n.(*valueSymbol); ok { + f.putSym(s, val, throw) + } else { + f.putStr(n.String(), val, throw) + } } func (f *funcObject) deleteStr(name string, throw bool) bool { @@ -68,6 +72,9 @@ func (f *funcObject) deleteStr(name string, throw bool) bool { } func (f *funcObject) delete(n Value, throw bool) bool { + if s, ok := n.(*valueSymbol); ok { + return f.deleteSym(s, throw) + } return f.deleteStr(n.String(), throw) } From a4c36d5c28f9961c166c1d0cfd22638d821bba88 Mon Sep 17 00:00:00 2001 From: Dmitry Panov Date: Fri, 6 Mar 2020 19:50:15 +0000 Subject: [PATCH 08/46] Array.prototype.copyWithin() --- builtin_array.go | 103 ++++++++++++++++++++++++++++++++++------------- runtime.go | 9 +++++ tc39_test.go | 5 ++- 3 files changed, 89 insertions(+), 28 deletions(-) diff --git a/builtin_array.go b/builtin_array.go index f6de92de..2258467b 100644 --- a/builtin_array.go +++ b/builtin_array.go @@ -337,16 +337,10 @@ func (r *Runtime) arrayproto_slice(call FunctionCall) Value { a := r.newArrayLength(count) n := int64(0) - descr := propertyDescr{ - Writable: FLAG_TRUE, - Enumerable: FLAG_TRUE, - Configurable: FLAG_TRUE, - } for start < end { p := o.self.get(intToValue(start)) - if p != nil && p != _undefined { - descr.Value = p - a.self.defineOwnProperty(intToValue(n), descr, false) + if p != nil { + defineDataPropertyOrThrow(a, intToValue(n), p) } start++ n++ @@ -828,15 +822,78 @@ func (r *Runtime) arrayproto_values(call FunctionCall) Value { return r.createArrayIterator(call.This.ToObject(r), iterationKindValue) } -func (r *Runtime) checkStdArrayIter(v Value) *arrayObject { +func (r *Runtime) arrayproto_copyWithin(call FunctionCall) Value { + o := call.This.ToObject(r) + l := toLength(o.self.getStr("length")) + relTarget := call.Argument(0).ToInteger() + var from, to, relEnd, final, dir int64 + if relTarget < 0 { + to = max(l+relTarget, 0) + } else { + to = min(relTarget, l) + } + relStart := call.Argument(1).ToInteger() + if relStart < 0 { + from = max(l+relStart, 0) + } else { + from = min(relStart, l) + } + if end := call.Argument(2); end != _undefined { + relEnd = end.ToInteger() + } else { + relEnd = l + } + if relEnd < 0 { + final = max(l+relEnd, 0) + } else { + final = min(relEnd, l) + } + count := min(final-from, l-to) + if from < to && to < from+count { + dir = -1 + from = from + count - 1 + to = to + count - 1 + } else { + dir = 1 + } + for count > 0 { + if p := o.self.get(intToValue(from)); p != nil { + o.self.put(intToValue(to), p, true) + } else { + o.self.delete(intToValue(to), true) + } + from += dir + to += dir + count-- + } + + return o +} + +func (r *Runtime) checkStdArrayObj(obj *Object) *arrayObject { + if arr, ok := obj.self.(*arrayObject); ok && + arr.propValueCount == 0 && + arr.length == int64(len(arr.values)) { + + return arr + } + + return nil +} + +func (r *Runtime) checkStdArray(v Value) *arrayObject { if obj, ok := v.(*Object); ok { - if arr, ok := obj.self.(*arrayObject); ok && - arr.propValueCount == 0 && - arr.length == int64(len(arr.values)) && - arr.getSym(symIterator) == r.global.arrayValues { + return r.checkStdArrayObj(obj) + } - return arr - } + return nil +} + +func (r *Runtime) checkStdArrayIter(v Value) *arrayObject { + if arr := r.checkStdArray(v); arr != nil && + arr.getSym(symIterator) == r.global.arrayValues { + + return arr } return nil @@ -886,7 +943,7 @@ func (r *Runtime) array_from(call FunctionCall) Value { } iter := r.getIterator(items, usingIterator) if mapFn == nil { - if a := r.checkStdArrayIter(arr); a != nil { + if a := r.checkStdArrayObj(arr); a != nil { var values []Value r.iterate(iter, func(val Value) { values = append(values, val) @@ -913,7 +970,7 @@ func (r *Runtime) array_from(call FunctionCall) Value { arr = r.newArrayValues(nil) } if mapFn == nil { - if a := r.checkStdArrayIter(arr); a != nil { + if a := r.checkStdArrayObj(arr); a != nil { values := make([]Value, l) for k := int64(0); k < l; k++ { values[k] = nilSafe(arrayLike.self.get(intToValue(k))) @@ -947,15 +1004,6 @@ func (r *Runtime) array_isArray(call FunctionCall) Value { return valueFalse } -func defineDataPropertyOrThrow(o *Object, p Value, v Value) { - o.self.defineOwnProperty(p, propertyDescr{ - Writable: FLAG_TRUE, - Enumerable: FLAG_TRUE, - Configurable: FLAG_TRUE, - Value: v, - }, true) -} - func (r *Runtime) array_of(call FunctionCall) Value { var ctor func(args []Value) *Object if call.This != r.global.Array { @@ -999,6 +1047,7 @@ func (r *Runtime) createArrayProto(val *Object) objectImpl { o.init() o._putProp("constructor", r.global.Array, true, false, true) + o._putProp("copyWithin", r.newNativeFunc(r.arrayproto_copyWithin, nil, "copyWithin", nil, 2), true, false, true) o._putProp("pop", r.newNativeFunc(r.arrayproto_pop, nil, "pop", nil, 0), true, false, true) o._putProp("push", r.newNativeFunc(r.arrayproto_push, nil, "push", nil, 1), true, false, true) o._putProp("join", r.newNativeFunc(r.arrayproto_join, nil, "join", nil, 1), true, false, true) @@ -1022,7 +1071,7 @@ func (r *Runtime) createArrayProto(val *Object) objectImpl { o._putProp("reduceRight", r.newNativeFunc(r.arrayproto_reduceRight, nil, "reduceRight", nil, 1), true, false, true) valuesFunc := r.newNativeFunc(r.arrayproto_values, nil, "values", nil, 0) o._putProp("values", valuesFunc, true, false, true) - o.put(symIterator, valueProp(valuesFunc, false, false, true), true) + o.put(symIterator, valueProp(valuesFunc, true, false, true), true) r.global.arrayValues = valuesFunc return o diff --git a/runtime.go b/runtime.go index e56beb5a..1889eee1 100644 --- a/runtime.go +++ b/runtime.go @@ -1620,6 +1620,15 @@ func (r *Runtime) returnThis(call FunctionCall) Value { return call.This } +func defineDataPropertyOrThrow(o *Object, p Value, v Value) { + o.self.defineOwnProperty(p, propertyDescr{ + Writable: FLAG_TRUE, + Enumerable: FLAG_TRUE, + Configurable: FLAG_TRUE, + Value: v, + }, true) +} + func toPropertyKey(key Value) Value { return key.ToPrimitiveString() } diff --git a/tc39_test.go b/tc39_test.go index c510ea37..8f67bff1 100644 --- a/tc39_test.go +++ b/tc39_test.go @@ -102,6 +102,7 @@ var ( "22.1.2.3", "22.1.2.5", //"22.1.3.1", + "22.1.3.3", "22.1.3.29", "23.1", "23.2", @@ -259,7 +260,9 @@ func (ctx *tc39TestCtx) runTC39File(name string, t testing.TB) { } else { if meta.Es6id != "" { for _, prefix := range es6IdWhiteList { - if strings.HasPrefix(meta.Es6id, prefix) { + if strings.HasPrefix(meta.Es6id, prefix) && + (len(meta.Es6id) == len(prefix) || meta.Es6id[len(prefix)] == '.') { + skip = false break } From d3dfac6c0aa8da264995b4551b52aa3c8e3e85c9 Mon Sep 17 00:00:00 2001 From: Dmitry Panov Date: Sat, 7 Mar 2020 00:03:44 +0000 Subject: [PATCH 09/46] Array optimisations --- builtin_array.go | 75 +++++++++++++++++++++++++++--------------- builtin_arrray_test.go | 22 +++++++++---- builtin_weakset.go | 5 +-- compiler_expr.go | 11 ++++++- vm.go | 15 +++++++++ 5 files changed, 90 insertions(+), 38 deletions(-) diff --git a/builtin_array.go b/builtin_array.go index 2258467b..751d4139 100644 --- a/builtin_array.go +++ b/builtin_array.go @@ -237,32 +237,21 @@ func isConcatSpreadable(obj *Object) bool { } func (r *Runtime) arrayproto_concat_append(a *Object, item Value) { - descr := propertyDescr{ - Writable: FLAG_TRUE, - Enumerable: FLAG_TRUE, - Configurable: FLAG_TRUE, - } - aLength := toLength(a.self.getStr("length")) - if obj, ok := item.(*Object); ok { - if isConcatSpreadable(obj) { - length := toLength(obj.self.getStr("length")) - for i := int64(0); i < length; i++ { - v := obj.self.get(intToValue(i)) - if v != nil { - descr.Value = v - a.self.defineOwnProperty(intToValue(aLength), descr, false) - aLength++ - } else { - aLength++ - a.self.putStr("length", intToValue(aLength), false) - } + if obj, ok := item.(*Object); ok && isConcatSpreadable(obj) { + length := toLength(obj.self.getStr("length")) + for i := int64(0); i < length; i++ { + v := obj.self.get(intToValue(i)) + if v != nil { + defineDataPropertyOrThrow(a, intToValue(aLength), v) } - return + aLength++ } + } else { + defineDataPropertyOrThrow(a, intToValue(aLength), item) + aLength++ } - descr.Value = item - a.self.defineOwnProperty(intToValue(aLength), descr, false) + a.self.putStr("length", intToValue(aLength), true) } func arraySpeciesCreate(obj *Object, size int) *Object { @@ -334,8 +323,14 @@ func (r *Runtime) arrayproto_slice(call FunctionCall) Value { if count < 0 { count = 0 } - a := r.newArrayLength(count) + if arr := r.checkStdArrayObj(o); arr != nil { + values := make([]Value, count) + copy(values, arr.values[start:]) + return r.newArrayValues(values) + } + + a := r.newArrayLength(count) n := int64(0) for start < end { p := o.self.get(intToValue(start)) @@ -466,6 +461,15 @@ func (r *Runtime) arrayproto_indexOf(call FunctionCall) Value { searchElement := call.Argument(0) + if arr := r.checkStdArrayObj(o); arr != nil { + for i, val := range arr.values[n:] { + if searchElement.StrictEquals(val) { + return intToValue(n + int64(i)) + } + } + return intToValue(-1) + } + for ; n < length; n++ { idx := intToValue(n) if val := o.self.get(idx); val != nil { @@ -500,6 +504,16 @@ func (r *Runtime) arrayproto_lastIndexOf(call FunctionCall) Value { searchElement := call.Argument(0) + if arr := r.checkStdArrayObj(o); arr != nil { + vals := arr.values + for k := fromIndex; k >= 0; k-- { + if v := vals[k]; v != nil && searchElement.StrictEquals(v) { + return intToValue(k) + } + } + return intToValue(-1) + } + for k := fromIndex; k >= 0; k-- { idx := intToValue(k) if val := o.self.get(idx); val != nil { @@ -849,6 +863,12 @@ func (r *Runtime) arrayproto_copyWithin(call FunctionCall) Value { final = min(relEnd, l) } count := min(final-from, l-to) + if arr := r.checkStdArrayObj(o); arr != nil { + if count > 0 { + copy(arr.values[to:to+count], arr.values[from:from+count]) + } + return o + } if from < to && to < from+count { dir = -1 from = from + count - 1 @@ -873,7 +893,8 @@ func (r *Runtime) arrayproto_copyWithin(call FunctionCall) Value { func (r *Runtime) checkStdArrayObj(obj *Object) *arrayObject { if arr, ok := obj.self.(*arrayObject); ok && arr.propValueCount == 0 && - arr.length == int64(len(arr.values)) { + arr.length == int64(len(arr.values)) && + arr.objCount == arr.length { return arr } @@ -916,11 +937,11 @@ func (r *Runtime) array_from(call FunctionCall) Value { if mapFn == nil && call.This == r.global.Array { // mapFn may mutate the array if arr := r.checkStdArrayIter(items); arr != nil { items := make([]Value, len(arr.values)) - for i, item := range arr.values { + copy(items, arr.values) + for i, item := range items { if item == nil { - item = nilSafe(arr.get(intToValue(int64(i)))) + items[i] = _undefined } - items[i] = item } return r.newArrayValues(items) } diff --git a/builtin_arrray_test.go b/builtin_arrray_test.go index d20aa94d..a25f2f23 100644 --- a/builtin_arrray_test.go +++ b/builtin_arrray_test.go @@ -101,7 +101,7 @@ func TestArraySetLengthWithPropItems(t *testing.T) { func TestArrayFrom(t *testing.T) { const SCRIPT = ` - function checkDest(dest, prefix) { + function checkDestHoles(dest, prefix) { assert(dest !== source, prefix + ": dest !== source"); assert.sameValue(dest.length, 3, prefix + ": dest.length"); assert.sameValue(dest[0], 1, prefix + ": [0]"); @@ -110,10 +110,19 @@ func TestArrayFrom(t *testing.T) { assert.sameValue(dest[2], 3, prefix + ": [2]"); } - var source = []; - source[0] = 1; - source[2] = 3; + function checkDest(dest, prefix) { + assert(dest !== source, prefix + ": dest !== source"); + assert.sameValue(dest.length, 3, prefix + ": dest.length"); + assert.sameValue(dest[0], 1, prefix + ": [0]"); + assert.sameValue(dest[1], 2, prefix + ": [1]"); + assert.sameValue(dest[2], 3, prefix + ": [2]"); + } + + var source = [1,2,3]; + var srcHoles = [1,,3]; + checkDest(Array.from(source), "std source/std dest"); + checkDestHoles(Array.from(srcHoles), "std source (holes)/std dest"); function Iter() { this.idx = 0; @@ -133,13 +142,14 @@ func TestArrayFrom(t *testing.T) { checkDest(Array.from(src), "iter src/std dest"); src = {0: 1, 2: 3, length: 3}; - checkDest(Array.from(src), "arrayLike src/std dest"); + checkDestHoles(Array.from(src), "arrayLike src/std dest"); function A() {} A.from = Array.from; checkDest(A.from(source), "std src/cust dest"); - checkDest(A.from(src), "arrayLike src/cust dest"); + checkDestHoles(A.from(srcHoles), "std src (holes)/cust dest"); + checkDestHoles(A.from(src), "arrayLike src/cust dest"); function T2() { Object.defineProperty(this, 0, { diff --git a/builtin_weakset.go b/builtin_weakset.go index ae2f29c6..d73697c9 100644 --- a/builtin_weakset.go +++ b/builtin_weakset.go @@ -128,10 +128,7 @@ func (r *Runtime) builtin_newWeakSet(args []Value) *Object { adder := wso.getStr("add") if adder == r.global.weakSetAdder { if arr := r.checkStdArrayIter(arg); arr != nil { - for i, v := range arr.values { - if v == nil { - v = arr.get(intToValue(int64(i))) - } + for _, v := range arr.values { wso.set.add(r.toObject(v)) } return o diff --git a/compiler_expr.go b/compiler_expr.go index 0d966ea6..6bc6da54 100644 --- a/compiler_expr.go +++ b/compiler_expr.go @@ -1365,14 +1365,23 @@ func (c *compiler) compileObjectLiteral(v *ast.ObjectLiteral) compiledExpr { func (e *compiledArrayLiteral) emitGetter(putOnStack bool) { e.addSrcMap() + objCount := uint32(0) for _, v := range e.expr.Value { if v != nil { e.c.compileExpression(v).emitGetter(true) + objCount++ } else { e.c.emit(loadNil) } } - e.c.emit(newArray(len(e.expr.Value))) + if objCount == uint32(len(e.expr.Value)) { + e.c.emit(newArray(objCount)) + } else { + e.c.emit(&newArraySparse{ + l: uint32(len(e.expr.Value)), + objCount: objCount, + }) + } if !putOnStack { e.c.emit(pop) } diff --git a/vm.go b/vm.go index 3eea5d93..b54ab3f5 100644 --- a/vm.go +++ b/vm.go @@ -1248,6 +1248,21 @@ func (l newArray) exec(vm *vm) { vm.pc++ } +type newArraySparse struct { + l, objCount uint32 +} + +func (n *newArraySparse) exec(vm *vm) { + values := make([]Value, n.l) + copy(values, vm.stack[vm.sp-int(n.l):vm.sp]) + arr := vm.r.newArrayObject() + setArrayValues(arr, values) + arr.objCount = int64(n.objCount) + vm.sp -= int(n.l) - 1 + vm.stack[vm.sp-1] = arr.val + vm.pc++ +} + type newRegexp struct { pattern regexpPattern src valueString From 4b6f505854630fdf1ead2d46537ec6ea20b1be9d Mon Sep 17 00:00:00 2001 From: Dmitry Panov Date: Sat, 7 Mar 2020 14:08:07 +0000 Subject: [PATCH 10/46] Array.prototype.entries() and fill() --- array.go | 8 ++--- builtin_array.go | 85 +++++++++++++++++++++++++----------------------- tc39_test.go | 5 +++ 3 files changed, 53 insertions(+), 45 deletions(-) diff --git a/array.go b/array.go index 0d624a0f..53f777e9 100644 --- a/array.go +++ b/array.go @@ -24,16 +24,16 @@ func (ai *arrayIterObject) next() Value { return ai.val.runtime.createIterResultObject(_undefined, true) } ai.nextIdx++ + idxVal := intToValue(index) if ai.kind == iterationKindKey { - return ai.val.runtime.createIterResultObject(intToValue(index), false) + return ai.val.runtime.createIterResultObject(idxVal, false) } - elementKey := asciiString(strconv.FormatInt(index, 10)) - elementValue := ai.obj.self.get(intToValue(index)) + elementValue := ai.obj.self.get(idxVal) var result Value if ai.kind == iterationKindValue { result = elementValue } else { - result = ai.val.runtime.newArrayValues([]Value{elementKey, elementValue}) + result = ai.val.runtime.newArrayValues([]Value{idxVal, elementValue}) } return ai.val.runtime.createIterResultObject(result, false) } diff --git a/builtin_array.go b/builtin_array.go index 751d4139..b853b1eb 100644 --- a/builtin_array.go +++ b/builtin_array.go @@ -35,6 +35,13 @@ func setArrayLength(a *arrayObject, l int64) *arrayObject { return a } +func relToIdx(rel, l int64) int64 { + if rel >= 0 { + return min(rel, l) + } + return max(l+rel, 0) +} + func (r *Runtime) newArrayValues(values []Value) *Object { return setArrayValues(r.newArrayObject(), values).val } @@ -301,23 +308,14 @@ func min(a, b int64) int64 { func (r *Runtime) arrayproto_slice(call FunctionCall) Value { o := call.This.ToObject(r) length := toLength(o.self.getStr("length")) - start := call.Argument(0).ToInteger() - if start < 0 { - start = max(length+start, 0) - } else { - start = min(start, length) - } + start := relToIdx(call.Argument(0).ToInteger(), length) var end int64 if endArg := call.Argument(1); endArg != _undefined { end = endArg.ToInteger() } else { end = length } - if end < 0 { - end = max(length+end, 0) - } else { - end = min(end, length) - } + end = relToIdx(end, length) count := end - start if count < 0 { @@ -365,13 +363,7 @@ func (r *Runtime) arrayproto_splice(call FunctionCall) Value { o := call.This.ToObject(r) a := r.newArrayValues(nil) length := toLength(o.self.getStr("length")) - relativeStart := call.Argument(0).ToInteger() - var actualStart int64 - if relativeStart < 0 { - actualStart = max(length+relativeStart, 0) - } else { - actualStart = min(relativeStart, length) - } + actualStart := relToIdx(call.Argument(0).ToInteger(), length) actualDeleteCount := min(max(call.Argument(1).ToInteger(), 0), length-actualStart) @@ -839,29 +831,15 @@ func (r *Runtime) arrayproto_values(call FunctionCall) Value { func (r *Runtime) arrayproto_copyWithin(call FunctionCall) Value { o := call.This.ToObject(r) l := toLength(o.self.getStr("length")) - relTarget := call.Argument(0).ToInteger() - var from, to, relEnd, final, dir int64 - if relTarget < 0 { - to = max(l+relTarget, 0) - } else { - to = min(relTarget, l) - } - relStart := call.Argument(1).ToInteger() - if relStart < 0 { - from = max(l+relStart, 0) - } else { - from = min(relStart, l) - } + var relEnd, dir int64 + to := relToIdx(call.Argument(0).ToInteger(), l) + from := relToIdx(call.Argument(1).ToInteger(), l) if end := call.Argument(2); end != _undefined { relEnd = end.ToInteger() } else { relEnd = l } - if relEnd < 0 { - final = max(l+relEnd, 0) - } else { - final = min(relEnd, l) - } + final := relToIdx(relEnd, l) count := min(final-from, l-to) if arr := r.checkStdArrayObj(o); arr != nil { if count > 0 { @@ -890,6 +868,34 @@ func (r *Runtime) arrayproto_copyWithin(call FunctionCall) Value { return o } +func (r *Runtime) arrayproto_entries(call FunctionCall) Value { + return r.createArrayIterator(call.This.ToObject(r), iterationKindKeyValue) +} + +func (r *Runtime) arrayproto_fill(call FunctionCall) Value { + o := call.This.ToObject(r) + l := toLength(o.self.getStr("length")) + k := relToIdx(call.Argument(1).ToInteger(), l) + var relEnd int64 + if endArg := call.Argument(2); endArg != _undefined { + relEnd = endArg.ToInteger() + } else { + relEnd = l + } + final := relToIdx(relEnd, l) + value := call.Argument(0) + if arr := r.checkStdArrayObj(o); arr != nil { + for ; k < final; k++ { + arr.values[k] = value + } + } else { + for ; k < final; k++ { + o.self.put(intToValue(k), value, true) + } + } + return o +} + func (r *Runtime) checkStdArrayObj(obj *Object) *arrayObject { if arr, ok := obj.self.(*arrayObject); ok && arr.propValueCount == 0 && @@ -938,11 +944,6 @@ func (r *Runtime) array_from(call FunctionCall) Value { if arr := r.checkStdArrayIter(items); arr != nil { items := make([]Value, len(arr.values)) copy(items, arr.values) - for i, item := range items { - if item == nil { - items[i] = _undefined - } - } return r.newArrayValues(items) } } @@ -1069,6 +1070,8 @@ func (r *Runtime) createArrayProto(val *Object) objectImpl { o._putProp("constructor", r.global.Array, true, false, true) o._putProp("copyWithin", r.newNativeFunc(r.arrayproto_copyWithin, nil, "copyWithin", nil, 2), true, false, true) + o._putProp("entries", r.newNativeFunc(r.arrayproto_entries, nil, "entries", nil, 0), true, false, true) + o._putProp("fill", r.newNativeFunc(r.arrayproto_fill, nil, "fill", nil, 1), true, false, true) o._putProp("pop", r.newNativeFunc(r.arrayproto_pop, nil, "pop", nil, 0), true, false, true) o._putProp("push", r.newNativeFunc(r.arrayproto_push, nil, "push", nil, 1), true, false, true) o._putProp("join", r.newNativeFunc(r.arrayproto_join, nil, "join", nil, 1), true, false, true) diff --git a/tc39_test.go b/tc39_test.go index 8f67bff1..e2e1034f 100644 --- a/tc39_test.go +++ b/tc39_test.go @@ -80,6 +80,9 @@ var ( // Typed arrays "test/built-ins/Array/from/items-is-arraybuffer.js": true, + + // for-of + "test/language/statements/for-of/Array.prototype.entries.js": true, } featuresBlackList = []string{ @@ -103,6 +106,8 @@ var ( "22.1.2.5", //"22.1.3.1", "22.1.3.3", + "22.1.3.4", + "22.1.3.6", "22.1.3.29", "23.1", "23.2", From 3274eaa6014030dbb586fd042201027497cc664d Mon Sep 17 00:00:00 2001 From: Dmitry Panov Date: Mon, 9 Mar 2020 18:36:56 +0000 Subject: [PATCH 11/46] Array.prototype.filter() and find() --- builtin_array.go | 77 +++++++++++++++++++++++++++++++++++++----------- tc39_test.go | 49 +++++++++++++++++------------- 2 files changed, 88 insertions(+), 38 deletions(-) diff --git a/builtin_array.go b/builtin_array.go index b853b1eb..3a524f04 100644 --- a/builtin_array.go +++ b/builtin_array.go @@ -261,24 +261,28 @@ func (r *Runtime) arrayproto_concat_append(a *Object, item Value) { a.self.putStr("length", intToValue(aLength), true) } -func arraySpeciesCreate(obj *Object, size int) *Object { +func arraySpeciesCreate(obj *Object, size int64) *Object { if isArray(obj) { v := obj.self.getStr("constructor") if constructObj, ok := v.(*Object); ok { - species := constructObj.self.get(symSpecies) - if species != nil && !IsUndefined(species) && !IsNull(species) { - constructObj, _ = species.(*Object) - if constructObj != nil { - constructor := getConstructor(constructObj) - if constructor != nil { - return constructor([]Value{intToValue(int64(size))}) - } + v = constructObj.self.get(symSpecies) + if v == _null { + v = nil + } + } + + if v != nil && v != _undefined { + constructObj, _ := v.(*Object) + if constructObj != nil { + constructor := getConstructor(constructObj) + if constructor != nil { + return constructor([]Value{intToValue(size)}) } - panic(obj.runtime.NewTypeError()) } + panic(obj.runtime.NewTypeError("Species is not a constructor")) } } - return obj.runtime.newArrayValues(nil) + return obj.runtime.newArrayLength(size) } func (r *Runtime) arrayproto_concat(call FunctionCall) Value { @@ -624,24 +628,42 @@ func (r *Runtime) arrayproto_filter(call FunctionCall) Value { length := toLength(o.self.getStr("length")) callbackFn := call.Argument(0).ToObject(r) if callbackFn, ok := callbackFn.self.assertCallable(); ok { - a := r.newArrayObject() + a := arraySpeciesCreate(o, 0) fc := FunctionCall{ This: call.Argument(1), Arguments: []Value{nil, nil, o}, } + if _, stdSrc := o.self.(*arrayObject); stdSrc { + if arr := r.checkStdArrayObj(a); arr != nil { + var values []Value + for k := int64(0); k < length; k++ { + idx := intToValue(k) + if val := o.self.get(idx); val != nil { + fc.Arguments[0] = val + fc.Arguments[1] = idx + if callbackFn(fc).ToBoolean() { + values = append(values, val) + } + } + } + setArrayValues(arr, values) + return a + } + } + + to := int64(0) for k := int64(0); k < length; k++ { idx := intToValue(k) if val := o.self.get(idx); val != nil { fc.Arguments[0] = val fc.Arguments[1] = idx if callbackFn(fc).ToBoolean() { - a.values = append(a.values, val) + defineDataPropertyOrThrow(a, intToValue(to), val) + to++ } } } - a.length = int64(len(a.values)) - a.objCount = a.length - return a.val + return a } else { r.typeErrorResult(true, "%s is not a function", call.Argument(0)) } @@ -896,6 +918,26 @@ func (r *Runtime) arrayproto_fill(call FunctionCall) Value { return o } +func (r *Runtime) arrayproto_find(call FunctionCall) Value { + o := call.This.ToObject(r) + l := toLength(o.self.getStr("length")) + predicate := r.toCallable(call.Argument(0)) + fc := FunctionCall{ + This: call.Argument(1), + Arguments: []Value{nil, nil, o}, + } + for k := int64(0); k < l; k++ { + idx := intToValue(k) + kValue := o.self.get(idx) + fc.Arguments[0], fc.Arguments[1] = kValue, idx + if predicate(fc).ToBoolean() { + return kValue + } + } + + return _undefined +} + func (r *Runtime) checkStdArrayObj(obj *Object) *arrayObject { if arr, ok := obj.self.(*arrayObject); ok && arr.propValueCount == 0 && @@ -1072,6 +1114,8 @@ func (r *Runtime) createArrayProto(val *Object) objectImpl { o._putProp("copyWithin", r.newNativeFunc(r.arrayproto_copyWithin, nil, "copyWithin", nil, 2), true, false, true) o._putProp("entries", r.newNativeFunc(r.arrayproto_entries, nil, "entries", nil, 0), true, false, true) o._putProp("fill", r.newNativeFunc(r.arrayproto_fill, nil, "fill", nil, 1), true, false, true) + o._putProp("filter", r.newNativeFunc(r.arrayproto_filter, nil, "filter", nil, 1), true, false, true) + o._putProp("find", r.newNativeFunc(r.arrayproto_find, nil, "find", nil, 1), true, false, true) o._putProp("pop", r.newNativeFunc(r.arrayproto_pop, nil, "pop", nil, 0), true, false, true) o._putProp("push", r.newNativeFunc(r.arrayproto_push, nil, "push", nil, 1), true, false, true) o._putProp("join", r.newNativeFunc(r.arrayproto_join, nil, "join", nil, 1), true, false, true) @@ -1090,7 +1134,6 @@ func (r *Runtime) createArrayProto(val *Object) objectImpl { o._putProp("some", r.newNativeFunc(r.arrayproto_some, nil, "some", nil, 1), true, false, true) o._putProp("forEach", r.newNativeFunc(r.arrayproto_forEach, nil, "forEach", nil, 1), true, false, true) o._putProp("map", r.newNativeFunc(r.arrayproto_map, nil, "map", nil, 1), true, false, true) - o._putProp("filter", r.newNativeFunc(r.arrayproto_filter, nil, "filter", nil, 1), true, false, true) o._putProp("reduce", r.newNativeFunc(r.arrayproto_reduce, nil, "reduce", nil, 1), true, false, true) o._putProp("reduceRight", r.newNativeFunc(r.arrayproto_reduceRight, nil, "reduceRight", nil, 1), true, false, true) valuesFunc := r.newNativeFunc(r.arrayproto_values, nil, "values", nil, 0) diff --git a/tc39_test.go b/tc39_test.go index e2e1034f..88e81e8c 100644 --- a/tc39_test.go +++ b/tc39_test.go @@ -33,26 +33,30 @@ var ( "test/annexB/built-ins/escape/escape-above-astral.js": true, // \u{xxxxx} // cross-realm - "test/built-ins/Symbol/unscopables/cross-realm.js": true, - "test/built-ins/Symbol/toStringTag/cross-realm.js": true, - "test/built-ins/Symbol/toPrimitive/cross-realm.js": true, - "test/built-ins/Symbol/split/cross-realm.js": true, - "test/built-ins/Symbol/species/cross-realm.js": true, - "test/built-ins/Symbol/search/cross-realm.js": true, - "test/built-ins/Symbol/replace/cross-realm.js": true, - "test/built-ins/Symbol/match/cross-realm.js": true, - "test/built-ins/Symbol/keyFor/cross-realm.js": true, - "test/built-ins/Symbol/iterator/cross-realm.js": true, - "test/built-ins/Symbol/isConcatSpreadable/cross-realm.js": true, - "test/built-ins/Symbol/hasInstance/cross-realm.js": true, - "test/built-ins/Symbol/for/cross-realm.js": true, - "test/built-ins/WeakSet/proto-from-ctor-realm.js": true, - "test/built-ins/WeakMap/proto-from-ctor-realm.js": true, - "test/built-ins/Map/proto-from-ctor-realm.js": true, - "test/built-ins/Set/proto-from-ctor-realm.js": true, - "test/built-ins/Object/proto-from-ctor.js": true, - "test/built-ins/Array/from/proto-from-ctor-realm.js": true, - "test/built-ins/Array/of/proto-from-ctor-realm.js": true, + "test/built-ins/Symbol/unscopables/cross-realm.js": true, + "test/built-ins/Symbol/toStringTag/cross-realm.js": true, + "test/built-ins/Symbol/toPrimitive/cross-realm.js": true, + "test/built-ins/Symbol/split/cross-realm.js": true, + "test/built-ins/Symbol/species/cross-realm.js": true, + "test/built-ins/Symbol/search/cross-realm.js": true, + "test/built-ins/Symbol/replace/cross-realm.js": true, + "test/built-ins/Symbol/match/cross-realm.js": true, + "test/built-ins/Symbol/keyFor/cross-realm.js": true, + "test/built-ins/Symbol/iterator/cross-realm.js": true, + "test/built-ins/Symbol/isConcatSpreadable/cross-realm.js": true, + "test/built-ins/Symbol/hasInstance/cross-realm.js": true, + "test/built-ins/Symbol/for/cross-realm.js": true, + "test/built-ins/WeakSet/proto-from-ctor-realm.js": true, + "test/built-ins/WeakMap/proto-from-ctor-realm.js": true, + "test/built-ins/Map/proto-from-ctor-realm.js": true, + "test/built-ins/Set/proto-from-ctor-realm.js": true, + "test/built-ins/Object/proto-from-ctor.js": true, + "test/built-ins/Array/from/proto-from-ctor-realm.js": true, + "test/built-ins/Array/of/proto-from-ctor-realm.js": true, + "test/built-ins/Array/prototype/concat/create-proto-from-ctor-realm-non-array.js": true, + "test/built-ins/Array/prototype/concat/create-proto-from-ctor-realm-array.js": true, + "test/built-ins/Array/prototype/filter/create-proto-from-ctor-realm-non-array.js": true, + "test/built-ins/Array/prototype/filter/create-proto-from-ctor-realm-array.js": true, // class "test/language/statements/class/subclass/builtin-objects/Symbol/symbol-valid-as-extends-value.js": true, @@ -104,10 +108,13 @@ var ( "22.1.2.1", "22.1.2.3", "22.1.2.5", - //"22.1.3.1", + "22.1.3.1", "22.1.3.3", "22.1.3.4", + "22.1.3.5", "22.1.3.6", + "22.1.3.7", + "22.1.3.8", "22.1.3.29", "23.1", "23.2", From d71b8868274d739a0bcb126c8892863fdfeee837 Mon Sep 17 00:00:00 2001 From: Dmitry Panov Date: Wed, 11 Mar 2020 10:43:30 +0000 Subject: [PATCH 12/46] Array, Array.prototype --- builtin_array.go | 507 ++++++++++++++++++++++++++--------------------- tc39_test.go | 29 ++- 2 files changed, 299 insertions(+), 237 deletions(-) diff --git a/builtin_array.go b/builtin_array.go index 3a524f04..13e13bae 100644 --- a/builtin_array.go +++ b/builtin_array.go @@ -1,7 +1,6 @@ package goja import ( - "bytes" "sort" "strings" ) @@ -35,6 +34,44 @@ func setArrayLength(a *arrayObject, l int64) *arrayObject { return a } +func arraySpeciesCreate(obj *Object, size int64) *Object { + if isArray(obj) { + v := obj.self.getStr("constructor") + if constructObj, ok := v.(*Object); ok { + v = constructObj.self.get(symSpecies) + if v == _null { + v = nil + } + } + + if v != nil && v != _undefined { + constructObj, _ := v.(*Object) + if constructObj != nil { + constructor := getConstructor(constructObj) + if constructor != nil { + return constructor([]Value{intToValue(size)}) + } + } + panic(obj.runtime.NewTypeError("Species is not a constructor")) + } + } + return obj.runtime.newArrayLength(size) +} + +func max(a, b int64) int64 { + if a > b { + return a + } + return b +} + +func min(a, b int64) int64 { + if a < b { + return a + } + return b +} + func relToIdx(rel, l int64) int64 { if rel >= 0 { return min(rel, l) @@ -147,7 +184,7 @@ func (r *Runtime) arrayproto_join(call FunctionCall) Value { return stringEmpty } - var buf bytes.Buffer + var buf strings.Builder element0 := o.self.get(intToValue(0)) if element0 != nil && element0 != _undefined && element0 != _null { @@ -180,59 +217,43 @@ func (r *Runtime) arrayproto_toString(call FunctionCall) Value { }) } -func (r *Runtime) writeItemLocaleString(item Value, buf *bytes.Buffer) { +func (r *Runtime) writeItemLocaleString(item Value, buf *strings.Builder) { if item != nil && item != _undefined && item != _null { - itemObj := item.ToObject(r) - if f, ok := itemObj.self.getStr("toLocaleString").(*Object); ok { + if f, ok := r.getVStr(item, "toLocaleString").(*Object); ok { if c, ok := f.self.assertCallable(); ok { strVal := c(FunctionCall{ - This: itemObj, + This: item, }) - buf.WriteString(strVal.String()) + buf.WriteString(strVal.ToPrimitiveString().String()) return } } - r.typeErrorResult(true, "Property 'toLocaleString' of object %s is not a function", itemObj) + r.typeErrorResult(true, "Property 'toLocaleString' of object %s is not a function", item) } } -func (r *Runtime) arrayproto_toLocaleString_generic(obj *Object, start int64, buf *bytes.Buffer) Value { - length := toLength(obj.self.getStr("length")) - for i := int64(start); i < length; i++ { - if i > 0 { - buf.WriteByte(',') - } - item := obj.self.get(intToValue(i)) - r.writeItemLocaleString(item, buf) - } - return newStringValue(buf.String()) -} - func (r *Runtime) arrayproto_toLocaleString(call FunctionCall) Value { array := call.This.ToObject(r) - if a, ok := array.self.(*arrayObject); ok { - var buf bytes.Buffer - for i := int64(0); i < a.length; i++ { - var item Value - if i < int64(len(a.values)) { - item = a.values[i] - } - if item == nil { - return r.arrayproto_toLocaleString_generic(array, i, &buf) - } - if prop, ok := item.(*valueProperty); ok { - item = prop.get(array) - } + var buf strings.Builder + if a := r.checkStdArrayObj(array); a != nil { + for i, item := range a.values { if i > 0 { buf.WriteByte(',') } r.writeItemLocaleString(item, &buf) } - return newStringValue(buf.String()) } else { - return r.arrayproto_toLocaleString_generic(array, 0, bytes.NewBuffer(nil)) + length := toLength(array.self.getStr("length")) + for i := int64(0); i < length; i++ { + if i > 0 { + buf.WriteByte(',') + } + item := array.self.get(intToValue(i)) + r.writeItemLocaleString(item, &buf) + } } + return newStringValue(buf.String()) } func isConcatSpreadable(obj *Object) bool { @@ -261,30 +282,6 @@ func (r *Runtime) arrayproto_concat_append(a *Object, item Value) { a.self.putStr("length", intToValue(aLength), true) } -func arraySpeciesCreate(obj *Object, size int64) *Object { - if isArray(obj) { - v := obj.self.getStr("constructor") - if constructObj, ok := v.(*Object); ok { - v = constructObj.self.get(symSpecies) - if v == _null { - v = nil - } - } - - if v != nil && v != _undefined { - constructObj, _ := v.(*Object) - if constructObj != nil { - constructor := getConstructor(constructObj) - if constructor != nil { - return constructor([]Value{intToValue(size)}) - } - } - panic(obj.runtime.NewTypeError("Species is not a constructor")) - } - } - return obj.runtime.newArrayLength(size) -} - func (r *Runtime) arrayproto_concat(call FunctionCall) Value { obj := call.This.ToObject(r) a := arraySpeciesCreate(obj, 0) @@ -295,20 +292,6 @@ func (r *Runtime) arrayproto_concat(call FunctionCall) Value { return a } -func max(a, b int64) int64 { - if a > b { - return a - } - return b -} - -func min(a, b int64) int64 { - if a < b { - return a - } - return b -} - func (r *Runtime) arrayproto_slice(call FunctionCall) Value { o := call.This.ToObject(r) length := toLength(o.self.getStr("length")) @@ -326,13 +309,16 @@ func (r *Runtime) arrayproto_slice(call FunctionCall) Value { count = 0 } - if arr := r.checkStdArrayObj(o); arr != nil { - values := make([]Value, count) - copy(values, arr.values[start:]) - return r.newArrayValues(values) + a := arraySpeciesCreate(o, count) + if src := r.checkStdArrayObj(o); src != nil { + if dst, ok := a.self.(*arrayObject); ok { + values := make([]Value, count) + copy(values, src.values[start:]) + setArrayValues(dst, values) + return a + } } - a := r.newArrayLength(count) n := int64(0) for start < end { p := o.self.get(intToValue(start)) @@ -365,53 +351,97 @@ func (r *Runtime) arrayproto_sort(call FunctionCall) Value { func (r *Runtime) arrayproto_splice(call FunctionCall) Value { o := call.This.ToObject(r) - a := r.newArrayValues(nil) length := toLength(o.self.getStr("length")) actualStart := relToIdx(call.Argument(0).ToInteger(), length) - - actualDeleteCount := min(max(call.Argument(1).ToInteger(), 0), length-actualStart) - - for k := int64(0); k < actualDeleteCount; k++ { - from := intToValue(k + actualStart) - if o.self.hasProperty(from) { - a.self.put(intToValue(k), o.self.get(from), false) - } - } - + var actualDeleteCount int64 + switch len(call.Arguments) { + case 0: + case 1: + actualDeleteCount = length - actualStart + default: + actualDeleteCount = min(max(call.Argument(1).ToInteger(), 0), length-actualStart) + } + a := arraySpeciesCreate(o, actualDeleteCount) itemCount := max(int64(len(call.Arguments)-2), 0) - if itemCount < actualDeleteCount { - for k := actualStart; k < length-actualDeleteCount; k++ { - from := intToValue(k + actualDeleteCount) - to := intToValue(k + itemCount) - if o.self.hasProperty(from) { - o.self.put(to, o.self.get(from), true) + newLength := length - actualDeleteCount + itemCount + if src := r.checkStdArrayObj(o); src != nil { + if dst, ok := a.self.(*arrayObject); ok { + values := make([]Value, actualDeleteCount) + copy(values, src.values[actualStart:]) + setArrayValues(dst, values) + } else { + for k := int64(0); k < actualDeleteCount; k++ { + defineDataPropertyOrThrow(a, intToValue(k), src.values[k+actualStart]) + } + } + var values []Value + if itemCount < actualDeleteCount { + values = src.values + copy(values[actualStart+itemCount:], values[actualStart+actualDeleteCount:]) + tail := values[newLength:] + for k := range tail { + tail[k] = nil + } + values = values[:newLength] + } else if itemCount > actualDeleteCount { + if int64(cap(src.values)) >= newLength { + values = src.values[:newLength] + copy(values[actualStart+itemCount:], values[actualStart+actualDeleteCount:length]) } else { - o.self.delete(to, true) + values = make([]Value, newLength) + copy(values, src.values[:actualStart]) + copy(values[actualStart+itemCount:], src.values[actualStart+actualDeleteCount:]) } + } else { + values = src.values } - - for k := length; k > length-actualDeleteCount+itemCount; k-- { - o.self.delete(intToValue(k-1), true) + if itemCount > 0 { + copy(values[actualStart:], call.Arguments[2:]) } - } else if itemCount > actualDeleteCount { - for k := length - actualDeleteCount; k > actualStart; k-- { - from := intToValue(k + actualDeleteCount - 1) - to := intToValue(k + itemCount - 1) + src.values = values + src.objCount = int64(len(values)) + } else { + for k := int64(0); k < actualDeleteCount; k++ { + from := intToValue(k + actualStart) if o.self.hasProperty(from) { - o.self.put(to, o.self.get(from), true) - } else { - o.self.delete(to, true) + defineDataPropertyOrThrow(a, intToValue(k), o.self.get(from)) } } - } - if itemCount > 0 { - for i, item := range call.Arguments[2:] { - o.self.put(intToValue(actualStart+int64(i)), item, true) + if itemCount < actualDeleteCount { + for k := actualStart; k < length-actualDeleteCount; k++ { + from := intToValue(k + actualDeleteCount) + to := intToValue(k + itemCount) + if o.self.hasProperty(from) { + o.self.put(to, o.self.get(from), true) + } else { + o.self.delete(to, true) + } + } + + for k := length; k > length-actualDeleteCount+itemCount; k-- { + o.self.delete(intToValue(k-1), true) + } + } else if itemCount > actualDeleteCount { + for k := length - actualDeleteCount; k > actualStart; k-- { + from := intToValue(k + actualDeleteCount - 1) + to := intToValue(k + itemCount - 1) + if o.self.hasProperty(from) { + o.self.put(to, o.self.get(from), true) + } else { + o.self.delete(to, true) + } + } + } + + if itemCount > 0 { + for i, item := range call.Arguments[2:] { + o.self.put(intToValue(actualStart+int64(i)), item, true) + } } } - o.self.putStr("length", intToValue(length-actualDeleteCount+itemCount), true) + o.self.putStr("length", intToValue(newLength), true) return a } @@ -420,21 +450,35 @@ func (r *Runtime) arrayproto_unshift(call FunctionCall) Value { o := call.This.ToObject(r) length := toLength(o.self.getStr("length")) argCount := int64(len(call.Arguments)) - for k := length - 1; k >= 0; k-- { - from := intToValue(k) - to := intToValue(k + argCount) - if o.self.hasProperty(from) { - o.self.put(to, o.self.get(from), true) + newLen := intToValue(length + argCount) + if arr := r.checkStdArrayObj(o); arr != nil { + newSize := length + argCount + if int64(cap(arr.values)) >= newSize { + arr.values = arr.values[:newSize] + copy(arr.values[argCount:], arr.values[:length]) } else { - o.self.delete(to, true) + values := make([]Value, newSize) + copy(values[argCount:], arr.values) + arr.values = values + } + copy(arr.values, call.Arguments) + arr.objCount = arr.length + } else { + for k := length - 1; k >= 0; k-- { + from := intToValue(k) + to := intToValue(k + argCount) + if o.self.hasProperty(from) { + o.self.put(to, o.self.get(from), true) + } else { + o.self.delete(to, true) + } } - } - for k, arg := range call.Arguments { - o.self.put(intToValue(int64(k)), arg, true) + for k, arg := range call.Arguments { + o.self.put(intToValue(int64(k)), arg, true) + } } - newLen := intToValue(length + argCount) o.self.putStr("length", newLen, true) return newLen } @@ -525,24 +569,20 @@ func (r *Runtime) arrayproto_lastIndexOf(call FunctionCall) Value { func (r *Runtime) arrayproto_every(call FunctionCall) Value { o := call.This.ToObject(r) length := toLength(o.self.getStr("length")) - callbackFn := call.Argument(0).ToObject(r) - if callbackFn, ok := callbackFn.self.assertCallable(); ok { - fc := FunctionCall{ - This: call.Argument(1), - Arguments: []Value{nil, nil, o}, - } - for k := int64(0); k < length; k++ { - idx := intToValue(k) - if val := o.self.get(idx); val != nil { - fc.Arguments[0] = val - fc.Arguments[1] = idx - if !callbackFn(fc).ToBoolean() { - return valueFalse - } + callbackFn := r.toCallable(call.Argument(0)) + fc := FunctionCall{ + This: call.Argument(1), + Arguments: []Value{nil, nil, o}, + } + for k := int64(0); k < length; k++ { + idx := intToValue(k) + if val := o.self.get(idx); val != nil { + fc.Arguments[0] = val + fc.Arguments[1] = idx + if !callbackFn(fc).ToBoolean() { + return valueFalse } } - } else { - r.typeErrorResult(true, "%s is not a function", call.Argument(0)) } return valueTrue } @@ -550,24 +590,20 @@ func (r *Runtime) arrayproto_every(call FunctionCall) Value { func (r *Runtime) arrayproto_some(call FunctionCall) Value { o := call.This.ToObject(r) length := toLength(o.self.getStr("length")) - callbackFn := call.Argument(0).ToObject(r) - if callbackFn, ok := callbackFn.self.assertCallable(); ok { - fc := FunctionCall{ - This: call.Argument(1), - Arguments: []Value{nil, nil, o}, - } - for k := int64(0); k < length; k++ { - idx := intToValue(k) - if val := o.self.get(idx); val != nil { - fc.Arguments[0] = val - fc.Arguments[1] = idx - if callbackFn(fc).ToBoolean() { - return valueTrue - } + callbackFn := r.toCallable(call.Argument(0)) + fc := FunctionCall{ + This: call.Argument(1), + Arguments: []Value{nil, nil, o}, + } + for k := int64(0); k < length; k++ { + idx := intToValue(k) + if val := o.self.get(idx); val != nil { + fc.Arguments[0] = val + fc.Arguments[1] = idx + if callbackFn(fc).ToBoolean() { + return valueTrue } } - } else { - r.typeErrorResult(true, "%s is not a function", call.Argument(0)) } return valueFalse } @@ -575,22 +611,18 @@ func (r *Runtime) arrayproto_some(call FunctionCall) Value { func (r *Runtime) arrayproto_forEach(call FunctionCall) Value { o := call.This.ToObject(r) length := toLength(o.self.getStr("length")) - callbackFn := call.Argument(0).ToObject(r) - if callbackFn, ok := callbackFn.self.assertCallable(); ok { - fc := FunctionCall{ - This: call.Argument(1), - Arguments: []Value{nil, nil, o}, - } - for k := int64(0); k < length; k++ { - idx := intToValue(k) - if val := o.self.get(idx); val != nil { - fc.Arguments[0] = val - fc.Arguments[1] = idx - callbackFn(fc) - } + callbackFn := r.toCallable(call.Argument(0)) + fc := FunctionCall{ + This: call.Argument(1), + Arguments: []Value{nil, nil, o}, + } + for k := int64(0); k < length; k++ { + idx := intToValue(k) + if val := o.self.get(idx); val != nil { + fc.Arguments[0] = val + fc.Arguments[1] = idx + callbackFn(fc) } - } else { - r.typeErrorResult(true, "%s is not a function", call.Argument(0)) } return _undefined } @@ -598,29 +630,36 @@ func (r *Runtime) arrayproto_forEach(call FunctionCall) Value { func (r *Runtime) arrayproto_map(call FunctionCall) Value { o := call.This.ToObject(r) length := toLength(o.self.getStr("length")) - callbackFn := call.Argument(0).ToObject(r) - if callbackFn, ok := callbackFn.self.assertCallable(); ok { - fc := FunctionCall{ - This: call.Argument(1), - Arguments: []Value{nil, nil, o}, - } - a := r.newArrayObject() - a._setLengthInt(length, true) - a.values = make([]Value, length) - for k := int64(0); k < length; k++ { - idx := intToValue(k) - if val := o.self.get(idx); val != nil { - fc.Arguments[0] = val - fc.Arguments[1] = idx - a.values[k] = callbackFn(fc) - a.objCount++ + callbackFn := r.toCallable(call.Argument(0)) + fc := FunctionCall{ + This: call.Argument(1), + Arguments: []Value{nil, nil, o}, + } + a := arraySpeciesCreate(o, length) + if _, stdSrc := o.self.(*arrayObject); stdSrc { + if arr, ok := a.self.(*arrayObject); ok { + values := make([]Value, length) + for k := int64(0); k < length; k++ { + idx := intToValue(k) + if val := o.self.get(idx); val != nil { + fc.Arguments[0] = val + fc.Arguments[1] = idx + values[k] = callbackFn(fc) + } } + setArrayValues(arr, values) + return a } - return a.val - } else { - r.typeErrorResult(true, "%s is not a function", call.Argument(0)) } - panic("unreachable") + for k := int64(0); k < length; k++ { + idx := intToValue(k) + if val := o.self.get(idx); val != nil { + fc.Arguments[0] = val + fc.Arguments[1] = idx + defineDataPropertyOrThrow(a, idx, callbackFn(fc)) + } + } + return a } func (r *Runtime) arrayproto_filter(call FunctionCall) Value { @@ -785,35 +824,12 @@ func (r *Runtime) arrayproto_reverse_generic(o *Object, start int64) { func (r *Runtime) arrayproto_reverse(call FunctionCall) Value { o := call.This.ToObject(r) - if a, ok := o.self.(*arrayObject); ok { + if a := r.checkStdArrayObj(o); a != nil { l := a.length middle := l / 2 - al := int64(len(a.values)) for lower := int64(0); lower != middle; lower++ { upper := l - lower - 1 - var lowerValue, upperValue Value - if upper >= al || lower >= al { - goto bailout - } - lowerValue = a.values[lower] - if lowerValue == nil { - goto bailout - } - if _, ok := lowerValue.(*valueProperty); ok { - goto bailout - } - upperValue = a.values[upper] - if upperValue == nil { - goto bailout - } - if _, ok := upperValue.(*valueProperty); ok { - goto bailout - } - - a.values[lower], a.values[upper] = upperValue, lowerValue - continue - bailout: - arrayproto_reverse_generic_step(o, lower, upper) + a.values[lower], a.values[upper] = a.values[upper], a.values[lower] } //TODO: go arrays } else { @@ -832,7 +848,7 @@ func (r *Runtime) arrayproto_shift(call FunctionCall) Value { first := o.self.get(intToValue(0)) for i := int64(1); i < length; i++ { v := o.self.get(intToValue(i)) - if v != nil && v != _undefined { + if v != nil { o.self.put(intToValue(i-1), v, true) } else { o.self.delete(intToValue(i-1), true) @@ -850,6 +866,10 @@ func (r *Runtime) arrayproto_values(call FunctionCall) Value { return r.createArrayIterator(call.This.ToObject(r), iterationKindValue) } +func (r *Runtime) arrayproto_keys(call FunctionCall) Value { + return r.createArrayIterator(call.This.ToObject(r), iterationKindKey) +} + func (r *Runtime) arrayproto_copyWithin(call FunctionCall) Value { o := call.This.ToObject(r) l := toLength(o.self.getStr("length")) @@ -938,6 +958,26 @@ func (r *Runtime) arrayproto_find(call FunctionCall) Value { return _undefined } +func (r *Runtime) arrayproto_findIndex(call FunctionCall) Value { + o := call.This.ToObject(r) + l := toLength(o.self.getStr("length")) + predicate := r.toCallable(call.Argument(0)) + fc := FunctionCall{ + This: call.Argument(1), + Arguments: []Value{nil, nil, o}, + } + for k := int64(0); k < l; k++ { + idx := intToValue(k) + kValue := o.self.get(idx) + fc.Arguments[0], fc.Arguments[1] = kValue, idx + if predicate(fc).ToBoolean() { + return idx + } + } + + return intToValue(-1) +} + func (r *Runtime) checkStdArrayObj(obj *Object) *arrayObject { if arr, ok := obj.self.(*arrayObject); ok && arr.propValueCount == 0 && @@ -1111,35 +1151,48 @@ func (r *Runtime) createArrayProto(val *Object) objectImpl { o.init() o._putProp("constructor", r.global.Array, true, false, true) + o._putProp("concat", r.newNativeFunc(r.arrayproto_concat, nil, "concat", nil, 1), true, false, true) o._putProp("copyWithin", r.newNativeFunc(r.arrayproto_copyWithin, nil, "copyWithin", nil, 2), true, false, true) o._putProp("entries", r.newNativeFunc(r.arrayproto_entries, nil, "entries", nil, 0), true, false, true) + o._putProp("every", r.newNativeFunc(r.arrayproto_every, nil, "every", nil, 1), true, false, true) o._putProp("fill", r.newNativeFunc(r.arrayproto_fill, nil, "fill", nil, 1), true, false, true) o._putProp("filter", r.newNativeFunc(r.arrayproto_filter, nil, "filter", nil, 1), true, false, true) o._putProp("find", r.newNativeFunc(r.arrayproto_find, nil, "find", nil, 1), true, false, true) + o._putProp("findIndex", r.newNativeFunc(r.arrayproto_findIndex, nil, "findIndex", nil, 1), true, false, true) + o._putProp("forEach", r.newNativeFunc(r.arrayproto_forEach, nil, "forEach", nil, 1), true, false, true) + o._putProp("indexOf", r.newNativeFunc(r.arrayproto_indexOf, nil, "indexOf", nil, 1), true, false, true) + o._putProp("join", r.newNativeFunc(r.arrayproto_join, nil, "join", nil, 1), true, false, true) + o._putProp("keys", r.newNativeFunc(r.arrayproto_keys, nil, "keys", nil, 0), true, false, true) + o._putProp("lastIndexOf", r.newNativeFunc(r.arrayproto_lastIndexOf, nil, "lastIndexOf", nil, 1), true, false, true) + o._putProp("map", r.newNativeFunc(r.arrayproto_map, nil, "map", nil, 1), true, false, true) o._putProp("pop", r.newNativeFunc(r.arrayproto_pop, nil, "pop", nil, 0), true, false, true) o._putProp("push", r.newNativeFunc(r.arrayproto_push, nil, "push", nil, 1), true, false, true) - o._putProp("join", r.newNativeFunc(r.arrayproto_join, nil, "join", nil, 1), true, false, true) - o._putProp("toString", r.newNativeFunc(r.arrayproto_toString, nil, "toString", nil, 0), true, false, true) - o._putProp("toLocaleString", r.newNativeFunc(r.arrayproto_toLocaleString, nil, "toLocaleString", nil, 0), true, false, true) - o._putProp("concat", r.newNativeFunc(r.arrayproto_concat, nil, "concat", nil, 1), true, false, true) + o._putProp("reduce", r.newNativeFunc(r.arrayproto_reduce, nil, "reduce", nil, 1), true, false, true) + o._putProp("reduceRight", r.newNativeFunc(r.arrayproto_reduceRight, nil, "reduceRight", nil, 1), true, false, true) o._putProp("reverse", r.newNativeFunc(r.arrayproto_reverse, nil, "reverse", nil, 0), true, false, true) o._putProp("shift", r.newNativeFunc(r.arrayproto_shift, nil, "shift", nil, 0), true, false, true) o._putProp("slice", r.newNativeFunc(r.arrayproto_slice, nil, "slice", nil, 2), true, false, true) + o._putProp("some", r.newNativeFunc(r.arrayproto_some, nil, "some", nil, 1), true, false, true) o._putProp("sort", r.newNativeFunc(r.arrayproto_sort, nil, "sort", nil, 1), true, false, true) o._putProp("splice", r.newNativeFunc(r.arrayproto_splice, nil, "splice", nil, 2), true, false, true) + o._putProp("toLocaleString", r.newNativeFunc(r.arrayproto_toLocaleString, nil, "toLocaleString", nil, 0), true, false, true) + o._putProp("toString", r.newNativeFunc(r.arrayproto_toString, nil, "toString", nil, 0), true, false, true) o._putProp("unshift", r.newNativeFunc(r.arrayproto_unshift, nil, "unshift", nil, 1), true, false, true) - o._putProp("indexOf", r.newNativeFunc(r.arrayproto_indexOf, nil, "indexOf", nil, 1), true, false, true) - o._putProp("lastIndexOf", r.newNativeFunc(r.arrayproto_lastIndexOf, nil, "lastIndexOf", nil, 1), true, false, true) - o._putProp("every", r.newNativeFunc(r.arrayproto_every, nil, "every", nil, 1), true, false, true) - o._putProp("some", r.newNativeFunc(r.arrayproto_some, nil, "some", nil, 1), true, false, true) - o._putProp("forEach", r.newNativeFunc(r.arrayproto_forEach, nil, "forEach", nil, 1), true, false, true) - o._putProp("map", r.newNativeFunc(r.arrayproto_map, nil, "map", nil, 1), true, false, true) - o._putProp("reduce", r.newNativeFunc(r.arrayproto_reduce, nil, "reduce", nil, 1), true, false, true) - o._putProp("reduceRight", r.newNativeFunc(r.arrayproto_reduceRight, nil, "reduceRight", nil, 1), true, false, true) valuesFunc := r.newNativeFunc(r.arrayproto_values, nil, "values", nil, 0) + r.global.arrayValues = valuesFunc o._putProp("values", valuesFunc, true, false, true) + o.put(symIterator, valueProp(valuesFunc, true, false, true), true) - r.global.arrayValues = valuesFunc + + bl := r.NewObject() + bl.self.putStr("copyWithin", valueTrue, true) + bl.self.putStr("entries", valueTrue, true) + bl.self.putStr("fill", valueTrue, true) + bl.self.putStr("find", valueTrue, true) + bl.self.putStr("findIndex", valueTrue, true) + bl.self.putStr("keys", valueTrue, true) + bl.self.putStr("values", valueTrue, true) + o.put(symUnscopables, valueProp(bl, false, false, true), true) return o } diff --git a/tc39_test.go b/tc39_test.go index 88e81e8c..d3920cb3 100644 --- a/tc39_test.go +++ b/tc39_test.go @@ -32,6 +32,9 @@ var ( "test/built-ins/Date/prototype/toISOString/15.9.5.43-0-10.js": true, // timezone "test/annexB/built-ins/escape/escape-above-astral.js": true, // \u{xxxxx} + // utf-16 + "test/built-ins/Array/prototype/concat/Array.prototype.concat_spreadable-string-wrapper.js": true, + // cross-realm "test/built-ins/Symbol/unscopables/cross-realm.js": true, "test/built-ins/Symbol/toStringTag/cross-realm.js": true, @@ -57,6 +60,12 @@ var ( "test/built-ins/Array/prototype/concat/create-proto-from-ctor-realm-array.js": true, "test/built-ins/Array/prototype/filter/create-proto-from-ctor-realm-non-array.js": true, "test/built-ins/Array/prototype/filter/create-proto-from-ctor-realm-array.js": true, + "test/built-ins/Array/prototype/map/create-proto-from-ctor-realm-non-array.js": true, + "test/built-ins/Array/prototype/map/create-proto-from-ctor-realm-array.js": true, + "test/built-ins/Array/prototype/slice/create-proto-from-ctor-realm-non-array.js": true, + "test/built-ins/Array/prototype/slice/create-proto-from-ctor-realm-array.js": true, + "test/built-ins/Array/prototype/splice/create-proto-from-ctor-realm-non-array.js": true, + "test/built-ins/Array/prototype/splice/create-proto-from-ctor-realm-array.js": true, // class "test/language/statements/class/subclass/builtin-objects/Symbol/symbol-valid-as-extends-value.js": true, @@ -71,6 +80,8 @@ var ( "test/language/statements/class/subclass/builtin-objects/Set/regular-subclassing.js": true, "test/language/statements/class/subclass/builtin-objects/Object/replacing-prototype.js": true, "test/language/statements/class/subclass/builtin-objects/Object/regular-subclassing.js": true, + "test/built-ins/Array/prototype/concat/Array.prototype.concat_non-array.js": true, + "test/language/statements/class/subclass/builtin-objects/Array/length.js": true, // full unicode regexp flag "test/built-ins/RegExp/prototype/Symbol.match/u-advance-after-empty.js": true, @@ -83,10 +94,14 @@ var ( "test/built-ins/Array/from/source-object-iterator-2.js": true, // Typed arrays - "test/built-ins/Array/from/items-is-arraybuffer.js": true, + "test/built-ins/Array/from/items-is-arraybuffer.js": true, + "test/built-ins/Array/prototype/concat/Array.prototype.concat_small-typed-array.js": true, + "test/built-ins/Array/prototype/concat/Array.prototype.concat_large-typed-array.js": true, // for-of - "test/language/statements/for-of/Array.prototype.entries.js": true, + "test/language/statements/for-of/Array.prototype.keys.js": true, + "test/language/statements/for-of/Array.prototype.entries.js": true, + "test/language/statements/for-of/Array.prototype.Symbol.iterator.js": true, } featuresBlackList = []string{ @@ -108,14 +123,8 @@ var ( "22.1.2.1", "22.1.2.3", "22.1.2.5", - "22.1.3.1", - "22.1.3.3", - "22.1.3.4", - "22.1.3.5", - "22.1.3.6", - "22.1.3.7", - "22.1.3.8", - "22.1.3.29", + "22.1.3", + "22.1.4", "23.1", "23.2", "23.3", From a13c43b62b610fc21fc8928aada8be257962d63b Mon Sep 17 00:00:00 2001 From: Dmitry Panov Date: Tue, 24 Mar 2020 15:09:28 +0000 Subject: [PATCH 13/46] Proxy (#135) Proxy, Reflect, new.target, tons of refactoring. Co-authored-by: noctarius --- array.go | 515 ++++++++++-------- array_sparse.go | 349 ++++++------ array_sparse_test.go | 83 ++- ast/node.go | 10 + builtin_array.go | 350 ++++++------ builtin_date.go | 28 +- builtin_error.go | 1 - builtin_function.go | 77 +-- builtin_json.go | 78 +-- builtin_map.go | 30 +- builtin_math.go | 6 +- builtin_number.go | 16 +- builtin_object.go | 296 +++++----- builtin_proxy.go | 281 ++++++++++ builtin_proxy_test.go | 828 ++++++++++++++++++++++++++++ builtin_reflect.go | 132 +++++ builtin_regexp.go | 96 ++-- builtin_set.go | 18 +- builtin_string.go | 25 +- builtin_symbol.go | 4 +- builtin_weakmap.go | 20 +- builtin_weakset.go | 20 +- builtin_weakset_test.go | 6 +- compiler_expr.go | 37 +- compiler_test.go | 2 +- date.go | 4 +- func.go | 114 ++-- object.go | 948 +++++++++++++++++++++++++-------- object_args.go | 79 ++- object_gomap.go | 156 ++---- object_gomap_reflect.go | 195 ++++--- object_gomap_reflect_test.go | 87 ++- object_gomap_test.go | 124 +++++ object_goreflect.go | 178 +++---- object_goreflect_test.go | 117 +++- object_goslice.go | 305 ++++++----- object_goslice_reflect.go | 308 ++++++----- object_goslice_reflect_test.go | 148 ++++- object_goslice_test.go | 103 +++- object_lazy.go | 158 ++++-- object_test.go | 62 ++- parser/expression.go | 17 + proxy.go | 764 ++++++++++++++++++++++++++ regexp.go | 10 +- runtime.go | 296 ++++++---- runtime_test.go | 69 +++ string.go | 144 ++--- string_ascii.go | 22 +- string_unicode.go | 26 +- tc39_test.go | 94 ++-- value.go | 216 ++------ vm.go | 224 ++++---- vm_test.go | 56 +- 53 files changed, 5857 insertions(+), 2475 deletions(-) create mode 100644 builtin_proxy.go create mode 100644 builtin_proxy_test.go create mode 100644 builtin_reflect.go create mode 100644 proxy.go diff --git a/array.go b/array.go index 53f777e9..5d795ba2 100644 --- a/array.go +++ b/array.go @@ -2,6 +2,7 @@ package goja import ( "math" + "math/bits" "reflect" "strconv" ) @@ -17,18 +18,18 @@ func (ai *arrayIterObject) next() Value { if ai.obj == nil { return ai.val.runtime.createIterResultObject(_undefined, true) } - l := toLength(ai.obj.self.getStr("length")) + l := toLength(ai.obj.self.getStr("length", nil)) index := ai.nextIdx if index >= l { ai.obj = nil return ai.val.runtime.createIterResultObject(_undefined, true) } ai.nextIdx++ - idxVal := intToValue(index) + idxVal := valueInt(index) if ai.kind == iterationKindKey { return ai.val.runtime.createIterResultObject(idxVal, false) } - elementValue := ai.obj.self.get(idxVal) + elementValue := ai.obj.self.getIdx(idxVal, nil) var result Value if ai.kind == iterationKindValue { result = elementValue @@ -58,8 +59,8 @@ func (r *Runtime) createArrayIterator(iterObj *Object, kind iterationKind) Value type arrayObject struct { baseObject values []Value - length int64 - objCount int64 + length uint32 + objCount int propValueCount int lengthProp valueProperty } @@ -73,20 +74,15 @@ func (a *arrayObject) init() { func (a *arrayObject) _setLengthInt(l int64, throw bool) bool { if l >= 0 && l <= math.MaxUint32 { + l := uint32(l) ret := true if l <= a.length { if a.propValueCount > 0 { // Slow path - var s int64 - if a.length < int64(len(a.values)) { - s = a.length - 1 - } else { - s = int64(len(a.values)) - 1 - } - for i := s; i >= l; i-- { + for i := len(a.values) - 1; i >= int(l); i-- { if prop, ok := a.values[i].(*valueProperty); ok { if !prop.configurable { - l = i + 1 + l = uint32(i) + 1 ret = false break } @@ -95,8 +91,8 @@ func (a *arrayObject) _setLengthInt(l int64, throw bool) bool { } } } - if l <= int64(len(a.values)) { - if l >= 16 && l < int64(cap(a.values))>>2 { + if l <= uint32(len(a.values)) { + if l >= 16 && l < uint32(cap(a.values))>>2 { ar := make([]Value, l) copy(ar, a.values) a.values = ar @@ -118,7 +114,7 @@ func (a *arrayObject) _setLengthInt(l int64, throw bool) bool { } func (a *arrayObject) setLengthInt(l int64, throw bool) bool { - if l == a.length { + if l == int64(a.length) { return true } if !a.lengthProp.writable { @@ -130,7 +126,7 @@ func (a *arrayObject) setLengthInt(l int64, throw bool) bool { func (a *arrayObject) setLength(v Value, throw bool) bool { l, ok := toIntIgnoreNegZero(v) - if ok && l == a.length { + if ok && l == int64(a.length) { return true } if !a.lengthProp.writable { @@ -143,18 +139,46 @@ func (a *arrayObject) setLength(v Value, throw bool) bool { panic(a.val.runtime.newError(a.val.runtime.global.RangeError, "Invalid array length")) } -func (a *arrayObject) getIdx(idx int64, origNameStr string, origName Value) (v Value) { - if idx >= 0 && idx < int64(len(a.values)) { - v = a.values[idx] +func (a *arrayObject) getIdx(idx valueInt, receiver Value) Value { + prop := a.getOwnPropIdx(idx) + if prop == nil { + if a.prototype != nil { + if receiver == nil { + return a.prototype.self.getIdx(idx, a.val) + } + return a.prototype.self.getIdx(idx, receiver) + } } - if v == nil && a.prototype != nil { - if origName != nil { - v = a.prototype.self.getProp(origName) - } else { - v = a.prototype.self.getPropStr(origNameStr) + if prop, ok := prop.(*valueProperty); ok { + if receiver == nil { + return prop.get(a.val) + } + return prop.get(receiver) + } + return prop +} + +func (a *arrayObject) getOwnPropStr(name string) Value { + if i := strToIdx(name); i != math.MaxUint32 { + if i < uint32(len(a.values)) { + return a.values[i] + } + } + if name == "length" { + return a.getLengthProp() + } + return a.baseObject.getOwnPropStr(name) +} + +func (a *arrayObject) getOwnPropIdx(idx valueInt) Value { + if i := toIdx(idx); i != math.MaxUint32 { + if i < uint32(len(a.values)) { + return a.values[i] } + return nil } - return + + return a.baseObject.getOwnPropStr(idx.String()) } func (a *arrayObject) sortLen() int64 { @@ -173,159 +197,91 @@ func (a *arrayObject) swap(i, j int64) { a.values[i], a.values[j] = a.values[j], a.values[i] } -func toIdx(v Value) (idx int64) { - idx = -1 - if idxVal, ok1 := v.(valueInt); ok1 { - idx = int64(idxVal) - } else { - if _, ok := v.(*valueSymbol); ok { - return -1 - } - if i, err := strconv.ParseInt(v.String(), 10, 64); err == nil { - idx = i - } - } - if idx >= 0 && idx < math.MaxUint32 { - return - } - return -1 -} - -func strToIdx(s string) (idx int64) { - idx = -1 - if i, err := strconv.ParseInt(s, 10, 64); err == nil { - idx = i - } - - if idx >= 0 && idx < math.MaxUint32 { - return - } - return -1 -} - -func (a *arrayObject) getProp(n Value) Value { - if idx := toIdx(n); idx >= 0 { - return a.getIdx(idx, "", n) - } - if _, ok := n.(*valueSymbol); !ok { - if n.String() == "length" { - return a.getLengthProp() - } - } - return a.baseObject.getProp(n) +func (a *arrayObject) getStr(name string, receiver Value) Value { + return a.getStrWithOwnProp(a.getOwnPropStr(name), name, receiver) } func (a *arrayObject) getLengthProp() Value { - a.lengthProp.value = intToValue(a.length) + a.lengthProp.value = intToValue(int64(a.length)) return &a.lengthProp } -func (a *arrayObject) getPropStr(name string) Value { - if i := strToIdx(name); i >= 0 { - return a.getIdx(i, name, nil) - } - if name == "length" { - return a.getLengthProp() - } - return a.baseObject.getPropStr(name) -} - -func (a *arrayObject) getOwnPropStr(name string) Value { - if i := strToIdx(name); i >= 0 { - if i >= 0 && i < int64(len(a.values)) { - return a.values[i] - } - } - if name == "length" { - return a.getLengthProp() +func (a *arrayObject) setOwnIdx(idx valueInt, val Value, throw bool) bool { + if i := toIdx(idx); i != math.MaxUint32 { + return a._setOwnIdx(i, val, throw) + } else { + return a.baseObject.setOwnStr(idx.String(), val, throw) } - return a.baseObject.getOwnPropStr(name) } -func (a *arrayObject) putIdx(idx int64, val Value, throw bool, origNameStr string, origName Value) { +func (a *arrayObject) _setOwnIdx(idx uint32, val Value, throw bool) bool { var prop Value - if idx < int64(len(a.values)) { + if idx < uint32(len(a.values)) { prop = a.values[idx] } if prop == nil { - if a.prototype != nil { - var pprop Value - if origName != nil { - pprop = a.prototype.self.getProp(origName) - } else { - pprop = a.prototype.self.getPropStr(origNameStr) - } - if pprop, ok := pprop.(*valueProperty); ok { - if !pprop.isWritable() { - a.val.runtime.typeErrorResult(throw) - return - } - if pprop.accessor { - pprop.set(a.val, val) - return - } + if proto := a.prototype; proto != nil { + // we know it's foreign because prototype loops are not allowed + if res, ok := proto.self.setForeignIdx(valueInt(idx), val, a.val, throw); ok { + return res } } - + // new property if !a.extensible { - a.val.runtime.typeErrorResult(throw) - return - } - if idx >= a.length { - if !a.setLengthInt(idx+1, throw) { - return + a.val.runtime.typeErrorResult(throw, "Cannot add property %d, object is not extensible", idx) + return false + } else { + if idx >= a.length { + if !a.setLengthInt(int64(idx)+1, throw) { + return false + } } - } - if idx >= int64(len(a.values)) { - if !a.expand(idx) { - a.val.self.(*sparseArrayObject).putIdx(idx, val, throw, origNameStr, origName) - return + if idx >= uint32(len(a.values)) { + if !a.expand(idx) { + a.val.self.(*sparseArrayObject).add(idx, val) + return true + } } + a.objCount++ } } else { if prop, ok := prop.(*valueProperty); ok { if !prop.isWritable() { a.val.runtime.typeErrorResult(throw) - return + return false } prop.set(a.val, val) - return + return true } } - a.values[idx] = val - a.objCount++ + return true } -func (a *arrayObject) put(n Value, val Value, throw bool) { - if idx := toIdx(n); idx >= 0 { - a.putIdx(idx, val, throw, "", n) +func (a *arrayObject) setOwnStr(name string, val Value, throw bool) bool { + if idx := strToIdx(name); idx != math.MaxUint32 { + return a._setOwnIdx(idx, val, throw) } else { - if n.String() == "length" { - a.setLength(val, throw) + if name == "length" { + return a.setLength(val, throw) } else { - a.baseObject.put(n, val, throw) + return a.baseObject.setOwnStr(name, val, throw) } } } -func (a *arrayObject) putStr(name string, val Value, throw bool) { - if idx := strToIdx(name); idx >= 0 { - a.putIdx(idx, val, throw, name, nil) - } else { - if name == "length" { - a.setLength(val, throw) - } else { - a.baseObject.putStr(name, val, throw) - } - } +func (a *arrayObject) setForeignIdx(idx valueInt, val, receiver Value, throw bool) (bool, bool) { + return a._setForeignIdx(idx, a.getOwnPropIdx(idx), val, receiver, throw) +} + +func (a *arrayObject) setForeignStr(name string, val, receiver Value, throw bool) (bool, bool) { + return a._setForeignStr(name, a.getOwnPropStr(name), val, receiver, throw) } type arrayPropIter struct { - a *arrayObject - recursive bool - idx int + a *arrayObject + idx int } func (i *arrayPropIter) next() (propIterItem, iterNextFunc) { @@ -338,74 +294,85 @@ func (i *arrayPropIter) next() (propIterItem, iterNextFunc) { } } - return i.a.baseObject._enumerate(i.recursive)() + return i.a.baseObject.enumerateUnfiltered()() } -func (a *arrayObject) _enumerate(recursive bool) iterNextFunc { +func (a *arrayObject) enumerateUnfiltered() iterNextFunc { return (&arrayPropIter{ - a: a, - recursive: recursive, + a: a, }).next } -func (a *arrayObject) enumerate(all, recursive bool) iterNextFunc { - return (&propFilterIter{ - wrapped: a._enumerate(recursive), - all: all, - seen: make(map[string]bool), - }).next -} - -func (a *arrayObject) hasOwnProperty(n Value) bool { - if idx := toIdx(n); idx >= 0 { - return idx < int64(len(a.values)) && a.values[idx] != nil - } else { - return a.baseObject.hasOwnProperty(n) +func (a *arrayObject) ownKeys(all bool, accum []Value) []Value { + for i, prop := range a.values { + name := strconv.Itoa(i) + if prop != nil { + if !all { + if prop, ok := prop.(*valueProperty); ok && !prop.enumerable { + continue + } + } + accum = append(accum, asciiString(name)) + } } + return a.baseObject.ownKeys(all, accum) } func (a *arrayObject) hasOwnPropertyStr(name string) bool { - if idx := strToIdx(name); idx >= 0 { - return idx < int64(len(a.values)) && a.values[idx] != nil + if idx := strToIdx(name); idx != math.MaxUint32 { + return idx < uint32(len(a.values)) && a.values[idx] != nil } else { return a.baseObject.hasOwnPropertyStr(name) } } -func (a *arrayObject) expand(idx int64) bool { +func (a *arrayObject) hasOwnPropertyIdx(idx valueInt) bool { + if idx := toIdx(idx); idx != math.MaxUint32 { + return idx < uint32(len(a.values)) && a.values[idx] != nil + } + return a.baseObject.hasOwnPropertyStr(idx.String()) +} + +func (a *arrayObject) expand(idx uint32) bool { targetLen := idx + 1 - if targetLen > int64(len(a.values)) { - if targetLen < int64(cap(a.values)) { + if targetLen > uint32(len(a.values)) { + if targetLen < uint32(cap(a.values)) { a.values = a.values[:targetLen] } else { - if idx > 4096 && (a.objCount == 0 || idx/a.objCount > 10) { + if idx > 4096 && (a.objCount == 0 || idx/uint32(a.objCount) > 10) { //log.Println("Switching standard->sparse") sa := &sparseArrayObject{ baseObject: a.baseObject, - length: a.length, + length: uint32(a.length), propValueCount: a.propValueCount, } - sa.setValues(a.values) + sa.setValues(a.values, a.objCount+1) sa.val.self = sa sa.init() sa.lengthProp.writable = a.lengthProp.writable return false } else { + if bits.UintSize == 32 { + if targetLen >= math.MaxInt32 { + panic(a.val.runtime.NewTypeError("Array index overflows int")) + } + } + tl := int(targetLen) // Use the same algorithm as in runtime.growSlice - newcap := int64(cap(a.values)) + newcap := cap(a.values) doublecap := newcap + newcap - if targetLen > doublecap { - newcap = targetLen + if tl > doublecap { + newcap = tl } else { if len(a.values) < 1024 { newcap = doublecap } else { - for newcap < targetLen { + for newcap < tl { newcap += newcap / 4 } } } - newValues := make([]Value, targetLen, newcap) + newValues := make([]Value, tl, newcap) copy(newValues, a.values) a.values = newValues } @@ -414,7 +381,7 @@ func (a *arrayObject) expand(idx int64) bool { return true } -func (r *Runtime) defineArrayLength(prop *valueProperty, descr propertyDescr, setter func(Value, bool) bool, throw bool) bool { +func (r *Runtime) defineArrayLength(prop *valueProperty, descr PropertyDescriptor, setter func(Value, bool) bool, throw bool) bool { ret := true if descr.Configurable == FLAG_TRUE || descr.Enumerable == FLAG_TRUE || descr.Getter != nil || descr.Setter != nil { @@ -448,40 +415,50 @@ Reject: return ret } -func (a *arrayObject) defineOwnProperty(n Value, descr propertyDescr, throw bool) bool { - if idx := toIdx(n); idx >= 0 { - var existing Value - if idx < int64(len(a.values)) { - existing = a.values[idx] - } - prop, ok := a.baseObject._defineOwnProperty(n, existing, descr, throw) - if ok { - if idx >= a.length { - if !a.setLengthInt(idx+1, throw) { - return false - } - } - if a.expand(idx) { - a.values[idx] = prop - a.objCount++ - if _, ok := prop.(*valueProperty); ok { - a.propValueCount++ - } - } else { - a.val.self.(*sparseArrayObject).putIdx(idx, prop, throw, "", nil) +func (a *arrayObject) _defineIdxProperty(idx uint32, desc PropertyDescriptor, throw bool) bool { + var existing Value + if idx < uint32(len(a.values)) { + existing = a.values[idx] + } + prop, ok := a.baseObject._defineOwnProperty(strconv.FormatUint(uint64(idx), 10), existing, desc, throw) + if ok { + if idx >= uint32(a.length) { + if !a.setLengthInt(int64(idx)+1, throw) { + return false } } - return ok - } else { - if n.String() == "length" { - return a.val.runtime.defineArrayLength(&a.lengthProp, descr, a.setLength, throw) + if a.expand(idx) { + a.values[idx] = prop + a.objCount++ + if _, ok := prop.(*valueProperty); ok { + a.propValueCount++ + } + } else { + a.val.self.(*sparseArrayObject).add(uint32(idx), prop) } - return a.baseObject.defineOwnProperty(n, descr, throw) } + return ok } -func (a *arrayObject) _deleteProp(idx int64, throw bool) bool { - if idx < int64(len(a.values)) { +func (a *arrayObject) defineOwnPropertyStr(name string, descr PropertyDescriptor, throw bool) bool { + if idx := strToIdx(name); idx != math.MaxUint32 { + return a._defineIdxProperty(idx, descr, throw) + } + if name == "length" { + return a.val.runtime.defineArrayLength(&a.lengthProp, descr, a.setLength, throw) + } + return a.baseObject.defineOwnPropertyStr(name, descr, throw) +} + +func (a *arrayObject) defineOwnPropertyIdx(idx valueInt, descr PropertyDescriptor, throw bool) bool { + if idx := toIdx(idx); idx != math.MaxUint32 { + return a._defineIdxProperty(idx, descr, throw) + } + return a.baseObject.defineOwnPropertyStr(idx.String(), descr, throw) +} + +func (a *arrayObject) _deleteIdxProp(idx uint32, throw bool) bool { + if idx < uint32(len(a.values)) { if v := a.values[idx]; v != nil { if p, ok := v.(*valueProperty); ok { if !p.configurable { @@ -497,18 +474,18 @@ func (a *arrayObject) _deleteProp(idx int64, throw bool) bool { return true } -func (a *arrayObject) delete(n Value, throw bool) bool { - if idx := toIdx(n); idx >= 0 { - return a._deleteProp(idx, throw) +func (a *arrayObject) deleteStr(name string, throw bool) bool { + if idx := strToIdx(name); idx != math.MaxUint32 { + return a._deleteIdxProp(idx, throw) } - return a.baseObject.delete(n, throw) + return a.baseObject.deleteStr(name, throw) } -func (a *arrayObject) deleteStr(name string, throw bool) bool { - if idx := strToIdx(name); idx >= 0 { - return a._deleteProp(idx, throw) +func (a *arrayObject) deleteIdx(idx valueInt, throw bool) bool { + if idx := toIdx(idx); idx != math.MaxUint32 { + return a._deleteIdxProp(idx, throw) } - return a.baseObject.deleteStr(name, throw) + return a.baseObject.deleteStr(idx.String(), throw) } func (a *arrayObject) export() interface{} { @@ -526,10 +503,130 @@ func (a *arrayObject) exportType() reflect.Type { return reflectTypeArray } -func (a *arrayObject) setValuesFromSparse(items []sparseArrayItem) { - a.values = make([]Value, int(items[len(items)-1].idx+1)) +func (a *arrayObject) setValuesFromSparse(items []sparseArrayItem, newMaxIdx int) { + a.values = make([]Value, newMaxIdx+1) for _, item := range items { a.values[item.idx] = item.value } - a.objCount = int64(len(items)) + a.objCount = len(items) +} + +func toIdx(v valueInt) uint32 { + if v >= 0 && v < math.MaxUint32 { + return uint32(v) + } + return math.MaxUint32 +} + +func strToIdx64(s string) int64 { + if s == "" { + return -1 + } + l := len(s) + if s[0] == '0' { + if l == 1 { + return 0 + } + return -1 + } + var n int64 + if l < 19 { + // guaranteed not to overflow + for i := 0; i < len(s); i++ { + c := s[i] + if c < '0' || c > '9' { + return -1 + } + n = n*10 + int64(c-'0') + } + return n + } + if l > 19 { + // guaranteed to overflow + return -1 + } + c18 := s[18] + if c18 < '0' || c18 > '9' { + return -1 + } + for i := 0; i < 18; i++ { + c := s[i] + if c < '0' || c > '9' { + return -1 + } + n = n*10 + int64(c-'0') + } + if n >= math.MaxInt64/10+1 { + return -1 + } + n *= 10 + n1 := n + int64(c18-'0') + if n1 < n { + return -1 + } + return n1 +} + +func strToIdx(s string) uint32 { + if s == "" { + return math.MaxUint32 + } + l := len(s) + if s[0] == '0' { + if l == 1 { + return 0 + } + return math.MaxUint32 + } + var n uint32 + if l < 10 { + // guaranteed not to overflow + for i := 0; i < len(s); i++ { + c := s[i] + if c < '0' || c > '9' { + return math.MaxUint32 + } + n = n*10 + uint32(c-'0') + } + return n + } + if l > 10 { + // guaranteed to overflow + return math.MaxUint32 + } + c9 := s[9] + if c9 < '0' || c9 > '9' { + return math.MaxUint32 + } + for i := 0; i < 9; i++ { + c := s[i] + if c < '0' || c > '9' { + return math.MaxUint32 + } + n = n*10 + uint32(c-'0') + } + if n >= math.MaxUint32/10+1 { + return math.MaxUint32 + } + n *= 10 + n1 := n + uint32(c9-'0') + if n1 < n { + return math.MaxUint32 + } + + return n1 +} + +func strToGoIdx(s string) int { + if bits.UintSize == 64 { + return int(strToIdx64(s)) + } + i := strToIdx(s) + if i == math.MaxUint32 { + return -1 + } + if i >= math.MaxInt32 { + return -1 + } + return int(i) } diff --git a/array_sparse.go b/array_sparse.go index a454ba58..69456b6d 100644 --- a/array_sparse.go +++ b/array_sparse.go @@ -2,20 +2,21 @@ package goja import ( "math" + "math/bits" "reflect" "sort" "strconv" ) type sparseArrayItem struct { - idx int64 + idx uint32 value Value } type sparseArrayObject struct { baseObject items []sparseArrayItem - length int64 + length uint32 propValueCount int lengthProp valueProperty } @@ -27,7 +28,7 @@ func (a *sparseArrayObject) init() { a._put("length", &a.lengthProp) } -func (a *sparseArrayObject) findIdx(idx int64) int { +func (a *sparseArrayObject) findIdx(idx uint32) int { return sort.Search(len(a.items), func(i int) bool { return a.items[i].idx >= idx }) @@ -36,7 +37,7 @@ func (a *sparseArrayObject) findIdx(idx int64) int { func (a *sparseArrayObject) _setLengthInt(l int64, throw bool) bool { if l >= 0 && l <= math.MaxUint32 { ret := true - + l := uint32(l) if l <= a.length { if a.propValueCount > 0 { // Slow path @@ -74,7 +75,7 @@ func (a *sparseArrayObject) _setLengthInt(l int64, throw bool) bool { } func (a *sparseArrayObject) setLengthInt(l int64, throw bool) bool { - if l == a.length { + if l == int64(a.length) { return true } if !a.lengthProp.writable { @@ -86,7 +87,7 @@ func (a *sparseArrayObject) setLengthInt(l int64, throw bool) bool { func (a *sparseArrayObject) setLength(v Value, throw bool) bool { l, ok := toIntIgnoreNegZero(v) - if ok && l == a.length { + if ok && l == int64(a.length) { return true } if !a.lengthProp.writable { @@ -99,48 +100,46 @@ func (a *sparseArrayObject) setLength(v Value, throw bool) bool { panic(a.val.runtime.newError(a.val.runtime.global.RangeError, "Invalid array length")) } -func (a *sparseArrayObject) getIdx(idx int64, origNameStr string, origName Value) (v Value) { +func (a *sparseArrayObject) _getIdx(idx uint32) Value { i := a.findIdx(idx) if i < len(a.items) && a.items[i].idx == idx { return a.items[i].value } - if a.prototype != nil { - if origName != nil { - v = a.prototype.self.getProp(origName) - } else { - v = a.prototype.self.getPropStr(origNameStr) - } - } - return + return nil } -func (a *sparseArrayObject) getProp(n Value) Value { - if idx := toIdx(n); idx >= 0 { - return a.getIdx(idx, "", n) - } +func (a *sparseArrayObject) getStr(name string, receiver Value) Value { + return a.getStrWithOwnProp(a.getOwnPropStr(name), name, receiver) +} - if _, ok := n.(*valueSymbol); !ok { - if n.String() == "length" { - return a.getLengthProp() +func (a *sparseArrayObject) getIdx(idx valueInt, receiver Value) Value { + prop := a.getOwnPropIdx(idx) + if prop == nil { + if a.prototype != nil { + if receiver == nil { + return a.prototype.self.getIdx(idx, a.val) + } + return a.prototype.self.getIdx(idx, receiver) } } - - return a.baseObject.getProp(n) + if prop, ok := prop.(*valueProperty); ok { + if receiver == nil { + return prop.get(a.val) + } + return prop.get(receiver) + } + return prop } func (a *sparseArrayObject) getLengthProp() Value { - a.lengthProp.value = intToValue(a.length) + a.lengthProp.value = intToValue(int64(a.length)) return &a.lengthProp } func (a *sparseArrayObject) getOwnPropStr(name string) Value { - if idx := strToIdx(name); idx >= 0 { - i := a.findIdx(idx) - if i < len(a.items) && a.items[i].idx == idx { - return a.items[i].value - } - return nil + if idx := strToIdx(name); idx != math.MaxUint32 { + return a._getIdx(idx) } if name == "length" { return a.getLengthProp() @@ -148,17 +147,24 @@ func (a *sparseArrayObject) getOwnPropStr(name string) Value { return a.baseObject.getOwnPropStr(name) } -func (a *sparseArrayObject) getPropStr(name string) Value { - if i := strToIdx(name); i >= 0 { - return a.getIdx(i, name, nil) +func (a *sparseArrayObject) getOwnPropIdx(idx valueInt) Value { + if idx := toIdx(idx); idx != math.MaxUint32 { + return a._getIdx(idx) } - if name == "length" { - return a.getLengthProp() + return a.baseObject.getOwnPropStr(idx.String()) +} + +func (a *sparseArrayObject) add(idx uint32, val Value) { + i := a.findIdx(idx) + a.items = append(a.items, sparseArrayItem{}) + copy(a.items[i+1:], a.items[i:]) + a.items[i] = sparseArrayItem{ + idx: idx, + value: val, } - return a.baseObject.getPropStr(name) } -func (a *sparseArrayObject) putIdx(idx int64, val Value, throw bool, origNameStr string, origName Value) { +func (a *sparseArrayObject) _setOwnIdx(idx uint32, val Value, throw bool) bool { var prop Value i := a.findIdx(idx) if i < len(a.items) && a.items[i].idx == idx { @@ -166,37 +172,26 @@ func (a *sparseArrayObject) putIdx(idx int64, val Value, throw bool, origNameStr } if prop == nil { - if a.prototype != nil { - var pprop Value - if origName != nil { - pprop = a.prototype.self.getProp(origName) - } else { - pprop = a.prototype.self.getPropStr(origNameStr) - } - if pprop, ok := pprop.(*valueProperty); ok { - if !pprop.isWritable() { - a.val.runtime.typeErrorResult(throw) - return - } - if pprop.accessor { - pprop.set(a.val, val) - return - } + if proto := a.prototype; proto != nil { + // we know it's foreign because prototype loops are not allowed + if res, ok := proto.self.setForeignIdx(valueInt(idx), val, a.val, throw); ok { + return res } } + // new property if !a.extensible { - a.val.runtime.typeErrorResult(throw) - return + a.val.runtime.typeErrorResult(throw, "Cannot add property %d, object is not extensible", idx) + return false } if idx >= a.length { - if !a.setLengthInt(idx+1, throw) { - return + if !a.setLengthInt(int64(idx)+1, throw) { + return false } } - if a.expand() { + if a.expand(idx) { a.items = append(a.items, sparseArrayItem{}) copy(a.items[i+1:], a.items[i:]) a.items[i] = sparseArrayItem{ @@ -204,52 +199,56 @@ func (a *sparseArrayObject) putIdx(idx int64, val Value, throw bool, origNameStr value: val, } } else { - a.val.self.(*arrayObject).putIdx(idx, val, throw, origNameStr, origName) - return + ar := a.val.self.(*arrayObject) + ar.values[idx] = val + ar.objCount++ + return true } } else { if prop, ok := prop.(*valueProperty); ok { if !prop.isWritable() { a.val.runtime.typeErrorResult(throw) - return + return false } prop.set(a.val, val) - return } else { a.items[i].value = val } } - + return true } -func (a *sparseArrayObject) put(n Value, val Value, throw bool) { - if idx := toIdx(n); idx >= 0 { - a.putIdx(idx, val, throw, "", n) +func (a *sparseArrayObject) setOwnStr(name string, val Value, throw bool) bool { + if idx := strToIdx(name); idx != math.MaxUint32 { + return a._setOwnIdx(idx, val, throw) } else { - if n.String() == "length" { - a.setLength(val, throw) + if name == "length" { + return a.setLength(val, throw) } else { - a.baseObject.put(n, val, throw) + return a.baseObject.setOwnStr(name, val, throw) } } } -func (a *sparseArrayObject) putStr(name string, val Value, throw bool) { - if idx := strToIdx(name); idx >= 0 { - a.putIdx(idx, val, throw, name, nil) - } else { - if name == "length" { - a.setLength(val, throw) - } else { - a.baseObject.putStr(name, val, throw) - } +func (a *sparseArrayObject) setOwnIdx(idx valueInt, val Value, throw bool) bool { + if idx := toIdx(idx); idx != math.MaxUint32 { + return a._setOwnIdx(idx, val, throw) } + + return a.baseObject.setOwnStr(idx.String(), val, throw) +} + +func (a *sparseArrayObject) setForeignStr(name string, val, receiver Value, throw bool) (bool, bool) { + return a._setForeignStr(name, a.getOwnPropStr(name), val, receiver, throw) +} + +func (a *sparseArrayObject) setForeignIdx(name valueInt, val, receiver Value, throw bool) (bool, bool) { + return a._setForeignIdx(name, a.getOwnPropIdx(name), val, receiver, throw) } type sparseArrayPropIter struct { - a *sparseArrayObject - recursive bool - idx int + a *sparseArrayObject + idx int } func (i *sparseArrayPropIter) next() (propIterItem, iterNextFunc) { @@ -262,70 +261,75 @@ func (i *sparseArrayPropIter) next() (propIterItem, iterNextFunc) { } } - return i.a.baseObject._enumerate(i.recursive)() + return i.a.baseObject.enumerateUnfiltered()() } -func (a *sparseArrayObject) _enumerate(recursive bool) iterNextFunc { +func (a *sparseArrayObject) enumerateUnfiltered() iterNextFunc { return (&sparseArrayPropIter{ - a: a, - recursive: recursive, + a: a, }).next } -func (a *sparseArrayObject) enumerate(all, recursive bool) iterNextFunc { - return (&propFilterIter{ - wrapped: a._enumerate(recursive), - all: all, - seen: make(map[string]bool), - }).next +func (a *sparseArrayObject) ownKeys(all bool, accum []Value) []Value { + if all { + for _, item := range a.items { + accum = append(accum, asciiString(strconv.FormatUint(uint64(item.idx), 10))) + } + } else { + for _, item := range a.items { + if prop, ok := item.value.(*valueProperty); ok && !prop.enumerable { + continue + } + accum = append(accum, asciiString(strconv.FormatUint(uint64(item.idx), 10))) + } + } + + return a.baseObject.ownKeys(all, accum) } -func (a *sparseArrayObject) setValues(values []Value) { - a.items = nil +func (a *sparseArrayObject) setValues(values []Value, objCount int) { + a.items = make([]sparseArrayItem, 0, objCount) for i, val := range values { if val != nil { a.items = append(a.items, sparseArrayItem{ - idx: int64(i), + idx: uint32(i), value: val, }) } } } -func (a *sparseArrayObject) hasOwnProperty(n Value) bool { - if idx := toIdx(n); idx >= 0 { +func (a *sparseArrayObject) hasOwnPropertyStr(name string) bool { + if idx := strToIdx(name); idx != math.MaxUint32 { i := a.findIdx(idx) - if i < len(a.items) && a.items[i].idx == idx { - return a.items[i].value != _undefined - } - return false + return i < len(a.items) && a.items[i].idx == idx } else { - return a.baseObject.hasOwnProperty(n) + return a.baseObject.hasOwnPropertyStr(name) } } -func (a *sparseArrayObject) hasOwnPropertyStr(name string) bool { - if idx := strToIdx(name); idx >= 0 { +func (a *sparseArrayObject) hasOwnPropertyIdx(idx valueInt) bool { + if idx := toIdx(idx); idx != math.MaxUint32 { i := a.findIdx(idx) - if i < len(a.items) && a.items[i].idx == idx { - return a.items[i].value != _undefined - } - return false - } else { - return a.baseObject.hasOwnPropertyStr(name) + return i < len(a.items) && a.items[i].idx == idx } + + return a.baseObject.hasOwnPropertyStr(idx.String()) } -func (a *sparseArrayObject) expand() bool { +func (a *sparseArrayObject) expand(idx uint32) bool { if l := len(a.items); l >= 1024 { - if int(a.items[l-1].idx)/l < 8 { + if ii := a.items[l-1].idx; ii > idx { + idx = ii + } + if (bits.UintSize == 64 || idx < math.MaxInt32) && int(idx)>>3 < l { //log.Println("Switching sparse->standard") ar := &arrayObject{ baseObject: a.baseObject, length: a.length, propValueCount: a.propValueCount, } - ar.setValuesFromSparse(a.items) + ar.setValuesFromSparse(a.items, int(idx)) ar.val.self = ar ar.init() ar.lengthProp.writable = a.lengthProp.writable @@ -335,51 +339,61 @@ func (a *sparseArrayObject) expand() bool { return true } -func (a *sparseArrayObject) defineOwnProperty(n Value, descr propertyDescr, throw bool) bool { - if idx := toIdx(n); idx >= 0 { - var existing Value - i := a.findIdx(idx) - if i < len(a.items) && a.items[i].idx == idx { - existing = a.items[i].value +func (a *sparseArrayObject) _defineIdxProperty(idx uint32, desc PropertyDescriptor, throw bool) bool { + var existing Value + i := a.findIdx(idx) + if i < len(a.items) && a.items[i].idx == idx { + existing = a.items[i].value + } + prop, ok := a.baseObject._defineOwnProperty(strconv.FormatUint(uint64(idx), 10), existing, desc, throw) + if ok { + if idx >= a.length { + if !a.setLengthInt(int64(idx)+1, throw) { + return false + } } - prop, ok := a.baseObject._defineOwnProperty(n, existing, descr, throw) - if ok { - if idx >= a.length { - if !a.setLengthInt(idx+1, throw) { - return false + if i >= len(a.items) || a.items[i].idx != idx { + if a.expand(idx) { + a.items = append(a.items, sparseArrayItem{}) + copy(a.items[i+1:], a.items[i:]) + a.items[i] = sparseArrayItem{ + idx: idx, + value: prop, } - } - if i >= len(a.items) || a.items[i].idx != idx { - if a.expand() { - a.items = append(a.items, sparseArrayItem{}) - copy(a.items[i+1:], a.items[i:]) - a.items[i] = sparseArrayItem{ - idx: idx, - value: prop, - } - if idx >= a.length { - a.length = idx + 1 - } - } else { - return a.val.self.defineOwnProperty(n, descr, throw) + if idx >= a.length { + a.length = idx + 1 } } else { - a.items[i].value = prop - } - if _, ok := prop.(*valueProperty); ok { - a.propValueCount++ + a.val.self.(*arrayObject).values[idx] = prop } + } else { + a.items[i].value = prop } - return ok - } else { - if n.String() == "length" { - return a.val.runtime.defineArrayLength(&a.lengthProp, descr, a.setLength, throw) + if _, ok := prop.(*valueProperty); ok { + a.propValueCount++ } - return a.baseObject.defineOwnProperty(n, descr, throw) } + return ok +} + +func (a *sparseArrayObject) defineOwnPropertyStr(name string, descr PropertyDescriptor, throw bool) bool { + if idx := strToIdx(name); idx != math.MaxUint32 { + return a._defineIdxProperty(idx, descr, throw) + } + if name == "length" { + return a.val.runtime.defineArrayLength(&a.lengthProp, descr, a.setLength, throw) + } + return a.baseObject.defineOwnPropertyStr(name, descr, throw) +} + +func (a *sparseArrayObject) defineOwnPropertyIdx(idx valueInt, descr PropertyDescriptor, throw bool) bool { + if idx := toIdx(idx); idx != math.MaxUint32 { + return a._defineIdxProperty(idx, descr, throw) + } + return a.baseObject.defineOwnPropertyStr(idx.String(), descr, throw) } -func (a *sparseArrayObject) _deleteProp(idx int64, throw bool) bool { +func (a *sparseArrayObject) _deleteIdxProp(idx uint32, throw bool) bool { i := a.findIdx(idx) if i < len(a.items) && a.items[i].idx == idx { if p, ok := a.items[i].value.(*valueProperty); ok { @@ -396,49 +410,28 @@ func (a *sparseArrayObject) _deleteProp(idx int64, throw bool) bool { return true } -func (a *sparseArrayObject) delete(n Value, throw bool) bool { - if idx := toIdx(n); idx >= 0 { - return a._deleteProp(idx, throw) +func (a *sparseArrayObject) deleteStr(name string, throw bool) bool { + if idx := strToIdx(name); idx != math.MaxUint32 { + return a._deleteIdxProp(idx, throw) } - return a.baseObject.delete(n, throw) + return a.baseObject.deleteStr(name, throw) } -func (a *sparseArrayObject) deleteStr(name string, throw bool) bool { - if idx := strToIdx(name); idx >= 0 { - return a._deleteProp(idx, throw) +func (a *sparseArrayObject) deleteIdx(idx valueInt, throw bool) bool { + if idx := toIdx(idx); idx != math.MaxUint32 { + return a._deleteIdxProp(idx, throw) } - return a.baseObject.deleteStr(name, throw) + return a.baseObject.deleteStr(idx.String(), throw) } func (a *sparseArrayObject) sortLen() int64 { if len(a.items) > 0 { - return a.items[len(a.items)-1].idx + 1 + return int64(a.items[len(a.items)-1].idx) + 1 } return 0 } -func (a *sparseArrayObject) sortGet(i int64) Value { - idx := a.findIdx(i) - if idx < len(a.items) && a.items[idx].idx == i { - v := a.items[idx].value - if p, ok := v.(*valueProperty); ok { - v = p.get(a.val) - } - return v - } - return nil -} - -func (a *sparseArrayObject) swap(i, j int64) { - idxI := a.findIdx(i) - idxJ := a.findIdx(j) - - if idxI < len(a.items) && a.items[idxI].idx == i && idxJ < len(a.items) && a.items[idxJ].idx == j { - a.items[idxI].value, a.items[idxJ].value = a.items[idxJ].value, a.items[idxI].value - } -} - func (a *sparseArrayObject) export() interface{} { arr := make([]interface{}, a.length) for _, item := range a.items { diff --git a/array_sparse_test.go b/array_sparse_test.go index fe92305d..28791342 100644 --- a/array_sparse_test.go +++ b/array_sparse_test.go @@ -1,6 +1,8 @@ package goja -import "testing" +import ( + "testing" +) func TestSparseArraySetLengthWithPropItems(t *testing.T) { const SCRIPT = ` @@ -21,9 +23,18 @@ func TestSparseArraySetLengthWithPropItems(t *testing.T) { } func TestSparseArraySwitch(t *testing.T) { - const SCRIPT = ` + vm := New() + _, err := vm.RunString(` var a = []; - a[20470] = 5; // switch to sparse + a[20470] = 5; // switch to sparse`) + if err != nil { + t.Fatal(err) + } + a := vm.Get("a").(*Object) + if _, ok := a.self.(*sparseArrayObject); !ok { + t.Fatal("1: array is not sparse") + } + _, err = vm.RunString(` var cutoffIdx = Math.round(20470 - 20470/8); for (var i = a.length - 1; i >= cutoffIdx; i--) { a[i] = i; @@ -44,8 +55,14 @@ func TestSparseArraySwitch(t *testing.T) { if (a[i] !== i) { throw new Error("Invalid value at " + i + ": " + a[i]); } + }`) + if err != nil { + t.Fatal(err) } - + if _, ok := a.self.(*arrayObject); !ok { + t.Fatal("2: array is not normal") + } + _, err = vm.RunString(` // Now try to expand. Should stay a normal array a[20471] = 20471; if (a.length != 20472) { @@ -62,8 +79,14 @@ func TestSparseArraySwitch(t *testing.T) { if (a[i] !== i) { throw new Error("Invalid value at " + i + ": " + a[i]); } + }`) + if err != nil { + t.Fatal(err) } - + if _, ok := a.self.(*arrayObject); !ok { + t.Fatal("3: array is not normal") + } + _, err = vm.RunString(` // Delete enough elements for it to become sparse again. var cutoffIdx1 = Math.round(20472 - 20472/10); for (var i = cutoffIdx; i < cutoffIdx1; i++) { @@ -97,7 +120,55 @@ func TestSparseArraySwitch(t *testing.T) { if (a[25590] !== 25590) { throw new Error("Invalid value at 25590: " + a[25590]); } + `) + if err != nil { + t.Fatal(err) + } + if _, ok := a.self.(*sparseArrayObject); !ok { + t.Fatal("4: array is not sparse") + } +} + +func TestSparseArrayOwnKeys(t *testing.T) { + const SCRIPT = ` + var a1 = []; + a1[500000] = 1; + var seen = false; + var count = 0; + var keys = Object.keys(a1); + keys.length === 1 && keys[0] === "500000"; + ` + + testScript1(SCRIPT, valueTrue, t) +} + +func TestSparseArrayEnumerate(t *testing.T) { + const SCRIPT = ` + var a1 = []; + a1[500000] = 1; + var seen = false; + var count = 0; + for (var i in a1) { + if (i === "500000") { + if (seen) { + throw new Error("seen twice"); + } + seen = true; + } + count++; + } + seen && count === 1; + ` + + testScript1(SCRIPT, valueTrue, t) +} + +func TestArraySparseMaxLength(t *testing.T) { + const SCRIPT = ` + var a = []; + a[4294967294]=1; + a.length === 4294967295 && a[4294967294] === 1; ` - testScript1(SCRIPT, _undefined, t) + testScript1(SCRIPT, valueTrue, t) } diff --git a/ast/node.go b/ast/node.go index 6c4ac7d0..33d49900 100644 --- a/ast/node.go +++ b/ast/node.go @@ -172,6 +172,11 @@ type ( Idx file.Idx Initializer Expression } + + MetaProperty struct { + Meta, Property *Identifier + Idx file.Idx + } ) // _expressionNode @@ -197,6 +202,7 @@ func (*StringLiteral) _expressionNode() {} func (*ThisExpression) _expressionNode() {} func (*UnaryExpression) _expressionNode() {} func (*VariableExpression) _expressionNode() {} +func (*MetaProperty) _expressionNode() {} // ========= // // Statement // @@ -413,6 +419,7 @@ func (self *StringLiteral) Idx0() file.Idx { return self.Idx } func (self *ThisExpression) Idx0() file.Idx { return self.Idx } func (self *UnaryExpression) Idx0() file.Idx { return self.Idx } func (self *VariableExpression) Idx0() file.Idx { return self.Idx } +func (self *MetaProperty) Idx0() file.Idx { return self.Idx } func (self *BadStatement) Idx0() file.Idx { return self.From } func (self *BlockStatement) Idx0() file.Idx { return self.LeftBrace } @@ -471,6 +478,9 @@ func (self *VariableExpression) Idx1() file.Idx { } return self.Initializer.Idx1() } +func (self *MetaProperty) Idx1() file.Idx { + return self.Property.Idx1() +} func (self *BadStatement) Idx1() file.Idx { return self.To } func (self *BlockStatement) Idx1() file.Idx { return self.RightBrace + 1 } diff --git a/builtin_array.go b/builtin_array.go index 13e13bae..9b521700 100644 --- a/builtin_array.go +++ b/builtin_array.go @@ -1,6 +1,7 @@ package goja import ( + "math" "sort" "strings" ) @@ -24,21 +25,21 @@ func (r *Runtime) newArrayObject() *arrayObject { func setArrayValues(a *arrayObject, values []Value) *arrayObject { a.values = values - a.length = int64(len(values)) - a.objCount = a.length + a.length = uint32(len(values)) + a.objCount = len(values) return a } func setArrayLength(a *arrayObject, l int64) *arrayObject { - a.putStr("length", intToValue(l), true) + a.setOwnStr("length", intToValue(l), true) return a } func arraySpeciesCreate(obj *Object, size int64) *Object { if isArray(obj) { - v := obj.self.getStr("constructor") + v := obj.self.getStr("constructor", nil) if constructObj, ok := v.(*Object); ok { - v = constructObj.self.get(symSpecies) + v = constructObj.self.getSym(symSpecies, nil) if v == _null { v = nil } @@ -47,9 +48,8 @@ func arraySpeciesCreate(obj *Object, size int64) *Object { if v != nil && v != _undefined { constructObj, _ := v.(*Object) if constructObj != nil { - constructor := getConstructor(constructObj) - if constructor != nil { - return constructor([]Value{intToValue(size)}) + if constructor := constructObj.self.assertConstructor(); constructor != nil { + return constructor([]Value{intToValue(size)}, constructObj) } } panic(obj.runtime.NewTypeError("Species is not a constructor")) @@ -90,11 +90,11 @@ func (r *Runtime) newArrayLength(l int64) *Object { func (r *Runtime) builtin_newArray(args []Value, proto *Object) *Object { l := len(args) if l == 1 { - if al, ok := args[0].assertInt(); ok { - return setArrayLength(r.newArray(proto), al).val - } else if f, ok := args[0].assertFloat(); ok { + if al, ok := args[0].(valueInt); ok { + return setArrayLength(r.newArray(proto), int64(al)).val + } else if f, ok := args[0].(valueFloat); ok { al := int64(f) - if float64(al) == f { + if float64(al) == float64(f) { return r.newArrayLength(al) } else { panic(r.newError(r.global.RangeError, "Invalid array length")) @@ -109,17 +109,17 @@ func (r *Runtime) builtin_newArray(args []Value, proto *Object) *Object { } func (r *Runtime) generic_push(obj *Object, call FunctionCall) Value { - l := toLength(obj.self.getStr("length")) + l := toLength(obj.self.getStr("length", nil)) nl := l + int64(len(call.Arguments)) if nl >= maxInt { r.typeErrorResult(true, "Invalid array length") panic("unreachable") } for i, arg := range call.Arguments { - obj.self.put(intToValue(l+int64(i)), arg, true) + obj.self.setOwnIdx(valueInt(l+int64(i)), arg, true) } - n := intToValue(nl) - obj.self.putStr("length", n, true) + n := valueInt(nl) + obj.self.setOwnStr("length", n, true) return n } @@ -129,15 +129,15 @@ func (r *Runtime) arrayproto_push(call FunctionCall) Value { } func (r *Runtime) arrayproto_pop_generic(obj *Object) Value { - l := toLength(obj.self.getStr("length")) + l := toLength(obj.self.getStr("length", nil)) if l == 0 { - obj.self.putStr("length", intToValue(0), true) + obj.self.setOwnStr("length", intToValue(0), true) return _undefined } - idx := intToValue(l - 1) - val := obj.self.get(idx) - obj.self.delete(idx, true) - obj.self.putStr("length", idx, true) + idx := valueInt(l - 1) + val := obj.self.getIdx(idx, nil) + obj.self.deleteIdx(idx, true) + obj.self.setOwnStr("length", idx, true) return val } @@ -148,7 +148,7 @@ func (r *Runtime) arrayproto_pop(call FunctionCall) Value { if l > 0 { var val Value l-- - if l < int64(len(a.values)) { + if l < uint32(len(a.values)) { val = a.values[l] } if val == nil { @@ -173,7 +173,7 @@ func (r *Runtime) arrayproto_pop(call FunctionCall) Value { func (r *Runtime) arrayproto_join(call FunctionCall) Value { o := call.This.ToObject(r) - l := int(toLength(o.self.getStr("length"))) + l := int(toLength(o.self.getStr("length", nil))) sep := "" if s := call.Argument(0); s != _undefined { sep = s.String() @@ -186,14 +186,14 @@ func (r *Runtime) arrayproto_join(call FunctionCall) Value { var buf strings.Builder - element0 := o.self.get(intToValue(0)) + element0 := o.self.getIdx(valueInt(0), nil) if element0 != nil && element0 != _undefined && element0 != _null { buf.WriteString(element0.String()) } for i := 1; i < l; i++ { buf.WriteString(sep) - element := o.self.get(intToValue(int64(i))) + element := o.self.getIdx(valueInt(int64(i)), nil) if element != nil && element != _undefined && element != _null { buf.WriteString(element.String()) } @@ -204,7 +204,7 @@ func (r *Runtime) arrayproto_join(call FunctionCall) Value { func (r *Runtime) arrayproto_toString(call FunctionCall) Value { array := call.This.ToObject(r) - f := array.self.getStr("join") + f := array.self.getStr("join", nil) if fObj, ok := f.(*Object); ok { if fcall, ok := fObj.self.assertCallable(); ok { return fcall(FunctionCall{ @@ -243,12 +243,12 @@ func (r *Runtime) arrayproto_toLocaleString(call FunctionCall) Value { r.writeItemLocaleString(item, &buf) } } else { - length := toLength(array.self.getStr("length")) + length := toLength(array.self.getStr("length", nil)) for i := int64(0); i < length; i++ { if i > 0 { buf.WriteByte(',') } - item := array.self.get(intToValue(i)) + item := array.self.getIdx(valueInt(i), nil) r.writeItemLocaleString(item, &buf) } } @@ -257,7 +257,7 @@ func (r *Runtime) arrayproto_toLocaleString(call FunctionCall) Value { } func isConcatSpreadable(obj *Object) bool { - spreadable := obj.self.get(symIsConcatSpreadable) + spreadable := obj.self.getSym(symIsConcatSpreadable, nil) if spreadable != nil && spreadable != _undefined { return spreadable.ToBoolean() } @@ -265,21 +265,21 @@ func isConcatSpreadable(obj *Object) bool { } func (r *Runtime) arrayproto_concat_append(a *Object, item Value) { - aLength := toLength(a.self.getStr("length")) + aLength := toLength(a.self.getStr("length", nil)) if obj, ok := item.(*Object); ok && isConcatSpreadable(obj) { - length := toLength(obj.self.getStr("length")) + length := toLength(obj.self.getStr("length", nil)) for i := int64(0); i < length; i++ { - v := obj.self.get(intToValue(i)) + v := obj.self.getIdx(valueInt(i), nil) if v != nil { - defineDataPropertyOrThrow(a, intToValue(aLength), v) + createDataPropertyOrThrow(a, intToValue(aLength), v) } aLength++ } } else { - defineDataPropertyOrThrow(a, intToValue(aLength), item) + createDataPropertyOrThrow(a, intToValue(aLength), item) aLength++ } - a.self.putStr("length", intToValue(aLength), true) + a.self.setOwnStr("length", intToValue(aLength), true) } func (r *Runtime) arrayproto_concat(call FunctionCall) Value { @@ -294,7 +294,7 @@ func (r *Runtime) arrayproto_concat(call FunctionCall) Value { func (r *Runtime) arrayproto_slice(call FunctionCall) Value { o := call.This.ToObject(r) - length := toLength(o.self.getStr("length")) + length := toLength(o.self.getStr("length", nil)) start := relToIdx(call.Argument(0).ToInteger(), length) var end int64 if endArg := call.Argument(1); endArg != _undefined { @@ -321,9 +321,9 @@ func (r *Runtime) arrayproto_slice(call FunctionCall) Value { n := int64(0) for start < end { - p := o.self.get(intToValue(start)) + p := o.self.getIdx(valueInt(start), nil) if p != nil { - defineDataPropertyOrThrow(a, intToValue(n), p) + createDataPropertyOrThrow(a, valueInt(n), p) } start++ n++ @@ -351,7 +351,7 @@ func (r *Runtime) arrayproto_sort(call FunctionCall) Value { func (r *Runtime) arrayproto_splice(call FunctionCall) Value { o := call.This.ToObject(r) - length := toLength(o.self.getStr("length")) + length := toLength(o.self.getStr("length", nil)) actualStart := relToIdx(call.Argument(0).ToInteger(), length) var actualDeleteCount int64 switch len(call.Arguments) { @@ -371,7 +371,7 @@ func (r *Runtime) arrayproto_splice(call FunctionCall) Value { setArrayValues(dst, values) } else { for k := int64(0); k < actualDeleteCount; k++ { - defineDataPropertyOrThrow(a, intToValue(k), src.values[k+actualStart]) + createDataPropertyOrThrow(a, intToValue(k), src.values[k+actualStart]) } } var values []Value @@ -399,60 +399,60 @@ func (r *Runtime) arrayproto_splice(call FunctionCall) Value { copy(values[actualStart:], call.Arguments[2:]) } src.values = values - src.objCount = int64(len(values)) + src.objCount = len(values) } else { for k := int64(0); k < actualDeleteCount; k++ { - from := intToValue(k + actualStart) - if o.self.hasProperty(from) { - defineDataPropertyOrThrow(a, intToValue(k), o.self.get(from)) + from := valueInt(k + actualStart) + if o.self.hasPropertyIdx(from) { + createDataPropertyOrThrow(a, valueInt(k), o.self.getIdx(from, nil)) } } if itemCount < actualDeleteCount { for k := actualStart; k < length-actualDeleteCount; k++ { - from := intToValue(k + actualDeleteCount) - to := intToValue(k + itemCount) - if o.self.hasProperty(from) { - o.self.put(to, o.self.get(from), true) + from := valueInt(k + actualDeleteCount) + to := valueInt(k + itemCount) + if o.self.hasPropertyIdx(from) { + o.self.setOwnIdx(to, o.self.getIdx(from, nil), true) } else { - o.self.delete(to, true) + o.self.deleteIdx(to, true) } } for k := length; k > length-actualDeleteCount+itemCount; k-- { - o.self.delete(intToValue(k-1), true) + o.self.deleteIdx(valueInt(k-1), true) } } else if itemCount > actualDeleteCount { for k := length - actualDeleteCount; k > actualStart; k-- { - from := intToValue(k + actualDeleteCount - 1) - to := intToValue(k + itemCount - 1) - if o.self.hasProperty(from) { - o.self.put(to, o.self.get(from), true) + from := valueInt(k + actualDeleteCount - 1) + to := valueInt(k + itemCount - 1) + if o.self.hasPropertyIdx(from) { + o.self.setOwnIdx(to, o.self.getIdx(from, nil), true) } else { - o.self.delete(to, true) + o.self.deleteIdx(to, true) } } } if itemCount > 0 { for i, item := range call.Arguments[2:] { - o.self.put(intToValue(actualStart+int64(i)), item, true) + o.self.setOwnIdx(valueInt(actualStart+int64(i)), item, true) } } } - o.self.putStr("length", intToValue(newLength), true) + o.self.setOwnStr("length", intToValue(newLength), true) return a } func (r *Runtime) arrayproto_unshift(call FunctionCall) Value { o := call.This.ToObject(r) - length := toLength(o.self.getStr("length")) + length := toLength(o.self.getStr("length", nil)) argCount := int64(len(call.Arguments)) newLen := intToValue(length + argCount) - if arr := r.checkStdArrayObj(o); arr != nil { - newSize := length + argCount + newSize := length + argCount + if arr := r.checkStdArrayObj(o); arr != nil && newSize < math.MaxUint32 { if int64(cap(arr.values)) >= newSize { arr.values = arr.values[:newSize] copy(arr.values[argCount:], arr.values[:length]) @@ -462,30 +462,30 @@ func (r *Runtime) arrayproto_unshift(call FunctionCall) Value { arr.values = values } copy(arr.values, call.Arguments) - arr.objCount = arr.length + arr.objCount = int(arr.length) } else { for k := length - 1; k >= 0; k-- { - from := intToValue(k) - to := intToValue(k + argCount) - if o.self.hasProperty(from) { - o.self.put(to, o.self.get(from), true) + from := valueInt(k) + to := valueInt(k + argCount) + if o.self.hasPropertyIdx(from) { + o.self.setOwnIdx(to, o.self.getIdx(from, nil), true) } else { - o.self.delete(to, true) + o.self.deleteIdx(to, true) } } for k, arg := range call.Arguments { - o.self.put(intToValue(int64(k)), arg, true) + o.self.setOwnIdx(valueInt(int64(k)), arg, true) } } - o.self.putStr("length", newLen, true) + o.self.setOwnStr("length", newLen, true) return newLen } func (r *Runtime) arrayproto_indexOf(call FunctionCall) Value { o := call.This.ToObject(r) - length := toLength(o.self.getStr("length")) + length := toLength(o.self.getStr("length", nil)) if length == 0 { return intToValue(-1) } @@ -511,22 +511,22 @@ func (r *Runtime) arrayproto_indexOf(call FunctionCall) Value { } for ; n < length; n++ { - idx := intToValue(n) - if val := o.self.get(idx); val != nil { + idx := valueInt(n) + if val := o.self.getIdx(idx, nil); val != nil { if searchElement.StrictEquals(val) { return idx } } } - return intToValue(-1) + return valueInt(-1) } func (r *Runtime) arrayproto_lastIndexOf(call FunctionCall) Value { o := call.This.ToObject(r) - length := toLength(o.self.getStr("length")) + length := toLength(o.self.getStr("length", nil)) if length == 0 { - return intToValue(-1) + return valueInt(-1) } var fromIndex int64 @@ -555,8 +555,8 @@ func (r *Runtime) arrayproto_lastIndexOf(call FunctionCall) Value { } for k := fromIndex; k >= 0; k-- { - idx := intToValue(k) - if val := o.self.get(idx); val != nil { + idx := valueInt(k) + if val := o.self.getIdx(idx, nil); val != nil { if searchElement.StrictEquals(val) { return idx } @@ -568,15 +568,15 @@ func (r *Runtime) arrayproto_lastIndexOf(call FunctionCall) Value { func (r *Runtime) arrayproto_every(call FunctionCall) Value { o := call.This.ToObject(r) - length := toLength(o.self.getStr("length")) + length := toLength(o.self.getStr("length", nil)) callbackFn := r.toCallable(call.Argument(0)) fc := FunctionCall{ This: call.Argument(1), Arguments: []Value{nil, nil, o}, } for k := int64(0); k < length; k++ { - idx := intToValue(k) - if val := o.self.get(idx); val != nil { + idx := valueInt(k) + if val := o.self.getIdx(idx, nil); val != nil { fc.Arguments[0] = val fc.Arguments[1] = idx if !callbackFn(fc).ToBoolean() { @@ -589,15 +589,15 @@ func (r *Runtime) arrayproto_every(call FunctionCall) Value { func (r *Runtime) arrayproto_some(call FunctionCall) Value { o := call.This.ToObject(r) - length := toLength(o.self.getStr("length")) + length := toLength(o.self.getStr("length", nil)) callbackFn := r.toCallable(call.Argument(0)) fc := FunctionCall{ This: call.Argument(1), Arguments: []Value{nil, nil, o}, } for k := int64(0); k < length; k++ { - idx := intToValue(k) - if val := o.self.get(idx); val != nil { + idx := valueInt(k) + if val := o.self.getIdx(idx, nil); val != nil { fc.Arguments[0] = val fc.Arguments[1] = idx if callbackFn(fc).ToBoolean() { @@ -610,15 +610,15 @@ func (r *Runtime) arrayproto_some(call FunctionCall) Value { func (r *Runtime) arrayproto_forEach(call FunctionCall) Value { o := call.This.ToObject(r) - length := toLength(o.self.getStr("length")) + length := toLength(o.self.getStr("length", nil)) callbackFn := r.toCallable(call.Argument(0)) fc := FunctionCall{ This: call.Argument(1), Arguments: []Value{nil, nil, o}, } for k := int64(0); k < length; k++ { - idx := intToValue(k) - if val := o.self.get(idx); val != nil { + idx := valueInt(k) + if val := o.self.getIdx(idx, nil); val != nil { fc.Arguments[0] = val fc.Arguments[1] = idx callbackFn(fc) @@ -629,7 +629,7 @@ func (r *Runtime) arrayproto_forEach(call FunctionCall) Value { func (r *Runtime) arrayproto_map(call FunctionCall) Value { o := call.This.ToObject(r) - length := toLength(o.self.getStr("length")) + length := toLength(o.self.getStr("length", nil)) callbackFn := r.toCallable(call.Argument(0)) fc := FunctionCall{ This: call.Argument(1), @@ -640,8 +640,8 @@ func (r *Runtime) arrayproto_map(call FunctionCall) Value { if arr, ok := a.self.(*arrayObject); ok { values := make([]Value, length) for k := int64(0); k < length; k++ { - idx := intToValue(k) - if val := o.self.get(idx); val != nil { + idx := valueInt(k) + if val := o.self.getIdx(idx, nil); val != nil { fc.Arguments[0] = val fc.Arguments[1] = idx values[k] = callbackFn(fc) @@ -652,11 +652,11 @@ func (r *Runtime) arrayproto_map(call FunctionCall) Value { } } for k := int64(0); k < length; k++ { - idx := intToValue(k) - if val := o.self.get(idx); val != nil { + idx := valueInt(k) + if val := o.self.getIdx(idx, nil); val != nil { fc.Arguments[0] = val fc.Arguments[1] = idx - defineDataPropertyOrThrow(a, idx, callbackFn(fc)) + createDataPropertyOrThrow(a, idx, callbackFn(fc)) } } return a @@ -664,7 +664,7 @@ func (r *Runtime) arrayproto_map(call FunctionCall) Value { func (r *Runtime) arrayproto_filter(call FunctionCall) Value { o := call.This.ToObject(r) - length := toLength(o.self.getStr("length")) + length := toLength(o.self.getStr("length", nil)) callbackFn := call.Argument(0).ToObject(r) if callbackFn, ok := callbackFn.self.assertCallable(); ok { a := arraySpeciesCreate(o, 0) @@ -676,8 +676,8 @@ func (r *Runtime) arrayproto_filter(call FunctionCall) Value { if arr := r.checkStdArrayObj(a); arr != nil { var values []Value for k := int64(0); k < length; k++ { - idx := intToValue(k) - if val := o.self.get(idx); val != nil { + idx := valueInt(k) + if val := o.self.getIdx(idx, nil); val != nil { fc.Arguments[0] = val fc.Arguments[1] = idx if callbackFn(fc).ToBoolean() { @@ -692,12 +692,12 @@ func (r *Runtime) arrayproto_filter(call FunctionCall) Value { to := int64(0) for k := int64(0); k < length; k++ { - idx := intToValue(k) - if val := o.self.get(idx); val != nil { + idx := valueInt(k) + if val := o.self.getIdx(idx, nil); val != nil { fc.Arguments[0] = val fc.Arguments[1] = idx if callbackFn(fc).ToBoolean() { - defineDataPropertyOrThrow(a, intToValue(to), val) + createDataPropertyOrThrow(a, intToValue(to), val) to++ } } @@ -711,7 +711,7 @@ func (r *Runtime) arrayproto_filter(call FunctionCall) Value { func (r *Runtime) arrayproto_reduce(call FunctionCall) Value { o := call.This.ToObject(r) - length := toLength(o.self.getStr("length")) + length := toLength(o.self.getStr("length", nil)) callbackFn := call.Argument(0).ToObject(r) if callbackFn, ok := callbackFn.self.assertCallable(); ok { fc := FunctionCall{ @@ -725,8 +725,8 @@ func (r *Runtime) arrayproto_reduce(call FunctionCall) Value { fc.Arguments[0] = call.Argument(1) } else { for ; k < length; k++ { - idx := intToValue(k) - if val := o.self.get(idx); val != nil { + idx := valueInt(k) + if val := o.self.getIdx(idx, nil); val != nil { fc.Arguments[0] = val break } @@ -739,8 +739,8 @@ func (r *Runtime) arrayproto_reduce(call FunctionCall) Value { } for ; k < length; k++ { - idx := intToValue(k) - if val := o.self.get(idx); val != nil { + idx := valueInt(k) + if val := o.self.getIdx(idx, nil); val != nil { fc.Arguments[1] = val fc.Arguments[2] = idx fc.Arguments[0] = callbackFn(fc) @@ -755,7 +755,7 @@ func (r *Runtime) arrayproto_reduce(call FunctionCall) Value { func (r *Runtime) arrayproto_reduceRight(call FunctionCall) Value { o := call.This.ToObject(r) - length := toLength(o.self.getStr("length")) + length := toLength(o.self.getStr("length", nil)) callbackFn := call.Argument(0).ToObject(r) if callbackFn, ok := callbackFn.self.assertCallable(); ok { fc := FunctionCall{ @@ -769,8 +769,8 @@ func (r *Runtime) arrayproto_reduceRight(call FunctionCall) Value { fc.Arguments[0] = call.Argument(1) } else { for ; k >= 0; k-- { - idx := intToValue(k) - if val := o.self.get(idx); val != nil { + idx := valueInt(k) + if val := o.self.getIdx(idx, nil); val != nil { fc.Arguments[0] = val break } @@ -783,8 +783,8 @@ func (r *Runtime) arrayproto_reduceRight(call FunctionCall) Value { } for ; k >= 0; k-- { - idx := intToValue(k) - if val := o.self.get(idx); val != nil { + idx := valueInt(k) + if val := o.self.getIdx(idx, nil); val != nil { fc.Arguments[1] = val fc.Arguments[2] = idx fc.Arguments[0] = callbackFn(fc) @@ -798,24 +798,24 @@ func (r *Runtime) arrayproto_reduceRight(call FunctionCall) Value { } func arrayproto_reverse_generic_step(o *Object, lower, upper int64) { - lowerP := intToValue(lower) - upperP := intToValue(upper) - lowerValue := o.self.get(lowerP) - upperValue := o.self.get(upperP) + lowerP := valueInt(lower) + upperP := valueInt(upper) + lowerValue := o.self.getIdx(lowerP, nil) + upperValue := o.self.getIdx(upperP, nil) if lowerValue != nil && upperValue != nil { - o.self.put(lowerP, upperValue, true) - o.self.put(upperP, lowerValue, true) + o.self.setOwnIdx(lowerP, upperValue, true) + o.self.setOwnIdx(upperP, lowerValue, true) } else if lowerValue == nil && upperValue != nil { - o.self.put(lowerP, upperValue, true) - o.self.delete(upperP, true) + o.self.setOwnIdx(lowerP, upperValue, true) + o.self.deleteIdx(upperP, true) } else if lowerValue != nil && upperValue == nil { - o.self.delete(lowerP, true) - o.self.put(upperP, lowerValue, true) + o.self.deleteIdx(lowerP, true) + o.self.setOwnIdx(upperP, lowerValue, true) } } func (r *Runtime) arrayproto_reverse_generic(o *Object, start int64) { - l := toLength(o.self.getStr("length")) + l := toLength(o.self.getStr("length", nil)) middle := l / 2 for lower := start; lower != middle; lower++ { arrayproto_reverse_generic_step(o, lower, l-lower-1) @@ -825,9 +825,9 @@ func (r *Runtime) arrayproto_reverse_generic(o *Object, start int64) { func (r *Runtime) arrayproto_reverse(call FunctionCall) Value { o := call.This.ToObject(r) if a := r.checkStdArrayObj(o); a != nil { - l := a.length + l := len(a.values) middle := l / 2 - for lower := int64(0); lower != middle; lower++ { + for lower := 0; lower != middle; lower++ { upper := l - lower - 1 a.values[lower], a.values[upper] = a.values[upper], a.values[lower] } @@ -840,24 +840,24 @@ func (r *Runtime) arrayproto_reverse(call FunctionCall) Value { func (r *Runtime) arrayproto_shift(call FunctionCall) Value { o := call.This.ToObject(r) - length := toLength(o.self.getStr("length")) + length := toLength(o.self.getStr("length", nil)) if length == 0 { - o.self.putStr("length", intToValue(0), true) + o.self.setOwnStr("length", intToValue(0), true) return _undefined } - first := o.self.get(intToValue(0)) + first := o.self.getIdx(valueInt(0), nil) for i := int64(1); i < length; i++ { - v := o.self.get(intToValue(i)) + v := o.self.getIdx(valueInt(i), nil) if v != nil { - o.self.put(intToValue(i-1), v, true) + o.self.setOwnIdx(valueInt(i-1), v, true) } else { - o.self.delete(intToValue(i-1), true) + o.self.deleteIdx(valueInt(i-1), true) } } - lv := intToValue(length - 1) - o.self.delete(lv, true) - o.self.putStr("length", lv, true) + lv := valueInt(length - 1) + o.self.deleteIdx(lv, true) + o.self.setOwnStr("length", lv, true) return first } @@ -872,7 +872,7 @@ func (r *Runtime) arrayproto_keys(call FunctionCall) Value { func (r *Runtime) arrayproto_copyWithin(call FunctionCall) Value { o := call.This.ToObject(r) - l := toLength(o.self.getStr("length")) + l := toLength(o.self.getStr("length", nil)) var relEnd, dir int64 to := relToIdx(call.Argument(0).ToInteger(), l) from := relToIdx(call.Argument(1).ToInteger(), l) @@ -897,10 +897,10 @@ func (r *Runtime) arrayproto_copyWithin(call FunctionCall) Value { dir = 1 } for count > 0 { - if p := o.self.get(intToValue(from)); p != nil { - o.self.put(intToValue(to), p, true) + if o.self.hasPropertyIdx(valueInt(from)) { + o.self.setOwnIdx(valueInt(to), o.self.getIdx(valueInt(from), nil), true) } else { - o.self.delete(intToValue(to), true) + o.self.deleteIdx(valueInt(to), true) } from += dir to += dir @@ -916,7 +916,7 @@ func (r *Runtime) arrayproto_entries(call FunctionCall) Value { func (r *Runtime) arrayproto_fill(call FunctionCall) Value { o := call.This.ToObject(r) - l := toLength(o.self.getStr("length")) + l := toLength(o.self.getStr("length", nil)) k := relToIdx(call.Argument(1).ToInteger(), l) var relEnd int64 if endArg := call.Argument(2); endArg != _undefined { @@ -932,7 +932,7 @@ func (r *Runtime) arrayproto_fill(call FunctionCall) Value { } } else { for ; k < final; k++ { - o.self.put(intToValue(k), value, true) + o.self.setOwnIdx(valueInt(k), value, true) } } return o @@ -940,15 +940,15 @@ func (r *Runtime) arrayproto_fill(call FunctionCall) Value { func (r *Runtime) arrayproto_find(call FunctionCall) Value { o := call.This.ToObject(r) - l := toLength(o.self.getStr("length")) + l := toLength(o.self.getStr("length", nil)) predicate := r.toCallable(call.Argument(0)) fc := FunctionCall{ This: call.Argument(1), Arguments: []Value{nil, nil, o}, } for k := int64(0); k < l; k++ { - idx := intToValue(k) - kValue := o.self.get(idx) + idx := valueInt(k) + kValue := o.self.getIdx(idx, nil) fc.Arguments[0], fc.Arguments[1] = kValue, idx if predicate(fc).ToBoolean() { return kValue @@ -960,29 +960,29 @@ func (r *Runtime) arrayproto_find(call FunctionCall) Value { func (r *Runtime) arrayproto_findIndex(call FunctionCall) Value { o := call.This.ToObject(r) - l := toLength(o.self.getStr("length")) + l := toLength(o.self.getStr("length", nil)) predicate := r.toCallable(call.Argument(0)) fc := FunctionCall{ This: call.Argument(1), Arguments: []Value{nil, nil, o}, } for k := int64(0); k < l; k++ { - idx := intToValue(k) - kValue := o.self.get(idx) + idx := valueInt(k) + kValue := o.self.getIdx(idx, nil) fc.Arguments[0], fc.Arguments[1] = kValue, idx if predicate(fc).ToBoolean() { return idx } } - return intToValue(-1) + return valueInt(-1) } func (r *Runtime) checkStdArrayObj(obj *Object) *arrayObject { if arr, ok := obj.self.(*arrayObject); ok && arr.propValueCount == 0 && - arr.length == int64(len(arr.values)) && - arr.objCount == arr.length { + arr.length == uint32(len(arr.values)) && + uint32(arr.objCount) == arr.length { return arr } @@ -1000,7 +1000,7 @@ func (r *Runtime) checkStdArray(v Value) *arrayObject { func (r *Runtime) checkStdArrayIter(v Value) *arrayObject { if arr := r.checkStdArray(v); arr != nil && - arr.getSym(symIterator) == r.global.arrayValues { + arr.getSym(symIterator, nil) == r.global.arrayValues { return arr } @@ -1030,10 +1030,10 @@ func (r *Runtime) array_from(call FunctionCall) Value { } } - var ctor func(args []Value) *Object + var ctor func(args []Value, newTarget *Object) *Object if call.This != r.global.Array { if o, ok := call.This.(*Object); ok { - if c := getConstructor(o); c != nil { + if c := o.self.assertConstructor(); c != nil { ctor = c } } @@ -1041,7 +1041,7 @@ func (r *Runtime) array_from(call FunctionCall) Value { var arr *Object if usingIterator := toMethod(r.getV(items, symIterator)); usingIterator != nil { if ctor != nil { - arr = ctor([]Value{}) + arr = ctor([]Value{}, nil) } else { arr = r.newArrayValues(nil) } @@ -1061,15 +1061,15 @@ func (r *Runtime) array_from(call FunctionCall) Value { if mapFn != nil { val = mapFn(FunctionCall{This: t, Arguments: []Value{val, intToValue(k)}}) } - defineDataPropertyOrThrow(arr, intToValue(k), val) + createDataPropertyOrThrow(arr, intToValue(k), val) k++ }) - arr.self.putStr("length", intToValue(k), true) + arr.self.setOwnStr("length", intToValue(k), true) } else { arrayLike := items.ToObject(r) - l := toLength(arrayLike.self.getStr("length")) + l := toLength(arrayLike.self.getStr("length", nil)) if ctor != nil { - arr = ctor([]Value{intToValue(l)}) + arr = ctor([]Value{intToValue(l)}, nil) } else { arr = r.newArrayValues(nil) } @@ -1077,23 +1077,23 @@ func (r *Runtime) array_from(call FunctionCall) Value { if a := r.checkStdArrayObj(arr); a != nil { values := make([]Value, l) for k := int64(0); k < l; k++ { - values[k] = nilSafe(arrayLike.self.get(intToValue(k))) + values[k] = nilSafe(arrayLike.self.getIdx(valueInt(k), nil)) } setArrayValues(a, values) return arr } } for k := int64(0); k < l; k++ { - idx := intToValue(k) - item := arrayLike.self.get(idx) + idx := valueInt(k) + item := arrayLike.self.getIdx(idx, nil) if mapFn != nil { item = mapFn(FunctionCall{This: t, Arguments: []Value{item, idx}}) } else { item = nilSafe(item) } - defineDataPropertyOrThrow(arr, idx, item) + createDataPropertyOrThrow(arr, idx, item) } - arr.self.putStr("length", intToValue(l), true) + arr.self.setOwnStr("length", intToValue(l), true) } return arr @@ -1109,10 +1109,10 @@ func (r *Runtime) array_isArray(call FunctionCall) Value { } func (r *Runtime) array_of(call FunctionCall) Value { - var ctor func(args []Value) *Object + var ctor func(args []Value, newTarget *Object) *Object if call.This != r.global.Array { if o, ok := call.This.(*Object); ok { - if c := getConstructor(o); c != nil { + if c := o.self.assertConstructor(); c != nil { ctor = c } } @@ -1123,11 +1123,11 @@ func (r *Runtime) array_of(call FunctionCall) Value { return r.newArrayValues(values) } l := intToValue(int64(len(call.Arguments))) - arr := ctor([]Value{l}) + arr := ctor([]Value{l}, nil) for i, val := range call.Arguments { - defineDataPropertyOrThrow(arr, intToValue(int64(i)), val) + createDataPropertyOrThrow(arr, intToValue(int64(i)), val) } - arr.self.putStr("length", l, true) + arr.self.setOwnStr("length", l, true) return arr } @@ -1182,17 +1182,17 @@ func (r *Runtime) createArrayProto(val *Object) objectImpl { r.global.arrayValues = valuesFunc o._putProp("values", valuesFunc, true, false, true) - o.put(symIterator, valueProp(valuesFunc, true, false, true), true) + o._putSym(symIterator, valueProp(valuesFunc, true, false, true)) bl := r.NewObject() - bl.self.putStr("copyWithin", valueTrue, true) - bl.self.putStr("entries", valueTrue, true) - bl.self.putStr("fill", valueTrue, true) - bl.self.putStr("find", valueTrue, true) - bl.self.putStr("findIndex", valueTrue, true) - bl.self.putStr("keys", valueTrue, true) - bl.self.putStr("values", valueTrue, true) - o.put(symUnscopables, valueProp(bl, false, false, true), true) + bl.self.setOwnStr("copyWithin", valueTrue, true) + bl.self.setOwnStr("entries", valueTrue, true) + bl.self.setOwnStr("fill", valueTrue, true) + bl.self.setOwnStr("find", valueTrue, true) + bl.self.setOwnStr("findIndex", valueTrue, true) + bl.self.setOwnStr("keys", valueTrue, true) + bl.self.setOwnStr("values", valueTrue, true) + o._putSym(symUnscopables, valueProp(bl, false, false, true)) return o } @@ -1202,11 +1202,11 @@ func (r *Runtime) createArray(val *Object) objectImpl { o._putProp("from", r.newNativeFunc(r.array_from, nil, "from", nil, 1), true, false, true) o._putProp("isArray", r.newNativeFunc(r.array_isArray, nil, "isArray", nil, 1), true, false, true) o._putProp("of", r.newNativeFunc(r.array_of, nil, "of", nil, 0), true, false, true) - o.putSym(symSpecies, &valueProperty{ + o._putSym(symSpecies, &valueProperty{ getterFunc: r.newNativeFunc(r.returnThis, nil, "get [Symbol.species]", nil, 0), accessor: true, configurable: true, - }, true) + }) return o } @@ -1215,7 +1215,7 @@ func (r *Runtime) createArrayIterProto(val *Object) objectImpl { o := newBaseObjectObj(val, r.global.IteratorPrototype, classObject) o._putProp("next", r.newNativeFunc(r.arrayIterProto_next, nil, "next", nil, 0), true, false, true) - o.put(symToStringTag, valueProp(asciiString(classArrayIterator), false, false, true), true) + o._putSym(symToStringTag, valueProp(asciiString(classArrayIterator), false, false, true)) return o } diff --git a/builtin_date.go b/builtin_date.go index a0ae93e8..fe976a1b 100644 --- a/builtin_date.go +++ b/builtin_date.go @@ -26,8 +26,8 @@ func (r *Runtime) makeDate(args []Value, loc *time.Location) (t time.Time, valid return default_, true } value := args[index] - if valueInt, ok := value.assertInt(); ok { - return valueInt, true + if valueInt, ok := value.(valueInt); ok { + return int64(valueInt), true } valueFloat := value.ToFloat() if math.IsNaN(valueFloat) || math.IsInf(valueFloat, 0) { @@ -71,14 +71,15 @@ func (r *Runtime) makeDate(args []Value, loc *time.Location) (t time.Time, valid valid = true default: // one argument pv := toPrimitiveNumber(args[0]) - if val, ok := pv.assertString(); ok { + if val, ok := pv.(valueString); ok { return dateParse(val.String()) } var n int64 - if i, ok := pv.assertInt(); ok { - n = i - } else if f, ok := pv.assertFloat(); ok { + if i, ok := pv.(valueInt); ok { + n = int64(i) + } else if f, ok := pv.(valueFloat); ok { + f := float64(f) if math.IsNaN(f) || math.IsInf(f, 0) { return } @@ -102,13 +103,13 @@ func (r *Runtime) makeDate(args []Value, loc *time.Location) (t time.Time, valid return } -func (r *Runtime) newDateTime(args []Value, loc *time.Location) *Object { +func (r *Runtime) newDateTime(args []Value, loc *time.Location, proto *Object) *Object { t, isSet := r.makeDate(args, loc) - return r.newDateObject(t, isSet) + return r.newDateObject(t, isSet, proto) } -func (r *Runtime) builtin_newDate(args []Value) *Object { - return r.newDateTime(args, time.Local) +func (r *Runtime) builtin_newDate(args []Value, proto *Object) *Object { + return r.newDateTime(args, time.Local, proto) } func (r *Runtime) builtin_date(FunctionCall) Value { @@ -183,15 +184,16 @@ func (r *Runtime) dateproto_toISOString(call FunctionCall) Value { func (r *Runtime) dateproto_toJSON(call FunctionCall) Value { obj := r.toObject(call.This) tv := obj.self.toPrimitiveNumber() - if f, ok := tv.assertFloat(); ok { + if f, ok := tv.(valueFloat); ok { + f := float64(f) if math.IsNaN(f) || math.IsInf(f, 0) { return _null } - } else if _, ok := tv.assertInt(); !ok { + } else if _, ok := tv.(valueInt); !ok { return _null } - if toISO, ok := obj.self.getStr("toISOString").(*Object); ok { + if toISO, ok := obj.self.getStr("toISOString", nil).(*Object); ok { if toISO, ok := toISO.self.assertCallable(); ok { return toISO(FunctionCall{ This: obj, diff --git a/builtin_error.go b/builtin_error.go index 5e95b426..0209931f 100644 --- a/builtin_error.go +++ b/builtin_error.go @@ -8,7 +8,6 @@ func (r *Runtime) initErrors() { o._putProp("toString", r.newNativeFunc(r.error_toString, nil, "toString", nil, 0), true, false, true) r.global.Error = r.newNativeFuncConstruct(r.builtin_Error, "Error", r.global.ErrorPrototype, 1) - o = r.global.Error.self r.addToGlobal("Error", r.global.Error) r.global.TypeErrorPrototype = r.builtin_new(r.global.Error, []Value{}) diff --git a/builtin_function.go b/builtin_function.go index 78fd7252..f5b75f87 100644 --- a/builtin_function.go +++ b/builtin_function.go @@ -18,7 +18,9 @@ func (r *Runtime) builtin_Function(args []Value, proto *Object) *Object { } src += "){" + body + "})" - return r.toObject(r.eval(src, false, false, _undefined)) + ret := r.toObject(r.eval(src, false, false, _undefined)) + ret.self.setProto(proto, true) + return ret } func (r *Runtime) functionproto_toString(call FunctionCall) Value { @@ -34,28 +36,49 @@ repeat: case *lazyObject: obj.self = f.create(obj) goto repeat + case *proxyObject: + var name string + repeat2: + switch c := f.target.self.(type) { + case *funcObject: + name = c.src + case *nativeFuncObject: + name = c.nameProp.get(call.This).String() + case *boundFuncObject: + name = c.nameProp.get(call.This).String() + case *lazyObject: + f.target.self = c.create(obj) + goto repeat2 + default: + name = f.target.String() + } + return newStringValue(fmt.Sprintf("function proxy() { [%s] }", name)) } r.typeErrorResult(true, "Object is not a function") return nil } -func (r *Runtime) toValueArray(a Value) []Value { - obj := r.toObject(a) - l := toUInt32(obj.self.getStr("length")) - ret := make([]Value, l) - for i := uint32(0); i < l; i++ { - ret[i] = obj.self.get(valueInt(i)) +func (r *Runtime) createListFromArrayLike(a Value) []Value { + o := r.toObject(a) + if arr := r.checkStdArrayObj(o); arr != nil { + return arr.values } - return ret + l := toLength(o.self.getStr("length", nil)) + res := make([]Value, 0, l) + for k := int64(0); k < l; k++ { + res = append(res, o.self.getIdx(valueInt(k), nil)) + } + return res } func (r *Runtime) functionproto_apply(call FunctionCall) Value { - f := r.toCallable(call.This) var args []Value if len(call.Arguments) >= 2 { - args = r.toValueArray(call.Arguments[1]) + args = r.createListFromArrayLike(call.Arguments[1]) } + + f := r.toCallable(call.This) return f(FunctionCall{ This: call.Argument(0), Arguments: args, @@ -63,11 +86,12 @@ func (r *Runtime) functionproto_apply(call FunctionCall) Value { } func (r *Runtime) functionproto_call(call FunctionCall) Value { - f := r.toCallable(call.This) var args []Value if len(call.Arguments) > 0 { args = call.Arguments[1:] } + + f := r.toCallable(call.This) return f(FunctionCall{ This: call.Argument(0), Arguments: args, @@ -93,7 +117,7 @@ func (r *Runtime) boundCallable(target func(FunctionCall) Value, boundArgs []Val } } -func (r *Runtime) boundConstruct(target func([]Value) *Object, boundArgs []Value) func([]Value) *Object { +func (r *Runtime) boundConstruct(target func([]Value, *Object) *Object, boundArgs []Value) func([]Value, *Object) *Object { if target == nil { return nil } @@ -102,37 +126,20 @@ func (r *Runtime) boundConstruct(target func([]Value) *Object, boundArgs []Value args = make([]Value, len(boundArgs)-1) copy(args, boundArgs[1:]) } - return func(fargs []Value) *Object { + return func(fargs []Value, newTarget *Object) *Object { a := append(args, fargs...) copy(a, args) - return target(a) + return target(a, newTarget) } } func (r *Runtime) functionproto_bind(call FunctionCall) Value { obj := r.toObject(call.This) - f := obj.self - var fcall func(FunctionCall) Value - var construct func([]Value) *Object -repeat: - switch ff := f.(type) { - case *funcObject: - fcall = ff.Call - construct = ff.construct - case *nativeFuncObject: - fcall = ff.f - construct = ff.construct - case *boundFuncObject: - f = &ff.nativeFuncObject - goto repeat - case *lazyObject: - f = ff.create(obj) - goto repeat - default: - r.typeErrorResult(true, "Value is not callable: %s", obj.toString()) - } - l := int(toUInt32(obj.self.getStr("length"))) + fcall := r.toCallable(call.This) + construct := obj.self.assertConstructor() + + l := int(toUInt32(obj.self.getStr("length", nil))) l -= len(call.Arguments) - 1 if l < 0 { l = 0 diff --git a/builtin_json.go b/builtin_json.go index bbcb18e1..dea31878 100644 --- a/builtin_json.go +++ b/builtin_json.go @@ -31,7 +31,7 @@ func (r *Runtime) builtinJSON_parse(call FunctionCall) Value { if reviver != nil { root := r.NewObject() - root.self.putStr("", value, false) + root.self.setOwnStr("", value, false) return r.builtinJSON_reviveWalk(reviver, root, stringEmpty) } @@ -85,17 +85,7 @@ func (r *Runtime) builtinJSON_decodeObject(d *json.Decoder) (*Object, error) { return nil, err } - if key == __proto__ { - descr := propertyDescr{ - Value: value, - Writable: FLAG_TRUE, - Enumerable: FLAG_TRUE, - Configurable: FLAG_TRUE, - } - object.self.defineOwnProperty(string__proto__, descr, false) - } else { - object.self.putStr(key, value, false) - } + object.self._putProp(key, value, true, true, true) } return object, nil } @@ -138,40 +128,31 @@ func (r *Runtime) builtinJSON_decodeArray(d *json.Decoder) (*Object, error) { return r.newArrayValues(arrayValue), nil } -func isArray(object *Object) bool { - switch object.self.className() { - case classArray: - return true - default: - return false - } -} - func (r *Runtime) builtinJSON_reviveWalk(reviver func(FunctionCall) Value, holder *Object, name Value) Value { - value := holder.self.get(name) + value := holder.get(name, nil) if value == nil { value = _undefined } if object := value.(*Object); object != nil { if isArray(object) { - length := object.self.getStr("length").ToInteger() + length := object.self.getStr("length", nil).ToInteger() for index := int64(0); index < length; index++ { name := intToValue(index) value := r.builtinJSON_reviveWalk(reviver, object, name) if value == _undefined { - object.self.delete(name, false) + object.delete(name, false) } else { - object.self.put(name, value, false) + object.setOwn(name, value, false) } } } else { - for item, f := object.self.enumerate(false, false)(); f != nil; item, f = f() { + for _, itemName := range object.self.ownKeys(false, nil) { value := r.builtinJSON_reviveWalk(reviver, object, name) if value == _undefined { - object.self.deleteStr(item.name, false) + object.self.deleteStr(itemName.String(), false) } else { - object.self.putStr(item.name, value, false) + object.self.setOwnStr(itemName.String(), value, false) } } } @@ -199,21 +180,18 @@ func (r *Runtime) builtinJSON_stringify(call FunctionCall) Value { replacer, _ := call.Argument(1).(*Object) if replacer != nil { if isArray(replacer) { - length := replacer.self.getStr("length").ToInteger() + length := replacer.self.getStr("length", nil).ToInteger() seen := map[string]bool{} propertyList := make([]Value, length) length = 0 for index := range propertyList { var name string - value := replacer.self.get(intToValue(int64(index))) - if s, ok := value.assertString(); ok { - name = s.String() - } else if _, ok := value.assertInt(); ok { - name = value.String() - } else if _, ok := value.assertFloat(); ok { + value := replacer.self.getIdx(valueInt(int64(index)), nil) + switch v := value.(type) { + case valueFloat, valueInt, valueString: name = value.String() - } else if o, ok := value.(*Object); ok { - switch o.self.className() { + case *Object: + switch v.self.className() { case classNumber, classString: name = value.String() } @@ -241,12 +219,12 @@ func (r *Runtime) builtinJSON_stringify(call FunctionCall) Value { } isNum := false var num int64 - num, isNum = spaceValue.assertInt() - if !isNum { - if f, ok := spaceValue.assertFloat(); ok { - num = int64(f) - isNum = true - } + if i, ok := spaceValue.(valueInt); ok { + num = int64(i) + isNum = true + } else if f, ok := spaceValue.(valueFloat); ok { + num = int64(f) + isNum = true } if isNum { if num > 0 { @@ -256,7 +234,7 @@ func (r *Runtime) builtinJSON_stringify(call FunctionCall) Value { ctx.gap = strings.Repeat(" ", int(num)) } } else { - if s, ok := spaceValue.assertString(); ok { + if s, ok := spaceValue.(valueString); ok { str := s.String() if len(str) > 10 { ctx.gap = str[:10] @@ -275,18 +253,18 @@ func (r *Runtime) builtinJSON_stringify(call FunctionCall) Value { func (ctx *_builtinJSON_stringifyContext) do(v Value) bool { holder := ctx.r.NewObject() - holder.self.putStr("", v, false) + holder.self.setOwnStr("", v, false) return ctx.str(stringEmpty, holder) } func (ctx *_builtinJSON_stringifyContext) str(key Value, holder *Object) bool { - value := holder.self.get(key) + value := holder.get(key, nil) if value == nil { value = _undefined } if object, ok := value.(*Object); ok { - if toJSON, ok := object.self.getStr("toJSON").(*Object); ok { + if toJSON, ok := object.self.getStr("toJSON", nil).(*Object); ok { if c, ok := toJSON.self.assertCallable(); ok { value = c(FunctionCall{ This: value, @@ -384,7 +362,7 @@ func (ctx *_builtinJSON_stringifyContext) ja(array *Object) { stepback = ctx.indent ctx.indent += ctx.gap } - length := array.self.getStr("length").ToInteger() + length := array.self.getStr("length", nil).ToInteger() if length == 0 { ctx.buf.WriteString("[]") return @@ -436,9 +414,7 @@ func (ctx *_builtinJSON_stringifyContext) jo(object *Object) { var props []Value if ctx.propertyList == nil { - for item, f := object.self.enumerate(false, false)(); f != nil; item, f = f() { - props = append(props, newStringValue(item.name)) - } + props = append(props, object.self.ownKeys(false, nil)...) } else { props = ctx.propertyList } diff --git a/builtin_map.go b/builtin_map.go index e0c437e9..d32fae65 100644 --- a/builtin_map.go +++ b/builtin_map.go @@ -138,7 +138,7 @@ func (r *Runtime) mapProto_getSize(call FunctionCall) Value { return intToValue(int64(mo.m.size)) } -func (r *Runtime) builtin_newMap(args []Value) *Object { +func (r *Runtime) builtin_newMap(args []Value, proto *Object) *Object { o := &Object{runtime: r} mo := &mapObject{} @@ -146,19 +146,19 @@ func (r *Runtime) builtin_newMap(args []Value) *Object { mo.val = o mo.extensible = true o.self = mo - mo.prototype = r.global.MapPrototype + mo.prototype = proto mo.init() if len(args) > 0 { if arg := args[0]; arg != nil && arg != _undefined && arg != _null { - adder := mo.getStr("set") + adder := mo.getStr("set", nil) iter := r.getIterator(arg, nil) - i0 := intToValue(0) - i1 := intToValue(1) + i0 := valueInt(0) + i1 := valueInt(1) if adder == r.global.mapAdder { r.iterate(iter, func(item Value) { itemObj := r.toObject(item) - k := nilSafe(itemObj.self.get(i0)) - v := nilSafe(itemObj.self.get(i1)) + k := nilSafe(itemObj.self.getIdx(i0, nil)) + v := nilSafe(itemObj.self.getIdx(i1, nil)) mo.m.set(k, v) }) } else { @@ -168,8 +168,8 @@ func (r *Runtime) builtin_newMap(args []Value) *Object { } r.iterate(iter, func(item Value) { itemObj := r.toObject(item) - k := itemObj.self.get(i0) - v := itemObj.self.get(i1) + k := itemObj.self.getIdx(i0, nil) + v := itemObj.self.getIdx(i1, nil) adderFn(FunctionCall{This: o, Arguments: []Value{k, v}}) }) } @@ -220,7 +220,7 @@ func (r *Runtime) createMapProto(val *Object) objectImpl { o._putProp("forEach", r.newNativeFunc(r.mapProto_forEach, nil, "forEach", nil, 1), true, false, true) o._putProp("has", r.newNativeFunc(r.mapProto_has, nil, "has", nil, 1), true, false, true) o._putProp("get", r.newNativeFunc(r.mapProto_get, nil, "get", nil, 1), true, false, true) - o.putStr("size", &valueProperty{ + o.setOwnStr("size", &valueProperty{ getterFunc: r.newNativeFunc(r.mapProto_getSize, nil, "get size", nil, 0), accessor: true, writable: true, @@ -231,19 +231,19 @@ func (r *Runtime) createMapProto(val *Object) objectImpl { entriesFunc := r.newNativeFunc(r.mapProto_entries, nil, "entries", nil, 0) o._putProp("entries", entriesFunc, true, false, true) - o.put(symIterator, valueProp(entriesFunc, true, false, true), true) - o.put(symToStringTag, valueProp(asciiString(classMap), false, false, true), true) + o._putSym(symIterator, valueProp(entriesFunc, true, false, true)) + o._putSym(symToStringTag, valueProp(asciiString(classMap), false, false, true)) return o } func (r *Runtime) createMap(val *Object) objectImpl { o := r.newNativeFuncObj(val, r.constructorThrower("Map"), r.builtin_newMap, "Map", r.global.MapPrototype, 0) - o.putSym(symSpecies, &valueProperty{ + o._putSym(symSpecies, &valueProperty{ getterFunc: r.newNativeFunc(r.returnThis, nil, "get [Symbol.species]", nil, 0), accessor: true, configurable: true, - }, true) + }) return o } @@ -252,7 +252,7 @@ func (r *Runtime) createMapIterProto(val *Object) objectImpl { o := newBaseObjectObj(val, r.global.IteratorPrototype, classObject) o._putProp("next", r.newNativeFunc(r.mapIterProto_next, nil, "next", nil, 0), true, false, true) - o.put(symToStringTag, valueProp(asciiString(classMapIterator), false, false, true), true) + o._putSym(symToStringTag, valueProp(asciiString(classMapIterator), false, false, true)) return o } diff --git a/builtin_math.go b/builtin_math.go index f6ec2162..c275e94f 100644 --- a/builtin_math.go +++ b/builtin_math.go @@ -88,15 +88,15 @@ func (r *Runtime) math_min(call FunctionCall) Value { func (r *Runtime) math_pow(call FunctionCall) Value { x := call.Argument(0) y := call.Argument(1) - if x, ok := x.assertInt(); ok { - if y, ok := y.assertInt(); ok && y >= 0 && y < 64 { + if x, ok := x.(valueInt); ok { + if y, ok := y.(valueInt); ok && y >= 0 && y < 64 { if y == 0 { return intToValue(1) } if x == 0 { return intToValue(0) } - ip := ipow(x, y) + ip := ipow(int64(x), int64(y)) if ip != 0 { return intToValue(ip) } diff --git a/builtin_number.go b/builtin_number.go index a1516398..2154e481 100644 --- a/builtin_number.go +++ b/builtin_number.go @@ -10,22 +10,16 @@ func (r *Runtime) numberproto_valueOf(call FunctionCall) Value { if !isNumber(this) { r.typeErrorResult(true, "Value is not a number") } - if _, ok := this.assertInt(); ok { + switch t := this.(type) { + case valueInt, valueFloat: return this - } - - if _, ok := this.assertFloat(); ok { - return this - } - - if obj, ok := this.(*Object); ok { - if v, ok := obj.self.(*primitiveValueObject); ok { + case *Object: + if v, ok := t.self.(*primitiveValueObject); ok { return v.pValue } } - r.typeErrorResult(true, "Number.prototype.valueOf is not generic") - return nil + panic(r.NewTypeError("Number.prototype.valueOf is not generic")) } func isNumber(v Value) bool { diff --git a/builtin_object.go b/builtin_object.go index 1ae0abdc..bd679026 100644 --- a/builtin_object.go +++ b/builtin_object.go @@ -23,10 +23,7 @@ func (r *Runtime) object_getPrototypeOf(call FunctionCall) Value { return p } -func (r *Runtime) object_getOwnPropertyDescriptor(call FunctionCall) Value { - obj := call.Argument(0).ToObject(r) - propName := toPropertyKey(call.Argument(1)) - desc := obj.self.getOwnProp(propName) +func (r *Runtime) valuePropToDescriptorObject(desc Value) Value { if desc == nil { return _undefined } @@ -49,63 +46,114 @@ func (r *Runtime) object_getOwnPropertyDescriptor(call FunctionCall) Value { } ret := r.NewObject() - o := ret.self + obj := ret.self if !accessor { - o.putStr("value", value, false) - o.putStr("writable", r.toBoolean(writable), false) + obj.setOwnStr("value", value, false) + obj.setOwnStr("writable", r.toBoolean(writable), false) } else { if get != nil { - o.putStr("get", get, false) + obj.setOwnStr("get", get, false) } else { - o.putStr("get", _undefined, false) + obj.setOwnStr("get", _undefined, false) } if set != nil { - o.putStr("set", set, false) + obj.setOwnStr("set", set, false) } else { - o.putStr("set", _undefined, false) + obj.setOwnStr("set", _undefined, false) } } - o.putStr("enumerable", r.toBoolean(enumerable), false) - o.putStr("configurable", r.toBoolean(configurable), false) + obj.setOwnStr("enumerable", r.toBoolean(enumerable), false) + obj.setOwnStr("configurable", r.toBoolean(configurable), false) return ret } +func (r *Runtime) object_getOwnPropertyDescriptor(call FunctionCall) Value { + o := call.Argument(0).ToObject(r) + propName := toPropertyKey(call.Argument(1)) + return r.valuePropToDescriptorObject(o.getOwnProp(propName)) +} + func (r *Runtime) object_getOwnPropertyNames(call FunctionCall) Value { - // ES6 obj := call.Argument(0).ToObject(r) - // obj := r.toObject(call.Argument(0)) - var values []Value - for item, f := obj.self.enumerate(true, false)(); f != nil; item, f = f() { - values = append(values, newStringValue(item.name)) - } - return r.newArrayValues(values) + return r.newArrayValues(obj.self.ownKeys(true, nil)) } func (r *Runtime) object_getOwnPropertySymbols(call FunctionCall) Value { obj := call.Argument(0).ToObject(r) - return r.newArrayValues(obj.self.getOwnSymbols()) + return r.newArrayValues(obj.self.ownSymbols()) +} + +func (r *Runtime) toValueProp(v Value) *valueProperty { + if v == nil || v == _undefined { + return nil + } + obj := r.toObject(v) + getter := obj.self.getStr("get", nil) + setter := obj.self.getStr("set", nil) + writable := obj.self.getStr("writable", nil) + value := obj.self.getStr("value", nil) + if (getter != nil || setter != nil) && (value != nil || writable != nil) { + r.typeErrorResult(true, "Invalid property descriptor. Cannot both specify accessors and a value or writable attribute") + } + + ret := &valueProperty{} + if writable != nil && writable.ToBoolean() { + ret.writable = true + } + if e := obj.self.getStr("enumerable", nil); e != nil && e.ToBoolean() { + ret.enumerable = true + } + if c := obj.self.getStr("configurable", nil); c != nil && c.ToBoolean() { + ret.configurable = true + } + ret.value = value + + if getter != nil && getter != _undefined { + o := r.toObject(getter) + if _, ok := o.self.assertCallable(); !ok { + r.typeErrorResult(true, "getter must be a function") + } + ret.getterFunc = o + } + + if setter != nil && setter != _undefined { + o := r.toObject(v) + if _, ok := o.self.assertCallable(); !ok { + r.typeErrorResult(true, "setter must be a function") + } + ret.setterFunc = o + } + + if ret.getterFunc != nil || ret.setterFunc != nil { + ret.accessor = true + } + + return ret } -func (r *Runtime) toPropertyDescr(v Value) (ret propertyDescr) { +func (r *Runtime) toPropertyDescriptor(v Value) (ret PropertyDescriptor) { if o, ok := v.(*Object); ok { descr := o.self - ret.Value = descr.getStr("value") + // Save the original descriptor for reference + ret.jsDescriptor = o + + ret.Value = descr.getStr("value", nil) - if p := descr.getStr("writable"); p != nil { + if p := descr.getStr("writable", nil); p != nil { ret.Writable = ToFlag(p.ToBoolean()) } - if p := descr.getStr("enumerable"); p != nil { + if p := descr.getStr("enumerable", nil); p != nil { ret.Enumerable = ToFlag(p.ToBoolean()) } - if p := descr.getStr("configurable"); p != nil { + if p := descr.getStr("configurable", nil); p != nil { ret.Configurable = ToFlag(p.ToBoolean()) } - ret.Getter = descr.getStr("get") - ret.Setter = descr.getStr("set") + ret.Getter = descr.getStr("get", nil) + ret.Setter = descr.getStr("set", nil) if ret.Getter != nil && ret.Getter != _undefined { if _, ok := r.toObject(ret.Getter).self.assertCallable(); !ok { @@ -121,7 +169,6 @@ func (r *Runtime) toPropertyDescr(v Value) (ret propertyDescr) { if (ret.Getter != nil || ret.Setter != nil) && (ret.Value != nil || ret.Writable != FLAG_NOT_SET) { r.typeErrorResult(true, "Invalid property descriptor. Cannot both specify accessors and a value or writable attribute") - return } } else { r.typeErrorResult(true, "Property description must be an object: %s", v.String()) @@ -133,18 +180,20 @@ func (r *Runtime) toPropertyDescr(v Value) (ret propertyDescr) { func (r *Runtime) _defineProperties(o *Object, p Value) { type propItem struct { name string - prop propertyDescr + prop PropertyDescriptor } props := p.ToObject(r) - var list []propItem - for item, f := props.self.enumerate(false, false)(); f != nil; item, f = f() { + names := props.self.ownKeys(false, nil) + list := make([]propItem, 0, len(names)) + for _, itemName := range names { + itemNameStr := itemName.String() list = append(list, propItem{ - name: item.name, - prop: r.toPropertyDescr(props.self.getStr(item.name)), + name: itemNameStr, + prop: r.toPropertyDescriptor(props.self.getStr(itemNameStr, nil)), }) } for _, prop := range list { - o.self.defineOwnProperty(newStringValue(prop.name), prop.prop, true) + o.self.defineOwnPropertyStr(prop.name, prop.prop, true) } } @@ -168,8 +217,8 @@ func (r *Runtime) object_create(call FunctionCall) Value { func (r *Runtime) object_defineProperty(call FunctionCall) (ret Value) { if obj, ok := call.Argument(0).(*Object); ok { - descr := r.toPropertyDescr(call.Argument(2)) - obj.self.defineOwnProperty(call.Argument(1), descr, true) + descr := r.toPropertyDescriptor(call.Argument(2)) + obj.defineOwnProperty(toPropertyKey(call.Argument(1)), descr, true) ret = call.Argument(0) } else { r.typeErrorResult(true, "Object.defineProperty called on non-object") @@ -187,26 +236,13 @@ func (r *Runtime) object_seal(call FunctionCall) Value { // ES6 arg := call.Argument(0) if obj, ok := arg.(*Object); ok { - descr := propertyDescr{ + descr := PropertyDescriptor{ Writable: FLAG_TRUE, Enumerable: FLAG_TRUE, Configurable: FLAG_FALSE, } - for item, f := obj.self.enumerate(true, false)(); f != nil; item, f = f() { - v := obj.self.getOwnPropStr(item.name) - if prop, ok := v.(*valueProperty); ok { - if !prop.configurable { - continue - } - prop.configurable = false - } else { - descr.Value = v - obj.self.defineOwnProperty(newStringValue(item.name), descr, true) - //obj.self._putProp(item.name, v, true, true, false) - } - } - for _, sym := range obj.self.getOwnSymbols() { - v := obj.self.getOwnProp(sym) + for _, key := range obj.self.ownPropertyKeys(true, nil) { + v := obj.getOwnProp(key) if prop, ok := v.(*valueProperty); ok { if !prop.configurable { continue @@ -214,10 +250,10 @@ func (r *Runtime) object_seal(call FunctionCall) Value { prop.configurable = false } else { descr.Value = v - obj.self.defineOwnProperty(sym, descr, true) + obj.defineOwnProperty(key, descr, true) } } - obj.self.preventExtensions() + obj.self.preventExtensions(false) return obj } return arg @@ -226,13 +262,13 @@ func (r *Runtime) object_seal(call FunctionCall) Value { func (r *Runtime) object_freeze(call FunctionCall) Value { arg := call.Argument(0) if obj, ok := arg.(*Object); ok { - descr := propertyDescr{ + descr := PropertyDescriptor{ Writable: FLAG_FALSE, Enumerable: FLAG_TRUE, Configurable: FLAG_FALSE, } - for item, f := obj.self.enumerate(true, false)(); f != nil; item, f = f() { - v := obj.self.getOwnPropStr(item.name) + for _, key := range obj.self.ownPropertyKeys(true, nil) { + v := obj.getOwnProp(key) if prop, ok := v.(*valueProperty); ok { prop.configurable = false if prop.value != nil { @@ -240,22 +276,10 @@ func (r *Runtime) object_freeze(call FunctionCall) Value { } } else { descr.Value = v - obj.self.defineOwnProperty(newStringValue(item.name), descr, true) + obj.defineOwnProperty(key, descr, true) } } - for _, sym := range obj.self.getOwnSymbols() { - v := obj.self.getOwnProp(sym) - if prop, ok := v.(*valueProperty); ok { - prop.configurable = false - if prop.value != nil { - prop.writable = false - } - } else { - descr.Value = v - obj.self.defineOwnProperty(sym, descr, true) - } - } - obj.self.preventExtensions() + obj.self.preventExtensions(false) return obj } else { // ES6 behavior @@ -266,7 +290,7 @@ func (r *Runtime) object_freeze(call FunctionCall) Value { func (r *Runtime) object_preventExtensions(call FunctionCall) (ret Value) { arg := call.Argument(0) if obj, ok := arg.(*Object); ok { - obj.self.preventExtensions() + obj.self.preventExtensions(false) return obj } // ES6 @@ -280,8 +304,8 @@ func (r *Runtime) object_isSealed(call FunctionCall) Value { if obj.self.isExtensible() { return valueFalse } - for item, f := obj.self.enumerate(true, false)(); f != nil; item, f = f() { - prop := obj.self.getOwnPropStr(item.name) + for _, key := range obj.self.ownPropertyKeys(true, nil) { + prop := obj.getOwnProp(key) if prop, ok := prop.(*valueProperty); ok { if prop.configurable { return valueFalse @@ -290,20 +314,6 @@ func (r *Runtime) object_isSealed(call FunctionCall) Value { return valueFalse } } - for _, sym := range obj.self.getOwnSymbols() { - prop := obj.self.getOwnProp(sym) - if prop, ok := prop.(*valueProperty); ok { - if prop.configurable { - return valueFalse - } - } else { - return valueFalse - } - } - } else { - // ES6 - //r.typeErrorResult(true, "Object.isSealed called on non-object") - return valueTrue } return valueTrue } @@ -313,8 +323,8 @@ func (r *Runtime) object_isFrozen(call FunctionCall) Value { if obj.self.isExtensible() { return valueFalse } - for item, f := obj.self.enumerate(true, false)(); f != nil; item, f = f() { - prop := obj.self.getOwnPropStr(item.name) + for _, key := range obj.self.ownPropertyKeys(true, nil) { + prop := obj.getOwnProp(key) if prop, ok := prop.(*valueProperty); ok { if prop.configurable || prop.value != nil && prop.writable { return valueFalse @@ -323,20 +333,6 @@ func (r *Runtime) object_isFrozen(call FunctionCall) Value { return valueFalse } } - for _, sym := range obj.self.getOwnSymbols() { - prop := obj.self.getOwnProp(sym) - if prop, ok := prop.(*valueProperty); ok { - if prop.configurable || prop.value != nil && prop.writable { - return valueFalse - } - } else { - return valueFalse - } - } - } else { - // ES6 - //r.typeErrorResult(true, "Object.isFrozen called on non-object") - return valueTrue } return valueTrue } @@ -355,24 +351,15 @@ func (r *Runtime) object_isExtensible(call FunctionCall) Value { } func (r *Runtime) object_keys(call FunctionCall) Value { - // ES6 obj := call.Argument(0).ToObject(r) - //if obj, ok := call.Argument(0).(*valueObject); ok { - var keys []Value - for item, f := obj.self.enumerate(false, false)(); f != nil; item, f = f() { - keys = append(keys, newStringValue(item.name)) - } - return r.newArrayValues(keys) - //} else { - // r.typeErrorResult(true, "Object.keys called on non-object") - //} - //return nil + + return r.newArrayValues(obj.self.ownKeys(false, nil)) } func (r *Runtime) objectproto_hasOwnProperty(call FunctionCall) Value { p := toPropertyKey(call.Argument(0)) o := call.This.ToObject(r) - if o.self.hasOwnProperty(p) { + if o.hasOwnProperty(p) { return valueTrue } else { return valueFalse @@ -398,7 +385,7 @@ func (r *Runtime) objectproto_isPrototypeOf(call FunctionCall) Value { func (r *Runtime) objectproto_propertyIsEnumerable(call FunctionCall) Value { p := toPropertyKey(call.Argument(0)) o := call.This.ToObject(r) - pv := o.self.getOwnProp(p) + pv := o.getOwnProp(p) if pv == nil { return valueFalse } @@ -419,14 +406,16 @@ func (r *Runtime) objectproto_toString(call FunctionCall) Value { default: obj := o.ToObject(r) var clsName string - if tag := obj.self.get(symToStringTag); tag != nil { - if str, ok := tag.assertString(); ok { + if isArray(obj) { + clsName = classArray + } else { + clsName = obj.self.className() + } + if tag := obj.self.getSym(symToStringTag, nil); tag != nil { + if str, ok := tag.(valueString); ok { clsName = str.String() } } - if clsName == "" { - clsName = obj.self.className() - } return newStringValue(fmt.Sprintf("[object %s]", clsName)) } } @@ -436,6 +425,25 @@ func (r *Runtime) objectproto_toLocaleString(call FunctionCall) Value { return toString(FunctionCall{This: call.This}) } +func (r *Runtime) objectproto_getProto(call FunctionCall) Value { + proto := call.This.ToObject(r).self.proto() + if proto != nil { + return proto + } + return _null +} + +func (r *Runtime) objectproto_setProto(call FunctionCall) Value { + o := call.This + r.checkObjectCoercible(o) + proto := r.toProto(call.Argument(0)) + if o, ok := o.(*Object); ok { + o.self.setProto(proto, true) + } + + return _undefined +} + func (r *Runtime) objectproto_valueOf(call FunctionCall) Value { return call.This.ToObject(r) } @@ -446,23 +454,15 @@ func (r *Runtime) object_assign(call FunctionCall) Value { for _, arg := range call.Arguments[1:] { if arg != _undefined && arg != _null { source := arg.ToObject(r) - for item, f := source.self.enumerate(false, false)(); f != nil; item, f = f() { - p := source.self.getOwnPropStr(item.name) - if v, ok := p.(*valueProperty); ok { - p = v.get(source) + for _, key := range source.self.ownPropertyKeys(false, nil) { + p := source.getOwnProp(key) + if p == nil { + continue } - to.self.putStr(item.name, p, true) - } - - for _, sym := range source.self.getOwnSymbols() { - p := source.self.getOwnProp(sym) if v, ok := p.(*valueProperty); ok { - if !v.enumerable { - continue - } p = v.get(source) } - to.self.put(sym, p, true) + to.setOwn(key, p, true) } } } @@ -475,22 +475,23 @@ func (r *Runtime) object_is(call FunctionCall) Value { return r.toBoolean(call.Argument(0).SameAs(call.Argument(1))) } -func (r *Runtime) object_setPrototypeOf(call FunctionCall) Value { - o := call.Argument(0) - r.checkObjectCoercible(o) - proto := call.Argument(1) - var protoObj *Object +func (r *Runtime) toProto(proto Value) *Object { if proto != _null { if obj, ok := proto.(*Object); ok { - protoObj = obj + return obj } else { panic(r.NewTypeError("Object prototype may only be an Object or null: %s", proto)) } } + return nil +} + +func (r *Runtime) object_setPrototypeOf(call FunctionCall) Value { + o := call.Argument(0) + r.checkObjectCoercible(o) + proto := r.toProto(call.Argument(1)) if o, ok := o.(*Object); ok { - if res := o.self.setProto(protoObj); res != nil { - panic(res) - } + o.self.setProto(proto, true) } return o @@ -504,6 +505,11 @@ func (r *Runtime) initObject() { o._putProp("hasOwnProperty", r.newNativeFunc(r.objectproto_hasOwnProperty, nil, "hasOwnProperty", nil, 1), true, false, true) o._putProp("isPrototypeOf", r.newNativeFunc(r.objectproto_isPrototypeOf, nil, "isPrototypeOf", nil, 1), true, false, true) o._putProp("propertyIsEnumerable", r.newNativeFunc(r.objectproto_propertyIsEnumerable, nil, "propertyIsEnumerable", nil, 1), true, false, true) + o.defineOwnPropertyStr(__proto__, PropertyDescriptor{ + Getter: r.newNativeFunc(r.objectproto_getProto, nil, "get __proto__", nil, 0), + Setter: r.newNativeFunc(r.objectproto_setProto, nil, "set __proto__", nil, 1), + Configurable: FLAG_TRUE, + }, true) r.global.Object = r.newNativeFuncConstruct(r.builtin_Object, classObject, r.global.ObjectPrototype, 1) o = r.global.Object.self diff --git a/builtin_proxy.go b/builtin_proxy.go new file mode 100644 index 00000000..faae41cc --- /dev/null +++ b/builtin_proxy.go @@ -0,0 +1,281 @@ +package goja + +import "fmt" + +func (r *Runtime) newNativeProxyHandler(nativeHandler *ProxyTrapConfig) *Object { + handler := r.NewObject() + r.proxyproto_nativehandler_gen_obj_obj(proxy_trap_getPrototypeOf, nativeHandler.GetPrototypeOf, handler) + r.proxyproto_nativehandler_setPrototypeOf(nativeHandler.SetPrototypeOf, handler) + r.proxyproto_nativehandler_gen_obj_bool(proxy_trap_isExtensible, nativeHandler.IsExtensible, handler) + r.proxyproto_nativehandler_gen_obj_bool(proxy_trap_preventExtensions, nativeHandler.PreventExtensions, handler) + r.proxyproto_nativehandler_getOwnPropertyDescriptor(nativeHandler.GetOwnPropertyDescriptor, handler) + r.proxyproto_nativehandler_defineProperty(nativeHandler.DefineProperty, handler) + r.proxyproto_nativehandler_gen_obj_string_bool(proxy_trap_has, nativeHandler.Has, handler) + r.proxyproto_nativehandler_get(nativeHandler.Get, handler) + r.proxyproto_nativehandler_set(nativeHandler.Set, handler) + r.proxyproto_nativehandler_gen_obj_string_bool(proxy_trap_deleteProperty, nativeHandler.DeleteProperty, handler) + r.proxyproto_nativehandler_gen_obj_obj(proxy_trap_ownKeys, nativeHandler.OwnKeys, handler) + r.proxyproto_nativehandler_apply(nativeHandler.Apply, handler) + r.proxyproto_nativehandler_construct(nativeHandler.Construct, handler) + return handler +} + +func (r *Runtime) proxyproto_nativehandler_gen_obj_obj(name proxyTrap, native func(*Object) *Object, handler *Object) { + if native != nil { + handler.self._putProp(string(name), r.newNativeFunc(func(call FunctionCall) Value { + if len(call.Arguments) >= 1 { + if t, ok := call.Argument(0).(*Object); ok { + return native(t) + } + } + panic(r.NewTypeError("%s needs to be called with target as Object", name)) + }, nil, fmt.Sprintf("[native %s]", name), nil, 1), true, true, true) + } +} + +func (r *Runtime) proxyproto_nativehandler_setPrototypeOf(native func(*Object, *Object) bool, handler *Object) { + if native != nil { + handler.self._putProp("setPrototypeOf", r.newNativeFunc(func(call FunctionCall) Value { + if len(call.Arguments) >= 2 { + if t, ok := call.Argument(0).(*Object); ok { + if p, ok := call.Argument(1).(*Object); ok { + s := native(t, p) + return r.ToValue(s) + } + } + } + panic(r.NewTypeError("setPrototypeOf needs to be called with target and prototype as Object")) + }, nil, "[native setPrototypeOf]", nil, 2), true, true, true) + } +} + +func (r *Runtime) proxyproto_nativehandler_gen_obj_bool(name proxyTrap, native func(*Object) bool, handler *Object) { + if native != nil { + handler.self._putProp(string(name), r.newNativeFunc(func(call FunctionCall) Value { + if len(call.Arguments) >= 1 { + if t, ok := call.Argument(0).(*Object); ok { + s := native(t) + return r.ToValue(s) + } + } + panic(r.NewTypeError("%s needs to be called with target as Object", name)) + }, nil, fmt.Sprintf("[native %s]", name), nil, 1), true, true, true) + } +} + +func (r *Runtime) proxyproto_nativehandler_getOwnPropertyDescriptor(native func(*Object, string) PropertyDescriptor, handler *Object) { + if native != nil { + handler.self._putProp("getOwnPropertyDescriptor", r.newNativeFunc(func(call FunctionCall) Value { + if len(call.Arguments) >= 2 { + if t, ok := call.Argument(0).(*Object); ok { + if p, ok := call.Argument(1).(valueString); ok { + desc := native(t, p.String()) + return desc.toValue(r) + } + } + } + panic(r.NewTypeError("getOwnPropertyDescriptor needs to be called with target as Object and prop as string")) + }, nil, "[native getOwnPropertyDescriptor]", nil, 2), true, true, true) + } +} + +func (r *Runtime) proxyproto_nativehandler_defineProperty(native func(*Object, string, PropertyDescriptor) bool, handler *Object) { + if native != nil { + handler.self._putProp("defineProperty", r.newNativeFunc(func(call FunctionCall) Value { + if len(call.Arguments) >= 3 { + if t, ok := call.Argument(0).(*Object); ok { + if k, ok := call.Argument(1).(valueString); ok { + propertyDescriptor := r.toPropertyDescriptor(call.Argument(2)) + s := native(t, k.String(), propertyDescriptor) + return r.ToValue(s) + } + } + } + panic(r.NewTypeError("defineProperty needs to be called with target as Object and propertyDescriptor as string and key as string")) + }, nil, "[native defineProperty]", nil, 3), true, true, true) + } +} + +func (r *Runtime) proxyproto_nativehandler_gen_obj_string_bool(name proxyTrap, native func(*Object, string) bool, handler *Object) { + if native != nil { + handler.self._putProp(string(name), r.newNativeFunc(func(call FunctionCall) Value { + if len(call.Arguments) >= 2 { + if t, ok := call.Argument(0).(*Object); ok { + if p, ok := call.Argument(1).(valueString); ok { + o := native(t, p.String()) + return r.ToValue(o) + } + } + } + panic(r.NewTypeError("%s needs to be called with target as Object and property as string", name)) + }, nil, fmt.Sprintf("[native %s]", name), nil, 2), true, true, true) + } +} + +func (r *Runtime) proxyproto_nativehandler_get(native func(*Object, string, *Object) Value, handler *Object) { + if native != nil { + handler.self._putProp("get", r.newNativeFunc(func(call FunctionCall) Value { + if len(call.Arguments) >= 3 { + if t, ok := call.Argument(0).(*Object); ok { + if p, ok := call.Argument(1).(valueString); ok { + if r, ok := call.Argument(2).(*Object); ok { + return native(t, p.String(), r) + } + } + } + } + panic(r.NewTypeError("get needs to be called with target and receiver as Object and property as string")) + }, nil, "[native get]", nil, 3), true, true, true) + } +} + +func (r *Runtime) proxyproto_nativehandler_set(native func(*Object, string, Value, *Object) bool, handler *Object) { + if native != nil { + handler.self._putProp("set", r.newNativeFunc(func(call FunctionCall) Value { + if len(call.Arguments) >= 4 { + if t, ok := call.Argument(0).(*Object); ok { + if p, ok := call.Argument(1).(valueString); ok { + v := call.Argument(2) + if re, ok := call.Argument(3).(*Object); ok { + s := native(t, p.String(), v, re) + return r.ToValue(s) + } + } + } + } + panic(r.NewTypeError("set needs to be called with target and receiver as Object, property as string and value as a legal javascript value")) + }, nil, "[native set]", nil, 4), true, true, true) + } +} + +func (r *Runtime) proxyproto_nativehandler_apply(native func(*Object, *Object, []Value) Value, handler *Object) { + if native != nil { + handler.self._putProp("apply", r.newNativeFunc(func(call FunctionCall) Value { + if len(call.Arguments) >= 3 { + if t, ok := call.Argument(0).(*Object); ok { + if this, ok := call.Argument(1).(*Object); ok { + if v, ok := call.Argument(2).(*Object); ok { + if a, ok := v.self.(*arrayObject); ok { + v := native(t, this, a.values) + return r.ToValue(v) + } + } + } + } + } + panic(r.NewTypeError("apply needs to be called with target and this as Object and argumentsList as an array of legal javascript values")) + }, nil, "[native apply]", nil, 3), true, true, true) + } +} + +func (r *Runtime) proxyproto_nativehandler_construct(native func(*Object, []Value, *Object) *Object, handler *Object) { + if native != nil { + handler.self._putProp("construct", r.newNativeFunc(func(call FunctionCall) Value { + if len(call.Arguments) >= 3 { + if t, ok := call.Argument(0).(*Object); ok { + if v, ok := call.Argument(1).(*Object); ok { + if newTarget, ok := call.Argument(2).(*Object); ok { + if a, ok := v.self.(*arrayObject); ok { + return native(t, a.values, newTarget) + } + } + } + } + } + panic(r.NewTypeError("construct needs to be called with target and newTarget as Object and argumentsList as an array of legal javascript values")) + }, nil, "[native construct]", nil, 3), true, true, true) + } +} + +type ProxyTrapConfig struct { + // A trap for Object.getPrototypeOf, Reflect.getPrototypeOf, __proto__, Object.prototype.isPrototypeOf, instanceof + GetPrototypeOf func(target *Object) (prototype *Object) + + // A trap for Object.setPrototypeOf, Reflect.setPrototypeOf + SetPrototypeOf func(target *Object, prototype *Object) (success bool) + + // A trap for Object.isExtensible, Reflect.isExtensible + IsExtensible func(target *Object) (success bool) + + // A trap for Object.preventExtensions, Reflect.preventExtensions + PreventExtensions func(target *Object) (success bool) + + // A trap for Object.getOwnPropertyDescriptor, Reflect.getOwnPropertyDescriptor + GetOwnPropertyDescriptor func(target *Object, prop string) (propertyDescriptor PropertyDescriptor) + + // A trap for Object.defineProperty, Reflect.defineProperty + DefineProperty func(target *Object, key string, propertyDescriptor PropertyDescriptor) (success bool) + + // A trap for the in operator, with operator, Reflect.has + Has func(target *Object, property string) (available bool) + + // A trap for getting property values, Reflect.get + Get func(target *Object, property string, receiver *Object) (value Value) + + // A trap for setting property values, Reflect.set + Set func(target *Object, property string, value Value, receiver *Object) (success bool) + + // A trap for the delete operator, Reflect.deleteProperty + DeleteProperty func(target *Object, property string) (success bool) + + // A trap for Object.getOwnPropertyNames, Object.getOwnPropertySymbols, Object.keys, Reflect.ownKeys + OwnKeys func(target *Object) (object *Object) + + // A trap for a function call, Function.prototype.apply, Function.prototype.call, Reflect.apply + Apply func(target *Object, this *Object, argumentsList []Value) (value Value) + + // A trap for the new operator, Reflect.construct + Construct func(target *Object, argumentsList []Value, newTarget *Object) (value *Object) +} + +func (r *Runtime) newProxy(args []Value, proto *Object) *Object { + if len(args) >= 2 { + if target, ok := args[0].(*Object); ok { + if proxyHandler, ok := args[1].(*Object); ok { + return r.newProxyObject(target, proxyHandler, proto).val + } + } + } + panic(r.NewTypeError("Cannot create proxy with a non-object as target or handler")) +} + +func (r *Runtime) builtin_newProxy(args []Value, proto *Object) *Object { + return r.newProxy(args, proto) +} + +func (r *Runtime) NewProxy(target *Object, nativeHandler *ProxyTrapConfig) *Proxy { + handler := r.newNativeProxyHandler(nativeHandler) + proxy := r.newProxyObject(target, handler, r.global.Proxy) + return &Proxy{proxy: proxy} +} + +func (r *Runtime) builtin_proxy_revocable(call FunctionCall) Value { + if len(call.Arguments) >= 2 { + if target, ok := call.Argument(0).(*Object); ok { + if proxyHandler, ok := call.Argument(1).(*Object); ok { + proxy := r.newProxyObject(target, proxyHandler, nil) + revoke := r.newNativeFunc(func(FunctionCall) Value { + proxy.revoke() + return _undefined + }, nil, "", nil, 0) + ret := r.NewObject() + ret.self._putProp("proxy", proxy.val, true, true, true) + ret.self._putProp("revoke", revoke, true, true, true) + return ret + } + } + } + panic(r.NewTypeError("Cannot create proxy with a non-object as target or handler")) +} + +func (r *Runtime) createProxy(val *Object) objectImpl { + o := r.newNativeFuncObj(val, r.constructorThrower("Proxy"), r.builtin_newProxy, "Proxy", nil, 2) + + o._putProp("revocable", r.newNativeFunc(r.builtin_proxy_revocable, nil, "revocable", nil, 2), true, false, true) + return o +} + +func (r *Runtime) initProxy() { + r.global.Proxy = r.newLazyObject(r.createProxy) + r.addToGlobal("Proxy", r.global.Proxy) +} diff --git a/builtin_proxy_test.go b/builtin_proxy_test.go new file mode 100644 index 00000000..fc0a50f6 --- /dev/null +++ b/builtin_proxy_test.go @@ -0,0 +1,828 @@ +package goja + +import ( + "testing" +) + +func TestProxy_Object_target_getPrototypeOf(t *testing.T) { + const SCRIPT = ` + var proto = {}; + var obj = Object.create(proto); + var proxy = new Proxy(obj, {}); + var p = Object.getPrototypeOf(proxy); + assert.sameValue(proto, p); + ` + + testScript1(TESTLIB+SCRIPT, _undefined, t) +} + +func TestProxy_Object_proxy_getPrototypeOf(t *testing.T) { + const SCRIPT = ` + var proto = {}; + var proto2 = {}; + var obj = Object.create(proto); + var proxy = new Proxy(obj, { + getPrototypeOf: function(target) { + return proto2; + } + }); + var p = Object.getPrototypeOf(proxy); + assert.sameValue(proto2, p); + ` + + testScript1(TESTLIB+SCRIPT, _undefined, t) +} + +func TestProxy_Object_native_proxy_getPrototypeOf(t *testing.T) { + const SCRIPT = ` + var p = Object.getPrototypeOf(proxy); + assert.sameValue(proto, p); + ` + + runtime := New() + + prototype := runtime.NewObject() + runtime.Set("proto", prototype) + + target := runtime.NewObject() + proxy := runtime.NewProxy(target, &ProxyTrapConfig{ + GetPrototypeOf: func(target *Object) *Object { + return prototype + }, + }) + runtime.Set("proxy", proxy) + + _, err := runtime.RunString(TESTLIB + SCRIPT) + if err != nil { + panic(err) + } +} + +/*func TestProxy_Object_target_setPrototypeOf(t *testing.T) { + const SCRIPT = ` + var proto = {}; + var obj = {}; + Object.setPrototypeOf(obj, proto); + var proxy = new Proxy(obj, {}); + var p = Object.getPrototypeOf(proxy); + assert.sameValue(proto, p); + ` + + testScript1(TESTLIB+SCRIPT, _undefined, t) +} + +func TestProxy_Object_proxy_setPrototypeOf(t *testing.T) { + const SCRIPT = ` + var proto = {}; + var proto2 = {}; + var obj = {}; + Object.setPrototypeOf(obj, proto); + var proxy = new Proxy(obj, { + setPrototypeOf: function(target, prototype) { + return Object.setPrototypeOf(target, proto2); + } + }); + var p = Object.getPrototypeOf(proxy); + assert.sameValue(proto2, p); + ` + + testScript1(TESTLIB+SCRIPT, _undefined, t) +}*/ + +func TestProxy_Object_target_isExtensible(t *testing.T) { + const SCRIPT = ` + var obj = {}; + Object.seal(obj); + var proxy = new Proxy(obj, {}); + Object.isExtensible(proxy); + ` + + testScript1(SCRIPT, valueFalse, t) +} + +func TestProxy_proxy_isExtensible(t *testing.T) { + const SCRIPT = ` + var obj = {}; + Object.seal(obj); + var proxy = new Proxy(obj, { + isExtensible: function(target) { + return false; + } + }); + Object.isExtensible(proxy); + ` + + testScript1(SCRIPT, valueFalse, t) +} + +func TestProxy_native_proxy_isExtensible(t *testing.T) { + const SCRIPT = ` + (function() { + Object.preventExtensions(target); + return Object.isExtensible(proxy); + })(); + ` + + runtime := New() + + target := runtime.NewObject() + runtime.Set("target", target) + + proxy := runtime.NewProxy(target, &ProxyTrapConfig{ + IsExtensible: func(target *Object) (success bool) { + return false + }, + }) + runtime.Set("proxy", proxy) + + val, err := runtime.RunString(SCRIPT) + if err != nil { + panic(err) + } + if val.ToBoolean() { + t.Fatal() + } +} + +func TestProxy_Object_target_preventExtensions(t *testing.T) { + const SCRIPT = ` + var obj = { + canEvolve: true + }; + var proxy = new Proxy(obj, {}); + Object.preventExtensions(proxy); + proxy.canEvolve + ` + + testScript1(SCRIPT, valueTrue, t) +} + +func TestProxy_proxy_preventExtensions(t *testing.T) { + const SCRIPT = ` + var obj = { + canEvolve: true + }; + var proxy = new Proxy(obj, { + preventExtensions: function(target) { + target.canEvolve = false; + return false; + } + }); + Object.preventExtensions(proxy); + proxy.canEvolve; + ` + + testScript1(SCRIPT, valueFalse, t) +} + +func TestProxy_native_proxy_preventExtensions(t *testing.T) { + const SCRIPT = ` + (function() { + Object.preventExtensions(proxy); + return proxy.canEvolve; + })(); + ` + + runtime := New() + + target := runtime.NewObject() + target.Set("canEvolve", true) + runtime.Set("target", target) + + proxy := runtime.NewProxy(target, &ProxyTrapConfig{ + PreventExtensions: func(target *Object) (success bool) { + target.Set("canEvolve", false) + return false + }, + }) + runtime.Set("proxy", proxy) + + val, err := runtime.RunString(SCRIPT) + if err != nil { + panic(err) + } + if val.ToBoolean() { + t.Fatal() + } +} + +func TestProxy_Object_target_getOwnPropertyDescriptor(t *testing.T) { + const SCRIPT = ` + var desc = { + configurable: false, + enumerable: false, + value: 42, + writable: false + }; + + var obj = {}; + Object.defineProperty(obj, "foo", desc); + + var proxy = new Proxy(obj, {}); + + var desc2 = Object.getOwnPropertyDescriptor(proxy, "foo"); + desc2.value + ` + + testScript1(SCRIPT, valueInt(42), t) +} + +func TestProxy_proxy_getOwnPropertyDescriptor(t *testing.T) { + const SCRIPT = ` + var desc = { + configurable: false, + enumerable: false, + value: 42, + writable: false + }; + var proxy_desc = { + configurable: false, + enumerable: false, + value: 24, + writable: false + }; + + var obj = {}; + Object.defineProperty(obj, "foo", desc); + + var proxy = new Proxy(obj, { + getOwnPropertyDescriptor: function(target, property) { + return proxy_desc; + } + }); + + assert.throws(TypeError, function() { + Object.getOwnPropertyDescriptor(proxy, "foo"); + }); + undefined; + ` + + testScript1(TESTLIB+SCRIPT, _undefined, t) +} + +func TestProxy_native_proxy_getOwnPropertyDescriptor(t *testing.T) { + const SCRIPT = ` + (function() { + var desc = { + configurable: true, + enumerable: false, + value: 42, + writable: false + }; + var proxy_desc = { + configurable: true, + enumerable: false, + value: 24, + writable: false + }; + + var obj = {}; + Object.defineProperty(obj, "foo", desc); + + return function(constructor) { + var proxy = constructor(obj, proxy_desc); + + var desc2 = Object.getOwnPropertyDescriptor(proxy, "foo"); + return desc2.value + } + })(); + ` + + runtime := New() + + constructor := func(call FunctionCall) Value { + target := call.Argument(0).(*Object) + proxyDesc := call.Argument(1).(*Object) + + return runtime.NewProxy(target, &ProxyTrapConfig{ + GetOwnPropertyDescriptor: func(target *Object, prop string) PropertyDescriptor { + return runtime.toPropertyDescriptor(proxyDesc) + }, + }).proxy.val + } + + val, err := runtime.RunString(SCRIPT) + if err != nil { + panic(err) + } + + if c, ok := val.(*Object).self.assertCallable(); ok { + val := c(FunctionCall{ + This: val, + Arguments: []Value{runtime.ToValue(constructor)}, + }) + if i := val.ToInteger(); i != 24 { + t.Fatalf("val: %d", i) + } + } else { + t.Fatal("not a function") + } +} + +func TestProxy_Object_target_defineProperty(t *testing.T) { + const SCRIPT = ` + var obj = {}; + var proxy = new Proxy(obj, {}); + Object.defineProperty(proxy, "foo", { + value: "test123" + }); + proxy.foo; + ` + + testScript1(SCRIPT, asciiString("test123"), t) +} + +func TestProxy_proxy_defineProperty(t *testing.T) { + const SCRIPT = ` + var obj = {}; + var proxy = new Proxy(obj, { + defineProperty: function(target, prop, descriptor) { + target.foo = "321tset"; + return true; + } + }); + Object.defineProperty(proxy, "foo", { + value: "test123" + }); + proxy.foo; + ` + + testScript1(SCRIPT, asciiString("321tset"), t) +} + +func TestProxy_native_proxy_defineProperty(t *testing.T) { + const SCRIPT = ` + Object.defineProperty(proxy, "foo", { + value: "test123" + }); + proxy.foo; + ` + + runtime := New() + + target := runtime.NewObject() + + proxy := runtime.NewProxy(target, &ProxyTrapConfig{ + DefineProperty: func(target *Object, key string, propertyDescriptor PropertyDescriptor) (success bool) { + target.Set("foo", "321tset") + return true + }, + }) + runtime.Set("proxy", proxy) + + val, err := runtime.RunString(SCRIPT) + if err != nil { + panic(err) + } + if s := val.String(); s != "321tset" { + t.Fatalf("val: %s", s) + } +} + +func TestProxy_target_has_in(t *testing.T) { + const SCRIPT = ` + var obj = { + secret: true + }; + var proxy = new Proxy(obj, {}); + + "secret" in proxy + ` + + testScript1(SCRIPT, valueTrue, t) +} + +func TestProxy_proxy_has_in(t *testing.T) { + const SCRIPT = ` + var obj = { + secret: true + }; + var proxy = new Proxy(obj, { + has: function(target, key) { + return key !== "secret"; + } + }); + + "secret" in proxy + ` + + testScript1(SCRIPT, valueFalse, t) +} + +func TestProxy_target_has_with(t *testing.T) { + const SCRIPT = ` + var obj = { + secret: true + }; + var proxy = new Proxy(obj, {}); + + with(proxy) { + (secret); + } + ` + + testScript1(SCRIPT, valueTrue, t) +} + +func TestProxy_proxy_has_with(t *testing.T) { + const SCRIPT = ` + var obj = { + secret: true + }; + var proxy = new Proxy(obj, { + has: function(target, key) { + return key !== "secret"; + } + }); + + var thrown = false; + try { + with(proxy) { + (secret); + } + } catch (e) { + if (e instanceof ReferenceError) { + thrown = true; + } else { + throw e; + } + } + thrown; + ` + + testScript1(SCRIPT, valueTrue, t) +} + +func TestProxy_target_get(t *testing.T) { + const SCRIPT = ` + var obj = {}; + var proxy = new Proxy(obj, {}); + Object.defineProperty(proxy, "foo", { + value: "test123" + }); + proxy.foo; + ` + + testScript1(SCRIPT, asciiString("test123"), t) +} + +func TestProxy_proxy_get(t *testing.T) { + const SCRIPT = ` + var obj = {}; + var proxy = new Proxy(obj, { + get: function(target, prop, receiver) { + return "321tset" + } + }); + Object.defineProperty(proxy, "foo", { + value: "test123", + configurable: true, + }); + proxy.foo; + ` + + testScript1(SCRIPT, asciiString("321tset"), t) +} + +func TestProxy_target_set_prop(t *testing.T) { + const SCRIPT = ` + var obj = {}; + var proxy = new Proxy(obj, {}); + proxy.foo = "test123"; + proxy.foo; + ` + + testScript1(SCRIPT, asciiString("test123"), t) +} + +func TestProxy_proxy_set_prop(t *testing.T) { + const SCRIPT = ` + var obj = {}; + var proxy = new Proxy(obj, { + set: function(target, prop, receiver) { + target.foo = "321tset"; + return true; + } + }); + proxy.foo = "test123"; + proxy.foo; + ` + + testScript1(SCRIPT, asciiString("321tset"), t) +} +func TestProxy_target_set_associative(t *testing.T) { + const SCRIPT = ` + var obj = {}; + var proxy = new Proxy(obj, {}); + proxy["foo"] = "test123"; + proxy.foo; + ` + + testScript1(SCRIPT, asciiString("test123"), t) +} + +func TestProxy_proxy_set_associative(t *testing.T) { + const SCRIPT = ` + var obj = {}; + var proxy = new Proxy(obj, { + set: function(target, property, value, receiver) { + target["foo"] = "321tset"; + return true; + } + }); + proxy["foo"] = "test123"; + proxy.foo; + ` + + testScript1(SCRIPT, asciiString("321tset"), t) +} + +func TestProxy_target_delete(t *testing.T) { + const SCRIPT = ` + var obj = { + foo: "test" + }; + var proxy = new Proxy(obj, {}); + delete proxy.foo; + + proxy.foo; + ` + + testScript1(SCRIPT, _undefined, t) +} + +func TestProxy_proxy_delete(t *testing.T) { + const SCRIPT = ` + var obj = { + foo: "test" + }; + var proxy = new Proxy(obj, { + deleteProperty: function(target, prop) { + return true; + } + }); + delete proxy.foo; + + proxy.foo; + ` + + testScript1(SCRIPT, asciiString("test"), t) +} + +func TestProxy_target_keys(t *testing.T) { + const SCRIPT = ` + var obj = { + foo: "test" + }; + var proxy = new Proxy(obj, {}); + + var keys = Object.keys(proxy); + if (keys.length != 1) { + throw new Error("assertion error"); + } + ` + + testScript1(SCRIPT, _undefined, t) +} + +func TestProxy_proxy_keys(t *testing.T) { + const SCRIPT = ` + var obj = { + foo: "test" + }; + var proxy = new Proxy(obj, { + ownKeys: function(target) { + return ["foo", "bar"]; + } + }); + + var keys = Object.keys(proxy); + if (keys.length !== 1) { + throw new Error("length is "+keys.length); + } + if (keys[0] !== "foo") { + throw new Error("keys[0] is "+keys[0]); + } + ` + + testScript1(SCRIPT, _undefined, t) +} + +func TestProxy_target_call(t *testing.T) { + const SCRIPT = ` + var obj = function() { + return "test" + } + + var proxy = new Proxy(obj, {}); + + proxy(); + ` + + testScript1(SCRIPT, asciiString("test"), t) +} + +func TestProxy_proxy_call(t *testing.T) { + const SCRIPT = ` + var obj = function() { + return "test" + } + + var proxy = new Proxy(obj, { + apply: function(target, thisArg, args) { + return "tset" + } + }); + + proxy(); + ` + + testScript1(SCRIPT, asciiString("tset"), t) +} + +func TestProxy_target_func_apply(t *testing.T) { + const SCRIPT = ` + var obj = function() { + return "test" + } + + var proxy = new Proxy(obj, {}); + + proxy.apply(); + ` + + testScript1(SCRIPT, asciiString("test"), t) +} + +func TestProxy_proxy_func_apply(t *testing.T) { + const SCRIPT = ` + var obj = function() { + return "test" + } + + var proxy = new Proxy(obj, { + apply: function(target, thisArg, args) { + return "tset" + } + }); + + proxy.apply(); + ` + + testScript1(SCRIPT, asciiString("tset"), t) +} + +func TestProxy_target_func_call(t *testing.T) { + const SCRIPT = ` + var obj = function() { + return "test" + } + + var proxy = new Proxy(obj, {}); + + proxy.call(); + ` + + testScript1(SCRIPT, asciiString("test"), t) +} + +func TestProxy_proxy_func_call(t *testing.T) { + const SCRIPT = ` + var obj = function() { + return "test" + } + + var proxy = new Proxy(obj, { + apply: function(target, thisArg, args) { + return "tset" + } + }); + + proxy.call(); + ` + + testScript1(SCRIPT, asciiString("tset"), t) +} + +func TestProxy_target_new(t *testing.T) { + const SCRIPT = ` + var obj = function(word) { + this.foo = function() { + return word; + } + } + + var proxy = new Proxy(obj, {}); + + var instance = new proxy("test"); + instance.foo(); + ` + + testScript1(SCRIPT, asciiString("test"), t) +} + +func TestProxy_proxy_new(t *testing.T) { + const SCRIPT = ` + var obj = function(word) { + this.foo = function() { + return word; + } + } + + var proxy = new Proxy(obj, { + construct: function(target, args, newTarget) { + var word = args[0]; + return { + foo: function() { + return "caught-" + word + } + } + } + }); + + var instance = new proxy("test"); + instance.foo(); + ` + + testScript1(SCRIPT, asciiString("caught-test"), t) +} + +func TestProxy_Object_native_proxy_ownKeys(t *testing.T) { + headers := map[string][]string{ + "k0": {}, + } + vm := New() + proxy := vm.NewProxy(vm.NewObject(), &ProxyTrapConfig{ + OwnKeys: func(target *Object) (object *Object) { + keys := make([]interface{}, 0, len(headers)) + for k := range headers { + keys = append(keys, k) + } + return vm.ToValue(keys).ToObject(vm) + }, + GetOwnPropertyDescriptor: func(target *Object, prop string) PropertyDescriptor { + v, exists := headers[prop] + if exists { + return PropertyDescriptor{ + Value: vm.ToValue(v), + Enumerable: FLAG_TRUE, + Configurable: FLAG_TRUE, + } + } + return PropertyDescriptor{} + }, + }) + vm.Set("headers", proxy) + v, err := vm.RunString(` + var keys = Object.keys(headers); + keys.length === 1 && keys[0] === "k0"; + `) + if err != nil { + t.Fatal(err) + } + if v != valueTrue { + t.Fatal("not true", v) + } +} + +func TestProxy_proxy_forIn(t *testing.T) { + const SCRIPT = ` + var proto = { + a: 2, + protoProp: 1 + } + Object.defineProperty(proto, "protoNonEnum", { + value: 2, + writable: true, + configurable: true + }); + var target = Object.create(proto); + var proxy = new Proxy(target, { + ownKeys: function() { + return ["a", "b"]; + }, + getOwnPropertyDescriptor: function(target, p) { + switch (p) { + case "a": + case "b": + return { + value: 42, + enumerable: true, + configurable: true + } + } + }, + }); + + var forInResult = []; + for (var key in proxy) { + if (forInResult.indexOf(key) !== -1) { + throw new Error("Duplicate property "+key); + } + forInResult.push(key); + } + forInResult.length === 3 && forInResult[0] === "a" && forInResult[1] === "b" && forInResult[2] === "protoProp"; + ` + + testScript1(SCRIPT, valueTrue, t) +} diff --git a/builtin_reflect.go b/builtin_reflect.go new file mode 100644 index 00000000..d8e78471 --- /dev/null +++ b/builtin_reflect.go @@ -0,0 +1,132 @@ +package goja + +func (r *Runtime) builtin_reflect_apply(call FunctionCall) Value { + return r.toCallable(call.Argument(0))(FunctionCall{ + This: call.Argument(1), + Arguments: r.createListFromArrayLike(call.Argument(2))}) +} + +func (r *Runtime) toConstructor(v Value) func(args []Value, newTarget *Object) *Object { + if ctor := r.toObject(v).self.assertConstructor(); ctor != nil { + return ctor + } + panic(r.NewTypeError("Value is not a constructor")) +} + +func (r *Runtime) builtin_reflect_construct(call FunctionCall) Value { + target := call.Argument(0) + ctor := r.toConstructor(target) + var newTarget Value + if len(call.Arguments) > 2 { + newTarget = call.Argument(2) + r.toConstructor(newTarget) + } else { + newTarget = target + } + return ctor(r.createListFromArrayLike(call.Argument(1)), r.toObject(newTarget)) +} + +func (r *Runtime) builtin_reflect_defineProperty(call FunctionCall) Value { + target := r.toObject(call.Argument(0)) + key := toPropertyKey(call.Argument(1)) + desc := r.toPropertyDescriptor(call.Argument(2)) + + return r.toBoolean(target.defineOwnProperty(key, desc, false)) +} + +func (r *Runtime) builtin_reflect_deleteProperty(call FunctionCall) Value { + target := r.toObject(call.Argument(0)) + key := toPropertyKey(call.Argument(1)) + + return r.toBoolean(target.delete(key, false)) +} + +func (r *Runtime) builtin_reflect_get(call FunctionCall) Value { + target := r.toObject(call.Argument(0)) + key := toPropertyKey(call.Argument(1)) + var receiver Value + if len(call.Arguments) > 2 { + receiver = call.Arguments[2] + } + return target.get(key, receiver) +} + +func (r *Runtime) builtin_reflect_getOwnPropertyDescriptor(call FunctionCall) Value { + target := r.toObject(call.Argument(0)) + key := toPropertyKey(call.Argument(1)) + return r.valuePropToDescriptorObject(target.getOwnProp(key)) +} + +func (r *Runtime) builtin_reflect_getPrototypeOf(call FunctionCall) Value { + target := r.toObject(call.Argument(0)) + if proto := target.self.proto(); proto != nil { + return proto + } + + return _null +} + +func (r *Runtime) builtin_reflect_has(call FunctionCall) Value { + target := r.toObject(call.Argument(0)) + key := toPropertyKey(call.Argument(1)) + return r.toBoolean(target.hasProperty(key)) +} + +func (r *Runtime) builtin_reflect_isExtensible(call FunctionCall) Value { + target := r.toObject(call.Argument(0)) + return r.toBoolean(target.self.isExtensible()) +} + +func (r *Runtime) builtin_reflect_ownKeys(call FunctionCall) Value { + target := r.toObject(call.Argument(0)) + return r.newArrayValues(target.self.ownPropertyKeys(true, nil)) +} + +func (r *Runtime) builtin_reflect_preventExtensions(call FunctionCall) Value { + target := r.toObject(call.Argument(0)) + return r.toBoolean(target.self.preventExtensions(false)) +} + +func (r *Runtime) builtin_reflect_set(call FunctionCall) Value { + target := r.toObject(call.Argument(0)) + var receiver Value + if len(call.Arguments) >= 4 { + receiver = call.Argument(3) + } else { + receiver = target + } + return r.toBoolean(target.set(call.Argument(1), call.Argument(2), receiver, false)) +} + +func (r *Runtime) builtin_reflect_setPrototypeOf(call FunctionCall) Value { + target := r.toObject(call.Argument(0)) + var proto *Object + if arg := call.Argument(1); arg != _null { + proto = r.toObject(arg) + } + return r.toBoolean(target.self.setProto(proto, false)) +} + +func (r *Runtime) createReflect(val *Object) objectImpl { + o := newBaseObjectObj(val, r.global.ObjectPrototype, classObject) + + o._putProp("apply", r.newNativeFunc(r.builtin_reflect_apply, nil, "apply", nil, 3), true, false, true) + o._putProp("construct", r.newNativeFunc(r.builtin_reflect_construct, nil, "construct", nil, 2), true, false, true) + o._putProp("defineProperty", r.newNativeFunc(r.builtin_reflect_defineProperty, nil, "defineProperty", nil, 3), true, false, true) + o._putProp("deleteProperty", r.newNativeFunc(r.builtin_reflect_deleteProperty, nil, "deleteProperty", nil, 2), true, false, true) + o._putProp("get", r.newNativeFunc(r.builtin_reflect_get, nil, "get", nil, 2), true, false, true) + o._putProp("getOwnPropertyDescriptor", r.newNativeFunc(r.builtin_reflect_getOwnPropertyDescriptor, nil, "getOwnPropertyDescriptor", nil, 2), true, false, true) + o._putProp("getPrototypeOf", r.newNativeFunc(r.builtin_reflect_getPrototypeOf, nil, "getPrototypeOf", nil, 1), true, false, true) + o._putProp("has", r.newNativeFunc(r.builtin_reflect_has, nil, "has", nil, 2), true, false, true) + o._putProp("isExtensible", r.newNativeFunc(r.builtin_reflect_isExtensible, nil, "isExtensible", nil, 1), true, false, true) + o._putProp("ownKeys", r.newNativeFunc(r.builtin_reflect_ownKeys, nil, "ownKeys", nil, 1), true, false, true) + o._putProp("preventExtensions", r.newNativeFunc(r.builtin_reflect_preventExtensions, nil, "preventExtensions", nil, 1), true, false, true) + o._putProp("set", r.newNativeFunc(r.builtin_reflect_set, nil, "set", nil, 3), true, false, true) + o._putProp("setPrototypeOf", r.newNativeFunc(r.builtin_reflect_setPrototypeOf, nil, "setPrototypeOf", nil, 2), true, false, true) + + return o +} + +func (r *Runtime) initReflect() { + r.addToGlobal("Reflect", r.newLazyObject(r.createReflect)) +} diff --git a/builtin_regexp.go b/builtin_regexp.go index 42749635..76cbc732 100644 --- a/builtin_regexp.go +++ b/builtin_regexp.go @@ -119,16 +119,16 @@ func (r *Runtime) newRegExp(patternStr valueString, flags string, proto *Object) return r.newRegExpp(pattern, patternStr, global, ignoreCase, multiline, sticky, proto) } -func (r *Runtime) builtin_newRegExp(args []Value) *Object { +func (r *Runtime) builtin_newRegExp(args []Value, proto *Object) *Object { var pattern valueString var flags string if len(args) > 0 { if obj, ok := args[0].(*Object); ok { - if regexp, ok := obj.self.(*regexpObject); ok { + if rx, ok := obj.self.(*regexpObject); ok { if len(args) < 2 || args[1] == _undefined { - return regexp.clone() + return rx.clone() } else { - return r.newRegExp(regexp.source, args[1].String(), r.global.RegExpPrototype) + return r.newRegExp(rx.source, args[1].String(), proto) } } } @@ -144,7 +144,7 @@ func (r *Runtime) builtin_newRegExp(args []Value) *Object { if pattern == nil { pattern = stringEmpty } - return r.newRegExp(pattern, flags, r.global.RegExpPrototype) + return r.newRegExp(pattern, flags, proto) } func (r *Runtime) builtin_RegExp(call FunctionCall) Value { @@ -156,7 +156,7 @@ func (r *Runtime) builtin_RegExp(call FunctionCall) Value { } } } - return r.builtin_newRegExp(call.Arguments) + return r.builtin_newRegExp(call.Arguments, r.global.RegExpPrototype) } func (r *Runtime) regexpproto_exec(call FunctionCall) Value { @@ -271,16 +271,16 @@ func (r *Runtime) regexpproto_getFlags(call FunctionCall) Value { if this, ok := thisObj.self.(*regexpObject); ok { global, ignoreCase, multiline, sticky = this.global, this.ignoreCase, this.multiline, this.sticky } else { - if v := thisObj.self.getStr("global"); v != nil { + if v := thisObj.self.getStr("global", nil); v != nil { global = v.ToBoolean() } - if v := thisObj.self.getStr("ignoreCase"); v != nil { + if v := thisObj.self.getStr("ignoreCase", nil); v != nil { ignoreCase = v.ToBoolean() } - if v := thisObj.self.getStr("multiline"); v != nil { + if v := thisObj.self.getStr("multiline", nil); v != nil { multiline = v.ToBoolean() } - if v := thisObj.self.getStr("sticky"); v != nil { + if v := thisObj.self.getStr("sticky", nil); v != nil { sticky = v.ToBoolean() } } @@ -319,10 +319,10 @@ func (r *Runtime) regExpExec(execFn func(FunctionCall) Value, rxObj *Object, arg func (r *Runtime) regexpproto_stdMatcherGeneric(rxObj *Object, arg Value) Value { rx := rxObj.self - global := rx.getStr("global") + global := rx.getStr("global", nil) if global != nil && global.ToBoolean() { - rx.putStr("lastIndex", intToValue(0), true) - execFn, ok := r.toObject(rx.getStr("exec")).self.assertCallable() + rx.setOwnStr("lastIndex", intToValue(0), true) + execFn, ok := r.toObject(rx.getStr("exec", nil)).self.assertCallable() if !ok { panic(r.NewTypeError("exec is not a function")) } @@ -332,11 +332,11 @@ func (r *Runtime) regexpproto_stdMatcherGeneric(rxObj *Object, arg Value) Value if res == _null { break } - matchStr := nilSafe(r.toObject(res).self.get(intToValue(0))).toString() + matchStr := nilSafe(r.toObject(res).self.getIdx(valueInt(0), nil)).toString() a = append(a, matchStr) if matchStr.length() == 0 { - thisIndex := rx.getStr("lastIndex").ToInteger() - rx.putStr("lastIndex", intToValue(thisIndex+1), true) // TODO fullUnicode + thisIndex := rx.getStr("lastIndex", nil).ToInteger() + rx.setOwnStr("lastIndex", intToValue(thisIndex+1), true) // TODO fullUnicode } } if len(a) == 0 { @@ -345,7 +345,7 @@ func (r *Runtime) regexpproto_stdMatcherGeneric(rxObj *Object, arg Value) Value return r.newArrayValues(a) } - execFn, ok := r.toObject(rx.getStr("exec")).self.assertCallable() + execFn, ok := r.toObject(rx.getStr("exec", nil)).self.assertCallable() if !ok { panic(r.NewTypeError("exec is not a function")) } @@ -358,7 +358,7 @@ func (r *Runtime) checkStdRegexp(rxObj *Object) *regexpObject { if !ok { return nil } - execFn := rx.getPropStr("exec") + execFn := rx.getStr("exec", nil) if execFn != nil && execFn != r.global.regexpProtoExec { return nil } @@ -374,7 +374,7 @@ func (r *Runtime) regexpproto_stdMatcher(call FunctionCall) Value { return r.regexpproto_stdMatcherGeneric(thisObj, s) } if rx.global { - rx.putStr("lastIndex", intToValue(0), true) + rx.setOwnStr("lastIndex", intToValue(0), true) var a []Value var previousLastIndex int64 for { @@ -382,10 +382,10 @@ func (r *Runtime) regexpproto_stdMatcher(call FunctionCall) Value { if !match { break } - thisIndex := rx.getStr("lastIndex").ToInteger() + thisIndex := rx.getStr("lastIndex", nil).ToInteger() if thisIndex == previousLastIndex { previousLastIndex++ - rx.putStr("lastIndex", intToValue(previousLastIndex), true) + rx.setOwnStr("lastIndex", intToValue(previousLastIndex), true) } else { previousLastIndex = thisIndex } @@ -402,21 +402,21 @@ func (r *Runtime) regexpproto_stdMatcher(call FunctionCall) Value { func (r *Runtime) regexpproto_stdSearchGeneric(rxObj *Object, arg valueString) Value { rx := rxObj.self - previousLastIndex := rx.getStr("lastIndex") - rx.putStr("lastIndex", intToValue(0), true) - execFn, ok := r.toObject(rx.getStr("exec")).self.assertCallable() + previousLastIndex := rx.getStr("lastIndex", nil) + rx.setOwnStr("lastIndex", intToValue(0), true) + execFn, ok := r.toObject(rx.getStr("exec", nil)).self.assertCallable() if !ok { panic(r.NewTypeError("exec is not a function")) } result := r.regExpExec(execFn, rxObj, arg) - rx.putStr("lastIndex", previousLastIndex, true) + rx.setOwnStr("lastIndex", previousLastIndex, true) if result == _null { return intToValue(-1) } - return r.toObject(result).self.getStr("index") + return r.toObject(result).self.getStr("index", nil) } func (r *Runtime) regexpproto_stdSearch(call FunctionCall) Value { @@ -427,11 +427,11 @@ func (r *Runtime) regexpproto_stdSearch(call FunctionCall) Value { return r.regexpproto_stdSearchGeneric(thisObj, s) } - previousLastIndex := rx.getStr("lastIndex") - rx.putStr("lastIndex", intToValue(0), true) + previousLastIndex := rx.getStr("lastIndex", nil) + rx.setOwnStr("lastIndex", intToValue(0), true) match, result := rx.execRegexp(s) - rx.putStr("lastIndex", previousLastIndex, true) + rx.setOwnStr("lastIndex", previousLastIndex, true) if !match { return intToValue(-1) @@ -452,7 +452,7 @@ func (r *Runtime) regexpproto_stdSplitterGeneric(splitter *Object, s valueString if lim == 0 { return r.newArrayValues(a) } - execFn := toMethod(splitter.ToObject(r).self.getStr("exec")) // must be non-nil + execFn := toMethod(splitter.ToObject(r).self.getStr("exec", nil)) // must be non-nil if size == 0 { if r.regExpExec(execFn, splitter, s) == _null { @@ -463,13 +463,13 @@ func (r *Runtime) regexpproto_stdSplitterGeneric(splitter *Object, s valueString q := p for q < size { - splitter.self.putStr("lastIndex", intToValue(q), true) + splitter.self.setOwnStr("lastIndex", intToValue(q), true) z := r.regExpExec(execFn, splitter, s) if z == _null { q++ } else { z := r.toObject(z) - e := toLength(splitter.self.getStr("lastIndex")) + e := toLength(splitter.self.getStr("lastIndex", nil)) if e == p { q++ } else { @@ -478,9 +478,9 @@ func (r *Runtime) regexpproto_stdSplitterGeneric(splitter *Object, s valueString return r.newArrayValues(a) } p = e - numberOfCaptures := max(toLength(z.self.getStr("length"))-1, 0) + numberOfCaptures := max(toLength(z.self.getStr("length", nil))-1, 0) for i := int64(1); i <= numberOfCaptures; i++ { - a = append(a, z.self.get(intToValue(i))) + a = append(a, z.self.getIdx(valueInt(i), nil)) if int64(len(a)) == lim { return r.newArrayValues(a) } @@ -496,13 +496,13 @@ func (r *Runtime) regexpproto_stdSplitterGeneric(splitter *Object, s valueString func (r *Runtime) regexpproto_stdSplitter(call FunctionCall) Value { rxObj := r.toObject(call.This) c := r.speciesConstructor(rxObj, r.global.RegExp) - flags := nilSafe(rxObj.self.getStr("flags")).toString() + flags := nilSafe(rxObj.self.getStr("flags", nil)).toString() // Add 'y' flag if missing if flagsStr := flags.String(); !strings.Contains(flagsStr, "y") { flags = newStringValue(flagsStr + "y") } - splitter := c([]Value{rxObj, flags}) + splitter := c([]Value{rxObj, flags}, nil) s := call.Argument(0).toString() limitValue := call.Argument(1) @@ -582,50 +582,50 @@ func (r *Runtime) initRegExp() { r.global.RegExpPrototype = r.NewObject() o := r.global.RegExpPrototype.self r.global.regexpProtoExec = valueProp(r.newNativeFunc(r.regexpproto_exec, nil, "exec", nil, 1), true, false, true) - o.putStr("exec", r.global.regexpProtoExec, true) + o.setOwnStr("exec", r.global.regexpProtoExec, true) o._putProp("test", r.newNativeFunc(r.regexpproto_test, nil, "test", nil, 1), true, false, true) o._putProp("toString", r.newNativeFunc(r.regexpproto_toString, nil, "toString", nil, 0), true, false, true) - o.putStr("source", &valueProperty{ + o.setOwnStr("source", &valueProperty{ configurable: true, getterFunc: r.newNativeFunc(r.regexpproto_getSource, nil, "get source", nil, 0), accessor: true, }, false) - o.putStr("global", &valueProperty{ + o.setOwnStr("global", &valueProperty{ configurable: true, getterFunc: r.newNativeFunc(r.regexpproto_getGlobal, nil, "get global", nil, 0), accessor: true, }, false) - o.putStr("multiline", &valueProperty{ + o.setOwnStr("multiline", &valueProperty{ configurable: true, getterFunc: r.newNativeFunc(r.regexpproto_getMultiline, nil, "get multiline", nil, 0), accessor: true, }, false) - o.putStr("ignoreCase", &valueProperty{ + o.setOwnStr("ignoreCase", &valueProperty{ configurable: true, getterFunc: r.newNativeFunc(r.regexpproto_getIgnoreCase, nil, "get ignoreCase", nil, 0), accessor: true, }, false) - o.putStr("sticky", &valueProperty{ + o.setOwnStr("sticky", &valueProperty{ configurable: true, getterFunc: r.newNativeFunc(r.regexpproto_getSticky, nil, "get sticky", nil, 0), accessor: true, }, false) - o.putStr("flags", &valueProperty{ + o.setOwnStr("flags", &valueProperty{ configurable: true, getterFunc: r.newNativeFunc(r.regexpproto_getFlags, nil, "get flags", nil, 0), accessor: true, }, false) - o.put(symMatch, valueProp(r.newNativeFunc(r.regexpproto_stdMatcher, nil, "[Symbol.match]", nil, 1), true, false, true), true) - o.put(symSearch, valueProp(r.newNativeFunc(r.regexpproto_stdSearch, nil, "[Symbol.search]", nil, 1), true, false, true), true) - o.put(symSplit, valueProp(r.newNativeFunc(r.regexpproto_stdSplitter, nil, "[Symbol.split]", nil, 2), true, false, true), true) + o._putSym(symMatch, valueProp(r.newNativeFunc(r.regexpproto_stdMatcher, nil, "[Symbol.match]", nil, 1), true, false, true)) + o._putSym(symSearch, valueProp(r.newNativeFunc(r.regexpproto_stdSearch, nil, "[Symbol.search]", nil, 1), true, false, true)) + o._putSym(symSplit, valueProp(r.newNativeFunc(r.regexpproto_stdSplitter, nil, "[Symbol.split]", nil, 2), true, false, true)) r.global.RegExp = r.newNativeFunc(r.builtin_RegExp, r.builtin_newRegExp, "RegExp", r.global.RegExpPrototype, 2) o = r.global.RegExp.self - o.put(symSpecies, &valueProperty{ + o._putSym(symSpecies, &valueProperty{ getterFunc: r.newNativeFunc(r.returnThis, nil, "get [Symbol.species]", nil, 0), accessor: true, configurable: true, - }, true) + }) r.addToGlobal("RegExp", r.global.RegExp) } diff --git a/builtin_set.go b/builtin_set.go index e4ffbbf3..bbd70770 100644 --- a/builtin_set.go +++ b/builtin_set.go @@ -121,7 +121,7 @@ func (r *Runtime) setProto_values(call FunctionCall) Value { return r.createSetIterator(call.This, iterationKindValue) } -func (r *Runtime) builtin_newSet(args []Value) *Object { +func (r *Runtime) builtin_newSet(args []Value, proto *Object) *Object { o := &Object{runtime: r} so := &setObject{} @@ -129,11 +129,11 @@ func (r *Runtime) builtin_newSet(args []Value) *Object { so.val = o so.extensible = true o.self = so - so.prototype = r.global.SetPrototype + so.prototype = proto so.init() if len(args) > 0 { if arg := args[0]; arg != nil && arg != _undefined && arg != _null { - adder := so.getStr("add") + adder := so.getStr("add", nil) iter := r.getIterator(arg, nil) if adder == r.global.setAdder { r.iterate(iter, func(item Value) { @@ -195,7 +195,7 @@ func (r *Runtime) createSetProto(val *Object) objectImpl { o._putProp("delete", r.newNativeFunc(r.setProto_delete, nil, "delete", nil, 1), true, false, true) o._putProp("forEach", r.newNativeFunc(r.setProto_forEach, nil, "forEach", nil, 1), true, false, true) o._putProp("has", r.newNativeFunc(r.setProto_has, nil, "has", nil, 1), true, false, true) - o.putStr("size", &valueProperty{ + o.setOwnStr("size", &valueProperty{ getterFunc: r.newNativeFunc(r.setProto_getSize, nil, "get size", nil, 0), accessor: true, writable: true, @@ -206,19 +206,19 @@ func (r *Runtime) createSetProto(val *Object) objectImpl { o._putProp("values", valuesFunc, true, false, true) o._putProp("keys", valuesFunc, true, false, true) o._putProp("entries", r.newNativeFunc(r.setProto_entries, nil, "entries", nil, 0), true, false, true) - o.put(symIterator, valueProp(valuesFunc, true, false, true), true) - o.put(symToStringTag, valueProp(asciiString(classSet), false, false, true), true) + o._putSym(symIterator, valueProp(valuesFunc, true, false, true)) + o._putSym(symToStringTag, valueProp(asciiString(classSet), false, false, true)) return o } func (r *Runtime) createSet(val *Object) objectImpl { o := r.newNativeFuncObj(val, r.constructorThrower("Set"), r.builtin_newSet, "Set", r.global.SetPrototype, 0) - o.putSym(symSpecies, &valueProperty{ + o._putSym(symSpecies, &valueProperty{ getterFunc: r.newNativeFunc(r.returnThis, nil, "get [Symbol.species]", nil, 0), accessor: true, configurable: true, - }, true) + }) return o } @@ -227,7 +227,7 @@ func (r *Runtime) createSetIterProto(val *Object) objectImpl { o := newBaseObjectObj(val, r.global.IteratorPrototype, classObject) o._putProp("next", r.newNativeFunc(r.setIterProto_next, nil, "next", nil, 0), true, false, true) - o.put(symToStringTag, valueProp(asciiString(classSetIterator), false, false, true), true) + o._putSym(symToStringTag, valueProp(asciiString(classSetIterator), false, false, true)) return o } diff --git a/builtin_string.go b/builtin_string.go index 305470f8..4a6113ff 100644 --- a/builtin_string.go +++ b/builtin_string.go @@ -21,7 +21,7 @@ func (r *Runtime) collator() *collate.Collator { } func toString(arg Value) valueString { - if s, ok := arg.assertString(); ok { + if s, ok := arg.(valueString); ok { return s } if s, ok := arg.(*valueSymbol); ok { @@ -38,7 +38,7 @@ func (r *Runtime) builtin_String(call FunctionCall) Value { } } -func (r *Runtime) _newString(s valueString) *Object { +func (r *Runtime) _newString(s valueString, proto *Object) *Object { v := &Object{runtime: r} o := &stringObject{} @@ -46,7 +46,7 @@ func (r *Runtime) _newString(s valueString) *Object { o.val = v o.extensible = true v.self = o - o.prototype = r.global.StringPrototype + o.prototype = proto if s != nil { o.value = s } @@ -54,14 +54,14 @@ func (r *Runtime) _newString(s valueString) *Object { return v } -func (r *Runtime) builtin_newString(args []Value) *Object { +func (r *Runtime) builtin_newString(args []Value, proto *Object) *Object { var s valueString if len(args) > 0 { s = toString(args[0]) } else { s = stringEmpty } - return r._newString(s) + return r._newString(s, proto) } func searchSubstringUTF8(str, search string) (ret [][]int) { @@ -79,7 +79,7 @@ func searchSubstringUTF8(str, search string) (ret [][]int) { } func (r *Runtime) stringproto_toStringValueOf(this Value, funcName string) Value { - if str, ok := this.assertString(); ok { + if str, ok := this.(valueString); ok { return str } if obj, ok := this.(*Object); ok { @@ -206,7 +206,7 @@ func (r *Runtime) stringproto_lastIndexOf(call FunctionCall) Value { numPos := call.Argument(1).ToNumber() var pos int64 - if f, ok := numPos.assertFloat(); ok && math.IsNaN(f) { + if f, ok := numPos.(valueFloat); ok && math.IsNaN(float64(f)) { pos = value.length() } else { pos = numPos.ToInteger() @@ -248,10 +248,10 @@ func (r *Runtime) stringproto_match(call FunctionCall) Value { } if rx == nil { - rx = r.builtin_newRegExp([]Value{regexp}).self.(*regexpObject) + rx = r.builtin_newRegExp([]Value{regexp}, r.global.RegExpPrototype).self.(*regexpObject) } - if matcher, ok := r.toObject(rx.getSym(symMatch)).self.assertCallable(); ok { + if matcher, ok := r.toObject(rx.getSym(symMatch, nil)).self.assertCallable(); ok { return matcher(FunctionCall{ This: rx.val, Arguments: []Value{call.This.toString()}, @@ -431,10 +431,10 @@ func (r *Runtime) stringproto_search(call FunctionCall) Value { } if rx == nil { - rx = r.builtin_newRegExp([]Value{regexp}).self.(*regexpObject) + rx = r.builtin_newRegExp([]Value{regexp}, r.global.RegExpPrototype).self.(*regexpObject) } - if searcher, ok := r.toObject(rx.getSym(symSearch)).self.assertCallable(); ok { + if searcher, ok := r.toObject(rx.getSym(symSearch, nil)).self.assertCallable(); ok { return searcher(FunctionCall{ This: rx.val, Arguments: []Value{call.This.toString()}, @@ -615,10 +615,9 @@ func (r *Runtime) stringproto_substr(call FunctionCall) Value { } func (r *Runtime) initString() { - r.global.StringPrototype = r.builtin_newString([]Value{stringEmpty}) + r.global.StringPrototype = r.builtin_newString([]Value{stringEmpty}, r.global.ObjectPrototype) o := r.global.StringPrototype.self - o.(*stringObject).prototype = r.global.ObjectPrototype o._putProp("toString", r.newNativeFunc(r.stringproto_toString, nil, "toString", nil, 0), true, false, true) o._putProp("valueOf", r.newNativeFunc(r.stringproto_valueOf, nil, "valueOf", nil, 0), true, false, true) o._putProp("charAt", r.newNativeFunc(r.stringproto_charAt, nil, "charAt", nil, 1), true, false, true) diff --git a/builtin_symbol.go b/builtin_symbol.go index 599078d1..83346e14 100644 --- a/builtin_symbol.go +++ b/builtin_symbol.go @@ -99,8 +99,8 @@ func (r *Runtime) createSymbolProto(val *Object) objectImpl { o._putProp("constructor", r.global.Symbol, true, false, true) o._putProp("toString", r.newNativeFunc(r.symbolproto_tostring, nil, "toString", nil, 0), true, false, true) o._putProp("valueOf", r.newNativeFunc(r.symbolproto_valueOf, nil, "valueOf", nil, 0), true, false, true) - o.putSym(symToPrimitive, valueProp(r.newNativeFunc(r.symbolproto_valueOf, nil, "[Symbol.toPrimitive]", nil, 1), false, false, true), true) - o.putSym(symToStringTag, valueProp(newStringValue("Symbol"), false, false, true), true) + o._putSym(symToPrimitive, valueProp(r.newNativeFunc(r.symbolproto_valueOf, nil, "[Symbol.toPrimitive]", nil, 1), false, false, true)) + o._putSym(symToStringTag, valueProp(newStringValue("Symbol"), false, false, true)) return o } diff --git a/builtin_weakmap.go b/builtin_weakmap.go index be0f36e9..a9debccf 100644 --- a/builtin_weakmap.go +++ b/builtin_weakmap.go @@ -133,7 +133,7 @@ func (r *Runtime) weakMapProto_set(call FunctionCall) Value { return call.This } -func (r *Runtime) builtin_newWeakMap(args []Value) *Object { +func (r *Runtime) builtin_newWeakMap(args []Value, proto *Object) *Object { o := &Object{runtime: r} wmo := &weakMapObject{} @@ -141,19 +141,19 @@ func (r *Runtime) builtin_newWeakMap(args []Value) *Object { wmo.val = o wmo.extensible = true o.self = wmo - wmo.prototype = r.global.WeakMapPrototype + wmo.prototype = proto wmo.init() if len(args) > 0 { if arg := args[0]; arg != nil && arg != _undefined && arg != _null { - adder := wmo.getStr("set") + adder := wmo.getStr("set", nil) iter := r.getIterator(arg, nil) - i0 := intToValue(0) - i1 := intToValue(1) + i0 := valueInt(0) + i1 := valueInt(1) if adder == r.global.weakMapAdder { r.iterate(iter, func(item Value) { itemObj := r.toObject(item) - k := itemObj.self.get(i0) - v := nilSafe(itemObj.self.get(i1)) + k := itemObj.self.getIdx(i0, nil) + v := nilSafe(itemObj.self.getIdx(i1, nil)) wmo.m.set(r.toObject(k), v) }) } else { @@ -163,8 +163,8 @@ func (r *Runtime) builtin_newWeakMap(args []Value) *Object { } r.iterate(iter, func(item Value) { itemObj := r.toObject(item) - k := itemObj.self.get(i0) - v := itemObj.self.get(i1) + k := itemObj.self.getIdx(i0, nil) + v := itemObj.self.getIdx(i1, nil) adderFn(FunctionCall{This: o, Arguments: []Value{k, v}}) }) } @@ -183,7 +183,7 @@ func (r *Runtime) createWeakMapProto(val *Object) objectImpl { o._putProp("has", r.newNativeFunc(r.weakMapProto_has, nil, "has", nil, 1), true, false, true) o._putProp("get", r.newNativeFunc(r.weakMapProto_get, nil, "get", nil, 1), true, false, true) - o.put(symToStringTag, valueProp(asciiString(classWeakMap), false, false, true), true) + o._putSym(symToStringTag, valueProp(asciiString(classWeakMap), false, false, true)) return o } diff --git a/builtin_weakset.go b/builtin_weakset.go index d73697c9..e3aa4243 100644 --- a/builtin_weakset.go +++ b/builtin_weakset.go @@ -11,7 +11,7 @@ type weakSet struct { type weakSetObject struct { baseObject - set *weakSet + s *weakSet } func newWeakSet() *weakSet { @@ -22,7 +22,7 @@ func newWeakSet() *weakSet { func (ws *weakSetObject) init() { ws.baseObject.init() - ws.set = newWeakSet() + ws.s = newWeakSet() } func (ws *weakSet) removePtr(ptr uintptr) { @@ -72,7 +72,7 @@ func (r *Runtime) weakSetProto_add(call FunctionCall) Value { if !ok { panic(r.NewTypeError("Method WeakSet.prototype.add called on incompatible receiver %s", thisObj.String())) } - wso.set.add(r.toObject(call.Argument(0))) + wso.s.add(r.toObject(call.Argument(0))) return call.This } @@ -83,7 +83,7 @@ func (r *Runtime) weakSetProto_delete(call FunctionCall) Value { panic(r.NewTypeError("Method WeakSet.prototype.delete called on incompatible receiver %s", thisObj.String())) } obj, ok := call.Argument(0).(*Object) - if ok && wso.set.remove(obj) { + if ok && wso.s.remove(obj) { return valueTrue } return valueFalse @@ -96,7 +96,7 @@ func (r *Runtime) weakSetProto_has(call FunctionCall) Value { panic(r.NewTypeError("Method WeakSet.prototype.has called on incompatible receiver %s", thisObj.String())) } obj, ok := call.Argument(0).(*Object) - if ok && wso.set.has(obj) { + if ok && wso.s.has(obj) { return valueTrue } return valueFalse @@ -113,7 +113,7 @@ func (r *Runtime) populateWeakSetGeneric(s *Object, adderValue Value, iterable V }) } -func (r *Runtime) builtin_newWeakSet(args []Value) *Object { +func (r *Runtime) builtin_newWeakSet(args []Value, proto *Object) *Object { o := &Object{runtime: r} wso := &weakSetObject{} @@ -121,15 +121,15 @@ func (r *Runtime) builtin_newWeakSet(args []Value) *Object { wso.val = o wso.extensible = true o.self = wso - wso.prototype = r.global.WeakSetPrototype + wso.prototype = proto wso.init() if len(args) > 0 { if arg := args[0]; arg != nil && arg != _undefined && arg != _null { - adder := wso.getStr("add") + adder := wso.getStr("add", nil) if adder == r.global.weakSetAdder { if arr := r.checkStdArrayIter(arg); arr != nil { for _, v := range arr.values { - wso.set.add(r.toObject(v)) + wso.s.add(r.toObject(v)) } return o } @@ -149,7 +149,7 @@ func (r *Runtime) createWeakSetProto(val *Object) objectImpl { o._putProp("delete", r.newNativeFunc(r.weakSetProto_delete, nil, "delete", nil, 1), true, false, true) o._putProp("has", r.newNativeFunc(r.weakSetProto_has, nil, "has", nil, 1), true, false, true) - o.put(symToStringTag, valueProp(asciiString(classWeakSet), false, false, true), true) + o._putSym(symToStringTag, valueProp(asciiString(classWeakSet), false, false, true)) return o } diff --git a/builtin_weakset_test.go b/builtin_weakset_test.go index 0194a981..3b01fa3f 100644 --- a/builtin_weakset_test.go +++ b/builtin_weakset_test.go @@ -37,9 +37,9 @@ func TestWeakSetExpiry(t *testing.T) { } runtime.GC() wso := vm.Get("s").ToObject(vm).self.(*weakSetObject) - wso.set.Lock() - l := len(wso.set.data) - wso.set.Unlock() + wso.s.Lock() + l := len(wso.s.data) + wso.s.Unlock() if l > 0 { t.Fatal("Object has not been removed") } diff --git a/compiler_expr.go b/compiler_expr.go index 6bc6da54..581208e8 100644 --- a/compiler_expr.go +++ b/compiler_expr.go @@ -115,6 +115,10 @@ type compiledNewExpr struct { args []compiledExpr } +type compiledNewTarget struct { + baseCompiledExpr +} + type compiledSequenceExpr struct { baseCompiledExpr sequence []compiledExpr @@ -232,6 +236,8 @@ func (c *compiler) compileExpression(v ast.Expression) compiledExpr { return c.compileSequenceExpression(v) case *ast.NewExpression: return c.compileNewExpression(v) + case *ast.MetaProperty: + return c.compileMetaProperty(v) default: panic(fmt.Errorf("Unknown expression type: %T", v)) } @@ -246,7 +252,7 @@ func (e *baseCompiledExpr) init(c *compiler, idx file.Idx) { e.offset = int(idx) - 1 } -func (e *baseCompiledExpr) emitSetter(valueExpr compiledExpr) { +func (e *baseCompiledExpr) emitSetter(compiledExpr) { e.c.throwSyntaxError(e.offset, "Not a valid left-value expression") } @@ -258,7 +264,7 @@ func (e *baseCompiledExpr) deleteExpr() compiledExpr { return r } -func (e *baseCompiledExpr) emitUnary(prepare, body func(), postfix bool, putOnStack bool) { +func (e *baseCompiledExpr) emitUnary(func(), func(), bool, bool) { e.c.throwSyntaxError(e.offset, "Not a valid left-value expression") } @@ -923,6 +929,23 @@ func (c *compiler) compileNewExpression(v *ast.NewExpression) compiledExpr { return r } +func (e *compiledNewTarget) emitGetter(putOnStack bool) { + if putOnStack { + e.addSrcMap() + e.c.emit(loadNewTarget) + } +} + +func (c *compiler) compileMetaProperty(v *ast.MetaProperty) compiledExpr { + if v.Meta.Name == "new" || v.Property.Name != "target" { + r := &compiledNewTarget{} + r.init(c, v.Idx0()) + return r + } + c.throwSyntaxError(int(v.Idx)-1, "Unsupported meta property: %s.%s", v.Meta.Name, v.Property.Name) + return nil +} + func (e *compiledSequenceExpr) emitGetter(putOnStack bool) { if len(e.sequence) > 0 { for i := 0; i < len(e.sequence)-1; i++ { @@ -950,11 +973,11 @@ func (c *compiler) compileSequenceExpression(v *ast.SequenceExpression) compiled func (c *compiler) emitThrow(v Value) { if o, ok := v.(*Object); ok { - t := o.self.getStr("name").String() + t := o.self.getStr("name", nil).String() switch t { case "TypeError": c.emit(getVar1(t)) - msg := o.self.getStr("message") + msg := o.self.getStr("message", nil) if msg != nil { c.emit(loadVal(c.p.defineLiteralValue(msg))) c.emit(_new(1)) @@ -1365,7 +1388,7 @@ func (c *compiler) compileObjectLiteral(v *ast.ObjectLiteral) compiledExpr { func (e *compiledArrayLiteral) emitGetter(putOnStack bool) { e.addSrcMap() - objCount := uint32(0) + objCount := 0 for _, v := range e.expr.Value { if v != nil { e.c.compileExpression(v).emitGetter(true) @@ -1374,11 +1397,11 @@ func (e *compiledArrayLiteral) emitGetter(putOnStack bool) { e.c.emit(loadNil) } } - if objCount == uint32(len(e.expr.Value)) { + if objCount == len(e.expr.Value) { e.c.emit(newArray(objCount)) } else { e.c.emit(&newArraySparse{ - l: uint32(len(e.expr.Value)), + l: len(e.expr.Value), objCount: objCount, }) } diff --git a/compiler_test.go b/compiler_test.go index 8920b809..341681c7 100644 --- a/compiler_test.go +++ b/compiler_test.go @@ -27,7 +27,7 @@ func testScript(script string, expectedResult Value, t *testing.T) { t.Logf("stack size: %d", len(vm.stack)) t.Logf("stashAllocs: %d", vm.stashAllocs) - v := vm.r.globalObject.self.getStr("rv") + v := vm.r.globalObject.self.getStr("rv", nil) if v == nil { v = _undefined } diff --git a/date.go b/date.go index 67aaf029..a22deb1a 100644 --- a/date.go +++ b/date.go @@ -68,13 +68,13 @@ func dateParse(date string) (time.Time, bool) { return t, err == nil && unix >= -8640000000000000 && unix <= 8640000000000000 } -func (r *Runtime) newDateObject(t time.Time, isSet bool) *Object { +func (r *Runtime) newDateObject(t time.Time, isSet bool, proto *Object) *Object { v := &Object{runtime: r} d := &dateObject{} v.self = d d.val = v d.class = classDate - d.prototype = r.global.DatePrototype + d.prototype = proto d.extensible = true d.init() d.time = t.In(time.Local) diff --git a/func.go b/func.go index 6b8e4cd9..138afc65 100644 --- a/func.go +++ b/func.go @@ -20,7 +20,7 @@ type nativeFuncObject struct { baseFuncObject f func(FunctionCall) Value - construct func(args []Value) *Object + construct func(args []Value, newTarget *Object) *Object } type boundFuncObject struct { @@ -45,25 +45,25 @@ func (f *funcObject) _addProto(n string) Value { return nil } -func (f *funcObject) getPropStr(name string) Value { +func (f *funcObject) getStr(p string, receiver Value) Value { + return f.getStrWithOwnProp(f.getOwnPropStr(p), p, receiver) +} + +func (f *funcObject) getOwnPropStr(name string) Value { if v := f._addProto(name); v != nil { return v } - return f.baseObject.getPropStr(name) + return f.baseObject.getOwnPropStr(name) } -func (f *funcObject) putStr(name string, val Value, throw bool) { +func (f *funcObject) setOwnStr(name string, val Value, throw bool) bool { f._addProto(name) - f.baseObject.putStr(name, val, throw) + return f.baseObject.setOwnStr(name, val, throw) } -func (f *funcObject) put(n Value, val Value, throw bool) { - if s, ok := n.(*valueSymbol); ok { - f.putSym(s, val, throw) - } else { - f.putStr(n.String(), val, throw) - } +func (f *funcObject) setForeignStr(name string, val, receiver Value, throw bool) (bool, bool) { + return f._setForeignStr(name, f.getOwnPropStr(name), val, receiver, throw) } func (f *funcObject) deleteStr(name string, throw bool) bool { @@ -71,55 +71,49 @@ func (f *funcObject) deleteStr(name string, throw bool) bool { return f.baseObject.deleteStr(name, throw) } -func (f *funcObject) delete(n Value, throw bool) bool { - if s, ok := n.(*valueSymbol); ok { - return f.deleteSym(s, throw) - } - return f.deleteStr(n.String(), throw) -} - func (f *funcObject) addPrototype() Value { proto := f.val.runtime.NewObject() proto.self._putProp("constructor", f.val, true, false, true) return f._putProp("prototype", proto, true, false, false) } -func (f *funcObject) hasOwnProperty(n Value) bool { - if r := f.baseObject.hasOwnProperty(n); r { +func (f *funcObject) hasOwnPropertyStr(name string) bool { + if r := f.baseObject.hasOwnPropertyStr(name); r { return true } - name := n.String() if name == "prototype" { return true } return false } -func (f *funcObject) hasOwnPropertyStr(name string) bool { - if r := f.baseObject.hasOwnPropertyStr(name); r { - return true - } - - if name == "prototype" { - return true +func (f *funcObject) ownKeys(all bool, accum []Value) []Value { + if all { + if _, exists := f.values["prototype"]; !exists { + accum = append(accum, asciiString("prototype")) + } } - return false + return f.baseFuncObject.ownKeys(all, accum) } -func (f *funcObject) construct(args []Value) *Object { - proto := f.getStr("prototype") +func (f *funcObject) construct(args []Value, newTarget *Object) *Object { + if newTarget == nil { + newTarget = f.val + } + proto := newTarget.self.getStr("prototype", nil) var protoObj *Object if p, ok := proto.(*Object); ok { protoObj = p } else { protoObj = f.val.runtime.global.ObjectPrototype } + obj := f.val.runtime.newBaseObject(protoObj, classObject).val - ret := f.Call(FunctionCall{ + ret := f.call(FunctionCall{ This: obj, Arguments: args, - }) + }, newTarget) if ret, ok := ret.(*Object); ok { return ret @@ -128,6 +122,10 @@ func (f *funcObject) construct(args []Value) *Object { } func (f *funcObject) Call(call FunctionCall) Value { + return f.call(call, nil) +} + +func (f *funcObject) call(call FunctionCall, newTarget Value) Value { vm := f.val.runtime.vm pc := vm.pc @@ -154,6 +152,7 @@ func (f *funcObject) Call(call FunctionCall) Value { vm.args = len(call.Arguments) vm.prg = f.prg vm.stash = f.stash + vm.newTarget = newTarget vm.pc = 0 vm.run() vm.pc = pc @@ -173,12 +172,18 @@ func (f *funcObject) assertCallable() (func(FunctionCall) Value, bool) { return f.Call, true } +func (f *funcObject) assertConstructor() func(args []Value, newTarget *Object) *Object { + return f.construct +} + func (f *baseFuncObject) init(name string, length int) { f.baseObject.init() - f.nameProp.configurable = true - f.nameProp.value = newStringValue(name) - f._put("name", &f.nameProp) + if name != "" { + f.nameProp.configurable = true + f.nameProp.value = newStringValue(name) + f._put("name", &f.nameProp) + } f.lenProp.configurable = true f.lenProp.value = valueInt(length) @@ -187,7 +192,7 @@ func (f *baseFuncObject) init(name string, length int) { func (f *baseFuncObject) hasInstance(v Value) bool { if v, ok := v.(*Object); ok { - o := f.val.self.getStr("prototype") + o := f.val.self.getStr("prototype", nil) if o1, ok := o.(*Object); ok { for { v = v.self.proto() @@ -207,7 +212,7 @@ func (f *baseFuncObject) hasInstance(v Value) bool { } func (f *nativeFuncObject) defaultConstruct(ccall func(ConstructorCall) *Object, args []Value) *Object { - proto := f.getStr("prototype") + proto := f.getStr("prototype", nil) var protoObj *Object if p, ok := proto.(*Object); ok { protoObj = p @@ -233,19 +238,20 @@ func (f *nativeFuncObject) assertCallable() (func(FunctionCall) Value, bool) { return nil, false } -func (f *boundFuncObject) getPropStr(name string) Value { +func (f *nativeFuncObject) assertConstructor() func(args []Value, newTarget *Object) *Object { + return f.construct +} + +func (f *boundFuncObject) getStr(p string, receiver Value) Value { + return f.getStrWithOwnProp(f.getOwnPropStr(p), p, receiver) +} + +func (f *boundFuncObject) getOwnPropStr(name string) Value { if name == "caller" || name == "arguments" { - //f.runtime.typeErrorResult(true, "'caller' and 'arguments' are restricted function properties and cannot be accessed in this context.") return f.val.runtime.global.throwerProperty } - return f.nativeFuncObject.getPropStr(name) -} -func (f *boundFuncObject) delete(n Value, throw bool) bool { - if s, ok := n.(*valueSymbol); ok { - return f.deleteSym(s, throw) - } - return f.deleteStr(n.String(), throw) + return f.nativeFuncObject.getOwnPropStr(name) } func (f *boundFuncObject) deleteStr(name string, throw bool) bool { @@ -255,19 +261,15 @@ func (f *boundFuncObject) deleteStr(name string, throw bool) bool { return f.nativeFuncObject.deleteStr(name, throw) } -func (f *boundFuncObject) putStr(name string, val Value, throw bool) { +func (f *boundFuncObject) setOwnStr(name string, val Value, throw bool) bool { if name == "caller" || name == "arguments" { - f.val.runtime.typeErrorResult(true, "'caller' and 'arguments' are restricted function properties and cannot be accessed in this context.") + panic(f.val.runtime.NewTypeError("'caller' and 'arguments' are restricted function properties and cannot be accessed in this context.")) } - f.nativeFuncObject.putStr(name, val, throw) + return f.nativeFuncObject.setOwnStr(name, val, throw) } -func (f *boundFuncObject) put(n Value, val Value, throw bool) { - if s, ok := n.(*valueSymbol); ok { - f.putSym(s, val, throw) - return - } - f.putStr(n.String(), val, throw) +func (f *boundFuncObject) setForeignStr(name string, val, receiver Value, throw bool) (bool, bool) { + return f._setForeignStr(name, f.getOwnPropStr(name), val, receiver, throw) } func (f *boundFuncObject) hasInstance(v Value) bool { diff --git a/object.go b/object.go index 73de00f0..620ddec8 100644 --- a/object.go +++ b/object.go @@ -2,8 +2,10 @@ package goja import ( "fmt" + "math" "reflect" "runtime" + "sort" "unsafe" ) @@ -93,7 +95,9 @@ type Object struct { type iterNextFunc func() (propIterItem, iterNextFunc) -type propertyDescr struct { +type PropertyDescriptor struct { + jsDescriptor *Object + Value Value Writable, Configurable, Enumerable Flag @@ -101,40 +105,125 @@ type propertyDescr struct { Getter, Setter Value } +func (p *PropertyDescriptor) Empty() bool { + var empty PropertyDescriptor + return *p == empty +} + +func (p *PropertyDescriptor) toValue(r *Runtime) Value { + if p.jsDescriptor != nil { + return p.jsDescriptor + } + + o := r.NewObject() + s := o.self + + if p.Value != nil { + s._putProp("value", p.Value, true, true, true) + } + + if p.Writable != FLAG_NOT_SET { + s._putProp("writable", valueBool(p.Writable.Bool()), true, true, true) + } + + if p.Enumerable != FLAG_NOT_SET { + s._putProp("enumerable", valueBool(p.Enumerable.Bool()), true, true, true) + } + + if p.Configurable != FLAG_NOT_SET { + s._putProp("configurable", valueBool(p.Configurable.Bool()), true, true, true) + } + + if p.Getter != nil { + s._putProp("get", p.Getter, true, true, true) + } + if p.Setter != nil { + s._putProp("set", p.Setter, true, true, true) + } + + return o +} + +func (p *PropertyDescriptor) complete() { + if p.Getter == nil && p.Setter == nil { + if p.Value == nil { + p.Value = _undefined + } + if p.Writable == FLAG_NOT_SET { + p.Writable = FLAG_FALSE + } + } else { + if p.Getter == nil { + p.Getter = _undefined + } + if p.Setter == nil { + p.Setter = _undefined + } + } + if p.Enumerable == FLAG_NOT_SET { + p.Enumerable = FLAG_FALSE + } + if p.Configurable == FLAG_NOT_SET { + p.Configurable = FLAG_FALSE + } +} + type objectImpl interface { sortable className() string - get(Value) Value - getProp(Value) Value - getPropStr(string) Value - getStr(string) Value - getOwnProp(Value) Value + getStr(p string, receiver Value) Value + getIdx(p valueInt, receiver Value) Value + getSym(p *valueSymbol, receiver Value) Value + getOwnPropStr(string) Value - put(Value, Value, bool) - putStr(string, Value, bool) - hasProperty(Value) bool + getOwnPropIdx(valueInt) Value + getOwnPropSym(*valueSymbol) Value + + setOwnStr(p string, v Value, throw bool) bool + setOwnIdx(p valueInt, v Value, throw bool) bool + setOwnSym(p *valueSymbol, v Value, throw bool) bool + + setForeignStr(p string, v, receiver Value, throw bool) (res bool, handled bool) + setForeignIdx(p valueInt, v, receiver Value, throw bool) (res bool, handled bool) + setForeignSym(p *valueSymbol, v, receiver Value, throw bool) (res bool, handled bool) + hasPropertyStr(string) bool - hasOwnProperty(Value) bool + hasPropertyIdx(idx valueInt) bool + hasPropertySym(s *valueSymbol) bool + hasOwnPropertyStr(string) bool - _putProp(name string, value Value, writable, enumerable, configurable bool) Value - defineOwnProperty(name Value, descr propertyDescr, throw bool) bool + hasOwnPropertyIdx(valueInt) bool + hasOwnPropertySym(s *valueSymbol) bool + + defineOwnPropertyStr(name string, desc PropertyDescriptor, throw bool) bool + defineOwnPropertyIdx(name valueInt, desc PropertyDescriptor, throw bool) bool + defineOwnPropertySym(name *valueSymbol, desc PropertyDescriptor, throw bool) bool + + deleteStr(name string, throw bool) bool + deleteIdx(idx valueInt, throw bool) bool + deleteSym(s *valueSymbol, throw bool) bool + toPrimitiveNumber() Value toPrimitiveString() Value toPrimitive() Value assertCallable() (call func(FunctionCall) Value, ok bool) - deleteStr(name string, throw bool) bool - delete(name Value, throw bool) bool + assertConstructor() func(args []Value, newTarget *Object) *Object proto() *Object - setProto(proto *Object) *Object + setProto(proto *Object, throw bool) bool hasInstance(v Value) bool isExtensible() bool - preventExtensions() - enumerate(all, recursive bool) iterNextFunc - _enumerate(recursive bool) iterNextFunc + preventExtensions(throw bool) bool + enumerate() iterNextFunc + enumerateUnfiltered() iterNextFunc export() interface{} exportType() reflect.Type equal(objectImpl) bool - getOwnSymbols() []Value + ownKeys(all bool, accum []Value) []Value + ownSymbols() []Value + ownPropertyKeys(all bool, accum []Value) []Value + + _putProp(name string, value Value, writable, enumerable, configurable bool) Value + _putSym(s *valueSymbol, prop Value) } type baseObject struct { @@ -146,7 +235,9 @@ type baseObject struct { values map[string]Value propNames []string - symValues map[*valueSymbol]Value + lastSortedPropLen, idxPropCount int + + symValues *orderedMap } type primitiveValueObject struct { @@ -194,78 +285,102 @@ func (o *baseObject) className() string { return o.class } -func (o *baseObject) getPropStr(name string) Value { - if val := o.getOwnPropStr(name); val != nil { - return val +func (o *baseObject) hasPropertyStr(name string) bool { + if o.val.self.hasOwnPropertyStr(name) { + return true } if o.prototype != nil { - return o.prototype.self.getPropStr(name) + return o.prototype.self.hasPropertyStr(name) } - return nil + return false +} + +func (o *baseObject) hasPropertyIdx(idx valueInt) bool { + return o.val.self.hasPropertyStr(idx.String()) } -func (o *baseObject) getPropSym(s *valueSymbol) Value { - if val := o.symValues[s]; val != nil { - return val +func (o *baseObject) hasPropertySym(s *valueSymbol) bool { + if o.hasOwnPropertySym(s) { + return true } if o.prototype != nil { - return o.prototype.self.getProp(s) + return o.prototype.self.hasPropertySym(s) } - return nil + return false } -func (o *baseObject) getProp(n Value) Value { - if s, ok := n.(*valueSymbol); ok { - return o.getPropSym(s) +func (o *baseObject) getWithOwnProp(prop, p, receiver Value) Value { + if prop == nil && o.prototype != nil { + if receiver == nil { + return o.prototype.get(p, o.val) + } + return o.prototype.get(p, receiver) } - return o.val.self.getPropStr(n.String()) + if prop, ok := prop.(*valueProperty); ok { + if receiver == nil { + return prop.get(o.val) + } + return prop.get(receiver) + } + return prop } -func (o *baseObject) hasProperty(n Value) bool { - return o.val.self.getProp(n) != nil +func (o *baseObject) getStrWithOwnProp(prop Value, name string, receiver Value) Value { + if prop == nil && o.prototype != nil { + if receiver == nil { + return o.prototype.self.getStr(name, o.val) + } + return o.prototype.self.getStr(name, receiver) + } + if prop, ok := prop.(*valueProperty); ok { + if receiver == nil { + return prop.get(o.val) + } + return prop.get(receiver) + } + return prop } -func (o *baseObject) hasPropertyStr(name string) bool { - return o.val.self.getPropStr(name) != nil +func (o *baseObject) getIdx(idx valueInt, receiver Value) Value { + return o.val.self.getStr(idx.String(), receiver) } -func (o *baseObject) _getStr(name string) Value { - p := o.getOwnPropStr(name) +func (o *baseObject) getSym(s *valueSymbol, receiver Value) Value { + return o.getWithOwnProp(o.getOwnPropSym(s), s, receiver) +} - if p == nil && o.prototype != nil { - p = o.prototype.self.getPropStr(name) +func (o *baseObject) getStr(name string, receiver Value) Value { + prop := o.values[name] + if prop == nil { + if o.prototype != nil { + if receiver == nil { + return o.prototype.self.getStr(name, o.val) + } + return o.prototype.self.getStr(name, receiver) + } } - - if p, ok := p.(*valueProperty); ok { - return p.get(o.val) + if prop, ok := prop.(*valueProperty); ok { + if receiver == nil { + return prop.get(o.val) + } + return prop.get(receiver) } - - return p + return prop } -func (o *baseObject) getStr(name string) Value { - p := o.val.self.getPropStr(name) - if p, ok := p.(*valueProperty); ok { - return p.get(o.val) - } - - return p +func (o *baseObject) getOwnPropIdx(idx valueInt) Value { + return o.val.self.getOwnPropStr(idx.String()) } -func (o *baseObject) getSym(s *valueSymbol) Value { - p := o.getPropSym(s) - if p, ok := p.(*valueProperty); ok { - return p.get(o.val) +func (o *baseObject) getOwnPropSym(s *valueSymbol) Value { + if o.symValues != nil { + return o.symValues.get(s) } - - return p + return nil } -func (o *baseObject) get(n Value) Value { - if s, ok := n.(*valueSymbol); ok { - return o.getSym(s) - } - return o.getStr(n.String()) +func (o *baseObject) getOwnPropStr(name string) Value { + return o.values[name] } func (o *baseObject) checkDeleteProp(name string, prop *valueProperty, throw bool) bool { @@ -289,194 +404,246 @@ func (o *baseObject) _delete(name string) { if n == name { copy(o.propNames[i:], o.propNames[i+1:]) o.propNames = o.propNames[:len(o.propNames)-1] + if i < o.lastSortedPropLen { + o.lastSortedPropLen-- + if i < o.idxPropCount { + o.idxPropCount-- + } + } break } } } -func (o *baseObject) deleteStr(name string, throw bool) bool { - if val, exists := o.values[name]; exists { - if !o.checkDelete(name, val, throw) { - return false - } - o._delete(name) - } - return true +func (o *baseObject) deleteIdx(idx valueInt, throw bool) bool { + return o.val.self.deleteStr(idx.String(), throw) } func (o *baseObject) deleteSym(s *valueSymbol, throw bool) bool { - if val, exists := o.symValues[s]; exists { - if !o.checkDelete(s.String(), val, throw) { - return false + if o.symValues != nil { + if val := o.symValues.get(s); val != nil { + if !o.checkDelete(s.String(), val, throw) { + return false + } + o.symValues.remove(s) } - delete(o.symValues, s) } return true } -func (o *baseObject) delete(n Value, throw bool) bool { - if s, ok := n.(*valueSymbol); ok { - return o.deleteSym(s, throw) - } - return o.deleteStr(n.String(), throw) -} - -func (o *baseObject) put(n Value, val Value, throw bool) { - if s, ok := n.(*valueSymbol); ok { - o.putSym(s, val, throw) - } else { - o.putStr(n.String(), val, throw) - } -} - -func (o *baseObject) getOwnPropStr(name string) Value { - v := o.values[name] - if v == nil && name == __proto__ { - return o.prototype - } - return v -} - -func (o *baseObject) getOwnProp(name Value) Value { - if s, ok := name.(*valueSymbol); ok { - return o.symValues[s] +func (o *baseObject) deleteStr(name string, throw bool) bool { + if val, exists := o.values[name]; exists { + if !o.checkDelete(name, val, throw) { + return false + } + o._delete(name) } - - return o.val.self.getOwnPropStr(name.String()) + return true } -func (o *baseObject) setProto(proto *Object) *Object { +func (o *baseObject) setProto(proto *Object, throw bool) bool { current := o.prototype if current.SameAs(proto) { - return nil + return true } if !o.extensible { - return o.val.runtime.NewTypeError("%s is not extensible", o.val) + o.val.runtime.typeErrorResult(throw, "%s is not extensible", o.val) + return false } for p := proto; p != nil; { if p.SameAs(o.val) { - return o.val.runtime.NewTypeError("Cyclic __proto__ value") + o.val.runtime.typeErrorResult(throw, "Cyclic __proto__ value") + return false } p = p.self.proto() } o.prototype = proto - return nil + return true } -func (o *baseObject) putStr(name string, val Value, throw bool) { - if v, exists := o.values[name]; exists { - if prop, ok := v.(*valueProperty); ok { - if !prop.isWritable() { - o.val.runtime.typeErrorResult(throw, "Cannot assign to read only property '%s'", name) - return +func (o *baseObject) _setProto(val Value) { + var proto *Object + if val != _null { + if obj, ok := val.(*Object); ok { + proto = obj + } else { + return + } + } + o.setProto(proto, true) +} + +func (o *baseObject) setOwnStr(name string, val Value, throw bool) bool { + ownDesc := o.values[name] + if ownDesc == nil { + if proto := o.prototype; proto != nil { + // we know it's foreign because prototype loops are not allowed + if res, handled := proto.self.setForeignStr(name, val, o.val, throw); handled { + return res } + } + // new property + if !o.extensible { + o.val.runtime.typeErrorResult(throw, "Cannot add property %s, object is not extensible", name) + return false + } else { + o.values[name] = val + o.propNames = append(o.propNames, name) + } + return true + } + if prop, ok := ownDesc.(*valueProperty); ok { + if !prop.isWritable() { + o.val.runtime.typeErrorResult(throw, "Cannot assign to read only property '%s'", name) + return false + } else { prop.set(o.val, val) - return } + } else { o.values[name] = val - return } + return true +} - if name == __proto__ { - var proto *Object - if val != _null { - if obj, ok := val.(*Object); ok { - proto = obj - } else { - return +func (o *baseObject) setOwnIdx(idx valueInt, val Value, throw bool) bool { + return o.val.self.setOwnStr(idx.String(), val, throw) +} + +func (o *baseObject) setOwnSym(name *valueSymbol, val Value, throw bool) bool { + var ownDesc Value + if o.symValues != nil { + ownDesc = o.symValues.get(name) + } + if ownDesc == nil { + if proto := o.prototype; proto != nil { + // we know it's foreign because prototype loops are not allowed + if res, handled := proto.self.setForeignSym(name, val, o.val, throw); handled { + return res } } - if ex := o.setProto(proto); ex != nil { - panic(ex) + // new property + if !o.extensible { + o.val.runtime.typeErrorResult(throw, "Cannot add property %s, object is not extensible", name) + return false + } else { + if o.symValues == nil { + o.symValues = newOrderedMap() + } + o.symValues.set(name, val) } - return + return true } - - var pprop Value - if proto := o.prototype; proto != nil { - pprop = proto.self.getPropStr(name) + if prop, ok := ownDesc.(*valueProperty); ok { + if !prop.isWritable() { + o.val.runtime.typeErrorResult(throw, "Cannot assign to read only property '%s'", name) + return false + } else { + prop.set(o.val, val) + } + } else { + o.symValues.set(name, val) } + return true +} - if pprop != nil { - if prop, ok := pprop.(*valueProperty); ok { +func (o *baseObject) _setForeignStr(name string, prop, val, receiver Value, throw bool) (bool, bool) { + if prop != nil { + if prop, ok := prop.(*valueProperty); ok { if !prop.isWritable() { - o.val.runtime.typeErrorResult(throw) - return + o.val.runtime.typeErrorResult(throw, "Cannot assign to read only property '%s'", name) + return false, true } - if prop.accessor { - prop.set(o.val, val) - return + if prop.setterFunc != nil { + prop.set(receiver, val) + return true, true } } } else { - if !o.extensible { - o.val.runtime.typeErrorResult(throw) - return + if proto := o.prototype; proto != nil { + if receiver != proto { + return proto.self.setForeignStr(name, val, receiver, throw) + } + return proto.self.setOwnStr(name, val, throw), true } } - - o.values[name] = val - o.propNames = append(o.propNames, name) + return false, false } -func (o *baseObject) putSym(s *valueSymbol, val Value, throw bool) { - if v, exists := o.symValues[s]; exists { - if prop, ok := v.(*valueProperty); ok { +func (o *baseObject) _setForeignIdx(idx valueInt, prop, val, receiver Value, throw bool) (bool, bool) { + if prop != nil { + if prop, ok := prop.(*valueProperty); ok { if !prop.isWritable() { - o.val.runtime.typeErrorResult(throw, "Cannot assign to read only property '%s'", s.String()) - return + o.val.runtime.typeErrorResult(throw, "Cannot assign to read only property '%d'", idx) + return false, true + } + if prop.setterFunc != nil { + prop.set(receiver, val) + return true, true } - prop.set(o.val, val) - return } - o.symValues[s] = val - return + } else { + if proto := o.prototype; proto != nil { + if receiver != proto { + return proto.self.setForeignIdx(idx, val, receiver, throw) + } + return proto.self.setOwnIdx(idx, val, throw), true + } } + return false, false +} - var pprop Value - if proto := o.prototype; proto != nil { - pprop = proto.self.getProp(s) - } +func (o *baseObject) setForeignStr(name string, val, receiver Value, throw bool) (bool, bool) { + return o._setForeignStr(name, o.values[name], val, receiver, throw) +} - if pprop != nil { - if prop, ok := pprop.(*valueProperty); ok { +func (o *baseObject) setForeignIdx(name valueInt, val, receiver Value, throw bool) (bool, bool) { + return o.val.self.setForeignStr(name.String(), val, receiver, throw) +} + +func (o *baseObject) setForeignSym(name *valueSymbol, val, receiver Value, throw bool) (bool, bool) { + var prop Value + if o.symValues != nil { + prop = o.symValues.get(name) + } + if prop != nil { + if prop, ok := prop.(*valueProperty); ok { if !prop.isWritable() { - o.val.runtime.typeErrorResult(throw) - return + o.val.runtime.typeErrorResult(throw, "Cannot assign to read only property '%s'", name) + return false, true } - if prop.accessor { - prop.set(o.val, val) - return + if prop.setterFunc != nil { + prop.set(receiver, val) + return true, true } } } else { - if !o.extensible { - o.val.runtime.typeErrorResult(throw) - return + if proto := o.prototype; proto != nil { + if receiver != o.val { + return proto.self.setForeignSym(name, val, receiver, throw) + } + return proto.self.setOwnSym(name, val, throw), true } } - - if o.symValues == nil { - o.symValues = make(map[*valueSymbol]Value, 1) - } - o.symValues[s] = val + return false, false } -func (o *baseObject) hasOwnProperty(n Value) bool { - if s, ok := n.(*valueSymbol); ok { - _, exists := o.symValues[s] - return exists +func (o *baseObject) hasOwnPropertySym(s *valueSymbol) bool { + if o.symValues != nil { + return o.symValues.has(s) } - v := o.values[n.String()] - return v != nil + return false } func (o *baseObject) hasOwnPropertyStr(name string) bool { - v := o.values[name] - return v != nil + _, exists := o.values[name] + return exists +} + +func (o *baseObject) hasOwnPropertyIdx(idx valueInt) bool { + return o.val.self.hasOwnPropertyStr(idx.String()) } -func (o *baseObject) _defineOwnProperty(name, existingValue Value, descr propertyDescr, throw bool) (val Value, ok bool) { +func (o *baseObject) _defineOwnProperty(name string, existingValue Value, descr PropertyDescriptor, throw bool) (val Value, ok bool) { getterObj, _ := descr.Getter.(*Object) setterObj, _ := descr.Setter.(*Object) @@ -485,7 +652,7 @@ func (o *baseObject) _defineOwnProperty(name, existingValue Value, descr propert if existingValue == nil { if !o.extensible { - o.val.runtime.typeErrorResult(throw) + o.val.runtime.typeErrorResult(throw, "Cannot define property %s, object is not extensible", name) return nil, false } existing = &valueProperty{} @@ -574,27 +741,14 @@ func (o *baseObject) _defineOwnProperty(name, existingValue Value, descr propert return existing, true Reject: - o.val.runtime.typeErrorResult(throw, "Cannot redefine property: %s", name.toString()) + o.val.runtime.typeErrorResult(throw, "Cannot redefine property: %s", name) return nil, false } -func (o *baseObject) defineOwnProperty(n Value, descr propertyDescr, throw bool) bool { - n = toPropertyKey(n) - if s, ok := n.(*valueSymbol); ok { - existingVal := o.symValues[s] - if v, ok := o._defineOwnProperty(n, existingVal, descr, throw); ok { - if o.symValues == nil { - o.symValues = make(map[*valueSymbol]Value, 1) - } - o.symValues[s] = v - return true - } - return false - } - name := n.String() +func (o *baseObject) defineOwnPropertyStr(name string, descr PropertyDescriptor, throw bool) bool { existingVal := o.values[name] - if v, ok := o._defineOwnProperty(n, existingVal, descr, throw); ok { + if v, ok := o._defineOwnProperty(name, existingVal, descr, throw); ok { o.values[name] = v if existingVal == nil { o.propNames = append(o.propNames, name) @@ -604,6 +758,25 @@ func (o *baseObject) defineOwnProperty(n Value, descr propertyDescr, throw bool) return false } +func (o *baseObject) defineOwnPropertyIdx(idx valueInt, desc PropertyDescriptor, throw bool) bool { + return o.val.self.defineOwnPropertyStr(idx.String(), desc, throw) +} + +func (o *baseObject) defineOwnPropertySym(s *valueSymbol, descr PropertyDescriptor, throw bool) bool { + var existingVal Value + if o.symValues != nil { + existingVal = o.symValues.get(s) + } + if v, ok := o._defineOwnProperty(s.String(), existingVal, descr, throw); ok { + if o.symValues == nil { + o.symValues = newOrderedMap() + } + o.symValues.set(s, v) + return true + } + return false +} + func (o *baseObject) _put(name string, v Value) { if _, exists := o.values[name]; !exists { o.propNames = append(o.propNames, name) @@ -630,8 +803,15 @@ func (o *baseObject) _putProp(name string, value Value, writable, enumerable, co return prop } +func (o *baseObject) _putSym(s *valueSymbol, prop Value) { + if o.symValues == nil { + o.symValues = newOrderedMap() + } + o.symValues.set(s, prop) +} + func (o *baseObject) tryExoticToPrimitive(hint string) Value { - exoticToPrimitive := toMethod(o.getSym(symToPrimitive)) + exoticToPrimitive := toMethod(o.getSym(symToPrimitive, nil)) if exoticToPrimitive != nil { return exoticToPrimitive(FunctionCall{ This: o.val, @@ -642,7 +822,7 @@ func (o *baseObject) tryExoticToPrimitive(hint string) Value { } func (o *baseObject) tryPrimitive(methodName string) Value { - if method, ok := o.getStr(methodName).(*Object); ok { + if method, ok := o.val.self.getStr(methodName, nil).(*Object); ok { if call, ok := method.self.assertCallable(); ok { v := call(FunctionCall{ This: o.val, @@ -697,6 +877,10 @@ func (o *baseObject) assertCallable() (func(FunctionCall) Value, bool) { return nil, false } +func (o *baseObject) assertConstructor() func(args []Value, newTarget *Object) *Object { + return nil +} + func (o *baseObject) proto() *Object { return o.prototype } @@ -705,43 +889,42 @@ func (o *baseObject) isExtensible() bool { return o.extensible } -func (o *baseObject) preventExtensions() { +func (o *baseObject) preventExtensions(bool) bool { o.extensible = false + return true } func (o *baseObject) sortLen() int64 { - return toLength(o.val.self.getStr("length")) + return toLength(o.val.self.getStr("length", nil)) } func (o *baseObject) sortGet(i int64) Value { - return o.val.self.get(intToValue(i)) + return o.val.self.getIdx(valueInt(i), nil) } func (o *baseObject) swap(i, j int64) { - ii := intToValue(i) - jj := intToValue(j) + ii := valueInt(i) + jj := valueInt(j) - x := o.val.self.get(ii) - y := o.val.self.get(jj) + x := o.val.self.getIdx(ii, nil) + y := o.val.self.getIdx(jj, nil) - o.val.self.put(ii, y, false) - o.val.self.put(jj, x, false) + o.val.self.setOwnIdx(ii, y, false) + o.val.self.setOwnIdx(jj, x, false) } func (o *baseObject) export() interface{} { m := make(map[string]interface{}) - - for item, f := o.enumerate(false, false)(); f != nil; item, f = f() { - v := item.value - if v == nil { - v = o.getStr(item.name) - } + for _, itemName := range o.ownKeys(false, nil) { + itemNameStr := itemName.String() + v := o.val.self.getStr(itemNameStr, nil) if v != nil { - m[item.name] = v.Export() + m[itemNameStr] = v.Export() } else { - m[item.name] = nil + m[itemNameStr] = nil } } + return m } @@ -766,7 +949,6 @@ type propIterItem struct { type objectPropIter struct { o *baseObject propNames []string - recursive bool idx int } @@ -813,43 +995,130 @@ func (i *objectPropIter) next() (propIterItem, iterNextFunc) { } } - if i.recursive && i.o.prototype != nil { - return i.o.prototype.self._enumerate(i.recursive)() - } return propIterItem{}, nil } -func (o *baseObject) _enumerate(recursive bool) iterNextFunc { +func (o *baseObject) enumerate() iterNextFunc { + return (&propFilterIter{ + wrapped: o.val.self.enumerateUnfiltered(), + seen: make(map[string]bool), + }).next +} + +func (o *baseObject) ownIter() iterNextFunc { + if len(o.propNames) > o.lastSortedPropLen { + o.fixPropOrder() + } propNames := make([]string, len(o.propNames)) copy(propNames, o.propNames) return (&objectPropIter{ o: o, propNames: propNames, - recursive: recursive, }).next } -func (o *baseObject) enumerate(all, recursive bool) iterNextFunc { - return (&propFilterIter{ - wrapped: o._enumerate(recursive), - all: all, - seen: make(map[string]bool), +func (o *baseObject) recursiveIter(iter iterNextFunc) iterNextFunc { + return (&recursiveIter{ + o: o, + wrapped: iter, }).next } +func (o *baseObject) enumerateUnfiltered() iterNextFunc { + return o.recursiveIter(o.ownIter()) +} + +type recursiveIter struct { + o *baseObject + wrapped iterNextFunc +} + +func (iter *recursiveIter) next() (propIterItem, iterNextFunc) { + item, next := iter.wrapped() + if next != nil { + iter.wrapped = next + return item, iter.next + } + if proto := iter.o.prototype; proto != nil { + return proto.self.enumerateUnfiltered()() + } + return propIterItem{}, nil +} + func (o *baseObject) equal(objectImpl) bool { // Rely on parent reference comparison return false } -func (o *baseObject) getOwnSymbols() (res []Value) { - for s := range o.symValues { - res = append(res, s) +// Reorder property names so that any integer properties are shifted to the beginning of the list +// in ascending order. This is to conform to ES6 9.1.12. +// Personally I think this requirement is strange. I can sort of understand where they are coming from, +// this way arrays can be specified just as objects with a 'magic' length property. However, I think +// it's safe to assume most devs don't use Objects to store integer properties. Therefore, performing +// property type checks when adding (and potentially looking up) properties would be unreasonable. +// Instead, we keep insertion order and only change it when (if) the properties get enumerated. +func (o *baseObject) fixPropOrder() { + names := o.propNames + for i := o.lastSortedPropLen; i < len(names); i++ { + name := names[i] + if idx := strToIdx(name); idx != math.MaxUint32 { + k := sort.Search(o.idxPropCount, func(j int) bool { + return strToIdx(names[j]) >= idx + }) + if k < i { + copy(names[k+1:i+1], names[k:i]) + names[k] = name + } + o.idxPropCount++ + } + } + o.lastSortedPropLen = len(names) +} + +func (o *baseObject) ownKeys(all bool, keys []Value) []Value { + if len(o.propNames) > o.lastSortedPropLen { + o.fixPropOrder() + } + if all { + for _, k := range o.propNames { + keys = append(keys, newStringValue(k)) + } + } else { + for _, k := range o.propNames { + prop := o.values[k] + if prop, ok := prop.(*valueProperty); ok && !prop.enumerable { + continue + } + keys = append(keys, newStringValue(k)) + } + } + return keys +} + +func (o *baseObject) ownSymbols() (res []Value) { + if o.symValues != nil { + iter := o.symValues.newIter() + for { + entry := iter.next() + if entry == nil { + break + } + res = append(res, entry.key) + } } return } +func (o *baseObject) ownPropertyKeys(all bool, accum []Value) []Value { + accum = o.val.self.ownKeys(all, accum) + if all { + accum = append(accum, o.ownSymbols()...) + } + + return accum +} + func (o *baseObject) hasInstance(Value) bool { panic(o.val.runtime.NewTypeError("Expecting a function in instanceof check, but got %s", o.val.toString())) } @@ -867,7 +1136,7 @@ func toMethod(v Value) func(FunctionCall) Value { } func instanceOfOperator(o Value, c *Object) bool { - if instOfHandler := toMethod(c.self.get(symHasInstance)); instOfHandler != nil { + if instOfHandler := toMethod(c.self.getSym(symHasInstance, c)); instOfHandler != nil { return instOfHandler(FunctionCall{ This: c, Arguments: []Value{o}, @@ -877,6 +1146,219 @@ func instanceOfOperator(o Value, c *Object) bool { return c.self.hasInstance(o) } +func (o *Object) get(p Value, receiver Value) Value { + switch p := p.(type) { + case valueString: + return o.self.getStr(p.String(), receiver) + case valueInt: + return o.self.getIdx(p, receiver) + case *valueSymbol: + return o.self.getSym(p, receiver) + default: + return o.self.getStr(p.String(), receiver) + } +} + +func (o *Object) getOwnProp(p Value) Value { + switch p := p.(type) { + case valueString: + return o.self.getOwnPropStr(p.String()) + case valueInt: + return o.self.getOwnPropIdx(p) + case *valueSymbol: + return o.self.getOwnPropSym(p) + default: + return o.self.getOwnPropStr(p.String()) + } +} + +func (o *Object) hasOwnProperty(p Value) bool { + switch p := p.(type) { + case valueString: + return o.self.hasOwnPropertyStr(p.String()) + case valueInt: + return o.self.hasOwnPropertyIdx(p) + case *valueSymbol: + return o.self.hasOwnPropertySym(p) + default: + return o.self.hasOwnPropertyStr(p.String()) + } +} + +func (o *Object) hasProperty(p Value) bool { + switch p := p.(type) { + case valueString: + return o.self.hasPropertyStr(p.String()) + case valueInt: + return o.self.hasPropertyIdx(p) + case *valueSymbol: + return o.self.hasPropertySym(p) + default: + return o.self.hasPropertyStr(p.String()) + } +} + +func (o *Object) setStr(name string, val, receiver Value, throw bool) bool { + if receiver == o { + return o.self.setOwnStr(name, val, throw) + } else { + if res, ok := o.self.setForeignStr(name, val, receiver, throw); !ok { + if robj, ok := receiver.(*Object); ok { + if prop := robj.self.getOwnPropStr(name); prop != nil { + if desc, ok := prop.(*valueProperty); ok { + if desc.accessor { + o.runtime.typeErrorResult(throw, "Receiver property %s is an accessor", name) + return false + } + if !desc.writable { + o.runtime.typeErrorResult(throw, "Cannot assign to read only property '%s'", name) + return false + } + } + robj.self.defineOwnPropertyStr(name, PropertyDescriptor{Value: val}, throw) + } else { + robj.self.defineOwnPropertyStr(name, PropertyDescriptor{ + Value: val, + Writable: FLAG_TRUE, + Configurable: FLAG_TRUE, + Enumerable: FLAG_TRUE, + }, throw) + } + } else { + o.runtime.typeErrorResult(throw, "Receiver is not an object: %v", receiver) + return false + } + } else { + return res + } + } + return true +} + +func (o *Object) set(name Value, val, receiver Value, throw bool) bool { + switch name := name.(type) { + case valueString: + return o.setStr(name.String(), val, receiver, throw) + case valueInt: + return o.setIdx(name, val, receiver, throw) + case *valueSymbol: + return o.setSym(name, val, receiver, throw) + default: + return o.setStr(name.String(), val, receiver, throw) + } +} + +func (o *Object) setOwn(name Value, val Value, throw bool) bool { + switch name := name.(type) { + case valueInt: + return o.self.setOwnIdx(name, val, throw) + case *valueSymbol: + return o.self.setOwnSym(name, val, throw) + default: + return o.self.setOwnStr(name.String(), val, throw) + } +} + +func (o *Object) setIdx(name valueInt, val, receiver Value, throw bool) bool { + if receiver == o { + return o.self.setOwnIdx(name, val, throw) + } else { + if res, ok := o.self.setForeignIdx(name, val, receiver, throw); !ok { + if robj, ok := receiver.(*Object); ok { + if prop := robj.self.getOwnPropIdx(name); prop != nil { + if desc, ok := prop.(*valueProperty); ok { + if desc.accessor { + o.runtime.typeErrorResult(throw, "Receiver property %s is an accessor", name) + return false + } + if !desc.writable { + o.runtime.typeErrorResult(throw, "Cannot assign to read only property '%s'", name) + return false + } + } + robj.self.defineOwnPropertyIdx(name, PropertyDescriptor{Value: val}, throw) + } else { + robj.self.defineOwnPropertyIdx(name, PropertyDescriptor{ + Value: val, + Writable: FLAG_TRUE, + Configurable: FLAG_TRUE, + Enumerable: FLAG_TRUE, + }, throw) + } + } else { + o.runtime.typeErrorResult(throw, "Receiver is not an object: %v", receiver) + return false + } + } else { + return res + } + } + return true +} + +func (o *Object) setSym(name *valueSymbol, val, receiver Value, throw bool) bool { + if receiver == o { + return o.self.setOwnSym(name, val, throw) + } else { + if res, ok := o.self.setForeignSym(name, val, receiver, throw); !ok { + if robj, ok := receiver.(*Object); ok { + if prop := robj.self.getOwnPropSym(name); prop != nil { + if desc, ok := prop.(*valueProperty); ok { + if desc.accessor { + o.runtime.typeErrorResult(throw, "Receiver property %s is an accessor", name) + return false + } + if !desc.writable { + o.runtime.typeErrorResult(throw, "Cannot assign to read only property '%s'", name) + return false + } + } + robj.self.defineOwnPropertySym(name, PropertyDescriptor{Value: val}, throw) + } else { + robj.self.defineOwnPropertySym(name, PropertyDescriptor{ + Value: val, + Writable: FLAG_TRUE, + Configurable: FLAG_TRUE, + Enumerable: FLAG_TRUE, + }, throw) + } + } else { + o.runtime.typeErrorResult(throw, "Receiver is not an object: %v", receiver) + return false + } + } else { + return res + } + } + return true +} + +func (o *Object) delete(n Value, throw bool) bool { + switch n := n.(type) { + case valueString: + return o.self.deleteStr(n.String(), throw) + case valueInt: + return o.self.deleteIdx(n, throw) + case *valueSymbol: + return o.self.deleteSym(n, throw) + default: + return o.self.deleteStr(n.String(), throw) + } +} + +func (o *Object) defineOwnProperty(n Value, desc PropertyDescriptor, throw bool) bool { + switch n := n.(type) { + case valueString: + return o.self.defineOwnPropertyStr(n.String(), desc, throw) + case valueInt: + return o.self.defineOwnPropertyIdx(n, desc, throw) + case *valueSymbol: + return o.self.defineOwnPropertySym(n, desc, throw) + default: + return o.self.defineOwnPropertyStr(n.String(), desc, throw) + } +} + func (o *Object) getWeakCollRefs() *weakCollections { if o.weakColls == nil { o.weakColls = &weakCollections{} diff --git a/object_args.go b/object_args.go index d0ee818e..85aaf5b7 100644 --- a/object_args.go +++ b/object_args.go @@ -10,15 +10,16 @@ type mappedProperty struct { v *Value } -func (a *argumentsObject) getPropStr(name string) Value { - if prop, ok := a.values[name].(*mappedProperty); ok { - return *prop.v - } - return a.baseObject.getPropStr(name) +func (a *argumentsObject) getStr(name string, receiver Value) Value { + return a.getStrWithOwnProp(a.getOwnPropStr(name), name, receiver) } -func (a *argumentsObject) getProp(n Value) Value { - return a.getPropStr(n.String()) +func (a *argumentsObject) getOwnPropStr(name string) Value { + if mapped, ok := a.values[name].(*mappedProperty); ok { + return *mapped.v + } + + return a.baseObject.getOwnPropStr(name) } func (a *argumentsObject) init() { @@ -26,15 +27,23 @@ func (a *argumentsObject) init() { a._putProp("length", intToValue(int64(a.length)), true, false, true) } -func (a *argumentsObject) put(n Value, val Value, throw bool) { - if s, ok := n.(*valueSymbol); ok { - a.putSym(s, val, throw) - return +func (a *argumentsObject) setOwnStr(name string, val Value, throw bool) bool { + if prop, ok := a.values[name].(*mappedProperty); ok { + if !prop.writable { + a.val.runtime.typeErrorResult(throw, "Property is not writable: %s", name) + return false + } + *prop.v = val + return true } - a.putStr(n.String(), val, throw) + return a.baseObject.setOwnStr(name, val, throw) +} + +func (a *argumentsObject) setForeignStr(name string, val, receiver Value, throw bool) (bool, bool) { + return a._setForeignStr(name, a.getOwnPropStr(name), val, receiver, throw) } -func (a *argumentsObject) putStr(name string, val Value, throw bool) { +/*func (a *argumentsObject) putStr(name string, val Value, throw bool) { if prop, ok := a.values[name].(*mappedProperty); ok { if !prop.writable { a.val.runtime.typeErrorResult(throw, "Property is not writable: %s", name) @@ -44,7 +53,7 @@ func (a *argumentsObject) putStr(name string, val Value, throw bool) { return } a.baseObject.putStr(name, val, throw) -} +}*/ func (a *argumentsObject) deleteStr(name string, throw bool) bool { if prop, ok := a.values[name].(*mappedProperty); ok { @@ -58,13 +67,6 @@ func (a *argumentsObject) deleteStr(name string, throw bool) bool { return a.baseObject.deleteStr(name, throw) } -func (a *argumentsObject) delete(n Value, throw bool) bool { - if s, ok := n.(*valueSymbol); ok { - return a.deleteSym(s, throw) - } - return a.deleteStr(n.String(), throw) -} - type argumentsPropIter struct { wrapped iterNextFunc } @@ -81,24 +83,13 @@ func (i *argumentsPropIter) next() (propIterItem, iterNextFunc) { return item, i.next } -func (a *argumentsObject) _enumerate(recursive bool) iterNextFunc { - return (&argumentsPropIter{ - wrapped: a.baseObject._enumerate(recursive), - }).next - -} - -func (a *argumentsObject) enumerate(all, recursive bool) iterNextFunc { - return (&argumentsPropIter{ - wrapped: a.baseObject.enumerate(all, recursive), - }).next +func (a *argumentsObject) enumerateUnfiltered() iterNextFunc { + return a.recursiveIter((&argumentsPropIter{ + wrapped: a.ownIter(), + }).next) } -func (a *argumentsObject) defineOwnProperty(n Value, descr propertyDescr, throw bool) bool { - if _, ok := n.(*valueSymbol); ok { - return a.baseObject.defineOwnProperty(n, descr, throw) - } - name := n.String() +func (a *argumentsObject) defineOwnPropertyStr(name string, descr PropertyDescriptor, throw bool) bool { if mapped, ok := a.values[name].(*mappedProperty); ok { existing := &valueProperty{ configurable: mapped.configurable, @@ -107,7 +98,7 @@ func (a *argumentsObject) defineOwnProperty(n Value, descr propertyDescr, throw value: mapped.get(a.val), } - val, ok := a.baseObject._defineOwnProperty(n, existing, descr, throw) + val, ok := a.baseObject._defineOwnProperty(name, existing, descr, throw) if !ok { return false } @@ -131,21 +122,13 @@ func (a *argumentsObject) defineOwnProperty(n Value, descr propertyDescr, throw return true } - return a.baseObject.defineOwnProperty(n, descr, throw) -} - -func (a *argumentsObject) getOwnPropStr(name string) Value { - if mapped, ok := a.values[name].(*mappedProperty); ok { - return *mapped.v - } - - return a.baseObject.getOwnPropStr(name) + return a.baseObject.defineOwnPropertyStr(name, descr, throw) } func (a *argumentsObject) export() interface{} { arr := make([]interface{}, a.length) for i := range arr { - v := a.get(intToValue(int64(i))) + v := a.getIdx(valueInt(int64(i)), nil) if v != nil { arr[i] = v.Export() } diff --git a/object_gomap.go b/object_gomap.go index 54c826a0..ad9d4e6a 100644 --- a/object_gomap.go +++ b/object_gomap.go @@ -2,7 +2,6 @@ package goja import ( "reflect" - "strconv" ) type objectGoMapSimple struct { @@ -17,10 +16,6 @@ func (o *objectGoMapSimple) init() { o.extensible = true } -func (o *objectGoMapSimple) _get(n Value) Value { - return o._getStr(n.String()) -} - func (o *objectGoMapSimple) _getStr(name string) Value { v, exists := o.data[name] if !exists { @@ -29,93 +24,73 @@ func (o *objectGoMapSimple) _getStr(name string) Value { return o.val.runtime.ToValue(v) } -func (o *objectGoMapSimple) get(n Value) Value { - return o.getStr(n.String()) -} - -func (o *objectGoMapSimple) getProp(n Value) Value { - return o.getPropStr(n.String()) -} - -func (o *objectGoMapSimple) getPropStr(name string) Value { +func (o *objectGoMapSimple) getStr(name string, receiver Value) Value { if v := o._getStr(name); v != nil { return v } - return o.baseObject.getPropStr(name) -} - -func (o *objectGoMapSimple) getStr(name string) Value { - if v := o._getStr(name); v != nil { - return v - } - return o.baseObject._getStr(name) + return o.baseObject.getStr(name, receiver) } func (o *objectGoMapSimple) getOwnPropStr(name string) Value { if v := o._getStr(name); v != nil { return v } - return o.baseObject.getOwnPropStr(name) -} - -func (o *objectGoMapSimple) put(n Value, val Value, throw bool) { - if _, ok := n.(*valueSymbol); ok { - o.val.runtime.typeErrorResult(throw, "Cannot set Symbol properties on Go maps") - return - } - o.putStr(n.String(), val, throw) + return nil } -func (o *objectGoMapSimple) _hasStr(name string) bool { - _, exists := o.data[name] - return exists -} - -func (o *objectGoMapSimple) _has(n Value) bool { - return o._hasStr(n.String()) -} - -func (o *objectGoMapSimple) putStr(name string, val Value, throw bool) { - if o.extensible || o._hasStr(name) { +func (o *objectGoMapSimple) setOwnStr(name string, val Value, throw bool) bool { + if _, exists := o.data[name]; exists { o.data[name] = val.Export() + return true + } + if proto := o.prototype; proto != nil { + // we know it's foreign because prototype loops are not allowed + if res, ok := proto.self.setForeignStr(name, val, o.val, throw); ok { + return res + } + } + // new property + if !o.extensible { + o.val.runtime.typeErrorResult(throw, "Cannot add property %s, object is not extensible", name) + return false } else { - o.val.runtime.typeErrorResult(throw, "Host object is not extensible") + o.data[name] = val.Export() } + return true } -func (o *objectGoMapSimple) hasProperty(n Value) bool { - if o._has(n) { - return true +func trueValIfPresent(present bool) Value { + if present { + return valueTrue } - return o.baseObject.hasProperty(n) + return nil } -func (o *objectGoMapSimple) hasPropertyStr(name string) bool { - if o._hasStr(name) { - return true - } - return o.baseObject.hasOwnPropertyStr(name) +func (o *objectGoMapSimple) setForeignStr(name string, val, receiver Value, throw bool) (bool, bool) { + return o._setForeignStr(name, trueValIfPresent(o._hasStr(name)), val, receiver, throw) } -func (o *objectGoMapSimple) hasOwnProperty(n Value) bool { - return o._has(n) +func (o *objectGoMapSimple) _hasStr(name string) bool { + _, exists := o.data[name] + return exists } func (o *objectGoMapSimple) hasOwnPropertyStr(name string) bool { return o._hasStr(name) } -func (o *objectGoMapSimple) _putProp(name string, value Value, writable, enumerable, configurable bool) Value { - o.putStr(name, value, false) - return value -} - -func (o *objectGoMapSimple) defineOwnProperty(name Value, descr propertyDescr, throw bool) bool { +func (o *objectGoMapSimple) defineOwnPropertyStr(name string, descr PropertyDescriptor, throw bool) bool { if !o.val.runtime.checkHostObjectPropertyDescr(name, descr, throw) { return false } - o.put(name, descr.Value, throw) - return true + + if o.extensible || o._hasStr(name) { + o.data[name] = descr.Value.Export() + return true + } + + o.val.runtime.typeErrorResult(throw, "Cannot define property %s, object is not extensible", name) + return false } /* @@ -136,23 +111,14 @@ func (o *objectGoMapSimple) assertCallable() (call func(FunctionCall) Value, ok } */ -func (o *objectGoMapSimple) deleteStr(name string, throw bool) bool { +func (o *objectGoMapSimple) deleteStr(name string, _ bool) bool { delete(o.data, name) return true } -func (o *objectGoMapSimple) delete(name Value, throw bool) bool { - if _, ok := name.(*valueSymbol); ok { - return true - } - - return o.deleteStr(name.String(), throw) -} - type gomapPropIter struct { o *objectGoMapSimple propNames []string - recursive bool idx int } @@ -165,33 +131,29 @@ func (i *gomapPropIter) next() (propIterItem, iterNextFunc) { } } - if i.recursive { - return i.o.prototype.self._enumerate(true)() - } - return propIterItem{}, nil } -func (o *objectGoMapSimple) enumerate(all, recursive bool) iterNextFunc { - return (&propFilterIter{ - wrapped: o._enumerate(recursive), - all: all, - seen: make(map[string]bool), - }).next -} - -func (o *objectGoMapSimple) _enumerate(recursive bool) iterNextFunc { +func (o *objectGoMapSimple) enumerateUnfiltered() iterNextFunc { propNames := make([]string, len(o.data)) i := 0 for key := range o.data { propNames[i] = key i++ } - return (&gomapPropIter{ + + return o.recursiveIter((&gomapPropIter{ o: o, propNames: propNames, - recursive: recursive, - }).next + }).next) +} + +func (o *objectGoMapSimple) ownKeys(_ bool, accum []Value) []Value { + // all own keys are enumerable + for key := range o.data { + accum = append(accum, newStringValue(key)) + } + return accum } func (o *objectGoMapSimple) export() interface{} { @@ -208,21 +170,3 @@ func (o *objectGoMapSimple) equal(other objectImpl) bool { } return false } - -func (o *objectGoMapSimple) sortLen() int64 { - return int64(len(o.data)) -} - -func (o *objectGoMapSimple) sortGet(i int64) Value { - return o.getStr(strconv.FormatInt(i, 10)) -} - -func (o *objectGoMapSimple) swap(i, j int64) { - ii := strconv.FormatInt(i, 10) - jj := strconv.FormatInt(j, 10) - x := o.getStr(ii) - y := o.getStr(jj) - - o.putStr(ii, y, false) - o.putStr(jj, x, false) -} diff --git a/object_gomap_reflect.go b/object_gomap_reflect.go index fee6db3c..74ae87c9 100644 --- a/object_gomap_reflect.go +++ b/object_gomap_reflect.go @@ -15,10 +15,6 @@ func (o *objectGoMapReflect) init() { } func (o *objectGoMapReflect) toKey(n Value, throw bool) reflect.Value { - if _, ok := n.(*valueSymbol); ok { - o.val.runtime.typeErrorResult(throw, "Cannot set Symbol properties on Go maps") - return reflect.Value{} - } key, err := o.val.runtime.toReflectValue(n, o.keyType) if err != nil { o.val.runtime.typeErrorResult(throw, "map key conversion error: %v", err) @@ -58,26 +54,18 @@ func (o *objectGoMapReflect) _getStr(name string) Value { return nil } -func (o *objectGoMapReflect) get(n Value) Value { - if v := o._get(n); v != nil { +func (o *objectGoMapReflect) getStr(name string, receiver Value) Value { + if v := o._getStr(name); v != nil { return v } - return o.objectGoReflect.get(n) + return o.objectGoReflect.getStr(name, receiver) } -func (o *objectGoMapReflect) getStr(name string) Value { - if v := o._getStr(name); v != nil { +func (o *objectGoMapReflect) getIdx(idx valueInt, receiver Value) Value { + if v := o._get(idx); v != nil { return v } - return o.objectGoReflect.getStr(name) -} - -func (o *objectGoMapReflect) getProp(n Value) Value { - return o.get(n) -} - -func (o *objectGoMapReflect) getPropStr(name string) Value { - return o.getStr(name) + return o.objectGoReflect.getIdx(idx, receiver) } func (o *objectGoMapReflect) getOwnPropStr(name string) Value { @@ -91,6 +79,17 @@ func (o *objectGoMapReflect) getOwnPropStr(name string) Value { return o.objectGoReflect.getOwnPropStr(name) } +func (o *objectGoMapReflect) getOwnPropIdx(idx valueInt) Value { + if v := o._get(idx); v != nil { + return &valueProperty{ + value: v, + writable: true, + enumerable: true, + } + } + return o.objectGoReflect.getOwnPropStr(idx.String()) +} + func (o *objectGoMapReflect) toValue(val Value, throw bool) (reflect.Value, bool) { v, err := o.val.runtime.toReflectValue(val, o.valueType) if err != nil { @@ -101,74 +100,113 @@ func (o *objectGoMapReflect) toValue(val Value, throw bool) (reflect.Value, bool return v, true } -func (o *objectGoMapReflect) put(key, val Value, throw bool) { - k := o.toKey(key, throw) - v, ok := o.toValue(val, throw) - if !ok { - return +func (o *objectGoMapReflect) _put(key reflect.Value, val Value, throw bool) bool { + if key.IsValid() { + if o.extensible || o.value.MapIndex(key).IsValid() { + v, ok := o.toValue(val, throw) + if !ok { + return false + } + o.value.SetMapIndex(key, v) + } else { + o.val.runtime.typeErrorResult(throw, "Cannot set property %s, object is not extensible", key.String()) + return false + } + return true } - o.value.SetMapIndex(k, v) + return false } -func (o *objectGoMapReflect) putStr(name string, val Value, throw bool) { - k := o.strToKey(name, throw) - if !k.IsValid() { - return - } - v, ok := o.toValue(val, throw) - if !ok { - return +func (o *objectGoMapReflect) setOwnStr(name string, val Value, throw bool) bool { + key := o.strToKey(name, false) + if !key.IsValid() || !o.value.MapIndex(key).IsValid() { + if proto := o.prototype; proto != nil { + // we know it's foreign because prototype loops are not allowed + if res, ok := proto.self.setForeignStr(name, val, o.val, throw); ok { + return res + } + } + // new property + if !o.extensible { + o.val.runtime.typeErrorResult(throw, "Cannot add property %s, object is not extensible", name) + return false + } else { + if throw && !key.IsValid() { + o.strToKey(name, true) + return false + } + } } - o.value.SetMapIndex(k, v) + o._put(key, val, throw) + return true } -func (o *objectGoMapReflect) _putProp(name string, value Value, writable, enumerable, configurable bool) Value { - o.putStr(name, value, true) - return value +func (o *objectGoMapReflect) setOwnIdx(idx valueInt, val Value, throw bool) bool { + key := o.toKey(idx, false) + if !key.IsValid() || !o.value.MapIndex(key).IsValid() { + if proto := o.prototype; proto != nil { + // we know it's foreign because prototype loops are not allowed + if res, ok := proto.self.setForeignIdx(idx, val, o.val, throw); ok { + return res + } + } + // new property + if !o.extensible { + o.val.runtime.typeErrorResult(throw, "Cannot add property %d, object is not extensible", idx) + return false + } else { + if throw && !key.IsValid() { + o.toKey(idx, true) + return false + } + } + } + o._put(key, val, throw) + return true } -func (o *objectGoMapReflect) defineOwnProperty(n Value, descr propertyDescr, throw bool) bool { - if !o.val.runtime.checkHostObjectPropertyDescr(n, descr, throw) { - return false - } +func (o *objectGoMapReflect) setForeignStr(name string, val, receiver Value, throw bool) (bool, bool) { + return o._setForeignStr(name, trueValIfPresent(o.hasOwnPropertyStr(name)), val, receiver, throw) +} - o.put(n, descr.Value, throw) - return true +func (o *objectGoMapReflect) setForeignIdx(idx valueInt, val, receiver Value, throw bool) (bool, bool) { + return o._setForeignIdx(idx, trueValIfPresent(o.hasOwnPropertyIdx(idx)), val, receiver, throw) } -func (o *objectGoMapReflect) hasOwnPropertyStr(name string) bool { - key := o.strToKey(name, false) - if !key.IsValid() { +func (o *objectGoMapReflect) defineOwnPropertyStr(name string, descr PropertyDescriptor, throw bool) bool { + if !o.val.runtime.checkHostObjectPropertyDescr(name, descr, throw) { return false } - return o.value.MapIndex(key).IsValid() + + return o._put(o.strToKey(name, throw), descr.Value, throw) } -func (o *objectGoMapReflect) hasOwnProperty(n Value) bool { - key := o.toKey(n, false) - if !key.IsValid() { +func (o *objectGoMapReflect) defineOwnPropertyIdx(idx valueInt, descr PropertyDescriptor, throw bool) bool { + if !o.val.runtime.checkHostObjectPropertyDescr(idx.String(), descr, throw) { return false } - return o.value.MapIndex(key).IsValid() + return o._put(o.toKey(idx, throw), descr.Value, throw) } -func (o *objectGoMapReflect) hasProperty(n Value) bool { - if o.hasOwnProperty(n) { +func (o *objectGoMapReflect) hasOwnPropertyStr(name string) bool { + key := o.strToKey(name, false) + if key.IsValid() && o.value.MapIndex(key).IsValid() { return true } - return o.objectGoReflect.hasProperty(n) + return false } -func (o *objectGoMapReflect) hasPropertyStr(name string) bool { - if o.hasOwnPropertyStr(name) { +func (o *objectGoMapReflect) hasOwnPropertyIdx(idx valueInt) bool { + key := o.toKey(idx, false) + if key.IsValid() && o.value.MapIndex(key).IsValid() { return true } - return o.objectGoReflect.hasPropertyStr(name) + return false } -func (o *objectGoMapReflect) delete(n Value, throw bool) bool { - key := o.toKey(n, throw) +func (o *objectGoMapReflect) deleteStr(name string, throw bool) bool { + key := o.strToKey(name, throw) if !key.IsValid() { return false } @@ -176,8 +214,8 @@ func (o *objectGoMapReflect) delete(n Value, throw bool) bool { return true } -func (o *objectGoMapReflect) deleteStr(name string, throw bool) bool { - key := o.strToKey(name, throw) +func (o *objectGoMapReflect) deleteIdx(idx valueInt, throw bool) bool { + key := o.toKey(idx, throw) if !key.IsValid() { return false } @@ -186,10 +224,9 @@ func (o *objectGoMapReflect) deleteStr(name string, throw bool) bool { } type gomapReflectPropIter struct { - o *objectGoMapReflect - keys []reflect.Value - idx int - recursive bool + o *objectGoMapReflect + keys []reflect.Value + idx int } func (i *gomapReflectPropIter) next() (propIterItem, iterNextFunc) { @@ -202,28 +239,26 @@ func (i *gomapReflectPropIter) next() (propIterItem, iterNextFunc) { } } - if i.recursive { - return i.o.objectGoReflect._enumerate(true)() + if i.o.prototype != nil { + return i.o.prototype.self.enumerateUnfiltered()() } - return propIterItem{}, nil } -func (o *objectGoMapReflect) _enumerate(recusrive bool) iterNextFunc { - r := &gomapReflectPropIter{ - o: o, - keys: o.value.MapKeys(), - recursive: recusrive, - } - return r.next +func (o *objectGoMapReflect) enumerateUnfiltered() iterNextFunc { + return (&gomapReflectPropIter{ + o: o, + keys: o.value.MapKeys(), + }).next } -func (o *objectGoMapReflect) enumerate(all, recursive bool) iterNextFunc { - return (&propFilterIter{ - wrapped: o._enumerate(recursive), - all: all, - seen: make(map[string]bool), - }).next +func (o *objectGoMapReflect) ownKeys(_ bool, accum []Value) []Value { + // all own keys are enumerable + for _, key := range o.value.MapKeys() { + accum = append(accum, newStringValue(key.String())) + } + + return accum } func (o *objectGoMapReflect) equal(other objectImpl) bool { diff --git a/object_gomap_reflect_test.go b/object_gomap_reflect_test.go index cb81ce06..9c07bf3c 100644 --- a/object_gomap_reflect_test.go +++ b/object_gomap_reflect_test.go @@ -1,6 +1,8 @@ package goja -import "testing" +import ( + "testing" +) func TestGoMapReflectGetSet(t *testing.T) { const SCRIPT = ` @@ -164,3 +166,86 @@ func TestGoMapReflectWithMethods(t *testing.T) { } } + +func TestGoMapReflectWithProto(t *testing.T) { + vm := New() + m := map[string]string{ + "t": "42", + } + vm.Set("m", m) + _, err := vm.RunString(TESTLIB + ` + (function() { + 'use strict'; + var proto = {}; + var getterAllowed = false; + var setterAllowed = false; + var tHolder = "proto t"; + Object.defineProperty(proto, "t", { + get: function() { + if (!getterAllowed) throw new Error("getter is called"); + return tHolder; + }, + set: function(v) { + if (!setterAllowed) throw new Error("setter is called"); + tHolder = v; + } + }); + var t1Holder; + Object.defineProperty(proto, "t1", { + get: function() { + return t1Holder; + }, + set: function(v) { + t1Holder = v; + } + }); + Object.setPrototypeOf(m, proto); + assert.sameValue(m.t, "42"); + m.t = 43; + assert.sameValue(m.t, "43"); + t1Holder = "test"; + assert.sameValue(m.t1, "test"); + m.t1 = "test1"; + assert.sameValue(m.t1, "test1"); + delete m.t; + getterAllowed = true; + assert.sameValue(m.t, "proto t", "after delete"); + setterAllowed = true; + m.t = true; + assert.sameValue(m.t, true, "m.t === true"); + assert.sameValue(tHolder, true, "tHolder === true"); + Object.preventExtensions(m); + assert.throws(TypeError, function() { + m.t2 = 1; + }); + m.t1 = "test2"; + assert.sameValue(m.t1, "test2"); + })(); + `) + if err != nil { + t.Fatal(err) + } +} + +func TestGoMapReflectProtoProp(t *testing.T) { + const SCRIPT = ` + (function() { + "use strict"; + var proto = {}; + Object.defineProperty(proto, "ro", {value: 42}); + Object.setPrototypeOf(m, proto); + assert.throws(TypeError, function() { + m.ro = 43; + }); + Object.defineProperty(m, "ro", {value: 43}); + assert.sameValue(m.ro, "43"); + })(); + ` + + r := New() + r.Set("m", map[string]string{}) + _, err := r.RunString(TESTLIB + SCRIPT) + if err != nil { + t.Fatal(err) + } +} diff --git a/object_gomap_test.go b/object_gomap_test.go index 730bab1c..0e966e45 100644 --- a/object_gomap_test.go +++ b/object_gomap_test.go @@ -182,3 +182,127 @@ func TestGoMapExtensibility(t *testing.T) { } } + +func TestGoMapWithProto(t *testing.T) { + vm := New() + m := map[string]interface{}{ + "t": "42", + } + vm.Set("m", m) + _, err := vm.RunString(TESTLIB + ` + (function() { + 'use strict'; + var proto = {}; + var getterAllowed = false; + var setterAllowed = false; + var tHolder = "proto t"; + Object.defineProperty(proto, "t", { + get: function() { + if (!getterAllowed) throw new Error("getter is called"); + return tHolder; + }, + set: function(v) { + if (!setterAllowed) throw new Error("setter is called"); + tHolder = v; + } + }); + var t1Holder; + Object.defineProperty(proto, "t1", { + get: function() { + return t1Holder; + }, + set: function(v) { + t1Holder = v; + } + }); + Object.setPrototypeOf(m, proto); + assert.sameValue(m.t, "42"); + m.t = 43; + assert.sameValue(m.t, 43); + t1Holder = "test"; + assert.sameValue(m.t1, "test"); + m.t1 = "test1"; + assert.sameValue(m.t1, "test1"); + delete m.t; + getterAllowed = true; + assert.sameValue(m.t, "proto t", "after delete"); + setterAllowed = true; + m.t = true; + assert.sameValue(m.t, true); + assert.sameValue(tHolder, true); + Object.preventExtensions(m); + assert.throws(TypeError, function() { + m.t2 = 1; + }); + m.t1 = "test2"; + assert.sameValue(m.t1, "test2"); + })(); + `) + if err != nil { + t.Fatal(err) + } +} + +func TestGoMapProtoProp(t *testing.T) { + const SCRIPT = ` + (function() { + "use strict"; + var proto = {}; + Object.defineProperty(proto, "ro", {value: 42}); + Object.setPrototypeOf(m, proto); + assert.throws(TypeError, function() { + m.ro = 43; + }); + Object.defineProperty(m, "ro", {value: 43}); + assert.sameValue(m.ro, 43); + })(); + ` + + r := New() + r.Set("m", map[string]interface{}{}) + _, err := r.RunString(TESTLIB + SCRIPT) + if err != nil { + t.Fatal(err) + } +} + +func TestGoMapProtoPropChain(t *testing.T) { + const SCRIPT = ` + (function() { + "use strict"; + var p1 = Object.create(null); + m.__proto__ = p1; + + Object.defineProperty(p1, "test", { + value: 42 + }); + + Object.defineProperty(m, "test", { + value: 43, + writable: true, + }); + var o = Object.create(m); + o.test = 44; + assert.sameValue(o.test, 44); + + var sym = Symbol(true); + Object.defineProperty(p1, sym, { + value: 42 + }); + + Object.defineProperty(m, sym, { + value: 43, + writable: true, + }); + o[sym] = 44; + assert.sameValue(o[sym], 44); + })(); + ` + + r := New() + r.Set("m", map[string]interface{}{}) + _, err := r.RunString(TESTLIB + SCRIPT) + if err != nil { + t.Fatal(err) + } +} diff --git a/object_goreflect.go b/object_goreflect.go index 9b85d3a6..f5cf9d26 100644 --- a/object_goreflect.go +++ b/object_goreflect.go @@ -62,6 +62,7 @@ func (o *objectGoReflect) init() { o.class = classObject o.prototype = o.val.runtime.global.ObjectPrototype } + o.extensible = true o.baseObject._putProp("toString", o.val.runtime.newNativeFunc(o.toStringFunc, nil, "toString", nil, 0), true, false, true) o.baseObject._putProp("valueOf", o.val.runtime.newNativeFunc(o.valueOfFunc, nil, "valueOf", nil, 0), true, false, true) @@ -74,16 +75,19 @@ func (o *objectGoReflect) init() { } } -func (o *objectGoReflect) toStringFunc(call FunctionCall) Value { +func (o *objectGoReflect) toStringFunc(FunctionCall) Value { return o.toPrimitiveString() } -func (o *objectGoReflect) valueOfFunc(call FunctionCall) Value { +func (o *objectGoReflect) valueOfFunc(FunctionCall) Value { return o.toPrimitive() } -func (o *objectGoReflect) get(n Value) Value { - return o.getStr(n.String()) +func (o *objectGoReflect) getStr(name string, receiver Value) Value { + if v := o._get(name); v != nil { + return v + } + return o.baseObject.getStr(name, receiver) } func (o *objectGoReflect) _getField(jsName string) reflect.Value { @@ -103,10 +107,17 @@ func (o *objectGoReflect) _getMethod(jsName string) reflect.Value { return reflect.Value{} } +func (o *objectGoReflect) getAddr(v reflect.Value) reflect.Value { + if (v.Kind() == reflect.Struct || v.Kind() == reflect.Slice) && v.CanAddr() { + return v.Addr() + } + return v +} + func (o *objectGoReflect) _get(name string) Value { if o.value.Kind() == reflect.Struct { if v := o._getField(name); v.IsValid() { - return o.val.runtime.ToValue(v.Interface()) + return o.val.runtime.ToValue(o.getAddr(v).Interface()) } } @@ -117,30 +128,12 @@ func (o *objectGoReflect) _get(name string) Value { return nil } -func (o *objectGoReflect) getStr(name string) Value { - if v := o._get(name); v != nil { - return v - } - return o.baseObject._getStr(name) -} - -func (o *objectGoReflect) getPropStr(name string) Value { - if v := o.getOwnPropStr(name); v != nil { - return v - } - return o.baseObject.getPropStr(name) -} - func (o *objectGoReflect) getOwnPropStr(name string) Value { if o.value.Kind() == reflect.Struct { if v := o._getField(name); v.IsValid() { - canSet := v.CanSet() - if (v.Kind() == reflect.Struct || v.Kind() == reflect.Slice) && v.CanAddr() { - v = v.Addr() - } return &valueProperty{ - value: o.val.runtime.ToValue(v.Interface()), - writable: canSet, + value: o.val.runtime.ToValue(o.getAddr(v).Interface()), + writable: v.CanSet(), enumerable: true, } } @@ -156,90 +149,75 @@ func (o *objectGoReflect) getOwnPropStr(name string) Value { return nil } -func (o *objectGoReflect) put(n Value, val Value, throw bool) { - if _, ok := n.(*valueSymbol); ok { - o.val.runtime.typeErrorResult(throw, "Cannot assign to Symbol property %s of a host object", n.String()) - return +func (o *objectGoReflect) setOwnStr(name string, val Value, throw bool) bool { + has, ok := o._put(name, val, throw) + if !has { + if res, ok := o._setForeignStr(name, nil, val, o.val, throw); !ok { + o.val.runtime.typeErrorResult(throw, "Cannot assign to property %s of a host object", name) + return false + } else { + return res + } } - o.putStr(n.String(), val, throw) + return ok } -func (o *objectGoReflect) putStr(name string, val Value, throw bool) { - if !o._put(name, val, throw) { - o.val.runtime.typeErrorResult(throw, "Cannot assign to property %s of a host object", name) - } +func (o *objectGoReflect) setForeignStr(name string, val, receiver Value, throw bool) (bool, bool) { + return o._setForeignStr(name, trueValIfPresent(o._has(name)), val, receiver, throw) } -func (o *objectGoReflect) _put(name string, val Value, throw bool) bool { +func (o *objectGoReflect) _put(name string, val Value, throw bool) (has, ok bool) { if o.value.Kind() == reflect.Struct { if v := o._getField(name); v.IsValid() { if !v.CanSet() { o.val.runtime.typeErrorResult(throw, "Cannot assign to a non-addressable or read-only property %s of a host object", name) - return false + return true, false } vv, err := o.val.runtime.toReflectValue(val, v.Type()) if err != nil { o.val.runtime.typeErrorResult(throw, "Go struct conversion error: %v", err) - return false + return true, false } v.Set(vv) - return true + return true, true } } - return false + return false, false } func (o *objectGoReflect) _putProp(name string, value Value, writable, enumerable, configurable bool) Value { - if o._put(name, value, false) { + if _, ok := o._put(name, value, false); ok { return value } return o.baseObject._putProp(name, value, writable, enumerable, configurable) } -func (r *Runtime) checkHostObjectPropertyDescr(n Value, descr propertyDescr, throw bool) bool { - if _, ok := n.(*valueSymbol); ok { - r.typeErrorResult(throw, "Host objects do not support symbol properties") - return false - } +func (r *Runtime) checkHostObjectPropertyDescr(name string, descr PropertyDescriptor, throw bool) bool { if descr.Getter != nil || descr.Setter != nil { r.typeErrorResult(throw, "Host objects do not support accessor properties") return false } if descr.Writable == FLAG_FALSE { - r.typeErrorResult(throw, "Host object field %s cannot be made read-only", n.String()) + r.typeErrorResult(throw, "Host object field %s cannot be made read-only", name) return false } if descr.Configurable == FLAG_TRUE { - r.typeErrorResult(throw, "Host object field %s cannot be made configurable", n.String()) + r.typeErrorResult(throw, "Host object field %s cannot be made configurable", name) return false } return true } -func (o *objectGoReflect) defineOwnProperty(n Value, descr propertyDescr, throw bool) bool { - if _, ok := n.(*valueSymbol); !ok { - if o.value.Kind() == reflect.Struct { - name := n.String() - if v := o._getField(name); v.IsValid() { - if !o.val.runtime.checkHostObjectPropertyDescr(n, descr, throw) { - return false - } - val := descr.Value - if val == nil { - val = _undefined - } - vv, err := o.val.runtime.toReflectValue(val, v.Type()) - if err != nil { - o.val.runtime.typeErrorResult(throw, "Go struct conversion error: %v", err) - return false - } - v.Set(vv) - return true - } +func (o *objectGoReflect) defineOwnPropertyStr(name string, descr PropertyDescriptor, throw bool) bool { + if o.val.runtime.checkHostObjectPropertyDescr(name, descr, throw) { + if has, ok := o._put(name, descr.Value, throw); !has { + o.val.runtime.typeErrorResult(throw, "Cannot define property '%s' on a host object", name) + return false + } else { + return ok } } - - return o.baseObject.defineOwnProperty(n, descr, throw) + return false } func (o *objectGoReflect) _has(name string) bool { @@ -254,25 +232,6 @@ func (o *objectGoReflect) _has(name string) bool { return false } -func (o *objectGoReflect) hasProperty(n Value) bool { - name := n.String() - if o._has(name) { - return true - } - return o.baseObject.hasProperty(n) -} - -func (o *objectGoReflect) hasPropertyStr(name string) bool { - if o._has(name) { - return true - } - return o.baseObject.hasPropertyStr(name) -} - -func (o *objectGoReflect) hasOwnProperty(n Value) bool { - return o._has(n.String()) -} - func (o *objectGoReflect) hasOwnPropertyStr(name string) bool { return o._has(name) } @@ -342,14 +301,9 @@ func (o *objectGoReflect) deleteStr(name string, throw bool) bool { return o.baseObject.deleteStr(name, throw) } -func (o *objectGoReflect) delete(name Value, throw bool) bool { - return o.deleteStr(name.String(), throw) -} - type goreflectPropIter struct { - o *objectGoReflect - idx int - recursive bool + o *objectGoReflect + idx int } func (i *goreflectPropIter) nextField() (propIterItem, iterNextFunc) { @@ -372,30 +326,34 @@ func (i *goreflectPropIter) nextMethod() (propIterItem, iterNextFunc) { return propIterItem{name: name, enumerable: _ENUM_TRUE}, i.nextMethod } - if i.recursive { - return i.o.baseObject._enumerate(true)() - } - return propIterItem{}, nil } -func (o *objectGoReflect) _enumerate(recursive bool) iterNextFunc { +func (o *objectGoReflect) enumerateUnfiltered() iterNextFunc { r := &goreflectPropIter{ - o: o, - recursive: recursive, + o: o, } + var next iterNextFunc if o.value.Kind() == reflect.Struct { - return r.nextField + next = r.nextField + } else { + next = r.nextMethod } - return r.nextMethod + + return o.recursiveIter(next) } -func (o *objectGoReflect) enumerate(all, recursive bool) iterNextFunc { - return (&propFilterIter{ - wrapped: o._enumerate(recursive), - all: all, - seen: make(map[string]bool), - }).next +func (o *objectGoReflect) ownKeys(_ bool, accum []Value) []Value { + // all own keys are enumerable + for _, name := range o.valueTypeInfo.FieldNames { + accum = append(accum, newStringValue(name)) + } + + for _, name := range o.valueTypeInfo.MethodNames { + accum = append(accum, newStringValue(name)) + } + + return accum } func (o *objectGoReflect) export() interface{} { diff --git a/object_goreflect_test.go b/object_goreflect_test.go index c0df1afb..94fbf10e 100644 --- a/object_goreflect_test.go +++ b/object_goreflect_test.go @@ -23,7 +23,7 @@ func TestGoReflectGet(t *testing.T) { t.Fatal(err) } - if s, ok := v.assertString(); ok { + if s, ok := v.(valueString); ok { if s.String() != "42" { t.Fatalf("Unexpected string: %s", s) } @@ -487,14 +487,14 @@ func TestGoReflectEmbeddedStruct(t *testing.T) { type jsonTagNamer struct{} -func (jsonTagNamer) FieldName(t reflect.Type, field reflect.StructField) string { +func (jsonTagNamer) FieldName(_ reflect.Type, field reflect.StructField) string { if jsonTag := field.Tag.Get("json"); jsonTag != "" { return jsonTag } return field.Name } -func (jsonTagNamer) MethodName(t reflect.Type, method reflect.Method) string { +func (jsonTagNamer) MethodName(_ reflect.Type, method reflect.Method) string { return method.Name } @@ -588,11 +588,11 @@ func TestGoReflectCustomObjNaming(t *testing.T) { type fieldNameMapper1 struct{} -func (fieldNameMapper1) FieldName(t reflect.Type, f reflect.StructField) string { +func (fieldNameMapper1) FieldName(_ reflect.Type, f reflect.StructField) string { return strings.ToLower(f.Name) } -func (fieldNameMapper1) MethodName(t reflect.Type, m reflect.Method) string { +func (fieldNameMapper1) MethodName(_ reflect.Type, m reflect.Method) string { return m.Name } @@ -672,7 +672,7 @@ func TestStructNonAddressable(t *testing.T) { type testFieldMapper struct { } -func (testFieldMapper) FieldName(t reflect.Type, f reflect.StructField) string { +func (testFieldMapper) FieldName(_ reflect.Type, f reflect.StructField) string { if tag := f.Tag.Get("js"); tag != "" { if tag == "-" { return "" @@ -683,7 +683,7 @@ func (testFieldMapper) FieldName(t reflect.Type, f reflect.StructField) string { return f.Name } -func (testFieldMapper) MethodName(t reflect.Type, m reflect.Method) string { +func (testFieldMapper) MethodName(_ reflect.Type, m reflect.Method) string { return m.Name } @@ -792,7 +792,7 @@ func TestDefinePropertyUnexportedJsName(t *testing.T) { throw new Error("Unexpected value: " + f.field); } if (f.hasOwnProperty("unexported")) { - throw new Error("hasOwnProporty('unexported') is true"); + throw new Error("hasOwnProperty('unexported') is true"); } var thrown; try { @@ -811,11 +811,11 @@ func TestDefinePropertyUnexportedJsName(t *testing.T) { type fieldNameMapperToLower struct{} -func (fieldNameMapperToLower) FieldName(t reflect.Type, f reflect.StructField) string { +func (fieldNameMapperToLower) FieldName(_ reflect.Type, f reflect.StructField) string { return strings.ToLower(f.Name) } -func (fieldNameMapperToLower) MethodName(t reflect.Type, m reflect.Method) string { +func (fieldNameMapperToLower) MethodName(_ reflect.Type, m reflect.Method) string { return strings.ToLower(m.Name) } @@ -950,3 +950,100 @@ func TestStructNonAddressableAnonStruct(t *testing.T) { } } + +func TestGoReflectWithProto(t *testing.T) { + type S struct { + Field int + } + var s S + vm := New() + vm.Set("s", &s) + _, err := vm.RunString(TESTLIB + ` + (function() { + 'use strict'; + var proto = { + Field: "protoField", + test: 42 + }; + var test1Holder; + Object.defineProperty(proto, "test1", { + set: function(v) { + test1Holder = v; + }, + get: function() { + return test1Holder; + } + }); + Object.setPrototypeOf(s, proto); + assert.sameValue(s.Field, 0, "s.Field"); + s.Field = 2; + assert.sameValue(s.Field, 2, "s.Field"); + assert.sameValue(s.test, 42, "s.test"); + assert.throws(TypeError, function() { + Object.defineProperty(s, "test", {value: 43}); + }); + test1Holder = 1; + assert.sameValue(s.test1, 1, "s.test1"); + s.test1 = 2; + assert.sameValue(test1Holder, 2, "test1Holder"); + })(); + `) + if err != nil { + t.Fatal(err) + } +} + +func TestGoReflectSymbols(t *testing.T) { + type S struct { + Field int + } + var s S + vm := New() + vm.Set("s", &s) + _, err := vm.RunString(` + 'use strict'; + var sym = Symbol(66); + s[sym] = "Test"; + if (s[sym] !== "Test") { + throw new Error("s[sym]=" + s[sym]); + } + `) + if err != nil { + t.Fatal(err) + } +} + +func TestGoObj__Proto__(t *testing.T) { + type S struct { + Field int + } + vm := New() + vm.Set("s", S{}) + vm.Set("m", map[string]interface{}{}) + vm.Set("mr", map[int]string{}) + vm.Set("a", []interface{}{}) + vm.Set("ar", []string{}) + _, err := vm.RunString(` + function f(s, expectedCtor, prefix) { + if (s.__proto__ !== expectedCtor.prototype) { + throw new Error(prefix + ": __proto__: " + s.__proto__); + } + s.__proto__ = null; + if (s.__proto__ !== undefined) { // as there is no longer a prototype, there is no longer the __proto__ property + throw new Error(prefix + ": __proto__ is not undefined: " + s.__proto__); + } + var proto = Object.getPrototypeOf(s); + if (proto !== null) { + throw new Error(prefix + ": proto is not null: " + proto); + } + } + f(s, Object, "struct"); + f(m, Object, "simple map"); + f(mr, Object, "reflect map"); + f(a, Array, "slice"); + f(ar, Array, "reflect slice"); + `) + if err != nil { + t.Fatal(err) + } +} diff --git a/object_goslice.go b/object_goslice.go index c13ea040..48822a0c 100644 --- a/object_goslice.go +++ b/object_goslice.go @@ -1,6 +1,8 @@ package goja import ( + "math" + "math/bits" "reflect" "strconv" ) @@ -17,76 +19,73 @@ func (o *objectGoSlice) init() { o.class = classArray o.prototype = o.val.runtime.global.ArrayPrototype o.lengthProp.writable = o.sliceExtensible - o._setLen() + o.extensible = true + o.updateLen() o.baseObject._put("length", &o.lengthProp) } -func (o *objectGoSlice) _setLen() { +func (o *objectGoSlice) updateLen() { o.lengthProp.value = intToValue(int64(len(*o.data))) } -func (o *objectGoSlice) getIdx(idx int64) Value { - if idx < int64(len(*o.data)) { - return o.val.runtime.ToValue((*o.data)[idx]) +func (o *objectGoSlice) getStr(name string, receiver Value) Value { + var ownProp Value + if idx := strToGoIdx(name); idx >= 0 && idx < len(*o.data) { + v := (*o.data)[idx] + ownProp = o.val.runtime.ToValue(v) + } else if name == "length" { + ownProp = &o.lengthProp } - return nil -} -func (o *objectGoSlice) _get(n Value) Value { - if idx := toIdx(n); idx >= 0 { - return o.getIdx(idx) - } - return nil + return o.getStrWithOwnProp(ownProp, name, receiver) } -func (o *objectGoSlice) _getStr(name string) Value { - if idx := strToIdx(name); idx >= 0 { - return o.getIdx(idx) +func (o *objectGoSlice) getIdx(idx valueInt, receiver Value) Value { + if idx := int64(idx); idx >= 0 && idx < int64(len(*o.data)) { + v := (*o.data)[idx] + return o.val.runtime.ToValue(v) } - return nil -} - -func (o *objectGoSlice) get(n Value) Value { - if v := o._get(n); v != nil { - return v - } - return o.baseObject._getStr(n.String()) -} - -func (o *objectGoSlice) getStr(name string) Value { - if v := o._getStr(name); v != nil { - return v + if o.prototype != nil { + if receiver == nil { + return o.prototype.self.getIdx(idx, o.val) + } + return o.prototype.self.getIdx(idx, receiver) } - return o.baseObject._getStr(name) + return nil } -func (o *objectGoSlice) getProp(n Value) Value { - if v := o._get(n); v != nil { - return v +func (o *objectGoSlice) getOwnPropStr(name string) Value { + if idx := strToGoIdx(name); idx >= 0 { + if idx < len(*o.data) { + v := o.val.runtime.ToValue((*o.data)[idx]) + return &valueProperty{ + value: v, + writable: true, + enumerable: true, + } + } + return nil } - return o.baseObject.getPropStr(n.String()) -} - -func (o *objectGoSlice) getPropStr(name string) Value { - if v := o._getStr(name); v != nil { - return v + if name == "length" { + return &o.lengthProp } - return o.baseObject.getPropStr(name) + return nil } -func (o *objectGoSlice) getOwnPropStr(name string) Value { - if v := o._getStr(name); v != nil { +func (o *objectGoSlice) getOwnPropIdx(idx valueInt) Value { + if idx := int64(idx); idx >= 0 && idx < int64(len(*o.data)) { + v := o.val.runtime.ToValue((*o.data)[idx]) return &valueProperty{ value: v, writable: true, enumerable: true, } } - return o.baseObject.getOwnPropStr(name) + return nil } -func (o *objectGoSlice) grow(size int64) { - newcap := int64(cap(*o.data)) +func (o *objectGoSlice) grow(size int) { + newcap := cap(*o.data) if newcap < size { // Use the same algorithm as in runtime.growSlice doublecap := newcap + newcap @@ -106,13 +105,26 @@ func (o *objectGoSlice) grow(size int64) { copy(n, *o.data) *o.data = n } else { + tail := (*o.data)[len(*o.data):size] + for k := range tail { + tail[k] = nil + } *o.data = (*o.data)[:size] } - o._setLen() + o.updateLen() } -func (o *objectGoSlice) putIdx(idx int64, v Value, throw bool) { - if idx >= int64(len(*o.data)) { +func (o *objectGoSlice) shrink(size int) { + tail := (*o.data)[size:] + for k := range tail { + tail[k] = nil + } + *o.data = (*o.data)[:size] + o.updateLen() +} + +func (o *objectGoSlice) putIdx(idx int, v Value, throw bool) { + if idx >= len(*o.data) { if !o.sliceExtensible { o.val.runtime.typeErrorResult(throw, "Cannot extend Go slice") return @@ -122,74 +134,118 @@ func (o *objectGoSlice) putIdx(idx int64, v Value, throw bool) { (*o.data)[idx] = v.Export() } -func (o *objectGoSlice) put(n Value, val Value, throw bool) { - if idx := toIdx(n); idx >= 0 { - o.putIdx(idx, val, throw) - return +func toInt(i int64) int { + if bits.UintSize == 64 { + return int(i) + } + if i >= math.MaxInt32 { + panic(typeError("Integer value overflows 32-bit int")) } - // TODO: length - o.baseObject.put(n, val, throw) + return int(i) } -func (o *objectGoSlice) putStr(name string, val Value, throw bool) { - if idx := strToIdx(name); idx >= 0 { - o.putIdx(idx, val, throw) - return +func (o *objectGoSlice) putLength(v Value, throw bool) bool { + newLen := toInt(toLength(v)) + curLen := len(*o.data) + if newLen > curLen { + if !o.sliceExtensible { + o.val.runtime.typeErrorResult(throw, "Cannot extend Go slice") + return false + } + o.grow(newLen) + } else if newLen < curLen { + if !o.sliceExtensible { + o.val.runtime.typeErrorResult(throw, "Cannot shrink Go slice") + return false + } + o.shrink(newLen) } - // TODO: length - o.baseObject.putStr(name, val, throw) + return true } -func (o *objectGoSlice) _has(n Value) bool { - if idx := toIdx(n); idx >= 0 { - return idx < int64(len(*o.data)) +func (o *objectGoSlice) setOwnIdx(idx valueInt, val Value, throw bool) bool { + if i := toInt(int64(idx)); i >= 0 { + if i >= len(*o.data) { + if res, ok := o._setForeignIdx(idx, nil, val, o.val, throw); ok { + return res + } + } + o.putIdx(i, val, throw) + } else { + name := idx.String() + if res, ok := o._setForeignStr(name, nil, val, o.val, throw); !ok { + o.val.runtime.typeErrorResult(throw, "Can't set property '%s' on Go slice", name) + return false + } else { + return res + } } - return false + return true } -func (o *objectGoSlice) _hasStr(name string) bool { - if idx := strToIdx(name); idx >= 0 { - return idx < int64(len(*o.data)) +func (o *objectGoSlice) setOwnStr(name string, val Value, throw bool) bool { + if idx := strToGoIdx(name); idx >= 0 { + if idx >= len(*o.data) { + if res, ok := o._setForeignStr(name, nil, val, o.val, throw); ok { + return res + } + } + o.putIdx(idx, val, throw) + } else { + if name == "length" { + return o.putLength(val, throw) + } + if res, ok := o._setForeignStr(name, nil, val, o.val, throw); !ok { + o.val.runtime.typeErrorResult(throw, "Can't set property '%s' on Go slice", name) + return false + } else { + return res + } } - return false + return true } -func (o *objectGoSlice) hasProperty(n Value) bool { - if o._has(n) { - return true - } - return o.baseObject.hasProperty(n) +func (o *objectGoSlice) setForeignIdx(idx valueInt, val, receiver Value, throw bool) (bool, bool) { + return o._setForeignIdx(idx, trueValIfPresent(o.hasOwnPropertyIdx(idx)), val, receiver, throw) } -func (o *objectGoSlice) hasPropertyStr(name string) bool { - if o._hasStr(name) { - return true - } - return o.baseObject.hasPropertyStr(name) +func (o *objectGoSlice) setForeignStr(name string, val, receiver Value, throw bool) (bool, bool) { + return o._setForeignStr(name, trueValIfPresent(o.hasOwnPropertyStr(name)), val, receiver, throw) } -func (o *objectGoSlice) hasOwnProperty(n Value) bool { - if o._has(n) { - return true +func (o *objectGoSlice) hasOwnPropertyIdx(idx valueInt) bool { + if idx := int64(idx); idx >= 0 { + return idx < int64(len(*o.data)) } - return o.baseObject.hasOwnProperty(n) + return false } func (o *objectGoSlice) hasOwnPropertyStr(name string) bool { - if o._hasStr(name) { - return true + if idx := strToIdx64(name); idx >= 0 { + return idx < int64(len(*o.data)) } - return o.baseObject.hasOwnPropertyStr(name) + return false } -func (o *objectGoSlice) _putProp(name string, value Value, writable, enumerable, configurable bool) Value { - o.putStr(name, value, false) - return value +func (o *objectGoSlice) defineOwnPropertyIdx(idx valueInt, descr PropertyDescriptor, throw bool) bool { + if i := toInt(int64(idx)); i >= 0 { + if !o.val.runtime.checkHostObjectPropertyDescr(idx.String(), descr, throw) { + return false + } + val := descr.Value + if val == nil { + val = _undefined + } + o.putIdx(i, val, throw) + return true + } + o.val.runtime.typeErrorResult(throw, "Cannot define property '%d' on a Go slice", idx) + return false } -func (o *objectGoSlice) defineOwnProperty(n Value, descr propertyDescr, throw bool) bool { - if idx := toIdx(n); idx >= 0 { - if !o.val.runtime.checkHostObjectPropertyDescr(n, descr, throw) { +func (o *objectGoSlice) defineOwnPropertyStr(name string, descr PropertyDescriptor, throw bool) bool { + if idx := strToGoIdx(name); idx >= 0 { + if !o.val.runtime.checkHostObjectPropertyDescr(name, descr, throw) { return false } val := descr.Value @@ -199,7 +255,11 @@ func (o *objectGoSlice) defineOwnProperty(n Value, descr propertyDescr, throw bo o.putIdx(idx, val, throw) return true } - return o.baseObject.defineOwnProperty(n, descr, throw) + if name == "length" { + return o.val.runtime.defineArrayLength(&o.lengthProp, descr, o.putLength, throw) + } + o.val.runtime.typeErrorResult(throw, "Cannot define property '%s' on a Go slice", name) + return false } func (o *objectGoSlice) toPrimitiveNumber() Value { @@ -217,24 +277,29 @@ func (o *objectGoSlice) toPrimitive() Value { } func (o *objectGoSlice) deleteStr(name string, throw bool) bool { - if idx := strToIdx(name); idx >= 0 && idx < int64(len(*o.data)) { - (*o.data)[idx] = nil + if idx := strToIdx64(name); idx >= 0 { + if idx < int64(len(*o.data)) { + o.val.runtime.typeErrorResult(throw, "Can't delete from Go slice") + return false + } return true } return o.baseObject.deleteStr(name, throw) } -func (o *objectGoSlice) delete(name Value, throw bool) bool { - if idx := toIdx(name); idx >= 0 && idx < int64(len(*o.data)) { - (*o.data)[idx] = nil - return true +func (o *objectGoSlice) deleteIdx(i valueInt, throw bool) bool { + idx := int64(i) + if idx >= 0 { + if idx < int64(len(*o.data)) { + o.val.runtime.typeErrorResult(throw, "Can't delete from Go slice") + return false + } } - return o.baseObject.delete(name, throw) + return true } type goslicePropIter struct { o *objectGoSlice - recursive bool idx, limit int } @@ -245,28 +310,22 @@ func (i *goslicePropIter) next() (propIterItem, iterNextFunc) { return propIterItem{name: name, enumerable: _ENUM_TRUE}, i.next } - if i.recursive { - return i.o.prototype.self._enumerate(i.recursive)() - } - return propIterItem{}, nil } -func (o *objectGoSlice) enumerate(all, recursive bool) iterNextFunc { - return (&propFilterIter{ - wrapped: o._enumerate(recursive), - all: all, - seen: make(map[string]bool), - }).next - +func (o *objectGoSlice) enumerateUnfiltered() iterNextFunc { + return o.recursiveIter((&goslicePropIter{ + o: o, + limit: len(*o.data), + }).next) } -func (o *objectGoSlice) _enumerate(recursive bool) iterNextFunc { - return (&goslicePropIter{ - o: o, - recursive: recursive, - limit: len(*o.data), - }).next +func (o *objectGoSlice) ownKeys(_ bool, accum []Value) []Value { + for i := range *o.data { + accum = append(accum, asciiString(strconv.Itoa(i))) + } + + return accum } func (o *objectGoSlice) export() interface{} { @@ -289,15 +348,15 @@ func (o *objectGoSlice) sortLen() int64 { } func (o *objectGoSlice) sortGet(i int64) Value { - return o.get(intToValue(i)) + return o.getIdx(valueInt(i), nil) } func (o *objectGoSlice) swap(i, j int64) { - ii := intToValue(i) - jj := intToValue(j) - x := o.get(ii) - y := o.get(jj) + ii := valueInt(i) + jj := valueInt(j) + x := o.getIdx(ii, nil) + y := o.getIdx(jj, nil) - o.put(ii, y, false) - o.put(jj, x, false) + o.setOwnIdx(ii, y, false) + o.setOwnIdx(jj, x, false) } diff --git a/object_goslice_reflect.go b/object_goslice_reflect.go index e2593618..f4ff9046 100644 --- a/object_goslice_reflect.go +++ b/object_goslice_reflect.go @@ -17,98 +17,98 @@ func (o *objectGoSliceReflect) init() { o.prototype = o.val.runtime.global.ArrayPrototype o.sliceExtensible = o.value.CanSet() o.lengthProp.writable = o.sliceExtensible - o._setLen() + o.updateLen() o.baseObject._put("length", &o.lengthProp) } -func (o *objectGoSliceReflect) _setLen() { +func (o *objectGoSliceReflect) updateLen() { o.lengthProp.value = intToValue(int64(o.value.Len())) } -func (o *objectGoSliceReflect) _has(n Value) bool { - if idx := toIdx(n); idx >= 0 { - return idx < int64(o.value.Len()) +func (o *objectGoSliceReflect) _hasIdx(idx valueInt) bool { + if idx := int64(idx); idx >= 0 && idx < int64(o.value.Len()) { + return true } return false } func (o *objectGoSliceReflect) _hasStr(name string) bool { - if idx := strToIdx(name); idx >= 0 { - return idx < int64(o.value.Len()) + if idx := strToIdx64(name); idx >= 0 && idx < int64(o.value.Len()) { + return true } return false } -func (o *objectGoSliceReflect) getIdx(idx int64) Value { - if idx < int64(o.value.Len()) { - return o.val.runtime.ToValue(o.value.Index(int(idx)).Interface()) - } - return nil -} - -func (o *objectGoSliceReflect) _get(n Value) Value { - if idx := toIdx(n); idx >= 0 { - return o.getIdx(idx) - } - return nil -} - -func (o *objectGoSliceReflect) _getStr(name string) Value { - if idx := strToIdx(name); idx >= 0 { - return o.getIdx(idx) +func (o *objectGoSliceReflect) _getIdx(idx int) Value { + v := o.value.Index(idx) + if (v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface) && v.IsNil() { + return _null } - return nil + return o.val.runtime.ToValue(v.Interface()) } -func (o *objectGoSliceReflect) get(n Value) Value { - if v := o._get(n); v != nil { - return v +func (o *objectGoSliceReflect) getIdx(idx valueInt, receiver Value) Value { + if idx := toInt(int64(idx)); idx >= 0 && idx < o.value.Len() { + return o._getIdx(idx) } - return o.objectGoReflect.get(n) + return o.objectGoReflect.getStr(idx.String(), receiver) } -func (o *objectGoSliceReflect) getStr(name string) Value { - if v := o._getStr(name); v != nil { - return v +func (o *objectGoSliceReflect) getStr(name string, receiver Value) Value { + var ownProp Value + if idx := strToGoIdx(name); idx >= 0 && idx < o.value.Len() { + ownProp = o._getIdx(idx) + } else if name == "length" { + ownProp = &o.lengthProp + } else { + ownProp = o.objectGoReflect.getOwnPropStr(name) } - return o.objectGoReflect.getStr(name) + return o.getStrWithOwnProp(ownProp, name, receiver) } -func (o *objectGoSliceReflect) getProp(n Value) Value { - if v := o._get(n); v != nil { - return v +func (o *objectGoSliceReflect) getOwnPropStr(name string) Value { + if idx := strToGoIdx(name); idx >= 0 { + if idx < o.value.Len() { + return &valueProperty{ + value: o._getIdx(idx), + writable: true, + enumerable: true, + } + } + return nil } - return o.objectGoReflect.getProp(n) -} - -func (o *objectGoSliceReflect) getPropStr(name string) Value { - if v := o._getStr(name); v != nil { - return v + if name == "length" { + return &o.lengthProp } - return o.objectGoReflect.getPropStr(name) + return o.objectGoReflect.getOwnPropStr(name) } -func (o *objectGoSliceReflect) getOwnPropStr(name string) Value { - if v := o._getStr(name); v != nil { - return v +func (o *objectGoSliceReflect) getOwnPropIdx(idx valueInt) Value { + if idx := toInt(int64(idx)); idx >= 0 && idx < o.value.Len() { + return &valueProperty{ + value: o._getIdx(idx), + writable: true, + enumerable: true, + } } - return o.objectGoReflect.getOwnPropStr(name) + return nil } -func (o *objectGoSliceReflect) putIdx(idx int64, v Value, throw bool) { - if idx >= int64(o.value.Len()) { +func (o *objectGoSliceReflect) putIdx(idx int, v Value, throw bool) bool { + if idx >= o.value.Len() { if !o.sliceExtensible { o.val.runtime.typeErrorResult(throw, "Cannot extend a Go unaddressable reflect slice") - return + return false } - o.grow(int(idx + 1)) + o.grow(idx + 1) } val, err := o.val.runtime.toReflectValue(v, o.value.Type().Elem()) if err != nil { o.val.runtime.typeErrorResult(throw, "Go type conversion error: %v", err) - return + return false } - o.value.Index(int(idx)).Set(val) + o.value.Index(idx).Set(val) + return true } func (o *objectGoSliceReflect) grow(size int) { @@ -132,71 +132,136 @@ func (o *objectGoSliceReflect) grow(size int) { reflect.Copy(n, o.value) o.value.Set(n) } else { + tail := o.value.Slice(o.value.Len(), size) + zero := reflect.Zero(o.value.Type().Elem()) + for i := 0; i < tail.Len(); i++ { + tail.Index(i).Set(zero) + } o.value.SetLen(size) } - o._setLen() + o.updateLen() } -func (o *objectGoSliceReflect) put(n Value, val Value, throw bool) { - if idx := toIdx(n); idx >= 0 { - o.putIdx(idx, val, throw) - return +func (o *objectGoSliceReflect) shrink(size int) { + tail := o.value.Slice(size, o.value.Len()) + zero := reflect.Zero(o.value.Type().Elem()) + for i := 0; i < tail.Len(); i++ { + tail.Index(i).Set(zero) } - // TODO: length - o.objectGoReflect.put(n, val, throw) + o.value.SetLen(size) + o.updateLen() } -func (o *objectGoSliceReflect) putStr(name string, val Value, throw bool) { - if idx := strToIdx(name); idx >= 0 { - o.putIdx(idx, val, throw) - return - } - if name == "length" { - o.baseObject.putStr(name, val, throw) - return +func (o *objectGoSliceReflect) putLength(v Value, throw bool) bool { + newLen := toInt(toLength(v)) + curLen := o.value.Len() + if newLen > curLen { + if !o.sliceExtensible { + o.val.runtime.typeErrorResult(throw, "Cannot extend Go slice") + return false + } + o.grow(newLen) + } else if newLen < curLen { + if !o.sliceExtensible { + o.val.runtime.typeErrorResult(throw, "Cannot shrink Go slice") + return false + } + o.shrink(newLen) } - o.objectGoReflect.putStr(name, val, throw) + return true } -func (o *objectGoSliceReflect) hasProperty(n Value) bool { - if o._has(n) { - return true +func (o *objectGoSliceReflect) setOwnIdx(idx valueInt, val Value, throw bool) bool { + if i := toInt(int64(idx)); i >= 0 { + if i >= o.value.Len() { + if res, ok := o._setForeignIdx(idx, nil, val, o.val, throw); ok { + return res + } + } + o.putIdx(i, val, throw) + } else { + name := idx.String() + if res, ok := o._setForeignStr(name, nil, val, o.val, throw); !ok { + o.val.runtime.typeErrorResult(throw, "Can't set property '%s' on Go slice", name) + return false + } else { + return res + } } - return o.objectGoReflect.hasProperty(n) + return true } -func (o *objectGoSliceReflect) hasPropertyStr(name string) bool { - if o._hasStr(name) { - return true +func (o *objectGoSliceReflect) setOwnStr(name string, val Value, throw bool) bool { + if idx := strToGoIdx(name); idx >= 0 { + if idx >= o.value.Len() { + if res, ok := o._setForeignStr(name, nil, val, o.val, throw); ok { + return res + } + } + o.putIdx(idx, val, throw) + } else { + if name == "length" { + return o.putLength(val, throw) + } + if res, ok := o._setForeignStr(name, nil, val, o.val, throw); !ok { + o.val.runtime.typeErrorResult(throw, "Can't set property '%s' on Go slice", name) + return false + } else { + return res + } } - return o.objectGoReflect.hasOwnPropertyStr(name) + return true } -func (o *objectGoSliceReflect) hasOwnProperty(n Value) bool { - if o._has(n) { - return true - } - return o.objectGoReflect.hasOwnProperty(n) +func (o *objectGoSliceReflect) setForeignIdx(idx valueInt, val, receiver Value, throw bool) (bool, bool) { + return o._setForeignIdx(idx, trueValIfPresent(o._hasIdx(idx)), val, receiver, throw) +} + +func (o *objectGoSliceReflect) setForeignStr(name string, val, receiver Value, throw bool) (bool, bool) { + return o._setForeignStr(name, trueValIfPresent(o._hasStr(name)), val, receiver, throw) +} + +func (o *objectGoSliceReflect) hasOwnPropertyIdx(idx valueInt) bool { + return o._hasIdx(idx) } func (o *objectGoSliceReflect) hasOwnPropertyStr(name string) bool { if o._hasStr(name) { return true } - return o.objectGoReflect.hasOwnPropertyStr(name) + return o.objectGoReflect._has(name) } -func (o *objectGoSliceReflect) _putProp(name string, value Value, writable, enumerable, configurable bool) Value { - o.putStr(name, value, false) - return value +func (o *objectGoSliceReflect) defineOwnPropertyIdx(idx valueInt, descr PropertyDescriptor, throw bool) bool { + if i := toInt(int64(idx)); i >= 0 { + if !o.val.runtime.checkHostObjectPropertyDescr(idx.String(), descr, throw) { + return false + } + val := descr.Value + if val == nil { + val = _undefined + } + o.putIdx(i, val, throw) + return true + } + o.val.runtime.typeErrorResult(throw, "Cannot define property '%d' on a Go slice", idx) + return false } -func (o *objectGoSliceReflect) defineOwnProperty(name Value, descr propertyDescr, throw bool) bool { - if !o.val.runtime.checkHostObjectPropertyDescr(name, descr, throw) { - return false +func (o *objectGoSliceReflect) defineOwnPropertyStr(name string, descr PropertyDescriptor, throw bool) bool { + if idx := strToGoIdx(name); idx >= 0 { + if !o.val.runtime.checkHostObjectPropertyDescr(name, descr, throw) { + return false + } + val := descr.Value + if val == nil { + val = _undefined + } + o.putIdx(idx, val, throw) + return true } - o.put(name, descr.Value, throw) - return true + o.val.runtime.typeErrorResult(throw, "Cannot define property '%s' on a Go slice", name) + return false } func (o *objectGoSliceReflect) toPrimitiveNumber() Value { @@ -214,24 +279,30 @@ func (o *objectGoSliceReflect) toPrimitive() Value { } func (o *objectGoSliceReflect) deleteStr(name string, throw bool) bool { - if idx := strToIdx(name); idx >= 0 && idx < int64(o.value.Len()) { - o.value.Index(int(idx)).Set(reflect.Zero(o.value.Type().Elem())) + if idx := strToIdx64(name); idx >= 0 { + if idx < int64(o.value.Len()) { + o.val.runtime.typeErrorResult(throw, "Can't delete from Go slice") + return false + } return true } + return o.objectGoReflect.deleteStr(name, throw) } -func (o *objectGoSliceReflect) delete(name Value, throw bool) bool { - if idx := toIdx(name); idx >= 0 && idx < int64(o.value.Len()) { - o.value.Index(int(idx)).Set(reflect.Zero(o.value.Type().Elem())) - return true +func (o *objectGoSliceReflect) deleteIdx(i valueInt, throw bool) bool { + idx := int64(i) + if idx >= 0 { + if idx < int64(o.value.Len()) { + o.val.runtime.typeErrorResult(throw, "Can't delete from Go slice") + return false + } } - return o.objectGoReflect.delete(name, throw) + return true } type gosliceReflectPropIter struct { o *objectGoSliceReflect - recursive bool idx, limit int } @@ -242,26 +313,21 @@ func (i *gosliceReflectPropIter) next() (propIterItem, iterNextFunc) { return propIterItem{name: name, enumerable: _ENUM_TRUE}, i.next } - if i.recursive { - return i.o.prototype.self._enumerate(i.recursive)() - } - - return propIterItem{}, nil + return i.o.objectGoReflect.enumerateUnfiltered()() } -func (o *objectGoSliceReflect) enumerate(all, recursive bool) iterNextFunc { - return (&propFilterIter{ - wrapped: o._enumerate(recursive), - all: all, - seen: make(map[string]bool), - }).next +func (o *objectGoSliceReflect) ownKeys(all bool, accum []Value) []Value { + for i := 0; i < o.value.Len(); i++ { + accum = append(accum, asciiString(strconv.Itoa(i))) + } + + return o.objectGoReflect.ownKeys(all, accum) } -func (o *objectGoSliceReflect) _enumerate(recursive bool) iterNextFunc { +func (o *objectGoSliceReflect) enumerateUnfiltered() iterNextFunc { return (&gosliceReflectPropIter{ - o: o, - recursive: recursive, - limit: o.value.Len(), + o: o, + limit: o.value.Len(), }).next } @@ -277,15 +343,15 @@ func (o *objectGoSliceReflect) sortLen() int64 { } func (o *objectGoSliceReflect) sortGet(i int64) Value { - return o.get(intToValue(i)) + return o.getIdx(valueInt(i), nil) } func (o *objectGoSliceReflect) swap(i, j int64) { - ii := intToValue(i) - jj := intToValue(j) - x := o.get(ii) - y := o.get(jj) + ii := valueInt(i) + jj := valueInt(j) + x := o.getIdx(ii, nil) + y := o.getIdx(jj, nil) - o.put(ii, y, false) - o.put(jj, x, false) + o.setOwnIdx(ii, y, false) + o.setOwnIdx(jj, x, false) } diff --git a/object_goslice_reflect_test.go b/object_goslice_reflect_test.go index 1bad711e..cc5dcf74 100644 --- a/object_goslice_reflect_test.go +++ b/object_goslice_reflect_test.go @@ -1,6 +1,8 @@ package goja -import "testing" +import ( + "testing" +) func TestGoSliceReflectBasic(t *testing.T) { const SCRIPT = ` @@ -109,7 +111,7 @@ func TestGoSliceReflectPush(t *testing.T) { } -func TestGoSliceReflectProto(t *testing.T) { +func TestGoSliceReflectProtoMethod(t *testing.T) { const SCRIPT = ` a.join(",") ` @@ -160,3 +162,145 @@ func TestGoSliceReflectGetStr(t *testing.T) { } } } + +func TestGoSliceReflectNilObjectIfaceVal(t *testing.T) { + r := New() + a := []Value{(*Object)(nil)} + r.Set("a", a) + ret, err := r.RunString(` + ""+a[0]; + `) + if err != nil { + t.Fatal(err) + } + if !asciiString("null").SameAs(ret) { + t.Fatalf("ret: %v", ret) + } +} + +func TestGoSliceReflectSetLength(t *testing.T) { + r := New() + a := []int{1, 2, 3, 4} + b := []testing.TB{&testing.T{}, &testing.T{}, (*testing.T)(nil)} + r.Set("a", &a) + r.Set("b", &b) + _, err := r.RunString(` + 'use strict'; + a.length = 3; + if (a.length !== 3) { + throw new Error("length="+a.length); + } + if (a[3] !== undefined) { + throw new Error("a[3]="+a[3]); + } + a.length = 5; + if (a.length !== 5) { + throw new Error("a.length="+a.length); + } + if (a[3] !== 0) { + throw new Error("a[3]="+a[3]); + } + if (a[4] !== 0) { + throw new Error("a[4]="+a[4]); + } + + b.length = 3; + if (b.length !== 3) { + throw new Error("b.length="+b.length); + } + if (b[3] !== undefined) { + throw new Error("b[3]="+b[3]); + } + b.length = 5; + if (b.length !== 5) { + throw new Error("length="+b.length); + } + if (b[3] !== null) { + throw new Error("b[3]="+b[3]); + } + if (b[4] !== null) { + throw new Error("b[4]="+b[4]); + } + if (b[2] !== null) { + throw new Error("b[2]="+b[2]); + } + `) + if err != nil { + t.Fatal(err) + } +} + +func TestGoSliceReflectProto(t *testing.T) { + r := New() + a := []*Object{{}, nil, {}} + r.Set("a", &a) + _, err := r.RunString(TESTLIB + ` + var proto = [,2,,4]; + Object.setPrototypeOf(a, proto); + assert.sameValue(a[1], null, "a[1]"); + assert.sameValue(a[3], 4, "a[3]"); + var desc = Object.getOwnPropertyDescriptor(a, "1"); + assert.sameValue(desc.value, null, "desc.value"); + assert(desc.writable, "writable"); + assert(desc.enumerable, "enumerable"); + assert(!desc.configurable, "configurable"); + var v5; + Object.defineProperty(proto, "5", { + set: function(v) { + v5 = v; + } + }); + a[5] = "test"; + assert.sameValue(v5, "test", "v5"); + `) + if err != nil { + t.Fatal(err) + } + +} + +func TestGoSliceReflectProtoProto(t *testing.T) { + r := New() + a := []*Object{{}, nil, {}} + proto := []*Object{{}, {}, {}, {}} + r.Set("a", &a) + r.Set("proto", proto) + _, err := r.RunString(` + "use strict"; + var protoproto = {}; + Object.defineProperty(protoproto, "3", { + value: 42 + }); + Object.setPrototypeOf(proto, protoproto); + Object.setPrototypeOf(a, proto); + if (a.hasOwnProperty("3")) { + throw new Error("a.hasOwnProperty(\"3\")"); + } + if (a[3] !== null) { + throw new Error("a[3]="+a[3]); + } + a[3] = null; + if (a[3] !== null) { + throw new Error("a[3]=" + a[3]); + } + `) + if err != nil { + t.Fatal(err) + } + +} + +func TestGoSliceReflectDelete(t *testing.T) { + r := New() + a := []*Object{{}, nil, {}} + r.Set("a", a) + v, err := r.RunString(` + !delete a[0] && !delete a[1] && delete a[3]; + `) + if err != nil { + t.Fatal(err) + } + if v != valueTrue { + t.Fatalf("not true: %v", v) + } +} diff --git a/object_goslice_test.go b/object_goslice_test.go index b23eefb6..106d0e1f 100644 --- a/object_goslice_test.go +++ b/object_goslice_test.go @@ -1,6 +1,8 @@ package goja -import "testing" +import ( + "testing" +) func TestGoSliceBasic(t *testing.T) { const SCRIPT = ` @@ -69,7 +71,7 @@ func TestGoSliceExpand(t *testing.T) { } } -func TestGoSliceProto(t *testing.T) { +func TestGoSliceProtoMethod(t *testing.T) { const SCRIPT = ` a.join(",") ` @@ -85,3 +87,100 @@ func TestGoSliceProto(t *testing.T) { t.Fatalf("Unexpected result: '%s'", s) } } + +func TestGoSliceSetLength(t *testing.T) { + r := New() + a := []interface{}{1, 2, 3, 4} + r.Set("a", &a) + _, err := r.RunString(` + 'use strict'; + a.length = 3; + if (a.length !== 3) { + throw new Error("length="+a.length); + } + if (a[3] !== undefined) { + throw new Error("a[3](1)="+a[3]); + } + a.length = 5; + if (a.length !== 5) { + throw new Error("length="+a.length); + } + if (a[3] !== null) { + throw new Error("a[3](2)="+a[3]); + } + if (a[4] !== null) { + throw new Error("a[4]="+a[4]); + } + `) + if err != nil { + t.Fatal(err) + } +} + +func TestGoSliceProto(t *testing.T) { + r := New() + a := []interface{}{1, nil, 3} + r.Set("a", &a) + _, err := r.RunString(TESTLIB + ` + var proto = [,2,,4]; + Object.setPrototypeOf(a, proto); + assert.sameValue(a[1], null, "a[1]"); + assert.sameValue(a[3], 4, "a[3]"); + var desc = Object.getOwnPropertyDescriptor(a, "1"); + assert.sameValue(desc.value, null, "desc.value"); + assert(desc.writable, "writable"); + assert(desc.enumerable, "enumerable"); + assert(!desc.configurable, "configurable"); + var v5; + Object.defineProperty(proto, "5", { + set: function(v) { + v5 = v; + } + }); + a[5] = "test"; + assert.sameValue(v5, "test", "v5"); + `) + if err != nil { + t.Fatal(err) + } + +} + +func TestGoSliceProtoProto(t *testing.T) { + r := New() + a := []interface{}{1, nil, 3} + proto := []interface{}{1, 2, 3, 4} + r.Set("a", &a) + r.Set("proto", proto) + _, err := r.RunString(` + "use strict"; + var protoproto = Object.create(null); + Object.defineProperty(protoproto, "3", { + value: 42 + }); + Object.setPrototypeOf(proto, protoproto); + Object.setPrototypeOf(a, proto); + a[3] = 11; + if (a[3] !== 11) { + throw new Error("a[3]=" + a[3]); + } + `) + if err != nil { + t.Fatal(err) + } +} + +func TestGoSliceDelete(t *testing.T) { + r := New() + a := []interface{}{1, nil, 3} + r.Set("a", a) + v, err := r.RunString(` + !delete a[0] && !delete a[1] && delete a[3]; + `) + if err != nil { + t.Fatal(err) + } + if v != valueTrue { + t.Fatalf("not true: %v", v) + } +} diff --git a/object_lazy.go b/object_lazy.go index e86b7a66..d75a9744 100644 --- a/object_lazy.go +++ b/object_lazy.go @@ -13,88 +13,150 @@ func (o *lazyObject) className() string { return obj.className() } -func (o *lazyObject) get(n Value) Value { +func (o *lazyObject) getIdx(p valueInt, receiver Value) Value { obj := o.create(o.val) o.val.self = obj - return obj.get(n) + return obj.getIdx(p, receiver) } -func (o *lazyObject) getProp(n Value) Value { +func (o *lazyObject) getSym(p *valueSymbol, receiver Value) Value { obj := o.create(o.val) o.val.self = obj - return obj.getProp(n) + return obj.getSym(p, receiver) } -func (o *lazyObject) getPropStr(name string) Value { +func (o *lazyObject) getOwnPropIdx(idx valueInt) Value { obj := o.create(o.val) o.val.self = obj - return obj.getPropStr(name) + return obj.getOwnPropIdx(idx) } -func (o *lazyObject) getStr(name string) Value { +func (o *lazyObject) getOwnPropSym(s *valueSymbol) Value { obj := o.create(o.val) o.val.self = obj - return obj.getStr(name) + return obj.getOwnPropSym(s) } -func (o *lazyObject) getOwnPropStr(name string) Value { +func (o *lazyObject) hasPropertyIdx(idx valueInt) bool { obj := o.create(o.val) o.val.self = obj - return obj.getOwnPropStr(name) + return obj.hasPropertyIdx(idx) } -func (o *lazyObject) getOwnProp(name Value) Value { +func (o *lazyObject) hasPropertySym(s *valueSymbol) bool { obj := o.create(o.val) o.val.self = obj - return obj.getOwnProp(name) + return obj.hasPropertySym(s) } -func (o *lazyObject) put(n Value, val Value, throw bool) { +func (o *lazyObject) hasOwnPropertyIdx(idx valueInt) bool { obj := o.create(o.val) o.val.self = obj - obj.put(n, val, throw) + return obj.hasOwnPropertyIdx(idx) } -func (o *lazyObject) putStr(name string, val Value, throw bool) { +func (o *lazyObject) hasOwnPropertySym(s *valueSymbol) bool { obj := o.create(o.val) o.val.self = obj - obj.putStr(name, val, throw) + return obj.hasOwnPropertySym(s) } -func (o *lazyObject) hasProperty(n Value) bool { +func (o *lazyObject) defineOwnPropertyStr(name string, desc PropertyDescriptor, throw bool) bool { obj := o.create(o.val) o.val.self = obj - return obj.hasProperty(n) + return obj.defineOwnPropertyStr(name, desc, throw) } -func (o *lazyObject) hasPropertyStr(name string) bool { +func (o *lazyObject) defineOwnPropertyIdx(name valueInt, desc PropertyDescriptor, throw bool) bool { obj := o.create(o.val) o.val.self = obj - return obj.hasPropertyStr(name) + return obj.defineOwnPropertyIdx(name, desc, throw) } -func (o *lazyObject) hasOwnProperty(n Value) bool { +func (o *lazyObject) defineOwnPropertySym(name *valueSymbol, desc PropertyDescriptor, throw bool) bool { obj := o.create(o.val) o.val.self = obj - return obj.hasOwnProperty(n) + return obj.defineOwnPropertySym(name, desc, throw) } -func (o *lazyObject) hasOwnPropertyStr(name string) bool { +func (o *lazyObject) deleteIdx(idx valueInt, throw bool) bool { obj := o.create(o.val) o.val.self = obj - return obj.hasOwnPropertyStr(name) + return obj.deleteIdx(idx, throw) +} + +func (o *lazyObject) deleteSym(s *valueSymbol, throw bool) bool { + obj := o.create(o.val) + o.val.self = obj + return obj.deleteSym(s, throw) +} + +func (o *lazyObject) getStr(name string, receiver Value) Value { + obj := o.create(o.val) + o.val.self = obj + return obj.getStr(name, receiver) +} + +func (o *lazyObject) getOwnPropStr(name string) Value { + obj := o.create(o.val) + o.val.self = obj + return obj.getOwnPropStr(name) +} + +func (o *lazyObject) setOwnStr(p string, v Value, throw bool) bool { + obj := o.create(o.val) + o.val.self = obj + return obj.setOwnStr(p, v, throw) +} + +func (o *lazyObject) setOwnIdx(p valueInt, v Value, throw bool) bool { + obj := o.create(o.val) + o.val.self = obj + return obj.setOwnIdx(p, v, throw) +} + +func (o *lazyObject) setOwnSym(p *valueSymbol, v Value, throw bool) bool { + obj := o.create(o.val) + o.val.self = obj + return obj.setOwnSym(p, v, throw) } -func (o *lazyObject) _putProp(name string, value Value, writable, enumerable, configurable bool) Value { +func (o *lazyObject) setForeignStr(p string, v, receiver Value, throw bool) (bool, bool) { obj := o.create(o.val) o.val.self = obj - return obj._putProp(name, value, writable, enumerable, configurable) + return obj.setForeignStr(p, v, receiver, throw) } -func (o *lazyObject) defineOwnProperty(name Value, descr propertyDescr, throw bool) bool { +func (o *lazyObject) setForeignIdx(p valueInt, v, receiver Value, throw bool) (bool, bool) { obj := o.create(o.val) o.val.self = obj - return obj.defineOwnProperty(name, descr, throw) + return obj.setForeignIdx(p, v, receiver, throw) +} + +func (o *lazyObject) setForeignSym(p *valueSymbol, v, receiver Value, throw bool) (bool, bool) { + obj := o.create(o.val) + o.val.self = obj + return obj.setForeignSym(p, v, receiver, throw) +} + +func (o *lazyObject) hasPropertyStr(name string) bool { + obj := o.create(o.val) + o.val.self = obj + return obj.hasPropertyStr(name) +} + +func (o *lazyObject) hasOwnPropertyStr(name string) bool { + obj := o.create(o.val) + o.val.self = obj + return obj.hasOwnPropertyStr(name) +} + +func (o *lazyObject) _putProp(string, Value, bool, bool, bool) Value { + panic("cannot use _putProp() in lazy object") +} + +func (o *lazyObject) _putSym(*valueSymbol, Value) { + panic("cannot use _putSym() in lazy object") } func (o *lazyObject) toPrimitiveNumber() Value { @@ -121,16 +183,16 @@ func (o *lazyObject) assertCallable() (call func(FunctionCall) Value, ok bool) { return obj.assertCallable() } -func (o *lazyObject) deleteStr(name string, throw bool) bool { +func (o *lazyObject) assertConstructor() func(args []Value, newTarget *Object) *Object { obj := o.create(o.val) o.val.self = obj - return obj.deleteStr(name, throw) + return obj.assertConstructor() } -func (o *lazyObject) delete(name Value, throw bool) bool { +func (o *lazyObject) deleteStr(name string, throw bool) bool { obj := o.create(o.val) o.val.self = obj - return obj.delete(name, throw) + return obj.deleteStr(name, throw) } func (o *lazyObject) proto() *Object { @@ -151,22 +213,22 @@ func (o *lazyObject) isExtensible() bool { return obj.isExtensible() } -func (o *lazyObject) preventExtensions() { +func (o *lazyObject) preventExtensions(throw bool) bool { obj := o.create(o.val) o.val.self = obj - obj.preventExtensions() + return obj.preventExtensions(throw) } -func (o *lazyObject) enumerate(all, recusrive bool) iterNextFunc { +func (o *lazyObject) enumerateUnfiltered() iterNextFunc { obj := o.create(o.val) o.val.self = obj - return obj.enumerate(all, recusrive) + return obj.enumerateUnfiltered() } -func (o *lazyObject) _enumerate(recursive bool) iterNextFunc { +func (o *lazyObject) enumerate() iterNextFunc { obj := o.create(o.val) o.val.self = obj - return obj._enumerate(recursive) + return obj.enumerate() } func (o *lazyObject) export() interface{} { @@ -187,16 +249,28 @@ func (o *lazyObject) equal(other objectImpl) bool { return obj.equal(other) } -func (o *lazyObject) getOwnSymbols() []Value { +func (o *lazyObject) ownKeys(all bool, accum []Value) []Value { + obj := o.create(o.val) + o.val.self = obj + return obj.ownKeys(all, accum) +} + +func (o *lazyObject) ownSymbols() []Value { + obj := o.create(o.val) + o.val.self = obj + return obj.ownSymbols() +} + +func (o *lazyObject) ownPropertyKeys(all bool, accum []Value) []Value { obj := o.create(o.val) o.val.self = obj - return obj.getOwnSymbols() + return obj.ownPropertyKeys(all, accum) } -func (o *lazyObject) setProto(proto *Object) *Object { +func (o *lazyObject) setProto(proto *Object, throw bool) bool { obj := o.create(o.val) o.val.self = obj - return obj.setProto(proto) + return obj.setProto(proto, throw) } func (o *lazyObject) sortLen() int64 { diff --git a/object_test.go b/object_test.go index 6231fc57..9bce00f8 100644 --- a/object_test.go +++ b/object_test.go @@ -5,8 +5,8 @@ import "testing" func TestArray1(t *testing.T) { r := &Runtime{} a := r.newArray(nil) - a.put(valueInt(0), asciiString("test"), true) - if l := a.getStr("length").ToInteger(); l != 1 { + a.setOwnIdx(valueInt(0), asciiString("test"), true) + if l := a.getStr("length", nil).ToInteger(); l != 1 { t.Fatalf("Unexpected length: %d", l) } } @@ -67,6 +67,36 @@ func TestDefineProperty(t *testing.T) { } } +func TestPropertyOrder(t *testing.T) { + const SCRIPT = ` + var o = {}; + var sym1 = Symbol(1); + var sym2 = Symbol(2); + o[sym2] = 1; + o[4294967294] = 1; + o[2] = 1; + o[1] = 1; + o[0] = 1; + o["02"] = 1; + o[4294967295] = 1; + o["01"] = 1; + o["00"] = 1; + o[sym1] = 1; + var expected = ["0", "1", "2", "4294967294", "02", "4294967295", "01", "00", sym2, sym1]; + var actual = Reflect.ownKeys(o); + if (actual.length !== expected.length) { + throw new Error("Unexpected length: "+actual.length); + } + for (var i = 0; i < actual.length; i++) { + if (actual[i] !== expected[i]) { + throw new Error("Unexpected list: " + actual); + } + } + ` + + testScript1(SCRIPT, _undefined, t) +} + func BenchmarkPut(b *testing.B) { v := &Object{} @@ -82,7 +112,7 @@ func BenchmarkPut(b *testing.B) { var val Value = valueInt(123) for i := 0; i < b.N; i++ { - o.put(key, val, false) + v.setOwn(key, val, false) } } @@ -101,7 +131,7 @@ func BenchmarkPutStr(b *testing.B) { var val Value = valueInt(123) for i := 0; i < b.N; i++ { - o.putStr("test", val, false) + o.setOwnStr("test", val, false) } } @@ -119,7 +149,7 @@ func BenchmarkGet(b *testing.B) { var n Value = asciiString("test") for i := 0; i < b.N; i++ { - o.get(n) + v.get(n, nil) } } @@ -136,7 +166,7 @@ func BenchmarkGetStr(b *testing.B) { o.init() for i := 0; i < b.N; i++ { - o.getStr("test") + o.getStr("test", nil) } } @@ -190,11 +220,11 @@ func BenchmarkArrayGetStr(b *testing.B) { a.init() - a.put(valueInt(0), asciiString("test"), false) + v.setOwn(valueInt(0), asciiString("test"), false) b.StartTimer() for i := 0; i < b.N; i++ { - a.getStr("0") + a.getStr("0", nil) } } @@ -216,12 +246,12 @@ func BenchmarkArrayGet(b *testing.B) { var idx Value = valueInt(0) - a.put(idx, asciiString("test"), false) + v.setOwn(idx, asciiString("test"), false) b.StartTimer() for i := 0; i < b.N; i++ { - a.get(idx) + v.get(idx, nil) } } @@ -249,7 +279,7 @@ func BenchmarkArrayPut(b *testing.B) { b.StartTimer() for i := 0; i < b.N; i++ { - a.put(idx, val, false) + v.setOwn(idx, val, false) } } @@ -267,9 +297,9 @@ func BenchmarkAdd(b *testing.B) { y = valueInt(2) for i := 0; i < b.N; i++ { - if xi, ok := x.assertInt(); ok { - if yi, ok := y.assertInt(); ok { - x = valueInt(xi + yi) + if xi, ok := x.(valueInt); ok { + if yi, ok := y.(valueInt); ok { + x = xi + yi } } } @@ -284,8 +314,8 @@ func BenchmarkAddString(b *testing.B) { for i := 0; i < b.N; i++ { var z Value - if xi, ok := x.assertString(); ok { - if yi, ok := y.assertString(); ok { + if xi, ok := x.(valueString); ok { + if yi, ok := y.(valueString); ok { z = xi.concat(yi) } } diff --git a/parser/expression.go b/parser/expression.go index 3eaa64e3..a90bc1a9 100644 --- a/parser/expression.go +++ b/parser/expression.go @@ -373,6 +373,23 @@ func (self *_parser) parseBracketMember(left ast.Expression) ast.Expression { func (self *_parser) parseNewExpression() ast.Expression { idx := self.expect(token.NEW) + if self.token == token.PERIOD { + self.next() + prop := self.parseIdentifier() + if prop.Name == "target" { + if !self.scope.inFunction { + self.error(idx, "new.target expression is not allowed here") + } + return &ast.MetaProperty{ + Meta: &ast.Identifier{ + Name: token.NEW.String(), + Idx: idx, + }, + Property: prop, + } + } + self.errorUnexpectedToken(token.IDENTIFIER) + } callee := self.parseLeftHandSideExpression() node := &ast.NewExpression{ New: idx, diff --git a/proxy.go b/proxy.go new file mode 100644 index 00000000..21526a96 --- /dev/null +++ b/proxy.go @@ -0,0 +1,764 @@ +package goja + +type Proxy struct { + proxy *proxyObject +} + +type proxyPropIter struct { + p *proxyObject + names []Value + idx int +} + +func (i *proxyPropIter) next() (propIterItem, iterNextFunc) { + for i.idx < len(i.names) { + name := i.names[i.idx] + i.idx++ + if prop := i.p.val.getOwnProp(name); prop != nil { + return propIterItem{name: name.String(), value: prop}, i.next + } + } + if proto := i.p.proto(); proto != nil { + return proto.self.enumerateUnfiltered()() + } + return propIterItem{}, nil +} + +func (r *Runtime) newProxyObject(target *Object, handler *Object, proto *Object) *proxyObject { + if p, ok := target.self.(*proxyObject); ok { + if p.handler == nil { + panic(r.NewTypeError("Cannot create proxy with a revoked proxy as target")) + } + } + if p, ok := handler.self.(*proxyObject); ok { + if p.handler == nil { + panic(r.NewTypeError("Cannot create proxy with a revoked proxy as handler")) + } + } + v := &Object{runtime: r} + p := &proxyObject{} + v.self = p + p.val = v + p.class = classObject + if proto == nil { + p.prototype = r.global.ObjectPrototype + } else { + p.prototype = proto + } + p.extensible = false + p.init() + p.target = target + p.handler = handler + if call, ok := target.self.assertCallable(); ok { + p.call = call + } + if ctor := target.self.assertConstructor(); ctor != nil { + p.ctor = ctor + } + return p +} + +func (p *Proxy) Revoke() { + p.proxy.revoke() +} + +type proxyTrap string + +const ( + proxy_trap_getPrototypeOf = "getPrototypeOf" + proxy_trap_setPrototypeOf = "setPrototypeOf" + proxy_trap_isExtensible = "isExtensible" + proxy_trap_preventExtensions = "preventExtensions" + proxy_trap_getOwnPropertyDescriptor = "getOwnPropertyDescriptor" + proxy_trap_defineProperty = "defineProperty" + proxy_trap_has = "has" + proxy_trap_get = "get" + proxy_trap_set = "set" + proxy_trap_deleteProperty = "deleteProperty" + proxy_trap_ownKeys = "ownKeys" + proxy_trap_apply = "apply" + proxy_trap_construct = "construct" +) + +func (p proxyTrap) String() (name string) { + return string(p) +} + +type proxyObject struct { + baseObject + target *Object + handler *Object + call func(FunctionCall) Value + ctor func(args []Value, newTarget *Object) *Object +} + +func (p *proxyObject) proxyCall(trap proxyTrap, args ...Value) (Value, bool) { + r := p.val.runtime + if p.handler == nil { + panic(r.NewTypeError("Proxy already revoked")) + } + + if m := toMethod(r.getVStr(p.handler, trap.String())); m != nil { + return m(FunctionCall{ + This: p.handler, + Arguments: args, + }), true + } + + return nil, false +} + +func (p *proxyObject) proto() *Object { + if v, ok := p.proxyCall(proxy_trap_getPrototypeOf, p.target); ok { + var handlerProto *Object + if v != _null { + handlerProto = p.val.runtime.toObject(v) + } + if !p.target.self.isExtensible() && !p.__sameValue(handlerProto, p.target.self.proto()) { + panic(p.val.runtime.NewTypeError("'getPrototypeOf' on proxy: proxy target is non-extensible but the trap did not return its actual prototype")) + } + return handlerProto + } + + return p.target.self.proto() +} + +func (p *proxyObject) setProto(proto *Object, throw bool) bool { + if v, ok := p.proxyCall(proxy_trap_setPrototypeOf, p.target, proto); ok { + if v.ToBoolean() { + if !p.target.self.isExtensible() && !p.__sameValue(proto, p.target.self.proto()) { + panic(p.val.runtime.NewTypeError("'setPrototypeOf' on proxy: trap returned truish for setting a new prototype on the non-extensible proxy target")) + } + return true + } else { + p.val.runtime.typeErrorResult(throw, "'setPrototypeOf' on proxy: trap returned falsish") + } + } + + return p.target.self.setProto(proto, throw) +} + +func (p *proxyObject) isExtensible() bool { + if v, ok := p.proxyCall(proxy_trap_isExtensible, p.target); ok { + booleanTrapResult := v.ToBoolean() + if te := p.target.self.isExtensible(); booleanTrapResult != te { + panic(p.val.runtime.NewTypeError("'isExtensible' on proxy: trap result does not reflect extensibility of proxy target (which is '%v')", te)) + } + return booleanTrapResult + } + + return p.target.self.isExtensible() +} + +func (p *proxyObject) preventExtensions(throw bool) bool { + if v, ok := p.proxyCall(proxy_trap_preventExtensions, p.target); ok { + booleanTrapResult := v.ToBoolean() + if !booleanTrapResult { + p.val.runtime.typeErrorResult(throw, "'preventExtensions' on proxy: trap returned falsish") + return false + } + if te := p.target.self.isExtensible(); booleanTrapResult && te { + panic(p.val.runtime.NewTypeError("'preventExtensions' on proxy: trap returned truish but the proxy target is extensible")) + } + } + + return p.target.self.preventExtensions(throw) +} + +func propToValueProp(v Value) *valueProperty { + if v == nil { + return nil + } + if v, ok := v.(*valueProperty); ok { + return v + } + return &valueProperty{ + value: v, + writable: true, + configurable: true, + enumerable: true, + } +} + +func (p *proxyObject) proxyDefineOwnProperty(name Value, descr PropertyDescriptor, throw bool) (bool, bool) { + if v, ok := p.proxyCall(proxy_trap_defineProperty, p.target, name, descr.toValue(p.val.runtime)); ok { + booleanTrapResult := v.ToBoolean() + if !booleanTrapResult { + p.val.runtime.typeErrorResult(throw, "'defineProperty' on proxy: trap returned falsish") + return false, true + } + targetDesc := propToValueProp(p.target.getOwnProp(name)) + extensibleTarget := p.target.self.isExtensible() + settingConfigFalse := descr.Configurable == FLAG_FALSE + if targetDesc == nil { + if !extensibleTarget { + panic(p.val.runtime.NewTypeError()) + } + if settingConfigFalse { + panic(p.val.runtime.NewTypeError()) + } + } else { + if !p.__isCompatibleDescriptor(extensibleTarget, &descr, targetDesc) { + panic(p.val.runtime.NewTypeError()) + } + if settingConfigFalse && targetDesc.configurable { + panic(p.val.runtime.NewTypeError()) + } + } + return booleanTrapResult, true + } + return false, false +} + +func (p *proxyObject) defineOwnPropertyStr(name string, descr PropertyDescriptor, throw bool) bool { + if v, ok := p.proxyDefineOwnProperty(newStringValue(name), descr, throw); ok { + return v + } + return p.target.self.defineOwnPropertyStr(name, descr, throw) +} + +func (p *proxyObject) defineOwnPropertyIdx(idx valueInt, descr PropertyDescriptor, throw bool) bool { + if v, ok := p.proxyDefineOwnProperty(idx, descr, throw); ok { + return v + } + return p.target.self.defineOwnPropertyIdx(idx, descr, throw) +} + +func (p *proxyObject) defineOwnPropertySym(s *valueSymbol, descr PropertyDescriptor, throw bool) bool { + if v, ok := p.proxyDefineOwnProperty(s, descr, throw); ok { + return v + } + return p.target.self.defineOwnPropertySym(s, descr, throw) +} + +func (p *proxyObject) proxyHas(name Value) (bool, bool) { + if v, ok := p.proxyCall(proxy_trap_has, p.target, name); ok { + booleanTrapResult := v.ToBoolean() + if !booleanTrapResult { + targetDesc := propToValueProp(p.target.getOwnProp(name)) + if targetDesc != nil { + if !targetDesc.configurable { + panic(p.val.runtime.NewTypeError("'has' on proxy: trap returned falsish for property '%s' which exists in the proxy target as non-configurable", name.String())) + } + if !p.target.self.isExtensible() { + panic(p.val.runtime.NewTypeError("'has' on proxy: trap returned falsish for property '%s' but the proxy target is not extensible", name.String())) + } + } + } + return booleanTrapResult, true + } + + return false, false +} + +func (p *proxyObject) hasPropertyStr(name string) bool { + if b, ok := p.proxyHas(newStringValue(name)); ok { + return b + } + + return p.target.self.hasPropertyStr(name) +} + +func (p *proxyObject) hasPropertyIdx(idx valueInt) bool { + if b, ok := p.proxyHas(idx); ok { + return b + } + + return p.target.self.hasPropertyIdx(idx) +} + +func (p *proxyObject) hasPropertySym(s *valueSymbol) bool { + if b, ok := p.proxyHas(s); ok { + return b + } + + return p.target.self.hasPropertySym(s) +} + +func (p *proxyObject) hasOwnPropertyStr(name string) bool { + return p.getOwnPropStr(name) != nil +} + +func (p *proxyObject) hasOwnPropertyIdx(idx valueInt) bool { + return p.getOwnPropIdx(idx) != nil +} + +func (p *proxyObject) hasOwnPropertySym(s *valueSymbol) bool { + return p.getOwnPropSym(s) != nil +} + +func (p *proxyObject) proxyGetOwnPropertyDescriptor(name Value) (Value, bool) { + target := p.target + if v, ok := p.proxyCall(proxy_trap_getOwnPropertyDescriptor, target, name); ok { + r := p.val.runtime + + targetDesc := propToValueProp(target.getOwnProp(name)) + + var trapResultObj *Object + if v != nil && v != _undefined { + if obj, ok := v.(*Object); ok { + trapResultObj = obj + } else { + panic(r.NewTypeError("'getOwnPropertyDescriptor' on proxy: trap returned neither object nor undefined for property '%s'", name.String())) + } + } + if trapResultObj == nil { + if targetDesc == nil { + return nil, true + } + if !targetDesc.configurable { + panic(r.NewTypeError()) + } + if !target.self.isExtensible() { + panic(r.NewTypeError()) + } + return nil, true + } + extensibleTarget := target.self.isExtensible() + resultDesc := r.toPropertyDescriptor(trapResultObj) + resultDesc.complete() + if !p.__isCompatibleDescriptor(extensibleTarget, &resultDesc, targetDesc) { + panic(r.NewTypeError("'getOwnPropertyDescriptor' on proxy: trap returned descriptor for property '%s' that is incompatible with the existing property in the proxy target", name.String())) + } + + if resultDesc.Configurable == FLAG_FALSE { + if targetDesc == nil { + panic(r.NewTypeError("'getOwnPropertyDescriptor' on proxy: trap reported non-configurability for property '%s' which is non-existent in the proxy target", name.String())) + } + + if targetDesc.configurable { + panic(r.NewTypeError("'getOwnPropertyDescriptor' on proxy: trap reported non-configurability for property '%s' which is configurable in the proxy target", name.String())) + } + } + + if resultDesc.Writable == FLAG_TRUE && resultDesc.Configurable == FLAG_TRUE && + resultDesc.Enumerable == FLAG_TRUE { + return resultDesc.Value, true + } + return r.toValueProp(trapResultObj), true + } + + return nil, false +} + +func (p *proxyObject) getOwnPropStr(name string) Value { + if v, ok := p.proxyGetOwnPropertyDescriptor(newStringValue(name)); ok { + return v + } + + return p.target.self.getOwnPropStr(name) +} + +func (p *proxyObject) getOwnPropIdx(idx valueInt) Value { + if v, ok := p.proxyGetOwnPropertyDescriptor(idx.toString()); ok { + return v + } + + return p.target.self.getOwnPropIdx(idx) +} + +func (p *proxyObject) getOwnPropSym(s *valueSymbol) Value { + if v, ok := p.proxyGetOwnPropertyDescriptor(s); ok { + return v + } + + return p.target.self.getOwnPropSym(s) +} + +func (p *proxyObject) getStr(name string, receiver Value) Value { + if v, ok := p.proxyGet(newStringValue(name), receiver); ok { + return v + } + return p.target.self.getStr(name, receiver) +} + +func (p *proxyObject) getIdx(idx valueInt, receiver Value) Value { + if v, ok := p.proxyGet(idx.toString(), receiver); ok { + return v + } + return p.target.self.getIdx(idx, receiver) +} + +func (p *proxyObject) getSym(s *valueSymbol, receiver Value) Value { + if v, ok := p.proxyGet(s, receiver); ok { + return v + } + return p.target.self.getSym(s, receiver) + +} + +func (p *proxyObject) proxyGet(name, receiver Value) (Value, bool) { + target := p.target + if v, ok := p.proxyCall(proxy_trap_get, target, name, receiver); ok { + if targetDesc, ok := target.getOwnProp(name).(*valueProperty); ok { + if !targetDesc.accessor { + if !targetDesc.writable && !targetDesc.configurable && !v.SameAs(targetDesc.value) { + panic(p.val.runtime.NewTypeError("'get' on proxy: property '%s' is a read-only and non-configurable data property on the proxy target but the proxy did not return its actual value (expected '%s' but got '%s')", name.String(), nilSafe(targetDesc.value), ret)) + } + } else { + if !targetDesc.configurable && targetDesc.getterFunc == nil && v != _undefined { + panic(p.val.runtime.NewTypeError("'get' on proxy: property '%s' is a non-configurable accessor property on the proxy target and does not have a getter function, but the trap did not return 'undefined' (got '%s')", name.String(), ret)) + } + } + } + return v, true + } + + return nil, false +} + +func (p *proxyObject) proxySet(name, value, receiver Value, throw bool) (bool, bool) { + target := p.target + if v, ok := p.proxyCall(proxy_trap_set, target, name, value, receiver); ok { + if v.ToBoolean() { + if prop, ok := target.getOwnProp(name).(*valueProperty); ok { + if prop.accessor { + if !prop.configurable && prop.setterFunc == nil { + panic(p.val.runtime.NewTypeError("'set' on proxy: trap returned truish for property '%s' which exists in the proxy target as a non-configurable and non-writable accessor property without a setter", name.String())) + } + } else if !prop.configurable && !prop.writable && !p.__sameValue(prop.value, value) { + panic(p.val.runtime.NewTypeError("'set' on proxy: trap returned truish for property '%s' which exists in the proxy target as a non-configurable and non-writable data property with a different value", name.String())) + } + } + return true, true + } + if throw { + panic(p.val.runtime.NewTypeError("'set' on proxy: trap returned falsish for property '%s'", name.String())) + } + return false, true + } + + return false, false +} + +func (p *proxyObject) setOwnStr(name string, v Value, throw bool) bool { + if res, ok := p.proxySet(newStringValue(name), v, p.val, throw); ok { + return res + } + return p.target.setStr(name, v, p.val, throw) +} + +func (p *proxyObject) setOwnIdx(idx valueInt, v Value, throw bool) bool { + if res, ok := p.proxySet(idx.toString(), v, p.val, throw); ok { + return res + } + return p.target.setIdx(idx, v, p.val, throw) +} + +func (p *proxyObject) setOwnSym(s *valueSymbol, v Value, throw bool) bool { + if res, ok := p.proxySet(s, v, p.val, throw); ok { + return res + } + return p.target.setSym(s, v, p.val, throw) +} + +func (p *proxyObject) setForeignStr(name string, v, receiver Value, throw bool) (bool, bool) { + if res, ok := p.proxySet(newStringValue(name), v, receiver, throw); ok { + return res, true + } + return p.target.setStr(name, v, receiver, throw), true +} + +func (p *proxyObject) setForeignIdx(idx valueInt, v, receiver Value, throw bool) (bool, bool) { + if res, ok := p.proxySet(idx.toString(), v, receiver, throw); ok { + return res, true + } + return p.target.setIdx(idx, v, receiver, throw), true +} + +func (p *proxyObject) setForeignSym(s *valueSymbol, v, receiver Value, throw bool) (bool, bool) { + if res, ok := p.proxySet(s, v, receiver, throw); ok { + return res, true + } + return p.target.setSym(s, v, receiver, throw), true +} + +func (p *proxyObject) proxyDelete(n Value) (bool, bool) { + target := p.target + if v, ok := p.proxyCall(proxy_trap_deleteProperty, target, n); ok { + if v.ToBoolean() { + if targetDesc, ok := target.getOwnProp(n).(*valueProperty); ok { + if !targetDesc.configurable { + panic(p.val.runtime.NewTypeError("'deleteProperty' on proxy: property '%s' is a non-configurable property but the trap returned truish", n.String())) + } + } + return true, true + } + return false, true + } + return false, false +} + +func (p *proxyObject) deleteStr(name string, throw bool) bool { + if ret, ok := p.proxyDelete(newStringValue(name)); ok { + return ret + } + + return p.target.self.deleteStr(name, throw) +} + +func (p *proxyObject) deleteIdx(idx valueInt, throw bool) bool { + if ret, ok := p.proxyDelete(idx.toString()); ok { + return ret + } + + return p.target.self.deleteIdx(idx, throw) +} + +func (p *proxyObject) deleteSym(s *valueSymbol, throw bool) bool { + if ret, ok := p.proxyDelete(s); ok { + return ret + } + + return p.target.self.deleteSym(s, throw) +} + +func (p *proxyObject) ownPropertyKeys(all bool, _ []Value) []Value { + if v, ok := p.proxyOwnKeys(); ok { + return v + } + return p.target.self.ownPropertyKeys(all, nil) +} + +func (p *proxyObject) proxyOwnKeys() ([]Value, bool) { + target := p.target + if v, ok := p.proxyCall(proxy_trap_ownKeys, p.target); ok { + keys := p.val.runtime.toObject(v) + var keyList []Value + keySet := make(map[Value]struct{}) + l := toLength(keys.self.getStr("length", nil)) + for k := int64(0); k < l; k++ { + item := keys.self.getIdx(valueInt(k), nil) + if _, ok := item.(valueString); !ok { + if _, ok := item.(*valueSymbol); !ok { + panic(p.val.runtime.NewTypeError("%s is not a valid property name", item.String())) + } + } + keyList = append(keyList, item) + keySet[item] = struct{}{} + } + ext := target.self.isExtensible() + for _, itemName := range target.self.ownPropertyKeys(true, nil) { + if _, exists := keySet[itemName]; exists { + delete(keySet, itemName) + } else { + if !ext { + panic(p.val.runtime.NewTypeError("'ownKeys' on proxy: trap result did not include '%s'", itemName.String())) + } + prop := target.getOwnProp(itemName) + if prop, ok := prop.(*valueProperty); ok && !prop.configurable { + panic(p.val.runtime.NewTypeError("'ownKeys' on proxy: trap result did not include non-configurable '%s'", itemName.String())) + } + } + } + if !ext && len(keyList) > 0 && len(keySet) > 0 { + panic(p.val.runtime.NewTypeError("'ownKeys' on proxy: trap returned extra keys but proxy target is non-extensible")) + } + + return keyList, true + } + + return nil, false +} + +func (p *proxyObject) enumerateUnfiltered() iterNextFunc { + return (&proxyPropIter{ + p: p, + names: p.ownKeys(true, nil), + }).next +} + +func (p *proxyObject) assertCallable() (call func(FunctionCall) Value, ok bool) { + if p.call != nil { + return func(call FunctionCall) Value { + return p.apply(call) + }, true + } + return nil, false +} + +func (p *proxyObject) assertConstructor() func(args []Value, newTarget *Object) *Object { + if p.ctor != nil { + return p.construct + } + return nil +} + +func (p *proxyObject) apply(call FunctionCall) Value { + if p.call == nil { + p.val.runtime.NewTypeError("proxy target is not a function") + } + if v, ok := p.proxyCall(proxy_trap_apply, p.target, nilSafe(call.This), p.val.runtime.newArrayValues(call.Arguments)); ok { + return v + } + return p.call(call) +} + +func (p *proxyObject) construct(args []Value, newTarget *Object) *Object { + if p.ctor == nil { + panic(p.val.runtime.NewTypeError("proxy target is not a constructor")) + } + if newTarget == nil { + newTarget = p.val + } + if v, ok := p.proxyCall(proxy_trap_construct, p.target, p.val.runtime.newArrayValues(args), newTarget); ok { + return p.val.runtime.toObject(v) + } + return p.ctor(args, newTarget) +} + +func (p *proxyObject) __isCompatibleDescriptor(extensible bool, desc *PropertyDescriptor, current *valueProperty) bool { + if current == nil { + return extensible + } + + /*if desc.Empty() { + return true + }*/ + + /*if p.__isEquivalentDescriptor(desc, current) { + return true + }*/ + + if !current.configurable { + if desc.Configurable == FLAG_TRUE { + return false + } + + if desc.Enumerable != FLAG_NOT_SET && desc.Enumerable.Bool() != current.enumerable { + return false + } + + if p.__isGenericDescriptor(desc) { + return true + } + + if p.__isDataDescriptor(desc) != !current.accessor { + return desc.Configurable != FLAG_FALSE + } + + if p.__isDataDescriptor(desc) && !current.accessor { + if desc.Configurable == FLAG_FALSE { + if desc.Writable == FLAG_FALSE && current.writable { + return false + } + if desc.Writable == FLAG_FALSE { + if desc.Value != nil && !desc.Value.SameAs(current.value) { + return false + } + } + } + return true + } + if p.__isAccessorDescriptor(desc) && current.accessor { + if desc.Configurable == FLAG_FALSE { + if desc.Setter != nil && desc.Setter.SameAs(current.setterFunc) { + return false + } + if desc.Getter != nil && desc.Getter.SameAs(current.getterFunc) { + return false + } + } + } + } + return true +} + +func (p *proxyObject) __isAccessorDescriptor(desc *PropertyDescriptor) bool { + return desc.Setter != nil || desc.Getter != nil +} + +func (p *proxyObject) __isDataDescriptor(desc *PropertyDescriptor) bool { + return desc.Value != nil || desc.Writable != FLAG_NOT_SET +} + +func (p *proxyObject) __isGenericDescriptor(desc *PropertyDescriptor) bool { + return !p.__isAccessorDescriptor(desc) && !p.__isDataDescriptor(desc) +} + +func (p *proxyObject) __sameValue(val1, val2 Value) bool { + if val1 == nil && val2 == nil { + return true + } + if val1 != nil { + return val1.SameAs(val2) + } + return false +} + +func (p *proxyObject) filterKeys(vals []Value, all, symbols bool) []Value { + if !all { + k := 0 + for i, val := range vals { + var prop Value + if symbols { + if s, ok := val.(*valueSymbol); ok { + prop = p.getOwnPropSym(s) + } else { + continue + } + } else { + if _, ok := val.(*valueSymbol); !ok { + prop = p.getOwnPropStr(val.String()) + } else { + continue + } + } + if prop == nil { + continue + } + if prop, ok := prop.(*valueProperty); ok && !prop.enumerable { + continue + } + if k != i { + vals[k] = vals[i] + } + k++ + } + vals = vals[:k] + } else { + k := 0 + for i, val := range vals { + if _, ok := val.(*valueSymbol); ok != symbols { + continue + } + if k != i { + vals[k] = vals[i] + } + k++ + } + vals = vals[:k] + } + return vals +} + +func (p *proxyObject) ownKeys(all bool, _ []Value) []Value { // we can assume accum is empty + if vals, ok := p.proxyOwnKeys(); ok { + return p.filterKeys(vals, all, false) + } + + return p.target.self.ownKeys(all, nil) +} + +func (p *proxyObject) ownSymbols() []Value { + if vals, ok := p.proxyOwnKeys(); ok { + return p.filterKeys(vals, true, true) + } + + return p.target.self.ownSymbols() +} + +func (p *proxyObject) className() string { + if p.target == nil { + panic(p.val.runtime.NewTypeError("proxy has been revoked")) + } + if p.call != nil || p.ctor != nil { + return classFunction + } + return classObject +} + +func (p *proxyObject) revoke() { + p.handler = nil + p.target = nil +} diff --git a/regexp.go b/regexp.go index b50b56dd..df57847b 100644 --- a/regexp.go +++ b/regexp.go @@ -294,14 +294,14 @@ func (r *regexpObject) execResultToArray(target valueString, result []int) Value } } match := r.val.runtime.newArrayValues(valueArray) - match.self.putStr("input", target, false) - match.self.putStr("index", intToValue(int64(matchIndex)), false) + match.self.setOwnStr("input", target, false) + match.self.setOwnStr("index", intToValue(int64(matchIndex)), false) return match } func (r *regexpObject) execRegexp(target valueString) (match bool, result []int) { lastIndex := int64(0) - if p := r.getStr("lastIndex"); p != nil { + if p := r.getStr("lastIndex", nil); p != nil { lastIndex = p.ToInteger() if lastIndex < 0 { lastIndex = 0 @@ -315,7 +315,7 @@ func (r *regexpObject) execRegexp(target valueString) (match bool, result []int) result = r.pattern.FindSubmatchIndex(target, int(index)) } if result == nil || r.sticky && result[0] != 0 { - r.putStr("lastIndex", intToValue(0), true) + r.setOwnStr("lastIndex", intToValue(0), true) return } match = true @@ -325,7 +325,7 @@ func (r *regexpObject) execRegexp(target valueString) (match bool, result []int) result[i] += int(index) } if r.global || r.sticky { - r.putStr("lastIndex", intToValue(int64(result[1])), true) + r.setOwnStr("lastIndex", intToValue(int64(result[1])), true) } return } diff --git a/runtime.go b/runtime.go index d072973d..28c94d18 100644 --- a/runtime.go +++ b/runtime.go @@ -15,6 +15,7 @@ import ( js_ast "github.com/dop251/goja/ast" "github.com/dop251/goja/parser" + "runtime" ) const ( @@ -24,6 +25,7 @@ const ( var ( typeCallable = reflect.TypeOf(Callable(nil)) typeValue = reflect.TypeOf((*Value)(nil)).Elem() + typeObject = reflect.TypeOf((*Object)(nil)) typeTime = reflect.TypeOf(time.Time{}) ) @@ -45,6 +47,7 @@ type global struct { RegExp *Object Date *Object Symbol *Object + Proxy *Object ArrayBuffer *Object WeakSet *Object @@ -146,17 +149,40 @@ type Runtime struct { vm *vm } -type stackFrame struct { +type StackFrame struct { prg *Program funcName string pc int } -func (f *stackFrame) position() Position { +func (f *StackFrame) SrcName() string { + if f.prg == nil { + return "" + } + return f.prg.src.name +} + +func (f *StackFrame) FuncName() string { + if f.funcName == "" && f.prg == nil { + return "" + } + if f.funcName == "" { + return "" + } + return f.funcName +} + +func (f *StackFrame) Position() Position { + if f.prg == nil || f.prg.src == nil { + return Position{ + 0, + 0, + } + } return f.prg.src.Position(f.prg.sourceOffset(f.pc)) } -func (f *stackFrame) write(b *bytes.Buffer) { +func (f *StackFrame) Write(b *bytes.Buffer) { if f.prg != nil { if n := f.prg.funcName; n != "" { b.WriteString(n) @@ -168,7 +194,7 @@ func (f *stackFrame) write(b *bytes.Buffer) { b.WriteString("") } b.WriteByte(':') - b.WriteString(f.position().String()) + b.WriteString(f.Position().String()) b.WriteByte('(') b.WriteString(strconv.Itoa(f.pc)) b.WriteByte(')') @@ -189,7 +215,7 @@ func (f *stackFrame) write(b *bytes.Buffer) { type Exception struct { val Value - stack []stackFrame + stack []StackFrame } type InterruptedError struct { @@ -227,7 +253,7 @@ func (e *InterruptedError) Error() string { func (e *Exception) writeFullStack(b *bytes.Buffer) { for _, frame := range e.stack { b.WriteString("\tat ") - frame.write(b) + frame.Write(b) b.WriteByte('\n') } } @@ -235,7 +261,7 @@ func (e *Exception) writeFullStack(b *bytes.Buffer) { func (e *Exception) writeShortStack(b *bytes.Buffer) { if len(e.stack) > 0 && (e.stack[0].prg != nil || e.stack[0].funcName != "") { b.WriteString(" at ") - e.stack[0].write(b) + e.stack[0].Write(b) } } @@ -273,7 +299,7 @@ func (r *Runtime) addToGlobal(name string, value Value) { func (r *Runtime) createIterProto(val *Object) objectImpl { o := newBaseObjectObj(val, r.global.ObjectPrototype, classObject) - o.put(symIterator, valueProp(r.newNativeFunc(r.returnThis, nil, "[Symbol.iterator]", nil, 0), true, false, true), true) + o._putSym(symIterator, valueProp(r.newNativeFunc(r.returnThis, nil, "[Symbol.iterator]", nil, 0), true, false, true)) return o } @@ -299,6 +325,8 @@ func (r *Runtime) init() { r.initRegExp() r.initDate() r.initBoolean() + r.initProxy() + r.initReflect() r.initErrors() @@ -341,7 +369,7 @@ func (r *Runtime) throwReferenceError(name string) { } func (r *Runtime) newSyntaxError(msg string, offset int) Value { - return r.builtin_new((r.global.SyntaxError), []Value{newStringValue(msg)}) + return r.builtin_new(r.global.SyntaxError, []Value{newStringValue(msg)}) } func newBaseObjectObj(obj, proto *Object, class string) *baseObject { @@ -402,7 +430,7 @@ func (r *Runtime) newFunc(name string, len int, strict bool) (f *funcObject) { return } -func (r *Runtime) newNativeFuncObj(v *Object, call func(FunctionCall) Value, construct func(args []Value) *Object, name string, proto *Object, length int) *nativeFuncObject { +func (r *Runtime) newNativeFuncObj(v *Object, call func(FunctionCall) Value, construct func(args []Value, proto *Object) *Object, name string, proto *Object, length int) *nativeFuncObject { f := &nativeFuncObject{ baseFuncObject: baseFuncObject{ baseObject: baseObject{ @@ -413,7 +441,7 @@ func (r *Runtime) newNativeFuncObj(v *Object, call func(FunctionCall) Value, con }, }, f: call, - construct: construct, + construct: r.wrapNativeConstruct(construct, proto), } v.self = f f.init(name, length) @@ -441,7 +469,7 @@ func (r *Runtime) newNativeConstructor(call func(ConstructorCall) *Object, name return f.defaultConstruct(call, c.Arguments) } - f.construct = func(args []Value) *Object { + f.construct = func(args []Value, proto *Object) *Object { return f.defaultConstruct(call, args) } @@ -455,7 +483,7 @@ func (r *Runtime) newNativeConstructor(call func(ConstructorCall) *Object, name return v } -func (r *Runtime) newNativeFunc(call func(FunctionCall) Value, construct func(args []Value) *Object, name string, proto *Object, length int) *Object { +func (r *Runtime) newNativeFunc(call func(FunctionCall) Value, construct func(args []Value, proto *Object) *Object, name string, proto *Object, length int) *Object { v := &Object{runtime: r} f := &nativeFuncObject{ @@ -468,7 +496,7 @@ func (r *Runtime) newNativeFunc(call func(FunctionCall) Value, construct func(ar }, }, f: call, - construct: construct, + construct: r.wrapNativeConstruct(construct, proto), } v.self = f f.init(name, length) @@ -489,10 +517,8 @@ func (r *Runtime) newNativeFuncConstructObj(v *Object, construct func(args []Val prototype: r.global.FunctionPrototype, }, }, - f: r.constructWrap(construct, proto), - construct: func(args []Value) *Object { - return construct(args, proto) - }, + f: r.constructToCall(construct, proto), + construct: r.wrapNativeConstruct(construct, proto), } f.init(name, length) @@ -515,10 +541,8 @@ func (r *Runtime) newNativeFuncConstructProto(construct func(args []Value, proto f.extensible = true v.self = f f.prototype = proto - f.f = r.constructWrap(construct, prototype) - f.construct = func(args []Value) *Object { - return construct(args, prototype) - } + f.f = r.constructToCall(construct, prototype) + f.construct = r.wrapNativeConstruct(construct, prototype) f.init(name, length) if prototype != nil { f._putProp("prototype", prototype, false, false, false) @@ -545,18 +569,18 @@ func (r *Runtime) builtin_Number(call FunctionCall) Value { if len(call.Arguments) > 0 { return call.Arguments[0].ToNumber() } else { - return intToValue(0) + return valueInt(0) } } -func (r *Runtime) builtin_newNumber(args []Value) *Object { +func (r *Runtime) builtin_newNumber(args []Value, proto *Object) *Object { var v Value if len(args) > 0 { v = args[0].ToNumber() } else { v = intToValue(0) } - return r.newPrimitiveObject(v, r.global.NumberPrototype, classNumber) + return r.newPrimitiveObject(v, proto, classNumber) } func (r *Runtime) builtin_Boolean(call FunctionCall) Value { @@ -571,7 +595,7 @@ func (r *Runtime) builtin_Boolean(call FunctionCall) Value { } } -func (r *Runtime) builtin_newBoolean(args []Value) *Object { +func (r *Runtime) builtin_newBoolean(args []Value, proto *Object) *Object { var v Value if len(args) > 0 { if args[0].ToBoolean() { @@ -582,13 +606,13 @@ func (r *Runtime) builtin_newBoolean(args []Value) *Object { } else { v = valueFalse } - return r.newPrimitiveObject(v, r.global.BooleanPrototype, classBoolean) + return r.newPrimitiveObject(v, proto, classBoolean) } func (r *Runtime) error_toString(call FunctionCall) Value { obj := call.This.ToObject(r).self - msg := obj.getStr("message") - name := obj.getStr("name") + msg := obj.getStr("message", nil) + name := obj.getStr("name", nil) var nameStr, msgStr string if name != nil && name != _undefined { nameStr = name.String() @@ -615,33 +639,8 @@ func (r *Runtime) builtin_Error(args []Value, proto *Object) *Object { return obj.val } -func getConstructor(construct *Object) func(args []Value) *Object { -repeat: - switch f := construct.self.(type) { - case *nativeFuncObject: - if f.construct != nil { - return f.construct - } - case *boundFuncObject: - if f.construct != nil { - return f.construct - } - case *funcObject: - return f.construct - case *lazyObject: - construct.self = f.create(construct) - goto repeat - } - - return nil -} - func (r *Runtime) builtin_new(construct *Object, args []Value) *Object { - f := getConstructor(construct) - if f == nil { - panic("Not a constructor") - } - return f(args) + return r.toConstructor(construct)(args, nil) } func (r *Runtime) throw(e Value) { @@ -687,18 +686,33 @@ func (r *Runtime) builtin_eval(call FunctionCall) Value { if len(call.Arguments) == 0 { return _undefined } - if str, ok := call.Arguments[0].assertString(); ok { + if str, ok := call.Arguments[0].(valueString); ok { return r.eval(str.String(), false, false, r.globalObject) } return call.Arguments[0] } -func (r *Runtime) constructWrap(construct func(args []Value, proto *Object) *Object, proto *Object) func(call FunctionCall) Value { +func (r *Runtime) constructToCall(construct func(args []Value, proto *Object) *Object, proto *Object) func(call FunctionCall) Value { return func(call FunctionCall) Value { return construct(call.Arguments, proto) } } +func (r *Runtime) wrapNativeConstruct(c func(args []Value, proto *Object) *Object, proto *Object) func(args []Value, newTarget *Object) *Object { + if c == nil { + return nil + } + return func(args []Value, newTarget *Object) *Object { + var p *Object + if newTarget != nil { + p = r.toObject(newTarget.self.getStr("prototype", nil)) + } else { + p = proto + } + return c(args, p) + } +} + func (r *Runtime) toCallable(v Value) func(FunctionCall) Value { if call, ok := r.toObject(v).self.assertCallable(); ok { return call @@ -716,11 +730,12 @@ func (r *Runtime) checkObjectCoercible(v Value) { func toUInt32(v Value) uint32 { v = v.ToNumber() - if i, ok := v.assertInt(); ok { + if i, ok := v.(valueInt); ok { return uint32(i) } - if f, ok := v.assertFloat(); ok { + if f, ok := v.(valueFloat); ok { + f := float64(f) if !math.IsNaN(f) && !math.IsInf(f, 0) { return uint32(int64(f)) } @@ -730,11 +745,12 @@ func toUInt32(v Value) uint32 { func toUInt16(v Value) uint16 { v = v.ToNumber() - if i, ok := v.assertInt(); ok { + if i, ok := v.(valueInt); ok { return uint16(i) } - if f, ok := v.assertFloat(); ok { + if f, ok := v.(valueFloat); ok { + f := float64(f) if !math.IsNaN(f) && !math.IsInf(f, 0) { return uint16(int64(f)) } @@ -758,11 +774,12 @@ func toLength(v Value) int64 { func toInt32(v Value) int32 { v = v.ToNumber() - if i, ok := v.assertInt(); ok { + if i, ok := v.(valueInt); ok { return int32(i) } - if f, ok := v.assertFloat(); ok { + if f, ok := v.(valueFloat); ok { + f := float64(f) if !math.IsNaN(f) && !math.IsInf(f, 0) { return int32(int64(f)) } @@ -928,6 +945,18 @@ func (r *Runtime) RunProgram(p *Program) (result Value, err error) { return } +func (r *Runtime) CaptureCallStack(n int) []StackFrame { + m := len(r.vm.callStack) + if n > 0 { + m -= m - n + } else { + m = 0 + } + stackFrames := make([]StackFrame, 0) + stackFrames = r.vm.captureStack(stackFrames, m) + return stackFrames +} + // Interrupt a running JavaScript. The corresponding Go call will return an *InterruptedError containing v. // Note, it only works while in JavaScript code, it does not interrupt native Go functions (which includes all built-ins). // If the runtime is currently not running, it will be immediately interrupted on the next Run*() call. @@ -975,8 +1004,15 @@ func (r *Runtime) ToValue(i interface{}) Value { switch i := i.(type) { case nil: return _null + case *Object: + if i == nil || i.runtime == nil { + return _null + } + if i.runtime != r { + panic(r.NewTypeError("Illegal runtime transition of an Object")) + } + return i case Value: - // TODO: prevent importing Objects from a different runtime return i case string: return newStringValue(i) @@ -987,9 +1023,20 @@ func (r *Runtime) ToValue(i interface{}) Value { return valueFalse } case func(FunctionCall) Value: - return r.newNativeFunc(i, nil, "", nil, 0) + name := runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name() + return r.newNativeFunc(i, nil, name, nil, 0) case func(ConstructorCall) *Object: - return r.newNativeConstructor(i, "", 0) + name := runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name() + return r.newNativeConstructor(i, name, 0) + case *Proxy: + if i == nil { + return _null + } + proxy := i.proxy.val + if proxy.runtime != r { + panic(r.NewTypeError("Illegal runtime transition of a Proxy")) + } + return proxy case int: return intToValue(int64(i)) case int8: @@ -1116,7 +1163,8 @@ func (r *Runtime) ToValue(i interface{}) Value { obj.self = a return obj case reflect.Func: - return r.newNativeFunc(r.wrapReflectFunc(value), nil, "", nil, value.Type().NumIn()) + name := runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name() + return r.newNativeFunc(r.wrapReflectFunc(value), nil, name, nil, value.Type().NumIn()) } obj := &Object{runtime: r} @@ -1236,34 +1284,34 @@ func (r *Runtime) toReflectValue(v Value, typ reflect.Type) (reflect.Value, erro case reflect.Bool: return reflect.ValueOf(v.ToBoolean()).Convert(typ), nil case reflect.Int: - i, _ := toInt(v) + i, _ := toInt64(v) return reflect.ValueOf(int(i)).Convert(typ), nil case reflect.Int64: - i, _ := toInt(v) + i, _ := toInt64(v) return reflect.ValueOf(i).Convert(typ), nil case reflect.Int32: - i, _ := toInt(v) + i, _ := toInt64(v) return reflect.ValueOf(int32(i)).Convert(typ), nil case reflect.Int16: - i, _ := toInt(v) + i, _ := toInt64(v) return reflect.ValueOf(int16(i)).Convert(typ), nil case reflect.Int8: - i, _ := toInt(v) + i, _ := toInt64(v) return reflect.ValueOf(int8(i)).Convert(typ), nil case reflect.Uint: - i, _ := toInt(v) + i, _ := toInt64(v) return reflect.ValueOf(uint(i)).Convert(typ), nil case reflect.Uint64: - i, _ := toInt(v) + i, _ := toInt64(v) return reflect.ValueOf(uint64(i)).Convert(typ), nil case reflect.Uint32: - i, _ := toInt(v) + i, _ := toInt64(v) return reflect.ValueOf(uint32(i)).Convert(typ), nil case reflect.Uint16: - i, _ := toInt(v) + i, _ := toInt64(v) return reflect.ValueOf(uint16(i)).Convert(typ), nil case reflect.Uint8: - i, _ := toInt(v) + i, _ := toInt64(v) return reflect.ValueOf(uint8(i)).Convert(typ), nil } @@ -1273,12 +1321,18 @@ func (r *Runtime) toReflectValue(v Value, typ reflect.Type) (reflect.Value, erro } } - if typ.Implements(typeValue) { + if typeValue.AssignableTo(typ) { return reflect.ValueOf(v), nil } + if typeObject.AssignableTo(typ) { + if obj, ok := v.(*Object); ok { + return reflect.ValueOf(obj), nil + } + } + et := v.ExportType() - if et == nil { + if et == nil || et == reflectTypeNil { return reflect.Zero(typ), nil } if et.AssignableTo(typ) { @@ -1299,11 +1353,11 @@ func (r *Runtime) toReflectValue(v Value, typ reflect.Type) (reflect.Value, erro case reflect.Slice: if o, ok := v.(*Object); ok { if o.self.className() == classArray { - l := int(toLength(o.self.getStr("length"))) + l := int(toLength(o.self.getStr("length", nil))) s := reflect.MakeSlice(typ, l, l) elemTyp := typ.Elem() for i := 0; i < l; i++ { - item := o.self.get(intToValue(int64(i))) + item := o.self.getIdx(valueInt(int64(i)), nil) itemval, err := r.toReflectValue(item, elemTyp) if err != nil { return reflect.Value{}, fmt.Errorf("Could not convert array element %v to %v at %d: %s", v, typ, i, err) @@ -1319,31 +1373,29 @@ func (r *Runtime) toReflectValue(v Value, typ reflect.Type) (reflect.Value, erro keyTyp := typ.Key() elemTyp := typ.Elem() needConvertKeys := !reflect.ValueOf("").Type().AssignableTo(keyTyp) - for item, f := o.self.enumerate(false, false)(); f != nil; item, f = f() { + for _, itemName := range o.self.ownKeys(false, nil) { var kv reflect.Value var err error if needConvertKeys { - kv, err = r.toReflectValue(newStringValue(item.name), keyTyp) + kv, err = r.toReflectValue(itemName, keyTyp) if err != nil { - return reflect.Value{}, fmt.Errorf("Could not convert map key %s to %v", item.name, typ) + return reflect.Value{}, fmt.Errorf("Could not convert map key %s to %v", itemName.String(), typ) } } else { - kv = reflect.ValueOf(item.name) + kv = reflect.ValueOf(itemName.String()) } - ival := item.value - if ival == nil { - ival = o.self.getStr(item.name) - } + ival := o.get(itemName, nil) if ival != nil { vv, err := r.toReflectValue(ival, elemTyp) if err != nil { - return reflect.Value{}, fmt.Errorf("Could not convert map value %v to %v at key %s", ival, typ, item.name) + return reflect.Value{}, fmt.Errorf("Could not convert map value %v to %v at key %s", ival, typ, itemName.String()) } m.SetMapIndex(kv, vv) } else { m.SetMapIndex(kv, reflect.Zero(elemTyp)) } + } return m, nil } @@ -1361,7 +1413,7 @@ func (r *Runtime) toReflectValue(v Value, typ reflect.Type) (reflect.Value, erro if field.Anonymous { v = o } else { - v = o.self.getStr(name) + v = o.self.getStr(name, nil) } if v != nil { @@ -1393,7 +1445,7 @@ func (r *Runtime) toReflectValue(v Value, typ reflect.Type) (reflect.Value, erro return ptrVal, nil } - return reflect.Value{}, fmt.Errorf("Could not convert %v to %v", v, typ) + return reflect.Value{}, fmt.Errorf("could not convert %v to %v", v, typ) } func (r *Runtime) wrapJSFunc(fn Callable, typ reflect.Type) func(args []reflect.Value) (results []reflect.Value) { @@ -1452,12 +1504,12 @@ func (r *Runtime) GlobalObject() *Object { // Set the specified value as a property of the global object. // The value is first converted using ToValue() func (r *Runtime) Set(name string, value interface{}) { - r.globalObject.self.putStr(name, r.ToValue(value), false) + r.globalObject.self.setOwnStr(name, r.ToValue(value), false) } // Get the specified property of the global object. func (r *Runtime) Get(name string) Value { - return r.globalObject.self.getStr(name) + return r.globalObject.self.getStr(name, nil) } // SetRandSource sets random source for this Runtime. If not called, the default math/rand is used. @@ -1535,8 +1587,8 @@ func IsNull(v Value) bool { // IsNaN returns true if the supplied value is NaN. func IsNaN(v Value) bool { - f, ok := v.assertFloat() - return ok && math.IsNaN(f) + f, ok := v.(valueFloat) + return ok && math.IsNaN(float64(f)) } // IsInfinity returns true if the supplied is (+/-)Infinity @@ -1609,23 +1661,23 @@ func (r *Runtime) toObject(v Value, args ...interface{}) *Object { } } -func (r *Runtime) speciesConstructor(o, defaultConstructor *Object) func(args []Value) *Object { - c := o.self.getStr("constructor") +func (r *Runtime) speciesConstructor(o, defaultConstructor *Object) func(args []Value, newTarget *Object) *Object { + c := o.self.getStr("constructor", nil) if c != nil && c != _undefined { - c = r.toObject(c).self.get(symSpecies) + c = r.toObject(c).self.getSym(symSpecies, nil) } if c == nil || c == _undefined { c = defaultConstructor } - return getConstructor(r.toObject(c)) + return r.toConstructor(c) } func (r *Runtime) returnThis(call FunctionCall) Value { return call.This } -func defineDataPropertyOrThrow(o *Object, p Value, v Value) { - o.self.defineOwnProperty(p, propertyDescr{ +func createDataPropertyOrThrow(o *Object, p Value, v Value) { + o.defineOwnProperty(p, PropertyDescriptor{ Writable: FLAG_TRUE, Enumerable: FLAG_TRUE, Configurable: FLAG_TRUE, @@ -1639,20 +1691,12 @@ func toPropertyKey(key Value) Value { func (r *Runtime) getVStr(v Value, p string) Value { o := v.ToObject(r) - prop := o.self.getPropStr(p) - if prop, ok := prop.(*valueProperty); ok { - return prop.get(v) - } - return prop + return o.self.getStr(p, v) } func (r *Runtime) getV(v Value, p Value) Value { o := v.ToObject(r) - prop := o.self.getProp(p) - if prop, ok := prop.(*valueProperty); ok { - return prop.get(v) - } - return prop + return o.get(p, v) } func (r *Runtime) getIterator(obj Value, method func(FunctionCall) Value) *Object { @@ -1670,15 +1714,15 @@ func (r *Runtime) getIterator(obj Value, method func(FunctionCall) Value) *Objec func (r *Runtime) iterate(iter *Object, step func(Value)) { for { - res := r.toObject(toMethod(iter.self.getStr("next"))(FunctionCall{This: iter})) - if nilSafe(res.self.getStr("done")).ToBoolean() { + res := r.toObject(toMethod(iter.self.getStr("next", nil))(FunctionCall{This: iter})) + if nilSafe(res.self.getStr("done", nil)).ToBoolean() { break } err := tryFunc(func() { - step(nilSafe(res.self.getStr("value"))) + step(nilSafe(res.self.getStr("value", nil))) }) if err != nil { - retMethod := toMethod(iter.self.getStr("return")) + retMethod := toMethod(iter.self.getStr("return", nil)) if retMethod != nil { _ = tryFunc(func() { retMethod(FunctionCall{This: iter}) @@ -1691,8 +1735,8 @@ func (r *Runtime) iterate(iter *Object, step func(Value)) { func (r *Runtime) createIterResultObject(value Value, done bool) Value { o := r.NewObject() - o.self.putStr("value", value, false) - o.self.putStr("done", r.toBoolean(done), false) + o.self.setOwnStr("value", value, false) + o.self.setOwnStr("done", r.toBoolean(done), false) return o } @@ -1718,3 +1762,19 @@ func nilSafe(v Value) Value { } return _undefined } + +func isArray(object *Object) bool { + self := object.self + if proxy, ok := self.(*proxyObject); ok { + if proxy.target == nil { + panic(typeError("Cannot perform 'IsArray' on a proxy that has been revoked")) + } + return isArray(proxy.target) + } + switch self.className() { + case classArray: + return true + default: + return false + } +} diff --git a/runtime_test.go b/runtime_test.go index 372a1a04..04dac30c 100644 --- a/runtime_test.go +++ b/runtime_test.go @@ -1356,6 +1356,75 @@ func TestPrimThisValueGetter(t *testing.T) { testScript1(TESTLIB+SCRIPT, _undefined, t) } +func TestObjSetSym(t *testing.T) { + const SCRIPT = ` + 'use strict'; + var sym = Symbol(true); + var p1 = Object.create(null); + var p2 = Object.create(p1); + + Object.defineProperty(p1, sym, { + value: 42 + }); + + Object.defineProperty(p2, sym, { + value: 43, + writable: true, + }); + var o = Object.create(p2); + o[sym] = 44; + o[sym]; + ` + testScript1(SCRIPT, intToValue(44), t) +} + +func TestObjSet(t *testing.T) { + const SCRIPT = ` + 'use strict'; + var p1 = Object.create(null); + var p2 = Object.create(p1); + + Object.defineProperty(p1, "test", { + value: 42 + }); + + Object.defineProperty(p2, "test", { + value: 43, + writable: true, + }); + var o = Object.create(p2); + o.test = 44; + o.test; + ` + testScript1(SCRIPT, intToValue(44), t) +} + +func TestToValueNilValue(t *testing.T) { + r := New() + var a Value + r.Set("a", a) + ret, err := r.RunString(` + ""+a; + `) + if err != nil { + t.Fatal(err) + } + if !asciiString("null").SameAs(ret) { + t.Fatalf("ret: %v", ret) + } +} + +func TestNativeCtorNewTarget(t *testing.T) { + const SCRIPT = ` + function NewTarget() { + } + + var o = Reflect.construct(Number, [1], NewTarget); + o.__proto__ === NewTarget.prototype && o.toString() === "[object Number]"; + ` + testScript1(SCRIPT, valueTrue, t) +} + /* func TestArrayConcatSparse(t *testing.T) { function foo(a,b,c) diff --git a/string.go b/string.go index 35175099..58b0249f 100644 --- a/string.go +++ b/string.go @@ -27,7 +27,6 @@ var ( stringPlusInfinity = asciiString("+Infinity") stringNegInfinity = asciiString("-Infinity") stringEmpty valueString = asciiString("") - string__proto__ valueString = asciiString(__proto__) stringError valueString = asciiString("Error") stringTypeError valueString = asciiString("TypeError") @@ -93,37 +92,27 @@ func (s *stringObject) setLength() { s._put("length", &s.lengthProp) } -func (s *stringObject) get(n Value) Value { - if idx := toIdx(n); idx >= 0 && idx < s.length { - return s.getIdx(idx) +func (s *stringObject) getStr(name string, receiver Value) Value { + if i := strToIdx64(name); i >= 0 && i < s.length { + return s._getIdx(i) } - return s.baseObject.get(n) + return s.baseObject.getStr(name, receiver) } -func (s *stringObject) getStr(name string) Value { - if i := strToIdx(name); i >= 0 && i < s.length { - return s.getIdx(i) - } - return s.baseObject.getStr(name) -} - -func (s *stringObject) getPropStr(name string) Value { - if i := strToIdx(name); i >= 0 && i < s.length { - return s.getIdx(i) - } - return s.baseObject.getPropStr(name) -} - -func (s *stringObject) getProp(n Value) Value { - if i := toIdx(n); i >= 0 && i < s.length { - return s.getIdx(i) +func (s *stringObject) getIdx(idx valueInt, receiver Value) Value { + i := int64(idx) + if i >= 0 { + if i < s.length { + return s._getIdx(i) + } + return nil } - return s.baseObject.getProp(n) + return s.baseObject.getStr(idx.String(), receiver) } func (s *stringObject) getOwnPropStr(name string) Value { - if i := strToIdx(name); i >= 0 && i < s.length { - val := s.getIdx(i) + if i := strToIdx64(name); i >= 0 && i < s.length { + val := s._getIdx(i) return &valueProperty{ value: val, enumerable: true, @@ -133,42 +122,76 @@ func (s *stringObject) getOwnPropStr(name string) Value { return s.baseObject.getOwnPropStr(name) } -func (s *stringObject) getIdx(idx int64) Value { +func (s *stringObject) getOwnPropIdx(idx valueInt) Value { + i := int64(idx) + if i >= 0 { + if i < s.length { + val := s._getIdx(i) + return &valueProperty{ + value: val, + enumerable: true, + } + } + return nil + } + + return s.baseObject.getOwnPropStr(idx.String()) +} + +func (s *stringObject) _getIdx(idx int64) Value { return s.value.substring(idx, idx+1) } -func (s *stringObject) put(n Value, val Value, throw bool) { - if i := toIdx(n); i >= 0 && i < s.length { +func (s *stringObject) setOwnStr(name string, val Value, throw bool) bool { + if i := strToIdx64(name); i >= 0 && i < s.length { s.val.runtime.typeErrorResult(throw, "Cannot assign to read only property '%d' of a String", i) - return + return false } - s.baseObject.put(n, val, throw) + return s.baseObject.setOwnStr(name, val, throw) } -func (s *stringObject) putStr(name string, val Value, throw bool) { - if i := strToIdx(name); i >= 0 && i < s.length { +func (s *stringObject) setOwnIdx(idx valueInt, val Value, throw bool) bool { + i := int64(idx) + if i >= 0 && i < s.length { s.val.runtime.typeErrorResult(throw, "Cannot assign to read only property '%d' of a String", i) - return + return false } - s.baseObject.putStr(name, val, throw) + return s.baseObject.setOwnStr(idx.String(), val, throw) +} + +func (s *stringObject) setForeignStr(name string, val, receiver Value, throw bool) (bool, bool) { + return s._setForeignStr(name, s.getOwnPropStr(name), val, receiver, throw) } -func (s *stringObject) defineOwnProperty(n Value, descr propertyDescr, throw bool) bool { - if i := toIdx(n); i >= 0 && i < s.length { +func (s *stringObject) setForeignIdx(idx valueInt, val, receiver Value, throw bool) (bool, bool) { + return s._setForeignIdx(idx, s.getOwnPropIdx(idx), val, receiver, throw) +} + +func (s *stringObject) defineOwnPropertyStr(name string, descr PropertyDescriptor, throw bool) bool { + if i := strToIdx64(name); i >= 0 && i < s.length { + s.val.runtime.typeErrorResult(throw, "Cannot redefine property: %d", i) + return false + } + + return s.baseObject.defineOwnPropertyStr(name, descr, throw) +} + +func (s *stringObject) defineOwnPropertyIdx(idx valueInt, descr PropertyDescriptor, throw bool) bool { + i := int64(idx) + if i >= 0 && i < s.length { s.val.runtime.typeErrorResult(throw, "Cannot redefine property: %d", i) return false } - return s.baseObject.defineOwnProperty(n, descr, throw) + return s.baseObject.defineOwnPropertyStr(idx.String(), descr, throw) } type stringPropIter struct { str valueString // separate, because obj can be the singleton obj *stringObject idx, length int64 - recursive bool } func (i *stringPropIter) next() (propIterItem, iterNextFunc) { @@ -178,28 +201,27 @@ func (i *stringPropIter) next() (propIterItem, iterNextFunc) { return propIterItem{name: name, enumerable: _ENUM_TRUE}, i.next } - return i.obj.baseObject._enumerate(i.recursive)() + return i.obj.baseObject.enumerateUnfiltered()() } -func (s *stringObject) _enumerate(recursive bool) iterNextFunc { +func (s *stringObject) enumerateUnfiltered() iterNextFunc { return (&stringPropIter{ - str: s.value, - obj: s, - length: s.length, - recursive: recursive, + str: s.value, + obj: s, + length: s.length, }).next } -func (s *stringObject) enumerate(all, recursive bool) iterNextFunc { - return (&propFilterIter{ - wrapped: s._enumerate(recursive), - all: all, - seen: make(map[string]bool), - }).next +func (s *stringObject) ownKeys(all bool, accum []Value) []Value { + for i := int64(0); i < s.length; i++ { + accum = append(accum, asciiString(strconv.FormatInt(i, 10))) + } + + return s.baseObject.ownKeys(all, accum) } func (s *stringObject) deleteStr(name string, throw bool) bool { - if i := strToIdx(name); i >= 0 && i < s.length { + if i := strToIdx64(name); i >= 0 && i < s.length { s.val.runtime.typeErrorResult(throw, "Cannot delete property '%d' of a String", i) return false } @@ -207,25 +229,27 @@ func (s *stringObject) deleteStr(name string, throw bool) bool { return s.baseObject.deleteStr(name, throw) } -func (s *stringObject) delete(n Value, throw bool) bool { - if i := toIdx(n); i >= 0 && i < s.length { +func (s *stringObject) deleteIdx(idx valueInt, throw bool) bool { + i := int64(idx) + if i >= 0 && i < s.length { s.val.runtime.typeErrorResult(throw, "Cannot delete property '%d' of a String", i) return false } - return s.baseObject.delete(n, throw) + return s.baseObject.deleteStr(idx.String(), throw) } -func (s *stringObject) hasOwnProperty(n Value) bool { - if i := toIdx(n); i >= 0 && i < s.length { +func (s *stringObject) hasOwnPropertyStr(name string) bool { + if i := strToIdx64(name); i >= 0 && i < s.length { return true } - return s.baseObject.hasOwnProperty(n) + return s.baseObject.hasOwnPropertyStr(name) } -func (s *stringObject) hasOwnPropertyStr(name string) bool { - if i := strToIdx(name); i >= 0 && i < s.length { +func (s *stringObject) hasOwnPropertyIdx(idx valueInt) bool { + i := int64(idx) + if i >= 0 && i < s.length { return true } - return s.baseObject.hasOwnPropertyStr(name) + return s.baseObject.hasOwnPropertyStr(idx.String()) } diff --git a/string_ascii.go b/string_ascii.go index 16270a8a..8cbd443e 100644 --- a/string_ascii.go +++ b/string_ascii.go @@ -164,7 +164,7 @@ func (s asciiString) ToNumber() Value { } func (s asciiString) ToObject(r *Runtime) *Object { - return r._newString(s) + return r._newString(s, r.global.StringPrototype) } func (s asciiString) SameAs(other Value) bool { @@ -179,15 +179,15 @@ func (s asciiString) Equals(other Value) bool { return s == o } - if o, ok := other.assertInt(); ok { + if o, ok := other.(valueInt); ok { if o1, e := s._toInt(); e == nil { - return o1 == o + return o1 == int64(o) } return false } - if o, ok := other.assertFloat(); ok { - return s.ToFloat() == o + if o, ok := other.(valueFloat); ok { + return s.ToFloat() == float64(o) } if o, ok := other.(valueBool); ok { @@ -210,18 +210,6 @@ func (s asciiString) StrictEquals(other Value) bool { return false } -func (s asciiString) assertInt() (int64, bool) { - return 0, false -} - -func (s asciiString) assertFloat() (float64, bool) { - return 0, false -} - -func (s asciiString) assertString() (valueString, bool) { - return s, true -} - func (s asciiString) baseObject(r *Runtime) *Object { ss := r.stringSingleton ss.value = s diff --git a/string_unicode.go b/string_unicode.go index 06b1912a..b3cd74d6 100644 --- a/string_unicode.go +++ b/string_unicode.go @@ -109,7 +109,7 @@ func (s unicodeString) ToNumber() Value { } func (s unicodeString) ToObject(r *Runtime) *Object { - return r._newString(s) + return r._newString(s, r.global.StringPrototype) } func (s unicodeString) equals(other unicodeString) bool { @@ -137,18 +137,6 @@ func (s unicodeString) Equals(other Value) bool { return true } - if _, ok := other.assertInt(); ok { - return false - } - - if _, ok := other.assertFloat(); ok { - return false - } - - if _, ok := other.(valueBool); ok { - return false - } - if o, ok := other.(*Object); ok { return s.Equals(o.self.toPrimitive()) } @@ -159,18 +147,6 @@ func (s unicodeString) StrictEquals(other Value) bool { return s.SameAs(other) } -func (s unicodeString) assertInt() (int64, bool) { - return 0, false -} - -func (s unicodeString) assertFloat() (float64, bool) { - return 0, false -} - -func (s unicodeString) assertString() (valueString, bool) { - return s, true -} - func (s unicodeString) baseObject(r *Runtime) *Object { ss := r.stringSingleton ss.value = s diff --git a/tc39_test.go b/tc39_test.go index d3920cb3..6bffdd35 100644 --- a/tc39_test.go +++ b/tc39_test.go @@ -26,7 +26,6 @@ var ( "test/language/literals/regexp/S7.8.5_A1.4_T2.js": true, // UTF-16 "test/language/literals/regexp/S7.8.5_A2.1_T2.js": true, // UTF-16 "test/language/literals/regexp/S7.8.5_A2.4_T2.js": true, // UTF-16 - "test/built-ins/Object/getOwnPropertyNames/15.2.3.4-4-44.js": true, // property order "test/built-ins/Date/prototype/toISOString/15.9.5.43-0-8.js": true, // timezone "test/built-ins/Date/prototype/toISOString/15.9.5.43-0-9.js": true, // timezone "test/built-ins/Date/prototype/toISOString/15.9.5.43-0-10.js": true, // timezone @@ -36,36 +35,62 @@ var ( "test/built-ins/Array/prototype/concat/Array.prototype.concat_spreadable-string-wrapper.js": true, // cross-realm - "test/built-ins/Symbol/unscopables/cross-realm.js": true, - "test/built-ins/Symbol/toStringTag/cross-realm.js": true, - "test/built-ins/Symbol/toPrimitive/cross-realm.js": true, - "test/built-ins/Symbol/split/cross-realm.js": true, - "test/built-ins/Symbol/species/cross-realm.js": true, - "test/built-ins/Symbol/search/cross-realm.js": true, - "test/built-ins/Symbol/replace/cross-realm.js": true, - "test/built-ins/Symbol/match/cross-realm.js": true, - "test/built-ins/Symbol/keyFor/cross-realm.js": true, - "test/built-ins/Symbol/iterator/cross-realm.js": true, - "test/built-ins/Symbol/isConcatSpreadable/cross-realm.js": true, - "test/built-ins/Symbol/hasInstance/cross-realm.js": true, - "test/built-ins/Symbol/for/cross-realm.js": true, - "test/built-ins/WeakSet/proto-from-ctor-realm.js": true, - "test/built-ins/WeakMap/proto-from-ctor-realm.js": true, - "test/built-ins/Map/proto-from-ctor-realm.js": true, - "test/built-ins/Set/proto-from-ctor-realm.js": true, - "test/built-ins/Object/proto-from-ctor.js": true, - "test/built-ins/Array/from/proto-from-ctor-realm.js": true, - "test/built-ins/Array/of/proto-from-ctor-realm.js": true, - "test/built-ins/Array/prototype/concat/create-proto-from-ctor-realm-non-array.js": true, - "test/built-ins/Array/prototype/concat/create-proto-from-ctor-realm-array.js": true, - "test/built-ins/Array/prototype/filter/create-proto-from-ctor-realm-non-array.js": true, - "test/built-ins/Array/prototype/filter/create-proto-from-ctor-realm-array.js": true, - "test/built-ins/Array/prototype/map/create-proto-from-ctor-realm-non-array.js": true, - "test/built-ins/Array/prototype/map/create-proto-from-ctor-realm-array.js": true, - "test/built-ins/Array/prototype/slice/create-proto-from-ctor-realm-non-array.js": true, - "test/built-ins/Array/prototype/slice/create-proto-from-ctor-realm-array.js": true, - "test/built-ins/Array/prototype/splice/create-proto-from-ctor-realm-non-array.js": true, - "test/built-ins/Array/prototype/splice/create-proto-from-ctor-realm-array.js": true, + "test/built-ins/Symbol/unscopables/cross-realm.js": true, + "test/built-ins/Symbol/toStringTag/cross-realm.js": true, + "test/built-ins/Symbol/toPrimitive/cross-realm.js": true, + "test/built-ins/Symbol/split/cross-realm.js": true, + "test/built-ins/Symbol/species/cross-realm.js": true, + "test/built-ins/Symbol/search/cross-realm.js": true, + "test/built-ins/Symbol/replace/cross-realm.js": true, + "test/built-ins/Symbol/match/cross-realm.js": true, + "test/built-ins/Symbol/keyFor/cross-realm.js": true, + "test/built-ins/Symbol/iterator/cross-realm.js": true, + "test/built-ins/Symbol/isConcatSpreadable/cross-realm.js": true, + "test/built-ins/Symbol/hasInstance/cross-realm.js": true, + "test/built-ins/Symbol/for/cross-realm.js": true, + "test/built-ins/WeakSet/proto-from-ctor-realm.js": true, + "test/built-ins/WeakMap/proto-from-ctor-realm.js": true, + "test/built-ins/Map/proto-from-ctor-realm.js": true, + "test/built-ins/Set/proto-from-ctor-realm.js": true, + "test/built-ins/Object/proto-from-ctor.js": true, + "test/built-ins/Array/from/proto-from-ctor-realm.js": true, + "test/built-ins/Array/of/proto-from-ctor-realm.js": true, + "test/built-ins/Array/prototype/concat/create-proto-from-ctor-realm-non-array.js": true, + "test/built-ins/Array/prototype/concat/create-proto-from-ctor-realm-array.js": true, + "test/built-ins/Array/prototype/filter/create-proto-from-ctor-realm-non-array.js": true, + "test/built-ins/Array/prototype/filter/create-proto-from-ctor-realm-array.js": true, + "test/built-ins/Array/prototype/map/create-proto-from-ctor-realm-non-array.js": true, + "test/built-ins/Array/prototype/map/create-proto-from-ctor-realm-array.js": true, + "test/built-ins/Array/prototype/slice/create-proto-from-ctor-realm-non-array.js": true, + "test/built-ins/Array/prototype/slice/create-proto-from-ctor-realm-array.js": true, + "test/built-ins/Array/prototype/splice/create-proto-from-ctor-realm-non-array.js": true, + "test/built-ins/Array/prototype/splice/create-proto-from-ctor-realm-array.js": true, + "test/built-ins/Proxy/construct/arguments-realm.js": true, + "test/built-ins/Proxy/setPrototypeOf/trap-is-not-callable-realm.js": true, + "test/built-ins/Proxy/getPrototypeOf/trap-is-not-callable-realm.js": true, + "test/built-ins/Proxy/set/trap-is-not-callable-realm.js": true, + "test/built-ins/Proxy/getOwnPropertyDescriptor/trap-is-not-callable-realm.js": true, + "test/built-ins/Proxy/getOwnPropertyDescriptor/result-type-is-not-object-nor-undefined-realm.js": true, + "test/built-ins/Proxy/get/trap-is-not-callable-realm.js": true, + "test/built-ins/Proxy/preventExtensions/trap-is-not-callable-realm.js": true, + "test/built-ins/Proxy/defineProperty/null-handler-realm.js": true, + "test/built-ins/Proxy/ownKeys/trap-is-not-callable-realm.js": true, + "test/built-ins/Proxy/ownKeys/return-not-list-object-throws-realm.js": true, + "test/built-ins/Proxy/deleteProperty/trap-is-not-callable-realm.js": true, + "test/built-ins/Proxy/isExtensible/trap-is-not-callable-realm.js": true, + "test/built-ins/Proxy/defineProperty/trap-is-not-callable-realm.js": true, + "test/built-ins/Proxy/defineProperty/targetdesc-undefined-target-is-not-extensible-realm.js": true, + "test/built-ins/Proxy/defineProperty/targetdesc-undefined-not-configurable-descriptor-realm.js": true, + "test/built-ins/Proxy/defineProperty/targetdesc-not-compatible-descriptor.js": true, + "test/built-ins/Proxy/defineProperty/targetdesc-not-compatible-descriptor-realm.js": true, + "test/built-ins/Proxy/defineProperty/targetdesc-not-compatible-descriptor-not-configurable-target-realm.js": true, + "test/built-ins/Proxy/defineProperty/targetdesc-configurable-desc-not-configurable-realm.js": true, + "test/built-ins/Proxy/has/trap-is-not-callable-realm.js": true, + "test/built-ins/Proxy/defineProperty/desc-realm.js": true, + "test/built-ins/Proxy/apply/trap-is-not-callable-realm.js": true, + "test/built-ins/Proxy/apply/arguments-realm.js": true, + "test/built-ins/Proxy/construct/trap-is-undefined-proto-from-ctor-realm.js": true, + "test/built-ins/Proxy/construct/trap-is-not-callable-realm.js": true, // class "test/language/statements/class/subclass/builtin-objects/Symbol/symbol-valid-as-extends-value.js": true, @@ -102,16 +127,19 @@ var ( "test/language/statements/for-of/Array.prototype.keys.js": true, "test/language/statements/for-of/Array.prototype.entries.js": true, "test/language/statements/for-of/Array.prototype.Symbol.iterator.js": true, + + // arrow-function + "test/built-ins/Object/prototype/toString/proxy-function.js": true, } featuresBlackList = []string{ - "Proxy", "arrow-function", } es6WhiteList = map[string]bool{} es6IdWhiteList = []string{ + "9.5", "12.9.3", "12.9.4", "19.1", @@ -130,6 +158,8 @@ var ( "23.3", "23.4", "25.1.2", + "26.1", + "26.2", "B.2.1", "B.2.2", } diff --git a/value.go b/value.go index 44ba6ae8..6ef9d592 100644 --- a/value.go +++ b/value.go @@ -17,7 +17,7 @@ var ( _NaN Value = valueFloat(math.NaN()) _positiveInf Value = valueFloat(math.Inf(+1)) _negativeInf Value = valueFloat(math.Inf(-1)) - _positiveZero Value + _positiveZero Value = valueInt(0) _negativeZero Value = valueFloat(math.Float64frombits(0 | (1 << 63))) _epsilon = valueFloat(2.2204460492503130808472633361816e-16) _undefined Value = valueUndefined{} @@ -33,12 +33,12 @@ var ( reflectTypeString = reflect.TypeOf("") ) -var intCache [256]Value - var ( mapHasher maphash.Hash ) +var intCache [256]Value + type Value interface { ToInteger() int64 toString() valueString @@ -54,10 +54,6 @@ type Value interface { Export() interface{} ExportType() reflect.Type - assertInt() (int64, bool) - assertString() (valueString, bool) - assertFloat() (float64, bool) - baseObject(r *Runtime) *Object hash() uint64 @@ -154,50 +150,35 @@ func (i valueInt) ToNumber() Value { } func (i valueInt) SameAs(other Value) bool { - if otherInt, ok := other.assertInt(); ok { - return int64(i) == otherInt - } - return false + return i == other } func (i valueInt) Equals(other Value) bool { - if o, ok := other.assertInt(); ok { - return int64(i) == o - } - if o, ok := other.assertFloat(); ok { - return float64(i) == o - } - if o, ok := other.assertString(); ok { + switch o := other.(type) { + case valueInt: + return i == o + case valueFloat: + return float64(i) == float64(o) + case valueString: return o.ToNumber().Equals(i) - } - if o, ok := other.(valueBool); ok { + case valueBool: return int64(i) == o.ToInteger() - } - if o, ok := other.(*Object); ok { + case *Object: return i.Equals(o.self.toPrimitiveNumber()) } + return false } func (i valueInt) StrictEquals(other Value) bool { - if otherInt, ok := other.assertInt(); ok { - return int64(i) == otherInt - } else if otherFloat, ok := other.assertFloat(); ok { - return float64(i) == otherFloat + switch o := other.(type) { + case valueInt: + return i == o + case valueFloat: + return float64(i) == float64(o) } - return false -} - -func (i valueInt) assertInt() (int64, bool) { - return int64(i), true -} - -func (i valueInt) assertFloat() (float64, bool) { - return 0, false -} -func (i valueInt) assertString() (valueString, bool) { - return nil, false + return false } func (i valueInt) baseObject(r *Runtime) *Object { @@ -290,18 +271,6 @@ func (o valueBool) StrictEquals(other Value) bool { return false } -func (o valueBool) assertInt() (int64, bool) { - return 0, false -} - -func (o valueBool) assertFloat() (float64, bool) { - return 0, false -} - -func (o valueBool) assertString() (valueString, bool) { - return nil, false -} - func (o valueBool) baseObject(r *Runtime) *Object { return r.global.BooleanPrototype } @@ -407,18 +376,6 @@ func (n valueNull) StrictEquals(other Value) bool { return same } -func (n valueNull) assertInt() (int64, bool) { - return 0, false -} - -func (n valueNull) assertFloat() (float64, bool) { - return 0, false -} - -func (n valueNull) assertString() (valueString, bool) { - return nil, false -} - func (n valueNull) baseObject(*Runtime) *Object { return nil } @@ -467,18 +424,6 @@ func (p *valueProperty) ToNumber() Value { return nil } -func (p *valueProperty) assertInt() (int64, bool) { - return 0, false -} - -func (p *valueProperty) assertFloat() (float64, bool) { - return 0, false -} - -func (p *valueProperty) assertString() (valueString, bool) { - return nil, false -} - func (p *valueProperty) isWritable() bool { return p.writable || p.setterFunc != nil } @@ -598,18 +543,20 @@ func (f valueFloat) ToNumber() Value { } func (f valueFloat) SameAs(other Value) bool { - if o, ok := other.assertFloat(); ok { + switch o := other.(type) { + case valueFloat: this := float64(f) - if math.IsNaN(this) && math.IsNaN(o) { + o1 := float64(o) + if math.IsNaN(this) && math.IsNaN(o1) { return true } else { - ret := this == o + ret := this == o1 if ret && this == 0 { - ret = math.Signbit(this) == math.Signbit(o) + ret = math.Signbit(this) == math.Signbit(o1) } return ret } - } else if o, ok := other.assertInt(); ok { + case valueInt: this := float64(f) ret := this == float64(o) if ret && this == 0 { @@ -617,27 +564,19 @@ func (f valueFloat) SameAs(other Value) bool { } return ret } + return false } func (f valueFloat) Equals(other Value) bool { - if o, ok := other.assertFloat(); ok { - return float64(f) == o - } - - if o, ok := other.assertInt(); ok { + switch o := other.(type) { + case valueFloat: + return f == o + case valueInt: return float64(f) == float64(o) - } - - if _, ok := other.assertString(); ok { - return float64(f) == other.ToFloat() - } - - if o, ok := other.(valueBool); ok { + case valueString, valueBool: return float64(f) == o.ToFloat() - } - - if o, ok := other.(*Object); ok { + case *Object: return f.Equals(o.self.toPrimitiveNumber()) } @@ -645,24 +584,14 @@ func (f valueFloat) Equals(other Value) bool { } func (f valueFloat) StrictEquals(other Value) bool { - if o, ok := other.assertFloat(); ok { - return float64(f) == o - } else if o, ok := other.assertInt(); ok { + switch o := other.(type) { + case valueFloat: + return f == o + case valueInt: return float64(f) == float64(o) } - return false -} -func (f valueFloat) assertInt() (int64, bool) { - return 0, false -} - -func (f valueFloat) assertFloat() (float64, bool) { - return float64(f), true -} - -func (f valueFloat) assertString() (valueString, bool) { - return nil, false + return false } func (f valueFloat) baseObject(r *Runtime) *Object { @@ -728,21 +657,13 @@ func (o *Object) Equals(other Value) bool { return o == other || o.self.equal(other.self) } - if _, ok := other.assertInt(); ok { - return o.self.toPrimitive().Equals(other) - } - - if _, ok := other.assertFloat(); ok { + switch o1 := other.(type) { + case valueInt, valueFloat, valueString: return o.self.toPrimitive().Equals(other) + case valueBool: + return o.Equals(o1.ToNumber()) } - if other, ok := other.(valueBool); ok { - return o.Equals(other.ToNumber()) - } - - if _, ok := other.assertString(); ok { - return o.self.toPrimitive().Equals(other) - } return false } @@ -753,18 +674,6 @@ func (o *Object) StrictEquals(other Value) bool { return false } -func (o *Object) assertInt() (int64, bool) { - return 0, false -} - -func (o *Object) assertFloat() (float64, bool) { - return 0, false -} - -func (o *Object) assertString() (valueString, bool) { - return nil, false -} - func (o *Object) baseObject(*Runtime) *Object { return o } @@ -782,12 +691,14 @@ func (o *Object) hash() uint64 { } func (o *Object) Get(name string) Value { - return o.self.getStr(name) + return o.self.getStr(name, nil) } func (o *Object) Keys() (keys []string) { - for item, f := o.self.enumerate(false, false)(); f != nil; item, f = f() { - keys = append(keys, item.name) + names := o.self.ownKeys(false, nil) + keys = make([]string, 0, len(names)) + for _, name := range names { + keys = append(keys, name.String()) } return @@ -797,7 +708,7 @@ func (o *Object) Keys() (keys []string) { // configurable: configurable, enumerable: enumerable}) func (o *Object) DefineDataProperty(name string, value Value, writable, configurable, enumerable Flag) error { return tryFunc(func() { - o.self.defineOwnProperty(newStringValue(name), propertyDescr{ + o.self.defineOwnPropertyStr(name, PropertyDescriptor{ Value: value, Writable: writable, Configurable: configurable, @@ -810,7 +721,7 @@ func (o *Object) DefineDataProperty(name string, value Value, writable, configur // configurable: configurable, enumerable: enumerable}) func (o *Object) DefineAccessorProperty(name string, getter, setter Value, configurable, enumerable Flag) error { return tryFunc(func() { - o.self.defineOwnProperty(newStringValue(name), propertyDescr{ + o.self.defineOwnPropertyStr(name, PropertyDescriptor{ Getter: getter, Setter: setter, Configurable: configurable, @@ -821,7 +732,7 @@ func (o *Object) DefineAccessorProperty(name string, getter, setter Value, confi func (o *Object) Set(name string, value interface{}) error { return tryFunc(func() { - o.self.putStr(name, o.runtime.ToValue(value), true) + o.self.setOwnStr(name, o.runtime.ToValue(value), true) }) } @@ -906,21 +817,6 @@ func (o valueUnresolved) StrictEquals(Value) bool { return false } -func (o valueUnresolved) assertInt() (int64, bool) { - o.throw() - return 0, false -} - -func (o valueUnresolved) assertFloat() (float64, bool) { - o.throw() - return 0, false -} - -func (o valueUnresolved) assertString() (valueString, bool) { - o.throw() - return nil, false -} - func (o valueUnresolved) baseObject(*Runtime) *Object { o.throw() return nil @@ -996,18 +892,6 @@ func (s *valueSymbol) ExportType() reflect.Type { return reflectTypeString } -func (s *valueSymbol) assertInt() (int64, bool) { - return 0, false -} - -func (s *valueSymbol) assertString() (valueString, bool) { - return nil, false -} - -func (s *valueSymbol) assertFloat() (float64, bool) { - return 0, false -} - func (s *valueSymbol) baseObject(r *Runtime) *Object { return r.newPrimitiveObject(s, r.global.SymbolPrototype, "Symbol") } diff --git a/vm.go b/vm.go index b54ab3f5..88fe9c85 100644 --- a/vm.go +++ b/vm.go @@ -25,11 +25,12 @@ type stash struct { } type context struct { - prg *Program - funcName string - stash *stash - pc, sb int - args int + prg *Program + funcName string + stash *stash + newTarget Value + pc, sb int + args int } type iterStackItem struct { @@ -67,11 +68,11 @@ type objRef struct { } func (r *objRef) get() Value { - return r.base.getStr(r.name) + return r.base.getStr(r.name, nil) } func (r *objRef) set(v Value) { - r.base.putStr(r.name, v, r.strict) + r.base.setOwnStr(r.name, v, r.strict) } func (r *objRef) refname() string { @@ -108,6 +109,7 @@ type vm struct { callStack []context iterStack []iterStackItem refStack []ref + newTarget Value stashAllocs int halt bool @@ -155,13 +157,13 @@ func floatToValue(f float64) (result Value) { return valueFloat(f) } -func toInt(v Value) (int64, bool) { +func toInt64(v Value) (int64, bool) { num := v.ToNumber() - if i, ok := num.assertInt(); ok { - return i, true + if i, ok := num.(valueInt); ok { + return int64(i), true } - if f, ok := num.assertFloat(); ok { - if i, ok := floatToInt(f); ok { + if f, ok := num.(valueFloat); ok { + if i, ok := floatToInt(float64(f)); ok { return i, true } } @@ -170,14 +172,14 @@ func toInt(v Value) (int64, bool) { func toIntIgnoreNegZero(v Value) (int64, bool) { num := v.ToNumber() - if i, ok := num.assertInt(); ok { - return i, true + if i, ok := num.(valueInt); ok { + return int64(i), true } - if f, ok := num.assertFloat(); ok { + if f, ok := num.(valueFloat); ok { if v == _negativeZero { return 0, true } - if i, ok := floatToInt(f); ok { + if i, ok := floatToInt(float64(f)); ok { return i, true } } @@ -200,8 +202,8 @@ func (s *valueStack) expand(idx int) { func (s *stash) put(name string, v Value) bool { if s.obj != nil { - if found := s.obj.getStr(name); found != nil { - s.obj.putStr(name, v, false) + if found := s.obj.getStr(name, nil); found != nil { + s.obj.setOwnStr(name, v, false) return true } return false @@ -232,12 +234,10 @@ func (s *stash) getByIdx(idx uint32) Value { func (s *stash) getByName(name string, _ *vm) (v Value, exists bool) { if s.obj != nil { - v = s.obj.getStr(name) - if v == nil { - return nil, false - //return valueUnresolved{r: vm.r, ref: name}, false + if s.obj.hasPropertyStr(name) { + return nilSafe(s.obj.getStr(name, nil)), true } - return v, true + return nil, false } if idx, exists := s.names[name]; exists { return s.values[idx], true @@ -317,12 +317,12 @@ func (vm *vm) ClearInterrupt() { atomic.StoreUint32(&vm.interrupted, 0) } -func (vm *vm) captureStack(stack []stackFrame, ctxOffset int) []stackFrame { +func (vm *vm) captureStack(stack []StackFrame, ctxOffset int) []StackFrame { // Unroll the context stack - stack = append(stack, stackFrame{prg: vm.prg, pc: vm.pc, funcName: vm.funcName}) + stack = append(stack, StackFrame{prg: vm.prg, pc: vm.pc, funcName: vm.funcName}) for i := len(vm.callStack) - 1; i > ctxOffset-1; i-- { if vm.callStack[i].pc != -1 { - stack = append(stack, stackFrame{prg: vm.callStack[i].prg, pc: vm.callStack[i].pc - 1, funcName: vm.callStack[i].funcName}) + stack = append(stack, StackFrame{prg: vm.callStack[i].prg, pc: vm.callStack[i].pc - 1, funcName: vm.callStack[i].funcName}) } } return stack @@ -409,8 +409,13 @@ func (vm *vm) peek() Value { func (vm *vm) saveCtx(ctx *context) { ctx.prg = vm.prg - ctx.funcName = vm.funcName + if vm.funcName != "" { + ctx.funcName = vm.funcName + } else if ctx.prg != nil && ctx.prg.funcName != "" { + ctx.funcName = ctx.prg.funcName + } ctx.stash = vm.stash + ctx.newTarget = vm.newTarget ctx.pc = vm.pc ctx.sb = vm.sb ctx.args = vm.args @@ -436,6 +441,7 @@ func (vm *vm) restoreCtx(ctx *context) { vm.stash = ctx.stash vm.sb = ctx.sb vm.args = ctx.args + vm.newTarget = ctx.newTarget } func (vm *vm) popCtx() { @@ -595,8 +601,8 @@ func (_add) exec(vm *vm) { var ret Value - leftString, isLeftString := left.assertString() - rightString, isRightString := right.assertString() + leftString, isLeftString := left.(valueString) + rightString, isRightString := right.(valueString) if isLeftString || isRightString { if !isLeftString { @@ -607,8 +613,8 @@ func (_add) exec(vm *vm) { } ret = leftString.concat(rightString) } else { - if leftInt, ok := left.assertInt(); ok { - if rightInt, ok := right.assertInt(); ok { + if leftInt, ok := left.(valueInt); ok { + if rightInt, ok := right.(valueInt); ok { ret = intToValue(int64(leftInt) + int64(rightInt)) } else { ret = floatToValue(float64(leftInt) + right.ToFloat()) @@ -633,9 +639,9 @@ func (_sub) exec(vm *vm) { var result Value - if left, ok := left.assertInt(); ok { - if right, ok := right.assertInt(); ok { - result = intToValue(left - right) + if left, ok := left.(valueInt); ok { + if right, ok := right.(valueInt); ok { + result = intToValue(int64(left) - int64(right)) goto end } } @@ -657,8 +663,8 @@ func (_mul) exec(vm *vm) { var result Value - if left, ok := toInt(left); ok { - if right, ok := toInt(right); ok { + if left, ok := toInt64(left); ok { + if right, ok := toInt64(right); ok { if left == 0 && right == -1 || left == -1 && right == 0 { result = _negativeZero goto end @@ -750,8 +756,8 @@ func (_mod) exec(vm *vm) { var result Value - if leftInt, ok := toInt(left); ok { - if rightInt, ok := toInt(right); ok { + if leftInt, ok := toInt64(left); ok { + if rightInt, ok := toInt64(right); ok { if rightInt == 0 { result = _NaN goto end @@ -782,7 +788,7 @@ func (_neg) exec(vm *vm) { var result Value - if i, ok := toInt(operand); ok { + if i, ok := toInt64(operand); ok { if i == 0 { result = _negativeZero } else { @@ -816,7 +822,7 @@ var inc _inc func (_inc) exec(vm *vm) { v := vm.stack[vm.sp-1] - if i, ok := toInt(v); ok { + if i, ok := toInt64(v); ok { v = intToValue(i + 1) goto end } @@ -835,7 +841,7 @@ var dec _dec func (_dec) exec(vm *vm) { v := vm.stack[vm.sp-1] - if i, ok := toInt(v); ok { + if i, ok := toInt64(v); ok { v = intToValue(i - 1) goto end } @@ -953,7 +959,7 @@ func (_setElem) exec(vm *vm) { propName := toPropertyKey(vm.stack[vm.sp-2]) val := vm.stack[vm.sp-1] - obj.self.put(propName, val, false) + obj.setOwn(propName, val, false) vm.sp -= 2 vm.stack[vm.sp-1] = val @@ -969,7 +975,7 @@ func (_setElemStrict) exec(vm *vm) { propName := toPropertyKey(vm.stack[vm.sp-2]) val := vm.stack[vm.sp-1] - obj.self.put(propName, val, true) + obj.setOwn(propName, val, true) vm.sp -= 2 vm.stack[vm.sp-1] = val @@ -983,7 +989,7 @@ var deleteElem _deleteElem func (_deleteElem) exec(vm *vm) { obj := vm.r.toObject(vm.stack[vm.sp-2]) propName := toPropertyKey(vm.stack[vm.sp-1]) - if !obj.self.hasProperty(propName) || obj.self.delete(propName, false) { + if obj.delete(propName, false) { vm.stack[vm.sp-2] = valueTrue } else { vm.stack[vm.sp-2] = valueFalse @@ -999,7 +1005,7 @@ var deleteElemStrict _deleteElemStrict func (_deleteElemStrict) exec(vm *vm) { obj := vm.r.toObject(vm.stack[vm.sp-2]) propName := toPropertyKey(vm.stack[vm.sp-1]) - obj.self.delete(propName, true) + obj.delete(propName, true) vm.stack[vm.sp-2] = valueTrue vm.sp-- vm.pc++ @@ -1009,7 +1015,7 @@ type deleteProp string func (d deleteProp) exec(vm *vm) { obj := vm.r.toObject(vm.stack[vm.sp-1]) - if !obj.self.hasPropertyStr(string(d)) || obj.self.deleteStr(string(d), false) { + if obj.self.deleteStr(string(d), false) { vm.stack[vm.sp-1] = valueTrue } else { vm.stack[vm.sp-1] = valueFalse @@ -1030,7 +1036,7 @@ type setProp string func (p setProp) exec(vm *vm) { val := vm.stack[vm.sp-1] - vm.stack[vm.sp-2].ToObject(vm.r).self.putStr(string(p), val, false) + vm.stack[vm.sp-2].ToObject(vm.r).self.setOwnStr(string(p), val, false) vm.stack[vm.sp-2] = val vm.sp-- vm.pc++ @@ -1043,7 +1049,7 @@ func (p setPropStrict) exec(vm *vm) { val := vm.stack[vm.sp-1] obj1 := vm.r.toObject(obj) - obj1.self.putStr(string(p), val, true) + obj1.self.setOwnStr(string(p), val, true) vm.stack[vm.sp-2] = val vm.sp-- vm.pc++ @@ -1063,7 +1069,7 @@ type _setProto struct{} var setProto _setProto func (_setProto) exec(vm *vm) { - vm.r.toObject(vm.stack[vm.sp-2]).self.putStr(__proto__, vm.stack[vm.sp-1], true) + vm.r.toObject(vm.stack[vm.sp-2]).self.setProto(vm.r.toProto(vm.stack[vm.sp-1]), true) vm.sp-- vm.pc++ @@ -1075,13 +1081,13 @@ func (s setPropGetter) exec(vm *vm) { obj := vm.r.toObject(vm.stack[vm.sp-2]) val := vm.stack[vm.sp-1] - descr := propertyDescr{ + descr := PropertyDescriptor{ Getter: val, Configurable: FLAG_TRUE, Enumerable: FLAG_TRUE, } - obj.self.defineOwnProperty(newStringValue(string(s)), descr, false) + obj.self.defineOwnPropertyStr(string(s), descr, false) vm.sp-- vm.pc++ @@ -1093,13 +1099,13 @@ func (s setPropSetter) exec(vm *vm) { obj := vm.r.toObject(vm.stack[vm.sp-2]) val := vm.stack[vm.sp-1] - descr := propertyDescr{ + descr := PropertyDescriptor{ Setter: val, Configurable: FLAG_TRUE, Enumerable: FLAG_TRUE, } - obj.self.defineOwnProperty(newStringValue(string(s)), descr, false) + obj.self.defineOwnPropertyStr(string(s), descr, false) vm.sp-- vm.pc++ @@ -1113,15 +1119,7 @@ func (g getProp) exec(vm *vm) { if obj == nil { panic(vm.r.NewTypeError("Cannot read property '%s' of undefined", g)) } - prop := obj.self.getPropStr(string(g)) - if prop1, ok := prop.(*valueProperty); ok { - vm.stack[vm.sp-1] = prop1.get(v) - } else { - if prop == nil { - prop = _undefined - } - vm.stack[vm.sp-1] = prop - } + vm.stack[vm.sp-1] = nilSafe(obj.self.getStr(string(g), v)) vm.pc++ } @@ -1132,17 +1130,13 @@ func (g getPropCallee) exec(vm *vm) { v := vm.stack[vm.sp-1] obj := v.baseObject(vm.r) if obj == nil { - panic(vm.r.NewTypeError("Cannot read property '%s' of undefined", g)) + panic(vm.r.NewTypeError("Cannot read property '%s' of undefined or null", g)) } - prop := obj.self.getPropStr(string(g)) - if prop1, ok := prop.(*valueProperty); ok { - vm.stack[vm.sp-1] = prop1.get(v) - } else { - if prop == nil { - prop = memberUnresolved{valueUnresolved{r: vm.r, ref: string(g)}} - } - vm.stack[vm.sp-1] = prop + prop := obj.self.getStr(string(g), v) + if prop == nil { + prop = memberUnresolved{valueUnresolved{r: vm.r, ref: string(g)}} } + vm.stack[vm.sp-1] = prop vm.pc++ } @@ -1159,15 +1153,7 @@ func (_getElem) exec(vm *vm) { panic(vm.r.NewTypeError("Cannot read property '%s' of undefined", propName.String())) } - prop := obj.self.getProp(propName) - if prop1, ok := prop.(*valueProperty); ok { - vm.stack[vm.sp-2] = prop1.get(v) - } else { - if prop == nil { - prop = _undefined - } - vm.stack[vm.sp-2] = prop - } + vm.stack[vm.sp-2] = nilSafe(obj.get(propName, v)) vm.sp-- vm.pc++ @@ -1185,15 +1171,11 @@ func (_getElemCallee) exec(vm *vm) { panic(vm.r.NewTypeError("Cannot read property '%s' of undefined", propName.String())) } - prop := obj.self.getProp(propName) - if prop1, ok := prop.(*valueProperty); ok { - vm.stack[vm.sp-2] = prop1.get(v) - } else { - if prop == nil { - prop = memberUnresolved{valueUnresolved{r: vm.r, ref: propName.String()}} - } - vm.stack[vm.sp-2] = prop + prop := obj.get(propName, v) + if prop == nil { + prop = memberUnresolved{valueUnresolved{r: vm.r, ref: propName.String()}} } + vm.stack[vm.sp-2] = prop vm.sp-- vm.pc++ @@ -1249,7 +1231,7 @@ func (l newArray) exec(vm *vm) { } type newArraySparse struct { - l, objCount uint32 + l, objCount int } func (n *newArraySparse) exec(vm *vm) { @@ -1257,7 +1239,7 @@ func (n *newArraySparse) exec(vm *vm) { copy(values, vm.stack[vm.sp-int(n.l):vm.sp]) arr := vm.r.newArrayObject() setArrayValues(arr, values) - arr.objCount = int64(n.objCount) + arr.objCount = n.objCount vm.sp -= int(n.l) - 1 vm.stack[vm.sp-1] = arr.val vm.pc++ @@ -1309,7 +1291,7 @@ func (s setVar) exec(vm *vm) { v := vm.peek() level := int(s.idx >> 24) - idx := uint32(s.idx & 0x00FFFFFF) + idx := s.idx & 0x00FFFFFF stash := vm.stash name := s.name for i := 0; i < level; i++ { @@ -1322,7 +1304,7 @@ func (s setVar) exec(vm *vm) { if stash != nil { stash.putByIdx(idx, v) } else { - vm.r.globalObject.self.putStr(name, v, false) + vm.r.globalObject.self.setOwnStr(name, v, false) } end: @@ -1462,7 +1444,7 @@ type setGlobal string func (s setGlobal) exec(vm *vm) { v := vm.peek() - vm.r.globalObject.self.putStr(string(s), v, false) + vm.r.globalObject.self.setOwnStr(string(s), v, false) vm.pc++ } @@ -1474,7 +1456,7 @@ func (s setGlobalStrict) exec(vm *vm) { name := string(s) o := vm.r.globalObject.self if o.hasOwnPropertyStr(name) { - o.putStr(name, v, true) + o.setOwnStr(name, v, true) } else { vm.r.throwReferenceError(name) } @@ -1516,7 +1498,7 @@ func (g getVar) exec(vm *vm) { if stash != nil { vm.push(stash.getByIdx(idx)) } else { - v := vm.r.globalObject.self.getStr(name) + v := vm.r.globalObject.self.getStr(name, nil) if v == nil { if g.ref { v = valueUnresolved{r: vm.r, ref: name} @@ -1538,7 +1520,7 @@ type resolveVar struct { func (r resolveVar) exec(vm *vm) { level := int(r.idx >> 24) - idx := uint32(r.idx & 0x00FFFFFF) + idx := r.idx & 0x00FFFFFF stash := vm.stash var ref ref for i := 0; i < level; i++ { @@ -1627,7 +1609,7 @@ func (n getVar1) exec(vm *vm) { } } if val == nil { - val = vm.r.globalObject.self.getStr(name) + val = vm.r.globalObject.self.getStr(name, nil) if val == nil { vm.r.throwReferenceError(name) } @@ -1648,7 +1630,7 @@ func (n getVar1Callee) exec(vm *vm) { } } if val == nil { - val = vm.r.globalObject.self.getStr(name) + val = vm.r.globalObject.self.getStr(name, nil) if val == nil { val = valueUnresolved{r: vm.r, ref: name} } @@ -1670,7 +1652,7 @@ func (vm *vm) callEval(n int, strict bool) { if vm.r.toObject(vm.stack[vm.sp-n-1]) == vm.r.global.Eval { if n > 0 { srcVal := vm.stack[vm.sp-n] - if src, ok := srcVal.assertString(); ok { + if src, ok := srcVal.(valueString); ok { var this Value if vm.sb != 0 { this = vm.stack[vm.sb] @@ -1745,6 +1727,18 @@ repeat: vm._nativeCall(f, n) case *boundFuncObject: vm._nativeCall(&f.nativeFuncObject, n) + case *proxyObject: + vm.pushCtx() + vm.prg = nil + vm.funcName = "proxy" + ret := f.apply(FunctionCall{This: vm.stack[vm.sp-n-2], Arguments: vm.stack[vm.sp-n : vm.sp]}) + if ret == nil { + ret = _undefined + } + vm.stack[vm.sp-n-2] = ret + vm.popCtx() + vm.sp -= n + 1 + vm.pc++ case *lazyObject: obj.self = f.create(obj) goto repeat @@ -1971,15 +1965,15 @@ func cmp(px, py Value) Value { var ret bool var nx, ny float64 - if xs, ok := px.assertString(); ok { - if ys, ok := py.assertString(); ok { + if xs, ok := px.(valueString); ok { + if ys, ok := py.(valueString); ok { ret = xs.compareTo(ys) < 0 goto end } } - if xi, ok := px.assertInt(); ok { - if yi, ok := py.assertInt(); ok { + if xi, ok := px.(valueInt); ok { + if yi, ok := py.(valueInt); ok { ret = xi < yi goto end } @@ -2158,7 +2152,7 @@ func (_op_in) exec(vm *vm) { left := vm.stack[vm.sp-2] right := vm.r.toObject(vm.stack[vm.sp-1]) - if right.self.hasProperty(left) { + if right.hasProperty(left) { vm.stack[vm.sp-2] = valueTrue } else { vm.stack[vm.sp-2] = valueFalse @@ -2243,12 +2237,22 @@ type _new uint32 func (n _new) exec(vm *vm) { sp := vm.sp - int(n) - obj := vm.r.toObject(vm.stack[sp-1]) - if ctor := getConstructor(obj); ctor != nil { - vm.stack[sp-1] = ctor(vm.stack[sp:vm.sp]) - vm.sp = sp + obj := vm.stack[sp-1] + ctor := vm.r.toConstructor(obj) + vm.stack[sp-1] = ctor(vm.stack[sp:vm.sp], nil) + vm.sp = sp + vm.pc++ +} + +type _loadNewTarget struct{} + +var loadNewTarget _loadNewTarget + +func (_loadNewTarget) exec(vm *vm) { + if t := vm.newTarget; t != nil { + vm.push(t) } else { - panic(vm.r.NewTypeError("Not a constructor")) + vm.push(_undefined) } vm.pc++ } @@ -2387,7 +2391,7 @@ func (_enumerate) exec(vm *vm) { if v == _undefined || v == _null { vm.iterStack = append(vm.iterStack, iterStackItem{f: emptyIter}) } else { - vm.iterStack = append(vm.iterStack, iterStackItem{f: v.ToObject(vm.r).self.enumerate(false, true)}) + vm.iterStack = append(vm.iterStack, iterStackItem{f: v.ToObject(vm.r).self.enumerate()}) } vm.sp-- vm.pc++ diff --git a/vm_test.go b/vm_test.go index 91bdfad3..ef23fa25 100644 --- a/vm_test.go +++ b/vm_test.go @@ -33,7 +33,7 @@ func TestVM1(t *testing.T) { rv := vm.pop() if obj, ok := rv.(*Object); ok { - if v := obj.self.getStr("test").ToInteger(); v != 5 { + if v := obj.self.getStr("test", nil).ToInteger(); v != 5 { t.Fatalf("Unexpected property value: %v", v) } } else { @@ -72,48 +72,6 @@ func f_loadVal(vm *vm, i *instr) { vm.pc++ } -func f_add(vm *vm) { - right := vm.stack[vm.sp-1] - left := vm.stack[vm.sp-2] - - if o, ok := left.(*Object); ok { - left = o.self.toPrimitive() - } - - if o, ok := right.(*Object); ok { - right = o.self.toPrimitive() - } - - var ret Value - - leftString, isLeftString := left.assertString() - rightString, isRightString := right.assertString() - - if isLeftString || isRightString { - if !isLeftString { - leftString = left.toString() - } - if !isRightString { - rightString = right.toString() - } - ret = leftString.concat(rightString) - } else { - if leftInt, ok := left.assertInt(); ok { - if rightInt, ok := right.assertInt(); ok { - ret = intToValue(int64(leftInt) + int64(rightInt)) - } else { - ret = floatToValue(float64(leftInt) + right.ToFloat()) - } - } else { - ret = floatToValue(left.ToFloat() + right.ToFloat()) - } - } - - vm.stack[vm.sp-2] = ret - vm.sp-- - vm.pc++ -} - type instr struct { code int prim int @@ -221,8 +179,6 @@ func BenchmarkVmNOP1(b *testing.B) { break L case 2: f_loadVal(vm, instr) - case 3: - f_add(vm) default: jumptable[instr.code](vm, instr) } @@ -388,3 +344,13 @@ func BenchmarkFuncCall(b *testing.B) { b.Fatal("f is not a function") } } + +func BenchmarkAssertInt(b *testing.B) { + var v Value + v = intToValue(42) + for i := 0; i < b.N; i++ { + if i, ok := v.(valueInt); !ok || int64(i) != 42 { + b.Fatal() + } + } +} From f62cd66a925b1338877b13423a85d576bd71e723 Mon Sep 17 00:00:00 2001 From: Dmitry Panov Date: Tue, 24 Mar 2020 20:18:16 +0000 Subject: [PATCH 14/46] Unscopables --- builtin_arrray_test.go | 13 +++++++++++++ tc39_test.go | 1 + vm.go | 31 +++++++++++++++++++++++-------- 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/builtin_arrray_test.go b/builtin_arrray_test.go index a25f2f23..c27c69ea 100644 --- a/builtin_arrray_test.go +++ b/builtin_arrray_test.go @@ -206,3 +206,16 @@ func TestArrayOf(t *testing.T) { testScript1(TESTLIB+SCRIPT, _undefined, t) } + +func TestUnscopables(t *testing.T) { + const SCRIPT = ` + var keys = []; + var _length; + with (Array.prototype) { + _length = length; + keys.push('something'); + } + _length === 0 && keys.length === 1 && keys[0] === "something"; + ` + testScript1(SCRIPT, valueTrue, t) +} diff --git a/tc39_test.go b/tc39_test.go index 6bffdd35..43e5dca8 100644 --- a/tc39_test.go +++ b/tc39_test.go @@ -139,6 +139,7 @@ var ( es6WhiteList = map[string]bool{} es6IdWhiteList = []string{ + "8.1.2.1", "9.5", "12.9.3", "12.9.4", diff --git a/vm.go b/vm.go index 88fe9c85..f47b5c55 100644 --- a/vm.go +++ b/vm.go @@ -200,9 +200,21 @@ func (s *valueStack) expand(idx int) { } } +func stashObjHas(obj objectImpl, name string) bool { + if obj.hasPropertyStr(name) { + if unscopables, ok := obj.getSym(symUnscopables, nil).(*Object); ok { + if b := unscopables.self.getStr(name, nil); b != nil { + return !b.ToBoolean() + } + } + return true + } + return false +} + func (s *stash) put(name string, v Value) bool { if s.obj != nil { - if found := s.obj.getStr(name, nil); found != nil { + if stashObjHas(s.obj, name) { s.obj.setOwnStr(name, v, false) return true } @@ -234,7 +246,7 @@ func (s *stash) getByIdx(idx uint32) Value { func (s *stash) getByName(name string, _ *vm) (v Value, exists bool) { if s.obj != nil { - if s.obj.hasPropertyStr(name) { + if stashObjHas(s.obj, name) { return nilSafe(s.obj.getStr(name, nil)), true } return nil, false @@ -258,7 +270,10 @@ func (s *stash) createBinding(name string) { func (s *stash) deleteBinding(name string) bool { if s.obj != nil { - return s.obj.deleteStr(name, false) + if stashObjHas(s.obj, name) { + return s.obj.deleteStr(name, false) + } + return false } if idx, found := s.names[name]; found { s.values[idx] = nil @@ -1318,7 +1333,7 @@ func (s resolveVar1) exec(vm *vm) { var ref ref for stash := vm.stash; stash != nil; stash = stash.outer { if stash.obj != nil { - if stash.obj.hasPropertyStr(name) { + if stashObjHas(stash.obj, name) { ref = &objRef{ base: stash.obj, name: name, @@ -1352,7 +1367,7 @@ func (d deleteVar) exec(vm *vm) { ret := true for stash := vm.stash; stash != nil; stash = stash.outer { if stash.obj != nil { - if stash.obj.hasPropertyStr(name) { + if stashObjHas(stash.obj, name) { ret = stash.obj.deleteStr(name, false) goto end } @@ -1402,7 +1417,7 @@ func (s resolveVar1Strict) exec(vm *vm) { var ref ref for stash := vm.stash; stash != nil; stash = stash.outer { if stash.obj != nil { - if stash.obj.hasPropertyStr(name) { + if stashObjHas(stash.obj, name) { ref = &objRef{ base: stash.obj, name: name, @@ -1524,8 +1539,8 @@ func (r resolveVar) exec(vm *vm) { stash := vm.stash var ref ref for i := 0; i < level; i++ { - if stash.obj != nil { - if stash.obj.hasPropertyStr(r.name) { + if obj := stash.obj; obj != nil { + if stashObjHas(obj, r.name) { ref = &objRef{ base: stash.obj, name: r.name, From 9f519f4c9bf183ad17ff8f46c7050ccbb60b64e2 Mon Sep 17 00:00:00 2001 From: Dmitry Panov Date: Wed, 25 Mar 2020 17:49:40 +0000 Subject: [PATCH 15/46] For-of --- ast/node.go | 10 +++++ compiler_stmt.go | 40 ++++++++++++++++++++ compiler_test.go | 49 +++++++++++++++++++++++++ parser/statement.go | 43 +++++++++++++++++----- runtime.go | 16 +++++--- token/token_const.go | 87 +++++++++++++++++++++++--------------------- vm.go | 33 ++++++++++++++++- 7 files changed, 220 insertions(+), 58 deletions(-) diff --git a/ast/node.go b/ast/node.go index 33d49900..8968fc5c 100644 --- a/ast/node.go +++ b/ast/node.go @@ -269,6 +269,13 @@ type ( Body Statement } + ForOfStatement struct { + For file.Idx + Into Expression + Source Expression + Body Statement + } + ForStatement struct { For file.Idx Initializer Expression @@ -344,6 +351,7 @@ func (*DoWhileStatement) _statementNode() {} func (*EmptyStatement) _statementNode() {} func (*ExpressionStatement) _statementNode() {} func (*ForInStatement) _statementNode() {} +func (*ForOfStatement) _statementNode() {} func (*ForStatement) _statementNode() {} func (*IfStatement) _statementNode() {} func (*LabelledStatement) _statementNode() {} @@ -431,6 +439,7 @@ func (self *DoWhileStatement) Idx0() file.Idx { return self.Do } func (self *EmptyStatement) Idx0() file.Idx { return self.Semicolon } func (self *ExpressionStatement) Idx0() file.Idx { return self.Expression.Idx0() } func (self *ForInStatement) Idx0() file.Idx { return self.For } +func (self *ForOfStatement) Idx0() file.Idx { return self.For } func (self *ForStatement) Idx0() file.Idx { return self.For } func (self *IfStatement) Idx0() file.Idx { return self.If } func (self *LabelledStatement) Idx0() file.Idx { return self.Label.Idx0() } @@ -492,6 +501,7 @@ func (self *DoWhileStatement) Idx1() file.Idx { return self.Test.Idx1() } func (self *EmptyStatement) Idx1() file.Idx { return self.Semicolon + 1 } func (self *ExpressionStatement) Idx1() file.Idx { return self.Expression.Idx1() } func (self *ForInStatement) Idx1() file.Idx { return self.Body.Idx1() } +func (self *ForOfStatement) Idx1() file.Idx { return self.Body.Idx1() } func (self *ForStatement) Idx1() file.Idx { return self.Body.Idx1() } func (self *IfStatement) Idx1() file.Idx { if self.Alternate != nil { diff --git a/compiler_stmt.go b/compiler_stmt.go index a98ac2c4..099920d7 100644 --- a/compiler_stmt.go +++ b/compiler_stmt.go @@ -28,6 +28,8 @@ func (c *compiler) compileStatement(v ast.Statement, needResult bool) { c.compileForStatement(v, needResult) case *ast.ForInStatement: c.compileForInStatement(v, needResult) + case *ast.ForOfStatement: + c.compileForOfStatement(v, needResult) case *ast.WhileStatement: c.compileWhileStatement(v, needResult) case *ast.BranchStatement: @@ -63,6 +65,8 @@ func (c *compiler) compileLabeledStatement(v *ast.LabelledStatement, needResult switch s := v.Statement.(type) { case *ast.ForInStatement: c.compileLabeledForInStatement(s, needResult, label) + case *ast.ForOfStatement: + c.compileLabeledForOfStatement(s, needResult, label) case *ast.ForStatement: c.compileLabeledForStatement(s, needResult, label) case *ast.WhileStatement: @@ -333,6 +337,42 @@ func (c *compiler) compileLabeledForInStatement(v *ast.ForInStatement, needResul c.emit(enumPop) } +func (c *compiler) compileForOfStatement(v *ast.ForOfStatement, needResult bool) { + c.compileLabeledForOfStatement(v, needResult, "") +} + +func (c *compiler) compileLabeledForOfStatement(v *ast.ForOfStatement, needResult bool, label string) { + c.block = &block{ + typ: blockLoop, + outer: c.block, + label: label, + needResult: needResult, + } + + c.compileExpression(v.Source).emitGetter(true) + c.emit(iterate) + if needResult { + c.emit(loadUndef) + } + start := len(c.p.code) + c.markBlockStart() + c.block.cont = start + + c.emit(nil) + c.compileExpression(v.Into).emitSetter(&c.enumGetExpr) + c.emit(pop) + if needResult { + c.emit(pop) // remove last result + } + c.markBlockStart() + c.compileStatement(v.Body, needResult) + c.emit(jump(start - len(c.p.code))) + c.p.code[start] = iterNext(len(c.p.code) - start) + c.leaveBlock() + c.markBlockStart() + c.emit(enumPop) +} + func (c *compiler) compileWhileStatement(v *ast.WhileStatement, needResult bool) { c.compileLabeledWhileStatement(v, needResult, "") } diff --git a/compiler_test.go b/compiler_test.go index 341681c7..963a90fd 100644 --- a/compiler_test.go +++ b/compiler_test.go @@ -1968,6 +1968,55 @@ func TestEmptyCodeError(t *testing.T) { } } +func TestForOfArray(t *testing.T) { + const SCRIPT = ` + var array = [0, 'a', true, false, null, /* hole */, undefined, NaN]; + var i = 0; + + for (var value of array) { + assert.sameValue(value, array[i], 'element at index ' + i); + i++; + } + + assert.sameValue(i, 8, 'Visits all elements'); + ` + testScript1(TESTLIB+SCRIPT, _undefined, t) +} + +func TestForOfReturn(t *testing.T) { + const SCRIPT = ` + var callCount = 0; + var iterationCount = 0; + var iterable = {}; + var x = { + set attr(_) { + throw new Test262Error(); + } + }; + + iterable[Symbol.iterator] = function() { + return { + next: function() { + return { done: false, value: 0 }; + }, + return: function() { + callCount += 1; + } + } + }; + + assert.throws(Test262Error, function() { + for (x.attr of iterable) { + iterationCount += 1; + } + }); + + assert.sameValue(iterationCount, 0, 'The loop body is not evaluated'); + assert.sameValue(callCount, 1, 'Iterator is closed'); + ` + testScript1(TESTLIB+SCRIPT, _undefined, t) +} + // FIXME /* func TestDummyCompile(t *testing.T) { diff --git a/parser/statement.go b/parser/statement.go index 7ac0ac59..1438f27d 100644 --- a/parser/statement.go +++ b/parser/statement.go @@ -378,6 +378,21 @@ func (self *_parser) parseForIn(idx file.Idx, into ast.Expression) *ast.ForInSta } } +func (self *_parser) parseForOf(idx file.Idx, into ast.Expression) *ast.ForOfStatement { + + // Already have consumed " of" + + source := self.parseExpression() + self.expect(token.RIGHT_PARENTHESIS) + + return &ast.ForOfStatement{ + For: idx, + Into: into, + Source: source, + Body: self.parseIterationStatement(), + } +} + func (self *_parser) parseFor(idx file.Idx, initializer ast.Expression) *ast.ForStatement { // Already have consumed " ;" @@ -410,6 +425,7 @@ func (self *_parser) parseForOrForInStatement() ast.Statement { var left []ast.Expression forIn := false + forOf := false if self.token != token.SEMICOLON { allowIn := self.scope.allowIn @@ -418,33 +434,42 @@ func (self *_parser) parseForOrForInStatement() ast.Statement { var_ := self.idx self.next() list := self.parseVariableDeclarationList(var_) - if len(list) == 1 && self.token == token.IN { - self.next() // in - forIn = true - left = []ast.Expression{list[0]} // There is only one declaration - } else { - left = list + if len(list) == 1 { + if self.token == token.IN { + self.next() // in + forIn = true + } else if self.token == token.OF { + self.next() // of + forOf = true + } } + left = list } else { left = append(left, self.parseExpression()) if self.token == token.IN { self.next() forIn = true + } else if self.token == token.OF { + self.next() + forOf = true } } self.scope.allowIn = allowIn } - if forIn { + if forIn || forOf { switch left[0].(type) { case *ast.Identifier, *ast.DotExpression, *ast.BracketExpression, *ast.VariableExpression: // These are all acceptable default: - self.error(idx, "Invalid left-hand side in for-in") + self.error(idx, "Invalid left-hand side in for-in or for-of") self.nextStatement() return &ast.BadStatement{From: idx, To: self.idx} } - return self.parseForIn(idx, left[0]) + if forIn { + return self.parseForIn(idx, left[0]) + } + return self.parseForOf(idx, left[0]) } self.expect(token.SEMICOLON) diff --git a/runtime.go b/runtime.go index 28c94d18..1176117e 100644 --- a/runtime.go +++ b/runtime.go @@ -1712,6 +1712,15 @@ func (r *Runtime) getIterator(obj Value, method func(FunctionCall) Value) *Objec })) } +func returnIter(iter *Object) { + retMethod := toMethod(iter.self.getStr("return", nil)) + if retMethod != nil { + _ = tryFunc(func() { + retMethod(FunctionCall{This: iter}) + }) + } +} + func (r *Runtime) iterate(iter *Object, step func(Value)) { for { res := r.toObject(toMethod(iter.self.getStr("next", nil))(FunctionCall{This: iter})) @@ -1722,12 +1731,7 @@ func (r *Runtime) iterate(iter *Object, step func(Value)) { step(nilSafe(res.self.getStr("value", nil))) }) if err != nil { - retMethod := toMethod(iter.self.getStr("return", nil)) - if retMethod != nil { - _ = tryFunc(func() { - retMethod(FunctionCall{This: iter}) - }) - } + returnIter(iter) panic(err) } } diff --git a/token/token_const.go b/token/token_const.go index b1d83c6d..e266e6ca 100644 --- a/token/token_const.go +++ b/token/token_const.go @@ -77,6 +77,7 @@ const ( firstKeyword IF IN + OF DO VAR @@ -173,6 +174,7 @@ var token2string = [...]string{ QUESTION_MARK: "?", IF: "if", IN: "in", + OF: "of", DO: "do", VAR: "var", FOR: "for", @@ -200,148 +202,151 @@ var token2string = [...]string{ } var keywordTable = map[string]_keyword{ - "if": _keyword{ + "if": { token: IF, }, - "in": _keyword{ + "in": { token: IN, }, - "do": _keyword{ + "of": { + token: OF, + }, + "do": { token: DO, }, - "var": _keyword{ + "var": { token: VAR, }, - "for": _keyword{ + "for": { token: FOR, }, - "new": _keyword{ + "new": { token: NEW, }, - "try": _keyword{ + "try": { token: TRY, }, - "this": _keyword{ + "this": { token: THIS, }, - "else": _keyword{ + "else": { token: ELSE, }, - "case": _keyword{ + "case": { token: CASE, }, - "void": _keyword{ + "void": { token: VOID, }, - "with": _keyword{ + "with": { token: WITH, }, - "while": _keyword{ + "while": { token: WHILE, }, - "break": _keyword{ + "break": { token: BREAK, }, - "catch": _keyword{ + "catch": { token: CATCH, }, - "throw": _keyword{ + "throw": { token: THROW, }, - "return": _keyword{ + "return": { token: RETURN, }, - "typeof": _keyword{ + "typeof": { token: TYPEOF, }, - "delete": _keyword{ + "delete": { token: DELETE, }, - "switch": _keyword{ + "switch": { token: SWITCH, }, - "default": _keyword{ + "default": { token: DEFAULT, }, - "finally": _keyword{ + "finally": { token: FINALLY, }, - "function": _keyword{ + "function": { token: FUNCTION, }, - "continue": _keyword{ + "continue": { token: CONTINUE, }, - "debugger": _keyword{ + "debugger": { token: DEBUGGER, }, - "instanceof": _keyword{ + "instanceof": { token: INSTANCEOF, }, - "const": _keyword{ + "const": { token: KEYWORD, futureKeyword: true, }, - "class": _keyword{ + "class": { token: KEYWORD, futureKeyword: true, }, - "enum": _keyword{ + "enum": { token: KEYWORD, futureKeyword: true, }, - "export": _keyword{ + "export": { token: KEYWORD, futureKeyword: true, }, - "extends": _keyword{ + "extends": { token: KEYWORD, futureKeyword: true, }, - "import": _keyword{ + "import": { token: KEYWORD, futureKeyword: true, }, - "super": _keyword{ + "super": { token: KEYWORD, futureKeyword: true, }, - "implements": _keyword{ + "implements": { token: KEYWORD, futureKeyword: true, strict: true, }, - "interface": _keyword{ + "interface": { token: KEYWORD, futureKeyword: true, strict: true, }, - "let": _keyword{ + "let": { token: KEYWORD, futureKeyword: true, strict: true, }, - "package": _keyword{ + "package": { token: KEYWORD, futureKeyword: true, strict: true, }, - "private": _keyword{ + "private": { token: KEYWORD, futureKeyword: true, strict: true, }, - "protected": _keyword{ + "protected": { token: KEYWORD, futureKeyword: true, strict: true, }, - "public": _keyword{ + "public": { token: KEYWORD, futureKeyword: true, strict: true, }, - "static": _keyword{ + "static": { token: KEYWORD, futureKeyword: true, strict: true, diff --git a/vm.go b/vm.go index f47b5c55..3c0f5d10 100644 --- a/vm.go +++ b/vm.go @@ -34,8 +34,9 @@ type context struct { } type iterStackItem struct { - val Value - f iterNextFunc + val Value + f iterNextFunc + iter *Object } type ref interface { @@ -362,6 +363,9 @@ func (vm *vm) try(f func()) (ex *Exception) { // Restore other stacks iterTail := vm.iterStack[iterLen:] for i := range iterTail { + if iter := iterTail[i].iter; iter != nil { + returnIter(iter) + } iterTail[i] = iterStackItem{} } vm.iterStack = vm.iterStack[:iterLen] @@ -2446,3 +2450,28 @@ func (_enumPop) exec(vm *vm) { vm.iterStack = vm.iterStack[:l] vm.pc++ } + +type _iterate struct{} + +var iterate _iterate + +func (_iterate) exec(vm *vm) { + iter := vm.r.getIterator(vm.stack[vm.sp-1], nil) + vm.iterStack = append(vm.iterStack, iterStackItem{iter: iter}) + vm.sp-- + vm.pc++ +} + +type iterNext int32 + +func (jmp iterNext) exec(vm *vm) { + l := len(vm.iterStack) - 1 + iter := vm.iterStack[l].iter + res := vm.r.toObject(toMethod(iter.self.getStr("next", nil))(FunctionCall{This: iter})) + if nilSafe(res.self.getStr("done", nil)).ToBoolean() { + vm.pc += int(jmp) + } else { + vm.iterStack[l].val = nilSafe(res.self.getStr("value", nil)) + vm.pc++ + } +} From 00cc9a56978babe3a4e430c6c1963ade3164bd95 Mon Sep 17 00:00:00 2001 From: Dmitry Panov Date: Wed, 25 Mar 2020 18:02:41 +0000 Subject: [PATCH 16/46] Fixed tests. --- parser/parser_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/parser/parser_test.go b/parser/parser_test.go index 84af3b9c..ed4c0471 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -160,7 +160,7 @@ func TestParserErr(t *testing.T) { test("--1", "(anonymous): Line 1:1 Invalid left-hand side in assignment") - test("for((1 + 1) in abc) def();", "(anonymous): Line 1:1 Invalid left-hand side in for-in") + test("for((1 + 1) in abc) def();", "(anonymous): Line 1:1 Invalid left-hand side in for-in or for-of") test("[", "(anonymous): Line 1:2 Unexpected end of input") @@ -251,7 +251,7 @@ func TestParserErr(t *testing.T) { test("for ((abc in {}));", "(anonymous): Line 1:17 Unexpected token )") - test("for (+abc in {});", "(anonymous): Line 1:1 Invalid left-hand side in for-in") + test("for (+abc in {});", "(anonymous): Line 1:1 Invalid left-hand side in for-in or for-of") test("if (false)", "(anonymous): Line 1:11 Unexpected end of input") @@ -384,7 +384,7 @@ func TestParserErr(t *testing.T) { test(`for (var abc, def in {}) {}`, "(anonymous): Line 1:19 Unexpected token in") - test(`for (abc, def in {}) {}`, "(anonymous): Line 1:1 Invalid left-hand side in for-in") + test(`for (abc, def in {}) {}`, "(anonymous): Line 1:1 Invalid left-hand side in for-in or for-of") test(`for (var abc=def, ghi=("abc" in {}); true;) {}`, nil) From 47fce3238471e90204d288e9a73e616a8c6d60a1 Mon Sep 17 00:00:00 2001 From: Dmitry Panov Date: Sat, 28 Mar 2020 13:26:56 +0000 Subject: [PATCH 17/46] Fixed concurrent use of hashmap.Hash --- builtin_map.go | 2 +- builtin_set.go | 2 +- map.go | 24 ++++++++++++++---------- map_test.go | 18 ++++++++++-------- object.go | 6 +++--- runtime.go | 4 +++- string_ascii.go | 9 +++++---- string_unicode.go | 9 +++++---- value.go | 26 +++++++++++--------------- 9 files changed, 53 insertions(+), 47 deletions(-) diff --git a/builtin_map.go b/builtin_map.go index d32fae65..7cb464c8 100644 --- a/builtin_map.go +++ b/builtin_map.go @@ -37,7 +37,7 @@ func (o *mapIterObject) next() Value { func (mo *mapObject) init() { mo.baseObject.init() - mo.m = newOrderedMap() + mo.m = newOrderedMap(&mo.val.runtime.hash) } func (r *Runtime) mapProto_clear(call FunctionCall) Value { diff --git a/builtin_set.go b/builtin_set.go index bbd70770..0ef9e658 100644 --- a/builtin_set.go +++ b/builtin_set.go @@ -35,7 +35,7 @@ func (o *setIterObject) next() Value { func (so *setObject) init() { so.baseObject.init() - so.m = newOrderedMap() + so.m = newOrderedMap(&so.val.runtime.hash) } func (r *Runtime) setProto_add(call FunctionCall) Value { diff --git a/map.go b/map.go index 50fae8ae..907206e7 100644 --- a/map.go +++ b/map.go @@ -1,5 +1,7 @@ package goja +import "hash" + type mapEntry struct { key, value Value @@ -8,7 +10,8 @@ type mapEntry struct { } type orderedMap struct { - hash map[uint64]*mapEntry + hash hash.Hash64 + hashTable map[uint64]*mapEntry iterFirst, iterLast *mapEntry size int } @@ -22,8 +25,8 @@ func (m *orderedMap) lookup(key Value) (h uint64, entry, hPrev *mapEntry) { if key == _negativeZero { key = intToValue(0) } - h = key.hash() - for entry = m.hash[h]; entry != nil && !entry.key.SameAs(key); hPrev, entry = entry, entry.hNext { + h = key.hash(m.hash) + for entry = m.hashTable[h]; entry != nil && !entry.key.SameAs(key); hPrev, entry = entry, entry.hNext { } return } @@ -38,7 +41,7 @@ func (m *orderedMap) set(key, value Value) { } entry = &mapEntry{key: key, value: value} if hPrev == nil { - m.hash[h] = entry + m.hashTable[h] = entry } else { hPrev.hNext = entry } @@ -80,12 +83,12 @@ func (m *orderedMap) remove(key Value) bool { m.iterLast = entry.iterPrev } - // remove from the hash + // remove from the hashTable if hPrev == nil { if entry.hNext == nil { - delete(m.hash, h) + delete(m.hashTable, h) } else { - m.hash[h] = entry.hNext + m.hashTable[h] = entry.hNext } } else { hPrev.hNext = entry.hNext @@ -135,9 +138,10 @@ func (iter *orderedMapIter) close() { iter.cur = nil } -func newOrderedMap() *orderedMap { +func newOrderedMap(h hash.Hash64) *orderedMap { return &orderedMap{ - hash: make(map[uint64]*mapEntry), + hash: h, + hashTable: make(map[uint64]*mapEntry), } } @@ -158,6 +162,6 @@ func (m *orderedMap) clear() { } m.iterFirst = nil m.iterLast = nil - m.hash = make(map[uint64]*mapEntry) + m.hashTable = make(map[uint64]*mapEntry) m.size = 0 } diff --git a/map_test.go b/map_test.go index cdf7addb..98afab7f 100644 --- a/map_test.go +++ b/map_test.go @@ -1,13 +1,15 @@ package goja import ( + "hash/maphash" "math" "strconv" "testing" ) func testMapHashVal(v1, v2 Value, expected bool, t *testing.T) { - actual := v1.hash() == v2.hash() + var h maphash.Hash + actual := v1.hash(&h) == v2.hash(&h) if actual != expected { t.Fatalf("testMapHashVal failed for %v, %v", v1, v2) } @@ -31,7 +33,7 @@ func TestMapHash(t *testing.T) { } func TestOrderedMap(t *testing.T) { - m := newOrderedMap() + m := newOrderedMap(&maphash.Hash{}) for i := int64(0); i < 50; i++ { m.set(intToValue(i), asciiString(strconv.FormatInt(i, 10))) } @@ -77,14 +79,14 @@ func TestOrderedMap(t *testing.T) { } func TestOrderedMapCollision(t *testing.T) { - m := newOrderedMap() + m := newOrderedMap(&maphash.Hash{}) n1 := uint64(123456789) n2 := math.Float64frombits(n1) n1Key := intToValue(int64(n1)) n2Key := floatToValue(n2) m.set(n1Key, asciiString("n1")) m.set(n2Key, asciiString("n2")) - if m.size == len(m.hash) { + if m.size == len(m.hashTable) { t.Fatal("Expected a collision but there wasn't one") } if n2Val := m.get(n2Key); !asciiString("n2").SameAs(n2Val) { @@ -103,7 +105,7 @@ func TestOrderedMapCollision(t *testing.T) { } func TestOrderedMapIter(t *testing.T) { - m := newOrderedMap() + m := newOrderedMap(&maphash.Hash{}) iter := m.newIter() ent := iter.next() if ent != nil { @@ -128,7 +130,7 @@ func TestOrderedMapIter(t *testing.T) { } func TestOrderedMapIterVisitAfterReAdd(t *testing.T) { - m := newOrderedMap() + m := newOrderedMap(&maphash.Hash{}) one := intToValue(1) two := intToValue(2) @@ -157,7 +159,7 @@ func TestOrderedMapIterVisitAfterReAdd(t *testing.T) { } func TestOrderedMapIterAddAfterClear(t *testing.T) { - m := newOrderedMap() + m := newOrderedMap(&maphash.Hash{}) one := intToValue(1) m.set(one, valueTrue) iter := m.newIter() @@ -178,7 +180,7 @@ func TestOrderedMapIterAddAfterClear(t *testing.T) { } func TestOrderedMapIterDeleteCurrent(t *testing.T) { - m := newOrderedMap() + m := newOrderedMap(&maphash.Hash{}) one := intToValue(1) two := intToValue(2) iter := m.newIter() diff --git a/object.go b/object.go index 620ddec8..f656a381 100644 --- a/object.go +++ b/object.go @@ -527,7 +527,7 @@ func (o *baseObject) setOwnSym(name *valueSymbol, val Value, throw bool) bool { return false } else { if o.symValues == nil { - o.symValues = newOrderedMap() + o.symValues = newOrderedMap(&o.val.runtime.hash) } o.symValues.set(name, val) } @@ -769,7 +769,7 @@ func (o *baseObject) defineOwnPropertySym(s *valueSymbol, descr PropertyDescript } if v, ok := o._defineOwnProperty(s.String(), existingVal, descr, throw); ok { if o.symValues == nil { - o.symValues = newOrderedMap() + o.symValues = newOrderedMap(&o.val.runtime.hash) } o.symValues.set(s, v) return true @@ -805,7 +805,7 @@ func (o *baseObject) _putProp(name string, value Value, writable, enumerable, co func (o *baseObject) _putSym(s *valueSymbol, prop Value) { if o.symValues == nil { - o.symValues = newOrderedMap() + o.symValues = newOrderedMap(&o.val.runtime.hash) } o.symValues.set(s, prop) } diff --git a/runtime.go b/runtime.go index 1176117e..0e78de67 100644 --- a/runtime.go +++ b/runtime.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "go/ast" + "hash/maphash" "math" "math/rand" "reflect" @@ -146,7 +147,8 @@ type Runtime struct { typeInfoCache map[reflect.Type]*reflectTypeInfo fieldNameMapper FieldNameMapper - vm *vm + vm *vm + hash maphash.Hash } type StackFrame struct { diff --git a/string_ascii.go b/string_ascii.go index 8cbd443e..b55dd7fb 100644 --- a/string_ascii.go +++ b/string_ascii.go @@ -2,6 +2,7 @@ package goja import ( "fmt" + "hash" "io" "math" "reflect" @@ -217,10 +218,10 @@ func (s asciiString) baseObject(r *Runtime) *Object { return ss.val } -func (s asciiString) hash() uint64 { - _, _ = mapHasher.WriteString(string(s)) - h := mapHasher.Sum64() - mapHasher.Reset() +func (s asciiString) hash(hash hash.Hash64) uint64 { + _, _ = hash.Write([]byte(s)) + h := hash.Sum64() + hash.Reset() return h } diff --git a/string_unicode.go b/string_unicode.go index b3cd74d6..bbc2626b 100644 --- a/string_unicode.go +++ b/string_unicode.go @@ -6,6 +6,7 @@ import ( "github.com/dop251/goja/parser" "golang.org/x/text/cases" "golang.org/x/text/language" + "hash" "io" "math" "reflect" @@ -299,9 +300,9 @@ func (s unicodeString) ExportType() reflect.Type { return reflectTypeString } -func (s unicodeString) hash() uint64 { - _, _ = mapHasher.Write(*(*[]byte)(unsafe.Pointer(&s))) - h := mapHasher.Sum64() - mapHasher.Reset() +func (s unicodeString) hash(hash hash.Hash64) uint64 { + _, _ = hash.Write(*(*[]byte)(unsafe.Pointer(&s))) + h := hash.Sum64() + hash.Reset() return h } diff --git a/value.go b/value.go index 6ef9d592..a3cdded2 100644 --- a/value.go +++ b/value.go @@ -2,7 +2,7 @@ package goja import ( "fmt" - "hash/maphash" + "hash" "math" "reflect" "regexp" @@ -33,10 +33,6 @@ var ( reflectTypeString = reflect.TypeOf("") ) -var ( - mapHasher maphash.Hash -) - var intCache [256]Value type Value interface { @@ -56,7 +52,7 @@ type Value interface { baseObject(r *Runtime) *Object - hash() uint64 + hash(hash64 hash.Hash64) uint64 } type typeError string @@ -193,7 +189,7 @@ func (i valueInt) ExportType() reflect.Type { return reflectTypeInt } -func (i valueInt) hash() uint64 { +func (i valueInt) hash(hash.Hash64) uint64 { return uint64(i) } @@ -283,7 +279,7 @@ func (o valueBool) ExportType() reflect.Type { return reflectTypeBool } -func (b valueBool) hash() uint64 { +func (b valueBool) hash(hash.Hash64) uint64 { if b { return uint64(uintptr(unsafe.Pointer(&valueTrue))) } @@ -336,7 +332,7 @@ func (u valueUndefined) ToFloat() float64 { return math.NaN() } -func (u valueUndefined) hash() uint64 { +func (u valueUndefined) hash(hash.Hash64) uint64 { return uint64(uintptr(unsafe.Pointer(&_undefined))) } @@ -388,7 +384,7 @@ func (n valueNull) ExportType() reflect.Type { return reflectTypeNil } -func (n valueNull) hash() uint64 { +func (n valueNull) hash(hash.Hash64) uint64 { return uint64(uintptr(unsafe.Pointer(&_null))) } @@ -481,7 +477,7 @@ func (n *valueProperty) ExportType() reflect.Type { panic("Cannot export valueProperty") } -func (n *valueProperty) hash() uint64 { +func (n *valueProperty) hash(hash.Hash64) uint64 { panic("valueProperty should never be used in maps or sets") } @@ -606,7 +602,7 @@ func (f valueFloat) ExportType() reflect.Type { return reflectTypeFloat } -func (f valueFloat) hash() uint64 { +func (f valueFloat) hash(hash.Hash64) uint64 { if f == _negativeZero { return 0 } @@ -686,7 +682,7 @@ func (o *Object) ExportType() reflect.Type { return o.self.exportType() } -func (o *Object) hash() uint64 { +func (o *Object) hash(hash.Hash64) uint64 { return uint64(uintptr(unsafe.Pointer(o))) } @@ -832,7 +828,7 @@ func (o valueUnresolved) ExportType() reflect.Type { return nil } -func (o valueUnresolved) hash() uint64 { +func (o valueUnresolved) hash(hash.Hash64) uint64 { o.throw() return 0 } @@ -896,7 +892,7 @@ func (s *valueSymbol) baseObject(r *Runtime) *Object { return r.newPrimitiveObject(s, r.global.SymbolPrototype, "Symbol") } -func (s *valueSymbol) hash() uint64 { +func (s *valueSymbol) hash(hash.Hash64) uint64 { return uint64(uintptr(unsafe.Pointer(s))) } From 44654ab7ccef0f4625a1d181a724fd5548154d00 Mon Sep 17 00:00:00 2001 From: Dmitry Panov Date: Mon, 30 Mar 2020 01:18:18 +0100 Subject: [PATCH 18/46] Travis s390x (#136) * Enabled travis build on s390x (which is big endian) --- .travis.yml | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0556ae71..5d48ef9a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,9 +2,24 @@ language: go go: - 1.x +arch: + - amd64 + - s390x + env: - - GIMME_OS=linux GIMME_ARCH=amd64 RACE="-race" - - GIMME_OS=linux GIMME_ARCH=386 + - RACE="-race" + - GIMME_ARCH=386 + - RACE="" + +jobs: + exclude: + - arch: s390x + env: GIMME_ARCH=386 + - arch: s390x + env: RACE="-race" + - arch: amd64 + env: RACE="" + before_install: # use local source tree when testing forks From 1236576b0dcea8441829871349e2862cd53ce168 Mon Sep 17 00:00:00 2001 From: Dmitry Panov Date: Sat, 4 Apr 2020 12:40:05 +0100 Subject: [PATCH 19/46] Typedarrays (#137) Implemented ArrayBuffer, DataView and the typed arrays. --- array.go | 2 +- builtin_array.go | 54 +- builtin_function.go | 2 +- builtin_map.go | 8 +- builtin_map_test.go | 44 +- builtin_proxy.go | 15 +- builtin_proxy_test.go | 19 +- builtin_regexp.go | 2 +- builtin_set.go | 8 +- builtin_string.go | 6 +- builtin_typedarrays.go | 1407 +++++++++++++++++++++++++++++++++-- builtin_typedarrays_test.go | 254 +++++++ builtin_weakmap.go | 23 +- builtin_weakmap_test.go | 1 + builtin_weakset.go | 8 +- builtin_weakset_test.go | 1 + date_test.go | 13 + object.go | 26 - parser/statement.go | 16 +- proxy.go | 42 +- runtime.go | 227 +++++- tc39_test.go | 140 ++-- token/token_const.go | 3 - typedarrays.go | 884 ++++++++++++++++++++++ typedarrays_test.go | 71 ++ value.go | 7 +- vm.go | 8 +- 27 files changed, 3059 insertions(+), 232 deletions(-) create mode 100644 typedarrays.go create mode 100644 typedarrays_test.go diff --git a/array.go b/array.go index 5d795ba2..4c903605 100644 --- a/array.go +++ b/array.go @@ -422,7 +422,7 @@ func (a *arrayObject) _defineIdxProperty(idx uint32, desc PropertyDescriptor, th } prop, ok := a.baseObject._defineOwnProperty(strconv.FormatUint(uint64(idx), 10), existing, desc, throw) if ok { - if idx >= uint32(a.length) { + if idx >= a.length { if !a.setLengthInt(int64(idx)+1, throw) { return false } diff --git a/builtin_array.go b/builtin_array.go index 9b521700..4d38ff44 100644 --- a/builtin_array.go +++ b/builtin_array.go @@ -176,7 +176,7 @@ func (r *Runtime) arrayproto_join(call FunctionCall) Value { l := int(toLength(o.self.getStr("length", nil))) sep := "" if s := call.Argument(0); s != _undefined { - sep = s.String() + sep = s.toString().String() } else { sep = "," } @@ -519,14 +519,55 @@ func (r *Runtime) arrayproto_indexOf(call FunctionCall) Value { } } - return valueInt(-1) + return intToValue(-1) +} + +func (r *Runtime) arrayproto_includes(call FunctionCall) Value { + o := call.This.ToObject(r) + length := toLength(o.self.getStr("length", nil)) + if length == 0 { + return valueFalse + } + + n := call.Argument(1).ToInteger() + if n >= length { + return valueFalse + } + + if n < 0 { + n = max(length+n, 0) + } + + searchElement := call.Argument(0) + if searchElement == _negativeZero { + searchElement = _positiveZero + } + + if arr := r.checkStdArrayObj(o); arr != nil { + for _, val := range arr.values[n:] { + if searchElement.SameAs(val) { + return valueTrue + } + } + return valueFalse + } + + for ; n < length; n++ { + idx := valueInt(n) + val := nilSafe(o.self.getIdx(idx, nil)) + if searchElement.SameAs(val) { + return valueTrue + } + } + + return valueFalse } func (r *Runtime) arrayproto_lastIndexOf(call FunctionCall) Value { o := call.This.ToObject(r) length := toLength(o.self.getStr("length", nil)) if length == 0 { - return valueInt(-1) + return intToValue(-1) } var fromIndex int64 @@ -975,7 +1016,7 @@ func (r *Runtime) arrayproto_findIndex(call FunctionCall) Value { } } - return valueInt(-1) + return intToValue(-1) } func (r *Runtime) checkStdArrayObj(obj *Object) *arrayObject { @@ -1160,6 +1201,7 @@ func (r *Runtime) createArrayProto(val *Object) objectImpl { o._putProp("find", r.newNativeFunc(r.arrayproto_find, nil, "find", nil, 1), true, false, true) o._putProp("findIndex", r.newNativeFunc(r.arrayproto_findIndex, nil, "findIndex", nil, 1), true, false, true) o._putProp("forEach", r.newNativeFunc(r.arrayproto_forEach, nil, "forEach", nil, 1), true, false, true) + o._putProp("includes", r.newNativeFunc(r.arrayproto_includes, nil, "includes", nil, 1), true, false, true) o._putProp("indexOf", r.newNativeFunc(r.arrayproto_indexOf, nil, "indexOf", nil, 1), true, false, true) o._putProp("join", r.newNativeFunc(r.arrayproto_join, nil, "join", nil, 1), true, false, true) o._putProp("keys", r.newNativeFunc(r.arrayproto_keys, nil, "keys", nil, 0), true, false, true) @@ -1176,7 +1218,7 @@ func (r *Runtime) createArrayProto(val *Object) objectImpl { o._putProp("sort", r.newNativeFunc(r.arrayproto_sort, nil, "sort", nil, 1), true, false, true) o._putProp("splice", r.newNativeFunc(r.arrayproto_splice, nil, "splice", nil, 2), true, false, true) o._putProp("toLocaleString", r.newNativeFunc(r.arrayproto_toLocaleString, nil, "toLocaleString", nil, 0), true, false, true) - o._putProp("toString", r.newNativeFunc(r.arrayproto_toString, nil, "toString", nil, 0), true, false, true) + o._putProp("toString", r.global.arrayToString, true, false, true) o._putProp("unshift", r.newNativeFunc(r.arrayproto_unshift, nil, "unshift", nil, 1), true, false, true) valuesFunc := r.newNativeFunc(r.arrayproto_values, nil, "values", nil, 0) r.global.arrayValues = valuesFunc @@ -1221,6 +1263,8 @@ func (r *Runtime) createArrayIterProto(val *Object) objectImpl { } func (r *Runtime) initArray() { + r.global.arrayToString = r.newNativeFunc(r.arrayproto_toString, nil, "toString", nil, 0) + r.global.ArrayIteratorPrototype = r.newLazyObject(r.createArrayIterProto) //r.global.ArrayPrototype = r.newArray(r.global.ObjectPrototype).val //o := r.global.ArrayPrototype.self diff --git a/builtin_function.go b/builtin_function.go index f5b75f87..7cc45f79 100644 --- a/builtin_function.go +++ b/builtin_function.go @@ -139,7 +139,7 @@ func (r *Runtime) functionproto_bind(call FunctionCall) Value { fcall := r.toCallable(call.This) construct := obj.self.assertConstructor() - l := int(toUInt32(obj.self.getStr("length", nil))) + l := int(toUint32(obj.self.getStr("length", nil))) l -= len(call.Arguments) - 1 if l < 0 { l = 0 diff --git a/builtin_map.go b/builtin_map.go index 7cb464c8..d79a51c6 100644 --- a/builtin_map.go +++ b/builtin_map.go @@ -138,7 +138,11 @@ func (r *Runtime) mapProto_getSize(call FunctionCall) Value { return intToValue(int64(mo.m.size)) } -func (r *Runtime) builtin_newMap(args []Value, proto *Object) *Object { +func (r *Runtime) builtin_newMap(args []Value, newTarget *Object) *Object { + if newTarget == nil { + panic(r.needNew("Map")) + } + proto := r.getPrototypeFromCtor(newTarget, r.global.Map, r.global.MapPrototype) o := &Object{runtime: r} mo := &mapObject{} @@ -238,7 +242,7 @@ func (r *Runtime) createMapProto(val *Object) objectImpl { } func (r *Runtime) createMap(val *Object) objectImpl { - o := r.newNativeFuncObj(val, r.constructorThrower("Map"), r.builtin_newMap, "Map", r.global.MapPrototype, 0) + o := r.newNativeConstructOnly(val, r.builtin_newMap, r.global.MapPrototype, "Map", 0) o._putSym(symSpecies, &valueProperty{ getterFunc: r.newNativeFunc(r.returnThis, nil, "get [Symbol.species]", nil, 0), accessor: true, diff --git a/builtin_map_test.go b/builtin_map_test.go index 691eb320..7880305b 100644 --- a/builtin_map_test.go +++ b/builtin_map_test.go @@ -1,6 +1,9 @@ package goja -import "testing" +import ( + "hash/maphash" + "testing" +) func TestMapEvilIterator(t *testing.T) { const SCRIPT = ` @@ -57,3 +60,42 @@ func TestMapEvilIterator(t *testing.T) { ` testScript1(TESTLIB+SCRIPT, _undefined, t) } + +func BenchmarkMapDelete(b *testing.B) { + var key1 Value = asciiString("a") + var key2 Value = asciiString("b") + one := intToValue(1) + two := intToValue(2) + for i := 0; i < b.N; i++ { + m := newOrderedMap(&maphash.Hash{}) + m.set(key1, one) + m.set(key2, two) + if !m.remove(key1) { + b.Fatal("remove() returned false") + } + } +} + +func BenchmarkMapDeleteJS(b *testing.B) { + prg, err := Compile("test.js", ` + var m = new Map([['a',1], ['b', 2]]); + + var result = m.delete('a'); + + if (!result || m.size !== 1) { + throw new Error("Fail!"); + } + `, + false) + if err != nil { + b.Fatal(err) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + vm := New() + _, err := vm.RunProgram(prg) + if err != nil { + b.Fatal(err) + } + } +} diff --git a/builtin_proxy.go b/builtin_proxy.go index faae41cc..7f2f479a 100644 --- a/builtin_proxy.go +++ b/builtin_proxy.go @@ -239,14 +239,17 @@ func (r *Runtime) newProxy(args []Value, proto *Object) *Object { panic(r.NewTypeError("Cannot create proxy with a non-object as target or handler")) } -func (r *Runtime) builtin_newProxy(args []Value, proto *Object) *Object { - return r.newProxy(args, proto) +func (r *Runtime) builtin_newProxy(args []Value, newTarget *Object) *Object { + if newTarget == nil { + panic(r.needNew("Proxy")) + } + return r.newProxy(args, r.getPrototypeFromCtor(newTarget, r.global.Proxy, r.global.ObjectPrototype)) } -func (r *Runtime) NewProxy(target *Object, nativeHandler *ProxyTrapConfig) *Proxy { +func (r *Runtime) NewProxy(target *Object, nativeHandler *ProxyTrapConfig) Proxy { handler := r.newNativeProxyHandler(nativeHandler) - proxy := r.newProxyObject(target, handler, r.global.Proxy) - return &Proxy{proxy: proxy} + proxy := r.newProxyObject(target, handler, nil) + return Proxy{proxy: proxy} } func (r *Runtime) builtin_proxy_revocable(call FunctionCall) Value { @@ -269,7 +272,7 @@ func (r *Runtime) builtin_proxy_revocable(call FunctionCall) Value { } func (r *Runtime) createProxy(val *Object) objectImpl { - o := r.newNativeFuncObj(val, r.constructorThrower("Proxy"), r.builtin_newProxy, "Proxy", nil, 2) + o := r.newNativeConstructOnly(val, r.builtin_newProxy, nil, "Proxy", 2) o._putProp("revocable", r.newNativeFunc(r.builtin_proxy_revocable, nil, "revocable", nil, 2), true, false, true) return o diff --git a/builtin_proxy_test.go b/builtin_proxy_test.go index fc0a50f6..7cf5fe14 100644 --- a/builtin_proxy_test.go +++ b/builtin_proxy_test.go @@ -58,7 +58,7 @@ func TestProxy_Object_native_proxy_getPrototypeOf(t *testing.T) { } } -/*func TestProxy_Object_target_setPrototypeOf(t *testing.T) { +func TestProxy_Object_target_setPrototypeOf(t *testing.T) { const SCRIPT = ` var proto = {}; var obj = {}; @@ -82,12 +82,13 @@ func TestProxy_Object_proxy_setPrototypeOf(t *testing.T) { return Object.setPrototypeOf(target, proto2); } }); + Object.setPrototypeOf(proxy, null); var p = Object.getPrototypeOf(proxy); assert.sameValue(proto2, p); ` testScript1(TESTLIB+SCRIPT, _undefined, t) -}*/ +} func TestProxy_Object_target_isExtensible(t *testing.T) { const SCRIPT = ` @@ -826,3 +827,17 @@ func TestProxy_proxy_forIn(t *testing.T) { testScript1(SCRIPT, valueTrue, t) } + +func TestProxyExport(t *testing.T) { + vm := New() + v, err := vm.RunString(` + new Proxy({}, {}); + `) + if err != nil { + t.Fatal(err) + } + v1 := v.Export() + if _, ok := v1.(Proxy); !ok { + t.Fatalf("Export returned unexpected type: %T", v1) + } +} diff --git a/builtin_regexp.go b/builtin_regexp.go index 76cbc732..573dc765 100644 --- a/builtin_regexp.go +++ b/builtin_regexp.go @@ -513,7 +513,7 @@ func (r *Runtime) regexpproto_stdSplitter(call FunctionCall) Value { limit := -1 if limitValue != _undefined { - limit = int(toUInt32(limitValue)) + limit = int(toUint32(limitValue)) } if limit == 0 { diff --git a/builtin_set.go b/builtin_set.go index 0ef9e658..e063caa2 100644 --- a/builtin_set.go +++ b/builtin_set.go @@ -121,7 +121,11 @@ func (r *Runtime) setProto_values(call FunctionCall) Value { return r.createSetIterator(call.This, iterationKindValue) } -func (r *Runtime) builtin_newSet(args []Value, proto *Object) *Object { +func (r *Runtime) builtin_newSet(args []Value, newTarget *Object) *Object { + if newTarget == nil { + panic(r.needNew("Set")) + } + proto := r.getPrototypeFromCtor(newTarget, r.global.Set, r.global.SetPrototype) o := &Object{runtime: r} so := &setObject{} @@ -213,7 +217,7 @@ func (r *Runtime) createSetProto(val *Object) objectImpl { } func (r *Runtime) createSet(val *Object) objectImpl { - o := r.newNativeFuncObj(val, r.constructorThrower("Set"), r.builtin_newSet, "Set", r.global.SetPrototype, 0) + o := r.newNativeConstructOnly(val, r.builtin_newSet, r.global.SetPrototype, "Set", 0) o._putSym(symSpecies, &valueProperty{ getterFunc: r.newNativeFunc(r.returnThis, nil, "get [Symbol.species]", nil, 0), accessor: true, diff --git a/builtin_string.go b/builtin_string.go index 4a6113ff..cfb39d37 100644 --- a/builtin_string.go +++ b/builtin_string.go @@ -102,7 +102,7 @@ func (r *Runtime) stringproto_valueOf(call FunctionCall) Value { func (r *Runtime) string_fromcharcode(call FunctionCall) Value { b := make([]byte, len(call.Arguments)) for i, arg := range call.Arguments { - chr := toUInt16(arg) + chr := toUint16(arg) if chr >= utf8.RuneSelf { bb := make([]uint16, len(call.Arguments)) for j := 0; j < i; j++ { @@ -111,7 +111,7 @@ func (r *Runtime) string_fromcharcode(call FunctionCall) Value { bb[i] = chr i++ for j, arg := range call.Arguments[i:] { - bb[i+j] = toUInt16(arg) + bb[i+j] = toUint16(arg) } return unicodeString(bb) } @@ -501,7 +501,7 @@ func (r *Runtime) stringproto_split(call FunctionCall) Value { limit := -1 if limitValue != _undefined { - limit = int(toUInt32(limitValue)) + limit = int(toUint32(limitValue)) } if limit == 0 { diff --git a/builtin_typedarrays.go b/builtin_typedarrays.go index 507c1843..59e679a0 100644 --- a/builtin_typedarrays.go +++ b/builtin_typedarrays.go @@ -1,33 +1,55 @@ package goja -type objectArrayBuffer struct { - baseObject - data []byte +import ( + "sort" + "strings" + "unsafe" +) + +type typedArraySortCtx struct { + ta *typedArrayObject + compare func(FunctionCall) Value + needValidate bool } -func (o *objectArrayBuffer) export() interface{} { - return o.data +func (ctx *typedArraySortCtx) Len() int { + return ctx.ta.length } -func (r *Runtime) _newArrayBuffer(proto *Object, o *Object) *objectArrayBuffer { - if o == nil { - o = &Object{runtime: r} +func (ctx *typedArraySortCtx) Less(i, j int) bool { + if ctx.needValidate { + ctx.ta.viewedArrayBuf.ensureNotDetached() + ctx.needValidate = false } - b := &objectArrayBuffer{ - baseObject: baseObject{ - class: classObject, - val: o, - prototype: proto, - extensible: true, - }, + offset := ctx.ta.offset + if ctx.compare != nil { + x := ctx.ta.typedArray.get(offset + i) + y := ctx.ta.typedArray.get(offset + j) + res := ctx.compare(FunctionCall{ + This: _undefined, + Arguments: []Value{x, y}, + }).ToInteger() + ctx.needValidate = true + return res < 0 } - o.self = b - b.init() - return b + + return ctx.ta.typedArray.less(offset+i, offset+j) +} + +func (ctx *typedArraySortCtx) Swap(i, j int) { + if ctx.needValidate { + ctx.ta.viewedArrayBuf.ensureNotDetached() + ctx.needValidate = false + } + offset := ctx.ta.offset + ctx.ta.typedArray.swap(offset+i, offset+j) } -func (r *Runtime) builtin_ArrayBuffer(args []Value, proto *Object) *Object { - b := r._newArrayBuffer(proto, nil) +func (r *Runtime) builtin_newArrayBuffer(args []Value, newTarget *Object) *Object { + if newTarget == nil { + panic(r.needNew("ArrayBuffer")) + } + b := r._newArrayBuffer(r.getPrototypeFromCtor(newTarget, r.global.ArrayBuffer, r.global.ArrayBufferPrototype), nil) if len(args) > 0 { b.data = make([]byte, toLength(args[0])) } @@ -36,70 +58,1357 @@ func (r *Runtime) builtin_ArrayBuffer(args []Value, proto *Object) *Object { func (r *Runtime) arrayBufferProto_getByteLength(call FunctionCall) Value { o := r.toObject(call.This) - if b, ok := o.self.(*objectArrayBuffer); ok { + if b, ok := o.self.(*arrayBufferObject); ok { + if b.data == nil { + panic(r.NewTypeError("ArrayBuffer is detached")) + } return intToValue(int64(len(b.data))) } - r.typeErrorResult(true, "Object is not ArrayBuffer: %s", o) - panic("unreachable") + panic(r.NewTypeError("Object is not ArrayBuffer: %s", o)) } func (r *Runtime) arrayBufferProto_slice(call FunctionCall) Value { o := r.toObject(call.This) - if b, ok := o.self.(*objectArrayBuffer); ok { + if b, ok := o.self.(*arrayBufferObject); ok { l := int64(len(b.data)) - start := toLength(call.Argument(0)) - if start < 0 { - start = l + start - } - if start < 0 { - start = 0 - } else if start > l { - start = l - } + start := relToIdx(toLength(call.Argument(0)), l) var stop int64 if arg := call.Argument(1); arg != _undefined { stop = toLength(arg) - if stop < 0 { - stop = int64(len(b.data)) + stop + } else { + stop = l + } + stop = relToIdx(stop, l) + newLen := max(stop-start, 0) + ret := r.speciesConstructor(o, r.global.ArrayBuffer)([]Value{intToValue(newLen)}, nil) + if ab, ok := ret.self.(*arrayBufferObject); ok { + if ab.data == nil { + panic(r.NewTypeError("Species constructor returned a detached ArrayBuffer")) } - if stop < 0 { - stop = 0 - } else if stop > l { - stop = l + if ret == o { + panic(r.NewTypeError("Species constructor returned the same ArrayBuffer")) } + if int64(len(ab.data)) < newLen { + panic(r.NewTypeError("Species constructor returned an ArrayBuffer that is too small: %d", len(ab.data))) + } + if b.data == nil { + panic(r.NewTypeError("Species constructor has detached the current ArrayBuffer")) + } + + if stop > start { + copy(ab.data, b.data[start:stop]) + } + return ret + } + panic(r.NewTypeError("Species constructor did not return an ArrayBuffer: %s", ret.String())) + } + panic(r.NewTypeError("Object is not ArrayBuffer: %s", o)) +} + +func (r *Runtime) arrayBuffer_isView(call FunctionCall) Value { + if o, ok := call.This.(*Object); ok { + if _, ok := o.self.(*dataViewObject); ok { + return valueTrue + } + } + return valueFalse +} + +func (r *Runtime) newDataView(args []Value, newTarget *Object) *Object { + if newTarget == nil { + panic(r.needNew("DataView")) + } + proto := r.getPrototypeFromCtor(newTarget, r.global.DataView, r.global.DataViewPrototype) + var bufArg Value + if len(args) > 0 { + bufArg = args[0] + } + var buffer *arrayBufferObject + if o, ok := bufArg.(*Object); ok { + if b, ok := o.self.(*arrayBufferObject); ok { + buffer = b + } + } + if buffer == nil { + panic(r.NewTypeError("First argument to DataView constructor must be an ArrayBuffer")) + } + var byteOffset, byteLen int + if len(args) > 1 { + offsetArg := nilSafe(args[1]) + byteOffset = r.toIndex(offsetArg) + buffer.ensureNotDetached() + if byteOffset > len(buffer.data) { + panic(r.newError(r.global.RangeError, "Start offset %s is outside the bounds of the buffer", offsetArg.String())) + } + } + if len(args) > 2 && args[2] != nil && args[2] != _undefined { + byteLen = r.toIndex(args[2]) + if byteOffset+byteLen > len(buffer.data) { + panic(r.newError(r.global.RangeError, "Invalid DataView length %d", byteLen)) + } + } else { + byteLen = len(buffer.data) - byteOffset + } + o := &Object{runtime: r} + b := &dataViewObject{ + baseObject: baseObject{ + class: classObject, + val: o, + prototype: proto, + extensible: true, + }, + viewedArrayBuf: buffer, + byteOffset: byteOffset, + byteLen: byteLen, + } + o.self = b + b.init() + return o +} + +func (r *Runtime) dataViewProto_getBuffer(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + return dv.viewedArrayBuf.val + } + panic(r.NewTypeError("Method get DataView.prototype.buffer called on incompatible receiver %s", call.This.String())) +} + +func (r *Runtime) dataViewProto_getByteLen(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + dv.viewedArrayBuf.ensureNotDetached() + return intToValue(int64(dv.byteLen)) + } + panic(r.NewTypeError("Method get DataView.prototype.byteLength called on incompatible receiver %s", call.This.String())) +} + +func (r *Runtime) dataViewProto_getByteOffset(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + dv.viewedArrayBuf.ensureNotDetached() + return intToValue(int64(dv.byteOffset)) + } + panic(r.NewTypeError("Method get DataView.prototype.byteOffset called on incompatible receiver %s", call.This.String())) +} + +func (r *Runtime) dataViewProto_getFloat32(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + return floatToValue(float64(dv.viewedArrayBuf.getFloat32(dv.getIdxAndByteOrder(call.Argument(0), call.Argument(1), 4)))) + } + panic(r.NewTypeError("Method DataView.prototype.getFloat32 called on incompatible receiver %s", call.This.String())) +} + +func (r *Runtime) dataViewProto_getFloat64(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + return floatToValue(dv.viewedArrayBuf.getFloat64(dv.getIdxAndByteOrder(call.Argument(0), call.Argument(1), 8))) + } + panic(r.NewTypeError("Method DataView.prototype.getFloat64 called on incompatible receiver %s", call.This.String())) +} + +func (r *Runtime) dataViewProto_getInt8(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + idx, _ := dv.getIdxAndByteOrder(call.Argument(0), call.Argument(1), 1) + return intToValue(int64(dv.viewedArrayBuf.getInt8(idx))) + } + panic(r.NewTypeError("Method DataView.prototype.getInt8 called on incompatible receiver %s", call.This.String())) +} + +func (r *Runtime) dataViewProto_getInt16(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + return intToValue(int64(dv.viewedArrayBuf.getInt16(dv.getIdxAndByteOrder(call.Argument(0), call.Argument(1), 2)))) + } + panic(r.NewTypeError("Method DataView.prototype.getInt16 called on incompatible receiver %s", call.This.String())) +} + +func (r *Runtime) dataViewProto_getInt32(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + return intToValue(int64(dv.viewedArrayBuf.getInt32(dv.getIdxAndByteOrder(call.Argument(0), call.Argument(1), 4)))) + } + panic(r.NewTypeError("Method DataView.prototype.getInt32 called on incompatible receiver %s", call.This.String())) +} + +func (r *Runtime) dataViewProto_getUint8(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + idx, _ := dv.getIdxAndByteOrder(call.Argument(0), call.Argument(1), 1) + return intToValue(int64(dv.viewedArrayBuf.getUint8(idx))) + } + panic(r.NewTypeError("Method DataView.prototype.getUint8 called on incompatible receiver %s", call.This.String())) +} + +func (r *Runtime) dataViewProto_getUint16(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + return intToValue(int64(dv.viewedArrayBuf.getUint16(dv.getIdxAndByteOrder(call.Argument(0), call.Argument(1), 2)))) + } + panic(r.NewTypeError("Method DataView.prototype.getUint16 called on incompatible receiver %s", call.This.String())) +} + +func (r *Runtime) dataViewProto_getUint32(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + return intToValue(int64(dv.viewedArrayBuf.getUint32(dv.getIdxAndByteOrder(call.Argument(0), call.Argument(1), 4)))) + } + panic(r.NewTypeError("Method DataView.prototype.getUint32 called on incompatible receiver %s", call.This.String())) +} + +func (r *Runtime) dataViewProto_setFloat32(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + val := toFloat32(call.Argument(1)) + idx, bo := dv.getIdxAndByteOrder(call.Argument(0), call.Argument(2), 4) + dv.viewedArrayBuf.setFloat32(idx, val, bo) + return _undefined + } + panic(r.NewTypeError("Method DataView.prototype.setFloat32 called on incompatible receiver %s", call.This.String())) +} + +func (r *Runtime) dataViewProto_setFloat64(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + val := call.Argument(1).ToFloat() + idx, bo := dv.getIdxAndByteOrder(call.Argument(0), call.Argument(2), 8) + dv.viewedArrayBuf.setFloat64(idx, val, bo) + return _undefined + } + panic(r.NewTypeError("Method DataView.prototype.setFloat64 called on incompatible receiver %s", call.This.String())) +} + +func (r *Runtime) dataViewProto_setInt8(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + val := toInt8(call.Argument(1)) + idx, _ := dv.getIdxAndByteOrder(call.Argument(0), call.Argument(2), 1) + dv.viewedArrayBuf.setInt8(idx, val) + return _undefined + } + panic(r.NewTypeError("Method DataView.prototype.setInt8 called on incompatible receiver %s", call.This.String())) +} + +func (r *Runtime) dataViewProto_setInt16(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + val := toInt16(call.Argument(1)) + idx, bo := dv.getIdxAndByteOrder(call.Argument(0), call.Argument(2), 2) + dv.viewedArrayBuf.setInt16(idx, val, bo) + return _undefined + } + panic(r.NewTypeError("Method DataView.prototype.setInt16 called on incompatible receiver %s", call.This.String())) +} + +func (r *Runtime) dataViewProto_setInt32(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + val := toInt32(call.Argument(1)) + idx, bo := dv.getIdxAndByteOrder(call.Argument(0), call.Argument(2), 4) + dv.viewedArrayBuf.setInt32(idx, val, bo) + return _undefined + } + panic(r.NewTypeError("Method DataView.prototype.setInt32 called on incompatible receiver %s", call.This.String())) +} + +func (r *Runtime) dataViewProto_setUint8(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + val := toUint8(call.Argument(1)) + idx, _ := dv.getIdxAndByteOrder(call.Argument(0), call.Argument(2), 1) + dv.viewedArrayBuf.setUint8(idx, val) + return _undefined + } + panic(r.NewTypeError("Method DataView.prototype.setUint8 called on incompatible receiver %s", call.This.String())) +} + +func (r *Runtime) dataViewProto_setUint16(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + val := toUint16(call.Argument(1)) + idx, bo := dv.getIdxAndByteOrder(call.Argument(0), call.Argument(2), 2) + dv.viewedArrayBuf.setUint16(idx, val, bo) + return _undefined + } + panic(r.NewTypeError("Method DataView.prototype.setUint16 called on incompatible receiver %s", call.This.String())) +} +func (r *Runtime) dataViewProto_setUint32(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + val := toUint32(call.Argument(1)) + idx, bo := dv.getIdxAndByteOrder(call.Argument(0), call.Argument(2), 4) + dv.viewedArrayBuf.setUint32(idx, val, bo) + return _undefined + } + panic(r.NewTypeError("Method DataView.prototype.setUint32 called on incompatible receiver %s", call.This.String())) +} + +func (r *Runtime) typedArrayProto_getBuffer(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + return ta.viewedArrayBuf.val + } + panic(r.NewTypeError("Method get TypedArray.prototype.buffer called on incompatible receiver %s", call.This.String())) +} + +func (r *Runtime) typedArrayProto_getByteLen(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + if ta.viewedArrayBuf.data == nil { + return _positiveZero + } + return intToValue(int64(ta.length) * int64(ta.elemSize)) + } + panic(r.NewTypeError("Method get TypedArray.prototype.byteLength called on incompatible receiver %s", call.This.String())) +} + +func (r *Runtime) typedArrayProto_getLength(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + if ta.viewedArrayBuf.data == nil { + return _positiveZero + } + return intToValue(int64(ta.length)) + } + panic(r.NewTypeError("Method get TypedArray.prototype.length called on incompatible receiver %s", call.This.String())) +} + +func (r *Runtime) typedArrayProto_getByteOffset(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + if ta.viewedArrayBuf.data == nil { + return _positiveZero + } + return intToValue(int64(ta.offset) * int64(ta.elemSize)) + } + panic(r.NewTypeError("Method get TypedArray.prototype.byteOffset called on incompatible receiver %s", call.This.String())) +} + +func (r *Runtime) typedArrayProto_copyWithin(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached() + l := int64(ta.length) + var relEnd int64 + to := toInt(relToIdx(call.Argument(0).ToInteger(), l)) + from := toInt(relToIdx(call.Argument(1).ToInteger(), l)) + if end := call.Argument(2); end != _undefined { + relEnd = end.ToInteger() } else { - stop = l + relEnd = l + } + final := toInt(relToIdx(relEnd, l)) + data := ta.viewedArrayBuf.data + offset := ta.offset + elemSize := ta.elemSize + if final > from { + copy(data[(offset+to)*elemSize:], data[(offset+from)*elemSize:(offset+final)*elemSize]) + } + return call.This + } + panic(r.NewTypeError("Method TypedArray.prototype.copyWithin called on incompatible receiver %s", call.This.String())) +} + +func (r *Runtime) typedArrayProto_entries(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached() + return r.createArrayIterator(ta.val, iterationKindKeyValue) + } + panic(r.NewTypeError("Method TypedArray.prototype.entries called on incompatible receiver %s", call.This.String())) +} + +func (r *Runtime) typedArrayProto_every(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached() + callbackFn := r.toCallable(call.Argument(0)) + fc := FunctionCall{ + This: call.Argument(1), + Arguments: []Value{nil, nil, call.This}, + } + for k := 0; k < ta.length; k++ { + ta.viewedArrayBuf.ensureNotDetached() + fc.Arguments[0] = ta.typedArray.get(ta.offset + k) + fc.Arguments[1] = intToValue(int64(k)) + if !callbackFn(fc).ToBoolean() { + return valueFalse + } + } + return valueTrue + + } + panic(r.NewTypeError("Method TypedArray.prototype.every called on incompatible receiver %s", call.This.String())) +} + +func (r *Runtime) typedArrayProto_fill(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached() + l := int64(ta.length) + k := toInt(relToIdx(call.Argument(1).ToInteger(), l)) + var relEnd int64 + if endArg := call.Argument(2); endArg != _undefined { + relEnd = endArg.ToInteger() + } else { + relEnd = l + } + final := toInt(relToIdx(relEnd, l)) + value := ta.typedArray.toRaw(call.Argument(0)) + ta.viewedArrayBuf.ensureNotDetached() + for ; k < final; k++ { + ta.typedArray.setRaw(ta.offset+k, value) + } + return call.This + } + panic(r.NewTypeError("Method TypedArray.prototype.fill called on incompatible receiver %s", call.This.String())) +} + +func (r *Runtime) typedArrayProto_filter(call FunctionCall) Value { + o := r.toObject(call.This) + if ta, ok := o.self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached() + callbackFn := r.toCallable(call.Argument(0)) + fc := FunctionCall{ + This: call.Argument(1), + Arguments: []Value{nil, nil, call.This}, + } + buf := make([]byte, 0, ta.length*ta.elemSize) + captured := 0 + for k := 0; k < ta.length; k++ { + ta.viewedArrayBuf.ensureNotDetached() + fc.Arguments[0] = ta.typedArray.get(k) + fc.Arguments[1] = intToValue(int64(k)) + if callbackFn(fc).ToBoolean() { + i := (ta.offset + k) * ta.elemSize + buf = append(buf, ta.viewedArrayBuf.data[i:i+ta.elemSize]...) + captured++ + } + } + c := r.speciesConstructorObj(o, ta.defaultCtor) + ab := r._newArrayBuffer(r.global.ArrayBufferPrototype, nil) + ab.data = buf + kept := r.toConstructor(ta.defaultCtor)([]Value{ab.val}, ta.defaultCtor) + if c == ta.defaultCtor { + return kept + } else { + ret := r.typedArrayCreate(c, []Value{intToValue(int64(captured))}) + keptTa := kept.self.(*typedArrayObject) + for i := 0; i < captured; i++ { + ret.typedArray.set(i, keptTa.typedArray.get(i)) + } + return ret.val + } + } + panic(r.NewTypeError("Method TypedArray.prototype.filter called on incompatible receiver %s", call.This.String())) +} + +func (r *Runtime) typedArrayProto_find(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached() + predicate := r.toCallable(call.Argument(0)) + fc := FunctionCall{ + This: call.Argument(1), + Arguments: []Value{nil, nil, call.This}, + } + for k := 0; k < ta.length; k++ { + ta.viewedArrayBuf.ensureNotDetached() + val := ta.typedArray.get(ta.offset + k) + fc.Arguments[0] = val + fc.Arguments[1] = intToValue(int64(k)) + if predicate(fc).ToBoolean() { + return val + } + } + return _undefined + } + panic(r.NewTypeError("Method TypedArray.prototype.find called on incompatible receiver %s", call.This.String())) +} + +func (r *Runtime) typedArrayProto_findIndex(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached() + predicate := r.toCallable(call.Argument(0)) + fc := FunctionCall{ + This: call.Argument(1), + Arguments: []Value{nil, nil, call.This}, + } + for k := 0; k < ta.length; k++ { + ta.viewedArrayBuf.ensureNotDetached() + fc.Arguments[0] = ta.typedArray.get(ta.offset + k) + fc.Arguments[1] = intToValue(int64(k)) + if predicate(fc).ToBoolean() { + return fc.Arguments[1] + } + } + return intToValue(-1) + } + panic(r.NewTypeError("Method TypedArray.prototype.findIndex called on incompatible receiver %s", call.This.String())) +} + +func (r *Runtime) typedArrayProto_forEach(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached() + callbackFn := r.toCallable(call.Argument(0)) + fc := FunctionCall{ + This: call.Argument(1), + Arguments: []Value{nil, nil, call.This}, + } + for k := 0; k < ta.length; k++ { + ta.viewedArrayBuf.ensureNotDetached() + if val := ta.typedArray.get(k); val != nil { + fc.Arguments[0] = val + fc.Arguments[1] = intToValue(int64(k)) + callbackFn(fc) + } } + return _undefined + } + panic(r.NewTypeError("Method TypedArray.prototype.forEach called on incompatible receiver %s", call.This.String())) +} - ret := r._newArrayBuffer(r.global.ArrayBufferPrototype, nil) +func (r *Runtime) typedArrayProto_includes(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached() + length := int64(ta.length) + if length == 0 { + return valueFalse + } - if stop > start { - ret.data = b.data[start:stop] + n := call.Argument(1).ToInteger() + if n >= length { + return valueFalse } - return ret.val + if n < 0 { + n = max(length+n, 0) + } + + ta.viewedArrayBuf.ensureNotDetached() + searchElement := call.Argument(0) + if searchElement == _negativeZero { + searchElement = _positiveZero + } + if ta.typedArray.typeMatch(searchElement) { + se := ta.typedArray.toRaw(searchElement) + for k := toInt(n); k < ta.length; k++ { + if ta.typedArray.getRaw(ta.offset+k) == se { + return valueTrue + } + } + } + return valueFalse } - r.typeErrorResult(true, "Object is not ArrayBuffer: %s", o) - panic("unreachable") + panic(r.NewTypeError("Method TypedArray.prototype.includes called on incompatible receiver %s", call.This.String())) +} + +func (r *Runtime) typedArrayProto_indexOf(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached() + length := int64(ta.length) + if length == 0 { + return intToValue(-1) + } + + n := call.Argument(1).ToInteger() + if n >= length { + return intToValue(-1) + } + + if n < 0 { + n = max(length+n, 0) + } + + ta.viewedArrayBuf.ensureNotDetached() + searchElement := call.Argument(0) + if searchElement == _negativeZero { + searchElement = _positiveZero + } + if !IsNaN(searchElement) && ta.typedArray.typeMatch(searchElement) { + se := ta.typedArray.toRaw(searchElement) + for k := toInt(n); k < ta.length; k++ { + if ta.typedArray.getRaw(ta.offset+k) == se { + return intToValue(int64(k)) + } + } + } + return intToValue(-1) + } + panic(r.NewTypeError("Method TypedArray.prototype.indexOf called on incompatible receiver %s", call.This.String())) +} + +func (r *Runtime) typedArrayProto_join(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached() + s := call.Argument(0) + sep := "" + if s != _undefined { + sep = s.toString().String() + } else { + sep = "," + } + l := ta.length + if l == 0 { + return stringEmpty + } + + var buf strings.Builder + + ta.viewedArrayBuf.ensureNotDetached() + element0 := ta.typedArray.get(0) + if element0 != nil && element0 != _undefined && element0 != _null { + buf.WriteString(element0.String()) + } + + for i := 1; i < l; i++ { + ta.viewedArrayBuf.ensureNotDetached() + buf.WriteString(sep) + element := ta.typedArray.get(i) + if element != nil && element != _undefined && element != _null { + buf.WriteString(element.String()) + } + } + + return newStringValue(buf.String()) + } + panic(r.NewTypeError("Method TypedArray.prototype.join called on incompatible receiver")) +} + +func (r *Runtime) typedArrayProto_keys(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached() + return r.createArrayIterator(ta.val, iterationKindKey) + } + panic(r.NewTypeError("Method TypedArray.prototype.keys called on incompatible receiver %s", call.This.String())) +} + +func (r *Runtime) typedArrayProto_lastIndexOf(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached() + length := int64(ta.length) + if length == 0 { + return intToValue(-1) + } + + var fromIndex int64 + + if len(call.Arguments) < 2 { + fromIndex = length - 1 + } else { + fromIndex = call.Argument(1).ToInteger() + if fromIndex >= 0 { + fromIndex = min(fromIndex, length-1) + } else { + fromIndex += length + } + } + + ta.viewedArrayBuf.ensureNotDetached() + searchElement := call.Argument(0) + if searchElement == _negativeZero { + searchElement = _positiveZero + } + if !IsNaN(searchElement) && ta.typedArray.typeMatch(searchElement) { + se := ta.typedArray.toRaw(searchElement) + for k := toInt(fromIndex); k >= 0; k-- { + if ta.typedArray.getRaw(ta.offset+k) == se { + return intToValue(int64(k)) + } + } + } + + return intToValue(-1) + } + panic(r.NewTypeError("Method TypedArray.prototype.lastIndexOf called on incompatible receiver %s", call.This.String())) +} + +func (r *Runtime) typedArrayProto_map(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached() + callbackFn := r.toCallable(call.Argument(0)) + fc := FunctionCall{ + This: call.Argument(1), + Arguments: []Value{nil, nil, call.This}, + } + dst := r.typedArraySpeciesCreate(ta, []Value{intToValue(int64(ta.length))}) + for i := 0; i < ta.length; i++ { + ta.viewedArrayBuf.ensureNotDetached() + fc.Arguments[0] = ta.typedArray.get(ta.offset + i) + fc.Arguments[1] = intToValue(int64(i)) + dst.typedArray.set(i, callbackFn(fc)) + } + return dst.val + } + panic(r.NewTypeError("Method TypedArray.prototype.map called on incompatible receiver %s", call.This.String())) +} + +func (r *Runtime) typedArrayProto_reduce(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached() + callbackFn := r.toCallable(call.Argument(0)) + fc := FunctionCall{ + This: _undefined, + Arguments: []Value{nil, nil, nil, call.This}, + } + k := 0 + if len(call.Arguments) >= 2 { + fc.Arguments[0] = call.Argument(1) + } else { + if ta.length > 0 { + fc.Arguments[0] = ta.typedArray.get(ta.offset + 0) + k = 1 + } + } + if fc.Arguments[0] == nil { + panic(r.NewTypeError("Reduce of empty array with no initial value")) + } + for ; k < ta.length; k++ { + ta.viewedArrayBuf.ensureNotDetached() + idx := valueInt(k) + fc.Arguments[1] = ta.typedArray.get(ta.offset + k) + fc.Arguments[2] = idx + fc.Arguments[0] = callbackFn(fc) + } + return fc.Arguments[0] + } + panic(r.NewTypeError("Method TypedArray.prototype.reduce called on incompatible receiver %s", call.This.String())) +} + +func (r *Runtime) typedArrayProto_reduceRight(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached() + callbackFn := r.toCallable(call.Argument(0)) + fc := FunctionCall{ + This: _undefined, + Arguments: []Value{nil, nil, nil, call.This}, + } + k := ta.length - 1 + if len(call.Arguments) >= 2 { + fc.Arguments[0] = call.Argument(1) + } else { + if k >= 0 { + fc.Arguments[0] = ta.typedArray.get(ta.offset + k) + k-- + } + } + if fc.Arguments[0] == nil { + panic(r.NewTypeError("Reduce of empty array with no initial value")) + } + for ; k >= 0; k-- { + ta.viewedArrayBuf.ensureNotDetached() + idx := valueInt(k) + fc.Arguments[1] = ta.typedArray.get(ta.offset + k) + fc.Arguments[2] = idx + fc.Arguments[0] = callbackFn(fc) + } + return fc.Arguments[0] + } + panic(r.NewTypeError("Method TypedArray.prototype.reduceRight called on incompatible receiver %s", call.This.String())) +} + +func (r *Runtime) typedArrayProto_reverse(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached() + l := ta.length + middle := l / 2 + for lower := 0; lower != middle; lower++ { + upper := l - lower - 1 + ta.typedArray.swap(ta.offset+lower, ta.offset+upper) + } + + return call.This + } + panic(r.NewTypeError("Method TypedArray.prototype.reverse called on incompatible receiver %s", call.This.String())) +} + +func (r *Runtime) typedArrayProto_set(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + srcObj := r.toObject(call.Argument(0)) + targetOffset := toInt(call.Argument(1).ToInteger()) + if targetOffset < 0 { + panic(r.newError(r.global.RangeError, "offset should be >= 0")) + } + ta.viewedArrayBuf.ensureNotDetached() + targetLen := ta.length + if src, ok := srcObj.self.(*typedArrayObject); ok { + src.viewedArrayBuf.ensureNotDetached() + srcLen := src.length + if x := srcLen + targetOffset; x < 0 || x > targetLen { + panic(r.newError(r.global.RangeError, "Source is too large")) + } + if src.defaultCtor == ta.defaultCtor { + copy(ta.viewedArrayBuf.data[(ta.offset+targetOffset)*ta.elemSize:], + src.viewedArrayBuf.data[src.offset*src.elemSize:(src.offset+srcLen)*src.elemSize]) + } else { + curSrc := uintptr(unsafe.Pointer(&src.viewedArrayBuf.data[src.offset*src.elemSize])) + endSrc := curSrc + uintptr(srcLen*src.elemSize) + curDst := uintptr(unsafe.Pointer(&ta.viewedArrayBuf.data[(ta.offset+targetOffset)*ta.elemSize])) + dstOffset := ta.offset + targetOffset + srcOffset := src.offset + if ta.elemSize == src.elemSize { + if curDst <= curSrc || curDst >= endSrc { + for i := 0; i < srcLen; i++ { + ta.typedArray.set(dstOffset+i, src.typedArray.get(srcOffset+i)) + } + } else { + for i := srcLen - 1; i >= 0; i-- { + ta.typedArray.set(dstOffset+i, src.typedArray.get(srcOffset+i)) + } + } + } else { + x := int(curDst-curSrc) / (src.elemSize - ta.elemSize) + if x < 0 { + x = 0 + } else if x > srcLen { + x = srcLen + } + if ta.elemSize < src.elemSize { + for i := x; i < srcLen; i++ { + ta.typedArray.set(dstOffset+i, src.typedArray.get(srcOffset+i)) + } + for i := x - 1; i >= 0; i-- { + ta.typedArray.set(dstOffset+i, src.typedArray.get(srcOffset+i)) + } + } else { + for i := 0; i < x; i++ { + ta.typedArray.set(dstOffset+i, src.typedArray.get(srcOffset+i)) + } + for i := srcLen - 1; i >= x; i-- { + ta.typedArray.set(dstOffset+i, src.typedArray.get(srcOffset+i)) + } + } + } + } + } else { + targetLen := ta.length + srcLen := toInt(toLength(srcObj.self.getStr("length", nil))) + if x := srcLen + targetOffset; x < 0 || x > targetLen { + panic(r.newError(r.global.RangeError, "Source is too large")) + } + for i := 0; i < srcLen; i++ { + val := nilSafe(srcObj.self.getIdx(valueInt(i), nil)) + ta.viewedArrayBuf.ensureNotDetached() + ta.typedArray.set(targetOffset+i, val) + } + } + return _undefined + } + panic(r.NewTypeError("Method TypedArray.prototype.set called on incompatible receiver %s", call.This.String())) +} + +func (r *Runtime) typedArrayProto_slice(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached() + length := int64(ta.length) + start := toInt(relToIdx(call.Argument(0).ToInteger(), length)) + var e int64 + if endArg := call.Argument(1); endArg != _undefined { + e = endArg.ToInteger() + } else { + e = length + } + end := toInt(relToIdx(e, length)) + + count := end - start + if count < 0 { + count = 0 + } + dst := r.typedArraySpeciesCreate(ta, []Value{intToValue(int64(count))}) + if dst.defaultCtor == ta.defaultCtor { + if count > 0 { + ta.viewedArrayBuf.ensureNotDetached() + offset := ta.offset + elemSize := ta.elemSize + copy(dst.viewedArrayBuf.data, ta.viewedArrayBuf.data[(offset+start)*elemSize:(offset+start+count)*elemSize]) + } + } else { + for i := 0; i < count; i++ { + ta.viewedArrayBuf.ensureNotDetached() + dst.typedArray.set(i, ta.typedArray.get(ta.offset+start+i)) + } + } + return dst.val + } + panic(r.NewTypeError("Method TypedArray.prototype.slice called on incompatible receiver %s", call.This.String())) +} + +func (r *Runtime) typedArrayProto_some(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached() + callbackFn := r.toCallable(call.Argument(0)) + fc := FunctionCall{ + This: call.Argument(1), + Arguments: []Value{nil, nil, call.This}, + } + for k := 0; k < ta.length; k++ { + ta.viewedArrayBuf.ensureNotDetached() + fc.Arguments[0] = ta.typedArray.get(ta.offset + k) + fc.Arguments[1] = intToValue(int64(k)) + if callbackFn(fc).ToBoolean() { + return valueTrue + } + } + return valueFalse + } + panic(r.NewTypeError("Method TypedArray.prototype.some called on incompatible receiver %s", call.This.String())) +} + +func (r *Runtime) typedArrayProto_sort(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached() + var compareFn func(FunctionCall) Value + + if arg, ok := call.Argument(0).(*Object); ok { + compareFn, _ = arg.self.assertCallable() + } + + ctx := typedArraySortCtx{ + ta: ta, + compare: compareFn, + } + + sort.Sort(&ctx) + return call.This + } + panic(r.NewTypeError("Method TypedArray.prototype.sort called on incompatible receiver %s", call.This.String())) +} + +func (r *Runtime) typedArrayProto_subarray(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + l := int64(ta.length) + beginIdx := relToIdx(call.Argument(0).ToInteger(), l) + var relEnd int64 + if endArg := call.Argument(1); endArg != _undefined { + relEnd = endArg.ToInteger() + } else { + relEnd = l + } + endIdx := relToIdx(relEnd, l) + newLen := max(endIdx-beginIdx, 0) + return r.typedArraySpeciesCreate(ta, []Value{ta.viewedArrayBuf.val, + intToValue((int64(ta.offset) + beginIdx) * int64(ta.elemSize)), + intToValue(newLen), + }).val + } + panic(r.NewTypeError("Method TypedArray.prototype.subarray called on incompatible receiver %s", call.This.String())) +} + +func (r *Runtime) typedArrayProto_toLocaleString(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + length := ta.length + var buf strings.Builder + for i := 0; i < length; i++ { + ta.viewedArrayBuf.ensureNotDetached() + if i > 0 { + buf.WriteByte(',') + } + item := ta.typedArray.get(i) + r.writeItemLocaleString(item, &buf) + } + return newStringValue(buf.String()) + } + panic(r.NewTypeError("Method TypedArray.prototype.toLocaleString called on incompatible receiver %s", call.This.String())) +} + +func (r *Runtime) typedArrayProto_values(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached() + return r.createArrayIterator(ta.val, iterationKindValue) + } + panic(r.NewTypeError("Method TypedArray.prototype.values called on incompatible receiver %s", call.This.String())) +} + +func (r *Runtime) typedArrayProto_toStringTag(call FunctionCall) Value { + if obj, ok := call.This.(*Object); ok { + if ta, ok := obj.self.(*typedArrayObject); ok { + return ta.defaultCtor.self.getStr("name", nil) + } + } + + return _undefined +} + +func (r *Runtime) newTypedArray([]Value, *Object) *Object { + panic(r.NewTypeError("Abstract class TypedArray not directly constructable")) +} + +func (r *Runtime) typedArray_from(call FunctionCall) Value { + mapFn := call.Argument(1) + if mapFn == _undefined { + mapFn = nil + } + return r.typedArrayFrom(r.toObject(call.This), call.Argument(0).ToObject(r), mapFn, call.Argument(2)) +} + +func (r *Runtime) typedArray_of(call FunctionCall) Value { + ta := r.typedArrayCreate(r.toObject(call.This), []Value{intToValue(int64(len(call.Arguments)))}) + for i, val := range call.Arguments { + ta.typedArray.set(i, val) + } + return ta.val +} + +func (r *Runtime) allocateTypedArray(newTarget *Object, length int, taCtor typedArrayObjectCtor) *Object { + buf := r._newArrayBuffer(r.global.ArrayBufferPrototype, nil) + ta := taCtor(buf, 0, length, r.getPrototypeFromCtor(newTarget, nil, r.global.TypedArrayPrototype)) + if length > 0 { + buf.data = make([]byte, length*ta.elemSize) + } + return ta.val +} + +func (r *Runtime) typedArraySpeciesCreate(ta *typedArrayObject, args []Value) *typedArrayObject { + return r.typedArrayCreate(r.speciesConstructorObj(ta.val, ta.defaultCtor), args) +} + +func (r *Runtime) typedArrayCreate(ctor *Object, args []Value) *typedArrayObject { + o := r.toConstructor(ctor)(args, ctor) + if ta, ok := o.self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached() + if len(args) == 1 { + if l, ok := args[0].(valueInt); ok { + if ta.length < int(l) { + panic(r.NewTypeError("Derived TypedArray constructor created an array which was too small")) + } + } + } + return ta + } + panic(r.NewTypeError("Invalid TypedArray: %s", o)) +} + +func (r *Runtime) typedArrayFrom(ctor, items *Object, mapFn, thisValue Value) *Object { + var mapFc func(call FunctionCall) Value + if mapFn != nil { + mapFc = r.toCallable(mapFn) + if thisValue == nil { + thisValue = _undefined + } + } + usingIter := toMethod(items.self.getSym(symIterator, nil)) + if usingIter != nil { + iter := r.getIterator(items, usingIter) + var values []Value + r.iterate(iter, func(item Value) { + values = append(values, item) + }) + ta := r.typedArrayCreate(ctor, []Value{intToValue(int64(len(values)))}) + if mapFc == nil { + for idx, val := range values { + ta.typedArray.set(idx, val) + } + } else { + fc := FunctionCall{ + This: thisValue, + Arguments: []Value{nil, nil}, + } + for idx, val := range values { + fc.Arguments[0], fc.Arguments[1] = val, intToValue(int64(idx)) + val = mapFc(fc) + ta.typedArray.set(idx, val) + } + } + return ta.val + } + length := toInt(toLength(items.self.getStr("length", nil))) + ta := r.typedArrayCreate(ctor, []Value{intToValue(int64(length))}) + if mapFc == nil { + for i := 0; i < length; i++ { + ta.typedArray.set(i, nilSafe(items.self.getIdx(valueInt(i), nil))) + } + } else { + fc := FunctionCall{ + This: thisValue, + Arguments: []Value{nil, nil}, + } + for i := 0; i < length; i++ { + idx := valueInt(i) + fc.Arguments[0], fc.Arguments[1] = items.self.getIdx(idx, nil), idx + ta.typedArray.set(i, mapFc(fc)) + } + } + return ta.val +} + +func (r *Runtime) _newTypedArrayFromArrayBuffer(ab *arrayBufferObject, args []Value, newTarget *Object, taCtor typedArrayObjectCtor) *Object { + ta := taCtor(ab, 0, 0, r.getPrototypeFromCtor(newTarget, nil, r.global.TypedArrayPrototype)) + var byteOffset int + if len(args) > 1 && args[1] != nil && args[1] != _undefined { + byteOffset = r.toIndex(args[1]) + if byteOffset%ta.elemSize != 0 { + panic(r.newError(r.global.RangeError, "Start offset of %s should be a multiple of %d", newTarget.self.getStr("name", nil), ta.elemSize)) + } + } + ab.ensureNotDetached() + var length int + if len(args) > 2 && args[2] != nil && args[2] != _undefined { + length = r.toIndex(args[2]) + if byteOffset+length*ta.elemSize > len(ab.data) { + panic(r.newError(r.global.RangeError, "Invalid typed array length: %d", length)) + } + } else { + if len(ab.data)%ta.elemSize != 0 { + panic(r.newError(r.global.RangeError, "Byte length of %s should be a multiple of %d", newTarget.self.getStr("name", nil), ta.elemSize)) + } + length = (len(ab.data) - byteOffset) / ta.elemSize + } + ta.offset = byteOffset / ta.elemSize + ta.length = length + return ta.val +} + +func (r *Runtime) _newTypedArrayFromTypedArray(src *typedArrayObject, newTarget *Object) *Object { + dst := r.typedArrayCreate(newTarget, []Value{_positiveZero}) + src.viewedArrayBuf.ensureNotDetached() + l := src.length + dst.viewedArrayBuf.prototype = r.getPrototypeFromCtor(r.toObject(src.viewedArrayBuf.getStr("constructor", nil)), r.global.ArrayBuffer, r.global.ArrayBufferPrototype) + dst.viewedArrayBuf.data = make([]byte, int64(l)*int64(dst.elemSize)) + if src.defaultCtor == dst.defaultCtor { + copy(dst.viewedArrayBuf.data, src.viewedArrayBuf.data[src.offset*src.elemSize:]) + dst.length = src.length + return dst.val + } + dst.length = l + for i := 0; i < l; i++ { + dst.typedArray.set(i, src.typedArray.get(src.offset+i)) + } + return dst.val +} + +func (r *Runtime) _newTypedArray(args []Value, newTarget *Object, taCtor typedArrayObjectCtor) *Object { + if newTarget == nil { + panic(r.needNew("TypedArray")) + } + if len(args) > 0 { + if obj, ok := args[0].(*Object); ok { + switch o := obj.self.(type) { + case *arrayBufferObject: + return r._newTypedArrayFromArrayBuffer(o, args, newTarget, taCtor) + case *typedArrayObject: + return r._newTypedArrayFromTypedArray(o, newTarget) + default: + return r.typedArrayFrom(newTarget, obj, nil, nil) + } + } + } + var l int + if len(args) > 0 { + if arg0 := args[0]; arg0 != nil { + l = r.toIndex(arg0) + } + } + return r.allocateTypedArray(newTarget, l, taCtor) +} + +func (r *Runtime) newUint8Array(args []Value, newTarget *Object) *Object { + return r._newTypedArray(args, newTarget, r.newUint8ArrayObject) +} + +func (r *Runtime) newUint8ClampedArray(args []Value, newTarget *Object) *Object { + return r._newTypedArray(args, newTarget, r.newUint8ClampedArrayObject) +} + +func (r *Runtime) newInt8Array(args []Value, newTarget *Object) *Object { + return r._newTypedArray(args, newTarget, r.newInt8ArrayObject) +} + +func (r *Runtime) newUint16Array(args []Value, newTarget *Object) *Object { + return r._newTypedArray(args, newTarget, r.newUint16ArrayObject) +} + +func (r *Runtime) newInt16Array(args []Value, newTarget *Object) *Object { + return r._newTypedArray(args, newTarget, r.newInt16ArrayObject) +} + +func (r *Runtime) newUint32Array(args []Value, newTarget *Object) *Object { + return r._newTypedArray(args, newTarget, r.newUint32ArrayObject) +} + +func (r *Runtime) newInt32Array(args []Value, newTarget *Object) *Object { + return r._newTypedArray(args, newTarget, r.newInt32ArrayObject) +} + +func (r *Runtime) newFloat32Array(args []Value, newTarget *Object) *Object { + return r._newTypedArray(args, newTarget, r.newFloat32ArrayObject) +} + +func (r *Runtime) newFloat64Array(args []Value, newTarget *Object) *Object { + return r._newTypedArray(args, newTarget, r.newFloat64ArrayObject) } func (r *Runtime) createArrayBufferProto(val *Object) objectImpl { - b := r._newArrayBuffer(r.global.Object, val) + b := newBaseObjectObj(val, r.global.ObjectPrototype, classObject) byteLengthProp := &valueProperty{ accessor: true, configurable: true, getterFunc: r.newNativeFunc(r.arrayBufferProto_getByteLength, nil, "get byteLength", nil, 0), } b._put("byteLength", byteLengthProp) + b._putProp("constructor", r.global.ArrayBuffer, true, false, true) b._putProp("slice", r.newNativeFunc(r.arrayBufferProto_slice, nil, "slice", nil, 2), true, false, true) + b._putSym(symToStringTag, valueProp(asciiString("ArrayBuffer"), false, false, true)) return b } +func (r *Runtime) createArrayBuffer(val *Object) objectImpl { + o := r.newNativeConstructOnly(val, r.builtin_newArrayBuffer, r.global.ArrayBufferPrototype, "ArrayBuffer", 1) + o._putProp("isView", r.newNativeFunc(r.arrayBuffer_isView, nil, "isView", nil, 1), true, false, true) + o._putSym(symSpecies, &valueProperty{ + getterFunc: r.newNativeFunc(r.returnThis, nil, "get [Symbol.species]", nil, 0), + accessor: true, + configurable: true, + }) + return o +} + +func (r *Runtime) createDataViewProto(val *Object) objectImpl { + b := newBaseObjectObj(val, r.global.ObjectPrototype, classObject) + b._put("buffer", &valueProperty{ + accessor: true, + configurable: true, + getterFunc: r.newNativeFunc(r.dataViewProto_getBuffer, nil, "get buffer", nil, 0), + }) + b._put("byteLength", &valueProperty{ + accessor: true, + configurable: true, + getterFunc: r.newNativeFunc(r.dataViewProto_getByteLen, nil, "get byteLength", nil, 0), + }) + b._put("byteOffset", &valueProperty{ + accessor: true, + configurable: true, + getterFunc: r.newNativeFunc(r.dataViewProto_getByteOffset, nil, "get byteOffset", nil, 0), + }) + b._putProp("constructor", r.global.DataView, true, false, true) + b._putProp("getFloat32", r.newNativeFunc(r.dataViewProto_getFloat32, nil, "getFloat32", nil, 1), true, false, true) + b._putProp("getFloat64", r.newNativeFunc(r.dataViewProto_getFloat64, nil, "getFloat64", nil, 1), true, false, true) + b._putProp("getInt8", r.newNativeFunc(r.dataViewProto_getInt8, nil, "getInt8", nil, 1), true, false, true) + b._putProp("getInt16", r.newNativeFunc(r.dataViewProto_getInt16, nil, "getInt16", nil, 1), true, false, true) + b._putProp("getInt32", r.newNativeFunc(r.dataViewProto_getInt32, nil, "getInt32", nil, 1), true, false, true) + b._putProp("getUint8", r.newNativeFunc(r.dataViewProto_getUint8, nil, "getUint8", nil, 1), true, false, true) + b._putProp("getUint16", r.newNativeFunc(r.dataViewProto_getUint16, nil, "getUint16", nil, 1), true, false, true) + b._putProp("getUint32", r.newNativeFunc(r.dataViewProto_getUint32, nil, "getUint32", nil, 1), true, false, true) + b._putProp("setFloat32", r.newNativeFunc(r.dataViewProto_setFloat32, nil, "setFloat32", nil, 2), true, false, true) + b._putProp("setFloat64", r.newNativeFunc(r.dataViewProto_setFloat64, nil, "setFloat64", nil, 2), true, false, true) + b._putProp("setInt8", r.newNativeFunc(r.dataViewProto_setInt8, nil, "setInt8", nil, 2), true, false, true) + b._putProp("setInt16", r.newNativeFunc(r.dataViewProto_setInt16, nil, "setInt16", nil, 2), true, false, true) + b._putProp("setInt32", r.newNativeFunc(r.dataViewProto_setInt32, nil, "setInt32", nil, 2), true, false, true) + b._putProp("setUint8", r.newNativeFunc(r.dataViewProto_setUint8, nil, "setUint8", nil, 2), true, false, true) + b._putProp("setUint16", r.newNativeFunc(r.dataViewProto_setUint16, nil, "setUint16", nil, 2), true, false, true) + b._putProp("setUint32", r.newNativeFunc(r.dataViewProto_setUint32, nil, "setUint32", nil, 2), true, false, true) + b._putSym(symToStringTag, valueProp(asciiString("DataView"), false, false, true)) + + return b +} + +func (r *Runtime) createDataView(val *Object) objectImpl { + o := r.newNativeConstructOnly(val, r.newDataView, r.global.DataViewPrototype, "DataView", 3) + return o +} + +func (r *Runtime) createTypedArrayProto(val *Object) objectImpl { + b := newBaseObjectObj(val, r.global.ObjectPrototype, classObject) + b._put("buffer", &valueProperty{ + accessor: true, + configurable: true, + getterFunc: r.newNativeFunc(r.typedArrayProto_getBuffer, nil, "get buffer", nil, 0), + }) + b._put("byteLength", &valueProperty{ + accessor: true, + configurable: true, + getterFunc: r.newNativeFunc(r.typedArrayProto_getByteLen, nil, "get byteLength", nil, 0), + }) + b._put("byteOffset", &valueProperty{ + accessor: true, + configurable: true, + getterFunc: r.newNativeFunc(r.typedArrayProto_getByteOffset, nil, "get byteOffset", nil, 0), + }) + b._putProp("constructor", r.global.TypedArray, true, false, true) + b._putProp("copyWithin", r.newNativeFunc(r.typedArrayProto_copyWithin, nil, "copyWithin", nil, 2), true, false, true) + b._putProp("entries", r.newNativeFunc(r.typedArrayProto_entries, nil, "entries", nil, 0), true, false, true) + b._putProp("every", r.newNativeFunc(r.typedArrayProto_every, nil, "every", nil, 1), true, false, true) + b._putProp("fill", r.newNativeFunc(r.typedArrayProto_fill, nil, "fill", nil, 1), true, false, true) + b._putProp("filter", r.newNativeFunc(r.typedArrayProto_filter, nil, "filter", nil, 1), true, false, true) + b._putProp("find", r.newNativeFunc(r.typedArrayProto_find, nil, "find", nil, 1), true, false, true) + b._putProp("findIndex", r.newNativeFunc(r.typedArrayProto_findIndex, nil, "findIndex", nil, 1), true, false, true) + b._putProp("forEach", r.newNativeFunc(r.typedArrayProto_forEach, nil, "forEach", nil, 1), true, false, true) + b._putProp("includes", r.newNativeFunc(r.typedArrayProto_includes, nil, "includes", nil, 1), true, false, true) + b._putProp("indexOf", r.newNativeFunc(r.typedArrayProto_indexOf, nil, "indexOf", nil, 1), true, false, true) + b._putProp("join", r.newNativeFunc(r.typedArrayProto_join, nil, "join", nil, 1), true, false, true) + b._putProp("keys", r.newNativeFunc(r.typedArrayProto_keys, nil, "keys", nil, 0), true, false, true) + b._putProp("lastIndexOf", r.newNativeFunc(r.typedArrayProto_lastIndexOf, nil, "lastIndexOf", nil, 1), true, false, true) + b._put("length", &valueProperty{ + accessor: true, + configurable: true, + getterFunc: r.newNativeFunc(r.typedArrayProto_getLength, nil, "get length", nil, 0), + }) + b._putProp("map", r.newNativeFunc(r.typedArrayProto_map, nil, "map", nil, 1), true, false, true) + b._putProp("reduce", r.newNativeFunc(r.typedArrayProto_reduce, nil, "reduce", nil, 1), true, false, true) + b._putProp("reduceRight", r.newNativeFunc(r.typedArrayProto_reduceRight, nil, "reduceRight", nil, 1), true, false, true) + b._putProp("reverse", r.newNativeFunc(r.typedArrayProto_reverse, nil, "reverse", nil, 0), true, false, true) + b._putProp("set", r.newNativeFunc(r.typedArrayProto_set, nil, "set", nil, 1), true, false, true) + b._putProp("slice", r.newNativeFunc(r.typedArrayProto_slice, nil, "slice", nil, 2), true, false, true) + b._putProp("some", r.newNativeFunc(r.typedArrayProto_some, nil, "some", nil, 1), true, false, true) + b._putProp("sort", r.newNativeFunc(r.typedArrayProto_sort, nil, "sort", nil, 1), true, false, true) + b._putProp("subarray", r.newNativeFunc(r.typedArrayProto_subarray, nil, "subarray", nil, 2), true, false, true) + b._putProp("toLocaleString", r.newNativeFunc(r.typedArrayProto_toLocaleString, nil, "toLocaleString", nil, 0), true, false, true) + b._putProp("toString", r.global.arrayToString, true, false, true) + valuesFunc := r.newNativeFunc(r.typedArrayProto_values, nil, "values", nil, 0) + b._putProp("values", valuesFunc, true, false, true) + b._putSym(symIterator, valueProp(valuesFunc, true, false, true)) + b._putSym(symToStringTag, &valueProperty{ + getterFunc: r.newNativeFunc(r.typedArrayProto_toStringTag, nil, "get [Symbol.toStringTag]", nil, 0), + accessor: true, + configurable: true, + }) + + return b +} + +func (r *Runtime) createTypedArray(val *Object) objectImpl { + o := r.newNativeConstructOnly(val, r.newTypedArray, r.global.TypedArrayPrototype, "TypedArray", 0) + o._putProp("from", r.newNativeFunc(r.typedArray_from, nil, "from", nil, 1), true, false, true) + o._putProp("of", r.newNativeFunc(r.typedArray_of, nil, "of", nil, 0), true, false, true) + o._putSym(symSpecies, &valueProperty{ + getterFunc: r.newNativeFunc(r.returnThis, nil, "get [Symbol.species]", nil, 0), + accessor: true, + configurable: true, + }) + + return o +} + +func (r *Runtime) addPrototype(ctor *Object, proto *Object) *baseObject { + p := r.newBaseObject(proto, classObject) + p._putProp("constructor", ctor, true, false, true) + ctor.self._putProp("prototype", p.val, false, false, false) + return p +} + +func (r *Runtime) typedArrayCreator(ctor func(args []Value, newTarget *Object) *Object, name string, bytesPerElement int) func(val *Object) objectImpl { + return func(val *Object) objectImpl { + o := r.newNativeConstructOnly(val, ctor, nil, name, 3) + o.prototype = r.global.TypedArray + proto := r.addPrototype(o.val, r.global.TypedArrayPrototype) + bpe := intToValue(int64(bytesPerElement)) + o._putProp("BYTES_PER_ELEMENT", bpe, false, false, false) + proto._putProp("BYTES_PER_ELEMENT", bpe, false, false, false) + return o + } +} + func (r *Runtime) initTypedArrays() { r.global.ArrayBufferPrototype = r.newLazyObject(r.createArrayBufferProto) - - r.global.ArrayBuffer = r.newNativeFuncConstruct(r.builtin_ArrayBuffer, "ArrayBuffer", r.global.ArrayBufferPrototype, 1) + r.global.ArrayBuffer = r.newLazyObject(r.createArrayBuffer) r.addToGlobal("ArrayBuffer", r.global.ArrayBuffer) + + r.global.DataViewPrototype = r.newLazyObject(r.createDataViewProto) + r.global.DataView = r.newLazyObject(r.createDataView) + r.addToGlobal("DataView", r.global.DataView) + + r.global.TypedArrayPrototype = r.newLazyObject(r.createTypedArrayProto) + r.global.TypedArray = r.newLazyObject(r.createTypedArray) + + r.global.Uint8Array = r.newLazyObject(r.typedArrayCreator(r.newUint8Array, "Uint8Array", 1)) + r.addToGlobal("Uint8Array", r.global.Uint8Array) + + r.global.Uint8ClampedArray = r.newLazyObject(r.typedArrayCreator(r.newUint8ClampedArray, "Uint8ClampedArray", 1)) + r.addToGlobal("Uint8ClampedArray", r.global.Uint8ClampedArray) + + r.global.Int8Array = r.newLazyObject(r.typedArrayCreator(r.newInt8Array, "Int8Array", 1)) + r.addToGlobal("Int8Array", r.global.Int8Array) + + r.global.Uint16Array = r.newLazyObject(r.typedArrayCreator(r.newUint16Array, "Uint16Array", 2)) + r.addToGlobal("Uint16Array", r.global.Uint16Array) + + r.global.Int16Array = r.newLazyObject(r.typedArrayCreator(r.newInt16Array, "Int16Array", 2)) + r.addToGlobal("Int16Array", r.global.Int16Array) + + r.global.Uint32Array = r.newLazyObject(r.typedArrayCreator(r.newUint32Array, "Uint32Array", 4)) + r.addToGlobal("Uint32Array", r.global.Uint32Array) + + r.global.Int32Array = r.newLazyObject(r.typedArrayCreator(r.newInt32Array, "Int32Array", 4)) + r.addToGlobal("Int32Array", r.global.Int32Array) + + r.global.Float32Array = r.newLazyObject(r.typedArrayCreator(r.newFloat32Array, "Float32Array", 4)) + r.addToGlobal("Float32Array", r.global.Float32Array) + + r.global.Float64Array = r.newLazyObject(r.typedArrayCreator(r.newFloat64Array, "Float64Array", 8)) + r.addToGlobal("Float64Array", r.global.Float64Array) } diff --git a/builtin_typedarrays_test.go b/builtin_typedarrays_test.go index 3603f15f..d816754a 100644 --- a/builtin_typedarrays_test.go +++ b/builtin_typedarrays_test.go @@ -1,5 +1,9 @@ package goja +import ( + "testing" +) + /* func TestArrayBufferNew(t *testing.T) { const SCRIPT = ` @@ -10,3 +14,253 @@ func TestArrayBufferNew(t *testing.T) { testScript1(SCRIPT, intToValue(16), t) } */ + +func TestArrayBufferSetUint32(t *testing.T) { + vm := New() + b := vm._newArrayBuffer(vm.global.ArrayBufferPrototype, nil) + b.data = make([]byte, 4) + b.setUint32(0, 0xCAFEBABE, bigEndian) + + i := b.getUint32(0, bigEndian) + if i != 0xCAFEBABE { + t.Fatal(i) + } + i = b.getUint32(0, littleEndian) + if i != 0xBEBAFECA { + t.Fatal(i) + } + + b.setUint32(0, 0xBEBAFECA, littleEndian) + i = b.getUint32(0, bigEndian) + if i != 0xCAFEBABE { + t.Fatal(i) + } +} + +func TestArrayBufferSetInt32(t *testing.T) { + vm := New() + b := vm._newArrayBuffer(vm.global.ArrayBufferPrototype, nil) + b.data = make([]byte, 4) + b.setInt32(0, -42, littleEndian) + if v := b.getInt32(0, littleEndian); v != -42 { + t.Fatal(v) + } + + b.setInt32(0, -42, bigEndian) + if v := b.getInt32(0, bigEndian); v != -42 { + t.Fatal(v) + } +} + +func TestNewUint8Array(t *testing.T) { + const SCRIPT = ` + var a = new Uint8Array(1); + a[0] = 42; + a.byteLength === 1 && a.length === 1 && a[0] === 42; + ` + + testScript1(SCRIPT, valueTrue, t) +} + +func TestNewUint16Array(t *testing.T) { + const SCRIPT = ` + var a = new Uint16Array(1); + a[0] = 42; + a.byteLength === 2 && a.length === 1 && a[0] === 42; + ` + + testScript1(SCRIPT, valueTrue, t) +} + +func TestTypedArraysSpeciesConstructor(t *testing.T) { + const SCRIPT = ` + 'use strict'; + function MyArray() { + var NewTarget = this.__proto__.constructor; + return Reflect.construct(Uint16Array, arguments, NewTarget); + } + MyArray.prototype = Object.create(Uint16Array.prototype, { + constructor: { + value: MyArray, + writable: true, + configurable: true + } + }); + var a = new MyArray(1); + Object.defineProperty(MyArray, Symbol.species, {value: Uint8Array, configurable: true}); + a[0] = 32767; + var b = a.filter(function() { + return true; + }); + if (a[0] !== 32767) { + throw new Error("a[0]=" + a[0]); + } + if (!(b instanceof Uint8Array)) { + throw new Error("b instanceof Uint8Array"); + } + if (b[0] != 255) { + throw new Error("b[0]=" + b[0]); + } + ` + + testScript1(SCRIPT, _undefined, t) +} + +func TestTypedArrayFromArrayBuffer(t *testing.T) { + const SCRIPT = ` + var buf = new ArrayBuffer(2); + var a16 = new Uint16Array(buf); + if (!(a16 instanceof Uint16Array)) { + throw new Error("a16 is not an instance"); + } + if (a16.buffer !== buf) { + throw new Error("a16.buffer !== buf"); + } + if (a16.length !== 1) { + throw new Error("a16.length=" + a16.length); + } + var a8 = new Uint8Array(buf); + a8.fill(0xAA); + if (a16[0] !== 0xAAAA) { + throw new Error("a16[0]=" + a16[0]); + } + ` + + testScript1(SCRIPT, _undefined, t) +} + +func TestTypedArraySetOverlapDifSize(t *testing.T) { + const SCRIPT = ` + var buf = new ArrayBuffer(4); + var src = new Uint8Array(buf, 1, 2); + src[0] = 1; + src[1] = 2; + var dst = new Uint16Array(buf); + dst.set(src); + if (dst[0] !== 1 || dst[1] !== 2) { + throw new Error("dst: " + dst.join(",")); + } + ` + testScript1(SCRIPT, _undefined, t) +} + +func TestTypedArraySetOverlapDifSize2(t *testing.T) { + const SCRIPT = ` + var buf = new ArrayBuffer(4); + var src = new Uint8Array(buf, 0, 2); + src[0] = 1; + src[1] = 2; + var dst = new Uint16Array(buf); + dst.set(src); + if (dst[0] !== 1 || dst[1] !== 2) { + throw new Error("dst: " + dst.join(",")); + } + ` + testScript1(SCRIPT, _undefined, t) +} + +func TestTypedArraySetOverlapDifSize3(t *testing.T) { + const SCRIPT = ` + var buf = new ArrayBuffer(8); + var src = new Uint8Array(buf, 2, 4); + src[0] = 1; + src[1] = 2; + src[2] = 3; + src[3] = 4; + var dst = new Uint16Array(buf); + dst.set(src); + if (dst[0] !== 1 || dst[1] !== 2 || dst[2] !== 3 || dst[3] !== 4) { + throw new Error("dst: " + dst.join(",")); + } + ` + testScript1(SCRIPT, _undefined, t) +} + +func TestTypedArraySetOverlapDifSize4(t *testing.T) { + const SCRIPT = ` + var buf = new ArrayBuffer(10); + var dst = new Uint8Array(buf, 2, 5); + var src = new Uint16Array(buf); + src[0] = 1; + src[1] = 2; + src[2] = 3; + src[3] = 4; + src[4] = 5; + dst.set(src); + if (dst[0] !== 1 || dst[1] !== 2 || dst[2] !== 3 || dst[3] !== 4 || dst[4] !== 5) { + throw new Error("dst: " + dst.join(",")); + } + ` + testScript1(SCRIPT, _undefined, t) +} + +func TestTypedArraySetNoOverlapDifSizeForward(t *testing.T) { + const SCRIPT = ` + var buf = new ArrayBuffer(10); + var dst = new Uint8Array(buf, 7, 2); + var src = new Uint16Array(buf, 0, 2); + src[0] = 1; + src[1] = 2; + dst.set(src); + if (dst[0] !== 1 || dst[1] !== 2 || src[0] !== 1 || src[1] !== 2) { + throw new Error("dst: " + dst.join(",")); + } + ` + testScript1(SCRIPT, _undefined, t) +} + +func TestTypedArraySetNoOverlapDifSizeBackward(t *testing.T) { + const SCRIPT = ` + var buf = new ArrayBuffer(10); + var dst = new Uint8Array(buf, 0, 2); + var src = new Uint16Array(buf, 6, 2); + src[0] = 1; + src[1] = 2; + dst.set(src); + if (dst[0] !== 1 || dst[1] !== 2 || src[0] !== 1 || src[1] !== 2) { + throw new Error("dst: " + dst.join(",")); + } + ` + testScript1(SCRIPT, _undefined, t) +} + +func TestTypedArraySetNoOverlapDifSizeDifBuffers(t *testing.T) { + const SCRIPT = ` + var dstBuf = new ArrayBuffer(1024); + var dst = new Uint8Array(dstBuf, 0, 2); + var src = new Uint16Array(2); + src[0] = 1; + src[1] = 2; + dst.set(src); + if (dst[0] !== 1 || dst[1] !== 2 || src[0] !== 1 || src[1] !== 2) { + throw new Error("dst: " + dst.join(",")); + } + ` + testScript1(SCRIPT, _undefined, t) +} + +func TestTypedArraySliceSameType(t *testing.T) { + const SCRIPT = ` + var src = Uint8Array.of(1,2,3,4); + var dst = src.slice(1, 3); + if (dst.length !== 2 || dst[0] !== 2 || dst[1] !== 3) { + throw new Error("dst: " + dst.join(",")); + } + ` + testScript1(SCRIPT, _undefined, t) +} + +func TestTypedArraySliceDifType(t *testing.T) { + const SCRIPT = ` + var src = Uint8Array.of(1,2,3,4); + Object.defineProperty(Uint8Array, Symbol.species, {value: Uint16Array, configurable: true}); + var dst = src.slice(1, 3); + if (!(dst instanceof Uint16Array)) { + throw new Error("wrong dst type: " + dst); + } + if (dst.length !== 2 || dst[0] !== 2 || dst[1] !== 3) { + throw new Error("dst: " + dst.join(",")); + } + ` + testScript1(SCRIPT, _undefined, t) +} diff --git a/builtin_weakmap.go b/builtin_weakmap.go index a9debccf..82c35223 100644 --- a/builtin_weakmap.go +++ b/builtin_weakmap.go @@ -133,7 +133,26 @@ func (r *Runtime) weakMapProto_set(call FunctionCall) Value { return call.This } -func (r *Runtime) builtin_newWeakMap(args []Value, proto *Object) *Object { +func (r *Runtime) needNew(name string) *Object { + return r.NewTypeError("Constructor %s requires 'new'", name) +} + +func (r *Runtime) getPrototypeFromCtor(newTarget, defCtor, defProto *Object) *Object { + if newTarget == defCtor { + return defProto + } + proto := newTarget.self.getStr("prototype", nil) + if obj, ok := proto.(*Object); ok { + return obj + } + return defProto +} + +func (r *Runtime) builtin_newWeakMap(args []Value, newTarget *Object) *Object { + if newTarget == nil { + panic(r.needNew("WeakMap")) + } + proto := r.getPrototypeFromCtor(newTarget, r.global.WeakMap, r.global.WeakMapPrototype) o := &Object{runtime: r} wmo := &weakMapObject{} @@ -189,7 +208,7 @@ func (r *Runtime) createWeakMapProto(val *Object) objectImpl { } func (r *Runtime) createWeakMap(val *Object) objectImpl { - o := r.newNativeFuncObj(val, r.constructorThrower("WeakMap"), r.builtin_newWeakMap, "WeakMap", r.global.WeakMapPrototype, 0) + o := r.newNativeConstructOnly(val, r.builtin_newWeakMap, r.global.WeakMapPrototype, "WeakMap", 0) return o } diff --git a/builtin_weakmap_test.go b/builtin_weakmap_test.go index 39eeae5e..eda61865 100644 --- a/builtin_weakmap_test.go +++ b/builtin_weakmap_test.go @@ -23,6 +23,7 @@ func TestWeakMapExpiry(t *testing.T) { t.Fatal(err) } runtime.GC() + runtime.GC() wmo := vm.Get("m").ToObject(vm).self.(*weakMapObject) wmo.m.Lock() l := len(wmo.m.data) diff --git a/builtin_weakset.go b/builtin_weakset.go index e3aa4243..7faeedf6 100644 --- a/builtin_weakset.go +++ b/builtin_weakset.go @@ -113,7 +113,11 @@ func (r *Runtime) populateWeakSetGeneric(s *Object, adderValue Value, iterable V }) } -func (r *Runtime) builtin_newWeakSet(args []Value, proto *Object) *Object { +func (r *Runtime) builtin_newWeakSet(args []Value, newTarget *Object) *Object { + if newTarget == nil { + panic(r.needNew("WeakSet")) + } + proto := r.getPrototypeFromCtor(newTarget, r.global.WeakSet, r.global.WeakSetPrototype) o := &Object{runtime: r} wso := &weakSetObject{} @@ -155,7 +159,7 @@ func (r *Runtime) createWeakSetProto(val *Object) objectImpl { } func (r *Runtime) createWeakSet(val *Object) objectImpl { - o := r.newNativeFuncObj(val, r.constructorThrower("WeakSet"), r.builtin_newWeakSet, "WeakSet", r.global.WeakSetPrototype, 0) + o := r.newNativeConstructOnly(val, r.builtin_newWeakSet, r.global.WeakSetPrototype, "WeakSet", 0) return o } diff --git a/builtin_weakset_test.go b/builtin_weakset_test.go index 3b01fa3f..aee428e4 100644 --- a/builtin_weakset_test.go +++ b/builtin_weakset_test.go @@ -36,6 +36,7 @@ func TestWeakSetExpiry(t *testing.T) { t.Fatal(err) } runtime.GC() + runtime.GC() wso := vm.Get("s").ToObject(vm).self.(*weakSetObject) wso.s.Lock() l := len(wso.s.data) diff --git a/date_test.go b/date_test.go index e27b2227..266d3db1 100644 --- a/date_test.go +++ b/date_test.go @@ -79,6 +79,19 @@ assert.throws = function (expectedErrorConstructor, func, message) { $ERROR(message); }; +function compareArray(a, b) { + if (b.length !== a.length) { + return false; + } + + for (var i = 0; i < a.length; i++) { + if (b[i] !== a[i]) { + return false; + } + } + return true; +} + ` func TestDateUTC(t *testing.T) { diff --git a/object.go b/object.go index f656a381..be5bac5b 100644 --- a/object.go +++ b/object.go @@ -461,18 +461,6 @@ func (o *baseObject) setProto(proto *Object, throw bool) bool { return true } -func (o *baseObject) _setProto(val Value) { - var proto *Object - if val != _null { - if obj, ok := val.(*Object); ok { - proto = obj - } else { - return - } - } - o.setProto(proto, true) -} - func (o *baseObject) setOwnStr(name string, val Value, throw bool) bool { ownDesc := o.values[name] if ownDesc == nil { @@ -1148,8 +1136,6 @@ func instanceOfOperator(o Value, c *Object) bool { func (o *Object) get(p Value, receiver Value) Value { switch p := p.(type) { - case valueString: - return o.self.getStr(p.String(), receiver) case valueInt: return o.self.getIdx(p, receiver) case *valueSymbol: @@ -1161,8 +1147,6 @@ func (o *Object) get(p Value, receiver Value) Value { func (o *Object) getOwnProp(p Value) Value { switch p := p.(type) { - case valueString: - return o.self.getOwnPropStr(p.String()) case valueInt: return o.self.getOwnPropIdx(p) case *valueSymbol: @@ -1174,8 +1158,6 @@ func (o *Object) getOwnProp(p Value) Value { func (o *Object) hasOwnProperty(p Value) bool { switch p := p.(type) { - case valueString: - return o.self.hasOwnPropertyStr(p.String()) case valueInt: return o.self.hasOwnPropertyIdx(p) case *valueSymbol: @@ -1187,8 +1169,6 @@ func (o *Object) hasOwnProperty(p Value) bool { func (o *Object) hasProperty(p Value) bool { switch p := p.(type) { - case valueString: - return o.self.hasPropertyStr(p.String()) case valueInt: return o.self.hasPropertyIdx(p) case *valueSymbol: @@ -1237,8 +1217,6 @@ func (o *Object) setStr(name string, val, receiver Value, throw bool) bool { func (o *Object) set(name Value, val, receiver Value, throw bool) bool { switch name := name.(type) { - case valueString: - return o.setStr(name.String(), val, receiver, throw) case valueInt: return o.setIdx(name, val, receiver, throw) case *valueSymbol: @@ -1335,8 +1313,6 @@ func (o *Object) setSym(name *valueSymbol, val, receiver Value, throw bool) bool func (o *Object) delete(n Value, throw bool) bool { switch n := n.(type) { - case valueString: - return o.self.deleteStr(n.String(), throw) case valueInt: return o.self.deleteIdx(n, throw) case *valueSymbol: @@ -1348,8 +1324,6 @@ func (o *Object) delete(n Value, throw bool) bool { func (o *Object) defineOwnProperty(n Value, desc PropertyDescriptor, throw bool) bool { switch n := n.(type) { - case valueString: - return o.self.defineOwnPropertyStr(n.String(), desc, throw) case valueInt: return o.self.defineOwnPropertyIdx(n, desc, throw) case *valueSymbol: diff --git a/parser/statement.go b/parser/statement.go index 1438f27d..45581cec 100644 --- a/parser/statement.go +++ b/parser/statement.go @@ -438,9 +438,11 @@ func (self *_parser) parseForOrForInStatement() ast.Statement { if self.token == token.IN { self.next() // in forIn = true - } else if self.token == token.OF { - self.next() // of - forOf = true + } else if self.token == token.IDENTIFIER { + if self.literal == "of" { + self.next() + forOf = true + } } } left = list @@ -449,9 +451,11 @@ func (self *_parser) parseForOrForInStatement() ast.Statement { if self.token == token.IN { self.next() forIn = true - } else if self.token == token.OF { - self.next() - forOf = true + } else if self.token == token.IDENTIFIER { + if self.literal == "of" { + self.next() + forOf = true + } } } self.scope.allowIn = allowIn diff --git a/proxy.go b/proxy.go index 21526a96..ba7f6622 100644 --- a/proxy.go +++ b/proxy.go @@ -1,9 +1,18 @@ package goja +import "reflect" + +// Proxy is a Go wrapper around ECMAScript Proxy. Calling Runtime.ToValue() on it +// returns the underlying Proxy. Calling Export() on an ECMAScript Proxy returns a wrapper. +// Use Runtime.NewProxy() to create one. type Proxy struct { proxy *proxyObject } +var ( + proxyType = reflect.TypeOf(Proxy{}) +) + type proxyPropIter struct { p *proxyObject names []Value @@ -24,7 +33,7 @@ func (i *proxyPropIter) next() (propIterItem, iterNextFunc) { return propIterItem{}, nil } -func (r *Runtime) newProxyObject(target *Object, handler *Object, proto *Object) *proxyObject { +func (r *Runtime) newProxyObject(target, handler, proto *Object) *proxyObject { if p, ok := target.self.(*proxyObject); ok { if p.handler == nil { panic(r.NewTypeError("Cannot create proxy with a revoked proxy as target")) @@ -58,10 +67,21 @@ func (r *Runtime) newProxyObject(target *Object, handler *Object, proto *Object) return p } -func (p *Proxy) Revoke() { +func (p Proxy) Revoke() { p.proxy.revoke() } +func (p Proxy) toValue(r *Runtime) Value { + if p.proxy == nil { + return _null + } + proxy := p.proxy.val + if proxy.runtime != r { + panic(r.NewTypeError("Illegal runtime transition of a Proxy")) + } + return proxy +} + type proxyTrap string const ( @@ -638,11 +658,11 @@ func (p *proxyObject) __isCompatibleDescriptor(extensible bool, desc *PropertyDe } if p.__isDataDescriptor(desc) && !current.accessor { - if desc.Configurable == FLAG_FALSE { - if desc.Writable == FLAG_FALSE && current.writable { + if !current.configurable { + if desc.Writable == FLAG_TRUE && !current.writable { return false } - if desc.Writable == FLAG_FALSE { + if !current.writable { if desc.Value != nil && !desc.Value.SameAs(current.value) { return false } @@ -651,7 +671,7 @@ func (p *proxyObject) __isCompatibleDescriptor(extensible bool, desc *PropertyDe return true } if p.__isAccessorDescriptor(desc) && current.accessor { - if desc.Configurable == FLAG_FALSE { + if !current.configurable { if desc.Setter != nil && desc.Setter.SameAs(current.setterFunc) { return false } @@ -758,6 +778,16 @@ func (p *proxyObject) className() string { return classObject } +func (p *proxyObject) exportType() reflect.Type { + return proxyType +} + +func (p *proxyObject) export() interface{} { + return Proxy{ + proxy: p, + } +} + func (p *proxyObject) revoke() { p.handler = nil p.target = nil diff --git a/runtime.go b/runtime.go index 0e78de67..ed5f088f 100644 --- a/runtime.go +++ b/runtime.go @@ -7,6 +7,7 @@ import ( "go/ast" "hash/maphash" "math" + "math/bits" "math/rand" "reflect" "strconv" @@ -50,11 +51,23 @@ type global struct { Symbol *Object Proxy *Object - ArrayBuffer *Object - WeakSet *Object - WeakMap *Object - Map *Object - Set *Object + ArrayBuffer *Object + DataView *Object + TypedArray *Object + Uint8Array *Object + Uint8ClampedArray *Object + Int8Array *Object + Uint16Array *Object + Int16Array *Object + Uint32Array *Object + Int32Array *Object + Float32Array *Object + Float64Array *Object + + WeakSet *Object + WeakMap *Object + Map *Object + Set *Object Error *Object TypeError *Object @@ -78,6 +91,8 @@ type global struct { ArrayIterator *Object ArrayBufferPrototype *Object + DataViewPrototype *Object + TypedArrayPrototype *Object WeakSetPrototype *Object WeakMapPrototype *Object MapPrototype *Object @@ -109,6 +124,7 @@ type global struct { mapAdder *Object setAdder *Object arrayValues *Object + arrayToString *Object } type Flag int @@ -340,7 +356,7 @@ func (r *Runtime) init() { r.initMath() r.initJSON() - //r.initTypedArrays() + r.initTypedArrays() r.initSymbol() r.initWeakSet() r.initWeakMap() @@ -485,6 +501,39 @@ func (r *Runtime) newNativeConstructor(call func(ConstructorCall) *Object, name return v } +func (r *Runtime) newNativeConstructOnly(v *Object, ctor func(args []Value, newTarget *Object) *Object, defaultProto *Object, name string, length int) *nativeFuncObject { + if v == nil { + v = &Object{runtime: r} + } + + f := &nativeFuncObject{ + baseFuncObject: baseFuncObject{ + baseObject: baseObject{ + class: classFunction, + val: v, + extensible: true, + prototype: r.global.FunctionPrototype, + }, + }, + f: func(call FunctionCall) Value { + return ctor(call.Arguments, nil) + }, + construct: func(args []Value, newTarget *Object) *Object { + if newTarget == nil { + newTarget = v + } + return ctor(args, newTarget) + }, + } + v.self = f + f.init(name, length) + if defaultProto != nil { + f._putProp("prototype", defaultProto, false, false, false) + } + + return f +} + func (r *Runtime) newNativeFunc(call func(FunctionCall) Value, construct func(args []Value, proto *Object) *Object, name string, proto *Object, length int) *Object { v := &Object{runtime: r} @@ -707,8 +756,11 @@ func (r *Runtime) wrapNativeConstruct(c func(args []Value, proto *Object) *Objec return func(args []Value, newTarget *Object) *Object { var p *Object if newTarget != nil { - p = r.toObject(newTarget.self.getStr("prototype", nil)) - } else { + if pp, ok := newTarget.self.getStr("prototype", nil).(*Object); ok { + p = pp + } + } + if p == nil { p = proto } return c(args, p) @@ -730,48 +782,103 @@ func (r *Runtime) checkObjectCoercible(v Value) { } } -func toUInt32(v Value) uint32 { +func toInt8(v Value) int8 { v = v.ToNumber() if i, ok := v.(valueInt); ok { - return uint32(i) + return int8(i) } if f, ok := v.(valueFloat); ok { f := float64(f) if !math.IsNaN(f) && !math.IsInf(f, 0) { - return uint32(int64(f)) + return int8(int64(f)) } } return 0 } -func toUInt16(v Value) uint16 { +func toUint8(v Value) uint8 { v = v.ToNumber() if i, ok := v.(valueInt); ok { - return uint16(i) + return uint8(i) } if f, ok := v.(valueFloat); ok { f := float64(f) if !math.IsNaN(f) && !math.IsInf(f, 0) { - return uint16(int64(f)) + return uint8(int64(f)) } } return 0 } -func toLength(v Value) int64 { - if v == nil { - return 0 +func toUint8Clamp(v Value) uint8 { + v = v.ToNumber() + if i, ok := v.(valueInt); ok { + if i < 0 { + return 0 + } + if i <= 255 { + return uint8(i) + } + return 255 } - i := v.ToInteger() - if i < 0 { - return 0 + + if num, ok := v.(valueFloat); ok { + num := float64(num) + if !math.IsNaN(num) { + if num < 0 { + return 0 + } + if num > 255 { + return 255 + } + f := math.Floor(num) + f1 := f + 0.5 + if f1 < num { + return uint8(f + 1) + } + if f1 > num { + return uint8(f) + } + r := uint8(f) + if r&1 != 0 { + return r + 1 + } + return r + } } - if i >= maxInt { - return maxInt - 1 + return 0 +} + +func toInt16(v Value) int16 { + v = v.ToNumber() + if i, ok := v.(valueInt); ok { + return int16(i) } - return i + + if f, ok := v.(valueFloat); ok { + f := float64(f) + if !math.IsNaN(f) && !math.IsInf(f, 0) { + return int16(int64(f)) + } + } + return 0 +} + +func toUint16(v Value) uint16 { + v = v.ToNumber() + if i, ok := v.(valueInt); ok { + return uint16(i) + } + + if f, ok := v.(valueFloat); ok { + f := float64(f) + if !math.IsNaN(f) && !math.IsInf(f, 0) { + return uint16(int64(f)) + } + } + return 0 } func toInt32(v Value) int32 { @@ -789,6 +896,50 @@ func toInt32(v Value) int32 { return 0 } +func toUint32(v Value) uint32 { + v = v.ToNumber() + if i, ok := v.(valueInt); ok { + return uint32(i) + } + + if f, ok := v.(valueFloat); ok { + f := float64(f) + if !math.IsNaN(f) && !math.IsInf(f, 0) { + return uint32(int64(f)) + } + } + return 0 +} + +func toFloat32(v Value) float32 { + return float32(v.ToFloat()) +} + +func toLength(v Value) int64 { + if v == nil { + return 0 + } + i := v.ToInteger() + if i < 0 { + return 0 + } + if i >= maxInt { + return maxInt - 1 + } + return i +} + +func (r *Runtime) toIndex(v Value) int { + intIdx := v.ToInteger() + if intIdx >= 0 && intIdx < maxInt { + if bits.UintSize == 32 && intIdx >= math.MaxInt32 { + panic(r.newError(r.global.RangeError, "Index %s overflows int", v.String())) + } + return int(intIdx) + } + panic(r.newError(r.global.RangeError, "Invalid index %s", v.String())) +} + func (r *Runtime) toBoolean(b bool) Value { if b { return valueTrue @@ -1014,6 +1165,8 @@ func (r *Runtime) ToValue(i interface{}) Value { panic(r.NewTypeError("Illegal runtime transition of an Object")) } return i + case valueContainer: + return i.toValue(r) case Value: return i case string: @@ -1030,15 +1183,6 @@ func (r *Runtime) ToValue(i interface{}) Value { case func(ConstructorCall) *Object: name := runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name() return r.newNativeConstructor(i, name, 0) - case *Proxy: - if i == nil { - return _null - } - proxy := i.proxy.val - if proxy.runtime != r { - panic(r.NewTypeError("Illegal runtime transition of a Proxy")) - } - return proxy case int: return intToValue(int64(i)) case int8: @@ -1668,12 +1812,23 @@ func (r *Runtime) speciesConstructor(o, defaultConstructor *Object) func(args [] if c != nil && c != _undefined { c = r.toObject(c).self.getSym(symSpecies, nil) } - if c == nil || c == _undefined { + if c == nil || c == _undefined || c == _null { c = defaultConstructor } return r.toConstructor(c) } +func (r *Runtime) speciesConstructorObj(o, defaultConstructor *Object) *Object { + c := o.self.getStr("constructor", nil) + if c != nil && c != _undefined { + c = r.toObject(c).self.getSym(symSpecies, nil) + } + if c == nil || c == _undefined || c == _null { + return defaultConstructor + } + return r.toObject(c) +} + func (r *Runtime) returnThis(call FunctionCall) Value { return call.This } @@ -1756,12 +1911,6 @@ func (r *Runtime) newLazyObject(create func(*Object) objectImpl) *Object { return val } -func (r *Runtime) constructorThrower(name string) func(call FunctionCall) Value { - return func(FunctionCall) Value { - panic(r.NewTypeError("Constructor %s requires 'new'", name)) - } -} - func nilSafe(v Value) Value { if v != nil { return v diff --git a/tc39_test.go b/tc39_test.go index 43e5dca8..090c2b6d 100644 --- a/tc39_test.go +++ b/tc39_test.go @@ -18,6 +18,16 @@ const ( var ( invalidFormatError = errors.New("Invalid file format") + + ignorableTestError = &valueSymbol{} + + sabStub = MustCompile("sabStub.js", ` + Object.defineProperty(this, "SharedArrayBuffer", { + get: function() { + throw IgnorableTestError; + } + });`, + false) ) var ( @@ -34,64 +44,6 @@ var ( // utf-16 "test/built-ins/Array/prototype/concat/Array.prototype.concat_spreadable-string-wrapper.js": true, - // cross-realm - "test/built-ins/Symbol/unscopables/cross-realm.js": true, - "test/built-ins/Symbol/toStringTag/cross-realm.js": true, - "test/built-ins/Symbol/toPrimitive/cross-realm.js": true, - "test/built-ins/Symbol/split/cross-realm.js": true, - "test/built-ins/Symbol/species/cross-realm.js": true, - "test/built-ins/Symbol/search/cross-realm.js": true, - "test/built-ins/Symbol/replace/cross-realm.js": true, - "test/built-ins/Symbol/match/cross-realm.js": true, - "test/built-ins/Symbol/keyFor/cross-realm.js": true, - "test/built-ins/Symbol/iterator/cross-realm.js": true, - "test/built-ins/Symbol/isConcatSpreadable/cross-realm.js": true, - "test/built-ins/Symbol/hasInstance/cross-realm.js": true, - "test/built-ins/Symbol/for/cross-realm.js": true, - "test/built-ins/WeakSet/proto-from-ctor-realm.js": true, - "test/built-ins/WeakMap/proto-from-ctor-realm.js": true, - "test/built-ins/Map/proto-from-ctor-realm.js": true, - "test/built-ins/Set/proto-from-ctor-realm.js": true, - "test/built-ins/Object/proto-from-ctor.js": true, - "test/built-ins/Array/from/proto-from-ctor-realm.js": true, - "test/built-ins/Array/of/proto-from-ctor-realm.js": true, - "test/built-ins/Array/prototype/concat/create-proto-from-ctor-realm-non-array.js": true, - "test/built-ins/Array/prototype/concat/create-proto-from-ctor-realm-array.js": true, - "test/built-ins/Array/prototype/filter/create-proto-from-ctor-realm-non-array.js": true, - "test/built-ins/Array/prototype/filter/create-proto-from-ctor-realm-array.js": true, - "test/built-ins/Array/prototype/map/create-proto-from-ctor-realm-non-array.js": true, - "test/built-ins/Array/prototype/map/create-proto-from-ctor-realm-array.js": true, - "test/built-ins/Array/prototype/slice/create-proto-from-ctor-realm-non-array.js": true, - "test/built-ins/Array/prototype/slice/create-proto-from-ctor-realm-array.js": true, - "test/built-ins/Array/prototype/splice/create-proto-from-ctor-realm-non-array.js": true, - "test/built-ins/Array/prototype/splice/create-proto-from-ctor-realm-array.js": true, - "test/built-ins/Proxy/construct/arguments-realm.js": true, - "test/built-ins/Proxy/setPrototypeOf/trap-is-not-callable-realm.js": true, - "test/built-ins/Proxy/getPrototypeOf/trap-is-not-callable-realm.js": true, - "test/built-ins/Proxy/set/trap-is-not-callable-realm.js": true, - "test/built-ins/Proxy/getOwnPropertyDescriptor/trap-is-not-callable-realm.js": true, - "test/built-ins/Proxy/getOwnPropertyDescriptor/result-type-is-not-object-nor-undefined-realm.js": true, - "test/built-ins/Proxy/get/trap-is-not-callable-realm.js": true, - "test/built-ins/Proxy/preventExtensions/trap-is-not-callable-realm.js": true, - "test/built-ins/Proxy/defineProperty/null-handler-realm.js": true, - "test/built-ins/Proxy/ownKeys/trap-is-not-callable-realm.js": true, - "test/built-ins/Proxy/ownKeys/return-not-list-object-throws-realm.js": true, - "test/built-ins/Proxy/deleteProperty/trap-is-not-callable-realm.js": true, - "test/built-ins/Proxy/isExtensible/trap-is-not-callable-realm.js": true, - "test/built-ins/Proxy/defineProperty/trap-is-not-callable-realm.js": true, - "test/built-ins/Proxy/defineProperty/targetdesc-undefined-target-is-not-extensible-realm.js": true, - "test/built-ins/Proxy/defineProperty/targetdesc-undefined-not-configurable-descriptor-realm.js": true, - "test/built-ins/Proxy/defineProperty/targetdesc-not-compatible-descriptor.js": true, - "test/built-ins/Proxy/defineProperty/targetdesc-not-compatible-descriptor-realm.js": true, - "test/built-ins/Proxy/defineProperty/targetdesc-not-compatible-descriptor-not-configurable-target-realm.js": true, - "test/built-ins/Proxy/defineProperty/targetdesc-configurable-desc-not-configurable-realm.js": true, - "test/built-ins/Proxy/has/trap-is-not-callable-realm.js": true, - "test/built-ins/Proxy/defineProperty/desc-realm.js": true, - "test/built-ins/Proxy/apply/trap-is-not-callable-realm.js": true, - "test/built-ins/Proxy/apply/arguments-realm.js": true, - "test/built-ins/Proxy/construct/trap-is-undefined-proto-from-ctor-realm.js": true, - "test/built-ins/Proxy/construct/trap-is-not-callable-realm.js": true, - // class "test/language/statements/class/subclass/builtin-objects/Symbol/symbol-valid-as-extends-value.js": true, "test/language/statements/class/subclass/builtin-objects/Symbol/new-symbol-with-super-throws.js": true, @@ -107,6 +59,10 @@ var ( "test/language/statements/class/subclass/builtin-objects/Object/regular-subclassing.js": true, "test/built-ins/Array/prototype/concat/Array.prototype.concat_non-array.js": true, "test/language/statements/class/subclass/builtin-objects/Array/length.js": true, + "test/language/statements/class/subclass/builtin-objects/TypedArray/super-must-be-called.js": true, + "test/language/statements/class/subclass/builtin-objects/TypedArray/regular-subclassing.js": true, + "test/language/statements/class/subclass/builtin-objects/DataView/super-must-be-called.js": true, + "test/language/statements/class/subclass/builtin-objects/DataView/regular-subclassing.js": true, // full unicode regexp flag "test/built-ins/RegExp/prototype/Symbol.match/u-advance-after-empty.js": true, @@ -115,18 +71,18 @@ var ( "test/built-ins/RegExp/prototype/Symbol.match/builtin-infer-unicode.js": true, // object literals - "test/built-ins/Array/from/source-object-iterator-1.js": true, - "test/built-ins/Array/from/source-object-iterator-2.js": true, - - // Typed arrays - "test/built-ins/Array/from/items-is-arraybuffer.js": true, - "test/built-ins/Array/prototype/concat/Array.prototype.concat_small-typed-array.js": true, - "test/built-ins/Array/prototype/concat/Array.prototype.concat_large-typed-array.js": true, - - // for-of - "test/language/statements/for-of/Array.prototype.keys.js": true, - "test/language/statements/for-of/Array.prototype.entries.js": true, - "test/language/statements/for-of/Array.prototype.Symbol.iterator.js": true, + "test/built-ins/Array/from/source-object-iterator-1.js": true, + "test/built-ins/Array/from/source-object-iterator-2.js": true, + "test/built-ins/TypedArray/prototype/fill/fill-values-conversion-once.js": true, + "test/built-ins/TypedArrays/of/this-is-not-constructor.js": true, + "test/built-ins/TypedArrays/of/argument-number-value-throws.js": true, + "test/built-ins/TypedArrays/from/this-is-not-constructor.js": true, + "test/built-ins/TypedArrays/from/set-value-abrupt-completion.js": true, + "test/built-ins/TypedArrays/from/property-abrupt-completion.js": true, + "test/built-ins/TypedArray/of/this-is-not-constructor.js": true, + "test/built-ins/TypedArray/from/this-is-not-constructor.js": true, + "test/built-ins/DataView/custom-proto-access-throws.js": true, + "test/built-ins/DataView/custom-proto-access-throws-sab.js": true, // arrow-function "test/built-ins/Object/prototype/toString/proxy-function.js": true, @@ -154,16 +110,23 @@ var ( "22.1.2.5", "22.1.3", "22.1.4", + "22.2", "23.1", "23.2", "23.3", "23.4", + "24.2", "25.1.2", "26.1", "26.2", "B.2.1", "B.2.2", } + + esIdPrefixWhiteList = []string{ + "sec-array.prototype.includes", + "sec-%typedarray%", + } ) type tc39Test struct { @@ -239,6 +202,20 @@ func parseTC39File(name string) (*tc39Meta, string, error) { return &meta, str, nil } +func (*tc39TestCtx) detachArrayBuffer(call FunctionCall) Value { + if obj, ok := call.Argument(0).(*Object); ok { + if buf, ok := obj.self.(*arrayBufferObject); ok { + buf.detach() + return _undefined + } + } + panic(typeError("detachArrayBuffer() is called with incompatible argument")) +} + +func (*tc39TestCtx) throwIgnorableTestError(FunctionCall) Value { + panic(ignorableTestError) +} + func (ctx *tc39TestCtx) runTC39Test(name, src string, meta *tc39Meta, t testing.TB) { defer func() { if x := recover(); x != nil { @@ -246,10 +223,21 @@ func (ctx *tc39TestCtx) runTC39Test(name, src string, meta *tc39Meta, t testing. } }() vm := New() + _262 := vm.NewObject() + _262.Set("detachArrayBuffer", ctx.detachArrayBuffer) + _262.Set("createRealm", ctx.throwIgnorableTestError) + vm.Set("$262", _262) + vm.Set("IgnorableTestError", ignorableTestError) + vm.RunProgram(sabStub) err, early := ctx.runTC39Script(name, src, meta.Includes, vm) if err != nil { if meta.Negative.Type == "" { + if err, ok := err.(*Exception); ok { + if err.Value() == ignorableTestError { + t.Skip("Test threw IgnorableTestError") + } + } t.Fatalf("%s: %v", name, err) } else { if meta.Negative.Phase == "early" && !early || meta.Negative.Phase == "runtime" && early { @@ -321,6 +309,18 @@ func (ctx *tc39TestCtx) runTC39File(name string, t testing.TB) { } } } + if skip { + if meta.Esid != "" { + for _, prefix := range esIdPrefixWhiteList { + if strings.HasPrefix(meta.Esid, prefix) && + (len(meta.Esid) == len(prefix) || meta.Esid[len(prefix)] == '.') { + + skip = false + break + } + } + } + } if skip { t.Skip("Not ES5") } diff --git a/token/token_const.go b/token/token_const.go index e266e6ca..16976284 100644 --- a/token/token_const.go +++ b/token/token_const.go @@ -208,9 +208,6 @@ var keywordTable = map[string]_keyword{ "in": { token: IN, }, - "of": { - token: OF, - }, "do": { token: DO, }, diff --git a/typedarrays.go b/typedarrays.go new file mode 100644 index 00000000..885bf119 --- /dev/null +++ b/typedarrays.go @@ -0,0 +1,884 @@ +package goja + +import ( + "math" + "math/bits" + "reflect" + "strconv" + "unsafe" +) + +type byteOrder bool + +const ( + bigEndian byteOrder = false + littleEndian byteOrder = true +) + +var ( + nativeEndian byteOrder + + arrayBufferType = reflect.TypeOf(ArrayBuffer{}) +) + +type typedArrayObjectCtor func(buf *arrayBufferObject, offset, length int, proto *Object) *typedArrayObject + +type arrayBufferObject struct { + baseObject + detached bool + data []byte +} + +// ArrayBuffer is a Go wrapper around ECMAScript ArrayBuffer. Calling Runtime.ToValue() on it +// returns the underlying ArrayBuffer. Calling Export() on an ECMAScript ArrayBuffer returns a wrapper. +// Use Runtime.NewArrayBuffer([]byte) to create one. +type ArrayBuffer struct { + buf *arrayBufferObject +} + +type dataViewObject struct { + baseObject + viewedArrayBuf *arrayBufferObject + byteLen, byteOffset int +} + +type typedArray interface { + toRaw(Value) uint64 + get(idx int) Value + set(idx int, value Value) + getRaw(idx int) uint64 + setRaw(idx int, raw uint64) + less(i, j int) bool + swap(i, j int) + typeMatch(v Value) bool +} + +type uint8Array []uint8 +type uint8ClampedArray []uint8 +type int8Array []int8 +type uint16Array []uint16 +type int16Array []int16 +type uint32Array []uint32 +type int32Array []int32 +type float32Array []float32 +type float64Array []float64 + +type typedArrayObject struct { + baseObject + viewedArrayBuf *arrayBufferObject + defaultCtor *Object + length, offset int + elemSize int + typedArray typedArray +} + +func (a ArrayBuffer) toValue(r *Runtime) Value { + if a.buf == nil { + return _null + } + v := a.buf.val + if v.runtime != r { + panic(r.NewTypeError("Illegal runtime transition of an ArrayBuffer")) + } + return v +} + +// Bytes returns the underlying []byte for this ArrayBuffer. +// For detached ArrayBuffers returns nil. +func (a ArrayBuffer) Bytes() []byte { + return a.buf.data +} + +// Detach the ArrayBuffer. After this, the underlying []byte becomes unreferenced and any attempt +// to use this ArrayBuffer results in a TypeError. +// Returns false if it was already detached, true otherwise. +// Note, this method may only be called from the goroutine that 'owns' the Runtime, it may not +// be called concurrently. +func (a ArrayBuffer) Detach() bool { + if a.buf.detached { + return false + } + a.buf.detach() + return true +} + +// Detached returns true if the ArrayBuffer is detached. +func (a ArrayBuffer) Detached() bool { + return a.buf.detached +} + +func (r *Runtime) NewArrayBuffer(data []byte) ArrayBuffer { + buf := r._newArrayBuffer(r.global.ArrayBufferPrototype, nil) + buf.data = data + return ArrayBuffer{ + buf: buf, + } +} + +func (a *uint8Array) get(idx int) Value { + return intToValue(int64((*a)[idx])) +} + +func (a *uint8Array) getRaw(idx int) uint64 { + return uint64((*a)[idx]) +} + +func (a *uint8Array) set(idx int, value Value) { + (*a)[idx] = toUint8(value) +} + +func (a *uint8Array) toRaw(v Value) uint64 { + return uint64(toUint8(v)) +} + +func (a *uint8Array) setRaw(idx int, v uint64) { + (*a)[idx] = uint8(v) +} + +func (a *uint8Array) less(i, j int) bool { + return (*a)[i] < (*a)[j] +} + +func (a *uint8Array) swap(i, j int) { + (*a)[i], (*a)[j] = (*a)[j], (*a)[i] +} + +func (a *uint8Array) typeMatch(v Value) bool { + if i, ok := v.(valueInt); ok { + return i >= 0 && i <= 255 + } + return false +} + +func (a *uint8ClampedArray) get(idx int) Value { + return intToValue(int64((*a)[idx])) +} + +func (a *uint8ClampedArray) getRaw(idx int) uint64 { + return uint64((*a)[idx]) +} + +func (a *uint8ClampedArray) set(idx int, value Value) { + (*a)[idx] = toUint8Clamp(value) +} + +func (a *uint8ClampedArray) toRaw(v Value) uint64 { + return uint64(toUint8Clamp(v)) +} + +func (a *uint8ClampedArray) setRaw(idx int, v uint64) { + (*a)[idx] = uint8(v) +} + +func (a *uint8ClampedArray) less(i, j int) bool { + return (*a)[i] < (*a)[j] +} + +func (a *uint8ClampedArray) swap(i, j int) { + (*a)[i], (*a)[j] = (*a)[j], (*a)[i] +} + +func (a *uint8ClampedArray) typeMatch(v Value) bool { + if i, ok := v.(valueInt); ok { + return i >= 0 && i <= 255 + } + return false +} + +func (a *int8Array) get(idx int) Value { + return intToValue(int64((*a)[idx])) +} + +func (a *int8Array) getRaw(idx int) uint64 { + return uint64((*a)[idx]) +} + +func (a *int8Array) set(idx int, value Value) { + (*a)[idx] = toInt8(value) +} + +func (a *int8Array) toRaw(v Value) uint64 { + return uint64(toInt8(v)) +} + +func (a *int8Array) setRaw(idx int, v uint64) { + (*a)[idx] = int8(v) +} + +func (a *int8Array) less(i, j int) bool { + return (*a)[i] < (*a)[j] +} + +func (a *int8Array) swap(i, j int) { + (*a)[i], (*a)[j] = (*a)[j], (*a)[i] +} + +func (a *int8Array) typeMatch(v Value) bool { + if i, ok := v.(valueInt); ok { + return i >= math.MinInt8 && i <= math.MaxInt8 + } + return false +} + +func (a *uint16Array) get(idx int) Value { + return intToValue(int64((*a)[idx])) +} + +func (a *uint16Array) getRaw(idx int) uint64 { + return uint64((*a)[idx]) +} + +func (a *uint16Array) set(idx int, value Value) { + (*a)[idx] = toUint16(value) +} + +func (a *uint16Array) toRaw(v Value) uint64 { + return uint64(toUint16(v)) +} + +func (a *uint16Array) setRaw(idx int, v uint64) { + (*a)[idx] = uint16(v) +} + +func (a *uint16Array) less(i, j int) bool { + return (*a)[i] < (*a)[j] +} + +func (a *uint16Array) swap(i, j int) { + (*a)[i], (*a)[j] = (*a)[j], (*a)[i] +} + +func (a *uint16Array) typeMatch(v Value) bool { + if i, ok := v.(valueInt); ok { + return i >= 0 && i <= math.MaxUint16 + } + return false +} + +func (a *int16Array) get(idx int) Value { + return intToValue(int64((*a)[idx])) +} + +func (a *int16Array) getRaw(idx int) uint64 { + return uint64((*a)[idx]) +} + +func (a *int16Array) set(idx int, value Value) { + (*a)[idx] = toInt16(value) +} + +func (a *int16Array) toRaw(v Value) uint64 { + return uint64(toInt16(v)) +} + +func (a *int16Array) setRaw(idx int, v uint64) { + (*a)[idx] = int16(v) +} + +func (a *int16Array) less(i, j int) bool { + return (*a)[i] < (*a)[j] +} + +func (a *int16Array) swap(i, j int) { + (*a)[i], (*a)[j] = (*a)[j], (*a)[i] +} + +func (a *int16Array) typeMatch(v Value) bool { + if i, ok := v.(valueInt); ok { + return i >= math.MinInt16 && i <= math.MaxInt16 + } + return false +} + +func (a *uint32Array) get(idx int) Value { + return intToValue(int64((*a)[idx])) +} + +func (a *uint32Array) getRaw(idx int) uint64 { + return uint64((*a)[idx]) +} + +func (a *uint32Array) set(idx int, value Value) { + (*a)[idx] = toUint32(value) +} + +func (a *uint32Array) toRaw(v Value) uint64 { + return uint64(toUint32(v)) +} + +func (a *uint32Array) setRaw(idx int, v uint64) { + (*a)[idx] = uint32(v) +} + +func (a *uint32Array) less(i, j int) bool { + return (*a)[i] < (*a)[j] +} + +func (a *uint32Array) swap(i, j int) { + (*a)[i], (*a)[j] = (*a)[j], (*a)[i] +} + +func (a *uint32Array) typeMatch(v Value) bool { + if i, ok := v.(valueInt); ok { + return i >= 0 && i <= math.MaxUint32 + } + return false +} + +func (a *int32Array) get(idx int) Value { + return intToValue(int64((*a)[idx])) +} + +func (a *int32Array) getRaw(idx int) uint64 { + return uint64((*a)[idx]) +} + +func (a *int32Array) set(idx int, value Value) { + (*a)[idx] = toInt32(value) +} + +func (a *int32Array) toRaw(v Value) uint64 { + return uint64(toInt32(v)) +} + +func (a *int32Array) setRaw(idx int, v uint64) { + (*a)[idx] = int32(v) +} + +func (a *int32Array) less(i, j int) bool { + return (*a)[i] < (*a)[j] +} + +func (a *int32Array) swap(i, j int) { + (*a)[i], (*a)[j] = (*a)[j], (*a)[i] +} + +func (a *int32Array) typeMatch(v Value) bool { + if i, ok := v.(valueInt); ok { + return i >= math.MinInt32 && i <= math.MaxInt32 + } + return false +} + +func (a *float32Array) get(idx int) Value { + return floatToValue(float64((*a)[idx])) +} + +func (a *float32Array) getRaw(idx int) uint64 { + return uint64(math.Float32bits((*a)[idx])) +} + +func (a *float32Array) set(idx int, value Value) { + (*a)[idx] = toFloat32(value) +} + +func (a *float32Array) toRaw(v Value) uint64 { + return uint64(math.Float32bits(toFloat32(v))) +} + +func (a *float32Array) setRaw(idx int, v uint64) { + (*a)[idx] = math.Float32frombits(uint32(v)) +} + +func typedFloatLess(x, y float64) bool { + xNan := math.IsNaN(x) + yNan := math.IsNaN(y) + if xNan && yNan { + return false + } + if xNan { + return false + } + if yNan { + return true + } + if x >= y { + return false + } + return true +} + +func (a *float32Array) less(i, j int) bool { + return typedFloatLess(float64((*a)[i]), float64((*a)[j])) +} + +func (a *float32Array) swap(i, j int) { + (*a)[i], (*a)[j] = (*a)[j], (*a)[i] +} + +func (a *float32Array) typeMatch(v Value) bool { + switch v.(type) { + case valueInt, valueFloat: + return true + } + return false +} + +func (a *float64Array) get(idx int) Value { + return floatToValue((*a)[idx]) +} + +func (a *float64Array) getRaw(idx int) uint64 { + return math.Float64bits((*a)[idx]) +} + +func (a *float64Array) set(idx int, value Value) { + (*a)[idx] = value.ToFloat() +} + +func (a *float64Array) toRaw(v Value) uint64 { + return math.Float64bits(v.ToFloat()) +} + +func (a *float64Array) setRaw(idx int, v uint64) { + (*a)[idx] = math.Float64frombits(v) +} + +func (a *float64Array) less(i, j int) bool { + return typedFloatLess((*a)[i], (*a)[j]) +} + +func (a *float64Array) swap(i, j int) { + (*a)[i], (*a)[j] = (*a)[j], (*a)[i] +} + +func (a *float64Array) typeMatch(v Value) bool { + switch v.(type) { + case valueInt, valueFloat: + return true + } + return false +} + +func (a *typedArrayObject) _getIdx(idx int) Value { + a.viewedArrayBuf.ensureNotDetached() + if idx < a.length { + return a.typedArray.get(idx + a.offset) + } + return nil +} + +func strToTAIdx(s string) (int, bool) { + i, err := strconv.ParseInt(s, 10, bits.UintSize) + if err != nil { + return 0, false + } + return int(i), true +} + +func (a *typedArrayObject) getOwnPropStr(name string) Value { + if idx, ok := strToTAIdx(name); ok { + v := a._getIdx(idx) + if v != nil { + return &valueProperty{ + value: v, + writable: true, + enumerable: true, + } + } + return nil + } + return a.baseObject.getOwnPropStr(name) +} + +func (a *typedArrayObject) getOwnPropIdx(idx valueInt) Value { + v := a._getIdx(toInt(int64(idx))) + if v != nil { + return &valueProperty{ + value: v, + writable: true, + enumerable: true, + } + } + return nil +} + +func (a *typedArrayObject) getStr(name string, receiver Value) Value { + if idx, ok := strToTAIdx(name); ok { + prop := a._getIdx(idx) + if prop == nil { + if a.prototype != nil { + if receiver == nil { + return a.prototype.self.getStr(name, a.val) + } + return a.prototype.self.getStr(name, receiver) + } + } + return prop + } + return a.baseObject.getStr(name, receiver) +} + +func (a *typedArrayObject) getIdx(idx valueInt, receiver Value) Value { + prop := a._getIdx(toInt(int64(idx))) + if prop == nil { + if a.prototype != nil { + if receiver == nil { + return a.prototype.self.getIdx(idx, a.val) + } + return a.prototype.self.getIdx(idx, receiver) + } + } + return prop +} + +func (a *typedArrayObject) _putIdx(idx int, v Value, throw bool) bool { + v = v.ToNumber() + a.viewedArrayBuf.ensureNotDetached() + if idx >= 0 && idx < a.length { + a.typedArray.set(idx+a.offset, v) + return true + } + // As far as I understand the specification this should throw, but neither V8 nor SpiderMonkey does + return false +} + +func (a *typedArrayObject) _hasIdx(idx int) bool { + a.viewedArrayBuf.ensureNotDetached() + return idx >= 0 && idx < a.length +} + +func (a *typedArrayObject) setOwnStr(p string, v Value, throw bool) bool { + if idx, ok := strToTAIdx(p); ok { + return a._putIdx(idx, v, throw) + } + return a.baseObject.setOwnStr(p, v, throw) +} + +func (a *typedArrayObject) setOwnIdx(p valueInt, v Value, throw bool) bool { + return a._putIdx(toInt(int64(p)), v, throw) +} + +func (a *typedArrayObject) setForeignStr(p string, v, receiver Value, throw bool) (res bool, handled bool) { + return a._setForeignStr(p, a.getOwnPropStr(p), v, receiver, throw) +} + +func (a *typedArrayObject) setForeignIdx(p valueInt, v, receiver Value, throw bool) (res bool, handled bool) { + return a._setForeignIdx(p, trueValIfPresent(a.hasOwnPropertyIdx(p)), v, receiver, throw) +} + +func (a *typedArrayObject) hasOwnPropertyStr(name string) bool { + if idx, ok := strToTAIdx(name); ok { + a.viewedArrayBuf.ensureNotDetached() + return idx < a.length + } + + return a.baseObject.hasOwnPropertyStr(name) +} + +func (a *typedArrayObject) hasOwnPropertyIdx(idx valueInt) bool { + return a._hasIdx(toInt(int64(idx))) +} + +func (a *typedArrayObject) _defineIdxProperty(idx int, desc PropertyDescriptor, throw bool) bool { + prop, ok := a._defineOwnProperty(strconv.Itoa(idx), a.getOwnPropIdx(valueInt(idx)), desc, throw) + if ok { + return a._putIdx(idx, prop, throw) + } + return ok +} + +func (a *typedArrayObject) defineOwnPropertyStr(name string, desc PropertyDescriptor, throw bool) bool { + if idx, ok := strToTAIdx(name); ok { + return a._defineIdxProperty(idx, desc, throw) + } + return a.baseObject.defineOwnPropertyStr(name, desc, throw) +} + +func (a *typedArrayObject) defineOwnPropertyIdx(name valueInt, desc PropertyDescriptor, throw bool) bool { + return a._defineIdxProperty(toInt(int64(name)), desc, throw) +} + +func (a *typedArrayObject) deleteStr(name string, throw bool) bool { + if idx, ok := strToTAIdx(name); ok { + if idx < a.length { + a.val.runtime.typeErrorResult(throw, "Cannot delete property '%d' of %s", idx, a.val.String()) + } + } + + return a.baseObject.deleteStr(name, throw) +} + +func (a *typedArrayObject) deleteIdx(idx valueInt, throw bool) bool { + if idx >= 0 && int64(idx) < int64(a.length) { + a.val.runtime.typeErrorResult(throw, "Cannot delete property '%d' of %s", idx, a.val.String()) + } + + return true +} + +func (a *typedArrayObject) ownKeys(all bool, accum []Value) []Value { + if accum == nil { + accum = make([]Value, 0, a.length) + } + for i := 0; i < a.length; i++ { + accum = append(accum, asciiString(strconv.Itoa(i))) + } + return a.baseObject.ownKeys(all, accum) +} + +type typedArrayPropIter struct { + a *typedArrayObject + idx int +} + +func (i *typedArrayPropIter) next() (propIterItem, iterNextFunc) { + if i.idx < i.a.length { + name := strconv.Itoa(i.idx) + prop := i.a._getIdx(i.idx) + i.idx++ + return propIterItem{name: name, value: prop}, i.next + } + + return i.a.baseObject.enumerateUnfiltered()() +} + +func (a *typedArrayObject) enumerateUnfiltered() iterNextFunc { + return (&typedArrayPropIter{ + a: a, + }).next +} + +func (r *Runtime) _newTypedArrayObject(buf *arrayBufferObject, offset, length, elemSize int, defCtor *Object, arr typedArray, proto *Object) *typedArrayObject { + o := &Object{runtime: r} + a := &typedArrayObject{ + baseObject: baseObject{ + val: o, + class: classObject, + prototype: proto, + extensible: true, + }, + viewedArrayBuf: buf, + offset: offset, + length: length, + elemSize: elemSize, + defaultCtor: defCtor, + typedArray: arr, + } + o.self = a + a.init() + return a + +} + +func (r *Runtime) newUint8ArrayObject(buf *arrayBufferObject, offset, length int, proto *Object) *typedArrayObject { + return r._newTypedArrayObject(buf, offset, length, 1, r.global.Uint8Array, (*uint8Array)(&buf.data), proto) +} + +func (r *Runtime) newUint8ClampedArrayObject(buf *arrayBufferObject, offset, length int, proto *Object) *typedArrayObject { + return r._newTypedArrayObject(buf, offset, length, 1, r.global.Uint8ClampedArray, (*uint8ClampedArray)(&buf.data), proto) +} + +func (r *Runtime) newInt8ArrayObject(buf *arrayBufferObject, offset, length int, proto *Object) *typedArrayObject { + return r._newTypedArrayObject(buf, offset, length, 1, r.global.Int8Array, (*int8Array)(unsafe.Pointer(&buf.data)), proto) +} + +func (r *Runtime) newUint16ArrayObject(buf *arrayBufferObject, offset, length int, proto *Object) *typedArrayObject { + return r._newTypedArrayObject(buf, offset, length, 2, r.global.Uint16Array, (*uint16Array)(unsafe.Pointer(&buf.data)), proto) +} + +func (r *Runtime) newInt16ArrayObject(buf *arrayBufferObject, offset, length int, proto *Object) *typedArrayObject { + return r._newTypedArrayObject(buf, offset, length, 2, r.global.Int16Array, (*int16Array)(unsafe.Pointer(&buf.data)), proto) +} + +func (r *Runtime) newUint32ArrayObject(buf *arrayBufferObject, offset, length int, proto *Object) *typedArrayObject { + return r._newTypedArrayObject(buf, offset, length, 4, r.global.Uint32Array, (*uint32Array)(unsafe.Pointer(&buf.data)), proto) +} + +func (r *Runtime) newInt32ArrayObject(buf *arrayBufferObject, offset, length int, proto *Object) *typedArrayObject { + return r._newTypedArrayObject(buf, offset, length, 4, r.global.Int32Array, (*int32Array)(unsafe.Pointer(&buf.data)), proto) +} + +func (r *Runtime) newFloat32ArrayObject(buf *arrayBufferObject, offset, length int, proto *Object) *typedArrayObject { + return r._newTypedArrayObject(buf, offset, length, 4, r.global.Float32Array, (*float32Array)(unsafe.Pointer(&buf.data)), proto) +} + +func (r *Runtime) newFloat64ArrayObject(buf *arrayBufferObject, offset, length int, proto *Object) *typedArrayObject { + return r._newTypedArrayObject(buf, offset, length, 8, r.global.Float64Array, (*float64Array)(unsafe.Pointer(&buf.data)), proto) +} + +func (o *dataViewObject) getIdxAndByteOrder(idxVal, littleEndianVal Value, size int) (int, byteOrder) { + getIdx := o.val.runtime.toIndex(idxVal) + o.viewedArrayBuf.ensureNotDetached() + if getIdx+size > o.byteLen { + panic(o.val.runtime.newError(o.val.runtime.global.RangeError, "Index %d is out of bounds", getIdx)) + } + getIdx += o.byteOffset + var bo byteOrder + if littleEndianVal != nil { + if littleEndianVal.ToBoolean() { + bo = littleEndian + } else { + bo = bigEndian + } + } else { + bo = nativeEndian + } + return getIdx, bo +} + +func (o *arrayBufferObject) ensureNotDetached() { + if o.detached { + panic(o.val.runtime.NewTypeError("ArrayBuffer is detached")) + } +} + +func (o *arrayBufferObject) getFloat32(idx int, byteOrder byteOrder) float32 { + return math.Float32frombits(o.getUint32(idx, byteOrder)) +} + +func (o *arrayBufferObject) setFloat32(idx int, val float32, byteOrder byteOrder) { + o.setUint32(idx, math.Float32bits(val), byteOrder) +} + +func (o *arrayBufferObject) getFloat64(idx int, byteOrder byteOrder) float64 { + return math.Float64frombits(o.getUint64(idx, byteOrder)) +} + +func (o *arrayBufferObject) setFloat64(idx int, val float64, byteOrder byteOrder) { + o.setUint64(idx, math.Float64bits(val), byteOrder) +} + +func (o *arrayBufferObject) getUint64(idx int, byteOrder byteOrder) uint64 { + var b []byte + if byteOrder == nativeEndian { + b = o.data[idx : idx+8] + } else { + b = make([]byte, 8) + d := o.data[idx : idx+8] + b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7] = d[7], d[6], d[5], d[4], d[3], d[2], d[1], d[0] + } + return *((*uint64)(unsafe.Pointer(&b[0]))) +} + +func (o *arrayBufferObject) setUint64(idx int, val uint64, byteOrder byteOrder) { + if byteOrder == nativeEndian { + *(*uint64)(unsafe.Pointer(&o.data[idx])) = val + } else { + b := (*[8]byte)(unsafe.Pointer(&val)) + d := o.data[idx : idx+8] + d[0], d[1], d[2], d[3], d[4], d[5], d[6], d[7] = b[7], b[6], b[5], b[4], b[3], b[2], b[1], b[0] + } +} + +func (o *arrayBufferObject) getUint32(idx int, byteOrder byteOrder) uint32 { + var b []byte + if byteOrder == nativeEndian { + b = o.data[idx : idx+4] + } else { + b = make([]byte, 4) + d := o.data[idx : idx+4] + b[0], b[1], b[2], b[3] = d[3], d[2], d[1], d[0] + } + return *((*uint32)(unsafe.Pointer(&b[0]))) +} + +func (o *arrayBufferObject) setUint32(idx int, val uint32, byteOrder byteOrder) { + if byteOrder == nativeEndian { + *(*uint32)(unsafe.Pointer(&o.data[idx])) = val + } else { + b := (*[4]byte)(unsafe.Pointer(&val)) + d := o.data[idx : idx+4] + d[0], d[1], d[2], d[3] = b[3], b[2], b[1], b[0] + } +} + +func (o *arrayBufferObject) getUint16(idx int, byteOrder byteOrder) uint16 { + var b []byte + if byteOrder == nativeEndian { + b = o.data[idx : idx+2] + } else { + b = make([]byte, 2) + d := o.data[idx : idx+2] + b[0], b[1] = d[1], d[0] + } + return *((*uint16)(unsafe.Pointer(&b[0]))) +} + +func (o *arrayBufferObject) setUint16(idx int, val uint16, byteOrder byteOrder) { + if byteOrder == nativeEndian { + *(*uint16)(unsafe.Pointer(&o.data[idx])) = val + } else { + b := (*[2]byte)(unsafe.Pointer(&val)) + d := o.data[idx : idx+2] + d[0], d[1] = b[1], b[0] + } +} + +func (o *arrayBufferObject) getUint8(idx int) uint8 { + return o.data[idx] +} + +func (o *arrayBufferObject) setUint8(idx int, val uint8) { + o.data[idx] = val +} + +func (o *arrayBufferObject) getInt32(idx int, byteOrder byteOrder) int32 { + return int32(o.getUint32(idx, byteOrder)) +} + +func (o *arrayBufferObject) setInt32(idx int, val int32, byteOrder byteOrder) { + o.setUint32(idx, uint32(val), byteOrder) +} + +func (o *arrayBufferObject) getInt16(idx int, byteOrder byteOrder) int16 { + return int16(o.getUint16(idx, byteOrder)) +} + +func (o *arrayBufferObject) setInt16(idx int, val int16, byteOrder byteOrder) { + o.setUint16(idx, uint16(val), byteOrder) +} + +func (o *arrayBufferObject) getInt8(idx int) int8 { + return int8(o.data[idx]) +} + +func (o *arrayBufferObject) setInt8(idx int, val int8) { + o.setUint8(idx, uint8(val)) +} + +func (o *arrayBufferObject) detach() { + o.data = nil + o.detached = true +} + +func (o *arrayBufferObject) exportType() reflect.Type { + return arrayBufferType +} + +func (o *arrayBufferObject) export() interface{} { + return ArrayBuffer{ + buf: o, + } +} + +func (r *Runtime) _newArrayBuffer(proto *Object, o *Object) *arrayBufferObject { + if o == nil { + o = &Object{runtime: r} + } + b := &arrayBufferObject{ + baseObject: baseObject{ + class: classObject, + val: o, + prototype: proto, + extensible: true, + }, + } + o.self = b + b.init() + return b +} + +func init() { + buf := [2]byte{} + *(*uint16)(unsafe.Pointer(&buf[0])) = uint16(0xCAFE) + + switch buf { + case [2]byte{0xFE, 0xCA}: + nativeEndian = littleEndian + case [2]byte{0xCA, 0xFE}: + nativeEndian = bigEndian + default: + panic("Could not determine native endianness.") + } +} diff --git a/typedarrays_test.go b/typedarrays_test.go new file mode 100644 index 00000000..2629240a --- /dev/null +++ b/typedarrays_test.go @@ -0,0 +1,71 @@ +package goja + +import "testing" + +func TestUint16ArrayObject(t *testing.T) { + vm := New() + buf := vm._newArrayBuffer(vm.global.ArrayBufferPrototype, nil) + buf.data = make([]byte, 16) + if nativeEndian == littleEndian { + buf.data[2] = 0xFE + buf.data[3] = 0xCA + } else { + buf.data[2] = 0xCA + buf.data[3] = 0xFE + } + a := vm.newUint16ArrayObject(buf, 1, 1, nil) + v := a.getIdx(valueInt(0), nil) + if v != valueInt(0xCAFE) { + t.Fatalf("v: %v", v) + } +} + +func TestArrayBufferGoWrapper(t *testing.T) { + vm := New() + data := []byte{0xAA, 0xBB} + buf := vm.NewArrayBuffer(data) + vm.Set("buf", buf) + _, err := vm.RunString(` + var a = new Uint8Array(buf); + if (a.length !== 2 || a[0] !== 0xAA || a[1] !== 0xBB) { + throw new Error(a); + } + `) + if err != nil { + t.Fatal(err) + } + ret, err := vm.RunString(` + var b = Uint8Array.of(0xCC, 0xDD); + b.buffer; + `) + if err != nil { + t.Fatal(err) + } + buf1 := ret.Export().(ArrayBuffer) + data1 := buf1.Bytes() + if len(data1) != 2 || data1[0] != 0xCC || data1[1] != 0xDD { + t.Fatal(data1) + } + if buf1.Detached() { + t.Fatal("buf1.Detached() returned true") + } + if !buf1.Detach() { + t.Fatal("buf1.Detach() returned false") + } + if !buf1.Detached() { + t.Fatal("buf1.Detached() returned false") + } + _, err = vm.RunString(` + try { + (b[0]); + throw new Error("expected TypeError"); + } catch (e) { + if (!(e instanceof TypeError)) { + throw e; + } + } + `) + if err != nil { + t.Fatal(err) + } +} diff --git a/value.go b/value.go index a3cdded2..a2541de5 100644 --- a/value.go +++ b/value.go @@ -18,7 +18,8 @@ var ( _positiveInf Value = valueFloat(math.Inf(+1)) _negativeInf Value = valueFloat(math.Inf(-1)) _positiveZero Value = valueInt(0) - _negativeZero Value = valueFloat(math.Float64frombits(0 | (1 << 63))) + negativeZero = math.Float64frombits(0 | (1 << 63)) + _negativeZero Value = valueFloat(negativeZero) _epsilon = valueFloat(2.2204460492503130808472633361816e-16) _undefined Value = valueUndefined{} ) @@ -55,6 +56,10 @@ type Value interface { hash(hash64 hash.Hash64) uint64 } +type valueContainer interface { + toValue(*Runtime) Value +} + type typeError string type valueInt int64 diff --git a/vm.go b/vm.go index 3c0f5d10..f86d4f8b 100644 --- a/vm.go +++ b/vm.go @@ -924,7 +924,7 @@ var sal _sal func (_sal) exec(vm *vm) { left := toInt32(vm.stack[vm.sp-2]) - right := toUInt32(vm.stack[vm.sp-1]) + right := toUint32(vm.stack[vm.sp-1]) vm.stack[vm.sp-2] = intToValue(int64(left << (right & 0x1F))) vm.sp-- vm.pc++ @@ -936,7 +936,7 @@ var sar _sar func (_sar) exec(vm *vm) { left := toInt32(vm.stack[vm.sp-2]) - right := toUInt32(vm.stack[vm.sp-1]) + right := toUint32(vm.stack[vm.sp-1]) vm.stack[vm.sp-2] = intToValue(int64(left >> (right & 0x1F))) vm.sp-- vm.pc++ @@ -947,8 +947,8 @@ type _shr struct{} var shr _shr func (_shr) exec(vm *vm) { - left := toUInt32(vm.stack[vm.sp-2]) - right := toUInt32(vm.stack[vm.sp-1]) + left := toUint32(vm.stack[vm.sp-2]) + right := toUint32(vm.stack[vm.sp-1]) vm.stack[vm.sp-2] = intToValue(int64(left >> (right & 0x1F))) vm.sp-- vm.pc++ From 6567f6ffae6e7db30aaeee7511ddac5a84e59b2e Mon Sep 17 00:00:00 2001 From: Dmitry Panov Date: Sat, 4 Apr 2020 19:42:43 +0100 Subject: [PATCH 20/46] Minor array index conversion fixes for 32-bit platforms. --- array.go | 3 --- builtin_typedarrays.go | 3 +++ object_goslice.go | 9 ++++----- value.go | 1 + vm.go | 4 ++++ 5 files changed, 12 insertions(+), 8 deletions(-) diff --git a/array.go b/array.go index 4c903605..c5844f0e 100644 --- a/array.go +++ b/array.go @@ -622,9 +622,6 @@ func strToGoIdx(s string) int { return int(strToIdx64(s)) } i := strToIdx(s) - if i == math.MaxUint32 { - return -1 - } if i >= math.MaxInt32 { return -1 } diff --git a/builtin_typedarrays.go b/builtin_typedarrays.go index 59e679a0..03e99213 100644 --- a/builtin_typedarrays.go +++ b/builtin_typedarrays.go @@ -676,6 +676,9 @@ func (r *Runtime) typedArrayProto_lastIndexOf(call FunctionCall) Value { fromIndex = min(fromIndex, length-1) } else { fromIndex += length + if fromIndex < 0 { + fromIndex = -1 // prevent underflow in toInt() on 32-bit platforms + } } } diff --git a/object_goslice.go b/object_goslice.go index 48822a0c..10892bee 100644 --- a/object_goslice.go +++ b/object_goslice.go @@ -135,11 +135,10 @@ func (o *objectGoSlice) putIdx(idx int, v Value, throw bool) { } func toInt(i int64) int { - if bits.UintSize == 64 { - return int(i) - } - if i >= math.MaxInt32 { - panic(typeError("Integer value overflows 32-bit int")) + if bits.UintSize == 32 { + if i >= math.MaxInt32 || i < math.MinInt32 { + panic(rangeError("Integer value overflows 32-bit int")) + } } return int(i) } diff --git a/value.go b/value.go index a2541de5..18e864ec 100644 --- a/value.go +++ b/value.go @@ -61,6 +61,7 @@ type valueContainer interface { } type typeError string +type rangeError string type valueInt int64 type valueFloat float64 diff --git a/vm.go b/vm.go index f86d4f8b..65199369 100644 --- a/vm.go +++ b/vm.go @@ -389,6 +389,10 @@ func (vm *vm) try(f func()) (ex *Exception) { ex = &Exception{ val: vm.r.NewTypeError(string(x1)), } + case rangeError: + ex = &Exception{ + val: vm.r.newError(vm.r.global.RangeError, string(x1)), + } default: /* if vm.prg != nil { From af442b8aa04ebbce0b16ec478d032217c8187fa1 Mon Sep 17 00:00:00 2001 From: Dmitry Panov Date: Thu, 9 Apr 2020 12:31:51 +0100 Subject: [PATCH 21/46] Applied fix for #144 to typed arrays. --- builtin_typedarrays.go | 18 ++++++++++++++++-- builtin_typedarrays_test.go | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/builtin_typedarrays.go b/builtin_typedarrays.go index 03e99213..2fb0985f 100644 --- a/builtin_typedarrays.go +++ b/builtin_typedarrays.go @@ -1,6 +1,7 @@ package goja import ( + "math" "sort" "strings" "unsafe" @@ -28,9 +29,22 @@ func (ctx *typedArraySortCtx) Less(i, j int) bool { res := ctx.compare(FunctionCall{ This: _undefined, Arguments: []Value{x, y}, - }).ToInteger() + }).ToNumber() ctx.needValidate = true - return res < 0 + if i, ok := res.(valueInt); ok { + return i < 0 + } + f := res.ToFloat() + if f < 0 { + return true + } + if f > 0 { + return false + } + if math.Signbit(f) { + return true + } + return false } return ctx.ta.typedArray.less(offset+i, offset+j) diff --git a/builtin_typedarrays_test.go b/builtin_typedarrays_test.go index d816754a..69cbe86d 100644 --- a/builtin_typedarrays_test.go +++ b/builtin_typedarrays_test.go @@ -264,3 +264,35 @@ func TestTypedArraySliceDifType(t *testing.T) { ` testScript1(SCRIPT, _undefined, t) } + +func TestTypedArraySortComparatorReturnValueFloats(t *testing.T) { + const SCRIPT = ` + var a = Float64Array.of( + 5.97, + 9.91, + 4.13, + 9.28, + 3.29 + ); + a.sort( function(a, b) { return a - b; } ); + for (var i = 1; i < a.length; i++) { + if (a[i] < a[i-1]) { + throw new Error("Array is not sorted: " + a); + } + } + ` + testScript1(SCRIPT, _undefined, t) +} + +func TestTypedArraySortComparatorReturnValueNegZero(t *testing.T) { + const SCRIPT = ` + var a = new Uint8Array([2, 1]); + a.sort( function(a, b) { return a > b ? 0 : -0; } ); + for (var i = 1; i < a.length; i++) { + if (a[i] < a[i-1]) { + throw new Error("Array is not sorted: " + a); + } + } + ` + testScript1(SCRIPT, _undefined, t) +} From 5df89c3d318663e84856824b8ce098c61543bd90 Mon Sep 17 00:00:00 2001 From: Dmitry Panov Date: Sat, 11 Apr 2020 14:25:58 +0100 Subject: [PATCH 22/46] Support for malformed UTF-16 strings and property keys. Missing String methods. (#146) --- array.go | 36 +-- array_sparse.go | 30 +-- ast/node.go | 9 +- builtin_array.go | 6 +- builtin_global.go | 16 +- builtin_json.go | 10 +- builtin_object.go | 13 +- builtin_proxy.go | 18 +- builtin_regexp.go | 26 +- builtin_string.go | 444 ++++++++++++++++++++++++++++++++--- builtin_string_test.go | 22 ++ builtin_symbol.go | 55 +++-- builtin_typedarrays.go | 4 +- compiler.go | 32 +-- compiler_expr.go | 26 +- compiler_stmt.go | 58 +++-- func.go | 36 +-- map.go | 8 +- object.go | 159 +++++++------ object_args.go | 14 +- object_gomap.go | 42 ++-- object_gomap_reflect.go | 43 ++-- object_gomap_reflect_test.go | 29 +++ object_gomap_test.go | 29 +++ object_goreflect.go | 49 ++-- object_goreflect_test.go | 17 ++ object_goslice.go | 33 +-- object_goslice_reflect.go | 28 ++- object_lazy.go | 28 ++- object_test.go | 12 + parser/expression.go | 47 ++-- parser/lexer.go | 305 ++++++++++++++++-------- parser/lexer_test.go | 10 +- parser/parser.go | 10 +- parser/parser_test.go | 91 +++---- parser/scope.go | 5 +- proxy.go | 53 +++-- regexp.go | 8 +- runtime.go | 78 +++--- runtime_test.go | 46 +++- string.go | 179 ++++++++++---- string_ascii.go | 44 ++-- string_unicode.go | 171 ++++++++++---- tc39_test.go | 118 +++++++--- typedarrays.go | 24 +- unistring/string.go | 122 ++++++++++ value.go | 137 +++++++---- vm.go | 146 ++++++------ 48 files changed, 2040 insertions(+), 886 deletions(-) create mode 100644 unistring/string.go diff --git a/array.go b/array.go index c5844f0e..fb69e096 100644 --- a/array.go +++ b/array.go @@ -5,6 +5,8 @@ import ( "math/bits" "reflect" "strconv" + + "github.com/dop251/goja/unistring" ) type arrayIterObject struct { @@ -158,7 +160,7 @@ func (a *arrayObject) getIdx(idx valueInt, receiver Value) Value { return prop } -func (a *arrayObject) getOwnPropStr(name string) Value { +func (a *arrayObject) getOwnPropStr(name unistring.String) Value { if i := strToIdx(name); i != math.MaxUint32 { if i < uint32(len(a.values)) { return a.values[i] @@ -178,7 +180,7 @@ func (a *arrayObject) getOwnPropIdx(idx valueInt) Value { return nil } - return a.baseObject.getOwnPropStr(idx.String()) + return a.baseObject.getOwnPropStr(idx.string()) } func (a *arrayObject) sortLen() int64 { @@ -197,7 +199,7 @@ func (a *arrayObject) swap(i, j int64) { a.values[i], a.values[j] = a.values[j], a.values[i] } -func (a *arrayObject) getStr(name string, receiver Value) Value { +func (a *arrayObject) getStr(name unistring.String, receiver Value) Value { return a.getStrWithOwnProp(a.getOwnPropStr(name), name, receiver) } @@ -210,7 +212,7 @@ func (a *arrayObject) setOwnIdx(idx valueInt, val Value, throw bool) bool { if i := toIdx(idx); i != math.MaxUint32 { return a._setOwnIdx(i, val, throw) } else { - return a.baseObject.setOwnStr(idx.String(), val, throw) + return a.baseObject.setOwnStr(idx.string(), val, throw) } } @@ -259,7 +261,7 @@ func (a *arrayObject) _setOwnIdx(idx uint32, val Value, throw bool) bool { return true } -func (a *arrayObject) setOwnStr(name string, val Value, throw bool) bool { +func (a *arrayObject) setOwnStr(name unistring.String, val Value, throw bool) bool { if idx := strToIdx(name); idx != math.MaxUint32 { return a._setOwnIdx(idx, val, throw) } else { @@ -275,7 +277,7 @@ func (a *arrayObject) setForeignIdx(idx valueInt, val, receiver Value, throw boo return a._setForeignIdx(idx, a.getOwnPropIdx(idx), val, receiver, throw) } -func (a *arrayObject) setForeignStr(name string, val, receiver Value, throw bool) (bool, bool) { +func (a *arrayObject) setForeignStr(name unistring.String, val, receiver Value, throw bool) (bool, bool) { return a._setForeignStr(name, a.getOwnPropStr(name), val, receiver, throw) } @@ -286,7 +288,7 @@ type arrayPropIter struct { func (i *arrayPropIter) next() (propIterItem, iterNextFunc) { for i.idx < len(i.a.values) { - name := strconv.Itoa(i.idx) + name := unistring.String(strconv.Itoa(i.idx)) prop := i.a.values[i.idx] i.idx++ if prop != nil { @@ -318,7 +320,7 @@ func (a *arrayObject) ownKeys(all bool, accum []Value) []Value { return a.baseObject.ownKeys(all, accum) } -func (a *arrayObject) hasOwnPropertyStr(name string) bool { +func (a *arrayObject) hasOwnPropertyStr(name unistring.String) bool { if idx := strToIdx(name); idx != math.MaxUint32 { return idx < uint32(len(a.values)) && a.values[idx] != nil } else { @@ -330,7 +332,7 @@ func (a *arrayObject) hasOwnPropertyIdx(idx valueInt) bool { if idx := toIdx(idx); idx != math.MaxUint32 { return idx < uint32(len(a.values)) && a.values[idx] != nil } - return a.baseObject.hasOwnPropertyStr(idx.String()) + return a.baseObject.hasOwnPropertyStr(idx.string()) } func (a *arrayObject) expand(idx uint32) bool { @@ -420,7 +422,7 @@ func (a *arrayObject) _defineIdxProperty(idx uint32, desc PropertyDescriptor, th if idx < uint32(len(a.values)) { existing = a.values[idx] } - prop, ok := a.baseObject._defineOwnProperty(strconv.FormatUint(uint64(idx), 10), existing, desc, throw) + prop, ok := a.baseObject._defineOwnProperty(unistring.String(strconv.FormatUint(uint64(idx), 10)), existing, desc, throw) if ok { if idx >= a.length { if !a.setLengthInt(int64(idx)+1, throw) { @@ -440,7 +442,7 @@ func (a *arrayObject) _defineIdxProperty(idx uint32, desc PropertyDescriptor, th return ok } -func (a *arrayObject) defineOwnPropertyStr(name string, descr PropertyDescriptor, throw bool) bool { +func (a *arrayObject) defineOwnPropertyStr(name unistring.String, descr PropertyDescriptor, throw bool) bool { if idx := strToIdx(name); idx != math.MaxUint32 { return a._defineIdxProperty(idx, descr, throw) } @@ -454,7 +456,7 @@ func (a *arrayObject) defineOwnPropertyIdx(idx valueInt, descr PropertyDescripto if idx := toIdx(idx); idx != math.MaxUint32 { return a._defineIdxProperty(idx, descr, throw) } - return a.baseObject.defineOwnPropertyStr(idx.String(), descr, throw) + return a.baseObject.defineOwnPropertyStr(idx.string(), descr, throw) } func (a *arrayObject) _deleteIdxProp(idx uint32, throw bool) bool { @@ -474,7 +476,7 @@ func (a *arrayObject) _deleteIdxProp(idx uint32, throw bool) bool { return true } -func (a *arrayObject) deleteStr(name string, throw bool) bool { +func (a *arrayObject) deleteStr(name unistring.String, throw bool) bool { if idx := strToIdx(name); idx != math.MaxUint32 { return a._deleteIdxProp(idx, throw) } @@ -485,7 +487,7 @@ func (a *arrayObject) deleteIdx(idx valueInt, throw bool) bool { if idx := toIdx(idx); idx != math.MaxUint32 { return a._deleteIdxProp(idx, throw) } - return a.baseObject.deleteStr(idx.String(), throw) + return a.baseObject.deleteStr(idx.string(), throw) } func (a *arrayObject) export() interface{} { @@ -518,7 +520,7 @@ func toIdx(v valueInt) uint32 { return math.MaxUint32 } -func strToIdx64(s string) int64 { +func strToIdx64(s unistring.String) int64 { if s == "" { return -1 } @@ -567,7 +569,7 @@ func strToIdx64(s string) int64 { return n1 } -func strToIdx(s string) uint32 { +func strToIdx(s unistring.String) uint32 { if s == "" { return math.MaxUint32 } @@ -617,7 +619,7 @@ func strToIdx(s string) uint32 { return n1 } -func strToGoIdx(s string) int { +func strToGoIdx(s unistring.String) int { if bits.UintSize == 64 { return int(strToIdx64(s)) } diff --git a/array_sparse.go b/array_sparse.go index 69456b6d..17b64c5b 100644 --- a/array_sparse.go +++ b/array_sparse.go @@ -6,6 +6,8 @@ import ( "reflect" "sort" "strconv" + + "github.com/dop251/goja/unistring" ) type sparseArrayItem struct { @@ -109,7 +111,7 @@ func (a *sparseArrayObject) _getIdx(idx uint32) Value { return nil } -func (a *sparseArrayObject) getStr(name string, receiver Value) Value { +func (a *sparseArrayObject) getStr(name unistring.String, receiver Value) Value { return a.getStrWithOwnProp(a.getOwnPropStr(name), name, receiver) } @@ -137,7 +139,7 @@ func (a *sparseArrayObject) getLengthProp() Value { return &a.lengthProp } -func (a *sparseArrayObject) getOwnPropStr(name string) Value { +func (a *sparseArrayObject) getOwnPropStr(name unistring.String) Value { if idx := strToIdx(name); idx != math.MaxUint32 { return a._getIdx(idx) } @@ -151,7 +153,7 @@ func (a *sparseArrayObject) getOwnPropIdx(idx valueInt) Value { if idx := toIdx(idx); idx != math.MaxUint32 { return a._getIdx(idx) } - return a.baseObject.getOwnPropStr(idx.String()) + return a.baseObject.getOwnPropStr(idx.string()) } func (a *sparseArrayObject) add(idx uint32, val Value) { @@ -218,7 +220,7 @@ func (a *sparseArrayObject) _setOwnIdx(idx uint32, val Value, throw bool) bool { return true } -func (a *sparseArrayObject) setOwnStr(name string, val Value, throw bool) bool { +func (a *sparseArrayObject) setOwnStr(name unistring.String, val Value, throw bool) bool { if idx := strToIdx(name); idx != math.MaxUint32 { return a._setOwnIdx(idx, val, throw) } else { @@ -235,10 +237,10 @@ func (a *sparseArrayObject) setOwnIdx(idx valueInt, val Value, throw bool) bool return a._setOwnIdx(idx, val, throw) } - return a.baseObject.setOwnStr(idx.String(), val, throw) + return a.baseObject.setOwnStr(idx.string(), val, throw) } -func (a *sparseArrayObject) setForeignStr(name string, val, receiver Value, throw bool) (bool, bool) { +func (a *sparseArrayObject) setForeignStr(name unistring.String, val, receiver Value, throw bool) (bool, bool) { return a._setForeignStr(name, a.getOwnPropStr(name), val, receiver, throw) } @@ -253,7 +255,7 @@ type sparseArrayPropIter struct { func (i *sparseArrayPropIter) next() (propIterItem, iterNextFunc) { for i.idx < len(i.a.items) { - name := strconv.Itoa(int(i.a.items[i.idx].idx)) + name := unistring.String(strconv.Itoa(int(i.a.items[i.idx].idx))) prop := i.a.items[i.idx].value i.idx++ if prop != nil { @@ -299,7 +301,7 @@ func (a *sparseArrayObject) setValues(values []Value, objCount int) { } } -func (a *sparseArrayObject) hasOwnPropertyStr(name string) bool { +func (a *sparseArrayObject) hasOwnPropertyStr(name unistring.String) bool { if idx := strToIdx(name); idx != math.MaxUint32 { i := a.findIdx(idx) return i < len(a.items) && a.items[i].idx == idx @@ -314,7 +316,7 @@ func (a *sparseArrayObject) hasOwnPropertyIdx(idx valueInt) bool { return i < len(a.items) && a.items[i].idx == idx } - return a.baseObject.hasOwnPropertyStr(idx.String()) + return a.baseObject.hasOwnPropertyStr(idx.string()) } func (a *sparseArrayObject) expand(idx uint32) bool { @@ -345,7 +347,7 @@ func (a *sparseArrayObject) _defineIdxProperty(idx uint32, desc PropertyDescript if i < len(a.items) && a.items[i].idx == idx { existing = a.items[i].value } - prop, ok := a.baseObject._defineOwnProperty(strconv.FormatUint(uint64(idx), 10), existing, desc, throw) + prop, ok := a.baseObject._defineOwnProperty(unistring.String(strconv.FormatUint(uint64(idx), 10)), existing, desc, throw) if ok { if idx >= a.length { if !a.setLengthInt(int64(idx)+1, throw) { @@ -376,7 +378,7 @@ func (a *sparseArrayObject) _defineIdxProperty(idx uint32, desc PropertyDescript return ok } -func (a *sparseArrayObject) defineOwnPropertyStr(name string, descr PropertyDescriptor, throw bool) bool { +func (a *sparseArrayObject) defineOwnPropertyStr(name unistring.String, descr PropertyDescriptor, throw bool) bool { if idx := strToIdx(name); idx != math.MaxUint32 { return a._defineIdxProperty(idx, descr, throw) } @@ -390,7 +392,7 @@ func (a *sparseArrayObject) defineOwnPropertyIdx(idx valueInt, descr PropertyDes if idx := toIdx(idx); idx != math.MaxUint32 { return a._defineIdxProperty(idx, descr, throw) } - return a.baseObject.defineOwnPropertyStr(idx.String(), descr, throw) + return a.baseObject.defineOwnPropertyStr(idx.string(), descr, throw) } func (a *sparseArrayObject) _deleteIdxProp(idx uint32, throw bool) bool { @@ -410,7 +412,7 @@ func (a *sparseArrayObject) _deleteIdxProp(idx uint32, throw bool) bool { return true } -func (a *sparseArrayObject) deleteStr(name string, throw bool) bool { +func (a *sparseArrayObject) deleteStr(name unistring.String, throw bool) bool { if idx := strToIdx(name); idx != math.MaxUint32 { return a._deleteIdxProp(idx, throw) } @@ -421,7 +423,7 @@ func (a *sparseArrayObject) deleteIdx(idx valueInt, throw bool) bool { if idx := toIdx(idx); idx != math.MaxUint32 { return a._deleteIdxProp(idx, throw) } - return a.baseObject.deleteStr(idx.String(), throw) + return a.baseObject.deleteStr(idx.string(), throw) } func (a *sparseArrayObject) sortLen() int64 { diff --git a/ast/node.go b/ast/node.go index 8968fc5c..02460205 100644 --- a/ast/node.go +++ b/ast/node.go @@ -12,6 +12,7 @@ package ast import ( "github.com/dop251/goja/file" "github.com/dop251/goja/token" + "github.com/dop251/goja/unistring" "github.com/go-sourcemap/sourcemap" ) @@ -98,7 +99,7 @@ type ( } Identifier struct { - Name string + Name unistring.String Idx file.Idx } @@ -134,7 +135,7 @@ type ( } Property struct { - Key string + Key unistring.String Kind string Value Expression } @@ -153,7 +154,7 @@ type ( StringLiteral struct { Idx file.Idx Literal string - Value string + Value unistring.String } ThisExpression struct { @@ -168,7 +169,7 @@ type ( } VariableExpression struct { - Name string + Name unistring.String Idx file.Idx Initializer Expression } diff --git a/builtin_array.go b/builtin_array.go index 307e019a..0fadea6a 100644 --- a/builtin_array.go +++ b/builtin_array.go @@ -1289,7 +1289,7 @@ type arraySortCtx struct { compare func(FunctionCall) Value } -func (ctx *arraySortCtx) sortCompare(x, y Value) int { +func (a *arraySortCtx) sortCompare(x, y Value) int { if x == nil && y == nil { return 0 } @@ -1314,8 +1314,8 @@ func (ctx *arraySortCtx) sortCompare(x, y Value) int { return -1 } - if ctx.compare != nil { - f := ctx.compare(FunctionCall{ + if a.compare != nil { + f := a.compare(FunctionCall{ This: _undefined, Arguments: []Value{x, y}, }).ToFloat() diff --git a/builtin_global.go b/builtin_global.go index 2f22d62d..68853d1c 100644 --- a/builtin_global.go +++ b/builtin_global.go @@ -2,12 +2,12 @@ package goja import ( "errors" + "github.com/dop251/goja/unistring" "io" "math" "regexp" "strconv" "strings" - "unicode/utf16" "unicode/utf8" ) @@ -95,7 +95,7 @@ func (r *Runtime) _encode(uriString valueString, unescaped *[256]bool) valueStri reader = uriString.reader(0) for { rn, _, err := reader.ReadRune() - if err != nil { + if err == io.EOF { break } @@ -189,7 +189,7 @@ func (r *Runtime) _decode(sv valueString, reservedSet *[256]bool) valueString { us = append(us, rn) t = t[size:] } - return unicodeString(utf16.Encode(us)) + return unicodeStringFromRunes(us) } func ishex(c byte) bool { @@ -240,7 +240,7 @@ func (r *Runtime) builtin_escape(call FunctionCall) Value { s := call.Argument(0).toString() var sb strings.Builder l := s.length() - for i := int64(0); i < l; i++ { + for i := 0; i < l; i++ { r := uint16(s.charAt(i)) if r >= 'A' && r <= 'Z' || r >= 'a' && r <= 'z' || r >= '0' && r <= '9' || r == '@' || r == '*' || r == '_' || r == '+' || r == '-' || r == '.' || r == '/' { @@ -267,11 +267,12 @@ func (r *Runtime) builtin_unescape(call FunctionCall) Value { var asciiBuf []byte var unicodeBuf []uint16 if unicode { - unicodeBuf = make([]uint16, 0, l) + unicodeBuf = make([]uint16, 1, l+1) + unicodeBuf[0] = unistring.BOM } else { asciiBuf = make([]byte, 0, l) } - for i := int64(0); i < l; { + for i := 0; i < l; { r := s.charAt(i) if r == '%' { if i <= l-6 && s.charAt(i+1) == 'u' { @@ -303,7 +304,8 @@ func (r *Runtime) builtin_unescape(call FunctionCall) Value { } out: if r >= utf8.RuneSelf && !unicode { - unicodeBuf = make([]uint16, 0, l) + unicodeBuf = make([]uint16, 1, l+1) + unicodeBuf[0] = unistring.BOM for _, b := range asciiBuf { unicodeBuf = append(unicodeBuf, uint16(b)) } diff --git a/builtin_json.go b/builtin_json.go index dea31878..c4de85fb 100644 --- a/builtin_json.go +++ b/builtin_json.go @@ -7,9 +7,11 @@ import ( "io" "math" "strings" + + "github.com/dop251/goja/unistring" ) -var hex = "0123456789abcdef" +const hex = "0123456789abcdef" func (r *Runtime) builtinJSON_parse(call FunctionCall) Value { d := json.NewDecoder(bytes.NewBufferString(call.Argument(0).String())) @@ -85,7 +87,7 @@ func (r *Runtime) builtinJSON_decodeObject(d *json.Decoder) (*Object, error) { return nil, err } - object.self._putProp(key, value, true, true, true) + object.self._putProp(unistring.NewFromString(key), value, true, true, true) } return object, nil } @@ -150,9 +152,9 @@ func (r *Runtime) builtinJSON_reviveWalk(reviver func(FunctionCall) Value, holde for _, itemName := range object.self.ownKeys(false, nil) { value := r.builtinJSON_reviveWalk(reviver, object, name) if value == _undefined { - object.self.deleteStr(itemName.String(), false) + object.self.deleteStr(itemName.string(), false) } else { - object.self.setOwnStr(itemName.String(), value, false) + object.self.setOwnStr(itemName.string(), value, false) } } } diff --git a/builtin_object.go b/builtin_object.go index bd679026..71010e21 100644 --- a/builtin_object.go +++ b/builtin_object.go @@ -82,7 +82,7 @@ func (r *Runtime) object_getOwnPropertyNames(call FunctionCall) Value { func (r *Runtime) object_getOwnPropertySymbols(call FunctionCall) Value { obj := call.Argument(0).ToObject(r) - return r.newArrayValues(obj.self.ownSymbols()) + return r.newArrayValues(obj.self.ownSymbols(true, nil)) } func (r *Runtime) toValueProp(v Value) *valueProperty { @@ -179,21 +179,20 @@ func (r *Runtime) toPropertyDescriptor(v Value) (ret PropertyDescriptor) { func (r *Runtime) _defineProperties(o *Object, p Value) { type propItem struct { - name string + name Value prop PropertyDescriptor } props := p.ToObject(r) - names := props.self.ownKeys(false, nil) + names := props.self.ownPropertyKeys(false, nil) list := make([]propItem, 0, len(names)) for _, itemName := range names { - itemNameStr := itemName.String() list = append(list, propItem{ - name: itemNameStr, - prop: r.toPropertyDescriptor(props.self.getStr(itemNameStr, nil)), + name: itemName, + prop: r.toPropertyDescriptor(props.get(itemName, nil)), }) } for _, prop := range list { - o.self.defineOwnPropertyStr(prop.name, prop.prop, true) + o.defineOwnProperty(prop.name, prop.prop, true) } } diff --git a/builtin_proxy.go b/builtin_proxy.go index 7f2f479a..2a3b87c7 100644 --- a/builtin_proxy.go +++ b/builtin_proxy.go @@ -1,6 +1,10 @@ package goja -import "fmt" +import ( + "fmt" + + "github.com/dop251/goja/unistring" +) func (r *Runtime) newNativeProxyHandler(nativeHandler *ProxyTrapConfig) *Object { handler := r.NewObject() @@ -22,14 +26,14 @@ func (r *Runtime) newNativeProxyHandler(nativeHandler *ProxyTrapConfig) *Object func (r *Runtime) proxyproto_nativehandler_gen_obj_obj(name proxyTrap, native func(*Object) *Object, handler *Object) { if native != nil { - handler.self._putProp(string(name), r.newNativeFunc(func(call FunctionCall) Value { + handler.self._putProp(unistring.String(name), r.newNativeFunc(func(call FunctionCall) Value { if len(call.Arguments) >= 1 { if t, ok := call.Argument(0).(*Object); ok { return native(t) } } panic(r.NewTypeError("%s needs to be called with target as Object", name)) - }, nil, fmt.Sprintf("[native %s]", name), nil, 1), true, true, true) + }, nil, unistring.String(fmt.Sprintf("[native %s]", name)), nil, 1), true, true, true) } } @@ -51,7 +55,7 @@ func (r *Runtime) proxyproto_nativehandler_setPrototypeOf(native func(*Object, * func (r *Runtime) proxyproto_nativehandler_gen_obj_bool(name proxyTrap, native func(*Object) bool, handler *Object) { if native != nil { - handler.self._putProp(string(name), r.newNativeFunc(func(call FunctionCall) Value { + handler.self._putProp(unistring.String(name), r.newNativeFunc(func(call FunctionCall) Value { if len(call.Arguments) >= 1 { if t, ok := call.Argument(0).(*Object); ok { s := native(t) @@ -59,7 +63,7 @@ func (r *Runtime) proxyproto_nativehandler_gen_obj_bool(name proxyTrap, native f } } panic(r.NewTypeError("%s needs to be called with target as Object", name)) - }, nil, fmt.Sprintf("[native %s]", name), nil, 1), true, true, true) + }, nil, unistring.String(fmt.Sprintf("[native %s]", name)), nil, 1), true, true, true) } } @@ -98,7 +102,7 @@ func (r *Runtime) proxyproto_nativehandler_defineProperty(native func(*Object, s func (r *Runtime) proxyproto_nativehandler_gen_obj_string_bool(name proxyTrap, native func(*Object, string) bool, handler *Object) { if native != nil { - handler.self._putProp(string(name), r.newNativeFunc(func(call FunctionCall) Value { + handler.self._putProp(unistring.String(name), r.newNativeFunc(func(call FunctionCall) Value { if len(call.Arguments) >= 2 { if t, ok := call.Argument(0).(*Object); ok { if p, ok := call.Argument(1).(valueString); ok { @@ -108,7 +112,7 @@ func (r *Runtime) proxyproto_nativehandler_gen_obj_string_bool(name proxyTrap, n } } panic(r.NewTypeError("%s needs to be called with target as Object and property as string", name)) - }, nil, fmt.Sprintf("[native %s]", name), nil, 2), true, true, true) + }, nil, unistring.String(fmt.Sprintf("[native %s]", name)), nil, 2), true, true, true) } } diff --git a/builtin_regexp.go b/builtin_regexp.go index 573dc765..e3a54da0 100644 --- a/builtin_regexp.go +++ b/builtin_regexp.go @@ -389,7 +389,7 @@ func (r *Runtime) regexpproto_stdMatcher(call FunctionCall) Value { } else { previousLastIndex = thisIndex } - a = append(a, s.substring(int64(result[0]), int64(result[1]))) + a = append(a, s.substring(result[0], result[1])) } if len(a) == 0 { return _null @@ -447,11 +447,11 @@ func (r *Runtime) regexpproto_stdSplitterGeneric(splitter *Object, s valueString } else { lim = toLength(limit) } - size := s.length() - p := int64(0) if lim == 0 { return r.newArrayValues(a) } + size := s.length() + p := 0 execFn := toMethod(splitter.ToObject(r).self.getStr("exec", nil)) // must be non-nil if size == 0 { @@ -463,21 +463,25 @@ func (r *Runtime) regexpproto_stdSplitterGeneric(splitter *Object, s valueString q := p for q < size { - splitter.self.setOwnStr("lastIndex", intToValue(q), true) + splitter.self.setOwnStr("lastIndex", intToValue(int64(q)), true) z := r.regExpExec(execFn, splitter, s) if z == _null { q++ } else { z := r.toObject(z) e := toLength(splitter.self.getStr("lastIndex", nil)) - if e == p { + if e == int64(p) { q++ } else { a = append(a, s.substring(p, q)) if int64(len(a)) == lim { return r.newArrayValues(a) } - p = e + if e > int64(size) { + p = size + } else { + p = int(e) + } numberOfCaptures := max(toLength(z.self.getStr("length", nil))-1, 0) for i := int64(1); i <= numberOfCaptures; i++ { a = append(a, z.self.getIdx(valueInt(i), nil)) @@ -529,13 +533,13 @@ func (r *Runtime) regexpproto_stdSplitter(call FunctionCall) Value { for _, match := range result { if match[0] == match[1] { // FIXME Ugh, this is a hack - if match[0] == 0 || int64(match[0]) == targetLength { + if match[0] == 0 || match[0] == targetLength { continue } } if lastIndex != match[0] { - valueArray = append(valueArray, s.substring(int64(lastIndex), int64(match[0]))) + valueArray = append(valueArray, s.substring(lastIndex, match[0])) found++ } else if lastIndex == match[0] { if lastIndex != -1 { @@ -554,7 +558,7 @@ func (r *Runtime) regexpproto_stdSplitter(call FunctionCall) Value { offset := index * 2 var value Value if match[offset] != -1 { - value = s.substring(int64(match[offset]), int64(match[offset+1])) + value = s.substring(match[offset], match[offset+1]) } else { value = _undefined } @@ -567,8 +571,8 @@ func (r *Runtime) regexpproto_stdSplitter(call FunctionCall) Value { } if found != limit { - if int64(lastIndex) != targetLength { - valueArray = append(valueArray, s.substring(int64(lastIndex), targetLength)) + if lastIndex != targetLength { + valueArray = append(valueArray, s.substring(lastIndex, targetLength)) } else { valueArray = append(valueArray, stringEmpty) } diff --git a/builtin_string.go b/builtin_string.go index cfb39d37..6373520c 100644 --- a/builtin_string.go +++ b/builtin_string.go @@ -2,13 +2,16 @@ package goja import ( "bytes" + "github.com/dop251/goja/unistring" + "math" + "strings" + "unicode/utf16" + "unicode/utf8" + "github.com/dop251/goja/parser" "golang.org/x/text/collate" "golang.org/x/text/language" "golang.org/x/text/unicode/norm" - "math" - "strings" - "unicode/utf8" ) func (r *Runtime) collator() *collate.Collator { @@ -25,7 +28,7 @@ func toString(arg Value) valueString { return s } if s, ok := arg.(*valueSymbol); ok { - return newStringValue(s.descString()) + return s.desc } return arg.toString() } @@ -57,7 +60,7 @@ func (r *Runtime) _newString(s valueString, proto *Object) *Object { func (r *Runtime) builtin_newString(args []Value, proto *Object) *Object { var s valueString if len(args) > 0 { - s = toString(args[0]) + s = args[0].toString() } else { s = stringEmpty } @@ -99,19 +102,26 @@ func (r *Runtime) stringproto_valueOf(call FunctionCall) Value { return r.stringproto_toStringValueOf(call.This, "valueOf") } +func (r *Runtime) stringproto_iterator(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + return r.createStringIterator(call.This.toString()) +} + func (r *Runtime) string_fromcharcode(call FunctionCall) Value { b := make([]byte, len(call.Arguments)) for i, arg := range call.Arguments { chr := toUint16(arg) if chr >= utf8.RuneSelf { - bb := make([]uint16, len(call.Arguments)) + bb := make([]uint16, len(call.Arguments)+1) + bb[0] = unistring.BOM + bb1 := bb[1:] for j := 0; j < i; j++ { - bb[j] = uint16(b[j]) + bb1[j] = uint16(b[j]) } - bb[i] = chr + bb1[i] = chr i++ for j, arg := range call.Arguments[i:] { - bb[i+j] = toUint16(arg) + bb1[i+j] = toUint16(arg) } return unicodeString(bb) } @@ -121,24 +131,107 @@ func (r *Runtime) string_fromcharcode(call FunctionCall) Value { return asciiString(b) } +func (r *Runtime) string_fromcodepoint(call FunctionCall) Value { + var b []byte + var sb unicodeStringBuilder + unicode := false + for i, arg := range call.Arguments { + num := arg.ToNumber() + var c rune + if numInt, ok := num.(valueInt); ok { + if numInt < 0 || numInt > utf8.MaxRune { + panic(r.newError(r.global.RangeError, "Invalid code point %d", numInt)) + } + c = rune(numInt) + } else { + panic(r.newError(r.global.RangeError, "Invalid code point %s", num)) + } + if c >= utf8.RuneSelf { + if !unicode { + unicode = true + sb.Grow(len(call.Arguments)) + sb.writeASCII(b[:i]) + b = nil + } + } + if unicode { + sb.writeRune(c) + } else { + if b == nil { + b = make([]byte, 0, len(call.Arguments)) + } + b = append(b, byte(c)) + } + } + if !unicode { + return asciiString(b) + } + return sb.string() +} + +func (r *Runtime) string_raw(call FunctionCall) Value { + cooked := call.Argument(0).ToObject(r) + raw := nilSafe(cooked.self.getStr("raw", nil)).ToObject(r) + literalSegments := toLength(raw.self.getStr("length", nil)) + if literalSegments <= 0 { + return stringEmpty + } + var stringElements unicodeStringBuilder + nextIndex := int64(0) + numberOfSubstitutions := int64(len(call.Arguments) - 1) + for { + nextSeg := nilSafe(raw.self.getIdx(valueInt(nextIndex), nil)).toString() + stringElements.writeString(nextSeg) + if nextIndex+1 == literalSegments { + return stringElements.string() + } + if nextIndex < numberOfSubstitutions { + stringElements.writeString(nilSafe(call.Arguments[nextIndex+1]).toString()) + } + nextIndex++ + } +} + func (r *Runtime) stringproto_charAt(call FunctionCall) Value { r.checkObjectCoercible(call.This) s := call.This.toString() pos := call.Argument(0).ToInteger() - if pos < 0 || pos >= s.length() { + if pos < 0 || pos >= int64(s.length()) { return stringEmpty } - return newStringValue(string(s.charAt(pos))) + return newStringValue(string(s.charAt(toInt(pos)))) } func (r *Runtime) stringproto_charCodeAt(call FunctionCall) Value { r.checkObjectCoercible(call.This) s := call.This.toString() pos := call.Argument(0).ToInteger() - if pos < 0 || pos >= s.length() { + if pos < 0 || pos >= int64(s.length()) { return _NaN } - return intToValue(int64(s.charAt(pos) & 0xFFFF)) + return intToValue(int64(s.charAt(toInt(pos)) & 0xFFFF)) +} + +func (r *Runtime) stringproto_codePointAt(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + s := call.This.toString() + p := call.Argument(0).ToInteger() + size := s.length() + if p < 0 || p >= int64(size) { + return _undefined + } + pos := toInt(p) + first := s.charAt(pos) + if isUTF16FirstSurrogate(first) { + pos++ + if pos < size { + second := s.charAt(pos) + if isUTF16SecondSurrogate(second) { + return intToValue(int64(utf16.DecodeRune(first, second))) + } + } + } + return intToValue(int64(first & 0xFFFF)) } func (r *Runtime) stringproto_concat(call FunctionCall) Value { @@ -163,8 +256,9 @@ func (r *Runtime) stringproto_concat(call FunctionCall) Value { } return asciiString(buf.String()) } else { - buf := make([]uint16, totalLen) - pos := int64(0) + buf := make([]uint16, totalLen+1) + buf[0] = unistring.BOM + pos := 1 for _, s := range strs { switch s := s.(type) { case asciiString: @@ -173,7 +267,7 @@ func (r *Runtime) stringproto_concat(call FunctionCall) Value { pos++ } case unicodeString: - copy(buf[pos:], s) + copy(buf[pos:], s[1:]) pos += s.length() } } @@ -181,6 +275,56 @@ func (r *Runtime) stringproto_concat(call FunctionCall) Value { } } +func (r *Runtime) stringproto_endsWith(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + s := call.This.toString() + searchString := call.Argument(0) + if isRegexp(searchString) { + panic(r.NewTypeError("First argument to String.prototype.endsWith must not be a regular expression")) + } + searchStr := searchString.toString() + l := int64(s.length()) + var pos int64 + if posArg := call.Argument(1); posArg != _undefined { + pos = posArg.ToInteger() + } else { + pos = l + } + end := toInt(min(max(pos, 0), l)) + searchLength := searchStr.length() + start := end - searchLength + if start < 0 { + return valueFalse + } + for i := 0; i < searchLength; i++ { + if s.charAt(start+i) != searchStr.charAt(i) { + return valueFalse + } + } + return valueTrue +} + +func (r *Runtime) stringproto_includes(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + s := call.This.toString() + searchString := call.Argument(0) + if isRegexp(searchString) { + panic(r.NewTypeError("First argument to String.prototype.includes must not be a regular expression")) + } + searchStr := searchString.toString() + var pos int64 + if posArg := call.Argument(1); posArg != _undefined { + pos = posArg.ToInteger() + } else { + pos = 0 + } + start := toInt(min(max(pos, 0), int64(s.length()))) + if s.index(searchStr, start) != -1 { + return valueTrue + } + return valueFalse +} + func (r *Runtime) stringproto_indexOf(call FunctionCall) Value { r.checkObjectCoercible(call.This) value := call.This.toString() @@ -190,13 +334,13 @@ func (r *Runtime) stringproto_indexOf(call FunctionCall) Value { if pos < 0 { pos = 0 } else { - l := value.length() + l := int64(value.length()) if pos > l { pos = l } } - return intToValue(value.index(target, pos)) + return intToValue(int64(value.index(target, toInt(pos)))) } func (r *Runtime) stringproto_lastIndexOf(call FunctionCall) Value { @@ -207,20 +351,20 @@ func (r *Runtime) stringproto_lastIndexOf(call FunctionCall) Value { var pos int64 if f, ok := numPos.(valueFloat); ok && math.IsNaN(float64(f)) { - pos = value.length() + pos = int64(value.length()) } else { pos = numPos.ToInteger() if pos < 0 { pos = 0 } else { - l := value.length() + l := int64(value.length()) if pos > l { pos = l } } } - return intToValue(value.lastIndex(target, pos)) + return intToValue(int64(value.lastIndex(target, toInt(pos)))) } func (r *Runtime) stringproto_localeCompare(call FunctionCall) Value { @@ -261,6 +405,173 @@ func (r *Runtime) stringproto_match(call FunctionCall) Value { panic(r.NewTypeError("RegExp matcher is not a function")) } +func (r *Runtime) stringproto_normalize(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + s := call.This.toString() + var form string + if formArg := call.Argument(0); formArg != _undefined { + form = formArg.toString().String() + } else { + form = "NFC" + } + var f norm.Form + switch form { + case "NFC": + f = norm.NFC + case "NFD": + f = norm.NFD + case "NFKC": + f = norm.NFKC + case "NFKD": + f = norm.NFKD + default: + panic(r.newError(r.global.RangeError, "The normalization form should be one of NFC, NFD, NFKC, NFKD")) + } + + if s, ok := s.(unicodeString); ok { + ss := s.String() + return newStringValue(f.String(ss)) + } + + return s +} + +func (r *Runtime) stringproto_padEnd(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + s := call.This.toString() + maxLength := toLength(call.Argument(0)) + stringLength := int64(s.length()) + if maxLength <= stringLength { + return s + } + var filler valueString + var fillerASCII bool + if fillString := call.Argument(1); fillString != _undefined { + filler = fillString.toString() + if filler.length() == 0 { + return s + } + _, fillerASCII = filler.(asciiString) + } else { + filler = asciiString(" ") + fillerASCII = true + } + remaining := toInt(maxLength - stringLength) + _, stringASCII := s.(asciiString) + if fillerASCII && stringASCII { + fl := filler.length() + var sb strings.Builder + sb.Grow(toInt(maxLength)) + sb.WriteString(s.String()) + fs := filler.String() + for remaining >= fl { + sb.WriteString(fs) + remaining -= fl + } + if remaining > 0 { + sb.WriteString(fs[:remaining]) + } + return asciiString(sb.String()) + } + var sb unicodeStringBuilder + sb.Grow(toInt(maxLength)) + sb.writeString(s) + fl := filler.length() + for remaining >= fl { + sb.writeString(filler) + remaining -= fl + } + if remaining > 0 { + sb.writeString(filler.substring(0, remaining)) + } + + return sb.string() +} + +func (r *Runtime) stringproto_padStart(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + s := call.This.toString() + maxLength := toLength(call.Argument(0)) + stringLength := int64(s.length()) + if maxLength <= stringLength { + return s + } + var filler valueString + var fillerASCII bool + if fillString := call.Argument(1); fillString != _undefined { + filler = fillString.toString() + if filler.length() == 0 { + return s + } + _, fillerASCII = filler.(asciiString) + } else { + filler = asciiString(" ") + fillerASCII = true + } + remaining := toInt(maxLength - stringLength) + _, stringASCII := s.(asciiString) + if fillerASCII && stringASCII { + fl := filler.length() + var sb strings.Builder + sb.Grow(toInt(maxLength)) + fs := filler.String() + for remaining >= fl { + sb.WriteString(fs) + remaining -= fl + } + if remaining > 0 { + sb.WriteString(fs[:remaining]) + } + sb.WriteString(s.String()) + return asciiString(sb.String()) + } + var sb unicodeStringBuilder + sb.Grow(toInt(maxLength)) + fl := filler.length() + for remaining >= fl { + sb.writeString(filler) + remaining -= fl + } + if remaining > 0 { + sb.writeString(filler.substring(0, remaining)) + } + sb.writeString(s) + + return sb.string() +} + +func (r *Runtime) stringproto_repeat(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + s := call.This.toString() + n := call.Argument(0).ToNumber() + if n == _positiveInf { + panic(r.newError(r.global.RangeError, "Invalid count value")) + } + numInt := n.ToInteger() + if numInt < 0 { + panic(r.newError(r.global.RangeError, "Invalid count value")) + } + if numInt == 0 || s.length() == 0 { + return stringEmpty + } + num := toInt(numInt) + if s, ok := s.(asciiString); ok { + var sb strings.Builder + sb.Grow(len(s) * num) + for i := 0; i < num; i++ { + sb.WriteString(string(s)) + } + return asciiString(sb.String()) + } + + var sb unicodeStringBuilder + sb.Grow(s.length() * num) + for i := 0; i < num; i++ { + sb.writeString(s) + } + return sb.string() +} + func (r *Runtime) stringproto_replace(call FunctionCall) Value { r.checkObjectCoercible(call.This) searchValue := call.Argument(0) @@ -448,7 +759,7 @@ func (r *Runtime) stringproto_slice(call FunctionCall) Value { r.checkObjectCoercible(call.This) s := call.This.toString() - l := s.length() + l := int64(s.length()) start := call.Argument(0).ToInteger() var end int64 if arg1 := call.Argument(1); arg1 != _undefined { @@ -480,7 +791,7 @@ func (r *Runtime) stringproto_slice(call FunctionCall) Value { } if end > start { - return s.substring(start, end) + return s.substring(int(start), int(end)) } return stringEmpty } @@ -539,11 +850,37 @@ func (r *Runtime) stringproto_split(call FunctionCall) Value { return r.newArrayValues(valueArray) } +func (r *Runtime) stringproto_startsWith(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + s := call.This.toString() + searchString := call.Argument(0) + if isRegexp(searchString) { + panic(r.NewTypeError("First argument to String.prototype.startsWith must not be a regular expression")) + } + searchStr := searchString.toString() + l := int64(s.length()) + var pos int64 + if posArg := call.Argument(1); posArg != _undefined { + pos = posArg.ToInteger() + } + start := toInt(min(max(pos, 0), l)) + searchLength := searchStr.length() + if int64(searchLength+start) > l { + return valueFalse + } + for i := 0; i < searchLength; i++ { + if s.charAt(start+i) != searchStr.charAt(i) { + return valueFalse + } + } + return valueTrue +} + func (r *Runtime) stringproto_substring(call FunctionCall) Value { r.checkObjectCoercible(call.This) s := call.This.toString() - l := s.length() + l := int64(s.length()) intStart := call.Argument(0).ToInteger() var intEnd int64 if end := call.Argument(1); end != _undefined { @@ -567,7 +904,7 @@ func (r *Runtime) stringproto_substring(call FunctionCall) Value { intStart, intEnd = intEnd, intStart } - return s.substring(intStart, intEnd) + return s.substring(int(intStart), int(intEnd)) } func (r *Runtime) stringproto_toLowerCase(call FunctionCall) Value { @@ -591,7 +928,22 @@ func (r *Runtime) stringproto_trim(call FunctionCall) Value { return newStringValue(strings.Trim(s.String(), parser.WhitespaceChars)) } +func (r *Runtime) stringproto_trimEnd(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + s := call.This.toString() + + return newStringValue(strings.TrimRight(s.String(), parser.WhitespaceChars)) +} + +func (r *Runtime) stringproto_trimStart(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + s := call.This.toString() + + return newStringValue(strings.TrimLeft(s.String(), parser.WhitespaceChars)) +} + func (r *Runtime) stringproto_substr(call FunctionCall) Value { + r.checkObjectCoercible(call.This) s := call.This.toString() start := call.Argument(0).ToInteger() var length int64 @@ -611,32 +963,62 @@ func (r *Runtime) stringproto_substr(call FunctionCall) Value { return stringEmpty } - return s.substring(start, start+length) + return s.substring(int(start), int(start+length)) +} + +func (r *Runtime) stringIterProto_next(call FunctionCall) Value { + thisObj := r.toObject(call.This) + if iter, ok := thisObj.self.(*stringIterObject); ok { + return iter.next() + } + panic(r.NewTypeError("Method String Iterator.prototype.next called on incompatible receiver %s", thisObj.String())) +} + +func (r *Runtime) createStringIterProto(val *Object) objectImpl { + o := newBaseObjectObj(val, r.global.IteratorPrototype, classObject) + + o._putProp("next", r.newNativeFunc(r.stringIterProto_next, nil, "next", nil, 0), true, false, true) + o._putSym(symToStringTag, valueProp(asciiString(classStringIterator), false, false, true)) + + return o } func (r *Runtime) initString() { + r.global.StringIteratorPrototype = r.newLazyObject(r.createStringIterProto) r.global.StringPrototype = r.builtin_newString([]Value{stringEmpty}, r.global.ObjectPrototype) o := r.global.StringPrototype.self - o._putProp("toString", r.newNativeFunc(r.stringproto_toString, nil, "toString", nil, 0), true, false, true) - o._putProp("valueOf", r.newNativeFunc(r.stringproto_valueOf, nil, "valueOf", nil, 0), true, false, true) o._putProp("charAt", r.newNativeFunc(r.stringproto_charAt, nil, "charAt", nil, 1), true, false, true) o._putProp("charCodeAt", r.newNativeFunc(r.stringproto_charCodeAt, nil, "charCodeAt", nil, 1), true, false, true) + o._putProp("codePointAt", r.newNativeFunc(r.stringproto_codePointAt, nil, "codePointAt", nil, 1), true, false, true) o._putProp("concat", r.newNativeFunc(r.stringproto_concat, nil, "concat", nil, 1), true, false, true) + o._putProp("endsWith", r.newNativeFunc(r.stringproto_endsWith, nil, "endsWith", nil, 1), true, false, true) + o._putProp("includes", r.newNativeFunc(r.stringproto_includes, nil, "includes", nil, 1), true, false, true) o._putProp("indexOf", r.newNativeFunc(r.stringproto_indexOf, nil, "indexOf", nil, 1), true, false, true) o._putProp("lastIndexOf", r.newNativeFunc(r.stringproto_lastIndexOf, nil, "lastIndexOf", nil, 1), true, false, true) o._putProp("localeCompare", r.newNativeFunc(r.stringproto_localeCompare, nil, "localeCompare", nil, 1), true, false, true) o._putProp("match", r.newNativeFunc(r.stringproto_match, nil, "match", nil, 1), true, false, true) + o._putProp("normalize", r.newNativeFunc(r.stringproto_normalize, nil, "normalize", nil, 0), true, false, true) + o._putProp("padEnd", r.newNativeFunc(r.stringproto_padEnd, nil, "padEnd", nil, 1), true, false, true) + o._putProp("padStart", r.newNativeFunc(r.stringproto_padStart, nil, "padStart", nil, 1), true, false, true) + o._putProp("repeat", r.newNativeFunc(r.stringproto_repeat, nil, "repeat", nil, 1), true, false, true) o._putProp("replace", r.newNativeFunc(r.stringproto_replace, nil, "replace", nil, 2), true, false, true) o._putProp("search", r.newNativeFunc(r.stringproto_search, nil, "search", nil, 1), true, false, true) o._putProp("slice", r.newNativeFunc(r.stringproto_slice, nil, "slice", nil, 2), true, false, true) o._putProp("split", r.newNativeFunc(r.stringproto_split, nil, "split", nil, 2), true, false, true) + o._putProp("startsWith", r.newNativeFunc(r.stringproto_startsWith, nil, "startsWith", nil, 1), true, false, true) o._putProp("substring", r.newNativeFunc(r.stringproto_substring, nil, "substring", nil, 2), true, false, true) - o._putProp("toLowerCase", r.newNativeFunc(r.stringproto_toLowerCase, nil, "toLowerCase", nil, 0), true, false, true) o._putProp("toLocaleLowerCase", r.newNativeFunc(r.stringproto_toLowerCase, nil, "toLocaleLowerCase", nil, 0), true, false, true) - o._putProp("toUpperCase", r.newNativeFunc(r.stringproto_toUpperCase, nil, "toUpperCase", nil, 0), true, false, true) o._putProp("toLocaleUpperCase", r.newNativeFunc(r.stringproto_toUpperCase, nil, "toLocaleUpperCase", nil, 0), true, false, true) + o._putProp("toLowerCase", r.newNativeFunc(r.stringproto_toLowerCase, nil, "toLowerCase", nil, 0), true, false, true) + o._putProp("toString", r.newNativeFunc(r.stringproto_toString, nil, "toString", nil, 0), true, false, true) + o._putProp("toUpperCase", r.newNativeFunc(r.stringproto_toUpperCase, nil, "toUpperCase", nil, 0), true, false, true) o._putProp("trim", r.newNativeFunc(r.stringproto_trim, nil, "trim", nil, 0), true, false, true) + o._putProp("trimEnd", r.newNativeFunc(r.stringproto_trimEnd, nil, "trimEnd", nil, 0), true, false, true) + o._putProp("trimStart", r.newNativeFunc(r.stringproto_trimStart, nil, "trimStart", nil, 0), true, false, true) + o._putProp("valueOf", r.newNativeFunc(r.stringproto_valueOf, nil, "valueOf", nil, 0), true, false, true) + + o._putSym(symIterator, valueProp(r.newNativeFunc(r.stringproto_iterator, nil, "[Symbol.iterator]", nil, 0), true, false, true)) // Annex B o._putProp("substr", r.newNativeFunc(r.stringproto_substr, nil, "substr", nil, 2), true, false, true) @@ -644,6 +1026,8 @@ func (r *Runtime) initString() { r.global.String = r.newNativeFunc(r.builtin_String, r.builtin_newString, "String", r.global.StringPrototype, 1) o = r.global.String.self o._putProp("fromCharCode", r.newNativeFunc(r.string_fromcharcode, nil, "fromCharCode", nil, 1), true, false, true) + o._putProp("fromCodePoint", r.newNativeFunc(r.string_fromcodepoint, nil, "fromCodePoint", nil, 1), true, false, true) + o._putProp("raw", r.newNativeFunc(r.string_raw, nil, "raw", nil, 1), true, false, true) r.addToGlobal("String", r.global.String) diff --git a/builtin_string_test.go b/builtin_string_test.go index 8f538ee0..dbbb71c7 100644 --- a/builtin_string_test.go +++ b/builtin_string_test.go @@ -145,3 +145,25 @@ res.length === 3 && res[0] === "a" && res[1] === "b" && res[2] === "c"; ` testScript1(SCRIPT, valueTrue, t) } + +func TestStringIterSurrPair(t *testing.T) { + const SCRIPT = ` +var lo = '\uD834'; +var hi = '\uDF06'; +var pair = lo + hi; +var string = 'a' + pair + 'b' + lo + pair + hi + lo; +var iterator = string[Symbol.iterator](); +var result; + +result = iterator.next(); +if (result.value !== 'a') { + throw new Error("at 0: " + result.value); +} +result = iterator.next(); +if (result.value !== pair) { + throw new Error("at 1: " + result.value); +} + +` + testScript1(SCRIPT, _undefined, t) +} diff --git a/builtin_symbol.go b/builtin_symbol.go index 83346e14..1e90a824 100644 --- a/builtin_symbol.go +++ b/builtin_symbol.go @@ -1,27 +1,29 @@ package goja +import "github.com/dop251/goja/unistring" + var ( - symHasInstance = &valueSymbol{desc: "Symbol.hasInstance"} - symIsConcatSpreadable = &valueSymbol{desc: "Symbol.isConcatSpreadable"} - symIterator = &valueSymbol{desc: "Symbol.iterator"} - symMatch = &valueSymbol{desc: "Symbol.match"} - symReplace = &valueSymbol{desc: "Symbol.replace"} - symSearch = &valueSymbol{desc: "Symbol.search"} - symSpecies = &valueSymbol{desc: "Symbol.species"} - symSplit = &valueSymbol{desc: "Symbol.split"} - symToPrimitive = &valueSymbol{desc: "Symbol.toPrimitive"} - symToStringTag = &valueSymbol{desc: "Symbol.toStringTag"} - symUnscopables = &valueSymbol{desc: "Symbol.unscopables"} + symHasInstance = newSymbol(asciiString("Symbol.hasInstance")) + symIsConcatSpreadable = newSymbol(asciiString("Symbol.isConcatSpreadable")) + symIterator = newSymbol(asciiString("Symbol.iterator")) + symMatch = newSymbol(asciiString("Symbol.match")) + symReplace = newSymbol(asciiString("Symbol.replace")) + symSearch = newSymbol(asciiString("Symbol.search")) + symSpecies = newSymbol(asciiString("Symbol.species")) + symSplit = newSymbol(asciiString("Symbol.split")) + symToPrimitive = newSymbol(asciiString("Symbol.toPrimitive")) + symToStringTag = newSymbol(asciiString("Symbol.toStringTag")) + symUnscopables = newSymbol(asciiString("Symbol.unscopables")) ) func (r *Runtime) builtin_symbol(call FunctionCall) Value { - desc := "" + var desc valueString if arg := call.Argument(0); !IsUndefined(arg) { - desc = arg.toString().String() - } - return &valueSymbol{ - desc: desc, + desc = arg.toString() + } else { + desc = stringEmpty } + return newSymbol(desc) } func (r *Runtime) symbolproto_tostring(call FunctionCall) Value { @@ -38,7 +40,7 @@ func (r *Runtime) symbolproto_tostring(call FunctionCall) Value { if sym == nil { panic(r.NewTypeError("Method Symbol.prototype.toString is called on incompatible receiver")) } - return newStringValue(sym.descString()) + return sym.desc } func (r *Runtime) symbolproto_valueOf(call FunctionCall) Value { @@ -59,17 +61,16 @@ func (r *Runtime) symbolproto_valueOf(call FunctionCall) Value { } func (r *Runtime) symbol_for(call FunctionCall) Value { - key := call.Argument(0).toString().String() - if v := r.symbolRegistry[key]; v != nil { + key := call.Argument(0).toString() + keyStr := key.string() + if v := r.symbolRegistry[keyStr]; v != nil { return v } if r.symbolRegistry == nil { - r.symbolRegistry = make(map[string]*valueSymbol) - } - v := &valueSymbol{ - desc: key, + r.symbolRegistry = make(map[unistring.String]*valueSymbol) } - r.symbolRegistry[key] = v + v := newSymbol(key) + r.symbolRegistry[keyStr] = v return v } @@ -81,7 +82,7 @@ func (r *Runtime) symbol_keyfor(call FunctionCall) Value { } for key, s := range r.symbolRegistry { if s == sym { - return r.ToValue(key) + return stringValueFromRaw(key) } } return _undefined @@ -124,7 +125,9 @@ func (r *Runtime) createSymbol(val *Object) objectImpl { symToStringTag, symUnscopables, } { - o._putProp(s.desc[len("Symbol."):], s, false, false, false) + n := s.desc.(asciiString) + n = n[len("Symbol(Symbol.") : len(n)-1] + o._putProp(unistring.String(n), s, false, false, false) } return o diff --git a/builtin_typedarrays.go b/builtin_typedarrays.go index 2fb0985f..00f54883 100644 --- a/builtin_typedarrays.go +++ b/builtin_typedarrays.go @@ -5,6 +5,8 @@ import ( "sort" "strings" "unsafe" + + "github.com/dop251/goja/unistring" ) type typedArraySortCtx struct { @@ -1377,7 +1379,7 @@ func (r *Runtime) addPrototype(ctor *Object, proto *Object) *baseObject { return p } -func (r *Runtime) typedArrayCreator(ctor func(args []Value, newTarget *Object) *Object, name string, bytesPerElement int) func(val *Object) objectImpl { +func (r *Runtime) typedArrayCreator(ctor func(args []Value, newTarget *Object) *Object, name unistring.String, bytesPerElement int) func(val *Object) objectImpl { return func(val *Object) objectImpl { o := r.newNativeConstructOnly(val, ctor, nil, name, 3) o.prototype = r.global.TypedArray diff --git a/compiler.go b/compiler.go index 385ac0dc..c8218918 100644 --- a/compiler.go +++ b/compiler.go @@ -2,10 +2,12 @@ package goja import ( "fmt" - "github.com/dop251/goja/ast" - "github.com/dop251/goja/file" "sort" "strconv" + + "github.com/dop251/goja/ast" + "github.com/dop251/goja/file" + "github.com/dop251/goja/unistring" ) const ( @@ -39,7 +41,7 @@ type Program struct { code []instruction values []Value - funcName string + funcName unistring.String src *SrcFile srcMap []srcMapItem } @@ -56,7 +58,7 @@ type compiler struct { } type scope struct { - names map[string]uint32 + names map[unistring.String]uint32 outer *scope strict bool eval bool @@ -66,13 +68,13 @@ type scope struct { argsNeeded bool thisNeeded bool - namesMap map[string]string + namesMap map[unistring.String]unistring.String lastFreeTmp int } type block struct { typ int - label string + label unistring.String needResult bool cont int breaks []int @@ -111,9 +113,9 @@ func (c *compiler) newScope() { } c.scope = &scope{ outer: c.scope, - names: make(map[string]uint32), + names: make(map[unistring.String]uint32), strict: strict, - namesMap: make(map[string]string), + namesMap: make(map[unistring.String]unistring.String), } } @@ -176,7 +178,7 @@ func (s *scope) isFunction() bool { return s.outer.isFunction() } -func (s *scope) lookupName(name string) (idx uint32, found, noDynamics bool) { +func (s *scope) lookupName(name unistring.String) (idx uint32, found, noDynamics bool) { var level uint32 = 0 noDynamics = true for curScope := s; curScope != nil; curScope = curScope.outer { @@ -186,7 +188,7 @@ func (s *scope) lookupName(name string) (idx uint32, found, noDynamics bool) { if curScope.dynamic { noDynamics = false } else { - var mapped string + var mapped unistring.String if m, exists := curScope.namesMap[name]; exists { mapped = m } else { @@ -210,7 +212,7 @@ func (s *scope) lookupName(name string) (idx uint32, found, noDynamics bool) { return } -func (s *scope) bindName(name string) (uint32, bool) { +func (s *scope) bindName(name unistring.String) (uint32, bool) { if s.lexical { return s.outer.bindName(name) } @@ -223,7 +225,7 @@ func (s *scope) bindName(name string) (uint32, bool) { return idx, true } -func (s *scope) bindNameShadow(name string) (uint32, bool) { +func (s *scope) bindNameShadow(name unistring.String) (uint32, bool) { if s.lexical { return s.outer.bindName(name) } @@ -234,7 +236,7 @@ func (s *scope) bindNameShadow(name string) (uint32, bool) { unique = false // shadow the var delete(s.names, name) - n := strconv.Itoa(int(idx)) + n := unistring.String(strconv.Itoa(int(idx))) s.names[n] = idx } idx := uint32(len(s.names)) @@ -446,14 +448,14 @@ func (c *compiler) isStrictStatement(s ast.Statement) bool { return false } -func (c *compiler) checkIdentifierName(name string, offset int) { +func (c *compiler) checkIdentifierName(name unistring.String, offset int) { switch name { case "implements", "interface", "let", "package", "private", "protected", "public", "static", "yield": c.throwSyntaxError(offset, "Unexpected strict mode reserved word") } } -func (c *compiler) checkIdentifierLName(name string, offset int) { +func (c *compiler) checkIdentifierLName(name unistring.String, offset int) { switch name { case "eval", "arguments": c.throwSyntaxError(offset, "Assignment to eval or arguments is not allowed in strict mode") diff --git a/compiler_expr.go b/compiler_expr.go index 581208e8..2df63f28 100644 --- a/compiler_expr.go +++ b/compiler_expr.go @@ -2,10 +2,12 @@ package goja import ( "fmt" + "regexp" + "github.com/dop251/goja/ast" "github.com/dop251/goja/file" "github.com/dop251/goja/token" - "regexp" + "github.com/dop251/goja/unistring" ) var ( @@ -60,18 +62,18 @@ type compiledAssignExpr struct { type deleteGlobalExpr struct { baseCompiledExpr - name string + name unistring.String } type deleteVarExpr struct { baseCompiledExpr - name string + name unistring.String } type deletePropExpr struct { baseCompiledExpr left compiledExpr - name string + name unistring.String } type deleteElemExpr struct { @@ -91,7 +93,7 @@ type baseCompiledExpr struct { type compiledIdentifierExpr struct { baseCompiledExpr - name string + name unistring.String } type compiledFunctionLiteral struct { @@ -154,7 +156,7 @@ type compiledBinaryExpr struct { type compiledVariableExpr struct { baseCompiledExpr - name string + name unistring.String initializer compiledExpr expr *ast.VariableExpression } @@ -320,7 +322,7 @@ func (e *compiledIdentifierExpr) emitGetterOrRef() { } } -func (c *compiler) emitVarSetter1(name string, offset int, emitRight func(isRef bool)) { +func (c *compiler) emitVarSetter1(name unistring.String, offset int, emitRight func(isRef bool)) { if c.scope.strict { c.checkIdentifierLName(name, offset) } @@ -353,7 +355,7 @@ func (c *compiler) emitVarSetter1(name string, offset int, emitRight func(isRef } } -func (c *compiler) emitVarSetter(name string, offset int, valueExpr compiledExpr) { +func (c *compiler) emitVarSetter(name unistring.String, offset int, valueExpr compiledExpr) { c.emitVarSetter1(name, offset, func(bool) { c.emitExpr(valueExpr, true) }) @@ -432,7 +434,7 @@ func (e *compiledIdentifierExpr) deleteExpr() compiledExpr { type compiledDotExpr struct { baseCompiledExpr left compiledExpr - name string + name unistring.String } func (e *compiledDotExpr) emitGetter(putOnStack bool) { @@ -854,7 +856,7 @@ func (e *compiledFunctionLiteral) emitGetter(putOnStack bool) { e.c.popScope() e.c.p = savedPrg e.c.blockStart = savedBlockStart - name := "" + var name unistring.String if e.expr.Name != nil { name = e.expr.Name.Name } @@ -1444,7 +1446,7 @@ func (c *compiler) compileRegexpLiteral(v *ast.RegExpLiteral) compiledExpr { } func (e *compiledCallExpr) emitGetter(putOnStack bool) { - var calleeName string + var calleeName unistring.String switch callee := e.callee.(type) { case *compiledDotExpr: callee.left.emitGetter(true) @@ -1549,7 +1551,7 @@ func (c *compiler) compileNumberLiteral(v *ast.NumberLiteral) compiledExpr { func (c *compiler) compileStringLiteral(v *ast.StringLiteral) compiledExpr { r := &compiledLiteral{ - val: newStringValue(v.Value), + val: stringValueFromRaw(v.Value), } r.init(c, v.Idx0()) return r diff --git a/compiler_stmt.go b/compiler_stmt.go index 099920d7..d869921d 100644 --- a/compiler_stmt.go +++ b/compiler_stmt.go @@ -2,10 +2,12 @@ package goja import ( "fmt" + "strconv" + "github.com/dop251/goja/ast" "github.com/dop251/goja/file" "github.com/dop251/goja/token" - "strconv" + "github.com/dop251/goja/unistring" ) func (c *compiler) compileStatement(v ast.Statement, needResult bool) { @@ -126,7 +128,7 @@ func (c *compiler) compileTryStatement(v *ast.TryStatement) { // remap newIdx, exists := m[idx] if !exists { - exname := " __tmp" + strconv.Itoa(c.scope.lastFreeTmp) + exname := unistring.String(" __tmp" + strconv.Itoa(c.scope.lastFreeTmp)) c.scope.lastFreeTmp++ newIdx, _ = c.scope.bindName(exname) m[idx] = newIdx @@ -207,7 +209,7 @@ func (c *compiler) compileDoWhileStatement(v *ast.DoWhileStatement, needResult b c.compileLabeledDoWhileStatement(v, needResult, "") } -func (c *compiler) compileLabeledDoWhileStatement(v *ast.DoWhileStatement, needResult bool, label string) { +func (c *compiler) compileLabeledDoWhileStatement(v *ast.DoWhileStatement, needResult bool, label unistring.String) { c.block = &block{ typ: blockLoop, outer: c.block, @@ -234,7 +236,7 @@ func (c *compiler) compileForStatement(v *ast.ForStatement, needResult bool) { c.compileLabeledForStatement(v, needResult, "") } -func (c *compiler) compileLabeledForStatement(v *ast.ForStatement, needResult bool, label string) { +func (c *compiler) compileLabeledForStatement(v *ast.ForStatement, needResult bool, label unistring.String) { c.block = &block{ typ: blockLoop, outer: c.block, @@ -306,7 +308,7 @@ func (c *compiler) compileForInStatement(v *ast.ForInStatement, needResult bool) c.compileLabeledForInStatement(v, needResult, "") } -func (c *compiler) compileLabeledForInStatement(v *ast.ForInStatement, needResult bool, label string) { +func (c *compiler) compileLabeledForInStatement(v *ast.ForInStatement, needResult bool, label unistring.String) { c.block = &block{ typ: blockLoop, outer: c.block, @@ -341,7 +343,7 @@ func (c *compiler) compileForOfStatement(v *ast.ForOfStatement, needResult bool) c.compileLabeledForOfStatement(v, needResult, "") } -func (c *compiler) compileLabeledForOfStatement(v *ast.ForOfStatement, needResult bool, label string) { +func (c *compiler) compileLabeledForOfStatement(v *ast.ForOfStatement, needResult bool, label unistring.String) { c.block = &block{ typ: blockLoop, outer: c.block, @@ -377,7 +379,7 @@ func (c *compiler) compileWhileStatement(v *ast.WhileStatement, needResult bool) c.compileLabeledWhileStatement(v, needResult, "") } -func (c *compiler) compileLabeledWhileStatement(v *ast.WhileStatement, needResult bool, label string) { +func (c *compiler) compileLabeledWhileStatement(v *ast.WhileStatement, needResult bool, label unistring.String) { c.block = &block{ typ: blockLoop, outer: c.block, @@ -517,6 +519,10 @@ func (c *compiler) compileBreak(label *ast.Identifier, idx file.Idx) { break } } + if block == nil { + c.throwSyntaxError(int(idx)-1, "Undefined label '%s'", label.Name) + return + } } else { // find the nearest loop or switch L: @@ -531,17 +537,17 @@ func (c *compiler) compileBreak(label *ast.Identifier, idx file.Idx) { break L } } + if block == nil { + c.throwSyntaxError(int(idx)-1, "Could not find block") + return + } } - if block != nil { - if len(c.p.code) == c.blockStart && block.needResult { - c.emit(loadUndef) - } - block.breaks = append(block.breaks, len(c.p.code)) - c.emit(nil) - } else { - c.throwSyntaxError(int(idx)-1, "Undefined label '%s'", label.Name) + if len(c.p.code) == c.blockStart && block.needResult { + c.emit(loadUndef) } + block.breaks = append(block.breaks, len(c.p.code)) + c.emit(nil) } func (c *compiler) compileContinue(label *ast.Identifier, idx file.Idx) { @@ -555,6 +561,10 @@ func (c *compiler) compileContinue(label *ast.Identifier, idx file.Idx) { break } } + if block == nil { + c.throwSyntaxError(int(idx)-1, "Undefined label '%s'", label.Name) + return + } } else { // find the nearest loop for b := c.block; b != nil; b = b.outer { @@ -565,17 +575,17 @@ func (c *compiler) compileContinue(label *ast.Identifier, idx file.Idx) { break } } + if block == nil { + c.throwSyntaxError(int(idx)-1, "Could not find block") + return + } } - if block != nil { - if len(c.p.code) == c.blockStart && block.needResult { - c.emit(loadUndef) - } - block.conts = append(block.conts, len(c.p.code)) - c.emit(nil) - } else { - c.throwSyntaxError(int(idx)-1, "Undefined label '%s'", label.Name) + if len(c.p.code) == c.blockStart && block.needResult { + c.emit(loadUndef) } + block.conts = append(block.conts, len(c.p.code)) + c.emit(nil) } func (c *compiler) compileIfStatement(v *ast.IfStatement, needResult bool) { @@ -720,7 +730,7 @@ func (c *compiler) compileStatements(list []ast.Statement, needResult bool) { } } -func (c *compiler) compileGenericLabeledStatement(v ast.Statement, needResult bool, label string) { +func (c *compiler) compileGenericLabeledStatement(v ast.Statement, needResult bool, label unistring.String) { c.block = &block{ typ: blockBranch, outer: c.block, diff --git a/func.go b/func.go index 138afc65..10b6bbe4 100644 --- a/func.go +++ b/func.go @@ -1,6 +1,10 @@ package goja -import "reflect" +import ( + "reflect" + + "github.com/dop251/goja/unistring" +) type baseFuncObject struct { baseObject @@ -36,20 +40,20 @@ func (f *nativeFuncObject) exportType() reflect.Type { return reflect.TypeOf(f.f) } -func (f *funcObject) _addProto(n string) Value { +func (f *funcObject) _addProto(n unistring.String) Value { if n == "prototype" { - if _, exists := f.values["prototype"]; !exists { + if _, exists := f.values[n]; !exists { return f.addPrototype() } } return nil } -func (f *funcObject) getStr(p string, receiver Value) Value { +func (f *funcObject) getStr(p unistring.String, receiver Value) Value { return f.getStrWithOwnProp(f.getOwnPropStr(p), p, receiver) } -func (f *funcObject) getOwnPropStr(name string) Value { +func (f *funcObject) getOwnPropStr(name unistring.String) Value { if v := f._addProto(name); v != nil { return v } @@ -57,16 +61,16 @@ func (f *funcObject) getOwnPropStr(name string) Value { return f.baseObject.getOwnPropStr(name) } -func (f *funcObject) setOwnStr(name string, val Value, throw bool) bool { +func (f *funcObject) setOwnStr(name unistring.String, val Value, throw bool) bool { f._addProto(name) return f.baseObject.setOwnStr(name, val, throw) } -func (f *funcObject) setForeignStr(name string, val, receiver Value, throw bool) (bool, bool) { +func (f *funcObject) setForeignStr(name unistring.String, val, receiver Value, throw bool) (bool, bool) { return f._setForeignStr(name, f.getOwnPropStr(name), val, receiver, throw) } -func (f *funcObject) deleteStr(name string, throw bool) bool { +func (f *funcObject) deleteStr(name unistring.String, throw bool) bool { f._addProto(name) return f.baseObject.deleteStr(name, throw) } @@ -77,7 +81,7 @@ func (f *funcObject) addPrototype() Value { return f._putProp("prototype", proto, true, false, false) } -func (f *funcObject) hasOwnPropertyStr(name string) bool { +func (f *funcObject) hasOwnPropertyStr(name unistring.String) bool { if r := f.baseObject.hasOwnPropertyStr(name); r { return true } @@ -176,12 +180,12 @@ func (f *funcObject) assertConstructor() func(args []Value, newTarget *Object) * return f.construct } -func (f *baseFuncObject) init(name string, length int) { +func (f *baseFuncObject) init(name unistring.String, length int) { f.baseObject.init() if name != "" { f.nameProp.configurable = true - f.nameProp.value = newStringValue(name) + f.nameProp.value = stringValueFromRaw(name) f._put("name", &f.nameProp) } @@ -242,11 +246,11 @@ func (f *nativeFuncObject) assertConstructor() func(args []Value, newTarget *Obj return f.construct } -func (f *boundFuncObject) getStr(p string, receiver Value) Value { +func (f *boundFuncObject) getStr(p unistring.String, receiver Value) Value { return f.getStrWithOwnProp(f.getOwnPropStr(p), p, receiver) } -func (f *boundFuncObject) getOwnPropStr(name string) Value { +func (f *boundFuncObject) getOwnPropStr(name unistring.String) Value { if name == "caller" || name == "arguments" { return f.val.runtime.global.throwerProperty } @@ -254,21 +258,21 @@ func (f *boundFuncObject) getOwnPropStr(name string) Value { return f.nativeFuncObject.getOwnPropStr(name) } -func (f *boundFuncObject) deleteStr(name string, throw bool) bool { +func (f *boundFuncObject) deleteStr(name unistring.String, throw bool) bool { if name == "caller" || name == "arguments" { return true } return f.nativeFuncObject.deleteStr(name, throw) } -func (f *boundFuncObject) setOwnStr(name string, val Value, throw bool) bool { +func (f *boundFuncObject) setOwnStr(name unistring.String, val Value, throw bool) bool { if name == "caller" || name == "arguments" { panic(f.val.runtime.NewTypeError("'caller' and 'arguments' are restricted function properties and cannot be accessed in this context.")) } return f.nativeFuncObject.setOwnStr(name, val, throw) } -func (f *boundFuncObject) setForeignStr(name string, val, receiver Value, throw bool) (bool, bool) { +func (f *boundFuncObject) setForeignStr(name unistring.String, val, receiver Value, throw bool) (bool, bool) { return f._setForeignStr(name, f.getOwnPropStr(name), val, receiver, throw) } diff --git a/map.go b/map.go index 907206e7..b092b0d0 100644 --- a/map.go +++ b/map.go @@ -1,6 +1,8 @@ package goja -import "hash" +import ( + "hash/maphash" +) type mapEntry struct { key, value Value @@ -10,7 +12,7 @@ type mapEntry struct { } type orderedMap struct { - hash hash.Hash64 + hash *maphash.Hash hashTable map[uint64]*mapEntry iterFirst, iterLast *mapEntry size int @@ -138,7 +140,7 @@ func (iter *orderedMapIter) close() { iter.cur = nil } -func newOrderedMap(h hash.Hash64) *orderedMap { +func newOrderedMap(h *maphash.Hash) *orderedMap { return &orderedMap{ hash: h, hashTable: make(map[uint64]*mapEntry), diff --git a/object.go b/object.go index be5bac5b..898f6860 100644 --- a/object.go +++ b/object.go @@ -7,6 +7,8 @@ import ( "runtime" "sort" "unsafe" + + "github.com/dop251/goja/unistring" ) const ( @@ -24,9 +26,10 @@ const ( classRegExp = "RegExp" classDate = "Date" - classArrayIterator = "Array Iterator" - classMapIterator = "Map Iterator" - classSetIterator = "Set Iterator" + classArrayIterator = "Array Iterator" + classMapIterator = "Map Iterator" + classSetIterator = "Set Iterator" + classStringIterator = "String Iterator" ) type weakCollection interface { @@ -171,35 +174,35 @@ func (p *PropertyDescriptor) complete() { type objectImpl interface { sortable className() string - getStr(p string, receiver Value) Value + getStr(p unistring.String, receiver Value) Value getIdx(p valueInt, receiver Value) Value getSym(p *valueSymbol, receiver Value) Value - getOwnPropStr(string) Value + getOwnPropStr(unistring.String) Value getOwnPropIdx(valueInt) Value getOwnPropSym(*valueSymbol) Value - setOwnStr(p string, v Value, throw bool) bool + setOwnStr(p unistring.String, v Value, throw bool) bool setOwnIdx(p valueInt, v Value, throw bool) bool setOwnSym(p *valueSymbol, v Value, throw bool) bool - setForeignStr(p string, v, receiver Value, throw bool) (res bool, handled bool) + setForeignStr(p unistring.String, v, receiver Value, throw bool) (res bool, handled bool) setForeignIdx(p valueInt, v, receiver Value, throw bool) (res bool, handled bool) setForeignSym(p *valueSymbol, v, receiver Value, throw bool) (res bool, handled bool) - hasPropertyStr(string) bool + hasPropertyStr(unistring.String) bool hasPropertyIdx(idx valueInt) bool hasPropertySym(s *valueSymbol) bool - hasOwnPropertyStr(string) bool + hasOwnPropertyStr(unistring.String) bool hasOwnPropertyIdx(valueInt) bool hasOwnPropertySym(s *valueSymbol) bool - defineOwnPropertyStr(name string, desc PropertyDescriptor, throw bool) bool + defineOwnPropertyStr(name unistring.String, desc PropertyDescriptor, throw bool) bool defineOwnPropertyIdx(name valueInt, desc PropertyDescriptor, throw bool) bool defineOwnPropertySym(name *valueSymbol, desc PropertyDescriptor, throw bool) bool - deleteStr(name string, throw bool) bool + deleteStr(name unistring.String, throw bool) bool deleteIdx(idx valueInt, throw bool) bool deleteSym(s *valueSymbol, throw bool) bool @@ -219,10 +222,10 @@ type objectImpl interface { exportType() reflect.Type equal(objectImpl) bool ownKeys(all bool, accum []Value) []Value - ownSymbols() []Value + ownSymbols(all bool, accum []Value) []Value ownPropertyKeys(all bool, accum []Value) []Value - _putProp(name string, value Value, writable, enumerable, configurable bool) Value + _putProp(name unistring.String, value Value, writable, enumerable, configurable bool) Value _putSym(s *valueSymbol, prop Value) } @@ -232,8 +235,8 @@ type baseObject struct { prototype *Object extensible bool - values map[string]Value - propNames []string + values map[unistring.String]Value + propNames []unistring.String lastSortedPropLen, idxPropCount int @@ -278,14 +281,14 @@ func (f ConstructorCall) Argument(idx int) Value { } func (o *baseObject) init() { - o.values = make(map[string]Value) + o.values = make(map[unistring.String]Value) } func (o *baseObject) className() string { return o.class } -func (o *baseObject) hasPropertyStr(name string) bool { +func (o *baseObject) hasPropertyStr(name unistring.String) bool { if o.val.self.hasOwnPropertyStr(name) { return true } @@ -296,7 +299,7 @@ func (o *baseObject) hasPropertyStr(name string) bool { } func (o *baseObject) hasPropertyIdx(idx valueInt) bool { - return o.val.self.hasPropertyStr(idx.String()) + return o.val.self.hasPropertyStr(idx.string()) } func (o *baseObject) hasPropertySym(s *valueSymbol) bool { @@ -325,7 +328,7 @@ func (o *baseObject) getWithOwnProp(prop, p, receiver Value) Value { return prop } -func (o *baseObject) getStrWithOwnProp(prop Value, name string, receiver Value) Value { +func (o *baseObject) getStrWithOwnProp(prop Value, name unistring.String, receiver Value) Value { if prop == nil && o.prototype != nil { if receiver == nil { return o.prototype.self.getStr(name, o.val) @@ -342,14 +345,14 @@ func (o *baseObject) getStrWithOwnProp(prop Value, name string, receiver Value) } func (o *baseObject) getIdx(idx valueInt, receiver Value) Value { - return o.val.self.getStr(idx.String(), receiver) + return o.val.self.getStr(idx.string(), receiver) } func (o *baseObject) getSym(s *valueSymbol, receiver Value) Value { return o.getWithOwnProp(o.getOwnPropSym(s), s, receiver) } -func (o *baseObject) getStr(name string, receiver Value) Value { +func (o *baseObject) getStr(name unistring.String, receiver Value) Value { prop := o.values[name] if prop == nil { if o.prototype != nil { @@ -369,7 +372,7 @@ func (o *baseObject) getStr(name string, receiver Value) Value { } func (o *baseObject) getOwnPropIdx(idx valueInt) Value { - return o.val.self.getOwnPropStr(idx.String()) + return o.val.self.getOwnPropStr(idx.string()) } func (o *baseObject) getOwnPropSym(s *valueSymbol) Value { @@ -379,11 +382,11 @@ func (o *baseObject) getOwnPropSym(s *valueSymbol) Value { return nil } -func (o *baseObject) getOwnPropStr(name string) Value { +func (o *baseObject) getOwnPropStr(name unistring.String) Value { return o.values[name] } -func (o *baseObject) checkDeleteProp(name string, prop *valueProperty, throw bool) bool { +func (o *baseObject) checkDeleteProp(name unistring.String, prop *valueProperty, throw bool) bool { if !prop.configurable { o.val.runtime.typeErrorResult(throw, "Cannot delete property '%s' of %s", name, o.val.toString()) return false @@ -391,14 +394,14 @@ func (o *baseObject) checkDeleteProp(name string, prop *valueProperty, throw boo return true } -func (o *baseObject) checkDelete(name string, val Value, throw bool) bool { +func (o *baseObject) checkDelete(name unistring.String, val Value, throw bool) bool { if val, ok := val.(*valueProperty); ok { return o.checkDeleteProp(name, val, throw) } return true } -func (o *baseObject) _delete(name string) { +func (o *baseObject) _delete(name unistring.String) { delete(o.values, name) for i, n := range o.propNames { if n == name { @@ -416,13 +419,13 @@ func (o *baseObject) _delete(name string) { } func (o *baseObject) deleteIdx(idx valueInt, throw bool) bool { - return o.val.self.deleteStr(idx.String(), throw) + return o.val.self.deleteStr(idx.string(), throw) } func (o *baseObject) deleteSym(s *valueSymbol, throw bool) bool { if o.symValues != nil { if val := o.symValues.get(s); val != nil { - if !o.checkDelete(s.String(), val, throw) { + if !o.checkDelete(s.desc.string(), val, throw) { return false } o.symValues.remove(s) @@ -431,7 +434,7 @@ func (o *baseObject) deleteSym(s *valueSymbol, throw bool) bool { return true } -func (o *baseObject) deleteStr(name string, throw bool) bool { +func (o *baseObject) deleteStr(name unistring.String, throw bool) bool { if val, exists := o.values[name]; exists { if !o.checkDelete(name, val, throw) { return false @@ -461,7 +464,7 @@ func (o *baseObject) setProto(proto *Object, throw bool) bool { return true } -func (o *baseObject) setOwnStr(name string, val Value, throw bool) bool { +func (o *baseObject) setOwnStr(name unistring.String, val Value, throw bool) bool { ownDesc := o.values[name] if ownDesc == nil { if proto := o.prototype; proto != nil { @@ -494,7 +497,7 @@ func (o *baseObject) setOwnStr(name string, val Value, throw bool) bool { } func (o *baseObject) setOwnIdx(idx valueInt, val Value, throw bool) bool { - return o.val.self.setOwnStr(idx.String(), val, throw) + return o.val.self.setOwnStr(idx.string(), val, throw) } func (o *baseObject) setOwnSym(name *valueSymbol, val Value, throw bool) bool { @@ -534,7 +537,7 @@ func (o *baseObject) setOwnSym(name *valueSymbol, val Value, throw bool) bool { return true } -func (o *baseObject) _setForeignStr(name string, prop, val, receiver Value, throw bool) (bool, bool) { +func (o *baseObject) _setForeignStr(name unistring.String, prop, val, receiver Value, throw bool) (bool, bool) { if prop != nil { if prop, ok := prop.(*valueProperty); ok { if !prop.isWritable() { @@ -580,12 +583,12 @@ func (o *baseObject) _setForeignIdx(idx valueInt, prop, val, receiver Value, thr return false, false } -func (o *baseObject) setForeignStr(name string, val, receiver Value, throw bool) (bool, bool) { +func (o *baseObject) setForeignStr(name unistring.String, val, receiver Value, throw bool) (bool, bool) { return o._setForeignStr(name, o.values[name], val, receiver, throw) } func (o *baseObject) setForeignIdx(name valueInt, val, receiver Value, throw bool) (bool, bool) { - return o.val.self.setForeignStr(name.String(), val, receiver, throw) + return o.val.self.setForeignStr(name.string(), val, receiver, throw) } func (o *baseObject) setForeignSym(name *valueSymbol, val, receiver Value, throw bool) (bool, bool) { @@ -622,16 +625,16 @@ func (o *baseObject) hasOwnPropertySym(s *valueSymbol) bool { return false } -func (o *baseObject) hasOwnPropertyStr(name string) bool { +func (o *baseObject) hasOwnPropertyStr(name unistring.String) bool { _, exists := o.values[name] return exists } func (o *baseObject) hasOwnPropertyIdx(idx valueInt) bool { - return o.val.self.hasOwnPropertyStr(idx.String()) + return o.val.self.hasOwnPropertyStr(idx.string()) } -func (o *baseObject) _defineOwnProperty(name string, existingValue Value, descr PropertyDescriptor, throw bool) (val Value, ok bool) { +func (o *baseObject) _defineOwnProperty(name unistring.String, existingValue Value, descr PropertyDescriptor, throw bool) (val Value, ok bool) { getterObj, _ := descr.Getter.(*Object) setterObj, _ := descr.Setter.(*Object) @@ -734,7 +737,7 @@ Reject: } -func (o *baseObject) defineOwnPropertyStr(name string, descr PropertyDescriptor, throw bool) bool { +func (o *baseObject) defineOwnPropertyStr(name unistring.String, descr PropertyDescriptor, throw bool) bool { existingVal := o.values[name] if v, ok := o._defineOwnProperty(name, existingVal, descr, throw); ok { o.values[name] = v @@ -747,7 +750,7 @@ func (o *baseObject) defineOwnPropertyStr(name string, descr PropertyDescriptor, } func (o *baseObject) defineOwnPropertyIdx(idx valueInt, desc PropertyDescriptor, throw bool) bool { - return o.val.self.defineOwnPropertyStr(idx.String(), desc, throw) + return o.val.self.defineOwnPropertyStr(idx.string(), desc, throw) } func (o *baseObject) defineOwnPropertySym(s *valueSymbol, descr PropertyDescriptor, throw bool) bool { @@ -755,7 +758,7 @@ func (o *baseObject) defineOwnPropertySym(s *valueSymbol, descr PropertyDescript if o.symValues != nil { existingVal = o.symValues.get(s) } - if v, ok := o._defineOwnProperty(s.String(), existingVal, descr, throw); ok { + if v, ok := o._defineOwnProperty(s.desc.string(), existingVal, descr, throw); ok { if o.symValues == nil { o.symValues = newOrderedMap(&o.val.runtime.hash) } @@ -765,7 +768,7 @@ func (o *baseObject) defineOwnPropertySym(s *valueSymbol, descr PropertyDescript return false } -func (o *baseObject) _put(name string, v Value) { +func (o *baseObject) _put(name unistring.String, v Value) { if _, exists := o.values[name]; !exists { o.propNames = append(o.propNames, name) } @@ -785,7 +788,7 @@ func valueProp(value Value, writable, enumerable, configurable bool) Value { } } -func (o *baseObject) _putProp(name string, value Value, writable, enumerable, configurable bool) Value { +func (o *baseObject) _putProp(name unistring.String, value Value, writable, enumerable, configurable bool) Value { prop := valueProp(value, writable, enumerable, configurable) o._put(name, prop) return prop @@ -810,7 +813,7 @@ func (o *baseObject) tryExoticToPrimitive(hint string) Value { } func (o *baseObject) tryPrimitive(methodName string) Value { - if method, ok := o.val.self.getStr(methodName, nil).(*Object); ok { + if method, ok := o.val.self.getStr(unistring.String(methodName), nil).(*Object); ok { if call, ok := method.self.assertCallable(); ok { v := call(FunctionCall{ This: o.val, @@ -905,7 +908,7 @@ func (o *baseObject) export() interface{} { m := make(map[string]interface{}) for _, itemName := range o.ownKeys(false, nil) { itemNameStr := itemName.String() - v := o.val.self.getStr(itemNameStr, nil) + v := o.val.self.getStr(itemName.string(), nil) if v != nil { m[itemNameStr] = v.Export() } else { @@ -929,21 +932,21 @@ const ( ) type propIterItem struct { - name string + name unistring.String value Value // set only when enumerable == _ENUM_UNKNOWN enumerable enumerableFlag } type objectPropIter struct { o *baseObject - propNames []string + propNames []unistring.String idx int } type propFilterIter struct { wrapped iterNextFunc all bool - seen map[string]bool + seen map[unistring.String]bool } func (i *propFilterIter) next() (propIterItem, iterNextFunc) { @@ -989,7 +992,7 @@ func (i *objectPropIter) next() (propIterItem, iterNextFunc) { func (o *baseObject) enumerate() iterNextFunc { return (&propFilterIter{ wrapped: o.val.self.enumerateUnfiltered(), - seen: make(map[string]bool), + seen: make(map[unistring.String]bool), }).next } @@ -997,7 +1000,7 @@ func (o *baseObject) ownIter() iterNextFunc { if len(o.propNames) > o.lastSortedPropLen { o.fixPropOrder() } - propNames := make([]string, len(o.propNames)) + propNames := make([]unistring.String, len(o.propNames)) copy(propNames, o.propNames) return (&objectPropIter{ o: o, @@ -1069,7 +1072,7 @@ func (o *baseObject) ownKeys(all bool, keys []Value) []Value { } if all { for _, k := range o.propNames { - keys = append(keys, newStringValue(k)) + keys = append(keys, stringValueFromRaw(k)) } } else { for _, k := range o.propNames { @@ -1077,34 +1080,44 @@ func (o *baseObject) ownKeys(all bool, keys []Value) []Value { if prop, ok := prop.(*valueProperty); ok && !prop.enumerable { continue } - keys = append(keys, newStringValue(k)) + keys = append(keys, stringValueFromRaw(k)) } } return keys } -func (o *baseObject) ownSymbols() (res []Value) { +func (o *baseObject) ownSymbols(all bool, accum []Value) []Value { if o.symValues != nil { iter := o.symValues.newIter() - for { - entry := iter.next() - if entry == nil { - break + if all { + for { + entry := iter.next() + if entry == nil { + break + } + accum = append(accum, entry.key) + } + } else { + for { + entry := iter.next() + if entry == nil { + break + } + if prop, ok := entry.value.(*valueProperty); ok { + if !prop.enumerable { + continue + } + } + accum = append(accum, entry.key) } - res = append(res, entry.key) } } - return + return accum } func (o *baseObject) ownPropertyKeys(all bool, accum []Value) []Value { - accum = o.val.self.ownKeys(all, accum) - if all { - accum = append(accum, o.ownSymbols()...) - } - - return accum + return o.ownSymbols(all, o.val.self.ownKeys(all, accum)) } func (o *baseObject) hasInstance(Value) bool { @@ -1141,7 +1154,7 @@ func (o *Object) get(p Value, receiver Value) Value { case *valueSymbol: return o.self.getSym(p, receiver) default: - return o.self.getStr(p.String(), receiver) + return o.self.getStr(p.string(), receiver) } } @@ -1152,7 +1165,7 @@ func (o *Object) getOwnProp(p Value) Value { case *valueSymbol: return o.self.getOwnPropSym(p) default: - return o.self.getOwnPropStr(p.String()) + return o.self.getOwnPropStr(p.string()) } } @@ -1163,7 +1176,7 @@ func (o *Object) hasOwnProperty(p Value) bool { case *valueSymbol: return o.self.hasOwnPropertySym(p) default: - return o.self.hasOwnPropertyStr(p.String()) + return o.self.hasOwnPropertyStr(p.string()) } } @@ -1174,11 +1187,11 @@ func (o *Object) hasProperty(p Value) bool { case *valueSymbol: return o.self.hasPropertySym(p) default: - return o.self.hasPropertyStr(p.String()) + return o.self.hasPropertyStr(p.string()) } } -func (o *Object) setStr(name string, val, receiver Value, throw bool) bool { +func (o *Object) setStr(name unistring.String, val, receiver Value, throw bool) bool { if receiver == o { return o.self.setOwnStr(name, val, throw) } else { @@ -1222,7 +1235,7 @@ func (o *Object) set(name Value, val, receiver Value, throw bool) bool { case *valueSymbol: return o.setSym(name, val, receiver, throw) default: - return o.setStr(name.String(), val, receiver, throw) + return o.setStr(name.string(), val, receiver, throw) } } @@ -1233,7 +1246,7 @@ func (o *Object) setOwn(name Value, val Value, throw bool) bool { case *valueSymbol: return o.self.setOwnSym(name, val, throw) default: - return o.self.setOwnStr(name.String(), val, throw) + return o.self.setOwnStr(name.string(), val, throw) } } @@ -1318,7 +1331,7 @@ func (o *Object) delete(n Value, throw bool) bool { case *valueSymbol: return o.self.deleteSym(n, throw) default: - return o.self.deleteStr(n.String(), throw) + return o.self.deleteStr(n.string(), throw) } } @@ -1329,7 +1342,7 @@ func (o *Object) defineOwnProperty(n Value, desc PropertyDescriptor, throw bool) case *valueSymbol: return o.self.defineOwnPropertySym(n, desc, throw) default: - return o.self.defineOwnPropertyStr(n.String(), desc, throw) + return o.self.defineOwnPropertyStr(n.string(), desc, throw) } } diff --git a/object_args.go b/object_args.go index 85aaf5b7..e272ce3b 100644 --- a/object_args.go +++ b/object_args.go @@ -1,5 +1,7 @@ package goja +import "github.com/dop251/goja/unistring" + type argumentsObject struct { baseObject length int @@ -10,11 +12,11 @@ type mappedProperty struct { v *Value } -func (a *argumentsObject) getStr(name string, receiver Value) Value { +func (a *argumentsObject) getStr(name unistring.String, receiver Value) Value { return a.getStrWithOwnProp(a.getOwnPropStr(name), name, receiver) } -func (a *argumentsObject) getOwnPropStr(name string) Value { +func (a *argumentsObject) getOwnPropStr(name unistring.String) Value { if mapped, ok := a.values[name].(*mappedProperty); ok { return *mapped.v } @@ -27,7 +29,7 @@ func (a *argumentsObject) init() { a._putProp("length", intToValue(int64(a.length)), true, false, true) } -func (a *argumentsObject) setOwnStr(name string, val Value, throw bool) bool { +func (a *argumentsObject) setOwnStr(name unistring.String, val Value, throw bool) bool { if prop, ok := a.values[name].(*mappedProperty); ok { if !prop.writable { a.val.runtime.typeErrorResult(throw, "Property is not writable: %s", name) @@ -39,7 +41,7 @@ func (a *argumentsObject) setOwnStr(name string, val Value, throw bool) bool { return a.baseObject.setOwnStr(name, val, throw) } -func (a *argumentsObject) setForeignStr(name string, val, receiver Value, throw bool) (bool, bool) { +func (a *argumentsObject) setForeignStr(name unistring.String, val, receiver Value, throw bool) (bool, bool) { return a._setForeignStr(name, a.getOwnPropStr(name), val, receiver, throw) } @@ -55,7 +57,7 @@ func (a *argumentsObject) setForeignStr(name string, val, receiver Value, throw a.baseObject.putStr(name, val, throw) }*/ -func (a *argumentsObject) deleteStr(name string, throw bool) bool { +func (a *argumentsObject) deleteStr(name unistring.String, throw bool) bool { if prop, ok := a.values[name].(*mappedProperty); ok { if !a.checkDeleteProp(name, &prop.valueProperty, throw) { return false @@ -89,7 +91,7 @@ func (a *argumentsObject) enumerateUnfiltered() iterNextFunc { }).next) } -func (a *argumentsObject) defineOwnPropertyStr(name string, descr PropertyDescriptor, throw bool) bool { +func (a *argumentsObject) defineOwnPropertyStr(name unistring.String, descr PropertyDescriptor, throw bool) bool { if mapped, ok := a.values[name].(*mappedProperty); ok { existing := &valueProperty{ configurable: mapped.configurable, diff --git a/object_gomap.go b/object_gomap.go index ad9d4e6a..601b3638 100644 --- a/object_gomap.go +++ b/object_gomap.go @@ -2,6 +2,8 @@ package goja import ( "reflect" + + "github.com/dop251/goja/unistring" ) type objectGoMapSimple struct { @@ -24,23 +26,24 @@ func (o *objectGoMapSimple) _getStr(name string) Value { return o.val.runtime.ToValue(v) } -func (o *objectGoMapSimple) getStr(name string, receiver Value) Value { - if v := o._getStr(name); v != nil { +func (o *objectGoMapSimple) getStr(name unistring.String, receiver Value) Value { + if v := o._getStr(name.String()); v != nil { return v } return o.baseObject.getStr(name, receiver) } -func (o *objectGoMapSimple) getOwnPropStr(name string) Value { - if v := o._getStr(name); v != nil { +func (o *objectGoMapSimple) getOwnPropStr(name unistring.String) Value { + if v := o._getStr(name.String()); v != nil { return v } return nil } -func (o *objectGoMapSimple) setOwnStr(name string, val Value, throw bool) bool { - if _, exists := o.data[name]; exists { - o.data[name] = val.Export() +func (o *objectGoMapSimple) setOwnStr(name unistring.String, val Value, throw bool) bool { + n := name.String() + if _, exists := o.data[n]; exists { + o.data[n] = val.Export() return true } if proto := o.prototype; proto != nil { @@ -54,7 +57,7 @@ func (o *objectGoMapSimple) setOwnStr(name string, val Value, throw bool) bool { o.val.runtime.typeErrorResult(throw, "Cannot add property %s, object is not extensible", name) return false } else { - o.data[name] = val.Export() + o.data[n] = val.Export() } return true } @@ -66,8 +69,8 @@ func trueValIfPresent(present bool) Value { return nil } -func (o *objectGoMapSimple) setForeignStr(name string, val, receiver Value, throw bool) (bool, bool) { - return o._setForeignStr(name, trueValIfPresent(o._hasStr(name)), val, receiver, throw) +func (o *objectGoMapSimple) setForeignStr(name unistring.String, val, receiver Value, throw bool) (bool, bool) { + return o._setForeignStr(name, trueValIfPresent(o._hasStr(name.String())), val, receiver, throw) } func (o *objectGoMapSimple) _hasStr(name string) bool { @@ -75,21 +78,22 @@ func (o *objectGoMapSimple) _hasStr(name string) bool { return exists } -func (o *objectGoMapSimple) hasOwnPropertyStr(name string) bool { - return o._hasStr(name) +func (o *objectGoMapSimple) hasOwnPropertyStr(name unistring.String) bool { + return o._hasStr(name.String()) } -func (o *objectGoMapSimple) defineOwnPropertyStr(name string, descr PropertyDescriptor, throw bool) bool { +func (o *objectGoMapSimple) defineOwnPropertyStr(name unistring.String, descr PropertyDescriptor, throw bool) bool { if !o.val.runtime.checkHostObjectPropertyDescr(name, descr, throw) { return false } - if o.extensible || o._hasStr(name) { - o.data[name] = descr.Value.Export() + n := name.String() + if o.extensible || o._hasStr(n) { + o.data[n] = descr.Value.Export() return true } - o.val.runtime.typeErrorResult(throw, "Cannot define property %s, object is not extensible", name) + o.val.runtime.typeErrorResult(throw, "Cannot define property %s, object is not extensible", n) return false } @@ -111,8 +115,8 @@ func (o *objectGoMapSimple) assertCallable() (call func(FunctionCall) Value, ok } */ -func (o *objectGoMapSimple) deleteStr(name string, _ bool) bool { - delete(o.data, name) +func (o *objectGoMapSimple) deleteStr(name unistring.String, _ bool) bool { + delete(o.data, name.String()) return true } @@ -127,7 +131,7 @@ func (i *gomapPropIter) next() (propIterItem, iterNextFunc) { name := i.propNames[i.idx] i.idx++ if _, exists := i.o.data[name]; exists { - return propIterItem{name: name, enumerable: _ENUM_TRUE}, i.next + return propIterItem{name: unistring.NewFromString(name), enumerable: _ENUM_TRUE}, i.next } } diff --git a/object_gomap_reflect.go b/object_gomap_reflect.go index 74ae87c9..03491d36 100644 --- a/object_gomap_reflect.go +++ b/object_gomap_reflect.go @@ -1,6 +1,10 @@ package goja -import "reflect" +import ( + "reflect" + + "github.com/dop251/goja/unistring" +) type objectGoMapReflect struct { objectGoReflect @@ -54,8 +58,8 @@ func (o *objectGoMapReflect) _getStr(name string) Value { return nil } -func (o *objectGoMapReflect) getStr(name string, receiver Value) Value { - if v := o._getStr(name); v != nil { +func (o *objectGoMapReflect) getStr(name unistring.String, receiver Value) Value { + if v := o._getStr(name.String()); v != nil { return v } return o.objectGoReflect.getStr(name, receiver) @@ -68,8 +72,8 @@ func (o *objectGoMapReflect) getIdx(idx valueInt, receiver Value) Value { return o.objectGoReflect.getIdx(idx, receiver) } -func (o *objectGoMapReflect) getOwnPropStr(name string) Value { - if v := o._getStr(name); v != nil { +func (o *objectGoMapReflect) getOwnPropStr(name unistring.String) Value { + if v := o._getStr(name.String()); v != nil { return &valueProperty{ value: v, writable: true, @@ -87,7 +91,7 @@ func (o *objectGoMapReflect) getOwnPropIdx(idx valueInt) Value { enumerable: true, } } - return o.objectGoReflect.getOwnPropStr(idx.String()) + return o.objectGoReflect.getOwnPropStr(idx.string()) } func (o *objectGoMapReflect) toValue(val Value, throw bool) (reflect.Value, bool) { @@ -117,8 +121,9 @@ func (o *objectGoMapReflect) _put(key reflect.Value, val Value, throw bool) bool return false } -func (o *objectGoMapReflect) setOwnStr(name string, val Value, throw bool) bool { - key := o.strToKey(name, false) +func (o *objectGoMapReflect) setOwnStr(name unistring.String, val Value, throw bool) bool { + n := name.String() + key := o.strToKey(n, false) if !key.IsValid() || !o.value.MapIndex(key).IsValid() { if proto := o.prototype; proto != nil { // we know it's foreign because prototype loops are not allowed @@ -128,11 +133,11 @@ func (o *objectGoMapReflect) setOwnStr(name string, val Value, throw bool) bool } // new property if !o.extensible { - o.val.runtime.typeErrorResult(throw, "Cannot add property %s, object is not extensible", name) + o.val.runtime.typeErrorResult(throw, "Cannot add property %s, object is not extensible", n) return false } else { if throw && !key.IsValid() { - o.strToKey(name, true) + o.strToKey(n, true) return false } } @@ -165,7 +170,7 @@ func (o *objectGoMapReflect) setOwnIdx(idx valueInt, val Value, throw bool) bool return true } -func (o *objectGoMapReflect) setForeignStr(name string, val, receiver Value, throw bool) (bool, bool) { +func (o *objectGoMapReflect) setForeignStr(name unistring.String, val, receiver Value, throw bool) (bool, bool) { return o._setForeignStr(name, trueValIfPresent(o.hasOwnPropertyStr(name)), val, receiver, throw) } @@ -173,24 +178,24 @@ func (o *objectGoMapReflect) setForeignIdx(idx valueInt, val, receiver Value, th return o._setForeignIdx(idx, trueValIfPresent(o.hasOwnPropertyIdx(idx)), val, receiver, throw) } -func (o *objectGoMapReflect) defineOwnPropertyStr(name string, descr PropertyDescriptor, throw bool) bool { +func (o *objectGoMapReflect) defineOwnPropertyStr(name unistring.String, descr PropertyDescriptor, throw bool) bool { if !o.val.runtime.checkHostObjectPropertyDescr(name, descr, throw) { return false } - return o._put(o.strToKey(name, throw), descr.Value, throw) + return o._put(o.strToKey(name.String(), throw), descr.Value, throw) } func (o *objectGoMapReflect) defineOwnPropertyIdx(idx valueInt, descr PropertyDescriptor, throw bool) bool { - if !o.val.runtime.checkHostObjectPropertyDescr(idx.String(), descr, throw) { + if !o.val.runtime.checkHostObjectPropertyDescr(idx.string(), descr, throw) { return false } return o._put(o.toKey(idx, throw), descr.Value, throw) } -func (o *objectGoMapReflect) hasOwnPropertyStr(name string) bool { - key := o.strToKey(name, false) +func (o *objectGoMapReflect) hasOwnPropertyStr(name unistring.String) bool { + key := o.strToKey(name.String(), false) if key.IsValid() && o.value.MapIndex(key).IsValid() { return true } @@ -205,8 +210,8 @@ func (o *objectGoMapReflect) hasOwnPropertyIdx(idx valueInt) bool { return false } -func (o *objectGoMapReflect) deleteStr(name string, throw bool) bool { - key := o.strToKey(name, throw) +func (o *objectGoMapReflect) deleteStr(name unistring.String, throw bool) bool { + key := o.strToKey(name.String(), throw) if !key.IsValid() { return false } @@ -235,7 +240,7 @@ func (i *gomapReflectPropIter) next() (propIterItem, iterNextFunc) { v := i.o.value.MapIndex(key) i.idx++ if v.IsValid() { - return propIterItem{name: key.String(), enumerable: _ENUM_TRUE}, i.next + return propIterItem{name: unistring.NewFromString(key.String()), enumerable: _ENUM_TRUE}, i.next } } diff --git a/object_gomap_reflect_test.go b/object_gomap_reflect_test.go index 9c07bf3c..93266bcc 100644 --- a/object_gomap_reflect_test.go +++ b/object_gomap_reflect_test.go @@ -249,3 +249,32 @@ func TestGoMapReflectProtoProp(t *testing.T) { t.Fatal(err) } } + +func TestGoMapReflectUnicode(t *testing.T) { + const SCRIPT = ` + Object.setPrototypeOf(m, s); + if (m.ВСст !== "passed") { + throw new Error("m.ВСст: " + m.ВСст); + } + m["Γ©"]; + ` + type S struct { + ВСст string + } + vm := New() + m := map[string]int{ + "Γ©": 42, + } + s := S{ + ВСст: "passed", + } + vm.Set("m", m) + vm.Set("s", &s) + res, err := vm.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + if res == nil || !res.StrictEquals(valueInt(42)) { + t.Fatalf("Unexpected value: %v", res) + } +} diff --git a/object_gomap_test.go b/object_gomap_test.go index 0e966e45..451ac01b 100644 --- a/object_gomap_test.go +++ b/object_gomap_test.go @@ -306,3 +306,32 @@ func TestGoMapProtoPropChain(t *testing.T) { t.Fatal(err) } } + +func TestGoMapUnicode(t *testing.T) { + const SCRIPT = ` + Object.setPrototypeOf(m, s); + if (m.ВСст !== "passed") { + throw new Error("m.ВСст: " + m.ВСст); + } + m["Γ©"]; + ` + type S struct { + ВСст string + } + vm := New() + m := map[string]interface{}{ + "Γ©": 42, + } + s := S{ + ВСст: "passed", + } + vm.Set("m", m) + vm.Set("s", &s) + res, err := vm.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + if res == nil || !res.StrictEquals(valueInt(42)) { + t.Fatalf("Unexpected value: %v", res) + } +} diff --git a/object_goreflect.go b/object_goreflect.go index f5cf9d26..0e8bbc69 100644 --- a/object_goreflect.go +++ b/object_goreflect.go @@ -4,6 +4,8 @@ import ( "fmt" "go/ast" "reflect" + + "github.com/dop251/goja/unistring" ) // JsonEncodable allows custom JSON encoding by JSON.stringify() @@ -83,8 +85,8 @@ func (o *objectGoReflect) valueOfFunc(FunctionCall) Value { return o.toPrimitive() } -func (o *objectGoReflect) getStr(name string, receiver Value) Value { - if v := o._get(name); v != nil { +func (o *objectGoReflect) getStr(name unistring.String, receiver Value) Value { + if v := o._get(name.String()); v != nil { return v } return o.baseObject.getStr(name, receiver) @@ -128,9 +130,10 @@ func (o *objectGoReflect) _get(name string) Value { return nil } -func (o *objectGoReflect) getOwnPropStr(name string) Value { +func (o *objectGoReflect) getOwnPropStr(name unistring.String) Value { + n := name.String() if o.value.Kind() == reflect.Struct { - if v := o._getField(name); v.IsValid() { + if v := o._getField(n); v.IsValid() { return &valueProperty{ value: o.val.runtime.ToValue(o.getAddr(v).Interface()), writable: v.CanSet(), @@ -139,7 +142,7 @@ func (o *objectGoReflect) getOwnPropStr(name string) Value { } } - if v := o._getMethod(name); v.IsValid() { + if v := o._getMethod(n); v.IsValid() { return &valueProperty{ value: o.val.runtime.ToValue(v.Interface()), enumerable: true, @@ -149,8 +152,8 @@ func (o *objectGoReflect) getOwnPropStr(name string) Value { return nil } -func (o *objectGoReflect) setOwnStr(name string, val Value, throw bool) bool { - has, ok := o._put(name, val, throw) +func (o *objectGoReflect) setOwnStr(name unistring.String, val Value, throw bool) bool { + has, ok := o._put(name.String(), val, throw) if !has { if res, ok := o._setForeignStr(name, nil, val, o.val, throw); !ok { o.val.runtime.typeErrorResult(throw, "Cannot assign to property %s of a host object", name) @@ -162,8 +165,8 @@ func (o *objectGoReflect) setOwnStr(name string, val Value, throw bool) bool { return ok } -func (o *objectGoReflect) setForeignStr(name string, val, receiver Value, throw bool) (bool, bool) { - return o._setForeignStr(name, trueValIfPresent(o._has(name)), val, receiver, throw) +func (o *objectGoReflect) setForeignStr(name unistring.String, val, receiver Value, throw bool) (bool, bool) { + return o._setForeignStr(name, trueValIfPresent(o._has(name.String())), val, receiver, throw) } func (o *objectGoReflect) _put(name string, val Value, throw bool) (has, ok bool) { @@ -185,14 +188,14 @@ func (o *objectGoReflect) _put(name string, val Value, throw bool) (has, ok bool return false, false } -func (o *objectGoReflect) _putProp(name string, value Value, writable, enumerable, configurable bool) Value { - if _, ok := o._put(name, value, false); ok { +func (o *objectGoReflect) _putProp(name unistring.String, value Value, writable, enumerable, configurable bool) Value { + if _, ok := o._put(name.String(), value, false); ok { return value } return o.baseObject._putProp(name, value, writable, enumerable, configurable) } -func (r *Runtime) checkHostObjectPropertyDescr(name string, descr PropertyDescriptor, throw bool) bool { +func (r *Runtime) checkHostObjectPropertyDescr(name unistring.String, descr PropertyDescriptor, throw bool) bool { if descr.Getter != nil || descr.Setter != nil { r.typeErrorResult(throw, "Host objects do not support accessor properties") return false @@ -208,10 +211,11 @@ func (r *Runtime) checkHostObjectPropertyDescr(name string, descr PropertyDescri return true } -func (o *objectGoReflect) defineOwnPropertyStr(name string, descr PropertyDescriptor, throw bool) bool { +func (o *objectGoReflect) defineOwnPropertyStr(name unistring.String, descr PropertyDescriptor, throw bool) bool { if o.val.runtime.checkHostObjectPropertyDescr(name, descr, throw) { - if has, ok := o._put(name, descr.Value, throw); !has { - o.val.runtime.typeErrorResult(throw, "Cannot define property '%s' on a host object", name) + n := name.String() + if has, ok := o._put(n, descr.Value, throw); !has { + o.val.runtime.typeErrorResult(throw, "Cannot define property '%s' on a host object", n) return false } else { return ok @@ -232,8 +236,8 @@ func (o *objectGoReflect) _has(name string) bool { return false } -func (o *objectGoReflect) hasOwnPropertyStr(name string) bool { - return o._has(name) +func (o *objectGoReflect) hasOwnPropertyStr(name unistring.String) bool { + return o._has(name.String()) } func (o *objectGoReflect) _toNumber() Value { @@ -293,9 +297,10 @@ func (o *objectGoReflect) toPrimitive() Value { return o.toPrimitiveString() } -func (o *objectGoReflect) deleteStr(name string, throw bool) bool { - if o._has(name) { - o.val.runtime.typeErrorResult(throw, "Cannot delete property %s from a Go type") +func (o *objectGoReflect) deleteStr(name unistring.String, throw bool) bool { + n := name.String() + if o._has(n) { + o.val.runtime.typeErrorResult(throw, "Cannot delete property %s from a Go type", n) return false } return o.baseObject.deleteStr(name, throw) @@ -311,7 +316,7 @@ func (i *goreflectPropIter) nextField() (propIterItem, iterNextFunc) { if i.idx < len(names) { name := names[i.idx] i.idx++ - return propIterItem{name: name, enumerable: _ENUM_TRUE}, i.nextField + return propIterItem{name: unistring.NewFromString(name), enumerable: _ENUM_TRUE}, i.nextField } i.idx = 0 @@ -323,7 +328,7 @@ func (i *goreflectPropIter) nextMethod() (propIterItem, iterNextFunc) { if i.idx < len(names) { name := names[i.idx] i.idx++ - return propIterItem{name: name, enumerable: _ENUM_TRUE}, i.nextMethod + return propIterItem{name: unistring.NewFromString(name), enumerable: _ENUM_TRUE}, i.nextMethod } return propIterItem{}, nil diff --git a/object_goreflect_test.go b/object_goreflect_test.go index 94fbf10e..8ddc13cb 100644 --- a/object_goreflect_test.go +++ b/object_goreflect_test.go @@ -1047,3 +1047,20 @@ func TestGoObj__Proto__(t *testing.T) { t.Fatal(err) } } + +func TestGoReflectUnicodeProps(t *testing.T) { + type S struct { + ВСст string + } + vm := New() + var s S + vm.Set("s", &s) + _, err := vm.RunString(` + if (!s.hasOwnProperty("ВСст")) { + throw new Error("hasOwnProperty"); + } + `) + if err != nil { + t.Fatal(err) + } +} diff --git a/object_goslice.go b/object_goslice.go index 10892bee..21dc8998 100644 --- a/object_goslice.go +++ b/object_goslice.go @@ -1,10 +1,10 @@ package goja import ( - "math" - "math/bits" "reflect" "strconv" + + "github.com/dop251/goja/unistring" ) type objectGoSlice struct { @@ -28,7 +28,7 @@ func (o *objectGoSlice) updateLen() { o.lengthProp.value = intToValue(int64(len(*o.data))) } -func (o *objectGoSlice) getStr(name string, receiver Value) Value { +func (o *objectGoSlice) getStr(name unistring.String, receiver Value) Value { var ownProp Value if idx := strToGoIdx(name); idx >= 0 && idx < len(*o.data) { v := (*o.data)[idx] @@ -54,7 +54,7 @@ func (o *objectGoSlice) getIdx(idx valueInt, receiver Value) Value { return nil } -func (o *objectGoSlice) getOwnPropStr(name string) Value { +func (o *objectGoSlice) getOwnPropStr(name unistring.String) Value { if idx := strToGoIdx(name); idx >= 0 { if idx < len(*o.data) { v := o.val.runtime.ToValue((*o.data)[idx]) @@ -134,15 +134,6 @@ func (o *objectGoSlice) putIdx(idx int, v Value, throw bool) { (*o.data)[idx] = v.Export() } -func toInt(i int64) int { - if bits.UintSize == 32 { - if i >= math.MaxInt32 || i < math.MinInt32 { - panic(rangeError("Integer value overflows 32-bit int")) - } - } - return int(i) -} - func (o *objectGoSlice) putLength(v Value, throw bool) bool { newLen := toInt(toLength(v)) curLen := len(*o.data) @@ -171,7 +162,7 @@ func (o *objectGoSlice) setOwnIdx(idx valueInt, val Value, throw bool) bool { } o.putIdx(i, val, throw) } else { - name := idx.String() + name := idx.string() if res, ok := o._setForeignStr(name, nil, val, o.val, throw); !ok { o.val.runtime.typeErrorResult(throw, "Can't set property '%s' on Go slice", name) return false @@ -182,7 +173,7 @@ func (o *objectGoSlice) setOwnIdx(idx valueInt, val Value, throw bool) bool { return true } -func (o *objectGoSlice) setOwnStr(name string, val Value, throw bool) bool { +func (o *objectGoSlice) setOwnStr(name unistring.String, val Value, throw bool) bool { if idx := strToGoIdx(name); idx >= 0 { if idx >= len(*o.data) { if res, ok := o._setForeignStr(name, nil, val, o.val, throw); ok { @@ -208,7 +199,7 @@ func (o *objectGoSlice) setForeignIdx(idx valueInt, val, receiver Value, throw b return o._setForeignIdx(idx, trueValIfPresent(o.hasOwnPropertyIdx(idx)), val, receiver, throw) } -func (o *objectGoSlice) setForeignStr(name string, val, receiver Value, throw bool) (bool, bool) { +func (o *objectGoSlice) setForeignStr(name unistring.String, val, receiver Value, throw bool) (bool, bool) { return o._setForeignStr(name, trueValIfPresent(o.hasOwnPropertyStr(name)), val, receiver, throw) } @@ -219,7 +210,7 @@ func (o *objectGoSlice) hasOwnPropertyIdx(idx valueInt) bool { return false } -func (o *objectGoSlice) hasOwnPropertyStr(name string) bool { +func (o *objectGoSlice) hasOwnPropertyStr(name unistring.String) bool { if idx := strToIdx64(name); idx >= 0 { return idx < int64(len(*o.data)) } @@ -228,7 +219,7 @@ func (o *objectGoSlice) hasOwnPropertyStr(name string) bool { func (o *objectGoSlice) defineOwnPropertyIdx(idx valueInt, descr PropertyDescriptor, throw bool) bool { if i := toInt(int64(idx)); i >= 0 { - if !o.val.runtime.checkHostObjectPropertyDescr(idx.String(), descr, throw) { + if !o.val.runtime.checkHostObjectPropertyDescr(idx.string(), descr, throw) { return false } val := descr.Value @@ -242,7 +233,7 @@ func (o *objectGoSlice) defineOwnPropertyIdx(idx valueInt, descr PropertyDescrip return false } -func (o *objectGoSlice) defineOwnPropertyStr(name string, descr PropertyDescriptor, throw bool) bool { +func (o *objectGoSlice) defineOwnPropertyStr(name unistring.String, descr PropertyDescriptor, throw bool) bool { if idx := strToGoIdx(name); idx >= 0 { if !o.val.runtime.checkHostObjectPropertyDescr(name, descr, throw) { return false @@ -275,7 +266,7 @@ func (o *objectGoSlice) toPrimitive() Value { return o.toPrimitiveString() } -func (o *objectGoSlice) deleteStr(name string, throw bool) bool { +func (o *objectGoSlice) deleteStr(name unistring.String, throw bool) bool { if idx := strToIdx64(name); idx >= 0 { if idx < int64(len(*o.data)) { o.val.runtime.typeErrorResult(throw, "Can't delete from Go slice") @@ -306,7 +297,7 @@ func (i *goslicePropIter) next() (propIterItem, iterNextFunc) { if i.idx < i.limit && i.idx < len(*i.o.data) { name := strconv.Itoa(i.idx) i.idx++ - return propIterItem{name: name, enumerable: _ENUM_TRUE}, i.next + return propIterItem{name: unistring.String(name), enumerable: _ENUM_TRUE}, i.next } return propIterItem{}, nil diff --git a/object_goslice_reflect.go b/object_goslice_reflect.go index f4ff9046..4532c838 100644 --- a/object_goslice_reflect.go +++ b/object_goslice_reflect.go @@ -3,6 +3,8 @@ package goja import ( "reflect" "strconv" + + "github.com/dop251/goja/unistring" ) type objectGoSliceReflect struct { @@ -32,7 +34,7 @@ func (o *objectGoSliceReflect) _hasIdx(idx valueInt) bool { return false } -func (o *objectGoSliceReflect) _hasStr(name string) bool { +func (o *objectGoSliceReflect) _hasStr(name unistring.String) bool { if idx := strToIdx64(name); idx >= 0 && idx < int64(o.value.Len()) { return true } @@ -51,10 +53,10 @@ func (o *objectGoSliceReflect) getIdx(idx valueInt, receiver Value) Value { if idx := toInt(int64(idx)); idx >= 0 && idx < o.value.Len() { return o._getIdx(idx) } - return o.objectGoReflect.getStr(idx.String(), receiver) + return o.objectGoReflect.getStr(idx.string(), receiver) } -func (o *objectGoSliceReflect) getStr(name string, receiver Value) Value { +func (o *objectGoSliceReflect) getStr(name unistring.String, receiver Value) Value { var ownProp Value if idx := strToGoIdx(name); idx >= 0 && idx < o.value.Len() { ownProp = o._getIdx(idx) @@ -66,7 +68,7 @@ func (o *objectGoSliceReflect) getStr(name string, receiver Value) Value { return o.getStrWithOwnProp(ownProp, name, receiver) } -func (o *objectGoSliceReflect) getOwnPropStr(name string) Value { +func (o *objectGoSliceReflect) getOwnPropStr(name unistring.String) Value { if idx := strToGoIdx(name); idx >= 0 { if idx < o.value.Len() { return &valueProperty{ @@ -180,7 +182,7 @@ func (o *objectGoSliceReflect) setOwnIdx(idx valueInt, val Value, throw bool) bo } o.putIdx(i, val, throw) } else { - name := idx.String() + name := idx.string() if res, ok := o._setForeignStr(name, nil, val, o.val, throw); !ok { o.val.runtime.typeErrorResult(throw, "Can't set property '%s' on Go slice", name) return false @@ -191,7 +193,7 @@ func (o *objectGoSliceReflect) setOwnIdx(idx valueInt, val Value, throw bool) bo return true } -func (o *objectGoSliceReflect) setOwnStr(name string, val Value, throw bool) bool { +func (o *objectGoSliceReflect) setOwnStr(name unistring.String, val Value, throw bool) bool { if idx := strToGoIdx(name); idx >= 0 { if idx >= o.value.Len() { if res, ok := o._setForeignStr(name, nil, val, o.val, throw); ok { @@ -217,7 +219,7 @@ func (o *objectGoSliceReflect) setForeignIdx(idx valueInt, val, receiver Value, return o._setForeignIdx(idx, trueValIfPresent(o._hasIdx(idx)), val, receiver, throw) } -func (o *objectGoSliceReflect) setForeignStr(name string, val, receiver Value, throw bool) (bool, bool) { +func (o *objectGoSliceReflect) setForeignStr(name unistring.String, val, receiver Value, throw bool) (bool, bool) { return o._setForeignStr(name, trueValIfPresent(o._hasStr(name)), val, receiver, throw) } @@ -225,16 +227,16 @@ func (o *objectGoSliceReflect) hasOwnPropertyIdx(idx valueInt) bool { return o._hasIdx(idx) } -func (o *objectGoSliceReflect) hasOwnPropertyStr(name string) bool { +func (o *objectGoSliceReflect) hasOwnPropertyStr(name unistring.String) bool { if o._hasStr(name) { return true } - return o.objectGoReflect._has(name) + return o.objectGoReflect._has(name.String()) } func (o *objectGoSliceReflect) defineOwnPropertyIdx(idx valueInt, descr PropertyDescriptor, throw bool) bool { if i := toInt(int64(idx)); i >= 0 { - if !o.val.runtime.checkHostObjectPropertyDescr(idx.String(), descr, throw) { + if !o.val.runtime.checkHostObjectPropertyDescr(idx.string(), descr, throw) { return false } val := descr.Value @@ -248,7 +250,7 @@ func (o *objectGoSliceReflect) defineOwnPropertyIdx(idx valueInt, descr Property return false } -func (o *objectGoSliceReflect) defineOwnPropertyStr(name string, descr PropertyDescriptor, throw bool) bool { +func (o *objectGoSliceReflect) defineOwnPropertyStr(name unistring.String, descr PropertyDescriptor, throw bool) bool { if idx := strToGoIdx(name); idx >= 0 { if !o.val.runtime.checkHostObjectPropertyDescr(name, descr, throw) { return false @@ -278,7 +280,7 @@ func (o *objectGoSliceReflect) toPrimitive() Value { return o.toPrimitiveString() } -func (o *objectGoSliceReflect) deleteStr(name string, throw bool) bool { +func (o *objectGoSliceReflect) deleteStr(name unistring.String, throw bool) bool { if idx := strToIdx64(name); idx >= 0 { if idx < int64(o.value.Len()) { o.val.runtime.typeErrorResult(throw, "Can't delete from Go slice") @@ -310,7 +312,7 @@ func (i *gosliceReflectPropIter) next() (propIterItem, iterNextFunc) { if i.idx < i.limit && i.idx < i.o.value.Len() { name := strconv.Itoa(i.idx) i.idx++ - return propIterItem{name: name, enumerable: _ENUM_TRUE}, i.next + return propIterItem{name: unistring.String(name), enumerable: _ENUM_TRUE}, i.next } return i.o.objectGoReflect.enumerateUnfiltered()() diff --git a/object_lazy.go b/object_lazy.go index d75a9744..ef9f0e49 100644 --- a/object_lazy.go +++ b/object_lazy.go @@ -1,6 +1,10 @@ package goja -import "reflect" +import ( + "reflect" + + "github.com/dop251/goja/unistring" +) type lazyObject struct { val *Object @@ -61,7 +65,7 @@ func (o *lazyObject) hasOwnPropertySym(s *valueSymbol) bool { return obj.hasOwnPropertySym(s) } -func (o *lazyObject) defineOwnPropertyStr(name string, desc PropertyDescriptor, throw bool) bool { +func (o *lazyObject) defineOwnPropertyStr(name unistring.String, desc PropertyDescriptor, throw bool) bool { obj := o.create(o.val) o.val.self = obj return obj.defineOwnPropertyStr(name, desc, throw) @@ -91,19 +95,19 @@ func (o *lazyObject) deleteSym(s *valueSymbol, throw bool) bool { return obj.deleteSym(s, throw) } -func (o *lazyObject) getStr(name string, receiver Value) Value { +func (o *lazyObject) getStr(name unistring.String, receiver Value) Value { obj := o.create(o.val) o.val.self = obj return obj.getStr(name, receiver) } -func (o *lazyObject) getOwnPropStr(name string) Value { +func (o *lazyObject) getOwnPropStr(name unistring.String) Value { obj := o.create(o.val) o.val.self = obj return obj.getOwnPropStr(name) } -func (o *lazyObject) setOwnStr(p string, v Value, throw bool) bool { +func (o *lazyObject) setOwnStr(p unistring.String, v Value, throw bool) bool { obj := o.create(o.val) o.val.self = obj return obj.setOwnStr(p, v, throw) @@ -121,7 +125,7 @@ func (o *lazyObject) setOwnSym(p *valueSymbol, v Value, throw bool) bool { return obj.setOwnSym(p, v, throw) } -func (o *lazyObject) setForeignStr(p string, v, receiver Value, throw bool) (bool, bool) { +func (o *lazyObject) setForeignStr(p unistring.String, v, receiver Value, throw bool) (bool, bool) { obj := o.create(o.val) o.val.self = obj return obj.setForeignStr(p, v, receiver, throw) @@ -139,19 +143,19 @@ func (o *lazyObject) setForeignSym(p *valueSymbol, v, receiver Value, throw bool return obj.setForeignSym(p, v, receiver, throw) } -func (o *lazyObject) hasPropertyStr(name string) bool { +func (o *lazyObject) hasPropertyStr(name unistring.String) bool { obj := o.create(o.val) o.val.self = obj return obj.hasPropertyStr(name) } -func (o *lazyObject) hasOwnPropertyStr(name string) bool { +func (o *lazyObject) hasOwnPropertyStr(name unistring.String) bool { obj := o.create(o.val) o.val.self = obj return obj.hasOwnPropertyStr(name) } -func (o *lazyObject) _putProp(string, Value, bool, bool, bool) Value { +func (o *lazyObject) _putProp(unistring.String, Value, bool, bool, bool) Value { panic("cannot use _putProp() in lazy object") } @@ -189,7 +193,7 @@ func (o *lazyObject) assertConstructor() func(args []Value, newTarget *Object) * return obj.assertConstructor() } -func (o *lazyObject) deleteStr(name string, throw bool) bool { +func (o *lazyObject) deleteStr(name unistring.String, throw bool) bool { obj := o.create(o.val) o.val.self = obj return obj.deleteStr(name, throw) @@ -255,10 +259,10 @@ func (o *lazyObject) ownKeys(all bool, accum []Value) []Value { return obj.ownKeys(all, accum) } -func (o *lazyObject) ownSymbols() []Value { +func (o *lazyObject) ownSymbols(all bool, accum []Value) []Value { obj := o.create(o.val) o.val.self = obj - return obj.ownSymbols() + return obj.ownSymbols(all, accum) } func (o *lazyObject) ownPropertyKeys(all bool, accum []Value) []Value { diff --git a/object_test.go b/object_test.go index 9bce00f8..03444c02 100644 --- a/object_test.go +++ b/object_test.go @@ -97,6 +97,18 @@ func TestPropertyOrder(t *testing.T) { testScript1(SCRIPT, _undefined, t) } +func TestDefinePropertiesSymbol(t *testing.T) { + const SCRIPT = ` + var desc = {}; + desc[Symbol.toStringTag] = {value: "Test"}; + var o = {}; + Object.defineProperties(o, desc); + o[Symbol.toStringTag] === "Test"; + ` + + testScript1(SCRIPT, valueTrue, t) +} + func BenchmarkPut(b *testing.B) { v := &Object{} diff --git a/parser/expression.go b/parser/expression.go index a90bc1a9..ae418d06 100644 --- a/parser/expression.go +++ b/parser/expression.go @@ -4,10 +4,11 @@ import ( "github.com/dop251/goja/ast" "github.com/dop251/goja/file" "github.com/dop251/goja/token" + "github.com/dop251/goja/unistring" ) func (self *_parser) parseIdentifier() *ast.Identifier { - literal := self.literal + literal := self.parsedLiteral idx := self.idx self.next() return &ast.Identifier{ @@ -17,7 +18,7 @@ func (self *_parser) parseIdentifier() *ast.Identifier { } func (self *_parser) parsePrimaryExpression() ast.Expression { - literal := self.literal + literal, parsedLiteral := self.literal, self.parsedLiteral idx := self.idx switch self.token { case token.IDENTIFIER: @@ -31,7 +32,7 @@ func (self *_parser) parsePrimaryExpression() ast.Expression { } } return &ast.Identifier{ - Name: literal, + Name: parsedLiteral, Idx: idx, } case token.NULL: @@ -43,7 +44,7 @@ func (self *_parser) parsePrimaryExpression() ast.Expression { case token.BOOLEAN: self.next() value := false - switch literal { + switch parsedLiteral { case "true": value = true case "false": @@ -58,14 +59,10 @@ func (self *_parser) parsePrimaryExpression() ast.Expression { } case token.STRING: self.next() - value, err := parseStringLiteral(literal[1 : len(literal)-1]) - if err != nil { - self.error(idx, err.Error()) - } return &ast.StringLiteral{ Idx: idx, Literal: literal, - Value: value, + Value: parsedLiteral, } case token.NUMBER: self.next() @@ -112,7 +109,7 @@ func (self *_parser) parseRegExpLiteral() *ast.RegExpLiteral { } idx := self.idxOf(offset) - pattern, err := self.scanString(offset) + pattern, _, err := self.scanString(offset, false) endOffset := self.chrOffset if err == nil { @@ -151,11 +148,11 @@ func (self *_parser) parseVariableDeclaration(declarationList *[]*ast.VariableEx return &ast.BadExpression{From: idx, To: self.idx} } - literal := self.literal + name := self.parsedLiteral idx := self.idx self.next() node := &ast.VariableExpression{ - Name: literal, + Name: name, Idx: idx, } @@ -192,31 +189,27 @@ func (self *_parser) parseVariableDeclarationList(var_ file.Idx) []ast.Expressio return list } -func (self *_parser) parseObjectPropertyKey() (string, string) { - idx, tkn, literal := self.idx, self.token, self.literal - value := "" +func (self *_parser) parseObjectPropertyKey() (string, unistring.String) { + idx, tkn, literal, parsedLiteral := self.idx, self.token, self.literal, self.parsedLiteral + var value unistring.String self.next() switch tkn { case token.IDENTIFIER: - value = literal + value = parsedLiteral case token.NUMBER: var err error _, err = parseNumberLiteral(literal) if err != nil { self.error(idx, err.Error()) } else { - value = literal + value = unistring.String(literal) } case token.STRING: - var err error - value, err = parseStringLiteral(literal[1 : len(literal)-1]) - if err != nil { - self.error(idx, err.Error()) - } + value = parsedLiteral default: // null, false, class, etc. - if matchIdentifier.MatchString(literal) { - value = literal + if isId(tkn) { + value = unistring.String(literal) } } return literal, value @@ -339,10 +332,10 @@ func (self *_parser) parseCallExpression(left ast.Expression) ast.Expression { func (self *_parser) parseDotMember(left ast.Expression) ast.Expression { period := self.expect(token.PERIOD) - literal := self.literal + literal := self.parsedLiteral idx := self.idx - if !matchIdentifier.MatchString(literal) { + if self.token != token.IDENTIFIER && !isId(self.token) { self.expect(token.IDENTIFIER) self.nextStatement() return &ast.BadExpression{From: period, To: self.idx} @@ -382,7 +375,7 @@ func (self *_parser) parseNewExpression() ast.Expression { } return &ast.MetaProperty{ Meta: &ast.Identifier{ - Name: token.NEW.String(), + Name: unistring.String(token.NEW.String()), Idx: idx, }, Property: prop, diff --git a/parser/lexer.go b/parser/lexer.go index 0626b007..de6baadc 100644 --- a/parser/lexer.go +++ b/parser/lexer.go @@ -1,27 +1,19 @@ package parser import ( - "bytes" "errors" "fmt" - "regexp" "strconv" "strings" "unicode" + "unicode/utf16" "unicode/utf8" "github.com/dop251/goja/file" "github.com/dop251/goja/token" - "unicode/utf16" + "github.com/dop251/goja/unistring" ) -type _chr struct { - value rune - width int -} - -var matchIdentifier = regexp.MustCompile(`^[$_\p{L}][$_\p{L}\d}]*$`) - func isDecimalDigit(chr rune) bool { return '0' <= chr && chr <= '9' } @@ -55,45 +47,65 @@ func isIdentifierPart(chr rune) bool { chr >= utf8.RuneSelf && (unicode.IsLetter(chr) || unicode.IsDigit(chr)) } -func (self *_parser) scanIdentifier() (string, error) { +func (self *_parser) scanIdentifier() (string, unistring.String, bool, error) { offset := self.chrOffset - parse := false + hasEscape := false + isUnicode := false + length := 0 for isIdentifierPart(self.chr) { - if self.chr == '\\' { + r := self.chr + length++ + if r == '\\' { + hasEscape = true distance := self.chrOffset - offset self.read() if self.chr != 'u' { - return "", fmt.Errorf("Invalid identifier escape character: %c (%s)", self.chr, string(self.chr)) + return "", "", false, fmt.Errorf("Invalid identifier escape character: %c (%s)", self.chr, string(self.chr)) } - parse = true var value rune for j := 0; j < 4; j++ { self.read() decimal, ok := hex2decimal(byte(self.chr)) if !ok { - return "", fmt.Errorf("Invalid identifier escape character: %c (%s)", self.chr, string(self.chr)) + return "", "", false, fmt.Errorf("Invalid identifier escape character: %c (%s)", self.chr, string(self.chr)) } value = value<<4 | decimal } if value == '\\' { - return "", fmt.Errorf("Invalid identifier escape value: %c (%s)", value, string(value)) + return "", "", false, fmt.Errorf("Invalid identifier escape value: %c (%s)", value, string(value)) } else if distance == 0 { if !isIdentifierStart(value) { - return "", fmt.Errorf("Invalid identifier escape value: %c (%s)", value, string(value)) + return "", "", false, fmt.Errorf("Invalid identifier escape value: %c (%s)", value, string(value)) } } else if distance > 0 { if !isIdentifierPart(value) { - return "", fmt.Errorf("Invalid identifier escape value: %c (%s)", value, string(value)) + return "", "", false, fmt.Errorf("Invalid identifier escape value: %c (%s)", value, string(value)) } } + r = value + } + if r >= utf8.RuneSelf { + isUnicode = true + if r > 0xFFFF { + length++ + } } self.read() } - literal := string(self.str[offset:self.chrOffset]) - if parse { - return parseStringLiteral(literal) + + literal := self.str[offset:self.chrOffset] + var parsed unistring.String + if hasEscape || isUnicode { + var err error + parsed, err = parseStringLiteral1(literal, length, isUnicode) + if err != nil { + return "", "", false, err + } + } else { + parsed = unistring.String(literal) } - return literal, nil + + return literal, parsed, hasEscape, nil } // 7.2 @@ -118,7 +130,52 @@ func isLineTerminator(chr rune) bool { return false } -func (self *_parser) scan() (tkn token.Token, literal string, idx file.Idx) { +func isId(tkn token.Token) bool { + switch tkn { + case token.KEYWORD, + token.BOOLEAN, + token.NULL, + token.THIS, + token.IF, + token.IN, + token.OF, + token.DO, + + token.VAR, + token.FOR, + token.NEW, + token.TRY, + + token.ELSE, + token.CASE, + token.VOID, + token.WITH, + + token.WHILE, + token.BREAK, + token.CATCH, + token.THROW, + + token.RETURN, + token.TYPEOF, + token.DELETE, + token.SWITCH, + + token.DEFAULT, + token.FINALLY, + + token.FUNCTION, + token.CONTINUE, + token.DEBUGGER, + + token.INSTANCEOF: + + return true + } + return false +} + +func (self *_parser) scan() (tkn token.Token, literal string, parsedLiteral unistring.String, idx file.Idx) { self.implicitSemicolon = false @@ -131,30 +188,43 @@ func (self *_parser) scan() (tkn token.Token, literal string, idx file.Idx) { switch chr := self.chr; { case isIdentifierStart(chr): var err error - literal, err = self.scanIdentifier() + var hasEscape bool + literal, parsedLiteral, hasEscape, err = self.scanIdentifier() if err != nil { tkn = token.ILLEGAL break } - if len(literal) > 1 { + if len(parsedLiteral) > 1 { // Keywords are longer than 1 character, avoid lookup otherwise var strict bool - tkn, strict = token.IsKeyword(literal) + tkn, strict = token.IsKeyword(string(parsedLiteral)) switch tkn { case 0: // Not a keyword - if literal == "true" || literal == "false" { + if parsedLiteral == "true" || parsedLiteral == "false" { + if hasEscape { + tkn = token.ILLEGAL + return + } self.insertSemicolon = true tkn = token.BOOLEAN return - } else if literal == "null" { + } else if parsedLiteral == "null" { + if hasEscape { + tkn = token.ILLEGAL + return + } self.insertSemicolon = true tkn = token.NULL return } case token.KEYWORD: + if hasEscape { + tkn = token.ILLEGAL + return + } tkn = token.KEYWORD if strict { // TODO If strict and in strict mode, then this is not a break @@ -169,10 +239,17 @@ func (self *_parser) scan() (tkn token.Token, literal string, idx file.Idx) { token.RETURN, token.CONTINUE, token.DEBUGGER: + if hasEscape { + tkn = token.ILLEGAL + return + } self.insertSemicolon = true return default: + if hasEscape { + tkn = token.ILLEGAL + } return } @@ -286,7 +363,7 @@ func (self *_parser) scan() (tkn token.Token, literal string, idx file.Idx) { insertSemicolon = true tkn = token.STRING var err error - literal, err = self.scanString(self.chrOffset - 1) + literal, parsedLiteral, err = self.scanString(self.chrOffset-1, true) if err != nil { tkn = token.ILLEGAL } @@ -360,14 +437,6 @@ func (self *_parser) switch6(tkn0, tkn1 token.Token, chr2 rune, tkn2, tkn3 token return tkn0 } -func (self *_parser) chrAt(index int) _chr { - value, width := utf8.DecodeRuneInString(self.str[index:]) - return _chr{ - value: value, - width: width, - } -} - func (self *_parser) _peek() rune { if self.offset+1 < self.length { return rune(self.str[self.offset+1]) @@ -475,19 +544,30 @@ func (self *_parser) scanMantissa(base int) { } } -func (self *_parser) scanEscape(quote rune) { +func (self *_parser) scanEscape(quote rune) (int, bool) { var length, base uint32 - switch self.chr { - //case '0', '1', '2', '3', '4', '5', '6', '7': - // Octal: - // length, base, limit = 3, 8, 255 - case 'a', 'b', 'f', 'n', 'r', 't', 'v', '\\', '"', '\'', '0': + chr := self.chr + switch chr { + case '0', '1', '2', '3', '4', '5', '6', '7': + // Octal: + length, base = 3, 8 + case 'a', 'b', 'f', 'n', 'r', 't', 'v', '\\', '"', '\'': self.read() - return - case '\r', '\n', '\u2028', '\u2029': - self.scanNewline() - return + return 1, false + case '\r': + self.read() + if self.chr == '\n' { + self.read() + return 2, false + } + return 1, false + case '\n': + self.read() + return 1, false + case '\u2028', '\u2029': + self.read() + return 1, true case 'x': self.read() length, base = 2, 16 @@ -496,24 +576,34 @@ func (self *_parser) scanEscape(quote rune) { length, base = 4, 16 default: self.read() // Always make progress - return } - var value uint32 - for ; length > 0 && self.chr != quote && self.chr >= 0; length-- { - digit := uint32(digitValue(self.chr)) - if digit >= base { - break + if length > 0 { + var value uint32 + for ; length > 0 && self.chr != quote && self.chr >= 0; length-- { + digit := uint32(digitValue(self.chr)) + if digit >= base { + break + } + value = value*base + digit + self.read() } - value = value*base + digit - self.read() + chr = rune(value) + } + if chr >= utf8.RuneSelf { + if chr > 0xFFFF { + return 2, true + } + return 1, true } + return 1, false } -func (self *_parser) scanString(offset int) (string, error) { +func (self *_parser) scanString(offset int, parse bool) (literal string, parsed unistring.String, err error) { // " ' / quote := rune(self.str[offset]) - + length := 0 + isUnicode := false for self.chr != quote { chr := self.chr if chr == '\n' || chr == '\r' || chr == '\u2028' || chr == '\u2029' || chr < 0 { @@ -521,14 +611,19 @@ func (self *_parser) scanString(offset int) (string, error) { } self.read() if chr == '\\' { - if quote == '/' { - if self.chr == '\n' || self.chr == '\r' || self.chr == '\u2028' || self.chr == '\u2029' || self.chr < 0 { + if self.chr == '\n' || self.chr == '\r' || self.chr == '\u2028' || self.chr == '\u2029' || self.chr < 0 { + if quote == '/' { goto newline } - self.read() + self.scanNewline() } else { - self.scanEscape(quote) + l, u := self.scanEscape(quote) + length += l + if u { + isUnicode = true + } } + continue } else if chr == '[' && quote == '/' { // Allow a slash (/) in a bracket character class ([...]) // TODO Fix this, this is hacky... @@ -536,21 +631,31 @@ func (self *_parser) scanString(offset int) (string, error) { } else if chr == ']' && quote == -1 { quote = '/' } + if chr >= utf8.RuneSelf { + isUnicode = true + if chr > 0xFFFF { + length++ + } + } + length++ } // " ' / self.read() - - return string(self.str[offset:self.chrOffset]), nil + literal = self.str[offset:self.chrOffset] + if parse { + parsed, err = parseStringLiteral1(literal[1:len(literal)-1], length, isUnicode) + } + return newline: self.scanNewline() - err := "String not terminated" + errStr := "String not terminated" if quote == '/' { - err = "Invalid regular expression: missing /" - self.error(self.idxOf(offset), err) + errStr = "Invalid regular expression: missing /" + self.error(self.idxOf(offset), errStr) } - return "", errors.New(err) + return "", "", errors.New(errStr) } func (self *_parser) scanNewline() { @@ -617,21 +722,16 @@ error: return nil, errors.New("Illegal numeric literal") } -func parseStringLiteral(literal string) (string, error) { - // Best case scenario... - if literal == "" { - return "", nil - } - - // Slightly less-best case scenario... - if !strings.ContainsRune(literal, '\\') { - return literal, nil +func parseStringLiteral1(literal string, length int, unicode bool) (unistring.String, error) { + var sb strings.Builder + var chars []uint16 + if unicode { + chars = make([]uint16, 1, length+1) + chars[0] = unistring.BOM + } else { + sb.Grow(length) } - str := literal - buffer := bytes.NewBuffer(make([]byte, 0, 3*len(literal)/2)) - var surrogate rune -S: for len(str) > 0 { switch chr := str[0]; { // We do not explicitly handle the case of the quote @@ -639,11 +739,20 @@ S: // This assumes we're already passed a partially well-formed literal case chr >= utf8.RuneSelf: chr, size := utf8.DecodeRuneInString(str) - buffer.WriteRune(chr) + if chr <= 0xFFFF { + chars = append(chars, uint16(chr)) + } else { + first, second := utf16.EncodeRune(chr) + chars = append(chars, uint16(first), uint16(second)) + } str = str[size:] continue case chr != '\\': - buffer.WriteByte(chr) + if unicode { + chars = append(chars, uint16(chr)) + } else { + sb.WriteByte(chr) + } str = str[1:] continue } @@ -736,20 +845,32 @@ S: default: value = rune(chr) } - if surrogate != 0 { - value = utf16.DecodeRune(surrogate, value) - surrogate = 0 + } + if unicode { + if value <= 0xFFFF { + chars = append(chars, uint16(value)) } else { - if utf16.IsSurrogate(value) { - surrogate = value - continue S - } + first, second := utf16.EncodeRune(value) + chars = append(chars, uint16(first), uint16(second)) + } + } else { + if value >= utf8.RuneSelf { + return "", fmt.Errorf("Unexpected unicode character") } + sb.WriteByte(byte(value)) } - buffer.WriteRune(value) } - return buffer.String(), nil + if unicode { + if len(chars) != length+1 { + panic(fmt.Errorf("unexpected unicode length while parsing '%s'", literal)) + } + return unistring.FromUtf16(chars), nil + } + if sb.Len() != length { + panic(fmt.Errorf("unexpected length while parsing '%s'", literal)) + } + return unistring.String(sb.String()), nil } func (self *_parser) scanNumericLiteral(decimalPoint bool) (token.Token, string) { diff --git a/parser/lexer_test.go b/parser/lexer_test.go index a25858e1..e91183fc 100644 --- a/parser/lexer_test.go +++ b/parser/lexer_test.go @@ -5,6 +5,7 @@ import ( "github.com/dop251/goja/file" "github.com/dop251/goja/token" + "github.com/dop251/goja/unistring" ) func TestLexer(t *testing.T) { @@ -17,13 +18,13 @@ func TestLexer(t *testing.T) { test := func(src string, test ...interface{}) { parser := setup(src) for len(test) > 0 { - tkn, literal, idx := parser.scan() + tkn, literal, _, idx := parser.scan() if len(test) > 0 { is(tkn, test[0].(token.Token)) test = test[1:] } if len(test) > 0 { - is(literal, test[0].(string)) + is(literal, unistring.String(test[0].(string))) test = test[1:] } if len(test) > 0 { @@ -184,7 +185,7 @@ Second line \ test(`var \u0024 = 1`, token.VAR, "var", 1, - token.IDENTIFIER, "$", 5, + token.IDENTIFIER, "\\u0024", 5, token.ASSIGN, "", 12, token.NUMBER, "1", 14, token.EOF, "", 15, @@ -368,7 +369,8 @@ Second line \ ) test(`"\x0G"`, - token.STRING, "\"\\x0G\"", 1, + token.ILLEGAL, "\"\\x0G\"", 1, + //token.STRING, "\"\\x0G\"", 1, token.EOF, "", 7, ) diff --git a/parser/parser.go b/parser/parser.go index 20f86c9c..a5784d33 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -42,6 +42,7 @@ import ( "github.com/dop251/goja/ast" "github.com/dop251/goja/file" "github.com/dop251/goja/token" + "github.com/dop251/goja/unistring" ) // A Mode value is a set of flags (or 0). They control optional parser functionality. @@ -60,9 +61,10 @@ type _parser struct { chrOffset int // The offset of current character offset int // The offset after current character (may be greater than 1) - idx file.Idx // The index of token - token token.Token // The token - literal string // The literal of the token, if any + idx file.Idx // The index of token + token token.Token // The token + literal string // The literal of the token, if any + parsedLiteral unistring.String scope *_scope insertSemicolon bool // If we see a newline, then insert an implicit semicolon @@ -188,7 +190,7 @@ func (self *_parser) parse() (*ast.Program, error) { } func (self *_parser) next() { - self.token, self.literal, self.idx = self.scan() + self.token, self.literal, self.parsedLiteral, self.idx = self.scan() } func (self *_parser) optionalSemicolon() { diff --git a/parser/parser_test.go b/parser/parser_test.go index ed4c0471..3dd8736c 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -8,6 +8,7 @@ import ( "github.com/dop251/goja/ast" "github.com/dop251/goja/file" + "github.com/dop251/goja/unistring" ) func firstErr(err error) error { @@ -86,9 +87,9 @@ func TestParserErr(t *testing.T) { return program, parser } - program, parser := test("", nil) + test("", nil) - program, parser = test(` + program, parser := test(` var abc; break; do { } while(true); @@ -513,7 +514,7 @@ func TestParser(t *testing.T) { abc() `, nil) - program := test("", nil) + test("", nil) test("//", nil) @@ -531,7 +532,7 @@ func TestParser(t *testing.T) { test("new +", "(anonymous): Line 1:5 Unexpected token +") - program = test(";", nil) + program := test(";", nil) is(len(program.Body), 1) is(program.Body[0].(*ast.EmptyStatement).Semicolon, file.Idx(1)) @@ -874,71 +875,77 @@ func TestParser(t *testing.T) { func Test_parseStringLiteral(t *testing.T) { tt(t, func() { - test := func(have, want string) { - have, err := parseStringLiteral(have) + test := func(have string, want unistring.String) { + parser := newParser("", have) + parser.read() + parser.read() + _, res, err := parser.scanString(0, true) is(err, nil) - is(have, want) + is(res, want) } - test("", "") + test(`""`, "") + test(`/=/`, "=") - test("1(\\\\d+)", "1(\\d+)") + test("'1(\\\\d+)'", "1(\\d+)") - test("\\u2029", "\u2029") + test("'\\u2029'", "\u2029") - test("abc\\uFFFFabc", "abc\uFFFFabc") + test("'abc\\uFFFFabc'", "abc\uFFFFabc") - test("[First line \\\nSecond line \\\n Third line\\\n. ]", + test("'[First line \\\nSecond line \\\n Third line\\\n. ]'", "[First line Second line Third line. ]") - test("\\u007a\\x79\\u000a\\x78", "zy\nx") + test("'\\u007a\\x79\\u000a\\x78'", "zy\nx") // S7.8.4_A4.2_T3 - test("\\a", "a") - test("\u0410", "\u0410") + test("'\\a'", "a") + test("'\u0410'", "\u0410") // S7.8.4_A5.1_T1 - test("\\0", "\u0000") + test("'\\0'", "\u0000") // S8.4_A5 - test("\u0000", "\u0000") + test("'\u0000'", "\u0000") // 15.5.4.20 - test("'abc'\\\n'def'", "'abc''def'") + test("\"'abc'\\\n'def'\"", "'abc''def'") // 15.5.4.20-4-1 - test("'abc'\\\r\n'def'", "'abc''def'") + test("\"'abc'\\\r\n'def'\"", "'abc''def'") // Octal - test("\\0", "\000") - test("\\00", "\000") - test("\\000", "\000") - test("\\09", "\0009") - test("\\009", "\0009") - test("\\0009", "\0009") - test("\\1", "\001") - test("\\01", "\001") - test("\\001", "\001") - test("\\0011", "\0011") - test("\\1abc", "\001abc") - - test("\\\u4e16", "\u4e16") + test("'\\0'", "\000") + test("'\\00'", "\000") + test("'\\000'", "\000") + test("'\\09'", "\0009") + test("'\\009'", "\0009") + test("'\\0009'", "\0009") + test("'\\1'", "\001") + test("'\\01'", "\001") + test("'\\001'", "\001") + test("'\\0011'", "\0011") + test("'\\1abc'", "\001abc") + + test("'\\\u4e16'", "\u4e16") // err - test = func(have, want string) { - have, err := parseStringLiteral(have) + test = func(have string, want unistring.String) { + parser := newParser("", have) + parser.read() + parser.read() + _, res, err := parser.scanString(0, true) is(err.Error(), want) - is(have, "") + is(res, "") } - test(`\u`, `invalid escape: \u: len("") != 4`) - test(`\u0`, `invalid escape: \u: len("0") != 4`) - test(`\u00`, `invalid escape: \u: len("00") != 4`) - test(`\u000`, `invalid escape: \u: len("000") != 4`) + test(`"\u"`, `invalid escape: \u: len("") != 4`) + test(`"\u0"`, `invalid escape: \u: len("0") != 4`) + test(`"\u00"`, `invalid escape: \u: len("00") != 4`) + test(`"\u000"`, `invalid escape: \u: len("000") != 4`) - test(`\x`, `invalid escape: \x: len("") != 2`) - test(`\x0`, `invalid escape: \x: len("0") != 2`) - test(`\x0`, `invalid escape: \x: len("0") != 2`) + test(`"\x"`, `invalid escape: \x: len("") != 2`) + test(`"\x0"`, `invalid escape: \x: len("0") != 2`) }) } diff --git a/parser/scope.go b/parser/scope.go index 1710d5fe..d28ad20b 100644 --- a/parser/scope.go +++ b/parser/scope.go @@ -2,6 +2,7 @@ package parser import ( "github.com/dop251/goja/ast" + "github.com/dop251/goja/unistring" ) type _scope struct { @@ -12,7 +13,7 @@ type _scope struct { inFunction bool declarationList []ast.Declaration - labels []string + labels []unistring.String } func (self *_parser) openScope() { @@ -30,7 +31,7 @@ func (self *_scope) declare(declaration ast.Declaration) { self.declarationList = append(self.declarationList, declaration) } -func (self *_scope) hasLabel(name string) bool { +func (self *_scope) hasLabel(name unistring.String) bool { for _, label := range self.labels { if label == name { return true diff --git a/proxy.go b/proxy.go index ba7f6622..51e0c861 100644 --- a/proxy.go +++ b/proxy.go @@ -1,6 +1,10 @@ package goja -import "reflect" +import ( + "reflect" + + "github.com/dop251/goja/unistring" +) // Proxy is a Go wrapper around ECMAScript Proxy. Calling Runtime.ToValue() on it // returns the underlying Proxy. Calling Export() on an ECMAScript Proxy returns a wrapper. @@ -24,7 +28,7 @@ func (i *proxyPropIter) next() (propIterItem, iterNextFunc) { name := i.names[i.idx] i.idx++ if prop := i.p.val.getOwnProp(name); prop != nil { - return propIterItem{name: name.String(), value: prop}, i.next + return propIterItem{name: name.string(), value: prop}, i.next } } if proto := i.p.proto(); proto != nil { @@ -118,7 +122,7 @@ func (p *proxyObject) proxyCall(trap proxyTrap, args ...Value) (Value, bool) { panic(r.NewTypeError("Proxy already revoked")) } - if m := toMethod(r.getVStr(p.handler, trap.String())); m != nil { + if m := toMethod(r.getVStr(p.handler, unistring.String(trap.String()))); m != nil { return m(FunctionCall{ This: p.handler, Arguments: args, @@ -230,8 +234,8 @@ func (p *proxyObject) proxyDefineOwnProperty(name Value, descr PropertyDescripto return false, false } -func (p *proxyObject) defineOwnPropertyStr(name string, descr PropertyDescriptor, throw bool) bool { - if v, ok := p.proxyDefineOwnProperty(newStringValue(name), descr, throw); ok { +func (p *proxyObject) defineOwnPropertyStr(name unistring.String, descr PropertyDescriptor, throw bool) bool { + if v, ok := p.proxyDefineOwnProperty(stringValueFromRaw(name), descr, throw); ok { return v } return p.target.self.defineOwnPropertyStr(name, descr, throw) @@ -271,8 +275,8 @@ func (p *proxyObject) proxyHas(name Value) (bool, bool) { return false, false } -func (p *proxyObject) hasPropertyStr(name string) bool { - if b, ok := p.proxyHas(newStringValue(name)); ok { +func (p *proxyObject) hasPropertyStr(name unistring.String) bool { + if b, ok := p.proxyHas(stringValueFromRaw(name)); ok { return b } @@ -295,7 +299,7 @@ func (p *proxyObject) hasPropertySym(s *valueSymbol) bool { return p.target.self.hasPropertySym(s) } -func (p *proxyObject) hasOwnPropertyStr(name string) bool { +func (p *proxyObject) hasOwnPropertyStr(name unistring.String) bool { return p.getOwnPropStr(name) != nil } @@ -361,8 +365,8 @@ func (p *proxyObject) proxyGetOwnPropertyDescriptor(name Value) (Value, bool) { return nil, false } -func (p *proxyObject) getOwnPropStr(name string) Value { - if v, ok := p.proxyGetOwnPropertyDescriptor(newStringValue(name)); ok { +func (p *proxyObject) getOwnPropStr(name unistring.String) Value { + if v, ok := p.proxyGetOwnPropertyDescriptor(stringValueFromRaw(name)); ok { return v } @@ -385,8 +389,8 @@ func (p *proxyObject) getOwnPropSym(s *valueSymbol) Value { return p.target.self.getOwnPropSym(s) } -func (p *proxyObject) getStr(name string, receiver Value) Value { - if v, ok := p.proxyGet(newStringValue(name), receiver); ok { +func (p *proxyObject) getStr(name unistring.String, receiver Value) Value { + if v, ok := p.proxyGet(stringValueFromRaw(name), receiver); ok { return v } return p.target.self.getStr(name, receiver) @@ -451,8 +455,8 @@ func (p *proxyObject) proxySet(name, value, receiver Value, throw bool) (bool, b return false, false } -func (p *proxyObject) setOwnStr(name string, v Value, throw bool) bool { - if res, ok := p.proxySet(newStringValue(name), v, p.val, throw); ok { +func (p *proxyObject) setOwnStr(name unistring.String, v Value, throw bool) bool { + if res, ok := p.proxySet(stringValueFromRaw(name), v, p.val, throw); ok { return res } return p.target.setStr(name, v, p.val, throw) @@ -472,8 +476,8 @@ func (p *proxyObject) setOwnSym(s *valueSymbol, v Value, throw bool) bool { return p.target.setSym(s, v, p.val, throw) } -func (p *proxyObject) setForeignStr(name string, v, receiver Value, throw bool) (bool, bool) { - if res, ok := p.proxySet(newStringValue(name), v, receiver, throw); ok { +func (p *proxyObject) setForeignStr(name unistring.String, v, receiver Value, throw bool) (bool, bool) { + if res, ok := p.proxySet(stringValueFromRaw(name), v, receiver, throw); ok { return res, true } return p.target.setStr(name, v, receiver, throw), true @@ -509,8 +513,8 @@ func (p *proxyObject) proxyDelete(n Value) (bool, bool) { return false, false } -func (p *proxyObject) deleteStr(name string, throw bool) bool { - if ret, ok := p.proxyDelete(newStringValue(name)); ok { +func (p *proxyObject) deleteStr(name unistring.String, throw bool) bool { + if ret, ok := p.proxyDelete(stringValueFromRaw(name)); ok { return ret } @@ -719,7 +723,7 @@ func (p *proxyObject) filterKeys(vals []Value, all, symbols bool) []Value { } } else { if _, ok := val.(*valueSymbol); !ok { - prop = p.getOwnPropStr(val.String()) + prop = p.getOwnPropStr(val.string()) } else { continue } @@ -760,12 +764,17 @@ func (p *proxyObject) ownKeys(all bool, _ []Value) []Value { // we can assume ac return p.target.self.ownKeys(all, nil) } -func (p *proxyObject) ownSymbols() []Value { +func (p *proxyObject) ownSymbols(all bool, accum []Value) []Value { if vals, ok := p.proxyOwnKeys(); ok { - return p.filterKeys(vals, true, true) + res := p.filterKeys(vals, true, true) + if accum == nil { + return res + } + accum = append(accum, res...) + return accum } - return p.target.self.ownSymbols() + return p.target.self.ownSymbols(all, accum) } func (p *proxyObject) className() string { diff --git a/regexp.go b/regexp.go index df57847b..31205f5a 100644 --- a/regexp.go +++ b/regexp.go @@ -35,7 +35,7 @@ func (r *regexp2Wrapper) FindSubmatchIndex(s valueString, start int) (result []i case asciiString: match, err = wrapped.FindStringMatch(string(s)[start:]) case unicodeString: - match, err = wrapped.FindRunesMatch(utf16.Decode(s[start:])) + match, err = wrapped.FindRunesMatch(utf16.Decode(s[start+1:])) default: panic(fmt.Errorf("Unknown string type: %T", s)) } @@ -208,7 +208,7 @@ func (r *regexp2Wrapper) MatchString(s valueString) bool { matched, _ := wrapped.MatchString(string(s)) return matched case unicodeString: - matched, _ := wrapped.MatchRunes(utf16.Decode(s)) + matched, _ := wrapped.MatchRunes(utf16.Decode(s[1:])) return matched default: panic(fmt.Errorf("Unknown string type: %T", s)) @@ -287,7 +287,7 @@ func (r *regexpObject) execResultToArray(target valueString, result []int) Value for index := 0; index < captureCount; index++ { offset := index << 1 if result[offset] >= lowerBound { - valueArray[index] = target.substring(int64(result[offset]), int64(result[offset+1])) + valueArray[index] = target.substring(result[offset], result[offset+1]) lowerBound = result[offset] } else { valueArray[index] = _undefined @@ -311,7 +311,7 @@ func (r *regexpObject) execRegexp(target valueString) (match bool, result []int) if !r.global && !r.sticky { index = 0 } - if index >= 0 && index <= target.length() { + if index >= 0 && index <= int64(target.length()) { result = r.pattern.FindSubmatchIndex(target, int(index)) } if result == nil || r.sticky && result[0] != 0 { diff --git a/runtime.go b/runtime.go index ed5f088f..d7f45694 100644 --- a/runtime.go +++ b/runtime.go @@ -10,6 +10,7 @@ import ( "math/bits" "math/rand" "reflect" + "runtime" "strconv" "time" @@ -17,7 +18,7 @@ import ( js_ast "github.com/dop251/goja/ast" "github.com/dop251/goja/parser" - "runtime" + "github.com/dop251/goja/unistring" ) const ( @@ -88,7 +89,6 @@ type global struct { RegExpPrototype *Object DatePrototype *Object SymbolPrototype *Object - ArrayIterator *Object ArrayBufferPrototype *Object DataViewPrototype *Object @@ -98,10 +98,11 @@ type global struct { MapPrototype *Object SetPrototype *Object - IteratorPrototype *Object - ArrayIteratorPrototype *Object - MapIteratorPrototype *Object - SetIteratorPrototype *Object + IteratorPrototype *Object + ArrayIteratorPrototype *Object + MapIteratorPrototype *Object + SetIteratorPrototype *Object + StringIteratorPrototype *Object ErrorPrototype *Object TypeErrorPrototype *Object @@ -158,7 +159,7 @@ type Runtime struct { now Now _collator *collate.Collator - symbolRegistry map[string]*valueSymbol + symbolRegistry map[unistring.String]*valueSymbol typeInfoCache map[reflect.Type]*reflectTypeInfo fieldNameMapper FieldNameMapper @@ -169,7 +170,7 @@ type Runtime struct { type StackFrame struct { prg *Program - funcName string + funcName unistring.String pc int } @@ -187,7 +188,7 @@ func (f *StackFrame) FuncName() string { if f.funcName == "" { return "" } - return f.funcName + return f.funcName.String() } func (f *StackFrame) Position() Position { @@ -203,7 +204,7 @@ func (f *StackFrame) Position() Position { func (f *StackFrame) Write(b *bytes.Buffer) { if f.prg != nil { if n := f.prg.funcName; n != "" { - b.WriteString(n) + b.WriteString(n.String()) b.WriteString(" (") } if n := f.prg.src.name; n != "" { @@ -221,7 +222,7 @@ func (f *StackFrame) Write(b *bytes.Buffer) { } } else { if f.funcName != "" { - b.WriteString(f.funcName) + b.WriteString(f.funcName.String()) b.WriteString(" (") } b.WriteString("native") @@ -311,7 +312,7 @@ func (e *Exception) Value() Value { } func (r *Runtime) addToGlobal(name string, value Value) { - r.globalObject.self._putProp(name, value, true, false, true) + r.globalObject.self._putProp(unistring.String(name), value, true, false, true) } func (r *Runtime) createIterProto(val *Object) objectImpl { @@ -382,7 +383,7 @@ func (r *Runtime) newError(typ *Object, format string, args ...interface{}) Valu return r.builtin_new(typ, []Value{newStringValue(msg)}) } -func (r *Runtime) throwReferenceError(name string) { +func (r *Runtime) throwReferenceError(name unistring.String) { panic(r.newError(r.global.ReferenceError, "%s is not defined", name)) } @@ -431,7 +432,7 @@ func (r *Runtime) NewGoError(err error) *Object { return e } -func (r *Runtime) newFunc(name string, len int, strict bool) (f *funcObject) { +func (r *Runtime) newFunc(name unistring.String, len int, strict bool) (f *funcObject) { v := &Object{runtime: r} f = &funcObject{} @@ -448,7 +449,7 @@ func (r *Runtime) newFunc(name string, len int, strict bool) (f *funcObject) { return } -func (r *Runtime) newNativeFuncObj(v *Object, call func(FunctionCall) Value, construct func(args []Value, proto *Object) *Object, name string, proto *Object, length int) *nativeFuncObject { +func (r *Runtime) newNativeFuncObj(v *Object, call func(FunctionCall) Value, construct func(args []Value, proto *Object) *Object, name unistring.String, proto *Object, length int) *nativeFuncObject { f := &nativeFuncObject{ baseFuncObject: baseFuncObject{ baseObject: baseObject{ @@ -469,7 +470,7 @@ func (r *Runtime) newNativeFuncObj(v *Object, call func(FunctionCall) Value, con return f } -func (r *Runtime) newNativeConstructor(call func(ConstructorCall) *Object, name string, length int) *Object { +func (r *Runtime) newNativeConstructor(call func(ConstructorCall) *Object, name unistring.String, length int) *Object { v := &Object{runtime: r} f := &nativeFuncObject{ @@ -501,7 +502,7 @@ func (r *Runtime) newNativeConstructor(call func(ConstructorCall) *Object, name return v } -func (r *Runtime) newNativeConstructOnly(v *Object, ctor func(args []Value, newTarget *Object) *Object, defaultProto *Object, name string, length int) *nativeFuncObject { +func (r *Runtime) newNativeConstructOnly(v *Object, ctor func(args []Value, newTarget *Object) *Object, defaultProto *Object, name unistring.String, length int) *nativeFuncObject { if v == nil { v = &Object{runtime: r} } @@ -534,7 +535,7 @@ func (r *Runtime) newNativeConstructOnly(v *Object, ctor func(args []Value, newT return f } -func (r *Runtime) newNativeFunc(call func(FunctionCall) Value, construct func(args []Value, proto *Object) *Object, name string, proto *Object, length int) *Object { +func (r *Runtime) newNativeFunc(call func(FunctionCall) Value, construct func(args []Value, proto *Object) *Object, name unistring.String, proto *Object, length int) *Object { v := &Object{runtime: r} f := &nativeFuncObject{ @@ -558,7 +559,7 @@ func (r *Runtime) newNativeFunc(call func(FunctionCall) Value, construct func(ar return v } -func (r *Runtime) newNativeFuncConstructObj(v *Object, construct func(args []Value, proto *Object) *Object, name string, proto *Object, length int) *nativeFuncObject { +func (r *Runtime) newNativeFuncConstructObj(v *Object, construct func(args []Value, proto *Object) *Object, name unistring.String, proto *Object, length int) *nativeFuncObject { f := &nativeFuncObject{ baseFuncObject: baseFuncObject{ baseObject: baseObject{ @@ -579,11 +580,11 @@ func (r *Runtime) newNativeFuncConstructObj(v *Object, construct func(args []Val return f } -func (r *Runtime) newNativeFuncConstruct(construct func(args []Value, proto *Object) *Object, name string, prototype *Object, length int) *Object { +func (r *Runtime) newNativeFuncConstruct(construct func(args []Value, proto *Object) *Object, name unistring.String, prototype *Object, length int) *Object { return r.newNativeFuncConstructProto(construct, name, prototype, r.global.FunctionPrototype, length) } -func (r *Runtime) newNativeFuncConstructProto(construct func(args []Value, proto *Object) *Object, name string, prototype, proto *Object, length int) *Object { +func (r *Runtime) newNativeFuncConstructProto(construct func(args []Value, proto *Object) *Object, name unistring.String, prototype, proto *Object, length int) *Object { v := &Object{runtime: r} f := &nativeFuncObject{} @@ -929,6 +930,15 @@ func toLength(v Value) int64 { return i } +func toInt(i int64) int { + if bits.UintSize == 32 { + if i > math.MaxInt32 || i < math.MinInt32 { + panic(rangeError("Integer value overflows 32-bit int")) + } + } + return int(i) +} + func (r *Runtime) toIndex(v Value) int { intIdx := v.ToInteger() if intIdx >= 0 && intIdx < maxInt { @@ -1178,10 +1188,10 @@ func (r *Runtime) ToValue(i interface{}) Value { return valueFalse } case func(FunctionCall) Value: - name := runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name() + name := unistring.NewFromString(runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()) return r.newNativeFunc(i, nil, name, nil, 0) case func(ConstructorCall) *Object: - name := runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name() + name := unistring.NewFromString(runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()) return r.newNativeConstructor(i, name, 0) case int: return intToValue(int64(i)) @@ -1309,7 +1319,7 @@ func (r *Runtime) ToValue(i interface{}) Value { obj.self = a return obj case reflect.Func: - name := runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name() + name := unistring.NewFromString(runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()) return r.newNativeFunc(r.wrapReflectFunc(value), nil, name, nil, value.Type().NumIn()) } @@ -1559,7 +1569,7 @@ func (r *Runtime) toReflectValue(v Value, typ reflect.Type) (reflect.Value, erro if field.Anonymous { v = o } else { - v = o.self.getStr(name, nil) + v = o.self.getStr(unistring.NewFromString(name), nil) } if v != nil { @@ -1650,12 +1660,12 @@ func (r *Runtime) GlobalObject() *Object { // Set the specified value as a property of the global object. // The value is first converted using ToValue() func (r *Runtime) Set(name string, value interface{}) { - r.globalObject.self.setOwnStr(name, r.ToValue(value), false) + r.globalObject.self.setOwnStr(unistring.NewFromString(name), r.ToValue(value), false) } // Get the specified property of the global object. func (r *Runtime) Get(name string) Value { - return r.globalObject.self.getStr(name, nil) + return r.globalObject.self.getStr(unistring.NewFromString(name), nil) } // SetRandSource sets random source for this Runtime. If not called, the default math/rand is used. @@ -1846,7 +1856,7 @@ func toPropertyKey(key Value) Value { return key.ToPrimitiveString() } -func (r *Runtime) getVStr(v Value, p string) Value { +func (r *Runtime) getVStr(v Value, p unistring.String) Value { o := v.ToObject(r) return o.self.getStr(p, v) } @@ -1933,3 +1943,15 @@ func isArray(object *Object) bool { return false } } + +func isRegexp(v Value) bool { + if o, ok := v.(*Object); ok { + matcher := o.self.getSym(symMatch, nil) + if matcher != nil && matcher != _undefined { + return matcher.ToBoolean() + } + _, reg := o.self.(*regexpObject) + return reg + } + return false +} diff --git a/runtime_test.go b/runtime_test.go index ae1127f3..aef72246 100644 --- a/runtime_test.go +++ b/runtime_test.go @@ -5,6 +5,7 @@ import ( "fmt" "reflect" "runtime" + "strconv" "testing" "time" ) @@ -1146,7 +1147,7 @@ func TestInterruptInWrappedFunction(t *testing.T) { rt.Interrupt(errors.New("hi")) }() - v, err = fn(nil) + _, err = fn(nil) if err == nil { t.Fatal("expected error") } @@ -1173,7 +1174,7 @@ func TestRunLoopPreempt(t *testing.T) { vm.Interrupt(errors.New("hi")) }() - v, err = fn(nil) + _, err = fn(nil) if err == nil { t.Fatal("expected error") } @@ -1523,3 +1524,44 @@ func BenchmarkMainLoop(b *testing.B) { vm.RunProgram(prg) } } + +func BenchmarkStringMapGet(b *testing.B) { + m := make(map[string]Value) + for i := 0; i < 100; i++ { + m[strconv.Itoa(i)] = intToValue(int64(i)) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + if m["50"] == nil { + b.Fatal() + } + } +} + +func BenchmarkValueStringMapGet(b *testing.B) { + m := make(map[valueString]Value) + for i := 0; i < 100; i++ { + m[asciiString(strconv.Itoa(i))] = intToValue(int64(i)) + } + b.ResetTimer() + var key valueString = asciiString("50") + for i := 0; i < b.N; i++ { + if m[key] == nil { + b.Fatal() + } + } +} + +func BenchmarkAsciiStringMapGet(b *testing.B) { + m := make(map[asciiString]Value) + for i := 0; i < 100; i++ { + m[asciiString(strconv.Itoa(i))] = intToValue(int64(i)) + } + b.ResetTimer() + var key = asciiString("50") + for i := 0; i < b.N; i++ { + if m[key] == nil { + b.Fatal() + } + } +} diff --git a/string.go b/string.go index 58b0249f..f1e42022 100644 --- a/string.go +++ b/string.go @@ -3,8 +3,11 @@ package goja import ( "io" "strconv" + "strings" "unicode/utf16" "unicode/utf8" + + "github.com/dop251/goja/unistring" ) const ( @@ -46,37 +49,121 @@ var ( type valueString interface { Value - charAt(int64) rune - length() int64 + charAt(int) rune + length() int concat(valueString) valueString - substring(start, end int64) valueString + substring(start, end int) valueString compareTo(valueString) int reader(start int) io.RuneReader - index(valueString, int64) int64 - lastIndex(valueString, int64) int64 + index(valueString, int) int + lastIndex(valueString, int) int toLower() valueString toUpper() valueString toTrimmedUTF8() string } +type stringIterObject struct { + baseObject + reader io.RuneReader +} + +func isUTF16FirstSurrogate(r rune) bool { + return r >= 0xD800 && r <= 0xDBFF +} + +func isUTF16SecondSurrogate(r rune) bool { + return r >= 0xDC00 && r <= 0xDFFF +} + +func (si *stringIterObject) next() Value { + if si.reader == nil { + return si.val.runtime.createIterResultObject(_undefined, true) + } + r, _, err := si.reader.ReadRune() + if err == io.EOF { + si.reader = nil + return si.val.runtime.createIterResultObject(_undefined, true) + } + return si.val.runtime.createIterResultObject(stringFromRune(r), false) +} + +func stringFromRune(r rune) valueString { + if r < utf8.RuneSelf { + var sb strings.Builder + sb.Grow(1) + sb.WriteByte(byte(r)) + return asciiString(sb.String()) + } + var sb unicodeStringBuilder + if r <= 0xFFFF { + sb.Grow(1) + } else { + sb.Grow(2) + } + sb.writeRune(r) + return sb.string() +} + +func (r *Runtime) createStringIterator(s valueString) Value { + o := &Object{runtime: r} + + si := &stringIterObject{ + reader: s.reader(0), + } + si.class = classStringIterator + si.val = o + si.extensible = true + o.self = si + si.prototype = r.global.StringIteratorPrototype + si.init() + + return o +} + type stringObject struct { baseObject value valueString - length int64 + length int lengthProp valueProperty } -func newUnicodeString(s string) valueString { - return unicodeString(utf16.Encode([]rune(s))) -} - func newStringValue(s string) valueString { + utf16Size := 0 + ascii := true for _, chr := range s { + utf16Size++ if chr >= utf8.RuneSelf { - return newUnicodeString(s) + ascii = false + if chr > 0xFFFF { + utf16Size++ + } } } - return asciiString(s) + if ascii { + return asciiString(s) + } + buf := make([]uint16, utf16Size+1) + buf[0] = unistring.BOM + c := 1 + for _, chr := range s { + if chr <= 0xFFFF { + buf[c] = uint16(chr) + } else { + first, second := utf16.EncodeRune(chr) + buf[c] = uint16(first) + c++ + buf[c] = uint16(second) + } + c++ + } + return unicodeString(buf) +} + +func stringValueFromRaw(raw unistring.String) valueString { + if b := raw.AsUtf16(); b != nil { + return unicodeString(b) + } + return asciiString(raw) } func (s *stringObject) init() { @@ -88,12 +175,12 @@ func (s *stringObject) setLength() { if s.value != nil { s.length = s.value.length() } - s.lengthProp.value = intToValue(s.length) + s.lengthProp.value = intToValue(int64(s.length)) s._put("length", &s.lengthProp) } -func (s *stringObject) getStr(name string, receiver Value) Value { - if i := strToIdx64(name); i >= 0 && i < s.length { +func (s *stringObject) getStr(name unistring.String, receiver Value) Value { + if i := strToGoIdx(name); i >= 0 && i < s.length { return s._getIdx(i) } return s.baseObject.getStr(name, receiver) @@ -102,16 +189,16 @@ func (s *stringObject) getStr(name string, receiver Value) Value { func (s *stringObject) getIdx(idx valueInt, receiver Value) Value { i := int64(idx) if i >= 0 { - if i < s.length { - return s._getIdx(i) + if i < int64(s.length) { + return s._getIdx(int(i)) } return nil } - return s.baseObject.getStr(idx.String(), receiver) + return s.baseObject.getStr(idx.string(), receiver) } -func (s *stringObject) getOwnPropStr(name string) Value { - if i := strToIdx64(name); i >= 0 && i < s.length { +func (s *stringObject) getOwnPropStr(name unistring.String) Value { + if i := strToGoIdx(name); i >= 0 && i < s.length { val := s._getIdx(i) return &valueProperty{ value: val, @@ -125,8 +212,8 @@ func (s *stringObject) getOwnPropStr(name string) Value { func (s *stringObject) getOwnPropIdx(idx valueInt) Value { i := int64(idx) if i >= 0 { - if i < s.length { - val := s._getIdx(i) + if i < int64(s.length) { + val := s._getIdx(int(i)) return &valueProperty{ value: val, enumerable: true, @@ -135,15 +222,15 @@ func (s *stringObject) getOwnPropIdx(idx valueInt) Value { return nil } - return s.baseObject.getOwnPropStr(idx.String()) + return s.baseObject.getOwnPropStr(idx.string()) } -func (s *stringObject) _getIdx(idx int64) Value { +func (s *stringObject) _getIdx(idx int) Value { return s.value.substring(idx, idx+1) } -func (s *stringObject) setOwnStr(name string, val Value, throw bool) bool { - if i := strToIdx64(name); i >= 0 && i < s.length { +func (s *stringObject) setOwnStr(name unistring.String, val Value, throw bool) bool { + if i := strToGoIdx(name); i >= 0 && i < s.length { s.val.runtime.typeErrorResult(throw, "Cannot assign to read only property '%d' of a String", i) return false } @@ -153,15 +240,15 @@ func (s *stringObject) setOwnStr(name string, val Value, throw bool) bool { func (s *stringObject) setOwnIdx(idx valueInt, val Value, throw bool) bool { i := int64(idx) - if i >= 0 && i < s.length { + if i >= 0 && i < int64(s.length) { s.val.runtime.typeErrorResult(throw, "Cannot assign to read only property '%d' of a String", i) return false } - return s.baseObject.setOwnStr(idx.String(), val, throw) + return s.baseObject.setOwnStr(idx.string(), val, throw) } -func (s *stringObject) setForeignStr(name string, val, receiver Value, throw bool) (bool, bool) { +func (s *stringObject) setForeignStr(name unistring.String, val, receiver Value, throw bool) (bool, bool) { return s._setForeignStr(name, s.getOwnPropStr(name), val, receiver, throw) } @@ -169,8 +256,8 @@ func (s *stringObject) setForeignIdx(idx valueInt, val, receiver Value, throw bo return s._setForeignIdx(idx, s.getOwnPropIdx(idx), val, receiver, throw) } -func (s *stringObject) defineOwnPropertyStr(name string, descr PropertyDescriptor, throw bool) bool { - if i := strToIdx64(name); i >= 0 && i < s.length { +func (s *stringObject) defineOwnPropertyStr(name unistring.String, descr PropertyDescriptor, throw bool) bool { + if i := strToGoIdx(name); i >= 0 && i < s.length { s.val.runtime.typeErrorResult(throw, "Cannot redefine property: %d", i) return false } @@ -180,25 +267,25 @@ func (s *stringObject) defineOwnPropertyStr(name string, descr PropertyDescripto func (s *stringObject) defineOwnPropertyIdx(idx valueInt, descr PropertyDescriptor, throw bool) bool { i := int64(idx) - if i >= 0 && i < s.length { + if i >= 0 && i < int64(s.length) { s.val.runtime.typeErrorResult(throw, "Cannot redefine property: %d", i) return false } - return s.baseObject.defineOwnPropertyStr(idx.String(), descr, throw) + return s.baseObject.defineOwnPropertyStr(idx.string(), descr, throw) } type stringPropIter struct { str valueString // separate, because obj can be the singleton obj *stringObject - idx, length int64 + idx, length int } func (i *stringPropIter) next() (propIterItem, iterNextFunc) { if i.idx < i.length { - name := strconv.FormatInt(i.idx, 10) + name := strconv.Itoa(i.idx) i.idx++ - return propIterItem{name: name, enumerable: _ENUM_TRUE}, i.next + return propIterItem{name: unistring.String(name), enumerable: _ENUM_TRUE}, i.next } return i.obj.baseObject.enumerateUnfiltered()() @@ -213,15 +300,15 @@ func (s *stringObject) enumerateUnfiltered() iterNextFunc { } func (s *stringObject) ownKeys(all bool, accum []Value) []Value { - for i := int64(0); i < s.length; i++ { - accum = append(accum, asciiString(strconv.FormatInt(i, 10))) + for i := 0; i < s.length; i++ { + accum = append(accum, asciiString(strconv.Itoa(i))) } return s.baseObject.ownKeys(all, accum) } -func (s *stringObject) deleteStr(name string, throw bool) bool { - if i := strToIdx64(name); i >= 0 && i < s.length { +func (s *stringObject) deleteStr(name unistring.String, throw bool) bool { + if i := strToGoIdx(name); i >= 0 && i < s.length { s.val.runtime.typeErrorResult(throw, "Cannot delete property '%d' of a String", i) return false } @@ -231,16 +318,16 @@ func (s *stringObject) deleteStr(name string, throw bool) bool { func (s *stringObject) deleteIdx(idx valueInt, throw bool) bool { i := int64(idx) - if i >= 0 && i < s.length { + if i >= 0 && i < int64(s.length) { s.val.runtime.typeErrorResult(throw, "Cannot delete property '%d' of a String", i) return false } - return s.baseObject.deleteStr(idx.String(), throw) + return s.baseObject.deleteStr(idx.string(), throw) } -func (s *stringObject) hasOwnPropertyStr(name string) bool { - if i := strToIdx64(name); i >= 0 && i < s.length { +func (s *stringObject) hasOwnPropertyStr(name unistring.String) bool { + if i := strToGoIdx(name); i >= 0 && i < s.length { return true } return s.baseObject.hasOwnPropertyStr(name) @@ -248,8 +335,8 @@ func (s *stringObject) hasOwnPropertyStr(name string) bool { func (s *stringObject) hasOwnPropertyIdx(idx valueInt) bool { i := int64(idx) - if i >= 0 && i < s.length { + if i >= 0 && i < int64(s.length) { return true } - return s.baseObject.hasOwnPropertyStr(idx.String()) + return s.baseObject.hasOwnPropertyStr(idx.string()) } diff --git a/string_ascii.go b/string_ascii.go index b55dd7fb..ea4d92ec 100644 --- a/string_ascii.go +++ b/string_ascii.go @@ -2,12 +2,14 @@ package goja import ( "fmt" - "hash" + "hash/maphash" "io" "math" "reflect" "strconv" "strings" + + "github.com/dop251/goja/unistring" ) type asciiString string @@ -218,19 +220,19 @@ func (s asciiString) baseObject(r *Runtime) *Object { return ss.val } -func (s asciiString) hash(hash hash.Hash64) uint64 { - _, _ = hash.Write([]byte(s)) +func (s asciiString) hash(hash *maphash.Hash) uint64 { + _, _ = hash.WriteString(string(s)) h := hash.Sum64() hash.Reset() return h } -func (s asciiString) charAt(idx int64) rune { +func (s asciiString) charAt(idx int) rune { return rune(s[idx]) } -func (s asciiString) length() int64 { - return int64(len(s)) +func (s asciiString) length() int { + return len(s) } func (s asciiString) concat(other valueString) valueString { @@ -240,21 +242,21 @@ func (s asciiString) concat(other valueString) valueString { copy(b, s) copy(b[len(s):], other) return asciiString(b) - //return asciiString(string(s) + string(other)) case unicodeString: b := make([]uint16, len(s)+len(other)) + b[0] = unistring.BOM for i := 0; i < len(s); i++ { - b[i] = uint16(s[i]) + b[i+1] = uint16(s[i]) } - copy(b[len(s):], other) + copy(b[len(s)+1:], other[1:]) return unicodeString(b) default: - panic(fmt.Errorf("Unknown string type: %T", other)) + panic(fmt.Errorf("unknown string type: %T", other)) } } -func (s asciiString) substring(start, end int64) valueString { - return asciiString(s[start:end]) +func (s asciiString) substring(start, end int) valueString { + return s[start:end] } func (s asciiString) compareTo(other valueString) int { @@ -264,13 +266,13 @@ func (s asciiString) compareTo(other valueString) int { case unicodeString: return strings.Compare(string(s), other.String()) default: - panic(fmt.Errorf("Unknown string type: %T", other)) + panic(fmt.Errorf("unknown string type: %T", other)) } } -func (s asciiString) index(substr valueString, start int64) int64 { +func (s asciiString) index(substr valueString, start int) int { if substr, ok := substr.(asciiString); ok { - p := int64(strings.Index(string(s[start:]), string(substr))) + p := strings.Index(string(s[start:]), string(substr)) if p >= 0 { return p + start } @@ -278,16 +280,16 @@ func (s asciiString) index(substr valueString, start int64) int64 { return -1 } -func (s asciiString) lastIndex(substr valueString, pos int64) int64 { +func (s asciiString) lastIndex(substr valueString, pos int) int { if substr, ok := substr.(asciiString); ok { - end := pos + int64(len(substr)) + end := pos + len(substr) var ss string - if end > int64(len(s)) { + if end > len(s) { ss = string(s) } else { ss = string(s[:end]) } - return int64(strings.LastIndex(ss, string(substr))) + return strings.LastIndex(ss, string(substr)) } return -1 } @@ -304,6 +306,10 @@ func (s asciiString) toTrimmedUTF8() string { return strings.TrimSpace(string(s)) } +func (s asciiString) string() unistring.String { + return unistring.String(s) +} + func (s asciiString) Export() interface{} { return string(s) } diff --git a/string_unicode.go b/string_unicode.go index d20149fa..53238f48 100644 --- a/string_unicode.go +++ b/string_unicode.go @@ -3,17 +3,18 @@ package goja import ( "errors" "fmt" - "github.com/dop251/goja/parser" - "golang.org/x/text/cases" - "golang.org/x/text/language" - "hash" + "hash/maphash" "io" "math" "reflect" "strings" "unicode/utf16" "unicode/utf8" - "unsafe" + + "github.com/dop251/goja/parser" + "github.com/dop251/goja/unistring" + "golang.org/x/text/cases" + "golang.org/x/text/language" ) type unicodeString []uint16 @@ -27,6 +28,11 @@ type runeReaderReplace struct { wrapped io.RuneReader } +type unicodeStringBuilder struct { + buf []uint16 + unicode bool +} + var ( InvalidRuneError = errors.New("Invalid rune") ) @@ -43,33 +49,101 @@ func (rr runeReaderReplace) ReadRune() (r rune, size int, err error) { func (rr *unicodeRuneReader) ReadRune() (r rune, size int, err error) { if rr.pos < len(rr.s) { r = rune(rr.s[rr.pos]) - if r != utf8.RuneError { - if utf16.IsSurrogate(r) { - if rr.pos+1 < len(rr.s) { - r1 := utf16.DecodeRune(r, rune(rr.s[rr.pos+1])) + size++ + rr.pos++ + if isUTF16FirstSurrogate(r) { + if rr.pos < len(rr.s) { + second := rune(rr.s[rr.pos]) + if isUTF16SecondSurrogate(second) { + r = utf16.DecodeRune(r, second) size++ rr.pos++ - if r1 == utf8.RuneError { - err = InvalidRuneError - } else { - r = r1 - } } else { err = InvalidRuneError } + } else { + err = InvalidRuneError } + } else if isUTF16SecondSurrogate(r) { + err = InvalidRuneError } - size++ - rr.pos++ } else { err = io.EOF } return } +func (b *unicodeStringBuilder) grow(n int) { + if cap(b.buf)-len(b.buf) < n { + buf := make([]uint16, len(b.buf), 2*cap(b.buf)+n) + copy(buf, b.buf) + b.buf = buf + } +} + +func (b *unicodeStringBuilder) Grow(n int) { + b.grow(n + 1) +} + +func (b *unicodeStringBuilder) ensureStarted(initialSize int) { + b.grow(len(b.buf) + initialSize + 1) + if len(b.buf) == 0 { + b.buf = append(b.buf, unistring.BOM) + } +} + +func (b *unicodeStringBuilder) writeString(s valueString) { + b.ensureStarted(int(s.length())) + switch s := s.(type) { + case unicodeString: + b.buf = append(b.buf, s[1:]...) + b.unicode = true + case asciiString: + for i := 0; i < len(s); i++ { + b.buf = append(b.buf, uint16(s[i])) + } + default: + panic(fmt.Errorf("unsupported string type: %T", s)) + } +} + +func (b *unicodeStringBuilder) string() valueString { + if b.unicode { + return unicodeString(b.buf) + } + if len(b.buf) == 0 { + return stringEmpty + } + buf := make([]byte, 0, len(b.buf)-1) + for _, c := range b.buf[1:] { + buf = append(buf, byte(c)) + } + return asciiString(buf) +} + +func (b *unicodeStringBuilder) writeRune(r rune) { + if r <= 0xFFFF { + b.ensureStarted(1) + b.buf = append(b.buf, uint16(r)) + b.unicode = r >= utf8.RuneSelf + } else { + b.ensureStarted(2) + first, second := utf16.EncodeRune(r) + b.buf = append(b.buf, uint16(first), uint16(second)) + b.unicode = true + } +} + +func (b *unicodeStringBuilder) writeASCII(bytes []byte) { + b.ensureStarted(len(bytes)) + for _, c := range bytes { + b.buf = append(b.buf, uint16(c)) + } +} + func (s unicodeString) reader(start int) io.RuneReader { return &unicodeRuneReader{ - s: s[start:], + s: s[start+1:], } } @@ -150,18 +224,21 @@ func (s unicodeString) baseObject(r *Runtime) *Object { return ss.val } -func (s unicodeString) charAt(idx int64) rune { - return rune(s[idx]) +func (s unicodeString) charAt(idx int) rune { + return rune(s[idx+1]) } -func (s unicodeString) length() int64 { - return int64(len(s)) +func (s unicodeString) length() int { + return len(s) - 1 } func (s unicodeString) concat(other valueString) valueString { switch other := other.(type) { case unicodeString: - return unicodeString(append(s, other...)) + b := make(unicodeString, len(s)+len(other)-1) + copy(b, s) + copy(b[len(s):], other[1:]) + return b case asciiString: b := make([]uint16, len(s)+len(other)) copy(b, s) @@ -175,11 +252,14 @@ func (s unicodeString) concat(other valueString) valueString { } } -func (s unicodeString) substring(start, end int64) valueString { - ss := s[start:end] +func (s unicodeString) substring(start, end int) valueString { + ss := s[start+1 : end+1] for _, c := range ss { if c >= utf8.RuneSelf { - return unicodeString(ss) + b := make(unicodeString, end-start+1) + b[0] = unistring.BOM + copy(b[1:], ss) + return b } } as := make([]byte, end-start) @@ -190,32 +270,32 @@ func (s unicodeString) substring(start, end int64) valueString { } func (s unicodeString) String() string { - return string(utf16.Decode(s)) + return string(utf16.Decode(s[1:])) } func (s unicodeString) compareTo(other valueString) int { return strings.Compare(s.String(), other.String()) } -func (s unicodeString) index(substr valueString, start int64) int64 { +func (s unicodeString) index(substr valueString, start int) int { var ss []uint16 switch substr := substr.(type) { case unicodeString: - ss = substr + ss = substr[1:] case asciiString: ss = make([]uint16, len(substr)) for i := 0; i < len(substr); i++ { ss[i] = uint16(substr[i]) } default: - panic(fmt.Errorf("Unknown string type: %T", substr)) + panic(fmt.Errorf("unknown string type: %T", substr)) } - + s1 := s[1:] // TODO: optimise - end := int64(len(s) - len(ss)) + end := len(s1) - len(ss) for start <= end { - for i := int64(0); i < int64(len(ss)); i++ { - if s[start+i] != ss[i] { + for i := 0; i < len(ss); i++ { + if s1[start+i] != ss[i] { goto nomatch } } @@ -227,11 +307,11 @@ func (s unicodeString) index(substr valueString, start int64) int64 { return -1 } -func (s unicodeString) lastIndex(substr valueString, start int64) int64 { +func (s unicodeString) lastIndex(substr valueString, start int) int { var ss []uint16 switch substr := substr.(type) { case unicodeString: - ss = substr + ss = substr[1:] case asciiString: ss = make([]uint16, len(substr)) for i := 0; i < len(substr); i++ { @@ -241,13 +321,14 @@ func (s unicodeString) lastIndex(substr valueString, start int64) int64 { panic(fmt.Errorf("Unknown string type: %T", substr)) } - if maxStart := int64(len(s) - len(ss)); start > maxStart { + s1 := s[1:] + if maxStart := len(s1) - len(ss); start > maxStart { start = maxStart } // TODO: optimise for start >= 0 { - for i := int64(0); i < int64(len(ss)); i++ { - if s[start+i] != ss[i] { + for i := 0; i < len(ss); i++ { + if s1[start+i] != ss[i] { goto nomatch } } @@ -259,6 +340,10 @@ func (s unicodeString) lastIndex(substr valueString, start int64) int64 { return -1 } +func unicodeStringFromRunes(r []rune) unicodeString { + return unistring.NewFromRunes(r).AsUtf16() +} + func (s unicodeString) toLower() valueString { caser := cases.Lower(language.Und) r := []rune(caser.String(s.String())) @@ -279,7 +364,7 @@ func (s unicodeString) toLower() valueString { if ascii { return asciiString(r) } - return unicodeString(utf16.Encode(r)) + return unicodeStringFromRunes(r) } func (s unicodeString) toUpper() valueString { @@ -295,9 +380,13 @@ func (s unicodeString) ExportType() reflect.Type { return reflectTypeString } -func (s unicodeString) hash(hash hash.Hash64) uint64 { - _, _ = hash.Write(*(*[]byte)(unsafe.Pointer(&s))) +func (s unicodeString) hash(hash *maphash.Hash) uint64 { + _, _ = hash.WriteString(string(unistring.FromUtf16(s))) h := hash.Sum64() hash.Reset() return h } + +func (s unicodeString) string() unistring.String { + return unistring.FromUtf16(s) +} diff --git a/tc39_test.go b/tc39_test.go index 090c2b6d..a704ce9c 100644 --- a/tc39_test.go +++ b/tc39_test.go @@ -7,9 +7,11 @@ import ( "io/ioutil" "os" "path" + "sort" "strings" "sync" "testing" + "time" ) const ( @@ -19,7 +21,7 @@ const ( var ( invalidFormatError = errors.New("Invalid file format") - ignorableTestError = &valueSymbol{} + ignorableTestError = newSymbol(stringEmpty) sabStub = MustCompile("sabStub.js", ` Object.defineProperty(this, "SharedArrayBuffer", { @@ -32,18 +34,11 @@ var ( var ( skipList = map[string]bool{ - "test/language/literals/regexp/S7.8.5_A1.1_T2.js": true, // UTF-16 - "test/language/literals/regexp/S7.8.5_A1.4_T2.js": true, // UTF-16 - "test/language/literals/regexp/S7.8.5_A2.1_T2.js": true, // UTF-16 - "test/language/literals/regexp/S7.8.5_A2.4_T2.js": true, // UTF-16 "test/built-ins/Date/prototype/toISOString/15.9.5.43-0-8.js": true, // timezone "test/built-ins/Date/prototype/toISOString/15.9.5.43-0-9.js": true, // timezone "test/built-ins/Date/prototype/toISOString/15.9.5.43-0-10.js": true, // timezone "test/annexB/built-ins/escape/escape-above-astral.js": true, // \u{xxxxx} - // utf-16 - "test/built-ins/Array/prototype/concat/Array.prototype.concat_spreadable-string-wrapper.js": true, - // class "test/language/statements/class/subclass/builtin-objects/Symbol/symbol-valid-as-extends-value.js": true, "test/language/statements/class/subclass/builtin-objects/Symbol/new-symbol-with-super-throws.js": true, @@ -63,12 +58,16 @@ var ( "test/language/statements/class/subclass/builtin-objects/TypedArray/regular-subclassing.js": true, "test/language/statements/class/subclass/builtin-objects/DataView/super-must-be-called.js": true, "test/language/statements/class/subclass/builtin-objects/DataView/regular-subclassing.js": true, + "test/language/statements/class/subclass/builtin-objects/String/super-must-be-called.js": true, + "test/language/statements/class/subclass/builtin-objects/String/regular-subclassing.js": true, + "test/language/statements/class/subclass/builtin-objects/String/length.js": true, // full unicode regexp flag "test/built-ins/RegExp/prototype/Symbol.match/u-advance-after-empty.js": true, "test/built-ins/RegExp/prototype/Symbol.match/get-unicode-error.js": true, "test/built-ins/RegExp/prototype/Symbol.match/builtin-success-u-return-val-groups.js": true, "test/built-ins/RegExp/prototype/Symbol.match/builtin-infer-unicode.js": true, + "test/built-ins/RegExp/unicode_identity_escape.js": true, // object literals "test/built-ins/Array/from/source-object-iterator-1.js": true, @@ -86,6 +85,12 @@ var ( // arrow-function "test/built-ins/Object/prototype/toString/proxy-function.js": true, + + // template strings + "test/built-ins/String/raw/zero-literal-segments.js": true, + "test/built-ins/String/raw/template-substitutions-are-appended-on-same-index.js": true, + "test/built-ins/String/raw/special-characters.js": true, + "test/built-ins/String/raw/return-the-string-value-from-template.js": true, } featuresBlackList = []string{ @@ -101,9 +106,7 @@ var ( "12.9.4", "19.1", "19.4", - "21.1.3.14", - "21.1.3.15", - "21.1.3.17", + "21.1", "21.2.5.6", "22.1.2.1", "22.1.2.3", @@ -126,6 +129,7 @@ var ( esIdPrefixWhiteList = []string{ "sec-array.prototype.includes", "sec-%typedarray%", + "sec-string.prototype", } ) @@ -134,11 +138,21 @@ type tc39Test struct { f func(t *testing.T) } +type tc39BenchmarkItem struct { + name string + duration time.Duration +} + +type tc39BenchmarkData []tc39BenchmarkItem + type tc39TestCtx struct { base string t *testing.T prgCache map[string]*Program prgCacheLock sync.Mutex + enableBench bool + benchmark tc39BenchmarkData + benchLock sync.Mutex testQueue []tc39Test } @@ -334,6 +348,11 @@ func (ctx *tc39TestCtx) runTC39File(name string, t testing.TB) { } } + var startTime time.Time + if ctx.enableBench { + startTime = time.Now() + } + hasRaw := meta.hasFlag("raw") if hasRaw || !meta.hasFlag("onlyStrict") { @@ -348,6 +367,15 @@ func (ctx *tc39TestCtx) runTC39File(name string, t testing.TB) { ctx.runTC39Test(name, "'use strict';\n"+src, meta, t) } + if ctx.enableBench { + ctx.benchLock.Lock() + ctx.benchmark = append(ctx.benchmark, tc39BenchmarkItem{ + name: name, + duration: time.Since(startTime), + }) + ctx.benchLock.Unlock() + } + } func (ctx *tc39TestCtx) init() { @@ -459,32 +487,48 @@ func TestTC39(t *testing.T) { ctx := &tc39TestCtx{ base: tc39BASE, - t: t, } ctx.init() - - //ctx.runTC39File("test/language/types/number/8.5.1.js", t) - //ctx.runTC39Tests("test/language") - ctx.runTC39Tests("test/language/expressions") - ctx.runTC39Tests("test/language/arguments-object") - ctx.runTC39Tests("test/language/asi") - ctx.runTC39Tests("test/language/directive-prologue") - ctx.runTC39Tests("test/language/function-code") - ctx.runTC39Tests("test/language/eval-code") - ctx.runTC39Tests("test/language/global-code") - ctx.runTC39Tests("test/language/identifier-resolution") - ctx.runTC39Tests("test/language/identifiers") - //ctx.runTC39Tests("test/language/literals") // octal sequences in strict mode - ctx.runTC39Tests("test/language/punctuators") - ctx.runTC39Tests("test/language/reserved-words") - ctx.runTC39Tests("test/language/source-text") - ctx.runTC39Tests("test/language/statements") - ctx.runTC39Tests("test/language/types") - ctx.runTC39Tests("test/language/white-space") - ctx.runTC39Tests("test/built-ins") - ctx.runTC39Tests("test/annexB/built-ins/String/prototype/substr") - ctx.runTC39Tests("test/annexB/built-ins/escape") - ctx.runTC39Tests("test/annexB/built-ins/unescape") - - ctx.flush() + //ctx.enableBench = true + + t.Run("tc39", func(t *testing.T) { + ctx.t = t + //ctx.runTC39File("test/language/types/number/8.5.1.js", t) + //ctx.runTC39Tests("test/language") + ctx.runTC39Tests("test/language/expressions") + ctx.runTC39Tests("test/language/arguments-object") + ctx.runTC39Tests("test/language/asi") + ctx.runTC39Tests("test/language/directive-prologue") + ctx.runTC39Tests("test/language/function-code") + ctx.runTC39Tests("test/language/eval-code") + ctx.runTC39Tests("test/language/global-code") + ctx.runTC39Tests("test/language/identifier-resolution") + ctx.runTC39Tests("test/language/identifiers") + //ctx.runTC39Tests("test/language/literals") // octal sequences in strict mode + ctx.runTC39Tests("test/language/punctuators") + ctx.runTC39Tests("test/language/reserved-words") + ctx.runTC39Tests("test/language/source-text") + ctx.runTC39Tests("test/language/statements") + ctx.runTC39Tests("test/language/types") + ctx.runTC39Tests("test/language/white-space") + ctx.runTC39Tests("test/built-ins") + ctx.runTC39Tests("test/annexB/built-ins/String/prototype/substr") + ctx.runTC39Tests("test/annexB/built-ins/escape") + ctx.runTC39Tests("test/annexB/built-ins/unescape") + + ctx.flush() + }) + + if ctx.enableBench { + sort.Slice(ctx.benchmark, func(i, j int) bool { + return ctx.benchmark[i].duration > ctx.benchmark[j].duration + }) + bench := ctx.benchmark + if len(bench) > 50 { + bench = bench[:50] + } + for _, item := range bench { + fmt.Printf("%s\t%d\n", item.name, item.duration/time.Millisecond) + } + } } diff --git a/typedarrays.go b/typedarrays.go index 885bf119..9a10b0a8 100644 --- a/typedarrays.go +++ b/typedarrays.go @@ -6,6 +6,8 @@ import ( "reflect" "strconv" "unsafe" + + "github.com/dop251/goja/unistring" ) type byteOrder bool @@ -458,15 +460,15 @@ func (a *typedArrayObject) _getIdx(idx int) Value { return nil } -func strToTAIdx(s string) (int, bool) { - i, err := strconv.ParseInt(s, 10, bits.UintSize) +func strToTAIdx(s unistring.String) (int, bool) { + i, err := strconv.ParseInt(string(s), 10, bits.UintSize) if err != nil { return 0, false } return int(i), true } -func (a *typedArrayObject) getOwnPropStr(name string) Value { +func (a *typedArrayObject) getOwnPropStr(name unistring.String) Value { if idx, ok := strToTAIdx(name); ok { v := a._getIdx(idx) if v != nil { @@ -493,7 +495,7 @@ func (a *typedArrayObject) getOwnPropIdx(idx valueInt) Value { return nil } -func (a *typedArrayObject) getStr(name string, receiver Value) Value { +func (a *typedArrayObject) getStr(name unistring.String, receiver Value) Value { if idx, ok := strToTAIdx(name); ok { prop := a._getIdx(idx) if prop == nil { @@ -538,7 +540,7 @@ func (a *typedArrayObject) _hasIdx(idx int) bool { return idx >= 0 && idx < a.length } -func (a *typedArrayObject) setOwnStr(p string, v Value, throw bool) bool { +func (a *typedArrayObject) setOwnStr(p unistring.String, v Value, throw bool) bool { if idx, ok := strToTAIdx(p); ok { return a._putIdx(idx, v, throw) } @@ -549,7 +551,7 @@ func (a *typedArrayObject) setOwnIdx(p valueInt, v Value, throw bool) bool { return a._putIdx(toInt(int64(p)), v, throw) } -func (a *typedArrayObject) setForeignStr(p string, v, receiver Value, throw bool) (res bool, handled bool) { +func (a *typedArrayObject) setForeignStr(p unistring.String, v, receiver Value, throw bool) (res bool, handled bool) { return a._setForeignStr(p, a.getOwnPropStr(p), v, receiver, throw) } @@ -557,7 +559,7 @@ func (a *typedArrayObject) setForeignIdx(p valueInt, v, receiver Value, throw bo return a._setForeignIdx(p, trueValIfPresent(a.hasOwnPropertyIdx(p)), v, receiver, throw) } -func (a *typedArrayObject) hasOwnPropertyStr(name string) bool { +func (a *typedArrayObject) hasOwnPropertyStr(name unistring.String) bool { if idx, ok := strToTAIdx(name); ok { a.viewedArrayBuf.ensureNotDetached() return idx < a.length @@ -571,14 +573,14 @@ func (a *typedArrayObject) hasOwnPropertyIdx(idx valueInt) bool { } func (a *typedArrayObject) _defineIdxProperty(idx int, desc PropertyDescriptor, throw bool) bool { - prop, ok := a._defineOwnProperty(strconv.Itoa(idx), a.getOwnPropIdx(valueInt(idx)), desc, throw) + prop, ok := a._defineOwnProperty(unistring.String(strconv.Itoa(idx)), a.getOwnPropIdx(valueInt(idx)), desc, throw) if ok { return a._putIdx(idx, prop, throw) } return ok } -func (a *typedArrayObject) defineOwnPropertyStr(name string, desc PropertyDescriptor, throw bool) bool { +func (a *typedArrayObject) defineOwnPropertyStr(name unistring.String, desc PropertyDescriptor, throw bool) bool { if idx, ok := strToTAIdx(name); ok { return a._defineIdxProperty(idx, desc, throw) } @@ -589,7 +591,7 @@ func (a *typedArrayObject) defineOwnPropertyIdx(name valueInt, desc PropertyDesc return a._defineIdxProperty(toInt(int64(name)), desc, throw) } -func (a *typedArrayObject) deleteStr(name string, throw bool) bool { +func (a *typedArrayObject) deleteStr(name unistring.String, throw bool) bool { if idx, ok := strToTAIdx(name); ok { if idx < a.length { a.val.runtime.typeErrorResult(throw, "Cannot delete property '%d' of %s", idx, a.val.String()) @@ -627,7 +629,7 @@ func (i *typedArrayPropIter) next() (propIterItem, iterNextFunc) { name := strconv.Itoa(i.idx) prop := i.a._getIdx(i.idx) i.idx++ - return propIterItem{name: name, value: prop}, i.next + return propIterItem{name: unistring.String(name), value: prop}, i.next } return i.a.baseObject.enumerateUnfiltered()() diff --git a/unistring/string.go b/unistring/string.go new file mode 100644 index 00000000..481f06cf --- /dev/null +++ b/unistring/string.go @@ -0,0 +1,122 @@ +// Package unistring contains an implementation of a hybrid ASCII/UTF-16 string. +// For ASCII strings the underlying representation is equivalent to a normal Go string. +// For unicode strings the underlying representation is UTF-16 as []uint16 with 0th element set to 0xFEFF. +// unicode.String allows representing malformed UTF-16 values (e.g. stand-alone parts of surrogate pairs) +// which cannot be represented in UTF-8. +// At the same time it is possible to use unicode.String as property keys just as efficiently as simple strings, +// (the leading 0xFEFF ensures there is no clash with ASCII string), and it is possible to convert it +// to valueString without extra allocations. +package unistring + +import ( + "reflect" + "unicode/utf16" + "unicode/utf8" + "unsafe" +) + +const ( + BOM = 0xFEFF +) + +type String string + +func NewFromString(s string) String { + ascii := true + size := 0 + for _, c := range s { + if c >= utf8.RuneSelf { + ascii = false + if c > 0xFFFF { + size++ + } + } + size++ + } + if ascii { + return String(s) + } + b := make([]uint16, size+1) + b[0] = BOM + i := 1 + for _, c := range s { + if c <= 0xFFFF { + b[i] = uint16(c) + } else { + first, second := utf16.EncodeRune(c) + b[i] = uint16(first) + i++ + b[i] = uint16(second) + } + i++ + } + return FromUtf16(b) +} + +func NewFromRunes(s []rune) String { + ascii := true + size := 0 + for _, c := range s { + if c >= utf8.RuneSelf { + ascii = false + if c > 0xFFFF { + size++ + } + } + size++ + } + if ascii { + return String(s) + } + b := make([]uint16, size+1) + b[0] = BOM + i := 1 + for _, c := range s { + if c <= 0xFFFF { + b[i] = uint16(c) + } else { + first, second := utf16.EncodeRune(c) + b[i] = uint16(first) + i++ + b[i] = uint16(second) + } + i++ + } + return FromUtf16(b) +} + +func FromUtf16(b []uint16) String { + var str string + hdr := (*reflect.StringHeader)(unsafe.Pointer(&str)) + hdr.Data = uintptr(unsafe.Pointer(&b[0])) + hdr.Len = len(b) * 2 + + return String(str) +} + +func (s String) String() string { + if b := s.AsUtf16(); b != nil { + return string(utf16.Decode(b[1:])) + } + + return string(s) +} + +func (s String) AsUtf16() []uint16 { + if len(s) < 4 || len(s)&1 != 0 { + return nil + } + l := len(s) / 2 + raw := string(s) + hdr := (*reflect.StringHeader)(unsafe.Pointer(&raw)) + a := *(*[]uint16)(unsafe.Pointer(&reflect.SliceHeader{ + Data: hdr.Data, + Len: l, + Cap: l, + })) + if a[0] == BOM { + return a + } + + return nil +} diff --git a/value.go b/value.go index 18e864ec..969bca86 100644 --- a/value.go +++ b/value.go @@ -1,13 +1,14 @@ package goja import ( - "fmt" - "hash" + "hash/maphash" "math" "reflect" "regexp" "strconv" "unsafe" + + "github.com/dop251/goja/unistring" ) var ( @@ -39,6 +40,7 @@ var intCache [256]Value type Value interface { ToInteger() int64 toString() valueString + string() unistring.String ToPrimitiveString() Value String() string ToFloat() float64 @@ -53,7 +55,7 @@ type Value interface { baseObject(r *Runtime) *Object - hash(hash64 hash.Hash64) uint64 + hash(hasher *maphash.Hash) uint64 } type valueContainer interface { @@ -71,12 +73,12 @@ type valueUndefined struct { valueNull } type valueSymbol struct { - desc string + desc valueString } type valueUnresolved struct { r *Runtime - ref string + ref unistring.String } type memberUnresolved struct { @@ -127,6 +129,10 @@ func (i valueInt) toString() valueString { return asciiString(i.String()) } +func (i valueInt) string() unistring.String { + return unistring.String(i.String()) +} + func (i valueInt) ToPrimitiveString() Value { return i } @@ -195,60 +201,64 @@ func (i valueInt) ExportType() reflect.Type { return reflectTypeInt } -func (i valueInt) hash(hash.Hash64) uint64 { +func (i valueInt) hash(*maphash.Hash) uint64 { return uint64(i) } -func (o valueBool) ToInteger() int64 { - if o { +func (b valueBool) ToInteger() int64 { + if b { return 1 } return 0 } -func (o valueBool) toString() valueString { - if o { +func (b valueBool) toString() valueString { + if b { return stringTrue } return stringFalse } -func (o valueBool) ToPrimitiveString() Value { - return o +func (b valueBool) ToPrimitiveString() Value { + return b } -func (o valueBool) String() string { - if o { +func (b valueBool) String() string { + if b { return "true" } return "false" } -func (o valueBool) ToFloat() float64 { - if o { +func (b valueBool) string() unistring.String { + return unistring.String(b.String()) +} + +func (b valueBool) ToFloat() float64 { + if b { return 1.0 } return 0 } -func (o valueBool) ToBoolean() bool { - return bool(o) +func (b valueBool) ToBoolean() bool { + return bool(b) } -func (o valueBool) ToObject(r *Runtime) *Object { - return r.newPrimitiveObject(o, r.global.BooleanPrototype, "Boolean") +func (b valueBool) ToObject(r *Runtime) *Object { + return r.newPrimitiveObject(b, r.global.BooleanPrototype, "Boolean") } -func (o valueBool) ToNumber() Value { - if o { +func (b valueBool) ToNumber() Value { + if b { return valueInt(1) } return valueInt(0) } -func (o valueBool) SameAs(other Value) bool { +func (b valueBool) SameAs(other Value) bool { if other, ok := other.(valueBool); ok { - return o == other + return b == other } return false } @@ -266,26 +276,26 @@ func (b valueBool) Equals(other Value) bool { } -func (o valueBool) StrictEquals(other Value) bool { +func (b valueBool) StrictEquals(other Value) bool { if other, ok := other.(valueBool); ok { - return o == other + return b == other } return false } -func (o valueBool) baseObject(r *Runtime) *Object { +func (b valueBool) baseObject(r *Runtime) *Object { return r.global.BooleanPrototype } -func (o valueBool) Export() interface{} { - return bool(o) +func (b valueBool) Export() interface{} { + return bool(b) } -func (o valueBool) ExportType() reflect.Type { +func (b valueBool) ExportType() reflect.Type { return reflectTypeBool } -func (b valueBool) hash(hash.Hash64) uint64 { +func (b valueBool) hash(*maphash.Hash) uint64 { if b { return uint64(uintptr(unsafe.Pointer(&valueTrue))) } @@ -300,6 +310,10 @@ func (n valueNull) toString() valueString { return stringNull } +func (n valueNull) string() unistring.String { + return stringNull.string() +} + func (n valueNull) ToPrimitiveString() Value { return n } @@ -320,6 +334,10 @@ func (u valueUndefined) String() string { return "undefined" } +func (u valueUndefined) string() unistring.String { + return "undefined" +} + func (u valueUndefined) ToNumber() Value { return _NaN } @@ -338,7 +356,7 @@ func (u valueUndefined) ToFloat() float64 { return math.NaN() } -func (u valueUndefined) hash(hash.Hash64) uint64 { +func (u valueUndefined) hash(*maphash.Hash) uint64 { return uint64(uintptr(unsafe.Pointer(&_undefined))) } @@ -390,7 +408,7 @@ func (n valueNull) ExportType() reflect.Type { return reflectTypeNil } -func (n valueNull) hash(hash.Hash64) uint64 { +func (n valueNull) hash(*maphash.Hash) uint64 { return uint64(uintptr(unsafe.Pointer(&_null))) } @@ -402,6 +420,10 @@ func (p *valueProperty) toString() valueString { return stringEmpty } +func (p *valueProperty) string() unistring.String { + return "" +} + func (p *valueProperty) ToPrimitiveString() Value { return _undefined } @@ -470,20 +492,20 @@ func (p *valueProperty) StrictEquals(Value) bool { return false } -func (n *valueProperty) baseObject(r *Runtime) *Object { +func (p *valueProperty) baseObject(r *Runtime) *Object { r.typeErrorResult(true, "BUG: baseObject() is called on valueProperty") // TODO error message return nil } -func (n *valueProperty) Export() interface{} { +func (p *valueProperty) Export() interface{} { panic("Cannot export valueProperty") } -func (n *valueProperty) ExportType() reflect.Type { +func (p *valueProperty) ExportType() reflect.Type { panic("Cannot export valueProperty") } -func (n *valueProperty) hash(hash.Hash64) uint64 { +func (p *valueProperty) hash(*maphash.Hash) uint64 { panic("valueProperty should never be used in maps or sets") } @@ -503,6 +525,10 @@ func (f valueFloat) toString() valueString { return asciiString(f.String()) } +func (f valueFloat) string() unistring.String { + return unistring.String(f.String()) +} + func (f valueFloat) ToPrimitiveString() Value { return f } @@ -608,7 +634,7 @@ func (f valueFloat) ExportType() reflect.Type { return reflectTypeFloat } -func (f valueFloat) hash(hash.Hash64) uint64 { +func (f valueFloat) hash(*maphash.Hash) uint64 { if f == _negativeZero { return 0 } @@ -623,6 +649,10 @@ func (o *Object) toString() valueString { return o.self.toPrimitiveString().toString() } +func (o *Object) string() unistring.String { + return o.self.toPrimitiveString().string() +} + func (o *Object) ToPrimitiveString() Value { return o.self.toPrimitiveString().ToPrimitiveString() } @@ -688,12 +718,12 @@ func (o *Object) ExportType() reflect.Type { return o.self.exportType() } -func (o *Object) hash(hash.Hash64) uint64 { +func (o *Object) hash(*maphash.Hash) uint64 { return uint64(uintptr(unsafe.Pointer(o))) } func (o *Object) Get(name string) Value { - return o.self.getStr(name, nil) + return o.self.getStr(unistring.NewFromString(name), nil) } func (o *Object) Keys() (keys []string) { @@ -710,7 +740,7 @@ func (o *Object) Keys() (keys []string) { // configurable: configurable, enumerable: enumerable}) func (o *Object) DefineDataProperty(name string, value Value, writable, configurable, enumerable Flag) error { return tryFunc(func() { - o.self.defineOwnPropertyStr(name, PropertyDescriptor{ + o.self.defineOwnPropertyStr(unistring.NewFromString(name), PropertyDescriptor{ Value: value, Writable: writable, Configurable: configurable, @@ -723,7 +753,7 @@ func (o *Object) DefineDataProperty(name string, value Value, writable, configur // configurable: configurable, enumerable: enumerable}) func (o *Object) DefineAccessorProperty(name string, getter, setter Value, configurable, enumerable Flag) error { return tryFunc(func() { - o.self.defineOwnPropertyStr(name, PropertyDescriptor{ + o.self.defineOwnPropertyStr(unistring.NewFromString(name), PropertyDescriptor{ Getter: getter, Setter: setter, Configurable: configurable, @@ -734,7 +764,7 @@ func (o *Object) DefineAccessorProperty(name string, getter, setter Value, confi func (o *Object) Set(name string, value interface{}) error { return tryFunc(func() { - o.self.setOwnStr(name, o.runtime.ToValue(value), true) + o.self.setOwnStr(unistring.NewFromString(name), o.runtime.ToValue(value), true) }) } @@ -774,6 +804,11 @@ func (o valueUnresolved) toString() valueString { return nil } +func (o valueUnresolved) string() unistring.String { + o.throw() + return "" +} + func (o valueUnresolved) ToPrimitiveString() Value { o.throw() return nil @@ -834,7 +869,7 @@ func (o valueUnresolved) ExportType() reflect.Type { return nil } -func (o valueUnresolved) hash(hash.Hash64) uint64 { +func (o valueUnresolved) hash(*maphash.Hash) uint64 { o.throw() return 0 } @@ -852,7 +887,11 @@ func (s *valueSymbol) ToPrimitiveString() Value { } func (s *valueSymbol) String() string { - return s.descString() + return s.desc.String() +} + +func (s *valueSymbol) string() unistring.String { + return s.desc.string() } func (s *valueSymbol) ToFloat() float64 { @@ -898,12 +937,14 @@ func (s *valueSymbol) baseObject(r *Runtime) *Object { return r.newPrimitiveObject(s, r.global.SymbolPrototype, "Symbol") } -func (s *valueSymbol) hash(hash.Hash64) uint64 { +func (s *valueSymbol) hash(*maphash.Hash) uint64 { return uint64(uintptr(unsafe.Pointer(s))) } -func (s *valueSymbol) descString() string { - return fmt.Sprintf("Symbol(%s)", s.desc) +func newSymbol(s valueString) *valueSymbol { + return &valueSymbol{ + desc: asciiString("Symbol(").concat(s).concat(asciiString(")")), + } } func init() { diff --git a/vm.go b/vm.go index 65199369..eff4b4c3 100644 --- a/vm.go +++ b/vm.go @@ -7,6 +7,8 @@ import ( "strconv" "sync" "sync/atomic" + + "github.com/dop251/goja/unistring" ) const ( @@ -18,7 +20,7 @@ type valueStack []Value type stash struct { values valueStack extraArgs valueStack - names map[string]uint32 + names map[unistring.String]uint32 obj objectImpl outer *stash @@ -26,7 +28,7 @@ type stash struct { type context struct { prg *Program - funcName string + funcName unistring.String stash *stash newTarget Value pc, sb int @@ -42,12 +44,12 @@ type iterStackItem struct { type ref interface { get() Value set(Value) - refname() string + refname() unistring.String } type stashRef struct { v *Value - n string + n unistring.String } func (r stashRef) get() Value { @@ -58,13 +60,13 @@ func (r *stashRef) set(v Value) { *r.v = v } -func (r *stashRef) refname() string { +func (r *stashRef) refname() unistring.String { return r.n } type objRef struct { base objectImpl - name string + name unistring.String strict bool } @@ -76,13 +78,13 @@ func (r *objRef) set(v Value) { r.base.setOwnStr(r.name, v, r.strict) } -func (r *objRef) refname() string { +func (r *objRef) refname() unistring.String { return r.name } type unresolvedRef struct { runtime *Runtime - name string + name unistring.String } func (r *unresolvedRef) get() Value { @@ -94,14 +96,14 @@ func (r *unresolvedRef) set(Value) { r.get() } -func (r *unresolvedRef) refname() string { +func (r *unresolvedRef) refname() unistring.String { return r.name } type vm struct { r *Runtime prg *Program - funcName string + funcName unistring.String pc int stack valueStack sp, sb, args int @@ -201,7 +203,7 @@ func (s *valueStack) expand(idx int) { } } -func stashObjHas(obj objectImpl, name string) bool { +func stashObjHas(obj objectImpl, name unistring.String) bool { if obj.hasPropertyStr(name) { if unscopables, ok := obj.getSym(symUnscopables, nil).(*Object); ok { if b := unscopables.self.getStr(name, nil); b != nil { @@ -213,7 +215,7 @@ func stashObjHas(obj objectImpl, name string) bool { return false } -func (s *stash) put(name string, v Value) bool { +func (s *stash) put(name unistring.String, v Value) bool { if s.obj != nil { if stashObjHas(s.obj, name) { s.obj.setOwnStr(name, v, false) @@ -245,7 +247,7 @@ func (s *stash) getByIdx(idx uint32) Value { return _undefined } -func (s *stash) getByName(name string, _ *vm) (v Value, exists bool) { +func (s *stash) getByName(name unistring.String, _ *vm) (v Value, exists bool) { if s.obj != nil { if stashObjHas(s.obj, name) { return nilSafe(s.obj.getStr(name, nil)), true @@ -259,9 +261,9 @@ func (s *stash) getByName(name string, _ *vm) (v Value, exists bool) { //return valueUnresolved{r: vm.r, ref: name}, false } -func (s *stash) createBinding(name string) { +func (s *stash) createBinding(name unistring.String) { if s.names == nil { - s.names = make(map[string]uint32) + s.names = make(map[unistring.String]uint32) } if _, exists := s.names[name]; !exists { s.names[name] = uint32(len(s.names)) @@ -269,7 +271,7 @@ func (s *stash) createBinding(name string) { } } -func (s *stash) deleteBinding(name string) bool { +func (s *stash) deleteBinding(name unistring.String) bool { if s.obj != nil { if stashObjHas(s.obj, name) { return s.obj.deleteStr(name, false) @@ -1034,11 +1036,11 @@ func (_deleteElemStrict) exec(vm *vm) { vm.pc++ } -type deleteProp string +type deleteProp unistring.String func (d deleteProp) exec(vm *vm) { obj := vm.r.toObject(vm.stack[vm.sp-1]) - if obj.self.deleteStr(string(d), false) { + if obj.self.deleteStr(unistring.String(d), false) { vm.stack[vm.sp-1] = valueTrue } else { vm.stack[vm.sp-1] = valueFalse @@ -1046,42 +1048,42 @@ func (d deleteProp) exec(vm *vm) { vm.pc++ } -type deletePropStrict string +type deletePropStrict unistring.String func (d deletePropStrict) exec(vm *vm) { obj := vm.r.toObject(vm.stack[vm.sp-1]) - obj.self.deleteStr(string(d), true) + obj.self.deleteStr(unistring.String(d), true) vm.stack[vm.sp-1] = valueTrue vm.pc++ } -type setProp string +type setProp unistring.String func (p setProp) exec(vm *vm) { val := vm.stack[vm.sp-1] - vm.stack[vm.sp-2].ToObject(vm.r).self.setOwnStr(string(p), val, false) + vm.stack[vm.sp-2].ToObject(vm.r).self.setOwnStr(unistring.String(p), val, false) vm.stack[vm.sp-2] = val vm.sp-- vm.pc++ } -type setPropStrict string +type setPropStrict unistring.String func (p setPropStrict) exec(vm *vm) { obj := vm.stack[vm.sp-2] val := vm.stack[vm.sp-1] obj1 := vm.r.toObject(obj) - obj1.self.setOwnStr(string(p), val, true) + obj1.self.setOwnStr(unistring.String(p), val, true) vm.stack[vm.sp-2] = val vm.sp-- vm.pc++ } -type setProp1 string +type setProp1 unistring.String func (p setProp1) exec(vm *vm) { - vm.r.toObject(vm.stack[vm.sp-2]).self._putProp(string(p), vm.stack[vm.sp-1], true, true, true) + vm.r.toObject(vm.stack[vm.sp-2]).self._putProp(unistring.String(p), vm.stack[vm.sp-1], true, true, true) vm.sp-- vm.pc++ @@ -1098,7 +1100,7 @@ func (_setProto) exec(vm *vm) { vm.pc++ } -type setPropGetter string +type setPropGetter unistring.String func (s setPropGetter) exec(vm *vm) { obj := vm.r.toObject(vm.stack[vm.sp-2]) @@ -1110,13 +1112,13 @@ func (s setPropGetter) exec(vm *vm) { Enumerable: FLAG_TRUE, } - obj.self.defineOwnPropertyStr(string(s), descr, false) + obj.self.defineOwnPropertyStr(unistring.String(s), descr, false) vm.sp-- vm.pc++ } -type setPropSetter string +type setPropSetter unistring.String func (s setPropSetter) exec(vm *vm) { obj := vm.r.toObject(vm.stack[vm.sp-2]) @@ -1128,13 +1130,13 @@ func (s setPropSetter) exec(vm *vm) { Enumerable: FLAG_TRUE, } - obj.self.defineOwnPropertyStr(string(s), descr, false) + obj.self.defineOwnPropertyStr(unistring.String(s), descr, false) vm.sp-- vm.pc++ } -type getProp string +type getProp unistring.String func (g getProp) exec(vm *vm) { v := vm.stack[vm.sp-1] @@ -1142,22 +1144,23 @@ func (g getProp) exec(vm *vm) { if obj == nil { panic(vm.r.NewTypeError("Cannot read property '%s' of undefined", g)) } - vm.stack[vm.sp-1] = nilSafe(obj.self.getStr(string(g), v)) + vm.stack[vm.sp-1] = nilSafe(obj.self.getStr(unistring.String(g), v)) vm.pc++ } -type getPropCallee string +type getPropCallee unistring.String func (g getPropCallee) exec(vm *vm) { v := vm.stack[vm.sp-1] obj := v.baseObject(vm.r) + n := unistring.String(g) if obj == nil { - panic(vm.r.NewTypeError("Cannot read property '%s' of undefined or null", g)) + panic(vm.r.NewTypeError("Cannot read property '%s' of undefined or null", n)) } - prop := obj.self.getStr(string(g), v) + prop := obj.self.getStr(n, v) if prop == nil { - prop = memberUnresolved{valueUnresolved{r: vm.r, ref: string(g)}} + prop = memberUnresolved{valueUnresolved{r: vm.r, ref: n}} } vm.stack[vm.sp-1] = prop @@ -1196,7 +1199,7 @@ func (_getElemCallee) exec(vm *vm) { prop := obj.get(propName, v) if prop == nil { - prop = memberUnresolved{valueUnresolved{r: vm.r, ref: propName.String()}} + prop = memberUnresolved{valueUnresolved{r: vm.r, ref: propName.string()}} } vm.stack[vm.sp-2] = prop @@ -1306,7 +1309,7 @@ func (s setLocalP) exec(vm *vm) { } type setVar struct { - name string + name unistring.String idx uint32 } @@ -1334,10 +1337,10 @@ end: vm.pc++ } -type resolveVar1 string +type resolveVar1 unistring.String func (s resolveVar1) exec(vm *vm) { - name := string(s) + name := unistring.String(s) var ref ref for stash := vm.stash; stash != nil; stash = stash.outer { if stash.obj != nil { @@ -1368,10 +1371,10 @@ end: vm.pc++ } -type deleteVar string +type deleteVar unistring.String func (d deleteVar) exec(vm *vm) { - name := string(d) + name := unistring.String(d) ret := true for stash := vm.stash; stash != nil; stash = stash.outer { if stash.obj != nil { @@ -1400,10 +1403,10 @@ end: vm.pc++ } -type deleteGlobal string +type deleteGlobal unistring.String func (d deleteGlobal) exec(vm *vm) { - name := string(d) + name := unistring.String(d) var ret bool if vm.r.globalObject.self.hasPropertyStr(name) { ret = vm.r.globalObject.self.deleteStr(name, false) @@ -1418,10 +1421,10 @@ func (d deleteGlobal) exec(vm *vm) { vm.pc++ } -type resolveVar1Strict string +type resolveVar1Strict unistring.String func (s resolveVar1Strict) exec(vm *vm) { - name := string(s) + name := unistring.String(s) var ref ref for stash := vm.stash; stash != nil; stash = stash.outer { if stash.obj != nil { @@ -1454,7 +1457,7 @@ func (s resolveVar1Strict) exec(vm *vm) { ref = &unresolvedRef{ runtime: vm.r, - name: string(s), + name: name, } end: @@ -1462,21 +1465,21 @@ end: vm.pc++ } -type setGlobal string +type setGlobal unistring.String func (s setGlobal) exec(vm *vm) { v := vm.peek() - vm.r.globalObject.self.setOwnStr(string(s), v, false) + vm.r.globalObject.self.setOwnStr(unistring.String(s), v, false) vm.pc++ } -type setGlobalStrict string +type setGlobalStrict unistring.String func (s setGlobalStrict) exec(vm *vm) { v := vm.peek() - name := string(s) + name := unistring.String(s) o := vm.r.globalObject.self if o.hasOwnPropertyStr(name) { o.setOwnStr(name, v, true) @@ -1501,14 +1504,14 @@ func (g getLocal) exec(vm *vm) { } type getVar struct { - name string + name unistring.String idx uint32 ref bool } func (g getVar) exec(vm *vm) { level := int(g.idx >> 24) - idx := uint32(g.idx & 0x00FFFFFF) + idx := g.idx & 0x00FFFFFF stash := vm.stash name := g.name for i := 0; i < level; i++ { @@ -1536,7 +1539,7 @@ end: } type resolveVar struct { - name string + name unistring.String idx uint32 strict bool } @@ -1620,10 +1623,10 @@ func (_putValue) exec(vm *vm) { vm.pc++ } -type getVar1 string +type getVar1 unistring.String func (n getVar1) exec(vm *vm) { - name := string(n) + name := unistring.String(n) var val Value for stash := vm.stash; stash != nil; stash = stash.outer { if v, exists := stash.getByName(name, vm); exists { @@ -1641,10 +1644,10 @@ func (n getVar1) exec(vm *vm) { vm.pc++ } -type getVar1Callee string +type getVar1Callee unistring.String func (n getVar1Callee) exec(vm *vm) { - name := string(n) + name := unistring.String(n) var val Value for stash := vm.stash; stash != nil; stash = stash.outer { if v, exists := stash.getByName(name, vm); exists { @@ -1774,7 +1777,7 @@ func (vm *vm) _nativeCall(f *nativeFuncObject, n int) { if f.f != nil { vm.pushCtx() vm.prg = nil - vm.funcName = f.nameProp.get(nil).String() + vm.funcName = f.nameProp.get(nil).string() ret := f.f(FunctionCall{ Arguments: vm.stack[vm.sp-n : vm.sp], This: vm.stack[vm.sp-n-2], @@ -1895,7 +1898,7 @@ func (_retStashless) exec(vm *vm) { type newFunc struct { prg *Program - name string + name unistring.String length uint32 strict bool @@ -1911,13 +1914,14 @@ func (n *newFunc) exec(vm *vm) { vm.pc++ } -type bindName string +type bindName unistring.String func (d bindName) exec(vm *vm) { + name := unistring.String(d) if vm.stash != nil { - vm.stash.createBinding(string(d)) + vm.stash.createBinding(name) } else { - vm.r.globalObject.self._putProp(string(d), _undefined, true, true, false) + vm.r.globalObject.self._putProp(name, _undefined, true, true, false) } vm.pc++ } @@ -2239,11 +2243,11 @@ func (_retFinally) exec(vm *vm) { vm.pc++ } -type enterCatch string +type enterCatch unistring.String func (varName enterCatch) exec(vm *vm) { - vm.stash.names = map[string]uint32{ - string(varName): 0, + vm.stash.names = map[unistring.String]uint32{ + unistring.String(varName): 0, } vm.pc++ } @@ -2335,7 +2339,7 @@ func (formalArgs createArgs) exec(vm *vm) { c = vm.args } for ; i < c; i++ { - args._put(strconv.Itoa(i), &mappedProperty{ + args._put(unistring.String(strconv.Itoa(i)), &mappedProperty{ valueProperty: valueProperty{ writable: true, configurable: true, @@ -2346,7 +2350,7 @@ func (formalArgs createArgs) exec(vm *vm) { } for _, v := range vm.stash.extraArgs { - args._put(strconv.Itoa(i), v) + args._put(unistring.String(strconv.Itoa(i)), v) i++ } @@ -2365,12 +2369,12 @@ func (formalArgs createArgsStrict) exec(vm *vm) { c = vm.args } for _, v := range vm.stash.values[:c] { - args._put(strconv.Itoa(i), v) + args._put(unistring.String(strconv.Itoa(i)), v) i++ } for _, v := range vm.stash.extraArgs { - args._put(strconv.Itoa(i), v) + args._put(unistring.String(strconv.Itoa(i)), v) i++ } @@ -2426,7 +2430,7 @@ func (jmp enumNext) exec(vm *vm) { l := len(vm.iterStack) - 1 item, n := vm.iterStack[l].f() if n != nil { - vm.iterStack[l].val = newStringValue(item.name) + vm.iterStack[l].val = stringValueFromRaw(item.name) vm.iterStack[l].f = n vm.pc++ } else { From 8d9f8c267024ab580198d4769563e4037bc70828 Mon Sep 17 00:00:00 2001 From: Dmitry Panov Date: Sat, 11 Apr 2020 21:50:48 +0100 Subject: [PATCH 23/46] Use a sequencer instead of pointer for object ids (because pointers are not guaranteed to remain the same). --- builtin_map.go | 2 +- builtin_set.go | 2 +- builtin_weakmap.go | 8 ++++---- builtin_weakset.go | 8 ++++---- object.go | 33 ++++++++++++++++++++++++--------- runtime.go | 12 ++++++++++-- value.go | 38 +++++++++++++++++++++++++++++++------- 7 files changed, 75 insertions(+), 28 deletions(-) diff --git a/builtin_map.go b/builtin_map.go index d79a51c6..20dc414d 100644 --- a/builtin_map.go +++ b/builtin_map.go @@ -37,7 +37,7 @@ func (o *mapIterObject) next() Value { func (mo *mapObject) init() { mo.baseObject.init() - mo.m = newOrderedMap(&mo.val.runtime.hash) + mo.m = newOrderedMap(mo.val.runtime.getHash()) } func (r *Runtime) mapProto_clear(call FunctionCall) Value { diff --git a/builtin_set.go b/builtin_set.go index e063caa2..b3818b25 100644 --- a/builtin_set.go +++ b/builtin_set.go @@ -35,7 +35,7 @@ func (o *setIterObject) next() Value { func (so *setObject) init() { so.baseObject.init() - so.m = newOrderedMap(&so.val.runtime.hash) + so.m = newOrderedMap(so.val.runtime.getHash()) } func (r *Runtime) setProto_add(call FunctionCall) Value { diff --git a/builtin_weakmap.go b/builtin_weakmap.go index 82c35223..583576ed 100644 --- a/builtin_weakmap.go +++ b/builtin_weakmap.go @@ -6,7 +6,7 @@ type weakMap struct { // need to synchronise access to the data map because it may be accessed // from the finalizer goroutine sync.Mutex - data map[uintptr]Value + data map[uint64]Value } type weakMapObject struct { @@ -16,7 +16,7 @@ type weakMapObject struct { func newWeakMap() *weakMap { return &weakMap{ - data: make(map[uintptr]Value), + data: make(map[uint64]Value), } } @@ -25,9 +25,9 @@ func (wmo *weakMapObject) init() { wmo.m = newWeakMap() } -func (wm *weakMap) removePtr(ptr uintptr) { +func (wm *weakMap) removeId(id uint64) { wm.Lock() - delete(wm.data, ptr) + delete(wm.data, id) wm.Unlock() } diff --git a/builtin_weakset.go b/builtin_weakset.go index 7faeedf6..91e72fd5 100644 --- a/builtin_weakset.go +++ b/builtin_weakset.go @@ -6,7 +6,7 @@ type weakSet struct { // need to synchronise access to the data map because it may be accessed // from the finalizer goroutine sync.Mutex - data map[uintptr]struct{} + data map[uint64]struct{} } type weakSetObject struct { @@ -16,7 +16,7 @@ type weakSetObject struct { func newWeakSet() *weakSet { return &weakSet{ - data: make(map[uintptr]struct{}), + data: make(map[uint64]struct{}), } } @@ -25,9 +25,9 @@ func (ws *weakSetObject) init() { ws.s = newWeakSet() } -func (ws *weakSet) removePtr(ptr uintptr) { +func (ws *weakSet) removeId(id uint64) { ws.Lock() - delete(ws.data, ptr) + delete(ws.data, id) ws.Unlock() } diff --git a/object.go b/object.go index 898f6860..1b0639cc 100644 --- a/object.go +++ b/object.go @@ -6,7 +6,6 @@ import ( "reflect" "runtime" "sort" - "unsafe" "github.com/dop251/goja/unistring" ) @@ -33,10 +32,11 @@ const ( ) type weakCollection interface { - removePtr(uintptr) + removeId(uint64) } type weakCollections struct { + objId uint64 colls []weakCollection } @@ -49,8 +49,8 @@ func (r *weakCollections) add(c weakCollection) { r.colls = append(r.colls, c) } -func (r *weakCollections) id() uintptr { - return uintptr(unsafe.Pointer(r)) +func (r *weakCollections) id() uint64 { + return r.objId } func (r *weakCollections) remove(c weakCollection) { @@ -79,12 +79,13 @@ func (r *weakCollections) remove(c weakCollection) { func finalizeObjectWeakRefs(r *weakCollections) { id := r.id() for _, c := range r.colls { - c.removePtr(id) + c.removeId(id) } r.colls = nil } type Object struct { + id uint64 runtime *Runtime self objectImpl @@ -518,7 +519,7 @@ func (o *baseObject) setOwnSym(name *valueSymbol, val Value, throw bool) bool { return false } else { if o.symValues == nil { - o.symValues = newOrderedMap(&o.val.runtime.hash) + o.symValues = newOrderedMap(nil) } o.symValues.set(name, val) } @@ -760,7 +761,7 @@ func (o *baseObject) defineOwnPropertySym(s *valueSymbol, descr PropertyDescript } if v, ok := o._defineOwnProperty(s.desc.string(), existingVal, descr, throw); ok { if o.symValues == nil { - o.symValues = newOrderedMap(&o.val.runtime.hash) + o.symValues = newOrderedMap(nil) } o.symValues.set(s, v) return true @@ -796,7 +797,7 @@ func (o *baseObject) _putProp(name unistring.String, value Value, writable, enum func (o *baseObject) _putSym(s *valueSymbol, prop Value) { if o.symValues == nil { - o.symValues = newOrderedMap(&o.val.runtime.hash) + o.symValues = newOrderedMap(nil) } o.symValues.set(s, prop) } @@ -1348,9 +1349,23 @@ func (o *Object) defineOwnProperty(n Value, desc PropertyDescriptor, throw bool) func (o *Object) getWeakCollRefs() *weakCollections { if o.weakColls == nil { - o.weakColls = &weakCollections{} + o.weakColls = &weakCollections{ + objId: o.getId(), + } runtime.SetFinalizer(o.weakColls, finalizeObjectWeakRefs) } return o.weakColls } + +func (o *Object) getId() uint64 { + for o.id == 0 { + if o.runtime.hash == nil { + h := o.runtime.getHash() + o.runtime.idSeq = h.Sum64() + } + o.id = o.runtime.idSeq + o.runtime.idSeq++ + } + return o.id +} diff --git a/runtime.go b/runtime.go index d7f45694..119c62e1 100644 --- a/runtime.go +++ b/runtime.go @@ -164,8 +164,9 @@ type Runtime struct { typeInfoCache map[reflect.Type]*reflectTypeInfo fieldNameMapper FieldNameMapper - vm *vm - hash maphash.Hash + vm *vm + hash *maphash.Hash + idSeq uint64 } type StackFrame struct { @@ -1921,6 +1922,13 @@ func (r *Runtime) newLazyObject(create func(*Object) objectImpl) *Object { return val } +func (r *Runtime) getHash() *maphash.Hash { + if r.hash == nil { + r.hash = &maphash.Hash{} + } + return r.hash +} + func nilSafe(v Value) Value { if v != nil { return v diff --git a/value.go b/value.go index 969bca86..d86e3141 100644 --- a/value.go +++ b/value.go @@ -11,6 +11,22 @@ import ( "github.com/dop251/goja/unistring" ) +var ( + // Not goroutine-safe, do not use for anything other than package level init + pkgHasher maphash.Hash + + hashFalse = randomHash() + hashTrue = randomHash() + hashNull = randomHash() + hashUndef = randomHash() +) + +// Not goroutine-safe, do not use for anything other than package level init +func randomHash() uint64 { + pkgHasher.WriteByte(0) + return pkgHasher.Sum64() +} + var ( valueFalse Value = valueBool(false) valueTrue Value = valueBool(true) @@ -73,6 +89,7 @@ type valueUndefined struct { valueNull } type valueSymbol struct { + h uintptr desc valueString } @@ -297,9 +314,10 @@ func (b valueBool) ExportType() reflect.Type { func (b valueBool) hash(*maphash.Hash) uint64 { if b { - return uint64(uintptr(unsafe.Pointer(&valueTrue))) + return hashTrue } - return uint64(uintptr(unsafe.Pointer(&valueFalse))) + + return hashFalse } func (n valueNull) ToInteger() int64 { @@ -357,7 +375,7 @@ func (u valueUndefined) ToFloat() float64 { } func (u valueUndefined) hash(*maphash.Hash) uint64 { - return uint64(uintptr(unsafe.Pointer(&_undefined))) + return hashUndef } func (n valueNull) ToFloat() float64 { @@ -409,7 +427,7 @@ func (n valueNull) ExportType() reflect.Type { } func (n valueNull) hash(*maphash.Hash) uint64 { - return uint64(uintptr(unsafe.Pointer(&_null))) + return hashNull } func (p *valueProperty) ToInteger() int64 { @@ -719,7 +737,7 @@ func (o *Object) ExportType() reflect.Type { } func (o *Object) hash(*maphash.Hash) uint64 { - return uint64(uintptr(unsafe.Pointer(o))) + return o.getId() } func (o *Object) Get(name string) Value { @@ -938,13 +956,19 @@ func (s *valueSymbol) baseObject(r *Runtime) *Object { } func (s *valueSymbol) hash(*maphash.Hash) uint64 { - return uint64(uintptr(unsafe.Pointer(s))) + return uint64(s.h) } func newSymbol(s valueString) *valueSymbol { - return &valueSymbol{ + r := &valueSymbol{ desc: asciiString("Symbol(").concat(s).concat(asciiString(")")), } + // This may need to be reconsidered in the future. + // Depending on changes in Go's allocation policy and/or introduction of a compacting GC + // this may no longer provide sufficient dispersion. The alternative, however, is a globally + // synchronised random generator/hasher/sequencer and I don't want to go down that route just yet. + r.h = uintptr(unsafe.Pointer(r)) + return r } func init() { From 60b86f2254fd83d2a1ab333b46ce3e23a8bc83aa Mon Sep 17 00:00:00 2001 From: Dmitry Panov Date: Tue, 14 Apr 2020 11:44:33 +0100 Subject: [PATCH 24/46] Missing Date methods, better overflow handling, refactoring (#147) --- builtin_date.go | 871 +++++++++++++++++++++++++--------------------- date.go | 73 +++- date_test.go | 10 + object.go | 76 ++-- runtime.go | 8 + string_ascii.go | 2 +- string_unicode.go | 2 +- tc39_test.go | 4 + value.go | 38 +- vm.go | 15 +- 10 files changed, 659 insertions(+), 440 deletions(-) diff --git a/builtin_date.go b/builtin_date.go index fe976a1b..c706f1c7 100644 --- a/builtin_date.go +++ b/builtin_date.go @@ -6,110 +6,65 @@ import ( "time" ) -const ( - maxTime = 8.64e15 -) - -func timeFromMsec(msec int64) time.Time { - sec := msec / 1000 - nsec := (msec % 1000) * 1e6 - return time.Unix(sec, nsec) -} - -func timeToMsec(t time.Time) int64 { - return t.Unix()*1000 + int64(t.Nanosecond())/1e6 -} - -func (r *Runtime) makeDate(args []Value, loc *time.Location) (t time.Time, valid bool) { - pick := func(index int, default_ int64) (int64, bool) { - if index >= len(args) { - return default_, true - } - value := args[index] - if valueInt, ok := value.(valueInt); ok { - return int64(valueInt), true - } - valueFloat := value.ToFloat() - if math.IsNaN(valueFloat) || math.IsInf(valueFloat, 0) { - return 0, false - } - return int64(valueFloat), true - } - +func (r *Runtime) makeDate(args []Value, utc bool) (t time.Time, valid bool) { switch { case len(args) >= 2: - var year, month, day, hour, minute, second, millisecond int64 - if year, valid = pick(0, 1900); !valid { - return - } - if month, valid = pick(1, 0); !valid { - return - } - if day, valid = pick(2, 1); !valid { - return - } - if hour, valid = pick(3, 0); !valid { - return - } - if minute, valid = pick(4, 0); !valid { - return - } - if second, valid = pick(5, 0); !valid { - return - } - if millisecond, valid = pick(6, 0); !valid { - return - } - - if year >= 0 && year <= 99 { - year += 1900 - } - - t = time.Date(int(year), time.Month(int(month)+1), int(day), int(hour), int(minute), int(second), int(millisecond)*1e6, loc) + t = time.Date(1970, time.January, 1, 0, 0, 0, 0, time.Local) + t, valid = _dateSetYear(t, FunctionCall{Arguments: args}, 0, utc) case len(args) == 0: t = r.now() valid = true default: // one argument - pv := toPrimitiveNumber(args[0]) - if val, ok := pv.(valueString); ok { - return dateParse(val.String()) - } - - var n int64 - if i, ok := pv.(valueInt); ok { - n = int64(i) - } else if f, ok := pv.(valueFloat); ok { - f := float64(f) - if math.IsNaN(f) || math.IsInf(f, 0) { - return + if o, ok := args[0].(*Object); ok { + if d, ok := o.self.(*dateObject); ok { + t = d.time() + valid = true } - if math.Abs(f) > maxTime { - return + } + if !valid { + pv := toPrimitive(args[0]) + if val, ok := pv.(valueString); ok { + return dateParse(val.String()) } - n = int64(f) - } else { - n = pv.ToInteger() + pv = pv.ToNumber() + var n int64 + if i, ok := pv.(valueInt); ok { + n = int64(i) + } else if f, ok := pv.(valueFloat); ok { + f := float64(f) + if math.IsNaN(f) || math.IsInf(f, 0) { + return + } + if math.Abs(f) > maxTime { + return + } + n = int64(f) + } else { + n = pv.ToInteger() + } + t = timeFromMsec(n) + valid = true } - t = timeFromMsec(n) - valid = true - } - msec := t.Unix()*1000 + int64(t.Nanosecond()/1e6) - if msec < 0 { - msec = -msec } - if msec > maxTime { - valid = false + if valid { + msec := t.Unix()*1000 + int64(t.Nanosecond()/1e6) + if msec < 0 { + msec = -msec + } + if msec > maxTime { + valid = false + } } return } -func (r *Runtime) newDateTime(args []Value, loc *time.Location, proto *Object) *Object { - t, isSet := r.makeDate(args, loc) +func (r *Runtime) newDateTime(args []Value, proto *Object) *Object { + t, isSet := r.makeDate(args, false) return r.newDateObject(t, isSet, proto) } func (r *Runtime) builtin_newDate(args []Value, proto *Object) *Object { - return r.newDateTime(args, time.Local, proto) + return r.newDateTime(args, proto) } func (r *Runtime) builtin_date(FunctionCall) Value { @@ -125,7 +80,13 @@ func (r *Runtime) date_parse(call FunctionCall) Value { } func (r *Runtime) date_UTC(call FunctionCall) Value { - t, valid := r.makeDate(call.Arguments, time.UTC) + var args []Value + if len(call.Arguments) < 2 { + args = []Value{call.Argument(0), _positiveZero} + } else { + args = call.Arguments + } + t, valid := r.makeDate(args, true) if !valid { return _NaN } @@ -139,34 +100,32 @@ func (r *Runtime) date_now(FunctionCall) Value { func (r *Runtime) dateproto_toString(call FunctionCall) Value { obj := r.toObject(call.This) if d, ok := obj.self.(*dateObject); ok { - if d.isSet { - return asciiString(d.time.Format(dateTimeLayout)) + if d.isSet() { + return asciiString(d.time().Format(dateTimeLayout)) } else { return stringInvalidDate } } - r.typeErrorResult(true, "Method Date.prototype.toString is called on incompatible receiver") - panic("Unreachable") + panic(r.NewTypeError("Method Date.prototype.toString is called on incompatible receiver")) } func (r *Runtime) dateproto_toUTCString(call FunctionCall) Value { obj := r.toObject(call.This) if d, ok := obj.self.(*dateObject); ok { - if d.isSet { - return asciiString(d.time.In(time.UTC).Format(utcDateTimeLayout)) + if d.isSet() { + return asciiString(d.timeUTC().Format(utcDateTimeLayout)) } else { return stringInvalidDate } } - r.typeErrorResult(true, "Method Date.prototype.toUTCString is called on incompatible receiver") - panic("Unreachable") + panic(r.NewTypeError("Method Date.prototype.toUTCString is called on incompatible receiver")) } func (r *Runtime) dateproto_toISOString(call FunctionCall) Value { obj := r.toObject(call.This) if d, ok := obj.self.(*dateObject); ok { - if d.isSet { - utc := d.time.In(time.UTC) + if d.isSet() { + utc := d.timeUTC() year := utc.Year() if year >= -9999 && year <= 9999 { return asciiString(utc.Format(isoDateTimeLayout)) @@ -177,13 +136,12 @@ func (r *Runtime) dateproto_toISOString(call FunctionCall) Value { panic(r.newError(r.global.RangeError, "Invalid time value")) } } - r.typeErrorResult(true, "Method Date.prototype.toISOString is called on incompatible receiver") - panic("Unreachable") + panic(r.NewTypeError("Method Date.prototype.toISOString is called on incompatible receiver")) } func (r *Runtime) dateproto_toJSON(call FunctionCall) Value { obj := r.toObject(call.This) - tv := obj.self.toPrimitiveNumber() + tv := obj.toPrimitiveNumber() if f, ok := tv.(valueFloat); ok { f := float64(f) if math.IsNaN(f) || math.IsInf(f, 0) { @@ -201,648 +159,784 @@ func (r *Runtime) dateproto_toJSON(call FunctionCall) Value { } } - r.typeErrorResult(true, "toISOString is not a function") - panic("Unreachable") + panic(r.NewTypeError("toISOString is not a function")) +} + +func (r *Runtime) dateproto_toPrimitive(call FunctionCall) Value { + o := r.toObject(call.This) + arg := call.Argument(0) + + if asciiString("string").StrictEquals(arg) || asciiString("default").StrictEquals(arg) { + return o.self.toPrimitiveString() + } + if asciiString("number").StrictEquals(arg) { + return o.self.toPrimitiveNumber() + } + panic(r.NewTypeError("Invalid hint: %s", arg)) } func (r *Runtime) dateproto_toDateString(call FunctionCall) Value { obj := r.toObject(call.This) if d, ok := obj.self.(*dateObject); ok { - if d.isSet { - return asciiString(d.time.Format(dateLayout)) + if d.isSet() { + return asciiString(d.time().Format(dateLayout)) } else { return stringInvalidDate } } - r.typeErrorResult(true, "Method Date.prototype.toDateString is called on incompatible receiver") - panic("Unreachable") + panic(r.NewTypeError("Method Date.prototype.toDateString is called on incompatible receiver")) } func (r *Runtime) dateproto_toTimeString(call FunctionCall) Value { obj := r.toObject(call.This) if d, ok := obj.self.(*dateObject); ok { - if d.isSet { - return asciiString(d.time.Format(timeLayout)) + if d.isSet() { + return asciiString(d.time().Format(timeLayout)) } else { return stringInvalidDate } } - r.typeErrorResult(true, "Method Date.prototype.toTimeString is called on incompatible receiver") - panic("Unreachable") + panic(r.NewTypeError("Method Date.prototype.toTimeString is called on incompatible receiver")) } func (r *Runtime) dateproto_toLocaleString(call FunctionCall) Value { obj := r.toObject(call.This) if d, ok := obj.self.(*dateObject); ok { - if d.isSet { - return asciiString(d.time.Format(datetimeLayout_en_GB)) + if d.isSet() { + return asciiString(d.time().Format(datetimeLayout_en_GB)) } else { return stringInvalidDate } } - r.typeErrorResult(true, "Method Date.prototype.toLocaleString is called on incompatible receiver") - panic("Unreachable") + panic(r.NewTypeError("Method Date.prototype.toLocaleString is called on incompatible receiver")) } func (r *Runtime) dateproto_toLocaleDateString(call FunctionCall) Value { obj := r.toObject(call.This) if d, ok := obj.self.(*dateObject); ok { - if d.isSet { - return asciiString(d.time.Format(dateLayout_en_GB)) + if d.isSet() { + return asciiString(d.time().Format(dateLayout_en_GB)) } else { return stringInvalidDate } } - r.typeErrorResult(true, "Method Date.prototype.toLocaleDateString is called on incompatible receiver") - panic("Unreachable") + panic(r.NewTypeError("Method Date.prototype.toLocaleDateString is called on incompatible receiver")) } func (r *Runtime) dateproto_toLocaleTimeString(call FunctionCall) Value { obj := r.toObject(call.This) if d, ok := obj.self.(*dateObject); ok { - if d.isSet { - return asciiString(d.time.Format(timeLayout_en_GB)) + if d.isSet() { + return asciiString(d.time().Format(timeLayout_en_GB)) } else { return stringInvalidDate } } - r.typeErrorResult(true, "Method Date.prototype.toLocaleTimeString is called on incompatible receiver") - panic("Unreachable") + panic(r.NewTypeError("Method Date.prototype.toLocaleTimeString is called on incompatible receiver")) } func (r *Runtime) dateproto_valueOf(call FunctionCall) Value { obj := r.toObject(call.This) if d, ok := obj.self.(*dateObject); ok { - if d.isSet { - return intToValue(d.time.Unix()*1000 + int64(d.time.Nanosecond()/1e6)) + if d.isSet() { + return intToValue(d.msec) } else { return _NaN } } - r.typeErrorResult(true, "Method Date.prototype.valueOf is called on incompatible receiver") - return nil + panic(r.NewTypeError("Method Date.prototype.valueOf is called on incompatible receiver")) } func (r *Runtime) dateproto_getTime(call FunctionCall) Value { obj := r.toObject(call.This) if d, ok := obj.self.(*dateObject); ok { - if d.isSet { - return intToValue(timeToMsec(d.time)) + if d.isSet() { + return intToValue(d.msec) } else { return _NaN } } - r.typeErrorResult(true, "Method Date.prototype.getTime is called on incompatible receiver") - return nil + panic(r.NewTypeError("Method Date.prototype.getTime is called on incompatible receiver")) } func (r *Runtime) dateproto_getFullYear(call FunctionCall) Value { obj := r.toObject(call.This) if d, ok := obj.self.(*dateObject); ok { - if d.isSet { - return intToValue(int64(d.time.Year())) + if d.isSet() { + return intToValue(int64(d.time().Year())) } else { return _NaN } } - r.typeErrorResult(true, "Method Date.prototype.getFullYear is called on incompatible receiver") - return nil + panic(r.NewTypeError("Method Date.prototype.getFullYear is called on incompatible receiver")) } func (r *Runtime) dateproto_getUTCFullYear(call FunctionCall) Value { obj := r.toObject(call.This) if d, ok := obj.self.(*dateObject); ok { - if d.isSet { - return intToValue(int64(d.time.In(time.UTC).Year())) + if d.isSet() { + return intToValue(int64(d.timeUTC().Year())) } else { return _NaN } } - r.typeErrorResult(true, "Method Date.prototype.getUTCFullYear is called on incompatible receiver") - return nil + panic(r.NewTypeError("Method Date.prototype.getUTCFullYear is called on incompatible receiver")) } func (r *Runtime) dateproto_getMonth(call FunctionCall) Value { obj := r.toObject(call.This) if d, ok := obj.self.(*dateObject); ok { - if d.isSet { - return intToValue(int64(d.time.Month()) - 1) + if d.isSet() { + return intToValue(int64(d.time().Month()) - 1) } else { return _NaN } } - r.typeErrorResult(true, "Method Date.prototype.getMonth is called on incompatible receiver") - return nil + panic(r.NewTypeError("Method Date.prototype.getMonth is called on incompatible receiver")) } func (r *Runtime) dateproto_getUTCMonth(call FunctionCall) Value { obj := r.toObject(call.This) if d, ok := obj.self.(*dateObject); ok { - if d.isSet { - return intToValue(int64(d.time.In(time.UTC).Month()) - 1) + if d.isSet() { + return intToValue(int64(d.timeUTC().Month()) - 1) } else { return _NaN } } - r.typeErrorResult(true, "Method Date.prototype.getUTCMonth is called on incompatible receiver") - return nil + panic(r.NewTypeError("Method Date.prototype.getUTCMonth is called on incompatible receiver")) } func (r *Runtime) dateproto_getHours(call FunctionCall) Value { obj := r.toObject(call.This) if d, ok := obj.self.(*dateObject); ok { - if d.isSet { - return intToValue(int64(d.time.Hour())) + if d.isSet() { + return intToValue(int64(d.time().Hour())) } else { return _NaN } } - r.typeErrorResult(true, "Method Date.prototype.getHours is called on incompatible receiver") - return nil + panic(r.NewTypeError("Method Date.prototype.getHours is called on incompatible receiver")) } func (r *Runtime) dateproto_getUTCHours(call FunctionCall) Value { obj := r.toObject(call.This) if d, ok := obj.self.(*dateObject); ok { - if d.isSet { - return intToValue(int64(d.time.In(time.UTC).Hour())) + if d.isSet() { + return intToValue(int64(d.timeUTC().Hour())) } else { return _NaN } } - r.typeErrorResult(true, "Method Date.prototype.getUTCHours is called on incompatible receiver") - return nil + panic(r.NewTypeError("Method Date.prototype.getUTCHours is called on incompatible receiver")) } func (r *Runtime) dateproto_getDate(call FunctionCall) Value { obj := r.toObject(call.This) if d, ok := obj.self.(*dateObject); ok { - if d.isSet { - return intToValue(int64(d.time.Day())) + if d.isSet() { + return intToValue(int64(d.time().Day())) } else { return _NaN } } - r.typeErrorResult(true, "Method Date.prototype.getDate is called on incompatible receiver") - return nil + panic(r.NewTypeError("Method Date.prototype.getDate is called on incompatible receiver")) } func (r *Runtime) dateproto_getUTCDate(call FunctionCall) Value { obj := r.toObject(call.This) if d, ok := obj.self.(*dateObject); ok { - if d.isSet { - return intToValue(int64(d.time.In(time.UTC).Day())) + if d.isSet() { + return intToValue(int64(d.timeUTC().Day())) } else { return _NaN } } - r.typeErrorResult(true, "Method Date.prototype.getUTCDate is called on incompatible receiver") - return nil + panic(r.NewTypeError("Method Date.prototype.getUTCDate is called on incompatible receiver")) } func (r *Runtime) dateproto_getDay(call FunctionCall) Value { obj := r.toObject(call.This) if d, ok := obj.self.(*dateObject); ok { - if d.isSet { - return intToValue(int64(d.time.Weekday())) + if d.isSet() { + return intToValue(int64(d.time().Weekday())) } else { return _NaN } } - r.typeErrorResult(true, "Method Date.prototype.getDay is called on incompatible receiver") - return nil + panic(r.NewTypeError("Method Date.prototype.getDay is called on incompatible receiver")) } func (r *Runtime) dateproto_getUTCDay(call FunctionCall) Value { obj := r.toObject(call.This) if d, ok := obj.self.(*dateObject); ok { - if d.isSet { - return intToValue(int64(d.time.In(time.UTC).Weekday())) + if d.isSet() { + return intToValue(int64(d.timeUTC().Weekday())) } else { return _NaN } } - r.typeErrorResult(true, "Method Date.prototype.getUTCDay is called on incompatible receiver") - return nil + panic(r.NewTypeError("Method Date.prototype.getUTCDay is called on incompatible receiver")) } func (r *Runtime) dateproto_getMinutes(call FunctionCall) Value { obj := r.toObject(call.This) if d, ok := obj.self.(*dateObject); ok { - if d.isSet { - return intToValue(int64(d.time.Minute())) + if d.isSet() { + return intToValue(int64(d.time().Minute())) } else { return _NaN } } - r.typeErrorResult(true, "Method Date.prototype.getMinutes is called on incompatible receiver") - return nil + panic(r.NewTypeError("Method Date.prototype.getMinutes is called on incompatible receiver")) } func (r *Runtime) dateproto_getUTCMinutes(call FunctionCall) Value { obj := r.toObject(call.This) if d, ok := obj.self.(*dateObject); ok { - if d.isSet { - return intToValue(int64(d.time.In(time.UTC).Minute())) + if d.isSet() { + return intToValue(int64(d.timeUTC().Minute())) } else { return _NaN } } - r.typeErrorResult(true, "Method Date.prototype.getUTCMinutes is called on incompatible receiver") - return nil + panic(r.NewTypeError("Method Date.prototype.getUTCMinutes is called on incompatible receiver")) } func (r *Runtime) dateproto_getSeconds(call FunctionCall) Value { obj := r.toObject(call.This) if d, ok := obj.self.(*dateObject); ok { - if d.isSet { - return intToValue(int64(d.time.Second())) + if d.isSet() { + return intToValue(int64(d.time().Second())) } else { return _NaN } } - r.typeErrorResult(true, "Method Date.prototype.getSeconds is called on incompatible receiver") - return nil + panic(r.NewTypeError("Method Date.prototype.getSeconds is called on incompatible receiver")) } func (r *Runtime) dateproto_getUTCSeconds(call FunctionCall) Value { obj := r.toObject(call.This) if d, ok := obj.self.(*dateObject); ok { - if d.isSet { - return intToValue(int64(d.time.In(time.UTC).Second())) + if d.isSet() { + return intToValue(int64(d.timeUTC().Second())) } else { return _NaN } } - r.typeErrorResult(true, "Method Date.prototype.getUTCSeconds is called on incompatible receiver") - return nil + panic(r.NewTypeError("Method Date.prototype.getUTCSeconds is called on incompatible receiver")) } func (r *Runtime) dateproto_getMilliseconds(call FunctionCall) Value { obj := r.toObject(call.This) if d, ok := obj.self.(*dateObject); ok { - if d.isSet { - return intToValue(int64(d.time.Nanosecond() / 1e6)) + if d.isSet() { + return intToValue(int64(d.time().Nanosecond() / 1e6)) } else { return _NaN } } - r.typeErrorResult(true, "Method Date.prototype.getMilliseconds is called on incompatible receiver") - return nil + panic(r.NewTypeError("Method Date.prototype.getMilliseconds is called on incompatible receiver")) } func (r *Runtime) dateproto_getUTCMilliseconds(call FunctionCall) Value { obj := r.toObject(call.This) if d, ok := obj.self.(*dateObject); ok { - if d.isSet { - return intToValue(int64(d.time.In(time.UTC).Nanosecond() / 1e6)) + if d.isSet() { + return intToValue(int64(d.timeUTC().Nanosecond() / 1e6)) } else { return _NaN } } - r.typeErrorResult(true, "Method Date.prototype.getUTCMilliseconds is called on incompatible receiver") - return nil + panic(r.NewTypeError("Method Date.prototype.getUTCMilliseconds is called on incompatible receiver")) } func (r *Runtime) dateproto_getTimezoneOffset(call FunctionCall) Value { obj := r.toObject(call.This) if d, ok := obj.self.(*dateObject); ok { - if d.isSet { - _, offset := d.time.Zone() + if d.isSet() { + _, offset := d.time().Zone() return floatToValue(float64(-offset) / 60) } else { return _NaN } } - r.typeErrorResult(true, "Method Date.prototype.getTimezoneOffset is called on incompatible receiver") - return nil + panic(r.NewTypeError("Method Date.prototype.getTimezoneOffset is called on incompatible receiver")) } func (r *Runtime) dateproto_setTime(call FunctionCall) Value { obj := r.toObject(call.This) if d, ok := obj.self.(*dateObject); ok { - msec := call.Argument(0).ToInteger() - d.time = timeFromMsec(msec) - return intToValue(msec) + n := call.Argument(0).ToNumber() + if IsNaN(n) { + d.unset() + return _NaN + } + return d.setTimeMs(n.ToInteger()) + } + panic(r.NewTypeError("Method Date.prototype.setTime is called on incompatible receiver")) +} + +// _norm returns nhi, nlo such that +// hi * base + lo == nhi * base + nlo +// 0 <= nlo < base +func _norm(hi, lo, base int64) (nhi, nlo int64, ok bool) { + if lo < 0 { + if hi == math.MinInt64 && lo <= -base { + // underflow + ok = false + return + } + n := (-lo-1)/base + 1 + hi -= n + lo += n * base + } + if lo >= base { + if hi == math.MaxInt64 { + // overflow + ok = false + return + } + n := lo / base + hi += n + lo -= n * base } - r.typeErrorResult(true, "Method Date.prototype.setTime is called on incompatible receiver") - panic("Unreachable") + return hi, lo, true +} + +func mkTime(year, m, day, hour, min, sec, nsec int64, loc *time.Location) (t time.Time, ok bool) { + year, m, ok = _norm(year, m, 12) + if !ok { + return + } + + // Normalise nsec, sec, min, hour, overflowing into day. + sec, nsec, ok = _norm(sec, nsec, 1e9) + if !ok { + return + } + min, sec, ok = _norm(min, sec, 60) + if !ok { + return + } + hour, min, ok = _norm(hour, min, 60) + if !ok { + return + } + day, hour, ok = _norm(day, hour, 24) + if !ok { + return + } + if year > math.MaxInt32 || year < math.MinInt32 || + day > math.MaxInt32 || day < math.MinInt32 || + m >= math.MaxInt32 || m < math.MinInt32-1 { + return time.Time{}, false + } + month := time.Month(m) + 1 + return time.Date(int(year), month, int(day), int(hour), int(min), int(sec), int(nsec), loc), true +} + +func _intArg(call FunctionCall, argNum int) (int64, bool) { + n := call.Argument(argNum).ToNumber() + if IsNaN(n) { + return 0, false + } + return n.ToInteger(), true +} + +func _dateSetYear(t time.Time, call FunctionCall, argNum int, utc bool) (time.Time, bool) { + var year int64 + if argNum == 0 || argNum > 0 && argNum < len(call.Arguments) { + var ok bool + year, ok = _intArg(call, argNum) + if !ok { + return time.Time{}, false + } + if year >= 0 && year <= 99 { + year += 1900 + } + } else { + year = int64(t.Year()) + } + + return _dateSetMonth(year, t, call, argNum+1, utc) +} + +func _dateSetFullYear(t time.Time, call FunctionCall, argNum int, utc bool) (time.Time, bool) { + var year int64 + if argNum == 0 || argNum > 0 && argNum < len(call.Arguments) { + var ok bool + year, ok = _intArg(call, argNum) + if !ok { + return time.Time{}, false + } + } else { + year = int64(t.Year()) + } + return _dateSetMonth(year, t, call, argNum+1, utc) +} + +func _dateSetMonth(year int64, t time.Time, call FunctionCall, argNum int, utc bool) (time.Time, bool) { + var mon int64 + if argNum == 0 || argNum > 0 && argNum < len(call.Arguments) { + var ok bool + mon, ok = _intArg(call, argNum) + if !ok { + return time.Time{}, false + } + } else { + mon = int64(t.Month()) - 1 + } + + return _dateSetDay(year, mon, t, call, argNum+1, utc) +} + +func _dateSetDay(year, mon int64, t time.Time, call FunctionCall, argNum int, utc bool) (time.Time, bool) { + var day int64 + if argNum == 0 || argNum > 0 && argNum < len(call.Arguments) { + var ok bool + day, ok = _intArg(call, argNum) + if !ok { + return time.Time{}, false + } + } else { + day = int64(t.Day()) + } + + return _dateSetHours(year, mon, day, t, call, argNum+1, utc) +} + +func _dateSetHours(year, mon, day int64, t time.Time, call FunctionCall, argNum int, utc bool) (time.Time, bool) { + var hours int64 + if argNum == 0 || argNum > 0 && argNum < len(call.Arguments) { + var ok bool + hours, ok = _intArg(call, argNum) + if !ok { + return time.Time{}, false + } + } else { + hours = int64(t.Hour()) + } + return _dateSetMinutes(year, mon, day, hours, t, call, argNum+1, utc) +} + +func _dateSetMinutes(year, mon, day, hours int64, t time.Time, call FunctionCall, argNum int, utc bool) (time.Time, bool) { + var min int64 + if argNum == 0 || argNum > 0 && argNum < len(call.Arguments) { + var ok bool + min, ok = _intArg(call, argNum) + if !ok { + return time.Time{}, false + } + } else { + min = int64(t.Minute()) + } + return _dateSetSeconds(year, mon, day, hours, min, t, call, argNum+1, utc) +} + +func _dateSetSeconds(year, mon, day, hours, min int64, t time.Time, call FunctionCall, argNum int, utc bool) (time.Time, bool) { + var sec int64 + if argNum == 0 || argNum > 0 && argNum < len(call.Arguments) { + var ok bool + sec, ok = _intArg(call, argNum) + if !ok { + return time.Time{}, false + } + } else { + sec = int64(t.Second()) + } + return _dateSetMilliseconds(year, mon, day, hours, min, sec, t, call, argNum+1, utc) +} + +func _dateSetMilliseconds(year, mon, day, hours, min, sec int64, t time.Time, call FunctionCall, argNum int, utc bool) (time.Time, bool) { + var msec int64 + if argNum == 0 || argNum > 0 && argNum < len(call.Arguments) { + var ok bool + msec, ok = _intArg(call, argNum) + if !ok { + return time.Time{}, false + } + } else { + msec = int64(t.Nanosecond() / 1e6) + } + var ok bool + sec, msec, ok = _norm(sec, msec, 1e3) + if !ok { + return time.Time{}, false + } + + var loc *time.Location + if utc { + loc = time.UTC + } else { + loc = time.Local + } + r, ok := mkTime(year, mon, day, hours, min, sec, msec*1e6, loc) + if !ok { + return time.Time{}, false + } + if utc { + return r.In(time.Local), true + } + return r, true } func (r *Runtime) dateproto_setMilliseconds(call FunctionCall) Value { obj := r.toObject(call.This) if d, ok := obj.self.(*dateObject); ok { - if d.isSet { - msec := call.Argument(0).ToInteger() - m := timeToMsec(d.time) - int64(d.time.Nanosecond())/1e6 + msec - d.time = timeFromMsec(m) - return intToValue(m) + if d.isSet() { + n := call.Argument(0).ToNumber() + if IsNaN(n) { + d.unset() + return _NaN + } + msec := n.ToInteger() + sec := d.msec / 1e3 + var ok bool + sec, msec, ok = _norm(sec, msec, 1e3) + if !ok { + d.unset() + return _NaN + } + return d.setTimeMs(sec*1e3 + msec) } else { return _NaN } } - r.typeErrorResult(true, "Method Date.prototype.setMilliseconds is called on incompatible receiver") - panic("Unreachable") + panic(r.NewTypeError("Method Date.prototype.setMilliseconds is called on incompatible receiver")) } func (r *Runtime) dateproto_setUTCMilliseconds(call FunctionCall) Value { obj := r.toObject(call.This) if d, ok := obj.self.(*dateObject); ok { - if d.isSet { - msec := call.Argument(0).ToInteger() - m := timeToMsec(d.time) - int64(d.time.Nanosecond())/1e6 + msec - d.time = timeFromMsec(m) - return intToValue(m) + if d.isSet() { + n := call.Argument(0).ToNumber() + if IsNaN(n) { + d.unset() + return _NaN + } + msec := n.ToInteger() + sec := d.msec / 1e3 + var ok bool + sec, msec, ok = _norm(sec, msec, 1e3) + if !ok { + d.unset() + return _NaN + } + return d.setTimeMs(sec*1e3 + msec) } else { return _NaN } } - r.typeErrorResult(true, "Method Date.prototype.setUTCMilliseconds is called on incompatible receiver") - panic("Unreachable") + panic(r.NewTypeError("Method Date.prototype.setUTCMilliseconds is called on incompatible receiver")) } func (r *Runtime) dateproto_setSeconds(call FunctionCall) Value { obj := r.toObject(call.This) if d, ok := obj.self.(*dateObject); ok { - if d.isSet { - sec := int(call.Argument(0).ToInteger()) - var nsec int - if len(call.Arguments) > 1 { - nsec = int(call.Arguments[1].ToInteger() * 1e6) - } else { - nsec = d.time.Nanosecond() + if d.isSet() { + t, ok := _dateSetFullYear(d.time(), call, -5, false) + if !ok { + d.unset() + return _NaN } - d.time = time.Date(d.time.Year(), d.time.Month(), d.time.Day(), d.time.Hour(), d.time.Minute(), sec, nsec, time.Local) - return intToValue(timeToMsec(d.time)) + return d.setTimeMs(timeToMsec(t)) } else { return _NaN } } - r.typeErrorResult(true, "Method Date.prototype.setSeconds is called on incompatible receiver") - panic("Unreachable") + panic(r.NewTypeError("Method Date.prototype.setSeconds is called on incompatible receiver")) } func (r *Runtime) dateproto_setUTCSeconds(call FunctionCall) Value { obj := r.toObject(call.This) if d, ok := obj.self.(*dateObject); ok { - if d.isSet { - sec := int(call.Argument(0).ToInteger()) - var nsec int - t := d.time.In(time.UTC) - if len(call.Arguments) > 1 { - nsec = int(call.Arguments[1].ToInteger() * 1e6) - } else { - nsec = t.Nanosecond() + if d.isSet() { + t, ok := _dateSetFullYear(d.timeUTC(), call, -5, true) + if !ok { + d.unset() + return _NaN } - d.time = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), sec, nsec, time.UTC).In(time.Local) - return intToValue(timeToMsec(d.time)) + return d.setTimeMs(timeToMsec(t)) } else { return _NaN } } - r.typeErrorResult(true, "Method Date.prototype.setUTCSeconds is called on incompatible receiver") - panic("Unreachable") + panic(r.NewTypeError("Method Date.prototype.setUTCSeconds is called on incompatible receiver")) } func (r *Runtime) dateproto_setMinutes(call FunctionCall) Value { obj := r.toObject(call.This) if d, ok := obj.self.(*dateObject); ok { - if d.isSet { - min := int(call.Argument(0).ToInteger()) - var sec, nsec int - if len(call.Arguments) > 1 { - sec = int(call.Arguments[1].ToInteger()) - } else { - sec = d.time.Second() - } - if len(call.Arguments) > 2 { - nsec = int(call.Arguments[2].ToInteger() * 1e6) - } else { - nsec = d.time.Nanosecond() + if d.isSet() { + t, ok := _dateSetFullYear(d.time(), call, -4, false) + if !ok { + d.unset() + return _NaN } - d.time = time.Date(d.time.Year(), d.time.Month(), d.time.Day(), d.time.Hour(), min, sec, nsec, time.Local) - return intToValue(timeToMsec(d.time)) + return d.setTimeMs(timeToMsec(t)) } else { return _NaN } } - r.typeErrorResult(true, "Method Date.prototype.setMinutes is called on incompatible receiver") - panic("Unreachable") + panic(r.NewTypeError("Method Date.prototype.setMinutes is called on incompatible receiver")) } func (r *Runtime) dateproto_setUTCMinutes(call FunctionCall) Value { obj := r.toObject(call.This) if d, ok := obj.self.(*dateObject); ok { - if d.isSet { - min := int(call.Argument(0).ToInteger()) - var sec, nsec int - t := d.time.In(time.UTC) - if len(call.Arguments) > 1 { - sec = int(call.Arguments[1].ToInteger()) - } else { - sec = t.Second() + if d.isSet() { + t, ok := _dateSetFullYear(d.timeUTC(), call, -4, true) + if !ok { + d.unset() + return _NaN } - if len(call.Arguments) > 2 { - nsec = int(call.Arguments[2].ToInteger() * 1e6) - } else { - nsec = t.Nanosecond() - } - d.time = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), min, sec, nsec, time.UTC).In(time.Local) - return intToValue(timeToMsec(d.time)) + return d.setTimeMs(timeToMsec(t)) } else { return _NaN } } - r.typeErrorResult(true, "Method Date.prototype.setUTCMinutes is called on incompatible receiver") - panic("Unreachable") + panic(r.NewTypeError("Method Date.prototype.setUTCMinutes is called on incompatible receiver")) } func (r *Runtime) dateproto_setHours(call FunctionCall) Value { obj := r.toObject(call.This) if d, ok := obj.self.(*dateObject); ok { - if d.isSet { - hour := int(call.Argument(0).ToInteger()) - var min, sec, nsec int - if len(call.Arguments) > 1 { - min = int(call.Arguments[1].ToInteger()) - } else { - min = d.time.Minute() - } - if len(call.Arguments) > 2 { - sec = int(call.Arguments[2].ToInteger()) - } else { - sec = d.time.Second() - } - if len(call.Arguments) > 3 { - nsec = int(call.Arguments[3].ToInteger() * 1e6) - } else { - nsec = d.time.Nanosecond() + if d.isSet() { + t, ok := _dateSetFullYear(d.time(), call, -3, false) + if !ok { + d.unset() + return _NaN } - d.time = time.Date(d.time.Year(), d.time.Month(), d.time.Day(), hour, min, sec, nsec, time.Local) - return intToValue(timeToMsec(d.time)) + return d.setTimeMs(timeToMsec(t)) } else { return _NaN } } - r.typeErrorResult(true, "Method Date.prototype.setHours is called on incompatible receiver") - panic("Unreachable") + panic(r.NewTypeError("Method Date.prototype.setHours is called on incompatible receiver")) } func (r *Runtime) dateproto_setUTCHours(call FunctionCall) Value { obj := r.toObject(call.This) if d, ok := obj.self.(*dateObject); ok { - if d.isSet { - hour := int(call.Argument(0).ToInteger()) - var min, sec, nsec int - t := d.time.In(time.UTC) - if len(call.Arguments) > 1 { - min = int(call.Arguments[1].ToInteger()) - } else { - min = t.Minute() - } - if len(call.Arguments) > 2 { - sec = int(call.Arguments[2].ToInteger()) - } else { - sec = t.Second() - } - if len(call.Arguments) > 3 { - nsec = int(call.Arguments[3].ToInteger() * 1e6) - } else { - nsec = t.Nanosecond() + if d.isSet() { + t, ok := _dateSetFullYear(d.timeUTC(), call, -3, true) + if !ok { + d.unset() + return _NaN } - d.time = time.Date(d.time.Year(), d.time.Month(), d.time.Day(), hour, min, sec, nsec, time.UTC).In(time.Local) - return intToValue(timeToMsec(d.time)) + return d.setTimeMs(timeToMsec(t)) } else { return _NaN } } - r.typeErrorResult(true, "Method Date.prototype.setUTCHours is called on incompatible receiver") - panic("Unreachable") + panic(r.NewTypeError("Method Date.prototype.setUTCHours is called on incompatible receiver")) } func (r *Runtime) dateproto_setDate(call FunctionCall) Value { obj := r.toObject(call.This) if d, ok := obj.self.(*dateObject); ok { - if d.isSet { - d.time = time.Date(d.time.Year(), d.time.Month(), int(call.Argument(0).ToInteger()), d.time.Hour(), d.time.Minute(), d.time.Second(), d.time.Nanosecond(), time.Local) - return intToValue(timeToMsec(d.time)) + if d.isSet() { + t, ok := _dateSetFullYear(d.time(), limitCallArgs(call, 1), -2, false) + if !ok { + d.unset() + return _NaN + } + return d.setTimeMs(timeToMsec(t)) } else { return _NaN } } - r.typeErrorResult(true, "Method Date.prototype.setDate is called on incompatible receiver") - panic("Unreachable") + panic(r.NewTypeError("Method Date.prototype.setDate is called on incompatible receiver")) } func (r *Runtime) dateproto_setUTCDate(call FunctionCall) Value { obj := r.toObject(call.This) if d, ok := obj.self.(*dateObject); ok { - if d.isSet { - t := d.time.In(time.UTC) - d.time = time.Date(t.Year(), t.Month(), int(call.Argument(0).ToInteger()), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), time.UTC).In(time.Local) - return intToValue(timeToMsec(d.time)) + if d.isSet() { + t, ok := _dateSetFullYear(d.timeUTC(), limitCallArgs(call, 1), -2, true) + if !ok { + d.unset() + return _NaN + } + return d.setTimeMs(timeToMsec(t)) } else { return _NaN } } - r.typeErrorResult(true, "Method Date.prototype.setUTCDate is called on incompatible receiver") - panic("Unreachable") + panic(r.NewTypeError("Method Date.prototype.setUTCDate is called on incompatible receiver")) } func (r *Runtime) dateproto_setMonth(call FunctionCall) Value { obj := r.toObject(call.This) if d, ok := obj.self.(*dateObject); ok { - if d.isSet { - month := time.Month(int(call.Argument(0).ToInteger()) + 1) - var day int - if len(call.Arguments) > 1 { - day = int(call.Arguments[1].ToInteger()) - } else { - day = d.time.Day() + if d.isSet() { + t, ok := _dateSetFullYear(d.time(), limitCallArgs(call, 2), -1, false) + if !ok { + d.unset() + return _NaN } - d.time = time.Date(d.time.Year(), month, day, d.time.Hour(), d.time.Minute(), d.time.Second(), d.time.Nanosecond(), time.Local) - return intToValue(timeToMsec(d.time)) + return d.setTimeMs(timeToMsec(t)) } else { return _NaN } } - r.typeErrorResult(true, "Method Date.prototype.setMonth is called on incompatible receiver") - panic("Unreachable") + panic(r.NewTypeError("Method Date.prototype.setMonth is called on incompatible receiver")) } func (r *Runtime) dateproto_setUTCMonth(call FunctionCall) Value { obj := r.toObject(call.This) if d, ok := obj.self.(*dateObject); ok { - if d.isSet { - month := time.Month(int(call.Argument(0).ToInteger()) + 1) - var day int - t := d.time.In(time.UTC) - if len(call.Arguments) > 1 { - day = int(call.Arguments[1].ToInteger()) - } else { - day = t.Day() + if d.isSet() { + t, ok := _dateSetFullYear(d.timeUTC(), limitCallArgs(call, 2), -1, true) + if !ok { + d.unset() + return _NaN } - d.time = time.Date(t.Year(), month, day, t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), time.UTC).In(time.Local) - return intToValue(timeToMsec(d.time)) + return d.setTimeMs(timeToMsec(t)) } else { return _NaN } } - r.typeErrorResult(true, "Method Date.prototype.setUTCMonth is called on incompatible receiver") - panic("Unreachable") + panic(r.NewTypeError("Method Date.prototype.setUTCMonth is called on incompatible receiver")) } func (r *Runtime) dateproto_setFullYear(call FunctionCall) Value { obj := r.toObject(call.This) if d, ok := obj.self.(*dateObject); ok { - if !d.isSet { - d.time = time.Unix(0, 0) - } - year := int(call.Argument(0).ToInteger()) - var month time.Month - var day int - if len(call.Arguments) > 1 { - month = time.Month(call.Arguments[1].ToInteger() + 1) + var t time.Time + if d.isSet() { + t = d.time() } else { - month = d.time.Month() + t = time.Date(1970, time.January, 1, 0, 0, 0, 0, time.Local) } - if len(call.Arguments) > 2 { - day = int(call.Arguments[2].ToInteger()) - } else { - day = d.time.Day() + t, ok := _dateSetFullYear(t, limitCallArgs(call, 3), 0, false) + if !ok { + d.unset() + return _NaN } - d.time = time.Date(year, month, day, d.time.Hour(), d.time.Minute(), d.time.Second(), d.time.Nanosecond(), time.Local) - return intToValue(timeToMsec(d.time)) + return d.setTimeMs(timeToMsec(t)) } - r.typeErrorResult(true, "Method Date.prototype.setFullYear is called on incompatible receiver") - panic("Unreachable") + panic(r.NewTypeError("Method Date.prototype.setFullYear is called on incompatible receiver")) } func (r *Runtime) dateproto_setUTCFullYear(call FunctionCall) Value { obj := r.toObject(call.This) if d, ok := obj.self.(*dateObject); ok { - if !d.isSet { - d.time = time.Unix(0, 0) - } - year := int(call.Argument(0).ToInteger()) - var month time.Month - var day int - t := d.time.In(time.UTC) - if len(call.Arguments) > 1 { - month = time.Month(call.Arguments[1].ToInteger() + 1) + var t time.Time + if d.isSet() { + t = d.timeUTC() } else { - month = t.Month() + t = time.Date(1970, time.January, 1, 0, 0, 0, 0, time.UTC) } - if len(call.Arguments) > 2 { - day = int(call.Arguments[2].ToInteger()) - } else { - day = t.Day() + t, ok := _dateSetFullYear(t, limitCallArgs(call, 3), 0, true) + if !ok { + d.unset() + return _NaN } - d.time = time.Date(year, month, day, t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), time.UTC).In(time.Local) - return intToValue(timeToMsec(d.time)) + return d.setTimeMs(timeToMsec(t)) } - r.typeErrorResult(true, "Method Date.prototype.setUTCFullYear is called on incompatible receiver") - panic("Unreachable") + panic(r.NewTypeError("Method Date.prototype.setUTCFullYear is called on incompatible receiver")) } func (r *Runtime) createDateProto(val *Object) objectImpl { @@ -899,6 +993,8 @@ func (r *Runtime) createDateProto(val *Object) objectImpl { o._putProp("toISOString", r.newNativeFunc(r.dateproto_toISOString, nil, "toISOString", nil, 0), true, false, true) o._putProp("toJSON", r.newNativeFunc(r.dateproto_toJSON, nil, "toJSON", nil, 1), true, false, true) + o._putSym(symToPrimitive, valueProp(r.newNativeFunc(r.dateproto_toPrimitive, nil, "[Symbol.toPrimitive]", nil, 1), false, false, true)) + return o } @@ -913,13 +1009,8 @@ func (r *Runtime) createDate(val *Object) objectImpl { } func (r *Runtime) initDate() { - //r.global.DatePrototype = r.newObject() - //o := r.global.DatePrototype.self r.global.DatePrototype = r.newLazyObject(r.createDateProto) - //r.global.Date = r.newNativeFunc(r.builtin_date, r.builtin_newDate, "Date", r.global.DatePrototype, 7) - //o := r.global.Date.self r.global.Date = r.newLazyObject(r.createDate) - r.addToGlobal("Date", r.global.Date) } diff --git a/date.go b/date.go index a22deb1a..1df98cee 100644 --- a/date.go +++ b/date.go @@ -1,6 +1,7 @@ package goja import ( + "math" "time" ) @@ -13,12 +14,14 @@ const ( datetimeLayout_en_GB = "01/02/2006, 15:04:05" dateLayout_en_GB = "01/02/2006" timeLayout_en_GB = "15:04:05" + + maxTime = 8.64e15 + timeUnset = math.MinInt64 ) type dateObject struct { baseObject - time time.Time - isSet bool + msec int64 } var ( @@ -65,7 +68,7 @@ func dateParse(date string) (time.Time, bool) { } } unix := timeToMsec(t) - return t, err == nil && unix >= -8640000000000000 && unix <= 8640000000000000 + return t, err == nil && unix >= -maxTime && unix <= maxTime } func (r *Runtime) newDateObject(t time.Time, isSet bool, proto *Object) *Object { @@ -77,8 +80,11 @@ func (r *Runtime) newDateObject(t time.Time, isSet bool, proto *Object) *Object d.prototype = proto d.extensible = true d.init() - d.time = t.In(time.Local) - d.isSet = isSet + if isSet { + d.msec = timeToMsec(t) + } else { + d.msec = timeUnset + } return v } @@ -86,13 +92,68 @@ func dateFormat(t time.Time) string { return t.Local().Format(dateTimeLayout) } +func timeFromMsec(msec int64) time.Time { + sec := msec / 1000 + nsec := (msec % 1000) * 1e6 + return time.Unix(sec, nsec) +} + +func timeToMsec(t time.Time) int64 { + return t.Unix()*1000 + int64(t.Nanosecond())/1e6 +} + func (d *dateObject) toPrimitive() Value { return d.toPrimitiveString() } func (d *dateObject) export() interface{} { - if d.isSet { + if d.isSet() { return d.time } return nil } + +func (d *dateObject) setTime(year, m, day, hour, min, sec, nsec int64) Value { + t, ok := mkTime(year, m, day, hour, min, sec, nsec, time.Local) + if ok { + return d.setTimeMs(timeToMsec(t)) + } + d.unset() + return _NaN +} + +func (d *dateObject) setTimeUTC(year, m, day, hour, min, sec, nsec int64) Value { + t, ok := mkTime(year, m, day, hour, min, sec, nsec, time.UTC) + if ok { + t = t.In(time.Local) + return d.setTimeMs(timeToMsec(t)) + } + d.unset() + return _NaN +} + +func (d *dateObject) setTimeMs(ms int64) Value { + if ms >= 0 && ms <= maxTime || ms < 0 && ms >= -maxTime { + d.msec = ms + return intToValue(ms) + } + + d.unset() + return _NaN +} + +func (d *dateObject) isSet() bool { + return d.msec != timeUnset +} + +func (d *dateObject) unset() { + d.msec = timeUnset +} + +func (d *dateObject) time() time.Time { + return timeFromMsec(d.msec) +} + +func (d *dateObject) timeUTC() time.Time { + return timeFromMsec(d.msec).In(time.UTC) +} diff --git a/date_test.go b/date_test.go index 266d3db1..3c5e3d86 100644 --- a/date_test.go +++ b/date_test.go @@ -367,3 +367,13 @@ assert.sameValue(Date.parse(aboveRange), NaN, "parse above maximum time value"); testScript1(TESTLIB+SCRIPT, _undefined, t) } + +func TestDateMaxValues(t *testing.T) { + const SCRIPT = ` + assert.sameValue((new Date(0)).setUTCMilliseconds(8.64e15), 8.64e15); + assert.sameValue((new Date(0)).setUTCSeconds(8640000000000), 8.64e15); + assert.sameValue((new Date(0)).setUTCMilliseconds(-8.64e15), -8.64e15); + assert.sameValue((new Date(0)).setUTCSeconds(-8640000000000), -8.64e15); + ` + testScript1(TESTLIB+SCRIPT, _undefined, t) +} diff --git a/object.go b/object.go index 1b0639cc..e371d8e6 100644 --- a/object.go +++ b/object.go @@ -31,6 +31,12 @@ const ( classStringIterator = "String Iterator" ) +var ( + hintDefault Value = asciiString("default") + hintNumber Value = asciiString("number") + hintString Value = asciiString("string") +) + type weakCollection interface { removeId(uint64) } @@ -802,19 +808,8 @@ func (o *baseObject) _putSym(s *valueSymbol, prop Value) { o.symValues.set(s, prop) } -func (o *baseObject) tryExoticToPrimitive(hint string) Value { - exoticToPrimitive := toMethod(o.getSym(symToPrimitive, nil)) - if exoticToPrimitive != nil { - return exoticToPrimitive(FunctionCall{ - This: o.val, - Arguments: []Value{newStringValue(hint)}, - }) - } - return nil -} - -func (o *baseObject) tryPrimitive(methodName string) Value { - if method, ok := o.val.self.getStr(unistring.String(methodName), nil).(*Object); ok { +func (o *baseObject) tryPrimitive(methodName unistring.String) Value { + if method, ok := o.val.self.getStr(methodName, nil).(*Object); ok { if call, ok := method.self.assertCallable(); ok { v := call(FunctionCall{ This: o.val, @@ -828,10 +823,6 @@ func (o *baseObject) tryPrimitive(methodName string) Value { } func (o *baseObject) toPrimitiveNumber() Value { - if v := o.tryExoticToPrimitive("number"); v != nil { - return v - } - if v := o.tryPrimitive("valueOf"); v != nil { return v } @@ -845,24 +836,67 @@ func (o *baseObject) toPrimitiveNumber() Value { } func (o *baseObject) toPrimitiveString() Value { - if v := o.tryExoticToPrimitive("string"); v != nil { + if v := o.tryPrimitive("toString"); v != nil { return v } - if v := o.tryPrimitive("toString"); v != nil { + if v := o.tryPrimitive("valueOf"); v != nil { return v } + o.val.runtime.typeErrorResult(true, "Could not convert %v to primitive", o) + return nil +} + +func (o *baseObject) toPrimitive() Value { if v := o.tryPrimitive("valueOf"); v != nil { return v } + if v := o.tryPrimitive("toString"); v != nil { + return v + } + o.val.runtime.typeErrorResult(true, "Could not convert %v to primitive", o) return nil } -func (o *baseObject) toPrimitive() Value { - return o.toPrimitiveNumber() +func (o *Object) tryExoticToPrimitive(hint Value) Value { + exoticToPrimitive := toMethod(o.self.getSym(symToPrimitive, nil)) + if exoticToPrimitive != nil { + ret := exoticToPrimitive(FunctionCall{ + This: o, + Arguments: []Value{hint}, + }) + if _, fail := ret.(*Object); !fail { + return ret + } + panic(o.runtime.NewTypeError("Cannot convert object to primitive value")) + } + return nil +} + +func (o *Object) toPrimitiveNumber() Value { + if v := o.tryExoticToPrimitive(hintNumber); v != nil { + return v + } + + return o.self.toPrimitiveNumber() +} + +func (o *Object) toPrimitiveString() Value { + if v := o.tryExoticToPrimitive(hintString); v != nil { + return v + } + + return o.self.toPrimitiveString() +} + +func (o *Object) toPrimitive() Value { + if v := o.tryExoticToPrimitive(hintDefault); v != nil { + return v + } + return o.self.toPrimitive() } func (o *baseObject) assertCallable() (func(FunctionCall) Value, bool) { diff --git a/runtime.go b/runtime.go index 119c62e1..0788fb9f 100644 --- a/runtime.go +++ b/runtime.go @@ -1963,3 +1963,11 @@ func isRegexp(v Value) bool { } return false } + +func limitCallArgs(call FunctionCall, n int) FunctionCall { + if len(call.Arguments) > n { + return FunctionCall{This: call.This, Arguments: call.Arguments[:n]} + } else { + return call + } +} diff --git a/string_ascii.go b/string_ascii.go index ea4d92ec..7487e6e8 100644 --- a/string_ascii.go +++ b/string_ascii.go @@ -201,7 +201,7 @@ func (s asciiString) Equals(other Value) bool { } if o, ok := other.(*Object); ok { - return s.Equals(o.self.toPrimitive()) + return s.Equals(o.toPrimitive()) } return false } diff --git a/string_unicode.go b/string_unicode.go index 53238f48..8fea1aca 100644 --- a/string_unicode.go +++ b/string_unicode.go @@ -208,7 +208,7 @@ func (s unicodeString) Equals(other Value) bool { } if o, ok := other.(*Object); ok { - return s.Equals(o.self.toPrimitive()) + return s.Equals(o.toPrimitive()) } return false } diff --git a/tc39_test.go b/tc39_test.go index a704ce9c..d8eb59d0 100644 --- a/tc39_test.go +++ b/tc39_test.go @@ -61,6 +61,8 @@ var ( "test/language/statements/class/subclass/builtin-objects/String/super-must-be-called.js": true, "test/language/statements/class/subclass/builtin-objects/String/regular-subclassing.js": true, "test/language/statements/class/subclass/builtin-objects/String/length.js": true, + "test/language/statements/class/subclass/builtin-objects/Date/super-must-be-called.js": true, + "test/language/statements/class/subclass/builtin-objects/Date/regular-subclassing.js": true, // full unicode regexp flag "test/built-ins/RegExp/prototype/Symbol.match/u-advance-after-empty.js": true, @@ -106,6 +108,7 @@ var ( "12.9.4", "19.1", "19.4", + "20.3", "21.1", "21.2.5.6", "22.1.2.1", @@ -130,6 +133,7 @@ var ( "sec-array.prototype.includes", "sec-%typedarray%", "sec-string.prototype", + "sec-date", } ) diff --git a/value.go b/value.go index d86e3141..1aaf0c62 100644 --- a/value.go +++ b/value.go @@ -189,7 +189,7 @@ func (i valueInt) Equals(other Value) bool { case valueBool: return int64(i) == o.ToInteger() case *Object: - return i.Equals(o.self.toPrimitiveNumber()) + return i.Equals(o.toPrimitiveNumber()) } return false @@ -527,16 +527,20 @@ func (p *valueProperty) hash(*maphash.Hash) uint64 { panic("valueProperty should never be used in maps or sets") } -func (f valueFloat) ToInteger() int64 { +func floatToIntClip(n float64) int64 { switch { - case math.IsNaN(float64(f)): + case math.IsNaN(n): return 0 - case math.IsInf(float64(f), 1): - return int64(math.MaxInt64) - case math.IsInf(float64(f), -1): - return int64(math.MinInt64) + case n >= math.MaxInt64: + return math.MaxInt64 + case n <= math.MinInt64: + return math.MinInt64 } - return int64(f) + return int64(n) +} + +func (f valueFloat) ToInteger() int64 { + return floatToIntClip(float64(f)) } func (f valueFloat) toString() valueString { @@ -623,7 +627,7 @@ func (f valueFloat) Equals(other Value) bool { case valueString, valueBool: return float64(f) == o.ToFloat() case *Object: - return f.Equals(o.self.toPrimitiveNumber()) + return f.Equals(o.toPrimitiveNumber()) } return false @@ -660,27 +664,27 @@ func (f valueFloat) hash(*maphash.Hash) uint64 { } func (o *Object) ToInteger() int64 { - return o.self.toPrimitiveNumber().ToNumber().ToInteger() + return o.toPrimitiveNumber().ToNumber().ToInteger() } func (o *Object) toString() valueString { - return o.self.toPrimitiveString().toString() + return o.toPrimitiveString().toString() } func (o *Object) string() unistring.String { - return o.self.toPrimitiveString().string() + return o.toPrimitiveString().string() } func (o *Object) ToPrimitiveString() Value { - return o.self.toPrimitiveString().ToPrimitiveString() + return o.toPrimitiveString().ToPrimitiveString() } func (o *Object) String() string { - return o.self.toPrimitiveString().String() + return o.toPrimitiveString().String() } func (o *Object) ToFloat() float64 { - return o.self.toPrimitiveNumber().ToFloat() + return o.toPrimitiveNumber().ToFloat() } func (o *Object) ToBoolean() bool { @@ -692,7 +696,7 @@ func (o *Object) ToObject(*Runtime) *Object { } func (o *Object) ToNumber() Value { - return o.self.toPrimitiveNumber().ToNumber() + return o.toPrimitiveNumber().ToNumber() } func (o *Object) SameAs(other Value) bool { @@ -709,7 +713,7 @@ func (o *Object) Equals(other Value) bool { switch o1 := other.(type) { case valueInt, valueFloat, valueString: - return o.self.toPrimitive().Equals(other) + return o.toPrimitive().Equals(other) case valueBool: return o.Equals(o1.ToNumber()) } diff --git a/vm.go b/vm.go index eff4b4c3..f3f1b7fe 100644 --- a/vm.go +++ b/vm.go @@ -133,7 +133,7 @@ func intToValue(i int64) Value { } return valueInt(i) } - return valueFloat(float64(i)) + return valueFloat(i) } func floatToInt(f float64) (result int64, ok bool) { @@ -617,11 +617,11 @@ func (_add) exec(vm *vm) { left := vm.stack[vm.sp-2] if o, ok := left.(*Object); ok { - left = o.self.toPrimitive() + left = o.toPrimitive() } if o, ok := right.(*Object); ok { - right = o.self.toPrimitive() + right = o.toPrimitive() } var ret Value @@ -1983,7 +1983,14 @@ func (_not) exec(vm *vm) { func toPrimitiveNumber(v Value) Value { if o, ok := v.(*Object); ok { - return o.self.toPrimitiveNumber() + return o.toPrimitiveNumber() + } + return v +} + +func toPrimitive(v Value) Value { + if o, ok := v.(*Object); ok { + return o.toPrimitive() } return v } From 33474631bf6e55491d2342e0bc2aa4b2efc048c3 Mon Sep 17 00:00:00 2001 From: Dmitry Panov Date: Tue, 14 Apr 2020 15:35:07 +0100 Subject: [PATCH 25/46] Applied fix for #148 to for-of loops. --- compiler_stmt.go | 2 +- compiler_test.go | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/compiler_stmt.go b/compiler_stmt.go index be30948f..e64b7160 100644 --- a/compiler_stmt.go +++ b/compiler_stmt.go @@ -345,7 +345,7 @@ func (c *compiler) compileForOfStatement(v *ast.ForOfStatement, needResult bool) func (c *compiler) compileLabeledForOfStatement(v *ast.ForOfStatement, needResult bool, label unistring.String) { c.block = &block{ - typ: blockLoop, + typ: blockLoopEnum, outer: c.block, label: label, needResult: needResult, diff --git a/compiler_test.go b/compiler_test.go index 5b0b3e0c..771e76ed 100644 --- a/compiler_test.go +++ b/compiler_test.go @@ -2032,6 +2032,17 @@ func TestReturnFromForInLoop(t *testing.T) { testScript1(SCRIPT, valueTrue, t) } +func TestReturnFromForOfLoop(t *testing.T) { + const SCRIPT = ` + (function f() { + for (var i of [1]) { + return true; + } + })(); + ` + testScript1(SCRIPT, valueTrue, t) +} + func TestIfStackLeaks(t *testing.T) { const SCRIPT = ` var t = 0; From d0b8fda54cd050cf727e06a1a7fe839ccab1abe6 Mon Sep 17 00:00:00 2001 From: Dmitry Panov Date: Fri, 24 Apr 2020 16:21:03 +0100 Subject: [PATCH 26/46] Added es6 Number methods, switched to own formatter. --- builtin_number.go | 132 ++++-- compiler_test.go | 4 +- ftoa/common.go | 149 +++++++ dtoa.go => ftoa/ftobasestr.go | 149 +------ ftoa/ftobasestr_test.go | 9 + ftoa/ftostr.go | 814 ++++++++++++++++++++++++++++++++++ ftoa/ftostr_test.go | 37 ++ runtime.go | 15 +- runtime_test.go | 23 + string_ascii.go | 9 +- tc39_test.go | 4 + value.go | 26 +- 12 files changed, 1161 insertions(+), 210 deletions(-) create mode 100644 ftoa/common.go rename dtoa.go => ftoa/ftobasestr.go (57%) create mode 100644 ftoa/ftobasestr_test.go create mode 100644 ftoa/ftostr.go create mode 100644 ftoa/ftostr_test.go diff --git a/builtin_number.go b/builtin_number.go index 2154e481..6a15bd56 100644 --- a/builtin_number.go +++ b/builtin_number.go @@ -2,7 +2,8 @@ package goja import ( "math" - "strconv" + + "github.com/dop251/goja/ftoa" ) func (r *Runtime) numberproto_valueOf(call FunctionCall) Value { @@ -65,84 +66,141 @@ func (r *Runtime) numberproto_toString(call FunctionCall) Value { } if radix == 10 { - var fmt byte - if math.Abs(num) >= 1e21 { - fmt = 'e' - } else { - fmt = 'f' - } - return asciiString(strconv.FormatFloat(num, fmt, -1, 64)) + return asciiString(fToStr(num, ftoa.ModeStandard, 0)) } - return asciiString(dtobasestr(num, radix)) + return asciiString(ftoa.FToBaseStr(num, radix)) } func (r *Runtime) numberproto_toFixed(call FunctionCall) Value { + num := r.toNumber(call.This).ToFloat() prec := call.Argument(0).ToInteger() - if prec < 0 || prec > 20 { - panic(r.newError(r.global.RangeError, "toFixed() precision must be between 0 and 20")) - } - num := call.This.ToFloat() + if prec < 0 || prec > 100 { + panic(r.newError(r.global.RangeError, "toFixed() precision must be between 0 and 100")) + } if math.IsNaN(num) { return stringNaN } - if math.Abs(num) >= 1e21 { - return asciiString(strconv.FormatFloat(num, 'g', -1, 64)) - } - return asciiString(strconv.FormatFloat(num, 'f', int(prec), 64)) + return asciiString(fToStr(num, ftoa.ModeFixed, int(prec))) } func (r *Runtime) numberproto_toExponential(call FunctionCall) Value { - prec := call.Argument(0).ToInteger() - if prec < 0 || prec > 20 { - panic(r.newError(r.global.RangeError, "toExponential() precision must be between 0 and 20")) + num := r.toNumber(call.This).ToFloat() + precVal := call.Argument(0) + var prec int64 + if precVal == _undefined { + return asciiString(fToStr(num, ftoa.ModeStandardExponential, 0)) + } else { + prec = precVal.ToInteger() } - num := call.This.ToFloat() if math.IsNaN(num) { return stringNaN } - if math.Abs(num) >= 1e21 { - return asciiString(strconv.FormatFloat(num, 'g', -1, 64)) + if math.IsInf(num, 1) { + return stringInfinity + } + if math.IsInf(num, -1) { + return stringNegInfinity + } + + if prec < 0 || prec > 100 { + panic(r.newError(r.global.RangeError, "toExponential() precision must be between 0 and 100")) } - return asciiString(strconv.FormatFloat(num, 'e', int(prec), 64)) + + return asciiString(fToStr(num, ftoa.ModeExponential, int(prec+1))) } func (r *Runtime) numberproto_toPrecision(call FunctionCall) Value { - prec := call.Argument(0).ToInteger() - if prec < 0 || prec > 20 { - panic(r.newError(r.global.RangeError, "toPrecision() precision must be between 0 and 20")) + numVal := r.toNumber(call.This) + precVal := call.Argument(0) + if precVal == _undefined { + return numVal.toString() } + num := numVal.ToFloat() + prec := precVal.ToInteger() - num := call.This.ToFloat() if math.IsNaN(num) { return stringNaN } - if math.Abs(num) >= 1e21 { - return asciiString(strconv.FormatFloat(num, 'g', -1, 64)) + if math.IsInf(num, 1) { + return stringInfinity + } + if math.IsInf(num, -1) { + return stringNegInfinity + } + if prec < 1 || prec > 100 { + panic(r.newError(r.global.RangeError, "toPrecision() precision must be between 1 and 100")) + } + + return asciiString(fToStr(num, ftoa.ModePrecision, int(prec))) +} + +func (r *Runtime) number_isFinite(call FunctionCall) Value { + switch arg := call.Argument(0).(type) { + case valueInt: + return valueTrue + case valueFloat: + f := float64(arg) + return r.toBoolean(!math.IsInf(f, 0) && !math.IsNaN(f)) + default: + return valueFalse + } +} + +func (r *Runtime) number_isInteger(call FunctionCall) Value { + switch arg := call.Argument(0).(type) { + case valueInt: + return valueTrue + case valueFloat: + f := float64(arg) + return r.toBoolean(!math.IsNaN(f) && !math.IsInf(f, 0) && math.Floor(f) == f) + default: + return valueFalse + } +} + +func (r *Runtime) number_isNaN(call FunctionCall) Value { + if f, ok := call.Argument(0).(valueFloat); ok && math.IsNaN(float64(f)) { + return valueTrue + } + return valueFalse +} + +func (r *Runtime) number_isSafeInteger(call FunctionCall) Value { + if i, ok := call.Argument(0).(valueInt); ok && i >= -(maxInt-1) && i <= maxInt-1 { + return valueTrue } - return asciiString(strconv.FormatFloat(num, 'g', int(prec), 64)) + return valueFalse } func (r *Runtime) initNumber() { r.global.NumberPrototype = r.newPrimitiveObject(valueInt(0), r.global.ObjectPrototype, classNumber) o := r.global.NumberPrototype.self - o._putProp("valueOf", r.newNativeFunc(r.numberproto_valueOf, nil, "valueOf", nil, 0), true, false, true) - o._putProp("toString", r.newNativeFunc(r.numberproto_toString, nil, "toString", nil, 0), true, false, true) - o._putProp("toLocaleString", r.newNativeFunc(r.numberproto_toString, nil, "toLocaleString", nil, 0), true, false, true) - o._putProp("toFixed", r.newNativeFunc(r.numberproto_toFixed, nil, "toFixed", nil, 1), true, false, true) o._putProp("toExponential", r.newNativeFunc(r.numberproto_toExponential, nil, "toExponential", nil, 1), true, false, true) + o._putProp("toFixed", r.newNativeFunc(r.numberproto_toFixed, nil, "toFixed", nil, 1), true, false, true) + o._putProp("toLocaleString", r.newNativeFunc(r.numberproto_toString, nil, "toLocaleString", nil, 0), true, false, true) o._putProp("toPrecision", r.newNativeFunc(r.numberproto_toPrecision, nil, "toPrecision", nil, 1), true, false, true) + o._putProp("toString", r.newNativeFunc(r.numberproto_toString, nil, "toString", nil, 1), true, false, true) + o._putProp("valueOf", r.newNativeFunc(r.numberproto_valueOf, nil, "valueOf", nil, 0), true, false, true) r.global.Number = r.newNativeFunc(r.builtin_Number, r.builtin_newNumber, "Number", r.global.NumberPrototype, 1) o = r.global.Number.self - o._putProp("MAX_VALUE", valueFloat(math.MaxFloat64), false, false, false) + o._putProp("EPSILON", _epsilon, false, false, false) + o._putProp("isFinite", r.newNativeFunc(r.number_isFinite, nil, "isFinite", nil, 1), true, false, true) + o._putProp("isInteger", r.newNativeFunc(r.number_isInteger, nil, "isInteger", nil, 1), true, false, true) + o._putProp("isNaN", r.newNativeFunc(r.number_isNaN, nil, "isNaN", nil, 1), true, false, true) + o._putProp("isSafeInteger", r.newNativeFunc(r.number_isSafeInteger, nil, "isSafeInteger", nil, 1), true, false, true) + o._putProp("MAX_SAFE_INTEGER", valueInt(maxInt-1), false, false, false) + o._putProp("MIN_SAFE_INTEGER", valueInt(-(maxInt - 1)), false, false, false) o._putProp("MIN_VALUE", valueFloat(math.SmallestNonzeroFloat64), false, false, false) + o._putProp("MAX_VALUE", valueFloat(math.MaxFloat64), false, false, false) o._putProp("NaN", _NaN, false, false, false) o._putProp("NEGATIVE_INFINITY", _negativeInf, false, false, false) + o._putProp("parseFloat", r.Get("parseFloat"), true, false, true) + o._putProp("parseInt", r.Get("parseInt"), true, false, true) o._putProp("POSITIVE_INFINITY", _positiveInf, false, false, false) - o._putProp("EPSILON", _epsilon, false, false, false) r.addToGlobal("Number", r.global.Number) } diff --git a/compiler_test.go b/compiler_test.go index 771e76ed..08fd4230 100644 --- a/compiler_test.go +++ b/compiler_test.go @@ -890,10 +890,10 @@ func TestPostDecObj(t *testing.T) { func TestPropAcc1(t *testing.T) { const SCRIPT = ` - 1..toString() === "1" + 1..toString() ` - testScript1(SCRIPT, valueTrue, t) + testScript1(SCRIPT, asciiString("1"), t) } func TestEvalDirect(t *testing.T) { diff --git a/ftoa/common.go b/ftoa/common.go new file mode 100644 index 00000000..5906464f --- /dev/null +++ b/ftoa/common.go @@ -0,0 +1,149 @@ +/* +Package ftoa provides ECMAScript-compliant floating point number conversion to string. +It contains code ported from Rhino (https://github.com/mozilla/rhino/blob/master/src/org/mozilla/javascript/DToA.java) +*/ +package ftoa + +import ( + "math" + "math/big" +) + +const ( + frac_mask = 0xfffff + exp_shift = 20 + exp_msk1 = 0x100000 + + exp_shiftL = 52 + exp_mask_shifted = 0x7ff + frac_maskL = 0xfffffffffffff + exp_msk1L = 0x10000000000000 + exp_shift1 = 20 + exp_mask = 0x7ff00000 + bias = 1023 + p = 53 + bndry_mask = 0xfffff + log2P = 1 +) + +func lo0bits(x uint32) (k int) { + + if (x & 7) != 0 { + if (x & 1) != 0 { + return 0 + } + if (x & 2) != 0 { + return 1 + } + return 2 + } + if (x & 0xffff) == 0 { + k = 16 + x >>= 16 + } + if (x & 0xff) == 0 { + k += 8 + x >>= 8 + } + if (x & 0xf) == 0 { + k += 4 + x >>= 4 + } + if (x & 0x3) == 0 { + k += 2 + x >>= 2 + } + if (x & 1) == 0 { + k++ + x >>= 1 + if (x & 1) == 0 { + return 32 + } + } + return +} + +func hi0bits(x uint32) (k int) { + + if (x & 0xffff0000) == 0 { + k = 16 + x <<= 16 + } + if (x & 0xff000000) == 0 { + k += 8 + x <<= 8 + } + if (x & 0xf0000000) == 0 { + k += 4 + x <<= 4 + } + if (x & 0xc0000000) == 0 { + k += 2 + x <<= 2 + } + if (x & 0x80000000) == 0 { + k++ + if (x & 0x40000000) == 0 { + return 32 + } + } + return +} + +func stuffBits(bits []byte, offset int, val uint32) { + bits[offset] = byte(val >> 24) + bits[offset+1] = byte(val >> 16) + bits[offset+2] = byte(val >> 8) + bits[offset+3] = byte(val) +} + +func d2b(d float64) (b *big.Int, e, bits int) { + dBits := math.Float64bits(d) + d0 := uint32(dBits >> 32) + d1 := uint32(dBits) + + z := d0 & frac_mask + d0 &= 0x7fffffff /* clear sign bit, which we ignore */ + + var de, k, i int + var dbl_bits []byte + if de = int(d0 >> exp_shift); de != 0 { + z |= exp_msk1 + } + + y := d1 + if y != 0 { + dbl_bits = make([]byte, 8) + k = lo0bits(y) + y >>= k + if k != 0 { + stuffBits(dbl_bits, 4, y|z<<(32-k)) + z >>= k + } else { + stuffBits(dbl_bits, 4, y) + } + stuffBits(dbl_bits, 0, z) + if z != 0 { + i = 2 + } else { + i = 1 + } + } else { + dbl_bits = make([]byte, 4) + k = lo0bits(z) + z >>= k + stuffBits(dbl_bits, 0, z) + k += 32 + i = 1 + } + + if de != 0 { + e = de - bias - (p - 1) + k + bits = p - k + } else { + e = de - bias - (p - 1) + 1 + k + bits = 32*i - hi0bits(z) + } + b = (&big.Int{}).SetBytes(dbl_bits) + return +} diff --git a/dtoa.go b/ftoa/ftobasestr.go similarity index 57% rename from dtoa.go rename to ftoa/ftobasestr.go index 0962eb85..a79050fe 100644 --- a/dtoa.go +++ b/ftoa/ftobasestr.go @@ -1,157 +1,18 @@ -package goja - -// Ported from Rhino (https://github.com/mozilla/rhino/blob/master/src/org/mozilla/javascript/DToA.java) +package ftoa import ( - "bytes" "fmt" "math" "math/big" "strconv" + "strings" ) const ( - frac_mask = 0xfffff - exp_shift = 20 - exp_msk1 = 0x100000 - - exp_shiftL = 52 - exp_mask_shifted = 0x7ff - frac_maskL = 0xfffffffffffff - exp_msk1L = 0x10000000000000 - exp_shift1 = 20 - exp_mask = 0x7ff00000 - bias = 1023 - p = 53 - bndry_mask = 0xfffff - log2P = 1 - digits = "0123456789abcdefghijklmnopqrstuvwxyz" ) -func lo0bits(x uint32) (k uint32) { - - if (x & 7) != 0 { - if (x & 1) != 0 { - return 0 - } - if (x & 2) != 0 { - return 1 - } - return 2 - } - if (x & 0xffff) == 0 { - k = 16 - x >>= 16 - } - if (x & 0xff) == 0 { - k += 8 - x >>= 8 - } - if (x & 0xf) == 0 { - k += 4 - x >>= 4 - } - if (x & 0x3) == 0 { - k += 2 - x >>= 2 - } - if (x & 1) == 0 { - k++ - x >>= 1 - if (x & 1) == 0 { - return 32 - } - } - return -} - -func hi0bits(x uint32) (k uint32) { - - if (x & 0xffff0000) == 0 { - k = 16 - x <<= 16 - } - if (x & 0xff000000) == 0 { - k += 8 - x <<= 8 - } - if (x & 0xf0000000) == 0 { - k += 4 - x <<= 4 - } - if (x & 0xc0000000) == 0 { - k += 2 - x <<= 2 - } - if (x & 0x80000000) == 0 { - k++ - if (x & 0x40000000) == 0 { - return 32 - } - } - return -} - -func stuffBits(bits []byte, offset int, val uint32) { - bits[offset] = byte(val >> 24) - bits[offset+1] = byte(val >> 16) - bits[offset+2] = byte(val >> 8) - bits[offset+3] = byte(val) -} - -func d2b(d float64) (b *big.Int, e int32, bits uint32) { - dBits := math.Float64bits(d) - d0 := uint32(dBits >> 32) - d1 := uint32(dBits) - - z := d0 & frac_mask - d0 &= 0x7fffffff /* clear sign bit, which we ignore */ - - var de, k, i uint32 - var dbl_bits []byte - if de = (d0 >> exp_shift); de != 0 { - z |= exp_msk1 - } - - y := d1 - if y != 0 { - dbl_bits = make([]byte, 8) - k = lo0bits(y) - y >>= k - if k != 0 { - stuffBits(dbl_bits, 4, y|z<<(32-k)) - z >>= k - } else { - stuffBits(dbl_bits, 4, y) - } - stuffBits(dbl_bits, 0, z) - if z != 0 { - i = 2 - } else { - i = 1 - } - } else { - dbl_bits = make([]byte, 4) - k = lo0bits(z) - z >>= k - stuffBits(dbl_bits, 0, z) - k += 32 - i = 1 - } - - if de != 0 { - e = int32(de - bias - (p - 1) + k) - bits = p - k - } else { - e = int32(de - bias - (p - 1) + 1 + k) - bits = 32*i - hi0bits(z) - } - b = (&big.Int{}).SetBytes(dbl_bits) - return -} - -func dtobasestr(num float64, radix int) string { +func FToBaseStr(num float64, radix int) string { var negative bool if num < 0 { num = -num @@ -194,7 +55,7 @@ func dtobasestr(num float64, radix int) string { return intDigits } else { /* We have a fraction. */ - var buffer bytes.Buffer + var buffer strings.Builder buffer.WriteString(intDigits) buffer.WriteByte('.') df := num - dfloor @@ -207,7 +68,7 @@ func dtobasestr(num float64, radix int) string { // JS_ASSERT(e < 0); /* At this point df = b * 2^e. e must be less than zero because 0 < df < 1. */ - s2 := -int32((word0 >> exp_shift1) & (exp_mask >> exp_shift1)) + s2 := -int((word0 >> exp_shift1) & (exp_mask >> exp_shift1)) if s2 == 0 { s2 = -1 } diff --git a/ftoa/ftobasestr_test.go b/ftoa/ftobasestr_test.go new file mode 100644 index 00000000..0a3fecb1 --- /dev/null +++ b/ftoa/ftobasestr_test.go @@ -0,0 +1,9 @@ +package ftoa + +import "testing" + +func TestFToBaseStr(t *testing.T) { + if s := FToBaseStr(0.8466400793967279, 36); s != "0.uh8u81s3fz" { + t.Fatal(s) + } +} diff --git a/ftoa/ftostr.go b/ftoa/ftostr.go new file mode 100644 index 00000000..f1c296bf --- /dev/null +++ b/ftoa/ftostr.go @@ -0,0 +1,814 @@ +package ftoa + +import ( + "math" + "math/big" + "strconv" +) + +const ( + exp_11 = 0x3ff00000 + frac_mask1 = 0xfffff + bletch = 0x10 + quick_max = 14 + int_max = 14 +) + +type FToStrMode int + +const ( + ModeStandard FToStrMode = iota /* Either fixed or exponential format; round-trip */ + ModeStandardExponential /* Always exponential format; round-trip */ + ModeFixed /* Round to digits after the decimal point; exponential if number is large */ + ModeExponential /* Always exponential format; significant digits */ + ModePrecision /* Either fixed or exponential format; significant digits */ +) + +var ( + tens = [...]float64{ + 1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, + 1e10, 1e11, 1e12, 1e13, 1e14, 1e15, 1e16, 1e17, 1e18, 1e19, + 1e20, 1e21, 1e22, + } + + bigtens = [...]float64{1e16, 1e32, 1e64, 1e128, 1e256} + + big5 = big.NewInt(5) + big10 = big.NewInt(10) + + dtoaModes = []int{ + ModeStandard: 0, + ModeStandardExponential: 0, + ModeFixed: 3, + ModeExponential: 2, + ModePrecision: 2, + } +) + +/* +mode: + 0 ==> shortest string that yields d when read in + and rounded to nearest. + 1 ==> like 0, but with Steele & White stopping rule; + e.g. with IEEE P754 arithmetic , mode 0 gives + 1e23 whereas mode 1 gives 9.999999999999999e22. + 2 ==> max(1,ndigits) significant digits. This gives a + return value similar to that of ecvt, except + that trailing zeros are suppressed. + 3 ==> through ndigits past the decimal point. This + gives a return value similar to that from fcvt, + except that trailing zeros are suppressed, and + ndigits can be negative. + 4,5 ==> similar to 2 and 3, respectively, but (in + round-nearest mode) with the tests of mode 0 to + possibly return a shorter string that rounds to d. + With IEEE arithmetic and compilation with + -DHonor_FLT_ROUNDS, modes 4 and 5 behave the same + as modes 2 and 3 when FLT_ROUNDS != 1. + 6-9 ==> Debugging modes similar to mode - 4: don't try + fast floating-point estimate (if applicable). + + Values of mode other than 0-9 are treated as mode 0. +*/ +func ftoa(d float64, mode int, biasUp bool, ndigits int, buf []byte) ([]byte, int) { + var sign bool + if math.IsNaN(d) { + buf = append(buf, "NaN"...) + return buf, 9999 + } + if math.Signbit(d) { + sign = true + d = math.Copysign(d, 1.0) + } else { + sign = false + } + if math.IsInf(d, 0) { + if sign { + buf = append(buf, '-') + } + buf = append(buf, "Infinity"...) + return buf, 9999 + } + dBits := math.Float64bits(d) + word0 := uint32(dBits >> 32) + word1 := uint32(dBits) + if d == 0 { + // no_digits + buf = append(buf, '0') + return buf, 1 + } + if sign { + buf = append(buf, '-') + } + b, be, bbits := d2b(d) + i := int((word0 >> exp_shift1) & (exp_mask >> exp_shift1)) + var d2 float64 + var denorm bool + if i != 0 { + d2 = setWord0(d, (word0&frac_mask1)|exp_11) + i -= bias + denorm = false + } else { + /* d is denormalized */ + i = bbits + be + (bias + (p - 1) - 1) + var x uint64 + if i > 32 { + x = uint64(word0)<<(64-i) | uint64(word1)>>(i-32) + } else { + x = uint64(word1) << (32 - i) + } + d2 = setWord0(float64(x), uint32((x>>32)-31*exp_mask)) + i -= (bias + (p - 1) - 1) + 1 + denorm = true + } + /* At this point d = f*2^i, where 1 <= f < 2. d2 is an approximation of f. */ + ds := (d2-1.5)*0.289529654602168 + 0.1760912590558 + float64(i)*0.301029995663981 + k := int(ds) + if ds < 0.0 && ds != float64(k) { + k-- /* want k = floor(ds) */ + } + k_check := true + if k >= 0 && k < len(tens) { + if d < tens[k] { + k-- + } + k_check = false + } + /* At this point floor(log10(d)) <= k <= floor(log10(d))+1. + If k_check is zero, we're guaranteed that k = floor(log10(d)). */ + j := bbits - i - 1 + var b2, s2, b5, s5 int + /* At this point d = b/2^j, where b is an odd integer. */ + if j >= 0 { + b2 = 0 + s2 = j + } else { + b2 = -j + s2 = 0 + } + if k >= 0 { + b5 = 0 + s5 = k + s2 += k + } else { + b2 -= k + b5 = -k + s5 = 0 + } + /* At this point d/10^k = (b * 2^b2 * 5^b5) / (2^s2 * 5^s5), where b is an odd integer, + b2 >= 0, b5 >= 0, s2 >= 0, and s5 >= 0. */ + if mode < 0 || mode > 9 { + mode = 0 + } + try_quick := true + if mode > 5 { + mode -= 4 + try_quick = false + } + leftright := true + var ilim, ilim1 int + switch mode { + case 0, 1: + ilim, ilim1 = -1, -1 + ndigits = 0 + case 2: + leftright = false + fallthrough + case 4: + if ndigits <= 0 { + ndigits = 1 + } + ilim, ilim1 = ndigits, ndigits + case 3: + leftright = false + fallthrough + case 5: + i = ndigits + k + 1 + ilim = i + ilim1 = i - 1 + } + /* ilim is the maximum number of significant digits we want, based on k and ndigits. */ + /* ilim1 is the maximum number of significant digits we want, based on k and ndigits, + when it turns out that k was computed too high by one. */ + fast_failed := false + if ilim >= 0 && ilim <= quick_max && try_quick { + + /* Try to get by with floating-point arithmetic. */ + + i = 0 + d2 = d + k0 := k + ilim0 := ilim + ieps := 2 /* conservative */ + /* Divide d by 10^k, keeping track of the roundoff error and avoiding overflows. */ + if k > 0 { + ds = tens[k&0xf] + j = k >> 4 + if (j & bletch) != 0 { + /* prevent overflows */ + j &= bletch - 1 + d /= bigtens[len(bigtens)-1] + ieps++ + } + for ; j != 0; i++ { + if (j & 1) != 0 { + ieps++ + ds *= bigtens[i] + } + j >>= 1 + } + d /= ds + } else if j1 := -k; j1 != 0 { + d *= tens[j1&0xf] + for j = j1 >> 4; j != 0; i++ { + if (j & 1) != 0 { + ieps++ + d *= bigtens[i] + } + j >>= 1 + } + } + /* Check that k was computed correctly. */ + if k_check && d < 1.0 && ilim > 0 { + if ilim1 <= 0 { + fast_failed = true + } else { + ilim = ilim1 + k-- + d *= 10. + ieps++ + } + } + /* eps bounds the cumulative error. */ + eps := float64(ieps)*d + 7.0 + eps = setWord0(eps, _word0(eps)-(p-1)*exp_msk1) + if ilim == 0 { + d -= 5.0 + if d > eps { + buf = append(buf, '1') + k++ + return buf, int(k + 1) + } + if d < -eps { + buf = append(buf, '0') + return buf, 1 + } + fast_failed = true + } + if !fast_failed { + fast_failed = true + if leftright { + /* Use Steele & White method of only + * generating digits needed. + */ + eps = 0.5/tens[ilim-1] - eps + for i = 0; ; { + l := int64(d) + d -= float64(l) + buf = append(buf, byte('0'+l)) + if d < eps { + return buf, k + 1 + } + if 1.0-d < eps { + buf, k = bumpUp(buf, k) + return buf, k + 1 + } + i++ + if i >= ilim { + break + } + eps *= 10.0 + d *= 10.0 + } + } else { + /* Generate ilim digits, then fix them up. */ + eps *= tens[ilim-1] + for i = 1; ; i++ { + l := int64(d) + d -= float64(l) + buf = append(buf, byte('0'+l)) + if i == ilim { + if d > 0.5+eps { + buf, k = bumpUp(buf, k) + return buf, k + 1 + } else if d < 0.5-eps { + buf = stripTrailingZeroes(buf) + return buf, k + 1 + } + break + } + d *= 10.0 + } + } + } + if fast_failed { + if sign { + buf = buf[:1] + } else { + buf = buf[:0] + } + d = d2 + k = k0 + ilim = ilim0 + } + } + + /* Do we have a "small" integer? */ + if be >= 0 && k <= int_max { + /* Yes. */ + ds = tens[k] + if ndigits < 0 && ilim <= 0 { + if ilim < 0 || d < 5*ds || (!biasUp && d == 5*ds) { + if sign { + buf = buf[:1] + } else { + buf = buf[:0] + } + buf = append(buf, '0') + return buf, 1 + } + buf = append(buf, '1') + k++ + return buf, k + 1 + } + for i = 1; ; i++ { + l := int64(d / ds) + d -= float64(l) * ds + buf = append(buf, byte('0'+l)) + if i == ilim { + d += d + if (d > ds) || (d == ds && (((l & 1) != 0) || biasUp)) { + buf, k = bumpUp(buf, k) + } + break + } + d *= 10.0 + if d == 0 { + break + } + } + return buf, k + 1 + } + + m2 := b2 + m5 := b5 + var mhi, mlo *big.Int + if leftright { + if mode < 2 { + if denorm { + i = be + (bias + (p - 1) - 1 + 1) + } else { + i = 1 + p - bbits + } + /* i is 1 plus the number of trailing zero bits in d's significand. Thus, + (2^m2 * 5^m5) / (2^(s2+i) * 5^s5) = (1/2 lsb of d)/10^k. */ + } else { + j = ilim - 1 + if m5 >= j { + m5 -= j + } else { + j -= m5 + s5 += j + b5 += j + m5 = 0 + } + i = ilim + if i < 0 { + m2 -= i + i = 0 + } + /* (2^m2 * 5^m5) / (2^(s2+i) * 5^s5) = (1/2 * 10^(1-ilim))/10^k. */ + } + b2 += i + s2 += i + mhi = big.NewInt(1) + /* (mhi * 2^m2 * 5^m5) / (2^s2 * 5^s5) = one-half of last printed (when mode >= 2) or + input (when mode < 2) significant digit, divided by 10^k. */ + } + + /* We still have d/10^k = (b * 2^b2 * 5^b5) / (2^s2 * 5^s5). Reduce common factors in + b2, m2, and s2 without changing the equalities. */ + if m2 > 0 && s2 > 0 { + if m2 < s2 { + i = m2 + } else { + i = s2 + } + b2 -= i + m2 -= i + s2 -= i + } + + /* Fold b5 into b and m5 into mhi. */ + if b5 > 0 { + if leftright { + if m5 > 0 { + pow5mult(mhi, m5) + b.Mul(mhi, b) + } + j = b5 - m5 + if j != 0 { + pow5mult(b, j) + } + } else { + pow5mult(b, b5) + } + } + /* Now we have d/10^k = (b * 2^b2) / (2^s2 * 5^s5) and + (mhi * 2^m2) / (2^s2 * 5^s5) = one-half of last printed or input significant digit, divided by 10^k. */ + + S := big.NewInt(1) + if s5 > 0 { + pow5mult(S, s5) + } + /* Now we have d/10^k = (b * 2^b2) / (S * 2^s2) and + (mhi * 2^m2) / (S * 2^s2) = one-half of last printed or input significant digit, divided by 10^k. */ + + /* Check for special case that d is a normalized power of 2. */ + spec_case := false + if mode < 2 { + if (_word1(d) == 0) && ((_word0(d) & bndry_mask) == 0) && + ((_word0(d) & (exp_mask & (exp_mask << 1))) != 0) { + /* The special case. Here we want to be within a quarter of the last input + significant digit instead of one half of it when the decimal output string's value is less than d. */ + b2 += log2P + s2 += log2P + spec_case = true + } + } + + /* Arrange for convenient computation of quotients: + * shift left if necessary so divisor has 4 leading 0 bits. + * + * Perhaps we should just compute leading 28 bits of S once + * and for all and pass them and a shift to quorem, so it + * can do shifts and ors to compute the numerator for q. + */ + S_bytes := S.Bytes() + var S_hiWord uint32 + for idx := 0; idx < 4; idx++ { + S_hiWord = S_hiWord << 8 + if idx < len(S_bytes) { + S_hiWord |= uint32(S_bytes[idx]) + } + } + var zz int + if s5 != 0 { + zz = 32 - hi0bits(S_hiWord) + } else { + zz = 1 + } + i = (zz + s2) & 0x1f + if i != 0 { + i = 32 - i + } + /* i is the number of leading zero bits in the most significant word of S*2^s2. */ + if i > 4 { + i -= 4 + b2 += i + m2 += i + s2 += i + } else if i < 4 { + i += 28 + b2 += i + m2 += i + s2 += i + } + /* Now S*2^s2 has exactly four leading zero bits in its most significant word. */ + if b2 > 0 { + b = b.Lsh(b, uint(b2)) + } + if s2 > 0 { + S.Lsh(S, uint(s2)) + } + /* Now we have d/10^k = b/S and + (mhi * 2^m2) / S = maximum acceptable error, divided by 10^k. */ + if k_check { + if b.Cmp(S) < 0 { + k-- + b.Mul(b, big10) /* we botched the k estimate */ + if leftright { + mhi.Mul(mhi, big10) + } + ilim = ilim1 + } + } + /* At this point 1 <= d/10^k = b/S < 10. */ + + if ilim <= 0 && mode > 2 { + /* We're doing fixed-mode output and d is less than the minimum nonzero output in this mode. + Output either zero or the minimum nonzero output depending on which is closer to d. */ + if ilim >= 0 { + i = b.Cmp(S.Mul(S, big5)) + } + if ilim < 0 || i < 0 || i == 0 && !biasUp { + /* Always emit at least one digit. If the number appears to be zero + using the current mode, then emit one '0' digit and set decpt to 1. */ + if sign { + buf = buf[:1] + } else { + buf = buf[:0] + } + buf = append(buf, '0') + return buf, 1 + } + buf = append(buf, '1') + k++ + return buf, int(k + 1) + } + + var dig byte + if leftright { + if m2 > 0 { + mhi.Lsh(mhi, uint(m2)) + } + + /* Compute mlo -- check for special case + * that d is a normalized power of 2. + */ + + mlo = mhi + if spec_case { + mhi = mlo + mhi = new(big.Int).Lsh(mhi, log2P) + } + /* mlo/S = maximum acceptable error, divided by 10^k, if the output is less than d. */ + /* mhi/S = maximum acceptable error, divided by 10^k, if the output is greater than d. */ + + for i = 1; ; i++ { + z := new(big.Int) + z.DivMod(b, S, b) + dig = byte(z.Int64() + '0') + /* Do we yet have the shortest decimal string + * that will round to d? + */ + j = b.Cmp(mlo) + /* j is b/S compared with mlo/S. */ + delta := new(big.Int).Sub(S, mhi) + var j1 int + if delta.Sign() <= 0 { + j1 = 1 + } else { + j1 = b.Cmp(delta) + } + /* j1 is b/S compared with 1 - mhi/S. */ + if (j1 == 0) && (mode == 0) && ((_word1(d) & 1) == 0) { + if dig == '9' { + var flag bool + buf = append(buf, '9') + if buf, flag = roundOff(buf); flag { + k++ + buf = append(buf, '1') + } + return buf, int(k + 1) + } + if j > 0 { + dig++ + } + buf = append(buf, dig) + return buf, int(k + 1) + } + if (j < 0) || ((j == 0) && (mode == 0) && ((_word1(d) & 1) == 0)) { + if j1 > 0 { + /* Either dig or dig+1 would work here as the least significant decimal digit. + Use whichever would produce a decimal value closer to d. */ + b.Lsh(b, 1) + j1 = b.Cmp(S) + if (j1 > 0) || (j1 == 0 && (((dig & 1) == 1) || biasUp)) { + dig++ + if dig == '9' { + buf = append(buf, '9') + buf, flag := roundOff(buf) + if flag { + k++ + buf = append(buf, '1') + } + return buf, int(k + 1) + } + } + } + buf = append(buf, dig) + return buf, int(k + 1) + } + if j1 > 0 { + if dig == '9' { /* possible if i == 1 */ + // round_9_up: + // *s++ = '9'; + // goto roundoff; + buf = append(buf, '9') + buf, flag := roundOff(buf) + if flag { + k++ + buf = append(buf, '1') + } + return buf, int(k + 1) + } + buf = append(buf, dig+1) + return buf, int(k + 1) + } + buf = append(buf, dig) + if i == ilim { + break + } + b.Mul(b, big10) + if mlo == mhi { + mhi.Mul(mhi, big10) + } else { + mlo.Mul(mlo, big10) + mhi.Mul(mhi, big10) + } + } + } else { + for i = 1; ; i++ { + // (char)(dig = quorem(b,S) + '0'); + z := new(big.Int) + z.DivMod(b, S, b) + dig = byte(z.Int64() + '0') + buf = append(buf, dig) + if i >= ilim { + break + } + + b.Mul(b, big10) + } + } + /* Round off last digit */ + + b.Lsh(b, 1) + j = b.Cmp(S) + if (j > 0) || (j == 0 && (((dig & 1) == 1) || biasUp)) { + var flag bool + buf, flag = roundOff(buf) + if flag { + k++ + buf = append(buf, '1') + return buf, int(k + 1) + } + } else { + buf = stripTrailingZeroes(buf) + } + + return buf, int(k + 1) +} + +func insert(b []byte, p int, c byte) []byte { + b = append(b, 0) + copy(b[p+1:], b[p:]) + b[p] = c + return b +} + +func FToStr(d float64, mode FToStrMode, precision int, buffer []byte) []byte { + if mode == ModeFixed && (d >= 1e21 || d <= -1e21) { + mode = ModeStandard + } + + buffer, decPt := ftoa(d, dtoaModes[mode], mode >= ModeFixed, precision, buffer) + if decPt != 9999 { + exponentialNotation := false + minNDigits := 0 /* Minimum number of significand digits required by mode and precision */ + sign := false + nDigits := len(buffer) + if len(buffer) > 0 && buffer[0] == '-' { + sign = true + nDigits-- + } + + switch mode { + case ModeStandard: + if decPt < -5 || decPt > 21 { + exponentialNotation = true + } else { + minNDigits = decPt + } + case ModeFixed: + if precision >= 0 { + minNDigits = decPt + precision + } else { + minNDigits = decPt + } + case ModeExponential: + // JS_ASSERT(precision > 0); + minNDigits = precision + fallthrough + case ModeStandardExponential: + exponentialNotation = true + case ModePrecision: + // JS_ASSERT(precision > 0); + minNDigits = precision + if decPt < -5 || decPt > precision { + exponentialNotation = true + } + } + + for nDigits < minNDigits { + buffer = append(buffer, '0') + nDigits++ + } + + if exponentialNotation { + /* Insert a decimal point if more than one significand digit */ + if nDigits != 1 { + p := 1 + if sign { + p = 2 + } + buffer = insert(buffer, p, '.') + } + buffer = append(buffer, 'e') + if decPt-1 >= 0 { + buffer = append(buffer, '+') + } + buffer = strconv.AppendInt(buffer, int64(decPt-1), 10) + } else if decPt != nDigits { + /* Some kind of a fraction in fixed notation */ + // JS_ASSERT(decPt <= nDigits); + if decPt > 0 { + /* dd...dd . dd...dd */ + if sign { + buffer = insert(buffer, decPt+1, '.') + } else { + buffer = insert(buffer, decPt, '.') + } + } else { + /* 0 . 00...00dd...dd */ + o := 0 + if sign { + o = 1 + } + for i := 0; i < 1-decPt; i++ { + buffer = insert(buffer, o, '0') + } + buffer = insert(buffer, o+1, '.') + } + } + } + return buffer +} + +func bumpUp(buf []byte, k int) ([]byte, int) { + var lastCh byte + stop := 0 + if len(buf) > 0 && buf[0] == '-' { + stop = 1 + } + for { + lastCh = buf[len(buf)-1] + buf = buf[:len(buf)-1] + if lastCh != '9' { + break + } + if len(buf) == stop { + k++ + lastCh = '0' + break + } + } + buf = append(buf, lastCh+1) + return buf, k +} + +func setWord0(d float64, w uint32) float64 { + dBits := math.Float64bits(d) + return math.Float64frombits(uint64(w)<<32 | dBits&0xffffffff) +} + +func _word0(d float64) uint32 { + dBits := math.Float64bits(d) + return uint32(dBits >> 32) +} + +func _word1(d float64) uint32 { + dBits := math.Float64bits(d) + return uint32(dBits) +} + +func stripTrailingZeroes(buf []byte) []byte { + bl := len(buf) - 1 + for bl >= 0 && buf[bl] == '0' { + bl-- + } + return buf[:bl+1] +} + +/* Set b = b * 5^k. k must be nonnegative. */ +// XXXX the C version built a cache of these +func pow5mult(b *big.Int, k int) *big.Int { + return b.Mul(b, new(big.Int).Exp(big5, big.NewInt(int64(k)), nil)) +} + +func roundOff(buf []byte) ([]byte, bool) { + i := len(buf) + stop := 0 + if i > 0 && buf[0] == '-' { + stop = 1 + } + for i != stop { + i-- + if buf[i] != '9' { + buf[i]++ + return buf[:i+1], false + } + } + return buf[:stop], true +} diff --git a/ftoa/ftostr_test.go b/ftoa/ftostr_test.go new file mode 100644 index 00000000..c3b343c7 --- /dev/null +++ b/ftoa/ftostr_test.go @@ -0,0 +1,37 @@ +package ftoa + +import ( + "math" + "testing" +) + +func _testFToStr(num float64, mode FToStrMode, precision int, expected string, t *testing.T) { + buf := FToStr(num, mode, precision, nil) + if s := string(buf); s != expected { + t.Fatalf("expected: '%s', actual: '%s", expected, s) + } + if !math.IsNaN(num) && num != 0 && !math.Signbit(num) { + _testFToStr(-num, mode, precision, "-"+expected, t) + } +} + +func testFToStr(num float64, mode FToStrMode, precision int, expected string, t *testing.T) { + t.Run("", func(t *testing.T) { + t.Parallel() + _testFToStr(num, mode, precision, expected, t) + }) +} + +func TestDtostr(t *testing.T) { + testFToStr(0, ModeStandard, 0, "0", t) + testFToStr(1, ModeStandard, 0, "1", t) + testFToStr(9007199254740991, ModeStandard, 0, "9007199254740991", t) + testFToStr(math.MaxInt64, ModeStandardExponential, 0, "9.223372036854776e+18", t) + testFToStr(1e-5, ModeFixed, 1, "0.0", t) + testFToStr(8.85, ModeExponential, 2, "8.8e+0", t) + testFToStr(885, ModeExponential, 2, "8.9e+2", t) + testFToStr(25, ModeExponential, 1, "3e+1", t) + testFToStr(math.Inf(1), ModeStandard, 0, "Infinity", t) + testFToStr(math.NaN(), ModeStandard, 0, "NaN", t) + testFToStr(math.SmallestNonzeroFloat64, ModeExponential, 40, "4.940656458412465441765687928682213723651e-324", t) +} diff --git a/runtime.go b/runtime.go index 0788fb9f..9fa55f43 100644 --- a/runtime.go +++ b/runtime.go @@ -341,6 +341,7 @@ func (r *Runtime) init() { r.initFunction() r.initArray() r.initString() + r.initGlobalObject() r.initNumber() r.initRegExp() r.initDate() @@ -353,8 +354,6 @@ func (r *Runtime) init() { r.global.Eval = r.newNativeFunc(r.builtin_eval, nil, "eval", nil, 1) r.addToGlobal("eval", r.global.Eval) - r.initGlobalObject() - r.initMath() r.initJSON() @@ -1818,6 +1817,18 @@ func (r *Runtime) toObject(v Value, args ...interface{}) *Object { } } +func (r *Runtime) toNumber(v Value) Value { + switch o := v.(type) { + case valueInt, valueFloat: + return v + case *Object: + if pvo, ok := o.self.(*primitiveValueObject); ok { + return r.toNumber(pvo.pValue) + } + } + panic(r.NewTypeError("Value is not a number: %s", v)) +} + func (r *Runtime) speciesConstructor(o, defaultConstructor *Object) func(args []Value, newTarget *Object) *Object { c := o.self.getStr("constructor", nil) if c != nil && c != _undefined { diff --git a/runtime_test.go b/runtime_test.go index aef72246..7fa9ca99 100644 --- a/runtime_test.go +++ b/runtime_test.go @@ -137,6 +137,29 @@ func TestFractionalNumberToStringRadix(t *testing.T) { testScript1(SCRIPT, asciiString("3f.gez4w97ry"), t) } +func TestNumberFormatRounding(t *testing.T) { + const SCRIPT = ` + assert.sameValue((123.456).toExponential(undefined), "1.23456e+2", "undefined"); + assert.sameValue((0.000001).toPrecision(2), "0.0000010") + assert.sameValue((-7).toPrecision(1), "-7"); + assert.sameValue((-42).toPrecision(1), "-4e+1"); + assert.sameValue((0.000001).toPrecision(1), "0.000001"); + assert.sameValue((123.456).toPrecision(1), "1e+2", "1"); + assert.sameValue((123.456).toPrecision(2), "1.2e+2", "2"); + + var n = new Number("0.000000000000000000001"); // 1e-21 + assert.sameValue((n).toPrecision(1), "1e-21"); + assert.sameValue((25).toExponential(0), "3e+1"); + assert.sameValue((-25).toExponential(0), "-3e+1"); + assert.sameValue((12345).toExponential(3), "1.235e+4"); + assert.sameValue((25.5).toFixed(0), "26"); + assert.sameValue((-25.5).toFixed(0), "-26"); + assert.sameValue((99.9).toFixed(0), "100"); + assert.sameValue((99.99).toFixed(1), "100.0"); + ` + testScript1(TESTLIB+SCRIPT, _undefined, t) +} + func TestSetFunc(t *testing.T) { const SCRIPT = ` sum(40, 2); diff --git a/string_ascii.go b/string_ascii.go index 7487e6e8..179a4714 100644 --- a/string_ascii.go +++ b/string_ascii.go @@ -47,14 +47,11 @@ func strToInt(ss string) (int64, error) { if len(ss) > 2 { switch ss[:2] { case "0x", "0X": - i, _ := strconv.ParseInt(ss[2:], 16, 64) - return i, nil + return strconv.ParseInt(ss[2:], 16, 64) case "0b", "0B": - i, _ := strconv.ParseInt(ss[2:], 2, 64) - return i, nil + return strconv.ParseInt(ss[2:], 2, 64) case "0o", "0O": - i, _ := strconv.ParseInt(ss[2:], 8, 64) - return i, nil + return strconv.ParseInt(ss[2:], 8, 64) } } return strconv.ParseInt(ss, 10, 64) diff --git a/tc39_test.go b/tc39_test.go index 57432021..7585a774 100644 --- a/tc39_test.go +++ b/tc39_test.go @@ -63,6 +63,8 @@ var ( "test/language/statements/class/subclass/builtin-objects/String/length.js": true, "test/language/statements/class/subclass/builtin-objects/Date/super-must-be-called.js": true, "test/language/statements/class/subclass/builtin-objects/Date/regular-subclassing.js": true, + "test/language/statements/class/subclass/builtin-objects/Number/super-must-be-called.js": true, + "test/language/statements/class/subclass/builtin-objects/Number/regular-subclassing.js": true, // full unicode regexp flag "test/built-ins/RegExp/prototype/Symbol.match/u-advance-after-empty.js": true, @@ -108,6 +110,7 @@ var ( "12.9.4", "19.1", "19.4", + "20.1", "20.3", "21.1", "21.2.5.6", @@ -134,6 +137,7 @@ var ( "sec-%typedarray%", "sec-string.prototype", "sec-date", + "sec-number", } ) diff --git a/value.go b/value.go index 1aaf0c62..e82628b0 100644 --- a/value.go +++ b/value.go @@ -4,10 +4,10 @@ import ( "hash/maphash" "math" "reflect" - "regexp" "strconv" "unsafe" + "github.com/dop251/goja/ftoa" "github.com/dop251/goja/unistring" ) @@ -138,6 +138,11 @@ func propSetter(o Value, v Value, r *Runtime) *Object { return nil } +func fToStr(num float64, mode ftoa.FToStrMode, prec int) string { + var buf1 [128]byte + return string(ftoa.FToStr(num, mode, prec, buf1[:0])) +} + func (i valueInt) ToInteger() int64 { return int64(i) } @@ -555,25 +560,8 @@ func (f valueFloat) ToPrimitiveString() Value { return f } -var matchLeading0Exponent = regexp.MustCompile(`([eE][+\-])0+([1-9])`) // 1e-07 => 1e-7 - func (f valueFloat) String() string { - value := float64(f) - if math.IsNaN(value) { - return "NaN" - } else if math.IsInf(value, 0) { - if math.Signbit(value) { - return "-Infinity" - } - return "Infinity" - } else if f == _negativeZero { - return "0" - } - exponent := math.Log10(math.Abs(value)) - if exponent >= 21 || exponent < -6 { - return matchLeading0Exponent.ReplaceAllString(strconv.FormatFloat(value, 'g', -1, 64), "$1$2") - } - return strconv.FormatFloat(value, 'f', -1, 64) + return fToStr(float64(f), ftoa.ModeStandard, 0) } func (f valueFloat) ToFloat() float64 { From 8acb9eda7d074e81106a02f7537e2211e32e3141 Mon Sep 17 00:00:00 2001 From: Dmitry Panov Date: Fri, 24 Apr 2020 23:03:52 +0100 Subject: [PATCH 27/46] Performance optimisations --- ftoa/common.go | 4 +-- ftoa/ftobasestr.go | 3 +- ftoa/ftostr.go | 83 +++++++++++++++++++++++++++++++++------------ ftoa/ftostr_test.go | 34 +++++++++++++++++++ 4 files changed, 99 insertions(+), 25 deletions(-) diff --git a/ftoa/common.go b/ftoa/common.go index 5906464f..ea632df4 100644 --- a/ftoa/common.go +++ b/ftoa/common.go @@ -97,7 +97,7 @@ func stuffBits(bits []byte, offset int, val uint32) { bits[offset+3] = byte(val) } -func d2b(d float64) (b *big.Int, e, bits int) { +func d2b(d float64, bi *big.Int) (e, bits int) { dBits := math.Float64bits(d) d0 := uint32(dBits >> 32) d1 := uint32(dBits) @@ -144,6 +144,6 @@ func d2b(d float64) (b *big.Int, e, bits int) { e = de - bias - (p - 1) + 1 + k bits = 32*i - hi0bits(z) } - b = (&big.Int{}).SetBytes(dbl_bits) + bi.SetBytes(dbl_bits) return } diff --git a/ftoa/ftobasestr.go b/ftoa/ftobasestr.go index a79050fe..424a5788 100644 --- a/ftoa/ftobasestr.go +++ b/ftoa/ftobasestr.go @@ -64,7 +64,8 @@ func FToBaseStr(num float64, radix int) string { word0 := uint32(dBits >> 32) word1 := uint32(dBits) - b, e, _ := d2b(df) + b := new(big.Int) + e, _ := d2b(df, b) // JS_ASSERT(e < 0); /* At this point df = b * 2^e. e must be less than zero because 0 < df < 1. */ diff --git a/ftoa/ftostr.go b/ftoa/ftostr.go index f1c296bf..e0d7a097 100644 --- a/ftoa/ftostr.go +++ b/ftoa/ftostr.go @@ -36,6 +36,9 @@ var ( big5 = big.NewInt(5) big10 = big.NewInt(10) + p05 = []*big.Int{big5, big.NewInt(25), big.NewInt(125)} + pow5Cache [7]*big.Int + dtoaModes = []int{ ModeStandard: 0, ModeStandardExponential: 0, @@ -100,7 +103,8 @@ func ftoa(d float64, mode int, biasUp bool, ndigits int, buf []byte) ([]byte, in if sign { buf = append(buf, '-') } - b, be, bbits := d2b(d) + b := new(big.Int) + be, bbits := d2b(d, b) i := int((word0 >> exp_shift1) & (exp_mask >> exp_shift1)) var d2 float64 var denorm bool @@ -534,9 +538,8 @@ func ftoa(d float64, mode int, biasUp bool, ndigits int, buf []byte) ([]byte, in } /* mlo/S = maximum acceptable error, divided by 10^k, if the output is less than d. */ /* mhi/S = maximum acceptable error, divided by 10^k, if the output is greater than d. */ - + var z, delta big.Int for i = 1; ; i++ { - z := new(big.Int) z.DivMod(b, S, b) dig = byte(z.Int64() + '0') /* Do we yet have the shortest decimal string @@ -544,12 +547,12 @@ func ftoa(d float64, mode int, biasUp bool, ndigits int, buf []byte) ([]byte, in */ j = b.Cmp(mlo) /* j is b/S compared with mlo/S. */ - delta := new(big.Int).Sub(S, mhi) + delta.Sub(S, mhi) var j1 int if delta.Sign() <= 0 { j1 = 1 } else { - j1 = b.Cmp(delta) + j1 = b.Cmp(&delta) } /* j1 is b/S compared with 1 - mhi/S. */ if (j1 == 0) && (mode == 0) && ((_word1(d) & 1) == 0) { @@ -560,13 +563,13 @@ func ftoa(d float64, mode int, biasUp bool, ndigits int, buf []byte) ([]byte, in k++ buf = append(buf, '1') } - return buf, int(k + 1) + return buf, k + 1 } if j > 0 { dig++ } buf = append(buf, dig) - return buf, int(k + 1) + return buf, k + 1 } if (j < 0) || ((j == 0) && (mode == 0) && ((_word1(d) & 1) == 0)) { if j1 > 0 { @@ -583,28 +586,25 @@ func ftoa(d float64, mode int, biasUp bool, ndigits int, buf []byte) ([]byte, in k++ buf = append(buf, '1') } - return buf, int(k + 1) + return buf, k + 1 } } } buf = append(buf, dig) - return buf, int(k + 1) + return buf, k + 1 } if j1 > 0 { if dig == '9' { /* possible if i == 1 */ - // round_9_up: - // *s++ = '9'; - // goto roundoff; buf = append(buf, '9') buf, flag := roundOff(buf) if flag { k++ buf = append(buf, '1') } - return buf, int(k + 1) + return buf, k + 1 } buf = append(buf, dig+1) - return buf, int(k + 1) + return buf, k + 1 } buf = append(buf, dig) if i == ilim { @@ -619,9 +619,8 @@ func ftoa(d float64, mode int, biasUp bool, ndigits int, buf []byte) ([]byte, in } } } else { + var z big.Int for i = 1; ; i++ { - // (char)(dig = quorem(b,S) + '0'); - z := new(big.Int) z.DivMod(b, S, b) dig = byte(z.Int64() + '0') buf = append(buf, dig) @@ -642,13 +641,13 @@ func ftoa(d float64, mode int, biasUp bool, ndigits int, buf []byte) ([]byte, in if flag { k++ buf = append(buf, '1') - return buf, int(k + 1) + return buf, k + 1 } } else { buf = stripTrailingZeroes(buf) } - return buf, int(k + 1) + return buf, k + 1 } func insert(b []byte, p int, c byte) []byte { @@ -658,6 +657,16 @@ func insert(b []byte, p int, c byte) []byte { return b } +func expand(b []byte, delta int) []byte { + newLen := len(b) + delta + if newLen <= cap(b) { + return b[:newLen] + } + b1 := make([]byte, newLen) + copy(b1, b) + return b1 +} + func FToStr(d float64, mode FToStrMode, precision int, buffer []byte) []byte { if mode == ModeFixed && (d >= 1e21 || d <= -1e21) { mode = ModeStandard @@ -736,10 +745,13 @@ func FToStr(d float64, mode FToStrMode, precision int, buffer []byte) []byte { if sign { o = 1 } - for i := 0; i < 1-decPt; i++ { - buffer = insert(buffer, o, '0') + buffer = expand(buffer, 2-decPt) + copy(buffer[o+2-decPt:], buffer[o:]) + buffer[o] = '0' + buffer[o+1] = '.' + for i := o + 2; i < o+2-decPt; i++ { + buffer[i] = '0' } - buffer = insert(buffer, o+1, '.') } } } @@ -792,8 +804,26 @@ func stripTrailingZeroes(buf []byte) []byte { } /* Set b = b * 5^k. k must be nonnegative. */ -// XXXX the C version built a cache of these func pow5mult(b *big.Int, k int) *big.Int { + if k < (1 << (len(pow5Cache) + 2)) { + i := k & 3 + if i != 0 { + b.Mul(b, p05[i-1]) + } + k >>= 2 + i = 0 + for { + if k&1 != 0 { + b.Mul(b, pow5Cache[i]) + } + k >>= 1 + if k == 0 { + break + } + i++ + } + return b + } return b.Mul(b, new(big.Int).Exp(big5, big.NewInt(int64(k)), nil)) } @@ -812,3 +842,12 @@ func roundOff(buf []byte) ([]byte, bool) { } return buf[:stop], true } + +func init() { + p := big.NewInt(625) + pow5Cache[0] = p + for i := 1; i < len(pow5Cache); i++ { + p = new(big.Int).Mul(p, p) + pow5Cache[i] = p + } +} diff --git a/ftoa/ftostr_test.go b/ftoa/ftostr_test.go index c3b343c7..e645b598 100644 --- a/ftoa/ftostr_test.go +++ b/ftoa/ftostr_test.go @@ -2,6 +2,7 @@ package ftoa import ( "math" + "strconv" "testing" ) @@ -31,7 +32,40 @@ func TestDtostr(t *testing.T) { testFToStr(8.85, ModeExponential, 2, "8.8e+0", t) testFToStr(885, ModeExponential, 2, "8.9e+2", t) testFToStr(25, ModeExponential, 1, "3e+1", t) + testFToStr(1e-6, ModeFixed, 7, "0.0000010", t) testFToStr(math.Inf(1), ModeStandard, 0, "Infinity", t) testFToStr(math.NaN(), ModeStandard, 0, "NaN", t) testFToStr(math.SmallestNonzeroFloat64, ModeExponential, 40, "4.940656458412465441765687928682213723651e-324", t) } + +func BenchmarkDtostrSmall(b *testing.B) { + var buf [128]byte + b.ReportAllocs() + for i := 0; i < b.N; i++ { + FToStr(math.Pi, ModeExponential, 0, buf[:0]) + } +} + +func BenchmarkDtostrBig(b *testing.B) { + var buf [128]byte + b.ReportAllocs() + for i := 0; i < b.N; i++ { + FToStr(math.SmallestNonzeroFloat64, ModeExponential, 40, buf[:0]) + } +} + +func BenchmarkAppendFloatBig(b *testing.B) { + var buf [128]byte + b.ReportAllocs() + for i := 0; i < b.N; i++ { + strconv.AppendFloat(buf[:0], math.SmallestNonzeroFloat64, 'e', 40, 64) + } +} + +func BenchmarkAppendFloatSmall(b *testing.B) { + var buf [128]byte + b.ReportAllocs() + for i := 0; i < b.N; i++ { + strconv.AppendFloat(buf[:0], math.Pi, 'e', -1, 64) + } +} From 1f899b16c63b199016cd1168e76c634ade2d085b Mon Sep 17 00:00:00 2001 From: Dmitry Panov Date: Sat, 25 Apr 2020 15:21:34 +0100 Subject: [PATCH 28/46] Added ES6 Math properties and methods --- builtin_math.go | 140 +++++++++++++++++++++++++++++++++++++++++++++++- object.go | 1 + tc39_test.go | 2 + 3 files changed, 141 insertions(+), 2 deletions(-) diff --git a/builtin_math.go b/builtin_math.go index c275e94f..90866c44 100644 --- a/builtin_math.go +++ b/builtin_math.go @@ -2,6 +2,7 @@ package goja import ( "math" + "math/bits" ) func (r *Runtime) math_abs(call FunctionCall) Value { @@ -12,14 +13,26 @@ func (r *Runtime) math_acos(call FunctionCall) Value { return floatToValue(math.Acos(call.Argument(0).ToFloat())) } +func (r *Runtime) math_acosh(call FunctionCall) Value { + return floatToValue(math.Acosh(call.Argument(0).ToFloat())) +} + func (r *Runtime) math_asin(call FunctionCall) Value { return floatToValue(math.Asin(call.Argument(0).ToFloat())) } +func (r *Runtime) math_asinh(call FunctionCall) Value { + return floatToValue(math.Asinh(call.Argument(0).ToFloat())) +} + func (r *Runtime) math_atan(call FunctionCall) Value { return floatToValue(math.Atan(call.Argument(0).ToFloat())) } +func (r *Runtime) math_atanh(call FunctionCall) Value { + return floatToValue(math.Atanh(call.Argument(0).ToFloat())) +} + func (r *Runtime) math_atan2(call FunctionCall) Value { y := call.Argument(0).ToFloat() x := call.Argument(1).ToFloat() @@ -27,26 +40,103 @@ func (r *Runtime) math_atan2(call FunctionCall) Value { return floatToValue(math.Atan2(y, x)) } +func (r *Runtime) math_cbrt(call FunctionCall) Value { + return floatToValue(math.Cbrt(call.Argument(0).ToFloat())) +} + func (r *Runtime) math_ceil(call FunctionCall) Value { return floatToValue(math.Ceil(call.Argument(0).ToFloat())) } +func (r *Runtime) math_clz32(call FunctionCall) Value { + return intToValue(int64(bits.LeadingZeros32(toUint32(call.Argument(0))))) +} + func (r *Runtime) math_cos(call FunctionCall) Value { return floatToValue(math.Cos(call.Argument(0).ToFloat())) } +func (r *Runtime) math_cosh(call FunctionCall) Value { + return floatToValue(math.Cosh(call.Argument(0).ToFloat())) +} + func (r *Runtime) math_exp(call FunctionCall) Value { return floatToValue(math.Exp(call.Argument(0).ToFloat())) } +func (r *Runtime) math_expm1(call FunctionCall) Value { + return floatToValue(math.Expm1(call.Argument(0).ToFloat())) +} + func (r *Runtime) math_floor(call FunctionCall) Value { return floatToValue(math.Floor(call.Argument(0).ToFloat())) } +func (r *Runtime) math_fround(call FunctionCall) Value { + return floatToValue(float64(float32(call.Argument(0).ToFloat()))) +} + +func (r *Runtime) math_hypot(call FunctionCall) Value { + var max float64 + var hasNaN bool + absValues := make([]float64, 0, len(call.Arguments)) + for _, v := range call.Arguments { + arg := nilSafe(v).ToFloat() + if math.IsNaN(arg) { + hasNaN = true + } else { + abs := math.Abs(arg) + if abs > max { + max = abs + } + absValues = append(absValues, abs) + } + } + if math.IsInf(max, 1) { + return _positiveInf + } + if hasNaN { + return _NaN + } + if max == 0 { + return _positiveZero + } + + // Kahan summation to avoid rounding errors. + // Normalize the numbers to the largest one to avoid overflow. + var sum, compensation float64 + for _, n := range absValues { + n /= max + summand := n*n - compensation + preliminary := sum + summand + compensation = (preliminary - sum) - summand + sum = preliminary + } + return floatToValue(math.Sqrt(sum) * max) +} + +func (r *Runtime) math_imul(call FunctionCall) Value { + x := toUint32(call.Argument(0)) + y := toUint32(call.Argument(1)) + return intToValue(int64(int32(x * y))) +} + func (r *Runtime) math_log(call FunctionCall) Value { return floatToValue(math.Log(call.Argument(0).ToFloat())) } +func (r *Runtime) math_log1p(call FunctionCall) Value { + return floatToValue(math.Log1p(call.Argument(0).ToFloat())) +} + +func (r *Runtime) math_log10(call FunctionCall) Value { + return floatToValue(math.Log10(call.Argument(0).ToFloat())) +} + +func (r *Runtime) math_log2(call FunctionCall) Value { + return floatToValue(math.Log2(call.Argument(0).ToFloat())) +} + func (r *Runtime) math_max(call FunctionCall) Value { if len(call.Arguments) == 0 { return _negativeInf @@ -135,10 +225,26 @@ func (r *Runtime) math_round(call FunctionCall) Value { return floatToValue(t) } +func (r *Runtime) math_sign(call FunctionCall) Value { + arg := call.Argument(0) + num := arg.ToFloat() + if math.IsNaN(num) || num == 0 { // this will match -0 too + return arg + } + if num > 0 { + return intToValue(1) + } + return intToValue(-1) +} + func (r *Runtime) math_sin(call FunctionCall) Value { return floatToValue(math.Sin(call.Argument(0).ToFloat())) } +func (r *Runtime) math_sinh(call FunctionCall) Value { + return floatToValue(math.Sinh(call.Argument(0).ToFloat())) +} + func (r *Runtime) math_sqrt(call FunctionCall) Value { return floatToValue(math.Sqrt(call.Argument(0).ToFloat())) } @@ -147,9 +253,21 @@ func (r *Runtime) math_tan(call FunctionCall) Value { return floatToValue(math.Tan(call.Argument(0).ToFloat())) } +func (r *Runtime) math_tanh(call FunctionCall) Value { + return floatToValue(math.Tanh(call.Argument(0).ToFloat())) +} + +func (r *Runtime) math_trunc(call FunctionCall) Value { + arg := call.Argument(0) + if i, ok := arg.(valueInt); ok { + return i + } + return floatToValue(math.Trunc(arg.ToFloat())) +} + func (r *Runtime) createMath(val *Object) objectImpl { m := &baseObject{ - class: "Math", + class: classMath, val: val, extensible: true, prototype: r.global.ObjectPrototype, @@ -159,30 +277,48 @@ func (r *Runtime) createMath(val *Object) objectImpl { m._putProp("E", valueFloat(math.E), false, false, false) m._putProp("LN10", valueFloat(math.Ln10), false, false, false) m._putProp("LN2", valueFloat(math.Ln2), false, false, false) - m._putProp("LOG2E", valueFloat(math.Log2E), false, false, false) m._putProp("LOG10E", valueFloat(math.Log10E), false, false, false) + m._putProp("LOG2E", valueFloat(math.Log2E), false, false, false) m._putProp("PI", valueFloat(math.Pi), false, false, false) m._putProp("SQRT1_2", valueFloat(sqrt1_2), false, false, false) m._putProp("SQRT2", valueFloat(math.Sqrt2), false, false, false) + m._putSym(symToStringTag, valueProp(asciiString(classMath), false, false, true)) m._putProp("abs", r.newNativeFunc(r.math_abs, nil, "abs", nil, 1), true, false, true) m._putProp("acos", r.newNativeFunc(r.math_acos, nil, "acos", nil, 1), true, false, true) + m._putProp("acosh", r.newNativeFunc(r.math_acosh, nil, "acosh", nil, 1), true, false, true) m._putProp("asin", r.newNativeFunc(r.math_asin, nil, "asin", nil, 1), true, false, true) + m._putProp("asinh", r.newNativeFunc(r.math_asinh, nil, "asinh", nil, 1), true, false, true) m._putProp("atan", r.newNativeFunc(r.math_atan, nil, "atan", nil, 1), true, false, true) + m._putProp("atanh", r.newNativeFunc(r.math_atanh, nil, "atanh", nil, 1), true, false, true) m._putProp("atan2", r.newNativeFunc(r.math_atan2, nil, "atan2", nil, 2), true, false, true) + m._putProp("cbrt", r.newNativeFunc(r.math_cbrt, nil, "cbrt", nil, 1), true, false, true) m._putProp("ceil", r.newNativeFunc(r.math_ceil, nil, "ceil", nil, 1), true, false, true) + m._putProp("clz32", r.newNativeFunc(r.math_clz32, nil, "clz32", nil, 1), true, false, true) m._putProp("cos", r.newNativeFunc(r.math_cos, nil, "cos", nil, 1), true, false, true) + m._putProp("cosh", r.newNativeFunc(r.math_cosh, nil, "cosh", nil, 1), true, false, true) m._putProp("exp", r.newNativeFunc(r.math_exp, nil, "exp", nil, 1), true, false, true) + m._putProp("expm1", r.newNativeFunc(r.math_expm1, nil, "expm1", nil, 1), true, false, true) m._putProp("floor", r.newNativeFunc(r.math_floor, nil, "floor", nil, 1), true, false, true) + m._putProp("fround", r.newNativeFunc(r.math_fround, nil, "fround", nil, 1), true, false, true) + m._putProp("hypot", r.newNativeFunc(r.math_hypot, nil, "hypot", nil, 2), true, false, true) + m._putProp("imul", r.newNativeFunc(r.math_imul, nil, "imul", nil, 2), true, false, true) m._putProp("log", r.newNativeFunc(r.math_log, nil, "log", nil, 1), true, false, true) + m._putProp("log1p", r.newNativeFunc(r.math_log1p, nil, "log1p", nil, 1), true, false, true) + m._putProp("log10", r.newNativeFunc(r.math_log10, nil, "log10", nil, 1), true, false, true) + m._putProp("log2", r.newNativeFunc(r.math_log2, nil, "log2", nil, 1), true, false, true) m._putProp("max", r.newNativeFunc(r.math_max, nil, "max", nil, 2), true, false, true) m._putProp("min", r.newNativeFunc(r.math_min, nil, "min", nil, 2), true, false, true) m._putProp("pow", r.newNativeFunc(r.math_pow, nil, "pow", nil, 2), true, false, true) m._putProp("random", r.newNativeFunc(r.math_random, nil, "random", nil, 0), true, false, true) m._putProp("round", r.newNativeFunc(r.math_round, nil, "round", nil, 1), true, false, true) + m._putProp("sign", r.newNativeFunc(r.math_sign, nil, "sign", nil, 1), true, false, true) m._putProp("sin", r.newNativeFunc(r.math_sin, nil, "sin", nil, 1), true, false, true) + m._putProp("sinh", r.newNativeFunc(r.math_sinh, nil, "sinh", nil, 1), true, false, true) m._putProp("sqrt", r.newNativeFunc(r.math_sqrt, nil, "sqrt", nil, 1), true, false, true) m._putProp("tan", r.newNativeFunc(r.math_tan, nil, "tan", nil, 1), true, false, true) + m._putProp("tanh", r.newNativeFunc(r.math_tanh, nil, "tanh", nil, 1), true, false, true) + m._putProp("trunc", r.newNativeFunc(r.math_trunc, nil, "trunc", nil, 1), true, false, true) return m } diff --git a/object.go b/object.go index e371d8e6..45fe1f08 100644 --- a/object.go +++ b/object.go @@ -16,6 +16,7 @@ const ( classWeakSet = "WeakSet" classWeakMap = "WeakMap" classMap = "Map" + classMath = "Math" classSet = "Set" classFunction = "Function" classNumber = "Number" diff --git a/tc39_test.go b/tc39_test.go index 7585a774..017c34e1 100644 --- a/tc39_test.go +++ b/tc39_test.go @@ -111,6 +111,7 @@ var ( "19.1", "19.4", "20.1", + "20.2", "20.3", "21.1", "21.2.5.6", @@ -138,6 +139,7 @@ var ( "sec-string.prototype", "sec-date", "sec-number", + "sec-math", } ) From 99a706fa11c2009e2ed7db4df74b273bc1cdcd63 Mon Sep 17 00:00:00 2001 From: Dmitry Panov Date: Sun, 26 Apr 2020 13:25:22 +0100 Subject: [PATCH 29/46] Standard compliance fixes for Array, Error and Function --- builtin_array.go | 19 ++++----- builtin_error.go | 33 +++++++--------- builtin_function.go | 29 +++++++++++--- runtime.go | 5 ++- string.go | 1 + tc39_test.go | 94 ++++++++++++++++++++++++++++++--------------- 6 files changed, 115 insertions(+), 66 deletions(-) diff --git a/builtin_array.go b/builtin_array.go index 0fadea6a..b8d4b729 100644 --- a/builtin_array.go +++ b/builtin_array.go @@ -1226,15 +1226,16 @@ func (r *Runtime) createArrayProto(val *Object) objectImpl { o._putSym(symIterator, valueProp(valuesFunc, true, false, true)) - bl := r.NewObject() - bl.self.setOwnStr("copyWithin", valueTrue, true) - bl.self.setOwnStr("entries", valueTrue, true) - bl.self.setOwnStr("fill", valueTrue, true) - bl.self.setOwnStr("find", valueTrue, true) - bl.self.setOwnStr("findIndex", valueTrue, true) - bl.self.setOwnStr("keys", valueTrue, true) - bl.self.setOwnStr("values", valueTrue, true) - o._putSym(symUnscopables, valueProp(bl, false, false, true)) + bl := r.newBaseObject(nil, classObject) + bl.setOwnStr("copyWithin", valueTrue, true) + bl.setOwnStr("entries", valueTrue, true) + bl.setOwnStr("fill", valueTrue, true) + bl.setOwnStr("find", valueTrue, true) + bl.setOwnStr("findIndex", valueTrue, true) + bl.setOwnStr("includes", valueTrue, true) + bl.setOwnStr("keys", valueTrue, true) + bl.setOwnStr("values", valueTrue, true) + o._putSym(symUnscopables, valueProp(bl.val, false, false, true)) return o } diff --git a/builtin_error.go b/builtin_error.go index 0209931f..5880b88d 100644 --- a/builtin_error.go +++ b/builtin_error.go @@ -1,5 +1,12 @@ package goja +func (r *Runtime) createErrorPrototype(name valueString) *Object { + o := r.newBaseObject(r.global.ErrorPrototype, classObject) + o._putProp("message", stringEmpty, true, false, true) + o._putProp("name", name, true, false, true) + return o.val +} + func (r *Runtime) initErrors() { r.global.ErrorPrototype = r.NewObject() o := r.global.ErrorPrototype.self @@ -10,51 +17,39 @@ func (r *Runtime) initErrors() { r.global.Error = r.newNativeFuncConstruct(r.builtin_Error, "Error", r.global.ErrorPrototype, 1) r.addToGlobal("Error", r.global.Error) - r.global.TypeErrorPrototype = r.builtin_new(r.global.Error, []Value{}) - o = r.global.TypeErrorPrototype.self - o._putProp("name", stringTypeError, true, false, true) + r.global.TypeErrorPrototype = r.createErrorPrototype(stringTypeError) r.global.TypeError = r.newNativeFuncConstructProto(r.builtin_Error, "TypeError", r.global.TypeErrorPrototype, r.global.Error, 1) r.addToGlobal("TypeError", r.global.TypeError) - r.global.ReferenceErrorPrototype = r.builtin_new(r.global.Error, []Value{}) - o = r.global.ReferenceErrorPrototype.self - o._putProp("name", stringReferenceError, true, false, true) + r.global.ReferenceErrorPrototype = r.createErrorPrototype(stringReferenceError) r.global.ReferenceError = r.newNativeFuncConstructProto(r.builtin_Error, "ReferenceError", r.global.ReferenceErrorPrototype, r.global.Error, 1) r.addToGlobal("ReferenceError", r.global.ReferenceError) - r.global.SyntaxErrorPrototype = r.builtin_new(r.global.Error, []Value{}) - o = r.global.SyntaxErrorPrototype.self - o._putProp("name", stringSyntaxError, true, false, true) + r.global.SyntaxErrorPrototype = r.createErrorPrototype(stringSyntaxError) r.global.SyntaxError = r.newNativeFuncConstructProto(r.builtin_Error, "SyntaxError", r.global.SyntaxErrorPrototype, r.global.Error, 1) r.addToGlobal("SyntaxError", r.global.SyntaxError) - r.global.RangeErrorPrototype = r.builtin_new(r.global.Error, []Value{}) - o = r.global.RangeErrorPrototype.self - o._putProp("name", stringRangeError, true, false, true) + r.global.RangeErrorPrototype = r.createErrorPrototype(stringRangeError) r.global.RangeError = r.newNativeFuncConstructProto(r.builtin_Error, "RangeError", r.global.RangeErrorPrototype, r.global.Error, 1) r.addToGlobal("RangeError", r.global.RangeError) - r.global.EvalErrorPrototype = r.builtin_new(r.global.Error, []Value{}) + r.global.EvalErrorPrototype = r.createErrorPrototype(stringEvalError) o = r.global.EvalErrorPrototype.self o._putProp("name", stringEvalError, true, false, true) r.global.EvalError = r.newNativeFuncConstructProto(r.builtin_Error, "EvalError", r.global.EvalErrorPrototype, r.global.Error, 1) r.addToGlobal("EvalError", r.global.EvalError) - r.global.URIErrorPrototype = r.builtin_new(r.global.Error, []Value{}) - o = r.global.URIErrorPrototype.self - o._putProp("name", stringURIError, true, false, true) + r.global.URIErrorPrototype = r.createErrorPrototype(stringURIError) r.global.URIError = r.newNativeFuncConstructProto(r.builtin_Error, "URIError", r.global.URIErrorPrototype, r.global.Error, 1) r.addToGlobal("URIError", r.global.URIError) - r.global.GoErrorPrototype = r.builtin_new(r.global.Error, []Value{}) - o = r.global.GoErrorPrototype.self - o._putProp("name", stringGoError, true, false, true) + r.global.GoErrorPrototype = r.createErrorPrototype(stringGoError) r.global.GoError = r.newNativeFuncConstructProto(r.builtin_Error, "GoError", r.global.GoErrorPrototype, r.global.Error, 1) r.addToGlobal("GoError", r.global.GoError) diff --git a/builtin_function.go b/builtin_function.go index 7cc45f79..249e29bd 100644 --- a/builtin_function.go +++ b/builtin_function.go @@ -59,6 +59,16 @@ repeat: return nil } +func (r *Runtime) functionproto_hasInstance(call FunctionCall) Value { + if o, ok := call.This.(*Object); ok { + if _, ok = o.self.assertCallable(); ok { + return r.toBoolean(o.self.hasInstance(call.Argument(0))) + } + } + + return valueFalse +} + func (r *Runtime) createListFromArrayLike(a Value) []Value { o := r.toObject(a) if arr := r.checkStdArrayObj(o); arr != nil { @@ -145,9 +155,15 @@ func (r *Runtime) functionproto_bind(call FunctionCall) Value { l = 0 } + name := obj.self.getStr("name", nil) + nameStr := stringBound_ + if s, ok := name.(valueString); ok { + nameStr = nameStr.concat(s) + } + v := &Object{runtime: r} - ff := r.newNativeFuncObj(v, r.boundCallable(fcall, call.Arguments), r.boundConstruct(construct, call.Arguments), "", nil, l) + ff := r.newNativeFuncObj(v, r.boundCallable(fcall, call.Arguments), r.boundConstruct(construct, call.Arguments), nameStr.string(), nil, l) v.self = &boundFuncObject{ nativeFuncObject: *ff, wrapped: obj, @@ -161,12 +177,15 @@ func (r *Runtime) functionproto_bind(call FunctionCall) Value { } func (r *Runtime) initFunction() { - o := r.global.FunctionPrototype.self - o.(*nativeFuncObject).prototype = r.global.ObjectPrototype - o._putProp("toString", r.newNativeFunc(r.functionproto_toString, nil, "toString", nil, 0), true, false, true) + o := r.global.FunctionPrototype.self.(*nativeFuncObject) + o.prototype = r.global.ObjectPrototype + o.nameProp.value = stringEmpty + o._putProp("apply", r.newNativeFunc(r.functionproto_apply, nil, "apply", nil, 2), true, false, true) - o._putProp("call", r.newNativeFunc(r.functionproto_call, nil, "call", nil, 1), true, false, true) o._putProp("bind", r.newNativeFunc(r.functionproto_bind, nil, "bind", nil, 1), true, false, true) + o._putProp("call", r.newNativeFunc(r.functionproto_call, nil, "call", nil, 1), true, false, true) + o._putProp("toString", r.newNativeFunc(r.functionproto_toString, nil, "toString", nil, 0), true, false, true) + o._putSym(symHasInstance, valueProp(r.newNativeFunc(r.functionproto_hasInstance, nil, "[Symbol.hasInstance]", nil, 1), false, false, false)) r.global.Function = r.newNativeFuncConstruct(r.builtin_Function, "Function", r.global.FunctionPrototype, 1) r.addToGlobal("Function", r.global.Function) diff --git a/runtime.go b/runtime.go index 9fa55f43..55657c40 100644 --- a/runtime.go +++ b/runtime.go @@ -334,7 +334,10 @@ func (r *Runtime) init() { } r.vm.init() - r.global.FunctionPrototype = r.newNativeFunc(nil, nil, "Empty", nil, 0) + r.global.FunctionPrototype = r.newNativeFunc(func(FunctionCall) Value { + return _undefined + }, nil, " ", nil, 0) + r.global.IteratorPrototype = r.newLazyObject(r.createIterProto) r.initObject() diff --git a/string.go b/string.go index f1e42022..4f600e15 100644 --- a/string.go +++ b/string.go @@ -29,6 +29,7 @@ var ( stringInfinity = asciiString("Infinity") stringPlusInfinity = asciiString("+Infinity") stringNegInfinity = asciiString("-Infinity") + stringBound_ valueString = asciiString("bound ") stringEmpty valueString = asciiString("") stringError valueString = asciiString("Error") diff --git a/tc39_test.go b/tc39_test.go index 017c34e1..ebf046a7 100644 --- a/tc39_test.go +++ b/tc39_test.go @@ -40,31 +40,62 @@ var ( "test/annexB/built-ins/escape/escape-above-astral.js": true, // \u{xxxxx} // class - "test/language/statements/class/subclass/builtin-objects/Symbol/symbol-valid-as-extends-value.js": true, - "test/language/statements/class/subclass/builtin-objects/Symbol/new-symbol-with-super-throws.js": true, - "test/language/statements/class/subclass/builtin-objects/WeakSet/super-must-be-called.js": true, - "test/language/statements/class/subclass/builtin-objects/WeakSet/regular-subclassing.js": true, - "test/language/statements/class/subclass/builtin-objects/WeakMap/super-must-be-called.js": true, - "test/language/statements/class/subclass/builtin-objects/WeakMap/regular-subclassing.js": true, - "test/language/statements/class/subclass/builtin-objects/Map/super-must-be-called.js": true, - "test/language/statements/class/subclass/builtin-objects/Map/regular-subclassing.js": true, - "test/language/statements/class/subclass/builtin-objects/Set/super-must-be-called.js": true, - "test/language/statements/class/subclass/builtin-objects/Set/regular-subclassing.js": true, - "test/language/statements/class/subclass/builtin-objects/Object/replacing-prototype.js": true, - "test/language/statements/class/subclass/builtin-objects/Object/regular-subclassing.js": true, - "test/built-ins/Array/prototype/concat/Array.prototype.concat_non-array.js": true, - "test/language/statements/class/subclass/builtin-objects/Array/length.js": true, - "test/language/statements/class/subclass/builtin-objects/TypedArray/super-must-be-called.js": true, - "test/language/statements/class/subclass/builtin-objects/TypedArray/regular-subclassing.js": true, - "test/language/statements/class/subclass/builtin-objects/DataView/super-must-be-called.js": true, - "test/language/statements/class/subclass/builtin-objects/DataView/regular-subclassing.js": true, - "test/language/statements/class/subclass/builtin-objects/String/super-must-be-called.js": true, - "test/language/statements/class/subclass/builtin-objects/String/regular-subclassing.js": true, - "test/language/statements/class/subclass/builtin-objects/String/length.js": true, - "test/language/statements/class/subclass/builtin-objects/Date/super-must-be-called.js": true, - "test/language/statements/class/subclass/builtin-objects/Date/regular-subclassing.js": true, - "test/language/statements/class/subclass/builtin-objects/Number/super-must-be-called.js": true, - "test/language/statements/class/subclass/builtin-objects/Number/regular-subclassing.js": true, + "test/language/statements/class/subclass/builtin-objects/Symbol/symbol-valid-as-extends-value.js": true, + "test/language/statements/class/subclass/builtin-objects/Symbol/new-symbol-with-super-throws.js": true, + "test/language/statements/class/subclass/builtin-objects/WeakSet/super-must-be-called.js": true, + "test/language/statements/class/subclass/builtin-objects/WeakSet/regular-subclassing.js": true, + "test/language/statements/class/subclass/builtin-objects/WeakMap/super-must-be-called.js": true, + "test/language/statements/class/subclass/builtin-objects/WeakMap/regular-subclassing.js": true, + "test/language/statements/class/subclass/builtin-objects/Map/super-must-be-called.js": true, + "test/language/statements/class/subclass/builtin-objects/Map/regular-subclassing.js": true, + "test/language/statements/class/subclass/builtin-objects/Set/super-must-be-called.js": true, + "test/language/statements/class/subclass/builtin-objects/Set/regular-subclassing.js": true, + "test/language/statements/class/subclass/builtin-objects/Object/replacing-prototype.js": true, + "test/language/statements/class/subclass/builtin-objects/Object/regular-subclassing.js": true, + "test/built-ins/Array/prototype/concat/Array.prototype.concat_non-array.js": true, + "test/language/statements/class/subclass/builtin-objects/Array/length.js": true, + "test/language/statements/class/subclass/builtin-objects/TypedArray/super-must-be-called.js": true, + "test/language/statements/class/subclass/builtin-objects/TypedArray/regular-subclassing.js": true, + "test/language/statements/class/subclass/builtin-objects/DataView/super-must-be-called.js": true, + "test/language/statements/class/subclass/builtin-objects/DataView/regular-subclassing.js": true, + "test/language/statements/class/subclass/builtin-objects/String/super-must-be-called.js": true, + "test/language/statements/class/subclass/builtin-objects/String/regular-subclassing.js": true, + "test/language/statements/class/subclass/builtin-objects/String/length.js": true, + "test/language/statements/class/subclass/builtin-objects/Date/super-must-be-called.js": true, + "test/language/statements/class/subclass/builtin-objects/Date/regular-subclassing.js": true, + "test/language/statements/class/subclass/builtin-objects/Number/super-must-be-called.js": true, + "test/language/statements/class/subclass/builtin-objects/Number/regular-subclassing.js": true, + "test/language/statements/class/subclass/builtin-objects/Function/super-must-be-called.js": true, + "test/language/statements/class/subclass/builtin-objects/Function/regular-subclassing.js": true, + "test/language/statements/class/subclass/builtin-objects/Function/instance-name.js": true, + "test/language/statements/class/subclass/builtin-objects/Function/instance-length.js": true, + "test/language/statements/class/subclass/builtin-objects/Boolean/super-must-be-called.js": true, + "test/language/statements/class/subclass/builtin-objects/Boolean/regular-subclassing.js": true, + "test/language/statements/class/subclass/builtin-objects/NativeError/URIError-super.js": true, + "test/language/statements/class/subclass/builtin-objects/NativeError/URIError-name.js": true, + "test/language/statements/class/subclass/builtin-objects/NativeError/URIError-message.js": true, + "test/language/statements/class/subclass/builtin-objects/NativeError/TypeError-super.js": true, + "test/language/statements/class/subclass/builtin-objects/NativeError/TypeError-name.js": true, + "test/language/statements/class/subclass/builtin-objects/NativeError/TypeError-message.js": true, + "test/language/statements/class/subclass/builtin-objects/NativeError/SyntaxError-super.js": true, + "test/language/statements/class/subclass/builtin-objects/NativeError/SyntaxError-name.js": true, + "test/language/statements/class/subclass/builtin-objects/NativeError/SyntaxError-message.js": true, + "test/language/statements/class/subclass/builtin-objects/NativeError/ReferenceError-super.js": true, + "test/language/statements/class/subclass/builtin-objects/NativeError/ReferenceError-name.js": true, + "test/language/statements/class/subclass/builtin-objects/NativeError/ReferenceError-message.js": true, + "test/language/statements/class/subclass/builtin-objects/NativeError/RangeError-super.js": true, + "test/language/statements/class/subclass/builtin-objects/NativeError/RangeError-name.js": true, + "test/language/statements/class/subclass/builtin-objects/NativeError/RangeError-message.js": true, + "test/language/statements/class/subclass/builtin-objects/NativeError/EvalError-super.js": true, + "test/language/statements/class/subclass/builtin-objects/NativeError/EvalError-name.js": true, + "test/language/statements/class/subclass/builtin-objects/NativeError/EvalError-message.js": true, + "test/language/statements/class/subclass/builtin-objects/Error/super-must-be-called.js": true, + "test/language/statements/class/subclass/builtin-objects/Error/regular-subclassing.js": true, + "test/language/statements/class/subclass/builtin-objects/Error/message-property-assignment.js": true, + "test/language/statements/class/subclass/builtin-objects/Array/super-must-be-called.js": true, + "test/language/statements/class/subclass/builtin-objects/Array/regular-subclassing.js": true, + "test/language/statements/class/subclass/builtin-objects/Array/contructor-calls-super-single-argument.js": true, + "test/language/statements/class/subclass/builtin-objects/Array/contructor-calls-super-multiple-arguments.js": true, // full unicode regexp flag "test/built-ins/RegExp/prototype/Symbol.match/u-advance-after-empty.js": true, @@ -109,17 +140,16 @@ var ( "12.9.3", "12.9.4", "19.1", + "19.2", + "19.3", "19.4", + "19.5", "20.1", "20.2", "20.3", "21.1", "21.2.5.6", - "22.1.2.1", - "22.1.2.3", - "22.1.2.5", - "22.1.3", - "22.1.4", + "22.1", "22.2", "23.1", "23.2", @@ -134,9 +164,9 @@ var ( } esIdPrefixWhiteList = []string{ - "sec-array.prototype.includes", + "sec-array", "sec-%typedarray%", - "sec-string.prototype", + "sec-string", "sec-date", "sec-number", "sec-math", From 1edec8603b20bcf22cf748c88a8da0248955e616 Mon Sep 17 00:00:00 2001 From: Dmitry Panov Date: Sun, 26 Apr 2020 14:59:13 +0100 Subject: [PATCH 30/46] Standard compliance and other fixes for ArrayBuffer --- builtin_typedarrays.go | 41 ++++++++++++++++++++++++++--------------- tc39_test.go | 10 ++++++++++ 2 files changed, 36 insertions(+), 15 deletions(-) diff --git a/builtin_typedarrays.go b/builtin_typedarrays.go index 00f54883..d7384fbc 100644 --- a/builtin_typedarrays.go +++ b/builtin_typedarrays.go @@ -1,6 +1,7 @@ package goja import ( + "fmt" "math" "sort" "strings" @@ -61,13 +62,26 @@ func (ctx *typedArraySortCtx) Swap(i, j int) { ctx.ta.typedArray.swap(offset+i, offset+j) } +func allocByteSlice(size int) (b []byte) { + defer func() { + if x := recover(); x != nil { + panic(rangeError(fmt.Sprintf("Buffer size is too large: %d", size))) + } + }() + if size < 0 { + panic(rangeError(fmt.Sprintf("Invalid buffer size: %d", size))) + } + b = make([]byte, size) + return +} + func (r *Runtime) builtin_newArrayBuffer(args []Value, newTarget *Object) *Object { if newTarget == nil { panic(r.needNew("ArrayBuffer")) } b := r._newArrayBuffer(r.getPrototypeFromCtor(newTarget, r.global.ArrayBuffer, r.global.ArrayBufferPrototype), nil) if len(args) > 0 { - b.data = make([]byte, toLength(args[0])) + b.data = allocByteSlice(r.toIndex(args[0])) } return b.val } @@ -75,9 +89,7 @@ func (r *Runtime) builtin_newArrayBuffer(args []Value, newTarget *Object) *Objec func (r *Runtime) arrayBufferProto_getByteLength(call FunctionCall) Value { o := r.toObject(call.This) if b, ok := o.self.(*arrayBufferObject); ok { - if b.data == nil { - panic(r.NewTypeError("ArrayBuffer is detached")) - } + b.ensureNotDetached() return intToValue(int64(len(b.data))) } panic(r.NewTypeError("Object is not ArrayBuffer: %s", o)) @@ -87,10 +99,10 @@ func (r *Runtime) arrayBufferProto_slice(call FunctionCall) Value { o := r.toObject(call.This) if b, ok := o.self.(*arrayBufferObject); ok { l := int64(len(b.data)) - start := relToIdx(toLength(call.Argument(0)), l) + start := relToIdx(call.Argument(0).ToInteger(), l) var stop int64 if arg := call.Argument(1); arg != _undefined { - stop = toLength(arg) + stop = arg.ToInteger() } else { stop = l } @@ -98,18 +110,14 @@ func (r *Runtime) arrayBufferProto_slice(call FunctionCall) Value { newLen := max(stop-start, 0) ret := r.speciesConstructor(o, r.global.ArrayBuffer)([]Value{intToValue(newLen)}, nil) if ab, ok := ret.self.(*arrayBufferObject); ok { - if ab.data == nil { - panic(r.NewTypeError("Species constructor returned a detached ArrayBuffer")) - } + ab.ensureNotDetached() if ret == o { panic(r.NewTypeError("Species constructor returned the same ArrayBuffer")) } if int64(len(ab.data)) < newLen { panic(r.NewTypeError("Species constructor returned an ArrayBuffer that is too small: %d", len(ab.data))) } - if b.data == nil { - panic(r.NewTypeError("Species constructor has detached the current ArrayBuffer")) - } + b.ensureNotDetached() if stop > start { copy(ab.data, b.data[start:stop]) @@ -122,10 +130,13 @@ func (r *Runtime) arrayBufferProto_slice(call FunctionCall) Value { } func (r *Runtime) arrayBuffer_isView(call FunctionCall) Value { - if o, ok := call.This.(*Object); ok { + if o, ok := call.Argument(0).(*Object); ok { if _, ok := o.self.(*dataViewObject); ok { return valueTrue } + if _, ok := o.self.(*typedArrayObject); ok { + return valueTrue + } } return valueFalse } @@ -1047,7 +1058,7 @@ func (r *Runtime) allocateTypedArray(newTarget *Object, length int, taCtor typed buf := r._newArrayBuffer(r.global.ArrayBufferPrototype, nil) ta := taCtor(buf, 0, length, r.getPrototypeFromCtor(newTarget, nil, r.global.TypedArrayPrototype)) if length > 0 { - buf.data = make([]byte, length*ta.elemSize) + buf.data = allocByteSlice(length * ta.elemSize) } return ta.val } @@ -1157,7 +1168,7 @@ func (r *Runtime) _newTypedArrayFromTypedArray(src *typedArrayObject, newTarget src.viewedArrayBuf.ensureNotDetached() l := src.length dst.viewedArrayBuf.prototype = r.getPrototypeFromCtor(r.toObject(src.viewedArrayBuf.getStr("constructor", nil)), r.global.ArrayBuffer, r.global.ArrayBufferPrototype) - dst.viewedArrayBuf.data = make([]byte, int64(l)*int64(dst.elemSize)) + dst.viewedArrayBuf.data = allocByteSlice(toInt(int64(l) * int64(dst.elemSize))) if src.defaultCtor == dst.defaultCtor { copy(dst.viewedArrayBuf.data, src.viewedArrayBuf.data[src.offset*src.elemSize:]) dst.length = src.length diff --git a/tc39_test.go b/tc39_test.go index ebf046a7..da19abbb 100644 --- a/tc39_test.go +++ b/tc39_test.go @@ -39,6 +39,9 @@ var ( "test/built-ins/Date/prototype/toISOString/15.9.5.43-0-10.js": true, // timezone "test/annexB/built-ins/escape/escape-above-astral.js": true, // \u{xxxxx} + // SharedArrayBuffer + "test/built-ins/ArrayBuffer/prototype/slice/this-is-sharedarraybuffer.js": true, + // class "test/language/statements/class/subclass/builtin-objects/Symbol/symbol-valid-as-extends-value.js": true, "test/language/statements/class/subclass/builtin-objects/Symbol/new-symbol-with-super-throws.js": true, @@ -96,6 +99,10 @@ var ( "test/language/statements/class/subclass/builtin-objects/Array/regular-subclassing.js": true, "test/language/statements/class/subclass/builtin-objects/Array/contructor-calls-super-single-argument.js": true, "test/language/statements/class/subclass/builtin-objects/Array/contructor-calls-super-multiple-arguments.js": true, + "test/language/statements/class/subclass/builtin-objects/ArrayBuffer/super-must-be-called.js": true, + "test/language/statements/class/subclass/builtin-objects/ArrayBuffer/regular-subclassing.js": true, + "test/built-ins/ArrayBuffer/isView/arg-is-typedarray-subclass-instance.js": true, + "test/built-ins/ArrayBuffer/isView/arg-is-dataview-subclass-instance.js": true, // full unicode regexp flag "test/built-ins/RegExp/prototype/Symbol.match/u-advance-after-empty.js": true, @@ -155,6 +162,7 @@ var ( "23.2", "23.3", "23.4", + "24.1", "24.2", "25.1.2", "26.1", @@ -170,6 +178,8 @@ var ( "sec-date", "sec-number", "sec-math", + "sec-arraybuffer-length", + "sec-arraybuffer", } ) From 351feaff5375becdf437fcd412973bf025ca5d8d Mon Sep 17 00:00:00 2001 From: Dmitry Panov Date: Mon, 27 Apr 2020 22:36:20 +0100 Subject: [PATCH 31/46] Ported V8's fast-dtoa. Most conversions should now run at comparable speed to strconv.FormatFloat(). --- date.go | 19 - ftoa/LICENSE_LUCENE | 21 + ftoa/common.go | 22 +- ftoa/ftoa.go | 698 +++++++++++++++++++++++ ftoa/ftobasestr.go | 5 +- ftoa/ftostr.go | 896 ++++-------------------------- ftoa/ftostr_test.go | 23 +- ftoa/internal/fast/LICENSE_V8 | 26 + ftoa/internal/fast/cachedpower.go | 120 ++++ ftoa/internal/fast/common.go | 18 + ftoa/internal/fast/diyfp.go | 152 +++++ ftoa/internal/fast/dtoa.go | 624 +++++++++++++++++++++ 12 files changed, 1791 insertions(+), 833 deletions(-) create mode 100644 ftoa/LICENSE_LUCENE create mode 100644 ftoa/ftoa.go create mode 100644 ftoa/internal/fast/LICENSE_V8 create mode 100644 ftoa/internal/fast/cachedpower.go create mode 100644 ftoa/internal/fast/common.go create mode 100644 ftoa/internal/fast/diyfp.go create mode 100644 ftoa/internal/fast/dtoa.go diff --git a/date.go b/date.go index 1df98cee..e8ae5808 100644 --- a/date.go +++ b/date.go @@ -113,25 +113,6 @@ func (d *dateObject) export() interface{} { return nil } -func (d *dateObject) setTime(year, m, day, hour, min, sec, nsec int64) Value { - t, ok := mkTime(year, m, day, hour, min, sec, nsec, time.Local) - if ok { - return d.setTimeMs(timeToMsec(t)) - } - d.unset() - return _NaN -} - -func (d *dateObject) setTimeUTC(year, m, day, hour, min, sec, nsec int64) Value { - t, ok := mkTime(year, m, day, hour, min, sec, nsec, time.UTC) - if ok { - t = t.In(time.Local) - return d.setTimeMs(timeToMsec(t)) - } - d.unset() - return _NaN -} - func (d *dateObject) setTimeMs(ms int64) Value { if ms >= 0 && ms <= maxTime || ms < 0 && ms >= -maxTime { d.msec = ms diff --git a/ftoa/LICENSE_LUCENE b/ftoa/LICENSE_LUCENE new file mode 100644 index 00000000..c8da489c --- /dev/null +++ b/ftoa/LICENSE_LUCENE @@ -0,0 +1,21 @@ +Copyright (C) 1998, 1999 by Lucent Technologies +All Rights Reserved + +Permission to use, copy, modify, and distribute this software and +its documentation for any purpose and without fee is hereby +granted, provided that the above copyright notice appear in all +copies and that both that the copyright notice and this +permission notice and warranty disclaimer appear in supporting +documentation, and that the name of Lucent or any of its entities +not be used in advertising or publicity pertaining to +distribution of the software without specific, written prior +permission. + +LUCENT DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, +INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. +IN NO EVENT SHALL LUCENT OR ANY OF ITS ENTITIES BE LIABLE FOR ANY +SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER +IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. diff --git a/ftoa/common.go b/ftoa/common.go index ea632df4..207fb5fa 100644 --- a/ftoa/common.go +++ b/ftoa/common.go @@ -1,12 +1,16 @@ /* Package ftoa provides ECMAScript-compliant floating point number conversion to string. + It contains code ported from Rhino (https://github.com/mozilla/rhino/blob/master/src/org/mozilla/javascript/DToA.java) +as well as from the original code by David M. Gay. + +See LICENSE_LUCENE for the original copyright message and disclaimer. + */ package ftoa import ( "math" - "math/big" ) const ( @@ -97,7 +101,7 @@ func stuffBits(bits []byte, offset int, val uint32) { bits[offset+3] = byte(val) } -func d2b(d float64, bi *big.Int) (e, bits int) { +func d2b(d float64, b []byte) (e, bits int, dblBits []byte) { dBits := math.Float64bits(d) d0 := uint32(dBits >> 32) d1 := uint32(dBits) @@ -106,33 +110,32 @@ func d2b(d float64, bi *big.Int) (e, bits int) { d0 &= 0x7fffffff /* clear sign bit, which we ignore */ var de, k, i int - var dbl_bits []byte if de = int(d0 >> exp_shift); de != 0 { z |= exp_msk1 } y := d1 if y != 0 { - dbl_bits = make([]byte, 8) + dblBits = b[:8] k = lo0bits(y) y >>= k if k != 0 { - stuffBits(dbl_bits, 4, y|z<<(32-k)) + stuffBits(dblBits, 4, y|z<<(32-k)) z >>= k } else { - stuffBits(dbl_bits, 4, y) + stuffBits(dblBits, 4, y) } - stuffBits(dbl_bits, 0, z) + stuffBits(dblBits, 0, z) if z != 0 { i = 2 } else { i = 1 } } else { - dbl_bits = make([]byte, 4) + dblBits = b[:4] k = lo0bits(z) z >>= k - stuffBits(dbl_bits, 0, z) + stuffBits(dblBits, 0, z) k += 32 i = 1 } @@ -144,6 +147,5 @@ func d2b(d float64, bi *big.Int) (e, bits int) { e = de - bias - (p - 1) + 1 + k bits = 32*i - hi0bits(z) } - bi.SetBytes(dbl_bits) return } diff --git a/ftoa/ftoa.go b/ftoa/ftoa.go new file mode 100644 index 00000000..59b516d7 --- /dev/null +++ b/ftoa/ftoa.go @@ -0,0 +1,698 @@ +package ftoa + +import ( + "math" + "math/big" +) + +const ( + exp_11 = 0x3ff00000 + frac_mask1 = 0xfffff + bletch = 0x10 + quick_max = 14 + int_max = 14 +) + +var ( + tens = [...]float64{ + 1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, + 1e10, 1e11, 1e12, 1e13, 1e14, 1e15, 1e16, 1e17, 1e18, 1e19, + 1e20, 1e21, 1e22, + } + + bigtens = [...]float64{1e16, 1e32, 1e64, 1e128, 1e256} + + big5 = big.NewInt(5) + big10 = big.NewInt(10) + + p05 = []*big.Int{big5, big.NewInt(25), big.NewInt(125)} + pow5Cache [7]*big.Int + + dtoaModes = []int{ + ModeStandard: 0, + ModeStandardExponential: 0, + ModeFixed: 3, + ModeExponential: 2, + ModePrecision: 2, + } +) + +/* +d must be > 0 and must not be Inf + +mode: + 0 ==> shortest string that yields d when read in + and rounded to nearest. + 1 ==> like 0, but with Steele & White stopping rule; + e.g. with IEEE P754 arithmetic , mode 0 gives + 1e23 whereas mode 1 gives 9.999999999999999e22. + 2 ==> max(1,ndigits) significant digits. This gives a + return value similar to that of ecvt, except + that trailing zeros are suppressed. + 3 ==> through ndigits past the decimal point. This + gives a return value similar to that from fcvt, + except that trailing zeros are suppressed, and + ndigits can be negative. + 4,5 ==> similar to 2 and 3, respectively, but (in + round-nearest mode) with the tests of mode 0 to + possibly return a shorter string that rounds to d. + With IEEE arithmetic and compilation with + -DHonor_FLT_ROUNDS, modes 4 and 5 behave the same + as modes 2 and 3 when FLT_ROUNDS != 1. + 6-9 ==> Debugging modes similar to mode - 4: don't try + fast floating-point estimate (if applicable). + + Values of mode other than 0-9 are treated as mode 0. +*/ +func ftoa(d float64, mode int, biasUp bool, ndigits int, buf []byte) ([]byte, int) { + startPos := len(buf) + dblBits := make([]byte, 0, 8) + be, bbits, dblBits := d2b(d, dblBits) + + dBits := math.Float64bits(d) + word0 := uint32(dBits >> 32) + word1 := uint32(dBits) + + i := int((word0 >> exp_shift1) & (exp_mask >> exp_shift1)) + var d2 float64 + var denorm bool + if i != 0 { + d2 = setWord0(d, (word0&frac_mask1)|exp_11) + i -= bias + denorm = false + } else { + /* d is denormalized */ + i = bbits + be + (bias + (p - 1) - 1) + var x uint64 + if i > 32 { + x = uint64(word0)<<(64-i) | uint64(word1)>>(i-32) + } else { + x = uint64(word1) << (32 - i) + } + d2 = setWord0(float64(x), uint32((x>>32)-31*exp_mask)) + i -= (bias + (p - 1) - 1) + 1 + denorm = true + } + /* At this point d = f*2^i, where 1 <= f < 2. d2 is an approximation of f. */ + ds := (d2-1.5)*0.289529654602168 + 0.1760912590558 + float64(i)*0.301029995663981 + k := int(ds) + if ds < 0.0 && ds != float64(k) { + k-- /* want k = floor(ds) */ + } + k_check := true + if k >= 0 && k < len(tens) { + if d < tens[k] { + k-- + } + k_check = false + } + /* At this point floor(log10(d)) <= k <= floor(log10(d))+1. + If k_check is zero, we're guaranteed that k = floor(log10(d)). */ + j := bbits - i - 1 + var b2, s2, b5, s5 int + /* At this point d = b/2^j, where b is an odd integer. */ + if j >= 0 { + b2 = 0 + s2 = j + } else { + b2 = -j + s2 = 0 + } + if k >= 0 { + b5 = 0 + s5 = k + s2 += k + } else { + b2 -= k + b5 = -k + s5 = 0 + } + /* At this point d/10^k = (b * 2^b2 * 5^b5) / (2^s2 * 5^s5), where b is an odd integer, + b2 >= 0, b5 >= 0, s2 >= 0, and s5 >= 0. */ + if mode < 0 || mode > 9 { + mode = 0 + } + try_quick := true + if mode > 5 { + mode -= 4 + try_quick = false + } + leftright := true + var ilim, ilim1 int + switch mode { + case 0, 1: + ilim, ilim1 = -1, -1 + ndigits = 0 + case 2: + leftright = false + fallthrough + case 4: + if ndigits <= 0 { + ndigits = 1 + } + ilim, ilim1 = ndigits, ndigits + case 3: + leftright = false + fallthrough + case 5: + i = ndigits + k + 1 + ilim = i + ilim1 = i - 1 + } + /* ilim is the maximum number of significant digits we want, based on k and ndigits. */ + /* ilim1 is the maximum number of significant digits we want, based on k and ndigits, + when it turns out that k was computed too high by one. */ + fast_failed := false + if ilim >= 0 && ilim <= quick_max && try_quick { + + /* Try to get by with floating-point arithmetic. */ + + i = 0 + d2 = d + k0 := k + ilim0 := ilim + ieps := 2 /* conservative */ + /* Divide d by 10^k, keeping track of the roundoff error and avoiding overflows. */ + if k > 0 { + ds = tens[k&0xf] + j = k >> 4 + if (j & bletch) != 0 { + /* prevent overflows */ + j &= bletch - 1 + d /= bigtens[len(bigtens)-1] + ieps++ + } + for ; j != 0; i++ { + if (j & 1) != 0 { + ieps++ + ds *= bigtens[i] + } + j >>= 1 + } + d /= ds + } else if j1 := -k; j1 != 0 { + d *= tens[j1&0xf] + for j = j1 >> 4; j != 0; i++ { + if (j & 1) != 0 { + ieps++ + d *= bigtens[i] + } + j >>= 1 + } + } + /* Check that k was computed correctly. */ + if k_check && d < 1.0 && ilim > 0 { + if ilim1 <= 0 { + fast_failed = true + } else { + ilim = ilim1 + k-- + d *= 10. + ieps++ + } + } + /* eps bounds the cumulative error. */ + eps := float64(ieps)*d + 7.0 + eps = setWord0(eps, _word0(eps)-(p-1)*exp_msk1) + if ilim == 0 { + d -= 5.0 + if d > eps { + buf = append(buf, '1') + k++ + return buf, k + 1 + } + if d < -eps { + buf = append(buf, '0') + return buf, 1 + } + fast_failed = true + } + if !fast_failed { + fast_failed = true + if leftright { + /* Use Steele & White method of only + * generating digits needed. + */ + eps = 0.5/tens[ilim-1] - eps + for i = 0; ; { + l := int64(d) + d -= float64(l) + buf = append(buf, byte('0'+l)) + if d < eps { + return buf, k + 1 + } + if 1.0-d < eps { + buf, k = bumpUp(buf, k) + return buf, k + 1 + } + i++ + if i >= ilim { + break + } + eps *= 10.0 + d *= 10.0 + } + } else { + /* Generate ilim digits, then fix them up. */ + eps *= tens[ilim-1] + for i = 1; ; i++ { + l := int64(d) + d -= float64(l) + buf = append(buf, byte('0'+l)) + if i == ilim { + if d > 0.5+eps { + buf, k = bumpUp(buf, k) + return buf, k + 1 + } else if d < 0.5-eps { + buf = stripTrailingZeroes(buf, startPos) + return buf, k + 1 + } + break + } + d *= 10.0 + } + } + } + if fast_failed { + buf = buf[:startPos] + d = d2 + k = k0 + ilim = ilim0 + } + } + + /* Do we have a "small" integer? */ + if be >= 0 && k <= int_max { + /* Yes. */ + ds = tens[k] + if ndigits < 0 && ilim <= 0 { + if ilim < 0 || d < 5*ds || (!biasUp && d == 5*ds) { + buf = buf[:startPos] + buf = append(buf, '0') + return buf, 1 + } + buf = append(buf, '1') + k++ + return buf, k + 1 + } + for i = 1; ; i++ { + l := int64(d / ds) + d -= float64(l) * ds + buf = append(buf, byte('0'+l)) + if i == ilim { + d += d + if (d > ds) || (d == ds && (((l & 1) != 0) || biasUp)) { + buf, k = bumpUp(buf, k) + } + break + } + d *= 10.0 + if d == 0 { + break + } + } + return buf, k + 1 + } + + m2 := b2 + m5 := b5 + var mhi, mlo *big.Int + if leftright { + if mode < 2 { + if denorm { + i = be + (bias + (p - 1) - 1 + 1) + } else { + i = 1 + p - bbits + } + /* i is 1 plus the number of trailing zero bits in d's significand. Thus, + (2^m2 * 5^m5) / (2^(s2+i) * 5^s5) = (1/2 lsb of d)/10^k. */ + } else { + j = ilim - 1 + if m5 >= j { + m5 -= j + } else { + j -= m5 + s5 += j + b5 += j + m5 = 0 + } + i = ilim + if i < 0 { + m2 -= i + i = 0 + } + /* (2^m2 * 5^m5) / (2^(s2+i) * 5^s5) = (1/2 * 10^(1-ilim))/10^k. */ + } + b2 += i + s2 += i + mhi = big.NewInt(1) + /* (mhi * 2^m2 * 5^m5) / (2^s2 * 5^s5) = one-half of last printed (when mode >= 2) or + input (when mode < 2) significant digit, divided by 10^k. */ + } + + /* We still have d/10^k = (b * 2^b2 * 5^b5) / (2^s2 * 5^s5). Reduce common factors in + b2, m2, and s2 without changing the equalities. */ + if m2 > 0 && s2 > 0 { + if m2 < s2 { + i = m2 + } else { + i = s2 + } + b2 -= i + m2 -= i + s2 -= i + } + + b := new(big.Int).SetBytes(dblBits) + /* Fold b5 into b and m5 into mhi. */ + if b5 > 0 { + if leftright { + if m5 > 0 { + pow5mult(mhi, m5) + b.Mul(mhi, b) + } + j = b5 - m5 + if j != 0 { + pow5mult(b, j) + } + } else { + pow5mult(b, b5) + } + } + /* Now we have d/10^k = (b * 2^b2) / (2^s2 * 5^s5) and + (mhi * 2^m2) / (2^s2 * 5^s5) = one-half of last printed or input significant digit, divided by 10^k. */ + + S := big.NewInt(1) + if s5 > 0 { + pow5mult(S, s5) + } + /* Now we have d/10^k = (b * 2^b2) / (S * 2^s2) and + (mhi * 2^m2) / (S * 2^s2) = one-half of last printed or input significant digit, divided by 10^k. */ + + /* Check for special case that d is a normalized power of 2. */ + spec_case := false + if mode < 2 { + if (_word1(d) == 0) && ((_word0(d) & bndry_mask) == 0) && + ((_word0(d) & (exp_mask & (exp_mask << 1))) != 0) { + /* The special case. Here we want to be within a quarter of the last input + significant digit instead of one half of it when the decimal output string's value is less than d. */ + b2 += log2P + s2 += log2P + spec_case = true + } + } + + /* Arrange for convenient computation of quotients: + * shift left if necessary so divisor has 4 leading 0 bits. + * + * Perhaps we should just compute leading 28 bits of S once + * and for all and pass them and a shift to quorem, so it + * can do shifts and ors to compute the numerator for q. + */ + var zz int + if s5 != 0 { + S_bytes := S.Bytes() + var S_hiWord uint32 + for idx := 0; idx < 4; idx++ { + S_hiWord = S_hiWord << 8 + if idx < len(S_bytes) { + S_hiWord |= uint32(S_bytes[idx]) + } + } + zz = 32 - hi0bits(S_hiWord) + } else { + zz = 1 + } + i = (zz + s2) & 0x1f + if i != 0 { + i = 32 - i + } + /* i is the number of leading zero bits in the most significant word of S*2^s2. */ + if i > 4 { + i -= 4 + b2 += i + m2 += i + s2 += i + } else if i < 4 { + i += 28 + b2 += i + m2 += i + s2 += i + } + /* Now S*2^s2 has exactly four leading zero bits in its most significant word. */ + if b2 > 0 { + b = b.Lsh(b, uint(b2)) + } + if s2 > 0 { + S.Lsh(S, uint(s2)) + } + /* Now we have d/10^k = b/S and + (mhi * 2^m2) / S = maximum acceptable error, divided by 10^k. */ + if k_check { + if b.Cmp(S) < 0 { + k-- + b.Mul(b, big10) /* we botched the k estimate */ + if leftright { + mhi.Mul(mhi, big10) + } + ilim = ilim1 + } + } + /* At this point 1 <= d/10^k = b/S < 10. */ + + if ilim <= 0 && mode > 2 { + /* We're doing fixed-mode output and d is less than the minimum nonzero output in this mode. + Output either zero or the minimum nonzero output depending on which is closer to d. */ + if ilim >= 0 { + i = b.Cmp(S.Mul(S, big5)) + } + if ilim < 0 || i < 0 || i == 0 && !biasUp { + /* Always emit at least one digit. If the number appears to be zero + using the current mode, then emit one '0' digit and set decpt to 1. */ + buf = buf[:startPos] + buf = append(buf, '0') + return buf, 1 + } + buf = append(buf, '1') + k++ + return buf, k + 1 + } + + var dig byte + if leftright { + if m2 > 0 { + mhi.Lsh(mhi, uint(m2)) + } + + /* Compute mlo -- check for special case + * that d is a normalized power of 2. + */ + + mlo = mhi + if spec_case { + mhi = mlo + mhi = new(big.Int).Lsh(mhi, log2P) + } + /* mlo/S = maximum acceptable error, divided by 10^k, if the output is less than d. */ + /* mhi/S = maximum acceptable error, divided by 10^k, if the output is greater than d. */ + var z, delta big.Int + for i = 1; ; i++ { + z.DivMod(b, S, b) + dig = byte(z.Int64() + '0') + /* Do we yet have the shortest decimal string + * that will round to d? + */ + j = b.Cmp(mlo) + /* j is b/S compared with mlo/S. */ + delta.Sub(S, mhi) + var j1 int + if delta.Sign() <= 0 { + j1 = 1 + } else { + j1 = b.Cmp(&delta) + } + /* j1 is b/S compared with 1 - mhi/S. */ + if (j1 == 0) && (mode == 0) && ((_word1(d) & 1) == 0) { + if dig == '9' { + var flag bool + buf = append(buf, '9') + if buf, flag = roundOff(buf, startPos); flag { + k++ + buf = append(buf, '1') + } + return buf, k + 1 + } + if j > 0 { + dig++ + } + buf = append(buf, dig) + return buf, k + 1 + } + if (j < 0) || ((j == 0) && (mode == 0) && ((_word1(d) & 1) == 0)) { + if j1 > 0 { + /* Either dig or dig+1 would work here as the least significant decimal digit. + Use whichever would produce a decimal value closer to d. */ + b.Lsh(b, 1) + j1 = b.Cmp(S) + if (j1 > 0) || (j1 == 0 && (((dig & 1) == 1) || biasUp)) { + dig++ + if dig == '9' { + buf = append(buf, '9') + buf, flag := roundOff(buf, startPos) + if flag { + k++ + buf = append(buf, '1') + } + return buf, k + 1 + } + } + } + buf = append(buf, dig) + return buf, k + 1 + } + if j1 > 0 { + if dig == '9' { /* possible if i == 1 */ + buf = append(buf, '9') + buf, flag := roundOff(buf, startPos) + if flag { + k++ + buf = append(buf, '1') + } + return buf, k + 1 + } + buf = append(buf, dig+1) + return buf, k + 1 + } + buf = append(buf, dig) + if i == ilim { + break + } + b.Mul(b, big10) + if mlo == mhi { + mhi.Mul(mhi, big10) + } else { + mlo.Mul(mlo, big10) + mhi.Mul(mhi, big10) + } + } + } else { + var z big.Int + for i = 1; ; i++ { + z.DivMod(b, S, b) + dig = byte(z.Int64() + '0') + buf = append(buf, dig) + if i >= ilim { + break + } + + b.Mul(b, big10) + } + } + /* Round off last digit */ + + b.Lsh(b, 1) + j = b.Cmp(S) + if (j > 0) || (j == 0 && (((dig & 1) == 1) || biasUp)) { + var flag bool + buf, flag = roundOff(buf, startPos) + if flag { + k++ + buf = append(buf, '1') + return buf, k + 1 + } + } else { + buf = stripTrailingZeroes(buf, startPos) + } + + return buf, k + 1 +} + +func bumpUp(buf []byte, k int) ([]byte, int) { + var lastCh byte + stop := 0 + if len(buf) > 0 && buf[0] == '-' { + stop = 1 + } + for { + lastCh = buf[len(buf)-1] + buf = buf[:len(buf)-1] + if lastCh != '9' { + break + } + if len(buf) == stop { + k++ + lastCh = '0' + break + } + } + buf = append(buf, lastCh+1) + return buf, k +} + +func setWord0(d float64, w uint32) float64 { + dBits := math.Float64bits(d) + return math.Float64frombits(uint64(w)<<32 | dBits&0xffffffff) +} + +func _word0(d float64) uint32 { + dBits := math.Float64bits(d) + return uint32(dBits >> 32) +} + +func _word1(d float64) uint32 { + dBits := math.Float64bits(d) + return uint32(dBits) +} + +func stripTrailingZeroes(buf []byte, startPos int) []byte { + bl := len(buf) - 1 + for bl >= startPos && buf[bl] == '0' { + bl-- + } + return buf[:bl+1] +} + +/* Set b = b * 5^k. k must be nonnegative. */ +func pow5mult(b *big.Int, k int) *big.Int { + if k < (1 << (len(pow5Cache) + 2)) { + i := k & 3 + if i != 0 { + b.Mul(b, p05[i-1]) + } + k >>= 2 + i = 0 + for { + if k&1 != 0 { + b.Mul(b, pow5Cache[i]) + } + k >>= 1 + if k == 0 { + break + } + i++ + } + return b + } + return b.Mul(b, new(big.Int).Exp(big5, big.NewInt(int64(k)), nil)) +} + +func roundOff(buf []byte, startPos int) ([]byte, bool) { + i := len(buf) + for i != startPos { + i-- + if buf[i] != '9' { + buf[i]++ + return buf[:i+1], false + } + } + return buf[:startPos], true +} + +func init() { + p := big.NewInt(625) + pow5Cache[0] = p + for i := 1; i < len(pow5Cache); i++ { + p = new(big.Int).Mul(p, p) + pow5Cache[i] = p + } +} diff --git a/ftoa/ftobasestr.go b/ftoa/ftobasestr.go index 424a5788..9dc9b2d0 100644 --- a/ftoa/ftobasestr.go +++ b/ftoa/ftobasestr.go @@ -64,8 +64,8 @@ func FToBaseStr(num float64, radix int) string { word0 := uint32(dBits >> 32) word1 := uint32(dBits) - b := new(big.Int) - e, _ := d2b(df, b) + dblBits := make([]byte, 0, 8) + e, _, dblBits := d2b(df, dblBits) // JS_ASSERT(e < 0); /* At this point df = b * 2^e. e must be less than zero because 0 < df < 1. */ @@ -88,6 +88,7 @@ func FToBaseStr(num float64, radix int) string { mhi = big.NewInt(1 << log2P) } + b := new(big.Int).SetBytes(dblBits) b.Lsh(b, uint(e+s2)) s := big.NewInt(1) s.Lsh(s, uint(s2)) diff --git a/ftoa/ftostr.go b/ftoa/ftostr.go index e0d7a097..a9d2d240 100644 --- a/ftoa/ftostr.go +++ b/ftoa/ftostr.go @@ -2,654 +2,26 @@ package ftoa import ( "math" - "math/big" "strconv" -) -const ( - exp_11 = 0x3ff00000 - frac_mask1 = 0xfffff - bletch = 0x10 - quick_max = 14 - int_max = 14 + "github.com/dop251/goja/ftoa/internal/fast" ) type FToStrMode int const ( - ModeStandard FToStrMode = iota /* Either fixed or exponential format; round-trip */ - ModeStandardExponential /* Always exponential format; round-trip */ - ModeFixed /* Round to digits after the decimal point; exponential if number is large */ - ModeExponential /* Always exponential format; significant digits */ - ModePrecision /* Either fixed or exponential format; significant digits */ + // Either fixed or exponential format; round-trip + ModeStandard FToStrMode = iota + // Always exponential format; round-trip + ModeStandardExponential + // Round to digits after the decimal point; exponential if number is large + ModeFixed + // Always exponential format; significant digits + ModeExponential + // Either fixed or exponential format; significant digits + ModePrecision ) -var ( - tens = [...]float64{ - 1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, - 1e10, 1e11, 1e12, 1e13, 1e14, 1e15, 1e16, 1e17, 1e18, 1e19, - 1e20, 1e21, 1e22, - } - - bigtens = [...]float64{1e16, 1e32, 1e64, 1e128, 1e256} - - big5 = big.NewInt(5) - big10 = big.NewInt(10) - - p05 = []*big.Int{big5, big.NewInt(25), big.NewInt(125)} - pow5Cache [7]*big.Int - - dtoaModes = []int{ - ModeStandard: 0, - ModeStandardExponential: 0, - ModeFixed: 3, - ModeExponential: 2, - ModePrecision: 2, - } -) - -/* -mode: - 0 ==> shortest string that yields d when read in - and rounded to nearest. - 1 ==> like 0, but with Steele & White stopping rule; - e.g. with IEEE P754 arithmetic , mode 0 gives - 1e23 whereas mode 1 gives 9.999999999999999e22. - 2 ==> max(1,ndigits) significant digits. This gives a - return value similar to that of ecvt, except - that trailing zeros are suppressed. - 3 ==> through ndigits past the decimal point. This - gives a return value similar to that from fcvt, - except that trailing zeros are suppressed, and - ndigits can be negative. - 4,5 ==> similar to 2 and 3, respectively, but (in - round-nearest mode) with the tests of mode 0 to - possibly return a shorter string that rounds to d. - With IEEE arithmetic and compilation with - -DHonor_FLT_ROUNDS, modes 4 and 5 behave the same - as modes 2 and 3 when FLT_ROUNDS != 1. - 6-9 ==> Debugging modes similar to mode - 4: don't try - fast floating-point estimate (if applicable). - - Values of mode other than 0-9 are treated as mode 0. -*/ -func ftoa(d float64, mode int, biasUp bool, ndigits int, buf []byte) ([]byte, int) { - var sign bool - if math.IsNaN(d) { - buf = append(buf, "NaN"...) - return buf, 9999 - } - if math.Signbit(d) { - sign = true - d = math.Copysign(d, 1.0) - } else { - sign = false - } - if math.IsInf(d, 0) { - if sign { - buf = append(buf, '-') - } - buf = append(buf, "Infinity"...) - return buf, 9999 - } - dBits := math.Float64bits(d) - word0 := uint32(dBits >> 32) - word1 := uint32(dBits) - if d == 0 { - // no_digits - buf = append(buf, '0') - return buf, 1 - } - if sign { - buf = append(buf, '-') - } - b := new(big.Int) - be, bbits := d2b(d, b) - i := int((word0 >> exp_shift1) & (exp_mask >> exp_shift1)) - var d2 float64 - var denorm bool - if i != 0 { - d2 = setWord0(d, (word0&frac_mask1)|exp_11) - i -= bias - denorm = false - } else { - /* d is denormalized */ - i = bbits + be + (bias + (p - 1) - 1) - var x uint64 - if i > 32 { - x = uint64(word0)<<(64-i) | uint64(word1)>>(i-32) - } else { - x = uint64(word1) << (32 - i) - } - d2 = setWord0(float64(x), uint32((x>>32)-31*exp_mask)) - i -= (bias + (p - 1) - 1) + 1 - denorm = true - } - /* At this point d = f*2^i, where 1 <= f < 2. d2 is an approximation of f. */ - ds := (d2-1.5)*0.289529654602168 + 0.1760912590558 + float64(i)*0.301029995663981 - k := int(ds) - if ds < 0.0 && ds != float64(k) { - k-- /* want k = floor(ds) */ - } - k_check := true - if k >= 0 && k < len(tens) { - if d < tens[k] { - k-- - } - k_check = false - } - /* At this point floor(log10(d)) <= k <= floor(log10(d))+1. - If k_check is zero, we're guaranteed that k = floor(log10(d)). */ - j := bbits - i - 1 - var b2, s2, b5, s5 int - /* At this point d = b/2^j, where b is an odd integer. */ - if j >= 0 { - b2 = 0 - s2 = j - } else { - b2 = -j - s2 = 0 - } - if k >= 0 { - b5 = 0 - s5 = k - s2 += k - } else { - b2 -= k - b5 = -k - s5 = 0 - } - /* At this point d/10^k = (b * 2^b2 * 5^b5) / (2^s2 * 5^s5), where b is an odd integer, - b2 >= 0, b5 >= 0, s2 >= 0, and s5 >= 0. */ - if mode < 0 || mode > 9 { - mode = 0 - } - try_quick := true - if mode > 5 { - mode -= 4 - try_quick = false - } - leftright := true - var ilim, ilim1 int - switch mode { - case 0, 1: - ilim, ilim1 = -1, -1 - ndigits = 0 - case 2: - leftright = false - fallthrough - case 4: - if ndigits <= 0 { - ndigits = 1 - } - ilim, ilim1 = ndigits, ndigits - case 3: - leftright = false - fallthrough - case 5: - i = ndigits + k + 1 - ilim = i - ilim1 = i - 1 - } - /* ilim is the maximum number of significant digits we want, based on k and ndigits. */ - /* ilim1 is the maximum number of significant digits we want, based on k and ndigits, - when it turns out that k was computed too high by one. */ - fast_failed := false - if ilim >= 0 && ilim <= quick_max && try_quick { - - /* Try to get by with floating-point arithmetic. */ - - i = 0 - d2 = d - k0 := k - ilim0 := ilim - ieps := 2 /* conservative */ - /* Divide d by 10^k, keeping track of the roundoff error and avoiding overflows. */ - if k > 0 { - ds = tens[k&0xf] - j = k >> 4 - if (j & bletch) != 0 { - /* prevent overflows */ - j &= bletch - 1 - d /= bigtens[len(bigtens)-1] - ieps++ - } - for ; j != 0; i++ { - if (j & 1) != 0 { - ieps++ - ds *= bigtens[i] - } - j >>= 1 - } - d /= ds - } else if j1 := -k; j1 != 0 { - d *= tens[j1&0xf] - for j = j1 >> 4; j != 0; i++ { - if (j & 1) != 0 { - ieps++ - d *= bigtens[i] - } - j >>= 1 - } - } - /* Check that k was computed correctly. */ - if k_check && d < 1.0 && ilim > 0 { - if ilim1 <= 0 { - fast_failed = true - } else { - ilim = ilim1 - k-- - d *= 10. - ieps++ - } - } - /* eps bounds the cumulative error. */ - eps := float64(ieps)*d + 7.0 - eps = setWord0(eps, _word0(eps)-(p-1)*exp_msk1) - if ilim == 0 { - d -= 5.0 - if d > eps { - buf = append(buf, '1') - k++ - return buf, int(k + 1) - } - if d < -eps { - buf = append(buf, '0') - return buf, 1 - } - fast_failed = true - } - if !fast_failed { - fast_failed = true - if leftright { - /* Use Steele & White method of only - * generating digits needed. - */ - eps = 0.5/tens[ilim-1] - eps - for i = 0; ; { - l := int64(d) - d -= float64(l) - buf = append(buf, byte('0'+l)) - if d < eps { - return buf, k + 1 - } - if 1.0-d < eps { - buf, k = bumpUp(buf, k) - return buf, k + 1 - } - i++ - if i >= ilim { - break - } - eps *= 10.0 - d *= 10.0 - } - } else { - /* Generate ilim digits, then fix them up. */ - eps *= tens[ilim-1] - for i = 1; ; i++ { - l := int64(d) - d -= float64(l) - buf = append(buf, byte('0'+l)) - if i == ilim { - if d > 0.5+eps { - buf, k = bumpUp(buf, k) - return buf, k + 1 - } else if d < 0.5-eps { - buf = stripTrailingZeroes(buf) - return buf, k + 1 - } - break - } - d *= 10.0 - } - } - } - if fast_failed { - if sign { - buf = buf[:1] - } else { - buf = buf[:0] - } - d = d2 - k = k0 - ilim = ilim0 - } - } - - /* Do we have a "small" integer? */ - if be >= 0 && k <= int_max { - /* Yes. */ - ds = tens[k] - if ndigits < 0 && ilim <= 0 { - if ilim < 0 || d < 5*ds || (!biasUp && d == 5*ds) { - if sign { - buf = buf[:1] - } else { - buf = buf[:0] - } - buf = append(buf, '0') - return buf, 1 - } - buf = append(buf, '1') - k++ - return buf, k + 1 - } - for i = 1; ; i++ { - l := int64(d / ds) - d -= float64(l) * ds - buf = append(buf, byte('0'+l)) - if i == ilim { - d += d - if (d > ds) || (d == ds && (((l & 1) != 0) || biasUp)) { - buf, k = bumpUp(buf, k) - } - break - } - d *= 10.0 - if d == 0 { - break - } - } - return buf, k + 1 - } - - m2 := b2 - m5 := b5 - var mhi, mlo *big.Int - if leftright { - if mode < 2 { - if denorm { - i = be + (bias + (p - 1) - 1 + 1) - } else { - i = 1 + p - bbits - } - /* i is 1 plus the number of trailing zero bits in d's significand. Thus, - (2^m2 * 5^m5) / (2^(s2+i) * 5^s5) = (1/2 lsb of d)/10^k. */ - } else { - j = ilim - 1 - if m5 >= j { - m5 -= j - } else { - j -= m5 - s5 += j - b5 += j - m5 = 0 - } - i = ilim - if i < 0 { - m2 -= i - i = 0 - } - /* (2^m2 * 5^m5) / (2^(s2+i) * 5^s5) = (1/2 * 10^(1-ilim))/10^k. */ - } - b2 += i - s2 += i - mhi = big.NewInt(1) - /* (mhi * 2^m2 * 5^m5) / (2^s2 * 5^s5) = one-half of last printed (when mode >= 2) or - input (when mode < 2) significant digit, divided by 10^k. */ - } - - /* We still have d/10^k = (b * 2^b2 * 5^b5) / (2^s2 * 5^s5). Reduce common factors in - b2, m2, and s2 without changing the equalities. */ - if m2 > 0 && s2 > 0 { - if m2 < s2 { - i = m2 - } else { - i = s2 - } - b2 -= i - m2 -= i - s2 -= i - } - - /* Fold b5 into b and m5 into mhi. */ - if b5 > 0 { - if leftright { - if m5 > 0 { - pow5mult(mhi, m5) - b.Mul(mhi, b) - } - j = b5 - m5 - if j != 0 { - pow5mult(b, j) - } - } else { - pow5mult(b, b5) - } - } - /* Now we have d/10^k = (b * 2^b2) / (2^s2 * 5^s5) and - (mhi * 2^m2) / (2^s2 * 5^s5) = one-half of last printed or input significant digit, divided by 10^k. */ - - S := big.NewInt(1) - if s5 > 0 { - pow5mult(S, s5) - } - /* Now we have d/10^k = (b * 2^b2) / (S * 2^s2) and - (mhi * 2^m2) / (S * 2^s2) = one-half of last printed or input significant digit, divided by 10^k. */ - - /* Check for special case that d is a normalized power of 2. */ - spec_case := false - if mode < 2 { - if (_word1(d) == 0) && ((_word0(d) & bndry_mask) == 0) && - ((_word0(d) & (exp_mask & (exp_mask << 1))) != 0) { - /* The special case. Here we want to be within a quarter of the last input - significant digit instead of one half of it when the decimal output string's value is less than d. */ - b2 += log2P - s2 += log2P - spec_case = true - } - } - - /* Arrange for convenient computation of quotients: - * shift left if necessary so divisor has 4 leading 0 bits. - * - * Perhaps we should just compute leading 28 bits of S once - * and for all and pass them and a shift to quorem, so it - * can do shifts and ors to compute the numerator for q. - */ - S_bytes := S.Bytes() - var S_hiWord uint32 - for idx := 0; idx < 4; idx++ { - S_hiWord = S_hiWord << 8 - if idx < len(S_bytes) { - S_hiWord |= uint32(S_bytes[idx]) - } - } - var zz int - if s5 != 0 { - zz = 32 - hi0bits(S_hiWord) - } else { - zz = 1 - } - i = (zz + s2) & 0x1f - if i != 0 { - i = 32 - i - } - /* i is the number of leading zero bits in the most significant word of S*2^s2. */ - if i > 4 { - i -= 4 - b2 += i - m2 += i - s2 += i - } else if i < 4 { - i += 28 - b2 += i - m2 += i - s2 += i - } - /* Now S*2^s2 has exactly four leading zero bits in its most significant word. */ - if b2 > 0 { - b = b.Lsh(b, uint(b2)) - } - if s2 > 0 { - S.Lsh(S, uint(s2)) - } - /* Now we have d/10^k = b/S and - (mhi * 2^m2) / S = maximum acceptable error, divided by 10^k. */ - if k_check { - if b.Cmp(S) < 0 { - k-- - b.Mul(b, big10) /* we botched the k estimate */ - if leftright { - mhi.Mul(mhi, big10) - } - ilim = ilim1 - } - } - /* At this point 1 <= d/10^k = b/S < 10. */ - - if ilim <= 0 && mode > 2 { - /* We're doing fixed-mode output and d is less than the minimum nonzero output in this mode. - Output either zero or the minimum nonzero output depending on which is closer to d. */ - if ilim >= 0 { - i = b.Cmp(S.Mul(S, big5)) - } - if ilim < 0 || i < 0 || i == 0 && !biasUp { - /* Always emit at least one digit. If the number appears to be zero - using the current mode, then emit one '0' digit and set decpt to 1. */ - if sign { - buf = buf[:1] - } else { - buf = buf[:0] - } - buf = append(buf, '0') - return buf, 1 - } - buf = append(buf, '1') - k++ - return buf, int(k + 1) - } - - var dig byte - if leftright { - if m2 > 0 { - mhi.Lsh(mhi, uint(m2)) - } - - /* Compute mlo -- check for special case - * that d is a normalized power of 2. - */ - - mlo = mhi - if spec_case { - mhi = mlo - mhi = new(big.Int).Lsh(mhi, log2P) - } - /* mlo/S = maximum acceptable error, divided by 10^k, if the output is less than d. */ - /* mhi/S = maximum acceptable error, divided by 10^k, if the output is greater than d. */ - var z, delta big.Int - for i = 1; ; i++ { - z.DivMod(b, S, b) - dig = byte(z.Int64() + '0') - /* Do we yet have the shortest decimal string - * that will round to d? - */ - j = b.Cmp(mlo) - /* j is b/S compared with mlo/S. */ - delta.Sub(S, mhi) - var j1 int - if delta.Sign() <= 0 { - j1 = 1 - } else { - j1 = b.Cmp(&delta) - } - /* j1 is b/S compared with 1 - mhi/S. */ - if (j1 == 0) && (mode == 0) && ((_word1(d) & 1) == 0) { - if dig == '9' { - var flag bool - buf = append(buf, '9') - if buf, flag = roundOff(buf); flag { - k++ - buf = append(buf, '1') - } - return buf, k + 1 - } - if j > 0 { - dig++ - } - buf = append(buf, dig) - return buf, k + 1 - } - if (j < 0) || ((j == 0) && (mode == 0) && ((_word1(d) & 1) == 0)) { - if j1 > 0 { - /* Either dig or dig+1 would work here as the least significant decimal digit. - Use whichever would produce a decimal value closer to d. */ - b.Lsh(b, 1) - j1 = b.Cmp(S) - if (j1 > 0) || (j1 == 0 && (((dig & 1) == 1) || biasUp)) { - dig++ - if dig == '9' { - buf = append(buf, '9') - buf, flag := roundOff(buf) - if flag { - k++ - buf = append(buf, '1') - } - return buf, k + 1 - } - } - } - buf = append(buf, dig) - return buf, k + 1 - } - if j1 > 0 { - if dig == '9' { /* possible if i == 1 */ - buf = append(buf, '9') - buf, flag := roundOff(buf) - if flag { - k++ - buf = append(buf, '1') - } - return buf, k + 1 - } - buf = append(buf, dig+1) - return buf, k + 1 - } - buf = append(buf, dig) - if i == ilim { - break - } - b.Mul(b, big10) - if mlo == mhi { - mhi.Mul(mhi, big10) - } else { - mlo.Mul(mlo, big10) - mhi.Mul(mhi, big10) - } - } - } else { - var z big.Int - for i = 1; ; i++ { - z.DivMod(b, S, b) - dig = byte(z.Int64() + '0') - buf = append(buf, dig) - if i >= ilim { - break - } - - b.Mul(b, big10) - } - } - /* Round off last digit */ - - b.Lsh(b, 1) - j = b.Cmp(S) - if (j > 0) || (j == 0 && (((dig & 1) == 1) || biasUp)) { - var flag bool - buf, flag = roundOff(buf) - if flag { - k++ - buf = append(buf, '1') - return buf, k + 1 - } - } else { - buf = stripTrailingZeroes(buf) - } - - return buf, k + 1 -} - func insert(b []byte, p int, c byte) []byte { b = append(b, 0) copy(b[p+1:], b[p:]) @@ -668,186 +40,108 @@ func expand(b []byte, delta int) []byte { } func FToStr(d float64, mode FToStrMode, precision int, buffer []byte) []byte { + if math.IsNaN(d) { + buffer = append(buffer, "NaN"...) + return buffer + } + if math.IsInf(d, 0) { + if math.Signbit(d) { + buffer = append(buffer, '-') + } + buffer = append(buffer, "Infinity"...) + return buffer + } + if mode == ModeFixed && (d >= 1e21 || d <= -1e21) { mode = ModeStandard } - buffer, decPt := ftoa(d, dtoaModes[mode], mode >= ModeFixed, precision, buffer) - if decPt != 9999 { - exponentialNotation := false - minNDigits := 0 /* Minimum number of significand digits required by mode and precision */ - sign := false - nDigits := len(buffer) - if len(buffer) > 0 && buffer[0] == '-' { - sign = true - nDigits-- - } + var decPt int + var ok bool + startPos := len(buffer) - switch mode { - case ModeStandard: - if decPt < -5 || decPt > 21 { - exponentialNotation = true - } else { - minNDigits = decPt - } - case ModeFixed: - if precision >= 0 { - minNDigits = decPt + precision - } else { - minNDigits = decPt - } - case ModeExponential: - // JS_ASSERT(precision > 0); - minNDigits = precision - fallthrough - case ModeStandardExponential: - exponentialNotation = true - case ModePrecision: - // JS_ASSERT(precision > 0); - minNDigits = precision - if decPt < -5 || decPt > precision { - exponentialNotation = true - } + if d != 0 { // also matches -0 + if d < 0 { + buffer = append(buffer, '-') + d = -d + startPos++ } - - for nDigits < minNDigits { - buffer = append(buffer, '0') - nDigits++ - } - - if exponentialNotation { - /* Insert a decimal point if more than one significand digit */ - if nDigits != 1 { - p := 1 - if sign { - p = 2 - } - buffer = insert(buffer, p, '.') - } - buffer = append(buffer, 'e') - if decPt-1 >= 0 { - buffer = append(buffer, '+') - } - buffer = strconv.AppendInt(buffer, int64(decPt-1), 10) - } else if decPt != nDigits { - /* Some kind of a fraction in fixed notation */ - // JS_ASSERT(decPt <= nDigits); - if decPt > 0 { - /* dd...dd . dd...dd */ - if sign { - buffer = insert(buffer, decPt+1, '.') - } else { - buffer = insert(buffer, decPt, '.') - } - } else { - /* 0 . 00...00dd...dd */ - o := 0 - if sign { - o = 1 - } - buffer = expand(buffer, 2-decPt) - copy(buffer[o+2-decPt:], buffer[o:]) - buffer[o] = '0' - buffer[o+1] = '.' - for i := o + 2; i < o+2-decPt; i++ { - buffer[i] = '0' - } - } + switch mode { + case ModeStandard, ModeStandardExponential: + buffer, decPt, ok = fast.Dtoa(d, fast.ModeShortest, 0, buffer) + case ModeExponential, ModePrecision: + buffer, decPt, ok = fast.Dtoa(d, fast.ModePrecision, precision, buffer) } + } else { + buffer = append(buffer, '0') + decPt, ok = 1, true } - return buffer -} - -func bumpUp(buf []byte, k int) ([]byte, int) { - var lastCh byte - stop := 0 - if len(buf) > 0 && buf[0] == '-' { - stop = 1 + if !ok { + buffer, decPt = ftoa(d, dtoaModes[mode], mode >= ModeFixed, precision, buffer) } - for { - lastCh = buf[len(buf)-1] - buf = buf[:len(buf)-1] - if lastCh != '9' { - break + exponentialNotation := false + minNDigits := 0 /* Minimum number of significand digits required by mode and precision */ + nDigits := len(buffer) - startPos + + switch mode { + case ModeStandard: + if decPt < -5 || decPt > 21 { + exponentialNotation = true + } else { + minNDigits = decPt } - if len(buf) == stop { - k++ - lastCh = '0' - break + case ModeFixed: + if precision >= 0 { + minNDigits = decPt + precision + } else { + minNDigits = decPt + } + case ModeExponential: + // JS_ASSERT(precision > 0); + minNDigits = precision + fallthrough + case ModeStandardExponential: + exponentialNotation = true + case ModePrecision: + // JS_ASSERT(precision > 0); + minNDigits = precision + if decPt < -5 || decPt > precision { + exponentialNotation = true } } - buf = append(buf, lastCh+1) - return buf, k -} -func setWord0(d float64, w uint32) float64 { - dBits := math.Float64bits(d) - return math.Float64frombits(uint64(w)<<32 | dBits&0xffffffff) -} - -func _word0(d float64) uint32 { - dBits := math.Float64bits(d) - return uint32(dBits >> 32) -} - -func _word1(d float64) uint32 { - dBits := math.Float64bits(d) - return uint32(dBits) -} - -func stripTrailingZeroes(buf []byte) []byte { - bl := len(buf) - 1 - for bl >= 0 && buf[bl] == '0' { - bl-- + for nDigits < minNDigits { + buffer = append(buffer, '0') + nDigits++ } - return buf[:bl+1] -} -/* Set b = b * 5^k. k must be nonnegative. */ -func pow5mult(b *big.Int, k int) *big.Int { - if k < (1 << (len(pow5Cache) + 2)) { - i := k & 3 - if i != 0 { - b.Mul(b, p05[i-1]) + if exponentialNotation { + /* Insert a decimal point if more than one significand digit */ + if nDigits != 1 { + buffer = insert(buffer, startPos+1, '.') } - k >>= 2 - i = 0 - for { - if k&1 != 0 { - b.Mul(b, pow5Cache[i]) - } - k >>= 1 - if k == 0 { - break - } - i++ + buffer = append(buffer, 'e') + if decPt-1 >= 0 { + buffer = append(buffer, '+') } - return b - } - return b.Mul(b, new(big.Int).Exp(big5, big.NewInt(int64(k)), nil)) -} - -func roundOff(buf []byte) ([]byte, bool) { - i := len(buf) - stop := 0 - if i > 0 && buf[0] == '-' { - stop = 1 - } - for i != stop { - i-- - if buf[i] != '9' { - buf[i]++ - return buf[:i+1], false + buffer = strconv.AppendInt(buffer, int64(decPt-1), 10) + } else if decPt != nDigits { + /* Some kind of a fraction in fixed notation */ + // JS_ASSERT(decPt <= nDigits); + if decPt > 0 { + /* dd...dd . dd...dd */ + buffer = insert(buffer, startPos+decPt, '.') + } else { + /* 0 . 00...00dd...dd */ + buffer = expand(buffer, 2-decPt) + copy(buffer[startPos+2-decPt:], buffer[startPos:]) + buffer[startPos] = '0' + buffer[startPos+1] = '.' + for i := startPos + 2; i < startPos+2-decPt; i++ { + buffer[i] = '0' + } } } - return buf[:stop], true -} -func init() { - p := big.NewInt(625) - pow5Cache[0] = p - for i := 1; i < len(pow5Cache); i++ { - p = new(big.Int).Mul(p, p) - pow5Cache[i] = p - } + return buffer } diff --git a/ftoa/ftostr_test.go b/ftoa/ftostr_test.go index e645b598..db2441f8 100644 --- a/ftoa/ftostr_test.go +++ b/ftoa/ftostr_test.go @@ -33,16 +33,37 @@ func TestDtostr(t *testing.T) { testFToStr(885, ModeExponential, 2, "8.9e+2", t) testFToStr(25, ModeExponential, 1, "3e+1", t) testFToStr(1e-6, ModeFixed, 7, "0.0000010", t) + testFToStr(math.Pi, ModeStandardExponential, 0, "3.141592653589793e+0", t) testFToStr(math.Inf(1), ModeStandard, 0, "Infinity", t) testFToStr(math.NaN(), ModeStandard, 0, "NaN", t) testFToStr(math.SmallestNonzeroFloat64, ModeExponential, 40, "4.940656458412465441765687928682213723651e-324", t) + testFToStr(3.5844466002796428e+298, ModeStandard, 0, "3.5844466002796428e+298", t) + testFToStr(math.Float64frombits(0x0010000000000000), ModeStandard, 0, "2.2250738585072014e-308", t) // smallest normal + testFToStr(math.Float64frombits(0x000FFFFFFFFFFFFF), ModeStandard, 0, "2.225073858507201e-308", t) // largest denormal + testFToStr(4294967272.0, ModePrecision, 14, "4294967272.0000", t) } func BenchmarkDtostrSmall(b *testing.B) { var buf [128]byte b.ReportAllocs() for i := 0; i < b.N; i++ { - FToStr(math.Pi, ModeExponential, 0, buf[:0]) + FToStr(math.Pi, ModeStandardExponential, 0, buf[:0]) + } +} + +func BenchmarkDtostrShort(b *testing.B) { + var buf [128]byte + b.ReportAllocs() + for i := 0; i < b.N; i++ { + FToStr(3.1415, ModeStandard, 0, buf[:0]) + } +} + +func BenchmarkDtostrFixed(b *testing.B) { + var buf [128]byte + b.ReportAllocs() + for i := 0; i < b.N; i++ { + FToStr(math.Pi, ModeFixed, 4, buf[:0]) } } diff --git a/ftoa/internal/fast/LICENSE_V8 b/ftoa/internal/fast/LICENSE_V8 new file mode 100644 index 00000000..bbad2662 --- /dev/null +++ b/ftoa/internal/fast/LICENSE_V8 @@ -0,0 +1,26 @@ +Copyright 2014, 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. diff --git a/ftoa/internal/fast/cachedpower.go b/ftoa/internal/fast/cachedpower.go new file mode 100644 index 00000000..4f7e49fc --- /dev/null +++ b/ftoa/internal/fast/cachedpower.go @@ -0,0 +1,120 @@ +package fast + +import "math" + +const ( + kCachedPowersOffset = 348 // -1 * the first decimal_exponent. + kD_1_LOG2_10 = 0.30102999566398114 // 1 / lg(10) + kDecimalExponentDistance = 8 +) + +type cachedPower struct { + significand uint64 + binary_exponent int16 + decimal_exponent int16 +} + +var ( + cachedPowers = [...]cachedPower{ + {0xFA8FD5A0081C0288, -1220, -348}, + {0xBAAEE17FA23EBF76, -1193, -340}, + {0x8B16FB203055AC76, -1166, -332}, + {0xCF42894A5DCE35EA, -1140, -324}, + {0x9A6BB0AA55653B2D, -1113, -316}, + {0xE61ACF033D1A45DF, -1087, -308}, + {0xAB70FE17C79AC6CA, -1060, -300}, + {0xFF77B1FCBEBCDC4F, -1034, -292}, + {0xBE5691EF416BD60C, -1007, -284}, + {0x8DD01FAD907FFC3C, -980, -276}, + {0xD3515C2831559A83, -954, -268}, + {0x9D71AC8FADA6C9B5, -927, -260}, + {0xEA9C227723EE8BCB, -901, -252}, + {0xAECC49914078536D, -874, -244}, + {0x823C12795DB6CE57, -847, -236}, + {0xC21094364DFB5637, -821, -228}, + {0x9096EA6F3848984F, -794, -220}, + {0xD77485CB25823AC7, -768, -212}, + {0xA086CFCD97BF97F4, -741, -204}, + {0xEF340A98172AACE5, -715, -196}, + {0xB23867FB2A35B28E, -688, -188}, + {0x84C8D4DFD2C63F3B, -661, -180}, + {0xC5DD44271AD3CDBA, -635, -172}, + {0x936B9FCEBB25C996, -608, -164}, + {0xDBAC6C247D62A584, -582, -156}, + {0xA3AB66580D5FDAF6, -555, -148}, + {0xF3E2F893DEC3F126, -529, -140}, + {0xB5B5ADA8AAFF80B8, -502, -132}, + {0x87625F056C7C4A8B, -475, -124}, + {0xC9BCFF6034C13053, -449, -116}, + {0x964E858C91BA2655, -422, -108}, + {0xDFF9772470297EBD, -396, -100}, + {0xA6DFBD9FB8E5B88F, -369, -92}, + {0xF8A95FCF88747D94, -343, -84}, + {0xB94470938FA89BCF, -316, -76}, + {0x8A08F0F8BF0F156B, -289, -68}, + {0xCDB02555653131B6, -263, -60}, + {0x993FE2C6D07B7FAC, -236, -52}, + {0xE45C10C42A2B3B06, -210, -44}, + {0xAA242499697392D3, -183, -36}, + {0xFD87B5F28300CA0E, -157, -28}, + {0xBCE5086492111AEB, -130, -20}, + {0x8CBCCC096F5088CC, -103, -12}, + {0xD1B71758E219652C, -77, -4}, + {0x9C40000000000000, -50, 4}, + {0xE8D4A51000000000, -24, 12}, + {0xAD78EBC5AC620000, 3, 20}, + {0x813F3978F8940984, 30, 28}, + {0xC097CE7BC90715B3, 56, 36}, + {0x8F7E32CE7BEA5C70, 83, 44}, + {0xD5D238A4ABE98068, 109, 52}, + {0x9F4F2726179A2245, 136, 60}, + {0xED63A231D4C4FB27, 162, 68}, + {0xB0DE65388CC8ADA8, 189, 76}, + {0x83C7088E1AAB65DB, 216, 84}, + {0xC45D1DF942711D9A, 242, 92}, + {0x924D692CA61BE758, 269, 100}, + {0xDA01EE641A708DEA, 295, 108}, + {0xA26DA3999AEF774A, 322, 116}, + {0xF209787BB47D6B85, 348, 124}, + {0xB454E4A179DD1877, 375, 132}, + {0x865B86925B9BC5C2, 402, 140}, + {0xC83553C5C8965D3D, 428, 148}, + {0x952AB45CFA97A0B3, 455, 156}, + {0xDE469FBD99A05FE3, 481, 164}, + {0xA59BC234DB398C25, 508, 172}, + {0xF6C69A72A3989F5C, 534, 180}, + {0xB7DCBF5354E9BECE, 561, 188}, + {0x88FCF317F22241E2, 588, 196}, + {0xCC20CE9BD35C78A5, 614, 204}, + {0x98165AF37B2153DF, 641, 212}, + {0xE2A0B5DC971F303A, 667, 220}, + {0xA8D9D1535CE3B396, 694, 228}, + {0xFB9B7CD9A4A7443C, 720, 236}, + {0xBB764C4CA7A44410, 747, 244}, + {0x8BAB8EEFB6409C1A, 774, 252}, + {0xD01FEF10A657842C, 800, 260}, + {0x9B10A4E5E9913129, 827, 268}, + {0xE7109BFBA19C0C9D, 853, 276}, + {0xAC2820D9623BF429, 880, 284}, + {0x80444B5E7AA7CF85, 907, 292}, + {0xBF21E44003ACDD2D, 933, 300}, + {0x8E679C2F5E44FF8F, 960, 308}, + {0xD433179D9C8CB841, 986, 316}, + {0x9E19DB92B4E31BA9, 1013, 324}, + {0xEB96BF6EBADF77D9, 1039, 332}, + {0xAF87023B9BF0EE6B, 1066, 340}, + } +) + +func getCachedPowerForBinaryExponentRange(min_exponent, max_exponent int) (power diyfp, decimal_exponent int) { + kQ := diyFpKSignificandSize + k := int(math.Ceil(float64(min_exponent+kQ-1) * kD_1_LOG2_10)) + index := (kCachedPowersOffset+k-1)/kDecimalExponentDistance + 1 + cached_power := cachedPowers[index] + _DCHECK(min_exponent <= int(cached_power.binary_exponent)) + _DCHECK(int(cached_power.binary_exponent) <= max_exponent) + decimal_exponent = int(cached_power.decimal_exponent) + power = diyfp{f: cached_power.significand, e: int(cached_power.binary_exponent)} + + return +} diff --git a/ftoa/internal/fast/common.go b/ftoa/internal/fast/common.go new file mode 100644 index 00000000..6ffaaf92 --- /dev/null +++ b/ftoa/internal/fast/common.go @@ -0,0 +1,18 @@ +/* +Package fast contains code ported from V8 (https://github.com/v8/v8/blob/master/src/numbers/fast-dtoa.cc) + +See LICENSE_V8 for the original copyright message and disclaimer. +*/ +package fast + +import "errors" + +var ( + dcheckFailure = errors.New("DCHECK assertion failed") +) + +func _DCHECK(f bool) { + if !f { + panic(dcheckFailure) + } +} diff --git a/ftoa/internal/fast/diyfp.go b/ftoa/internal/fast/diyfp.go new file mode 100644 index 00000000..727a7472 --- /dev/null +++ b/ftoa/internal/fast/diyfp.go @@ -0,0 +1,152 @@ +package fast + +import "math" + +const ( + diyFpKSignificandSize = 64 + kSignificandSize = 53 + kUint64MSB uint64 = 1 << 63 + + kSignificandMask = 0x000FFFFFFFFFFFFF + kHiddenBit = 0x0010000000000000 + kExponentMask = 0x7FF0000000000000 + + kPhysicalSignificandSize = 52 // Excludes the hidden bit. + kExponentBias = 0x3FF + kPhysicalSignificandSize + kDenormalExponent = -kExponentBias + 1 +) + +type double float64 + +type diyfp struct { + f uint64 + e int +} + +// f =- o. +// The exponents of both numbers must be the same and the significand of this +// must be bigger than the significand of other. +// The result will not be normalized. +func (f *diyfp) subtract(o diyfp) { + _DCHECK(f.e == o.e) + _DCHECK(f.f >= o.f) + f.f -= o.f +} + +// Returns f - o +// The exponents of both numbers must be the same and this must be bigger +// than other. The result will not be normalized. +func (f diyfp) minus(o diyfp) diyfp { + res := f + res.subtract(o) + return res +} + +// f *= o +func (f *diyfp) mul(o diyfp) { + // Simply "emulates" a 128 bit multiplication. + // However: the resulting number only contains 64 bits. The least + // significant 64 bits are only used for rounding the most significant 64 + // bits. + const kM32 uint64 = 0xFFFFFFFF + a := f.f >> 32 + b := f.f & kM32 + c := o.f >> 32 + d := o.f & kM32 + ac := a * c + bc := b * c + ad := a * d + bd := b * d + tmp := (bd >> 32) + (ad & kM32) + (bc & kM32) + // By adding 1U << 31 to tmp we round the final result. + // Halfway cases will be round up. + tmp += 1 << 31 + result_f := ac + (ad >> 32) + (bc >> 32) + (tmp >> 32) + f.e += o.e + 64 + f.f = result_f +} + +// Returns f * o +func (f diyfp) times(o diyfp) diyfp { + res := f + res.mul(o) + return res +} + +func (f *diyfp) _normalize() { + f_, e := f.f, f.e + // This method is mainly called for normalizing boundaries. In general + // boundaries need to be shifted by 10 bits. We thus optimize for this case. + const k10MSBits uint64 = 0x3FF << 54 + for f_&k10MSBits == 0 { + f_ <<= 10 + e -= 10 + } + for f_&kUint64MSB == 0 { + f_ <<= 1 + e-- + } + f.f, f.e = f_, e +} + +func normalizeDiyfp(f diyfp) diyfp { + res := f + res._normalize() + return res +} + +// f must be strictly greater than 0. +func (d double) toNormalizedDiyfp() diyfp { + f, e := d.sigExp() + + // The current float could be a denormal. + for (f & kHiddenBit) == 0 { + f <<= 1 + e-- + } + // Do the final shifts in one go. + f <<= diyFpKSignificandSize - kSignificandSize + e -= diyFpKSignificandSize - kSignificandSize + return diyfp{f, e} +} + +// Returns the two boundaries of this. +// The bigger boundary (m_plus) is normalized. The lower boundary has the same +// exponent as m_plus. +// Precondition: the value encoded by this Double must be greater than 0. +func (d double) normalizedBoundaries() (m_minus, m_plus diyfp) { + v := d.toDiyFp() + significand_is_zero := v.f == kHiddenBit + m_plus = normalizeDiyfp(diyfp{f: (v.f << 1) + 1, e: v.e - 1}) + if significand_is_zero && v.e != kDenormalExponent { + // The boundary is closer. Think of v = 1000e10 and v- = 9999e9. + // Then the boundary (== (v - v-)/2) is not just at a distance of 1e9 but + // at a distance of 1e8. + // The only exception is for the smallest normal: the largest denormal is + // at the same distance as its successor. + // Note: denormals have the same exponent as the smallest normals. + m_minus = diyfp{f: (v.f << 2) - 1, e: v.e - 2} + } else { + m_minus = diyfp{f: (v.f << 1) - 1, e: v.e - 1} + } + m_minus.f <<= m_minus.e - m_plus.e + m_minus.e = m_plus.e + return +} + +func (d double) toDiyFp() diyfp { + f, e := d.sigExp() + return diyfp{f: f, e: e} +} + +func (d double) sigExp() (significand uint64, exponent int) { + d64 := math.Float64bits(float64(d)) + significand = d64 & kSignificandMask + if d64&kExponentMask != 0 { // not denormal + significand += kHiddenBit + exponent = int((d64&kExponentMask)>>kPhysicalSignificandSize) - kExponentBias + } else { + exponent = kDenormalExponent + } + return +} diff --git a/ftoa/internal/fast/dtoa.go b/ftoa/internal/fast/dtoa.go new file mode 100644 index 00000000..a82e31e9 --- /dev/null +++ b/ftoa/internal/fast/dtoa.go @@ -0,0 +1,624 @@ +package fast + +import ( + "fmt" + "strconv" +) + +const ( + kMinimalTargetExponent = -60 + kMaximalTargetExponent = -32 + + kTen4 = 10000 + kTen5 = 100000 + kTen6 = 1000000 + kTen7 = 10000000 + kTen8 = 100000000 + kTen9 = 1000000000 +) + +type Mode int + +const ( + ModeShortest Mode = iota + ModePrecision +) + +// Adjusts the last digit of the generated number, and screens out generated +// solutions that may be inaccurate. A solution may be inaccurate if it is +// outside the safe interval, or if we cannot prove that it is closer to the +// input than a neighboring representation of the same length. +// +// Input: * buffer containing the digits of too_high / 10^kappa +// * distance_too_high_w == (too_high - w).f() * unit +// * unsafe_interval == (too_high - too_low).f() * unit +// * rest = (too_high - buffer * 10^kappa).f() * unit +// * ten_kappa = 10^kappa * unit +// * unit = the common multiplier +// Output: returns true if the buffer is guaranteed to contain the closest +// representable number to the input. +// Modifies the generated digits in the buffer to approach (round towards) w. +func roundWeed(buffer []byte, distance_too_high_w, unsafe_interval, rest, ten_kappa, unit uint64) bool { + small_distance := distance_too_high_w - unit + big_distance := distance_too_high_w + unit + + // Let w_low = too_high - big_distance, and + // w_high = too_high - small_distance. + // Note: w_low < w < w_high + // + // The real w (* unit) must lie somewhere inside the interval + // ]w_low; w_high[ (often written as "(w_low; w_high)") + + // Basically the buffer currently contains a number in the unsafe interval + // ]too_low; too_high[ with too_low < w < too_high + // + // too_high - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + // ^v 1 unit ^ ^ ^ ^ + // boundary_high --------------------- . . . . + // ^v 1 unit . . . . + // - - - - - - - - - - - - - - - - - - - + - - + - - - - - - . . + // . . ^ . . + // . big_distance . . . + // . . . . rest + // small_distance . . . . + // v . . . . + // w_high - - - - - - - - - - - - - - - - - - . . . . + // ^v 1 unit . . . . + // w ---------------------------------------- . . . . + // ^v 1 unit v . . . + // w_low - - - - - - - - - - - - - - - - - - - - - . . . + // . . v + // buffer --------------------------------------------------+-------+-------- + // . . + // safe_interval . + // v . + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - . + // ^v 1 unit . + // boundary_low ------------------------- unsafe_interval + // ^v 1 unit v + // too_low - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + // + // + // Note that the value of buffer could lie anywhere inside the range too_low + // to too_high. + // + // boundary_low, boundary_high and w are approximations of the real boundaries + // and v (the input number). They are guaranteed to be precise up to one unit. + // In fact the error is guaranteed to be strictly less than one unit. + // + // Anything that lies outside the unsafe interval is guaranteed not to round + // to v when read again. + // Anything that lies inside the safe interval is guaranteed to round to v + // when read again. + // If the number inside the buffer lies inside the unsafe interval but not + // inside the safe interval then we simply do not know and bail out (returning + // false). + // + // Similarly we have to take into account the imprecision of 'w' when finding + // the closest representation of 'w'. If we have two potential + // representations, and one is closer to both w_low and w_high, then we know + // it is closer to the actual value v. + // + // By generating the digits of too_high we got the largest (closest to + // too_high) buffer that is still in the unsafe interval. In the case where + // w_high < buffer < too_high we try to decrement the buffer. + // This way the buffer approaches (rounds towards) w. + // There are 3 conditions that stop the decrementation process: + // 1) the buffer is already below w_high + // 2) decrementing the buffer would make it leave the unsafe interval + // 3) decrementing the buffer would yield a number below w_high and farther + // away than the current number. In other words: + // (buffer{-1} < w_high) && w_high - buffer{-1} > buffer - w_high + // Instead of using the buffer directly we use its distance to too_high. + // Conceptually rest ~= too_high - buffer + // We need to do the following tests in this order to avoid over- and + // underflows. + _DCHECK(rest <= unsafe_interval) + for rest < small_distance && // Negated condition 1 + unsafe_interval-rest >= ten_kappa && // Negated condition 2 + (rest+ten_kappa < small_distance || // buffer{-1} > w_high + small_distance-rest >= rest+ten_kappa-small_distance) { + buffer[len(buffer)-1]-- + rest += ten_kappa + } + + // We have approached w+ as much as possible. We now test if approaching w- + // would require changing the buffer. If yes, then we have two possible + // representations close to w, but we cannot decide which one is closer. + if rest < big_distance && unsafe_interval-rest >= ten_kappa && + (rest+ten_kappa < big_distance || + big_distance-rest > rest+ten_kappa-big_distance) { + return false + } + + // Weeding test. + // The safe interval is [too_low + 2 ulp; too_high - 2 ulp] + // Since too_low = too_high - unsafe_interval this is equivalent to + // [too_high - unsafe_interval + 4 ulp; too_high - 2 ulp] + // Conceptually we have: rest ~= too_high - buffer + return (2*unit <= rest) && (rest <= unsafe_interval-4*unit) +} + +// Rounds the buffer upwards if the result is closer to v by possibly adding +// 1 to the buffer. If the precision of the calculation is not sufficient to +// round correctly, return false. +// The rounding might shift the whole buffer in which case the kappa is +// adjusted. For example "99", kappa = 3 might become "10", kappa = 4. +// +// If 2*rest > ten_kappa then the buffer needs to be round up. +// rest can have an error of +/- 1 unit. This function accounts for the +// imprecision and returns false, if the rounding direction cannot be +// unambiguously determined. +// +// Precondition: rest < ten_kappa. +func roundWeedCounted(buffer []byte, rest, ten_kappa, unit uint64, kappa *int) bool { + _DCHECK(rest < ten_kappa) + // The following tests are done in a specific order to avoid overflows. They + // will work correctly with any uint64 values of rest < ten_kappa and unit. + // + // If the unit is too big, then we don't know which way to round. For example + // a unit of 50 means that the real number lies within rest +/- 50. If + // 10^kappa == 40 then there is no way to tell which way to round. + if unit >= ten_kappa { + return false + } + // Even if unit is just half the size of 10^kappa we are already completely + // lost. (And after the previous test we know that the expression will not + // over/underflow.) + if ten_kappa-unit <= unit { + return false + } + // If 2 * (rest + unit) <= 10^kappa we can safely round down. + if (ten_kappa-rest > rest) && (ten_kappa-2*rest >= 2*unit) { + return true + } + + // If 2 * (rest - unit) >= 10^kappa, then we can safely round up. + if (rest > unit) && (ten_kappa-(rest-unit) <= (rest - unit)) { + // Increment the last digit recursively until we find a non '9' digit. + buffer[len(buffer)-1]++ + for i := len(buffer) - 1; i > 0; i-- { + if buffer[i] != '0'+10 { + break + } + buffer[i] = '0' + buffer[i-1]++ + } + // If the first digit is now '0'+ 10 we had a buffer with all '9's. With the + // exception of the first digit all digits are now '0'. Simply switch the + // first digit to '1' and adjust the kappa. Example: "99" becomes "10" and + // the power (the kappa) is increased. + if buffer[0] == '0'+10 { + buffer[0] = '1' + *kappa += 1 + } + return true + } + return false +} + +// Returns the biggest power of ten that is less than or equal than the given +// number. We furthermore receive the maximum number of bits 'number' has. +// If number_bits == 0 then 0^-1 is returned +// The number of bits must be <= 32. +// Precondition: number < (1 << (number_bits + 1)). +func biggestPowerTen(number uint32, number_bits int) (power uint32, exponent int) { + switch number_bits { + case 32, 31, 30: + if kTen9 <= number { + power = kTen9 + exponent = 9 + break + } + fallthrough + case 29, 28, 27: + if kTen8 <= number { + power = kTen8 + exponent = 8 + break + } + fallthrough + case 26, 25, 24: + if kTen7 <= number { + power = kTen7 + exponent = 7 + break + } + fallthrough + case 23, 22, 21, 20: + if kTen6 <= number { + power = kTen6 + exponent = 6 + break + } + fallthrough + case 19, 18, 17: + if kTen5 <= number { + power = kTen5 + exponent = 5 + break + } + fallthrough + case 16, 15, 14: + if kTen4 <= number { + power = kTen4 + exponent = 4 + break + } + fallthrough + case 13, 12, 11, 10: + if 1000 <= number { + power = 1000 + exponent = 3 + break + } + fallthrough + case 9, 8, 7: + if 100 <= number { + power = 100 + exponent = 2 + break + } + fallthrough + case 6, 5, 4: + if 10 <= number { + power = 10 + exponent = 1 + break + } + fallthrough + case 3, 2, 1: + if 1 <= number { + power = 1 + exponent = 0 + break + } + fallthrough + case 0: + power = 0 + exponent = -1 + } + return +} + +// Generates the digits of input number w. +// w is a floating-point number (DiyFp), consisting of a significand and an +// exponent. Its exponent is bounded by kMinimalTargetExponent and +// kMaximalTargetExponent. +// Hence -60 <= w.e() <= -32. +// +// Returns false if it fails, in which case the generated digits in the buffer +// should not be used. +// Preconditions: +// * low, w and high are correct up to 1 ulp (unit in the last place). That +// is, their error must be less than a unit of their last digits. +// * low.e() == w.e() == high.e() +// * low < w < high, and taking into account their error: low~ <= high~ +// * kMinimalTargetExponent <= w.e() <= kMaximalTargetExponent +// Postconditions: returns false if procedure fails. +// otherwise: +// * buffer is not null-terminated, but len contains the number of digits. +// * buffer contains the shortest possible decimal digit-sequence +// such that LOW < buffer * 10^kappa < HIGH, where LOW and HIGH are the +// correct values of low and high (without their error). +// * if more than one decimal representation gives the minimal number of +// decimal digits then the one closest to W (where W is the correct value +// of w) is chosen. +// Remark: this procedure takes into account the imprecision of its input +// numbers. If the precision is not enough to guarantee all the postconditions +// then false is returned. This usually happens rarely (~0.5%). +// +// Say, for the sake of example, that +// w.e() == -48, and w.f() == 0x1234567890ABCDEF +// w's value can be computed by w.f() * 2^w.e() +// We can obtain w's integral digits by simply shifting w.f() by -w.e(). +// -> w's integral part is 0x1234 +// w's fractional part is therefore 0x567890ABCDEF. +// Printing w's integral part is easy (simply print 0x1234 in decimal). +// In order to print its fraction we repeatedly multiply the fraction by 10 and +// get each digit. Example the first digit after the point would be computed by +// (0x567890ABCDEF * 10) >> 48. -> 3 +// The whole thing becomes slightly more complicated because we want to stop +// once we have enough digits. That is, once the digits inside the buffer +// represent 'w' we can stop. Everything inside the interval low - high +// represents w. However we have to pay attention to low, high and w's +// imprecision. +func digitGen(low, w, high diyfp, buffer []byte) (kappa int, buf []byte, res bool) { + _DCHECK(low.e == w.e && w.e == high.e) + _DCHECK(low.f+1 <= high.f-1) + _DCHECK(kMinimalTargetExponent <= w.e && w.e <= kMaximalTargetExponent) + // low, w and high are imprecise, but by less than one ulp (unit in the last + // place). + // If we remove (resp. add) 1 ulp from low (resp. high) we are certain that + // the new numbers are outside of the interval we want the final + // representation to lie in. + // Inversely adding (resp. removing) 1 ulp from low (resp. high) would yield + // numbers that are certain to lie in the interval. We will use this fact + // later on. + // We will now start by generating the digits within the uncertain + // interval. Later we will weed out representations that lie outside the safe + // interval and thus _might_ lie outside the correct interval. + unit := uint64(1) + too_low := diyfp{f: low.f - unit, e: low.e} + too_high := diyfp{f: high.f + unit, e: high.e} + // too_low and too_high are guaranteed to lie outside the interval we want the + // generated number in. + unsafe_interval := too_high.minus(too_low) + // We now cut the input number into two parts: the integral digits and the + // fractionals. We will not write any decimal separator though, but adapt + // kappa instead. + // Reminder: we are currently computing the digits (stored inside the buffer) + // such that: too_low < buffer * 10^kappa < too_high + // We use too_high for the digit_generation and stop as soon as possible. + // If we stop early we effectively round down. + one := diyfp{f: 1 << -w.e, e: w.e} + // Division by one is a shift. + integrals := uint32(too_high.f >> -one.e) + // Modulo by one is an and. + fractionals := too_high.f & (one.f - 1) + divisor, divisor_exponent := biggestPowerTen(integrals, diyFpKSignificandSize-(-one.e)) + kappa = divisor_exponent + 1 + buf = buffer + for kappa > 0 { + digit := int(integrals / divisor) + buf = append(buf, byte('0'+digit)) + integrals %= divisor + kappa-- + // Note that kappa now equals the exponent of the divisor and that the + // invariant thus holds again. + rest := uint64(integrals)<<-one.e + fractionals + // Invariant: too_high = buffer * 10^kappa + DiyFp(rest, one.e) + // Reminder: unsafe_interval.e == one.e + if rest < unsafe_interval.f { + // Rounding down (by not emitting the remaining digits) yields a number + // that lies within the unsafe interval. + res = roundWeed(buf, too_high.minus(w).f, + unsafe_interval.f, rest, + uint64(divisor)<<-one.e, unit) + return + } + divisor /= 10 + } + // The integrals have been generated. We are at the point of the decimal + // separator. In the following loop we simply multiply the remaining digits by + // 10 and divide by one. We just need to pay attention to multiply associated + // data (like the interval or 'unit'), too. + // Note that the multiplication by 10 does not overflow, because w.e >= -60 + // and thus one.e >= -60. + _DCHECK(one.e >= -60) + _DCHECK(fractionals < one.f) + _DCHECK(0xFFFFFFFFFFFFFFFF/10 >= one.f) + for { + fractionals *= 10 + unit *= 10 + unsafe_interval.f *= 10 + // Integer division by one. + digit := byte(fractionals >> -one.e) + buf = append(buf, '0'+digit) + fractionals &= one.f - 1 // Modulo by one. + kappa-- + if fractionals < unsafe_interval.f { + res = roundWeed(buf, too_high.minus(w).f*unit, unsafe_interval.f, fractionals, one.f, unit) + return + } + } +} + +// Generates (at most) requested_digits of input number w. +// w is a floating-point number (DiyFp), consisting of a significand and an +// exponent. Its exponent is bounded by kMinimalTargetExponent and +// kMaximalTargetExponent. +// Hence -60 <= w.e() <= -32. +// +// Returns false if it fails, in which case the generated digits in the buffer +// should not be used. +// Preconditions: +// * w is correct up to 1 ulp (unit in the last place). That +// is, its error must be strictly less than a unit of its last digit. +// * kMinimalTargetExponent <= w.e() <= kMaximalTargetExponent +// +// Postconditions: returns false if procedure fails. +// otherwise: +// * buffer is not null-terminated, but length contains the number of +// digits. +// * the representation in buffer is the most precise representation of +// requested_digits digits. +// * buffer contains at most requested_digits digits of w. If there are less +// than requested_digits digits then some trailing '0's have been removed. +// * kappa is such that +// w = buffer * 10^kappa + eps with |eps| < 10^kappa / 2. +// +// Remark: This procedure takes into account the imprecision of its input +// numbers. If the precision is not enough to guarantee all the postconditions +// then false is returned. This usually happens rarely, but the failure-rate +// increases with higher requested_digits. +func digitGenCounted(w diyfp, requested_digits int, buffer []byte) (kappa int, buf []byte, res bool) { + _DCHECK(kMinimalTargetExponent <= w.e && w.e <= kMaximalTargetExponent) + + // w is assumed to have an error less than 1 unit. Whenever w is scaled we + // also scale its error. + w_error := uint64(1) + // We cut the input number into two parts: the integral digits and the + // fractional digits. We don't emit any decimal separator, but adapt kappa + // instead. Example: instead of writing "1.2" we put "12" into the buffer and + // increase kappa by 1. + one := diyfp{f: 1 << -w.e, e: w.e} + // Division by one is a shift. + integrals := uint32(w.f >> -one.e) + // Modulo by one is an and. + fractionals := w.f & (one.f - 1) + divisor, divisor_exponent := biggestPowerTen(integrals, diyFpKSignificandSize-(-one.e)) + kappa = divisor_exponent + 1 + buf = buffer + // Loop invariant: buffer = w / 10^kappa (integer division) + // The invariant holds for the first iteration: kappa has been initialized + // with the divisor exponent + 1. And the divisor is the biggest power of ten + // that is smaller than 'integrals'. + for kappa > 0 { + digit := byte(integrals / divisor) + buf = append(buf, '0'+digit) + requested_digits-- + integrals %= divisor + kappa-- + // Note that kappa now equals the exponent of the divisor and that the + // invariant thus holds again. + if requested_digits == 0 { + break + } + divisor /= 10 + } + + if requested_digits == 0 { + rest := uint64(integrals)<<-one.e + fractionals + res = roundWeedCounted(buf, rest, uint64(divisor)<<-one.e, w_error, &kappa) + return + } + + // The integrals have been generated. We are at the point of the decimal + // separator. In the following loop we simply multiply the remaining digits by + // 10 and divide by one. We just need to pay attention to multiply associated + // data (the 'unit'), too. + // Note that the multiplication by 10 does not overflow, because w.e >= -60 + // and thus one.e >= -60. + _DCHECK(one.e >= -60) + _DCHECK(fractionals < one.f) + _DCHECK(0xFFFFFFFFFFFFFFFF/10 >= one.f) + for requested_digits > 0 && fractionals > w_error { + fractionals *= 10 + w_error *= 10 + // Integer division by one. + digit := byte(fractionals >> -one.e) + buf = append(buf, '0'+digit) + requested_digits-- + fractionals &= one.f - 1 // Modulo by one. + kappa-- + } + if requested_digits != 0 { + res = false + } else { + res = roundWeedCounted(buf, fractionals, one.f, w_error, &kappa) + } + return +} + +// Provides a decimal representation of v. +// Returns true if it succeeds, otherwise the result cannot be trusted. +// There will be *length digits inside the buffer (not null-terminated). +// If the function returns true then +// v == (double) (buffer * 10^decimal_exponent). +// The digits in the buffer are the shortest representation possible: no +// 0.09999999999999999 instead of 0.1. The shorter representation will even be +// chosen even if the longer one would be closer to v. +// The last digit will be closest to the actual v. That is, even if several +// digits might correctly yield 'v' when read again, the closest will be +// computed. +func grisu3(f float64, buffer []byte) (digits []byte, decimal_exponent int, result bool) { + v := double(f) + w := v.toNormalizedDiyfp() + + // boundary_minus and boundary_plus are the boundaries between v and its + // closest floating-point neighbors. Any number strictly between + // boundary_minus and boundary_plus will round to v when convert to a double. + // Grisu3 will never output representations that lie exactly on a boundary. + boundary_minus, boundary_plus := v.normalizedBoundaries() + ten_mk_minimal_binary_exponent := kMinimalTargetExponent - (w.e + diyFpKSignificandSize) + ten_mk_maximal_binary_exponent := kMaximalTargetExponent - (w.e + diyFpKSignificandSize) + ten_mk, mk := getCachedPowerForBinaryExponentRange(ten_mk_minimal_binary_exponent, ten_mk_maximal_binary_exponent) + + _DCHECK( + (kMinimalTargetExponent <= + w.e+ten_mk.e+diyFpKSignificandSize) && + (kMaximalTargetExponent >= w.e+ten_mk.e+diyFpKSignificandSize)) + // Note that ten_mk is only an approximation of 10^-k. A DiyFp only contains a + // 64 bit significand and ten_mk is thus only precise up to 64 bits. + + // The DiyFp::Times procedure rounds its result, and ten_mk is approximated + // too. The variable scaled_w (as well as scaled_boundary_minus/plus) are now + // off by a small amount. + // In fact: scaled_w - w*10^k < 1ulp (unit in the last place) of scaled_w. + // In other words: let f = scaled_w.f() and e = scaled_w.e(), then + // (f-1) * 2^e < w*10^k < (f+1) * 2^e + scaled_w := w.times(ten_mk) + _DCHECK(scaled_w.e == + boundary_plus.e+ten_mk.e+diyFpKSignificandSize) + // In theory it would be possible to avoid some recomputations by computing + // the difference between w and boundary_minus/plus (a power of 2) and to + // compute scaled_boundary_minus/plus by subtracting/adding from + // scaled_w. However the code becomes much less readable and the speed + // enhancements are not terrific. + scaled_boundary_minus := boundary_minus.times(ten_mk) + scaled_boundary_plus := boundary_plus.times(ten_mk) + // DigitGen will generate the digits of scaled_w. Therefore we have + // v == (double) (scaled_w * 10^-mk). + // Set decimal_exponent == -mk and pass it to DigitGen. If scaled_w is not an + // integer than it will be updated. For instance if scaled_w == 1.23 then + // the buffer will be filled with "123" und the decimal_exponent will be + // decreased by 2. + var kappa int + kappa, digits, result = digitGen(scaled_boundary_minus, scaled_w, scaled_boundary_plus, buffer) + decimal_exponent = -mk + kappa + return +} + +// The "counted" version of grisu3 (see above) only generates requested_digits +// number of digits. This version does not generate the shortest representation, +// and with enough requested digits 0.1 will at some point print as 0.9999999... +// Grisu3 is too imprecise for real halfway cases (1.5 will not work) and +// therefore the rounding strategy for halfway cases is irrelevant. +func grisu3Counted(v float64, requested_digits int, buffer []byte) (digits []byte, decimal_exponent int, result bool) { + w := double(v).toNormalizedDiyfp() + ten_mk_minimal_binary_exponent := kMinimalTargetExponent - (w.e + diyFpKSignificandSize) + ten_mk_maximal_binary_exponent := kMaximalTargetExponent - (w.e + diyFpKSignificandSize) + ten_mk, mk := getCachedPowerForBinaryExponentRange(ten_mk_minimal_binary_exponent, ten_mk_maximal_binary_exponent) + + _DCHECK( + (kMinimalTargetExponent <= + w.e+ten_mk.e+diyFpKSignificandSize) && + (kMaximalTargetExponent >= w.e+ten_mk.e+diyFpKSignificandSize)) + // Note that ten_mk is only an approximation of 10^-k. A DiyFp only contains a + // 64 bit significand and ten_mk is thus only precise up to 64 bits. + + // The DiyFp::Times procedure rounds its result, and ten_mk is approximated + // too. The variable scaled_w (as well as scaled_boundary_minus/plus) are now + // off by a small amount. + // In fact: scaled_w - w*10^k < 1ulp (unit in the last place) of scaled_w. + // In other words: let f = scaled_w.f() and e = scaled_w.e(), then + // (f-1) * 2^e < w*10^k < (f+1) * 2^e + scaled_w := w.times(ten_mk) + // We now have (double) (scaled_w * 10^-mk). + // DigitGen will generate the first requested_digits digits of scaled_w and + // return together with a kappa such that scaled_w ~= buffer * 10^kappa. (It + // will not always be exactly the same since DigitGenCounted only produces a + // limited number of digits.) + var kappa int + kappa, digits, result = digitGenCounted(scaled_w, requested_digits, buffer) + decimal_exponent = -mk + kappa + + return +} + +// v must be > 0 and must not be Inf or NaN +func Dtoa(v float64, mode Mode, requested_digits int, buffer []byte) (digits []byte, decimal_point int, result bool) { + defer func() { + if x := recover(); x != nil { + if x == dcheckFailure { + panic(fmt.Errorf("DCHECK assertion failed while formatting %s in mode %d", strconv.FormatFloat(v, 'e', 50, 64), mode)) + } + panic(x) + } + }() + var decimal_exponent int + startPos := len(buffer) + switch mode { + case ModeShortest: + digits, decimal_exponent, result = grisu3(v, buffer) + case ModePrecision: + digits, decimal_exponent, result = grisu3Counted(v, requested_digits, buffer) + } + if result { + decimal_point = len(digits) - startPos + decimal_exponent + } else { + digits = digits[:startPos] + } + return +} From ee27b039da42413e8a0003e2faed328663b859bf Mon Sep 17 00:00:00 2001 From: Dmitry Panov Date: Mon, 4 May 2020 10:56:02 +0100 Subject: [PATCH 32/46] Fixed receiver value passed to the get proxy trap. Fixes #149. --- builtin_proxy_test.go | 75 +++++++++++++++++++++++++++++++++++++++++++ proxy.go | 3 ++ 2 files changed, 78 insertions(+) diff --git a/builtin_proxy_test.go b/builtin_proxy_test.go index 7cf5fe14..5581eb8b 100644 --- a/builtin_proxy_test.go +++ b/builtin_proxy_test.go @@ -485,6 +485,81 @@ func TestProxy_proxy_get(t *testing.T) { testScript1(SCRIPT, asciiString("321tset"), t) } +func TestProxy_proxy_get_json_stringify(t *testing.T) { + const SCRIPT = ` + var obj = {}; + var propValue = "321tset"; + var _handler, _target, _prop, _receiver; + var proxy = new Proxy(obj, { + ownKeys: function() { + return ["foo"]; + }, + getOwnPropertyDescriptor: function(target, prop) { + if (prop === "foo") { + return { + value: propValue, + enumerable: true, + configurable: true + } + } + }, + get: function(target, prop, receiver) { + if (prop === "foo") { + _prop = prop; + _receiver = receiver; + return propValue; + } + return obj[prop]; + } + }); + /*Object.defineProperty(proxy, "foo", { + value: "test123", + configurable: true, + });*/ + var res = JSON.stringify(proxy); + assert.sameValue(res, '{"foo":"321tset"}'); + assert.sameValue(_prop, "foo"); + assert.sameValue(_receiver, proxy); + ` + + testScript1(TESTLIB+SCRIPT, _undefined, t) +} + +func TestProxy_native_proxy_get_json_stringify(t *testing.T) { + vm := New() + propValue := vm.ToValue("321tset") + obj := vm.NewObject() + proxy := vm.NewProxy(obj, &ProxyTrapConfig{ + OwnKeys: func(*Object) *Object { + return vm.newArrayValues([]Value{vm.ToValue("foo")}) + }, + GetOwnPropertyDescriptor: func(target *Object, prop string) (propertyDescriptor PropertyDescriptor) { + if prop == "foo" { + return PropertyDescriptor{ + Value: propValue, + Enumerable: FLAG_TRUE, + Configurable: FLAG_TRUE, + } + } + return PropertyDescriptor{} + }, + Get: func(target *Object, property string, receiver *Object) (value Value) { + if property == "foo" { + return propValue + } + return obj.Get(property) + }, + }) + vm.Set("proxy", proxy) + res, err := vm.RunString(`JSON.stringify(proxy)`) + if err != nil { + t.Fatal(err) + } + if !res.SameAs(asciiString(`{"foo":"321tset"}`)) { + t.Fatalf("res: %v", res) + } +} + func TestProxy_target_set_prop(t *testing.T) { const SCRIPT = ` var obj = {}; diff --git a/proxy.go b/proxy.go index 51e0c861..534b50b2 100644 --- a/proxy.go +++ b/proxy.go @@ -413,6 +413,9 @@ func (p *proxyObject) getSym(s *valueSymbol, receiver Value) Value { func (p *proxyObject) proxyGet(name, receiver Value) (Value, bool) { target := p.target + if receiver == nil { + receiver = p.val + } if v, ok := p.proxyCall(proxy_trap_get, target, name, receiver); ok { if targetDesc, ok := target.getOwnProp(name).(*valueProperty); ok { if !targetDesc.accessor { From c61e8285ade6a5b8b9f4008deff96331a3105048 Mon Sep 17 00:00:00 2001 From: Dmitry Panov Date: Tue, 5 May 2020 11:55:32 +0100 Subject: [PATCH 33/46] Handling Symbol properties in native Proxy handler. Fixes #149. --- builtin_proxy.go | 24 ++++++++++++++++++++---- builtin_proxy_test.go | 29 ++++++++++++++++++++++++----- 2 files changed, 44 insertions(+), 9 deletions(-) diff --git a/builtin_proxy.go b/builtin_proxy.go index 2a3b87c7..a4236ac7 100644 --- a/builtin_proxy.go +++ b/builtin_proxy.go @@ -72,7 +72,10 @@ func (r *Runtime) proxyproto_nativehandler_getOwnPropertyDescriptor(native func( handler.self._putProp("getOwnPropertyDescriptor", r.newNativeFunc(func(call FunctionCall) Value { if len(call.Arguments) >= 2 { if t, ok := call.Argument(0).(*Object); ok { - if p, ok := call.Argument(1).(valueString); ok { + switch p := call.Argument(1).(type) { + case *valueSymbol: + return _undefined + default: desc := native(t, p.String()) return desc.toValue(r) } @@ -105,7 +108,10 @@ func (r *Runtime) proxyproto_nativehandler_gen_obj_string_bool(name proxyTrap, n handler.self._putProp(unistring.String(name), r.newNativeFunc(func(call FunctionCall) Value { if len(call.Arguments) >= 2 { if t, ok := call.Argument(0).(*Object); ok { - if p, ok := call.Argument(1).(valueString); ok { + switch p := call.Argument(1).(type) { + case *valueSymbol: + return valueFalse + default: o := native(t, p.String()) return r.ToValue(o) } @@ -121,8 +127,11 @@ func (r *Runtime) proxyproto_nativehandler_get(native func(*Object, string, *Obj handler.self._putProp("get", r.newNativeFunc(func(call FunctionCall) Value { if len(call.Arguments) >= 3 { if t, ok := call.Argument(0).(*Object); ok { - if p, ok := call.Argument(1).(valueString); ok { - if r, ok := call.Argument(2).(*Object); ok { + if r, ok := call.Argument(2).(*Object); ok { + switch p := call.Argument(1).(type) { + case *valueSymbol: + return _undefined + default: return native(t, p.String(), r) } } @@ -191,6 +200,13 @@ func (r *Runtime) proxyproto_nativehandler_construct(native func(*Object, []Valu } } +// ProxyTrapConfig provides a simplified Go-friendly API for implementing Proxy traps. +// Note that the Proxy may not have Symbol properties when using this as a handler because property keys are +// passed as strings. +// get() and getOwnPropertyDescriptor() for Symbol properties will always return undefined; +// has() and deleteProperty() for Symbol properties will always return false; +// set() and defineProperty() for Symbol properties will throw a TypeError. +// If you need Symbol properties implement the handler in JavaScript. type ProxyTrapConfig struct { // A trap for Object.getPrototypeOf, Reflect.getPrototypeOf, __proto__, Object.prototype.isPrototypeOf, instanceof GetPrototypeOf func(target *Object) (prototype *Object) diff --git a/builtin_proxy_test.go b/builtin_proxy_test.go index 5581eb8b..8344f153 100644 --- a/builtin_proxy_test.go +++ b/builtin_proxy_test.go @@ -512,10 +512,6 @@ func TestProxy_proxy_get_json_stringify(t *testing.T) { return obj[prop]; } }); - /*Object.defineProperty(proxy, "foo", { - value: "test123", - configurable: true, - });*/ var res = JSON.stringify(proxy); assert.sameValue(res, '{"foo":"321tset"}'); assert.sameValue(_prop, "foo"); @@ -525,7 +521,7 @@ func TestProxy_proxy_get_json_stringify(t *testing.T) { testScript1(TESTLIB+SCRIPT, _undefined, t) } -func TestProxy_native_proxy_get_json_stringify(t *testing.T) { +func TestProxy_native_proxy_get(t *testing.T) { vm := New() propValue := vm.ToValue("321tset") obj := vm.NewObject() @@ -558,6 +554,29 @@ func TestProxy_native_proxy_get_json_stringify(t *testing.T) { if !res.SameAs(asciiString(`{"foo":"321tset"}`)) { t.Fatalf("res: %v", res) } + res, err = vm.RunString(`proxy[Symbol.toPrimitive]`) + if err != nil { + t.Fatal(err) + } + if !IsUndefined(res) { + t.Fatalf("res: %v", res) + } + + res, err = vm.RunString(`proxy.hasOwnProperty(Symbol.toPrimitive)`) + if err != nil { + t.Fatal(err) + } + if !res.SameAs(valueFalse) { + t.Fatalf("res: %v", res) + } + + res, err = vm.RunString(`proxy.toString()`) + if err != nil { + t.Fatal(err) + } + if !res.SameAs(asciiString(`[object Object]`)) { + t.Fatalf("res: %v", res) + } } func TestProxy_target_set_prop(t *testing.T) { From 91317b8079295c0c47f8ee3c0066c2472512cb1d Mon Sep 17 00:00:00 2001 From: Dmitry Panov Date: Sat, 23 May 2020 23:01:22 +0100 Subject: [PATCH 34/46] Merged the fix for #151. --- vm.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/vm.go b/vm.go index a2f4eb29..1540064e 100644 --- a/vm.go +++ b/vm.go @@ -203,9 +203,9 @@ func (s *valueStack) expand(idx int) { } } -func stashObjHas(obj objectImpl, name unistring.String) bool { - if obj.hasPropertyStr(name) { - if unscopables, ok := obj.getSym(symUnscopables, nil).(*Object); ok { +func stashObjHas(obj *Object, name unistring.String) bool { + if obj.self.hasPropertyStr(name) { + if unscopables, ok := obj.self.getSym(symUnscopables, nil).(*Object); ok { if b := unscopables.self.getStr(name, nil); b != nil { return !b.ToBoolean() } @@ -1504,9 +1504,9 @@ func (g getLocal) exec(vm *vm) { } type getVar struct { - name unistring.String - idx uint32 - ref, callee bool + name unistring.String + idx uint32 + ref, callee bool } func (g getVar) exec(vm *vm) { @@ -1657,7 +1657,7 @@ func (n getVar1) exec(vm *vm) { type getVar1Ref string func (n getVar1Ref) exec(vm *vm) { - name := string(n) + name := unistring.String(n) var val Value for stash := vm.stash; stash != nil; stash = stash.outer { if v, exists := stash.getByName(name, vm); exists { @@ -1666,7 +1666,7 @@ func (n getVar1Ref) exec(vm *vm) { } } if val == nil { - val = vm.r.globalObject.self.getStr(name) + val = vm.r.globalObject.self.getStr(name, nil) if val == nil { val = valueUnresolved{r: vm.r, ref: name} } From 76b747147f7058836a0457924e601da1b339e61c Mon Sep 17 00:00:00 2001 From: Dmitry Panov Date: Tue, 26 May 2020 18:27:49 +0100 Subject: [PATCH 35/46] Merged #152. --- parser/lexer.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/parser/lexer.go b/parser/lexer.go index 5e5dbb95..da6e739c 100644 --- a/parser/lexer.go +++ b/parser/lexer.go @@ -3,6 +3,7 @@ package parser import ( "errors" "fmt" + "regexp" "strconv" "strings" "unicode" @@ -14,6 +15,8 @@ import ( "github.com/dop251/goja/unistring" ) +var matchIdentifier = regexp.MustCompile(`^[$_\p{L}][$_\p{L}\d}]*$`) + func isDecimalDigit(chr rune) bool { return '0' <= chr && chr <= '9' } From b292a24dd11101b2a4a45e6a809748516697eeb0 Mon Sep 17 00:00:00 2001 From: Dmitry Panov Date: Tue, 14 Jul 2020 12:02:24 +0100 Subject: [PATCH 36/46] Enabled JSON tests. --- builtin_json.go | 1 + object.go | 1 + tc39_test.go | 1 + 3 files changed, 3 insertions(+) diff --git a/builtin_json.go b/builtin_json.go index 4d792483..2325a2b1 100644 --- a/builtin_json.go +++ b/builtin_json.go @@ -493,6 +493,7 @@ func (r *Runtime) initJSON() { JSON := r.newBaseObject(r.global.ObjectPrototype, "JSON") JSON._putProp("parse", r.newNativeFunc(r.builtinJSON_parse, nil, "parse", nil, 2), true, false, true) JSON._putProp("stringify", r.newNativeFunc(r.builtinJSON_stringify, nil, "stringify", nil, 3), true, false, true) + JSON._putSym(symToStringTag, valueProp(asciiString(classJSON), false, false, true)) r.addToGlobal("JSON", JSON.val) } diff --git a/object.go b/object.go index 45fe1f08..70c86ae7 100644 --- a/object.go +++ b/object.go @@ -25,6 +25,7 @@ const ( classError = "Error" classRegExp = "RegExp" classDate = "Date" + classJSON = "JSON" classArrayIterator = "Array Iterator" classMapIterator = "Map Iterator" diff --git a/tc39_test.go b/tc39_test.go index da19abbb..47aec98a 100644 --- a/tc39_test.go +++ b/tc39_test.go @@ -164,6 +164,7 @@ var ( "23.4", "24.1", "24.2", + "24.3", "25.1.2", "26.1", "26.2", From b622c6a4ca9546396f3b70cb6b6f2114ad74b9c7 Mon Sep 17 00:00:00 2001 From: Dmitry Panov Date: Tue, 21 Jul 2020 20:10:00 +0100 Subject: [PATCH 37/46] Fixed toReflectType(interface{}) (should do Export() rather than return the original Value) --- runtime.go | 4 ++-- runtime_test.go | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/runtime.go b/runtime.go index 55657c40..60715f89 100644 --- a/runtime.go +++ b/runtime.go @@ -1480,11 +1480,11 @@ func (r *Runtime) toReflectValue(v Value, typ reflect.Type) (reflect.Value, erro } } - if typeValue.AssignableTo(typ) { + if typ == typeValue { return reflect.ValueOf(v), nil } - if typeObject.AssignableTo(typ) { + if typ == typeObject { if obj, ok := v.(*Object); ok { return reflect.ValueOf(obj), nil } diff --git a/runtime_test.go b/runtime_test.go index 7fa9ca99..93d07ce7 100644 --- a/runtime_test.go +++ b/runtime_test.go @@ -752,6 +752,23 @@ func TestToValueFloat(t *testing.T) { } } +func TestToValueInterface(t *testing.T) { + + f := func(i interface{}) bool { + return i == t + } + vm := New() + vm.Set("f", f) + vm.Set("t", t) + v, err := vm.RunString(`f(t)`) + if err != nil { + t.Fatal(err) + } + if v != valueTrue { + t.Fatalf("v: %v", v) + } +} + func TestJSONEscape(t *testing.T) { const SCRIPT = ` var a = "\\+1"; From bc102619cc2a1ef6c709adbf7083b6595bcd6265 Mon Sep 17 00:00:00 2001 From: Dmitry Panov Date: Sat, 25 Jul 2020 18:59:42 +0100 Subject: [PATCH 38/46] Fixed CaptureCallStack() --- runtime.go | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/runtime.go b/runtime.go index 60715f89..3ca3f458 100644 --- a/runtime.go +++ b/runtime.go @@ -1111,16 +1111,22 @@ func (r *Runtime) RunProgram(p *Program) (result Value, err error) { return } -func (r *Runtime) CaptureCallStack(n int) []StackFrame { - m := len(r.vm.callStack) - if n > 0 { - m -= m - n - } else { - m = 0 - } - stackFrames := make([]StackFrame, 0) - stackFrames = r.vm.captureStack(stackFrames, m) - return stackFrames +// CaptureCallStack appends the current call stack frames to the stack slice (which may be nil) up to the specified depth. +// The most recent frame will be the first one. +// If depth <= 0 or more than the number of available frames, returns the entire stack. +func (r *Runtime) CaptureCallStack(depth int, stack []StackFrame) []StackFrame { + l := len(r.vm.callStack) + var offset int + if depth > 0 { + offset = l - depth + 1 + if offset < 0 { + offset = 0 + } + } + if stack == nil { + stack = make([]StackFrame, 0, l-offset+1) + } + return r.vm.captureStack(stack, offset) } // Interrupt a running JavaScript. The corresponding Go call will return an *InterruptedError containing v. From f19759604c88e17c96c01c568e10ee1e5d0912ad Mon Sep 17 00:00:00 2001 From: Dmitry Panov Date: Sun, 26 Jul 2020 11:16:49 +0100 Subject: [PATCH 39/46] Renamed Value.ToPrimitiveString() to ToString() which matches a method that existed in the master. --- builtin_array.go | 2 +- runtime.go | 2 +- string_ascii.go | 2 +- string_unicode.go | 2 +- value.go | 22 +++++++++++----------- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/builtin_array.go b/builtin_array.go index b8d4b729..483e761d 100644 --- a/builtin_array.go +++ b/builtin_array.go @@ -224,7 +224,7 @@ func (r *Runtime) writeItemLocaleString(item Value, buf *strings.Builder) { strVal := c(FunctionCall{ This: item, }) - buf.WriteString(strVal.ToPrimitiveString().String()) + buf.WriteString(strVal.ToString().String()) return } } diff --git a/runtime.go b/runtime.go index 3ca3f458..54b3ee7d 100644 --- a/runtime.go +++ b/runtime.go @@ -1874,7 +1874,7 @@ func createDataPropertyOrThrow(o *Object, p Value, v Value) { } func toPropertyKey(key Value) Value { - return key.ToPrimitiveString() + return key.ToString() } func (r *Runtime) getVStr(v Value, p unistring.String) Value { diff --git a/string_ascii.go b/string_ascii.go index 179a4714..dab1bcb7 100644 --- a/string_ascii.go +++ b/string_ascii.go @@ -108,7 +108,7 @@ func (s asciiString) toString() valueString { return s } -func (s asciiString) ToPrimitiveString() Value { +func (s asciiString) ToString() Value { return s } diff --git a/string_unicode.go b/string_unicode.go index 8fea1aca..9a2b3374 100644 --- a/string_unicode.go +++ b/string_unicode.go @@ -155,7 +155,7 @@ func (s unicodeString) toString() valueString { return s } -func (s unicodeString) ToPrimitiveString() Value { +func (s unicodeString) ToString() Value { return s } diff --git a/value.go b/value.go index e82628b0..d6ac9a74 100644 --- a/value.go +++ b/value.go @@ -57,7 +57,7 @@ type Value interface { ToInteger() int64 toString() valueString string() unistring.String - ToPrimitiveString() Value + ToString() Value String() string ToFloat() float64 ToNumber() Value @@ -155,7 +155,7 @@ func (i valueInt) string() unistring.String { return unistring.String(i.String()) } -func (i valueInt) ToPrimitiveString() Value { +func (i valueInt) ToString() Value { return i } @@ -241,7 +241,7 @@ func (b valueBool) toString() valueString { return stringFalse } -func (b valueBool) ToPrimitiveString() Value { +func (b valueBool) ToString() Value { return b } @@ -337,7 +337,7 @@ func (n valueNull) string() unistring.String { return stringNull.string() } -func (n valueNull) ToPrimitiveString() Value { +func (n valueNull) ToString() Value { return n } @@ -349,7 +349,7 @@ func (u valueUndefined) toString() valueString { return stringUndefined } -func (u valueUndefined) ToPrimitiveString() Value { +func (u valueUndefined) ToString() Value { return u } @@ -447,7 +447,7 @@ func (p *valueProperty) string() unistring.String { return "" } -func (p *valueProperty) ToPrimitiveString() Value { +func (p *valueProperty) ToString() Value { return _undefined } @@ -556,7 +556,7 @@ func (f valueFloat) string() unistring.String { return unistring.String(f.String()) } -func (f valueFloat) ToPrimitiveString() Value { +func (f valueFloat) ToString() Value { return f } @@ -663,8 +663,8 @@ func (o *Object) string() unistring.String { return o.toPrimitiveString().string() } -func (o *Object) ToPrimitiveString() Value { - return o.toPrimitiveString().ToPrimitiveString() +func (o *Object) ToString() Value { + return o.toPrimitiveString().ToString() } func (o *Object) String() string { @@ -819,7 +819,7 @@ func (o valueUnresolved) string() unistring.String { return "" } -func (o valueUnresolved) ToPrimitiveString() Value { +func (o valueUnresolved) ToString() Value { o.throw() return nil } @@ -892,7 +892,7 @@ func (s *valueSymbol) toString() valueString { panic(typeError("Cannot convert a Symbol value to a string")) } -func (s *valueSymbol) ToPrimitiveString() Value { +func (s *valueSymbol) ToString() Value { return s } From b206dd5e2c2a4907bb6d9edeadab331eff55fc1b Mon Sep 17 00:00:00 2001 From: Dmitry Panov Date: Thu, 6 Aug 2020 15:44:43 +0100 Subject: [PATCH 40/46] Added missing Regexp functionality, enhanced unicode and UTF-16 support (#171) --- builtin_array.go | 29 +- builtin_date.go | 2 +- builtin_function.go | 24 +- builtin_global.go | 2 +- builtin_json.go | 15 +- builtin_json_test.go | 4 + builtin_regexp.go | 747 ++++++++++++++++++++++++++++++++++------- builtin_string.go | 246 +++++--------- builtin_string_test.go | 58 ++++ builtin_typedarrays.go | 21 +- compiler_expr.go | 14 +- object.go | 44 +++ regexp.go | 508 ++++++++++++++++++---------- regexp_test.go | 160 +++++++++ runtime.go | 76 +++-- string.go | 9 +- string_ascii.go | 16 + string_unicode.go | 184 +++++++++- tc39_test.go | 47 ++- vm.go | 8 +- 20 files changed, 1652 insertions(+), 562 deletions(-) diff --git a/builtin_array.go b/builtin_array.go index 483e761d..b493e51d 100644 --- a/builtin_array.go +++ b/builtin_array.go @@ -3,7 +3,6 @@ package goja import ( "math" "sort" - "strings" ) func (r *Runtime) newArray(prototype *Object) (a *arrayObject) { @@ -174,32 +173,32 @@ func (r *Runtime) arrayproto_pop(call FunctionCall) Value { func (r *Runtime) arrayproto_join(call FunctionCall) Value { o := call.This.ToObject(r) l := int(toLength(o.self.getStr("length", nil))) - sep := "" + var sep valueString = asciiString("") if s := call.Argument(0); s != _undefined { - sep = s.toString().String() + sep = s.toString() } else { - sep = "," + sep = asciiString(",") } if l == 0 { return stringEmpty } - var buf strings.Builder + var buf valueStringBuilder element0 := o.self.getIdx(valueInt(0), nil) if element0 != nil && element0 != _undefined && element0 != _null { - buf.WriteString(element0.String()) + buf.WriteString(element0.toString()) } for i := 1; i < l; i++ { buf.WriteString(sep) element := o.self.getIdx(valueInt(int64(i)), nil) if element != nil && element != _undefined && element != _null { - buf.WriteString(element.String()) + buf.WriteString(element.toString()) } } - return newStringValue(buf.String()) + return buf.String() } func (r *Runtime) arrayproto_toString(call FunctionCall) Value { @@ -217,14 +216,14 @@ func (r *Runtime) arrayproto_toString(call FunctionCall) Value { }) } -func (r *Runtime) writeItemLocaleString(item Value, buf *strings.Builder) { +func (r *Runtime) writeItemLocaleString(item Value, buf *valueStringBuilder) { if item != nil && item != _undefined && item != _null { if f, ok := r.getVStr(item, "toLocaleString").(*Object); ok { if c, ok := f.self.assertCallable(); ok { strVal := c(FunctionCall{ This: item, }) - buf.WriteString(strVal.ToString().String()) + buf.WriteString(strVal.toString()) return } } @@ -234,11 +233,11 @@ func (r *Runtime) writeItemLocaleString(item Value, buf *strings.Builder) { func (r *Runtime) arrayproto_toLocaleString(call FunctionCall) Value { array := call.This.ToObject(r) - var buf strings.Builder + var buf valueStringBuilder if a := r.checkStdArrayObj(array); a != nil { for i, item := range a.values { if i > 0 { - buf.WriteByte(',') + buf.WriteRune(',') } r.writeItemLocaleString(item, &buf) } @@ -246,14 +245,14 @@ func (r *Runtime) arrayproto_toLocaleString(call FunctionCall) Value { length := toLength(array.self.getStr("length", nil)) for i := int64(0); i < length; i++ { if i > 0 { - buf.WriteByte(',') + buf.WriteRune(',') } item := array.self.getIdx(valueInt(i), nil) r.writeItemLocaleString(item, &buf) } } - return newStringValue(buf.String()) + return buf.String() } func isConcatSpreadable(obj *Object) bool { @@ -1331,7 +1330,7 @@ func (a *arraySortCtx) sortCompare(x, y Value) int { } return 0 } - return strings.Compare(x.String(), y.String()) + return x.toString().compareTo(y.toString()) } // sort.Interface diff --git a/builtin_date.go b/builtin_date.go index c706f1c7..910e381a 100644 --- a/builtin_date.go +++ b/builtin_date.go @@ -72,7 +72,7 @@ func (r *Runtime) builtin_date(FunctionCall) Value { } func (r *Runtime) date_parse(call FunctionCall) Value { - t, set := dateParse(call.Argument(0).String()) + t, set := dateParse(call.Argument(0).toString().String()) if set { return intToValue(timeToMsec(t)) } diff --git a/builtin_function.go b/builtin_function.go index 249e29bd..c8a91f8b 100644 --- a/builtin_function.go +++ b/builtin_function.go @@ -5,20 +5,24 @@ import ( ) func (r *Runtime) builtin_Function(args []Value, proto *Object) *Object { - src := "(function anonymous(" + var sb valueStringBuilder + sb.WriteString(asciiString("(function anonymous(")) if len(args) > 1 { - for _, arg := range args[:len(args)-1] { - src += arg.String() + "," + ar := args[:len(args)-1] + for i, arg := range ar { + sb.WriteString(arg.toString()) + if i < len(ar)-1 { + sb.WriteRune(',') + } } - src = src[:len(src)-1] } - body := "" + sb.WriteString(asciiString("){")) if len(args) > 0 { - body = args[len(args)-1].String() + sb.WriteString(args[len(args)-1].toString()) } - src += "){" + body + "})" + sb.WriteString(asciiString("})")) - ret := r.toObject(r.eval(src, false, false, _undefined)) + ret := r.toObject(r.eval(sb.String(), false, false, _undefined)) ret.self.setProto(proto, true) return ret } @@ -43,9 +47,9 @@ repeat: case *funcObject: name = c.src case *nativeFuncObject: - name = c.nameProp.get(call.This).String() + name = nilSafe(c.nameProp.get(call.This)).toString().String() case *boundFuncObject: - name = c.nameProp.get(call.This).String() + name = nilSafe(c.nameProp.get(call.This)).toString().String() case *lazyObject: f.target.self = c.create(obj) goto repeat2 diff --git a/builtin_global.go b/builtin_global.go index 68853d1c..71bfdde8 100644 --- a/builtin_global.go +++ b/builtin_global.go @@ -117,7 +117,7 @@ func (r *Runtime) _encode(uriString valueString, unescaped *[256]bool) valueStri i++ } } - return asciiString(string(buf)) + return asciiString(buf) } func (r *Runtime) _decode(sv valueString, reservedSet *[256]bool) valueString { diff --git a/builtin_json.go b/builtin_json.go index 2325a2b1..2d2e9e74 100644 --- a/builtin_json.go +++ b/builtin_json.go @@ -7,6 +7,7 @@ import ( "io" "math" "strings" + "unicode/utf16" "github.com/dop251/goja/unistring" ) @@ -14,7 +15,7 @@ import ( const hex = "0123456789abcdef" func (r *Runtime) builtinJSON_parse(call FunctionCall) Value { - d := json.NewDecoder(bytes.NewBufferString(call.Argument(0).String())) + d := json.NewDecoder(bytes.NewBufferString(call.Argument(0).toString().String())) value, err := r.builtinJSON_decodeValue(d) if err != nil { @@ -456,7 +457,7 @@ func (ctx *_builtinJSON_stringifyContext) jo(object *Object) { func (ctx *_builtinJSON_stringifyContext) quote(str valueString) { ctx.buf.WriteByte('"') - reader := str.reader(0) + reader := &lenientUtf16Decoder{utf16Reader: str.utf16Reader(0)} for { r, _, err := reader.ReadRune() if err != nil { @@ -482,7 +483,15 @@ func (ctx *_builtinJSON_stringifyContext) quote(str valueString) { ctx.buf.WriteByte(hex[r>>4]) ctx.buf.WriteByte(hex[r&0xF]) } else { - ctx.buf.WriteRune(r) + if utf16.IsSurrogate(r) { + ctx.buf.WriteString(`\u`) + ctx.buf.WriteByte(hex[r>>12]) + ctx.buf.WriteByte(hex[(r>>8)&0xF]) + ctx.buf.WriteByte(hex[(r>>4)&0xF]) + ctx.buf.WriteByte(hex[r&0xF]) + } else { + ctx.buf.WriteRune(r) + } } } } diff --git a/builtin_json_test.go b/builtin_json_test.go index 42c27452..3e261bf0 100644 --- a/builtin_json_test.go +++ b/builtin_json_test.go @@ -61,6 +61,10 @@ func TestJSONParseReviver(t *testing.T) { testScript1(SCRIPT, intToValue(10), t) } +func TestQuoteMalformedSurrogatePair(t *testing.T) { + testScript1(`JSON.stringify("\uD800")`, asciiString(`"\ud800"`), t) +} + func BenchmarkJSONStringify(b *testing.B) { b.StopTimer() vm := New() diff --git a/builtin_regexp.go b/builtin_regexp.go index e3a54da0..35dfdea3 100644 --- a/builtin_regexp.go +++ b/builtin_regexp.go @@ -2,10 +2,11 @@ package goja import ( "fmt" - "github.com/dlclark/regexp2" "github.com/dop251/goja/parser" "regexp" "strings" + "unicode/utf16" + "unicode/utf8" ) func (r *Runtime) newRegexpObject(proto *Object) *regexpObject { @@ -21,20 +22,167 @@ func (r *Runtime) newRegexpObject(proto *Object) *regexpObject { return o } -func (r *Runtime) newRegExpp(pattern regexpPattern, patternStr valueString, global, ignoreCase, multiline, sticky bool, proto *Object) *Object { +func (r *Runtime) newRegExpp(pattern *regexpPattern, patternStr valueString, proto *Object) *Object { o := r.newRegexpObject(proto) o.pattern = pattern o.source = patternStr - o.global = global - o.ignoreCase = ignoreCase - o.multiline = multiline - o.sticky = sticky return o.val } -func compileRegexp(patternStr, flags string) (p regexpPattern, global, ignoreCase, multiline, sticky bool, err error) { +func decodeHex(s string) (int, bool) { + var hex int + for i := 0; i < len(s); i++ { + var n byte + chr := s[i] + switch { + case '0' <= chr && chr <= '9': + n = chr - '0' + case 'a' <= chr && chr <= 'f': + n = chr - 'a' + 10 + case 'A' <= chr && chr <= 'F': + n = chr - 'A' + 10 + default: + return 0, false + } + hex = hex*16 + int(n) + } + return hex, true +} + +func writeHex4(b *strings.Builder, i int) { + b.WriteByte(hex[i>>12]) + b.WriteByte(hex[(i>>8)&0xF]) + b.WriteByte(hex[(i>>4)&0xF]) + b.WriteByte(hex[i&0xF]) +} + +// Convert any valid surrogate pairs in the form of \uXXXX\uXXXX to unicode characters +func convertRegexpToUnicode(patternStr string) string { + var sb strings.Builder + pos := 0 + for i := 0; i < len(patternStr)-11; { + r, size := utf8.DecodeRuneInString(patternStr[i:]) + if r == '\\' { + i++ + if patternStr[i] == 'u' && patternStr[i+5] == '\\' && patternStr[i+6] == 'u' { + if first, ok := decodeHex(patternStr[i+1 : i+5]); ok { + if isUTF16FirstSurrogate(rune(first)) { + if second, ok := decodeHex(patternStr[i+7 : i+11]); ok { + if isUTF16SecondSurrogate(rune(second)) { + r = utf16.DecodeRune(rune(first), rune(second)) + sb.WriteString(patternStr[pos : i-1]) + sb.WriteRune(r) + i += 11 + pos = i + continue + } + } + } + } + } + i++ + } else { + i += size + } + } + if pos > 0 { + sb.WriteString(patternStr[pos:]) + return sb.String() + } + return patternStr +} + +// Convert any extended unicode characters to UTF-16 in the form of \uXXXX\uXXXX +func convertRegexpToUtf16(patternStr string) string { + var sb strings.Builder + pos := 0 + var prevRune rune + for i := 0; i < len(patternStr); { + r, size := utf8.DecodeRuneInString(patternStr[i:]) + if r > 0xFFFF { + sb.WriteString(patternStr[pos:i]) + if prevRune == '\\' { + sb.WriteRune('\\') + } + first, second := utf16.EncodeRune(r) + sb.WriteString(`\u`) + writeHex4(&sb, int(first)) + sb.WriteString(`\u`) + writeHex4(&sb, int(second)) + pos = i + size + } + i += size + prevRune = r + } + if pos > 0 { + sb.WriteString(patternStr[pos:]) + return sb.String() + } + return patternStr +} + +// convert any broken UTF-16 surrogate pairs to \uXXXX +func escapeInvalidUtf16(s valueString) string { + if ascii, ok := s.(asciiString); ok { + return ascii.String() + } + var sb strings.Builder + rd := &lenientUtf16Decoder{utf16Reader: s.utf16Reader(0)} + pos := 0 + utf8Size := 0 + var utf8Buf [utf8.UTFMax]byte + for { + c, size, err := rd.ReadRune() + if err != nil { + break + } + if utf16.IsSurrogate(c) { + if sb.Len() == 0 { + sb.Grow(utf8Size + 7) + hrd := s.reader(0) + var c rune + for p := 0; p < pos; { + var size int + var err error + c, size, err = hrd.ReadRune() + if err != nil { + // will not happen + panic(fmt.Errorf("error while reading string head %q, pos: %d: %w", s.String(), pos, err)) + } + sb.WriteRune(c) + p += size + } + if c == '\\' { + sb.WriteRune(c) + } + } + sb.WriteString(`\u`) + writeHex4(&sb, int(c)) + } else { + if sb.Len() > 0 { + sb.WriteRune(c) + } else { + utf8Size += utf8.EncodeRune(utf8Buf[:], c) + pos += size + } + } + } + if sb.Len() > 0 { + return sb.String() + } + return s.String() +} + +func compileRegexpFromValueString(patternStr valueString, flags string) (*regexpPattern, error) { + return compileRegexp(escapeInvalidUtf16(patternStr), flags) +} + +func compileRegexp(patternStr, flags string) (p *regexpPattern, err error) { + var global, ignoreCase, multiline, sticky, unicode bool + var wrapper *regexpWrapper + var wrapper2 *regexp2Wrapper if flags != "" { invalidFlags := func() { @@ -66,6 +214,11 @@ func compileRegexp(patternStr, flags string) (p regexpPattern, global, ignoreCas return } sticky = true + case 'u': + if unicode { + invalidFlags() + } + unicode = true default: invalidFlags() return @@ -73,8 +226,14 @@ func compileRegexp(patternStr, flags string) (p regexpPattern, global, ignoreCas } } + if unicode { + patternStr = convertRegexpToUnicode(patternStr) + } else { + patternStr = convertRegexpToUtf16(patternStr) + } + re2Str, err1 := parser.TransformRegExp(patternStr) - if /*false &&*/ err1 == nil { + if err1 == nil { re2flags := "" if multiline { re2flags += "m" @@ -91,72 +250,138 @@ func compileRegexp(patternStr, flags string) (p regexpPattern, global, ignoreCas err = fmt.Errorf("Invalid regular expression (re2): %s (%v)", re2Str, err1) return } - - p = (*regexpWrapper)(pattern) + wrapper = (*regexpWrapper)(pattern) } else { - var opts regexp2.RegexOptions = regexp2.ECMAScript - if multiline { - opts |= regexp2.Multiline - } - if ignoreCase { - opts |= regexp2.IgnoreCase - } - regexp2Pattern, err1 := regexp2.Compile(patternStr, opts) - if err1 != nil { + wrapper2, err = compileRegexp2(patternStr, multiline, ignoreCase) + if err != nil { err = fmt.Errorf("Invalid regular expression (regexp2): %s (%v)", patternStr, err1) - return } - p = (*regexp2Wrapper)(regexp2Pattern) + } + + p = ®expPattern{ + src: patternStr, + regexpWrapper: wrapper, + regexp2Wrapper: wrapper2, + global: global, + ignoreCase: ignoreCase, + multiline: multiline, + sticky: sticky, + unicode: unicode, } return } -func (r *Runtime) newRegExp(patternStr valueString, flags string, proto *Object) *Object { - pattern, global, ignoreCase, multiline, sticky, err := compileRegexp(patternStr.String(), flags) +func (r *Runtime) _newRegExp(patternStr valueString, flags string, proto *Object) *Object { + pattern, err := compileRegexpFromValueString(patternStr, flags) if err != nil { panic(r.newSyntaxError(err.Error(), -1)) } - return r.newRegExpp(pattern, patternStr, global, ignoreCase, multiline, sticky, proto) + return r.newRegExpp(pattern, patternStr, proto) } func (r *Runtime) builtin_newRegExp(args []Value, proto *Object) *Object { + var patternVal, flagsVal Value + if len(args) > 0 { + patternVal = args[0] + } + if len(args) > 1 { + flagsVal = args[1] + } + return r.newRegExp(patternVal, flagsVal, proto) +} + +func (r *Runtime) newRegExp(patternVal, flagsVal Value, proto *Object) *Object { var pattern valueString var flags string - if len(args) > 0 { - if obj, ok := args[0].(*Object); ok { - if rx, ok := obj.self.(*regexpObject); ok { - if len(args) < 2 || args[1] == _undefined { - return rx.clone() + if obj, ok := patternVal.(*Object); ok { + if rx, ok := obj.self.(*regexpObject); ok { + if flagsVal == nil || flagsVal == _undefined { + return rx.clone() + } else { + return r._newRegExp(rx.source, flagsVal.toString().String(), proto) + } + } else { + if isRegexp(patternVal) { + pattern = nilSafe(obj.self.getStr("source", nil)).toString() + if flagsVal == nil || flagsVal == _undefined { + flags = nilSafe(obj.self.getStr("flags", nil)).toString().String() } else { - return r.newRegExp(rx.source, args[1].String(), proto) + flags = flagsVal.toString().String() } + goto exit } } - if args[0] != _undefined { - pattern = args[0].toString() - } } - if len(args) > 1 { - if a := args[1]; a != _undefined { - flags = a.String() - } + + if patternVal != nil && patternVal != _undefined { + pattern = patternVal.toString() + } + if flagsVal != nil && flagsVal != _undefined { + flags = flagsVal.toString().String() } + if pattern == nil { pattern = stringEmpty } - return r.newRegExp(pattern, flags, proto) +exit: + return r._newRegExp(pattern, flags, proto) } func (r *Runtime) builtin_RegExp(call FunctionCall) Value { + pattern := call.Argument(0) + patternIsRegExp := isRegexp(pattern) flags := call.Argument(1) - if flags == _undefined { + if patternIsRegExp && flags == _undefined { if obj, ok := call.Argument(0).(*Object); ok { - if _, ok := obj.self.(*regexpObject); ok { - return call.Arguments[0] + patternConstructor := obj.self.getStr("constructor", nil) + if patternConstructor == r.global.RegExp { + return pattern + } + } + } + return r.newRegExp(pattern, flags, r.global.RegExpPrototype) +} + +func (r *Runtime) regexpproto_compile(call FunctionCall) Value { + if this, ok := r.toObject(call.This).self.(*regexpObject); ok { + var ( + pattern *regexpPattern + source valueString + flags string + err error + ) + patternVal := call.Argument(0) + flagsVal := call.Argument(1) + if o, ok := patternVal.(*Object); ok { + if p, ok := o.self.(*regexpObject); ok { + if flagsVal != _undefined { + panic(r.NewTypeError("Cannot supply flags when constructing one RegExp from another")) + } + this.pattern = p.pattern + this.source = p.source + goto exit } } + if patternVal != _undefined { + source = patternVal.toString() + } else { + source = stringEmpty + } + if flagsVal != _undefined { + flags = flagsVal.toString().String() + } + pattern, err = compileRegexpFromValueString(source, flags) + if err != nil { + panic(r.newSyntaxError(err.Error(), -1)) + } + this.pattern = pattern + this.source = source + exit: + this.setOwnStr("lastIndex", intToValue(0), true) + return call.This } - return r.builtin_newRegExp(call.Arguments, r.global.RegExpPrototype) + + panic(r.NewTypeError("Method RegExp.prototype.compile called on incompatible receiver %s", call.This.toString())) } func (r *Runtime) regexpproto_exec(call FunctionCall) Value { @@ -182,39 +407,97 @@ func (r *Runtime) regexpproto_test(call FunctionCall) Value { } func (r *Runtime) regexpproto_toString(call FunctionCall) Value { - if this, ok := r.toObject(call.This).self.(*regexpObject); ok { - var g, i, m, y string - if this.global { - g = "g" + obj := r.toObject(call.This) + if this := r.checkStdRegexp(obj); this != nil { + var sb valueStringBuilder + sb.WriteRune('/') + if !this.writeEscapedSource(&sb) { + sb.WriteString(this.source) } - if this.ignoreCase { - i = "i" + sb.WriteRune('/') + if this.pattern.global { + sb.WriteRune('g') } - if this.multiline { - m = "m" + if this.pattern.ignoreCase { + sb.WriteRune('i') } - if this.sticky { - y = "y" + if this.pattern.multiline { + sb.WriteRune('m') } - return newStringValue(fmt.Sprintf("/%s/%s%s%s%s", this.source.String(), g, i, m, y)) - } else { - r.typeErrorResult(true, "Method RegExp.prototype.toString called on incompatible receiver %s", call.This) - return nil + if this.pattern.unicode { + sb.WriteRune('u') + } + if this.pattern.sticky { + sb.WriteRune('y') + } + return sb.String() } + pattern := nilSafe(obj.self.getStr("source", nil)).toString() + flags := nilSafe(obj.self.getStr("flags", nil)).toString() + var sb valueStringBuilder + sb.WriteRune('/') + sb.WriteString(pattern) + sb.WriteRune('/') + sb.WriteString(flags) + return sb.String() +} + +func (r *regexpObject) writeEscapedSource(sb *valueStringBuilder) bool { + if r.source.length() == 0 { + sb.WriteString(asciiString("(?:)")) + return true + } + pos := 0 + lastPos := 0 + rd := &lenientUtf16Decoder{utf16Reader: r.source.utf16Reader(0)} + for { + c, size, err := rd.ReadRune() + if err != nil { + break + } + switch c { + case '/', '\u000a', '\u000d', '\u2028', '\u2029': + sb.WriteSubstring(r.source, lastPos, pos) + sb.WriteRune('\\') + switch c { + case '\u000a': + sb.WriteRune('n') + case '\u000d': + sb.WriteRune('r') + default: + sb.WriteRune('u') + sb.WriteRune(rune(hex[c>>12])) + sb.WriteRune(rune(hex[(c>>8)&0xF])) + sb.WriteRune(rune(hex[(c>>4)&0xF])) + sb.WriteRune(rune(hex[c&0xF])) + } + lastPos = pos + size + } + pos += size + } + if lastPos > 0 { + sb.WriteSubstring(r.source, lastPos, r.source.length()) + return true + } + return false } func (r *Runtime) regexpproto_getSource(call FunctionCall) Value { if this, ok := r.toObject(call.This).self.(*regexpObject); ok { + var sb valueStringBuilder + if this.writeEscapedSource(&sb) { + return sb.String() + } return this.source } else { - r.typeErrorResult(true, "Method RegExp.prototype.source getter called on incompatible receiver %s", call.This.toString()) + r.typeErrorResult(true, "Method RegExp.prototype.source getter called on incompatible receiver") return nil } } func (r *Runtime) regexpproto_getGlobal(call FunctionCall) Value { if this, ok := r.toObject(call.This).self.(*regexpObject); ok { - if this.global { + if this.pattern.global { return valueTrue } else { return valueFalse @@ -227,7 +510,7 @@ func (r *Runtime) regexpproto_getGlobal(call FunctionCall) Value { func (r *Runtime) regexpproto_getMultiline(call FunctionCall) Value { if this, ok := r.toObject(call.This).self.(*regexpObject); ok { - if this.multiline { + if this.pattern.multiline { return valueTrue } else { return valueFalse @@ -240,7 +523,7 @@ func (r *Runtime) regexpproto_getMultiline(call FunctionCall) Value { func (r *Runtime) regexpproto_getIgnoreCase(call FunctionCall) Value { if this, ok := r.toObject(call.This).self.(*regexpObject); ok { - if this.ignoreCase { + if this.pattern.ignoreCase { return valueTrue } else { return valueFalse @@ -251,9 +534,22 @@ func (r *Runtime) regexpproto_getIgnoreCase(call FunctionCall) Value { } } +func (r *Runtime) regexpproto_getUnicode(call FunctionCall) Value { + if this, ok := r.toObject(call.This).self.(*regexpObject); ok { + if this.pattern.unicode { + return valueTrue + } else { + return valueFalse + } + } else { + r.typeErrorResult(true, "Method RegExp.prototype.unicode getter called on incompatible receiver %s", call.This.toString()) + return nil + } +} + func (r *Runtime) regexpproto_getSticky(call FunctionCall) Value { if this, ok := r.toObject(call.This).self.(*regexpObject); ok { - if this.sticky { + if this.pattern.sticky { return valueTrue } else { return valueFalse @@ -265,27 +561,43 @@ func (r *Runtime) regexpproto_getSticky(call FunctionCall) Value { } func (r *Runtime) regexpproto_getFlags(call FunctionCall) Value { - var global, ignoreCase, multiline, sticky bool + var global, ignoreCase, multiline, sticky, unicode bool thisObj := r.toObject(call.This) - if this, ok := thisObj.self.(*regexpObject); ok { - global, ignoreCase, multiline, sticky = this.global, this.ignoreCase, this.multiline, this.sticky - } else { - if v := thisObj.self.getStr("global", nil); v != nil { - global = v.ToBoolean() + size := 0 + if v := thisObj.self.getStr("global", nil); v != nil { + global = v.ToBoolean() + if global { + size++ } - if v := thisObj.self.getStr("ignoreCase", nil); v != nil { - ignoreCase = v.ToBoolean() + } + if v := thisObj.self.getStr("ignoreCase", nil); v != nil { + ignoreCase = v.ToBoolean() + if ignoreCase { + size++ } - if v := thisObj.self.getStr("multiline", nil); v != nil { - multiline = v.ToBoolean() + } + if v := thisObj.self.getStr("multiline", nil); v != nil { + multiline = v.ToBoolean() + if multiline { + size++ } - if v := thisObj.self.getStr("sticky", nil); v != nil { - sticky = v.ToBoolean() + } + if v := thisObj.self.getStr("sticky", nil); v != nil { + sticky = v.ToBoolean() + if sticky { + size++ + } + } + if v := thisObj.self.getStr("unicode", nil); v != nil { + unicode = v.ToBoolean() + if unicode { + size++ } } var sb strings.Builder + sb.Grow(size) if global { sb.WriteByte('g') } @@ -295,6 +607,9 @@ func (r *Runtime) regexpproto_getFlags(call FunctionCall) Value { if multiline { sb.WriteByte('m') } + if unicode { + sb.WriteByte('u') + } if sticky { sb.WriteByte('y') } @@ -317,32 +632,45 @@ func (r *Runtime) regExpExec(execFn func(FunctionCall) Value, rxObj *Object, arg return res } -func (r *Runtime) regexpproto_stdMatcherGeneric(rxObj *Object, arg Value) Value { +func (r *Runtime) getGlobalRegexpMatches(rxObj *Object, s valueString) []Value { + fullUnicode := nilSafe(rxObj.self.getStr("unicode", nil)).ToBoolean() + rxObj.self.setOwnStr("lastIndex", intToValue(0), true) + execFn, ok := r.toObject(rxObj.self.getStr("exec", nil)).self.assertCallable() + if !ok { + panic(r.NewTypeError("exec is not a function")) + } + var a []Value + for { + res := r.regExpExec(execFn, rxObj, s) + if res == _null { + break + } + a = append(a, res) + matchStr := nilSafe(r.toObject(res).self.getIdx(valueInt(0), nil)).toString() + if matchStr.length() == 0 { + thisIndex := toInt(nilSafe(rxObj.self.getStr("lastIndex", nil)).ToInteger()) + rxObj.self.setOwnStr("lastIndex", valueInt(advanceStringIndex(s, thisIndex, fullUnicode)), true) + } + } + + return a +} + +func (r *Runtime) regexpproto_stdMatcherGeneric(rxObj *Object, s valueString) Value { rx := rxObj.self global := rx.getStr("global", nil) if global != nil && global.ToBoolean() { - rx.setOwnStr("lastIndex", intToValue(0), true) - execFn, ok := r.toObject(rx.getStr("exec", nil)).self.assertCallable() - if !ok { - panic(r.NewTypeError("exec is not a function")) - } - var a []Value - for { - res := r.regExpExec(execFn, rxObj, arg) - if res == _null { - break - } - matchStr := nilSafe(r.toObject(res).self.getIdx(valueInt(0), nil)).toString() - a = append(a, matchStr) - if matchStr.length() == 0 { - thisIndex := rx.getStr("lastIndex", nil).ToInteger() - rx.setOwnStr("lastIndex", intToValue(thisIndex+1), true) // TODO fullUnicode - } - } + a := r.getGlobalRegexpMatches(rxObj, s) if len(a) == 0 { return _null } - return r.newArrayValues(a) + ar := make([]Value, 0, len(a)) + for _, result := range a { + obj := r.toObject(result) + matchStr := nilSafe(obj.self.getIdx(valueInt(0), nil)).ToString() + ar = append(ar, matchStr) + } + return r.newArrayValues(ar) } execFn, ok := r.toObject(rx.getStr("exec", nil)).self.assertCallable() @@ -350,16 +678,20 @@ func (r *Runtime) regexpproto_stdMatcherGeneric(rxObj *Object, arg Value) Value panic(r.NewTypeError("exec is not a function")) } - return r.regExpExec(execFn, rxObj, arg) + return r.regExpExec(execFn, rxObj, s) } func (r *Runtime) checkStdRegexp(rxObj *Object) *regexpObject { + if deoptimiseRegexp { + return nil + } + rx, ok := rxObj.self.(*regexpObject) if !ok { return nil } - execFn := rx.getStr("exec", nil) - if execFn != nil && execFn != r.global.regexpProtoExec { + + if !rx.standard || rx.prototype == nil || rx.prototype.self != r.global.stdRegexpProto { return nil } @@ -373,7 +705,7 @@ func (r *Runtime) regexpproto_stdMatcher(call FunctionCall) Value { if rx == nil { return r.regexpproto_stdMatcherGeneric(thisObj, s) } - if rx.global { + if rx.pattern.global { rx.setOwnStr("lastIndex", intToValue(0), true) var a []Value var previousLastIndex int64 @@ -384,7 +716,7 @@ func (r *Runtime) regexpproto_stdMatcher(call FunctionCall) Value { } thisIndex := rx.getStr("lastIndex", nil).ToInteger() if thisIndex == previousLastIndex { - previousLastIndex++ + previousLastIndex = int64(advanceStringIndex(s, toInt(previousLastIndex), rx.pattern.unicode)) rx.setOwnStr("lastIndex", intToValue(previousLastIndex), true) } else { previousLastIndex = thisIndex @@ -402,15 +734,21 @@ func (r *Runtime) regexpproto_stdMatcher(call FunctionCall) Value { func (r *Runtime) regexpproto_stdSearchGeneric(rxObj *Object, arg valueString) Value { rx := rxObj.self - previousLastIndex := rx.getStr("lastIndex", nil) - rx.setOwnStr("lastIndex", intToValue(0), true) + previousLastIndex := nilSafe(rx.getStr("lastIndex", nil)) + zero := intToValue(0) + if !previousLastIndex.SameAs(zero) { + rx.setOwnStr("lastIndex", zero, true) + } execFn, ok := r.toObject(rx.getStr("exec", nil)).self.assertCallable() if !ok { panic(r.NewTypeError("exec is not a function")) } result := r.regExpExec(execFn, rxObj, arg) - rx.setOwnStr("lastIndex", previousLastIndex, true) + currentLastIndex := nilSafe(rx.getStr("lastIndex", nil)) + if !currentLastIndex.SameAs(previousLastIndex) { + rx.setOwnStr("lastIndex", previousLastIndex, true) + } if result == _null { return intToValue(-1) @@ -439,7 +777,7 @@ func (r *Runtime) regexpproto_stdSearch(call FunctionCall) Value { return intToValue(int64(result[0])) } -func (r *Runtime) regexpproto_stdSplitterGeneric(splitter *Object, s valueString, limit Value) Value { +func (r *Runtime) regexpproto_stdSplitterGeneric(splitter *Object, s valueString, limit Value, unicodeMatching bool) Value { var a []Value var lim int64 if limit == nil || limit == _undefined { @@ -466,12 +804,12 @@ func (r *Runtime) regexpproto_stdSplitterGeneric(splitter *Object, s valueString splitter.self.setOwnStr("lastIndex", intToValue(int64(q)), true) z := r.regExpExec(execFn, splitter, s) if z == _null { - q++ + q = advanceStringIndex(s, q, unicodeMatching) } else { z := r.toObject(z) e := toLength(splitter.self.getStr("lastIndex", nil)) if e == int64(p) { - q++ + q = advanceStringIndex(s, q, unicodeMatching) } else { a = append(a, s.substring(p, q)) if int64(len(a)) == lim { @@ -497,13 +835,32 @@ func (r *Runtime) regexpproto_stdSplitterGeneric(splitter *Object, s valueString return r.newArrayValues(a) } +func advanceStringIndex(s valueString, pos int, unicode bool) int { + next := pos + 1 + if !unicode { + return next + } + l := s.length() + if next >= l { + return next + } + if !isUTF16FirstSurrogate(s.charAt(pos)) { + return next + } + if !isUTF16SecondSurrogate(s.charAt(next)) { + return next + } + return next + 1 +} + func (r *Runtime) regexpproto_stdSplitter(call FunctionCall) Value { rxObj := r.toObject(call.This) c := r.speciesConstructor(rxObj, r.global.RegExp) flags := nilSafe(rxObj.self.getStr("flags", nil)).toString() + flagsStr := flags.String() // Add 'y' flag if missing - if flagsStr := flags.String(); !strings.Contains(flagsStr, "y") { + if !strings.Contains(flagsStr, "y") { flags = newStringValue(flagsStr + "y") } splitter := c([]Value{rxObj, flags}, nil) @@ -512,7 +869,7 @@ func (r *Runtime) regexpproto_stdSplitter(call FunctionCall) Value { limitValue := call.Argument(1) search := r.checkStdRegexp(splitter) if search == nil { - return r.regexpproto_stdSplitterGeneric(splitter, s, limitValue) + return r.regexpproto_stdSplitterGeneric(splitter, s, limitValue, strings.Contains(flagsStr, "u")) } limit := -1 @@ -526,10 +883,17 @@ func (r *Runtime) regexpproto_stdSplitter(call FunctionCall) Value { targetLength := s.length() var valueArray []Value - result := search.pattern.FindAllSubmatchIndex(s, -1) lastIndex := 0 found := 0 + result := search.pattern.findAllSubmatchIndex(s, 0, -1, false) + if targetLength == 0 { + if result == nil { + valueArray = append(valueArray, s) + } + goto RETURN + } + for _, match := range result { if match[0] == match[1] { // FIXME Ugh, this is a hack @@ -582,11 +946,161 @@ RETURN: return r.newArrayValues(valueArray) } +func (r *Runtime) regexpproto_stdReplacerGeneric(rxObj *Object, s, replaceStr valueString, rcall func(FunctionCall) Value) Value { + var results []Value + if nilSafe(rxObj.self.getStr("global", nil)).ToBoolean() { + results = r.getGlobalRegexpMatches(rxObj, s) + } else { + execFn := toMethod(rxObj.self.getStr("exec", nil)) // must be non-nil + result := r.regExpExec(execFn, rxObj, s) + if result != _null { + results = append(results, result) + } + } + lengthS := s.length() + nextSourcePosition := 0 + var resultBuf valueStringBuilder + for _, result := range results { + obj := r.toObject(result) + nCaptures := max(toLength(obj.self.getStr("length", nil))-1, 0) + matched := nilSafe(obj.self.getIdx(valueInt(0), nil)).toString() + matchLength := matched.length() + position := toInt(max(min(nilSafe(obj.self.getStr("index", nil)).ToInteger(), int64(lengthS)), 0)) + var captures []Value + if rcall != nil { + captures = make([]Value, 0, nCaptures+3) + } else { + captures = make([]Value, 0, nCaptures+1) + } + captures = append(captures, matched) + for n := int64(1); n <= nCaptures; n++ { + capN := nilSafe(obj.self.getIdx(valueInt(n), nil)) + if capN != _undefined { + capN = capN.ToString() + } + captures = append(captures, capN) + } + var replacement valueString + if rcall != nil { + captures = append(captures, intToValue(int64(position)), s) + replacement = rcall(FunctionCall{ + This: _undefined, + Arguments: captures, + }).toString() + if position >= nextSourcePosition { + resultBuf.WriteString(s.substring(nextSourcePosition, position)) + resultBuf.WriteString(replacement) + nextSourcePosition = position + matchLength + } + } else { + if position >= nextSourcePosition { + resultBuf.WriteString(s.substring(nextSourcePosition, position)) + writeSubstitution(s, position, len(captures), func(idx int) valueString { + capture := captures[idx] + if capture != _undefined { + return capture.toString() + } + return stringEmpty + }, replaceStr, &resultBuf) + nextSourcePosition = position + matchLength + } + } + } + if nextSourcePosition < lengthS { + resultBuf.WriteString(s.substring(nextSourcePosition, lengthS)) + } + return resultBuf.String() +} + +func writeSubstitution(s valueString, position int, numCaptures int, getCapture func(int) valueString, replaceStr valueString, buf *valueStringBuilder) { + l := s.length() + rl := replaceStr.length() + matched := getCapture(0) + tailPos := position + matched.length() + + for i := 0; i < rl; i++ { + c := replaceStr.charAt(i) + if c == '$' && i < rl-1 { + ch := replaceStr.charAt(i + 1) + switch ch { + case '$': + buf.WriteRune('$') + case '`': + buf.WriteString(s.substring(0, position)) + case '\'': + if tailPos < l { + buf.WriteString(s.substring(tailPos, l)) + } + case '&': + buf.WriteString(matched) + default: + matchNumber := 0 + j := i + 1 + for j < rl { + ch := replaceStr.charAt(j) + if ch >= '0' && ch <= '9' { + m := matchNumber*10 + int(ch-'0') + if m >= numCaptures { + break + } + matchNumber = m + j++ + } else { + break + } + } + if matchNumber > 0 { + buf.WriteString(getCapture(matchNumber)) + i = j - 1 + continue + } else { + buf.WriteRune('$') + buf.WriteRune(ch) + } + } + i++ + } else { + buf.WriteRune(c) + } + } +} + +func (r *Runtime) regexpproto_stdReplacer(call FunctionCall) Value { + rxObj := r.toObject(call.This) + s := call.Argument(0).toString() + replaceStr, rcall := getReplaceValue(call.Argument(1)) + + rx := r.checkStdRegexp(rxObj) + if rx == nil { + return r.regexpproto_stdReplacerGeneric(rxObj, s, replaceStr, rcall) + } + + var index int64 + find := 1 + if rx.pattern.global { + find = -1 + rx.setOwnStr("lastIndex", intToValue(0), true) + } else { + index = rx.getLastIndex() + } + found := rx.pattern.findAllSubmatchIndex(s, toInt(index), find, rx.pattern.sticky) + if len(found) > 0 { + if !rx.updateLastIndex(index, found[0], found[len(found)-1]) { + found = nil + } + } else { + rx.updateLastIndex(index, nil, nil) + } + + return stringReplace(s, found, replaceStr, rcall) +} + func (r *Runtime) initRegExp() { - r.global.RegExpPrototype = r.NewObject() - o := r.global.RegExpPrototype.self - r.global.regexpProtoExec = valueProp(r.newNativeFunc(r.regexpproto_exec, nil, "exec", nil, 1), true, false, true) - o.setOwnStr("exec", r.global.regexpProtoExec, true) + o := r.newGuardedObject(r.global.ObjectPrototype, classObject) + r.global.RegExpPrototype = o.val + r.global.stdRegexpProto = o + o._putProp("compile", r.newNativeFunc(r.regexpproto_compile, nil, "compile", nil, 2), true, false, true) + o._putProp("exec", r.newNativeFunc(r.regexpproto_exec, nil, "exec", nil, 1), true, false, true) o._putProp("test", r.newNativeFunc(r.regexpproto_test, nil, "test", nil, 1), true, false, true) o._putProp("toString", r.newNativeFunc(r.regexpproto_toString, nil, "toString", nil, 0), true, false, true) o.setOwnStr("source", &valueProperty{ @@ -609,6 +1123,11 @@ func (r *Runtime) initRegExp() { getterFunc: r.newNativeFunc(r.regexpproto_getIgnoreCase, nil, "get ignoreCase", nil, 0), accessor: true, }, false) + o.setOwnStr("unicode", &valueProperty{ + configurable: true, + getterFunc: r.newNativeFunc(r.regexpproto_getUnicode, nil, "get unicode", nil, 0), + accessor: true, + }, false) o.setOwnStr("sticky", &valueProperty{ configurable: true, getterFunc: r.newNativeFunc(r.regexpproto_getSticky, nil, "get sticky", nil, 0), @@ -623,10 +1142,12 @@ func (r *Runtime) initRegExp() { o._putSym(symMatch, valueProp(r.newNativeFunc(r.regexpproto_stdMatcher, nil, "[Symbol.match]", nil, 1), true, false, true)) o._putSym(symSearch, valueProp(r.newNativeFunc(r.regexpproto_stdSearch, nil, "[Symbol.search]", nil, 1), true, false, true)) o._putSym(symSplit, valueProp(r.newNativeFunc(r.regexpproto_stdSplitter, nil, "[Symbol.split]", nil, 2), true, false, true)) + o._putSym(symReplace, valueProp(r.newNativeFunc(r.regexpproto_stdReplacer, nil, "[Symbol.replace]", nil, 2), true, false, true)) + o.guard("exec", "global", "multiline", "ignoreCase", "unicode", "sticky") r.global.RegExp = r.newNativeFunc(r.builtin_RegExp, r.builtin_newRegExp, "RegExp", r.global.RegExpPrototype, 2) - o = r.global.RegExp.self - o._putSym(symSpecies, &valueProperty{ + rx := r.global.RegExp.self + rx._putSym(symSpecies, &valueProperty{ getterFunc: r.newNativeFunc(r.returnThis, nil, "get [Symbol.species]", nil, 0), accessor: true, configurable: true, diff --git a/builtin_string.go b/builtin_string.go index 6373520c..9039c726 100644 --- a/builtin_string.go +++ b/builtin_string.go @@ -1,7 +1,6 @@ package goja import ( - "bytes" "github.com/dop251/goja/unistring" "math" "strings" @@ -67,20 +66,6 @@ func (r *Runtime) builtin_newString(args []Value, proto *Object) *Object { return r._newString(s, proto) } -func searchSubstringUTF8(str, search string) (ret [][]int) { - searchPos := 0 - l := len(str) - if searchPos < l { - p := strings.Index(str[searchPos:], search) - if p != -1 { - p += searchPos - searchPos = p + len(search) - ret = append(ret, []int{p, searchPos}) - } - } - return -} - func (r *Runtime) stringproto_toStringValueOf(this Value, funcName string) Value { if str, ok := this.(valueString); ok { return str @@ -132,10 +117,8 @@ func (r *Runtime) string_fromcharcode(call FunctionCall) Value { } func (r *Runtime) string_fromcodepoint(call FunctionCall) Value { - var b []byte - var sb unicodeStringBuilder - unicode := false - for i, arg := range call.Arguments { + var sb valueStringBuilder + for _, arg := range call.Arguments { num := arg.ToNumber() var c rune if numInt, ok := num.(valueInt); ok { @@ -146,27 +129,9 @@ func (r *Runtime) string_fromcodepoint(call FunctionCall) Value { } else { panic(r.newError(r.global.RangeError, "Invalid code point %s", num)) } - if c >= utf8.RuneSelf { - if !unicode { - unicode = true - sb.Grow(len(call.Arguments)) - sb.writeASCII(b[:i]) - b = nil - } - } - if unicode { - sb.writeRune(c) - } else { - if b == nil { - b = make([]byte, 0, len(call.Arguments)) - } - b = append(b, byte(c)) - } - } - if !unicode { - return asciiString(b) + sb.WriteRune(c) } - return sb.string() + return sb.String() } func (r *Runtime) string_raw(call FunctionCall) Value { @@ -176,17 +141,17 @@ func (r *Runtime) string_raw(call FunctionCall) Value { if literalSegments <= 0 { return stringEmpty } - var stringElements unicodeStringBuilder + var stringElements valueStringBuilder nextIndex := int64(0) numberOfSubstitutions := int64(len(call.Arguments) - 1) for { nextSeg := nilSafe(raw.self.getIdx(valueInt(nextIndex), nil)).toString() - stringElements.writeString(nextSeg) + stringElements.WriteString(nextSeg) if nextIndex+1 == literalSegments { - return stringElements.string() + return stringElements.String() } if nextIndex < numberOfSubstitutions { - stringElements.writeString(nilSafe(call.Arguments[nextIndex+1]).toString()) + stringElements.WriteString(nilSafe(call.Arguments[nextIndex+1]).toString()) } nextIndex++ } @@ -250,7 +215,8 @@ func (r *Runtime) stringproto_concat(call FunctionCall) Value { } if allAscii { - buf := bytes.NewBuffer(make([]byte, 0, totalLen)) + var buf strings.Builder + buf.Grow(totalLen) for _, s := range strs { buf.WriteString(s.String()) } @@ -369,8 +335,8 @@ func (r *Runtime) stringproto_lastIndexOf(call FunctionCall) Value { func (r *Runtime) stringproto_localeCompare(call FunctionCall) Value { r.checkObjectCoercible(call.This) - this := norm.NFD.String(call.This.String()) - that := norm.NFD.String(call.Argument(0).String()) + this := norm.NFD.String(call.This.toString().String()) + that := norm.NFD.String(call.Argument(0).toString().String()) return intToValue(int64(r.collator().CompareString(this, that))) } @@ -392,7 +358,7 @@ func (r *Runtime) stringproto_match(call FunctionCall) Value { } if rx == nil { - rx = r.builtin_newRegExp([]Value{regexp}, r.global.RegExpPrototype).self.(*regexpObject) + rx = r.newRegExp(regexp, nil, r.global.RegExpPrototype).self.(*regexpObject) } if matcher, ok := r.toObject(rx.getSym(symMatch, nil)).self.assertCallable(); ok { @@ -410,7 +376,7 @@ func (r *Runtime) stringproto_normalize(call FunctionCall) Value { s := call.This.toString() var form string if formArg := call.Argument(0); formArg != _undefined { - form = formArg.toString().String() + form = formArg.toString().toString().String() } else { form = "NFC" } @@ -475,17 +441,17 @@ func (r *Runtime) stringproto_padEnd(call FunctionCall) Value { } var sb unicodeStringBuilder sb.Grow(toInt(maxLength)) - sb.writeString(s) + sb.WriteString(s) fl := filler.length() for remaining >= fl { - sb.writeString(filler) + sb.WriteString(filler) remaining -= fl } if remaining > 0 { - sb.writeString(filler.substring(0, remaining)) + sb.WriteString(filler.substring(0, remaining)) } - return sb.string() + return sb.String() } func (r *Runtime) stringproto_padStart(call FunctionCall) Value { @@ -529,15 +495,15 @@ func (r *Runtime) stringproto_padStart(call FunctionCall) Value { sb.Grow(toInt(maxLength)) fl := filler.length() for remaining >= fl { - sb.writeString(filler) + sb.WriteString(filler) remaining -= fl } if remaining > 0 { - sb.writeString(filler.substring(0, remaining)) + sb.WriteString(filler.substring(0, remaining)) } - sb.writeString(s) + sb.WriteString(s) - return sb.string() + return sb.String() } func (r *Runtime) stringproto_repeat(call FunctionCall) Value { @@ -567,76 +533,42 @@ func (r *Runtime) stringproto_repeat(call FunctionCall) Value { var sb unicodeStringBuilder sb.Grow(s.length() * num) for i := 0; i < num; i++ { - sb.writeString(s) + sb.WriteString(s) } - return sb.string() + return sb.String() } -func (r *Runtime) stringproto_replace(call FunctionCall) Value { - r.checkObjectCoercible(call.This) - searchValue := call.Argument(0) - replaceValue := call.Argument(1) - if searchValue != _undefined && searchValue != _null { - if replacer := toMethod(r.getV(searchValue, symReplace)); replacer != nil { - return replacer(FunctionCall{ - This: searchValue, - Arguments: []Value{call.This, replaceValue}, - }) +func getReplaceValue(replaceValue Value) (str valueString, rcall func(FunctionCall) Value) { + if replaceValue, ok := replaceValue.(*Object); ok { + if c, ok := replaceValue.self.assertCallable(); ok { + rcall = c + return } } + str = replaceValue.toString() + return +} + +func stringReplace(s valueString, found [][]int, newstring valueString, rcall func(FunctionCall) Value) Value { + if len(found) == 0 { + return s + } - s := call.This.toString() var str string var isASCII bool if astr, ok := s.(asciiString); ok { str = string(astr) isASCII = true - } else { - str = s.String() - } - - var found [][]int - - if searchValue, ok := searchValue.(*Object); ok { - if regexp, ok := searchValue.self.(*regexpObject); ok { - find := 1 - if regexp.global { - find = -1 - } - if isASCII { - found = regexp.pattern.FindAllSubmatchIndexASCII(str, find) - } else { - found = regexp.pattern.FindAllSubmatchIndexUTF8(str, find) - } - if found == nil { - return s - } - } } - if found == nil { - found = searchSubstringUTF8(str, searchValue.String()) - } - - if len(found) == 0 { - return s - } + var buf valueStringBuilder - var buf bytes.Buffer lastIndex := 0 - - var rcall func(FunctionCall) Value - - if replaceValue, ok := replaceValue.(*Object); ok { - if c, ok := replaceValue.self.assertCallable(); ok { - rcall = c - } - } - + lengthS := s.length() if rcall != nil { for _, item := range found { if item[0] != lastIndex { - buf.WriteString(str[lastIndex:item[0]]) + buf.WriteSubstring(s, lastIndex, item[0]) } matchCount := len(item) / 2 argumentList := make([]Value, matchCount+2) @@ -646,7 +578,7 @@ func (r *Runtime) stringproto_replace(call FunctionCall) Value { if isASCII { argumentList[index] = asciiString(str[item[offset]:item[offset+1]]) } else { - argumentList[index] = newStringValue(str[item[offset]:item[offset+1]]) + argumentList[index] = s.substring(item[offset], item[offset+1]) } } else { argumentList[index] = _undefined @@ -657,71 +589,59 @@ func (r *Runtime) stringproto_replace(call FunctionCall) Value { replacement := rcall(FunctionCall{ This: _undefined, Arguments: argumentList, - }).String() + }).toString() buf.WriteString(replacement) lastIndex = item[1] } } else { - newstring := replaceValue.String() - for _, item := range found { if item[0] != lastIndex { - buf.WriteString(str[lastIndex:item[0]]) + buf.WriteString(s.substring(lastIndex, item[0])) } - matches := len(item) / 2 - for i := 0; i < len(newstring); i++ { - if newstring[i] == '$' && i < len(newstring)-1 { - ch := newstring[i+1] - switch ch { - case '$': - buf.WriteByte('$') - case '`': - buf.WriteString(str[0:item[0]]) - case '\'': - buf.WriteString(str[item[1]:]) - case '&': - buf.WriteString(str[item[0]:item[1]]) - default: - matchNumber := 0 - l := 0 - for _, ch := range newstring[i+1:] { - if ch >= '0' && ch <= '9' { - m := matchNumber*10 + int(ch-'0') - if m >= matches { - break - } - matchNumber = m - l++ - } else { - break - } - } - if l > 0 { - offset := 2 * matchNumber - if offset < len(item) && item[offset] != -1 { - buf.WriteString(str[item[offset]:item[offset+1]]) - } - i += l - 1 - } else { - buf.WriteByte('$') - buf.WriteByte(ch) - } - + matchCount := len(item) / 2 + writeSubstitution(s, item[0], matchCount, func(idx int) valueString { + if item[idx*2] != -1 { + if isASCII { + return asciiString(str[item[idx*2]:item[idx*2+1]]) } - i++ - } else { - buf.WriteByte(newstring[i]) + return s.substring(item[idx*2], item[idx*2+1]) } - } + return stringEmpty + }, newstring, &buf) lastIndex = item[1] } } - if lastIndex != len(str) { - buf.WriteString(str[lastIndex:]) + if lastIndex != lengthS { + buf.WriteString(s.substring(lastIndex, lengthS)) + } + + return buf.String() +} + +func (r *Runtime) stringproto_replace(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + searchValue := call.Argument(0) + replaceValue := call.Argument(1) + if searchValue != _undefined && searchValue != _null { + if replacer := toMethod(r.getV(searchValue, symReplace)); replacer != nil { + return replacer(FunctionCall{ + This: searchValue, + Arguments: []Value{call.This, replaceValue}, + }) + } + } + + s := call.This.toString() + var found [][]int + searchStr := searchValue.toString() + pos := s.index(searchStr, 0) + if pos != -1 { + found = append(found, []int{pos, pos + searchStr.length()}) } - return newStringValue(buf.String()) + str, rcall := getReplaceValue(replaceValue) + return stringReplace(s, found, str, rcall) } func (r *Runtime) stringproto_search(call FunctionCall) Value { @@ -742,7 +662,7 @@ func (r *Runtime) stringproto_search(call FunctionCall) Value { } if rx == nil { - rx = r.builtin_newRegExp([]Value{regexp}, r.global.RegExpPrototype).self.(*regexpObject) + rx = r.newRegExp(regexp, nil, r.global.RegExpPrototype).self.(*regexpObject) } if searcher, ok := r.toObject(rx.getSym(symSearch, nil)).self.assertCallable(); ok { @@ -823,7 +743,7 @@ func (r *Runtime) stringproto_split(call FunctionCall) Value { return r.newArrayValues([]Value{s}) } - separator := separatorValue.String() + separator := separatorValue.toString().String() excess := false str := s.String() @@ -836,6 +756,7 @@ func (r *Runtime) stringproto_split(call FunctionCall) Value { excess = true } + // TODO handle invalid UTF-16 split := strings.SplitN(str, separator, splitLimit) if excess && len(split) > limit { @@ -925,6 +846,7 @@ func (r *Runtime) stringproto_trim(call FunctionCall) Value { r.checkObjectCoercible(call.This) s := call.This.toString() + // TODO handle invalid UTF-16 return newStringValue(strings.Trim(s.String(), parser.WhitespaceChars)) } @@ -932,6 +854,7 @@ func (r *Runtime) stringproto_trimEnd(call FunctionCall) Value { r.checkObjectCoercible(call.This) s := call.This.toString() + // TODO handle invalid UTF-16 return newStringValue(strings.TrimRight(s.String(), parser.WhitespaceChars)) } @@ -939,6 +862,7 @@ func (r *Runtime) stringproto_trimStart(call FunctionCall) Value { r.checkObjectCoercible(call.This) s := call.This.toString() + // TODO handle invalid UTF-16 return newStringValue(strings.TrimLeft(s.String(), parser.WhitespaceChars)) } diff --git a/builtin_string_test.go b/builtin_string_test.go index dbbb71c7..5ad3aede 100644 --- a/builtin_string_test.go +++ b/builtin_string_test.go @@ -167,3 +167,61 @@ if (result.value !== pair) { ` testScript1(SCRIPT, _undefined, t) } + +func TestValueStringBuilder(t *testing.T) { + t.Run("substringASCII", func(t *testing.T) { + t.Parallel() + var sb valueStringBuilder + str := newStringValue("a\U00010000b") + sb.WriteSubstring(str, 0, 1) + res := sb.String() + if res != asciiString("a") { + t.Fatal(res) + } + }) + + t.Run("substringASCIIPure", func(t *testing.T) { + t.Parallel() + var sb valueStringBuilder + str := newStringValue("ab") + sb.WriteSubstring(str, 0, 1) + res := sb.String() + if res != asciiString("a") { + t.Fatal(res) + } + }) + + t.Run("substringUnicode", func(t *testing.T) { + t.Parallel() + var sb valueStringBuilder + str := newStringValue("a\U00010000b") + sb.WriteSubstring(str, 1, 3) + res := sb.String() + if !res.SameAs(unicodeStringFromRunes([]rune{0x10000})) { + t.Fatal(res) + } + }) + + t.Run("substringASCIIUnicode", func(t *testing.T) { + t.Parallel() + var sb valueStringBuilder + str := newStringValue("a\U00010000b") + sb.WriteSubstring(str, 0, 2) + res := sb.String() + if !res.SameAs(unicodeStringFromRunes([]rune{'a', 0xD800})) { + t.Fatal(res) + } + }) + + t.Run("substringUnicodeASCII", func(t *testing.T) { + t.Parallel() + var sb valueStringBuilder + str := newStringValue("a\U00010000b") + sb.WriteSubstring(str, 2, 4) + res := sb.String() + if !res.SameAs(unicodeStringFromRunes([]rune{0xDC00, 'b'})) { + t.Fatal(res) + } + }) + +} diff --git a/builtin_typedarrays.go b/builtin_typedarrays.go index d7384fbc..6027add0 100644 --- a/builtin_typedarrays.go +++ b/builtin_typedarrays.go @@ -4,7 +4,6 @@ import ( "fmt" "math" "sort" - "strings" "unsafe" "github.com/dop251/goja/unistring" @@ -644,23 +643,23 @@ func (r *Runtime) typedArrayProto_join(call FunctionCall) Value { if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { ta.viewedArrayBuf.ensureNotDetached() s := call.Argument(0) - sep := "" + sep := stringEmpty if s != _undefined { - sep = s.toString().String() + sep = s.toString() } else { - sep = "," + sep = asciiString(",") } l := ta.length if l == 0 { return stringEmpty } - var buf strings.Builder + var buf valueStringBuilder ta.viewedArrayBuf.ensureNotDetached() element0 := ta.typedArray.get(0) if element0 != nil && element0 != _undefined && element0 != _null { - buf.WriteString(element0.String()) + buf.WriteString(element0.toString()) } for i := 1; i < l; i++ { @@ -668,11 +667,11 @@ func (r *Runtime) typedArrayProto_join(call FunctionCall) Value { buf.WriteString(sep) element := ta.typedArray.get(i) if element != nil && element != _undefined && element != _null { - buf.WriteString(element.String()) + buf.WriteString(element.toString()) } } - return newStringValue(buf.String()) + return buf.String() } panic(r.NewTypeError("Method TypedArray.prototype.join called on incompatible receiver")) } @@ -1002,16 +1001,16 @@ func (r *Runtime) typedArrayProto_subarray(call FunctionCall) Value { func (r *Runtime) typedArrayProto_toLocaleString(call FunctionCall) Value { if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { length := ta.length - var buf strings.Builder + var buf valueStringBuilder for i := 0; i < length; i++ { ta.viewedArrayBuf.ensureNotDetached() if i > 0 { - buf.WriteByte(',') + buf.WriteRune(',') } item := ta.typedArray.get(i) r.writeItemLocaleString(item, &buf) } - return newStringValue(buf.String()) + return buf.String() } panic(r.NewTypeError("Method TypedArray.prototype.toLocaleString called on incompatible receiver %s", call.This.String())) } diff --git a/compiler_expr.go b/compiler_expr.go index 7099cc1f..479f0594 100644 --- a/compiler_expr.go +++ b/compiler_expr.go @@ -993,7 +993,7 @@ func (c *compiler) compileSequenceExpression(v *ast.SequenceExpression) compiled func (c *compiler) emitThrow(v Value) { if o, ok := v.(*Object); ok { - t := o.self.getStr("name", nil).String() + t := nilSafe(o.self.getStr("name", nil)).toString().String() switch t { case "TypeError": c.emit(getVar1(t)) @@ -1008,7 +1008,7 @@ func (c *compiler) emitThrow(v Value) { return } } - panic(fmt.Errorf("Unknown exception type thrown while evaliating constant expression: %s", v.String())) + panic(fmt.Errorf("unknown exception type thrown while evaliating constant expression: %s", v.String())) } func (c *compiler) emitConst(expr compiledExpr, putOnStack bool) { @@ -1440,18 +1440,12 @@ func (c *compiler) compileArrayLiteral(v *ast.ArrayLiteral) compiledExpr { func (e *compiledRegexpLiteral) emitGetter(putOnStack bool) { if putOnStack { - pattern, global, ignoreCase, multiline, sticky, err := compileRegexp(e.expr.Pattern, e.expr.Flags) + pattern, err := compileRegexp(e.expr.Pattern, e.expr.Flags) if err != nil { e.c.throwSyntaxError(e.offset, err.Error()) } - e.c.emit(&newRegexp{pattern: pattern, - src: newStringValue(e.expr.Pattern), - global: global, - ignoreCase: ignoreCase, - multiline: multiline, - sticky: sticky, - }) + e.c.emit(&newRegexp{pattern: pattern, src: newStringValue(e.expr.Pattern)}) } } diff --git a/object.go b/object.go index 70c86ae7..c25637de 100644 --- a/object.go +++ b/object.go @@ -252,6 +252,11 @@ type baseObject struct { symValues *orderedMap } +type guardedObject struct { + baseObject + guardedProps map[unistring.String]struct{} +} + type primitiveValueObject struct { baseObject pValue Value @@ -1405,3 +1410,42 @@ func (o *Object) getId() uint64 { } return o.id } + +func (o *guardedObject) guard(props ...unistring.String) { + if o.guardedProps == nil { + o.guardedProps = make(map[unistring.String]struct{}) + } + for _, p := range props { + o.guardedProps[p] = struct{}{} + } +} + +func (o *guardedObject) check(p unistring.String) { + if _, exists := o.guardedProps[p]; exists { + o.val.self = &o.baseObject + } +} + +func (o *guardedObject) setOwnStr(p unistring.String, v Value, throw bool) bool { + res := o.baseObject.setOwnStr(p, v, throw) + if res { + o.check(p) + } + return res +} + +func (o *guardedObject) defineOwnPropertyStr(name unistring.String, desc PropertyDescriptor, throw bool) bool { + res := o.baseObject.defineOwnPropertyStr(name, desc, throw) + if res { + o.check(name) + } + return res +} + +func (o *guardedObject) deleteStr(name unistring.String, throw bool) bool { + res := o.baseObject.deleteStr(name, throw) + if res { + o.check(name) + } + return res +} diff --git a/regexp.go b/regexp.go index 31205f5a..5303c990 100644 --- a/regexp.go +++ b/regexp.go @@ -3,42 +3,176 @@ package goja import ( "fmt" "github.com/dlclark/regexp2" + "github.com/dop251/goja/unistring" + "io" "regexp" + "sort" + "strings" "unicode/utf16" - "unicode/utf8" ) -type regexpPattern interface { - FindSubmatchIndex(valueString, int) []int - FindAllSubmatchIndex(valueString, int) [][]int - FindAllSubmatchIndexUTF8(string, int) [][]int - FindAllSubmatchIndexASCII(string, int) [][]int - MatchString(valueString) bool -} - type regexp2Wrapper regexp2.Regexp type regexpWrapper regexp.Regexp +type positionMapItem struct { + src, dst int +} +type positionMap []positionMapItem + +func (m positionMap) get(src int) int { + if src == 0 { + return 0 + } + res := sort.Search(len(m), func(n int) bool { return m[n].src >= src }) + if res >= len(m) || m[res].src != src { + panic("index not found") + } + return m[res].dst +} + +type arrayRuneReader struct { + runes []rune + pos int +} + +func (rd *arrayRuneReader) ReadRune() (r rune, size int, err error) { + if rd.pos < len(rd.runes) { + r = rd.runes[rd.pos] + size = 1 + rd.pos++ + } else { + err = io.EOF + } + return +} + +type regexpPattern struct { + src string + + global, ignoreCase, multiline, sticky, unicode bool + + regexpWrapper *regexpWrapper + regexp2Wrapper *regexp2Wrapper +} + +func compileRegexp2(src string, multiline, ignoreCase bool) (*regexp2Wrapper, error) { + var opts regexp2.RegexOptions = regexp2.ECMAScript + if multiline { + opts |= regexp2.Multiline + } + if ignoreCase { + opts |= regexp2.IgnoreCase + } + regexp2Pattern, err1 := regexp2.Compile(src, opts) + if err1 != nil { + return nil, fmt.Errorf("Invalid regular expression (regexp2): %s (%v)", src, err1) + } + + return (*regexp2Wrapper)(regexp2Pattern), nil +} + +func (p *regexpPattern) createRegexp2() { + if p.regexp2Wrapper != nil { + return + } + rx, err := compileRegexp2(p.src, p.multiline, p.ignoreCase) + if err != nil { + // At this point the regexp should have been successfully converted to re2, if it fails now, it's a bug. + panic(err) + } + p.regexp2Wrapper = rx +} + +func buildUTF8PosMap(s valueString) (positionMap, string) { + pm := make(positionMap, 0, s.length()) + rd := s.reader(0) + sPos, utf8Pos := 0, 0 + var sb strings.Builder + for { + r, size, err := rd.ReadRune() + if err == io.EOF { + break + } + if err != nil { + // the string contains invalid UTF-16, bailing out + return nil, "" + } + utf8Size, _ := sb.WriteRune(r) + sPos += size + utf8Pos += utf8Size + pm = append(pm, positionMapItem{src: utf8Pos, dst: sPos}) + } + return pm, sb.String() +} + +func (p *regexpPattern) findSubmatchIndex(s valueString, start int) []int { + if p.regexpWrapper == nil { + return p.regexp2Wrapper.findSubmatchIndex(s, start, p.unicode) + } + if start != 0 { + // Unfortunately Go's regexp library does not allow starting from an arbitrary position. + // If we just drop the first _start_ characters of the string the assertions (^, $, \b and \B) will not + // work correctly. + p.createRegexp2() + return p.regexp2Wrapper.findSubmatchIndex(s, start, p.unicode) + } + return p.regexpWrapper.findSubmatchIndex(s, p.unicode) +} + +func (p *regexpPattern) findAllSubmatchIndex(s valueString, start int, limit int, sticky bool) [][]int { + if p.regexpWrapper == nil { + return p.regexp2Wrapper.findAllSubmatchIndex(s, start, limit, sticky, p.unicode) + } + if start == 0 { + if s, ok := s.(asciiString); ok { + return p.regexpWrapper.findAllSubmatchIndex(s.String(), limit, sticky) + } + if limit == 1 { + result := p.regexpWrapper.findSubmatchIndex(s, p.unicode) + if result == nil { + return nil + } + return [][]int{result} + } + // Unfortunately Go's regexp library lacks FindAllReaderSubmatchIndex(), so we have to use a UTF-8 string as an + // input. + if p.unicode { + // Try to convert s to UTF-8. If it does not contain any invalid UTF-16 we can do the matching in UTF-8. + pm, str := buildUTF8PosMap(s) + if pm != nil { + res := p.regexpWrapper.findAllSubmatchIndex(str, limit, sticky) + for _, result := range res { + for i, idx := range result { + result[i] = pm.get(idx) + } + } + return res + } + } + } + + p.createRegexp2() + return p.regexp2Wrapper.findAllSubmatchIndex(s, start, limit, sticky, p.unicode) +} + type regexpObject struct { baseObject - pattern regexpPattern + pattern *regexpPattern source valueString - global, multiline, ignoreCase, sticky bool + standard bool } -func (r *regexp2Wrapper) FindSubmatchIndex(s valueString, start int) (result []int) { - wrapped := (*regexp2.Regexp)(r) - var match *regexp2.Match - var err error - switch s := s.(type) { - case asciiString: - match, err = wrapped.FindStringMatch(string(s)[start:]) - case unicodeString: - match, err = wrapped.FindRunesMatch(utf16.Decode(s[start+1:])) - default: - panic(fmt.Errorf("Unknown string type: %T", s)) +func (r *regexp2Wrapper) findSubmatchIndex(s valueString, start int, fullUnicode bool) (result []int) { + if fullUnicode { + return r.findSubmatchIndexUnicode(s, start) } + return r.findSubmatchIndexUTF16(s, start) +} + +func (r *regexp2Wrapper) findSubmatchIndexUTF16(s valueString, start int) (result []int) { + wrapped := (*regexp2.Regexp)(r) + match, err := wrapped.FindRunesMatchStartingAt(s.utf16Runes(), start) if err != nil { return } @@ -59,109 +193,116 @@ func (r *regexp2Wrapper) FindSubmatchIndex(s valueString, start int) (result []i return } -func (r *regexp2Wrapper) FindAllSubmatchIndexUTF8(s string, n int) [][]int { +func (r *regexp2Wrapper) findSubmatchIndexUnicode(s valueString, start int) (result []int) { wrapped := (*regexp2.Regexp)(r) - if n < 0 { - n = len(s) + 1 - } - results := make([][]int, 0, n) - - idxMap := make([]int, 0, len(s)) - runes := make([]rune, 0, len(s)) - for pos, rr := range s { - runes = append(runes, rr) - idxMap = append(idxMap, pos) - } - idxMap = append(idxMap, len(s)) - - match, err := wrapped.FindRunesMatch(runes) + posMap, runes, mappedStart := buildPosMap(&lenientUtf16Decoder{utf16Reader: s.utf16Reader(0)}, s.length(), start) + match, err := wrapped.FindRunesMatchStartingAt(runes, mappedStart) if err != nil { - return nil + return } - i := 0 - for match != nil && i < n { - groups := match.Groups() - - result := make([]int, 0, len(groups)<<1) - for _, group := range groups { - if len(group.Captures) > 0 { - result = append(result, idxMap[group.Index], idxMap[group.Index+group.Length]) - } else { - result = append(result, -1, 0) - } - } + if match == nil { + return + } + groups := match.Groups() - results = append(results, result) - match, err = wrapped.FindNextMatch(match) - if err != nil { - return nil + result = make([]int, 0, len(groups)<<1) + for _, group := range groups { + if len(group.Captures) > 0 { + result = append(result, posMap[group.Index], posMap[group.Index+group.Length]) + } else { + result = append(result, -1, 0) } - i++ } - return results + return } -func (r *regexp2Wrapper) FindAllSubmatchIndexASCII(s string, n int) [][]int { +func (r *regexp2Wrapper) findAllSubmatchIndexUTF16(s valueString, start, limit int, sticky bool) [][]int { wrapped := (*regexp2.Regexp)(r) - if n < 0 { - n = len(s) + 1 - } - results := make([][]int, 0, n) - - match, err := wrapped.FindStringMatch(s) + runes := s.utf16Runes() + match, err := wrapped.FindRunesMatchStartingAt(runes, start) if err != nil { return nil } - i := 0 - for match != nil && i < n { + if limit < 0 { + limit = len(runes) + 1 + } + results := make([][]int, 0, limit) + for match != nil { groups := match.Groups() result := make([]int, 0, len(groups)<<1) for _, group := range groups { if len(group.Captures) > 0 { - result = append(result, group.Index, group.Index+group.Length) + startPos := group.Index + endPos := group.Index + group.Length + result = append(result, startPos, endPos) } else { result = append(result, -1, 0) } } + if sticky && len(result) > 1 { + if result[0] != start { + break + } + start = result[1] + } + results = append(results, result) + limit-- + if limit <= 0 { + break + } match, err = wrapped.FindNextMatch(match) if err != nil { return nil } - i++ } return results } -func (r *regexp2Wrapper) findAllSubmatchIndexUTF16(s unicodeString, n int) [][]int { - wrapped := (*regexp2.Regexp)(r) - if n < 0 { - n = len(s) + 1 - } - results := make([][]int, 0, n) - - rd := runeReaderReplace{s.reader(0)} - posMap := make([]int, s.length()+1) +func buildPosMap(rd io.RuneReader, l, start int) (posMap []int, runes []rune, mappedStart int) { + posMap = make([]int, 0, l+1) curPos := 0 - curRuneIdx := 0 - runes := make([]rune, 0, s.length()) + runes = make([]rune, 0, l) + startFound := false for { + if !startFound { + if curPos == start { + mappedStart = len(runes) + startFound = true + } + if curPos > start { + // start position splits a surrogate pair + mappedStart = len(runes) - 1 + _, second := utf16.EncodeRune(runes[mappedStart]) + runes[mappedStart] = second + startFound = true + } + } rn, size, err := rd.ReadRune() if err != nil { break } runes = append(runes, rn) - posMap[curRuneIdx] = curPos - curRuneIdx++ + posMap = append(posMap, curPos) curPos += size } - posMap[curRuneIdx] = curPos + posMap = append(posMap, curPos) + return +} - match, err := wrapped.FindRunesMatch(runes) +func (r *regexp2Wrapper) findAllSubmatchIndexUnicode(s unicodeString, start, limit int, sticky bool) [][]int { + wrapped := (*regexp2.Regexp)(r) + if limit < 0 { + limit = len(s) + 1 + } + results := make([][]int, 0, limit) + posMap, runes, mappedStart := buildPosMap(&lenientUtf16Decoder{utf16Reader: s.utf16Reader(0)}, s.length(), start) + + match, err := wrapped.FindRunesMatchStartingAt(runes, mappedStart) if err != nil { return nil } @@ -180,6 +321,13 @@ func (r *regexp2Wrapper) findAllSubmatchIndexUTF16(s unicodeString, n int) [][]i } } + if sticky && len(result) > 1 { + if result[0] != start { + break + } + start = result[1] + } + results = append(results, result) match, err = wrapped.FindNextMatch(match) if err != nil { @@ -189,94 +337,48 @@ func (r *regexp2Wrapper) findAllSubmatchIndexUTF16(s unicodeString, n int) [][]i return results } -func (r *regexp2Wrapper) FindAllSubmatchIndex(s valueString, n int) [][]int { +func (r *regexp2Wrapper) findAllSubmatchIndex(s valueString, start, limit int, sticky, fullUnicode bool) [][]int { switch s := s.(type) { case asciiString: - return r.FindAllSubmatchIndexASCII(string(s), n) + return r.findAllSubmatchIndexUTF16(s, start, limit, sticky) case unicodeString: - return r.findAllSubmatchIndexUTF16(s, n) + if fullUnicode { + return r.findAllSubmatchIndexUnicode(s, start, limit, sticky) + } + return r.findAllSubmatchIndexUTF16(s, start, limit, sticky) default: panic("Unsupported string type") } } -func (r *regexp2Wrapper) MatchString(s valueString) bool { - wrapped := (*regexp2.Regexp)(r) - - switch s := s.(type) { - case asciiString: - matched, _ := wrapped.MatchString(string(s)) - return matched - case unicodeString: - matched, _ := wrapped.MatchRunes(utf16.Decode(s[1:])) - return matched - default: - panic(fmt.Errorf("Unknown string type: %T", s)) - } -} - -func (r *regexpWrapper) FindSubmatchIndex(s valueString, start int) (result []int) { - wrapped := (*regexp.Regexp)(r) - return wrapped.FindReaderSubmatchIndex(runeReaderReplace{s.reader(start)}) -} - -func (r *regexpWrapper) MatchString(s valueString) bool { - wrapped := (*regexp.Regexp)(r) - return wrapped.MatchReader(runeReaderReplace{s.reader(0)}) -} - -func (r *regexpWrapper) FindAllSubmatchIndex(s valueString, n int) [][]int { +func (r *regexpWrapper) findAllSubmatchIndex(s string, limit int, sticky bool) (results [][]int) { wrapped := (*regexp.Regexp)(r) - switch s := s.(type) { - case asciiString: - return wrapped.FindAllStringSubmatchIndex(string(s), n) - case unicodeString: - return r.findAllSubmatchIndexUTF16(s, n) - default: - panic("Unsupported string type") + results = wrapped.FindAllStringSubmatchIndex(s, limit) + pos := 0 + if sticky { + for i, result := range results { + if len(result) > 1 { + if result[0] != pos { + return results[:i] + } + pos = result[1] + } + } } + return } -func (r *regexpWrapper) FindAllSubmatchIndexUTF8(s string, n int) [][]int { - wrapped := (*regexp.Regexp)(r) - return wrapped.FindAllStringSubmatchIndex(s, n) -} - -func (r *regexpWrapper) FindAllSubmatchIndexASCII(s string, n int) [][]int { - return r.FindAllSubmatchIndexUTF8(s, n) -} - -func (r *regexpWrapper) findAllSubmatchIndexUTF16(s unicodeString, n int) [][]int { +func (r *regexpWrapper) findSubmatchIndex(s valueString, fullUnicode bool) (result []int) { wrapped := (*regexp.Regexp)(r) - utf8Bytes := make([]byte, 0, len(s)*2) - posMap := make(map[int]int) - curPos := 0 - rd := runeReaderReplace{s.reader(0)} - for { - rn, size, err := rd.ReadRune() - if err != nil { - break + if fullUnicode { + posMap, runes, _ := buildPosMap(&lenientUtf16Decoder{utf16Reader: s.utf16Reader(0)}, s.length(), 0) + res := wrapped.FindReaderSubmatchIndex(&arrayRuneReader{runes: runes}) + for i, item := range res { + res[i] = posMap[item] } - l := len(utf8Bytes) - utf8Bytes = append(utf8Bytes, 0, 0, 0, 0) - n := utf8.EncodeRune(utf8Bytes[l:], rn) - utf8Bytes = utf8Bytes[:l+n] - posMap[l] = curPos - curPos += size + return res } - posMap[len(utf8Bytes)] = curPos - - rr := wrapped.FindAllSubmatchIndex(utf8Bytes, n) - for _, res := range rr { - for j, pos := range res { - mapped, exists := posMap[pos] - if !exists { - panic("Unicode match is not on rune boundary") - } - res[j] = mapped - } - } - return rr + return wrapped.FindReaderSubmatchIndex(s.utf16Reader(0)) } func (r *regexpObject) execResultToArray(target valueString, result []int) Value { @@ -299,34 +401,41 @@ func (r *regexpObject) execResultToArray(target valueString, result []int) Value return match } -func (r *regexpObject) execRegexp(target valueString) (match bool, result []int) { - lastIndex := int64(0) - if p := r.getStr("lastIndex", nil); p != nil { - lastIndex = p.ToInteger() - if lastIndex < 0 { - lastIndex = 0 +func (r *regexpObject) getLastIndex() int64 { + lastIndex := toLength(r.getStr("lastIndex", nil)) + if !r.pattern.global && !r.pattern.sticky { + return 0 + } + return lastIndex +} + +func (r *regexpObject) updateLastIndex(index int64, firstResult, lastResult []int) bool { + if r.pattern.sticky { + if firstResult == nil || int64(firstResult[0]) != index { + r.setOwnStr("lastIndex", intToValue(0), true) + return false + } + } else { + if firstResult == nil { + if r.pattern.global { + r.setOwnStr("lastIndex", intToValue(0), true) + } + return false } } - index := lastIndex - if !r.global && !r.sticky { - index = 0 + + if r.pattern.global || r.pattern.sticky { + r.setOwnStr("lastIndex", intToValue(int64(lastResult[1])), true) } + return true +} + +func (r *regexpObject) execRegexp(target valueString) (match bool, result []int) { + index := r.getLastIndex() if index >= 0 && index <= int64(target.length()) { - result = r.pattern.FindSubmatchIndex(target, int(index)) - } - if result == nil || r.sticky && result[0] != 0 { - r.setOwnStr("lastIndex", intToValue(0), true) - return - } - match = true - // We do this shift here because the .FindStringSubmatchIndex above - // was done on a local subordinate slice of the string, not the whole string - for i := range result { - result[i] += int(index) - } - if r.global || r.sticky { - r.setOwnStr("lastIndex", intToValue(int64(result[1])), true) + result = r.pattern.findSubmatchIndex(target, int(index)) } + match = r.updateLastIndex(index, result, result) return } @@ -347,14 +456,49 @@ func (r *regexpObject) clone() *Object { r1 := r.val.runtime.newRegexpObject(r.prototype) r1.source = r.source r1.pattern = r.pattern - r1.global = r.global - r1.ignoreCase = r.ignoreCase - r1.multiline = r.multiline - r1.sticky = r.sticky + return r1.val } func (r *regexpObject) init() { r.baseObject.init() + r.standard = true r._putProp("lastIndex", intToValue(0), true, false, false) } + +func (r *regexpObject) setProto(proto *Object, throw bool) bool { + res := r.baseObject.setProto(proto, throw) + if res { + r.standard = false + } + return res +} + +func (r *regexpObject) defineOwnPropertyStr(name unistring.String, desc PropertyDescriptor, throw bool) bool { + res := r.baseObject.defineOwnPropertyStr(name, desc, throw) + if res { + r.standard = false + } + return res +} + +func (r *regexpObject) deleteStr(name unistring.String, throw bool) bool { + res := r.baseObject.deleteStr(name, throw) + if res { + r.standard = false + } + return res +} + +func (r *regexpObject) setOwnStr(name unistring.String, value Value, throw bool) bool { + if r.standard { + if name == "exec" { + res := r.baseObject.setOwnStr(name, value, throw) + if res { + r.standard = false + } + return res + } + } + return r.baseObject.setOwnStr(name, value, throw) +} diff --git a/regexp_test.go b/regexp_test.go index a6c99200..25fbbbec 100644 --- a/regexp_test.go +++ b/regexp_test.go @@ -190,6 +190,166 @@ func TestEscapeNonASCII(t *testing.T) { testScript1(SCRIPT, valueTrue, t) } +func TestRegexpUTF16(t *testing.T) { + const SCRIPT = ` + var str = "\uD800\uDC00"; + + assert(/\uD800/g.test(str), "#1"); + assert(/\uD800/.test(str), "#2"); + assert(/𐀀/.test(str), "#3"); + + var re = /\uD800/; + + assert(compareArray(str.replace(re, "X"), ["X", "\uDC00"]), "#4"); + assert(compareArray(str.split(re), ["", "\uDC00"]), "#5"); + assert(compareArray("a\uD800\uDC00b".split(/\uD800/g), ["a", "\uDC00b"]), "#6"); + assert(compareArray("a\uD800\uDC00b".split(/(?:)/g), ["a", "\uD800", "\uDC00", "b"]), "#7"); + + re = /(?=)a/; // a hack to use regexp2 + assert.sameValue(re.exec('\ud83d\ude02a').index, 2, "#8"); + + assert.sameValue(/./.exec('\ud83d\ude02')[0], '\ud83d', "#9"); + + assert(RegExp("\uD800").test("\uD800"), "#10"); + + var cu = 0xD800; + var xx = "a\\" + String.fromCharCode(cu); + var pattern = eval("/" + xx + "/"); + assert.sameValue(pattern.source, "a\\\\\\ud800", "Code unit: " + cu.toString(16), "#11"); + assert(pattern.test("a\\\uD800"), "#12"); + ` + + testScript1(TESTLIB+SCRIPT, _undefined, t) +} + +func TestRegexpUnicode(t *testing.T) { + const SCRIPT = ` + + assert(!/\uD800/u.test("\uD800\uDC00"), "#1"); + assert(!/\uFFFD/u.test("\uD800\uDC00"), "#2"); + + assert(/\uD800\uDC00/u.test("\uD800\uDC00"), "#3"); + + assert(/\uD800/u.test("\uD800"), "#4"); + + assert(compareArray("a\uD800\uDC00b".split(/\uD800/gu), ["a\uD800\uDC00b"]), "#5"); + + assert(compareArray("a\uD800\uDC00b".split(/(?:)/gu), ["a", "𐀀", "b"]), "#6"); + + var re = eval('/' + /\ud834\udf06/u.source + '/u'); + assert(re.test('\ud834\udf06'), "#9"); + + /*re = RegExp("\\p{L}", "u"); + if (!re.test("A")) { + throw new Error("Test 9 failed"); + }*/ + ` + + testScript1(TESTLIB+SCRIPT, _undefined, t) +} + +func TestConvertRegexpToUnicode(t *testing.T) { + if s := convertRegexpToUnicode(`test\uD800\u0C00passed`); s != `test\uD800\u0C00passed` { + t.Fatal(s) + } + if s := convertRegexpToUnicode(`test\uD800\uDC00passed`); s != `test𐀀passed` { + t.Fatal(s) + } + if s := convertRegexpToUnicode(`test\u0023passed`); s != `test\u0023passed` { + t.Fatal(s) + } + if s := convertRegexpToUnicode(`test\u0passed`); s != `test\u0passed` { + t.Fatal(s) + } + if s := convertRegexpToUnicode(`test\uD800passed`); s != `test\uD800passed` { + t.Fatal(s) + } + if s := convertRegexpToUnicode(`test\uD800`); s != `test\uD800` { + t.Fatal(s) + } + if s := convertRegexpToUnicode(`test\uD80`); s != `test\uD80` { + t.Fatal(s) + } + if s := convertRegexpToUnicode(`\\uD800\uDC00passed`); s != `\\uD800\uDC00passed` { + t.Fatal(s) + } + if s := convertRegexpToUnicode(`testpassed`); s != `testpassed` { + t.Fatal(s) + } +} + +func TestConvertRegexpToUtf16(t *testing.T) { + if s := convertRegexpToUtf16(`𐀀`); s != `\ud800\udc00` { + t.Fatal(s) + } + if s := convertRegexpToUtf16(`\𐀀`); s != `\\\ud800\udc00` { + t.Fatal(s) + } +} + +func TestEscapeInvalidUtf16(t *testing.T) { + if s := escapeInvalidUtf16(asciiString("test")); s != "test" { + t.Fatal(s) + } + if s := escapeInvalidUtf16(newStringValue("test\U00010000")); s != "test\U00010000" { + t.Fatal(s) + } + if s := escapeInvalidUtf16(unicodeStringFromRunes([]rune{'t', 0xD800})); s != "t\\ud800" { + t.Fatal(s) + } + if s := escapeInvalidUtf16(unicodeStringFromRunes([]rune{'t', 0xD800, 'p'})); s != "t\\ud800p" { + t.Fatal(s) + } + if s := escapeInvalidUtf16(unicodeStringFromRunes([]rune{0xD800, 'p'})); s != "\\ud800p" { + t.Fatal(s) + } + if s := escapeInvalidUtf16(unicodeStringFromRunes([]rune{'t', '\\', 0xD800, 'p'})); s != `t\\\ud800p` { + t.Fatal(s) + } +} + +func TestRegexpAssertion(t *testing.T) { + const SCRIPT = ` + var res = 'aaa'.match(/^a/g); + res.length === 1 || res[0] === 'a'; + ` + testScript1(SCRIPT, valueTrue, t) +} + +func TestRegexpUnicodeAdvanceStringIndex(t *testing.T) { + const SCRIPT = ` + // deoptimise RegExp + var origExec = RegExp.prototype.exec; + RegExp.prototype.exec = function(s) { + return origExec.call(this, s); + }; + + var re = /(?:)/gu; + var str = "a\uD800\uDC00b"; + assert(compareArray(str.split(re), ["a", "𐀀", "b"]), "#1"); + + re.lastIndex = 3; + assert.sameValue(re.exec(str).index, 3, "#2"); + + re.lastIndex = 2; + assert.sameValue(re.exec(str).index, 1, "#3"); + + re.lastIndex = 4; + assert.sameValue(re.exec(str).index, 4, "#4"); + + re.lastIndex = 5; + assert.sameValue(re.exec(str), null, "#5"); + ` + testScript1(TESTLIB+SCRIPT, _undefined, t) +} + +func TestRegexpInit(t *testing.T) { + const SCRIPT = ` + RegExp(".").lastIndex; + ` + testScript1(SCRIPT, intToValue(0), t) +} + func BenchmarkRegexpSplitWithBackRef(b *testing.B) { const SCRIPT = ` "aaaaaaaaaaaaaaaaaaaaaaaaa++bbbbbbbbbbbbbbbbbbbbbb+-ccccccccccccccccccccccc".split(/([+-])\1/) diff --git a/runtime.go b/runtime.go index 54b3ee7d..4ce967fb 100644 --- a/runtime.go +++ b/runtime.go @@ -23,6 +23,8 @@ import ( const ( sqrt1_2 float64 = math.Sqrt2 / 2 + + deoptimiseRegexp = false ) var ( @@ -119,13 +121,14 @@ type global struct { thrower *Object throwerProperty Value - regexpProtoExec Value - weakSetAdder *Object - weakMapAdder *Object - mapAdder *Object - setAdder *Object - arrayValues *Object - arrayToString *Object + stdRegexpProto *guardedObject + + weakSetAdder *Object + weakMapAdder *Object + mapAdder *Object + setAdder *Object + arrayValues *Object + arrayToString *Object } type Flag int @@ -406,11 +409,30 @@ func newBaseObjectObj(obj, proto *Object, class string) *baseObject { return o } +func newGuardedObj(proto *Object, class string) *guardedObject { + return &guardedObject{ + baseObject: baseObject{ + class: class, + extensible: true, + prototype: proto, + }, + } +} + func (r *Runtime) newBaseObject(proto *Object, class string) (o *baseObject) { v := &Object{runtime: r} return newBaseObjectObj(v, proto, class) } +func (r *Runtime) newGuardedObject(proto *Object, class string) (o *guardedObject) { + v := &Object{runtime: r} + o = newGuardedObj(proto, class) + v.self = o + o.val = v + o.init() + return +} + func (r *Runtime) NewObject() (v *Object) { return r.newBaseObject(r.global.ObjectPrototype, classObject).val } @@ -665,25 +687,31 @@ func (r *Runtime) builtin_newBoolean(args []Value, proto *Object) *Object { } func (r *Runtime) error_toString(call FunctionCall) Value { + var nameStr, msgStr valueString obj := call.This.ToObject(r).self - msg := obj.getStr("message", nil) name := obj.getStr("name", nil) - var nameStr, msgStr string - if name != nil && name != _undefined { - nameStr = name.String() - } - if msg != nil && msg != _undefined { - msgStr = msg.String() + if name == nil || name == _undefined { + nameStr = asciiString("Error") + } else { + nameStr = name.toString() } - if nameStr != "" && msgStr != "" { - return newStringValue(fmt.Sprintf("%s: %s", name.String(), msgStr)) + msg := obj.getStr("message", nil) + if msg == nil || msg == _undefined { + msgStr = stringEmpty } else { - if nameStr != "" { - return name.toString() - } else { - return msg.toString() - } + msgStr = msg.toString() + } + if nameStr.length() == 0 { + return msgStr } + if msgStr.length() == 0 { + return nameStr + } + var sb valueStringBuilder + sb.WriteString(nameStr) + sb.WriteString(asciiString(": ")) + sb.WriteString(msgStr) + return sb.String() } func (r *Runtime) builtin_Error(args []Value, proto *Object) *Object { @@ -707,8 +735,8 @@ func (r *Runtime) builtin_thrower(FunctionCall) Value { return nil } -func (r *Runtime) eval(src string, direct, strict bool, this Value) Value { - +func (r *Runtime) eval(srcVal valueString, direct, strict bool, this Value) Value { + src := escapeInvalidUtf16(srcVal) p, err := r.compile("", src, strict, true) if err != nil { panic(err) @@ -742,7 +770,7 @@ func (r *Runtime) builtin_eval(call FunctionCall) Value { return _undefined } if str, ok := call.Arguments[0].(valueString); ok { - return r.eval(str.String(), false, false, r.globalObject) + return r.eval(str, false, false, r.globalObject) } return call.Arguments[0] } diff --git a/string.go b/string.go index 4f600e15..1a35f442 100644 --- a/string.go +++ b/string.go @@ -56,6 +56,9 @@ type valueString interface { substring(start, end int) valueString compareTo(valueString) int reader(start int) io.RuneReader + utf16Reader(start int) io.RuneReader + runes() []rune + utf16Runes() []rune index(valueString, int) int lastIndex(valueString, int) int toLower() valueString @@ -101,15 +104,15 @@ func stringFromRune(r rune) valueString { } else { sb.Grow(2) } - sb.writeRune(r) - return sb.string() + sb.WriteRune(r) + return sb.String() } func (r *Runtime) createStringIterator(s valueString) Value { o := &Object{runtime: r} si := &stringIterObject{ - reader: s.reader(0), + reader: &lenientUtf16Decoder{utf16Reader: s.utf16Reader(0)}, } si.class = classStringIterator si.val = o diff --git a/string_ascii.go b/string_ascii.go index dab1bcb7..d997df9d 100644 --- a/string_ascii.go +++ b/string_ascii.go @@ -36,6 +36,22 @@ func (s asciiString) reader(start int) io.RuneReader { } } +func (s asciiString) utf16Reader(start int) io.RuneReader { + return s.reader(start) +} + +func (s asciiString) runes() []rune { + runes := make([]rune, len(s)) + for i := 0; i < len(s); i++ { + runes[i] = rune(s[i]) + } + return runes +} + +func (s asciiString) utf16Runes() []rune { + return s.runes() +} + // ss must be trimmed func strToInt(ss string) (int64, error) { if ss == "" { diff --git a/string_unicode.go b/string_unicode.go index 9a2b3374..e976ca1c 100644 --- a/string_unicode.go +++ b/string_unicode.go @@ -24,8 +24,21 @@ type unicodeRuneReader struct { pos int } -type runeReaderReplace struct { - wrapped io.RuneReader +type utf16RuneReader struct { + s unicodeString + pos int +} + +// passes through invalid surrogate pairs +type lenientUtf16Decoder struct { + utf16Reader io.RuneReader + prev rune + prevSet bool +} + +type valueStringBuilder struct { + asciiBuilder strings.Builder + unicodeBuilder unicodeStringBuilder } type unicodeStringBuilder struct { @@ -34,15 +47,48 @@ type unicodeStringBuilder struct { } var ( - InvalidRuneError = errors.New("Invalid rune") + InvalidRuneError = errors.New("invalid rune") ) -func (rr runeReaderReplace) ReadRune() (r rune, size int, err error) { - r, size, err = rr.wrapped.ReadRune() - if err == InvalidRuneError { - err = nil - r = utf8.RuneError +func (rr *utf16RuneReader) ReadRune() (r rune, size int, err error) { + if rr.pos < len(rr.s) { + r = rune(rr.s[rr.pos]) + size++ + rr.pos++ + return + } + err = io.EOF + return +} + +func (rr *lenientUtf16Decoder) ReadRune() (r rune, size int, err error) { + if rr.prevSet { + r = rr.prev + size = 1 + rr.prevSet = false + } else { + r, size, err = rr.utf16Reader.ReadRune() + if err != nil { + return + } } + if isUTF16FirstSurrogate(r) { + second, _, err1 := rr.utf16Reader.ReadRune() + if err1 != nil { + if err1 != io.EOF { + err = err1 + } + return + } + if isUTF16SecondSurrogate(second) { + r = utf16.DecodeRune(r, second) + size++ + } else { + rr.prev = second + rr.prevSet = true + } + } + return } @@ -92,8 +138,8 @@ func (b *unicodeStringBuilder) ensureStarted(initialSize int) { } } -func (b *unicodeStringBuilder) writeString(s valueString) { - b.ensureStarted(int(s.length())) +func (b *unicodeStringBuilder) WriteString(s valueString) { + b.ensureStarted(s.length()) switch s := s.(type) { case unicodeString: b.buf = append(b.buf, s[1:]...) @@ -107,7 +153,7 @@ func (b *unicodeStringBuilder) writeString(s valueString) { } } -func (b *unicodeStringBuilder) string() valueString { +func (b *unicodeStringBuilder) String() valueString { if b.unicode { return unicodeString(b.buf) } @@ -121,11 +167,13 @@ func (b *unicodeStringBuilder) string() valueString { return asciiString(buf) } -func (b *unicodeStringBuilder) writeRune(r rune) { +func (b *unicodeStringBuilder) WriteRune(r rune) { if r <= 0xFFFF { b.ensureStarted(1) b.buf = append(b.buf, uint16(r)) - b.unicode = r >= utf8.RuneSelf + if !b.unicode && r >= utf8.RuneSelf { + b.unicode = true + } } else { b.ensureStarted(2) first, second := utf16.EncodeRune(r) @@ -134,19 +182,126 @@ func (b *unicodeStringBuilder) writeRune(r rune) { } } -func (b *unicodeStringBuilder) writeASCII(bytes []byte) { +func (b *unicodeStringBuilder) writeASCIIString(bytes string) { b.ensureStarted(len(bytes)) for _, c := range bytes { b.buf = append(b.buf, uint16(c)) } } +func (b *valueStringBuilder) ascii() bool { + return len(b.unicodeBuilder.buf) == 0 +} + +func (b *valueStringBuilder) WriteString(s valueString) { + if ascii, ok := s.(asciiString); ok { + if b.ascii() { + b.asciiBuilder.WriteString(string(ascii)) + } else { + b.unicodeBuilder.writeASCIIString(string(ascii)) + } + } else { + b.switchToUnicode(s.length()) + b.unicodeBuilder.WriteString(s) + } +} + +func (b *valueStringBuilder) WriteRune(r rune) { + if r < utf8.RuneSelf { + if b.ascii() { + b.asciiBuilder.WriteByte(byte(r)) + } else { + b.unicodeBuilder.WriteRune(r) + } + } else { + var extraLen int + if r <= 0xFFFF { + extraLen = 1 + } else { + extraLen = 2 + } + b.switchToUnicode(extraLen) + b.unicodeBuilder.WriteRune(r) + } +} + +func (b *valueStringBuilder) String() valueString { + if b.ascii() { + return asciiString(b.asciiBuilder.String()) + } + return b.unicodeBuilder.String() +} + +func (b *valueStringBuilder) Grow(n int) { + if b.ascii() { + b.asciiBuilder.Grow(n) + } else { + b.unicodeBuilder.Grow(n) + } +} + +func (b *valueStringBuilder) switchToUnicode(extraLen int) { + if b.ascii() { + b.unicodeBuilder.ensureStarted(b.asciiBuilder.Len() + extraLen) + b.unicodeBuilder.writeASCIIString(b.asciiBuilder.String()) + b.asciiBuilder.Reset() + } +} + +func (b *valueStringBuilder) WriteSubstring(source valueString, start int, end int) { + if ascii, ok := source.(asciiString); ok { + if b.ascii() { + b.asciiBuilder.WriteString(string(ascii[start:end])) + return + } + } + us := source.(unicodeString) + if b.ascii() { + uc := false + for i := start; i < end; i++ { + if us.charAt(i) >= utf8.RuneSelf { + uc = true + break + } + } + if uc { + b.switchToUnicode(end - start + 1) + } else { + b.asciiBuilder.Grow(end - start + 1) + for i := start; i < end; i++ { + b.asciiBuilder.WriteByte(byte(us.charAt(i))) + } + return + } + } + b.unicodeBuilder.buf = append(b.unicodeBuilder.buf, us[start+1:end+1]...) + b.unicodeBuilder.unicode = true +} + func (s unicodeString) reader(start int) io.RuneReader { return &unicodeRuneReader{ s: s[start+1:], } } +func (s unicodeString) utf16Reader(start int) io.RuneReader { + return &utf16RuneReader{ + s: s[start+1:], + } +} + +func (s unicodeString) runes() []rune { + return utf16.Decode(s[1:]) +} + +func (s unicodeString) utf16Runes() []rune { + runes := make([]rune, len(s)-1) + for i, ch := range s[1:] { + runes[i] = rune(ch) + } + return runes +} + func (s unicodeString) ToInteger() int64 { return 0 } @@ -274,6 +429,7 @@ func (s unicodeString) String() string { } func (s unicodeString) compareTo(other valueString) int { + // TODO handle invalid UTF-16 return strings.Compare(s.String(), other.String()) } diff --git a/tc39_test.go b/tc39_test.go index 47aec98a..86515f2e 100644 --- a/tc39_test.go +++ b/tc39_test.go @@ -37,7 +37,10 @@ var ( "test/built-ins/Date/prototype/toISOString/15.9.5.43-0-8.js": true, // timezone "test/built-ins/Date/prototype/toISOString/15.9.5.43-0-9.js": true, // timezone "test/built-ins/Date/prototype/toISOString/15.9.5.43-0-10.js": true, // timezone - "test/annexB/built-ins/escape/escape-above-astral.js": true, // \u{xxxxx} + + // \u{xxxxx} + "test/annexB/built-ins/escape/escape-above-astral.js": true, + "test/built-ins/RegExp/prototype/source/value-u.js": true, // SharedArrayBuffer "test/built-ins/ArrayBuffer/prototype/slice/this-is-sharedarraybuffer.js": true, @@ -103,13 +106,9 @@ var ( "test/language/statements/class/subclass/builtin-objects/ArrayBuffer/regular-subclassing.js": true, "test/built-ins/ArrayBuffer/isView/arg-is-typedarray-subclass-instance.js": true, "test/built-ins/ArrayBuffer/isView/arg-is-dataview-subclass-instance.js": true, - - // full unicode regexp flag - "test/built-ins/RegExp/prototype/Symbol.match/u-advance-after-empty.js": true, - "test/built-ins/RegExp/prototype/Symbol.match/get-unicode-error.js": true, - "test/built-ins/RegExp/prototype/Symbol.match/builtin-success-u-return-val-groups.js": true, - "test/built-ins/RegExp/prototype/Symbol.match/builtin-infer-unicode.js": true, - "test/built-ins/RegExp/unicode_identity_escape.js": true, + "test/language/statements/class/subclass/builtin-objects/RegExp/super-must-be-called.js": true, + "test/language/statements/class/subclass/builtin-objects/RegExp/regular-subclassing.js": true, + "test/language/statements/class/subclass/builtin-objects/RegExp/lastIndex.js": true, // object literals "test/built-ins/Array/from/source-object-iterator-1.js": true, @@ -133,6 +132,33 @@ var ( "test/built-ins/String/raw/template-substitutions-are-appended-on-same-index.js": true, "test/built-ins/String/raw/special-characters.js": true, "test/built-ins/String/raw/return-the-string-value-from-template.js": true, + + // restricted unicode regexp syntax + "test/built-ins/RegExp/unicode_restricted_quantifiable_assertion.js": true, + "test/built-ins/RegExp/unicode_restricted_octal_escape.js": true, + "test/built-ins/RegExp/unicode_restricted_incomple_quantifier.js": true, + "test/built-ins/RegExp/unicode_restricted_identity_escape_x.js": true, + "test/built-ins/RegExp/unicode_restricted_identity_escape_u.js": true, + "test/built-ins/RegExp/unicode_restricted_identity_escape_c.js": true, + "test/built-ins/RegExp/unicode_restricted_identity_escape_alpha.js": true, + "test/built-ins/RegExp/unicode_restricted_identity_escape.js": true, + "test/built-ins/RegExp/unicode_restricted_brackets.js": true, + "test/built-ins/RegExp/unicode_restricted_character_class_escape.js": true, + "test/annexB/built-ins/RegExp/prototype/compile/pattern-string-invalid-u.js": true, + + // Because goja parser works in UTF-8 it is not possible to pass strings containing invalid UTF-16 code points. + // This is mitigated by escaping them as \uXXXX, however because of this the RegExp source becomes + // `\uXXXX` instead of ``. + // The resulting RegExp will work exactly the same, but it causes these two tests to fail. + "test/annexB/built-ins/RegExp/RegExp-leading-escape-BMP.js": true, + "test/annexB/built-ins/RegExp/RegExp-trailing-escape-BMP.js": true, + + // Looks like a bug in regexp2: decimal escapes that do not represent a capture are simply ignored instead + // of being treated as a character with the specified code. + "test/annexB/built-ins/RegExp/RegExp-decimal-escape-not-capturing.js": true, + + // Promise + "test/built-ins/Symbol/species/builtin-getter-name.js": true, } featuresBlackList = []string{ @@ -155,7 +181,7 @@ var ( "20.2", "20.3", "21.1", - "21.2.5.6", + "21.2", "22.1", "22.2", "23.1", @@ -181,6 +207,7 @@ var ( "sec-math", "sec-arraybuffer-length", "sec-arraybuffer", + "sec-regexp", } ) @@ -293,6 +320,7 @@ func (ctx *tc39TestCtx) runTC39Test(name, src string, meta *tc39Meta, t testing. _262.Set("createRealm", ctx.throwIgnorableTestError) vm.Set("$262", _262) vm.Set("IgnorableTestError", ignorableTestError) + vm.Set("print", t.Log) vm.RunProgram(sabStub) err, early := ctx.runTC39Script(name, src, meta.Includes, vm) @@ -574,6 +602,7 @@ func TestTC39(t *testing.T) { ctx.runTC39Tests("test/annexB/built-ins/String/prototype/substr") ctx.runTC39Tests("test/annexB/built-ins/escape") ctx.runTC39Tests("test/annexB/built-ins/unescape") + ctx.runTC39Tests("test/annexB/built-ins/RegExp") ctx.flush() }) diff --git a/vm.go b/vm.go index 1540064e..05420c19 100644 --- a/vm.go +++ b/vm.go @@ -1272,14 +1272,12 @@ func (n *newArraySparse) exec(vm *vm) { } type newRegexp struct { - pattern regexpPattern + pattern *regexpPattern src valueString - - global, ignoreCase, multiline, sticky bool } func (n *newRegexp) exec(vm *vm) { - vm.push(vm.r.newRegExpp(n.pattern, n.src, n.global, n.ignoreCase, n.multiline, n.sticky, vm.r.global.RegExpPrototype)) + vm.push(vm.r.newRegExpp(n.pattern, n.src, vm.r.global.RegExpPrototype)) vm.pc++ } @@ -1723,7 +1721,7 @@ func (vm *vm) callEval(n int, strict bool) { } else { this = vm.r.globalObject } - ret := vm.r.eval(src.String(), true, strict, this) + ret := vm.r.eval(src, true, strict, this) vm.stack[vm.sp-n-2] = ret } else { vm.stack[vm.sp-n-2] = srcVal From ae5ee69c94cfddbca76c83f30b27977c042a0bd5 Mon Sep 17 00:00:00 2001 From: Dmitry Panov Date: Fri, 7 Aug 2020 10:41:58 +0100 Subject: [PATCH 41/46] Re-enabled a test after the regexp2 bug is fixed. --- tc39_test.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tc39_test.go b/tc39_test.go index 86515f2e..10be1339 100644 --- a/tc39_test.go +++ b/tc39_test.go @@ -153,10 +153,6 @@ var ( "test/annexB/built-ins/RegExp/RegExp-leading-escape-BMP.js": true, "test/annexB/built-ins/RegExp/RegExp-trailing-escape-BMP.js": true, - // Looks like a bug in regexp2: decimal escapes that do not represent a capture are simply ignored instead - // of being treated as a character with the specified code. - "test/annexB/built-ins/RegExp/RegExp-decimal-escape-not-capturing.js": true, - // Promise "test/built-ins/Symbol/species/builtin-getter-name.js": true, } From e36d2cba6a5cbda5c1b8fc31311c2a9902afa134 Mon Sep 17 00:00:00 2001 From: Dmitry Panov Date: Fri, 7 Aug 2020 11:26:15 +0100 Subject: [PATCH 42/46] Updated documentation. --- README.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index e24af1ce..f3a0d515 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,9 @@ Features * Full ECMAScript 5.1 support (yes, including regex and strict mode). * Passes nearly all [tc39 tests](https://github.com/tc39/test262) tagged with es5id. The goal is to pass all of them. Note, the last working commit is https://github.com/tc39/test262/commit/1ba3a7c4a93fc93b3d0d7e4146f59934a896837d. The next commit made use of template strings which goja does not support. - * Capable of running Babel (up to v7), Typescript compiler and pretty much anything written in ES5. + * Capable of running Babel, Typescript compiler and pretty much anything written in ES5. * Sourcemaps. + * Some ES6 functionality, still work in progress, see https://github.com/dop251/goja/milestone/1?closed=1 FAQ --- @@ -57,14 +58,16 @@ and it includes an event loop. ### Can you implement (feature X from ES6 or higher)? -There is now an es6 branch. This is work in progress and all new ES6 features will go there. Every commit +Some ES6 functionality has been implemented. So far this is mostly built-ins, not syntax enhancements. +See https://github.com/dop251/goja/milestone/1 for more details. + +The ongoing work is done in the es6 branch which is merged into master when appropriate. Every commit in this branch represents a relatively stable state (i.e. it compiles and passes all enabled tc39 tests), however because the version of tc39 tests I use is quite old, it may be not as well tested as the ES5.1 functionality. Because ES6 is a superset of ES5.1 it should not break your existing code. I will be adding features in their dependency order and as quickly as my time allows. Please do not ask -for ETA. Eventually it will be merged into master. If you wish to implement a new feature please contact -me first and read the section below. +for ETA. Features that are open in the [milestone](https://github.com/dop251/goja/milestone/1) are either in progress or will be worked on next. ### How do I contribute? @@ -78,9 +81,8 @@ do not just base it on a couple of examples that work fine. Current Status -------------- - * API is still work in progress and is subject to change. + * There should be no breaking changes in the API, however it may be extended. * Some of the AnnexB functionality is missing. - * No typed arrays yet. Basic Example ------------- From 52a8eb19c9a9265179a3faa544deffb10efafca3 Mon Sep 17 00:00:00 2001 From: Dmitry Panov Date: Mon, 10 Aug 2020 11:03:19 +0100 Subject: [PATCH 43/46] Updated documentation, fixed Date export. See #170, closes #182 --- README.md | 50 ++++----------- date.go | 2 +- date_test.go | 19 ++++++ object_goreflect_test.go | 30 +++++++++ runtime.go | 134 ++++++++++++++++++++++++++++++++++----- runtime_test.go | 25 ++++++++ 6 files changed, 204 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index f3a0d515..58985ae0 100644 --- a/README.md +++ b/README.md @@ -10,10 +10,12 @@ performance. This project was largely inspired by [otto](https://github.com/robertkrimen/otto). +Minimum required Go version is 1.14. + Features -------- - * Full ECMAScript 5.1 support (yes, including regex and strict mode). + * Full ECMAScript 5.1 support (including regex and strict mode). * Passes nearly all [tc39 tests](https://github.com/tc39/test262) tagged with es5id. The goal is to pass all of them. Note, the last working commit is https://github.com/tc39/test262/commit/1ba3a7c4a93fc93b3d0d7e4146f59934a896837d. The next commit made use of template strings which goja does not support. * Capable of running Babel, Typescript compiler and pretty much anything written in ES5. * Sourcemaps. @@ -34,11 +36,11 @@ You can find some benchmarks [here](https://github.com/dop251/goja/issues/2). It greatly depends on your usage scenario. If most of the work is done in javascript (for example crypto or any other heavy calculations) you are definitely better off with V8. -If you need a scripting language that drives an engine written in Go so +If you need a scripting language that drives an engine written in Go so that you need to make frequent calls between Go and javascript passing complex data structures then the cgo overhead may outweigh the benefits of having a faster javascript engine. -Because it's written in pure Go there are no external dependencies, it's very easy to build and it +Because it's written in pure Go there are no cgo dependencies, it's very easy to build and it should run on any platform supported by Go. It gives you a much better control over execution environment so can be useful for research. @@ -53,7 +55,7 @@ it's not possible to pass object values between runtimes. setTimeout() assumes concurrent execution of code which requires an execution environment, for example an event loop similar to nodejs or a browser. -There is a [separate project](https://github.com/dop251/goja_nodejs) aimed at providing some of the NodeJS functionality +There is a [separate project](https://github.com/dop251/goja_nodejs) aimed at providing some NodeJS functionality, and it includes an event loop. ### Can you implement (feature X from ES6 or higher)? @@ -67,7 +69,8 @@ however because the version of tc39 tests I use is quite old, it may be not as w functionality. Because ES6 is a superset of ES5.1 it should not break your existing code. I will be adding features in their dependency order and as quickly as my time allows. Please do not ask -for ETA. Features that are open in the [milestone](https://github.com/dop251/goja/milestone/1) are either in progress or will be worked on next. +for ETAs. Features that are open in the [milestone](https://github.com/dop251/goja/milestone/1) are either in progress +or will be worked on next. ### How do I contribute? @@ -101,37 +104,7 @@ if num := v.Export().(int64); num != 4 { Passing Values to JS -------------------- -Any Go value can be passed to JS using Runtime.ToValue() method. Primitive types (ints and uints, floats, string, bool) -are converted to the corresponding JavaScript primitives. - -*func(FunctionCall) Value* is treated as a native JavaScript function. - -*func(ConstructorCall) \*Object* is treated as a JavaScript constructor (see Native Constructors). - -*map[string]interface{}* is converted into a host object that largely behaves like a JavaScript Object. - -*[]interface{}* is converted into a host object that behaves largely like a JavaScript Array, however it's not extensible -because extending it can change the pointer so it becomes detached from the original. - -**[]interface{}* is same as above, but the array becomes extensible. - -A function is wrapped within a native JavaScript function. When called the arguments are automatically converted to -the appropriate Go types. If conversion is not possible, a TypeError is thrown. - -A slice type is converted into a generic reflect based host object that behaves similar to an unexpandable Array. - -A map type with numeric or string keys and no methods is converted into a host object where properties are map keys. - -A map type with methods is converted into a host object where properties are method names, -the map values are not accessible. This is to avoid ambiguity between m\["Property"\] and m.Property. - -Any other type is converted to a generic reflect based host object. Depending on the underlying type it behaves similar -to a Number, String, Boolean or Object. This includes pointers to primitive types (*string, *int, etc...). -Internally they remain pointers, so changes to the pointed values will be reflected in JS. - -Note that these conversions wrap the original value which means any changes made inside JS -are reflected on the value and calling Export() returns the original value. This applies to all -reflect based types. +Any Go value can be passed to JS using Runtime.ToValue() method. See the method's [documentation](https://godoc.org/github.com/dop251/goja#Runtime.ToValue) for more details. Exporting Values from JS ------------------------ @@ -144,7 +117,7 @@ Mapping struct field and method names ------------------------------------- By default, the names are passed through as is which means they are capitalised. This does not match the standard JavaScript naming convention, so if you need to make your JS code look more natural or if you are -dealing with a 3rd party library, you can use a FieldNameMapper: +dealing with a 3rd party library, you can use a [FieldNameMapper](https://godoc.org/github.com/dop251/goja#FieldNameMapper): ```go vm := New() @@ -158,7 +131,8 @@ fmt.Println(res.Export()) // Output: 42 ``` -There are two standard mappers: `TagFieldNameMapper` and `UncapFieldNameMapper`, or you can use your own implementation. +There are two standard mappers: [TagFieldNameMapper](https://godoc.org/github.com/dop251/goja#TagFieldNameMapper) and +[UncapFieldNameMapper](https://godoc.org/github.com/dop251/goja#UncapFieldNameMapper), or you can use your own implementation. Native Constructors ------------------- diff --git a/date.go b/date.go index e8ae5808..6dbc0ae3 100644 --- a/date.go +++ b/date.go @@ -108,7 +108,7 @@ func (d *dateObject) toPrimitive() Value { func (d *dateObject) export() interface{} { if d.isSet() { - return d.time + return d.time() } return nil } diff --git a/date_test.go b/date_test.go index 3c5e3d86..0bf9cf46 100644 --- a/date_test.go +++ b/date_test.go @@ -377,3 +377,22 @@ func TestDateMaxValues(t *testing.T) { ` testScript1(TESTLIB+SCRIPT, _undefined, t) } + +func TestDateExport(t *testing.T) { + vm := New() + res, err := vm.RunString(`new Date(1000)`) + if err != nil { + t.Fatal(err) + } + exp := res.Export() + if d, ok := exp.(time.Time); ok { + if d.UnixNano()/1e6 != 1000 { + t.Fatalf("Invalid exported date: %v", d) + } + if loc := d.Location(); loc != time.Local { + t.Fatalf("Invalid timezone: %v", loc) + } + } else { + t.Fatalf("Invalid export type: %T", exp) + } +} diff --git a/object_goreflect_test.go b/object_goreflect_test.go index 627ad8a9..4324aa7f 100644 --- a/object_goreflect_test.go +++ b/object_goreflect_test.go @@ -1088,6 +1088,36 @@ func TestGoReflectSymbols(t *testing.T) { } } +func TestGoReflectSymbolEqualityQuirk(t *testing.T) { + type Field struct{ + } + type S struct { + Field *Field + } + var s = S{ + Field: &Field{}, + } + vm := New() + vm.Set("s", &s) + res, err := vm.RunString(` + var sym = Symbol(66); + var field1 = s.Field; + field1[sym] = true; + var field2 = s.Field; + // Because a wrapper is created every time the property is accessed + // field1 and field2 will be different instances of the wrapper. + // Symbol properties only exist in the wrapper, they cannot be placed into the original Go value, + // hence the following: + field1 === field2 && field1[sym] === true && field2[sym] === undefined; + `) + if err != nil { + t.Fatal(err) + } + if res != valueTrue { + t.Fatal(res) + } +} + func TestGoObj__Proto__(t *testing.T) { type S struct { Field int diff --git a/runtime.go b/runtime.go index 4ce967fb..d6399b49 100644 --- a/runtime.go +++ b/runtime.go @@ -1176,23 +1176,123 @@ func (r *Runtime) ClearInterrupt() { } /* -ToValue converts a Go value into JavaScript value. - -Primitive types (ints and uints, floats, string, bool) are converted to the corresponding JavaScript primitives. - -func(FunctionCall) Value is treated as a native JavaScript function. - -map[string]interface{} is converted into a host object that largely behaves like a JavaScript Object. - -[]interface{} is converted into a host object that behaves largely like a JavaScript Array, however it's not extensible -because extending it can change the pointer so it becomes detached from the original. - -*[]interface{} same as above, but the array becomes extensible. - -A function is wrapped within a native JavaScript function. When called the arguments are automatically converted to -the appropriate Go types. If conversion is not possible, a TypeError is thrown. - -A slice type is converted into a generic reflect based host object that behaves similar to an unexpandable Array. +ToValue converts a Go value into a JavaScript value of a most appropriate type. Structural types (such as structs, maps +and slices) are wrapped so that changes are reflected on the original value which can be retrieved using Value.Export(). + +Notes on individual types: + +Primitive types. +----- +Primitive types (numbers, string, bool) are converted to the corresponding JavaScript primitives. + +Strings. +----- +Because of the difference in internal string representation between ECMAScript (which uses UTF-16) and Go (which uses +UTF-8) conversion from JS to Go may be lossy. In particular, code points that can be part of UTF-16 surrogate pairs +(0xD800-0xDFFF) cannot be represented in UTF-8 unless they form a valid surrogate pair and are replaced with +utf8.RuneError. + +Nil. +----- +Nil is converted to `null`. + +Functions. +----- +`func(FunctionCall) Value` is treated as a native JavaScript function. This increases performance because there are no +automatic argument and return value type conversions (which involves reflect). + +Any other Go function is wrapped so that the arguments are automatically converted into the required Go types and the +return value is converted to a JavaScript value (using this method). If conversion is not possible, a TypeError is +thrown. + +Functions with multiple return values return an Array. If the last return value is an `error` it is not returned but +converted into a JS exception. If the error is *Exception, it is thrown as is, otherwise it's wrapped in a GoEerror. +Note that if there are exactly two return values and the last is an `error`, the function returns the first value as is, +not an Array. + +Structs. +--- +Structs are converted to Object-like values. Fields and methods are available as properties, their values are +results of this method (ToValue()) applied to the corresponding Go value. + +Field properties are writable (if the struct is addressable) and non-configurable. +Method properties are non-writable and non-configurable. + +Attempt to define a new property or delete an existing property will fail (throw in strict mode) unless it's a Symbol +property. Symbol properties only exist in the wrapper and do not affect the underlying Go value. +Note that because a wrapper is created every time a property is accessed it may lead to unexpected results such as this: + +``` +type Field struct{ +} +type S struct { + Field *Field +} +var s = S{ + Field: &Field{}, +} +vm := New() +vm.Set("s", &s) +res, err := vm.RunString(` +var sym = Symbol(66); +var field1 = s.Field; +field1[sym] = true; +var field2 = s.Field; +field1 === field2; // true, because the equality operation compares the wrapped values, not the wrappers +field1[sym] === true; // true +field2[sym] === undefined; // also true +`) +``` + +The same applies to values from maps and slices as well. + +time.Time. +--- +time.Time does not get special treatment and therefore is converted just like any other `struct` providing access to +all its methods. This is done deliberately instead of converting it to a `Date` because these two types are not fully +compatible: `time.Time` includes zone, whereas JS `Date` doesn't. Doing the conversion implicitly therefore would +result in a loss of information. + +If you need to convert it to a `Date`, it can be done either in JS: +``` +var d = new Date(goval.UnixNano()/1e6); +``` + +... or in Go: + +``` +now := time.Now() +vm := New() +val, err := vm.New(vm.Get("Date").ToObject(vm), vm.ToValue(now.UnixNano()/1e6)) +if err != nil { + ... +} +vm.Set("d", val) +``` + +Note that Value.Export() for a `Date` value returns time.Time in local timezone. + +Maps. +--- +Maps with string or integer key type are converted into host objects that largely behave like a JavaScript Object. + +Maps with methods. +--- +If a map type has at least one method defined, the properties of the resulting Object represent methods, not map keys. +This is because in JavaScript there is no distinction between 'object.key` and `object[key]`, unlike Go. +If access to the map values is required, it can be achieved by defining another method or, if it's not possible, by +defining an external getter function. + +Slices. +--- +Slices are converted into host objects that behave largely like JavaScript Array. It has the appropriate +prototype and all the usual methods should work. There are, however, some caveats: + +- If the slice is not addressable, the array cannot be extended or shrunk. Any attempt to do so (by setting an index +beyond the current length or by modifying the length) will result in a TypeError. +- Converted Arrays may not contain holes (because Go slices cannot). This means that hasOwnProperty(n) will always +return `true` if n < length. Attempt to delete an item with an index < length will fail. Nil slice elements will be +converted to `null`. Accessing an element beyond `length` will return `undefined`. Any other type is converted to a generic reflect based host object. Depending on the underlying type it behaves similar to a Number, String, Boolean or Object. diff --git a/runtime_test.go b/runtime_test.go index 93d07ce7..da90dc38 100644 --- a/runtime_test.go +++ b/runtime_test.go @@ -1487,6 +1487,31 @@ func TestToValueNilValue(t *testing.T) { } } +func TestDateConversion(t *testing.T) { + now := time.Now() + vm := New() + val, err := vm.New(vm.Get("Date").ToObject(vm), vm.ToValue(now.UnixNano()/1e6)) + if err != nil { + t.Fatal(err) + } + vm.Set("d", val) + res, err := vm.RunString(`+d`) + if err != nil { + t.Fatal(err) + } + if exp := res.Export(); exp != now.UnixNano()/1e6 { + t.Fatalf("Value does not match: %v", exp) + } + vm.Set("goval", now) + res, err = vm.RunString(`+(new Date(goval.UnixNano()/1e6))`) + if err != nil { + t.Fatal(err) + } + if exp := res.Export(); exp != now.UnixNano()/1e6 { + t.Fatalf("Value does not match: %v", exp) + } +} + func TestNativeCtorNewTarget(t *testing.T) { const SCRIPT = ` function NewTarget() { From 28cf031cb85e4b8c154773d9ca3ac5beee825c3e Mon Sep 17 00:00:00 2001 From: Dmitry Panov Date: Mon, 10 Aug 2020 11:06:25 +0100 Subject: [PATCH 44/46] Fixed formatting. --- object_goreflect_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/object_goreflect_test.go b/object_goreflect_test.go index 4324aa7f..d35b9086 100644 --- a/object_goreflect_test.go +++ b/object_goreflect_test.go @@ -1089,7 +1089,7 @@ func TestGoReflectSymbols(t *testing.T) { } func TestGoReflectSymbolEqualityQuirk(t *testing.T) { - type Field struct{ + type Field struct { } type S struct { Field *Field From 3ec8e20a8928f47780c2fa1186ebfa1ee9e1f086 Mon Sep 17 00:00:00 2001 From: Dmitry Panov Date: Tue, 11 Aug 2020 11:56:55 +0100 Subject: [PATCH 45/46] Fixed bugs in Array.prototype.sort(), Object.assign() and Date.prototype.toJSON() --- builtin_array.go | 13 ++++++++---- builtin_arrray_test.go | 46 ++++++++++++++++++++++++++++++++++++++++++ builtin_date.go | 2 -- builtin_object.go | 5 ++++- builtin_typedarrays.go | 2 +- date_test.go | 43 +++++++++++++++++++++++++++++++++++++++ object_test.go | 16 +++++++++++++++ regexp_test.go | 9 +++++++++ 8 files changed, 128 insertions(+), 8 deletions(-) diff --git a/builtin_array.go b/builtin_array.go index b493e51d..64e49d76 100644 --- a/builtin_array.go +++ b/builtin_array.go @@ -173,7 +173,7 @@ func (r *Runtime) arrayproto_pop(call FunctionCall) Value { func (r *Runtime) arrayproto_join(call FunctionCall) Value { o := call.This.ToObject(r) l := int(toLength(o.self.getStr("length", nil))) - var sep valueString = asciiString("") + var sep valueString if s := call.Argument(0); s != _undefined { sep = s.toString() } else { @@ -334,9 +334,14 @@ func (r *Runtime) arrayproto_sort(call FunctionCall) Value { o := call.This.ToObject(r) var compareFn func(FunctionCall) Value - - if arg, ok := call.Argument(0).(*Object); ok { - compareFn, _ = arg.self.assertCallable() + arg := call.Argument(0) + if arg != _undefined { + if arg, ok := call.Argument(0).(*Object); ok { + compareFn, _ = arg.self.assertCallable() + } + if compareFn == nil { + panic(r.NewTypeError("The comparison function must be either a function or undefined")) + } } ctx := arraySortCtx{ diff --git a/builtin_arrray_test.go b/builtin_arrray_test.go index c27c69ea..a83687fd 100644 --- a/builtin_arrray_test.go +++ b/builtin_arrray_test.go @@ -219,3 +219,49 @@ func TestUnscopables(t *testing.T) { ` testScript1(SCRIPT, valueTrue, t) } + +func TestArraySort(t *testing.T) { + const SCRIPT = ` + assert.throws(TypeError, function() { + [1,2].sort(null); + }, "null compare function"); + assert.throws(TypeError, function() { + [1,2].sort({}); + }, "non-callable compare function"); + ` + testScript1(TESTLIB+SCRIPT, _undefined, t) +} + +func TestArrayConcat(t *testing.T) { + const SCRIPT = ` + var concat = Array.prototype.concat; + var array = [1, 2]; + var sparseArray = [1, , 2]; + var nonSpreadableArray = [1, 2]; + nonSpreadableArray[Symbol.isConcatSpreadable] = false; + var arrayLike = { 0: 1, 1: 2, length: 2 }; + var spreadableArrayLike = { 0: 1, 1: 2, length: 2 }; + spreadableArrayLike[Symbol.isConcatSpreadable] = true; + assert(looksNative(concat)); + assert(deepEqual(array.concat(), [1, 2]), '#1'); + assert(deepEqual(sparseArray.concat(), [1, , 2]), '#2'); + assert(deepEqual(nonSpreadableArray.concat(), [[1, 2]]), '#3'); + assert(deepEqual(concat.call(arrayLike), [{ 0: 1, 1: 2, length: 2 }]), '#4'); + assert(deepEqual(concat.call(spreadableArrayLike), [1, 2]), '#5'); + assert(deepEqual([].concat(array), [1, 2]), '#6'); + assert(deepEqual([].concat(sparseArray), [1, , 2]), '#7'); + assert(deepEqual([].concat(nonSpreadableArray), [[1, 2]]), '#8'); + assert(deepEqual([].concat(arrayLike), [{ 0: 1, 1: 2, length: 2 }]), '#9'); + assert(deepEqual([].concat(spreadableArrayLike), [1, 2]), '#10'); + assert(deepEqual(array.concat(sparseArray, nonSpreadableArray, arrayLike, spreadableArrayLike), [ + 1, 2, 1, , 2, [1, 2], { 0: 1, 1: 2, length: 2 }, 1, 2, + ]), '#11'); + array = []; + array.constructor = {}; + array.constructor[Symbol.species] = function () { + return { foo: 1 }; + } + assert.sameValue(array.concat().foo, 1, '@@species'); + ` + testScript1(TESTLIBX+SCRIPT, _undefined, t) +} diff --git a/builtin_date.go b/builtin_date.go index 910e381a..ab24235d 100644 --- a/builtin_date.go +++ b/builtin_date.go @@ -147,8 +147,6 @@ func (r *Runtime) dateproto_toJSON(call FunctionCall) Value { if math.IsNaN(f) || math.IsInf(f, 0) { return _null } - } else if _, ok := tv.(valueInt); !ok { - return _null } if toISO, ok := obj.self.getStr("toISOString", nil).(*Object); ok { diff --git a/builtin_object.go b/builtin_object.go index 71010e21..1ca89ac1 100644 --- a/builtin_object.go +++ b/builtin_object.go @@ -453,12 +453,15 @@ func (r *Runtime) object_assign(call FunctionCall) Value { for _, arg := range call.Arguments[1:] { if arg != _undefined && arg != _null { source := arg.ToObject(r) - for _, key := range source.self.ownPropertyKeys(false, nil) { + for _, key := range source.self.ownPropertyKeys(true, nil) { p := source.getOwnProp(key) if p == nil { continue } if v, ok := p.(*valueProperty); ok { + if !v.enumerable { + continue + } p = v.get(source) } to.setOwn(key, p, true) diff --git a/builtin_typedarrays.go b/builtin_typedarrays.go index 6027add0..42029bd0 100644 --- a/builtin_typedarrays.go +++ b/builtin_typedarrays.go @@ -643,7 +643,7 @@ func (r *Runtime) typedArrayProto_join(call FunctionCall) Value { if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { ta.viewedArrayBuf.ensureNotDetached() s := call.Argument(0) - sep := stringEmpty + var sep valueString if s != _undefined { sep = s.toString() } else { diff --git a/date_test.go b/date_test.go index 0bf9cf46..8905ece8 100644 --- a/date_test.go +++ b/date_test.go @@ -91,7 +91,43 @@ function compareArray(a, b) { } return true; } +` + +const TESTLIBX = TESTLIB + + `function looksNative(fn) { + return /native code/.test(Function.prototype.toString.call(fn)); + } + function deepEqual(a, b) { + if (typeof a === "object") { + if (typeof b === "object") { + if (a === b) { + return true; + } + if (Reflect.getPrototypeOf(a) !== Reflect.getPrototypeOf(b)) { + return false; + } + var keysA = Object.keys(a); + var keysB = Object.keys(b); + if (keysA.length !== keysB.length) { + return false; + } + if (!compareArray(keysA.sort(), keysB.sort())) { + return false; + } + for (var i = 0; i < keysA.length; i++) { + var key = keysA[i]; + if (!deepEqual(a[key], b[key])) { + return false; + } + } + return true; + } else { + return false; + } + } + return assert._isSameValue(a, b); + } ` func TestDateUTC(t *testing.T) { @@ -396,3 +432,10 @@ func TestDateExport(t *testing.T) { t.Fatalf("Invalid export type: %T", exp) } } + +func TestDateToJSON(t *testing.T) { + const SCRIPT = ` + Date.prototype.toJSON.call({ toISOString: function () { return 1; } }) + ` + testScript1(SCRIPT, intToValue(1), t) +} diff --git a/object_test.go b/object_test.go index 03444c02..80c80480 100644 --- a/object_test.go +++ b/object_test.go @@ -109,6 +109,22 @@ func TestDefinePropertiesSymbol(t *testing.T) { testScript1(SCRIPT, valueTrue, t) } +func TestObjectAssign(t *testing.T) { + const SCRIPT = ` + assert.sameValue(Object.assign({ b: 1 }, { get a() { + Object.defineProperty(this, "b", { + value: 3, + enumerable: false + }); + }, b: 2 }).b, 1, "#1"); + + assert.sameValue(Object.assign({ b: 1 }, { get a() { + delete this.b; + }, b: 2 }).b, 1, "#2"); + ` + testScript1(TESTLIB+SCRIPT, _undefined, t) +} + func BenchmarkPut(b *testing.B) { v := &Object{} diff --git a/regexp_test.go b/regexp_test.go index 25fbbbec..96ad3801 100644 --- a/regexp_test.go +++ b/regexp_test.go @@ -350,6 +350,15 @@ func TestRegexpInit(t *testing.T) { testScript1(SCRIPT, intToValue(0), t) } +func TestRegexpToString(t *testing.T) { + const SCRIPT = ` + RegExp.prototype.toString.call({ + source: 'foo', + flags: 'bar'}); + ` + testScript1(SCRIPT, asciiString("/foo/bar"), t) +} + func BenchmarkRegexpSplitWithBackRef(b *testing.B) { const SCRIPT = ` "aaaaaaaaaaaaaaaaaaaaaaaaa++bbbbbbbbbbbbbbbbbbbbbb+-ccccccccccccccccccccccc".split(/([+-])\1/) From 8cff03df8eb33595908a1526fad834c28ae69c8e Mon Sep 17 00:00:00 2001 From: Dmitry Panov Date: Tue, 11 Aug 2020 15:48:24 +0100 Subject: [PATCH 46/46] Disabled s390x builds for now due to problems with Travis. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 5d48ef9a..2de782e6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,7 @@ go: arch: - amd64 - - s390x +# - s390x env: - RACE="-race"