#
# si.rb - Sather-like iterators for Ruby
#
# Author: Oren Ben-Kiki 2006
#
# == Overview
#
# This file extends Ruby with very basic support for sather-like iterators.
# Simply use "si_loop" instead of "loop" and prefix each iterator call with
# ".si".
#
# === Example
#
#   a = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
#   h = { :A => :a, :B => :b, :C => :c, :D => :d }
#   si_loop do
#     puts "---"
#     v1 = a.si(:foo1).each
#     v2 = a.si(:foo1).each
#     p [ v1, v2 ]
#     p a.si.each
#     p h.si.each
#   end
#
# Produces:
#
#   ---
#   [0, 1]
#   0
#   [:D, :d]
#   ---
#   [2, 3]
#   1
#   [:A, :a]
#   ---
#   [4, 5]
#   2
#   [:B, :b]
#   ---
#   [6, 7]
#   3
#   [:C, :c]
#   ---
#   [8, 9]
#   4

# Context for Sather iteration loops.
#
# This class is not meant to be used directly. See the documentation for si.rb
# for an overview.
class SiContext

  # Push a new context on the stack when a new loop begins.
  def SiContext.push
    @@maps ||= []
    @@maps.push({})
  end

  # Pop the current context from the stack when a loop ends.
  def SiContext.pop
    @@maps.pop
  end

  # Access an SiInterator in the current scope. The +id+ uniquely identifies
  # the iterator instance, and +object+ is the one that has the iterator
  # method that will be wrapped.
  def SiContext.iterator(id, object)
    @@maps.last[id] ||= SiIterator.new(object)
  end

end

# Wrap a Ruby iterator for a Sather iteration loop.
#
# This class is not meant to be used directly. Instances are created by calls
# to SiContext.iterator. See the documentation for si.rb for an overview.
class SiIterator

  # Create a new iterator wrapper.
  #
  # +object+ is the object that provides the Ruby iterator method.
  def initialize(object)
    @object = object
    @is_first_fetch = true
    @to_call_out = true
    @to_call_in = false
  end

  # Capture a call to a ruby iterator.
  #
  # It is implicitly assumed that the missing method is actually the call
  # to the Ruby iterator meant for the original object. As usual, +symbol+ is
  # the missing method name, and +args+ are the arguments passed to it.
  def method_missing(symbol, *args)
    si_fetch { |b| @object.send(symbol, *args, &b) }
  end

  # Fetch the next value from a Ruby iterator.
  def si_fetch
    callcc { |@out_cont| }
    if @is_first_fetch
      @is_first_fetch = false
      yield Proc.new { |value| si_capture(value) }
      si_break
    elsif @to_call_in
      @to_call_in = false
      @in_cont.call
    else
      @to_call_in = true
    end
    @value
  end

  # Capture the value yielded by a Ruby iterator.
  def si_capture(v)
    @value = v
    callcc { |@in_cont| }
    if @to_call_out
      @to_call_out = false
      @out_cont.call
    else
      @to_call_out = true
    end
  end

end

module Kernel

  # Return a Sather iterator wrapper for an iterator method. The optional +id+
  # parameter identifies the iterator. If it is not provided, the source file
  # and line number of the call serve as a unique id. This means that two calls
  # in the same line for the same object will be treated as a single iterator
  # unless explicitly given explicit +id+.
  def si(id = caller[0])
    SiContext.iterator(id, self)
  end

  # Begin a Sather iterator loop.
  def si_loop(&block)
    SiContext.push
    catch(:si_break) { loop(&block) }
  ensure
    SiContext.pop
  end

  # Break a Sather iteration loop. A normal break statement will also work, but
  # this one will work even if called from a sub-function, which may be useful.
  def si_break
    throw :si_break
  end

end
