yeptris 0.6.12.3 → 0.6.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5900164f14038b2ef4e6c7b3e7764c335faf037559f9743099787e2ffcadb2f7
4
- data.tar.gz: b539d2090254785c57c6dfd5a08e46200b668b0061619691e779a5c186a3978a
3
+ metadata.gz: 60e8ec13d8221947494e0f163b57a619a3bb6d4f179f53d3f9b324e0ae0a4ffb
4
+ data.tar.gz: 6eda638121e069206be5d900cc044c942b84e74989d6482255b0e9177aab13f4
5
5
  SHA512:
6
- metadata.gz: d837519cc77193279158957e4757a0b2ee6f250467d293422676135b6aee89058eac9dbb48c6b7a5f3eb79ca77cc1e076399f93935ed75d652612244b845db74
7
- data.tar.gz: 5eff7ba15bd3c4314508d226ebaa4d2e3aa0eb0a7cc2887645078acbbecd193cbaa443af6877891b226224719b8955700f8567ec8a0446cc42fa713349993864
6
+ metadata.gz: dbe32b83a5b017191c37a5978325516a5f99a7d85819ead1dcd97c030fec14f586acfb6d5965275cf1416c74f4dbeecb08abea77074ce769f51340ef3c1e9a7a
7
+ data.tar.gz: 9b6db9635acbdc9e83017626594b8417fec8c2953e38d17878bf2e99e5a4e0976f4087862890f78a23155ea3eb34d8ca8afeb87e536991fa80091b626e40875e
data/lib/yeptris/ffi.rb CHANGED
@@ -174,6 +174,8 @@ module Yeptris
174
174
  attach_function :yeptris_node_bool, %i[yeptris_node pointer], :yeptris_status
175
175
  attach_function :yeptris_node_seq_count, [:yeptris_node], :size_t
176
176
  attach_function :yeptris_node_seq_at, %i[yeptris_node size_t], :yeptris_node
177
+ attach_function :yeptris_node_children,
178
+ %i[yeptris_node pointer size_t], :size_t
177
179
  attach_function :yeptris_node_map_count, [:yeptris_node], :size_t
178
180
  attach_function :yeptris_node_map_get, %i[yeptris_node pointer size_t], :yeptris_node
179
181
  attach_function :yeptris_node_map_at,
@@ -227,6 +229,10 @@ module Yeptris
227
229
  attach_function :yeptris_marshal_node,
228
230
  %i[yeptris_node pointer pointer], :int
229
231
  attach_function :yeptris_marshal_free, [:pointer], :void
232
+ # The bulk child drain (#168's quadratic): O(n) iteration for
233
+ # #each/#each_pair. Engines without it fall back to the per-index
234
+ # walk (correct, quadratic).
235
+ CHILDREN_DRAIN = !(@missing ||= []).include?(:yeptris_node_children)
230
236
  MARSHAL = !(@missing ||= []).include?(:yeptris_marshal_node)
231
237
 
232
238
  # bulk build (TODO.impl/15 phase D): one call raises a document
data/lib/yeptris/node.rb CHANGED
@@ -150,7 +150,17 @@ class Yeptris::Node
150
150
  return enum_for(:each) unless block_given?
151
151
  raise Yeptris::Error, "#each is for sequences" unless sequence?
152
152
 
153
- (0...seq_count).each { |i| yield seq_at(i) }
153
+ if Yeptris::FFI::CHILDREN_DRAIN
154
+ # one bulk walk (#168): the per-index seq_at loop paid i sibling
155
+ # hops per element — n^2/2 over an 80k-row sequence
156
+ n = alive { Yeptris::FFI.yeptris_node_seq_count(@c_ptr) }
157
+ buf = ::FFI::MemoryPointer.new(:pointer, n)
158
+ alive { Yeptris::FFI.yeptris_node_children(@c_ptr, buf, n) }
159
+ step = buf.type_size
160
+ n.times { |i| yield @document.wrap_node(buf.get_pointer(i * step)) }
161
+ else
162
+ (0...seq_count).each { |i| yield seq_at(i) }
163
+ end
154
164
  end
155
165
 
156
166
  # ---- mapping access ----
@@ -177,13 +187,25 @@ class Yeptris::Node
177
187
  return enum_for(:each_pair) unless block_given?
178
188
  raise Yeptris::Error, "#each_pair is for mappings" unless mapping?
179
189
 
180
- k = ::FFI::MemoryPointer.new(:pointer)
181
- v = ::FFI::MemoryPointer.new(:pointer)
182
- (0...map_count).each do |i|
183
- rc = alive { Yeptris::FFI.yeptris_node_map_at(@c_ptr, i, k, v) }
184
- next unless rc.zero?
190
+ if Yeptris::FFI::CHILDREN_DRAIN
191
+ # key,value interleaved in one walk — the same #168 cure
192
+ pairs = alive { Yeptris::FFI.yeptris_node_map_count(@c_ptr) }
193
+ buf = ::FFI::MemoryPointer.new(:pointer, pairs * 2)
194
+ alive { Yeptris::FFI.yeptris_node_children(@c_ptr, buf, pairs * 2) }
195
+ step = buf.type_size
196
+ pairs.times do |p|
197
+ yield @document.wrap_node(buf.get_pointer((2 * p) * step)),
198
+ @document.wrap_node(buf.get_pointer((2 * p + 1) * step))
199
+ end
200
+ else
201
+ k = ::FFI::MemoryPointer.new(:pointer)
202
+ v = ::FFI::MemoryPointer.new(:pointer)
203
+ (0...map_count).each do |i|
204
+ rc = alive { Yeptris::FFI.yeptris_node_map_at(@c_ptr, i, k, v) }
205
+ next unless rc.zero?
185
206
 
186
- yield @document.wrap_node(k.read_pointer), @document.wrap_node(v.read_pointer)
207
+ yield @document.wrap_node(k.read_pointer), @document.wrap_node(v.read_pointer)
208
+ end
187
209
  end
188
210
  end
189
211
 
@@ -9,13 +9,37 @@ module Yeptris
9
9
  module_function
10
10
 
11
11
  KIND = { scalar: 0, sequence: 1, mapping: 2, callback: 3 }.freeze
12
- TYPE = { str: 0, int: 1, float: 2, bool: 3, null: 4 }.freeze
12
+ TYPE = { str: 0, int: 1, float: 2, bool: 3, null: 4, any: 5 }.freeze
13
13
  REQUIRED = 1 << 0
14
+ FIRST_WINS = 1 << 1 # duplicates within one mapping: first kept
15
+
16
+ # The :any verdict (#238's headroom): the resolver's own typing,
17
+ # one 24-byte record per value — Integer/Float/true/false/nil, a
18
+ # zero-copy String for str/timestamp spans
19
+ ANY_SIZE = 24 # yeptris_value.h's YeptrisValue: kind@0 tag_id@1
20
+ # is_key@2 b@3 off@4 len@8 p@16 — the C layout, ABI-pinned
21
+ ANY_KIND = { 1 => :null, 2 => :bool, 3 => :int, 4 => :float,
22
+ 5 => :str, 6 => :timestamp }.freeze
14
23
 
15
24
  class Error < Yeptris::Error; end
25
+
26
+ # #238's executed headroom (ST_ANY/FIRST_WINS) rides the C tag
27
+ # AFTER v0.6.12; probe the engine rather than trusting versions
28
+ class << self
29
+ def headroom_supported?
30
+ return @headroom_supported unless @headroom_supported.nil?
31
+ @headroom_supported = begin
32
+ load("x: 1\n",
33
+ desc: [{ kind: :mapping, child_index: 1, child_count: 1 },
34
+ { wire_name: "x", kind: :scalar, type: :any }]) && true
35
+ rescue Error, Yeptris::ParseError, Yeptris::Error
36
+ false
37
+ end
38
+ end
39
+ end
16
40
  class RequiredMissing < Error; end
17
41
 
18
- ELEMENT_SIZE = { 0 => 16, 1 => 8, 2 => 8, 3 => 1, 4 => 1 }.freeze # by TYPE value
42
+ ELEMENT_SIZE = { 0 => 16, 1 => 8, 2 => 8, 3 => 1, 4 => 1, 5 => ANY_SIZE }.freeze # by TYPE value
19
43
 
20
44
  # A materialized span: zero-copy off/len into the source plus the
21
45
  # node's byte offset (the CALLBACK escape hatch).
@@ -47,7 +71,7 @@ module Yeptris
47
71
  { kind: KIND.fetch(n.fetch(:kind)),
48
72
  type: TYPE.fetch(n[:type] || :str),
49
73
  wire: n[:wire_name],
50
- flags: n[:required] ? REQUIRED : 0,
74
+ flags: (n[:required] ? REQUIRED : 0) | (n[:first_wins] ? FIRST_WINS : 0),
51
75
  child_index: n[:child_index] || 0,
52
76
  child_count: n[:child_count] || 0 }
53
77
  end
@@ -120,6 +144,16 @@ module Yeptris
120
144
  when TYPE[:float] then data.get_double(k * 8)
121
145
  when TYPE[:bool] then data.get_uchar(k) == 1
122
146
  when TYPE[:null] then nil
147
+ when TYPE[:any]
148
+ rec = k * ANY_SIZE
149
+ case ANY_KIND[data.get_uint8(rec)]
150
+ when :int then data.get_int64(rec + 16)
151
+ when :float then data.get_double(rec + 16)
152
+ when :bool then data.get_uint8(rec + 3) == 1
153
+ when :null then nil
154
+ else # str/timestamp: the zero-copy span
155
+ source.byteslice(data.get_uint32(rec + 4), data.get_uint32(rec + 8))
156
+ end
123
157
  else
124
158
  off = data.get_uint32(k * 16)
125
159
  len = data.get_uint32(k * 16 + 4)
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.12.3".freeze
6
+ VERSION = "0.6.14.1".freeze
7
7
  # The error hierarchy lives in THIS file (the parent namespace's
8
8
  # own file): nested constants do not trigger a parent-constant
9
9
  # autoload, and the law forbids internal requires — defining the
@@ -3,7 +3,7 @@
3
3
  cmake_minimum_required(VERSION 3.20)
4
4
 
5
5
  project(yeptris
6
- VERSION 0.6.12
6
+ VERSION 0.6.14
7
7
  DESCRIPTION "Ultra-fast YAML 1.2 parser, emitter and streamer in C"
8
8
  LANGUAGES C CXX
9
9
  )
@@ -76,6 +76,15 @@ YEPTRIS_API YeptrisNode yeptris_node_map_get(YeptrisNode node, const char* key,
76
76
  YEPTRIS_API int yeptris_node_map_at(YeptrisNode node, size_t index, YeptrisNode* key,
77
77
  YeptrisNode* value);
78
78
 
79
+ /* Bulk children drain — the O(n) iteration primitive (ruby #168: the
80
+ * per-index seq_at/map_at walks make an 80k-row sequence O(n^2)). One
81
+ * call walks the child list once and fills out[0..cap) with handles:
82
+ * sequence elements in order; mapping KEY,VALUE pairs interleaved.
83
+ * Returns the total child count (mapping pairs count twice); out==NULL
84
+ * or cap==0 returns the count without allocating handles. Handles come
85
+ * from the document's pool — document_free reclaims them. */
86
+ YEPTRIS_API size_t yeptris_node_children(YeptrisNode node, YeptrisNode* out, size_t cap);
87
+
79
88
  /* ---- Construction (TODO.impl/11 phase 3) ----
80
89
  *
81
90
  * Build documents from scratch and mutate them; the emitter and every
@@ -734,6 +734,7 @@ static YeptrisDocument cbor_wrap(yep_dom* dom, const void* buf, const yep_alloca
734
734
  d->transcoded_len = 0;
735
735
  d->input = (const char*)buf;
736
736
  d->finish_pool = NULL;
737
+ d->lazy_tape = NULL; /* field-by-field ctor: no garbage for free */
737
738
  return (YeptrisDocument)d;
738
739
  }
739
740
 
@@ -839,6 +840,7 @@ YEPTRIS_API YeptrisDocument yeptris_cbor_decode(const void* buf, size_t len, uin
839
840
  doc->transcoded_len = 0;
840
841
  doc->input = (const char*)buf;
841
842
  doc->finish_pool = NULL;
843
+ doc->lazy_tape = NULL; /* field-by-field ctor: no garbage for free */
842
844
  if (status != NULL) {
843
845
  *status = YEPTRIS_OK;
844
846
  }
@@ -703,7 +703,10 @@ static void cbor_write_item(cenc* e, uint32_t id, uint8_t* out, size_t* pos) {
703
703
  static int cbor_run(YeptrisDocument handle, uint32_t opts, uint8_t* out, size_t cap,
704
704
  size_t* out_len, int* written) {
705
705
  yeptris_document* doc = (yeptris_document*)handle;
706
- yep_dom* d = doc->dom;
706
+ yep_dom* d = yep_doc_dom(doc); /* #342 lazy: materialize on encode */
707
+ if (d == NULL) {
708
+ return YEPTRIS_ERROR_MEMORY;
709
+ }
707
710
  if (d->dcount == 0) {
708
711
  return YEPTRIS_ERROR_ARG;
709
712
  }
@@ -22,16 +22,35 @@ typedef struct yeptris_document {
22
22
  void* finish_pool; /* engine finish pool: resolved tags, folded and
23
23
  escaped scalars outlive the engine through the
24
24
  document */
25
+ void* lazy_tape; /* #342 slice 2: the parsed tape when the tree is
26
+ * deferred (gate-clean strict JSON). dom==NULL until
27
+ * the first tree access materializes via dom_from_tape;
28
+ * freed with the document. void* to avoid a header
29
+ * cycle — parse.c casts to yeptris_json_tape* */
25
30
  } yeptris_document;
26
31
 
27
32
  /* Node handle: a (document, node-id) pair so nodes stay usable even if
28
33
  * the node pool grows (ids are stable; pointers are not). Defined here
29
34
  * (not parse.c) since the query layer and the builder share it. */
35
+ /* #342 slice 2: the lazy-tree choke point. Returns the document's dom,
36
+ * materializing it from lazy_tape on first access (NULL when materialization
37
+ * fails — callers treat it as an empty/unusable tree). Every consumer that
38
+ * reads ->dom on a parse-produced document routes through here; the builder
39
+ * and CBOR decode paths set dom eagerly and are unaffected. parse.c owns it. */
30
40
  typedef struct yeptris_node {
31
41
  yeptris_document* doc;
32
42
  uint32_t id;
33
43
  } yeptris_node;
34
44
 
45
+ #ifdef __cplusplus
46
+ extern "C" {
47
+ #endif
48
+
49
+ yep_dom* yep_doc_dom(yeptris_document* doc);
35
50
  yeptris_node* yep_handle_new(yeptris_document* doc, uint32_t id);
36
51
 
52
+ #ifdef __cplusplus
53
+ }
54
+ #endif
55
+
37
56
  #endif /* YEP_DOC_H */
@@ -1,5 +1,6 @@
1
1
  /* dom.c — event-built DOM (builder sink + storage). */
2
2
 
3
+ #include <stdio.h>
3
4
  #include <string.h>
4
5
 
5
6
  #include "common/simd_text.h"
@@ -864,6 +865,132 @@ int dom_on_flow_commit(void* ctx) {
864
865
  return 1;
865
866
  }
866
867
 
868
+ int dom_from_tape(yep_dom* d, const yeptris_json_tape* t) {
869
+ if (d == NULL || t == NULL || t->_src == NULL) {
870
+ return -1;
871
+ }
872
+ /* the columns are the one representation valid on every route
873
+ * (strict: native; lenient fused: lazily materialized from the
874
+ * records; scalar-root lenient: native column writes) */
875
+ if (yeptris_tape_columns((yeptris_json_tape*)t) != 0) {
876
+ return -1;
877
+ }
878
+ if (t->kinds == NULL) {
879
+ return -1;
880
+ }
881
+ d->input_base = (const char*)t->_src;
882
+ d->input_len = t->_srclen;
883
+ const char* src = (const char*)t->_src;
884
+ d->depth = 0;
885
+ for (uint32_t i = 0; i < t->count; i++) {
886
+ const uint8_t kind = t->kinds[i];
887
+ const uint32_t len = t->lens[i];
888
+ const uint32_t off = t->offs[i];
889
+ switch (kind) {
890
+ case YEP_T_DOC:
891
+ break; /* the stream boundary record */
892
+ case YEP_T_SEQ_OPEN:
893
+ case YEP_T_MAP_OPEN: {
894
+ if (d->depth >= YEP_DOM_MAX_DEPTH) {
895
+ return -1;
896
+ }
897
+ uint32_t id =
898
+ dom_open_node(d, kind == YEP_T_SEQ_OPEN ? YEP_DOM_SEQUENCE : YEP_DOM_MAPPING, NULL,
899
+ NULL, 0, 0, 0, 1);
900
+ if (id == UINT32_MAX || dom_place(d, id) != 0) {
901
+ return -1;
902
+ }
903
+ d->map_pending_key[d->depth] = 0;
904
+ d->stack[d->depth++] = id;
905
+ break;
906
+ }
907
+ case YEP_T_CLOSE:
908
+ if (d->depth == 0) {
909
+ return -1;
910
+ }
911
+ d->depth--;
912
+ break;
913
+ case YEP_T_NULL:
914
+ case YEP_T_TRUE:
915
+ case YEP_T_FALSE:
916
+ case YEP_T_INT:
917
+ case YEP_T_FLOAT:
918
+ case YEP_T_NUM:
919
+ case YEP_T_STR: {
920
+ yep_tag_id tag = YEPTRIS_TAG_STR;
921
+ uint8_t style = YEP_STYLE_PLAIN;
922
+ int implicit = 0;
923
+ switch (kind) {
924
+ case YEP_T_NULL:
925
+ tag = YEPTRIS_TAG_NULL;
926
+ implicit = 1;
927
+ break;
928
+ case YEP_T_TRUE:
929
+ case YEP_T_FALSE:
930
+ tag = YEPTRIS_TAG_BOOL;
931
+ implicit = 1;
932
+ break;
933
+ case YEP_T_INT:
934
+ case YEP_T_FLOAT:
935
+ case YEP_T_NUM:
936
+ tag = YEPTRIS_TAG_INT;
937
+ implicit = 1;
938
+ if (kind == YEP_T_NUM) {
939
+ /* the lenient route's deferred contract: the span
940
+ * carried NO number grammar at parse — classify
941
+ * now (the tape convert's own authority) */
942
+ size_t adv = 0;
943
+ int flt = 0;
944
+ if (yep_json_number_shape(src + off, len, &adv, &flt) == 0 || adv != len) {
945
+ return -1; /* malformed: the strict route rejected at parse */
946
+ }
947
+ tag = flt ? YEPTRIS_TAG_FLOAT : YEPTRIS_TAG_INT;
948
+ } else if (kind == YEP_T_FLOAT) {
949
+ tag = YEPTRIS_TAG_FLOAT;
950
+ }
951
+ break;
952
+ default:
953
+ style = YEP_STYLE_DOUBLE_QUOTED;
954
+ break;
955
+ }
956
+ uint32_t id =
957
+ dom_open_node(d, YEP_DOM_SCALAR, NULL, NULL, 0, style, (uint8_t)implicit, 0);
958
+ if (id == UINT32_MAX) {
959
+ return -1;
960
+ }
961
+ if (kind == YEP_T_STR) {
962
+ /* the record's span is the INNER bytes (quotes off) */
963
+ if (memchr(src + off, '\\', len) != NULL) {
964
+ /* escaped: unescape into the arena (the fused
965
+ * builder's arm, byte for byte) */
966
+ char* dst = yep_dom_str_tail(d, len);
967
+ if (dst == NULL) {
968
+ return -1;
969
+ }
970
+ d->nodes[id].value = yep_dom_str_commit(
971
+ d, yep_finish_double_into(src, off, off + len, dst, len));
972
+ } else {
973
+ yep_view v = {src + off, len};
974
+ d->nodes[id].value = dom_str_in(d, &v, 1);
975
+ }
976
+ } else {
977
+ yep_view v = {src + off, len};
978
+ d->nodes[id].value = dom_str_in(d, &v, 1);
979
+ }
980
+ d->nodes[id].tag_id = tag;
981
+ if (dom_place(d, id) != 0) {
982
+ return -1;
983
+ }
984
+ break;
985
+ }
986
+ default:
987
+ return -1; /* a CONT run or an unknown kind: neither reaches a
988
+ strict-legal tape (CONT only past 16 MiB tokens) */
989
+ }
990
+ }
991
+ return d->depth == 0 ? 0 : -1;
992
+ }
993
+
867
994
  void dom_on_flow_rollback(void* ctx) {
868
995
  yep_dom* d = (yep_dom*)ctx;
869
996
  if (d->flow_staged) {
@@ -18,6 +18,7 @@
18
18
  #include <stdint.h>
19
19
 
20
20
  #include <yeptris/api.h> /* YEPTRIS_CT_ASSERT (the layout gates) */
21
+ #include <yeptris/tape.h> /* yeptris_json_tape (dom_from_tape) */ /* YEPTRIS_CT_ASSERT (the layout gates) */
21
22
 
22
23
  #include "../common/simd_text.h"
23
24
 
@@ -246,6 +247,15 @@ void yep_mut_set_depths(yep_dom* d, uint32_t id, uint16_t depth);
246
247
  * strictly-validated span [open, close] directly through the DOM's own
247
248
  * placement/link/anchor laws — node-for-node identical to the event
248
249
  * path (the flow-direct-diff ctest pins it). 1 = built, <0 = abort. */
250
+ /* dom_from_tape — the lazy-DOM seam (#342 slice 1): builds the node
251
+ * tree from the tape's records, mirroring dom_on_flow_build's arms
252
+ * (borrowed spans for clean strings, an arena unescape for escaped
253
+ * ones, typed literals by kind). The records are pre-validated, so
254
+ * the walk is placement only. Returns 0, or -1 (allocation/depth/an
255
+ * unexpected record). STRICT tapes only — YEP_T_NUM (the lenient
256
+ * route's deferred numbers) is rejected. */
257
+ int dom_from_tape(yep_dom* d, const yeptris_json_tape* t);
258
+
249
259
  int dom_on_flow_build(void* ctx, const char* p, size_t open, size_t len, uint32_t line,
250
260
  size_t line_start, yep_view anchor, yep_view tag, uint32_t anchor_id,
251
261
  int max_depth, size_t* close);
@@ -45,6 +45,9 @@ YEPTRIS_API size_t yeptris_serialize_into_ex(YeptrisDocument handle,
45
45
  em.w.grow = 0;
46
46
  em.w.oom = 0;
47
47
  em.doc = (const yeptris_document*)handle;
48
+ if (yep_doc_dom((yeptris_document*)handle) == NULL) { /* #342 lazy */
49
+ return 0;
50
+ }
48
51
  em.w.p = NULL;
49
52
  em.w.last = 0;
50
53
  em.w.force_flow = 0;
@@ -100,6 +103,9 @@ YEPTRIS_API char* yeptris_serialize_ex(YeptrisDocument handle, const yeptris_emi
100
103
  em.w.grow = 0;
101
104
  em.w.oom = 0;
102
105
  em.doc = (const yeptris_document*)handle;
106
+ if (yep_doc_dom((yeptris_document*)handle) == NULL) { /* #342 lazy */
107
+ return NULL;
108
+ }
103
109
  em.w.p = NULL;
104
110
  em.w.last = 0;
105
111
  em.w.force_flow = 0;
@@ -174,6 +180,9 @@ YEPTRIS_API char* yeptris_serialize_json(YeptrisDocument handle, size_t* len) {
174
180
  em.w.grow = 0;
175
181
  em.w.oom = 0;
176
182
  em.doc = (const yeptris_document*)handle;
183
+ if (yep_doc_dom((yeptris_document*)handle) == NULL) { /* #342 lazy */
184
+ return NULL;
185
+ }
177
186
  em.w.p = NULL;
178
187
  em.w.last = 0;
179
188
  em.w.force_flow = 0;
@@ -224,6 +233,9 @@ YEPTRIS_API char* yeptris_serialize_json_ex(YeptrisDocument handle, size_t* len,
224
233
  em.w.grow = 0;
225
234
  em.w.oom = 0;
226
235
  em.doc = (const yeptris_document*)handle;
236
+ if (yep_doc_dom((yeptris_document*)handle) == NULL) { /* #342 lazy */
237
+ return NULL;
238
+ }
227
239
  em.w.p = NULL;
228
240
  em.w.last = 0;
229
241
  em.w.force_flow = 0;
@@ -378,6 +390,9 @@ YEPTRIS_API size_t yeptris_serialize_stream(YeptrisDocument handle,
378
390
  em.w.grow = 0;
379
391
  em.w.oom = 0;
380
392
  em.doc = (const yeptris_document*)handle;
393
+ if (yep_doc_dom((yeptris_document*)handle) == NULL) { /* #342 lazy */
394
+ return 0;
395
+ }
381
396
  em.w.p = NULL;
382
397
  em.w.last = 0;
383
398
  em.w.force_flow = 0;
@@ -12,6 +12,7 @@
12
12
  #include "parse/numbers.h"
13
13
  #include "resolve/resolver.h"
14
14
  #include "scan/json.h"
15
+ #include "tape_in.h"
15
16
 
16
17
  #include "parse/events.h"
17
18
  #include <errno.h>
@@ -38,6 +39,59 @@ YEPTRIS_API YeptrisDocument yeptris_parse(const char* buf, size_t len, YeptrisSt
38
39
  return yeptris_parse_ex(buf, len, NULL, status);
39
40
  }
40
41
 
42
+ /* #342 slice 2: materialize the deferred tree on first access. The
43
+ * gate-clean JSON route carries the parsed TAPE (simdjson's design:
44
+ * its "DOM" is a tape) and pays dom_from_tape only when a consumer
45
+ * touches the tree — parse-only workloads never build nodes. */
46
+ yep_dom* yep_doc_dom(yeptris_document* doc) {
47
+ if (doc == NULL) {
48
+ return NULL;
49
+ }
50
+ if (doc->dom == NULL && doc->lazy_tape != NULL) {
51
+ yeptris_json_tape* t = (yeptris_json_tape*)doc->lazy_tape;
52
+ yep_dom* dom = yep_dom_create(doc->sys);
53
+ if (dom != NULL && dom_from_tape(dom, t) == 0) {
54
+ doc->dom = dom;
55
+ yeptris_tape_free(t);
56
+ yep_free(doc->sys, t);
57
+ doc->lazy_tape = NULL;
58
+ } else {
59
+ yep_dom_destroy(dom);
60
+ dom = NULL; /* the tape stays: a later access retries, or
61
+ free drops it (the materializer only fails on
62
+ malformed NUM spans, which the strict gate
63
+ already rejected at parse) */
64
+ }
65
+ return dom;
66
+ }
67
+ return doc->dom;
68
+ }
69
+
70
+ /* The strict-JSON document wrapper (both routes share it). */
71
+ /* The lazy route's acceptance debt: the fused lenient walk records
72
+ * number runs UNVALIDATED (its simdjson-style deferred contract).
73
+ * parse_json's contract is RFC 8259 at parse time, so the deferred
74
+ * spans settle HERE — full-span shape check per NUM record, decoding
75
+ * the packed records directly (no columns build at parse; the walk
76
+ * guarantees recs are primary on this route). */
77
+ static int lazy_nums_settled(const yeptris_json_tape* t) {
78
+ const char* p = (const char*)t->_src;
79
+ for (uint32_t i = 0; i < t->count; i++) {
80
+ uint64_t r = t->recs[i];
81
+ if ((uint8_t)(r & 0xFFu) != YEP_T_NUM) {
82
+ continue;
83
+ }
84
+ uint32_t len = (uint32_t)((r >> 8) & 0x7FFFFFu);
85
+ uint32_t off = (uint32_t)(r >> 32);
86
+ size_t adv = 0;
87
+ int flt = 0;
88
+ if (yep_json_number_shape(p + off, len, &adv, &flt) == 0 || adv != (size_t)len) {
89
+ return -1;
90
+ }
91
+ }
92
+ return 0;
93
+ }
94
+
41
95
  /* The strict-JSON document wrapper (both routes share it). */
42
96
  static YeptrisDocument yep_json_doc_wrap(yep_dom* dom, const char* buf, size_t len,
43
97
  const yep_allocator* sys, YeptrisStatus* status) {
@@ -52,6 +106,7 @@ static YeptrisDocument yep_json_doc_wrap(yep_dom* dom, const char* buf, size_t l
52
106
  }
53
107
  doc->dom = dom;
54
108
  doc->sys = sys;
109
+ doc->lazy_tape = NULL;
55
110
  doc->schema = YEPTRIS_SCHEMA_12_CORE; /* strict JSON is core by construction */
56
111
  doc->transcoded = NULL;
57
112
  doc->transcoded_len = 0;
@@ -75,41 +130,44 @@ YEPTRIS_API YeptrisDocument yeptris_parse_json(const char* buf, size_t len, Yept
75
130
  * builds. Rejects, non-opener roots, and surprises fall to the
76
131
  * original sequence below, whose error precedence is byte-for-byte
77
132
  * the pinned behavior (json-suite-strict gates this). */
78
- if (len > 0 && !yep_text_active()->gate_scan(buf, len)) {
133
+ int gated = len > 0 && yep_text_active()->gate_scan(buf, len);
134
+ if (!gated) {
79
135
  size_t off = 0;
80
136
  while (off < len &&
81
137
  (buf[off] == ' ' || buf[off] == '\t' || buf[off] == '\n' || buf[off] == '\r')) {
82
138
  off++;
83
139
  }
84
- if (off < len && (buf[off] == '[' || buf[off] == '{')) {
140
+ /* tabs: the lenient walk's own pinned acceptance (its route
141
+ * takes them); the strict routes reject them (ErrorParity's
142
+ * pinned agreement). A tab-carrying buffer must NOT take the
143
+ * lazy walk — the validating sequence below reports exactly
144
+ * the pinned reject. */
145
+ if (off < len && (buf[off] == '[' || buf[off] == '{') && memchr(buf, '\t', len) == NULL) {
146
+ /* #342 slice 2: the fused LENIENT walk (one pass, records
147
+ * only — no node building) settles the deferred number
148
+ * grammar inline, then the tape rides the document and
149
+ * dom_from_tape builds nodes on the first tree access.
150
+ * A reject or malformed span falls to the original
151
+ * sequence below, whose error precedence is byte-for-byte
152
+ * the pinned behavior. */
85
153
  const yep_allocator* sys = yep_system_allocator();
86
- yep_dom* dom = yep_dom_create(sys);
87
- if (dom == NULL) {
154
+ yeptris_json_tape* t = yep_alloc(sys, sizeof(*t));
155
+ if (t == NULL) {
88
156
  st = YEPTRIS_ERROR_MEMORY;
89
157
  goto jfail;
90
158
  }
91
- dom->input_base = buf;
92
- dom->input_len = len;
93
- dom->flow_strict = 1;
94
- size_t close = 0;
95
- int rc = dom_on_flow_build(dom, buf, off, len, 1, 0, (yep_view){0}, (yep_view){0}, 0,
96
- YEP_DOM_MAX_DEPTH, &close);
97
- dom->flow_strict = 0;
98
- int tail_ok = 0;
99
- if (rc == 1) {
100
- /* trailing garbage after the closer is a reject the
101
- * fallback reports exactly as before */
102
- size_t t = close + 1;
103
- while (t < len &&
104
- (buf[t] == ' ' || buf[t] == '\t' || buf[t] == '\n' || buf[t] == '\r')) {
105
- t++;
159
+ if (yep_tape_walk_lenient_fused(buf, len, off, t) == YEPTRIS_OK &&
160
+ lazy_nums_settled(t) == 0) {
161
+ YeptrisDocument h = yep_json_doc_wrap(NULL, buf, len, sys, status);
162
+ if (h != NULL) {
163
+ ((yeptris_document*)h)->lazy_tape = t;
164
+ return h;
106
165
  }
107
- tail_ok = (t == len);
166
+ yeptris_tape_free(t); /* wrap failed (memory): below */
167
+ } else {
168
+ yeptris_tape_free(t); /* reject/malformed: below */
108
169
  }
109
- if (rc == 1 && tail_ok && dom_on_flow_commit(dom) > 0) {
110
- return yep_json_doc_wrap(dom, buf, len, sys, status);
111
- }
112
- yep_dom_destroy(dom); /* reject/rollback: the sequence below */
170
+ yep_free(sys, t);
113
171
  }
114
172
  }
115
173
  size_t verr = 0;
@@ -120,8 +178,7 @@ YEPTRIS_API YeptrisDocument yeptris_parse_json(const char* buf, size_t len, Yept
120
178
  goto jfail;
121
179
  }
122
180
  size_t uerr = 0;
123
- if (yep_text_active()->gate_scan(buf, len) &&
124
- !yep_utf8_validate((const unsigned char*)buf, len, &uerr)) {
181
+ if (gated && !yep_utf8_validate((const unsigned char*)buf, len, &uerr)) {
125
182
  yep_error_set(yep_error_tls(), YEP_ERR_ENCODING, 0, 0, uerr, "ill-formed UTF-8 at byte %zu",
126
183
  uerr);
127
184
  st = YEPTRIS_ERROR_ENCODING;
@@ -343,6 +400,8 @@ engine_enter:
343
400
  * or an arena copy (dom_ev_str) — nothing references it */
344
401
  yep_pool_destroy(finish);
345
402
  doc->finish_pool = NULL;
403
+ doc->lazy_tape = NULL; /* field-by-field ctor: leave no garbage
404
+ * (document_free frees a non-NULL tape) */
346
405
  return (YeptrisDocument)doc;
347
406
 
348
407
  fail:
@@ -363,6 +422,10 @@ YEPTRIS_API void yeptris_document_free(YeptrisDocument handle) {
363
422
  if (doc == NULL) {
364
423
  return;
365
424
  }
425
+ if (doc->lazy_tape != NULL) { /* never materialized: free stays free */
426
+ yeptris_tape_free((yeptris_json_tape*)doc->lazy_tape);
427
+ yep_free(doc->sys, doc->lazy_tape);
428
+ }
366
429
  yep_dom_destroy(doc->dom);
367
430
  yep_pool_destroy((yep_pool*)doc->finish_pool);
368
431
  yep_free(doc->sys, doc->transcoded);
@@ -371,7 +434,7 @@ YEPTRIS_API void yeptris_document_free(YeptrisDocument handle) {
371
434
 
372
435
  YEPTRIS_API size_t yeptris_document_count(YeptrisDocument handle) {
373
436
  yeptris_document* doc = (yeptris_document*)handle;
374
- return doc ? doc->dom->dcount : 0;
437
+ return doc ? yep_doc_dom(doc)->dcount : 0;
375
438
  }
376
439
 
377
440
  yeptris_node* yep_handle_new(yeptris_document* doc, uint32_t id) {
@@ -390,10 +453,14 @@ static YeptrisNode node_new_handle(yeptris_document* doc, uint32_t id) {
390
453
 
391
454
  YEPTRIS_API YeptrisNode yeptris_document_root(YeptrisDocument handle, size_t index) {
392
455
  yeptris_document* doc = (yeptris_document*)handle;
393
- if (doc == NULL || index >= doc->dom->dcount) {
456
+ if (doc == NULL) {
394
457
  return NULL;
395
458
  }
396
- return node_new_handle(doc, doc->dom->docs[index]);
459
+ yep_dom* dom = yep_doc_dom(doc);
460
+ if (dom == NULL || index >= dom->dcount) {
461
+ return NULL;
462
+ }
463
+ return node_new_handle(doc, dom->docs[index]);
397
464
  }
398
465
 
399
466
  static const yep_dnode* node_of(YeptrisNode handle) {
@@ -588,6 +655,24 @@ YEPTRIS_API YeptrisNode yeptris_node_seq_at(YeptrisNode handle, size_t index) {
588
655
  return wrap((yeptris_node*)handle, id);
589
656
  }
590
657
 
658
+ YEPTRIS_API size_t yeptris_node_children(YeptrisNode handle, YeptrisNode* out, size_t cap) {
659
+ const yep_dnode* n = node_of(handle);
660
+ if (n == NULL || (n->kind != YEP_DOM_SEQUENCE && n->kind != YEP_DOM_MAPPING)) {
661
+ return 0;
662
+ }
663
+ size_t count = 0;
664
+ uint32_t id = n->first_child;
665
+ while (id != UINT32_MAX) {
666
+ if (out != NULL && count < cap) {
667
+ out[count] = wrap((yeptris_node*)handle, id);
668
+ }
669
+ count++;
670
+ const yep_dnode* cur = yep_dom_node(((yeptris_node*)handle)->doc->dom, id);
671
+ id = cur ? cur->next_sibling : UINT32_MAX;
672
+ }
673
+ return count;
674
+ }
675
+
591
676
  YEPTRIS_API size_t yeptris_node_map_count(YeptrisNode handle) {
592
677
  const yep_dnode* n = node_of(handle);
593
678
  return (n && n->kind == YEP_DOM_MAPPING) ? n->count / 2 : 0;
@@ -79,12 +79,16 @@ YEPTRIS_API yeptris_plan* yeptris_plan_compile(const char* spec, size_t len, Yep
79
79
  }
80
80
  return NULL;
81
81
  }
82
- const yeptris_document* d = (const yeptris_document*)doc;
83
- const yep_dnode* root = yep_dom_node(d->dom, d->dom->docs[0]);
84
- yeptris_plan* plan = calloc(1, sizeof(*plan));
82
+ yeptris_document* d = (yeptris_document*)doc;
83
+ yeptris_plan* plan = calloc(1, sizeof(*plan)); /* before any goto: out frees it */
85
84
  if (plan == NULL) {
86
85
  goto mem;
87
86
  }
87
+ yep_dom* dd = yep_doc_dom(d); /* #342 lazy: the spec parse may defer */
88
+ if (dd == NULL) {
89
+ goto mem;
90
+ }
91
+ const yep_dnode* root = yep_dom_node(dd, dd->docs[0]);
88
92
  if (root == NULL || root->kind != 2) { /* the spec root is a mapping */
89
93
  goto bad;
90
94
  }
@@ -95,10 +99,10 @@ YEPTRIS_API yeptris_plan* yeptris_plan_compile(const char* spec, size_t len, Yep
95
99
  int saw_kind = 0;
96
100
  uint32_t pair = root->first_child;
97
101
  while (pair != UINT32_MAX) {
98
- const yep_dnode* kn = yep_dom_node(d->dom, pair);
99
- const yep_dnode* vn = yep_dom_node(d->dom, kn->next_sibling);
100
- yep_view kv = sv_view(d->dom, kn->value);
101
- yep_view vv = sv_view(d->dom, vn->value);
102
+ const yep_dnode* kn = yep_dom_node(dd, pair);
103
+ const yep_dnode* vn = yep_dom_node(dd, kn->next_sibling);
104
+ yep_view kv = sv_view(dd, kn->value);
105
+ yep_view vv = sv_view(dd, vn->value);
102
106
  if (kv.len == 4 && memcmp(kv.p, "kind", 4) == 0) {
103
107
  saw_kind = 1;
104
108
  if (!(vv.len == 3 && (memcmp(vv.p, "seq", 3) == 0 || memcmp(vv.p, "map", 3) == 0))) {
@@ -130,7 +134,7 @@ YEPTRIS_API yeptris_plan* yeptris_plan_compile(const char* spec, size_t len, Yep
130
134
  if (path_node->tag_id != YEPTRIS_TAG_STR) {
131
135
  goto bad; /* the path is a string, not a number */
132
136
  }
133
- yep_view vv = sv_view(d->dom, path_node->value);
137
+ yep_view vv = sv_view(dd, path_node->value);
134
138
  if (vv.len != 0) { /* "" rides the seq root like no path */
135
139
  plan->seg_off = malloc(sizeof(uint32_t));
136
140
  plan->seg_len = malloc(sizeof(uint32_t));
@@ -159,11 +163,11 @@ YEPTRIS_API yeptris_plan* yeptris_plan_compile(const char* spec, size_t len, Yep
159
163
  size_t off = 0;
160
164
  uint32_t cn = path_node->first_child;
161
165
  for (size_t i = 0; i < n; i++) {
162
- const yep_dnode* seg = yep_dom_node(d->dom, cn);
166
+ const yep_dnode* seg = yep_dom_node(dd, cn);
163
167
  if (seg == NULL || seg->kind != 0 || seg->tag_id != YEPTRIS_TAG_STR) {
164
168
  goto bad; /* every segment is a string */
165
169
  }
166
- yep_view sv = sv_view(d->dom, seg->value);
170
+ yep_view sv = sv_view(dd, seg->value);
167
171
  if (sv.len == 0) {
168
172
  goto bad;
169
173
  }
@@ -191,7 +195,7 @@ YEPTRIS_API yeptris_plan* yeptris_plan_compile(const char* spec, size_t len, Yep
191
195
  }
192
196
  uint32_t cn = children->first_child;
193
197
  for (size_t i = 0; i < plan->ncols; i++) {
194
- const yep_dnode* leaf = yep_dom_node(d->dom, cn);
198
+ const yep_dnode* leaf = yep_dom_node(dd, cn);
195
199
  if (leaf == NULL || leaf->kind != 2) {
196
200
  goto bad;
197
201
  }
@@ -200,10 +204,10 @@ YEPTRIS_API yeptris_plan* yeptris_plan_compile(const char* spec, size_t len, Yep
200
204
  int kind = -1;
201
205
  uint32_t lp = leaf->first_child;
202
206
  while (lp != UINT32_MAX) {
203
- const yep_dnode* lk = yep_dom_node(d->dom, lp);
204
- const yep_dnode* lv = yep_dom_node(d->dom, lk->next_sibling);
205
- yep_view lkv = sv_view(d->dom, lk->value);
206
- yep_view lvv = sv_view(d->dom, lv->value);
207
+ const yep_dnode* lk = yep_dom_node(dd, lp);
208
+ const yep_dnode* lv = yep_dom_node(dd, lk->next_sibling);
209
+ yep_view lkv = sv_view(dd, lk->value);
210
+ yep_view lvv = sv_view(dd, lv->value);
207
211
  if (lkv.len == 4 && memcmp(lkv.p, "name", 4) == 0) {
208
212
  if (lv->kind != 0 || lvv.len == 0) {
209
213
  goto bad;
@@ -657,15 +661,15 @@ yeptris_document_plan_walk(YeptrisDocument doc, const yeptris_plan* plan, Yeptri
657
661
  if (st != NULL) {
658
662
  *st = YEPTRIS_OK;
659
663
  }
660
- const yeptris_document* d = (const yeptris_document*)doc;
661
- if (doc == NULL || plan == NULL || d->dom == NULL || d->dom->dcount == 0) {
664
+ const yep_dom* dd = doc == NULL ? NULL : yep_doc_dom((yeptris_document*)doc);
665
+ if (doc == NULL || plan == NULL || dd == NULL || dd->dcount == 0) {
662
666
  if (st != NULL) {
663
667
  *st = YEPTRIS_ERROR_ARG;
664
668
  }
665
669
  return NULL;
666
670
  }
667
- const yep_dnode* root = yep_dom_node(d->dom, d->dom->docs[0]);
668
- const yep_dnode* container = dom_find_rows(d->dom, root, plan);
671
+ const yep_dnode* root = yep_dom_node(dd, dd->docs[0]);
672
+ const yep_dnode* container = dom_find_rows(dd, root, plan);
669
673
  if (container == NULL) {
670
674
  if (st != NULL) {
671
675
  *st = YEPTRIS_ERROR_PARSE; /* document shape disagreement */
@@ -676,7 +680,7 @@ yeptris_document_plan_walk(YeptrisDocument doc, const yeptris_plan* plan, Yeptri
676
680
  size_t rows = 0;
677
681
  for (uint32_t cur = container->first_child; cur != UINT32_MAX;) {
678
682
  const yep_dnode* row;
679
- cur = dom_rows_next(d->dom, container, cur, &row);
683
+ cur = dom_rows_next(dd, container, cur, &row);
680
684
  if (row != NULL) {
681
685
  rows++;
682
686
  }
@@ -690,15 +694,15 @@ yeptris_document_plan_walk(YeptrisDocument doc, const yeptris_plan* plan, Yeptri
690
694
  size_t row_i = 0;
691
695
  for (uint32_t cur = container->first_child; cur != UINT32_MAX && row_i < rows;) {
692
696
  const yep_dnode* row;
693
- cur = dom_rows_next(d->dom, container, cur, &row);
697
+ cur = dom_rows_next(dd, container, cur, &row);
694
698
  if (row == NULL) {
695
699
  continue;
696
700
  }
697
701
  uint32_t cn = row->first_child;
698
702
  while (cn != UINT32_MAX) {
699
- const yep_dnode* k = yep_dom_node(d->dom, cn);
700
- const yep_dnode* v = yep_dom_node(d->dom, k->next_sibling);
701
- yep_view kv = sv_view(d->dom, k->value);
703
+ const yep_dnode* k = yep_dom_node(dd, cn);
704
+ const yep_dnode* v = yep_dom_node(dd, k->next_sibling);
705
+ yep_view kv = sv_view(dd, k->value);
702
706
  int col_idx = -1;
703
707
  if (k->kind == YEP_DOM_SCALAR) {
704
708
  for (size_t c = 0; c < plan->ncols; c++) {
@@ -710,10 +714,10 @@ yeptris_document_plan_walk(YeptrisDocument doc, const yeptris_plan* plan, Yeptri
710
714
  }
711
715
  }
712
716
  if (col_idx >= 0) {
713
- const yep_dnode* val = dom_alias_final(d->dom, v);
717
+ const yep_dnode* val = dom_alias_final(dd, v);
714
718
  yep_result_col* col = &r->cols[col_idx];
715
719
  if (val != NULL && val->kind == YEP_DOM_SCALAR && val->tag_id != YEPTRIS_TAG_NULL) {
716
- yep_view sv = sv_view(d->dom, val->value);
720
+ yep_view sv = sv_view(dd, val->value);
717
721
  int filled = 0;
718
722
  switch (col->kind) {
719
723
  case YEP_PLAN_STR:
@@ -13,6 +13,7 @@
13
13
  #include <string.h>
14
14
 
15
15
  #include <yeptris/schema.h>
16
+ #include <yeptris/values.h> /* YeptrisValue: ST_ANY records (#238) */
16
17
 
17
18
  #include "dom/dom.h"
18
19
  #include "memory/allocator.h"
@@ -27,7 +28,9 @@ typedef struct {
27
28
  const yep_dom* dom;
28
29
  int oom;
29
30
  int overflow;
30
- int type_mismatch; /* node index */
31
+ int type_mismatch; /* node index */
32
+ uint32_t* seen_map; /* per-node: the mapping instance that last
33
+ * emitted — YEP_SF_FIRST_WINS skips a repeat */
31
34
  } yep_schema_ctx;
32
35
 
33
36
  static int col_put(yep_schema_ctx* c, uint32_t node, const void* rec, size_t size) {
@@ -84,6 +87,56 @@ static int emit_scalar(yep_schema_ctx* c, uint32_t node, uint32_t at) {
84
87
  case YEP_ST_NULL:
85
88
  col_put(c, node, &(uint8_t){0}, 0);
86
89
  return 1;
90
+ case YEP_ST_ANY: {
91
+ /* the resolver's verdict, verbatim: a YeptrisValue record
92
+ * (#238's headroom) — kind/tag_id from the node, the span
93
+ * borrowed, and the numeric payload converted when the
94
+ * verdict says so (the values-drain's conversion, DOM-side) */
95
+ YeptrisValue r = {0, 0, 0, 0, 0, 0, 0};
96
+ r.tag_id = n->tag_id;
97
+ r.off = (uint32_t)((const char*)v.p - c->dom->input_base);
98
+ r.len = v.len;
99
+ switch (n->tag_id) {
100
+ case YEPTRIS_TAG_NULL:
101
+ r.kind = YEP_V_NULL;
102
+ break;
103
+ case YEPTRIS_TAG_BOOL: {
104
+ r.kind = YEP_V_BOOL;
105
+ r.b = (uint8_t)(v.len > 0 && (v.p[0] == 't' || v.p[0] == 'T' || v.p[0] == 'y' ||
106
+ v.p[0] == 'Y' || v.p[0] == 'o' || v.p[0] == '1'));
107
+ break;
108
+ }
109
+ case YEPTRIS_TAG_INT:
110
+ case YEPTRIS_TAG_FLOAT: {
111
+ int64_t iv = 0;
112
+ double dv = 0;
113
+ int shape = 0;
114
+ size_t end = 0;
115
+ if (v.len != 0 && v.p != NULL &&
116
+ yep_json_number_scan(v.p, v.len, &end, &shape, &iv, &dv) != 0 && end == v.len) {
117
+ if (n->tag_id == YEPTRIS_TAG_INT) {
118
+ r.kind = YEP_V_INT;
119
+ r.p = (uint64_t)iv;
120
+ } else {
121
+ r.kind = YEP_V_FLOAT;
122
+ double fd = shape == 0 ? (double)iv : dv;
123
+ memcpy(&r.p, &fd, sizeof(fd));
124
+ }
125
+ } else {
126
+ r.kind = YEP_V_STR; /* the resolver said number, the
127
+ bytes disagree: keep the span */
128
+ }
129
+ break;
130
+ }
131
+ case YEPTRIS_TAG_TIMESTAMP:
132
+ r.kind = YEP_V_TIMESTAMP;
133
+ break;
134
+ default:
135
+ r.kind = YEP_V_STR;
136
+ break;
137
+ }
138
+ return col_put(c, node, &r, sizeof(r));
139
+ }
87
140
  default:
88
141
  return -1;
89
142
  }
@@ -140,10 +193,17 @@ static int emit(yep_schema_ctx* c, uint32_t node, uint32_t at) {
140
193
  size_t wl = strlen(cd->wire_name ? cd->wire_name : "");
141
194
  if (cd->wire_name != NULL && key.len == wl &&
142
195
  memcmp(key.p, cd->wire_name, wl) == 0) {
196
+ if ((cd->flags & YEP_SF_FIRST_WINS) && c->seen_map != NULL &&
197
+ c->seen_map[ci] == at + 1) {
198
+ break; /* a repeat within THIS mapping: first wins */
199
+ }
143
200
  int rc = emit(c, ci, val);
144
201
  if (rc != 1) {
145
202
  return rc;
146
203
  }
204
+ if ((cd->flags & YEP_SF_FIRST_WINS) && c->seen_map != NULL) {
205
+ c->seen_map[ci] = at + 1;
206
+ }
147
207
  break;
148
208
  }
149
209
  }
@@ -178,6 +238,7 @@ YEPTRIS_API YeptrisStatus yeptris_schema_load(const char* source, size_t len, Ye
178
238
  const yep_allocator* sys = yep_system_allocator();
179
239
  yep_engine* eng = yep_engine_create(sys);
180
240
  yep_dom* dom = yep_dom_create(sys);
241
+ uint32_t* seen_map_ptr = NULL;
181
242
  YeptrisStatus st = YEPTRIS_OK;
182
243
  yep_sink sink = {.on_event = yep_dom_on_event,
183
244
  .ctx = dom,
@@ -194,6 +255,9 @@ YEPTRIS_API YeptrisStatus yeptris_schema_load(const char* source, size_t len, Ye
194
255
  }
195
256
  if (schema == YEPTRIS_SCHEMA_11_COMPAT) {
196
257
  yep_engine_set_resolver(eng, yep_resolver_compat11());
258
+ dom->resolver = yep_resolver_compat11(); /* the DOM's direct
259
+ builders resolve their own tag_ids —
260
+ ST_ANY reads the NODE's verdict */
197
261
  }
198
262
  dom->input_base = source;
199
263
  dom->input_len = len;
@@ -204,7 +268,12 @@ YEPTRIS_API YeptrisStatus yeptris_schema_load(const char* source, size_t len, Ye
204
268
  }
205
269
 
206
270
  {
207
- yep_schema_ctx c = {desc, desc_len, cols, dom, 0, 0, -1};
271
+ uint32_t* seen = seen_map_ptr = calloc(desc_len, sizeof(uint32_t));
272
+ if (seen == NULL) {
273
+ st = YEPTRIS_ERROR_MEMORY;
274
+ goto out;
275
+ }
276
+ yep_schema_ctx c = {desc, desc_len, cols, dom, 0, 0, -1, seen};
208
277
  int rc = emit(&c, 0, dom->docs[0]);
209
278
  if (rc < 0 && c.type_mismatch >= 0) {
210
279
  yep_error_set(yep_error_tls(), YEP_ERR_UNEXPECTED, 0, 0, (uint32_t)c.type_mismatch,
@@ -232,6 +301,7 @@ YEPTRIS_API YeptrisStatus yeptris_schema_load(const char* source, size_t len, Ye
232
301
  }
233
302
 
234
303
  out:
304
+ free(seen_map_ptr);
235
305
  yep_dom_destroy(dom);
236
306
  yep_engine_destroy(eng);
237
307
  return st;
@@ -601,8 +601,8 @@ 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
- static YeptrisStatus tape_walk_lnt_fused(const char* p, size_t len, size_t open,
605
- yeptris_json_tape* t) {
604
+ YeptrisStatus yep_tape_walk_lenient_fused(const char* p, size_t len, size_t open,
605
+ yeptris_json_tape* t) {
606
606
  if (tape_carve(t, len) != YEPTRIS_OK) {
607
607
  return YEPTRIS_ERROR_MEMORY;
608
608
  }
@@ -1090,7 +1090,7 @@ YEPTRIS_API YeptrisStatus yeptris_parse_json_tape_lenient(const char* source, si
1090
1090
  }
1091
1091
  free(idx);
1092
1092
  #endif
1093
- return tape_walk_lnt_fused(source, len, at, tape);
1093
+ return yep_tape_walk_lenient_fused(source, len, at, tape);
1094
1094
  }
1095
1095
 
1096
1096
  /* scalar root: one record, same deferred split for numbers */
@@ -0,0 +1,19 @@
1
+ /* tape_in.h — the tape module's internal seams (not public ABI).
2
+ *
3
+ * parse.c's #342 slice-2 lazy route drives the fused lenient walk
4
+ * directly: the gate and opener checks it already ran must not be
5
+ * repeated (the lenient public entry re-gates). The walk's contract
6
+ * is the public entry's: gate-clean buffer, p[open] is '[' or '{',
7
+ * tail must be whitespace-only. */
8
+ #ifndef YEP_TAPE_IN_H
9
+ #define YEP_TAPE_IN_H
10
+
11
+ #include <stddef.h>
12
+
13
+ #include <yeptris/tape.h>
14
+
15
+ /* tape.c (was tape_walk_lnt_fused) */
16
+ YeptrisStatus yep_tape_walk_lenient_fused(const char* p, size_t len, size_t open,
17
+ yeptris_json_tape* t);
18
+
19
+ #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.12.3
4
+ version: 0.6.14.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-09-20 00:00:00.000000000 Z
11
+ date: 2026-09-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ffi
@@ -164,6 +164,7 @@ files:
164
164
  - vendor/libyeptris/src/yeptris/scan/scan.h
165
165
  - vendor/libyeptris/src/yeptris/schema.c
166
166
  - vendor/libyeptris/src/yeptris/tape.c
167
+ - vendor/libyeptris/src/yeptris/tape_in.h
167
168
  - vendor/libyeptris/src/yeptris/version.c
168
169
  - vendor/libyeptris/src/yeptris/visit/dom_visit.c
169
170
  - vendor/libyeptris/src/yeptris/visit/json_visit.c