tco_method 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Gem Version](https://badge.fury.io/rb/tco_method.svg)](http://badge.fury.io/rb/tco_method)
|
3
|
+
[![Yard Docs](http://img.shields.io/badge/yard-docs-blue.svg)](http://www.rubydoc.info/gems/tco_method)
|
4
|
+
[![Build Status](https://travis-ci.org/tdg5/tco_method.svg)](https://travis-ci.org/tdg5/tco_method)
|
5
|
+
[![Coverage Status](https://coveralls.io/repos/tdg5/tco_method/badge.svg)](https://coveralls.io/r/tdg5/tco_method)
|
6
|
+
[![Code Climate](https://codeclimate.com/github/tdg5/tco_method/badges/gpa.svg)](https://codeclimate.com/github/tdg5/tco_method)
|
7
|
+
[![Dependency Status](https://gemnasium.com/tdg5/tco_method.svg)](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:
|