Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save JoshCheek/7155818c8a60a6db589018d0bd3e6b0a to your computer and use it in GitHub Desktop.
Save JoshCheek/7155818c8a60a6db589018d0bd3e6b0a to your computer and use it in GitHub Desktop.
A second implementation of the Sorbet API, this time with TracePoint
class Sig
INSTANCE = new
def declared(mod, param_types)
@current = [mod, param_types]
self
end
def returns(return_type)
@current << return_type if @current
self
end
def observed(method)
return unless @current
mod, param_types, return_type = @current
@current = nil
known[mod][method.name] = {receives: param_types, returns: return_type}
declared = param_types.keys.sort
observed = method.parameters.map(&:last).sort
declared == observed or raise TypeError, <<~MSG
Sig doesn't match the method for #{mod}##{method.name}
declared: #{declared.inspect}
observed: #{observed.inspect}
MSG
end
def check_call(binding, obj, method_name)
return unless sig = get_sig(obj, method_name)
sig[:receives].each do |name, type|
value = binding.local_variable_get name
next if value.kind_of? type
raise TypeError, "#{name} was #{value.inspect}, but expected a #{type.inspect}"
end
end
def check_return(obj, method_name, value)
return unless sig = get_sig(obj, method_name)
return if value.kind_of? sig[:returns]
raise TypeError, "#{method_name} returned #{value.inspect}, but expected a #{sig[:returns].inspect}"
end
private
def known
@known ||= Hash.new { |h, mod| h[mod] = {} }
end
def get_sig(obj, method_name)
known.key?(obj.class) && known[obj.class][method_name]
end
end
class Module
def sig(**types)
Sig::INSTANCE.declared(self, types)
end
def method_added(name)
Sig::INSTANCE.observed instance_method(name)
rescue TypeError
$!.set_backtrace caller.drop(1)
raise
end
end
erroring = false # need this b/c, tp will still call return (w/ return_value set to nil)
TracePoint.trace :call, :return do |tp|
if erroring
erroring = false
next
end
begin
case tp.event
when :call
Sig::INSTANCE.check_call tp.binding, tp.self, tp.method_id
when :return
Sig::INSTANCE.check_return tp.self, tp.method_id, tp.return_value
end
rescue TypeError # block rescue added in 2.5, but that's new enough that I won't use it
$!.set_backtrace caller.drop(1)
erroring = true
raise
end
end
###############################################
class BoxedInt
sig(val: Integer).returns(Integer)
def initialize(val)
@val = val
end
sig(num: Integer).returns(BoxedInt)
def + num
BoxedInt.new @val + num
end
sig(num: Integer).returns(BoxedInt)
def - num
BoxedInt.new @val - num
end
sig.returns(String)
def inspect
"BoxedInt.new(#{@val.inspect})"
end
end
n = BoxedInt.new 2
n + 10 # => BoxedInt.new(12)
n - 3 # => BoxedInt.new(-1)
n + 5 - 3 # => BoxedInt.new(4)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment