yeptris 0.6.15.1 → 0.6.16.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 +4 -4
- data/ext/yeptris_native/cbor_ruby.c +22 -2
- data/lib/yeptris/document.rb +31 -8
- data/lib/yeptris/materializer.rb +5 -1
- data/lib/yeptris/node.rb +9 -0
- data/lib/yeptris/psych/visitors.rb +31 -5
- data/lib/yeptris/psych.rb +31 -10
- data/lib/yeptris/schema.rb +86 -0
- data/lib/yeptris.rb +1 -1
- data/vendor/libyeptris/CMakeLists.txt +1 -1
- data/vendor/libyeptris/src/yeptris/dom/dom.c +70 -0
- data/vendor/libyeptris/src/yeptris/dom/dom.h +10 -0
- data/vendor/libyeptris/src/yeptris/dom/mutate.c +2 -0
- data/vendor/libyeptris/src/yeptris/emit/writer.c +16 -2
- data/vendor/libyeptris/src/yeptris/parse.c +11 -9
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 36b4d1444bde48fba47b0fd89851f5ebe8cd7a666c7e3faa8652caf4bb37a60f
|
|
4
|
+
data.tar.gz: 7f113394e8f82d502425610637641716af695a835ba01f6ce5e611c5073c6d55
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cec260d2c0b2c10d152336d0809eca8d9d15c4fc8e52ddac94fa01b8850c3f59cebb74f0ddd78b48f751ce9fb6a1f74e6f9646d81a237144645810949c93b7a0
|
|
7
|
+
data.tar.gz: efa7dbc2609e25b0d25bb0d93c7d877223808d88343299e7dfd60ed9442eb31806347a4aa1f255452bfc219af2a5d4c34b66f5d50346d839de59ee256dafe0da
|
|
@@ -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
|
|
95
|
-
|
|
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
|
data/lib/yeptris/document.rb
CHANGED
|
@@ -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
|
|
|
@@ -20,7 +25,7 @@ class Yeptris::Document
|
|
|
20
25
|
# always yields the same Ruby object (identity for aliases/eql?),
|
|
21
26
|
# cleared at free — no stale entries, no GC-race weak maps.
|
|
22
27
|
@wrapper_cache = {}
|
|
23
|
-
ObjectSpace.define_finalizer(self, self.class.finalize(c_ptr
|
|
28
|
+
ObjectSpace.define_finalizer(self, self.class.finalize(c_ptr)) unless c_ptr.null?
|
|
24
29
|
end
|
|
25
30
|
|
|
26
31
|
def self.parse(yaml, schema: :core_12, max_depth: 0)
|
|
@@ -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
|
|
@@ -88,6 +105,9 @@ class Yeptris::Document
|
|
|
88
105
|
return if @freed.state == :freed
|
|
89
106
|
|
|
90
107
|
@freed.state = :freed
|
|
108
|
+
# stop the GC finalizer BEFORE the C free — it captures the same
|
|
109
|
+
# pointer, and its firing after this free is the double free
|
|
110
|
+
ObjectSpace.undefine_finalizer(self)
|
|
91
111
|
@wrapper_cache.clear
|
|
92
112
|
Yeptris::FFI.yeptris_document_free(@c_ptr)
|
|
93
113
|
end
|
|
@@ -221,12 +241,15 @@ class Yeptris::Document
|
|
|
221
241
|
self
|
|
222
242
|
end
|
|
223
243
|
|
|
224
|
-
# GC safety net:
|
|
225
|
-
#
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
244
|
+
# GC safety net (#182): the proc captures ONLY the raw pointer — no
|
|
245
|
+
# Ruby objects, no wrapper, no freed-flag struct. A finalizer that
|
|
246
|
+
# closes over the owning wrapper races that wrapper's own sweep
|
|
247
|
+
# (the classic use-after-free: freed?/free against half-collected
|
|
248
|
+
# ivars, then a garbage C pointer into yeptris_document_free →
|
|
249
|
+
# SIGABRT). Explicit #free undefines this finalizer BEFORE the C
|
|
250
|
+
# free, so the double free cannot happen; finalizers run on the
|
|
251
|
+
# main thread, so there is no cross-thread window.
|
|
252
|
+
def self.finalize(c_ptr)
|
|
253
|
+
proc { Yeptris::FFI.yeptris_document_free(c_ptr) }
|
|
231
254
|
end
|
|
232
255
|
end
|
data/lib/yeptris/materializer.rb
CHANGED
|
@@ -115,7 +115,11 @@ module Yeptris
|
|
|
115
115
|
end
|
|
116
116
|
|
|
117
117
|
def parse_timestamp(v)
|
|
118
|
-
|
|
118
|
+
# stdlib scalar_scanner: date-only strings parse with
|
|
119
|
+
# strptime('%F', Date::GREGORIAN) — the PROLEPTIC calendar —
|
|
120
|
+
# not Date.parse's default ITALY reform (a 1582 cycle fails
|
|
121
|
+
# the ported test_julian_date otherwise)
|
|
122
|
+
return ::Date.strptime(v, "%F", ::Date::GREGORIAN) unless v.match?(/[Tt ]\d/)
|
|
119
123
|
|
|
120
124
|
# normalize the YAML 1.1 space forms onto iso8601 for
|
|
121
125
|
# xmlschema: "2001-12-14 21:59:43.10 -05:00" ->
|
data/lib/yeptris/node.rb
CHANGED
|
@@ -394,6 +394,15 @@ class Yeptris::Node
|
|
|
394
394
|
def scalar_to_ruby
|
|
395
395
|
return symbolize if symbol? && tag_id == :str
|
|
396
396
|
|
|
397
|
+
# stdlib deserialize's tagged-scalar arms: the class tag carries
|
|
398
|
+
# DateTime (dumped tagged — a plain timestamp re-loads as Time)
|
|
399
|
+
if tag == "!ruby/object:DateTime"
|
|
400
|
+
ts = value.to_s.sub(/ (\d)/, 'T\\1').sub(/ ([+-]\d)/, '\\1')
|
|
401
|
+
t = ::Time.xmlschema(ts)
|
|
402
|
+
return ::DateTime.civil(t.year, t.month, t.day, t.hour, t.min, t.sec,
|
|
403
|
+
Rational(t.utc_offset, 86_400)) + Rational(t.subsec, 86_400)
|
|
404
|
+
end
|
|
405
|
+
|
|
397
406
|
case tag_id
|
|
398
407
|
when :null then nil
|
|
399
408
|
when :bool then to_bool
|
|
@@ -66,11 +66,34 @@ module Yeptris
|
|
|
66
66
|
end
|
|
67
67
|
end
|
|
68
68
|
|
|
69
|
+
# stdlib register()s every object — scalars included — so a
|
|
70
|
+
# repeated scalar dumps &name/*name (test_float_references,
|
|
71
|
+
# test_alias_with_time). Containers already route through
|
|
72
|
+
# anchor_for; this is the scalar arm of the same machinery.
|
|
73
|
+
def anchored_scalar(obj, text: nil, tag: nil)
|
|
74
|
+
state, name = anchor_for(obj)
|
|
75
|
+
return alias_of(obj, name) if state == :alias
|
|
76
|
+
|
|
77
|
+
n = scalar(text || obj, tag: tag)
|
|
78
|
+
n.set_anchor(name) if name
|
|
79
|
+
remember(obj, n)
|
|
80
|
+
n
|
|
81
|
+
end
|
|
82
|
+
|
|
69
83
|
def visit(obj)
|
|
70
84
|
case obj
|
|
85
|
+
# strings/floats/ints: stdlib yaml_tree does NOT anchor them
|
|
86
|
+
# (its float test is load-side only) — and CBOR encode
|
|
87
|
+
# rejects the anchors the cbor profile feeds it
|
|
71
88
|
when nil, true, false, ::Integer, ::Float, ::String then scalar(obj)
|
|
72
89
|
when ::Symbol then @tree.new_scalar(":#{obj}", :plain) # psych emits symbols bare, never through visit_String's quoting rules
|
|
73
|
-
|
|
90
|
+
# stdlib visit_DateTime: the class tag carries the type (a
|
|
91
|
+
# plain timestamp re-loads as Time) — "!ruby/object:DateTime
|
|
92
|
+
# 2017-04-13 12:00:00.500000000 +09:00", to_s text, one line
|
|
93
|
+
when ::DateTime
|
|
94
|
+
anchored_scalar(obj, text: obj.strftime("%F %H:%M:%S.%9N %:z"),
|
|
95
|
+
tag: "!ruby/object:DateTime")
|
|
96
|
+
when ::Date, ::Time then anchored_scalar(obj) # timestamp text, never ivars
|
|
74
97
|
when ::Hash then visit_hash(obj)
|
|
75
98
|
when ::Array then visit_array(obj)
|
|
76
99
|
when ::Struct then visit_struct(obj)
|
|
@@ -101,7 +124,7 @@ module Yeptris
|
|
|
101
124
|
if @refs[oid] < 2
|
|
102
125
|
return [:none, nil]
|
|
103
126
|
end
|
|
104
|
-
name =
|
|
127
|
+
name = (@counter += 1).to_s # stdlib: &1, *1
|
|
105
128
|
@names[oid] = name
|
|
106
129
|
[:new, name]
|
|
107
130
|
end
|
|
@@ -156,7 +179,7 @@ module Yeptris
|
|
|
156
179
|
# neutral surface alone was pinned)
|
|
157
180
|
::Yeptris::YAML::BulkBuilder.time_text(obj)
|
|
158
181
|
elsif obj.is_a?(::Date)
|
|
159
|
-
obj.iso8601 # canonical calendar form
|
|
182
|
+
obj.iso8601 # canonical calendar form
|
|
160
183
|
elsif obj.nil?
|
|
161
184
|
"" # libyaml's null rendering rides bare (issue #290)
|
|
162
185
|
elsif obj.is_a?(::Float)
|
|
@@ -165,7 +188,7 @@ module Yeptris
|
|
|
165
188
|
obj.to_s
|
|
166
189
|
end
|
|
167
190
|
n =
|
|
168
|
-
if obj.is_a?(::String)
|
|
191
|
+
if obj.is_a?(::String) && !tag
|
|
169
192
|
::Yeptris::YAML::Builder.build_string(@tree, obj)
|
|
170
193
|
else
|
|
171
194
|
@tree.new_scalar(text, :plain)
|
|
@@ -220,7 +243,7 @@ module Yeptris
|
|
|
220
243
|
m = @tree.new_mapping
|
|
221
244
|
remember(strct, m)
|
|
222
245
|
m.set_anchor(name) if name
|
|
223
|
-
m.set_tag(strct.class.name ? "!ruby/struct
|
|
246
|
+
m.set_tag(strct.class.name.to_s.empty? ? "!ruby/struct" : "!ruby/struct:#{strct.class.name}")
|
|
224
247
|
strct.each_pair { |k, v| m.map_add(key_text(k), visit(v)) }
|
|
225
248
|
m
|
|
226
249
|
end
|
|
@@ -326,6 +349,9 @@ module Yeptris
|
|
|
326
349
|
when ::Yeptris::Psych::Nodes::Mapping then visit_mapping(node)
|
|
327
350
|
else node&.to_ruby
|
|
328
351
|
end
|
|
352
|
+
# stdlib register(): an anchored node's result is THE object
|
|
353
|
+
# for every alias to that anchor (scalars included)
|
|
354
|
+
anchors[node.anchor] = result if node.respond_to?(:anchor) && node.anchor
|
|
329
355
|
# the domain-types post-pass (stdlib ToRuby#accept's tail):
|
|
330
356
|
# registered blocks transform the revived result per node
|
|
331
357
|
if node && !::Yeptris::Psych.domain_types.empty?
|
data/lib/yeptris/psych.rb
CHANGED
|
@@ -291,7 +291,15 @@ module Yeptris
|
|
|
291
291
|
raise SyntaxError.from_parse_error(e)
|
|
292
292
|
end
|
|
293
293
|
|
|
294
|
-
def parse_stream(yaml)
|
|
294
|
+
def parse_stream(yaml, &block)
|
|
295
|
+
# stdlib yields each document to a block if one was given (and
|
|
296
|
+
# the stream it returns holds the same children either way).
|
|
297
|
+
# #182: the wrapper is the sole owner of the C memory — the
|
|
298
|
+
# stream merely REFERENCES it; the wrapper's own finalizer
|
|
299
|
+
# handles GC-free. (The block-yielding form is tracked under
|
|
300
|
+
# #179: the current implementation materializes eagerly; the
|
|
301
|
+
# block is only honored as a no-op convenience.)
|
|
302
|
+
_ = block # reserved for the future yielding form (#179)
|
|
295
303
|
doc = Yeptris::Document.parse(yaml, schema: :compat_11)
|
|
296
304
|
return nil if doc.nil? || doc.document_count.zero?
|
|
297
305
|
|
|
@@ -299,14 +307,17 @@ module Yeptris
|
|
|
299
307
|
(0...doc.document_count).each do |i|
|
|
300
308
|
stream.children << Nodes::Builder.document_stream_child(doc, i)
|
|
301
309
|
end
|
|
310
|
+
# ownership: the DOCUMENT wrapper is the sole owner — its own
|
|
311
|
+
# finalizer (pointer-only closure) frees the C memory; the
|
|
312
|
+
# stream merely references it. A tree-side finalizer closing
|
|
313
|
+
# over the wrapper raced the wrapper's sweep (#182's abort).
|
|
302
314
|
stream.owner = doc
|
|
303
|
-
ObjectSpace.define_finalizer(
|
|
304
|
-
stream, proc { doc.free unless doc.freed? }
|
|
305
|
-
)
|
|
306
315
|
stream
|
|
307
316
|
rescue Yeptris::ParseError => e
|
|
308
317
|
raise SyntaxError.from_parse_error(e)
|
|
309
318
|
ensure
|
|
319
|
+
# the returned stream OWNS the doc's lifetime (its free delegates
|
|
320
|
+
# to the owner); only free here when the stream wasn't built
|
|
310
321
|
doc&.free if doc && !stream
|
|
311
322
|
end
|
|
312
323
|
|
|
@@ -327,6 +338,11 @@ module Yeptris
|
|
|
327
338
|
# data anyway
|
|
328
339
|
out =
|
|
329
340
|
case obj
|
|
341
|
+
# DateTime < Date in ruby's hierarchy — the fast scalar
|
|
342
|
+
# path would emit it tag-less iso8601 (re-loading as Time);
|
|
343
|
+
# the visitor carries stdlib's !ruby/object:DateTime tag
|
|
344
|
+
when ::DateTime
|
|
345
|
+
Visitors::YAMLTree.new.push(obj).finish
|
|
330
346
|
when nil, true, false, ::String, ::Integer, ::Float, ::Symbol, ::Date, ::Time
|
|
331
347
|
Yeptris::YAML.dump(obj, header: true)
|
|
332
348
|
else
|
|
@@ -442,12 +458,13 @@ module Yeptris
|
|
|
442
458
|
@tags = tags
|
|
443
459
|
end
|
|
444
460
|
|
|
445
|
-
# @api private —
|
|
461
|
+
# @api private — attach the document: the tree REFERENCES it
|
|
462
|
+
# (keeping the C memory alive while the tree is reachable);
|
|
463
|
+
# the wrapper's own finalizer is the sole freer (#182: a
|
|
464
|
+
# tree-side finalizer closing over the wrapper raced the
|
|
465
|
+
# wrapper's sweep — SIGABRT under document churn)
|
|
446
466
|
def own(yeptris_doc)
|
|
447
467
|
@owner = yeptris_doc
|
|
448
|
-
ObjectSpace.define_finalizer(
|
|
449
|
-
self, proc { yeptris_doc.free unless yeptris_doc.freed? }
|
|
450
|
-
)
|
|
451
468
|
self
|
|
452
469
|
end
|
|
453
470
|
|
|
@@ -515,8 +532,12 @@ module Yeptris
|
|
|
515
532
|
# stream owns the yeptris document)
|
|
516
533
|
def document_stream_child(doc, index)
|
|
517
534
|
root = doc.root(index)
|
|
518
|
-
|
|
519
|
-
|
|
535
|
+
# #182's root cause: three per-stream Document wrappers all
|
|
536
|
+
# capture the same c_ptr in their finalizers → three frees.
|
|
537
|
+
# The stream-children WRAPPER owns nothing (the owner wrapper
|
|
538
|
+
# is the real Document; this one just references it). Build
|
|
539
|
+
# a non-owning wrapper with no finalizer.
|
|
540
|
+
d = Document.send(:new, root&.document, false)
|
|
520
541
|
d.children << node(root) if root
|
|
521
542
|
d
|
|
522
543
|
end
|
data/lib/yeptris/schema.rb
CHANGED
|
@@ -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.
|
|
6
|
+
VERSION = "0.6.16.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
|
|
@@ -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--;
|
|
@@ -134,8 +134,17 @@ static void wr_indent(yep_writer* w, int depth) {
|
|
|
134
134
|
|
|
135
135
|
static void emit_dq(yep_writer* w, const char* p, uint32_t n) {
|
|
136
136
|
wr_byte(w, '"');
|
|
137
|
+
uint32_t run = 0;
|
|
137
138
|
for (uint32_t i = 0; i < n; i++) {
|
|
138
139
|
unsigned char c = (unsigned char)p[i];
|
|
140
|
+
if (c >= 0x20 && c != '"' && c != '\\' && c != 0x7f) {
|
|
141
|
+
run++;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (run) {
|
|
145
|
+
wr_put(w, p + i - run, run);
|
|
146
|
+
run = 0;
|
|
147
|
+
}
|
|
139
148
|
if (c == '"' || c == '\\') {
|
|
140
149
|
wr_byte(w, '\\');
|
|
141
150
|
wr_byte(w, (char)c);
|
|
@@ -161,10 +170,11 @@ static void emit_dq(yep_writer* w, const char* p, uint32_t n) {
|
|
|
161
170
|
char hex[4] = {'\\', 'x', hd[(c >> 4) & 0xf], hd[c & 0xf]};
|
|
162
171
|
wr_put(w, hex, 4);
|
|
163
172
|
}
|
|
164
|
-
} else {
|
|
165
|
-
wr_byte(w, (char)c);
|
|
166
173
|
}
|
|
167
174
|
}
|
|
175
|
+
if (run) {
|
|
176
|
+
wr_put(w, p + n - run, run);
|
|
177
|
+
}
|
|
168
178
|
wr_byte(w, '"');
|
|
169
179
|
}
|
|
170
180
|
|
|
@@ -315,6 +325,10 @@ enum { YEP_SC_NOTHING = 0, YEP_SC_PLAIN, YEP_SC_SQ, YEP_SC_DQ, YEP_SC_LITERAL };
|
|
|
315
325
|
|
|
316
326
|
static uint8_t sc_route(yep_writer* w, const char* p, uint32_t len, int as_key, uint8_t sty_in,
|
|
317
327
|
int has_tag) {
|
|
328
|
+
if ((sty_in == 1) && len > 0 &&
|
|
329
|
+
(as_key ? yep_style_plain_key_safe(p, len) : yep_style_plain_safe(p, len))) {
|
|
330
|
+
return YEP_SC_PLAIN; /* plain-safe implies no breaks: memchr skipped */
|
|
331
|
+
}
|
|
318
332
|
int multiline = (len > 0 && memchr(p, '\n', len) != NULL);
|
|
319
333
|
int blockable = 0;
|
|
320
334
|
if (multiline && !as_key) {
|
|
@@ -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
|
-
|
|
650
|
-
uint32_t
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
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
|
-
|
|
689
|
-
uint32_t
|
|
690
|
-
|
|
691
|
-
|
|
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);
|