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.
@@ -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
+ }
@@ -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
+ }