multimethod 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. data/.svn/README.txt +2 -0
  2. data/.svn/empty-file +0 -0
  3. data/.svn/entries +68 -0
  4. data/.svn/format +1 -0
  5. data/.svn/text-base/ChangeLog.svn-base +3 -0
  6. data/.svn/text-base/Manifest.txt.svn-base +54 -0
  7. data/.svn/text-base/README.txt.svn-base +40 -0
  8. data/.svn/text-base/Rakefile.svn-base +132 -0
  9. data/.svn/text-base/Releases.txt.svn-base +7 -0
  10. data/ChangeLog +3 -0
  11. data/Manifest.txt +54 -0
  12. data/README.txt +40 -0
  13. data/Rakefile +132 -0
  14. data/Releases.txt +7 -0
  15. data/examples/.svn/README.txt +2 -0
  16. data/examples/.svn/empty-file +0 -0
  17. data/examples/.svn/entries +22 -0
  18. data/examples/.svn/format +1 -0
  19. data/examples/.svn/props/ex1.rb.svn-work +5 -0
  20. data/examples/ex1.rb +26 -0
  21. data/lib/.svn/README.txt +2 -0
  22. data/lib/.svn/empty-file +0 -0
  23. data/lib/.svn/entries +24 -0
  24. data/lib/.svn/format +1 -0
  25. data/lib/.svn/text-base/multimethod.rb.svn-base +8 -0
  26. data/lib/multimethod.rb +8 -0
  27. data/lib/multimethod/.svn/README.txt +2 -0
  28. data/lib/multimethod/.svn/empty-file +0 -0
  29. data/lib/multimethod/.svn/entries +53 -0
  30. data/lib/multimethod/.svn/format +1 -0
  31. data/lib/multimethod/.svn/text-base/core_extensions.rb.svn-base +38 -0
  32. data/lib/multimethod/.svn/text-base/method.rb.svn-base +232 -0
  33. data/lib/multimethod/.svn/text-base/multimethod.rb.svn-base +171 -0
  34. data/lib/multimethod/.svn/text-base/parameter.rb.svn-base +64 -0
  35. data/lib/multimethod/.svn/text-base/table.rb.svn-base +99 -0
  36. data/lib/multimethod/core_extensions.rb +38 -0
  37. data/lib/multimethod/method.rb +232 -0
  38. data/lib/multimethod/multimethod.rb +171 -0
  39. data/lib/multimethod/parameter.rb +64 -0
  40. data/lib/multimethod/table.rb +99 -0
  41. data/test/.svn/README.txt +2 -0
  42. data/test/.svn/empty-file +0 -0
  43. data/test/.svn/entries +53 -0
  44. data/test/.svn/format +1 -0
  45. data/test/.svn/text-base/method_test.rb.svn-base +89 -0
  46. data/test/.svn/text-base/multimethod_test.rb.svn-base +92 -0
  47. data/test/.svn/text-base/parameter_test.rb.svn-base +31 -0
  48. data/test/.svn/text-base/test_base.rb.svn-base +25 -0
  49. data/test/.svn/text-base/usage_test.rb.svn-base +146 -0
  50. data/test/method_test.rb +89 -0
  51. data/test/multimethod_test.rb +92 -0
  52. data/test/parameter_test.rb +31 -0
  53. data/test/test_base.rb +25 -0
  54. data/test/usage_test.rb +146 -0
  55. metadata +108 -0
@@ -0,0 +1,171 @@
1
+ module Multimethod
2
+ class Multimethod
3
+
4
+ attr_accessor :name
5
+ attr_accessor :method
6
+ attr_accessor :table
7
+
8
+ def initialize(name, *opts)
9
+ raise NameError, "multimethod name not specified" unless name && name.to_s.size > 0
10
+
11
+ @name = name
12
+ @name_i = 0
13
+ @method = [ ]
14
+ @dispatch = { }
15
+
16
+ @lookup_method = { }
17
+ end
18
+
19
+
20
+ def gensym(name = nil)
21
+ name ||= @name
22
+ "_multimethod_#{@name_i = @name_i + 1}_#{name}"
23
+ end
24
+
25
+
26
+ def new_method(mod, *args)
27
+ m = Method.new(mod, gensym, *args)
28
+ add_method(m)
29
+ m
30
+ end
31
+
32
+
33
+ def add_method(method)
34
+ # THREAD CRITICAL BEGIN
35
+ @method.push(method)
36
+ method.multimethod = self
37
+ @lookup_method = { } # flush cache
38
+ # THREAD CRITICAL END
39
+ end
40
+
41
+
42
+ def dispatch(rcvr, args)
43
+ apply_method(lookup_method(rcvr, args), rcvr, args)
44
+ end
45
+
46
+
47
+ # Interface to Multimethod::Table
48
+ def apply_method(meth, rcvr, args)
49
+ unless meth # && false
50
+ $stderr.puts "Available multimethods for #{rcvr.class.name}##{@name}(#{args}):"
51
+ $stderr.puts " " + @method.sort{|a,b| a.min_args <=> b.min_args }.collect{|x| x.to_s(name)}.join("\n ")
52
+ $stderr.puts "\n"
53
+ end
54
+ raise NameError, "Cannot find multimethod for #{rcvr.class.name}##{@name}(#{args})" unless meth
55
+ rcvr.send(meth.name, *args)
56
+ end
57
+
58
+
59
+ def lookup_method(rcvr, args)
60
+ args = args.clone
61
+ args.unshift(rcvr)
62
+ lookup_method_cached_(args)
63
+ end
64
+
65
+
66
+ def lookup_method_cached_(args)
67
+ args_type = args.collect{|x| x.class}
68
+
69
+ # THREAD CRITICAL BEGIN
70
+ unless result = @lookup_method[args_type]
71
+ result = @lookup_method[args_type] =
72
+ lookup_method_(args_type)
73
+ end
74
+ # THREAD CRITICAL END
75
+
76
+ result
77
+ end
78
+
79
+
80
+ def lookup_method_(args)
81
+ scores = score_methods(@method, args)
82
+ if scores.empty?
83
+ result = nil
84
+ else
85
+ result = scores[0][1]
86
+ raise("Ambigious method") if scores.select{|x| x[0] == result}.size > 1
87
+ end
88
+
89
+ #if @name.to_s == 'bar'
90
+ # $stderr.puts "args = " + args.collect{|x| x.class.name + ' ' + x.to_s}.join(", ")
91
+ # $stderr.puts "scores:\n " + scores.collect{|x| x.inspect}.join("\n ")
92
+ # end
93
+
94
+
95
+ result
96
+ end
97
+
98
+ def score_methods(meths, args)
99
+ scores = meths.collect do |meth|
100
+ score = meth.score_cached(args)
101
+ if score
102
+ score = [ score, meth ]
103
+ else
104
+ score = nil
105
+ end
106
+
107
+ score
108
+ end
109
+
110
+ scores.compact!
111
+ scores.sort!
112
+
113
+ # $stderr.puts "score_methods(#{args.inspect}) => \n#{scores.inspect}"
114
+
115
+ scores
116
+ end
117
+
118
+
119
+ def install_dispatch(mod)
120
+ # THREAD CRITICAL BEGIN
121
+ unless @dispatch[mod]
122
+ @dispatch[mod] = true
123
+ # $stderr.puts "install_dispatch(#{name}) into #{mod}\n";
124
+ mod.class_eval(body = <<-"end_eval", __FILE__, __LINE__)
125
+ def #{name}(*args)
126
+ ::#{table.class.name}.instance.dispatch(#{name.inspect}, self, args)
127
+ end
128
+ end_eval
129
+ # $stderr.puts "install_dispatch = #{body}"
130
+ end
131
+ # THREAD CRITICAL END
132
+ end
133
+
134
+
135
+ ##################################################
136
+ # Support
137
+ #
138
+
139
+ @@name_map = {
140
+ '@' => 'AT',
141
+ '=' => 'EQ',
142
+ '<' => 'LT',
143
+ '>' => 'GT',
144
+ '+' => 'ADD',
145
+ '-' => 'SUB',
146
+ '*' => 'MUL',
147
+ '/' => 'DIV',
148
+ '%' => 'MOD',
149
+ '^' => 'XOR',
150
+ '|' => 'OR',
151
+ '&' => 'AND',
152
+ '!' => 'NOT',
153
+ '~' => 'TIL',
154
+ nil => nil
155
+ };
156
+ @@name_map.delete(nil)
157
+
158
+ @@name_rx = Regexp.new('(' + @@name_map.keys.collect{|x| Regexp.quote(x)}.join('|') + ')')
159
+
160
+ def self.normalize_name(name)
161
+ name = name.to_s.clone
162
+ name.sub!(@@name_rx){|x| "_#{@@name_map[x] || '_'}_"}
163
+
164
+ name.intern
165
+ end
166
+
167
+ end # class
168
+
169
+ end # module
170
+
171
+
@@ -0,0 +1,64 @@
1
+ module Multimethod
2
+ class Parameter
3
+ RESTARG_SCORE = 9999
4
+
5
+ attr_accessor :name
6
+ attr_accessor :i
7
+ attr_accessor :type
8
+ attr_accessor :default
9
+ attr_accessor :restarg
10
+
11
+ attr_accessor :method
12
+
13
+ def initialize(name, type = nil, default = nil, restarg = false)
14
+ # $stderr.puts "initialize(#{name.inspect}, #{type}, #{restarg.inspect})"
15
+ name = name.to_s
16
+ if name.sub!(/^\*/, '')
17
+ restarg = true
18
+ end
19
+
20
+ name = name.intern unless name.kind_of?(Symbol)
21
+ @name = name
22
+ @i = nil
23
+ @type = type || Kernel
24
+ @default = default
25
+ @restarg = restarg
26
+ end
27
+
28
+
29
+ def score(arg)
30
+ return RESTARG_SCORE if @restarg
31
+ score = all_types(arg).index(type_object)
32
+ end
33
+
34
+
35
+ def all_types(arg)
36
+ arg.ancestors
37
+ end
38
+
39
+
40
+ def type_object
41
+ if @type.kind_of?(String)
42
+ @type = @method.multimethod.table.name_to_object(@type, @method.mod, @method)
43
+ end
44
+ @type
45
+ end
46
+
47
+
48
+ def to_s
49
+ @restarg ? "*#{@name}" : @name
50
+ end
51
+
52
+
53
+ def to_s_long
54
+ "#{@type} #{to_ruby}"
55
+ end
56
+
57
+
58
+ def to_ruby
59
+ "#{to_s}#{@default ? ' = ' + @default : ''}"
60
+ end
61
+
62
+ end # class
63
+ end # module
64
+
@@ -0,0 +1,99 @@
1
+ module Multimethod
2
+ class Table
3
+
4
+ @@instance = nil
5
+ def self.instance
6
+ # THREAD CRITICAL BEGIN
7
+ @@instance ||= self.new
8
+ # TRREAD CRITICAL END
9
+ end
10
+
11
+ @@hook_var = :@@__multimethods
12
+
13
+ def initialize(*opts)
14
+ @method = { }
15
+
16
+ # Type name lookup cache
17
+ @name_to_object = { }
18
+ end
19
+
20
+
21
+ @@match_def = /^\s*def\s+(\w+)([(](.*)[)])?/
22
+ # Interface to Multimethod::Module mixin multimethod
23
+ def install_method(mod, body, file = nil, line = nil)
24
+ name = nil
25
+ params = nil
26
+
27
+ # Parse first line.
28
+ if md = body.match(@@match_def)
29
+ name = md[1]
30
+ params = md[3] || ''
31
+ else
32
+ raise("Cannot determine name(params) from body")
33
+ end
34
+
35
+ # Get our Multimethod for this
36
+ mm = lookup_multimethod(name)
37
+ mm.install_dispatch(mod)
38
+ m = mm.new_method(mod, params)
39
+
40
+ # Evaluate our method.
41
+ new_body = body.clone
42
+ new_body.sub!(@@match_def, m.to_ruby)
43
+
44
+ # if m.restarg
45
+ # $stderr.puts "install_method(#{mod}) => #{m.to_ruby}:\n#{new_body}"
46
+ # end
47
+
48
+ file ||= __FILE__; line ||= __LINE__
49
+ m.file = file; m.line = line
50
+ mod.module_eval(new_body, file || __FILE__, line || __LINE__)
51
+ end
52
+
53
+
54
+ def lookup_multimethod(name)
55
+ name = name.to_s
56
+
57
+ # THREAD CRITICAL BEGIN
58
+ unless mm = @method[name]
59
+ mm = Multimethod.new(name)
60
+ mm.table = self
61
+ @method[name] = mm
62
+ end
63
+ # THREAD CRITICAL END
64
+
65
+ mm
66
+ end
67
+
68
+
69
+ # Interface to code generated by #install_dispatch.
70
+ def dispatch(name, rcvr, args)
71
+ unless mm = @method[name]
72
+ raise NameError, 'No method for multmethod #{name}' unless mm
73
+ end
74
+ mm.dispatch(rcvr, args)
75
+ end
76
+
77
+
78
+ #################################################
79
+ # Support
80
+ #
81
+
82
+ def name_to_object(name, scope = nil, method = nil)
83
+ scope ||= Kernel
84
+ # THREAD CRITICAL
85
+ unless x = (@name_to_object[scope] ||= { })[name]
86
+ # $stderr.puts " name_to_object(#{name.inspect}) in #{scope}"
87
+ x =
88
+ @name_to_object[scope][name] =
89
+ scope.module_eval(name, method ? method.file : __FILE__, method ? method.line : __LINE__)
90
+ end
91
+
92
+ x
93
+ end
94
+
95
+ end # class
96
+
97
+ end # module
98
+
99
+
@@ -0,0 +1,38 @@
1
+ module Multimethod
2
+
3
+ module ObjectExtension
4
+ def self.append_features(base) # :nodoc:
5
+ # puts "append_features{#{base}}"
6
+ super
7
+ base.extend(ClassMethods)
8
+ end
9
+
10
+ module ClassMethods
11
+ def multimethod(body, file = nil, line = nil)
12
+ unless file && line
13
+ fileline = caller(1)[0]
14
+ if fileline && md = /^(.*):(\d+)$/.match(fileline)
15
+ file, line = md[1], md[2].to_i
16
+
17
+ newlines = 0
18
+ body.gsub(/\n/s){|x| newlines = newlines + 1}
19
+ line -= newlines
20
+ end
21
+
22
+ # $stderr.puts "file = #{file.inspect}, line = #{line.inspect}"
23
+ end
24
+
25
+ ::Multimethod::Table.instance.install_method(self, body, file, line)
26
+ end
27
+ end
28
+ end # class
29
+ end # module
30
+
31
+
32
+ # Add to Module
33
+ Object.class_eval do
34
+ include Multimethod::ObjectExtension
35
+ end
36
+
37
+
38
+
@@ -0,0 +1,232 @@
1
+ module Multimethod
2
+
3
+ class Method
4
+ attr_accessor :mod
5
+ attr_accessor :name
6
+ attr_accessor :parameter
7
+
8
+ attr_accessor :min_args
9
+ attr_accessor :max_args
10
+ attr_accessor :restarg
11
+ attr_accessor :default
12
+
13
+ attr_accessor :multimethod
14
+ attr_accessor :file
15
+ attr_accessor :line
16
+
17
+ def initialize(mod, name, params)
18
+ raise NameError, "multimethod method name not specified" unless name && name.to_s.size > 0
19
+
20
+ name = Multimethod.normalize_name(name)
21
+
22
+ @mod = mod
23
+ @name = name
24
+ @parameter = [ ]
25
+ @min_args = 0
26
+ @max_args = 0
27
+ @restarg = nil
28
+ @default = nil
29
+
30
+ @score = { }
31
+
32
+ # Add self parameter at front.
33
+ add_parameter(Parameter.new('self', mod))
34
+
35
+ # Handle other parameters.
36
+ case params
37
+ when Array
38
+ scan_parameters(params)
39
+ when String
40
+ scan_parameters_string(params)
41
+ end
42
+ end
43
+
44
+
45
+ # For sort
46
+ def <=>(x)
47
+ 0
48
+ end
49
+
50
+
51
+ def scan_parameters_string(params)
52
+
53
+ #$stderr.puts "scan_parameters_string(#{params.inspect})"
54
+
55
+ str = params.clone
56
+
57
+ until str.empty?
58
+ name = nil
59
+ type = nil
60
+ default = nil
61
+
62
+ str.sub!(/^\s+/, '')
63
+
64
+ # $stderr.puts " str=#{str.inspect}"
65
+
66
+ if md = /^(\w+(::\w+)*)\s+(\w+)/i.match(str)
67
+ str = md.post_match
68
+ type = md[1]
69
+ name = md[3]
70
+ elsif md = /^(\*?\w+)/i.match(str)
71
+ str = md.post_match
72
+ type = nil
73
+ name = md[1]
74
+ else
75
+ raise NameError, "Syntax error in multimethod parameters: #{params.inspect} before #{str.inspect}"
76
+ end
77
+
78
+ if md = /^\s*=\s*([^,]+)/.match(str)
79
+ str = md.post_match
80
+ default = md[1]
81
+ end
82
+
83
+
84
+ str.sub!(/^\s+/, '')
85
+ if ! str.empty?
86
+ if md = /^,/.match(str)
87
+ str = md.post_match
88
+ else
89
+ raise NameError, "Syntax error in multimethod parameters: expected ',' before #{str.inspect}"
90
+ end
91
+ end
92
+
93
+ p = Parameter.new(name, type, default)
94
+ add_parameter(p)
95
+ end
96
+ end
97
+
98
+
99
+ def scan_parameters(params)
100
+ until params.empty?
101
+ name = nil
102
+ type = nil
103
+ restarg = false
104
+ default = nil
105
+
106
+ if x = params.shift
107
+ case x
108
+ when Class
109
+ type = x
110
+ else
111
+ name = x
112
+ end
113
+ end
114
+
115
+ if ! name && (x = params.shift)
116
+ name = x
117
+ end
118
+
119
+ raise("Parameter name expected, found #{name.inspect}") unless name.kind_of?(String) || name.kind_of?(Symbol)
120
+ raise("Parameter type expected, found #{type.inspect}") unless type.kind_of?(Module) || type.nil?
121
+
122
+ p = Parameter.new(name, type, default)
123
+ add_parameter(p)
124
+ end
125
+
126
+ end
127
+
128
+ def add_parameter(p)
129
+ if p.restarg
130
+ raise("Too many restargs") if @restarg
131
+ @restarg = p
132
+ @max_args = nil
133
+ end
134
+ if p.default
135
+ (@default ||= [ ]).push(p)
136
+ end
137
+
138
+ p.i = @parameter.size
139
+ @parameter.push(p)
140
+ p.method = self
141
+
142
+ unless p.default || p.restarg
143
+ @min_args = @parameter.size
144
+ end
145
+
146
+ unless @restarg
147
+ @max_args = @parameter.size
148
+ end
149
+ end
150
+
151
+
152
+ def score_cached(args)
153
+ unless x = @score[args]
154
+ x = @score[args] =
155
+ score(args)
156
+ end
157
+ x
158
+ end
159
+
160
+
161
+ def score(args)
162
+
163
+ if @min_args > args.size
164
+ # Not enough args
165
+ score = nil
166
+ elsif @max_args && @max_args < args.size
167
+ # Too many args?
168
+ # $stderr.puts "max_args = #{@max_args}, args.size = #{args.size}"
169
+ score = nil
170
+ else
171
+ # Interpret how close the argument type is to the parameter's type.
172
+ i = -1
173
+ score = args.collect{|a| parameter_at(i = i + 1).score(a)}
174
+
175
+ # Handle score for trailing restargs.
176
+ if @restarg || @default
177
+ while (i = i + 1) < @parameter.size
178
+ # $stderr.puts " Adding score i=#{i}"
179
+ score << parameter_at(i).score(NilClass)
180
+ end
181
+ end
182
+
183
+ # If any argument cannot match, avoid this method.
184
+ score = nil if score.index(nil)
185
+ end
186
+
187
+ # if true || @name =~ /_bar$/
188
+ # $stderr.puts " Method: score #{self.to_s} #{args.inspect} => #{score.inspect}"
189
+ # end
190
+
191
+ score
192
+ end
193
+
194
+
195
+ def parameter_at(i)
196
+ if i >= @parameter.size && @restarg
197
+ @restarg
198
+ else
199
+ @parameter[i]
200
+ end
201
+ end
202
+
203
+
204
+ def parameter_to_s(p = nil)
205
+ p ||= @parameter
206
+ p.collect{|x| x.to_s_long}.join(', ')
207
+ end
208
+
209
+ def to_s(name = nil)
210
+ name ||= @name
211
+ p = @parameter.clone
212
+ rcvr = p.shift
213
+ "#{rcvr.type.name}##{name}(#{parameter_to_s(p)})"
214
+ end
215
+
216
+ def to_ruby
217
+ "def #{name}(#{to_s_arg})"
218
+ end
219
+
220
+ def to_s_arg
221
+ x = @parameter.clone
222
+ x.shift
223
+ x.collect{|x| x.to_ruby}.join(', ')
224
+ end
225
+
226
+ def inspect
227
+ to_s
228
+ end
229
+ end # class
230
+ end # module
231
+
232
+