#!/usr/bin/ruby -w
# proof of concept: static typed variables and functions
# http://www.xs4all.nl/~hipster

# TODO
# - problem: how to pass params into the self.class.const_get("").call,
#   the receiving block (specified with the tdef) should have the following
#   form: tdef [:f, String, [[:p1, String]]] do |p1| body; end
#   Resurrect the original Binding instance? Use class variables?
# - a more readable declaration syntax
# - void parameter
# - parametrize attribute reader/writer generation

# passing multiple procs for pre/postconditions:
# def mcall(*fs)
#   fs.each{ |f| f.call }
# end
# mcall lambda { p 1 }, lambda { p 2 }, lambda { p 3 }
# or:
# mcall proc { pre }, proc { body }, proc { post }
# default values for pre/post:
# mcall proc { body }, proc { pre } = { true }, proc { post } = { true }

class TypeMismatch < StandardError
  def initialize msg
    super "Type mismatch: " + msg
  end
end

class Module
  # usage: tattr [:name, Type], ...
  def tattr(*attrs)
    for attr in attrs
      name = attr[0].id2name
      type = attr[1]
      # generate attribute getter/setter and type checks
      class_eval %!
        def #{name}
          @#{name}
        end
        def #{name}= value
          unless value.is_a? #{type}
            fail TypeMismatch.new(
              "attr #{self.to_s}::#{name}: expected #{type}, got " +
              value.class.to_s)
          end
          @#{name} = value
        end
      !
    end
  end

  # usage: tdef [:fname, rtype, [[:param, Type], ...]] do body; end
  # note: functions must explicitly return a type, e.g. nil
  def tdef signature, &body
    fname = signature[0].id2name
    rtype = signature[1].nil? ? NilClass : signature[1]
    const_set "#{self}_#{fname}", body
    code = "def #{fname} "
    # generate parameter type checks
    parnames = [] # for generating .call() later on
    if signature[2]
      code += signature[2].collect{ |par|
        parnames << par[0].id2name
        par[0].id2name
      }.join(", ")
      code += "\n" + signature[2].collect{ |p|
        %!  unless #{p[0].id2name}.is_a? #{p[1]}\n! +
        %!    fail TypeMismatch.new("func #{self}::#{fname} ! +
        %!param #{p[0].id2name}: expected #{p[1].to_s}, got " + ! +
        %!#{p[0].id2name}.class.to_s)\n! +
        %!  end!
      }.join("\n")
    else
      # void param: f(void)
      code += "(*args)" #\n  fail 'nonvoidbarfout' if args"
    end
    # generate function body and return type check
    code += %!\n! +
      #%!  puts "#{fname} precondition"\n! +
      %!  begin\n! +
      %!    _result = self.class.const_get("_#{self}_#{fname}").! +
      %!call(#{parnames.join(", ")})\n! +
      %!  ensure\n! +
      #%!    puts "#{fname} postcondition"\n! +
      %!  end\n! +
      %!  unless _result.is_a? #{rtype}\n! +
      %!    fail TypeMismatch.new("func #{self.to_s}::#{fname} retval: ! +
      %!expected #{rtype.to_s}, got " + _result.class.to_s)\n! +
      %!  end\n! +
      %!  _result\n! +
      %!end!
    puts code
    class_eval code
    puts "-" * 75
  end
end

class Foo
  # tattr: typed attribute
  # C: String str, Fixnum int, Foo nephew
  tattr [:str, String], [:int, Fixnum], [:nephew, Foo]

  # tdef: typed def
  # C: String f1()
  tdef([:f1, String]) { "f1.body" }

  # C: String f2(String p1, Fixnum p2)
  tdef [:f2, String, [[:p1, String], [:p2, Fixnum]]] do "f2.body"; end

  # C: String f3(String txt, Fixnum iter)
  tdef [:f3, String, [[:txt, String], [:iter, Fixnum]]] do
    #iter * txt # FIXME barfs, params aren't passed in
    "f3.body"
  end

  # C: String f4(void)
  tdef [:f4, String] do end             # barfs on purpose

  # C: void f(void)
  tdef [:f5, nil] do end                # ne pas de barf

  def foo
    nephew.bar                          # relies on correct type of nephew
  end

  def bar
    puts "hi from #{self.class}"
  end

  def typed_funcs
    p f1
    p f2("hopsa", 1)
    p f2("baz", Foo.new) rescue excp    # Foo should be Fixnum
    p f3("abc", 4)
    p f3(Bar.new, 4) rescue excp        # Bar should be String
    #p f4 "aap"  FIXME `f4': wrong # of arguments(1 for 0) (ArgumentError)
    p f4 rescue excp                    # return value should be Array
    p f5
  end
end

class Bar; end
class Qux < Foo; end

def excp
  puts $!
end

z = Foo.new
z.str = "hopsa"
z.str = 42 rescue excp                  # should be String
z.int = 42
z.int = "42" rescue excp                # should be Fixnum
z.nephew = Foo.new
z.nephew = Qux.new                      # fine, as Qux < Foo
z.nephew = Bar.new rescue excp          # should be Foo
z.foo
z.typed_funcs
