nosj 0.3.2 → 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 +55 -0
- data/Cargo.lock +41 -41
- data/README.md +19 -1
- data/ext/nosj/Cargo.toml +6 -6
- data/ext/nosj/fuzz/Cargo.toml +1 -1
- data/ext/nosj/src/errors.rs +3 -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 +50 -49
- data/ext/nosj/src/gen/ruby.rs +54 -22
- data/ext/nosj/src/gen/walker.rs +56 -73
- data/ext/nosj/src/lazy.rs +60 -50
- data/ext/nosj/src/lib.rs +17 -0
- 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/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/lib.rs
CHANGED
|
@@ -37,6 +37,14 @@ use magnus::{method, prelude::*, Error, Ruby};
|
|
|
37
37
|
// "nosj/nosj"), not the package name nosj_native (see Cargo.toml).
|
|
38
38
|
#[magnus::init(name = "nosj")]
|
|
39
39
|
fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
40
|
+
// Declared before any method exists: Ruby marks the methods defined
|
|
41
|
+
// after this call as callable from every Ractor. What backs it: the
|
|
42
|
+
// generate scratch is taken out of its thread-local for a call (see
|
|
43
|
+
// gen::GEN_SCRATCH), the key caches hold only shareable VALUEs
|
|
44
|
+
// (interned strings, static symbols), and the warm-up at the end of
|
|
45
|
+
// init resolves every lazily-initialized static on the main Ractor.
|
|
46
|
+
// SAFETY: a flag write on the VM's extension-load state.
|
|
47
|
+
unsafe { rb_sys::rb_ext_ractor_safe(true) };
|
|
40
48
|
compile_info();
|
|
41
49
|
|
|
42
50
|
let module = ruby.define_module("NOSJ")?;
|
|
@@ -104,6 +112,15 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
104
112
|
// its `generate` through a Ruby frame into C, so skipping our own
|
|
105
113
|
// forwarder frame is a straight per-call win on small documents.
|
|
106
114
|
module.define_singleton_method("generate", method!(gen::generate_entry, -1))?;
|
|
115
|
+
|
|
116
|
+
// Warm-up on the main Ractor: magnus resolves a TypedData class (and
|
|
117
|
+
// gen/ruby.rs its interned IDs) behind a blocking lazy initializer
|
|
118
|
+
// that calls into the VM, and two Ractors racing that first touch
|
|
119
|
+
// deadlock (the loser parks natively and never joins the VM barrier
|
|
120
|
+
// the winner needs).
|
|
121
|
+
let _ = <state::ShadowHandle as magnus::TypedData>::class(ruby);
|
|
122
|
+
let _ = <lazy::LazyNode as magnus::TypedData>::class(ruby);
|
|
123
|
+
gen::warm_up();
|
|
107
124
|
Ok(())
|
|
108
125
|
}
|
|
109
126
|
|
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..]);
|
data/ext/nosj/src/pointer.rs
CHANGED
|
@@ -7,7 +7,7 @@ use magnus::{Error, RString, Ruby, Value};
|
|
|
7
7
|
|
|
8
8
|
use crate::errors::parser_error_at;
|
|
9
9
|
use crate::parse::{materialize_at, parse_native_opts, span_of, utf8_input, ParseNativeOpts};
|
|
10
|
-
use crate::state::
|
|
10
|
+
use crate::state::with_pull_state;
|
|
11
11
|
|
|
12
12
|
/// Resolve one JSON Pointer against `data`, materializing the matched
|
|
13
13
|
/// subtree; `nil` when the pointer misses.
|
|
@@ -19,12 +19,11 @@ fn at_pointer_impl(
|
|
|
19
19
|
) -> Result<Value, Error> {
|
|
20
20
|
use magnus::value::ReprValue;
|
|
21
21
|
let input = utf8_input(ruby, &data)?;
|
|
22
|
-
// Resolve
|
|
23
|
-
//
|
|
24
|
-
let resolved =
|
|
25
|
-
let mut state = cell.borrow_mut();
|
|
22
|
+
// Resolve, then materialize, as two separate uses of the parse
|
|
23
|
+
// state; the resolved slice borrows `input`, not the state.
|
|
24
|
+
let resolved = with_pull_state(|state| {
|
|
26
25
|
// Safety: coderange verified above.
|
|
27
|
-
unsafe { nosj::
|
|
26
|
+
unsafe { nosj::pointer_utf8_unchecked_with(input, pointer, &mut state.bufs, o.popts) }
|
|
28
27
|
});
|
|
29
28
|
match resolved {
|
|
30
29
|
Ok(None) => Ok(ruby.qnil().as_value()),
|
|
@@ -125,12 +124,11 @@ fn at_pointers_impl(
|
|
|
125
124
|
let input = utf8_input(ruby, &data)?;
|
|
126
125
|
let live: Vec<&str> = pointers.iter().flatten().map(String::as_str).collect();
|
|
127
126
|
|
|
128
|
-
// Resolve first (one
|
|
129
|
-
// `input`, not the
|
|
130
|
-
let resolved =
|
|
131
|
-
let mut state = cell.borrow_mut();
|
|
127
|
+
// Resolve first (one use of the parse state); the resolved slices
|
|
128
|
+
// borrow `input`, not the state, so each materializes separately.
|
|
129
|
+
let resolved = with_pull_state(|state| {
|
|
132
130
|
// Safety: coderange verified by utf8_input.
|
|
133
|
-
unsafe { nosj::
|
|
131
|
+
unsafe { nosj::pointers_utf8_unchecked_with(input, &live, &mut state.bufs, o.popts) }
|
|
134
132
|
});
|
|
135
133
|
let mut hits = match resolved {
|
|
136
134
|
Ok(hits) => hits.into_iter(),
|
data/ext/nosj/src/reformat.rs
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
//! canonical spelling (`1.50` becomes `1.5`), and string escapes are
|
|
14
14
|
//! normalized by the emission kernels.
|
|
15
15
|
|
|
16
|
-
use std::cell::
|
|
16
|
+
use std::cell::Cell;
|
|
17
17
|
|
|
18
18
|
use magnus::value::ReprValue;
|
|
19
19
|
use magnus::{Error, RString, Ruby, Value};
|
|
@@ -23,16 +23,15 @@ use nosj::{FloatFormat, WriteOptions, Writer};
|
|
|
23
23
|
use crate::errors::{nesting_error, nosj_exception, parser_error, parser_error_at};
|
|
24
24
|
use crate::files::with_mapped_file;
|
|
25
25
|
use crate::gen::opts::{parse_gen_opts, GenConfig, DEFAULT_CONFIG};
|
|
26
|
-
use crate::parse::{parse_native_opts, utf8_input};
|
|
26
|
+
use crate::parse::{parse_native_opts, utf8_input, ParseNativeOpts};
|
|
27
27
|
use crate::patch::finish_string;
|
|
28
28
|
use crate::sink::SinkAbort;
|
|
29
|
-
use crate::state::
|
|
29
|
+
use crate::state::{with_pull_state, with_taken};
|
|
30
30
|
|
|
31
31
|
thread_local! {
|
|
32
|
-
/// Pooled output buffer: capacity survives across calls
|
|
33
|
-
///
|
|
34
|
-
|
|
35
|
-
static PIPE_BUF: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
|
|
32
|
+
/// Pooled output buffer: capacity survives across calls (see
|
|
33
|
+
/// `state::with_taken`).
|
|
34
|
+
static PIPE_BUF: Cell<Vec<u8>> = const { Cell::new(Vec::new()) };
|
|
36
35
|
}
|
|
37
36
|
|
|
38
37
|
/// The event-to-Writer pipe. Structure events forward to the Writer's
|
|
@@ -230,37 +229,50 @@ fn write_options(cfg: &GenConfig) -> WriteOptions {
|
|
|
230
229
|
w
|
|
231
230
|
}
|
|
232
231
|
|
|
232
|
+
/// Decoded reformat options: parse acceptance plus generate formatting.
|
|
233
|
+
/// Decoded before the source is borrowed, since decoding can run Ruby
|
|
234
|
+
/// (an option value's `to_int`; see `parse::utf8_input`).
|
|
235
|
+
struct ReformatOpts {
|
|
236
|
+
parse: ParseNativeOpts,
|
|
237
|
+
generate: Option<GenConfig>,
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
impl ReformatOpts {
|
|
241
|
+
fn decode(ruby: &Ruby, opts: Value) -> Result<Self, Error> {
|
|
242
|
+
Ok(Self {
|
|
243
|
+
parse: parse_native_opts(ruby, opts)?,
|
|
244
|
+
generate: if opts.is_nil() {
|
|
245
|
+
None
|
|
246
|
+
} else {
|
|
247
|
+
Some(parse_gen_opts(ruby, opts)?.0)
|
|
248
|
+
},
|
|
249
|
+
})
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
233
253
|
/// Run the pipe over already-UTF-8-vouched bytes.
|
|
234
|
-
fn reformat_over(ruby: &Ruby, input: &[u8], opts:
|
|
235
|
-
let po =
|
|
236
|
-
let
|
|
237
|
-
let gcfg: &GenConfig = if opts.is_nil() {
|
|
238
|
-
&DEFAULT_CONFIG
|
|
239
|
-
} else {
|
|
240
|
-
built = parse_gen_opts(ruby, opts)?.0;
|
|
241
|
-
&built
|
|
242
|
-
};
|
|
254
|
+
fn reformat_over(ruby: &Ruby, input: &[u8], opts: &ReformatOpts) -> Result<RString, Error> {
|
|
255
|
+
let po = &opts.parse;
|
|
256
|
+
let gcfg = opts.generate.as_ref().unwrap_or(&DEFAULT_CONFIG);
|
|
243
257
|
let wopts = write_options(gcfg);
|
|
244
258
|
|
|
245
|
-
PIPE_BUF
|
|
246
|
-
let mut buf = cell.borrow_mut();
|
|
259
|
+
with_taken(&PIPE_BUF, |buf| {
|
|
247
260
|
buf.clear();
|
|
248
261
|
// The output is at least input-sized for minify-shaped runs.
|
|
249
262
|
buf.reserve(input.len());
|
|
250
263
|
let mut sink = PipeSink {
|
|
251
|
-
w: Writer::new(
|
|
264
|
+
w: Writer::new(buf, &wopts),
|
|
252
265
|
depth: 0,
|
|
253
266
|
max_nesting: po.max_nesting,
|
|
254
267
|
mode: gcfg.mode,
|
|
255
268
|
allow_nan: gcfg.allow_nan,
|
|
256
269
|
};
|
|
257
|
-
let result =
|
|
258
|
-
let mut state = state_cell.borrow_mut();
|
|
270
|
+
let result = with_pull_state(|state| {
|
|
259
271
|
// Safety: callers verified UTF-8 (coderange or full scan).
|
|
260
272
|
unsafe { nosj::parse_utf8_unchecked_with(input, &mut state.bufs, &mut sink, po.popts) }
|
|
261
273
|
});
|
|
262
274
|
match result {
|
|
263
|
-
Ok(()) => finish_string(
|
|
275
|
+
Ok(()) => finish_string(buf),
|
|
264
276
|
Err(nosj::DriveError::Sink(SinkAbort::TooDeep)) => Err(nesting_error(
|
|
265
277
|
ruby,
|
|
266
278
|
format!(
|
|
@@ -296,8 +308,9 @@ pub fn reformat_native(
|
|
|
296
308
|
data: RString,
|
|
297
309
|
opts: Value,
|
|
298
310
|
) -> Result<RString, Error> {
|
|
311
|
+
let opts = ReformatOpts::decode(ruby, opts)?;
|
|
299
312
|
let input = utf8_input(ruby, &data)?;
|
|
300
|
-
reformat_over(ruby, input, opts)
|
|
313
|
+
reformat_over(ruby, input, &opts)
|
|
301
314
|
}
|
|
302
315
|
|
|
303
316
|
/// `NOSJ.reformat_file_native(path, opts)`: the pipe over a read-only
|
|
@@ -309,12 +322,13 @@ pub fn reformat_file_native(
|
|
|
309
322
|
opts: Value,
|
|
310
323
|
) -> Result<RString, Error> {
|
|
311
324
|
let p = path.to_string()?;
|
|
325
|
+
let opts = ReformatOpts::decode(ruby, opts)?;
|
|
312
326
|
// Mapping a zero-length file fails with EINVAL on Linux; route an
|
|
313
327
|
// empty file to the parser's own "unexpected end of input" so the
|
|
314
328
|
// error class is deterministic across platforms. Metadata failures
|
|
315
329
|
// fall through for the mapper's Errno.
|
|
316
330
|
if std::fs::metadata(&p).is_ok_and(|m| m.len() == 0) {
|
|
317
|
-
return reformat_over(ruby, &[], opts);
|
|
331
|
+
return reformat_over(ruby, &[], &opts);
|
|
318
332
|
}
|
|
319
|
-
with_mapped_file(ruby, &p, |map| reformat_over(ruby, &map, opts))
|
|
333
|
+
with_mapped_file(ruby, &p, |map| reformat_over(ruby, &map, &opts))
|
|
320
334
|
}
|