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,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Line position caching for efficient line/column lookups.
|
|
4
|
+
# Stores line ending positions to enable O(log n) line number queries.
|
|
5
|
+
#
|
|
6
|
+
# Inspired by Parslet (MIT License).
|
|
7
|
+
|
|
8
|
+
module Parsanol
|
|
9
|
+
class Source
|
|
10
|
+
# Caches line ending positions for quick line/column resolution.
|
|
11
|
+
# Uses binary search for efficient position lookup.
|
|
12
|
+
class LineCache
|
|
13
|
+
def initialize
|
|
14
|
+
# Array of byte offsets where each line ends
|
|
15
|
+
@breaks = []
|
|
16
|
+
@breaks.extend(IntervalLookup)
|
|
17
|
+
@max_scanned = nil
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Converts a byte offset to [line_number, column_number].
|
|
21
|
+
# Line and column numbers are 1-indexed.
|
|
22
|
+
#
|
|
23
|
+
# @param position [Integer, #bytepos] the byte offset to convert
|
|
24
|
+
# @return [Array<Integer, Integer>] [line, column] tuple
|
|
25
|
+
def line_and_column(position)
|
|
26
|
+
position = position.bytepos if position.respond_to?(:bytepos)
|
|
27
|
+
|
|
28
|
+
line_idx = @breaks.lower_bound_index(position)
|
|
29
|
+
|
|
30
|
+
if line_idx
|
|
31
|
+
# Found a line ending after this position
|
|
32
|
+
line_start = line_idx.positive? ? @breaks[line_idx - 1] : 0
|
|
33
|
+
[line_idx + 1, position - line_start + 1]
|
|
34
|
+
else
|
|
35
|
+
# Position is beyond all known line endings
|
|
36
|
+
line_start = @breaks.last || 0
|
|
37
|
+
[@breaks.size + 1, position - line_start + 1]
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Scans a string buffer for line endings and caches their positions.
|
|
42
|
+
# Avoids re-scanning already processed regions.
|
|
43
|
+
#
|
|
44
|
+
# @param start_offset [Integer] the byte offset where buffer starts
|
|
45
|
+
# @param buffer [String] the string content to scan
|
|
46
|
+
def scan_for_line_endings(start_offset, buffer)
|
|
47
|
+
return unless buffer
|
|
48
|
+
|
|
49
|
+
scanner = StringScanner.new(buffer)
|
|
50
|
+
return unless scanner.exist?(/\n/)
|
|
51
|
+
|
|
52
|
+
# Skip already-scanned content
|
|
53
|
+
scanner.pos = @max_scanned - start_offset if @max_scanned && start_offset < @max_scanned
|
|
54
|
+
|
|
55
|
+
# Record all newline positions
|
|
56
|
+
while scanner.skip_until(/\n/)
|
|
57
|
+
@max_scanned = start_offset + scanner.pos
|
|
58
|
+
@breaks << @max_scanned
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Mixin providing binary search for interval containment queries.
|
|
64
|
+
# Treats array values as interval endpoints where each interval [n-1, n]
|
|
65
|
+
# is represented by value at index n.
|
|
66
|
+
#
|
|
67
|
+
# @example
|
|
68
|
+
# [10, 20, 30] represents intervals [0,10], (10,20], (20,30]
|
|
69
|
+
module IntervalLookup
|
|
70
|
+
# Calculates midpoint index for binary search.
|
|
71
|
+
# Uses floor to ensure integer result.
|
|
72
|
+
def midpoint_index(lo, hi)
|
|
73
|
+
lo + ((hi - lo) / 2).floor
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Finds the index of the first value greater than the bound.
|
|
77
|
+
# Returns nil if no such value exists.
|
|
78
|
+
#
|
|
79
|
+
# @param bound [Numeric] the value to search against
|
|
80
|
+
# @return [Integer, nil] index of first value > bound, or nil
|
|
81
|
+
def lower_bound_index(bound)
|
|
82
|
+
return nil if empty?
|
|
83
|
+
return nil unless last > bound
|
|
84
|
+
|
|
85
|
+
lo = 0
|
|
86
|
+
hi = size - 1
|
|
87
|
+
|
|
88
|
+
loop do
|
|
89
|
+
mid = midpoint_index(lo, hi)
|
|
90
|
+
|
|
91
|
+
if self[mid] > bound
|
|
92
|
+
hi = mid
|
|
93
|
+
else
|
|
94
|
+
lo = mid + 1
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
return hi if hi <= lo
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "stringio"
|
|
4
|
+
require "strscan"
|
|
5
|
+
|
|
6
|
+
require "parsanol/position"
|
|
7
|
+
require "parsanol/source/line_cache"
|
|
8
|
+
require "parsanol/pools/slice_pool"
|
|
9
|
+
require "parsanol/pools/position_pool"
|
|
10
|
+
|
|
11
|
+
module Parsanol
|
|
12
|
+
# Encapsulates input source for parsing operations. Provides position tracking,
|
|
13
|
+
# character consumption, line/column calculation, and object pooling for
|
|
14
|
+
# memory efficiency.
|
|
15
|
+
#
|
|
16
|
+
# @example Creating a source
|
|
17
|
+
# src = Parsanol::Source.new("input string")
|
|
18
|
+
# src.matches?(/a/) # => true if 'a' is at current position
|
|
19
|
+
# src.consume(1) # => Slice containing one character
|
|
20
|
+
#
|
|
21
|
+
# Inspired by source/position tracking patterns in parser implementations.
|
|
22
|
+
#
|
|
23
|
+
class Source
|
|
24
|
+
# @return [Parsanol::Pools::SlicePool] pool for Slice objects
|
|
25
|
+
attr_reader :slice_pool
|
|
26
|
+
|
|
27
|
+
# @return [Parsanol::Pools::PositionPool] pool for Position objects
|
|
28
|
+
attr_reader :position_pool
|
|
29
|
+
|
|
30
|
+
# Creates a new source wrapper around a string.
|
|
31
|
+
#
|
|
32
|
+
# @param input [#to_str] string-like object to parse
|
|
33
|
+
# @raise [ArgumentError] if input doesn't respond to to_str
|
|
34
|
+
#
|
|
35
|
+
def initialize(input)
|
|
36
|
+
unless input.respond_to?(:to_str)
|
|
37
|
+
raise ArgumentError,
|
|
38
|
+
"Source requires a string-like object (responds to to_str)"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Core scanner for input traversal
|
|
42
|
+
@scanner = StringScanner.new(input)
|
|
43
|
+
@raw_string = input.to_str
|
|
44
|
+
|
|
45
|
+
# Regex cache: maps count n to /(.|$){n}/m pattern
|
|
46
|
+
@regex_cache = Hash.new do |h, count|
|
|
47
|
+
h[count] = Regexp.new("(.|$){#{count}}", Regexp::MULTILINE)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Line ending cache for position-to-line/column mapping
|
|
51
|
+
@line_data = LineCache.new
|
|
52
|
+
@line_data.scan_for_line_endings(0, input)
|
|
53
|
+
|
|
54
|
+
# Object pools for memory efficiency
|
|
55
|
+
# SlicePool: reduces Slice allocations during matching
|
|
56
|
+
@slice_pool = Parsanol::Pools::SlicePool.new(size: 5000, preallocate: false)
|
|
57
|
+
|
|
58
|
+
# PositionPool: reduces Position allocations for error reporting
|
|
59
|
+
@position_pool = Parsanol::Pools::PositionPool.new(size: 1000)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Checks if a pattern matches at the current input position without consuming.
|
|
63
|
+
#
|
|
64
|
+
# @param pattern [Regexp] pattern to test
|
|
65
|
+
# @return [Boolean] true if pattern matches at current position
|
|
66
|
+
#
|
|
67
|
+
def matches?(pattern)
|
|
68
|
+
@scanner.match?(pattern)
|
|
69
|
+
end
|
|
70
|
+
alias match matches?
|
|
71
|
+
|
|
72
|
+
# Consumes n characters from input and returns them as a pooled Slice.
|
|
73
|
+
#
|
|
74
|
+
# @param count [Integer] number of characters to consume
|
|
75
|
+
# @return [Parsanol::Slice] slice containing consumed characters
|
|
76
|
+
#
|
|
77
|
+
def consume(count)
|
|
78
|
+
current_pos = @scanner.pos
|
|
79
|
+
content = @scanner.scan(@regex_cache[count])
|
|
80
|
+
@slice_pool.acquire_with(current_pos, content, @line_data)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Creates a pooled slice at a specific position.
|
|
84
|
+
# Preferred method for atoms to construct slices.
|
|
85
|
+
#
|
|
86
|
+
# @param offset [Integer] byte position in source
|
|
87
|
+
# @param content [String] slice content
|
|
88
|
+
# @return [Parsanol::Slice] pooled slice instance
|
|
89
|
+
#
|
|
90
|
+
def slice(offset, content)
|
|
91
|
+
@slice_pool.acquire_with(offset, content, @line_data)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Returns a slice to the pool for reuse.
|
|
95
|
+
#
|
|
96
|
+
# @param sl [Parsanol::Slice] slice to release
|
|
97
|
+
#
|
|
98
|
+
def release_slice(sl)
|
|
99
|
+
@slice_pool.release(sl)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Returns count of remaining characters in input.
|
|
103
|
+
#
|
|
104
|
+
# @return [Integer] characters left to consume
|
|
105
|
+
#
|
|
106
|
+
def chars_left
|
|
107
|
+
@scanner.rest_size
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Counts characters from current position until a target string.
|
|
111
|
+
# Returns chars_left if target is not found.
|
|
112
|
+
#
|
|
113
|
+
# @param target [String] string to search for
|
|
114
|
+
# @return [Integer] count of chars until target or remaining chars
|
|
115
|
+
#
|
|
116
|
+
def chars_until(target)
|
|
117
|
+
found = @scanner.check_until(Regexp.new(Regexp.escape(target)))
|
|
118
|
+
return chars_left unless found
|
|
119
|
+
|
|
120
|
+
found.size - target.size
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Finds the byte position of the next occurrence of a character.
|
|
124
|
+
# Does not move the scanner position.
|
|
125
|
+
#
|
|
126
|
+
# @param ch [String] character to search for
|
|
127
|
+
# @return [Integer, nil] byte position or nil if not found
|
|
128
|
+
#
|
|
129
|
+
def index_of_char(ch)
|
|
130
|
+
rel_idx = @scanner.rest.index(ch)
|
|
131
|
+
return nil unless rel_idx
|
|
132
|
+
|
|
133
|
+
@scanner.pos + rel_idx
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Returns current byte position in input.
|
|
137
|
+
#
|
|
138
|
+
# @return [Integer] current byte offset
|
|
139
|
+
# @note Encoding-aware: position is in bytes, not characters
|
|
140
|
+
#
|
|
141
|
+
def pos
|
|
142
|
+
@scanner.pos
|
|
143
|
+
end
|
|
144
|
+
alias bytepos pos
|
|
145
|
+
|
|
146
|
+
# Sets the current byte position.
|
|
147
|
+
#
|
|
148
|
+
# @param new_pos [Integer] target byte position
|
|
149
|
+
#
|
|
150
|
+
def bytepos=(new_pos)
|
|
151
|
+
@scanner.pos = new_pos
|
|
152
|
+
rescue RangeError
|
|
153
|
+
# Silently ignore out-of-range positions
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Converts a byte position to line and column numbers.
|
|
157
|
+
#
|
|
158
|
+
# @param offset [Integer, nil] byte position (defaults to current)
|
|
159
|
+
# @return [Array<Integer, Integer>] [line, column] tuple (1-indexed)
|
|
160
|
+
#
|
|
161
|
+
def line_and_column(offset = nil)
|
|
162
|
+
effective = offset || @scanner.pos
|
|
163
|
+
@line_data.line_and_column(effective)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Creates a pooled Position object for error reporting.
|
|
167
|
+
#
|
|
168
|
+
# @param offset [Integer, nil] byte position (defaults to current)
|
|
169
|
+
# @return [Parsanol::Position] pooled position instance
|
|
170
|
+
#
|
|
171
|
+
def position(offset = nil)
|
|
172
|
+
effective = offset || @scanner.pos
|
|
173
|
+
line_and_column(effective)
|
|
174
|
+
|
|
175
|
+
# Character position approximation
|
|
176
|
+
char_pos = @raw_string.byteslice(0, effective).size
|
|
177
|
+
|
|
178
|
+
@position_pool.acquire_with(
|
|
179
|
+
string: @raw_string,
|
|
180
|
+
bytepos: effective,
|
|
181
|
+
charpos: char_pos,
|
|
182
|
+
)
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Parsanol::SourceLocation - Source Location Tracking
|
|
4
|
+
#
|
|
5
|
+
# Track source positions (line, column, offset) through the parsing and
|
|
6
|
+
# transformation pipeline. This is useful for error reporting, IDE integration,
|
|
7
|
+
# and source mapping.
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# # Parse with source tracking
|
|
11
|
+
# result = parser.parse_with_spans("hello world")
|
|
12
|
+
# tree = result.tree
|
|
13
|
+
# spans = result.spans
|
|
14
|
+
#
|
|
15
|
+
# # Access span for a node
|
|
16
|
+
# span = spans[node_id]
|
|
17
|
+
# puts "Matched at line #{span.start.line}, column #{span.start.column}"
|
|
18
|
+
#
|
|
19
|
+
# Requires native extension for full functionality.
|
|
20
|
+
|
|
21
|
+
module Parsanol
|
|
22
|
+
# Represents a position in source code
|
|
23
|
+
class SourcePosition
|
|
24
|
+
attr_reader :offset, :line, :column
|
|
25
|
+
|
|
26
|
+
def initialize(offset:, line:, column:)
|
|
27
|
+
@offset = offset
|
|
28
|
+
@line = line
|
|
29
|
+
@column = column
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def to_s
|
|
33
|
+
"line #{@line}, column #{@column} (offset #{@offset})"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def to_h
|
|
37
|
+
{ offset: @offset, line: @line, column: @column }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def ==(other)
|
|
41
|
+
return false unless other.is_a?(SourcePosition)
|
|
42
|
+
|
|
43
|
+
@offset == other.offset && @line == other.line && @column == other.column
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def eql?(other)
|
|
47
|
+
self == other
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def hash
|
|
51
|
+
[@offset, @line, @column].hash
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Represents a span in source code (from start to end position)
|
|
56
|
+
class SourceSpan
|
|
57
|
+
attr_reader :start, :end
|
|
58
|
+
|
|
59
|
+
def initialize(start_pos:, end_pos:)
|
|
60
|
+
@start = start_pos.is_a?(SourcePosition) ? start_pos : SourcePosition.new(**start_pos)
|
|
61
|
+
@end = end_pos.is_a?(SourcePosition) ? end_pos : SourcePosition.new(**end_pos)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Create a span from offsets (computes line/column from input)
|
|
65
|
+
def self.from_offsets(input, start_offset, end_offset)
|
|
66
|
+
start_pos = compute_position(input, start_offset)
|
|
67
|
+
end_pos = compute_position(input, end_offset)
|
|
68
|
+
new(start_pos: start_pos, end_pos: end_pos)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Merge two spans (returns a new span covering both)
|
|
72
|
+
def merge(other)
|
|
73
|
+
return self if other.nil?
|
|
74
|
+
|
|
75
|
+
SourceSpan.new(
|
|
76
|
+
start_pos: [@start, other.start].min_by(&:offset),
|
|
77
|
+
end_pos: [@end, other.end].max_by(&:offset),
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Check if this span overlaps with another
|
|
82
|
+
def overlaps?(other)
|
|
83
|
+
return false if other.nil?
|
|
84
|
+
|
|
85
|
+
@start.offset < other.end.offset && @end.offset > other.start.offset
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Check if this span is adjacent to another
|
|
89
|
+
def adjacent?(other)
|
|
90
|
+
return false if other.nil?
|
|
91
|
+
|
|
92
|
+
@end.offset == other.start.offset || other.end.offset == @start.offset
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Check if a position is within this span
|
|
96
|
+
def contains?(position)
|
|
97
|
+
offset = position.is_a?(SourcePosition) ? position.offset : position
|
|
98
|
+
offset.between?(@start.offset, @end.offset)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Get the length of the span in bytes
|
|
102
|
+
def length
|
|
103
|
+
@end.offset - @start.offset
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Extract the source text from the input
|
|
107
|
+
def extract(input)
|
|
108
|
+
input.byteslice(@start.offset, length)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def to_s
|
|
112
|
+
"#{@start} - #{@end}"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def to_h
|
|
116
|
+
{ start: @start.to_h, end: @end.to_h }
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def ==(other)
|
|
120
|
+
return false unless other.is_a?(SourceSpan)
|
|
121
|
+
|
|
122
|
+
@start == other.start && @end == other.end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Compute line and column from offset
|
|
126
|
+
def self.compute_position(input, offset)
|
|
127
|
+
line = 1
|
|
128
|
+
column = 1
|
|
129
|
+
current_offset = 0
|
|
130
|
+
|
|
131
|
+
input.each_char do |char|
|
|
132
|
+
break if current_offset >= offset
|
|
133
|
+
|
|
134
|
+
if char == "\n"
|
|
135
|
+
line += 1
|
|
136
|
+
column = 1
|
|
137
|
+
else
|
|
138
|
+
column += 1
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
current_offset += 1
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
SourcePosition.new(offset: offset, line: line, column: column)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Result wrapper for parse_with_spans
|
|
149
|
+
class ParseResultWithSpans
|
|
150
|
+
attr_reader :tree, :spans
|
|
151
|
+
|
|
152
|
+
def initialize(tree:, spans:)
|
|
153
|
+
@tree = tree
|
|
154
|
+
@spans = spans
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Get span for a specific node
|
|
158
|
+
def span_for(node_id)
|
|
159
|
+
@spans[node_id]
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Get all spans that contain a position
|
|
163
|
+
def spans_at(offset)
|
|
164
|
+
@spans.values.select { |span| span.contains?(offset) }
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Parsanol::StreamingParser - Streaming Parser for Large Inputs
|
|
4
|
+
#
|
|
5
|
+
# Parse large inputs in chunks without loading the entire input into memory.
|
|
6
|
+
# Useful for file parsing, network streams, or very large documents.
|
|
7
|
+
#
|
|
8
|
+
# Usage:
|
|
9
|
+
# parser = Parsanol::StreamingParser.new(json_grammar)
|
|
10
|
+
#
|
|
11
|
+
# File.open("large.json") do |f|
|
|
12
|
+
# parser.parse_stream(f) do |partial_result|
|
|
13
|
+
# # Process each complete element as it's parsed
|
|
14
|
+
# process_item(partial_result)
|
|
15
|
+
# end
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# Requires native extension for full functionality.
|
|
19
|
+
|
|
20
|
+
module Parsanol
|
|
21
|
+
class StreamingParser
|
|
22
|
+
# Default chunk size (4KB)
|
|
23
|
+
DEFAULT_CHUNK_SIZE = 4096
|
|
24
|
+
|
|
25
|
+
# Create a new streaming parser
|
|
26
|
+
#
|
|
27
|
+
# @param grammar [Parsanol::Parser, Parsanol::Atoms::Base] Grammar to use
|
|
28
|
+
# @param chunk_size [Integer] Size of chunks to read (default: 4096)
|
|
29
|
+
def initialize(grammar, chunk_size: DEFAULT_CHUNK_SIZE)
|
|
30
|
+
@grammar = grammar
|
|
31
|
+
@chunk_size = chunk_size
|
|
32
|
+
|
|
33
|
+
if Parsanol::Native.available?
|
|
34
|
+
grammar_json = Parsanol::Native.serialize_grammar(grammar.root)
|
|
35
|
+
@native_parser = Parsanol::Native.streaming_parser_new(grammar_json)
|
|
36
|
+
else
|
|
37
|
+
@native_parser = nil
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
@buffer = +""
|
|
41
|
+
@position = 0
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Add a chunk of input
|
|
45
|
+
#
|
|
46
|
+
# @param chunk [String] Input chunk to add
|
|
47
|
+
# @return [Boolean] True if more chunks needed, false if ready for parsing
|
|
48
|
+
def add_chunk(chunk)
|
|
49
|
+
@buffer << chunk
|
|
50
|
+
|
|
51
|
+
if @native_parser
|
|
52
|
+
Parsanol::Native.streaming_parser_add_chunk(@native_parser, chunk)
|
|
53
|
+
else
|
|
54
|
+
# Pure Ruby fallback
|
|
55
|
+
false
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Parse what we have so far
|
|
60
|
+
#
|
|
61
|
+
# @return [Object, nil] Parsed result or nil if need more data
|
|
62
|
+
def parse_chunk
|
|
63
|
+
if @native_parser
|
|
64
|
+
Parsanol::Native.streaming_parser_parse_chunk(@native_parser)
|
|
65
|
+
else
|
|
66
|
+
# Pure Ruby fallback - not supported
|
|
67
|
+
raise NotImplementedError,
|
|
68
|
+
"Streaming parser requires native extension for full functionality."
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Check if we have enough data to make progress
|
|
73
|
+
#
|
|
74
|
+
# @return [Boolean] True if parser can make progress
|
|
75
|
+
def enough_data?
|
|
76
|
+
if @native_parser
|
|
77
|
+
!Parsanol::Native.streaming_parser_parse_chunk(@native_parser).nil?
|
|
78
|
+
else
|
|
79
|
+
false
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Parse entire stream (yields partial results)
|
|
84
|
+
#
|
|
85
|
+
# @param io [IO, StringIO] Input source to read from
|
|
86
|
+
# @param chunk_size [Integer] Size of chunks to read
|
|
87
|
+
# @yield [Object] Each complete element as it's parsed
|
|
88
|
+
# @return [Array] All parsed results
|
|
89
|
+
def parse_stream(io, chunk_size: @chunk_size)
|
|
90
|
+
results = []
|
|
91
|
+
|
|
92
|
+
loop do
|
|
93
|
+
chunk = io.read(chunk_size)
|
|
94
|
+
break if chunk.nil? || chunk.empty?
|
|
95
|
+
|
|
96
|
+
add_chunk(chunk)
|
|
97
|
+
|
|
98
|
+
while (result = parse_chunk)
|
|
99
|
+
results << result
|
|
100
|
+
yield result if block_given?
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
results
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Reset the parser for reuse
|
|
108
|
+
def reset
|
|
109
|
+
@buffer = +""
|
|
110
|
+
@position = 0
|
|
111
|
+
|
|
112
|
+
return unless @native_parser
|
|
113
|
+
|
|
114
|
+
grammar_json = Parsanol::Native.serialize_grammar(@grammar.root)
|
|
115
|
+
@native_parser = Parsanol::Native.streaming_parser_new(grammar_json)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Get the current buffer
|
|
119
|
+
attr_reader :buffer
|
|
120
|
+
|
|
121
|
+
# Get the chunk size
|
|
122
|
+
attr_reader :chunk_size
|
|
123
|
+
end
|
|
124
|
+
end
|