デコレータRubyの作り方

長いけど例によって基本コピペ。相変わらず一度動けばおkって感じで結構ひどいですね。

大枠は

  1. node.hで
    1. RNodeにnd_annotationsていうの追加して
      struct RNode *nd_annotations;
  2. parse.yで
    1. @{...} をデコレータ?とみなして
      annotation : '@' tLBRACE method_call '}'
    2. ステートメントでデコレータ?を受け入れるようにして
      stmt : annotation stmt
  3. compile.cで
    1. デコレータのあるメソッド定義は、メソッド定義後にデコレータを「self」と「定義されたメソッド名」を引数に加えて呼び出す(例えば"def do_something"の前に@{ReturnType(String)}ってあったらdo_something定義後にReturnType(self, 'do_something', String)というメソッド呼び出しを追加)
  4. prelude.rbで
    1. デフォルトのデコレータ(ReturnType, ParamTypes)を実装

見ての通り最初はデコレータじゃなくてアノテーションにしたくて結局諦めました。メソッドをデコレートするだけならcompile.cに書いてある処理をparse.yに持ってきてもう少し単純にできたはず。

Common subdirectories: ruby-1.9.3-p0/bcc32 and annotated_ruby/bcc32
Common subdirectories: ruby-1.9.3-p0/benchmark and annotated_ruby/benchmark
Common subdirectories: ruby-1.9.3-p0/bin and annotated_ruby/bin
Common subdirectories: ruby-1.9.3-p0/bootstraptest and annotated_ruby/bootstraptest
diff -c ruby-1.9.3-p0/compile.c annotated_ruby/compile.c
*** ruby-1.9.3-p0/compile.c     2011-07-10 12:11:52.000000000 +0900
--- annotated_ruby/compile.c    2011-12-24 10:02:33.000000000 +0900
***************
*** 265,270 ****
--- 265,274 ----
    (debug_compile("== " desc "\n", \
                   iseq_compile_each(iseq, (anchor), (node), (poped))))
  
+ #define COMPILE_ANNOTATIONS(anchor, desc, node, recv, target) \
+   (debug_compile("== " desc "\n", \
+                  iseq_compile_annotations(iseq, (anchor), (node), (recv), (target))))
+ 
  #define OPERAND_AT(insn, idx) \
    (((INSN*)(insn))->operands[(idx)])
  
***************
*** 332,337 ****
--- 336,342 ----
  static ADJUST *new_adjust_body(rb_iseq_t *iseq, LABEL *label, int line);
  
  static int iseq_compile_each(rb_iseq_t *iseq, LINK_ANCHOR *anchor, NODE * n, int);
+ static int iseq_compile_annotations(rb_iseq_t *iseq, LINK_ANCHOR *anchor, NODE * n, NODE * recv, NODE * target);
  static int iseq_setup(rb_iseq_t *iseq, LINK_ANCHOR *anchor);
  static int iseq_optimize(rb_iseq_t *iseq, LINK_ANCHOR *anchor);
  static int iseq_insns_unification(rb_iseq_t *iseq, LINK_ANCHOR *anchor);
***************
*** 4618,4623 ****
--- 4623,4630 ----
        ADD_INSN1(ret, nd_line(node), putobject, ID2SYM(node->nd_mid));
        ADD_INSN1(ret, nd_line(node), putiseq, iseqval);
        ADD_SEND (ret, nd_line(node), ID2SYM(id_core_define_method), INT2FIX(3));
+       NODE *method_name = NEW_STR(rb_sprintf("%s", rb_id2name(node->nd_mid)));
+       COMPILE_ANNOTATIONS(ret, "annotations", node->nd_annotations, NULL, method_name);
  
        if (poped) {
            ADD_INSN(ret, nd_line(node), pop);
***************
*** 4638,4643 ****
--- 4645,4652 ----
        ADD_INSN1(ret, nd_line(node), putobject, ID2SYM(node->nd_mid));
        ADD_INSN1(ret, nd_line(node), putiseq, iseqval);
        ADD_SEND (ret, nd_line(node), ID2SYM(id_core_define_singleton_method), INT2FIX(3));
+       NODE *method_name = NEW_STR(rb_sprintf("%s", rb_id2name(node->nd_mid)));
+       COMPILE_ANNOTATIONS(ret, "annotations", node->nd_annotations, node->nd_recv, method_name);

        if (poped) {
            ADD_INSN(ret, nd_line(node), pop);
***************
*** 4689,4694 ****
--- 4698,4706 ----
        ADD_INSN3(ret, nd_line(node), defineclass,
                  ID2SYM(node->nd_cpath->nd_mid), iseqval, INT2FIX(noscope ? 3 : 0));

+       NODE *class_name = NEW_STR(rb_sprintf("%s", rb_id2name(node->nd_cpath->nd_mid)));
+       COMPILE_ANNOTATIONS(ret, "annotations", node->nd_annotations, NULL, class_name);
+ 
        if (poped) {
            ADD_INSN(ret, nd_line(node), pop);
        }
***************
*** 5039,5044 ****
--- 5051,5117 ----
      return COMPILE_OK;
  }

+ /* concat two lists */ // copy from parse.y
+ static NODE*
+ list_concat_gen(NODE *head, NODE *tail)
+ {
+     NODE *last;
+ 
+     if (head->nd_next) {
+       last = head->nd_next->nd_end;
+     }
+     else {
+       last = head;
+     }
+ 
+     head->nd_alen += tail->nd_alen;
+     last->nd_next = tail;
+     if (tail->nd_next) {
+       head->nd_next->nd_end = tail->nd_next->nd_end;
+     }
+     else {
+       head->nd_next->nd_end = tail;
+     }
+ 
+     return head;
+ }
+ static int
+ iseq_compile_annotations(rb_iseq_t *iseq, LINK_ANCHOR *ret, NODE * vals, NODE * r, NODE *target)
+ {
+     if (vals != NULL) {
+       do {
+           NODE *val = vals->nd_head;
+           // copy from NODE_FCALL/NODE_VCALL
+           DECL_ANCHOR(recv);
+           DECL_ANCHOR(args);
+           VALUE argc;
+           VALUE flag = 0;
+           VALUE parent_block = iseq->compile_data->current_block;
+           iseq->compile_data->current_block = Qfalse;
+       
+           INIT_ANCHOR(recv);
+           INIT_ANCHOR(args);
+           ADD_CALL_RECEIVER(recv, nd_line(val));
+       
+           if (val->nd_args == NULL) {
+               val->nd_args = NEW_LIST(target);
+           }
+           else {
+               val->nd_args = list_concat_gen(NEW_LIST(target), val->nd_args);
+           }
+           val->nd_args = list_concat_gen(NEW_LIST(r), val->nd_args);
+       
+           argc = setup_args(iseq, args, val->nd_args, &flag);
+           ADD_SEQ(ret, recv);
+           ADD_SEQ(ret, args);
+           flag |= VM_CALL_FCALL_BIT;
+           ADD_SEND_R(ret, nd_line(val), ID2SYM(val->nd_mid),
+           argc, parent_block, LONG2FIX(flag));
+           ADD_INSN(ret, nd_line(val), pop);
+       } while ((vals = vals->nd_next) != NULL);
+     }
+ }
+ 
  /***************************/
  /* instruction information */
  /***************************/
Common subdirectories: ruby-1.9.3-p0/cygwin and annotated_ruby/cygwin
Common subdirectories: ruby-1.9.3-p0/defs and annotated_ruby/defs
Common subdirectories: ruby-1.9.3-p0/doc and annotated_ruby/doc
Common subdirectories: ruby-1.9.3-p0/enc and annotated_ruby/enc
Common subdirectories: ruby-1.9.3-p0/ext and annotated_ruby/ext
Common subdirectories: ruby-1.9.3-p0/include and annotated_ruby/include
Common subdirectories: ruby-1.9.3-p0/lib and annotated_ruby/lib
Common subdirectories: ruby-1.9.3-p0/man and annotated_ruby/man
Common subdirectories: ruby-1.9.3-p0/misc and annotated_ruby/misc
Common subdirectories: ruby-1.9.3-p0/missing and annotated_ruby/missing
diff -c ruby-1.9.3-p0/node.h annotated_ruby/node.h
*** ruby-1.9.3-p0/node.h        2011-06-18 07:43:38.000000000 +0900
--- annotated_ruby/node.h       2011-12-24 10:02:49.000000000 +0900
***************
*** 235,240 ****
--- 235,241 ----
  typedef struct RNode {
      VALUE flags;
      VALUE nd_reserved;                /* ex nd_file */
+     struct RNode *nd_annotations;
      union {
        struct RNode *node;
        ID id;
diff -c ruby-1.9.3-p0/parse.y annotated_ruby/parse.y
*** ruby-1.9.3-p0/parse.y       2011-09-24 14:21:19.000000000 +0900
--- annotated_ruby/parse.y      2011-12-24 10:02:56.000000000 +0900
***************
*** 703,708 ****
--- 703,709 ----
  %type <node> mlhs mlhs_head mlhs_basic mlhs_item mlhs_node mlhs_post mlhs_inner
  %type <id>   fsym keyword_variable user_variable sym symbol operation operation2 operation3
  %type <id>   cname fname op f_rest_arg f_block_arg opt_f_block_arg f_norm_arg f_bad_arg
+ %type <node> annotation
  /*%%%*/
  /*%
  %type <val> program reswords then do dot_or_colon
***************
*** 952,958 ****
                    }
                ;

! stmt          : keyword_alias fitem {lex_state = EXPR_FNAME;} fitem
                    {
                    /*%%%*/
                        $$ = NEW_ALIAS($2, $4);
--- 953,973 ----
                    }
                ;

! stmt          : annotation stmt
!                   {
!                   /*%%%*/
!                       $$ = $2;
!                       if ($$->nd_annotations != NULL) {
!                           $$->nd_annotations = list_append($$->nd_annotations, $1);
!                       }
!                       else {
!                           $$->nd_annotations = NEW_LIST($1);
!                       }
!                   /*%
!                       $$ = $2;
!                   %*/
!                   }
!               | keyword_alias fitem {lex_state = EXPR_FNAME;} fitem
                    {
                    /*%%%*/
                        $$ = NEW_ALIAS($2, $4);
***************
*** 4813,4818 ****
--- 4828,4851 ----
                    }
                ;

+ annotation    : '@' tLBRACE method_call '}'
+                   {
+                   /*%%%*/
+                       $$ = $3;
+                   /*%
+                       //$$ = dispatch2(assoc_new, $1, $2);
+                   %*/
+                   }
+               | annotation term
+                   {
+                   /*%%%*/
+                       $$ = $1;
+                   /*%
+                       //$$ = dispatch2(assoc_new, $1, $2);
+                   %*/
+                   }
+               ;
+ 
  operation     : tIDENTIFIER
                | tCONSTANT
                | tFID
diff -c ruby-1.9.3-p0/prelude.rb annotated_ruby/prelude.rb
*** ruby-1.9.3-p0/prelude.rb    2011-05-15 20:55:52.000000000 +0900
--- annotated_ruby/prelude.rb   2011-12-24 10:03:18.000000000 +0900
***************
*** 29,31 ****
--- 29,117 ----
      }
    end
  end
+ 
+ module Annotations
+   class AnnotationTypeError < RuntimeError; end
+ 
+   def annotation_params(receiver, method_name)
+     method = (receiver.nil? ? self : class <<self; self end).instance_method method_name
+     params = (0...method.arity.abs).to_a.map{|i| "v#{i}"}.to_a
+     params[-1] = "*#{params[-1]}" if method.arity < 0
+     params
+   end
+ 
+   def annotation_chain(receiver, method_name, function_name)
+     return if method_name =~ /.*_with(out)?_.*/
+ 
+     (receiver.nil? ? self : class <<self; self end).instance_eval do
+       alias_method "#{method_name}_without_#{function_name}", method_name
+       alias_method method_name, "#{method_name}_with_#{function_name}"
+     end
+   end
+ 
+   def ReturnType(receiver, method_name, type)
+     params = annotation_params(receiver, method_name).join ','
+ 
+     type_check = 
+       case type
+       when Array
+         type.map do |t| 
+           "raise AnnotationTypeError.new('No Method: #{t}') " \
+             "unless ret.respond_to? #{t.inspect}"
+         end.join("\n")
+       when Symbol  
+         "raise AnnotationTypeError.new('No Method: #{type}') " \
+           "unless ret.respond_to? #{type.inspect}"
+       else
+         "raise AnnotationTypeError.new('Invalid Class: #{type}') " \
+           "unless ret.is_a? #{type}"
+       end
+ 
+     eval <<-EOS
+       def #{method_name}_with_returntype(#{params})
+         ret = #{method_name}_without_returntype(#{params})
+         #{type_check}
+         ret
+       end
+     EOS
+ 
+     annotation_chain receiver, method_name, 'returntype'
+   end
+ 
+   def ParamTypes(receiver, method_name, *types)
+     params = annotation_params receiver, method_name
+     type_check = []
+     params.zip(types) do |p, t|
+       p = $1 if p =~ /^\*(.+)/
+       type_check.push(
+         case t
+         when Symbol
+           "raise AnnotationTypeError.new('No Method: #{t}') " \
+             "unless #{p}.respond_to? #{t.inspect}"
+         when Array
+           t.map do |tt| 
+             "raise AnnotationTypeError.new('No Method: #{tt}') " \
+               "unless #{p}.respond_to? #{tt.inspect}"
+           end 
+         when Class
+           "raise AnnotationTypeError.new('Invalid Class: #{t}') " \
+             "unless #{p}.is_a? #{t}"
+         end
+       )
+     end
+ 
+     eval <<-EOS
+       def #{method_name}_with_paramtypes(#{params.join(',')})
+         #{type_check.flatten.join("\n")}
+         #{method_name}_without_paramtypes(#{params.join(',')})
+       end
+     EOS
+ 
+     annotation_chain receiver, method_name, 'paramtypes'
+   end
+ end
+ 
+ class Object
+   include Annotations 
+ end
+ 
Common subdirectories: ruby-1.9.3-p0/sample and annotated_ruby/sample
Common subdirectories: ruby-1.9.3-p0/spec and annotated_ruby/spec
Common subdirectories: ruby-1.9.3-p0/symbian and annotated_ruby/symbian
Common subdirectories: ruby-1.9.3-p0/template and annotated_ruby/template
Common subdirectories: ruby-1.9.3-p0/test and annotated_ruby/test
Common subdirectories: ruby-1.9.3-p0/tool and annotated_ruby/tool
Common subdirectories: ruby-1.9.3-p0/win32 and annotated_ruby/win32