yeptris 0.1.13.3-x86_64-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.
@@ -0,0 +1,278 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yeptris
4
+ # The neutral Ruby surface (the Psych-compat namespace arrives with
5
+ # the recorder-driven Visitors in phase B; this is the yeptris-native
6
+ # face users target first).
7
+ module YAML
8
+ module_function
9
+
10
+ # Loads the FIRST document of a YAML stream as native Ruby objects.
11
+ # schema: :compat_11 selects Psych/libyaml implicit typing
12
+ # (yes/no, 0o/octal, sexagesimal); :core_12 (default) is YAML 1.2.
13
+ #
14
+ # This surface keeps the Psych contract for EVERY input — including
15
+ # JSON-shaped ones (`{"a": [1,]}` is legal flow YAML; `"1e3"` is a
16
+ # Psych String). Strict RFC 8259 semantics live on Yeptris::JSON
17
+ # (TODO.restructure/31): defaults follow proof, not benchmarks.
18
+ def load(yaml, schema: :compat_11)
19
+ yaml = Yeptris.read_input(yaml)
20
+ yaml = yaml.to_s
21
+ docs = _drain_all(yaml, schema)
22
+ docs.empty? ? nil : docs.first
23
+ end
24
+
25
+ # Every document in the stream, in order.
26
+ def load_stream(yaml, schema: :compat_11)
27
+ yaml = Yeptris.read_input(yaml)
28
+ yaml = yaml.to_s
29
+ _drain_all(yaml, schema)
30
+ end
31
+
32
+ # The Marshal fast path when the loaded libyeptris has it (>= 0.1.11
33
+ # era builds), falling back to the columnar drain and finally the
34
+ # record drain — one code path, the fastest the library offers.
35
+ def _drain_all(yaml, schema)
36
+ if FFI::MARSHAL
37
+ result = ValueML.load_all_marshal(yaml, schema: schema, mode: :all)
38
+ return result unless result.nil?
39
+ end
40
+ if FFI::COLUMNS
41
+ ValueML.load_all_columns(yaml, schema: schema)
42
+ else
43
+ ValueML.load_all(yaml, schema: schema)
44
+ end
45
+ end
46
+
47
+ # The Psych-suite port's spelling (spec/psych/): compat typing.
48
+ def parse_yaml(yaml)
49
+ load(yaml)
50
+ end
51
+
52
+ def load_file(path, schema: :compat_11)
53
+ File.open(path, "rb") { |f| load(f, schema: schema) }
54
+ end
55
+
56
+ # Parses without materializing: the first document's root Node.
57
+ def parse(yaml, schema: :core_12)
58
+ doc = Document.parse(yaml, schema: schema)
59
+ return nil if doc.document_count.zero?
60
+
61
+ doc.root(0)
62
+ end
63
+
64
+ # Serializes a Ruby object graph to YAML via the DOM builder
65
+ # (TODO.impl/11 phase 3). Strings are emitted plain only when the
66
+ # resolver round-trips them as strings — everything else takes a
67
+ # quoted style, so dump(load(x)) == x for the scalar types.
68
+ def dump(obj, canonical: false)
69
+ BulkBuilder.dump(obj, canonical: canonical)
70
+ end
71
+
72
+
73
+ # The dump-side mirror of the Materializer's bulk drain (TODO.impl
74
+ # 15 phase D): the tree walks into one flat entry array plus one
75
+ # string blob, and yeptris_document_build raises the DOM in a
76
+ # SINGLE FFI call — per-node FFI is gone. Same semantics as the
77
+ # per-node Builder it replaces (cycle refusal, :symbol scalars,
78
+ # Date/Time iso8601, plain-only-when-it-round-trips strings),
79
+ # with plain_string? pinned to the resolver by a differential
80
+ # spec.
81
+ module BulkBuilder
82
+ SCALAR = Yeptris::FFI::BUILD_SCALAR
83
+ SEQ = Yeptris::FFI::BUILD_SEQ
84
+ MAP = Yeptris::FFI::BUILD_MAP
85
+ STOP = Yeptris::FFI::BUILD_END
86
+ STYLE_PLAIN = 1
87
+ STYLE_DQ = 3
88
+
89
+ module_function
90
+
91
+ # container entries are constant bytes — no pack per op
92
+ MAP_ENTRY = [Yeptris::FFI::BUILD_MAP, 0, 0, 0].pack("CCx2VV").freeze
93
+ SEQ_ENTRY = [Yeptris::FFI::BUILD_SEQ, 0, 0, 0].pack("CCx2VV").freeze
94
+ END_ENTRY = [Yeptris::FFI::BUILD_END, 0, 0, 0].pack("CCx2VV").freeze
95
+ CONT_ENTRY = { Yeptris::FFI::BUILD_MAP => MAP_ENTRY,
96
+ Yeptris::FFI::BUILD_SEQ => SEQ_ENTRY,
97
+ Yeptris::FFI::BUILD_END => END_ENTRY }.freeze
98
+
99
+ def dump(obj, canonical: false)
100
+ parts = []
101
+ blob = String.new(encoding: Encoding::BINARY)
102
+ off = [0]
103
+ # one pack per SCALAR; containers reuse frozen constants
104
+ emit = lambda do |op, style, o, len|
105
+ parts << (o.zero? && len.zero? && style.zero? ? CONT_ENTRY[op] :
106
+ [op, style, o, len].pack("CCx2VV"))
107
+ end
108
+ place(obj, emit, blob, off, {})
109
+ doc = Document.create
110
+ buf = ::FFI::MemoryPointer.from_string(parts.join)
111
+ bblob = ::FFI::MemoryPointer.from_string(blob)
112
+ rc = doc.build_entries(buf, parts.length, bblob, blob.bytesize)
113
+ raise DumpError, "document_build failed: #{rc}" unless rc == FFI::OK
114
+ doc.serialize(canonical: canonical)
115
+ ensure
116
+ doc&.free
117
+ end
118
+
119
+ def place(obj, emit, blob, off, seen)
120
+ case obj
121
+ when Hash
122
+ cycle_guard(obj, seen) do
123
+ emit.call(MAP, 0, 0, 0)
124
+ obj.each do |k, v|
125
+ place(key_text(k), emit, blob, off, seen)
126
+ place(v, emit, blob, off, seen)
127
+ end
128
+ emit.call(STOP, 0, 0, 0)
129
+ end
130
+ when Array
131
+ cycle_guard(obj, seen) do
132
+ emit.call(SEQ, 0, 0, 0)
133
+ obj.each { |e| place(e, emit, blob, off, seen) }
134
+ emit.call(STOP, 0, 0, 0)
135
+ end
136
+ when String then scalar(obj, plain_string?(obj), emit, blob, off)
137
+ when Symbol then scalar(":#{obj}", true, emit, blob, off)
138
+ when Integer, Float then scalar(obj.to_s, true, emit, blob, off)
139
+ when true, false then scalar(obj.to_s, true, emit, blob, off)
140
+ when nil then scalar("null", true, emit, blob, off)
141
+ when Date, Time then scalar(obj.iso8601, true, emit, blob, off)
142
+ else
143
+ raise DumpError,
144
+ "cannot dump #{obj.class}: unsupported object " \
145
+ "(custom to_yaml support lands with the Psych Visitors)"
146
+ end
147
+ end
148
+
149
+ def scalar(text, plain, emit, blob, off)
150
+ # a BINARY blob absorbs any String bytewise (String#<< on
151
+ # ASCII-8BIT is compatible with every encoding) — the old
152
+ # text.b made a throwaway copy of every scalar before the
153
+ # blob's own copy
154
+ emit.call(SCALAR, plain ? STYLE_PLAIN : STYLE_DQ, off[0], text.bytesize)
155
+ blob << text
156
+ off[0] += text.bytesize
157
+ end
158
+
159
+ def key_text(k)
160
+ k = ":#{k}" if k.is_a?(Symbol)
161
+ k.to_s
162
+ end
163
+
164
+ # A plain scalar that the compat resolver re-reads as STR stays
165
+ # plain; anything resolvable (null/bool words, int/float/timestamp
166
+ # shapes, indicators, merge '<<') takes double quotes so the
167
+ # reparse yields String again. Pinned to the C resolver's own
168
+ # verdicts by the differential spec (spec/yaml_spec.rb).
169
+ RESHAPES = %w[~ null Null NULL y Y yes Yes YES n N no No NO true True
170
+ TRUE false False FALSE on On ON off Off OFF <<].freeze
171
+ # the fast lane consults the reshape table per string: an Array
172
+ # scan of 36 words was ~a third of the whole dump walk
173
+ RESHAPES_SET = RESHAPES.each_with_object({}) { |w, h| h[w] = true }.freeze
174
+
175
+ SAFE_WORD = /\A[A-Za-z][A-Za-z0-9_\-.\/ ]*\z/
176
+
177
+ def plain_string?(s)
178
+ # fast lane: letter-started, safe characters incl. spaces — no
179
+ # number/timestamp/indicator shape is possible; only the
180
+ # reshape words need the set lookup
181
+ if SAFE_WORD.match?(s) && !s.end_with?(" ") && !RESHAPES_SET[s]
182
+ return true
183
+ end
184
+ return false if s.empty? || s != s.strip
185
+ return false if s.match?(/[\n\t]/)
186
+ c = s[0]
187
+ return false if "#,[]{}&*!|>'\"%@`".include?(c)
188
+ return false if "-?:".include?(c) && (s.length == 1 || s[1] =~ /[ \t]/)
189
+ return false if s.include?(": ") || s.end_with?(":") || s.include?(" #")
190
+ return false if RESHAPES_SET[s]
191
+ # compat's float grammar REQUIRES the dot ("1e3" re-reads as a
192
+ # String and may dump plain); ints/sexagesimals still reshape
193
+ return false if s.match?(/\A[-+]?(0|[1-9][0-9_]*)(:[0-5]?[0-9])+\z/)
194
+ return false if s.match?(/\A[-+]?(0|[1-9][0-9_]*)\z/)
195
+ return false if s.match?(/\A[-+]?[0-9][0-9_]*\.[0-9_]*([eE][-+]?[0-9]+)?([.:][0-9_:.]*)?\z/)
196
+ return false if s.match?(/\A[-+]?(0x[0-9a-fA-F_]+|0b[01_]+|0o?[0-7_]+)\z/)
197
+ return false if s.match?(/\A[-+]?\.(inf|Inf|INF)\z|\A\.(nan|NaN|NAN)\z/)
198
+ !s.match?(/\A\d{4}-\d\d?-\d\d?([Tt ]|$)/)
199
+ end
200
+
201
+ def cycle_guard(obj, seen)
202
+ id = obj.object_id
203
+ raise DumpError, "cycle detected: cannot dump recursive #{obj.class}" if seen[id]
204
+
205
+ seen[id] = true
206
+ out = yield
207
+ seen.delete(id)
208
+ out
209
+ end
210
+ end
211
+
212
+ # From-scratch builder over the public construction API.
213
+ module Builder
214
+ module_function
215
+
216
+ def build(doc, obj, seen = {})
217
+ case obj
218
+ when Hash then build_map(doc, obj, seen)
219
+ when Array then build_seq(doc, obj, seen)
220
+ when String then build_string(doc, obj)
221
+ when Symbol then new_scalar(doc, ":#{obj}")
222
+ when Integer, Float then new_scalar(doc, obj.to_s)
223
+ when true, false then new_scalar(doc, obj.to_s)
224
+ when nil then new_scalar(doc, "null")
225
+ when Date, Time then new_scalar(doc, obj.iso8601)
226
+ else
227
+ raise DumpError,
228
+ "cannot dump #{obj.class}: unsupported object " \
229
+ "(custom to_yaml support lands with the Psych Visitors)"
230
+ end
231
+ end
232
+
233
+ def build_map(doc, h, seen)
234
+ cycle_guard(h, seen) do
235
+ m = doc.new_mapping
236
+ h.each { |k, v| m.map_add(key_text(k), build(doc, v, seen)) }
237
+ m
238
+ end
239
+ end
240
+
241
+ def build_seq(doc, a, seen)
242
+ cycle_guard(a, seen) do
243
+ s = doc.new_sequence
244
+ a.each { |e| s.seq_add(build(doc, e, seen)) }
245
+ s
246
+ end
247
+ end
248
+
249
+ # A plain scalar that re-resolves to STR stays plain (nice
250
+ # round-trips); anything ambiguous is double-quoted so the
251
+ # reparse yields String again.
252
+ def build_string(doc, s)
253
+ n = new_scalar(doc, s)
254
+ n.tag_id == :str ? n : new_scalar(doc, s, :force_str)
255
+ end
256
+
257
+ def key_text(k)
258
+ k = ":#{k}" if k.is_a?(Symbol)
259
+ k.to_s
260
+ end
261
+
262
+ def new_scalar(doc, text, mode = nil)
263
+ doc.new_scalar(text.to_s,
264
+ mode == :force_str ? :double_quoted : :plain)
265
+ end
266
+
267
+ def cycle_guard(obj, seen)
268
+ id = obj.object_id
269
+ raise DumpError, "cycle detected: cannot dump recursive #{obj.class}" if seen[id]
270
+
271
+ seen[id] = true
272
+ out = yield
273
+ seen.delete(id)
274
+ out
275
+ end
276
+ end
277
+ end
278
+ end
data/lib/yeptris.rb ADDED
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yeptris
4
+ # The gem's version lives in the parent namespace's file — the last
5
+ # internal require (yeptris/version) retired with it.
6
+ VERSION = "0.1.13.3".freeze
7
+ # The error hierarchy lives in THIS file (the parent namespace's
8
+ # own file): nested constants do not trigger a parent-constant
9
+ # autoload, and the law forbids internal requires — defining the
10
+ # hierarchy here makes it eager by construction.
11
+
12
+ # The base error for everything this library raises deliberately.
13
+ class Error < StandardError; end
14
+
15
+ # The input is not valid YAML (or valid for the requested mode).
16
+ # message carries the C parser's line/column detail.
17
+ class ParseError < Error; end
18
+
19
+ # A handle was used after its document was freed. Raised, never a
20
+ # segfault: the Document is the sole C-memory owner and every Node
21
+ # checks liveness through it.
22
+ class FreedError < Error; end
23
+
24
+ # Building a document from a Ruby object graph hit something the
25
+ # builder refuses (cycles, unsupported objects).
26
+ class DumpError < Error; end
27
+
28
+ # Input coercion — the ONE place the input boundary is typed
29
+ # (no respond_to? duck-probing): IO-like objects read, Strings
30
+ # pass through, anything else must be stringable and is.
31
+ def self.read_input(source)
32
+ case source
33
+ when String then source
34
+ when IO, StringIO then source.read
35
+ else source.to_s
36
+ end
37
+ end
38
+
39
+ autoload :Document, "yeptris/document"
40
+ autoload :Node, "yeptris/node"
41
+ autoload :YAML, "yeptris/yaml"
42
+ autoload :JSON, "yeptris/json"
43
+ autoload :Materializer, "yeptris/materializer"
44
+ autoload :ValueML, "yeptris/valueml"
45
+ autoload :Psych, "yeptris/psych"
46
+ end
47
+
48
+ # Eager native-library resolution (leptris-ruby lesson): fail at
49
+ # require time, not at first parse. The ffi require MUST come after
50
+ # the autoload registrations — ffi.rb opens module Yeptris, and
51
+ # requiring it first would shadow the manifest (leptris-ruby#53).
52
+ begin
53
+ require "yeptris/ffi"
54
+ rescue LoadError => e
55
+ raise LoadError, <<~MSG
56
+ Yeptris could not load the native libyeptris library.
57
+ Set YEPTRIS_LIB_PATH to a libyeptris.{so,dylib,dll}, or use the
58
+ platform gem that vendors it.
59
+ (Underlying error: #{e.message})
60
+ MSG
61
+ end
62
+
63
+ # Optional C-API materializer (TODO.restructure/22): fused visit →
64
+ # Ruby objects via the Ruby C API. Feature-detected — LoadError leaves
65
+ # the FFI ladder (Marshal → columns → records) as the sole path.
66
+ begin
67
+ require "yeptris/native"
68
+ rescue LoadError
69
+ # FFI ladder only
70
+ end
data/libyeptris.so ADDED
Binary file
metadata ADDED
@@ -0,0 +1,82 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: yeptris
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.13.3
5
+ platform: x86_64-linux
6
+ authors:
7
+ - Ribose Inc.
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-09-07 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: ffi
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.15'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.15'
27
+ description: A Ruby YAML library over libyeptris — Psych-compatible semantics with
28
+ libleptris-class performance, and a fused native JSON materializer that outperforms
29
+ JSON.parse on JSON-shaped input.
30
+ email:
31
+ - open.source@ribose.com
32
+ executables: []
33
+ extensions: []
34
+ extra_rdoc_files: []
35
+ files:
36
+ - README.adoc
37
+ - ext/yeptris_native/extconf.rb
38
+ - ext/yeptris_native/json_ruby.c
39
+ - ext/yeptris_native/yeptris_native.c
40
+ - lib/yeptris.rb
41
+ - lib/yeptris/document.rb
42
+ - lib/yeptris/ffi.rb
43
+ - lib/yeptris/json.rb
44
+ - lib/yeptris/materializer.rb
45
+ - lib/yeptris/native.so
46
+ - lib/yeptris/node.rb
47
+ - lib/yeptris/psych.rb
48
+ - lib/yeptris/psych/coder_shim.rb
49
+ - lib/yeptris/psych/encodable.rb
50
+ - lib/yeptris/psych/handler.rb
51
+ - lib/yeptris/psych/parser.rb
52
+ - lib/yeptris/psych/visitors.rb
53
+ - lib/yeptris/valueml.rb
54
+ - lib/yeptris/yaml.rb
55
+ - libyeptris.so
56
+ homepage: https://github.com/leptris/yeptris
57
+ licenses:
58
+ - MIT
59
+ metadata:
60
+ homepage_uri: https://github.com/leptris/yeptris
61
+ source_code_uri: https://github.com/leptris/yeptris
62
+ changelog_uri: https://github.com/leptris/yeptris/releases
63
+ post_install_message:
64
+ rdoc_options: []
65
+ require_paths:
66
+ - lib
67
+ required_ruby_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: '3.1'
72
+ required_rubygems_version: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ requirements: []
78
+ rubygems_version: 3.5.22
79
+ signing_key:
80
+ specification_version: 4
81
+ summary: 'The YAML counterpart of libleptris: ultra-performance YAML 1.2 for Ruby'
82
+ test_files: []