Skip to content

Commit

Permalink
Async/await functionality (dop251#464)
Browse files Browse the repository at this point in the history
* Implemented async/await. See dop251#460.
  • Loading branch information
dop251 authored Dec 24, 2022
1 parent d4bf6fd commit 33bff8f
Show file tree
Hide file tree
Showing 24 changed files with 1,752 additions and 660 deletions.
11 changes: 11 additions & 0 deletions ast/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ type (
_pattern()
}

AwaitExpression struct {
Await file.Idx
Argument Expression
}

ArrayLiteral struct {
LeftBracket file.Idx
RightBracket file.Idx
Expand Down Expand Up @@ -138,6 +143,8 @@ type (
Source string

DeclarationList []*VariableDeclaration

Async bool
}

ClassLiteral struct {
Expand All @@ -164,6 +171,7 @@ type (
Body ConciseBody
Source string
DeclarationList []*VariableDeclaration
Async bool
}

Identifier struct {
Expand Down Expand Up @@ -292,6 +300,7 @@ type (

func (*ArrayLiteral) _expressionNode() {}
func (*AssignExpression) _expressionNode() {}
func (*AwaitExpression) _expressionNode() {}
func (*BadExpression) _expressionNode() {}
func (*BinaryExpression) _expressionNode() {}
func (*BooleanLiteral) _expressionNode() {}
Expand Down Expand Up @@ -624,6 +633,7 @@ type Program struct {

func (self *ArrayLiteral) Idx0() file.Idx { return self.LeftBracket }
func (self *ArrayPattern) Idx0() file.Idx { return self.LeftBracket }
func (self *AwaitExpression) Idx0() file.Idx { return self.Await }
func (self *ObjectPattern) Idx0() file.Idx { return self.LeftBrace }
func (self *AssignExpression) Idx0() file.Idx { return self.Left.Idx0() }
func (self *BadExpression) Idx0() file.Idx { return self.From }
Expand Down Expand Up @@ -698,6 +708,7 @@ func (self *ForIntoExpression) Idx0() file.Idx { return self.Expression.Idx0() }
func (self *ArrayLiteral) Idx1() file.Idx { return self.RightBracket + 1 }
func (self *ArrayPattern) Idx1() file.Idx { return self.RightBracket + 1 }
func (self *AssignExpression) Idx1() file.Idx { return self.Right.Idx1() }
func (self *AwaitExpression) Idx1() file.Idx { return self.Argument.Idx1() }
func (self *BadExpression) Idx1() file.Idx { return self.To }
func (self *BinaryExpression) Idx1() file.Idx { return self.Right.Idx1() }
func (self *BooleanLiteral) Idx1() file.Idx { return file.Idx(int(self.Idx) + len(self.Literal)) }
Expand Down
77 changes: 48 additions & 29 deletions builtin_function.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package goja

import (
"fmt"
"math"
)

func (r *Runtime) builtin_Function(args []Value, proto *Object) *Object {
func (r *Runtime) functionCtor(args []Value, proto *Object, async bool) *Object {
var sb valueStringBuilder
sb.WriteString(asciiString("(function anonymous("))
if async {
sb.WriteString(asciiString("(async function anonymous("))
} else {
sb.WriteString(asciiString("(function anonymous("))
}
if len(args) > 1 {
ar := args[:len(args)-1]
for i, arg := range ar {
Expand All @@ -28,39 +31,28 @@ func (r *Runtime) builtin_Function(args []Value, proto *Object) *Object {
return ret
}

func nativeFuncString(f *nativeFuncObject) Value {
return newStringValue(fmt.Sprintf("function %s() { [native code] }", nilSafe(f.getStr("name", nil)).toString()))
func (r *Runtime) builtin_Function(args []Value, proto *Object) *Object {
return r.functionCtor(args, proto, false)
}

func (r *Runtime) builtin_asyncFunction(args []Value, proto *Object) *Object {
return r.functionCtor(args, proto, true)
}

func (r *Runtime) functionproto_toString(call FunctionCall) Value {
obj := r.toObject(call.This)
repeat:
if lazy, ok := obj.self.(*lazyObject); ok {
obj.self = lazy.create(obj)
}
switch f := obj.self.(type) {
case *funcObject:
return newStringValue(f.src)
case *classFuncObject:
return newStringValue(f.src)
case *methodFuncObject:
return newStringValue(f.src)
case *arrowFuncObject:
return newStringValue(f.src)
case *nativeFuncObject:
return nativeFuncString(f)
case *boundFuncObject:
return nativeFuncString(&f.nativeFuncObject)
case *wrappedFuncObject:
return nativeFuncString(&f.nativeFuncObject)
case *lazyObject:
obj.self = f.create(obj)
goto repeat
case funcObjectImpl:
return f.source()
case *proxyObject:
repeat2:
switch c := f.target.self.(type) {
case *classFuncObject, *methodFuncObject, *funcObject, *arrowFuncObject, *nativeFuncObject, *boundFuncObject:
if lazy, ok := f.target.self.(*lazyObject); ok {
f.target.self = lazy.create(f.target)
}
if _, ok := f.target.self.(funcObjectImpl); ok {
return asciiString("function () { [native code] }")
case *lazyObject:
f.target.self = c.create(obj)
goto repeat2
}
}
panic(r.NewTypeError("Function.prototype.toString requires that 'this' be a Function"))
Expand Down Expand Up @@ -219,3 +211,30 @@ func (r *Runtime) initFunction() {
r.global.Function = r.newNativeFuncConstruct(r.builtin_Function, "Function", r.global.FunctionPrototype, 1)
r.addToGlobal("Function", r.global.Function)
}

func (r *Runtime) createAsyncFunctionProto(val *Object) objectImpl {
o := &baseObject{
class: classObject,
val: val,
extensible: true,
prototype: r.global.FunctionPrototype,
}
o.init()

o._putProp("constructor", r.global.AsyncFunction, true, false, true)

o._putSym(SymToStringTag, valueProp(asciiString(classAsyncFunction), false, false, true))

return o
}

func (r *Runtime) createAsyncFunction(val *Object) objectImpl {
o := r.newNativeFuncConstructObj(val, r.builtin_asyncFunction, "AsyncFunction", r.global.AsyncFunctionPrototype, 1)

return o
}

func (r *Runtime) initAsyncFunction() {
r.global.AsyncFunctionPrototype = r.newLazyObject(r.createAsyncFunctionProto)
r.global.AsyncFunction = r.newLazyObject(r.createAsyncFunction)
}
73 changes: 43 additions & 30 deletions builtin_promise.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,10 @@ type promiseCapability struct {
}

type promiseReaction struct {
capability *promiseCapability
typ promiseReactionType
handler *jobCallback
capability *promiseCapability
typ promiseReactionType
handler *jobCallback
asyncRunner *asyncRunner
}

var typePromise = reflect.TypeOf((*Promise)(nil))
Expand Down Expand Up @@ -147,6 +148,24 @@ func (p *Promise) export(*objectExportCtx) interface{} {
return p
}

func (p *Promise) addReactions(fulfillReaction *promiseReaction, rejectReaction *promiseReaction) {
r := p.val.runtime
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
}

func (r *Runtime) newPromiseResolveThenableJob(p *Promise, thenable Value, then *jobCallback) func() {
return func() {
resolve, reject := p.createResolvingFunctions()
Expand Down Expand Up @@ -297,20 +316,7 @@ func (r *Runtime) performPromiseThen(p *Promise, onFulfilled, onRejected Value,
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
p.addReactions(fulfillReaction, rejectReaction)
if resultCapability == nil {
return _undefined
}
Expand Down Expand Up @@ -577,19 +583,19 @@ func (r *Runtime) wrapPromiseReaction(fObj *Object) func(interface{}) {
// 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)
// })
// }()
// }
// 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()
Expand All @@ -606,3 +612,10 @@ func (r *Runtime) NewPromise() (promise *Promise, resolve func(result interface{
func (r *Runtime) SetPromiseRejectionTracker(tracker PromiseRejectionTracker) {
r.promiseRejectionTracker = tracker
}

// SetAsyncContextTracker registers a handler that allows to track async execution contexts. See AsyncContextTracker
// documentation for more details. Setting it to nil disables the functionality.
// This method (as Runtime in general) is not goroutine-safe.
func (r *Runtime) SetAsyncContextTracker(tracker AsyncContextTracker) {
r.asyncContextTracker = tracker
}
33 changes: 29 additions & 4 deletions compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -420,10 +420,16 @@ func (p *Program) _dumpCode(indent string, logger func(format string, args ...in
switch f := ins.(type) {
case *newFunc:
prg = f.prg
case *newAsyncFunc:
prg = f.prg
case *newArrowFunc:
prg = f.prg
case *newAsyncArrowFunc:
prg = f.prg
case *newMethod:
prg = f.prg
case *newAsyncMethod:
prg = f.prg
case *newDerivedClass:
if f.initFields != nil {
dumpInitFields(f.initFields)
Expand Down Expand Up @@ -982,8 +988,6 @@ func (c *compiler) compile(in *ast.Program, strict, inGlobal bool, evalVm *vm) {
c.popScope()
}

c.p.code = append(c.p.code, halt)

scope.finaliseVarAlloc(0)
}

Expand Down Expand Up @@ -1024,8 +1028,26 @@ func (c *compiler) createFunctionBindings(funcs []*ast.FunctionDeclaration) {
s := c.scope
if s.outer != nil {
unique := !s.isFunction() && !s.variable && s.strict
for _, decl := range funcs {
s.bindNameLexical(decl.Function.Name.Name, unique, int(decl.Function.Name.Idx1())-1)
if !unique {
hasNonStandard := false
for _, decl := range funcs {
if !decl.Function.Async {
s.bindNameLexical(decl.Function.Name.Name, false, int(decl.Function.Name.Idx1())-1)
} else {
hasNonStandard = true
}
}
if hasNonStandard {
for _, decl := range funcs {
if decl.Function.Async {
s.bindNameLexical(decl.Function.Name.Name, true, int(decl.Function.Name.Idx1())-1)
}
}
}
} else {
for _, decl := range funcs {
s.bindNameLexical(decl.Function.Name.Name, true, int(decl.Function.Name.Idx1())-1)
}
}
} else {
for _, decl := range funcs {
Expand Down Expand Up @@ -1222,6 +1244,9 @@ func (c *compiler) compileFunction(v *ast.FunctionDeclaration) {
}

func (c *compiler) compileStandaloneFunctionDecl(v *ast.FunctionDeclaration) {
if v.Function.Async {
c.throwSyntaxError(int(v.Idx0())-1, "Async functions can only be declared at top level or inside a block.")
}
if c.scope.strict {
c.throwSyntaxError(int(v.Idx0())-1, "In strict mode code, functions can only be declared at top level or inside a block.")
}
Expand Down
Loading

0 comments on commit 33bff8f

Please sign in to comment.