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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4ba1ff94687c90184c892c44b7d301e66edb31a2d945323efd2a0ad82a26be21
4
- data.tar.gz: '05090e440ad8c351564c9effb8057f4bb4e96da3405eb3502eb72501328f8ff2'
3
+ metadata.gz: 88619cb26e9e27d9a68e8bef60cc387a3d49bb0452d7dde483e35288965a9d31
4
+ data.tar.gz: 66565201383cac8685a5607e864fc09f2f0baacee46a734169adadefe9976663
5
5
  SHA512:
6
- metadata.gz: 9259f394a917e14cb96f56d9b4de1bb7427231abe232dec3e469cfb777cf01482cb0263647cb6a7e64e66b344a1852b48b3d6fed959f736fc591075b091042b7
7
- data.tar.gz: d094de19cb2781dcb69aecbbfd1c46d6c3307be63deb6f9979330427346e21b13573ee97ec2da04207f85c3b0027617e1cab4e94a46bfcaefa9644e18336058d
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.5", features = ["ruby"] }
31
+ parsanol = { version = "0.8.8", features = ["ruby"] }
32
32
 
33
33
  # Logging
34
34
  log = "0.4"
@@ -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
- attach_function :c_parse, :parsanol_c_parse,
101
- %i[uint64 string pointer size_t], :long_long
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.c_parse(handle, input, nil, 0)
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.c_parse(handle, input, @buffer, cap)
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 iteration
129
- key = hash.keys.first
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
- [REPETITION_SYM, REPETITION_TAG].include?(value.first)
145
+ (value.first.equal?(REPETITION_SYM) || value.first == REPETITION_TAG)
141
146
 
142
147
  # Check RAW value for repetition pattern BEFORE transformation
143
- # Array with items that all have the parent key
144
- # e.g., [{x: 1}, {x: 2}] where parent key is :x
145
- is_raw_array_repetition = value.is_a?(Array) && !value.empty? &&
146
- value.all? do |item|
147
- item.is_a?(Hash) && item.keys.length == 1 && item.key?(key)
148
- end
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
- # Special handling for arrays that look like character repetitions
155
- # (arrays of single-character Slices/strings should be joined)
156
- if transformed.is_a?(Array) && !transformed.empty? &&
157
- transformed.all? do |item|
158
- slice_or_string?(item) && item_length(item) == 1
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
- # Join preserving position from first Slice
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
- # Check for UNTAGGED repetition pattern (native output):
172
- # If array items all have the same key as parent, it's a repetition
173
- is_transformed_repetition = transformed.is_a?(Array) && !transformed.empty? &&
174
- transformed.all? do |item|
175
- item.is_a?(Hash) && item.keys.length == 1 && item.key?(sym_key)
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
- is_repetition = is_tagged_repetition || is_raw_array_repetition || is_transformed_repetition || is_empty_repetition
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). Build one Slice
317
- # from the first Slice's offset, joining contents in place.
318
- if list.all? { |x| x.is_a?(::Parsanol::Slice) || x.is_a?(String) }
319
- first_slice = list.find { |x| x.is_a?(::Parsanol::Slice) }
320
- content = list.map { |x| x.is_a?(::Parsanol::Slice) ? x.content : x.to_s }.join
321
- return first_slice ? ::Parsanol::Slice.new(first_slice.offset, content, first_slice.input) : content
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 → foldl via concat.
431
+ # Hot path: all-stringlike repetition → fused single-pass concat.
376
432
  # Cold path: parslet's exact fold.
377
- if items.all? { |x| x.is_a?(::Parsanol::Slice) || x.is_a?(String) }
378
- return EMPTY_STRING if items.empty?
433
+ if items.empty?
434
+ return EMPTY_STRING
435
+ end
379
436
 
380
- first_slice = items.find { |x| x.is_a?(::Parsanol::Slice) }
381
- content = items.map { |x| x.is_a?(::Parsanol::Slice) ? x.content : x.to_s }.join
382
- return first_slice ? ::Parsanol::Slice.new(first_slice.offset, content, first_slice.input) : content
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 }
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Parsanol
4
- VERSION = "1.3.49"
4
+ VERSION = "1.3.50"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: parsanol
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.49
4
+ version: 1.3.50
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.