class Zeitwerk::Cref::Map

:nodoc: all
feels natural, since crefs are central objects in Zeitwerk’s implementation.
based on class and module names, even being synchronized. Also, client code
Finally, I came with this solution which is 1.6x faster than the previous one
and the matching eql?, but that was about 1.8x slower.
real_mod_hash(mod) ^ cname.hash
Another option would be to make crefs hashable. I tried with hash code
The gem used this approach for several years.
{ “M::X” => 0, “M::Y” => 1, “N::Z” => 2 }
module names. In the example above it would be:
We can also use a 1-level hash whose keys are the corresponding class and
may have been overridden:
class and module objects are not guaranteed to be hashable, the ‘hash` method
Why not use tables that map pairs [Module, Symbol] to their values? Because
This structure is internal, so only the needed interface is implemented.
{ M => { X: 0, :Y => 1 }, N => { Z: 2 } }
correspond to `M::X`, `M::Y`, and `N::Z`, the map will look like this:
For example, if we store values 0, 1, and 2 for the crefs that would
constant names as symbols. We finally store the values in those.
IDs (see why below). Then, each one of them stores a hash table keyed on
are class and module objects, but their hash code is forced to be their object
It is a synchronized 2-level hash. The keys of the top one, stored in `@map`,
This class emulates a hash table whose keys are of type Zeitwerk::Cref.

def [](cref)

@sig (Zeitwerk::Cref) -> top?
def [](cref)
  @mutex.synchronize do
    @map[cref.mod]&.[](cref.cname)
  end
end

def []=(cref, value)

@sig (Zeitwerk::Cref, V) -> V
def []=(cref, value)
  @mutex.synchronize do
    cnames = (@map[cref.mod] ||= {})
    cnames[cref.cname] = value
  end
end

def clear

@sig () -> void
def clear
  @mutex.synchronize do
    @map.clear
  end
end

def delete(cref)

@sig (Zeitwerk::Cref) -> top?
def delete(cref)
  delete_mod_cname(cref.mod, cref.cname)
end

def delete_by_value(value)

@sig (top) -> void
def delete_by_value(value)
  @mutex.synchronize do
    @map.delete_if do |mod, cnames|
      cnames.delete_if { _2 == value }
      cnames.empty?
    end
  end
end

def delete_mod_cname(mod, cname)

@sig (Module, Symbol) -> top?

to not create a cref in every call, since that is global.
Ad-hoc for loader_for, called from const_added. That is a hot path, I prefer
def delete_mod_cname(mod, cname)
  @mutex.synchronize do
    if cnames = @map[mod]
      value = cnames.delete(cname)
      @map.delete(mod) if cnames.empty?
      value
    end
  end
end

def each_key

@sig () { (Zeitwerk::Cref) -> void } -> void

Order of yielded crefs is undefined.
def each_key
  @mutex.synchronize do
    @map.each do |mod, cnames|
      cnames.each_key do |cname|
        yield Zeitwerk::Cref.new(mod, cname)
      end
    end
  end
end

def empty? # for tests

for tests
@sig () -> bool
def empty? # for tests
  @mutex.synchronize do
    @map.empty?
  end
end

def get_or_set(cref, &block)

@sig (Zeitwerk::Cref, { () -> V }) -> V
def get_or_set(cref, &block)
  @mutex.synchronize do
    cnames = (@map[cref.mod] ||= {})
    cnames.fetch(cref.cname) { cnames[cref.cname] = block.call }
  end
end

def initialize

:nodoc: all
feels natural, since crefs are central objects in Zeitwerk's implementation.
based on class and module names, even being synchronized. Also, client code
Finally, I came with this solution which is 1.6x faster than the previous one

and the matching eql?, but that was about 1.8x slower.

real_mod_hash(mod) ^ cname.hash

Another option would be to make crefs hashable. I tried with hash code

The gem used this approach for several years.

{ "M::X" => 0, "M::Y" => 1, "N::Z" => 2 }

module names. In the example above it would be:
We can also use a 1-level hash whose keys are the corresponding class and

https://github.com/fxn/zeitwerk/issues/188

may have been overridden:
class and module objects are not guaranteed to be hashable, the `hash` method
Why not use tables that map pairs [Module, Symbol] to their values? Because

This structure is internal, so only the needed interface is implemented.

{ M => { X: 0, :Y => 1 }, N => { Z: 2 } }

correspond to `M::X`, `M::Y`, and `N::Z`, the map will look like this:
For example, if we store values 0, 1, and 2 for the crefs that would

constant names as symbols. We finally store the values in those.
IDs (see why below). Then, each one of them stores a hash table keyed on
are class and module objects, but their hash code is forced to be their object
It is a synchronized 2-level hash. The keys of the top one, stored in `@map`,

This class emulates a hash table whose keys are of type Zeitwerk::Cref.
def initialize
  @map = {}
  @map.compare_by_identity
  @mutex = Mutex.new
end