nosj 0.4.0 → 0.4.1

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,8 @@ 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_to_json, protected_to_json_if_responds, protected_to_s, rstring_bytes, str_coderange,
20
+ str_enc_index, utf8_encindexes, CR_7BIT, CR_VALID, QFALSE, QNIL, QTRUE,
20
21
  };
21
22
 
22
23
  /// Whether keys escaped under `mode` may be cached: the cached bytes
@@ -265,13 +266,15 @@ impl Gen<'_> {
265
266
  let s = unsafe { rb_sys::rb_sym2str(k) };
266
267
  return self.emit_rstring_quoted(s);
267
268
  }
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
- }
269
+ let s = self.reraise(protected_to_s(k))?;
270
+ self.emit_rstring_quoted(s)
271
+ }
272
+
273
+ /// A protected Ruby call's outcome inside the walk: a raised
274
+ /// exception becomes this walk's failure, re-raised unchanged once
275
+ /// the walk has unwound.
276
+ fn reraise<T>(&mut self, r: Result<T, Error>) -> Result<T, ()> {
277
+ r.map_err(|exc| self.fail = Some(GenFail::Reraise(exc)))
275
278
  }
276
279
 
277
280
  /// Non-native type: strict raises (except `JSON::Fragment`, which
@@ -285,7 +288,7 @@ impl Gen<'_> {
285
288
  return self.emit_rails_fallback::<PRETTY>(raw, depth);
286
289
  }
287
290
  if self.cfg.strict {
288
- if is_json_fragment(raw) {
291
+ if self.reraise(is_json_fragment(raw))? {
289
292
  return self.splice_to_json(raw);
290
293
  }
291
294
  let name = unsafe {
@@ -296,53 +299,32 @@ impl Gen<'_> {
296
299
  self.fail = Some(GenFail::Generator(format!("{name} not allowed in JSON")));
297
300
  return Err(());
298
301
  }
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
- }
313
- }
314
- }
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(())
302
+ if let Some(json) = self.reraise(protected_to_json_if_responds(raw))? {
303
+ if !is_special_const(json)
304
+ && unsafe { RB_BUILTIN_TYPE(json) } == ruby_value_type::RUBY_T_STRING
305
+ {
306
+ self.append_rstring_raw(json);
307
+ return Ok(());
320
308
  }
321
309
  }
310
+ let s = self.reraise(protected_to_s(raw))?;
311
+ self.emit_rstring_quoted(s)
322
312
  }
323
313
 
324
314
  /// Splice `raw`'s `to_json` result verbatim: the JSON::Fragment
325
315
  /// path (pre-rendered JSON, trusted like the gem trusts it).
326
316
  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
- }
317
+ let json = self.reraise(protected_to_json(raw))?;
318
+ if is_special_const(json)
319
+ || unsafe { RB_BUILTIN_TYPE(json) } != ruby_value_type::RUBY_T_STRING
320
+ {
321
+ self.fail = Some(GenFail::Generator(
322
+ "JSON::Fragment#to_json did not return a String".to_string(),
323
+ ));
324
+ return Err(());
345
325
  }
326
+ self.append_rstring_raw(json);
327
+ Ok(())
346
328
  }
347
329
 
348
330
  /// Rails-mode fallback, mirroring JSONGemEncoder#jsonify:
@@ -356,27 +338,22 @@ impl Gen<'_> {
356
338
  raw: VALUE,
357
339
  depth: usize,
358
340
  ) -> Result<(), ()> {
359
- if is_json_fragment(raw) {
341
+ if self.reraise(is_json_fragment(raw))? {
360
342
  return self.splice_to_json(raw);
361
343
  }
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
- }
344
+ let json = self.reraise(protected_as_json(raw))?;
345
+ if json == raw {
346
+ let name = unsafe {
347
+ std::ffi::CStr::from_ptr(rb_sys::rb_obj_classname(raw))
348
+ .to_string_lossy()
349
+ .into_owned()
350
+ };
351
+ self.fail = Some(GenFail::Generator(format!(
352
+ "{name}#as_json returned the receiver"
353
+ )));
354
+ return Err(());
379
355
  }
356
+ self.emit_value::<PRETTY>(json, depth)
380
357
  }
381
358
 
382
359
  fn nesting_check(&mut self, inner: usize) -> Result<(), ()> {
@@ -390,14 +367,23 @@ impl Gen<'_> {
390
367
  fn emit_array<const PRETTY: bool>(&mut self, ary: VALUE, depth: usize) -> Result<(), ()> {
391
368
  let inner = depth + 1;
392
369
  self.nesting_check(inner)?;
393
- let len = unsafe { RARRAY_LEN(ary) } as usize;
394
370
  self.out.push(b'[');
395
- if len == 0 {
371
+ if unsafe { RARRAY_LEN(ary) } == 0 {
396
372
  self.out.push(b']');
397
373
  return Ok(());
398
374
  }
399
375
  let mut i = 0usize;
400
- while i < len {
376
+ loop {
377
+ // Length and pointer are re-read every element, like the
378
+ // json gem: a user callback inside the recursion (to_json,
379
+ // to_s, as_json) may shrink the array (slots past the live
380
+ // length hold freed or reused VALUEs; growth is emitted),
381
+ // and any allocation may compact it elsewhere.
382
+ let len = unsafe { RARRAY_LEN(ary) } as usize;
383
+ if i >= len {
384
+ break;
385
+ }
386
+ let elem = unsafe { *RARRAY_CONST_PTR(ary).add(i) };
401
387
  if i > 0 {
402
388
  self.out.push(b',');
403
389
  }
@@ -405,9 +391,6 @@ impl Gen<'_> {
405
391
  self.out.extend_from_slice(&self.cfg.array_nl);
406
392
  self.push_indent(inner);
407
393
  }
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
394
  // Numeric runs (compact mode) emit through a raw local
412
395
  // cursor under one chunked reservation: per-element Vec
413
396
  // operations round-trip length and pointer through memory
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 {
@@ -24,22 +24,31 @@ fn blank(line: &[u8]) -> bool {
24
24
  /// Yield one parsed value per non-blank line. Each line parses through
25
25
  /// the shared sink machinery against the FULL source, so a malformed
26
26
  /// line raises the rich ParserError whose `#line` is the physical
27
- /// NDJSON line number. The thread state is borrowed per line, never
28
- /// across a yield: the block is free to call back into NOSJ.
29
- fn walk_lines(ruby: &Ruby, source: &[u8], o: &ParseNativeOpts) -> Result<(), Error> {
27
+ /// NDJSON line number. No slice is held across a yield: the block runs
28
+ /// arbitrary Ruby (even deduplicating a frozen source swaps its buffer,
29
+ /// see `lazy::DocBytes`), so `source` hands out the bytes afresh for
30
+ /// every line, with identical content and so identical offsets.
31
+ fn walk_lines<'s>(
32
+ ruby: &Ruby,
33
+ source: impl Fn() -> &'s [u8],
34
+ o: &ParseNativeOpts,
35
+ ) -> Result<(), Error> {
30
36
  let mut pos = 0;
31
- while pos < source.len() {
32
- let line_end = source[pos..]
37
+ loop {
38
+ let bytes = source();
39
+ if pos >= bytes.len() {
40
+ return Ok(());
41
+ }
42
+ let line_end = bytes[pos..]
33
43
  .iter()
34
44
  .position(|&b| b == b'\n')
35
- .map_or(source.len(), |p| pos + p);
36
- if !blank(&source[pos..line_end]) {
37
- let value = materialize_at(ruby, source, pos, line_end, o)?;
45
+ .map_or(bytes.len(), |p| pos + p);
46
+ if !blank(&bytes[pos..line_end]) {
47
+ let value = materialize_at(ruby, bytes, pos, line_end, o)?;
38
48
  let _: Value = ruby.yield_value(value)?;
39
49
  }
40
50
  pos = line_end + 1;
41
51
  }
42
- Ok(())
43
52
  }
44
53
 
45
54
  /// `NOSJ.each_line(source, opts) { |value| }`: the Ruby wrapper
@@ -53,15 +62,16 @@ pub fn each_line_native(
53
62
  let o = parse_native_opts(ruby, opts)?;
54
63
  let input = utf8_input(ruby, &data)?;
55
64
  if data.as_value().is_frozen() {
56
- // A frozen source cannot be mutated by the block, and `data`
57
- // lives on this frame, so the borrow stays valid across yields.
58
- walk_lines(ruby, input, &o)?;
65
+ // A frozen source keeps its content (never its buffer, see
66
+ // walk_lines), so it is re-read per line, zero-copy.
67
+ // SAFETY: validated UTF-8 above; `data` lives on this frame.
68
+ walk_lines(ruby, || unsafe { data.as_slice() }, &o)?;
59
69
  } else {
60
- // The block could mutate (or free the buffer of) an unfrozen
61
- // source mid-iteration; walk a private copy. Same policy as
62
- // NOSJ.lazy: pass a frozen string for zero-copy.
70
+ // The block could rewrite an unfrozen source mid-iteration;
71
+ // walk a private copy. Same policy as NOSJ.lazy: pass a frozen
72
+ // string for zero-copy.
63
73
  let owned = input.to_vec();
64
- walk_lines(ruby, &owned, &o)?;
74
+ walk_lines(ruby, || &owned, &o)?;
65
75
  }
66
76
  Ok(ruby.qnil().as_value())
67
77
  }
@@ -84,7 +94,7 @@ pub fn each_line_file_native(
84
94
  return Ok(ruby.qnil().as_value());
85
95
  }
86
96
  with_mapped_file(ruby, &p, |map| {
87
- walk_lines(ruby, &map, &o)?;
97
+ walk_lines(ruby, || &map, &o)?;
88
98
  Ok(ruby.qnil().as_value())
89
99
  })
90
100
  }
@@ -8,7 +8,7 @@ use magnus::{Error, RString, Ruby, Value};
8
8
 
9
9
  use crate::errors::{nesting_error, parser_error, parser_error_at};
10
10
  use crate::sink::{NullSink, RubyValueSink, SinkAbort, MAX_NESTING};
11
- use crate::state::{ensure_marked_shadow, PullState, PULL_STATE};
11
+ use crate::state::{ensure_marked_shadow, with_pull_state, PullState};
12
12
 
13
13
  pub(crate) use crate::errors::parser_error as err;
14
14
 
@@ -22,6 +22,12 @@ pub(crate) fn span_of(source: &[u8], sub: &[u8]) -> (usize, usize) {
22
22
 
23
23
  /// Validate that `data` is UTF-8 (or US-ASCII) with intact coderange and
24
24
  /// hand out its byte slice.
25
+ ///
26
+ /// The slice borrows the string's buffer, which Ruby may reallocate or
27
+ /// swap (even for a frozen string: see `lazy::DocBytes`), so it must
28
+ /// not be held across anything that can run Ruby code: user callbacks,
29
+ /// yields, or option decoding (`to_int` and friends). Decode options
30
+ /// first; re-borrow after callbacks.
25
31
  pub(crate) fn utf8_input<'a>(ruby: &Ruby, data: &'a RString) -> Result<&'a [u8], Error> {
26
32
  let raw = data.as_raw();
27
33
  unsafe {
@@ -178,8 +184,7 @@ pub(crate) fn materialize_at(
178
184
  end: usize,
179
185
  o: &ParseNativeOpts,
180
186
  ) -> Result<Value, Error> {
181
- PULL_STATE.with(|cell| {
182
- let mut state = cell.borrow_mut();
187
+ with_pull_state(|state| {
183
188
  ensure_marked_shadow(&mut state.vstack);
184
189
  ensure_marked_shadow(&mut state.key_shadow);
185
190
 
@@ -190,7 +195,7 @@ pub(crate) fn materialize_at(
190
195
  vstack,
191
196
  key_shadow,
192
197
  ..
193
- } = &mut *state;
198
+ } = state;
194
199
  let stack = &mut vstack.as_mut().unwrap().values;
195
200
  stack.clear();
196
201
 
@@ -238,8 +243,7 @@ pub fn valid_native(
238
243
  let Ok(input) = utf8_input(ruby, &data) else {
239
244
  return Ok(false);
240
245
  };
241
- let ok = PULL_STATE.with(|cell| {
242
- let mut state = cell.borrow_mut();
246
+ let ok = with_pull_state(|state| {
243
247
  let mut sink = NullSink {
244
248
  depth: 0,
245
249
  max_nesting: o.max_nesting,
@@ -16,7 +16,7 @@ use magnus::{Error, ExceptionClass, RArray, RHash, RString, Ruby, Value};
16
16
  use crate::errors::{nosj_exception, parser_error_at};
17
17
  use crate::gen::{self, opts::GenConfig};
18
18
  use crate::parse::{materialize_at, span_of, utf8_input, ParseNativeOpts};
19
- use crate::state::PULL_STATE;
19
+ use crate::state::with_pull_state;
20
20
 
21
21
  const WS: [u8; 4] = *b" \t\n\r";
22
22
 
@@ -46,8 +46,7 @@ fn gen_config<'a>(
46
46
  /// Parse failures raise the rich ParserError (absolute positions);
47
47
  /// pointer syntax errors raise ArgumentError, like `at_pointer`.
48
48
  fn span_at(ruby: &Ruby, doc: &[u8], pointer: &str) -> Result<Option<(usize, usize)>, Error> {
49
- let resolved = PULL_STATE.with(|cell| {
50
- let mut state = cell.borrow_mut();
49
+ let resolved = with_pull_state(|state| {
51
50
  // SAFETY: every entry validated the document bytes as UTF-8.
52
51
  unsafe { nosj::pointer_utf8_unchecked(doc, pointer, &mut state.bufs) }
53
52
  });
@@ -82,8 +81,7 @@ fn container_children(
82
81
  ) -> Result<(u8, Vec<ChildSpan>), Error> {
83
82
  let span = &doc[start..end];
84
83
  let kind = span.first().copied().unwrap_or(0);
85
- let walk: Result<Vec<ChildSpan>, nosj::ParseError> = PULL_STATE.with(|cell| {
86
- let mut state = cell.borrow_mut();
84
+ let walk: Result<Vec<ChildSpan>, nosj::ParseError> = with_pull_state(|state| {
87
85
  // SAFETY: doc validated UTF-8 by the entry; spans lie on token
88
86
  // edges.
89
87
  let mut r = unsafe { nosj::Reader::from_utf8_unchecked(span, &mut state.bufs) };
@@ -360,6 +358,12 @@ fn op_test(ruby: &Ruby, doc: &[u8], path: &str, expected: Value) -> Result<(), E
360
358
  /// `NOSJ.splice(json, edits, opts)`: batch pointer replacement. All
361
359
  /// targets resolve in ONE forward pass; the output is built in one
362
360
  /// sweep copying every byte outside the target spans untouched.
361
+ ///
362
+ /// Every value is generated BEFORE the source bytes are borrowed:
363
+ /// generation runs user code (`to_json`, `to_s`) that may mutate or
364
+ /// reallocate the source, or drop the only Ruby reference to a later
365
+ /// value. Rendering inside the edits iteration keeps each value a live
366
+ /// argument while it runs, and nothing after the borrow calls Ruby.
363
367
  pub fn splice_native(
364
368
  ruby: &Ruby,
365
369
  _rb_self: Value,
@@ -369,21 +373,26 @@ pub fn splice_native(
369
373
  ) -> Result<RString, Error> {
370
374
  let mut slot = None;
371
375
  let cfg = gen_config(ruby, opts, &mut slot)?;
372
- let input = utf8_input(ruby, &data)?;
376
+ // Validate the encoding up front (error precedence); the bytes are
377
+ // borrowed again only after every callback has run.
378
+ utf8_input(ruby, &data)?;
373
379
 
374
380
  let mut pointers: Vec<String> = Vec::with_capacity(edits.len());
375
- let mut values: Vec<Value> = Vec::with_capacity(edits.len());
381
+ let mut rendered: Vec<u8> = Vec::new();
382
+ let mut ranges: Vec<(usize, usize)> = Vec::with_capacity(edits.len());
376
383
  edits.foreach(|k: Value, v: Value| {
377
384
  let ptr = RString::from_value(k)
378
385
  .ok_or_else(|| arg_error(ruby, "splice pointers must be Strings".into()))?;
379
386
  pointers.push(ptr.to_string()?);
380
- values.push(v);
387
+ let start = rendered.len();
388
+ gen::emit_into(ruby, v, cfg, &mut rendered)?;
389
+ ranges.push((start, rendered.len()));
381
390
  Ok(magnus::r_hash::ForEach::Continue)
382
391
  })?;
383
392
 
393
+ let input = utf8_input(ruby, &data)?;
384
394
  let refs: Vec<&str> = pointers.iter().map(String::as_str).collect();
385
- let resolved = PULL_STATE.with(|cell| {
386
- let mut state = cell.borrow_mut();
395
+ let resolved = with_pull_state(|state| {
387
396
  // SAFETY: coderange verified by utf8_input.
388
397
  unsafe { nosj::pointers_utf8_unchecked(input, &refs, &mut state.bufs) }
389
398
  });
@@ -395,7 +404,7 @@ pub fn splice_native(
395
404
  Err(e) => return Err(parser_error_at(ruby, input, e.offset, e.to_string())),
396
405
  };
397
406
 
398
- let mut spans: Vec<(usize, usize, Value)> = Vec::with_capacity(pointers.len());
407
+ let mut spans: Vec<(usize, usize, usize)> = Vec::with_capacity(pointers.len());
399
408
  for (i, hit) in hits.into_iter().enumerate() {
400
409
  let Some(slice) = hit else {
401
410
  let exc: ExceptionClass = ruby.exception_key_error();
@@ -405,7 +414,7 @@ pub fn splice_native(
405
414
  ));
406
415
  };
407
416
  let (s, e) = span_of(input, slice.as_bytes());
408
- spans.push((s, e, values[i]));
417
+ spans.push((s, e, i));
409
418
  }
410
419
  spans.sort_unstable_by_key(|&(s, _, _)| s);
411
420
  for pair in spans.windows(2) {
@@ -417,11 +426,12 @@ pub fn splice_native(
417
426
  }
418
427
  }
419
428
 
420
- let mut out = Vec::with_capacity(input.len() + 64);
429
+ let mut out = Vec::with_capacity(input.len() + rendered.len());
421
430
  let mut pos = 0;
422
- for &(s, e, v) in &spans {
431
+ for &(s, e, edit) in &spans {
432
+ let (rs, re) = ranges[edit];
423
433
  out.extend_from_slice(&input[pos..s]);
424
- gen::emit_into(ruby, v, cfg, &mut out)?;
434
+ out.extend_from_slice(&rendered[rs..re]);
425
435
  pos = e;
426
436
  }
427
437
  out.extend_from_slice(&input[pos..]);