parsanol 1.3.13-arm-linux → 1.3.15-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 +4 -4
- 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/atoms/base.rb +26 -1
- data/lib/parsanol/atoms/can_flatten.rb +64 -14
- data/lib/parsanol/atoms/repetition.rb +18 -21
- data/lib/parsanol/atoms/sequence.rb +10 -19
- data/lib/parsanol/native/parser.rb +32 -4
- data/lib/parsanol/native/transformer.rb +8 -7
- data/lib/parsanol/native.rb +43 -17
- data/lib/parsanol/parser.rb +27 -23
- data/lib/parsanol/version.rb +1 -1
- data/lib/parsanol/vm.rb +962 -0
- data/lib/parsanol.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e98e448bc4374451a62e175387759b4db66c90160e5df3204e652c17a751ddd7
|
|
4
|
+
data.tar.gz: 7f081c2cd9e6fac46cd4e43d15c91a6755af3aad8b01c35b5cb542d190cf2786
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 682268466119952e99a678a79270d6fe69a449630665a8e082e7f01f709903f6190552053e6328ec74f47fd452d1b946bedd5b89687a20594debbe33a6e9a2d2
|
|
7
|
+
data.tar.gz: 386e99e3a11788991e7b1fc5fb0ae55a6c95caa0b60508c62c3f997492c213887135d9d7b4bcb04a72b7d9d8c8bd20a9b20b105b6a2e31cc78b814db32380306
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
data/lib/parsanol/atoms/base.rb
CHANGED
|
@@ -30,9 +30,34 @@ module Parsanol
|
|
|
30
30
|
# @return [Object] the parsed result
|
|
31
31
|
# @raise [Parsanol::ParseFailed] on parse failure
|
|
32
32
|
def parse(source, options = {})
|
|
33
|
-
input = normalize_input(source)
|
|
34
33
|
must_consume_all = !options[:prefix]
|
|
35
34
|
|
|
35
|
+
# Compiled-VM fast path: String input, full-consumption semantics.
|
|
36
|
+
# On failure (or unsupported grammar / budget exhaustion) fall
|
|
37
|
+
# through to the interpreter, which also produces the exact
|
|
38
|
+
# cause-tree diagnostics.
|
|
39
|
+
if must_consume_all && source.is_a?(String) &&
|
|
40
|
+
(program = VM.program_for(self))
|
|
41
|
+
result = VM.run(program, source, true)
|
|
42
|
+
if result == VM::BAIL
|
|
43
|
+
# Internal bail: fall back to the interpreter and skip the VM
|
|
44
|
+
# for this grammar from now on.
|
|
45
|
+
VM.disable_for!(self)
|
|
46
|
+
elsif result.first == :heavy
|
|
47
|
+
# Succeeded but burned >1/8 of the step budget —
|
|
48
|
+
# heavy-backtracking grammar; the memoizing interpreter is
|
|
49
|
+
# the better engine from now on.
|
|
50
|
+
VM.disable_for!(self)
|
|
51
|
+
return finalize_result(result[1])
|
|
52
|
+
elsif result.first
|
|
53
|
+
return finalize_result(result[1])
|
|
54
|
+
end
|
|
55
|
+
# Clean [false, nil]: the input does not parse — the interpreter
|
|
56
|
+
# reparse below produces the detailed cause tree.
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
input = normalize_input(source)
|
|
60
|
+
|
|
36
61
|
# Initial parse attempt (no error collection)
|
|
37
62
|
success, value = run_with_context(input, nil, must_consume_all)
|
|
38
63
|
return finalize_result(value) if success
|
|
@@ -65,18 +65,25 @@ module Parsanol
|
|
|
65
65
|
raise "BUG: Unknown tag #{tag.inspect}."
|
|
66
66
|
end
|
|
67
67
|
|
|
68
|
-
# Lisp style fold left where the first element builds the
|
|
69
|
-
# an inject.
|
|
68
|
+
# Lisp style fold left where the first non-nil element builds the
|
|
69
|
+
# basis for an inject. Nil entries are skipped in a single pass so
|
|
70
|
+
# callers do not need an intermediate #compact.
|
|
70
71
|
#
|
|
71
72
|
def foldl(list)
|
|
72
73
|
len = list.size
|
|
73
|
-
|
|
74
|
-
|
|
74
|
+
i = 0
|
|
75
|
+
while i < len && list[i].nil?
|
|
76
|
+
i += 1
|
|
77
|
+
end
|
|
78
|
+
return "" if i >= len
|
|
79
|
+
|
|
80
|
+
result = list[i]
|
|
81
|
+
i += 1
|
|
82
|
+
return result if i >= len # Fast path for single non-nil element
|
|
75
83
|
|
|
76
|
-
result = list[0]
|
|
77
|
-
i = 1
|
|
78
84
|
while i < len
|
|
79
|
-
|
|
85
|
+
e = list[i]
|
|
86
|
+
result = yield(result, e) unless e.nil?
|
|
80
87
|
i += 1
|
|
81
88
|
end
|
|
82
89
|
result
|
|
@@ -87,7 +94,7 @@ module Parsanol
|
|
|
87
94
|
# @api private
|
|
88
95
|
#
|
|
89
96
|
def flatten_sequence(list)
|
|
90
|
-
foldl(list
|
|
97
|
+
foldl(list) do |r, e|
|
|
91
98
|
merge_fold(r, e)
|
|
92
99
|
end
|
|
93
100
|
end
|
|
@@ -161,22 +168,65 @@ module Parsanol
|
|
|
161
168
|
if has_hash
|
|
162
169
|
# If keyed subtrees are in the array, we'll want to discard all
|
|
163
170
|
# strings inbetween. To keep them, name them.
|
|
164
|
-
|
|
171
|
+
hashes = []
|
|
172
|
+
i = 0
|
|
173
|
+
while i < len
|
|
174
|
+
e = list[i]
|
|
175
|
+
hashes << e if e.instance_of?(Hash)
|
|
176
|
+
i += 1
|
|
177
|
+
end
|
|
178
|
+
return hashes
|
|
165
179
|
end
|
|
166
180
|
|
|
167
181
|
if has_array
|
|
168
182
|
# If any arrays are nested in this array, flatten all arrays to this
|
|
169
183
|
# level.
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
184
|
+
flat = []
|
|
185
|
+
i = 0
|
|
186
|
+
while i < len
|
|
187
|
+
e = list[i]
|
|
188
|
+
if e.instance_of?(Array)
|
|
189
|
+
flat.concat(e)
|
|
190
|
+
end
|
|
191
|
+
i += 1
|
|
192
|
+
end
|
|
193
|
+
return flat
|
|
173
194
|
end
|
|
174
195
|
|
|
175
196
|
# Consistent handling of empty lists, when we act on a named result
|
|
176
197
|
return [] if named && list.empty?
|
|
177
198
|
|
|
178
|
-
#
|
|
179
|
-
|
|
199
|
+
# All-string (or Slice) repetition: join once instead of O(n)
|
|
200
|
+
# intermediate Slice allocations via repeated #+.
|
|
201
|
+
join_string_list(list)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Join a list of string-like values into a single Slice (or String),
|
|
205
|
+
# preserving the first Slice's offset and input. Nils are skipped.
|
|
206
|
+
def join_string_list(list)
|
|
207
|
+
first_slice = nil
|
|
208
|
+
content = nil
|
|
209
|
+
i = 0
|
|
210
|
+
len = list.size
|
|
211
|
+
while i < len
|
|
212
|
+
e = list[i]
|
|
213
|
+
unless e.nil?
|
|
214
|
+
if first_slice.nil? && e.instance_of?(Parsanol::Slice)
|
|
215
|
+
first_slice = e
|
|
216
|
+
end
|
|
217
|
+
piece = e.instance_of?(Parsanol::Slice) ? e.content : e.to_s
|
|
218
|
+
if content.nil?
|
|
219
|
+
content = +piece
|
|
220
|
+
else
|
|
221
|
+
content << piece
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
i += 1
|
|
225
|
+
end
|
|
226
|
+
return "" if content.nil?
|
|
227
|
+
return first_slice.class.new(first_slice.offset, content, first_slice.input) if first_slice
|
|
228
|
+
|
|
229
|
+
content
|
|
180
230
|
end
|
|
181
231
|
|
|
182
232
|
# That annoying warning 'Duplicate subtrees while merging result' comes
|
|
@@ -171,16 +171,15 @@ module Parsanol
|
|
|
171
171
|
context.err_at(self, source, @min_error, source.bytepos, [v3])
|
|
172
172
|
end
|
|
173
173
|
|
|
174
|
-
# General repetition
|
|
174
|
+
# General repetition: builds the tagged result Array in place so
|
|
175
|
+
# finalize_result can flatten without an intermediate Buffer/LazyResult
|
|
176
|
+
# materialization.
|
|
175
177
|
def try_general(source, context, consume_all)
|
|
176
178
|
start_pos = source.bytepos
|
|
177
179
|
occurrence = 0
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
buffer = context.acquire_buffer(size: estimate + 1)
|
|
182
|
-
buffer.push(@result_tag)
|
|
183
|
-
|
|
180
|
+
# Pre-size for the tag + a modest run; Array growth is amortized.
|
|
181
|
+
result = Array.new([@max || 8, 8].min + 1)
|
|
182
|
+
result[0] = @result_tag
|
|
184
183
|
last_error = nil
|
|
185
184
|
|
|
186
185
|
loop do
|
|
@@ -190,14 +189,13 @@ module Parsanol
|
|
|
190
189
|
break unless success
|
|
191
190
|
|
|
192
191
|
occurrence += 1
|
|
193
|
-
|
|
192
|
+
result[occurrence] = value
|
|
194
193
|
|
|
195
194
|
break if @max && occurrence >= @max
|
|
196
195
|
end
|
|
197
196
|
|
|
198
197
|
# Check minimum bound
|
|
199
198
|
if occurrence < @min
|
|
200
|
-
context.release_buffer(buffer)
|
|
201
199
|
source.bytepos = start_pos
|
|
202
200
|
return context.err_at(self, source, @min_error, start_pos,
|
|
203
201
|
[last_error])
|
|
@@ -205,11 +203,12 @@ module Parsanol
|
|
|
205
203
|
|
|
206
204
|
# Check complete consumption
|
|
207
205
|
if consume_all && source.chars_left.positive?
|
|
208
|
-
context.release_buffer(buffer)
|
|
209
206
|
return context.err(self, source, @extra_error, [last_error])
|
|
210
207
|
end
|
|
211
208
|
|
|
212
|
-
|
|
209
|
+
# Trim to the actual filled length (tag + occurrence matches).
|
|
210
|
+
result.pop(result.size - occurrence - 1) if result.size > occurrence + 1
|
|
211
|
+
ok(result)
|
|
213
212
|
end
|
|
214
213
|
|
|
215
214
|
# Tree memoization for GPEG-style caching
|
|
@@ -227,23 +226,21 @@ module Parsanol
|
|
|
227
226
|
|
|
228
227
|
# Parse and cache
|
|
229
228
|
occurrence = 0
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
buffer.push(@result_tag)
|
|
229
|
+
result = Array.new([@max || 8, 8].min + 1)
|
|
230
|
+
result[0] = @result_tag
|
|
233
231
|
|
|
234
232
|
positions = context.acquire_array
|
|
235
233
|
positions << start_pos
|
|
236
234
|
last_error = nil
|
|
237
235
|
|
|
238
236
|
loop do
|
|
239
|
-
source.bytepos
|
|
240
237
|
success, value = @parslet.apply(source, context, false)
|
|
241
238
|
last_error = value
|
|
242
239
|
|
|
243
240
|
break unless success
|
|
244
241
|
|
|
245
242
|
occurrence += 1
|
|
246
|
-
|
|
243
|
+
result[occurrence] = value
|
|
247
244
|
positions << source.bytepos
|
|
248
245
|
|
|
249
246
|
break if @max && occurrence >= @max
|
|
@@ -252,13 +249,13 @@ module Parsanol
|
|
|
252
249
|
# Cache successful prefix
|
|
253
250
|
if occurrence.positive?
|
|
254
251
|
end_pos = positions[occurrence]
|
|
255
|
-
context.store_tree_memo(cache_key, start_pos,
|
|
256
|
-
end_pos)
|
|
252
|
+
context.store_tree_memo(cache_key, start_pos,
|
|
253
|
+
result[1, occurrence], end_pos)
|
|
257
254
|
end
|
|
255
|
+
context.release_array(positions) if context.respond_to?(:release_array)
|
|
258
256
|
|
|
259
257
|
# Check minimum
|
|
260
258
|
if occurrence < @min
|
|
261
|
-
context.release_buffer(buffer)
|
|
262
259
|
source.bytepos = start_pos
|
|
263
260
|
return context.err_at(self, source, @min_error, start_pos,
|
|
264
261
|
[last_error])
|
|
@@ -266,11 +263,11 @@ module Parsanol
|
|
|
266
263
|
|
|
267
264
|
# Check consumption
|
|
268
265
|
if consume_all && source.chars_left.positive?
|
|
269
|
-
context.release_buffer(buffer)
|
|
270
266
|
return context.err(self, source, @extra_error, [last_error])
|
|
271
267
|
end
|
|
272
268
|
|
|
273
|
-
|
|
269
|
+
result.pop(result.size - occurrence - 1) if result.size > occurrence + 1
|
|
270
|
+
ok(result)
|
|
274
271
|
end
|
|
275
272
|
end
|
|
276
273
|
end
|
|
@@ -105,7 +105,7 @@ module Parsanol
|
|
|
105
105
|
ok([:sequence, value])
|
|
106
106
|
end
|
|
107
107
|
|
|
108
|
-
# Two-element sequence
|
|
108
|
+
# Two-element sequence
|
|
109
109
|
def match_pair(p1, p2, source, context, consume_all)
|
|
110
110
|
success, v1 = p1.apply(source, context, false)
|
|
111
111
|
return context.err(self, source, @fail_msg, [v1]) unless success
|
|
@@ -113,14 +113,10 @@ module Parsanol
|
|
|
113
113
|
success, v2 = p2.apply(source, context, consume_all)
|
|
114
114
|
return context.err(self, source, @fail_msg, [v2]) unless success
|
|
115
115
|
|
|
116
|
-
|
|
117
|
-
buffer.push(:sequence)
|
|
118
|
-
buffer.push(v1)
|
|
119
|
-
buffer.push(v2)
|
|
120
|
-
ok(Parsanol::LazyResult.new(buffer, context))
|
|
116
|
+
ok([:sequence, v1, v2])
|
|
121
117
|
end
|
|
122
118
|
|
|
123
|
-
# Three-element sequence
|
|
119
|
+
# Three-element sequence
|
|
124
120
|
def match_triple(p1, p2, p3, source, context, consume_all)
|
|
125
121
|
success, v1 = p1.apply(source, context, false)
|
|
126
122
|
return context.err(self, source, @fail_msg, [v1]) unless success
|
|
@@ -131,20 +127,16 @@ module Parsanol
|
|
|
131
127
|
success, v3 = p3.apply(source, context, consume_all)
|
|
132
128
|
return context.err(self, source, @fail_msg, [v3]) unless success
|
|
133
129
|
|
|
134
|
-
|
|
135
|
-
buffer.push(:sequence)
|
|
136
|
-
buffer.push(v1)
|
|
137
|
-
buffer.push(v2)
|
|
138
|
-
buffer.push(v3)
|
|
139
|
-
ok(Parsanol::LazyResult.new(buffer, context))
|
|
130
|
+
ok([:sequence, v1, v2, v3])
|
|
140
131
|
end
|
|
141
132
|
|
|
142
133
|
# General case for N elements
|
|
143
134
|
def match_general(components, source, context, consume_all)
|
|
144
|
-
|
|
145
|
-
|
|
135
|
+
n = components.size
|
|
136
|
+
result = Array.new(n + 1)
|
|
137
|
+
result[0] = :sequence
|
|
146
138
|
|
|
147
|
-
last_idx =
|
|
139
|
+
last_idx = n - 1
|
|
148
140
|
idx = 0
|
|
149
141
|
|
|
150
142
|
while idx <= last_idx
|
|
@@ -152,15 +144,14 @@ module Parsanol
|
|
|
152
144
|
success, value = components[idx].apply(source, context, must_consume)
|
|
153
145
|
|
|
154
146
|
unless success
|
|
155
|
-
context.release_buffer(buffer)
|
|
156
147
|
return context.err(self, source, @fail_msg, [value])
|
|
157
148
|
end
|
|
158
149
|
|
|
159
|
-
|
|
150
|
+
result[idx + 1] = value
|
|
160
151
|
idx += 1
|
|
161
152
|
end
|
|
162
153
|
|
|
163
|
-
ok(
|
|
154
|
+
ok(result)
|
|
164
155
|
end
|
|
165
156
|
|
|
166
157
|
# Merges adjacent string atoms using Rope for efficiency
|
|
@@ -8,6 +8,9 @@ module Parsanol
|
|
|
8
8
|
module Parser
|
|
9
9
|
GRAMMAR_HASH_CACHE = Hash.new
|
|
10
10
|
GRAMMAR_CACHE = Hash.new
|
|
11
|
+
# structure-hash => Rust-side grammar handle. Keyed by content so a
|
|
12
|
+
# recycled object_id can never alias a different grammar.
|
|
13
|
+
HANDLE_CACHE = Hash.new
|
|
11
14
|
|
|
12
15
|
class << self
|
|
13
16
|
@cached_available = nil
|
|
@@ -40,26 +43,51 @@ module Parsanol
|
|
|
40
43
|
|
|
41
44
|
# Serialize a Ruby grammar to JSON (cached).
|
|
42
45
|
def serialize_grammar(root_atom)
|
|
43
|
-
root_atom
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
grammar_json(root_atom)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Resolve a Rust-side handle for the grammar, registering it once.
|
|
50
|
+
# Per-call cost is the memoized structure-hash lookup plus one Hash
|
|
51
|
+
# access — no JSON marshal, no re-hash inside Rust.
|
|
52
|
+
def grammar_handle(root_atom)
|
|
53
|
+
cache_key = grammar_cache_key(root_atom)
|
|
54
|
+
HANDLE_CACHE[cache_key] ||= Native._register_grammar(grammar_json(root_atom))
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Drop a handle whose Rust-side entry no longer exists; the next
|
|
58
|
+
# grammar_handle call re-registers.
|
|
59
|
+
def invalidate_handle(handle)
|
|
60
|
+
HANDLE_CACHE.reject! { |_key, cached| cached == handle }
|
|
47
61
|
end
|
|
48
62
|
|
|
49
63
|
def clear_cache
|
|
50
64
|
GRAMMAR_HASH_CACHE.clear
|
|
51
65
|
GRAMMAR_CACHE.clear
|
|
66
|
+
HANDLE_CACHE.clear
|
|
52
67
|
end
|
|
53
68
|
|
|
54
69
|
def cache_stats
|
|
55
70
|
{
|
|
56
71
|
hash_cache_size: GRAMMAR_HASH_CACHE.size,
|
|
57
72
|
grammar_cache_size: GRAMMAR_CACHE.size,
|
|
73
|
+
handle_cache_size: HANDLE_CACHE.size,
|
|
58
74
|
}
|
|
59
75
|
end
|
|
60
76
|
|
|
61
77
|
private
|
|
62
78
|
|
|
79
|
+
def grammar_cache_key(root_atom)
|
|
80
|
+
root_atom = root_atom.root if root_atom.is_a?(::Parsanol::Parser)
|
|
81
|
+
obj_id = root_atom.object_id
|
|
82
|
+
GRAMMAR_HASH_CACHE[obj_id] ||= grammar_structure_hash(root_atom)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def grammar_json(root_atom)
|
|
86
|
+
root_atom = root_atom.root if root_atom.is_a?(::Parsanol::Parser)
|
|
87
|
+
cache_key = grammar_cache_key(root_atom)
|
|
88
|
+
GRAMMAR_CACHE[cache_key] ||= GrammarSerializer.serialize(root_atom)
|
|
89
|
+
end
|
|
90
|
+
|
|
63
91
|
def grammar_structure_hash(atom)
|
|
64
92
|
Digest::MD5.hexdigest(atom_structure(atom).to_s)
|
|
65
93
|
end
|
|
@@ -132,9 +132,11 @@ module Parsanol
|
|
|
132
132
|
# Transform the value
|
|
133
133
|
transformed = transform(value, named: true)
|
|
134
134
|
|
|
135
|
-
# Check if value is a tagged repetition from native parser
|
|
135
|
+
# Check if value is a tagged repetition from native parser.
|
|
136
|
+
# The Rust handle path tags with Symbols, the batch decoder with
|
|
137
|
+
# ":repetition" Strings — accept both.
|
|
136
138
|
is_tagged_repetition = value.is_a?(Array) && !value.empty? &&
|
|
137
|
-
|
|
139
|
+
[REPETITION_SYM, REPETITION_TAG].include?(value.first)
|
|
138
140
|
|
|
139
141
|
# Check RAW value for repetition pattern BEFORE transformation
|
|
140
142
|
# Array with items that all have the parent key
|
|
@@ -203,13 +205,12 @@ module Parsanol
|
|
|
203
205
|
# Empty array from repetition stays as empty array
|
|
204
206
|
if transformed.empty?
|
|
205
207
|
{ sym_key => EMPTY_ARRAY }
|
|
206
|
-
#
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
208
|
+
# Hash items already carry their own capture names — a
|
|
209
|
+
# repetition of named captures keeps them as-is (parslet
|
|
210
|
+
# semantics); only unnamed items get the parent name per item.
|
|
211
|
+
elsif transformed.all?(Hash)
|
|
210
212
|
{ sym_key => transformed }
|
|
211
213
|
else
|
|
212
|
-
# Wrap each item with the name
|
|
213
214
|
{ sym_key => transformed.map { |item| { sym_key => item } } }
|
|
214
215
|
end
|
|
215
216
|
elsif transformed.is_a?(::Parsanol::Slice) && transformed.empty?
|
data/lib/parsanol/native.rb
CHANGED
|
@@ -38,22 +38,14 @@ module Parsanol
|
|
|
38
38
|
def parse(grammar, input)
|
|
39
39
|
raise LoadError, "Native parser not available" unless available?
|
|
40
40
|
|
|
41
|
-
#
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
# Use _parse_raw which returns properly tagged Ruby arrays via transform_ast.
|
|
49
|
-
# The batch format doesn't preserve :repetition/:sequence tags, so we use
|
|
50
|
-
# the direct FFI path. Apply the Ruby transformer to handle tags correctly.
|
|
51
|
-
begin
|
|
52
|
-
raw_ast = _parse_raw(grammar_json, input)
|
|
53
|
-
rescue RuntimeError => e
|
|
54
|
-
raise_native_parse_error(e, grammar, input)
|
|
41
|
+
# Both sub-methods return the final decoded tree; on native failure
|
|
42
|
+
# they fall back to the pure-Ruby parser, whose result is already
|
|
43
|
+
# final and must not be transformed again.
|
|
44
|
+
if grammar.is_a?(String)
|
|
45
|
+
parse_json_grammar(grammar, input)
|
|
46
|
+
else
|
|
47
|
+
parse_atom_grammar(grammar, input)
|
|
55
48
|
end
|
|
56
|
-
BatchDecoder.decode_and_flatten(raw_ast, input, Parsanol::Slice)
|
|
57
49
|
end
|
|
58
50
|
|
|
59
51
|
# Memory-bounded parsing without packrat cache.
|
|
@@ -73,12 +65,14 @@ module Parsanol
|
|
|
73
65
|
Parser.serialize_grammar(grammar)
|
|
74
66
|
end
|
|
75
67
|
|
|
68
|
+
# Decode here, not around the fallback: raise_native_parse_error
|
|
69
|
+
# returns the Ruby parser's already-final tree on success.
|
|
76
70
|
begin
|
|
77
|
-
|
|
71
|
+
BatchDecoder.decode_and_flatten(_parse_fresh_raw(grammar_json, input),
|
|
72
|
+
input, Parsanol::Slice)
|
|
78
73
|
rescue RuntimeError => e
|
|
79
74
|
raise_native_parse_error(e, grammar, input)
|
|
80
75
|
end
|
|
81
|
-
BatchDecoder.decode_and_flatten(raw_ast, input, Parsanol::Slice)
|
|
82
76
|
end
|
|
83
77
|
|
|
84
78
|
# Parse and return RAW AST without transformation.
|
|
@@ -206,6 +200,38 @@ module Parsanol
|
|
|
206
200
|
cause = Parsanol::Cause.new(error.message, source, source.bytepos)
|
|
207
201
|
raise Parsanol::ParseFailed.new(cause.to_s, cause)
|
|
208
202
|
end
|
|
203
|
+
|
|
204
|
+
# Pre-serialized JSON grammar path (library authors with cached JSON).
|
|
205
|
+
def parse_json_grammar(grammar_json, input)
|
|
206
|
+
BatchDecoder.decode_and_flatten(_parse_raw(grammar_json, input),
|
|
207
|
+
input, Parsanol::Slice)
|
|
208
|
+
rescue RuntimeError => e
|
|
209
|
+
raise_native_parse_error(e, grammar_json, input)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Grammar-atom path: registers once and parses by Rust-side handle,
|
|
213
|
+
# so steady-state calls marshal no JSON and copy no input string.
|
|
214
|
+
def parse_atom_grammar(grammar, input)
|
|
215
|
+
handle = Parser.grammar_handle(grammar)
|
|
216
|
+
|
|
217
|
+
begin
|
|
218
|
+
BatchDecoder.decode_and_flatten(_parse_handle(handle, input),
|
|
219
|
+
input, Parsanol::Slice)
|
|
220
|
+
rescue ArgumentError
|
|
221
|
+
# Handle dropped Rust-side (e.g. cache cleared): re-register once.
|
|
222
|
+
Parser.invalidate_handle(handle)
|
|
223
|
+
begin
|
|
224
|
+
BatchDecoder.decode_and_flatten(
|
|
225
|
+
_parse_handle(Parser.grammar_handle(grammar), input),
|
|
226
|
+
input, Parsanol::Slice
|
|
227
|
+
)
|
|
228
|
+
rescue RuntimeError => e
|
|
229
|
+
raise_native_parse_error(e, grammar, input)
|
|
230
|
+
end
|
|
231
|
+
rescue RuntimeError => e
|
|
232
|
+
raise_native_parse_error(e, grammar, input)
|
|
233
|
+
end
|
|
234
|
+
end
|
|
209
235
|
end
|
|
210
236
|
end
|
|
211
237
|
end
|
data/lib/parsanol/parser.rb
CHANGED
|
@@ -109,32 +109,36 @@ module Parsanol
|
|
|
109
109
|
# result[:name].to_s # => "hello"
|
|
110
110
|
#
|
|
111
111
|
def parse(input, mode_or_opts = {}, **kwargs)
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
# Native backend by default; falls back to pure Ruby inside
|
|
118
|
-
# parse_native when the extension is missing.
|
|
119
|
-
parse_native(input, merged)
|
|
112
|
+
opts =
|
|
113
|
+
if mode_or_opts.is_a?(Hash)
|
|
114
|
+
mode_or_opts.merge(kwargs)
|
|
115
|
+
elsif mode_or_opts.nil?
|
|
116
|
+
kwargs
|
|
120
117
|
else
|
|
121
|
-
|
|
118
|
+
# New API: parse(input, :mode, **options)
|
|
119
|
+
kwargs[:mode] = mode_or_opts
|
|
120
|
+
kwargs
|
|
122
121
|
end
|
|
122
|
+
# parse(input, {mode: :ruby, ...}) is the same call shape a subclass
|
|
123
|
+
# forwarding options via super produces; :mode must be honored
|
|
124
|
+
# wherever it appears, or those callers silently get the native path.
|
|
125
|
+
mode = opts.delete(:mode) ||
|
|
126
|
+
(if Parsanol::Native.available? && !opts.key?(:prefix) &&
|
|
127
|
+
!opts.key?(:reporter)
|
|
128
|
+
:native
|
|
129
|
+
else
|
|
130
|
+
:ruby
|
|
131
|
+
end)
|
|
132
|
+
case mode
|
|
133
|
+
when :ruby
|
|
134
|
+
super(input, opts)
|
|
135
|
+
when :native
|
|
136
|
+
parse_native(input, opts)
|
|
137
|
+
when :json
|
|
138
|
+
parse_json(input, opts)
|
|
123
139
|
else
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
(Parsanol::Native.available? ? :native : :ruby)
|
|
127
|
-
case mode
|
|
128
|
-
when :ruby
|
|
129
|
-
super(input, kwargs)
|
|
130
|
-
when :native
|
|
131
|
-
parse_native(input, kwargs)
|
|
132
|
-
when :json
|
|
133
|
-
parse_json(input, kwargs)
|
|
134
|
-
else
|
|
135
|
-
raise ArgumentError,
|
|
136
|
-
"Unknown mode: #{mode}. Valid modes: :ruby, :native, :json"
|
|
137
|
-
end
|
|
140
|
+
raise ArgumentError,
|
|
141
|
+
"Unknown mode: #{mode}. Valid modes: :ruby, :native, :json"
|
|
138
142
|
end
|
|
139
143
|
end
|
|
140
144
|
|
data/lib/parsanol/version.rb
CHANGED