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,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
|