parsanol 1.3.12 → 1.3.14
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/Cargo.lock +2 -2
- 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.rb +33 -14
- data/lib/parsanol/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c4577d99080eb74c30e3de09fe01d342818d6776e8ee7e852b0de178c131469c
|
|
4
|
+
data.tar.gz: 332412b5d27238d167bb78e68d1bc9e378139bc9bc3377b304bfc201f91d7b51
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 71f3e40a0eafe7a15d543eab66911393cee50f478fda9ab65c8986dd54b3ceba116f1f61cea2b7801c2a2d061e7c6d33e505fc4111b69fa2f04ef2eb1685528a
|
|
7
|
+
data.tar.gz: 5ce7080b01d79c2f18438115eaa29a96afce668a3ebdd6849f724b72b9d9aae90263c3db2aec9fcec478ff840532ff9c9e52c93be61e5801f5c961c5cbd05452
|
data/Cargo.lock
CHANGED
|
@@ -253,7 +253,7 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
|
|
253
253
|
[[package]]
|
|
254
254
|
name = "parsanol"
|
|
255
255
|
version = "0.5.1"
|
|
256
|
-
source = "git+https://github.com/parsanol/parsanol-rs?branch=main#
|
|
256
|
+
source = "git+https://github.com/parsanol/parsanol-rs?branch=main#f6782c030b42aaa722357cd107fd56ed9853c4ea"
|
|
257
257
|
dependencies = [
|
|
258
258
|
"ahash",
|
|
259
259
|
"getrandom 0.3.4",
|
|
@@ -270,7 +270,7 @@ dependencies = [
|
|
|
270
270
|
[[package]]
|
|
271
271
|
name = "parsanol-derive"
|
|
272
272
|
version = "0.5.1"
|
|
273
|
-
source = "git+https://github.com/parsanol/parsanol-rs?branch=main#
|
|
273
|
+
source = "git+https://github.com/parsanol/parsanol-rs?branch=main#f6782c030b42aaa722357cd107fd56ed9853c4ea"
|
|
274
274
|
dependencies = [
|
|
275
275
|
"proc-macro2",
|
|
276
276
|
"quote",
|
|
@@ -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
|
data/lib/parsanol/native.rb
CHANGED
|
@@ -38,21 +38,13 @@ 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
|
-
|
|
41
|
+
raw_ast =
|
|
42
|
+
if grammar.is_a?(String)
|
|
43
|
+
parse_json_grammar(grammar, input)
|
|
44
|
+
else
|
|
45
|
+
parse_atom_grammar(grammar, input)
|
|
46
|
+
end
|
|
47
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)
|
|
55
|
-
end
|
|
56
48
|
BatchDecoder.decode_and_flatten(raw_ast, input, Parsanol::Slice)
|
|
57
49
|
end
|
|
58
50
|
|
|
@@ -206,6 +198,33 @@ module Parsanol
|
|
|
206
198
|
cause = Parsanol::Cause.new(error.message, source, source.bytepos)
|
|
207
199
|
raise Parsanol::ParseFailed.new(cause.to_s, cause)
|
|
208
200
|
end
|
|
201
|
+
|
|
202
|
+
# Pre-serialized JSON grammar path (library authors with cached JSON).
|
|
203
|
+
def parse_json_grammar(grammar_json, input)
|
|
204
|
+
_parse_raw(grammar_json, input)
|
|
205
|
+
rescue RuntimeError => e
|
|
206
|
+
raise_native_parse_error(e, grammar_json, input)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Grammar-atom path: registers once and parses by Rust-side handle,
|
|
210
|
+
# so steady-state calls marshal no JSON and copy no input string.
|
|
211
|
+
def parse_atom_grammar(grammar, input)
|
|
212
|
+
handle = Parser.grammar_handle(grammar)
|
|
213
|
+
|
|
214
|
+
begin
|
|
215
|
+
_parse_handle(handle, input)
|
|
216
|
+
rescue ArgumentError
|
|
217
|
+
# Handle dropped Rust-side (e.g. cache cleared): re-register once.
|
|
218
|
+
Parser.invalidate_handle(handle)
|
|
219
|
+
begin
|
|
220
|
+
_parse_handle(Parser.grammar_handle(grammar), input)
|
|
221
|
+
rescue RuntimeError => e
|
|
222
|
+
raise_native_parse_error(e, grammar, input)
|
|
223
|
+
end
|
|
224
|
+
rescue RuntimeError => e
|
|
225
|
+
raise_native_parse_error(e, grammar, input)
|
|
226
|
+
end
|
|
227
|
+
end
|
|
209
228
|
end
|
|
210
229
|
end
|
|
211
230
|
end
|
data/lib/parsanol/version.rb
CHANGED