Steepコードリーディング(9日目)
TypeCheckService#.type_check
の続きを見ていきます。
どうもconstruction.synthesize(source.node)
で型検査してるっぽいんだよな。
Steep::TypeConstruction
Steep::TypeConstruction#synthesize
Qiitaのエディタが固まってまともに使えなくなるくらい長いです。別記事で1つずつ見ていきます。
def synthesize(node, hint: nil, condition: false)
Steep.logger.tagged "synthesize:(#{node.location&.yield_self {|loc| loc.expression.to_s.split(/:/, 2).last } || "-"})" do
Steep.logger.debug node.type
case node.type
when :begin, :kwbegin
yield_self do
end_pos = node.loc.expression.end_pos
*mid_nodes, last_node = each_child_node(node).to_a
if last_node
pair = mid_nodes.inject(Pair.new(type: AST::Builtin.nil_type, constr: self)) do |pair, node|
pair.constr.synthesize(node).yield_self {|p| pair + p }.tap do |new_pair|
if new_pair.constr.context != pair.constr.context
# update context
range = node.loc.expression.end_pos..end_pos
typing.cursor_context.set(range, new_pair.constr.context)
end
end
end
p = pair.constr.synthesize(last_node, hint: hint, condition: condition)
last_pair = pair + p
last_pair.constr.add_typing(node, type: last_pair.type, constr: last_pair.constr)
else
add_typing(node, type: AST::Builtin.nil_type)
end
end
when :lvasgn
yield_self do
name, rhs = node.children
case name
when :_, :__any__
synthesize(rhs, hint: AST::Builtin.any_type).yield_self do |pair|
add_typing(node, type: AST::Builtin.any_type, constr: pair.constr)
end
when :__skip__
add_typing(node, type: AST::Builtin.any_type)
else
if enforced_type = context.type_env.enforced_type(name)
case
when !hint
hint = enforced_type
when check_relation(sub_type: enforced_type, super_type: hint).success?
# enforced_type is compatible with hint and more specific to hint.
# This typically happens when hint is untyped, top, or void.
hint = enforced_type
end
end
if rhs
rhs_type, rhs_constr, rhs_context = synthesize(rhs, hint: hint).to_ary
constr = rhs_constr.update_type_env do |type_env|
var_type = rhs_type
if enforced_type = type_env.enforced_type(name)
if result = no_subtyping?(sub_type: rhs_type, super_type: enforced_type)
typing.add_error(
Diagnostic::Ruby::IncompatibleAssignment.new(
node: node,
lhs_type: enforced_type,
rhs_type: rhs_type,
result: result
)
)
var_type = enforced_type
end
if rhs_type.is_a?(AST::Types::Any)
var_type = enforced_type
end
end
type_env.assign_local_variable(name, var_type, enforced_type)
end
constr.add_typing(node, type: rhs_type)
else
add_typing(node, type: enforced_type || AST::Builtin.any_type)
end
end
end
when :lvar
yield_self do
var = node.children[0]
if SPECIAL_LVAR_NAMES.include?(var)
add_typing node, type: AST::Builtin.any_type
else
if (type = context.type_env[var])
add_typing node, type: type
else
fallback_to_any(node)
end
end
end
when :ivasgn
name = node.children[0]
rhs = node.children[1]
rhs_type, constr = synthesize(rhs, hint: context.type_env[name])
constr.ivasgn(node, rhs_type)
when :ivar
yield_self do
name = node.children[0]
if type = context.type_env[name]
add_typing(node, type: type)
else
fallback_to_any node
end
end
when :match_with_lvasgn
each_child_node(node) do |child|
synthesize(child)
end
add_typing(node, type: AST::Builtin.any_type)
when :op_asgn
yield_self do
lhs, op, rhs = node.children
case lhs.type
when :lvasgn
var_node = lhs.updated(:lvar)
send_node = rhs.updated(:send, [var_node, op, rhs])
new_node = node.updated(:lvasgn, [lhs.children[0], send_node])
type, constr = synthesize(new_node, hint: hint)
constr.add_typing(node, type: type)
when :ivasgn
var_node = lhs.updated(:ivar)
send_node = rhs.updated(:send, [var_node, op, rhs])
new_node = node.updated(:ivasgn, [lhs.children[0], send_node])
type, constr = synthesize(new_node, hint: hint)
constr.add_typing(node, type: type)
when :cvasgn
var_node = lhs.updated(:cvar)
send_node = rhs.updated(:send, [var_node, op, rhs])
new_node = node.updated(:cvasgn, [lhs.children[0], send_node])
type, constr = synthesize(new_node, hint: hint)
constr.add_typing(node, type: type)
when :gvasgn
var_node = lhs.updated(:gvar)
send_node = rhs.updated(:send, [var_node, op, rhs])
new_node = node.updated(:gvasgn, [lhs.children[0], send_node])
type, constr = synthesize(new_node, hint: hint)
constr.add_typing(node, type: type)
when :send
new_rhs = rhs.updated(:send, [lhs, node.children[1], node.children[2]])
new_node = lhs.updated(:send, [lhs.children[0], :"#{lhs.children[1]}=", *lhs.children.drop(2), new_rhs])
type, constr = synthesize(new_node, hint: hint)
constr.add_typing(node, type: type)
else
Steep.logger.error("Unexpected op_asgn lhs: #{lhs.type}")
_, constr = synthesize(rhs)
constr.add_typing(node, type: AST::Builtin.any_type)
end
end
when :super
yield_self do
if self_type && method_context!.method
if super_def = method_context!.super_method
super_method = Interface::Shape::Entry.new(
method_name: method_context!.name,
private_method: true,
overloads: super_def.defs.map {|type_def|
type = checker.factory.method_type(type_def.type)
Interface::Shape::MethodOverload.new(type, [type_def])
}
)
call, constr = type_method_call(
node,
receiver_type: self_type,
method_name: method_context!.name || raise("method context must have a name"),
method: super_method,
arguments: node.children,
block_params: nil,
block_body: nil,
tapp: nil,
hint: hint
)
if call && constr
constr.add_call(call)
else
error = Diagnostic::Ruby::UnresolvedOverloading.new(
node: node,
receiver_type: self_type,
method_name: method_context!.name,
method_types: super_method.method_types
)
call = TypeInference::MethodCall::Error.new(
node: node,
context: context.call_context,
method_name: method_context!.name || raise("method context must have a name"),
receiver_type: self_type,
errors: [error]
)
constr = synthesize_children(node)
fallback_to_any(node) { error }
end
else
type_check_untyped_args(node.children).fallback_to_any(node) do
Diagnostic::Ruby::UnexpectedSuper.new(node: node, method: method_context!.name)
end
end
else
type_check_untyped_args(node.children).fallback_to_any(node)
end
end
when :def
yield_self do
# @type var node: Parser::AST::Node & Parser::AST::_DefNode
name, args_node, body_node = node.children
with_method_constr(
name,
node,
args: args_node.children,
self_type: module_context&.instance_type,
definition: module_context&.instance_definition
) do |new|
# @type var new: TypeConstruction
new.typing.cursor_context.set_node_context(node, new.context)
new.typing.cursor_context.set_body_context(node, new.context)
new.method_context!.tap do |method_context|
if method_context.method
if owner = method_context.method.implemented_in || method_context.method.defined_in
method_name = InstanceMethodName.new(type_name: owner, method_name: name)
new.typing.source_index.add_definition(method: method_name, definition: node)
end
end
end
new = new.synthesize_children(args_node)
body_pair = if body_node
return_type = expand_alias(new.method_context!.return_type)
if !return_type.is_a?(AST::Types::Void)
new.check(body_node, return_type) do |_, actual_type, result|
if new.method_context!.attribute_setter?
typing.add_error(
Diagnostic::Ruby::SetterBodyTypeMismatch.new(
node: node,
expected: new.method_context!.return_type,
actual: actual_type,
result: result,
method_name: new.method_context!.name
)
)
else
typing.add_error(
Diagnostic::Ruby::MethodBodyTypeMismatch.new(
node: node,
expected: new.method_context!.return_type,
actual: actual_type,
result: result
)
)
end
end
else
new.synthesize(body_node)
end
else
return_type = expand_alias(new.method_context!.return_type)
if !return_type.is_a?(AST::Types::Void)
result = check_relation(sub_type: AST::Builtin.nil_type, super_type: return_type)
if result.failure?
if new.method_context!.attribute_setter?
typing.add_error(
Diagnostic::Ruby::SetterBodyTypeMismatch.new(
node: node,
expected: new.method_context!.return_type,
actual: AST::Builtin.nil_type,
result: result,
method_name: new.method_context!.name
)
)
else
typing.add_error(
Diagnostic::Ruby::MethodBodyTypeMismatch.new(
node: node,
expected: new.method_context!.return_type,
actual: AST::Builtin.nil_type,
result: result
)
)
end
end
end
Pair.new(type: AST::Builtin.nil_type, constr: new)
end
if body_node
# Add context to ranges from the end of the method body to the beginning of the `end` keyword
if node.loc.end
# Skip end-less def
begin_pos = body_node.loc.expression.end_pos
end_pos = node.loc.end.begin_pos
typing.cursor_context.set(begin_pos..end_pos, body_pair.context)
end
end
if module_context
module_context.defined_instance_methods << node.children[0]
end
add_typing(node, type: AST::Builtin::Symbol.instance_type)
end
end
when :defs
synthesize(node.children[0]).type.tap do |self_type|
self_type = expand_self(self_type)
definition =
case self_type
when AST::Types::Name::Instance
name = self_type.name
checker.factory.definition_builder.build_instance(name)
when AST::Types::Name::Singleton
name = self_type.name
checker.factory.definition_builder.build_singleton(name)
end
args_node = node.children[2]
new = for_new_method(
node.children[1],
node,
args: args_node.children,
self_type: self_type,
definition: definition
)
new.typing.cursor_context.set_node_context(node, new.context)
new.typing.cursor_context.set_body_context(node, new.context)
new.method_context!.tap do |method_context|
if method_context.method
name_ = node.children[1]
method_name =
case self_type
when AST::Types::Name::Instance
InstanceMethodName.new(type_name: method_context.method.implemented_in || raise, method_name: name_)
when AST::Types::Name::Singleton
SingletonMethodName.new(type_name: method_context.method.implemented_in || raise, method_name: name_)
end
new.typing.source_index.add_definition(method: method_name, definition: node)
end
end
new = new.synthesize_children(args_node)
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.is_a?(AST::Types::Void)
new.check(node.children[3], return_type) do |return_type, actual_type, result|
typing.add_error(
Diagnostic::Ruby::MethodBodyTypeMismatch.new(
node: node,
expected: return_type,
actual: actual_type,
result: result
)
)
end
else
new.synthesize(node.children[3])
end
end
end
if node.children[0].type == :self
module_context.defined_module_methods << node.children[1]
end
add_typing(node, type: AST::Builtin::Symbol.instance_type)
when :return
yield_self do
method_return_type =
if method_context
expand_alias(method_context.return_type)
end
case node.children.size
when 0
value_type = AST::Builtin.nil_type
constr = self
when 1
return_value_node = node.children[0]
value_type, constr = synthesize(return_value_node, hint: method_return_type)
else
# It returns an array
array = node.updated(:array)
value_type, constr = synthesize(array, hint: method_return_type)
end
if method_return_type
unless method_context.nil? || method_return_type.is_a?(AST::Types::Void)
result = constr.check_relation(sub_type: value_type, super_type: method_return_type)
if result.failure?
if method_context.attribute_setter?
typing.add_error(
Diagnostic::Ruby::SetterReturnTypeMismatch.new(
node: node,
method_name: method_context.name,
expected: method_return_type,
actual: value_type,
result: result
)
)
else
typing.add_error(
Diagnostic::Ruby::ReturnTypeMismatch.new(
node: node,
expected: method_return_type,
actual: value_type,
result: result
)
)
end
end
end
end
constr.add_typing(node, type: AST::Builtin.bottom_type)
end
when :break
value = node.children[0]
if break_context
break_type = break_context.break_type
if value
check(value, break_type) do |break_type, actual_type, result|
typing.add_error(
Diagnostic::Ruby::BreakTypeMismatch.new(
node: node,
expected: break_type,
actual: actual_type,
result: result
)
)
end
else
unless break_type.is_a?(AST::Types::Bot)
check_relation(sub_type: AST::Builtin.nil_type, super_type: break_type).else do |result|
typing.add_error(
Diagnostic::Ruby::ImplicitBreakValueMismatch.new(
node: node,
jump_type: break_type,
result: result
)
)
end
end
end
else
synthesize(value) if value
typing.add_error Diagnostic::Ruby::UnexpectedJump.new(node: node)
end
add_typing(node, type: AST::Builtin.bottom_type)
when :next
value = node.children[0]
if break_context
if next_type = break_context.next_type
next_type = deep_expand_alias(next_type) || next_type
if value
_, constr = check(value, next_type) do |break_type, actual_type, result|
typing.add_error(
Diagnostic::Ruby::BreakTypeMismatch.new(
node: node,
expected: break_type,
actual: actual_type,
result: result
)
)
end
else
check_relation(sub_type: AST::Builtin.nil_type, super_type: next_type).else do |result|
typing.add_error(
Diagnostic::Ruby::BreakTypeMismatch.new(
node: node,
expected: next_type,
actual: AST::Builtin.nil_type,
result: result
)
)
end
end
else
if value
synthesize(value)
typing.add_error Diagnostic::Ruby::UnexpectedJumpValue.new(node: node)
end
end
else
synthesize(value) if value
typing.add_error Diagnostic::Ruby::UnexpectedJump.new(node: node)
end
add_typing(node, type: AST::Builtin.bottom_type)
when :retry
add_typing(node, type: AST::Builtin.bottom_type)
when :procarg0
yield_self do
constr = self #: TypeConstruction
node.children.each do |arg|
if arg.is_a?(Symbol)
if SPECIAL_LVAR_NAMES === arg
_, constr = add_typing(node, type: AST::Builtin.any_type)
else
type = context.type_env[arg]
if type
_, constr = add_typing(node, type: type)
else
type = AST::Builtin.any_type
_, constr = lvasgn(node, type)
end
end
else
_, constr = constr.synthesize(arg)
end
end
Pair.new(constr: constr, type: AST::Builtin.any_type)
end
when :mlhs
yield_self do
constr = self #: TypeConstruction
node.children.each do |arg|
_, constr = constr.synthesize(arg)
end
Pair.new(constr: constr, type: AST::Builtin.any_type)
end
when :arg, :kwarg
yield_self do
var = node.children[0]
if SPECIAL_LVAR_NAMES.include?(var)
add_typing(node, type: AST::Builtin.any_type)
else
type = context.type_env[var]
if type
add_typing(node, type: type)
else
type = AST::Builtin.any_type
lvasgn(node, type)
end
end
end
when :optarg, :kwoptarg
yield_self do
var = node.children[0]
rhs = node.children[1]
if SPECIAL_LVAR_NAMES.include?(var)
synthesize(rhs)
add_typing(node, type: AST::Builtin.any_type)
else
var_type = context.type_env[var]
if var_type
type, constr = check(rhs, var_type) do |expected_type, actual_type, result|
typing.add_error(
Diagnostic::Ruby::IncompatibleAssignment.new(
node: node,
lhs_type: expected_type,
rhs_type: actual_type,
result: result
)
)
end
else
type, constr = synthesize(rhs)
end
constr.add_typing(node, type: type)
end
end
when :restarg
yield_self do
var = node.children[0]
if SPECIAL_LVAR_NAMES.include?(var)
return add_typing(node, type: AST::Builtin.any_type)
end
type = context.type_env[var]
unless type
if context.method_context&.method_type
Steep.logger.error { "Unknown variable: #{node}" }
end
typing.add_error Diagnostic::Ruby::FallbackAny.new(node: node)
type = AST::Builtin::Array.instance_type(AST::Builtin.any_type)
end
add_typing(node, type: type)
end
when :kwrestarg
yield_self do
var = node.children[0]
if SPECIAL_LVAR_NAMES.include?(var)
return add_typing(node, type: AST::Builtin.any_type)
end
type = context.type_env[var]
unless type
if context.method_context&.method_type
Steep.logger.error { "Unknown variable: #{node}" }
end
typing.add_error Diagnostic::Ruby::FallbackAny.new(node: node)
type = AST::Builtin::Hash.instance_type(AST::Builtin::Symbol.instance_type, AST::Builtin.any_type)
end
add_typing(node, type: type)
end
when :float
add_typing(node, type: AST::Builtin::Float.instance_type)
when :rational
add_typing(node, type: AST::Types::Name::Instance.new(name: TypeName("::Rational"), args: []))
when :complex
add_typing(node, type: AST::Types::Name::Instance.new(name: TypeName("::Complex"), args: []))
when :nil
add_typing(node, type: AST::Builtin.nil_type)
when :int
yield_self do
literal_type = test_literal_type(node.children[0], hint)
if literal_type
add_typing(node, type: literal_type)
else
add_typing(node, type: AST::Builtin::Integer.instance_type)
end
end
when :sym
yield_self do
literal_type = test_literal_type(node.children[0], hint)
if literal_type
add_typing(node, type: literal_type)
else
add_typing(node, type: AST::Builtin::Symbol.instance_type)
end
end
when :str
yield_self do
literal_type = test_literal_type(node.children[0], hint)
if literal_type
add_typing(node, type: literal_type)
else
add_typing(node, type: AST::Builtin::String.instance_type)
end
end
when :true, :false
ty = node.type == :true ? AST::Types::Literal.new(value: true) : AST::Types::Literal.new(value: false)
case
when hint && check_relation(sub_type: ty, super_type: hint).success? && !hint.is_a?(AST::Types::Any) && !hint.is_a?(AST::Types::Top)
add_typing(node, type: hint)
when condition
add_typing(node, type: ty)
else
add_typing(node, type: AST::Types::Boolean.instance)
end
when :hash, :kwargs
# :kwargs happens for method calls with keyword argument, but the method doesn't have keyword params.
# Conversion from kwargs to hash happens, and this when-clause is to support it.
type_hash(node, hint: hint).tap do |pair|
if pair.type == AST::Builtin::Hash.instance_type(fill_untyped: true)
case hint
when AST::Types::Any, AST::Types::Top, AST::Types::Void
# ok
else
unless hint == pair.type
pair.constr.typing.add_error Diagnostic::Ruby::UnannotatedEmptyCollection.new(node: node)
end
end
end
end
when :dstr, :xstr
each_child_node(node) do |child|
synthesize(child)
end
add_typing(node, type: AST::Builtin::String.instance_type)
when :dsym
each_child_node(node) do |child|
synthesize(child)
end
add_typing(node, type: AST::Builtin::Symbol.instance_type)
when :class
yield_self do
constr = self
# @type var name_node: Parser::AST::Node
# @type var super_node: Parser::AST::Node?
name_node, super_node, _ = node.children
if name_node.type == :const
_, constr, class_name = synthesize_constant_decl(name_node, name_node.children[0], name_node.children[1]) do
typing.add_error(
Diagnostic::Ruby::UnknownConstant.new(node: name_node, name: name_node.children[1]).class!
)
end
else
_, constr = synthesize(name_node)
end
if class_name
typing.source_index.add_definition(constant: class_name, definition: name_node)
end
if super_node
if super_node.type == :const
_, constr, super_name = constr.synthesize_constant(super_node, super_node.children[0], super_node.children[1]) do
typing.add_error(
Diagnostic::Ruby::UnknownConstant.new(node: super_node, name: super_node.children[1]).class!
)
end
if super_name
typing.source_index.add_reference(constant: super_name, ref: super_node)
end
else
_, constr = synthesize(super_node, hint: nil, condition: false)
end
end
with_class_constr(node, class_name, super_name) do |constructor|
if module_type = constructor.module_context&.module_type
_, constructor = constructor.add_typing(name_node, type: module_type)
else
_, constructor = constructor.fallback_to_any(name_node)
end
constructor.typing.cursor_context.set_node_context(node, constructor.context)
constructor.typing.cursor_context.set_body_context(node, constructor.context)
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 || raise)
end
end
add_typing(node, type: AST::Builtin.nil_type)
end
when :module
yield_self do
constr = self
# @type var name_node: Parser::AST::Node
name_node, _ = node.children
if name_node.type == :const
_, constr, module_name = synthesize_constant_decl(name_node, name_node.children[0], name_node.children[1]) do
typing.add_error Diagnostic::Ruby::UnknownConstant.new(node: name_node, name: name_node.children[1]).module!
end
else
_, constr = synthesize(name_node)
end
if module_name
constr.typing.source_index.add_definition(constant: module_name, definition: name_node)
end
with_module_constr(node, module_name) do |constructor|
if module_type = constructor.module_context&.module_type
_, constructor = constructor.add_typing(name_node, type: module_type)
else
_, constructor = constructor.fallback_to_any(name_node)
end
constructor.typing.cursor_context.set_node_context(node, constructor.context)
constructor.typing.cursor_context.set_body_context(node, constructor.context)
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 || raise)
end
end
add_typing(node, type: AST::Builtin.nil_type)
end
when :sclass
yield_self do
type, constr = synthesize(node.children[0]).to_ary
with_sclass_constr(node, type) do |constructor|
unless constructor
typing.add_error(
Diagnostic::Ruby::UnsupportedSyntax.new(
node: node,
message: "sclass receiver must be instance type or singleton type, but type given `#{type}`"
)
)
return constr.add_typing(node, type: AST::Builtin.nil_type)
end
constructor.typing.cursor_context.set_node_context(node, constructor.context)
constructor.typing.cursor_context.set_body_context(node, constructor.context)
constructor.synthesize(node.children[1]) if node.children[1]
if constructor.module_context.instance_definition && module_context.module_definition
if constructor.module_context.instance_definition.type_name == module_context.module_definition.type_name
module_context.defined_module_methods.merge(constructor.module_context.defined_instance_methods)
end
end
end
constr.add_typing(node, type: AST::Builtin.nil_type)
end
when :self
add_typing node, type: AST::Types::Self.instance
when :cbase
add_typing node, type: AST::Types::Void.instance
when :const
yield_self do
type, constr, name = synthesize_constant(node, node.children[0], node.children[1])
if name
typing.source_index.add_reference(constant: name, ref: node)
end
Pair.new(type: type, constr: constr)
end
when :casgn
yield_self do
constant_type, constr, constant_name = synthesize_constant_decl(nil, node.children[0], node.children[1]) do
typing.add_error(
Diagnostic::Ruby::UnknownConstant.new(
node: node,
name: node.children[1]
)
)
end
if constant_name
typing.source_index.add_definition(constant: constant_name, definition: node)
end
value_type, constr = constr.synthesize(node.children.last, hint: constant_type)
result = check_relation(sub_type: value_type, super_type: constant_type)
if result.failure?
typing.add_error(
Diagnostic::Ruby::IncompatibleAssignment.new(
node: node,
lhs_type: constant_type,
rhs_type: value_type,
result: result
)
)
constr.add_typing(node, type: constant_type)
else
constr.add_typing(node, type: value_type)
end
end
when :yield
if method_context && method_context.method_type
if block_type = method_context.block_type
if block_type.type.params
type = AST::Types::Proc.new(
type: block_type.type,
block: nil,
self_type: block_type.self_type
)
args = TypeInference::SendArgs.new(
node: node,
arguments: node.children,
type: type
)
# @type var errors: Array[Diagnostic::Ruby::Base]
errors = []
constr = type_check_args(
nil,
args,
Subtyping::Constraints.new(unknowns: []),
errors
)
errors.each do |error|
typing.add_error(error)
end
else
constr = type_check_untyped_args(node.children)
end
add_typing(node, type: block_type.type.return_type)
else
typing.add_error(Diagnostic::Ruby::UnexpectedYield.new(node: node))
fallback_to_any node
end
else
fallback_to_any node
end
when :zsuper
yield_self do
if method_context && method_context.method
if method_context.super_method
types = method_context.super_method.method_types.map {|method_type|
checker.factory.method_type(method_type).type.return_type
}
add_typing(node, type: union_type(*types))
else
fallback_to_any(node) do
Diagnostic::Ruby::UnexpectedSuper.new(node: node, method: method_context.name)
end
end
else
fallback_to_any node
end
end
when :array
yield_self do
if node.children.empty?
if hint
array = AST::Builtin::Array.instance_type(AST::Builtin.any_type)
if check_relation(sub_type: array, super_type: hint).success?
add_typing node, type: hint
else
add_typing node, type: array
end
else
typing.add_error Diagnostic::Ruby::UnannotatedEmptyCollection.new(node: node)
add_typing node, type: AST::Builtin::Array.instance_type(AST::Builtin.any_type)
end
else
if hint
tuples = select_flatten_types(hint) {|type| type.is_a?(AST::Types::Tuple) } #: Array[AST::Types::Tuple]
unless tuples.empty?
tuples.each do |tuple|
typing.new_child() do |child_typing|
if pair = with_new_typing(child_typing).try_tuple_type(node, tuple)
return pair.with(constr: pair.constr.save_typing)
end
end
end
end
end
if hint
arrays = select_flatten_types(hint) {|type| AST::Builtin::Array.instance_type?(type) } #: Array[AST::Types::Name::Instance]
unless arrays.empty?
arrays.each do |array|
typing.new_child() do |child_typing|
pair = with_new_typing(child_typing).try_array_type(node, array)
if pair.constr.check_relation(sub_type: pair.type, super_type: hint).success?
return pair.with(constr: pair.constr.save_typing)
end
end
end
end
end
try_array_type(node, nil)
end
end
when :and
yield_self do
left_node, right_node = node.children
left_type, constr, left_context = synthesize(left_node, hint: hint, condition: true).to_ary
interpreter = TypeInference::LogicTypeInterpreter.new(subtyping: checker, typing: typing, config: builder_config)
left_truthy, left_falsy = interpreter.eval(env: left_context.type_env, node: left_node)
if left_type.is_a?(AST::Types::Logic::Env)
left_type = left_type.type
end
right_type, constr, right_context =
constr
.update_type_env { left_truthy.env }
.tap {|constr| typing.cursor_context.set_node_context(right_node, constr.context) }
.for_branch(right_node)
.synthesize(right_node, hint: hint, condition: true).to_ary
right_truthy, right_falsy = interpreter.eval(env: right_context.type_env, node: right_node)
case
when left_truthy.unreachable
# Always left_falsy
env = left_falsy.env
type = left_falsy.type
when left_falsy.unreachable
# Always left_truthy ==> right
env = right_context.type_env
type = right_type
when right_truthy.unreachable && right_falsy.unreachable
env = left_falsy.env
type = left_falsy.type
else
env = context.type_env.join(left_falsy.env, right_context.type_env)
type = union_type(left_falsy.type, right_type)
unless type.is_a?(AST::Types::Any)
if check_relation(sub_type: type, super_type: AST::Types::Boolean.instance).success?
type = AST::Types::Boolean.instance
end
end
end
if condition
type = AST::Types::Logic::Env.new(
truthy: right_truthy.env,
falsy: context.type_env.join(left_falsy.env, right_falsy.env),
type: type
)
end
constr.update_type_env { env }.add_typing(node, type: type)
end
when :or
yield_self do
left_node, right_node = node.children
if hint
left_hint = union_type_unify(hint, AST::Builtin.nil_type, AST::Builtin.false_type)
end
left_type, constr, left_context = synthesize(left_node, hint: left_hint, condition: true).to_ary
interpreter = TypeInference::LogicTypeInterpreter.new(subtyping: checker, typing: typing, config: builder_config)
left_truthy, left_falsy = interpreter.eval(env: left_context.type_env, node: left_node)
if left_type.is_a?(AST::Types::Logic::Env)
left_type = left_type.type
end
right_type, constr, right_context =
constr
.update_type_env { left_falsy.env }
.tap {|constr| typing.cursor_context.set_node_context(right_node, constr.context) }
.for_branch(right_node)
.synthesize(right_node, hint: left_truthy.type, condition: true).to_ary
right_truthy, right_falsy = interpreter.eval(env: right_context.type_env, node: right_node)
case
when left_falsy.unreachable
env = left_truthy.env
type = left_truthy.type
when left_truthy.unreachable
# Always left_falsy ==> right
env = right_context.type_env
type = right_type
when right_truthy.unreachable && right_falsy.unreachable
env = left_truthy.env
type = left_truthy.type
else
env = context.type_env.join(left_truthy.env, right_context.type_env)
type = union_type(left_truthy.type, right_type)
unless type.is_a?(AST::Types::Any)
if check_relation(sub_type: type, super_type: AST::Types::Boolean.instance).success?
type = AST::Types::Boolean.instance
end
end
end
if condition
type = AST::Types::Logic::Env.new(
truthy: context.type_env.join(left_truthy.env, right_truthy.env),
falsy: right_falsy.env,
type: type
)
end
constr.update_type_env { env }.add_typing(node, type: type)
end
when :if
yield_self do
cond, true_clause, false_clause = node.children
cond_type, constr = synthesize(cond, condition: true).to_ary
interpreter = TypeInference::LogicTypeInterpreter.new(subtyping: checker, typing: constr.typing, config: builder_config)
truthy, falsy = interpreter.eval(env: constr.context.type_env, node: cond)
if true_clause
true_pair =
constr
.update_type_env { truthy.env }
.for_branch(true_clause)
.tap {|constr| typing.cursor_context.set_node_context(true_clause, constr.context) }
.synthesize(true_clause, hint: hint)
end
if false_clause
false_pair =
constr
.update_type_env { falsy.env }
.for_branch(false_clause)
.tap {|constr| typing.cursor_context.set_node_context(false_clause, constr.context) }
.synthesize(false_clause, hint: hint)
end
constr = constr.update_type_env do |env|
envs = [] #: Array[TypeInference::TypeEnv]
unless truthy.unreachable
if true_pair
unless true_pair.type.is_a?(AST::Types::Bot)
envs << true_pair.context.type_env
end
else
envs << truthy.env
end
end
if false_pair
unless falsy.unreachable
unless false_pair.type.is_a?(AST::Types::Bot)
envs << false_pair.context.type_env
end
end
else
envs << falsy.env
end
env.join(*envs)
end
if truthy.unreachable
if true_clause
_, _, _, loc = deconstruct_if_node!(node)
if loc.respond_to?(:keyword)
condition_loc = loc #: NodeHelper::condition_loc
case condition_loc.keyword.source
when "if", "elsif"
location = condition_loc.begin || condition_loc.keyword
when "unless"
# `else` token always exists
location = condition_loc.else || raise
end
else
location = true_clause.loc.expression
end
typing.add_error(
Diagnostic::Ruby::UnreachableBranch.new(
node: true_clause,
location: location || raise
)
)
end
end
if falsy.unreachable
if false_clause
_, _, _, loc = deconstruct_if_node!(node)
if loc.respond_to?(:keyword)
condition_loc = loc #: NodeHelper::condition_loc
case condition_loc.keyword.source
when "if", "elsif"
# `else` token always exists
location = condition_loc.else || raise
when "unless"
location = condition_loc.begin || condition_loc.keyword
end
else
location = false_clause.loc.expression
end
typing.add_error(
Diagnostic::Ruby::UnreachableBranch.new(
node: false_clause,
location: location || raise
)
)
end
end
node_type = union_type_unify(true_pair&.type || AST::Builtin.nil_type, false_pair&.type || AST::Builtin.nil_type)
add_typing(node, type: node_type, constr: constr)
end
when :case
yield_self do
# @type var node: Parser::AST::Node & Parser::AST::_CaseNode
cond, *whens, els = node.children
constr = self #: TypeConstruction
interpreter = TypeInference::LogicTypeInterpreter.new(subtyping: checker, typing: typing, config: builder_config)
if cond
types, envs = TypeInference::CaseWhen.type_check(constr, node, interpreter, hint: hint, condition: condition)
else
branch_results = [] #: Array[Pair]
condition_constr = constr
whens.each do |when_clause|
when_clause_constr = condition_constr
body_envs = [] #: Array[TypeInference::TypeEnv]
# @type var tests: Array[Parser::AST::Node]
# @type var body: Parser::AST::Node?
*tests, body = when_clause.children
branch_reachable = false
tests.each do |test|
test_type, condition_constr = condition_constr.synthesize(test, condition: true)
truthy, falsy = interpreter.eval(env: condition_constr.context.type_env, node: test)
truthy_env = truthy.env
falsy_env = falsy.env
condition_constr = condition_constr.update_type_env { falsy_env }
body_envs << truthy_env
branch_reachable ||= !truthy.unreachable
end
branch_result =
if body
when_clause_constr
.for_branch(body)
.update_type_env {|env| env.join(*body_envs) }
.tap {|constr| typing.cursor_context.set_node_context(body, constr.context) }
.synthesize(body, hint: hint)
else
Pair.new(type: AST::Builtin.nil_type, constr: when_clause_constr)
end
branch_results << branch_result
unless branch_reachable
unless branch_result.type.is_a?(AST::Types::Bot)
typing.add_error(
Diagnostic::Ruby::UnreachableValueBranch.new(
node: when_clause,
type: branch_result.type,
location: when_clause.location.keyword || raise
)
)
end
end
end
if els
branch_results << condition_constr.synthesize(els, hint: hint)
end
types = branch_results.map(&:type)
envs = branch_results.map {|result| result.constr.context.type_env }
unless els
types << AST::Builtin.nil_type
end
end
constr = constr.update_type_env do |env|
env.join(*envs)
end
add_typing(node, type: union_type_unify(*types), constr: constr)
end
when :rescue
yield_self do
body, *resbodies, else_node = node.children
body_pair = synthesize(body, hint: hint) if body
# @type var body_constr: TypeConstruction
body_constr = if body_pair
update_type_env do |env|
env.join(env, body_pair.context.type_env)
end
else
self
end
resbody_pairs = resbodies.map do |resbody|
# @type var exn_classes: Parser::AST::Node
# @type var assignment: Parser::AST::Node?
# @type var body: Parser::AST::Node?
exn_classes, assignment, body = resbody.children
if exn_classes
case exn_classes.type
when :array
exn_types = exn_classes.children.map {|child| synthesize(child).type }
else
Steep.logger.error "Unexpected exception list: #{exn_classes.type}"
end
end
if assignment
case assignment.type
when :lvasgn
var_name = assignment.children[0]
else
Steep.logger.error "Unexpected rescue variable assignment: #{assignment.type}"
end
end
resbody_construction = body_constr.for_branch(resbody).update_type_env do |env|
assignments = {} #: Hash[Symbol, AST::Types::t]
case
when exn_classes && var_name && exn_types
instance_types = exn_types.map do |type|
type = expand_alias(type)
case
when type.is_a?(AST::Types::Name::Singleton)
to_instance_type(type)
else
AST::Builtin.any_type
end
end
assignments[var_name] = AST::Types::Union.build(types: instance_types)
when var_name
assignments[var_name] = AST::Builtin.any_type
end
env.assign_local_variables(assignments)
end
if body
resbody_construction.typing.cursor_context.set_node_context(body, resbody_construction.context)
resbody_construction.synthesize(body, hint: hint)
else
Pair.new(constr: body_constr, type: AST::Builtin.nil_type)
end
end
resbody_pairs.select! do |pair|
no_subtyping?(sub_type: pair.type, super_type: AST::Types::Bot.instance)
end
resbody_types = resbody_pairs.map(&:type)
resbody_envs = resbody_pairs.map {|pair| pair.context.type_env }
else_constr = body_pair&.constr || self
if else_node
else_type, else_constr = else_constr.for_branch(else_node).synthesize(else_node, hint: hint)
else_constr
.update_type_env {|env| env.join(*resbody_envs, env) }
.add_typing(node, type: union_type(else_type, *resbody_types))
else
if resbody_types.empty?
constr = body_pair ? body_pair.constr : self
constr.add_typing(node, type: body_pair&.type || AST::Builtin.nil_type)
else
update_type_env {|env| env.join(*resbody_envs, else_constr.context.type_env) }
.add_typing(node, type: union_type(*[body_pair&.type, *resbody_types].compact))
end
end
end
when :resbody
yield_self do
klasses, asgn, body = node.children
synthesize(klasses) if klasses
synthesize(asgn) if asgn
if body
body_type = synthesize(body, hint: hint).type
add_typing(node, type: body_type)
else
add_typing(node, type: AST::Builtin.nil_type)
end
end
when :ensure
yield_self do
body, ensure_body = node.children
body_type = synthesize(body).type if body
synthesize(ensure_body) if ensure_body
if body_type
add_typing(node, type: body_type)
else
add_typing(node, type: AST::Builtin.nil_type)
end
end
when :masgn
type_masgn(node)
when :for
yield_self do
asgn, collection, body = node.children
collection_type, constr, collection_context = synthesize(collection).to_ary
collection_type = expand_self(collection_type)
if collection_type.is_a?(AST::Types::Any)
var_type = AST::Builtin.any_type
else
if each = calculate_interface(collection_type, :each, private: true)
method_type = (each.method_types || []).find do |type|
if type.block
if type.block.type.params
type.block.type.params.first_param
else
true
end
end
end
if method_type
if block = method_type.block
if first_param = block.type&.params&.first_param
var_type = first_param.type #: AST::Types::t
else
var_type = AST::Builtin.any_type
end
end
end
end
end
var_name = asgn.children[0] #: Symbol
if var_type
if body
body_constr = constr.update_type_env do |type_env|
type_env = type_env.assign_local_variables({ var_name => var_type })
pins = type_env.pin_local_variables(nil)
type_env.merge(local_variable_types: pins)
end
typing.cursor_context.set_body_context(node, body_constr.context)
_, _, body_context = body_constr.synthesize(body).to_ary
constr = constr.update_type_env do |env|
env.join(collection_context.type_env, body_context.type_env)
end
else
constr = self
end
add_typing(node, type: collection_type, constr: constr)
else
constr = synthesize_children(node, skips: [collection])
constr.fallback_to_any(node) do
Diagnostic::Ruby::NoMethod.new(
node: node,
method: :each,
type: collection_type
)
end
end
end
when :while, :until
yield_self do
cond, body = node.children
cond_type, constr = synthesize(cond, condition: true).to_ary
interpreter = TypeInference::LogicTypeInterpreter.new(subtyping: checker, typing: typing, config: builder_config)
truthy, falsy = interpreter.eval(env: constr.context.type_env, node: cond)
truthy_env = truthy.env
falsy_env = falsy.env
case node.type
when :while
body_env, exit_env = truthy_env, falsy_env
when :until
exit_env, body_env = truthy_env, falsy_env
else
raise
end
body_env or raise
exit_env or raise
if body
pins = body_env.pin_local_variables(nil)
body_env = body_env.merge(local_variable_types: pins)
_, body_constr =
constr
.update_type_env { body_env }
.for_branch(body, break_context: TypeInference::Context::BreakContext.new(break_type: hint || AST::Builtin.nil_type, next_type: nil))
.tap {|constr| typing.cursor_context.set_node_context(body, constr.context) }
.synthesize(body).to_ary
constr = constr.update_type_env {|env| env.join(exit_env, body_constr.context.type_env) }
else
constr = constr.update_type_env { exit_env }
end
add_typing(node, type: AST::Builtin.nil_type, constr: constr)
end
when :while_post, :until_post
yield_self do
cond, body = node.children
_, cond_constr, = synthesize(cond)
if body
for_loop =
cond_constr
.update_type_env {|env| env.merge(local_variable_types: env.pin_local_variables(nil)) }
.for_branch(body, break_context: TypeInference::Context::BreakContext.new(break_type: hint || AST::Builtin.nil_type, next_type: nil))
typing.cursor_context.set_node_context(body, for_loop.context)
_, body_constr, body_context = for_loop.synthesize(body)
constr = cond_constr.update_type_env {|env| env.join(env, body_context.type_env) }
add_typing(node, type: AST::Builtin.nil_type, constr: constr)
else
add_typing(node, type: AST::Builtin.nil_type, constr: cond_constr)
end
end
when :irange, :erange
begin_node, end_node = node.children
constr = self
begin_type, constr = if begin_node
constr.synthesize(begin_node).to_ary
else
[AST::Builtin.nil_type, constr]
end
end_type, constr = if end_node
constr.synthesize(end_node).to_ary
else
[AST::Builtin.nil_type, constr]
end
type = AST::Builtin::Range.instance_type(union_type(begin_type, end_type))
add_typing(node, type: type, constr: constr)
when :regexp
each_child_node(node) do |child|
synthesize(child)
end
add_typing(node, type: AST::Builtin::Regexp.instance_type)
when :regopt
# ignore
add_typing(node, type: AST::Builtin.any_type)
when :nth_ref
add_typing(node, type: union_type(AST::Builtin::String.instance_type, AST::Builtin.nil_type))
when :back_ref
synthesize(node.updated(:gvar), hint: hint)
when :or_asgn, :and_asgn
yield_self do
asgn, rhs = node.children
case asgn.type
when :lvasgn
type, constr = synthesize(rhs, hint: hint)
constr.lvasgn(asgn, type)
when :ivasgn
type, constr = synthesize(rhs, hint: hint)
constr.ivasgn(asgn, type)
when :gvasgn
type, constr = synthesize(rhs, hint: hint)
constr.gvasgn(asgn, type)
when :send
children = asgn.children.dup
children[1] = :"#{children[1]}="
send_arg_nodes = [*children, rhs]
rhs_ = node.updated(:send, send_arg_nodes)
node_type = case node.type
when :or_asgn
:or
when :and_asgn
:and
end
node_ = node.updated(node_type, [asgn, rhs_])
synthesize(node_, hint: hint)
else
Steep.logger.error { "#{node.type} with #{asgn.type} lhs is not supported"}
fallback_to_any(node)
end
end
when :defined?
type_any_rec(node, only_children: true)
add_typing(node, type: AST::Builtin.optional(AST::Builtin::String.instance_type))
when :gvasgn
yield_self do
name, rhs = node.children
lhs_type = context.type_env[name]
rhs_type, constr = synthesize(rhs, hint: lhs_type).to_ary
type, constr = constr.gvasgn(node, rhs_type)
constr.add_typing(node, type: type)
end
when :gvar
yield_self do
name = node.children.first
if type = context.type_env[name]
add_typing(node, type: type)
else
fallback_to_any(node) do
Diagnostic::Ruby::UnknownGlobalVariable.new(node: node, name: name)
end
end
end
when :block_pass
yield_self do
value_node = node.children[0]
constr = self #: TypeConstruction
if value_node
type, constr = synthesize(value_node, hint: hint)
if hint.is_a?(AST::Types::Proc) && value_node.type == :sym
if hint.one_arg?
if hint.type.params
# Assumes Symbol#to_proc implementation
param_type = hint.type.params.required[0]
case param_type
when AST::Types::Any
type = AST::Types::Any.instance
else
if method = calculate_interface(param_type, private: true)&.methods&.[](value_node.children[0])
return_types = method.method_types.filter_map do |method_type|
if method_type.type.params.nil? || method_type.type.params.optional?
method_type.type.return_type
end
end
unless return_types.empty?
type = AST::Types::Proc.new(
type: Interface::Function.new(
params: Interface::Function::Params.empty.with_first_param(
Interface::Function::Params::PositionalParams::Required.new(param_type)
),
return_type: return_types[0],
location: nil
),
block: nil,
self_type: nil
)
end
end
end
end
else
Steep.logger.error "Passing multiple args through Symbol#to_proc is not supported yet"
end
end
case
when type.is_a?(AST::Types::Proc)
# nop
when AST::Builtin::Proc.instance_type?(type)
# nop
else
type = try_convert(type, :to_proc) || type
end
else
# Anonymous block_pass only happens inside method definition
if block_type = method_context!.block_type
type = AST::Types::Proc.new(
type: block_type.type,
block: nil,
self_type: block_type.self_type
)
if block_type.optional?
type = union_type(type, AST::Builtin.nil_type)
end
else
type = AST::Builtin.nil_type
end
end
add_typing node, type: type
end
when :blockarg
yield_self do
each_child_node node do |child|
synthesize(child)
end
add_typing node, type: AST::Builtin.any_type
end
when :cvasgn
name, rhs = node.children
type, constr = synthesize(rhs, hint: hint)
var_type =
if class_vars = module_context.class_variables
if ty = class_vars[name]
checker.factory.type(ty)
end
end
if var_type
result = constr.check_relation(sub_type: type, super_type: var_type)
if result.success?
add_typing node, type: type, constr: constr
else
fallback_to_any node do
Diagnostic::Ruby::IncompatibleAssignment.new(
node: node,
lhs_type: var_type,
rhs_type: type,
result: result
)
end
end
else
fallback_to_any(node)
end
when :cvar
name = node.children[0] #: Symbol
var_type =
if cvs = module_context.class_variables
if ty = cvs[name]
checker.factory.type(ty)
end
end
if var_type
add_typing node, type: var_type
else
fallback_to_any node
end
when :alias
add_typing node, type: AST::Builtin.nil_type
when :splat
yield_self do
typing.add_error(
Diagnostic::Ruby::UnsupportedSyntax.new(
node: node,
message: "Unsupported splat node occurrence"
)
)
each_child_node node do |child|
synthesize(child)
end
add_typing node, type: AST::Builtin.any_type
end
when :args
constr = self #: TypeConstruction
each_child_node(node) do |child|
_, constr = constr.synthesize(child)
end
add_typing node, type: AST::Builtin.any_type, constr: constr
when :assertion
yield_self do
# @type var as_type: AST::Node::TypeAssertion
asserted_node, as_type = node.children
type = as_type.type(module_context.nesting, checker, [])
case type
when Array
type.each do |error|
typing.add_error(
Diagnostic::Ruby::RBSError.new(
error: error,
node: node,
location: error.location || raise
)
)
end
synthesize(asserted_node, hint: hint)
when nil, RBS::ParsingError
synthesize(asserted_node, hint: hint)
else
actual_type, constr = synthesize(asserted_node, hint: type)
if no_subtyping?(sub_type: type, super_type: actual_type) && no_subtyping?(sub_type: actual_type, super_type: type)
typing.add_error(
Diagnostic::Ruby::FalseAssertion.new(
node: node,
assertion_type: type,
node_type: actual_type
)
)
end
constr.add_typing(node, type: type)
end
end
when :tapp
yield_self do
# @type var tapp: AST::Node::TypeApplication
sendish, tapp = node.children
if (array = tapp.types(module_context.nesting, checker, [])).is_a?(Enumerator)
array.each do |error|
typing.add_error(
Diagnostic::Ruby::RBSError.new(
error: error,
node: node,
location: error.location || raise
)
)
end
end
type, constr = synthesize_sendish(sendish, hint: hint, tapp: tapp)
constr.add_typing(node, type: type)
end
when :block, :numblock, :send, :csend
synthesize_sendish(node, hint: hint, tapp: nil)
when :forwarded_args, :forward_arg
add_typing(node, type: AST::Builtin.any_type)
else
typing.add_error(Diagnostic::Ruby::UnsupportedSyntax.new(node: node))
add_typing(node, type: AST::Builtin.any_type)
end.tap do |pair|
unless pair.is_a?(Pair) && !pair.type.is_a?(Pair)
# Steep.logger.error { "result = #{pair.inspect}" }
# Steep.logger.error { "node = #{node.type}" }
raise "#synthesize should return an instance of Pair: #{pair.class}, node=#{node.inspect}"
end
end
rescue RBS::BaseError => exn
Steep.logger.warn("hello")
Steep.logger.warn { "Unexpected RBS error: #{exn.message}" }
exn.backtrace&.each {|loc| Steep.logger.warn " #{loc}" }
typing.add_error(Diagnostic::Ruby::UnexpectedError.new(node: node, error: exn))
type_any_rec(node)
rescue StandardError => exn
Steep.log_error exn
typing.add_error(Diagnostic::Ruby::UnexpectedError.new(node: node, error: exn))
type_any_rec(node)
end
end