From eb649c964e172199a6041387a49a3346025faffa Mon Sep 17 00:00:00 2001 From: Michael Edgar Date: Sat, 13 Aug 2011 13:48:40 -0400 Subject: [PATCH] Refactored dispatch calculation so send.rb gets arity+privacy checking. --- lib/laser.rb | 1 + .../analysis/bootstrap/dispatch_results.rb | 192 ++++++++++++++++++ .../analysis/control_flow/cfg_builder.rb | 4 +- .../control_flow/constant_propagation.rb | 105 +--------- lib/laser/analysis/special_methods/send.rb | 39 ++-- .../yield_properties_spec.rb | 3 +- 6 files changed, 221 insertions(+), 123 deletions(-) create mode 100644 lib/laser/analysis/bootstrap/dispatch_results.rb diff --git a/lib/laser.rb b/lib/laser.rb index 1645c02..eef2aec 100644 --- a/lib/laser.rb +++ b/lib/laser.rb @@ -62,6 +62,7 @@ def self.debug? require 'laser/analysis/bootstrap/laser_singleton_class' require 'laser/analysis/bootstrap/laser_proc' require 'laser/analysis/bootstrap/laser_method' +require 'laser/analysis/bootstrap/dispatch_results' require 'laser/analysis/laser_utils.rb' require 'laser/analysis/protocol_registry' require 'laser/analysis/scope' diff --git a/lib/laser/analysis/bootstrap/dispatch_results.rb b/lib/laser/analysis/bootstrap/dispatch_results.rb new file mode 100644 index 0000000..41f9525 --- /dev/null +++ b/lib/laser/analysis/bootstrap/dispatch_results.rb @@ -0,0 +1,192 @@ +module Laser + module Analysis + # Collects the results of attempted dispatches and calculates the return type, + # raise type, and raise frequency. + # + # Is used whenever a dispatch is discovered that needs analysis. This + # includes, for example, performing CPA and analyzing a call to send/public_send. + # + # Is responsible for noting that a method has been used, as this is the central + # place for the logic pertaining to success/failure on dispatch. Keep in mind: + # a method isn't used if it is called with incorrect arity, and that arity + # checking should occur here. + class DispatchResults + ArityError = Struct.new(:receiver_class, :method_name, :provided, :expected) + ArityError.class_eval do + def message + "#{receiver_class.name}##{method_name} expects " + + "#{expected} arguments, got #{provided}." + end + end + + PrivacyError = Struct.new(:receiver_class, :method_name) + PrivacyError.class_eval do + def message + "Tried to call #{receiver_class.privacy_for(method_name)} method " + + "#{receiver_class.name}##{method_name}." + end + end + + attr_reader :result_type + + def initialize + @raise_type = Types::EMPTY + @result_type = Types::EMPTY + @privacy_failures = 0 + @privacy_samples = 0 + @privacy_errors = Set[] + @arity_failures = 0 + @arity_samples = 0 + @arity_errors = Set[] + @normal_failures = 0 + @normal_samples = 0 + end + + def add_samples_from_dispatch(methods, self_type, cartesian, ignore_privacy) + if methods.empty? + @privacy_failures += 1 + @privacy_samples += 1 + @raise_type |= ClassRegistry['NoMethodError'].as_type + end + methods.each do |method| + next unless check_privacy(method, self_type, ignore_privacy) + cartesian.each do |*type_list, block_type| + next unless check_arity(method, self_type, type_list.size) + method.been_used! + normal_dispatch(method, self_type, cartesian) + end + end + end + + def check_privacy(method, self_type, ignore_privacy) + result = false + if ignore_privacy + passes_privacy + result = true + else + self_type.possible_classes.each do |self_class| + if self_class.visibility_for(method.name) == :public + passes_privacy + result = true + else + fails_privacy(self_class, method.name) + end + end + end + result + end + + def check_arity(method, self_type, proposed_arity) + result = false + self_type.possible_classes.each do |self_class| + if method.valid_arity?(proposed_arity) + passes_arity + result = true + else + fails_arity(self_class, method.name, proposed_arity, method.arity) + end + end + result + end + + def normal_dispatch(method, self_type, cartesian) + cartesian.each do |*type_list, block_type| + raise_frequency = method.raise_frequency_for_types(self_type, type_list, block_type) + if raise_frequency > Frequency::NEVER + fails_dispatch(method.raise_type_for_types(self_type, type_list, block_type)) + end + if raise_frequency < Frequency::ALWAYS + passes_dispatch(method.return_type_for_types(self_type, type_list, block_type)) + end + end + end + + ########## Result Accessors ############## + + def raise_type + if @privacy_samples.zero? + ClassRegistry['NoMethodError'].as_type + else + @raise_type + end + end + + def raise_frequency + if @privacy_samples.zero? + Frequency::ALWAYS + else + [arity_failure_frequency, privacy_failure_frequency, + normal_failure_frequency].max + end + end + + ############ Arity-related dispatch issues ############ + def arity_failure_frequency + if @arity_failures == 0 + Frequency::NEVER + elsif @arity_failures == @arity_samples + Frequency::ALWAYS + else + Frequency::MAYBE + end + end + + def passes_arity + @arity_samples += 1 + end + + def fails_arity(receiver_class, method_name, provided, expected) + @arity_errors << ArityError.new(receiver_class, method_name, provided, expected) + @arity_samples += 1 + @arity_failures += 1 + @raise_type |= ClassRegistry['ArgumentError'].as_type + end + + ########## Privacy-related dispatch issues ############ + + def privacy_failure_frequency + if @privacy_failures == 0 + Frequency::NEVER + elsif @privacy_failures == @privacy_samples + Frequency::ALWAYS + else + Frequency::MAYBE + end + end + + def passes_privacy + @privacy_samples += 1 + end + + def fails_privacy(receiver_class, method_name) + @privacy_errors << PrivacyError.new(receiver_class, method_name) + @privacy_samples += 1 + @privacy_failures += 1 + @raise_type |= ClassRegistry['NoMethodError'].as_type + end + + ######### Calculated dispatch issues ########### + + def normal_failure_frequency + if @normal_failures == 0 + Frequency::NEVER + elsif @normal_failures == @normal_samples + Frequency::ALWAYS + else + Frequency::MAYBE + end + end + + def passes_dispatch(return_type) + @result_type |= return_type + @normal_samples += 1 + end + + def fails_dispatch(raise_type) + @raise_type |= raise_type + @normal_failures += 1 + @normal_samples += 1 + end + end + end +end \ No newline at end of file diff --git a/lib/laser/analysis/control_flow/cfg_builder.rb b/lib/laser/analysis/control_flow/cfg_builder.rb index 5c9b609..a13341f 100644 --- a/lib/laser/analysis/control_flow/cfg_builder.rb +++ b/lib/laser/analysis/control_flow/cfg_builder.rb @@ -956,10 +956,8 @@ def yield_instruct(arg_node, opts={}) start_block no_block message = const_instruct('no block given (yield)') - file_name = const_instruct(@current_node.file_name) - line_number = const_instruct(@current_node.line_number || 0) raise_instance_of_instruct( - ClassRegistry['LocalJumpError'].binding, message, file_name, line_number, + ClassRegistry['LocalJumpError'].binding, message, target: current_yield_fail) start_block if_block diff --git a/lib/laser/analysis/control_flow/constant_propagation.rb b/lib/laser/analysis/control_flow/constant_propagation.rb index 2a212bd..6bfb283 100644 --- a/lib/laser/analysis/control_flow/constant_propagation.rb +++ b/lib/laser/analysis/control_flow/constant_propagation.rb @@ -335,13 +335,9 @@ def infer_type_and_raising(instruction, receiver, method_name, args, opts) def cpa_call_properties(receiver, method, args, instruction, opts) ignore_privacy, block = instruction.ignore_privacy, instruction.block_operand dispatches = cpa_dispatches(receiver, instruction, method, opts) - cartesian = calculate_possible_templates(dispatches, args, block) - result = cpa_for_templates(dispatches, cartesian) - raise_result, raise_type = raisability_for_templates(dispatches, cartesian, ignore_privacy) - if result.empty? - raise TypeError.new("No methods named #{method} with matching types were found.") - end - [Types::UnionType.new(result), raise_result, raise_type] + cartesian = calculate_possible_templates(args, block) + results = collect_dispatch_results(dispatches, cartesian, ignore_privacy) + [results.result_type, results.raise_frequency, results.raise_type] end # Calculates all possible (self_type, dispatches) pairs for a call. @@ -360,18 +356,16 @@ def cpa_dispatches(receiver, instruction, method, opts) # Calculates the set of methods potentially invoked in dynamic dispatch, # and the set of all possible argument type combinations. - def calculate_possible_templates(possible_dispatches, args, block) + def calculate_possible_templates(args, block) if Bindings::Base === args && Types::TupleType === args.expr_type cartesian_parts = args.element_types - empty = cartesian_parts.empty? elsif Bindings::Base === args && Types::UnionType === args.expr_type && Types::TupleType === args.expr_type.member_types.first cartesian_parts = args.expr_type.member_types.first.element_types.map { |x| [x] } - empty = cartesian_parts.empty? else cartesian_parts = args.map(&:expr_type).map(&:member_types).map(&:to_a) - empty = args.empty? end + empty = cartesian_parts.empty? if empty && !block cartesian = [ [Types::NILCLASS] ] else @@ -384,91 +378,12 @@ def calculate_possible_templates(possible_dispatches, args, block) cartesian end - # Calculates the CPA-based return type of a dynamic call. - def cpa_for_templates(possible_dispatches, cartesian) - result = Set.new - possible_dispatches.each do |self_type, methods| - result |= methods.map do |method| - cartesian.map do |*type_list, block_type| - begin - method.return_type_for_types(self_type, type_list, block_type) - rescue TypeError => err - Laser.debug_puts("Invalid argument types found.") - nil - end - end.compact - end.flatten - end - result - end - - # TODO(adgar): Optimize this. Use lattice-style expression of raisability - # until types need to be added too. - def raisability_for_templates(possible_dispatches, cartesian, ignore_privacy) - raise_type = Types::EMPTY - seen_public = seen_private = seen_raise = seen_succeed = seen_any = seen_missing = false - seen_valid_arity = seen_invalid_arity = false - arity = cartesian.first.size - 1 # -1 for block arg - possible_dispatches.each do |self_type, methods| - seen_any = true if methods.size > 0 && !seen_any - seen_missing = true if methods.empty? && !seen_missing - methods.each do |method| - if !seen_valid_arity && method.valid_arity?(arity) - seen_valid_arity = true - end - if !seen_invalid_arity && !method.valid_arity?(arity) - seen_invalid_arity = true - end - if !ignore_privacy - self_type.possible_classes.each do |self_class| - if self_class.visibility_for(method.name) == :public - seen_public = true - method.been_used! if method.valid_arity?(arity) - end - if !seen_private - seen_private = (self_class.visibility_for(method.name) != :public) - end - end - else - method.been_used! if method.valid_arity?(arity) - end - cartesian.each do |*type_list, block_type| - raise_frequency = method.raise_frequency_for_types(self_type, type_list, block_type) - if raise_frequency > Frequency::NEVER - seen_raise = true - raise_type = raise_type | method.raise_type_for_types(self_type, type_list, block_type) - end - seen_succeed = raise_frequency < Frequency::ALWAYS if !seen_succeed - end - end - end - - if seen_any - fails_lookup = seen_missing ? Frequency::MAYBE : Frequency::NEVER - fails_privacy = if ignore_privacy - then Frequency::NEVER - else Frequency.for_samples(seen_private, seen_public) - end - failed_arity = Frequency.for_samples(seen_invalid_arity, seen_valid_arity) - if fails_privacy == Frequency::ALWAYS - raise_type = ClassRegistry['NoMethodError'].as_type - elsif failed_arity == Frequency::ALWAYS - raise_type = ClassRegistry['ArgumentError'].as_type - else - if fails_lookup > Frequency::NEVER || fails_privacy > Frequency::NEVER - raise_type |= ClassRegistry['NoMethodError'].as_type - end - if failed_arity > Frequency::NEVER - raise_type |= ClassRegistry['ArgumentError'].as_type - end - end - raised = Frequency.for_samples(seen_raise, seen_succeed) - raise_freq = [fails_privacy, raised, fails_lookup, failed_arity].max - else - raise_freq = Frequency::ALWAYS # no method! - raise_type = ClassRegistry['NoMethodError'].as_type + def collect_dispatch_results(dispatches, cartesian, ignore_privacy) + results = DispatchResults.new + dispatches.each do |self_type, methods| + results.add_samples_from_dispatch(methods, self_type, cartesian, ignore_privacy) end - [raise_freq, raise_type] + results end # Evaluates the instruction, and if the constant value is lowered, diff --git a/lib/laser/analysis/special_methods/send.rb b/lib/laser/analysis/special_methods/send.rb index b251499..d55e7d4 100644 --- a/lib/laser/analysis/special_methods/send.rb +++ b/lib/laser/analysis/special_methods/send.rb @@ -18,48 +18,39 @@ def initialize(name, privacy) @privacy = privacy end - def each_target_method(self_type, arg_type) + def all_target_methods(self_type, arg_type) + collection = Set[] arg_type.possible_classes.each do |target_klass| if LaserSingletonClass === target_klass target_method_name = target_klass.get_instance.to_s self_type.possible_classes.each do |self_class| - if passes_visibility?(self_class, target_method_name) - method = self_class.instance_method(target_method_name) - method.been_used! - yield(method) - end + method = self_class.instance_method(target_method_name) + collection << method end end end - end - - def passes_visibility?(klass, name) - return true if @privacy == :any - klass.visibility_for(name) == @privacy + collection end - def collect_type_from_targets(to_call, self_type, arg_types, block_type) - result_type = Types::UnionType.new([]) - each_target_method(self_type, arg_types[0]) do |method| - result_type |= method.send(to_call, self_type, arg_types[1..-1], block_type) - end - result_type + def dispatch_results(self_type, arg_types, block_type) + methods = all_target_methods(self_type, arg_types[0]) + cartesian = [[*arg_types[1..-1], block_type]] + ignore_privacy = @privacy == :any + results = DispatchResults.new + results.add_samples_from_dispatch(methods, self_type, cartesian, ignore_privacy) + results end def return_type_for_types(self_type, arg_types, block_type) - collect_type_from_targets(:return_type_for_types, self_type, arg_types, block_type) + dispatch_results(self_type, arg_types, block_type).result_type end def raise_type_for_types(self_type, arg_types, block_type) - collect_type_from_targets(:raise_type_for_types, self_type, arg_types, block_type) + dispatch_results(self_type, arg_types, block_type).raise_type end def raise_frequency_for_types(self_type, arg_types, block_type) - all_frequencies = [] - each_target_method(self_type, arg_types[0]) do |method| - all_frequencies << method.raise_frequency_for_types(self_type, arg_types[1..-1], block_type) - end - Frequency.combine_samples(all_frequencies) + dispatch_results(self_type, arg_types, block_type).raise_frequency end end end diff --git a/spec/analysis_specs/control_flow_specs/yield_properties_spec.rb b/spec/analysis_specs/control_flow_specs/yield_properties_spec.rb index 60779c4..a7c60a5 100644 --- a/spec/analysis_specs/control_flow_specs/yield_properties_spec.rb +++ b/spec/analysis_specs/control_flow_specs/yield_properties_spec.rb @@ -256,10 +256,11 @@ def one 2 end EOF + g.yield_type.should be :required g.yield_arity.should == Set[1] end - + it 'infers yield likelihood with to_proc block syntax' do cfg <<-EOF class YP1