parsanol 1.3.5-aarch64-linux → 1.3.7-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 +4 -4
- data/Rakefile +48 -48
- data/lib/parsanol/ast_visitor.rb +1 -1
- data/lib/parsanol/atoms/alternative.rb +3 -2
- data/lib/parsanol/atoms/base.rb +12 -6
- data/lib/parsanol/atoms/can_flatten.rb +8 -8
- data/lib/parsanol/atoms/context.rb +23 -16
- data/lib/parsanol/atoms/custom.rb +2 -2
- data/lib/parsanol/atoms/dynamic.rb +1 -1
- data/lib/parsanol/atoms/infix.rb +10 -5
- data/lib/parsanol/atoms/lookahead.rb +7 -4
- data/lib/parsanol/atoms/re.rb +1 -1
- data/lib/parsanol/atoms/repetition.rb +29 -11
- data/lib/parsanol/atoms/sequence.rb +3 -2
- data/lib/parsanol/atoms/str.rb +9 -3
- data/lib/parsanol/atoms.rb +20 -20
- data/lib/parsanol/builder_callbacks.rb +2 -2
- data/lib/parsanol/cause.rb +2 -2
- data/lib/parsanol/context.rb +2 -2
- data/lib/parsanol/error_reporter.rb +5 -5
- data/lib/parsanol/expression/treetop.rb +17 -17
- data/lib/parsanol/expression.rb +1 -1
- data/lib/parsanol/fast_mode.rb +50 -12
- data/lib/parsanol/first_set.rb +1 -1
- data/lib/parsanol/grammar_builder.rb +10 -8
- data/lib/parsanol/incremental_parser.rb +13 -8
- data/lib/parsanol/interval_tree.rb +12 -3
- data/lib/parsanol/lazy_result.rb +2 -2
- data/lib/parsanol/mermaid.rb +12 -9
- data/lib/parsanol/native/batch_decoder.rb +13 -9
- data/lib/parsanol/native/dynamic.rb +7 -6
- data/lib/parsanol/native/parser.rb +7 -5
- data/lib/parsanol/native/serializer.rb +42 -42
- data/lib/parsanol/native/transformer.rb +55 -28
- data/lib/parsanol/native/types.rb +3 -3
- data/lib/parsanol/native.rb +26 -20
- data/lib/parsanol/optimizer.rb +6 -6
- data/lib/parsanol/optimizers/choice_optimizer.rb +1 -1
- data/lib/parsanol/optimizers/cut_inserter.rb +5 -2
- data/lib/parsanol/optimizers/lookahead_optimizer.rb +9 -3
- data/lib/parsanol/optimizers/quantifier_optimizer.rb +5 -5
- data/lib/parsanol/optimizers/sequence_optimizer.rb +1 -1
- data/lib/parsanol/options/zero_copy.rb +1 -1
- data/lib/parsanol/options.rb +1 -1
- data/lib/parsanol/parallel.rb +4 -3
- data/lib/parsanol/parser.rb +18 -16
- data/lib/parsanol/parslet.rb +7 -7
- data/lib/parsanol/pattern/binding.rb +1 -1
- data/lib/parsanol/pattern.rb +4 -1
- data/lib/parsanol/pool.rb +3 -3
- data/lib/parsanol/pools/buffer_pool.rb +2 -2
- data/lib/parsanol/pools/position_pool.rb +2 -2
- data/lib/parsanol/position.rb +1 -1
- data/lib/parsanol/result_builder.rb +4 -4
- data/lib/parsanol/result_stream.rb +10 -5
- data/lib/parsanol/slice.rb +11 -8
- data/lib/parsanol/source.rb +14 -9
- data/lib/parsanol/source_location.rb +1 -1
- data/lib/parsanol/streaming_parser.rb +3 -3
- data/lib/parsanol/string_view.rb +4 -1
- data/lib/parsanol/transform.rb +2 -2
- data/lib/parsanol/version.rb +1 -1
- data/lib/parsanol/wasm_parser.rb +1 -1
- data/lib/parsanol.rb +37 -39
- data/parsanol.gemspec +30 -30
- metadata +1 -1
data/lib/parsanol/native.rb
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
3
|
+
require "json"
|
|
4
|
+
require "digest"
|
|
5
5
|
|
|
6
|
-
require
|
|
7
|
-
require
|
|
8
|
-
require
|
|
9
|
-
require
|
|
6
|
+
require "parsanol/native/types"
|
|
7
|
+
require "parsanol/native/parser"
|
|
8
|
+
require "parsanol/native/serializer"
|
|
9
|
+
require "parsanol/native/batch_decoder"
|
|
10
10
|
|
|
11
11
|
module Parsanol
|
|
12
12
|
module Native
|
|
13
|
-
VERSION =
|
|
13
|
+
VERSION = "0.1.0"
|
|
14
14
|
|
|
15
15
|
class << self
|
|
16
16
|
# Check if native extension is available
|
|
@@ -53,7 +53,8 @@ module Parsanol
|
|
|
53
53
|
# The batch format doesn't preserve :repetition/:sequence tags, so we use
|
|
54
54
|
# the direct FFI path. Apply the Ruby transformer to handle tags correctly.
|
|
55
55
|
raw_ast = _parse_raw(grammar_json, input)
|
|
56
|
-
BatchDecoder.decode_and_flatten(raw_ast, input, Parsanol::Slice,
|
|
56
|
+
BatchDecoder.decode_and_flatten(raw_ast, input, Parsanol::Slice,
|
|
57
|
+
grammar_atom)
|
|
57
58
|
end
|
|
58
59
|
|
|
59
60
|
# Parse and return RAW AST without transformation.
|
|
@@ -76,11 +77,11 @@ module Parsanol
|
|
|
76
77
|
raise LoadError, "Native parser not available" unless available?
|
|
77
78
|
|
|
78
79
|
# Handle both grammar atoms and pre-serialized JSON strings
|
|
79
|
-
if grammar.is_a?(String)
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
80
|
+
grammar_json = if grammar.is_a?(String)
|
|
81
|
+
grammar
|
|
82
|
+
else
|
|
83
|
+
Parser.serialize_grammar(grammar)
|
|
84
|
+
end
|
|
84
85
|
|
|
85
86
|
# Use batch_raw format for raw AST (no transformation)
|
|
86
87
|
slice_class = Parsanol::Slice
|
|
@@ -112,11 +113,6 @@ module Parsanol
|
|
|
112
113
|
BatchDecoder.decode(batch_data, input, slice_class)
|
|
113
114
|
end
|
|
114
115
|
|
|
115
|
-
# Get the Slice class
|
|
116
|
-
private def get_slice_class
|
|
117
|
-
Parsanol::Slice
|
|
118
|
-
end
|
|
119
|
-
|
|
120
116
|
# Serialize a Ruby grammar to JSON (cached).
|
|
121
117
|
#
|
|
122
118
|
# @param root_atom [Parsanol::Atoms::Base] Root atom of the grammar
|
|
@@ -139,6 +135,7 @@ module Parsanol
|
|
|
139
135
|
# @return [nil]
|
|
140
136
|
def clear_grammar_cache
|
|
141
137
|
raise LoadError, "Native parser not available" unless available?
|
|
138
|
+
|
|
142
139
|
_clear_grammar_cache
|
|
143
140
|
end
|
|
144
141
|
|
|
@@ -147,6 +144,7 @@ module Parsanol
|
|
|
147
144
|
# @return [Integer] Number of cached grammars
|
|
148
145
|
def grammar_cache_size
|
|
149
146
|
raise LoadError, "Native parser not available" unless available?
|
|
147
|
+
|
|
150
148
|
_grammar_cache_size
|
|
151
149
|
end
|
|
152
150
|
|
|
@@ -155,6 +153,7 @@ module Parsanol
|
|
|
155
153
|
# @return [Integer] Maximum cache capacity
|
|
156
154
|
def grammar_cache_capacity
|
|
157
155
|
raise LoadError, "Native parser not available" unless available?
|
|
156
|
+
|
|
158
157
|
_grammar_cache_capacity
|
|
159
158
|
end
|
|
160
159
|
|
|
@@ -167,17 +166,24 @@ module Parsanol
|
|
|
167
166
|
end
|
|
168
167
|
stats
|
|
169
168
|
end
|
|
169
|
+
|
|
170
|
+
private
|
|
171
|
+
|
|
172
|
+
# Get the Slice class
|
|
173
|
+
def get_slice_class
|
|
174
|
+
Parsanol::Slice
|
|
175
|
+
end
|
|
170
176
|
end
|
|
171
177
|
end
|
|
172
178
|
end
|
|
173
179
|
|
|
174
180
|
# Attempt to load native extension
|
|
175
181
|
begin
|
|
176
|
-
ruby_version = RUBY_VERSION.split(
|
|
182
|
+
ruby_version = RUBY_VERSION.split(".").take(2).join(".")
|
|
177
183
|
require "parsanol/#{ruby_version}/parsanol_native"
|
|
178
184
|
rescue LoadError
|
|
179
185
|
begin
|
|
180
|
-
require
|
|
186
|
+
require "parsanol/parsanol_native"
|
|
181
187
|
rescue LoadError
|
|
182
188
|
# Native extension not built yet
|
|
183
189
|
end
|
data/lib/parsanol/optimizer.rb
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative
|
|
4
|
-
require_relative
|
|
5
|
-
require_relative
|
|
6
|
-
require_relative
|
|
7
|
-
require_relative
|
|
8
|
-
require_relative
|
|
3
|
+
require_relative "ast_visitor"
|
|
4
|
+
require_relative "optimizers/quantifier_optimizer"
|
|
5
|
+
require_relative "optimizers/sequence_optimizer"
|
|
6
|
+
require_relative "optimizers/choice_optimizer"
|
|
7
|
+
require_relative "optimizers/lookahead_optimizer"
|
|
8
|
+
require_relative "optimizers/cut_inserter"
|
|
9
9
|
|
|
10
10
|
# Grammar-level optimizations for Parslet parsers
|
|
11
11
|
# These optimizations transform the parser AST to reduce runtime overhead
|
|
@@ -84,7 +84,7 @@ module Parsanol
|
|
|
84
84
|
Parsanol::Atoms::Repetition.new(
|
|
85
85
|
optimized_parslet,
|
|
86
86
|
rep.min,
|
|
87
|
-
rep.max
|
|
87
|
+
rep.max,
|
|
88
88
|
)
|
|
89
89
|
end
|
|
90
90
|
|
|
@@ -101,7 +101,10 @@ module Parsanol
|
|
|
101
101
|
# For sequences, find the longest safe prefix
|
|
102
102
|
if parslet.is_a?(Parsanol::Atoms::Sequence)
|
|
103
103
|
prefix_parslets = find_deterministic_prefix(parslet)
|
|
104
|
-
|
|
104
|
+
if prefix_parslets && !prefix_parslets.empty?
|
|
105
|
+
return build_cut_sequence(parslet,
|
|
106
|
+
prefix_parslets)
|
|
107
|
+
end
|
|
105
108
|
end
|
|
106
109
|
|
|
107
110
|
# For other atoms, cut the whole thing if safe
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative
|
|
3
|
+
require_relative "../ast_visitor"
|
|
4
4
|
|
|
5
5
|
module Parsanol
|
|
6
6
|
module Optimizers
|
|
@@ -26,13 +26,19 @@ module Parsanol
|
|
|
26
26
|
inner_positive = inner.positive
|
|
27
27
|
|
|
28
28
|
# !(!x) => &x (double negation)
|
|
29
|
-
|
|
29
|
+
if !outer_positive && !inner_positive
|
|
30
|
+
return Parsanol::Atoms::Lookahead.new(inner.bound_parslet,
|
|
31
|
+
true)
|
|
32
|
+
end
|
|
30
33
|
|
|
31
34
|
# &(&x) => &x (idempotent)
|
|
32
35
|
return inner if outer_positive && inner_positive
|
|
33
36
|
|
|
34
37
|
# !(&x) => !x (negative of positive)
|
|
35
|
-
|
|
38
|
+
if !outer_positive && inner_positive
|
|
39
|
+
return Parsanol::Atoms::Lookahead.new(inner.bound_parslet,
|
|
40
|
+
false)
|
|
41
|
+
end
|
|
36
42
|
|
|
37
43
|
# &(!x) => !x (positive of negative)
|
|
38
44
|
return inner if outer_positive && !inner_positive
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative
|
|
3
|
+
require_relative "../ast_visitor"
|
|
4
4
|
|
|
5
5
|
module Parsanol
|
|
6
6
|
module Optimizers
|
|
@@ -26,19 +26,19 @@ module Parsanol
|
|
|
26
26
|
if inner.is_a?(Parsanol::Atoms::Repetition)
|
|
27
27
|
# repeat(0,1).repeat(0,1) => repeat(0,1) (idempotent)
|
|
28
28
|
if parslet.min.zero? && parslet.max == 1 &&
|
|
29
|
-
|
|
29
|
+
inner.min.zero? && inner.max == 1
|
|
30
30
|
return inner
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
# repeat(n,n).repeat(m,m) => repeat(n*m,n*m) for exact counts
|
|
34
34
|
if parslet.min == parslet.max && inner.min == inner.max &&
|
|
35
|
-
|
|
35
|
+
parslet.max && inner.max
|
|
36
36
|
new_count = parslet.min * inner.min
|
|
37
37
|
return Parsanol::Atoms::Repetition.new(
|
|
38
38
|
inner.parslet,
|
|
39
39
|
new_count,
|
|
40
40
|
new_count,
|
|
41
|
-
parslet.result_tag
|
|
41
|
+
parslet.result_tag,
|
|
42
42
|
)
|
|
43
43
|
end
|
|
44
44
|
end
|
|
@@ -51,7 +51,7 @@ module Parsanol
|
|
|
51
51
|
inner,
|
|
52
52
|
parslet.min,
|
|
53
53
|
parslet.max,
|
|
54
|
-
parslet.result_tag
|
|
54
|
+
parslet.result_tag,
|
|
55
55
|
)
|
|
56
56
|
end
|
|
57
57
|
end
|
|
@@ -119,7 +119,7 @@ module Parsanol
|
|
|
119
119
|
# @param type_map [Hash] Override type mapping for this parse
|
|
120
120
|
# @return [Object] Direct Ruby object
|
|
121
121
|
def parse_with_types(input, type_map)
|
|
122
|
-
raise LoadError,
|
|
122
|
+
raise LoadError, "ZeroCopy mode requires native extension." unless Parsanol::Native.available?
|
|
123
123
|
|
|
124
124
|
grammar_json = Parsanol::Native.serialize_grammar(root)
|
|
125
125
|
Parsanol::Native.parse_to_objects(grammar_json, input, type_map)
|
data/lib/parsanol/options.rb
CHANGED
data/lib/parsanol/parallel.rb
CHANGED
|
@@ -86,7 +86,7 @@ module Parsanol
|
|
|
86
86
|
Parsanol::Native.parse_batch_parallel(
|
|
87
87
|
grammar_json,
|
|
88
88
|
inputs,
|
|
89
|
-
num_threads: config.num_threads
|
|
89
|
+
num_threads: config.num_threads,
|
|
90
90
|
)
|
|
91
91
|
end
|
|
92
92
|
|
|
@@ -98,7 +98,8 @@ module Parsanol
|
|
|
98
98
|
# @param config [Config] Parallel configuration (optional)
|
|
99
99
|
# @return [Array<Object>] Array of transformed results
|
|
100
100
|
#
|
|
101
|
-
def parse_batch_with_transform(grammar_json, inputs, transform,
|
|
101
|
+
def parse_batch_with_transform(grammar_json, inputs, transform,
|
|
102
|
+
config: Config.new)
|
|
102
103
|
results = parse_batch(grammar_json, inputs, config: config)
|
|
103
104
|
results.map { |result| transform.apply(result) }
|
|
104
105
|
end
|
|
@@ -107,7 +108,7 @@ module Parsanol
|
|
|
107
108
|
#
|
|
108
109
|
# @return [Integer] Number of available cores
|
|
109
110
|
def available_cores
|
|
110
|
-
require
|
|
111
|
+
require "etc"
|
|
111
112
|
Etc.nprocessors
|
|
112
113
|
rescue StandardError
|
|
113
114
|
1
|
data/lib/parsanol/parser.rb
CHANGED
|
@@ -110,13 +110,23 @@ module Parsanol
|
|
|
110
110
|
#
|
|
111
111
|
def parse(input, mode_or_opts = {}, **kwargs)
|
|
112
112
|
if mode_or_opts.is_a?(Hash) && !kwargs.key?(:mode)
|
|
113
|
-
# Legacy API: parse(input, options={})
|
|
113
|
+
# Legacy API: parse(input, options={})
|
|
114
114
|
merged = mode_or_opts.merge(kwargs)
|
|
115
115
|
super(input, merged)
|
|
116
116
|
else
|
|
117
117
|
# New API: parse(input, mode:, **options)
|
|
118
118
|
mode = kwargs.delete(:mode) || :ruby
|
|
119
|
-
|
|
119
|
+
case mode
|
|
120
|
+
when :ruby
|
|
121
|
+
super(input, kwargs)
|
|
122
|
+
when :native
|
|
123
|
+
parse_native(input, kwargs)
|
|
124
|
+
when :json
|
|
125
|
+
parse_json(input, kwargs)
|
|
126
|
+
else
|
|
127
|
+
raise ArgumentError,
|
|
128
|
+
"Unknown mode: #{mode}. Valid modes: :ruby, :native, :json"
|
|
129
|
+
end
|
|
120
130
|
end
|
|
121
131
|
end
|
|
122
132
|
|
|
@@ -175,26 +185,19 @@ module Parsanol
|
|
|
175
185
|
def dispatch_parse(mode, input, opts)
|
|
176
186
|
case mode
|
|
177
187
|
when :ruby
|
|
178
|
-
|
|
188
|
+
# Call base class parse directly (send needed since parse is defined in parent)
|
|
189
|
+
Parsanol::Atoms::Base.instance_method(:parse).bind_call(self, input,
|
|
190
|
+
opts)
|
|
179
191
|
when :native
|
|
180
192
|
parse_native(input, opts)
|
|
181
193
|
when :json
|
|
182
194
|
parse_json(input, opts)
|
|
183
195
|
else
|
|
184
|
-
raise ArgumentError,
|
|
196
|
+
raise ArgumentError,
|
|
197
|
+
"Unknown mode: #{mode}. Valid modes: :ruby, :native, :json"
|
|
185
198
|
end
|
|
186
199
|
end
|
|
187
200
|
|
|
188
|
-
# Pure Ruby parsing (delegates to Base implementation).
|
|
189
|
-
#
|
|
190
|
-
# @param input [String] input to parse
|
|
191
|
-
# @param opts [Hash] parsing options
|
|
192
|
-
# @return [Object] parse result
|
|
193
|
-
#
|
|
194
|
-
def parse_ruby(input, opts)
|
|
195
|
-
super
|
|
196
|
-
end
|
|
197
|
-
|
|
198
201
|
# Native extension parsing with Ruby fallback.
|
|
199
202
|
# Returns results with Slice objects containing position info.
|
|
200
203
|
#
|
|
@@ -204,10 +207,9 @@ module Parsanol
|
|
|
204
207
|
#
|
|
205
208
|
def parse_native(input, opts)
|
|
206
209
|
if Parsanol::Native.available?
|
|
207
|
-
# Parse with lazy line/column support
|
|
208
210
|
Parsanol::Native.parse(root, input)
|
|
209
211
|
else
|
|
210
|
-
|
|
212
|
+
super
|
|
211
213
|
end
|
|
212
214
|
end
|
|
213
215
|
|
data/lib/parsanol/parslet.rb
CHANGED
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
# class MyParser < Parsanol::Parslet::Parser
|
|
36
36
|
# include Parsanol::Parslet
|
|
37
37
|
|
|
38
|
-
require
|
|
38
|
+
require "parsanol"
|
|
39
39
|
|
|
40
40
|
module Parsanol
|
|
41
41
|
module Parslet
|
|
@@ -90,16 +90,16 @@ module Parsanol
|
|
|
90
90
|
Parsanol.any
|
|
91
91
|
end
|
|
92
92
|
|
|
93
|
-
def scope(&
|
|
94
|
-
Parsanol.scope(&
|
|
93
|
+
def scope(&)
|
|
94
|
+
Parsanol.scope(&)
|
|
95
95
|
end
|
|
96
96
|
|
|
97
|
-
def dynamic(&
|
|
98
|
-
Parsanol.dynamic(&
|
|
97
|
+
def dynamic(&)
|
|
98
|
+
Parsanol.dynamic(&)
|
|
99
99
|
end
|
|
100
100
|
|
|
101
|
-
def infix_expression(element, *operations, &
|
|
102
|
-
Parsanol.infix_expression(element, *operations, &
|
|
101
|
+
def infix_expression(element, *operations, &)
|
|
102
|
+
Parsanol.infix_expression(element, *operations, &)
|
|
103
103
|
end
|
|
104
104
|
|
|
105
105
|
# Parses a treetop-style expression string and returns the corresponding atom.
|
data/lib/parsanol/pattern.rb
CHANGED
|
@@ -51,7 +51,10 @@ module Parsanol
|
|
|
51
51
|
#
|
|
52
52
|
def match(subtree, bindings = nil)
|
|
53
53
|
current_bindings = bindings ? bindings.dup : {}
|
|
54
|
-
check_match(subtree, @pattern_def,
|
|
54
|
+
if check_match(subtree, @pattern_def,
|
|
55
|
+
current_bindings)
|
|
56
|
+
current_bindings
|
|
57
|
+
end
|
|
55
58
|
end
|
|
56
59
|
|
|
57
60
|
private
|
data/lib/parsanol/pool.rb
CHANGED
|
@@ -67,7 +67,7 @@ module Parsanol
|
|
|
67
67
|
created: 0,
|
|
68
68
|
reused: 0,
|
|
69
69
|
released: 0,
|
|
70
|
-
discarded: 0
|
|
70
|
+
discarded: 0,
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
# Pre-allocate objects for efficiency if requested
|
|
@@ -154,7 +154,7 @@ module Parsanol
|
|
|
154
154
|
reused: @stats[:reused],
|
|
155
155
|
released: @stats[:released],
|
|
156
156
|
discarded: @stats[:discarded],
|
|
157
|
-
utilization: utilization.round(2)
|
|
157
|
+
utilization: utilization.round(2),
|
|
158
158
|
}
|
|
159
159
|
end
|
|
160
160
|
|
|
@@ -174,7 +174,7 @@ module Parsanol
|
|
|
174
174
|
created: 0,
|
|
175
175
|
reused: 0,
|
|
176
176
|
released: 0,
|
|
177
|
-
discarded: 0
|
|
177
|
+
discarded: 0,
|
|
178
178
|
}
|
|
179
179
|
end
|
|
180
180
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative
|
|
3
|
+
require_relative "../buffer"
|
|
4
4
|
|
|
5
5
|
module Parsanol
|
|
6
6
|
module Pools
|
|
@@ -124,7 +124,7 @@ module Parsanol
|
|
|
124
124
|
reused: stats[:reused],
|
|
125
125
|
released: stats[:released],
|
|
126
126
|
discarded: stats[:discarded],
|
|
127
|
-
utilization: utilization.round(2)
|
|
127
|
+
utilization: utilization.round(2),
|
|
128
128
|
}
|
|
129
129
|
end
|
|
130
130
|
result
|
|
@@ -48,7 +48,7 @@ module Parsanol
|
|
|
48
48
|
if @available.empty?
|
|
49
49
|
@stats[:created] += 1
|
|
50
50
|
# Create Position with default values since it requires arguments
|
|
51
|
-
Parsanol::Position.new(
|
|
51
|
+
Parsanol::Position.new("", 0, 0)
|
|
52
52
|
else
|
|
53
53
|
@stats[:reused] += 1
|
|
54
54
|
@available.pop
|
|
@@ -81,7 +81,7 @@ module Parsanol
|
|
|
81
81
|
end
|
|
82
82
|
|
|
83
83
|
# Reset position state with default values before returning to pool
|
|
84
|
-
pos.reset!(
|
|
84
|
+
pos.reset!("", 0, 0)
|
|
85
85
|
|
|
86
86
|
@stats[:released] += 1
|
|
87
87
|
@available.push(pos)
|
data/lib/parsanol/position.rb
CHANGED
|
@@ -45,7 +45,7 @@ module Parsanol
|
|
|
45
45
|
|
|
46
46
|
def calculate_charpos
|
|
47
47
|
# Calculate it based on platform
|
|
48
|
-
if defined?(RUBY_ENGINE) && RUBY_ENGINE ==
|
|
48
|
+
if defined?(RUBY_ENGINE) && RUBY_ENGINE == "opal"
|
|
49
49
|
# In Opal, convert byte position to character position.
|
|
50
50
|
# We need to calculate how many characters occupy the first @bytepos bytes.
|
|
51
51
|
`
|
|
@@ -27,14 +27,14 @@ module Parsanol
|
|
|
27
27
|
# @param options [Hash] Builder options
|
|
28
28
|
# @return [ResultBuilder] Appropriate builder instance
|
|
29
29
|
#
|
|
30
|
-
def self.for(type, context, **
|
|
30
|
+
def self.for(type, context, **)
|
|
31
31
|
case type
|
|
32
32
|
when :repetition
|
|
33
|
-
RepetitionBuilder.new(context, **
|
|
33
|
+
RepetitionBuilder.new(context, **)
|
|
34
34
|
when :sequence
|
|
35
|
-
SequenceBuilder.new(context, **
|
|
35
|
+
SequenceBuilder.new(context, **)
|
|
36
36
|
when :hash
|
|
37
|
-
HashBuilder.new(context, **
|
|
37
|
+
HashBuilder.new(context, **)
|
|
38
38
|
else
|
|
39
39
|
raise ArgumentError, "Unknown builder type: #{type}"
|
|
40
40
|
end
|
|
@@ -86,10 +86,10 @@ module Parsanol
|
|
|
86
86
|
# enum = stream.each # Returns Enumerator
|
|
87
87
|
# enum.next # Get next node
|
|
88
88
|
#
|
|
89
|
-
def each(&
|
|
89
|
+
def each(&)
|
|
90
90
|
return enum_for(:each) unless block_given?
|
|
91
91
|
|
|
92
|
-
traverse(@tree, &
|
|
92
|
+
traverse(@tree, &)
|
|
93
93
|
self
|
|
94
94
|
end
|
|
95
95
|
|
|
@@ -206,7 +206,10 @@ module Parsanol
|
|
|
206
206
|
# @return [Enumerator]
|
|
207
207
|
#
|
|
208
208
|
def depth_traverse(node, current_depth, target_depth, &block)
|
|
209
|
-
|
|
209
|
+
unless block
|
|
210
|
+
return enum_for(:depth_traverse, node, current_depth,
|
|
211
|
+
target_depth)
|
|
212
|
+
end
|
|
210
213
|
|
|
211
214
|
# Check if we're at target depth
|
|
212
215
|
return [node].to_enum if current_depth == target_depth && yield(node)
|
|
@@ -217,13 +220,15 @@ module Parsanol
|
|
|
217
220
|
case node
|
|
218
221
|
when Array
|
|
219
222
|
node.each do |item|
|
|
220
|
-
depth_traverse(item, current_depth + 1, target_depth,
|
|
223
|
+
depth_traverse(item, current_depth + 1, target_depth,
|
|
224
|
+
&block).each do |result|
|
|
221
225
|
results << result
|
|
222
226
|
end
|
|
223
227
|
end
|
|
224
228
|
when Hash
|
|
225
229
|
node.each_value do |value|
|
|
226
|
-
depth_traverse(value, current_depth + 1, target_depth,
|
|
230
|
+
depth_traverse(value, current_depth + 1, target_depth,
|
|
231
|
+
&block).each do |result|
|
|
227
232
|
results << result
|
|
228
233
|
end
|
|
229
234
|
end
|
data/lib/parsanol/slice.rb
CHANGED
|
@@ -12,14 +12,14 @@ module Parsanol
|
|
|
12
12
|
|
|
13
13
|
attr_reader :content, :input
|
|
14
14
|
|
|
15
|
-
def initialize(byte_offset = 0, string_content =
|
|
15
|
+
def initialize(byte_offset = 0, string_content = "", input = nil)
|
|
16
16
|
@byte_position = byte_offset
|
|
17
17
|
@content = string_content
|
|
18
18
|
@input = input
|
|
19
19
|
@line_and_column = nil
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
def reset!(new_offset = 0, new_content =
|
|
22
|
+
def reset!(new_offset = 0, new_content = "", new_input = nil)
|
|
23
23
|
@byte_position = new_offset
|
|
24
24
|
@content = new_content
|
|
25
25
|
@input = new_input
|
|
@@ -44,6 +44,7 @@ module Parsanol
|
|
|
44
44
|
def ==(other)
|
|
45
45
|
return content == other if other.is_a?(String)
|
|
46
46
|
return content == other.content if other.is_a?(Parsanol::Slice)
|
|
47
|
+
|
|
47
48
|
content == other
|
|
48
49
|
end
|
|
49
50
|
|
|
@@ -72,7 +73,8 @@ module Parsanol
|
|
|
72
73
|
|
|
73
74
|
# Lazy line/column — computed once and cached.
|
|
74
75
|
def line_and_column
|
|
75
|
-
raise ArgumentError,
|
|
76
|
+
raise ArgumentError, "Line/column requires input" unless @input
|
|
77
|
+
|
|
76
78
|
@line_and_column ||= compute_line_and_column
|
|
77
79
|
end
|
|
78
80
|
|
|
@@ -109,11 +111,11 @@ module Parsanol
|
|
|
109
111
|
end
|
|
110
112
|
|
|
111
113
|
def as_json(_options = {})
|
|
112
|
-
result = {
|
|
114
|
+
result = { "value" => content, "offset" => offset, "length" => length }
|
|
113
115
|
if @input
|
|
114
116
|
line, column = line_and_column
|
|
115
|
-
result[
|
|
116
|
-
result[
|
|
117
|
+
result["line"] = line
|
|
118
|
+
result["column"] = column
|
|
117
119
|
end
|
|
118
120
|
result
|
|
119
121
|
end
|
|
@@ -123,7 +125,8 @@ module Parsanol
|
|
|
123
125
|
line, column = line_and_column
|
|
124
126
|
end_line, end_column = line_and_column_at(offset + length)
|
|
125
127
|
start_pos = SourcePosition.new(offset: offset, line: line, column: column)
|
|
126
|
-
end_pos = SourcePosition.new(offset: offset + length, line: end_line,
|
|
128
|
+
end_pos = SourcePosition.new(offset: offset + length, line: end_line,
|
|
129
|
+
column: end_column)
|
|
127
130
|
SourceSpan.new(start_pos: start_pos, end_pos: end_pos)
|
|
128
131
|
end
|
|
129
132
|
|
|
@@ -142,7 +145,7 @@ module Parsanol
|
|
|
142
145
|
@input.line_and_column(pos)
|
|
143
146
|
else
|
|
144
147
|
# String input
|
|
145
|
-
prefix = @input.byteslice(0, pos) ||
|
|
148
|
+
prefix = @input.byteslice(0, pos) || ""
|
|
146
149
|
line = 1 + prefix.count("\n")
|
|
147
150
|
last_nl = prefix.rindex("\n")
|
|
148
151
|
column = last_nl ? pos - last_nl : pos + 1
|
data/lib/parsanol/source.rb
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
3
|
+
require "stringio"
|
|
4
|
+
require "strscan"
|
|
5
5
|
|
|
6
|
-
require
|
|
7
|
-
require
|
|
8
|
-
require
|
|
9
|
-
require
|
|
6
|
+
require "parsanol/position"
|
|
7
|
+
require "parsanol/source/line_cache"
|
|
8
|
+
require "parsanol/pools/slice_pool"
|
|
9
|
+
require "parsanol/pools/position_pool"
|
|
10
10
|
|
|
11
11
|
module Parsanol
|
|
12
12
|
# Encapsulates input source for parsing operations. Provides position tracking,
|
|
@@ -33,14 +33,19 @@ module Parsanol
|
|
|
33
33
|
# @raise [ArgumentError] if input doesn't respond to to_str
|
|
34
34
|
#
|
|
35
35
|
def initialize(input)
|
|
36
|
-
|
|
36
|
+
unless input.respond_to?(:to_str)
|
|
37
|
+
raise ArgumentError,
|
|
38
|
+
"Source requires a string-like object (responds to to_str)"
|
|
39
|
+
end
|
|
37
40
|
|
|
38
41
|
# Core scanner for input traversal
|
|
39
42
|
@scanner = StringScanner.new(input)
|
|
40
43
|
@raw_string = input.to_str
|
|
41
44
|
|
|
42
45
|
# Regex cache: maps count n to /(.|$){n}/m pattern
|
|
43
|
-
@regex_cache = Hash.new
|
|
46
|
+
@regex_cache = Hash.new do |h, count|
|
|
47
|
+
h[count] = Regexp.new("(.|$){#{count}}", Regexp::MULTILINE)
|
|
48
|
+
end
|
|
44
49
|
|
|
45
50
|
# Line ending cache for position-to-line/column mapping
|
|
46
51
|
@line_data = LineCache.new
|
|
@@ -173,7 +178,7 @@ module Parsanol
|
|
|
173
178
|
@position_pool.acquire_with(
|
|
174
179
|
string: @raw_string,
|
|
175
180
|
bytepos: effective,
|
|
176
|
-
charpos: char_pos
|
|
181
|
+
charpos: char_pos,
|
|
177
182
|
)
|
|
178
183
|
end
|
|
179
184
|
end
|