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.
- checksums.yaml +7 -0
- data/HISTORY.txt +98 -0
- data/LICENSE +23 -0
- data/README.adoc +905 -0
- data/Rakefile +141 -0
- data/lib/parsanol/3.2/parsanol_native.so +0 -0
- data/lib/parsanol/3.3/parsanol_native.so +0 -0
- data/lib/parsanol/3.4/parsanol_native.so +0 -0
- data/lib/parsanol/4.0/parsanol_native.so +0 -0
- data/lib/parsanol/ast_visitor.rb +122 -0
- data/lib/parsanol/atoms/alternative.rb +123 -0
- data/lib/parsanol/atoms/base.rb +208 -0
- data/lib/parsanol/atoms/can_flatten.rb +194 -0
- data/lib/parsanol/atoms/capture.rb +38 -0
- data/lib/parsanol/atoms/context.rb +357 -0
- data/lib/parsanol/atoms/context_optimized.rb +38 -0
- data/lib/parsanol/atoms/custom.rb +110 -0
- data/lib/parsanol/atoms/cut.rb +66 -0
- data/lib/parsanol/atoms/dsl.rb +96 -0
- data/lib/parsanol/atoms/dynamic.rb +39 -0
- data/lib/parsanol/atoms/entity.rb +75 -0
- data/lib/parsanol/atoms/ignored.rb +37 -0
- data/lib/parsanol/atoms/infix.rb +167 -0
- data/lib/parsanol/atoms/lookahead.rb +85 -0
- data/lib/parsanol/atoms/named.rb +74 -0
- data/lib/parsanol/atoms/re.rb +83 -0
- data/lib/parsanol/atoms/repetition.rb +277 -0
- data/lib/parsanol/atoms/scope.rb +35 -0
- data/lib/parsanol/atoms/sequence.rb +195 -0
- data/lib/parsanol/atoms/str.rb +109 -0
- data/lib/parsanol/atoms/visitor.rb +91 -0
- data/lib/parsanol/atoms.rb +46 -0
- data/lib/parsanol/buffer.rb +133 -0
- data/lib/parsanol/builder_callbacks.rb +353 -0
- data/lib/parsanol/cause.rb +122 -0
- data/lib/parsanol/context.rb +39 -0
- data/lib/parsanol/convenience.rb +36 -0
- data/lib/parsanol/edit_tracker.rb +111 -0
- data/lib/parsanol/error_reporter/contextual.rb +99 -0
- data/lib/parsanol/error_reporter/deepest.rb +120 -0
- data/lib/parsanol/error_reporter/tree.rb +63 -0
- data/lib/parsanol/error_reporter.rb +100 -0
- data/lib/parsanol/expression/treetop.rb +154 -0
- data/lib/parsanol/expression.rb +106 -0
- data/lib/parsanol/fast_mode.rb +187 -0
- data/lib/parsanol/first_set.rb +79 -0
- data/lib/parsanol/grammar_builder.rb +179 -0
- data/lib/parsanol/incremental_parser.rb +182 -0
- data/lib/parsanol/interval_tree.rb +226 -0
- data/lib/parsanol/lazy_result.rb +179 -0
- data/lib/parsanol/mermaid.rb +142 -0
- data/lib/parsanol/native/batch_decoder.rb +255 -0
- data/lib/parsanol/native/dynamic.rb +238 -0
- data/lib/parsanol/native/parser.rb +102 -0
- data/lib/parsanol/native/serializer.rb +252 -0
- data/lib/parsanol/native/transformer.rb +604 -0
- data/lib/parsanol/native/types.rb +29 -0
- data/lib/parsanol/native.rb +223 -0
- data/lib/parsanol/optimizer.rb +85 -0
- data/lib/parsanol/optimizers/choice_optimizer.rb +78 -0
- data/lib/parsanol/optimizers/cut_inserter.rb +182 -0
- data/lib/parsanol/optimizers/lookahead_optimizer.rb +56 -0
- data/lib/parsanol/optimizers/quantifier_optimizer.rb +60 -0
- data/lib/parsanol/optimizers/sequence_optimizer.rb +97 -0
- data/lib/parsanol/options/zero_copy.rb +127 -0
- data/lib/parsanol/options.rb +21 -0
- data/lib/parsanol/parallel.rb +128 -0
- data/lib/parsanol/parser.rb +242 -0
- data/lib/parsanol/parslet.rb +151 -0
- data/lib/parsanol/pattern/binding.rb +91 -0
- data/lib/parsanol/pattern.rb +162 -0
- data/lib/parsanol/pool.rb +219 -0
- data/lib/parsanol/pools/array_pool.rb +75 -0
- data/lib/parsanol/pools/buffer_pool.rb +182 -0
- data/lib/parsanol/pools/position_pool.rb +92 -0
- data/lib/parsanol/pools/slice_pool.rb +64 -0
- data/lib/parsanol/position.rb +94 -0
- data/lib/parsanol/resettable.rb +29 -0
- data/lib/parsanol/result.rb +46 -0
- data/lib/parsanol/result_builder.rb +208 -0
- data/lib/parsanol/result_stream.rb +266 -0
- data/lib/parsanol/rig/rspec.rb +71 -0
- data/lib/parsanol/rope.rb +81 -0
- data/lib/parsanol/scope.rb +104 -0
- data/lib/parsanol/slice.rb +160 -0
- data/lib/parsanol/source/line_cache.rb +102 -0
- data/lib/parsanol/source.rb +185 -0
- data/lib/parsanol/source_location.rb +167 -0
- data/lib/parsanol/streaming_parser.rb +124 -0
- data/lib/parsanol/string_view.rb +198 -0
- data/lib/parsanol/transform.rb +226 -0
- data/lib/parsanol/version.rb +5 -0
- data/lib/parsanol/wasm/README.md +80 -0
- data/lib/parsanol/wasm/package.json +51 -0
- data/lib/parsanol/wasm/parsanol.js +252 -0
- data/lib/parsanol/wasm/parslet.d.ts +129 -0
- data/lib/parsanol/wasm_parser.rb +240 -0
- data/lib/parsanol.rb +278 -0
- data/parsanol.gemspec +67 -0
- 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
|