Recursively collects stack entries off the top of the stack according to the stack entry's depth.

def _collect_pops(stack, depth, pops, skip):
  """Recursively collects stack entries off the top of the stack according to the stack entry's depth."""
  if depth >= 0:
    return pops

  set_current_depth_after_recursion = False
  set_skip_for_current_entry_children = False
  set_skip_after_current_entry = False
  extract_next_tokens = False
  expect_extracted_tokens = []

  se = stack.pop()
  pops_len = len(pops)
  if (pops_len > 1
      and se.opname == 'BUILD_TUPLE'
      and pops[-1].opname == 'LOAD_CONST'
      and pops[-1].oparg_repr[0] in ['lambda', '{', '(', '[']
      and pops[-2].opname in ['MAKE_CLOSURE', 'MAKE_FUNCTION']):
    # Skip BUILD_TUPLE and its children if they are storing arguments for a closure, since those don't show up in the code.
    skip = -se.stack_depth + 1

  if (pops_len > 2
      and sys.version_info[0] >= 3
      and se.opname == 'BUILD_TUPLE'
      and pops[-1].opname == 'LOAD_CONST'
      and pops[-1].oparg_repr[0] in ['lambda', '{', '(', '[']
      and pops[-2].opname == 'LOAD_CONST'
      and pops[-3].opname in ['MAKE_CLOSURE', 'MAKE_FUNCTION']):
    # Skip BUILD_TUPLE and its children if they are storing arguments for a closure, since those don't show up in the code.
    skip = -se.stack_depth + 1

  if (pops_len > 0
      and se.opname == 'GET_ITER'
      and pops[-1].opname == 'CALL_FUNCTION'):
    # CALL_FUNCTION followed by GET_ITER means we are calling one of the comprehensions and we are about to load its arguments.
    # The CALL_FUNCTION at the top of the stack should be invisible, since it expects a ')' which won't appear in the code.
    pops[-1].oparg_repr = ['']
    # We need to extract the arguments that we're about to load so that we can store their tokens inside of the upcoming comprehension.
    extract_next_tokens = -1

  if (len(stack)
      and se.opname == 'BUILD_TUPLE_UNPACK_WITH_CALL'):
    extract_next_tokens = se.stack_depth
    expect_extracted_tokens = [
        # Expect LOAD_FAST as the first element (required=True), and prepend its oparg_repr with '*'.
        (0, 'LOAD_FAST', True, 'prepend', ['*']),
        # Expect BUILD_TUPLE as the last stack token extracted (required=False) and replace its oparg_repr with ''.
        (abs(extract_next_tokens) - 1, 'BUILD_TUPLE', False, 'replace', [''])
    ]
    set_current_depth_after_recursion = se.stack_depth
    se.stack_depth = 0

  if (pops_len > 0
      and sys.version_info[0] >= 3
      and se.opname == 'LOAD_CONST'
      and pops[-1].opname == 'MAKE_FUNCTION'):
    # In python 3, MAKE_FUNCTION followed by LOAD_CONST is loading the name of the function, which won't appear in the code.
    se.oparg_repr = ['']
    # Additionally, this entry shouldn't impact future stack computations, as MAKE_FUNCTION will be removed.
    set_current_depth_after_recursion = 0

  if pops_len and pops[-1].opname == 'LIST_APPEND':
    # Skip all but the first stack entry of list comprehensions. Sets the skip value to be all remaining stack entries.
    # The BUILD_LIST check below will disable skip at the right time.
    set_skip_after_current_entry = len(stack)

  if skip > 0 and se.opname == 'BUILD_LIST' and se.stack_depth == 0:
    # If we're in skip mode and we just hit what might be the beginning of a list comprehension, check for a LIST_APPEND in the current pops.
    for popped_se in pops[::-1]:
      if popped_se.opname == 'LIST_APPEND':
        skip = 0
        break

  children_skip = skip
  if (se.opname.startswith('UNARY_')
      or (se.opname.startswith('BINARY_') and se.opname != 'BINARY_SUBSCR')
      or se.opname == 'SLICE+2'
      or se.opname == 'SLICE+3'
      or se.opname == 'COMPARE_OP'):
    # Unary and binary ops come after their operand(s) on the stack, but before (or between) their operand(s) in code, so we need to reverse that.

    if set_skip_for_current_entry_children or skip > 0:
      children_skip = 1
    pops = _collect_pops(stack, -1, pops, children_skip)

    if skip <= 0:
      pops.append(se)
      qj._DEBUG_QJ and qj.LOG_FN('added se: %r' % se)
    else:
      qj._DEBUG_QJ and qj.LOG_FN('(skipping se: %r %r)' % (se.opname, se.oparg_repr))

    popped_depth = se.stack_depth + 1
  else:
    # Non prefix/infix ops -- their representations come after their children in code, or they don't have children.
    if skip <= 0:
      pops.append(se)
      qj._DEBUG_QJ and qj.LOG_FN('added se: %r' % se)
    else:
      qj._DEBUG_QJ and qj.LOG_FN('(skipping se: %r %r)' % (se.opname, se.oparg_repr))

    if ((se.stack_depth < 0
         and se.opname != 'BUILD_SLICE'
         and se.opname.startswith('BUILD_'))
        or se.stack_depth >= 0):
      next_depth = se.stack_depth
    else:
      next_depth = se.stack_depth - 1

    if set_skip_for_current_entry_children or skip > 0:
      children_skip = abs(next_depth)
    if se.opname == 'BUILD_SLICE':
      # BUILD_SLICE's arguments need to be collected, as missing args are replaced with Nones which don't appear in the code.
      slice_pops = _collect_pops(stack, next_depth, [], children_skip)
      added_colon = 0
      for slice_se in slice_pops:
        if slice_se.opname == 'LOAD_CONST' and slice_se.oparg_repr[0] == 'None':
          if added_colon >= 1:
            slice_se.oparg_repr = ['']
          else:
            slice_se.oparg_repr = [':']
            added_colon += 1
        pops.append(slice_se)
    else:
      pops = _collect_pops(stack, next_depth, pops, children_skip)

    # BUILD_LIST 0 marks the start of a list comprehension, but we need it to consume a slot on the stack.
    if se.stack_depth == 0 and se.opname != 'BUILD_LIST':
      popped_depth = 0
    else:
      popped_depth = 1

  tokens = []
  if extract_next_tokens < 0:
    tokens = _collect_pops(stack, extract_next_tokens, [], skip)
    for index, expected_token, required, fixup_type, fixup_value in expect_extracted_tokens:
      if qj._DEBUG_QJ:
        assert (index < 0 and index + len(tokens) > 0) or 0 <= index < len(tokens)
        if required:
          assert tokens[index].opname == expected_token
      if (index < 0 and index + len(tokens) > 0) or 0 <= index < len(tokens) and tokens[index].opname == expected_token:
        if fixup_type == 'prepend':
          tokens[index].oparg_repr = fixup_value + tokens[index].oparg_repr
        elif fixup_type == 'replace':
          tokens[index].oparg_repr = fixup_value
    tokens.reverse()
    popped_depth -= extract_next_tokens

  if children_skip > 0:
    skip -= popped_depth

  if set_skip_after_current_entry > 0:
    skip = set_skip_after_current_entry + max(0, skip)

  pops = _collect_pops(stack, depth + popped_depth, pops, skip)

  if len(tokens):  # pylint: disable=g-explicit-length-test
    target_se = pops[-1]
    target_se.children.append(tokens)
    target_se.oparg_repr = target_se.oparg_repr[:1] + [t for token in tokens for t in token.oparg_repr] + target_se.oparg_repr[1:]

  if set_current_depth_after_recursion is not False:
    se.stack_depth = set_current_depth_after_recursion

  return pops