Report abuse

class Ematcher < BlankSlate
  class Placeholder
    attr :name

    def initialize(name); @name = name; end
  end

  module AnyMany
    class Any; end
    class Many; end

    def _; Any; end
    def __; Many; end
  end 

  extend AnyMany
  include AnyMany

  def method_missing(sym, *args)
    case args.length
    when 0 then
      case @mode
      when :build then Placeholder.new(sym)
      when :match then super
      when :yield then match(sym)
      else             super
      end
    when 1 then
      return super unless @mode == :yield && ((name = sym.to_s).last == "=")
      name = name.chop.to_sym
      return super unless @match.key?(name)
      @match[name] = args[0]
    else
      super
    end
  end

  private

  def match(sym)
    raise ArgumentError, "No match #{sym}" unless @match.key?(sym)
    @match[sym]
  end

  def in_mode(mode, &block)
    begin
      @mode = mode
      yield
    ensure
      @mode = :build
    end
  end

  public

  def on(pattern, &block)
    return if @match

    @match = in_mode(:match) do Ematcher.match(pattern, input) end
    in_mode(:yield, &block) if @match
  end

  attr :input

  def self.match(pattern, input, env = {})
    return env if pattern == __
    return env if pattern == _

    if pattern.is_a?(Placeholder)
      return nil if env.key?(pattern.name) && env[pattern.name] != input
      return env.update(pattern.name => input)
    end

    if input.is_a?(Array) && pattern.is_a?(Array)
      if pattern.length != input.length
        return nil if pattern.length < input.length
        return nil if pattern.length > input.length+1
        return nil if pattern.last != __
      end

      return pattern.zip(input).inject(env) do |env, (p, i)|
        env && match(p, i, env)
      end
    end

    input == pattern && env
  end

  def initialize(input)
    @input = input.dup.freeze
  end

  def matching(&block)
    @mode = :build
    Proc.new.bind(self).call
    @match
  end
end

def matching!(input, &block)
  matching(input, &block) || raise(ArgumentError, "No match for #{input.inspect}")
end

def matching(input, &block)
  Ematcher.new(input).matching(&block)
end

m = matching! [ 1, [ :a, 2 ], 3 ] do
  on [ a ]                do
                            puts [ "1", a ].inspect
                          end
  on [ a, _, a ]          do
                            puts [ "2", a ].inspect
                          end
  on [ a, [ :a, 3 ], z ]  do
                            puts [ "3", a, z ].inspect
                          end
  on [ a, [ b, 2 ], z ]   do
                            puts [ "4", a, b, z ].inspect
                            self.z = :z
                          end
  on [ a, [ :a, 2 ] , z ] do
                            puts [ "5", a, z ].inspect
                          end
end

puts m.inspect