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.
Files changed (100) hide show
  1. checksums.yaml +7 -0
  2. data/HISTORY.txt +98 -0
  3. data/LICENSE +23 -0
  4. data/README.adoc +905 -0
  5. data/Rakefile +141 -0
  6. data/lib/parsanol/3.2/parsanol_native.so +0 -0
  7. data/lib/parsanol/3.3/parsanol_native.so +0 -0
  8. data/lib/parsanol/3.4/parsanol_native.so +0 -0
  9. data/lib/parsanol/4.0/parsanol_native.so +0 -0
  10. data/lib/parsanol/ast_visitor.rb +122 -0
  11. data/lib/parsanol/atoms/alternative.rb +123 -0
  12. data/lib/parsanol/atoms/base.rb +208 -0
  13. data/lib/parsanol/atoms/can_flatten.rb +194 -0
  14. data/lib/parsanol/atoms/capture.rb +38 -0
  15. data/lib/parsanol/atoms/context.rb +357 -0
  16. data/lib/parsanol/atoms/context_optimized.rb +38 -0
  17. data/lib/parsanol/atoms/custom.rb +110 -0
  18. data/lib/parsanol/atoms/cut.rb +66 -0
  19. data/lib/parsanol/atoms/dsl.rb +96 -0
  20. data/lib/parsanol/atoms/dynamic.rb +39 -0
  21. data/lib/parsanol/atoms/entity.rb +75 -0
  22. data/lib/parsanol/atoms/ignored.rb +37 -0
  23. data/lib/parsanol/atoms/infix.rb +167 -0
  24. data/lib/parsanol/atoms/lookahead.rb +85 -0
  25. data/lib/parsanol/atoms/named.rb +74 -0
  26. data/lib/parsanol/atoms/re.rb +83 -0
  27. data/lib/parsanol/atoms/repetition.rb +277 -0
  28. data/lib/parsanol/atoms/scope.rb +35 -0
  29. data/lib/parsanol/atoms/sequence.rb +195 -0
  30. data/lib/parsanol/atoms/str.rb +109 -0
  31. data/lib/parsanol/atoms/visitor.rb +91 -0
  32. data/lib/parsanol/atoms.rb +46 -0
  33. data/lib/parsanol/buffer.rb +133 -0
  34. data/lib/parsanol/builder_callbacks.rb +353 -0
  35. data/lib/parsanol/cause.rb +122 -0
  36. data/lib/parsanol/context.rb +39 -0
  37. data/lib/parsanol/convenience.rb +36 -0
  38. data/lib/parsanol/edit_tracker.rb +111 -0
  39. data/lib/parsanol/error_reporter/contextual.rb +99 -0
  40. data/lib/parsanol/error_reporter/deepest.rb +120 -0
  41. data/lib/parsanol/error_reporter/tree.rb +63 -0
  42. data/lib/parsanol/error_reporter.rb +100 -0
  43. data/lib/parsanol/expression/treetop.rb +154 -0
  44. data/lib/parsanol/expression.rb +106 -0
  45. data/lib/parsanol/fast_mode.rb +187 -0
  46. data/lib/parsanol/first_set.rb +79 -0
  47. data/lib/parsanol/grammar_builder.rb +179 -0
  48. data/lib/parsanol/incremental_parser.rb +182 -0
  49. data/lib/parsanol/interval_tree.rb +226 -0
  50. data/lib/parsanol/lazy_result.rb +179 -0
  51. data/lib/parsanol/mermaid.rb +142 -0
  52. data/lib/parsanol/native/batch_decoder.rb +255 -0
  53. data/lib/parsanol/native/dynamic.rb +238 -0
  54. data/lib/parsanol/native/parser.rb +102 -0
  55. data/lib/parsanol/native/serializer.rb +252 -0
  56. data/lib/parsanol/native/transformer.rb +604 -0
  57. data/lib/parsanol/native/types.rb +29 -0
  58. data/lib/parsanol/native.rb +223 -0
  59. data/lib/parsanol/optimizer.rb +85 -0
  60. data/lib/parsanol/optimizers/choice_optimizer.rb +78 -0
  61. data/lib/parsanol/optimizers/cut_inserter.rb +182 -0
  62. data/lib/parsanol/optimizers/lookahead_optimizer.rb +56 -0
  63. data/lib/parsanol/optimizers/quantifier_optimizer.rb +60 -0
  64. data/lib/parsanol/optimizers/sequence_optimizer.rb +97 -0
  65. data/lib/parsanol/options/zero_copy.rb +127 -0
  66. data/lib/parsanol/options.rb +21 -0
  67. data/lib/parsanol/parallel.rb +128 -0
  68. data/lib/parsanol/parser.rb +242 -0
  69. data/lib/parsanol/parslet.rb +151 -0
  70. data/lib/parsanol/pattern/binding.rb +91 -0
  71. data/lib/parsanol/pattern.rb +162 -0
  72. data/lib/parsanol/pool.rb +219 -0
  73. data/lib/parsanol/pools/array_pool.rb +75 -0
  74. data/lib/parsanol/pools/buffer_pool.rb +182 -0
  75. data/lib/parsanol/pools/position_pool.rb +92 -0
  76. data/lib/parsanol/pools/slice_pool.rb +64 -0
  77. data/lib/parsanol/position.rb +94 -0
  78. data/lib/parsanol/resettable.rb +29 -0
  79. data/lib/parsanol/result.rb +46 -0
  80. data/lib/parsanol/result_builder.rb +208 -0
  81. data/lib/parsanol/result_stream.rb +266 -0
  82. data/lib/parsanol/rig/rspec.rb +71 -0
  83. data/lib/parsanol/rope.rb +81 -0
  84. data/lib/parsanol/scope.rb +104 -0
  85. data/lib/parsanol/slice.rb +160 -0
  86. data/lib/parsanol/source/line_cache.rb +102 -0
  87. data/lib/parsanol/source.rb +185 -0
  88. data/lib/parsanol/source_location.rb +167 -0
  89. data/lib/parsanol/streaming_parser.rb +124 -0
  90. data/lib/parsanol/string_view.rb +198 -0
  91. data/lib/parsanol/transform.rb +226 -0
  92. data/lib/parsanol/version.rb +5 -0
  93. data/lib/parsanol/wasm/README.md +80 -0
  94. data/lib/parsanol/wasm/package.json +51 -0
  95. data/lib/parsanol/wasm/parsanol.js +252 -0
  96. data/lib/parsanol/wasm/parslet.d.ts +129 -0
  97. data/lib/parsanol/wasm_parser.rb +240 -0
  98. data/lib/parsanol.rb +278 -0
  99. data/parsanol.gemspec +67 -0
  100. metadata +279 -0
@@ -0,0 +1,194 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Parsanol
4
+ module Atoms
5
+ # A series of helper functions that have the common topic of flattening
6
+ # result values into the intermediary tree that consists of Ruby Hashes and
7
+ # Arrays.
8
+ #
9
+ # This module has one main function, #flatten, that takes an annotated
10
+ # structure as input and returns the reduced form that users expect from
11
+ # Atom#parse.
12
+ #
13
+ # NOTE: Since all of these functions are just that, functions without
14
+ # side effects, they are in a module and not in a class. Its hard to draw
15
+ # the line sometimes, but this is beyond.
16
+ #
17
+ module CanFlatten
18
+ # Takes a mixed value coming out of a parslet and converts it to a return
19
+ # value for the user by dropping things and merging hashes.
20
+ #
21
+ # Named is set to true if this result will be embedded in a Hash result from
22
+ # naming something using <code>.as(...)</code>. It changes the folding
23
+ # semantics of repetition.
24
+ #
25
+ def flatten(value, named = false)
26
+ # Passes through everything that isn't an array of things
27
+ # Phase 43: Use simpler check - if it's not an Array, return as-is
28
+ return value unless value.is_a?(Array)
29
+
30
+ # Extracts the s-expression tag
31
+ tag = value[0]
32
+
33
+ # Phase 43: Optimize flattening - reduce method call overhead
34
+ # For single element arrays (common case), handle directly
35
+ tail_size = value.size - 1
36
+ if tail_size == 1
37
+ flattened = flatten(value[1])
38
+ case tag
39
+ when :sequence
40
+ return flattened
41
+ when :maybe
42
+ return named ? flattened : (flattened || "")
43
+ when :repetition
44
+ return flatten_repetition([flattened], named)
45
+ end
46
+ end
47
+
48
+ # Flatten each element
49
+ result = Array.new(tail_size)
50
+ i = 0
51
+ while i < tail_size
52
+ result[i] = flatten(value[i + 1])
53
+ i += 1
54
+ end
55
+
56
+ case tag
57
+ when :sequence
58
+ return flatten_sequence(result)
59
+ when :maybe
60
+ return named ? result.first : result.first || ""
61
+ when :repetition
62
+ return flatten_repetition(result, named)
63
+ end
64
+
65
+ raise "BUG: Unknown tag #{tag.inspect}."
66
+ end
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.
70
+ #
71
+ def foldl(list)
72
+ len = list.size
73
+ return "" if len.zero?
74
+ return list[0] if len == 1 # Fast path for single element
75
+
76
+ result = list[0]
77
+ i = 1
78
+ while i < len
79
+ result = yield(result, list[i])
80
+ i += 1
81
+ end
82
+ result
83
+ end
84
+
85
+ # Flatten results from a sequence of parslets.
86
+ #
87
+ # @api private
88
+ #
89
+ def flatten_sequence(list)
90
+ foldl(list.compact) do |r, e| # and then merge flat elements
91
+ merge_fold(r, e)
92
+ end
93
+ end
94
+
95
+ # @api private
96
+ # Phase 43: Optimized merge_fold - reduce repeated class checks
97
+ def merge_fold(l, r)
98
+ l_class = l.class
99
+ r_class = r.class
100
+
101
+ # equal pairs: merge. ----------------------------------------------------
102
+ if l_class == r_class
103
+ return l + r unless l_class == Hash
104
+
105
+ warn_about_duplicate_keys(l, r)
106
+ return l.merge(r)
107
+
108
+ end
109
+
110
+ # Phase 43: Cache instance_of? checks to avoid repeated method calls
111
+ # unequal pairs: hoist to same level. ------------------------------------
112
+ l_is_slice = l.instance_of?(Parsanol::Slice)
113
+ r_is_slice = r.instance_of?(Parsanol::Slice)
114
+ l_is_str = l_class == String || l_is_slice
115
+ r_is_str = r_class == String || r_is_slice
116
+
117
+ # Maybe classes are not equal, but both are stringlike?
118
+ if l_is_str && r_is_str
119
+ # if we're merging a String with a Slice, the slice wins.
120
+ return r if r_is_slice
121
+ return l if l_is_slice
122
+
123
+ raise "NOTREACHED: What other stringlike classes are there?"
124
+ end
125
+
126
+ # special case: If one of them is a string/slice, the other is more important
127
+ return l if r_is_str
128
+ return r if l_is_str
129
+
130
+ # otherwise just create an array for one of them to live in
131
+ return l + [r] if r_class == Hash
132
+ return [l] + r if l_class == Hash
133
+
134
+ raise "Unhandled case when foldr'ing sequence."
135
+ end
136
+
137
+ # Flatten results from a repetition of a single parslet. named indicates
138
+ # whether the user has named the result or not. If the user has named
139
+ # the results, we want to leave an empty list alone - otherwise it is
140
+ # turned into an empty string.
141
+ #
142
+ # @api private
143
+ #
144
+ # Phase 43: Optimized flatten_repetition - reduce array iterations
145
+ def flatten_repetition(list, named)
146
+ # Phase 43: Single pass to check for hashes and arrays
147
+ has_hash = false
148
+ has_array = false
149
+
150
+ i = 0
151
+ len = list.size
152
+ while i < len
153
+ e = list[i]
154
+ has_hash = true if e.instance_of?(Hash)
155
+ has_array = true if e.instance_of?(Array)
156
+ break if has_hash && has_array # Early exit if both found
157
+
158
+ i += 1
159
+ end
160
+
161
+ if has_hash
162
+ # If keyed subtrees are in the array, we'll want to discard all
163
+ # strings inbetween. To keep them, name them.
164
+ return list.select { |e| e.instance_of?(Hash) }
165
+ end
166
+
167
+ if has_array
168
+ # If any arrays are nested in this array, flatten all arrays to this
169
+ # level.
170
+ return list
171
+ .select { |e| e.instance_of?(Array) }
172
+ .flatten(1)
173
+ end
174
+
175
+ # Consistent handling of empty lists, when we act on a named result
176
+ return [] if named && list.empty?
177
+
178
+ # If there are only strings, concatenate them and return that.
179
+ foldl(list.compact) { |s, e| s + e }
180
+ end
181
+
182
+ # That annoying warning 'Duplicate subtrees while merging result' comes
183
+ # from here. You should add more '.as(...)' names to your intermediary tree.
184
+ #
185
+ def warn_about_duplicate_keys(h1, h2)
186
+ d = h1.keys & h2.keys
187
+ return if d.empty?
188
+
189
+ warn "Duplicate subtrees while merging result of \n #{inspect}\nonly the values " \
190
+ "of the latter will be kept. (keys: #{d.inspect})"
191
+ end
192
+ end
193
+ end
194
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Captures the result of parsing and stores it for later use.
4
+ # Use the capture method to capture a sub-expression result, then
5
+ # access it via context.captures[:name] in dynamic blocks.
6
+ #
7
+ # @example
8
+ # str('a').capture(:first) >> dynamic { |ctx| str(ctx.captures[:first]) }
9
+ #
10
+ module Parsanol
11
+ module Atoms
12
+ class Capture < Parsanol::Atoms::Base
13
+ attr_reader :inner_atom, :capture_key
14
+
15
+ def initialize(atom, name)
16
+ super()
17
+ @inner_atom = atom
18
+ @capture_key = name.to_sym
19
+ end
20
+
21
+ def apply(source, context, consume_all)
22
+ success, result = @inner_atom.apply(source, context, consume_all)
23
+
24
+ if success
25
+ # Flatten and store the captured value in context
26
+ flattened = flatten(result)
27
+ context.captures[@capture_key] = flattened
28
+ end
29
+
30
+ [success, result]
31
+ end
32
+
33
+ def to_s_inner(prec)
34
+ "(#{@capture_key.inspect} = #{@inner_atom.to_s(prec)})"
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,357 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Parsanol
4
+ module Atoms
5
+ # Parsing context that coordinates memoization caching, error reporting,
6
+ # and resource pooling. Created fresh for each parse operation.
7
+ #
8
+ # Key responsibilities:
9
+ # - Packrat-style memoization (caching parse results by position+atom)
10
+ # - Pluggable error reporting through reporter interface
11
+ # - Object pooling for arrays and buffers to reduce GC pressure
12
+ # - Adaptive caching based on input size
13
+ #
14
+ # @example Basic usage
15
+ # ctx = Context.new(reporter)
16
+ # result = ctx.try_with_cache(parser, source, true)
17
+ #
18
+ # Inspired by packrat parsing memoization and incremental parsing techniques.
19
+ #
20
+ class Context
21
+ # Per-parser cache size thresholds based on profiling different grammar types
22
+ # Different grammars benefit from caching at different input sizes
23
+ PARSER_CACHE_LIMITS = {
24
+ "JsonParser" => 10_000, # JSON needs large inputs to benefit
25
+ "ErbParser" => 800, # ERB benefits earlier
26
+ "CalcParser" => 2000, # Calculator has low repetition
27
+ "SentenceParser" => 5000, # Linear grammar, minimal benefit
28
+ :default => 1000,
29
+ }.freeze
30
+
31
+ # Number of observed backtrack events before packrat caching engages.
32
+ BACKTRACK_ACTIVATION_LIMIT = 64
33
+
34
+ # Creates a new parsing context.
35
+ #
36
+ # @param error_reporter [#err, #err_at] error reporter instance
37
+ # @param interval_cache: [Boolean] enable GPeg-style interval caching
38
+ # @param adaptive_cache_threshold: [Integer, nil] minimum input size for caching
39
+ # @param parser_class: [Class, nil] parser class for threshold selection
40
+ #
41
+ def initialize(error_reporter = Parsanol::ErrorReporter::Tree.new,
42
+ interval_cache: false,
43
+ adaptive_cache_threshold: nil,
44
+ parser_class: nil)
45
+ # Core memoization cache: position -> { atom_id -> [result, advance] }
46
+ @memo = {}
47
+
48
+ # Error reporting delegate
49
+ @reporter = error_reporter
50
+
51
+ # Capture scope for variable bindings
52
+ @captures = Parsanol::Scope.new
53
+
54
+ # Cache eviction state
55
+ @furthest_pos = 0
56
+ @evict_threshold = 200
57
+ @evict_counter = 0
58
+ @evict_interval = 100
59
+
60
+ # Object pools for reducing allocations
61
+ @array_pool = Parsanol::Pools::ArrayPool.new(size: 10_000, preallocate: false)
62
+ @buffer_pool = Parsanol::Pools::BufferPool.new(pool_size: 100)
63
+
64
+ # Selective memoization tracking
65
+ @hit_stats = Hash.new(0)
66
+ @miss_stats = Hash.new(0)
67
+ @min_hits_for_cache = 2
68
+
69
+ # Optional GPeg-style interval caching
70
+ @use_intervals = interval_cache
71
+ if @use_intervals
72
+ require "parsanol/interval_tree"
73
+ require "parsanol/edit_tracker"
74
+ @interval_trees = Hash.new { |h, k| h[k] = Parsanol::IntervalTree.new }
75
+ @edits = Parsanol::EditTracker.new
76
+ end
77
+
78
+ # Cut operator support for aggressive eviction
79
+ @cut_pos = 0
80
+
81
+ # Determine adaptive cache threshold
82
+ threshold = adaptive_cache_threshold
83
+ if threshold.nil? && parser_class
84
+ name = parser_class.name&.split("::")&.last
85
+ threshold = PARSER_CACHE_LIMITS[name] || PARSER_CACHE_LIMITS[:default]
86
+ end
87
+ threshold ||= PARSER_CACHE_LIMITS[:default]
88
+
89
+ @adaptive_threshold = threshold
90
+ @input_len = nil
91
+ @caching_active = false
92
+ @backtrack_events = 0
93
+ end
94
+
95
+ # Attempts to parse using memoization. Returns cached result if available,
96
+ # otherwise executes the parser and caches the result.
97
+ #
98
+ # @param atom [Parsanol::Atoms::Base] parser to apply
99
+ # @param src [Parsanol::Source] input source
100
+ # @param must_consume_all [Boolean] require complete consumption
101
+ # @return [Array(Boolean, Object)] parse result tuple
102
+ #
103
+ def try_with_cache(atom, src, must_consume_all)
104
+ # Skip caching for atoms that don't benefit from it
105
+ return atom.try(src, self, must_consume_all) unless atom.cached?
106
+
107
+ # Use interval-based caching if enabled
108
+ return try_with_interval(atom, src, must_consume_all) if @use_intervals
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
+ pos = src.bytepos
117
+ key = atom.object_id
118
+ entry = @memo[pos]
119
+
120
+ # Periodic cache eviction to prevent unbounded growth
121
+ if pos > @furthest_pos
122
+ @furthest_pos = pos
123
+ @evict_counter += 1
124
+
125
+ if @evict_counter >= @evict_interval
126
+ @evict_counter = 0
127
+ cutoff = pos - @evict_threshold
128
+ @memo.delete_if { |p, _| p < cutoff }
129
+ end
130
+ end
131
+
132
+ # Check for cache hit (avoid default-block Hash allocation per probe)
133
+ if entry&.key?(key)
134
+ @hit_stats[key] += 1
135
+ outcome, delta = entry[key]
136
+ src.bytepos = pos + delta
137
+ return outcome
138
+ end
139
+
140
+ # Cache miss - execute and store
141
+ @miss_stats[key] += 1
142
+ outcome = atom.try(src, self, must_consume_all)
143
+ delta = src.bytepos - pos
144
+
145
+ # Only cache if beneficial (heuristic)
146
+ attempts = @hit_stats[key] + @miss_stats[key]
147
+ if attempts <= @min_hits_for_cache || @hit_stats[key].positive?
148
+ (@memo[pos] ||= {})[key] =
149
+ [outcome,
150
+ delta]
151
+ end
152
+
153
+ outcome
154
+ end
155
+
156
+ # GPeg-style interval-based caching for incremental parsing.
157
+ #
158
+ # @param atom [Parsanol::Atoms::Base] parser to apply
159
+ # @param src [Parsanol::Source] input source
160
+ # @param must_consume_all [Boolean] require complete consumption
161
+ # @return [Array(Boolean, Object)] parse result tuple
162
+ #
163
+ def try_with_interval(atom, src, must_consume_all)
164
+ pos = src.bytepos
165
+ key = atom.object_id
166
+
167
+ tree = @interval_trees[key]
168
+ cached = tree.query_exact(pos, pos)
169
+
170
+ if cached
171
+ @hit_stats[key] += 1
172
+ outcome, delta = cached
173
+ src.bytepos = pos + delta
174
+ return outcome
175
+ end
176
+
177
+ @miss_stats[key] += 1
178
+ outcome = atom.try(src, self, must_consume_all)
179
+ delta = src.bytepos - pos
180
+ end_pos = pos + delta
181
+
182
+ attempts = @hit_stats[key] + @miss_stats[key]
183
+ if attempts <= @min_hits_for_cache || @hit_stats[key].positive?
184
+ tree.insert(pos, end_pos,
185
+ [outcome, delta])
186
+ end
187
+
188
+ outcome
189
+ end
190
+
191
+ # Pre-allocated result constants
192
+ SUCCESS_RESULT = [true, nil].freeze
193
+ ERROR_RESULT = [false, nil].freeze
194
+
195
+ # Reports an error at a specific position.
196
+ #
197
+ # @return [Array(Boolean, Object)] error result tuple
198
+ #
199
+ def err_at(*)
200
+ return [false, @reporter.err_at(*)] if @reporter
201
+
202
+ ERROR_RESULT
203
+ end
204
+
205
+ # Reports an error at the current position.
206
+ #
207
+ # @return [Array(Boolean, Object)] error result tuple
208
+ #
209
+ def err(*)
210
+ return [false, @reporter.err(*)] if @reporter
211
+
212
+ ERROR_RESULT
213
+ end
214
+
215
+ # Reports a successful parse.
216
+ #
217
+ # @return [Array(Boolean, Object)] success result tuple
218
+ #
219
+ def succ(*)
220
+ return SUCCESS_RESULT unless @reporter
221
+
222
+ val = @reporter.succ(*)
223
+ return SUCCESS_RESULT if val.nil?
224
+
225
+ [true, val]
226
+ end
227
+
228
+ # @return [Parsanol::Scope] capture variable bindings
229
+ attr_reader :captures
230
+
231
+ # @return [Parsanol::Pools::ArrayPool] array object pool
232
+ attr_reader :array_pool
233
+
234
+ # @return [Parsanol::Pools::BufferPool] buffer object pool
235
+ attr_reader :buffer_pool
236
+
237
+ # Acquires an empty array from the pool.
238
+ #
239
+ # @return [Array] cleared array ready for use
240
+ #
241
+ def acquire_array
242
+ @array_pool.acquire
243
+ end
244
+
245
+ # Returns an array to the pool for reuse.
246
+ #
247
+ # @param arr [Array] array to release
248
+ # @return [Boolean] true if pooled, false if discarded
249
+ #
250
+ def release_array(arr)
251
+ @array_pool.release(arr)
252
+ end
253
+
254
+ # Acquires a buffer with minimum capacity from the pool.
255
+ #
256
+ # @param size: [Integer] minimum required capacity
257
+ # @return [Parsanol::Buffer] buffer with capacity >= size
258
+ #
259
+ def acquire_buffer(size:)
260
+ @buffer_pool.acquire(size: size)
261
+ end
262
+
263
+ # Returns a buffer to the pool for reuse.
264
+ #
265
+ # @param buf [Parsanol::Buffer] buffer to release
266
+ # @return [Boolean] true if pooled, false if discarded
267
+ #
268
+ def release_buffer(buf)
269
+ @buffer_pool.release(buf)
270
+ end
271
+
272
+ # Creates a new capture scope for the duration of the block.
273
+ #
274
+ # @yield block executed in new scope
275
+ #
276
+ def scope
277
+ captures.push
278
+ yield
279
+ ensure
280
+ captures.pop
281
+ end
282
+
283
+ # Checks if interval-based caching is active.
284
+ #
285
+ # @return [Boolean] true if interval caching enabled
286
+ #
287
+ def use_tree_memoization?
288
+ @use_intervals
289
+ end
290
+
291
+ # Queries interval cache for a cached result.
292
+ #
293
+ # @param key [Integer] cache key (atom object_id)
294
+ # @param start_pos [Integer] starting position
295
+ # @return [Array, nil] cached [values, end_pos] or nil
296
+ #
297
+ def query_tree_memo(key, start_pos)
298
+ return nil unless @use_intervals
299
+
300
+ tree = @interval_trees[key]
301
+ matches = tree.query_overlapping(start_pos, start_pos + 1)
302
+ found = matches.find { |interval, _| interval[0] == start_pos }
303
+ found ? found[1] : nil
304
+ end
305
+
306
+ # Stores a result in the interval cache.
307
+ #
308
+ # @param key [Integer] cache key
309
+ # @param start_pos [Integer] start position
310
+ # @param values [Array] parsed values
311
+ # @param end_pos [Integer] end position
312
+ #
313
+ def store_tree_memo(key, start_pos, values, end_pos)
314
+ return unless @use_intervals
315
+
316
+ @interval_trees[key].insert(start_pos, end_pos, [values, end_pos])
317
+ end
318
+
319
+ # Marks a cut position for aggressive cache eviction.
320
+ # Called when a cut operator succeeds.
321
+ #
322
+ # @param position [Integer] cut position
323
+ #
324
+ def cut!(position)
325
+ @cut_pos = position
326
+ @memo.delete_if { |pos, _| pos < position }
327
+ end
328
+
329
+ private
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
+
346
+ # Lookup cached result (uses object_id for speed)
347
+ def lookup(atom, pos)
348
+ @memo[pos][atom.object_id]
349
+ end
350
+
351
+ # Store result in cache
352
+ def set(atom, pos, val)
353
+ @memo[pos][atom.object_id] = val
354
+ end
355
+ end
356
+ end
357
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Experimental: Position-based cache eviction for Context
4
+ # Based on PEG theory: in linear parsing, positions behind current position
5
+ # will never be revisited, so we can evict them to reduce memory
6
+
7
+ module Parsanol
8
+ module Atoms
9
+ class Context
10
+ # Add position tracking for cache eviction
11
+ attr_reader :current_position
12
+
13
+ def try_with_cache(obj, source, consume_all)
14
+ return obj.try(source, self, consume_all) unless obj.cached?
15
+
16
+ key = source.pos
17
+ @current_position = key
18
+ atom_cache = @cache[obj]
19
+
20
+ # Try to fetch from cache
21
+ return atom_cache.fetch(key) if atom_cache.key?(key)
22
+
23
+ # Cache miss - compute result
24
+ result = obj.try(source, self, consume_all)
25
+ atom_cache[key] = result
26
+
27
+ # Evict old positions if cache is getting large
28
+ # Keep only positions within a window of current position
29
+ if atom_cache.size > 100
30
+ min_pos = key - 50 # Keep 50 positions behind
31
+ atom_cache.delete_if { |pos, _| pos < min_pos }
32
+ end
33
+
34
+ result
35
+ end
36
+ end
37
+ end
38
+ end