rubinius/rubinius

View on GitHub
core/enumerator.rb

Summary

Maintainability
D
2 days
Test Coverage
class Enumerator
  include Enumerable

  attr_writer :args
  private :args=

  attr_writer :size
  private :size=

  attr_reader :generator
  protected :generator

  attr_reader :lookahead
  protected :lookahead

  def initialize_enumerator(receiver, size, method_name, *method_args)
    @object = receiver
    @size = size
    @iter = method_name
    @args = method_args
    @generator = nil
    @lookahead = []
    @feedvalue = nil

    self
  end
  private :initialize_enumerator

  def initialize(receiver_or_size=undefined, method_name=:each, *method_args, &block)
    size = nil

    if block_given?
      unless undefined.equal? receiver_or_size
        size = if receiver_or_size.nil? || receiver_or_size == Float::INFINITY || receiver_or_size.respond_to?(:call)
                  receiver_or_size
                else
                  Rubinius::Type.coerce_to receiver_or_size, Integer, :to_int
                end
      end

      receiver = Generator.new(&block)
    else
      if undefined.equal? receiver_or_size
        raise ArgumentError, "Enumerator#initialize requires a block when called without arguments"
      end

      receiver = receiver_or_size
    end

    method_name = Rubinius::Type.coerce_to method_name, Symbol, :to_sym

    initialize_enumerator receiver, size, method_name, *method_args

    self
  end
  private :initialize

  def each(*args)
    enumerator = self
    new_args = @args

    unless args.empty?
      enumerator = dup
      new_args = @args.empty? ? args : (@args + args)
    end

    Rubinius.privately do
      enumerator.args = new_args
    end

    if block_given?
      Rubinius.privately do
        enumerator.each_with_block do |*yield_args|
          yield(*yield_args)
        end
      end
    else
      enumerator
    end
  end

  def each_with_block
    @object.__send__ @iter, *@args do |*args|
      ret = yield(*args)
      unless @feedvalue.nil?
        ret = @feedvalue
        @feedvalue = nil
      end
      ret
    end
  end
  private :each_with_block

  def each_with_index
    return to_enum(:each_with_index) { size } unless block_given?

    idx = 0

    each do
      o = Rubinius.single_block_arg
      val = yield(o, idx)
      idx += 1
      val
    end
  end

  def next
    return @lookahead.shift unless @lookahead.empty?

    @generator ||= Iterator.new self

    begin
      return @generator.next if @generator.next?
    rescue StopIteration
    end

    exception = StopIteration.new "iteration reached end"
    Rubinius.privately do
      exception.result = @generator.result
    end

    raise exception
  end

  def next_values
    Array(self.next)
  end

  def peek
    return @lookahead.first unless @lookahead.empty?
    item = self.next
    @lookahead << item
    item
  end

  def peek_values
    Array(self.peek).dup
  end

  def rewind
    @object.rewind if @object.respond_to? :rewind
    @generator.rewind if @generator
    @lookahead = []
    @feedvalue = nil
    self
  end

  def size
    @size.respond_to?(:call) ? @size.call : @size
  end

  def with_index(offset=0)
    if offset
      offset = Rubinius::Type.coerce_to offset, Integer, :to_int
    else
      offset = 0
    end

    return to_enum(:with_index, offset) { size } unless block_given?

    each do
      o = Rubinius.single_block_arg
      val = yield(o, offset)
      offset += 1
      val
    end
  end

  def feed(val)
    raise TypeError, "Feed value already set" unless @feedvalue.nil?
    @feedvalue = val
    nil
  end

  def dup
    raise TypeError, "Can't copy iteration state" if @generator&.instance_variable_get(:@fiber)
    super
  end

  def initialize_copy(other)
    @generator = other.generator&.dup
    @lookahead = other.lookahead.dup
    super
  end
  private :initialize_copy

  class Yielder
    def initialize(&block)
      raise LocalJumpError, "Expected a block to be given" unless block_given?

      @proc = block

      self
    end
    private :initialize

    def yield(*args)
      @proc.call *args
    end

    def <<(*args)
      self.yield(*args)

      self
    end
  end

  class Generator
    include Enumerable

    def initialize(&block)
      raise LocalJumpError, "Expected a block to be given" unless block_given?

      @proc = block

      self
    end
    private :initialize

    def each(*args)
      enclosed_yield = Proc.new { |*enclosed_args| yield *enclosed_args }

      @proc.call Yielder.new(&enclosed_yield), *args
    end
  end

  class Lazy < self
    class StopLazyError < Exception; end

    def initialize(receiver, size=nil)
      raise ArgumentError, "Lazy#initialize requires a block" unless block_given?
      Rubinius.check_frozen

      super(size) do |yielder, *each_args|
        begin
          receiver.each(*each_args) do |*args|
            yield yielder, *args
          end
        rescue Exception
          nil
        end
      end

      self
    end
    private :initialize

    def to_enum(method_name=:each, *method_args, &block)
      size = block_given? ? block : nil
      ret = Lazy.allocate

      Rubinius.privately do
        ret.initialize_enumerator self, size, method_name, *method_args
      end

      ret
    end
    alias_method :enum_for, :to_enum

    def lazy
      self
    end

    alias_method :force, :to_a

    def take(n)
      n = Rubinius::Type.coerce_to n, Integer, :to_int
      raise ArgumentError, "attempt to take negative size" if n < 0

      current_size = enumerator_size
      set_size = if current_size.kind_of?(Numeric)
        n < current_size ? n : current_size
      else
        current_size
      end

      return to_enum(:cycle, 0).lazy if n.zero?

      taken = 0
      Lazy.new(self, set_size) do |yielder, *args|
        if taken < n
          yielder.yield(*args)
          taken += 1
          raise StopLazyError unless taken < n
        else
          raise StopLazyError
        end
      end
    end

    def drop(n)
      n = Rubinius::Type.coerce_to n, Integer, :to_int
      raise ArgumentError, "attempt to drop negative size" if n < 0

      current_size = enumerator_size
      set_size = if current_size.kind_of?(Integer)
        n < current_size ? current_size - n : 0
      else
        current_size
      end

      dropped = 0
      Lazy.new(self, set_size) do |yielder, *args|
        if dropped < n
          dropped += 1
        else
          yielder.yield(*args)
        end
      end
    end

    def take_while
      raise ArgumentError, "Lazy#take_while requires a block" unless block_given?

      Lazy.new(self, nil) do |yielder, *args|
        if yield(*args)
          yielder.yield(*args)
        else
          raise StopLazyError
        end
      end
    end

    def drop_while
      raise ArgumentError, "Lazy#drop_while requires a block" unless block_given?

      succeeding = true
      Lazy.new(self, nil) do |yielder, *args|
        if succeeding
          unless yield(*args)
            succeeding = false
            yielder.yield(*args)
          end
        else
          yielder.yield(*args)
        end
      end
    end

    def select
      raise ArgumentError, 'Lazy#{select,find_all} requires a block' unless block_given?

      Lazy.new(self, nil) do |yielder, *args|
        val = args.length >= 2 ? args : args.first
        yielder.yield(*args) if yield(val)
      end
    end
    alias_method :find_all, :select

    def reject
      raise ArgumentError, "Lazy#reject requires a block" unless block_given?

      Lazy.new(self, nil) do |yielder, *args|
        val = args.length >= 2 ? args : args.first
        yielder.yield(*args) unless yield(val)
      end
    end

    def grep(pattern)
      Lazy.new(self, nil) do |yielder, *args|
        val = args.length >= 2 ? args : args.first

        if pattern === val
          Regexp.set_block_last_match

          if block_given?
            yielder.yield yield(val)
          else
            yielder.yield val
          end
        end
      end
    end

    def grep_v(pattern)
      Lazy.new(self, nil) do |yielder, *args|
        val = args.length >= 2 ? args : args.first

        unless pattern === val
          if block_given?
            yielder.yield yield(val)
          else
            yielder.yield val
          end
        end
      end
    end

    def map
      raise ArgumentError, 'Lazy#{map,collect} requires a block' unless block_given?

      Lazy.new(self, enumerator_size) do |yielder, *args|
        yielder.yield yield(*args)
      end
    end
    alias_method :collect, :map

    def collect_concat
      raise ArgumentError, 'Lazy#{collect_concat,flat_map} requires a block' unless block_given?

      Lazy.new(self, nil) do |yielder, *args|
        yield_ret = yield(*args)

        if Rubinius::Type.object_respond_to?(yield_ret, :force) &&
            Rubinius::Type.object_respond_to?(yield_ret, :each)
          yield_ret.each do |v|
            yielder.yield v
          end
        else
          array = Rubinius::Type.check_convert_type yield_ret, Array, :to_ary
          if array
            array.each do |v|
              yielder.yield v
            end
          else
            yielder.yield yield_ret
          end
        end
      end
    end
    alias_method :flat_map, :collect_concat

    def zip(*lists)
      return super(*lists) { |entry| yield entry } if block_given?

      lists.map! do |list|
        array = Rubinius::Type.check_convert_type list, Array, :to_ary

        case
        when array
          array
        when Rubinius::Type.object_respond_to?(list, :each)
          list.to_enum :each
        else
          raise TypeError, "wrong argument type #{list.class} (must respond to :each)"
        end
      end

      index = 0
      Lazy.new(self, enumerator_size) do |yielder, *args|
        val = args.length >= 2 ? args : args.first
        rests = lists.map { |list|
                  case list
                  when Array
                    list[index]
                  else
                    begin
                      list.next
                    rescue StopIteration
                      nil
                    end
                  end
                }
        yielder.yield [val, *rests]
        index += 1
      end
    end
  end

  class Iterator
    STACK_SIZE = 1_048_576

    attr_reader :result

    def initialize(obj)
      @object = obj
      rewind
    end

    private :initialize

    def next?
      !@done
    end

    def next
      reset unless @fiber

      val = @fiber.resume

      raise StopIteration, "iteration has ended" if @done

      return val
    end

    def rewind
      @fiber = nil
      @done = false
    end

    def reset
      @done = false
      @fiber = Fiber.new stack_size: STACK_SIZE do
        obj = @object
        @result = obj.each { |*val| Fiber.yield *val }
        @done = true
      end
    end
  end
end