yeptris 0.1.10.0 → 0.1.11.0

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: cec65c1f8da9f6256a42fdf00f6c03c637376c0a0e4d0305f4b540f31f43cc1e
4
- data.tar.gz: fb4d51fe7dad0b08c1364b32572b4abd92fa737fab79066665c9ce0820f3c6b9
3
+ metadata.gz: b2ddefc4cfb21c80a7238bbca6cfa46293fd0f3ff920a122e37bd6f4e3f78b0e
4
+ data.tar.gz: cfbfe8da20325e11ef811dd9372e464fd1e03a1c0eff21cae254f918c9f5c979
5
5
  SHA512:
6
- metadata.gz: 2c361ed6318a55ebdaeaacc56130e5972d699e065dcb7ed0453e84660f9c8f7054d7db1b55ab382bf438484b5a3cb42c201711517cd4feee7d08a90fd0b2fc17
7
- data.tar.gz: 9553bc1afea94e8188dd8d28d8b7186c697865cfc5757fb1e42ab1fc6196fe83ba9a5834af922e65acd5ddca861c0f6b6dddfeec9d14fc2eecc8f19aa46559fe
6
+ metadata.gz: 76f113166f28712bb328a7daf7f5b15f37f06c4ca5ce24dc56651878bded33800e3befbefed6b66f4da0a28a94c595797fb41004572d807a5ebca93a342e1cc6
7
+ data.tar.gz: 273c6e45ff73043c14c24869b3d98207710514dcd055a939d0fa4c5832d133bf5d14686ba9259c712115b4034cb5785c9af6aaa9bbabf929315dd43c313cf997
data/README.adoc CHANGED
@@ -7,8 +7,9 @@ compilation at install.
7
7
 
8
8
  == Install (development)
9
9
 
10
- The native library is vendored in the platform gems (TODO.impl/15
11
- phase D). For development against a local build:
10
+ The gem loads a shared `libyeptris` from `YEPTRIS_LIB_PATH`, a
11
+ vendored `lib/platform/<tag>/` copy, or the system paths. For
12
+ development against a local build:
12
13
 
13
14
  ....
14
15
  # the sibling C checkout: ~/src/leptris/yeptris
@@ -32,7 +33,9 @@ require "yeptris"
32
33
  Yeptris::YAML.load("name: yeptris\nrating: 10\n")
33
34
  # => {"name" => "yeptris", "rating" => 10}
34
35
 
35
- Yeptris::YAML.dump("name" => "yeptris", tags: [:yaml, :fast])
36
+ # brace mixed hashes on Ruby 3.x: a trailing symbol key otherwise
37
+ # splits the literal into keywords
38
+ Yeptris::YAML.dump({"name" => "yeptris", "tags" => [:yaml, :fast]})
36
39
 
37
40
  doc = Yeptris::Document.parse(config_yaml)
38
41
  doc.root["server"]["port"].to_i
@@ -49,11 +52,21 @@ Handles are document-scoped: `Document#free` releases everything
49
52
  (one C call), a GC finalizer backs it up, and any use after free
50
53
  raises `Yeptris::FreedError` — never a segfault.
51
54
 
52
- == Roadmap
55
+ == Shipped beyond the original plan
53
56
 
54
- * Phase B: recorder-driven Ruby materialization (bulk event records,
55
- the FFI tax paid per chunk, not per node) + the `Yeptris::Psych`
56
- drop-in namespace and the ported Psych suite.
57
- * Phase C: `.tml` corpora, readonly mode, YAMLTree dumps.
58
- * Phase D: vendored precompiled platform gems, lockstep versioning
59
- with the C library, symbol audit task.
57
+ * The columnar value drain (bulk, pre-converted typed columns — the
58
+ load fast path) and the bulk DOM builder (one `document_build`
59
+ call per dump).
60
+ * **Ruby Marshal 4.8 emission** (libyeptris >= 0.1.11): the C side
61
+ converts value records into Marshal bytes; one `Marshal.load`
62
+ materializes the whole graph. ~10× faster than the columnar walk
63
+ on JSON-shaped input, ~5× on YAML, ~50× on the per-node DOM walk
64
+ (`Node#to_ruby` becomes bulk). Alias identity preserved through
65
+ object links; merge keys and timestamps return UNSUPPORTED for the
66
+ record-walk fallback.
67
+ * `Yeptris::Psych` — the drop-in namespace with the ported Psych
68
+ suite (143 specs), `.tml` corpora conformance, and the object
69
+ visitor (`encode_with`/`init_with`).
70
+ * Lockstep versioning with the C library
71
+ (`{c-semver}.{gem-patch}`); releases published from the C repo's
72
+ workflow through the RubyGems trusted publisher.
data/lib/yeptris/ffi.rb CHANGED
@@ -136,6 +136,25 @@ module Yeptris
136
136
  :bools, :pointer, :arena, :pointer
137
137
  end
138
138
 
139
+ # Marshal 4.8 emission (TODO.restructure/21): the C side converts
140
+ # value records directly into Ruby's wire format; the binding
141
+ # materializes the whole graph with one core-C Marshal.load call
142
+ # instead of walking per-value records in pure Ruby. Same feature-
143
+ # detect discipline as the columnar drain (older libraries fall
144
+ # back to the record walk).
145
+ MARSHAL = begin
146
+ MARSHAL_ALL_DOCS = 0
147
+ MARSHAL_FIRST_DOC = 1
148
+ attach_function :yeptris_marshal,
149
+ %i[pointer size_t int int pointer pointer], :int
150
+ attach_function :yeptris_marshal_node,
151
+ %i[yeptris_node pointer pointer], :int
152
+ attach_function :yeptris_marshal_free, [:pointer], :void
153
+ true
154
+ rescue ::FFI::NotFoundError
155
+ false
156
+ end
157
+
139
158
  # bulk build (TODO.impl/15 phase D): one call raises a document
140
159
  BUILD_SCALAR = 1
141
160
  BUILD_SEQ = 2
@@ -229,6 +248,9 @@ module Yeptris
229
248
  ERROR_MEMORY = 2
230
249
  ERROR_DEPTH = 3
231
250
  ERROR_ENCODING = 4
251
+ ERROR_IO = 5
232
252
  ERROR_ARG = 6
253
+ ERROR_UNSUPPORTED = 7
254
+ ERROR_INTERNAL = 8
233
255
  end
234
256
  end
data/lib/yeptris/node.rb CHANGED
@@ -243,14 +243,48 @@ class Yeptris::Node
243
243
  end
244
244
 
245
245
  def to_ruby(memo = nil)
246
- # readonly documents memoize per node: repeated materialization
247
- # of a shared subtree returns the SAME object at zero walk cost
246
+ # readonly documents memoize per node: a second materialization
247
+ # returns the SAME object (the readonly cache is the ground truth
248
+ # for alias identity across calls).
248
249
  if @document.readonly?
249
250
  rm = @document.readonly_memo
250
251
  cached = rm[node_id]
251
252
  return cached if cached
252
-
253
- return rm[node_id] = to_ruby_walk({})
253
+ end
254
+ # Marshal fast path (TODO.restructure/21): one C call turns the
255
+ # whole subtree into Ruby objects, preserving alias identity via
256
+ # the object's own object-link table. Only at the TOP-LEVEL entry
257
+ # (memo nil): inside a recursive walk a partial marshal would
258
+ # materialize outside the shared memo and break alias identity.
259
+ # Falls back to the per-node FFI walk on constructs the format
260
+ # cannot express (merge keys, timestamps) and on older builds.
261
+ if Yeptris::FFI::MARSHAL && memo.nil?
262
+ result =
263
+ begin
264
+ out_p = ::FFI::MemoryPointer.new(:pointer)
265
+ olen_p = ::FFI::MemoryPointer.new(:size_t)
266
+ st = Yeptris::FFI.yeptris_marshal_node(@c_ptr, out_p, olen_p)
267
+ if st == Yeptris::FFI::ERROR_UNSUPPORTED
268
+ nil
269
+ elsif st != Yeptris::FFI::OK
270
+ raise Yeptris::ParseError, Yeptris::FFI.last_error_message
271
+ else
272
+ buf = out_p.read_pointer
273
+ len = olen_p.read_uint64
274
+ bytes = buf.read_bytes(len)
275
+ bytes.force_encoding(Encoding::ASCII_8BIT)
276
+ ::Marshal.load(bytes)
277
+ end
278
+ ensure
279
+ Yeptris::FFI.yeptris_marshal_free(out_p.read_pointer) if out_p
280
+ end
281
+ unless result.nil?
282
+ @document.readonly_memo[node_id] = result if @document.readonly?
283
+ return result
284
+ end
285
+ end
286
+ if @document.readonly?
287
+ return @document.readonly_memo[node_id] = to_ruby_walk({})
254
288
  end
255
289
  to_ruby_walk(memo || {})
256
290
  end
@@ -87,6 +87,35 @@ module Yeptris
87
87
  end
88
88
  end
89
89
 
90
+ # The Marshal fast path (TODO.restructure/21): the C side emits
91
+ # Ruby Marshal 4.8 bytes; one Marshal.load call (a core-C routine)
92
+ # materializes the whole object graph. ~10x faster than walking
93
+ # the columns in pure Ruby on JSON-shaped input; ~5x on YAML. Falls
94
+ # back to load_all_columns on the constructs the format cannot
95
+ # express (merge keys, timestamps — rare; the record walk handles
96
+ # them). Feature-detected: older libyeptris returns the walk.
97
+ def load_all_marshal(yaml, schema: :compat_11, mode: :first)
98
+ yaml = Yeptris.read_input(yaml)
99
+ yaml = yaml.to_s
100
+ out_p = ::FFI::MemoryPointer.new(:pointer)
101
+ olen_p = ::FFI::MemoryPointer.new(:size_t)
102
+ mode_id = mode == :all ? FFI::MARSHAL_ALL_DOCS : FFI::MARSHAL_FIRST_DOC
103
+ st = FFI.yeptris_marshal(
104
+ yaml, yaml.bytesize,
105
+ schema == :compat_11 ? FFI::SCHEMA_11_COMPAT : FFI::SCHEMA_12_CORE,
106
+ mode_id, out_p, olen_p
107
+ )
108
+ return nil if st == FFI::ERROR_UNSUPPORTED
109
+ raise ParseError, FFI.last_error_message if st != FFI::OK
110
+ buf = out_p.read_pointer
111
+ len = olen_p.read_uint64
112
+ bytes = buf.read_bytes(len)
113
+ bytes.force_encoding(Encoding::ASCII_8BIT)
114
+ ::Marshal.load(bytes)
115
+ ensure
116
+ FFI.yeptris_marshal_free(buf) if buf && !buf.null?
117
+ end
118
+
90
119
  def load(yaml, schema: :compat_11)
91
120
  docs = load_all(yaml, schema: schema)
92
121
  docs.empty? ? nil : docs.first
@@ -162,6 +191,12 @@ module Yeptris
162
191
  end
163
192
  slot(docs, stack, pending_key, pending_key_tag, v, merge_target, flat[i + IS_KEY], flat[i + TAG])
164
193
  when V_NULL
194
+ # the pending anchor binds to the null (an anchored empty
195
+ # scalar) — without this it would leak onto the next value
196
+ if pending_anchor
197
+ anchors[pending_anchor] = nil
198
+ pending_anchor = nil
199
+ end
165
200
  slot(docs, stack, pending_key, pending_key_tag, nil, merge_target, flat[i + IS_KEY], flat[i + TAG])
166
201
  when V_TS
167
202
  v = Materializer.parse_timestamp(arena.byteslice(flat[i + OFF], flat[i + LEN]))
@@ -315,6 +350,10 @@ module Yeptris
315
350
  end
316
351
  slot(docs, stack, pending_key, pending_key_tag, v, merge_target, ikeys[i], tags[i])
317
352
  when V_NULL
353
+ if pending_anchor
354
+ anchors[pending_anchor] = nil
355
+ pending_anchor = nil
356
+ end
318
357
  slot(docs, stack, pending_key, pending_key_tag, nil, merge_target, ikeys[i], tags[i])
319
358
  when V_TS
320
359
  v = Materializer.parse_timestamp(arena.byteslice(offs[i], lens[i]))
data/lib/yeptris/yaml.rb CHANGED
@@ -20,10 +20,14 @@ module Yeptris
20
20
  _drain_all(yaml, schema)
21
21
  end
22
22
 
23
- # The columnar drain when the loaded libyeptris has it (>= 0.1.2
24
- # era builds), the record drain otherwise one code path, the
25
- # fastest the library offers.
23
+ # The Marshal fast path when the loaded libyeptris has it (>= 0.1.11
24
+ # era builds), falling back to the columnar drain and finally the
25
+ # record drain — one code path, the fastest the library offers.
26
26
  def _drain_all(yaml, schema)
27
+ if FFI::MARSHAL
28
+ result = ValueML.load_all_marshal(yaml, schema: schema, mode: :all)
29
+ return result unless result.nil?
30
+ end
27
31
  if FFI::COLUMNS
28
32
  ValueML.load_all_columns(yaml, schema: schema)
29
33
  else
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.1.10.0".freeze
6
+ VERSION = "0.1.11.0".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
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.1.10.0
4
+ version: 0.1.11.0
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-06 00:00:00.000000000 Z
11
+ date: 2026-09-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ffi