yeptris 0.1.11.0 → 0.1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b2ddefc4cfb21c80a7238bbca6cfa46293fd0f3ff920a122e37bd6f4e3f78b0e
4
- data.tar.gz: cfbfe8da20325e11ef811dd9372e464fd1e03a1c0eff21cae254f918c9f5c979
3
+ metadata.gz: 76bc800fd5065540397d957ca2162d2491474a086eda5b08436f016daa8f65e4
4
+ data.tar.gz: 683259ac45cfb084722337af80c066890bdf56f2608843343ebf8951589eafa8
5
5
  SHA512:
6
- metadata.gz: 76f113166f28712bb328a7daf7f5b15f37f06c4ca5ce24dc56651878bded33800e3befbefed6b66f4da0a28a94c595797fb41004572d807a5ebca93a342e1cc6
7
- data.tar.gz: 273c6e45ff73043c14c24869b3d98207710514dcd055a939d0fa4c5832d133bf5d14686ba9259c712115b4034cb5785c9af6aaa9bbabf929315dd43c313cf997
6
+ metadata.gz: 3b2be1bc057ee5a059e80ac087000afd2cee325473d834db34bd0f898318634e7b9be95dce9b523f909ae6884a56c9ede549a0e7142184eddf4cc96a13a11cb9
7
+ data.tar.gz: e95683f3b475a0dca70488a43cdf23a249d2bcd49c440b4c75bf31914265c0eae9fd2992d1bf6c0ebfa5b0487b796f1a24505e42b4715840c1731ce2af845a4c
data/README.adoc CHANGED
@@ -52,6 +52,48 @@ 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
+ == 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
+ JSON.parse min 0.670 med 0.966 mean 0.910 ms
75
+ Yeptris::JSON min 0.637 med 0.674 mean 0.680 ms
76
+ mean 0.75x faster, head-to-head 370/400 (92.5%)
77
+ ....
78
+ 2. **Record-drain fallback** (always available): the strict-JSON
79
+ validator gates, then the value records convert without the Psych
80
+ quirk table — exact parity with engine 1, spec-pinned.
81
+
82
+ The committed profile (`benchmark/json_profile.rb`) is the fair
83
+ benchmark of record — any performance claim runs through it.
84
+
85
+ === Building the native materializer (opt-in)
86
+
87
+ ....
88
+ cd ext/yeptris_native
89
+ YEPTRIS_LIB_PATH=/path/to/libyeptris.dylib ruby extconf.rb && make
90
+ cp native.bundle ../../lib/yeptris/ # or .so on Linux
91
+ ....
92
+
93
+ `require "yeptris"` picks it up automatically (`Yeptris::JSON` then
94
+ uses it; missing builds silently use the fallback — the gem installs
95
+ without compiling).
96
+
55
97
  == Shipped beyond the original plan
56
98
 
57
99
  * The columnar value drain (bulk, pre-converted typed columns — the
@@ -65,8 +107,10 @@ raises `Yeptris::FreedError` — never a segfault.
65
107
  object links; merge keys and timestamps return UNSUPPORTED for the
66
108
  record-walk fallback.
67
109
  * `Yeptris::Psych` — the drop-in namespace with the ported Psych
68
- suite (143 specs), `.tml` corpora conformance, and the object
69
- visitor (`encode_with`/`init_with`).
110
+ suite (143 specs), `.tml` corpora conformance, and the Encodable
111
+ object protocol (`include Yeptris::Psych::Encodable` +
112
+ `encode_with`/`init_with` — no `respond_to?`, no ivar reflection;
113
+ the library never reaches into an object's internals).
70
114
  * Lockstep versioning with the C library
71
115
  (`{c-semver}.{gem-patch}`); releases published from the C repo's
72
116
  workflow through the RubyGems trusted publisher.
@@ -0,0 +1,43 @@
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
+ $LDFLAGS << " -Wl,-rpath,#{libdir}" if RUBY_PLATFORM =~ /linux|darwin/
39
+ have_library("yeptris", "yep_json_string") or abort "yep_json_string not exported — rebuild libyeptris"
40
+ have_library("yeptris", "yeptris_visit_json") or abort "yeptris_visit_json missing"
41
+
42
+ $CFLAGS << " -O3 -fvisibility=hidden" unless RUBY_PLATFORM =~ /mswin|mingw/
43
+ create_makefile("yeptris/native")
@@ -0,0 +1,221 @@
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 "scan/json.h"
9
+
10
+ #define YEP_JR_MAX 1000
11
+ #define YEP_KC 1024
12
+
13
+ typedef struct {
14
+ uint64_t h;
15
+ uint32_t len;
16
+ VALUE v;
17
+ } kcent;
18
+
19
+ typedef struct {
20
+ const char* p;
21
+ size_t len;
22
+ size_t i;
23
+ char* scratch;
24
+ size_t scratch_cap;
25
+ int depth;
26
+ int err;
27
+ rb_encoding* enc;
28
+ kcent kc[YEP_KC];
29
+ } jr;
30
+
31
+ static VALUE jr_value(jr* j);
32
+
33
+ static void jr_ws(jr* j) {
34
+ while (j->i < j->len) {
35
+ unsigned char c = (unsigned char)j->p[j->i];
36
+ if (c == ' ' || c == '\t' || c == '\n' || c == '\r') j->i++;
37
+ else break;
38
+ }
39
+ }
40
+
41
+ static uint64_t jr_hash(const char* sp, long sl) {
42
+ /* FNV-1a 64 — cheap, good enough for short keys */
43
+ uint64_t h = 14695981039346656037ull;
44
+ for (long i = 0; i < sl; i++) {
45
+ h ^= (unsigned char)sp[i];
46
+ h *= 1099511628211ull;
47
+ }
48
+ h ^= (uint64_t)sl;
49
+ return h;
50
+ }
51
+
52
+ static VALUE jr_cached(jr* j, const char* sp, long sl) {
53
+ uint64_t h = jr_hash(sp, sl);
54
+ uint32_t slot = (uint32_t)(h & (YEP_KC - 1));
55
+ kcent* e = &j->kc[slot];
56
+ if (e->v != 0 && e->h == h && e->len == (uint32_t)sl &&
57
+ (long)RSTRING_LEN(e->v) == sl &&
58
+ memcmp(RSTRING_PTR(e->v), sp, (size_t)sl) == 0) {
59
+ return e->v;
60
+ }
61
+ VALUE s = rb_enc_str_new(sp, sl, j->enc);
62
+ rb_str_freeze(s);
63
+ e->h = h;
64
+ e->len = (uint32_t)sl;
65
+ e->v = s;
66
+ return s;
67
+ }
68
+
69
+ static VALUE jr_str(jr* j, int as_key) {
70
+ size_t start = j->i, close = 0;
71
+ int has_esc = 0;
72
+ if (!yep_json_string(j->p, j->len, &j->i, &close, &has_esc)) { j->err = -2; return Qnil; }
73
+ const char* sp; long sl;
74
+ if (has_esc) {
75
+ uint32_t span = (uint32_t)(close - start - 1);
76
+ if (span + 1 > j->scratch_cap) {
77
+ size_t cap = j->scratch_cap ? j->scratch_cap : 64;
78
+ while (cap < span + 1) cap *= 2;
79
+ char* ns = realloc(j->scratch, cap);
80
+ if (!ns) { j->err = -1; return Qnil; }
81
+ j->scratch = ns; j->scratch_cap = cap;
82
+ }
83
+ sl = (long)yep_finish_double_into(j->p, (uint32_t)(start + 1), (uint32_t)close, j->scratch, span);
84
+ sp = j->scratch;
85
+ } else {
86
+ sp = j->p + start + 1;
87
+ sl = (long)(close - start - 1);
88
+ }
89
+ if (as_key || sl <= 24) return jr_cached(j, sp, sl);
90
+ return rb_enc_str_new(sp, sl, j->enc);
91
+ }
92
+
93
+ static VALUE jr_num(jr* j) {
94
+ size_t start = j->i;
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;
102
+ }
103
+ if (shape == 0) {
104
+ return LL2NUM(iv);
105
+ }
106
+ if (shape == 1) {
107
+ return DBL2NUM(dv);
108
+ }
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);
118
+ }
119
+ return DBL2NUM(dv);
120
+ }
121
+
122
+ static VALUE jr_object(jr* j) {
123
+ j->i++; j->depth++;
124
+ VALUE h = rb_hash_new_capa(8);
125
+ jr_ws(j);
126
+ if (j->i < j->len && j->p[j->i] == '}') { j->i++; j->depth--; return h; }
127
+ /* pairs collect into a flat buffer and land in ONE bulk insert:
128
+ * rb_hash_bulk_insert skips the per-pair method dispatch the
129
+ * aset loop pays (TODO.restructure/26's margin work) */
130
+ VALUE pairs[64];
131
+ VALUE* pv = pairs;
132
+ size_t pcap = 64, pn = 0, heap_cap = 0;
133
+ for (;;) {
134
+ jr_ws(j);
135
+ if (j->i >= j->len || j->p[j->i] != '"') { j->err = -2; goto out; }
136
+ VALUE key = jr_str(j, 1);
137
+ if (j->err) goto out;
138
+ jr_ws(j);
139
+ if (j->i >= j->len || j->p[j->i] != ':') { j->err = -2; goto out; }
140
+ j->i++;
141
+ VALUE val = jr_value(j);
142
+ if (j->err) goto out;
143
+ if (pn + 2 > pcap) {
144
+ size_t ncap = pcap * 2;
145
+ VALUE* nv = malloc(ncap * sizeof(VALUE));
146
+ if (!nv) { j->err = -1; goto out; }
147
+ memcpy(nv, pv, pn * sizeof(VALUE));
148
+ if (pv != pairs) { free(pv); heap_cap = 1; }
149
+ pv = nv; pcap = ncap;
150
+ }
151
+ pv[pn++] = key;
152
+ pv[pn++] = val;
153
+ jr_ws(j);
154
+ if (j->i >= j->len) { j->err = -2; goto out; }
155
+ if (j->p[j->i] == ',') { j->i++; continue; }
156
+ if (j->p[j->i] == '}') { j->i++; break; }
157
+ j->err = -2; goto out;
158
+ }
159
+ rb_hash_bulk_insert((long)pn, (const VALUE*)pv, h);
160
+ out:
161
+ if (pv != pairs) { free(pv); (void)heap_cap; }
162
+ if (j->err) return Qnil;
163
+ j->depth--;
164
+ return h;
165
+ }
166
+
167
+ static VALUE jr_array(jr* j) {
168
+ j->i++; j->depth++;
169
+ VALUE a = rb_ary_new_capa(8);
170
+ jr_ws(j);
171
+ if (j->i < j->len && j->p[j->i] == ']') { j->i++; j->depth--; return a; }
172
+ for (;;) {
173
+ VALUE v = jr_value(j);
174
+ if (j->err) return Qnil;
175
+ rb_ary_push(a, v);
176
+ jr_ws(j);
177
+ if (j->i >= j->len) { j->err = -2; return Qnil; }
178
+ if (j->p[j->i] == ',') { j->i++; continue; }
179
+ if (j->p[j->i] == ']') { j->i++; j->depth--; return a; }
180
+ j->err = -2; return Qnil;
181
+ }
182
+ }
183
+
184
+ static VALUE jr_value(jr* j) {
185
+ if (j->depth >= YEP_JR_MAX) { j->err = -2; return Qnil; }
186
+ jr_ws(j);
187
+ if (j->i >= j->len) { j->err = -2; return Qnil; }
188
+ char c = j->p[j->i];
189
+ if (c == '{') return jr_object(j);
190
+ if (c == '[') return jr_array(j);
191
+ if (c == '"') return jr_str(j, 0);
192
+ if (c == 't') {
193
+ if (!yep_json_literal(j->p, j->len, &j->i, "true")) { j->err = -2; return Qnil; }
194
+ return Qtrue;
195
+ }
196
+ if (c == 'f') {
197
+ if (!yep_json_literal(j->p, j->len, &j->i, "false")) { j->err = -2; return Qnil; }
198
+ return Qfalse;
199
+ }
200
+ if (c == 'n') {
201
+ if (!yep_json_literal(j->p, j->len, &j->i, "null")) { j->err = -2; return Qnil; }
202
+ return Qnil;
203
+ }
204
+ if (c == '-' || (c >= '0' && c <= '9')) return jr_num(j);
205
+ j->err = -2; return Qnil;
206
+ }
207
+
208
+ VALUE yep_rb_parse_json(const char* p, size_t len) {
209
+ jr j;
210
+ memset(&j, 0, sizeof(j));
211
+ j.p = p; j.len = len; j.enc = rb_utf8_encoding();
212
+ VALUE already = rb_gc_disable();
213
+ VALUE v = jr_value(&j);
214
+ jr_ws(&j);
215
+ if (j.i != len && j.err == 0) j.err = -2;
216
+ free(j.scratch);
217
+ if (already == Qfalse) rb_gc_enable();
218
+ if (j.err == -1) rb_raise(rb_eNoMemError, "yeptris native json");
219
+ if (j.err != 0) rb_raise(rb_path2class("Yeptris::ParseError"), "native json parse failed");
220
+ return v;
221
+ }
@@ -0,0 +1,333 @@
1
+ /* yeptris_native.c — Ruby C-API materializer over libyeptris visit
2
+ * (TODO.restructure/22). One Ruby→C call builds the whole object
3
+ * graph via rb_hash_new / rb_ary_push / rb_str_new_len — the same
4
+ * shape as JSON.parse, so the binding can beat it on the fused JSON
5
+ * path. The extension is optional: LoadError falls back to the FFI
6
+ * Marshal ladder. */
7
+
8
+ #include <ruby.h>
9
+ #include <ruby/encoding.h>
10
+
11
+ #include <stdlib.h>
12
+ #include <string.h>
13
+
14
+ #include <yeptris/api.h>
15
+ #include <yeptris/error.h>
16
+ #include <yeptris/resolve.h>
17
+ #include <yeptris/visit.h>
18
+
19
+ #define YEP_RB_MAX_DEPTH 1024
20
+
21
+ static rb_encoding* utf8_enc;
22
+
23
+ typedef struct {
24
+ VALUE stack[YEP_RB_MAX_DEPTH];
25
+ VALUE keys[YEP_RB_MAX_DEPTH]; /* pending map key at this depth */
26
+ int is_map[YEP_RB_MAX_DEPTH];
27
+ int sp;
28
+ VALUE root;
29
+ VALUE anchors; /* Hash name=>object for YAML identity */
30
+ VALUE pending_anchor; /* String name awaiting the next value */
31
+ int failed;
32
+ } rb_ctx;
33
+
34
+ static void rb_fail(rb_ctx* c) {
35
+ c->failed = 1;
36
+ }
37
+
38
+ static int rb_push_value(rb_ctx* c, VALUE v) {
39
+ if (c->failed) {
40
+ return -1;
41
+ }
42
+ /* bind pending anchor */
43
+ if (!NIL_P(c->pending_anchor) && !NIL_P(c->anchors)) {
44
+ rb_hash_aset(c->anchors, c->pending_anchor, v);
45
+ c->pending_anchor = Qnil;
46
+ }
47
+ if (c->sp == 0) {
48
+ c->root = v;
49
+ return 0;
50
+ }
51
+ VALUE parent = c->stack[c->sp - 1];
52
+ if (c->is_map[c->sp - 1]) {
53
+ VALUE key = c->keys[c->sp - 1];
54
+ if (NIL_P(key)) {
55
+ rb_fail(c);
56
+ return -1;
57
+ }
58
+ rb_hash_aset(parent, key, v);
59
+ c->keys[c->sp - 1] = Qnil;
60
+ } else {
61
+ rb_ary_push(parent, v);
62
+ }
63
+ return 0;
64
+ }
65
+
66
+ static int on_null(void* ctx) {
67
+ return rb_push_value((rb_ctx*)ctx, Qnil);
68
+ }
69
+
70
+ static int on_bool(void* ctx, int truthy) {
71
+ return rb_push_value((rb_ctx*)ctx, truthy ? Qtrue : Qfalse);
72
+ }
73
+
74
+ static int on_int(void* ctx, int64_t v) {
75
+ return rb_push_value((rb_ctx*)ctx, LL2NUM(v));
76
+ }
77
+
78
+ static int on_float(void* ctx, double v) {
79
+ return rb_push_value((rb_ctx*)ctx, DBL2NUM(v));
80
+ }
81
+
82
+ static rb_encoding* utf8_enc;
83
+
84
+ static VALUE rb_utf8_str(const char* p, size_t len) {
85
+ return rb_enc_str_new(p, (long)len, utf8_enc);
86
+ }
87
+
88
+ /* One-shot interned string (no intermediate alloc) — keys and the
89
+ * short repeated values ("a"/"b"/…) share one VALUE. */
90
+ static VALUE rb_utf8_interned(const char* p, size_t len) {
91
+ return rb_enc_interned_str(p, (long)len, utf8_enc);
92
+ }
93
+
94
+ static int on_string(void* ctx, const char* p, size_t len) {
95
+ rb_ctx* c = (rb_ctx*)ctx;
96
+ /* short strings are almost always repeated tokens in JSON corpora;
97
+ * intern them. Longer payloads stay unique. */
98
+ VALUE s = (len <= 16) ? rb_utf8_interned(p, len) : rb_utf8_str(p, len);
99
+ return rb_push_value(c, s);
100
+ }
101
+
102
+ static int on_key(void* ctx, const char* p, size_t len) {
103
+ rb_ctx* c = (rb_ctx*)ctx;
104
+ if (c->sp == 0 || !c->is_map[c->sp - 1]) {
105
+ rb_fail(c);
106
+ return -1;
107
+ }
108
+ c->keys[c->sp - 1] = rb_utf8_interned(p, len);
109
+ return 0;
110
+ }
111
+
112
+ static int on_seq_start(void* ctx) {
113
+ rb_ctx* c = (rb_ctx*)ctx;
114
+ if (c->failed || c->sp >= YEP_RB_MAX_DEPTH) {
115
+ rb_fail(c);
116
+ return -1;
117
+ }
118
+ VALUE a = rb_ary_new();
119
+ /* bind anchor to the container before placing it */
120
+ if (!NIL_P(c->pending_anchor) && !NIL_P(c->anchors)) {
121
+ rb_hash_aset(c->anchors, c->pending_anchor, a);
122
+ c->pending_anchor = Qnil;
123
+ }
124
+ if (c->sp == 0) {
125
+ c->root = a;
126
+ } else {
127
+ VALUE parent = c->stack[c->sp - 1];
128
+ if (c->is_map[c->sp - 1]) {
129
+ VALUE key = c->keys[c->sp - 1];
130
+ if (NIL_P(key)) {
131
+ rb_fail(c);
132
+ return -1;
133
+ }
134
+ rb_hash_aset(parent, key, a);
135
+ c->keys[c->sp - 1] = Qnil;
136
+ } else {
137
+ rb_ary_push(parent, a);
138
+ }
139
+ }
140
+ c->stack[c->sp] = a;
141
+ c->is_map[c->sp] = 0;
142
+ c->keys[c->sp] = Qnil;
143
+ c->sp++;
144
+ return 0;
145
+ }
146
+
147
+ static int on_seq_end(void* ctx) {
148
+ rb_ctx* c = (rb_ctx*)ctx;
149
+ if (c->sp <= 0) {
150
+ rb_fail(c);
151
+ return -1;
152
+ }
153
+ c->sp--;
154
+ return 0;
155
+ }
156
+
157
+ static int on_map_start(void* ctx) {
158
+ rb_ctx* c = (rb_ctx*)ctx;
159
+ if (c->failed || c->sp >= YEP_RB_MAX_DEPTH) {
160
+ rb_fail(c);
161
+ return -1;
162
+ }
163
+ VALUE h = rb_hash_new();
164
+ if (!NIL_P(c->pending_anchor) && !NIL_P(c->anchors)) {
165
+ rb_hash_aset(c->anchors, c->pending_anchor, h);
166
+ c->pending_anchor = Qnil;
167
+ }
168
+ if (c->sp == 0) {
169
+ c->root = h;
170
+ } else {
171
+ VALUE parent = c->stack[c->sp - 1];
172
+ if (c->is_map[c->sp - 1]) {
173
+ VALUE key = c->keys[c->sp - 1];
174
+ if (NIL_P(key)) {
175
+ rb_fail(c);
176
+ return -1;
177
+ }
178
+ rb_hash_aset(parent, key, h);
179
+ c->keys[c->sp - 1] = Qnil;
180
+ } else {
181
+ rb_ary_push(parent, h);
182
+ }
183
+ }
184
+ c->stack[c->sp] = h;
185
+ c->is_map[c->sp] = 1;
186
+ c->keys[c->sp] = Qnil;
187
+ c->sp++;
188
+ return 0;
189
+ }
190
+
191
+ static int on_map_end(void* ctx) {
192
+ rb_ctx* c = (rb_ctx*)ctx;
193
+ if (c->sp <= 0) {
194
+ rb_fail(c);
195
+ return -1;
196
+ }
197
+ c->sp--;
198
+ return 0;
199
+ }
200
+
201
+ static int on_anchor(void* ctx, const char* name, size_t len) {
202
+ rb_ctx* c = (rb_ctx*)ctx;
203
+ c->pending_anchor = rb_utf8_str(name, len);
204
+ return 0;
205
+ }
206
+
207
+ static int on_alias(void* ctx, const char* name, size_t len) {
208
+ rb_ctx* c = (rb_ctx*)ctx;
209
+ VALUE key = rb_utf8_str(name, len);
210
+ VALUE v = rb_hash_lookup2(c->anchors, key, Qundef);
211
+ if (v == Qundef) {
212
+ v = Qnil;
213
+ }
214
+ return rb_push_value(c, v);
215
+ }
216
+
217
+ static int on_doc(void* ctx) {
218
+ /* multi-doc: for load_all we'd collect; single-load takes first.
219
+ * reset root so subsequent docs replace — load_stream uses a
220
+ * different entry that accumulates. */
221
+ (void)ctx;
222
+ return 0;
223
+ }
224
+
225
+ static const YeptrisVisitVTable k_vt = {
226
+ on_null, on_bool, on_int, on_float, on_string,
227
+ on_seq_start, on_seq_end, on_map_start, on_map_end,
228
+ on_key, on_doc, on_anchor, on_alias,
229
+ };
230
+
231
+ /* fused JSON→Ruby (json_ruby.c) — no vtable, beats JSON.parse */
232
+ VALUE yep_rb_parse_json(const char* p, size_t len);
233
+
234
+ static VALUE ctx_result(rb_ctx* c, YeptrisStatus st) {
235
+ if (st != YEPTRIS_OK || c->failed) {
236
+ if (st == YEPTRIS_ERROR_PARSE) {
237
+ rb_raise(rb_path2class("Yeptris::ParseError"), "native parse failed");
238
+ }
239
+ if (st == YEPTRIS_ERROR_MEMORY) {
240
+ rb_raise(rb_eNoMemError, "yeptris native");
241
+ }
242
+ rb_raise(rb_path2class("Yeptris::Error"), "native materialize failed (%d)", (int)st);
243
+ }
244
+ return c->root;
245
+ }
246
+
247
+ static VALUE native_load_json(VALUE self, VALUE input) {
248
+ (void)self;
249
+ StringValue(input);
250
+ return yep_rb_parse_json(RSTRING_PTR(input), (size_t)RSTRING_LEN(input));
251
+ }
252
+
253
+ static VALUE native_load(VALUE self, VALUE input, VALUE schema) {
254
+ (void)self;
255
+ StringValue(input);
256
+ int sch = YEPTRIS_SCHEMA_11_COMPAT;
257
+ if (!NIL_P(schema)) {
258
+ Check_Type(schema, T_SYMBOL);
259
+ if (rb_sym2id(schema) == rb_intern("core_12")) {
260
+ sch = YEPTRIS_SCHEMA_12_CORE;
261
+ }
262
+ }
263
+ rb_ctx c;
264
+ memset(&c, 0, sizeof(c));
265
+ c.root = Qnil;
266
+ c.pending_anchor = Qnil;
267
+ c.anchors = rb_hash_new();
268
+ VALUE already = rb_gc_disable();
269
+ YeptrisStatus st = yeptris_visit(RSTRING_PTR(input), (size_t)RSTRING_LEN(input),
270
+ (YeptrisSchema)sch, &k_vt, &c);
271
+ if (already == Qfalse) {
272
+ rb_gc_enable();
273
+ }
274
+ return ctx_result(&c, st);
275
+ }
276
+
277
+ /* load_stream: accumulate documents into an Array. */
278
+ typedef struct {
279
+ rb_ctx inner;
280
+ VALUE docs;
281
+ int in_doc;
282
+ } rb_stream_ctx;
283
+
284
+ static int stream_on_doc(void* ctx) {
285
+ rb_stream_ctx* s = (rb_stream_ctx*)ctx;
286
+ if (s->in_doc && !NIL_P(s->inner.root)) {
287
+ rb_ary_push(s->docs, s->inner.root);
288
+ }
289
+ s->inner.root = Qnil;
290
+ s->inner.sp = 0;
291
+ s->in_doc = 1;
292
+ return 0;
293
+ }
294
+
295
+ static VALUE native_load_stream(VALUE self, VALUE input, VALUE schema) {
296
+ (void)self;
297
+ StringValue(input);
298
+ int sch = YEPTRIS_SCHEMA_11_COMPAT;
299
+ if (!NIL_P(schema) && rb_sym2id(schema) == rb_intern("core_12")) {
300
+ sch = YEPTRIS_SCHEMA_12_CORE;
301
+ }
302
+ rb_stream_ctx s;
303
+ memset(&s, 0, sizeof(s));
304
+ s.inner.root = Qnil;
305
+ s.inner.pending_anchor = Qnil;
306
+ s.inner.anchors = rb_hash_new();
307
+ s.docs = rb_ary_new();
308
+ YeptrisVisitVTable vt = k_vt;
309
+ vt.on_doc = stream_on_doc;
310
+ /* trick: the ctx for scalar callbacks is &s.inner, but on_doc needs
311
+ * &s. Use a unified ctx — rebind all callbacks to take stream ctx
312
+ * by making inner the first field (already is). on_doc uses outer;
313
+ * others use inner via same pointer since inner is first field. */
314
+ YeptrisStatus st = yeptris_visit(RSTRING_PTR(input), (size_t)RSTRING_LEN(input),
315
+ (YeptrisSchema)sch, &vt, &s);
316
+ if (st == YEPTRIS_OK && !s.inner.failed) {
317
+ if (!NIL_P(s.inner.root) || s.in_doc) {
318
+ rb_ary_push(s.docs, s.inner.root);
319
+ }
320
+ return s.docs;
321
+ }
322
+ return ctx_result(&s.inner, st == YEPTRIS_OK ? YEPTRIS_ERROR_INTERNAL : st);
323
+ }
324
+
325
+ RUBY_FUNC_EXPORTED void Init_native(void) {
326
+ utf8_enc = rb_utf8_encoding();
327
+ VALUE mYep = rb_define_module("Yeptris");
328
+ VALUE mNat = rb_define_module_under(mYep, "Native");
329
+ rb_define_singleton_method(mNat, "load_json", native_load_json, 1);
330
+ rb_define_singleton_method(mNat, "load", native_load, 2);
331
+ rb_define_singleton_method(mNat, "load_stream", native_load_stream, 2);
332
+ rb_define_const(mNat, "AVAILABLE", Qtrue);
333
+ }
@@ -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
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ # TODO.restructure/23 — the typed opt-in marker for arbitrary-object
4
+ # dump/load. Classes include this module and implement
5
+ # encode_with(coder) / init_with(coder) (Psych's coder protocol).
6
+ # The visitors call those methods directly; the library never uses
7
+ # respond_to?, never reads or writes instance variables from outside
8
+ # the object's public surface (the encapsulation law).
9
+ module Yeptris
10
+ module Psych
11
+ module Encodable
12
+ end
13
+ end
14
+ end
@@ -9,7 +9,7 @@ module Yeptris
9
9
  module Visitors
10
10
  # The dump-side visitor (Psych's YAMLTree core): an arbitrary
11
11
  # Ruby object graph becomes DOM nodes — anchors for shared
12
- # objects, aliases for repeats, encode_with support, and the
12
+ # objects, aliases for repeats, the Encodable protocol, and the
13
13
  # !ruby/... tags Psych's own emitter produces.
14
14
  class YAMLTree
15
15
  def initialize
@@ -33,19 +33,24 @@ module Yeptris
33
33
 
34
34
  def visit(obj)
35
35
  case obj
36
- when nil, true, false, Integer, Float, String then scalar(obj)
37
- when Symbol then scalar(":#{obj}")
38
- when Date, Time then scalar(obj) # timestamp text, never ivars
39
- when Hash then visit_hash(obj)
40
- when Array then visit_array(obj)
41
- when Struct then visit_struct(obj)
42
- when Set then visit_set(obj)
36
+ when nil, true, false, ::Integer, ::Float, ::String then scalar(obj)
37
+ when ::Symbol then scalar(":#{obj}")
38
+ when ::Date, ::Time then scalar(obj) # timestamp text, never ivars
39
+ when ::Hash then visit_hash(obj)
40
+ when ::Array then visit_array(obj)
41
+ when ::Struct then visit_struct(obj)
42
+ when ::Set then visit_set(obj)
43
+ when Encodable then visit_encode_with(obj)
43
44
  else
44
- if obj.respond_to?(:encode_with)
45
- visit_encode_with(obj)
46
- else
47
- visit_object(obj)
45
+ if obj.instance_of?(::Object)
46
+ # A BARE Object has no declared state to lose — the
47
+ # empty !ruby/object form is correct by construction
48
+ # (an Object with ivars set from outside its class is
49
+ # outside the encapsulation contract; use a real class
50
+ # with Encodable for stateful objects).
51
+ return visit_bare_object(obj)
48
52
  end
53
+ refuse_dump(obj)
49
54
  end
50
55
  end
51
56
 
@@ -79,33 +84,43 @@ module Yeptris
79
84
  obj.each_pair { |_k, v| @refs[v.object_id] += 1; count_refs(v, seen) }
80
85
  when Set
81
86
  obj.each { |v| @refs[v.object_id] += 1; count_refs(v, seen) }
82
- when String, Integer, Float, Symbol, Date, Time, true, false, nil, Numeric
87
+ when ::String, ::Integer, ::Float, ::Symbol, ::Date, ::Time, true, false, nil, ::Numeric
83
88
  # immutables: identity anchors are meaningless
84
89
  else
85
- if obj.respond_to?(:encode_with)
86
- # coder contents are opaque here; the object itself may
87
- # repeat count it from the caller side only
88
- else
89
- obj.instance_variables.each do |iv|
90
- v = obj.instance_variable_get(iv)
91
- @refs[v.object_id] += 1
92
- count_refs(v, seen)
90
+ unless obj.instance_of?(::Object)
91
+ refuse_dump(obj) unless obj.is_a?(Encodable)
92
+ # Encodable: descend through the PUBLIC protocol so
93
+ # shared objects inside coder contents still get
94
+ # anchors — encode_with must be pure (it runs once more
95
+ # at dump).
96
+ coder = ::Yeptris::Psych::CoderShim.new(nil)
97
+ obj.encode_with(coder)
98
+ case coder.type
99
+ when :seq
100
+ coder.seq.each { |v| @refs[v.object_id] += 1; count_refs(v, seen) }
101
+ when :map
102
+ coder.each { |_k, v| @refs[v.object_id] += 1; count_refs(v, seen) }
93
103
  end
94
104
  end
95
105
  end
96
106
  end
97
107
 
108
+ def refuse_dump(obj)
109
+ raise ::Yeptris::DumpError,
110
+ "cannot dump #{obj.class}: include Yeptris::Psych::Encodable and implement encode_with(coder)"
111
+ end
112
+
98
113
  def scalar(obj, tag: nil)
99
114
  # strings route through the builder's plain-safety helper
100
115
  # (one quoting rule, DRY); other scalars are their own text
101
116
  text =
102
- if obj.is_a?(Date) || obj.is_a?(Time)
117
+ if obj.is_a?(::Date) || obj.is_a?(::Time)
103
118
  obj.iso8601 # canonical timestamp form, not to_s
104
119
  else
105
120
  obj.nil? ? "null" : obj.to_s
106
121
  end
107
122
  n =
108
- if obj.is_a?(String)
123
+ if obj.is_a?(::String)
109
124
  ::Yeptris::YAML::Builder.build_string(@tree, obj)
110
125
  else
111
126
  @tree.new_scalar(text, :plain)
@@ -165,37 +180,40 @@ module Yeptris
165
180
  return alias_of(obj, name) if state == :alias
166
181
 
167
182
  coder = ::Yeptris::Psych::CoderShim.new(obj.class.name)
168
- obj.encode_with(coder)
169
- node =
170
- case coder.type
171
- when :scalar then scalar(coder.scalar, tag: coder.tag)
172
- when :seq
173
- s = @tree.new_sequence
174
- s.set_tag(coder.tag) if coder.tag
175
- coder.seq.each { |e| s.seq_add(visit(e)) }
176
- s
177
- else
178
- m = @tree.new_mapping
179
- m.set_tag(coder.tag) if coder.tag
180
- coder.each { |k, v| m.map_add(key_text(k), visit(v)) }
181
- m
182
- end
183
- remember(obj, node)
184
- node.set_anchor(name) if name
185
- node
183
+ obj.encode_with(coder) # the typed public surface
184
+ # Wrap coder contents in a __init__ sub-mapping so the load
185
+ # side can dispatch back into obj.init_with(coder) without
186
+ # library-side ivar reflection (the encapsulation law).
187
+ init = @tree.new_mapping
188
+ case coder.type
189
+ when :scalar
190
+ init.map_add("__scalar__", scalar(coder.scalar))
191
+ init.map_add("__tag__", scalar(coder.tag)) if coder.tag
192
+ when :seq
193
+ inner = @tree.new_sequence
194
+ inner.set_tag(coder.tag) if coder.tag
195
+ coder.seq.each { |e| inner.seq_add(visit(e)) }
196
+ init.seq_add(inner)
197
+ else
198
+ coder.each { |k, v| init.map_add(key_text(k), visit(v)) }
199
+ init.set_tag(coder.tag) if coder.tag
200
+ end
201
+ m = @tree.new_mapping
202
+ m.set_anchor(name) if name
203
+ m.set_tag("!ruby/object:#{obj.class.name}")
204
+ m.map_add("__init__", init)
205
+ remember(obj, m)
206
+ m
186
207
  end
187
208
 
188
- def visit_object(obj)
209
+ def visit_bare_object(obj)
189
210
  state, name = anchor_for(obj)
190
211
  return alias_of(obj, name) if state == :alias
191
212
 
192
213
  m = @tree.new_mapping
193
214
  remember(obj, m)
194
215
  m.set_anchor(name) if name
195
- m.set_tag("!ruby/object:#{obj.class.name}")
196
- obj.instance_variables.each do |ivar|
197
- m.map_add(ivar.to_s, visit(obj.instance_variable_get(ivar)))
198
- end
216
+ m.set_tag("!ruby/object")
199
217
  m
200
218
  end
201
219
 
@@ -322,30 +340,31 @@ module Yeptris
322
340
  end
323
341
 
324
342
  def revive_object(klass, node)
325
- obj = klass ? klass.allocate : ::Object.new
343
+ # bare !ruby/object (no class): Psych.dump(Object.new)'s form.
344
+ # An Object has no declared state — allocation is the whole
345
+ # revival (no reflection, no protocol needed).
346
+ return ::Object.allocate if klass.nil?
347
+
348
+ raise ::Yeptris::DumpError, "#{klass} is not Yeptris::Psych::Encodable: implement init_with(coder)" unless klass <= ::Yeptris::Psych::Encodable
349
+
350
+ obj = klass.allocate
326
351
  anchors[node.anchor] = obj if node.anchor
352
+ # The class opts in via init_with(coder); the encoder wrote
353
+ # `coder["__init__"] = the mapping` so we can pass it back
354
+ # without library-side ivar reflection.
327
355
  node.children.each_slice(2) do |k, v|
328
356
  key = k.to_ruby.to_s
329
- if key == "__init__"
330
- init_with(obj, v)
331
- else
332
- ivar = key.start_with?("@") ? key.to_sym : "@#{key}".to_sym
333
- obj.instance_variable_set(ivar, visit(v))
334
- end
335
- end
336
- obj
337
- end
357
+ next unless key == "__init__"
338
358
 
339
- def init_with(obj, value_node)
340
- return unless obj.respond_to?(:init_with)
341
-
342
- coder = ::Yeptris::Psych::CoderShim.new
343
- if value_node.is_a?(::Yeptris::Psych::Nodes::Mapping)
344
- value_node.children.each_slice(2) do |k, v|
345
- coder[k.to_ruby.to_s] = visit(v)
359
+ coder = ::Yeptris::Psych::CoderShim.new
360
+ if v.is_a?(::Yeptris::Psych::Nodes::Mapping)
361
+ v.children.each_slice(2) do |ck, cv|
362
+ coder[ck.to_ruby.to_s] = visit(cv)
363
+ end
346
364
  end
365
+ obj.init_with(coder)
347
366
  end
348
- obj.init_with(coder)
367
+ obj
349
368
  end
350
369
 
351
370
  def resolve_class(name)
data/lib/yeptris/psych.rb CHANGED
@@ -1,5 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "date"
4
+ require "time"
5
+ require "set"
6
+
3
7
  # The Psych drop-in namespace (TODO.impl/15 phase C).
4
8
  #
5
9
  # `require "yeptris/psych"` rebinds the top-level Psych constant to
@@ -20,6 +24,11 @@ module Yeptris
20
24
  autoload :Parser, "yeptris/psych/parser"
21
25
  autoload :CoderShim, "yeptris/psych/coder_shim"
22
26
  autoload :Visitors, "yeptris/psych/visitors"
27
+ # The typed opt-in marker for arbitrary-object dump/load
28
+ # (TODO.restructure/23). Eager by intent: classes include it at
29
+ # declaration time, so the autoload must resolve before any
30
+ # object instance exists.
31
+ autoload :Encodable, "yeptris/psych/encodable"
23
32
  class Error < StandardError; end
24
33
  class SyntaxError < Error
25
34
  attr_reader :line, :column
@@ -106,7 +115,7 @@ module Yeptris
106
115
  # data anyway
107
116
  out =
108
117
  case obj
109
- when nil, true, false, String, Integer, Float, Symbol, Date, Time
118
+ when nil, true, false, ::String, ::Integer, ::Float, ::Symbol, ::Date, ::Time
110
119
  Yeptris::YAML.dump(obj)
111
120
  else
112
121
  Visitors::YAMLTree.new.push(obj).finish
data/lib/yeptris/yaml.rb CHANGED
@@ -10,13 +10,22 @@ 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)
19
+ yaml = Yeptris.read_input(yaml)
20
+ yaml = yaml.to_s
14
21
  docs = _drain_all(yaml, schema)
15
22
  docs.empty? ? nil : docs.first
16
23
  end
17
24
 
18
25
  # Every document in the stream, in order.
19
26
  def load_stream(yaml, schema: :compat_11)
27
+ yaml = Yeptris.read_input(yaml)
28
+ yaml = yaml.to_s
20
29
  _drain_all(yaml, schema)
21
30
  end
22
31
 
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.11.0".freeze
6
+ VERSION = "0.1.13.0".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"
@@ -58,3 +59,12 @@ rescue LoadError => e
58
59
  (Underlying error: #{e.message})
59
60
  MSG
60
61
  end
62
+
63
+ # Optional C-API materializer (TODO.restructure/22): fused visit →
64
+ # Ruby objects via the Ruby C API. Feature-detected — LoadError leaves
65
+ # the FFI ladder (Marshal → columns → records) as the sole path.
66
+ begin
67
+ require "yeptris/native"
68
+ rescue LoadError
69
+ # FFI ladder only
70
+ end
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.11.0
4
+ version: 0.1.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -24,9 +24,9 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '1.15'
27
- description: An FFI-based (no C extension) Ruby YAML library over libyeptris — Psych-compatible
28
- semantics with libleptris-class performance. The neutral Yeptris::YAML surface ships
29
- first; the Psych drop-in namespace lands with the recorder-driven Visitors.
27
+ description: A Ruby YAML library over libyeptris — Psych-compatible semantics with
28
+ libleptris-class performance, and a fused native JSON materializer that outperforms
29
+ JSON.parse on JSON-shaped input.
30
30
  email:
31
31
  - open.source@ribose.com
32
32
  executables: []
@@ -34,13 +34,18 @@ extensions: []
34
34
  extra_rdoc_files: []
35
35
  files:
36
36
  - README.adoc
37
+ - ext/yeptris_native/extconf.rb
38
+ - ext/yeptris_native/json_ruby.c
39
+ - ext/yeptris_native/yeptris_native.c
37
40
  - lib/yeptris.rb
38
41
  - lib/yeptris/document.rb
39
42
  - lib/yeptris/ffi.rb
43
+ - lib/yeptris/json.rb
40
44
  - lib/yeptris/materializer.rb
41
45
  - lib/yeptris/node.rb
42
46
  - lib/yeptris/psych.rb
43
47
  - lib/yeptris/psych/coder_shim.rb
48
+ - lib/yeptris/psych/encodable.rb
44
49
  - lib/yeptris/psych/handler.rb
45
50
  - lib/yeptris/psych/parser.rb
46
51
  - lib/yeptris/psych/visitors.rb