parsanol 1.3.13 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 78de43242c21f1c37c83a6b432227683162687a6d9e46fc2a86f31bc451e576e
4
- data.tar.gz: 49a1bacad7a5006a471c0f8c194dd7931813869132a762c59fd928b1fde7cc40
3
+ metadata.gz: c4577d99080eb74c30e3de09fe01d342818d6776e8ee7e852b0de178c131469c
4
+ data.tar.gz: 332412b5d27238d167bb78e68d1bc9e378139bc9bc3377b304bfc201f91d7b51
5
5
  SHA512:
6
- metadata.gz: e4a02c77e7f95bb2ec1b9ac87393f77842d82c83c1c0f52f6aa53565908fd6328c2b93c6f3ebe80cad9605b4deab9d628faed737c5bfb656133ffa2b331b150c
7
- data.tar.gz: 3357e9dbab15be8e2020dac99429b0aa92c474b8a63e2e4fd21d86c825c87550bfb649105965d62ba17ea7abd6570ab357999984fe7022f38e8fd1561cf4dd2c
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#1a141f1a3471ba6ce83a768354a917d4ab83d172"
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#1a141f1a3471ba6ce83a768354a917d4ab83d172"
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 basis for
69
- # an inject. Optimized with early return and reduced method calls.
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
- return "" if len.zero?
74
- return list[0] if len == 1 # Fast path for single element
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
- result = yield(result, list[i])
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.compact) do |r, e| # and then merge flat elements
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
- return list.select { |e| e.instance_of?(Hash) }
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
- return list
171
- .select { |e| e.instance_of?(Array) }
172
- .flatten(1)
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
- # If there are only strings, concatenate them and return that.
179
- foldl(list.compact) { |s, e| s + e }
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 with buffer pooling
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
- # Estimate buffer size
180
- estimate = [@max || 10, 10].min
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
- buffer.push(value)
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
- ok(Parsanol::LazyResult.new(buffer, context))
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
- estimate = [@max || 10, 10].min
231
- buffer = context.acquire_buffer(size: estimate + 1)
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
- buffer.push(value)
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, buffer.to_a[1..],
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
- ok(Parsanol::LazyResult.new(buffer, context))
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 with buffer pooling
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
- buffer = context.acquire_buffer(size: 3)
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 with buffer pooling
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
- buffer = context.acquire_buffer(size: 4)
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
- buffer = context.acquire_buffer(size: components.size + 1)
145
- buffer.push(:sequence)
135
+ n = components.size
136
+ result = Array.new(n + 1)
137
+ result[0] = :sequence
146
138
 
147
- last_idx = components.size - 1
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
- buffer.push(value)
150
+ result[idx + 1] = value
160
151
  idx += 1
161
152
  end
162
153
 
163
- ok(Parsanol::LazyResult.new(buffer, context))
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 = root_atom.root if root_atom.is_a?(::Parsanol::Parser)
44
- obj_id = root_atom.object_id
45
- cache_key = GRAMMAR_HASH_CACHE[obj_id] ||= grammar_structure_hash(root_atom)
46
- GRAMMAR_CACHE[cache_key] ||= GrammarSerializer.serialize(root_atom)
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
@@ -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
- # Handle both grammar atoms and pre-serialized JSON strings
42
- grammar_json = if grammar.is_a?(String)
43
- grammar
44
- else
45
- Parser.serialize_grammar(grammar)
46
- end
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Parsanol
4
- VERSION = "1.3.13"
4
+ VERSION = "1.3.14"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: parsanol
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.13
4
+ version: 1.3.14
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.