enummify 0.1.0 → 0.2.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 16f397e8462e592ee835ac563f64ed9b5f5ae9166b051effe5c397b908ecfdcf
4
- data.tar.gz: d71a579357f34609457fb66c55535aa096a785e78d8361f1b14d96c891330b6d
3
+ metadata.gz: 18c5b92dafe4aae8f25c5389749bbf671cf17facc883ec20da917d3f8750f786
4
+ data.tar.gz: f7becca7d6149d08d7be46e67a541117e11ab892258b16286d9820b50eb48e29
5
5
  SHA512:
6
- metadata.gz: a6529db8dfa09dee22f886333559ba379da3ca7d9940210e117ad8cb5edf69a830915be4b923aea00cc9bfa38723a6c4a0c340e94a412cbdd7246873df652e0a
7
- data.tar.gz: 012263c4cf3610fde962d10315ef6ea487bd00edfcab29fa264e992ac681ae9cfcd54d1cf2169979edfdf4d8aa59451a16a9ce31553c6449bff95eb3e81a1844
6
+ metadata.gz: 55776b3e75d317357a17c8985571e98df46b3c99647de0f4ebc7ec2481a1b35cf68cf567342defcc18bad39b016f0d21231cc94914fa176021bcd5f0024d4792
7
+ data.tar.gz: 0502c8821c91c4157658ebe143ac7936e9a2d542a33d2eeb6feb7c382885c2551457b31eb0bc43fe040d2951c9993cb5c3d7b440de394b3a5f0c2c92bb18f6d2
data/lib/enummify/enum.rb CHANGED
@@ -7,13 +7,15 @@ module Enummify
7
7
  @serialized_to_value = {} #: Hash[String, Enum]
8
8
  @values = nil #: Array[Enum]?
9
9
 
10
- # Register direct constants so only named members belong to the enum.
10
+ # Register each constant as a member, which assumes every constant in the class body is one.
11
+ # Groupings such as sets belong outside the enum class.
11
12
  #: (Symbol) -> void
12
13
  def self.const_added(constant)
13
14
  super
14
15
 
15
- member = const_get(constant, false) #: as Enum
16
- serialized = member.send(:finalize, constant)
16
+ member = const_get(constant, false)
17
+ # The registry size is the member's declaration index, which EnumSet and EnumHash rely on.
18
+ serialized = member.send(:finalize, constant, @serialized_to_value.size) #: as String
17
19
  if @serialized_to_value.key?(serialized)
18
20
  raise ArgumentError, "Duplicate serialized value for #{name}: #{serialized.inspect} is already used"
19
21
  end
@@ -32,6 +34,17 @@ module Enummify
32
34
  alias _load deserialize
33
35
  end
34
36
 
37
+ # Build a set of this enum's members, backed by a bitmask.
38
+ # The attached class cannot appear in a parameter, so the member type is taken from the arguments instead.
39
+ # Mixing enums therefore yields a set of the wrong member type, which is rejected wherever that set is used.
40
+ # At least one member is required, because an empty set has nothing to take the member type from.
41
+ # Use EnumSet.none for that.
42
+ #: [M] (M & Enum, *(M & Enum)) -> EnumSet[M]
43
+ def self.set(member, *members)
44
+ # A set is unordered, so the first member goes on the end of the rest rather than paying to shift them along.
45
+ EnumSet.from(self, members.push(member)) #: as EnumSet[M]
46
+ end
47
+
35
48
  # Look up a member, returning nil for an unknown string.
36
49
  #: (String) -> instance?
37
50
  def self.try_deserialize(serialized)
@@ -59,6 +72,15 @@ module Enummify
59
72
  serialize
60
73
  end
61
74
 
75
+ # The member's bit in EnumSet and EnumHash masks, which is 1 shifted left by the ordinal.
76
+ # It is public only so those classes can read it without a slower private lookup, and is not meant for use
77
+ # outside Enummify.
78
+ # It is computed once because shifting at every use measured slower, both when building a mask and when testing
79
+ # one that is an immediate Integer.
80
+ # Like the ordinal, it changes when members are inserted or reordered.
81
+ #: Integer
82
+ attr_reader :bit
83
+
62
84
  # Enum members are immutable singletons, including copies requested as unfrozen.
63
85
  # rubocop:disable Lint/UnusedMethodArgument
64
86
  #: (?freeze: bool?) -> self
@@ -77,6 +99,13 @@ module Enummify
77
99
  "#<#{self.class.name}: #{serialize.inspect}>"
78
100
  end
79
101
 
102
+ # The member's 0-based position in declaration order, which EnumHash uses as its slot.
103
+ # It is public only so EnumSet and EnumHash can read it without a slower private lookup, and is not meant for use
104
+ # outside Enummify.
105
+ # It changes when members are inserted or reordered, so persist serialize rather than this.
106
+ #: Integer
107
+ attr_reader :ordinal
108
+
80
109
  #: () -> String
81
110
  def serialize
82
111
  @serialized || raise(ArgumentError, 'Enum members must be assigned to a constant before serialization')
@@ -90,8 +119,15 @@ module Enummify
90
119
  private
91
120
 
92
121
  # The constant name is only available after construction has returned.
93
- #: (Symbol) -> String
94
- def finalize(constant)
122
+ # The ordinal and bit must be assigned here rather than by the caller because this method freezes the member.
123
+ # Assigning conditionally leaves an already registered member untouched, so aliasing one reaches the caller's
124
+ # duplicate check rather than failing to write to a frozen member.
125
+ #: (Symbol, Integer) -> String
126
+ def finalize(constant, ordinal)
127
+ unless frozen?
128
+ @ordinal = ordinal
129
+ @bit = 1 << ordinal
130
+ end
95
131
  @serialized ||= constant.name
96
132
  freeze
97
133
  @serialized
@@ -99,6 +135,11 @@ module Enummify
99
135
 
100
136
  #: (?String?) -> void
101
137
  def initialize(serialized = nil)
138
+ # Declaration order and its bit, assigned during registration.
139
+ # They start as Integers rather than nil so the readers need no check, because every EnumSet and EnumHash
140
+ # operation reads one of them.
141
+ @bit = 0 #: Integer
142
+ @ordinal = -1 #: Integer
102
143
  @serialized = serialized&.dup&.freeze #: String?
103
144
  end
104
145
  end
@@ -0,0 +1,330 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Enummify
5
+ # A mutable map keyed by members of a single enum, stored as an array indexed by declaration order.
6
+ #
7
+ # A member's ordinal is its slot, so a lookup is an array index rather than a hash of the key.
8
+ # Keys are held in a bitmask alongside the values, which keeps a stored nil distinct from an absent key and makes
9
+ # the key set free to compute.
10
+ #
11
+ # Keys are checked statically, so nothing is re-checked at runtime.
12
+ # A key of another enum is a type error, and a consumer that defeats the type checker overwrites an unrelated slot.
13
+ #
14
+ # Entries are yielded in declaration order, not insertion order.
15
+ #: [Key, Value]
16
+ class EnumHash
17
+ # The type of NO_DEFAULT, which lets fetch narrow an omitted default away from a given one.
18
+ class NoDefault; end
19
+
20
+ # The default for an omitted fetch default, which is distinct from every value, including nil.
21
+ NO_DEFAULT = NoDefault.new.freeze #: NoDefault
22
+
23
+ private_constant :NO_DEFAULT, :NoDefault
24
+
25
+ # Accept type arguments at runtime without depending on sorbet-runtime.
26
+ # This matches T::Generic#[], which also ignores its arguments and returns self, so a consumer can write
27
+ # EnumHash[Status, Integer] inside a sig.
28
+ #: (*untyped) -> singleton(EnumHash)
29
+ def self.[](*)
30
+ self
31
+ end
32
+
33
+ # Build a map from a Hash whose type is already declared, which infers both the key and the value type.
34
+ # Sorbet cannot solve a type parameter out of a Hash literal, so a literal written at the call site yields an
35
+ # untyped map and its keys go unchecked.
36
+ # EnumHash.of infers from a literal, so prefer it unless the Hash is already typed.
37
+ #: [K < Enummify::Enum, V] (Class[K] & singleton(Enummify::Enum), Hash[K, V]) -> EnumHash[K, V]
38
+ def self.from(enum_class, entries)
39
+ result = new(enum_class) #: EnumHash[K, V]
40
+ result.merge!(entries)
41
+ result
42
+ end
43
+
44
+ # Build a map from key and value pairs, which infers both types even when they are written at the call site.
45
+ #: [K < Enummify::Enum, V] (Class[K] & singleton(Enummify::Enum), *[K & Enummify::Enum, V]) -> EnumHash[K, V]
46
+ def self.of(enum_class, *entries)
47
+ result = new(enum_class) #: EnumHash[K, V]
48
+ entries.each { |member, value| result[member] = value }
49
+ result
50
+ end
51
+
52
+ #: (Key & Enummify::Enum) -> Value?
53
+ def [](member)
54
+ @entries[member.ordinal]
55
+ end
56
+
57
+ # The slot is written first, so a write to a frozen map raises from its frozen slots before anything changes.
58
+ # Writing the presence mask first would raise from the map itself, but measured 6 ns slower per overwrite in the
59
+ # interpreter.
60
+ #: (Key & Enummify::Enum, Value) -> void
61
+ def []=(member, value)
62
+ @entries[member.ordinal] = value
63
+ bit = member.bit
64
+ # Overwriting a present key changes neither the key set nor the size, so both stay cached.
65
+ return if @present & bit != 0
66
+
67
+ @present |= bit
68
+ @size += 1
69
+ @keys = nil
70
+ end
71
+
72
+ # Equality accepts any object, so unlike the keyed operations it checks what it was given.
73
+ #: (untyped) -> bool
74
+ def ==(other)
75
+ same_enum_and_keys?(other) && other.entries == @entries
76
+ end
77
+
78
+ # Remove every entry, keeping the capacity already allocated for this enum.
79
+ #: () -> self
80
+ def clear
81
+ @entries = Array.new(@entries.length)
82
+ @present = 0
83
+ @size = 0
84
+ @keys = nil
85
+ self
86
+ end
87
+
88
+ # Remove an entry and return the value it held, or nil when the key was absent.
89
+ #: (Key & Enummify::Enum) -> Value?
90
+ def delete(member)
91
+ ordinal = member.ordinal
92
+ return nil unless @present[ordinal] == 1
93
+
94
+ value = @entries[ordinal]
95
+ @entries[ordinal] = nil
96
+ # The key is present, so subtracting its bit clears it.
97
+ # Subtracting measured faster than ^ in the interpreter, because subtraction has a specialized instruction and ^
98
+ # is an ordinary method call.
99
+ @present -= member.bit
100
+ @size -= 1
101
+ @keys = nil
102
+ value
103
+ end
104
+
105
+ # Yield each present key and its value in declaration order.
106
+ # A while loop over the cached key array measured faster than a block inside the key array's each, because it
107
+ # skips a block call per entry.
108
+ #: () { (Key & Enummify::Enum, Value) -> void } -> self
109
+ def each(&)
110
+ members = keys.to_a
111
+ entries = @entries
112
+ index = 0
113
+ while index < members.length
114
+ member = members[index] #: as !nil
115
+ # Only present keys are yielded, so the slot holds a value that was written rather than an empty slot.
116
+ value = entries[member.ordinal] #: as Value
117
+ yield(member, value)
118
+ index += 1
119
+ end
120
+ self
121
+ end
122
+
123
+ #: () -> bool
124
+ def empty?
125
+ @present.zero?
126
+ end
127
+
128
+ # Like Hash#eql?, this compares values with eql? rather than ==, so a map holding 1 is not eql? to one holding 1.0.
129
+ #: (untyped) -> bool
130
+ def eql?(other)
131
+ same_enum_and_keys?(other) && other.entries.eql?(@entries)
132
+ end
133
+
134
+ # Return the value for a member.
135
+ # When the key is absent, return the block's result or else the default, and raise KeyError when given neither.
136
+ # As in Hash#fetch, a block takes precedence over a default.
137
+ # Hash#fetch also warns when given both, which the exported RBI's overloads reject statically instead.
138
+ # The optional default measured about 5 ns slower per call in the interpreter whether it defaulted to a constant
139
+ # or to nil, so the cost is the optional parameter itself, and under YJIT it measured within noise.
140
+ #: [D] (Key & Enummify::Enum, ?(D | NoDefault)) ?{ (Key & Enummify::Enum) -> D } -> (Value | D)
141
+ def fetch(member, default = NO_DEFAULT, &block)
142
+ ordinal = member.ordinal
143
+ if @present[ordinal] == 1
144
+ # A present key was written, so the slot holds a value rather than an empty slot.
145
+ value = @entries[ordinal] #: as Value
146
+ return value
147
+ end
148
+ return block.call(member) if block
149
+
150
+ # Matching on the class rather than comparing with NO_DEFAULT narrows the default to its type parameter.
151
+ case default
152
+ when NoDefault then raise KeyError, "key not found: #{member.inspect}"
153
+ else default
154
+ end
155
+ end
156
+
157
+ # Freeze the map, first caching its keys and freezing its slots.
158
+ #: () -> self
159
+ def freeze
160
+ prepare_to_freeze
161
+ super
162
+ end
163
+
164
+ # Maps that are eql? hash alike, so a map can be a Hash key.
165
+ # Like a Hash, a map that changes while it is a key is no longer found under it.
166
+ #: () -> Integer
167
+ def hash
168
+ [@enum_class, @present, @entries].hash
169
+ end
170
+
171
+ #: () -> String
172
+ def inspect
173
+ "#<Enummify::EnumHash[#{@enum_class.name}]: #{to_h.inspect}>"
174
+ end
175
+
176
+ # Masking by the member's bit rather than indexing by ordinal matches EnumSet#include?, for the same reason.
177
+ #: (Key & Enummify::Enum) -> bool
178
+ def key?(member)
179
+ @present & member.bit != 0
180
+ end
181
+
182
+ # Return the present keys as a set, which makes key algebra across two maps a single Integer operation.
183
+ #: () -> EnumSet[Key & Enummify::Enum]
184
+ def keys
185
+ cached = @keys
186
+ return cached if cached
187
+
188
+ built = EnumSet.new(@enum_class, @present)
189
+ # Marshal.load with freeze: true freezes a map without calling freeze, so such a map builds its keys every time.
190
+ @keys = built unless frozen?
191
+ built
192
+ end
193
+
194
+ # A copy starts with this map's slots, size and keys, so only the other entries are stored.
195
+ #: (EnumHash[Key, Value] | Hash[Key, Value]) -> EnumHash[Key, Value]
196
+ def merge(entries)
197
+ dup.merge!(entries)
198
+ end
199
+
200
+ #: (EnumHash[Key, Value] | Hash[Key, Value]) -> self
201
+ def merge!(entries)
202
+ # Another map's slots line up with this map's, so they are copied directly rather than stored entry by entry.
203
+ if entries.is_a?(EnumHash)
204
+ overlay(entries)
205
+ else
206
+ entries.each do |member, value|
207
+ # A key is always an enum member, which a type member on its own does not carry into the body.
208
+ key = member #: as Key & Enummify::Enum
209
+ self[key] = value
210
+ end
211
+ end
212
+ self
213
+ end
214
+
215
+ # The number of present keys, which is counted as keys are added and removed so that reading it walks nothing.
216
+ #: Integer
217
+ attr_reader :size
218
+
219
+ #: () -> Hash[Key, Value]
220
+ def to_h
221
+ result = {} #: Hash[Key, Value]
222
+ each { |member, value| result[member] = value }
223
+ result
224
+ end
225
+
226
+ #: () -> String
227
+ def to_s
228
+ inspect
229
+ end
230
+
231
+ # Return the values of the present keys, in declaration order of their keys.
232
+ #: () -> Array[Value]
233
+ def values
234
+ result = [] #: Array[Value]
235
+ each { |_member, value| result << value }
236
+ result
237
+ end
238
+
239
+ alias each_pair each
240
+ alias has_key? key?
241
+ alias include? key?
242
+ alias length size
243
+ alias member? key?
244
+ alias store []=
245
+
246
+ protected
247
+
248
+ # Exposed to sibling maps so equality can compare state without widening the public API.
249
+ #: Array[Value?]
250
+ attr_reader :entries
251
+
252
+ #: singleton(Enummify::Enum)
253
+ attr_reader :enum_class
254
+
255
+ #: Integer
256
+ attr_reader :present
257
+
258
+ private
259
+
260
+ # new is public only for the reason given at EnumSet#initialize, and is not meant for use outside Enummify.
261
+ # The capacity is read once, which is sound because members are declared in the class body and never added later.
262
+ #: (singleton(Enummify::Enum)) -> void
263
+ def initialize(enum_class)
264
+ @enum_class = enum_class
265
+ @entries = Array.new(enum_class.values.length) #: Array[Value?]
266
+ @present = 0 #: Integer
267
+ @size = 0 #: Integer
268
+ @keys = nil #: EnumSet[Key & Enummify::Enum]?
269
+ end
270
+
271
+ # Kernel#clone freezes the copy without calling freeze, so a copy that will be frozen prepares for it here.
272
+ #: (EnumHash[Key, Value], ?freeze: bool?) -> void
273
+ def initialize_clone(source, freeze: nil)
274
+ super
275
+ prepare_to_freeze if freeze.nil? ? source.frozen? : freeze
276
+ end
277
+
278
+ # A copy gets its own slots, so writing to it leaves the source alone.
279
+ #: (EnumHash[Key, Value]) -> void
280
+ def initialize_copy(source)
281
+ super
282
+ @entries = @entries.dup
283
+ end
284
+
285
+ # Copy another map's present slots over this map's, then take the union of both key sets.
286
+ # A while loop over the other map's cached key array measured faster than storing its entries one by one.
287
+ # Counting the added keys while copying replaced counting the bits of a newly built key set.
288
+ # It measured about a quarter faster for 8 members, and for larger enums a tenth faster under YJIT but up to 5%
289
+ # slower in the interpreter.
290
+ # The slots are written first, so a frozen map raises from its frozen slots before its keys change.
291
+ #: (EnumHash[Key, Value]) -> void
292
+ def overlay(other)
293
+ entries = @entries
294
+ other_entries = other.entries
295
+ present = @present
296
+ size = @size
297
+ members = other.keys.to_a
298
+ index = 0
299
+ while index < members.length
300
+ member = members[index] #: as !nil
301
+ ordinal = member.ordinal
302
+ entries[ordinal] = other_entries[ordinal]
303
+ size += 1 unless present[ordinal] == 1
304
+ index += 1
305
+ end
306
+ return if size == @size
307
+
308
+ @present = present | other.present
309
+ @size = size
310
+ @keys = nil
311
+ end
312
+
313
+ # Cache the keys and freeze the slots, which a frozen map could no longer do.
314
+ # A write stores its value in a slot before it changes the map itself, so frozen slots are what make a write to a
315
+ # frozen map raise before anything changes.
316
+ #: () -> void
317
+ def prepare_to_freeze
318
+ keys
319
+ @entries.freeze
320
+ end
321
+
322
+ # Check that another object is a map over the same enum with the same keys, which both == and eql? require.
323
+ #: (untyped) -> bool
324
+ def same_enum_and_keys?(other)
325
+ return false unless other.is_a?(EnumHash)
326
+
327
+ other.enum_class.equal?(@enum_class) && other.present == @present
328
+ end
329
+ end
330
+ end
@@ -0,0 +1,314 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Enummify
5
+ # An immutable set of members of a single enum, stored as one Integer bitmask.
6
+ #
7
+ # Each member occupies the bit at its declaration index, so union, intersection, difference and subset tests are
8
+ # single Integer operations rather than hash walks.
9
+ # A mask for an enum with 62 or fewer members is an immediate value, so such a set holds no heap-allocated storage.
10
+ #
11
+ # Operations take a set of the same enum and are checked statically, so nothing is re-checked at runtime.
12
+ # Two enums number their members independently, so combining sets of different enums is a type error, and a
13
+ # consumer that defeats the type checker gets a meaningless result rather than an exception.
14
+ #
15
+ # Sets are immutable: every operation returns a new set.
16
+ # They are not frozen, because the member array and size are computed on first use and cached.
17
+ # Materializing eagerly instead would cost far more than the bitmask saves.
18
+ # Freezing a set caches both first, so a frozen set reads them as quickly as one that is not frozen.
19
+ #
20
+ # The type parameter is named Elem so that including Enumerable supplies map, select and friends with the member
21
+ # type already bound.
22
+ #: [Elem]
23
+ class EnumSet
24
+ include Enumerable
25
+
26
+ # Accept type arguments at runtime without depending on sorbet-runtime.
27
+ # This matches T::Generic#[], which also ignores its arguments and returns self, so a consumer can write
28
+ # EnumSet[Status] inside a sig.
29
+ #: (*untyped) -> singleton(EnumSet)
30
+ def self.[](*)
31
+ self
32
+ end
33
+
34
+ # Build the set of every member of the enum.
35
+ #: [M < Enummify::Enum] (Class[M] & singleton(Enummify::Enum)) -> EnumSet[M]
36
+ def self.all(enum_class)
37
+ new(enum_class, (1 << enum_class.values.length) - 1)
38
+ end
39
+
40
+ # Build a set from a collection of members, for callers that already have one.
41
+ # Enumerable is covariant in its element, so an Array of a specific member type is accepted here.
42
+ #: [M < Enummify::Enum] (Class[M] & singleton(Enummify::Enum), Enumerable[M & Enummify::Enum]) -> EnumSet[M]
43
+ def self.from(enum_class, members)
44
+ new(enum_class, mask_for(members))
45
+ end
46
+
47
+ # Build the empty set for the enum.
48
+ #: [M < Enummify::Enum] (Class[M] & singleton(Enummify::Enum)) -> EnumSet[M]
49
+ def self.none(enum_class)
50
+ new(enum_class, 0)
51
+ end
52
+
53
+ # Build a set from the given members.
54
+ #: [M < Enummify::Enum] (Class[M] & singleton(Enummify::Enum), *(M & Enummify::Enum)) -> EnumSet[M]
55
+ def self.of(enum_class, *members)
56
+ new(enum_class, mask_for(members))
57
+ end
58
+
59
+ # Fold members into a bitmask.
60
+ #: (Enumerable[Enummify::Enum]) -> Integer
61
+ def self.mask_for(members)
62
+ mask = 0
63
+ members.each do |member|
64
+ mask |= member.bit
65
+ end
66
+ mask
67
+ end
68
+
69
+ private_class_method :mask_for
70
+
71
+ #: (EnumSet[Elem]) -> EnumSet[Elem]
72
+ def &(other)
73
+ derive(@mask & other.mask)
74
+ end
75
+
76
+ #: (EnumSet[Elem]) -> EnumSet[Elem]
77
+ def +(other)
78
+ derive(@mask | other.mask)
79
+ end
80
+
81
+ #: (EnumSet[Elem]) -> EnumSet[Elem]
82
+ def -(other)
83
+ derive(@mask & ~other.mask)
84
+ end
85
+
86
+ # Equality accepts any object, so unlike the set operations it checks what it was given.
87
+ #: (untyped) -> bool
88
+ def ==(other)
89
+ return false unless other.is_a?(EnumSet)
90
+
91
+ other.enum_class.equal?(@enum_class) && other.mask == @mask
92
+ end
93
+
94
+ # Support `case member when SOME_SET`.
95
+ #: (untyped) -> bool
96
+ def ===(member)
97
+ include?(member)
98
+ end
99
+
100
+ #: (EnumSet[Elem]) -> EnumSet[Elem]
101
+ def ^(other)
102
+ derive(@mask ^ other.mask)
103
+ end
104
+
105
+ #: (EnumSet[Elem]) -> EnumSet[Elem]
106
+ def |(other)
107
+ derive(@mask | other.mask)
108
+ end
109
+
110
+ # Return the set of this enum's members that this set does not contain.
111
+ #: () -> EnumSet[Elem]
112
+ def ~
113
+ derive(((1 << @enum_class.values.length) - 1) ^ @mask)
114
+ end
115
+
116
+ # Return a set that also contains the member.
117
+ #: (Elem & Enummify::Enum) -> EnumSet[Elem]
118
+ def add(member)
119
+ derive(@mask | member.bit)
120
+ end
121
+
122
+ # Return a set without the member.
123
+ #: (Elem & Enummify::Enum) -> EnumSet[Elem]
124
+ def delete(member)
125
+ derive(@mask & ~member.bit)
126
+ end
127
+
128
+ #: (EnumSet[Elem]) -> EnumSet[Elem]
129
+ def difference(other)
130
+ derive(@mask & ~other.mask)
131
+ end
132
+
133
+ #: (EnumSet[Elem]) -> bool
134
+ def disjoint?(other)
135
+ (@mask & other.mask).zero?
136
+ end
137
+
138
+ # Reading the cached members directly rather than through to_a measured faster in the interpreter, because it
139
+ # skips two method calls.
140
+ # @override
141
+ #: () { (Elem) -> void } -> self
142
+ def each(&)
143
+ cached = @members || members #: as Array[Elem]
144
+ cached.each(&)
145
+ self
146
+ end
147
+
148
+ #: () -> bool
149
+ def empty?
150
+ @mask.zero?
151
+ end
152
+
153
+ #: (untyped) -> bool
154
+ def eql?(other)
155
+ self == other
156
+ end
157
+
158
+ # Freeze the set, first caching its members and size.
159
+ #: () -> self
160
+ def freeze
161
+ members
162
+ size
163
+ super
164
+ end
165
+
166
+ #: () -> Integer
167
+ def hash
168
+ [@enum_class, @mask].hash
169
+ end
170
+
171
+ # Masking by the member's bit measured faster than Set#include? for every enum of 62 or fewer members, whose masks
172
+ # are immediate Integers, and indexing the mask by ordinal did not in the interpreter.
173
+ # Enumerable#include? takes any member type, so narrowing the parameter is declared incompatible rather than cast
174
+ # inside, because the local that a cast needs measured 7 ns slower in the interpreter.
175
+ # Enums of more than 62 members are not a performance target, so masking is kept even though masking their Bignum
176
+ # masks allocates, which indexing by ordinal would not.
177
+ # @override(allow_incompatible: true)
178
+ #: (Elem & Enummify::Enum) -> bool
179
+ def include?(member)
180
+ @mask & member.bit != 0
181
+ end
182
+
183
+ #: () -> String
184
+ def inspect
185
+ "#<Enummify::EnumSet[#{@enum_class.name}]: #{members.map(&:serialize).inspect}>"
186
+ end
187
+
188
+ #: (EnumSet[Elem]) -> bool
189
+ def intersect?(other)
190
+ !(@mask & other.mask).zero?
191
+ end
192
+
193
+ #: (EnumSet[Elem]) -> EnumSet[Elem]
194
+ def intersection(other)
195
+ derive(@mask & other.mask)
196
+ end
197
+
198
+ # Mapping the cached members directly measured faster than Enumerable#map, which yields every member through each.
199
+ # Called without a block, Array#map returns an Enumerator just as Enumerable#map does.
200
+ # Sorbet allows overloads only in RBI files, so the blockless form is typed by Enumerable#map in the exported RBI.
201
+ # @override
202
+ #: [U] () { (Elem) -> U } -> Array[U]
203
+ def map(&)
204
+ cached = @members || members #: as Array[Elem]
205
+ cached.map(&)
206
+ end
207
+
208
+ #: () -> Integer
209
+ def size
210
+ @size || count_members
211
+ end
212
+
213
+ #: (EnumSet[Elem]) -> bool
214
+ def subset?(other)
215
+ @mask & other.mask == @mask
216
+ end
217
+
218
+ #: (EnumSet[Elem]) -> bool
219
+ def superset?(other)
220
+ mask = other.mask
221
+ @mask & mask == mask
222
+ end
223
+
224
+ # @override
225
+ #: () -> Array[Elem]
226
+ def to_a
227
+ members #: as Array[Elem]
228
+ end
229
+
230
+ #: () -> String
231
+ def to_s
232
+ inspect
233
+ end
234
+
235
+ #: (EnumSet[Elem]) -> EnumSet[Elem]
236
+ def union(other)
237
+ derive(@mask | other.mask)
238
+ end
239
+
240
+ alias collect map
241
+ alias complement ~
242
+ alias length size
243
+ alias member? include?
244
+
245
+ protected
246
+
247
+ # Exposed to sibling sets so operations can read the other side without widening the public API.
248
+ #: singleton(Enummify::Enum)
249
+ attr_reader :enum_class
250
+
251
+ #: Integer
252
+ attr_reader :mask
253
+
254
+ private
255
+
256
+ # Count the members and cache the count.
257
+ # Ruby has no popcount. Counting "1" in the binary representation measured fastest for small and large masks.
258
+ #: () -> Integer
259
+ def count_members
260
+ counted = @mask.to_s(2).count('1')
261
+ # Kernel#clone and Marshal.load with freeze: true freeze a set without calling freeze, so such a set counts every
262
+ # time.
263
+ return counted if frozen?
264
+
265
+ @size = counted
266
+ end
267
+
268
+ # Build a sibling set over the same enum.
269
+ #: (Integer) -> EnumSet[Elem]
270
+ def derive(mask)
271
+ EnumSet.new(@enum_class, mask)
272
+ end
273
+
274
+ # new is public only because hiding it with private_class_method adds a visibility override that Ruby 4.0's
275
+ # opt_new instruction does not recognize.
276
+ # Every new set then took the slow path, which measured about twice as slow in the interpreter and three times as
277
+ # slow under YJIT.
278
+ # It is not meant for use outside Enummify, and the exported RBI declares no initialize, so Sorbet rejects a
279
+ # consumer's call to new that passes it an enum and a mask.
280
+ #: (singleton(Enummify::Enum), Integer) -> void
281
+ def initialize(enum_class, mask)
282
+ @enum_class = enum_class
283
+ @mask = mask
284
+ @members = nil #: Array[Enummify::Enum]?
285
+ @size = nil #: Integer?
286
+ end
287
+
288
+ # Materialize the members once and cache them.
289
+ # Walking the mask is slower than iterating an Array, so every iteration after the first reads the cache.
290
+ # Extracting the lowest set bit costs one step per present member and yields declaration order.
291
+ # Negating as 0 - remaining and clearing by subtraction measured faster than -remaining and ^, because binary
292
+ # minus has a specialized instruction while unary minus and ^ are ordinary method calls.
293
+ #: () -> Array[Enummify::Enum]
294
+ def members
295
+ cached = @members
296
+ return cached if cached
297
+
298
+ materialized = []
299
+ values = @enum_class.values
300
+ remaining = @mask
301
+ while remaining != 0
302
+ lowest = remaining & (0 - remaining)
303
+ materialized << values[lowest.bit_length - 1]
304
+ remaining -= lowest
305
+ end
306
+ materialized.freeze
307
+ # Kernel#clone and Marshal.load with freeze: true freeze a set without calling freeze, so such a set materializes
308
+ # every time.
309
+ return materialized if frozen?
310
+
311
+ @members = materialized
312
+ end
313
+ end
314
+ end
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Enummify
5
- VERSION = '0.1.0'
5
+ VERSION = '0.2.0'
6
6
  end
data/lib/enummify.rb CHANGED
@@ -2,4 +2,6 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require_relative 'enummify/enum'
5
+ require_relative 'enummify/enum_hash'
6
+ require_relative 'enummify/enum_set'
5
7
  require_relative 'enummify/version'
data/rbi/enummify.rbi CHANGED
@@ -13,6 +13,16 @@ module Enummify
13
13
  alias _load deserialize
14
14
  end
15
15
 
16
+ sig do
17
+ type_parameters(:M)
18
+ .params(
19
+ member: T.all(T.type_parameter(:M), Enummify::Enum),
20
+ members: T.all(T.type_parameter(:M), Enummify::Enum)
21
+ )
22
+ .returns(EnumSet[T.type_parameter(:M)])
23
+ end
24
+ def self.set(member, *members); end
25
+
16
26
  sig { params(serialized: String).returns(T.nilable(T.attached_class)) }
17
27
  def self.try_deserialize(serialized); end
18
28
 
@@ -22,6 +32,11 @@ module Enummify
22
32
  sig { params(_depth: Integer).returns(String) }
23
33
  def _dump(_depth); end
24
34
 
35
+ # The member's bit in EnumSet and EnumHash masks.
36
+ # It is public only so those classes can read it quickly, and is not meant for use outside Enummify.
37
+ sig { returns(Integer) }
38
+ def bit; end
39
+
25
40
  sig { params(freeze: T.nilable(T::Boolean)).returns(T.self_type) }
26
41
  def clone(freeze: true); end
27
42
 
@@ -31,6 +46,11 @@ module Enummify
31
46
  sig { returns(String) }
32
47
  def inspect; end
33
48
 
49
+ # The member's 0-based position in declaration order.
50
+ # It is public only so EnumSet and EnumHash can read it quickly, and is not meant for use outside Enummify.
51
+ sig { returns(Integer) }
52
+ def ordinal; end
53
+
34
54
  sig { returns(String) }
35
55
  def serialize; end
36
56
 
@@ -42,4 +62,240 @@ module Enummify
42
62
  sig { params(serialized: T.nilable(String)).void }
43
63
  def initialize(serialized = nil); end
44
64
  end
65
+
66
+ # A mutable map keyed by members of a single enum, stored as an array indexed by declaration order.
67
+ # new is public at runtime only for speed, and initialize is deliberately not declared here, so Sorbet rejects a
68
+ # call to new with arguments.
69
+ class EnumHash
70
+ extend T::Generic
71
+ extend T::Sig
72
+
73
+ Key = type_member
74
+ Value = type_member
75
+
76
+ sig do
77
+ type_parameters(:K, :V)
78
+ .params(
79
+ enum_class: T.all(T::Class[T.type_parameter(:K)], T.class_of(Enummify::Enum)),
80
+ entries: T::Hash[T.type_parameter(:K), T.type_parameter(:V)]
81
+ )
82
+ .returns(EnumHash[T.type_parameter(:K), T.type_parameter(:V)])
83
+ end
84
+ def self.from(enum_class, entries); end
85
+
86
+ sig do
87
+ type_parameters(:K, :V)
88
+ .params(
89
+ enum_class: T.all(T::Class[T.type_parameter(:K)], T.class_of(Enummify::Enum)),
90
+ entries: [T.all(T.type_parameter(:K), Enummify::Enum), T.type_parameter(:V)]
91
+ )
92
+ .returns(EnumHash[T.type_parameter(:K), T.type_parameter(:V)])
93
+ end
94
+ def self.of(enum_class, *entries); end
95
+
96
+ sig { params(member: T.all(Key, Enummify::Enum)).returns(T.nilable(Value)) }
97
+ def [](member); end
98
+
99
+ sig { params(member: T.all(Key, Enummify::Enum), value: Value).void }
100
+ def []=(member, value); end
101
+
102
+ sig { params(other: T.untyped).returns(T::Boolean) }
103
+ def ==(other); end
104
+
105
+ sig { returns(T.self_type) }
106
+ def clear; end
107
+
108
+ sig { params(member: T.all(Key, Enummify::Enum)).returns(T.nilable(Value)) }
109
+ def delete(member); end
110
+
111
+ sig { params(block: T.proc.params(member: T.all(Key, Enummify::Enum), value: Value).void).returns(T.self_type) }
112
+ def each(&block); end
113
+
114
+ sig { returns(T::Boolean) }
115
+ def empty?; end
116
+
117
+ sig { params(other: T.untyped).returns(T::Boolean) }
118
+ def eql?(other); end
119
+
120
+ # The overloads mirror Hash#fetch, and accept a default or a block but not both.
121
+ sig { params(member: T.all(Key, Enummify::Enum)).returns(Value) }
122
+ sig do
123
+ type_parameters(:D)
124
+ .params(member: T.all(Key, Enummify::Enum), default: T.type_parameter(:D))
125
+ .returns(T.any(Value, T.type_parameter(:D)))
126
+ end
127
+ sig do
128
+ type_parameters(:D)
129
+ .params(
130
+ member: T.all(Key, Enummify::Enum),
131
+ block: T.proc.params(member: T.all(Key, Enummify::Enum)).returns(T.type_parameter(:D))
132
+ )
133
+ .returns(T.any(Value, T.type_parameter(:D)))
134
+ end
135
+ def fetch(member, default = T.unsafe(nil), &block); end
136
+
137
+ sig { returns(Integer) }
138
+ def hash; end
139
+
140
+ sig { returns(String) }
141
+ def inspect; end
142
+
143
+ sig { params(member: T.all(Key, Enummify::Enum)).returns(T::Boolean) }
144
+ def key?(member); end
145
+
146
+ sig { returns(EnumSet[T.all(Key, Enummify::Enum)]) }
147
+ def keys; end
148
+
149
+ sig { params(entries: T.any(EnumHash[Key, Value], T::Hash[Key, Value])).returns(EnumHash[Key, Value]) }
150
+ def merge(entries); end
151
+
152
+ sig { params(entries: T.any(EnumHash[Key, Value], T::Hash[Key, Value])).returns(T.self_type) }
153
+ def merge!(entries); end
154
+
155
+ sig { returns(Integer) }
156
+ def size; end
157
+
158
+ sig { returns(T::Hash[Key, Value]) }
159
+ def to_h; end
160
+
161
+ sig { returns(String) }
162
+ def to_s; end
163
+
164
+ sig { returns(T::Array[Value]) }
165
+ def values; end
166
+
167
+ alias each_pair each
168
+ alias has_key? key?
169
+ alias include? key?
170
+ alias length size
171
+ alias member? key?
172
+ alias store []=
173
+ end
174
+
175
+ # An immutable set of members of a single enum, stored as one Integer bitmask.
176
+ # new is public at runtime only for speed, and initialize is deliberately not declared here, so Sorbet rejects a
177
+ # call to new with arguments.
178
+ class EnumSet
179
+ extend T::Generic
180
+ extend T::Sig
181
+ include Enumerable
182
+
183
+ Elem = type_member
184
+
185
+ sig do
186
+ type_parameters(:M)
187
+ .params(enum_class: T.all(T::Class[T.type_parameter(:M)], T.class_of(Enummify::Enum)))
188
+ .returns(EnumSet[T.type_parameter(:M)])
189
+ end
190
+ def self.all(enum_class); end
191
+
192
+ sig do
193
+ type_parameters(:M)
194
+ .params(
195
+ enum_class: T.all(T::Class[T.type_parameter(:M)], T.class_of(Enummify::Enum)),
196
+ members: T::Enumerable[T.all(T.type_parameter(:M), Enummify::Enum)]
197
+ )
198
+ .returns(EnumSet[T.type_parameter(:M)])
199
+ end
200
+ def self.from(enum_class, members); end
201
+
202
+ sig do
203
+ type_parameters(:M)
204
+ .params(enum_class: T.all(T::Class[T.type_parameter(:M)], T.class_of(Enummify::Enum)))
205
+ .returns(EnumSet[T.type_parameter(:M)])
206
+ end
207
+ def self.none(enum_class); end
208
+
209
+ sig do
210
+ type_parameters(:M)
211
+ .params(
212
+ enum_class: T.all(T::Class[T.type_parameter(:M)], T.class_of(Enummify::Enum)),
213
+ members: T.all(T.type_parameter(:M), Enummify::Enum)
214
+ )
215
+ .returns(EnumSet[T.type_parameter(:M)])
216
+ end
217
+ def self.of(enum_class, *members); end
218
+
219
+ sig { params(other: EnumSet[Elem]).returns(EnumSet[Elem]) }
220
+ def &(other); end
221
+
222
+ sig { params(other: EnumSet[Elem]).returns(EnumSet[Elem]) }
223
+ def +(other); end
224
+
225
+ sig { params(other: EnumSet[Elem]).returns(EnumSet[Elem]) }
226
+ def -(other); end
227
+
228
+ sig { params(other: T.untyped).returns(T::Boolean) }
229
+ def ==(other); end
230
+
231
+ sig { params(member: T.untyped).returns(T::Boolean) }
232
+ def ===(member); end
233
+
234
+ sig { params(other: EnumSet[Elem]).returns(EnumSet[Elem]) }
235
+ def ^(other); end
236
+
237
+ sig { params(other: EnumSet[Elem]).returns(EnumSet[Elem]) }
238
+ def |(other); end
239
+
240
+ sig { returns(EnumSet[Elem]) }
241
+ def ~; end
242
+
243
+ sig { params(member: T.all(Elem, Enummify::Enum)).returns(EnumSet[Elem]) }
244
+ def add(member); end
245
+
246
+ sig { params(member: T.all(Elem, Enummify::Enum)).returns(EnumSet[Elem]) }
247
+ def delete(member); end
248
+
249
+ sig { params(other: EnumSet[Elem]).returns(EnumSet[Elem]) }
250
+ def difference(other); end
251
+
252
+ sig { params(other: EnumSet[Elem]).returns(T::Boolean) }
253
+ def disjoint?(other); end
254
+
255
+ sig { params(block: T.proc.params(member: Elem).void).returns(T.self_type) }
256
+ def each(&block); end
257
+
258
+ sig { returns(T::Boolean) }
259
+ def empty?; end
260
+
261
+ sig { params(other: T.untyped).returns(T::Boolean) }
262
+ def eql?(other); end
263
+
264
+ sig { returns(Integer) }
265
+ def hash; end
266
+
267
+ sig { params(member: Elem).returns(T::Boolean) }
268
+ def include?(member); end
269
+
270
+ sig { returns(String) }
271
+ def inspect; end
272
+
273
+ sig { params(other: EnumSet[Elem]).returns(T::Boolean) }
274
+ def intersect?(other); end
275
+
276
+ sig { params(other: EnumSet[Elem]).returns(EnumSet[Elem]) }
277
+ def intersection(other); end
278
+
279
+ sig { returns(Integer) }
280
+ def size; end
281
+
282
+ sig { params(other: EnumSet[Elem]).returns(T::Boolean) }
283
+ def subset?(other); end
284
+
285
+ sig { params(other: EnumSet[Elem]).returns(T::Boolean) }
286
+ def superset?(other); end
287
+
288
+ sig { returns(T::Array[Elem]) }
289
+ def to_a; end
290
+
291
+ sig { returns(String) }
292
+ def to_s; end
293
+
294
+ sig { params(other: EnumSet[Elem]).returns(EnumSet[Elem]) }
295
+ def union(other); end
296
+
297
+ alias complement ~
298
+ alias length size
299
+ alias member? include?
300
+ end
45
301
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: enummify
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin D. Harris
@@ -58,6 +58,8 @@ extra_rdoc_files: []
58
58
  files:
59
59
  - lib/enummify.rb
60
60
  - lib/enummify/enum.rb
61
+ - lib/enummify/enum_hash.rb
62
+ - lib/enummify/enum_set.rb
61
63
  - lib/enummify/version.rb
62
64
  - rbi/enummify.rbi
63
65
  homepage: https://github.com/juharris/enummify
@@ -80,7 +82,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
80
82
  - !ruby/object:Gem::Version
81
83
  version: '0'
82
84
  requirements: []
83
- rubygems_version: 4.0.16
85
+ rubygems_version: 4.0.20
84
86
  specification_version: 4
85
87
  summary: Typed Ruby enums using RBS.
86
88
  test_files: []