yeptris 0.1.12.0 → 0.1.13.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9fa74d905a2d19dbeaf70381760c3a629738808e507d0ef9f33c9e266b756854
4
- data.tar.gz: 2d3fb1c11b0d2dbb88c5bb75e837f41abe971992c1940a8c5be191b0168411b8
3
+ metadata.gz: 17b1311c9327a9986b7bb89f3731fd40ca040c49336fdb97596951fbf87ed1c0
4
+ data.tar.gz: 6c1b2f707d15d469963ffc0ee030fc90287a5e01954d7431996f24765bddf9e3
5
5
  SHA512:
6
- metadata.gz: e621ced9cf2707ada102a8633f7f75f597f63488f2a66da4efdb0dd3e292ef0719bbba509db321ca9e5cfd38f455a732a90cb3d1709b79f4ae7cab5ef0dd48da
7
- data.tar.gz: 9d5efd07ce2e2f498320133776e3b0ed08e1fec4977a7e5cd1b607616630377bc29f7d5708e00c058c2f7d97022a6b668d6e3865c33da17a7d9c8ba71d445f2e
6
+ metadata.gz: b04ca9a9d899c00207d8cf09ab1d1aa071c1cc63f2adab17cf87453d0a0646fc84c9df9f3e80f245bbc977a32e67feb7f25e2b562293a59adc102764b80507dc
7
+ data.tar.gz: 25c9033573e619dc69924f4ae15c102b650efdbf7d39c1dcc4d3d73243d1d0ac3037750c7b28b5e6d28562175296fd4b125e39253f4ab9c9ed799529d7fcb4ee
data/README.adoc CHANGED
@@ -52,9 +52,47 @@ Handles are document-scoped: `Document#free` releases everything
52
52
  (one C call), a GC finalizer backs it up, and any use after free
53
53
  raises `Yeptris::FreedError` — never a segfault.
54
54
 
55
- == Native materializer (opt-in): faster than JSON.parse
55
+ == Two load surfaces: YAML and strict JSON
56
+
57
+ `Yeptris::YAML.load` keeps the Psych contract for EVERY input —
58
+ including JSON-shaped ones. `{"a": [1,]}` is legal flow YAML (spec
59
+ production [141] allows the trailing comma); `"1e3"` is a Psych
60
+ String. These semantics never flip because input happens to look
61
+ like JSON.
62
+
63
+ `Yeptris::JSON.load` is the STRICT RFC 8259 surface — exact
64
+ `JSON.parse` semantics by construction (spec-pinned in
65
+ `spec/json_parity_spec.rb`: every value and every error case,
66
+ Bignum integers, exponent-only floats, duplicate keys). Engines,
67
+ fastest first:
68
+
69
+ 1. **Native materializer** (opt-in build; see below): a fused C scan
70
+ → `VALUE` parser. Order-alternating interleaved profile, 152 KB /
71
+ ~29k-value corpus, Ruby 3.4:
72
+ +
73
+ ....
74
+ CI referee (gated at 1.05 on both platforms, order-alternating
75
+ interleave, N=200):
76
+ ubuntu (x86_64, gc=disable) mean 0.961x vs JSON.parse
77
+ macos (arm64, gc=none) mean 0.718x (head-to-head 173/200)
78
+ ....
79
+
80
+ The parse-window GC strategy is per-arch and runtime-switchable:
81
+ `Yeptris::Native.gc_mode` / `YEPTRIS_NATIVE_GC` = `:none`
82
+ (steady-state: +0 heap pages, the stdlib's own GC cadence — the
83
+ default on arm64) or `:disable` (no GC during the parse — the
84
+ default on x86_64, faster where pages are free). On a LOADED x86_64
85
+ box prefer `:none`: `:disable` grows ~478 heap pages per 50 parses,
86
+ and page growth under contention is exactly the loaded-box
87
+ regression. `:start` (in-window collection) measured 8x — dead.
88
+ 2. **Record-drain fallback** (always available): the strict-JSON
89
+ validator gates, then the value records convert without the Psych
90
+ quirk table — exact parity with engine 1, spec-pinned.
91
+
92
+ The committed profile (`benchmark/json_profile.rb`) is the fair
93
+ benchmark of record — any performance claim runs through it.
56
94
 
57
- For JSON-shaped input, the binding can beat Ruby's own `JSON.parse`:
95
+ === Building the native materializer (opt-in)
58
96
 
59
97
  ....
60
98
  cd ext/yeptris_native
@@ -62,22 +100,9 @@ YEPTRIS_LIB_PATH=/path/to/libyeptris.dylib ruby extconf.rb && make
62
100
  cp native.bundle ../../lib/yeptris/ # or .so on Linux
63
101
  ....
64
102
 
65
- `require "yeptris"` picks it up automatically (a missing build falls
66
- back to the FFI Marshal ladder silently). Measured on the 152 KB /
67
- 29.4k-value JSON corpus (mean of 300, Ruby 3.4):
68
-
69
- ....
70
- JSON.parse mean 1.597 ms
71
- Yeptris::YAML.load mean 1.145 ms (0.72x — faster than JSON.parse)
72
- Psych.load mean 40.60 ms (35x slower than yeptris)
73
- ....
74
-
75
- The extension is a fused RFC 8259 → `VALUE` parser (one pass, no
76
- intermediate records): libyeptris scan kernels tokenize, the Ruby C
77
- API allocates, repeated keys/tokens share one frozen `String`, and GC
78
- is paused for the duration. YAML inputs keep the FFI ladder so
79
- timestamps, aliases, and Psych's scalar quirks resolve through the
80
- same path `Psych.load` uses.
103
+ `require "yeptris"` picks it up automatically (`Yeptris::JSON` then
104
+ uses it; missing builds silently use the fallback the gem installs
105
+ without compiling).
81
106
 
82
107
  == Shipped beyond the original plan
83
108
 
@@ -2,6 +2,9 @@
2
2
 
3
3
  require "mkmf"
4
4
 
5
+ # libyeptris location: YEPTRIS_LIB_PATH (file or dir), then sibling
6
+ # checkouts. CI sets YEPTRIS_LIB_PATH (the built shared library) and
7
+ # YEPTRIS_SRC (the C checkout) explicitly.
5
8
  lib_path = ENV["YEPTRIS_LIB_PATH"]
6
9
  candidates = []
7
10
  if lib_path
@@ -13,11 +16,12 @@ candidates << File.expand_path("../../../../yeptris/build/src", __dir__)
13
16
  candidates << File.expand_path("../../../yeptris/build-validate/src", __dir__)
14
17
  candidates << File.expand_path("../../../yeptris/build/src", __dir__)
15
18
 
16
- src_root = [
17
- File.expand_path("../../../../yeptris/src", __dir__),
18
- File.expand_path("../../../yeptris/src", __dir__),
19
- ].find { |d| d && File.directory?(File.join(d, "include")) }
20
- abort "yeptris sources not found" unless src_root
19
+ # Source root: YEPTRIS_SRC (CI / explicit), then sibling checkouts.
20
+ src_roots = [ENV["YEPTRIS_SRC"]].compact
21
+ src_roots << File.expand_path("../../../../yeptris/src", __dir__)
22
+ src_roots << File.expand_path("../../../yeptris/src", __dir__)
23
+ src_root = src_roots.find { |d| d && File.directory?(File.join(d, "include")) }
24
+ abort "yeptris sources not found (set YEPTRIS_SRC)" unless src_root
21
25
 
22
26
  $INCFLAGS << " -I#{src_root}/include -I#{src_root}/yeptris"
23
27
  %w[build-validate/generated build/generated].each do |g|
@@ -1,14 +1,14 @@
1
1
  /* json_ruby.c — fused RFC 8259 → Ruby VALUE (TODO.restructure/22). */
2
2
  #include <ruby.h>
3
3
  #include <ruby/encoding.h>
4
+ #include <ruby/intern.h>
4
5
  #include <stdlib.h>
5
6
  #include <string.h>
6
- #include "parse/numbers.h"
7
7
  #include "parse/scalars.h"
8
8
  #include "scan/json.h"
9
9
 
10
10
  #define YEP_JR_MAX 1000
11
- #define YEP_KC 256
11
+ #define YEP_KC 1024
12
12
 
13
13
  typedef struct {
14
14
  uint64_t h;
@@ -86,64 +86,92 @@ static VALUE jr_str(jr* j, int as_key) {
86
86
  sp = j->p + start + 1;
87
87
  sl = (long)(close - start - 1);
88
88
  }
89
- if (as_key || sl <= 2) return jr_cached(j, sp, sl);
89
+ if (as_key || sl <= 24) return jr_cached(j, sp, sl);
90
90
  return rb_enc_str_new(sp, sl, j->enc);
91
91
  }
92
92
 
93
93
  static VALUE jr_num(jr* j) {
94
94
  size_t start = j->i;
95
- if (!yep_json_number(j->p, j->len, &j->i)) { j->err = -2; return Qnil; }
96
- const char* s = j->p + start;
97
- uint32_t n = (uint32_t)(j->i - start);
98
- int is_float = 0, neg = 0;
99
- uint32_t k = 0;
100
- if (s[0] == '-') { neg = 1; k = 1; }
101
- for (; k < n; k++) {
102
- char c = s[k];
103
- if (c == '.' || c == 'e' || c == 'E') { is_float = 1; break; }
95
+ int shape = 0;
96
+ int64_t iv = 0;
97
+ double dv = 0.0;
98
+ /* the fused kernel (scan/json.h): ONE grammar walk, values out */
99
+ if (!yep_json_number_scan(j->p, j->len, &j->i, &shape, &iv, &dv)) {
100
+ j->err = -2;
101
+ return Qnil;
104
102
  }
105
- if (!is_float && n - (uint32_t)neg <= 18) {
106
- int64_t v = 0;
107
- for (k = (uint32_t)neg; k < n; k++) v = v * 10 + (s[k] - '0');
108
- if (neg) v = -v;
109
- return LL2NUM(v);
103
+ if (shape == 0) {
104
+ return LL2NUM(iv);
110
105
  }
111
- if (is_float) {
112
- double d = 0.0;
113
- if (yep_num_f64(s, n, &d) != 0) { j->err = -2; return Qnil; }
114
- return DBL2NUM(d);
106
+ if (shape == 1) {
107
+ return DBL2NUM(dv);
115
108
  }
116
- int64_t v = 0;
117
- if (yep_num_i64(s, n, &v) != 0) {
118
- double d = 0.0;
119
- if (yep_num_f64(s, n, &d) != 0) { j->err = -2; return Qnil; }
120
- return DBL2NUM(d);
109
+ /* integer text beyond int64: exact Bignum from the validated
110
+ * span (JSON.parse's behavior). Absurd lengths degrade to the
111
+ * approximate double. */
112
+ size_t n = j->i - start;
113
+ if (n < 512) {
114
+ char buf[512];
115
+ memcpy(buf, j->p + start, n);
116
+ buf[n] = '\0';
117
+ return rb_cstr_to_inum(buf, 10, TRUE);
121
118
  }
122
- return LL2NUM(v);
119
+ return DBL2NUM(dv);
123
120
  }
124
121
 
122
+ /* Insert strategy (TODO.restructure/34): bulk lands all pairs in one
123
+ * rb_hash_bulk_insert (skips per-pair dispatch) but costs +1.4k
124
+ * intermediate allocations on the reference corpus — the CI referee
125
+ * rules per platform. aset = the per-pair rb_hash_aset loop. */
126
+ enum { YEP_INS_BULK = 0, YEP_INS_ASET = 1 };
127
+ static int yep_ins_mode = YEP_INS_BULK;
128
+
125
129
  static VALUE jr_object(jr* j) {
126
130
  j->i++; j->depth++;
127
131
  VALUE h = rb_hash_new_capa(8);
128
132
  jr_ws(j);
129
133
  if (j->i < j->len && j->p[j->i] == '}') { j->i++; j->depth--; return h; }
134
+ VALUE pairs[64];
135
+ VALUE* pv = pairs;
136
+ size_t pcap = 64, pn = 0, heap_cap = 0;
130
137
  for (;;) {
131
138
  jr_ws(j);
132
- if (j->i >= j->len || j->p[j->i] != '"') { j->err = -2; return Qnil; }
139
+ if (j->i >= j->len || j->p[j->i] != '"') { j->err = -2; goto out; }
133
140
  VALUE key = jr_str(j, 1);
134
- if (j->err) return Qnil;
141
+ if (j->err) goto out;
135
142
  jr_ws(j);
136
- if (j->i >= j->len || j->p[j->i] != ':') { j->err = -2; return Qnil; }
143
+ if (j->i >= j->len || j->p[j->i] != ':') { j->err = -2; goto out; }
137
144
  j->i++;
138
145
  VALUE val = jr_value(j);
139
- if (j->err) return Qnil;
140
- rb_hash_aset(h, key, val);
146
+ if (j->err) goto out;
147
+ if (yep_ins_mode == YEP_INS_ASET) {
148
+ rb_hash_aset(h, key, val);
149
+ } else {
150
+ if (pn + 2 > pcap) {
151
+ size_t ncap = pcap * 2;
152
+ VALUE* nv = malloc(ncap * sizeof(VALUE));
153
+ if (!nv) { j->err = -1; goto out; }
154
+ memcpy(nv, pv, pn * sizeof(VALUE));
155
+ if (pv != pairs) { free(pv); heap_cap = 1; }
156
+ pv = nv; pcap = ncap;
157
+ }
158
+ pv[pn++] = key;
159
+ pv[pn++] = val;
160
+ }
141
161
  jr_ws(j);
142
- if (j->i >= j->len) { j->err = -2; return Qnil; }
162
+ if (j->i >= j->len) { j->err = -2; goto out; }
143
163
  if (j->p[j->i] == ',') { j->i++; continue; }
144
- if (j->p[j->i] == '}') { j->i++; j->depth--; return h; }
145
- j->err = -2; return Qnil;
164
+ if (j->p[j->i] == '}') { j->i++; break; }
165
+ j->err = -2; goto out;
146
166
  }
167
+ if (yep_ins_mode == YEP_INS_BULK) {
168
+ rb_hash_bulk_insert((long)pn, (const VALUE*)pv, h);
169
+ }
170
+ out:
171
+ if (pv != pairs) { free(pv); (void)heap_cap; }
172
+ if (j->err) return Qnil;
173
+ j->depth--;
174
+ return h;
147
175
  }
148
176
 
149
177
  static VALUE jr_array(jr* j) {
@@ -187,16 +215,56 @@ static VALUE jr_value(jr* j) {
187
215
  j->err = -2; return Qnil;
188
216
  }
189
217
 
218
+ /* GC strategy for the parse window (TODO.restructure/34): JSON.parse
219
+ * pays minor GCs mid-parse and recycles slots continuously; a blanket
220
+ * disable defers every collection — fresh pages each iteration, which
221
+ * is cheap idle and expensive exactly under memory contention (the
222
+ * loaded-box regression). The strategy is a runtime choice so the CI
223
+ * referee can A/B without rebuilds:
224
+ * disable (default) — pause GC for the window
225
+ * none — never pause
226
+ * start — pause, then one gc_start before returning
227
+ * (pay the minor GC in-window, like JSON.parse)
228
+ */
229
+ enum { YEP_GC_DISABLE = 0, YEP_GC_NONE = 1, YEP_GC_START = 2 };
230
+ /* Default per arch (TODO.restructure/35, two CI rounds of evidence):
231
+ * - aarch64/darwin: NONE — +0 heap pages, the stdlib's own GC cadence
232
+ * (0.745x mean, h2h 91% on mac runners; disable was 0.899x/48%).
233
+ * - x86_64: DISABLE — fresh CI VMs have free pages and cheaper
234
+ * page faults than minor GCs (0.911-0.922x vs none's 1.06-1.20x).
235
+ * The mechanism cuts both ways under load: disable's +478 pages/50
236
+ * iters is exactly what a LOADED x86 box punishes — set
237
+ * YEPTRIS_NATIVE_GC=none there (documented in README). */
238
+ #if defined(__aarch64__) || defined(__arm__) || defined(__ARMEL__) || defined(_M_ARM64)
239
+ #define YEP_GC_DEFAULT YEP_GC_NONE
240
+ #else
241
+ #define YEP_GC_DEFAULT YEP_GC_DISABLE
242
+ #endif
243
+ static int yep_gc_mode = YEP_GC_DEFAULT;
244
+
245
+ int yep_rb_gc_mode(void) { return yep_gc_mode; }
246
+ int yep_rb_ins_mode(void) { return yep_ins_mode; }
247
+ void yep_rb_set_ins_mode(int mode) {
248
+ if (mode == YEP_INS_BULK || mode == YEP_INS_ASET) yep_ins_mode = mode;
249
+ }
250
+ void yep_rb_set_gc_mode(int mode) {
251
+ if (mode >= YEP_GC_DISABLE && mode <= YEP_GC_START) {
252
+ yep_gc_mode = mode;
253
+ }
254
+ }
255
+
190
256
  VALUE yep_rb_parse_json(const char* p, size_t len) {
191
257
  jr j;
192
258
  memset(&j, 0, sizeof(j));
193
259
  j.p = p; j.len = len; j.enc = rb_utf8_encoding();
194
- VALUE already = rb_gc_disable();
260
+ VALUE already = Qtrue;
261
+ if (yep_gc_mode != YEP_GC_NONE) already = rb_gc_disable();
195
262
  VALUE v = jr_value(&j);
196
263
  jr_ws(&j);
197
264
  if (j.i != len && j.err == 0) j.err = -2;
198
265
  free(j.scratch);
199
266
  if (already == Qfalse) rb_gc_enable();
267
+ if (yep_gc_mode == YEP_GC_START && j.err == 0) rb_gc_start();
200
268
  if (j.err == -1) rb_raise(rb_eNoMemError, "yeptris native json");
201
269
  if (j.err != 0) rb_raise(rb_path2class("Yeptris::ParseError"), "native json parse failed");
202
270
  return v;
@@ -322,6 +322,49 @@ static VALUE native_load_stream(VALUE self, VALUE input, VALUE schema) {
322
322
  return ctx_result(&s.inner, st == YEPTRIS_OK ? YEPTRIS_ERROR_INTERNAL : st);
323
323
  }
324
324
 
325
+ /* GC-strategy surface (TODO.restructure/34): ENV at load sets the
326
+ * default; the setter re-picks at runtime so the CI referee can A/B
327
+ * in-process. Symbols: :disable, :none, :start. */
328
+ extern int yep_rb_gc_mode(void);
329
+ extern void yep_rb_set_gc_mode(int mode);
330
+ extern int yep_rb_ins_mode(void);
331
+ extern void yep_rb_set_ins_mode(int mode);
332
+
333
+ static VALUE native_gc_mode(VALUE self) {
334
+ (void)self;
335
+ switch (yep_rb_gc_mode()) {
336
+ case 1: return ID2SYM(rb_intern("none"));
337
+ case 2: return ID2SYM(rb_intern("start"));
338
+ default: return ID2SYM(rb_intern("disable"));
339
+ }
340
+ }
341
+
342
+ static VALUE native_ins_mode(VALUE self) {
343
+ (void)self;
344
+ return yep_rb_ins_mode() == 1 ? ID2SYM(rb_intern("aset")) : ID2SYM(rb_intern("bulk"));
345
+ }
346
+
347
+ static VALUE native_ins_mode_set(VALUE self, VALUE mode) {
348
+ (void)self;
349
+ Check_Type(mode, T_SYMBOL);
350
+ ID id = rb_sym2id(mode);
351
+ if (id == rb_intern("bulk")) yep_rb_set_ins_mode(0);
352
+ else if (id == rb_intern("aset")) yep_rb_set_ins_mode(1);
353
+ else rb_raise(rb_eArgError, "ins_mode must be :bulk or :aset");
354
+ return mode;
355
+ }
356
+
357
+ static VALUE native_gc_mode_set(VALUE self, VALUE mode) {
358
+ (void)self;
359
+ Check_Type(mode, T_SYMBOL);
360
+ ID id = rb_sym2id(mode);
361
+ if (id == rb_intern("disable")) yep_rb_set_gc_mode(0);
362
+ else if (id == rb_intern("none")) yep_rb_set_gc_mode(1);
363
+ else if (id == rb_intern("start")) yep_rb_set_gc_mode(2);
364
+ else rb_raise(rb_eArgError, "gc_mode must be :disable, :none, or :start");
365
+ return mode;
366
+ }
367
+
325
368
  RUBY_FUNC_EXPORTED void Init_native(void) {
326
369
  utf8_enc = rb_utf8_encoding();
327
370
  VALUE mYep = rb_define_module("Yeptris");
@@ -329,5 +372,15 @@ RUBY_FUNC_EXPORTED void Init_native(void) {
329
372
  rb_define_singleton_method(mNat, "load_json", native_load_json, 1);
330
373
  rb_define_singleton_method(mNat, "load", native_load, 2);
331
374
  rb_define_singleton_method(mNat, "load_stream", native_load_stream, 2);
375
+ rb_define_singleton_method(mNat, "gc_mode", native_gc_mode, 0);
376
+ rb_define_singleton_method(mNat, "gc_mode=", native_gc_mode_set, 1);
377
+ rb_define_singleton_method(mNat, "ins_mode", native_ins_mode, 0);
378
+ rb_define_singleton_method(mNat, "ins_mode=", native_ins_mode_set, 1);
332
379
  rb_define_const(mNat, "AVAILABLE", Qtrue);
380
+ const char* env = getenv("YEPTRIS_NATIVE_GC");
381
+ if (env != NULL && strcmp(env, "none") == 0) yep_rb_set_gc_mode(1);
382
+ else if (env != NULL && strcmp(env, "start") == 0) yep_rb_set_gc_mode(2);
383
+ else if (env != NULL && strcmp(env, "disable") == 0) yep_rb_set_gc_mode(0);
384
+ const char* ins = getenv("YEPTRIS_NATIVE_INSERT");
385
+ if (ins != NULL && strcmp(ins, "aset") == 0) yep_rb_set_ins_mode(1);
333
386
  }
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yeptris
4
+ # The STRICT JSON surface (TODO.restructure/31).
5
+ #
6
+ # `Yeptris::JSON.load` targets EXACT `JSON.parse` semantics — that
7
+ # is its parity target, spec-pinned (spec/json_parity_spec.rb).
8
+ # It is deliberately separate from Yeptris::YAML: the YAML surface
9
+ # keeps the Psych contract for every input (JSON-shaped included),
10
+ # so the two never drift into each other's semantics.
11
+ #
12
+ # Engines, fastest first (both exact — the parity spec runs against
13
+ # whichever is loaded):
14
+ # 1. the native materializer (fused C scan → VALUE; opt-in build)
15
+ # 2. the record drain + strict conversion walk (always available)
16
+ module JSON
17
+ class Error < ::Yeptris::Error; end
18
+ class ParseError < Error; end
19
+
20
+ module_function
21
+
22
+ def load(source)
23
+ source = ::Yeptris.read_input(source)
24
+ source = source.to_s
25
+ if defined?(::Yeptris::Native)
26
+ begin
27
+ return ::Yeptris::Native.load_json(source)
28
+ rescue ::Yeptris::ParseError => e
29
+ raise ParseError, e.message
30
+ end
31
+ end
32
+ strict_fallback(source)
33
+ end
34
+
35
+ # The always-available engine: the strict-JSON validator gates
36
+ # (parse_json raises on anything RFC 8259 rejects), then the
37
+ # value records convert WITHOUT the Psych quirk table — floats
38
+ # are always Floats, bools always bools ("1e3" is 1000.0 here
39
+ # and a String on the YAML surface; each surface is its own
40
+ # contract).
41
+ def strict_fallback(source)
42
+ begin
43
+ gate = ::Yeptris::Document.parse_json(source)
44
+ rescue ::Yeptris::ParseError => e
45
+ raise ParseError, e.message
46
+ end
47
+ begin
48
+ cols = ::Yeptris::FFI::ValueColumns.new
49
+ st = ::Yeptris::FFI.yeptris_value_drain_columns(
50
+ source, source.bytesize, ::Yeptris::FFI::SCHEMA_12_CORE, cols
51
+ )
52
+ raise ParseError, ::Yeptris::FFI.last_error_message if st != ::Yeptris::FFI::OK
53
+
54
+ begin
55
+ walk_strict(cols)
56
+ ensure
57
+ ::Yeptris::FFI.yeptris_value_free_columns(cols)
58
+ end
59
+ ensure
60
+ gate.free
61
+ end
62
+ end
63
+
64
+ # Placement mechanics mirror ValueML.walk_columns; the CONVERSION
65
+ # is the strict-JSON one (no ':sym' scan, no y/n quirk, no
66
+ # dot-required floats). Anchors/aliases/timestamps cannot occur
67
+ # in strict JSON — reaching them is an internal error.
68
+ def walk_strict(cols)
69
+ n = cols[:count]
70
+ kinds = cols[:kinds].read_bytes(n).unpack("C*")
71
+ ikeys = cols[:is_keys].read_bytes(n).unpack("C*")
72
+ bools = cols[:bools].read_bytes(n).unpack("C*")
73
+ offs = cols[:offs].read_bytes(n * 4).unpack("V*")
74
+ lens = cols[:lens].read_bytes(n * 4).unpack("V*")
75
+ pays = cols[:payloads].read_bytes(n * 8).unpack("q<*")
76
+ arena = cols[:arena_len].zero? ? +"" : cols[:arena].read_bytes(cols[:arena_len])
77
+ arena.force_encoding(Encoding::UTF_8)
78
+
79
+ docs = []
80
+ stack = []
81
+ pending_key = nil
82
+ i = 0
83
+ while i < n
84
+ case kinds[i]
85
+ when ValueML::DOC
86
+ docs.push(nil)
87
+ when ValueML::SEQ_OPEN
88
+ place(docs, stack, pending_key) { [] }
89
+ pending_key = nil
90
+ when ValueML::MAP_OPEN
91
+ place(docs, stack, pending_key) { {} }
92
+ pending_key = nil
93
+ when ValueML::CLOSE
94
+ stack.pop
95
+ when ValueML::V_STR
96
+ text = arena.byteslice(offs[i], lens[i])
97
+ if ikeys[i] == 1 && !stack.empty? && stack.last.is_a?(Hash)
98
+ pending_key = text
99
+ else
100
+ # Records carry int64 payloads: an integer-beyond-int64
101
+ # degrades to a PLAIN string (b==1). In strict JSON a
102
+ # plain (unquoted) scalar can ONLY be a number — every
103
+ # real string is quoted and arrives b==0 — so rebuild the
104
+ # exact Integer (JSON.parse parity, Bignum included).
105
+ if bools[i] == 1
106
+ place(docs, stack, pending_key) { Integer(text, 10) }
107
+ else
108
+ place(docs, stack, pending_key) { text }
109
+ end
110
+ pending_key = nil
111
+ end
112
+ when ValueML::V_INT
113
+ place(docs, stack, pending_key) { pays[i] }
114
+ pending_key = nil
115
+ when ValueML::V_FLOAT
116
+ place(docs, stack, pending_key) { [pays[i]].pack("q<").unpack1("E") }
117
+ pending_key = nil
118
+ when ValueML::V_BOOL
119
+ place(docs, stack, pending_key) { bools[i] == 1 }
120
+ pending_key = nil
121
+ when ValueML::V_NULL
122
+ place(docs, stack, pending_key) { nil }
123
+ pending_key = nil
124
+ else
125
+ raise Error, "internal: impossible record #{kinds[i]} in strict JSON"
126
+ end
127
+ i += 1
128
+ end
129
+ docs.empty? ? nil : docs.first
130
+ end
131
+
132
+ def place(docs, stack, key)
133
+ v = yield
134
+ if stack.empty?
135
+ docs[-1] = v
136
+ elsif key
137
+ stack.last[key] = v
138
+ else
139
+ stack.last.push(v)
140
+ end
141
+ stack.push(v) if v.is_a?(Array) || v.is_a?(Hash)
142
+ v
143
+ end
144
+ end
145
+ end
data/lib/yeptris/yaml.rb CHANGED
@@ -10,16 +10,14 @@ module Yeptris
10
10
  # Loads the FIRST document of a YAML stream as native Ruby objects.
11
11
  # schema: :compat_11 selects Psych/libyaml implicit typing
12
12
  # (yes/no, 0o/octal, sexagesimal); :core_12 (default) is YAML 1.2.
13
+ #
14
+ # This surface keeps the Psych contract for EVERY input — including
15
+ # JSON-shaped ones (`{"a": [1,]}` is legal flow YAML; `"1e3"` is a
16
+ # Psych String). Strict RFC 8259 semantics live on Yeptris::JSON
17
+ # (TODO.restructure/31): defaults follow proof, not benchmarks.
13
18
  def load(yaml, schema: :compat_11)
14
19
  yaml = Yeptris.read_input(yaml)
15
20
  yaml = yaml.to_s
16
- # The native C materializer fuses strict-JSON scan→VALUE in one
17
- # pass (beats JSON.parse on the 152 KB corpus). YAML inputs keep
18
- # the FFI ladder so timestamps, aliases, and Psych's scalar
19
- # quirks all resolve through the same path Psych.load uses.
20
- if defined?(Yeptris::Native) && native_json?(yaml)
21
- return Yeptris::Native.load_json(yaml)
22
- end
23
21
  docs = _drain_all(yaml, schema)
24
22
  docs.empty? ? nil : docs.first
25
23
  end
@@ -31,22 +29,6 @@ module Yeptris
31
29
  _drain_all(yaml, schema)
32
30
  end
33
31
 
34
- # Strict-JSON sniff: first non-space is { or [ — the fused native
35
- # path beats JSON.parse on this shape (TODO.restructure/22).
36
- def native_json?(bytes)
37
- i = 0
38
- len = bytes.bytesize
39
- while i < len
40
- c = bytes.getbyte(i)
41
- return true if c == 0x7b || c == 0x5b # { or [
42
- return false unless c == 0x20 || c == 0x09 || c == 0x0a || c == 0x0d
43
-
44
- i += 1
45
- end
46
- false
47
- end
48
- private_class_method :native_json?
49
-
50
32
  # The Marshal fast path when the loaded libyeptris has it (>= 0.1.11
51
33
  # era builds), falling back to the columnar drain and finally the
52
34
  # record drain — one code path, the fastest the library offers.
data/lib/yeptris.rb CHANGED
@@ -3,7 +3,7 @@
3
3
  module Yeptris
4
4
  # The gem's version lives in the parent namespace's file — the last
5
5
  # internal require (yeptris/version) retired with it.
6
- VERSION = "0.1.12.0".freeze
6
+ VERSION = "0.1.13.1".freeze
7
7
  # The error hierarchy lives in THIS file (the parent namespace's
8
8
  # own file): nested constants do not trigger a parent-constant
9
9
  # autoload, and the law forbids internal requires — defining the
@@ -39,6 +39,7 @@ module Yeptris
39
39
  autoload :Document, "yeptris/document"
40
40
  autoload :Node, "yeptris/node"
41
41
  autoload :YAML, "yeptris/yaml"
42
+ autoload :JSON, "yeptris/json"
42
43
  autoload :Materializer, "yeptris/materializer"
43
44
  autoload :ValueML, "yeptris/valueml"
44
45
  autoload :Psych, "yeptris/psych"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yeptris
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.12.0
4
+ version: 0.1.13.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -40,6 +40,7 @@ files:
40
40
  - lib/yeptris.rb
41
41
  - lib/yeptris/document.rb
42
42
  - lib/yeptris/ffi.rb
43
+ - lib/yeptris/json.rb
43
44
  - lib/yeptris/materializer.rb
44
45
  - lib/yeptris/node.rb
45
46
  - lib/yeptris/psych.rb