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.
@@ -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
- protected_to_json, protected_to_s, rstring_bytes, str_coderange, str_enc_index, to_json_id,
19
- utf8_encindexes, CR_7BIT, CR_VALID, QFALSE, QNIL, QTRUE,
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>(cfg: &GenConfig, keys: &'k GenKeyCache, k: VALUE) -> Option<&'k [u8]> {
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 mode_cacheable(cfg.mode)
171
- && !is_special_const(k)
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(&mut self, k: VALUE, comma: bool, value: i64) -> Result<(), ()> {
224
- if let Some(bytes) = Self::cached_key_bytes(self.cfg, self.keys, k) {
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: String and Symbol direct, anything else via to_s
253
- /// (the gem's key coercion).
254
- fn emit_key(&mut self, k: VALUE) -> Result<(), ()> {
255
- if !is_special_const(k) {
256
- match unsafe { RB_BUILTIN_TYPE(k) } {
257
- ruby_value_type::RUBY_T_STRING => return self.emit_key_cached(k),
258
- ruby_value_type::RUBY_T_SYMBOL => {
259
- let s = unsafe { rb_sys::rb_sym2str(k) };
260
- return self.emit_rstring_quoted(s);
261
- }
262
- _ => {}
263
- }
264
- } else if STATIC_SYM_P(k) {
265
- let s = unsafe { rb_sys::rb_sym2str(k) };
266
- return self.emit_rstring_quoted(s);
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 unsafe { rb_sys::rb_respond_to(raw, to_json_id()) } != 0 {
300
- match protected_to_json(raw) {
301
- Ok(json) => {
302
- if !is_special_const(json)
303
- && unsafe { RB_BUILTIN_TYPE(json) } == ruby_value_type::RUBY_T_STRING
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
- match protected_to_s(raw) {
316
- Ok(s) => self.emit_rstring_quoted(s),
317
- Err(exc) => {
318
- self.fail = Some(GenFail::Reraise(exc));
319
- Err(())
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
- match protected_to_json(raw) {
328
- Ok(json)
329
- if !is_special_const(json)
330
- && unsafe { RB_BUILTIN_TYPE(json) } == ruby_value_type::RUBY_T_STRING =>
331
- {
332
- self.append_rstring_raw(json);
333
- Ok(())
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
- match protected_as_json(raw) {
363
- Ok(json) if json == raw => {
364
- let name = unsafe {
365
- std::ffi::CStr::from_ptr(rb_sys::rb_obj_classname(raw))
366
- .to_string_lossy()
367
- .into_owned()
368
- };
369
- self.fail = Some(GenFail::Generator(format!(
370
- "{name}#as_json returned the receiver"
371
- )));
372
- Err(())
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 len == 0 {
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
- while i < len {
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::PULL_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 mutation, and every node
27
- /// GC-marks the string with `rb_gc_mark` semantics, which both keeps it
28
- /// alive and pins it against compaction, so the captured pointer stays
29
- /// valid for as long as any node exists. Anything else is copied once.
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 bytes are immutable for the document's whole life (an
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; the
54
- // raw VALUE is only dereferenced by the GC mark, which runs at
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: the source string is frozen (no mutation, no
65
- // buffer reallocation) and pinned+kept alive by every
66
- // node's GC mark; see DocBytes.
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
- if let DocBytes::Frozen { source, .. } = self.doc.bytes {
92
- use magnus::rb_sys::FromRawValue;
93
- // SAFETY: the VALUE was a live, frozen string at node
94
- // creation and this mark is what keeps it that way.
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 first (one PULL_STATE borrow, slice borrows the doc, not
129
- // the buffers), then materialize (which re-borrows internally).
130
- let resolved = PULL_STATE.with(|cell| {
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 { nosj::pointer_utf8_unchecked(node.span(), pointer, &mut state.bufs) }
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
- use magnus::rb_sys::AsRawValue;
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
- PULL_STATE.with(|cell| -> Result<(), Error> {
292
- let mut state = cell.borrow_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
- PULL_STATE.with(|cell| {
328
- let mut state = cell.borrow_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 Reader's buffer borrow ends
370
- /// before materialization re-borrows the thread state.
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> = PULL_STATE.with(|cell| {
374
- let mut state = cell.borrow_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 {