This is the default type system for RubyBreaker. It can be overridden by a
user specified type system. See pluggable.rb
for how this can
be done.
This method occurs after every call to a “monitored” method call of a module/class specified for “breaking”. It updates the type information.
# File lib/rubybreaker/runtime/type_system.rb, line 386 def break_after_method(obj, meth_info) is_obj_mod = (obj.class == Class or obj.class == Module) mod = is_obj_mod ? Runtime.eigen_class(obj) : obj.class # Take things out of the method info object meth_name = meth_info.meth_name retval = meth_info.ret args = meth_info.args blk = meth_info.blk RubyBreaker.log("break_after_method #{mod}##{meth_name} started") # Compute the least upper bound lub(obj, TYPE_MAP[mod], meth_name, retval, *args, &blk) if obj == retval # It is possible that the method receiver is a wrapped object if # it is an argument to a method in the current call stack. So this # check is to return the wrapped object and not the stripped off # version. (Remember, == is overridden for the wrapped object.) meth_info.ret = obj end RubyBreaker.log("break_after_method #{mod}##{meth_name} ended") end
This method occurs before every call to a “monitored” method in a module/class specified for breaking. It wraps each argument with the object wrapper so it can be tracked of the method calls.
# File lib/rubybreaker/runtime/type_system.rb, line 314 def break_before_method(obj, meth_info) # Use the eigen class if the object is a module/class and use the # object's class otherwise. is_obj_mod = (obj.class == Class or obj.class == Module) mod = is_obj_mod ? Runtime.eigen_class(obj) : obj.class # Let's take things out of the MethodInfo object meth_name = meth_info.meth_name args = meth_info.args blk = meth_info.blk ret = meth_info.ret RubyBreaker.log("break_before_method #{mod}##{meth_name} started") args = args.map do |arg| if arg == nil || arg.kind_of?(TrueClass) || arg.kind_of?(FalseClass) # XXX: would overrides resolve this issue? arg elsif arg.respond_to?(WRAPPED_INDICATOR) # Don't need to wrap an object that is already wrapped arg else ObjectWrapper.new(arg) end end # Using this module object, retrieve the method type map which # maps method names to method types. meth_type_map = TYPE_MAP[mod] # Using the method name, get the type of this method from the map. meth_type = meth_type_map[meth_name] if meth_type # This means the method type has been created previously. unless !meth_type.instance_of?(MethodType) || (blk == nil && meth_type.blk_type == nil) && (!blk || blk.arity == meth_type.blk_type.arg_types.length) raise Errors::TypeError.new("Block usage is inconsistent") end else # No method type has been created for this method yet. Create a # blank method type (where each argument type, block type, and # return type are all nil). # # First, use the orignal method's arity to find out # of # arguments. meth_obj = obj.method(Monitor.get_alt_meth_name(meth_name)) arity = meth_obj.arity arg_types = [nil] * meth_obj.arity.abs if blk # Do the same for the block too if there is one blk_arity = blk.arity blk_arg_types = [nil] * blk_artiy.abs blk_type = BlockType.new(blk_arg_types, nil, nil) else blk_type = nil end meth_type = MethodType.new(meth_name, arg_types, blk_type, nil) meth_type_map[meth_name] = meth_type end meth_info.args = args RubyBreaker.log("break_before_method #{mod}##{meth_name} ended") end
This method is invoked after the original method is executed.
# File lib/rubybreaker/runtime/type_system.rb, line 284 def check_after_method(obj, meth_info) is_obj_mod = (obj.class == Class or obj.class == Module) mod = is_obj_mod ? Runtime.eigen_class(obj) : obj.class # Get the method type map for the module/class from the global map. meth_type_map = TYPE_MAP[mod] return unless meth_type_map # Let's take things out of the MethodInfo object meth_name = meth_info.meth_name ret = meth_info.ret RubyBreaker.log("check_after_method #{mod}##{meth_name} started") # Get the registered method type for this method meth_type = meth_type_map[meth_name] ret_type = NominalType.new(ret.class) if !meth_type.ret_type.subtype_of?(ret_type) msg = type_error_msg_prefix(mod, meth_name) + " return value does not have type #{ret_type.unparse()}." raise Errors::ReturnTypeError.new(msg) end RubyBreaker.log("check_after_method #{mod}##{meth_name} started") end
This method is invoked before the original method is executed.
# File lib/rubybreaker/runtime/type_system.rb, line 216 def check_before_method(obj, meth_info) is_obj_mod = (obj.class == Class or obj.class == Module) mod = is_obj_mod ? Runtime.eigen_class(obj) : obj.class meth_type_map = TYPE_MAP[mod] return unless meth_type_map # Let's take things out of the MethodInfo object meth_name = meth_info.meth_name args = meth_info.args # blk = meth_info.blk RubyBreaker.log("check_before_method #{mod}##{meth_name} started") # Get the registered method type for this method meth_type = meth_type_map[meth_name] # Do an arity check first. if !arity_check(args.size, meth_type) msg = type_error_msg_prefix(mod, meth_name) + " has an arity of #{meth_type.arg_types.size} " + "but #{args.size} arguments were passed in" raise Errors::ArityError.new(msg) end # Remember what the last formal argument type was so that, if it is # a variable length argument type, we use to check the remaining # arguments. last_supertype = nil # Check actual arguments up until the last position of the formal # argument type. If the number of the formal arguments is less than # actual arguments, it means the last formal argument is a variable # length. If it's the other way around, there are optional # arguments. meth_type.arg_types.each_with_index do |supertype, i| if supertype.kind_of?(OptionalType) || supertype.kind_of?(VarLengthType) supertype = supertype.type end last_supertype = supertype break if i >= args.size subtype = NominalType.new(args[i].class) if !subtype.subtype_of?(supertype) msg = type_error_msg_prefix(mod, meth_name) + "'s #{Util.ordinalize(i+1)} argument " + "does not have type #{supertype.unparse()}." raise Errors::ArgumentTypeError.new(msg) end end # Handle the remaining actual arguments if meth_type.arg_types.size < args.size for i in meth_type.arg_types.size..args.size-1 subtype = NominalType.new(args[i].class) if !subtype.subtype_of?(last_supertype) msg = type_error_msg_prefix(mod, meth_name) + "'s #{Util.ordinalize(i+1)} argument " + "does not have type #{last_supertype.unparse()}." raise Errors::ArgumentTypeError.new(msg) end end end RubyBreaker.log("check_before_method #{mod}##{meth_name} ended") end
This method performs the arity check.
# File lib/rubybreaker/runtime/type_system.rb, line 194 def arity_check(num_of_args, meth_type) arg_types = meth_type.arg_types opt = false varlen = false arg_types.each_with_index do |arg_type, i| if arg_type.kind_of?(OptionalType) opt = true elsif arg_type.kind_of?(VarLengthType) varlen = true end end check = (opt && num_of_args <= arg_types.size) || (varlen && num_of_args >= arg_types.size) || num_of_args == arg_types.size return check end
Check if the object is wrapped by a monitor
# File lib/rubybreaker/runtime/type_system.rb, line 31 def is_object_wrapped?(obj) return obj.respond_to?(WRAPPED_INDICATOR) end
This method computes the least upper bound of the existing method type and newly observed argument/block/return types. There are a few cases to consider:
If the existing type is a method list type, the new observed type will be either “consolidated” into one of the method types in the list or added to the list.
If there is no compatibility between the existing method type and the observed type, then the method type will be promoted to a method list type. And the newly observed type will be added to the list.
For each method type,
It basically consolidates the existing type information for the invoked method and the observed type.
For arguments, we look for most general type that can handle all types we have seen. This means we find the super type of all types we have seen (excluding unknown types).
For return, we look for most specific type that can handle both types. Therefore, if two types have no subtype relation, we AND them. But we do not allow AND types in the return type. We must turn the method type to a method list type.
the receive of the method call
a hash object that maps method names to method types
the name of the method being invoked
the return value of the original method call
the arguments
the block argument
# File lib/rubybreaker/runtime/type_system.rb, line 128 def lub(obj, meth_type_map, meth_name, retval, *args, &blk) exist_meth_type = meth_type_map[meth_name.to_sym] # Again, find the arity meth_obj = obj.method(Monitor.get_alt_meth_name(meth_name)) arity = meth_obj.arity # Construct the newly observed method type first new_meth_type = MethodType.new(meth_name,[]) args.each_with_index do |arg,idx| if is_object_wrapped?(arg) arg_type = arg.__rubybreaker_type else arg_type = NominalType.new(arg.class) end # Check if the last argument should be a variable length argument if arity < 0 && (idx + 1 == arity.abs) new_meth_type.arg_types << VarLengthType.new(arg_type) break end new_meth_type.arg_types << arg_type end if (obj == retval) # the return value is same as the message receiver. This means the # return value has the self type. SelfType.set_self(obj.class) ret_type = SelfType.new() else # Otherwise, construct a nominal type. ret_type = NominalType.new(retval.class) end new_meth_type.ret_type = ret_type resolved = false if exist_meth_type.instance_of?(MethodListType) exist_meth_type.types.each {|meth_type| resolved = lub_helper(meth_type, new_meth_type) break if resolved } else resolved = lub_helper(exist_meth_type, new_meth_type) if !resolved # Could not resolve the types, so promote the method type to a # method list type exist_meth_type = MethodListType.new([exist_meth_type]) meth_type_map[meth_name.to_sym] = exist_meth_type end end if !resolved exist_meth_type.types << new_meth_type end end
This method is a helper for computing the least upper bound. It handles the case where existing method type is a method type (and not a method list type). If there is no compatibility of the two types, then it returns false.
# File lib/rubybreaker/runtime/type_system.rb, line 39 def lub_helper(exist_meth_type, new_meth_type) # most restrictive for the given test cases. arg_types = [] # Resolve the argument types first. exist_meth_type.arg_types.each_with_index do |exist_arg_type, i| arg_type = nil new_arg_type = new_meth_type.arg_types[i] if !exist_arg_type # nil means there hasn't been any type observed so use the new # argument type as "resolved". arg_type = new_arg_type elsif new_arg_type.subtype_of?(exist_arg_type) # Pick the subtype argument since we are resolving in # contra-variance arg_type = new_arg_type elsif exist_arg_type.subtype_of?(new_arg_type) # Pick the subtype argument since we are resolving in # contra-variance arg_type = exist_arg_type else # No subtype relation between them, so OR them. arg_type = OrType.new([new_arg_type, exist_arg_type]) end arg_types << arg_type end # Now, resolve the return type new_ret_type = new_meth_type.ret_type exist_ret_type = exist_meth_type.ret_type if !exist_ret_type ret_type = new_ret_type resolved = true elsif exist_ret_type.subtype_of?(new_ret_type) # Co-variance ret_type = new_ret_type resolved = true elsif new_ret_type.subtype_of?(exist_ret_type) # Co-variance ret_type = exist_ret_type resolved = true else resolved = false end if resolved exist_meth_type.arg_types = arg_types exist_meth_type.ret_type = ret_type end return resolved end
This method creates the prefix for the type error message.
# File lib/rubybreaker/runtime/type_system.rb, line 185 def type_error_msg_prefix(mod, meth_name) # Match eigen class and returns a prefix that is a class method # using dot (.) instead of double colons (::). result = %r#<Class:(.+)>/.match("#{mod}") prefix = result ? "#{result[1]}." : "#{mod}#" return "#{prefix}#{meth_name}" end