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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +18 -0
- data/docs/modes.md +1 -1
- data/docs/utilities.md +1 -0
- 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/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 +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f4a0f5afb79dd6bc07800615fb635a4d27e43e86182ab8755e30acdf098a1b02
|
|
4
|
+
data.tar.gz: 5ac35e034510294a7ec161dd0b507dd08da1c1789163b15595d97066cacbe4d4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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).
|
|
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
|
|
data/lib/farce/abstract/atom.rb
CHANGED
|
@@ -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, &)
|
|
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
|
-
#
|
|
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 [
|
|
21
|
+
# @return [Integer] The value produced by this atomic update.
|
|
21
22
|
#
|
|
22
23
|
# @!method decrement(by = 1)
|
|
23
|
-
#
|
|
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 [
|
|
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) =
|
|
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
|
|
@@ -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)
|