nosj 0.4.0 → 0.5.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/CHANGELOG.md +105 -0
- data/Cargo.lock +17 -17
- data/README.md +32 -15
- data/ext/nosj/Cargo.toml +6 -6
- data/ext/nosj/fuzz/Cargo.toml +1 -1
- data/ext/nosj/fuzz/src/prelude.rb +20 -44
- data/ext/nosj/src/files.rs +21 -20
- data/ext/nosj/src/gen/errors.rs +12 -10
- data/ext/nosj/src/gen/mod.rs +8 -10
- data/ext/nosj/src/gen/opts.rs +76 -141
- data/ext/nosj/src/gen/ruby.rs +50 -22
- data/ext/nosj/src/gen/walker.rs +219 -102
- data/ext/nosj/src/lazy.rs +60 -50
- data/ext/nosj/src/lib.rs +4 -0
- data/ext/nosj/src/lines.rs +27 -17
- data/ext/nosj/src/locate.rs +162 -0
- data/ext/nosj/src/opt_reader.rs +170 -0
- data/ext/nosj/src/parse.rs +170 -99
- data/ext/nosj/src/patch.rs +25 -15
- data/ext/nosj/src/pointer.rs +9 -11
- data/ext/nosj/src/reformat.rs +83 -140
- data/ext/nosj/src/sink.rs +193 -60
- data/ext/nosj/src/state.rs +65 -14
- data/ext/nosj/src/stats.rs +35 -33
- data/lib/nosj/json.rb +105 -39
- data/lib/nosj/version.rb +1 -1
- data/lib/nosj.rb +36 -22
- data/sig/nosj.rbs +4 -3
- metadata +4 -2
data/ext/nosj/src/gen/walker.rs
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
//! Compact and pretty modes are one const-generic body, so the compact
|
|
4
4
|
//! hot path carries no formatting branches.
|
|
5
5
|
|
|
6
|
+
use magnus::Error;
|
|
6
7
|
use nosj::emit::{self, copy_short_raw, EscapeMode};
|
|
7
8
|
use rb_sys::macros::{
|
|
8
9
|
FIX2LONG, FIXNUM_P, FLONUM_P, RARRAY_CONST_PTR, RARRAY_LEN, RB_BUILTIN_TYPE, RHASH_SIZE,
|
|
@@ -15,8 +16,9 @@ use super::keys::GenKeyCache;
|
|
|
15
16
|
use super::opts::GenConfig;
|
|
16
17
|
use super::ruby::{
|
|
17
18
|
is_json_fragment, is_special_const, protected_as_json, protected_encode_utf8,
|
|
18
|
-
|
|
19
|
-
utf8_encindexes, CR_7BIT, CR_VALID, QFALSE, QNIL,
|
|
19
|
+
protected_inspect, protected_to_json, protected_to_json_if_responds, protected_to_s,
|
|
20
|
+
rstring_bytes, str_coderange, str_enc_index, utf8_encindexes, CR_7BIT, CR_VALID, QFALSE, QNIL,
|
|
21
|
+
QTRUE,
|
|
20
22
|
};
|
|
21
23
|
|
|
22
24
|
/// Whether keys escaped under `mode` may be cached: the cached bytes
|
|
@@ -26,6 +28,84 @@ pub(super) fn mode_cacheable(mode: EscapeMode) -> bool {
|
|
|
26
28
|
matches!(mode, EscapeMode::Standard | EscapeMode::HtmlSafe)
|
|
27
29
|
}
|
|
28
30
|
|
|
31
|
+
/// A hash key's kind: how it renders (see [`key_string`]) and json 3's
|
|
32
|
+
/// duplicate-key rule.
|
|
33
|
+
#[derive(Clone, Copy, PartialEq, Eq)]
|
|
34
|
+
enum KeyKind {
|
|
35
|
+
String,
|
|
36
|
+
Symbol,
|
|
37
|
+
Other,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
#[inline(always)]
|
|
41
|
+
fn key_kind(k: VALUE) -> KeyKind {
|
|
42
|
+
if !is_special_const(k) {
|
|
43
|
+
match unsafe { RB_BUILTIN_TYPE(k) } {
|
|
44
|
+
ruby_value_type::RUBY_T_STRING => KeyKind::String,
|
|
45
|
+
ruby_value_type::RUBY_T_SYMBOL => KeyKind::Symbol,
|
|
46
|
+
_ => KeyKind::Other,
|
|
47
|
+
}
|
|
48
|
+
} else if STATIC_SYM_P(k) {
|
|
49
|
+
KeyKind::Symbol
|
|
50
|
+
} else {
|
|
51
|
+
KeyKind::Other
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// The String a key renders as, the gem's key coercion: Strings as they
|
|
56
|
+
/// are, Symbols by name, anything else through `to_s`.
|
|
57
|
+
fn key_string(k: VALUE, kind: KeyKind) -> Result<VALUE, Error> {
|
|
58
|
+
match kind {
|
|
59
|
+
KeyKind::String => Ok(k),
|
|
60
|
+
KeyKind::Symbol => Ok(unsafe { rb_sys::rb_sym2str(k) }),
|
|
61
|
+
KeyKind::Other => protected_to_s(k),
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/// json 3's cheap trigger for its duplicate-key check, per hash: keys of
|
|
66
|
+
/// one kind cannot render alike, so only a String or Symbol key in a
|
|
67
|
+
/// hash whose first key was of another kind runs the full check (once).
|
|
68
|
+
/// A key of the first key's kind costs one byte compare (the Option
|
|
69
|
+
/// packs into one byte), a hash's first key one more; only actual
|
|
70
|
+
/// mixing goes out of line.
|
|
71
|
+
struct MixedKeys {
|
|
72
|
+
first: Option<KeyKind>,
|
|
73
|
+
/// Checked already, or `allow_duplicate_key`: nothing left to do.
|
|
74
|
+
done: bool,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
impl MixedKeys {
|
|
78
|
+
#[inline(always)]
|
|
79
|
+
fn new(allow_duplicate_key: bool) -> Self {
|
|
80
|
+
MixedKeys {
|
|
81
|
+
first: None,
|
|
82
|
+
done: allow_duplicate_key,
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
#[inline(always)]
|
|
87
|
+
fn needs_check(&mut self, kind: KeyKind) -> bool {
|
|
88
|
+
match self.first {
|
|
89
|
+
Some(first) if first == kind => false,
|
|
90
|
+
None => {
|
|
91
|
+
self.first = Some(kind);
|
|
92
|
+
false
|
|
93
|
+
}
|
|
94
|
+
Some(_) => self.mixed(kind),
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
#[cold]
|
|
99
|
+
#[inline(never)]
|
|
100
|
+
fn mixed(&mut self, kind: KeyKind) -> bool {
|
|
101
|
+
if self.done || kind == KeyKind::Other {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
self.done = true;
|
|
105
|
+
true
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
29
109
|
pub(super) struct Gen<'a> {
|
|
30
110
|
/// The pooled per-thread output buffer, borrowed for the call.
|
|
31
111
|
pub(super) out: &'a mut Vec<u8>,
|
|
@@ -163,13 +243,19 @@ impl Gen<'_> {
|
|
|
163
243
|
/// The pre-escaped bytes for `k` when the cache may serve it:
|
|
164
244
|
/// frozen string key, cacheable escape mode (see
|
|
165
245
|
/// [`Gen::emit_key_cached`]). An associated fn over the split-out
|
|
166
|
-
/// fields so callers keep `self.out` free.
|
|
246
|
+
/// fields so callers keep `self.out` free. `kind` is the caller's
|
|
247
|
+
/// [`key_kind`] of `k`, computed once per pair for both this and
|
|
248
|
+
/// the duplicate-key tracking.
|
|
167
249
|
#[inline(always)]
|
|
168
|
-
fn cached_key_bytes<'k>(
|
|
250
|
+
fn cached_key_bytes<'k>(
|
|
251
|
+
cfg: &GenConfig,
|
|
252
|
+
keys: &'k GenKeyCache,
|
|
253
|
+
k: VALUE,
|
|
254
|
+
kind: KeyKind,
|
|
255
|
+
) -> Option<&'k [u8]> {
|
|
169
256
|
const FL_FREEZE: u64 = rb_sys::ruby_fl_type::RUBY_FL_FREEZE as u64;
|
|
170
|
-
if
|
|
171
|
-
&&
|
|
172
|
-
&& unsafe { RB_BUILTIN_TYPE(k) } == ruby_value_type::RUBY_T_STRING
|
|
257
|
+
if kind == KeyKind::String
|
|
258
|
+
&& mode_cacheable(cfg.mode)
|
|
173
259
|
&& unsafe { (*(k as *const rb_sys::RBasic)).flags } & FL_FREEZE != 0
|
|
174
260
|
{
|
|
175
261
|
keys.get(k)
|
|
@@ -186,8 +272,8 @@ impl Gen<'_> {
|
|
|
186
272
|
/// all through the cache-hit path here. Misses take the plain path
|
|
187
273
|
/// below, which also populates the cache.
|
|
188
274
|
#[inline(always)]
|
|
189
|
-
fn emit_pair_prefix_compact(&mut self, k: VALUE, comma: bool) -> Result<(), ()> {
|
|
190
|
-
if let Some(bytes) = Self::cached_key_bytes(self.cfg, self.keys, k) {
|
|
275
|
+
fn emit_pair_prefix_compact(&mut self, k: VALUE, kind: KeyKind, comma: bool) -> Result<(), ()> {
|
|
276
|
+
if let Some(bytes) = Self::cached_key_bytes(self.cfg, self.keys, k, kind) {
|
|
191
277
|
let n = bytes.len();
|
|
192
278
|
self.out.reserve(n + 2);
|
|
193
279
|
// SAFETY: `n + 2` bytes reserved above; `bytes` borrows
|
|
@@ -210,7 +296,7 @@ impl Gen<'_> {
|
|
|
210
296
|
if comma {
|
|
211
297
|
self.out.push(b',');
|
|
212
298
|
}
|
|
213
|
-
self.emit_key(k)?;
|
|
299
|
+
self.emit_key(k, kind)?;
|
|
214
300
|
self.out.push(b':');
|
|
215
301
|
Ok(())
|
|
216
302
|
}
|
|
@@ -220,8 +306,14 @@ impl Gen<'_> {
|
|
|
220
306
|
/// separator, cached key, colon, and digits in one reservation and
|
|
221
307
|
/// one raw cursor.
|
|
222
308
|
#[inline(always)]
|
|
223
|
-
fn emit_pair_int_compact(
|
|
224
|
-
|
|
309
|
+
fn emit_pair_int_compact(
|
|
310
|
+
&mut self,
|
|
311
|
+
k: VALUE,
|
|
312
|
+
kind: KeyKind,
|
|
313
|
+
comma: bool,
|
|
314
|
+
value: i64,
|
|
315
|
+
) -> Result<(), ()> {
|
|
316
|
+
if let Some(bytes) = Self::cached_key_bytes(self.cfg, self.keys, k, kind) {
|
|
225
317
|
let n = bytes.len();
|
|
226
318
|
self.out.reserve(n + 2 + emit::I64_MAX_LEN);
|
|
227
319
|
// SAFETY: the reservation covers separator + key + colon +
|
|
@@ -244,34 +336,26 @@ impl Gen<'_> {
|
|
|
244
336
|
}
|
|
245
337
|
return Ok(());
|
|
246
338
|
}
|
|
247
|
-
self.emit_pair_prefix_compact(k, comma)?;
|
|
339
|
+
self.emit_pair_prefix_compact(k, kind, comma)?;
|
|
248
340
|
emit::write_i64(&mut *self.out, value);
|
|
249
341
|
Ok(())
|
|
250
342
|
}
|
|
251
343
|
|
|
252
|
-
/// Object/hash key
|
|
253
|
-
///
|
|
254
|
-
fn emit_key(&mut self, k: VALUE) -> Result<(), ()> {
|
|
255
|
-
if
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
}
|
|
268
|
-
match protected_to_s(k) {
|
|
269
|
-
Ok(s) => self.emit_rstring_quoted(s),
|
|
270
|
-
Err(exc) => {
|
|
271
|
-
self.fail = Some(GenFail::Reraise(exc));
|
|
272
|
-
Err(())
|
|
273
|
-
}
|
|
274
|
-
}
|
|
344
|
+
/// Object/hash key of `kind` (the caller's [`key_kind`]), rendered
|
|
345
|
+
/// by [`key_string`]; String keys go through the pre-escaped cache.
|
|
346
|
+
fn emit_key(&mut self, k: VALUE, kind: KeyKind) -> Result<(), ()> {
|
|
347
|
+
if kind == KeyKind::String {
|
|
348
|
+
return self.emit_key_cached(k);
|
|
349
|
+
}
|
|
350
|
+
let s = self.reraise(key_string(k, kind))?;
|
|
351
|
+
self.emit_rstring_quoted(s)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/// A protected Ruby call's outcome inside the walk: a raised
|
|
355
|
+
/// exception becomes this walk's failure, re-raised unchanged once
|
|
356
|
+
/// the walk has unwound.
|
|
357
|
+
fn reraise<T>(&mut self, r: Result<T, Error>) -> Result<T, ()> {
|
|
358
|
+
r.map_err(|exc| self.fail = Some(GenFail::Reraise(exc)))
|
|
275
359
|
}
|
|
276
360
|
|
|
277
361
|
/// Non-native type: strict raises (except `JSON::Fragment`, which
|
|
@@ -285,7 +369,7 @@ impl Gen<'_> {
|
|
|
285
369
|
return self.emit_rails_fallback::<PRETTY>(raw, depth);
|
|
286
370
|
}
|
|
287
371
|
if self.cfg.strict {
|
|
288
|
-
if is_json_fragment(raw) {
|
|
372
|
+
if self.reraise(is_json_fragment(raw))? {
|
|
289
373
|
return self.splice_to_json(raw);
|
|
290
374
|
}
|
|
291
375
|
let name = unsafe {
|
|
@@ -296,53 +380,76 @@ impl Gen<'_> {
|
|
|
296
380
|
self.fail = Some(GenFail::Generator(format!("{name} not allowed in JSON")));
|
|
297
381
|
return Err(());
|
|
298
382
|
}
|
|
299
|
-
if
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
self.append_rstring_raw(json);
|
|
306
|
-
return Ok(());
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
Err(exc) => {
|
|
310
|
-
self.fail = Some(GenFail::Reraise(exc));
|
|
311
|
-
return Err(());
|
|
312
|
-
}
|
|
383
|
+
if let Some(json) = self.reraise(protected_to_json_if_responds(raw))? {
|
|
384
|
+
if !is_special_const(json)
|
|
385
|
+
&& unsafe { RB_BUILTIN_TYPE(json) } == ruby_value_type::RUBY_T_STRING
|
|
386
|
+
{
|
|
387
|
+
self.append_rstring_raw(json);
|
|
388
|
+
return Ok(());
|
|
313
389
|
}
|
|
314
390
|
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
391
|
+
let s = self.reraise(protected_to_s(raw))?;
|
|
392
|
+
self.emit_rstring_quoted(s)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/// json 3's full duplicate check, run once for a hash whose keys
|
|
396
|
+
/// mixed kinds: every key through `to_s`, first repeat raises the
|
|
397
|
+
/// gem's exact message. Cold: only mixed-kind hashes reach it.
|
|
398
|
+
#[cold]
|
|
399
|
+
fn check_duplicate_keys(&mut self, hash: VALUE) -> Result<(), ()> {
|
|
400
|
+
use super::hash_iter::{foreach_raw, Step};
|
|
401
|
+
let mut seen = std::collections::HashSet::new();
|
|
402
|
+
let mut outcome: Result<Option<VALUE>, Error> = Ok(None);
|
|
403
|
+
// SAFETY: only reached for T_HASH values.
|
|
404
|
+
unsafe {
|
|
405
|
+
foreach_raw(hash, |k, _| {
|
|
406
|
+
let key_str = match key_string(k, key_kind(k)) {
|
|
407
|
+
Ok(s) => s,
|
|
408
|
+
Err(exc) => {
|
|
409
|
+
outcome = Err(exc);
|
|
410
|
+
return Step::Stop;
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
if seen.insert(rstring_bytes(key_str).to_vec()) {
|
|
414
|
+
Step::Continue
|
|
415
|
+
} else {
|
|
416
|
+
outcome = Ok(Some(key_str));
|
|
417
|
+
Step::Stop
|
|
418
|
+
}
|
|
419
|
+
});
|
|
321
420
|
}
|
|
421
|
+
let Some(key_str) = self.reraise(outcome)? else {
|
|
422
|
+
return Ok(());
|
|
423
|
+
};
|
|
424
|
+
let key_inspect = self.reraise(protected_inspect(key_str))?;
|
|
425
|
+
let hash_inspect = self.reraise(protected_inspect(hash))?;
|
|
426
|
+
// SAFETY: rb_inspect returns Strings; both are copied out here.
|
|
427
|
+
let (key_inspect, hash_inspect) = unsafe {
|
|
428
|
+
(
|
|
429
|
+
String::from_utf8_lossy(rstring_bytes(key_inspect)).into_owned(),
|
|
430
|
+
String::from_utf8_lossy(rstring_bytes(hash_inspect)).into_owned(),
|
|
431
|
+
)
|
|
432
|
+
};
|
|
433
|
+
self.fail = Some(GenFail::Generator(format!(
|
|
434
|
+
"detected duplicate key {key_inspect} in {hash_inspect}"
|
|
435
|
+
)));
|
|
436
|
+
Err(())
|
|
322
437
|
}
|
|
323
438
|
|
|
324
439
|
/// Splice `raw`'s `to_json` result verbatim: the JSON::Fragment
|
|
325
440
|
/// path (pre-rendered JSON, trusted like the gem trusts it).
|
|
326
441
|
fn splice_to_json(&mut self, raw: VALUE) -> Result<(), ()> {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
Ok(_) => {
|
|
336
|
-
self.fail = Some(GenFail::Generator(
|
|
337
|
-
"JSON::Fragment#to_json did not return a String".to_string(),
|
|
338
|
-
));
|
|
339
|
-
Err(())
|
|
340
|
-
}
|
|
341
|
-
Err(exc) => {
|
|
342
|
-
self.fail = Some(GenFail::Reraise(exc));
|
|
343
|
-
Err(())
|
|
344
|
-
}
|
|
442
|
+
let json = self.reraise(protected_to_json(raw))?;
|
|
443
|
+
if is_special_const(json)
|
|
444
|
+
|| unsafe { RB_BUILTIN_TYPE(json) } != ruby_value_type::RUBY_T_STRING
|
|
445
|
+
{
|
|
446
|
+
self.fail = Some(GenFail::Generator(
|
|
447
|
+
"JSON::Fragment#to_json did not return a String".to_string(),
|
|
448
|
+
));
|
|
449
|
+
return Err(());
|
|
345
450
|
}
|
|
451
|
+
self.append_rstring_raw(json);
|
|
452
|
+
Ok(())
|
|
346
453
|
}
|
|
347
454
|
|
|
348
455
|
/// Rails-mode fallback, mirroring JSONGemEncoder#jsonify:
|
|
@@ -356,27 +463,22 @@ impl Gen<'_> {
|
|
|
356
463
|
raw: VALUE,
|
|
357
464
|
depth: usize,
|
|
358
465
|
) -> Result<(), ()> {
|
|
359
|
-
if is_json_fragment(raw) {
|
|
466
|
+
if self.reraise(is_json_fragment(raw))? {
|
|
360
467
|
return self.splice_to_json(raw);
|
|
361
468
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
}
|
|
374
|
-
Ok(json) => self.emit_value::<PRETTY>(json, depth),
|
|
375
|
-
Err(exc) => {
|
|
376
|
-
self.fail = Some(GenFail::Reraise(exc));
|
|
377
|
-
Err(())
|
|
378
|
-
}
|
|
469
|
+
let json = self.reraise(protected_as_json(raw))?;
|
|
470
|
+
if json == raw {
|
|
471
|
+
let name = unsafe {
|
|
472
|
+
std::ffi::CStr::from_ptr(rb_sys::rb_obj_classname(raw))
|
|
473
|
+
.to_string_lossy()
|
|
474
|
+
.into_owned()
|
|
475
|
+
};
|
|
476
|
+
self.fail = Some(GenFail::Generator(format!(
|
|
477
|
+
"{name}#as_json returned the receiver"
|
|
478
|
+
)));
|
|
479
|
+
return Err(());
|
|
379
480
|
}
|
|
481
|
+
self.emit_value::<PRETTY>(json, depth)
|
|
380
482
|
}
|
|
381
483
|
|
|
382
484
|
fn nesting_check(&mut self, inner: usize) -> Result<(), ()> {
|
|
@@ -390,14 +492,23 @@ impl Gen<'_> {
|
|
|
390
492
|
fn emit_array<const PRETTY: bool>(&mut self, ary: VALUE, depth: usize) -> Result<(), ()> {
|
|
391
493
|
let inner = depth + 1;
|
|
392
494
|
self.nesting_check(inner)?;
|
|
393
|
-
let len = unsafe { RARRAY_LEN(ary) } as usize;
|
|
394
495
|
self.out.push(b'[');
|
|
395
|
-
if
|
|
496
|
+
if unsafe { RARRAY_LEN(ary) } == 0 {
|
|
396
497
|
self.out.push(b']');
|
|
397
498
|
return Ok(());
|
|
398
499
|
}
|
|
399
500
|
let mut i = 0usize;
|
|
400
|
-
|
|
501
|
+
loop {
|
|
502
|
+
// Length and pointer are re-read every element, like the
|
|
503
|
+
// json gem: a user callback inside the recursion (to_json,
|
|
504
|
+
// to_s, as_json) may shrink the array (slots past the live
|
|
505
|
+
// length hold freed or reused VALUEs; growth is emitted),
|
|
506
|
+
// and any allocation may compact it elsewhere.
|
|
507
|
+
let len = unsafe { RARRAY_LEN(ary) } as usize;
|
|
508
|
+
if i >= len {
|
|
509
|
+
break;
|
|
510
|
+
}
|
|
511
|
+
let elem = unsafe { *RARRAY_CONST_PTR(ary).add(i) };
|
|
401
512
|
if i > 0 {
|
|
402
513
|
self.out.push(b',');
|
|
403
514
|
}
|
|
@@ -405,9 +516,6 @@ impl Gen<'_> {
|
|
|
405
516
|
self.out.extend_from_slice(&self.cfg.array_nl);
|
|
406
517
|
self.push_indent(inner);
|
|
407
518
|
}
|
|
408
|
-
// Re-read the pointer every element: an allocation inside the
|
|
409
|
-
// recursion may trigger GC compaction and move the array.
|
|
410
|
-
let elem = unsafe { *RARRAY_CONST_PTR(ary).add(i) };
|
|
411
519
|
// Numeric runs (compact mode) emit through a raw local
|
|
412
520
|
// cursor under one chunked reservation: per-element Vec
|
|
413
521
|
// operations round-trip length and pointer through memory
|
|
@@ -523,18 +631,23 @@ impl Gen<'_> {
|
|
|
523
631
|
self.out.push(b'}');
|
|
524
632
|
return Ok(());
|
|
525
633
|
}
|
|
634
|
+
let mut mixed = MixedKeys::new(self.cfg.allow_duplicate_key);
|
|
526
635
|
// SAFETY: emit_object is only reached for T_HASH values.
|
|
527
636
|
if PRETTY {
|
|
528
637
|
let mut first = true;
|
|
529
638
|
unsafe {
|
|
530
639
|
foreach_raw(hash, |k, v| {
|
|
640
|
+
let kind = key_kind(k);
|
|
641
|
+
if mixed.needs_check(kind) && self.check_duplicate_keys(hash).is_err() {
|
|
642
|
+
return Step::Stop;
|
|
643
|
+
}
|
|
531
644
|
if !first {
|
|
532
645
|
self.out.push(b',');
|
|
533
646
|
}
|
|
534
647
|
first = false;
|
|
535
648
|
self.out.extend_from_slice(&self.cfg.object_nl);
|
|
536
649
|
self.push_indent(inner);
|
|
537
|
-
if self.emit_key(k).is_err() {
|
|
650
|
+
if self.emit_key(k, kind).is_err() {
|
|
538
651
|
return Step::Stop;
|
|
539
652
|
}
|
|
540
653
|
self.out.extend_from_slice(&self.cfg.space_before);
|
|
@@ -549,20 +662,24 @@ impl Gen<'_> {
|
|
|
549
662
|
} else {
|
|
550
663
|
unsafe {
|
|
551
664
|
foreach_raw(hash, |k, v| {
|
|
665
|
+
let kind = key_kind(k);
|
|
666
|
+
if mixed.needs_check(kind) && self.check_duplicate_keys(hash).is_err() {
|
|
667
|
+
return Step::Stop;
|
|
668
|
+
}
|
|
552
669
|
// `out` always ends with '{' (just pushed) or the
|
|
553
670
|
// previous pair.
|
|
554
671
|
let comma = *self.out.last().unwrap_unchecked() != b'{';
|
|
555
672
|
// Int values fuse with their key into one write.
|
|
556
673
|
if FIXNUM_P(v) {
|
|
557
674
|
if self
|
|
558
|
-
.emit_pair_int_compact(k, comma, FIX2LONG(v) as i64)
|
|
675
|
+
.emit_pair_int_compact(k, kind, comma, FIX2LONG(v) as i64)
|
|
559
676
|
.is_err()
|
|
560
677
|
{
|
|
561
678
|
return Step::Stop;
|
|
562
679
|
}
|
|
563
680
|
return Step::Continue;
|
|
564
681
|
}
|
|
565
|
-
if self.emit_pair_prefix_compact(k, comma).is_err() {
|
|
682
|
+
if self.emit_pair_prefix_compact(k, kind, comma).is_err() {
|
|
566
683
|
return Step::Stop;
|
|
567
684
|
}
|
|
568
685
|
// Value fast arms, mirroring emit_value's: one
|
data/ext/nosj/src/lazy.rs
CHANGED
|
@@ -20,20 +20,20 @@ use magnus::{DataTypeFunctions, Error, RArray, RString, Ruby, TypedData, Value};
|
|
|
20
20
|
use crate::errors::parser_error_at;
|
|
21
21
|
use crate::parse::{materialize_at, parse_native_opts, span_of, utf8_input, ParseNativeOpts};
|
|
22
22
|
use crate::pointer::{path_to_pointer, push_escaped_token};
|
|
23
|
-
use crate::state::
|
|
23
|
+
use crate::state::with_pull_state;
|
|
24
24
|
|
|
25
25
|
/// The document bytes behind a node tree. A frozen Ruby source is
|
|
26
|
-
/// borrowed zero-copy: freezing rules out
|
|
27
|
-
/// GC-marks the string with `rb_gc_mark` semantics
|
|
28
|
-
/// alive and
|
|
29
|
-
///
|
|
26
|
+
/// borrowed zero-copy: freezing rules out any change to its CONTENT,
|
|
27
|
+
/// and every node GC-marks the string with `rb_gc_mark` semantics
|
|
28
|
+
/// (alive, and pinned against compaction). Freezing does NOT pin the
|
|
29
|
+
/// buffer, though: deduplicating a frozen String subclass or a string
|
|
30
|
+
/// carrying ivars (`-str`) swaps in an identical shared buffer and
|
|
31
|
+
/// frees the old one. So the bytes are re-read from the string on every
|
|
32
|
+
/// access; same content means every span stays valid. Anything else is
|
|
33
|
+
/// copied once.
|
|
30
34
|
pub(crate) enum DocBytes {
|
|
31
35
|
Owned(Vec<u8>),
|
|
32
|
-
Frozen
|
|
33
|
-
source: rb_sys::VALUE,
|
|
34
|
-
ptr: *const u8,
|
|
35
|
-
len: usize,
|
|
36
|
-
},
|
|
36
|
+
Frozen(RString),
|
|
37
37
|
/// A read-only file mapping (`NOSJ.load_lazy_file`): pages never
|
|
38
38
|
/// touched are never read off disk. Concurrent modification of the
|
|
39
39
|
/// mapped file by another process is documented as unsupported
|
|
@@ -48,23 +48,24 @@ struct DocInner {
|
|
|
48
48
|
opts: ParseNativeOpts,
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
// SAFETY: the
|
|
51
|
+
// SAFETY: the content is immutable for the document's whole life (an
|
|
52
52
|
// owned Vec, or a frozen Ruby string pinned and kept alive by every
|
|
53
|
-
// node's GC mark), so cross-thread reads are plain shared reads
|
|
54
|
-
//
|
|
55
|
-
// safepoints (the ShadowHandle contract in state.rs). There is no
|
|
56
|
-
// interior mutability anywhere in the type.
|
|
53
|
+
// node's GC mark), so cross-thread reads are plain shared reads of it.
|
|
54
|
+
// There is no interior mutability anywhere in the type.
|
|
57
55
|
unsafe impl Send for DocInner {}
|
|
58
56
|
unsafe impl Sync for DocInner {}
|
|
59
57
|
|
|
60
58
|
impl DocInner {
|
|
59
|
+
/// The document bytes. For a frozen source the slice is valid only
|
|
60
|
+
/// until the next Ruby call (which could swap the string's buffer;
|
|
61
|
+
/// see DocBytes): callers finish with it, or call this again,
|
|
62
|
+
/// before running Ruby code.
|
|
61
63
|
fn bytes(&self) -> &[u8] {
|
|
62
64
|
match &self.bytes {
|
|
63
65
|
DocBytes::Owned(v) => v,
|
|
64
|
-
// SAFETY:
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
DocBytes::Frozen { ptr, len, .. } => unsafe { std::slice::from_raw_parts(*ptr, *len) },
|
|
66
|
+
// SAFETY: a live string (kept alive and pinned by every
|
|
67
|
+
// node's GC mark), read fresh on each call; see DocBytes.
|
|
68
|
+
DocBytes::Frozen(source) => unsafe { source.as_slice() },
|
|
68
69
|
DocBytes::Mmap(m) => m,
|
|
69
70
|
}
|
|
70
71
|
}
|
|
@@ -77,8 +78,16 @@ const KIND_ARRAY: u8 = b'[';
|
|
|
77
78
|
/// come from the crate's resolver (token edges within the doc bytes),
|
|
78
79
|
/// and never cross the Ruby boundary, so they cannot be forged from
|
|
79
80
|
/// Ruby.
|
|
81
|
+
///
|
|
82
|
+
/// `wb_protected`: a node's only Ruby reference (a frozen source, in
|
|
83
|
+
/// the shared `DocInner`) is set before the root node is wrapped and
|
|
84
|
+
/// never written again, so there is no write to put a barrier on, and
|
|
85
|
+
/// nodes promote to the old generation instead of being rescanned by
|
|
86
|
+
/// every minor GC (children cache Ruby-side, in barrier-protected
|
|
87
|
+
/// ivars). `free_immediately`: dropping a node calls no Ruby API (it
|
|
88
|
+
/// releases a Vec, an mmap, or nothing).
|
|
80
89
|
#[derive(TypedData)]
|
|
81
|
-
#[magnus(class = "NOSJ::Lazy", mark)]
|
|
90
|
+
#[magnus(class = "NOSJ::Lazy", free_immediately, mark, wb_protected)]
|
|
82
91
|
pub struct LazyNode {
|
|
83
92
|
doc: Arc<DocInner>,
|
|
84
93
|
start: usize,
|
|
@@ -88,11 +97,10 @@ pub struct LazyNode {
|
|
|
88
97
|
|
|
89
98
|
impl DataTypeFunctions for LazyNode {
|
|
90
99
|
fn mark(&self, marker: &magnus::gc::Marker) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
marker.mark(unsafe { Value::from_raw(source) });
|
|
100
|
+
// The source was a live, frozen string at node creation, and
|
|
101
|
+
// this mark is what keeps it that way.
|
|
102
|
+
if let DocBytes::Frozen(source) = self.doc.bytes {
|
|
103
|
+
marker.mark(source);
|
|
96
104
|
}
|
|
97
105
|
}
|
|
98
106
|
}
|
|
@@ -101,6 +109,13 @@ impl LazyNode {
|
|
|
101
109
|
fn span(&self) -> &[u8] {
|
|
102
110
|
&self.doc.bytes()[self.start..self.end]
|
|
103
111
|
}
|
|
112
|
+
|
|
113
|
+
/// A Reader over this node's span, walking the grammar the document
|
|
114
|
+
/// was opened with (trailing commas, NaN keywords).
|
|
115
|
+
fn reader<'a, 'b>(&'a self, bufs: &'b mut nosj::Buffers) -> nosj::Reader<'a, 'b> {
|
|
116
|
+
// SAFETY: spans are valid UTF-8 (see resolve_in_span).
|
|
117
|
+
unsafe { nosj::Reader::from_utf8_unchecked_with(self.span(), bufs, self.doc.opts.popts) }
|
|
118
|
+
}
|
|
104
119
|
}
|
|
105
120
|
|
|
106
121
|
/// Wrap a resolved raw-value slice: containers become new lazy nodes,
|
|
@@ -125,13 +140,19 @@ fn resolved_to_value(ruby: &Ruby, doc: &Arc<DocInner>, sub: &[u8]) -> Result<Val
|
|
|
125
140
|
/// Resolve `pointer` within `node`'s span. Shared by `__get` and
|
|
126
141
|
/// `__at_pointer`; both misses and negative-index paths return nil.
|
|
127
142
|
fn resolve_in_span(ruby: &Ruby, node: &LazyNode, pointer: &str) -> Result<Value, Error> {
|
|
128
|
-
// Resolve
|
|
129
|
-
// the
|
|
130
|
-
let resolved =
|
|
131
|
-
let mut state = cell.borrow_mut();
|
|
143
|
+
// Resolve, then materialize, as two separate uses of the parse
|
|
144
|
+
// state; the resolved slice borrows the doc, not the state.
|
|
145
|
+
let resolved = with_pull_state(|state| {
|
|
132
146
|
// SAFETY: doc bytes were coderange-gated at NOSJ.lazy creation,
|
|
133
147
|
// and spans lie on token edges, so the span is valid UTF-8.
|
|
134
|
-
unsafe {
|
|
148
|
+
unsafe {
|
|
149
|
+
nosj::pointer_utf8_unchecked_with(
|
|
150
|
+
node.span(),
|
|
151
|
+
pointer,
|
|
152
|
+
&mut state.bufs,
|
|
153
|
+
node.doc.opts.popts,
|
|
154
|
+
)
|
|
155
|
+
}
|
|
135
156
|
});
|
|
136
157
|
match resolved {
|
|
137
158
|
Ok(None) => Ok(ruby.qnil().as_value()),
|
|
@@ -165,12 +186,7 @@ pub fn lazy_native(
|
|
|
165
186
|
// on the caller's machine stack, so it stays pinned through this
|
|
166
187
|
// call, and the node's mark takes over from the first GC on.
|
|
167
188
|
let bytes = if data.as_value().is_frozen() {
|
|
168
|
-
|
|
169
|
-
DocBytes::Frozen {
|
|
170
|
-
source: data.as_raw(),
|
|
171
|
-
ptr: input.as_ptr(),
|
|
172
|
-
len: input.len(),
|
|
173
|
-
}
|
|
189
|
+
DocBytes::Frozen(data)
|
|
174
190
|
} else {
|
|
175
191
|
DocBytes::Owned(input.to_vec())
|
|
176
192
|
};
|
|
@@ -288,10 +304,8 @@ pub fn lazy_keys(ruby: &Ruby, rb_self: Obj<LazyNode>) -> Result<RArray, Error> {
|
|
|
288
304
|
));
|
|
289
305
|
}
|
|
290
306
|
let out = ruby.ary_new();
|
|
291
|
-
|
|
292
|
-
let mut
|
|
293
|
-
// SAFETY: spans are valid UTF-8 (see resolve_in_span).
|
|
294
|
-
let mut r = unsafe { nosj::Reader::from_utf8_unchecked(rb_self.span(), &mut state.bufs) };
|
|
307
|
+
with_pull_state(|state| -> Result<(), Error> {
|
|
308
|
+
let mut r = rb_self.reader(&mut state.bufs);
|
|
295
309
|
r.next_node().map_err(|e| reader_err(ruby, &rb_self, e))?;
|
|
296
310
|
let mut has = match r
|
|
297
311
|
.object_first_key()
|
|
@@ -324,10 +338,8 @@ pub fn lazy_keys(ruby: &Ruby, rb_self: Obj<LazyNode>) -> Result<RArray, Error> {
|
|
|
324
338
|
/// `__size`: entry count (object pairs or array elements), one walk,
|
|
325
339
|
/// nothing materialized.
|
|
326
340
|
pub fn lazy_size(ruby: &Ruby, rb_self: Obj<LazyNode>) -> Result<usize, Error> {
|
|
327
|
-
|
|
328
|
-
let mut
|
|
329
|
-
// SAFETY: spans are valid UTF-8 (see resolve_in_span).
|
|
330
|
-
let mut r = unsafe { nosj::Reader::from_utf8_unchecked(rb_self.span(), &mut state.bufs) };
|
|
341
|
+
with_pull_state(|state| {
|
|
342
|
+
let mut r = rb_self.reader(&mut state.bufs);
|
|
331
343
|
r.next_node().map_err(|e| reader_err(ruby, &rb_self, e))?;
|
|
332
344
|
let mut n = 0usize;
|
|
333
345
|
if rb_self.kind == KIND_OBJECT {
|
|
@@ -366,14 +378,12 @@ struct ChildDesc {
|
|
|
366
378
|
|
|
367
379
|
/// `__children`: every direct child in ONE walk. Objects yield
|
|
368
380
|
/// `[key, child]` pairs, arrays yield children; containers wrap lazily,
|
|
369
|
-
/// scalars materialize. Two phases so the
|
|
370
|
-
/// before materialization
|
|
381
|
+
/// scalars materialize. Two phases so the walk's use of the parse state
|
|
382
|
+
/// ends before materialization needs it (nested, it would start fresh).
|
|
371
383
|
pub fn lazy_children(ruby: &Ruby, rb_self: Obj<LazyNode>) -> Result<RArray, Error> {
|
|
372
384
|
let base = rb_self.doc.bytes().as_ptr() as usize;
|
|
373
|
-
let descs: Result<Vec<ChildDesc>, nosj::ParseError> =
|
|
374
|
-
let mut
|
|
375
|
-
// SAFETY: spans are valid UTF-8 (see resolve_in_span).
|
|
376
|
-
let mut r = unsafe { nosj::Reader::from_utf8_unchecked(rb_self.span(), &mut state.bufs) };
|
|
385
|
+
let descs: Result<Vec<ChildDesc>, nosj::ParseError> = with_pull_state(|state| {
|
|
386
|
+
let mut r = rb_self.reader(&mut state.bufs);
|
|
377
387
|
r.next_node()?;
|
|
378
388
|
let mut out = Vec::new();
|
|
379
389
|
if rb_self.kind == KIND_OBJECT {
|