crystal-lang / crystal

The Crystal Programming Language
https://crystal-lang.org
Apache License 2.0
19.36k stars 1.62k forks source link

Nilable `Proc` types inside libs #14690

Open HertzDevil opened 3 months ago

HertzDevil commented 3 months ago

It is possible to use nilable Procs as parameter types of lib funs or field types of lib structs:

https://github.com/crystal-lang/crystal/blob/2d71e3546f700b5ade54d10d455da69f78adb65f/src/compiler/crystal/semantic/lib.cr#L284-L285

But actually assigning anything to them doesn't seem to work:

lib Foo
  struct Bar
    x : (-> Int32)?
  end

  fun foo(x : (-> Int32)?)
end

Foo.foo(-> { 1 }) # Error: argument 'x' of 'Foo#foo' must be (Proc(Int32) | Nil), not Proc(Int32)
Foo.foo(nil)      # Error: argument 'x' of 'Foo#foo' must be (Proc(Int32) | Nil), not Nil

bar = Foo::Bar.new
bar.x = -> { 1 } # Error: field 'x' of struct Foo::Bar has type (Proc(Int32) | Nil), not Proc(Int32)
bar.x = nil      # Error: field 'x' of struct Foo::Bar has type (Proc(Int32) | Nil), not Nil

It seems nilable Proc types are allowed in the first place because their binary representations are identical to non-nilable Procs (function pointer + closure data pointer). The workaround, however, is to simply drop the Nil types, because extern Procs are equivalent to single pointers and therefore implicitly nilable:

lib Foo
  struct Bar
    x : -> Int32
  end

  fun foo(x : -> Int32)
end

Foo.foo(-> { 1 }) # okay
Foo.foo(nil)      # okay

bar = Foo::Bar.new
bar.x = -> { 1 } # okay
bar.x = nil      # okay

So maybe we don't need to support nilable Procs here? Returning from a top-level fun seems to be an exception though:

# Error: expected fun to return Proc(Int32) but it returned (Proc(Int32) | Nil)
fun foo : -> Int32
  nil
end

# no semantic error, but fails due to #14691
fun bar : (-> Int32)?
  nil
end