tco_method 0.0.1
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 +7 -0
- data/.gitignore +17 -0
- data/.travis.yml +6 -0
- data/Gemfile +16 -0
- data/Guardfile +5 -0
- data/LICENSE +22 -0
- data/README.md +92 -0
- data/Rakefile +9 -0
- data/lib/tco_method.rb +71 -0
- data/lib/tco_method/method_info.rb +39 -0
- data/lib/tco_method/mixin.rb +45 -0
- data/lib/tco_method/version.rb +4 -0
- data/tco_method.gemspec +27 -0
- data/test/test_helper.rb +25 -0
- data/test/test_helpers/factorial_stack_buster_helper.rb +31 -0
- data/test/test_helpers/stack_busters/factorial_stack_buster.rb +60 -0
- data/test/test_helpers/stack_busters/vanilla_stack_buster.rb +25 -0
- data/test/test_helpers/vanilla_stack_buster_helper.rb +16 -0
- data/test/test_helpers/vm_stack_helper.rb +28 -0
- data/test/unit/method_info_test.rb +64 -0
- data/test/unit/mixin_test.rb +39 -0
- data/test/unit/tco_method_test.rb +139 -0
- metadata +117 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 74462b42288aee51eb1533d2a233c9a39c4bea07
|
4
|
+
data.tar.gz: 8a4ebe308889ee0edf86489e4491ea3d9d656f52
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b3cdc64cac48fd98ca99c52fdc076b9e58e2f209bd8bbb7d5b10bf4feb81af392c4077a891652fadad165d5c3086e200254ef51753eabd45e1e01ec0049210c1
|
7
|
+
data.tar.gz: 807f4ae4f07dbaf15412c9b4dbfe66e1e856e06e647dcd2028b498bac587f4b8dddba890de212ef4250f188ea51a162e796164d3010c09ce22a6a6e50564f32c
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
source "https://rubygems.org"
|
2
|
+
|
3
|
+
gemspec
|
4
|
+
|
5
|
+
group :doc do
|
6
|
+
gem "yard"
|
7
|
+
end
|
8
|
+
|
9
|
+
group :test do
|
10
|
+
gem "coveralls", :require => false
|
11
|
+
gem "guard"
|
12
|
+
gem "guard-minitest"
|
13
|
+
gem "minitest", ">= 3.0"
|
14
|
+
gem "mocha"
|
15
|
+
gem "simplecov", :require => false
|
16
|
+
end
|
data/Guardfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2015 Danny
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
22
|
+
|
data/README.md
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
# TCOMethod
|
2
|
+
[](http://badge.fury.io/rb/tco_method)
|
3
|
+
[](http://www.rubydoc.info/gems/tco_method)
|
4
|
+
[](https://travis-ci.org/tdg5/tco_method)
|
5
|
+
[](https://coveralls.io/r/tdg5/tco_method)
|
6
|
+
[](https://codeclimate.com/github/tdg5/tco_method)
|
7
|
+
[](https://gemnasium.com/tdg5/tco_method)
|
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. Also provides `TCOMethod.tco_eval` providing an easy means
|
12
|
+
to evaluate code strings with tail call optimization enabled.
|
13
|
+
|
14
|
+
## Installation
|
15
|
+
|
16
|
+
Add this line to your application's Gemfile:
|
17
|
+
|
18
|
+
```bash
|
19
|
+
gem 'tco_method'
|
20
|
+
```
|
21
|
+
|
22
|
+
And then execute:
|
23
|
+
|
24
|
+
```bash
|
25
|
+
$ bundle
|
26
|
+
```
|
27
|
+
|
28
|
+
Or install it yourself as:
|
29
|
+
|
30
|
+
```bash
|
31
|
+
$ gem install tco_method
|
32
|
+
```
|
33
|
+
|
34
|
+
## Usage
|
35
|
+
|
36
|
+
Require the `TCOMethod` library:
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
require "tco_method"
|
40
|
+
```
|
41
|
+
|
42
|
+
Extend a class with the `TCOMethod::Mixin` and enjoy!
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
class MyClass
|
46
|
+
extend TCOMethod::Mixin
|
47
|
+
|
48
|
+
def factorial(n, acc = 1)
|
49
|
+
n <= 1 ? acc : factorial(n - 1, n * acc)
|
50
|
+
end
|
51
|
+
tco_method :factorial
|
52
|
+
end
|
53
|
+
|
54
|
+
MyClass.new.factorial(10_000).to_s.length
|
55
|
+
# => 35660
|
56
|
+
```
|
57
|
+
|
58
|
+
Or, use `TCOMethod.tco_eval` directly. Cumbersome, but much more flexible and
|
59
|
+
powerful:
|
60
|
+
|
61
|
+
```ruby
|
62
|
+
TCOMethod.tco_eval(<<-CODE)
|
63
|
+
class MyClass
|
64
|
+
def factorial(n, acc = 1)
|
65
|
+
n <= 1 ? acc : factorial(n - 1, n * acc)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
CODE
|
69
|
+
|
70
|
+
MyClass.new.factorial(10_000).to_s.length
|
71
|
+
# => 35660
|
72
|
+
```
|
73
|
+
|
74
|
+
## Gotchas
|
75
|
+
|
76
|
+
The list so far:
|
77
|
+
|
78
|
+
- Currently only works with methods defined using the `def` keyword.
|
79
|
+
- Class annotations use the [`method_source` gem](https://github.com/banister/method_source)
|
80
|
+
to retrieve the method source to reevaluate. As a result, class annotations
|
81
|
+
can act strangely when used in more dynamic contexts like `irb` or `pry`.
|
82
|
+
|
83
|
+
|
84
|
+
I'm sure there are more and I will document them here as I come across them.
|
85
|
+
|
86
|
+
## Contributing
|
87
|
+
|
88
|
+
1. Fork it ( https://github.com/tdg5/tco_method/fork )
|
89
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
90
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
91
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
92
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
data/lib/tco_method.rb
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
require "method_source"
|
2
|
+
require "tco_method/version"
|
3
|
+
require "tco_method/method_info"
|
4
|
+
require "tco_method/mixin"
|
5
|
+
|
6
|
+
# The namespace for the TCOMethod gem. Home to private API methods employed by
|
7
|
+
# the {TCOMethod::Mixin} module to provide tail call optimized behavior to
|
8
|
+
# extending Classes and Modules.
|
9
|
+
module TCOMethod
|
10
|
+
# Options that must be provided to RubyVM::InstructionSequence in order to
|
11
|
+
# compile code with tail call optimization enabled. Beyond simply enabling the
|
12
|
+
# `tailcall optimization` option, the `trace_instruction` option must also be
|
13
|
+
# disabled because the RubyVM doesn't currently support `set_trace_func` for
|
14
|
+
# code that is compiled with tail call optimization.
|
15
|
+
ISEQ_OPTIONS = {
|
16
|
+
tailcall_optimization: true,
|
17
|
+
trace_instruction: false,
|
18
|
+
}.freeze
|
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
|
+
code = <<-CODE
|
51
|
+
class #{receiver.name}
|
52
|
+
#{existing_method.source}
|
53
|
+
end
|
54
|
+
CODE
|
55
|
+
tco_eval(code)
|
56
|
+
method_name.to_sym
|
57
|
+
end
|
58
|
+
|
59
|
+
# Provides a mechanism for evaluating Strings of code with tail call
|
60
|
+
# optimization enabled.
|
61
|
+
#
|
62
|
+
# @param [String] code The code to evaluate with tail call optimization
|
63
|
+
# enabled.
|
64
|
+
# @return [Object] Returns the value of the final expression of the provided
|
65
|
+
# code String.
|
66
|
+
# @raise [ArgumentError] if the provided code argument is not a String.
|
67
|
+
def self.tco_eval(code)
|
68
|
+
raise ArgumentError, "Invalid code string!" unless code.is_a?(String)
|
69
|
+
RubyVM::InstructionSequence.new(code, nil, nil, nil, ISEQ_OPTIONS).eval
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module TCOMethod
|
2
|
+
# Class encapsulating the behaviors required to extract information about a
|
3
|
+
# method from the 14-element Array of data representing the instruction
|
4
|
+
# sequence of that method.
|
5
|
+
class MethodInfo
|
6
|
+
# A collection of those classes that will be recognized as methods and can
|
7
|
+
# be used effectively with this class.
|
8
|
+
VALID_METHOD_CLASSES = [
|
9
|
+
Method,
|
10
|
+
UnboundMethod,
|
11
|
+
].freeze
|
12
|
+
|
13
|
+
# Creates a new MethodInfo instance.
|
14
|
+
#
|
15
|
+
# @param [Method] method_obj The Method or UnboundMethod object representing
|
16
|
+
# the method for which more information should be retrieved.
|
17
|
+
# @raise [TypeError] Raised if the provided method object is not a Method or
|
18
|
+
# Unbound method.
|
19
|
+
# @see VALID_METHOD_CLASSES
|
20
|
+
def initialize(method_obj)
|
21
|
+
unless VALID_METHOD_CLASSES.any? { |klass| method_obj.is_a?(klass) }
|
22
|
+
msg = "Invalid argument! Method or UnboundMethod expected, received #{method_obj.class.name}"
|
23
|
+
raise TypeError, msg
|
24
|
+
end
|
25
|
+
@info = RubyVM::InstructionSequence.of(method_obj).to_a
|
26
|
+
end
|
27
|
+
|
28
|
+
# Returns the type of the method object as reported by the Array of data
|
29
|
+
# describing the instruction sequence representing the method.
|
30
|
+
#
|
31
|
+
# @return [Symbol] A Symbol identifying the type of the instruction
|
32
|
+
# sequence. Typical values will be :method or :block, but all of the
|
33
|
+
# following are valid return values: :top, :method, :block, :class,
|
34
|
+
# :rescue, :ensure, :eval, :main, and :defined_guard.
|
35
|
+
def type
|
36
|
+
@info[9]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module TCOMethod
|
2
|
+
# Mixin providing tail call optimization eval and class annotations. When
|
3
|
+
# extended by a Class or Module adds methods for evaluating code with tail
|
4
|
+
# call optimization enabled and re-evaluating existing methods with tail call
|
5
|
+
# optimization enabled.
|
6
|
+
module Mixin
|
7
|
+
# Class annotation causing the class or module method identified by the
|
8
|
+
# given method name to be reevaluated with tail call optimization enabled.
|
9
|
+
# Only works for methods defined using the `def` keyword.
|
10
|
+
#
|
11
|
+
# @param [String, Symbol] method_name The name of the class or module method
|
12
|
+
# that should be reeevaluated with tail call optimization enabled.
|
13
|
+
# @return [Symbol] The symbolized method name.
|
14
|
+
# @see TCOMethod.reevaluate_method_with_tco
|
15
|
+
def tco_class_method(method_name)
|
16
|
+
TCOMethod.reevaluate_method_with_tco(self, method_name, :class)
|
17
|
+
end
|
18
|
+
alias_method :tco_module_method, :tco_class_method
|
19
|
+
|
20
|
+
# Evaluate the given code String with tail call optimization enabled.
|
21
|
+
#
|
22
|
+
# @param [String] code The code to evaluate with tail call optimization
|
23
|
+
# enabled.
|
24
|
+
# @return [Object] Returns the value of the final expression of the provided
|
25
|
+
# code String.
|
26
|
+
# @raise [ArgumentError] if the provided code argument is not a String.
|
27
|
+
# @see TCOMethod.tco_eval
|
28
|
+
def tco_eval(code)
|
29
|
+
TCOMethod.tco_eval(code)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Class annotation causing the instance method identified by the given
|
33
|
+
# method name to be reevaluated with tail call optimization enabled. Only
|
34
|
+
# works for methods defined using the `def` keyword.
|
35
|
+
#
|
36
|
+
# @param [String, Symbol] method_name The name of the instance method that
|
37
|
+
# should be reeevaluated with tail call optimization enabled.
|
38
|
+
# @return [Symbol] The symbolized method name.
|
39
|
+
# @see TCOMethod.reevaluate_method_with_tco
|
40
|
+
def tco_method(method_name)
|
41
|
+
TCOMethod.reevaluate_method_with_tco(self, method_name, :instance)
|
42
|
+
method_name.to_sym
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/tco_method.gemspec
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "tco_method/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "tco_method"
|
8
|
+
spec.version = TCOMethod::VERSION
|
9
|
+
spec.authors = ["Danny Guinther"]
|
10
|
+
spec.email = ["dannyguinther@gmail.com"]
|
11
|
+
spec.summary = %q{Simplifies creating tail call optimized procs/lambdas/methods in Ruby.}
|
12
|
+
spec.description = %q{Simplifies creating tail call optimized procs/lambdas/methods in Ruby.}
|
13
|
+
spec.homepage = "https://github.com/tdg5/tco_method"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.required_ruby_version = "~> 2"
|
17
|
+
|
18
|
+
spec.files = `git ls-files -z`.split("\x0")
|
19
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
20
|
+
spec.test_files = spec.files.grep(%r{^test/})
|
21
|
+
spec.require_paths = ["lib"]
|
22
|
+
|
23
|
+
spec.add_dependency "method_source", "~> 0"
|
24
|
+
|
25
|
+
spec.add_development_dependency "bundler", "~> 1.6"
|
26
|
+
spec.add_development_dependency "rake", "~> 0"
|
27
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
if ENV["CI"]
|
2
|
+
require "simplecov"
|
3
|
+
require "coveralls"
|
4
|
+
Coveralls.wear!
|
5
|
+
SimpleCov.formatter = Coveralls::SimpleCov::Formatter
|
6
|
+
SimpleCov.root(File.expand_path("../..", __FILE__))
|
7
|
+
end
|
8
|
+
|
9
|
+
require "minitest/autorun"
|
10
|
+
require "mocha/setup"
|
11
|
+
require "tco_method"
|
12
|
+
|
13
|
+
require "test_helpers/vm_stack_helper"
|
14
|
+
require "test_helpers/factorial_stack_buster_helper"
|
15
|
+
require "test_helpers/vanilla_stack_buster_helper"
|
16
|
+
|
17
|
+
# Use alternate shoulda-style DSL for tests
|
18
|
+
class TCOMethod::TestCase < Minitest::Spec
|
19
|
+
class << self
|
20
|
+
alias :setup :before
|
21
|
+
alias :teardown :after
|
22
|
+
alias :context :describe
|
23
|
+
alias :should :it
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require "test_helpers/stack_busters/factorial_stack_buster"
|
2
|
+
|
3
|
+
module TCOMethod
|
4
|
+
module TestHelpers
|
5
|
+
module FactorialStackBusterHelper
|
6
|
+
extend Forwardable
|
7
|
+
|
8
|
+
def_delegators "#{self.name}.stack_buster", :unoptimized_factorial
|
9
|
+
|
10
|
+
long_alias = :factorial_stack_buster_stack_depth_remaining
|
11
|
+
def_delegator "#{self.name}.stack_buster", :stack_depth_remaining, long_alias
|
12
|
+
|
13
|
+
def self.stack_buster
|
14
|
+
@@stack_buster ||= StackBusters::FactorialStackBuster.new
|
15
|
+
end
|
16
|
+
|
17
|
+
def assert_unoptimized_factorial_stack_overflow(depth)
|
18
|
+
# Subtract 1 to account for this methiod call since result should be
|
19
|
+
# relative to caller
|
20
|
+
unoptimized_factorial(depth - 1)
|
21
|
+
assert false, "Factorial for depth #{depth} did not overflow!"
|
22
|
+
rescue SystemStackError
|
23
|
+
assert true
|
24
|
+
end
|
25
|
+
|
26
|
+
def iterative_factorial(n)
|
27
|
+
(2..n).inject(1, :*)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require "pry"
|
2
|
+
|
3
|
+
module TCOMethod
|
4
|
+
module TestHelpers
|
5
|
+
module StackBusters
|
6
|
+
class FactorialStackBuster
|
7
|
+
include VMStackHelper
|
8
|
+
|
9
|
+
def frame_size
|
10
|
+
@frame_size ||= vm_stack_size / stack_overflow_threshold
|
11
|
+
end
|
12
|
+
|
13
|
+
def unoptimized_factorial(n, acc = 1)
|
14
|
+
n <= 1 ? acc : unoptimized_factorial(n - 1, n * acc)
|
15
|
+
end
|
16
|
+
|
17
|
+
def stack_depth_remaining
|
18
|
+
stack_depth_remaining_for_frame_size(frame_size)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
# Stack buster based on binary search for point of stack oveflow of
|
24
|
+
# unoptimized_factorial
|
25
|
+
def stack_overflow_threshold
|
26
|
+
# Use a frame size that's larger than the expected frame size to ensure
|
27
|
+
# that limit is less than the point of overflow
|
28
|
+
limit = last_good_limit = stack_depth_limit_for_frame_size(LARGEST_VM_STACK_SIZE * 2)
|
29
|
+
last_overflow_limit = nil
|
30
|
+
# Determine an upper-bound for binary search
|
31
|
+
loop do
|
32
|
+
begin
|
33
|
+
unoptimized_factorial(limit)
|
34
|
+
last_good_limit = limit
|
35
|
+
limit *= 2
|
36
|
+
rescue SystemStackError
|
37
|
+
last_overflow_limit = limit
|
38
|
+
break
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Reset for binary search for point of stack overflow
|
43
|
+
limit = last_good_limit
|
44
|
+
loop do
|
45
|
+
return last_overflow_limit if last_overflow_limit == last_good_limit + 1
|
46
|
+
begin
|
47
|
+
half_the_distance_to_overflow = (last_overflow_limit - limit) / 2
|
48
|
+
limit += half_the_distance_to_overflow
|
49
|
+
unoptimized_factorial(limit)
|
50
|
+
last_good_limit = limit
|
51
|
+
rescue SystemStackError
|
52
|
+
last_overflow_limit = limit
|
53
|
+
limit = last_good_limit
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module TCOMethod
|
2
|
+
module TestHelpers
|
3
|
+
module StackBusters
|
4
|
+
class VanillaStackBuster
|
5
|
+
include VMStackHelper
|
6
|
+
|
7
|
+
def frame_size
|
8
|
+
@frame_size ||= vm_stack_size / stack_overflow_threshold
|
9
|
+
end
|
10
|
+
|
11
|
+
def stack_depth_remaining
|
12
|
+
stack_depth_remaining_for_frame_size(frame_size)
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def stack_overflow_threshold(depth = 1)
|
18
|
+
stack_overflow_threshold(depth + 1)
|
19
|
+
rescue SystemStackError
|
20
|
+
depth
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require "test_helpers/stack_busters/vanilla_stack_buster"
|
2
|
+
|
3
|
+
module TCOMethod
|
4
|
+
module TestHelpers
|
5
|
+
module VanillaStackBusterHelper
|
6
|
+
extend Forwardable
|
7
|
+
|
8
|
+
def_delegator "#{self.name}.stack_buster", :stack_depth_remaining, :vanilla_stack_depth_remaining
|
9
|
+
|
10
|
+
def self.stack_buster
|
11
|
+
@@stack_buster ||= StackBusters::VanillaStackBuster.new
|
12
|
+
end
|
13
|
+
private_class_method :stack_buster
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module TCOMethod
|
2
|
+
module TestHelpers
|
3
|
+
module VMStackHelper
|
4
|
+
LARGEST_VM_STACK_SIZE = 128
|
5
|
+
|
6
|
+
# Calculate the maximum number of times a stack frame of a given size could
|
7
|
+
# be repeated before running out of room on the stack.
|
8
|
+
def stack_depth_limit_for_frame_size(frame_size)
|
9
|
+
vm_stack_size / frame_size
|
10
|
+
end
|
11
|
+
module_function :stack_depth_limit_for_frame_size
|
12
|
+
|
13
|
+
# Calculate how many more times a frame could be repeated before running out of
|
14
|
+
# room on the stack.
|
15
|
+
def stack_depth_remaining_for_frame_size(frame_size)
|
16
|
+
stack_depth_limit_for_frame_size(frame_size) - caller.length + 1
|
17
|
+
end
|
18
|
+
module_function :stack_depth_remaining_for_frame_size
|
19
|
+
|
20
|
+
# Determine maximum size of VM stack for a single thread based on
|
21
|
+
# environment or default value.
|
22
|
+
def vm_stack_size
|
23
|
+
ENV["RUBY_THREAD_VM_STACK_SIZE"] || RubyVM::DEFAULT_PARAMS[:thread_vm_stack_size]
|
24
|
+
end
|
25
|
+
module_function :vm_stack_size
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
class MethodInfoTest < TCOMethod::TestCase
|
4
|
+
Subject = TCOMethod::MethodInfo
|
5
|
+
|
6
|
+
TestClass = Class.new do
|
7
|
+
class << self; define_method(:class_block_method) { }; end
|
8
|
+
def self.class_def_method; end
|
9
|
+
define_method(:instance_block_method) { }
|
10
|
+
def instance_def_method; end
|
11
|
+
end
|
12
|
+
|
13
|
+
context "#initialize" do
|
14
|
+
should "raise TypeError unless given a Method object" do
|
15
|
+
non_methods = [
|
16
|
+
proc { },
|
17
|
+
lambda { },
|
18
|
+
Proc.new { },
|
19
|
+
]
|
20
|
+
non_methods.each do |non_method|
|
21
|
+
assert_raises(TypeError) do
|
22
|
+
Subject.new(non_method)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
should "accept Method objects defined on a class using def" do
|
28
|
+
method_obj = TestClass.method(:class_def_method)
|
29
|
+
assert_kind_of Method, method_obj
|
30
|
+
assert_kind_of Subject, Subject.new(method_obj)
|
31
|
+
end
|
32
|
+
|
33
|
+
should "accept UnboundMethod objects defined on an instance using def" do
|
34
|
+
method_obj = TestClass.instance_method(:instance_def_method)
|
35
|
+
assert_kind_of UnboundMethod, method_obj
|
36
|
+
assert_kind_of Subject, Subject.new(method_obj)
|
37
|
+
end
|
38
|
+
|
39
|
+
should "accept Method objects defined on a class using define_method" do
|
40
|
+
method_obj = TestClass.method(:class_block_method)
|
41
|
+
assert_kind_of Method, method_obj
|
42
|
+
assert_kind_of Subject, Subject.new(method_obj)
|
43
|
+
end
|
44
|
+
|
45
|
+
should "accept UnboundMethod objects defined on an instance using define_method" do
|
46
|
+
method_obj = TestClass.instance_method(:instance_block_method)
|
47
|
+
assert_kind_of UnboundMethod, method_obj
|
48
|
+
assert_kind_of Subject, Subject.new(method_obj)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
context "#type" do
|
53
|
+
subject { TestClass.new }
|
54
|
+
should "return :method for methods defined using def" do
|
55
|
+
assert_equal :method, Subject.new(TestClass.method(:class_def_method)).type
|
56
|
+
assert_equal :method, Subject.new(subject.method(:instance_def_method)).type
|
57
|
+
end
|
58
|
+
|
59
|
+
should "return :block for methods defined using define_method" do
|
60
|
+
assert_equal :block, Subject.new(TestClass.method(:class_block_method)).type
|
61
|
+
assert_equal :block, Subject.new(subject.method(:instance_block_method)).type
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
class MixinTest < TCOMethod::TestCase
|
4
|
+
include TCOMethod::TestHelpers::FactorialStackBusterHelper
|
5
|
+
|
6
|
+
TestClass = Class.new do
|
7
|
+
extend TCOMethod::Mixin
|
8
|
+
end
|
9
|
+
|
10
|
+
subject { TestClass }
|
11
|
+
|
12
|
+
[:tco_class_method, :tco_module_method].each do |method_alias|
|
13
|
+
context "##{method_alias}" do
|
14
|
+
should "call TCOMethod.reevaluate_method_with_tco with expected arguments" do
|
15
|
+
method_name = :some_method
|
16
|
+
args = [TestClass, method_name, :class]
|
17
|
+
TCOMethod.expects(:reevaluate_method_with_tco).with(*args)
|
18
|
+
subject.send(method_alias, method_name)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
context "#tco_eval" do
|
24
|
+
should "call TCOMethod.eval with expected arguments" do
|
25
|
+
code = "some_code"
|
26
|
+
TCOMethod.expects(:tco_eval).with(code)
|
27
|
+
subject.tco_eval(code)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
context "#tco_method" do
|
32
|
+
should "call TCOMethod.reevaluate_method_with_tco with expected arguments" do
|
33
|
+
method_name = :some_method
|
34
|
+
args = [TestClass, method_name, :instance]
|
35
|
+
TCOMethod.expects(:reevaluate_method_with_tco).with(*args)
|
36
|
+
subject.tco_method(method_name)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
class TCOMethodTest < TCOMethod::TestCase
|
4
|
+
include TCOMethod::TestHelpers::FactorialStackBusterHelper
|
5
|
+
|
6
|
+
Subject = TCOMethod
|
7
|
+
|
8
|
+
TestClass = Class.new do
|
9
|
+
extend TCOMethod::Mixin
|
10
|
+
|
11
|
+
class << self;
|
12
|
+
define_method(:class_block_method) { }
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.class_factorial(n, acc = 1)
|
16
|
+
n <= 1 ? acc : class_factorial(n - 1, n * acc)
|
17
|
+
end
|
18
|
+
|
19
|
+
define_method(:instance_block_method) { }
|
20
|
+
|
21
|
+
def instance_factorial(n, acc = 1)
|
22
|
+
n <= 1 ? acc : instance_factorial(n - 1, n * acc)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
subject { Subject }
|
27
|
+
|
28
|
+
context Subject.name do
|
29
|
+
should "be defined" do
|
30
|
+
assert defined?(subject), "Expected #{subject.name} to be defined!"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
context "::tco_eval" do
|
35
|
+
should "raise ArgumentError unless code is a String" do
|
36
|
+
bad_code = [
|
37
|
+
:bad_code,
|
38
|
+
5,
|
39
|
+
proc { puts "hello" },
|
40
|
+
]
|
41
|
+
bad_code.each do |non_code|
|
42
|
+
assert_raises(ArgumentError) do
|
43
|
+
subject.tco_eval(non_code)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
should "compile the given code with tail call optimization" do
|
49
|
+
FactorialEvalDummy = dummy_class = Class.new
|
50
|
+
subject.tco_eval(<<-CODE)
|
51
|
+
class #{dummy_class.name}
|
52
|
+
def factorial(n, acc = 1)
|
53
|
+
n <= 1 ? acc : factorial(n - 1, n * acc)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
CODE
|
57
|
+
|
58
|
+
# Exceed maximum available stack depth by 100 for good measure
|
59
|
+
factorial_seed = factorial_stack_buster_stack_depth_remaining + 100
|
60
|
+
assert_unoptimized_factorial_stack_overflow(factorial_seed)
|
61
|
+
|
62
|
+
expected_result = iterative_factorial(factorial_seed)
|
63
|
+
assert_equal expected_result, dummy_class.new.factorial(factorial_seed)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
context "::reevaluate_method_with_tco" do
|
68
|
+
subject { Subject.method(:reevaluate_method_with_tco) }
|
69
|
+
|
70
|
+
context "validation" do
|
71
|
+
should "raise ArgumentError unless receiver given" do
|
72
|
+
assert_raises(ArgumentError) do
|
73
|
+
subject.call(nil, :nil?, :instance)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
should "raise ArgumentError unless method name given" do
|
78
|
+
assert_raises(ArgumentError) do
|
79
|
+
subject.call(TestClass, nil, :instance)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
should "raise ArgumentError unless method owner given" do
|
84
|
+
assert_raises(ArgumentError) do
|
85
|
+
subject.call(TestClass, :class_factorial, nil)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
should "raise TypeError for block methods" do
|
90
|
+
assert_raises(TypeError) do
|
91
|
+
subject.call(TestClass, :class_block_method, :class)
|
92
|
+
end
|
93
|
+
assert_raises(TypeError) do
|
94
|
+
subject.call(TestClass, :instance_block_method, :instance)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
context "with class method" do
|
100
|
+
should "raise NameError if no class method with given name defined" do
|
101
|
+
assert_raises(NameError) do
|
102
|
+
subject.call(TestClass, :marmalade, :class)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
should "re-compile the given method with tail call optimization" do
|
107
|
+
# Exceed maximum available stack depth by 100 for good measure
|
108
|
+
factorial_seed = factorial_stack_buster_stack_depth_remaining + 100
|
109
|
+
assert_raises(SystemStackError) do
|
110
|
+
TestClass.class_factorial(factorial_seed)
|
111
|
+
end
|
112
|
+
|
113
|
+
subject.call(TestClass, :class_factorial, :class)
|
114
|
+
expected_result = iterative_factorial(factorial_seed)
|
115
|
+
assert_equal expected_result, TestClass.class_factorial(factorial_seed)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
context "with instance method" do
|
120
|
+
should "raise NameError if no instance method with given name defined" do
|
121
|
+
assert_raises(NameError) do
|
122
|
+
subject.call(TestClass, :marmalade, :instance)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
should "re-compile the given method with tail call optimization" do
|
127
|
+
# Exceed maximum available stack depth by 100 for good measure
|
128
|
+
factorial_seed = factorial_stack_buster_stack_depth_remaining + 100
|
129
|
+
assert_raises(SystemStackError) do
|
130
|
+
TestClass.new.instance_factorial(factorial_seed)
|
131
|
+
end
|
132
|
+
|
133
|
+
subject.call(TestClass, :instance_factorial, :instance)
|
134
|
+
expected_result = iterative_factorial(factorial_seed)
|
135
|
+
assert_equal expected_result, TestClass.new.instance_factorial(factorial_seed)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
metadata
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tco_method
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Danny Guinther
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-03-12 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: method_source
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.6'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.6'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
description: Simplifies creating tail call optimized procs/lambdas/methods in Ruby.
|
56
|
+
email:
|
57
|
+
- dannyguinther@gmail.com
|
58
|
+
executables: []
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- ".gitignore"
|
63
|
+
- ".travis.yml"
|
64
|
+
- Gemfile
|
65
|
+
- Guardfile
|
66
|
+
- LICENSE
|
67
|
+
- README.md
|
68
|
+
- Rakefile
|
69
|
+
- lib/tco_method.rb
|
70
|
+
- lib/tco_method/method_info.rb
|
71
|
+
- lib/tco_method/mixin.rb
|
72
|
+
- lib/tco_method/version.rb
|
73
|
+
- tco_method.gemspec
|
74
|
+
- test/test_helper.rb
|
75
|
+
- test/test_helpers/factorial_stack_buster_helper.rb
|
76
|
+
- test/test_helpers/stack_busters/factorial_stack_buster.rb
|
77
|
+
- test/test_helpers/stack_busters/vanilla_stack_buster.rb
|
78
|
+
- test/test_helpers/vanilla_stack_buster_helper.rb
|
79
|
+
- test/test_helpers/vm_stack_helper.rb
|
80
|
+
- test/unit/method_info_test.rb
|
81
|
+
- test/unit/mixin_test.rb
|
82
|
+
- test/unit/tco_method_test.rb
|
83
|
+
homepage: https://github.com/tdg5/tco_method
|
84
|
+
licenses:
|
85
|
+
- MIT
|
86
|
+
metadata: {}
|
87
|
+
post_install_message:
|
88
|
+
rdoc_options: []
|
89
|
+
require_paths:
|
90
|
+
- lib
|
91
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - "~>"
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '2'
|
96
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
97
|
+
requirements:
|
98
|
+
- - ">="
|
99
|
+
- !ruby/object:Gem::Version
|
100
|
+
version: '0'
|
101
|
+
requirements: []
|
102
|
+
rubyforge_project:
|
103
|
+
rubygems_version: 2.2.2
|
104
|
+
signing_key:
|
105
|
+
specification_version: 4
|
106
|
+
summary: Simplifies creating tail call optimized procs/lambdas/methods in Ruby.
|
107
|
+
test_files:
|
108
|
+
- test/test_helper.rb
|
109
|
+
- test/test_helpers/factorial_stack_buster_helper.rb
|
110
|
+
- test/test_helpers/stack_busters/factorial_stack_buster.rb
|
111
|
+
- test/test_helpers/stack_busters/vanilla_stack_buster.rb
|
112
|
+
- test/test_helpers/vanilla_stack_buster_helper.rb
|
113
|
+
- test/test_helpers/vm_stack_helper.rb
|
114
|
+
- test/unit/method_info_test.rb
|
115
|
+
- test/unit/mixin_test.rb
|
116
|
+
- test/unit/tco_method_test.rb
|
117
|
+
has_rdoc:
|