Skip to content

Commit

Permalink
Refactored dispatch calculation so send.rb gets arity+privacy checking.
Browse files Browse the repository at this point in the history
  • Loading branch information
Michael Edgar committed Aug 13, 2011
1 parent b4145b6 commit eb649c9
Show file tree
Hide file tree
Showing 6 changed files with 221 additions and 123 deletions.
1 change: 1 addition & 0 deletions lib/laser.rb
Expand Up @@ -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'
Expand Down
192 changes: 192 additions & 0 deletions 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
4 changes: 1 addition & 3 deletions lib/laser/analysis/control_flow/cfg_builder.rb
Expand Up @@ -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
Expand Down
105 changes: 10 additions & 95 deletions lib/laser/analysis/control_flow/constant_propagation.rb
Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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,
Expand Down

0 comments on commit eb649c9

Please sign in to comment.