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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 5b38db19eea395f824f107cdf26128b24f4a0de7
4
- data.tar.gz: 061f302f00571f8d3290b2c33deb15c8e3e12c85
3
+ metadata.gz: 15a6ac46846b32e0e851d33bc6285481da316a59
4
+ data.tar.gz: b3e6b3fb09936ff95b3342e009c6216edbf756d6
5
5
  SHA512:
6
- metadata.gz: 5582d4e9e64fc235526dc5c5504ad041e1736252f9017c45934990f0bbd8c005327dc12ecad192373a1cce42a00e76209b5abff4038248170105860b79222987
7
- data.tar.gz: 8e38fa82f97b0cb9f60678ef5342f59f7c7042ed5cd4104f90b49a66cb82de4e9c6571cd36aaa9ea8695d2f3c4d885c718794372c19a8f15bd5f7a0e21d7503f
6
+ metadata.gz: 7651b179452b164aabba4ef9b7435f06c81f84f50496555a7eb2e05ed0fefcc8e47144c18902fb57f96cc331c5e1c52ea6dba8c8c3f4de30c6c689bc89a94737
7
+ data.tar.gz: 876eadb63edf03f8ea5b05f2e53a5d69dc5d9b9ec12a86de083a78c1b2e137f149db166be7e3d61c065fb467e6fb4258bb60eabd46b7020061b090573dc6fcba
data/.gitignore CHANGED
File without changes
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
- Provides `TCOMethod::Mixin` for extending Classes and Modules with helper methods
10
- to facilitate evaluating code and some types of methods with tail call
11
- optimization enabled in MRI Ruby. Also provides `TCOMethod.tco_eval` providing a
12
- direct and easy means to evaluate code strings with tail call optimization
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
- Extend a class with the [`TCOMethod::Mixin`](http://www.rubydoc.info/gems/tco_method/TCOMethod/Mixin)
45
- and let the fun begin!
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
- Or, for more power and flexibility (at the cost of stringified code blocks) use
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 this by dynamically reading the code you want to
100
- compile with tail call optimization, but this approach also has downsides in
101
- that it goes around the standard Ruby `require` model. For example, consider the
102
- Fibonacci example broken across two scripts, one script serving as a loader and
103
- the other script acting as a more standard library:
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 could include the `TCOMethod::Mixin` module
126
- in the Module class and add these behaviors to all Modules and Classes. To quote
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
- Quirks with Module and Class annotations:
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
- I'm sure there are more and I will document them here as I come across them.
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
@@ -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