module YARP::Debug

def self.cruby_locals(source)

sets of local variables that were encountered.
For the given source, compiles with CRuby and returns a list of all of the
def self.cruby_locals(source)
  verbose = $VERBOSE
  $VERBOSE = nil
  begin
    locals = []
    stack = [ISeq.new(RubyVM::InstructionSequence.compile(source).to_a)]
    while (iseq = stack.pop)
      if iseq.type != :once
        names = iseq.local_table
        # CRuby will push on a special local variable when there are keyword
        # arguments. We get rid of that here.
        names = names.grep_v(Integer)
        # TODO: We don't support numbered local variables yet, so we get rid
        # of those here.
        names = names.grep_v(/^_\d$/)
        # Now push them onto the list of locals.
        locals << names
      end
      iseq.each_child { |child| stack << child }
    end
    locals
  ensure
    $VERBOSE = verbose
  end
end

def self.newlines(source)

def self.newlines(source)
  YARP.parse(source).source.offsets
end

def self.parse_serialize_file(filepath)

def self.parse_serialize_file(filepath)
  parse_serialize_file_metadata(filepath, [filepath.bytesize, filepath.b, 0].pack("LA*L"))
end

def self.yarp_locals(source)

sets of local variables that were encountered.
For the given source, parses with YARP and returns a list of all of the
def self.yarp_locals(source)
  locals = []
  stack = [YARP.parse(source).value]
  while (node = stack.pop)
    case node
    when BlockNode, DefNode, LambdaNode
      names = node.locals
      params = node.parameters
      params = params&.parameters unless node.is_a?(DefNode)
      # YARP places parameters in the same order that they appear in the
      # source. CRuby places them in the order that they need to appear
      # according to their own internal calling convention. We mimic that
      # order here so that we can compare properly.
      if params
        sorted = [
          *params.requireds.grep(RequiredParameterNode).map(&:constant_id),
          *params.optionals.map(&:constant_id),
          *((params.rest.name ? params.rest.name.to_sym : :*) if params.rest && params.rest.operator != ","),
          *params.posts.grep(RequiredParameterNode).map(&:constant_id),
          *params.keywords.reject(&:value).map { |param| param.name.chomp(":").to_sym },
          *params.keywords.select(&:value).map { |param| param.name.chomp(":").to_sym }
        ]
        # TODO: When we get a ... parameter, we should be pushing * and &
        # onto the local list. We don't do that yet, so we need to add them
        # in here.
        if params.keyword_rest.is_a?(ForwardingParameterNode)
          sorted.push(:*, :&, :"...")
        end
        # Recurse down the parameter tree to find any destructured
        # parameters and add them after the other parameters.
        param_stack = params.requireds.concat(params.posts).grep(RequiredDestructuredParameterNode).reverse
        while (param = param_stack.pop)
          case param
          when RequiredDestructuredParameterNode
            param_stack.concat(param.parameters.reverse)
          when RequiredParameterNode
            sorted << param.constant_id
          when SplatNode
            sorted << param.expression.constant_id if param.expression
          end
        end
        names = sorted.concat(names - sorted)
      end
      locals << names
    when ClassNode, ModuleNode, ProgramNode, SingletonClassNode
      locals << node.locals
    when ForNode
      locals << []
    when PostExecutionNode
      locals.push([], [])
    end
    stack.concat(node.child_nodes.compact)
  end
  locals
end