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.
Files changed (100) hide show
  1. checksums.yaml +7 -0
  2. data/HISTORY.txt +98 -0
  3. data/LICENSE +23 -0
  4. data/README.adoc +905 -0
  5. data/Rakefile +141 -0
  6. data/lib/parsanol/3.2/parsanol_native.so +0 -0
  7. data/lib/parsanol/3.3/parsanol_native.so +0 -0
  8. data/lib/parsanol/3.4/parsanol_native.so +0 -0
  9. data/lib/parsanol/4.0/parsanol_native.so +0 -0
  10. data/lib/parsanol/ast_visitor.rb +122 -0
  11. data/lib/parsanol/atoms/alternative.rb +123 -0
  12. data/lib/parsanol/atoms/base.rb +208 -0
  13. data/lib/parsanol/atoms/can_flatten.rb +194 -0
  14. data/lib/parsanol/atoms/capture.rb +38 -0
  15. data/lib/parsanol/atoms/context.rb +357 -0
  16. data/lib/parsanol/atoms/context_optimized.rb +38 -0
  17. data/lib/parsanol/atoms/custom.rb +110 -0
  18. data/lib/parsanol/atoms/cut.rb +66 -0
  19. data/lib/parsanol/atoms/dsl.rb +96 -0
  20. data/lib/parsanol/atoms/dynamic.rb +39 -0
  21. data/lib/parsanol/atoms/entity.rb +75 -0
  22. data/lib/parsanol/atoms/ignored.rb +37 -0
  23. data/lib/parsanol/atoms/infix.rb +167 -0
  24. data/lib/parsanol/atoms/lookahead.rb +85 -0
  25. data/lib/parsanol/atoms/named.rb +74 -0
  26. data/lib/parsanol/atoms/re.rb +83 -0
  27. data/lib/parsanol/atoms/repetition.rb +277 -0
  28. data/lib/parsanol/atoms/scope.rb +35 -0
  29. data/lib/parsanol/atoms/sequence.rb +195 -0
  30. data/lib/parsanol/atoms/str.rb +109 -0
  31. data/lib/parsanol/atoms/visitor.rb +91 -0
  32. data/lib/parsanol/atoms.rb +46 -0
  33. data/lib/parsanol/buffer.rb +133 -0
  34. data/lib/parsanol/builder_callbacks.rb +353 -0
  35. data/lib/parsanol/cause.rb +122 -0
  36. data/lib/parsanol/context.rb +39 -0
  37. data/lib/parsanol/convenience.rb +36 -0
  38. data/lib/parsanol/edit_tracker.rb +111 -0
  39. data/lib/parsanol/error_reporter/contextual.rb +99 -0
  40. data/lib/parsanol/error_reporter/deepest.rb +120 -0
  41. data/lib/parsanol/error_reporter/tree.rb +63 -0
  42. data/lib/parsanol/error_reporter.rb +100 -0
  43. data/lib/parsanol/expression/treetop.rb +154 -0
  44. data/lib/parsanol/expression.rb +106 -0
  45. data/lib/parsanol/fast_mode.rb +187 -0
  46. data/lib/parsanol/first_set.rb +79 -0
  47. data/lib/parsanol/grammar_builder.rb +179 -0
  48. data/lib/parsanol/incremental_parser.rb +182 -0
  49. data/lib/parsanol/interval_tree.rb +226 -0
  50. data/lib/parsanol/lazy_result.rb +179 -0
  51. data/lib/parsanol/mermaid.rb +142 -0
  52. data/lib/parsanol/native/batch_decoder.rb +255 -0
  53. data/lib/parsanol/native/dynamic.rb +238 -0
  54. data/lib/parsanol/native/parser.rb +102 -0
  55. data/lib/parsanol/native/serializer.rb +252 -0
  56. data/lib/parsanol/native/transformer.rb +604 -0
  57. data/lib/parsanol/native/types.rb +29 -0
  58. data/lib/parsanol/native.rb +223 -0
  59. data/lib/parsanol/optimizer.rb +85 -0
  60. data/lib/parsanol/optimizers/choice_optimizer.rb +78 -0
  61. data/lib/parsanol/optimizers/cut_inserter.rb +182 -0
  62. data/lib/parsanol/optimizers/lookahead_optimizer.rb +56 -0
  63. data/lib/parsanol/optimizers/quantifier_optimizer.rb +60 -0
  64. data/lib/parsanol/optimizers/sequence_optimizer.rb +97 -0
  65. data/lib/parsanol/options/zero_copy.rb +127 -0
  66. data/lib/parsanol/options.rb +21 -0
  67. data/lib/parsanol/parallel.rb +128 -0
  68. data/lib/parsanol/parser.rb +242 -0
  69. data/lib/parsanol/parslet.rb +151 -0
  70. data/lib/parsanol/pattern/binding.rb +91 -0
  71. data/lib/parsanol/pattern.rb +162 -0
  72. data/lib/parsanol/pool.rb +219 -0
  73. data/lib/parsanol/pools/array_pool.rb +75 -0
  74. data/lib/parsanol/pools/buffer_pool.rb +182 -0
  75. data/lib/parsanol/pools/position_pool.rb +92 -0
  76. data/lib/parsanol/pools/slice_pool.rb +64 -0
  77. data/lib/parsanol/position.rb +94 -0
  78. data/lib/parsanol/resettable.rb +29 -0
  79. data/lib/parsanol/result.rb +46 -0
  80. data/lib/parsanol/result_builder.rb +208 -0
  81. data/lib/parsanol/result_stream.rb +266 -0
  82. data/lib/parsanol/rig/rspec.rb +71 -0
  83. data/lib/parsanol/rope.rb +81 -0
  84. data/lib/parsanol/scope.rb +104 -0
  85. data/lib/parsanol/slice.rb +160 -0
  86. data/lib/parsanol/source/line_cache.rb +102 -0
  87. data/lib/parsanol/source.rb +185 -0
  88. data/lib/parsanol/source_location.rb +167 -0
  89. data/lib/parsanol/streaming_parser.rb +124 -0
  90. data/lib/parsanol/string_view.rb +198 -0
  91. data/lib/parsanol/transform.rb +226 -0
  92. data/lib/parsanol/version.rb +5 -0
  93. data/lib/parsanol/wasm/README.md +80 -0
  94. data/lib/parsanol/wasm/package.json +51 -0
  95. data/lib/parsanol/wasm/parsanol.js +252 -0
  96. data/lib/parsanol/wasm/parslet.d.ts +129 -0
  97. data/lib/parsanol/wasm_parser.rb +240 -0
  98. data/lib/parsanol.rb +278 -0
  99. data/parsanol.gemspec +67 -0
  100. 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