rubybreaker 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/AUTHORS +7 -0
- data/LICENSE +26 -0
- data/README.md +403 -0
- data/Rakefile +90 -0
- data/TODO +30 -0
- data/bin/gen_stub_rubylib +64 -0
- data/bin/rubybreaker +67 -0
- data/lib/rubybreaker/context.rb +122 -0
- data/lib/rubybreaker/debug.rb +48 -0
- data/lib/rubybreaker/error.rb +59 -0
- data/lib/rubybreaker/rubylib/core.rb +2316 -0
- data/lib/rubybreaker/rubylib.rb +3 -0
- data/lib/rubybreaker/runtime/inspector.rb +57 -0
- data/lib/rubybreaker/runtime/monitor.rb +235 -0
- data/lib/rubybreaker/runtime/object_wrapper.rb +77 -0
- data/lib/rubybreaker/runtime/overrides.rb +42 -0
- data/lib/rubybreaker/runtime/pluggable.rb +57 -0
- data/lib/rubybreaker/runtime/type_placeholder.rb +27 -0
- data/lib/rubybreaker/runtime/type_system.rb +228 -0
- data/lib/rubybreaker/runtime/typesig_parser.rb +45 -0
- data/lib/rubybreaker/runtime.rb +103 -0
- data/lib/rubybreaker/test/testcase.rb +39 -0
- data/lib/rubybreaker/test.rb +1 -0
- data/lib/rubybreaker/type/type.rb +241 -0
- data/lib/rubybreaker/type/type_comparer.rb +143 -0
- data/lib/rubybreaker/type/type_grammar.treetop +285 -0
- data/lib/rubybreaker/type/type_unparser.rb +142 -0
- data/lib/rubybreaker/type.rb +2 -0
- data/lib/rubybreaker/typing/rubytype.rb +47 -0
- data/lib/rubybreaker/typing/subtyping.rb +480 -0
- data/lib/rubybreaker/typing.rb +3 -0
- data/lib/rubybreaker/util.rb +31 -0
- data/lib/rubybreaker.rb +193 -0
- data/test/integrated/tc_method_missing.rb +30 -0
- data/test/integrated/tc_simple1.rb +77 -0
- data/test/runtime/tc_obj_wrapper.rb +73 -0
- data/test/runtime/tc_typesig_parser.rb +33 -0
- data/test/ts_integrated.rb +4 -0
- data/test/ts_runtime.rb +5 -0
- data/test/ts_type.rb +5 -0
- data/test/ts_typing.rb +4 -0
- data/test/type/tc_comparer.rb +211 -0
- data/test/type/tc_parser.rb +219 -0
- data/test/type/tc_unparser.rb +276 -0
- data/test/typing/tc_rubytype.rb +63 -0
- data/test/typing/tc_typing.rb +219 -0
- data/webpage/footer.html +5 -0
- data/webpage/generated_toc.js +319 -0
- data/webpage/header.html +14 -0
- data/webpage/images/logo.png +0 -0
- data/webpage/index.html +439 -0
- data/webpage/rubybreaker.css +53 -0
- metadata +119 -0
@@ -0,0 +1,57 @@
|
|
1
|
+
#--
|
2
|
+
# This file defines Inspector which finds the type placeholder for a
|
3
|
+
# module.
|
4
|
+
|
5
|
+
require_relative "monitor"
|
6
|
+
|
7
|
+
module RubyBreaker
|
8
|
+
|
9
|
+
module Runtime
|
10
|
+
|
11
|
+
# This module inspects a Breakable module and retrieves type information
|
12
|
+
# if there is any.
|
13
|
+
module Inspector
|
14
|
+
|
15
|
+
# This method inspects the module for specified method name. It
|
16
|
+
# returns the method type or method list type for the given method. If
|
17
|
+
# no method exists or if there is no type information for the method,
|
18
|
+
# it returns nil
|
19
|
+
def self.inspect_meth(mod, mname)
|
20
|
+
mname = mname.to_sym
|
21
|
+
if mod.included_modules.include?(Breakable)
|
22
|
+
placeholder = Breakable::TYPE_PLACEHOLDER_MAP[mod]
|
23
|
+
elsif mod.included_modules.include?(Broken)
|
24
|
+
placeholder = Broken::TYPE_PLACEHOLDER_MAP[mod]
|
25
|
+
else
|
26
|
+
# TODO
|
27
|
+
end
|
28
|
+
t = placeholder.inst_meths[mname] if placeholder
|
29
|
+
return t
|
30
|
+
end
|
31
|
+
|
32
|
+
# Similar to inspect_meth but returns a hash of (mname, mtype) pairs.
|
33
|
+
def self.inspect_meths(mod, mnames)
|
34
|
+
mtype_hash = {}
|
35
|
+
mnames.each {|mname|
|
36
|
+
mtype_hash[mname] = self.inspect_meth(mod, mname)
|
37
|
+
}
|
38
|
+
return mtype_hash
|
39
|
+
end
|
40
|
+
|
41
|
+
# This method inspects the module for all methods. It returns a Hash
|
42
|
+
# containing (method name, method type) pairs.
|
43
|
+
def self.inspect_all(mod)
|
44
|
+
mtypes = {}
|
45
|
+
mm = Breakable::TYPE_PLACEHOLDER_MAP[mod]
|
46
|
+
# mm = MonitorUtils.get_module_monitor(mod)
|
47
|
+
mm.inst_meths.each_pair {|im,mtype|
|
48
|
+
mtypes[im] = mtype if mtype
|
49
|
+
}
|
50
|
+
return mtypes
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,235 @@
|
|
1
|
+
#--
|
2
|
+
# This file contains the core of the runtime framework which injects the
|
3
|
+
# monitoring code into Breakable classes/modules and actually monitors the
|
4
|
+
# instances of those classes/modules at runtime. It also provides some utility
|
5
|
+
# functions for later use (after runtime).
|
6
|
+
|
7
|
+
dir = File.dirname(__FILE__)
|
8
|
+
require_relative "type_placeholder"
|
9
|
+
require_relative "../context"
|
10
|
+
require_relative "../debug"
|
11
|
+
require_relative "pluggable"
|
12
|
+
require_relative "type_system"
|
13
|
+
|
14
|
+
module RubyBreaker
|
15
|
+
|
16
|
+
module Runtime
|
17
|
+
|
18
|
+
DEFAULT_TYPE_SYSTEM = TypeSystem.new
|
19
|
+
|
20
|
+
# This class monitors method calls before and after. It simply reroutes
|
21
|
+
# the responsibility to the appropriate pluggable type system which does
|
22
|
+
# the actual work of gathering type information.
|
23
|
+
class Monitor
|
24
|
+
|
25
|
+
# attr_accessor :mod
|
26
|
+
attr_accessor :pluggable
|
27
|
+
|
28
|
+
public
|
29
|
+
|
30
|
+
def initialize(mod, pluggable)
|
31
|
+
# @mod = mod
|
32
|
+
@pluggable = pluggable
|
33
|
+
end
|
34
|
+
|
35
|
+
# Starts monitoring of a method; it wraps each argument so that they
|
36
|
+
# can gather type information in the callee.
|
37
|
+
def monitor_before_method(obj, meth_info)
|
38
|
+
@pluggable.before_method(obj, meth_info)
|
39
|
+
end
|
40
|
+
|
41
|
+
# This method is invoked after the actual method is invoked.
|
42
|
+
def monitor_after_method(obj, meth_info)
|
43
|
+
@pluggable.after_method(obj, meth_info)
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
# This class is a switch to turn on and off the type monitoring system.
|
49
|
+
# It is important to turn off the monitor once the process is inside the
|
50
|
+
# monitor; otherwise, it WILL fall into an infinite loop.
|
51
|
+
class MonitorSwitch
|
52
|
+
|
53
|
+
attr_accessor :switch
|
54
|
+
|
55
|
+
def initialize(); @switch = true end
|
56
|
+
|
57
|
+
def turn_on();
|
58
|
+
Debug.msg("Switch turned on")
|
59
|
+
@switch = true;
|
60
|
+
end
|
61
|
+
|
62
|
+
def turn_off();
|
63
|
+
Debug.msg("Switch turned off")
|
64
|
+
@switch = false;
|
65
|
+
end
|
66
|
+
|
67
|
+
def set_to(mode); @switch = mode; end
|
68
|
+
end
|
69
|
+
|
70
|
+
# TODO:For now, we use a global switch; but in future, a switch per
|
71
|
+
# object should be used for multi-process apps. However, there is still
|
72
|
+
# a concern for module tracking in which case, there isn't really a way
|
73
|
+
# to do this unless we track with the process or some unique id for that
|
74
|
+
# process.
|
75
|
+
GLOBAL_MONITOR_SWITCH = MonitorSwitch.new
|
76
|
+
|
77
|
+
# This context is used for keeping track of context in the user code.
|
78
|
+
# This will ignore the context within the RubyBreaker code, so it is
|
79
|
+
# easy to pinpoint program locations without distraction from the
|
80
|
+
# RubyBreaker code.
|
81
|
+
CONTEXT = Context.new(ObjectPosition.new(self,"main"))
|
82
|
+
|
83
|
+
# This module contains helper methods for monitoring objects and
|
84
|
+
# modules.
|
85
|
+
module MonitorUtils
|
86
|
+
|
87
|
+
public
|
88
|
+
|
89
|
+
# This will do the actual routing work for a particular "monitored"
|
90
|
+
# method call.
|
91
|
+
#
|
92
|
+
# obj:: is the object receiving the message; is never wrapped object
|
93
|
+
# meth_name:: is the original method name being called args:: is a
|
94
|
+
# list of arguments for the original method call blk:: is the block
|
95
|
+
# argument for the original method call
|
96
|
+
#
|
97
|
+
#--
|
98
|
+
# NOTE: This method should not assume that obj is a monitored
|
99
|
+
# object. That is, no special method should be called to obj unless it
|
100
|
+
# checks first.
|
101
|
+
def self.route(obj,meth_name,*args,&blk)
|
102
|
+
|
103
|
+
# remember the switch mode before turning it off
|
104
|
+
switch = GLOBAL_MONITOR_SWITCH.switch
|
105
|
+
|
106
|
+
# turn off the monitor so we do not fall into an infinite loop
|
107
|
+
GLOBAL_MONITOR_SWITCH.turn_off()
|
108
|
+
|
109
|
+
# use symbol instead of string throughout this code
|
110
|
+
meth_name = :"#{meth_name}"
|
111
|
+
|
112
|
+
# first, get the context right
|
113
|
+
# notice the argument 2 to the caller!
|
114
|
+
#
|
115
|
+
# CONTEXT.push(obj, meth_name,
|
116
|
+
# Position.convert_caller_to_pos(caller(2)))
|
117
|
+
CONTEXT.push(Position.convert_caller_to_pos(caller(2)))
|
118
|
+
|
119
|
+
# this is what the renamed method
|
120
|
+
stub_meth_name = get_alt_meth_name(meth_name)
|
121
|
+
|
122
|
+
Debug.msg("Route to #{stub_meth_name}",CONTEXT)
|
123
|
+
|
124
|
+
# short-circuit if switch was off--i.e., no monitoring
|
125
|
+
if !switch
|
126
|
+
retval = obj.send(stub_meth_name.to_sym,*args,&blk)
|
127
|
+
CONTEXT.pop() # do not forget to pop the context before returning
|
128
|
+
return retval
|
129
|
+
end
|
130
|
+
|
131
|
+
is_obj_mod = (obj.class == Class or obj.class == Module)
|
132
|
+
|
133
|
+
# from here, do more work for module monitoring
|
134
|
+
mod = obj.class
|
135
|
+
|
136
|
+
# TODO:
|
137
|
+
meta = false
|
138
|
+
|
139
|
+
# mm = get_module_monitor(mod) unless is_obj_mod
|
140
|
+
mm = Breakable::MONITOR_MAP[mod] if !is_obj_mod
|
141
|
+
|
142
|
+
# There is something wrong if there isn't a module monitor
|
143
|
+
# associated with the call.
|
144
|
+
# raise Exception if mm == nil || !mm.inst_meths.include?(meth_name)
|
145
|
+
|
146
|
+
meth_info = MethodInfo.new(meta, meth_name, args, blk, nil)
|
147
|
+
|
148
|
+
mm.monitor_before_method(obj, meth_info)
|
149
|
+
|
150
|
+
Debug.msg("monitor_before_method ended")
|
151
|
+
|
152
|
+
# we are going to turn the switch back on
|
153
|
+
GLOBAL_MONITOR_SWITCH.turn_on()
|
154
|
+
|
155
|
+
# call the original method which was renamed
|
156
|
+
retval = obj.send(stub_meth_name.to_sym, *meth_info.args,
|
157
|
+
&meth_info.blk)
|
158
|
+
|
159
|
+
# turn it off
|
160
|
+
GLOBAL_MONITOR_SWITCH.turn_off()
|
161
|
+
|
162
|
+
meth_info.ret = retval
|
163
|
+
mm.monitor_after_method(obj, meth_info)
|
164
|
+
|
165
|
+
# things are done in this context. pop it off.
|
166
|
+
CONTEXT.pop()
|
167
|
+
|
168
|
+
# it is always the case that the switch was off when this particular
|
169
|
+
# call was made. (Otherwise, it would have quit somewhere above
|
170
|
+
GLOBAL_MONITOR_SWITCH.turn_on()
|
171
|
+
|
172
|
+
return retval # always return the return value
|
173
|
+
end
|
174
|
+
|
175
|
+
# This method returns the alternative (renamed) method name
|
176
|
+
def self.get_alt_meth_name(meth_name)
|
177
|
+
return "__#{meth_name}"
|
178
|
+
end
|
179
|
+
|
180
|
+
# This method returns the original method name
|
181
|
+
def self.get_orig_meth_name(meth_name)
|
182
|
+
return meth_name[2..-1]
|
183
|
+
end
|
184
|
+
|
185
|
+
end
|
186
|
+
|
187
|
+
# This module installs a monitor in the object.
|
188
|
+
module MonitorInstaller
|
189
|
+
|
190
|
+
include MonitorUtils
|
191
|
+
|
192
|
+
# returns true if the receiver is a module or a class
|
193
|
+
def self.is_module?(recv)
|
194
|
+
return recv.respond_to?(:class) && recv.kind_of?(Module)
|
195
|
+
end
|
196
|
+
|
197
|
+
# renames the method in essence; this method also "installs" the
|
198
|
+
# module monitor for the class
|
199
|
+
def self.rename_meth(recv,meth_name)
|
200
|
+
alt_meth_name = MonitorUtils.get_alt_meth_name(meth_name)
|
201
|
+
recv.module_eval("alias :\"#{alt_meth_name}\" :\"#{meth_name}\"")
|
202
|
+
Debug.msg("Adding alternate method for #{meth_name}")
|
203
|
+
recv.module_eval <<-EOF
|
204
|
+
def #{meth_name}(*args, &blk)
|
205
|
+
RubyBreaker::Runtime::MonitorUtils.route(self,
|
206
|
+
"#{meth_name}",
|
207
|
+
*args,
|
208
|
+
&blk)
|
209
|
+
end
|
210
|
+
EOF
|
211
|
+
end
|
212
|
+
|
213
|
+
# Installs an module (class) monitor to the object.
|
214
|
+
def self.install_module_monitor(mod,infer=false)
|
215
|
+
Debug.short_msg("Installing module monitor for #{mod}")
|
216
|
+
if infer
|
217
|
+
Breakable::MONITOR_MAP[mod] = Monitor.new(mod, DEFAULT_TYPE_SYSTEM)
|
218
|
+
Breakable::TYPE_PLACEHOLDER_MAP[mod] = TypePlaceholder.new
|
219
|
+
end
|
220
|
+
inst_meths = []
|
221
|
+
meths = mod.instance_methods(false)
|
222
|
+
meths.each do |m|
|
223
|
+
self.rename_meth(mod,m)
|
224
|
+
end
|
225
|
+
Debug.feed_line()
|
226
|
+
end
|
227
|
+
|
228
|
+
def self.report(mod)
|
229
|
+
end
|
230
|
+
|
231
|
+
end
|
232
|
+
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
@@ -0,0 +1,77 @@
|
|
1
|
+
#--
|
2
|
+
# This program keeps track of every method call to a wrapped object.
|
3
|
+
|
4
|
+
module RubyBreaker
|
5
|
+
|
6
|
+
module Runtime
|
7
|
+
|
8
|
+
# This constant is used to determine if an object is a wrapped object.
|
9
|
+
WRAPPED_INDICATOR = :"__is_wrapped?__"
|
10
|
+
|
11
|
+
# This class represents the shell object that wraps around another
|
12
|
+
# object. Note that it is a subclass of BasicObject to keep it really
|
13
|
+
# concise. It also redirects the following methods (from BasicObject):
|
14
|
+
#
|
15
|
+
# !, !=, ==, equal?, eql?, __id__, object_id,
|
16
|
+
# send, __send__, instance_eval, instance_exec
|
17
|
+
#
|
18
|
+
class ObjectWrapper < BasicObject
|
19
|
+
|
20
|
+
def initialize(obj)
|
21
|
+
@__rubybreaker_obj = obj
|
22
|
+
nom_type = TypeDefs::NominalType.new(obj.class)
|
23
|
+
@__rubybreaker_type = TypeDefs::FusionType.new(nom_type,[])
|
24
|
+
end
|
25
|
+
|
26
|
+
# This method returns the original object.
|
27
|
+
def __rubybreaker_obj()
|
28
|
+
return @__rubybreaker_obj
|
29
|
+
end
|
30
|
+
|
31
|
+
# This method returns the type gathered so far for this object.
|
32
|
+
def __rubybreaker_type()
|
33
|
+
return @__rubybreaker_type
|
34
|
+
end
|
35
|
+
|
36
|
+
#--
|
37
|
+
# The following code will generate "serious problem" warning. But it's
|
38
|
+
# ok. This meta programming code re-defines BasicObject's methods to
|
39
|
+
# redirect to the actual object.
|
40
|
+
[:"!", :"!=", :"==", :"equal?", :"eql?", :"__id__", :"object_id",
|
41
|
+
:"send", :"__send__", :"instance_eval",
|
42
|
+
:"instance_exec"].each do |meth|
|
43
|
+
|
44
|
+
eval <<-EOS
|
45
|
+
|
46
|
+
def #{meth}(*args,&blk)
|
47
|
+
self.method_missing(:"#{meth}", *args, &blk)
|
48
|
+
end
|
49
|
+
|
50
|
+
EOS
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
# Only behave differently if it's looking for +WRAPPED_INDICATOR+
|
55
|
+
# method
|
56
|
+
def respond_to?(mname)
|
57
|
+
return true if mname.to_sym == WRAPPED_INDICATOR
|
58
|
+
return @__rubybreaker_obj.respond_to?(mname)
|
59
|
+
end
|
60
|
+
|
61
|
+
# This method missing method redirects all other method calls.
|
62
|
+
def method_missing(mname,*args,&blk)
|
63
|
+
Debug.msg("Method_missing for #{mname}")
|
64
|
+
if GLOBAL_MONITOR_SWITCH.switch
|
65
|
+
@__rubybreaker_type.add_meth(mname)
|
66
|
+
retval = @__rubybreaker_obj.send(mname,*args,&blk)
|
67
|
+
retval = ObjectWrapper.new(retval)
|
68
|
+
else
|
69
|
+
retval = @__rubybreaker_obj.send(mname,*args,&blk)
|
70
|
+
end
|
71
|
+
return retval
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
#--
|
2
|
+
# This file contains methods that need to override the existing ones to
|
3
|
+
# accommodate the object wrapper.
|
4
|
+
|
5
|
+
require_relative "object_wrapper"
|
6
|
+
|
7
|
+
module RubyBreaker
|
8
|
+
|
9
|
+
module Runtime
|
10
|
+
|
11
|
+
# This constant holds the string used internally by RubyBreaker to
|
12
|
+
# indicate overridden methods.
|
13
|
+
OVERRIDE_PREFIX = "__rubybreaker_"
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
class Numeric
|
21
|
+
|
22
|
+
[:"==", :equal?, :eql?].each do |m|
|
23
|
+
|
24
|
+
eval <<-EOS
|
25
|
+
|
26
|
+
alias :"#{RubyBreaker::Runtime::OVERRIDE_PREFIX}#{m}" :"#{m}"
|
27
|
+
def #{m}(other)
|
28
|
+
if other.respond_to?(RubyBreaker::Runtime::WRAPPED_INDICATOR)
|
29
|
+
other = other.__rubybreaker_obj
|
30
|
+
end
|
31
|
+
return self.send(:"#{RubyBreaker::Runtime::OVERRIDE_PREFIX}#{m}", other)
|
32
|
+
end
|
33
|
+
|
34
|
+
EOS
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
|
42
|
+
|
@@ -0,0 +1,57 @@
|
|
1
|
+
#--
|
2
|
+
# This file contains a module that explains what is eligible to be a
|
3
|
+
# pluggable type system (or anything really). (Think of AOP but for metod
|
4
|
+
# calls only.)
|
5
|
+
|
6
|
+
module RubyBreaker
|
7
|
+
|
8
|
+
module Runtime
|
9
|
+
|
10
|
+
# This class has information (and data) of the method being called. Used
|
11
|
+
# by Pluggable and Monitor
|
12
|
+
class MethodInfo
|
13
|
+
|
14
|
+
attr_accessor :meta
|
15
|
+
attr_accessor :meth_name
|
16
|
+
attr_accessor :args
|
17
|
+
attr_accessor :blk
|
18
|
+
attr_accessor :ret
|
19
|
+
|
20
|
+
def initialize(meta, meth_name, args, blk, ret)
|
21
|
+
@meta = meta
|
22
|
+
@meth_name = meth_name
|
23
|
+
@args = args
|
24
|
+
@blk = blk
|
25
|
+
@ret = ret
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
# Any Pluggable module can be "plugged" into the RubyBreaker monitoring
|
31
|
+
# system. For example, if you write your own type system for
|
32
|
+
# RubyBreaker, you can include this module to use it instead of the
|
33
|
+
# default type system that comes with RubyBreaker.
|
34
|
+
module Pluggable
|
35
|
+
|
36
|
+
# This method will be invoked right before the actual method is
|
37
|
+
# invoked.
|
38
|
+
#
|
39
|
+
# obj:: the receiver of the method call (message)
|
40
|
+
# method_info:: a MethodInfo object containing the method call
|
41
|
+
# information
|
42
|
+
def before_method_call(obj, meth_info)
|
43
|
+
end
|
44
|
+
|
45
|
+
# This method will be invoked right after the actual method is
|
46
|
+
# invoked.
|
47
|
+
#
|
48
|
+
# obj:: the receiver of the method call (message)
|
49
|
+
# method_info:: a MethodInfo object containing the method call
|
50
|
+
# information
|
51
|
+
def after_method_call(obj, meth_info)
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
#--
|
2
|
+
# This file defines the placeholder for types which will be tracked by
|
3
|
+
# two (global) constant--one for Breakable and the other for Broken.
|
4
|
+
|
5
|
+
module RubyBreaker
|
6
|
+
|
7
|
+
module Runtime
|
8
|
+
|
9
|
+
# This class is a placeholder for method types
|
10
|
+
class TypePlaceholder
|
11
|
+
|
12
|
+
# This accessor sets/gets instance method map
|
13
|
+
attr_accessor :inst_meths # method name => method type
|
14
|
+
|
15
|
+
# This accessor sets/gets module method map (XXX: not used)
|
16
|
+
attr_accessor :mod_meths
|
17
|
+
|
18
|
+
def initialize()
|
19
|
+
@inst_meths = {}
|
20
|
+
@mod_meths = {}
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|