yeptris 0.6.14.1 → 0.6.15.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: 60e8ec13d8221947494e0f163b57a619a3bb6d4f179f53d3f9b324e0ae0a4ffb
4
- data.tar.gz: 6eda638121e069206be5d900cc044c942b84e74989d6482255b0e9177aab13f4
3
+ metadata.gz: 0033737c829c7beb3ae83ca816033386b4fe23b53968f2b0ff40a74c97aac469
4
+ data.tar.gz: c27accdae4e4807500d4edbfa39228b003c70e5380de3f259bd5315dc9a2dc6f
5
5
  SHA512:
6
- metadata.gz: dbe32b83a5b017191c37a5978325516a5f99a7d85819ead1dcd97c030fec14f586acfb6d5965275cf1416c74f4dbeecb08abea77074ce769f51340ef3c1e9a7a
7
- data.tar.gz: 9b6db9635acbdc9e83017626594b8417fec8c2953e38d17878bf2e99e5a4e0976f4087862890f78a23155ea3eb34d8ca8afeb87e536991fa80091b626e40875e
6
+ metadata.gz: ca7d477d959df73f231e443f15ceef0403774129e2df0af49d94e2f926d24c380cbacf6e92d88e9488715c028de0533e93fa82ca039ee129dd13b90f4b4badd0
7
+ data.tar.gz: 54b35dfe9c5399157ffbbb1e33b058afbe523e36222771d000ff697ccaccd059d5b049a3383ac2fbad61086d4548d3bc23343f600311c4c8438535d2a87cc2a8
@@ -26,18 +26,23 @@ class Yeptris::Document
26
26
  def self.parse(yaml, schema: :core_12, max_depth: 0)
27
27
  yaml = Yeptris.read_input(yaml)
28
28
  yaml = yaml.to_s
29
+ status = ::FFI::MemoryPointer.new(:int)
29
30
  doc =
30
31
  if schema == :core_12 && max_depth.zero?
31
- Yeptris::FFI.yeptris_parse(yaml, yaml.bytesize, nil)
32
+ Yeptris::FFI.yeptris_parse(yaml, yaml.bytesize, status)
32
33
  else
33
34
  opts = Yeptris::FFI::ParseOptions.new
34
35
  opts[:schema] = schema == :compat_11 ? Yeptris::FFI::SCHEMA_11_COMPAT : Yeptris::FFI::SCHEMA_12_CORE
35
36
  opts[:max_depth] = max_depth
36
- Yeptris::FFI.yeptris_parse_ex(yaml, yaml.bytesize, opts, nil)
37
+ Yeptris::FFI.yeptris_parse_ex(yaml, yaml.bytesize, opts, status)
37
38
  end
38
39
  if doc.null?
39
- # the status out-param is skipped: the thread-local error
40
- # channel carries the failure detail (measurable on small docs)
40
+ # NULL + OK is the legal empty stream (no documents): nil, like
41
+ # stdlib. The status out-param is the discriminator the TLS
42
+ # error message cannot serve (a stale error from an earlier
43
+ # parse persists on the thread).
44
+ return nil if status.read_int.zero?
45
+
41
46
  raise Yeptris::ParseError,
42
47
  "parse failed: #{Yeptris::FFI.last_error_message}"
43
48
  end
data/lib/yeptris/node.rb CHANGED
@@ -276,6 +276,31 @@ class Yeptris::Node
276
276
  alive { Yeptris::FFI.yeptris_node_id(@c_ptr) }
277
277
  end
278
278
 
279
+ # The marshal fast path (TODO.restructure/21; #178): one C call
280
+ # turns this subtree into Marshal 4.8 bytes — Marshal.load builds
281
+ # the Ruby objects in C, no per-node FFI. nil when the document
282
+ # carries constructs the format cannot express (merge keys,
283
+ # timestamps, tagged revivals): the caller walks instead. Raises
284
+ # ParseError on engine errors (not on the bail).
285
+ def marshal_fast
286
+ return nil unless Yeptris::FFI::MARSHAL
287
+
288
+ out_p = ::FFI::MemoryPointer.new(:pointer)
289
+ olen_p = ::FFI::MemoryPointer.new(:size_t)
290
+ st = alive { Yeptris::FFI.yeptris_marshal_node(@c_ptr, out_p, olen_p) }
291
+ if st == Yeptris::FFI::ERROR_UNSUPPORTED
292
+ nil
293
+ elsif st != Yeptris::FFI::OK
294
+ raise Yeptris::ParseError, Yeptris::FFI.last_error_message
295
+ else
296
+ bytes = out_p.read_pointer.read_bytes(olen_p.read_uint64)
297
+ bytes.force_encoding(Encoding::ASCII_8BIT)
298
+ ::Marshal.load(bytes)
299
+ end
300
+ ensure
301
+ Yeptris::FFI.yeptris_marshal_free(out_p.read_pointer) if out_p
302
+ end
303
+
279
304
  def to_ruby(memo = nil)
280
305
  # readonly documents memoize per node: a second materialization
281
306
  # returns the SAME object (the readonly cache is the ground truth
@@ -293,25 +318,7 @@ class Yeptris::Node
293
318
  # Falls back to the per-node FFI walk on constructs the format
294
319
  # cannot express (merge keys, timestamps) and on older builds.
295
320
  if Yeptris::FFI::MARSHAL && memo.nil?
296
- result =
297
- begin
298
- out_p = ::FFI::MemoryPointer.new(:pointer)
299
- olen_p = ::FFI::MemoryPointer.new(:size_t)
300
- st = Yeptris::FFI.yeptris_marshal_node(@c_ptr, out_p, olen_p)
301
- if st == Yeptris::FFI::ERROR_UNSUPPORTED
302
- nil
303
- elsif st != Yeptris::FFI::OK
304
- raise Yeptris::ParseError, Yeptris::FFI.last_error_message
305
- else
306
- buf = out_p.read_pointer
307
- len = olen_p.read_uint64
308
- bytes = buf.read_bytes(len)
309
- bytes.force_encoding(Encoding::ASCII_8BIT)
310
- ::Marshal.load(bytes)
311
- end
312
- ensure
313
- Yeptris::FFI.yeptris_marshal_free(out_p.read_pointer) if out_p
314
- end
321
+ result = marshal_fast
315
322
  unless result.nil?
316
323
  @document.readonly_memo[node_id] = result if @document.readonly?
317
324
  return result
@@ -332,7 +339,32 @@ class Yeptris::Node
332
339
  h = {}
333
340
  memo[node_id] = h
334
341
  each_pair do |k, v|
335
- h[k.to_ruby_key] = v.to_ruby(memo)
342
+ key = k.to_ruby_key
343
+ val = v.to_ruby(memo)
344
+ if key == "<<" && k.tag != "tag:yaml.org,2002:str"
345
+ # stdlib revive_hash's merge branch: the VALUE's node kind
346
+ # picks the arm; every merge is TypeError-guarded (a bad
347
+ # element keeps the whole '<<' pair literal)
348
+ if v.kind == :alias || v.mapping?
349
+ begin
350
+ h.merge!(val)
351
+ rescue ::TypeError
352
+ h[key] = val
353
+ end
354
+ elsif v.sequence?
355
+ begin
356
+ merged = {}
357
+ val.reverse_each { |e| merged.merge!(e) }
358
+ h.merge!(merged)
359
+ rescue ::TypeError
360
+ h[key] = val
361
+ end
362
+ else
363
+ h[key] = val
364
+ end
365
+ else
366
+ h[key] = val
367
+ end
336
368
  end
337
369
  h
338
370
  when :sequence
@@ -342,7 +374,11 @@ class Yeptris::Node
342
374
  a
343
375
  when :alias
344
376
  t = alias_target
345
- t.nil? ? nil : t.to_ruby(memo)
377
+ if t.nil?
378
+ raise ::Yeptris::Psych::AnchorNotDefined,
379
+ "Unknown anchor: #{anchor}"
380
+ end
381
+ t.to_ruby(memo)
346
382
  else
347
383
  scalar_to_ruby
348
384
  end
@@ -188,6 +188,13 @@ module Yeptris
188
188
  key_node = @tree.new_scalar("", :single_quoted)
189
189
  key_node.set_tag("!")
190
190
  m.map_add_node(key_node, visit(v))
191
+ elsif k.is_a?(::String) && k == "<<"
192
+ # stdlib yaml_tree: a literal chevron key carries the
193
+ # explicit !!str tag (single-quoted) so it does not
194
+ # re-load as a merge
195
+ key_node = @tree.new_scalar("<<", :single_quoted)
196
+ key_node.set_tag("!!str")
197
+ m.map_add_node(key_node, visit(v))
191
198
  else
192
199
  m.map_add(key_text(k), visit(v))
193
200
  end
@@ -415,10 +422,38 @@ module Yeptris
415
422
  end
416
423
  end
417
424
 
425
+ # stdlib visit_hash's merge branch, verbatim in structure: the
426
+ # key's SHAPE decides (an explicit !!str '<<' is a literal key,
427
+ # not a merge), the VALUE's node kind picks the arm, and every
428
+ # merge is TypeError-guarded (a bad element keeps the whole
429
+ # '<<' pair literal — merges are all-or-nothing).
418
430
  def hash_into(h, node)
419
431
  anchors[node.anchor] = h if node.anchor
420
432
  node.children.each_slice(2) do |k, v|
421
- h[visit(k)] = visit(v)
433
+ key = visit(k)
434
+ val = visit(v)
435
+ if key == "<<" && k.tag != "tag:yaml.org,2002:str"
436
+ case v
437
+ when ::Yeptris::Psych::Nodes::Alias, ::Yeptris::Psych::Nodes::Mapping
438
+ begin
439
+ h.merge!(val)
440
+ rescue ::TypeError
441
+ h[key] = val
442
+ end
443
+ when ::Yeptris::Psych::Nodes::Sequence
444
+ begin
445
+ merged = {}
446
+ val.reverse_each { |value| merged.merge!(value) }
447
+ h.merge!(merged)
448
+ rescue ::TypeError
449
+ h[key] = val
450
+ end
451
+ else
452
+ h[key] = val
453
+ end
454
+ else
455
+ h[key] = val
456
+ end
422
457
  end
423
458
  h
424
459
  end
data/lib/yeptris/psych.rb CHANGED
@@ -91,6 +91,9 @@ module Yeptris
91
91
  # object instance exists.
92
92
  autoload :Encodable, "yeptris/psych/encodable"
93
93
  class Error < StandardError; end
94
+
95
+ # stdlib psych 5: a *foo with no matching &foo anchor
96
+ class AnchorNotDefined < Error; end
94
97
  # Psych's exact interface (issue #32): same constructor arity,
95
98
  # same reader set (file/line/column/offset/problem/context), same
96
99
  # message shape — drop-in consumers' rescues and constructors
@@ -143,6 +146,31 @@ module Yeptris
143
146
  # !ruby/set, encode_with/init_with, alias identity. Plain-data
144
147
  # loads should use load/safe_load (the Materializer fast path).
145
148
  def unsafe_load(yaml, **)
149
+ # #178: the marshal fast path. Nodes::Builder's tree is per-node
150
+ # FFI (kind/tag/anchor/value + a walk per container) — the whole
151
+ # 11 MB relaton index paid ~9.6 s there. When the document is
152
+ # plain data (no timestamps/merge keys/tagged revivals), ONE C
153
+ # call materializes the tree as Marshal bytes and Marshal.load
154
+ # builds the objects in C (~1 s end to end). Construct-heavy
155
+ # documents keep the full revival walk below, byte for byte.
156
+ if Yeptris::FFI::MARSHAL && ::Yeptris::Psych.domain_types.empty?
157
+ begin
158
+ doc = ::Yeptris::Document.parse(yaml, schema: :compat_11)
159
+ return nil if doc.nil? || doc.document_count.zero?
160
+
161
+ root = doc.root(0)
162
+ fast = root&.marshal_fast
163
+ unless fast.nil?
164
+ doc.free
165
+ return fast
166
+ end
167
+ tree = Nodes::Builder.document(doc) # ownership: the tree frees
168
+ return Visitors::ToRuby.visit(tree.children.first)
169
+ rescue ::Yeptris::ParseError => e
170
+ doc&.free unless doc&.freed?
171
+ translate_parse_error(e)
172
+ end
173
+ end
146
174
  tree = parse(yaml)
147
175
  return nil if tree.nil?
148
176
 
@@ -150,11 +178,16 @@ module Yeptris
150
178
  end
151
179
 
152
180
  def safe_load(yaml, permitted_classes: [::Date, ::Time], aliases: false, **)
153
- doc = Yeptris::Document.parse(yaml, schema: :compat_11)
154
181
  begin
182
+ doc = Yeptris::Document.parse(yaml, schema: :compat_11)
183
+ return nil if doc.nil? # the legal empty stream
184
+
155
185
  force_utf8_scalars(walk_safe(doc.root(0), permitted_classes, aliases))
186
+ rescue ::Yeptris::ParseError => e
187
+ doc&.free unless doc&.freed?
188
+ translate_parse_error(e)
156
189
  ensure
157
- doc.free
190
+ doc&.free
158
191
  end
159
192
  end
160
193
 
@@ -230,6 +263,8 @@ module Yeptris
230
263
  # children share one C document, so their handles would all
231
264
  # resolve to the first document's tree
232
265
  doc = Yeptris::Document.parse(yaml, schema: :compat_11)
266
+ return nil if doc.nil? # the legal empty stream
267
+
233
268
  begin
234
269
  (0...doc.document_count).map { |i| doc.root(i).to_ruby }
235
270
  ensure
@@ -238,9 +273,18 @@ module Yeptris
238
273
  end
239
274
 
240
275
  # The first document's node tree (no Ruby materialization).
276
+ def translate_parse_error(e)
277
+ # yeptris rejects *foo without &foo at PARSE time (stdlib
278
+ # raises at visit) — surface it as stdlib's class so
279
+ # consumers' rescues keep working
280
+ raise AnchorNotDefined, e.message if e.message.include?("undefined anchor")
281
+
282
+ raise SyntaxError.from_parse_error(e)
283
+ end
284
+
241
285
  def parse(yaml)
242
286
  doc = Yeptris::Document.parse(yaml, schema: :compat_11)
243
- return nil if doc.document_count.zero?
287
+ return nil if doc.nil? || doc.document_count.zero?
244
288
 
245
289
  Nodes::Builder.document(doc)
246
290
  rescue Yeptris::ParseError => e
@@ -249,7 +293,7 @@ module Yeptris
249
293
 
250
294
  def parse_stream(yaml)
251
295
  doc = Yeptris::Document.parse(yaml, schema: :compat_11)
252
- return nil if doc.document_count.zero?
296
+ return nil if doc.nil? || doc.document_count.zero?
253
297
 
254
298
  stream = Nodes::Stream.new
255
299
  (0...doc.document_count).each do |i|
@@ -33,6 +33,8 @@ class Yeptris::YAML::Descriptor < ::Yeptris::JSON::Descriptor
33
33
  def walk(yaml)
34
34
  src = ::Yeptris.read_input(yaml).to_s
35
35
  doc = ::Yeptris::Document.parse(src, schema: @schema)
36
+ return [] if doc.nil? # the legal empty stream
37
+
36
38
  begin
37
39
  st = ::FFI::MemoryPointer.new(:int)
38
40
  raw = ::Yeptris::FFI.yeptris_document_plan_walk(doc.c_ptr, @handle, st)
data/lib/yeptris/yaml.rb CHANGED
@@ -73,7 +73,7 @@ module Yeptris
73
73
  # Parses without materializing: the first document's root Node.
74
74
  def parse(yaml, schema: :core_12)
75
75
  doc = Document.parse(yaml, schema: schema)
76
- return nil if doc.document_count.zero?
76
+ return nil if doc.nil? || doc.document_count.zero?
77
77
 
78
78
  doc.root(0)
79
79
  end
@@ -177,6 +177,14 @@ module Yeptris
177
177
  off[0] += 1
178
178
  elsif k.is_a?(Symbol)
179
179
  scalar(":#{k}", STYLE_PLAIN, emit, blob, off)
180
+ elsif k.is_a?(String) && k == "<<"
181
+ # stdlib yaml_tree: a literal chevron key carries the
182
+ # explicit !!str tag (single-quoted) so it does not
183
+ # re-load as a merge
184
+ scalar("<<", STYLE_SQ, emit, blob, off)
185
+ emit.call(BUILD_TAG, 0, off[0], 5)
186
+ blob << "!!str"
187
+ off[0] += 5
180
188
  elsif k.is_a?(String)
181
189
  place(k, emit, blob, off, seen)
182
190
  else
@@ -343,6 +351,12 @@ module Yeptris
343
351
  key_node = doc.new_scalar("", :single_quoted)
344
352
  key_node.set_tag("!")
345
353
  m.map_add_node(key_node, build(doc, v, seen))
354
+ elsif k.is_a?(::String) && k == "<<"
355
+ # stdlib yaml_tree: a literal chevron key carries the
356
+ # explicit !!str tag so it does not re-load as a merge
357
+ key_node = doc.new_scalar("<<", :single_quoted)
358
+ key_node.set_tag("!!str")
359
+ m.map_add_node(key_node, build(doc, v, seen))
346
360
  else
347
361
  m.map_add(key_text(k), build(doc, v, seen))
348
362
  end
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.14.1".freeze
6
+ VERSION = "0.6.15.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.14
6
+ VERSION 0.6.15
7
7
  DESCRIPTION "Ultra-fast YAML 1.2 parser, emitter and streamer in C"
8
8
  LANGUAGES C CXX
9
9
  )
@@ -721,6 +721,34 @@ YEPTRIS_API YeptrisStatus yeptris_marshal(const char* data, size_t len, YeptrisS
721
721
  return st;
722
722
  }
723
723
 
724
+ /* #178 (ruby #168's fast path): the value records carry the resolver's
725
+ * verdict, NOT the source's explicit tag — a `!ruby/object` mapping
726
+ * would marshal as a plain hash, silently dropping the tag (psych's
727
+ * visitor revives it). One cheap pre-walk: any explicitly tagged node
728
+ * in the subtree → UNSUPPORTED, the host walks. Explicit CORE tags
729
+ * (!!str and friends) ride the same bail — correct, just conservative;
730
+ * bulk data is untagged. */
731
+ static int dom_subtree_tagged(const yep_dom* d, uint32_t id) {
732
+ const yep_dnode* n = yep_dom_node(d, id);
733
+ if (n == NULL) {
734
+ return 0;
735
+ }
736
+ if (n->tag.len != 0) {
737
+ return 1;
738
+ }
739
+ for (uint32_t c = n->first_child; c != UINT32_MAX;) {
740
+ const yep_dnode* cn = yep_dom_node(d, c);
741
+ if (cn == NULL) {
742
+ break;
743
+ }
744
+ if (dom_subtree_tagged(d, c) != 0) {
745
+ return 1;
746
+ }
747
+ c = cn->next_sibling;
748
+ }
749
+ return 0;
750
+ }
751
+
724
752
  YEPTRIS_API YeptrisStatus yeptris_marshal_node(YeptrisNode node, char** out, size_t* out_len) {
725
753
  if (node == NULL || out == NULL || out_len == NULL) {
726
754
  return YEPTRIS_ERROR_ARG;
@@ -728,6 +756,12 @@ YEPTRIS_API YeptrisStatus yeptris_marshal_node(YeptrisNode node, char** out, siz
728
756
  *out = NULL;
729
757
  *out_len = 0;
730
758
  yeptris_node* h = (yeptris_node*)node;
759
+ if (dom_subtree_tagged(h->doc->dom, h->id) != 0) {
760
+ yep_error_set(yep_error_tls(), YEP_ERR_UNEXPECTED, 0, 0, 0,
761
+ "marshal: explicitly tagged node not expressible; "
762
+ "fall back to the value walk");
763
+ return YEPTRIS_ERROR_UNSUPPORTED;
764
+ }
731
765
  yep_value_ctx* c = NULL;
732
766
  if (yep_values_from_dom(h->doc->dom, h->id, 0, &c) != 0) {
733
767
  return YEPTRIS_ERROR_MEMORY;
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yeptris
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.14.1
4
+ version: 0.6.15.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.