class Bootsnap::LoadPathCache::LoadedFeaturesIndex

Experimental RBS support (using type sampling data from the type_fusion project).

# sig/bootsnap/load_path_cache/loaded_features_index.rbs

class Bootsnap::LoadPathCache::LoadedFeaturesIndex
  def key?: (String feature) -> bool
end

returning false like ruby.
With bootsnap and with LoadedFeaturesIndex, this skips the second load,
‘a.rb`s.
With bootsnap but with no LoadedFeaturesIndex, this loads two different
Ruby returns false from step 3.
3. `require ’a’‘
2. Prepend a new $LOAD_PATH element containing an `a.rb`
1. `require ’a’‘
If you disable LoadedFeaturesIndex, you can see the problem this solves by:
’/path/to/bundler’.
realize that it has already loaded ‘bundler’, and not just
different absolute path. This class makes bootsnap smart enough to
a new instance of bundler is added to the $LOAD_PATH which resolves to a
has already required a file by its short name (e.g. require ‘bundler’) if
This works around an issue where, without bootsnap, ruby knows that it
we can’t easily obtain an interface to.
LoadedFeaturesIndex partially mirrors an internal structure in ruby that

def cursor(short)

def cursor(short)
  unless Bootsnap.absolute_path?(short.to_s)
    $LOADED_FEATURES.size
  end
end

def extension_elidable?(feature)

See .

with calling a Ruby file 'x.dylib.rb' and then requiring it as 'x.dylib'.)
will _never_ run on MacOS, and therefore think they can get away
(E.g. It's unlikely that someone will know that their code
to name files in a way that assumes otherwise.
will be tried on all platforms, and that people are unlikely
So optimistically pretend that all known elidable extensions
handwavy about what will be tried when and in what order.
This is complex and platform-dependent, and the Ruby docs are a little

will implicitly try 'x.rb' if you ask for 'x'.
someone tries to 'require' the file without it? E.g. Ruby
Might Ruby automatically search for this extension if
def extension_elidable?(feature)
  feature.to_s.end_with?(".rb", ".so", ".o", ".dll", ".dylib")
end

def identify(short, cursor)

def identify(short, cursor)
  $LOADED_FEATURES[cursor..-1].detect do |feat|
    offset = 0
    while (offset = feat.index(short, offset))
      if feat.index(".", offset + 1) && !feat.index("/", offset + 2)
        break true
      else
        offset += 1
      end
    end
  end
end

def initialize

def initialize
  @lfi = {}
  @mutex = Mutex.new
  # In theory the user could mutate $LOADED_FEATURES and invalidate our
  # cache. If this ever comes up in practice - or if you, the
  # enterprising reader, feels inclined to solve this problem - we could
  # parallel the work done with ChangeObserver on $LOAD_PATH to mirror
  # updates to our @lfi.
  $LOADED_FEATURES.each do |feat|
    hash = feat.hash
    $LOAD_PATH.each do |lpe|
      next unless feat.start_with?(lpe)
      # /a/b/lib/my/foo.rb
      #          ^^^^^^^^^
      short = feat[(lpe.length + 1)..-1]
      stripped = strip_extension_if_elidable(short)
      @lfi[short] = hash
      @lfi[stripped] = hash
    end
  end
end

def key?(feature)

Experimental RBS support (using type sampling data from the type_fusion project).

def key?: (String feature) -> bool

This signature was generated using 3 samples from 1 application.

def key?(feature)
  @mutex.synchronize { @lfi.key?(feature) }
end

def purge(feature)

first purge and work from there.
If access patterns make this not-okay, we can lazy-invert the LFI on
We've optimized for initialize and register to be fast, and purge to be tolerable.
def purge(feature)
  @mutex.synchronize do
    feat_hash = feature.hash
    @lfi.reject! { |_, hash| hash == feat_hash }
  end
end

def purge_multi(features)

def purge_multi(features)
  rejected_hashes = features.each_with_object({}) { |f, h| h[f.hash] = true }
  @mutex.synchronize do
    @lfi.reject! { |_, hash| rejected_hashes.key?(hash) }
  end
end

def register(short, long)

entry.
2. Inspect $LOADED_FEATURES upon return from yield to find the matching
not quite right; or
1. Just add `bundler.rb`, `bundler.so`, and so on, which is close but

We could either:

to confidently add the `bundler.rb` form to @lfi.
pass `long` (the full expanded absolute path), then we did are not able
`FALLBACK_SCAN` pathway in `kernel_require.rb` and therefore did not
If the user asked for e.g. `require 'bundler'`, and we went through the

entry:
There is a relatively uncommon case where we could miss adding an
def register(short, long)
  return if Bootsnap.absolute_path?(short)
  hash = long.hash
  # Do we have a filename with an elidable extension, e.g.,
  # 'bundler.rb', or 'libgit2.so'?
  altname = if extension_elidable?(short)
    # Strip the extension off, e.g. 'bundler.rb' -> 'bundler'.
    strip_extension_if_elidable(short)
  elsif long && (ext = File.extname(long.freeze))
    # We already know the extension of the actual file this
    # resolves to, so put that back on.
    short + ext
  end
  @mutex.synchronize do
    @lfi[short] = hash
    (@lfi[altname] = hash) if altname
  end
end

def strip_extension_if_elidable(feature)

def strip_extension_if_elidable(feature)
  if extension_elidable?(feature)
    feature.sub(STRIP_EXTENSION, "")
  else
    feature
  end
end