Skip to content

Commit

Permalink
Docs improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
fxn committed Feb 15, 2025
1 parent 85a9b97 commit 177ab23
Show file tree
Hide file tree
Showing 3 changed files with 35 additions and 16 deletions.
49 changes: 33 additions & 16 deletions lib/zeitwerk/cref/map.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,59 +2,71 @@

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

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

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

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

# Ad-hoc for loader_for, called from const_added. That is a hot path, I prefer
# to not create a cref in every call, since that is global.
#
# @sig (Module, Symbol) -> Object?
def delete_mod_cname(mod, cname)
@mutex.synchronize do
if cnames = @map[mod]
Expand All @@ -65,6 +77,7 @@ def delete_mod_cname(mod, cname)
end
end

# @sig (Object) -> void
def delete_by_value(value)
@mutex.synchronize do
@map.delete_if do |mod, cnames|
Expand All @@ -74,7 +87,9 @@ def delete_by_value(value)
end
end

# Order is undefined.
# Order of yielded crefs is undefined.
#
# @sig () { () -> Zeitwerk::Cref } -> void
def each_key
@mutex.synchronize do
@map.each do |mod, cnames|
Expand All @@ -85,13 +100,15 @@ def each_key
end
end

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

def empty?
# @sig () -> bool
def empty? # for tests
@mutex.synchronize do
@map.empty?
end
Expand Down
1 change: 1 addition & 0 deletions lib/zeitwerk/internal.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

# This is a private module.
module Zeitwerk::Internal
# @sig (Symbol) -> void
def internal(method_name)
private method_name

Expand Down
1 change: 1 addition & 0 deletions lib/zeitwerk/null_inflector.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
class Zeitwerk::NullInflector
# @sig (String, String) -> String
def camelize(basename, _abspath)
basename
end
Expand Down

0 comments on commit 177ab23

Please sign in to comment.