mini_racer_rs 1.0.2 → 1.0.4

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.
@@ -1,3 +1,7 @@
1
+ use std::cmp::Ordering;
2
+ use std::ops::Range;
3
+
4
+ use magnus::typed_data::Obj;
1
5
  use magnus::value::ReprValue;
2
6
  use magnus::{
3
7
  Class, Error, Fixnum, IntoValue, Module, Object, RArray, RClass, RHash, RModule, RObject,
@@ -7,7 +11,7 @@ use mini_racer_wire::{
7
11
  Begun, Container, DecodeError, Limits, Scalar, Text, Visitor, decode_value, marker, text,
8
12
  };
9
13
 
10
- use crate::gc_root::StaticRoot;
14
+ use crate::gc_root::{Spill, StaticRoot};
11
15
  use crate::{errors, rb_raw};
12
16
 
13
17
  #[expect(
@@ -69,23 +73,194 @@ struct ErrorParts {
69
73
 
70
74
  const BATCH: usize = 64;
71
75
 
72
- #[derive(Clone, Copy)]
73
- enum Target {
74
- Array(RArray),
75
- Hash(RHash),
76
- }
77
-
78
- // Decoded elements and object pairs wait here so a batch of them costs one
79
- // rb_protect rather than one per value. Every container's begin and end
80
- // empties it, so only the innermost open container ever has values waiting,
81
- // and whatever a child does (a Map hashing an ancestor as its key) sees each
82
- // container with every value decoded so far, as mini_racer builds them.
76
+ // Decoded elements wait here so a batch of them costs one rb_protect rather
77
+ // than one per value. Every container's begin and end empties it, so only the
78
+ // innermost open array ever has elements waiting, and whatever a child does
79
+ // (a Map hashing an ancestor as its key) sees each array with every element
80
+ // decoded so far, as mini_racer builds them.
83
81
  struct Pending {
84
- target: Option<Target>,
82
+ target: Option<RArray>,
85
83
  values: [Value; BATCH],
86
84
  filled: usize,
87
85
  }
88
86
 
87
+ const PAIRS: usize = 128;
88
+
89
+ const SCRATCH: usize = 64 << 10;
90
+
91
+ // Every open hash's pairs wait here, innermost last, until the hash ends and
92
+ // takes them in one rb_hash_bulk_insert while it is still empty, so Ruby sizes
93
+ // its table once, as json 3.1's parser builds objects. A hash's run starts
94
+ // with the hash and the start of the run around it. Past PAIRS values the
95
+ // runs move to a Spill, whose mark pins them.
96
+ struct Pairs {
97
+ values: [Value; PAIRS],
98
+ spill: Option<Obj<Spill>>,
99
+ len: usize,
100
+ innermost: Option<usize>,
101
+ // Every pair below this is in its hash already.
102
+ inserted: usize,
103
+ }
104
+
105
+ impl Pairs {
106
+ // Out of line, as KeyCache::start is, so the values made here never take
107
+ // room in the decoder's recursive frames, which a deep result repeats.
108
+ #[inline(never)]
109
+ fn start<'p>(slot: &'p mut Option<Self>, ruby: &Ruby) -> &'p mut Self {
110
+ slot.insert(Self {
111
+ values: [ruby.qnil().as_value(); PAIRS],
112
+ spill: None,
113
+ len: 0,
114
+ innermost: None,
115
+ inserted: 0,
116
+ })
117
+ }
118
+
119
+ fn push(&mut self, ruby: &Ruby, value: Value) {
120
+ if self.spill.is_none() {
121
+ if let Some(slot) = self.values.get_mut(self.len) {
122
+ *slot = value;
123
+ self.len = self.len.saturating_add(1);
124
+ return;
125
+ }
126
+ self.spill = Some(Spill::new(ruby, &self.values));
127
+ }
128
+ if let Some(spill) = &self.spill {
129
+ spill.push(value);
130
+ }
131
+ self.len = self.len.saturating_add(1);
132
+ }
133
+
134
+ fn get(&self, ruby: &Ruby, at: usize) -> Option<Value> {
135
+ self.spill
136
+ .as_ref()
137
+ .map_or_else(|| self.values.get(at).copied(), |spill| spill.get(ruby, at))
138
+ }
139
+
140
+ fn open(&mut self, ruby: &Ruby, hash: RHash) {
141
+ let outer = self
142
+ .innermost
143
+ .and_then(|at| i64::try_from(at).ok())
144
+ .unwrap_or(-1);
145
+ self.innermost = Some(self.len);
146
+ self.push(ruby, hash.as_value());
147
+ self.push(ruby, ruby.integer_from_i64(outer).as_value());
148
+ }
149
+
150
+ fn outer(&self, ruby: &Ruby, at: usize) -> Option<usize> {
151
+ self.get(ruby, at.saturating_add(1))
152
+ .and_then(Fixnum::from_value)
153
+ .and_then(|outer| usize::try_from(outer.to_i64()).ok())
154
+ }
155
+
156
+ // An empty table takes the pairs at once; one that holds pairs already
157
+ // takes them one by one, since rb_hash_bulk_insert would rehash it whole.
158
+ fn put(&self, ruby: &Ruby, hash: RHash, range: Range<usize>) -> Result<(), Error> {
159
+ if range.is_empty() {
160
+ return Ok(());
161
+ }
162
+ let put = |pairs: &[Value]| {
163
+ if hash.is_empty() {
164
+ hash.bulk_insert(pairs)
165
+ } else {
166
+ rb_raw::hash_aset_pairs(ruby, hash, pairs.as_chunks::<2>().0)
167
+ }
168
+ };
169
+ match &self.spill {
170
+ Some(spill) => spill.with_values(ruby, range, put),
171
+ None => put(self.values.get(range).unwrap_or_default()),
172
+ }
173
+ }
174
+
175
+ fn close(&mut self, ruby: &Ruby, hash: RHash) -> Result<(), Error> {
176
+ let Some(at) = self.innermost else {
177
+ return Err(errors::internal(ruby, "decode pairs without a hash"));
178
+ };
179
+ self.put(
180
+ ruby,
181
+ hash,
182
+ at.saturating_add(2).max(self.inserted)..self.len,
183
+ )?;
184
+ self.innermost = self.outer(ruby, at);
185
+ self.len = at;
186
+ self.inserted = self.inserted.min(at);
187
+ if let Some(spill) = &self.spill {
188
+ spill.truncate(at);
189
+ }
190
+ Ok(())
191
+ }
192
+
193
+ fn put_all(&mut self, ruby: &Ruby) -> Result<(), Error> {
194
+ let mut end = self.len;
195
+ let mut run = self.innermost;
196
+ while let Some(at) = run
197
+ && end > self.inserted
198
+ {
199
+ let hash = self
200
+ .get(ruby, at)
201
+ .and_then(RHash::from_value)
202
+ .ok_or_else(|| errors::internal(ruby, "decode pairs without a hash"))?;
203
+ self.put(ruby, hash, at.saturating_add(2).max(self.inserted)..end)?;
204
+ end = at;
205
+ run = self.outer(ruby, at);
206
+ }
207
+ self.inserted = self.len;
208
+ Ok(())
209
+ }
210
+ }
211
+
212
+ // json 3.1's parser keeps the names of objects inside arrays, which repeat,
213
+ // for the rest of a parse: at most this many, each at most CACHED_KEY_BYTES
214
+ // long and starting with a letter.
215
+ const CACHED_KEYS: usize = 63;
216
+ const CACHED_KEY_BYTES: usize = 55;
217
+
218
+ // Interned keys sorted by length, then bytes, so a repeated key skips Ruby's
219
+ // fstring table.
220
+ struct KeyCache {
221
+ keys: [Value; CACHED_KEYS],
222
+ len: usize,
223
+ }
224
+
225
+ impl KeyCache {
226
+ #[inline(never)]
227
+ fn start<'c>(slot: &'c mut Option<Self>, ruby: &Ruby) -> &'c mut Self {
228
+ slot.insert(Self {
229
+ keys: [ruby.qnil().as_value(); CACHED_KEYS],
230
+ len: 0,
231
+ })
232
+ }
233
+
234
+ fn fetch(&mut self, ruby: &Ruby, bytes: &[u8]) -> Result<Value, Error> {
235
+ let held = self.keys.get(..self.len).unwrap_or_default();
236
+ let found = held.binary_search_by(|&key| {
237
+ RString::from_value(key).map_or(Ordering::Less, |key| {
238
+ rb_raw::with_string_bytes(ruby, key, |key| {
239
+ key.len().cmp(&bytes.len()).then_with(|| key.cmp(bytes))
240
+ })
241
+ })
242
+ });
243
+ let at = match found {
244
+ Ok(at) => {
245
+ return held
246
+ .get(at)
247
+ .copied()
248
+ .ok_or_else(|| errors::internal(ruby, "key cache"));
249
+ }
250
+ Err(at) => at,
251
+ };
252
+ let key = rb_raw::interned_utf8(ruby, bytes)?.as_value();
253
+ if self.len < CACHED_KEYS {
254
+ self.keys.copy_within(at..self.len, at.saturating_add(1));
255
+ if let Some(slot) = self.keys.get_mut(at) {
256
+ *slot = key;
257
+ }
258
+ self.len = self.len.saturating_add(1);
259
+ }
260
+ Ok(key)
261
+ }
262
+ }
263
+
89
264
  // The visitor and its frames live on the native stack for the whole decode,
90
265
  // where Ruby's GC finds them (I3); `refs` holds every value V8 gave an id,
91
266
  // made at the first, so a result without one allocates none.
@@ -93,10 +268,15 @@ struct RubyVisitor<'r> {
93
268
  ruby: &'r Ruby,
94
269
  refs: Option<RArray>,
95
270
  pending: Pending,
271
+ pairs: Option<Pairs>,
272
+ keys: Option<KeyCache>,
273
+ // Arrays and sets open around the current value.
274
+ arrays: u32,
275
+ scratch: Vec<u8>,
96
276
  }
97
277
 
98
278
  impl RubyVisitor<'_> {
99
- fn add(&mut self, target: Target, values: &[Value]) -> Result<(), Error> {
279
+ fn add(&mut self, target: RArray, values: &[Value]) -> Result<(), Error> {
100
280
  self.add_with(target, values.len(), |slots| slots.copy_from_slice(values))
101
281
  }
102
282
 
@@ -104,7 +284,7 @@ impl RubyVisitor<'_> {
104
284
  // sees each one as soon as it is made.
105
285
  fn add_with(
106
286
  &mut self,
107
- target: Target,
287
+ target: RArray,
108
288
  count: usize,
109
289
  fill: impl FnOnce(&mut [Value]),
110
290
  ) -> Result<(), Error> {
@@ -136,7 +316,7 @@ impl RubyVisitor<'_> {
136
316
  ) -> Result<(), Error> {
137
317
  let ruby = self.ruby;
138
318
  for chunk in values.chunks(BATCH) {
139
- self.add_with(Target::Array(array), chunk.len(), |slots| {
319
+ self.add_with(array, chunk.len(), |slots| {
140
320
  for (slot, &n) in slots.iter_mut().zip(chunk) {
141
321
  *slot = convert(ruby, n);
142
322
  }
@@ -164,32 +344,103 @@ impl RubyVisitor<'_> {
164
344
  let values = self.pending.values.get(..filled).unwrap_or_default();
165
345
  match self.pending.target {
166
346
  _ if filled == 0 => Ok(()),
167
- Some(Target::Array(array)) => array.cat(values),
168
- Some(Target::Hash(hash)) => {
169
- rb_raw::hash_aset_pairs(self.ruby, hash, values.as_chunks::<2>().0)
170
- }
347
+ Some(array) => array.cat(values),
171
348
  None => Err(errors::internal(self.ruby, "decode batch without a target")),
172
349
  }
173
350
  }
174
351
 
352
+ fn pairs(&mut self) -> Result<&mut Pairs, Error> {
353
+ let ruby = self.ruby;
354
+ self.pairs
355
+ .as_mut()
356
+ .ok_or_else(|| errors::internal(ruby, "decode pairs without a hash"))
357
+ }
358
+
359
+ fn open_hash(&mut self) -> RHash {
360
+ let ruby = self.ruby;
361
+ let hash = ruby.hash_new();
362
+ match &mut self.pairs {
363
+ Some(pairs) => pairs.open(ruby, hash),
364
+ None => Pairs::start(&mut self.pairs, ruby).open(ruby, hash),
365
+ }
366
+ hash
367
+ }
368
+
369
+ // A key whose hashing Ruby code could run for, or that holds other
370
+ // values, such as an Array that refers back to an open hash, first sees
371
+ // every open hash with all its pairs so far, as mini_racer stores each
372
+ // pair as it decodes it.
373
+ fn put_pair(&mut self, hash: RHash, key: Value, value: Value) -> Result<(), Error> {
374
+ let ruby = self.ruby;
375
+ let pairs = self.pairs()?;
376
+ if rb_raw::plain_key(ruby, key) {
377
+ pairs.push(ruby, key);
378
+ pairs.push(ruby, value);
379
+ return Ok(());
380
+ }
381
+ pairs.put_all(ruby)?;
382
+ rb_raw::hash_aset_pairs(ruby, hash, &[[key, value]])
383
+ }
384
+
175
385
  fn bad_state<T>(&self) -> Result<T, Error> {
176
386
  Err(errors::runtime(self.ruby, "bad state"))
177
387
  }
178
388
 
179
- fn string(&self, text: Text<'_>) -> Result<Value, Error> {
389
+ // Text is transcoded into one buffer, then copied into its String in one
390
+ // call: making a String to write into costs more Ruby calls than the copy
391
+ // saves. Up to SCRATCH bytes it is the decode's own, with room for the
392
+ // longest UTF-8 the text could be and zeroed only as it grows; longer
393
+ // text is measured first and gets a buffer its size, freed once copied,
394
+ // since the longest it could be is up to three times what it takes.
395
+ fn transcoded(
396
+ &mut self,
397
+ most: usize,
398
+ len: impl FnOnce() -> Option<usize>,
399
+ write: impl FnOnce(&mut [u8]) -> Option<usize>,
400
+ ) -> Option<Value> {
401
+ let ruby = self.ruby;
402
+ let mut measured;
403
+ let out = if most <= SCRATCH {
404
+ if self.scratch.len() < most {
405
+ self.scratch.resize(most, 0);
406
+ }
407
+ self.scratch.get_mut(..most).unwrap_or_default()
408
+ } else {
409
+ measured = vec![0; len()?];
410
+ measured.as_mut_slice()
411
+ };
412
+ let written = write(out)?;
413
+ Some(
414
+ ruby.enc_str_new(out.get(..written).unwrap_or_default(), ruby.utf8_encoding())
415
+ .as_value(),
416
+ )
417
+ }
418
+
419
+ fn string(&mut self, text: Text<'_>) -> Result<Value, Error> {
180
420
  let ruby = self.ruby;
181
421
  Ok(match text {
182
422
  Text::Utf8(bytes) => ruby.enc_str_new(bytes, ruby.utf8_encoding()).as_value(),
183
423
  Text::Latin1(bytes) if bytes.is_ascii() => {
184
424
  ruby.enc_str_new(bytes, ruby.utf8_encoding()).as_value()
185
425
  }
186
- Text::Latin1(bytes) => ruby.str_new(&text::latin1_to_utf8(bytes)).as_value(),
426
+ Text::Latin1(bytes) => self
427
+ .transcoded(
428
+ bytes.len().saturating_mul(2),
429
+ || Some(text::latin1_utf8_len(bytes)),
430
+ |out| Some(text::latin1_to_utf8_into(bytes, out)),
431
+ )
432
+ .ok_or_else(|| errors::internal(ruby, "Latin-1 text not transcoded"))?,
187
433
  Text::Utf16Le(bytes) if marker::is_javascript_function(bytes) => {
188
434
  JAVASCRIPT_FUNCTION.get(ruby).new_instance(())?
189
435
  }
190
436
  Text::Utf16Le(bytes) => {
191
- if let Some(utf8) = text::utf16le_to_utf8(bytes) {
192
- ruby.str_new(&utf8).as_value()
437
+ let most = bytes.len().saturating_mul(3).div_ceil(2);
438
+ if let Some(utf8) = self.transcoded(
439
+ most,
440
+ || text::utf16le_utf8_len(bytes),
441
+ |out| text::utf16le_to_utf8_into(bytes, out),
442
+ ) {
443
+ utf8
193
444
  } else {
194
445
  let utf16 = ruby
195
446
  .find_encoding("UTF-16LE")
@@ -200,6 +451,39 @@ impl RubyVisitor<'_> {
200
451
  })
201
452
  }
202
453
 
454
+ // The key `Hash#[]=` would store, made interned in the first place. One
455
+ // with a lone surrogate stays UTF-16LE, which the hash interns itself,
456
+ // and the function marker stays a JavaScriptFunction.
457
+ fn interned(&mut self, text: Text<'_>) -> Result<Value, Error> {
458
+ let ruby = self.ruby;
459
+ match text {
460
+ Text::Utf8(bytes) => self.cached(bytes),
461
+ Text::Latin1(bytes) if bytes.is_ascii() => self.cached(bytes),
462
+ Text::Latin1(bytes) => {
463
+ Ok(rb_raw::interned_utf8(ruby, text::latin1_to_utf8(bytes).as_bytes())?.as_value())
464
+ }
465
+ Text::Utf16Le(bytes) if marker::is_javascript_function(bytes) => self.string(text),
466
+ Text::Utf16Le(bytes) => match text::utf16le_to_utf8(bytes) {
467
+ Some(utf8) => Ok(rb_raw::interned_utf8(ruby, utf8.as_bytes())?.as_value()),
468
+ None => self.string(text),
469
+ },
470
+ }
471
+ }
472
+
473
+ fn cached(&mut self, bytes: &[u8]) -> Result<Value, Error> {
474
+ let ruby = self.ruby;
475
+ if self.arrays == 0
476
+ || bytes.len() > CACHED_KEY_BYTES
477
+ || !bytes.first().is_some_and(u8::is_ascii_alphabetic)
478
+ {
479
+ return Ok(rb_raw::interned_utf8(ruby, bytes)?.as_value());
480
+ }
481
+ match &mut self.keys {
482
+ Some(keys) => keys.fetch(ruby, bytes),
483
+ None => KeyCache::start(&mut self.keys, ruby).fetch(ruby, bytes),
484
+ }
485
+ }
486
+
203
487
  // mini_racer splits a millisecond time into seconds and microseconds,
204
488
  // truncating each, and lets Time normalize a negative remainder.
205
489
  #[expect(
@@ -286,8 +570,8 @@ impl Visitor for RubyVisitor<'_> {
286
570
  array: ruby.ary_new(),
287
571
  named: false,
288
572
  },
289
- Container::Object => Frame::Object(ruby.hash_new()),
290
- Container::Map => Frame::Map(ruby.hash_new()),
573
+ Container::Object => Frame::Object(self.open_hash()),
574
+ Container::Map => Frame::Map(self.open_hash()),
291
575
  // Allocated now so a cause can refer back to the error itself.
292
576
  Container::Error => Frame::Error(ErrorParts {
293
577
  error: errors::script_error(ruby).obj_alloc()?.as_value(),
@@ -297,7 +581,10 @@ impl Visitor for RubyVisitor<'_> {
297
581
  }),
298
582
  };
299
583
  let value = match &frame {
300
- Frame::Array(array) | Frame::Sparse { array, .. } => array.as_value(),
584
+ Frame::Array(array) | Frame::Sparse { array, .. } => {
585
+ self.arrays = self.arrays.saturating_add(1);
586
+ array.as_value()
587
+ }
301
588
  Frame::Object(hash) | Frame::Map(hash) => hash.as_value(),
302
589
  Frame::Error(parts) => parts.error,
303
590
  };
@@ -309,9 +596,7 @@ impl Visitor for RubyVisitor<'_> {
309
596
 
310
597
  fn push(&mut self, frame: &mut Frame, value: Value) -> Result<(), Error> {
311
598
  match frame {
312
- Frame::Array(array) | Frame::Sparse { array, .. } => {
313
- self.add(Target::Array(*array), &[value])
314
- }
599
+ Frame::Array(array) | Frame::Sparse { array, .. } => self.add(*array, &[value]),
315
600
  Frame::Object(_) | Frame::Map(_) | Frame::Error(_) => self.bad_state(),
316
601
  }
317
602
  }
@@ -321,7 +606,7 @@ impl Visitor for RubyVisitor<'_> {
321
606
  Frame::Array(_) => Ok(()),
322
607
  Frame::Sparse { array, named } => {
323
608
  if !*named && is_index(key) {
324
- self.add(Target::Array(*array), &[value])
609
+ self.add(*array, &[value])
325
610
  } else {
326
611
  *named = true;
327
612
  Ok(())
@@ -332,9 +617,9 @@ impl Visitor for RubyVisitor<'_> {
332
617
  Some(key) => key.as_value(),
333
618
  None => key.funcall::<_, _, Value>("to_s", ())?,
334
619
  };
335
- self.add(Target::Hash(*hash), &[key, value])
620
+ self.put_pair(*hash, key, value)
336
621
  }
337
- Frame::Map(hash) => hash.aset(key, value),
622
+ Frame::Map(hash) => self.put_pair(*hash, key, value),
338
623
  Frame::Error(parts) => {
339
624
  let field = RString::from_value(key)
340
625
  .map(|key| rb_raw::string_bytes(self.ruby, key))
@@ -353,12 +638,26 @@ impl Visitor for RubyVisitor<'_> {
353
638
  fn end(&mut self, frame: Frame) -> Result<Value, Error> {
354
639
  self.flush()?;
355
640
  match frame {
356
- Frame::Array(array) | Frame::Sparse { array, .. } => Ok(array.as_value()),
357
- Frame::Object(hash) | Frame::Map(hash) => Ok(hash.as_value()),
641
+ Frame::Array(array) | Frame::Sparse { array, .. } => {
642
+ self.arrays = self.arrays.saturating_sub(1);
643
+ Ok(array.as_value())
644
+ }
645
+ Frame::Object(hash) | Frame::Map(hash) => {
646
+ let ruby = self.ruby;
647
+ self.pairs()?.close(ruby, hash)?;
648
+ Ok(hash.as_value())
649
+ }
358
650
  Frame::Error(parts) => self.script_error(parts),
359
651
  }
360
652
  }
361
653
 
654
+ fn key(&mut self, frame: &Frame, text: Text<'_>) -> Result<Value, Error> {
655
+ match frame {
656
+ Frame::Object(_) | Frame::Map(_) => self.interned(text),
657
+ Frame::Array(_) | Frame::Sparse { .. } | Frame::Error(_) => self.string(text),
658
+ }
659
+ }
660
+
362
661
  fn register(&mut self, id: u32, value: Value) -> Result<(), Error> {
363
662
  let ruby = self.ruby;
364
663
  self.refs
@@ -429,6 +728,10 @@ pub fn value(ruby: &Ruby, bytes: &[u8]) -> Result<Value, Error> {
429
728
  values: [ruby.qnil().as_value(); BATCH],
430
729
  filled: 0,
431
730
  },
731
+ pairs: None,
732
+ keys: None,
733
+ arrays: 0,
734
+ scratch: Vec::new(),
432
735
  };
433
736
  decode_value(bytes, RESULT_LIMITS, &mut visitor).map_err(|e| match e {
434
737
  DecodeError::Visitor(error) => error,
@@ -1,4 +1,5 @@
1
1
  use std::cell::{Cell, RefCell};
2
+ use std::ops::Range;
2
3
  use std::sync::OnceLock;
3
4
 
4
5
  use magnus::gc::Marker;
@@ -110,8 +111,72 @@ impl Pins {
110
111
  }
111
112
  }
112
113
 
114
+ /// Ruby values a decode keeps in order past what fits on the native stack.
115
+ /// The holder's mark pins every value. Only touched under the GVL; a borrow
116
+ /// that spans a Ruby call is a shared one, which the mark shares, and no
117
+ /// mutable borrow does.
118
+ #[derive(Default, TypedData)]
119
+ #[magnus(class = "MiniRacerRs::Spill", free_immediately, mark)]
120
+ pub struct Spill(RefCell<Vec<Opaque<Value>>>);
121
+
122
+ impl DataTypeFunctions for Spill {
123
+ // As for Pins, a mutable borrow at mark time would leave values unmarked.
124
+ fn mark(&self, marker: &Marker) {
125
+ let Ok(values) = self.0.try_borrow() else {
126
+ std::process::abort();
127
+ };
128
+ for value in values.iter() {
129
+ marker.mark(*value);
130
+ }
131
+ }
132
+ }
133
+
134
+ impl Spill {
135
+ /// Holds `values` first. Keep using the returned `Obj` while the values
136
+ /// are needed; a reference into its data alone does not keep the holder
137
+ /// alive.
138
+ pub fn new(ruby: &Ruby, values: &[Value]) -> Obj<Self> {
139
+ let held = values.iter().copied().map(Opaque::from).collect();
140
+ ruby.obj_wrap(Self(RefCell::new(held)))
141
+ }
142
+
143
+ pub fn push(&self, value: Value) {
144
+ self.0.borrow_mut().push(Opaque::from(value));
145
+ }
146
+
147
+ pub fn truncate(&self, len: usize) {
148
+ self.0.borrow_mut().truncate(len);
149
+ }
150
+
151
+ pub fn get(&self, ruby: &Ruby, index: usize) -> Option<Value> {
152
+ self.0
153
+ .borrow()
154
+ .get(index)
155
+ .map(|value| ruby.get_inner(*value))
156
+ }
157
+
158
+ /// Runs `read` over the values in `range`; `read` may call Ruby.
159
+ pub fn with_values<R>(
160
+ &self,
161
+ _ruby: &Ruby,
162
+ range: Range<usize>,
163
+ read: impl FnOnce(&[Value]) -> R,
164
+ ) -> R {
165
+ let values = self.0.borrow();
166
+ let held = values.get(range).unwrap_or_default();
167
+ // SAFETY: Opaque is a transparent wrapper, so a slice of them has the
168
+ // layout of a slice of the values they wrap, which the `&Ruby`
169
+ // argument proves this thread may use.
170
+ let slice =
171
+ unsafe { std::slice::from_raw_parts(held.as_ptr().cast::<Value>(), held.len()) };
172
+ read(slice)
173
+ }
174
+ }
175
+
113
176
  pub fn init(ruby: &Ruby, rs: RModule) -> Result<(), Error> {
114
- let class = rs.define_class("Pins", ruby.class_object())?;
115
- class.undef_default_alloc_func();
177
+ for name in ["Pins", "Spill"] {
178
+ rs.define_class(name, ruby.class_object())?
179
+ .undef_default_alloc_func();
180
+ }
116
181
  Ok(())
117
182
  }
@@ -3,6 +3,10 @@ mod decode;
3
3
  mod encode;
4
4
  mod errors;
5
5
  mod fork;
6
+ #[allow(
7
+ unsafe_code,
8
+ reason = "a decode's spilled values are lent in place as the slice Ruby reads"
9
+ )]
6
10
  mod gc_root;
7
11
  mod guard;
8
12
  mod host;
@@ -1,4 +1,4 @@
1
- use std::ffi::{c_int, c_void};
1
+ use std::ffi::{c_int, c_long, c_void};
2
2
 
3
3
  use magnus::rb_sys::{AsRawId, AsRawValue, FromRawValue, protect};
4
4
  use magnus::value::{IntoId, LazyId, ReprValue};
@@ -185,6 +185,23 @@ pub fn ruby_type(_ruby: &Ruby, value: Value) -> RubyType {
185
185
  }
186
186
  }
187
187
 
188
+ /// Whether a Hash stores `key` without running Ruby code or reaching any
189
+ /// other object: Ruby hashes and compares these itself (`any_hash`,
190
+ /// `rb_any_cmp`), a String only while its class is String itself.
191
+ pub fn plain_key(ruby: &Ruby, key: Value) -> bool {
192
+ match ruby_type(ruby, key) {
193
+ RubyType::Fixnum
194
+ | RubyType::Bignum
195
+ | RubyType::Float
196
+ | RubyType::Symbol
197
+ | RubyType::Nil
198
+ | RubyType::True
199
+ | RubyType::False => true,
200
+ RubyType::String => key.class().as_raw() == ruby.class_string().as_raw(),
201
+ _ => false,
202
+ }
203
+ }
204
+
188
205
  /// An object's address, the identity `mini_racer`'s encoder keys repeated
189
206
  /// containers by; stable only while the object is pinned.
190
207
  pub fn identity(value: Value) -> u64 {
@@ -221,8 +238,9 @@ pub fn call_with_array(ruby: &Ruby, receiver: Value, args: RArray) -> Result<Val
221
238
  }
222
239
 
223
240
  /// Stores each key and value as `Hash#[]=` would, under one `rb_protect` for
224
- /// the whole slice. `pairs` must be where the GC sees them, on the native
225
- /// stack: storing a key can start a compacting collection.
241
+ /// the whole slice. `pairs` must be where the GC sees and pins them, on the
242
+ /// native stack or in a `gc_root::Spill`: storing a key can start a
243
+ /// compacting collection.
226
244
  // rb_hash_bulk_insert would take the slice in one call, but it rehashes the
227
245
  // whole table whenever the hash already has entries.
228
246
  pub fn hash_aset_pairs(_ruby: &Ruby, hash: RHash, pairs: &[[Value; 2]]) -> Result<(), Error> {
@@ -239,6 +257,24 @@ pub fn hash_aset_pairs(_ruby: &Ruby, hash: RHash, pairs: &[[Value; 2]]) -> Resul
239
257
  Ok(())
240
258
  }
241
259
 
260
+ /// The interned UTF-8 String of `bytes`, the frozen String `Hash#[]=` would
261
+ /// store as the key in place of a new one.
262
+ pub fn interned_utf8(ruby: &Ruby, bytes: &[u8]) -> Result<RString, Error> {
263
+ let len = c_long::try_from(bytes.len()).unwrap_or(c_long::MAX);
264
+ // SAFETY: rb_utf8_encoding only reads Ruby's built-in UTF-8 encoding.
265
+ let utf8 = unsafe { rb_sys::rb_utf8_encoding() };
266
+ let raw = protect(|| {
267
+ // SAFETY: the `&Ruby` argument proves this thread holds the GVL,
268
+ // `bytes` holds `len` bytes and outlives the call, which copies them
269
+ // when it makes a String, and `protect` catches an allocation failure.
270
+ unsafe { rb_sys::rb_enc_interned_str(bytes.as_ptr().cast(), len, utf8) }
271
+ })?;
272
+ // SAFETY: `raw` is the live String rb_enc_interned_str returned.
273
+ let value = unsafe { Value::from_raw(raw) };
274
+ RString::from_value(value)
275
+ .ok_or_else(|| Error::new(ruby.exception_type_error(), "expected String"))
276
+ }
277
+
242
278
  /// How Ruby's own type errors name `value`: nil, true and false by value,
243
279
  /// anything else by its class, never a singleton class, printed with `to_s`
244
280
  /// as Ruby prints it, overridden or not.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MiniRacerRs
4
- VERSION = "1.0.2"
4
+ VERSION = "1.0.4"
5
5
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mini_racer_rs
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.2
4
+ version: 1.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Aragon
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-10-18 00:00:00.000000000 Z
10
+ date: 2026-10-19 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: rb_sys