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,266 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Parsanol
|
|
4
|
+
# Streaming result iterator for memory-efficient parsing.
|
|
5
|
+
#
|
|
6
|
+
# Provides an Enumerable interface over parse results, allowing
|
|
7
|
+
# incremental processing without materializing the entire tree.
|
|
8
|
+
# Uses depth-first traversal to minimize memory usage.
|
|
9
|
+
#
|
|
10
|
+
# == Motivation
|
|
11
|
+
#
|
|
12
|
+
# Traditional parsing materializes the entire parse tree in memory:
|
|
13
|
+
#
|
|
14
|
+
# results = parser.parse(large_input) # Full tree in memory
|
|
15
|
+
# results.each { |node| process(node) }
|
|
16
|
+
#
|
|
17
|
+
# For large inputs, this can consume significant memory. ResultStream
|
|
18
|
+
# provides lazy iteration without full tree materialization:
|
|
19
|
+
#
|
|
20
|
+
# stream = ResultStream.new(parser.parse(input))
|
|
21
|
+
# stream.each { |node| process(node) } # Processes incrementally
|
|
22
|
+
#
|
|
23
|
+
# == Usage
|
|
24
|
+
#
|
|
25
|
+
# Basic iteration:
|
|
26
|
+
#
|
|
27
|
+
# stream = ResultStream.new(parse_tree)
|
|
28
|
+
# stream.each { |node| puts node }
|
|
29
|
+
#
|
|
30
|
+
# Filtering (leverages Enumerable):
|
|
31
|
+
#
|
|
32
|
+
# stream.select { |node| node.is_a?(Hash) }.each { |hash| process(hash) }
|
|
33
|
+
#
|
|
34
|
+
# Mapping:
|
|
35
|
+
#
|
|
36
|
+
# transformed = stream.map { |node| transform(node) }
|
|
37
|
+
#
|
|
38
|
+
# == Performance Characteristics
|
|
39
|
+
#
|
|
40
|
+
# - Memory: O(tree depth) instead of O(tree size)
|
|
41
|
+
# - Speed: Minimal overhead (~1-2% vs direct iteration)
|
|
42
|
+
# - Lazy evaluation: Nodes processed on-demand
|
|
43
|
+
#
|
|
44
|
+
# == Integration with Parser
|
|
45
|
+
#
|
|
46
|
+
# Can be used directly with parse results:
|
|
47
|
+
#
|
|
48
|
+
# parser = MyParser.new
|
|
49
|
+
# result = parser.parse(input)
|
|
50
|
+
# stream = ResultStream.new(result)
|
|
51
|
+
#
|
|
52
|
+
# Or through the optional stream method on Base:
|
|
53
|
+
#
|
|
54
|
+
# stream = parser.stream(input) # If available
|
|
55
|
+
#
|
|
56
|
+
class ResultStream
|
|
57
|
+
include Enumerable
|
|
58
|
+
|
|
59
|
+
# Creates a new result stream.
|
|
60
|
+
#
|
|
61
|
+
# @param tree [Object] Parse tree (Hash, Array, or scalar)
|
|
62
|
+
def initialize(tree)
|
|
63
|
+
@tree = tree
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Iterates over all nodes in the parse tree.
|
|
67
|
+
# Uses depth-first traversal to minimize memory usage.
|
|
68
|
+
#
|
|
69
|
+
# Traversal order:
|
|
70
|
+
# 1. Current node (pre-order)
|
|
71
|
+
# 2. Child nodes (recursive)
|
|
72
|
+
#
|
|
73
|
+
# This ensures that:
|
|
74
|
+
# - Only the current path is kept in memory (stack)
|
|
75
|
+
# - Parent nodes are yielded before children
|
|
76
|
+
# - Natural processing order for most use cases
|
|
77
|
+
#
|
|
78
|
+
# @yield [node] Each node in the tree
|
|
79
|
+
# @yieldparam node [Object] Current node (Hash, Array, or scalar)
|
|
80
|
+
# @return [Enumerator] if no block given
|
|
81
|
+
#
|
|
82
|
+
# @example Basic iteration
|
|
83
|
+
# stream.each { |node| puts node.class }
|
|
84
|
+
#
|
|
85
|
+
# @example Lazy enumeration
|
|
86
|
+
# enum = stream.each # Returns Enumerator
|
|
87
|
+
# enum.next # Get next node
|
|
88
|
+
#
|
|
89
|
+
def each(&)
|
|
90
|
+
return enum_for(:each) unless block_given?
|
|
91
|
+
|
|
92
|
+
traverse(@tree, &)
|
|
93
|
+
self
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Filters nodes by type.
|
|
97
|
+
#
|
|
98
|
+
# @param klass [Class] Class to filter by
|
|
99
|
+
# @return [Enumerator] Filtered nodes
|
|
100
|
+
#
|
|
101
|
+
# @example Get all hash nodes
|
|
102
|
+
# stream.nodes_of_type(Hash)
|
|
103
|
+
#
|
|
104
|
+
def nodes_of_type(klass)
|
|
105
|
+
grep(klass)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Returns all hash nodes in the tree.
|
|
109
|
+
#
|
|
110
|
+
# @return [Enumerator] Hash nodes
|
|
111
|
+
#
|
|
112
|
+
# @example
|
|
113
|
+
# stream.hashes.each { |h| puts h.keys }
|
|
114
|
+
#
|
|
115
|
+
def hashes
|
|
116
|
+
nodes_of_type(Hash)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Returns all array nodes in the tree.
|
|
120
|
+
#
|
|
121
|
+
# @return [Enumerator] Array nodes
|
|
122
|
+
#
|
|
123
|
+
# @example
|
|
124
|
+
# stream.arrays.each { |a| puts a.size }
|
|
125
|
+
#
|
|
126
|
+
def arrays
|
|
127
|
+
nodes_of_type(Array)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Returns all scalar nodes (non-Hash, non-Array).
|
|
131
|
+
#
|
|
132
|
+
# @return [Enumerator] Scalar nodes
|
|
133
|
+
#
|
|
134
|
+
# @example
|
|
135
|
+
# stream.scalars.each { |s| puts s }
|
|
136
|
+
#
|
|
137
|
+
def scalars
|
|
138
|
+
select { |node| !node.is_a?(Hash) && !node.is_a?(Array) }
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Returns nodes matching a predicate at a specific depth.
|
|
142
|
+
#
|
|
143
|
+
# @param depth [Integer] Tree depth (0 = root)
|
|
144
|
+
# @yield [node] Predicate to test each node
|
|
145
|
+
# @return [Enumerator] Matching nodes
|
|
146
|
+
#
|
|
147
|
+
# @example Get all nodes at depth 2
|
|
148
|
+
# stream.at_depth(2) { true }
|
|
149
|
+
#
|
|
150
|
+
def at_depth(target_depth, &predicate)
|
|
151
|
+
predicate ||= proc { true }
|
|
152
|
+
depth_traverse(@tree, 0, target_depth, &predicate)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Counts total nodes in the tree.
|
|
156
|
+
#
|
|
157
|
+
# @return [Integer] Total node count
|
|
158
|
+
#
|
|
159
|
+
# @example
|
|
160
|
+
# stream.count # => 42
|
|
161
|
+
#
|
|
162
|
+
def count
|
|
163
|
+
counter = 0
|
|
164
|
+
each { counter += 1 }
|
|
165
|
+
counter
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Returns maximum depth of the tree.
|
|
169
|
+
#
|
|
170
|
+
# @return [Integer] Maximum depth
|
|
171
|
+
#
|
|
172
|
+
# @example
|
|
173
|
+
# stream.max_depth # => 5
|
|
174
|
+
#
|
|
175
|
+
def max_depth
|
|
176
|
+
find_max_depth(@tree, 0)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
private
|
|
180
|
+
|
|
181
|
+
# Depth-first tree traversal with pre-order visiting.
|
|
182
|
+
#
|
|
183
|
+
# @param node [Object] Current node
|
|
184
|
+
# @yield [node] Each visited node
|
|
185
|
+
#
|
|
186
|
+
def traverse(node, &block)
|
|
187
|
+
# Yield current node first (pre-order)
|
|
188
|
+
yield node
|
|
189
|
+
|
|
190
|
+
# Recursively traverse children
|
|
191
|
+
case node
|
|
192
|
+
when Array
|
|
193
|
+
node.each { |item| traverse(item, &block) }
|
|
194
|
+
when Hash
|
|
195
|
+
node.each_value { |value| traverse(value, &block) }
|
|
196
|
+
end
|
|
197
|
+
# Scalars have no children, stop here
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Depth-aware traversal for filtering by level.
|
|
201
|
+
#
|
|
202
|
+
# @param node [Object] Current node
|
|
203
|
+
# @param current_depth [Integer] Current depth in tree
|
|
204
|
+
# @param target_depth [Integer] Depth to match
|
|
205
|
+
# @yield [node] Matching nodes at target depth
|
|
206
|
+
# @return [Enumerator]
|
|
207
|
+
#
|
|
208
|
+
def depth_traverse(node, current_depth, target_depth, &block)
|
|
209
|
+
unless block
|
|
210
|
+
return enum_for(:depth_traverse, node, current_depth,
|
|
211
|
+
target_depth)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Check if we're at target depth
|
|
215
|
+
return [node].to_enum if current_depth == target_depth && yield(node)
|
|
216
|
+
|
|
217
|
+
# Recurse to children if not at target depth yet
|
|
218
|
+
results = []
|
|
219
|
+
if current_depth < target_depth
|
|
220
|
+
case node
|
|
221
|
+
when Array
|
|
222
|
+
node.each do |item|
|
|
223
|
+
depth_traverse(item, current_depth + 1, target_depth,
|
|
224
|
+
&block).each do |result|
|
|
225
|
+
results << result
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
when Hash
|
|
229
|
+
node.each_value do |value|
|
|
230
|
+
depth_traverse(value, current_depth + 1, target_depth,
|
|
231
|
+
&block).each do |result|
|
|
232
|
+
results << result
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
results.to_enum
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Find maximum depth of tree recursively.
|
|
242
|
+
#
|
|
243
|
+
# @param node [Object] Current node
|
|
244
|
+
# @param current_depth [Integer] Current depth
|
|
245
|
+
# @return [Integer] Maximum depth from this node
|
|
246
|
+
#
|
|
247
|
+
def find_max_depth(node, current_depth)
|
|
248
|
+
max = current_depth
|
|
249
|
+
|
|
250
|
+
case node
|
|
251
|
+
when Array
|
|
252
|
+
node.each do |item|
|
|
253
|
+
depth = find_max_depth(item, current_depth + 1)
|
|
254
|
+
max = depth if depth > max
|
|
255
|
+
end
|
|
256
|
+
when Hash
|
|
257
|
+
node.each_value do |value|
|
|
258
|
+
depth = find_max_depth(value, current_depth + 1)
|
|
259
|
+
max = depth if depth > max
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
max
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# RSpec matcher for parsing expectations. Provides a fluent DSL for
|
|
4
|
+
# specifying parsing behavior in tests.
|
|
5
|
+
#
|
|
6
|
+
# @example Basic usage
|
|
7
|
+
# expect(parser).to parse("input")
|
|
8
|
+
#
|
|
9
|
+
# @example With expected output
|
|
10
|
+
# expect(parser).to parse("123").as(123)
|
|
11
|
+
#
|
|
12
|
+
# @example With block validation
|
|
13
|
+
# expect(parser).to parse("input").as { |result| result.size > 0 }
|
|
14
|
+
#
|
|
15
|
+
# Inspired by RSpec matcher patterns and Parslet's testing utilities.
|
|
16
|
+
#
|
|
17
|
+
RSpec::Matchers.define(:parse) do |input_text, options|
|
|
18
|
+
expected_output = nil
|
|
19
|
+
validator_block = nil
|
|
20
|
+
actual_result = nil
|
|
21
|
+
error_trace = nil
|
|
22
|
+
|
|
23
|
+
match do |parser_instance|
|
|
24
|
+
actual_result = parser_instance.parse(input_text)
|
|
25
|
+
if validator_block
|
|
26
|
+
validator_block.call(actual_result)
|
|
27
|
+
else
|
|
28
|
+
expected_output.nil? || expected_output == actual_result
|
|
29
|
+
end
|
|
30
|
+
rescue Parsanol::ParseFailed => e
|
|
31
|
+
error_trace = e.parse_failure_cause.ascii_tree if options && options[:trace]
|
|
32
|
+
false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
failure_message do |parser_instance|
|
|
36
|
+
if validator_block
|
|
37
|
+
"expected output of parsing #{input_text.inspect} with " \
|
|
38
|
+
"#{parser_instance.inspect} to meet block conditions, but it didn't"
|
|
39
|
+
else
|
|
40
|
+
msg = if expected_output
|
|
41
|
+
"expected output of parsing #{input_text.inspect} with " \
|
|
42
|
+
"#{parser_instance.inspect} to equal #{expected_output.inspect}, " \
|
|
43
|
+
"but was #{actual_result.inspect}"
|
|
44
|
+
else
|
|
45
|
+
"expected #{parser_instance.inspect} to be able to parse " \
|
|
46
|
+
"#{input_text.inspect}"
|
|
47
|
+
end
|
|
48
|
+
msg += "\n#{error_trace}" if error_trace
|
|
49
|
+
msg
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
failure_message_when_negated do |parser_instance|
|
|
54
|
+
if validator_block
|
|
55
|
+
"expected output of parsing #{input_text.inspect} with " \
|
|
56
|
+
"#{parser_instance.inspect} not to meet block conditions, but it did"
|
|
57
|
+
elsif expected_output
|
|
58
|
+
"expected output of parsing #{input_text.inspect} with " \
|
|
59
|
+
"#{parser_instance.inspect} not to equal #{expected_output.inspect}"
|
|
60
|
+
else
|
|
61
|
+
"expected #{parser_instance.inspect} to not parse " \
|
|
62
|
+
"#{input_text.inspect}, but it did"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Chain method for specifying expected output or validation block
|
|
67
|
+
chain :as do |expected = nil, &block|
|
|
68
|
+
expected_output = expected
|
|
69
|
+
validator_block = block
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Parsanol
|
|
4
|
+
# Rope data structure for efficient string accumulation.
|
|
5
|
+
#
|
|
6
|
+
# Uses deferred concatenation to avoid O(n²) repeated string building.
|
|
7
|
+
# Segments are accumulated in O(1) time and joined once in O(n) time when
|
|
8
|
+
# converted to a final string.
|
|
9
|
+
#
|
|
10
|
+
# @example Basic usage
|
|
11
|
+
# rope = Rope.new
|
|
12
|
+
# rope.append('hello')
|
|
13
|
+
# rope.append(' ')
|
|
14
|
+
# rope.append('world')
|
|
15
|
+
# rope.to_s # => "hello world"
|
|
16
|
+
#
|
|
17
|
+
# @example With Slices
|
|
18
|
+
# rope = Rope.new
|
|
19
|
+
# rope.append(Slice.new(0, 'hello'))
|
|
20
|
+
# rope.append(Slice.new(5, ' world'))
|
|
21
|
+
# rope.to_s # => "hello world"
|
|
22
|
+
#
|
|
23
|
+
class Rope
|
|
24
|
+
# Creates a new empty Rope.
|
|
25
|
+
def initialize
|
|
26
|
+
@segments = []
|
|
27
|
+
@frozen = false
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Appends a string or Slice to the rope.
|
|
31
|
+
#
|
|
32
|
+
# This is an O(1) operation. The segment is stored as-is and will be
|
|
33
|
+
# joined later when {#to_s} is called.
|
|
34
|
+
#
|
|
35
|
+
# @param segment [String, Slice] The segment to append
|
|
36
|
+
# @return [Rope] self for method chaining
|
|
37
|
+
# @raise [FrozenError] if rope has been frozen by calling {#to_s}
|
|
38
|
+
def append(segment)
|
|
39
|
+
raise FrozenError, "can't modify frozen Rope" if @frozen
|
|
40
|
+
|
|
41
|
+
@segments << segment
|
|
42
|
+
self
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Converts the rope to a final string.
|
|
46
|
+
#
|
|
47
|
+
# This is an O(n) operation performed once. After calling this method,
|
|
48
|
+
# the rope is frozen and cannot be modified further.
|
|
49
|
+
#
|
|
50
|
+
# @return [String] The concatenated result of all segments
|
|
51
|
+
def to_s
|
|
52
|
+
@frozen = true
|
|
53
|
+
@segments.join
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Checks if the rope is empty (contains no segments).
|
|
57
|
+
#
|
|
58
|
+
# @return [Boolean] true if no segments have been appended
|
|
59
|
+
def empty?
|
|
60
|
+
@segments.empty?
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Estimates the total size of all segments.
|
|
64
|
+
#
|
|
65
|
+
# This is an estimate because segments may be Slice objects or other
|
|
66
|
+
# types that respond to #size or #to_s.
|
|
67
|
+
#
|
|
68
|
+
# @return [Integer] The sum of all segment sizes
|
|
69
|
+
def size
|
|
70
|
+
@segments.sum { |s| s.respond_to?(:size) ? s.size : s.to_s.size }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Creates a rope from an existing string.
|
|
74
|
+
#
|
|
75
|
+
# @param str [String] The string to initialize the rope with
|
|
76
|
+
# @return [Rope] A new rope containing the string
|
|
77
|
+
def self.from_string(str)
|
|
78
|
+
new.tap { |r| r.append(str) unless str.empty? }
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Scoped variable bindings for parser context management. Provides a
|
|
4
|
+
# stack-like interface for creating nested scopes that inherit from
|
|
5
|
+
# parent scopes.
|
|
6
|
+
#
|
|
7
|
+
# @example Basic usage
|
|
8
|
+
# scope = Parsanol::Scope.new
|
|
9
|
+
# scope[:x] = 1
|
|
10
|
+
# scope.push # Create nested scope
|
|
11
|
+
# scope[:x] # => 1 (inherited from parent)
|
|
12
|
+
# scope[:y] = 2
|
|
13
|
+
# scope.pop # Return to parent scope
|
|
14
|
+
# scope[:y] # raises NotFound
|
|
15
|
+
#
|
|
16
|
+
# Inspired by lexical scoping patterns in programming languages.
|
|
17
|
+
#
|
|
18
|
+
module Parsanol
|
|
19
|
+
class Scope
|
|
20
|
+
# Error raised when attempting to access an undefined binding.
|
|
21
|
+
class UndefinedVariable < StandardError
|
|
22
|
+
end
|
|
23
|
+
# Legacy alias for backward compatibility
|
|
24
|
+
NotFound = UndefinedVariable
|
|
25
|
+
|
|
26
|
+
# Internal class representing a single scope level. Each frame can
|
|
27
|
+
# look up values in its parent frame if not found locally.
|
|
28
|
+
class Frame
|
|
29
|
+
# @return [Frame, nil] parent frame in the scope chain
|
|
30
|
+
attr_reader :parent_frame
|
|
31
|
+
|
|
32
|
+
# Creates a new frame optionally linked to a parent.
|
|
33
|
+
#
|
|
34
|
+
# @param parent [Frame, nil] the parent frame to inherit from
|
|
35
|
+
def initialize(parent = nil)
|
|
36
|
+
@parent_frame = parent
|
|
37
|
+
@bindings = {}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Retrieves a value by key, searching parent frames if necessary.
|
|
41
|
+
#
|
|
42
|
+
# @param key [Symbol] the variable name to look up
|
|
43
|
+
# @return [Object] the bound value
|
|
44
|
+
# @raise [UndefinedVariable] if key not found in any frame
|
|
45
|
+
def fetch(key)
|
|
46
|
+
if @bindings.key?(key)
|
|
47
|
+
@bindings[key]
|
|
48
|
+
elsif @parent_frame
|
|
49
|
+
@parent_frame.fetch(key)
|
|
50
|
+
else
|
|
51
|
+
raise UndefinedVariable, "No binding for #{key.inspect}"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Stores a value in the current frame.
|
|
56
|
+
#
|
|
57
|
+
# @param key [Symbol] the variable name
|
|
58
|
+
# @param value [Object] the value to bind
|
|
59
|
+
# @return [Object] the stored value
|
|
60
|
+
def store(key, value)
|
|
61
|
+
@bindings[key] = value
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
alias [] fetch
|
|
65
|
+
alias []= store
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Creates a new scope with an empty root frame.
|
|
69
|
+
def initialize
|
|
70
|
+
@active_frame = Frame.new
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Retrieves a value from the current scope chain.
|
|
74
|
+
#
|
|
75
|
+
# @param key [Symbol] the variable name
|
|
76
|
+
# @return [Object] the bound value
|
|
77
|
+
# @raise [UndefinedVariable] if not found
|
|
78
|
+
def [](key)
|
|
79
|
+
@active_frame.fetch(key)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Stores a value in the current frame.
|
|
83
|
+
#
|
|
84
|
+
# @param key [Symbol] the variable name
|
|
85
|
+
# @param value [Object] the value to bind
|
|
86
|
+
def []=(key, value)
|
|
87
|
+
@active_frame.store(key, value)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Creates a new nested scope frame. Call #pop to restore.
|
|
91
|
+
#
|
|
92
|
+
# @return [void]
|
|
93
|
+
def push
|
|
94
|
+
@active_frame = Frame.new(@active_frame)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Returns to the parent scope frame.
|
|
98
|
+
#
|
|
99
|
+
# @return [void]
|
|
100
|
+
def pop
|
|
101
|
+
@active_frame = @active_frame.parent_frame
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Source position tracker for parsed content.
|
|
4
|
+
# Preserves both the string value and its byte offset in the original input,
|
|
5
|
+
# enabling precise error reporting and source mapping.
|
|
6
|
+
#
|
|
7
|
+
# Line/column is computed LAZILY on first access — zero overhead
|
|
8
|
+
# for users who don't need position info.
|
|
9
|
+
module Parsanol
|
|
10
|
+
class Slice
|
|
11
|
+
include Parsanol::Resettable
|
|
12
|
+
|
|
13
|
+
attr_reader :content, :input
|
|
14
|
+
|
|
15
|
+
def initialize(byte_offset = 0, string_content = "", input = nil)
|
|
16
|
+
@byte_position = byte_offset
|
|
17
|
+
@content = string_content
|
|
18
|
+
@input = input
|
|
19
|
+
@line_and_column = nil
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def reset!(new_offset = 0, new_content = "", new_input = nil)
|
|
23
|
+
@byte_position = new_offset
|
|
24
|
+
@content = new_content
|
|
25
|
+
@input = new_input
|
|
26
|
+
@line_and_column = nil
|
|
27
|
+
self
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.from_rope(rope, offset, input = nil)
|
|
31
|
+
new(offset, rope.to_s, input)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Position
|
|
35
|
+
def offset
|
|
36
|
+
@byte_position
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
alias bytepos offset
|
|
40
|
+
alias charpos offset
|
|
41
|
+
alias str content
|
|
42
|
+
|
|
43
|
+
# Equality
|
|
44
|
+
def ==(other)
|
|
45
|
+
return content == other if other.is_a?(String)
|
|
46
|
+
return content == other.content if other.is_a?(Parsanol::Slice)
|
|
47
|
+
|
|
48
|
+
content == other
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def eql?(other)
|
|
52
|
+
other.is_a?(Parsanol::Slice) && content.eql?(other.content)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def hash
|
|
56
|
+
[content, offset].hash
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Delegated methods
|
|
60
|
+
def match(pattern)
|
|
61
|
+
content.match(pattern)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def size
|
|
65
|
+
content.size
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
alias length size
|
|
69
|
+
|
|
70
|
+
def empty?
|
|
71
|
+
content.empty?
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def +(other)
|
|
75
|
+
self.class.new(@byte_position, content + other.to_s, @input)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Lazy line/column — computed once and cached.
|
|
79
|
+
def line_and_column
|
|
80
|
+
raise ArgumentError, "Line/column requires input" unless @input
|
|
81
|
+
|
|
82
|
+
@line_and_column ||= compute_line_and_column
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Conversions
|
|
86
|
+
def to_str
|
|
87
|
+
content.to_s
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
alias to_s to_str
|
|
91
|
+
|
|
92
|
+
def to_slice
|
|
93
|
+
self
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def to_sym
|
|
97
|
+
content.to_sym
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def to_i
|
|
101
|
+
content.to_i
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def to_f
|
|
105
|
+
content.to_f
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def inspect
|
|
109
|
+
"#{content.inspect}@#{offset}"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# JSON
|
|
113
|
+
def to_json(*)
|
|
114
|
+
as_json.to_json(*)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def as_json
|
|
118
|
+
result = { "value" => content, "offset" => offset, "length" => length }
|
|
119
|
+
if @input
|
|
120
|
+
line, column = line_and_column
|
|
121
|
+
result["line"] = line
|
|
122
|
+
result["column"] = column
|
|
123
|
+
end
|
|
124
|
+
result
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Source span
|
|
128
|
+
def to_span(_input = nil)
|
|
129
|
+
line, column = line_and_column
|
|
130
|
+
end_line, end_column = line_and_column_at(offset + length)
|
|
131
|
+
start_pos = SourcePosition.new(offset: offset, line: line, column: column)
|
|
132
|
+
end_pos = SourcePosition.new(offset: offset + length, line: end_line,
|
|
133
|
+
column: end_column)
|
|
134
|
+
SourceSpan.new(start_pos: start_pos, end_pos: end_pos)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
private
|
|
138
|
+
|
|
139
|
+
def compute_line_and_column
|
|
140
|
+
line_and_column_at(@byte_position)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Unified line/column computation:
|
|
144
|
+
# - String input: compute from input string
|
|
145
|
+
# - LineCache: delegate to cache
|
|
146
|
+
def line_and_column_at(pos)
|
|
147
|
+
if @input.respond_to?(:line_and_column)
|
|
148
|
+
# LineCache or duck-typed object
|
|
149
|
+
@input.line_and_column(pos)
|
|
150
|
+
else
|
|
151
|
+
# String input
|
|
152
|
+
prefix = @input.byteslice(0, pos) || ""
|
|
153
|
+
line = 1 + prefix.count("\n")
|
|
154
|
+
last_nl = prefix.rindex("\n")
|
|
155
|
+
column = last_nl ? pos - last_nl : pos + 1
|
|
156
|
+
[line, column]
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|