parsanol 1.3.49 → 1.3.50
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 +4 -4
- data/ext/parsanol_native/Cargo.toml +1 -1
- data/lib/parsanol/native/ffi.rb +11 -5
- data/lib/parsanol/native/transformer.rb +114 -43
- data/lib/parsanol/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 88619cb26e9e27d9a68e8bef60cc387a3d49bb0452d7dde483e35288965a9d31
|
|
4
|
+
data.tar.gz: 66565201383cac8685a5607e864fc09f2f0baacee46a734169adadefe9976663
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bffa5d597bb727763303af4dd72ad3394809fbe68a4fafeebb4dc520028ac836eea366a42548f8f02c1a76158207ecfbe52ddb9b6ce627328f32b8d61a22724d
|
|
7
|
+
data.tar.gz: bd4a8e1daf6e626d4626b90fe59c0703bab00c3a78e593f69d3e46d24b68376b5bf6002108a6d4302fc158e72ba3fd804e9df70338c46291702ce0d51aa93764
|
|
@@ -28,7 +28,7 @@ rb-sys = { version = "0.9.124", features = ["global-allocator"] }
|
|
|
28
28
|
magnus = { version = "0.9" }
|
|
29
29
|
|
|
30
30
|
# parsanol parser library (from git for latest features)
|
|
31
|
-
parsanol = { version = "0.8.
|
|
31
|
+
parsanol = { version = "0.8.8", features = ["ruby"] }
|
|
32
32
|
|
|
33
33
|
# Logging
|
|
34
34
|
log = "0.4"
|
data/lib/parsanol/native/ffi.rb
CHANGED
|
@@ -97,8 +97,13 @@ module Parsanol
|
|
|
97
97
|
ffi_lib path
|
|
98
98
|
attach_function :c_register, :parsanol_c_register,
|
|
99
99
|
%i[string], :uint64
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
# Explicit-length entry point: interior NULs stay intact. The
|
|
101
|
+
# input crosses as a raw buffer (:string rejects NUL bytes by
|
|
102
|
+
# design); a cdylib without the symbol is stale relative to
|
|
103
|
+
# this binding and fails availability loudly (pure-Ruby
|
|
104
|
+
# fallback) instead of silently truncating at NULs.
|
|
105
|
+
attach_function :c_parse_len, :parsanol_c_parse_len,
|
|
106
|
+
%i[uint64 buffer_in size_t pointer size_t], :long_long
|
|
102
107
|
attach_function :c_last_error, :parsanol_c_last_error,
|
|
103
108
|
[], :string
|
|
104
109
|
attach_function :c_release, :parsanol_c_release,
|
|
@@ -114,9 +119,10 @@ module Parsanol
|
|
|
114
119
|
|
|
115
120
|
# Two-call buffer protocol: cap=0 asks for the needed size, then
|
|
116
121
|
# the batch lands in a persistent buffer that only grows —
|
|
117
|
-
# steady-state parses allocate nothing.
|
|
122
|
+
# steady-state parses allocate nothing. The explicit-length call
|
|
123
|
+
# keeps interior NULs intact.
|
|
118
124
|
def parse_into(handle, input)
|
|
119
|
-
needed = @binding.
|
|
125
|
+
needed = @binding.c_parse_len(handle, input, input.bytesize, nil, 0)
|
|
120
126
|
return needed unless needed.negative?
|
|
121
127
|
|
|
122
128
|
cap = -needed
|
|
@@ -124,7 +130,7 @@ module Parsanol
|
|
|
124
130
|
@buffer&.free
|
|
125
131
|
@buffer = FFI::MemoryPointer.new(:uint64, cap * 2)
|
|
126
132
|
end
|
|
127
|
-
@binding.
|
|
133
|
+
@binding.c_parse_len(handle, input, input.bytesize, @buffer, cap)
|
|
128
134
|
end
|
|
129
135
|
end
|
|
130
136
|
end
|
|
@@ -123,10 +123,15 @@ module Parsanol
|
|
|
123
123
|
transform_multi_key_hash(hash)
|
|
124
124
|
end
|
|
125
125
|
|
|
126
|
-
# Optimized handling for single-key hashes (the common case)
|
|
126
|
+
# Optimized handling for single-key hashes (the common case).
|
|
127
|
+
# The shape scans below are fused into as few passes as possible:
|
|
128
|
+
# each predicate was previously its own `all?` walk with a lambda
|
|
129
|
+
# dispatch, and `item.keys.length` allocated a keys array per
|
|
130
|
+
# element (Hash#length is O(1)).
|
|
127
131
|
def self.transform_single_key_hash(hash)
|
|
128
|
-
# Extract the single key-value pair without
|
|
129
|
-
key
|
|
132
|
+
# Extract the single key-value pair without iterating (the hash
|
|
133
|
+
# is guaranteed single-key by the caller).
|
|
134
|
+
key, = hash.first
|
|
130
135
|
value = hash[key]
|
|
131
136
|
sym_key = cached_symbol(key)
|
|
132
137
|
|
|
@@ -137,45 +142,74 @@ module Parsanol
|
|
|
137
142
|
# The Rust handle path tags with Symbols, the batch decoder with
|
|
138
143
|
# ":repetition" Strings — accept both.
|
|
139
144
|
is_tagged_repetition = value.is_a?(Array) && !value.empty? &&
|
|
140
|
-
|
|
145
|
+
(value.first.equal?(REPETITION_SYM) || value.first == REPETITION_TAG)
|
|
141
146
|
|
|
142
147
|
# Check RAW value for repetition pattern BEFORE transformation
|
|
143
|
-
#
|
|
144
|
-
# e.g
|
|
145
|
-
is_raw_array_repetition =
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
148
|
+
# (bare repeated sibling captures, #36): array items that all
|
|
149
|
+
# carry the parent key, e.g. [{x: 1}, {x: 2}].
|
|
150
|
+
is_raw_array_repetition = false
|
|
151
|
+
if value.is_a?(Array) && !value.empty? && !is_tagged_repetition
|
|
152
|
+
is_raw_array_repetition = raw_items_all_named?(value, key)
|
|
153
|
+
end
|
|
149
154
|
|
|
150
155
|
# Empty array from native parser is a repetition result (not a sequence)
|
|
151
156
|
# Sequences produce arrays of arrays like [[], []], not empty arrays
|
|
152
157
|
is_empty_repetition = value.is_a?(Array) && value.empty?
|
|
153
158
|
|
|
154
|
-
#
|
|
155
|
-
#
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
+
# Single fused pass over the transformed array: detect the
|
|
160
|
+
# single-character join shape AND the untagged repetition shape
|
|
161
|
+
# in one walk (an element cannot be both a Hash and a
|
|
162
|
+
# single-character stringlike, so the predicates share a loop).
|
|
163
|
+
joined = nil
|
|
164
|
+
is_transformed_repetition = false
|
|
165
|
+
if transformed.is_a?(Array) && !transformed.empty?
|
|
166
|
+
all_named = true
|
|
167
|
+
all_single_char = true
|
|
168
|
+
content = nil
|
|
169
|
+
first_slice = nil
|
|
170
|
+
transformed.each do |item|
|
|
171
|
+
if item.is_a?(Hash)
|
|
172
|
+
all_single_char = false
|
|
173
|
+
unless item.length == 1 && item.key?(sym_key)
|
|
174
|
+
all_named = false
|
|
175
|
+
break
|
|
176
|
+
end
|
|
177
|
+
elsif item.is_a?(::Parsanol::Slice) || item.is_a?(String)
|
|
178
|
+
all_named = false
|
|
179
|
+
all_single_char = false unless item.length == 1
|
|
180
|
+
break if !all_single_char && !all_named
|
|
181
|
+
else
|
|
182
|
+
all_named = false
|
|
183
|
+
all_single_char = false
|
|
184
|
+
break
|
|
159
185
|
end
|
|
160
|
-
|
|
161
|
-
first_slice = transformed.find { |i| i.is_a?(::Parsanol::Slice) }
|
|
162
|
-
content = transformed.map { |i| slice_content(i) }.join
|
|
163
|
-
transformed = if first_slice
|
|
164
|
-
::Parsanol::Slice.new(first_slice.offset, content,
|
|
165
|
-
first_slice.input)
|
|
166
|
-
else
|
|
167
|
-
content
|
|
168
|
-
end
|
|
169
|
-
end
|
|
186
|
+
end
|
|
170
187
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
188
|
+
if all_single_char
|
|
189
|
+
# Join preserving position from the first Slice
|
|
190
|
+
content = +""
|
|
191
|
+
transformed.each do |item|
|
|
192
|
+
if item.is_a?(::Parsanol::Slice)
|
|
193
|
+
first_slice ||= item
|
|
194
|
+
content << item.content
|
|
195
|
+
else
|
|
196
|
+
content << item.to_s
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
joined = if first_slice
|
|
200
|
+
::Parsanol::Slice.new(first_slice.offset,
|
|
201
|
+
content, first_slice.input)
|
|
202
|
+
else
|
|
203
|
+
content
|
|
204
|
+
end
|
|
205
|
+
elsif all_named
|
|
206
|
+
is_transformed_repetition = true
|
|
176
207
|
end
|
|
208
|
+
end
|
|
177
209
|
|
|
178
|
-
|
|
210
|
+
transformed = joined if joined
|
|
211
|
+
is_repetition = is_tagged_repetition || is_raw_array_repetition ||
|
|
212
|
+
is_transformed_repetition || is_empty_repetition
|
|
179
213
|
|
|
180
214
|
# Handle based on type
|
|
181
215
|
if is_repetition
|
|
@@ -190,6 +224,14 @@ module Parsanol
|
|
|
190
224
|
end
|
|
191
225
|
end
|
|
192
226
|
|
|
227
|
+
# Alloc-free raw repetition scan: Hash#length instead of
|
|
228
|
+
# item.keys.length (which allocated per element).
|
|
229
|
+
def self.raw_items_all_named?(value, key)
|
|
230
|
+
value.all? do |item|
|
|
231
|
+
item.is_a?(Hash) && item.length == 1 && item.key?(key)
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
193
235
|
# Get content from Slice or string
|
|
194
236
|
def self.slice_content(value)
|
|
195
237
|
value.is_a?(::Parsanol::Slice) ? value.content : value.to_s
|
|
@@ -313,12 +355,26 @@ module Parsanol
|
|
|
313
355
|
return list.reduce { |acc, hash| acc.merge(hash) }
|
|
314
356
|
end
|
|
315
357
|
|
|
316
|
-
# Hot path: all stringlike (String/Slice).
|
|
317
|
-
#
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
358
|
+
# Hot path: all stringlike (String/Slice). One fused pass detects
|
|
359
|
+
# the shape and joins contents in place (no per-item lambda
|
|
360
|
+
# dispatch, no intermediate map array).
|
|
361
|
+
content = +""
|
|
362
|
+
first_slice = nil
|
|
363
|
+
all_stringlike = list.all? do |x|
|
|
364
|
+
if x.is_a?(::Parsanol::Slice)
|
|
365
|
+
first_slice ||= x
|
|
366
|
+
content << x.content
|
|
367
|
+
true
|
|
368
|
+
elsif x.is_a?(String)
|
|
369
|
+
content << x
|
|
370
|
+
true
|
|
371
|
+
else
|
|
372
|
+
false
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
if all_stringlike
|
|
376
|
+
return first_slice ? ::Parsanol::Slice.new(first_slice.offset, content,
|
|
377
|
+
first_slice.input) : content
|
|
322
378
|
end
|
|
323
379
|
|
|
324
380
|
# Cold path: parslet's exact fold (Hash/Slice/String/Array
|
|
@@ -372,14 +428,29 @@ module Parsanol
|
|
|
372
428
|
|
|
373
429
|
return EMPTY_ARRAY if named && items.empty?
|
|
374
430
|
|
|
375
|
-
# Hot path: all-stringlike repetition →
|
|
431
|
+
# Hot path: all-stringlike repetition → fused single-pass concat.
|
|
376
432
|
# Cold path: parslet's exact fold.
|
|
377
|
-
if items.
|
|
378
|
-
return EMPTY_STRING
|
|
433
|
+
if items.empty?
|
|
434
|
+
return EMPTY_STRING
|
|
435
|
+
end
|
|
379
436
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
437
|
+
content = +""
|
|
438
|
+
first_slice = nil
|
|
439
|
+
all_stringlike = items.all? do |x|
|
|
440
|
+
if x.is_a?(::Parsanol::Slice)
|
|
441
|
+
first_slice ||= x
|
|
442
|
+
content << x.content
|
|
443
|
+
true
|
|
444
|
+
elsif x.is_a?(String)
|
|
445
|
+
content << x
|
|
446
|
+
true
|
|
447
|
+
else
|
|
448
|
+
false
|
|
449
|
+
end
|
|
450
|
+
end
|
|
451
|
+
if all_stringlike
|
|
452
|
+
return first_slice ? ::Parsanol::Slice.new(first_slice.offset, content,
|
|
453
|
+
first_slice.input) : content
|
|
383
454
|
end
|
|
384
455
|
|
|
385
456
|
foldl(items.compact) { |acc, item| acc + item }
|
data/lib/parsanol/version.rb
CHANGED