parsanol 1.3.11-aarch64-linux → 1.3.13-aarch64-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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4aed187d34b80f934b13f77ed2967b33625c97315dd5222382b3e701469863c6
4
- data.tar.gz: 44b0f250d6d7a096a507c83db4993ff34db5cba69818409d96216faa97a5397f
3
+ metadata.gz: 505377abc4ee0881c63542fd97282e9a0ca5593cc2c33e07a2d7d633c971812e
4
+ data.tar.gz: 4d19be4d748835f54c4e9cc8ef8147c8b9ef5f80fb65e1539d3bcac93062cbe2
5
5
  SHA512:
6
- metadata.gz: 21a32ac830df5952dc9fd33038aecac5b0e26ea9f8c97e80c81bba6bd6111744642973dd971d03d9493ca6d72f1c3da6835e5f8afd0c0575fbbee2852568d29e
7
- data.tar.gz: b302062fc371a19c393a2ceb58cdb4eb70defefac240a3141bc22e123813e562fadca3c4d2f638684b5eef1a2b695a2f7ec88e998033ae832183be2662e78088
6
+ metadata.gz: b8ee108a7a194731eac0e33f659950c5210aadeb4ca1051c6e6e5910d711d2429f19a2bf57360b10fc444ec88ba221a0bb7932a316b8d5ed8943b1002243efac
7
+ data.tar.gz: 1b40341768f14294e00c24fba277a4ec187dafdab70e104c6fc6468df493d4fd07f871bc06bf972941796ad8ab01805eb095cecc26d74248004c1834631b18a3
Binary file
Binary file
Binary file
Binary file
@@ -28,6 +28,9 @@ module Parsanol
28
28
  :default => 1000,
29
29
  }.freeze
30
30
 
31
+ # Number of observed backtrack events before packrat caching engages.
32
+ BACKTRACK_ACTIVATION_LIMIT = 64
33
+
31
34
  # Creates a new parsing context.
32
35
  #
33
36
  # @param error_reporter [#err, #err_at] error reporter instance
@@ -40,7 +43,7 @@ module Parsanol
40
43
  adaptive_cache_threshold: nil,
41
44
  parser_class: nil)
42
45
  # Core memoization cache: position -> { atom_id -> [result, advance] }
43
- @memo = Hash.new { |h, k| h[k] = {} }
46
+ @memo = {}
44
47
 
45
48
  # Error reporting delegate
46
49
  @reporter = error_reporter
@@ -55,7 +58,7 @@ module Parsanol
55
58
  @evict_interval = 100
56
59
 
57
60
  # Object pools for reducing allocations
58
- @array_pool = Parsanol::Pools::ArrayPool.new(size: 10_000)
61
+ @array_pool = Parsanol::Pools::ArrayPool.new(size: 10_000, preallocate: false)
59
62
  @buffer_pool = Parsanol::Pools::BufferPool.new(pool_size: 100)
60
63
 
61
64
  # Selective memoization tracking
@@ -85,7 +88,8 @@ module Parsanol
85
88
 
86
89
  @adaptive_threshold = threshold
87
90
  @input_len = nil
88
- @caching_active = nil
91
+ @caching_active = false
92
+ @backtrack_events = 0
89
93
  end
90
94
 
91
95
  # Attempts to parse using memoization. Returns cached result if available,
@@ -100,21 +104,18 @@ module Parsanol
100
104
  # Skip caching for atoms that don't benefit from it
101
105
  return atom.try(src, self, must_consume_all) unless atom.cached?
102
106
 
103
- # Determine if caching should be active (lazy initialization)
104
- if @caching_active.nil?
105
- total_len = src.bytepos + src.chars_left
106
- @input_len = total_len
107
- @caching_active = total_len >= @adaptive_threshold
108
- end
109
-
110
- # For small inputs, skip caching overhead
111
- return atom.try(src, self, must_consume_all) unless @caching_active
112
-
113
107
  # Use interval-based caching if enabled
114
108
  return try_with_interval(atom, src, must_consume_all) if @use_intervals
115
109
 
110
+ # Adaptive activation: packrat memoization costs more than it saves
111
+ # on deterministic forward-only grammars, so start uncached and only
112
+ # engage once real backtracking (re-parsing behind the progress
113
+ # frontier) is observed.
114
+ return try_uncached_probe(atom, src, must_consume_all) unless @caching_active
115
+
116
116
  pos = src.bytepos
117
117
  key = atom.object_id
118
+ entry = @memo[pos]
118
119
 
119
120
  # Periodic cache eviction to prevent unbounded growth
120
121
  if pos > @furthest_pos
@@ -128,10 +129,10 @@ module Parsanol
128
129
  end
129
130
  end
130
131
 
131
- # Check for cache hit
132
- if @memo[pos].key?(key)
132
+ # Check for cache hit (avoid default-block Hash allocation per probe)
133
+ if entry&.key?(key)
133
134
  @hit_stats[key] += 1
134
- outcome, delta = @memo[pos][key]
135
+ outcome, delta = entry[key]
135
136
  src.bytepos = pos + delta
136
137
  return outcome
137
138
  end
@@ -144,7 +145,7 @@ module Parsanol
144
145
  # Only cache if beneficial (heuristic)
145
146
  attempts = @hit_stats[key] + @miss_stats[key]
146
147
  if attempts <= @min_hits_for_cache || @hit_stats[key].positive?
147
- @memo[pos][key] =
148
+ (@memo[pos] ||= {})[key] =
148
149
  [outcome,
149
150
  delta]
150
151
  end
@@ -327,6 +328,21 @@ module Parsanol
327
328
 
328
329
  private
329
330
 
331
+ # Executes an atom without memoization while watching for backtracking.
332
+ # A failed attempt at a position behind the progress frontier means work
333
+ # is being re-done; once that repeats, packrat caching engages.
334
+ def try_uncached_probe(atom, src, must_consume_all)
335
+ pos = src.bytepos
336
+ outcome = atom.try(src, self, must_consume_all)
337
+ if pos > @furthest_pos
338
+ @furthest_pos = pos
339
+ elsif !outcome[0] && pos < @furthest_pos
340
+ @backtrack_events += 1
341
+ @caching_active = true if @backtrack_events >= BACKTRACK_ACTIVATION_LIMIT
342
+ end
343
+ outcome
344
+ end
345
+
330
346
  # Lookup cached result (uses object_id for speed)
331
347
  def lookup(atom, pos)
332
348
  @memo[pos][atom.object_id]
@@ -120,6 +120,7 @@ module Parsanol
120
120
  "atom" => serialize_atom(atom.parslet),
121
121
  "min" => atom.min,
122
122
  "max" => atom.max,
123
+ "tag" => atom.result_tag == :maybe ? "Maybe" : "Repetition",
123
124
  },
124
125
  }
125
126
  end
@@ -28,10 +28,19 @@ module Parsanol
28
28
  # This is a class variable to share across all transformations
29
29
  @@symbol_cache = {}
30
30
 
31
- def self.transform(ast)
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)
32
41
  case ast
33
42
  when Array
34
- transform_array(ast)
43
+ transform_array(ast, named: named)
35
44
  when Hash
36
45
  transform_hash(ast)
37
46
  else
@@ -52,11 +61,7 @@ module Parsanol
52
61
  @@symbol_cache[key] ||= key.to_sym
53
62
  end
54
63
 
55
- # Symbol tags from native parser
56
- SEQUENCE_SYM = :sequence
57
- REPETITION_SYM = :repetition
58
-
59
- def self.transform_array(arr)
64
+ def self.transform_array(arr, named: false)
60
65
  return EMPTY_ARRAY if arr.empty? # Match Parsanol Ruby mode behavior
61
66
 
62
67
  # Check if this is a tagged array from native parser
@@ -87,6 +92,16 @@ module Parsanol
87
92
  i += 1
88
93
  end
89
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
90
105
  elsif first.is_a?(Symbol) || (first.is_a?(String) && first.start_with?(":"))
91
106
  # Other tagged arrays - pass through
92
107
  arr.map { |item| transform(item) }
@@ -115,7 +130,7 @@ module Parsanol
115
130
  sym_key = cached_symbol(key)
116
131
 
117
132
  # Transform the value
118
- transformed = transform(value)
133
+ transformed = transform(value, named: true)
119
134
 
120
135
  # Check if value is a tagged repetition from native parser
121
136
  is_tagged_repetition = value.is_a?(Array) && !value.empty? &&
@@ -240,7 +255,7 @@ module Parsanol
240
255
  is_repetition = value.is_a?(Array) && !value.empty? &&
241
256
  value.first.is_a?(String) && value.first == REPETITION_TAG
242
257
 
243
- transformed = transform(value)
258
+ transformed = transform(value, named: true)
244
259
 
245
260
  result[sym_key] = if is_repetition
246
261
  if transformed.is_a?(Array)
@@ -48,7 +48,11 @@ module Parsanol
48
48
  # Use _parse_raw which returns properly tagged Ruby arrays via transform_ast.
49
49
  # The batch format doesn't preserve :repetition/:sequence tags, so we use
50
50
  # the direct FFI path. Apply the Ruby transformer to handle tags correctly.
51
- raw_ast = _parse_raw(grammar_json, input)
51
+ begin
52
+ raw_ast = _parse_raw(grammar_json, input)
53
+ rescue RuntimeError => e
54
+ raise_native_parse_error(e, grammar, input)
55
+ end
52
56
  BatchDecoder.decode_and_flatten(raw_ast, input, Parsanol::Slice)
53
57
  end
54
58
 
@@ -69,7 +73,11 @@ module Parsanol
69
73
  Parser.serialize_grammar(grammar)
70
74
  end
71
75
 
72
- raw_ast = _parse_fresh_raw(grammar_json, input)
76
+ begin
77
+ raw_ast = _parse_fresh_raw(grammar_json, input)
78
+ rescue RuntimeError => e
79
+ raise_native_parse_error(e, grammar, input)
80
+ end
73
81
  BatchDecoder.decode_and_flatten(raw_ast, input, Parsanol::Slice)
74
82
  end
75
83
 
@@ -182,6 +190,22 @@ module Parsanol
182
190
  end
183
191
  stats
184
192
  end
193
+
194
+ # Translates a native backend failure into the Parsanol error protocol.
195
+ #
196
+ # When the grammar atom is at hand, reparses through the pure Ruby
197
+ # backend: a Ruby failure raises Parsanol::ParseFailed with the full
198
+ # cause-tree diagnostics, and a Ruby success recovers grammars the
199
+ # native serializer cannot express (e.g. custom atoms). For
200
+ # pre-serialized JSON grammars the native message is wrapped in a
201
+ # Parsanol::ParseFailed directly.
202
+ def raise_native_parse_error(error, grammar, input)
203
+ return grammar.parse(input) if grammar.respond_to?(:parse)
204
+
205
+ source = Parsanol::Source.new(input)
206
+ cause = Parsanol::Cause.new(error.message, source, source.bytepos)
207
+ raise Parsanol::ParseFailed.new(cause.to_s, cause)
208
+ end
185
209
  end
186
210
  end
187
211
  end
@@ -112,10 +112,18 @@ module Parsanol
112
112
  if mode_or_opts.is_a?(Hash) && !kwargs.key?(:mode)
113
113
  # Legacy API: parse(input, options={})
114
114
  merged = mode_or_opts.merge(kwargs)
115
- super(input, merged)
115
+ if Parsanol::Native.available? && !merged.key?(:prefix) &&
116
+ !merged.key?(:reporter)
117
+ # Native backend by default; falls back to pure Ruby inside
118
+ # parse_native when the extension is missing.
119
+ parse_native(input, merged)
120
+ else
121
+ super(input, merged)
122
+ end
116
123
  else
117
124
  # New API: parse(input, mode:, **options)
118
- mode = kwargs.delete(:mode) || :ruby
125
+ mode = kwargs.delete(:mode) ||
126
+ (Parsanol::Native.available? ? :native : :ruby)
119
127
  case mode
120
128
  when :ruby
121
129
  super(input, kwargs)
@@ -209,7 +217,8 @@ module Parsanol
209
217
  if Parsanol::Native.available?
210
218
  Parsanol::Native.parse(root, input)
211
219
  else
212
- super
220
+ Parsanol::Atoms::Base.instance_method(:parse).bind_call(self, input,
221
+ opts)
213
222
  end
214
223
  end
215
224
 
@@ -151,7 +151,14 @@ module Parsanol
151
151
  # @return [Integer] Size class
152
152
  #
153
153
  def select_size_class(size)
154
- SIZE_CLASSES.find { |sc| sc >= size } || next_power_of_2(size)
154
+ i = 0
155
+ n = SIZE_CLASSES.length
156
+ while i < n
157
+ return SIZE_CLASSES[i] if SIZE_CLASSES[i] >= size
158
+
159
+ i += 1
160
+ end
161
+ next_power_of_2(size)
155
162
  end
156
163
 
157
164
  # Find next power of 2 greater than or equal to n.
@@ -53,7 +53,7 @@ module Parsanol
53
53
 
54
54
  # Object pools for memory efficiency
55
55
  # SlicePool: reduces Slice allocations during matching
56
- @slice_pool = Parsanol::Pools::SlicePool.new(size: 5000)
56
+ @slice_pool = Parsanol::Pools::SlicePool.new(size: 5000, preallocate: false)
57
57
 
58
58
  # PositionPool: reduces Position allocations for error reporting
59
59
  @position_pool = Parsanol::Pools::PositionPool.new(size: 1000)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Parsanol
4
- VERSION = "1.3.11"
4
+ VERSION = "1.3.13"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: parsanol
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.11
4
+ version: 1.3.13
5
5
  platform: aarch64-linux
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-04-15 00:00:00.000000000 Z
11
+ date: 2026-09-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -151,6 +151,7 @@ files:
151
151
  - README.adoc
152
152
  - Rakefile
153
153
  - lib/parsanol.rb
154
+ - lib/parsanol/3.2/parsanol_native.so
154
155
  - lib/parsanol/3.3/parsanol_native.so
155
156
  - lib/parsanol/3.4/parsanol_native.so
156
157
  - lib/parsanol/4.0/parsanol_native.so
@@ -261,7 +262,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
261
262
  requirements:
262
263
  - - ">="
263
264
  - !ruby/object:Gem::Version
264
- version: '3.3'
265
+ version: '3.2'
265
266
  - - "<"
266
267
  - !ruby/object:Gem::Version
267
268
  version: 4.1.dev