tco_method 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +0 -0
- data/.travis.yml +0 -0
- data/Gemfile +0 -0
- data/Guardfile +0 -0
- data/LICENSE +0 -0
- data/README.md +102 -27
- data/Rakefile +0 -0
- data/lib/tco_method.rb +33 -43
- data/lib/tco_method/ambiguous_source_error.rb +42 -0
- data/lib/tco_method/block_extractor.rb +107 -0
- data/lib/tco_method/block_with_tco.rb +22 -0
- data/lib/tco_method/method_info.rb +0 -0
- data/lib/tco_method/method_reevaluator.rb +45 -0
- data/lib/tco_method/mixin.rb +20 -4
- data/lib/tco_method/version.rb +1 -1
- data/tco_method.gemspec +0 -0
- data/test/test_helper.rb +2 -2
- data/test/test_helpers/assertions.rb +0 -0
- data/test/test_helpers/fibbers.rb +37 -0
- data/test/unit/block_extractor_test.rb +150 -0
- data/test/unit/block_with_tco_test.rb +53 -0
- data/test/unit/method_info_test.rb +0 -0
- data/test/unit/method_reevaluator_test.rb +112 -0
- data/test/unit/mixin_test.rb +66 -46
- data/test/unit/tco_method_test.rb +42 -156
- metadata +15 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 15a6ac46846b32e0e851d33bc6285481da316a59
|
4
|
+
data.tar.gz: b3e6b3fb09936ff95b3342e009c6216edbf756d6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7651b179452b164aabba4ef9b7435f06c81f84f50496555a7eb2e05ed0fefcc8e47144c18902fb57f96cc331c5e1c52ea6dba8c8c3f4de30c6c689bc89a94737
|
7
|
+
data.tar.gz: 876eadb63edf03f8ea5b05f2e53a5d69dc5d9b9ec12a86de083a78c1b2e137f149db166be7e3d61c065fb467e6fb4258bb60eabd46b7020061b090573dc6fcba
|
data/.gitignore
CHANGED
File without changes
|
data/.travis.yml
CHANGED
File without changes
|
data/Gemfile
CHANGED
File without changes
|
data/Guardfile
CHANGED
File without changes
|
data/LICENSE
CHANGED
File without changes
|
data/README.md
CHANGED
@@ -6,11 +6,29 @@
|
|
6
6
|
[](https://codeclimate.com/github/tdg5/tco_method)
|
7
7
|
[](https://gemnasium.com/tdg5/tco_method)
|
8
8
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
enabled.
|
9
|
+
The `tco_method` gem provides a number of different APIs to facilitate
|
10
|
+
evaluating code with tail call optimization enabled in MRI Ruby.
|
11
|
+
|
12
|
+
The `TCOMethod.with_tco` method is perhaps the simplest means of evaluating code
|
13
|
+
with tail call optimization enabled. `TCOMethod.with_tco` takes a block and
|
14
|
+
compiles all code **in that block** with tail call optimization enabled.
|
15
|
+
|
16
|
+
The `TCOMethod::Mixin` module extends Classes and Modules with helper methods
|
17
|
+
(kind of like method annotations) to facilitate compiling some types of methods
|
18
|
+
with tail call optimization enabled.
|
19
|
+
|
20
|
+
The `TCOMethod.tco_eval` method provides a direct means to evaluate code strings
|
21
|
+
with tail call optimization enabled. This API is the most cumbersome, but it can
|
22
|
+
be useful for loading full files with tail call optimization enabled (see
|
23
|
+
examples below). It is also the foundation of all of the other `TCOMethod` APIs.
|
24
|
+
|
25
|
+
Be warned, there are a few gotchas. For example, even when using one of the APIs
|
26
|
+
provided by the `tco_method` gem, `require`, `load`, and `Kernel#eval` still
|
27
|
+
won't evaluate code with tail call optimization enabled without changing the
|
28
|
+
`RubyVM` settings globally. More on the various limitations of the `tco_method`
|
29
|
+
gem are outlined in the docs in the
|
30
|
+
[Gotchas](http://www.rubydoc.info/gems/tco_method/file/README.md#Gotchas)
|
31
|
+
section.
|
14
32
|
|
15
33
|
## Installation
|
16
34
|
|
@@ -41,8 +59,38 @@ library:
|
|
41
59
|
require "tco_method"
|
42
60
|
```
|
43
61
|
|
44
|
-
|
45
|
-
|
62
|
+
### `TCOMethod.with_tco`
|
63
|
+
|
64
|
+
The fastest road to tail call optimized glory is the
|
65
|
+
[`TCOMethod.with_tco`](http://www.rubydoc.info/gems/tco_method/TCOMethod#with_tco-class_method)
|
66
|
+
method. Using
|
67
|
+
[`TCOMethod.with_tco`](http://www.rubydoc.info/gems/tco_method/TCOMethod#with_tco-class_method)
|
68
|
+
you can evaluate a block of code with tail call optimization enabled liked so:
|
69
|
+
|
70
|
+
```ruby
|
71
|
+
TCOMethod.with_tco do
|
72
|
+
class MyClass
|
73
|
+
def factorial(n, acc = 1)
|
74
|
+
n <= 1 ? acc : factorial(n - 1, n * acc)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
puts MyClass.new.factorial(10_000).to_s.length
|
80
|
+
# => 35660
|
81
|
+
```
|
82
|
+
|
83
|
+
It's worth noting that in the example above the actual optimized tail call
|
84
|
+
occurs outside of the `TCOMethod.with_tco` block. `TCOMethod.with_tco` is used
|
85
|
+
to compile code in such a way that tail call optimization is enabled. Once
|
86
|
+
compiled, the tail call optimized code can be invoked from anywhere in the
|
87
|
+
program.
|
88
|
+
|
89
|
+
### `TCOMethod::Mixin`
|
90
|
+
|
91
|
+
Alternatively, you can extend a Class or Module with the
|
92
|
+
[`TCOMethod::Mixin`](http://www.rubydoc.info/gems/tco_method/TCOMethod/Mixin)
|
93
|
+
and let the TCO fun begin using helpers that act like method annotations.
|
46
94
|
|
47
95
|
To redefine an instance method with tail call optimization enabled, use
|
48
96
|
[`tco_method`](http://www.rubydoc.info/gems/tco_method/TCOMethod/Mixin:tco_method):
|
@@ -79,9 +127,16 @@ puts MyFibonacci.fibonacci(10_000).to_s.length
|
|
79
127
|
# => 2090
|
80
128
|
```
|
81
129
|
|
82
|
-
|
130
|
+
### `TCOMethod.tco_eval`
|
131
|
+
|
132
|
+
Finally, depending on your needs (and your love for stringified code blocks),
|
133
|
+
you can also use
|
83
134
|
[`TCOMethod.tco_eval`](http://www.rubydoc.info/gems/tco_method/TCOMethod/Mixin:tco_eval)
|
84
|
-
directly
|
135
|
+
directly.
|
136
|
+
[`TCOMethod.tco_eval`](http://www.rubydoc.info/gems/tco_method/TCOMethod/Mixin:tco_eval)
|
137
|
+
can be useful in situations where the `method_source` gem is unable to determine
|
138
|
+
the source of a particular block or for loading entire files with tail call
|
139
|
+
optimization enabled.
|
85
140
|
|
86
141
|
```ruby
|
87
142
|
TCOMethod.tco_eval(<<-CODE)
|
@@ -96,11 +151,12 @@ MyClass.new.factorial(10_000).to_s.length
|
|
96
151
|
# => 35660
|
97
152
|
```
|
98
153
|
|
99
|
-
You can kind of get around
|
100
|
-
compile with tail call optimization
|
101
|
-
that it goes around the standard Ruby
|
102
|
-
|
103
|
-
the other script acting as a
|
154
|
+
You can kind of get around the need for stringified code blocks by reading the
|
155
|
+
code you want to compile with tail call optimization dynamically at runtime, but
|
156
|
+
this approach also has downsides in that it goes around the standard Ruby
|
157
|
+
`require`/`load` process. For example, consider the `Fibonacci` example broken across
|
158
|
+
two scripts, one script serving as a loader and the other script acting as a
|
159
|
+
more standard library:
|
104
160
|
|
105
161
|
```ruby
|
106
162
|
# loader.rb
|
@@ -122,9 +178,9 @@ module MyFibonacci
|
|
122
178
|
end
|
123
179
|
```
|
124
180
|
|
125
|
-
If you really want to get crazy, you
|
126
|
-
in the Module class
|
127
|
-
VIM plugin author extraordinaire Tim Pope, "I don't like to get crazy." Consider
|
181
|
+
If you really want to get crazy, you can include the `TCOMethod::Mixin` module
|
182
|
+
in the `Module` class to add these behaviors to all Modules and Classes. To quote
|
183
|
+
VIM plugin author extraordinaire, Tim Pope, "I don't like to get crazy." Consider
|
128
184
|
yourself warned.
|
129
185
|
|
130
186
|
```ruby
|
@@ -144,17 +200,36 @@ puts MyFibonacci.fibonacci(10_000).to_s.length
|
|
144
200
|
```
|
145
201
|
|
146
202
|
## Gotchas
|
147
|
-
|
148
|
-
|
203
|
+
**Quirks with the `method_source` gem**:
|
204
|
+
- Annotations and `TCOMethod.with_tco` use the
|
205
|
+
[`method_source` gem](https://github.com/banister/method_source) to retrieve
|
206
|
+
the method source to evaluate. As a result, class annotations and
|
207
|
+
`TCOMethod.with_tco` can act strangely when used in more dynamic contexts like
|
208
|
+
`irb` or `pry`. Additionally, if the code to be evaluated is formatted in
|
209
|
+
unconventional ways, it can make it difficult for `method_source` and/or
|
210
|
+
`tco_method` to determine the unambiguous source of the method or code block.
|
211
|
+
Most of these ambiguities can be solved by following standard Ruby formating
|
212
|
+
conventions.
|
213
|
+
|
214
|
+
**Quirks with `TCOMethod.with_tco`**:
|
215
|
+
- Because the source code of the specified block is determined using the
|
216
|
+
`method_source` gem, the given block will be evaluated with a binding
|
217
|
+
different from the one it was defined in. Attempts have been made to get around
|
218
|
+
this, but so far, no dice. Seems like a job for a C extension.
|
219
|
+
- `require`, `load`, and `eval` will still load code **without tail call
|
220
|
+
optimization enabled** even when called from within a block given to
|
221
|
+
`TCOMethod.with_tco`. Each of these methods compiles code using the primary
|
222
|
+
`RubyVM::InstructionSequence` object which honors the configuration specified
|
223
|
+
by `RubyVM::InstructionSequence.compile_option`.
|
224
|
+
|
225
|
+
**Quirks with Module and Class annotations**:
|
149
226
|
- Annotations only work with methods defined using the `def` keyword.
|
150
|
-
- Annotations use the [`method_source` gem](https://github.com/banister/method_source)
|
151
|
-
to retrieve the method source to reevaluate. As a result, class annotations
|
152
|
-
can act strangely when used in more dynamic contexts like `irb` or `pry`.
|
153
227
|
- Annotations reopen the Module or Class by name to redefine the given method.
|
154
228
|
This process will fail for dynamic Modules and Classes that aren't assigned to
|
155
|
-
constants and, ergo, don't have names.
|
229
|
+
constants and, ergo, don't have names that can be used for lookup.
|
156
230
|
|
157
|
-
|
231
|
+
There are almost certainly more gotchas, so check back for more in the future if
|
232
|
+
you run into weirdness while using this gem. Issues are welcome.
|
158
233
|
|
159
234
|
## Contributing
|
160
235
|
|
@@ -169,10 +244,10 @@ I'm sure there are more and I will document them here as I come across them.
|
|
169
244
|
- Class annotations are based on [Nithin Bekal's blog post *Tail Call
|
170
245
|
Optimization in Ruby*](http://nithinbekal.com/posts/ruby-tco/) which follows
|
171
246
|
his efforts to create a method decorator to recompile methods with tail call
|
172
|
-
optimization
|
247
|
+
optimization.
|
173
248
|
- For more background on how tail call optimization is implemented in MRI Ruby,
|
174
|
-
see [Danny Guinther's *Tail Call Optimization in Ruby: Deep Dive*](http://blog.tdg5.com/tail-call-optimization-ruby-deep-dive/)
|
249
|
+
see [Danny Guinther's *Tail Call Optimization in Ruby: Deep Dive*](http://blog.tdg5.com/tail-call-optimization-ruby-deep-dive/).
|
175
250
|
- For those on flavors of Ruby other than MRI, check out [Magnus Holm's *Tailin'
|
176
251
|
Ruby*](http://timelessrepo.com/tailin-ruby) for some insight into how else
|
177
252
|
tail call optimization (or at least tail call optimization like behavior) can
|
178
|
-
be achieved in Ruby
|
253
|
+
be achieved in Ruby.
|
data/Rakefile
CHANGED
File without changes
|
data/lib/tco_method.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
require "method_source"
|
2
2
|
require "tco_method/version"
|
3
|
-
require "tco_method/method_info"
|
4
3
|
require "tco_method/mixin"
|
4
|
+
require "tco_method/block_with_tco"
|
5
5
|
|
6
6
|
# The namespace for the TCOMethod gem. Home to private API methods employed by
|
7
7
|
# the {TCOMethod::Mixin} module to provide tail call optimized behavior to
|
@@ -17,48 +17,6 @@ module TCOMethod
|
|
17
17
|
trace_instruction: false,
|
18
18
|
}.freeze
|
19
19
|
|
20
|
-
# Reevaluates a method with tail call optimization enabled.
|
21
|
-
#
|
22
|
-
# @note This method is not part of the public API and should not be used
|
23
|
-
# directly. See {TCOMethod::Mixin} for a module that provides publicly
|
24
|
-
# supported access to the behaviors provided by this method.
|
25
|
-
# @param [Class, Module] receiver The Class or Module for which the specified
|
26
|
-
# module, class, or instance method should be reevaluated with tail call
|
27
|
-
# optimization enabled.
|
28
|
-
# @param [String, Symbol] method_name The name of the method that should be
|
29
|
-
# reevaluated with tail call optimization enabled.
|
30
|
-
# @param [Symbol] method_owner A symbol representing whether the specified
|
31
|
-
# method is expected to be owned by a class, module, or instance.
|
32
|
-
# @return [Symbol] The symbolized method name.
|
33
|
-
# @raise [ArgumentError] Raised if receiver, method_name, or method_owner
|
34
|
-
# argument is omitted.
|
35
|
-
# @raise [TypeError] Raised if the specified method is not a method that can
|
36
|
-
# be reevaluated with tail call optimization enabled.
|
37
|
-
def self.reevaluate_method_with_tco(receiver, method_name, method_owner)
|
38
|
-
raise ArgumentError, "Receiver required!" unless receiver
|
39
|
-
raise ArgumentError, "Method name required!" unless method_name
|
40
|
-
raise ArgumentError, "Method owner required!" unless method_owner
|
41
|
-
if method_owner == :instance
|
42
|
-
existing_method = receiver.instance_method(method_name)
|
43
|
-
elsif method_owner == :class || method_owner== :module
|
44
|
-
existing_method = receiver.method(method_name)
|
45
|
-
end
|
46
|
-
method_info = MethodInfo.new(existing_method)
|
47
|
-
if method_info.type != :method
|
48
|
-
raise TypeError, "Invalid method type: #{method_info.type}"
|
49
|
-
end
|
50
|
-
receiver_class = receiver.is_a?(Class) ? :class : :module
|
51
|
-
code = <<-CODE
|
52
|
-
#{receiver_class} #{receiver.name}
|
53
|
-
#{existing_method.source}
|
54
|
-
end
|
55
|
-
CODE
|
56
|
-
|
57
|
-
file, line = existing_method.source_location
|
58
|
-
tco_eval(code, file, File.dirname(file), line - 1)
|
59
|
-
method_name.to_sym
|
60
|
-
end
|
61
|
-
|
62
20
|
# Provides a mechanism for evaluating Strings of code with tail call
|
63
21
|
# optimization enabled.
|
64
22
|
#
|
@@ -71,4 +29,36 @@ module TCOMethod
|
|
71
29
|
raise ArgumentError, "Invalid code string!" unless code.is_a?(String)
|
72
30
|
RubyVM::InstructionSequence.new(code, file, path, line, ISEQ_OPTIONS).eval
|
73
31
|
end
|
32
|
+
|
33
|
+
# Allows for executing a block of code with tail call optimization enabled.
|
34
|
+
#
|
35
|
+
# All code that is evaluated in the block will be evaluated with tail call
|
36
|
+
# optimization enabled, however here be dragons, so be warned of a few things:
|
37
|
+
#
|
38
|
+
# 1. Though it may not be obvious, any call to `require`, `load`, or similar
|
39
|
+
# methods from within the block will be evaluated by another part of the VM
|
40
|
+
# and will not be tail call optimized. This applies for `tco_eval` as well.
|
41
|
+
#
|
42
|
+
# 2. The block will be evaluated with a different binding than the binding it
|
43
|
+
# was defined in. That means that references to variables or other binding
|
44
|
+
# context will result in method errors. For example:
|
45
|
+
#
|
46
|
+
# some_variable = "Hello, World!"
|
47
|
+
# womp_womp = TCOMethod.with_tco { some_variable }
|
48
|
+
# # => NameError: Undefined local variable or method 'some_variable'
|
49
|
+
#
|
50
|
+
# 3. Though this approach is some what nicer than working with strings of
|
51
|
+
# code, it comes with the tradeoff that it relies on the the `method_source`
|
52
|
+
# gem to do the work of finding the source of the block. There are situations
|
53
|
+
# where `method_source` can't accurately determine the source location of a
|
54
|
+
# block. That said, if you don't format your code like a maniac, you should be
|
55
|
+
# fine.
|
56
|
+
#
|
57
|
+
# @param [Proc] block The proc to evaluate with tail call optimization
|
58
|
+
# enabled.
|
59
|
+
# @return [Object] Returns whatever the result of evaluating the given block.
|
60
|
+
def self.with_tco(&block)
|
61
|
+
raise ArgumentError, "Block required" unless block_given?
|
62
|
+
BlockWithTCO.new(&block).result
|
63
|
+
end
|
74
64
|
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module TCOMethod
|
2
|
+
# Exception raised when it's not possible to reliably determine the source
|
3
|
+
# code of a block.
|
4
|
+
class AmbiguousSourceError < StandardError
|
5
|
+
# Default message template.
|
6
|
+
MESSAGE = "Could not determine source of block".freeze
|
7
|
+
|
8
|
+
# Returns the exception that this exception was created to wrap if any such
|
9
|
+
# exception exists. Used only when this exception is created to wrap
|
10
|
+
# another.
|
11
|
+
attr_accessor :original_exception
|
12
|
+
|
13
|
+
# Create an exception from a problematic block.
|
14
|
+
#
|
15
|
+
# @param [Proc] block The block for which the source is ambiguous.
|
16
|
+
# @return [AmbiguousBlockError] A new exception instance wrapping the given
|
17
|
+
# exception.
|
18
|
+
def self.from_proc(block)
|
19
|
+
new(MESSAGE + " #{block.inspect}")
|
20
|
+
end
|
21
|
+
|
22
|
+
# Wrap another exception with an AmbiguousBlockError. Useful for wrapping
|
23
|
+
# errors raised by MethodSource.
|
24
|
+
#
|
25
|
+
# @param [Exception] exception The exception instance that should be
|
26
|
+
# wrapped.
|
27
|
+
# @return [AmbiguousBlockError] A new exception instance wrapping the given
|
28
|
+
# exception.
|
29
|
+
def self.wrap(exception)
|
30
|
+
error = new(exception.message)
|
31
|
+
error.original_exception = exception
|
32
|
+
error
|
33
|
+
end
|
34
|
+
|
35
|
+
# Creates a new instance of the exception.
|
36
|
+
#
|
37
|
+
# @param [String] message The message to use with the exception.
|
38
|
+
def initialize(message = MESSAGE)
|
39
|
+
super
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
require "tco_method/ambiguous_source_error"
|
2
|
+
require "method_source"
|
3
|
+
require "ripper"
|
4
|
+
|
5
|
+
module TCOMethod
|
6
|
+
# Object encapsulating the logic to extract the source code of a given block.
|
7
|
+
class BlockExtractor
|
8
|
+
DO_STR = "do".freeze
|
9
|
+
END_STR = "end".freeze
|
10
|
+
|
11
|
+
attr_reader :source
|
12
|
+
|
13
|
+
def initialize(block)
|
14
|
+
source = block.source
|
15
|
+
type = block.lambda? ? :lambda : :proc
|
16
|
+
start_offset, end_offset = determine_offsets(block, source)
|
17
|
+
@source = "#{type} #{source[start_offset..end_offset]}"
|
18
|
+
rescue MethodSource::SourceNotFoundError => ex
|
19
|
+
raise AmbiguousSourceError.wrap(ex)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
# Encapsulates the logic required to determine the offset of the end of the
|
25
|
+
# block. The end of the block is characterized by a matching curly brace
|
26
|
+
# (`}`) or the `end` keyword.
|
27
|
+
def determine_end_offset(block, tokens, source, expected_matcher)
|
28
|
+
lines = source.lines
|
29
|
+
last_line_number = lines.length
|
30
|
+
end_offset = nil
|
31
|
+
tokens.reverse_each do |token|
|
32
|
+
# Break once we're through with the last line.
|
33
|
+
break if token[0][0] != last_line_number
|
34
|
+
|
35
|
+
# Look for expected match to block opener
|
36
|
+
next if token[1] != expected_matcher
|
37
|
+
next if token[1] == :on_kw && token[2] != END_STR
|
38
|
+
|
39
|
+
# Raise if we've already found something that looks like a block end.
|
40
|
+
raise AmbiguousSourceError.from_proc(block) if end_offset
|
41
|
+
# Ending offset is the position of the ending token, plus the length of
|
42
|
+
# that token.
|
43
|
+
end_offset = token[0][1] + token[2].length
|
44
|
+
end
|
45
|
+
raise AmbiguousSourceError.from_proc(block) unless end_offset
|
46
|
+
determine_end_offset_relative_to_source(end_offset, lines.last.length)
|
47
|
+
end
|
48
|
+
|
49
|
+
# We subract the length of the last line from end offset to determine the
|
50
|
+
# negative offset into the source string. However we must subtract 1 to
|
51
|
+
# correct for the negative offset referring to the character after the
|
52
|
+
# desired terminal character.
|
53
|
+
def determine_end_offset_relative_to_source(end_offset, last_line_length)
|
54
|
+
end_offset - last_line_length - 1
|
55
|
+
end
|
56
|
+
|
57
|
+
# Tokenizes the source of the block as determined by the `method_source` gem
|
58
|
+
# and determines the beginning and end of the block.
|
59
|
+
#
|
60
|
+
# In both cases the entire line is checked to ensure there's no unexpected
|
61
|
+
# ambiguity as to the start or end of the block. See the test file for this
|
62
|
+
# class for examples of ambiguous situations.
|
63
|
+
#
|
64
|
+
# @param [Proc] block The proc for which the starting offset of its source
|
65
|
+
# code should be determined.
|
66
|
+
# @param [String] source The source code of the provided block.
|
67
|
+
# @raise [AmbiguousSourceError] Raised when the source of the block cannot
|
68
|
+
# be determined unambiguously.
|
69
|
+
# @return [Array<Integer>] The start and end offsets of the block's source
|
70
|
+
# code as 2-element Array.
|
71
|
+
def determine_offsets(block, source)
|
72
|
+
tokens = Ripper.lex(source)
|
73
|
+
start_offset, start_token = determine_start_offset(block, tokens)
|
74
|
+
expected_match = start_token == :on_kw ? :on_kw : :on_rbrace
|
75
|
+
end_offset = determine_end_offset(block, tokens, source, expected_match)
|
76
|
+
[start_offset, end_offset]
|
77
|
+
end
|
78
|
+
|
79
|
+
# The logic required to determine the starting offset of the block. The
|
80
|
+
# start of the block is characterized by the opening left curly brace (`{`)
|
81
|
+
# of the block or the `do` keyword. Everything prior to the start of the
|
82
|
+
# block is ignored because we can determine whether the block should be a
|
83
|
+
# lambda or a proc by asking the block directly, and we may not always have
|
84
|
+
# such a keyword available to us, e.g. a method that takes a block like
|
85
|
+
# TCOMethod.with_tco.
|
86
|
+
def determine_start_offset(block, tokens)
|
87
|
+
start_offset = start_token = nil
|
88
|
+
# The start of the block should occur somewhere on line 1.
|
89
|
+
# Check the whole line to ensure there aren't multiple blocks on the line.
|
90
|
+
tokens.each do |token|
|
91
|
+
# Break after line 1.
|
92
|
+
break if token[0][0] != 1
|
93
|
+
|
94
|
+
# Look for a left brace (`{`) or `do` keyword.
|
95
|
+
if token[1] == :on_lbrace || (token[1] == :on_kw && token[2] == DO_STR)
|
96
|
+
# Raise if we've already found something that looks like a block
|
97
|
+
# start.
|
98
|
+
raise AmbiguousSourceError.from_proc(block) if start_offset
|
99
|
+
start_token = token[1]
|
100
|
+
start_offset = token[0][1]
|
101
|
+
end
|
102
|
+
end
|
103
|
+
raise AmbiguousSourceError.from_proc(block) unless start_offset
|
104
|
+
[start_offset, start_token]
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|