diff --git a/compiler_expr.go b/compiler_expr.go index 76f364a..ecd627e 100644 --- a/compiler_expr.go +++ b/compiler_expr.go @@ -14,7 +14,7 @@ type compiledExpr interface { emitSetter(valueExpr compiledExpr, putOnStack bool) emitRef() emitUnary(prepare, body func(), postfix, putOnStack bool) - deleteExpr() compiledExpr + emitDelete(putOnStack bool) constant() bool addSrcMap() } @@ -79,32 +79,6 @@ type compiledArrayAssignmentPattern struct { expr *ast.ArrayPattern } -type deleteGlobalExpr struct { - baseCompiledExpr - name unistring.String -} - -type deleteVarExpr struct { - baseCompiledExpr - name unistring.String -} - -type deletePropExpr struct { - baseCompiledExpr - left compiledExpr - name unistring.String -} - -type deleteElemExpr struct { - baseCompiledExpr - left, member compiledExpr -} - -type constantExpr struct { - baseCompiledExpr - val Value -} - type baseCompiledExpr struct { c *compiler offset int @@ -217,11 +191,6 @@ type compiledEnumGetExpr struct { baseCompiledExpr } -type defaultDeleteExpr struct { - baseCompiledExpr - expr compiledExpr -} - type compiledSpreadCallArgument struct { baseCompiledExpr expr compiledExpr @@ -240,13 +209,6 @@ type compiledDynamicImport struct { baseCompiledExpr } -func (e *defaultDeleteExpr) emitGetter(putOnStack bool) { - e.expr.emitGetter(false) - if putOnStack { - e.c.emitLiteralValue(valueTrue) - } -} - func (c *compiler) compileExpression(v ast.Expression) compiledExpr { // log.Printf("compileExpression: %T", v) switch v := v.(type) { @@ -361,12 +323,11 @@ func (e *baseCompiledExpr) emitRef() { e.c.assert(false, e.offset, "Cannot emit reference for this type of expression") } -func (e *baseCompiledExpr) deleteExpr() compiledExpr { - r := &constantExpr{ - val: valueTrue, +func (e *baseCompiledExpr) emitDelete(putOnStack bool) { + if putOnStack { + e.addSrcMap() + e.c.emitLiteralValue(valueTrue) } - r.init(e.c, file.Idx(e.offset+1)) - return r } func (e *baseCompiledExpr) emitUnary(func(), func(), bool, bool) { @@ -379,13 +340,6 @@ func (e *baseCompiledExpr) addSrcMap() { } } -func (e *constantExpr) emitGetter(putOnStack bool) { - if putOnStack { - e.addSrcMap() - e.c.emitLiteralValue(e.val) - } -} - func (e *compiledIdentifierExpr) emitGetter(putOnStack bool) { e.addSrcMap() if b, noDynamics := e.c.scope.lookupName(e.name); noDynamics { @@ -536,33 +490,34 @@ func (e *compiledIdentifierExpr) emitUnary(prepare, body func(), postfix, putOnS } } -func (e *compiledIdentifierExpr) deleteExpr() compiledExpr { +func (e *compiledIdentifierExpr) emitDelete(putOnStack bool) { if e.c.scope.strict { e.c.throwSyntaxError(e.offset, "Delete of an unqualified identifier in strict mode") panic("Unreachable") } if b, noDynamics := e.c.scope.lookupName(e.name); noDynamics { if b == nil { - r := &deleteGlobalExpr{ - name: e.name, + e.addSrcMap() + e.c.emit(deleteGlobal(e.name)) + if !putOnStack { + e.c.emit(pop) } - r.init(e.c, file.Idx(0)) - return r + return } } else { if b == nil { - r := &deleteVarExpr{ - name: e.name, + e.addSrcMap() + e.c.emit(deleteVar(e.name)) + if !putOnStack { + e.c.emit(pop) } - r.init(e.c, file.Idx(e.offset+1)) - return r + return } } - r := &compiledLiteral{ - val: valueFalse, + if putOnStack { + e.addSrcMap() + e.c.emitLiteralValue(valueFalse) } - r.init(e.c, file.Idx(e.offset+1)) - return r } type compiledSuperDotExpr struct { @@ -654,8 +609,13 @@ func (e *compiledSuperDotExpr) emitRef() { } } -func (e *compiledSuperDotExpr) deleteExpr() compiledExpr { - return e.c.superDeleteError(e.offset) +func (c *compiler) emitSuperReferenceError() { + c.emit(throwConst{referenceError("Unsupported reference to 'super'")}) +} + +func (e *compiledSuperDotExpr) emitDelete(_ bool) { + e.addSrcMap() + e.c.emitSuperReferenceError() } type compiledDotExpr struct { @@ -790,9 +750,8 @@ func (e *compiledPrivateDotExpr) emitUnary(prepare, body func(), postfix, putOnS } } -func (e *compiledPrivateDotExpr) deleteExpr() compiledExpr { +func (e *compiledPrivateDotExpr) emitDelete(_ bool) { e.c.throwSyntaxError(e.offset, "Private fields can not be deleted") - panic("unreachable") } func (e *compiledPrivateDotExpr) emitRef() { @@ -900,14 +859,9 @@ func (e *compiledSuperBracketExpr) emitRef() { } } -func (c *compiler) superDeleteError(offset int) compiledExpr { - return c.compileEmitterExpr(func() { - c.emit(throwConst{referenceError("Unsupported reference to 'super'")}) - }, file.Idx(offset+1)) -} - -func (e *compiledSuperBracketExpr) deleteExpr() compiledExpr { - return e.c.superDeleteError(e.offset) +func (e *compiledSuperBracketExpr) emitDelete(_ bool) { + e.addSrcMap() + e.c.emitSuperReferenceError() } func (c *compiler) checkConstantString(expr compiledExpr) (unistring.String, bool) { @@ -1043,13 +997,17 @@ func (e *compiledDotExpr) emitUnary(prepare, body func(), postfix, putOnStack bo } } -func (e *compiledDotExpr) deleteExpr() compiledExpr { - r := &deletePropExpr{ - left: e.left, - name: e.name, +func (e *compiledDotExpr) emitDelete(putOnStack bool) { + e.left.emitGetter(true) + e.addSrcMap() + if e.c.scope.strict { + e.c.emit(deletePropStrict(e.name)) + } else { + e.c.emit(deleteProp(e.name)) + } + if !putOnStack { + e.c.emit(pop) } - r.init(e.c, file.Idx(e.offset)+1) - return r } func (e *compiledBracketExpr) emitGetter(putOnStack bool) { @@ -1139,16 +1097,7 @@ func (e *compiledBracketExpr) emitUnary(prepare, body func(), postfix, putOnStac } } -func (e *compiledBracketExpr) deleteExpr() compiledExpr { - r := &deleteElemExpr{ - left: e.left, - member: e.member, - } - r.init(e.c, file.Idx(e.offset)+1) - return r -} - -func (e *deleteElemExpr) emitGetter(putOnStack bool) { +func (e *compiledBracketExpr) emitDelete(putOnStack bool) { e.left.emitGetter(true) e.member.emitGetter(true) e.addSrcMap() @@ -1162,42 +1111,6 @@ func (e *deleteElemExpr) emitGetter(putOnStack bool) { } } -func (e *deletePropExpr) emitGetter(putOnStack bool) { - e.left.emitGetter(true) - e.addSrcMap() - if e.c.scope.strict { - e.c.emit(deletePropStrict(e.name)) - } else { - e.c.emit(deleteProp(e.name)) - } - if !putOnStack { - e.c.emit(pop) - } -} - -func (e *deleteVarExpr) emitGetter(putOnStack bool) { - /*if e.c.scope.strict { - e.c.throwSyntaxError(e.offset, "Delete of an unqualified identifier in strict mode") - return - }*/ - e.c.emit(deleteVar(e.name)) - if !putOnStack { - e.c.emit(pop) - } -} - -func (e *deleteGlobalExpr) emitGetter(putOnStack bool) { - /*if e.c.scope.strict { - e.c.throwSyntaxError(e.offset, "Delete of an unqualified identifier in strict mode") - return - }*/ - - e.c.emit(deleteGlobal(e.name)) - if !putOnStack { - e.c.emit(pop) - } -} - func (e *compiledAssignExpr) emitGetter(putOnStack bool) { switch e.operator { case token.ASSIGN: @@ -2608,7 +2521,7 @@ func (e *compiledUnaryExpr) emitGetter(putOnStack bool) { e.c.emit(typeof) goto end case token.DELETE: - e.operand.deleteExpr().emitGetter(putOnStack) + e.operand.emitDelete(putOnStack) return case token.MINUS: e.c.emitExpr(e.operand, true) @@ -3221,12 +3134,12 @@ func (e *compiledCallExpr) emitGetter(putOnStack bool) { } } -func (e *compiledCallExpr) deleteExpr() compiledExpr { - r := &defaultDeleteExpr{ - expr: e, +func (e *compiledCallExpr) emitDelete(putOnStack bool) { + e.emitGetter(false) + if putOnStack { + e.addSrcMap() + e.c.emitLiteralValue(valueTrue) } - r.init(e.c, file.Idx(e.offset+1)) - return r } func (c *compiler) compileSpreadCallArgument(spread *ast.SpreadElement) compiledExpr { @@ -3634,6 +3547,28 @@ func (c *compiler) endOptChain() { c.block = c.block.outer } +func (c *compiler) endOptChainDelete() { + lbl := len(c.p.code) + for _, item := range c.block.breaks { + c.p.code[item] = joptdel(lbl - item) + } + for _, item := range c.block.conts { + c.p.code[item] = joptdelc(lbl - item) + } + c.block = c.block.outer +} + +func (c *compiler) endOptChainDeleteP() { + lbl := len(c.p.code) + for _, item := range c.block.breaks { + c.p.code[item] = joptdelP(lbl - item) + } + for _, item := range c.block.conts { + c.p.code[item] = joptdelcP(lbl - item) + } + c.block = c.block.outer +} + func (e *compiledOptionalChain) emitGetter(putOnStack bool) { e.c.startOptChain() e.expr.emitGetter(true) @@ -3643,6 +3578,16 @@ func (e *compiledOptionalChain) emitGetter(putOnStack bool) { } } +func (e *compiledOptionalChain) emitDelete(putOnStack bool) { + e.c.startOptChain() + e.expr.emitDelete(putOnStack) + if putOnStack { + e.c.endOptChainDelete() + } else { + e.c.endOptChainDeleteP() + } +} + func (e *compiledOptional) emitGetter(putOnStack bool) { e.expr.emitGetter(putOnStack) if putOnStack { diff --git a/compiler_test.go b/compiler_test.go index 383847e..0cc9a19 100644 --- a/compiler_test.go +++ b/compiler_test.go @@ -4841,6 +4841,49 @@ func TestOptChainCallee(t *testing.T) { testScriptWithTestLib(SCRIPT, _undefined, t) } +func TestOptChainDelete(t *testing.T) { + const SCRIPT = ` + { + const o = { + a: 1, + } + const a = { + b: { + c: 123, + d: 1, + }, + f: () => o, + }; + assert(delete a.b?.c); + assert(!('c' in a.b)); + assert(delete a.z?.c); + assert(delete a.z?.().c); + assert(delete a.f?.().a); + assert(!('a' in o)); + } + + { + const o = { + a: 1, + } + const a = { + b: { + c: 123, + d: 1, + }, + f: () => o, + }; + delete a.b?.c; + assert(!('c' in a.b)); + delete a.z?.c; + delete a.z?.().c; + delete a.f?.().a; + assert(!('a' in o)); + } +` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + func TestObjectLiteralSuper(t *testing.T) { const SCRIPT = ` const proto = { diff --git a/func.go b/func.go index 2a044a0..53b288e 100644 --- a/func.go +++ b/func.go @@ -748,8 +748,9 @@ func (ar *asyncRunner) start(nArgs int) { } type generator struct { - ctx execCtx - vm *vm + ctx execCtx + vm *vm + returning Value tryStackLen, iterStackLen, refStackLen uint32 } @@ -765,29 +766,96 @@ func (g *generator) enter() { g.storeLengths() } -func (g *generator) step() (res Value, resultType resultType, ex *Exception) { - for { - ex = g.vm.runTryInner() +func (g *generator) enterNextFinallyFrame() (canContinue bool) { + vm := g.vm + callStackLen := len(vm.callStack) + + for len(vm.tryStack) > 0 { + tf := &vm.tryStack[len(vm.tryStack)-1] + if int(tf.callStackLen) != callStackLen { // have we breached the function boundary? + break + } + ex := vm.restoreStacks(tf.iterLen, tf.refLen) if ex != nil { - return + vm.throw(ex) + return true + } + if tf.finallyPos >= 0 { + vm.sp = int(tf.sp) + vm.stash = tf.stash + vm.privEnv = tf.privEnv + vm.pc = int(tf.finallyPos) + tf.catchPos = tryPanicMarker + tf.finallyPos = -1 + tf.finallyRet = -2 // -1 would cause it to continue after leaveFinally + return true + } + vm.popTryFrame() + } + return +} + +func (g *generator) step() (res Value, resultType resultType, ex *Exception) { + vm := g.vm + if g.returning == nil { + for { + ex = vm.runTryInner() + if ex != nil { + return + } + if vm.halted() { + break + } } - if g.vm.halted() { + res = vm.pop() + } else { + for { + ex = vm.runTryInner() + if ex != nil { + if vm.prg != nil || vm.pc != -2 { + // The exception was thrown in the outermost finally block, it never got to leaveFinally + // which does popTryFrame() + vm.popTryFrame() + } + return + } + + if vm.prg != nil && vm.pc == -2 { // normal exit from finally + if g.enterNextFinallyFrame() { + continue + } + + // All finally blocks have exited without result + res, g.returning = g.returning, nil + ex = vm.restoreStacks(g.iterStackLen, g.refStackLen) + if ex != nil { + return + } + vm.sp = vm.sb - 1 + vm.callStack = vm.callStack[:len(vm.callStack)-1] + + return + } + res = vm.pop() + if vm.prg == nil { // It was a return, not a yield + return + } break } } - res = g.vm.pop() + if ym, ok := res.(*yieldMarker); ok { resultType = ym.resultType g.ctx = execCtx{} - g.vm.pc = -g.vm.pc + 1 + vm.pc = -vm.pc + 1 if res != yieldEmpty { - res = g.vm.pop() + res = vm.pop() } else { res = nil } - g.vm.suspend(&g.ctx, g.tryStackLen, g.iterStackLen, g.refStackLen) - g.vm.sp = g.vm.sb - 1 - g.vm.callStack = g.vm.callStack[:len(g.vm.callStack)-1] // remove the frame with pc == -2, as ret would do + vm.suspend(&g.ctx, g.tryStackLen, g.iterStackLen, g.refStackLen) + vm.sp = vm.sb - 1 + vm.callStack = vm.callStack[:len(vm.callStack)-1] // remove the frame with pc == -2, as ret would do } return } @@ -990,66 +1058,34 @@ func (g *generatorObject) _return(v Value) Value { } } + g.gen.returning = v g.state = genStateExecuting - g.gen.enterNext() + canContinue := g.gen.enterNextFinallyFrame() + if !canContinue { + vm := g.gen.vm + g.state = genStateCompleted - vm := g.gen.vm - var ex *Exception - for len(vm.tryStack) > 0 { - tf := &vm.tryStack[len(vm.tryStack)-1] - if int(tf.callStackLen) != len(vm.callStack) { - break - } + vm.popTryFrame() - if tf.finallyPos >= 0 { - vm.sp = int(tf.sp) - vm.stash = tf.stash - vm.privEnv = tf.privEnv - ex1 := vm.restoreStacks(tf.iterLen, tf.refLen) - if ex1 != nil { - ex = ex1 - vm.popTryFrame() - continue - } + ex := vm.restoreStacks(g.gen.iterStackLen, g.gen.refStackLen) - vm.pc = int(tf.finallyPos) - tf.catchPos = tryPanicMarker - tf.finallyPos = -1 - tf.finallyRet = -2 // -1 would cause it to continue after leaveFinally - for { - ex1 := vm.runTryInner() - if ex1 != nil { - ex = ex1 - vm.popTryFrame() - break - } - if vm.halted() { - break - } - } - } else { - vm.popTryFrame() + if ex != nil { + panic(ex) } - } - - g.state = genStateCompleted - - vm.popTryFrame() - if ex == nil { - ex = vm.restoreStacks(g.gen.iterStackLen, g.gen.refStackLen) - } + vm.callStack = vm.callStack[:len(vm.callStack)-1] + vm.sp = vm.sb - 1 + vm.popCtx() - if ex != nil { - panic(ex) + return g.val.runtime.createIterResultObject(v, true) } - - vm.callStack = vm.callStack[:len(vm.callStack)-1] - vm.sp = vm.sb - 1 + res, done, ex := g.gen.step() + vm := g.gen.vm + vm.popTryFrame() vm.popCtx() - return g.val.runtime.createIterResultObject(v, true) + return g.step(res, done, ex) } func (f *baseJsFuncObject) generatorCall(vmCall func(*vm, int), nArgs int) Value { diff --git a/func_test.go b/func_test.go index 595e392..ef87b20 100644 --- a/func_test.go +++ b/func_test.go @@ -303,3 +303,604 @@ func TestAsyncContextTracker(t *testing.T) { } }) } + +func TestGeneratorReturnIterCleanup(t *testing.T) { + const SCRIPT = ` + let iterReturnCalled = false; + function* g() { + const iter = { + next() { + return { value: 43 }; + }, + return() { + iterReturnCalled = true; + return {}; + }, + [Symbol.iterator]() { + return this; + } + } + try { + for (const v of iter) { + yield v; + } + yield 'working'; + } finally { + yield 'cleanup'; + } + } + + const gen = g(); + const r1 = gen.next(); + assert.sameValue(r1.value, 43); assert(!r1.done); + assert(!iterReturnCalled); + + const r2 = gen.return('X'); + assert.sameValue(r2.value, 'cleanup'); assert(!r2.done); + assert(iterReturnCalled); + + const r3 = gen.next(); + assert.sameValue(r3.value, 'X'); assert(r3.done); + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestGeneratorReturnIterCleanupThrow(t *testing.T) { + const SCRIPT = ` + class TestError extends Error { + } + + let iterReturnCalled = false; + function* g() { + const iter = { + next() { + return { value: 43 }; + }, + return() { + iterReturnCalled = true; + throw new TestError('boo!'); + }, + [Symbol.iterator]() { + return this; + } + } + try { + for (const v of iter) { + yield v; + } + } finally { + yield 'cleanup'; + } + } + + const gen = g(); + const r1 = gen.next(); + assert.sameValue(r1.value, 43); assert(!r1.done); + assert(!iterReturnCalled); + + const r2 = gen.return('X'); + assert.sameValue(r2.value, 'cleanup'); assert(!r2.done); + assert(iterReturnCalled); + assert.throws(TestError, () => gen.next()); + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestGeneratorReturnIterCleanupThrow1(t *testing.T) { + const SCRIPT = ` + class TestError extends Error { + } + + let iterReturnCalled = false; + function* g() { + const iter = { + next() { + return { value: 43 }; + }, + return() { + iterReturnCalled = true; + throw new Error('boo!'); + }, + [Symbol.iterator]() { + return this; + } + } + try { + for (const v of iter) { + yield 1; + throw new TestError(); + } + } finally { + yield 2; + } + } + + const gen = g(); + assert.sameValue(gen.next().value, 1); + assert.sameValue(gen.next().value, 2); + assert.sameValue(gen.return('x').value, 'x'); + assert(iterReturnCalled); + + const gen1 = g(); + iterReturnCalled = false; + assert.sameValue(gen1.next().value, 1); + assert.sameValue(gen1.next().value, 2); + assert.throws(TestError, () => gen1.next()); + assert(iterReturnCalled); +` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestGeneratorReturnIterCleanupThrowNoCatch(t *testing.T) { + const SCRIPT = ` + class TestError extends Error { + } + + let iterReturnCalled = false; + function* g() { + const iter = { + next() { + return { value: 43 }; + }, + return() { + iterReturnCalled = true; + throw new TestError('boo!'); + }, + [Symbol.iterator]() { + return this; + } + } + for (const v of iter) { + yield v; + } + } + + const gen = g(); + const r1 = gen.next(); + assert.sameValue(r1.value, 43); assert(!r1.done); + assert(!iterReturnCalled); + + assert.throws(TestError, () => gen.return('X')); + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestGeneratorReturnIterCleanupThrowCaught(t *testing.T) { + const SCRIPT = ` + class TestError extends Error { + } + + let iterReturnCalled = false; + let caught; + function* g() { + const iter = { + next() { + return { value: 43 }; + }, + return() { + iterReturnCalled = true; + throw new TestError('boo!'); + }, + [Symbol.iterator]() { + return this; + } + } + try { + for (const v of iter) { + yield v; + } + } catch (e) { + caught = e; + } finally { + yield 'cleanup'; + } + } + + const gen = g(); + const r1 = gen.next(); + assert.sameValue(r1.value, 43); assert(!r1.done); + assert(!iterReturnCalled); + + const r2 = gen.return('X'); + assert.sameValue(r2.value, 'cleanup'); assert(!r2.done); + assert(iterReturnCalled); + gen.next(); + assert(caught instanceof TestError); + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestGeneratorReturnIterCleanupThrowNested(t *testing.T) { + const SCRIPT = ` + class TestError extends Error { + } + + let iterReturnCalled = false; + function* g() { + const iter = { + next() { + return { value: 43 }; + }, + return() { + iterReturnCalled = true; + throw new Error('boo!'); + }, + [Symbol.iterator]() { + return this; + } + } + try { + try { + for (const v of iter) { + yield v; + } + } finally { + yield 'cleanup inner'; + throw new TestError('test'); + } + } finally { + yield 'cleanup outer'; + } + throw new Error('must not get here'); + } + + const gen = g(); + const r1 = gen.next(); + assert.sameValue(r1.value, 43); assert(!r1.done); + assert(!iterReturnCalled); + + const r2 = gen.return('X'); + assert.sameValue(r2.value, 'cleanup inner'); assert(!r2.done); + assert(iterReturnCalled); + const r3 = gen.next(); + assert.sameValue(r3.value, 'cleanup outer'); assert(!r3.done); + + assert.throws(TestError, () => gen.next()); + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestGeneratorReturn(t *testing.T) { + const SCRIPT = ` + function* withCleanup() { + try { + yield 'working'; + } finally { + yield 'cleanup'; // Should suspend here + } + } + + const gen = withCleanup(); + const r1 = gen.next(); // {value: 'working', done: false} + assert.sameValue(r1.value, 'working'); + assert(!r1.done); + const r2 = gen.return('X'); // {value: 'cleanup', done: false} ← should suspend + assert.sameValue(r2.value, 'cleanup'); + assert(!r2.done); + const r3 = gen.next(); // {value: 'X', done: true} ← then complete + assert.sameValue(r3.value, 'X'); + assert(r3.done); + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +// The following 8 tests are copied from https://github.com/tc39/test262/pull/4939 +// If that PR gets merged and tc39 tests are updated to a version that includes it, they can be removed. + +func TestGeneratorReturn1(t *testing.T) { + const SCRIPT = ` + function* nestedCleanup() { + try { + try { + yield "work"; + } finally { + yield "inner-cleanup"; + } + } finally { + yield "outer-cleanup"; + } + } + + var gen = nestedCleanup(); + var result; + + result = gen.next(); + assert.sameValue(result.value, "work", "r1.value"); + assert.sameValue(result.done, false, "r1.done"); + + result = gen.return("cancelled"); + assert.sameValue( + result.value, + "inner-cleanup", + "r2.value (inner finally yield)", + ); + assert.sameValue(result.done, false, "r2.done (suspended at inner finally)"); + + result = gen.next(); + assert.sameValue( + result.value, + "outer-cleanup", + "r3.value (outer finally yield)", + ); + assert.sameValue(result.done, false, "r3.done (suspended at outer finally)"); + + result = gen.next(); + assert.sameValue(result.value, "cancelled", "r4.value (return value)"); + assert.sameValue(result.done, true, "r4.done (completed)"); + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestGeneratorReturn2(t *testing.T) { + const SCRIPT = ` + function* genWithOverride() { + try { + yield "work"; + } finally { + return "cleanup-override"; + } + } + + var gen = genWithOverride(); + gen.next(); + + var result = gen.return("cancelled"); + + assert.sameValue( + result.value, + "cleanup-override", + "Finally return overrides generator.return() value", + ); + assert.sameValue(result.done, true, "Generator is done"); + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestGeneratorReturn3(t *testing.T) { + const SCRIPT = ` + function* genWithSuspendingThrowingCleanup() { + try { + yield "work"; + } finally { + yield "cleanup"; + throw new Error("cleanup-failed-after-yield"); + } + } + + var gen = genWithSuspendingThrowingCleanup(); + var result; + + result = gen.next(); + assert.sameValue(result.value, "work", "r1.value"); + + result = gen.return("cancelled"); + assert.sameValue(result.value, "cleanup", "r2.value (yield in finally)"); + assert.sameValue(result.done, false, "r2.done (suspended at yield)"); + + var caught; + try { + gen.next(); + } catch (e) { + caught = e.message; + } + + assert.sameValue( + caught, + "cleanup-failed-after-yield", + "Exception after yield should propagate on resume", + ); + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestGeneratorReturn4(t *testing.T) { + const SCRIPT = ` + function* genWithThrowingCleanup() { + try { + yield "work"; + } finally { + throw new Error("cleanup-failed"); + } + } + + var gen = genWithThrowingCleanup(); + gen.next(); + + var caught; + try { + gen.return("cancelled"); + } catch (e) { + caught = e.message; + } + + assert.sameValue( + caught, + "cleanup-failed", + "Exception from finally should propagate", + ); + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestGeneratorReturn5(t *testing.T) { + const SCRIPT = ` + var cleanupInput; + + function* cleanupNeedsAck() { + try { + yield "work"; + } finally { + cleanupInput = yield "cleanup-1"; + yield "cleanup-2"; + } + } + + var gen = cleanupNeedsAck(); + var result; + + result = gen.next(); + assert.sameValue(result.value, "work", "r1.value"); + + result = gen.return("cancelled"); + assert.sameValue(result.value, "cleanup-1", "r2.value"); + assert.sameValue(result.done, false, "r2.done"); + + result = gen.next("ack"); + assert.sameValue(result.value, "cleanup-2", "r3.value"); + assert.sameValue(result.done, false, "r3.done"); + + result = gen.next(); + assert.sameValue(result.value, "cancelled", "r4.value"); + assert.sameValue(result.done, true, "r4.done"); + + assert.sameValue( + cleanupInput, + "ack", + "yield in finally received value from next()", + ); + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestGeneratorReturn6(t *testing.T) { + const SCRIPT = ` + var cleanupOrder = []; + + function* inner() { + try { + yield "inner-work"; + return "inner-done"; + } finally { + yield "inner-cleanup"; + cleanupOrder.push("inner"); + } + } + + function* outer() { + try { + var result = yield* inner(); + return result; + } finally { + yield "outer-cleanup"; + cleanupOrder.push("outer"); + } + } + + var gen = outer(); + var result; + + result = gen.next(); + assert.sameValue( + result.value, + "inner-work", + "First yield from inner generator", + ); + assert.sameValue(result.done, false, "First result done"); + + result = gen.return("cancelled"); + assert.sameValue( + result.value, + "inner-cleanup", + "Should yield from inner finally", + ); + assert.sameValue(result.done, false, "Should suspend at inner finally yield"); + + result = gen.next(); + assert.sameValue( + result.value, + "outer-cleanup", + "Should yield from outer finally", + ); + assert.sameValue(result.done, false, "Should suspend at outer finally yield"); + + result = gen.next(); + assert.sameValue(result.done, true, "Should be done after all finally blocks"); + assert.sameValue( + cleanupOrder.join(","), + "inner,outer", + "Cleanup order should be inner then outer", + ); + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestGeneratorReturn7(t *testing.T) { + const SCRIPT = ` + function* delegatedCleanup() { + yield "cleanup-1"; + yield "cleanup-2"; + } + + function* withYieldStarCleanup() { + try { + yield "work"; + } finally { + yield* delegatedCleanup(); + } + } + + var gen = withYieldStarCleanup(); + var result; + + result = gen.next(); + assert.sameValue(result.value, "work", "r1.value"); + + result = gen.return("cancelled"); + assert.sameValue(result.value, "cleanup-1", "r2.value (first delegated yield)"); + assert.sameValue(result.done, false, "r2.done"); + + result = gen.next(); + assert.sameValue( + result.value, + "cleanup-2", + "r3.value (second delegated yield)", + ); + assert.sameValue(result.done, false, "r3.done"); + + result = gen.next(); + assert.sameValue(result.value, "cancelled", "r4.value (return value)"); + assert.sameValue(result.done, true, "r4.done"); + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestGeneratorReturn8(t *testing.T) { + const SCRIPT = ` + var inFinally = 0; + var afterYield = 0; + + function* g() { + try { + yield "in-try"; + } finally { + inFinally += 1; + yield "in-finally"; + afterYield += 1; + } + } + + var iter = g(); + var result; + + result = iter.next(); + assert.sameValue(result.value, "in-try", "First result value"); + assert.sameValue(result.done, false, "First result done"); + assert.sameValue(inFinally, 0, "finally not yet entered"); + + result = iter.return(42); + assert.sameValue( + result.value, + "in-finally", + "Second result value (yield in finally)", + ); + assert.sameValue(result.done, false, "Second result done (suspended at yield)"); + assert.sameValue(inFinally, 1, "finally block entered"); + assert.sameValue(afterYield, 0, "code after yield not yet executed"); + + result = iter.next(); + assert.sameValue(result.value, 42, "Third result value (return value)"); + assert.sameValue(result.done, true, "Third result done (completed)"); + assert.sameValue(afterYield, 1, "code after yield executed"); + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} diff --git a/tc39_test.go b/tc39_test.go index 200a454..b66e4f7 100644 --- a/tc39_test.go +++ b/tc39_test.go @@ -203,9 +203,6 @@ var ( "test/built-ins/RegExp/nullable-quantifier.js": true, "test/built-ins/RegExp/lookahead-quantifier-match-groups.js": true, - // Fixed in https://github.com/grafana/sobek/pull/115 - "test/built-ins/GeneratorPrototype/return/try-finally-set-property-within-try.js": true, - // TypedArray internals "test/built-ins/TypedArrayConstructors/internals/Set/key-is-valid-index-reflect-set.js": true, "test/built-ins/TypedArrayConstructors/internals/Set/key-is-out-of-bounds-receiver-is-proto.js": true, diff --git a/vm.go b/vm.go index a9adf9f..fb550fa 100644 --- a/vm.go +++ b/vm.go @@ -4522,6 +4522,55 @@ func (j joptc) exec(vm *vm) { } } +type joptdel int32 + +func (j joptdel) exec(vm *vm) { + switch vm.stack[vm.sp-1].(type) { + case valueNull, valueUndefined: + vm.stack[vm.sp-1] = valueTrue + vm.pc += int(j) + default: + vm.pc++ + } +} + +type joptdelc int32 + +func (j joptdelc) exec(vm *vm) { + switch vm.stack[vm.sp-1].(type) { + case valueNull, valueUndefined, memberUnresolved: + vm.sp-- + vm.stack[vm.sp-1] = valueTrue + vm.pc += int(j) + default: + vm.pc++ + } +} + +type joptdelP int32 + +func (j joptdelP) exec(vm *vm) { + switch vm.stack[vm.sp-1].(type) { + case valueNull, valueUndefined: + vm.sp-- + vm.pc += int(j) + default: + vm.pc++ + } +} + +type joptdelcP int32 + +func (j joptdelcP) exec(vm *vm) { + switch vm.stack[vm.sp-1].(type) { + case valueNull, valueUndefined, memberUnresolved: + vm.sp -= 2 + vm.pc += int(j) + default: + vm.pc++ + } +} + type jcoalesc int32 func (j jcoalesc) exec(vm *vm) {