wouterken/crystalruby

Interaction with Sorbet signatures?

Closed this issue · 3 comments

How does this gem interact with Sorbet's sig definitions?

Not well I'm afraid!

While Sorbet is satisfied during the static check, it will complain within the runtime.
You'll see an error like:

You're trying to replace `add` on `SorbetAndCrystal`, but that method exists in a prepended module (#<Module:0x000000011e9ceba8>), which we don't currently support

The current version of the library relies on one or two prepended modules for wrapping the FFI call to the Crystal library.

  • The first is to perform argument transforms before invoking FFI
  • The second (optional), is if the block form of crystalize is used, to allow you to essentially implement an around hook in Ruby for any crytalized methods.

See the sorbet code here where it complains that this is unsupported (for good reason).
https://github.com/sorbet/sorbet/blob/443cd113c1162d9e3e45d40a5e02c420671a7c2a/gems/sorbet-runtime/lib/types/private/class_utils.rb#L115

That particular area of the Sorbet codebase is interesting as it has similar goals, but with a different technique.
While crystalruby wraps methods to provide this pre and post functionality, sorbet redefines them in place. It's likely the same technique could be applied in crystalruby. I plan to come back to this.

Thanks for the detailed explanation!

No worries. I know there's a large community of Sorbet users out there, and use of this library will be an easier sell if it is fully compatible.
I believe there's some relatively low hanging fruit to be reaped here, and I plan to explore this space more.

For now, anybody that would like to enjoy some combination of Sorbet and crystalruby, it is possible with a few caveats:

  • Sorbet will only typecheck crystalized instance methods (related to crystalized methods being equivalent to Ruby module_functions available at both module and instance level, see #3)
  • Runtime checks must be disabled using T::Sig::WithoutRuntime.sig.
    E.g.
# typed: true

require 'crystalruby'
require 'sorbet-runtime'

module AdderMixin
  extend T::Sig

  T::Sig::WithoutRuntime.sig { params(a: Integer, b: Integer).returns(Integer) }
  crystalize [a: :int, b: :int] => :int
  def add(a, b)
    a + b * 3
  end
end

class Adder
  include AdderMixin
end

puts Adder.new.add(1, 'two')
bundle exec srb tc

Outputs:

main.rb:20: Expected Integer but found String("two") for argument b https://srb.help/7002
    20 |puts Adder.new.add(1, 'two')
                              ^^^^^
  Expected Integer for argument b of method AdderMixin#add:
    main.rb:9:
     9 |  T::Sig::WithoutRuntime.sig { params(a: Integer, b: Integer).returns(Integer) }
                                                          ^
  Got String("two") originating from:
    main.rb:20:
    20 |puts Adder.new.add(1, 'two')
                              ^^^^^
Errors: 1

During runtime, you'll get a TypeError only when the method is called:

.../gems/crystalruby-0.1.12/lib/crystalruby.rb:99:in `add': no implicit conversion of String into Integer (TypeError)