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.
- checksums.yaml +7 -0
- data/HISTORY.txt +98 -0
- data/LICENSE +23 -0
- data/README.adoc +905 -0
- data/Rakefile +141 -0
- 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/ast_visitor.rb +122 -0
- data/lib/parsanol/atoms/alternative.rb +123 -0
- data/lib/parsanol/atoms/base.rb +208 -0
- data/lib/parsanol/atoms/can_flatten.rb +194 -0
- data/lib/parsanol/atoms/capture.rb +38 -0
- data/lib/parsanol/atoms/context.rb +357 -0
- data/lib/parsanol/atoms/context_optimized.rb +38 -0
- data/lib/parsanol/atoms/custom.rb +110 -0
- data/lib/parsanol/atoms/cut.rb +66 -0
- data/lib/parsanol/atoms/dsl.rb +96 -0
- data/lib/parsanol/atoms/dynamic.rb +39 -0
- data/lib/parsanol/atoms/entity.rb +75 -0
- data/lib/parsanol/atoms/ignored.rb +37 -0
- data/lib/parsanol/atoms/infix.rb +167 -0
- data/lib/parsanol/atoms/lookahead.rb +85 -0
- data/lib/parsanol/atoms/named.rb +74 -0
- data/lib/parsanol/atoms/re.rb +83 -0
- data/lib/parsanol/atoms/repetition.rb +277 -0
- data/lib/parsanol/atoms/scope.rb +35 -0
- data/lib/parsanol/atoms/sequence.rb +195 -0
- data/lib/parsanol/atoms/str.rb +109 -0
- data/lib/parsanol/atoms/visitor.rb +91 -0
- data/lib/parsanol/atoms.rb +46 -0
- data/lib/parsanol/buffer.rb +133 -0
- data/lib/parsanol/builder_callbacks.rb +353 -0
- data/lib/parsanol/cause.rb +122 -0
- data/lib/parsanol/context.rb +39 -0
- data/lib/parsanol/convenience.rb +36 -0
- data/lib/parsanol/edit_tracker.rb +111 -0
- data/lib/parsanol/error_reporter/contextual.rb +99 -0
- data/lib/parsanol/error_reporter/deepest.rb +120 -0
- data/lib/parsanol/error_reporter/tree.rb +63 -0
- data/lib/parsanol/error_reporter.rb +100 -0
- data/lib/parsanol/expression/treetop.rb +154 -0
- data/lib/parsanol/expression.rb +106 -0
- data/lib/parsanol/fast_mode.rb +187 -0
- data/lib/parsanol/first_set.rb +79 -0
- data/lib/parsanol/grammar_builder.rb +179 -0
- data/lib/parsanol/incremental_parser.rb +182 -0
- data/lib/parsanol/interval_tree.rb +226 -0
- data/lib/parsanol/lazy_result.rb +179 -0
- data/lib/parsanol/mermaid.rb +142 -0
- data/lib/parsanol/native/batch_decoder.rb +255 -0
- data/lib/parsanol/native/dynamic.rb +238 -0
- data/lib/parsanol/native/parser.rb +102 -0
- data/lib/parsanol/native/serializer.rb +252 -0
- data/lib/parsanol/native/transformer.rb +604 -0
- data/lib/parsanol/native/types.rb +29 -0
- data/lib/parsanol/native.rb +223 -0
- data/lib/parsanol/optimizer.rb +85 -0
- data/lib/parsanol/optimizers/choice_optimizer.rb +78 -0
- data/lib/parsanol/optimizers/cut_inserter.rb +182 -0
- data/lib/parsanol/optimizers/lookahead_optimizer.rb +56 -0
- data/lib/parsanol/optimizers/quantifier_optimizer.rb +60 -0
- data/lib/parsanol/optimizers/sequence_optimizer.rb +97 -0
- data/lib/parsanol/options/zero_copy.rb +127 -0
- data/lib/parsanol/options.rb +21 -0
- data/lib/parsanol/parallel.rb +128 -0
- data/lib/parsanol/parser.rb +242 -0
- data/lib/parsanol/parslet.rb +151 -0
- data/lib/parsanol/pattern/binding.rb +91 -0
- data/lib/parsanol/pattern.rb +162 -0
- data/lib/parsanol/pool.rb +219 -0
- data/lib/parsanol/pools/array_pool.rb +75 -0
- data/lib/parsanol/pools/buffer_pool.rb +182 -0
- data/lib/parsanol/pools/position_pool.rb +92 -0
- data/lib/parsanol/pools/slice_pool.rb +64 -0
- data/lib/parsanol/position.rb +94 -0
- data/lib/parsanol/resettable.rb +29 -0
- data/lib/parsanol/result.rb +46 -0
- data/lib/parsanol/result_builder.rb +208 -0
- data/lib/parsanol/result_stream.rb +266 -0
- data/lib/parsanol/rig/rspec.rb +71 -0
- data/lib/parsanol/rope.rb +81 -0
- data/lib/parsanol/scope.rb +104 -0
- data/lib/parsanol/slice.rb +160 -0
- data/lib/parsanol/source/line_cache.rb +102 -0
- data/lib/parsanol/source.rb +185 -0
- data/lib/parsanol/source_location.rb +167 -0
- data/lib/parsanol/streaming_parser.rb +124 -0
- data/lib/parsanol/string_view.rb +198 -0
- data/lib/parsanol/transform.rb +226 -0
- data/lib/parsanol/version.rb +5 -0
- data/lib/parsanol/wasm/README.md +80 -0
- data/lib/parsanol/wasm/package.json +51 -0
- data/lib/parsanol/wasm/parsanol.js +252 -0
- data/lib/parsanol/wasm/parslet.d.ts +129 -0
- data/lib/parsanol/wasm_parser.rb +240 -0
- data/lib/parsanol.rb +278 -0
- data/parsanol.gemspec +67 -0
- 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
|