yeptris 0.6.20.2-aarch64-linux → 0.6.21.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9f38d811bfb5d528f0fe714a37d5b1d3098c4387b4e17ddc4f5ceb1b1d4e792d
4
- data.tar.gz: 97317d6f12a73d35fa2b38e780742631729823daa9f6e87f9af8a6cb30673791
3
+ metadata.gz: 5d1fa4e8d3637757def7e98646b997efcf9f34e5b2335e6204200233bac8513a
4
+ data.tar.gz: 72a73f4d7545cdb8bbf4c276833675805ca331e7d7327954418207c24c32eaf8
5
5
  SHA512:
6
- metadata.gz: 9d2d9c6092f09c30e2db903e8ffd1c43c16c5c146a0696732c3aa6393d510cfd9f8dc30abbdd7e8d51f7798b0f7b088227b10d60731350ca872061364b95915a
7
- data.tar.gz: c2b04861f13e33c47a90387e909c4179d846888a640bd8c754e9bdcd59a45e4648d4d6e7ee8edd0c8944935d044fd2fd5beea9ff497d85f6697c5433cbbe25f3
6
+ metadata.gz: b4ac43d4cf4e3bc79c97cc0e617760d5ce0f7a14bd6eae195b9cd13fe801c7a146175ef3786f5523de51499827bc52b7daf60b08920e21a8ed4b18ca08e5fded
7
+ data.tar.gz: e24ea1b81fc184268f6ee5a88464c2d91d9f4ad17ac36c84aa193de972fc0f9dd5359ff6081f43ebfd6dc1145fa5f3dc50ba794161cb4c8436c09d61cb770dce
Binary file
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.6.20.2".freeze
6
+ VERSION = "0.6.21.1".freeze
7
7
 
8
8
  # Informational notices are OPT-IN (yeptris-ruby#217): consumers
9
9
  # embed yeptris inside CLIs that assert clean stderr, and a
data/libyeptris.so CHANGED
Binary file
@@ -3,7 +3,7 @@
3
3
  cmake_minimum_required(VERSION 3.20)
4
4
 
5
5
  project(yeptris
6
- VERSION 0.6.20
6
+ VERSION 0.6.21
7
7
  DESCRIPTION "Ultra-fast YAML 1.2 parser, emitter and streamer in C"
8
8
  LANGUAGES C CXX
9
9
  )
@@ -31,6 +31,7 @@ YEPTRIS_API YeptrisDocument yeptris_document_new(void) {
31
31
  yep_dom_destroy(dom);
32
32
  return NULL;
33
33
  }
34
+ yep_mutex_init(&doc->lazy_mu);
34
35
  memset(doc, 0, sizeof(*doc));
35
36
  doc->dom = dom;
36
37
  doc->sys = sys;
@@ -860,6 +860,7 @@ static YeptrisDocument cbor_wrap(yep_dom* dom, const void* buf, const yep_alloca
860
860
  yep_dom_destroy(dom);
861
861
  return NULL;
862
862
  }
863
+ yep_mutex_init(&d->lazy_mu);
863
864
  d->dom = dom;
864
865
  d->sys = sys;
865
866
  d->schema = YEPTRIS_SCHEMA_12_CORE;
@@ -967,6 +968,7 @@ YEPTRIS_API YeptrisDocument yeptris_cbor_decode(const void* buf, size_t len, uin
967
968
  }
968
969
  return NULL;
969
970
  }
971
+ yep_mutex_init(&doc->lazy_mu);
970
972
  doc->dom = dom;
971
973
  doc->sys = sys;
972
974
  doc->schema = YEPTRIS_SCHEMA_12_CORE; /* CBOR's model is the core schema */
@@ -7,6 +7,7 @@
7
7
 
8
8
  #include <stddef.h>
9
9
 
10
+ #include "common/mutex.h"
10
11
  #include "dom/dom.h"
11
12
  #include "memory/allocator.h"
12
13
 
@@ -18,19 +19,24 @@ typedef struct yeptris_document {
18
19
  * conditioned on it (TODO.restructure/32) */
19
20
  unsigned char* transcoded; /* owned when non-NULL */
20
21
  size_t transcoded_len;
21
- const char* input; /* borrowed input (lifetime documentation) */
22
- void* finish_pool; /* engine finish pool: resolved tags, folded and
23
- escaped scalars outlive the engine through the
24
- document */
25
- void* lazy_tape; /* #342 slice 2: the parsed tape when the tree is
26
- * deferred (gate-clean strict JSON; #378: the YAML
27
- * record tape). dom==NULL until the first tree
28
- * access materializes; freed with the document.
29
- * void* to avoid a header cycle — parse.c casts
30
- * per lazy_kind */
31
- int lazy_kind; /* 0 = none/json (yeptris_json_tape), 1 = yaml
32
- * (yep_ytape) — every constructor must set it
33
- * (the field-by-field ctor trap) */
22
+ const char* input; /* borrowed input (lifetime documentation) */
23
+ void* finish_pool; /* engine finish pool: resolved tags, folded and
24
+ escaped scalars outlive the engine through the
25
+ document */
26
+ void* lazy_tape; /* #342 slice 2: the parsed tape when the tree is
27
+ * deferred (gate-clean strict JSON; #378: the YAML
28
+ * record tape). dom==NULL until the first tree
29
+ * access materializes; freed with the document.
30
+ * void* to avoid a header cycle — parse.c casts
31
+ * per lazy_kind */
32
+ int lazy_kind; /* 0 = none/json (yeptris_json_tape), 1 = yaml
33
+ * (yep_ytape) — every constructor must set it
34
+ * (the field-by-field ctor trap) */
35
+ yep_mutex_raw lazy_mu; /* materialization lock: the first tree
36
+ * access builds; concurrent first accesses
37
+ * wait (read-only sharing stays lock-free
38
+ * once dom is set — every constructor must
39
+ * yep_mutex_init it, the ctor trap) */
34
40
  } yeptris_document;
35
41
 
36
42
  /* Node handle: a (document, node-id) pair so nodes stay usable even if
@@ -542,18 +542,18 @@ uint32_t dom_indexed_child(yep_dom* d, uint32_t cid, size_t index, uint32_t* cou
542
542
  *count_out = 0;
543
543
  return UINT32_MAX;
544
544
  }
545
+ /* the midx discipline (TSAN, Threads.ReadOnlySharing): READS of
546
+ * the cache ride the mutex too — a concurrent builder publishes
547
+ * under it, so the old unlocked hit path raced the build's
548
+ * stores. Uncontended in the single-threaded hot path. */
549
+ yep_mutex_lock(&d->midx.mu);
550
+ uint32_t out;
545
551
  *count_out = n->count;
546
552
  if (d->child_cache_id == cid) {
547
- if (index < d->child_cache_len) {
548
- return d->child_cache[index];
549
- }
550
- return UINT32_MAX;
551
- }
552
- /* miss: build once under the lazy-init mutex (Threads.
553
- * ReadOnlySharing — concurrent first calls race here exactly like
554
- * the handle pool's) */
555
- yep_mutex_lock(&d->midx.mu);
556
- if (d->child_cache_id != cid) {
553
+ out = index < d->child_cache_len ? d->child_cache[index] : UINT32_MAX;
554
+ } else {
555
+ /* miss: build once under the lazy-init mutex (concurrent
556
+ * first calls race here exactly like the handle pool's) */
557
557
  yep_free(d->sys, d->child_cache);
558
558
  d->child_cache = NULL;
559
559
  d->child_cache_len = 0;
@@ -574,12 +574,10 @@ uint32_t dom_indexed_child(yep_dom* d, uint32_t cid, size_t index, uint32_t* cou
574
574
  }
575
575
  }
576
576
  d->child_cache_id = cid;
577
+ out = index < d->child_cache_len ? d->child_cache[index] : UINT32_MAX;
577
578
  }
578
579
  yep_mutex_unlock(&d->midx.mu);
579
- if (index < d->child_cache_len) {
580
- return d->child_cache[index];
581
- }
582
- return UINT32_MAX;
580
+ return out;
583
581
  }
584
582
 
585
583
  void yep_dom_destroy(yep_dom* d) {
@@ -14,6 +14,7 @@
14
14
  * (test/unit/test_ytape.cpp) pins tree identity across the corpora.
15
15
  */
16
16
 
17
+ #include <stdio.h>
17
18
  #include <string.h>
18
19
 
19
20
  #include "dom/dom.h"
@@ -327,6 +328,7 @@ static int yt_on_flow_build(void* ctx, const char* p, size_t open, size_t len, u
327
328
  * the same reject/long-key verdicts, then ONE record for the
328
329
  * whole staged span (replay runs the full build through
329
330
  * dom_on_flow_build). */
331
+ t->max_depth = max_depth; /* replay re-walks at the engine's limit */
330
332
  yep_json_walk w;
331
333
  yep_json_tok tok;
332
334
  yep_json_walk_init(&w, p, len, open, max_depth);
@@ -756,14 +756,19 @@ YEPTRIS_API YeptrisStatus yeptris_marshal_node(YeptrisNode node, char** out, siz
756
756
  *out = NULL;
757
757
  *out_len = 0;
758
758
  yeptris_node* h = (yeptris_node*)node;
759
- if (dom_subtree_tagged(h->doc->dom, h->id) != 0) {
759
+ yep_dom* dom = yep_doc_dom(h->doc); /* #378: a lazy parse carries the
760
+ * tape — the tree materializes here */
761
+ if (dom == NULL) {
762
+ return YEPTRIS_ERROR_MEMORY; /* the lazy route's retry contract */
763
+ }
764
+ if (dom_subtree_tagged(dom, h->id) != 0) {
760
765
  yep_error_set(yep_error_tls(), YEP_ERR_UNEXPECTED, 0, 0, 0,
761
766
  "marshal: explicitly tagged node not expressible; "
762
767
  "fall back to the value walk");
763
768
  return YEPTRIS_ERROR_UNSUPPORTED;
764
769
  }
765
770
  yep_value_ctx* c = NULL;
766
- if (yep_values_from_dom(h->doc->dom, h->id, 0, &c) != 0) {
771
+ if (yep_values_from_dom(dom, h->id, 0, &c) != 0) {
767
772
  return YEPTRIS_ERROR_MEMORY;
768
773
  }
769
774
  YeptrisStatus st = marshal_records(c, h->doc->schema == YEPTRIS_SCHEMA_11_COMPAT,
@@ -48,89 +48,57 @@ yep_dom* yep_doc_dom(yeptris_document* doc) {
48
48
  if (doc == NULL) {
49
49
  return NULL;
50
50
  }
51
- if (doc->dom == NULL && doc->lazy_tape != NULL && doc->lazy_kind == 1) {
52
- /* #378: the YAML record tape — replay through the eager
53
- * builders; the finish pool (whose spans the records carry)
54
- * dies once the builders copied what must outlive it */
55
- yep_ytape* t = (yep_ytape*)doc->lazy_tape;
56
- yep_dom* dom = yep_dom_create(doc->sys);
57
- if (dom != NULL) {
58
- dom->resolver = (doc->schema == YEPTRIS_SCHEMA_11_COMPAT) ? yep_resolver_compat11()
59
- : yep_resolver_core12();
60
- }
61
- if (dom != NULL && dom_from_ytape(dom, t) == 0) {
62
- doc->dom = dom;
63
- ytap_free(t);
64
- yep_free(doc->sys, t);
65
- doc->lazy_tape = NULL;
66
- yep_pool_destroy((yep_pool*)doc->finish_pool);
67
- doc->finish_pool = NULL;
68
- } else {
69
- yep_dom_destroy(dom);
70
- dom = NULL; /* the tape stays: a later access retries, or
71
- * free drops it (replay fails only on
72
- * allocation/depth, as the builders do) */
73
- }
74
- return dom;
75
- }
51
+ /* the midx discipline (the #157 TSAN lesson): EVERY dom read and
52
+ * write rides the mutex — the unlock publishes the built tree
53
+ * (readers get their happens-before through the lock), so the
54
+ * plain field needs no atomics. Entry-level only: node walks ride
55
+ * handles created after this returns. */
56
+ yep_mutex_lock(&doc->lazy_mu);
76
57
  if (doc->dom == NULL && doc->lazy_tape != NULL) {
77
- yeptris_json_tape* t = (yeptris_json_tape*)doc->lazy_tape;
78
- yep_dom* dom = yep_dom_create(doc->sys);
79
- if (dom != NULL && dom_from_tape(dom, t) == 0) {
80
- doc->dom = dom;
81
- yeptris_tape_free(t);
82
- yep_free(doc->sys, t);
83
- doc->lazy_tape = NULL;
84
- } else {
85
- yep_dom_destroy(dom);
86
- dom = NULL; /* the tape stays: a later access retries, or
87
- free drops it (the materializer only fails on
88
- malformed NUM spans, which the strict gate
89
- already rejected at parse) */
90
- }
91
- return dom;
92
- }
93
- return doc->dom;
94
- }
95
-
96
- /* The strict-JSON document wrapper (both routes share it). */
97
- /* The lazy route's acceptance debt: the fused lenient walk records
98
- * number runs UNVALIDATED (its simdjson-style deferred contract).
99
- * parse_json's contract is RFC 8259 at parse time, so the deferred
100
- * spans settle HERE — full-span shape check per NUM record, decoding
101
- * the packed records directly (no columns build at parse; the walk
102
- * guarantees recs are primary on this route). */
103
- static int lazy_nums_settled(const yeptris_json_tape* t) {
104
- const char* p = (const char*)t->_src;
105
- for (uint32_t i = 0; i < t->count; i++) {
106
- uint64_t r = t->recs[i];
107
- if ((uint8_t)(r & 0xFFu) != YEP_T_NUM) {
108
- continue;
109
- }
110
- uint32_t len = (uint32_t)((r >> 8) & 0x7FFFFFu);
111
- uint32_t off = (uint32_t)(r >> 32);
112
- { /* the pure-integer fast path: all digits (optional '-'), no
113
- * leading zero unless a lone digit — number_shape would
114
- * consume the whole span and answer float-less; skip it */
115
- const char* q = p + off;
116
- size_t d = (*q == '-') ? 1 : 0;
117
- size_t w = d;
118
- for (; w < len; w++) {
119
- if ((unsigned)(unsigned char)q[w] - '0' > 9u) {
120
- break;
121
- }
58
+ if (doc->lazy_kind == 1) {
59
+ /* #378: the YAML record tape — replay through the eager
60
+ * builders; the finish pool (whose spans the records
61
+ * carry) dies once the builders copied what must outlive
62
+ * it */
63
+ yep_ytape* t = (yep_ytape*)doc->lazy_tape;
64
+ yep_dom* dom = yep_dom_create(doc->sys);
65
+ if (dom != NULL) {
66
+ dom->resolver = (doc->schema == YEPTRIS_SCHEMA_11_COMPAT) ? yep_resolver_compat11()
67
+ : yep_resolver_core12();
122
68
  }
123
- if (w == len && len > d && (len - d == 1 || q[d] != '0')) {
124
- continue;
69
+ if (dom != NULL && dom_from_ytape(dom, t) == 0) {
70
+ doc->dom = dom;
71
+ ytap_free(t);
72
+ yep_free(doc->sys, t);
73
+ doc->lazy_tape = NULL;
74
+ yep_pool_destroy((yep_pool*)doc->finish_pool);
75
+ doc->finish_pool = NULL;
76
+ } else {
77
+ yep_dom_destroy(dom);
78
+ /* the tape stays: a later access retries, or free
79
+ * drops it (replay fails only on allocation/depth, as
80
+ * the builders do) */
81
+ }
82
+ } else {
83
+ yeptris_json_tape* t = (yeptris_json_tape*)doc->lazy_tape;
84
+ yep_dom* dom = yep_dom_create(doc->sys);
85
+ if (dom != NULL && dom_from_tape(dom, t) == 0) {
86
+ doc->dom = dom;
87
+ yeptris_tape_free(t);
88
+ yep_free(doc->sys, t);
89
+ doc->lazy_tape = NULL;
90
+ } else {
91
+ yep_dom_destroy(dom);
92
+ /* the tape stays: a later access retries, or free
93
+ * drops it (the materializer only fails on malformed
94
+ * NUM spans, which the strict gate already rejected at
95
+ * parse) */
125
96
  }
126
- }
127
- size_t adv = 0;
128
- int flt = 0;
129
- if (yep_json_number_shape(p + off, len, &adv, &flt) == 0 || adv != (size_t)len) {
130
- return -1;
131
97
  }
132
98
  }
133
- return 0;
99
+ yep_dom* dom = doc->dom;
100
+ yep_mutex_unlock(&doc->lazy_mu);
101
+ return dom;
134
102
  }
135
103
 
136
104
  /* The strict-JSON document wrapper (both routes share it). */
@@ -145,6 +113,7 @@ static YeptrisDocument yep_json_doc_wrap(yep_dom* dom, const char* buf, size_t l
145
113
  }
146
114
  return NULL;
147
115
  }
116
+ yep_mutex_init(&doc->lazy_mu);
148
117
  doc->dom = dom;
149
118
  doc->sys = sys;
150
119
  doc->lazy_tape = NULL;
@@ -166,40 +135,31 @@ YEPTRIS_API YeptrisDocument yeptris_parse_json(const char* buf, size_t len, Yept
166
135
  st = YEPTRIS_ERROR_ARG;
167
136
  goto jfail;
168
137
  }
169
- /* Clean-gate fast route (TODO.restructure/81 stage 2): a gate-clean
170
- * buffer is printable ASCII, and the FUSED builder's walk runs
171
- * STRICT here (dom->flow_strict) — one pass validates RFC 8259 and
172
- * builds. Rejects, non-opener roots, and surprises fall to the
173
- * original sequence below, whose error precedence is byte-for-byte
174
- * the pinned behavior (json-suite-strict gates this). */
175
- int gated = len > 0 && yep_text_active()->gate_scan(buf, len);
176
- if (!gated) {
138
+ /* Clean-walk fast route (TODO.restructure/81 stage 2, #342): the
139
+ * fused walk runs clean_only — tabs and non-ASCII bytes REJECT —
140
+ * and ANY reject falls to the original sequence below, whose error
141
+ * precedence is byte-for-byte the pinned behavior
142
+ * (json-suite-strict gates this). The entry's full-input gate_scan
143
+ * + tab-memchr pre-passes are GONE: the walk's own byte guarantees
144
+ * route identically by construction, and the strict path re-gates
145
+ * only when actually taken (the profile's 5% + 2% lanes). */
146
+ {
177
147
  size_t off = 0;
178
148
  while (off < len &&
179
149
  (buf[off] == ' ' || buf[off] == '\t' || buf[off] == '\n' || buf[off] == '\r')) {
180
150
  off++;
181
151
  }
182
- /* tabs: the lenient walk's own pinned acceptance (its route
183
- * takes them); the strict routes reject them (ErrorParity's
184
- * pinned agreement). A tab-carrying buffer must NOT take the
185
- * lazy walk — the validating sequence below reports exactly
186
- * the pinned reject. */
187
- if (off < len && (buf[off] == '[' || buf[off] == '{') && memchr(buf, '\t', len) == NULL) {
188
- /* #342 slice 2: the fused LENIENT walk (one pass, records
189
- * only — no node building) settles the deferred number
190
- * grammar inline, then the tape rides the document and
191
- * dom_from_tape builds nodes on the first tree access.
192
- * A reject or malformed span falls to the original
193
- * sequence below, whose error precedence is byte-for-byte
194
- * the pinned behavior. */
152
+ if (off < len && (buf[off] == '[' || buf[off] == '{')) {
195
153
  const yep_allocator* sys = yep_system_allocator();
196
154
  yeptris_json_tape* t = yep_alloc(sys, sizeof(*t));
197
155
  if (t == NULL) {
198
156
  st = YEPTRIS_ERROR_MEMORY;
199
157
  goto jfail;
200
158
  }
201
- if (yep_tape_walk_lenient_fused(buf, len, off, t) == YEPTRIS_OK &&
202
- lazy_nums_settled(t) == 0) {
159
+ if (yep_tape_walk_lenient_fused(buf, len, off, t, 1, 1) == YEPTRIS_OK) {
160
+ /* strict_nums=1: the arms validated the numbers at
161
+ * record time — the settle pass is gone (the profile's
162
+ * 10% lane; the spans were cache-warm in the walk) */
203
163
  YeptrisDocument h = yep_json_doc_wrap(NULL, buf, len, sys, status);
204
164
  if (h != NULL) {
205
165
  ((yeptris_document*)h)->lazy_tape = t;
@@ -208,7 +168,8 @@ YEPTRIS_API YeptrisDocument yeptris_parse_json(const char* buf, size_t len, Yept
208
168
  }
209
169
  yeptris_tape_free(t); /* wrap failed (memory): below */
210
170
  } else {
211
- yeptris_tape_free(t); /* reject/malformed: below */
171
+ yeptris_tape_free(t); /* reject (incl. clean-route tabs
172
+ and non-ASCII): below */
212
173
  }
213
174
  yep_free(sys, t);
214
175
  }
@@ -220,6 +181,9 @@ YEPTRIS_API YeptrisDocument yeptris_parse_json(const char* buf, size_t len, Yept
220
181
  st = YEPTRIS_ERROR_PARSE;
221
182
  goto jfail;
222
183
  }
184
+ /* the fallback re-gates: a buffer here was walk-rejected, so its
185
+ * printability decides whether UTF-8 validation is owed */
186
+ int gated = len > 0 && yep_text_active()->gate_scan(buf, len);
223
187
  size_t uerr = 0;
224
188
  if (gated && !yep_utf8_validate((const unsigned char*)buf, len, &uerr)) {
225
189
  yep_error_set(yep_error_tls(), YEP_ERR_ENCODING, 0, 0, uerr, "ill-formed UTF-8 at byte %zu",
@@ -370,9 +334,11 @@ engine_enter:
370
334
  dom->input_base = transcoded ? (const char*)transcoded : buf;
371
335
  yep_dom_prepare_len(dom, data_len);
372
336
  }
373
- {
337
+ if (!json_mode) {
374
338
  /* the nametab reserve survives: a memchr chain for '&' is
375
- * far cheaper than the class sweep it replaced */
339
+ * far cheaper than the class sweep it replaced. JSON skips it
340
+ * entirely — no anchors exist (the profile's 4.4% memchr
341
+ * lane on the json corpora). */
376
342
  yep_text_stats amp_only = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
377
343
  const char* q = data;
378
344
  const char* end = data + data_len;
@@ -465,6 +431,7 @@ engine_enter:
465
431
  st = YEPTRIS_ERROR_MEMORY;
466
432
  goto fail;
467
433
  }
434
+ yep_mutex_init(&doc->lazy_mu);
468
435
  doc->sys = sys;
469
436
  doc->schema = (opts != NULL && opts->schema == YEPTRIS_SCHEMA_11_COMPAT)
470
437
  ? YEPTRIS_SCHEMA_11_COMPAT
@@ -504,7 +471,11 @@ fail:
504
471
  YEPTRIS_API YeptrisDocument yeptris_parse_ex(const char* buf, size_t len,
505
472
  const YeptrisParseOptions* opts,
506
473
  YeptrisStatus* status) {
507
- return parse_impl(buf, len, opts, 0, 0, status);
474
+ /* #378 slice 2: the default YAML route carries the packed record
475
+ * tape — the tree materializes on first access (yep_doc_dom).
476
+ * Same grammar, same errors; parse-only workloads never build
477
+ * nodes. The eager form stays on yeptris_parse_eager_ex. */
478
+ return parse_impl(buf, len, opts, 0, 1, status);
508
479
  }
509
480
 
510
481
  /* #378 slice 1 (internal seam): the YAML parse rides the packed record
@@ -515,6 +486,13 @@ YeptrisDocument yeptris_parse_ytape_ex(const char* buf, size_t len, const Yeptri
515
486
  return parse_impl(buf, len, opts, 0, 1, status);
516
487
  }
517
488
 
489
+ /* The eager form, kept for the parse-only A/B (bench_matrix's #378
490
+ * table) and as the reference lane the differential gates ride. */
491
+ YeptrisDocument yeptris_parse_eager_ex(const char* buf, size_t len, const YeptrisParseOptions* opts,
492
+ YeptrisStatus* status) {
493
+ return parse_impl(buf, len, opts, 0, 0, status);
494
+ }
495
+
518
496
  YEPTRIS_API void yeptris_document_free(YeptrisDocument handle) {
519
497
  yeptris_document* doc = (yeptris_document*)handle;
520
498
  if (doc == NULL) {
@@ -528,6 +506,7 @@ YEPTRIS_API void yeptris_document_free(YeptrisDocument handle) {
528
506
  }
529
507
  yep_free(doc->sys, doc->lazy_tape);
530
508
  }
509
+ yep_mutex_destroy(&doc->lazy_mu);
531
510
  yep_dom_destroy(doc->dom);
532
511
  yep_pool_destroy((yep_pool*)doc->finish_pool);
533
512
  yep_free(doc->sys, doc->transcoded);
@@ -536,7 +515,10 @@ YEPTRIS_API void yeptris_document_free(YeptrisDocument handle) {
536
515
 
537
516
  YEPTRIS_API size_t yeptris_document_count(YeptrisDocument handle) {
538
517
  yeptris_document* doc = (yeptris_document*)handle;
539
- return doc ? yep_doc_dom(doc)->dcount : 0;
518
+ yep_dom* dom = doc ? yep_doc_dom(doc) : NULL; /* NULL: the lazy
519
+ * route's failed/retrying materialization — an
520
+ * unusable tree counts as zero documents */
521
+ return dom ? dom->dcount : 0;
540
522
  }
541
523
 
542
524
  yeptris_node* yep_handle_new(yeptris_document* doc, uint32_t id) {
@@ -601,8 +601,54 @@ reject:
601
601
  * (the classify scan carries no accept/reject structure) and
602
602
  * yeptris_tape_convert owns validation. Everything else matches
603
603
  * tape_walk state for state. */
604
+ /* The strict RFC 8259 number check over one number-ish run — the
605
+ * in-walk float/exp validator (the out-of-line number_shape call +
606
+ * rescan was 13% of json-doc; the digits-only case never gets here).
607
+ * One pass, no state machine words: sign, digits (leading-zero rule),
608
+ * optional .digits, optional e[+-]digits, consume exactly len. */
609
+ static int tape_num_ok(const char* p, size_t len) {
610
+ size_t k = 0;
611
+ if (k < len && p[k] == '-') {
612
+ k++;
613
+ }
614
+ size_t int_start = k;
615
+ while (k < len && (unsigned)(unsigned char)p[k] - '0' <= 9u) {
616
+ k++;
617
+ }
618
+ if (k == int_start) {
619
+ return 0; /* no integer part */
620
+ }
621
+ if (k - int_start > 1 && p[int_start] == '0') {
622
+ return 0; /* leading zero */
623
+ }
624
+ if (k < len && p[k] == '.') {
625
+ k++;
626
+ size_t frac = k;
627
+ while (k < len && (unsigned)(unsigned char)p[k] - '0' <= 9u) {
628
+ k++;
629
+ }
630
+ if (k == frac) {
631
+ return 0;
632
+ }
633
+ }
634
+ if (k < len && (p[k] == 'e' || p[k] == 'E')) {
635
+ k++;
636
+ if (k < len && (p[k] == '+' || p[k] == '-')) {
637
+ k++;
638
+ }
639
+ size_t exp = k;
640
+ while (k < len && (unsigned)(unsigned char)p[k] - '0' <= 9u) {
641
+ k++;
642
+ }
643
+ if (k == exp) {
644
+ return 0;
645
+ }
646
+ }
647
+ return k == len;
648
+ }
649
+
604
650
  YeptrisStatus yep_tape_walk_lenient_fused(const char* p, size_t len, size_t open,
605
- yeptris_json_tape* t) {
651
+ yeptris_json_tape* t, int strict_nums, int clean_only) {
606
652
  if (tape_carve(t, len) != YEPTRIS_OK) {
607
653
  return YEPTRIS_ERROR_MEMORY;
608
654
  }
@@ -624,11 +670,24 @@ YeptrisStatus yep_tape_walk_lenient_fused(const char* p, size_t len, size_t open
624
670
  kind[0] = top_kind;
625
671
 
626
672
  size_t i = open + 1;
673
+ /* clean_only (parse_json's route): tabs and non-ASCII bytes REJECT
674
+ * — the caller falls to the strict sequence, which owns the pinned
675
+ * error precedences those inputs carry (the tab reject, the UTF-8
676
+ * gate). This replaces the entry's full-input gate_scan +
677
+ * tab-memchr pre-passes: same routing by construction, zero scans
678
+ * before the walk. */
679
+ int cl = clean_only;
627
680
  if (top_kind) {
628
681
  goto lmap1;
629
682
  }
630
683
  goto lseq1;
631
684
 
685
+ /* strict_nums: the parse_json route's contract is RFC 8259 AT
686
+ * parse time, so its number arms validate the run in-place (the
687
+ * settle pass disappears — the span is cache-warm here); the
688
+ * standalone lenient entry keeps its deferred contract (0: the
689
+ * run records unvalidated, yeptris_tape_convert owns it). */
690
+
632
691
  /* The specialized member loops (the #342 dispatch-chain cut): once
633
692
  * inside a container the grammar is a 2-state cycle — member or
634
693
  * separator — so the general chain's expect/key_slot machine and
@@ -643,7 +702,8 @@ YeptrisStatus yep_tape_walk_lenient_fused(const char* p, size_t len, size_t open
643
702
 
644
703
  #define LWS() \
645
704
  do { \
646
- while (i < len && (p[i] == ' ' || p[i] == '\n' || p[i] == '\r' || p[i] == '\t')) { \
705
+ while (i < len && \
706
+ (p[i] == ' ' || p[i] == '\n' || p[i] == '\r' || (p[i] == '\t' && !cl))) { \
647
707
  i++; \
648
708
  } \
649
709
  if (i >= len) { \
@@ -701,7 +761,8 @@ YeptrisStatus yep_tape_walk_lenient_fused(const char* p, size_t len, size_t open
701
761
  qm_ = (qm_ - 0x0101010101010101ull) & ~qm_ & 0x8080808080808080ull; \
702
762
  bm_ = (bm_ - 0x0101010101010101ull) & ~bm_ & 0x8080808080808080ull; \
703
763
  uint64_t c0_ = (w_ - 0x2020202020202020ull) & ~w_ & 0x8080808080808080ull; \
704
- if (qm_ != 0 && ((bm_ | c0_) & (qm_ - 1)) == 0) { \
764
+ uint64_t hi_ = cl ? (w_ & 0x8080808080808080ull) : 0; \
765
+ if (qm_ != 0 && ((bm_ | c0_ | hi_) & (qm_ - 1)) == 0) { \
705
766
  size_t cl_ = j_ + (size_t)yep_ctz64(qm_) / 8; \
706
767
  recs[count] = ((uint64_t)((uint32_t)j_) << 32) | \
707
768
  ((uint64_t)((uint32_t)(cl_ - j_)) << 8) | (uint64_t)(YEP_T_STR); \
@@ -720,12 +781,17 @@ YeptrisStatus yep_tape_walk_lenient_fused(const char* p, size_t len, size_t open
720
781
  if (room_ >= 8) { \
721
782
  uint64_t v_; \
722
783
  memcpy(&v_, p + b_, 8); \
723
- if ((v_ - 0x2020202020202020ull) & ~v_ & 0x8080808080808080ull) { \
784
+ uint64_t bad_ = \
785
+ (v_ - 0x2020202020202020ull) & ~v_ & 0x8080808080808080ull; \
786
+ if (cl) { \
787
+ bad_ |= v_ & 0x8080808080808080ull; \
788
+ } \
789
+ if (bad_) { \
724
790
  goto lreject; \
725
791
  } \
726
792
  b_ += 8; \
727
793
  } else { \
728
- if ((unsigned char)p[b_] < 0x20) { \
794
+ if ((unsigned char)p[b_] < 0x20 || (cl && (unsigned char)p[b_] >= 0x80)) { \
729
795
  goto lreject; \
730
796
  } \
731
797
  b_++; \
@@ -744,6 +810,16 @@ YeptrisStatus yep_tape_walk_lenient_fused(const char* p, size_t len, size_t open
744
810
  if (!yep_json_string(p, len, &i, &cl_, &esc_)) { \
745
811
  goto lreject; \
746
812
  } \
813
+ if (cl) { /* escaped strings skip the sweeps above: the \
814
+ * clean route still owes the strict sequence's \
815
+ * byte guarantees over the content */ \
816
+ for (size_t b_ = at_ + 1; b_ < cl_; b_++) { \
817
+ unsigned char uc_ = (unsigned char)p[b_]; \
818
+ if (uc_ < 0x20 || uc_ >= 0x80) { \
819
+ goto lreject; \
820
+ } \
821
+ } \
822
+ } \
747
823
  recs[count] = ((uint64_t)((uint32_t)(at_ + 1)) << 32) | \
748
824
  ((uint64_t)((uint32_t)(cl_ - at_ - 1)) << 8) | (uint64_t)(YEP_T_STR); \
749
825
  count++; \
@@ -788,15 +864,36 @@ lmapcolon:
788
864
  }
789
865
  if ((unsigned)(c - '0') <= 9u || c == '-') {
790
866
  size_t k = i + 1;
867
+ /* the digits-only tracking makes the pure-integer case a
868
+ * 3-cycle leading-zero check — number_shape (a call + a
869
+ * rescan, 13% of json-doc) runs only for dot/exp spans */
870
+ int digits_only = 1, saw_digit = (c != '-');
791
871
  while (k < len) {
792
872
  char d = p[k];
793
- if ((d >= '0' && d <= '9') || d == '-' || d == '+' || d == '.' || d == 'e' ||
794
- d == 'E') {
873
+ if ((unsigned)(d - '0') <= 9u) {
874
+ k++;
875
+ saw_digit = 1;
876
+ continue;
877
+ }
878
+ digits_only = 0;
879
+ if (d == '-' || d == '+' || d == '.' || d == 'e' || d == 'E') {
795
880
  k++;
796
881
  continue;
797
882
  }
798
883
  break;
799
884
  }
885
+ if (strict_nums) {
886
+ int ok;
887
+ if (digits_only && saw_digit) {
888
+ size_t d0 = (c == '-') ? i + 1 : i;
889
+ ok = (k - d0 == 1) || p[d0] != '0';
890
+ } else {
891
+ ok = tape_num_ok(p + i, (size_t)(k - i));
892
+ }
893
+ if (!ok) {
894
+ goto lreject;
895
+ }
896
+ }
800
897
  recs[count] = ((uint64_t)((uint32_t)i) << 32) | ((uint64_t)((uint32_t)(k - i)) << 8) |
801
898
  (uint64_t)(YEP_T_NUM);
802
899
  count++;
@@ -868,15 +965,33 @@ lseqval: {
868
965
  }
869
966
  if ((unsigned)(c - '0') <= 9u || c == '-') {
870
967
  size_t k = i + 1;
968
+ int digits_only = 1, saw_digit = (c != '-');
871
969
  while (k < len) {
872
970
  char d = p[k];
873
- if ((d >= '0' && d <= '9') || d == '-' || d == '+' || d == '.' || d == 'e' ||
874
- d == 'E') {
971
+ if ((unsigned)(d - '0') <= 9u) {
972
+ k++;
973
+ saw_digit = 1;
974
+ continue;
975
+ }
976
+ digits_only = 0;
977
+ if (d == '-' || d == '+' || d == '.' || d == 'e' || d == 'E') {
875
978
  k++;
876
979
  continue;
877
980
  }
878
981
  break;
879
982
  }
983
+ if (strict_nums) {
984
+ int ok;
985
+ if (digits_only && saw_digit) {
986
+ size_t d0 = (c == '-') ? i + 1 : i;
987
+ ok = (k - d0 == 1) || p[d0] != '0';
988
+ } else {
989
+ ok = tape_num_ok(p + i, (size_t)(k - i));
990
+ }
991
+ if (!ok) {
992
+ goto lreject;
993
+ }
994
+ }
880
995
  recs[count] = ((uint64_t)((uint32_t)i) << 32) | ((uint64_t)((uint32_t)(k - i)) << 8) |
881
996
  (uint64_t)(YEP_T_NUM);
882
997
  count++;
@@ -940,7 +1055,7 @@ ldone:
940
1055
  {
941
1056
  size_t tail = i;
942
1057
  while (tail < len &&
943
- (p[tail] == ' ' || p[tail] == '\t' || p[tail] == '\n' || p[tail] == '\r')) {
1058
+ (p[tail] == ' ' || p[tail] == '\n' || p[tail] == '\r' || (p[tail] == '\t' && !cl))) {
944
1059
  tail++;
945
1060
  }
946
1061
  if (tail != len) {
@@ -1465,7 +1580,7 @@ YEPTRIS_API YeptrisStatus yeptris_parse_json_tape_lenient(const char* source, si
1465
1580
  return st;
1466
1581
  }
1467
1582
  free(blocks);
1468
- return yep_tape_walk_lenient_fused(source, len, at, tape);
1583
+ return yep_tape_walk_lenient_fused(source, len, at, tape, 0, 0);
1469
1584
  }
1470
1585
 
1471
1586
  /* scalar root: one record, same deferred split for numbers */
@@ -15,7 +15,7 @@
15
15
 
16
16
  /* tape.c (was tape_walk_lnt_fused) */
17
17
  YeptrisStatus yep_tape_walk_lenient_fused(const char* p, size_t len, size_t open,
18
- yeptris_json_tape* t);
18
+ yeptris_json_tape* t, int strict_nums, int clean_only);
19
19
 
20
20
  /* parse.c (#378 slice 1): the YAML parse carrying the packed record
21
21
  * tape — the tree materializes on first access (yep_doc_dom). */
@@ -24,6 +24,11 @@ extern "C" {
24
24
  #endif
25
25
  YeptrisDocument yeptris_parse_ytape_ex(const char* buf, size_t len, const YeptrisParseOptions* opts,
26
26
  YeptrisStatus* status);
27
+
28
+ /* the eager form (the A/B reference lane): the engine builds nodes at
29
+ * parse, no tape */
30
+ YeptrisDocument yeptris_parse_eager_ex(const char* buf, size_t len, const YeptrisParseOptions* opts,
31
+ YeptrisStatus* status);
27
32
  #ifdef __cplusplus
28
33
  }
29
34
  #endif
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yeptris
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.20.2
4
+ version: 0.6.21.1
5
5
  platform: aarch64-linux
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-09-24 00:00:00.000000000 Z
11
+ date: 2026-09-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ffi