yeptris 0.6.15.2-aarch64-linux → 0.6.16.2-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: 419470a0e038bec65880a3d764da46f072d7993e5aca9de71825a8428d5b2f98
4
- data.tar.gz: 19cf714c7f80abc07d8729420700ef2c08bf1a140de03c3e11a8bb6ff177a4f4
3
+ metadata.gz: 9505473e51a840dbf7c50d8ed7ae56b7e3a9b3592e26787ba0a0b231320bfe86
4
+ data.tar.gz: 78fbfa73c88cc29e5376cde241e310b1b4d65b878e96198faf733640bc695454
5
5
  SHA512:
6
- metadata.gz: 9d11c53e5305c76f780f431d73be9c62228e250ab164c4fd636429ad4fca4e3705b1d46b0e9cf7abed5d8a0662d0347a2399de0ce3a16f5eec987d05d0fb053f
7
- data.tar.gz: 5c24e95902b4ae44595710274ede3efbd605885d8cbc0747fc0965fcb8a2addb32ea8f18648606fef823d09a6f106d777d175f4e8ed0f1c8e74810f22d6cbee6
6
+ metadata.gz: 88c4aaa0e2b48a7b5ce25da76e0035e0cd2d83f2901bde878c6114cdf046ade9c9e7ae74ff70ebc3fe403ea98a5dce86504de060cec35a5fb2e4e6a58a4c83b6
7
+ data.tar.gz: ed0b241f30ae93ea79f8d4deae9ca1f33dd14c124698484c79ea9905e42149564487bbad18234f704125185b0583f7a740c76817eaa2c522b28fa2f3dce20240
@@ -17,6 +17,14 @@
17
17
  #include <stdlib.h>
18
18
  #include <string.h>
19
19
 
20
+ static rb_encoding* cr_utf8_enc;
21
+
22
+ /* Map keys repeat across records in CBOR corpora; interning shares one
23
+ * VALUE per distinct key (the json_ruby.c on_key pattern, #157). */
24
+ static VALUE cr_key_str(const char* p, size_t len) {
25
+ return rb_enc_interned_str(p, (long)len, cr_utf8_enc);
26
+ }
27
+
20
28
  #include "dom/dom.h"
21
29
  #include "doc.h" /* the public YeptrisDocument wrapper: ->dom */
22
30
  #include <yeptris/cbor.h>
@@ -91,8 +99,17 @@ static VALUE cr_walk(const yep_dom* d, uint32_t id) {
91
99
  VALUE h = HASH_NEW_CAPA(pairs);
92
100
  uint32_t c = n->first_child;
93
101
  for (long i = 0; i < pairs && c != UINT32_MAX; i++) {
94
- VALUE k = cr_walk(d, c);
95
- c = d->nodes[c].next_sibling;
102
+ VALUE k;
103
+ const yep_dnode* kn = &d->nodes[c];
104
+ if (kn->kind == YEP_DOM_SCALAR && kn->tag_id == YEPTRIS_TAG_STR) {
105
+ uint32_t klen = 0;
106
+ const char* kp = cr_view(d, kn->value, &klen);
107
+ k = cr_key_str(kp, klen);
108
+ c = kn->next_sibling;
109
+ } else {
110
+ k = cr_walk(d, c);
111
+ c = d->nodes[c].next_sibling;
112
+ }
96
113
  VALUE v = (c != UINT32_MAX) ? cr_walk(d, c) : Qnil;
97
114
  if (c != UINT32_MAX) {
98
115
  c = d->nodes[c].next_sibling;
@@ -107,6 +124,9 @@ static VALUE cr_walk(const yep_dom* d, uint32_t id) {
107
124
  }
108
125
 
109
126
  VALUE yep_rb_cbor_load(const char* p, size_t len, int strict) {
127
+ if (cr_utf8_enc == NULL) {
128
+ cr_utf8_enc = rb_utf8_encoding();
129
+ }
110
130
  YeptrisStatus st = YEPTRIS_OK;
111
131
  /* the DOM borrows the input buffer zero-copy; the walk ALLOCATES,
112
132
  * and a GC compaction mid-walk moves the caller's String — the
@@ -423,8 +423,51 @@ static VALUE native_gc_mode_set(VALUE self, VALUE mode) {
423
423
  return mode;
424
424
  }
425
425
 
426
+ /* ABI-drift self-check (the #157-audit silent-nil class): the bundle
427
+ * is compiled against a specific yep_dom layout; engine drift makes
428
+ * the walkers read fields at stale offsets — cbor_load returned NIL
429
+ * on valid CBOR in the dev-checkout audit, with no error anywhere.
430
+ * Both walkers must round-trip a known document at load, or Init
431
+ * raises LoadError: lib/yeptris.rb's existing rescue falls back to
432
+ * the FFI ladder (loudly), and the stale bundle never answers with
433
+ * garbage. Runs inside rb_protect so a drifted walk raises instead of
434
+ * crashing the load. */
435
+ static VALUE native_self_check_body(VALUE unused) {
436
+ /* JSON: {"a"=>[1, 2.5, "x", true, nil]} */
437
+ const char* json = "{\"a\":[1,2.5,\"x\",true,null]}";
438
+ VALUE j = yep_rb_parse_json(json, strlen(json), 0);
439
+ if (j == Qundef || !RB_TYPE_P(j, T_HASH)) return Qfalse;
440
+ VALUE a = rb_hash_lookup(j, rb_str_new_cstr("a"));
441
+ if (!RB_TYPE_P(a, T_ARRAY) || RARRAY_LEN(a) != 5) return Qfalse;
442
+ if (!RB_INTEGER_TYPE_P(rb_ary_entry(a, 0))) return Qfalse;
443
+ if (!RB_FLOAT_TYPE_P(rb_ary_entry(a, 1))) return Qfalse;
444
+ if (!RB_TYPE_P(rb_ary_entry(a, 2), T_STRING)) return Qfalse;
445
+ if (rb_ary_entry(a, 3) != Qtrue || !NIL_P(rb_ary_entry(a, 4))) return Qfalse;
446
+
447
+ /* CBOR: A1 61 6B 01 = {"k" => 1} (canonical, hand-encoded) */
448
+ const char cbor[] = "\xA1\x61\x6B\x01";
449
+ VALUE c = yep_rb_cbor_load(cbor, sizeof(cbor) - 1, 0);
450
+ if (c == Qundef || !RB_TYPE_P(c, T_HASH)) return Qfalse;
451
+ VALUE one = rb_hash_lookup(c, rb_str_new_cstr("k"));
452
+ if (!RB_INTEGER_TYPE_P(one) || !rb_eql(one, INT2FIX(1))) return Qfalse;
453
+ return Qtrue;
454
+ }
455
+
456
+ static void native_self_check(void) {
457
+ int state = 0;
458
+ VALUE ok = rb_protect(native_self_check_body, Qnil, &state);
459
+ if (state != 0 || ok != Qtrue) {
460
+ if (state != 0) rb_set_errinfo(Qnil);
461
+ rb_raise(rb_eLoadError,
462
+ "yeptris: native materializer failed its ABI self-check "
463
+ "(stale build against a drifted engine?) — refusing to "
464
+ "register; the FFI ladder will carry the load");
465
+ }
466
+ }
467
+
426
468
  RUBY_FUNC_EXPORTED void Init_native(void) {
427
469
  utf8_enc = rb_utf8_encoding();
470
+ native_self_check();
428
471
  VALUE mYep = rb_define_module("Yeptris");
429
472
  VALUE mNat = rb_define_module_under(mYep, "Native");
430
473
  rb_define_singleton_method(mNat, "load_json", native_load_json, -1);
@@ -8,6 +8,11 @@ class Yeptris::Document
8
8
  # explicit free path and the finalizer both flip the same flag.
9
9
  Freed = Struct.new(:state) # :alive | :freed
10
10
 
11
+ # The raw C handle — the Yeptris::Node wrappers cache by its
12
+ # address; stream-children wrappers (Document.without_finalizer)
13
+ # carry it without registering a finalizer (#182).
14
+ attr_reader :c_ptr
15
+
11
16
  # The schema this document was parsed with (a parse property).
12
17
  attr_reader :parse_schema
13
18
 
@@ -80,6 +85,18 @@ class Yeptris::Document
80
85
  new(c_ptr, Freed.new(:alive), schema)
81
86
  end
82
87
 
88
+ # #182: the stream-children wrapper owns NOTHING (the parent
89
+ # Document wrapper is the sole freer of the C memory). A plain
90
+ # Document.new would register a finalizer on a c_ptr we don't own;
91
+ # a parse_stream on a multi-doc stream would free the same pointer
92
+ # three times at GC → SIGABRT. This factory builds the wrapper
93
+ # WITHOUT a finalizer; the wrapper's lifetime follows the owner.
94
+ def self.without_finalizer(c_ptr)
95
+ doc = new(c_ptr)
96
+ ObjectSpace.undefine_finalizer(doc)
97
+ doc
98
+ end
99
+
83
100
  def ensure_alive!
84
101
  raise Yeptris::FreedError, "document is freed" if @freed.state == :freed
85
102
  end
data/lib/yeptris/json.rb CHANGED
@@ -119,7 +119,7 @@ module Yeptris
119
119
  end
120
120
  end
121
121
 
122
- def walk_tape(src, tape)
122
+ def walk_tape(src, tape, strict_dup = STRICT_DUPLICATE_KEYS)
123
123
  n = tape[:count]
124
124
  kinds = tape[:kinds].read_bytes(n).unpack("C*")
125
125
  offs = tape[:offs].read_bytes(n * 4).unpack("V*")
@@ -139,7 +139,7 @@ module Yeptris
139
139
 
140
140
  docs = [nil] # record 0 (DOC) pre-consumed — the root's slot
141
141
  stack = []
142
- key_sets = STRICT_DUPLICATE_KEYS ? [{}] : nil
142
+ key_sets = strict_dup ? [{}] : nil
143
143
  pending_key = nil
144
144
  i = 1
145
145
  while i < n
@@ -152,8 +152,12 @@ module Yeptris
152
152
  place(docs, stack, pending_key) { {} }
153
153
  pending_key = nil
154
154
  when T_CLOSE
155
+ # Key frames are pushed per MAP_OPEN — an ARRAY close must
156
+ # not pop the enclosing map's frame (issue found by ea's CI:
157
+ # {"a":[1],"b":[2],"c":[3]} exhausted the frames and the next
158
+ # map key hit key_sets.last.key? on nil under strict mode).
159
+ key_sets&.pop if stack.last.is_a?(Hash)
155
160
  stack.pop
156
- key_sets&.pop
157
161
  when T_STR
158
162
  # empty-string literals are frozen+shared since Ruby 3.4
159
163
  # (and `+""` binds after the method chain anyway), so the
@@ -256,8 +260,8 @@ module Yeptris
256
260
  place(docs, stack, pending_key) { {} }
257
261
  pending_key = nil
258
262
  when ValueML::CLOSE
263
+ key_sets&.pop if stack.last.is_a?(Hash)
259
264
  stack.pop
260
- key_sets&.pop
261
265
  when ValueML::V_STR
262
266
  text = arena.byteslice(offs[i], lens[i])
263
267
  if ikeys[i] == 1 && !stack.empty? && stack.last.is_a?(Hash)
Binary file
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+
5
+ module Yeptris
6
+ module Psych
7
+ # The minimal ClassLoader face the ScalarScanner port drives
8
+ # (stdlib's loader resolves through the class hierarchy; the
9
+ # scanner only needs the date class and symbolize).
10
+ class ClassLoader
11
+ def date
12
+ Date
13
+ end
14
+
15
+ def symbolize str
16
+ str.to_sym
17
+ end
18
+
19
+ def load name
20
+ Object.const_get(name)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+
5
+ module Yeptris
6
+ module Psych
7
+ ###
8
+ # Scan scalars for built in types — the stdlib Psych::ScalarScanner
9
+ # port (#179): same regexes, same precedence, same edge verdicts.
10
+ # The load path types through the C resolver; this class is the
11
+ # standalone API stdlib code (and Psych's own tests) drive directly.
12
+ class ScalarScanner
13
+ # Taken from http://yaml.org/type/timestamp.html
14
+ TIME = /^-?\d{4}-\d{1,2}-\d{1,2}(?:[Tt]|\s+)\d{1,2}:\d\d:\d\d(?:\.\d*)?(?:\s*(?:Z|[-+]\d{1,2}:?(?:\d\d)?))?$/
15
+
16
+ # Taken from http://yaml.org/type/float.html
17
+ # Base 60, [-+]inf and NaN are handled separately
18
+ FLOAT = /^(?:[-+]?([0-9][0-9_,]*)?\.[0-9]*([eE][-+][0-9]+)?(?# base 10))$/x
19
+
20
+ # Taken from http://yaml.org/type/int.html and modified to ensure at least one numerical symbol exists
21
+ INTEGER_STRICT = /^(?:[-+]?0b[_]*[0-1][0-1_]* (?# base 2)
22
+ |[-+]?0[_]*[0-7][0-7_]* (?# base 8)
23
+ |[-+]?(0|[1-9][0-9_]*) (?# base 10)
24
+ |[-+]?0x[_]*[0-9a-fA-F][0-9a-fA-F_]* (?# base 16))$/x
25
+
26
+ # Same as above, but allows commas.
27
+ # Not to YML spec, but kept for backwards compatibility
28
+ INTEGER_LEGACY = /^(?:[-+]?0b[_,]*[0-1][0-1_,]* (?# base 2)
29
+ |[-+]?0[_,]*[0-7][0-7_,]* (?# base 8)
30
+ |[-+]?(?:0|[1-9](?:[0-9]|,[0-9]|_[0-9])*) (?# base 10)
31
+ |[-+]?0x[_,]*[0-9a-fA-F][0-9a-fA-F_,]* (?# base 16))$/x
32
+
33
+ BOOLEAN_TRUE = /^(yes|true|on)$/i
34
+ BOOLEAN_FALSE = /^(no|false|off)$/i
35
+
36
+ attr_reader :class_loader
37
+
38
+ # Create a new scanner
39
+ def initialize class_loader = ClassLoader, strict_integer: false, parse_symbols: true
40
+ @symbol_cache = {}
41
+ @class_loader = class_loader
42
+ @strict_integer = strict_integer
43
+ @parse_symbols = parse_symbols
44
+ end
45
+
46
+ # Tokenize +string+ returning the Ruby object
47
+ def tokenize string
48
+ return nil if string.empty?
49
+ return @symbol_cache[string] if @symbol_cache.key?(string)
50
+ integer_regex = @strict_integer ? INTEGER_STRICT : INTEGER_LEGACY
51
+ # Check for a String type, being careful not to get caught by hash keys, hex values, and
52
+ # special floats (e.g., -.inf).
53
+ if string.match?(%r{^[^\d.:-]?[[:alpha:]_\s!@#$%\^&*(){}<>|/\\~;=]+}) || string.match?(/\n/)
54
+ return string if string.length > 5
55
+
56
+ if string.match?(/^[^ytonf~]/i)
57
+ string
58
+ elsif string == '~' || string.match?(/^null$/i)
59
+ nil
60
+ elsif string.match?(BOOLEAN_TRUE)
61
+ true
62
+ elsif string.match?(BOOLEAN_FALSE)
63
+ false
64
+ else
65
+ string
66
+ end
67
+ elsif string.match?(TIME)
68
+ begin
69
+ parse_time string
70
+ rescue ArgumentError
71
+ string
72
+ end
73
+ elsif string.match?(/^\d{4}-(?:1[012]|0\d|\d)-(?:[12]\d|3[01]|0\d|\d)$/)
74
+ begin
75
+ class_loader.date.strptime(string, '%F', Date::GREGORIAN)
76
+ rescue ArgumentError
77
+ string
78
+ end
79
+ elsif string.match?(/^\+?\.inf$/i)
80
+ Float::INFINITY
81
+ elsif string.match?(/^-\.inf$/i)
82
+ -Float::INFINITY
83
+ elsif string.match?(/^\.nan$/i)
84
+ Float::NAN
85
+ elsif @parse_symbols && string.match?(/^:./)
86
+ if string =~ /^:(["'])(.*)\1/
87
+ @symbol_cache[string] = class_loader.symbolize($2.sub(/^:/, ''))
88
+ else
89
+ @symbol_cache[string] = class_loader.symbolize(string.sub(/^:/, ''))
90
+ end
91
+ elsif string.match?(/^[-+]?[0-9][0-9_]*(:[0-5]?[0-9]){1,2}$/)
92
+ i = 0
93
+ string.split(':').each_with_index do |n, e|
94
+ i += (n.to_i * 60**(e - 2).abs)
95
+ end
96
+ i
97
+ elsif string.match?(/^[-+]?[0-9][0-9_]*(:[0-5]?[0-9]){1,2}\.[0-9_]*$/)
98
+ i = 0
99
+ string.split(':').each_with_index do |n, e|
100
+ i += (n.to_f * 60**(e - 2).abs)
101
+ end
102
+ i
103
+ elsif string.match?(FLOAT)
104
+ if string.match?(/\A[-+]?\.\Z/)
105
+ string
106
+ else
107
+ Float(string.delete(',_').gsub(/\.([Ee]|$)/, '\1'))
108
+ end
109
+ elsif string.match?(integer_regex)
110
+ parse_int string
111
+ else
112
+ string
113
+ end
114
+ end
115
+
116
+ # Parse and return an int from +string+
117
+ def parse_int string
118
+ Integer(string.delete(',_'))
119
+ end
120
+
121
+ ###
122
+ # Parse and return a Time from +string+
123
+ def parse_time string
124
+ date, time = *(string.split(/[Tt]|\s+/, 2))
125
+ (yy, m, dd) = date.match(/^(-?\d{4})-(\d{1,2})-(\d{1,2})/).captures.map { |x| x.to_i }
126
+ md = time.match(/(\d+:\d+:\d+)(?:\.(\d*))?\s*(Z|[-+]\d+(:\d\d)?)?/)
127
+
128
+ (hh, mm, ss) = md[1].split(':').map { |x| x.to_i }
129
+ us = (md[2] ? Rational("0.#{md[2]}") : 0) * 1_000_000
130
+
131
+ time = Time.utc(yy, m, dd, hh, mm, ss, us)
132
+
133
+ return time if 'Z' == md[3]
134
+ return Time.at(time.to_i, us) unless md[3]
135
+
136
+ tz = md[3].match(/^([+\-]?\d{1,2})\:?(\d{1,2})?$/)[1..-1].compact.map { |digit| Integer(digit, 10) }
137
+ offset = tz.first * 3600
138
+
139
+ if offset < 0
140
+ offset -= ((tz[1] || 0) * 60)
141
+ else
142
+ offset += ((tz[1] || 0) * 60)
143
+ end
144
+
145
+ Time.new(yy, m, dd, hh, mm, ss + us / 1_000_000r, offset)
146
+ end
147
+ end
148
+ end
149
+ end
data/lib/yeptris/psych.rb CHANGED
@@ -22,6 +22,9 @@ require "yeptris"
22
22
  # document without materializing.
23
23
  module Yeptris
24
24
  module Psych
25
+ autoload :ClassLoader, "yeptris/psych/class_loader"
26
+ autoload :ScalarScanner, "yeptris/psych/scalar_scanner"
27
+
25
28
  # The tag registries (Psych's class-level API, #95 bug 4):
26
29
  # load_tags maps a serialized tag to the Class that revives it;
27
30
  # dump_tags overrides the emitted tag for a Class. Consulted by
@@ -258,18 +261,28 @@ module Yeptris
258
261
  ::Yeptris::Psych.dump(obj, io, options)
259
262
  end
260
263
 
261
- def load_stream(yaml, **kwargs)
264
+ def load_stream(yaml, **kwargs, &block)
262
265
  # materialize each document's root directly — the stream
263
266
  # children share one C document, so their handles would all
264
- # resolve to the first document's tree
267
+ # resolve to the first document's tree. stdlib's block form
268
+ # yields each loaded document's object (#179 round 4).
265
269
  doc = Yeptris::Document.parse(yaml, schema: :compat_11)
266
270
  return nil if doc.nil? # the legal empty stream
267
271
 
268
272
  begin
269
- (0...doc.document_count).map { |i| doc.root(i).to_ruby }
273
+ docs = (0...doc.document_count).map { |i| doc.root(i).to_ruby }
270
274
  ensure
271
275
  doc.free
272
276
  end
277
+ docs.each { |d| block.call(d) } if block
278
+ docs
279
+ end
280
+
281
+ # safe_load_stream: stdlib's surface — the stream form of
282
+ # safe_load. Our load_stream is already safe-by-default
283
+ # (Psych 5 semantics), so this is the yielding wrapper.
284
+ def safe_load_stream(yaml, **kwargs, &block)
285
+ load_stream(yaml, **kwargs, &block)
273
286
  end
274
287
 
275
288
  # The first document's node tree (no Ruby materialization).
@@ -291,13 +304,22 @@ module Yeptris
291
304
  raise SyntaxError.from_parse_error(e)
292
305
  end
293
306
 
294
- def parse_stream(yaml)
307
+ def parse_stream(yaml, &block)
308
+ # stdlib yields each document to a block if one was given (and
309
+ # the stream it returns holds the same children either way).
310
+ # #182: the wrapper is the sole owner of the C memory — the
311
+ # stream merely REFERENCES it; the wrapper's own finalizer
312
+ # handles GC-free. (The block-yielding form is tracked under
313
+ # #179: the current implementation materializes eagerly; the
314
+ # block is only honored as a no-op convenience.)
295
315
  doc = Yeptris::Document.parse(yaml, schema: :compat_11)
296
316
  return nil if doc.nil? || doc.document_count.zero?
297
317
 
298
318
  stream = Nodes::Stream.new
299
319
  (0...doc.document_count).each do |i|
300
- stream.children << Nodes::Builder.document_stream_child(doc, i)
320
+ child = Nodes::Builder.document_stream_child(doc, i)
321
+ stream.children << child
322
+ block.call(child) if block # stdlib's yielding form (#179 round 4)
301
323
  end
302
324
  # ownership: the DOCUMENT wrapper is the sole owner — its own
303
325
  # finalizer (pointer-only closure) frees the C memory; the
@@ -308,6 +330,8 @@ module Yeptris
308
330
  rescue Yeptris::ParseError => e
309
331
  raise SyntaxError.from_parse_error(e)
310
332
  ensure
333
+ # the returned stream OWNS the doc's lifetime (its free delegates
334
+ # to the owner); only free here when the stream wasn't built
311
335
  doc&.free if doc && !stream
312
336
  end
313
337
 
@@ -440,6 +464,10 @@ module Yeptris
440
464
  # tree stay valid while the tree is reachable; a GC finalizer
441
465
  # releases the C memory when it is not.
442
466
  class Document < Node
467
+ # The document's root as a Ruby object (the stream-yield face).
468
+ def to_ruby
469
+ children.first&.to_ruby
470
+ end
443
471
  attr_reader :version, :tags
444
472
 
445
473
  def initialize(version = [], tags = {})
@@ -522,8 +550,12 @@ module Yeptris
522
550
  # stream owns the yeptris document)
523
551
  def document_stream_child(doc, index)
524
552
  root = doc.root(index)
525
- d = Document.new
526
- d.handle = root&.document
553
+ # #182's root cause: three per-stream Document wrappers all
554
+ # capture the same c_ptr in their finalizers → three frees.
555
+ # The stream-children WRAPPER owns nothing (the owner wrapper
556
+ # is the real Document; this one just references it). Build
557
+ # a non-owning wrapper with no finalizer.
558
+ d = Document.send(:new, root&.document, false)
527
559
  d.children << node(root) if root
528
560
  d
529
561
  end
@@ -66,6 +66,92 @@ module Yeptris
66
66
  end
67
67
  end
68
68
 
69
+ # #184 (lutaml-model KV path): zip Schema columns into record
70
+ # hashes ready for Serializable.instantiate. Mapping root → one
71
+ # hash; sequence-of-mappings → one hash per element; nested
72
+ # mappings → nested hashes. Returns Array<Hash> always. when_attribute
73
+ # / polymorphic stay out of scope (interpretive fallback).
74
+ def load_records(source, schema: :core_12, desc:, capacity: 64)
75
+ cols = load(source, schema: schema, desc: desc, capacity: capacity)
76
+ zip_records(desc, cols)
77
+ end
78
+
79
+ def zip_records(desc, cols)
80
+ root = desc[0] || {}
81
+ case root[:kind]
82
+ when :sequence
83
+ child_start = root[:child_index] || 1
84
+ child_count = root[:child_count] || 0
85
+ return [] if child_count.zero?
86
+
87
+ child = desc[child_start]
88
+ if child && child[:kind] == :mapping
89
+ field_start = child[:child_index] || (child_start + 1)
90
+ field_count = child[:child_count] || 0
91
+ fields = desc[field_start, field_count] || []
92
+ field_cols = cols[field_start, field_count] || []
93
+ nrows = field_cols.map(&:length).max || 0
94
+ Array.new(nrows) do |r|
95
+ h = {}
96
+ fields.each_with_index do |f, i|
97
+ next unless f[:wire_name]
98
+ col = field_cols[i] || []
99
+ # ABSENT keys are OMITTED, not nil-filled (lutaml-model's
100
+ # load-bearing edge: absent seeds defaults +
101
+ # using_default?; explicit nil is a present value with
102
+ # different render semantics). The C columns fill in
103
+ # document order — col.length > r means present.
104
+ next if col.length <= r
105
+
106
+ h[f[:wire_name].to_sym] = materialize_field(f, desc, cols, col[r])
107
+ end
108
+ h
109
+ end
110
+ else
111
+ (cols[child_start] || []).map { |v| { value: v } }
112
+ end
113
+ when :mapping
114
+ field_start = root[:child_index] || 1
115
+ field_count = root[:child_count] || 0
116
+ fields = desc[field_start, field_count] || []
117
+ field_cols = cols[field_start, field_count] || []
118
+ h = {}
119
+ fields.each_with_index do |f, i|
120
+ next unless f[:wire_name]
121
+ col = field_cols[i] || []
122
+ next if col.empty? # absent: omitted, not nil-filled (#184)
123
+
124
+ h[f[:wire_name].to_sym] = materialize_field(f, desc, cols, col[0])
125
+ end
126
+ [h]
127
+ else
128
+ [{ value: cols[0]&.first }]
129
+ end
130
+ end
131
+ private_class_method :zip_records
132
+
133
+ def materialize_field(field, desc, cols, cell)
134
+ case field[:kind]
135
+ when :mapping
136
+ start = field[:child_index] || 0
137
+ count = field[:child_count] || 0
138
+ return nil if count.zero? || cell.nil?
139
+
140
+ nested = {}
141
+ desc[start, count]&.each_with_index do |nf, i|
142
+ next unless nf[:wire_name]
143
+ ncol = cols[start + i] || []
144
+ nested[nf[:wire_name].to_sym] = ncol.is_a?(Array) ? (ncol[0] rescue cell) : cell
145
+ end
146
+ nested
147
+ when :sequence
148
+ cell.is_a?(Array) ? cell : Array(cell)
149
+ else
150
+ cell
151
+ end
152
+ end
153
+ private_class_method :materialize_field
154
+
69
155
  def compile(desc)
70
156
  desc.map do |n|
71
157
  { kind: KIND.fetch(n.fetch(:kind)),
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.15.2".freeze
6
+ VERSION = "0.6.16.2".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
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.15
6
+ VERSION 0.6.16
7
7
  DESCRIPTION "Ultra-fast YAML 1.2 parser, emitter and streamer in C"
8
8
  LANGUAGES C CXX
9
9
  )
@@ -241,6 +241,9 @@ void dom_link(yep_dom* d, uint32_t parent, uint32_t child) {
241
241
  }
242
242
  p->last_child = child;
243
243
  p->count++;
244
+ /* every link changes SOME container's child list (#377's cache) */
245
+ d->child_cache_id = UINT32_MAX;
246
+ d->child_cache_len = 0;
244
247
  }
245
248
 
246
249
  /* ---- mutation side tables (64-2a) ---- */
@@ -485,6 +488,9 @@ yep_dom* yep_dom_create(const yep_allocator* sys) {
485
488
  * document that decode-only consumers (CBOR load, serialize, free)
486
489
  * never touch; yep_dom_handles() materializes it on first use */
487
490
  d->handles = NULL;
491
+ d->child_cache = NULL;
492
+ d->child_cache_id = UINT32_MAX;
493
+ d->child_cache_len = 0;
488
494
  if (yep_mutex_init(&d->midx.mu) != 0) {
489
495
  yep_pool_destroy(pool);
490
496
  yep_free(sys, d);
@@ -513,6 +519,69 @@ struct yep_hpool* yep_dom_handles(yep_dom* d) {
513
519
  return h;
514
520
  }
515
521
 
522
+ /* #377 (ruby #168): seq_at/map_at were O(i) sibling walks — the
523
+ * bindings' per-element loops made an 80k-row sequence quadratic
524
+ * (the relaton index: 238-328s where stdlib takes ~3s). The bulk
525
+ * drain (yeptris_node_children) fixed the bindings; THIS fixes the
526
+ * accessor: the last indexed container's child ids cache under the
527
+ * lazy-init mutex, O(1) after the first call. Invalidated by every
528
+ * child-list mutation (dom_invalidate_child_cache). */
529
+ void dom_invalidate_child_cache(yep_dom* d) {
530
+ if (d == NULL) {
531
+ return;
532
+ }
533
+ d->child_cache_id = UINT32_MAX;
534
+ d->child_cache_len = 0;
535
+ }
536
+
537
+ /* Returns child id at index (UINT32_MAX when out of range); the
538
+ * container's child count rides *count_out. */
539
+ uint32_t dom_indexed_child(yep_dom* d, uint32_t cid, size_t index, uint32_t* count_out) {
540
+ const yep_dnode* n = yep_dom_node(d, cid);
541
+ if (n == NULL) {
542
+ *count_out = 0;
543
+ return UINT32_MAX;
544
+ }
545
+ *count_out = n->count;
546
+ 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) {
557
+ yep_free(d->sys, d->child_cache);
558
+ d->child_cache = NULL;
559
+ d->child_cache_len = 0;
560
+ if (n->count > 0) {
561
+ uint32_t* arr = yep_alloc(d->sys, n->count * sizeof(uint32_t));
562
+ if (arr != NULL) {
563
+ uint32_t k = 0;
564
+ for (uint32_t id = n->first_child; id != UINT32_MAX && k < n->count;) {
565
+ const yep_dnode* cn = yep_dom_node(d, id);
566
+ if (cn == NULL) {
567
+ break;
568
+ }
569
+ arr[k++] = id;
570
+ id = cn->next_sibling;
571
+ }
572
+ d->child_cache = arr;
573
+ d->child_cache_len = k;
574
+ }
575
+ }
576
+ d->child_cache_id = cid;
577
+ }
578
+ yep_mutex_unlock(&d->midx.mu);
579
+ if (index < d->child_cache_len) {
580
+ return d->child_cache[index];
581
+ }
582
+ return UINT32_MAX;
583
+ }
584
+
516
585
  void yep_dom_destroy(yep_dom* d) {
517
586
  if (d == NULL) {
518
587
  return;
@@ -523,6 +592,7 @@ void yep_dom_destroy(yep_dom* d) {
523
592
  yep_free(d->sys, d->mut_att);
524
593
  yep_free(d->sys, d->mut_depth);
525
594
  yep_hpool_destroy(d->handles);
595
+ yep_free(d->sys, d->child_cache);
526
596
  yep_pool_destroy(d->pool);
527
597
  yep_free(d->sys, d);
528
598
  }
@@ -114,6 +114,13 @@ typedef struct yep_dom {
114
114
  * YeptrisNode wrappers here, so read-only document sharing across
115
115
  * threads is safe; parse-path pools stay single-threaded */
116
116
  struct yep_hpool* handles;
117
+ /* #377: the lazy indexed-children cache — child ids of the LAST
118
+ * container indexed via seq_at/map_at. Built once per container
119
+ * under the lazy-init mutex (read-only sharing), O(1) after;
120
+ * invalidated by every child-list mutation. NULL when idle. */
121
+ uint32_t* child_cache;
122
+ uint32_t child_cache_id; /* UINT32_MAX = empty */
123
+ uint32_t child_cache_len;
117
124
  yep_dnode* nodes;
118
125
  uint32_t ncount, ncap;
119
126
  uint32_t* docs; /* document root node ids */
@@ -200,6 +207,9 @@ void dom_mut_set_depth(yep_dom* d, uint32_t id, uint16_t depth);
200
207
  struct yep_hpool* yep_hpool_create(const yep_allocator* sys);
201
208
  /* Lazy handle-pool acquisition (#157): NULL dom or OOM stays NULL. */
202
209
  struct yep_hpool* yep_dom_handles(yep_dom* d);
210
+ /* #377: the lazy indexed-children cache (seq_at/map_at's O(1) leg) */
211
+ void dom_invalidate_child_cache(yep_dom* d);
212
+ uint32_t dom_indexed_child(yep_dom* d, uint32_t cid, size_t index, uint32_t* count_out);
203
213
  void yep_hpool_destroy(struct yep_hpool* p);
204
214
  void* yep_hpool_alloc(struct yep_hpool* p, size_t size, size_t align);
205
215
 
@@ -268,6 +268,8 @@ static int unlink_child(yep_dom* d, uint32_t parent, uint32_t child) {
268
268
  p->last_child = c;
269
269
  }
270
270
  }
271
+ d->child_cache_id = UINT32_MAX; /* #377: the list changed */
272
+ d->child_cache_len = 0;
271
273
  d->nodes[child].next_sibling = UINT32_MAX;
272
274
  dom_mut_set_att(d, child, 0);
273
275
  p->count--;
@@ -646,11 +646,12 @@ YEPTRIS_API YeptrisNode yeptris_node_seq_at(YeptrisNode handle, size_t index) {
646
646
  if (n == NULL || n->kind != YEP_DOM_SEQUENCE || index >= n->count) {
647
647
  return NULL;
648
648
  }
649
- const yep_dom* dom = ((yeptris_node*)handle)->doc->dom;
650
- uint32_t id = n->first_child;
651
- for (size_t i = 0; i < index && id != UINT32_MAX; i++) {
652
- const yep_dnode* cur = yep_dom_node(dom, id);
653
- id = cur ? cur->next_sibling : UINT32_MAX;
649
+ yep_dom* dom = ((yeptris_node*)handle)->doc->dom;
650
+ uint32_t count = 0;
651
+ /* the node id IS its dnode index */
652
+ uint32_t id = dom_indexed_child(dom, ((yeptris_node*)handle)->id, index, &count); /* #377 */
653
+ if (id == UINT32_MAX) {
654
+ return NULL;
654
655
  }
655
656
  return wrap((yeptris_node*)handle, id);
656
657
  }
@@ -685,10 +686,11 @@ YEPTRIS_API int yeptris_node_map_at(YeptrisNode handle, size_t index, YeptrisNod
685
686
  return -1;
686
687
  }
687
688
  yeptris_node* h = (yeptris_node*)handle;
688
- const yep_dom* d = h->doc->dom;
689
- uint32_t child = n->first_child;
690
- for (size_t i = 0; i < index * 2; i++) {
691
- child = d->nodes[child].next_sibling; /* pairs are key,value,… */
689
+ yep_dom* d = h->doc->dom;
690
+ uint32_t count = 0;
691
+ uint32_t child = dom_indexed_child(d, h->id, (size_t)index * 2, &count); /* #377 */
692
+ if (child == UINT32_MAX) {
693
+ return -1;
692
694
  }
693
695
  if (key != NULL) {
694
696
  *key = wrap(h, child);
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.15.2
4
+ version: 0.6.16.2
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-21 00:00:00.000000000 Z
11
+ date: 2026-09-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ffi
@@ -50,11 +50,13 @@ files:
50
50
  - lib/yeptris/native-3.3.so
51
51
  - lib/yeptris/node.rb
52
52
  - lib/yeptris/psych.rb
53
+ - lib/yeptris/psych/class_loader.rb
53
54
  - lib/yeptris/psych/coder_shim.rb
54
55
  - lib/yeptris/psych/drop_in.rb
55
56
  - lib/yeptris/psych/encodable.rb
56
57
  - lib/yeptris/psych/handler.rb
57
58
  - lib/yeptris/psych/parser.rb
59
+ - lib/yeptris/psych/scalar_scanner.rb
58
60
  - lib/yeptris/psych/visitors.rb
59
61
  - lib/yeptris/schema.rb
60
62
  - lib/yeptris/valueml.rb