farce 0.5.0-x86-linux-gnu → 0.6.0-x86-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 +18 -0
- data/docs/modes.md +1 -1
- data/docs/utilities.md +1 -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 +101 -3
- data/ext/farce/map.c +1 -4
- data/ext/farce/trie.c +140 -72
- data/ext/farce/trie_search.h +114 -0
- data/ext/farce/vector.c +50 -18
- data/ext/farce/weak_map.c +4 -8
- data/ext/farce/weak_references.h +23 -0
- data/ext/java/org/farce/FiberScheduler.java +8 -6
- data/lib/farce/abstract/atom.rb +5 -1
- data/lib/farce/abstract/counter.rb +5 -4
- data/lib/farce/abstract/molecule.rb +14 -14
- data/lib/farce/abstract/vector.rb +1 -1
- data/lib/farce/atom.rb +6 -0
- data/lib/farce/clock.rb +8 -7
- data/lib/farce/counter.rb +6 -0
- data/lib/farce/deduper.rb +12 -4
- data/lib/farce/engine/jruby/fiber_scheduler.jar +0 -0
- data/lib/farce/engine/jvm/counter.rb +0 -2
- 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/weak_cache_mirror.rb +56 -0
- data/lib/farce/engine/ruby.rb +5 -1
- 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_vector.rb +3 -1
- data/lib/farce/engine/shared/vector.rb +2 -0
- data/lib/farce/engine/shared.rb +2 -0
- data/lib/farce/engine/truffleruby/native/counter.rb +0 -2
- data/lib/farce/engine/truffleruby.rb +9 -0
- data/lib/farce/extension.rb +5 -3
- data/lib/farce/integrations/sorted_set.rb +1 -0
- data/lib/farce/internal/fake_ractor.rb +1 -1
- data/lib/farce/internal/freeze.rb +1 -5
- data/lib/farce/internal/map_value_modes.rb +8 -7
- data/lib/farce/internal.rb +11 -0
- data/lib/farce/local/counter.rb +4 -10
- data/lib/farce/map.rb +8 -1
- data/lib/farce/memoize.rb +10 -5
- data/lib/farce/shareable.rb +3 -0
- data/lib/farce/strict/weak_cache.rb +199 -0
- data/lib/farce/vector.rb +8 -7
- data/lib/farce/version.rb +1 -1
- data/lib/farce/walker/definitions.rb +18 -0
- data/lib/farce/walker/modification.rb +28 -14
- data/lib/farce/walker.rb +9 -1
- 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
- metadata +6 -1
|
@@ -76,6 +76,20 @@ module Farce
|
|
|
76
76
|
# @return [Boolean] the default comparison policy for new records
|
|
77
77
|
def self.compare_by_identity? = false
|
|
78
78
|
|
|
79
|
+
# @return [Array<Symbol>] the atom accessor names in declaration order
|
|
80
|
+
def self.atoms
|
|
81
|
+
return @atoms if defined?(@atoms) && @atoms
|
|
82
|
+
return superclass.atoms if superclass.respond_to?(:atoms)
|
|
83
|
+
EMPTY_ARRAY
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# @return [Array<Symbol>] the field names in declaration order
|
|
87
|
+
def self.members
|
|
88
|
+
return @members if defined?(@members) && @members
|
|
89
|
+
return superclass.members if superclass.respond_to?(:members)
|
|
90
|
+
EMPTY_ARRAY
|
|
91
|
+
end
|
|
92
|
+
|
|
79
93
|
# @!visibility private
|
|
80
94
|
def self.define_member(field)
|
|
81
95
|
raise ArgumentError, "#{field.inspect} is not a valid field name" if field == :compare_by_identity
|
|
@@ -99,20 +113,6 @@ module Farce
|
|
|
99
113
|
end
|
|
100
114
|
private_class_method :define_member
|
|
101
115
|
|
|
102
|
-
# @return [Array<Symbol>] the atom accessor names in declaration order
|
|
103
|
-
def self.atoms
|
|
104
|
-
return @atoms if defined?(@atoms) && @atoms
|
|
105
|
-
return superclass.atoms if superclass.respond_to?(:atoms)
|
|
106
|
-
EMPTY_ARRAY
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
# @return [Array<Symbol>] the field names in declaration order
|
|
110
|
-
def self.members
|
|
111
|
-
return @members if defined?(@members) && @members
|
|
112
|
-
return superclass.members if superclass.respond_to?(:members)
|
|
113
|
-
EMPTY_ARRAY
|
|
114
|
-
end
|
|
115
|
-
|
|
116
116
|
# Initialize fields by position or name. A field may only be supplied once.
|
|
117
117
|
# Existing {Atom} instances are retained directly, preserving their policy.
|
|
118
118
|
# @param input [Array<BasicObject>] values in declaration order
|
|
@@ -1390,7 +1390,7 @@ module Farce
|
|
|
1390
1390
|
|
|
1391
1391
|
private
|
|
1392
1392
|
|
|
1393
|
-
def normalize_vector_source(source) =
|
|
1393
|
+
def normalize_vector_source(source) = Internal::Vector.normalize_source(source)
|
|
1394
1394
|
|
|
1395
1395
|
def mutate_vector(timeout: nil)
|
|
1396
1396
|
result = nil
|
data/lib/farce/atom.rb
CHANGED
|
@@ -140,6 +140,12 @@ module Farce
|
|
|
140
140
|
raise LocalJumpError, "no block given" unless block_given?
|
|
141
141
|
check_frozen!
|
|
142
142
|
|
|
143
|
+
if timeout.nil?
|
|
144
|
+
# A hit must still wait for an update in progress.
|
|
145
|
+
current = @atom.get
|
|
146
|
+
return unwrap_value(current) unless NIL_VALUE.equal?(current)
|
|
147
|
+
end
|
|
148
|
+
|
|
143
149
|
result = @atom.update(timeout:) do |current|
|
|
144
150
|
next current unless NIL_VALUE.equal?(current)
|
|
145
151
|
|
data/lib/farce/clock.rb
CHANGED
|
@@ -115,13 +115,14 @@ module Farce
|
|
|
115
115
|
raise TypeError, "Cannot convert #{value.class} to clock time"
|
|
116
116
|
end
|
|
117
117
|
|
|
118
|
-
alias
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
118
|
+
# Copy the bodies eagerly. Lazy module alias resolution can race between Ractors.
|
|
119
|
+
define_method :now, instance_method(:current)
|
|
120
|
+
define_method :at, instance_method(:time)
|
|
121
|
+
define_method :timeout_at, instance_method(:time)
|
|
122
|
+
define_method :delay, instance_method(:offset)
|
|
123
|
+
define_method :in, instance_method(:offset)
|
|
124
|
+
define_method :timeout, instance_method(:offset)
|
|
125
|
+
define_method :wait, instance_method(:offset)
|
|
125
126
|
|
|
126
127
|
private
|
|
127
128
|
|
data/lib/farce/counter.rb
CHANGED
|
@@ -13,6 +13,12 @@ module Farce
|
|
|
13
13
|
#
|
|
14
14
|
# On CRuby, subclasses must not add Ruby instance variables.
|
|
15
15
|
#
|
|
16
|
+
# @example Allocate sequence numbers across threads or Ractors
|
|
17
|
+
# counter = Farce::Counter.new
|
|
18
|
+
# ticket = counter.increment # => 1
|
|
19
|
+
# counter.increment # => 2
|
|
20
|
+
# ticket # => 1
|
|
21
|
+
#
|
|
16
22
|
# @example Cross-Ractor counting
|
|
17
23
|
# counter = Farce::Counter.new
|
|
18
24
|
# counter.value # => 0
|
data/lib/farce/deduper.rb
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
module Farce
|
|
6
6
|
# Reuse equal frozen values throughout object graphs.
|
|
7
|
-
# Plain strings use Ruby's string deduplication. Arrays, hashes,
|
|
8
|
-
# and cached by default. Other objects are
|
|
9
|
-
# their class matches {#store}.
|
|
7
|
+
# Plain strings use Ruby's string deduplication. Arrays, hashes, Ruby Sets, regular
|
|
8
|
+
# expressions, Data records, and ranges are frozen and cached by default. Other objects are
|
|
9
|
+
# traversed but are not cached unless their class matches {#store}.
|
|
10
|
+
# Modules and classes are skipped by default.
|
|
10
11
|
#
|
|
11
12
|
# Cached objects are held weakly. Keep a reference to a returned object if its
|
|
12
13
|
# identity matters. Shareable results can be reused across Ractors. Other
|
|
@@ -28,7 +29,7 @@ module Farce
|
|
|
28
29
|
@shared = Strict::WeakKeyMap.new
|
|
29
30
|
@local = Local::WeakKeyMap.new
|
|
30
31
|
@skip = Strict::Vector[Module]
|
|
31
|
-
@store = Strict::Vector[Hash, Array, ::Set]
|
|
32
|
+
@store = Strict::Vector[Hash, Array, ::Set, Regexp, Data, Range]
|
|
32
33
|
super
|
|
33
34
|
end
|
|
34
35
|
|
|
@@ -68,6 +69,13 @@ module Farce
|
|
|
68
69
|
# Use copy: true to preserve the input. Skipped objects are always returned
|
|
69
70
|
# unchanged. Strings are deduplicated before skip rules are checked.
|
|
70
71
|
# Noncopyable coordination objects follow {Walker.modify}'s in-place behavior.
|
|
72
|
+
#
|
|
73
|
+
# @note
|
|
74
|
+
# Deduplication follows Ruby's `hash` and `eql?` semantics. Equal values
|
|
75
|
+
# may differ in observable details. For example, `Time` values representing
|
|
76
|
+
# the same instant can have different time zones. Deduplicating these values
|
|
77
|
+
# or containers holding them may reuse a different timezone representation.
|
|
78
|
+
#
|
|
71
79
|
# @param object [Object] the root object
|
|
72
80
|
# @param copy [Boolean, Symbol] copying policy passed to {Walker.modify}
|
|
73
81
|
# @param skip [Module, Array<Module>, nil] additional exclusions for this call
|
|
Binary file
|
|
@@ -68,7 +68,6 @@ module Farce
|
|
|
68
68
|
check_frozen!
|
|
69
69
|
end
|
|
70
70
|
@counter.addAndGet(delta).tap { @signal.broadcast }
|
|
71
|
-
self
|
|
72
71
|
end
|
|
73
72
|
|
|
74
73
|
def decrement(delta = 1)
|
|
@@ -78,7 +77,6 @@ module Farce
|
|
|
78
77
|
check_frozen!
|
|
79
78
|
end
|
|
80
79
|
@counter.addAndGet(-delta).tap { @signal.broadcast }
|
|
81
|
-
self
|
|
82
80
|
end
|
|
83
81
|
|
|
84
82
|
alias value get
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|
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
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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
|
data/lib/farce/engine/shared.rb
CHANGED
|
@@ -54,6 +54,8 @@ module Farce
|
|
|
54
54
|
end
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
+
def native_weak_maps? = true
|
|
58
|
+
|
|
57
59
|
# define_method supplies the receiver. Rebinding first is unnecessary without
|
|
58
60
|
# native Ractor isolation and can change the method context used by super.
|
|
59
61
|
def prepare_method_definition(&definition) = definition
|
|
@@ -39,6 +39,15 @@ module Farce
|
|
|
39
39
|
|
|
40
40
|
def native_ractors? = false
|
|
41
41
|
|
|
42
|
+
# Range#initialize rejects open endpoints on TruffleRuby, while Range.new accepts them.
|
|
43
|
+
def initialize_range(target, first, last, exclude_end)
|
|
44
|
+
if first.nil? || last.nil?
|
|
45
|
+
target.__send__(:initialize_copy, target.class.new(first, last, exclude_end))
|
|
46
|
+
else
|
|
47
|
+
target.__send__(:initialize, first, last, exclude_end)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
42
51
|
# @api private
|
|
43
52
|
# TruffleRuby probes this additional hook before using _dump.
|
|
44
53
|
def marshal_protocol_method?(name)
|
data/lib/farce/extension.rb
CHANGED
|
@@ -100,13 +100,15 @@ module Farce
|
|
|
100
100
|
# @yield the block containing method definitions
|
|
101
101
|
# @yieldreceiver [Module, Refinement] the module/refinement used for extending the target class.
|
|
102
102
|
# @return [self] the extension instance
|
|
103
|
-
|
|
103
|
+
# rubocop:disable-next Naming/BlockForwarding, Style/ArgumentsForwarding
|
|
104
|
+
def define(*targets, &block)
|
|
105
|
+
# JRuby rejects repeated anonymous block forwarding inside a block.
|
|
104
106
|
@lock.synchronize do
|
|
105
107
|
check_state!
|
|
106
108
|
targets.each do |target|
|
|
107
|
-
refine(target, &)
|
|
109
|
+
refine(target, &block)
|
|
108
110
|
mixin = @definitions.store_if_absent(target) { Mixin.new(self, target) }
|
|
109
|
-
mixin.module_eval(&)
|
|
111
|
+
mixin.module_eval(&block)
|
|
110
112
|
mixin.install if installed?
|
|
111
113
|
end
|
|
112
114
|
end
|
|
@@ -8,11 +8,7 @@ module Farce
|
|
|
8
8
|
module Freeze
|
|
9
9
|
# Structurally freeze a Ruby facade, then validate and publish its object graph.
|
|
10
10
|
# This deliberately bypasses an override that controls logical frozen state.
|
|
11
|
-
def self.publish(object)
|
|
12
|
-
Object.instance_method(:freeze).bind_call(object)
|
|
13
|
-
::Ractor.make_shareable(object) if Internal.native_ractors?
|
|
14
|
-
object
|
|
15
|
-
end
|
|
11
|
+
def self.publish(object) = Internal.publish_shareable(object)
|
|
16
12
|
|
|
17
13
|
def self.check(object)
|
|
18
14
|
return object unless object.frozen?
|
|
@@ -22,9 +22,7 @@ module Farce
|
|
|
22
22
|
compare_keys_by_identity: compare_by_identity, compare_values_by_identity: compare_by_identity)
|
|
23
23
|
initial_mapping = convert_entries(initial_mapping)
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
valid_comparison = identity.bind_call(compare_values_by_identity, true) ||
|
|
27
|
-
identity.bind_call(compare_values_by_identity, false)
|
|
25
|
+
valid_comparison = true.equal?(compare_values_by_identity) || false.equal?(compare_values_by_identity)
|
|
28
26
|
|
|
29
27
|
raise ArgumentError, "compare_values_by_identity must be a boolean" unless valid_comparison
|
|
30
28
|
@compare_values_by_identity = compare_values_by_identity
|
|
@@ -33,9 +31,11 @@ module Farce
|
|
|
33
31
|
@map = new_map(compare_by_identity:, compare_keys_by_identity:, compare_values_by_identity: true)
|
|
34
32
|
KeyNormalizer.prepare_concurrent(@map)
|
|
35
33
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
unless nil.equal?(normalize_keys)
|
|
35
|
+
restoring = KeyNormalizer.restoration?(normalize_keys)
|
|
36
|
+
normalizer = KeyNormalizer.build(normalize_keys, shareable: true)
|
|
37
|
+
KeyNormalizer.install_concurrent(self, normalizer) unless restoring
|
|
38
|
+
end
|
|
39
39
|
|
|
40
40
|
initial_mapping&.each { |key, value| self[key] = value }
|
|
41
41
|
KeyNormalizer.install_concurrent(self, normalizer) if restoring
|
|
@@ -154,7 +154,8 @@ module Farce
|
|
|
154
154
|
prepared_key = @map.normalize_external_key(key)
|
|
155
155
|
expected = wrap_comparison(expected)
|
|
156
156
|
deadline = Internal.timeout_deadline(timeout)
|
|
157
|
-
|
|
157
|
+
# JRuby rejects repeated anonymous block forwarding inside a block.
|
|
158
|
+
while true
|
|
158
159
|
current = @map.get_prepared(prepared_key, timeout: Clock.remaining(deadline)) { TIMED_OUT }
|
|
159
160
|
return unwrap_result(current, &) if TIMED_OUT.equal?(current)
|
|
160
161
|
return unwrap_value(current) unless values_equal?(nil.equal?(current) ? NIL_VALUE : current, expected)
|
data/lib/farce/internal.rb
CHANGED
|
@@ -113,9 +113,20 @@ module Farce
|
|
|
113
113
|
def storage_thread(thread) = thread
|
|
114
114
|
def prepare_mutable_numeric(_) = nil
|
|
115
115
|
def prepare_map_access(_klass, _kind) = nil
|
|
116
|
+
def prepare_shareable_class(_base) = nil
|
|
116
117
|
def finalize_engine = nil
|
|
117
118
|
def with_scheduler_thread_group = yield(ThreadGroup.new)
|
|
118
119
|
|
|
120
|
+
def initialize_range(target, first, last, exclude_end)
|
|
121
|
+
target.__send__(:initialize, first, last, exclude_end)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def publish_shareable(object)
|
|
125
|
+
Object.instance_method(:freeze).bind_call(object)
|
|
126
|
+
::Ractor.make_shareable(object) if native_ractors?
|
|
127
|
+
object
|
|
128
|
+
end
|
|
129
|
+
|
|
119
130
|
def delegate(from, to, *methods)
|
|
120
131
|
methods.each do |method|
|
|
121
132
|
if to.is_a?(Module)
|