discard """
  matrix: "; --experimental:strictdefs"
  targets: "c cpp"
"""

var closureIterResult = newSeq[int]()

proc checkpoint(arg: int) =
  closureIterResult.add(arg)

type
  TestError = object of CatchableError
  AnotherError = object of CatchableError

proc testClosureIterAux(it: iterator(): int, exceptionExpected: bool, expectedResults: varargs[int]) =
  closureIterResult.setLen(0)

  var exceptionCaught = false

  var maxIterations = 10000
  try:
    for i in it():
      closureIterResult.add(i)
      dec maxIterations
      doAssert(maxIterations > 0, "Too many iterations in test. Infinite loop?")
  except TestError:
    exceptionCaught = true

  if closureIterResult != @expectedResults or exceptionCaught != exceptionExpected:
    if closureIterResult != @expectedResults:
      echo "Expected: ", @expectedResults
      echo "Actual:   ", closureIterResult
    if exceptionCaught != exceptionExpected:
      echo "Expected exception: ", exceptionExpected
      echo "Got exception:      ", exceptionCaught
    doAssert(false)

proc test(it: iterator(): int, expectedResults: varargs[int]) =
  testClosureIterAux(it, false, expectedResults)

proc testExc(it: iterator(): int, expectedResults: varargs[int]) =
  testClosureIterAux(it, true, expectedResults)

proc raiseTestError() =
  raise newException(TestError, "Test exception!")

block:
  iterator it(): int {.closure.} =
    var i = 5
    while i != 0:
      yield i
      if i == 3:
        yield 123
      dec i

  test(it, 5, 4, 3, 123, 2, 1)

block:
  iterator it(): int {.closure.} =
    yield 0
    try:
      checkpoint(1)
      raiseTestError()
    except TestError:
      checkpoint(2)
      yield 3
      checkpoint(4)
    finally:
      checkpoint(5)

    checkpoint(6)

  test(it, 0, 1, 2, 3, 4, 5, 6)

block:
  iterator it(): int {.closure.} =
    yield 0
    try:
      yield 1
      checkpoint(2)
    finally:
      checkpoint(3)
      yield 4
      checkpoint(5)
      yield 6

  test(it, 0, 1, 2, 3, 4, 5, 6)

block:
  iterator it(): int {.closure.} =
    yield 0
    try:
      yield 1
      raiseTestError()
      yield 2
    finally:
      checkpoint(3)
      yield 4
      checkpoint(5)
      yield 6

  testExc(it, 0, 1, 3, 4, 5, 6)

block:
  iterator it(): int {.closure.} =
    try:
      try:
        raiseTestError()
      except AnotherError:
        yield 123
      finally:
        checkpoint(3)
    finally:
      checkpoint(4)

  testExc(it, 3, 4)

block:
  iterator it(): int {.closure.} =
    try:
      yield 1
      raiseTestError()
    except AnotherError:
      checkpoint(123)
    finally:
      checkpoint(2)
    checkpoint(3)

  testExc(it, 1, 2)

block:
  iterator it(): int {.closure.} =
    try:
      yield 0
      try:
        yield 1
        try:
          yield 2
          raiseTestError()
        except AnotherError:
          yield 123
        finally:
          yield 3
      except AnotherError:
        yield 124
      finally:
        yield 4
      checkpoint(1234)
    except:
      yield 5
      checkpoint(6)
    finally:
      checkpoint(7)
      yield 8
    checkpoint(9)

  test(it, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9)

block:
  iterator it(): int {.closure.} =
    try:
      yield 0
      return 2
    finally:
      checkpoint(1)
    checkpoint(123)

  test(it, 0, 1)

block:
  iterator it(): int {.closure.} =
    try:
      try:
        yield 0
        raiseTestError()
      finally:
        checkpoint(1)
    except TestError:
      yield 2
      return
    finally:
      yield 3

    checkpoint(123)

  test(it, 0, 1, 2, 3)

block: # Wrong except
  iterator it(): int {.closure.} =
    try:
      try:
        yield 0
        raiseTestError()
      except ValueError:
        doAssert(false, "Unreachable")
      finally:
        checkpoint(1)
    except ValueError:
      yield 123
      return

    checkpoint(123)

  testExc(it, 0, 1)

block: # Nested except without finally
  iterator it(): int {.closure.} =
    try:
      try:
        yield 0
        raiseTestError()
      except ValueError:
        doAssert(false, "Unreachable")
    except ValueError:
      yield 123

    checkpoint(123)

  testExc(it, 0)

block: # Return in except with no finallies around
  iterator it(): int {.closure.} =
    try:
      try:
        yield 0
        raiseTestError()
      except ValueError:
        doAssert(false, "Unreachable")
      finally:
        checkpoint(1)
    except TestError:
      yield 2
      return

    checkpoint(123)

  test(it, 0, 1, 2)

block:
  iterator it(): int {.closure.} =
    try:
      try:
        yield 0
        raiseTestError()
      finally:
        return # Return in finally should stop exception propagation
    except AnotherError:
      yield 2
      return
    finally:
      yield 3
    checkpoint(123)

  test(it, 0, 3)

block: # Yield in yield
  iterator it(): int {.closure.} =
    template foo(): int =
      yield 1
      2

    for i in 0 .. 2:
      checkpoint(0)
      yield foo()

  test(it, 0, 1, 2, 0, 1, 2, 0, 1, 2)

block:
  iterator it(): int {.closure.} =
    let i = if true:
        yield 0
        1
      else:
        2
    yield i

  test(it, 0, 1)

block:
  iterator it(): int {.closure.} =
    var foo = 123
    let i = try:
        yield 0
        raiseTestError()
        1
      except TestError as e:
        assert(e.msg == "Test exception!")
        case foo
        of 1:
          yield 123
          2
        of 123:
          yield 5
          6
        else:
          7
    yield i

  test(it, 0, 5, 6)

block:
  iterator it(): int {.closure.} =
    proc voidFoo(i1, i2, i3: int) =
      checkpoint(i1)
      checkpoint(i2)
      checkpoint(i3)

    proc foo(i1, i2, i3: int): int =
      voidFoo(i1, i2, i3)
      i3

    proc bar(i1: int): int =
      checkpoint(i1)

    template tryexcept: int =
      try:
        yield 1
        raiseTestError()
        123
      except TestError:
        yield 2
        checkpoint(3)
        4

    let e1 = true

    template ifelse1: int =
      if e1:
        yield 10
        11
      else:
        12

    template ifelse2: int =
      if ifelse1() == 12:
        yield 20
        21
      else:
        yield 22
        23

    let i = foo(bar(0), tryexcept, ifelse2)
    discard foo(bar(0), tryexcept, ifelse2)
    voidFoo(bar(0), tryexcept, ifelse2)
    yield i

  test(it,

    # let i = foo(bar(0), tryexcept, ifelse2)
    0, # bar(0)
    1, 2, 3, # tryexcept
    10, # ifelse1
    22, # ifelse22
    0, 4, 23, # foo

    # discard foo(bar(0), tryexcept, ifelse2)
    0, # bar(0)
    1, 2, 3, # tryexcept
    10, # ifelse1
    22, # ifelse22
    0, 4, 23, # foo

    # voidFoo(bar(0), tryexcept, ifelse2)
    0, # bar(0)
    1, 2, 3, # tryexcept
    10, # ifelse1
    22, # ifelse22
    0, 4, 23, # foo

    23 # i
  )

block:
  iterator it(): int {.closure.} =
    checkpoint(0)
    for i in 0 .. 1:
      try:
        yield 1
        raiseTestError()
      except TestError as e:
        doAssert(e.msg == "Test exception!")
        yield 2
      except AnotherError:
        yield 123
      except:
        yield 1234
      finally:
        yield 3
        checkpoint(4)
        yield 5

  test(it, 0, 1, 2, 3, 4, 5, 1, 2, 3, 4, 5)

block:
  iterator it(): int {.closure.} =
    var i = 5
    template foo(): bool =
      yield i
      true

    while foo():
      dec i
      if i == 0:
        break

  test(it, 5, 4, 3, 2, 1)

block: # Short cirquits
  iterator it(): int {.closure.} =
    template trueYield: bool =
      yield 1
      true

    template falseYield: bool =
      yield 0
      false

    if trueYield or falseYield:
      discard falseYield and trueYield

    if falseYield and trueYield:
      checkpoint(123)

  test(it, 1, 0, 0)

block: #7969
  type
    SomeObj = object
      id: int

  iterator it(): int {.closure.} =
    template yieldAndSomeObj: SomeObj =
      var s: SomeObj
      s.id = 2
      yield 1
      s

    checkpoint(yieldAndSomeObj().id)

    var i = 5
    case i
    of 0:
      checkpoint(123)
    of 1, 2, 5:
      checkpoint(3)
    else:
      checkpoint(123)

  test(it, 1, 2, 3)

block: # yield in blockexpr
  iterator it(): int {.closure.} =
    yield(block:
      checkpoint(1)
      yield 2
      3
    )

  test(it, 1, 2, 3)

block: #8851
  type
    Foo = ref object of RootObj
  template someFoo(): Foo =
    var f: Foo
    yield 1
    f
  iterator it(): int {.closure.} =
    var o: RootRef
    o = someFoo()

  test(it, 1)

block: # 8243
  iterator it(): int {.closure.} =
    template yieldAndSeq: seq[int] =
      yield 1
      @[123, 5, 123]

    checkpoint(yieldAndSeq[1])

  test(it, 1, 5)

block:
  iterator it(): int {.closure.} =
    template yieldAndSeq: seq[int] =
      yield 1
      @[123, 5, 123]

    template yieldAndNum: int =
      yield 2
      1

    checkpoint(yieldAndSeq[yieldAndNum])

  test(it, 1, 2, 5)

block: #9694 - yield in ObjConstr
  type Foo = object
    a, b: int

  template yieldAndNum: int =
    yield 1
    2

  iterator it(): int {.closure.} =
    let a = Foo(a: 5, b: yieldAndNum())
    checkpoint(a.b)

  test(it, 1, 2)

block: #9716
  iterator it(): int {.closure.} =
    var a = 0
    for i in 1 .. 3:
      var a: int # Make sure the "local" var is reset
      var b: string # ditto
      yield 1
      a += 5
      b &= "hello"
      doAssert(a == 5)
      doAssert(b == "hello")
  test(it, 1, 1, 1)

block: # nnkChckRange
  type Foo = distinct uint64
  template yieldDistinct: Foo =
    yield 2
    Foo(0)

  iterator it(): int {.closure.} =
    yield 1
    var a: int
    a = int(yieldDistinct())
    yield 3

  test(it, 1, 2, 3)

block: #17849 - yield in case subject
  template yieldInCase: int =
    yield 2
    3

  iterator it(): int {.closure.} =
    yield 1
    case yieldInCase()
    of 1: checkpoint(11)
    of 3: checkpoint(13)
    else: checkpoint(14)
    yield 5

  test(it, 1, 2, 13, 5)

block: # void iterator
  iterator it() {.closure.} =
    try:
      yield
    except:
      discard
  var a = it

block: # Locals present in only 1 state should be on the stack
  proc checkOnStack(a: pointer, shouldBeOnStack: bool) =
    # Quick and dirty way to check if a points to stack
    var dummy = 0
    let dummyAddr = addr dummy
    let distance = abs(cast[int](dummyAddr) - cast[int](a))
    const requiredDistance = 300
    if shouldBeOnStack:
      doAssert(distance <= requiredDistance, "a is not on stack, but should")
    else:
      doAssert(distance > requiredDistance, "a is on stack, but should not")

  iterator it(): int {.closure.} =
    var a = 1
    var b = 2
    var c {.liftLocals.} = 3
    checkOnStack(addr a, true)
    checkOnStack(addr b, false)
    checkOnStack(addr c, false)
    yield a
    yield b
  test(it, 1, 2)

block: # Complex finallies (#24978)
  iterator it(): int {.closure.} =
    try:
      for i in 1..2:
        try:
          yield i + 10
        except:
          doAssert(false, "Should not get here")
        checkpoint(i + 20)
      raiseTestError()
    finally:
      for i in 3..4:
        try:
          yield i + 30
        except:
          doAssert(false, "Should not get here")
        finally:
          checkpoint(i + 40)
        checkpoint(i + 50)
      checkpoint(100)

  testExc(it, 11, 21, 12, 22, 33, 43, 53, 34, 44, 54, 100)

block: # break
  iterator it(): int {.closure.} =
    while true:
      try:
        yield 1
        while true:
          yield 2
          if true:
            break
        break
      finally:
        var localHere = 3
        checkpoint(localHere)

  test(it, 1, 2, 3)

block: # break
  iterator it(): int {.closure.} =
    while true:
      try:
        try:
          yield 1
          while true:
            yield 2
            break
          break
        finally:
          var localHere = 4
          yield 3
          checkpoint(localHere)
        doAssert(false, "Should not get here")
      finally:
        yield 5
        checkpoint(6)
      doAssert(false, "Should not reach here")

  test(it, 1, 2, 3, 4, 5, 6)

block: # continue
  iterator it(): int {.closure.} =
    for i in 1 .. 3:
      try:
        try:
          yield i + 10
          while true:
            yield i + 20
            break
          if i == 2:
            continue
          checkpoint(i + 30)
        finally:
          yield 3
          checkpoint(4)
        checkpoint(5)
      finally:
        yield 6
        checkpoint(7)

  test(it, 11, 21, 31, 3, 4, 5, 6, 7, 12, 22, 3, 4, 6, 7, 13, 23, 33, 3, 4, 5, 6, 7)

block: # return without finally
  iterator it(): int {.closure.} =
    try:
      yield 1
      if true:
        return
    except:
      doAssert(false, "Unreachable")
    yield 2

  test(it, 1)

block: # return in finally
  iterator it(): int {.closure.} =
    try:
      yield 1
    except:
      doAssert(false, "Unreachable")
    finally:
      return
    yield 2

  test(it, 1)

block: # launch iter with current exception
  iterator it(): int {.closure.} =
    try:
      yield 1
    finally:
      discard

  try:
    raise newException(ValueError, "")
  except:
    test(it, 1)

block: #21235
  proc myFunc() =
    iterator myFuncIter(): int {.closure.} =
      if false:
        try:
          yield 5
        except:
          discard
    var nameIterVar = myFuncIter
    discard nameIterVar()

  var ok = false
  try:
    try:
      raise ValueError.newException("foo")
    finally:
      myFunc()
  except ValueError:
    ok = true
  doAssert(ok)

block: # break in for without yield in try
  iterator it(): int {.closure.} =
    try:
      block:
        checkpoint(1)
        for i in 0 .. 10:
          checkpoint(2)
          break
        checkpoint(3)

      try:
        yield 4
      except:
        checkpoint(123)
    except:
      discard
    finally:
      checkpoint(5)

  test(it, 1, 2, 3, 4, 5)

block: #25038
  template m(w: untyped): untyped =
    var g: typeof(w)
    g

  iterator d(): int {.closure.} =
    discard m:
      for _ in [0]:
        continue
      0

  test(d)

block: #25202
  proc p() =
    iterator p_1073741828(checkpoints: var seq[int]): int {.
        closure, raises: [].} =
      var closureSucceeded_1073741827 = true
      try:
        try:
          try:
            yield 0
            raise newException(ValueError, "value error")
          except ValueError:
            checkpoints.add(1)
            raise newException(IOError, "io error")
        finally:
          yield 2
      except IOError as exc:
        closureSucceeded_1073741827 = false
        checkpoints.add(3)
      finally:
        checkpoints.add(4)
        if closureSucceeded_1073741827:
          discard

    var internalClosure = p_1073741828
    var internalClosure2 = p_1073741828

    var checkpoints1 = newSeq[int]()
    var checkpoints2 = newSeq[int]()

    while true:
      if not internalClosure.finished():
        checkpoints1.add internalClosure(checkpoints1)
        doAssert(getCurrentException() == nil)
      if not internalClosure2.finished():
        checkpoints2.add internalClosure2(checkpoints2)
        doAssert(getCurrentException() == nil)
      if internalClosure.finished() and internalClosure2.finished():
        break

    if checkpoints1[^1] == 0: checkpoints1.del(checkpoints1.high)
    if checkpoints2[^1] == 0: checkpoints2.del(checkpoints2.high)
    doAssert(checkpoints1 == @[0, 1, 2, 3, 4])
    doAssert(checkpoints1 == checkpoints2)

  p()

block: #25261
  iterator y(): int {.closure.} =
    try:
      try:
        raise newException(CatchableError, "Error")
      except CatchableError:
        return 123
      yield 0
    finally:
      discard

  let w = y
  doAssert(w() == 123)
  doAssert(getCurrentExceptionMsg() == "")

  try:
    raise newException(ValueError, "Outer error")
  except:
    doAssert(getCurrentExceptionMsg() == "Outer error")
    let w = y
    doAssert(w() == 123)
    doAssert(getCurrentExceptionMsg() == "Outer error")
  doAssert(getCurrentExceptionMsg() == "")

block:
  # Looks almost like above, but last finally changed to except
  iterator y(): int {.closure.} =
    try:
      try:
        raise newException(CatchableError, "Error")
      except CatchableError:
        return 123
      yield 0
    except:
      discard

  let w = y
  doAssert(w() == 123)
  doAssert(getCurrentExceptionMsg() == "")

  try:
    raise newException(ValueError, "Outer error")
  except:
    doAssert(getCurrentExceptionMsg() == "Outer error")
    let w = y
    doAssert(w() == 123)
    doAssert(getCurrentExceptionMsg() == "Outer error")
  doAssert(getCurrentExceptionMsg() == "")

block: #25330 (v1)
  iterator count1(): int {.closure.} =
    yield 1
    raiseTestError()

  iterator count0(): int {.closure.} =
    try:
      var count = count1
      while true:
        yield count()
        if finished(count): break
    finally:
      try:
        checkpoint(2)
        var count2 = count1
        while true:
          yield count2()
          if finished(count2): break
        discard  # removing this outputs "raise"
      except:
        checkpoint(3)
        raise

  testExc(count0, 1, 2, 1, 3)

block: #25330 (v2)
  iterator count1(): int {.closure.} =
    yield 1
    raiseTestError()

  iterator count0(): int {.closure.} =
    try:
      var count = count1
      for x in 0 .. 10:
        yield count()
    finally:
      try:
        checkpoint(2)
        var count2 = count1
        for x in 0 .. 10:
          yield count2()
      except:
        checkpoint(3)
        raise

  testExc(count0, 1, 2, 1, 3)
