Equivalent to Ruby's throw/catch (lightweight goto's)

I guess we could almost implement this entirely in Crystal. For example:

require "benchmark"

class ThrowError(T) < Exception
  getter value : T

  def initialize(@value : T)
  end
end

def catch(value : T) forall T
  begin
    yield
  rescue error : ThrowError(T)
    if error.value == value
      return
    else
      raise error
    end
  end
end

def throw(value)
  raise ThrowError.new(value)
end

# This is similar to `raise(Exception)` except that it doesn't compute a callstack.
def raise(exception : ThrowError(T)) : NoReturn forall T
  unwind_ex = Pointer(LibUnwind::Exception).malloc
  unwind_ex.value.exception_class = LibC::SizeT.zero
  unwind_ex.value.exception_cleanup = LibC::SizeT.zero
  unwind_ex.value.exception_object = exception.as(Void*)
  unwind_ex.value.exception_type_id = exception.crystal_type_id
  __crystal_raise(unwind_ex)
end

Benchmark.ips do |x|
  x.report("raise/rescue") do
    begin
      raise "OH NO!"
    rescue
    end
  end

  x.report("throw/catch") do
    catch(:foo) do
      throw :foo
    end
  end
end

Results are:

raise/rescue 680.32k (  1.47µs) (± 2.33%)  256B/op   1.66× slower
 throw/catch   1.13M (887.09ns) (± 3.23%)  128B/op        fastest

The only thing else we’d need to do is to avoid rescuing ThrowError when you do rescue e or rescue e : Exception. Maybe try to allocate even less memory, but I’m not sure it’s possible.

But then, I’m not sure this is all worth it. If it’s only going to be twice as fast, maybe it doesn’t make much difference.

2 Likes