fast_regexp 0.6.2-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: 631f8758ae432269424c547c8ea613ed475fc95d589f8d77eda6a9157a303297
4
- data.tar.gz: a7c2d5cf6328d43d2bb73692adfefcac19d1ffbdf22e7a91b3b053d02a53a195
3
+ metadata.gz: fb638b676baa678009503cd7a42a5a03befb73c00b79a72938480c74cd5a0538
4
+ data.tar.gz: 0033a9a765b873df3b9984fa138ebc534e9b3f0458d1a8018f71d3f5f768786e
5
5
  SHA512:
6
- metadata.gz: 4c9ce826241c325c128c72be6da2f4d3d5a3eb4a67f09b973bc752a2a1b1fdf6de8ee1515a89af5326df90a2726de2cd341dd888815ae1db34be73e6393ddd9b
7
- data.tar.gz: 7cc23ef4cdde598acf331187818e639ca846ecc8294958248d822d8fbd3f731d9da7a6021017b13b136660f982fc770201475d0e67a9755912a74a622e996016
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.2"
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",
@@ -96,8 +168,9 @@ module Fast
96
168
 
97
169
  def match(haystack)
98
170
  haystack = coerce_string(haystack)
99
- raw = fast? ? @backend._native_match(haystack) : @backend.match(haystack)
100
- raw && MatchData.new(raw, haystack)
171
+ return @backend._native_match(haystack) if fast?
172
+ m = @backend.match(haystack)
173
+ m && MatchData.new(m)
101
174
  end
102
175
 
103
176
  def match?(haystack)
@@ -113,8 +186,10 @@ module Fast
113
186
  # also returns bytes here for API consistency).
114
187
  def =~(other)
115
188
  return nil unless other.respond_to?(:to_str)
116
- m = match(other.to_str)
117
- 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]
118
193
  end
119
194
 
120
195
  def scan(haystack)
@@ -123,22 +198,17 @@ module Fast
123
198
 
124
199
  def scan_matches(haystack)
125
200
  haystack = coerce_string(haystack)
126
- if fast?
127
- @backend.scan_matches(haystack).map { |m| MatchData.new(m, haystack) }
128
- else
129
- results = []
130
- haystack.scan(@backend) { results << MatchData.new(::Regexp.last_match, haystack) }
131
- results
132
- end
201
+ return @backend.scan_matches(haystack) if fast?
202
+ results = []
203
+ haystack.scan(@backend) { results << MatchData.new(::Regexp.last_match) }
204
+ results
133
205
  end
134
206
 
135
207
  def sub(haystack, replacement = nil, literal: false, &block)
136
208
  haystack = coerce_string(haystack)
137
- if block
209
+ if block_given?
138
210
  raise ArgumentError, "wrong number of arguments (given 2, expected 1 with block)" if replacement
139
- m = match(haystack)
140
- return haystack.dup unless m
141
- "#{m.pre_match}#{block.call(m)}#{m.post_match}"
211
+ fast? ? @backend._native_sub_block(haystack, &block) : stdlib_sub_with_block(haystack, &block)
142
212
  else
143
213
  raise ArgumentError, "wrong number of arguments (given 1, expected 2)" if replacement.nil?
144
214
  replacement = coerce_string(replacement)
@@ -152,9 +222,9 @@ module Fast
152
222
 
153
223
  def gsub(haystack, replacement = nil, literal: false, &block)
154
224
  haystack = coerce_string(haystack)
155
- if block
225
+ if block_given?
156
226
  raise ArgumentError, "wrong number of arguments (given 2, expected 1 with block)" if replacement
157
- 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)
158
228
  else
159
229
  raise ArgumentError, "wrong number of arguments (given 1, expected 2)" if replacement.nil?
160
230
  replacement = coerce_string(replacement)
@@ -254,29 +324,14 @@ module Fast
254
324
  end
255
325
  end
256
326
 
257
- # Fast path: rust/regex has no native iterate-with-replace, so we scan all
258
- # match positions up-front, then splice the result by byte offset in one
259
- # pass. Stays UTF-8 since haystack and match slices are.
260
- def fast_gsub_with_block(haystack)
261
- matches = scan_matches(haystack)
262
- return haystack.dup if matches.empty?
263
-
264
- out = String.new(encoding: Encoding::UTF_8)
265
- cursor = 0
266
- matches.each do |m|
267
- bs, be = m.byteoffset(0)
268
- out << haystack.byteslice(cursor, bs - cursor) if bs > cursor
269
- out << yield(m).to_s
270
- cursor = be
271
- end
272
- out << haystack.byteslice(cursor, haystack.bytesize - cursor) if cursor < haystack.bytesize
273
- 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 }
274
331
  end
275
332
 
276
- # Stdlib path: String#gsub already does single-pass iterate-and-replace
277
- # and sets $~ inside the block, so wrap the current ::MatchData and yield.
278
333
  def stdlib_gsub_with_block(haystack)
279
- haystack.gsub(@backend) { yield(MatchData.new(::Regexp.last_match, haystack)).to_s }
334
+ haystack.gsub(@backend) { yield(MatchData.new(::Regexp.last_match)).to_s }
280
335
  end
281
336
 
282
337
  def coerce_string(value)
@@ -284,53 +339,5 @@ module Fast
284
339
  return value.to_str if value.respond_to?(:to_str)
285
340
  raise TypeError, "no implicit conversion of #{value.class} into String"
286
341
  end
287
-
288
- # Wraps either a Fast::Regexp::Native::MatchData or a stdlib ::MatchData
289
- # so callers see one type regardless of which backend ran.
290
- class MatchData
291
- include Enumerable
292
-
293
- attr_reader :backend, :string
294
-
295
- def initialize(backend, haystack)
296
- @backend = backend
297
- @string = haystack
298
- end
299
-
300
- def native? = @backend.is_a?(Fast::Regexp::Native::MatchData)
301
- def stdlib? = !native?
302
-
303
- def native = native? ? @backend : nil
304
- def stdlib = stdlib? ? @backend : nil
305
-
306
- def [](key) = @backend[key]
307
- def to_a = @backend.to_a
308
- def captures = @backend.captures
309
- def named_captures = @backend.named_captures
310
- def names = @backend.names
311
- def size = @backend.size
312
- alias_method :length, :size
313
- def pre_match = @backend.pre_match
314
- def post_match = @backend.post_match
315
- def to_s = @backend.to_s
316
-
317
- # Byte-based offsets. Both backends expose these (stdlib MatchData has
318
- # `byteoffset` / `byte_begin` / `byte_end` since Ruby 3.2).
319
- def byteoffset(key) = @backend.byteoffset(key)
320
- def byte_begin(key) = @backend.byte_begin(key)
321
- def byte_end(key) = @backend.byte_end(key)
322
-
323
- def each(&block) = to_a.each(&block)
324
- def values_at(*indices) = indices.map { |i| self[i] }
325
-
326
- def ==(other)
327
- other.is_a?(MatchData) && to_a == other.to_a && string == other.string
328
- end
329
- alias_method :eql?, :==
330
-
331
- def hash = [to_a, string].hash
332
-
333
- def inspect = "#<Fast::Regexp::MatchData #{to_s.inspect}>"
334
- end
335
342
  end
336
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.2
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: