def check_keyword_arg(receiver_type:, node:, method_type:, constraints:)
def check_keyword_arg(receiver_type:, node:, method_type:, constraints:)
params = method_type.params
case node.type
when :hash
keyword_hash_type = AST::Builtin::Hash.instance_type(AST::Builtin::Symbol.instance_type,
AST::Builtin.any_type)
typing.add_typing node, keyword_hash_type
given_keys = Set.new()
node.children.each do |element|
case element.type
when :pair
key_node, value_node = element.children
case key_node.type
when :sym
key_symbol = key_node.children[0]
keyword_type = case
when params.required_keywords.key?(key_symbol)
params.required_keywords[key_symbol]
when params.optional_keywords.key?(key_symbol)
AST::Types::Union.build(
types: [params.optional_keywords[key_symbol],
AST::Builtin.nil_type]
)
when params.rest_keywords
params.rest_keywords
end
typing.add_typing key_node, AST::Builtin::Symbol.instance_type
given_keys << key_symbol
if keyword_type
check(value_node, keyword_type, constraints: constraints) do |expected, actual, result|
return Errors::IncompatibleAssignment.new(
node: value_node,
lhs_type: expected,
rhs_type: actual,
result: result
)
end
else
synthesize(value_node)
end
else
check(key_node, AST::Builtin::Symbol.instance_type, constraints: constraints) do |expected, actual, result|
return Errors::IncompatibleAssignment.new(
node: key_node,
lhs_type: expected,
rhs_type: actual,
result: result
)
end
end
when :kwsplat
Steep.logger.warn("Keyword arg with kwsplat(**) node are not supported.")
check(element.children[0], keyword_hash_type, constraints: constraints) do |expected, actual, result|
return Errors::IncompatibleAssignment.new(
node: node,
lhs_type: expected,
rhs_type: actual,
result: result
)
end
given_keys = true
end
end
case given_keys
when Set
missing_keywords = Set.new(params.required_keywords.keys) - given_keys
unless missing_keywords.empty?
return Errors::MissingKeyword.new(node: node,
missing_keywords: missing_keywords)
end
extra_keywords = given_keys - Set.new(params.required_keywords.keys) - Set.new(params.optional_keywords.keys)
if extra_keywords.any? && !params.rest_keywords
return Errors::UnexpectedKeyword.new(node: node,
unexpected_keywords: extra_keywords)
end
end
else
if params.rest_keywords
Steep.logger.warn("Method call with rest keywords type is detected. Rough approximation to be improved.")
value_types = params.required_keywords.values +
params.optional_keywords.values.map {|type| AST::Types::Union.build(types: [type, AST::Builtin.nil_type]) } +
[params.rest_keywords]
hash_type = AST::Builtin::Hash.instance_type(
AST::Builtin::Symbol.instance_type,
AST::Types::Union.build(types: value_types,
location: method_type.location)
)
else
hash_elements = params.required_keywords.merge(
method_type.params.optional_keywords.transform_values do |type|
AST::Types::Union.build(types: [type, AST::Builtin.nil_type],
location: method_type.location)
end
)
hash_type = AST::Types::Record.new(elements: hash_elements)
end
node_type = synthesize(node, hint: hash_type)
relation = Subtyping::Relation.new(
sub_type: node_type,
super_type: hash_type
)
checker.check(relation, constraints: constraints).else do
return Errors::ArgumentTypeMismatch.new(
node: node,
receiver_type: receiver_type,
expected: relation.super_type,
actual: relation.sub_type
)
end
end
nil
end
def for_new_method(method_name, node, args:, self_type:)
def for_new_method(method_name, node, args:, self_type:)
annots = source.annotations(block: node, builder: checker.builder, current_module: current_namespace)
type_env = TypeInference::TypeEnv.new(subtyping: checker,
const_env: module_context&.const_env || self.type_env.const_env)
self.type_env.const_types.each do |name, type|
type_env.set(const: name, type: type)
end
self_type = expand_alias(annots.self_type || self_type)
self_interface = self_type && (self_type != AST::Builtin.any_type || nil) && case self_type
when AST::Types::Name::Instance
yield_self do
class_type = AST::Types::Name::Class.new(name: self_type.name, constructor: false)
checker.resolve_instance(self_type,
self_type: self_type,
instance_type: self_type,
module_type: class_type,
with_private: true)
end
else
checker.resolve(self_type)
end
interface_method = self_interface&.yield_self do |interface|
interface.methods[method_name]&.yield_self do |method|
if self_type.is_a?(AST::Types::Name::Base) && method.type_name == self_type.name
method
else
Interface::Method.new(type_name: self_type,
name: method_name,
types: method.types,
super_method: method,
attributes: [])
end
end
end
annotation_method = annotations.method_type(method_name)&.yield_self do |method_type|
subst = Interface::Substitution.build([],
instance_type: module_context&.instance_type || AST::Types::Instance.new,
module_type: module_context&.module_type || AST::Types::Class.new,
self_type: self_type)
Interface::Method.new(type_name: nil,
name: method_name,
types: [method_type.subst(subst)],
super_method: interface_method&.super_method,
attributes: [])
end
if interface_method && annotation_method
interface_types = interface_method.types.map do |method_type|
subst = Interface::Substitution.build(method_type.type_params)
method_type.instantiate(subst)
end
unknowns = []
annotation_types = annotation_method.types.each.with_object([]) do |method_type, array|
fresh = method_type.type_params.map {|var| AST::Types::Var.fresh(var) }
unknowns.push(*fresh)
subst = Interface::Substitution.build(method_type.type_params, fresh)
array << method_type.instantiate(subst)
end
constraints = Subtyping::Constraints.new(unknowns: unknowns)
interface_types.each do |type|
constraints.add_var(*type.free_variables.to_a)
end
result = checker.check_method(method_name,
annotation_method.with_types(annotation_types),
interface_method.with_types(interface_types),
assumption: Set.new,
trace: Subtyping::Trace.new,
constraints: constraints)
if result.failure?
typing.add_error Errors::IncompatibleMethodTypeAnnotation.new(
node: node,
annotation_method: annotation_method,
interface_method: interface_method,
result: result
)
end
end
method = annotation_method || interface_method
case
when method && method.types.size == 1
method_type = method.types.first
return_type = method_type.return_type
var_types = TypeConstruction.parameter_types(args, method_type)
unless TypeConstruction.valid_parameter_env?(var_types, args.reject {|arg| arg.type == :blockarg }, method_type.params)
typing.add_error Errors::MethodArityMismatch.new(node: node)
end
if (block_arg = args.find {|arg| arg.type == :blockarg })
if method_type.block
block_type = if method_type.block.optional?
AST::Types::Union.build(types: [method_type.block.type, AST::Builtin.nil_type])
else
method_type.block.type
end
var_types[block_arg.children[0].name] = block_type
end
end
when method
typing.add_error Errors::MethodDefinitionWithOverloading.new(node: node, method: method)
return_type = union_type(*method.types.map(&:return_type))
var_types = {}
else
var_types = {}
end
if annots.return_type && return_type
return_type_relation = Subtyping::Relation.new(sub_type: annots.return_type,
super_type: return_type)
checker.check(return_type_relation, constraints: Subtyping::Constraints.empty).else do |result|
typing.add_error Errors::MethodReturnTypeAnnotationMismatch.new(node: node,
method_type: return_type,
annotation_type: annots.return_type,
result: result)
end
end
constructor_method = method&.attributes&.include?(:constructor)
method_context = MethodContext.new(
name: method_name,
method: method,
method_type: method_type,
return_type: annots.return_type || return_type,
constructor: constructor_method
)
var_types.each do |name, type|
type_env.set(lvar: name, type: type)
end
ivar_types = {}
ivar_types.merge!(self_interface.ivars) if self_interface
ivar_types.merge!(annots.ivar_types)
ivar_types.each do |name, type|
type_env.set(ivar: name, type: type)
end
type_env = type_env.with_annotations(
lvar_types: annots.lvar_types,
ivar_types: annots.ivar_types,
const_types: annots.const_types,
)
self.class.new(
checker: checker,
source: source,
annotations: annots,
type_env: type_env,
block_context: nil,
self_type: self_type,
method_context: method_context,
typing: typing,
module_context: module_context,
break_context: nil
)
end
def synthesize(node, hint: nil)
def synthesize(node, hint: nil)
Steep.logger.tagged "synthesize:(#{node.location.expression.to_s.split(/:/, 2).last})" do
Steep.logger.debug node.type
case node.type
when :begin, :kwbegin
yield_self do
*mid_nodes, last_node = each_child_node(node).to_a
mid_nodes.each do |child|
synthesize(child)
end
if last_node
type = synthesize(last_node, hint: hint)
else
type = AST::Builtin.nil_type
end
typing.add_typing(node, type)
end
when :lvasgn
yield_self do
var = node.children[0]
rhs = node.children[1]
case var.name
when :_, :__any__
synthesize(rhs, hint: AST::Builtin.any_type)
typing.add_typing(node, AST::Builtin.any_type)
when :__skip__
typing.add_typing(node, AST::Builtin.any_type)
else
type_assignment(var, rhs, node, hint: hint)
end
end
when :lvar
yield_self do
var = node.children[0]
type = type_env.get(lvar: var.name) do
fallback_to_any node
end
typing.add_typing node, type
end
when :ivasgn
name = node.children[0]
value = node.children[1]
type_ivasgn(name, value, node)
when :ivar
yield_self do
name = node.children[0]
type = type_env.get(ivar: name) do
fallback_to_any node
end
typing.add_typing(node, type)
end
when :send
yield_self do
if self_class?(node)
module_type = expand_alias(module_context.module_type)
type = if module_type.is_a?(AST::Types::Name::Class)
AST::Types::Name::Class.new(name: module_type.name, constructor: method_context.constructor)
else
module_type
end
typing.add_typing(node, type)
else
type_send(node, send_node: node, block_params: nil, block_body: nil)
end
end
when :csend
yield_self do
type = if self_class?(node)
module_type = expand_alias(module_context.module_type)
type = if module_type.is_a?(AST::Types::Name::Class)
AST::Types::Name::Class.new(name: module_type.name, constructor: method_context.constructor)
else
module_type
end
typing.add_typing(node, type)
else
type_send(node, send_node: node, block_params: nil, block_body: nil, unwrap: true)
end
union_type(type, AST::Builtin.nil_type)
end
when :match_with_lvasgn
each_child_node(node) do |child|
synthesize(child)
end
typing.add_typing(node, AST::Builtin.any_type)
when :op_asgn
yield_self do
lhs, op, rhs = node.children
synthesize(rhs)
lhs_type = case lhs.type
when :lvasgn
type_env.get(lvar: lhs.children.first.name) do
break
end
when :ivasgn
type_env.get(ivar: lhs.children.first) do
break
end
else
Steep.logger.error("Unexpected op_asgn lhs: #{lhs.type}")
nil
end
case
when lhs_type == AST::Builtin.any_type
typing.add_typing(node, lhs_type)
when !lhs_type
fallback_to_any(node)
else
lhs_interface = checker.resolve(lhs_type)
op_method = lhs_interface.methods[op]
if op_method
args = TypeInference::SendArgs.from_nodes([rhs])
return_type_or_error = type_method_call(node,
receiver_type: lhs_type,
method: op_method,
args: args,
block_params: nil,
block_body: nil)
if return_type_or_error.is_a?(Errors::Base)
typing.add_error return_type_or_error
else
result = checker.check(
Subtyping::Relation.new(sub_type: return_type_or_error, super_type: lhs_type),
constraints: Subtyping::Constraints.empty
)
if result.failure?
typing.add_error(
Errors::IncompatibleAssignment.new(
node: node,
lhs_type: lhs_type,
rhs_type: return_type_or_error,
result: result
)
)
end
end
else
typing.add_error Errors::NoMethod.new(node: node, method: op, type: lhs_type)
end
typing.add_typing(node, lhs_type)
end
end
when :super
yield_self do
if self_type && method_context&.method
if method_context.super_method
each_child_node(node) do |child| synthesize(child) end
super_method = method_context.super_method
args = TypeInference::SendArgs.from_nodes(node.children.dup)
return_type_or_error = type_method_call(node,
receiver_type: self_type,
method: super_method,
args: args,
block_params: nil,
block_body: nil)
if return_type_or_error.is_a?(Errors::Base)
fallback_to_any node do
return_type_or_error
end
else
typing.add_typing node, return_type_or_error
end
else
fallback_to_any node do
Errors::UnexpectedSuper.new(node: node, method: method_context.name)
end
end
else
typing.add_typing node, AST::Builtin.any_type
end
end
when :block
yield_self do
send_node, params, body = node.children
if send_node.type == :lambda
type_lambda(node, block_params: params, block_body: body, type_hint: hint)
else
type_send(node, send_node: send_node, block_params: params, block_body: body, unwrap: send_node.type == :csend)
end
end
when :def
new = for_new_method(node.children[0],
node,
args: node.children[1].children,
self_type: module_context&.instance_type)
each_child_node(node.children[1]) do |arg|
new.synthesize(arg)
end
if node.children[2]
return_type = expand_alias(new.method_context&.return_type)
if return_type && !return_type.is_a?(AST::Types::Void)
new.check(node.children[2], return_type) do |_, actual_type, result|
typing.add_error(Errors::MethodBodyTypeMismatch.new(node: node,
expected: new.method_context&.return_type,
actual: actual_type,
result: result))
end
else
new.synthesize(node.children[2])
end
else
return_type = expand_alias(new.method_context&.return_type)
if return_type && !return_type.is_a?(AST::Types::Void)
result = checker.check(
Subtyping::Relation.new(sub_type: AST::Builtin.nil_type, super_type: return_type),
constraints: Subtyping::Constraints.empty
)
if result.failure?
typing.add_error(Errors::MethodBodyTypeMismatch.new(node: node,
expected: new.method_context&.return_type,
actual: AST::Builtin.nil_type,
result: result))
end
end
end
if module_context
module_context.defined_instance_methods << node.children[0]
end
typing.add_typing(node, AST::Builtin.any_type)
when :defs
synthesize(node.children[0]).tap do |self_type|
new = for_new_method(node.children[1],
node,
args: node.children[2].children,
self_type: self_type)
each_child_node(node.children[2]) do |arg|
new.synthesize(arg)
end
if node.children[3]
return_type = expand_alias(new.method_context&.return_type)
if return_type && !return_type.is_a?(AST::Types::Void)
new.check(node.children[3], return_type) do |return_type, actual_type, result|
typing.add_error(Errors::MethodBodyTypeMismatch.new(node: node,
expected: return_type,
actual: actual_type,
result: result))
end
else
new.synthesize(node.children[3])
end
end
end
if module_context
if node.children[0].type == :self
module_context.defined_module_methods << node.children[1]
end
end
typing.add_typing(node, AST::Builtin::Symbol.instance_type)
when :return
yield_self do
if node.children.size > 0
return_types = node.children.map do |value|
synthesize(value)
end
value_type = if return_types.size == 1
return_types.first
else
AST::Builtin::Array.instance_type(union_type(*return_types))
end
if (ret_type = expand_alias(method_context&.return_type))
unless ret_type.is_a?(AST::Types::Void)
result = checker.check(
Subtyping::Relation.new(sub_type: value_type,
super_type: ret_type),
constraints: Subtyping::Constraints.empty
)
if result.failure?
typing.add_error(Errors::ReturnTypeMismatch.new(node: node,
expected: method_context&.return_type,
actual: value_type,
result: result))
end
end
end
end
typing.add_typing(node, AST::Builtin.any_type)
end
when :break
value = node.children[0]
if break_context
case
when value && break_context.break_type
check(value, break_context.break_type) do |break_type, actual_type, result|
typing.add_error Errors::BreakTypeMismatch.new(node: node,
expected: break_type,
actual: actual_type,
result: result)
end
when !value
# ok
else
synthesize(value) if value
typing.add_error Errors::UnexpectedJumpValue.new(node: node)
end
else
synthesize(value)
typing.add_error Errors::UnexpectedJump.new(node: node)
end
typing.add_typing(node, AST::Builtin.any_type)
when :next
value = node.children[0]
if break_context
case
when value && break_context.next_type
check(value, break_context.next_type) do |break_type, actual_type, result|
typing.add_error Errors::BreakTypeMismatch.new(node: node,
expected: break_type,
actual: actual_type,
result: result)
end
when !value
# ok
else
synthesize(value) if value
typing.add_error Errors::UnexpectedJumpValue.new(node: node)
end
else
synthesize(value)
typing.add_error Errors::UnexpectedJump.new(node: node)
end
typing.add_typing(node, AST::Builtin.any_type)
when :retry
unless break_context
typing.add_error Errors::UnexpectedJump.new(node: node)
end
typing.add_typing(node, AST::Builtin.any_type)
when :arg, :kwarg, :procarg0
yield_self do
var = node.children[0]
type = type_env.get(lvar: var.name) do
fallback_to_any node
end
typing.add_typing(node, type)
end
when :optarg, :kwoptarg
yield_self do
var = node.children[0]
rhs = node.children[1]
type_assignment(var, rhs, node, hint: hint)
end
when :restarg
yield_self do
var = node.children[0]
type = type_env.get(lvar: var.name) do
typing.add_error Errors::FallbackAny.new(node: node)
AST::Builtin::Array.instance_type(AST::Builtin.any_type)
end
typing.add_typing(node, type)
end
when :kwrestarg
yield_self do
var = node.children[0]
type = type_env.get(lvar: var.name) do
typing.add_error Errors::FallbackAny.new(node: node)
AST::Builtin::Hash.instance_type(AST::Builtin::Symbol.instance_type, AST::Builtin.any_type)
end
typing.add_typing(node, type)
end
when :float
typing.add_typing(node, AST::Builtin::Float.instance_type)
when :nil
typing.add_typing(node, AST::Builtin.nil_type)
when :int
yield_self do
literal_type = expand_alias(hint) {|hint_| test_literal_type(node.children[0], hint_) }
if literal_type
typing.add_typing(node, literal_type)
else
typing.add_typing(node, AST::Builtin::Integer.instance_type)
end
end
when :sym
yield_self do
literal_type = expand_alias(hint) {|hint_| test_literal_type(node.children[0], hint_) }
if literal_type
typing.add_typing(node, literal_type)
else
typing.add_typing(node, AST::Builtin::Symbol.instance_type)
end
end
when :str
yield_self do
literal_type = expand_alias(hint) {|hint_| test_literal_type(node.children[0], hint_) }
if literal_type
typing.add_typing(node, literal_type)
else
typing.add_typing(node, AST::Builtin::String.instance_type)
end
end
when :true, :false
typing.add_typing(node, AST::Types::Boolean.new)
when :hash
yield_self do
ty = try_hash_type(node, hint) and return ty
if AST::Builtin::Hash.instance_type?(hint)
key_hint = hint.args[0]
value_hint = hint.args[1]
end
key_types = []
value_types = []
each_child_node(node) do |child|
case child.type
when :pair
key, value = child.children
key_types << synthesize(key).yield_self do |type|
select_super_type(type, key_hint)
end
value_types << synthesize(value).yield_self do |type|
select_super_type(type, value_hint)
end
when :kwsplat
expand_alias(synthesize(child.children[0])) do |splat_type, original_type|
if AST::Builtin::Hash.instance_type?(splat_type)
key_types << splat_type.args[0]
value_types << splat_type.args[1]
else
typing.add_error Errors::UnexpectedSplat.new(node: child, type: original_type)
key_types << AST::Builtin.any_type
value_types << AST::Builtin.any_type
end
end
else
raise "Unexpected non pair: #{child.inspect}" unless child.type == :pair
end
end
key_type = key_types.empty? ? AST::Builtin.any_type : AST::Types::Union.build(types: key_types)
value_type = value_types.empty? ? AST::Builtin.any_type : AST::Types::Union.build(types: value_types)
if key_types.empty? && value_types.empty? && !hint
typing.add_error Errors::FallbackAny.new(node: node)
end
typing.add_typing(node, AST::Builtin::Hash.instance_type(key_type, value_type))
end
when :dstr, :xstr
each_child_node(node) do |child|
synthesize(child)
end
typing.add_typing(node, AST::Builtin::String.instance_type)
when :dsym
each_child_node(node) do |child|
synthesize(child)
end
typing.add_typing(node, AST::Builtin::Symbol.instance_type)
when :class
yield_self do
for_class(node).tap do |constructor|
constructor.synthesize(node.children[2]) if node.children[2]
if constructor.module_context&.implement_name && !namespace_module?(node)
constructor.validate_method_definitions(node, constructor.module_context.implement_name)
end
end
typing.add_typing(node, AST::Builtin.nil_type)
end
when :module
yield_self do
for_module(node).yield_self do |constructor|
constructor.synthesize(node.children[1]) if node.children[1]
if constructor.module_context&.implement_name && !namespace_module?(node)
constructor.validate_method_definitions(node, constructor.module_context.implement_name)
end
end
typing.add_typing(node, AST::Builtin.nil_type)
end
when :self
if self_type
typing.add_typing(node, self_type)
else
fallback_to_any node
end
when :const
const_name = Names::Module.from_node(node)
if const_name
type = type_env.get(const: const_name) do
fallback_to_any node
end
typing.add_typing node, type
else
fallback_to_any node
end
when :casgn
yield_self do
const_name = Names::Module.from_node(node)
if const_name
value_type = synthesize(node.children.last)
type = type_env.assign(const: const_name, type: value_type) do |error|
case error
when Subtyping::Result::Failure
const_type = type_env.get(const: const_name)
typing.add_error(Errors::IncompatibleAssignment.new(node: node,
lhs_type: const_type,
rhs_type: value_type,
result: error))
when nil
typing.add_error(Errors::UnknownConstantAssigned.new(node: node, type: value_type))
end
end
typing.add_typing(node, type)
else
synthesize(node.children.last)
fallback_to_any(node)
end
end
when :yield
if method_context&.method_type
if method_context.block_type
block_type = method_context.block_type
block_type.type.params.flat_unnamed_params.map(&:last).zip(node.children).each do |(type, node)|
if node && type
check(node, type) do |_, rhs_type, result|
typing.add_error(Errors::IncompatibleAssignment.new(node: node,
lhs_type: type,
rhs_type: rhs_type,
result: result))
end
end
end
typing.add_typing(node, block_type.type.return_type)
else
typing.add_error(Errors::UnexpectedYield.new(node: node))
fallback_to_any node
end
else
fallback_to_any node
end
when :zsuper
yield_self do
if method_context&.method
if method_context.super_method
if method_context.method.incompatible?
typing.add_error Errors::IncompatibleZuper.new(node: node, method: method_context.name)
typing.add_typing node, AST::Builtin.any_type
else
types = method_context.super_method.types.map(&:return_type)
typing.add_typing(node, union_type(*types))
end
else
typing.add_error(Errors::UnexpectedSuper.new(node: node, method: method_context.name))
fallback_to_any node
end
else
fallback_to_any node
end
end
when :array
yield_self do
if node.children.empty?
typing.add_error Errors::FallbackAny.new(node: node) unless hint
array_type = if hint
relation = Subtyping::Relation.new(
sub_type: AST::Builtin::Array.instance_type(AST::Builtin.any_type),
super_type: hint
)
if checker.check(relation, constraints: Subtyping::Constraints.empty).success?
hint
end
end
typing.add_typing(node, array_type || AST::Builtin::Array.instance_type(AST::Builtin.any_type))
else
is_tuple = nil
expand_alias(hint) do |hint|
is_tuple = hint.is_a?(AST::Types::Tuple)
is_tuple &&= node.children.all? {|child| child.type != :splat }
is_tuple &&= node.children.size >= hint.types.size
is_tuple &&= hint.types.map.with_index do |child_type, index|
child_node = node.children[index]
[synthesize(child_node, hint: child_type), child_type]
end.all? do |node_type, hint_type|
relation = Subtyping::Relation.new(sub_type: node_type, super_type: hint_type)
result = checker.check(relation, constraints: Subtyping::Constraints.empty)
result.success?
end
end
if is_tuple
array_type = hint
else
element_hint = expand_alias(hint) do |hint|
AST::Builtin::Array.instance_type?(hint) && hint.args[0]
end
element_types = node.children.flat_map do |e|
if e.type == :splat
Steep.logger.info "Typing of splat in array is incompatible with Ruby; it does not use #to_a method"
synthesize(e.children.first).yield_self do |type|
expand_alias(type) do |ty|
case ty
when AST::Types::Union
ty.types
else
[ty]
end
end
end.map do |type|
case
when AST::Builtin::Array.instance_type?(type)
type.args.first
when AST::Builtin::Range.instance_type?(type)
type.args.first
else
type
end
end
else
[select_super_type(synthesize(e), element_hint)]
end
end
array_type = AST::Builtin::Array.instance_type(AST::Types::Union.build(types: element_types))
end
typing.add_typing(node, array_type)
end
end
when :and
yield_self do
left, right = node.children
left_type = synthesize(left)
truthy_vars = TypeConstruction.truthy_variables(left)
right_type, right_env = for_branch(right, truthy_vars: truthy_vars).yield_self do |constructor|
type = constructor.synthesize(right)
[type, constructor.type_env]
end
type_env.join!([right_env, TypeInference::TypeEnv.new(subtyping: checker,
const_env: nil)])
if left_type.is_a?(AST::Types::Boolean)
typing.add_typing(node, union_type(left_type, right_type))
else
typing.add_typing(node, union_type(right_type, AST::Builtin.nil_type))
end
end
when :or
yield_self do
c1, c2 = node.children
t1 = synthesize(c1, hint: hint)
t2 = synthesize(c2, hint: unwrap(t1))
type = union_type(unwrap(t1), t2)
typing.add_typing(node, type)
end
when :if
cond, true_clause, false_clause = node.children
synthesize cond
truthy_vars = TypeConstruction.truthy_variables(cond)
if true_clause
true_type, true_env = for_branch(true_clause, truthy_vars: truthy_vars).yield_self do |constructor|
type = constructor.synthesize(true_clause, hint: hint)
[type, constructor.type_env]
end
end
if false_clause
false_type, false_env = for_branch(false_clause).yield_self do |constructor|
type = constructor.synthesize(false_clause, hint: hint)
[type, constructor.type_env]
end
end
type_env.join!([true_env, false_env].compact)
typing.add_typing(node, union_type(true_type, false_type))
when :case
yield_self do
cond, *whens = node.children
if cond
cond_type = expand_alias(synthesize(cond))
if cond_type.is_a?(AST::Types::Union)
var_names = TypeConstruction.value_variables(cond)
var_types = cond_type.types.dup
end
end
pairs = whens.each.with_object([]) do |clause, pairs|
if clause&.type == :when
test_types = clause.children.take(clause.children.size - 1).map do |child|
expand_alias(synthesize(child, hint: hint))
end
if (body = clause.children.last)
if var_names && var_types && test_types.all? {|type| type.is_a?(AST::Types::Name::Class) }
var_types_in_body = test_types.flat_map {|test_type|
filtered_types = var_types.select {|var_type| var_type.is_a?(AST::Types::Name::Base) && var_type.name == test_type.name }
if filtered_types.empty?
to_instance_type(test_type)
else
filtered_types
end
}
var_types.reject! {|type|
var_types_in_body.any? {|test_type|
type.is_a?(AST::Types::Name::Base) && test_type.name == type.name
}
}
type_case_override = var_names.each.with_object({}) do |var_name, hash|
hash[var_name] = union_type(*var_types_in_body)
end
else
type_case_override = nil
end
for_branch(body, type_case_override: type_case_override).yield_self do |body_construction|
type = body_construction.synthesize(body, hint: hint)
pairs << [type, body_construction.type_env]
end
else
pairs << [AST::Builtin.nil_type, nil]
end
else
if clause
if var_types
if !var_types.empty?
type_case_override = var_names.each.with_object({}) do |var_name, hash|
hash[var_name] = union_type(*var_types)
end
var_types.clear
else
typing.add_error Errors::ElseOnExhaustiveCase.new(node: node, type: cond_type)
type_case_override = var_names.each.with_object({}) do |var_name, hash|
hash[var_name] = AST::Builtin.any_type
end
end
end
for_branch(clause, type_case_override: type_case_override).yield_self do |body_construction|
type = body_construction.synthesize(clause, hint: hint)
pairs << [type, body_construction.type_env]
end
end
end
end
types = pairs.map(&:first)
envs = pairs.map(&:last)
if var_types
unless var_types.empty? || whens.last
types.push AST::Builtin.nil_type
end
end
type_env.join!(envs.compact)
typing.add_typing(node, union_type(*types))
end
when :rescue
yield_self do
body, *resbodies, else_node = node.children
body_type = synthesize(body) if body
resbody_pairs = resbodies.map do |resbody|
exn_classes, assignment, body = resbody.children
if exn_classes
case exn_classes.type
when :array
exn_types = exn_classes.children.map {|child| synthesize(child) }
else
Steep.logger.error "Unexpected exception list: #{exn_classes.type}"
end
end
if assignment
case assignment.type
when :lvasgn
var_name = assignment.children[0].name
else
Steep.logger.error "Unexpected rescue variable assignment: #{assignment.type}"
end
end
type_override = {}
case
when exn_classes && var_name
instance_types = exn_types.map do |type|
type = expand_alias(type)
case
when type.is_a?(AST::Types::Name::Class)
to_instance_type(type)
else
AST::Builtin.any_type
end
end
type_override[var_name] = AST::Types::Union.build(types: instance_types)
when var_name
type_override[var_name] = AST::Builtin.any_type
end
resbody_construction = for_branch(resbody, type_case_override: type_override)
type = if body
resbody_construction.synthesize(body)
else
AST::Builtin.nil_type
end
[type, resbody_construction.type_env]
end
resbody_types, resbody_envs = resbody_pairs.transpose
if else_node
else_construction = for_branch(else_node)
else_type = else_construction.synthesize(else_node)
else_env = else_construction.type_env
end
type_env.join!([*resbody_envs, else_env].compact)
types = [body_type, *resbody_types, else_type].compact
typing.add_typing(node, union_type(*types))
end
when :resbody
yield_self do
klasses, asgn, body = node.children
synthesize(klasses) if klasses
synthesize(asgn) if asgn
body_type = synthesize(body) if body
typing.add_typing(node, body_type)
end
when :ensure
yield_self do
body, ensure_body = node.children
body_type = synthesize(body) if body
synthesize(ensure_body) if ensure_body
typing.add_typing(node, union_type(body_type))
end
when :masgn
type_masgn(node)
when :while, :while_post, :until, :until_post
yield_self do
cond, body = node.children
synthesize(cond)
truthy_vars = node.type == :while ? TypeConstruction.truthy_variables(cond) : Set.new
if body
for_loop = for_branch(body, truthy_vars: truthy_vars).with(break_context: BreakContext.new(break_type: nil, next_type: nil))
for_loop.synthesize(body)
type_env.join!([for_loop.type_env])
end
typing.add_typing(node, AST::Builtin.any_type)
end
when :irange, :erange
types = node.children.map {|n| synthesize(n) }
type = AST::Builtin::Range.instance_type(union_type(*types))
typing.add_typing(node, type)
when :regexp
each_child_node(node) do |child|
synthesize(child)
end
typing.add_typing(node, AST::Builtin::Regexp.instance_type)
when :regopt
# ignore
typing.add_typing(node, AST::Builtin.any_type)
when :nth_ref, :back_ref
typing.add_typing(node, AST::Builtin::String.instance_type)
when :or_asgn, :and_asgn
yield_self do
_, rhs = node.children
rhs_type = synthesize(rhs)
typing.add_typing(node, rhs_type)
end
when :defined?
each_child_node(node) do |child|
synthesize(child)
end
typing.add_typing(node, AST::Builtin.any_type)
when :gvasgn
yield_self do
name, rhs = node.children
type = checker.builder.absolute_type(checker.builder.signatures.find_gvar(name)&.type,
current: AST::Namespace.root)
if type
check(rhs, type) do |_, rhs_type, result|
typing.add_error(Errors::IncompatibleAssignment.new(node: node,
lhs_type: type,
rhs_type: rhs_type,
result: result))
end
else
synthesize(rhs)
fallback_to_any node
end
end
when :gvar
yield_self do
name = node.children.first
type = type_env.get(gvar: name) do
typing.add_error Errors::FallbackAny.new(node: node)
end
typing.add_typing(node, type)
end
when :block_pass
yield_self do
value = node.children[0]
if hint.is_a?(AST::Types::Proc) && value.type == :sym
if hint.one_arg?
# Assumes Symbol#to_proc implementation
param_type = hint.params.required[0]
interface = checker.resolve(param_type)
method = interface.methods[value.children[0]]
if method
return_types = method.types.flat_map do |method_type|
if method_type.params.each_type.count == 0
[method_type.return_type]
end
end
if return_types.any?
type = AST::Types::Proc.new(params: Interface::Params.empty.update(required: [param_type]),
return_type: AST::Types::Union.build(types: return_types))
end
end
else
Steep.logger.error "Passing multiple args through Symbol#to_proc is not supported yet"
end
end
type ||= synthesize(node.children[0], hint: hint)
typing.add_typing node, type
end
when :blockarg
yield_self do
each_child_node node do |child|
synthesize(child)
end
typing.add_typing node, AST::Builtin.any_type
end
when :splat, :sclass
yield_self do
Steep.logger.error "Unsupported node #{node.type}"
each_child_node node do |child|
synthesize(child)
end
typing.add_typing node, AST::Builtin.any_type
end
else
raise "Unexpected node: #{node.inspect}, #{node.location.expression}"
end
end
end
def try_method_type(node, receiver_type:, method_type:, args:, arg_pairs:, block_params:, block_body:, child_typing:)
def try_method_type(node, receiver_type:, method_type:, args:, arg_pairs:, block_params:, block_body:, child_typing:)
fresh_types = method_type.type_params.map {|x| AST::Types::Var.fresh(x) }
fresh_vars = Set.new(fresh_types.map(&:name))
instantiation = Interface::Substitution.build(method_type.type_params, fresh_types)
construction = self.class.new(
checker: checker,
source: source,
annotations: annotations,
type_env: type_env,
typing: child_typing,
self_type: self_type,
method_context: method_context,
block_context: block_context,
module_context: module_context,
break_context: break_context
)
method_type.instantiate(instantiation).yield_self do |method_type|
constraints = Subtyping::Constraints.new(unknowns: fresh_types.map(&:name))
variance = Subtyping::VariableVariance.from_method_type(method_type)
occurence = Subtyping::VariableOccurence.from_method_type(method_type)
arg_pairs.each do |pair|
case pair
when Array
(arg_node, param_type) = pair
param_type = param_type.subst(instantiation)
arg_type = if arg_node.type == :splat
type = construction.synthesize(arg_node.children[0])
child_typing.add_typing(arg_node, type)
else
construction.synthesize(arg_node, hint: param_type)
end
relation = Subtyping::Relation.new(
sub_type: arg_type,
super_type: param_type
)
checker.check(relation, constraints: constraints).else do |result|
return Errors::ArgumentTypeMismatch.new(
node: arg_node,
receiver_type: receiver_type,
expected: relation.super_type,
actual: relation.sub_type
)
end
else
# keyword
result = check_keyword_arg(receiver_type: receiver_type,
node: pair,
method_type: method_type,
constraints: constraints)
if result.is_a?(Errors::Base)
return result
end
end
end
if block_params && method_type.block
block_annotations = source.annotations(block: node, builder: checker.builder, current_module: current_namespace)
block_params_ = TypeInference::BlockParams.from_node(block_params, annotations: block_annotations)
block_param_hint = block_params_.params_type(hint: method_type.block.type.params)
relation = Subtyping::Relation.new(
sub_type: AST::Types::Proc.new(params: block_param_hint, return_type: AST::Types::Any.new),
super_type: method_type.block.type
)
checker.check(relation, constraints: constraints).else do |result|
return Errors::IncompatibleBlockParameters.new(
node: node,
method_type: method_type
)
end
end
case
when method_type.block && block_params
Steep.logger.debug "block is okay: method_type=#{method_type}"
Steep.logger.debug "Constraints = #{constraints}"
begin
method_type.subst(constraints.solution(checker, variance: variance, variables: occurence.params)).yield_self do |method_type|
block_type = construction.type_block(block_param_hint: method_type.block.type.params,
block_type_hint: method_type.block.type.return_type,
node_type_hint: method_type.return_type,
block_params: block_params_,
block_body: block_body,
block_annotations: block_annotations)
relation = Subtyping::Relation.new(sub_type: block_type.return_type,
super_type: method_type.block.type.return_type)
result = checker.check(relation, constraints: constraints)
case result
when Subtyping::Result::Success
method_type.return_type.subst(constraints.solution(checker, variance: variance, variables: fresh_vars)).yield_self do |ret_type|
if block_annotations.break_type
AST::Types::Union.new(types: [block_annotations.break_type, ret_type])
else
ret_type
end
end
when Subtyping::Result::Failure
Errors::BlockTypeMismatch.new(node: node,
expected: method_type.block.type,
actual: block_type,
result: result)
end
end
rescue Subtyping::Constraints::UnsatisfiableConstraint => exn
Errors::UnsatisfiableConstraint.new(node: node,
method_type: method_type,
var: exn.var,
sub_type: exn.sub_type,
super_type: exn.super_type,
result: exn.result)
end
when method_type.block && args.block_pass_arg
begin
method_type.subst(constraints.solution(checker, variance: variance, variables: occurence.params)).yield_self do |method_type|
block_type = synthesize(args.block_pass_arg, hint: method_type.block.type)
relation = Subtyping::Relation.new(sub_type: block_type,
super_type: method_type.block.yield_self do |expected_block|
if expected_block.optional?
AST::Builtin.optional(expected_block.type)
else
expected_block.type
end
end)
result = checker.check(relation, constraints: constraints)
case result
when Subtyping::Result::Success
method_type.return_type.subst(constraints.solution(checker, variance: variance, variables: fresh_vars))
when Subtyping::Result::Failure
Errors::BlockTypeMismatch.new(node: node,
expected: method_type.block.type,
actual: block_type,
result: result)
end
end
rescue Subtyping::Constraints::UnsatisfiableConstraint => exn
Errors::UnsatisfiableConstraint.new(node: node,
method_type: method_type,
var: exn.var,
sub_type: exn.sub_type,
super_type: exn.super_type,
result: exn.result)
end
when (!method_type.block || method_type.block.optional?) && !block_params && !block_body && !args.block_pass_arg
# OK, without block
method_type.subst(constraints.solution(checker, variance: variance, variables: fresh_vars)).return_type
when !method_type.block && (block_params || args.block_pass_arg)
Errors::UnexpectedBlockGiven.new(
node: node,
method_type: method_type
)
when method_type.block && !method_type.block.optional? && !block_params && !block_body && !args.block_pass_arg
Errors::RequiredBlockMissing.new(
node: node,
method_type: method_type
)
else
raise "Unexpected case condition"
end
end
end