nosj 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +10 -0
- data/Cargo.lock +375 -0
- data/Cargo.toml +27 -0
- data/LICENSE.txt +21 -0
- data/NOTICE +13 -0
- data/README.md +214 -0
- data/ext/nosj/Cargo.toml +25 -0
- data/ext/nosj/extconf.rb +6 -0
- data/ext/nosj/src/gen/errors.rs +61 -0
- data/ext/nosj/src/gen/hash_iter.rs +53 -0
- data/ext/nosj/src/gen/keys.rs +58 -0
- data/ext/nosj/src/gen/mod.rs +171 -0
- data/ext/nosj/src/gen/opts.rs +162 -0
- data/ext/nosj/src/gen/ruby.rs +100 -0
- data/ext/nosj/src/gen/walker.rs +638 -0
- data/ext/nosj/src/lib.rs +56 -0
- data/ext/nosj/src/parse.rs +211 -0
- data/ext/nosj/src/pointer.rs +190 -0
- data/ext/nosj/src/sink.rs +370 -0
- data/ext/nosj/src/state.rs +89 -0
- data/lib/nosj/json.rb +124 -0
- data/lib/nosj/multi_json.rb +51 -0
- data/lib/nosj/native.rb +11 -0
- data/lib/nosj/version.rb +6 -0
- data/lib/nosj.rb +193 -0
- data/sig/nosj.rbs +61 -0
- metadata +92 -0
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
//! The generation walker: recursive descent over a Ruby object graph,
|
|
2
|
+
//! emitting JSON bytes through nosj's escape and number kernels.
|
|
3
|
+
//! Compact and pretty modes are one const-generic body, so the compact
|
|
4
|
+
//! hot path carries no formatting branches.
|
|
5
|
+
|
|
6
|
+
use nosj::emit::{self, EscapeMode};
|
|
7
|
+
use rb_sys::macros::{
|
|
8
|
+
FIX2LONG, FIXNUM_P, FLONUM_P, RARRAY_CONST_PTR, RARRAY_LEN, RB_BUILTIN_TYPE, RHASH_SIZE,
|
|
9
|
+
STATIC_SYM_P,
|
|
10
|
+
};
|
|
11
|
+
use rb_sys::{ruby_value_type, VALUE};
|
|
12
|
+
|
|
13
|
+
use super::errors::GenFail;
|
|
14
|
+
use super::keys::GenKeyCache;
|
|
15
|
+
use super::opts::GenConfig;
|
|
16
|
+
use super::ruby::{
|
|
17
|
+
is_special_const, protected_encode_utf8, protected_to_json, protected_to_s, rstring_bytes,
|
|
18
|
+
str_coderange, str_enc_index, to_json_id, utf8_encindexes, CR_7BIT, CR_VALID, QFALSE, QNIL,
|
|
19
|
+
QTRUE,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/// Overlapping-word copy for short runs through a raw pointer (the
|
|
23
|
+
/// crate's `copy_small` shape, local because that helper is
|
|
24
|
+
/// crate-internal): a size-laddered pair of unaligned loads/stores
|
|
25
|
+
/// instead of a libc memmove call, whose per-call overhead measured
|
|
26
|
+
/// 42% on tiny-copy-heavy generation. Cached keys over 32 bytes are
|
|
27
|
+
/// rare enough for the memcpy fallback.
|
|
28
|
+
///
|
|
29
|
+
/// # Safety
|
|
30
|
+
///
|
|
31
|
+
/// `n` readable bytes at `src`, `n` writable bytes at `dst`, and the
|
|
32
|
+
/// ranges must not overlap.
|
|
33
|
+
#[inline(always)]
|
|
34
|
+
unsafe fn copy_short_raw(src: *const u8, dst: *mut u8, n: usize) {
|
|
35
|
+
/// One overlapping pair: word-size chunks at offset 0 and at
|
|
36
|
+
/// `n - size` cover `size..=2*size` bytes; the overlapped middle
|
|
37
|
+
/// is written twice with identical data.
|
|
38
|
+
macro_rules! word_pair {
|
|
39
|
+
($t:ty) => {{
|
|
40
|
+
const SIZE: usize = size_of::<$t>();
|
|
41
|
+
dst.cast::<$t>()
|
|
42
|
+
.write_unaligned(src.cast::<$t>().read_unaligned());
|
|
43
|
+
dst.add(n - SIZE)
|
|
44
|
+
.cast::<$t>()
|
|
45
|
+
.write_unaligned(src.add(n - SIZE).cast::<$t>().read_unaligned());
|
|
46
|
+
}};
|
|
47
|
+
}
|
|
48
|
+
if n >= 16 {
|
|
49
|
+
if n <= 32 {
|
|
50
|
+
word_pair!(u128);
|
|
51
|
+
} else {
|
|
52
|
+
std::ptr::copy_nonoverlapping(src, dst, n);
|
|
53
|
+
}
|
|
54
|
+
} else if n >= 8 {
|
|
55
|
+
word_pair!(u64);
|
|
56
|
+
} else if n >= 4 {
|
|
57
|
+
word_pair!(u32);
|
|
58
|
+
} else if n >= 2 {
|
|
59
|
+
word_pair!(u16);
|
|
60
|
+
} else if n == 1 {
|
|
61
|
+
*dst = *src;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
pub(super) struct Gen<'a> {
|
|
66
|
+
/// The pooled per-thread output buffer, borrowed for the call.
|
|
67
|
+
pub(super) out: &'a mut Vec<u8>,
|
|
68
|
+
pub(super) cfg: &'a GenConfig,
|
|
69
|
+
pub(super) fail: Option<GenFail>,
|
|
70
|
+
/// Pre-escaped key cache, borrowed for the whole document (one
|
|
71
|
+
/// thread-local borrow per generate call instead of one per key).
|
|
72
|
+
pub(super) keys: &'a mut GenKeyCache,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
impl Gen<'_> {
|
|
76
|
+
#[inline]
|
|
77
|
+
fn push_indent(&mut self, n: usize) {
|
|
78
|
+
for _ in 0..n {
|
|
79
|
+
self.out.extend_from_slice(&self.cfg.indent);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/// Quote and escape a Ruby String VALUE. Transcodes non-UTF-8 input and
|
|
84
|
+
/// rejects broken UTF-8, both matching the gem.
|
|
85
|
+
fn emit_rstring_quoted(&mut self, s: VALUE) -> Result<(), ()> {
|
|
86
|
+
let cr = str_coderange(s);
|
|
87
|
+
let valid = cr == CR_7BIT
|
|
88
|
+
|| (cr == CR_VALID && {
|
|
89
|
+
let idx = str_enc_index(s);
|
|
90
|
+
let (utf8, usascii) = utf8_encindexes();
|
|
91
|
+
idx == utf8 || idx == usascii
|
|
92
|
+
});
|
|
93
|
+
let s = if valid {
|
|
94
|
+
s
|
|
95
|
+
} else {
|
|
96
|
+
self.convert_invalid_encoding(s)?
|
|
97
|
+
};
|
|
98
|
+
// Bytes are copied into `out` before any further Ruby call, so the
|
|
99
|
+
// temporary (if transcoded) cannot be collected mid-read.
|
|
100
|
+
let bytes = unsafe { rstring_bytes(s) };
|
|
101
|
+
self.out.reserve(bytes.len() + 2);
|
|
102
|
+
self.out.push(b'"');
|
|
103
|
+
emit::escape_into(&mut *self.out, bytes, self.cfg.mode);
|
|
104
|
+
self.out.push(b'"');
|
|
105
|
+
Ok(())
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/// Cold path: string is not known-good UTF-8. Broken UTF-8-tagged input
|
|
109
|
+
/// is malformed (encode to the same encoding is a no-op, so the gem ends
|
|
110
|
+
/// up rejecting it too); anything else goes through the raising encode
|
|
111
|
+
/// and either converts or surfaces as GeneratorError with the exception's
|
|
112
|
+
/// message (the gem's exact behavior).
|
|
113
|
+
#[cold]
|
|
114
|
+
fn convert_invalid_encoding(&mut self, s: VALUE) -> Result<VALUE, ()> {
|
|
115
|
+
let (utf8, _) = utf8_encindexes();
|
|
116
|
+
if str_enc_index(s) == utf8 {
|
|
117
|
+
self.fail = Some(GenFail::Generator(
|
|
118
|
+
"source sequence is illegal/malformed utf-8".to_string(),
|
|
119
|
+
));
|
|
120
|
+
return Err(());
|
|
121
|
+
}
|
|
122
|
+
match protected_encode_utf8(s) {
|
|
123
|
+
Ok(v) => {
|
|
124
|
+
if str_coderange(v) == CR_7BIT || str_coderange(v) == CR_VALID {
|
|
125
|
+
Ok(v)
|
|
126
|
+
} else {
|
|
127
|
+
self.fail = Some(GenFail::Generator(
|
|
128
|
+
"source sequence is illegal/malformed utf-8".to_string(),
|
|
129
|
+
));
|
|
130
|
+
Err(())
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
Err(exc) => {
|
|
134
|
+
self.fail = Some(GenFail::GeneratorFrom(exc));
|
|
135
|
+
Err(())
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/// Append a Ruby String VALUE's bytes verbatim (bignum digits, to_json
|
|
141
|
+
/// results; already JSON).
|
|
142
|
+
fn append_rstring_raw(&mut self, s: VALUE) {
|
|
143
|
+
let bytes = unsafe { rstring_bytes(s) };
|
|
144
|
+
self.out.extend_from_slice(bytes);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
fn emit_float(&mut self, f: f64) -> Result<(), ()> {
|
|
148
|
+
if f.is_finite() {
|
|
149
|
+
emit::write_f64(&mut *self.out, f);
|
|
150
|
+
return Ok(());
|
|
151
|
+
}
|
|
152
|
+
let name = if f.is_nan() {
|
|
153
|
+
"NaN"
|
|
154
|
+
} else if f > 0.0 {
|
|
155
|
+
"Infinity"
|
|
156
|
+
} else {
|
|
157
|
+
"-Infinity"
|
|
158
|
+
};
|
|
159
|
+
if self.cfg.allow_nan {
|
|
160
|
+
self.out.extend_from_slice(name.as_bytes());
|
|
161
|
+
Ok(())
|
|
162
|
+
} else {
|
|
163
|
+
self.fail = Some(GenFail::Generator(format!("{name} not allowed in JSON")));
|
|
164
|
+
Err(())
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/// Emit an object key from the pre-escaped cache. Only frozen string
|
|
169
|
+
/// keys in Standard escape mode are cacheable: frozen guarantees the
|
|
170
|
+
/// content behind the VALUE can't change, and the cached bytes bake in
|
|
171
|
+
/// the escape mode.
|
|
172
|
+
fn emit_key_cached(&mut self, k: VALUE) -> Result<(), ()> {
|
|
173
|
+
const FL_FREEZE: u64 = rb_sys::ruby_fl_type::RUBY_FL_FREEZE as u64;
|
|
174
|
+
let frozen = unsafe { (*(k as *const rb_sys::RBasic)).flags } & FL_FREEZE != 0;
|
|
175
|
+
if !frozen || self.cfg.mode != EscapeMode::Standard {
|
|
176
|
+
return self.emit_rstring_quoted(k);
|
|
177
|
+
}
|
|
178
|
+
if let Some(bytes) = self.keys.get(k) {
|
|
179
|
+
if bytes.len() <= 16 {
|
|
180
|
+
emit::push_short(&mut *self.out, bytes);
|
|
181
|
+
} else {
|
|
182
|
+
self.out.extend_from_slice(bytes);
|
|
183
|
+
}
|
|
184
|
+
return Ok(());
|
|
185
|
+
}
|
|
186
|
+
let start = self.out.len();
|
|
187
|
+
self.emit_rstring_quoted(k)?;
|
|
188
|
+
self.keys.store(k, self.out[start..].into());
|
|
189
|
+
Ok(())
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/// The pre-escaped bytes for `k` when the cache may serve it:
|
|
193
|
+
/// frozen string key, Standard escape mode (see [`Gen::emit_key_cached`]
|
|
194
|
+
/// for why only that combination is cacheable). An associated fn
|
|
195
|
+
/// over the split-out fields so callers keep `self.out` free.
|
|
196
|
+
#[inline(always)]
|
|
197
|
+
fn cached_key_bytes<'k>(cfg: &GenConfig, keys: &'k GenKeyCache, k: VALUE) -> Option<&'k [u8]> {
|
|
198
|
+
const FL_FREEZE: u64 = rb_sys::ruby_fl_type::RUBY_FL_FREEZE as u64;
|
|
199
|
+
if cfg.mode == EscapeMode::Standard
|
|
200
|
+
&& !is_special_const(k)
|
|
201
|
+
&& unsafe { RB_BUILTIN_TYPE(k) } == ruby_value_type::RUBY_T_STRING
|
|
202
|
+
&& unsafe { (*(k as *const rb_sys::RBasic)).flags } & FL_FREEZE != 0
|
|
203
|
+
{
|
|
204
|
+
keys.get(k)
|
|
205
|
+
} else {
|
|
206
|
+
None
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/// Fused `,"key":` prefix for compact-mode object pairs: one
|
|
211
|
+
/// reservation, raw stores for the separator, the cached
|
|
212
|
+
/// pre-escaped key, and the colon, instead of three separate
|
|
213
|
+
/// buffer operations (comma push, key copy, colon push) per pair.
|
|
214
|
+
/// Twitter-class documents emit tens of thousands of pairs, almost
|
|
215
|
+
/// all through the cache-hit path here. Misses take the plain path
|
|
216
|
+
/// below, which also populates the cache.
|
|
217
|
+
#[inline(always)]
|
|
218
|
+
fn emit_pair_prefix_compact(&mut self, k: VALUE, comma: bool) -> Result<(), ()> {
|
|
219
|
+
if let Some(bytes) = Self::cached_key_bytes(self.cfg, self.keys, k) {
|
|
220
|
+
let n = bytes.len();
|
|
221
|
+
self.out.reserve(n + 2);
|
|
222
|
+
// SAFETY: `n + 2` bytes reserved above; `bytes` borrows
|
|
223
|
+
// the key cache, disjoint from the output.
|
|
224
|
+
unsafe {
|
|
225
|
+
let len = self.out.len();
|
|
226
|
+
let base = self.out.as_mut_ptr().add(len);
|
|
227
|
+
let mut w = 0usize;
|
|
228
|
+
if comma {
|
|
229
|
+
*base = b',';
|
|
230
|
+
w = 1;
|
|
231
|
+
}
|
|
232
|
+
copy_short_raw(bytes.as_ptr(), base.add(w), n);
|
|
233
|
+
w += n;
|
|
234
|
+
*base.add(w) = b':';
|
|
235
|
+
self.out.set_len(len + w + 1);
|
|
236
|
+
}
|
|
237
|
+
return Ok(());
|
|
238
|
+
}
|
|
239
|
+
if comma {
|
|
240
|
+
self.out.push(b',');
|
|
241
|
+
}
|
|
242
|
+
self.emit_key(k)?;
|
|
243
|
+
self.out.push(b':');
|
|
244
|
+
Ok(())
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/// Fused `,"key":123` for compact int-valued pairs (citm-class
|
|
248
|
+
/// documents are walls of these: prices, ids, seat numbers): the
|
|
249
|
+
/// separator, cached key, colon, and digits in one reservation and
|
|
250
|
+
/// one raw cursor.
|
|
251
|
+
#[inline(always)]
|
|
252
|
+
fn emit_pair_int_compact(&mut self, k: VALUE, comma: bool, value: i64) -> Result<(), ()> {
|
|
253
|
+
if let Some(bytes) = Self::cached_key_bytes(self.cfg, self.keys, k) {
|
|
254
|
+
let n = bytes.len();
|
|
255
|
+
self.out.reserve(n + 2 + emit::I64_MAX_LEN);
|
|
256
|
+
// SAFETY: the reservation covers separator + key + colon +
|
|
257
|
+
// I64_MAX_LEN digits; `bytes` borrows the key cache,
|
|
258
|
+
// disjoint from the output.
|
|
259
|
+
unsafe {
|
|
260
|
+
let len = self.out.len();
|
|
261
|
+
let base = self.out.as_mut_ptr().add(len);
|
|
262
|
+
let mut w = 0usize;
|
|
263
|
+
if comma {
|
|
264
|
+
*base = b',';
|
|
265
|
+
w = 1;
|
|
266
|
+
}
|
|
267
|
+
copy_short_raw(bytes.as_ptr(), base.add(w), n);
|
|
268
|
+
w += n;
|
|
269
|
+
*base.add(w) = b':';
|
|
270
|
+
w += 1;
|
|
271
|
+
w += emit::write_i64_raw(base.add(w), value);
|
|
272
|
+
self.out.set_len(len + w);
|
|
273
|
+
}
|
|
274
|
+
return Ok(());
|
|
275
|
+
}
|
|
276
|
+
self.emit_pair_prefix_compact(k, comma)?;
|
|
277
|
+
emit::write_i64(&mut *self.out, value);
|
|
278
|
+
Ok(())
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/// Object/hash key: String and Symbol direct, anything else via to_s
|
|
282
|
+
/// (the gem's key coercion).
|
|
283
|
+
fn emit_key(&mut self, k: VALUE) -> Result<(), ()> {
|
|
284
|
+
if !is_special_const(k) {
|
|
285
|
+
match unsafe { RB_BUILTIN_TYPE(k) } {
|
|
286
|
+
ruby_value_type::RUBY_T_STRING => return self.emit_key_cached(k),
|
|
287
|
+
ruby_value_type::RUBY_T_SYMBOL => {
|
|
288
|
+
let s = unsafe { rb_sys::rb_sym2str(k) };
|
|
289
|
+
return self.emit_rstring_quoted(s);
|
|
290
|
+
}
|
|
291
|
+
_ => {}
|
|
292
|
+
}
|
|
293
|
+
} else if STATIC_SYM_P(k) {
|
|
294
|
+
let s = unsafe { rb_sys::rb_sym2str(k) };
|
|
295
|
+
return self.emit_rstring_quoted(s);
|
|
296
|
+
}
|
|
297
|
+
match protected_to_s(k) {
|
|
298
|
+
Ok(s) => self.emit_rstring_quoted(s),
|
|
299
|
+
Err(exc) => {
|
|
300
|
+
self.fail = Some(GenFail::Reraise(exc));
|
|
301
|
+
Err(())
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/// Non-native type: strict raises; otherwise `to_json` if the object
|
|
307
|
+
/// responds (result appended verbatim), else `to_s` as a JSON string,
|
|
308
|
+
/// which is exactly what the gem's `Object#to_json` does.
|
|
309
|
+
fn emit_fallback(&mut self, raw: VALUE) -> Result<(), ()> {
|
|
310
|
+
if self.cfg.strict {
|
|
311
|
+
let name = unsafe {
|
|
312
|
+
std::ffi::CStr::from_ptr(rb_sys::rb_obj_classname(raw))
|
|
313
|
+
.to_string_lossy()
|
|
314
|
+
.into_owned()
|
|
315
|
+
};
|
|
316
|
+
self.fail = Some(GenFail::Generator(format!("{name} not allowed in JSON")));
|
|
317
|
+
return Err(());
|
|
318
|
+
}
|
|
319
|
+
if unsafe { rb_sys::rb_respond_to(raw, to_json_id()) } != 0 {
|
|
320
|
+
match protected_to_json(raw) {
|
|
321
|
+
Ok(json) => {
|
|
322
|
+
if !is_special_const(json)
|
|
323
|
+
&& unsafe { RB_BUILTIN_TYPE(json) } == ruby_value_type::RUBY_T_STRING
|
|
324
|
+
{
|
|
325
|
+
self.append_rstring_raw(json);
|
|
326
|
+
return Ok(());
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
Err(exc) => {
|
|
330
|
+
self.fail = Some(GenFail::Reraise(exc));
|
|
331
|
+
return Err(());
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
match protected_to_s(raw) {
|
|
336
|
+
Ok(s) => self.emit_rstring_quoted(s),
|
|
337
|
+
Err(exc) => {
|
|
338
|
+
self.fail = Some(GenFail::Reraise(exc));
|
|
339
|
+
Err(())
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
fn nesting_check(&mut self, inner: usize) -> Result<(), ()> {
|
|
345
|
+
if self.cfg.max_nesting > 0 && inner > self.cfg.max_nesting {
|
|
346
|
+
self.fail = Some(GenFail::Nesting(self.cfg.max_nesting));
|
|
347
|
+
return Err(());
|
|
348
|
+
}
|
|
349
|
+
Ok(())
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
fn emit_array<const PRETTY: bool>(&mut self, ary: VALUE, depth: usize) -> Result<(), ()> {
|
|
353
|
+
let inner = depth + 1;
|
|
354
|
+
self.nesting_check(inner)?;
|
|
355
|
+
let len = unsafe { RARRAY_LEN(ary) } as usize;
|
|
356
|
+
self.out.push(b'[');
|
|
357
|
+
if len == 0 {
|
|
358
|
+
self.out.push(b']');
|
|
359
|
+
return Ok(());
|
|
360
|
+
}
|
|
361
|
+
let mut i = 0usize;
|
|
362
|
+
while i < len {
|
|
363
|
+
if i > 0 {
|
|
364
|
+
self.out.push(b',');
|
|
365
|
+
}
|
|
366
|
+
if PRETTY {
|
|
367
|
+
self.out.extend_from_slice(&self.cfg.array_nl);
|
|
368
|
+
self.push_indent(inner);
|
|
369
|
+
}
|
|
370
|
+
// Re-read the pointer every element: an allocation inside the
|
|
371
|
+
// recursion may trigger GC compaction and move the array.
|
|
372
|
+
let elem = unsafe { *RARRAY_CONST_PTR(ary).add(i) };
|
|
373
|
+
// Numeric runs (compact mode) emit through a raw local
|
|
374
|
+
// cursor under one chunked reservation: per-element Vec
|
|
375
|
+
// operations round-trip length and pointer through memory
|
|
376
|
+
// and pay a capacity branch each, where the run keeps the
|
|
377
|
+
// cursor in a register and writes commas as plain stores
|
|
378
|
+
// (the C generator's shape). Numbers emit no Ruby calls,
|
|
379
|
+
// so neither GC nor compaction can run inside a run: the
|
|
380
|
+
// array pointer is cached for its duration. Non-finite
|
|
381
|
+
// floats break out pre-comma to the allow_nan-aware slow
|
|
382
|
+
// arm below.
|
|
383
|
+
if !PRETTY && (FLONUM_P(elem) || FIXNUM_P(elem)) {
|
|
384
|
+
/// Reservation ceiling per run (elements), so a huge
|
|
385
|
+
/// numeric array reserves incrementally instead of
|
|
386
|
+
/// worst-case-times-length at once.
|
|
387
|
+
const RUN_CHUNK: usize = 4096;
|
|
388
|
+
let run_start = i;
|
|
389
|
+
let chunk = (len - i).min(RUN_CHUNK);
|
|
390
|
+
self.out.reserve(chunk * (emit::F64_MAX_LEN + 1));
|
|
391
|
+
// SAFETY: the reservation above covers `chunk`
|
|
392
|
+
// elements at F64_MAX_LEN (>= I64_MAX_LEN) plus one
|
|
393
|
+
// comma each; `w` tracks exactly the bytes written and
|
|
394
|
+
// is published once on exit. No Ruby call happens
|
|
395
|
+
// inside the run, so `ptr` (and the array) are stable.
|
|
396
|
+
unsafe {
|
|
397
|
+
let base = self.out.as_mut_ptr();
|
|
398
|
+
let mut w = self.out.len();
|
|
399
|
+
let ptr = RARRAY_CONST_PTR(ary);
|
|
400
|
+
let chunk_end = i + chunk;
|
|
401
|
+
// The comma precedes every element but the run's
|
|
402
|
+
// first, and is only written once the element is
|
|
403
|
+
// known to belong to the run: every break path
|
|
404
|
+
// then leaves the output separator-clean, and the
|
|
405
|
+
// outer loop's `i > 0` arm owns the next comma.
|
|
406
|
+
let mut first = true;
|
|
407
|
+
loop {
|
|
408
|
+
let e = *ptr.add(i);
|
|
409
|
+
let mut float_val = 0.0f64;
|
|
410
|
+
let is_float = FLONUM_P(e);
|
|
411
|
+
if is_float {
|
|
412
|
+
float_val = rb_sys::macros::NUM2DBL(e);
|
|
413
|
+
if !float_val.is_finite() {
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
} else if !FIXNUM_P(e) {
|
|
417
|
+
break;
|
|
418
|
+
}
|
|
419
|
+
if !first {
|
|
420
|
+
*base.add(w) = b',';
|
|
421
|
+
w += 1;
|
|
422
|
+
}
|
|
423
|
+
first = false;
|
|
424
|
+
w += if is_float {
|
|
425
|
+
emit::write_f64_raw(base.add(w), float_val)
|
|
426
|
+
} else {
|
|
427
|
+
emit::write_i64_raw(base.add(w), FIX2LONG(e) as i64)
|
|
428
|
+
};
|
|
429
|
+
i += 1;
|
|
430
|
+
if i == chunk_end {
|
|
431
|
+
break;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
self.out.set_len(w);
|
|
435
|
+
}
|
|
436
|
+
// Anything the run consumed re-enters the outer loop,
|
|
437
|
+
// whose `i > 0` arm supplies the pending comma. A run
|
|
438
|
+
// that consumed nothing (first element is a non-finite
|
|
439
|
+
// flonum) falls through to the slow arms instead: its
|
|
440
|
+
// comma is already written.
|
|
441
|
+
if i > run_start {
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
// Scalar fast arms for the remaining shapes, hoisted out
|
|
446
|
+
// of the recursive emit_value (recursion blocks inlining,
|
|
447
|
+
// so every element otherwise pays a full call). FLONUM
|
|
448
|
+
// decodes inline via rb-sys's stable API; heap Floats
|
|
449
|
+
// (rare) still avoid the call boundary.
|
|
450
|
+
if FLONUM_P(elem) {
|
|
451
|
+
self.emit_float(unsafe { rb_sys::macros::NUM2DBL(elem) })?;
|
|
452
|
+
i += 1;
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
if !is_special_const(elem)
|
|
456
|
+
&& unsafe { RB_BUILTIN_TYPE(elem) } == ruby_value_type::RUBY_T_FLOAT
|
|
457
|
+
{
|
|
458
|
+
self.emit_float(unsafe { rb_sys::macros::NUM2DBL(elem) })?;
|
|
459
|
+
i += 1;
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
self.emit_value::<PRETTY>(elem, inner)?;
|
|
463
|
+
i += 1;
|
|
464
|
+
}
|
|
465
|
+
// The gem writes the closing newline+indent only when array_nl is
|
|
466
|
+
// set; per-element indent above is unconditional.
|
|
467
|
+
if PRETTY && !self.cfg.array_nl.is_empty() {
|
|
468
|
+
self.out.extend_from_slice(&self.cfg.array_nl);
|
|
469
|
+
self.push_indent(depth);
|
|
470
|
+
}
|
|
471
|
+
self.out.push(b']');
|
|
472
|
+
Ok(())
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/// Object pairs iterate through [`hash_iter::foreach_raw`] (the
|
|
476
|
+
/// extension's one C-ABI exception; see that module's docs).
|
|
477
|
+
/// Failures ride the `fail` flag + [`Step::Stop`], never a raise
|
|
478
|
+
/// across these frames.
|
|
479
|
+
fn emit_object<const PRETTY: bool>(&mut self, hash: VALUE, depth: usize) -> Result<(), ()> {
|
|
480
|
+
use super::hash_iter::{foreach_raw, Step};
|
|
481
|
+
let inner = depth + 1;
|
|
482
|
+
self.nesting_check(inner)?;
|
|
483
|
+
self.out.push(b'{');
|
|
484
|
+
if unsafe { RHASH_SIZE(hash) } == 0 {
|
|
485
|
+
self.out.push(b'}');
|
|
486
|
+
return Ok(());
|
|
487
|
+
}
|
|
488
|
+
// SAFETY: emit_object is only reached for T_HASH values.
|
|
489
|
+
if PRETTY {
|
|
490
|
+
let mut first = true;
|
|
491
|
+
unsafe {
|
|
492
|
+
foreach_raw(hash, |k, v| {
|
|
493
|
+
if !first {
|
|
494
|
+
self.out.push(b',');
|
|
495
|
+
}
|
|
496
|
+
first = false;
|
|
497
|
+
self.out.extend_from_slice(&self.cfg.object_nl);
|
|
498
|
+
self.push_indent(inner);
|
|
499
|
+
if self.emit_key(k).is_err() {
|
|
500
|
+
return Step::Stop;
|
|
501
|
+
}
|
|
502
|
+
self.out.extend_from_slice(&self.cfg.space_before);
|
|
503
|
+
self.out.push(b':');
|
|
504
|
+
self.out.extend_from_slice(&self.cfg.space);
|
|
505
|
+
if self.emit_value::<true>(v, inner).is_err() {
|
|
506
|
+
return Step::Stop;
|
|
507
|
+
}
|
|
508
|
+
Step::Continue
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
} else {
|
|
512
|
+
unsafe {
|
|
513
|
+
foreach_raw(hash, |k, v| {
|
|
514
|
+
// `out` always ends with '{' (just pushed) or the
|
|
515
|
+
// previous pair.
|
|
516
|
+
let comma = *self.out.last().unwrap_unchecked() != b'{';
|
|
517
|
+
// Int values fuse with their key into one write.
|
|
518
|
+
if FIXNUM_P(v) {
|
|
519
|
+
if self
|
|
520
|
+
.emit_pair_int_compact(k, comma, FIX2LONG(v) as i64)
|
|
521
|
+
.is_err()
|
|
522
|
+
{
|
|
523
|
+
return Step::Stop;
|
|
524
|
+
}
|
|
525
|
+
return Step::Continue;
|
|
526
|
+
}
|
|
527
|
+
if self.emit_pair_prefix_compact(k, comma).is_err() {
|
|
528
|
+
return Step::Stop;
|
|
529
|
+
}
|
|
530
|
+
// Value fast arms, mirroring emit_value's: one
|
|
531
|
+
// special-const branch splits heap values (strings
|
|
532
|
+
// dominate) from immediates (twitter-class objects
|
|
533
|
+
// carry a dozen-plus nulls and booleans per
|
|
534
|
+
// record). Every arm skips the non-inlinable
|
|
535
|
+
// recursive call.
|
|
536
|
+
if !is_special_const(v) {
|
|
537
|
+
if RB_BUILTIN_TYPE(v) == ruby_value_type::RUBY_T_STRING {
|
|
538
|
+
if self.emit_rstring_quoted(v).is_err() {
|
|
539
|
+
return Step::Stop;
|
|
540
|
+
}
|
|
541
|
+
return Step::Continue;
|
|
542
|
+
}
|
|
543
|
+
} else {
|
|
544
|
+
if v == QNIL {
|
|
545
|
+
self.out.extend_from_slice(b"null");
|
|
546
|
+
return Step::Continue;
|
|
547
|
+
}
|
|
548
|
+
if v == QTRUE {
|
|
549
|
+
self.out.extend_from_slice(b"true");
|
|
550
|
+
return Step::Continue;
|
|
551
|
+
}
|
|
552
|
+
if v == QFALSE {
|
|
553
|
+
self.out.extend_from_slice(b"false");
|
|
554
|
+
return Step::Continue;
|
|
555
|
+
}
|
|
556
|
+
if FLONUM_P(v) {
|
|
557
|
+
if self.emit_float(rb_sys::macros::NUM2DBL(v)).is_err() {
|
|
558
|
+
return Step::Stop;
|
|
559
|
+
}
|
|
560
|
+
return Step::Continue;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
if self.emit_value::<false>(v, inner).is_err() {
|
|
564
|
+
return Step::Stop;
|
|
565
|
+
}
|
|
566
|
+
Step::Continue
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
if self.fail.is_some() {
|
|
571
|
+
return Err(());
|
|
572
|
+
}
|
|
573
|
+
if PRETTY && !self.cfg.object_nl.is_empty() {
|
|
574
|
+
self.out.extend_from_slice(&self.cfg.object_nl);
|
|
575
|
+
self.push_indent(depth);
|
|
576
|
+
}
|
|
577
|
+
self.out.push(b'}');
|
|
578
|
+
Ok(())
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
pub(super) fn emit_value<const PRETTY: bool>(
|
|
582
|
+
&mut self,
|
|
583
|
+
raw: VALUE,
|
|
584
|
+
depth: usize,
|
|
585
|
+
) -> Result<(), ()> {
|
|
586
|
+
// Heap objects first: strings/hashes/arrays dominate real documents.
|
|
587
|
+
if !is_special_const(raw) {
|
|
588
|
+
return match unsafe { RB_BUILTIN_TYPE(raw) } {
|
|
589
|
+
ruby_value_type::RUBY_T_STRING => self.emit_rstring_quoted(raw),
|
|
590
|
+
ruby_value_type::RUBY_T_HASH => self.emit_object::<PRETTY>(raw, depth),
|
|
591
|
+
ruby_value_type::RUBY_T_ARRAY => self.emit_array::<PRETTY>(raw, depth),
|
|
592
|
+
ruby_value_type::RUBY_T_FLOAT => {
|
|
593
|
+
self.emit_float(unsafe { rb_sys::macros::NUM2DBL(raw) })
|
|
594
|
+
}
|
|
595
|
+
ruby_value_type::RUBY_T_BIGNUM => {
|
|
596
|
+
let s = unsafe { rb_sys::rb_big2str(raw, 10) };
|
|
597
|
+
self.append_rstring_raw(s);
|
|
598
|
+
Ok(())
|
|
599
|
+
}
|
|
600
|
+
ruby_value_type::RUBY_T_SYMBOL => {
|
|
601
|
+
let s = unsafe { rb_sys::rb_sym2str(raw) };
|
|
602
|
+
self.emit_rstring_quoted(s)
|
|
603
|
+
}
|
|
604
|
+
_ => self.emit_fallback(raw),
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
if FIXNUM_P(raw) {
|
|
608
|
+
emit::write_i64(&mut *self.out, unsafe { FIX2LONG(raw) } as i64);
|
|
609
|
+
return Ok(());
|
|
610
|
+
}
|
|
611
|
+
// Flonums before the nil/true/false compares: floats dominate
|
|
612
|
+
// real numeric documents while literals are rare, and a flonum
|
|
613
|
+
// here decodes inline (rb-sys stable API), no FFI call.
|
|
614
|
+
if FLONUM_P(raw) {
|
|
615
|
+
return self.emit_float(unsafe { rb_sys::macros::NUM2DBL(raw) });
|
|
616
|
+
}
|
|
617
|
+
match raw {
|
|
618
|
+
QNIL => {
|
|
619
|
+
self.out.extend_from_slice(b"null");
|
|
620
|
+
return Ok(());
|
|
621
|
+
}
|
|
622
|
+
QTRUE => {
|
|
623
|
+
self.out.extend_from_slice(b"true");
|
|
624
|
+
return Ok(());
|
|
625
|
+
}
|
|
626
|
+
QFALSE => {
|
|
627
|
+
self.out.extend_from_slice(b"false");
|
|
628
|
+
return Ok(());
|
|
629
|
+
}
|
|
630
|
+
_ => {}
|
|
631
|
+
}
|
|
632
|
+
if STATIC_SYM_P(raw) {
|
|
633
|
+
let s = unsafe { rb_sys::rb_sym2str(raw) };
|
|
634
|
+
return self.emit_rstring_quoted(s);
|
|
635
|
+
}
|
|
636
|
+
self.emit_fallback(raw)
|
|
637
|
+
}
|
|
638
|
+
}
|
data/ext/nosj/src/lib.rs
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
//! Native extension of the nosj Ruby gem: Ruby bindings over the
|
|
2
|
+
//! first-party `nosj` SIMD JSON crate.
|
|
3
|
+
//!
|
|
4
|
+
//! - `parse.rs`: whole-document entry points (parse, valid?, the
|
|
5
|
+
//! GVL-releasing indexed parse) plus shared option decoding and
|
|
6
|
+
//! input gating.
|
|
7
|
+
//! - `pointer.rs`: partial parsing (dig, at_pointer, batch forms).
|
|
8
|
+
//! - `sink.rs`: the VALUE-building and validation sinks with their
|
|
9
|
+
//! interned-key caches.
|
|
10
|
+
//! - `state.rs`: per-thread reusable state and the GC-marked value
|
|
11
|
+
//! stacks.
|
|
12
|
+
//! - `gen/`: generation (JSON.generate-compatible walker, options,
|
|
13
|
+
//! key cache, protect shims, error mapping).
|
|
14
|
+
|
|
15
|
+
pub mod gen;
|
|
16
|
+
pub mod parse;
|
|
17
|
+
pub mod pointer;
|
|
18
|
+
pub mod sink;
|
|
19
|
+
pub mod state;
|
|
20
|
+
|
|
21
|
+
use magnus::{method, prelude::*, Error, Ruby};
|
|
22
|
+
|
|
23
|
+
// Init_nosj must match the required bundle's basename (nosj.bundle,
|
|
24
|
+
// "nosj/nosj"), not the package name nosj_native (see Cargo.toml).
|
|
25
|
+
#[magnus::init(name = "nosj")]
|
|
26
|
+
fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
27
|
+
compile_info();
|
|
28
|
+
|
|
29
|
+
let module = ruby.define_module("NOSJ")?;
|
|
30
|
+
module.define_class("ValueStackShadow", ruby.class_object())?;
|
|
31
|
+
let _: magnus::Value =
|
|
32
|
+
module.funcall("private_constant", (ruby.to_symbol("ValueStackShadow"),))?;
|
|
33
|
+
module.define_singleton_method("parse_native", method!(parse::parse_native, 2))?;
|
|
34
|
+
module.define_singleton_method("valid_native", method!(parse::valid_native, 2))?;
|
|
35
|
+
module.define_singleton_method("dig_native", method!(pointer::dig_native, 2))?;
|
|
36
|
+
module.define_singleton_method("dig_many_native", method!(pointer::dig_many_native, 3))?;
|
|
37
|
+
module.define_singleton_method("at_pointer_native", method!(pointer::at_pointer_native, 3))?;
|
|
38
|
+
module.define_singleton_method(
|
|
39
|
+
"at_pointers_native",
|
|
40
|
+
method!(pointer::at_pointers_native, 3),
|
|
41
|
+
)?;
|
|
42
|
+
module.define_singleton_method("generate_native", method!(gen::generate_native, 2))?;
|
|
43
|
+
// `generate` itself is native and variadic: the json gem routes
|
|
44
|
+
// its `generate` through a Ruby frame into C, so skipping our own
|
|
45
|
+
// forwarder frame is a straight per-call win on small documents.
|
|
46
|
+
module.define_singleton_method("generate", method!(gen::generate_entry, -1))?;
|
|
47
|
+
Ok(())
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// Debug builds announce themselves so a stray unoptimized bundle is
|
|
51
|
+
/// never benchmarked by accident; release builds load silently.
|
|
52
|
+
#[inline]
|
|
53
|
+
fn compile_info() {
|
|
54
|
+
#[cfg(debug_assertions)]
|
|
55
|
+
println!("nosj: running in DEBUG mode — do not benchmark this build");
|
|
56
|
+
}
|