yeptris 0.2.0.1-aarch64-linux

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c852b0bf85ede7b70147a2f3de3da3d98a3ac25cfaedefdd1269152ae3330130
4
+ data.tar.gz: 600140b86aed5d7d24912bb4a7bbf017787c8f42cce6d6fc9392e228566d757e
5
+ SHA512:
6
+ metadata.gz: dc3f03a1c75abb628f9da3966ea11d55620123a63d520b57a55c146901d915c12a023a9043d1e5e8bf3978d2c773af4ccdca6bea0e0d89dbe893b3b28571d156
7
+ data.tar.gz: aada2ab8586d37c1eac33486b7c118f353361845469f6b248c747ae2dabba313f4826c4468a3164defe4bed340f98ffad7f3b097176b66ceaf39bbf401285002
data/README.adoc ADDED
@@ -0,0 +1,141 @@
1
+ = yeptris — YAML for Ruby at libleptris speed
2
+
3
+ An FFI-based (no C extension) Ruby YAML library over
4
+ https://github.com/leptris/yeptris[libyeptris] — the YAML counterpart
5
+ of libleptris. Psych-compatible semantics, one shared library, zero
6
+ compilation at install.
7
+
8
+ == Install (development)
9
+
10
+ The gem loads a shared `libyeptris` — from `YEPTRIS_LIB_PATH`, a
11
+ vendored `lib/platform/<tag>/` copy, or the system paths. For
12
+ development against a local build:
13
+
14
+ ....
15
+ # the sibling C checkout: ~/src/leptris/yeptris
16
+ cmake -B build -S . -DCMAKE_BUILD_TYPE=Release -DYEPTRIS_BUILD_SHARED=ON
17
+ cmake --build build
18
+
19
+ cd ~/src/leptris/yeptris-ruby
20
+ YEPTRIS_LIB_PATH=../yeptris/build/src/libyeptris.dylib bundle exec rspec
21
+ ....
22
+
23
+ Without `YEPTRIS_LIB_PATH` the spec helper falls back to a vendored
24
+ `lib/platform/<tag>/` copy, then to the sibling checkout's
25
+ `build-validate` — any `libyeptris.{so,dylib,dll}` path works.
26
+
27
+ == Usage
28
+
29
+ [source,ruby]
30
+ ----
31
+ require "yeptris"
32
+
33
+ Yeptris::YAML.load("name: yeptris\nrating: 10\n")
34
+ # => {"name" => "yeptris", "rating" => 10}
35
+
36
+ # brace mixed hashes on Ruby 3.x: a trailing symbol key otherwise
37
+ # splits the literal into keywords
38
+ Yeptris::YAML.dump({"name" => "yeptris", "tags" => [:yaml, :fast]})
39
+
40
+ doc = Yeptris::Document.parse(config_yaml)
41
+ doc.root["server"]["port"].to_i
42
+ doc.serialize
43
+ ----
44
+
45
+ `Yeptris::YAML.load` defaults to Psych's YAML 1.1 implicit typing
46
+ (`yes` is `true`, `017` is octal); pass `schema: :core_12` for YAML
47
+ 1.2 core semantics. Dump builds through the library's DOM mutation
48
+ API, so the writer's sizing/escape machinery applies to synthesized
49
+ trees unchanged.
50
+
51
+ Handles are document-scoped: `Document#free` releases everything
52
+ (one C call), a GC finalizer backs it up, and any use after free
53
+ raises `Yeptris::FreedError` — never a segfault.
54
+
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; prefix-hashed token cache):
76
+ ubuntu (x86_64) mean 0.745x vs JSON.parse, 200/200 head-to-head
77
+ macos (arm64) mean 0.759x vs JSON.parse, 169/200 head-to-head
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.
94
+
95
+ === Building the native materializer (opt-in)
96
+
97
+ ....
98
+ cd ext/yeptris_native
99
+ YEPTRIS_LIB_PATH=/path/to/libyeptris.dylib ruby extconf.rb && make
100
+ cp native.bundle ../../lib/yeptris/ # or .so on Linux
101
+ ....
102
+
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).
106
+
107
+ == Shipped beyond the original plan
108
+
109
+ * The columnar value drain (bulk, pre-converted typed columns — the
110
+ load fast path) and the bulk DOM builder (one `document_build`
111
+ call per dump).
112
+ * **Ruby Marshal 4.8 emission** (libyeptris >= 0.1.11): the C side
113
+ converts value records into Marshal bytes; one `Marshal.load`
114
+ materializes the whole graph. ~10× faster than the columnar walk
115
+ on JSON-shaped input, ~5× on YAML, ~50× on the per-node DOM walk
116
+ (`Node#to_ruby` becomes bulk). Alias identity preserved through
117
+ object links; merge keys and timestamps return UNSUPPORTED for the
118
+ record-walk fallback.
119
+ * `Yeptris::Psych` — the Psych-compatible namespace with the ported
120
+ Psych suite (143 specs), `.tml` corpora conformance, and the
121
+ Encodable object protocol (`include Yeptris::Psych::Encodable` +
122
+ `encode_with`/`init_with` — no `respond_to?`, no ivar reflection;
123
+ the library never reaches into an object's internals).
124
+ ** CO-EXISTENCE (issue #69): `require "yeptris/psych"` loads the
125
+ namespace WITHOUT touching the top-level `Psych` constant — it
126
+ coexists with stdlib psych in ANY load order. The process-
127
+ exclusive drop-in rebind (`::Psych = Yeptris::Psych`) is now
128
+ OPT-IN: `require "yeptris/psych/drop_in"`. Once the drop-in runs,
129
+ stdlib psych must NOT be loaded afterwards (its require re-opens
130
+ the rebound module and clobbers constants). Bundles that cannot
131
+ control the load order (activesupport et al.) should use
132
+ `Yeptris::Psych` / `Yeptris::YAML` directly.
133
+ ** `Yeptris::YAML.safe_load(yaml, permitted_classes: [], aliases:
134
+ false)` — Psych's safe_load semantics on the native surface:
135
+ plain data by default; a leaf that would materialize to a
136
+ non-permitted class (a compat_11 date) raises
137
+ `Yeptris::Psych::DisallowedClass`; alias use without `aliases:
138
+ true` raises `Yeptris::Psych::AliasesError`.
139
+ * Lockstep versioning with the C library
140
+ (`{c-semver}.{gem-patch}`); releases published from the C repo's
141
+ workflow through the RubyGems trusted publisher.
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mkmf"
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.
8
+ lib_path = ENV["YEPTRIS_LIB_PATH"]
9
+ candidates = []
10
+ if lib_path
11
+ candidates << File.dirname(lib_path)
12
+ candidates << lib_path if File.directory?(lib_path)
13
+ end
14
+ candidates << File.expand_path("../../../../yeptris/build-validate/src", __dir__)
15
+ candidates << File.expand_path("../../../../yeptris/build/src", __dir__)
16
+ candidates << File.expand_path("../../../yeptris/build-validate/src", __dir__)
17
+ candidates << File.expand_path("../../../yeptris/build/src", __dir__)
18
+
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
25
+
26
+ $INCFLAGS << " -I#{src_root}/include -I#{src_root}/yeptris"
27
+ %w[build-validate/generated build/generated].each do |g|
28
+ d = File.join(File.dirname(src_root), g)
29
+ $INCFLAGS << " -I#{d}" if File.directory?(d)
30
+ end
31
+
32
+ libdir = candidates.find do |d|
33
+ d && File.directory?(d) && Dir[File.join(d, "libyeptris*.{dylib,so,dll}")].any?
34
+ end
35
+ abort "libyeptris not found (set YEPTRIS_LIB_PATH)" unless libdir
36
+
37
+ $LIBPATH << libdir
38
+ # Platform gems bake a RELATIVE rpath so the bundle finds the
39
+ # vendored libyeptris next to itself; absolute build-time paths
40
+ # would dangle on user machines.
41
+ rpath = ENV["YEPTRIS_RPATH"] || libdir
42
+ $LDFLAGS << " -Wl,-rpath,#{rpath}" if RUBY_PLATFORM =~ /linux|darwin/
43
+ have_library("yeptris", "yep_json_string") or abort "yep_json_string not exported — rebuild libyeptris"
44
+ have_library("yeptris", "yeptris_visit_json") or abort "yeptris_visit_json missing"
45
+
46
+ $CFLAGS << " -O3 -fvisibility=hidden" unless RUBY_PLATFORM =~ /mswin|mingw/
47
+ create_makefile("yeptris/native")
@@ -0,0 +1,343 @@
1
+ /* json_ruby.c — fused RFC 8259 → Ruby VALUE (TODO.restructure/22). */
2
+ #include <ruby.h>
3
+ #include <ruby/encoding.h>
4
+ #include <ruby/intern.h>
5
+ #include <stdlib.h>
6
+ #include <string.h>
7
+ #include "parse/scalars.h"
8
+ #include <yeptris/visit.h>
9
+ #include "scan/json.h"
10
+
11
+ #define YEP_JR_MAX 1000
12
+ #define YEP_KC 1024
13
+
14
+ typedef struct {
15
+ uint64_t h;
16
+ uint32_t len;
17
+ VALUE v;
18
+ } kcent;
19
+
20
+ typedef struct {
21
+ const char* p;
22
+ size_t len;
23
+ size_t i;
24
+ char* scratch;
25
+ size_t scratch_cap;
26
+ int depth;
27
+ int err;
28
+ rb_encoding* enc;
29
+ int strict_dup; /* json gem >= 3: duplicate keys raise (issue #37) */
30
+ VALUE dup_key;
31
+ kcent kc[YEP_KC];
32
+ } jr;
33
+
34
+ static VALUE jr_value(jr* j);
35
+ static VALUE jr_object_body(jr* j, VALUE h_pre);
36
+
37
+ static void jr_ws(jr* j) {
38
+ while (j->i < j->len) {
39
+ unsigned char c = (unsigned char)j->p[j->i];
40
+ if (c == ' ' || c == '\t' || c == '\n' || c == '\r') j->i++;
41
+ else break;
42
+ }
43
+ }
44
+
45
+ /* Token-cache knob (TODO.restructure/37): off = fresh strings
46
+ * everywhere (JSON.parse's shape: every key pays its own aset
47
+ * hashing); on = interned keys/tokens (shared frozen VALUEs
48
+ * memoize their hash). The CI referee A/Bs the net sign per arch. */
49
+ enum { YEP_CACHE_ON = 0, YEP_CACHE_OFF = 1 };
50
+ static int yep_cache_mode = YEP_CACHE_ON;
51
+
52
+ /* Allocation-shape knob (TODO.restructure/39): pre = capa(8) up
53
+ * front (forces heap buffers even for tiny arrays); natural =
54
+ * rb_ary_new() so 3-element arrays stay EMBEDDED in the RVALUE, and
55
+ * bulk-path hashes get exact capacity from the counted pairs. */
56
+ enum { YEP_SHAPE_PRE = 0, YEP_SHAPE_NATURAL = 1 };
57
+ static int yep_shape_mode = YEP_SHAPE_PRE;
58
+
59
+ static uint64_t jr_hash(const char* sp, long sl) {
60
+ /* 8-byte-prefix key (the leptris nametab trick): one safe load
61
+ * of min(8, len) bytes + a multiply mix. Replaced byte-wise FNV
62
+ * (~15-20ns per token on the cached path, ~15k tokens per
63
+ * reference-corpus parse - the x86 materialization cost the
64
+ * decomposition isolated, TODO.restructure/37/38). */
65
+ uint64_t k = 0;
66
+ uint64_t take = (uint64_t)sl < 8 ? (uint64_t)sl : 8;
67
+ memcpy(&k, sp, (size_t)take);
68
+ k ^= (uint64_t)sl * 0x9E3779B97F4A7C15ull;
69
+ k *= 0xC2B2AE3D27D4EB4Full;
70
+ k ^= k >> 29;
71
+ return k;
72
+ }
73
+
74
+ static VALUE jr_cached(jr* j, const char* sp, long sl) {
75
+ uint64_t h = jr_hash(sp, sl);
76
+ uint32_t slot = (uint32_t)(h & (YEP_KC - 1));
77
+ kcent* e = &j->kc[slot];
78
+ if (e->v != 0 && e->h == h && e->len == (uint32_t)sl &&
79
+ (long)RSTRING_LEN(e->v) == sl &&
80
+ memcmp(RSTRING_PTR(e->v), sp, (size_t)sl) == 0) {
81
+ return e->v;
82
+ }
83
+ VALUE s = rb_enc_str_new(sp, sl, j->enc);
84
+ rb_str_freeze(s);
85
+ e->h = h;
86
+ e->len = (uint32_t)sl;
87
+ e->v = s;
88
+ return s;
89
+ }
90
+
91
+ static VALUE jr_str(jr* j, int as_key) {
92
+ size_t start = j->i, close = 0;
93
+ int has_esc = 0;
94
+ if (!yep_json_string(j->p, j->len, &j->i, &close, &has_esc)) { j->err = -2; return Qnil; }
95
+ const char* sp; long sl;
96
+ if (has_esc) {
97
+ uint32_t span = (uint32_t)(close - start - 1);
98
+ if (span + 1 > j->scratch_cap) {
99
+ size_t cap = j->scratch_cap ? j->scratch_cap : 64;
100
+ while (cap < span + 1) cap *= 2;
101
+ char* ns = realloc(j->scratch, cap);
102
+ if (!ns) { j->err = -1; return Qnil; }
103
+ j->scratch = ns; j->scratch_cap = cap;
104
+ }
105
+ sl = (long)yep_finish_double_into(j->p, (uint32_t)(start + 1), (uint32_t)close, j->scratch, span);
106
+ sp = j->scratch;
107
+ } else {
108
+ sp = j->p + start + 1;
109
+ sl = (long)(close - start - 1);
110
+ }
111
+ if (yep_cache_mode == YEP_CACHE_ON && (as_key || sl <= 24)) return jr_cached(j, sp, sl);
112
+ return rb_enc_str_new(sp, sl, j->enc);
113
+ }
114
+
115
+ static VALUE jr_num(jr* j) {
116
+ size_t start = j->i;
117
+ int shape = 0;
118
+ int64_t iv = 0;
119
+ double dv = 0.0;
120
+ /* the fused kernel (scan/json.h): ONE grammar walk, values out */
121
+ if (!yep_json_number_scan(j->p, j->len, &j->i, &shape, &iv, &dv)) {
122
+ j->err = -2;
123
+ return Qnil;
124
+ }
125
+ if (shape == 0) {
126
+ return LL2NUM(iv);
127
+ }
128
+ if (shape == 1) {
129
+ return DBL2NUM(dv);
130
+ }
131
+ /* integer text beyond int64: exact Bignum from the validated
132
+ * span (JSON.parse's behavior). Absurd lengths degrade to the
133
+ * approximate double. */
134
+ size_t n = j->i - start;
135
+ if (n < 512) {
136
+ char buf[512];
137
+ memcpy(buf, j->p + start, n);
138
+ buf[n] = '\0';
139
+ return rb_cstr_to_inum(buf, 10, TRUE);
140
+ }
141
+ return DBL2NUM(dv);
142
+ }
143
+
144
+ /* Insert strategy (TODO.restructure/34): bulk lands all pairs in one
145
+ * rb_hash_bulk_insert (skips per-pair dispatch) but costs +1.4k
146
+ * intermediate allocations on the reference corpus — the CI referee
147
+ * rules per platform. aset = the per-pair rb_hash_aset loop. */
148
+ enum { YEP_INS_BULK = 0, YEP_INS_ASET = 1 };
149
+ static int yep_ins_mode = YEP_INS_BULK;
150
+
151
+ static VALUE jr_object(jr* j) {
152
+ j->i++; j->depth++;
153
+ if (yep_shape_mode == YEP_SHAPE_PRE) {
154
+ VALUE h = rb_hash_new_capa(8);
155
+ jr_ws(j);
156
+ if (j->i < j->len && j->p[j->i] == '}') { j->i++; j->depth--; return h; }
157
+ return jr_object_body(j, h);
158
+ }
159
+ jr_ws(j);
160
+ if (j->i < j->len && j->p[j->i] == '}') { j->i++; j->depth--; return rb_hash_new(); }
161
+ return jr_object_body(j, Qundef); /* created post-loop with exact capa */
162
+
163
+ /* NOTREACHED */
164
+ }
165
+
166
+ static VALUE jr_object_body(jr* j, VALUE h_pre) {
167
+ VALUE pairs[64];
168
+ VALUE* pv = pairs;
169
+ size_t pcap = 64, pn = 0, heap_cap = 0;
170
+ for (;;) {
171
+ jr_ws(j);
172
+ if (j->i >= j->len || j->p[j->i] != '"') { j->err = -2; goto out; }
173
+ VALUE key = jr_str(j, 1);
174
+ if (j->err) goto out;
175
+ jr_ws(j);
176
+ if (j->i >= j->len || j->p[j->i] != ':') { j->err = -2; goto out; }
177
+ j->i++;
178
+ VALUE val = jr_value(j);
179
+ if (j->err) goto out;
180
+ if (yep_ins_mode == YEP_INS_ASET || j->strict_dup) {
181
+ VALUE h = h_pre == Qundef ? (h_pre = rb_hash_new_capa(8)) : h_pre;
182
+ if (j->strict_dup && !NIL_P(rb_hash_aref(h, key))) {
183
+ j->err = -3;
184
+ j->dup_key = key;
185
+ goto out;
186
+ }
187
+ rb_hash_aset(h, key, val);
188
+ } else {
189
+ if (pn + 2 > pcap) {
190
+ size_t ncap = pcap * 2;
191
+ VALUE* nv = malloc(ncap * sizeof(VALUE));
192
+ if (!nv) { j->err = -1; goto out; }
193
+ memcpy(nv, pv, pn * sizeof(VALUE));
194
+ if (pv != pairs) { free(pv); heap_cap = 1; }
195
+ pv = nv; pcap = ncap;
196
+ }
197
+ pv[pn++] = key;
198
+ pv[pn++] = val;
199
+ }
200
+ jr_ws(j);
201
+ if (j->i >= j->len) { j->err = -2; goto out; }
202
+ if (j->p[j->i] == ',') { j->i++; continue; }
203
+ if (j->p[j->i] == '}') { j->i++; break; }
204
+ j->err = -2; goto out;
205
+ }
206
+ if (yep_ins_mode == YEP_INS_BULK) {
207
+ VALUE h = (h_pre == Qundef) ? rb_hash_new_capa((long)(pn / 2)) : h_pre;
208
+ rb_hash_bulk_insert((long)pn, (const VALUE*)pv, h);
209
+ if (pv != pairs) { free(pv); }
210
+ j->depth--;
211
+ return h;
212
+ }
213
+ if (h_pre == Qundef) { h_pre = rb_hash_new_capa(8); }
214
+ out:
215
+ if (pv != pairs) { free(pv); (void)heap_cap; }
216
+ if (j->err) return Qnil;
217
+ j->depth--;
218
+ return h_pre;
219
+ }
220
+
221
+ static VALUE jr_array(jr* j) {
222
+ j->i++; j->depth++;
223
+ VALUE a = (yep_shape_mode == YEP_SHAPE_NATURAL) ? rb_ary_new() : rb_ary_new_capa(8);
224
+ jr_ws(j);
225
+ if (j->i < j->len && j->p[j->i] == ']') { j->i++; j->depth--; return a; }
226
+ for (;;) {
227
+ VALUE v = jr_value(j);
228
+ if (j->err) return Qnil;
229
+ rb_ary_push(a, v);
230
+ jr_ws(j);
231
+ if (j->i >= j->len) { j->err = -2; return Qnil; }
232
+ if (j->p[j->i] == ',') { j->i++; continue; }
233
+ if (j->p[j->i] == ']') { j->i++; j->depth--; return a; }
234
+ j->err = -2; return Qnil;
235
+ }
236
+ }
237
+
238
+ static VALUE jr_value(jr* j) {
239
+ if (j->depth >= YEP_JR_MAX) { j->err = -2; return Qnil; }
240
+ jr_ws(j);
241
+ if (j->i >= j->len) { j->err = -2; return Qnil; }
242
+ char c = j->p[j->i];
243
+ if (c == '{') return jr_object(j);
244
+ if (c == '[') return jr_array(j);
245
+ if (c == '"') return jr_str(j, 0);
246
+ if (c == 't') {
247
+ if (!yep_json_literal(j->p, j->len, &j->i, "true")) { j->err = -2; return Qnil; }
248
+ return Qtrue;
249
+ }
250
+ if (c == 'f') {
251
+ if (!yep_json_literal(j->p, j->len, &j->i, "false")) { j->err = -2; return Qnil; }
252
+ return Qfalse;
253
+ }
254
+ if (c == 'n') {
255
+ if (!yep_json_literal(j->p, j->len, &j->i, "null")) { j->err = -2; return Qnil; }
256
+ return Qnil;
257
+ }
258
+ if (c == '-' || (c >= '0' && c <= '9')) return jr_num(j);
259
+ j->err = -2; return Qnil;
260
+ }
261
+
262
+ /* GC strategy for the parse window (TODO.restructure/34): JSON.parse
263
+ * pays minor GCs mid-parse and recycles slots continuously; a blanket
264
+ * disable defers every collection — fresh pages each iteration, which
265
+ * is cheap idle and expensive exactly under memory contention (the
266
+ * loaded-box regression). The strategy is a runtime choice so the CI
267
+ * referee can A/B without rebuilds:
268
+ * disable (default) — pause GC for the window
269
+ * none — never pause
270
+ * start — pause, then one gc_start before returning
271
+ * (pay the minor GC in-window, like JSON.parse)
272
+ */
273
+ enum { YEP_GC_DISABLE = 0, YEP_GC_NONE = 1, YEP_GC_START = 2 };
274
+ /* Default per arch (TODO.restructure/35, two CI rounds of evidence):
275
+ * - aarch64/darwin: NONE — +0 heap pages, the stdlib's own GC cadence
276
+ * (0.745x mean, h2h 91% on mac runners; disable was 0.899x/48%).
277
+ * - x86_64: DISABLE — fresh CI VMs have free pages and cheaper
278
+ * page faults than minor GCs (0.911-0.922x vs none's 1.06-1.20x).
279
+ * The mechanism cuts both ways under load: disable's +478 pages/50
280
+ * iters is exactly what a LOADED x86 box punishes — set
281
+ * YEPTRIS_NATIVE_GC=none there (documented in README). */
282
+ #if defined(__aarch64__) || defined(__arm__) || defined(__ARMEL__) || defined(_M_ARM64)
283
+ #define YEP_GC_DEFAULT YEP_GC_NONE
284
+ #else
285
+ #define YEP_GC_DEFAULT YEP_GC_DISABLE
286
+ #endif
287
+ static int yep_gc_mode = YEP_GC_DEFAULT;
288
+
289
+ int yep_rb_gc_mode(void) { return yep_gc_mode; }
290
+ int yep_rb_ins_mode(void) { return yep_ins_mode; }
291
+ int yep_rb_cache_mode(void) { return yep_cache_mode; }
292
+ int yep_rb_shape_mode(void) { return yep_shape_mode; }
293
+ void yep_rb_set_shape_mode(int mode) {
294
+ if (mode == YEP_SHAPE_PRE || mode == YEP_SHAPE_NATURAL) yep_shape_mode = mode;
295
+ }
296
+ void yep_rb_set_cache_mode(int mode) {
297
+ if (mode == YEP_CACHE_ON || mode == YEP_CACHE_OFF) yep_cache_mode = mode;
298
+ }
299
+ void yep_rb_set_ins_mode(int mode) {
300
+ if (mode == YEP_INS_BULK || mode == YEP_INS_ASET) yep_ins_mode = mode;
301
+ }
302
+ void yep_rb_set_gc_mode(int mode) {
303
+ if (mode >= YEP_GC_DISABLE && mode <= YEP_GC_START) {
304
+ yep_gc_mode = mode;
305
+ }
306
+ }
307
+
308
+ /* Pure grammar walk, no materialization (TODO.restructure/37): the
309
+ * same scan kernels through the null vtable. Decomposes scan vs
310
+ * materialize cost. Returns seconds for n iterations. */
311
+ #include <time.h>
312
+ double yep_rb_scan_time(const char* p, size_t len, int n) {
313
+ static const YeptrisVisitVTable none = {0};
314
+ struct timespec t0, t1;
315
+ clock_gettime(CLOCK_MONOTONIC, &t0);
316
+ for (int i = 0; i < n; i++) {
317
+ (void)yeptris_visit_json(p, len, &none, NULL);
318
+ }
319
+ clock_gettime(CLOCK_MONOTONIC, &t1);
320
+ return (double)(t1.tv_sec - t0.tv_sec) + (double)(t1.tv_nsec - t0.tv_nsec) / 1e9;
321
+ }
322
+
323
+ VALUE yep_rb_parse_json(const char* p, size_t len, int strict_dup) {
324
+ jr j;
325
+ memset(&j, 0, sizeof(j));
326
+ j.p = p; j.len = len; j.enc = rb_utf8_encoding();
327
+ j.strict_dup = strict_dup;
328
+ VALUE already = Qtrue;
329
+ if (yep_gc_mode != YEP_GC_NONE) already = rb_gc_disable();
330
+ VALUE v = jr_value(&j);
331
+ jr_ws(&j);
332
+ if (j.i != len && j.err == 0) j.err = -2;
333
+ free(j.scratch);
334
+ if (already == Qfalse) rb_gc_enable();
335
+ if (yep_gc_mode == YEP_GC_START && j.err == 0) rb_gc_start();
336
+ if (j.err == -1) rb_raise(rb_eNoMemError, "yeptris native json");
337
+ if (j.err == -3) {
338
+ rb_raise(rb_path2class("Yeptris::ParseError"), "duplicate key \"%s\" in JSON object",
339
+ RSTRING_PTR(j.dup_key));
340
+ }
341
+ if (j.err != 0) rb_raise(rb_path2class("Yeptris::ParseError"), "native json parse failed");
342
+ return v;
343
+ }