Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
210 changes: 159 additions & 51 deletions func.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,8 @@ type generatorObject struct {
gen generator
delegated *iteratorRecord
state generatorState
returning bool
retVal Value
}

func (f *nativeFuncObject) source() String {
Expand Down Expand Up @@ -752,6 +754,7 @@ type generator struct {
vm *vm

tryStackLen, iterStackLen, refStackLen uint32
sentinelCallStackLen int
}

func (g *generator) storeLengths() {
Expand Down Expand Up @@ -796,6 +799,7 @@ func (g *generator) enterNext() {
g.vm.pushCtx()
g.vm.pushTryFrame(tryPanicMarker, -1)
g.vm.callStack = append(g.vm.callStack, context{pc: -2}) // extra frame so that vm.run() halts after ret
g.sentinelCallStackLen = len(g.vm.callStack)
g.storeLengths()
g.vm.resume(&g.ctx)
}
Expand Down Expand Up @@ -930,73 +934,108 @@ func (g *generatorObject) next(v Value) Value {
v = nil
}
g.state = genStateExecuting
if g.returning {
return g.resumeReturn(v, true)
}
return g.step(g.gen.next(v))
}

func (g *generatorObject) throw(v Value) Value {
g.validate()
if g.state == genStateSuspendedStart {
g.state = genStateCompleted
}
if g.state == genStateCompleted {
panic(v)
}
if d := g.delegated; d != nil {
res, done := g.tryCallDelegated(func() (Value, bool) {
method := toMethod(g.delegated.iterator.self.getStr("throw", nil))
if method != nil {
return g.callDelegated(method, v)
}
g.delegated = nil
d.returnIter()
panic(g.val.runtime.NewTypeError("The iterator does not provide a 'throw' method"))
})
if !done {
return res
}
if g.state != genStateSuspendedYieldRes {
res = nil
}
g.state = genStateExecuting
return g.step(g.gen.next(res))
func (g *generatorObject) captureReturnValue(retVal Value) {
if retVal != _undefined || g.retVal == nil || g.retVal == _undefined {
g.retVal = retVal
}
g.state = genStateExecuting
return g.step(g.gen.nextThrow(v))
}

func (g *generatorObject) _return(v Value) Value {
g.validate()
if g.state == genStateSuspendedStart {
g.state = genStateCompleted
func (g *generator) popSentinelCallFrame() {
if g.sentinelCallStackLen <= 0 {
return
}

if g.state == genStateCompleted {
return g.val.runtime.createIterResultObject(v, true)
vm := g.vm
if len(vm.callStack) < g.sentinelCallStackLen {
return
}

if d := g.delegated; d != nil {
res, done := g.tryCallDelegated(func() (Value, bool) {
method := toMethod(g.delegated.iterator.self.getStr("return", nil))
if method != nil {
return g.callDelegated(method, v)
idx := g.sentinelCallStackLen - 1
if vm.callStack[idx].pc != -2 {
for i := len(vm.callStack) - 1; i >= 0; i-- {
if vm.callStack[i].pc == -2 {
idx = i
break
}
g.delegated = nil
return v, true
})
if !done {
return res
} else {
v = res
}
}

g.state = genStateExecuting
for i := idx; i < len(vm.callStack); i++ {
vm.callStack[i] = context{}
}
vm.callStack = vm.callStack[:idx]
}

func (g *generatorObject) completeReturnYield(callerSp int) Value {
vm := g.gen.vm
marker := vm.pop()
ym := marker.(*yieldMarker)
g.gen.ctx = execCtx{}
vm.pc = -vm.pc + 1
var res Value
if marker != yieldEmpty {
res = vm.pop()
}
vm.suspend(&g.gen.ctx, g.gen.tryStackLen, g.gen.iterStackLen, g.gen.refStackLen)
vm.sp = callerSp
g.gen.popSentinelCallFrame()
vm.popTryFrame()
vm.popCtx()
switch ym.resultType {
case resultYield:
g.state = genStateSuspendedYield
return g.val.runtime.createIterResultObject(res, false)
case resultYieldDelegate:
g.state = genStateSuspendedYield
return g.delegate(res)
case resultYieldRes:
g.state = genStateSuspendedYieldRes
return g.val.runtime.createIterResultObject(res, false)
case resultYieldDelegateRes:
g.state = genStateSuspendedYieldRes
return g.delegate(res)
default:
panic(g.val.runtime.NewTypeError("Runtime bug: unexpected result type: %v", ym.resultType))
}
}

func (g *generatorObject) resumeReturn(v Value, continueCurrent bool) Value {
g.gen.enterNext()
if v != nil {
g.gen.vm.push(v)
}

vm := g.gen.vm
// Save the caller's sp now, before any code runs that might corrupt vm.sb.
// After enterNext() -> resume(), vm.sb = caller_sp + 1, so caller_sp = vm.sb - 1.
// We need this because running finally blocks can call functions that modify vm.sb,
// and by the time we reach the completion code, vm.sb might be invalid.
callerSp := vm.sb - 1
var ex *Exception
for len(vm.tryStack) > 0 {
if continueCurrent {
for {
ex1 := vm.runTryInner()
if ex1 != nil {
ex = ex1
break
}
if vm.halted() {
if _, ok := vm.peek().(*yieldMarker); ok {
return g.completeReturnYield(callerSp)
}
g.captureReturnValue(vm.pop())
break
}
}
}

for len(vm.tryStack) > int(g.gen.tryStackLen) {
tf := &vm.tryStack[len(vm.tryStack)-1]
if int(tf.callStackLen) != len(vm.callStack) {
break
Expand Down Expand Up @@ -1025,6 +1064,10 @@ func (g *generatorObject) _return(v Value) Value {
break
}
if vm.halted() {
if _, ok := vm.peek().(*yieldMarker); ok {
return g.completeReturnYield(callerSp)
}
g.captureReturnValue(vm.pop())
break
}
}
Expand All @@ -1033,6 +1076,7 @@ func (g *generatorObject) _return(v Value) Value {
}
}

g.returning = false
g.state = genStateCompleted

vm.popTryFrame()
Expand All @@ -1045,11 +1089,75 @@ func (g *generatorObject) _return(v Value) Value {
panic(ex)
}

vm.callStack = vm.callStack[:len(vm.callStack)-1]
vm.sp = vm.sb - 1
g.gen.popSentinelCallFrame()
vm.sp = callerSp
vm.popCtx()

return g.val.runtime.createIterResultObject(v, true)
return g.val.runtime.createIterResultObject(g.retVal, true)
}

func (g *generatorObject) throw(v Value) Value {
g.validate()
if g.state == genStateSuspendedStart {
g.state = genStateCompleted
}
if g.state == genStateCompleted {
panic(v)
}
if d := g.delegated; d != nil {
res, done := g.tryCallDelegated(func() (Value, bool) {
method := toMethod(g.delegated.iterator.self.getStr("throw", nil))
if method != nil {
return g.callDelegated(method, v)
}
g.delegated = nil
d.returnIter()
panic(g.val.runtime.NewTypeError("The iterator does not provide a 'throw' method"))
})
if !done {
return res
}
if g.state != genStateSuspendedYieldRes {
res = nil
}
g.state = genStateExecuting
return g.step(g.gen.next(res))
}
g.state = genStateExecuting
return g.step(g.gen.nextThrow(v))
}

func (g *generatorObject) _return(v Value) Value {
g.validate()
if g.state == genStateSuspendedStart {
g.state = genStateCompleted
}

if g.state == genStateCompleted {
g.returning = false
return g.val.runtime.createIterResultObject(v, true)
}

if d := g.delegated; d != nil {
res, done := g.tryCallDelegated(func() (Value, bool) {
method := toMethod(g.delegated.iterator.self.getStr("return", nil))
if method != nil {
return g.callDelegated(method, v)
}
g.delegated = nil
return v, true
})
if !done {
return res
} else {
v = res
}
}

g.retVal = v
g.returning = true
g.state = genStateExecuting
return g.resumeReturn(nil, false)
}

func (f *baseJsFuncObject) generatorCall(vmCall func(*vm, int), nArgs int) Value {
Expand Down
44 changes: 44 additions & 0 deletions func_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,50 @@ func TestFuncPrototypeRedefine(t *testing.T) {
testScript(SCRIPT, valueTrue, t)
}

func TestGeneratorPopSentinelCallFrameRemovesSentinelTail(t *testing.T) {
r := New()
vm := r.vm

vm.callStack = []context{
{pc: 1},
{pc: 2},
{pc: -2},
{pc: 99},
}

g := generator{vm: vm, sentinelCallStackLen: 3}
g.popSentinelCallFrame()

if len(vm.callStack) != 2 {
t.Fatalf("expected callStack len 2, got %d", len(vm.callStack))
}
if vm.callStack[0].pc != 1 || vm.callStack[1].pc != 2 {
t.Fatalf("unexpected remaining callStack: %#v", vm.callStack)
}
}

func TestGeneratorPopSentinelCallFrameFallsBackToScan(t *testing.T) {
r := New()
vm := r.vm

vm.callStack = []context{
{pc: 1},
{pc: -2},
{pc: 100},
}

// Deliberately point past the sentinel to exercise fallback scan.
g := generator{vm: vm, sentinelCallStackLen: 3}
g.popSentinelCallFrame()

if len(vm.callStack) != 1 {
t.Fatalf("expected callStack len 1, got %d", len(vm.callStack))
}
if vm.callStack[0].pc != 1 {
t.Fatalf("unexpected remaining frame: %#v", vm.callStack[0])
}
}

func TestFuncExport(t *testing.T) {
vm := New()
typ := reflect.TypeOf((func(FunctionCall) Value)(nil))
Expand Down
25 changes: 25 additions & 0 deletions tc39_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ import (

const (
tc39BASE = "testdata/test262"

// localTestsBase contains test262-format tests maintained locally by Sobek
// These tests fill gaps in the upstream test262 suite and are candidates
// for contribution to tc39/test262
localTestsBase = "testdata"
)

var (
Expand Down Expand Up @@ -997,3 +1002,23 @@ func TestTC39(t *testing.T) {
}
}
}

// TestGeneratorPrototypeReturn runs test262-format tests for generator.return()
// that fill gaps in the upstream test262 suite. These tests verify that
// yield in finally blocks during generator.return() works per ECMAScript spec.
func TestGeneratorPrototypeReturn(t *testing.T) {
if testing.Short() {
t.Skip()
}

ctx := &tc39TestCtx{
base: localTestsBase,
}
ctx.init()

t.Run("GeneratorPrototype/return", func(t *testing.T) {
ctx.t = t
ctx.runTC39Tests("GeneratorPrototype/return")
ctx.flush()
})
}
Loading
Loading