discard """
  output: '''Welcome to LoopTesterApp, Nim edition
Constructing Simple CFG...
5000 dummy loops
Constructing CFG...
Performing Loop Recognition
1 Iteration
Another 3 iterations...
...
Found 1 loops (including artificial root node) (3)'''
"""

# bug #3184

import tables, sets

when not declared(withScratchRegion):
  template withScratchRegion(body: untyped) = body

type
  BasicBlock = ref object
    inEdges: seq[BasicBlock]
    outEdges: seq[BasicBlock]
    name: int

proc newBasicBlock(name: int): BasicBlock =
  result = BasicBlock(
    inEdges: newSeq[BasicBlock](),
    outEdges: newSeq[BasicBlock](),
    name: name
  )

proc hash(x: BasicBlock): int {.inline.} =
  result = x.name

type
  BasicBlockEdge = object
    fr: BasicBlock
    to: BasicBlock

  Cfg = object
    basicBlockMap: Table[int, BasicBlock]
    edgeList: seq[BasicBlockEdge]
    startNode: BasicBlock

proc newCfg(): Cfg =
  result = Cfg(
    basicBlockMap: initTable[int, BasicBlock](),
    edgeList: newSeq[BasicBlockEdge](),
    startNode: nil)

proc createNode(self: var Cfg, name: int): BasicBlock =
  result = self.basicBlockMap.getOrDefault(name)
  if result == nil:
    result = newBasicBlock(name)
    self.basicBlockMap.add name, result

  if self.startNode == nil:
    self.startNode = result

proc newBasicBlockEdge(cfg: var Cfg, fromName, toName: int) =
  var result = BasicBlockEdge(
    fr: cfg.createNode(fromName),
    to: cfg.createNode(toName)
  )
  result.fr.outEdges.add(result.to)
  result.to.inEdges.add(result.fr)
  cfg.edgeList.add(result)

type
  SimpleLoop = ref object
    basicBlocks: seq[BasicBlock] # TODO: set here
    children: seq[SimpleLoop] # TODO: set here
    parent: SimpleLoop
    header: BasicBlock
    isRoot, isReducible: bool
    counter, nestingLevel, depthLevel: int

proc setParent(self: SimpleLoop, parent: SimpleLoop) =
  self.parent = parent
  self.parent.children.add self

proc setHeader(self: SimpleLoop, bb: BasicBlock) =
  self.basicBlocks.add(bb)
  self.header = bb

proc setNestingLevel(self: SimpleLoop, level: int) =
  self.nestingLevel = level
  if level == 0: self.isRoot = true

var loopCounter: int = 0

type
  Lsg = object
    loops: seq[SimpleLoop]
    root: SimpleLoop

proc createNewLoop(self: var Lsg): SimpleLoop =
  result = SimpleLoop(
    basicBlocks: newSeq[BasicBlock](),
    children: newSeq[SimpleLoop](),
    isReducible: true)
  loopCounter += 1
  result.counter = loopCounter

proc addLoop(self: var Lsg, l: SimpleLoop) =
  self.loops.add l

proc newLsg(): Lsg =
  result = Lsg(loops: newSeq[SimpleLoop](),
    root: result.createNewLoop())
  result.root.setNestingLevel(0)
  result.addLoop(result.root)

type
  UnionFindNode = ref object
    parent {.cursor.}: UnionFindNode
    bb: BasicBlock
    l: SimpleLoop
    dfsNumber: int

proc initNode(self: UnionFindNode, bb: BasicBlock, dfsNumber: int) =
  self.parent = self
  self.bb = bb
  self.dfsNumber = dfsNumber

proc findSet(self: UnionFindNode): UnionFindNode =
  var nodeList = newSeq[UnionFindNode]()
  var it {.cursor.} = self

  while it != it.parent:
    var parent {.cursor.} = it.parent
    if parent != parent.parent: nodeList.add it
    it = parent

  for iter in nodeList: iter.parent = it.parent
  result = it

proc union(self: UnionFindNode, unionFindNode: UnionFindNode) =
  self.parent = unionFindNode


const
  BB_NONHEADER = 1 # a regular BB
  BB_REDUCIBLE = 2 # reducible loop
  BB_SELF = 3 # single BB loop
  BB_IRREDUCIBLE = 4 # irreducible loop
  BB_DEAD = 5 # a dead BB

  # # Marker for uninitialized nodes.
  UNVISITED = -1

  # # Safeguard against pathologic algorithm behavior.
  MAXNONBACKPREDS = (32 * 1024)

type
  HavlakLoopFinder = object
    cfg: Cfg
    lsg: Lsg

proc newHavlakLoopFinder(cfg: Cfg, lsg: sink Lsg): HavlakLoopFinder =
  result = HavlakLoopFinder(cfg: cfg, lsg: lsg)

proc isAncestor(w, v: int, last: seq[int]): bool =
  w <= v and v <= last[w]

proc dfs(currentNode: BasicBlock, nodes: var seq[UnionFindNode],
         number: var Table[BasicBlock, int],
         last: var seq[int], current: int) =
  var stack = @[(currentNode, current)]
  while stack.len > 0:
    let (currentNode, current) = stack.pop()
    nodes[current].initNode(currentNode, current)
    number[currentNode] = current

    for target in currentNode.outEdges:
      if number[target] == UNVISITED:
        stack.add((target, current+1))
        #result = dfs(target, nodes, number, last, result + 1)
  last[number[currentNode]] = current

proc findLoops(self: var HavlakLoopFinder): int =
  var startNode = self.cfg.startNode
  if startNode == nil: return 0
  var size = self.cfg.basicBlockMap.len

  var nonBackPreds = newSeq[HashSet[int]]()
  var backPreds = newSeq[seq[int]]()
  var number = initTable[BasicBlock, int]()
  var header = newSeq[int](size)
  var types = newSeq[int](size)
  var last = newSeq[int](size)
  var nodes = newSeq[UnionFindNode]()

  for i in 1..size:
    nonBackPreds.add initHashSet[int](1)
    backPreds.add newSeq[int]()
    nodes.add(UnionFindNode())

  # Step a:
  #   - initialize all nodes as unvisited.
  #   - depth-first traversal and numbering.
  #   - unreached BB's are marked as dead.
  #
  for v in self.cfg.basicBlockMap.values: number[v] = UNVISITED
  dfs(startNode, nodes, number, last, 0)

  # Step b:
  #   - iterate over all nodes.
  #
  #   A backedge comes from a descendant in the DFS tree, and non-backedges
  #   from non-descendants (following Tarjan).
  #
  #   - check incoming edges 'v' and add them to either
  #     - the list of backedges (backPreds) or
  #     - the list of non-backedges (nonBackPreds)
  #
  for w in 0 ..< size:
    header[w] = 0
    types[w]  = BB_NONHEADER

    var nodeW = nodes[w].bb
    if nodeW != nil:
      for nodeV in nodeW.inEdges:
        var v = number[nodeV]
        if v != UNVISITED:
          if isAncestor(w, v, last):
            backPreds[w].add v
          else:
            nonBackPreds[w].incl v
    else:
      types[w] = BB_DEAD

  # Start node is root of all other loops.
  header[0] = 0

  # Step c:
  #
  # The outer loop, unchanged from Tarjan. It does nothing except
  # for those nodes which are the destinations of backedges.
  # For a header node w, we chase backward from the sources of the
  # backedges adding nodes to the set P, representing the body of
  # the loop headed by w.
  #
  # By running through the nodes in reverse of the DFST preorder,
  # we ensure that inner loop headers will be processed before the
  # headers for surrounding loops.

  for w in countdown(size - 1, 0):
    # this is 'P' in Havlak's paper
    var nodePool = newSeq[UnionFindNode]()

    var nodeW = nodes[w].bb
    if nodeW != nil: # dead BB
      # Step d:
      for v in backPreds[w]:
        if v != w:
          nodePool.add nodes[v].findSet
        else:
          types[w] = BB_SELF

      # Copy nodePool to workList.
      #
      var workList = newSeq[UnionFindNode]()
      for x in nodePool: workList.add x

      if nodePool.len != 0: types[w] = BB_REDUCIBLE

      # work the list...
      #
      while workList.len > 0:
        let x = workList[0]
        workList.del(0)

        # Step e:
        #
        # Step e represents the main difference from Tarjan's method.
        # Chasing upwards from the sources of a node w's backedges. If
        # there is a node y' that is not a descendant of w, w is marked
        # the header of an irreducible loop, there is another entry
        # into this loop that avoids w.
        #

        # The algorithm has degenerated. Break and
        # return in this case.
        #
        var nonBackSize = nonBackPreds[x.dfsNumber].len
        if nonBackSize > MAXNONBACKPREDS: return 0

        for iter in nonBackPreds[x.dfsNumber]:
          var y = nodes[iter]
          var ydash = y.findSet

          if not isAncestor(w, ydash.dfsNumber, last):
            types[w] = BB_IRREDUCIBLE
            nonBackPreds[w].incl ydash.dfsNumber
          else:
            if ydash.dfsNumber != w and not nodePool.contains(ydash):
              workList.add ydash
              nodePool.add ydash

      # Collapse/Unionize nodes in a SCC to a single node
      # For every SCC found, create a loop descriptor and link it in.
      #
      if nodePool.len > 0 or types[w] == BB_SELF:
        var l = self.lsg.createNewLoop

        l.setHeader(nodeW)
        l.isReducible = types[w] != BB_IRREDUCIBLE

        # At this point, one can set attributes to the loop, such as:
        #
        # the bottom node:
        #    iter  = backPreds(w).begin();
        #    loop bottom is: nodes(iter).node;
        #
        # the number of backedges:
        #    backPreds(w).size()
        #
        # whether this loop is reducible:
        #    types(w) != BB_IRREDUCIBLE
        #
        nodes[w].l = l

        for node in nodePool:
          # Add nodes to loop descriptor.
          header[node.dfsNumber] = w
          node.union(nodes[w])

          # Nested loops are not added, but linked together.
          var nodeL = node.l
          if nodeL != nil:
            nodeL.setParent(l)
          else:
            l.basicBlocks.add node.bb

        self.lsg.addLoop(l)

  result = self.lsg.loops.len


type
  LoopTesterApp = object
    cfg: Cfg
    lsg: Lsg

proc newLoopTesterApp(): LoopTesterApp =
  result.cfg = newCfg()
  result.lsg = newLsg()

proc buildDiamond(self: var LoopTesterApp, start: int): int =
  newBasicBlockEdge(self.cfg, start, start + 1)
  newBasicBlockEdge(self.cfg, start, start + 2)
  newBasicBlockEdge(self.cfg, start + 1, start + 3)
  newBasicBlockEdge(self.cfg, start + 2, start + 3)
  result = start + 3

proc buildConnect(self: var LoopTesterApp, start1, end1: int) =
  newBasicBlockEdge(self.cfg, start1, end1)

proc buildStraight(self: var LoopTesterApp, start, n: int): int =
  for i in 0..n-1:
    self.buildConnect(start + i, start + i + 1)
  result = start + n

proc buildBaseLoop(self: var LoopTesterApp, from1: int): int =
  let header = self.buildStraight(from1, 1)
  let diamond1 = self.buildDiamond(header)
  let d11 = self.buildStraight(diamond1, 1)
  let diamond2 = self.buildDiamond(d11)
  let footer = self.buildStraight(diamond2, 1)

  self.buildConnect(diamond2, d11)
  self.buildConnect(diamond1, header)
  self.buildConnect(footer, from1)
  result = self.buildStraight(footer, 1)

proc run(self: var LoopTesterApp) =
  echo "Welcome to LoopTesterApp, Nim edition"
  echo "Constructing Simple CFG..."

  discard self.cfg.createNode(0)
  discard self.buildBaseLoop(0)
  discard self.cfg.createNode(1)
  self.buildConnect(0, 2)

  echo "5000 dummy loops"

  for i in 1..5000:
    withScratchRegion:
      var h = newHavlakLoopFinder(self.cfg, newLsg())
      discard h.findLoops

  echo "Constructing CFG..."
  var n = 2

  when true: # not defined(gcOrc):
    # currently cycle detection is so slow that we disable this part
    for parlooptrees in 1..10:
      discard self.cfg.createNode(n + 1)
      self.buildConnect(2, n + 1)
      n += 1
      for i in 1..100:
        var top = n
        n = self.buildStraight(n, 1)
        for j in 1..25: n = self.buildBaseLoop(n)
        var bottom = self.buildStraight(n, 1)
        self.buildConnect n, top
        n = bottom
      self.buildConnect(n, 1)

  echo "Performing Loop Recognition\n1 Iteration"

  var h = newHavlakLoopFinder(self.cfg, newLsg())
  var loops = h.findLoops

  echo "Another 3 iterations..."

  var sum = 0
  for i in 1..3:
    withScratchRegion:
      write stdout, "."
      flushFile(stdout)
      var hlf = newHavlakLoopFinder(self.cfg, newLsg())
      sum += hlf.findLoops
      #echo getOccupiedMem()
  echo "\nFound ", loops, " loops (including artificial root node) (", sum, ")"

  when false:
    echo("Total memory available: " & formatSize(getTotalMem()) & " bytes")
    echo("Free memory: " & formatSize(getFreeMem()) & " bytes")

proc main =
  var l = newLoopTesterApp()
  l.run

let mem = getOccupiedMem()
main()
when defined(gcOrc):
  GC_fullCollect()
  doAssert getOccupiedMem() == mem
