Skip to content

Commit

Permalink
Showing 13 changed files with 194 additions and 18 deletions.
20 changes: 10 additions & 10 deletions spec/compiler/parser/parser_spec.cr
Original file line number Diff line number Diff line change
@@ -161,11 +161,11 @@ describe "Parser" do
it_parses "def foo(var = 1); end", Def.new("foo", [Arg.new("var", 1.int32)])
it_parses "def foo(var : Int); end", Def.new("foo", [Arg.new("var", restriction: "Int".path)])
it_parses "def foo(var : self); end", Def.new("foo", [Arg.new("var", restriction: Self.new)])
it_parses "def foo(var : self?); end", Def.new("foo", [Arg.new("var", restriction: Union.new([Self.new, Path.global("Nil")] of ASTNode))])
it_parses "def foo(var : self?); end", Def.new("foo", [Arg.new("var", restriction: Crystal::Union.new([Self.new, Path.global("Nil")] of ASTNode))])
it_parses "def foo(var : self.class); end", Def.new("foo", [Arg.new("var", restriction: Metaclass.new(Self.new))])
it_parses "def foo(var : self*); end", Def.new("foo", [Arg.new("var", restriction: Self.new.pointer_of)])
it_parses "def foo(var : Int | Double); end", Def.new("foo", [Arg.new("var", restriction: Union.new(["Int".path, "Double".path] of ASTNode))])
it_parses "def foo(var : Int?); end", Def.new("foo", [Arg.new("var", restriction: Union.new(["Int".path, "Nil".path(true)] of ASTNode))])
it_parses "def foo(var : Int | Double); end", Def.new("foo", [Arg.new("var", restriction: Crystal::Union.new(["Int".path, "Double".path] of ASTNode))])
it_parses "def foo(var : Int?); end", Def.new("foo", [Arg.new("var", restriction: Crystal::Union.new(["Int".path, "Nil".path(true)] of ASTNode))])
it_parses "def foo(var : Int*); end", Def.new("foo", [Arg.new("var", restriction: "Int".path.pointer_of)])
it_parses "def foo(var : Int**); end", Def.new("foo", [Arg.new("var", restriction: "Int".path.pointer_of.pointer_of)])
it_parses "def foo(var : Int -> Double); end", Def.new("foo", [Arg.new("var", restriction: Fun.new(["Int".path] of ASTNode, "Double".path))])
@@ -403,9 +403,9 @@ describe "Parser" do
it_parses "struct Foo; end", ClassDef.new("Foo".path, struct: true)

it_parses "Foo(T)", Generic.new("Foo".path, ["T".path] of ASTNode)
it_parses "Foo(T | U)", Generic.new("Foo".path, [Union.new(["T".path, "U".path] of ASTNode)] of ASTNode)
it_parses "Foo(Bar(T | U))", Generic.new("Foo".path, [Generic.new("Bar".path, [Union.new(["T".path, "U".path] of ASTNode)] of ASTNode)] of ASTNode)
it_parses "Foo(T?)", Generic.new("Foo".path, [Union.new(["T".path, Path.global("Nil")] of ASTNode)] of ASTNode)
it_parses "Foo(T | U)", Generic.new("Foo".path, [Crystal::Union.new(["T".path, "U".path] of ASTNode)] of ASTNode)
it_parses "Foo(Bar(T | U))", Generic.new("Foo".path, [Generic.new("Bar".path, [Crystal::Union.new(["T".path, "U".path] of ASTNode)] of ASTNode)] of ASTNode)
it_parses "Foo(T?)", Generic.new("Foo".path, [Crystal::Union.new(["T".path, Path.global("Nil")] of ASTNode)] of ASTNode)
it_parses "Foo(1)", Generic.new("Foo".path, [1.int32] of ASTNode)
it_parses "Foo(T, 1)", Generic.new("Foo".path, ["T".path, 1.int32] of ASTNode)
it_parses "Foo(T, U, 1)", Generic.new("Foo".path, ["T".path, "U".path, 1.int32] of ASTNode)
@@ -604,7 +604,7 @@ describe "Parser" do
it_parses "lib LibC\nfun getchar\nend", LibDef.new("LibC", [FunDef.new("getchar")] of ASTNode)
it_parses "lib LibC\nfun getchar(...)\nend", LibDef.new("LibC", [FunDef.new("getchar", varargs: true)] of ASTNode)
it_parses "lib LibC\nfun getchar : Int\nend", LibDef.new("LibC", [FunDef.new("getchar", return_type: "Int".path)] of ASTNode)
it_parses "lib LibC\nfun getchar : (->)?\nend", LibDef.new("LibC", [FunDef.new("getchar", return_type: Union.new([Fun.new, "Nil".path(true)] of ASTNode))] of ASTNode)
it_parses "lib LibC\nfun getchar : (->)?\nend", LibDef.new("LibC", [FunDef.new("getchar", return_type: Crystal::Union.new([Fun.new, "Nil".path(true)] of ASTNode))] of ASTNode)
it_parses "lib LibC\nfun getchar(Int, Float)\nend", LibDef.new("LibC", [FunDef.new("getchar", [Arg.new("", restriction: "Int".path), Arg.new("", restriction: "Float".path)])] of ASTNode)
it_parses "lib LibC\nfun getchar(a : Int, b : Float)\nend", LibDef.new("LibC", [FunDef.new("getchar", [Arg.new("a", restriction: "Int".path), Arg.new("b", restriction: "Float".path)])] of ASTNode)
it_parses "lib LibC\nfun getchar(a : Int)\nend", LibDef.new("LibC", [FunDef.new("getchar", [Arg.new("a", restriction: "Int".path)])] of ASTNode)
@@ -726,7 +726,7 @@ describe "Parser" do
it_parses "instance_sizeof(X)", InstanceSizeOf.new("X".path)

it_parses "foo.is_a?(Const)", IsA.new("foo".call, "Const".path)
it_parses "foo.is_a?(Foo | Bar)", IsA.new("foo".call, Union.new(["Foo".path, "Bar".path] of ASTNode))
it_parses "foo.is_a?(Foo | Bar)", IsA.new("foo".call, Crystal::Union.new(["Foo".path, "Bar".path] of ASTNode))
it_parses "foo.is_a? Const", IsA.new("foo".call, "Const".path)
it_parses "foo.responds_to?(:foo)", RespondsTo.new("foo".call, "foo")
it_parses "foo.responds_to? :foo", RespondsTo.new("foo".call, "foo")
@@ -856,9 +856,9 @@ describe "Parser" do
it_parses "a = 1\nfoo -a", [Assign.new("a".var, 1.int32), Call.new(nil, "foo", Call.new("a".var, "-"))]

it_parses "a : Foo", TypeDeclaration.new("a".var, "Foo".path)
it_parses "a : Foo | Int32", TypeDeclaration.new("a".var, Union.new(["Foo".path, "Int32".path] of ASTNode))
it_parses "a : Foo | Int32", TypeDeclaration.new("a".var, Crystal::Union.new(["Foo".path, "Int32".path] of ASTNode))
it_parses "@a : Foo", TypeDeclaration.new("@a".instance_var, "Foo".path)
it_parses "@a : Foo | Int32", TypeDeclaration.new("@a".instance_var, Union.new(["Foo".path, "Int32".path] of ASTNode))
it_parses "@a : Foo | Int32", TypeDeclaration.new("@a".instance_var, Crystal::Union.new(["Foo".path, "Int32".path] of ASTNode))
it_parses "@@a : Foo", TypeDeclaration.new("@@a".class_var, "Foo".path)
it_parses "$x : Foo", TypeDeclaration.new(Global.new("$x"), "Foo".path)

2 changes: 1 addition & 1 deletion spec/compiler/type_inference/generic_class_spec.cr
Original file line number Diff line number Diff line change
@@ -253,7 +253,7 @@ describe "Type inference: generic class" do
Foo(Char | String).bar
),
"can't lookup type in union (Char | String)"
"undefined constant T::Bar"
end

it "instantiates generic class with default argument in initialize (#394)" do
4 changes: 2 additions & 2 deletions spec/compiler/type_inference/restrictions_spec.cr
Original file line number Diff line number Diff line change
@@ -119,7 +119,7 @@ describe "Restrictions" do
foo(1 || 1.5)
),
"can't lookup type in union (Float64 | Int32)"
"undefined constant T::Baz"
end

it "errors on T::Type that's a union when used from block type restriction" do
@@ -131,7 +131,7 @@ describe "Restrictions" do
Foo(Int32 | Float64).foo { 1 + 2 }
),
"can't lookup type in union (Float64 | Int32)"
"undefined constant T::Baz"
end

it "errors if can't find type on lookup" do
82 changes: 82 additions & 0 deletions spec/compiler/type_inference/union_spec.cr
Original file line number Diff line number Diff line change
@@ -58,4 +58,86 @@ describe "Type inference: union" do
LibC::Foo.new.a
), flags: "some_flag") { int32 }
end

it "types union" do
assert_type(%(
Union(Int32, String)
)) { union_of(int32, string).metaclass }
end

it "types union of same type" do
assert_type(%(
Union(Int32, Int32, Int32)
)) { int32.metaclass }
end

it "can reopen Union" do
assert_type(%(
struct Union
def self.foo
1
end
end
Union(Int32, String).foo
)) { int32 }
end

it "can reopen Union and access T" do
assert_type(%(
struct Union
def self.types
T
end
end
Union(Int32, String).types
)) { tuple_of([int32, string]).metaclass }
end

it "can iterate T" do
assert_type(%(
struct Union
def self.types
{% begin %}
{
{% for type in T %}
{{type}},
{% end %}
}
{% end %}
end
end
Union(Int32, String).types
)) { tuple_of([int32.metaclass, string.metaclass]) }
end

it "errors if instantiates union" do
assert_error %(
Union(Int32, String).new
),
"can't create instance of a union type"
end

it "finds method in Object" do
assert_type(%(
class Object
def self.foo
1
end
end
Union(Int32, String).foo
)) { int32 }
end

it "finds method in Value" do
assert_type(%(
struct Value
def self.foo
1
end
end
Union(Int32, String).foo
)) { int32 }
end
end
6 changes: 6 additions & 0 deletions spec/std/json/serialization_spec.cr
Original file line number Diff line number Diff line change
@@ -111,6 +111,12 @@ describe "JSON serialization" do
Int32.from_json(%({"foo": 1}), root: "foo").should eq(1)
Array(Int32).from_json(%({"foo": [1, 2]}), root: "foo").should eq([1, 2])
end

{% if Crystal::VERSION == "0.18.0" %}
it "deserializes union" do
Array(Int32 | String).from_json(%([1, "hello"])).should eq([1, "hello"])
end
{% end %}
end

describe "to_json" do
4 changes: 3 additions & 1 deletion src/compiler/crystal/program.cr
Original file line number Diff line number Diff line change
@@ -156,6 +156,8 @@ module Crystal
proc.variadic = true
proc.allowed_in_generics = false

types["Union"] = @union = GenericUnionType.new self, self, "Union", value, ["T"]

types["Crystal"] = crystal_module = NonGenericModuleType.new self, self, "Crystal"
crystal_module.locations << Location.new(__LINE__ - 1, 0, __FILE__)

@@ -436,7 +438,7 @@ module Crystal

{% for name in %w(object no_return value number reference void nil bool char int int8 int16 int32 int64
uint8 uint16 uint32 uint64 float float32 float64 string symbol pointer array static_array
exception tuple named_tuple proc enum range regex crystal) %}
exception tuple named_tuple proc union enum range regex crystal) %}
def {{name.id}}
@{{name.id}}.not_nil!
end
3 changes: 3 additions & 0 deletions src/compiler/crystal/semantic/abstract_def_checker.cr
Original file line number Diff line number Diff line change
@@ -83,6 +83,9 @@ module Crystal
subtypes.try &.each do |subtype|
next if implements_with_parents?(subtype, method, base)

# Union doesn't need a hash, dup, to_s, etc., methods because it's special
next if subtype == @program.union

if subtype.abstract? || subtype.module?
check_implemented_in_subtypes(base, subtype, method)
else
5 changes: 4 additions & 1 deletion src/compiler/crystal/semantic/main_visitor.cr
Original file line number Diff line number Diff line change
@@ -2010,8 +2010,11 @@ module Crystal
def visit_allocate(node)
instance_type = scope.instance_type

if instance_type.is_a?(GenericClassType)
case instance_type
when GenericClassType
node.raise "can't create instance of generic class #{instance_type} without specifying its type vars"
when UnionType
node.raise "can't create instance of a union type"
end

if !instance_type.virtual? && instance_type.abstract?
5 changes: 4 additions & 1 deletion src/compiler/crystal/semantic/type_lookup.cr
Original file line number Diff line number Diff line change
@@ -342,7 +342,10 @@ module Crystal

class UnionType
def lookup_type(names : Array, already_looked_up = ObjectIdSet.new, lookup_in_container = true)
raise "can't lookup type in union #{self}"
if names.size == 1 && names[0] == "T"
return program.tuple_of(union_types)
end
program.lookup_type(names, already_looked_up, lookup_in_container)
end
end

46 changes: 44 additions & 2 deletions src/compiler/crystal/types.cr
Original file line number Diff line number Diff line change
@@ -143,6 +143,10 @@ module Crystal
self
end

def generic_class
raise "Bug: #{self} doesn't implement generic_class"
end

def includes_type?(type)
self == type
end
@@ -2577,7 +2581,7 @@ module Crystal
include DefInstanceContainer

getter program : Program
getter instance_type : GenericClassInstanceType
getter instance_type : Type

def initialize(@program, @instance_type)
end
@@ -2627,6 +2631,32 @@ module Crystal
end
end

class GenericUnionType < GenericClassType
def initialize(program, container, name, superclass, type_vars, add_subclass = true)
super
@variadic = true
@struct = true
end

def instantiate(type_vars)
types = type_vars.map do |type_var|
unless type_var.is_a?(Type)
type_var.raise "argument to Proc must be a type, not #{type_var}"
end
type_var
end
program.type_merge_union_of(types).not_nil!
end

def new_generic_instance(program, generic_type, type_vars)
raise "Bug: GenericUnionType#new_generic_instance shouldn't be invoked"
end

def type_desc
"union"
end
end

# Base class for union types.
abstract class UnionType < Type
include MultiType
@@ -2638,7 +2668,19 @@ module Crystal
end

def parents
nil
@parents ||= [@program.value] of Type
end

def superclass
@program.value
end

def generic_class
@program.union
end

def metaclass
@metaclass ||= GenericClassInstanceMetaclassType.new(program, self)
end

def generic_nest
14 changes: 14 additions & 0 deletions src/json/from_json.cr
Original file line number Diff line number Diff line change
@@ -187,6 +187,20 @@ def Enum.new(pull : JSON::PullParser)
end
end

{% if Crystal::VERSION == "0.18.0" %}
def Union.new(pull : JSON::PullParser)
string = pull.read_raw
\{% for type in T %}
begin
return \{{type}}.from_json(string)
rescue JSON::ParseException
# Ignore
end
\{% end %}
raise JSON::ParseException.new("couldn't parse #{self} from #{string}", 0, 0)
end
{% end %}

struct Time::Format
def from_json(pull : JSON::PullParser)
string = pull.read_string
1 change: 1 addition & 0 deletions src/prelude.cr
Original file line number Diff line number Diff line change
@@ -69,4 +69,5 @@ require "system"
require "thread"
require "time"
require "tuple"
require "union"
require "value"
20 changes: 20 additions & 0 deletions src/union.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# A union type represents the possibility of a variable or an expression
# having more than one possible type at compile time.
#
# When invoking a method on a union type, the language checks that the
# method exists and can be resolved (typed) for each type in the union.
# For this reason, adding instance methods to `Union` makes no sense and
# has no effect. However, adding class method to `Union` is possible
# and can be useful. One example is parsing `JSON` into one of many
# possible types.
#
# Union is special in that it is a generic type but instantiating it
# might not return a union type:
#
# ```
# Union(Int32 | String) # => (Int32 | String)
# Union(Int32) # => Int32
# Union(Int32, Int32, Int32) # => Int32
# ```
struct Union
end

0 comments on commit a3c5d7a

Please sign in to comment.