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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +44 -0
- data/Cargo.lock +17 -17
- data/README.md +2 -1
- data/ext/nosj/Cargo.toml +6 -6
- data/ext/nosj/fuzz/Cargo.toml +1 -1
- 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/ruby.rs +45 -22
- data/ext/nosj/src/gen/walker.rs +56 -73
- data/ext/nosj/src/lazy.rs +60 -50
- data/ext/nosj/src/lines.rs +27 -17
- data/ext/nosj/src/parse.rs +10 -6
- data/ext/nosj/src/patch.rs +25 -15
- data/ext/nosj/src/pointer.rs +9 -11
- data/ext/nosj/src/reformat.rs +39 -25
- data/ext/nosj/src/sink.rs +13 -16
- data/ext/nosj/src/state.rs +60 -14
- data/ext/nosj/src/stats.rs +2 -3
- data/lib/nosj/version.rb +1 -1
- data/lib/nosj.rb +2 -1
- metadata +2 -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,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,
|
|
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
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
|
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
|
-
|
|
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::
|
|
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 {
|
data/ext/nosj/src/lines.rs
CHANGED
|
@@ -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.
|
|
28
|
-
///
|
|
29
|
-
|
|
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
|
-
|
|
32
|
-
let
|
|
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(
|
|
36
|
-
if !blank(&
|
|
37
|
-
let value = materialize_at(ruby,
|
|
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
|
|
57
|
-
//
|
|
58
|
-
|
|
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
|
|
61
|
-
//
|
|
62
|
-
//
|
|
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
|
}
|
data/ext/nosj/src/parse.rs
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
} =
|
|
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 =
|
|
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,
|
data/ext/nosj/src/patch.rs
CHANGED
|
@@ -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::
|
|
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 =
|
|
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> =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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,
|
|
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,
|
|
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() +
|
|
429
|
+
let mut out = Vec::with_capacity(input.len() + rendered.len());
|
|
421
430
|
let mut pos = 0;
|
|
422
|
-
for &(s, e,
|
|
431
|
+
for &(s, e, edit) in &spans {
|
|
432
|
+
let (rs, re) = ranges[edit];
|
|
423
433
|
out.extend_from_slice(&input[pos..s]);
|
|
424
|
-
|
|
434
|
+
out.extend_from_slice(&rendered[rs..re]);
|
|
425
435
|
pos = e;
|
|
426
436
|
}
|
|
427
437
|
out.extend_from_slice(&input[pos..]);
|