tco_method 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![Code Climate](https://codeclimate.com/github/tdg5/tco_method/badges/gpa.svg)](https://codeclimate.com/github/tdg5/tco_method)
|
7
7
|
[![Dependency Status](https://gemnasium.com/tdg5/tco_method.svg)](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
|