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/sink.rs
CHANGED
|
@@ -35,24 +35,21 @@ pub(crate) enum SinkAbort {
|
|
|
35
35
|
NonFiniteFloat(&'static str),
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
/// `
|
|
39
|
-
///
|
|
40
|
-
/// the
|
|
41
|
-
///
|
|
42
|
-
///
|
|
43
|
-
///
|
|
44
|
-
///
|
|
45
|
-
// The
|
|
46
|
-
//
|
|
47
|
-
#[allow(clippy::
|
|
38
|
+
/// Integer VALUE for `i` via rb-sys's inline `LONG2NUM` (the header
|
|
39
|
+
/// macro: a Fixnum tagged inline when it fits, else a Bignum), which
|
|
40
|
+
/// saves the FFI call per integer that `rb_ll2inum` costs. The fixable
|
|
41
|
+
/// range is defined by the C `long`, which is 32-bit on Windows (LLP64):
|
|
42
|
+
/// fixnums there hold only 31 bits, and tagging anything wider crashes
|
|
43
|
+
/// Ruby with "Unnormalized Fixnum value"; values beyond `long` take
|
|
44
|
+
/// `rb_ll2inum`.
|
|
45
|
+
// The conversion is an identity on LP64 hosts (clippy flags it there)
|
|
46
|
+
// but narrows on Windows, where c_long is 32-bit.
|
|
47
|
+
#[allow(clippy::useless_conversion, clippy::unnecessary_fallible_conversions)]
|
|
48
48
|
#[inline(always)]
|
|
49
49
|
fn int_to_raw(i: i64) -> rb_sys::VALUE {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
((i as u64) << 1).wrapping_add(1) as rb_sys::VALUE
|
|
54
|
-
} else {
|
|
55
|
-
unsafe { rb_sys::rb_ll2inum(i) }
|
|
50
|
+
match std::os::raw::c_long::try_from(i) {
|
|
51
|
+
Ok(l) => rb_sys::macros::LONG2NUM(l),
|
|
52
|
+
Err(_) => unsafe { rb_sys::rb_ll2inum(i) },
|
|
56
53
|
}
|
|
57
54
|
}
|
|
58
55
|
|
data/ext/nosj/src/state.rs
CHANGED
|
@@ -10,18 +10,36 @@ use ahash::AHashMap;
|
|
|
10
10
|
use magnus::typed_data::Obj;
|
|
11
11
|
use magnus::{DataTypeFunctions, TypedData};
|
|
12
12
|
use nosj::Buffers;
|
|
13
|
-
use std::cell::
|
|
13
|
+
use std::cell::Cell;
|
|
14
|
+
use std::thread::LocalKey;
|
|
14
15
|
|
|
15
|
-
///
|
|
16
|
-
///
|
|
17
|
-
///
|
|
16
|
+
/// Run `f` on a pooled thread-local value, taken OUT of its cell for the
|
|
17
|
+
/// call and stored back afterwards. Pooled state is never borrowed
|
|
18
|
+
/// across a call that can reach Ruby: any allocation can raise
|
|
19
|
+
/// NoMemoryError, whose longjmp skips these frames, and a RefCell borrow
|
|
20
|
+
/// held across it would stay borrowed for good (under panic=abort, the
|
|
21
|
+
/// next borrow kills the process). A value lost that way is only leaked;
|
|
22
|
+
/// the next call, like a nested one finding the cell empty, starts from
|
|
23
|
+
/// `T::default()`. The outermost call's value is the one stored back.
|
|
24
|
+
pub(crate) fn with_taken<T: Default, R>(
|
|
25
|
+
key: &'static LocalKey<Cell<T>>,
|
|
26
|
+
f: impl FnOnce(&mut T) -> R,
|
|
27
|
+
) -> R {
|
|
28
|
+
let mut value = key.take();
|
|
29
|
+
let result = f(&mut value);
|
|
30
|
+
key.set(value);
|
|
31
|
+
result
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/// Everything a parse touches, reused across calls: nosj's scratch
|
|
35
|
+
/// buffers, the interned-key caches, and the GC-marked stacks.
|
|
18
36
|
pub(crate) struct PullState {
|
|
19
37
|
pub(crate) bufs: Buffers,
|
|
20
38
|
pub(crate) keys: AHashMap<Box<str>, rb_sys::VALUE>,
|
|
21
39
|
/// Separate cache for symbolize_names mode: symbol and string VALUEs
|
|
22
40
|
/// must never share a map.
|
|
23
41
|
pub(crate) sym_keys: AHashMap<Box<str>, rb_sys::VALUE>,
|
|
24
|
-
/// Leaked once per
|
|
42
|
+
/// Leaked once per state; kept alive + GC-marked via the wrapped
|
|
25
43
|
/// handle.
|
|
26
44
|
pub(crate) vstack: Option<&'static mut VStackShadow>,
|
|
27
45
|
/// Marked shadow holding the cached key VALUEs; keys are kept alive by
|
|
@@ -29,14 +47,36 @@ pub(crate) struct PullState {
|
|
|
29
47
|
pub(crate) key_shadow: Option<&'static mut VStackShadow>,
|
|
30
48
|
}
|
|
31
49
|
|
|
50
|
+
impl PullState {
|
|
51
|
+
#[cold]
|
|
52
|
+
fn fresh() -> Box<Self> {
|
|
53
|
+
Box::new(PullState {
|
|
54
|
+
bufs: Buffers::new(),
|
|
55
|
+
keys: AHashMap::with_capacity(256),
|
|
56
|
+
sym_keys: AHashMap::new(),
|
|
57
|
+
vstack: None,
|
|
58
|
+
key_shadow: None,
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
32
63
|
thread_local! {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
64
|
+
static PULL_STATE: Cell<Option<Box<PullState>>> = const { Cell::new(None) };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/// Run `f` on this thread's parse state, taken out like [`with_taken`]
|
|
68
|
+
/// (a state lost to a longjmp leaks its shadows' last VALUEs; a nested
|
|
69
|
+
/// call would start a fresh one). Unlike the generate scratch, parse
|
|
70
|
+
/// bodies never run Ruby code, so the thread cannot hop native threads
|
|
71
|
+
/// mid-call and one thread-local access serves both the take and the
|
|
72
|
+
/// put-back: measured ~5ns per call on tiny documents against two.
|
|
73
|
+
pub(crate) fn with_pull_state<R>(f: impl FnOnce(&mut PullState) -> R) -> R {
|
|
74
|
+
PULL_STATE.with(|cell| {
|
|
75
|
+
let mut state = cell.take().unwrap_or_else(PullState::fresh);
|
|
76
|
+
let result = f(&mut state);
|
|
77
|
+
cell.set(Some(state));
|
|
78
|
+
result
|
|
79
|
+
})
|
|
40
80
|
}
|
|
41
81
|
|
|
42
82
|
/// GC-marked holder for pending VALUEs.
|
|
@@ -48,6 +88,12 @@ pub(crate) struct VStackShadow {
|
|
|
48
88
|
/// through [`DataTypeFunctions::mark`] (its trampoline, not ours),
|
|
49
89
|
/// pinning every pending VALUE with `rb_gc_mark` semantics. The class
|
|
50
90
|
/// is defined (and made a private constant) at init.
|
|
91
|
+
///
|
|
92
|
+
/// Deliberately NOT `wb_protected`: parses push VALUEs into the shadow
|
|
93
|
+
/// with plain stores, no write barriers, so an old protected handle
|
|
94
|
+
/// would let the GC miss young values it holds (a use-after-free).
|
|
95
|
+
/// Staying write-barrier-unprotected makes every GC rescan the handle,
|
|
96
|
+
/// which costs nothing measurable: there are one to three per thread.
|
|
51
97
|
#[derive(TypedData)]
|
|
52
98
|
#[magnus(class = "NOSJ::ValueStackShadow", mark)]
|
|
53
99
|
pub(crate) struct ShadowHandle(*const VStackShadow);
|
|
@@ -74,7 +120,7 @@ impl DataTypeFunctions for ShadowHandle {
|
|
|
74
120
|
}
|
|
75
121
|
}
|
|
76
122
|
|
|
77
|
-
/// Create (once per
|
|
123
|
+
/// Create (once per owning state) a leaked, GC-marked VStackShadow.
|
|
78
124
|
pub(crate) fn ensure_marked_shadow(slot: &mut Option<&'static mut VStackShadow>) {
|
|
79
125
|
if slot.is_none() {
|
|
80
126
|
let ruby = magnus::Ruby::get().expect("called on a Ruby thread");
|
|
@@ -83,7 +129,7 @@ pub(crate) fn ensure_marked_shadow(slot: &mut Option<&'static mut VStackShadow>)
|
|
|
83
129
|
}));
|
|
84
130
|
let ptr = std::ptr::from_mut::<VStackShadow>(shadow).cast_const();
|
|
85
131
|
let handle: Obj<ShadowHandle> = ruby.obj_wrap(ShadowHandle(ptr));
|
|
86
|
-
|
|
132
|
+
ruby.gc_register_mark_object(handle);
|
|
87
133
|
*slot = Some(shadow);
|
|
88
134
|
}
|
|
89
135
|
}
|
data/ext/nosj/src/stats.rs
CHANGED
|
@@ -12,7 +12,7 @@ use crate::errors::{nesting_error, parser_error, parser_error_at};
|
|
|
12
12
|
use crate::files::with_mapped_file;
|
|
13
13
|
use crate::parse::{parse_native_opts, utf8_input};
|
|
14
14
|
use crate::sink::SinkAbort;
|
|
15
|
-
use crate::state::
|
|
15
|
+
use crate::state::with_pull_state;
|
|
16
16
|
|
|
17
17
|
/// What the document's root value was; reported as a Symbol. The
|
|
18
18
|
/// Default is never observable (a successful pass always saw a root
|
|
@@ -256,8 +256,7 @@ fn stats_over(ruby: &Ruby, input: &[u8], opts: Value) -> Result<Value, Error> {
|
|
|
256
256
|
},
|
|
257
257
|
..StatsSink::default()
|
|
258
258
|
};
|
|
259
|
-
let result =
|
|
260
|
-
let mut state = cell.borrow_mut();
|
|
259
|
+
let result = with_pull_state(|state| {
|
|
261
260
|
// Safety: callers verified UTF-8 (coderange or a full scan).
|
|
262
261
|
unsafe { nosj::parse_utf8_unchecked_with(input, &mut state.bufs, &mut sink, o.popts) }
|
|
263
262
|
});
|
data/lib/nosj/version.rb
CHANGED
data/lib/nosj.rb
CHANGED
|
@@ -205,7 +205,8 @@ module NOSJ
|
|
|
205
205
|
|
|
206
206
|
# Partial parsing by JSON Pointer (with the standard +~0+/+~1+
|
|
207
207
|
# escapes). The matched subtree materializes under the same options
|
|
208
|
-
# as {.parse}
|
|
208
|
+
# as {.parse}; +allow_nan+ and +allow_trailing_comma+ also govern the
|
|
209
|
+
# walk to it.
|
|
209
210
|
#
|
|
210
211
|
# @example
|
|
211
212
|
# NOSJ.at_pointer(json, "/users/3/name") #=> "grace" or nil
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: nosj
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yaroslav Markin
|
|
@@ -104,7 +104,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
104
104
|
- !ruby/object:Gem::Version
|
|
105
105
|
version: 3.3.11
|
|
106
106
|
requirements: []
|
|
107
|
-
rubygems_version: 4.0.
|
|
107
|
+
rubygems_version: 4.0.20
|
|
108
108
|
specification_version: 4
|
|
109
109
|
summary: An extremely fast JSON parser and generator for Ruby, written in Rust and
|
|
110
110
|
SIMD-accelerated on every platform.
|