mulligan 0.4.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/.travis.yml +6 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +249 -0
- data/Rakefile +12 -0
- data/ext/mulligan/extconf.rb +3 -0
- data/ext/mulligan/mulligan.c +203 -0
- data/lib/mulligan/condition.rb +88 -0
- data/lib/mulligan/kernel.rb +4 -0
- data/lib/mulligan/kernel_common.rb +12 -0
- data/lib/mulligan/kernel_pure.rb +31 -0
- data/lib/mulligan/version.rb +3 -0
- data/lib/mulligan.rb +16 -0
- data/mulligan.gemspec +31 -0
- data/plan.markdown +129 -0
- data/spec/mulligan_spec.rb +391 -0
- data/spec/spec_helper.rb +2 -0
- metadata +125 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 93a82e010fe6e85e2d4258e582448f6eba71aca0
|
4
|
+
data.tar.gz: e93698220f48079b7ed8a34889744fb84de4f657
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 68fa8ce73ecec03457111bd4acc355b260e7fb5faccff1bb1a9195a9b62d2c761dce7406e53f3078cdf0fd94dae20829a98b90e3074017b1661331ac9cacf801
|
7
|
+
data.tar.gz: 172b8934dfdca12793f9931833d10d4a53aac6a831183150926ad591d7e9fbbe0261804692b25cc557a21d2948b36b485a23c07e1a4f03606c1d40a7ab54b319
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 michaeljbishop
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,249 @@
|
|
1
|
+
[![Build Status](https://travis-ci.org/michaeljbishop/mulligan.png?branch=master)](https://travis-ci.org/michaeljbishop/mulligan)
|
2
|
+
# Mulligan
|
3
|
+
|
4
|
+
"In golf,...a stroke that is replayed from the spot of the previous stroke without penalty, due to an errant shot made on the previous stroke. The result is, as the hole is played and scored, as if the first errant shot had never been made." -- [Wikipedia](http://en.wikipedia.org/wiki/Mulligan_(games)#Mulligan_in_golf)
|
5
|
+
|
6
|
+
## Usage
|
7
|
+
|
8
|
+
When rescuing an exception, the Mulligan gem allows you to execute some recovery code, then continue your program as if the exception had never been thrown in the first place
|
9
|
+
|
10
|
+
Here's a very simple contrived example:
|
11
|
+
```ruby
|
12
|
+
1 require 'mulligan'
|
13
|
+
2
|
14
|
+
3 def method_that_raises
|
15
|
+
4 puts "RAISING"
|
16
|
+
5 raise "You can ignore this" do |e|
|
17
|
+
6 e.set_recovery :ignore do
|
18
|
+
7 puts "IGNORING"
|
19
|
+
8 end
|
20
|
+
9 end
|
21
|
+
10 puts "AFTER RAISE"
|
22
|
+
11 end
|
23
|
+
12
|
24
|
+
13 def calling_method
|
25
|
+
14 method_that_raises
|
26
|
+
15 "SUCCESS"
|
27
|
+
16 rescue Exception => e
|
28
|
+
17 puts "RESCUED"
|
29
|
+
18 e.recover :ignore
|
30
|
+
19 puts "HANDLED"
|
31
|
+
20 end
|
32
|
+
```
|
33
|
+
|
34
|
+
Running this at the REPL shows:
|
35
|
+
|
36
|
+
```
|
37
|
+
2.0.0-p353 :009 > calling_method
|
38
|
+
RAISING
|
39
|
+
RESCUED
|
40
|
+
IGNORING
|
41
|
+
AFTER RAISE
|
42
|
+
=> "SUCCESS"
|
43
|
+
```
|
44
|
+
|
45
|
+
### Yeah... wait, shouldn't we see "HANDLED" in that output?!
|
46
|
+
|
47
|
+
Here's what happened in detail:
|
48
|
+
|
49
|
+
1. `#method_that_raises` is called from `#calling_method` (line 14)
|
50
|
+
2. `#method_that_raises` raises an exception *but* before it is raised, a "recovery" can be added to the exception (line 6) in the block passed to `#raise`. (The exception is the parameter 'e' passed to the `#raise` block)
|
51
|
+
3. The exception is then raised (line 5) and rescued (line 16)
|
52
|
+
4. The "recovery" on the exception is called (line 18) which executes the statement in the recovery block (defined on line 7).
|
53
|
+
5. Since the exception has recovered, control taks us back to the point *immediately after the block passed to* `#raise` (line 10), continuing as if `#raise` hadn't been called in the first place.
|
54
|
+
6. The method exits (line 11) and we return to line 15 as if we never saw the exception.
|
55
|
+
7. We exit the method because there's no exception to rescue (line 20). The last value in the function was "SUCCESS" so that is returned.
|
56
|
+
|
57
|
+
### I see what you did there. That's cool, but why should I care?
|
58
|
+
|
59
|
+
You should care because your `rescue` statement is likely to be far from the `raise` in your program's execution and the further away it is, the harder it is to fix the error intelligently. It's even harder if that `raise` comes from a library you are calling.
|
60
|
+
|
61
|
+
Specifying recoveries on the exception allows the lower-level code to offer strategies for fixing the exception without the higher-level code needing to know the internals of those strategies.
|
62
|
+
|
63
|
+
Better yet, it offers the ability to "go back in time" [Groundhog Day](http://en.wikipedia.org/wiki/Groundhog_Day_(film))-style, but this time, your code knows how to play the piano, how to sculpt ice, and how to speak French.
|
64
|
+
|
65
|
+
Find your favorite chair and read these:
|
66
|
+
|
67
|
+
- [Dylan Reference Manual - Conditions - Background](http://opendylan.org/books/drm/Conditions_Background)
|
68
|
+
- [Beyond Exception Handling: Conditions and Restarts](http://www.gigamonkeys.com/book/beyond-exception-handling-conditions-and-restarts.html) (keep in mind the "restarts" are what we are calling "recoveries").
|
69
|
+
|
70
|
+
### This *seems* like a good thing, but what can I do with it?
|
71
|
+
|
72
|
+
Here are some use cases:
|
73
|
+
|
74
|
+
#### Fixing network connection errors
|
75
|
+
|
76
|
+
```ruby
|
77
|
+
def http_post(url, data)
|
78
|
+
... networking code...
|
79
|
+
raise CredentialsExpiredException if response == 401
|
80
|
+
raise ConnectionFailedException if response == 404
|
81
|
+
end
|
82
|
+
|
83
|
+
def post_resource(object)
|
84
|
+
... assemble url and data...
|
85
|
+
http_post(url, data)
|
86
|
+
rescue Exception => e
|
87
|
+
raise(e){|e|e.set_recovery(:retry){}}
|
88
|
+
retry if last_recovery == :retry
|
89
|
+
end
|
90
|
+
|
91
|
+
def save_resources
|
92
|
+
post_resource(user)
|
93
|
+
post_resource(post)
|
94
|
+
post_resource(comment)
|
95
|
+
|
96
|
+
rescue CredentialsExpiredException => e
|
97
|
+
... fix credentials...
|
98
|
+
e.recover :retry
|
99
|
+
rescue ConnectionFailedException => e
|
100
|
+
... switch from wifi to cellular...
|
101
|
+
e.recover :retry
|
102
|
+
end
|
103
|
+
```
|
104
|
+
|
105
|
+
#### Screen Scraping (in Dylan)
|
106
|
+
|
107
|
+
[The maling list post](https://groups.google.com/d/msg/comp.lang.dylan/gszO7d7BAok/zqVbQlNDKzAJ)
|
108
|
+
|
109
|
+
This is going to be inherently messy and for a long-running program like this, potentially painful to restart if the data is found to be incorrect. Much better to just put in some recoveries and choose from them if errors are found.
|
110
|
+
|
111
|
+
#### Handling errors in parsers
|
112
|
+
|
113
|
+
You might write a parser to read XML or a log file format and it might encounter malformed entries. You can make that low-level parser code much more reusable if you specify a few recoveries in the raised exceptions. Higher level code will have many more choices to handle errors.
|
114
|
+
|
115
|
+
BTW, Here's your second chance to read [Beyond Exception Handling: Conditions and Restarts](http://www.gigamonkeys.com/book/beyond-exception-handling-conditions-and-restarts.html). There's a log file parsing example in there.
|
116
|
+
|
117
|
+
#### Ask your friendly Lisp coder. They've been solving these problems for years.
|
118
|
+
|
119
|
+
You've always known he (or she) knew Lisp and now you have something to ask him about.
|
120
|
+
|
121
|
+
## Some Notes About the Ruby Implementation
|
122
|
+
### Methods
|
123
|
+
#### Kernel#raise
|
124
|
+
1. `Kernel#raise` now has a return value! It is the value returned from the recovery block.
|
125
|
+
2. `Kernel#raise` also yields the exception to a block. It does this since it's pretty common to have `Kernel#raise` create an exception for you and without this, you couldn't otherwise attach recoveries.
|
126
|
+
|
127
|
+
```ruby
|
128
|
+
def test
|
129
|
+
# (result is explicit for this example)
|
130
|
+
result = raise "Test" do |e| # yields Exception to the block
|
131
|
+
e.set_recovery(:test_return){"hello"} # recovery block returns a string
|
132
|
+
end
|
133
|
+
result
|
134
|
+
rescue Exception => e
|
135
|
+
e.recover :test_return
|
136
|
+
end
|
137
|
+
```
|
138
|
+
|
139
|
+
returns
|
140
|
+
|
141
|
+
```
|
142
|
+
2.0.0-p353 :012 > test
|
143
|
+
=> "hello"
|
144
|
+
```
|
145
|
+
|
146
|
+
#### You can pass parameters to Exception#recover
|
147
|
+
The first parameter is always the id of the recovery. The rest will be passed directly to the recovery block. Building on the above example:
|
148
|
+
|
149
|
+
```ruby
|
150
|
+
def test
|
151
|
+
# (result is explicit for this example)
|
152
|
+
result = raise "Test" do |e|
|
153
|
+
e.set_recovery(:test_return){|p|p} # pass back whatever is passed in
|
154
|
+
end
|
155
|
+
result
|
156
|
+
rescue Exception => e
|
157
|
+
e.recover :test_return, 5
|
158
|
+
end
|
159
|
+
```
|
160
|
+
|
161
|
+
returns
|
162
|
+
|
163
|
+
```
|
164
|
+
2.0.0-p353 :012 > test
|
165
|
+
=> 5
|
166
|
+
```
|
167
|
+
|
168
|
+
#### Your recovery can attach data to be read by the rescue clause
|
169
|
+
You can pass an options hash to the `rescue` clause that is attached to your recovery. This is handy if you want to attach extra data about the recovery or the circumstances in which it is being raised. Pass them as the second parameter in `Exception#set_recovery`. You can retrieve them with `Exception#recovery_options`. Reserved keys are `:summary`, and `:discussion`
|
170
|
+
```ruby
|
171
|
+
raise "Test" do |e|
|
172
|
+
summary = "Replaces the misparsed entry with one you specify."
|
173
|
+
e.set_recovery(:replace_value, :summary => summary){|p|p}
|
174
|
+
end
|
175
|
+
```
|
176
|
+
|
177
|
+
To demonstrate this, here's a rescue statement. The rescue simply prints out the description which is not really useful as a rescue statement, but it's an example of how a REPL might output to the user a list of recoveries to choose from and the details of what they do.
|
178
|
+
|
179
|
+
```ruby
|
180
|
+
rescue MisparsedEntryException => e
|
181
|
+
$stderr.puts "Choose a recovery:"
|
182
|
+
e.recovery_identifiers.each do |id|
|
183
|
+
$stderr.puts " #{id}: - #{e.recovery_options(id)[:summary]}"
|
184
|
+
end
|
185
|
+
... read choice and execute ...
|
186
|
+
```
|
187
|
+
|
188
|
+
#### Kernel#last_recovery
|
189
|
+
There is a new method: `Kernel#last_recovery` which will return the id of the last recovery invoked for the current thread. So you can do things like this:
|
190
|
+
|
191
|
+
```ruby
|
192
|
+
begin
|
193
|
+
... some code ...
|
194
|
+
rescue Exception => e
|
195
|
+
raise(e){|e|e.set_recovery(:retry){}}
|
196
|
+
retry if last_recovery == :retry
|
197
|
+
end
|
198
|
+
```
|
199
|
+
|
200
|
+
### Mulliigan uses #callcc
|
201
|
+
|
202
|
+
There is more than one way to do this. In the end, I wanted something that would fit very naturally into Ruby's existing Exception mechanism, yet offer as much of the benefits of Lisp's "restart" as I could.
|
203
|
+
|
204
|
+
However, to make that happen, I had to use the `#callcc` method. I'm not completely sure how supported this is across different Ruby implementations. Additionally, I've read that it can be a rather slow method. It's important to note that if an exception is raised but does not have any attached recoveries, `#callcc` will not be called and the standard exception mechanism is used.
|
205
|
+
|
206
|
+
### Ruby Support
|
207
|
+
|
208
|
+
[![Build Status](https://travis-ci.org/michaeljbishop/mulligan.png?branch=master)](https://travis-ci.org/michaeljbishop/mulligan)
|
209
|
+
Mulligan supports MRI versions 1.9.3 -> 2.1.1
|
210
|
+
|
211
|
+
### "Recovery"? What's wrong with "Restart"?
|
212
|
+
|
213
|
+
I had to make a hard choice about naming the thing that allows an exception to be recovered from. "Restart" is the word used in Lisp, but because it is used as a verb and as a noun, it makes it hard to know what a Ruby method named `#restart` would do. Does it return a "restart" or does it execute a restart?
|
214
|
+
|
215
|
+
Changing the name to a noun subtracts that confusion (though arguably adds some back for those coming from languages where the "restart" name is entrenched).
|
216
|
+
|
217
|
+
## Influences
|
218
|
+
- [Beyond Exception Handling: Conditions and Restarts](http://www.gigamonkeys.com/book/beyond-exception-handling-conditions-and-restarts.html) -- (from [Practical Common Lisp](http://www.gigamonkeys.com/book/))
|
219
|
+
- [Things You Didn't Know About Exceptions](http://avdi.org/talks/rockymtnruby-2011/things-you-didnt-know-about-exceptions.html) (Avdi Grimm)
|
220
|
+
- [Restartable Exceptions](http://chneukirchen.org/blog/archive/2005/03/restartable-exceptions.html) (Christian Neukirchen)
|
221
|
+
- [Common Lisp conditions](https://www.ruby-forum.com/topic/179474) (Ruby Forum)
|
222
|
+
|
223
|
+
### Acknowledgements
|
224
|
+
Thanks to [Ryan Angilly](https://twitter.com/angilly) of [Ramen](https://ramen.is) who graciously released the gem name 'mulligan' to be used with this project. If you've got a good software project, consider launching with them.
|
225
|
+
|
226
|
+
## Installation
|
227
|
+
|
228
|
+
Add this line to your application's Gemfile:
|
229
|
+
|
230
|
+
gem 'mulligan'
|
231
|
+
|
232
|
+
And then execute:
|
233
|
+
|
234
|
+
$ bundle
|
235
|
+
|
236
|
+
Or install it yourself as:
|
237
|
+
|
238
|
+
$ gem install mulligan
|
239
|
+
|
240
|
+
## Contributing
|
241
|
+
|
242
|
+
1. Fork it [http://github.com/michaeljbishop/mulligan](http://github.com/michaeljbishop/mulligan)
|
243
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
244
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
245
|
+
5. Create new Pull Request
|
246
|
+
|
247
|
+
## Homepage
|
248
|
+
|
249
|
+
[http://michaeljbishop.github.io/mulligan/](http://michaeljbishop.github.io/mulligan/)
|
data/Rakefile
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require "rspec/core/rake_task"
|
3
|
+
|
4
|
+
require "rake/extensiontask"
|
5
|
+
|
6
|
+
Rake::ExtensionTask.new "mulligan" do |ext|
|
7
|
+
ext.lib_dir = "lib/mulligan"
|
8
|
+
end
|
9
|
+
|
10
|
+
RSpec::Core::RakeTask.new(:spec => :compile)
|
11
|
+
|
12
|
+
task :default => :spec
|
@@ -0,0 +1,203 @@
|
|
1
|
+
#include <ruby.h>
|
2
|
+
|
3
|
+
|
4
|
+
// -----------------------------------------------------------
|
5
|
+
// UTILITIES
|
6
|
+
// -----------------------------------------------------------
|
7
|
+
|
8
|
+
#define ARRAY_SIZE(var) (sizeof(var)/sizeof(var[0]))
|
9
|
+
|
10
|
+
// -----------------------------------------------------------
|
11
|
+
// DECLARATIONS
|
12
|
+
// -----------------------------------------------------------
|
13
|
+
|
14
|
+
static VALUE rb_mulligan_raise(int argc, VALUE *argv, VALUE self);
|
15
|
+
static VALUE callcc_block(VALUE c, VALUE in_context, int argc, VALUE argv[]);
|
16
|
+
static VALUE __set_continuation__block(VALUE unused, VALUE in_context, int argc, VALUE argv[]);
|
17
|
+
|
18
|
+
static ID id_recoveries = 0;
|
19
|
+
static ID id_empty_ = 0;
|
20
|
+
static ID id_send = 0;
|
21
|
+
static ID id_callcc = 0;
|
22
|
+
static ID id_call = 0;
|
23
|
+
static ID id___set_continuation__ = 0;
|
24
|
+
static ID id_puts = 0;
|
25
|
+
|
26
|
+
// ===========================================================
|
27
|
+
// MAIN ENTRY POINT
|
28
|
+
// ===========================================================
|
29
|
+
|
30
|
+
void Init_mulligan(void)
|
31
|
+
{
|
32
|
+
VALUE mMulligan = rb_define_module("Mulligan");
|
33
|
+
VALUE mKernel = rb_define_module_under(mMulligan, "Kernel");
|
34
|
+
rb_define_method(mKernel, "raise", rb_mulligan_raise, -1);
|
35
|
+
rb_define_method(mKernel, "fail", rb_mulligan_raise, -1);
|
36
|
+
|
37
|
+
#if RUBY_API_VERSION_MAJOR < 2
|
38
|
+
// completely replaces Ruby's version of raise.
|
39
|
+
// In Ruby 2 we prepend (but don't call super)
|
40
|
+
rb_define_global_function("raise", rb_mulligan_raise, -1);
|
41
|
+
rb_define_global_function("fail", rb_mulligan_raise, -1);
|
42
|
+
#endif
|
43
|
+
|
44
|
+
rb_require("continuation");
|
45
|
+
id_recoveries = rb_intern("recoveries");
|
46
|
+
id_empty_ = rb_intern("empty?");
|
47
|
+
id_send = rb_intern("send");
|
48
|
+
id_callcc = rb_intern("callcc");
|
49
|
+
id_call = rb_intern("call");
|
50
|
+
id___set_continuation__ = rb_intern("__set_continuation__");
|
51
|
+
id_puts = rb_intern("puts");
|
52
|
+
}
|
53
|
+
|
54
|
+
|
55
|
+
/* -----------------------------------------------------------
|
56
|
+
// Here is the template ruby code that we are approximating
|
57
|
+
// in C. Native-only calls are surround by <<>>
|
58
|
+
// -----------------------------------------------------------
|
59
|
+
def raise(*args)
|
60
|
+
e = <<make_exception(*args)>>
|
61
|
+
yield e if block_given?
|
62
|
+
|
63
|
+
# only use callcc if there are restarts otherwise re-raise it
|
64
|
+
<<rb_exc_raise(e)>> if e.send(:recoveries).empty?
|
65
|
+
should_raise = true
|
66
|
+
result = callcc do |c|
|
67
|
+
e.send(:__set_continuation__) do |*args,&block|
|
68
|
+
should_raise = false
|
69
|
+
c.call(*args,&block)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
<<rb_exc_raise(e)>> if should_raise
|
73
|
+
result
|
74
|
+
end
|
75
|
+
// -----------------------------------------------------------
|
76
|
+
// There is one big difference which is at all costs, we only
|
77
|
+
// call `rb_exc_raise` from the current frame. This is important
|
78
|
+
// because we want the stack-trace and current stack-frame to
|
79
|
+
// be identical to what they were if we just called the standard
|
80
|
+
// #raise method. If we call `rb_exc_raise` within a block or
|
81
|
+
// call super, we will lose that stack-frame context.
|
82
|
+
// For this reason, you'll see that awkward `should_raise` variable
|
83
|
+
// in the source above.
|
84
|
+
// -----------------------------------------------------------*/
|
85
|
+
|
86
|
+
|
87
|
+
static VALUE
|
88
|
+
rb_mulligan_raise(int argc, VALUE *argv, VALUE self)
|
89
|
+
{
|
90
|
+
// -----------------------------------------------------------
|
91
|
+
// C90-compliance asks us to declare all variables at the top
|
92
|
+
// -----------------------------------------------------------
|
93
|
+
VALUE recoveries = Qnil;
|
94
|
+
VALUE should_raise_ary = Qnil;
|
95
|
+
VALUE contextVars[3];
|
96
|
+
VALUE context = Qnil;
|
97
|
+
VALUE result = Qnil;
|
98
|
+
VALUE is_empty = Qnil;
|
99
|
+
|
100
|
+
// -----------------------------------------------------------
|
101
|
+
// Get a reference to the Exception object
|
102
|
+
// -----------------------------------------------------------
|
103
|
+
VALUE e = rb_make_exception(argc, argv);
|
104
|
+
|
105
|
+
if (NIL_P(e)) {
|
106
|
+
// get whatever is in $!. I'm sure this is slow
|
107
|
+
e = rb_eval_string("$!");
|
108
|
+
// what I'd like to use reallu like to use
|
109
|
+
// e = rb_rubylevel_errinfo(); // internal ruby call, yet necessary
|
110
|
+
}
|
111
|
+
|
112
|
+
if (NIL_P(e)) {
|
113
|
+
e = rb_exc_new(rb_eRuntimeError, 0, 0);
|
114
|
+
}
|
115
|
+
|
116
|
+
// -----------------------------------------------------------
|
117
|
+
// With the Exception in place, yield to the block
|
118
|
+
// -----------------------------------------------------------
|
119
|
+
if (rb_block_given_p())
|
120
|
+
rb_yield(e);
|
121
|
+
|
122
|
+
// -----------------------------------------------------------
|
123
|
+
// If there are no recoveries, just throw it without a callcc
|
124
|
+
// -----------------------------------------------------------
|
125
|
+
recoveries = rb_funcall(e, id_send, 1, ID2SYM(id_recoveries));
|
126
|
+
is_empty = rb_funcall(recoveries, id_empty_, 0);
|
127
|
+
if (RTEST(is_empty))
|
128
|
+
goto raise;
|
129
|
+
|
130
|
+
// -----------------------------------------------------------
|
131
|
+
// There are recoveries so we first capture the continuation
|
132
|
+
// using callcc
|
133
|
+
// -----------------------------------------------------------
|
134
|
+
|
135
|
+
// A note about should_raise_ary:
|
136
|
+
// 2 callbacks from now (in `__set_continuation__block`) we are
|
137
|
+
// going to want to change the value of this from true to false. Since Qtrue
|
138
|
+
// is not a pointer, we are going to make a ghetto-pointer by putting it in
|
139
|
+
// an array of one. That array will be passed to the callback where it will be
|
140
|
+
// modified and we can read it still in this context after all the callbacks
|
141
|
+
// are complete.
|
142
|
+
should_raise_ary = rb_ary_to_ary(Qtrue);
|
143
|
+
contextVars[0] = self;
|
144
|
+
contextVars[1] = should_raise_ary;
|
145
|
+
contextVars[2] = e;
|
146
|
+
context = rb_ary_new4(ARRAY_SIZE(contextVars), contextVars);
|
147
|
+
|
148
|
+
result = rb_block_call(
|
149
|
+
self,
|
150
|
+
id_callcc,
|
151
|
+
0, // argc
|
152
|
+
0, // argv
|
153
|
+
RUBY_METHOD_FUNC(callcc_block),
|
154
|
+
context
|
155
|
+
);
|
156
|
+
|
157
|
+
// Here we read our "pointer"
|
158
|
+
if (!RTEST(rb_ary_entry(should_raise_ary, 0)))
|
159
|
+
return result;
|
160
|
+
|
161
|
+
raise:
|
162
|
+
rb_exc_raise(e);
|
163
|
+
// UNREACHABLE;
|
164
|
+
}
|
165
|
+
|
166
|
+
static VALUE
|
167
|
+
callcc_block(VALUE c, VALUE in_context, int argc, VALUE argv[])
|
168
|
+
{
|
169
|
+
VALUE self = rb_ary_entry(in_context, 0);
|
170
|
+
VALUE should_raise_ary = rb_ary_entry(in_context, 1);
|
171
|
+
VALUE e = rb_ary_entry(in_context, 2);
|
172
|
+
|
173
|
+
VALUE contextVars[] = {self, should_raise_ary, c};
|
174
|
+
VALUE context = rb_ary_new4(ARRAY_SIZE(contextVars), contextVars);
|
175
|
+
|
176
|
+
return rb_block_call(
|
177
|
+
e,
|
178
|
+
id___set_continuation__,
|
179
|
+
0, // argc
|
180
|
+
0, // argv
|
181
|
+
RUBY_METHOD_FUNC(__set_continuation__block),
|
182
|
+
context // data2
|
183
|
+
);
|
184
|
+
}
|
185
|
+
|
186
|
+
static VALUE
|
187
|
+
__set_continuation__block(VALUE unused, VALUE in_context, int argc, VALUE argv[])
|
188
|
+
{
|
189
|
+
// VALUE self = rb_ary_entry(in_context, 0);
|
190
|
+
VALUE should_raise_ary = rb_ary_entry(in_context, 1);
|
191
|
+
VALUE c = rb_ary_entry(in_context, 2);
|
192
|
+
VALUE proc = rb_block_proc();
|
193
|
+
|
194
|
+
// Here we set our `should_raise` pointer back to false
|
195
|
+
rb_ary_store(should_raise_ary, 0, Qfalse);
|
196
|
+
|
197
|
+
return rb_funcall_with_block(c, id_call, argc, argv, proc);
|
198
|
+
}
|
199
|
+
|
200
|
+
|
201
|
+
|
202
|
+
|
203
|
+
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module Mulligan
|
2
|
+
|
3
|
+
# An exception that is thrown when invoking a non-existent recovery.
|
4
|
+
class ControlException < Exception ; end
|
5
|
+
|
6
|
+
module Condition
|
7
|
+
|
8
|
+
# Creates or replaces a recovery strategy.
|
9
|
+
#
|
10
|
+
# @param [String or Symbol] id the key to reference the recovery later
|
11
|
+
# @param [Hash] options specifies a token for this recovery that can later be
|
12
|
+
# retrieved. Can only be set once. See {#recovery_options}
|
13
|
+
# Reserved Keys are as follows:
|
14
|
+
# :data - Use this as a parameter to send a piece of data to rescuers to use
|
15
|
+
# as they determine their strategy for recovering.
|
16
|
+
# :summary - A short, one-line description of this recovery
|
17
|
+
# :discussion - The complete documentation of this recovery. Please include a
|
18
|
+
# description of the behavior, the return parameter, and any parameters
|
19
|
+
# the recovery can take
|
20
|
+
def set_recovery(id, options={}, &block)
|
21
|
+
return if block.nil?
|
22
|
+
recoveries[id.to_sym] = options.merge(:block => block)
|
23
|
+
nil
|
24
|
+
end
|
25
|
+
|
26
|
+
# Checks for the presence of a recovery
|
27
|
+
#
|
28
|
+
# @param [String or Symbol] id the key for the recovery
|
29
|
+
# @return [Boolean] whether or not a recovery exists for this id
|
30
|
+
def has_recovery?(id)
|
31
|
+
recoveries.has_key?(id.to_sym)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Retrieves the options specified when a recovery was made
|
35
|
+
#
|
36
|
+
# @param [String or Symbol] id the key for the recovery
|
37
|
+
# @return [Hash] The options set on this recovery
|
38
|
+
def recovery_options(id)
|
39
|
+
return nil unless has_recovery?(id.to_sym)
|
40
|
+
recoveries[id.to_sym].dup.reject{|k,v| [:block, :continuation].include? k}
|
41
|
+
end
|
42
|
+
|
43
|
+
# Retrieves all the identifiers for available recoveries
|
44
|
+
#
|
45
|
+
# @return [Enumerable] The identifiers for all the recoveries
|
46
|
+
def recovery_identifiers
|
47
|
+
recoveries.keys
|
48
|
+
end
|
49
|
+
|
50
|
+
# Executes the recovery.
|
51
|
+
# This actually places the stack back to just after the `#raise` that brought
|
52
|
+
# us to this `rescue` clause. Then the recovery block is executed and the program
|
53
|
+
# continues on.
|
54
|
+
#
|
55
|
+
# @param [String or Symbol] id the key for the recovery
|
56
|
+
# @param params any additional parameters you want to pass to the recovery block
|
57
|
+
# @return This doesn't actually matter because you can't retrieve it
|
58
|
+
def recover(id, *params)
|
59
|
+
Thread.current[:__last_recovery__] = nil
|
60
|
+
raise ControlException unless has_recovery?(id.to_sym)
|
61
|
+
data = recoveries[id.to_sym]
|
62
|
+
if data[:continuation].nil?
|
63
|
+
$stderr.puts "Cannot invoke restart #{id}. Must first raise this exception: '#{self.inspect}'"
|
64
|
+
return
|
65
|
+
end
|
66
|
+
Thread.current[:__last_recovery__] = id
|
67
|
+
data[:continuation].call(*data[:block].call(*params))
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def recoveries
|
73
|
+
@recoveries ||= {}
|
74
|
+
end
|
75
|
+
|
76
|
+
def __set_continuation__
|
77
|
+
continuation = Proc.new
|
78
|
+
# the the continuation for any recoveries that are not yet assigned one
|
79
|
+
# It's important not to overwrite the existing continuations because a recovery
|
80
|
+
# should return to the place it was raised, always.
|
81
|
+
recoveries.each do |key, r|
|
82
|
+
next if r.has_key? :continuation
|
83
|
+
r[:continuation] = continuation
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'continuation'
|
2
|
+
|
3
|
+
module Mulligan
|
4
|
+
module Kernel
|
5
|
+
|
6
|
+
# Returns the identifier for the last recovery invoked in this thread.
|
7
|
+
# @return [Symbol] The identifier of the last recovery invoked in this thread.
|
8
|
+
def last_recovery
|
9
|
+
Thread.current[:__last_recovery__]
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require_relative 'kernel_common'
|
2
|
+
|
3
|
+
module Mulligan
|
4
|
+
module Kernel
|
5
|
+
|
6
|
+
# Raises an Exception.
|
7
|
+
# The Exception that is either passed in or generated is yielded to the block
|
8
|
+
# where you can specify recoveries on it.
|
9
|
+
#
|
10
|
+
# @param args the same args you would pass to the normal Kernel#raise
|
11
|
+
# @yield [e] Passes the exception-to-be-raised to the block.
|
12
|
+
# @return The value returned from the invoked recovery block.
|
13
|
+
def raise(*args)
|
14
|
+
super
|
15
|
+
rescue Exception => e
|
16
|
+
yield e if block_given?
|
17
|
+
|
18
|
+
# only use callcc if there are restarts otherwise re-raise it
|
19
|
+
super(e) if e.send(:recoveries).empty?
|
20
|
+
should_raise = true
|
21
|
+
result = callcc do |c|
|
22
|
+
e.send(:__set_continuation__) do |*args,&block|
|
23
|
+
should_raise = false
|
24
|
+
c.call(*args,&block)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
super(e) if should_raise
|
28
|
+
result
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|