diff --git a/Gemfile b/Gemfile index 19a8e2eeb..78c20f199 100644 --- a/Gemfile +++ b/Gemfile @@ -11,3 +11,8 @@ group :stackprof, optional: true do end gem 'minitest-slow_test' gem "rbs", "~> 2.5.1" + +group :ide, optional: true do + gem "ruby-debug-ide" + gem "debase", ">= 0.2.5.beta2" +end diff --git a/Gemfile.lock b/Gemfile.lock index 58b000985..5f4582d25 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -21,6 +21,9 @@ GEM tzinfo (~> 2.0) ast (2.4.2) concurrent-ruby (1.1.10) + debase (0.2.5.beta2) + debase-ruby_core_source (>= 0.10.12) + debase-ruby_core_source (0.10.16) ffi (1.15.5) i18n (1.10.0) concurrent-ruby (~> 1.0) @@ -42,6 +45,8 @@ GEM rb-inotify (0.10.1) ffi (~> 1.0) rbs (2.5.1) + ruby-debug-ide (0.7.3) + rake (>= 0.8.1) stackprof (0.2.19) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) @@ -53,11 +58,13 @@ PLATFORMS ruby DEPENDENCIES + debase (>= 0.2.5.beta2) minitest (~> 5.16) minitest-hooks minitest-slow_test rake rbs (~> 2.5.1) + ruby-debug-ide stackprof steep! diff --git a/Rakefile b/Rakefile index 79ae275ef..e35ab788e 100644 --- a/Rakefile +++ b/Rakefile @@ -1,6 +1,11 @@ require "bundler/gem_tasks" require "rake/testtask" +if ENV["VSCODE_CWD"] + require "minitest" + Minitest.seed = Time.now.to_i +end + Rake::TestTask.new(:test) do |t| t.libs << "test" t.libs << "lib" diff --git a/lib/steep.rb b/lib/steep.rb index c30a41cda..5d8da8585 100644 --- a/lib/steep.rb +++ b/lib/steep.rb @@ -88,6 +88,7 @@ require "steep/type_inference/type_env" require "steep/type_inference/type_env_builder" require "steep/type_inference/logic_type_interpreter" +require "steep/type_inference/multiple_assignment" require "steep/type_inference/method_call" require "steep/ast/types" diff --git a/lib/steep/diagnostic/ruby.rb b/lib/steep/diagnostic/ruby.rb index 09a95fcbe..2a23bf64a 100644 --- a/lib/steep/diagnostic/ruby.rb +++ b/lib/steep/diagnostic/ruby.rb @@ -717,6 +717,22 @@ def header_line end end + class MultipleAssignmentConversionError < Base + attr_reader :original_type, :returned_type + + def initialize(node:, original_type:, returned_type:) + super(node: node) + + @node = node + @original_type = original_type + @returned_type = returned_type + end + + def header_line + "Cannot convert `#{original_type}` to Array or tuple (`#to_ary` returns `#{returned_type}`)" + end + end + class UnsupportedSyntax < Base attr_reader :message diff --git a/lib/steep/type_construction.rb b/lib/steep/type_construction.rb index a36892210..a0fe4591b 100644 --- a/lib/steep/type_construction.rb +++ b/lib/steep/type_construction.rb @@ -1383,7 +1383,8 @@ def synthesize(node, hint: nil, condition: false) when :true, :false ty = node.type == :true ? AST::Types::Literal.new(value: true) : AST::Types::Literal.new(value: false) - if hint && check_relation(sub_type: ty, super_type: hint).success? + if 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) else add_typing(node, type: AST::Types::Boolean.new) @@ -2481,185 +2482,96 @@ def ivasgn(node, rhs_type) add_typing(node, type: rhs_type) end - def type_masgn(node) - lhs, rhs = node.children - rhs_pair = synthesize(rhs) - rhs_type = deep_expand_alias(rhs_pair.type) - - constr = rhs_pair.constr - - unless masgn_lhs?(lhs) - Steep.logger.error("Unsupported masgn lhs node: only lvasgn, ivasgn, and splat are supported") - _, constr = constr.fallback_to_any(lhs) - return add_typing(node, type: rhs_type, constr: constr) - end - - falseys, truthys = partition_flatten_types(rhs_type) do |type| - type.is_a?(AST::Types::Nil) || (type.is_a?(AST::Types::Literal) && type.value == false) - end - - unwrap_rhs_type = AST::Types::Union.build(types: truthys) - - case - when unwrap_rhs_type.is_a?(AST::Types::Tuple) || (rhs.type == :array && rhs.children.none? {|n| n.type == :splat }) - tuple_types = if unwrap_rhs_type.is_a?(AST::Types::Tuple) - unwrap_rhs_type.types.dup - else - rhs.children.map do |node| - typing.type_of(node: node) - end - end - - assignment_nodes = lhs.children.dup - leading_assignments = [] - trailing_assignments = [] - - until assignment_nodes.empty? - cursor = assignment_nodes.first - - if cursor.type == :splat - break - else - leading_assignments << assignment_nodes.shift - end - end - - until assignment_nodes.empty? - cursor = assignment_nodes.last - - if cursor.type == :splat - break - else - trailing_assignments.unshift assignment_nodes.pop - end - end + def type_masgn_type(mlhs_node, rhs_type, masgn:, optional:) + # @type var constr: TypeConstruction + constr = self - leading_assignments.each do |asgn| - type = tuple_types.first + if assignments = masgn.expand(mlhs_node, rhs_type || AST::Builtin.any_type, optional) + assignments.each do |pair| + node, type = pair - if type - tuple_types.shift - else - type = AST::Builtin.nil_type - end - - case asgn.type - when :lvasgn - _, constr = constr.lvasgn(asgn, type) - when :ivasgn - _, constr = constr.ivasgn(asgn, type) + if assignments.optional + type = AST::Builtin.optional(type) end - end - - trailing_assignments.reverse_each do |asgn| - type = tuple_types.last - if type - tuple_types.pop + if node.type == :splat + asgn_node = node.children[0] + next unless asgn_node + var_type = asgn_node.type else - type = AST::Builtin.nil_type + asgn_node = node + var_type = type end - case asgn.type + case asgn_node.type when :lvasgn - _, constr = constr.lvasgn(asgn, type) + _, constr = constr.lvasgn(asgn_node, type) when :ivasgn - _, constr = constr.ivasgn(asgn, type) + _, constr = constr.ivasgn(asgn_node, type) + when :gvasgn + raise + when :mlhs + constr = (constr.type_masgn_type(asgn_node, type, masgn: masgn, optional: optional) or return) end - end - element_type = if tuple_types.empty? - AST::Builtin.nil_type - else - AST::Types::Union.build(types: tuple_types) - end - array_type = AST::Builtin::Array.instance_type(element_type) - - assignment_nodes.each do |asgn| - case asgn.type - when :splat - case asgn.children[0]&.type - when :lvasgn - _, constr = constr.lvasgn(asgn.children[0], array_type) - when :ivasgn - _, constr = constr.ivasgn(asgn.children[0], array_type) - end - when :lvasgn - _, constr = constr.lvasgn(asgn, element_type) - when :ivasgn - _,constr = constr.ivasgn(asgn, element_type) + if node.type == :splat + _, constr = constr.add_typing(node, type: type) end end - unless falseys.empty? - constr = constr.update_type_env {|type_env| self.context.type_env.join(type_env, self.context.type_env)} - end + constr + end + end - add_typing(node, type: rhs_type, constr: constr) + def type_masgn(node) + lhs, rhs = node.children - when flatten_union(unwrap_rhs_type).all? {|type| AST::Builtin::Array.instance_type?(type) } - array_elements = flatten_union(unwrap_rhs_type).map {|type| type.args[0] } - element_type = AST::Types::Union.build(types: array_elements + [AST::Builtin.nil_type]) + masgn = TypeInference::MultipleAssignment.new() + hint = masgn.hint_for_mlhs(lhs, context.type_env) - constr = lhs.children.inject(constr) do |constr, assignment| - case assignment.type - when :lvasgn - _, constr = constr.lvasgn(assignment, element_type) + rhs_type, lhs_constr = try_tuple_type!(rhs, hint: hint).to_ary + rhs_type = deep_expand_alias(rhs_type) - when :ivasgn - _, constr = constr.ivasgn(assignment, element_type) - when :splat - case assignment.children[0]&.type - when :lvasgn - _, constr = constr.lvasgn(assignment.children[0], unwrap_rhs_type) - when :ivasgn - _, constr = constr.ivasgn(assignment.children[0], unwrap_rhs_type) - when nil - # foo, * = bar - else - raise - end - end + falsys, truthys = partition_flatten_types(rhs_type) do |type| + type.is_a?(AST::Types::Nil) || (type.is_a?(AST::Types::Literal) && type.value == false) + end - constr - end + truthy_rhs_type = AST::Types::Union.build(types: truthys) + optional = !falsys.empty? - unless falseys.empty? - constr = constr.update_lvar_env {|lvar_env| self.context.lvar_env.join(lvar_env, self.context.lvar_env)} - end - - add_typing(node, type: rhs_type, constr: constr) + if truthy_rhs_type.is_a?(AST::Types::Tuple) || AST::Builtin::Array.instance_type?(truthy_rhs_type) || truthy_rhs_type.is_a?(AST::Types::Any) + constr = lhs_constr.type_masgn_type(lhs, truthy_rhs_type, masgn: masgn, optional: optional) else - unless rhs_type.is_a?(AST::Types::Any) - Steep.logger.error("Unsupported masgn rhs type: array or tuple is supported (#{rhs_type})") - end + ary_type = try_convert(truthy_rhs_type, :to_ary) || try_convert(truthy_rhs_type, :to_a) || AST::Types::Tuple.new(types: [truthy_rhs_type]) + constr = lhs_constr.type_masgn_type(lhs, ary_type, masgn: masgn, optional: optional) + end + + unless constr + typing.add_error( + Diagnostic::Ruby::MultipleAssignmentConversionError.new( + node: rhs, + original_type: rhs_type, + returned_type: ary_type || AST::Builtin.bottom_type + ) + ) - untyped = AST::Builtin.any_type + constr = lhs_constr - constr = lhs.children.inject(constr) do |constr, assignment| - case assignment.type + each_descendant_node(lhs) do |node| + case node.type when :lvasgn - _, constr = constr.lvasgn(assignment, untyped) + _, constr = constr.lvasgn(node, AST::Builtin.any_type).to_ary when :ivasgn - _, constr = constr.ivasgn(assignment, untyped) - when :splat - case assignment.children[0]&.type - when :lvasgn - _, constr = constr.lvasgn(assignment.children[0], untyped) - when :ivasgn - _, constr = constr.ivasgn(assignment.children[0], untyped) - when nil - # foo, * = bar - else - raise - end + _, constr = constr.ivasgn(node, AST::Builtin.any_type).to_ary + when :gvasgn + raise + else + _, constr = constr.add_typing(node, type: AST::Builtin.any_type).to_ary end - - constr end - - add_typing(node, type: rhs_type, constr: constr) end + + constr.add_typing(node, type: truthy_rhs_type) end def synthesize_constant(node, parent_node, constant_name) @@ -4023,12 +3935,14 @@ def to_instance_type(type, args: nil) end def try_tuple_type!(node, hint: nil) - if node.type == :array && (hint.nil? || hint.is_a?(AST::Types::Tuple)) - node_range = node.loc.expression.yield_self {|l| l.begin_pos..l.end_pos } + if node.type == :array + if hint.nil? || hint.is_a?(AST::Types::Tuple) + node_range = node.loc.expression.yield_self {|l| l.begin_pos..l.end_pos } - typing.new_child(node_range) do |child_typing| - if pair = with_new_typing(child_typing).try_tuple_type(node, hint) - return pair.with(constr: pair.constr.save_typing) + typing.new_child(node_range) do |child_typing| + if pair = with_new_typing(child_typing).try_tuple_type(node, hint) + return pair.with(constr: pair.constr.save_typing) + end end end end @@ -4063,13 +3977,13 @@ def try_convert(type, method) return end - interface = checker.factory.interface(type, private: false) + interface = checker.factory.interface(type, private: false, self_type: self_type) if entry = interface.methods[method] method_type = entry.method_types.find do |method_type| method_type.type.params.optional? end - method_type.type.return_type + method_type.type.return_type if method_type end rescue => exn Steep.log_error(exn, message: "Unexpected error when converting #{type.to_s} with #{method}") diff --git a/lib/steep/type_inference/logic_type_interpreter.rb b/lib/steep/type_inference/logic_type_interpreter.rb index 57d9e7c3d..3f24b1bc3 100644 --- a/lib/steep/type_inference/logic_type_interpreter.rb +++ b/lib/steep/type_inference/logic_type_interpreter.rb @@ -60,12 +60,20 @@ def evaluate_node(env:, node:, refined_objects:) when :lvasgn name, rhs = node.children truthy_type, falsy_type, truthy_env, falsy_env = evaluate_node(env: env, node: rhs, refined_objects: refined_objects) - return [ truthy_type, falsy_type, - truthy_env.refine_types(local_variable_types: { name => truthy_type }), - falsy_env.refine_types(local_variable_types: { name => falsy_type }) + evaluate_assignment(node, truthy_env, truthy_type, refined_objects: refined_objects), + evaluate_assignment(node, falsy_env, falsy_type, refined_objects: refined_objects) + ] + when :masgn + lhs, rhs = node.children + truthy_type, falsy_type, truthy_env, falsy_env = evaluate_node(env: env, node: rhs, refined_objects: refined_objects) + return [ + truthy_type, + falsy_type, + evaluate_assignment(node, truthy_env, truthy_type, refined_objects: refined_objects), + evaluate_assignment(node, falsy_env, falsy_type, refined_objects: refined_objects) ] when :begin last_node = node.children.last or raise @@ -102,6 +110,37 @@ def evaluate_node(env:, node:, refined_objects:) return [truthy_type, falsy_type, env, env] end + def evaluate_assignment(assignment_node, env, rhs_type, refined_objects:) + case assignment_node.type + when :lvasgn + name, _ = assignment_node.children + refined_objects << name + env.refine_types(local_variable_types: { name => rhs_type }) + when :masgn + lhs, _ = assignment_node.children + + masgn = MultipleAssignment.new() + assignments = masgn.expand(lhs, rhs_type, false) + unless assignments + rhs_type_converted = try_convert(rhs_type, :to_ary) + rhs_type_converted ||= try_convert(rhs_type, :to_a) + rhs_type_converted ||= AST::Types::Tuple.new(types: [rhs_type]) + assignments = masgn.expand(lhs, rhs_type_converted, false) + end + + assignments or raise + + assignments.each do |pair| + node, type = pair + env = evaluate_assignment(node, env, type, refined_objects: refined_objects) + end + + env + else + env + end + end + def refine_node_type(env:, node:, truthy_type:, falsy_type:, refined_objects:) case node.type when :lvar @@ -331,6 +370,25 @@ def type_case_select0(type, klass) end end end + + def try_convert(type, method) + case type + when AST::Types::Any, AST::Types::Bot, AST::Types::Top, AST::Types::Var + return + end + + interface = factory.interface(type, private: false, self_type: type) + if entry = interface.methods[method] + method_type = entry.method_types.find do |method_type| + method_type.type.params.optional? + end + + method_type.type.return_type if method_type + end + rescue => exn + Steep.log_error(exn, message: "Unexpected error when converting #{type.to_s} with #{method}") + nil + end end end end diff --git a/lib/steep/type_inference/multiple_assignment.rb b/lib/steep/type_inference/multiple_assignment.rb new file mode 100644 index 000000000..9d85072cc --- /dev/null +++ b/lib/steep/type_inference/multiple_assignment.rb @@ -0,0 +1,189 @@ +module Steep + module TypeInference + class MultipleAssignment + Assignments = _ = Struct.new(:rhs_type, :optional, :leading_assignments, :trailing_assignments, :splat_assignment, keyword_init: true) do + # @implements Assignments + + def each(&block) + if block + leading_assignments.each(&block) + if sp = splat_assignment + yield sp + end + trailing_assignments.each(&block) + else + enum_for :each + end + end + end + + def expand(mlhs, rhs_type, optional) + lhss = mlhs.children + + case rhs_type + when AST::Types::Tuple + expand_tuple(lhss.dup, rhs_type, rhs_type.types.dup, optional) + when AST::Types::Name::Instance + if AST::Builtin::Array.instance_type?(rhs_type) + expand_array(lhss.dup, rhs_type, optional) + end + when AST::Types::Any + expand_any(lhss, rhs_type, AST::Builtin.any_type, optional) + end + end + + def expand_tuple(lhss, rhs_type, tuples, optional) + # @type var leading_assignments: Array[node_type_pair] + leading_assignments = [] + # @type var trailing_assignments: Array[node_type_pair] + trailing_assignments = [] + # @type var splat_assignment: node_type_pair? + splat_assignment = nil + + while !lhss.empty? + first = lhss.first or raise + + case + when first.type == :splat + break + else + leading_assignments << [first, tuples.first || AST::Builtin.nil_type] + lhss.shift + tuples.shift + end + end + + while !lhss.empty? + last = lhss.last or raise + + case + when last.type == :splat + break + else + trailing_assignments << [last, tuples.last || AST::Builtin.nil_type] + lhss.pop + tuples.pop + end + end + + case lhss.size + when 0 + # nop + when 1 + splat_assignment = [lhss.first || raise, AST::Types::Tuple.new(types: tuples)] + else + raise + end + + Assignments.new( + rhs_type: rhs_type, + optional: optional, + leading_assignments: leading_assignments, + trailing_assignments: trailing_assignments, + splat_assignment: splat_assignment + ) + end + + def expand_array(lhss, rhs_type, optional) + element_type = rhs_type.args[0] or raise + + # @type var leading_assignments: Array[node_type_pair] + leading_assignments = [] + # @type var trailing_assignments: Array[node_type_pair] + trailing_assignments = [] + # @type var splat_assignment: node_type_pair? + splat_assignment = nil + + while !lhss.empty? + first = lhss.first or raise + + case + when first.type == :splat + break + else + leading_assignments << [first, AST::Builtin.optional(element_type)] + lhss.shift + end + end + + while !lhss.empty? + last = lhss.last or raise + + case + when last.type == :splat + break + else + trailing_assignments << [last, AST::Builtin.optional(element_type)] + lhss.pop + end + end + + case lhss.size + when 0 + # nop + when 1 + splat_assignment = [ + lhss.first || raise, + AST::Builtin::Array.instance_type(element_type) + ] + else + raise + end + + Assignments.new( + rhs_type: rhs_type, + optional: optional, + leading_assignments: leading_assignments, + trailing_assignments: trailing_assignments, + splat_assignment: splat_assignment + ) + end + + def expand_any(nodes, rhs_type, element_type, optional) + # @type var leading_assignments: Array[node_type_pair] + leading_assignments = [] + # @type var trailing_assignments: Array[node_type_pair] + trailing_assignments = [] + # @type var splat_assignment: node_type_pair? + splat_assignment = nil + + array = leading_assignments + + nodes.each do |node| + case node.type + when :splat + splat_assignment = [node, AST::Builtin::Array.instance_type(element_type)] + array = trailing_assignments + else + array << [node, element_type] + end + end + + Assignments.new( + rhs_type: rhs_type, + optional: optional, + leading_assignments: leading_assignments, + trailing_assignments: trailing_assignments, + splat_assignment: splat_assignment + ) + end + + def hint_for_mlhs(mlhs, env) + case mlhs.type + when :mlhs + types = mlhs.children.map do |node| + hint_for_mlhs(node, env) or return + end + AST::Types::Tuple.new(types: types) + when :lvasgn, :ivasgn, :gvasgn + name = mlhs.children[0] + env[name] || AST::Builtin.any_type + when :splat + return + else + return + end + end + end + end +end diff --git a/sig/steep/diagnostic/ruby.rbs b/sig/steep/diagnostic/ruby.rbs index 065154eca..b55c3d143 100644 --- a/sig/steep/diagnostic/ruby.rbs +++ b/sig/steep/diagnostic/ruby.rbs @@ -437,6 +437,23 @@ module Steep def header_line: () -> ::String end + # The `#to_ary` of RHS of multiple assignment is called, but returns not tuple nor Array. + # + # ```ruby + # a, b = foo() + # ^^^^^ + # ``` + # + class MultipleAssignmentConversionError < Base + attr_reader original_type: AST::Types::t + + attr_reader returned_type: AST::Types::t + + def initialize: (node: Parser::AST::Node, original_type: AST::Types::t, returned_type: AST::Types::t) -> void + + def header_line: () -> ::String + end + class UnsupportedSyntax < Base attr_reader message: untyped diff --git a/sig/steep/type_construction.rbs b/sig/steep/type_construction.rbs index 8ee54d8d5..96f858b90 100644 --- a/sig/steep/type_construction.rbs +++ b/sig/steep/type_construction.rbs @@ -92,7 +92,7 @@ module Steep def for_branch: (Parser::AST::Node node, ?break_context: TypeInference::Context::BreakContext?) -> untyped - def add_typing: (untyped node, type: untyped, ?constr: untyped) -> untyped + def add_typing: (Parser::AST::Node node, type: AST::Types::t, ?constr: TypeConstruction) -> Pair def add_call: (untyped call) -> untyped @@ -102,11 +102,13 @@ module Steep def masgn_lhs?: (untyped lhs) -> untyped - def lvasgn: (untyped node, untyped `type`) -> untyped + def lvasgn: (Parser::AST::Node node, AST::Types::t) -> Pair - def ivasgn: (Parser::AST::Node node, untyped rhs_type) -> Pair + def ivasgn: (Parser::AST::Node node, AST::Types::t rhs_type) -> Pair - def type_masgn: (untyped node) -> untyped + def type_masgn: (Parser::AST::Node node) -> Pair + + def type_masgn_type: (Parser::AST::Node mlhs_node, AST::Types::t? rhs_type, masgn: TypeInference::MultipleAssignment, optional: bool) -> TypeConstruction? def constant_typename: (Parser::AST::Node parent, Symbol name) -> RBS::TypeName? @@ -204,7 +206,7 @@ module Steep def namespace_module?: (untyped node) -> (false | untyped) - def type_any_rec: (untyped node) -> untyped + def type_any_rec: (Parser::AST::Node node) -> Pair def unwrap: (untyped `type`) -> untyped @@ -226,11 +228,23 @@ module Steep def to_instance_type: (untyped `type`, ?args: untyped?) -> untyped - def try_tuple_type!: (untyped node, ?hint: untyped?) -> untyped + def try_tuple_type!: (Parser::AST::Node node, ?hint: AST::Types::t?) -> Pair - def try_tuple_type: (untyped node, untyped hint) -> (nil | untyped) + def try_tuple_type: (Parser::AST::Node node, AST::Types::Tuple? hint) -> (nil | untyped) - def try_convert: (untyped `type`, untyped method) -> untyped + # Try to convert a object of `type` with zero-arity method `method`. + # + # Returns `nil` when + # + # 1. The `type` cannot be converted to an interface, or + # 2. There is no that `conversion` method defined + # + # ```ruby + # try_convert(`::Object`, :to_s) # Returns `::String` + # try_convert(`::String`, :to_ary) # Returns nil + # ``` + # + def try_convert: (AST::Types::t `type`, Symbol method) -> AST::Types::t? def try_array_type: (untyped node, untyped hint) -> untyped diff --git a/sig/steep/type_inference/logic_type_interpreter.rbs b/sig/steep/type_inference/logic_type_interpreter.rbs index fc39bef36..cc81f5c43 100644 --- a/sig/steep/type_inference/logic_type_interpreter.rbs +++ b/sig/steep/type_inference/logic_type_interpreter.rbs @@ -43,6 +43,8 @@ module Steep private + def evaluate_assignment: (Parser::AST::Node node, TypeEnv env, AST::Types::t rhs_type, refined_objects: Set[Symbol | Parser::AST::Node]) -> TypeEnv + def guess_type_from_method: (Parser::AST::Node node) -> (AST::Types::Logic::ReceiverIsArg | AST::Types::Logic::ReceiverIsNil | AST::Types::Logic::Not | AST::Types::Logic::ArgIsReceiver | nil) # Decompose to given type to truthy and falsy types. @@ -63,6 +65,8 @@ module Steep def type_case_select: (AST::Types::t `type`, RBS::TypeName klass) -> [AST::Types::t, AST::Types::t] def type_case_select0: (AST::Types::t `type`, RBS::TypeName klass) -> [Array[AST::Types::t], Array[AST::Types::t]] + + def try_convert: (AST::Types::t, Symbol) -> AST::Types::t? end end end diff --git a/sig/steep/type_inference/multiple_assignment.rbs b/sig/steep/type_inference/multiple_assignment.rbs new file mode 100644 index 000000000..cfa89c67f --- /dev/null +++ b/sig/steep/type_inference/multiple_assignment.rbs @@ -0,0 +1,76 @@ +module Steep + module TypeInference + # This class provides an abstraction for multiple assignments. + # + class MultipleAssignment + type node_type_pair = [Parser::AST::Node, AST::Types::t] + + # Encapsulate assignments included in one `masgn` node + # + # ```ruby + # a, *b, c = rhs + # # ^ Leading assignments + # # ^^ Splat assignment + # # ^ Trailing assignments + # ``` + # + class Assignments + attr_reader rhs_type: AST::Types::t + + attr_reader optional: bool + + # Assignments before `*` assignment + attr_reader leading_assignments: Array[node_type_pair] + + # Assignments after `*` assignment + # + # Empty if there is no splat assignment. + # + attr_reader trailing_assignments: Array[node_type_pair] + + # Splat assignment if present + attr_reader splat_assignment: node_type_pair? + + def initialize: ( + rhs_type: AST::Types::t, + optional: bool, + leading_assignments: Array[node_type_pair], + trailing_assignments: Array[node_type_pair], + splat_assignment: node_type_pair? + ) -> void + + def each: () { (node_type_pair) -> void } -> void + | () -> Enumerator[node_type_pair, void] + end + + def initialize: () -> void + + # Receives multiple assignment left hand side, right hand side type, and `optional` flag, and returns Assignments object + # + # This implements a case analysis on `rhs_type`: + # + # 1. If `rhs_type` is tuple, it returns an Assignments object with corresponding assignments + # 2. If `rhs_type` is an array, it returns an Assignments object with corresponding assignments + # 3. If `rhs_type` is `untyped`, it returns an Assignments with `untyped` type + # 4. It returns `nil` otherwise + # + def expand: (Parser::AST::Node mlhs, AST::Types::t rhs_type, bool optional) -> Assignments? + + # Returns a type hint for multiple assignment right hand side + # + # It constructs a structure of tuple types, based on the assignment lhs, and variable types. + # + def hint_for_mlhs: (Parser::AST::Node mlhs, TypeEnv env) -> AST::Types::t? + + private + + def expand_tuple: (Array[Parser::AST::Node] assignments, AST::Types::t rhs_type, Array[AST::Types::t] types, bool optional) -> Assignments + + def expand_array: (Array[Parser::AST::Node] assignments, AST::Types::Name::Instance rhs_type, bool optional) -> Assignments + + def expand_any: (Array[Parser::AST::Node] assignments, AST::Types::t rhs_type, AST::Types::t element_type, bool optional) -> Assignments + + def expand_else: (Array[Parser::AST::Node] assignments, AST::Types::t rhs_type, bool optional) -> Assignments + end + end +end diff --git a/test/logic_type_interpreter_test.rb b/test/logic_type_interpreter_test.rb index 97b8f2dde..504ca2c31 100644 --- a/test/logic_type_interpreter_test.rb +++ b/test/logic_type_interpreter_test.rb @@ -39,27 +39,31 @@ def test_lvar_assignment end end - # def test_masgn - # with_checker do |checker| - # source = parse_ruby("a, b = @x") - - # pp source.node + def test_masgn + with_checker do |checker| + source = parse_ruby("a, b = @x") - # typing = Typing.new(source: source, root_context: nil) - # typing.add_typing(dig(source.node), parse_type("::String?"), nil) - # typing.add_typing(dig(source.node, 1), parse_type("::String?"), nil) + typing = Typing.new(source: source, root_context: nil) + typing.add_typing(dig(source.node), parse_type("[::String, ::Integer]?"), nil) + typing.add_typing(dig(source.node, 1), parse_type("[::String, ::Integer]?"), nil) - # env = type_env.assign_local_variable(:a, parse_type("::String?"), nil) + env = + type_env + .assign_local_variable(:a, parse_type("::String?"), nil) + .assign_local_variable(:b, parse_type("::Integer?"), nil) + .merge(instance_variable_types: { :@x => parse_type("[::String, ::Integer]?") }) - # interpreter = LogicTypeInterpreter.new(subtyping: checker, typing: typing) - # truthy_env, falsy_env, truthy_type, falsy_type = interpreter.eval(env: env, node: source.node) + interpreter = LogicTypeInterpreter.new(subtyping: checker, typing: typing) + truthy_env, falsy_env, symbols, truthy_type, falsy_type = interpreter.eval(env: env, node: source.node) - # assert_equal parse_type("::String"), truthy_type - # assert_equal parse_type("nil"), falsy_type - # assert_equal parse_type("::String"), truthy_env[:a] - # assert_equal parse_type("nil"), falsy_env[:a] - # end - # end + assert_equal parse_type("[::String, ::Integer]"), truthy_type + assert_equal parse_type("nil"), falsy_type + assert_equal parse_type("::String"), truthy_env[:a] + assert_equal parse_type("::Integer"), truthy_env[:b] + assert_equal parse_type("nil"), falsy_env[:a] + assert_equal parse_type("nil"), falsy_env[:b] + end + end def test_pure_call with_checker(<<-RBS) do |checker| diff --git a/test/multiple_assignment_test.rb b/test/multiple_assignment_test.rb new file mode 100644 index 000000000..4f1dfafb5 --- /dev/null +++ b/test/multiple_assignment_test.rb @@ -0,0 +1,147 @@ +require_relative "test_helper" + +class MultipleAssignmentTest < Minitest::Test + include TestHelper + include FactoryHelper + include SubtypingHelper + + include Steep + + MultipleAssignment = TypeInference::MultipleAssignment + TypeEnv = TypeInference::TypeEnv + ConstantEnv = TypeInference::ConstantEnv + + def node(type, *children) + Parser::AST::Node.new(type, children) + end + + def constant_env(context: nil) + ConstantEnv.new( + factory: factory, + context: context, + resolver: RBS::Resolver::ConstantResolver.new(builder: factory.definition_builder) + ) + end + + def test_tuple_assignment + with_checker do + source = parse_ruby("a, *b, c = _") + mlhs, rhs = source.node.children + + masgn = MultipleAssignment.new() + asgns = masgn.expand(mlhs, parse_type("[::Integer, ::String, ::Symbol]"), false) + + assert_equal( + MultipleAssignment::Assignments.new( + rhs_type: parse_type("[::Integer, ::String, ::Symbol]"), + optional: false, + leading_assignments: [[node(:lvasgn, :a), parse_type("::Integer")]], + trailing_assignments: [[node(:lvasgn, :c), parse_type("::Symbol")]], + splat_assignment: [node(:splat, node(:lvasgn, :b)), parse_type("[::String]")] + ), + asgns + ) + end + end + + def test_tuple_assignment_optional + with_checker do + source = parse_ruby("a, *b, c = _") + mlhs, rhs = source.node.children + + masgn = MultipleAssignment.new() + asgns = masgn.expand(mlhs, parse_type("[::Integer, ::String, ::Symbol]"), true) + + assert_equal( + MultipleAssignment::Assignments.new( + rhs_type: parse_type("[::Integer, ::String, ::Symbol]"), + optional: true, + leading_assignments: [[node(:lvasgn, :a), parse_type("::Integer")]], + trailing_assignments: [[node(:lvasgn, :c), parse_type("::Symbol")]], + splat_assignment: [node(:splat, node(:lvasgn, :b)), parse_type("[::String]")] + ), + asgns + ) + end + end + + def test_array_assignment + with_checker do + source = parse_ruby("a, *b, c = _") + mlhs, rhs = source.node.children + + masgn = MultipleAssignment.new() + asgns = masgn.expand(mlhs, parse_type("::Array[::Integer]"), false) + + assert_equal( + MultipleAssignment::Assignments.new( + rhs_type: parse_type("::Array[::Integer]"), + optional: false, + leading_assignments: [[node(:lvasgn, :a), parse_type("::Integer?")]], + trailing_assignments: [[node(:lvasgn, :c), parse_type("::Integer?")]], + splat_assignment: [node(:splat, node(:lvasgn, :b)), parse_type("::Array[::Integer]")] + ), + asgns + ) + end + end + + def test_array_assignment_optional + with_checker do + source = parse_ruby("a, *b, c = _") + mlhs, rhs = source.node.children + + masgn = MultipleAssignment.new() + asgns = masgn.expand(mlhs, parse_type("::Array[::Integer]"), true) + + assert_equal( + MultipleAssignment::Assignments.new( + rhs_type: parse_type("::Array[::Integer]"), + optional: true, + leading_assignments: [[node(:lvasgn, :a), parse_type("::Integer?")]], + trailing_assignments: [[node(:lvasgn, :c), parse_type("::Integer?")]], + splat_assignment: [node(:splat, node(:lvasgn, :b)), parse_type("::Array[::Integer]")] + ), + asgns + ) + end + end + + def test_hint_for_mlhs + with_checker do + env = + + masgn = MultipleAssignment.new() + + masgn.hint_for_mlhs( + parse_ruby("a, b = _").node.children[0], + TypeEnv.new(constant_env) + ).tap do |hint| + assert_equal parse_type("[untyped, untyped]"), hint + end + + masgn.hint_for_mlhs( + parse_ruby("a, b, *c = _").node.children[0], + TypeEnv.new(constant_env) + ).tap do |hint| + assert_nil hint + end + + masgn.hint_for_mlhs( + parse_ruby("a, (b, c) = _").node.children[0], + TypeEnv.new(constant_env) + ).tap do |hint| + assert_equal parse_type("[untyped, [untyped, untyped]]"), hint + end + + masgn.hint_for_mlhs( + parse_ruby("a, @b, $c = _").node.children[0], + TypeEnv.new(constant_env) + .assign_local_variables({ a: parse_type("::String") }) + .update(instance_variable_types: { :"@b" => parse_type("::Symbol") }, global_types: { :"$c" => parse_type("::Integer") }) + ).tap do |hint| + assert_equal parse_type("[::String, ::Symbol, ::Integer]"), hint + end + end + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index ac78926bb..8111fd945 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -13,6 +13,10 @@ require 'minitest/slow_test' require 'rbconfig' +unless Minitest.seed + Minitest.seed = Time.now.to_i +end + require_relative "lsp_double" Minitest::SlowTest.long_test_time = 5 diff --git a/test/type_construction_test.rb b/test/type_construction_test.rb index a3ced6da7..d46d9ece2 100644 --- a/test/type_construction_test.rb +++ b/test/type_construction_test.rb @@ -1548,6 +1548,24 @@ def test_masgn_tuple end end + def test_masgn_nested_tuple + with_checker do |checker| + source = parse_ruby(<<-EOF) +a, (b, c) = 1, [true, "hello"] + EOF + + with_standard_construction(checker, source) do |construction, typing| + pair = construction.synthesize(source.node) + + assert_no_error typing + + assert_equal parse_type("::Integer"), pair.context.type_env[:a] + assert_equal parse_type("bool"), pair.context.type_env[:b] + assert_equal parse_type("::String"), pair.context.type_env[:c] + end + end + end + def test_masgn_tuple_array with_checker do |checker| source = parse_ruby(<<-EOF) @@ -1560,7 +1578,7 @@ def test_masgn_tuple_array assert_no_error typing assert_equal parse_type("::Integer"), context.type_env[:a] - assert_equal parse_type("::Array[::Integer | ::String]"), context.type_env[:b] + assert_equal parse_type("[::Integer, ::String]"), context.type_env[:b] assert_equal parse_type("::Symbol"), context.type_env[:c] end end @@ -1597,23 +1615,6 @@ def test_masgn_array end end - def test_masgn_union - with_checker do |checker| - source = parse_ruby(<<-RUBY) -# @type var x: Array[Integer] | Array[String] -x = (_ = nil) -a, b = x - RUBY - - - with_standard_construction(checker, source) do |construction, typing| - construction.synthesize(source.node) - - assert_no_error typing - end - end - end - def test_masgn_splat with_checker do |checker| source = parse_ruby(<<-RUBY) @@ -1678,14 +1679,82 @@ def test_masgn_optional end end - def test_masgn_optional_conditional - skip "masgn in conditional!!" + def test_masgn_to_ary + with_checker(<<-RBS) do |checker| +class WithToAry + def to_ary: () -> [Integer, String, bool] +end + RBS + source = parse_ruby(<<-EOF) +x = (a, b = WithToAry.new()) + EOF + + with_standard_construction(checker, source) do |construction, typing| + _, _, context = construction.synthesize(source.node) + assert_no_error typing + + assert_equal parse_type("::Integer"), context.type_env[:a] + assert_equal parse_type("::String"), context.type_env[:b] + assert_equal parse_type("::WithToAry"), context.type_env[:x] + end + end + end + + def test_masgn_to_ary_error + with_checker(<<-RBS) do |checker| +class WithToAry + def to_ary: () -> Integer +end + RBS + source = parse_ruby(<<-EOF) +x = (a, b = WithToAry.new()) + EOF + + with_standard_construction(checker, source) do |construction, typing| + _, _, context = construction.synthesize(source.node) + + assert_typing_error(typing, size: 1) do |errors| + errors[0].tap do |error| + assert_instance_of Diagnostic::Ruby::MultipleAssignmentConversionError, error + assert_equal parse_type("::WithToAry"), error.original_type + assert_equal parse_type("::Integer"), error.returned_type + assert_equal "WithToAry.new()", error.location.source + end + end + + assert_equal parse_type("untyped"), context.type_env[:a] + assert_equal parse_type("untyped"), context.type_env[:b] + assert_equal parse_type("::WithToAry"), context.type_env[:x] + end + end + end + + def test_masgn_no_conversion + with_checker(<<-RBS) do |checker| + RBS + source = parse_ruby(<<-EOF) +x = (a, b = 123) + EOF + + with_standard_construction(checker, source) do |construction, typing| + _, _, context = construction.synthesize(source.node) + + assert_no_error typing + + assert_equal parse_type("::Integer"), context.type_env[:a] + assert_equal parse_type("nil"), context.type_env[:b] + assert_equal parse_type("::Integer"), context.type_env[:x] + end + end + end + + def test_masgn_optional_conditional with_checker do |checker| source = parse_ruby(<<-RUBY) # @type var tuple: [Integer, String]? tuple = _ = nil -if (a, b = x = tuple) +if x = (a, b = tuple) a + 1 b + "a" else @@ -1787,6 +1856,8 @@ def test_masgn_array_error with_standard_construction(checker, source) do |construction, typing| construction.synthesize(source.node) + assert_typing_error(typing, size: 1) + assert_equal 1, typing.errors.size assert_any typing.errors do |error| error.is_a?(Diagnostic::Ruby::UnknownInstanceVariable)