File: map.rb

package info (click to toggle)
ruby-zeitwerk 2.7.3-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 732 kB
  • sloc: ruby: 6,240; makefile: 4
file content (159 lines) | stat: -rw-r--r-- 3,846 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# frozen_string_literal: true

# Description of the structure
# ----------------------------
#
# This class emulates a hash table whose keys are of type Zeitwerk::Cref.
#
# 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 because class and module
# objects may not be hashable (https://github.com/fxn/zeitwerk/issues/188).
#
# Then, each one of them stores a hash table with their constants and values.
# Constants are stored as symbols.
#
# 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 } }
#
# This structure is internal, so only the needed interface is implemented.
#
# Alternative approaches
# -----------------------
#
# 1. We could also use a 1-level hash whose keys are constant paths. In the
#    example above it would be:
#
#      { "M::X" => 0, "M::Y" => 1, "N::Z" => 2 }
#
#    The gem used this approach for several years.
#
#  2. Write a custom `hash`/`eql?` in Zeitwerk::Cref. Hash code would be
#
#       real_mod_hash(@mod) ^ @cname.hash
#
#     where `real_mod_hash(@mod)` would actually be a call to the real `hash`
#     method in Module. Like what we do for module names to bypass overrides.
#
#  3. Similar to 2, but use
#
#       @mod.object_id ^ @cname.object_id
#
#     as hash code instead.
#
# Benchamrks
# ----------
#
# Writing:
#
#   map - baseline
#   (3) - 1.74x  slower
#   (2) - 2.91x  slower
#   (1) - 3.87x  slower
#
# Reading:
#
#   map - baseline
#   (3) - 1.99x  slower
#   (2) - 2.80x  slower
#   (1) - 3.48x  slower
#
# Extra ball
# ----------
#
# In addition to that, the map is synchronized and provides `delete_mod_cname`,
# which is ad-hoc for the hot path in `const_added`, we do not need to create
# unnecessary cref objects for constants we do not manage (but we do not know in
# advance there).

#: [Value]
class Zeitwerk::Cref::Map # :nodoc: all
  #: () -> void
  def initialize
    @map = {}
    @map.compare_by_identity
    @mutex = Mutex.new
  end

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

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

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

  #: (Zeitwerk::Cref) -> Value?
  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.
  #
  #: (Module, Symbol) -> Value?
  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

  #: (Value) -> 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

  # Order of yielded crefs is undefined.
  #
  #: () { (Zeitwerk::Cref) -> void } -> void
  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

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

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