smarter_json 0.9.2 → 0.9.9

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.
@@ -4,7 +4,7 @@
4
4
  #ifdef __ARM_NEON
5
5
  #include <arm_neon.h>
6
6
  #endif
7
- #include "vendor/ryu.h" /* Ryū string->double, correctly rounded (Ulf Adams, Apache-2.0) */
7
+ #include "vendor/eisel_lemire.h" /* Eisel-Lemire decimal->double, correctly rounded (fast_float) */
8
8
 
9
9
  /* Branch hints / prefetch on the hot scan loops. No-ops on compilers without the
10
10
  * builtins (the code is correct either way; these only steer code layout). */
@@ -12,10 +12,12 @@
12
12
  # define FJ_LIKELY(x) __builtin_expect(!!(x), 1)
13
13
  # define FJ_UNLIKELY(x) __builtin_expect(!!(x), 0)
14
14
  # define FJ_PREFETCH(p) __builtin_prefetch(p)
15
+ # define FJ_ALWAYS_INLINE inline __attribute__((always_inline))
15
16
  #else
16
17
  # define FJ_LIKELY(x) (x)
17
18
  # define FJ_UNLIKELY(x) (x)
18
19
  # define FJ_PREFETCH(p) ((void)0)
20
+ # define FJ_ALWAYS_INLINE inline
19
21
  #endif
20
22
 
21
23
  /*
@@ -48,8 +50,7 @@ static ID fj_name_id;
48
50
  static VALUE fj_sym_encoding;
49
51
  static VALUE fj_sym_symbolize_keys;
50
52
  static VALUE fj_sym_first_wins;
51
- static VALUE fj_sym_raise;
52
- static VALUE fj_sym_bigdecimal_load;
53
+ static VALUE fj_sym_decimal_precision;
53
54
  static VALUE fj_sym_float;
54
55
  static VALUE fj_sym_bigdecimal;
55
56
  static VALUE fj_sym_on_warning;
@@ -70,8 +71,7 @@ typedef struct {
70
71
  int depth;
71
72
  int symbolize_keys;
72
73
  int dup_first_wins;
73
- int dup_raise;
74
- int bigdecimal_load; /* 0 = float, 1 = auto, 2 = bigdecimal */
74
+ int decimal_precision; /* 0 = float, 1 = auto, 2 = bigdecimal */
75
75
  fj_kc_slot *kcache; /* per-parse key cache (NULL when interning unavailable) */
76
76
  VALUE on_warning; /* on_warning: callable invoked per non-fatal lenient fix, else Qnil */
77
77
  } fj_state;
@@ -168,20 +168,39 @@ static long fj_mbws(const char *p, long n) {
168
168
  return 0;
169
169
  }
170
170
 
171
+ /* Skip a run of whitespace. This is hot on pretty-printed input, where most of
172
+ * the bytes are indentation. Indentation is homogeneous — all spaces OR all tabs,
173
+ * the two common styles — so a run of it is skipped 8 bytes at a time with a
174
+ * single 64-bit compare (the uniform-byte patterns read the same regardless of
175
+ * endianness). Everything else — newlines, CR, short/partial runs, and Unicode
176
+ * whitespace — falls to the tight byte loop, which also avoids the per-byte helper
177
+ * calls (fj_byte / fj_is_ws / fj_advance) the previous byte-at-a-time version paid.
178
+ * The set of bytes treated as whitespace is unchanged. */
171
179
  static void fj_skip_pure_ws(fj_state *st) {
180
+ const char *p = st->buf + st->pos;
181
+ const char *end = st->buf + st->len;
172
182
  for (;;) {
173
- int b = fj_byte(st);
174
- if (b == -1) break;
175
- if (fj_is_ws(b)) {
176
- fj_advance(st, 1);
177
- } else if (b >= 0x80) {
178
- long m = fj_mbws(st->buf + st->pos, st->len - st->pos);
179
- if (m == 0) break;
180
- st->pos += m;
181
- } else {
183
+ while (end - p >= 8) {
184
+ uint64_t w;
185
+ memcpy(&w, p, 8);
186
+ if (w == 0x2020202020202020ULL || w == 0x0909090909090909ULL) { p += 8; continue; }
182
187
  break;
183
188
  }
189
+ if (p >= end) break;
190
+ {
191
+ unsigned char b = (unsigned char)*p;
192
+ if (b == 0x20 || (b >= 0x09 && b <= 0x0D)) {
193
+ p++;
194
+ } else if (b >= 0x80) {
195
+ long m = fj_mbws(p, end - p);
196
+ if (m == 0) break;
197
+ p += m;
198
+ } else {
199
+ break;
200
+ }
201
+ }
184
202
  }
203
+ st->pos = p - st->buf;
185
204
  }
186
205
 
187
206
  /* A comment marker only starts a comment when preceded by whitespace or at the
@@ -228,6 +247,18 @@ static void fj_skip_ws_comments(fj_state *st) {
228
247
  }
229
248
  }
230
249
 
250
+ /* Cheap guard for the hot loop: could the current byte begin whitespace or a
251
+ * comment marker, so the (otherwise no-op) fj_skip_ws_comments call is actually
252
+ * needed? Compact data — the next byte is already a structural char or a value
253
+ * start — answers no, and we elide both the call and its memcpy/lookahead. ASCII
254
+ * whitespace, '#', '/', and possible multibyte-ws lead bytes (>=0x80) answer yes;
255
+ * EOF (-1) answers no (the caller's existing -1 checks handle it). Behaviour is
256
+ * identical to always calling fj_skip_ws_comments — this only skips a known no-op. */
257
+ static inline int fj_needs_ws_skip(int b) {
258
+ if (b < 0) return 0;
259
+ return b == 0x20 || (b >= 0x09 && b <= 0x0D) || b == '#' || b == '/' || b >= 0x80;
260
+ }
261
+
231
262
  /* forward declarations (mutual recursion) */
232
263
  static VALUE fj_parse_value(fj_state *st);
233
264
  static VALUE fj_parse_member_value(fj_state *st);
@@ -471,7 +502,7 @@ static VALUE fj_to_bigdecimal_token(const char *p, long n) {
471
502
  * (quoteless path) call these, so the Integer/Float a token produces is identical
472
503
  * no matter which path scanned it. [p, n) is the raw token slice (with any sign),
473
504
  * needed only by the bignum / strtod fallbacks. */
474
- static VALUE fj_int_from_parts(uint64_t m, int digits, int neg, int overflow, const char *p, long n) {
505
+ static FJ_ALWAYS_INLINE VALUE fj_int_from_parts(uint64_t m, int digits, int neg, int overflow, const char *p, long n) {
475
506
  if (!overflow && digits >= 1 && digits <= 18) {
476
507
  int64_t v = (int64_t)m;
477
508
  return LL2NUM(neg ? -v : v);
@@ -481,16 +512,87 @@ static VALUE fj_int_from_parts(uint64_t m, int digits, int neg, int overflow, co
481
512
  return rb_str_to_inum(fj_strip_underscores(p, n), 10, 0);
482
513
  }
483
514
 
515
+ /* Convert a >17-digit / subnormal float token to a double. A double resolves ~17
516
+ * significant decimals; the digits past that affect only the final round-to-nearest-
517
+ * even, which a single sticky marker ("was any dropped digit nonzero?") captures. So
518
+ * we keep FJ_FLOAT_ODD_DIGITS significant digits and, if more nonzero digits follow,
519
+ * force the last kept digit odd (round-to-odd). strtod's round-to-nearest of that
520
+ * shorter mantissa then equals round-to-nearest of the full value — but strtod grinds
521
+ * far fewer digits. The kept count is well above 2x double's ~16 significant decimals,
522
+ * which is what round-to-odd needs to be exact (verified bit-for-bit against
523
+ * JSON.parse on the high-precision corpus). The token is rebuilt into a NUL-terminated
524
+ * "<digits>e<exp>" buffer (passing the raw input slice would make rb_cstr_to_dbl treat
525
+ * the trailing delimiter as garbage and re-run strtod a second time). */
526
+ #define FJ_FLOAT_ODD_DIGITS 40
527
+ static VALUE fj_float_strtod(const char *p, long n) {
528
+ char digits[FJ_FLOAT_ODD_DIGITS];
529
+ char out[FJ_FLOAT_ODD_DIGITS + 40];
530
+ long i = 0, ow = 0, kept = 0, point_pos = 0, lead_frac_zeros = 0;
531
+ int neg = 0, after_point = 0, seen_sig = 0, sticky = 0, esign = 0;
532
+ int64_t expl_exp = 0, x;
533
+
534
+ if (i < n && (p[i] == '+' || p[i] == '-')) { neg = (p[i] == '-'); i++; }
535
+
536
+ for (; i < n; i++) {
537
+ char c = p[i];
538
+ if (c == '_') continue;
539
+ if (c == '.') { after_point = 1; continue; }
540
+ if (c == 'e' || c == 'E') { i++; break; }
541
+ if (!seen_sig && c == '0') { if (after_point) lead_frac_zeros++; continue; }
542
+ seen_sig = 1;
543
+ if (!after_point) point_pos++;
544
+ if (kept < FJ_FLOAT_ODD_DIGITS) digits[kept++] = c;
545
+ else if (c != '0') sticky = 1;
546
+ }
547
+
548
+ if (i < n && (p[i] == '+' || p[i] == '-')) { esign = (p[i] == '-'); i++; }
549
+ for (; i < n; i++) {
550
+ char c = p[i];
551
+ if (c == '_') continue;
552
+ if (c < '0' || c > '9') break;
553
+ expl_exp = expl_exp * 10 + (c - '0');
554
+ }
555
+ if (esign) expl_exp = -expl_exp;
556
+
557
+ if (kept == 0) return rb_float_new(neg ? -0.0 : 0.0);
558
+
559
+ /* round-to-odd: a dropped nonzero tail forces the last kept digit odd. */
560
+ if (sticky && ((digits[kept - 1] - '0') % 2) == 0) digits[kept - 1]++;
561
+
562
+ x = expl_exp + point_pos - lead_frac_zeros - kept;
563
+ if (neg) out[ow++] = '-';
564
+ memcpy(out + ow, digits, (size_t)kept);
565
+ ow += kept;
566
+ /* Append "e<exp>" by hand. snprintf here showed up as BSD_vfprintf in profiling —
567
+ a full printf formatter per number is absurdly heavy for one integer. */
568
+ out[ow++] = 'e';
569
+ if (x < 0) { out[ow++] = '-'; x = -x; }
570
+ {
571
+ char ex[24];
572
+ int en = 0;
573
+ if (x == 0) ex[en++] = '0';
574
+ else while (x > 0) { ex[en++] = (char)('0' + (int)(x % 10)); x /= 10; }
575
+ while (en > 0) out[ow++] = ex[--en];
576
+ }
577
+ out[ow] = '\0';
578
+ return rb_float_new(rb_cstr_to_dbl(out, 0));
579
+ }
580
+
484
581
  /* e10 is the final base-10 exponent (already adjusted by the fraction length). */
485
- static VALUE fj_float_from_parts(uint64_t m10, int m10digits, int64_t e10, int neg, int overflow, const char *p, long n) {
486
- /* Ryū fast path: <=17 mantissa digits and not in the subnormal range. */
487
- if (!overflow && m10digits >= 1 && m10digits <= 17 && (long)m10digits + e10 >= -307) {
582
+ static FJ_ALWAYS_INLINE VALUE fj_float_from_parts(uint64_t m10, int m10digits, int64_t e10, int neg, int overflow, const char *p, long n) {
583
+ /* Fast path by mantissa width (our scanner accumulates m10 exactly up to 18
584
+ digits, flagging overflow beyond):
585
+ 1..18 digits -> Eisel-Lemire, correctly-rounded for any exact uint64 mantissa
586
+ (Mushtak-Lemire). This pulls full-double-precision data (e.g.
587
+ citylots coordinates, 18 sig digits) off the slow strtod
588
+ fallback — the stdlib json gem still strtods it.
589
+ >18 digits / overflow / extreme exponent -> strtod (round-to-odd). */
590
+ if (!overflow && m10digits >= 1 && m10digits <= 18 && (long)m10digits + e10 >= -307) {
488
591
  if (m10 == 0) return rb_float_new(neg ? -0.0 : 0.0);
489
- return rb_float_new(ryu_s2d_from_parts(m10, m10digits, (int32_t)e10, neg != 0));
592
+ return rb_float_new(fj_eisel_lemire_s2d(e10, m10, neg));
490
593
  }
491
- /* Fallback for >17 digits / extreme or subnormal exponents. */
492
- if (memchr(p, '_', (size_t)n) == NULL) return rb_float_new(rb_cstr_to_dbl(p, 0));
493
- return rb_float_new(rb_str_to_dbl(fj_strip_underscores(p, n), 0));
594
+ /* Fallback for >18 digits / extreme or subnormal exponents. */
595
+ return fj_float_strtod(p, n);
494
596
  }
495
597
 
496
598
  /* Scan an already-bounded quoteless token [p, p+n) exactly once: validate it as a
@@ -571,8 +673,8 @@ static int fj_try_decimal(fj_state *st, const char *p, long n, VALUE *out) {
571
673
  e10 -= frac;
572
674
  /* :bigdecimal always; :auto only when significant digits > 16. m10digits is >=
573
675
  * the significant-digit count, so m10digits <= 16 skips the fj_sig_digits scan. */
574
- if (st->bigdecimal_load == 2 ||
575
- (st->bigdecimal_load == 1 && m10digits > 16 && fj_sig_digits(p, n) > 16)) {
676
+ if (st->decimal_precision == 2 ||
677
+ (st->decimal_precision == 1 && m10digits > 16 && fj_sig_digits(p, n) > 16)) {
576
678
  *out = fj_to_bigdecimal_token(p, n);
577
679
  } else {
578
680
  *out = fj_float_from_parts(m10, m10digits, e10, neg, overflow, p, n);
@@ -596,7 +698,7 @@ static VALUE fj_parse_number(fj_state *st) {
596
698
  long nlen;
597
699
  int is_float = 0, neg = 0, overflow = 0;
598
700
  uint64_t m10 = 0; /* mantissa: integer + fraction digits */
599
- int m10digits = 0; /* mantissa digit chars (caps the Ryū fast path at 17) */
701
+ int m10digits = 0; /* mantissa digit chars (caps the Eisel-Lemire fast path at 18) */
600
702
  int frac = 0; /* fraction digit chars: e10 -= frac */
601
703
  int64_t e10 = 0;
602
704
 
@@ -683,8 +785,8 @@ static VALUE fj_parse_number(fj_state *st) {
683
785
  * when significant digits > 16. Since m10digits >= significant digits, m10digits
684
786
  * <= 16 guarantees not-BigDecimal and lets us skip the fj_sig_digits scan
685
787
  * entirely (the common case — e.g. every coordinate in canada.json). */
686
- if (st->bigdecimal_load == 2 ||
687
- (st->bigdecimal_load == 1 && m10digits > 16 && fj_sig_digits(np, nlen) > 16)) {
788
+ if (st->decimal_precision == 2 ||
789
+ (st->decimal_precision == 1 && m10digits > 16 && fj_sig_digits(np, nlen) > 16)) {
688
790
  return fj_to_bigdecimal_token(np, nlen);
689
791
  }
690
792
  return fj_float_from_parts(m10, m10digits, e10, neg, overflow, np, nlen);
@@ -851,7 +953,8 @@ static VALUE fj_classify_quoteless(fj_state *st, const char *p0, long n0) {
851
953
  * before the whitespace check. */
852
954
  enum { FJ_QL_ORD = 0, FJ_QL_TERM, FJ_QL_WS, FJ_QL_CMT };
853
955
  static const unsigned char fj_ql_class[256] = {
854
- [','] = FJ_QL_TERM, ['}'] = FJ_QL_TERM, [']'] = FJ_QL_TERM,
956
+ [','] = FJ_QL_TERM, ['{'] = FJ_QL_TERM, ['}'] = FJ_QL_TERM,
957
+ ['['] = FJ_QL_TERM, [']'] = FJ_QL_TERM,
855
958
  [0x0A] = FJ_QL_TERM, [0x0D] = FJ_QL_TERM,
856
959
  [0x09] = FJ_QL_WS, [0x0B] = FJ_QL_WS, [0x0C] = FJ_QL_WS, [' '] = FJ_QL_WS,
857
960
  ['#'] = FJ_QL_CMT, ['/'] = FJ_QL_CMT,
@@ -1078,7 +1181,8 @@ static int fj_try_member_number(fj_state *st, VALUE *out) {
1078
1181
  /* Commit only if the number abuts a value terminator; otherwise (whitespace,
1079
1182
  * letters, a second '.', "0x…", …) leave it to the quoteless scanner. */
1080
1183
  t = (unsigned char)*p;
1081
- if (!(t == ',' || t == '}' || t == ']' || t == 0x0A || t == 0x0D || p == buf + st->len)) {
1184
+ if (!(t == ',' || t == '{' || t == '}' || t == '[' || t == ']' ||
1185
+ t == 0x0A || t == 0x0D || p == buf + st->len)) {
1082
1186
  return 0;
1083
1187
  }
1084
1188
 
@@ -1089,8 +1193,8 @@ static int fj_try_member_number(fj_state *st, VALUE *out) {
1089
1193
  return 1;
1090
1194
  }
1091
1195
  e10 -= frac;
1092
- if (st->bigdecimal_load == 2 ||
1093
- (st->bigdecimal_load == 1 && m10digits > 16 && fj_sig_digits(np, nlen) > 16)) {
1196
+ if (st->decimal_precision == 2 ||
1197
+ (st->decimal_precision == 1 && m10digits > 16 && fj_sig_digits(np, nlen) > 16)) {
1094
1198
  *out = fj_to_bigdecimal_token(np, nlen);
1095
1199
  } else {
1096
1200
  *out = fj_float_from_parts(m10, m10digits, e10, neg, overflow, np, nlen);
@@ -1164,19 +1268,9 @@ static void fj_hash_bulk_insert(long count, const VALUE *pairs, VALUE hash) {
1164
1268
  void rb_hash_bulk_insert(long, const VALUE *, VALUE);
1165
1269
  #endif
1166
1270
 
1167
- /* Hash entry count as a C long. RHASH_SIZE is not part of the public C API on
1168
- * older Ruby (< ~2.7), but rb_hash_size (Hash#size's implementation) is available
1169
- * everywhere. Only used on the rare :raise duplicate-key path, so the boxing cost
1170
- * is irrelevant — and it keeps the extension buildable down to Ruby 2.5. */
1171
- static inline long fj_hash_len(VALUE hash) {
1172
- return NUM2LONG(rb_hash_size(hash));
1173
- }
1174
-
1175
1271
  /* Build a Hash from `count` interleaved key,value slots. Fast path (String keys,
1176
- * default :last_wins or :raise): pre-size + bulk insert, detecting duplicates by
1177
- * comparing the resulting size to the pair count free unless a collision
1178
- * actually happened. symbolize_keys / :first_wins use a per-member loop into the
1179
- * same pre-sized hash. */
1272
+ * default :last_wins): pre-size + bulk insert. symbolize_keys / :first_wins use a
1273
+ * per-member loop into the same pre-sized hash. */
1180
1274
  static VALUE fj_build_object(fj_state *st, const VALUE *pairs, long count) {
1181
1275
  long entries = count / 2, i;
1182
1276
  VALUE hash = rb_hash_new_capa(entries);
@@ -1185,22 +1279,13 @@ static VALUE fj_build_object(fj_state *st, const VALUE *pairs, long count) {
1185
1279
  * the per-member loop below to report each dropped duplicate key. */
1186
1280
  if (!st->symbolize_keys && !st->dup_first_wins && st->on_warning == Qnil) {
1187
1281
  rb_hash_bulk_insert(count, pairs, hash);
1188
- if (st->dup_raise && fj_hash_len(hash) < entries) {
1189
- VALUE seen = rb_hash_new_capa(entries);
1190
- for (i = 0; i + 1 < count; i += 2) {
1191
- long before = fj_hash_len(seen);
1192
- rb_hash_aset(seen, pairs[i], Qtrue);
1193
- if (fj_hash_len(seen) == before) fj_error(st, "duplicate key");
1194
- }
1195
- }
1196
1282
  return hash;
1197
1283
  }
1198
1284
 
1199
1285
  for (i = 0; i + 1 < count; i += 2) {
1200
1286
  VALUE k = st->symbolize_keys ? rb_funcall(pairs[i], fj_to_sym_id, 0) : pairs[i];
1201
- if (st->dup_first_wins || st->dup_raise || st->on_warning != Qnil) {
1287
+ if (st->dup_first_wins || st->on_warning != Qnil) {
1202
1288
  if (RTEST(rb_funcall(hash, fj_key_p_id, 1, k))) {
1203
- if (st->dup_raise) fj_error(st, "duplicate key");
1204
1289
  fj_warn(st, fj_sym_duplicate_key, "duplicate key");
1205
1290
  if (st->dup_first_wins) continue;
1206
1291
  }
@@ -1274,11 +1359,14 @@ static VALUE fj_parse_iter(fj_state *st, int implicit_root) {
1274
1359
  int is_obj;
1275
1360
 
1276
1361
  if (ps->fhead == 0) { /* top level: parse exactly one value */
1277
- fj_skip_ws_comments(st);
1278
1362
  b = fj_byte(st);
1363
+ if (FJ_UNLIKELY(fj_needs_ws_skip(b))) { fj_skip_ws_comments(st); b = fj_byte(st); }
1279
1364
  if (b == '{') { fj_advance(st, 1); fj_fpush(ps, ps->vhead, 1); vss = 0; continue; }
1280
1365
  if (b == '[') { fj_advance(st, 1); fj_fpush(ps, ps->vhead, 0); vss = 0; continue; }
1281
1366
  if (b == -1) fj_error(st, "unexpected end of input");
1367
+ /* Top-level scalar: must be a recognized JSON value (number / literal / quoted
1368
+ * string). A bare word raises — no top-level quoteless strings (B-broad). The
1369
+ * scalar-vs-separator boundary is enforced in fj_parse_c. */
1282
1370
  result = fj_parse_value(st);
1283
1371
  break;
1284
1372
  }
@@ -1288,8 +1376,8 @@ static VALUE fj_parse_iter(fj_state *st, int implicit_root) {
1288
1376
 
1289
1377
  if (is_obj) {
1290
1378
  VALUE key;
1291
- fj_skip_ws_comments(st);
1292
1379
  b = fj_byte(st);
1380
+ if (FJ_UNLIKELY(fj_needs_ws_skip(b))) { fj_skip_ws_comments(st); b = fj_byte(st); }
1293
1381
  if (b == ',') { /* collapsing separator: skip empty member */
1294
1382
  if (st->on_warning != Qnil && !vss) fj_warn(st, fj_sym_empty_slot, "extra comma, collapsed an empty slot");
1295
1383
  vss = 0;
@@ -1316,11 +1404,12 @@ static VALUE fj_parse_iter(fj_state *st, int implicit_root) {
1316
1404
  }
1317
1405
  if (b == ']') fj_error(st, "unexpected ']' — expected a key or '}'");
1318
1406
  key = fj_parse_object_key(st);
1319
- fj_skip_ws_comments(st);
1320
- if (fj_byte(st) != ':') fj_error(st, "expected ':' after object key");
1407
+ b = fj_byte(st);
1408
+ if (FJ_UNLIKELY(fj_needs_ws_skip(b))) { fj_skip_ws_comments(st); b = fj_byte(st); }
1409
+ if (b != ':') fj_error(st, "expected ':' after object key");
1321
1410
  fj_advance(st, 1);
1322
- fj_skip_ws_comments(st);
1323
1411
  b = fj_byte(st);
1412
+ if (FJ_UNLIKELY(fj_needs_ws_skip(b))) { fj_skip_ws_comments(st); b = fj_byte(st); }
1324
1413
  if (b == '{' || b == '[') {
1325
1414
  fj_vpush(ps, key);
1326
1415
  fj_advance(st, 1);
@@ -1340,8 +1429,8 @@ static VALUE fj_parse_iter(fj_state *st, int implicit_root) {
1340
1429
  fj_vpush(ps, fj_parse_member_value(st));
1341
1430
  vss = 1;
1342
1431
  } else { /* array */
1343
- fj_skip_ws_comments(st);
1344
1432
  b = fj_byte(st);
1433
+ if (FJ_UNLIKELY(fj_needs_ws_skip(b))) { fj_skip_ws_comments(st); b = fj_byte(st); }
1345
1434
  if (b == ',') { /* collapsing separator: skip empty slot */
1346
1435
  if (st->on_warning != Qnil && !vss) fj_warn(st, fj_sym_empty_slot, "extra comma, collapsed an empty slot");
1347
1436
  vss = 0;
@@ -1367,6 +1456,15 @@ static VALUE fj_parse_iter(fj_state *st, int implicit_root) {
1367
1456
  vss = 0;
1368
1457
  continue;
1369
1458
  }
1459
+ /* Strict hot path: inline the two commonest element types — a number and a
1460
+ plain double-quoted string — so they skip fj_parse_member_value's byte
1461
+ re-read + switch. Everything else (quoteless, single/triple-quote,
1462
+ smart-quote, literals) falls through to the full dispatch below. */
1463
+ if (b == '-' || b == '+' || b == '.' || (b >= '0' && b <= '9')) {
1464
+ VALUE num;
1465
+ if (fj_try_member_number(st, &num)) { fj_vpush(ps, num); vss = 1; continue; }
1466
+ }
1467
+ if (b == '"') { fj_vpush(ps, fj_parse_string(st, '"')); vss = 1; continue; }
1370
1468
  fj_vpush(ps, fj_parse_member_value(st));
1371
1469
  vss = 1;
1372
1470
  }
@@ -1391,9 +1489,46 @@ static int fj_implicit_root_ahead(fj_state *st) {
1391
1489
  return result;
1392
1490
  }
1393
1491
 
1492
+ /* Between top-level documents, whitespace, comments, AND commas all separate
1493
+ * (commas collapse like the in-container lenient-comma rule). A space alone never
1494
+ * separates — that is handled inside the document by the quoteless run. Mirrors
1495
+ * the Ruby Parser#skip_document_separators. */
1496
+ static void fj_skip_document_separators(fj_state *st) {
1497
+ for (;;) {
1498
+ fj_skip_ws_comments(st);
1499
+ if (fj_byte(st) != ',') break;
1500
+ fj_advance(st, 1);
1501
+ }
1502
+ }
1503
+
1504
+ static int fj_is_hws(int b) { return b == ' ' || b == '\t' || b == 0x0B || b == 0x0C; }
1505
+
1506
+ /* After a top-level value: a self-delimiting value (object / array / string) may be
1507
+ * followed by anything, but a bare scalar (number / keyword) must be followed by a
1508
+ * real separator — a newline, ',', a comment, or EOF. A space is NOT a separator, so
1509
+ * `1 2 3` and `42 "x" true` raise. Mirrors the Ruby Parser#enforce_scalar_boundary. */
1510
+ static void fj_enforce_scalar_boundary(fj_state *st, VALUE value) {
1511
+ int b, nx;
1512
+ if (RB_TYPE_P(value, T_STRING) || RB_TYPE_P(value, T_HASH) || RB_TYPE_P(value, T_ARRAY)) return;
1513
+ for (;;) {
1514
+ b = fj_byte(st);
1515
+ if (b != -1 && fj_is_hws(b)) { fj_advance(st, 1); continue; }
1516
+ if (b != -1 && b >= 0x80) {
1517
+ long m = fj_mbws(st->buf + st->pos, st->len - st->pos);
1518
+ if (m > 0) { st->pos += m; continue; } /* multibyte horizontal whitespace (NBSP, …) */
1519
+ }
1520
+ break;
1521
+ }
1522
+ b = fj_byte(st);
1523
+ if (b == -1 || b == 0x0A || b == 0x0D || b == ',') return;
1524
+ if (b == '#') return;
1525
+ if (b == '/') { nx = fj_byte_at(st, 1); if (nx == '/' || nx == '*') return; }
1526
+ fj_error(st, "a top-level number or keyword must be followed by a newline, ',', or end of input");
1527
+ }
1528
+
1394
1529
  static VALUE fj_parse_c(VALUE self, VALUE input, VALUE opts) {
1395
1530
  fj_state st;
1396
- VALUE value, enc_opt, dk;
1531
+ VALUE enc_opt, dk;
1397
1532
 
1398
1533
  Check_Type(input, T_STRING);
1399
1534
 
@@ -1423,13 +1558,12 @@ static VALUE fj_parse_c(VALUE self, VALUE input, VALUE opts) {
1423
1558
  st.symbolize_keys = RTEST(rb_hash_aref(opts, fj_sym_symbolize_keys));
1424
1559
  dk = rb_hash_aref(opts, fj_sym_duplicate_key);
1425
1560
  st.dup_first_wins = (dk == fj_sym_first_wins);
1426
- st.dup_raise = (dk == fj_sym_raise);
1427
1561
 
1428
1562
  {
1429
- VALUE bd = rb_hash_aref(opts, fj_sym_bigdecimal_load);
1430
- if (bd == fj_sym_float) st.bigdecimal_load = 0;
1431
- else if (bd == fj_sym_bigdecimal) st.bigdecimal_load = 2;
1432
- else st.bigdecimal_load = 1; /* :auto (default), including nil */
1563
+ VALUE bd = rb_hash_aref(opts, fj_sym_decimal_precision);
1564
+ if (bd == fj_sym_float) st.decimal_precision = 0;
1565
+ else if (bd == fj_sym_bigdecimal) st.decimal_precision = 2;
1566
+ else st.decimal_precision = 1; /* :auto (default), including nil */
1433
1567
  }
1434
1568
 
1435
1569
  st.on_warning = rb_hash_aref(opts, fj_sym_on_warning); /* Qnil when absent */
@@ -1439,36 +1573,37 @@ static VALUE fj_parse_c(VALUE self, VALUE input, VALUE opts) {
1439
1573
  st.pos = 3;
1440
1574
  }
1441
1575
 
1442
- /* With a block: yield each top-level value until EOF (JSONL / NDJSON /
1443
- * concatenated). Same loop as the Ruby each_value path, on the C parser. */
1576
+ /* With a block: yield each top-level document until EOF and return the document
1577
+ * count (NDJSON / JSONL / concatenated). Same loop as the Ruby each_value path. */
1444
1578
  if (rb_block_given_p()) {
1579
+ long count = 0;
1445
1580
  for (;;) {
1446
- fj_skip_ws_comments(&st);
1581
+ VALUE v;
1582
+ fj_skip_document_separators(&st);
1447
1583
  if (fj_eof(&st)) break;
1448
- rb_yield(fj_parse_iter(&st, fj_implicit_root_ahead(&st)));
1584
+ v = fj_parse_iter(&st, fj_implicit_root_ahead(&st));
1585
+ fj_enforce_scalar_boundary(&st, v);
1586
+ rb_yield(v);
1587
+ count++;
1449
1588
  }
1450
- return Qnil;
1589
+ return LONG2NUM(count);
1451
1590
  }
1452
1591
 
1453
- /* No block: auto-detect the document count for free it is the same "is there
1454
- * trailing content after the first value?" check that used to raise. 0 documents
1455
- * -> nil; 1 document -> the value itself (single-document hot path, no Array
1456
- * allocated); 2+ documents (NDJSON / JSONL / concatenated / whitespace-separated)
1457
- * -> an Array of every top-level value. Commas do NOT separate documents (only
1458
- * whitespace / newline / concatenation do), so a bracketless comma list still
1459
- * raises in fj_parse_iter — the unsupported implicit-root array. */
1460
- fj_skip_ws_comments(&st);
1461
- if (fj_eof(&st)) return Qnil;
1462
- value = fj_parse_iter(&st, fj_implicit_root_ahead(&st));
1463
- fj_skip_ws_comments(&st);
1464
- if (fj_eof(&st)) return value;
1592
+ /* No block: always return an Array of every top-level document (0 -> [], 1 ->
1593
+ * [doc], 2+ -> [d1, d2, …]) the always-array contract. Documents are separated by
1594
+ * newline / comma / concatenation (self-delimiting values); a space alone never
1595
+ * separates, and a bare scalar must be followed by a real separator, so `1 2 3`
1596
+ * raises while `1\n2\n3` and `1, 2, 3` are three documents. */
1465
1597
  {
1466
1598
  VALUE arr = rb_ary_new();
1467
- rb_ary_push(arr, value);
1468
- do {
1469
- rb_ary_push(arr, fj_parse_iter(&st, fj_implicit_root_ahead(&st)));
1470
- fj_skip_ws_comments(&st);
1471
- } while (!fj_eof(&st));
1599
+ for (;;) {
1600
+ VALUE v;
1601
+ fj_skip_document_separators(&st);
1602
+ if (fj_eof(&st)) break;
1603
+ v = fj_parse_iter(&st, fj_implicit_root_ahead(&st));
1604
+ fj_enforce_scalar_boundary(&st, v);
1605
+ rb_ary_push(arr, v);
1606
+ }
1472
1607
  return arr;
1473
1608
  }
1474
1609
  }
@@ -1493,8 +1628,7 @@ void Init_smarter_json(void) {
1493
1628
  fj_sym_encoding = ID2SYM(rb_intern("encoding"));
1494
1629
  fj_sym_symbolize_keys = ID2SYM(rb_intern("symbolize_keys"));
1495
1630
  fj_sym_first_wins = ID2SYM(rb_intern("first_wins"));
1496
- fj_sym_raise = ID2SYM(rb_intern("raise"));
1497
- fj_sym_bigdecimal_load = ID2SYM(rb_intern("bigdecimal_load"));
1631
+ fj_sym_decimal_precision = ID2SYM(rb_intern("decimal_precision"));
1498
1632
  fj_sym_float = ID2SYM(rb_intern("float"));
1499
1633
  fj_sym_bigdecimal = ID2SYM(rb_intern("bigdecimal"));
1500
1634
  fj_sym_on_warning = ID2SYM(rb_intern("on_warning"));
@@ -0,0 +1,27 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 The fast_float authors
4
+
5
+ Permission is hereby granted, free of charge, to any
6
+ person obtaining a copy of this software and associated
7
+ documentation files (the "Software"), to deal in the
8
+ Software without restriction, including without
9
+ limitation the rights to use, copy, modify, merge,
10
+ publish, distribute, sublicense, and/or sell copies of
11
+ the Software, and to permit persons to whom the Software
12
+ is furnished to do so, subject to the following
13
+ conditions:
14
+
15
+ The above copyright notice and this permission notice
16
+ shall be included in all copies or substantial portions
17
+ of the Software.
18
+
19
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
20
+ ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
21
+ TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
22
+ PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
23
+ SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
24
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
25
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
26
+ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
27
+ DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,117 @@
1
+ /* Eisel-Lemire decimal->double, ported from fast_float:
2
+ * include/fast_float/decimal_to_binary.h, the compute_float<binary_format<double>>
3
+ * + compute_product_approximation routines.
4
+ *
5
+ * Algorithm authors: Michael Eisel (original approach) and Daniel Lemire
6
+ * (formalization, proof, and the fast_float implementation) — hence "Eisel-Lemire".
7
+ *
8
+ * Copyright (c) 2021 The fast_float authors. Tri-licensed Apache-2.0 / MIT / BSL-1.0;
9
+ * used here under MIT — see LICENSE-fast_float-MIT in this directory.
10
+ *
11
+ * This is the "without fallback" variant (Noble Mushtak & Daniel Lemire, "Fast
12
+ * Number Parsing Without Fallback"): for ANY nonzero mantissa w that fits exactly
13
+ * in a uint64 (i.e. <= 19 significant digits, not truncated) and decimal exponent
14
+ * q, it returns the correctly-rounded binary64 with no slow-path needed.
15
+ *
16
+ * smarter_json uses it as THE decimal->double path for mantissas up to 18 digits
17
+ * (everything wider / overflowed / with an extreme exponent goes to the strtod
18
+ * round-to-odd fallback). It is correctly-rounded across that whole range, with no
19
+ * round-to-even tie loss, and is fast on the common short-mantissa case.
20
+ * Verified bit-for-bit vs JSON.parse. See eisel_lemire.md for provenance. */
21
+ #ifndef FJ_EISEL_LEMIRE_H
22
+ #define FJ_EISEL_LEMIRE_H
23
+
24
+ #include <stdint.h>
25
+ #include <string.h>
26
+ #include "eisel_lemire_powers.h"
27
+
28
+ /* binary_format<double> constants from fast_float. */
29
+ #define FJ_EL_MANTISSA_BITS 52
30
+ #define FJ_EL_MIN_EXPONENT (-1023)
31
+ #define FJ_EL_INFINITE_POWER 0x7FF
32
+ #define FJ_EL_SMALLEST_POW10 (-342)
33
+ #define FJ_EL_LARGEST_POW10 308
34
+ #define FJ_EL_MIN_RTE (-4) /* min_exponent_round_to_even */
35
+ #define FJ_EL_MAX_RTE 23 /* max_exponent_round_to_even */
36
+
37
+ /* (((152170 + 65536) * q) >> 16) + 63 == floor(log2(10^q)) + q + 63, see paper. */
38
+ static inline int32_t fj_el_power(int32_t q) {
39
+ return (((152170 + 65536) * q) >> 16) + 63;
40
+ }
41
+
42
+ static inline void fj_el_mul128(uint64_t a, uint64_t b, uint64_t *hi, uint64_t *lo) {
43
+ #if defined(__SIZEOF_INT128__)
44
+ __uint128_t p = (__uint128_t)a * (__uint128_t)b;
45
+ *lo = (uint64_t)p;
46
+ *hi = (uint64_t)(p >> 64);
47
+ #else
48
+ uint64_t a0 = (uint32_t)a, a1 = a >> 32, b0 = (uint32_t)b, b1 = b >> 32;
49
+ uint64_t p00 = a0 * b0, p01 = a0 * b1, p10 = a1 * b0, p11 = a1 * b1;
50
+ uint64_t mid = p10 + (p00 >> 32) + (uint32_t)p01;
51
+ *hi = p11 + (mid >> 32) + (p01 >> 32);
52
+ *lo = (mid << 32) | (uint32_t)p00;
53
+ #endif
54
+ }
55
+
56
+ static inline double fj_el_bits2double(uint64_t bits) {
57
+ double d;
58
+ memcpy(&d, &bits, sizeof(d));
59
+ return d;
60
+ }
61
+
62
+ /* q = power of ten, w = mantissa (NONZERO, exact, fits in uint64). neg = sign. */
63
+ static inline double fj_eisel_lemire_s2d(int64_t q, uint64_t w, int neg) {
64
+ const uint64_t sign = (uint64_t)(neg != 0) << 63;
65
+ uint64_t mantissa, prod_hi, prod_lo, sp_hi, sp_lo;
66
+ int32_t power2;
67
+ int lz, upperbit, shift, index;
68
+
69
+ if (q < FJ_EL_SMALLEST_POW10) return fj_el_bits2double(sign); /* underflow -> 0 */
70
+ if (q > FJ_EL_LARGEST_POW10)
71
+ return fj_el_bits2double(sign | ((uint64_t)FJ_EL_INFINITE_POWER << FJ_EL_MANTISSA_BITS));
72
+
73
+ lz = __builtin_clzll(w);
74
+ w <<= lz;
75
+
76
+ /* compute_product_approximation<mantissa_bits + 3 = 55>: precision_mask = 0x1FF. */
77
+ index = 2 * (int)(q - FJ_EL_SMALLEST_POWER_OF_FIVE);
78
+ fj_el_mul128(w, fj_power_of_five_128[index], &prod_hi, &prod_lo);
79
+ if ((prod_hi & 0x1FF) == 0x1FF) {
80
+ fj_el_mul128(w, fj_power_of_five_128[index + 1], &sp_hi, &sp_lo);
81
+ prod_lo += sp_hi;
82
+ if (sp_hi > prod_lo) prod_hi++;
83
+ }
84
+
85
+ upperbit = (int)(prod_hi >> 63);
86
+ shift = upperbit + 64 - FJ_EL_MANTISSA_BITS - 3; /* upperbit + 9 */
87
+ mantissa = prod_hi >> shift;
88
+ power2 = (int32_t)(fj_el_power((int32_t)q) + upperbit - lz - FJ_EL_MIN_EXPONENT);
89
+
90
+ if (power2 <= 0) { /* subnormal */
91
+ if (-power2 + 1 >= 64) return fj_el_bits2double(sign); /* far below min -> 0 */
92
+ mantissa >>= (-power2 + 1);
93
+ mantissa += (mantissa & 1);
94
+ mantissa >>= 1;
95
+ power2 = (mantissa < ((uint64_t)1 << FJ_EL_MANTISSA_BITS)) ? 0 : 1;
96
+ return fj_el_bits2double(sign | ((uint64_t)power2 << FJ_EL_MANTISSA_BITS) | mantissa);
97
+ }
98
+
99
+ /* round-to-even: if we land exactly between two doubles, round down. */
100
+ if ((prod_lo <= 1) && (q >= FJ_EL_MIN_RTE) && (q <= FJ_EL_MAX_RTE) &&
101
+ ((mantissa & 3) == 1) && ((mantissa << shift) == prod_hi)) {
102
+ mantissa &= ~(uint64_t)1;
103
+ }
104
+
105
+ mantissa += (mantissa & 1);
106
+ mantissa >>= 1;
107
+ if (mantissa >= ((uint64_t)2 << FJ_EL_MANTISSA_BITS)) {
108
+ mantissa = (uint64_t)1 << FJ_EL_MANTISSA_BITS;
109
+ power2++;
110
+ }
111
+ mantissa &= ~((uint64_t)1 << FJ_EL_MANTISSA_BITS); /* drop implicit bit */
112
+ if (power2 >= FJ_EL_INFINITE_POWER)
113
+ return fj_el_bits2double(sign | ((uint64_t)FJ_EL_INFINITE_POWER << FJ_EL_MANTISSA_BITS));
114
+ return fj_el_bits2double(sign | ((uint64_t)power2 << FJ_EL_MANTISSA_BITS) | mantissa);
115
+ }
116
+
117
+ #endif