From dc8c55024d06009a0cba080cd738192d368735de Mon Sep 17 00:00:00 2001 From: Dmitry Panov Date: Fri, 22 Oct 2021 12:31:20 +0100 Subject: [PATCH] Implemented Promise. Closes #178. --- builtin_array.go | 6 +- builtin_error.go | 26 ++ builtin_map.go | 6 +- builtin_promise.go | 597 +++++++++++++++++++++++++++++++++++++++++ builtin_regexp.go | 6 +- builtin_set.go | 6 +- builtin_typedarrays.go | 19 +- object.go | 2 + runtime.go | 67 ++++- runtime_test.go | 40 +++ string.go | 1 + tc39_test.go | 76 +++++- 12 files changed, 797 insertions(+), 55 deletions(-) create mode 100644 builtin_promise.go diff --git a/builtin_array.go b/builtin_array.go index 75011923..66829292 100644 --- a/builtin_array.go +++ b/builtin_array.go @@ -1354,11 +1354,7 @@ func (r *Runtime) createArray(val *Object) objectImpl { o._putProp("from", r.newNativeFunc(r.array_from, nil, "from", nil, 1), true, false, true) o._putProp("isArray", r.newNativeFunc(r.array_isArray, nil, "isArray", nil, 1), true, false, true) o._putProp("of", r.newNativeFunc(r.array_of, nil, "of", nil, 0), true, false, true) - o._putSym(SymSpecies, &valueProperty{ - getterFunc: r.newNativeFunc(r.returnThis, nil, "get [Symbol.species]", nil, 0), - accessor: true, - configurable: true, - }) + r.putSpeciesReturnThis(o) return o } diff --git a/builtin_error.go b/builtin_error.go index 5880b88d..129c80d1 100644 --- a/builtin_error.go +++ b/builtin_error.go @@ -1,5 +1,27 @@ package goja +func (r *Runtime) builtin_Error(args []Value, proto *Object) *Object { + obj := r.newBaseObject(proto, classError) + if len(args) > 0 && args[0] != _undefined { + obj._putProp("message", args[0], true, false, true) + } + return obj.val +} + +func (r *Runtime) builtin_AggregateError(args []Value, proto *Object) *Object { + obj := r.newBaseObject(proto, classAggError) + if len(args) > 1 && args[1] != _undefined { + obj._putProp("message", args[1], true, false, true) + } + var errors []Value + if len(args) > 0 { + errors = r.iterableToList(args[0], nil) + } + obj._putProp("errors", r.newArrayValues(errors), true, false, true) + + return obj.val +} + func (r *Runtime) createErrorPrototype(name valueString) *Object { o := r.newBaseObject(r.global.ErrorPrototype, classObject) o._putProp("message", stringEmpty, true, false, true) @@ -17,6 +39,10 @@ func (r *Runtime) initErrors() { r.global.Error = r.newNativeFuncConstruct(r.builtin_Error, "Error", r.global.ErrorPrototype, 1) r.addToGlobal("Error", r.global.Error) + r.global.AggregateErrorPrototype = r.createErrorPrototype(stringAggregateError) + r.global.AggregateError = r.newNativeFuncConstructProto(r.builtin_AggregateError, "AggregateError", r.global.AggregateErrorPrototype, r.global.Error, 1) + r.addToGlobal("AggregateError", r.global.AggregateError) + r.global.TypeErrorPrototype = r.createErrorPrototype(stringTypeError) r.global.TypeError = r.newNativeFuncConstructProto(r.builtin_Error, "TypeError", r.global.TypeErrorPrototype, r.global.Error, 1) diff --git a/builtin_map.go b/builtin_map.go index 097dbff6..f15c4677 100644 --- a/builtin_map.go +++ b/builtin_map.go @@ -243,11 +243,7 @@ func (r *Runtime) createMapProto(val *Object) objectImpl { 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, - }) + r.putSpeciesReturnThis(o) return o } diff --git a/builtin_promise.go b/builtin_promise.go new file mode 100644 index 00000000..9d21417c --- /dev/null +++ b/builtin_promise.go @@ -0,0 +1,597 @@ +package goja + +import ( + "github.com/dop251/goja/unistring" +) + +type PromiseState int +type PromiseRejectionOperation int + +type promiseReactionType int + +const ( + PromiseStatePending PromiseState = iota + PromiseStateFulfilled + PromiseStateRejected +) + +const ( + PromiseRejectionReject PromiseRejectionOperation = iota + PromiseRejectionHandle +) + +const ( + promiseReactionFulfill promiseReactionType = iota + promiseReactionReject +) + +type PromiseRejectionTracker func(p *Promise, operation PromiseRejectionOperation) + +type jobCallback struct { + callback func(FunctionCall) Value +} + +type promiseCapability struct { + promise *Object + resolveObj, rejectObj *Object +} + +type promiseReaction struct { + capability *promiseCapability + typ promiseReactionType + handler *jobCallback +} + +// Promise is a Go wrapper around ECMAScript Promise. Calling Runtime.ToValue() on it +// returns the underlying Object. Calling Export() on a Promise Object returns a Promise. +// +// Use Runtime.NewPromise() to create one. Calling Runtime.ToValue() on a zero object or nil returns null Value. +// +// WARNING: Instances of Promise are not goroutine-safe. See Runtime.NewPromise() for more details. +type Promise struct { + baseObject + state PromiseState + result Value + fulfillReactions []*promiseReaction + rejectReactions []*promiseReaction + handled bool +} + +func (p *Promise) State() PromiseState { + return p.state +} + +func (p *Promise) Result() Value { + return p.result +} + +func (p *Promise) toValue(r *Runtime) Value { + if p == nil || p.val == nil { + return _null + } + promise := p.val + if promise.runtime != r { + panic(r.NewTypeError("Illegal runtime transition of a Promise")) + } + return promise +} + +func (p *Promise) createResolvingFunctions() (resolve, reject *Object) { + r := p.val.runtime + alreadyResolved := false + return p.val.runtime.newNativeFunc(func(call FunctionCall) Value { + if alreadyResolved { + return _undefined + } + alreadyResolved = true + resolution := call.Argument(0) + if resolution.SameAs(p.val) { + return p.reject(r.NewTypeError("Promise self-resolution")) + } + if obj, ok := resolution.(*Object); ok { + var thenAction Value + ex := r.vm.try(func() { + thenAction = obj.self.getStr("then", nil) + }) + if ex != nil { + return p.reject(ex.val) + } + if call, ok := assertCallable(thenAction); ok { + job := r.newPromiseResolveThenableJob(p, resolution, &jobCallback{callback: call}) + r.enqueuePromiseJob(job) + return _undefined + } + } + return p.fulfill(resolution) + }, nil, "", nil, 1), + p.val.runtime.newNativeFunc(func(call FunctionCall) Value { + if alreadyResolved { + return _undefined + } + alreadyResolved = true + reason := call.Argument(0) + return p.reject(reason) + }, nil, "", nil, 1) +} + +func (p *Promise) reject(reason Value) Value { + reactions := p.rejectReactions + p.result = reason + p.fulfillReactions, p.rejectReactions = nil, nil + p.state = PromiseStateRejected + r := p.val.runtime + if !p.handled { + r.trackPromiseRejection(p, PromiseRejectionReject) + } + r.triggerPromiseReactions(reactions, reason) + return _undefined +} + +func (p *Promise) fulfill(value Value) Value { + reactions := p.fulfillReactions + p.result = value + p.fulfillReactions, p.rejectReactions = nil, nil + p.state = PromiseStateFulfilled + p.val.runtime.triggerPromiseReactions(reactions, value) + return _undefined +} + +func (r *Runtime) newPromiseResolveThenableJob(p *Promise, thenable Value, then *jobCallback) func() { + return func() { + resolve, reject := p.createResolvingFunctions() + ex := r.vm.try(func() { + r.callJobCallback(then, thenable, resolve, reject) + }) + if ex != nil { + if fn, ok := reject.self.assertCallable(); ok { + fn(FunctionCall{Arguments: []Value{ex.val}}) + } + } + } +} + +func (r *Runtime) enqueuePromiseJob(job func()) { + r.jobQueue = append(r.jobQueue, job) +} + +func (r *Runtime) triggerPromiseReactions(reactions []*promiseReaction, argument Value) { + for _, reaction := range reactions { + r.enqueuePromiseJob(r.newPromiseReactionJob(reaction, argument)) + } +} + +func (r *Runtime) newPromiseReactionJob(reaction *promiseReaction, argument Value) func() { + return func() { + var handlerResult Value + fulfill := false + if reaction.handler == nil { + handlerResult = argument + if reaction.typ == promiseReactionFulfill { + fulfill = true + } + } else { + ex := r.vm.try(func() { + handlerResult = r.callJobCallback(reaction.handler, _undefined, argument) + fulfill = true + }) + if ex != nil { + handlerResult = ex.val + } + } + if reaction.capability != nil { + if fulfill { + reaction.capability.resolve(handlerResult) + } else { + reaction.capability.reject(handlerResult) + } + } + } +} + +func (r *Runtime) newPromise(proto *Object) *Promise { + o := &Object{runtime: r} + + po := &Promise{} + po.class = classPromise + po.val = o + po.extensible = true + o.self = po + po.prototype = proto + po.init() + return po +} + +func (r *Runtime) builtin_newPromise(args []Value, newTarget *Object) *Object { + if newTarget == nil { + panic(r.needNew("Promise")) + } + var arg0 Value + if len(args) > 0 { + arg0 = args[0] + } + executor := r.toCallable(arg0) + + proto := r.getPrototypeFromCtor(newTarget, r.global.Promise, r.global.PromisePrototype) + po := r.newPromise(proto) + + resolve, reject := po.createResolvingFunctions() + ex := r.vm.try(func() { + executor(FunctionCall{Arguments: []Value{resolve, reject}}) + }) + if ex != nil { + if fn, ok := reject.self.assertCallable(); ok { + fn(FunctionCall{Arguments: []Value{ex.val}}) + } + } + return po.val +} + +func (r *Runtime) promiseProto_then(call FunctionCall) Value { + thisObj := r.toObject(call.This) + if p, ok := thisObj.self.(*Promise); ok { + c := r.speciesConstructorObj(thisObj, r.global.Promise) + resultCapability := r.newPromiseCapability(c) + return r.performPromiseThen(p, call.Argument(0), call.Argument(1), resultCapability) + } + panic(r.NewTypeError("Method Promise.prototype.then called on incompatible receiver %s", thisObj)) +} + +func (r *Runtime) newPromiseCapability(c *Object) *promiseCapability { + pcap := new(promiseCapability) + if c == r.global.Promise { + p := r.newPromise(r.global.PromisePrototype) + pcap.resolveObj, pcap.rejectObj = p.createResolvingFunctions() + pcap.promise = p.val + } else { + var resolve, reject Value + executor := r.newNativeFunc(func(call FunctionCall) Value { + if resolve != nil { + panic(r.NewTypeError("resolve is already set")) + } + if reject != nil { + panic(r.NewTypeError("reject is already set")) + } + if arg := call.Argument(0); arg != _undefined { + resolve = arg + } + if arg := call.Argument(1); arg != _undefined { + reject = arg + } + return nil + }, nil, "", nil, 2) + pcap.promise = r.toConstructor(c)([]Value{executor}, c) + pcap.resolveObj = r.toObject(resolve) + r.toCallable(pcap.resolveObj) // make sure it's callable + pcap.rejectObj = r.toObject(reject) + r.toCallable(pcap.rejectObj) + } + return pcap +} + +func (r *Runtime) performPromiseThen(p *Promise, onFulfilled, onRejected Value, resultCapability *promiseCapability) Value { + var onFulfilledJobCallback, onRejectedJobCallback *jobCallback + if f, ok := assertCallable(onFulfilled); ok { + onFulfilledJobCallback = &jobCallback{callback: f} + } + if f, ok := assertCallable(onRejected); ok { + onRejectedJobCallback = &jobCallback{callback: f} + } + fulfillReaction := &promiseReaction{ + capability: resultCapability, + typ: promiseReactionFulfill, + handler: onFulfilledJobCallback, + } + rejectReaction := &promiseReaction{ + capability: resultCapability, + typ: promiseReactionReject, + handler: onRejectedJobCallback, + } + switch p.state { + case PromiseStatePending: + p.fulfillReactions = append(p.fulfillReactions, fulfillReaction) + p.rejectReactions = append(p.rejectReactions, rejectReaction) + case PromiseStateFulfilled: + r.enqueuePromiseJob(r.newPromiseReactionJob(fulfillReaction, p.result)) + default: + reason := p.result + if !p.handled { + r.trackPromiseRejection(p, PromiseRejectionHandle) + } + r.enqueuePromiseJob(r.newPromiseReactionJob(rejectReaction, reason)) + } + p.handled = true + if resultCapability == nil { + return _undefined + } + return resultCapability.promise +} + +func (r *Runtime) promiseProto_catch(call FunctionCall) Value { + return r.invoke(call.This, "then", _undefined, call.Argument(0)) +} + +func (r *Runtime) promiseResolve(c *Object, x Value) *Object { + if obj, ok := x.(*Object); ok { + xConstructor := nilSafe(obj.self.getStr("constructor", nil)) + if xConstructor.SameAs(c) { + return obj + } + } + pcap := r.newPromiseCapability(c) + pcap.resolve(x) + return pcap.promise +} + +func (r *Runtime) promiseProto_finally(call FunctionCall) Value { + promise := r.toObject(call.This) + c := r.speciesConstructorObj(promise, r.global.Promise) + onFinally := call.Argument(0) + var thenFinally, catchFinally Value + if onFinallyFn, ok := assertCallable(onFinally); !ok { + thenFinally, catchFinally = onFinally, onFinally + } else { + thenFinally = r.newNativeFunc(func(call FunctionCall) Value { + value := call.Argument(0) + result := onFinallyFn(FunctionCall{}) + promise := r.promiseResolve(c, result) + valueThunk := r.newNativeFunc(func(call FunctionCall) Value { + return value + }, nil, "", nil, 0) + return r.invoke(promise, "then", valueThunk) + }, nil, "", nil, 1) + + catchFinally = r.newNativeFunc(func(call FunctionCall) Value { + reason := call.Argument(0) + result := onFinallyFn(FunctionCall{}) + promise := r.promiseResolve(c, result) + thrower := r.newNativeFunc(func(call FunctionCall) Value { + panic(reason) + }, nil, "", nil, 0) + return r.invoke(promise, "then", thrower) + }, nil, "", nil, 1) + } + return r.invoke(promise, "then", thenFinally, catchFinally) +} + +func (pcap *promiseCapability) resolve(result Value) { + pcap.promise.runtime.toCallable(pcap.resolveObj)(FunctionCall{Arguments: []Value{result}}) +} + +func (pcap *promiseCapability) reject(reason Value) { + pcap.promise.runtime.toCallable(pcap.rejectObj)(FunctionCall{Arguments: []Value{reason}}) +} + +func (pcap *promiseCapability) try(f func()) bool { + ex := pcap.promise.runtime.vm.try(f) + if ex != nil { + pcap.reject(ex.val) + return false + } + return true +} + +func (r *Runtime) promise_all(call FunctionCall) Value { + c := r.toObject(call.This) + pcap := r.newPromiseCapability(c) + + pcap.try(func() { + promiseResolve := r.toCallable(c.self.getStr("resolve", nil)) + iter := r.getIterator(call.Argument(0), nil) + var values []Value + remainingElementsCount := 1 + r.iterate(iter, func(nextValue Value) { + index := len(values) + values = append(values, _undefined) + nextPromise := promiseResolve(FunctionCall{This: c, Arguments: []Value{nextValue}}) + alreadyCalled := false + onFulfilled := r.newNativeFunc(func(call FunctionCall) Value { + if alreadyCalled { + return _undefined + } + alreadyCalled = true + values[index] = call.Argument(0) + remainingElementsCount-- + if remainingElementsCount == 0 { + pcap.resolve(r.newArrayValues(values)) + } + return _undefined + }, nil, "", nil, 1) + remainingElementsCount++ + r.invoke(nextPromise, "then", onFulfilled, pcap.rejectObj) + }) + remainingElementsCount-- + if remainingElementsCount == 0 { + pcap.resolve(r.newArrayValues(values)) + } + }) + return pcap.promise +} + +func (r *Runtime) promise_allSettled(call FunctionCall) Value { + c := r.toObject(call.This) + pcap := r.newPromiseCapability(c) + + pcap.try(func() { + promiseResolve := r.toCallable(c.self.getStr("resolve", nil)) + iter := r.getIterator(call.Argument(0), nil) + var values []Value + remainingElementsCount := 1 + r.iterate(iter, func(nextValue Value) { + index := len(values) + values = append(values, _undefined) + nextPromise := promiseResolve(FunctionCall{This: c, Arguments: []Value{nextValue}}) + alreadyCalled := false + reaction := func(status Value, valueKey unistring.String) *Object { + return r.newNativeFunc(func(call FunctionCall) Value { + if alreadyCalled { + return _undefined + } + alreadyCalled = true + obj := r.NewObject() + obj.self._putProp("status", status, true, true, true) + obj.self._putProp(valueKey, call.Argument(0), true, true, true) + values[index] = obj + remainingElementsCount-- + if remainingElementsCount == 0 { + pcap.resolve(r.newArrayValues(values)) + } + return _undefined + }, nil, "", nil, 1) + } + onFulfilled := reaction(asciiString("fulfilled"), "value") + onRejected := reaction(asciiString("rejected"), "reason") + remainingElementsCount++ + r.invoke(nextPromise, "then", onFulfilled, onRejected) + }) + remainingElementsCount-- + if remainingElementsCount == 0 { + pcap.resolve(r.newArrayValues(values)) + } + }) + return pcap.promise +} + +func (r *Runtime) promise_any(call FunctionCall) Value { + c := r.toObject(call.This) + pcap := r.newPromiseCapability(c) + + pcap.try(func() { + promiseResolve := r.toCallable(c.self.getStr("resolve", nil)) + iter := r.getIterator(call.Argument(0), nil) + var errors []Value + remainingElementsCount := 1 + r.iterate(iter, func(nextValue Value) { + index := len(errors) + errors = append(errors, _undefined) + nextPromise := promiseResolve(FunctionCall{This: c, Arguments: []Value{nextValue}}) + alreadyCalled := false + onRejected := r.newNativeFunc(func(call FunctionCall) Value { + if alreadyCalled { + return _undefined + } + alreadyCalled = true + errors[index] = call.Argument(0) + remainingElementsCount-- + if remainingElementsCount == 0 { + _error := r.builtin_new(r.global.AggregateError, nil) + _error.self._putProp("errors", r.newArrayValues(errors), true, false, true) + pcap.reject(_error) + } + return _undefined + }, nil, "", nil, 1) + + remainingElementsCount++ + r.invoke(nextPromise, "then", pcap.resolveObj, onRejected) + }) + remainingElementsCount-- + if remainingElementsCount == 0 { + _error := r.builtin_new(r.global.AggregateError, nil) + _error.self._putProp("errors", r.newArrayValues(errors), true, false, true) + pcap.reject(_error) + } + }) + return pcap.promise +} + +func (r *Runtime) promise_race(call FunctionCall) Value { + c := r.toObject(call.This) + pcap := r.newPromiseCapability(c) + + pcap.try(func() { + promiseResolve := r.toCallable(c.self.getStr("resolve", nil)) + iter := r.getIterator(call.Argument(0), nil) + r.iterate(iter, func(nextValue Value) { + nextPromise := promiseResolve(FunctionCall{This: c, Arguments: []Value{nextValue}}) + r.invoke(nextPromise, "then", pcap.resolveObj, pcap.rejectObj) + }) + }) + return pcap.promise +} + +func (r *Runtime) promise_reject(call FunctionCall) Value { + pcap := r.newPromiseCapability(r.toObject(call.This)) + pcap.reject(call.Argument(0)) + return pcap.promise +} + +func (r *Runtime) promise_resolve(call FunctionCall) Value { + return r.promiseResolve(r.toObject(call.This), call.Argument(0)) +} + +func (r *Runtime) createPromiseProto(val *Object) objectImpl { + o := newBaseObjectObj(val, r.global.ObjectPrototype, classObject) + o._putProp("constructor", r.global.Promise, true, false, true) + + o._putProp("catch", r.newNativeFunc(r.promiseProto_catch, nil, "catch", nil, 1), true, false, true) + o._putProp("finally", r.newNativeFunc(r.promiseProto_finally, nil, "finally", nil, 1), true, false, true) + o._putProp("then", r.newNativeFunc(r.promiseProto_then, nil, "then", nil, 2), true, false, true) + + o._putSym(SymToStringTag, valueProp(asciiString(classPromise), false, false, true)) + + return o +} + +func (r *Runtime) createPromise(val *Object) objectImpl { + o := r.newNativeConstructOnly(val, r.builtin_newPromise, r.global.PromisePrototype, "Promise", 1) + + o._putProp("all", r.newNativeFunc(r.promise_all, nil, "all", nil, 1), true, false, true) + o._putProp("allSettled", r.newNativeFunc(r.promise_allSettled, nil, "allSettled", nil, 1), true, false, true) + o._putProp("any", r.newNativeFunc(r.promise_any, nil, "any", nil, 1), true, false, true) + o._putProp("race", r.newNativeFunc(r.promise_race, nil, "race", nil, 1), true, false, true) + o._putProp("reject", r.newNativeFunc(r.promise_reject, nil, "reject", nil, 1), true, false, true) + o._putProp("resolve", r.newNativeFunc(r.promise_resolve, nil, "resolve", nil, 1), true, false, true) + + r.putSpeciesReturnThis(o) + + return o +} + +func (r *Runtime) initPromise() { + r.global.PromisePrototype = r.newLazyObject(r.createPromiseProto) + r.global.Promise = r.newLazyObject(r.createPromise) + + r.addToGlobal("Promise", r.global.Promise) +} + +func (r *Runtime) wrapPromiseReaction(fObj *Object) func(interface{}) { + f, _ := AssertFunction(fObj) + return func(x interface{}) { + _, _ = f(nil, r.ToValue(x)) + } +} + +// NewPromise creates and returns a Promise and resolving functions for it. +// +// WARNING: The returned values are not goroutine-safe and must not be called in parallel with VM running. +// In order to make use of this method you need an event loop such as the one in goja_nodejs (https://github.com/dop251/goja_nodejs) +// where it can be used like this: +// +// loop := NewEventLoop() +// loop.Start() +// defer loop.Stop() +// loop.RunOnLoop(func(vm *goja.Runtime) { +// p, resolve, _ := vm.NewPromise() +// vm.Set("p", p) +// go func() { +// time.Sleep(500 * time.Millisecond) // or perform any other blocking operation +// loop.RunOnLoop(func(*goja.Runtime) { // resolve() must be called on the loop, cannot call it here +// resolve(result) +// }) +// }() +// } +func (r *Runtime) NewPromise() (promise *Promise, resolve func(result interface{}), reject func(reason interface{})) { + p := r.newPromise(r.global.PromisePrototype) + resolveF, rejectF := p.createResolvingFunctions() + return p, r.wrapPromiseReaction(resolveF), r.wrapPromiseReaction(rejectF) +} + +// SetPromiseRejectionTracker registers a function that will be called in two scenarios: when a promise is rejected +// without any handlers (with operation argument set to PromiseRejectionReject), and when a handler is added to a +// rejected promise for the first time (with operation argument set to PromiseRejectionHandle). +// +// Setting a tracker replaces any existing one. Setting it to nil disables the functionality. +// +// See https://tc39.es/ecma262/#sec-host-promise-rejection-tracker for more details. +func (r *Runtime) SetPromiseRejectionTracker(tracker PromiseRejectionTracker) { + r.promiseRejectionTracker = tracker +} diff --git a/builtin_regexp.go b/builtin_regexp.go index ccccbd61..5f9191de 100644 --- a/builtin_regexp.go +++ b/builtin_regexp.go @@ -1267,10 +1267,6 @@ func (r *Runtime) initRegExp() { 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.putSpeciesReturnThis(rx) r.addToGlobal("RegExp", r.global.RegExp) } diff --git a/builtin_set.go b/builtin_set.go index 4a265405..66ebe584 100644 --- a/builtin_set.go +++ b/builtin_set.go @@ -218,11 +218,7 @@ func (r *Runtime) createSetProto(val *Object) objectImpl { 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, - }) + r.putSpeciesReturnThis(o) return o } diff --git a/builtin_typedarrays.go b/builtin_typedarrays.go index fe50a1f9..1e1a93ef 100644 --- a/builtin_typedarrays.go +++ b/builtin_typedarrays.go @@ -1094,11 +1094,7 @@ func (r *Runtime) typedArrayFrom(ctor, items *Object, mapFn, thisValue Value) *O } 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) - }) + values := r.iterableToList(items, usingIter) ta := r.typedArrayCreate(ctor, []Value{intToValue(int64(len(values)))}) if mapFc == nil { for idx, val := range values { @@ -1260,11 +1256,8 @@ func (r *Runtime) createArrayBufferProto(val *Object) objectImpl { 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, - }) + r.putSpeciesReturnThis(o) + return o } @@ -1375,11 +1368,7 @@ 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, - }) + r.putSpeciesReturnThis(o) return o } diff --git a/object.go b/object.go index 6990126b..049f456c 100644 --- a/object.go +++ b/object.go @@ -22,10 +22,12 @@ const ( classString = "String" classBoolean = "Boolean" classError = "Error" + classAggError = "AggregateError" classRegExp = "RegExp" classDate = "Date" classJSON = "JSON" classGlobal = "global" + classPromise = "Promise" classArrayIterator = "Array Iterator" classMapIterator = "Map Iterator" diff --git a/runtime.go b/runtime.go index 48dfd7e2..e087a7e2 100644 --- a/runtime.go +++ b/runtime.go @@ -58,6 +58,7 @@ type global struct { Date *Object Symbol *Object Proxy *Object + Promise *Object ArrayBuffer *Object DataView *Object @@ -78,6 +79,7 @@ type global struct { Set *Object Error *Object + AggregateError *Object TypeError *Object ReferenceError *Object SyntaxError *Object @@ -104,6 +106,7 @@ type global struct { WeakMapPrototype *Object MapPrototype *Object SetPrototype *Object + PromisePrototype *Object IteratorPrototype *Object ArrayIteratorPrototype *Object @@ -113,6 +116,7 @@ type global struct { RegExpStringIteratorPrototype *Object ErrorPrototype *Object + AggregateErrorPrototype *Object TypeErrorPrototype *Object SyntaxErrorPrototype *Object RangeErrorPrototype *Object @@ -177,6 +181,10 @@ type Runtime struct { vm *vm hash *maphash.Hash idSeq uint64 + + jobQueue []func() + + promiseRejectionTracker PromiseRejectionTracker } type StackFrame struct { @@ -387,6 +395,7 @@ func (r *Runtime) init() { r.initWeakMap() r.initMap() r.initSet() + r.initPromise() r.global.thrower = r.newNativeFunc(r.builtin_thrower, nil, "thrower", nil, 0) r.global.throwerProperty = &valueProperty{ @@ -776,14 +785,6 @@ func (r *Runtime) error_toString(call FunctionCall) Value { return sb.String() } -func (r *Runtime) builtin_Error(args []Value, proto *Object) *Object { - obj := r.newBaseObject(proto, classError) - if len(args) > 0 && args[0] != _undefined { - obj._putProp("message", args[0], true, false, true) - } - return obj.val -} - func (r *Runtime) builtin_new(construct *Object, args []Value) *Object { return r.toConstructor(construct)(args, nil) } @@ -2395,7 +2396,16 @@ func (r *Runtime) getHash() *maphash.Hash { // called when the top level function returns (i.e. control is passed outside the Runtime). func (r *Runtime) leave() { - // run jobs, etc... + for { + jobs := r.jobQueue + r.jobQueue = nil + if len(jobs) == 0 { + break + } + for _, job := range jobs { + job() + } + } } func nilSafe(v Value) Value { @@ -2505,6 +2515,38 @@ func (r *Runtime) setGlobal(name unistring.String, v Value, strict bool) { } } +func (r *Runtime) trackPromiseRejection(p *Promise, operation PromiseRejectionOperation) { + if r.promiseRejectionTracker != nil { + r.promiseRejectionTracker(p, operation) + } +} + +func (r *Runtime) callJobCallback(job *jobCallback, this Value, args ...Value) Value { + return job.callback(FunctionCall{This: this, Arguments: args}) +} + +func (r *Runtime) invoke(v Value, p unistring.String, args ...Value) Value { + o := v.ToObject(r) + return r.toCallable(o.self.getStr(p, nil))(FunctionCall{This: v, Arguments: args}) +} + +func (r *Runtime) iterableToList(items Value, method func(FunctionCall) Value) []Value { + iter := r.getIterator(items, method) + var values []Value + r.iterate(iter, func(item Value) { + values = append(values, item) + }) + return values +} + +func (r *Runtime) putSpeciesReturnThis(o objectImpl) { + o._putSym(SymSpecies, &valueProperty{ + getterFunc: r.newNativeFunc(r.returnThis, nil, "get [Symbol.species]", nil, 0), + accessor: true, + configurable: true, + }) +} + func strToArrayIdx(s unistring.String) uint32 { if s == "" { return math.MaxUint32 @@ -2716,3 +2758,10 @@ func strToIdx64(s unistring.String) int64 { } return -1 } + +func assertCallable(v Value) (func(FunctionCall) Value, bool) { + if obj, ok := v.(*Object); ok { + return obj.self.assertCallable() + } + return nil, false +} diff --git a/runtime_test.go b/runtime_test.go index b88ec055..a2c0b1f3 100644 --- a/runtime_test.go +++ b/runtime_test.go @@ -2275,6 +2275,46 @@ func TestStringToBytesConversion(t *testing.T) { } } +func TestPromiseAll(t *testing.T) { + const SCRIPT = ` +var p1 = new Promise(function() {}); +var p2 = new Promise(function() {}); +var p3 = new Promise(function() {}); +var callCount = 0; +var currentThis = p1; +var nextThis = p2; +var afterNextThis = p3; + +p1.then = p2.then = p3.then = function(a, b) { + assert.sameValue(typeof a, 'function', 'type of first argument'); + assert.sameValue( + a.length, + 1, + 'ES6 25.4.1.3.2: The length property of a promise resolve function is 1.' + ); + assert.sameValue(typeof b, 'function', 'type of second argument'); + assert.sameValue( + b.length, + 1, + 'ES6 25.4.1.3.1: The length property of a promise reject function is 1.' + ); + assert.sameValue(arguments.length, 2, '"then"" invoked with two arguments'); + assert.sameValue(this, currentThis, '"this" value'); + + currentThis = nextThis; + nextThis = afterNextThis; + afterNextThis = null; + + callCount += 1; +}; + +Promise.all([p1, p2, p3]); + +assert.sameValue(callCount, 3, '"then"" invoked once for every iterated value'); + ` + testScript1(TESTLIB+SCRIPT, _undefined, t) +} + /* func TestArrayConcatSparse(t *testing.T) { function foo(a,b,c) diff --git a/string.go b/string.go index 8df5e53f..85687edd 100644 --- a/string.go +++ b/string.go @@ -33,6 +33,7 @@ var ( stringEmpty valueString = asciiString("") stringError valueString = asciiString("Error") + stringAggregateError valueString = asciiString("AggregateError") stringTypeError valueString = asciiString("TypeError") stringReferenceError valueString = asciiString("ReferenceError") stringSyntaxError valueString = asciiString("SyntaxError") diff --git a/tc39_test.go b/tc39_test.go index 6d6e030d..8acecef5 100644 --- a/tc39_test.go +++ b/tc39_test.go @@ -77,13 +77,24 @@ var ( // 167e596a649ede35df11d03cb3c093941c9cf396 "test/built-ins/TypedArrayConstructors/internals/Set/detached-buffer.js": true, - // 59a1a016b7cf5cf43f66b274c7d1db4ec6066935 - "test/language/expressions/function/name.js": true, - "test/built-ins/Proxy/revocable/revocation-function-name.js": true, - - "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 + // Anonymous function name property (now always present) + "test/language/expressions/function/name.js": true, + "test/built-ins/Proxy/revocable/revocation-function-name.js": true, + "test/built-ins/Promise/all/resolve-element-function-name.js": true, + "test/built-ins/Promise/allSettled/resolve-element-function-name.js": true, + "test/built-ins/Promise/allSettled/reject-element-function-name.js": true, + "test/built-ins/Promise/resolve-function-name.js": true, + "test/built-ins/Promise/reject-function-name.js": true, + + // obsolete tests (to remove) + "test/built-ins/Promise/race/invoke-resolve-get-error-close.js": true, + "test/built-ins/Promise/allSettled/invoke-resolve-get-error-close.js": true, + "test/built-ins/Promise/all/invoke-resolve-get-error-close.js": true, + + // timezone + "test/built-ins/Date/prototype/toISOString/15.9.5.43-0-8.js": true, + "test/built-ins/Date/prototype/toISOString/15.9.5.43-0-9.js": true, + "test/built-ins/Date/prototype/toISOString/15.9.5.43-0-10.js": true, // SharedArrayBuffer "test/built-ins/ArrayBuffer/prototype/slice/this-is-sharedarraybuffer.js": true, @@ -222,6 +233,14 @@ var ( "test/language/expressions/arrow-function/lexical-super-property-from-within-constructor.js": true, "test/language/expressions/arrow-function/lexical-super-property.js": true, "test/language/expressions/arrow-function/lexical-supercall-from-immediately-invoked-arrow.js": true, + "test/language/statements/class/subclass/builtin-objects/Promise/super-must-be-called.js": true, + "test/language/statements/class/subclass/builtin-objects/Promise/regular-subclassing.js": true, + "test/built-ins/Promise/prototype/finally/subclass-species-constructor-resolve-count.js": true, + "test/built-ins/Promise/prototype/finally/subclass-species-constructor-reject-count.js": true, + "test/built-ins/Promise/prototype/finally/subclass-resolve-count.js": true, + "test/built-ins/Promise/prototype/finally/species-symbol.js": true, + "test/built-ins/Promise/prototype/finally/subclass-reject-count.js": true, + "test/built-ins/Promise/prototype/finally/species-constructor.js": true, // restricted unicode regexp syntax "test/built-ins/RegExp/unicode_restricted_quantifiable_assertion.js": true, @@ -343,6 +362,7 @@ var ( "23", "24", "25.1", + "25.4", "26", "B.2.1", "B.2.2", @@ -410,6 +430,14 @@ var ( "sec-literals-numeric-literals", "sec-literals-string-literals", "sec-additional-syntax-numeric-literals", + "sec-promise", + "sec-promise-constructor", + "sec-promise-executor", + "sec-promise-reject-functions", + "sec-promise-resolve-functions", + "sec-performpromiseall", + "sec-performpromiseallsettled", + "sec-properties-of-the-promise-prototype-object", } ) @@ -522,8 +550,21 @@ func (ctx *tc39TestCtx) runTC39Test(name, src string, meta *tc39Meta, t testing. _262.Set("createRealm", ctx.throwIgnorableTestError) vm.Set("$262", _262) vm.Set("IgnorableTestError", ignorableTestError) - vm.Set("print", t.Log) vm.RunProgram(sabStub) + var out []string + async := meta.hasFlag("async") + if async { + err := ctx.runFile(ctx.base, path.Join("harness", "doneprintHandle.js"), vm) + if err != nil { + t.Fatal(err) + } + vm.Set("print", func(msg string) { + out = append(out, msg) + }) + } else { + vm.Set("print", t.Log) + } + err, early := ctx.runTC39Script(name, src, meta.Includes, vm) if err != nil { @@ -582,6 +623,22 @@ func (ctx *tc39TestCtx) runTC39Test(name, src string, meta *tc39Meta, t testing. if l := len(vm.vm.iterStack); l > 0 { t.Fatalf("iter stack is not empty: %d", l) } + if async { + complete := false + for _, line := range out { + if strings.HasPrefix(line, "Test262:AsyncTestFailure:") { + t.Fatal(line) + } else if line == "Test262:AsyncTestComplete" { + complete = true + } + } + if !complete { + for _, line := range out { + t.Log(line) + } + t.Fatal("Test262:AsyncTestComplete was not printed") + } + } } func (ctx *tc39TestCtx) runTC39File(name string, t testing.TB) { @@ -595,9 +652,6 @@ func (ctx *tc39TestCtx) runTC39File(name string, t testing.TB) { t.Errorf("Could not parse %s: %v", name, err) return } - if meta.hasFlag("async") { - t.Skip("async") - } if meta.Es5id == "" { skip := true //t.Logf("%s: Not ES5, skipped", name)