parsanol 1.0.1-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 +7 -0
- data/HISTORY.txt +12 -0
- data/LICENSE +23 -0
- data/README.adoc +487 -0
- data/Rakefile +135 -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 +122 -0
- data/lib/parsanol/atoms/base.rb +202 -0
- data/lib/parsanol/atoms/can_flatten.rb +194 -0
- data/lib/parsanol/atoms/capture.rb +38 -0
- data/lib/parsanol/atoms/context.rb +334 -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 +162 -0
- data/lib/parsanol/atoms/lookahead.rb +82 -0
- data/lib/parsanol/atoms/named.rb +74 -0
- data/lib/parsanol/atoms/re.rb +83 -0
- data/lib/parsanol/atoms/repetition.rb +259 -0
- data/lib/parsanol/atoms/scope.rb +35 -0
- data/lib/parsanol/atoms/sequence.rb +194 -0
- data/lib/parsanol/atoms/str.rb +103 -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 +149 -0
- data/lib/parsanol/first_set.rb +79 -0
- data/lib/parsanol/grammar_builder.rb +177 -0
- data/lib/parsanol/incremental_parser.rb +177 -0
- data/lib/parsanol/interval_tree.rb +217 -0
- data/lib/parsanol/lazy_result.rb +179 -0
- data/lib/parsanol/lexer.rb +144 -0
- data/lib/parsanol/mermaid.rb +139 -0
- data/lib/parsanol/native/parser.rb +612 -0
- data/lib/parsanol/native/serializer.rb +248 -0
- data/lib/parsanol/native/transformer.rb +435 -0
- data/lib/parsanol/native/types.rb +42 -0
- data/lib/parsanol/native.rb +217 -0
- data/lib/parsanol/optimizer.rb +85 -0
- data/lib/parsanol/optimizers/choice_optimizer.rb +78 -0
- data/lib/parsanol/optimizers/cut_inserter.rb +179 -0
- data/lib/parsanol/optimizers/lookahead_optimizer.rb +50 -0
- data/lib/parsanol/optimizers/quantifier_optimizer.rb +60 -0
- data/lib/parsanol/optimizers/sequence_optimizer.rb +97 -0
- data/lib/parsanol/options/ruby_transform.rb +107 -0
- data/lib/parsanol/options/serialized.rb +94 -0
- data/lib/parsanol/options/zero_copy.rb +128 -0
- data/lib/parsanol/options.rb +20 -0
- data/lib/parsanol/parallel.rb +133 -0
- data/lib/parsanol/parser.rb +182 -0
- data/lib/parsanol/parslet.rb +151 -0
- data/lib/parsanol/pattern/binding.rb +91 -0
- data/lib/parsanol/pattern.rb +159 -0
- data/lib/parsanol/pool.rb +219 -0
- data/lib/parsanol/pools/array_pool.rb +75 -0
- data/lib/parsanol/pools/buffer_pool.rb +175 -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 +261 -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 +146 -0
- data/lib/parsanol/source/line_cache.rb +109 -0
- data/lib/parsanol/source.rb +180 -0
- data/lib/parsanol/source_location.rb +167 -0
- data/lib/parsanol/streaming_parser.rb +124 -0
- data/lib/parsanol/string_view.rb +195 -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 +280 -0
- data/parsanol-ruby.gemspec +67 -0
- metadata +280 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Encapsules the concept of a position inside a string.
|
|
4
|
+
#
|
|
5
|
+
module Parsanol
|
|
6
|
+
class Position
|
|
7
|
+
include Parsanol::Resettable
|
|
8
|
+
|
|
9
|
+
# Changed to accessor to support pooling
|
|
10
|
+
attr_accessor :bytepos
|
|
11
|
+
attr_accessor :string, :charpos
|
|
12
|
+
|
|
13
|
+
include Comparable
|
|
14
|
+
|
|
15
|
+
def initialize(string, bytepos, charpos = nil)
|
|
16
|
+
@string = string
|
|
17
|
+
@bytepos = bytepos
|
|
18
|
+
@charpos = charpos
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Reset the position for reuse in object pooling.
|
|
22
|
+
# This allows the position to be reinitialized with new values for efficient reuse.
|
|
23
|
+
#
|
|
24
|
+
# @param string [String] Source string for position tracking
|
|
25
|
+
# @param bytepos [Integer] New byte position
|
|
26
|
+
# @param charpos [Integer, nil] Optional character position
|
|
27
|
+
# @return [self] Returns self for method chaining
|
|
28
|
+
#
|
|
29
|
+
def reset!(string, bytepos, charpos = nil)
|
|
30
|
+
@string = string
|
|
31
|
+
@bytepos = bytepos
|
|
32
|
+
@charpos = charpos
|
|
33
|
+
self
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def charpos
|
|
37
|
+
# If charpos was provided during initialization, use it
|
|
38
|
+
return @charpos if @charpos
|
|
39
|
+
|
|
40
|
+
# Cache the calculated charpos to avoid repeated calculations
|
|
41
|
+
@charpos ||= calculate_charpos
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def calculate_charpos
|
|
47
|
+
# Calculate it based on platform
|
|
48
|
+
if defined?(RUBY_ENGINE) && RUBY_ENGINE == 'opal'
|
|
49
|
+
# In Opal, convert byte position to character position.
|
|
50
|
+
# We need to calculate how many characters occupy the first @bytepos bytes.
|
|
51
|
+
`
|
|
52
|
+
var str = #{@string};
|
|
53
|
+
var bytePos = #{@bytepos};
|
|
54
|
+
var chars = Array.from(str);
|
|
55
|
+
var byteCount = 0;
|
|
56
|
+
var charCount = 0;
|
|
57
|
+
|
|
58
|
+
for (var i = 0; i < chars.length; i++) {
|
|
59
|
+
if (byteCount >= bytePos) break;
|
|
60
|
+
|
|
61
|
+
var char = chars[i];
|
|
62
|
+
var codePoint = char.codePointAt(0);
|
|
63
|
+
|
|
64
|
+
// Calculate UTF-8 byte length for this character
|
|
65
|
+
if (codePoint < 0x80) {
|
|
66
|
+
byteCount += 1;
|
|
67
|
+
} else if (codePoint < 0x800) {
|
|
68
|
+
byteCount += 2;
|
|
69
|
+
} else if (codePoint < 0x10000) {
|
|
70
|
+
byteCount += 3;
|
|
71
|
+
} else {
|
|
72
|
+
byteCount += 4;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (byteCount <= bytePos) {
|
|
76
|
+
charCount++;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return charCount;
|
|
81
|
+
`
|
|
82
|
+
else
|
|
83
|
+
# Ruby: Use standard byteslice which handles Unicode correctly
|
|
84
|
+
@string.byteslice(0, @bytepos).size
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
public
|
|
89
|
+
|
|
90
|
+
def <=>(other)
|
|
91
|
+
bytepos <=> other.bytepos
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Parsanol
|
|
4
|
+
# Module for objects that can be reset for object pool reuse.
|
|
5
|
+
#
|
|
6
|
+
# Including this module signals that an object supports the reset!
|
|
7
|
+
# method for pooling purposes. This provides an explicit contract
|
|
8
|
+
# instead of duck-typing with respond_to?.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# class MyPooledObject
|
|
12
|
+
# include Parsanol::Resettable
|
|
13
|
+
#
|
|
14
|
+
# def reset!
|
|
15
|
+
# @state = nil
|
|
16
|
+
# self
|
|
17
|
+
# end
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
module Resettable
|
|
21
|
+
# Reset object state for reuse in object pool.
|
|
22
|
+
#
|
|
23
|
+
# @return [self] for method chaining
|
|
24
|
+
# @raise [NotImplementedError] if not implemented by including class
|
|
25
|
+
def reset!
|
|
26
|
+
raise NotImplementedError, "#{self.class} must implement #reset!"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Phase 58: Result wrapper to replace [success, value] arrays
|
|
4
|
+
#
|
|
5
|
+
# This class wraps parse results to eliminate array allocations.
|
|
6
|
+
# Instead of [true, value] or [false, cause], we use Result objects.
|
|
7
|
+
#
|
|
8
|
+
# Benefits:
|
|
9
|
+
# - Eliminates array allocations (40% reduction)
|
|
10
|
+
# - Cleaner API with success? method
|
|
11
|
+
# - Can be optimized further (object pooling, etc.)
|
|
12
|
+
#
|
|
13
|
+
module Parsanol
|
|
14
|
+
class Result
|
|
15
|
+
attr_reader :value
|
|
16
|
+
|
|
17
|
+
def initialize(success, value)
|
|
18
|
+
@success = success
|
|
19
|
+
@value = value
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def success?
|
|
23
|
+
@success
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def error?
|
|
27
|
+
!@success
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Compatibility: Allow destructuring like arrays
|
|
31
|
+
# This enables gradual migration: result.success?, result.value
|
|
32
|
+
# or: success, value = result (array-like)
|
|
33
|
+
def to_ary
|
|
34
|
+
[@success, @value]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Factory methods for common cases
|
|
38
|
+
def self.success(value)
|
|
39
|
+
new(true, value)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.error(cause)
|
|
43
|
+
new(false, cause)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Parsanol
|
|
4
|
+
# Base class for efficient result construction.
|
|
5
|
+
#
|
|
6
|
+
# ResultBuilder provides specialized construction patterns that avoid
|
|
7
|
+
# intermediate array allocations by building results directly.
|
|
8
|
+
#
|
|
9
|
+
# == Usage
|
|
10
|
+
#
|
|
11
|
+
# builder = ResultBuilder.for(:repetition, context, estimated_size: 10)
|
|
12
|
+
# builder.add_element(value1)
|
|
13
|
+
# builder.add_element(value2)
|
|
14
|
+
# result = builder.build # Returns LazyResult
|
|
15
|
+
#
|
|
16
|
+
# == Builders
|
|
17
|
+
#
|
|
18
|
+
# - RepetitionBuilder: For repetition results
|
|
19
|
+
# - SequenceBuilder: For sequence results
|
|
20
|
+
# - HashBuilder: For named capture results
|
|
21
|
+
#
|
|
22
|
+
class ResultBuilder
|
|
23
|
+
# Factory method to create appropriate builder.
|
|
24
|
+
#
|
|
25
|
+
# @param type [Symbol] Builder type (:repetition, :sequence, :hash)
|
|
26
|
+
# @param context [Context] Parse context
|
|
27
|
+
# @param options [Hash] Builder options
|
|
28
|
+
# @return [ResultBuilder] Appropriate builder instance
|
|
29
|
+
#
|
|
30
|
+
def self.for(type, context, **options)
|
|
31
|
+
case type
|
|
32
|
+
when :repetition
|
|
33
|
+
RepetitionBuilder.new(context, **options)
|
|
34
|
+
when :sequence
|
|
35
|
+
SequenceBuilder.new(context, **options)
|
|
36
|
+
when :hash
|
|
37
|
+
HashBuilder.new(context, **options)
|
|
38
|
+
else
|
|
39
|
+
raise ArgumentError, "Unknown builder type: #{type}"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Initialize builder.
|
|
44
|
+
#
|
|
45
|
+
# @param context [Context] Parse context for buffer access
|
|
46
|
+
#
|
|
47
|
+
def initialize(context)
|
|
48
|
+
@context = context
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Add element to result (subclasses implement).
|
|
52
|
+
#
|
|
53
|
+
# @param value [Object] Value to add
|
|
54
|
+
# @return [self] For method chaining
|
|
55
|
+
#
|
|
56
|
+
def add_element(value)
|
|
57
|
+
raise NotImplementedError
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Build final result (subclasses implement).
|
|
61
|
+
#
|
|
62
|
+
# @return [Object] Constructed result
|
|
63
|
+
#
|
|
64
|
+
def build
|
|
65
|
+
raise NotImplementedError
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Release resources (subclasses implement).
|
|
69
|
+
#
|
|
70
|
+
# @return [void]
|
|
71
|
+
#
|
|
72
|
+
def release
|
|
73
|
+
# Default: no-op
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Builder for repetition results.
|
|
78
|
+
#
|
|
79
|
+
# Constructs [:repetition, ...] arrays efficiently.
|
|
80
|
+
#
|
|
81
|
+
class RepetitionBuilder < ResultBuilder
|
|
82
|
+
# Initialize repetition builder.
|
|
83
|
+
#
|
|
84
|
+
# @param context [Context] Parse context
|
|
85
|
+
# @param tag [Symbol] Tag to use (default: :repetition)
|
|
86
|
+
# @param estimated_size [Integer] Estimated element count
|
|
87
|
+
#
|
|
88
|
+
def initialize(context, tag: :repetition, estimated_size: 10)
|
|
89
|
+
super(context)
|
|
90
|
+
@tag = tag
|
|
91
|
+
@buffer = context.acquire_buffer(size: estimated_size + 1)
|
|
92
|
+
@buffer.push(@tag)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Add element to repetition.
|
|
96
|
+
#
|
|
97
|
+
# @param value [Object] Element to add
|
|
98
|
+
# @return [self]
|
|
99
|
+
#
|
|
100
|
+
def add_element(value)
|
|
101
|
+
@buffer.push(value)
|
|
102
|
+
self
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Build LazyResult.
|
|
106
|
+
#
|
|
107
|
+
# @return [LazyResult] Lazy repetition result
|
|
108
|
+
#
|
|
109
|
+
def build
|
|
110
|
+
Parsanol::LazyResult.new(@buffer, @context)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Release buffer on failure.
|
|
114
|
+
#
|
|
115
|
+
# @return [void]
|
|
116
|
+
#
|
|
117
|
+
def release
|
|
118
|
+
@context.release_buffer(@buffer) if @buffer
|
|
119
|
+
@buffer = nil
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Builder for sequence results.
|
|
124
|
+
#
|
|
125
|
+
# Constructs [:sequence, ...] arrays efficiently.
|
|
126
|
+
#
|
|
127
|
+
class SequenceBuilder < ResultBuilder
|
|
128
|
+
# Initialize sequence builder.
|
|
129
|
+
#
|
|
130
|
+
# @param context [Context] Parse context
|
|
131
|
+
# @param size [Integer] Expected sequence length
|
|
132
|
+
#
|
|
133
|
+
def initialize(context, size: 5)
|
|
134
|
+
super(context)
|
|
135
|
+
@buffer = context.acquire_buffer(size: size + 1)
|
|
136
|
+
@buffer.push(:sequence)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Add element to sequence.
|
|
140
|
+
#
|
|
141
|
+
# @param value [Object] Element to add
|
|
142
|
+
# @return [self]
|
|
143
|
+
#
|
|
144
|
+
def add_element(value)
|
|
145
|
+
@buffer.push(value) if value # Skip nil values
|
|
146
|
+
self
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Build LazyResult.
|
|
150
|
+
#
|
|
151
|
+
# @return [LazyResult] Lazy sequence result
|
|
152
|
+
#
|
|
153
|
+
def build
|
|
154
|
+
Parsanol::LazyResult.new(@buffer, @context)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Release buffer on failure.
|
|
158
|
+
#
|
|
159
|
+
# @return [void]
|
|
160
|
+
#
|
|
161
|
+
def release
|
|
162
|
+
@context.release_buffer(@buffer) if @buffer
|
|
163
|
+
@buffer = nil
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Builder for hash results (named captures).
|
|
168
|
+
#
|
|
169
|
+
# Constructs hashes directly without intermediate arrays.
|
|
170
|
+
#
|
|
171
|
+
class HashBuilder < ResultBuilder
|
|
172
|
+
# Initialize hash builder.
|
|
173
|
+
#
|
|
174
|
+
# @param context [Context] Parse context
|
|
175
|
+
#
|
|
176
|
+
def initialize(context)
|
|
177
|
+
super
|
|
178
|
+
@hash = {}
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Add key-value pair.
|
|
182
|
+
#
|
|
183
|
+
# @param key [Symbol] Hash key
|
|
184
|
+
# @param value [Object] Hash value
|
|
185
|
+
# @return [self]
|
|
186
|
+
#
|
|
187
|
+
def add_pair(key, value)
|
|
188
|
+
@hash[key] = value
|
|
189
|
+
self
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Build hash result.
|
|
193
|
+
#
|
|
194
|
+
# @return [Hash] Constructed hash
|
|
195
|
+
#
|
|
196
|
+
def build
|
|
197
|
+
@hash
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Release resources (hash cleanup).
|
|
201
|
+
#
|
|
202
|
+
# @return [void]
|
|
203
|
+
#
|
|
204
|
+
def release
|
|
205
|
+
@hash = nil
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
@@ -0,0 +1,261 @@
|
|
|
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(&block)
|
|
90
|
+
return enum_for(:each) unless block_given?
|
|
91
|
+
|
|
92
|
+
traverse(@tree, &block)
|
|
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
|
+
return enum_for(:depth_traverse, node, current_depth, target_depth) unless block_given?
|
|
210
|
+
|
|
211
|
+
# Check if we're at target depth
|
|
212
|
+
return [node].to_enum if current_depth == target_depth && yield(node)
|
|
213
|
+
|
|
214
|
+
# Recurse to children if not at target depth yet
|
|
215
|
+
results = []
|
|
216
|
+
if current_depth < target_depth
|
|
217
|
+
case node
|
|
218
|
+
when Array
|
|
219
|
+
node.each do |item|
|
|
220
|
+
depth_traverse(item, current_depth + 1, target_depth, &block).each do |result|
|
|
221
|
+
results << result
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
when Hash
|
|
225
|
+
node.each_value do |value|
|
|
226
|
+
depth_traverse(value, current_depth + 1, target_depth, &block).each do |result|
|
|
227
|
+
results << result
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
results.to_enum
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Find maximum depth of tree recursively.
|
|
237
|
+
#
|
|
238
|
+
# @param node [Object] Current node
|
|
239
|
+
# @param current_depth [Integer] Current depth
|
|
240
|
+
# @return [Integer] Maximum depth from this node
|
|
241
|
+
#
|
|
242
|
+
def find_max_depth(node, current_depth)
|
|
243
|
+
max = current_depth
|
|
244
|
+
|
|
245
|
+
case node
|
|
246
|
+
when Array
|
|
247
|
+
node.each do |item|
|
|
248
|
+
depth = find_max_depth(item, current_depth + 1)
|
|
249
|
+
max = depth if depth > max
|
|
250
|
+
end
|
|
251
|
+
when Hash
|
|
252
|
+
node.each_value do |value|
|
|
253
|
+
depth = find_max_depth(value, current_depth + 1)
|
|
254
|
+
max = depth if depth > max
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
max
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
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
|