fast_regexp 0.8.0 → 0.9.0
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/Cargo.lock +1 -0
- data/README.md +82 -14
- data/ext/fast_regexp/Cargo.toml +1 -0
- data/ext/fast_regexp/src/lib.rs +48 -41
- data/lib/fast_regexp/version.rb +1 -1
- data/lib/fast_regexp.rb +8 -9
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e6b732dadd490b380381133c5d755016c834952213d151d909ab3bd4dc0276d6
|
|
4
|
+
data.tar.gz: c03fa2c959dbd59b65fbafac9dd72fb537131bef777722d9a95d4ad10a6be191
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1b8b6c63c8af3d0d0863465fa35b75b9d9d64f20c817b5782e3ed40022ab03ff53a414cd22a5843b58a919ac17ce21f8479d3becadd38bebfdfa4cdd92e67014
|
|
7
|
+
data.tar.gz: 22f5d0a36706934cc3c1b7cf7eb9b8bfef8de839e09c80dba181cb3947fcd92ee2c1ae56f8494c4aa03c850e5e4af7ae656b5d05bea983e137742b58aaead745
|
data/Cargo.lock
CHANGED
data/README.md
CHANGED
|
@@ -20,7 +20,7 @@ gem install fast_regexp
|
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
Precompiled native gems are published for **arm64-darwin**, **x86_64-linux**,
|
|
23
|
-
and **aarch64-linux** against Ruby **3.
|
|
23
|
+
and **aarch64-linux** against Ruby **3.4** and **4.0** — no Rust
|
|
24
24
|
toolchain required on those platforms.
|
|
25
25
|
|
|
26
26
|
On any other platform/Ruby combo, Bundler/RubyGems falls back to the source
|
|
@@ -39,7 +39,7 @@ Regular expressions should be pre-compiled before use:
|
|
|
39
39
|
|
|
40
40
|
```ruby
|
|
41
41
|
re = Fast::Regexp.new('p.t{2}ern*')
|
|
42
|
-
# => #<Fast::Regexp
|
|
42
|
+
# => #<Fast::Regexp "p.t{2}ern*">
|
|
43
43
|
```
|
|
44
44
|
|
|
45
45
|
> [!TIP]
|
|
@@ -101,6 +101,9 @@ Fast::Regexp.new('(\w+):(\d+)').scan("ruby:123, rust:456")
|
|
|
101
101
|
# => [["ruby", "123"], ["rust", "456"]]
|
|
102
102
|
```
|
|
103
103
|
|
|
104
|
+
Both `#scan` and `#scan_matches` run on either engine and return identical
|
|
105
|
+
results, including on patterns that fell back to stdlib `::Regexp`.
|
|
106
|
+
|
|
104
107
|
For per-match positions and pre/post-match access, use `#scan_matches`:
|
|
105
108
|
|
|
106
109
|
```ruby
|
|
@@ -216,13 +219,28 @@ Fast::Regexp::Set.new(["abc", "def"]).patterns
|
|
|
216
219
|
|
|
217
220
|
## Encoding
|
|
218
221
|
|
|
219
|
-
|
|
222
|
+
`fast_regexp` expects UTF-8 haystacks — ASCII-8BIT strings work too, and match
|
|
223
|
+
results always come back tagged UTF-8. Matching runs over the raw bytes
|
|
224
|
+
(rust/regex's `regex::bytes`), so searching a haystack that contains invalid
|
|
225
|
+
UTF-8 doesn't raise, where stdlib raises `ArgumentError`:
|
|
226
|
+
|
|
227
|
+
```ruby
|
|
228
|
+
"abc\xFFdef" =~ /a(.)c/
|
|
229
|
+
# => ArgumentError: invalid byte sequence in UTF-8
|
|
220
230
|
|
|
221
|
-
|
|
231
|
+
Fast::Regexp.new('a(.)c').match("abc\xFFdef")
|
|
232
|
+
# => #<Fast::Regexp::MatchData "abc">
|
|
233
|
+
```
|
|
222
234
|
|
|
223
|
-
In
|
|
235
|
+
In the default Unicode-aware mode a `.` or character class won't match
|
|
236
|
+
*across* an invalid byte. To match bytes anywhere, disable Unicode
|
|
237
|
+
awareness — both `Fast::Regexp` and `Fast::Regexp::Set` accept `unicode:`
|
|
238
|
+
(default `true`):
|
|
224
239
|
|
|
225
240
|
```ruby
|
|
241
|
+
Fast::Regexp.new('c(.)d', unicode: false).match("abc\xFFdef").to_a
|
|
242
|
+
# => ["c\xFFd", "\xFF"]
|
|
243
|
+
|
|
226
244
|
Fast::Regexp.new('\w+').match('ю٤夏')[0]
|
|
227
245
|
# => "ю٤夏"
|
|
228
246
|
|
|
@@ -236,26 +254,76 @@ Fast::Regexp::Set.new(['\w', '\d', '\s'], unicode: false).match("ю٤\u2000")
|
|
|
236
254
|
# => []
|
|
237
255
|
```
|
|
238
256
|
|
|
257
|
+
## Ractors
|
|
258
|
+
|
|
259
|
+
Patterns, sets, and match results support `Ractor.make_shareable` — it
|
|
260
|
+
returns the same object, now frozen. The native extension declares itself
|
|
261
|
+
Ractor-safe at load time, so there is nothing to enable:
|
|
262
|
+
|
|
263
|
+
```ruby
|
|
264
|
+
re = Ractor.make_shareable(Fast::Regexp.new('(?P<word>\w+)=(?P<num>\d+)'))
|
|
265
|
+
|
|
266
|
+
workers = Array.new(4) do
|
|
267
|
+
Ractor.new(re) do |pattern|
|
|
268
|
+
pattern.match("a=1 b=2")[:word] # => "a"
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
workers.map { |r| r.respond_to?(:value) ? r.value : r.take }
|
|
272
|
+
# => ["a", "a", "a", "a"]
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
Ruby 3.4 reads a Ractor's result with `#take`; Ruby 4.0 replaced it with
|
|
276
|
+
`#value`. The `respond_to?` read above runs on both — the same idiom the
|
|
277
|
+
spec suite uses.
|
|
278
|
+
|
|
279
|
+
What is guaranteed (each point pinned by
|
|
280
|
+
[`spec/fast_regexp/ractor_spec.rb`](spec/fast_regexp/ractor_spec.rb)):
|
|
281
|
+
|
|
282
|
+
- A shared pattern is fully usable in every worker: `match`, `match?`,
|
|
283
|
+
`===`, `=~`, `scan`, `scan_matches`, and `sub`/`gsub` in template and
|
|
284
|
+
block form all work, with the same results as on the main Ractor.
|
|
285
|
+
- Workers can `require "fast_regexp"` concurrently, including as the gem's
|
|
286
|
+
first load, and can construct their own patterns, matches, and sets —
|
|
287
|
+
the first construction of every native type works inside a non-main
|
|
288
|
+
Ractor.
|
|
289
|
+
- Matches are frozen snapshots: the haystack is snapshotted at match time,
|
|
290
|
+
so mutating it afterwards and then running GC with compaction leaves a
|
|
291
|
+
shared match's data intact. `MatchData#string` returns that snapshot.
|
|
292
|
+
- Stdlib-fallback patterns are shareable too, and substitute correctly
|
|
293
|
+
after being frozen or made shareable, without any warm-up call.
|
|
294
|
+
|
|
295
|
+
Recipes: [Use fast_regexp inside Ractors](docs/how-to/use-in-ractors.md).
|
|
296
|
+
Per-class details: the "Ractor safety" sections of
|
|
297
|
+
[`Fast::Regexp`](docs/reference/fast-regexp.md),
|
|
298
|
+
[`MatchData`](docs/reference/fast-regexp-matchdata.md), and
|
|
299
|
+
[`Set`](docs/reference/fast-regexp-set.md).
|
|
300
|
+
|
|
239
301
|
## Documentation
|
|
240
302
|
|
|
241
303
|
In-depth docs live under [`docs/`](docs/README.md), organized via the
|
|
242
304
|
[Diátaxis](https://diataxis.fr/) framework:
|
|
243
305
|
|
|
244
306
|
- **Tutorial:** [Getting started](docs/tutorials/getting-started.md)
|
|
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)
|
|
307
|
+
- **How-to:** [Migrate from stdlib `::Regexp`](docs/how-to/migrate-from-stdlib-regexp.md), [Handle unsupported syntax](docs/how-to/handle-unsupported-syntax.md), [Use fast_regexp inside Ractors](docs/how-to/use-in-ractors.md)
|
|
246
308
|
- **Reference:** [`Fast::Regexp`](docs/reference/fast-regexp.md), [`MatchData`](docs/reference/fast-regexp-matchdata.md), [`Set`](docs/reference/fast-regexp-set.md)
|
|
247
309
|
- **Explainers:** [Engine fallback](docs/explainers/engine-fallback.md), [Benchmarks](docs/explainers/benchmarks.md)
|
|
248
310
|
|
|
249
311
|
## Performance
|
|
250
312
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
313
|
+
Measured with `bundle exec ruby benchmark/operations.rb` — one run on
|
|
314
|
+
2026-11-05, Apple M4 Max, `ruby 4.0.9`, fast_regexp 0.9.0 (the full table is
|
|
315
|
+
on the [benchmarks page](docs/explainers/benchmarks.md)):
|
|
316
|
+
|
|
317
|
+
- `match?` runs 8–9x stdlib `::Regexp`, on a hit and on a 200 KB miss;
|
|
318
|
+
`scan` 2–5x; `gsub` with a template 7x on a 200 KB haystack — in one
|
|
319
|
+
object, where stdlib allocates a `String` per match. `Set#match?` over
|
|
320
|
+
four patterns is 2x a `Regexp.union`.
|
|
321
|
+
- Reading a `MatchData` costs the same objects as `::MatchData`: each
|
|
322
|
+
accessor allocates exactly what it returns. `match?`, `=~`, `===` and
|
|
323
|
+
`Set#match?` allocate nothing.
|
|
324
|
+
- Compiling a pattern is the one place rust/regex is slower: it builds its
|
|
325
|
+
automaton up front (~256 µs for a small pattern in that run, against
|
|
326
|
+
stdlib's ~1 µs), so compile once and keep the object.
|
|
259
327
|
|
|
260
328
|
## Development
|
|
261
329
|
|
data/ext/fast_regexp/Cargo.toml
CHANGED
data/ext/fast_regexp/src/lib.rs
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
use magnus::{
|
|
2
2
|
function, gc, method,
|
|
3
3
|
scan_args::{get_kwargs, scan_args},
|
|
4
|
+
typed_data::Obj,
|
|
4
5
|
value::{Opaque, ReprValue},
|
|
5
6
|
DataTypeFunctions, Error, Integer, Module, Object, RArray, RClass, RHash, RString, Ruby,
|
|
6
7
|
Symbol, TryConvert, TypedData, Value,
|
|
7
8
|
};
|
|
8
9
|
use regex::bytes::{NoExpand, Regex, RegexBuilder, RegexSet, RegexSetBuilder};
|
|
9
10
|
use std::collections::HashMap;
|
|
10
|
-
use std::sync::
|
|
11
|
+
use std::sync::Arc;
|
|
11
12
|
|
|
12
13
|
/// A haystack's bytes, read in place for the duration of one native call.
|
|
13
14
|
///
|
|
14
15
|
/// Sound because the call hands nothing back to Ruby while the borrow lives:
|
|
15
|
-
/// the GVL is held and no block is yielded
|
|
16
|
-
///
|
|
16
|
+
/// the current Ractor's GVL is held and no block is yielded; strings shared
|
|
17
|
+
/// with other Ractors are frozen. A GC triggered by result objects neither
|
|
17
18
|
/// frees nor moves it — a method argument is rooted and pinned on the VM
|
|
18
19
|
/// stack for the whole call. Paths that *do* run Ruby code mid-call (the
|
|
19
20
|
/// block forms of `sub`/`gsub`) read a frozen snapshot instead; see
|
|
@@ -42,61 +43,47 @@ struct RegexInner {
|
|
|
42
43
|
regex: Regex,
|
|
43
44
|
/// `name -> capture index`. Empty when the pattern has no named captures.
|
|
44
45
|
names: HashMap<String, usize>,
|
|
45
|
-
///
|
|
46
|
-
|
|
47
|
-
///
|
|
48
|
-
|
|
49
|
-
/// String objects on every call. Built on first use, from a method of a
|
|
50
|
-
/// live wrapper, so something marks it from the moment it exists; built
|
|
51
|
-
/// into the Array as each name is interned, because the interning table
|
|
52
|
-
/// holds its strings only weakly and a GC between two interns would
|
|
53
|
-
/// otherwise free the first. Marked by every wrapper holding this `Arc`.
|
|
54
|
-
ruby_names: OnceLock<Opaque<RArray>>,
|
|
46
|
+
/// Frozen Array of interned frozen Strings in capture order (nil for
|
|
47
|
+
/// unnamed groups). Built before publication so all Ruby state remains
|
|
48
|
+
/// immutable when the regex or a match is shared between Ractors.
|
|
49
|
+
ruby_names: Opaque<RArray>,
|
|
55
50
|
}
|
|
56
51
|
|
|
57
52
|
impl RegexInner {
|
|
58
|
-
fn from_pattern(pattern: &str, unicode: bool) -> Result<Self, Error> {
|
|
53
|
+
fn from_pattern(ruby: &Ruby, pattern: &str, unicode: bool) -> Result<Self, Error> {
|
|
59
54
|
let regex = RegexBuilder::new(pattern)
|
|
60
55
|
.unicode(unicode)
|
|
61
56
|
.build()
|
|
62
57
|
.map_err(|e| arg_error(e.to_string()))?;
|
|
63
58
|
|
|
64
59
|
let mut names = HashMap::new();
|
|
65
|
-
let
|
|
60
|
+
let ruby_names = ruby.ary_new_capa(regex.captures_len());
|
|
66
61
|
for (idx, name) in regex.capture_names().enumerate() {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
62
|
+
match name {
|
|
63
|
+
Some(name) => {
|
|
64
|
+
names.insert(name.to_owned(), idx);
|
|
65
|
+
// Root each interned string immediately: the interning
|
|
66
|
+
// table holds it weakly, and the next push may run GC.
|
|
67
|
+
ruby_names.push(ruby.str_new(name).to_interned_str())?;
|
|
68
|
+
}
|
|
69
|
+
None => ruby_names.push(())?,
|
|
70
|
+
}
|
|
71
71
|
}
|
|
72
|
+
ruby_names.freeze();
|
|
72
73
|
|
|
73
74
|
Ok(Self {
|
|
74
75
|
regex,
|
|
75
76
|
names,
|
|
76
|
-
|
|
77
|
-
ruby_names: OnceLock::new(),
|
|
77
|
+
ruby_names: ruby_names.into(),
|
|
78
78
|
})
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
fn mark(&self, marker: &gc::Marker) {
|
|
82
|
-
|
|
83
|
-
marker.mark(*names);
|
|
84
|
-
}
|
|
82
|
+
marker.mark(self.ruby_names);
|
|
85
83
|
}
|
|
86
84
|
|
|
87
|
-
/// The interned names Array (see the field), built on first use.
|
|
88
85
|
fn ruby_names(&self, ruby: &Ruby) -> RArray {
|
|
89
|
-
ruby.get_inner(
|
|
90
|
-
let names = ruby.ary_new_capa(self.name_index.len());
|
|
91
|
-
for name in &self.name_index {
|
|
92
|
-
let pushed = match name {
|
|
93
|
-
Some(name) => names.push(ruby.str_new(name).to_interned_str()),
|
|
94
|
-
None => names.push(()),
|
|
95
|
-
};
|
|
96
|
-
pushed.expect("pushing onto a fresh, unfrozen Array");
|
|
97
|
-
}
|
|
98
|
-
Opaque::from(names)
|
|
99
|
-
}))
|
|
86
|
+
ruby.get_inner(self.ruby_names)
|
|
100
87
|
}
|
|
101
88
|
|
|
102
89
|
/// Capture-group names in declaration order, unnamed groups skipped.
|
|
@@ -117,7 +104,13 @@ impl RegexInner {
|
|
|
117
104
|
}
|
|
118
105
|
|
|
119
106
|
#[derive(TypedData)]
|
|
120
|
-
#[magnus(
|
|
107
|
+
#[magnus(
|
|
108
|
+
class = "Fast::Regexp::Native",
|
|
109
|
+
free_immediately,
|
|
110
|
+
size,
|
|
111
|
+
mark,
|
|
112
|
+
frozen_shareable
|
|
113
|
+
)]
|
|
121
114
|
pub struct FastRegexp(Arc<RegexInner>);
|
|
122
115
|
|
|
123
116
|
impl DataTypeFunctions for FastRegexp {
|
|
@@ -127,7 +120,7 @@ impl DataTypeFunctions for FastRegexp {
|
|
|
127
120
|
}
|
|
128
121
|
|
|
129
122
|
impl FastRegexp {
|
|
130
|
-
pub fn new(args: &[Value]) -> Result<Self
|
|
123
|
+
pub fn new(ruby: &Ruby, args: &[Value]) -> Result<Obj<Self>, Error> {
|
|
131
124
|
let args = scan_args::<(String,), (), (), (), RHash, ()>(args)?;
|
|
132
125
|
let kwargs = get_kwargs::<_, (), (Option<bool>,), ()>(args.keywords, &[], &["unicode"])?;
|
|
133
126
|
|
|
@@ -135,7 +128,12 @@ impl FastRegexp {
|
|
|
135
128
|
let (unicode,) = kwargs.optional;
|
|
136
129
|
let unicode = unicode.unwrap_or(true);
|
|
137
130
|
|
|
138
|
-
|
|
131
|
+
let inner = RegexInner::from_pattern(ruby, &pattern, unicode)?;
|
|
132
|
+
let names = inner.ruby_names(ruby);
|
|
133
|
+
let wrapped = ruby.obj_wrap(Self(Arc::new(inner)));
|
|
134
|
+
// Keep the Array rooted on this stack until the wrapper can mark it.
|
|
135
|
+
std::hint::black_box(names);
|
|
136
|
+
Ok(wrapped)
|
|
139
137
|
}
|
|
140
138
|
|
|
141
139
|
fn match_data(&self, haystack: RString, captures: Vec<CaptureOffset>) -> FastMatchData {
|
|
@@ -326,7 +324,8 @@ impl FastRegexp {
|
|
|
326
324
|
class = "Fast::Regexp::Native::MatchData",
|
|
327
325
|
free_immediately,
|
|
328
326
|
size,
|
|
329
|
-
mark
|
|
327
|
+
mark,
|
|
328
|
+
frozen_shareable
|
|
330
329
|
)]
|
|
331
330
|
pub struct FastMatchData {
|
|
332
331
|
haystack: Opaque<RString>,
|
|
@@ -518,7 +517,7 @@ impl FastMatchData {
|
|
|
518
517
|
}
|
|
519
518
|
}
|
|
520
519
|
|
|
521
|
-
#[magnus::wrap(class = "Fast::Regexp::Set", free_immediately, size)]
|
|
520
|
+
#[magnus::wrap(class = "Fast::Regexp::Set", free_immediately, size, frozen_shareable)]
|
|
522
521
|
pub struct FastRegexpSet(RegexSet);
|
|
523
522
|
|
|
524
523
|
impl FastRegexpSet {
|
|
@@ -553,6 +552,8 @@ impl FastRegexpSet {
|
|
|
553
552
|
|
|
554
553
|
#[magnus::init]
|
|
555
554
|
pub fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
555
|
+
// All shared Rust state is Sync; retained Ruby objects are immutable.
|
|
556
|
+
unsafe { rb_sys::rb_ext_ractor_safe(true) };
|
|
556
557
|
let object_class = ruby.class_object();
|
|
557
558
|
// Fast::Regexp and Fast::Regexp::MatchData must already be defined by
|
|
558
559
|
// lib/fast_regexp.rb before this extension is required — the Ruby façade
|
|
@@ -604,5 +605,11 @@ pub fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
604
605
|
regexp_set_class.define_method("match?", method!(FastRegexpSet::is_match, 1))?;
|
|
605
606
|
regexp_set_class.define_method("patterns", method!(FastRegexpSet::patterns, 0))?;
|
|
606
607
|
|
|
608
|
+
// Magnus caches these classes and disables their allocators on first use.
|
|
609
|
+
// Complete that setup here, before workers can construct native objects.
|
|
610
|
+
FastRegexp::class(ruby);
|
|
611
|
+
FastMatchData::class(ruby);
|
|
612
|
+
FastRegexpSet::class(ruby);
|
|
613
|
+
|
|
607
614
|
Ok(())
|
|
608
615
|
}
|
data/lib/fast_regexp/version.rb
CHANGED
data/lib/fast_regexp.rb
CHANGED
|
@@ -157,6 +157,10 @@ module Fast
|
|
|
157
157
|
raise ArgumentError, "backend must be one of #{BACKENDS.inspect}" unless BACKENDS.include?(backend)
|
|
158
158
|
@pattern = pattern
|
|
159
159
|
@backend = compile_backend(pattern, original, backend, opts)
|
|
160
|
+
if stdlib?
|
|
161
|
+
# Prepare $N-to-name translation before the pattern can be frozen.
|
|
162
|
+
@stdlib_name_by_index = @backend.named_captures.flat_map { |n, idxs| idxs.map { |i| [i, n] } }.to_h.freeze
|
|
163
|
+
end
|
|
160
164
|
end
|
|
161
165
|
|
|
162
166
|
def fast? = @backend.is_a?(Native)
|
|
@@ -193,7 +197,9 @@ module Fast
|
|
|
193
197
|
end
|
|
194
198
|
|
|
195
199
|
def scan(haystack)
|
|
196
|
-
|
|
200
|
+
haystack = coerce_string(haystack)
|
|
201
|
+
return @backend.scan(haystack) if fast?
|
|
202
|
+
haystack.scan(@backend)
|
|
197
203
|
end
|
|
198
204
|
|
|
199
205
|
def scan_matches(haystack)
|
|
@@ -300,13 +306,6 @@ module Fast
|
|
|
300
306
|
raise(fallback_from ? ArgumentError.new(fallback_from.message) : e)
|
|
301
307
|
end
|
|
302
308
|
|
|
303
|
-
# Maps positional capture indices to names for the stdlib backend so we
|
|
304
|
-
# can translate `$N` to `\k<name>` (Ruby's gsub ignores `\N` for named
|
|
305
|
-
# groups). Only invoked from the stdlib path.
|
|
306
|
-
def stdlib_name_by_index
|
|
307
|
-
@stdlib_name_by_index ||= @backend.named_captures.flat_map { |n, idxs| idxs.map { |i| [i, n] } }.to_h
|
|
308
|
-
end
|
|
309
|
-
|
|
310
309
|
# Translate rust/regex replacement syntax ($N, ${name}, $$) into Ruby
|
|
311
310
|
# syntax (\N, \k<name>, $) for the stdlib fallback path.
|
|
312
311
|
def stdlib_replacement(template, literal)
|
|
@@ -317,7 +316,7 @@ module Fast
|
|
|
317
316
|
case token
|
|
318
317
|
when "$" then "$"
|
|
319
318
|
when /\A\d+\z/
|
|
320
|
-
name = stdlib_name_by_index[token.to_i]
|
|
319
|
+
name = @stdlib_name_by_index[token.to_i]
|
|
321
320
|
name ? "\\k<#{name}>" : "\\#{token}"
|
|
322
321
|
else "\\k<#{token[1..-2]}>"
|
|
323
322
|
end
|