Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow the empty instantiation NamedTuple() #12009

Merged
Merged
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
5 changes: 5 additions & 0 deletions spec/compiler/formatter/formatter_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,11 @@ describe Crystal::Formatter do
assert_format "::Pointer(T)"
assert_format "::StaticArray(T)"

assert_format "Tuple()"
assert_format "::Tuple()"
assert_format "NamedTuple()"
assert_format "::NamedTuple()"

%w(if unless).each do |keyword|
assert_format "#{keyword} a\n2\nend", "#{keyword} a\n 2\nend"
assert_format "#{keyword} a\n2\n3\nend", "#{keyword} a\n 2\n 3\nend"
Expand Down
13 changes: 5 additions & 8 deletions spec/compiler/semantic/named_tuple_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -104,20 +104,17 @@ describe "Semantic: named tuples" do
"can only use named arguments with NamedTuple"
end

it "gives error when using named args on Tuple" do
assert_error %(
Tuple(x: Int32, y: Char)
),
"can only use named arguments with NamedTuple"
end

it "gives error when not using named args with NamedTuple" do
it "gives error when using positional args with NamedTuple" do
assert_error %(
NamedTuple(Int32, Char)
),
"can only instantiate NamedTuple with named arguments"
end

it "doesn't error if NamedTuple has no args" do
assert_type("NamedTuple()") { named_tuple_of({} of String => Type).metaclass }
end

it "gets type at compile time" do
assert_type(%(
struct NamedTuple
Expand Down
46 changes: 46 additions & 0 deletions spec/compiler/semantic/restrictions_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -916,4 +916,50 @@ describe "Restrictions" do
foo x
)) { int32 }
end

it "errors if using Tuple with named args" do
assert_error <<-CR, "can only instantiate NamedTuple with named arguments"
def foo(x : Tuple(a: Int32))
end

foo({1})
CR
end

it "doesn't error if using Tuple with no args" do
assert_type(<<-CR) { tuple_of([] of Type) }
def foo(x : Tuple())
x
end

def bar(*args : *T) forall T
args
end

foo(bar)
CR
end

it "errors if using NamedTuple with positional args" do
assert_error <<-CR, "can only instantiate NamedTuple with named arguments"
def foo(x : NamedTuple(Int32))
end

foo({a: 1})
CR
end

it "doesn't error if using NamedTuple with no args" do
assert_type(<<-CR) { named_tuple_of({} of String => Type) }
def foo(x : NamedTuple())
x
end

def bar(**opts : **T) forall T
opts
end

foo(bar)
CR
end
end
11 changes: 11 additions & 0 deletions spec/compiler/semantic/tuple_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,17 @@ describe "Semantic: tuples" do
assert_type("Tuple(Int32, Float64)") { tuple_of([int32, float64]).metaclass }
end

it "gives error when using named args on Tuple" do
assert_error %(
Tuple(x: Int32, y: Char)
),
"can only use named arguments with NamedTuple"
end

it "doesn't error if Tuple has no args" do
assert_type("Tuple()") { tuple_of([] of Type).metaclass }
end

it "types T as a tuple of metaclasses" do
assert_type("
struct Tuple
Expand Down
5 changes: 3 additions & 2 deletions src/compiler/crystal/semantic/bindings.cr
Original file line number Diff line number Diff line change
Expand Up @@ -530,7 +530,8 @@ module Crystal
def update(from = nil)
instance_type = self.instance_type
if instance_type.is_a?(NamedTupleType)
entries = named_args.not_nil!.map do |named_arg|
entries = Array(NamedArgumentType).new(named_args.try(&.size) || 0)
named_args.try &.each do |named_arg|
node = named_arg.value

if node.is_a?(Path) && (syntax_replacement = node.syntax_replacement)
Expand All @@ -551,7 +552,7 @@ module Crystal
Crystal.check_type_can_be_stored(node, node_type, "can't use #{node_type} as generic type argument")
node_type = node_type.virtual_type

NamedArgumentType.new(named_arg.name, node_type)
entries << NamedArgumentType.new(named_arg.name, node_type)
end

generic_type = instance_type.instantiate_named_args(entries)
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/crystal/semantic/main_visitor.cr
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ module Crystal
end

if instance_type.double_variadic?
unless node.named_args
unless node.type_vars.empty?
node.raise "can only instantiate NamedTuple with named arguments"
end
else
Expand Down
23 changes: 15 additions & 8 deletions src/compiler/crystal/semantic/restrictions.cr
Original file line number Diff line number Diff line change
Expand Up @@ -761,14 +761,15 @@ module Crystal

generic_type = generic_type.as(GenericType)

if other.named_args
unless generic_type.is_a?(NamedTupleType)
other.raise "can only instantiate NamedTuple with named arguments"
end
# We match named tuples in NamedTupleInstanceType
# We match named tuples in NamedTupleInstanceType
if generic_type.is_a?(NamedTupleType)
return nil
end

if other.named_args
other.raise "can only instantiate NamedTuple with named arguments"
end

# Consider the case of a splat in the type vars
splat_index = self.splat_index
splat_given = other.type_vars.any?(Splat)
Expand Down Expand Up @@ -911,7 +912,9 @@ module Crystal
generic_type = get_generic_type(other, context)
return super unless generic_type == self.generic_type

generic_type = generic_type.as(TupleType)
if other.named_args
other.raise "can only instantiate NamedTuple with named arguments"
end

# Consider the case of a splat in the type vars
splat_given = other.type_vars.any?(Splat)
Expand Down Expand Up @@ -971,11 +974,15 @@ module Crystal
generic_type = get_generic_type(other, context)
return super unless generic_type == self.generic_type

other_named_args = other.named_args
unless other_named_args
unless other.type_vars.empty?
other.raise "can only instantiate NamedTuple with named arguments"
end

# Check for empty named tuples
unless other_named_args = other.named_args
return self.entries.empty? ? self : nil
end

# Check that the names are the same
other_names = other_named_args.map(&.name).sort!
self_names = self.entries.map(&.name).sort!
Expand Down
9 changes: 5 additions & 4 deletions src/compiler/crystal/semantic/type_lookup.cr
Original file line number Diff line number Diff line change
Expand Up @@ -174,12 +174,13 @@ class Crystal::Type

case instance_type
when NamedTupleType
named_args = node.named_args
unless named_args
unless node.type_vars.empty?
node.raise "can only instantiate NamedTuple with named arguments"
end

entries = named_args.map do |named_arg|
named_args = node.named_args
entries = Array(NamedArgumentType).new(named_args.try(&.size) || 0)
named_args.try &.each do |named_arg|
subnode = named_arg.value

if subnode.is_a?(NumberLiteral)
Expand All @@ -191,7 +192,7 @@ class Crystal::Type
type = type.not_nil!

check_type_can_be_stored(subnode, type, "can't use #{type} as a generic type argument")
NamedArgumentType.new(named_arg.name, type.virtual_type)
entries << NamedArgumentType.new(named_arg.name, type.virtual_type)
end

begin
Expand Down
5 changes: 3 additions & 2 deletions src/compiler/crystal/tools/formatter.cr
Original file line number Diff line number Diff line change
Expand Up @@ -1093,8 +1093,6 @@ module Crystal
check_open_paren

name = node.name.as(Path)
first_name = name.global? && name.names.size == 1 && name.names.first

if name.global? && @token.type.op_colon_colon?
write "::"
next_token_skip_space_or_newline
Expand Down Expand Up @@ -1147,6 +1145,9 @@ module Crystal
return false
end

# NOTE: not `name.single_name?` as this is only for the expanded tuple literals
first_name = name.names.first if name.global? && name.names.size == 1

# Check if it's {A, B} instead of Tuple(A, B)
if first_name == "Tuple" && @token.value != "Tuple"
write_token :OP_LCURLY
Expand Down
2 changes: 1 addition & 1 deletion src/named_tuple.cr
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ struct NamedTuple
options
{% elsif @type.name(generic_args: false) == "NamedTuple()" %}
# special case: empty named tuple
# TODO: check against `NamedTuple()` directly after 1.4.0
# TODO: check against `NamedTuple()` directly after 1.5.0
options
{% else %}
# explicitly provided type vars
Expand Down