mini_racer_rs 1.0.2 → 1.0.3
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 +7 -0
- data/Cargo.lock +4 -4
- data/Cargo.toml +1 -1
- data/crates/mini_racer_wire/src/decode.rs +24 -2
- data/crates/mini_racer_wire/src/text.rs +160 -49
- data/ext/mini_racer_rs/src/decode.rs +338 -35
- data/ext/mini_racer_rs/src/gc_root.rs +67 -2
- data/ext/mini_racer_rs/src/lib.rs +4 -0
- data/ext/mini_racer_rs/src/rb_raw.rs +39 -3
- data/lib/mini_racer_rs/version.rb +1 -1
- 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: dfd3987a3aebfd95013e67e552b2041b991fd7b0da4933f98ae92335f7caa3a4
|
|
4
|
+
data.tar.gz: 51ccdf38b39ab3f33f771f3cf7fdbd7422bda16037e8bc026cfd64dc6fdfbdae
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ca5321d48ea8913f86d8966b6f475857d11b962c1f109e15f049fed590a517233f2cc3772fad99a9ff189f110995d3b2a0268f54e9f90aa48fcdd3cc1ac2b916
|
|
7
|
+
data.tar.gz: 3c2a6c804148cfd0bdf198d5a63ef4b440d63c667d7468ab646e8b85b277eb15ec0fa26f5d9877f426c01427e8f841723cf31d00bec0ee546881c895d373c61b
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.0.3](https://github.com/andymai/mini_racer_rs/compare/v1.0.2...v1.0.3) (2026-10-18)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Performance
|
|
7
|
+
|
|
8
|
+
* **decode:** intern hash keys, build each Hash at once, and transcode strings in one pass ([#87](https://github.com/andymai/mini_racer_rs/issues/87)) ([220bfe7](https://github.com/andymai/mini_racer_rs/commit/220bfe7e6a9c8fa5a38befefcb4995756c377944))
|
|
9
|
+
|
|
3
10
|
## [1.0.2](https://github.com/andymai/mini_racer_rs/compare/v1.0.1...v1.0.2) (2026-10-18)
|
|
4
11
|
|
|
5
12
|
|
data/Cargo.lock
CHANGED
|
@@ -458,7 +458,7 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
|
|
458
458
|
|
|
459
459
|
[[package]]
|
|
460
460
|
name = "mini_racer_engine"
|
|
461
|
-
version = "1.0.
|
|
461
|
+
version = "1.0.3"
|
|
462
462
|
dependencies = [
|
|
463
463
|
"deno_core_icudata",
|
|
464
464
|
"libc",
|
|
@@ -470,7 +470,7 @@ dependencies = [
|
|
|
470
470
|
|
|
471
471
|
[[package]]
|
|
472
472
|
name = "mini_racer_rs"
|
|
473
|
-
version = "1.0.
|
|
473
|
+
version = "1.0.3"
|
|
474
474
|
dependencies = [
|
|
475
475
|
"magnus",
|
|
476
476
|
"mini_racer_engine",
|
|
@@ -481,7 +481,7 @@ dependencies = [
|
|
|
481
481
|
|
|
482
482
|
[[package]]
|
|
483
483
|
name = "mini_racer_sync"
|
|
484
|
-
version = "1.0.
|
|
484
|
+
version = "1.0.3"
|
|
485
485
|
dependencies = [
|
|
486
486
|
"loom",
|
|
487
487
|
"thiserror",
|
|
@@ -489,7 +489,7 @@ dependencies = [
|
|
|
489
489
|
|
|
490
490
|
[[package]]
|
|
491
491
|
name = "mini_racer_wire"
|
|
492
|
-
version = "1.0.
|
|
492
|
+
version = "1.0.3"
|
|
493
493
|
dependencies = [
|
|
494
494
|
"proptest",
|
|
495
495
|
"thiserror",
|
data/Cargo.toml
CHANGED
|
@@ -85,6 +85,12 @@ pub trait Visitor {
|
|
|
85
85
|
|
|
86
86
|
fn end(&mut self, frame: Self::Frame) -> Result<Self::Value, Self::Error>;
|
|
87
87
|
|
|
88
|
+
/// A key that is a string, for an entry of `frame` other than an error's
|
|
89
|
+
/// fields, which would otherwise reach `scalar` as text.
|
|
90
|
+
fn key(&mut self, _frame: &Self::Frame, text: Text<'_>) -> Result<Self::Value, Self::Error> {
|
|
91
|
+
self.scalar(Scalar::Text(text))
|
|
92
|
+
}
|
|
93
|
+
|
|
88
94
|
/// Remembers `value` as V8's object `id` for later back references. Ids
|
|
89
95
|
/// arrive in increasing order, except that a container whose `begin`
|
|
90
96
|
/// returned no value is registered when it ends.
|
|
@@ -650,11 +656,27 @@ impl<'a, 'v, V: Visitor> Decoder<'a, 'v, V> {
|
|
|
650
656
|
}
|
|
651
657
|
|
|
652
658
|
fn insert(&mut self, frame: &mut V::Frame, depth: u32) -> Result<(), DecodeError<V::Error>> {
|
|
653
|
-
let
|
|
654
|
-
let
|
|
659
|
+
let depth = Self::child(depth)?;
|
|
660
|
+
let key = self.entry_key(frame, depth)?;
|
|
661
|
+
let value = self.value(depth)?;
|
|
655
662
|
Self::visit(self.visitor.insert(frame, key, value))
|
|
656
663
|
}
|
|
657
664
|
|
|
665
|
+
// A key that is not a string is read again as a value: handing `value` its
|
|
666
|
+
// tag would split `value` in two, and the half with the containers takes
|
|
667
|
+
// them all inline. Out of line, so `insert` stays inside the container's
|
|
668
|
+
// frame; a deep result repeats every frame on the way down.
|
|
669
|
+
#[inline(never)]
|
|
670
|
+
fn entry_key(&mut self, frame: &V::Frame, depth: u32) -> Decoded<V> {
|
|
671
|
+
let start = self.reader;
|
|
672
|
+
let tag = self.tag()?;
|
|
673
|
+
if let Some(text) = self.text(tag)? {
|
|
674
|
+
return Self::visit(self.visitor.key(frame, text));
|
|
675
|
+
}
|
|
676
|
+
self.reader = start;
|
|
677
|
+
self.value(depth)
|
|
678
|
+
}
|
|
679
|
+
|
|
658
680
|
// Reads keyed entries up to `end`, which may follow padding only if a key does.
|
|
659
681
|
fn entries(
|
|
660
682
|
&mut self,
|
|
@@ -1,68 +1,130 @@
|
|
|
1
|
-
// Returned HTML is mostly ASCII, which is copied in
|
|
2
|
-
// another byte are converted a byte at a time.
|
|
1
|
+
// Returned HTML is mostly ASCII, which is copied in blocks; only blocks
|
|
2
|
+
// holding another byte are converted a byte at a time.
|
|
3
3
|
const LATIN1_BLOCK: usize = 32;
|
|
4
|
-
//
|
|
5
|
-
|
|
4
|
+
// Lengths are counted a block at a time in a u8, which vectorizes; a UTF-16
|
|
5
|
+
// unit adds at most 3.
|
|
6
|
+
const COUNT_BLOCK: usize = 64;
|
|
7
|
+
const _: () = assert!(COUNT_BLOCK * 3 <= u8::MAX as usize);
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
/// The length in UTF-8 of Latin-1 `bytes`.
|
|
10
|
+
#[must_use]
|
|
11
|
+
pub fn latin1_utf8_len(bytes: &[u8]) -> usize {
|
|
12
|
+
let wide = bytes.chunks(COUNT_BLOCK).fold(0_usize, |wide, block| {
|
|
13
|
+
let count = block.iter().fold(0_u8, |count, &byte| {
|
|
14
|
+
count.wrapping_add(u8::from(!byte.is_ascii()))
|
|
15
|
+
});
|
|
16
|
+
wide.saturating_add(usize::from(count))
|
|
17
|
+
});
|
|
18
|
+
bytes.len().saturating_add(wide)
|
|
10
19
|
}
|
|
11
20
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
.map(|&byte| u8::from(!byte.is_ascii()))
|
|
24
|
-
.sum::<u8>();
|
|
25
|
-
(
|
|
26
|
-
wide.saturating_add(usize::from(count)),
|
|
27
|
-
mixed.saturating_add(usize::from(count > 0)),
|
|
28
|
-
)
|
|
29
|
-
});
|
|
30
|
-
let mut out = String::with_capacity(bytes.len().saturating_add(wide));
|
|
31
|
-
// Text where most blocks need converting anyway, like accented prose, is
|
|
32
|
-
// faster a byte at a time throughout.
|
|
33
|
-
if mixed.saturating_mul(LATIN1_BLOCK).saturating_mul(2) > bytes.len() {
|
|
34
|
-
out.extend(bytes.iter().copied().map(char::from));
|
|
35
|
-
return out;
|
|
36
|
-
}
|
|
37
|
-
let mut span = 0;
|
|
21
|
+
// Writes `c` at `at` and returns where it ends, or `None` when it does not fit.
|
|
22
|
+
fn put(out: &mut [u8], at: usize, c: char) -> Option<usize> {
|
|
23
|
+
let end = at.checked_add(c.len_utf8())?;
|
|
24
|
+
c.encode_utf8(out.get_mut(at..end)?);
|
|
25
|
+
Some(end)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/// Writes the UTF-8 of Latin-1 `bytes` at the start of `out`, up to the last
|
|
29
|
+
/// character that fits, and returns how many bytes it wrote: all
|
|
30
|
+
/// `latin1_utf8_len(bytes)` of them when `out` is that long.
|
|
31
|
+
pub fn latin1_to_utf8_into(bytes: &[u8], out: &mut [u8]) -> usize {
|
|
38
32
|
let mut at: usize = 0;
|
|
39
33
|
for block in bytes.chunks(LATIN1_BLOCK) {
|
|
40
|
-
let
|
|
41
|
-
if
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
34
|
+
let end = at.saturating_add(block.len());
|
|
35
|
+
if block.is_ascii()
|
|
36
|
+
&& let Some(slots) = out.get_mut(at..end)
|
|
37
|
+
{
|
|
38
|
+
slots.copy_from_slice(block);
|
|
39
|
+
at = end;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
for &byte in block {
|
|
43
|
+
match put(out, at, char::from(byte)) {
|
|
44
|
+
Some(next) => at = next,
|
|
45
|
+
None => return at,
|
|
47
46
|
}
|
|
48
|
-
span = next;
|
|
49
47
|
}
|
|
50
|
-
at = next;
|
|
51
48
|
}
|
|
52
|
-
|
|
53
|
-
out
|
|
49
|
+
at
|
|
54
50
|
}
|
|
55
51
|
|
|
56
52
|
#[must_use]
|
|
57
|
-
|
|
53
|
+
#[expect(clippy::expect_used, reason = "only whole characters are written")]
|
|
54
|
+
pub fn latin1_to_utf8(bytes: &[u8]) -> String {
|
|
55
|
+
let mut out = vec![0; latin1_utf8_len(bytes)];
|
|
56
|
+
let written = latin1_to_utf8_into(bytes, &mut out);
|
|
57
|
+
out.truncate(written);
|
|
58
|
+
String::from_utf8(out).expect("Latin-1 transcodes to UTF-8")
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
fn utf16le_chars(bytes: &[u8]) -> Option<impl Iterator<Item = Result<char, ()>>> {
|
|
62
|
+
let (pairs, rest) = bytes.as_chunks::<2>();
|
|
63
|
+
let units = pairs.iter().copied().map(u16::from_le_bytes);
|
|
64
|
+
rest.is_empty()
|
|
65
|
+
.then(|| char::decode_utf16(units).map(|c| c.map_err(|_| ())))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/// The length in UTF-8 of UTF-16LE `bytes`, or `None` when they hold an odd
|
|
69
|
+
/// byte or a lone surrogate.
|
|
70
|
+
#[must_use]
|
|
71
|
+
pub fn utf16le_utf8_len(bytes: &[u8]) -> Option<usize> {
|
|
58
72
|
let (pairs, rest) = bytes.as_chunks::<2>();
|
|
59
73
|
if !rest.is_empty() {
|
|
60
74
|
return None;
|
|
61
75
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
76
|
+
// A unit takes one byte, one more from U+0080 and one more from U+0800,
|
|
77
|
+
// which counts the two units of a surrogate pair as six bytes for its four.
|
|
78
|
+
let mut len: usize = 0;
|
|
79
|
+
let mut surrogates = false;
|
|
80
|
+
for block in pairs.chunks(COUNT_BLOCK) {
|
|
81
|
+
let mut count: u8 = 0;
|
|
82
|
+
for &pair in block {
|
|
83
|
+
let unit = u16::from_le_bytes(pair);
|
|
84
|
+
count = count
|
|
85
|
+
.wrapping_add(1)
|
|
86
|
+
.wrapping_add(u8::from(unit >= 0x80))
|
|
87
|
+
.wrapping_add(u8::from(unit >= 0x800));
|
|
88
|
+
surrogates |= (0xD800..0xE000).contains(&unit);
|
|
89
|
+
}
|
|
90
|
+
len = len.saturating_add(usize::from(count));
|
|
91
|
+
}
|
|
92
|
+
if !surrogates {
|
|
93
|
+
return Some(len);
|
|
94
|
+
}
|
|
95
|
+
let mut surrogate_pairs: usize = 0;
|
|
96
|
+
let mut units = pairs.iter().copied().map(u16::from_le_bytes);
|
|
97
|
+
while let Some(unit) = units.next() {
|
|
98
|
+
if (0xD800..0xDC00).contains(&unit) {
|
|
99
|
+
units.next().filter(|low| (0xDC00..0xE000).contains(low))?;
|
|
100
|
+
surrogate_pairs = surrogate_pairs.saturating_add(1);
|
|
101
|
+
} else if (0xDC00..0xE000).contains(&unit) {
|
|
102
|
+
return None;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
Some(len.saturating_sub(surrogate_pairs.saturating_mul(2)))
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/// Writes the UTF-8 of UTF-16LE `bytes` at the start of `out` and returns how
|
|
109
|
+
/// many bytes it wrote.
|
|
110
|
+
///
|
|
111
|
+
/// `None` when they hold an odd byte or a lone surrogate, or when `out` lacks
|
|
112
|
+
/// room, which three bytes per unit never does.
|
|
113
|
+
pub fn utf16le_to_utf8_into(bytes: &[u8], out: &mut [u8]) -> Option<usize> {
|
|
114
|
+
let mut at = 0;
|
|
115
|
+
for c in utf16le_chars(bytes)? {
|
|
116
|
+
at = put(out, at, c.ok()?)?;
|
|
117
|
+
}
|
|
118
|
+
Some(at)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
#[must_use]
|
|
122
|
+
#[expect(clippy::expect_used, reason = "only whole characters are written")]
|
|
123
|
+
pub fn utf16le_to_utf8(bytes: &[u8]) -> Option<String> {
|
|
124
|
+
let mut out = vec![0; utf16le_utf8_len(bytes)?];
|
|
125
|
+
let written = utf16le_to_utf8_into(bytes, &mut out)?;
|
|
126
|
+
out.truncate(written);
|
|
127
|
+
Some(String::from_utf8(out).expect("UTF-16 transcodes to UTF-8"))
|
|
66
128
|
}
|
|
67
129
|
|
|
68
130
|
#[cfg(test)]
|
|
@@ -83,8 +145,38 @@ mod tests {
|
|
|
83
145
|
utf16le_to_utf8(&[0x3D, 0xD8, 0x00, 0xDE]).as_deref(),
|
|
84
146
|
Some("😀")
|
|
85
147
|
);
|
|
148
|
+
assert_eq!(
|
|
149
|
+
utf16le_utf8_len(&[0x3D, 0xD8, 0x00, 0xDE, 0x41, 0x00]),
|
|
150
|
+
Some(5)
|
|
151
|
+
);
|
|
86
152
|
assert_eq!(utf16le_to_utf8(&[0x41]), None);
|
|
153
|
+
assert_eq!(utf16le_utf8_len(&[0x41]), None);
|
|
87
154
|
assert_eq!(utf16le_to_utf8(&[0x3D, 0xD8]), None);
|
|
155
|
+
assert_eq!(utf16le_utf8_len(&[0x41, 0x00, 0x00, 0xDC]), None);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
#[test]
|
|
159
|
+
fn writers_stop_where_text_does_not_decode_or_fit() {
|
|
160
|
+
let mut out = [0; 8];
|
|
161
|
+
assert_eq!(
|
|
162
|
+
utf16le_to_utf8_into(&[0x41, 0x00, 0x00, 0xDC], &mut out),
|
|
163
|
+
None
|
|
164
|
+
);
|
|
165
|
+
assert_eq!(utf16le_to_utf8_into(&[0x41, 0x00, 0x42], &mut out), None);
|
|
166
|
+
assert_eq!(
|
|
167
|
+
utf16le_to_utf8_into(&[0x3D, 0xD8, 0x00, 0xDE], out.get_mut(..3).unwrap()),
|
|
168
|
+
None
|
|
169
|
+
);
|
|
170
|
+
assert_eq!(
|
|
171
|
+
utf16le_to_utf8_into(&[0x3D, 0xD8, 0x00, 0xDE], &mut out),
|
|
172
|
+
Some(4)
|
|
173
|
+
);
|
|
174
|
+
let written = latin1_to_utf8_into(b"ab\xE9c", out.get_mut(..3).unwrap());
|
|
175
|
+
assert_eq!(out.get(..written), Some(b"ab".as_slice()));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
fn utf16le(units: &[u16]) -> Vec<u8> {
|
|
179
|
+
units.iter().flat_map(|unit| unit.to_le_bytes()).collect()
|
|
88
180
|
}
|
|
89
181
|
|
|
90
182
|
proptest! {
|
|
@@ -100,7 +192,26 @@ mod tests {
|
|
|
100
192
|
)) {
|
|
101
193
|
let bytes: Vec<u8> = segments.into_iter().flat_map(|(run, wide)| run.into_iter().chain(wide)).collect();
|
|
102
194
|
let by_byte: String = bytes.iter().copied().map(char::from).collect();
|
|
195
|
+
prop_assert_eq!(latin1_utf8_len(&bytes), by_byte.len());
|
|
196
|
+
let mut out = vec![0; bytes.len().saturating_mul(2)];
|
|
197
|
+
let written = latin1_to_utf8_into(&bytes, &mut out);
|
|
198
|
+
prop_assert_eq!(out.get(..written), Some(by_byte.as_bytes()));
|
|
103
199
|
prop_assert_eq!(latin1_to_utf8(&bytes), by_byte);
|
|
104
200
|
}
|
|
201
|
+
|
|
202
|
+
// Units from each range UTF-8 sizes differently, surrogates included,
|
|
203
|
+
// so pairs, lone halves and pairs in the wrong order all come up.
|
|
204
|
+
#[test]
|
|
205
|
+
fn utf16_matches_std(units in proptest::collection::vec(
|
|
206
|
+
prop_oneof![0u16..0x80, 0x80u16..0x800, 0x800u16..0xD800, 0xD800u16..0xE000, 0xE000u16..=0xFFFF],
|
|
207
|
+
0..64,
|
|
208
|
+
)) {
|
|
209
|
+
let expected = String::from_utf16(&units).ok();
|
|
210
|
+
prop_assert_eq!(utf16le_utf8_len(&utf16le(&units)), expected.as_ref().map(String::len));
|
|
211
|
+
let mut out = vec![0; units.len().saturating_mul(3)];
|
|
212
|
+
let written = utf16le_to_utf8_into(&utf16le(&units), &mut out);
|
|
213
|
+
prop_assert_eq!(written.and_then(|n| out.get(..n)), expected.as_ref().map(String::as_bytes));
|
|
214
|
+
prop_assert_eq!(utf16le_to_utf8(&utf16le(&units)), expected);
|
|
215
|
+
}
|
|
105
216
|
}
|
|
106
217
|
}
|
|
@@ -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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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<
|
|
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:
|
|
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:
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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) =>
|
|
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
|
-
|
|
192
|
-
|
|
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(
|
|
290
|
-
Container::Map => Frame::Map(
|
|
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, .. } =>
|
|
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(
|
|
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.
|
|
620
|
+
self.put_pair(*hash, key, value)
|
|
336
621
|
}
|
|
337
|
-
Frame::Map(hash) =>
|
|
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, .. } =>
|
|
357
|
-
|
|
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
|
-
|
|
115
|
-
|
|
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
|
}
|
|
@@ -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
|
|
225
|
-
/// stack
|
|
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.
|