yeptris 0.2.0.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.
@@ -0,0 +1,455 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yeptris
4
+ # Value-stream materialization (TODO.impl/15 phase F): one drain of
5
+ # PRE-CONVERTED typed values from the C side, then a minimal Ruby
6
+ # walk — no per-scalar parsing (the number kernels already ran),
7
+ # key/value pairing rides the entries' is_key bit, anchors arrive
8
+ # as uniform entries decorating the value they bind. The Psych
9
+ # quirks re-decide from tag_id + the raw bytes every entry carries.
10
+ module ValueML
11
+ DOC = 0
12
+ V_NULL = 1
13
+ V_BOOL = 2
14
+ V_INT = 3
15
+ V_FLOAT = 4
16
+ V_STR = 5
17
+
18
+ # Psych's integer shape (scalar_scanner): sign optional, no
19
+ # leading zero, '_' / ',' separators only between digits. Beyond
20
+ # int64 the C resolver leaves the scalar a string; Psych
21
+ # materializes Integer (issue #31) — every walk rebuilds it.
22
+ PSYCH_INT_SHAPE = /\A[-+]?(?:0|[1-9](?:[0-9]|,[0-9]|_[0-9])*)\z/.freeze
23
+ V_TS = 6
24
+ SEQ_OPEN = 7
25
+ MAP_OPEN = 8
26
+ CLOSE = 9
27
+ ALIAS = 10
28
+ ANCHOR = 11
29
+
30
+ FIELDS = 7
31
+ VALUE_SIZE = 24
32
+ # kind, tag, is_key, b | off, len | pad4 | payload (INT/FLOAT bits)
33
+ UNPACK = "C4V2x4q<"
34
+
35
+ module_function
36
+
37
+ def load_all(yaml, schema: :compat_11)
38
+ yaml = Yeptris.read_input(yaml)
39
+ yaml = yaml.to_s
40
+ vals_p = ::FFI::MemoryPointer.new(:pointer)
41
+ count_p = ::FFI::MemoryPointer.new(:uint64)
42
+ arena_p = ::FFI::MemoryPointer.new(:pointer)
43
+ alen_p = ::FFI::MemoryPointer.new(:uint64)
44
+ st = FFI.yeptris_value_drain(
45
+ yaml, yaml.bytesize,
46
+ schema == :compat_11 ? FFI::SCHEMA_11_COMPAT : FFI::SCHEMA_12_CORE,
47
+ vals_p, count_p, arena_p, alen_p
48
+ )
49
+ raise ParseError, FFI.last_error_message if st != FFI::OK
50
+
51
+ vals = vals_p.read_pointer
52
+ arena = arena_p.read_pointer
53
+ begin
54
+ count = count_p.read_uint64
55
+ flat = vals.read_bytes(count * VALUE_SIZE).unpack(UNPACK * count)
56
+ arena_bytes = arena.read_bytes(alen_p.read_uint64)
57
+ arena_bytes.force_encoding(Encoding::UTF_8)
58
+ walk(flat, arena_bytes, schema == :compat_11)
59
+ ensure
60
+ FFI.yeptris_value_free(vals, arena)
61
+ end
62
+ end
63
+
64
+ # Columnar path (libyeptris > 0.1.1): whole columns unpack in one
65
+ # call each, and the walk indexes tight arrays (no 7-slot stride).
66
+ # Semantics are IDENTICAL to the record walk — the two bodies are
67
+ # maintained in lockstep; field-access shape is the only difference.
68
+ def load_all_columns(yaml, schema: :compat_11)
69
+ yaml = Yeptris.read_input(yaml)
70
+ yaml = yaml.to_s
71
+ cols = FFI::ValueColumns.new
72
+ st = FFI.yeptris_value_drain_columns(
73
+ yaml, yaml.bytesize,
74
+ schema == :compat_11 ? FFI::SCHEMA_11_COMPAT : FFI::SCHEMA_12_CORE,
75
+ cols
76
+ )
77
+ raise ParseError, FFI.last_error_message if st != FFI::OK
78
+
79
+ begin
80
+ n = cols[:count]
81
+ kinds = cols[:kinds].read_bytes(n).unpack("C*")
82
+ tags = cols[:tags].read_bytes(n).unpack("C*")
83
+ ikeys = cols[:is_keys].read_bytes(n).unpack("C*")
84
+ bools = cols[:bools].read_bytes(n).unpack("C*")
85
+ offs = cols[:offs].read_bytes(n * 4).unpack("V*")
86
+ lens = cols[:lens].read_bytes(n * 4).unpack("V*")
87
+ pays = cols[:payloads].read_bytes(n * 8).unpack("q<*")
88
+ arena_bytes = cols[:arena].read_bytes(cols[:arena_len])
89
+ arena_bytes.force_encoding(Encoding::UTF_8)
90
+ walk_columns(kinds, tags, ikeys, bools, offs, lens, pays, arena_bytes,
91
+ schema == :compat_11)
92
+ ensure
93
+ FFI.yeptris_value_free_columns(cols)
94
+ end
95
+ end
96
+
97
+ # The Marshal fast path (TODO.restructure/21): the C side emits
98
+ # Ruby Marshal 4.8 bytes; one Marshal.load call (a core-C routine)
99
+ # materializes the whole object graph. ~10x faster than walking
100
+ # the columns in pure Ruby on JSON-shaped input; ~5x on YAML. Falls
101
+ # back to load_all_columns on the constructs the format cannot
102
+ # express (merge keys, timestamps — rare; the record walk handles
103
+ # them). Feature-detected: older libyeptris returns the walk.
104
+ def load_all_marshal(yaml, schema: :compat_11, mode: :first)
105
+ yaml = Yeptris.read_input(yaml)
106
+ yaml = yaml.to_s
107
+ out_p = ::FFI::MemoryPointer.new(:pointer)
108
+ olen_p = ::FFI::MemoryPointer.new(:size_t)
109
+ mode_id = mode == :all ? FFI::MARSHAL_ALL_DOCS : FFI::MARSHAL_FIRST_DOC
110
+ st = FFI.yeptris_marshal(
111
+ yaml, yaml.bytesize,
112
+ schema == :compat_11 ? FFI::SCHEMA_11_COMPAT : FFI::SCHEMA_12_CORE,
113
+ mode_id, out_p, olen_p
114
+ )
115
+ return nil if st == FFI::ERROR_UNSUPPORTED
116
+ raise ParseError, FFI.last_error_message if st != FFI::OK
117
+ buf = out_p.read_pointer
118
+ len = olen_p.read_uint64
119
+ bytes = buf.read_bytes(len)
120
+ bytes.force_encoding(Encoding::ASCII_8BIT)
121
+ ::Marshal.load(bytes)
122
+ ensure
123
+ FFI.yeptris_marshal_free(buf) if buf && !buf.null?
124
+ end
125
+
126
+ def load(yaml, schema: :compat_11)
127
+ docs = load_all(yaml, schema: schema)
128
+ docs.empty? ? nil : docs.first
129
+ end
130
+
131
+ # field offsets in the unpacked 7-tuple
132
+ KIND = 0
133
+ TAG = 1
134
+ IS_KEY = 2
135
+ B = 3
136
+ OFF = 4
137
+ LEN = 5
138
+ P64 = 6
139
+
140
+ def walk(flat, arena, compat = true)
141
+ docs = []
142
+ stack = []
143
+ anchors = {}
144
+ pending_key = []
145
+ pending_key_tag = []
146
+ pending_anchor = nil
147
+ merge_target = []
148
+
149
+ i = 0
150
+ n = flat.length
151
+ while i < n
152
+ kind = flat[i + KIND]
153
+ case kind
154
+ when V_STR
155
+ text = arena.byteslice(flat[i + OFF], flat[i + LEN])
156
+ # implicit-plain ':name' scans to a Symbol (Psych's
157
+ # ScalarScanner); quoted ':x' stays a String
158
+ v = if flat[i + B] == 1 && text.length > 18 &&
159
+ PSYCH_INT_SHAPE.match?(text)
160
+ text.delete(",_").to_i
161
+ elsif flat[i + B] == 1 && text.length > 1 &&
162
+ text.start_with?(":") && !text.start_with?("::")
163
+ text[1..].to_sym
164
+ else
165
+ text
166
+ end
167
+ if pending_anchor
168
+ anchors[pending_anchor] = v
169
+ pending_anchor = nil
170
+ end
171
+ slot(docs, stack, pending_key, pending_key_tag, v, merge_target, flat[i + IS_KEY], flat[i + TAG])
172
+ when V_INT
173
+ v = flat[i + P64]
174
+ if pending_anchor
175
+ anchors[pending_anchor] = v
176
+ pending_anchor = nil
177
+ end
178
+ slot(docs, stack, pending_key, pending_key_tag, v, merge_target, flat[i + IS_KEY], flat[i + TAG])
179
+ when V_FLOAT
180
+ text = arena.byteslice(flat[i + OFF], flat[i + LEN])
181
+ # Psych's float grammar requires the dot (or an inf/nan
182
+ # word, or sexagesimal ':') — exponent-only forms are
183
+ # Strings even when the compat tag says FLOAT
184
+ v = if !compat || text.include?(".") || text.include?(":") || text.start_with?(".")
185
+ [flat[i + P64]].pack("q<").unpack1("E")
186
+ else
187
+ text
188
+ end
189
+ if pending_anchor
190
+ anchors[pending_anchor] = v
191
+ pending_anchor = nil
192
+ end
193
+ slot(docs, stack, pending_key, pending_key_tag, v, merge_target, flat[i + IS_KEY], flat[i + TAG])
194
+ when V_BOOL
195
+ text = arena.byteslice(flat[i + OFF], flat[i + LEN])
196
+ # Psych quirk: single-char y/n stay Strings
197
+ v = text.length == 1 ? text : flat[i + B] == 1
198
+ if pending_anchor
199
+ anchors[pending_anchor] = v
200
+ pending_anchor = nil
201
+ end
202
+ slot(docs, stack, pending_key, pending_key_tag, v, merge_target, flat[i + IS_KEY], flat[i + TAG])
203
+ when V_NULL
204
+ # the pending anchor binds to the null (an anchored empty
205
+ # scalar) — without this it would leak onto the next value
206
+ if pending_anchor
207
+ anchors[pending_anchor] = nil
208
+ pending_anchor = nil
209
+ end
210
+ slot(docs, stack, pending_key, pending_key_tag, nil, merge_target, flat[i + IS_KEY], flat[i + TAG])
211
+ when V_TS
212
+ v = Materializer.parse_timestamp(arena.byteslice(flat[i + OFF], flat[i + LEN]))
213
+ if pending_anchor
214
+ anchors[pending_anchor] = v
215
+ pending_anchor = nil
216
+ end
217
+ slot(docs, stack, pending_key, pending_key_tag, v, merge_target, flat[i + IS_KEY], flat[i + TAG])
218
+ when MAP_OPEN
219
+ h = {}
220
+ if pending_anchor
221
+ anchors[pending_anchor] = h
222
+ pending_anchor = nil
223
+ end
224
+ merge_target.push(slot(docs, stack, pending_key, pending_key_tag, h, merge_target,
225
+ flat[i + IS_KEY], flat[i + TAG]))
226
+ stack.push(h)
227
+ pending_key.push(nil)
228
+ pending_key_tag.push(nil)
229
+ when SEQ_OPEN
230
+ a = []
231
+ if pending_anchor
232
+ anchors[pending_anchor] = a
233
+ pending_anchor = nil
234
+ end
235
+ merge_target.push(slot(docs, stack, pending_key, pending_key_tag, a, merge_target,
236
+ flat[i + IS_KEY], flat[i + TAG]))
237
+ stack.push(a)
238
+ pending_key.push(nil)
239
+ pending_key_tag.push(nil)
240
+ when CLOSE
241
+ closed = stack.pop
242
+ pending_key.pop
243
+ pending_key_tag.pop
244
+ target = merge_target.pop
245
+ Materializer.merge_into(target, closed) if target
246
+ when DOC
247
+ docs.push(nil)
248
+ when ALIAS
249
+ v = anchors[arena.byteslice(flat[i + OFF], flat[i + LEN])]
250
+ slot(docs, stack, pending_key, pending_key_tag, v, merge_target, flat[i + IS_KEY], flat[i + TAG])
251
+ when ANCHOR
252
+ pending_anchor = arena.byteslice(flat[i + OFF], flat[i + LEN])
253
+ end
254
+ i += FIELDS
255
+ end
256
+ docs
257
+ end
258
+
259
+ # The columnar twin of walk above (lockstep: same semantics, same
260
+ # order; fields come from tight per-kind arrays, stride 1).
261
+ def walk_columns(kinds, tags, ikeys, bools, offs, lens, pays, arena, compat = true)
262
+ docs = []
263
+ stack = []
264
+ anchors = {}
265
+ pending_key = []
266
+ pending_key_tag = []
267
+ pending_anchor = nil
268
+ merge_target = []
269
+
270
+ i = 0
271
+ n = kinds.length
272
+ while i < n
273
+ # the dominant shape: a str:str pair inside one map — place
274
+ # both directly, same conversions as the STR arm (the ':sym'
275
+ # scan rides is_key's text; anchors/merges fall through)
276
+ if kinds[i] == V_STR && ikeys[i] == 1 && i + 1 < n &&
277
+ kinds[i + 1] == V_STR && ikeys[i + 1] == 0 &&
278
+ pending_anchor.nil? && !stack.empty? && stack.last.is_a?(Hash)
279
+ kt = arena.byteslice(offs[i], lens[i])
280
+ if kt != "<<"
281
+ key = if bools[i] == 1 && kt.length > 1 && kt.start_with?(":") &&
282
+ !kt.start_with?("::")
283
+ kt[1..].to_sym
284
+ else
285
+ kt
286
+ end
287
+ vt = arena.byteslice(offs[i + 1], lens[i + 1])
288
+ stack.last[key] =
289
+ if bools[i + 1] == 1 && vt.length > 1 && vt.start_with?(":") &&
290
+ !vt.start_with?("::")
291
+ vt[1..].to_sym
292
+ else
293
+ vt
294
+ end
295
+ i += 2
296
+ next
297
+ end
298
+ end
299
+ case kinds[i]
300
+ when V_STR
301
+ text = arena.byteslice(offs[i], lens[i])
302
+ v = if bools[i] == 1 && text.length > 18 &&
303
+ PSYCH_INT_SHAPE.match?(text)
304
+ text.delete(",_").to_i
305
+ elsif bools[i] == 1 && text.length > 1 &&
306
+ text.start_with?(":") && !text.start_with?("::")
307
+ text[1..].to_sym
308
+ else
309
+ text
310
+ end
311
+ if pending_anchor
312
+ anchors[pending_anchor] = v
313
+ pending_anchor = nil
314
+ end
315
+ # inline the dominant placement (Hash parent, value slot,
316
+ # non-merge key): a slot() call per pair was measurable
317
+ if !stack.empty?
318
+ parent = stack.last
319
+ if parent.is_a?(Hash) && ikeys[i] == 0 && (k = pending_key[-1]) && k != "<<"
320
+ parent[k] = v
321
+ pending_key[-1] = nil
322
+ else
323
+ slot(docs, stack, pending_key, pending_key_tag, v, merge_target, ikeys[i], tags[i])
324
+ end
325
+ else
326
+ docs[-1] = v
327
+ end
328
+ when V_INT
329
+ v = pays[i]
330
+ if pending_anchor
331
+ anchors[pending_anchor] = v
332
+ pending_anchor = nil
333
+ end
334
+ if !stack.empty?
335
+ parent = stack.last
336
+ if parent.is_a?(Hash) && ikeys[i] == 0 && (k = pending_key[-1]) && k != "<<"
337
+ parent[k] = v
338
+ pending_key[-1] = nil
339
+ else
340
+ slot(docs, stack, pending_key, pending_key_tag, v, merge_target, ikeys[i], tags[i])
341
+ end
342
+ else
343
+ docs[-1] = v
344
+ end
345
+ when V_FLOAT
346
+ text = arena.byteslice(offs[i], lens[i])
347
+ v = if !compat || text.include?(".") || text.include?(":") || text.start_with?(".")
348
+ [pays[i]].pack("q<").unpack1("E")
349
+ else
350
+ text
351
+ end
352
+ if pending_anchor
353
+ anchors[pending_anchor] = v
354
+ pending_anchor = nil
355
+ end
356
+ slot(docs, stack, pending_key, pending_key_tag, v, merge_target, ikeys[i], tags[i])
357
+ when V_BOOL
358
+ text = arena.byteslice(offs[i], lens[i])
359
+ v = text.length == 1 ? text : bools[i] == 1
360
+ if pending_anchor
361
+ anchors[pending_anchor] = v
362
+ pending_anchor = nil
363
+ end
364
+ slot(docs, stack, pending_key, pending_key_tag, v, merge_target, ikeys[i], tags[i])
365
+ when V_NULL
366
+ if pending_anchor
367
+ anchors[pending_anchor] = nil
368
+ pending_anchor = nil
369
+ end
370
+ slot(docs, stack, pending_key, pending_key_tag, nil, merge_target, ikeys[i], tags[i])
371
+ when V_TS
372
+ v = Materializer.parse_timestamp(arena.byteslice(offs[i], lens[i]))
373
+ if pending_anchor
374
+ anchors[pending_anchor] = v
375
+ pending_anchor = nil
376
+ end
377
+ slot(docs, stack, pending_key, pending_key_tag, v, merge_target, ikeys[i], tags[i])
378
+ when MAP_OPEN
379
+ h = {}
380
+ if pending_anchor
381
+ anchors[pending_anchor] = h
382
+ pending_anchor = nil
383
+ end
384
+ merge_target.push(slot(docs, stack, pending_key, pending_key_tag, h, merge_target,
385
+ ikeys[i], tags[i]))
386
+ stack.push(h)
387
+ pending_key.push(nil)
388
+ pending_key_tag.push(nil)
389
+ when SEQ_OPEN
390
+ a = []
391
+ if pending_anchor
392
+ anchors[pending_anchor] = a
393
+ pending_anchor = nil
394
+ end
395
+ merge_target.push(slot(docs, stack, pending_key, pending_key_tag, a, merge_target,
396
+ ikeys[i], tags[i]))
397
+ stack.push(a)
398
+ pending_key.push(nil)
399
+ pending_key_tag.push(nil)
400
+ when CLOSE
401
+ closed = stack.pop
402
+ pending_key.pop
403
+ pending_key_tag.pop
404
+ target = merge_target.pop
405
+ Materializer.merge_into(target, closed) if target
406
+ when DOC
407
+ docs.push(nil)
408
+ when ALIAS
409
+ v = anchors[arena.byteslice(offs[i], lens[i])]
410
+ slot(docs, stack, pending_key, pending_key_tag, v, merge_target, ikeys[i], tags[i])
411
+ when ANCHOR
412
+ pending_anchor = arena.byteslice(offs[i], lens[i])
413
+ end
414
+ i += 1
415
+ end
416
+ docs
417
+ end
418
+
419
+ # Places a completed value: document root, sequence entry, or a
420
+ # map's key (is_key) / value (completing the pending pair).
421
+ # Returns the merge TARGET when the value is a still-empty
422
+ # container placed under a '<<' key — the open-site caller pushes
423
+ # it so the matching CLOSE merges into it (an inline map's
424
+ # contents arrive after its open); scalar merges apply now.
425
+ def slot(docs, stack, pending_key, pending_key_tag, v, _merge_target, is_key, tag)
426
+ if stack.empty?
427
+ docs[-1] = v
428
+ return nil
429
+ end
430
+ parent = stack.last
431
+ if parent.is_a?(Array)
432
+ parent.push(v)
433
+ return nil
434
+ end
435
+ if is_key == 1
436
+ pending_key[-1] = v
437
+ pending_key_tag[-1] = tag
438
+ return nil
439
+ end
440
+ key = pending_key[-1]
441
+ pending_key[-1] = nil
442
+ if key == "<<" && pending_key_tag[-1] == 9 # TAG_MERGE
443
+ if (v.is_a?(Hash) || v.is_a?(Array)) && v.empty?
444
+ parent # deferred: contents arrive after the open
445
+ else
446
+ Materializer.merge_into(parent, v)
447
+ nil
448
+ end
449
+ else
450
+ parent[key] = v
451
+ nil
452
+ end
453
+ end
454
+ end
455
+ end