fast_regexp 0.6.1-aarch64-linux → 0.7.0-aarch64-linux

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9e8f43c5d2a9aa1903943558c35ab6a46b15df0b752c652f6442bd6a328bce7c
4
- data.tar.gz: 666effbf46f4db06dbc5b644c88202daac203e7c167cf060cbb5d1f2e63d66a5
3
+ metadata.gz: fb638b676baa678009503cd7a42a5a03befb73c00b79a72938480c74cd5a0538
4
+ data.tar.gz: 0033a9a765b873df3b9984fa138ebc534e9b3f0458d1a8018f71d3f5f768786e
5
5
  SHA512:
6
- metadata.gz: 38ca3692c0b727e5003acca7e24031b73c19fd2957bfbe715dae5c5edd2065fa93ba9e665d2f6cebbc721513957c25193c5c5ca3125e6be59bbc236244fefe52
7
- data.tar.gz: '040688216ab69d2ab613548b3dab2b798e90a776c2cff4d107ddc9f4208943b0439cb8d8124128dcb6015b81a316eae4cf8f8cc49d44bdcf5a7126c6b6216146'
6
+ metadata.gz: 4d30c68136e478c8adcf061bd06d2df042e1af051bce51fc38076f960c8bb9d6eec0336c895dec3dec369401f1bb9207975a128298cd1ac03e2752f0a3281cb8
7
+ data.tar.gz: aad4a7176414ee857ed229bc0c4cafcfeba8e2ed93b6743c05e924c5d3c8559541cbc1849553831d475c35946ec5d4dc6f13b389f6b8f308fe88111e64e81d71
data/README.md CHANGED
@@ -244,7 +244,18 @@ In-depth docs live under [`docs/`](docs/README.md), organized via the
244
244
  - **Tutorial:** [Getting started](docs/tutorials/getting-started.md)
245
245
  - **How-to:** [Migrate from stdlib `::Regexp`](docs/how-to/migrate-from-stdlib-regexp.md), [Handle unsupported syntax](docs/how-to/handle-unsupported-syntax.md)
246
246
  - **Reference:** [`Fast::Regexp`](docs/reference/fast-regexp.md), [`MatchData`](docs/reference/fast-regexp-matchdata.md), [`Set`](docs/reference/fast-regexp-set.md)
247
- - **Explainers:** [Engine fallback](docs/explainers/engine-fallback.md)
247
+ - **Explainers:** [Engine fallback](docs/explainers/engine-fallback.md), [Benchmarks](docs/explainers/benchmarks.md)
248
+
249
+ ## Performance
250
+
251
+ Matching runs 8–10x stdlib `::Regexp` on a hit or a miss, scanning 2–5x,
252
+ `gsub` with a template 7–13x; reading a `MatchData` costs the same objects
253
+ as `::MatchData`, and `match?`, `=~`, `===` and `Set#match?` allocate
254
+ nothing. Compiling a pattern is the one place rust/regex is slower (it
255
+ builds its automaton up front), so compile once and keep the object. Every
256
+ operation, with the stdlib equivalent beside it, is on the
257
+ [benchmarks page](docs/explainers/benchmarks.md); reproduce with
258
+ `bundle exec ruby benchmark/operations.rb`.
248
259
 
249
260
  ## Development
250
261
 
@@ -253,6 +264,7 @@ bin/setup # install deps
253
264
  bin/console # interactive prompt to play around
254
265
  rake compile # (re)compile extension
255
266
  rake spec # run tests
267
+ bundle exec ruby benchmark/operations.rb # the benchmarks page's table
256
268
  ```
257
269
 
258
270
  ## Contributing
Binary file
Binary file
Binary file
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Fast
4
4
  class Regexp
5
- VERSION = "0.6.1"
5
+ VERSION = "0.7.0"
6
6
  end
7
7
  end
data/lib/fast_regexp.rb CHANGED
@@ -21,6 +21,62 @@ module Fast
21
21
  candidates = [File.join(base, abi, "fast_regexp"), File.join(base, "fast_regexp")]
22
22
  candidates.find { |stem| NATIVE_EXTENSIONS.any? { |ext| File.exist?(stem + ext) } }
23
23
  end
24
+
25
+ # One match, whichever engine produced it. On the fast path the match
26
+ # *is* a `Fast::Regexp::Native::MatchData`, a subclass of this one
27
+ # defined by the extension (so it must exist before the extension
28
+ # loads); on the stdlib path it's an instance of this class wrapping the
29
+ # `::MatchData`. Same public surface either way.
30
+ class MatchData
31
+ include Enumerable
32
+
33
+ attr_reader :backend
34
+
35
+ def initialize(backend)
36
+ @backend = backend
37
+ end
38
+
39
+ def native? = false
40
+ def stdlib? = true
41
+
42
+ def native = nil
43
+ def stdlib = @backend
44
+
45
+ def [](key) = @backend[key]
46
+ def to_a = @backend.to_a
47
+ def captures = @backend.captures
48
+ def named_captures = @backend.named_captures
49
+ def names = @backend.names
50
+ def size = @backend.size
51
+ alias_method :length, :size
52
+ def pre_match = @backend.pre_match
53
+ def post_match = @backend.post_match
54
+ def to_s = @backend.to_s
55
+ alias_method :match, :to_s
56
+
57
+ # The haystack the match was taken over: `::MatchData#string` is already
58
+ # a frozen snapshot, the same thing the native subclass keeps.
59
+ def string = @backend.string
60
+
61
+ # Byte-based offsets. `::MatchData#byteoffset` exists since Ruby 3.2;
62
+ # its `byte_begin`/`byte_end` don't (3.4 added `bytebegin`/`byteend`),
63
+ # so those two derive from `byteoffset` here.
64
+ def byteoffset(key) = @backend.byteoffset(key)
65
+ def byte_begin(key) = byteoffset(key)[0]
66
+ def byte_end(key) = byteoffset(key)[1]
67
+
68
+ def each(&block) = to_a.each(&block)
69
+ def values_at(*indices) = indices.map { |i| self[i] }
70
+
71
+ def ==(other)
72
+ other.is_a?(MatchData) && to_a == other.to_a && string == other.string
73
+ end
74
+ alias_method :eql?, :==
75
+
76
+ def hash = [to_a, string].hash
77
+
78
+ def inspect = "#<Fast::Regexp::MatchData #{to_s.inspect}>"
79
+ end
24
80
  end
25
81
  end
26
82
 
@@ -30,6 +86,22 @@ require native
30
86
 
31
87
  module Fast
32
88
  class Regexp
89
+ class Native
90
+ class MatchData
91
+ def native? = true
92
+ def stdlib? = false
93
+
94
+ def native = self
95
+ def stdlib = nil
96
+ def backend = self
97
+
98
+ # Immutable, so a copy is the object itself (the wrapped class has no
99
+ # allocator for `dup`/`clone` to go through).
100
+ def dup = self
101
+ def clone(freeze: nil) = self
102
+ end
103
+ end
104
+
33
105
  RUBY_FLAG_MAP = {
34
106
  ::Regexp::IGNORECASE => "i",
35
107
  ::Regexp::EXTENDED => "x",
@@ -45,13 +117,21 @@ module Fast
45
117
  allocate.tap { |re| re.send(:initialize, translated, original: pattern, **opts) }
46
118
  end
47
119
 
48
- # Bulk-compile a symbol-keyed hash of patterns. Handy for defining a set
49
- # of regex constants in one shot:
120
+ # Bulk-compile a set of patterns. Two call shapes:
121
+ #
122
+ # Fast::Regexp.create_many(word: '\w+', num: '\d+')
123
+ # # => { word: #<Fast::Regexp ...>, num: #<Fast::Regexp ...> }
50
124
  #
51
- # RE = Fast::Regexp.create_many(word: '\w+', num: '\d+').freeze
52
- # RE[:word].match("hello")
53
- def create_many(**patterns)
54
- patterns.transform_values { |pat| new(pat) }
125
+ # Fast::Regexp.create_many('\w+', '\d+')
126
+ # # => [#<Fast::Regexp ...>, #<Fast::Regexp ...>]
127
+ #
128
+ # Mixing the two raises ArgumentError — pick one shape per call.
129
+ def create_many(*patterns, **named)
130
+ if !patterns.empty? && !named.empty?
131
+ raise ArgumentError, "create_many accepts positional patterns OR keyword patterns, not both"
132
+ end
133
+ return named.transform_values { |pat| new(pat) } if patterns.empty?
134
+ patterns.map { |pat| new(pat) }
55
135
  end
56
136
 
57
137
  private
@@ -88,8 +168,9 @@ module Fast
88
168
 
89
169
  def match(haystack)
90
170
  haystack = coerce_string(haystack)
91
- raw = fast? ? @backend._native_match(haystack) : @backend.match(haystack)
92
- raw && MatchData.new(raw, haystack)
171
+ return @backend._native_match(haystack) if fast?
172
+ m = @backend.match(haystack)
173
+ m && MatchData.new(m)
93
174
  end
94
175
 
95
176
  def match?(haystack)
@@ -105,8 +186,10 @@ module Fast
105
186
  # also returns bytes here for API consistency).
106
187
  def =~(other)
107
188
  return nil unless other.respond_to?(:to_str)
108
- m = match(other.to_str)
109
- m && m.byte_begin(0)
189
+ haystack = other.to_str
190
+ return @backend._native_find(haystack) if fast?
191
+ m = @backend.match(haystack)
192
+ m && m.byteoffset(0)[0]
110
193
  end
111
194
 
112
195
  def scan(haystack)
@@ -115,22 +198,17 @@ module Fast
115
198
 
116
199
  def scan_matches(haystack)
117
200
  haystack = coerce_string(haystack)
118
- if fast?
119
- @backend.scan_matches(haystack).map { |m| MatchData.new(m, haystack) }
120
- else
121
- results = []
122
- haystack.scan(@backend) { results << MatchData.new(::Regexp.last_match, haystack) }
123
- results
124
- end
201
+ return @backend.scan_matches(haystack) if fast?
202
+ results = []
203
+ haystack.scan(@backend) { results << MatchData.new(::Regexp.last_match) }
204
+ results
125
205
  end
126
206
 
127
207
  def sub(haystack, replacement = nil, literal: false, &block)
128
208
  haystack = coerce_string(haystack)
129
- if block
209
+ if block_given?
130
210
  raise ArgumentError, "wrong number of arguments (given 2, expected 1 with block)" if replacement
131
- m = match(haystack)
132
- return haystack.dup unless m
133
- "#{m.pre_match}#{block.call(m)}#{m.post_match}"
211
+ fast? ? @backend._native_sub_block(haystack, &block) : stdlib_sub_with_block(haystack, &block)
134
212
  else
135
213
  raise ArgumentError, "wrong number of arguments (given 1, expected 2)" if replacement.nil?
136
214
  replacement = coerce_string(replacement)
@@ -144,9 +222,9 @@ module Fast
144
222
 
145
223
  def gsub(haystack, replacement = nil, literal: false, &block)
146
224
  haystack = coerce_string(haystack)
147
- if block
225
+ if block_given?
148
226
  raise ArgumentError, "wrong number of arguments (given 2, expected 1 with block)" if replacement
149
- fast? ? fast_gsub_with_block(haystack, &block) : stdlib_gsub_with_block(haystack, &block)
227
+ fast? ? @backend._native_gsub_block(haystack, &block) : stdlib_gsub_with_block(haystack, &block)
150
228
  else
151
229
  raise ArgumentError, "wrong number of arguments (given 1, expected 2)" if replacement.nil?
152
230
  replacement = coerce_string(replacement)
@@ -246,29 +324,14 @@ module Fast
246
324
  end
247
325
  end
248
326
 
249
- # Fast path: rust/regex has no native iterate-with-replace, so we scan all
250
- # match positions up-front, then splice the result by byte offset in one
251
- # pass. Stays UTF-8 since haystack and match slices are.
252
- def fast_gsub_with_block(haystack)
253
- matches = scan_matches(haystack)
254
- return haystack.dup if matches.empty?
255
-
256
- out = String.new(encoding: Encoding::UTF_8)
257
- cursor = 0
258
- matches.each do |m|
259
- bs, be = m.byteoffset(0)
260
- out << haystack.byteslice(cursor, bs - cursor) if bs > cursor
261
- out << yield(m).to_s
262
- cursor = be
263
- end
264
- out << haystack.byteslice(cursor, haystack.bytesize - cursor) if cursor < haystack.bytesize
265
- out
327
+ # Stdlib path: String#sub/#gsub already do single-pass iterate-and-replace
328
+ # and set $~ inside the block, so wrap the current ::MatchData and yield.
329
+ def stdlib_sub_with_block(haystack)
330
+ haystack.sub(@backend) { yield(MatchData.new(::Regexp.last_match)).to_s }
266
331
  end
267
332
 
268
- # Stdlib path: String#gsub already does single-pass iterate-and-replace
269
- # and sets $~ inside the block, so wrap the current ::MatchData and yield.
270
333
  def stdlib_gsub_with_block(haystack)
271
- haystack.gsub(@backend) { yield(MatchData.new(::Regexp.last_match, haystack)).to_s }
334
+ haystack.gsub(@backend) { yield(MatchData.new(::Regexp.last_match)).to_s }
272
335
  end
273
336
 
274
337
  def coerce_string(value)
@@ -276,53 +339,5 @@ module Fast
276
339
  return value.to_str if value.respond_to?(:to_str)
277
340
  raise TypeError, "no implicit conversion of #{value.class} into String"
278
341
  end
279
-
280
- # Wraps either a Fast::Regexp::Native::MatchData or a stdlib ::MatchData
281
- # so callers see one type regardless of which backend ran.
282
- class MatchData
283
- include Enumerable
284
-
285
- attr_reader :backend, :string
286
-
287
- def initialize(backend, haystack)
288
- @backend = backend
289
- @string = haystack
290
- end
291
-
292
- def native? = @backend.is_a?(Fast::Regexp::Native::MatchData)
293
- def stdlib? = !native?
294
-
295
- def native = native? ? @backend : nil
296
- def stdlib = stdlib? ? @backend : nil
297
-
298
- def [](key) = @backend[key]
299
- def to_a = @backend.to_a
300
- def captures = @backend.captures
301
- def named_captures = @backend.named_captures
302
- def names = @backend.names
303
- def size = @backend.size
304
- alias_method :length, :size
305
- def pre_match = @backend.pre_match
306
- def post_match = @backend.post_match
307
- def to_s = @backend.to_s
308
-
309
- # Byte-based offsets. Both backends expose these (stdlib MatchData has
310
- # `byteoffset` / `byte_begin` / `byte_end` since Ruby 3.2).
311
- def byteoffset(key) = @backend.byteoffset(key)
312
- def byte_begin(key) = @backend.byte_begin(key)
313
- def byte_end(key) = @backend.byte_end(key)
314
-
315
- def each(&block) = to_a.each(&block)
316
- def values_at(*indices) = indices.map { |i| self[i] }
317
-
318
- def ==(other)
319
- other.is_a?(MatchData) && to_a == other.to_a && string == other.string
320
- end
321
- alias_method :eql?, :==
322
-
323
- def hash = [to_a, string].hash
324
-
325
- def inspect = "#<Fast::Regexp::MatchData #{to_s.inspect}>"
326
- end
327
342
  end
328
343
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fast_regexp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.1
4
+ version: 0.7.0
5
5
  platform: aarch64-linux
6
6
  authors:
7
7
  - Eric Jacobs
@@ -9,10 +9,10 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2026-05-16 00:00:00.000000000 Z
12
+ date: 2026-09-19 00:00:00.000000000 Z
13
13
  dependencies: []
14
14
  description: 'Ruby bindings to rust/regex with a Fast::Regexp API: MatchData, sub/gsub,
15
- ===, =~, named captures, and GVL release for thread/fiber-friendly matching.'
15
+ ===, =~, & named captures.'
16
16
  email:
17
17
  - eric@ebj.dev
18
18
  executables: []
@@ -31,6 +31,7 @@ licenses:
31
31
  - MIT
32
32
  metadata:
33
33
  bug_tracker_uri: https://github.com/jetpks/fast_regexp/issues
34
+ changelog_uri: https://github.com/jetpks/fast_regexp/blob/main/CHANGELOG.md
34
35
  homepage_uri: https://github.com/jetpks/fast_regexp
35
36
  source_code_uri: https://github.com/jetpks/fast_regexp
36
37
  post_install_message: