Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions core/array.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -576,7 +576,8 @@ class Array[unchecked out Elem] < Object
# true/false, or always return a number. It is undefined which value is
# actually picked up at each iteration.
#
def bsearch: () { (Elem) -> (true | false) } -> Elem?
def bsearch: () -> ::Enumerator[Elem, Elem?]
| () { (Elem) -> (true | false) } -> Elem?
| () { (Elem) -> ::Integer } -> Elem?

# By using binary search, finds an index of a value from this array which meets
Expand Down Expand Up @@ -627,8 +628,8 @@ class Array[unchecked out Elem] < Object
# a #=> ["", "b", "c!", "d!"]
#
# collect! is monomorphic because of RBS limitation.
def collect!: () { (Elem item) -> Elem } -> self
| () -> ::Enumerator[Elem, self]
def collect!: [U] () { (Elem item) -> U } -> ::Array[U]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is intentional, since RBS cannot change the type of the receiver by method calls.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, in such cases, is it really better to just return self?

I understand that it makes the signature describe the method behaviour better, however I think using generics here will help the signature to better describe the type behaviour. To me this is more what type signatures are for, as we can refer to the documentation or implementation if we are interested in learning the side-effects of the method.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The following is the case to detect a type error in my mind.

a = [1,2,3]
a.map! {|x| x.to_s }
a[0] + 1

On the other hand, the following runs without any error == Steep detects a false error.

a = [1,2,3]
b = a.map! {|x| x.to_s }
b[0] + "string"

I'm not sure if we can find a solution to make the both work, and we may have to choose one. My intuition is using #map! is usually done without assignment because it is in-place.

Any thoughts from @mame regarding to TypeProf?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, TypeProf performs weak-update of the element type of a container type when its method accepts a new type at a contravariance occurrence, i.e.,

ary = [1, 2, 3]
ary.map!(&:to_s)
p ary #=> Array[Integer | String]

https://mame.github.io/typeprof-playground/#rb=ary+%3D+%5B1%2C+2%2C+3%5D%0Aary.map%21%28%26%3Ato_s%29%0Ap+ary+%23%3D%3E+Array%5BInteger+%7C+String%5D&rbs=

Changing the RBS declaration as proposed will break this behavior of TypeProf.

That being said, I have no strong opinion about the change. I'm unsure how much is used a call to map! that changes the element type. If there are a lot of such cases, prohibiting them could be very conservative and annoying. @hjwylde do you have any statistics?
BTW, I guess the return type of map! is not important. IMO it could be even void.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm afraid I don't have any statistics. I too don't feel strongly about the return type of map!, so I will revert this one, and make the appropriate changes to the other points in this review - thank you.

TypeProf performs weak-update of the element type of a container type when its method accepts a new type

This is interesting, perhaps we can adopt something similar too. Initially I had disregarded the idea as I thought it would lead to issues with Array#include? etc., but it seems these types of methods take an untyped object and it would be okay.

| () -> ::Enumerator[Elem, ::Array[untyped]]

# When invoked with a block, yields all combinations of length `n` of elements
# from the array and then returns the array itself.
Expand Down Expand Up @@ -855,7 +856,7 @@ class Array[unchecked out Elem] < Object
# 0 -- 1 -- 2 --
#
def each_index: () { (::Integer index) -> void } -> self
| () -> ::Enumerator[Elem, self]
| () -> ::Enumerator[::Integer, self]

# Returns `true` if `self` contains no elements.
#
Expand Down Expand Up @@ -1585,7 +1586,7 @@ class Array[unchecked out Elem] < Object
# a.sample(4, random: Random.new(1)) #=> [6, 10, 9, 2]
#
def sample: (?random: _Rand rng) -> Elem?
| (?int n, ?random: _Rand rng) -> ::Array[Elem]
| (int n, ?random: _Rand rng) -> ::Array[Elem]

# Returns a new array containing all elements of `ary` for which the given
# `block` returns a true value.
Expand Down
21 changes: 11 additions & 10 deletions core/enumerable.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ module Enumerable[unchecked out Elem]: _Each[Elem]
def collect: [U] () { (Elem arg0) -> U } -> ::Array[U]
| () -> ::Enumerator[Elem, ::Array[untyped]]

def collect_concat: [U] () { (Elem arg0) -> ::Enumerator[U, untyped] } -> ::Array[U]
def collect_concat: [U] () { (Elem) -> (::Array[U] | U) } -> ::Array[U]
| () -> ::Enumerator[Elem, ::Array[untyped]]

# Returns the number of items in `enum` through enumeration. If an
# argument is given, the number of items in `enum` that are equal to
Expand Down Expand Up @@ -79,11 +80,11 @@ module Enumerable[unchecked out Elem]: _Each[Elem]
def each_cons: (Integer n) { (::Array[Elem] arg0) -> untyped } -> NilClass
| (Integer n) -> ::Enumerator[::Array[Elem], NilClass]

def each_with_index: () { (Elem arg0, Integer arg1) -> untyped } -> void
| () -> ::Enumerator[[ Elem, Integer ], void]
def each_with_index: () { (Elem, Integer index) -> untyped } -> self
| () -> ::Enumerator[[ Elem, Integer ], self]

def each_with_object: [U] (U arg0) { (Elem arg0, untyped arg1) -> untyped } -> U
| [U] (U arg0) -> ::Enumerator[[ Elem, U ], U]
def each_with_object: [U] (U obj) { (Elem, U obj) -> untyped } -> U
| [U] (U obj) -> ::Enumerator[[ Elem, U ], U]

# Returns an array containing the items in *enum* .
#
Expand All @@ -102,7 +103,7 @@ module Enumerable[unchecked out Elem]: _Each[Elem]
alias select find_all
alias filter find_all

def find_index: (?untyped value) -> Integer?
def find_index: (untyped value) -> Integer?
| () { (Elem) -> boolish } -> Integer?
| () -> ::Enumerator[Elem, Integer?]

Expand All @@ -123,8 +124,8 @@ module Enumerable[unchecked out Elem]: _Each[Elem]
def grep: (untyped arg0) -> ::Array[Elem]
| [U] (untyped arg0) { (Elem arg0) -> U } -> ::Array[U]

def grep_v: (untyped arg0) -> ::Array[Integer]
| [U] (untyped arg0) { (Elem arg0) -> U } -> ::Array[U]
def grep_v: (untyped) -> ::Array[Elem]
| [U] (untyped) { (Elem) -> U } -> ::Array[U]

def group_by: [U] () { (Elem arg0) -> U } -> ::Hash[U, ::Array[Elem]]
| () -> ::Enumerator[Elem, ::Array[Elem]]
Expand Down Expand Up @@ -410,8 +411,8 @@ module Enumerable[unchecked out Elem]: _Each[Elem]
def zip: [Elem2] (::Enumerable[Elem2] enum) -> ::Array[[Elem, Elem2 | nil]]
| [U, Elem2] (::Enumerable[Elem2]) { ([Elem, Elem2 | nil]) -> U } -> nil

def chunk: [U] () { (Elem elt) -> U } -> ::Enumerator[[U, Array[Elem]], void]
| () -> ::Enumerator[Elem, Enumerator[untyped, untyped]]
def chunk: [U] () { (Elem elt) -> U } -> ::Enumerator[[U, ::Array[Elem]], void]
| () -> ::Enumerator[Elem, ::Enumerator[[untyped, ::Array[Elem]], void]]

def chunk_while: () { (Elem elt_before, Elem elt_after) -> boolish } -> ::Enumerator[::Array[Elem], void]

Expand Down
4 changes: 2 additions & 2 deletions core/enumerator.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -238,8 +238,8 @@ class Enumerator[unchecked out Elem, out Return] < Object
def with_index: (?Integer offset) { (Elem arg0, Integer arg1) -> untyped } -> Return
| (?Integer offset) -> ::Enumerator[[ Elem, Integer ], Return]

def with_object: [U] (U arg0) { (Elem arg0, U arg1) -> untyped } -> U
| [U] (U arg0) -> ::Enumerator[[ Elem, U ], Return]
def with_object: [U] (U obj) { (Elem, U obj) -> untyped } -> U
| [U] (U obj) -> ::Enumerator[[ Elem, U ], U]
end

class Enumerator::Generator[out Elem] < Object
Expand Down
8 changes: 4 additions & 4 deletions core/false_class.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
class FalseClass
public

def !: () -> bool
def !: () -> true

# And---Returns `false`. *obj* is always evaluated as it is the argument to a
# method call---there is no short-circuit evaluation in this case.
#
def &: (untyped obj) -> bool
def &: (untyped obj) -> false

# Case Equality -- For class Object, effectively the same as calling `#==`, but
# typically overridden by descendants to provide meaningful semantics in `case`
Expand All @@ -24,7 +24,7 @@ class FalseClass
#
def ^: (nil) -> false
| (false) -> false
| (untyped obj) -> bool
| (untyped obj) -> true

alias inspect to_s

Expand All @@ -36,5 +36,5 @@ class FalseClass
#
def |: (nil) -> false
| (false) -> false
| (untyped obj) -> bool
| (untyped obj) -> true
end
2 changes: 1 addition & 1 deletion core/float.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ class Float < Numeric
# 1.2.coerce(3) #=> [3.0, 1.2]
# 2.5.coerce(1.1) #=> [1.1, 2.5]
#
def coerce: (Numeric) -> [Numeric, Numeric]
def coerce: (Numeric) -> [Float, Float]

def conj: () -> Float

Expand Down
12 changes: 6 additions & 6 deletions core/hash.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -493,8 +493,8 @@ class Hash[unchecked out K, unchecked out V] < Object
# h.fetch_values("cow", "bird") # raises KeyError
# h.fetch_values("cow", "bird") { |k| k.upcase } #=> ["bovine", "BIRD"]
#
def fetch_values: (*K) -> Array[V]
| [X] (*K) { (K) -> X } -> (V | X)
def fetch_values: (*K) -> ::Array[V]
| [X] (*K) { (K) -> X } -> ::Array[V | X]

# Returns a new hash consisting of entries for which the block returns true.
#
Expand Down Expand Up @@ -776,7 +776,7 @@ class Hash[unchecked out K, unchecked out V] < Object
# h = { "a" => 100, "b" => 200 }
# h.replace({ "c" => 300, "d" => 400 }) #=> {"c"=>300, "d"=>400}
#
def replace: (Hash[K, V]) -> self
def replace: [A, B] (Hash[A, B]) -> Hash[A, B]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def replace: [A, B] (Hash[A, B]) -> Hash[A, B]
def replace: [A, B] (Hash[A, B]) -> Hash[A | K, B | V]

Copy link
Contributor Author

@hjwylde hjwylde Aug 8, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you elaborate on the reason for having | K/| V please?

AFAIK replace clears the hash first, so there shouldn't be any Ks or Vs left. I could see a reason for having | K/| V for {a: 0, b: 1}.replace({}), however here I think it's better to just use clear which will retain the receiver's type information.

This change is probably related to the collect! discussion too, as it's essentially the same behaviour.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My bad. I was confusing with #merge.
Will continue at #collect! for in-place updates.


# Returns a new hash consisting of entries for which the block returns true.
#
Expand Down Expand Up @@ -920,7 +920,7 @@ class Hash[unchecked out K, unchecked out V] < Object
#
# If no block is given, an enumerator is returned instead.
#
def transform_values: () -> Enumerator[K, Hash[K, untyped]]
def transform_values: () -> Enumerator[V, Hash[K, untyped]]
| [A] () { (V) -> A } -> Hash[K, A]

# Invokes the given block once for each value in *hsh*, replacing it with the
Expand All @@ -935,8 +935,8 @@ class Hash[unchecked out K, unchecked out V] < Object
#
# If no block is given, an enumerator is returned instead.
#
def transform_values!: () -> Enumerator[K, Hash[K, untyped]]
| () { (V) -> V } -> Hash[K, V]
def transform_values!: () -> Enumerator[V, Hash[K, untyped]]
| [A] () { (V) -> A } -> Hash[K, A]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please keep the monomorphic version.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I'll just wait until the discussion on collect! is resolved as it's related.


# Adds the contents of the given hashes to the receiver.
#
Expand Down
3 changes: 1 addition & 2 deletions core/integer.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -361,8 +361,7 @@ class Integer < Numeric
# 18.floor(-1) #=> 10
# (-18).floor(-1) #=> -20
#
def floor: () -> Integer
| (int digits) -> (Integer | Float)
def floor: (?int digits) -> Integer

# Returns the greatest common divisor of the two integers. The result is always
# positive. 0.gcd(x) and x.gcd(0) return x.abs.
Expand Down
20 changes: 11 additions & 9 deletions core/range.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,9 @@ class Range[out Elem] < Object
# ```
def begin: () -> Elem # Begin-less ranges have type of Range[Integer?]

def bsearch: [U] () { (Elem) -> boolish } -> U?
def bsearch: () -> ::Enumerator[Elem, Elem?]
| () { (Elem) -> (true | false) } -> Elem?
| () { (Elem) -> ::Integer } -> Elem?

def cover?: (untyped obj) -> bool

Expand Down Expand Up @@ -132,7 +134,7 @@ class Range[out Elem] < Object
# (10..20).first(3) #=> [10, 11, 12]
# ```
def first: () -> Elem
| (?Integer n) -> ::Array[Elem]
| (Integer n) -> ::Array[Elem]

# Compute a hash-code for this range. Two ranges with equal begin and end
# points (using `eql?` ), and the same
Expand Down Expand Up @@ -165,7 +167,7 @@ class Range[out Elem] < Object
# (10...20).last(3) #=> [17, 18, 19]
# ```
def last: () -> Elem
| (?Integer n) -> ::Array[Elem]
| (Integer n) -> ::Array[Elem]

# Returns the maximum value in the range. Returns `nil` if the begin value
# of the range larger than the end value. Returns `nil` if the begin value
Expand All @@ -178,9 +180,9 @@ class Range[out Elem] < Object
# (10..20).max #=> 20
# ```
def max: () -> Elem
| () { (Elem arg0, Elem arg1) -> Integer } -> Elem
| (?Integer n) -> ::Array[Elem]
| (?Integer n) { (Elem arg0, Elem arg1) -> Integer } -> ::Array[Elem]
| () { (Elem a, Elem b) -> Integer } -> Elem
| (Integer n) -> ::Array[Elem]
| (Integer n) { (Elem a, Elem b) -> Integer } -> ::Array[Elem]

# Returns the minimum value in the range. Returns `nil` if the begin value
# of the range is larger than the end value. Returns `nil` if the begin
Expand All @@ -193,9 +195,9 @@ class Range[out Elem] < Object
# (10..20).min #=> 10
# ```
def min: () -> Elem
| () { (Elem arg0, Elem arg1) -> Integer } -> Elem
| (?Integer n) -> ::Array[Elem]
| (?Integer n) { (Elem arg0, Elem arg1) -> Integer } -> ::Array[Elem]
| () { (Elem a, Elem b) -> Integer } -> Elem
| (Integer n) -> ::Array[Elem]
| (Integer n) { (Elem a, Elem b) -> Integer } -> ::Array[Elem]

# Returns the number of elements in the range. Both the begin and the end
# of the [Range](Range.downloaded.ruby_doc) must be
Expand Down
8 changes: 4 additions & 4 deletions core/true_class.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
class TrueClass
public

def !: () -> bool
def !: () -> false

# And---Returns `false` if *obj* is `nil` or `false`, `true` otherwise.
#
def &: (nil) -> false
| (false) -> false
| (untyped obj) -> bool
| (untyped obj) -> true

# Case Equality -- For class Object, effectively the same as calling `#==`, but
# typically overridden by descendants to provide meaningful semantics in `case`
Expand All @@ -24,7 +24,7 @@ class TrueClass
#
def ^: (nil) -> true
| (false) -> true
| (untyped obj) -> bool
| (untyped obj) -> false

alias inspect to_s

Expand All @@ -42,5 +42,5 @@ class TrueClass
#
# or
#
def |: (boolish obj) -> bool
def |: (untyped obj) -> true
end
2 changes: 1 addition & 1 deletion test/rbs/method_type_parsing_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def test_method_param
Parser.parse_method_type("(untyped _)->void").yield_self do |type|
assert_equal "(untyped _) -> void", type.to_s
end
end
end

def test_method_type_eof_re
Parser.parse_method_type("()->void~ Integer", eof_re: /~/).yield_self do |type|
Expand Down
13 changes: 9 additions & 4 deletions test/stdlib/Array_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,9 @@ def test_at
end

def test_bsearch
assert_send_type "() -> Enumerable[String, Integer?]", [0,1,2,3,4],
:bsearch

assert_send_type "() { (Integer) -> (true | false) } -> Integer",
[0,1,2,3,4], :bsearch do |x| x > 2 end
assert_send_type "() { (Integer) -> (true | false) } -> nil",
Expand Down Expand Up @@ -188,6 +191,8 @@ def test_llear
def test_collect
assert_send_type "() { (Integer) -> String } -> Array[String]",
[1,2,3], :collect do |x| x.to_s end
assert_send_type "() -> Enumerator[Integer, Array[untyped]]",
[1,2,3], :collect
end

def test_collect!
Expand Down Expand Up @@ -309,10 +314,10 @@ def test_each
end

def test_each_index
assert_send_type "() { (Integer) -> void } -> Array[Integer]",
[1,2,3], :each_index do end
assert_send_type "() -> Enumerator[Integer, Array[Integer]]",
[1,2,3], :each_index
assert_send_type "() { (Integer) -> void } -> Array[String]",
['1','2','3'], :each_index do end
assert_send_type "() -> Enumerator[Integer, Array[String]]",
['1','2','3'], :each_index
end

def test_empty?
Expand Down
40 changes: 40 additions & 0 deletions test/stdlib/Enumerable_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,46 @@ def each

testing "::Enumerable[String]"

def test_chunk
assert_send_type "() -> ::Enumerator[String, ::Enumerator[[untyped, ::Array[String]], void]]",
TestEnumerable.new, :chunk
assert_send_type "() { (String) -> Integer } -> ::Enumerator[[Integer, ::Array[String]], void]",
TestEnumerable.new, :chunk do |x| x.to_i end
end

def test_collect_concat
assert_send_type "() -> ::Enumerator[String, ::Array[untyped]]",
TestEnumerable.new, :collect_concat

assert_send_type "{ (String) -> Integer } -> ::Array[Integer]",
TestEnumerable.new, :collect_concat do |x| x.to_i end
assert_send_type "{ (String) -> ::Array[Integer] } -> ::Array[Integer]",
TestEnumerable.new, :collect_concat do |x| [x.to_i] end
end

def test_each_with_object
assert_send_type "(Integer) -> ::Enumerator[[String, Integer], Integer]",
TestEnumerable.new, :each_with_object, 0
assert_send_type "(Integer) { (String, Integer) -> untyped } -> Integer",
TestEnumerable.new, :each_with_object, 0 do end
end

def test_find_index
assert_send_type "() -> ::Enumerator[String, Integer?]", TestEnumerable.new,
:find_index
assert_send_type "(untyped) -> Integer?", TestEnumerable.new, :find_index,
'0'
assert_send_type "() { (String) -> untyped } -> Integer?",
TestEnumerable.new, :find_index do end
end

def test_grepv
assert_send_type "(untyped) -> ::Array[String]", TestEnumerable.new,
:grep_v, '0'
assert_send_type "(untyped) { (String) -> Integer } -> ::Array[Integer]",
TestEnumerable.new, :grep_v, '0' do 0 end
end

def test_inject
assert_send_type "(String init, Symbol method) -> untyped", TestEnumerable.new, :inject, '', :<<
assert_send_type "(Symbol method) -> String", TestEnumerable.new, :inject, :+
Expand Down
6 changes: 6 additions & 0 deletions test/stdlib/Enumerator_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ def test_map
assert_send_type "() -> Enumerator[Integer, Array[untyped]]",
g, :map
end

def test_with_object
g = [1,2,3].to_enum
assert_send_type "(String) -> Enumerator[[Integer, String], String]", g, :with_object, ''
assert_send_type "(String) { (Integer, String) -> untyped } -> String", g, :with_object, '' do end
end
end

class EnumeratorYielderTest < Test::Unit::TestCase
Expand Down
5 changes: 5 additions & 0 deletions test/stdlib/Float_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ def test_ceil
a.ceil(ToInt.new)
end

def test_coerce
1.2.coerce(3)
2.5.coerce(1.1)
end

def test_conj
a = 31.4

Expand Down
Loading