farce 0.5.0-java → 0.6.0-java

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 (44) 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/lib/farce/abstract/atom.rb +5 -1
  6. data/lib/farce/abstract/counter.rb +5 -4
  7. data/lib/farce/abstract/molecule.rb +14 -14
  8. data/lib/farce/abstract/vector.rb +1 -1
  9. data/lib/farce/atom.rb +6 -0
  10. data/lib/farce/clock.rb +8 -7
  11. data/lib/farce/counter.rb +6 -0
  12. data/lib/farce/deduper.rb +12 -4
  13. data/lib/farce/engine/jruby/fiber_scheduler.jar +0 -0
  14. data/lib/farce/engine/jvm/counter.rb +0 -2
  15. data/lib/farce/engine/ruby/shared/weak_cache_mirror.rb +56 -0
  16. data/lib/farce/engine/ruby.rb +5 -1
  17. data/lib/farce/engine/shared/trie/ignore_case.rb +136 -0
  18. data/lib/farce/engine/shared/trie.rb +14 -1
  19. data/lib/farce/engine/shared/trie_builder.rb +19 -8
  20. data/lib/farce/engine/shared/unshared_vector.rb +3 -1
  21. data/lib/farce/engine/shared/vector.rb +2 -0
  22. data/lib/farce/engine/shared.rb +2 -0
  23. data/lib/farce/engine/truffleruby/native/counter.rb +0 -2
  24. data/lib/farce/engine/truffleruby.rb +9 -0
  25. data/lib/farce/extension.rb +5 -3
  26. data/lib/farce/integrations/sorted_set.rb +1 -0
  27. data/lib/farce/internal/fake_ractor.rb +1 -1
  28. data/lib/farce/internal/freeze.rb +1 -5
  29. data/lib/farce/internal/map_value_modes.rb +8 -7
  30. data/lib/farce/internal.rb +11 -0
  31. data/lib/farce/local/counter.rb +4 -10
  32. data/lib/farce/map.rb +8 -1
  33. data/lib/farce/memoize.rb +10 -5
  34. data/lib/farce/shareable.rb +3 -0
  35. data/lib/farce/strict/weak_cache.rb +199 -0
  36. data/lib/farce/vector.rb +8 -7
  37. data/lib/farce/version.rb +1 -1
  38. data/lib/farce/walker/definitions.rb +18 -0
  39. data/lib/farce/walker/modification.rb +28 -14
  40. data/lib/farce/walker.rb +9 -1
  41. data/lib/farce/weak_key_map.rb +1 -1
  42. data/lib/farce/weak_map.rb +1 -1
  43. data/lib/farce/weak_value_map.rb +1 -1
  44. metadata +5 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 565c706934f8c44d83e0e04621ba8f1750907cc4d36863e6cff44e5d7e60ba02
4
- data.tar.gz: 8bf03ad5685afafa3fc22e96f8c393c01a7f06681054f9661adc86a2d9c131f1
3
+ metadata.gz: f4a0f5afb79dd6bc07800615fb635a4d27e43e86182ab8755e30acdf098a1b02
4
+ data.tar.gz: 5ac35e034510294a7ec161dd0b507dd08da1c1789163b15595d97066cacbe4d4
5
5
  SHA512:
6
- metadata.gz: 4244dfd3d0e6f68cb8661f1861fffcbfdaf0890db3f6a56afb1b13809ea18e1186add54e95ed8ee156dd18f3eb0bd4fd6e9252a0e685bd2c5e276392add4167a
7
- data.tar.gz: 431ed85187432bd10bcb34ad8128bae689a797390510fd5687c61bbee3ad2e781a6bfb638f7ad97185ac7366bda8cd6cd37af678555c8d23d3143bb9fc9b8886
6
+ metadata.gz: a49dbafabb7541750f63181bc3d0dd624e7ae65d6fdd1aaa1cbb24757e0ed21d0e8724faac9319a2eb629723616badde31de1598028381bb80cda3dca0a9fa99
7
+ data.tar.gz: 8319b420ec31b816c842be424a114fd37a37313200210e15e4c4a07c0ce931a757e77064d5967fddfccec519aa3eaec7ad4c7190f33e33cb6155c0da0f9143c1
data/CHANGELOG.md CHANGED
@@ -6,6 +6,7 @@ Farce follows [Semantic Versioning 2.0](http://semver.org/). Any public method o
6
6
 
7
7
  Version | Date | Important Changes
8
8
  --------------------------------|------------|--------------------------------------
9
+ [0.6.0](#farce-060-2026-11-04) | 2026-11-04 | Performance improvements, better counters, and `WeakCache`
9
10
  [0.5.0](#farce-050-2026-10-21) | 2026-10-21 | Publishers, memoization, pool and queue resizing, and better concurrent-ruby compatibility
10
11
  [0.4.1](#farce-041-2026-10-18) | 2026-10-18 | Include improved io_uring support in prebuilt Linux binaries
11
12
  [0.4.0](#farce-040-2026-10-16) | 2026-10-16 | Introduce `Farce::Backports`
@@ -15,6 +16,23 @@ Farce follows [Semantic Versioning 2.0](http://semver.org/). Any public method o
15
16
 
16
17
  ## Release History
17
18
 
19
+ ### Farce 0.6.0 (2026-11-04)
20
+
21
+ #### Breaking Changes
22
+
23
+ * `Counter` and `Local::Counter` arithmetic methods (`increment`, `add`, `decrement`, `subtract`, and `remove`) now return the integer produced by the atomic update instead of the counter. Use `number = counter.increment` to allocate sequence numbers without a separate read. Calls can no longer be chained.
24
+
25
+ #### New Features
26
+
27
+ * `Farce.dedup` can now deduplicate `Regexp`, `Range`, `Data`, and `SortedSet` instances.
28
+ * `Farce::Walker` (and thus `Farce.freeze_graph`) correctly walk `Struct` and `Range` instances now.
29
+ * Add `Farce::Strict::WeakCache`. It offers a subset of the API of `Farce::Strict::WeakValueMap`, but without its performance penalty on CRuby 3.x and 4.0 (on other Rubies, including CRuby 4.1, it's just a tiny wrapper around the well-performing `WeakValueMap`).
30
+
31
+ #### Performance improvements
32
+
33
+ * Faster construction of Map and Vector instances.
34
+ * Speed up atoms and memoization.
35
+
18
36
  ### Farce 0.5.0 (2026-10-21)
19
37
 
20
38
  #### New Features
data/docs/modes.md CHANGED
@@ -390,7 +390,7 @@ The same choices appear in several Farce APIs. Containers normally default to `:
390
390
  | `Farce::Exchanger` | `new`, `exchange` | The value offered to a partner. |
391
391
  | `Farce::WeakAtom` | `new`, `store`, `update`, and other replacement operations | Weakly held values. Supports only `:raise`, `:make_shareable`, and `:dedup`. Defaults to `:raise`. |
392
392
  | `Farce::Atom` | `new`, `store`, `swap`, `update`, and other replacement operations | The stored value. |
393
- | `Farce::Memoize` | `memoize`, `[]` | Memoized results with `scope: :global` (the default). With `weak: true`, supports only `:raise`, `:make_shareable`, and `:dedup`, and defaults to `:raise`. Other scopes ignore the mode. |
393
+ | `Farce::Memoize` | `memoize`, `[]` | Memoized results with `scope: :global` (the default). `:strict` caches shareable results directly, including with `weak: true`. Weak memoization also accepts `:raise` (the default), `:make_shareable`, and `:dedup`. Other scopes ignore the mode. |
394
394
  | `Farce::Lazy`, `Farce::LazyRef` | `new` | The result computed once by the factory. |
395
395
  | `Farce::Task` | `new`, `schedule`, `try_schedule`, `in_parallel`, `on_main`, `add_argument` | Arguments and the successful result. `add_argument` overrides the mode for one argument. Scheduling can select local mode automatically. |
396
396
  | `Farce::Publisher`, `Farce::Unshared::Publisher`, `Farce::Local::Publisher` | `subscribe` with a block | Callback arguments and queued payloads. Defaults to `:smart` for `Publisher` and `:local` for its Unshared and Local variants. |
data/docs/utilities.md CHANGED
@@ -59,6 +59,7 @@ Other classes Farce provides include:
59
59
  * `ThreadExecutor`: A [task executor](scheduling.md#scheduling-code) creating a new thread for each unit of work. Used on TruffleRuby.
60
60
  * `Walker`: A tool for walking a Ruby object tree.
61
61
  * `WeakValue`: A value object version of [`WeakRef`](shims.md#weakref). Allows handling references more explicitly, without automatic method delegation.
62
+ * `Strict::WeakCache`: Ractor-shared weak maps are slow on CRuby before 4.1. `WeakCache` provides a faster alternative if entries aren't frequently garbage collected, but it does not allow deleting or replacing entries manually.
62
63
 
63
64
  In addition, Farce includes a range of error classes not listed here. Check the [API documentation](https://rkh.github.io/farce/) or [code base](https://github.com/rkh/farce/blob/main/lib/farce/error.rb) for these.
64
65
 
@@ -59,7 +59,11 @@ module Farce
59
59
  # @yield computes the value to store when the current value is nil
60
60
  # @yieldreturn [BasicObject, nil] the value to store
61
61
  # @return [BasicObject, nil] the current or newly stored value, or nil when the timeout expires
62
- def store_if_absent(timeout: nil, &) = internal_atom.store_if_absent(timeout:, &)
62
+ def store_if_absent(timeout: nil, &)
63
+ return internal_atom.store_if_absent(&) if timeout.nil?
64
+
65
+ internal_atom.store_if_absent(timeout:, &)
66
+ end
63
67
 
64
68
  # Atomically replace the current value if it matches the expected value.
65
69
  # @param expected [BasicObject, nil] the value to compare with the current value
@@ -15,14 +15,15 @@ module Farce
15
15
  # @return [Integer] The current value of the counter.
16
16
  #
17
17
  # @!method increment(by = 1)
18
- # Increment the counter by the given amount (default is 1).
18
+ # Atomically increment the counter by the given amount (default is 1).
19
+ # Use the returned integer to allocate a sequence number without a separate read.
19
20
  # @param [Numeric, String, #to_int] by The amount, converted to an Integer.
20
- # @return [self] Returns self for chaining.
21
+ # @return [Integer] The value produced by this atomic update.
21
22
  #
22
23
  # @!method decrement(by = 1)
23
- # Decrement the counter by the given amount (default is 1).
24
+ # Atomically decrement the counter by the given amount (default is 1).
24
25
  # @param [Numeric, String, #to_int] by The amount, converted to an Integer.
25
- # @return [self] Returns self for chaining.
26
+ # @return [Integer] The value produced by this atomic update.
26
27
  module Counter
27
28
  include Internal::MarshalSupport::Counter
28
29
  include Internal::Copyable
@@ -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
@@ -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)