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,266 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The Descriptor plan walk (yeptris#293 slice two, over the C plan
4
+ # ABI of libyeptris v0.6.3): compile a row shape once, then apply it
5
+ # to a JSON document in ONE native pass producing typed COLUMNS —
6
+ # the engine never materializes the intermediate Ruby Hash/Array
7
+ # tree (lutaml-model's hydration reads columns, not trees).
8
+ #
9
+ # descriptor = Yeptris::JSON::Descriptor.build(
10
+ # kind: :seq, # or :map with path: — the rows container
11
+ # children: [
12
+ # { name: "id", kind: :int },
13
+ # { name: "name", kind: :str },
14
+ # ])
15
+ # result = descriptor.walk(json)
16
+ # result.column("id") # => [1, 2, ...] (nil where null/missing)
17
+ # result.to_a # => [{ "id" => 1, "name" => "x" }, ...]
18
+ #
19
+ # Vocabulary alignment with Leptris::XML::Descriptor (the shape
20
+ # vocabulary lutaml-model shares across engines): :collection at the
21
+ # root is the rows container (the seq form), :scalar is the string
22
+ # leaf. Slice one covers rows of scalar leaves — nested plans, the
23
+ # YAML leg, and partial descriptors ride the board item
24
+ # (TODO.restructure/87).
25
+ class Yeptris::JSON::Descriptor
26
+ class Error < ::Yeptris::JSON::Error; end
27
+
28
+ # spec leaf kinds → the C column kind codes (plan.h)
29
+ LEAF_KINDS = {
30
+ int: 0,
31
+ float: 1,
32
+ str: 2,
33
+ bool: 3,
34
+ scalar: 2, # leptris's string-value row kind
35
+ }.freeze
36
+ private_constant :LEAF_KINDS
37
+
38
+ # root forms: the rows container's shape (:collection is leptris's
39
+ # name for a repeated-rows container)
40
+ ROOT_KINDS = %i[seq map collection].freeze
41
+ private_constant :ROOT_KINDS
42
+
43
+ LEAF_SPEC_NAMES = %w[int float str bool].freeze
44
+ private_constant :LEAF_SPEC_NAMES
45
+
46
+ class Handle < ::FFI::AutoPointer
47
+ def self.release(ptr)
48
+ ::Yeptris::FFI.yeptris_plan_free(ptr)
49
+ end
50
+ end
51
+
52
+ # +kind+: :seq (rows are the root array) or :map (rows live under
53
+ # +path:+'s value). +path+: a String, or an Array of Strings for a
54
+ # segmented (nested) path. +children+: the leaf columns; each row
55
+ # is a mapping, every child names one typed column.
56
+ def self.build(kind:, path: nil, children:)
57
+ handle, names = compile_handle(kind, path, children)
58
+ new(handle, names)
59
+ end
60
+
61
+ # The compile half of .build (shared with the YAML subclass's
62
+ # build — ONE owner of the spec grammar). Returns the owned handle
63
+ # and the leaf names.
64
+ def self.compile_handle(kind, path, children)
65
+ unless ROOT_KINDS.include?(kind)
66
+ raise ArgumentError, "kind must be one of #{ROOT_KINDS.inspect}, got #{kind.inspect}"
67
+ end
68
+ unless children.is_a?(::Array) && !children.empty?
69
+ raise ArgumentError, "children must be a non-empty Array"
70
+ end
71
+
72
+ leaves = children.map do |child|
73
+ name = child[:name]
74
+ code = LEAF_KINDS[child[:kind]]
75
+ if code.nil?
76
+ raise ArgumentError,
77
+ "leaf kind must be one of #{LEAF_KINDS.keys.inspect}, got #{child[:kind].inspect}"
78
+ end
79
+ unless name.is_a?(::String) && !name.empty?
80
+ raise ArgumentError, "leaf name must be a non-empty String, got #{name.inspect}"
81
+ end
82
+
83
+ { "name" => name, "kind" => LEAF_SPEC_NAMES[code] }
84
+ end
85
+
86
+ case path
87
+ when nil then nil
88
+ when ::String then spec_path = path
89
+ when ::Array
90
+ unless path.all? { |seg| seg.is_a?(::String) && !seg.empty? }
91
+ raise ArgumentError, "path segments must be non-empty Strings"
92
+ end
93
+ spec_path = path
94
+ else
95
+ raise ArgumentError, "path must be a String or an Array of Strings"
96
+ end
97
+
98
+ spec = { "kind" => kind == :map ? "map" : "seq", "children" => leaves }
99
+ spec["path"] = spec_path unless spec_path.nil?
100
+
101
+ # the spec is strict JSON (the C compiler parses it through the
102
+ # strict JSON DOM); compile once, the engine owns the copy
103
+ spec_json = ::JSON.generate(spec)
104
+ st = ::FFI::MemoryPointer.new(:int)
105
+ raw = ::Yeptris::FFI.yeptris_plan_compile(spec_json, spec_json.bytesize, st)
106
+ if raw.null?
107
+ raise Error, "plan compile failed (status=#{st.read_int}): #{spec_json}"
108
+ end
109
+
110
+ [Handle.new(raw), leaves.map { |leaf| leaf["name"] }]
111
+ end
112
+ private_class_method :compile_handle
113
+
114
+ attr_reader :names # the planned leaf names, in spec order
115
+
116
+ # @api private — handles come from .build
117
+ def initialize(handle, names)
118
+ @handle = handle
119
+ @names = names.dup.freeze
120
+ end
121
+
122
+ # Applies the plan to +json+ (String or IO): parse the tape, one C
123
+ # plan pass, bulk-read the typed columns. The returned PlanResult
124
+ # is standalone Ruby (nothing borrowed from the document).
125
+ def walk(json)
126
+ src = ::Yeptris.read_input(json).to_s
127
+ tape = ::Yeptris::FFI::JsonTape.new
128
+ rc = ::Yeptris::FFI.yeptris_parse_json_tape(src, src.bytesize, tape)
129
+ raise ::Yeptris::JSON::ParseError, ::Yeptris::FFI.last_error_message if rc != ::Yeptris::FFI::OK
130
+
131
+ begin
132
+ st = ::FFI::MemoryPointer.new(:int)
133
+ raw = ::Yeptris::FFI.yeptris_tape_plan_walk(tape, @handle, st)
134
+ if raw.null?
135
+ raise Error,
136
+ "plan walk failed (status=#{st.read_int}) — the document shape " \
137
+ "disagrees with the plan"
138
+ end
139
+
140
+ begin
141
+ PlanResult.read(raw, src, @names)
142
+ ensure
143
+ ::Yeptris::FFI.yeptris_plan_result_free(raw)
144
+ end
145
+ ensure
146
+ ::Yeptris::FFI.yeptris_tape_free(tape)
147
+ end
148
+ end
149
+
150
+ def column_count
151
+ ::Yeptris::FFI.yeptris_plan_column_count(@handle)
152
+ end
153
+
154
+ # The eager columnar result of Descriptor#walk: typed Ruby columns
155
+ # keyed by leaf name, plus the row-hash conveniences. Nothing here
156
+ # borrows C memory.
157
+ class PlanResult
158
+ # @api private — one bulk read per column; the C result is freed
159
+ # on return (PlanResult owns plain Ruby data)
160
+ def self.read(raw, src, names)
161
+ rows = ::Yeptris::FFI.yeptris_plan_result_rows(raw)
162
+ columns = {}
163
+ names.each_with_index do |name, c|
164
+ kind = ::Yeptris::FFI.yeptris_plan_result_kind(raw, c)
165
+ columns[name] = read_column(raw, c, kind, rows, src)
166
+ end
167
+ new(rows, names, columns)
168
+ end
169
+
170
+ def self.read_column(raw, c, kind, rows, src)
171
+ return [] if rows.zero?
172
+
173
+ nulls = ::Yeptris::FFI.yeptris_plan_result_nulls(raw, c).read_bytes(rows).unpack("C*")
174
+ has_nulls = nulls.include?(1)
175
+ case kind
176
+ when 0 # int: the unpacked lane IS the column when no nulls
177
+ ints = ::Yeptris::FFI.yeptris_plan_result_ints(raw, c).read_bytes(rows * 8).unpack("q<*")
178
+ return ints unless has_nulls
179
+
180
+ Array.new(rows) { |i| nulls[i] == 1 ? nil : ints[i] }
181
+ when 1
182
+ floats = ::Yeptris::FFI.yeptris_plan_result_floats(raw, c).read_bytes(rows * 8).unpack("E*")
183
+ return floats unless has_nulls
184
+
185
+ Array.new(rows) { |i| nulls[i] == 1 ? nil : floats[i] }
186
+ when 3 # bool: 0/1 → false/true (always maps)
187
+ ints = ::Yeptris::FFI.yeptris_plan_result_ints(raw, c).read_bytes(rows * 8).unpack("q<*")
188
+ Array.new(rows) { |i| nulls[i] == 1 ? nil : ints[i] == 1 }
189
+ else # str: spans into the source, escapes decoded on the Ruby side
190
+ offs = ::Yeptris::FFI.yeptris_plan_result_str_offs(raw, c).read_bytes(rows * 4).unpack("V*")
191
+ lens = ::Yeptris::FFI.yeptris_plan_result_str_lens(raw, c).read_bytes(rows * 4).unpack("V*")
192
+ decode = ::Yeptris::JSON.method(:decode_span)
193
+ Array.new(rows) { |i| nulls[i] == 1 ? nil : decode.call(src, offs[i], lens[i]) }
194
+ end
195
+ end
196
+ private_class_method :read_column
197
+
198
+ # @api private — the DOM leg's read: typed lanes as above; str
199
+ # columns materialize from the (ptr,len) views into the document
200
+ def self.read_dom(raw, names)
201
+ rows = ::Yeptris::FFI.yeptris_plan_result_rows(raw)
202
+ columns = {}
203
+ names.each_with_index do |name, c|
204
+ kind = ::Yeptris::FFI.yeptris_plan_result_kind(raw, c)
205
+ if kind == 2
206
+ nulls = ::Yeptris::FFI.yeptris_plan_result_nulls(raw, c).read_bytes(rows).unpack("C*")
207
+ base = ::FFI::Pointer.new(::Yeptris::FFI.yeptris_plan_result_strs(raw, c))
208
+ stride = ::Yeptris::FFI::PlanStr.size
209
+ columns[name] = Array.new(rows) do |i|
210
+ next nil if nulls[i] == 1
211
+
212
+ v = ::Yeptris::FFI::PlanStr.new(base + i * stride)
213
+ v[:p].read_bytes(v[:len]).force_encoding(Encoding::UTF_8)
214
+ end
215
+ else
216
+ columns[name] = read_column(raw, c, kind, rows, nil)
217
+ end
218
+ end
219
+ new(rows, names, columns)
220
+ end
221
+
222
+ def initialize(rows, names, columns)
223
+ @rows = rows
224
+ @names = names
225
+ @columns = columns
226
+ end
227
+
228
+ def rows
229
+ @rows
230
+ end
231
+ alias count rows
232
+
233
+ # The planned leaf names, in spec order.
234
+ def names
235
+ @names.dup
236
+ end
237
+
238
+ # One typed column: an Array of rows values (nil where the leaf
239
+ # was null, missing, or shape-mismatched).
240
+ def column(name)
241
+ @columns.fetch(name) { raise ArgumentError, "no planned column #{name.inspect}" }
242
+ end
243
+
244
+ # Row +i+ as a Hash of the planned leaves (nil out of range).
245
+ def at(i)
246
+ return nil if i >= @rows || i.negative?
247
+
248
+ row = {}
249
+ @names.each { |n| row[n] = @columns[n][i] }
250
+ row
251
+ end
252
+
253
+ # Rows as Hashes — the tree-shaped convenience; column readers
254
+ # are the allocation-lean path.
255
+ def to_a
256
+ Array.new(@rows) { |i| at(i) }
257
+ end
258
+ alias to_ruby to_a # leptris's eager-tree vocabulary
259
+
260
+ def each_row(&block)
261
+ return enum_for(:each_row) unless block
262
+
263
+ @rows.times { |i| block.call(at(i)) }
264
+ end
265
+ end
266
+ end
@@ -0,0 +1,394 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yeptris
4
+ # The STRICT JSON surface (TODO.restructure/31).
5
+ #
6
+ # `Yeptris::JSON.load` targets EXACT `JSON.parse` semantics — that
7
+ # is its parity target, spec-pinned (spec/json_parity_spec.rb).
8
+ # It is deliberately separate from Yeptris::YAML: the YAML surface
9
+ # keeps the Psych contract for every input (JSON-shaped included),
10
+ # so the two never drift into each other's semantics.
11
+ #
12
+ # Engines, fastest first (both exact — the parity spec runs against
13
+ # whichever is loaded):
14
+ # 1. the native materializer (fused C scan → VALUE; opt-in build)
15
+ # 2. the record drain + strict conversion walk (always available)
16
+ module JSON
17
+ class Error < ::Yeptris::Error; end
18
+ class ParseError < Error; end
19
+
20
+ autoload :Descriptor, "yeptris/json/descriptor"
21
+
22
+ # json gem 3.0 made duplicate keys an error by DEFAULT; 2.x is
23
+ # last-wins. The parity target is the RESOLVED json gem's own
24
+ # behavior — strictness follows it (issue #37, found by canon's
25
+ # CI where json 3.0.0 resolved while the dev box had 2.x).
26
+ require "json"
27
+ require "date" # place_obj's `when Date` — psych loads it elsewhere, JSON.dump must not depend on that
28
+ STRICT_DUPLICATE_KEYS = Gem::Version.new(::JSON::VERSION) >= Gem::Version.new("3")
29
+
30
+ module_function
31
+
32
+ def load(source)
33
+ source = ::Yeptris.read_input(source)
34
+ source = source.to_s
35
+ if defined?(::Yeptris::Native)
36
+ begin
37
+ return ::Yeptris::Native.load_json(source, STRICT_DUPLICATE_KEYS)
38
+ rescue ::Yeptris::ParseError => e
39
+ raise ParseError, e.message
40
+ end
41
+ end
42
+ tape_engine(source)
43
+ end
44
+
45
+ # The tape engine (issue #81): ONE call validates and materializes
46
+ # the record stream; the walk reads four bulk columns and slices
47
+ # strings straight out of the caller's source — no gate document,
48
+ # no recorder transform, no arena copy (the drain path paid all
49
+ # three, which is where medium/large throughput went).
50
+ T_DOC = 0
51
+ T_NULL = 1
52
+ T_TRUE = 2
53
+ T_FALSE = 3
54
+ T_INT = 4
55
+ T_FLOAT = 5
56
+ T_STR = 6
57
+ T_SEQ_OPEN = 7
58
+ T_MAP_OPEN = 8
59
+ T_CLOSE = 9
60
+
61
+ # The tape keeps RAW string spans (escapes untouched — the C
62
+ # contract); the walk decodes them here. The hot path (no
63
+ # backslash) returns the slice untouched.
64
+ SIMPLE_ESCAPES = {
65
+ '"' => '"'.b, "\\" => "\\".b, "/" => "/".b, "b" => "\b".b, "f" => "\f".b,
66
+ "n" => "\n".b, "r" => "\r".b, "t" => "\t".b
67
+ }.freeze
68
+
69
+ def decode_span(src, off, len)
70
+ s = src.byteslice(off, len)
71
+ s.force_encoding(Encoding::UTF_8)
72
+ return s unless s.include?("\\")
73
+
74
+ b = s.b
75
+ out = String.new(encoding: Encoding::BINARY)
76
+ i = 0
77
+ n = b.bytesize
78
+ while i < n
79
+ c = b.getbyte(i)
80
+ if c != 0x5C || i + 1 >= n
81
+ out << c
82
+ i += 1
83
+ next
84
+ end
85
+ e = b.getbyte(i + 1)
86
+ if e != 0x75 # simple escape
87
+ ch = e.chr
88
+ out << (SIMPLE_ESCAPES[ch] || ch)
89
+ i += 2
90
+ next
91
+ end
92
+ cp = b.byteslice(i + 2, 4).to_i(16)
93
+ if cp >= 0xD800 && cp <= 0xDBFF && i + 12 <= n &&
94
+ b.getbyte(i + 6) == 0x5C && b.getbyte(i + 7) == 0x75
95
+ lo = b.byteslice(i + 8, 4).to_i(16)
96
+ if lo >= 0xDC00 && lo <= 0xDFFF
97
+ cp = 0x10000 + ((cp - 0xD800) << 10) + (lo - 0xDC00)
98
+ i += 12
99
+ else
100
+ i += 6
101
+ end
102
+ else
103
+ i += 6
104
+ end
105
+ out << [cp].pack("U").b
106
+ end
107
+ out.force_encoding(Encoding::UTF_8)
108
+ end
109
+
110
+ def tape_engine(source)
111
+ tape = ::Yeptris::FFI::JsonTape.new
112
+ rc = ::Yeptris::FFI.yeptris_parse_json_tape(source, source.bytesize, tape)
113
+ raise ParseError, ::Yeptris::FFI.last_error_message if rc != ::Yeptris::FFI::OK
114
+
115
+ begin
116
+ walk_tape(source, tape)
117
+ ensure
118
+ ::Yeptris::FFI.yeptris_tape_free(tape)
119
+ end
120
+ end
121
+
122
+ def walk_tape(src, tape, strict_dup = STRICT_DUPLICATE_KEYS)
123
+ n = tape[:count]
124
+ kinds = tape[:kinds].read_bytes(n).unpack("C*")
125
+ offs = tape[:offs].read_bytes(n * 4).unpack("V*")
126
+ lens = tape[:lens].read_bytes(n * 4).unpack("V*")
127
+ # v2: parse records number spans only — ONE bulk convert call
128
+ # materializes them (per-number FFI calls would sink the ladder)
129
+ ivp = ::FFI::MemoryPointer.new(:int64, n, true)
130
+ dvp = ::FFI::MemoryPointer.new(:double, n, true)
131
+ ::Yeptris::FFI.yeptris_tape_convert(tape, 0, n, ivp, dvp)
132
+ vals_i = ivp.read_bytes(n * 8).unpack("q<*")
133
+ vals_f = dvp.read_bytes(n * 8).unpack("E*")
134
+ # any integer text beyond int64: every INT rebuilds from its span
135
+ # (exact Bignum; JSON.parse parity) — int_min is set by the
136
+ # convert call above
137
+ exact_ints = !tape[:int_min].zero?
138
+ utf8 = src.encoding == Encoding::UTF_8
139
+
140
+ docs = [nil] # record 0 (DOC) pre-consumed — the root's slot
141
+ stack = []
142
+ key_sets = strict_dup ? [{}] : nil
143
+ pending_key = nil
144
+ i = 1
145
+ while i < n
146
+ case kinds[i]
147
+ when T_SEQ_OPEN
148
+ place(docs, stack, pending_key) { [] }
149
+ pending_key = nil
150
+ when T_MAP_OPEN
151
+ key_sets&.push({})
152
+ place(docs, stack, pending_key) { {} }
153
+ pending_key = nil
154
+ when T_CLOSE
155
+ # Key frames are pushed per MAP_OPEN — an ARRAY close must
156
+ # not pop the enclosing map's frame (issue found by ea's CI:
157
+ # {"a":[1],"b":[2],"c":[3]} exhausted the frames and the next
158
+ # map key hit key_sets.last.key? on nil under strict mode).
159
+ key_sets&.pop if stack.last.is_a?(Hash)
160
+ stack.pop
161
+ when T_STR
162
+ # empty-string literals are frozen+shared since Ruby 3.4
163
+ # (and `+""` binds after the method chain anyway), so the
164
+ # fresh copy must come from the encoding call itself
165
+ text = lens[i] == 0 ? String.new(encoding: Encoding::UTF_8) : decode_span(src, offs[i], lens[i])
166
+ if pending_key.nil? && !stack.empty? && stack.last.is_a?(Hash)
167
+ if key_sets && key_sets.last.key?(text)
168
+ raise ParseError, %(duplicate key "#{text}" in JSON object)
169
+ end
170
+ key_sets&.last&.store(text, true)
171
+ pending_key = text
172
+ else
173
+ place(docs, stack, pending_key) { text }
174
+ pending_key = nil
175
+ end
176
+ when T_INT
177
+ if exact_ints
178
+ place(docs, stack, pending_key) { Integer(src.byteslice(offs[i], lens[i]), 10) }
179
+ else
180
+ v = vals_i[i]
181
+ place(docs, stack, pending_key) { v }
182
+ end
183
+ pending_key = nil
184
+ when T_FLOAT
185
+ v = vals_f[i]
186
+ place(docs, stack, pending_key) { v }
187
+ pending_key = nil
188
+ when T_TRUE, T_FALSE # v2: the kind IS the value
189
+ place(docs, stack, pending_key) { kinds[i] == T_TRUE }
190
+ pending_key = nil
191
+ when T_NULL
192
+ place(docs, stack, pending_key) { nil }
193
+ pending_key = nil
194
+ else
195
+ raise Error, "internal: impossible tape record #{kinds[i]}"
196
+ end
197
+ i += 1
198
+ end
199
+ docs.empty? ? nil : docs.first
200
+ end
201
+
202
+ # The always-available engine: the strict-JSON validator gates
203
+ # (parse_json raises on anything RFC 8259 rejects), then the
204
+ # value records convert WITHOUT the Psych quirk table — floats
205
+ # are always Floats, bools always bools ("1e3" is 1000.0 here
206
+ # and a String on the YAML surface; each surface is its own
207
+ # contract).
208
+ def strict_fallback(source)
209
+ begin
210
+ gate = ::Yeptris::Document.parse_json(source)
211
+ rescue ::Yeptris::ParseError => e
212
+ raise ParseError, e.message
213
+ end
214
+ begin
215
+ cols = ::Yeptris::FFI::ValueColumns.new
216
+ st = ::Yeptris::FFI.yeptris_value_drain_columns(
217
+ source, source.bytesize, ::Yeptris::FFI::SCHEMA_12_CORE, cols
218
+ )
219
+ raise ParseError, ::Yeptris::FFI.last_error_message if st != ::Yeptris::FFI::OK
220
+
221
+ begin
222
+ walk_strict(cols, STRICT_DUPLICATE_KEYS)
223
+ ensure
224
+ ::Yeptris::FFI.yeptris_value_free_columns(cols)
225
+ end
226
+ ensure
227
+ gate.free
228
+ end
229
+ end
230
+
231
+ # Placement mechanics mirror ValueML.walk_columns; the CONVERSION
232
+ # is the strict-JSON one (no ':sym' scan, no y/n quirk, no
233
+ # dot-required floats). Anchors/aliases/timestamps cannot occur
234
+ # in strict JSON — reaching them is an internal error.
235
+ def walk_strict(cols, strict_dup = STRICT_DUPLICATE_KEYS)
236
+ n = cols[:count]
237
+ kinds = cols[:kinds].read_bytes(n).unpack("C*")
238
+ ikeys = cols[:is_keys].read_bytes(n).unpack("C*")
239
+ bools = cols[:bools].read_bytes(n).unpack("C*")
240
+ offs = cols[:offs].read_bytes(n * 4).unpack("V*")
241
+ lens = cols[:lens].read_bytes(n * 4).unpack("V*")
242
+ pays = cols[:payloads].read_bytes(n * 8).unpack("q<*")
243
+ arena = cols[:arena_len].zero? ? +"" : cols[:arena].read_bytes(cols[:arena_len])
244
+ arena.force_encoding(Encoding::UTF_8)
245
+
246
+ docs = []
247
+ stack = []
248
+ key_sets = strict_dup ? [{}] : nil
249
+ pending_key = nil
250
+ i = 0
251
+ while i < n
252
+ case kinds[i]
253
+ when ValueML::DOC
254
+ docs.push(nil)
255
+ when ValueML::SEQ_OPEN
256
+ place(docs, stack, pending_key) { [] }
257
+ pending_key = nil
258
+ when ValueML::MAP_OPEN
259
+ key_sets&.push({})
260
+ place(docs, stack, pending_key) { {} }
261
+ pending_key = nil
262
+ when ValueML::CLOSE
263
+ key_sets&.pop if stack.last.is_a?(Hash)
264
+ stack.pop
265
+ when ValueML::V_STR
266
+ text = arena.byteslice(offs[i], lens[i])
267
+ if ikeys[i] == 1 && !stack.empty? && stack.last.is_a?(Hash)
268
+ if strict_dup && key_sets.last.key?(text)
269
+ raise ParseError, %(duplicate key "#{text}" in JSON object)
270
+ end
271
+ key_sets&.last&.store(text, true)
272
+ pending_key = text
273
+ else
274
+ # Records carry int64 payloads: an integer-beyond-int64
275
+ # degrades to a PLAIN string (b==1). In strict JSON a
276
+ # plain (unquoted) scalar can ONLY be a number — every
277
+ # real string is quoted and arrives b==0 — so rebuild the
278
+ # exact Integer (JSON.parse parity, Bignum included).
279
+ if bools[i] == 1
280
+ place(docs, stack, pending_key) { Integer(text, 10) }
281
+ else
282
+ place(docs, stack, pending_key) { text }
283
+ end
284
+ pending_key = nil
285
+ end
286
+ when ValueML::V_INT
287
+ place(docs, stack, pending_key) { pays[i] }
288
+ pending_key = nil
289
+ when ValueML::V_FLOAT
290
+ place(docs, stack, pending_key) { [pays[i]].pack("q<").unpack1("E") }
291
+ pending_key = nil
292
+ when ValueML::V_BOOL
293
+ place(docs, stack, pending_key) { bools[i] == 1 }
294
+ pending_key = nil
295
+ when ValueML::V_NULL
296
+ place(docs, stack, pending_key) { nil }
297
+ pending_key = nil
298
+ else
299
+ raise Error, "internal: impossible record #{kinds[i]} in strict JSON"
300
+ end
301
+ i += 1
302
+ end
303
+ docs.empty? ? nil : docs.first
304
+ end
305
+
306
+ # ---- generation (issue #81) ------------------------------------------
307
+ #
308
+ # `dump` targets JSON.generate semantics for the core types: the
309
+ # object walk reuses YAML.dump's packed-entry bulk builder (one
310
+ # FFI build call), then the strict-JSON writer serializes —
311
+ # Strings ride DOUBLE_QUOTED (a "42" stays quoted; numbers, bools
312
+ # and null stay plain), Symbols become their name.
313
+ class DumpError < Error; end
314
+
315
+ def dump(obj)
316
+ parts = String.new(encoding: Encoding::BINARY,
317
+ capacity: 12 * 16) # 12-byte entries, appended in place
318
+ blob = String.new(encoding: Encoding::BINARY)
319
+ off = [0]
320
+ place_obj(obj, parts, blob, off)
321
+ doc = ::Yeptris::Document.create
322
+ # FFI passes String args BY REFERENCE — no MemoryPointer copies
323
+ rc = doc.build_entries(parts, parts.bytesize / 12, blob, blob.bytesize)
324
+ raise DumpError, "document_build failed: #{rc}" unless rc == ::Yeptris::FFI::OK
325
+ out = ::Yeptris::FFI::JSON_EX ? doc.serialize_json_compact : doc.serialize_json
326
+ # JSON.generate emits no trailing newline (the document writer does)
327
+ out.end_with?("\n") ? out[0, out.bytesize - 1] : out
328
+ ensure
329
+ doc&.free
330
+ end
331
+
332
+ F = ::Yeptris::FFI
333
+ MAP_E = [F::BUILD_MAP, 0, 0, 0].pack("CCx2VV").freeze
334
+ SEQ_E = [F::BUILD_SEQ, 0, 0, 0].pack("CCx2VV").freeze
335
+ END_E = [F::BUILD_END, 0, 0, 0].pack("CCx2VV").freeze
336
+ DQ = F::STYLE_DOUBLE_QUOTED
337
+ PL = F::STYLE_PLAIN
338
+ SC_DQ = [F::BUILD_SCALAR, DQ].pack("CCx2").freeze
339
+ SC_PL = [F::BUILD_SCALAR, PL].pack("CCx2").freeze
340
+
341
+ def place_obj(obj, parts, blob, off)
342
+ case obj
343
+ when Hash
344
+ parts << MAP_E
345
+ obj.each do |k, v|
346
+ key = k.is_a?(String) ? k : k.is_a?(Symbol) ? k.name : k.to_s
347
+ scalar_json(key, SC_DQ, parts, blob, off)
348
+ place_obj(v, parts, blob, off)
349
+ end
350
+ parts << END_E
351
+ when Array
352
+ parts << SEQ_E
353
+ obj.each { |e| place_obj(e, parts, blob, off) }
354
+ parts << END_E
355
+ when String
356
+ scalar_json(obj, SC_DQ, parts, blob, off)
357
+ when Symbol
358
+ scalar_json(obj.name, SC_DQ, parts, blob, off)
359
+ when Integer, Float
360
+ scalar_json(obj.to_s, SC_PL, parts, blob, off)
361
+ when true
362
+ scalar_json("true", SC_PL, parts, blob, off)
363
+ when false
364
+ scalar_json("false", SC_PL, parts, blob, off)
365
+ when nil
366
+ scalar_json("null", SC_PL, parts, blob, off)
367
+ when Date, Time
368
+ scalar_json(obj.iso8601, SC_DQ, parts, blob, off)
369
+ else
370
+ raise DumpError, "cannot dump #{obj.class}: unsupported object " \
371
+ "(JSON.generate calls to_json on custom types)"
372
+ end
373
+ end
374
+
375
+ def scalar_json(text, prefix, parts, blob, off)
376
+ parts << (prefix + [off[0], text.bytesize].pack("VV"))
377
+ blob << text
378
+ off[0] += text.bytesize
379
+ end
380
+
381
+ def place(docs, stack, key)
382
+ v = yield
383
+ if stack.empty?
384
+ docs[-1] = v
385
+ elsif key
386
+ stack.last[key] = v
387
+ else
388
+ stack.last.push(v)
389
+ end
390
+ stack.push(v) if v.is_a?(Array) || v.is_a?(Hash)
391
+ v
392
+ end
393
+ end
394
+ end