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,182 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../buffer"
4
+
5
+ module Parsanol
6
+ module Pools
7
+ # Manages fixed-size buffers organized by size class.
8
+ #
9
+ # BufferPool provides efficient buffer allocation by maintaining
10
+ # separate pools for common buffer sizes. This reduces allocation
11
+ # overhead and enables buffer reuse across parses.
12
+ #
13
+ # == Usage
14
+ #
15
+ # pool = BufferPool.new
16
+ # buffer = pool.acquire(size: 8) # Get buffer with capacity >= 8
17
+ # buffer.push("a")
18
+ # pool.release(buffer)
19
+ #
20
+ # == Size Classes
21
+ #
22
+ # Buffers are organized into size classes:
23
+ # - Small: 2, 4, 8 (most common)
24
+ # - Medium: 16, 32 (common)
25
+ # - Large: 64+ (rare, allocated on demand)
26
+ #
27
+ # This matches typical parsing patterns where most arrays are small.
28
+ #
29
+ class BufferPool
30
+ # Standard size classes (power of 2 for efficiency)
31
+ SIZE_CLASSES = [2, 4, 8, 16, 32, 64].freeze
32
+
33
+ # Default pool size per class
34
+ DEFAULT_POOL_SIZE = 100
35
+
36
+ # @return [Hash] Pools by size class
37
+ attr_reader :pools
38
+
39
+ # @return [Hash] Statistics per size class
40
+ attr_reader :stats
41
+
42
+ # Initialize a new BufferPool.
43
+ #
44
+ # @param pool_size [Integer] Number of buffers per size class
45
+ #
46
+ def initialize(pool_size: DEFAULT_POOL_SIZE)
47
+ @pool_size = pool_size
48
+ @pools = {}
49
+ @stats = {}
50
+
51
+ # Create pool for each size class
52
+ SIZE_CLASSES.each do |size|
53
+ @pools[size] = []
54
+ @stats[size] = { created: 0, reused: 0, released: 0, discarded: 0 }
55
+ end
56
+ end
57
+
58
+ # Acquire a buffer with at least the requested capacity.
59
+ #
60
+ # Returns a buffer from the appropriate size class pool.
61
+ # If no buffer available, creates a new one.
62
+ #
63
+ # @param size [Integer] Minimum required capacity
64
+ # @return [Buffer] Buffer with capacity >= size
65
+ #
66
+ def acquire(size:)
67
+ size_class = select_size_class(size)
68
+
69
+ # For non-standard size classes, create buffer on demand
70
+ return Buffer.new(capacity: size_class) unless @pools.key?(size_class)
71
+
72
+ pool = @pools[size_class]
73
+
74
+ if pool.empty?
75
+ @stats[size_class][:created] += 1
76
+ Buffer.new(capacity: size_class)
77
+ else
78
+ @stats[size_class][:reused] += 1
79
+ pool.pop
80
+ end
81
+ end
82
+
83
+ # Release a buffer back to the pool.
84
+ #
85
+ # Clears the buffer and returns it to the appropriate size class pool.
86
+ #
87
+ # @param buffer [Buffer] Buffer to release
88
+ # @return [Boolean] true if returned to pool, false if discarded
89
+ #
90
+ def release(buffer)
91
+ size_class = buffer.capacity
92
+ pool = @pools[size_class]
93
+
94
+ # Discard if pool is full or size not in standard classes
95
+ if !pool || pool.size >= @pool_size
96
+ @stats[size_class][:discarded] += 1 if @stats[size_class]
97
+ return false
98
+ end
99
+
100
+ buffer.clear!
101
+ @stats[size_class][:released] += 1
102
+ pool.push(buffer)
103
+ true
104
+ end
105
+
106
+ # Get statistics for all size classes.
107
+ #
108
+ # @return [Hash] Statistics by size class
109
+ #
110
+ def statistics
111
+ result = {}
112
+ SIZE_CLASSES.each do |size|
113
+ stats = @stats[size]
114
+ total_acquires = stats[:created] + stats[:reused]
115
+ utilization = if total_acquires.zero?
116
+ 0.0
117
+ else
118
+ (stats[:reused].to_f / total_acquires * 100)
119
+ end
120
+
121
+ result[size] = {
122
+ available: @pools[size].size,
123
+ created: stats[:created],
124
+ reused: stats[:reused],
125
+ released: stats[:released],
126
+ discarded: stats[:discarded],
127
+ utilization: utilization.round(2),
128
+ }
129
+ end
130
+ result
131
+ end
132
+
133
+ # Clear all pools.
134
+ #
135
+ # @return [void]
136
+ #
137
+ def clear!
138
+ @pools.each_value(&:clear)
139
+ @stats.each_value do |s|
140
+ s[:created] = s[:reused] = s[:released] = s[:discarded] = 0
141
+ end
142
+ end
143
+
144
+ private
145
+
146
+ # Select appropriate size class for requested size.
147
+ #
148
+ # Returns smallest size class >= requested size.
149
+ #
150
+ # @param size [Integer] Requested size
151
+ # @return [Integer] Size class
152
+ #
153
+ def select_size_class(size)
154
+ i = 0
155
+ n = SIZE_CLASSES.length
156
+ while i < n
157
+ return SIZE_CLASSES[i] if SIZE_CLASSES[i] >= size
158
+
159
+ i += 1
160
+ end
161
+ next_power_of_2(size)
162
+ end
163
+
164
+ # Find next power of 2 greater than or equal to n.
165
+ #
166
+ # @param n [Integer] Input value
167
+ # @return [Integer] Next power of 2
168
+ #
169
+ def next_power_of_2(n)
170
+ return 1 if n <= 0
171
+
172
+ n -= 1
173
+ n |= n >> 1
174
+ n |= n >> 2
175
+ n |= n >> 4
176
+ n |= n >> 8
177
+ n |= n >> 16
178
+ n + 1
179
+ end
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Parsanol
4
+ module Pools
5
+ # Specialized object pool for Position instances.
6
+ #
7
+ # PositionPool extends ObjectPool to provide position-specific behavior,
8
+ # particularly managing the line and column state for reuse.
9
+ #
10
+ # == Usage
11
+ #
12
+ # pool = Parsanol::Pools::PositionPool.new(size: 1000)
13
+ #
14
+ # # Acquire a position with line/column
15
+ # pos = pool.acquire_with(string: "source", bytepos: 42, charpos: 42)
16
+ #
17
+ # # Return to pool (automatically reset)
18
+ # pool.release(pos)
19
+ #
20
+ # == Architecture
21
+ #
22
+ # v3.0.0 uses integer positions during parsing for efficiency.
23
+ # Position objects are only created when:
24
+ # - Generating error messages (need line/column)
25
+ # - Materializing error context
26
+ #
27
+ # By pooling Position objects, we reduce GC pressure at the
28
+ # materialization point without changing the fast integer-based
29
+ # parsing path.
30
+ #
31
+ class PositionPool < Parsanol::ObjectPool
32
+ # Initialize a new PositionPool.
33
+ #
34
+ # @param size [Integer] Maximum number of Position objects to pool
35
+ # @param preallocate [Boolean] Whether to pre-allocate positions
36
+ #
37
+ def initialize(size: 1000, preallocate: false)
38
+ # NOTE: Position requires arguments, so we cannot pre-allocate
39
+ super(Parsanol::Position, size: size, preallocate: false)
40
+ end
41
+
42
+ # Acquire a Position from the pool.
43
+ # Overrides ObjectPool#acquire to handle Position's required arguments.
44
+ #
45
+ # @return [Parsanol::Position] A position instance from pool or newly created
46
+ #
47
+ def acquire
48
+ if @available.empty?
49
+ @stats[:created] += 1
50
+ # Create Position with default values since it requires arguments
51
+ Parsanol::Position.new("", 0, 0)
52
+ else
53
+ @stats[:reused] += 1
54
+ @available.pop
55
+ end
56
+ end
57
+
58
+ # Acquire a Position from the pool and initialize it with values.
59
+ #
60
+ # @param string [String] Source string for position tracking
61
+ # @param bytepos [Integer] Byte position in source
62
+ # @param charpos [Integer, nil] Character position (optional)
63
+ # @return [Parsanol::Position] Initialized position from pool
64
+ #
65
+ def acquire_with(string:, bytepos:, charpos: nil)
66
+ pos = acquire
67
+ pos.reset!(string, bytepos, charpos)
68
+ pos
69
+ end
70
+
71
+ # Return a position to the pool after resetting it.
72
+ #
73
+ # @param pos [Parsanol::Position] The position to return
74
+ # @return [Boolean] true if returned to pool, false if discarded
75
+ #
76
+ def release(pos)
77
+ # Don't pool if we're at capacity - discard instead
78
+ if @available.size >= @size
79
+ @stats[:discarded] += 1
80
+ return false
81
+ end
82
+
83
+ # Reset position state with default values before returning to pool
84
+ pos.reset!("", 0, 0)
85
+
86
+ @stats[:released] += 1
87
+ @available.push(pos)
88
+ true
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Parsanol
4
+ module Pools
5
+ # Specialized object pool for Parsanol::Slice instances.
6
+ #
7
+ # SlicePool extends ObjectPool to provide convenient methods for
8
+ # acquiring and configuring Slice objects. Since Slices are frequently
9
+ # created during parsing, pooling them significantly reduces GC pressure.
10
+ #
11
+ # == Usage
12
+ #
13
+ # pool = Parsanol::Pools::SlicePool.new(size: 1000)
14
+ #
15
+ # # Acquire and initialize in one step
16
+ # slice = pool.acquire_with(0, "hello", line_cache)
17
+ #
18
+ # # Use the slice...
19
+ #
20
+ # # Return to pool
21
+ # pool.release(slice)
22
+ #
23
+ # == Why Pool Slices?
24
+ #
25
+ # Profiling (Session 19) showed that Slice allocation contributes
26
+ # significantly to GC overhead. By reusing Slice objects, we can:
27
+ # - Reduce object allocations by 70-80%
28
+ # - Decrease GC time from 67% to ~20%
29
+ # - Improve overall parsing throughput by 2-3x
30
+ #
31
+ class SlicePool < Parsanol::ObjectPool
32
+ # Initialize a new SlicePool.
33
+ #
34
+ # @param size [Integer] Maximum number of Slice objects to pool (default: 1000)
35
+ # @param preallocate [Boolean] Whether to pre-allocate slices (default: true)
36
+ #
37
+ # @example Create a SlicePool
38
+ # pool = SlicePool.new(size: 2000)
39
+ #
40
+ def initialize(size: 1000, preallocate: true)
41
+ super(Parsanol::Slice, size: size, preallocate: preallocate)
42
+ end
43
+
44
+ # Acquire a Slice from the pool and initialize it with given values.
45
+ #
46
+ # This is a convenience method that combines acquire + reset! into
47
+ # a single operation, making it easier to work with pooled slices.
48
+ #
49
+ # @param bytepos [Integer] Byte position in the original input
50
+ # @param str [String] The slice content
51
+ # @param line_cache [Object] Optional line cache for line/column info
52
+ # @return [Parsanol::Slice] An initialized slice ready for use
53
+ #
54
+ # @example Acquire and initialize
55
+ # slice = pool.acquire_with(0, "hello", line_cache)
56
+ #
57
+ def acquire_with(bytepos, str, line_cache = nil)
58
+ slice = acquire
59
+ slice.reset!(bytepos, str, line_cache)
60
+ slice
61
+ end
62
+ end
63
+ end
64
+ end
@@ -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, **)
31
+ case type
32
+ when :repetition
33
+ RepetitionBuilder.new(context, **)
34
+ when :sequence
35
+ SequenceBuilder.new(context, **)
36
+ when :hash
37
+ HashBuilder.new(context, **)
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