Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
30 changes: 19 additions & 11 deletions core/kernel.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -480,9 +480,12 @@ module Kernel : BasicObject
#
def self?.Complex: (_ToC complex_like, ?exception: true) -> Complex
| (_ToC complex_like, exception: bool) -> Complex?
| (Numeric | String real, ?Numeric | String imag, ?exception: true) -> Complex
| (Numeric | String real, ?Numeric | String imag, exception: bool) -> Complex?
| (untyped, ?untyped, ?exception: bool) -> Complex?
| (Numeric numeric, ?exception: bool) -> Complex
| (String real_or_both, ?exception: true) -> Complex
| (untyped real_or_both, exception: bool) -> Complex?
| (Numeric | String real, Numeric | String imag, ?exception: true) -> Complex
| (Numeric | String real, Integer | Float | Rational | Complex imag, exception: bool) -> Complex
| (Numeric | String real, untyped imag, exception: bool) -> Complex?
Comment on lines +483 to +488
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This one's been changed because, after looking into it, Complex is quite messy:

  • Complex(Numeric) can never return nil, regardless of exception
  • Complex(Numeric | String, denom, exception: false) will actually return nil when the denom is a non-Integer | Float | Complex | Rational Numeric. Wonky, because exception: true/omitted doesn't do that, and it's only for the second argument


# <!--
# rdoc-file=kernel.rb
Expand All @@ -502,7 +505,7 @@ module Kernel : BasicObject
#
def self?.Float: (_ToF float_like, ?exception: true) -> Float
| (_ToF float_like, exception: bool) -> Float?
| (untyped, ?exception: bool) -> Float?
| (untyped, exception: bool) -> Float?

# <!--
# rdoc-file=object.c
Expand All @@ -524,7 +527,7 @@ module Kernel : BasicObject
# Hash(nil) # => {}
# Hash([]) # => {}
#
def self?.Hash: [K, V] (nil | [] _empty) -> Hash[K, V]
def self?.Hash: [K, V] (nil | []) -> Hash[K, V]
| [K, V] (hash[K, V] hash_like) -> Hash[K, V]

# <!--
Expand Down Expand Up @@ -614,7 +617,7 @@ module Kernel : BasicObject
| (int | _ToI int_like, exception: bool) -> Integer?
| (string str, int base, ?exception: true) -> Integer
| (string str, int base, exception: bool) -> Integer?
| (untyped, ?untyped, ?exception: bool) -> Integer?
| (untyped, ?int base, exception: bool) -> Integer?
Copy link
Contributor Author

Choose a reason for hiding this comment

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

If provided, the second argument actually has to be an int, otherwise an error will be thrown (regardless of exception)


# <!--
# rdoc-file=rational.c
Expand Down Expand Up @@ -655,14 +658,19 @@ module Kernel : BasicObject
#
def self?.Rational: (int | _ToR rational_like, ?exception: true) -> Rational
| (int | _ToR rational_like, exception: bool) -> Rational?
| (int | _ToR numer, ?int | _ToR denom, ?exception: true) -> Rational
| (int | _ToR numer, ?int | _ToR denom, exception: bool) -> Rational?
| [T] (Numeric & _RationalDiv[T] numer, Numeric denom, ?exception: bool) -> T
| (int | _ToR numer, int | _ToR denom, ?exception: true) -> Rational
| (int | _ToR numer, int | _ToR denom, exception: bool) -> Rational?
Comment on lines +661 to +662
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 simplified the signature of this a bit, collapsing the int | _ToR numer, ?... into the ones above it

| [T < Numeric] (T value, 1, ?exception: bool) -> T
| (untyped, ?untyped, ?exception: bool) -> Rational?
| [T] (Numeric & _RationalDiv[T] numer, Numeric denom, ?exception: bool) -> T
| (untyped, ?untyped, exception: bool) -> Rational?

# An interface used in `Kernel.Rational` when both arguments are `Numeric`s,
# but don't define `to_r` or `to_int`.
#
# The return type of the division is the return type of `Rational(numer, denom)`.
interface _RationalDiv[T]
def /: (Numeric) -> T
# Divide the numerator by `denom`
def /: (Numeric denom) -> T
end

# <!--
Expand Down
7 changes: 6 additions & 1 deletion lib/rbs/test/type_check.rb
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,12 @@ def value(val, type)
when Types::Variable
true
when Types::Literal
type.literal == val
begin
type.literal == val
rescue NoMethodError
raise if defined?(val.==)
false
Comment on lines +344 to +347
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is needed because one of the Rational's signatures has a 1 in it, but is compared against a ToInt, which doesn't define ==.

end
when Types::Union
type.types.any? {|type| value(val, type) }
when Types::Intersection
Expand Down
4 changes: 2 additions & 2 deletions lib/rbs/unit_test/type_assertions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -180,11 +180,11 @@ def send_setup(method_type, receiver, method, args, proc)
)
errors = typecheck.method_call(method, method_type, trace, errors: [])

assert_empty errors.map {|x| RBS::Test::Errors.to_string(x) }, "Call trace does not match with given method type: #{trace.inspect}"
assert_empty errors.map {|x| RBS::Test::Errors.to_string(x) }, proc { "Call trace does not match with given method type: #{trace.inspect}" }
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Instead of always creating the string for the traces, passing a proc in delays it until later. This is necessary because some of the Complex methods would be valid (eg Complex(1, Class.new(Numeric).new)), but the trace.inspect would end up calling #inspect on the resulting Complex, which raised an exception.


method_defs = method_defs(method)
all_errors = method_defs.map {|t| typecheck.method_call(method, t.type, trace, errors: [], annotations: t.each_annotation.to_a) }
assert all_errors.any? {|es| es.empty? }, "Call trace does not match one of method definitions:\n #{trace.inspect}\n #{method_defs.map(&:type).join(" | ")}"
assert all_errors.any? {|es| es.empty? }, proc { "Call trace does not match one of method definitions:\n #{trace.inspect}\n #{method_defs.map(&:type).join(" | ")}" }

raise exception if exception

Expand Down
10 changes: 5 additions & 5 deletions sig/unit_test/type_assertions.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -30,19 +30,19 @@ module RBS
type target_type = Types::ClassInstance | Types::ClassSingleton

interface _BaseAssertions
def assert: (untyped, ?String?) -> void
def assert: (untyped, ?String? | Proc) -> void

def refute: (untyped, ?String?) -> void
def refute: (untyped, ?String? | Proc) -> void

def assert_empty: (untyped, ?String?) -> void
def assert_empty: (untyped, ?String | nil | Proc) -> void

def assert_operator: (untyped, Symbol, *untyped) -> void

def notify: (untyped) -> void

def assert_predicate: (untyped, Symbol, ?String?) -> void
def assert_predicate: (untyped, Symbol, ?String? | Proc) -> void

def refute_predicate: (untyped, Symbol, ?String?) -> void
def refute_predicate: (untyped, Symbol, ?String? | Proc) -> void
end

module ClassMethods
Expand Down
191 changes: 163 additions & 28 deletions test/stdlib/Kernel_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,73 +11,208 @@ def test_Array
assert_send_type "(nil) -> []",
Kernel, :Array, nil

with_array(1r, 2r).chain([ToA.new(1r,2r)]).each do |ary|
assert_send_type "(::array[Rational] | ::_ToA[Rational]) -> Array[Rational]",
Kernel, :Array, ary
with_untyped do |ele|
with_array(ele, ele).and ToA.new(ele, ele) do |ary|
assert_send_type "[T] (array[T] | _ToA[T]) -> Array[T]",
Kernel, :Array, ary
end

next if defined?(ele.to_a) || defined?(ele.to_ary)
assert_send_type "[T] (T) -> [T]",
Kernel, :Array, ele
end
end

def test_Complex
# (_ToC complex_like, ?exception: true) -> Complex
assert_send_type "(_ToC) -> Complex",
Kernel, :Complex, ToC.new
assert_send_type "(_ToC, exception: true) -> Complex",
Kernel, :Complex, ToC.new, exception: true

# (_ToC complex_like, exception: bool) -> Complex?
assert_send_type "(_ToC, exception: bool) -> Complex",
Kernel, :Complex, ToC.new, exception: false
assert_send_type "(_ToC, exception: bool) -> nil",
Kernel, :Complex, Class.new(BlankSlate){ def to_c = fail }.new, exception: false

assert_send_type "(Rational) -> [Rational]",
Kernel, :Array, 1r
numeric = Class.new(Numeric).new

# (Numeric numeric, ?exception: bool) -> Complex
with 1, 1r, 1.0, (1+0i), numeric do |real|
assert_send_type "(Numeric) -> Complex",
Kernel, :Complex, real

# Single `Numeric`s can never fail
with_bool do |exception|
assert_send_type "(Numeric, exception: bool) -> Complex",
Kernel, :Complex, real, exception: exception
end
end

# (String real_or_both, ?exception: true) -> Complex
assert_send_type "(String) -> Complex",
Kernel, :Complex, '1'
assert_send_type "(String, exception: true) -> Complex",
Kernel, :Complex, '1', exception: true

# (untyped real_or_both, exception: bool) -> Complex?
with_untyped.and 'oops' do |real_untype|
assert_send_type '(untyped, exception: bool) -> Complex?',
Kernel, :Complex, real_untype, exception: false
end

with '1', 1, 1r, 1.0, (1+0i), numeric do |real|
with '2', 2, 2r, 2.0, (2+0i), numeric do |imag|
# (Numeric | String real, Numeric | String imag, ?exception: true) -> Complex
assert_send_type "(Numeric | String, Numeric | String) -> Complex",
Kernel, :Complex, real, imag
assert_send_type "(Numeric | String, Numeric | String, exception: true) -> Complex",
Kernel, :Complex, real, imag, exception: true

# Complex has an awkward edgecase where `exception: false` will unconditionally return `nil`
# if the imaginary argument is not one of the builtin `Numeric`s. Oddly enough, it's not for
# the `real` one...
case imag
when Integer, Float, Rational, Complex
# (Numeric | String real, Integer | Float | Rational | Complex imag, exception: bool) -> Complex
assert_send_type "(Numeric | String, Integer | Float | Rational | Complex, exception: bool) -> Complex",
Kernel, :Complex, real, imag, exception: false
end
end

# (Numeric | String real, untyped, exception: bool) -> Complex?
with_untyped.and 'oops', numeric do |imag|
next if [Integer, Float, Rational, Complex].any? { _1 === imag }
assert_send_type "(Numeric | String, untyped, exception: bool) -> nil",
Kernel, :Complex, real, imag, exception: false
end
end
end


def test_Float
with_float 1.0 do |float|
assert_send_type "(::float) -> Float",
Kernel, :Float, float
assert_send_type "(::float, exception: true) -> Float",
Kernel, :Float, float, exception: true
assert_send_type "(::float, exception: bool) -> Float?",
Kernel, :Float, float, exception: false
with 1, 1.0, ToF.new(1.0), '1e3' do |float_like|
assert_send_type "(_ToF) -> Float",
Kernel, :Float, float_like
assert_send_type "(_ToF, exception: true) -> Float",
Kernel, :Float, float_like, exception: true
assert_send_type "(_ToF, exception: bool) -> Float",
Kernel, :Float, float_like, exception: false
end

assert_send_type "(untyped, ?exception: bool) -> Float?",
Kernel, :Float, :hello, exception: false
with_untyped do |untyped|
next if defined? untyped.to_f
assert_send_type "(untyped, exception: bool) -> nil",
Kernel, :Float, untyped, exception: false
end
end

def test_Hash
assert_send_type "(nil) -> Hash[untyped, untyped]",
assert_send_type "[K, V] (nil) -> Hash[K, V]",
Kernel, :Hash, nil
assert_send_type "([]) -> Hash[untyped, untyped]",
assert_send_type "[K, V] ([]) -> Hash[K, V]",
Kernel, :Hash, []

with_hash 'a' => 3 do |hash|
assert_send_type "(::hash[String, Integer]) -> Hash[String, Integer]",
assert_send_type "[K, V] (hash[K, V]) -> Hash[K, V]",
Kernel, :Hash, hash
end
end

def test_Integer
with_int(1).chain([ToI.new(1)]).each do |int|
assert_send_type "(::int | ::_ToI) -> Integer",
with_int.and ToI.new do |int|
assert_send_type "(int | _ToI) -> Integer",
Kernel, :Integer, int
assert_send_type "(::int | ::_ToI, exception: true) -> Integer",
assert_send_type "(int | _ToI, exception: true) -> Integer",
Kernel, :Integer, int, exception: true
assert_send_type "(::int | ::_ToI, exception: bool) -> Integer?",
assert_send_type "(int | _ToI, exception: bool) -> Integer?",
Kernel, :Integer, int, exception: false
end

with_string "123" do |string|
with_int 8 do |base|
assert_send_type "(::string, ::int) -> Integer",
assert_send_type "(string, int) -> Integer",
Kernel, :Integer, string, base
assert_send_type "(::string, ::int, exception: true) -> Integer",
assert_send_type "(string, int, exception: true) -> Integer",
Kernel, :Integer, string, base, exception: true
assert_send_type "(::string, ::int, exception: bool) -> Integer?",
assert_send_type "(string, int, exception: bool) -> Integer?",
Kernel, :Integer, string, base, exception: false
end
end

assert_send_type "(untyped, ?exception: bool) -> Integer?",
Kernel, :Integer, :hello, exception: false
with_untyped do |untyped|
assert_send_type "(untyped, exception: bool) -> Integer?",
Kernel, :Integer, untyped, exception: false

with_int 10 do |base|
assert_send_type "(untyped, int, exception: bool) -> Integer?",
Kernel, :Integer, untyped, base, exception: false
end
end
end

def test_Rational
with_int(1).and ToR.new(1r) do |numer|
assert_send_type "(int | _ToR) -> Rational",
Kernel, :Rational, numer
assert_send_type "(int | _ToR, exception: true) -> Rational",
Kernel, :Rational, numer, exception: true
assert_send_type "(int | _ToR, exception: bool) -> Rational",
Kernel, :Rational, numer, exception: false

with_int(2).and ToR.new(2r) do |denom|
assert_send_type "(int | _ToR, int | _ToR) -> Rational",
Kernel, :Rational, numer, denom
assert_send_type "(int | _ToR, int | _ToR, exception: true) -> Rational",
Kernel, :Rational, numer, denom, exception: true
assert_send_type "(int | _ToR, int | _ToR, exception: bool) -> Rational",
Kernel, :Rational, numer, denom, exception: false
end
end

bad_int = Class.new(BlankSlate){ def to_int = fail }.new
bad_rat = Class.new(BlankSlate){ def to_r = fail }.new
with bad_int, bad_rat do |bad_numer|
assert_send_type "(int | _ToR, exception: bool) -> nil",
Kernel, :Rational, bad_numer, exception: false
assert_send_type "(int | _ToR, int | _ToR, exception: bool) -> nil",
Kernel, :Rational, bad_numer, bad_numer, exception: false
end


numeric = Class.new(Numeric).new
assert_send_type "[T < _Numeric] (T numer, 1) -> T",
Kernel, :Rational, numeric, 1
assert_send_type "[T < _Numeric] (T numer, 1, exception: bool) -> T",
Kernel, :Rational, numeric, 1, exception: true
assert_send_type "[T < _Numeric] (T numer, 1, exception: bool) -> T",
Kernel, :Rational, numeric, 1, exception: false

numeric_div = Class.new(Numeric){ def /(other) = :hello }.new

assert_send_type "[T] (Numeric & Kernel::_RationalDiv[T] numer, Numeric denom) -> T",
Kernel, :Rational, numeric_div, numeric
assert_send_type "[T] (Numeric & Kernel::_RationalDiv[T] numer, Numeric denom, exception: bool) -> T",
Kernel, :Rational, numeric_div, numeric, exception: true
assert_send_type "[T] (Numeric & Kernel::_RationalDiv[T] numer, Numeric denom, exception: bool) -> T",
Kernel, :Rational, numeric_div, numeric, exception: false

with_untyped do |numer|
with_untyped do |denom|
assert_send_type "(untyped, untyped, exception: bool) -> Rational?",
Kernel, :Rational, numer, denom, exception: false
end
end
end

def test_String
with_string do |string|
assert_send_type "(::string) -> String",
assert_send_type "(string) -> String",
Kernel, :String, string
end

assert_send_type "(::_ToS) -> String",
assert_send_type "(_ToS) -> String",
Kernel, :String, ToS.new
end

Expand Down