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 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