Skip to content

Commit

Permalink
Merge pull request #605 from soutaro/masgn
Browse files Browse the repository at this point in the history
Better multiple assignment implementation
  • Loading branch information
soutaro authored Jul 19, 2022
2 parents 15c3ed5 + 6cb5222 commit ca1e690
Show file tree
Hide file tree
Showing 16 changed files with 740 additions and 208 deletions.
5 changes: 5 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
7 changes: 7 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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!

Expand Down
5 changes: 5 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
1 change: 1 addition & 0 deletions lib/steep.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
16 changes: 16 additions & 0 deletions lib/steep/diagnostic/ruby.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
232 changes: 73 additions & 159 deletions lib/steep/type_construction.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}")
Expand Down
Loading

0 comments on commit ca1e690

Please sign in to comment.