farce 0.5.0-arm-linux-gnu → 0.6.0-arm-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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +18 -0
  3. data/docs/modes.md +1 -1
  4. data/docs/utilities.md +1 -0
  5. data/ext/farce/atom.c +23 -7
  6. data/ext/farce/containers.h +3 -0
  7. data/ext/farce/counter.c +4 -6
  8. data/ext/farce/depend +3 -1
  9. data/ext/farce/extconf.rb +19 -14
  10. data/ext/farce/farce.c +101 -3
  11. data/ext/farce/map.c +1 -4
  12. data/ext/farce/trie.c +140 -72
  13. data/ext/farce/trie_search.h +114 -0
  14. data/ext/farce/vector.c +50 -18
  15. data/ext/farce/weak_map.c +4 -8
  16. data/ext/farce/weak_references.h +23 -0
  17. data/ext/java/org/farce/FiberScheduler.java +8 -6
  18. data/lib/farce/abstract/atom.rb +5 -1
  19. data/lib/farce/abstract/counter.rb +5 -4
  20. data/lib/farce/abstract/molecule.rb +14 -14
  21. data/lib/farce/abstract/vector.rb +1 -1
  22. data/lib/farce/atom.rb +6 -0
  23. data/lib/farce/clock.rb +8 -7
  24. data/lib/farce/counter.rb +6 -0
  25. data/lib/farce/deduper.rb +12 -4
  26. data/lib/farce/engine/jruby/fiber_scheduler.jar +0 -0
  27. data/lib/farce/engine/jvm/counter.rb +0 -2
  28. data/lib/farce/engine/ruby/3.4/farce.so +0 -0
  29. data/lib/farce/engine/ruby/4.0/farce.so +0 -0
  30. data/lib/farce/engine/ruby/shared/weak_cache_mirror.rb +56 -0
  31. data/lib/farce/engine/ruby.rb +5 -1
  32. data/lib/farce/engine/shared/trie/ignore_case.rb +136 -0
  33. data/lib/farce/engine/shared/trie.rb +14 -1
  34. data/lib/farce/engine/shared/trie_builder.rb +19 -8
  35. data/lib/farce/engine/shared/unshared_vector.rb +3 -1
  36. data/lib/farce/engine/shared/vector.rb +2 -0
  37. data/lib/farce/engine/shared.rb +2 -0
  38. data/lib/farce/engine/truffleruby/native/counter.rb +0 -2
  39. data/lib/farce/engine/truffleruby.rb +9 -0
  40. data/lib/farce/extension.rb +5 -3
  41. data/lib/farce/integrations/sorted_set.rb +1 -0
  42. data/lib/farce/internal/fake_ractor.rb +1 -1
  43. data/lib/farce/internal/freeze.rb +1 -5
  44. data/lib/farce/internal/map_value_modes.rb +8 -7
  45. data/lib/farce/internal.rb +11 -0
  46. data/lib/farce/local/counter.rb +4 -10
  47. data/lib/farce/map.rb +8 -1
  48. data/lib/farce/memoize.rb +10 -5
  49. data/lib/farce/shareable.rb +3 -0
  50. data/lib/farce/strict/weak_cache.rb +199 -0
  51. data/lib/farce/vector.rb +8 -7
  52. data/lib/farce/version.rb +1 -1
  53. data/lib/farce/walker/definitions.rb +18 -0
  54. data/lib/farce/walker/modification.rb +28 -14
  55. data/lib/farce/walker.rb +9 -1
  56. data/lib/farce/weak_key_map.rb +1 -1
  57. data/lib/farce/weak_map.rb +1 -1
  58. data/lib/farce/weak_value_map.rb +1 -1
  59. 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) = source.is_a?(Integer) ? Array.new(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 now current
119
- alias at time
120
- alias timeout_at time
121
- alias delay offset
122
- alias in offset
123
- alias timeout offset
124
- alias wait offset
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, and Ruby Sets are frozen
8
- # and cached by default. Other objects are traversed but are not cached unless
9
- # their class matches {#store}. Modules and classes are skipped by default.
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -60,7 +60,6 @@ module Farce
60
60
  check_frozen!
61
61
  end
62
62
  change(delta)
63
- self
64
63
  end
65
64
 
66
65
  def decrement(delta = 1)
@@ -70,7 +69,6 @@ module Farce
70
69
  check_frozen!
71
70
  end
72
71
  change(-delta)
73
- self
74
72
  end
75
73
 
76
74
  alias value get
@@ -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)
@@ -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
- def define(*targets, &)
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
@@ -7,5 +7,6 @@ require "farce" unless defined?(Farce::Abstract)
7
7
  require "sorted_set"
8
8
 
9
9
  module Farce
10
+ Farce.dedup.store(::SortedSet)
10
11
  Internal::Converter.define(::SortedSet, :SortedSet)
11
12
  end
@@ -61,7 +61,7 @@ module Farce
61
61
  STATES[self] = state
62
62
  GROUPS[group] = self
63
63
 
64
- state.main_thread = Thread.new do # rubocop:disable Lint/UselessSetterCall
64
+ state.main_thread = Thread.new do
65
65
  current = Thread.current
66
66
  group.add(current)
67
67
  current.freeze
@@ -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
- identity = BasicObject.instance_method(:equal?)
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
- restoring = KeyNormalizer.restoration?(normalize_keys)
37
- normalizer = KeyNormalizer.build(normalize_keys, shareable: true)
38
- KeyNormalizer.install_concurrent(self, normalizer) unless restoring
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
- loop do
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)
@@ -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)