Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

return_bang implements non-local exits from methods

  • Loading branch information...
commit 815dd4dfb6544153706f01d526e37e05e080696d 0 parents
@drbrain authored
8 .autotest
@@ -0,0 +1,8 @@
+# -*- ruby -*-
+
+require 'autotest/restart'
+
+Autotest.add_hook :initialize do |at|
+ at.testlib = 'minitest/autorun'
+end
+
5 .gitignore
@@ -0,0 +1,5 @@
+*.rbc
+*.swp
+/TAGS
+/doc
+/pkg
11 .travis.yml
@@ -0,0 +1,11 @@
+before_script:
+ - gem install hoe
+ - rake check_extra_deps
+rvm:
+ - 1.8.7
+ - 1.9.2
+ - 1.9.3
+ - ruby-head
+notifications:
+ email:
+ - drbrain@segment7.net
5 History.txt
@@ -0,0 +1,5 @@
+=== 1.0 / 2011-12-20
+
+* Major enhancements
+ * Birthday!
+
8 Manifest.txt
@@ -0,0 +1,8 @@
+.autotest
+History.txt
+Manifest.txt
+README.rdoc
+Rakefile
+lib/return_bang.rb
+lib/return_bang/kernel.rb
+test/test_return_bang.rb
83 README.rdoc
@@ -0,0 +1,83 @@
+= return_bang
+
+home :: https://github.com/drbrain/return_bang
+rdoc :: http://docs.seattlerb.org/return_bang
+bugs :: https://github.com/drbrain/return_bang/issues
+
+== Description
+
+return_bang implements non-local exits from methods. Use return_bang to exit
+back to a processing loop from deeply nested code, or just to confound your
+enemies *and* your friends! What could possibly go wrong?
+
+== Features
+
+* Implements non-local exits for named and stack-based exit points
+* Uses continuations
+* Ignores pesky ensure blocks
+
+== Testimonials
+
+"you'll wind up with your cock in /dev/null somehow"
+
+"Haha! Right! This skips ensure… SO EVIL‼‼"
+
+"This is so evil that 6 def test_… have turned into: 16 tests, 65 assertions,
+18 failures, 7 errors"
+
+== Synopsis
+
+ require 'return_bang/kernel'
+
+ def some_method
+ deeply_nested
+ # never reached
+ end
+
+ def deeply_nested
+ return!
+ end
+
+ return_here do
+ some_method
+ end
+ # resumes here
+
+== Install
+
+ sudo gem install return_bang
+
+== Developers
+
+After checking out the source, run:
+
+ $ rake newb
+
+This task will install any missing dependencies, run the tests/specs,
+and generate the RDoc.
+
+== License
+
+(The MIT License)
+
+Copyright (c) Eric Hodel
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+'Software'), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
17 Rakefile
@@ -0,0 +1,17 @@
+# -*- ruby -*-
+
+require 'rubygems'
+require 'hoe'
+
+Hoe.plugin :minitest
+Hoe.plugin :git
+
+Hoe.spec 'return_bang' do
+ developer 'Eric Hodel', 'drbrain@segment7.net'
+
+ rdoc_locations << 'docs.seattlerb.org:/data/www/docs.seattlerb.org/return_bang/'
+
+ self.readme_file = 'README.rdoc'
+end
+
+# vim: syntax=ruby
142 lib/return_bang.rb
@@ -0,0 +1,142 @@
+begin
+ require 'continuation'
+rescue LoadError
+ # in 1.8 it's built-in
+end
+
+##
+# ReturnBang is allows you to perform non-local exits from your methods. One
+# potential use of this is in a web framework so that a framework-provided
+# utility methods can jump directly back to the request loop.
+#
+# return_here is used to designate where execution should be resumed. Return
+# points may be arbitrarily nested. #return! resumes at the previous resume
+# point, #return_to returns to a named return point.
+#
+# Example:
+#
+# def framework_loop
+# loop do
+# # setup code
+#
+# return_here do
+# user_code
+# end
+#
+# # resume execution here
+# end
+# end
+#
+# def render_error_and_return message
+# # generate error
+#
+# return!
+# end
+#
+# def user_code
+# user_utility_method
+# # these lines never reached
+# # ...
+# end
+#
+# def user_utility_method
+# render_error_and_return "blah" if some_condition
+# # these lines never reached
+# # ...
+# end
+
+module ReturnBang
+
+ VERSION = '1.0'
+
+ ##
+ # Raised when attempting to return! when you haven't registered a location
+ # to return to, or are trying to return to a named point that wasn't
+ # registered.
+
+ class NonLocalJumpError < StandardError
+ end
+
+ def _return_bang_names # :nodoc:
+ Thread.current[:return_bang_names] ||= {}
+ end
+
+ if {}.respond_to? :key then
+ def _return_bang_pop # :nodoc:
+ return_point = _return_bang_stack.pop
+
+ _return_bang_names.delete _return_bang_names.key _return_bang_stack.length
+
+ return_point
+ end
+ else
+ def _return_bang_pop # :nodoc:
+ return_point = _return_bang_stack.pop
+ value = _return_bang_stack.length
+
+ _return_bang_names.delete _return_bang_names.index value
+
+ return_point
+ end
+ end
+
+ def _return_bang_stack # :nodoc:
+ Thread.current[:return_bang_stack] ||= []
+ end
+
+ ##
+ # Returns to the last return point in the stack. If no return points have
+ # been registered a NonLocalJumpError is raised. +value+ is returned at the
+ # registered return point.
+
+ def return! value = nil
+ raise NonLocalJumpError, 'nowhere to return to' if
+ _return_bang_stack.empty?
+
+ _return_bang_pop.call value
+ end
+
+ ##
+ # Registers a return point to jump back to. If a +name+ is given return_to
+ # can jump here.
+
+ def return_here name = nil
+ raise ArgumentError, "#{name} is already registered as a return point" if
+ _return_bang_names.include? name
+
+ value = callcc do |cc|
+ _return_bang_names[name] = _return_bang_stack.length if name
+ _return_bang_stack.push cc
+
+ begin
+ yield
+ ensure
+ _return_bang_pop
+ end
+ end
+
+ # here is where the magic happens
+ unwind_to = Thread.current[:unwind_to]
+
+ return! value if unwind_to and _return_bang_stack.length > unwind_to
+
+ return value
+ end
+
+ ##
+ # Returns to the return point +name+. +value+ is returned at the registered
+ # return point.
+
+ def return_to name, value = nil
+ unwind_to = _return_bang_names.delete name
+
+ raise NonLocalJumpError, "return point :nonexistent was not set" unless
+ unwind_to
+
+ Thread.current[:unwind_to] = unwind_to
+
+ return! value
+ end
+
+end
+
6 lib/return_bang/kernel.rb
@@ -0,0 +1,6 @@
+require 'return_bang'
+
+module Kernel
+ include ReturnBang
+end
+
83 test/test_return_bang.rb
@@ -0,0 +1,83 @@
+require 'minitest/autorun'
+require 'return_bang'
+
+class TestReturnBang < MiniTest::Unit::TestCase
+
+ include ReturnBang
+
+ def setup
+ @after_a = false
+ @after_e = false
+ end
+
+ def teardown
+ assert_empty _return_bang_stack
+ assert_empty _return_bang_names
+ end
+
+ def test_return_bang_no_return_here
+ e = assert_raises NonLocalJumpError do
+ return!
+ end
+
+ assert_equal 'nowhere to return to', e.message
+ end
+
+ def test_return_here
+ result = return_here do a end
+
+ refute @after_a, 'return! did not skip after_a'
+
+ assert_equal 42, result
+ end
+
+ def test_return_here_name
+ result = return_here :name do d end
+
+ refute @after_e, 'return_to did not skip after_e'
+
+ assert_equal 43, result
+ end
+
+ def test_return_here_name_no_return_bang
+ result = return_here :name do c end
+
+ assert_equal 24, result
+ end
+
+ def test_return_here_nest
+ result = return_here do
+ return_here do
+ a
+ end
+ end
+
+ refute @after_a, 'return! did not skip after_a'
+
+ assert_equal 42, result
+ end
+
+ def test_return_here_no_return_bang
+ result = return_here do c end
+
+ assert_equal 24, result
+ end
+
+ def test_return_to_no_return_here
+ e = assert_raises NonLocalJumpError do
+ return_to :nonexistent
+ end
+
+ assert_equal 'return point :nonexistent was not set', e.message
+ end
+
+ def a() b; @after_a = true end
+ def b() return! 42 end
+
+ def c() 24 end
+
+ def d() return_here do e end; @after_e = true end
+ def e() return_to :name, 43 end
+
+end
+
Please sign in to comment.
Something went wrong with that request. Please try again.