diff --git a/.travis.yml b/.travis.yml index 0556ae71..2de782e6 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 diff --git a/README.md b/README.md index e24af1ce..58985ae0 100644 --- a/README.md +++ b/README.md @@ -10,13 +10,16 @@ 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 (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 --- @@ -33,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. @@ -52,19 +55,22 @@ 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)? -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 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? @@ -78,9 +84,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 ------------- @@ -99,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 ------------------------ @@ -142,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() @@ -156,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/array.go b/array.go index abd24d8b..fb69e096 100644 --- a/array.go +++ b/array.go @@ -2,15 +2,67 @@ package goja import ( "math" + "math/bits" "reflect" "strconv" + + "github.com/dop251/goja/unistring" ) +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", nil)) + index := ai.nextIdx + if index >= l { + ai.obj = nil + return ai.val.runtime.createIterResultObject(_undefined, true) + } + ai.nextIdx++ + idxVal := valueInt(index) + if ai.kind == iterationKindKey { + return ai.val.runtime.createIterResultObject(idxVal, false) + } + elementValue := ai.obj.self.getIdx(idxVal, nil) + var result Value + if ai.kind == iterationKindValue { + result = elementValue + } else { + result = ai.val.runtime.newArrayValues([]Value{idxVal, 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 - length int64 - objCount int64 + length uint32 + objCount int propValueCount int lengthProp valueProperty } @@ -24,20 +76,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 } @@ -46,8 +93,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 @@ -69,7 +116,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 { @@ -81,7 +128,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 { @@ -94,18 +141,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 + return prop +} + +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] + } + } + 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 a.baseObject.getOwnPropStr(idx.string()) } func (a *arrayObject) sortLen() int64 { @@ -124,160 +199,96 @@ 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 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 n.String() == "length" { - return a.getLengthProp() - } - return a.baseObject.getProp(n) +func (a *arrayObject) getStr(name unistring.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) getOwnProp(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.getOwnProp(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 unistring.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 unistring.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) { 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 { @@ -285,74 +296,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) 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) hasOwnProperty(n Value) bool { - if idx := toIdx(n); idx >= 0 { - return idx < int64(len(a.values)) && a.values[idx] != nil && a.values[idx] != _undefined +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 { - return a.baseObject.hasOwnProperty(n) + return a.baseObject.hasOwnPropertyStr(name) } } -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 - } else { - return a.baseObject.hasOwnPropertyStr(name) +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 int64) bool { +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 } @@ -361,7 +383,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 { @@ -395,44 +417,54 @@ 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(unistring.String(strconv.FormatUint(uint64(idx), 10)), existing, desc, throw) + if ok { + if idx >= 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) defineOwnPropertyStr(name unistring.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) _deleteProp(idx int64, throw bool) bool { - if idx < int64(len(a.values)) { +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 { - 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-- @@ -444,18 +476,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 unistring.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{} { @@ -473,10 +505,127 @@ 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 unistring.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 unistring.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 unistring.String) int { + if bits.UintSize == 64 { + return int(strToIdx64(s)) + } + i := strToIdx(s) + if i >= math.MaxInt32 { + return -1 + } + return int(i) } diff --git a/array_sparse.go b/array_sparse.go index 50340caf..17b64c5b 100644 --- a/array_sparse.go +++ b/array_sparse.go @@ -2,20 +2,23 @@ package goja import ( "math" + "math/bits" "reflect" "sort" "strconv" + + "github.com/dop251/goja/unistring" ) 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 +30,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 +39,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 +77,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 +89,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,63 +102,71 @@ 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 unistring.String, receiver Value) Value { + return a.getStrWithOwnProp(a.getOwnPropStr(name), name, receiver) +} - 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) + } + } + if prop, ok := prop.(*valueProperty); ok { + if receiver == nil { + return prop.get(a.val) + } + return prop.get(receiver) } - return a.baseObject.getProp(n) + 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) getOwnProp(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 +func (a *sparseArrayObject) getOwnPropStr(name unistring.String) Value { + if idx := strToIdx(name); idx != math.MaxUint32 { + return a._getIdx(idx) } if name == "length" { return a.getLengthProp() } - return a.baseObject.getOwnProp(name) + 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 { @@ -163,37 +174,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{ @@ -201,57 +201,61 @@ 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 unistring.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 unistring.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) { 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 { @@ -259,70 +263,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 unistring.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 @@ -332,56 +341,66 @@ 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(unistring.String(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 unistring.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 { 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-- @@ -393,49 +412,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 unistring.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..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,10 +169,15 @@ type ( } VariableExpression struct { - Name string + Name unistring.String Idx file.Idx Initializer Expression } + + MetaProperty struct { + Meta, Property *Identifier + Idx file.Idx + } ) // _expressionNode @@ -197,6 +203,7 @@ func (*StringLiteral) _expressionNode() {} func (*ThisExpression) _expressionNode() {} func (*UnaryExpression) _expressionNode() {} func (*VariableExpression) _expressionNode() {} +func (*MetaProperty) _expressionNode() {} // ========= // // Statement // @@ -263,6 +270,13 @@ type ( Body Statement } + ForOfStatement struct { + For file.Idx + Into Expression + Source Expression + Body Statement + } + ForStatement struct { For file.Idx Initializer Expression @@ -338,6 +352,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() {} @@ -413,6 +428,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 } @@ -424,6 +440,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() } @@ -471,6 +488,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 } @@ -482,6 +502,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/builtin_array.go b/builtin_array.go index c4f46b46..64e49d76 100644 --- a/builtin_array.go +++ b/builtin_array.go @@ -1,45 +1,124 @@ package goja import ( - "bytes" "math" "sort" - "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 = uint32(len(values)) + a.objCount = len(values) + return a +} + +func setArrayLength(a *arrayObject, l int64) *arrayObject { + a.setOwnStr("length", intToValue(l), true) + return a +} + +func arraySpeciesCreate(obj *Object, size int64) *Object { + if isArray(obj) { + v := obj.self.getStr("constructor", nil) + if constructObj, ok := v.(*Object); ok { + v = constructObj.self.getSym(symSpecies, nil) + if v == _null { + v = nil + } + } + + if v != nil && v != _undefined { + constructObj, _ := v.(*Object) + if constructObj != nil { + if constructor := constructObj.self.assertConstructor(); constructor != nil { + return constructor([]Value{intToValue(size)}, constructObj) + } + } + 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) + } + return max(l+rel, 0) +} + +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) - } 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")) } } - 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 } } 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 } @@ -48,16 +127,16 @@ 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 { - l := toLength(obj.self.getStr("length")) +func (r *Runtime) arrayproto_pop_generic(obj *Object) Value { + 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 } @@ -68,16 +147,16 @@ 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 { // 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 @@ -87,44 +166,44 @@ func (r *Runtime) arrayproto_pop(call FunctionCall) Value { } return _undefined } else { - return r.arrayproto_pop_generic(obj, call) + return r.arrayproto_pop_generic(obj) } } func (r *Runtime) arrayproto_join(call FunctionCall) Value { o := call.This.ToObject(r) - l := int(toLength(o.self.getStr("length"))) - sep := "" + l := int(toLength(o.self.getStr("length", nil))) + var sep valueString if s := call.Argument(0); s != _undefined { - sep = s.String() + sep = s.toString() } else { - sep = "," + sep = asciiString(",") } if l == 0 { return stringEmpty } - var buf bytes.Buffer + var buf valueStringBuilder - element0 := o.self.get(intToValue(0)) + 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.get(intToValue(int64(i))) + 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 { 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{ @@ -137,92 +216,74 @@ func (r *Runtime) arrayproto_toString(call FunctionCall) Value { }) } -func (r *Runtime) writeItemLocaleString(item Value, buf *bytes.Buffer) { +func (r *Runtime) writeItemLocaleString(item Value, buf *valueStringBuilder) { 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.toString()) return } } - r.typeErrorResult(true, "Property 'toLocaleString' of object %s is not a function", itemObj) - } -} - -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) + r.typeErrorResult(true, "Property 'toLocaleString' of object %s is not a function", item) } - 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 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) } - return newStringValue(buf.String()) } else { - return r.arrayproto_toLocaleString_generic(array, 0, bytes.NewBuffer(nil)) + length := toLength(array.self.getStr("length", nil)) + for i := int64(0); i < length; i++ { + if i > 0 { + buf.WriteRune(',') + } + item := array.self.getIdx(valueInt(i), nil) + r.writeItemLocaleString(item, &buf) + } } + return buf.String() +} + +func isConcatSpreadable(obj *Object) bool { + spreadable := obj.self.getSym(symIsConcatSpreadable, nil) + 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, - Enumerable: FLAG_TRUE, - Configurable: FLAG_TRUE, - } - - aLength := toLength(a.self.getStr("length")) - if obj, ok := item.(*Object); ok { - if isArray(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) - } + aLength := toLength(a.self.getStr("length", nil)) + if obj, ok := item.(*Object); ok && isConcatSpreadable(obj) { + length := toLength(obj.self.getStr("length", nil)) + for i := int64(0); i < length; i++ { + v := obj.self.getIdx(valueInt(i), nil) + if v != nil { + createDataPropertyOrThrow(a, intToValue(aLength), v) } - return + aLength++ } + } else { + createDataPropertyOrThrow(a, intToValue(aLength), item) + aLength++ } - descr.Value = item - a.self.defineOwnProperty(intToValue(aLength), descr, false) + a.self.setOwnStr("length", intToValue(aLength), true) } 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) @@ -230,58 +291,38 @@ 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")) - start := call.Argument(0).ToInteger() - if start < 0 { - start = max(length+start, 0) - } else { - start = min(start, 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 { 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 { count = 0 } - a := r.newArrayLength(count) - n := int64(0) - descr := propertyDescr{ - Writable: FLAG_TRUE, - Enumerable: FLAG_TRUE, - Configurable: FLAG_TRUE, + 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 + } } + + n := int64(0) for start < end { - p := o.self.get(intToValue(start)) - if p != nil && p != _undefined { - descr.Value = p - a.self.defineOwnProperty(intToValue(n), descr, false) + p := o.self.getIdx(valueInt(start), nil) + if p != nil { + createDataPropertyOrThrow(a, valueInt(n), p) } start++ n++ @@ -293,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{ @@ -309,89 +355,141 @@ 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")) - relativeStart := call.Argument(0).ToInteger() - var actualStart int64 - if relativeStart < 0 { - actualStart = max(length+relativeStart, 0) - } else { - actualStart = min(relativeStart, length) + length := toLength(o.self.getStr("length", nil)) + actualStart := relToIdx(call.Argument(0).ToInteger(), length) + 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) } - - 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) - } - } - + 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++ { + createDataPropertyOrThrow(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) - if o.self.hasProperty(from) { - o.self.put(to, o.self.get(from), true) - } else { - o.self.delete(to, true) + src.values = values + src.objCount = len(values) + } else { + for k := int64(0); k < actualDeleteCount; k++ { + from := valueInt(k + actualStart) + if o.self.hasPropertyIdx(from) { + createDataPropertyOrThrow(a, valueInt(k), o.self.getIdx(from, nil)) } } - } - 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 := 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.deleteIdx(to, true) + } + } + + for k := length; k > length-actualDeleteCount+itemCount; k-- { + o.self.deleteIdx(valueInt(k-1), true) + } + } else if itemCount > actualDeleteCount { + for k := length - actualDeleteCount; k > actualStart; k-- { + 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.deleteIdx(to, true) + } + } + } + + if itemCount > 0 { + for i, item := range call.Arguments[2:] { + o.self.setOwnIdx(valueInt(actualStart+int64(i)), item, true) + } } } - o.self.putStr("length", intToValue(length-actualDeleteCount+itemCount), 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)) - 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) + 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]) } 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 = int(arr.length) + } else { + for k := length - 1; k >= 0; k-- { + 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.deleteIdx(to, true) + } } - } - for k, arg := range call.Arguments { - o.self.put(intToValue(int64(k)), arg, true) + for k, arg := range call.Arguments { + o.self.setOwnIdx(valueInt(int64(k)), arg, true) + } } - newLen := intToValue(length + argCount) - 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) } @@ -407,9 +505,18 @@ 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 { + idx := valueInt(n) + if val := o.self.getIdx(idx, nil); val != nil { if searchElement.StrictEquals(val) { return idx } @@ -419,9 +526,50 @@ func (r *Runtime) arrayproto_indexOf(call FunctionCall) Value { 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")) + length := toLength(o.self.getStr("length", nil)) if length == 0 { return intToValue(-1) } @@ -441,9 +589,19 @@ 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 { + idx := valueInt(k) + if val := o.self.getIdx(idx, nil); val != nil { if searchElement.StrictEquals(val) { return idx } @@ -455,128 +613,141 @@ 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 - } + 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 := valueInt(k) + if val := o.self.getIdx(idx, nil); 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 } 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 - } + 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 := valueInt(k) + if val := o.self.getIdx(idx, nil); 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 } 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) - } + 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 := valueInt(k) + if val := o.self.getIdx(idx, nil); 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 } 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++ + length := toLength(o.self.getStr("length", nil)) + 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 := valueInt(k) + if val := o.self.getIdx(idx, nil); 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 := valueInt(k) + if val := o.self.getIdx(idx, nil); val != nil { + fc.Arguments[0] = val + fc.Arguments[1] = idx + createDataPropertyOrThrow(a, idx, callbackFn(fc)) + } + } + return a } 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 := 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 := valueInt(k) + if val := o.self.getIdx(idx, nil); 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 { + idx := valueInt(k) + if val := o.self.getIdx(idx, nil); val != nil { fc.Arguments[0] = val fc.Arguments[1] = idx if callbackFn(fc).ToBoolean() { - a.values = append(a.values, val) + createDataPropertyOrThrow(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)) } @@ -585,7 +756,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{ @@ -599,8 +770,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 } @@ -613,8 +784,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) @@ -629,7 +800,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{ @@ -643,8 +814,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 } @@ -657,8 +828,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) @@ -672,24 +843,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) @@ -698,35 +869,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 { - l := a.length + if a := r.checkStdArrayObj(o); a != nil { + l := len(a.values) middle := l / 2 - al := int64(len(a.values)) - for lower := int64(0); lower != middle; lower++ { + for lower := 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 { @@ -737,28 +885,265 @@ 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)) - if v != nil && v != _undefined { - o.self.put(intToValue(i-1), v, true) + v := o.self.getIdx(valueInt(i), nil) + if v != nil { + 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 } +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", nil)) + 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 + } + final := relToIdx(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 + to = to + count - 1 + } else { + dir = 1 + } + for count > 0 { + if o.self.hasPropertyIdx(valueInt(from)) { + o.self.setOwnIdx(valueInt(to), o.self.getIdx(valueInt(from), nil), true) + } else { + o.self.deleteIdx(valueInt(to), true) + } + from += dir + to += dir + count-- + } + + 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", nil)) + 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.setOwnIdx(valueInt(k), value, true) + } + } + return o +} + +func (r *Runtime) arrayproto_find(call FunctionCall) Value { + o := call.This.ToObject(r) + 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 := valueInt(k) + kValue := o.self.getIdx(idx, nil) + fc.Arguments[0], fc.Arguments[1] = kValue, idx + if predicate(fc).ToBoolean() { + return kValue + } + } + + return _undefined +} + +func (r *Runtime) arrayproto_findIndex(call FunctionCall) Value { + o := call.This.ToObject(r) + 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 := 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) +} + +func (r *Runtime) checkStdArrayObj(obj *Object) *arrayObject { + if arr, ok := obj.self.(*arrayObject); ok && + arr.propValueCount == 0 && + arr.length == uint32(len(arr.values)) && + uint32(arr.objCount) == arr.length { + + return arr + } + + return nil +} + +func (r *Runtime) checkStdArray(v Value) *arrayObject { + if obj, ok := v.(*Object); ok { + return r.checkStdArrayObj(obj) + } + + return nil +} + +func (r *Runtime) checkStdArrayIter(v Value) *arrayObject { + if arr := r.checkStdArray(v); arr != nil && + arr.getSym(symIterator, nil) == 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)) + copy(items, arr.values) + return r.newArrayValues(items) + } + } + + var ctor func(args []Value, newTarget *Object) *Object + if call.This != r.global.Array { + if o, ok := call.This.(*Object); ok { + if c := o.self.assertConstructor(); c != nil { + ctor = c + } + } + } + var arr *Object + if usingIterator := toMethod(r.getV(items, symIterator)); usingIterator != nil { + if ctor != nil { + arr = ctor([]Value{}, nil) + } else { + arr = r.newArrayValues(nil) + } + iter := r.getIterator(items, usingIterator) + if mapFn == nil { + if a := r.checkStdArrayObj(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)}}) + } + createDataPropertyOrThrow(arr, intToValue(k), val) + k++ + }) + arr.self.setOwnStr("length", intToValue(k), true) + } else { + arrayLike := items.ToObject(r) + l := toLength(arrayLike.self.getStr("length", nil)) + if ctor != nil { + arr = ctor([]Value{intToValue(l)}, nil) + } else { + arr = r.newArrayValues(nil) + } + if mapFn == nil { + if a := r.checkStdArrayObj(arr); a != nil { + values := make([]Value, l) + for k := int64(0); k < l; k++ { + values[k] = nilSafe(arrayLike.self.getIdx(valueInt(k), nil)) + } + setArrayValues(a, values) + return arr + } + } + for k := int64(0); k < l; k++ { + 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) + } + createDataPropertyOrThrow(arr, idx, item) + } + arr.self.setOwnStr("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) { @@ -768,6 +1153,37 @@ func (r *Runtime) array_isArray(call FunctionCall) Value { return valueFalse } +func (r *Runtime) array_of(call FunctionCall) Value { + var ctor func(args []Value, newTarget *Object) *Object + if call.This != r.global.Array { + if o, ok := call.This.(*Object); ok { + if c := o.self.assertConstructor(); 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}, nil) + for i, val := range call.Arguments { + createDataPropertyOrThrow(arr, intToValue(int64(i)), val) + } + arr.self.setOwnStr("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 { + 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{ @@ -780,38 +1196,81 @@ 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("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) + 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.global.arrayToString, 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("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) + r.global.arrayValues = valuesFunc + o._putProp("values", valuesFunc, true, false, true) + + o._putSym(symIterator, valueProp(valuesFunc, true, 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 } 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, + configurable: 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._putSym(symToStringTag, valueProp(asciiString(classArrayIterator), false, false, true)) + return o } 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 r.global.ArrayPrototype = r.newLazyObject(r.createArrayProto) @@ -835,7 +1294,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 } @@ -860,8 +1319,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() @@ -876,7 +1335,7 @@ func (ctx *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_arrray_test.go b/builtin_arrray_test.go new file mode 100644 index 00000000..a83687fd --- /dev/null +++ b/builtin_arrray_test.go @@ -0,0 +1,267 @@ +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) +} + +func TestArrayFrom(t *testing.T) { + const SCRIPT = ` + 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]"); + assert.sameValue(dest[1], undefined, prefix + ": [1]"); + assert(dest.hasOwnProperty("1"), prefix + ': hasOwnProperty("1")'); + assert.sameValue(dest[2], 3, prefix + ": [2]"); + } + + 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; + } + 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}; + checkDestHoles(Array.from(src), "arrayLike src/std dest"); + + function A() {} + A.from = Array.from; + + checkDest(A.from(source), "std 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, { + 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) +} + +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) +} + +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 f4bbdbc6..ab24235d 100644 --- a/builtin_date.go +++ b/builtin_date.go @@ -6,117 +6,73 @@ 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.assertInt(); ok { - return 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.assertString(); ok { - return dateParse(val.String()) + if o, ok := args[0].(*Object); ok { + if d, ok := o.self.(*dateObject); ok { + t = d.time() + valid = true + } } - - var n int64 - if i, ok := pv.assertInt(); ok { - n = i - } else if f, ok := pv.assertFloat(); ok { - if math.IsNaN(f) || math.IsInf(f, 0) { - return + if !valid { + pv := toPrimitive(args[0]) + if val, ok := pv.(valueString); ok { + return dateParse(val.String()) } - if math.Abs(f) > maxTime { - return + 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() } - 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) *Object { - t, isSet := r.makeDate(args, loc) - return r.newDateObject(t, isSet) +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) *Object { - return r.newDateTime(args, time.Local) +func (r *Runtime) builtin_newDate(args []Value, proto *Object) *Object { + return r.newDateTime(args, proto) } -func (r *Runtime) builtin_date(call FunctionCall) Value { +func (r *Runtime) builtin_date(FunctionCall) Value { return asciiString(dateFormat(r.now())) } 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)) } @@ -124,48 +80,52 @@ 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 } return intToValue(timeToMsec(t)) } -func (r *Runtime) date_now(call FunctionCall) Value { +func (r *Runtime) date_now(FunctionCall) Value { return intToValue(timeToMsec(r.now())) } 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)) @@ -176,22 +136,20 @@ 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() - if f, ok := tv.assertFloat(); ok { + tv := obj.toPrimitiveNumber() + 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 { - 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, @@ -199,648 +157,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 + } + 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()) } - r.typeErrorResult(true, "Method Date.prototype.setTime is called on incompatible receiver") - panic("Unreachable") + + 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 d.isSet() { + t, ok := _dateSetFullYear(d.time(), call, -3, false) + if !ok { + d.unset() + return _NaN } - if len(call.Arguments) > 3 { - nsec = int(call.Arguments[3].ToInteger() * 1e6) - } else { - nsec = d.time.Nanosecond() - } - 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 { @@ -897,6 +991,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 } @@ -910,24 +1006,9 @@ 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 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/builtin_error.go b/builtin_error.go index 5e95b426..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 @@ -8,54 +15,41 @@ 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{}) - 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 075cd312..c8a91f8b 100644 --- a/builtin_function.go +++ b/builtin_function.go @@ -5,20 +5,26 @@ 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("})")) - return 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 } func (r *Runtime) functionproto_toString(call FunctionCall) Value { @@ -28,34 +34,65 @@ 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 + case *proxyObject: + var name string + repeat2: + switch c := f.target.self.(type) { + case *funcObject: + name = c.src + case *nativeFuncObject: + name = nilSafe(c.nameProp.get(call.This)).toString().String() + case *boundFuncObject: + name = nilSafe(c.nameProp.get(call.This)).toString().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) 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 ret + + return valueFalse +} + +func (r *Runtime) createListFromArrayLike(a Value) []Value { + o := r.toObject(a) + if arr := r.checkStdArrayObj(o); arr != nil { + return arr.values + } + 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 +100,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 +131,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,47 +140,37 @@ 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 } + 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, } //ret := r.newNativeFunc(r.boundCallable(f, call.Arguments), nil, "", nil, l) @@ -153,12 +181,15 @@ repeat: } 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/builtin_global.go b/builtin_global.go index da41f7e2..71bfdde8 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" ) @@ -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 { @@ -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 } @@ -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 { @@ -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 { @@ -217,30 +217,30 @@ 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++ { + 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 == '/' { @@ -261,17 +261,18 @@ 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 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 a17e3766..2d2e9e74 100644 --- a/builtin_json.go +++ b/builtin_json.go @@ -7,12 +7,15 @@ import ( "io" "math" "strings" + "unicode/utf16" + + "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())) + d := json.NewDecoder(bytes.NewBufferString(call.Argument(0).toString().String())) value, err := r.builtinJSON_decodeValue(d) if err != nil { @@ -31,7 +34,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 +88,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(unistring.NewFromString(key), value, true, true, true) } return object, nil } @@ -138,40 +131,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, ok := value.(*Object); ok { 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() { - value := r.builtinJSON_reviveWalk(reviver, object, newStringValue(item.name)) + for _, itemName := range object.self.ownKeys(false, nil) { + value := r.builtinJSON_reviveWalk(reviver, object, itemName) 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 +183,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 { + value := replacer.self.getIdx(valueInt(int64(index)), nil) + switch v := value.(type) { + case valueFloat, valueInt, valueString: name = value.String() - } else if _, ok := value.assertFloat(); ok { - 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 +222,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 +237,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 +256,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 +365,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 +417,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 } @@ -449,7 +428,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 { @@ -478,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 { @@ -504,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) + } } } } @@ -515,6 +502,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/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_map.go b/builtin_map.go new file mode 100644 index 00000000..20dc414d --- /dev/null +++ b/builtin_map.go @@ -0,0 +1,271 @@ +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(mo.val.runtime.getHash()) +} + +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, 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{} + mo.class = classMap + mo.val = o + mo.extensible = true + o.self = mo + mo.prototype = proto + mo.init() + if len(args) > 0 { + if arg := args[0]; arg != nil && arg != _undefined && arg != _null { + adder := mo.getStr("set", nil) + iter := r.getIterator(arg, nil) + 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.getIdx(i0, nil)) + v := nilSafe(itemObj.self.getIdx(i1, nil)) + 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.getIdx(i0, nil) + v := itemObj.self.getIdx(i1, nil) + 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.setOwnStr("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._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.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, + configurable: 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._putSym(symToStringTag, valueProp(asciiString(classMapIterator), false, false, 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_map_test.go b/builtin_map_test.go new file mode 100644 index 00000000..7880305b --- /dev/null +++ b/builtin_map_test.go @@ -0,0 +1,101 @@ +package goja + +import ( + "hash/maphash" + "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) +} + +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_math.go b/builtin_math.go index f6ec2162..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 @@ -88,15 +178,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) } @@ -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/builtin_number.go b/builtin_number.go index a1516398..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 { @@ -10,22 +11,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 { - return this - } - - if _, ok := this.assertFloat(); ok { + switch t := this.(type) { + case valueInt, valueFloat: 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 { @@ -71,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 } - return asciiString(strconv.FormatFloat(num, 'e', int(prec), 64)) + 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(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 asciiString(strconv.FormatFloat(num, 'g', int(prec), 64)) + 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 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/builtin_object.go b/builtin_object.go index 050512d7..1ca89ac1 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 { @@ -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 := call.Argument(1).String() - desc := obj.self.getOwnProp(propName) +func (r *Runtime) valuePropToDescriptorObject(desc Value) Value { if desc == nil { return _undefined } @@ -49,58 +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(obj.self.ownKeys(true, nil)) +} + +func (r *Runtime) object_getOwnPropertySymbols(call FunctionCall) Value { + obj := call.Argument(0).ToObject(r) + return r.newArrayValues(obj.self.ownSymbols(true, nil)) +} + +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 } - return r.newArrayValues(values) + + 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 { @@ -116,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()) @@ -127,19 +179,20 @@ func (r *Runtime) toPropertyDescr(v Value) (ret propertyDescr) { func (r *Runtime) _defineProperties(o *Object, p Value) { type propItem struct { - name string - prop propertyDescr + name Value + 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.ownPropertyKeys(false, nil) + list := make([]propItem, 0, len(names)) + for _, itemName := range names { list = append(list, propItem{ - name: item.name, - prop: r.toPropertyDescr(props.self.getStr(item.name)), + name: itemName, + prop: r.toPropertyDescriptor(props.get(itemName, nil)), }) } for _, prop := range list { - o.self.defineOwnProperty(newStringValue(prop.name), prop.prop, true) + o.defineOwnProperty(prop.name, prop.prop, true) } } @@ -163,8 +216,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") @@ -182,13 +235,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.getOwnProp(item.name) + for _, key := range obj.self.ownPropertyKeys(true, nil) { + v := obj.getOwnProp(key) if prop, ok := v.(*valueProperty); ok { if !prop.configurable { continue @@ -196,11 +249,10 @@ func (r *Runtime) object_seal(call FunctionCall) Value { prop.configurable = false } else { descr.Value = v - obj.self.defineOwnProperty(newStringValue(item.name), descr, true) - //obj.self._putProp(item.name, v, true, true, false) + obj.defineOwnProperty(key, descr, true) } } - obj.self.preventExtensions() + obj.self.preventExtensions(false) return obj } return arg @@ -209,13 +261,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.getOwnProp(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 { @@ -223,10 +275,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) } } - obj.self.preventExtensions() + obj.self.preventExtensions(false) return obj } else { // ES6 behavior @@ -237,7 +289,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 @@ -251,8 +303,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.getOwnProp(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 @@ -261,10 +313,6 @@ func (r *Runtime) object_isSealed(call FunctionCall) Value { return valueFalse } } - } else { - // ES6 - //r.typeErrorResult(true, "Object.isSealed called on non-object") - return valueTrue } return valueTrue } @@ -274,8 +322,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.getOwnProp(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 @@ -284,10 +332,6 @@ func (r *Runtime) object_isFrozen(call FunctionCall) Value { return valueFalse } } - } else { - // ES6 - //r.typeErrorResult(true, "Object.isFrozen called on non-object") - return valueTrue } return valueTrue } @@ -306,24 +350,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 := call.Argument(0).String() + p := toPropertyKey(call.Argument(0)) o := call.This.ToObject(r) - if o.self.hasOwnPropertyStr(p) { + if o.hasOwnProperty(p) { return valueTrue } else { return valueFalse @@ -347,9 +382,9 @@ func (r *Runtime) objectproto_isPrototypeOf(call FunctionCall) Value { } func (r *Runtime) objectproto_propertyIsEnumerable(call FunctionCall) Value { - p := call.Argument(0).ToString() + p := toPropertyKey(call.Argument(0)) o := call.This.ToObject(r) - pv := o.self.getOwnProp(p.String()) + pv := o.getOwnProp(p) if pv == nil { return valueFalse } @@ -367,22 +402,103 @@ 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 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() + } + } + return newStringValue(fmt.Sprintf("[object %s]", clsName)) } } 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_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) } +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 _, 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) + } + } + } + } + + return to +} + +func (r *Runtime) object_is(call FunctionCall) Value { + return r.toBoolean(call.Argument(0).SameAs(call.Argument(1))) +} + +func (r *Runtime) toProto(proto Value) *Object { + if proto != _null { + if obj, ok := proto.(*Object); ok { + 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 { + o.self.setProto(proto, true) + } + + 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) @@ -391,14 +507,22 @@ 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 + 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) 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) @@ -407,6 +531,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_proxy.go b/builtin_proxy.go new file mode 100644 index 00000000..a4236ac7 --- /dev/null +++ b/builtin_proxy.go @@ -0,0 +1,304 @@ +package goja + +import ( + "fmt" + + "github.com/dop251/goja/unistring" +) + +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(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, unistring.String(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(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) + return r.ToValue(s) + } + } + panic(r.NewTypeError("%s needs to be called with target as Object", name)) + }, nil, unistring.String(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 { + switch p := call.Argument(1).(type) { + case *valueSymbol: + return _undefined + default: + 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(unistring.String(name), r.newNativeFunc(func(call FunctionCall) Value { + if len(call.Arguments) >= 2 { + if t, ok := call.Argument(0).(*Object); ok { + switch p := call.Argument(1).(type) { + case *valueSymbol: + return valueFalse + default: + 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, unistring.String(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 r, ok := call.Argument(2).(*Object); ok { + switch p := call.Argument(1).(type) { + case *valueSymbol: + return _undefined + default: + 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) + } +} + +// 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) + + // 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, 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 { + handler := r.newNativeProxyHandler(nativeHandler) + proxy := r.newProxyObject(target, handler, nil) + 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.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 +} + +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..8344f153 --- /dev/null +++ b/builtin_proxy_test.go @@ -0,0 +1,937 @@ +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); + } + }); + 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 = ` + 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_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]; + } + }); + 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(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) + } + 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) { + 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) +} + +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_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 3a4d524b..35dfdea3 100644 --- a/builtin_regexp.go +++ b/builtin_regexp.go @@ -2,9 +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 { @@ -20,19 +22,167 @@ 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, proto *Object) *Object { o := r.newRegexpObject(proto) o.pattern = pattern o.source = patternStr - o.global = global - o.ignoreCase = ignoreCase - o.multiline = multiline return o.val } -func compileRegexp(patternStr, flags string) (p regexpPattern, global, ignoreCase, multiline 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() { @@ -58,6 +208,17 @@ func compileRegexp(patternStr, flags string) (p regexpPattern, global, ignoreCas return } ignoreCase = true + case 'y': + if sticky { + invalidFlags() + return + } + sticky = true + case 'u': + if unicode { + invalidFlags() + } + unicode = true default: invalidFlags() return @@ -65,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" @@ -83,190 +250,907 @@ 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, 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, 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) builtin_newRegExp(args []Value) *Object { +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 regexp, ok := obj.self.(*regexpObject); ok { - if len(args) < 2 || args[1] == _undefined { - return regexp.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(regexp.source, args[1].String(), r.global.RegExpPrototype) + 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, r.global.RegExpPrototype) +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) + + panic(r.NewTypeError("Method RegExp.prototype.compile called on incompatible receiver %s", call.This.toString())) } 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 } } func (r *Runtime) regexpproto_toString(call FunctionCall) Value { - if this, ok := r.toObject(call.This).self.(*regexpObject); ok { - var g, i, m 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') } - return newStringValue(fmt.Sprintf("/%s/%s%s%s", this.source.String(), g, i, m)) - } else { - r.typeErrorResult(true, "Method RegExp.prototype.toString called on incompatible receiver %s", call.This) - return nil + if this.pattern.multiline { + sb.WriteRune('m') + } + 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 } } 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 } } 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 } } 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 } } 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 } } 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 + } +} + +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.pattern.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, unicode bool + + thisObj := r.toObject(call.This) + 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 ignoreCase { + size++ + } + } + 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 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') + } + if ignoreCase { + sb.WriteByte('i') + } + if multiline { + sb.WriteByte('m') + } + if unicode { + sb.WriteByte('u') + } + 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) 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() { + a := r.getGlobalRegexpMatches(rxObj, s) + if len(a) == 0 { + return _null + } + 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() + if !ok { + panic(r.NewTypeError("exec is not a function")) + } + + 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 + } + + if !rx.standard || rx.prototype == nil || rx.prototype.self != r.global.stdRegexpProto { 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.pattern.global { + rx.setOwnStr("lastIndex", intToValue(0), true) + var a []Value + var previousLastIndex int64 + for { + match, result := rx.execRegexp(s) + if !match { + break + } + thisIndex := rx.getStr("lastIndex", nil).ToInteger() + if thisIndex == previousLastIndex { + previousLastIndex = int64(advanceStringIndex(s, toInt(previousLastIndex), rx.pattern.unicode)) + rx.setOwnStr("lastIndex", intToValue(previousLastIndex), true) + } else { + previousLastIndex = thisIndex + } + a = append(a, s.substring(result[0], 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 := 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) + currentLastIndex := nilSafe(rx.getStr("lastIndex", nil)) + if !currentLastIndex.SameAs(previousLastIndex) { + rx.setOwnStr("lastIndex", previousLastIndex, true) + } + + if result == _null { + return intToValue(-1) + } + + return r.toObject(result).self.getStr("index", nil) +} + +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", nil) + rx.setOwnStr("lastIndex", intToValue(0), true) + + match, result := rx.execRegexp(s) + rx.setOwnStr("lastIndex", previousLastIndex, true) + + if !match { + return intToValue(-1) + } + return intToValue(int64(result[0])) +} + +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 { + lim = maxInt - 1 + } else { + lim = toLength(limit) + } + 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 { + if r.regExpExec(execFn, splitter, s) == _null { + a = append(a, s) + } + return r.newArrayValues(a) + } + + q := p + for q < size { + splitter.self.setOwnStr("lastIndex", intToValue(int64(q)), true) + z := r.regExpExec(execFn, splitter, s) + if z == _null { + q = advanceStringIndex(s, q, unicodeMatching) + } else { + z := r.toObject(z) + e := toLength(splitter.self.getStr("lastIndex", nil)) + if e == int64(p) { + q = advanceStringIndex(s, q, unicodeMatching) + } else { + a = append(a, s.substring(p, q)) + if int64(len(a)) == lim { + return r.newArrayValues(a) + } + 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)) + if int64(len(a)) == lim { + return r.newArrayValues(a) + } + } + q = p + } + } + } + a = append(a, s.substring(p, size)) + 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 !strings.Contains(flagsStr, "y") { + flags = newStringValue(flagsStr + "y") + } + splitter := c([]Value{rxObj, flags}, nil) + + s := call.Argument(0).toString() + limitValue := call.Argument(1) + search := r.checkStdRegexp(splitter) + if search == nil { + return r.regexpproto_stdSplitterGeneric(splitter, s, limitValue, strings.Contains(flagsStr, "u")) + } + + limit := -1 + if limitValue != _undefined { + limit = int(toUint32(limitValue)) + } + + if limit == 0 { + return r.newArrayValues(nil) + } + + targetLength := s.length() + var valueArray []Value + 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 + if match[0] == 0 || match[0] == targetLength { + continue + } + } + + if lastIndex != match[0] { + valueArray = append(valueArray, s.substring(lastIndex, 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(match[offset], match[offset+1]) + } else { + value = _undefined + } + valueArray = append(valueArray, value) + found++ + if found == limit { + goto RETURN + } + } + } + + if found != limit { + if lastIndex != targetLength { + valueArray = append(valueArray, s.substring(lastIndex, targetLength)) + } else { + valueArray = append(valueArray, stringEmpty) + } + } + +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 + 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.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.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), + accessor: true, + }, false) + o.setOwnStr("flags", &valueProperty{ + configurable: true, + getterFunc: r.newNativeFunc(r.regexpproto_getFlags, nil, "get flags", nil, 0), + accessor: true, + }, false) + + 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) + rx := r.global.RegExp.self + rx._putSym(symSpecies, &valueProperty{ + getterFunc: r.newNativeFunc(r.returnThis, nil, "get [Symbol.species]", nil, 0), + accessor: true, + configurable: true, + }) r.addToGlobal("RegExp", r.global.RegExp) } diff --git a/builtin_set.go b/builtin_set.go new file mode 100644 index 00000000..b3818b25 --- /dev/null +++ b/builtin_set.go @@ -0,0 +1,246 @@ +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(so.val.runtime.getHash()) +} + +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, 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{} + so.class = classSet + so.val = o + so.extensible = true + o.self = so + so.prototype = proto + so.init() + if len(args) > 0 { + if arg := args[0]; arg != nil && arg != _undefined && arg != _null { + adder := so.getStr("add", nil) + iter := r.getIterator(arg, 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.setOwnStr("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._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.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, + configurable: 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._putSym(symToStringTag, valueProp(asciiString(classSetIterator), false, false, 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_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_string.go b/builtin_string.go index d659466a..9039c726 100644 --- a/builtin_string.go +++ b/builtin_string.go @@ -1,14 +1,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 { @@ -20,19 +22,25 @@ func (r *Runtime) collator() *collate.Collator { return collator } +func toString(arg Value) valueString { + if s, ok := arg.(valueString); ok { + return s + } + if s, ok := arg.(*valueSymbol); ok { + return s.desc + } + 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 } } -func (r *Runtime) _newString(s valueString) *Object { +func (r *Runtime) _newString(s valueString, proto *Object) *Object { v := &Object{runtime: r} o := &stringObject{} @@ -40,7 +48,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 } @@ -48,32 +56,18 @@ 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 = args[0].ToString() + s = args[0].toString() } else { s = stringEmpty } - return r._newString(s) -} - -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 + return r._newString(s, proto) } 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 { @@ -93,19 +87,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) + 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) } @@ -115,34 +116,97 @@ func (r *Runtime) string_fromcharcode(call FunctionCall) Value { return asciiString(b) } +func (r *Runtime) string_fromcodepoint(call FunctionCall) Value { + var sb valueStringBuilder + for _, 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)) + } + sb.WriteRune(c) + } + 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 valueStringBuilder + 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() + 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() + 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 { 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) } @@ -151,14 +215,16 @@ 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()) } 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: @@ -167,7 +233,7 @@ func (r *Runtime) stringproto_concat(call FunctionCall) Value { pos++ } case unicodeString: - copy(buf[pos:], s) + copy(buf[pos:], s[1:]) pos += s.length() } } @@ -175,150 +241,334 @@ 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() - target := call.Argument(0).ToString() + value := call.This.toString() + target := call.Argument(0).toString() pos := call.Argument(1).ToInteger() 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 { 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 - if f, ok := numPos.assertFloat(); ok && math.IsNaN(f) { - pos = value.length() + if f, ok := numPos.(valueFloat); ok && math.IsNaN(float64(f)) { + 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 { 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))) } 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(r.getV(regexp, 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) } if rx == nil { - rx = r.builtin_newRegExp([]Value{regexp}).self.(*regexpObject) + rx = r.newRegExp(regexp, nil, r.global.RegExpPrototype).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, nil)).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 { - s := call.This.ToString() - var str string - var isASCII bool - if astr, ok := s.(asciiString); ok { - str = string(astr) - isASCII = true +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().toString().String() } else { - str = s.String() + 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")) } - searchValue := call.Argument(0) - replaceValue := call.Argument(1) - var found [][]int + if s, ok := s.(unicodeString); ok { + ss := s.String() + return newStringValue(f.String(ss)) + } - 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 - } + 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()) } - - if found == nil { - found = searchSubstringUTF8(str, searchValue.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)) } - if len(found) == 0 { + 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) - var buf bytes.Buffer - lastIndex := 0 + return sb.String() +} - var rcall func(FunctionCall) Value +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 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 + } + var str string + var isASCII bool + if astr, ok := s.(asciiString); ok { + str = string(astr) + isASCII = true + } + + var buf valueStringBuilder + + lastIndex := 0 + 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) @@ -328,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 @@ -339,98 +589,97 @@ 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 { r.checkObjectCoercible(call.This) - s := call.This.ToString() regexp := call.Argument(0) + if regexp != _undefined && regexp != _null { + if searcher := toMethod(r.getV(regexp, 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) } if rx == nil { - rx = r.builtin_newRegExp([]Value{regexp}).self.(*regexpObject) + rx = r.newRegExp(regexp, nil, r.global.RegExpPrototype).self.(*regexpObject) } - match, result := rx.execRegexp(s) - if !match { - return intToValue(-1) + if searcher, ok := r.toObject(rx.getSym(symSearch, nil)).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 { r.checkObjectCoercible(call.This) - s := call.This.ToString() + 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 { @@ -462,20 +711,28 @@ 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 } 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(r.getV(separatorValue, 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)) + limit = int(toUint32(limitValue)) } if limit == 0 { @@ -486,104 +743,65 @@ 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 - } - } + separator := separatorValue.toString().String() - 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) + excess := false + str := s.String() + if limit > len(str) { + limit = len(str) + } + splitLimit := limit + if limit > 0 { + splitLimit = limit + 1 + excess = true + } - } else { - separator := separatorValue.String() + // TODO handle invalid UTF-16 + 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) +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 r.newArrayValues(valueArray) } - + return valueTrue } func (r *Runtime) stringproto_substring(call FunctionCall) Value { r.checkObjectCoercible(call.This) - s := call.This.ToString() + 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 { @@ -607,32 +825,50 @@ 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 { 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() + // TODO handle invalid UTF-16 return newStringValue(strings.Trim(s.String(), parser.WhitespaceChars)) } +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)) +} + +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)) +} + func (r *Runtime) stringproto_substr(call FunctionCall) Value { - s := call.This.ToString() + r.checkObjectCoercible(call.This) + s := call.This.toString() start := call.Argument(0).ToInteger() var length int64 sl := int64(s.length()) @@ -651,33 +887,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.StringPrototype = r.builtin_newString([]Value{stringEmpty}) + r.global.StringIteratorPrototype = r.newLazyObject(r.createStringIterProto) + 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) 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) @@ -685,6 +950,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 ebf261eb..5ad3aede 100644 --- a/builtin_string_test.go +++ b/builtin_string_test.go @@ -88,3 +88,140 @@ 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) +} + +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) +} + +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_symbol.go b/builtin_symbol.go new file mode 100644 index 00000000..1e90a824 --- /dev/null +++ b/builtin_symbol.go @@ -0,0 +1,142 @@ +package goja + +import "github.com/dop251/goja/unistring" + +var ( + 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 { + var desc valueString + if arg := call.Argument(0); !IsUndefined(arg) { + desc = arg.toString() + } else { + desc = stringEmpty + } + return newSymbol(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 sym.desc +} + +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() + keyStr := key.string() + if v := r.symbolRegistry[keyStr]; v != nil { + return v + } + if r.symbolRegistry == nil { + r.symbolRegistry = make(map[unistring.String]*valueSymbol) + } + v := newSymbol(key) + r.symbolRegistry[keyStr] = 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 stringValueFromRaw(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)) + o._putSym(symToStringTag, valueProp(newStringValue("Symbol"), false, false, 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, + } { + n := s.desc.(asciiString) + n = n[len("Symbol(Symbol.") : len(n)-1] + o._putProp(unistring.String(n), 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/builtin_typedarrays.go b/builtin_typedarrays.go index 507c1843..42029bd0 100644 --- a/builtin_typedarrays.go +++ b/builtin_typedarrays.go @@ -1,105 +1,1443 @@ package goja -type objectArrayBuffer struct { - baseObject - data []byte +import ( + "fmt" + "math" + "sort" + "unsafe" + + "github.com/dop251/goja/unistring" +) + +type typedArraySortCtx struct { + ta *typedArrayObject + compare func(FunctionCall) Value + needValidate bool +} + +func (ctx *typedArraySortCtx) Len() int { + return ctx.ta.length +} + +func (ctx *typedArraySortCtx) Less(i, j int) bool { + if ctx.needValidate { + ctx.ta.viewedArrayBuf.ensureNotDetached() + ctx.needValidate = false + } + 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}, + }).ToNumber() + ctx.needValidate = true + 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) +} + +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 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 = allocByteSlice(r.toIndex(args[0])) + } + return b.val +} + +func (r *Runtime) arrayBufferProto_getByteLength(call FunctionCall) Value { + o := r.toObject(call.This) + if b, ok := o.self.(*arrayBufferObject); ok { + b.ensureNotDetached() + return intToValue(int64(len(b.data))) + } + panic(r.NewTypeError("Object is not ArrayBuffer: %s", o)) } -func (o *objectArrayBuffer) export() interface{} { - return o.data +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(call.Argument(0).ToInteger(), l) + var stop int64 + if arg := call.Argument(1); arg != _undefined { + stop = arg.ToInteger() + } 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 { + 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))) + } + b.ensureNotDetached() + + 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) _newArrayBuffer(proto *Object, o *Object) *objectArrayBuffer { - if o == nil { - o = &Object{runtime: r} +func (r *Runtime) arrayBuffer_isView(call FunctionCall) Value { + 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 +} + +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())) + } } - b := &objectArrayBuffer{ + 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 b + return o } -func (r *Runtime) builtin_ArrayBuffer(args []Value, proto *Object) *Object { - b := r._newArrayBuffer(proto, nil) - if len(args) > 0 { - b.data = make([]byte, toLength(args[0])) +func (r *Runtime) dataViewProto_getBuffer(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + return dv.viewedArrayBuf.val } - return b.val + panic(r.NewTypeError("Method get DataView.prototype.buffer called on incompatible receiver %s", call.This.String())) } -func (r *Runtime) arrayBufferProto_getByteLength(call FunctionCall) Value { - o := r.toObject(call.This) - if b, ok := o.self.(*objectArrayBuffer); ok { - return intToValue(int64(len(b.data))) +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)) } - r.typeErrorResult(true, "Object is not ArrayBuffer: %s", o) - panic("unreachable") + panic(r.NewTypeError("Method get DataView.prototype.byteLength called on incompatible receiver %s", call.This.String())) } -func (r *Runtime) arrayBufferProto_slice(call FunctionCall) Value { +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 { + 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 b, ok := o.self.(*objectArrayBuffer); ok { - l := int64(len(b.data)) - start := toLength(call.Argument(0)) - if start < 0 { - start = l + start + 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}, } - if start < 0 { - start = 0 - } else if start > l { - start = l + 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++ + } } - var stop int64 - if arg := call.Argument(1); arg != _undefined { - stop = toLength(arg) - if stop < 0 { - stop = int64(len(b.data)) + stop + 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)) } - if stop < 0 { - stop = 0 - } else if stop > l { - stop = l + 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())) +} + +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 + } + + n := call.Argument(1).ToInteger() + if n >= length { + return valueFalse + } + + 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 + } + 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) + var sep valueString + if s != _undefined { + sep = s.toString() } else { - stop = l + sep = asciiString(",") + } + l := ta.length + if l == 0 { + return stringEmpty + } + + var buf valueStringBuilder + + ta.viewedArrayBuf.ensureNotDetached() + element0 := ta.typedArray.get(0) + if element0 != nil && element0 != _undefined && element0 != _null { + buf.WriteString(element0.toString()) + } + + 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.toString()) + } + } + + return 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 + if fromIndex < 0 { + fromIndex = -1 // prevent underflow in toInt() on 32-bit platforms + } + } + } + + 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 valueStringBuilder + for i := 0; i < length; i++ { + ta.viewedArrayBuf.ensureNotDetached() + if i > 0 { + buf.WriteRune(',') + } + item := ta.typedArray.get(i) + r.writeItemLocaleString(item, &buf) } + return buf.String() + } + panic(r.NewTypeError("Method TypedArray.prototype.toLocaleString called on incompatible receiver %s", call.This.String())) +} - ret := r._newArrayBuffer(r.global.ArrayBufferPrototype, nil) +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())) +} - if stop > start { - ret.data = b.data[start:stop] +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 +} - return ret.val +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 } - r.typeErrorResult(true, "Object is not ArrayBuffer: %s", o) - panic("unreachable") + 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 = allocByteSlice(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 = 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 + 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 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 + 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..69cbe86d 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,285 @@ 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) +} + +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) +} diff --git a/builtin_weakmap.go b/builtin_weakmap.go new file mode 100644 index 00000000..583576ed --- /dev/null +++ b/builtin_weakmap.go @@ -0,0 +1,221 @@ +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[uint64]Value +} + +type weakMapObject struct { + baseObject + m *weakMap +} + +func newWeakMap() *weakMap { + return &weakMap{ + data: make(map[uint64]Value), + } +} + +func (wmo *weakMapObject) init() { + wmo.baseObject.init() + wmo.m = newWeakMap() +} + +func (wm *weakMap) removeId(id uint64) { + wm.Lock() + delete(wm.data, id) + 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) 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{} + wmo.class = classWeakMap + wmo.val = o + wmo.extensible = true + o.self = wmo + wmo.prototype = proto + wmo.init() + if len(args) > 0 { + if arg := args[0]; arg != nil && arg != _undefined && arg != _null { + adder := wmo.getStr("set", nil) + iter := r.getIterator(arg, nil) + i0 := valueInt(0) + i1 := valueInt(1) + if adder == r.global.weakMapAdder { + r.iterate(iter, func(item Value) { + itemObj := r.toObject(item) + k := itemObj.self.getIdx(i0, nil) + v := nilSafe(itemObj.self.getIdx(i1, nil)) + 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.getIdx(i0, nil) + v := itemObj.self.getIdx(i1, nil) + 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._putSym(symToStringTag, valueProp(asciiString(classWeakMap), false, false, true)) + + return o +} + +func (r *Runtime) createWeakMap(val *Object) objectImpl { + o := r.newNativeConstructOnly(val, r.builtin_newWeakMap, r.global.WeakMapPrototype, "WeakMap", 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..eda61865 --- /dev/null +++ b/builtin_weakmap_test.go @@ -0,0 +1,34 @@ +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() + 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..91e72fd5 --- /dev/null +++ b/builtin_weakset.go @@ -0,0 +1,172 @@ +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[uint64]struct{} +} + +type weakSetObject struct { + baseObject + s *weakSet +} + +func newWeakSet() *weakSet { + return &weakSet{ + data: make(map[uint64]struct{}), + } +} + +func (ws *weakSetObject) init() { + ws.baseObject.init() + ws.s = newWeakSet() +} + +func (ws *weakSet) removeId(id uint64) { + ws.Lock() + delete(ws.data, id) + 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.s.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.s.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.s.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, nil) + r.iterate(iter, func(val Value) { + adder(FunctionCall{This: s, Arguments: []Value{val}}) + }) +} + +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{} + wso.class = classWeakSet + wso.val = o + wso.extensible = true + o.self = wso + wso.prototype = proto + wso.init() + if len(args) > 0 { + if arg := args[0]; arg != nil && arg != _undefined && arg != _null { + adder := wso.getStr("add", nil) + if adder == r.global.weakSetAdder { + if arr := r.checkStdArrayIter(arg); arr != nil { + for _, v := range arr.values { + wso.s.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._putSym(symToStringTag, valueProp(asciiString(classWeakSet), false, false, true)) + + return o +} + +func (r *Runtime) createWeakSet(val *Object) objectImpl { + o := r.newNativeConstructOnly(val, r.builtin_newWeakSet, r.global.WeakSetPrototype, "WeakSet", 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..aee428e4 --- /dev/null +++ b/builtin_weakset_test.go @@ -0,0 +1,89 @@ +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() + runtime.GC() + wso := vm.Get("s").ToObject(vm).self.(*weakSetObject) + wso.s.Lock() + l := len(wso.s.data) + wso.s.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/compiler.go b/compiler.go index 47d7fb74..0dd9d759 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 ( @@ -40,7 +42,7 @@ type Program struct { code []instruction values []Value - funcName string + funcName unistring.String src *SrcFile srcMap []srcMapItem } @@ -57,7 +59,7 @@ type compiler struct { } type scope struct { - names map[string]uint32 + names map[unistring.String]uint32 outer *scope strict bool eval bool @@ -67,13 +69,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 @@ -112,9 +114,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), } } @@ -177,7 +179,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 { @@ -187,7 +189,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 { @@ -211,7 +213,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) } @@ -224,7 +226,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) } @@ -235,7 +237,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)) @@ -447,14 +449,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 79f9731c..479f0594 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 { @@ -115,6 +117,10 @@ type compiledNewExpr struct { args []compiledExpr } +type compiledNewTarget struct { + baseCompiledExpr +} + type compiledSequenceExpr struct { baseCompiledExpr sequence []compiledExpr @@ -150,7 +156,7 @@ type compiledBinaryExpr struct { type compiledVariableExpr struct { baseCompiledExpr - name string + name unistring.String initializer compiledExpr expr *ast.VariableExpression } @@ -232,6 +238,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 +254,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 +266,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") } @@ -332,7 +340,7 @@ func (e *compiledIdentifierExpr) emitGetterAndCallee() { } } -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) } @@ -365,7 +373,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) }) @@ -444,7 +452,7 @@ func (e *compiledIdentifierExpr) deleteExpr() compiledExpr { type compiledDotExpr struct { baseCompiledExpr left compiledExpr - name string + name unistring.String } func (e *compiledDotExpr) emitGetter(putOnStack bool) { @@ -866,7 +874,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 } @@ -941,6 +949,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++ { @@ -968,11 +993,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 := nilSafe(o.self.getStr("name", nil)).toString().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)) @@ -983,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) { @@ -1383,14 +1408,23 @@ func (c *compiler) compileObjectLiteral(v *ast.ObjectLiteral) compiledExpr { func (e *compiledArrayLiteral) emitGetter(putOnStack bool) { e.addSrcMap() + objCount := 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 == len(e.expr.Value) { + e.c.emit(newArray(objCount)) + } else { + e.c.emit(&newArraySparse{ + l: len(e.expr.Value), + objCount: objCount, + }) + } if !putOnStack { e.c.emit(pop) } @@ -1406,17 +1440,12 @@ 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, 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, - }) + e.c.emit(&newRegexp{pattern: pattern, src: newStringValue(e.expr.Pattern)}) } } @@ -1429,7 +1458,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) @@ -1533,7 +1562,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 2f546ecb..1b7af710 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) { @@ -28,6 +30,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 +67,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: @@ -122,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 @@ -200,7 +206,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, @@ -227,7 +233,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, @@ -299,7 +305,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: blockLoopEnum, outer: c.block, @@ -330,11 +336,47 @@ 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 unistring.String) { + c.block = &block{ + typ: blockLoopEnum, + 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, "") } -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, @@ -474,6 +516,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: @@ -488,17 +534,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) { @@ -512,6 +558,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 { @@ -522,17 +572,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) { @@ -684,7 +734,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/compiler_test.go b/compiler_test.go index f849471f..22e91d12 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 } @@ -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) { @@ -1972,6 +1972,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) +} + func TestReturnFromForInLoop(t *testing.T) { const SCRIPT = ` (function f() { @@ -1983,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; diff --git a/date.go b/date.go index 67aaf029..6dbc0ae3 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,20 +68,23 @@ 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) *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) - d.isSet = isSet + if isSet { + d.msec = timeToMsec(t) + } else { + d.msec = timeUnset + } return v } @@ -86,13 +92,49 @@ 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 { - return d.time + if d.isSet() { + return d.time() } return nil } + +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 def2df69..8905ece8 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,7 +50,84 @@ 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); +}; + +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; +} +` +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) { @@ -323,3 +403,39 @@ 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) +} + +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) + } +} + +func TestDateToJSON(t *testing.T) { + const SCRIPT = ` + Date.prototype.toJSON.call({ toISOString: function () { return 1; } }) + ` + testScript1(SCRIPT, intToValue(1), t) +} 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 new file mode 100644 index 00000000..207fb5fa --- /dev/null +++ b/ftoa/common.go @@ -0,0 +1,151 @@ +/* +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" +) + +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 []byte) (e, bits int, dblBits []byte) { + 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 + if de = int(d0 >> exp_shift); de != 0 { + z |= exp_msk1 + } + + y := d1 + if y != 0 { + dblBits = b[:8] + k = lo0bits(y) + y >>= k + if k != 0 { + stuffBits(dblBits, 4, y|z<<(32-k)) + z >>= k + } else { + stuffBits(dblBits, 4, y) + } + stuffBits(dblBits, 0, z) + if z != 0 { + i = 2 + } else { + i = 1 + } + } else { + dblBits = b[:4] + k = lo0bits(z) + z >>= k + stuffBits(dblBits, 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) + } + 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/dtoa.go b/ftoa/ftobasestr.go similarity index 57% rename from dtoa.go rename to ftoa/ftobasestr.go index 0962eb85..9dc9b2d0 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 @@ -203,11 +64,12 @@ func dtobasestr(num float64, radix int) string { word0 := uint32(dBits >> 32) word1 := uint32(dBits) - b, e, _ := d2b(df) + 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. */ - s2 := -int32((word0 >> exp_shift1) & (exp_mask >> exp_shift1)) + s2 := -int((word0 >> exp_shift1) & (exp_mask >> exp_shift1)) if s2 == 0 { s2 = -1 } @@ -226,6 +88,7 @@ func dtobasestr(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/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..a9d2d240 --- /dev/null +++ b/ftoa/ftostr.go @@ -0,0 +1,147 @@ +package ftoa + +import ( + "math" + "strconv" + + "github.com/dop251/goja/ftoa/internal/fast" +) + +type FToStrMode int + +const ( + // 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 +) + +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 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 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 + } + + var decPt int + var ok bool + startPos := len(buffer) + + if d != 0 { // also matches -0 + if d < 0 { + buffer = append(buffer, '-') + d = -d + startPos++ + } + 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 + } + if !ok { + buffer, decPt = ftoa(d, dtoaModes[mode], mode >= ModeFixed, precision, buffer) + } + 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 + } + 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 { + buffer = insert(buffer, startPos+1, '.') + } + 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 */ + 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 buffer +} diff --git a/ftoa/ftostr_test.go b/ftoa/ftostr_test.go new file mode 100644 index 00000000..db2441f8 --- /dev/null +++ b/ftoa/ftostr_test.go @@ -0,0 +1,92 @@ +package ftoa + +import ( + "math" + "strconv" + "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(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, 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]) + } +} + +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) + } +} 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 +} diff --git a/func.go b/func.go index bfada1c0..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 @@ -20,11 +24,12 @@ type nativeFuncObject struct { baseFuncObject f func(FunctionCall) Value - construct func(args []Value) *Object + construct func(args []Value, newTarget *Object) *Object } type boundFuncObject struct { nativeFuncObject + wrapped *Object } func (f *nativeFuncObject) export() interface{} { @@ -35,87 +40,84 @@ 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) getPropStr(name string) Value { +func (f *funcObject) getStr(p unistring.String, receiver Value) Value { + return f.getStrWithOwnProp(f.getOwnPropStr(p), p, receiver) +} + +func (f *funcObject) getOwnPropStr(name unistring.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 unistring.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) { - f.putStr(n.String(), val, throw) +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) } -func (f *funcObject) delete(n Value, throw bool) bool { - 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) getProp(n Value) Value { - return f.getPropStr(n.String()) -} - -func (f *funcObject) hasOwnProperty(n Value) bool { - if r := f.baseObject.hasOwnProperty(n); r { +func (f *funcObject) hasOwnPropertyStr(name unistring.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 @@ -124,6 +126,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 @@ -150,6 +156,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 @@ -169,12 +176,18 @@ func (f *funcObject) assertCallable() (func(FunctionCall) Value, bool) { return f.Call, true } -func (f *baseFuncObject) init(name string, length int) { +func (f *funcObject) assertConstructor() func(args []Value, newTarget *Object) *Object { + return f.construct +} + +func (f *baseFuncObject) init(name unistring.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 = stringValueFromRaw(name) + f._put("name", &f.nameProp) + } f.lenProp.configurable = true f.lenProp.value = valueInt(length) @@ -183,7 +196,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() @@ -203,7 +216,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 @@ -229,36 +242,40 @@ 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 *nativeFuncObject) assertConstructor() func(args []Value, newTarget *Object) *Object { + return f.construct +} + +func (f *boundFuncObject) getStr(p unistring.String, receiver Value) Value { + return f.getStrWithOwnProp(f.getOwnPropStr(p), p, receiver) } -func (f *boundFuncObject) getPropStr(name string) Value { +func (f *boundFuncObject) getOwnPropStr(name unistring.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 { - return f.deleteStr(n.String(), throw) + 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) putStr(name string, val Value, throw bool) { +func (f *boundFuncObject) setOwnStr(name unistring.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) setForeignStr(name unistring.String, val, receiver Value, throw bool) (bool, bool) { + return f._setForeignStr(name, f.getOwnPropStr(name), val, receiver, throw) } -func (f *boundFuncObject) put(n Value, val Value, throw bool) { - f.putStr(n.String(), val, throw) +func (f *boundFuncObject) hasInstance(v Value) bool { + return instanceOfOperator(v, f.wrapped) } diff --git a/map.go b/map.go new file mode 100644 index 00000000..b092b0d0 --- /dev/null +++ b/map.go @@ -0,0 +1,169 @@ +package goja + +import ( + "hash/maphash" +) + +type mapEntry struct { + key, value Value + + iterPrev, iterNext *mapEntry + hNext *mapEntry +} + +type orderedMap struct { + hash *maphash.Hash + hashTable 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(m.hash) + for entry = m.hashTable[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.hashTable[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 hashTable + if hPrev == nil { + if entry.hNext == nil { + delete(m.hashTable, h) + } else { + m.hashTable[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(h *maphash.Hash) *orderedMap { + return &orderedMap{ + hash: h, + hashTable: 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.hashTable = make(map[uint64]*mapEntry) + m.size = 0 +} diff --git a/map_test.go b/map_test.go new file mode 100644 index 00000000..98afab7f --- /dev/null +++ b/map_test.go @@ -0,0 +1,198 @@ +package goja + +import ( + "hash/maphash" + "math" + "strconv" + "testing" +) + +func testMapHashVal(v1, v2 Value, expected bool, t *testing.T) { + var h maphash.Hash + actual := v1.hash(&h) == v2.hash(&h) + 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(&maphash.Hash{}) + 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(&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.hashTable) { + 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(&maphash.Hash{}) + 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(&maphash.Hash{}) + 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(&maphash.Hash{}) + 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(&maphash.Hash{}) + 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 85b3f600..c25637de 100644 --- a/object.go +++ b/object.go @@ -1,10 +1,23 @@ package goja -import "reflect" +import ( + "fmt" + "math" + "reflect" + "runtime" + "sort" + + "github.com/dop251/goja/unistring" +) const ( classObject = "Object" classArray = "Array" + classWeakSet = "WeakSet" + classWeakMap = "WeakMap" + classMap = "Map" + classMath = "Math" + classSet = "Set" classFunction = "Function" classNumber = "Number" classString = "String" @@ -12,16 +25,91 @@ const ( classError = "Error" classRegExp = "RegExp" classDate = "Date" + classJSON = "JSON" + + classArrayIterator = "Array Iterator" + classMapIterator = "Map Iterator" + classSetIterator = "Set Iterator" + classStringIterator = "String Iterator" +) + +var ( + hintDefault Value = asciiString("default") + hintNumber Value = asciiString("number") + hintString Value = asciiString("string") ) +type weakCollection interface { + removeId(uint64) +} + +type weakCollections struct { + objId uint64 + 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() uint64 { + return r.objId +} + +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.removeId(id) + } + r.colls = nil +} + type Object struct { + id uint64 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) -type propertyDescr struct { +type PropertyDescriptor struct { + jsDescriptor *Object + Value Value Writable, Configurable, Enumerable Flag @@ -29,37 +117,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(string) Value - put(Value, Value, bool) - putStr(string, Value, bool) - hasProperty(Value) bool - hasPropertyStr(string) bool - hasOwnProperty(Value) bool - hasOwnPropertyStr(string) bool - _putProp(name string, value Value, writable, enumerable, configurable bool) Value - defineOwnProperty(name Value, descr propertyDescr, throw bool) bool + getStr(p unistring.String, receiver Value) Value + getIdx(p valueInt, receiver Value) Value + getSym(p *valueSymbol, receiver Value) Value + + getOwnPropStr(unistring.String) Value + getOwnPropIdx(valueInt) Value + getOwnPropSym(*valueSymbol) Value + + 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 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(unistring.String) bool + hasPropertyIdx(idx valueInt) bool + hasPropertySym(s *valueSymbol) bool + + hasOwnPropertyStr(unistring.String) bool + hasOwnPropertyIdx(valueInt) bool + hasOwnPropertySym(s *valueSymbol) 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 unistring.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, throw bool) bool hasInstance(v Value) bool isExtensible() bool - preventExtensions() - enumerate(all, recusrive bool) iterNextFunc - _enumerate(recursive bool) iterNextFunc + preventExtensions(throw bool) bool + enumerate() iterNextFunc + enumerateUnfiltered() iterNextFunc export() interface{} exportType() reflect.Type equal(objectImpl) bool + ownKeys(all bool, accum []Value) []Value + ownSymbols(all bool, accum []Value) []Value + ownPropertyKeys(all bool, accum []Value) []Value + + _putProp(name unistring.String, value Value, writable, enumerable, configurable bool) Value + _putSym(s *valueSymbol, prop Value) } type baseObject struct { @@ -68,8 +244,17 @@ type baseObject struct { prototype *Object extensible bool - values map[string]Value - propNames []string + values map[unistring.String]Value + propNames []unistring.String + + lastSortedPropLen, idxPropCount int + + symValues *orderedMap +} + +type guardedObject struct { + baseObject + guardedProps map[unistring.String]struct{} } type primitiveValueObject struct { @@ -110,183 +295,360 @@ 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) getPropStr(name string) Value { - if val := o.getOwnProp(name); val != nil { - return val +func (o *baseObject) hasPropertyStr(name unistring.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) getProp(n Value) Value { - return o.val.self.getPropStr(n.String()) +func (o *baseObject) hasPropertyIdx(idx valueInt) bool { + return o.val.self.hasPropertyStr(idx.string()) } -func (o *baseObject) hasProperty(n Value) bool { - return o.val.self.getProp(n) != nil +func (o *baseObject) hasPropertySym(s *valueSymbol) bool { + if o.hasOwnPropertySym(s) { + return true + } + if o.prototype != nil { + return o.prototype.self.hasPropertySym(s) + } + return false } -func (o *baseObject) hasPropertyStr(name string) bool { - return o.val.self.getPropStr(name) != nil +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) + } + if prop, ok := prop.(*valueProperty); ok { + if receiver == nil { + return prop.get(o.val) + } + return prop.get(receiver) + } + return prop } -func (o *baseObject) _getStr(name string) Value { - p := o.getOwnProp(name) - - if p == nil && o.prototype != nil { - p = o.prototype.self.getPropStr(name) +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) + } + 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 prop +} - return p +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.val.self.getPropStr(name) - if p, ok := p.(*valueProperty); ok { - return p.get(o.val) +func (o *baseObject) getSym(s *valueSymbol, receiver Value) Value { + return o.getWithOwnProp(o.getOwnPropSym(s), s, receiver) +} + +func (o *baseObject) getStr(name unistring.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 prop, ok := prop.(*valueProperty); ok { + if receiver == nil { + return prop.get(o.val) + } + return prop.get(receiver) } + return prop +} + +func (o *baseObject) getOwnPropIdx(idx valueInt) Value { + return o.val.self.getOwnPropStr(idx.string()) +} - return p +func (o *baseObject) getOwnPropSym(s *valueSymbol) Value { + if o.symValues != nil { + return o.symValues.get(s) + } + return nil } -func (o *baseObject) get(n Value) Value { - return o.getStr(n.String()) +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()) + o.val.runtime.typeErrorResult(throw, "Cannot delete property '%s' of %s", name, o.val.toString()) return false } 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 { 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 { +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 o.symValues != nil { + if val := o.symValues.get(s); val != nil { + if !o.checkDelete(s.desc.string(), val, throw) { + return false + } + o.symValues.remove(s) + } + } + return true +} + +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 } o._delete(name) + } + return true +} + +func (o *baseObject) setProto(proto *Object, throw bool) bool { + current := o.prototype + if current.SameAs(proto) { return true } + if !o.extensible { + o.val.runtime.typeErrorResult(throw, "%s is not extensible", o.val) + return false + } + for p := proto; p != nil; { + if p.SameAs(o.val) { + o.val.runtime.typeErrorResult(throw, "Cyclic __proto__ value") + return false + } + p = p.self.proto() + } + o.prototype = proto return true } -func (o *baseObject) delete(n Value, throw bool) bool { - return o.deleteStr(n.String(), throw) +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 { + // 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) + } + } else { + o.values[name] = val + } + return true } -func (o *baseObject) put(n Value, val Value, throw bool) { - o.putStr(n.String(), val, throw) +func (o *baseObject) setOwnIdx(idx valueInt, val Value, throw bool) bool { + return o.val.self.setOwnStr(idx.string(), val, throw) } -func (o *baseObject) getOwnProp(name string) Value { - v := o.values[name] - if v == nil && name == __proto__ { - return o.prototype +func (o *baseObject) setOwnSym(name *valueSymbol, val Value, throw bool) bool { + var ownDesc Value + if o.symValues != nil { + ownDesc = o.symValues.get(name) } - return v + 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 + } + } + // 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(nil) + } + o.symValues.set(name, val) + } + 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) + } + } else { + o.symValues.set(name, val) + } + 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 { +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() { o.val.runtime.typeErrorResult(throw, "Cannot assign to read only property '%s'", name) - return + return false, true + } + if prop.setterFunc != nil { + prop.set(receiver, val) + return true, true } - prop.set(o.val, val) - return } - o.values[name] = val - return + } else { + 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 + } } + return false, false +} - if name == __proto__ { - if !o.extensible { - o.val.runtime.typeErrorResult(throw, "%s is not extensible", o.val) - return +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 '%d'", idx) + return false, true + } + if prop.setterFunc != nil { + prop.set(receiver, val) + return true, true + } } - if val == _undefined || val == _null { - o.prototype = nil - return - } else { - if val, ok := val.(*Object); ok { - o.prototype = val + } 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 } + return false, false +} - var pprop Value - if proto := o.prototype; proto != nil { - pprop = proto.self.getPropStr(name) - } +func (o *baseObject) setForeignStr(name unistring.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 } } + return false, false +} - o.values[name] = val - o.propNames = append(o.propNames, name) +func (o *baseObject) hasOwnPropertySym(s *valueSymbol) bool { + if o.symValues != nil { + return o.symValues.has(s) + } + return false } -func (o *baseObject) hasOwnProperty(n Value) bool { - v := o.values[n.String()] - return v != nil +func (o *baseObject) hasOwnPropertyStr(name unistring.String) bool { + _, exists := o.values[name] + return exists } -func (o *baseObject) hasOwnPropertyStr(name string) bool { - v := o.values[name] - return v != nil +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 unistring.String, existingValue Value, descr PropertyDescriptor, throw bool) (val Value, ok bool) { getterObj, _ := descr.Getter.(*Object) setterObj, _ := descr.Setter.(*Object) @@ -295,7 +657,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{} @@ -384,15 +746,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 { - name := n.String() +func (o *baseObject) defineOwnPropertyStr(name unistring.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) @@ -402,7 +763,26 @@ func (o *baseObject) defineOwnProperty(n Value, descr propertyDescr, throw bool) return false } -func (o *baseObject) _put(name string, v Value) { +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.desc.string(), existingVal, descr, throw); ok { + if o.symValues == nil { + o.symValues = newOrderedMap(nil) + } + o.symValues.set(s, v) + return true + } + return false +} + +func (o *baseObject) _put(name unistring.String, v Value) { if _, exists := o.values[name]; !exists { o.propNames = append(o.propNames, name) } @@ -410,24 +790,33 @@ 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 unistring.String, value Value, writable, enumerable, configurable bool) Value { + prop := valueProp(value, writable, enumerable, configurable) + o._put(name, prop) + return prop +} + +func (o *baseObject) _putSym(s *valueSymbol, prop Value) { + if o.symValues == nil { + o.symValues = newOrderedMap(nil) + } + o.symValues.set(s, prop) } -func (o *baseObject) tryPrimitive(methodName string) Value { - if method, ok := o.getStr(methodName).(*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, @@ -467,13 +856,64 @@ func (o *baseObject) toPrimitiveString() Value { } func (o *baseObject) toPrimitive() Value { - return o.toPrimitiveNumber() + 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 *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) { return nil, false } +func (o *baseObject) assertConstructor() func(args []Value, newTarget *Object) *Object { + return nil +} + func (o *baseObject) proto() *Object { return o.prototype } @@ -482,43 +922,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(itemName.string(), nil) if v != nil { - m[item.name] = v.Export() + m[itemNameStr] = v.Export() } else { - m[item.name] = nil + m[itemNameStr] = nil } } + return m } @@ -535,22 +974,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 - recursive bool + 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) { @@ -590,36 +1028,424 @@ 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 { - propNames := make([]string, len(o.propNames)) +func (o *baseObject) enumerate() iterNextFunc { + return (&propFilterIter{ + wrapped: o.val.self.enumerateUnfiltered(), + seen: make(map[unistring.String]bool), + }).next +} + +func (o *baseObject) ownIter() iterNextFunc { + if len(o.propNames) > o.lastSortedPropLen { + o.fixPropOrder() + } + propNames := make([]unistring.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) equal(other objectImpl) bool { +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) hasInstance(v Value) bool { - o.val.runtime.typeErrorResult(true, "Expecting a function in instanceof check, but got %s", o.val.ToString()) - panic("Unreachable") +// 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, stringValueFromRaw(k)) + } + } else { + for _, k := range o.propNames { + prop := o.values[k] + if prop, ok := prop.(*valueProperty); ok && !prop.enumerable { + continue + } + keys = append(keys, stringValueFromRaw(k)) + } + } + return keys +} + +func (o *baseObject) ownSymbols(all bool, accum []Value) []Value { + if o.symValues != nil { + iter := o.symValues.newIter() + 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) + } + } + } + + return accum +} + +func (o *baseObject) ownPropertyKeys(all bool, accum []Value) []Value { + return o.ownSymbols(all, o.val.self.ownKeys(all, accum)) +} + +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.getSym(symHasInstance, c)); instOfHandler != nil { + return instOfHandler(FunctionCall{ + This: c, + Arguments: []Value{o}, + }).ToBoolean() + } + + return c.self.hasInstance(o) +} + +func (o *Object) get(p Value, receiver Value) Value { + switch p := p.(type) { + 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 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 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 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 unistring.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 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 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 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{ + 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 +} + +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/object_args.go b/object_args.go index 16113dec..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,15 +12,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 unistring.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 unistring.String) Value { + if mapped, ok := a.values[name].(*mappedProperty); ok { + return *mapped.v + } + + return a.baseObject.getOwnPropStr(name) } func (a *argumentsObject) init() { @@ -26,11 +29,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) { - a.putStr(n.String(), val, throw) +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) + return false + } + *prop.v = val + return true + } + return a.baseObject.setOwnStr(name, val, throw) } -func (a *argumentsObject) putStr(name string, val Value, throw 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) +} + +/*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) @@ -40,9 +55,9 @@ 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 { +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 @@ -54,16 +69,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 { - return a.deleteStr(n.String(), throw) -} - -type argumentsPropIter1 struct { - a *argumentsObject - idx int - recursive bool -} - type argumentsPropIter struct { wrapped iterNextFunc } @@ -80,21 +85,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) enumerateUnfiltered() iterNextFunc { + return a.recursiveIter((&argumentsPropIter{ + wrapped: a.ownIter(), + }).next) } -func (a *argumentsObject) enumerate(all, recursive bool) iterNextFunc { - return (&argumentsPropIter{ - wrapped: a.baseObject.enumerate(all, recursive), - }).next -} - -func (a *argumentsObject) defineOwnProperty(n Value, descr propertyDescr, throw bool) bool { - name := n.String() +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, @@ -103,7 +100,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 } @@ -127,21 +124,13 @@ func (a *argumentsObject) defineOwnProperty(n Value, descr propertyDescr, throw return true } - return a.baseObject.defineOwnProperty(n, descr, throw) -} - -func (a *argumentsObject) getOwnProp(name string) Value { - if mapped, ok := a.values[name].(*mappedProperty); ok { - return *mapped.v - } - - return a.baseObject.getOwnProp(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))) + for i := range arr { + 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 678c5e30..601b3638 100644 --- a/object_gomap.go +++ b/object_gomap.go @@ -2,7 +2,8 @@ package goja import ( "reflect" - "strconv" + + "github.com/dop251/goja/unistring" ) type objectGoMapSimple struct { @@ -17,10 +18,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,37 +26,51 @@ 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) 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) getPropStr(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 o.baseObject.getPropStr(name) + return nil } -func (o *objectGoMapSimple) getStr(name string) Value { - if v := o._getStr(name); v != nil { - return v +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 { + // 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.data[n] = val.Export() } - return o.baseObject._getStr(name) + return true } -func (o *objectGoMapSimple) getOwnProp(name string) Value { - if v := o._getStr(name); v != nil { - return v +func trueValIfPresent(present bool) Value { + if present { + return valueTrue } - return o.baseObject.getOwnProp(name) + return nil } -func (o *objectGoMapSimple) put(n Value, val Value, throw bool) { - o.putStr(n.String(), val, 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 { @@ -67,52 +78,23 @@ func (o *objectGoMapSimple) _hasStr(name string) bool { 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) { - o.data[name] = val.Export() - } else { - o.val.runtime.typeErrorResult(throw, "Host object is not extensible") - } +func (o *objectGoMapSimple) hasOwnPropertyStr(name unistring.String) bool { + return o._hasStr(name.String()) } -func (o *objectGoMapSimple) hasProperty(n Value) bool { - if o._has(n) { - return true +func (o *objectGoMapSimple) defineOwnPropertyStr(name unistring.String, descr PropertyDescriptor, throw bool) bool { + if !o.val.runtime.checkHostObjectPropertyDescr(name, descr, throw) { + return false } - return o.baseObject.hasProperty(n) -} -func (o *objectGoMapSimple) hasPropertyStr(name string) bool { - if o._hasStr(name) { + n := name.String() + if o.extensible || o._hasStr(n) { + o.data[n] = descr.Value.Export() return true } - return o.baseObject.hasOwnPropertyStr(name) -} - -func (o *objectGoMapSimple) hasOwnProperty(n Value) bool { - return o._has(n) -} -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 { - if descr.Getter != nil || descr.Setter != nil { - o.val.runtime.typeErrorResult(throw, "Host objects do not support accessor properties") - return false - } - o.put(name, descr.Value, throw) - return true + o.val.runtime.typeErrorResult(throw, "Cannot define property %s, object is not extensible", n) + return false } /* @@ -133,19 +115,14 @@ func (o *objectGoMapSimple) assertCallable() (call func(FunctionCall) Value, ok } */ -func (o *objectGoMapSimple) deleteStr(name string, throw bool) bool { - delete(o.data, name) +func (o *objectGoMapSimple) deleteStr(name unistring.String, _ bool) bool { + delete(o.data, name.String()) return true } -func (o *objectGoMapSimple) delete(name Value, throw bool) bool { - return o.deleteStr(name.String(), throw) -} - type gomapPropIter struct { o *objectGoMapSimple propNames []string - recursive bool idx int } @@ -154,37 +131,33 @@ 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 } } - 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 { + 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{} { @@ -201,21 +174,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 a040d363..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 @@ -14,24 +18,28 @@ 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 { 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,44 +47,51 @@ 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()) } return nil } -func (o *objectGoMapReflect) get(n Value) Value { - if v := o._get(n); 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.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) + return o.objectGoReflect.getIdx(idx, receiver) } -func (o *objectGoMapReflect) getProp(n Value) Value { - return o.get(n) -} - -func (o *objectGoMapReflect) getPropStr(name string) Value { - return o.getStr(name) +func (o *objectGoMapReflect) getOwnPropStr(name unistring.String) Value { + if v := o._getStr(name.String()); v != nil { + return &valueProperty{ + value: v, + writable: true, + enumerable: true, + } + } + return o.objectGoReflect.getOwnPropStr(name) } -func (o *objectGoMapReflect) getOwnProp(name string) Value { - if v := o._getStr(name); v != nil { +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.getOwnProp(name) + return o.objectGoReflect.getOwnPropStr(idx.string()) } func (o *objectGoMapReflect) toValue(val Value, throw bool) (reflect.Value, bool) { @@ -89,76 +104,134 @@ 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) - 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) - v, ok := o.toValue(val, throw) - if !ok { - return +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 + 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", n) + return false + } else { + if throw && !key.IsValid() { + o.strToKey(n, 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 { - name := n.String() +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) +} + +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) defineOwnPropertyStr(name unistring.String, descr PropertyDescriptor, throw bool) bool { if !o.val.runtime.checkHostObjectPropertyDescr(name, descr, throw) { return false } - o.put(n, descr.Value, throw) - return true + return o._put(o.strToKey(name.String(), throw), descr.Value, throw) } -func (o *objectGoMapReflect) hasOwnPropertyStr(name string) bool { - return o.value.MapIndex(o.strToKey(name)).IsValid() -} +func (o *objectGoMapReflect) defineOwnPropertyIdx(idx valueInt, descr PropertyDescriptor, throw bool) bool { + if !o.val.runtime.checkHostObjectPropertyDescr(idx.string(), descr, throw) { + return false + } -func (o *objectGoMapReflect) hasOwnProperty(n Value) bool { - return o.value.MapIndex(o.toKey(n)).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 unistring.String) bool { + key := o.strToKey(name.String(), 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 { - o.value.SetMapIndex(o.toKey(n), reflect.Value{}) +func (o *objectGoMapReflect) deleteStr(name unistring.String, throw bool) bool { + key := o.strToKey(name.String(), 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{}) +func (o *objectGoMapReflect) deleteIdx(idx valueInt, throw bool) bool { + key := o.toKey(idx, throw) + if !key.IsValid() { + return false + } + o.value.SetMapIndex(key, reflect.Value{}) return true } 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) { @@ -167,32 +240,30 @@ 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 } } - 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..93266bcc 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,115 @@ 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) + } +} + +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 730bab1c..451ac01b 100644 --- a/object_gomap_test.go +++ b/object_gomap_test.go @@ -182,3 +182,156 @@ 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) + } +} + +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 7a0c58ee..aa9d0bd5 100644 --- a/object_goreflect.go +++ b/object_goreflect.go @@ -2,10 +2,12 @@ package goja import ( "fmt" - "github.com/dop251/goja/parser" "go/ast" "reflect" "strings" + + "github.com/dop251/goja/parser" + "github.com/dop251/goja/unistring" ) // JsonEncodable allows custom JSON encoding by JSON.stringify() @@ -102,6 +104,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) @@ -114,16 +117,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 unistring.String, receiver Value) Value { + if v := o._get(name.String()); v != nil { + return v + } + return o.baseObject.getStr(name, receiver) } func (o *objectGoReflect) _getField(jsName string) reflect.Value { @@ -143,10 +149,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()) } } @@ -157,44 +170,19 @@ 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) 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 { - return v - } - return o.baseObject.getPropStr(name) -} - -func (o *objectGoReflect) getOwnProp(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() { - canSet := v.CanSet() - if (v.Kind() == reflect.Struct || v.Kind() == reflect.Slice) && v.CanAddr() { - v = v.Addr() - } + if v := o._getField(n); v.IsValid() { 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, } } } - 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, @@ -204,43 +192,50 @@ func (o *objectGoReflect) getOwnProp(name string) Value { return nil } -func (o *objectGoReflect) put(n Value, val Value, throw bool) { - o.putStr(n.String(), 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) + return false + } else { + return res + } + } + 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 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) 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) { +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 propertyDescr, 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 @@ -256,28 +251,17 @@ func (r *Runtime) checkHostObjectPropertyDescr(name string, descr propertyDescr, 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 - } - v.Set(vv) - return true +func (o *objectGoReflect) defineOwnPropertyStr(name unistring.String, descr PropertyDescriptor, throw bool) bool { + if o.val.runtime.checkHostObjectPropertyDescr(name, descr, throw) { + 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 } } - - return o.baseObject.defineOwnProperty(n, descr, throw) + return false } func (o *objectGoReflect) _has(name string) bool { @@ -292,27 +276,8 @@ 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) +func (o *objectGoReflect) hasOwnPropertyStr(name unistring.String) bool { + return o._has(name.String()) } func (o *objectGoReflect) _toNumber() Value { @@ -363,7 +328,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() } @@ -375,22 +340,18 @@ 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) } -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) { @@ -398,7 +359,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 @@ -410,33 +371,37 @@ 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 - } - - if i.recursive { - return i.o.baseObject._enumerate(true)() + return propIterItem{name: unistring.NewFromString(name), enumerable: _ENUM_TRUE}, i.nextMethod } 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 dc71b0cf..d35b9086 100644 --- a/object_goreflect_test.go +++ b/object_goreflect_test.go @@ -25,7 +25,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) } @@ -489,14 +489,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 } @@ -590,11 +590,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 } @@ -674,7 +674,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 "" @@ -685,7 +685,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 } @@ -794,7 +794,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 { @@ -813,11 +813,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) } @@ -1025,3 +1025,147 @@ func ExampleUncapFieldNameMapper() { fmt.Println(res.Export()) // Output: passed and passed too } + +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 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 + } + 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) + } +} + +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 cb7df72e..21dc8998 100644 --- a/object_goslice.go +++ b/object_goslice.go @@ -3,6 +3,8 @@ package goja import ( "reflect" "strconv" + + "github.com/dop251/goja/unistring" ) type objectGoSlice struct { @@ -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]) - } - return nil -} - -func (o *objectGoSlice) _get(n Value) Value { - if idx := toIdx(n); idx >= 0 { - return o.getIdx(idx) +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] + ownProp = o.val.runtime.ToValue(v) + } else if name == "length" { + ownProp = &o.lengthProp } - return nil -} -func (o *objectGoSlice) _getStr(name string) Value { - if idx := strToIdx(name); idx >= 0 { - return o.getIdx(idx) - } - return nil + return o.getStrWithOwnProp(ownProp, name, receiver) } -func (o *objectGoSlice) get(n Value) Value { - if v := o._get(n); v != nil { - return v +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 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 unistring.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) getOwnProp(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.getOwnProp(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,108 @@ 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 (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.put(n, val, throw) + return true } -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) 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 + } } - // 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) 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 { + 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) _hasStr(name string) bool { - if idx := strToIdx(name); idx >= 0 { - return idx < int64(len(*o.data)) - } - return false +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) hasProperty(n Value) bool { - if o._has(n) { - return true - } - return o.baseObject.hasProperty(n) +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) } -func (o *objectGoSlice) hasPropertyStr(name string) bool { - if o._hasStr(name) { - return true +func (o *objectGoSlice) hasOwnPropertyIdx(idx valueInt) bool { + if idx := int64(idx); idx >= 0 { + return idx < int64(len(*o.data)) } - return o.baseObject.hasPropertyStr(name) + return false } -func (o *objectGoSlice) hasOwnProperty(n Value) bool { - if o._has(n) { - return true +func (o *objectGoSlice) hasOwnPropertyStr(name unistring.String) bool { + if idx := strToIdx64(name); 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) { +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 } - return o.baseObject.hasOwnPropertyStr(name) -} - -func (o *objectGoSlice) _putProp(name string, value Value, writable, enumerable, configurable bool) Value { - o.putStr(name, value, false) - return value + 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.String(), descr, throw) { +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 } val := descr.Value @@ -199,7 +245,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 { @@ -216,25 +266,30 @@ func (o *objectGoSlice) toPrimitive() Value { return o.toPrimitiveString() } -func (o *objectGoSlice) deleteStr(name string, throw bool) bool { - if idx := strToIdx(name); idx >= 0 && idx < int64(len(*o.data)) { - (*o.data)[idx] = nil +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") + 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 } @@ -242,31 +297,25 @@ 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 - } - - if i.recursive { - return i.o.prototype.self._enumerate(i.recursive)() + return propIterItem{name: unistring.String(name), enumerable: _ENUM_TRUE}, i.next } 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 +338,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 aa085b41..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 { @@ -17,98 +19,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()) +func (o *objectGoSliceReflect) _hasStr(name unistring.String) bool { + 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 unistring.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 unistring.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) getOwnProp(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.getOwnProp(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 +134,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 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 { + 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) hasOwnPropertyStr(name string) 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) +} + +func (o *objectGoSliceReflect) hasOwnPropertyIdx(idx valueInt) bool { + return o._hasIdx(idx) +} + +func (o *objectGoSliceReflect) hasOwnPropertyStr(name unistring.String) bool { if o._hasStr(name) { return true } - return o.objectGoReflect.hasOwnPropertyStr(name) + return o.objectGoReflect._has(name.String()) } -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.String(), descr, throw) { - return false +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 + } + 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 { @@ -213,25 +280,31 @@ func (o *objectGoSliceReflect) toPrimitive() Value { return o.toPrimitiveString() } -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())) +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") + 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 } @@ -239,29 +312,24 @@ 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 } - 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 +345,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 6fb405d0..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 @@ -13,82 +17,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) getOwnProp(name string) Value { +func (o *lazyObject) hasPropertyIdx(idx valueInt) bool { obj := o.create(o.val) o.val.self = obj - return obj.getOwnProp(name) + return obj.hasPropertyIdx(idx) } -func (o *lazyObject) put(n Value, val Value, throw bool) { +func (o *lazyObject) hasPropertySym(s *valueSymbol) bool { obj := o.create(o.val) o.val.self = obj - obj.put(n, val, throw) + return obj.hasPropertySym(s) } -func (o *lazyObject) putStr(name string, val Value, throw bool) { +func (o *lazyObject) hasOwnPropertyIdx(idx valueInt) bool { obj := o.create(o.val) o.val.self = obj - obj.putStr(name, val, throw) + return obj.hasOwnPropertyIdx(idx) } -func (o *lazyObject) hasProperty(n Value) bool { +func (o *lazyObject) hasOwnPropertySym(s *valueSymbol) bool { obj := o.create(o.val) o.val.self = obj - return obj.hasProperty(n) + return obj.hasOwnPropertySym(s) } -func (o *lazyObject) hasPropertyStr(name string) bool { +func (o *lazyObject) defineOwnPropertyStr(name unistring.String, desc PropertyDescriptor, throw bool) bool { obj := o.create(o.val) o.val.self = obj - return obj.hasPropertyStr(name) + return obj.defineOwnPropertyStr(name, desc, throw) } -func (o *lazyObject) hasOwnProperty(n Value) bool { +func (o *lazyObject) defineOwnPropertyIdx(name valueInt, desc PropertyDescriptor, throw bool) bool { obj := o.create(o.val) o.val.self = obj - return obj.hasOwnProperty(n) + return obj.defineOwnPropertyIdx(name, desc, throw) } -func (o *lazyObject) hasOwnPropertyStr(name string) bool { +func (o *lazyObject) defineOwnPropertySym(name *valueSymbol, desc PropertyDescriptor, throw bool) bool { obj := o.create(o.val) o.val.self = obj - return obj.hasOwnPropertyStr(name) + return obj.defineOwnPropertySym(name, desc, throw) +} + +func (o *lazyObject) deleteIdx(idx valueInt, throw bool) bool { + obj := o.create(o.val) + o.val.self = obj + 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 unistring.String, receiver Value) Value { + obj := o.create(o.val) + o.val.self = obj + return obj.getStr(name, receiver) +} + +func (o *lazyObject) getOwnPropStr(name unistring.String) Value { + obj := o.create(o.val) + o.val.self = obj + return obj.getOwnPropStr(name) } -func (o *lazyObject) _putProp(name string, value Value, writable, enumerable, configurable bool) Value { +func (o *lazyObject) setOwnStr(p unistring.String, v Value, throw bool) bool { obj := o.create(o.val) o.val.self = obj - return obj._putProp(name, value, writable, enumerable, configurable) + return obj.setOwnStr(p, v, throw) } -func (o *lazyObject) defineOwnProperty(name Value, descr propertyDescr, throw bool) bool { +func (o *lazyObject) setOwnIdx(p valueInt, v Value, throw bool) bool { obj := o.create(o.val) o.val.self = obj - return obj.defineOwnProperty(name, descr, throw) + 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) 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) +} + +func (o *lazyObject) setForeignIdx(p valueInt, v, receiver Value, throw bool) (bool, bool) { + obj := o.create(o.val) + o.val.self = obj + 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 unistring.String) bool { + obj := o.create(o.val) + o.val.self = obj + return obj.hasPropertyStr(name) +} + +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(unistring.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 { @@ -115,16 +187,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 unistring.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 { @@ -145,22 +217,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{} { @@ -181,6 +253,30 @@ func (o *lazyObject) equal(other objectImpl) bool { return obj.equal(other) } +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(all bool, accum []Value) []Value { + obj := o.create(o.val) + o.val.self = obj + return obj.ownSymbols(all, accum) +} + +func (o *lazyObject) ownPropertyKeys(all bool, accum []Value) []Value { + obj := o.create(o.val) + o.val.self = obj + return obj.ownPropertyKeys(all, accum) +} + +func (o *lazyObject) setProto(proto *Object, throw bool) bool { + obj := o.create(o.val) + o.val.self = obj + return obj.setProto(proto, throw) +} + 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..80c80480 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,64 @@ 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 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 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{} @@ -82,7 +140,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 +159,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 +177,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 +194,7 @@ func BenchmarkGetStr(b *testing.B) { o.init() for i := 0; i < b.N; i++ { - o.getStr("test") + o.getStr("test", nil) } } @@ -153,7 +211,7 @@ func BenchmarkToString1(b *testing.B) { v := asciiString("test") for i := 0; i < b.N; i++ { - v.ToString() + v.toString() } } @@ -190,11 +248,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 +274,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 +307,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 +325,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 +342,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..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} @@ -373,6 +366,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: unistring.String(token.NEW.String()), + Idx: idx, + }, + Property: prop, + } + } + self.errorUnexpectedToken(token.IDENTIFIER) + } callee := self.parseLeftHandSideExpression() node := &ast.NewExpression{ New: idx, diff --git a/parser/lexer.go b/parser/lexer.go index 0786efac..da6e739c 100644 --- a/parser/lexer.go +++ b/parser/lexer.go @@ -1,25 +1,20 @@ 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 { @@ -59,45 +54,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 @@ -122,7 +137,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 @@ -135,30 +195,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 @@ -173,10 +246,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 } @@ -290,7 +370,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 } @@ -364,14 +444,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]) @@ -479,19 +551,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 @@ -500,24 +583,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 { @@ -525,14 +618,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... @@ -540,21 +638,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() { @@ -621,21 +729,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 @@ -643,11 +746,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 } @@ -740,20 +852,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 84af3b9c..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); @@ -160,7 +161,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 +252,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 +385,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) @@ -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/parser/statement.go b/parser/statement.go index 7ac0ac59..45581cec 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,46 @@ 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.IDENTIFIER { + if self.literal == "of" { + self.next() + forOf = true + } + } } + left = list } else { left = append(left, self.parseExpression()) if self.token == token.IN { self.next() forIn = true + } else if self.token == token.IDENTIFIER { + if self.literal == "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/proxy.go b/proxy.go new file mode 100644 index 00000000..534b50b2 --- /dev/null +++ b/proxy.go @@ -0,0 +1,806 @@ +package goja + +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. +// Use Runtime.NewProxy() to create one. +type Proxy struct { + proxy *proxyObject +} + +var ( + proxyType = reflect.TypeOf(Proxy{}) +) + +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, 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")) + } + } + 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() +} + +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 ( + 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, unistring.String(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 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) +} + +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 unistring.String) bool { + if b, ok := p.proxyHas(stringValueFromRaw(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 unistring.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 unistring.String) Value { + if v, ok := p.proxyGetOwnPropertyDescriptor(stringValueFromRaw(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 unistring.String, receiver Value) Value { + if v, ok := p.proxyGet(stringValueFromRaw(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 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 { + 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 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) +} + +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 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 +} + +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 unistring.String, throw bool) bool { + if ret, ok := p.proxyDelete(stringValueFromRaw(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 !current.configurable { + if desc.Writable == FLAG_TRUE && !current.writable { + return false + } + if !current.writable { + if desc.Value != nil && !desc.Value.SameAs(current.value) { + return false + } + } + } + return true + } + if p.__isAccessorDescriptor(desc) && current.accessor { + if !current.configurable { + 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(all bool, accum []Value) []Value { + if vals, ok := p.proxyOwnKeys(); ok { + res := p.filterKeys(vals, true, true) + if accum == nil { + return res + } + accum = append(accum, res...) + return accum + } + + return p.target.self.ownSymbols(all, accum) +} + +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) 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/regexp.go b/regexp.go index 9c20d80f..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 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:])) - 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)) - 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 { @@ -287,48 +389,53 @@ 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 } } 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 { - 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 } - index := lastIndex - if !r.global { - index = 0 - } - if index >= 0 && index <= target.length() { - result = r.pattern.FindSubmatchIndex(target, int(index)) - } - if result == nil { - r.putStr("lastIndex", intToValue(0), true) - return + 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 + } } - 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) + + if r.pattern.global || r.pattern.sticky { + r.setOwnStr("lastIndex", intToValue(int64(lastResult[1])), true) } - if r.global { - r.putStr("lastIndex", intToValue(int64(endIndex)), 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)) } + match = r.updateLastIndex(index, result, result) return } @@ -349,13 +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 + 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..96ad3801 100644 --- a/regexp_test.go +++ b/regexp_test.go @@ -190,6 +190,175 @@ 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 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/) diff --git a/runtime.go b/runtime.go index 2966c1a7..d6399b49 100644 --- a/runtime.go +++ b/runtime.go @@ -5,9 +5,12 @@ import ( "errors" "fmt" "go/ast" + "hash/maphash" "math" + "math/bits" "math/rand" "reflect" + "runtime" "strconv" "time" @@ -15,18 +18,30 @@ import ( js_ast "github.com/dop251/goja/ast" "github.com/dop251/goja/parser" + "github.com/dop251/goja/unistring" ) const ( sqrt1_2 float64 = math.Sqrt2 / 2 + + deoptimiseRegexp = false ) var ( typeCallable = reflect.TypeOf(Callable(nil)) typeValue = reflect.TypeOf((*Value)(nil)).Elem() + typeObject = reflect.TypeOf((*Object)(nil)) typeTime = reflect.TypeOf(time.Time{}) ) +type iterationKind int + +const ( + iterationKindKey iterationKind = iota + iterationKindValue + iterationKindKeyValue +) + type global struct { Object *Object Array *Object @@ -36,8 +51,26 @@ type global struct { Boolean *Object RegExp *Object Date *Object - - ArrayBuffer *Object + Symbol *Object + Proxy *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 @@ -57,8 +90,21 @@ type global struct { FunctionPrototype *Object RegExpPrototype *Object DatePrototype *Object + SymbolPrototype *Object ArrayBufferPrototype *Object + DataViewPrototype *Object + TypedArrayPrototype *Object + WeakSetPrototype *Object + WeakMapPrototype *Object + MapPrototype *Object + SetPrototype *Object + + IteratorPrototype *Object + ArrayIteratorPrototype *Object + MapIteratorPrototype *Object + SetIteratorPrototype *Object + StringIteratorPrototype *Object ErrorPrototype *Object TypeErrorPrototype *Object @@ -74,6 +120,15 @@ type global struct { thrower *Object throwerProperty Value + + stdRegexpProto *guardedObject + + weakSetAdder *Object + weakMapAdder *Object + mapAdder *Object + setAdder *Object + arrayValues *Object + arrayToString *Object } type Flag int @@ -107,26 +162,53 @@ type Runtime struct { now Now _collator *collate.Collator + symbolRegistry map[unistring.String]*valueSymbol + typeInfoCache map[reflect.Type]*reflectTypeInfo fieldNameMapper FieldNameMapper - vm *vm + vm *vm + hash *maphash.Hash + idSeq uint64 } -type stackFrame struct { +type StackFrame struct { prg *Program - funcName string + funcName unistring.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.String() +} + +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) + b.WriteString(n.String()) b.WriteString(" (") } if n := f.prg.src.name; n != "" { @@ -135,7 +217,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(')') @@ -144,7 +226,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") @@ -156,7 +238,7 @@ func (f *stackFrame) write(b *bytes.Buffer) { type Exception struct { val Value - stack []stackFrame + stack []StackFrame } type InterruptedError struct { @@ -194,7 +276,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') } } @@ -202,7 +284,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) } } @@ -234,7 +316,14 @@ 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 { + o := newBaseObjectObj(val, r.global.ObjectPrototype, classObject) + + o._putSym(symIterator, valueProp(r.newNativeFunc(r.returnThis, nil, "[Symbol.iterator]", nil, 0), true, false, true)) + return o } func (r *Runtime) init() { @@ -248,27 +337,38 @@ 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() r.initFunction() r.initArray() r.initString() + r.initGlobalObject() r.initNumber() r.initRegExp() r.initDate() r.initBoolean() + r.initProxy() + r.initReflect() r.initErrors() r.global.Eval = r.newNativeFunc(r.builtin_eval, nil, "eval", nil, 1) r.addToGlobal("eval", r.global.Eval) - r.initGlobalObject() - r.initMath() r.initJSON() - //r.initTypedArrays() + 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{ @@ -289,62 +389,46 @@ 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)) } 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 (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 newBaseObjectObj(obj, proto *Object, class string) *baseObject { + o := &baseObject{ + class: class, + val: obj, + extensible: true, + prototype: proto, + } + obj.self = o + o.init() + return o } -func (r *Runtime) newArrayObject() *arrayObject { - return r.newArray(r.global.ArrayPrototype) +func newGuardedObj(proto *Object, class string) *guardedObject { + return &guardedObject{ + baseObject: baseObject{ + class: class, + extensible: true, + prototype: proto, + }, + } } -func (r *Runtime) newArrayValues(values []Value) *Object { +func (r *Runtime) newBaseObject(proto *Object, class string) (o *baseObject) { 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 + return newBaseObjectObj(v, proto, class) } -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) { +func (r *Runtime) newGuardedObject(proto *Object, class string) (o *guardedObject) { v := &Object{runtime: r} - - o = &baseObject{} - o.class = class - o.val = v - o.extensible = true + o = newGuardedObj(proto, class) v.self = o - o.prototype = proto + o.val = v o.init() return } @@ -373,7 +457,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{} @@ -390,7 +474,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 unistring.String, proto *Object, length int) *nativeFuncObject { f := &nativeFuncObject{ baseFuncObject: baseFuncObject{ baseObject: baseObject{ @@ -401,7 +485,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) @@ -411,7 +495,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{ @@ -429,7 +513,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) } @@ -443,7 +527,40 @@ 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) 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} + } + + 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 unistring.String, proto *Object, length int) *Object { v := &Object{runtime: r} f := &nativeFuncObject{ @@ -456,7 +573,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) @@ -467,7 +584,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{ @@ -477,10 +594,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) @@ -490,11 +605,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{} @@ -503,10 +618,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) @@ -533,18 +646,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 { @@ -559,7 +672,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() { @@ -570,29 +683,35 @@ 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 { + var nameStr, msgStr valueString obj := call.This.ToObject(r).self - msg := obj.getStr("message") - name := obj.getStr("name") - var nameStr, msgStr string - if name != nil && name != _undefined { - nameStr = name.String() - } - if msg != nil && msg != _undefined { - msgStr = msg.String() + name := obj.getStr("name", nil) + 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 { @@ -604,42 +723,20 @@ func (r *Runtime) builtin_Error(args []Value, proto *Object) *Object { } func (r *Runtime) builtin_new(construct *Object, args []Value) *Object { -repeat: - switch f := construct.self.(type) { - case *nativeFuncObject: - if f.construct != nil { - return f.construct(args) - } else { - panic("Not a constructor") - } - case *boundFuncObject: - if f.construct != nil { - return f.construct(args) - } else { - panic("Not a constructor") - } - case *funcObject: - // TODO: implement - panic("Not implemented") - case *lazyObject: - construct.self = f.create(construct) - goto repeat - default: - panic("Not a constructor") - } + return r.toConstructor(construct)(args, nil) } 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 } -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) @@ -672,23 +769,41 @@ func (r *Runtime) builtin_eval(call FunctionCall) Value { if len(call.Arguments) == 0 { return _undefined } - if str, ok := call.Arguments[0].assertString(); ok { - return r.eval(str.String(), false, false, r.globalObject) + if str, ok := call.Arguments[0].(valueString); ok { + return r.eval(str, 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 { + if pp, ok := newTarget.self.getStr("prototype", nil).(*Object); ok { + p = pp + } + } + if p == nil { + 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 } - r.typeErrorResult(true, "Value is not callable: %s", v.ToString()) + r.typeErrorResult(true, "Value is not callable: %s", v.toString()) return nil } @@ -699,27 +814,98 @@ func (r *Runtime) checkObjectCoercible(v Value) { } } -func toUInt32(v Value) uint32 { +func toInt8(v Value) int8 { v = v.ToNumber() - if i, ok := v.assertInt(); ok { - return uint32(i) + if i, ok := v.(valueInt); ok { + return int8(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)) + return int8(int64(f)) + } + } + return 0 +} + +func toUint8(v Value) uint8 { + v = v.ToNumber() + if i, ok := v.(valueInt); ok { + return uint8(i) + } + + if f, ok := v.(valueFloat); ok { + f := float64(f) + if !math.IsNaN(f) && !math.IsInf(f, 0) { + return uint8(int64(f)) + } + } + 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 + } + + 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 + } + } + return 0 +} + +func toInt16(v Value) int16 { + v = v.ToNumber() + if i, ok := v.(valueInt); ok { + return int16(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 { +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)) } @@ -727,6 +913,40 @@ func toUInt16(v Value) uint16 { return 0 } +func toInt32(v Value) int32 { + v = v.ToNumber() + if i, ok := v.(valueInt); ok { + return int32(i) + } + + if f, ok := v.(valueFloat); ok { + f := float64(f) + if !math.IsNaN(f) && !math.IsInf(f, 0) { + return int32(int64(f)) + } + } + 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 @@ -741,18 +961,24 @@ func toLength(v Value) int64 { return i } -func toInt32(v Value) int32 { - v = v.ToNumber() - if i, ok := v.assertInt(); ok { - return int32(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) +} - if f, ok := v.assertFloat(); ok { - if !math.IsNaN(f) && !math.IsInf(f, 0) { - return int32(int64(f)) +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) } - return 0 + panic(r.newError(r.global.RangeError, "Invalid index %s", v.String())) } func (r *Runtime) toBoolean(b bool) Value { @@ -913,6 +1139,24 @@ func (r *Runtime) RunProgram(p *Program) (result Value, err error) { return } +// 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. // 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. @@ -932,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. @@ -960,8 +1304,17 @@ 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 valueContainer: + return i.toValue(r) case Value: - // TODO: prevent importing Objects from a different runtime return i case string: return newStringValue(i) @@ -972,9 +1325,11 @@ func (r *Runtime) ToValue(i interface{}) Value { return valueFalse } case func(FunctionCall) Value: - return r.newNativeFunc(i, nil, "", nil, 0) + name := unistring.NewFromString(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 := unistring.NewFromString(runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()) + return r.newNativeConstructor(i, name, 0) case int: return intToValue(int64(i)) case int8: @@ -1101,7 +1456,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 := unistring.NewFromString(runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()) + return r.newNativeFunc(r.wrapReflectFunc(value), nil, name, nil, value.Type().NumIn()) } obj := &Object{runtime: r} @@ -1221,34 +1577,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 } @@ -1258,12 +1614,18 @@ func (r *Runtime) toReflectValue(v Value, typ reflect.Type) (reflect.Value, erro } } - if typ.Implements(typeValue) { + if typ == typeValue { return reflect.ValueOf(v), nil } + if typ == typeObject { + 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) { @@ -1273,22 +1635,22 @@ 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() { 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) @@ -1304,31 +1666,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 } @@ -1346,7 +1706,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(unistring.NewFromString(name), nil) } if v != nil { @@ -1378,7 +1738,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) { @@ -1437,12 +1797,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(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) + 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. @@ -1520,8 +1880,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 @@ -1576,3 +1936,186 @@ 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) 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 { + c = r.toObject(c).self.getSym(symSpecies, nil) + } + 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 +} + +func createDataPropertyOrThrow(o *Object, p Value, v Value) { + o.defineOwnProperty(p, PropertyDescriptor{ + Writable: FLAG_TRUE, + Enumerable: FLAG_TRUE, + Configurable: FLAG_TRUE, + Value: v, + }, true) +} + +func toPropertyKey(key Value) Value { + return key.ToString() +} + +func (r *Runtime) getVStr(v Value, p unistring.String) Value { + o := v.ToObject(r) + return o.self.getStr(p, v) +} + +func (r *Runtime) getV(v Value, p Value) Value { + o := v.ToObject(r) + return o.get(p, v) +} + +func (r *Runtime) getIterator(obj Value, method func(FunctionCall) Value) *Object { + if method == nil { + method = toMethod(r.getV(obj, symIterator)) + if method == nil { + panic(r.NewTypeError("object is not iterable")) + } + } + + return r.toObject(method(FunctionCall{ + This: obj, + })) +} + +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})) + if nilSafe(res.self.getStr("done", nil)).ToBoolean() { + break + } + err := tryFunc(func() { + step(nilSafe(res.self.getStr("value", nil))) + }) + if err != nil { + returnIter(iter) + panic(err) + } + } +} + +func (r *Runtime) createIterResultObject(value Value, done bool) Value { + o := r.NewObject() + o.self.setOwnStr("value", value, false) + o.self.setOwnStr("done", r.toBoolean(done), false) + 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) getHash() *maphash.Hash { + if r.hash == nil { + r.hash = &maphash.Hash{} + } + return r.hash +} + +func nilSafe(v Value) Value { + if v != nil { + return v + } + 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 + } +} + +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 +} + +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/runtime_test.go b/runtime_test.go index 6e766e55..da90dc38 100644 --- a/runtime_test.go +++ b/runtime_test.go @@ -5,6 +5,7 @@ import ( "fmt" "reflect" "runtime" + "strconv" "testing" "time" ) @@ -17,57 +18,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 +28,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 = {}; @@ -233,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); @@ -825,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"; @@ -1243,7 +1187,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") } @@ -1270,7 +1214,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") } @@ -1379,6 +1323,206 @@ func TestFuncProto(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 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]()"); + + 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) +} + +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 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 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() { + } + + 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) @@ -1445,3 +1589,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 b9fa1e2d..1a35f442 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 ( @@ -20,13 +23,14 @@ 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") stringPlusInfinity = asciiString("+Infinity") stringNegInfinity = asciiString("-Infinity") + stringBound_ valueString = asciiString("bound ") stringEmpty valueString = asciiString("") - string__proto__ valueString = asciiString(__proto__) stringError valueString = asciiString("Error") stringTypeError valueString = asciiString("TypeError") @@ -46,37 +50,124 @@ 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 + utf16Reader(start int) io.RuneReader + runes() []rune + utf16Runes() []rune + 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: &lenientUtf16Decoder{utf16Reader: s.utf16Reader(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,117 +179,140 @@ 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) get(n Value) Value { - if idx := toIdx(n); idx >= 0 && idx < s.length { - return s.getIdx(idx) +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.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) +func (s *stringObject) getIdx(idx valueInt, receiver Value) Value { + i := int64(idx) + if i >= 0 { + if i < int64(s.length) { + return s._getIdx(int(i)) + } + return nil } - return s.baseObject.getStr(name) + return s.baseObject.getStr(idx.string(), receiver) } -func (s *stringObject) getPropStr(name string) Value { - if i := strToIdx(name); i >= 0 && i < s.length { - return s.getIdx(i) +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, + enumerable: true, + } } - 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) - } - return s.baseObject.getProp(n) + return s.baseObject.getOwnPropStr(name) } -func (s *stringObject) getOwnProp(name string) Value { - if i := strToIdx(name); i >= 0 && i < s.length { - val := s.getIdx(i) - return &valueProperty{ - value: val, - enumerable: true, +func (s *stringObject) getOwnPropIdx(idx valueInt) Value { + i := int64(idx) + if i >= 0 { + if i < int64(s.length) { + val := s._getIdx(int(i)) + return &valueProperty{ + value: val, + enumerable: true, + } } + return nil } - return s.baseObject.getOwnProp(name) + 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) put(n Value, val Value, throw bool) { - if i := toIdx(n); 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 + 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 < int64(s.length) { s.val.runtime.typeErrorResult(throw, "Cannot assign to read only property '%d' of a String", i) - return + return false + } + + return s.baseObject.setOwnStr(idx.string(), val, throw) +} + +func (s *stringObject) setForeignStr(name unistring.String, val, receiver Value, throw bool) (bool, bool) { + return s._setForeignStr(name, s.getOwnPropStr(name), val, receiver, throw) +} + +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 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 } - s.baseObject.putStr(name, val, throw) + return s.baseObject.defineOwnPropertyStr(name, descr, throw) } -func (s *stringObject) defineOwnProperty(n Value, descr propertyDescr, throw bool) bool { - if i := toIdx(n); i >= 0 && i < s.length { +func (s *stringObject) defineOwnPropertyIdx(idx valueInt, descr PropertyDescriptor, throw bool) bool { + i := int64(idx) + if i >= 0 && i < int64(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 + 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._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 := 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 := strToIdx(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 } @@ -206,25 +320,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 < int64(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 unistring.String) bool { + if i := strToGoIdx(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 < int64(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 8fecf341..d997df9d 100644 --- a/string_ascii.go +++ b/string_ascii.go @@ -2,11 +2,14 @@ package goja import ( "fmt" + "hash/maphash" "io" "math" "reflect" "strconv" "strings" + + "github.com/dop251/goja/unistring" ) type asciiString string @@ -33,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 == "" { @@ -44,14 +63,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) @@ -104,7 +120,11 @@ func (s asciiString) ToInteger() int64 { return i } -func (s asciiString) ToString() valueString { +func (s asciiString) toString() valueString { + return s +} + +func (s asciiString) ToString() Value { return s } @@ -160,7 +180,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 { @@ -175,15 +195,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 { @@ -194,7 +214,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 } @@ -206,18 +226,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 @@ -225,12 +233,19 @@ func (s asciiString) baseObject(r *Runtime) *Object { return ss.val } -func (s asciiString) charAt(idx int64) rune { +func (s asciiString) hash(hash *maphash.Hash) uint64 { + _, _ = hash.WriteString(string(s)) + h := hash.Sum64() + hash.Reset() + return h +} + +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 +255,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 +279,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 +293,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 +319,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 23bc6588..e976ca1c 100644 --- a/string_unicode.go +++ b/string_unicode.go @@ -3,15 +3,18 @@ package goja import ( "errors" "fmt" - "github.com/dop251/goja/parser" - "golang.org/x/text/cases" - "golang.org/x/text/language" + "hash/maphash" "io" "math" "reflect" "strings" "unicode/utf16" "unicode/utf8" + + "github.com/dop251/goja/parser" + "github.com/dop251/goja/unistring" + "golang.org/x/text/cases" + "golang.org/x/text/language" ) type unicodeString []uint16 @@ -21,61 +24,293 @@ 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 { + buf []uint16 + unicode bool } 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 } 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(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)) + if !b.unicode && r >= utf8.RuneSelf { + b.unicode = true + } + } else { + b.ensureStarted(2) + first, second := utf16.EncodeRune(r) + b.buf = append(b.buf, uint16(first), uint16(second)) + b.unicode = true + } +} + +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:], + 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 } -func (s unicodeString) ToString() valueString { +func (s unicodeString) toString() valueString { + return s +} + +func (s unicodeString) ToString() Value { return s } @@ -99,7 +334,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 { @@ -127,20 +362,8 @@ 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()) + return s.Equals(o.toPrimitive()) } return false } @@ -149,18 +372,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 @@ -168,18 +379,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) @@ -193,11 +407,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) @@ -208,32 +425,33 @@ 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 { + // TODO handle invalid UTF-16 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 } } @@ -245,11 +463,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++ { @@ -259,13 +477,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 } } @@ -277,6 +496,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())) @@ -297,7 +520,7 @@ func (s unicodeString) toLower() valueString { if ascii { return asciiString(r) } - return unicodeString(utf16.Encode(r)) + return unicodeStringFromRunes(r) } func (s unicodeString) toUpper() valueString { @@ -312,3 +535,14 @@ func (s unicodeString) Export() interface{} { func (s unicodeString) ExportType() reflect.Type { return reflectTypeString } + +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 f11cd69c..10be1339 100644 --- a/tc39_test.go +++ b/tc39_test.go @@ -2,13 +2,16 @@ package goja import ( "errors" + "fmt" "gopkg.in/yaml.v2" "io/ioutil" "os" "path" + "sort" "strings" "sync" "testing" + "time" ) const ( @@ -17,45 +20,191 @@ const ( var ( invalidFormatError = errors.New("Invalid file format") + + ignorableTestError = newSymbol(stringEmpty) + + sabStub = MustCompile("sabStub.js", ` + Object.defineProperty(this, "SharedArrayBuffer", { + get: function() { + throw IgnorableTestError; + } + });`, + false) ) 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/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 + + // \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, + + // 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/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, + "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, + "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, + "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, + + // 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, + + // 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, + + // Promise + "test/built-ins/Symbol/species/builtin-getter-name.js": true, + } + + featuresBlackList = []string{ + "arrow-function", } - 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, - } - - es6IdWhiteList = []string{} + es6WhiteList = map[string]bool{} + + es6IdWhiteList = []string{ + "8.1.2.1", + "9.5", + "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", + "22.1", + "22.2", + "23.1", + "23.2", + "23.3", + "23.4", + "24.1", + "24.2", + "24.3", + "25.1.2", + "26.1", + "26.2", + "B.2.1", + "B.2.2", + } + + esIdPrefixWhiteList = []string{ + "sec-array", + "sec-%typedarray%", + "sec-string", + "sec-date", + "sec-number", + "sec-math", + "sec-arraybuffer-length", + "sec-arraybuffer", + "sec-regexp", + } ) type tc39Test struct { @@ -63,11 +212,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 } @@ -79,6 +238,7 @@ type tc39Meta struct { Negative TC39MetaNegative Includes []string Flags []string + Features []string Es5id string Es6id string Esid string @@ -130,12 +290,43 @@ 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 { + panic(fmt.Sprintf("panic while running %s: %v", name, x)) + } + }() vm := New() + _262 := vm.NewObject() + _262.Set("detachArrayBuffer", ctx.detachArrayBuffer) + _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) 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 { @@ -206,7 +397,21 @@ 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 + } + } + } + } + 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 } @@ -216,6 +421,19 @@ 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") + } + } + } + } + + var startTime time.Time + if ctx.enableBench { + startTime = time.Now() } hasRaw := meta.hasFlag("raw") @@ -232,6 +450,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() { @@ -343,32 +570,49 @@ 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.runTC39Tests("test/annexB/built-ins/RegExp") + + 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/token/token_const.go b/token/token_const.go index b1d83c6d..16976284 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,148 @@ var token2string = [...]string{ } var keywordTable = map[string]_keyword{ - "if": _keyword{ + "if": { token: IF, }, - "in": _keyword{ + "in": { token: IN, }, - "do": _keyword{ + "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/typedarrays.go b/typedarrays.go new file mode 100644 index 00000000..9a10b0a8 --- /dev/null +++ b/typedarrays.go @@ -0,0 +1,886 @@ +package goja + +import ( + "math" + "math/bits" + "reflect" + "strconv" + "unsafe" + + "github.com/dop251/goja/unistring" +) + +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 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 unistring.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 unistring.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 unistring.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 unistring.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 unistring.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(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 unistring.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 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()) + } + } + + 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: unistring.String(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/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 39225dbd..d6ac9a74 100644 --- a/value.go +++ b/value.go @@ -1,12 +1,32 @@ package goja import ( + "hash/maphash" "math" "reflect" - "regexp" "strconv" + "unsafe" + + "github.com/dop251/goja/ftoa" + "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) @@ -14,8 +34,9 @@ var ( _NaN Value = valueFloat(math.NaN()) _positiveInf Value = valueFloat(math.Inf(+1)) _negativeInf Value = valueFloat(math.Inf(-1)) - _positiveZero Value - _negativeZero Value = valueFloat(math.Float64frombits(0 | (1 << 63))) + _positiveZero Value = valueInt(0) + negativeZero = math.Float64frombits(0 | (1 << 63)) + _negativeZero Value = valueFloat(negativeZero) _epsilon = valueFloat(2.2204460492503130808472633361816e-16) _undefined Value = valueUndefined{} ) @@ -34,7 +55,9 @@ var intCache [256]Value type Value interface { ToInteger() int64 - ToString() valueString + toString() valueString + string() unistring.String + ToString() Value String() string ToFloat() float64 ToNumber() Value @@ -46,13 +69,18 @@ type Value interface { Export() interface{} ExportType() reflect.Type - assertInt() (int64, bool) - assertString() (valueString, bool) - assertFloat() (float64, bool) - baseObject(r *Runtime) *Object + + hash(hasher *maphash.Hash) uint64 +} + +type valueContainer interface { + toValue(*Runtime) Value } +type typeError string +type rangeError string + type valueInt int64 type valueFloat float64 type valueBool bool @@ -60,10 +88,14 @@ type valueNull struct{} type valueUndefined struct { valueNull } +type valueSymbol struct { + h uintptr + desc valueString +} type valueUnresolved struct { r *Runtime - ref string + ref unistring.String } type memberUnresolved struct { @@ -89,7 +121,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 } @@ -102,18 +134,31 @@ 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 } +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) } -func (i valueInt) ToString() valueString { +func (i valueInt) toString() valueString { return asciiString(i.String()) } +func (i valueInt) string() unistring.String { + return unistring.String(i.String()) +} + +func (i valueInt) ToString() Value { + return i +} + func (i valueInt) String() string { return strconv.FormatInt(int64(i), 10) } @@ -135,50 +180,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() + case *Object: + return i.Equals(o.toPrimitiveNumber()) } - if o, ok := other.(*Object); ok { - 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 { @@ -193,52 +223,64 @@ func (i valueInt) ExportType() reflect.Type { return reflectTypeInt } -func (o valueBool) ToInteger() int64 { - if o { +func (i valueInt) hash(*maphash.Hash) uint64 { + return uint64(i) +} + +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) String() string { - if o { +func (b valueBool) ToString() Value { + return b +} + +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 } @@ -256,57 +298,69 @@ 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) assertInt() (int64, bool) { - return 0, false -} - -func (o valueBool) assertFloat() (float64, bool) { - return 0, false +func (b valueBool) baseObject(r *Runtime) *Object { + return r.global.BooleanPrototype } -func (o valueBool) assertString() (valueString, bool) { - return nil, false +func (b valueBool) Export() interface{} { + return bool(b) } -func (o valueBool) baseObject(r *Runtime) *Object { - return r.global.BooleanPrototype +func (b valueBool) ExportType() reflect.Type { + return reflectTypeBool } -func (o valueBool) Export() interface{} { - return bool(o) -} +func (b valueBool) hash(*maphash.Hash) uint64 { + if b { + return hashTrue + } -func (o valueBool) ExportType() reflect.Type { - return reflectTypeBool + return hashFalse } func (n valueNull) ToInteger() int64 { return 0 } -func (n valueNull) ToString() valueString { +func (n valueNull) toString() valueString { return stringNull } +func (n valueNull) string() unistring.String { + return stringNull.string() +} + +func (n valueNull) ToString() 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) ToString() Value { + return u +} + func (u valueUndefined) String() string { return "undefined" } +func (u valueUndefined) string() unistring.String { + return "undefined" +} + func (u valueUndefined) ToNumber() Value { return _NaN } @@ -325,6 +379,10 @@ func (u valueUndefined) ToFloat() float64 { return math.NaN() } +func (u valueUndefined) hash(*maphash.Hash) uint64 { + return hashUndef +} + func (n valueNull) ToFloat() float64 { return 0 } @@ -361,19 +419,7 @@ 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(r *Runtime) *Object { +func (n valueNull) baseObject(*Runtime) *Object { return nil } @@ -385,14 +431,26 @@ func (n valueNull) ExportType() reflect.Type { return reflectTypeNil } +func (n valueNull) hash(*maphash.Hash) uint64 { + return hashNull +} + func (p *valueProperty) ToInteger() int64 { return 0 } -func (p *valueProperty) ToString() valueString { +func (p *valueProperty) toString() valueString { return stringEmpty } +func (p *valueProperty) string() unistring.String { + return "" +} + +func (p *valueProperty) ToString() Value { + return _undefined +} + func (p *valueProperty) String() string { return "" } @@ -405,7 +463,7 @@ func (p *valueProperty) ToBoolean() bool { return false } -func (p *valueProperty) ToObject(r *Runtime) *Object { +func (p *valueProperty) ToObject(*Runtime) *Object { return nil } @@ -413,18 +471,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 } @@ -461,62 +507,61 @@ 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 } -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 (f valueFloat) ToInteger() int64 { +func (p *valueProperty) hash(*maphash.Hash) uint64 { + panic("valueProperty should never be used in maps or sets") +} + +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 { +func (f valueFloat) toString() valueString { return asciiString(f.String()) } -var matchLeading0Exponent = regexp.MustCompile(`([eE][\+\-])0+([1-9])`) // 1e-07 => 1e-7 +func (f valueFloat) string() unistring.String { + return unistring.String(f.String()) +} + +func (f valueFloat) ToString() Value { + return f +} 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 { @@ -536,18 +581,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 { @@ -555,52 +602,34 @@ 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 { - return f.Equals(o.self.toPrimitiveNumber()) + case *Object: + return f.Equals(o.toPrimitiveNumber()) } return false } 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 { @@ -615,32 +644,47 @@ func (f valueFloat) ExportType() reflect.Type { return reflectTypeFloat } +func (f valueFloat) hash(*maphash.Hash) uint64 { + if f == _negativeZero { + return 0 + } + return math.Float64bits(float64(f)) +} + 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() +func (o *Object) toString() valueString { + return o.toPrimitiveString().toString() +} + +func (o *Object) string() unistring.String { + return o.toPrimitiveString().string() +} + +func (o *Object) ToString() Value { + return o.toPrimitiveString().ToString() } 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 { return true } -func (o *Object) ToObject(r *Runtime) *Object { +func (o *Object) ToObject(*Runtime) *Object { return o } func (o *Object) ToNumber() Value { - return o.self.toPrimitiveNumber().ToNumber() + return o.toPrimitiveNumber().ToNumber() } func (o *Object) SameAs(other Value) bool { @@ -655,21 +699,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 { - return o.self.toPrimitive().Equals(other) - } - - if other, ok := other.(valueBool); ok { - return o.Equals(other.ToNumber()) + switch o1 := other.(type) { + case valueInt, valueFloat, valueString: + return o.toPrimitive().Equals(other) + case valueBool: + return o.Equals(o1.ToNumber()) } - if _, ok := other.assertString(); ok { - return o.self.toPrimitive().Equals(other) - } return false } @@ -680,19 +716,7 @@ 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(r *Runtime) *Object { +func (o *Object) baseObject(*Runtime) *Object { return o } @@ -704,13 +728,19 @@ func (o *Object) ExportType() reflect.Type { return o.self.exportType() } +func (o *Object) hash(*maphash.Hash) uint64 { + return o.getId() +} + func (o *Object) Get(name string) Value { - return o.self.getStr(name) + return o.self.getStr(unistring.NewFromString(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 @@ -720,7 +750,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(unistring.NewFromString(name), PropertyDescriptor{ Value: value, Writable: writable, Configurable: configurable, @@ -733,7 +763,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(unistring.NewFromString(name), PropertyDescriptor{ Getter: getter, Setter: setter, Configurable: configurable, @@ -744,7 +774,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(unistring.NewFromString(name), o.runtime.ToValue(value), true) }) } @@ -779,7 +809,17 @@ func (o valueUnresolved) ToInteger() int64 { return 0 } -func (o valueUnresolved) ToString() valueString { +func (o valueUnresolved) toString() valueString { + o.throw() + return nil +} + +func (o valueUnresolved) string() unistring.String { + o.throw() + return "" +} + +func (o valueUnresolved) ToString() Value { o.throw() return nil } @@ -799,7 +839,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 } @@ -809,49 +849,118 @@ 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 } -func (o valueUnresolved) assertInt() (int64, bool) { +func (o valueUnresolved) baseObject(*Runtime) *Object { o.throw() - return 0, false + return nil } -func (o valueUnresolved) assertFloat() (float64, bool) { +func (o valueUnresolved) Export() interface{} { o.throw() - return 0, false + return nil } -func (o valueUnresolved) assertString() (valueString, bool) { +func (o valueUnresolved) ExportType() reflect.Type { o.throw() - return nil, false + return nil } -func (o valueUnresolved) baseObject(r *Runtime) *Object { +func (o valueUnresolved) hash(*maphash.Hash) uint64 { o.throw() - return nil + return 0 } -func (o valueUnresolved) Export() interface{} { - o.throw() - return nil +func (s *valueSymbol) ToInteger() int64 { + panic(typeError("Cannot convert a Symbol value to a number")) } -func (o valueUnresolved) ExportType() reflect.Type { - o.throw() - return nil +func (s *valueSymbol) toString() valueString { + panic(typeError("Cannot convert a Symbol value to a string")) +} + +func (s *valueSymbol) ToString() Value { + return s +} + +func (s *valueSymbol) String() string { + return s.desc.String() +} + +func (s *valueSymbol) string() unistring.String { + return s.desc.string() +} + +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) baseObject(r *Runtime) *Object { + return r.newPrimitiveObject(s, r.global.SymbolPrototype, "Symbol") +} + +func (s *valueSymbol) hash(*maphash.Hash) uint64 { + return uint64(s.h) +} + +func newSymbol(s valueString) *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() { diff --git a/vm.go b/vm.go index 8739945a..05420c19 100644 --- a/vm.go +++ b/vm.go @@ -7,6 +7,8 @@ import ( "strconv" "sync" "sync/atomic" + + "github.com/dop251/goja/unistring" ) const ( @@ -18,34 +20,36 @@ type valueStack []Value type stash struct { values valueStack extraArgs valueStack - names map[string]uint32 + names map[unistring.String]uint32 obj *Object outer *stash } type context struct { - prg *Program - funcName string - stash *stash - pc, sb int - args int + prg *Program + funcName unistring.String + stash *stash + newTarget Value + pc, sb int + args int } type iterStackItem struct { - val Value - f iterNextFunc + val Value + f iterNextFunc + iter *Object } 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 { @@ -56,31 +60,31 @@ 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 } 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 { +func (r *objRef) refname() unistring.String { return r.name } type unresolvedRef struct { runtime *Runtime - name string + name unistring.String } func (r *unresolvedRef) get() Value { @@ -92,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 @@ -108,6 +112,7 @@ type vm struct { callStack []context iterStack []iterStackItem refStack []ref + newTarget Value stashAllocs int halt bool @@ -128,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) { @@ -155,13 +160,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 +175,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 } } @@ -198,10 +203,22 @@ func (s *valueStack) expand(idx int) { } } -func (s *stash) put(name string, v Value) bool { +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() + } + } + return true + } + return false +} + +func (s *stash) put(name unistring.String, v Value) bool { if s.obj != nil { - if found := s.obj.self.getStr(name); found != nil { - s.obj.self.putStr(name, v, false) + if stashObjHas(s.obj, name) { + s.obj.self.setOwnStr(name, v, false) return true } return false @@ -230,14 +247,12 @@ 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 { - v = s.obj.self.getStr(name) - if v == nil { - return nil, false - //return valueUnresolved{r: vm.r, ref: name}, false + if stashObjHas(s.obj, name) { + return nilSafe(s.obj.self.getStr(name, nil)), true } - return v, true + return nil, false } if idx, exists := s.names[name]; exists { return s.values[idx], true @@ -246,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)) @@ -256,9 +271,12 @@ 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 { - return s.obj.self.deleteStr(name, false) + if stashObjHas(s.obj, name) { + return s.obj.self.deleteStr(name, false) + } + return false } if idx, found := s.names[name]; found { s.values[idx] = nil @@ -317,12 +335,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 @@ -347,6 +365,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] @@ -366,6 +387,14 @@ func (vm *vm) try(f func()) (ex *Exception) { panic(x1) case *Exception: ex = x1 + case typeError: + 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 { @@ -405,8 +434,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 @@ -432,6 +466,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() { @@ -448,19 +483,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 +492,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{} @@ -596,29 +617,29 @@ 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 - leftString, isLeftString := left.assertString() - rightString, isRightString := right.assertString() + leftString, isLeftString := left.(valueString) + rightString, isRightString := right.(valueString) if isLeftString || isRightString { if !isLeftString { - leftString = left.ToString() + leftString = left.toString() } if !isRightString { - rightString = right.ToString() + rightString = right.toString() } 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()) @@ -643,9 +664,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 } } @@ -667,8 +688,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 @@ -760,8 +781,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 @@ -792,7 +813,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 { @@ -826,7 +847,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 } @@ -845,7 +866,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 } @@ -909,7 +930,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++ @@ -921,7 +942,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++ @@ -932,8 +953,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++ @@ -960,10 +981,10 @@ 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) + obj.setOwn(propName, val, false) vm.sp -= 2 vm.stack[vm.sp-1] = val @@ -976,10 +997,10 @@ 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) + obj.setOwn(propName, val, true) vm.sp -= 2 vm.stack[vm.sp-1] = val @@ -992,8 +1013,8 @@ var deleteElem _deleteElem func (_deleteElem) exec(vm *vm) { obj := vm.r.toObject(vm.stack[vm.sp-2]) - propName := vm.stack[vm.sp-1] - if !obj.self.hasProperty(propName) || obj.self.delete(propName, false) { + propName := toPropertyKey(vm.stack[vm.sp-1]) + if obj.delete(propName, false) { vm.stack[vm.sp-2] = valueTrue } else { vm.stack[vm.sp-2] = valueFalse @@ -1008,18 +1029,18 @@ var deleteElemStrict _deleteElemStrict func (_deleteElemStrict) exec(vm *vm) { obj := vm.r.toObject(vm.stack[vm.sp-2]) - propName := vm.stack[vm.sp-1] - obj.self.delete(propName, true) + propName := toPropertyKey(vm.stack[vm.sp-1]) + obj.delete(propName, true) vm.stack[vm.sp-2] = valueTrue vm.sp-- 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.hasPropertyStr(string(d)) || 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 @@ -1027,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.putStr(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.putStr(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++ @@ -1073,49 +1094,49 @@ 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++ } -type setPropGetter string +type setPropGetter unistring.String 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(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]) 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(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] @@ -1123,36 +1144,25 @@ 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(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", g)) + panic(vm.r.NewTypeError("Cannot read property '%s' of undefined or null", n)) } - 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(n, v) + if prop == nil { + prop = memberUnresolved{valueUnresolved{r: vm.r, ref: n}} } + vm.stack[vm.sp-1] = prop vm.pc++ } @@ -1164,20 +1174,12 @@ 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())) } - 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++ @@ -1190,21 +1192,16 @@ 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) - 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++ @@ -1259,15 +1256,28 @@ func (l newArray) exec(vm *vm) { vm.pc++ } +type newArraySparse struct { + l, objCount int +} + +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 = n.objCount + vm.sp -= int(n.l) - 1 + vm.stack[vm.sp-1] = arr.val + vm.pc++ +} + type newRegexp struct { - pattern regexpPattern + pattern *regexpPattern src valueString - - global, ignoreCase, multiline 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, vm.r.global.RegExpPrototype)) vm.pc++ } @@ -1297,7 +1307,7 @@ func (s setLocalP) exec(vm *vm) { } type setVar struct { - name string + name unistring.String idx uint32 } @@ -1305,7 +1315,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++ { @@ -1318,21 +1328,21 @@ 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: 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 { - if stash.obj.self.hasPropertyStr(name) { + if stashObjHas(stash.obj, name) { ref = &objRef{ base: stash.obj.self, name: name, @@ -1359,14 +1369,14 @@ 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 { - if stash.obj.self.hasPropertyStr(name) { + if stashObjHas(stash.obj, name) { ret = stash.obj.self.deleteStr(name, false) goto end } @@ -1391,10 +1401,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) @@ -1409,14 +1419,14 @@ 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 { - if stash.obj.self.hasPropertyStr(name) { + if stashObjHas(stash.obj, name) { ref = &objRef{ base: stash.obj.self, name: name, @@ -1445,7 +1455,7 @@ func (s resolveVar1Strict) exec(vm *vm) { ref = &unresolvedRef{ runtime: vm.r, - name: string(s), + name: name, } end: @@ -1453,24 +1463,24 @@ end: vm.pc++ } -type setGlobal string +type setGlobal unistring.String func (s setGlobal) exec(vm *vm) { v := vm.peek() - vm.r.globalObject.self.putStr(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.putStr(name, v, true) + o.setOwnStr(name, v, true) } else { vm.r.throwReferenceError(name) } @@ -1492,7 +1502,7 @@ func (g getLocal) exec(vm *vm) { } type getVar struct { - name string + name unistring.String idx uint32 ref, callee bool } @@ -1522,7 +1532,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} @@ -1537,19 +1547,19 @@ end: } type resolveVar struct { - name string + name unistring.String idx uint32 strict bool } 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++ { - if stash.obj != nil { - if stash.obj.self.hasPropertyStr(r.name) { + if obj := stash.obj; obj != nil { + if stashObjHas(obj, r.name) { ref = &objRef{ base: stash.obj.self, name: r.name, @@ -1621,10 +1631,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 { @@ -1633,7 +1643,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) } @@ -1645,7 +1655,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 { @@ -1654,7 +1664,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} } @@ -1663,10 +1673,10 @@ func (n getVar1Ref) 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 var callee *Object for stash := vm.stash; stash != nil; stash = stash.outer { @@ -1677,7 +1687,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} } @@ -1704,14 +1714,14 @@ 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] } 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 @@ -1763,7 +1773,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: @@ -1779,11 +1789,23 @@ 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 default: - vm.r.typeErrorResult(true, "Not a function: %s", obj.ToString()) + vm.r.typeErrorResult(true, "Not a function: %s", obj.toString()) } } @@ -1791,7 +1813,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], @@ -1912,7 +1934,7 @@ func (_retStashless) exec(vm *vm) { type newFunc struct { prg *Program - name string + name unistring.String length uint32 strict bool @@ -1928,13 +1950,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++ } @@ -1996,7 +2019,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 } @@ -2005,15 +2035,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 } @@ -2174,7 +2204,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 @@ -2192,7 +2222,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 @@ -2256,11 +2286,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++ } @@ -2276,37 +2306,25 @@ 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") - } - + sp := vm.sp - int(n) + 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++ } -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) +type _loadNewTarget struct{} + +var loadNewTarget _loadNewTarget + +func (_loadNewTarget) exec(vm *vm) { + if t := vm.newTarget; t != nil { + vm.push(t) } else { - vm.r.typeErrorResult(true, "Not a constructor") + vm.push(_undefined) } + vm.pc++ } type _typeof struct{} @@ -2337,6 +2355,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)) } @@ -2362,7 +2382,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, @@ -2373,7 +2393,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++ } @@ -2392,12 +2412,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++ } @@ -2441,7 +2461,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++ @@ -2453,7 +2473,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 { @@ -2481,3 +2501,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++ + } +} diff --git a/vm_test.go b/vm_test.go index 24a8ba7f..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() + } + } +}