parsanol 1.3.13-arm-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.
Files changed (100) hide show
  1. checksums.yaml +7 -0
  2. data/HISTORY.txt +98 -0
  3. data/LICENSE +23 -0
  4. data/README.adoc +905 -0
  5. data/Rakefile +141 -0
  6. data/lib/parsanol/3.2/parsanol_native.so +0 -0
  7. data/lib/parsanol/3.3/parsanol_native.so +0 -0
  8. data/lib/parsanol/3.4/parsanol_native.so +0 -0
  9. data/lib/parsanol/4.0/parsanol_native.so +0 -0
  10. data/lib/parsanol/ast_visitor.rb +122 -0
  11. data/lib/parsanol/atoms/alternative.rb +123 -0
  12. data/lib/parsanol/atoms/base.rb +208 -0
  13. data/lib/parsanol/atoms/can_flatten.rb +194 -0
  14. data/lib/parsanol/atoms/capture.rb +38 -0
  15. data/lib/parsanol/atoms/context.rb +357 -0
  16. data/lib/parsanol/atoms/context_optimized.rb +38 -0
  17. data/lib/parsanol/atoms/custom.rb +110 -0
  18. data/lib/parsanol/atoms/cut.rb +66 -0
  19. data/lib/parsanol/atoms/dsl.rb +96 -0
  20. data/lib/parsanol/atoms/dynamic.rb +39 -0
  21. data/lib/parsanol/atoms/entity.rb +75 -0
  22. data/lib/parsanol/atoms/ignored.rb +37 -0
  23. data/lib/parsanol/atoms/infix.rb +167 -0
  24. data/lib/parsanol/atoms/lookahead.rb +85 -0
  25. data/lib/parsanol/atoms/named.rb +74 -0
  26. data/lib/parsanol/atoms/re.rb +83 -0
  27. data/lib/parsanol/atoms/repetition.rb +277 -0
  28. data/lib/parsanol/atoms/scope.rb +35 -0
  29. data/lib/parsanol/atoms/sequence.rb +195 -0
  30. data/lib/parsanol/atoms/str.rb +109 -0
  31. data/lib/parsanol/atoms/visitor.rb +91 -0
  32. data/lib/parsanol/atoms.rb +46 -0
  33. data/lib/parsanol/buffer.rb +133 -0
  34. data/lib/parsanol/builder_callbacks.rb +353 -0
  35. data/lib/parsanol/cause.rb +122 -0
  36. data/lib/parsanol/context.rb +39 -0
  37. data/lib/parsanol/convenience.rb +36 -0
  38. data/lib/parsanol/edit_tracker.rb +111 -0
  39. data/lib/parsanol/error_reporter/contextual.rb +99 -0
  40. data/lib/parsanol/error_reporter/deepest.rb +120 -0
  41. data/lib/parsanol/error_reporter/tree.rb +63 -0
  42. data/lib/parsanol/error_reporter.rb +100 -0
  43. data/lib/parsanol/expression/treetop.rb +154 -0
  44. data/lib/parsanol/expression.rb +106 -0
  45. data/lib/parsanol/fast_mode.rb +187 -0
  46. data/lib/parsanol/first_set.rb +79 -0
  47. data/lib/parsanol/grammar_builder.rb +179 -0
  48. data/lib/parsanol/incremental_parser.rb +182 -0
  49. data/lib/parsanol/interval_tree.rb +226 -0
  50. data/lib/parsanol/lazy_result.rb +179 -0
  51. data/lib/parsanol/mermaid.rb +142 -0
  52. data/lib/parsanol/native/batch_decoder.rb +255 -0
  53. data/lib/parsanol/native/dynamic.rb +238 -0
  54. data/lib/parsanol/native/parser.rb +102 -0
  55. data/lib/parsanol/native/serializer.rb +252 -0
  56. data/lib/parsanol/native/transformer.rb +604 -0
  57. data/lib/parsanol/native/types.rb +29 -0
  58. data/lib/parsanol/native.rb +223 -0
  59. data/lib/parsanol/optimizer.rb +85 -0
  60. data/lib/parsanol/optimizers/choice_optimizer.rb +78 -0
  61. data/lib/parsanol/optimizers/cut_inserter.rb +182 -0
  62. data/lib/parsanol/optimizers/lookahead_optimizer.rb +56 -0
  63. data/lib/parsanol/optimizers/quantifier_optimizer.rb +60 -0
  64. data/lib/parsanol/optimizers/sequence_optimizer.rb +97 -0
  65. data/lib/parsanol/options/zero_copy.rb +127 -0
  66. data/lib/parsanol/options.rb +21 -0
  67. data/lib/parsanol/parallel.rb +128 -0
  68. data/lib/parsanol/parser.rb +242 -0
  69. data/lib/parsanol/parslet.rb +151 -0
  70. data/lib/parsanol/pattern/binding.rb +91 -0
  71. data/lib/parsanol/pattern.rb +162 -0
  72. data/lib/parsanol/pool.rb +219 -0
  73. data/lib/parsanol/pools/array_pool.rb +75 -0
  74. data/lib/parsanol/pools/buffer_pool.rb +182 -0
  75. data/lib/parsanol/pools/position_pool.rb +92 -0
  76. data/lib/parsanol/pools/slice_pool.rb +64 -0
  77. data/lib/parsanol/position.rb +94 -0
  78. data/lib/parsanol/resettable.rb +29 -0
  79. data/lib/parsanol/result.rb +46 -0
  80. data/lib/parsanol/result_builder.rb +208 -0
  81. data/lib/parsanol/result_stream.rb +266 -0
  82. data/lib/parsanol/rig/rspec.rb +71 -0
  83. data/lib/parsanol/rope.rb +81 -0
  84. data/lib/parsanol/scope.rb +104 -0
  85. data/lib/parsanol/slice.rb +160 -0
  86. data/lib/parsanol/source/line_cache.rb +102 -0
  87. data/lib/parsanol/source.rb +185 -0
  88. data/lib/parsanol/source_location.rb +167 -0
  89. data/lib/parsanol/streaming_parser.rb +124 -0
  90. data/lib/parsanol/string_view.rb +198 -0
  91. data/lib/parsanol/transform.rb +226 -0
  92. data/lib/parsanol/version.rb +5 -0
  93. data/lib/parsanol/wasm/README.md +80 -0
  94. data/lib/parsanol/wasm/package.json +51 -0
  95. data/lib/parsanol/wasm/parsanol.js +252 -0
  96. data/lib/parsanol/wasm/parslet.d.ts +129 -0
  97. data/lib/parsanol/wasm_parser.rb +240 -0
  98. data/lib/parsanol.rb +278 -0
  99. data/parsanol.gemspec +67 -0
  100. metadata +279 -0
@@ -0,0 +1,604 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Parsanol
4
+ module Native
5
+ # Transforms native AST format to Parslet-compatible format
6
+ #
7
+ # Native format from Rust parser:
8
+ # - Slices: Parsanol::Slice objects (with position info)
9
+ # - Sequences: [":sequence", item1, item2, ...]
10
+ # - Repetitions: [":repetition", item1, item2, ...]
11
+ # - Named captures: {"name" => value}
12
+ #
13
+ # Parslet format:
14
+ # - Slices: Preserved as Parsanol::Slice (with position info)
15
+ # - Sequences: merged hash {:key1 => val1, :key2 => val2, ...}
16
+ # - Repetitions: array of items (or joined Slices if all are Slices)
17
+ # - Named wrapping Repetition: {:name => [{:name => item1}, {:name => item2}, ...]}
18
+ #
19
+ class AstTransformer
20
+ # Frozen string constants for tag comparisons (avoid allocations)
21
+ SEQUENCE_TAG = ":sequence"
22
+ REPETITION_TAG = ":repetition"
23
+ EMPTY_STRING = ""
24
+ EMPTY_ARRAY = [].freeze
25
+ EMPTY_HASH = {}.freeze
26
+
27
+ # Symbol cache to avoid repeated string-to-symbol conversions
28
+ # This is a class variable to share across all transformations
29
+ @@symbol_cache = {}
30
+
31
+ # Symbol tags from native parser
32
+ SEQUENCE_SYM = :sequence
33
+ REPETITION_SYM = :repetition
34
+ MAYBE_SYM = :maybe
35
+ MAYBE_TAG = ":maybe"
36
+
37
+ # `named` mirrors CanFlatten#flatten's named flag: inside a Named
38
+ # result (.as), an absent maybe flattens to nil; unnamed it flattens
39
+ # to "".
40
+ def self.transform(ast, named: false)
41
+ case ast
42
+ when Array
43
+ transform_array(ast, named: named)
44
+ when Hash
45
+ transform_hash(ast)
46
+ else
47
+ ast
48
+ end
49
+ end
50
+
51
+ # Batch transformation for multiple ASTs
52
+ # Provides better cache locality than transforming individually
53
+ def self.transform_batch(asts)
54
+ asts.map { |ast| transform(ast) }
55
+ end
56
+
57
+ # Convert string key to symbol with caching
58
+ def self.cached_symbol(key)
59
+ return key if key.is_a?(Symbol)
60
+
61
+ @@symbol_cache[key] ||= key.to_sym
62
+ end
63
+
64
+ def self.transform_array(arr, named: false)
65
+ return EMPTY_ARRAY if arr.empty? # Match Parsanol Ruby mode behavior
66
+
67
+ # Check if this is a tagged array from native parser
68
+ # Native parser produces Symbol tags: [:sequence, item1, item2, ...]
69
+ first = arr.first
70
+ if [SEQUENCE_SYM, SEQUENCE_TAG].include?(first)
71
+ # Optimized: transform items starting from index 1
72
+ # Avoid creating arr[1..] slice
73
+ len = arr.length
74
+ return EMPTY_ARRAY if len == 1
75
+
76
+ items = Array.new(len - 1)
77
+ i = 0
78
+ while i < len - 1
79
+ items[i] = transform(arr[i + 1])
80
+ i += 1
81
+ end
82
+ flatten_sequence(items)
83
+ elsif [REPETITION_SYM, REPETITION_TAG].include?(first)
84
+ # Optimized: transform items starting from index 1
85
+ len = arr.length
86
+ return EMPTY_ARRAY if len == 1
87
+
88
+ items = Array.new(len - 1)
89
+ i = 0
90
+ while i < len - 1
91
+ items[i] = transform(arr[i + 1])
92
+ i += 1
93
+ end
94
+ flatten_repetition(items)
95
+ elsif [MAYBE_SYM, MAYBE_TAG].include?(first)
96
+ # Maybe flattens to nil-or-value (named) or ""-or-value (unnamed),
97
+ # never to an array
98
+ len = arr.length
99
+ if len == 1
100
+ named ? nil : EMPTY_STRING
101
+ else
102
+ flattened = transform(arr[1])
103
+ named ? flattened : (flattened || EMPTY_STRING)
104
+ end
105
+ elsif first.is_a?(Symbol) || (first.is_a?(String) && first.start_with?(":"))
106
+ # Other tagged arrays - pass through
107
+ arr.map { |item| transform(item) }
108
+ else
109
+ # Untagged arrays from native parser are SEQUENCES
110
+ # Apply flatten_sequence to get Parslet-compatible output
111
+ items = arr.map { |item| transform(item) }
112
+ flatten_sequence(items)
113
+ end
114
+ end
115
+
116
+ def self.transform_hash(hash)
117
+ # Fast path: single-key hash (99.9% of cases from native parser)
118
+ # Native parser always produces single-key hashes: {"name" => value}
119
+ return transform_single_key_hash(hash) if hash.length == 1
120
+
121
+ # Slow path: multi-key hash (rare, from nested structures)
122
+ transform_multi_key_hash(hash)
123
+ end
124
+
125
+ # Optimized handling for single-key hashes (the common case)
126
+ def self.transform_single_key_hash(hash)
127
+ # Extract the single key-value pair without iteration
128
+ key = hash.keys.first
129
+ value = hash[key]
130
+ sym_key = cached_symbol(key)
131
+
132
+ # Transform the value
133
+ transformed = transform(value, named: true)
134
+
135
+ # Check if value is a tagged repetition from native parser
136
+ is_tagged_repetition = value.is_a?(Array) && !value.empty? &&
137
+ value.first.is_a?(String) && value.first == REPETITION_TAG
138
+
139
+ # Check RAW value for repetition pattern BEFORE transformation
140
+ # Array with items that all have the parent key
141
+ # e.g., [{x: 1}, {x: 2}] where parent key is :x
142
+ is_raw_array_repetition = value.is_a?(Array) && !value.empty? &&
143
+ value.all? do |item|
144
+ item.is_a?(Hash) && item.keys.length == 1 && item.key?(key)
145
+ end
146
+
147
+ # Empty array from native parser is a repetition result (not a sequence)
148
+ # Sequences produce arrays of arrays like [[], []], not empty arrays
149
+ is_empty_repetition = value.is_a?(Array) && value.empty?
150
+
151
+ # Special handling for arrays that look like character repetitions
152
+ # (arrays of single-character Slices/strings should be joined)
153
+ if transformed.is_a?(Array) && !transformed.empty? &&
154
+ transformed.all? do |item|
155
+ slice_or_string?(item) && item_length(item) == 1
156
+ end
157
+ # Join preserving position from first Slice
158
+ first_slice = transformed.find { |i| i.is_a?(::Parsanol::Slice) }
159
+ content = transformed.map { |i| slice_content(i) }.join
160
+ transformed = if first_slice
161
+ ::Parsanol::Slice.new(first_slice.offset, content,
162
+ first_slice.input)
163
+ else
164
+ content
165
+ end
166
+ end
167
+
168
+ # Check for UNTAGGED repetition pattern (native output):
169
+ # If array items all have the same key as parent, it's a repetition
170
+ is_transformed_repetition = transformed.is_a?(Array) && !transformed.empty? &&
171
+ transformed.all? do |item|
172
+ item.is_a?(Hash) && item.keys.length == 1 && item.key?(sym_key)
173
+ end
174
+
175
+ is_repetition = is_tagged_repetition || is_raw_array_repetition || is_transformed_repetition || is_empty_repetition
176
+
177
+ # Handle based on type
178
+ if is_repetition
179
+ transform_repetition_value(sym_key, transformed)
180
+ elsif transformed.is_a?(Hash)
181
+ { sym_key => transformed }
182
+ elsif transformed.is_a?(Array)
183
+ transform_array_value(sym_key, transformed)
184
+ else
185
+ # Simple value (Slice, string, nil, etc.) - most common case
186
+ { sym_key => transformed }
187
+ end
188
+ end
189
+
190
+ # Get content from Slice or string
191
+ def self.slice_content(value)
192
+ value.is_a?(::Parsanol::Slice) ? value.content : value.to_s
193
+ end
194
+
195
+ # Get length of Slice or string
196
+ def self.item_length(value)
197
+ value.is_a?(::Parsanol::Slice) ? value.length : value.to_s.length
198
+ end
199
+
200
+ # Handle repetition values (named wrapping repetition)
201
+ def self.transform_repetition_value(sym_key, transformed)
202
+ if transformed.is_a?(Array)
203
+ # Empty array from repetition stays as empty array
204
+ if transformed.empty?
205
+ { sym_key => EMPTY_ARRAY }
206
+ # Check if items already have the same key (avoid double-wrapping)
207
+ elsif transformed.all? do |item|
208
+ item.is_a?(Hash) && item.key?(sym_key)
209
+ end
210
+ { sym_key => transformed }
211
+ else
212
+ # Wrap each item with the name
213
+ { sym_key => transformed.map { |item| { sym_key => item } } }
214
+ end
215
+ elsif transformed.is_a?(::Parsanol::Slice) && transformed.empty?
216
+ { sym_key => EMPTY_ARRAY } # Empty repetition should be [], not empty Slice
217
+ elsif transformed.is_a?(String) && transformed == EMPTY_STRING
218
+ { sym_key => EMPTY_ARRAY } # Empty repetition should be [], not ""
219
+ else
220
+ { sym_key => transformed }
221
+ end
222
+ end
223
+
224
+ # Handle array values (non-repetition case)
225
+ def self.transform_array_value(sym_key, transformed)
226
+ if transformed.empty?
227
+ # For empty arrays, we need to determine if this is a repetition or sequence
228
+ # Repetitions should return [], sequences should return empty Slice
229
+ # We can't tell from the value alone, so we return empty Slice (sequence semantics)
230
+ # The repetition detection in transform_single_key_hash will handle the other case
231
+ { sym_key => ::Parsanol::Slice.new(0, EMPTY_STRING, nil) }
232
+ elsif transformed.all? do |v|
233
+ v.is_a?(Hash) && v.keys.length == 1 && v.key?(sym_key)
234
+ end
235
+ # Items already have the parent key (repetition pattern) - keep as-is
236
+ { sym_key => transformed }
237
+ elsif transformed.all?(Hash)
238
+ # Items are hashes with DIFFERENT keys (not the parent key)
239
+ # This is a repetition result from (separator >> item).repeat pattern
240
+ # The items already have their correct structure, DON'T wrap them
241
+ # Example: [{name: "b"}, {name: "c"}] for (str(',') >> item).repeat.as(:rest)
242
+ { sym_key => transformed }
243
+ else
244
+ { sym_key => transformed }
245
+ end
246
+ end
247
+
248
+ # Slow path: multi-key hash (rare)
249
+ def self.transform_multi_key_hash(hash)
250
+ result = {}
251
+
252
+ hash.each do |key, value|
253
+ sym_key = cached_symbol(key)
254
+
255
+ is_repetition = value.is_a?(Array) && !value.empty? &&
256
+ value.first.is_a?(String) && value.first == REPETITION_TAG
257
+
258
+ transformed = transform(value, named: true)
259
+
260
+ result[sym_key] = if is_repetition
261
+ if transformed.is_a?(Array)
262
+ if transformed.all? do |item|
263
+ item.is_a?(Hash) && item.key?(sym_key)
264
+ end
265
+ transformed
266
+ else
267
+ transformed.map { |item| { sym_key => item } }
268
+ end
269
+ elsif transformed == EMPTY_STRING
270
+ EMPTY_STRING
271
+ else
272
+ transformed
273
+ end
274
+ elsif transformed.is_a?(Hash)
275
+ transformed
276
+ elsif transformed.is_a?(Array)
277
+ if transformed.empty?
278
+ EMPTY_ARRAY
279
+ elsif transformed.all?(Hash)
280
+ transformed.map { |item| { sym_key => item } }
281
+ else
282
+ transformed
283
+ end
284
+ else
285
+ transformed
286
+ end
287
+ end
288
+
289
+ result
290
+ end
291
+
292
+ # Flatten sequence items according to Parslet semantics:
293
+ # 1. If ALL items are hashes, return as array (this is a repetition result)
294
+ # 2. If there are named captures (hashes) among Slices/strings, return ONLY the merged hash (discard Slices/strings)
295
+ # 3. If only Slices/strings, join them preserving position from first Slice
296
+ # 4. Return single value if only one item
297
+ #
298
+ # This matches Parslet's behavior where:
299
+ # str('SCHEMA') >> str(' ') >> match('[a-z]').repeat(1).as(:name) >> str(';')
300
+ # returns: {:name => "test"} (not ["SCHEMA ", {:name=>"test"}, ";"])
301
+ #
302
+ # But for repetitions with named captures:
303
+ # match('[a-z]').as(:x).repeat(2)
304
+ # returns: [{:x => "a"}, {:x => "b"}] (array of hashes, NOT merged!)
305
+ #
306
+ # Optimized: Single-pass with direct result building
307
+ def self.flatten_sequence(items)
308
+ return EMPTY_ARRAY if items.empty? # Match Parsanol Ruby mode
309
+ return items.first if items.length == 1
310
+
311
+ # Single pass: categorize items
312
+ merged_hash = {}
313
+ slice_or_string_parts = []
314
+ hash_count = 0
315
+ total_items = 0
316
+ has_non_empty_array = false
317
+
318
+ items.each do |item|
319
+ case item
320
+ when Hash
321
+ merged_hash.merge!(item)
322
+ hash_count += 1
323
+ total_items += 1
324
+ when ::Parsanol::Slice, String
325
+ slice_or_string_parts << item
326
+ total_items += 1
327
+ when Array
328
+ # Check if this is a non-empty array (repetition result with content)
329
+ # Parslet behavior: when a sequence contains a non-empty repetition,
330
+ # the WHOLE sequence should be kept as array, not merged.
331
+ if item.empty?
332
+ # Empty repetition - skip (sequence semantics: merge rest)
333
+ # Don't increment total_items for empty arrays
334
+ else
335
+ # Check if array contains only hashes (repetition wrapper pattern)
336
+ # In this case, merge the inner hashes into merged_hash
337
+ non_hash_items = item.grep_v(Hash)
338
+ all_items_are_hashes = non_hash_items.empty?
339
+
340
+ if all_items_are_hashes
341
+ # Check if merging would overwrite existing keys in merged_hash.
342
+ # If so, this is a repetition pattern (item >> (sep >> item).repeat)
343
+ # and should be kept as array, not merged.
344
+ # Example: merged_hash={namedTypeOrRename: A}, array=[{namedTypeOrRename: B}]
345
+ # → should produce [{namedTypeOrRename: A}, {namedTypeOrRename: B}]
346
+ existing_keys = merged_hash.keys
347
+ shares_keys = item.any? do |sub_item|
348
+ sub_item.is_a?(Hash) && sub_item.keys.intersect?(existing_keys)
349
+ end
350
+
351
+ if shares_keys
352
+ has_non_empty_array = true
353
+ item.each do |sub_item|
354
+ hash_count += 1 if sub_item.is_a?(Hash)
355
+ end
356
+ total_items += 1
357
+ else
358
+ item.each do |sub_item|
359
+ merged_hash.merge!(sub_item) if sub_item.is_a?(Hash)
360
+ end
361
+ end
362
+ else
363
+ # Non-empty repetition with non-hash items - mark that we should keep as array
364
+ has_non_empty_array = true
365
+ # Still collect items for potential array result
366
+ item.each do |sub_item|
367
+ case sub_item
368
+ when Hash
369
+ hash_count += 1
370
+ when ::Parsanol::Slice, String
371
+ slice_or_string_parts << sub_item
372
+ end
373
+ end
374
+ total_items += 1
375
+ end
376
+ end
377
+ when nil
378
+ # Skip nil values (from lookahead or optional that didn't match)
379
+ else
380
+ total_items += 1
381
+ end
382
+ end
383
+
384
+ # PARSLET SEQUENCE BEHAVIOR WITH REPETITIONS:
385
+ # If the sequence contains a non-empty repetition result (array with items),
386
+ # return as array instead of merging.
387
+ # Example: factor.as(:left) >> (op >> factor).as(:rhs).repeat
388
+ # With input "a+b" produces: [{left: {...}}, {rhs: {...}}]
389
+ # With input "a" produces: {left: {...}} (empty repetition, merge)
390
+ if has_non_empty_array
391
+ # Flatten the items: top-level hashes + array items
392
+ result = []
393
+ items.each do |item|
394
+ case item
395
+ when Hash
396
+ result << item
397
+ when Array
398
+ result.concat(item)
399
+ when ::Parsanol::Slice, String
400
+ # Skip unnamed Slices/strings when we have named captures
401
+ end
402
+ end
403
+ return result.length == 1 ? result.first : result
404
+ end
405
+
406
+ # KEY INSIGHT: If ALL items are hashes, we need to determine:
407
+ # 1. WRAPPER PATTERN: All hashes have the SAME single key, and values are HASHES
408
+ # => Merge the inner hashes under that key
409
+ # Example: [{:syntax => {:spaces => {...}}},
410
+ # {:syntax => {:schemaDecl => [...]}}]
411
+ # Result: {:syntax => {:spaces => {...}, :schemaDecl => [...]}}
412
+ #
413
+ # 2. REPETITION PATTERN: All hashes have the SAME single key, but values are SIMPLE
414
+ # => Keep as array (this is a repetition result)
415
+ # Example: [{:letter => "a"}, {:letter => "b"}, {:letter => "c"}]
416
+ # Result: [{:letter => "a"}, {:letter => "b"}, {:letter => "c"}]
417
+ #
418
+ # 3. MIXED KEYS: Hashes have DIFFERENT keys
419
+ # => Merge into single hash
420
+ # Example: [{:explicitAttr => {...}}, {:whereClause => {...}}]
421
+ # Result: {:explicitAttr => {...}, :whereClause => {...}}
422
+ if hash_count == total_items && hash_count > 1
423
+ # Check if all hashes have the same single key
424
+ first_item = items.first
425
+ if first_item.is_a?(Hash) && first_item.keys.length == 1
426
+ wrapper_key = first_item.keys.first
427
+
428
+ # Verify all items are hashes with the same single key
429
+ all_same_wrapper = items.all? do |item|
430
+ item.is_a?(Hash) && item.keys.length == 1 && item.keys.first == wrapper_key
431
+ end
432
+
433
+ if all_same_wrapper
434
+ # Check if values are all hashes (wrapper pattern) or not (repetition pattern)
435
+ all_values_are_hashes = items.all? do |item|
436
+ item[wrapper_key].is_a?(Hash)
437
+ end
438
+
439
+ return items unless all_values_are_hashes
440
+
441
+ # Check if inner hashes have the same keys or different keys
442
+ # REPETITION pattern (same keys like entity_decl): keep as array
443
+ # WRAPPER pattern (different keys like spaces vs schemaDecl): merge
444
+ first_inner_keys = items.first[wrapper_key].keys.to_set
445
+ items.all? do |item|
446
+ item[wrapper_key].keys.to_set == first_inner_keys
447
+ end
448
+
449
+ # Check if items have single keys or multiple keys
450
+ # - Single key items with repeated outer key = true repetition (keep array)
451
+ # - Multiple key items with repeated outer key = duplicate labels in sequence (merge)
452
+ max_keys_per_item = items.map do |item|
453
+ item.is_a?(Hash) ? item.keys.length : 0
454
+ end.max || 0
455
+
456
+ # Check if inner values are hashes with different keys
457
+ # This distinguishes:
458
+ # - True repetition: [{letter: 'a'}, {letter: 'b'}] - inner is string
459
+ # - Duplicate labels: [{group: {char: 'a'}}, {group: {digit: '5'}}] - inner is hash with different keys
460
+ inner_keys_all_same = true
461
+ first_inner_keys = nil
462
+ if items.all? do |item|
463
+ item.is_a?(Hash) && item[wrapper_key].is_a?(Hash)
464
+ end
465
+ first_inner_keys = items.first[wrapper_key].keys.to_set
466
+ inner_keys_all_same = items.all? do |item|
467
+ item[wrapper_key].keys.to_set == first_inner_keys
468
+ end
469
+ end
470
+
471
+ # DUPLICATE LABELS IN SEQUENCE: multiple keys per item with repeated outer key
472
+ # OR inner hashes with different keys
473
+ # Example: [{group: {char: 'a'}}, {group: {digit: '5'}}]
474
+ # Ruby semantics: merge with last value wins for the outer key
475
+ # This is different from true repetition where each item has exactly one key
476
+ has_duplicate_labels = max_keys_per_item > 1 || (first_inner_keys && !inner_keys_all_same)
477
+
478
+ # Check if inner hashes have the same keys or different keys
479
+ first_inner_keys ||= items.first[wrapper_key].keys.to_set
480
+ all_same_keys = items.all? do |item|
481
+ item[wrapper_key].keys.to_set == first_inner_keys
482
+ end
483
+
484
+ if has_duplicate_labels
485
+ # DUPLICATE LABELS PATTERN: items have multiple keys with repeated outer key
486
+ # OR inner hashes have different keys
487
+ # This is a SEQUENCE with duplicate .as() labels
488
+ # Ruby semantics: merge and keep last value for the outer key
489
+ merged = {}
490
+ items.each do |item|
491
+ item.each do |k, v|
492
+ merged[k] = v # Last value wins
493
+ end
494
+ end
495
+ # Return only the wrapper key with its last value
496
+ return { wrapper_key => merged[wrapper_key] }
497
+ elsif all_same_keys
498
+ # TRUE REPETITION: each item has exactly one key
499
+ # Keep as array of hashes
500
+ # Example: [{letter: 'a'}, {letter: 'b'}] or [{schemaDecl: ...}, {schemaDecl: ...}]
501
+ return items
502
+ else
503
+ # DIFFERENT INNER KEYS with single keys: Same outer key with different inner keys
504
+ # This is a WRAPPER pattern - keep all items as array
505
+ # Example: [{:syntax => {:entityDecl => ...}}, {:syntax => {:typeDecl => ...}}]
506
+ # Should NOT merge or drop items - keep all declarations
507
+ return items
508
+ end
509
+
510
+ return items unless all_values_are_hashes
511
+
512
+ # Repetition pattern: keep as array
513
+
514
+ end
515
+ end
516
+
517
+ # MIXED KEYS: Hashes have different keys
518
+ # Parslet sequence semantics: merge into single hash
519
+ return merged_hash
520
+ end
521
+
522
+ # PARSLET SEQUENCE SEMANTICS:
523
+ # If there are named captures (hashes) mixed with other things,
524
+ # return ONLY the merged hash (discard unnamed Slices/strings)
525
+ return merged_hash unless merged_hash.empty?
526
+
527
+ # No named captures - handle Slices/strings and other items
528
+ if slice_or_string_parts.any?
529
+ # Join Slices/strings, preserving position from first Slice
530
+ first_slice = slice_or_string_parts.find { |i| i.is_a?(::Parsanol::Slice) }
531
+ content = slice_or_string_parts.map do |i|
532
+ i.is_a?(::Parsanol::Slice) ? i.content : i
533
+ end.join
534
+
535
+ if first_slice
536
+ # Create new Slice with combined content, preserving position from first
537
+ return ::Parsanol::Slice.new(first_slice.offset, content,
538
+ first_slice.input)
539
+ else
540
+ # All plain strings (shouldn't happen with new decode_flat, but handle it)
541
+ return slice_or_string_parts.length == 1 ? slice_or_string_parts.first : content
542
+ end
543
+ end
544
+
545
+ # Only other items (arrays, etc.)
546
+ return EMPTY_ARRAY if total_items.zero?
547
+
548
+ items.length == 1 ? items.first : items
549
+ end
550
+
551
+ # Parslet/Parsanol repetition semantics:
552
+ # 1. Return [] for empty repetitions
553
+ # 2. If all items are Slices (or strings), join them preserving position
554
+ # 3. Otherwise return array
555
+ def self.flatten_repetition(items)
556
+ return EMPTY_ARRAY if items.empty?
557
+
558
+ # Single-pass flatten and check
559
+ flat_items = []
560
+ all_slices_or_strings = true
561
+
562
+ items.each do |item|
563
+ if item.is_a?(Array)
564
+ item.each do |sub|
565
+ flat_items << sub
566
+ all_slices_or_strings = false unless slice_or_string?(sub)
567
+ end
568
+ else
569
+ flat_items << item
570
+ all_slices_or_strings = false unless slice_or_string?(item)
571
+ end
572
+ end
573
+
574
+ return EMPTY_ARRAY if flat_items.empty?
575
+
576
+ # If all Slices or strings, join them preserving position from first Slice
577
+ if all_slices_or_strings
578
+ first_slice = flat_items.find { |i| i.is_a?(::Parsanol::Slice) }
579
+ content = flat_items.map do |i|
580
+ i.is_a?(::Parsanol::Slice) ? i.content : i
581
+ end.join
582
+
583
+ if first_slice
584
+ # Create new Slice with combined content, preserving position from first
585
+ ::Parsanol::Slice.new(first_slice.offset, content,
586
+ first_slice.input)
587
+ else
588
+ # All plain strings (shouldn't happen with new decode_flat, but handle it)
589
+ content
590
+ end
591
+ else
592
+ flat_items
593
+ end
594
+ end
595
+
596
+ # Check if value is a Slice or String
597
+ def self.slice_or_string?(value)
598
+ value.is_a?(::Parsanol::Slice) || value.is_a?(String)
599
+ end
600
+ end
601
+
602
+ private_constant :AstTransformer
603
+ end
604
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Parsanol
4
+ module Native
5
+ # Type tags used in AST serialization
6
+ # These must match the tags used by the Rust parser
7
+ module Types
8
+ # AST node type tags (must match Rust parser output)
9
+ TAG_NIL = 0x00
10
+ TAG_BOOL = 0x01
11
+ TAG_INT = 0x02
12
+ TAG_FLOAT = 0x03
13
+ TAG_STRING_REF = 0x04
14
+ TAG_ARRAY_START = 0x05
15
+ TAG_ARRAY_END = 0x06
16
+ TAG_HASH_START = 0x07
17
+ TAG_HASH_END = 0x08
18
+ TAG_HASH_KEY = 0x09
19
+ TAG_INLINE_STRING = 0x0A
20
+
21
+ # Frozen string constants for transformer (avoid allocations)
22
+ SEQUENCE_TAG = ":sequence"
23
+ REPETITION_TAG = ":repetition"
24
+ EMPTY_STRING = ""
25
+ EMPTY_ARRAY = [].freeze
26
+ EMPTY_HASH = {}.freeze
27
+ end
28
+ end
29
+ end