farce 0.4.1-aarch64-linux-gnu → 0.6.0-aarch64-linux-gnu

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.
Files changed (143) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +98 -2
  3. data/README.md +3 -1
  4. data/docs/benchmarks.md +37 -21
  5. data/docs/compatibility.md +13 -0
  6. data/docs/data-structures.md +37 -0
  7. data/docs/integrations.md +1 -0
  8. data/docs/introduction.md +19 -0
  9. data/docs/modes.md +2 -0
  10. data/docs/scheduling.md +35 -3
  11. data/docs/scopes.md +3 -1
  12. data/docs/shims.md +53 -0
  13. data/docs/utilities.md +32 -0
  14. data/docs/variants.md +9 -0
  15. data/ext/farce/atom.c +23 -7
  16. data/ext/farce/containers.h +3 -0
  17. data/ext/farce/counter.c +4 -6
  18. data/ext/farce/depend +3 -1
  19. data/ext/farce/extconf.rb +19 -14
  20. data/ext/farce/farce.c +110 -3
  21. data/ext/farce/map.c +15 -7
  22. data/ext/farce/priority_queue.c +46 -1
  23. data/ext/farce/queue.c +141 -7
  24. data/ext/farce/trie.c +140 -72
  25. data/ext/farce/trie_search.h +114 -0
  26. data/ext/farce/vector.c +53 -21
  27. data/ext/farce/weak_map.c +16 -11
  28. data/ext/farce/weak_references.h +23 -0
  29. data/ext/java/org/farce/FiberScheduler.java +8 -6
  30. data/ext/java/org/farce/PriorityQueue.java +18 -2
  31. data/lib/farce/abstract/atom.rb +5 -1
  32. data/lib/farce/abstract/concurrent_map.rb +122 -0
  33. data/lib/farce/abstract/counter.rb +36 -1
  34. data/lib/farce/abstract/executor.rb +30 -7
  35. data/lib/farce/abstract/flag.rb +12 -0
  36. data/lib/farce/abstract/molecule.rb +14 -14
  37. data/lib/farce/abstract/pool.rb +158 -47
  38. data/lib/farce/abstract/priority_queue.rb +1 -1
  39. data/lib/farce/abstract/publisher.rb +189 -0
  40. data/lib/farce/abstract/queue.rb +29 -3
  41. data/lib/farce/abstract/scheduler.rb +41 -14
  42. data/lib/farce/abstract/timer_queue.rb +3 -3
  43. data/lib/farce/abstract/vector.rb +3 -1
  44. data/lib/farce/atom.rb +15 -2
  45. data/lib/farce/clock.rb +20 -7
  46. data/lib/farce/concurrent/count_down_latch.rb +37 -0
  47. data/lib/farce/concurrent/cyclic_barrier.rb +109 -0
  48. data/lib/farce/concurrent/event.rb +60 -0
  49. data/lib/farce/concurrent/fiber_local_var.rb +31 -0
  50. data/lib/farce/concurrent/local_var.rb +65 -0
  51. data/lib/farce/concurrent/thread_local_var.rb +28 -0
  52. data/lib/farce/concurrent/tuple.rb +74 -0
  53. data/lib/farce/concurrent.rb +50 -0
  54. data/lib/farce/config.rb +112 -2
  55. data/lib/farce/counter.rb +5 -15
  56. data/lib/farce/deduper.rb +12 -4
  57. data/lib/farce/engine/jruby/fiber_scheduler.jar +0 -0
  58. data/lib/farce/engine/jruby/map.rb +8 -6
  59. data/lib/farce/engine/jruby/signal.rb +2 -2
  60. data/lib/farce/engine/jvm/counter.rb +0 -2
  61. data/lib/farce/engine/jvm/farce.jar +0 -0
  62. data/lib/farce/engine/jvm/priority_queue.rb +14 -4
  63. data/lib/farce/engine/ruby/3.4/farce.so +0 -0
  64. data/lib/farce/engine/ruby/4.0/farce.so +0 -0
  65. data/lib/farce/engine/ruby/shared/parallel_scheduler.rb +9 -1
  66. data/lib/farce/engine/ruby/shared/ractor_selector.rb +1 -4
  67. data/lib/farce/engine/ruby/shared/weak_cache_mirror.rb +56 -0
  68. data/lib/farce/engine/ruby/shared/weak_map.rb +9 -12
  69. data/lib/farce/engine/ruby.rb +5 -1
  70. data/lib/farce/engine/shared/exchanger.rb +2 -2
  71. data/lib/farce/engine/shared/queue.rb +43 -5
  72. data/lib/farce/engine/shared/ractor_selector.rb +1 -1
  73. data/lib/farce/engine/shared/strict_map.rb +5 -0
  74. data/lib/farce/engine/shared/trie/ignore_case.rb +136 -0
  75. data/lib/farce/engine/shared/trie.rb +14 -1
  76. data/lib/farce/engine/shared/trie_builder.rb +19 -8
  77. data/lib/farce/engine/shared/unshared_atom.rb +1 -1
  78. data/lib/farce/engine/shared/unshared_vector.rb +11 -8
  79. data/lib/farce/engine/shared/unshared_weak_map.rb +8 -6
  80. data/lib/farce/engine/shared/vector.rb +2 -0
  81. data/lib/farce/engine/shared/weak_atom/base.rb +5 -10
  82. data/lib/farce/engine/shared/weak_map/cell.rb +1 -1
  83. data/lib/farce/engine/shared.rb +2 -0
  84. data/lib/farce/engine/truffleruby/map.rb +8 -6
  85. data/lib/farce/engine/truffleruby/native/counter.rb +0 -2
  86. data/lib/farce/engine/truffleruby/native/priority_queue.rb +18 -4
  87. data/lib/farce/engine/truffleruby/signal.rb +1 -1
  88. data/lib/farce/engine/truffleruby.rb +9 -0
  89. data/lib/farce/extension.rb +5 -3
  90. data/lib/farce/integrations/concurrent.rb +16 -3
  91. data/lib/farce/integrations/sorted_set.rb +1 -0
  92. data/lib/farce/internal/blocking_priority_queue.rb +4 -9
  93. data/lib/farce/internal/fake_ractor.rb +1 -1
  94. data/lib/farce/internal/flag_waiting.rb +1 -1
  95. data/lib/farce/internal/freeze.rb +1 -5
  96. data/lib/farce/internal/lease_initialization.rb +1 -1
  97. data/lib/farce/internal/lease_pool_state.rb +1 -1
  98. data/lib/farce/internal/lease_state.rb +2 -2
  99. data/lib/farce/internal/local_executor_methods.rb +3 -3
  100. data/lib/farce/internal/map_value_modes.rb +17 -9
  101. data/lib/farce/internal/pool_settings.rb +9 -0
  102. data/lib/farce/internal/pool_worker.rb +34 -21
  103. data/lib/farce/internal/reservation_waiting.rb +1 -1
  104. data/lib/farce/internal/scheduler_lifecycle.rb +2 -2
  105. data/lib/farce/internal/weak_map_value_modes.rb +5 -0
  106. data/lib/farce/internal.rb +19 -8
  107. data/lib/farce/lazy.rb +3 -0
  108. data/lib/farce/lazy_ref.rb +3 -0
  109. data/lib/farce/local/counter.rb +4 -10
  110. data/lib/farce/local/lease.rb +2 -9
  111. data/lib/farce/local/lease_map.rb +2 -9
  112. data/lib/farce/local/pool.rb +38 -5
  113. data/lib/farce/local/publisher.rb +30 -0
  114. data/lib/farce/local/scheduler.rb +18 -5
  115. data/lib/farce/local/vector.rb +3 -2
  116. data/lib/farce/map.rb +8 -1
  117. data/lib/farce/memoize.rb +319 -0
  118. data/lib/farce/pool.rb +5 -0
  119. data/lib/farce/priority_queue.rb +1 -1
  120. data/lib/farce/publisher.rb +57 -0
  121. data/lib/farce/shareable.rb +3 -0
  122. data/lib/farce/signal.rb +1 -1
  123. data/lib/farce/strict/publisher.rb +31 -0
  124. data/lib/farce/strict/vector.rb +3 -2
  125. data/lib/farce/strict/weak_cache.rb +199 -0
  126. data/lib/farce/subscription/definitions.rb +27 -0
  127. data/lib/farce/subscription.rb +121 -0
  128. data/lib/farce/transaction/map.rb +35 -1
  129. data/lib/farce/unshared/pool.rb +1 -1
  130. data/lib/farce/unshared/publisher.rb +25 -0
  131. data/lib/farce/unshared/scheduler.rb +3 -3
  132. data/lib/farce/unshared/vector.rb +3 -2
  133. data/lib/farce/vector.rb +20 -16
  134. data/lib/farce/version.rb +1 -1
  135. data/lib/farce/walker/definitions.rb +31 -2
  136. data/lib/farce/walker/modification.rb +28 -14
  137. data/lib/farce/walker.rb +9 -1
  138. data/lib/farce/weak_atom.rb +5 -2
  139. data/lib/farce/weak_key_map.rb +1 -1
  140. data/lib/farce/weak_map.rb +1 -1
  141. data/lib/farce/weak_value_map.rb +1 -1
  142. data/lib/farce.rb +3 -1
  143. metadata +23 -1
@@ -125,10 +125,7 @@ module Farce
125
125
  command(:add, request)
126
126
  request.ready&.pop
127
127
 
128
- if deadline
129
- remaining = deadline - Clock.now
130
- remaining = 0 if remaining.negative?
131
- end
128
+ remaining = Clock.remaining(deadline)
132
129
 
133
130
  delivery = request.reply.pop(timeout: remaining)
134
131
  if delivery
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+ # shareable_constant_value: literal
3
+ # warn_indent: true
4
+
5
+ module Farce
6
+ module Internal # :nodoc: all
7
+ class WeakCacheMirror
8
+ include Shareable::Delegated
9
+
10
+ CACHED_NIL = Object.new.freeze
11
+ private_constant :CACHED_NIL
12
+
13
+ def initialize(shared)
14
+ @shared = shared
15
+ super()
16
+ end
17
+
18
+ def store_if_absent(key, &)
19
+ check_frozen!
20
+ return @shared.store_if_absent(key, &) unless Ractor.shareable?(key)
21
+
22
+ local = local_cache
23
+ value = local[key]
24
+ return CACHED_NIL.equal?(value) ? nil : value unless nil.equal?(value)
25
+
26
+ value = @shared.store_if_absent(key, &)
27
+ local[key] = nil.equal?(value) ? CACHED_NIL : value
28
+ value
29
+ end
30
+
31
+ def fetch(key)
32
+ return @shared.fetch(key) { yield(key) } unless Ractor.shareable?(key)
33
+
34
+ local = local_cache
35
+ value = local[key]
36
+ return CACHED_NIL.equal?(value) ? nil : value unless nil.equal?(value)
37
+
38
+ value = @shared.fetch(key) { return yield(key) }
39
+ local[key] = nil.equal?(value) ? CACHED_NIL : value
40
+ value
41
+ end
42
+
43
+ private
44
+
45
+ # CRuby's weak-map operations hold the GVL. Shared misses serialize generation,
46
+ # and a retained value cannot be replaced, so local hits need no reservation.
47
+ # Only shareable keys enter the mirror, since mutable strings can change meaning.
48
+ def local_cache
49
+ caches = Ractor.store_if_absent(:"Farce::Internal::WeakCacheMirror") { ObjectSpace::WeakKeyMap.new }
50
+ caches[self] ||= ObjectSpace::WeakMap.new
51
+ end
52
+
53
+ def freeze_backend = @shared
54
+ end
55
+ end
56
+ end
@@ -134,17 +134,19 @@ module Farce
134
134
  # Yield presence and value under the entry claim.
135
135
  # MAP_KEEP and MAP_DELETE are control results. Other results replace the value.
136
136
  # Return whether a change committed.
137
- def modify(key)
137
+ def modify(key, timeout: nil)
138
138
  raise LocalJumpError, "no block given" unless block_given?
139
139
  check_frozen!
140
140
  key = canonical_key(key)
141
+ deadline = timeout_deadline(timeout)
141
142
  ticket = Object.new.freeze
142
143
  sent = finished = false
143
144
  begin
144
- response = await_response(deadline: nil) do
145
+ response = await_response(deadline:) do
145
146
  sent = true
146
147
  request(:claim, key, ticket, true)
147
148
  end
149
+ return false if response.equal?(TIMED_OUT)
148
150
  present, current, signal = response[1], response[2], response[3]
149
151
  owners = claim_owners
150
152
  owners[signal] = Fiber.current
@@ -306,7 +308,7 @@ module Farce
306
308
 
307
309
  signal, generation = response[1], response[2]
308
310
  reject_recursive_wait!(signal)
309
- timeout = remaining_timeout(deadline)
311
+ timeout = Clock.remaining(deadline)
310
312
  if timeout&.zero?
311
313
  return TIMED_OUT if checked_after_timeout
312
314
  checked_after_timeout = true
@@ -332,11 +334,13 @@ module Farce
332
334
  key = response[5] if response.first == :ok
333
335
  present = response.first == :ok && response[1]
334
336
  current = present ? response[2] : nil
335
- return current if non_nil ? !current.nil? : !values_equal?(current, expected)
337
+
338
+ return current if non_nil ? !current.nil? :
339
+ Internal.value_changed?(current, expected, compare_values_by_identity?)
336
340
 
337
341
  signal = response.first == :ok ? response[3] : response[1]
338
342
  generation = response.first == :ok ? response[4] : response[2]
339
- changed = signal.wait_until_changed(generation, timeout: remaining_timeout(deadline)) { TIMED_OUT }
343
+ changed = signal.wait_until_changed(generation, timeout: Clock.remaining(deadline)) { TIMED_OUT }
340
344
  if changed.equal?(TIMED_OUT)
341
345
  return fallback&.call if checked_after_timeout
342
346
  checked_after_timeout = true
@@ -381,13 +385,6 @@ module Farce
381
385
  Clock.now + timeout
382
386
  end
383
387
 
384
- def remaining_timeout(deadline)
385
- return unless deadline
386
-
387
- remaining = deadline - Clock.now
388
- remaining.positive? ? remaining : 0
389
- end
390
-
391
388
  def validate_boolean(value, name)
392
389
  return if value.equal?(true) || value.equal?(false)
393
390
  raise ArgumentError, "#{name} must be true or false"
@@ -40,10 +40,14 @@ module Farce
40
40
  raise e unless e.path == "#{ENGINE_PATH}/patch"
41
41
  end
42
42
 
43
- unless const_defined?(:NATIVE_WEAK_MAPS, false) && NATIVE_WEAK_MAPS
43
+ if const_defined?(:NATIVE_WEAK_MAPS, false) && NATIVE_WEAK_MAPS
44
+ def native_weak_maps? = true
45
+ else
46
+ def native_weak_maps? = false
44
47
  autoload :WeakMap, "farce/engine/ruby/shared/weak_map"
45
48
  autoload :WeakKeyMap, "farce/engine/ruby/shared/weak_map"
46
49
  autoload :WeakValueMap, "farce/engine/ruby/shared/weak_map"
50
+ autoload :WeakCacheMirror, "farce/engine/ruby/shared/weak_cache_mirror"
47
51
  end
48
52
 
49
53
  StrictAtom = Atom
@@ -77,7 +77,7 @@ module Farce
77
77
  def wait_with_condition(waiter, deadline)
78
78
  @mutex.synchronize do
79
79
  until waiter.matched
80
- remaining = deadline - Clock.now if deadline
80
+ remaining = Clock.remaining(deadline)
81
81
  return cancel(waiter) if remaining && !remaining.positive?
82
82
  waiter.condition.wait(@mutex, remaining)
83
83
  end
@@ -91,7 +91,7 @@ module Farce
91
91
  if waiter.matched
92
92
  [nil, waiter.received]
93
93
  else
94
- remaining = deadline - Clock.now if deadline
94
+ remaining = Clock.remaining(deadline)
95
95
  remaining && !remaining.positive? ? [nil, cancel(waiter)] : [WAITING, remaining]
96
96
  end
97
97
  end
@@ -9,17 +9,56 @@ module Farce
9
9
  include Freeze::Unfreezable
10
10
 
11
11
  NIL_VALUE = Object.new.freeze
12
- attr_reader :capacity
13
12
 
14
13
  def initialize(capacity: 1024, track_age: false)
15
14
  @queue = capacity ? Thread::SizedQueue.new(capacity) : Thread::Queue.new
16
- @capacity = capacity
17
15
  @signal = Signal.new
18
16
  @tracking = track_age ? { lock: Mutex.new, timestamps: [], generation: 0 } : nil
19
- @lifecycle = { lock: Mutex.new, hard_closed: false }
17
+ @lifecycle = { lock: Mutex.new, hard_closed: false, token: nil, generation: -1 }
20
18
  Freeze.publish(self)
21
19
  end
22
20
 
21
+ def capacity = @queue.is_a?(Thread::SizedQueue) ? @queue.max : nil
22
+
23
+ def capacity=(value)
24
+ value = Integer(value) unless value.nil?
25
+ raise ArgumentError, "capacity must be positive or nil" if value && !value.positive?
26
+ if value.nil? != capacity.nil?
27
+ raise ArgumentError, "cannot switch between bounded and unbounded queues on this Ruby engine"
28
+ end
29
+ @queue.max = value if value
30
+ broadcast
31
+ value
32
+ end
33
+
34
+ def interrupt(token, generation)
35
+ @lifecycle[:lock].synchronize do
36
+ return self if generation <= @lifecycle[:generation]
37
+ @lifecycle[:token] = token
38
+ @lifecycle[:generation] = generation
39
+ end
40
+ broadcast
41
+ self
42
+ end
43
+
44
+ def pop_interruptible(token, timeout)
45
+ timeout_at = timeout_at(timeout)
46
+ loop do
47
+ return false unless @lifecycle[:lock].synchronize { @lifecycle[:token].equal?(token) }
48
+ begin
49
+ result = tracked_pop
50
+ raise ThreadError if nil.equal?(result) && @queue.closed?
51
+ broadcast
52
+ return NIL_VALUE.equal?(result) ? nil : result
53
+ rescue ThreadError
54
+ ready = wait(remaining_timeout(timeout_at)) do
55
+ !@lifecycle[:lock].synchronize { @lifecycle[:token].equal?(token) } || size.positive?
56
+ end
57
+ return block_given? ? yield : nil unless ready
58
+ end
59
+ end
60
+ end
61
+
23
62
  def pop(timeout: nil)
24
63
  timeout_at = timeout_at(timeout)
25
64
  # Observing an empty queue is sufficient for a nonblocking miss. Avoid
@@ -223,8 +262,7 @@ module Farce
223
262
  def remaining_timeout(timeout_at)
224
263
  return unless timeout_at
225
264
  return 0 if timeout_at.zero?
226
- timeout = timeout_at - Clock.now
227
- timeout.positive? ? timeout : 0
265
+ Clock.remaining(timeout_at)
228
266
  end
229
267
 
230
268
  def broadcast = @signal.broadcast
@@ -61,7 +61,7 @@ module Farce
61
61
  result = port.select_receive
62
62
  return result if result
63
63
  end
64
- remaining = deadline - Clock.now if deadline
64
+ remaining = Clock.remaining(deadline)
65
65
  return if remaining && !remaining.positive?
66
66
  return unless ReservationWaiting.wait(wakeup, generation, deadline)
67
67
  end
@@ -67,6 +67,11 @@ module Farce
67
67
  super(check_key(key), **) { |value| check_value(yield(value)) }
68
68
  end
69
69
 
70
+ def modify(key, **)
71
+ raise LocalJumpError, "no block given" unless block_given?
72
+ super(check_key(key), **) { |present, value| check_value(yield(present, value)) }
73
+ end
74
+
70
75
  def upsert(key, initial, **)
71
76
  raise LocalJumpError, "no block given" unless block_given?
72
77
  super(check_key(key), check_value(initial), **) { |value| check_value(yield(value)) }
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+ # shareable_constant_value: literal
3
+ # warn_indent: true
4
+
5
+ module Farce
6
+ module Internal
7
+ class PortableTrie
8
+ # @api private
9
+ module IgnoreCase
10
+ private
11
+
12
+ def each_result(input, peek:, deduplicate:, &)
13
+ raise TypeError, "input must be a String" unless input.is_a?(String)
14
+ input = String.new(input)
15
+ validate_encoding!(input, "input")
16
+
17
+ input_length = input.length
18
+ static_input = input.downcase
19
+ if static_input.length != input_length
20
+ return each_expanding_result(input, static_input, peek:, deduplicate:, &)
21
+ end
22
+ seen = {} if deduplicate
23
+ stack = [[@root, 0, EMPTY_CAPTURES, EMPTY_NAMED_CAPTURES, 0, 0]]
24
+
25
+ until stack.empty?
26
+ node, position, captures, named, phase, index = stack.pop
27
+ if phase.zero?
28
+ child = nil
29
+ stride = node.stride
30
+ if node.fast_static
31
+ key = static_input[position, stride]
32
+ child = node.fast_static[key] if key
33
+ elsif position < input_length && !node.static.empty?
34
+ child = node.static[static_input[position]]
35
+ end
36
+ if child
37
+ # Retain only frames with alternatives or a terminal result.
38
+ stack << [node, position, captures, named, 1, 0] unless node.dynamic.empty? && node.tokens.empty?
39
+ stack << [child, position + stride, captures, named, 0, 0]
40
+ next
41
+ end
42
+ end
43
+ if index < node.dynamic.length
44
+ regexp, child = node.dynamic[index]
45
+ if index + 1 < node.dynamic.length || !node.tokens.empty?
46
+ stack << [node, position, captures, named, 1, index + 1]
47
+ end
48
+ if (matched = regexp.match(input, position))
49
+ stack << [
50
+ child,
51
+ matched.end(0),
52
+ captures + matched.captures,
53
+ merge_named_captures(named, matched.named_captures),
54
+ 0,
55
+ 0
56
+ ]
57
+ end
58
+ elsif (peek || position == input_length) && !node.tokens.empty?
59
+ node.tokens.each do |token_id|
60
+ next if deduplicate && seen.key?(token_id)
61
+
62
+ seen[token_id] = true if deduplicate
63
+ yield(
64
+ @tokens.fetch(token_id),
65
+ captures.map { |value| value&.dup },
66
+ copy_named_captures(named),
67
+ input[position..] || +""
68
+ )
69
+ end
70
+ end
71
+ end
72
+ end
73
+
74
+ def each_expanding_result(input, static_input, peek:, deduplicate:)
75
+ to_original = [0]
76
+ from_original = [0]
77
+ input.each_char.with_index do |character, index|
78
+ count = character.downcase.length
79
+ # An internal lowercase boundary cannot start a regexp or end a result.
80
+ (count - 1).times { to_original << nil }
81
+ to_original << (index + 1)
82
+ from_original << (to_original.length - 1)
83
+ end
84
+ input_length = static_input.length
85
+ seen = {} if deduplicate
86
+ stack = [[@root, 0, EMPTY_CAPTURES, EMPTY_NAMED_CAPTURES, 0, 0]]
87
+
88
+ until stack.empty?
89
+ node, position, captures, named, phase, index = stack.pop
90
+ original_position = to_original[position]
91
+ case phase
92
+ when 0
93
+ stack << [node, position, captures, named, 1, 0]
94
+ if node.fast_static
95
+ key = static_input[position, node.stride]
96
+ if key && (child = node.fast_static[key])
97
+ stack << [child, position + node.stride, captures, named, 0, 0]
98
+ end
99
+ elsif position < input_length && (child = node.static[static_input[position]])
100
+ stack << [child, position + 1, captures, named, 0, 0]
101
+ end
102
+ when 1
103
+ if index < node.dynamic.length
104
+ regexp, child = node.dynamic[index]
105
+ stack << [node, position, captures, named, 1, index + 1]
106
+ if original_position && (matched = regexp.match(input, original_position))
107
+ stack << [
108
+ child,
109
+ from_original[matched.end(0)],
110
+ captures + matched.captures,
111
+ merge_named_captures(named, matched.named_captures),
112
+ 0,
113
+ 0
114
+ ]
115
+ end
116
+ elsif original_position && (peek || position == input_length) && !node.tokens.empty?
117
+ node.tokens.each do |token_id|
118
+ next if deduplicate && seen.key?(token_id)
119
+
120
+ seen[token_id] = true if deduplicate
121
+ yield(
122
+ @tokens.fetch(token_id),
123
+ captures.map { |value| value&.dup },
124
+ copy_named_captures(named),
125
+ input[original_position..] || +""
126
+ )
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
133
+ private_constant :IgnoreCase
134
+ end
135
+ end
136
+ end
@@ -76,11 +76,16 @@ module Farce
76
76
  end
77
77
  private_constant :Node
78
78
 
79
- def initialize(entries, tokens)
79
+ def initialize(entries, tokens, ignore_case: false)
80
80
  validate_array!(entries, "entries")
81
81
  validate_array!(tokens, "tokens")
82
82
  validate_tokens!(tokens)
83
83
 
84
+ if ignore_case
85
+ @ignore_case = true
86
+ extend IgnoreCase
87
+ end
88
+
84
89
  @tokens = Array.new(tokens).freeze
85
90
  @root = Node.new
86
91
  nodes = [@root]
@@ -103,6 +108,7 @@ module Farce
103
108
  when String
104
109
  literal = String.new(part)
105
110
  validate_encoding!(literal, "edge")
111
+ literal.downcase! if ignore_case
106
112
  literal.each_char do |character|
107
113
  child = node.static[character]
108
114
  unless child
@@ -171,6 +177,12 @@ module Farce
171
177
 
172
178
  private
173
179
 
180
+ def initialize_dup(other)
181
+ super
182
+ # Unlike clone, dup does not retain the instance's matching module.
183
+ extend IgnoreCase if @ignore_case
184
+ end
185
+
174
186
  def anchor(regexp)
175
187
  compile_regexp("\\G(?:#{regexp.source})", regexp)
176
188
  rescue RegexpError
@@ -314,4 +326,5 @@ module Farce
314
326
  end
315
327
  end
316
328
 
329
+ require "farce/engine/shared/trie/ignore_case"
317
330
  require "farce/engine/shared/trie_builder"
@@ -17,6 +17,13 @@ module Farce
17
17
  # terminal match. A regexp edge is atomic. Later edges cannot make a regexp
18
18
  # choose a different endpoint.
19
19
  #
20
+ # Pass `ignore_case: true` to {.build} or {Builder.new} to compare all static
21
+ # segments using Unicode downcasing. Regexp options do not change. Regexp
22
+ # matches, captures, and suffixes use the original input and retain its casing.
23
+ # This does not include every equivalence supported by `Regexp::IGNORECASE`.
24
+ # For example, `"SS"` matches `"ss"`, but `"ß"` does not match `"ss"`.
25
+ # Case-equivalent static entries share a branch and retain their token order.
26
+ #
20
27
  # Tokens must already be Ractor-shareable. They are stored and returned
21
28
  # unchanged. Equal but different token objects remain separate. Reusing the
22
29
  # same token object produces at most one result per lookup.
@@ -94,20 +101,23 @@ module Farce
94
101
  # Collects entries and publishes immutable {Trie} snapshots.
95
102
  class Builder
96
103
  # Create an empty builder.
97
- def initialize
104
+ # @param ignore_case [Boolean] compare all static segments without case
105
+ def initialize(ignore_case: false)
98
106
  @entries = []
107
+ @ignore_case = !!ignore_case
99
108
  end
100
109
 
101
110
  # @api private
102
111
  def marshal_dump
103
112
  entries = @entries.map { |parts, token| [parts, Internal::MarshalSupport.value(token)] }
104
- [1, entries, frozen?]
113
+ [1, entries, frozen?, @ignore_case]
105
114
  end
106
115
 
107
116
  # @api private
108
117
  def marshal_load(data)
109
- entries, frozen = Internal::MarshalSupport.payload(data, 2)
110
- initialize
118
+ length = data.is_a?(Array) && data.length == 3 ? 2 : 3
119
+ entries, frozen, ignore_case = Internal::MarshalSupport.payload(data, length)
120
+ initialize(ignore_case:)
111
121
  entries.each { |parts, token| add(parts, Internal::MarshalSupport.restore_value(token)) }
112
122
  freeze if frozen
113
123
  end
@@ -159,7 +169,7 @@ module Farce
159
169
  [parts.dup.freeze, token_ids.fetch(token)].freeze
160
170
  end.freeze
161
171
 
162
- Trie.send(:build_entries, entries, tokens.freeze)
172
+ Trie.send(:build_entries, entries, tokens.freeze, ignore_case: @ignore_case)
163
173
  end
164
174
 
165
175
  private
@@ -180,17 +190,18 @@ module Farce
180
190
  class << self
181
191
  # Build an immutable trie.
182
192
  #
193
+ # @param ignore_case [Boolean] compare all static segments without case
183
194
  # @yieldparam builder [Builder]
184
195
  # @return [Trie]
185
- def build
186
- builder = Builder.new
196
+ def build(ignore_case: false)
197
+ builder = Builder.new(ignore_case:)
187
198
  yield builder if block_given?
188
199
  builder.build
189
200
  end
190
201
 
191
202
  private
192
203
 
193
- def build_entries(entries, tokens) = new(entries, tokens)
204
+ def build_entries(entries, tokens, ignore_case:) = new(entries, tokens, ignore_case:)
194
205
 
195
206
  private :new
196
207
  end
@@ -129,7 +129,7 @@ module Farce
129
129
 
130
130
  loop do
131
131
  current, version = @mutex.synchronize { [@value, @version] }
132
- return current unless values_equal?(current, expected)
132
+ return current if Internal.value_changed?(current, expected, compare_by_identity?)
133
133
 
134
134
  result = @mutex.synchronize do
135
135
  next unless @version == version
@@ -6,9 +6,11 @@ module Farce
6
6
  # @!visibility private
7
7
  module Internal # :nodoc: all
8
8
  class UnsharedVector
9
+ def self.normalize_source(source) = source.is_a?(Integer) ? Array.new(source) : source
10
+
9
11
  def initialize(source = nil, compare_by_identity: false)
10
12
  validate_boolean(compare_by_identity, "compare_by_identity")
11
- raise TypeError, "source must be an Array" unless source.nil? || source.is_a?(Array)
13
+ raise TypeError, "source must be an Array" unless source.nil? || source.is_a?(Array) || source.is_a?(Integer)
12
14
 
13
15
  @values = source ? Array.new(source) : []
14
16
  @compare_by_identity = compare_by_identity
@@ -274,12 +276,12 @@ module Farce
274
276
  finish_update if completed
275
277
  end
276
278
 
277
- def wait_until_changed(index, expected, timeout: nil)
278
- wait_for_value(convert_index(index), expected, timeout_deadline(timeout), non_nil: false)
279
+ def wait_until_changed(index, expected, timeout: nil, &fallback)
280
+ wait_for_value(convert_index(index), expected, timeout_deadline(timeout), fallback, non_nil: false)
279
281
  end
280
282
 
281
283
  def wait_until_non_nil(index, timeout: nil)
282
- wait_for_value(convert_index(index), nil, timeout_deadline(timeout), non_nil: true)
284
+ wait_for_value(convert_index(index), nil, timeout_deadline(timeout), nil, non_nil: true)
283
285
  end
284
286
 
285
287
  def compare_by_identity? = @compare_by_identity
@@ -341,20 +343,21 @@ module Farce
341
343
  raise FrozenError.new("can't modify frozen #{self.class}", receiver: self)
342
344
  end
343
345
 
344
- def wait_for_value(index, expected, deadline, non_nil:)
346
+ def wait_for_value(index, expected, deadline, fallback, non_nil:)
345
347
  while true
346
348
  generation = @signal.generation
347
349
  current = @mutex.synchronize { @values[index] }
348
- ready = non_nil ? !current.nil? : !values_equal?(current, expected)
350
+ ready = non_nil ? !current.nil? :
351
+ Internal.value_changed?(current, expected, compare_by_identity?)
349
352
  return current if ready
350
- return unless wait_for_signal(generation, deadline)
353
+ return fallback&.call unless wait_for_signal(generation, deadline)
351
354
  end
352
355
  end
353
356
 
354
357
  def wait_for_signal(generation, deadline)
355
358
  return @signal.wait(generation) unless deadline
356
359
 
357
- remaining = deadline - Clock.now
360
+ remaining = Clock.remaining(deadline)
358
361
  return false unless remaining.positive?
359
362
  !@signal.wait(generation, timeout: remaining).nil?
360
363
  end
@@ -145,10 +145,11 @@ module Farce
145
145
  # Yield presence and value under the entry reservation.
146
146
  # MAP_KEEP and MAP_DELETE are control results. Other results replace the value.
147
147
  # Return whether a change committed.
148
- def modify(key) # rubocop:disable Naming/PredicateMethod
148
+ def modify(key, timeout: nil)
149
149
  raise LocalJumpError, "no block given" unless block_given?
150
150
  check_frozen!
151
- _, result = with_entry(key, nil, create: true, mutating: true) do |present, current, entry, index|
151
+ deadline = timeout_deadline(timeout)
152
+ completed, result = with_entry(key, deadline, create: true, mutating: true) do |present, current, entry, index|
152
153
  value = yield(present, current)
153
154
  check_frozen!
154
155
  if MAP_KEEP.equal?(value)
@@ -161,7 +162,7 @@ module Farce
161
162
  entry.store(value)
162
163
  end
163
164
  end
164
- !!result
165
+ completed && !!result
165
166
  end
166
167
 
167
168
  def wait_until_changed(key, expected, timeout: nil, &fallback)
@@ -352,7 +353,7 @@ module Farce
352
353
  return fallback&.call if timed_out == :timed_out
353
354
 
354
355
  unless entry
355
- return nil unless values_equal?(nil, expected)
356
+ return nil if Internal.value_changed?(nil, expected, compare_values_by_identity?)
356
357
  return fallback&.call unless signal_changed?(@index.change_signal, observed, deadline)
357
358
  next
358
359
  end
@@ -373,13 +374,14 @@ module Farce
373
374
  # lookup object. Otherwise GC could orphan the entry's change signal.
374
375
  key = stored_key
375
376
  current = nil unless present
376
- return current if non_nil ? !current.nil? : !values_equal?(current, expected)
377
+ return current if non_nil ? !current.nil? :
378
+ Internal.value_changed?(current, expected, compare_values_by_identity?)
377
379
  return fallback&.call unless entry.wait_for_change?(changed, deadline)
378
380
  end
379
381
  end
380
382
 
381
383
  def signal_changed?(signal, observed, deadline)
382
- timeout = deadline - Clock.now if deadline
384
+ timeout = Clock.remaining(deadline)
383
385
  return false if timeout && !timeout.positive?
384
386
 
385
387
  timed_out = Object.new
@@ -6,6 +6,8 @@ module Farce
6
6
  # @!visibility private
7
7
  module Internal # :nodoc: all
8
8
  class Vector < UnsharedVector
9
+ def self.new_for_modes(source) = new(source, compare_by_identity: true)
10
+
9
11
  def initialize(source = nil, **)
10
12
  source.each { check_value(it) } if source.is_a?(Array)
11
13
  super
@@ -146,7 +146,7 @@ module Farce
146
146
  def with_value(timeout)
147
147
  deadline = timeout_deadline(timeout)
148
148
  result = TIMED_OUT
149
- @lock.update(timeout: remaining_timeout(deadline)) do
149
+ @lock.update(timeout: Clock.remaining(deadline)) do
150
150
  result = yield
151
151
  nil
152
152
  end
@@ -158,11 +158,12 @@ module Farce
158
158
  loop do
159
159
  generation = @changes.value
160
160
  current = read_value
161
- return current if non_nil ? !current.nil? : !values_equal?(current, expected)
161
+ return current if non_nil ? !current.nil? :
162
+ Internal.value_changed?(current, expected, compare_by_identity?)
162
163
  # Detect recursive waits without waiting for another owner's update.
163
164
  @lock.get(timeout: 0)
164
- return fallback&.call if remaining_timeout(deadline)&.zero?
165
- @changes.wait_until_changed(generation, timeout: remaining_timeout(deadline))
165
+ return fallback&.call if Clock.remaining(deadline)&.zero?
166
+ @changes.wait_until_changed(generation, timeout: Clock.remaining(deadline))
166
167
  end
167
168
  end
168
169
 
@@ -177,12 +178,6 @@ module Farce
177
178
  raise ArgumentError, "timeout must be finite and non-negative" unless timeout.finite? && !timeout.negative?
178
179
  Clock.now + timeout
179
180
  end
180
-
181
- def remaining_timeout(deadline)
182
- return unless deadline
183
- remaining = deadline - Clock.now
184
- remaining.negative? ? 0 : remaining
185
- end
186
181
  end
187
182
  private_constant :WeakAtomBase
188
183
  end