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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +98 -2
- data/README.md +3 -1
- data/docs/benchmarks.md +37 -21
- data/docs/compatibility.md +13 -0
- data/docs/data-structures.md +37 -0
- data/docs/integrations.md +1 -0
- data/docs/introduction.md +19 -0
- data/docs/modes.md +2 -0
- data/docs/scheduling.md +35 -3
- data/docs/scopes.md +3 -1
- data/docs/shims.md +53 -0
- data/docs/utilities.md +32 -0
- data/docs/variants.md +9 -0
- data/ext/farce/atom.c +23 -7
- data/ext/farce/containers.h +3 -0
- data/ext/farce/counter.c +4 -6
- data/ext/farce/depend +3 -1
- data/ext/farce/extconf.rb +19 -14
- data/ext/farce/farce.c +110 -3
- data/ext/farce/map.c +15 -7
- data/ext/farce/priority_queue.c +46 -1
- data/ext/farce/queue.c +141 -7
- data/ext/farce/trie.c +140 -72
- data/ext/farce/trie_search.h +114 -0
- data/ext/farce/vector.c +53 -21
- data/ext/farce/weak_map.c +16 -11
- data/ext/farce/weak_references.h +23 -0
- data/ext/java/org/farce/FiberScheduler.java +8 -6
- data/ext/java/org/farce/PriorityQueue.java +18 -2
- data/lib/farce/abstract/atom.rb +5 -1
- data/lib/farce/abstract/concurrent_map.rb +122 -0
- data/lib/farce/abstract/counter.rb +36 -1
- data/lib/farce/abstract/executor.rb +30 -7
- data/lib/farce/abstract/flag.rb +12 -0
- data/lib/farce/abstract/molecule.rb +14 -14
- data/lib/farce/abstract/pool.rb +158 -47
- data/lib/farce/abstract/priority_queue.rb +1 -1
- data/lib/farce/abstract/publisher.rb +189 -0
- data/lib/farce/abstract/queue.rb +29 -3
- data/lib/farce/abstract/scheduler.rb +41 -14
- data/lib/farce/abstract/timer_queue.rb +3 -3
- data/lib/farce/abstract/vector.rb +3 -1
- data/lib/farce/atom.rb +15 -2
- data/lib/farce/clock.rb +20 -7
- data/lib/farce/concurrent/count_down_latch.rb +37 -0
- data/lib/farce/concurrent/cyclic_barrier.rb +109 -0
- data/lib/farce/concurrent/event.rb +60 -0
- data/lib/farce/concurrent/fiber_local_var.rb +31 -0
- data/lib/farce/concurrent/local_var.rb +65 -0
- data/lib/farce/concurrent/thread_local_var.rb +28 -0
- data/lib/farce/concurrent/tuple.rb +74 -0
- data/lib/farce/concurrent.rb +50 -0
- data/lib/farce/config.rb +112 -2
- data/lib/farce/counter.rb +5 -15
- data/lib/farce/deduper.rb +12 -4
- data/lib/farce/engine/jruby/fiber_scheduler.jar +0 -0
- data/lib/farce/engine/jruby/map.rb +8 -6
- data/lib/farce/engine/jruby/signal.rb +2 -2
- data/lib/farce/engine/jvm/counter.rb +0 -2
- data/lib/farce/engine/jvm/farce.jar +0 -0
- data/lib/farce/engine/jvm/priority_queue.rb +14 -4
- data/lib/farce/engine/ruby/3.4/farce.so +0 -0
- data/lib/farce/engine/ruby/4.0/farce.so +0 -0
- data/lib/farce/engine/ruby/shared/parallel_scheduler.rb +9 -1
- data/lib/farce/engine/ruby/shared/ractor_selector.rb +1 -4
- data/lib/farce/engine/ruby/shared/weak_cache_mirror.rb +56 -0
- data/lib/farce/engine/ruby/shared/weak_map.rb +9 -12
- data/lib/farce/engine/ruby.rb +5 -1
- data/lib/farce/engine/shared/exchanger.rb +2 -2
- data/lib/farce/engine/shared/queue.rb +43 -5
- data/lib/farce/engine/shared/ractor_selector.rb +1 -1
- data/lib/farce/engine/shared/strict_map.rb +5 -0
- data/lib/farce/engine/shared/trie/ignore_case.rb +136 -0
- data/lib/farce/engine/shared/trie.rb +14 -1
- data/lib/farce/engine/shared/trie_builder.rb +19 -8
- data/lib/farce/engine/shared/unshared_atom.rb +1 -1
- data/lib/farce/engine/shared/unshared_vector.rb +11 -8
- data/lib/farce/engine/shared/unshared_weak_map.rb +8 -6
- data/lib/farce/engine/shared/vector.rb +2 -0
- data/lib/farce/engine/shared/weak_atom/base.rb +5 -10
- data/lib/farce/engine/shared/weak_map/cell.rb +1 -1
- data/lib/farce/engine/shared.rb +2 -0
- data/lib/farce/engine/truffleruby/map.rb +8 -6
- data/lib/farce/engine/truffleruby/native/counter.rb +0 -2
- data/lib/farce/engine/truffleruby/native/priority_queue.rb +18 -4
- data/lib/farce/engine/truffleruby/signal.rb +1 -1
- data/lib/farce/engine/truffleruby.rb +9 -0
- data/lib/farce/extension.rb +5 -3
- data/lib/farce/integrations/concurrent.rb +16 -3
- data/lib/farce/integrations/sorted_set.rb +1 -0
- data/lib/farce/internal/blocking_priority_queue.rb +4 -9
- data/lib/farce/internal/fake_ractor.rb +1 -1
- data/lib/farce/internal/flag_waiting.rb +1 -1
- data/lib/farce/internal/freeze.rb +1 -5
- data/lib/farce/internal/lease_initialization.rb +1 -1
- data/lib/farce/internal/lease_pool_state.rb +1 -1
- data/lib/farce/internal/lease_state.rb +2 -2
- data/lib/farce/internal/local_executor_methods.rb +3 -3
- data/lib/farce/internal/map_value_modes.rb +17 -9
- data/lib/farce/internal/pool_settings.rb +9 -0
- data/lib/farce/internal/pool_worker.rb +34 -21
- data/lib/farce/internal/reservation_waiting.rb +1 -1
- data/lib/farce/internal/scheduler_lifecycle.rb +2 -2
- data/lib/farce/internal/weak_map_value_modes.rb +5 -0
- data/lib/farce/internal.rb +19 -8
- data/lib/farce/lazy.rb +3 -0
- data/lib/farce/lazy_ref.rb +3 -0
- data/lib/farce/local/counter.rb +4 -10
- data/lib/farce/local/lease.rb +2 -9
- data/lib/farce/local/lease_map.rb +2 -9
- data/lib/farce/local/pool.rb +38 -5
- data/lib/farce/local/publisher.rb +30 -0
- data/lib/farce/local/scheduler.rb +18 -5
- data/lib/farce/local/vector.rb +3 -2
- data/lib/farce/map.rb +8 -1
- data/lib/farce/memoize.rb +319 -0
- data/lib/farce/pool.rb +5 -0
- data/lib/farce/priority_queue.rb +1 -1
- data/lib/farce/publisher.rb +57 -0
- data/lib/farce/shareable.rb +3 -0
- data/lib/farce/signal.rb +1 -1
- data/lib/farce/strict/publisher.rb +31 -0
- data/lib/farce/strict/vector.rb +3 -2
- data/lib/farce/strict/weak_cache.rb +199 -0
- data/lib/farce/subscription/definitions.rb +27 -0
- data/lib/farce/subscription.rb +121 -0
- data/lib/farce/transaction/map.rb +35 -1
- data/lib/farce/unshared/pool.rb +1 -1
- data/lib/farce/unshared/publisher.rb +25 -0
- data/lib/farce/unshared/scheduler.rb +3 -3
- data/lib/farce/unshared/vector.rb +3 -2
- data/lib/farce/vector.rb +20 -16
- data/lib/farce/version.rb +1 -1
- data/lib/farce/walker/definitions.rb +31 -2
- data/lib/farce/walker/modification.rb +28 -14
- data/lib/farce/walker.rb +9 -1
- data/lib/farce/weak_atom.rb +5 -2
- data/lib/farce/weak_key_map.rb +1 -1
- data/lib/farce/weak_map.rb +1 -1
- data/lib/farce/weak_value_map.rb +1 -1
- data/lib/farce.rb +3 -1
- metadata +23 -1
|
@@ -125,10 +125,7 @@ module Farce
|
|
|
125
125
|
command(:add, request)
|
|
126
126
|
request.ready&.pop
|
|
127
127
|
|
|
128
|
-
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
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:
|
|
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"
|
data/lib/farce/engine/ruby.rb
CHANGED
|
@@ -40,10 +40,14 @@ module Farce
|
|
|
40
40
|
raise e unless e.path == "#{ENGINE_PATH}/patch"
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
110
|
-
|
|
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
|
|
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? :
|
|
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 =
|
|
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
|
|
148
|
+
def modify(key, timeout: nil)
|
|
149
149
|
raise LocalJumpError, "no block given" unless block_given?
|
|
150
150
|
check_frozen!
|
|
151
|
-
|
|
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
|
|
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? :
|
|
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 =
|
|
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:
|
|
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? :
|
|
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
|
|
165
|
-
@changes.wait_until_changed(generation, timeout:
|
|
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
|