bootsnap 1.4.6

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.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.github/CODEOWNERS +2 -0
  3. data/.github/probots.yml +2 -0
  4. data/.gitignore +17 -0
  5. data/.rubocop.yml +20 -0
  6. data/.travis.yml +21 -0
  7. data/CHANGELOG.md +122 -0
  8. data/CODE_OF_CONDUCT.md +74 -0
  9. data/CONTRIBUTING.md +21 -0
  10. data/Gemfile +9 -0
  11. data/LICENSE.txt +21 -0
  12. data/README.jp.md +231 -0
  13. data/README.md +304 -0
  14. data/Rakefile +13 -0
  15. data/bin/ci +10 -0
  16. data/bin/console +15 -0
  17. data/bin/setup +8 -0
  18. data/bin/test-minimal-support +7 -0
  19. data/bin/testunit +8 -0
  20. data/bootsnap.gemspec +46 -0
  21. data/dev.yml +10 -0
  22. data/ext/bootsnap/bootsnap.c +829 -0
  23. data/ext/bootsnap/bootsnap.h +6 -0
  24. data/ext/bootsnap/extconf.rb +19 -0
  25. data/lib/bootsnap.rb +48 -0
  26. data/lib/bootsnap/bundler.rb +15 -0
  27. data/lib/bootsnap/compile_cache.rb +43 -0
  28. data/lib/bootsnap/compile_cache/iseq.rb +73 -0
  29. data/lib/bootsnap/compile_cache/yaml.rb +63 -0
  30. data/lib/bootsnap/explicit_require.rb +50 -0
  31. data/lib/bootsnap/load_path_cache.rb +78 -0
  32. data/lib/bootsnap/load_path_cache/cache.rb +208 -0
  33. data/lib/bootsnap/load_path_cache/change_observer.rb +63 -0
  34. data/lib/bootsnap/load_path_cache/core_ext/active_support.rb +107 -0
  35. data/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb +93 -0
  36. data/lib/bootsnap/load_path_cache/core_ext/loaded_features.rb +18 -0
  37. data/lib/bootsnap/load_path_cache/loaded_features_index.rb +148 -0
  38. data/lib/bootsnap/load_path_cache/path.rb +114 -0
  39. data/lib/bootsnap/load_path_cache/path_scanner.rb +50 -0
  40. data/lib/bootsnap/load_path_cache/realpath_cache.rb +32 -0
  41. data/lib/bootsnap/load_path_cache/store.rb +90 -0
  42. data/lib/bootsnap/setup.rb +39 -0
  43. data/lib/bootsnap/version.rb +4 -0
  44. data/shipit.rubygems.yml +0 -0
  45. metadata +174 -0
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+ module Bootsnap
3
+ module LoadPathCache
4
+ module ChangeObserver
5
+ module ArrayMixin
6
+ # For each method that adds items to one end or another of the array
7
+ # (<<, push, unshift, concat), override that method to also notify the
8
+ # observer of the change.
9
+ def <<(entry)
10
+ @lpc_observer.push_paths(self, entry.to_s)
11
+ super
12
+ end
13
+
14
+ def push(*entries)
15
+ @lpc_observer.push_paths(self, *entries.map(&:to_s))
16
+ super
17
+ end
18
+
19
+ def unshift(*entries)
20
+ @lpc_observer.unshift_paths(self, *entries.map(&:to_s))
21
+ super
22
+ end
23
+
24
+ def concat(entries)
25
+ @lpc_observer.push_paths(self, *entries.map(&:to_s))
26
+ super
27
+ end
28
+
29
+ # uniq! keeps the first occurance of each path, otherwise preserving
30
+ # order, preserving the effective load path
31
+ def uniq!(*args)
32
+ ret = super
33
+ @lpc_observer.reinitialize if block_given? || !args.empty?
34
+ ret
35
+ end
36
+
37
+ # For each method that modifies the array more aggressively, override
38
+ # the method to also have the observer completely reconstruct its state
39
+ # after the modification. Many of these could be made to modify the
40
+ # internal state of the LoadPathCache::Cache more efficiently, but the
41
+ # accounting cost would be greater than the hit from these, since we
42
+ # actively discourage calling them.
43
+ %i(
44
+ []= clear collect! compact! delete delete_at delete_if fill flatten!
45
+ insert keep_if map! pop reject! replace reverse! rotate! select!
46
+ shift shuffle! slice! sort! sort_by!
47
+ ).each do |method_name|
48
+ define_method(method_name) do |*args, &block|
49
+ ret = super(*args, &block)
50
+ @lpc_observer.reinitialize
51
+ ret
52
+ end
53
+ end
54
+ end
55
+
56
+ def self.register(observer, arr)
57
+ return if arr.frozen? # can't register observer, but no need to.
58
+ arr.instance_variable_set(:@lpc_observer, observer)
59
+ arr.extend(ArrayMixin)
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+ module Bootsnap
3
+ module LoadPathCache
4
+ module CoreExt
5
+ module ActiveSupport
6
+ def self.without_bootsnap_cache
7
+ prev = Thread.current[:without_bootsnap_cache] || false
8
+ Thread.current[:without_bootsnap_cache] = true
9
+ yield
10
+ ensure
11
+ Thread.current[:without_bootsnap_cache] = prev
12
+ end
13
+
14
+ def self.allow_bootsnap_retry(allowed)
15
+ prev = Thread.current[:without_bootsnap_retry] || false
16
+ Thread.current[:without_bootsnap_retry] = !allowed
17
+ yield
18
+ ensure
19
+ Thread.current[:without_bootsnap_retry] = prev
20
+ end
21
+
22
+ module ClassMethods
23
+ def autoload_paths=(o)
24
+ super
25
+ Bootsnap::LoadPathCache.autoload_paths_cache.reinitialize(o)
26
+ end
27
+
28
+ def search_for_file(path)
29
+ return super if Thread.current[:without_bootsnap_cache]
30
+ begin
31
+ Bootsnap::LoadPathCache.autoload_paths_cache.find(path)
32
+ rescue Bootsnap::LoadPathCache::ReturnFalse
33
+ nil # doesn't really apply here
34
+ rescue Bootsnap::LoadPathCache::FallbackScan
35
+ nil # doesn't really apply here
36
+ end
37
+ end
38
+
39
+ def autoloadable_module?(path_suffix)
40
+ Bootsnap::LoadPathCache.autoload_paths_cache.load_dir(path_suffix)
41
+ end
42
+
43
+ def remove_constant(const)
44
+ CoreExt::ActiveSupport.without_bootsnap_cache { super }
45
+ end
46
+
47
+ def require_or_load(*)
48
+ CoreExt::ActiveSupport.allow_bootsnap_retry(true) do
49
+ super
50
+ end
51
+ end
52
+
53
+ # If we can't find a constant using the patched implementation of
54
+ # search_for_file, try again with the default implementation.
55
+ #
56
+ # These methods call search_for_file, and we want to modify its
57
+ # behaviour. The gymnastics here are a bit awkward, but it prevents
58
+ # 200+ lines of monkeypatches.
59
+ def load_missing_constant(from_mod, const_name)
60
+ CoreExt::ActiveSupport.allow_bootsnap_retry(false) do
61
+ super
62
+ end
63
+ rescue NameError => e
64
+ raise(e) if e.instance_variable_defined?(Bootsnap::LoadPathCache::ERROR_TAG_IVAR)
65
+ e.instance_variable_set(Bootsnap::LoadPathCache::ERROR_TAG_IVAR, true)
66
+
67
+ # This function can end up called recursively, we only want to
68
+ # retry at the top-level.
69
+ raise(e) if Thread.current[:without_bootsnap_retry]
70
+ # If we already had cache disabled, there's no use retrying
71
+ raise(e) if Thread.current[:without_bootsnap_cache]
72
+ # NoMethodError is a NameError, but we only want to handle actual
73
+ # NameError instances.
74
+ raise(e) unless e.class == NameError
75
+ # We can only confidently handle cases when *this* constant fails
76
+ # to load, not other constants referred to by it.
77
+ raise(e) unless e.name == const_name
78
+ # If the constant was actually loaded, something else went wrong?
79
+ raise(e) if from_mod.const_defined?(const_name)
80
+ CoreExt::ActiveSupport.without_bootsnap_cache { super }
81
+ end
82
+
83
+ # Signature has changed a few times over the years; easiest to not
84
+ # reiterate it with version polymorphism here...
85
+ def depend_on(*)
86
+ super
87
+ rescue LoadError => e
88
+ raise(e) if e.instance_variable_defined?(Bootsnap::LoadPathCache::ERROR_TAG_IVAR)
89
+ e.instance_variable_set(Bootsnap::LoadPathCache::ERROR_TAG_IVAR, true)
90
+
91
+ # If we already had cache disabled, there's no use retrying
92
+ raise(e) if Thread.current[:without_bootsnap_cache]
93
+ CoreExt::ActiveSupport.without_bootsnap_cache { super }
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
100
+
101
+ module ActiveSupport
102
+ module Dependencies
103
+ class << self
104
+ prepend(Bootsnap::LoadPathCache::CoreExt::ActiveSupport::ClassMethods)
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+ module Bootsnap
3
+ module LoadPathCache
4
+ module CoreExt
5
+ def self.make_load_error(path)
6
+ err = LoadError.new(+"cannot load such file -- #{path}")
7
+ err.instance_variable_set(Bootsnap::LoadPathCache::ERROR_TAG_IVAR, true)
8
+ err.define_singleton_method(:path) { path }
9
+ err
10
+ end
11
+ end
12
+ end
13
+ end
14
+
15
+ module Kernel
16
+ module_function # rubocop:disable Style/ModuleFunction
17
+
18
+ alias_method(:require_without_bootsnap, :require)
19
+
20
+ # Note that require registers to $LOADED_FEATURES while load does not.
21
+ def require_with_bootsnap_lfi(path, resolved = nil)
22
+ Bootsnap::LoadPathCache.loaded_features_index.register(path, resolved) do
23
+ require_without_bootsnap(resolved || path)
24
+ end
25
+ end
26
+
27
+ def require(path)
28
+ return false if Bootsnap::LoadPathCache.loaded_features_index.key?(path)
29
+
30
+ if (resolved = Bootsnap::LoadPathCache.load_path_cache.find(path))
31
+ return require_with_bootsnap_lfi(path, resolved)
32
+ end
33
+
34
+ raise(Bootsnap::LoadPathCache::CoreExt.make_load_error(path))
35
+ rescue LoadError => e
36
+ e.instance_variable_set(Bootsnap::LoadPathCache::ERROR_TAG_IVAR, true)
37
+ raise(e)
38
+ rescue Bootsnap::LoadPathCache::ReturnFalse
39
+ false
40
+ rescue Bootsnap::LoadPathCache::FallbackScan
41
+ require_with_bootsnap_lfi(path)
42
+ end
43
+
44
+ alias_method(:require_relative_without_bootsnap, :require_relative)
45
+ def require_relative(path)
46
+ realpath = Bootsnap::LoadPathCache.realpath_cache.call(
47
+ caller_locations(1..1).first.absolute_path, path
48
+ )
49
+ require(realpath)
50
+ end
51
+
52
+ alias_method(:load_without_bootsnap, :load)
53
+ def load(path, wrap = false)
54
+ if (resolved = Bootsnap::LoadPathCache.load_path_cache.find(path))
55
+ return load_without_bootsnap(resolved, wrap)
56
+ end
57
+
58
+ # load also allows relative paths from pwd even when not in $:
59
+ if File.exist?(relative = File.expand_path(path))
60
+ return load_without_bootsnap(relative, wrap)
61
+ end
62
+
63
+ raise(Bootsnap::LoadPathCache::CoreExt.make_load_error(path))
64
+ rescue LoadError => e
65
+ e.instance_variable_set(Bootsnap::LoadPathCache::ERROR_TAG_IVAR, true)
66
+ raise(e)
67
+ rescue Bootsnap::LoadPathCache::ReturnFalse
68
+ false
69
+ rescue Bootsnap::LoadPathCache::FallbackScan
70
+ load_without_bootsnap(path, wrap)
71
+ end
72
+ end
73
+
74
+ class Module
75
+ alias_method(:autoload_without_bootsnap, :autoload)
76
+ def autoload(const, path)
77
+ # NOTE: This may defeat LoadedFeaturesIndex, but it's not immediately
78
+ # obvious how to make it work. This feels like a pretty niche case, unclear
79
+ # if it will ever burn anyone.
80
+ #
81
+ # The challenge is that we don't control the point at which the entry gets
82
+ # added to $LOADED_FEATURES and won't be able to hook that modification
83
+ # since it's done in C-land.
84
+ autoload_without_bootsnap(const, Bootsnap::LoadPathCache.load_path_cache.find(path) || path)
85
+ rescue LoadError => e
86
+ e.instance_variable_set(Bootsnap::LoadPathCache::ERROR_TAG_IVAR, true)
87
+ raise(e)
88
+ rescue Bootsnap::LoadPathCache::ReturnFalse
89
+ false
90
+ rescue Bootsnap::LoadPathCache::FallbackScan
91
+ autoload_without_bootsnap(const, path)
92
+ end
93
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+ class << $LOADED_FEATURES
3
+ alias_method(:delete_without_bootsnap, :delete)
4
+ def delete(key)
5
+ Bootsnap::LoadPathCache.loaded_features_index.purge(key)
6
+ delete_without_bootsnap(key)
7
+ end
8
+
9
+ alias_method(:reject_without_bootsnap!, :reject!)
10
+ def reject!(&block)
11
+ backup = dup
12
+
13
+ # FIXME: if no block is passed we'd need to return a decorated iterator
14
+ reject_without_bootsnap!(&block)
15
+
16
+ Bootsnap::LoadPathCache.loaded_features_index.purge_multi(backup - self)
17
+ end
18
+ end
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bootsnap
4
+ module LoadPathCache
5
+ # LoadedFeaturesIndex partially mirrors an internal structure in ruby that
6
+ # we can't easily obtain an interface to.
7
+ #
8
+ # This works around an issue where, without bootsnap, *ruby* knows that it
9
+ # has already required a file by its short name (e.g. require 'bundler') if
10
+ # a new instance of bundler is added to the $LOAD_PATH which resolves to a
11
+ # different absolute path. This class makes bootsnap smart enough to
12
+ # realize that it has already loaded 'bundler', and not just
13
+ # '/path/to/bundler'.
14
+ #
15
+ # If you disable LoadedFeaturesIndex, you can see the problem this solves by:
16
+ #
17
+ # 1. `require 'a'`
18
+ # 2. Prepend a new $LOAD_PATH element containing an `a.rb`
19
+ # 3. `require 'a'`
20
+ #
21
+ # Ruby returns false from step 3.
22
+ # With bootsnap but with no LoadedFeaturesIndex, this loads two different
23
+ # `a.rb`s.
24
+ # With bootsnap and with LoadedFeaturesIndex, this skips the second load,
25
+ # returning false like ruby.
26
+ class LoadedFeaturesIndex
27
+ def initialize
28
+ @lfi = {}
29
+ @mutex = defined?(::Mutex) ? ::Mutex.new : ::Thread::Mutex.new # TODO: Remove once Ruby 2.2 support is dropped.
30
+
31
+ # In theory the user could mutate $LOADED_FEATURES and invalidate our
32
+ # cache. If this ever comes up in practice — or if you, the
33
+ # enterprising reader, feels inclined to solve this problem — we could
34
+ # parallel the work done with ChangeObserver on $LOAD_PATH to mirror
35
+ # updates to our @lfi.
36
+ $LOADED_FEATURES.each do |feat|
37
+ hash = feat.hash
38
+ $LOAD_PATH.each do |lpe|
39
+ next unless feat.start_with?(lpe)
40
+ # /a/b/lib/my/foo.rb
41
+ # ^^^^^^^^^
42
+ short = feat[(lpe.length + 1)..-1]
43
+ stripped = strip_extension_if_elidable(short)
44
+ @lfi[short] = hash
45
+ @lfi[stripped] = hash
46
+ end
47
+ end
48
+ end
49
+
50
+ # We've optimized for initialize and register to be fast, and purge to be tolerable.
51
+ # If access patterns make this not-okay, we can lazy-invert the LFI on
52
+ # first purge and work from there.
53
+ def purge(feature)
54
+ @mutex.synchronize do
55
+ feat_hash = feature.hash
56
+ @lfi.reject! { |_, hash| hash == feat_hash }
57
+ end
58
+ end
59
+
60
+ def purge_multi(features)
61
+ rejected_hashes = features.map(&:hash).to_set
62
+ @mutex.synchronize do
63
+ @lfi.reject! { |_, hash| rejected_hashes.include?(hash) }
64
+ end
65
+ end
66
+
67
+ def key?(feature)
68
+ @mutex.synchronize { @lfi.key?(feature) }
69
+ end
70
+
71
+ # There is a relatively uncommon case where we could miss adding an
72
+ # entry:
73
+ #
74
+ # If the user asked for e.g. `require 'bundler'`, and we went through the
75
+ # `FallbackScan` pathway in `kernel_require.rb` and therefore did not
76
+ # pass `long` (the full expanded absolute path), then we did are not able
77
+ # to confidently add the `bundler.rb` form to @lfi.
78
+ #
79
+ # We could either:
80
+ #
81
+ # 1. Just add `bundler.rb`, `bundler.so`, and so on, which is close but
82
+ # not quite right; or
83
+ # 2. Inspect $LOADED_FEATURES upon return from yield to find the matching
84
+ # entry.
85
+ def register(short, long = nil)
86
+ if long.nil?
87
+ pat = %r{/#{Regexp.escape(short)}(\.[^/]+)?$}
88
+ len = $LOADED_FEATURES.size
89
+ ret = yield
90
+ long = $LOADED_FEATURES[len..-1].detect { |feat| feat =~ pat }
91
+ else
92
+ ret = yield
93
+ end
94
+
95
+ hash = long.hash
96
+
97
+ # Do we have a filename with an elidable extension, e.g.,
98
+ # 'bundler.rb', or 'libgit2.so'?
99
+ altname = if extension_elidable?(short)
100
+ # Strip the extension off, e.g. 'bundler.rb' -> 'bundler'.
101
+ strip_extension_if_elidable(short)
102
+ elsif long && (ext = File.extname(long))
103
+ # We already know the extension of the actual file this
104
+ # resolves to, so put that back on.
105
+ short + ext
106
+ end
107
+
108
+ @mutex.synchronize do
109
+ @lfi[short] = hash
110
+ (@lfi[altname] = hash) if altname
111
+ end
112
+
113
+ ret
114
+ end
115
+
116
+ private
117
+
118
+ STRIP_EXTENSION = /\.[^.]*?$/
119
+ private_constant(:STRIP_EXTENSION)
120
+
121
+ # Might Ruby automatically search for this extension if
122
+ # someone tries to 'require' the file without it? E.g. Ruby
123
+ # will implicitly try 'x.rb' if you ask for 'x'.
124
+ #
125
+ # This is complex and platform-dependent, and the Ruby docs are a little
126
+ # handwavy about what will be tried when and in what order.
127
+ # So optimistically pretend that all known elidable extensions
128
+ # will be tried on all platforms, and that people are unlikely
129
+ # to name files in a way that assumes otherwise.
130
+ # (E.g. It's unlikely that someone will know that their code
131
+ # will _never_ run on MacOS, and therefore think they can get away
132
+ # with callling a Ruby file 'x.dylib.rb' and then requiring it as 'x.dylib'.)
133
+ #
134
+ # See <https://ruby-doc.org/core-2.6.4/Kernel.html#method-i-require>.
135
+ def extension_elidable?(f)
136
+ f.to_s.end_with?('.rb', '.so', '.o', '.dll', '.dylib')
137
+ end
138
+
139
+ def strip_extension_if_elidable(f)
140
+ if extension_elidable?(f)
141
+ f.sub(STRIP_EXTENSION, '')
142
+ else
143
+ f
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+ require_relative('path_scanner')
3
+
4
+ module Bootsnap
5
+ module LoadPathCache
6
+ class Path
7
+ # A path is considered 'stable' if it is part of a Gem.path or the ruby
8
+ # distribution. When adding or removing files in these paths, the cache
9
+ # must be cleared before the change will be noticed.
10
+ def stable?
11
+ stability == STABLE
12
+ end
13
+
14
+ # A path is considered volatile if it doesn't live under a Gem.path or
15
+ # the ruby distribution root. These paths are scanned for new additions
16
+ # more frequently.
17
+ def volatile?
18
+ stability == VOLATILE
19
+ end
20
+
21
+ attr_reader(:path)
22
+
23
+ def initialize(path)
24
+ @path = path.to_s
25
+ end
26
+
27
+ # True if the path exists, but represents a non-directory object
28
+ def non_directory?
29
+ !File.stat(path).directory?
30
+ rescue Errno::ENOENT, Errno::ENOTDIR
31
+ false
32
+ end
33
+
34
+ def relative?
35
+ !path.start_with?(SLASH)
36
+ end
37
+
38
+ # Return a list of all the requirable files and all of the subdirectories
39
+ # of this +Path+.
40
+ def entries_and_dirs(store)
41
+ if stable?
42
+ # the cached_mtime field is unused for 'stable' paths, but is
43
+ # set to zero anyway, just in case we change the stability heuristics.
44
+ _, entries, dirs = store.get(expanded_path)
45
+ return [entries, dirs] if entries # cache hit
46
+ entries, dirs = scan!
47
+ store.set(expanded_path, [0, entries, dirs])
48
+ return [entries, dirs]
49
+ end
50
+
51
+ cached_mtime, entries, dirs = store.get(expanded_path)
52
+
53
+ current_mtime = latest_mtime(expanded_path, dirs || [])
54
+ return [[], []] if current_mtime == -1 # path does not exist
55
+ return [entries, dirs] if cached_mtime == current_mtime
56
+
57
+ entries, dirs = scan!
58
+ store.set(expanded_path, [current_mtime, entries, dirs])
59
+ [entries, dirs]
60
+ end
61
+
62
+ def expanded_path
63
+ File.expand_path(path)
64
+ end
65
+
66
+ private
67
+
68
+ def scan! # (expensive) returns [entries, dirs]
69
+ PathScanner.call(expanded_path)
70
+ end
71
+
72
+ # last time a directory was modified in this subtree. +dirs+ should be a
73
+ # list of relative paths to directories under +path+. e.g. for /a/b and
74
+ # /a/b/c, pass ('/a/b', ['c'])
75
+ def latest_mtime(path, dirs)
76
+ max = -1
77
+ ["", *dirs].each do |dir|
78
+ curr = begin
79
+ File.mtime("#{path}/#{dir}").to_i
80
+ rescue Errno::ENOENT, Errno::ENOTDIR
81
+ -1
82
+ end
83
+ max = curr if curr > max
84
+ end
85
+ max
86
+ end
87
+
88
+ # a Path can be either stable of volatile, depending on how frequently we
89
+ # expect its contents may change. Stable paths aren't rescanned nearly as
90
+ # often.
91
+ STABLE = :stable
92
+ VOLATILE = :volatile
93
+
94
+ # Built-in ruby lib stuff doesn't change, but things can occasionally be
95
+ # installed into sitedir, which generally lives under libdir.
96
+ RUBY_LIBDIR = RbConfig::CONFIG['libdir']
97
+ RUBY_SITEDIR = RbConfig::CONFIG['sitedir']
98
+
99
+ def stability
100
+ @stability ||= begin
101
+ if Gem.path.detect { |p| expanded_path.start_with?(p.to_s) }
102
+ STABLE
103
+ elsif Bootsnap.bundler? && expanded_path.start_with?(Bundler.bundle_path.to_s)
104
+ STABLE
105
+ elsif expanded_path.start_with?(RUBY_LIBDIR) && !expanded_path.start_with?(RUBY_SITEDIR)
106
+ STABLE
107
+ else
108
+ VOLATILE
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end