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 +4 -4
- data/README.adoc +43 -18
- data/ext/yeptris_native/extconf.rb +9 -5
- data/ext/yeptris_native/json_ruby.c +104 -36
- data/ext/yeptris_native/yeptris_native.c +53 -0
- data/lib/yeptris/json.rb +145 -0
- data/lib/yeptris/yaml.rb +5 -23
- data/lib/yeptris.rb +2 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 17b1311c9327a9986b7bb89f3731fd40ca040c49336fdb97596951fbf87ed1c0
|
|
4
|
+
data.tar.gz: 6c1b2f707d15d469963ffc0ee030fc90287a5e01954d7431996f24765bddf9e3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
==
|
|
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
|
-
|
|
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 (
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
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 <=
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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 (
|
|
106
|
-
|
|
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 (
|
|
112
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
|
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;
|
|
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)
|
|
141
|
+
if (j->err) goto out;
|
|
135
142
|
jr_ws(j);
|
|
136
|
-
if (j->i >= j->len || j->p[j->i] != ':') { j->err = -2;
|
|
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)
|
|
140
|
-
|
|
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;
|
|
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++;
|
|
145
|
-
j->err = -2;
|
|
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 =
|
|
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
|
}
|
data/lib/yeptris/json.rb
ADDED
|
@@ -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.
|
|
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.
|
|
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
|