yeptris 0.2.3.1-aarch64-linux → 0.2.5.1-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: b81fde0cf18e3da68e631f46189f2a63cd34059bec03f27ee6d6c3618c3fda65
4
- data.tar.gz: 3bffc4c5d902f2f561cc46f43fd49f953dc6171ea10f075c3b31b699dad8dabb
3
+ metadata.gz: 607330f49f5f86cf870d5d4512934db643f6296be3624221e0493e5e7266bf9e
4
+ data.tar.gz: 8e2edb0acd3d681cbe673e0a395e0978b168bff5e4f2194e6a822daf3c816580
5
5
  SHA512:
6
- metadata.gz: fe74c81497443546719e865d53dcfe5a21626e386a041644c037a8aed3e535eed0976ca3543cb01a7d9332dd43d5eb3609bcb491238b3ba829d2eacfb913c277
7
- data.tar.gz: '08f02f4bd16dbcf01cf11e88a00c779dc12a297f7c8faee22b4e409fdbcf33364bb2b353ee528d2116e0c047245457715e2a455ff0d224dc5ea368f7fc3dc2e5'
6
+ metadata.gz: 4ef4f9db3563932be9244404b66742e6187271e0f6ec0f189a24c1edb6fc5a2c9aa600cfc1611157e0d42b3f432ba74b45ba81546dd459d61075528b72b0366e
7
+ data.tar.gz: 236554c32376824bf59a8cf2f44b0307621f34337cc8ef0738612f80a01df044c2a7148bc8358a617ab972d7c9aed3848f0181e513a05d68125babcd7484efe5
@@ -153,6 +153,11 @@ class Yeptris::Document
153
153
  Yeptris::FFI::Owned.string(ptr, len)
154
154
  end
155
155
 
156
+ def serialize_json_compact
157
+ len = ::FFI::MemoryPointer.new(:uint64)
158
+ Yeptris::FFI::Owned.string(Yeptris::FFI.yeptris_serialize_json_ex(@c_ptr, len, 1), len)
159
+ end
160
+
156
161
  def serialize_json
157
162
  ensure_alive!
158
163
  len = ::FFI::MemoryPointer.new(:uint64)
data/lib/yeptris/ffi.rb CHANGED
@@ -59,6 +59,17 @@ module Yeptris
59
59
  %i[pointer size_t pointer yeptris_status_out], :yeptris_document
60
60
  attach_function :yeptris_parse_json, %i[pointer size_t yeptris_status_out], :yeptris_document
61
61
 
62
+ # The JSON tape (TODO.restructure/85; the JSON surface's load
63
+ # engine — issue #81): ONE call, four bulk columns, spans borrow
64
+ # the caller's string (no arena copy, no second validating parse).
65
+ class JsonTape < ::FFI::Struct
66
+ layout :count, :size_t, :kinds, :pointer, :offs, :pointer,
67
+ :lens, :pointer, :vals, :pointer, :int_min, :int64, :_block, :pointer
68
+ end
69
+
70
+ attach_function :yeptris_parse_json_tape, %i[pointer size_t pointer], :int
71
+ attach_function :yeptris_tape_free, [:pointer], :void
72
+
62
73
  attach_function :yeptris_document_free, [:yeptris_document], :void
63
74
  attach_function :yeptris_document_count, [:yeptris_document], :size_t
64
75
  attach_function :yeptris_document_root, [:yeptris_document, :size_t], :yeptris_node
@@ -174,6 +185,15 @@ module Yeptris
174
185
  %i[yeptris_document pointer pointer], :pointer
175
186
  attach_function :yeptris_serialize_json, %i[yeptris_document pointer], :pointer
176
187
 
188
+ # compact JSON generation (JSON.generate's shape); absent on
189
+ # libraries before v0.2.5 — the JSON surface feature-detects
190
+ JSON_EX = begin
191
+ attach_function :yeptris_serialize_json_ex, %i[yeptris_document pointer int], :pointer
192
+ true
193
+ rescue ::FFI::NotFoundError
194
+ false
195
+ end
196
+
177
197
  # Owned char* results (serialize*): one reader, freed exactly once.
178
198
  # The buffers are plain malloc'd C memory (the header contract says
179
199
  # "caller frees"), so the release is libc free.
data/lib/yeptris/json.rb CHANGED
@@ -36,7 +36,104 @@ module Yeptris
36
36
  raise ParseError, e.message
37
37
  end
38
38
  end
39
- strict_fallback(source)
39
+ tape_engine(source)
40
+ end
41
+
42
+ # The tape engine (issue #81): ONE call validates and materializes
43
+ # the record stream; the walk reads four bulk columns and slices
44
+ # strings straight out of the caller's source — no gate document,
45
+ # no recorder transform, no arena copy (the drain path paid all
46
+ # three, which is where medium/large throughput went).
47
+ T_DOC = 0
48
+ T_NULL = 1
49
+ T_BOOL = 2
50
+ T_INT = 3
51
+ T_FLOAT = 4
52
+ T_STR = 5
53
+ T_SEQ_OPEN = 6
54
+ T_MAP_OPEN = 7
55
+ T_CLOSE = 8
56
+
57
+ def tape_engine(source)
58
+ tape = ::Yeptris::FFI::JsonTape.new
59
+ rc = ::Yeptris::FFI.yeptris_parse_json_tape(source, source.bytesize, tape)
60
+ raise ParseError, ::Yeptris::FFI.last_error_message if rc != ::Yeptris::FFI::OK
61
+
62
+ begin
63
+ walk_tape(source, tape)
64
+ ensure
65
+ ::Yeptris::FFI.yeptris_tape_free(tape)
66
+ end
67
+ end
68
+
69
+ def walk_tape(src, tape)
70
+ n = tape[:count]
71
+ kinds = tape[:kinds].read_bytes(n).unpack("C*")
72
+ offs = tape[:offs].read_bytes(n * 4).unpack("V*")
73
+ lens = tape[:lens].read_bytes(n * 4).unpack("V*")
74
+ raw = tape[:vals].read_bytes(n * 8)
75
+ vals_i = raw.unpack("q<*")
76
+ vals_f = raw.unpack("E")
77
+ # any integer text beyond int64: every INT rebuilds from its span
78
+ # (exact Bignum; JSON.parse parity)
79
+ exact_ints = !tape[:int_min].zero?
80
+ utf8 = src.encoding == Encoding::UTF_8
81
+
82
+ docs = []
83
+ stack = []
84
+ key_sets = STRICT_DUPLICATE_KEYS ? [{}] : nil
85
+ pending_key = nil
86
+ i = 1 # record 0 is the DOC boundary
87
+ while i < n
88
+ case kinds[i]
89
+ when T_SEQ_OPEN
90
+ place(docs, stack, pending_key) { [] }
91
+ pending_key = nil
92
+ when T_MAP_OPEN
93
+ key_sets&.push({})
94
+ place(docs, stack, pending_key) { {} }
95
+ pending_key = nil
96
+ when T_CLOSE
97
+ stack.pop
98
+ key_sets&.pop
99
+ when T_STR
100
+ text = src.byteslice(offs[i], lens[i])
101
+ text.force_encoding(Encoding::UTF_8) unless utf8
102
+ if pending_key.nil? && !stack.empty? && stack.last.is_a?(Hash)
103
+ if key_sets && key_sets.last.key?(text)
104
+ raise ParseError, %(duplicate key "#{text}" in JSON object)
105
+ end
106
+ key_sets&.last&.store(text, true)
107
+ pending_key = text
108
+ else
109
+ place(docs, stack, pending_key) { text }
110
+ pending_key = nil
111
+ end
112
+ when T_INT
113
+ if exact_ints
114
+ place(docs, stack, pending_key) { Integer(src.byteslice(offs[i], lens[i]), 10) }
115
+ else
116
+ v = vals_i[i]
117
+ place(docs, stack, pending_key) { v }
118
+ end
119
+ pending_key = nil
120
+ when T_FLOAT
121
+ v = vals_f[i]
122
+ place(docs, stack, pending_key) { v }
123
+ pending_key = nil
124
+ when T_BOOL
125
+ v = vals_i[i] == 1
126
+ place(docs, stack, pending_key) { v }
127
+ pending_key = nil
128
+ when T_NULL
129
+ place(docs, stack, pending_key) { nil }
130
+ pending_key = nil
131
+ else
132
+ raise Error, "internal: impossible tape record #{kinds[i]}"
133
+ end
134
+ i += 1
135
+ end
136
+ docs.empty? ? nil : docs.first
40
137
  end
41
138
 
42
139
  # The always-available engine: the strict-JSON validator gates
@@ -143,6 +240,78 @@ module Yeptris
143
240
  docs.empty? ? nil : docs.first
144
241
  end
145
242
 
243
+ # ---- generation (issue #81) ------------------------------------------
244
+ #
245
+ # `dump` targets JSON.generate semantics for the core types: the
246
+ # object walk reuses YAML.dump's packed-entry bulk builder (one
247
+ # FFI build call), then the strict-JSON writer serializes —
248
+ # Strings ride DOUBLE_QUOTED (a "42" stays quoted; numbers, bools
249
+ # and null stay plain), Symbols become their name.
250
+ class DumpError < Error; end
251
+
252
+ ENTRY_BYTES = 12
253
+ MAP_ENTRY = [::Yeptris::FFI::BUILD_MAP, 0, 0, 0].pack("CCx2VV").freeze
254
+ SEQ_ENTRY = [::Yeptris::FFI::BUILD_SEQ, 0, 0, 0].pack("CCx2VV").freeze
255
+ END_ENTRY = [::Yeptris::FFI::BUILD_END, 0, 0, 0].pack("CCx2VV").freeze
256
+ CONT_ENTRY = { ::Yeptris::FFI::BUILD_MAP => MAP_ENTRY, ::Yeptris::FFI::BUILD_SEQ => SEQ_ENTRY,
257
+ ::Yeptris::FFI::BUILD_END => END_ENTRY }.freeze
258
+
259
+ def dump(obj)
260
+ parts = []
261
+ blob = String.new(encoding: Encoding::BINARY)
262
+ off = [0]
263
+ emit = lambda do |op, style, o, len|
264
+ parts << (o.zero? && len.zero? && style.zero? ? CONT_ENTRY[op] :
265
+ [op, style, o, len].pack("CCx2VV"))
266
+ end
267
+ place_obj(obj, emit, blob, off)
268
+ doc = ::Yeptris::Document.create
269
+ buf = ::FFI::MemoryPointer.from_string(parts.join)
270
+ bblob = ::FFI::MemoryPointer.from_string(blob)
271
+ rc = doc.build_entries(buf, parts.length, bblob, blob.bytesize)
272
+ raise DumpError, "document_build failed: #{rc}" unless rc == ::Yeptris::FFI::OK
273
+ out = ::Yeptris::FFI::JSON_EX ? doc.serialize_json_compact : doc.serialize_json
274
+ # JSON.generate emits no trailing newline (the document writer does)
275
+ out.end_with?("\n") ? out[0, out.bytesize - 1] : out
276
+ ensure
277
+ doc&.free
278
+ end
279
+
280
+ def place_obj(obj, emit, blob, off)
281
+ case obj
282
+ when Hash
283
+ emit.call(::Yeptris::FFI::BUILD_MAP, 0, 0, 0)
284
+ obj.each do |k, v|
285
+ key = k.is_a?(String) ? k : k.is_a?(Symbol) ? k.name : k.to_s
286
+ scalar_json(key, ::Yeptris::FFI::STYLE_DOUBLE_QUOTED, emit, blob, off)
287
+ place_obj(v, emit, blob, off)
288
+ end
289
+ emit.call(::Yeptris::FFI::BUILD_END, 0, 0, 0)
290
+ when Array
291
+ emit.call(::Yeptris::FFI::BUILD_SEQ, 0, 0, 0)
292
+ obj.each { |e| place_obj(e, emit, blob, off) }
293
+ emit.call(::Yeptris::FFI::BUILD_END, 0, 0, 0)
294
+ when String
295
+ scalar_json(obj, ::Yeptris::FFI::STYLE_DOUBLE_QUOTED, emit, blob, off)
296
+ when Symbol
297
+ scalar_json(obj.name, ::Yeptris::FFI::STYLE_DOUBLE_QUOTED, emit, blob, off)
298
+ when Integer, Float, true, false, nil
299
+ text = obj.nil? ? "null" : obj.to_s
300
+ scalar_json(text, ::Yeptris::FFI::STYLE_PLAIN, emit, blob, off)
301
+ when Date, Time
302
+ scalar_json(obj.iso8601, ::Yeptris::FFI::STYLE_DOUBLE_QUOTED, emit, blob, off)
303
+ else
304
+ raise DumpError, "cannot dump #{obj.class}: unsupported object " \
305
+ "(JSON.generate calls to_json on custom types)"
306
+ end
307
+ end
308
+
309
+ def scalar_json(text, style, emit, blob, off)
310
+ emit.call(::Yeptris::FFI::BUILD_SCALAR, style, off[0], text.bytesize)
311
+ blob << text
312
+ off[0] += text.bytesize
313
+ end
314
+
146
315
  def place(docs, stack, key)
147
316
  v = yield
148
317
  if stack.empty?
Binary file
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.2.3.1".freeze
6
+ VERSION = "0.2.5.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
data/libyeptris.so CHANGED
Binary file
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.2.3.1
4
+ version: 0.2.5.1
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-13 00:00:00.000000000 Z
11
+ date: 2026-09-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ffi