module Steep
class TypeAssignability
attr_reader :signatures
attr_reader :errors
def initialize()
@signatures = {}
@klasses = []
@instances = []
@errors = []
if block_given?
yield self
validate
end
end
def with(klass: nil, instance: nil, &block)
@klasses.push(klass) if klass
@instances.push(instance) if instance
yield
ensure
@klasses.pop if klass
@instances.pop if instance
end
def klass
@klasses.last
end
def instance
@instances.last
end
def add_signature(signature)
raise "Signature Duplicated: #{signature.name}" if signatures.key?(signature.name)
signatures[signature.name] = signature
end
def test(src:, dest:, known_pairs: [])
case
when src.is_a?(Types::Any) || dest.is_a?(Types::Any)
true
when src == dest
true
when src.is_a?(Types::Union)
src.types.all? do |type|
test(src: type, dest: dest, known_pairs: known_pairs)
end
when dest.is_a?(Types::Union)
dest.types.any? do |type|
test(src: src, dest: type, known_pairs: known_pairs)
end
when src.is_a?(Types::Var) || dest.is_a?(Types::Var)
known_pairs.include?([src, dest])
when src.is_a?(Types::Name) && dest.is_a?(Types::Name)
test_interface(resolve_interface(src.name, src.params), resolve_interface(dest.name, dest.params), known_pairs)
else
raise "Unexpected type: src=#{src.inspect}, dest=#{dest.inspect}, known_pairs=#{known_pairs.inspect}"
end
end
def test_application(params:, argument:, index:)
param_type = params.flat_unnamed_params[index]&.last
if param_type
unless test(src: argument, dest: param_type)
yield param_type
end
end
end
def test_interface(src, dest, known_pairs)
if src == dest
return true
end
if known_pairs.include?([src, dest])
return true
end
pairs = known_pairs + [[src, dest]]
dest.methods.all? do |name, dest_methods|
if src.methods.key?(name)
src_methods = src.methods[name]
dest_methods.types.all? do |dest_method|
src_methods.types.any? do |src_method|
test_method(src_method, dest_method, pairs)
end
end
end
end
end
def test_method(src, dest, known_pairs)
test_params(src.params, dest.params, known_pairs) &&
test_block(src.block, dest.block, known_pairs) &&
test(src: src.return_type, dest: dest.return_type, known_pairs: known_pairs)
end
def test_params(src, dest, known_pairs)
assigning_pairs = []
src_flat = src.flat_unnamed_params
dest_flat = dest.flat_unnamed_params
case
when dest.rest
return false unless src.rest
while src_flat.size > 0
src_type = src_flat.shift
dest_type = dest_flat.shift
if dest_type
assigning_pairs << [src_type.last, dest_type.last]
else
assigning_pairs << [src_type.last, dest.rest]
end
end
if src.rest
assigning_pairs << [src.rest, dest.rest]
end
when src.rest
while src_flat.size > 0
src_type = src_flat.shift
dest_type = dest_flat.shift
if dest_type
assigning_pairs << [src_type.last, dest_type.last]
else
break
end
end
if src.rest && !dest_flat.empty?
dest_flat.each do |dest_type|
assigning_pairs << [src.rest, dest_type.last]
end
end
when src.required.size + src.optional.size >= dest.required.size + dest.optional.size
while src_flat.size > 0
src_type = src_flat.shift
dest_type = dest_flat.shift
if dest_type
assigning_pairs << [src_type.last, dest_type.last]
else
if src_type.first == :required
return false
else
break
end
end
end
else
return false
end
src_flat_kws = src.flat_keywords
dest_flat_kws = dest.flat_keywords
dest_flat_kws.each do |name, _|
if src_flat_kws.key?(name)
assigning_pairs << [src_flat_kws[name], dest_flat_kws[name]]
else
if src.rest_keywords
assigning_pairs << [src.rest_keywords, dest_flat_kws[name]]
else
return false
end
end
end
src.required_keywords.each do |name, _|
unless dest.required_keywords.key?(name)
return false
end
end
if src.rest_keywords && dest.rest_keywords
assigning_pairs << [src.rest_keywords, dest.rest_keywords]
end
assigning_pairs.all? do |pair|
src_type = pair.first
dest_type = pair.last
test(src: dest_type, dest: src_type, known_pairs: known_pairs)
end
end
def test_block(src, dest, known_pairs)
return true if !src && !dest
return false if !src || !dest
raise "Keyword args for block is not yet supported" unless src.params&.flat_keywords&.empty?
raise "Keyword args for block is not yet supported" unless dest.params&.flat_keywords&.empty?
ss = src.params.flat_unnamed_params
ds = dest.params.flat_unnamed_params
max = ss.size > ds.size ? ss.size : ds.size
for i in 0...max
s = ss[i]&.last || src.params.rest
d = ds[i]&.last || dest.params.rest
if s && d
test(src: s, dest: d, known_pairs: known_pairs) or return false
end
end
if src.params.rest && dest.params.rest
test(src: src.params.rest, dest: dest.params.rest, known_pairs: known_pairs) or return false
end
test(src: dest.return_type, dest: src.return_type, known_pairs: known_pairs)
end
def resolve_interface(name, params, klass: nil, instance: nil, constructor: nil)
klass ||= Types::Name.module(name: name.name, params: params)
instance ||= Types::Name.instance(name: name.name, params: params)
case name
when TypeName::Interface
signatures[name.name].to_interface(klass: klass, instance: instance, params: params)
when TypeName::Instance
methods = signatures[name.name].instance_methods(assignability: self, klass: klass, instance: instance, params: params)
Interface.new(name: name, params: params, methods: methods.reject {|key, _| key == :initialize })
when TypeName::Module
methods = signatures[name.name].module_methods(assignability: self, klass: klass, instance: instance, params: params, constructor: constructor)
Interface.new(name: name, params: params, methods: methods)
else
raise "Unexpected type name: #{name.inspect}"
end
end
def lookup_included_signature(type)
raise "#{self.class}#lookup_included_signature expects type name: #{type.inspect}" unless type.is_a?(Types::Name)
raise "#{self.class}#lookup_included_signature expects module instance name: #{type.name.inspect}" unless type.name.is_a?(TypeName::Instance)
signatures[type.name.name]
end
def lookup_super_class_signature(type)
raise "#{self.class}#lookup_super_class_signature expects type name: #{type.inspect}" unless type.is_a?(Types::Name)
raise "#{self.class}#lookup_super_class_signature expects module instance name: #{type.name.inspect}" unless type.name.is_a?(TypeName::Instance)
signature = signatures[type.name.name]
raise "#{self.class}#lookup_super_class_signature expects class: #{type.name.inspect}" unless signature.is_a?(Signature::Class)
signature
end
def lookup_class_signature(type)
raise "#{self.class}#lookup_class_signature expects type name: #{type.inspect}" unless type.is_a?(Types::Name)
raise "#{self.class}#lookup_class_signature expects instance name: #{type.name.inspect}" unless type.name.is_a?(TypeName::Instance)
signature = signatures[type.name.name]
raise "#{self.class}#lookup_super_class_signature expects class: #{signature.inspect}" unless signature.is_a?(Signature::Class)
signature
end
def lookup_extensions(module_name)
signatures.values.select do |signature|
case signature
when Signature::Extension
signature.module_name == module_name
end
end
end
def method_type(type, name)
case type
when Types::Any
return type
when Types::Merge
methods = type.types.map {|t|
resolve_interface(t.name, t.params, klass: Types::Var.new(name: :some_klass), instance: Types::Var.new(name: :some_instance))
}.each.with_object({}) {|interface, methods|
methods.merge! interface.methods
}
method = methods[name]
when Types::Name
constructor = type.name.is_a?(TypeName::Module) && type.name.constructor
interface = resolve_interface(type.name, type.params, constructor: constructor)
method = interface.methods[name]
else
raise "Unexpected type: #{type}"
end
if method
yield(method) || Types::Any.new
else
yield(nil) || Types::Any.new
end
end
def validate
signatures.each do |name, signature|
signature.validate(self)
end
end
def validate_type_presence(signature, type)
if type.is_a?(Types::Name)
unless signatures[type.name.name]
errors << Signature::Errors::UnknownTypeName.new(signature: signature, type: type)
end
end
end
def validate_method_compatibility(signature, method_name, method)
if method.super_method
test = method.types.all? {|method_type|
method.super_method.types.any? {|super_type|
test_method(method_type, super_type, [])
}
}
unless test
errors << Signature::Errors::IncompatibleOverride.new(signature: signature,
method_name: method_name,
this_method: method.types,
super_method: method.super_method.types)
end
end
end
def compact(types)
types = types.reject {|type| type.is_a?(Types::Any) }
if types.empty?
[Types::Any.new]
else
compact0(types)
end
end
def compact0(types)
if types.size == 1
types
else
type, *types_ = types
compacted = compact0(types_)
compacted.flat_map do |type_|
case
when type == type_
[type]
when test(src: type_, dest: type)
[type]
when test(src: type, dest: type_)
[type_]
else
[type, type_]
end
end.uniq
end
end
end
end