yeptris 0.6.18.2-arm-linux-eabihf

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.
Files changed (142) hide show
  1. checksums.yaml +7 -0
  2. data/README.adoc +141 -0
  3. data/ext/build_windows_native.rb +41 -0
  4. data/ext/libyeptris/extconf.rb +57 -0
  5. data/ext/yeptris_native/cbor_ruby.c +193 -0
  6. data/ext/yeptris_native/extconf.rb +47 -0
  7. data/ext/yeptris_native/json_ruby.c +354 -0
  8. data/ext/yeptris_native/yeptris_native.c +497 -0
  9. data/lib/yeptris/cbor.rb +130 -0
  10. data/lib/yeptris/document.rb +255 -0
  11. data/lib/yeptris/ffi.rb +426 -0
  12. data/lib/yeptris/json/descriptor.rb +266 -0
  13. data/lib/yeptris/json.rb +394 -0
  14. data/lib/yeptris/materializer.rb +354 -0
  15. data/lib/yeptris/native-3.4.so +0 -0
  16. data/lib/yeptris/node.rb +453 -0
  17. data/lib/yeptris/psych/class_loader.rb +24 -0
  18. data/lib/yeptris/psych/coder_shim.rb +55 -0
  19. data/lib/yeptris/psych/drop_in.rb +63 -0
  20. data/lib/yeptris/psych/encodable.rb +14 -0
  21. data/lib/yeptris/psych/handler.rb +90 -0
  22. data/lib/yeptris/psych/parser.rb +105 -0
  23. data/lib/yeptris/psych/scalar_scanner.rb +149 -0
  24. data/lib/yeptris/psych/visitors.rb +545 -0
  25. data/lib/yeptris/psych.rb +622 -0
  26. data/lib/yeptris/schema.rb +253 -0
  27. data/lib/yeptris/valueml.rb +455 -0
  28. data/lib/yeptris/yaml/descriptor.rb +56 -0
  29. data/lib/yeptris/yaml.rb +422 -0
  30. data/lib/yeptris.rb +113 -0
  31. data/libyeptris.so +0 -0
  32. data/vendor/libyeptris/CMakeLists.txt +254 -0
  33. data/vendor/libyeptris/cmake/yeptris-config.cmake.in +5 -0
  34. data/vendor/libyeptris/cmake/yeptris.pc.in +11 -0
  35. data/vendor/libyeptris/src/CMakeLists.txt +167 -0
  36. data/vendor/libyeptris/src/include/yeptris/api.h +32 -0
  37. data/vendor/libyeptris/src/include/yeptris/cbor.h +101 -0
  38. data/vendor/libyeptris/src/include/yeptris/dom.h +190 -0
  39. data/vendor/libyeptris/src/include/yeptris/emit.h +81 -0
  40. data/vendor/libyeptris/src/include/yeptris/error.h +41 -0
  41. data/vendor/libyeptris/src/include/yeptris/events.h +157 -0
  42. data/vendor/libyeptris/src/include/yeptris/json.h +45 -0
  43. data/vendor/libyeptris/src/include/yeptris/json.hpp +258 -0
  44. data/vendor/libyeptris/src/include/yeptris/jsonc_compat.h +115 -0
  45. data/vendor/libyeptris/src/include/yeptris/marshal.h +52 -0
  46. data/vendor/libyeptris/src/include/yeptris/parse.h +48 -0
  47. data/vendor/libyeptris/src/include/yeptris/plan.h +100 -0
  48. data/vendor/libyeptris/src/include/yeptris/resolve.h +71 -0
  49. data/vendor/libyeptris/src/include/yeptris/schema.h +121 -0
  50. data/vendor/libyeptris/src/include/yeptris/tape.h +123 -0
  51. data/vendor/libyeptris/src/include/yeptris/types.h +38 -0
  52. data/vendor/libyeptris/src/include/yeptris/values.h +102 -0
  53. data/vendor/libyeptris/src/include/yeptris/version.h.in +29 -0
  54. data/vendor/libyeptris/src/include/yeptris/visit.h +77 -0
  55. data/vendor/libyeptris/src/include/yeptris/yajl_compat.h +135 -0
  56. data/vendor/libyeptris/src/include/yeptris.h +23 -0
  57. data/vendor/libyeptris/src/yeptris/build.c +376 -0
  58. data/vendor/libyeptris/src/yeptris/cbor/cbor.h +25 -0
  59. data/vendor/libyeptris/src/yeptris/cbor/decode.c +981 -0
  60. data/vendor/libyeptris/src/yeptris/cbor/encode.c +835 -0
  61. data/vendor/libyeptris/src/yeptris/cbor/sink.h +58 -0
  62. data/vendor/libyeptris/src/yeptris/common/chartype.c +41 -0
  63. data/vendor/libyeptris/src/yeptris/common/chartype.h +77 -0
  64. data/vendor/libyeptris/src/yeptris/common/cpu.c +81 -0
  65. data/vendor/libyeptris/src/yeptris/common/cpu.h +40 -0
  66. data/vendor/libyeptris/src/yeptris/common/error.c +65 -0
  67. data/vendor/libyeptris/src/yeptris/common/error.h +79 -0
  68. data/vendor/libyeptris/src/yeptris/common/mutex.h +45 -0
  69. data/vendor/libyeptris/src/yeptris/common/nametab.c +275 -0
  70. data/vendor/libyeptris/src/yeptris/common/nametab.h +68 -0
  71. data/vendor/libyeptris/src/yeptris/common/port.h +74 -0
  72. data/vendor/libyeptris/src/yeptris/common/simd_text.c +49 -0
  73. data/vendor/libyeptris/src/yeptris/common/simd_text.h +295 -0
  74. data/vendor/libyeptris/src/yeptris/common/simd_text_avx2.c +534 -0
  75. data/vendor/libyeptris/src/yeptris/common/simd_text_neon.c +662 -0
  76. data/vendor/libyeptris/src/yeptris/common/simd_text_scalar.c +298 -0
  77. data/vendor/libyeptris/src/yeptris/common/string_view.c +34 -0
  78. data/vendor/libyeptris/src/yeptris/common/string_view.h +93 -0
  79. data/vendor/libyeptris/src/yeptris/doc.h +56 -0
  80. data/vendor/libyeptris/src/yeptris/dom/dom.c +1192 -0
  81. data/vendor/libyeptris/src/yeptris/dom/dom.h +312 -0
  82. data/vendor/libyeptris/src/yeptris/dom/hpool.c +107 -0
  83. data/vendor/libyeptris/src/yeptris/dom/mapindex.c +173 -0
  84. data/vendor/libyeptris/src/yeptris/dom/mapindex.h +61 -0
  85. data/vendor/libyeptris/src/yeptris/dom/mutate.c +431 -0
  86. data/vendor/libyeptris/src/yeptris/emit/emit.c +434 -0
  87. data/vendor/libyeptris/src/yeptris/emit/float/api.h +38 -0
  88. data/vendor/libyeptris/src/yeptris/emit/float/dragon.c +405 -0
  89. data/vendor/libyeptris/src/yeptris/emit/float/floatint.h +231 -0
  90. data/vendor/libyeptris/src/yeptris/emit/float/print.c +396 -0
  91. data/vendor/libyeptris/src/yeptris/emit/style.c +81 -0
  92. data/vendor/libyeptris/src/yeptris/emit/style.h +24 -0
  93. data/vendor/libyeptris/src/yeptris/emit/writer.c +816 -0
  94. data/vendor/libyeptris/src/yeptris/emit/writer.h +76 -0
  95. data/vendor/libyeptris/src/yeptris/encoding/bom.c +31 -0
  96. data/vendor/libyeptris/src/yeptris/encoding/encoding.h +63 -0
  97. data/vendor/libyeptris/src/yeptris/encoding/transcode.c +148 -0
  98. data/vendor/libyeptris/src/yeptris/encoding/utf8_validate.c +178 -0
  99. data/vendor/libyeptris/src/yeptris/events/capture.c +150 -0
  100. data/vendor/libyeptris/src/yeptris/events/capture.h +39 -0
  101. data/vendor/libyeptris/src/yeptris/events/iterparse.c +208 -0
  102. data/vendor/libyeptris/src/yeptris/events/pull.c +97 -0
  103. data/vendor/libyeptris/src/yeptris/events/push.c +104 -0
  104. data/vendor/libyeptris/src/yeptris/events/recorder.c +107 -0
  105. data/vendor/libyeptris/src/yeptris/events/values.c +527 -0
  106. data/vendor/libyeptris/src/yeptris/events/values_priv.h +51 -0
  107. data/vendor/libyeptris/src/yeptris/events/yaml_compat.c +86 -0
  108. data/vendor/libyeptris/src/yeptris/events/yaml_compat.h +90 -0
  109. data/vendor/libyeptris/src/yeptris/jsonapi/jsonc_compat.c +687 -0
  110. data/vendor/libyeptris/src/yeptris/jsonapi/yajl_compat.c +728 -0
  111. data/vendor/libyeptris/src/yeptris/marshal.c +782 -0
  112. data/vendor/libyeptris/src/yeptris/memory/allocator.c +20 -0
  113. data/vendor/libyeptris/src/yeptris/memory/allocator.h +43 -0
  114. data/vendor/libyeptris/src/yeptris/memory/arena.c +134 -0
  115. data/vendor/libyeptris/src/yeptris/memory/arena.h +52 -0
  116. data/vendor/libyeptris/src/yeptris/memory/pool.c +122 -0
  117. data/vendor/libyeptris/src/yeptris/memory/pool.h +36 -0
  118. data/vendor/libyeptris/src/yeptris/parse/engine.c +3698 -0
  119. data/vendor/libyeptris/src/yeptris/parse/engine.h +65 -0
  120. data/vendor/libyeptris/src/yeptris/parse/events.h +124 -0
  121. data/vendor/libyeptris/src/yeptris/parse/numbers.c +349 -0
  122. data/vendor/libyeptris/src/yeptris/parse/numbers.h +33 -0
  123. data/vendor/libyeptris/src/yeptris/parse/scalars.c +416 -0
  124. data/vendor/libyeptris/src/yeptris/parse/scalars.h +75 -0
  125. data/vendor/libyeptris/src/yeptris/parse.c +737 -0
  126. data/vendor/libyeptris/src/yeptris/plan.c +824 -0
  127. data/vendor/libyeptris/src/yeptris/resolve/compat11.c +334 -0
  128. data/vendor/libyeptris/src/yeptris/resolve/core12.c +143 -0
  129. data/vendor/libyeptris/src/yeptris/resolve/resolver.h +56 -0
  130. data/vendor/libyeptris/src/yeptris/resolve/tags.c +58 -0
  131. data/vendor/libyeptris/src/yeptris/scan/json.c +792 -0
  132. data/vendor/libyeptris/src/yeptris/scan/json.h +109 -0
  133. data/vendor/libyeptris/src/yeptris/scan/scan.c +463 -0
  134. data/vendor/libyeptris/src/yeptris/scan/scan.h +150 -0
  135. data/vendor/libyeptris/src/yeptris/schema.c +308 -0
  136. data/vendor/libyeptris/src/yeptris/tape.c +1308 -0
  137. data/vendor/libyeptris/src/yeptris/tape_in.h +19 -0
  138. data/vendor/libyeptris/src/yeptris/version.c +7 -0
  139. data/vendor/libyeptris/src/yeptris/visit/dom_visit.c +115 -0
  140. data/vendor/libyeptris/src/yeptris/visit/json_visit.c +293 -0
  141. data/vendor/libyeptris/src/yeptris/visit/yaml_visit.c +127 -0
  142. metadata +199 -0
@@ -0,0 +1,253 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yeptris
4
+ # The schema-descriptor materialization (issue #238; the C ABI lives
5
+ # in yeptris/schema.h and docs/schema-abi.md). One native pass fills
6
+ # typed columns against a caller-compiled descriptor; the
7
+ # intermediate generic document never exists.
8
+ module Schema
9
+ module_function
10
+
11
+ KIND = { scalar: 0, sequence: 1, mapping: 2, callback: 3 }.freeze
12
+ TYPE = { str: 0, int: 1, float: 2, bool: 3, null: 4, any: 5 }.freeze
13
+ REQUIRED = 1 << 0
14
+ FIRST_WINS = 1 << 1 # duplicates within one mapping: first kept
15
+
16
+ # The :any verdict (#238's headroom): the resolver's own typing,
17
+ # one 24-byte record per value — Integer/Float/true/false/nil, a
18
+ # zero-copy String for str/timestamp spans
19
+ ANY_SIZE = 24 # yeptris_value.h's YeptrisValue: kind@0 tag_id@1
20
+ # is_key@2 b@3 off@4 len@8 p@16 — the C layout, ABI-pinned
21
+ ANY_KIND = { 1 => :null, 2 => :bool, 3 => :int, 4 => :float,
22
+ 5 => :str, 6 => :timestamp }.freeze
23
+
24
+ class Error < Yeptris::Error; end
25
+
26
+ # #238's executed headroom (ST_ANY/FIRST_WINS) rides the C tag
27
+ # AFTER v0.6.12; probe the engine rather than trusting versions
28
+ class << self
29
+ def headroom_supported?
30
+ return @headroom_supported unless @headroom_supported.nil?
31
+ @headroom_supported = begin
32
+ load("x: 1\n",
33
+ desc: [{ kind: :mapping, child_index: 1, child_count: 1 },
34
+ { wire_name: "x", kind: :scalar, type: :any }]) && true
35
+ rescue Error, Yeptris::ParseError, Yeptris::Error
36
+ false
37
+ end
38
+ end
39
+ end
40
+ class RequiredMissing < Error; end
41
+
42
+ ELEMENT_SIZE = { 0 => 16, 1 => 8, 2 => 8, 3 => 1, 4 => 1, 5 => ANY_SIZE }.freeze # by TYPE value
43
+
44
+ # A materialized span: zero-copy off/len into the source plus the
45
+ # node's byte offset (the CALLBACK escape hatch).
46
+ Span = Struct.new(:offset, :length, :node_offset) do
47
+ def bytes_from(source)
48
+ source.byteslice(offset, length)
49
+ end
50
+ end
51
+
52
+ # desc: a flat array of node hashes, the C ABI 1:1 —
53
+ # { wire_name:, kind:, type:, required:, child_index:, child_count: }
54
+ # (kind/type symbolic; wire_name nil = the root). Returns one typed
55
+ # Ruby array per node: Integer/Float/true/false/nil/String for
56
+ # scalars and sequence elements (document order), Span for
57
+ # callbacks.
58
+ def load(source, schema: :core_12, desc:, capacity: 64)
59
+ source = Yeptris.read_input(source).to_s
60
+ schema_id = schema == :compat_11 ? FFI::SCHEMA_11_COMPAT : FFI::SCHEMA_12_CORE
61
+ plans = compile(desc)
62
+ loop do
63
+ result = run(source, schema_id, plans, capacity)
64
+ return result unless result == :grow
65
+ capacity *= 2
66
+ end
67
+ end
68
+
69
+ # #184 (lutaml-model KV path): zip Schema columns into record
70
+ # hashes ready for Serializable.instantiate. Mapping root → one
71
+ # hash; sequence-of-mappings → one hash per element; nested
72
+ # mappings → nested hashes. Returns Array<Hash> always. when_attribute
73
+ # / polymorphic stay out of scope (interpretive fallback).
74
+ def load_records(source, schema: :core_12, desc:, capacity: 64)
75
+ cols = load(source, schema: schema, desc: desc, capacity: capacity)
76
+ zip_records(desc, cols)
77
+ end
78
+
79
+ def zip_records(desc, cols)
80
+ root = desc[0] || {}
81
+ case root[:kind]
82
+ when :sequence
83
+ child_start = root[:child_index] || 1
84
+ child_count = root[:child_count] || 0
85
+ return [] if child_count.zero?
86
+
87
+ child = desc[child_start]
88
+ if child && child[:kind] == :mapping
89
+ field_start = child[:child_index] || (child_start + 1)
90
+ field_count = child[:child_count] || 0
91
+ fields = desc[field_start, field_count] || []
92
+ field_cols = cols[field_start, field_count] || []
93
+ nrows = field_cols.map(&:length).max || 0
94
+ Array.new(nrows) do |r|
95
+ h = {}
96
+ fields.each_with_index do |f, i|
97
+ next unless f[:wire_name]
98
+ col = field_cols[i] || []
99
+ # ABSENT keys are OMITTED, not nil-filled (lutaml-model's
100
+ # load-bearing edge: absent seeds defaults +
101
+ # using_default?; explicit nil is a present value with
102
+ # different render semantics). The C columns fill in
103
+ # document order — col.length > r means present.
104
+ next if col.length <= r
105
+
106
+ h[f[:wire_name].to_sym] = materialize_field(f, desc, cols, col[r])
107
+ end
108
+ h
109
+ end
110
+ else
111
+ (cols[child_start] || []).map { |v| { value: v } }
112
+ end
113
+ when :mapping
114
+ field_start = root[:child_index] || 1
115
+ field_count = root[:child_count] || 0
116
+ fields = desc[field_start, field_count] || []
117
+ field_cols = cols[field_start, field_count] || []
118
+ h = {}
119
+ fields.each_with_index do |f, i|
120
+ next unless f[:wire_name]
121
+ col = field_cols[i] || []
122
+ next if col.empty? # absent: omitted, not nil-filled (#184)
123
+
124
+ h[f[:wire_name].to_sym] = materialize_field(f, desc, cols, col[0])
125
+ end
126
+ [h]
127
+ else
128
+ [{ value: cols[0]&.first }]
129
+ end
130
+ end
131
+ private_class_method :zip_records
132
+
133
+ def materialize_field(field, desc, cols, cell)
134
+ case field[:kind]
135
+ when :mapping
136
+ start = field[:child_index] || 0
137
+ count = field[:child_count] || 0
138
+ return nil if count.zero? || cell.nil?
139
+
140
+ nested = {}
141
+ desc[start, count]&.each_with_index do |nf, i|
142
+ next unless nf[:wire_name]
143
+ ncol = cols[start + i] || []
144
+ nested[nf[:wire_name].to_sym] = ncol.is_a?(Array) ? (ncol[0] rescue cell) : cell
145
+ end
146
+ nested
147
+ when :sequence
148
+ cell.is_a?(Array) ? cell : Array(cell)
149
+ else
150
+ cell
151
+ end
152
+ end
153
+ private_class_method :materialize_field
154
+
155
+ def compile(desc)
156
+ desc.map do |n|
157
+ { kind: KIND.fetch(n.fetch(:kind)),
158
+ type: TYPE.fetch(n[:type] || :str),
159
+ wire: n[:wire_name],
160
+ flags: (n[:required] ? REQUIRED : 0) | (n[:first_wins] ? FIRST_WINS : 0),
161
+ child_index: n[:child_index] || 0,
162
+ child_count: n[:child_count] || 0 }
163
+ end
164
+ end
165
+ private_class_method :compile
166
+
167
+ def run(source, schema_id, plans, capacity) # rubocop:disable Metrics/MethodLength
168
+ n = plans.length
169
+ desc_buf = ::FFI::MemoryPointer.new(FFI::DescNode, n, true)
170
+ wires = []
171
+ plans.each_with_index do |p, i|
172
+ node = FFI::DescNode.new(desc_buf + i * FFI::DescNode.size)
173
+ if p[:wire]
174
+ wires << (wire = ::FFI::MemoryPointer.from_string(p[:wire]))
175
+ node[:wire_name] = wire
176
+ end
177
+ node[:kind] = p[:kind]
178
+ node[:type_tag] = p[:type]
179
+ node[:flags] = p[:flags]
180
+ node[:child_index] = p[:child_index]
181
+ node[:child_count] = p[:child_count]
182
+ node[:reserved] = 0
183
+ end
184
+ cols_buf = ::FFI::MemoryPointer.new(FFI::SchemaColumn, n, true)
185
+ data_ptrs = []
186
+ plans.each_with_index do |p, i|
187
+ col = FFI::SchemaColumn.new(cols_buf + i * FFI::SchemaColumn.size)
188
+ elem = ELEMENT_SIZE.fetch(p[:type], 16)
189
+ data = ::FFI::MemoryPointer.new(elem, capacity, true)
190
+ data_ptrs << data
191
+ col[:data] = data
192
+ col[:capacity] = capacity
193
+ col[:count] = 0
194
+ end
195
+
196
+ src = ::FFI::MemoryPointer.from_string(source) # NUL-safe copy for the C side
197
+ st = FFI.yeptris_schema_load(src, source.bytesize, schema_id, desc_buf, n,
198
+ FFI::DESC_ABI, cols_buf)
199
+ case st
200
+ when FFI::OK
201
+ plans.each_with_index.map { |p, i| drain(cols_buf, i, p, source) }
202
+ when FFI::ERROR_MEMORY
203
+ :grow
204
+ when FFI::SCHEMA_ERROR
205
+ msg = FFI.last_error_message
206
+ raise RequiredMissing, msg
207
+ else
208
+ raise Yeptris::ParseError, FFI.last_error_message
209
+ end
210
+ end
211
+ private_class_method :run
212
+
213
+ def drain(cols_buf, i, plan, source)
214
+ col = FFI::SchemaColumn.new(cols_buf + i * FFI::SchemaColumn.size)
215
+ count = col[:count]
216
+ data = col[:data]
217
+ case plan[:kind]
218
+ when KIND[:callback]
219
+ Array.new(count) do |k|
220
+ off = data.get_uint32(k * 16)
221
+ len = data.get_uint32(k * 16 + 4)
222
+ node_off = data.get_uint32(k * 16 + 8)
223
+ Span.new(off, len, node_off)
224
+ end
225
+ else
226
+ type = plan[:type]
227
+ Array.new(count) do |k|
228
+ case type
229
+ when TYPE[:int] then data.get_int64(k * 8)
230
+ when TYPE[:float] then data.get_double(k * 8)
231
+ when TYPE[:bool] then data.get_uchar(k) == 1
232
+ when TYPE[:null] then nil
233
+ when TYPE[:any]
234
+ rec = k * ANY_SIZE
235
+ case ANY_KIND[data.get_uint8(rec)]
236
+ when :int then data.get_int64(rec + 16)
237
+ when :float then data.get_double(rec + 16)
238
+ when :bool then data.get_uint8(rec + 3) == 1
239
+ when :null then nil
240
+ else # str/timestamp: the zero-copy span
241
+ source.byteslice(data.get_uint32(rec + 4), data.get_uint32(rec + 8))
242
+ end
243
+ else
244
+ off = data.get_uint32(k * 16)
245
+ len = data.get_uint32(k * 16 + 4)
246
+ source.byteslice(off, len)
247
+ end
248
+ end
249
+ end
250
+ end
251
+ private_class_method :drain
252
+ end
253
+ end
@@ -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(:size_t)
42
+ arena_p = ::FFI::MemoryPointer.new(:pointer)
43
+ alen_p = ::FFI::MemoryPointer.new(:size_t)
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 = Yeptris::FFI.read_c_size_t(count_p)
55
+ flat = vals.read_bytes(count * VALUE_SIZE).unpack(UNPACK * count)
56
+ arena_bytes = arena.read_bytes(Yeptris::FFI.read_c_size_t(alen_p))
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 = Yeptris::FFI.read_c_size_t(olen_p)
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